STM32笔记专栏      智能小车专栏

🎯 适用平台:STM32F103C8T6(STM32F10x系列通用)
🎯 开发环境:Keil5 + 标准库 V3.5.0
🎯 核心外设:TIM4(CH1~CH4)+ GPIOB(Pin6~Pin9)


一、前言

大家好!从今天开始我们就来讲解关于stm32的智能小车,这篇文章就来聊聊怎么用STM32的TIM4四通道PWM来控制智能小车的基本运动

如果你做过小车,一定遇到过这个问题:给电机直接通高电平,电机"嗡"的一下全速转,小车直接冲出去,根本没法控制快慢。而PWM(脉冲宽度调制)就是解决这个问题的关键——通过调节占空比,让电机获得不同的平均电压,从而实现调速

STM32F103C8T6的TIM4有4个独立的PWM通道(CH1~CH4),正好对应小车的四个轮子(左前、左后、右前、右后),简直是给小车量身定做的!

📌 本文要解决的问题
① TIM4四通道PWM怎么初始化?ARR和PSC怎么算?
② 四个通道怎么分别控制四个电机?
③ 前进、后退、左转、右转、原地旋转的代码逻辑怎么写?
④ 实际接线中有哪些坑要注意?


二、硬件接线与原理图说明

在讲代码之前,先搞清楚引脚对应关系,不然后面代码看懂了,线接错了小车照样不动。

功能TIM通道GPIO引脚对应电机
左前轮PWMTIM4_CH1PB6左前轮
左后轮PWMTIM4_CH2PB7左后轮
右前轮PWMTIM4_CH3PB8右前轮
右后轮PWMTIM4_CH4PB9右后轮

我的理解:TIM4挂在APB1总线上,最大时钟是72MHz,所以PSC和ARR的计算都要基于72MHz来。PB6~PB9这四个引脚默认是普通GPIO,必须配置成复用推挽输出(AF_PP),否则TIM4的PWM信号走不到引脚上,电机驱动板收不到信号。


三、知识点讲解

3.1 PWM频率的计算:20kHz是怎么来的?

PWM_Init()中,我们看到这样两行:

TIM_TimeBaseStructure.TIM_Period = 100 - 1;     // ARR = 99
TIM_TimeBaseStructure.TIM_Prescaler = 36 - 1;   // PSC = 35

PWM频率的计算公式是:

fPWM​=fCLK​​/(PSC+1)/(ARR+1)​=20kHz

为什么选20kHz? 因为人耳能听到的声音范围大概是20Hz~20kHz,如果PWM频率太低(比如1kHz),电机会发出刺耳的"吱吱"声,这就是电磁噪音。把频率拉到20kHz以上,就超出了人耳听力范围,电机运行起来就"静音"了。

⚠️有些电机驱动板(比如L298N)的开关频率不能太高,如果超过驱动芯片的响应速度,PWM波形会失真。建议先用示波器或逻辑分析仪看一下PB6~PB9输出的波形是否规整。


3.2 TIM4四通道PWM初始化详解(PWM.c)

下面是完整的PWM.c代码,每一行我都加了详细解析,建议对照着看:

#include "stm32f10x.h"                  // 包含STM32F10x标准外设库头文件,里面定义了所有寄存器和库函数

/**
 * @brief  TIM4四通道PWM初始化函数
 * @note   配置TIM4的CH1~CH4输出PWM,对应GPIOB_Pin6~Pin9
 *         PWM频率 = 72MHz / (36) / (100) = 20kHz
 */
void PWM_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;            // 定义GPIO初始化结构体,用于配置PB6~PB9
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;  // 定义定时器时基初始化结构体,配置ARR、PSC等
    TIM_OCInitTypeDef TIM_OCInitStructure;            // 定义定时器输出比较初始化结构体,配置PWM模式

    /* ========== 第一步:开启时钟 ========== */
    // 使能TIM4定时器时钟,TIM4挂在APB1总线上,必须用RCC_APB1PeriphClockCmd
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);
    // 使能GPIOB端口时钟,PWM输出引脚PB6~PB9属于GPIOB,用APB2总线时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    /* ========== 第二步:配置GPIO为复用推挽输出 ========== */
    // 同时选中PB6、PB7、PB8、PB9四个引脚,用按位或"|"连接
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9;
    // 配置为复用推挽输出模式(AF_PP),让TIM4的PWM信号能从引脚输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    // GPIO输出速度设为50MHz,PWM是高频信号,速度不能太低
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    // 调用GPIO_Init函数,把上面的配置写入GPIOB的寄存器
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* ========== 第三步:配置定时器时基参数 ========== */
    // 设置自动重装载值ARR = 99,计数器从0数到99,一共100个计数周期
    TIM_TimeBaseStructure.TIM_Period = 100 - 1;
    // 设置预分频系数PSC = 35,72MHz / 36 = 2MHz,定时器每0.5us计数一次
    TIM_TimeBaseStructure.TIM_Prescaler = 36 - 1;
    // 时钟分频因子设为0,表示不再对定时器时钟进行二次分频
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    // 选择向上计数模式,计数器从0开始递增,数到ARR后溢出归零
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    // 调用TIM_TimeBaseInit,把时基参数写入TIM4的寄存器
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);

    /* ========== 第四步:配置TIM4_CH1的PWM输出比较模式 ========== */
    // 选择PWM模式1:向上计数时,CNT < CCR输出有效电平(高电平),CNT >= CCR输出无效电平(低电平)
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    // 使能比较输出,把PWM信号真正输出到对应的GPIO引脚上
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    // 设置初始比较值CCR = 0,初始占空比 = CCR/(ARR+1) = 0/100 = 0%,电机初始不转
    TIM_OCInitStructure.TIM_Pulse = 0;
    // 设置输出极性为高电平有效,即CNT < CCR时输出高电平
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    // 初始化TIM4的通道1(CH1),对应PB6,控制左前轮
    TIM_OC1Init(TIM4, &TIM_OCInitStructure);

    /* ========== 第五步:配置TIM4_CH2的PWM输出比较模式 ========== */
    // 同样配置为PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    // 使能通道2的比较输出
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    // 初始占空比设为0
    TIM_OCInitStructure.TIM_Pulse = 0;
    // 高电平有效
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    // 初始化TIM4的通道2(CH2),对应PB7,控制左后轮
    TIM_OC2Init(TIM4, &TIM_OCInitStructure);

    /* ========== 第六步:配置TIM4_CH3的PWM输出比较模式 ========== */
    // PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    // 比较输出使能
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    // 初始占空比0
    TIM_OCInitStructure.TIM_Pulse = 0;
    // 高电平有效
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    // 初始化TIM4的通道3(CH3),对应PB8,控制右前轮
    TIM_OC3Init(TIM4, &TIM_OCInitStructure);

    /* ========== 第七步:配置TIM4_CH4的PWM输出比较模式 ========== */
    // PWM模式1
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
    // 比较输出使能
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
    // 初始占空比0
    TIM_OCInitStructure.TIM_Pulse = 0;
    // 高电平有效
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
    // 初始化TIM4的通道4(CH4),对应PB9,控制右后轮
    TIM_OC4Init(TIM4, &TIM_OCInitStructure);

    /* ========== 第八步:使能预装载寄存器(重要!) ========== */
    // 使能TIM4_CH1的CCR预装载,修改CCR值时不会立即生效,而是等下一个更新事件(溢出)时才生效
    // 这样可以避免PWM占空比在计数中途跳变,防止电机抖动
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);
    // 使能TIM4_CH2的CCR预装载
    TIM_OC2PreloadConfig(TIM4, TIM_OCPreload_Enable);
    // 使能TIM4_CH3的CCR预装载
    TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);
    // 使能TIM4_CH4的CCR预装载
    TIM_OC4PreloadConfig(TIM4, TIM_OCPreload_Enable);
    // 使能ARR自动重装载寄存器预装载,修改ARR时也是下一个周期生效
    TIM_ARRPreloadConfig(TIM4, ENABLE);

    /* ========== 第九步:启动定时器 ========== */
    // 使能TIM4定时器计数器,CNT开始从0向上计数,PWM波形正式输出
    TIM_Cmd(TIM4, ENABLE);
}

我的理解:这里最关键的一步是预装载寄存器的使能。如果没有TIM_OCxPreloadConfig,你调用TIM_SetComparex改占空比的时候,CCR会立即生效,如果此时计数器刚好数到一半,PWM波形就会"截断"一半,电机就会抖一下。开了预装载之后,新的CCR值会先存进预装载寄存器,等计数器溢出(更新事件)时才会真正生效,这样占空比切换就是"无缝"的。


3.3 小车运动控制代码详解(ROBOT.c)

PWM初始化好了,引脚也能输出波形了,接下来要解决的是四个轮子怎么配合才能让小车动起来

#include "stm32f10x.h"          // 包含STM32F10x标准外设库头文件
#include "PWM.h"                // 包含PWM初始化头文件,声明PWM_Init函数
#include "Delay.h"            // 包含延时函数头文件,用于运动持续时间控制

/**
 * @brief  机器人初始化函数
 * @note   调用PWM_Init完成TIM4四通道PWM的初始化,为后续电机驱动做准备
 */
void robot_Init(void)
{
    // 调用PWM初始化函数,配置TIM4的CH1~CH4输出PWM,四路PWM全部就绪
    PWM_Init();
}

/**
 * @brief  设置四路PWM占空比,直接控制四个电机的转速
 * @param  left1_speed  : 左前轮PWM占空比(对应TIM4_CH1 / PB6)
 * @param  left2_speed  : 左后轮PWM占空比(对应TIM4_CH2 / PB7)
 * @param  right1_speed : 右前轮PWM占空比(对应TIM4_CH3 / PB8)
 * @param  right2_speed : 右后轮PWM占空比(对应TIM4_CH4 / PB9)
 * @note   占空比范围0~100,与之前PWM_Init中ARR=99对应
 *         CCR值直接填0~100,占空比 = CCR / 100 × 100%
 */
void robot_speed(uint8_t left1_speed, uint8_t left2_speed, uint8_t right1_speed, uint8_t right2_speed)
{
    // 设置TIM4通道1的比较值CCR,控制左前轮转速,占空比 = left1_speed %
    TIM_SetCompare1(TIM4, left1_speed);
    // 设置TIM4通道2的比较值CCR,控制左后轮转速
    TIM_SetCompare2(TIM4, left2_speed);
    // 设置TIM4通道3的比较值CCR,控制右前轮转速
    TIM_SetCompare3(TIM4, right1_speed);
    // 设置TIM4通道4的比较值CCR,控制右后轮转速
    TIM_SetCompare4(TIM4, right2_speed);
}

我的理解robot_speed这个函数是小车运动的"原子操作",所有运动函数最终都是调它。这里有个细节——占空比和CCR值的关系。因为ARR=99,所以计数周期是100,CCR填50就是50%占空比。但注意TIM_SetComparex的参数范围是0~65535(16位),虽然ARR只有99,但库函数本身不会限制你传入的值,如果传了150,CCR=150 > ARR=99,那PWM就会一直输出高电平(100%占空比)。所以上层函数一定要做好限幅保护


3.3.1 前进与刹车
/**
 * @brief  机器人前进函数
 * @param  speed : 前进速度,范围0~100
 * @param  time  : 前进持续时间,单位毫秒(ms)
 * @note   左轮正转(speed),右轮正转(speed),小车直线前进
 *         这里假设电机驱动方式:speed给PWM=正转,0=停止
 */
void makerobo_run(int8_t speed, uint16_t time)
{
    // 判断传入的速度是否超过上限100,防止占空比计算越界
    if (speed > 100)
    {
        // 超出上限则强制限制为100,保护电机和驱动板
        speed = 100;
    }
    // 判断速度是否小于下限0,负数占空比没有意义
    if (speed < 0)
    {
        // 负数速度置为0,防止异常输出
        speed = 0;
    }
    // 设置四路PWM:左前轮speed,左后轮0,右前轮speed,右后轮0
    // 这里根据实际接线,左右各只有一个主驱动轮在转(或两轮都转,取决于你的机械结构)
    robot_speed(speed, 0, speed, 0);
    // 延时指定时间,保持当前运动状态,小车持续前进
    Delay_ms(time);
    // 四路PWM全部置0,电机停止转动,小车停下
    robot_speed(0, 0, 0, 0);
}

/**
 * @brief  机器人刹车函数(急停)
 * @param  time : 刹车保持时间,单位毫秒(ms)
 * @note   直接将四路PWM全部置0,所有电机停止转动
 */
void makerobo_brake(uint16_t time)
{
    // 四路PWM全部输出0,所有电机立即停止,实现"急停"效果
    robot_speed(0, 0, 0, 0);
    // 延时指定时间,保持停止状态
    Delay_ms(time);
}

我的理解makerobo_run里的速度限幅非常重要!如果没有if (speed > 100)if (speed < 0)这两道防线,万一主函数里传了个150或者-20进去,TIM_SetComparex会直接写150到CCR寄存器。虽然ARR=99,CCR=150也不会损坏芯片,但PWM会恒为高电平,电机全速运转不受控,小车可能直接冲出去撞墙。

⚠️ 这里speed用的是int8_t(有符号8位),范围是-128~127。如果用户传了-128,经过if (speed < 0) speed = 0;处理后会变成0,不会出错。但如果以后改成uint8_t,负数传入时会发生隐式类型转换,-1会变成255,那就会出大问题!所以参数类型选择要慎重


3.3.2 差速转向:原地左转与原地右转
/**
 * @brief  机器人左转函数(原地左转,以右轮为轴)
 * @param  speed : 转弯速度,范围0~100
 * @param  time  : 转弯持续时间,单位毫秒(ms)
 * @note   左轮停止(0),右轮正转(speed),小车以左侧为轴向右前方转弯
 *         实际效果取决于电机安装方向和驱动板接线
 */
void makerobo_Left(int8_t speed, uint16_t time)
{
    // 速度上限保护,超过100则限制为100
    if (speed > 100)
    {
        speed = 100;
    }
    // 速度下限保护,负数则置0
    if (speed < 0)
    {
        speed = 0;
    }
    // 左轮全部停止(0),右前轮以speed转动,右后轮停止(0)
    // 右轮转动、左轮静止,小车绕左侧轴向左转弯
    robot_speed(0, 0, speed, 0);
    // 延时保持转弯动作
    Delay_ms(time);
    // 停止所有电机,结束转弯
    robot_speed(0, 0, 0, 0);
}

/**
 * @brief  机器人原地左转函数(差速左转,左轮后退右轮前进)
 * @param  speed : 旋转速度,范围0~100
 * @param  time  : 旋转持续时间,单位毫秒(ms)
 * @note   左轮反转(speed),右轮正转(speed),两侧轮子反向转动
 *         小车以中心为轴原地逆时针旋转(从上方看)
 */
void makerobo_Spin_Left(int8_t speed, uint16_t time)
{
    // 速度上限保护
    if (speed > 100)
    {
        speed = 100;
    }
    // 速度下限保护
    if (speed < 0)
    {
        speed = 0;
    }
    // 左后轮speed(对应反转通道),右前轮speed(对应正转通道)
    // 左右轮反向转动,产生原地旋转力矩,小车绕中心逆时针旋转
    robot_speed(0, speed, speed, 0);
    // 延时保持旋转
    Delay_ms(time);
    // 停止所有电机
    robot_speed(0, 0, 0, 0);
}

/**
 * @brief  机器人右转函数(原地右转,以左轮为轴)
 * @param  speed : 转弯速度,范围0~100
 * @param  time  : 转弯持续时间,单位毫秒(ms)
 * @note   左轮正转(speed),右轮停止(0),小车以右侧为轴向左前方转弯
 */
void makerobo_Right(int8_t speed, uint16_t time)
{
    // 速度上限保护
    if (speed > 100)
    {
        speed = 100;
    }
    // 速度下限保护
    if (speed < 0)
    {
        speed = 0;
    }
    // 左前轮以speed转动,其余三轮停止
    // 左轮转动、右轮静止,小车绕右侧轴向右转弯
    robot_speed(speed, 0, 0, 0);
    // 延时保持转弯
    Delay_ms(time);
    // 停止所有电机
    robot_speed(0, 0, 0, 0);
}

/**
 * @brief  机器人原地右转函数(差速右转,左轮前进右轮后退)
 * @param  speed : 旋转速度,范围0~100
 * @param  time  : 旋转持续时间,单位毫秒(ms)
 * @note   左轮正转(speed),右轮反转(speed),两侧轮子反向转动
 *         小车以中心为轴原地顺时针旋转(从上方看)
 */
void makerobo_Spin_Right(int8_t speed, uint16_t time)
{
    // 速度上限保护
    if (speed > 100)
    {
        speed = 100;
    }
    // 速度下限保护
    if (speed < 0)
    {
        speed = 0;
    }
    // 左前轮speed(正转),右后轮speed(反转)
    // 左右轮反向,产生原地旋转力矩,小车绕中心顺时针旋转
    robot_speed(speed, 0, 0, speed);
    // 延时保持旋转
    Delay_ms(time);
    // 停止所有电机
    robot_speed(0, 0, 0, 0);
}

我的理解:这里要注意一个关键问题——"正转"和"反转"只是PWM占空比,真正决定轮子往哪个方向转的是电机驱动板的接线! 如果你的电机线接反了,给PWM本来想让轮子正转,结果它反着转,那makerobo_run就会变成后退,makerobo_Left会变成右转。所以第一次调试时,一定要先单独测试每个轮子,确认哪个PWM通道对应哪个轮子,以及PWM占空比增加时轮子是正转还是反转。


3.3.3 后退控制
/**
 * @brief  机器人后退函数
 * @param  speed : 后退速度,范围0~100
 * @param  time  : 后退持续时间,单位毫秒(ms)
 * @note   左轮反转(speed),右轮反转(speed),小车直线后退
 */
void makerobo_back(int8_t speed, uint16_t time)
{
    // 速度上限保护
    if (speed > 100)
    {
        speed = 100;
    }
    // 速度下限保护
    if (speed < 0)
    {
        speed = 0;
    }
    // 左后轮speed,右后轮speed
    // 这里对应轮子的"反转"通道,通过电机驱动板的IN1/IN2逻辑实现后退
    robot_speed(0, speed, 0, speed);
    // 延时保持后退
    Delay_ms(time);
    // 停止所有电机
    robot_speed(0, 0, 0, 0);
}

我的理解makerobo_back的实现很有意思——它并没有用"负占空比"这种概念,而是通过选择不同的电机驱动通道来实现方向控制。这说明在电机驱动板那一层,已经用两个GPIO(或驱动芯片的IN1/IN2)控制了电机的正反转,而PWM只是控制转速。STM32这边的PWM始终输出正占空比,方向由驱动板的另一路逻辑决定。这种设计很清晰,把"速度控制"和"方向控制"解耦了。

完整工程代码可见:基于STM32的智能小车: 基于STM32F103C8T6开发的智能小车代码教程


四、个人理解 / 心得

  1. 关于ARR=99的选择:我一开始想ARR设成999,这样占空比分辨率就是0.1%,调速更精细。但后来发现STM32的TIM_SetComparex传的是uint16_t,ARR=999也没问题。不过考虑到小车电机本身精度不高,0~100级调速完全够用,而且ARR=99时PSC=35,计算起来特别整,就采用了这个配置。

  2. 四个轮子不一定都要转:从代码里能看到,makerobo_run只驱动了左前轮和右前轮(robot_speed(speed, 0, speed, 0)),左后轮和右后轮是0。这是因为我的小车是两轮差速驱动,后面两个轮子是万向轮(从动轮),不需要电机。如果你的小车是四轮驱动,把0改成speed就行,代码结构不用大改。

  3. Delay_ms的阻塞问题:现在的运动函数都是阻塞式的——执行Delay_ms(time)时,CPU干不了别的事。如果后面要加红外循迹、超声波避障,这种阻塞方式就不行了,得改成非阻塞的状态机或者用定时器中断。不过作为基础运动控制的入门代码,阻塞式最简单直观,适合先理解原理。


五、踩坑记录

踩坑现象原因分析解决方法
电机不转,PWM引脚用万用表量有电压但电机没反应GPIO配置成了GPIO_Mode_Out_PP(普通推挽),而不是GPIO_Mode_AF_PP(复用推挽)改成GPIO_Mode_AF_PP,让TIM4的PWM信号能输出到引脚
小车应该前进,结果原地打转左右电机接线反了,或者左右轮子的PWM通道对应错了单独测试每个通道,用robot_speed(50,0,0,0)逐个确认哪个轮子在转
修改占空比后电机抖动明显没有使能CCR预装载寄存器,占空比在中途跳变加上TIM_OCxPreloadConfig(TIM4, TIM_OCPreload_Enable)
makerobo_back执行后小车反而前进电机驱动板的DIR方向控制线接反了交换电机两根线,或者在代码里调换对应的PWM通道

六、总结表格

项目说明
定时器TIM4(APB1总线,72MHz)
PWM通道CH1~CH4,对应PB6~PB9
PWM频率20kHz(PSC=35,ARR=99)
占空比范围0~100(CCR值直接对应百分比)
GPIO模式复用推挽输出(AF_PP)
关键配置必须使能CCR预装载和ARR预装载,防止PWM跳变
速度保护每个运动函数都带0~100限幅,防止越界
适用驱动板L298N、TB6612、DRV8833等支持PWM调速的电机驱动
验证方式①示波器/逻辑分析仪看PB6~PB9波形;②万用表测平均电压;③实际小车运行测试

七、验证方式与测试建议

  1. 第一步:静态测试
    下载程序后,用示波器或逻辑分析仪接PB6,应该能看到20kHz的PWM波形。初始占空比为0%,波形应该是一条低电平直线。

  2. 第二步:单轮测试
    main函数里调用robot_speed(50, 0, 0, 0),看左前轮是否以约50%占空比转动。逐个测试四个轮子,确认接线无误。

  1. 第三步:运动测试
    依次调用makerobo_runmakerobo_backmakerobo_Left等函数,观察小车运动方向是否与预期一致。如果方向反了,调整电机线或修改代码中的通道对应关系。

  2. 第四步:边界测试
    传入speed=150和speed=-10,验证限幅保护是否生效,电机不应出现异常高速或反转。

写这篇文章的时候,我把江科大的视频又回顾了一遍,特别是TIM定时器输出比较那一章。说实话,PWM这部分内容虽然代码量不大,但ARR、PSC、CCR、预装载寄存器这几个概念第一次接触的时候确实容易搞混。

如果你也在做智能小车,建议不要一次性把所有功能都堆上去,先把PWM调通、让轮子能转,再一步步加上运动控制。基础打扎实了,后面加红外循迹、超声波避障、蓝牙遥控都会轻松很多。

如果这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐