STM32智能小车入门实战:从硬件选型到PWM电机控制
1. 项目概述:从零到一,打造你的第一辆STM32智能小车
如果你手头正好有一块STM32开发板,几个电机和轮子,想动手做点有意思的东西,那么一辆能听你指挥前进、后退、左转、右转、停止的智能小车,绝对是个完美的入门项目。这不仅仅是让轮子转起来那么简单,它融合了单片机GPIO控制、PWM调速、电机驱动、电源管理以及最基础的软件逻辑设计,是嵌入式开发从理论走向实践的绝佳跳板。我当年就是从这样一辆“傻跑”的小车开始,一步步深入到更复杂的传感器融合和自动控制领域的。
这个项目的核心目标非常明确:通过STM32单片机,实现对两个直流电机的独立控制,从而组合出小车的五种基本运动状态。听起来简单,但里面藏着不少新手容易踩的坑,比如电机选型、驱动电路烧毁、PWM频率设置不当导致电机啸叫、电源干扰导致单片机复位等等。接下来,我会结合我多次搭建小车的经验,把这套流程掰开揉碎了讲清楚,让你不仅能做出能跑的小车,更能理解每一个环节背后的“为什么”。
2. 核心硬件选型与电路设计解析
一辆小车跑得稳不稳,硬件是地基。盲目堆料不可取,根据需求和预算做精准选型才是正道。
2.1 主控芯片:STM32家族如何选择
STM32系列型号繁多,对于基础的小车控制,我们不需要顶级的性能。F1系列的C8T6、F4系列的F411CEU6都是性价比极高的选择。它们都拥有足够多的GPIO口和定时器来产生PWM信号。
注意:我个人更倾向于推荐STM32F103C8T6(俗称“蓝桥杯”或“最小系统板”)。原因有三:一是资料极其丰富,任何问题几乎都能找到答案;二是价格便宜,烧了不心疼;三是性能对于本项目绰绰有余,其拥有的高级定时器(TIM1/8)和通用定时器(TIM2/3/4)完全满足电机PWM和编码器测速(未来升级用)的需求。
2.2 动力心脏:电机与驱动模块选型
这是决定小车动力和操控性的关键。
- 电机类型:最常用的是TT减速直流电机。它集成了直流电机和减速齿轮箱,输出扭矩大,转速适中(通常空载转速在200RPM左右),价格低廉。注意查看工作电压(常用3-6V)和空载电流。
- 驱动模块:单片机GPIO口的驱动电流(通常20mA)远不足以直接驱动电机,必须使用电机驱动模块。最经典的是L298N和TB6612FNG。
- L298N:老牌芯片,皮实耐造,驱动电流大(单桥2A),但发热严重,效率较低,需要外接散热片。其控制逻辑简单,适合新手理解H桥原理。
- TB6612FNG:我更推荐这个。它是MOSFET桥驱动,效率高、发热小,驱动能力(单桥1.2A)对于小型TT电机完全足够。关键它支持待机模式,功耗更低,逻辑控制也更简洁。
这里给出一个对比表格,方便你决策:
| 特性 | L298N驱动模块 | TB6612FNG驱动模块 | 选择建议 |
|---|---|---|---|
| 驱动方式 | 双H桥集成电路 | MOSFET双桥驱动 | - |
| 驱动电流 | 单桥2A(峰值3A) | 单桥1.2A(峰值3.2A) | 小型TT电机两者皆可 |
| 效率 | 较低,压降大,发热大 | 高,压降小,发热小 | TB6612效率优势明显 |
| 控制逻辑 | 每路需2个IO控制方向,1个PWM调速 | 每路需2个IO控制方向,1个PWM调速 | 类似 |
| 待机功能 | 无 | 有(STBY引脚) | TB6612可降低静态功耗 |
| 价格 | 略低 | 略高 | 相差不大 |
实操心得:对于新手的第一辆车,如果预算允许,直接上TB6612。它能让你省去很多散热和电源波动的烦恼,把精力更多集中在编程和逻辑上。我早期用L298N时,就曾因散热不良导致芯片过热保护,小车跑着跑着就停了,排查了半天。
2.3 电源系统设计:稳定大于一切
小车跑起来一窜一窜的,或者单片机莫名复位,八成是电源问题。
- 双电源供电:强烈建议电机驱动电源和单片机逻辑电源分开。电机启停时会产生很大的电流波动和电压毛刺,如果共用电源,会严重干扰单片机工作。通常方案是:
- 一块7.4V或12V的锂电池组(如18650两串)作为电机驱动电源,直接接入驱动模块的VM(电机电源)输入端。
- 驱动模块上的5V输出(或独立的5V降压模块,如LM2596)作为单片机和其它逻辑器件(如后续的传感器)的电源。TB6612的VCC引脚(逻辑供电)也接这个5V。
- 滤波电容:在电机驱动模块的电源输入端口附近,并联一个100uF以上的电解电容和一个0.1uF的瓷片电容。这是吸收低频和高频干扰的经典做法,成本极低但效果显著。
- 电压监测:为后续升级考虑,可以用STM32的ADC监测电池电压,避免电池过放。
3. 硬件连接与原理图详解
我们以STM32F103C8T6搭配TB6612FNG驱动模块和两个TT减速电机为例,进行连接。
3.1 控制逻辑剖析:H桥与PWM
要想理解接线,必须先明白驱动芯片如何控制一个电机的正反转和调速。
TB6612控制一路电机需要三个信号:
- AIN1/AIN2(或BIN1/BIN2):这两个是方向控制引脚,决定电流方向。
- PWMA/PWMB:这是PWM调速引脚,输入不同占空比的方波,来控制电机的平均电压,从而实现调速。
其真值表如下(以A路为例):
| STBY | AIN1 | AIN2 | PWMA | 电机A状态 |
|---|---|---|---|---|
| 1 | 0 | 0 | X | 停止(刹车) |
| 1 | 1 | 0 | PWM | 正转(速度由PWM占空比决定) |
| 1 | 0 | 1 | PWM | 反转(速度由PWM占空比决定) |
| 1 | 1 | 1 | X | 停止(刹车) |
| 0 | X | X | X | 待机(所有输出关断) |
核心原理:AIN1和AIN2的电平组合,控制了芯片内部H桥四个MOSFET的导通状态,从而改变了电机两端的电压极性。PWM信号则快速开关对应的MOSFET,通过改变一个周期内导通时间的比例(占空比)来改变电机两端的平均电压,实现调速。
3.2 具体接线步骤
请务必在断电状态下操作!
- 电源部分:
- 将锂电池正负极接至TB6612的
VM和GND。 - 将TB6612的
VCC(逻辑电压)和GND接至STM32的5V和GND。确保共地! - 将TB6612的
STBY引脚接高电平(可直接接VCC或STM32的某个置高的IO口)。
- 将锂电池正负极接至TB6612的
- 信号控制部分:我们使用STM32的TIM3的CH1和CH2产生两路PWM,用普通GPIO控制方向。
- 电机A(右侧电机):
AIN1-> STM32PA6(配置为普通GPIO输出)AIN2-> STM32PA7(配置为普通GPIO输出)PWMA-> STM32PA0(配置为TIM2_CH1 或 TIM5_CH1, 这里我们假设用PA0作为TIM2_CH1)
- 电机B(左侧电机):
BIN1-> STM32PB0(配置为普通GPIO输出)BIN2-> STM32PB1(配置为普通GPIO输出)PWMB-> STM32PA1(配置为TIM2_CH2)
- 电机A(右侧电机):
- 电机输出部分:
- 将电机A的两根线接至TB6612的
AO1和AO2。 - 将电机B的两根线接至TB6612的
BO1和BO2。如果电机转向与预期相反,只需对调这两根线即可。
- 将电机A的两根线接至TB6612的
重要提示:PWM引脚必须连接到STM32上具有定时器输出比较(OC)功能的引脚上。你需要查阅STM32F103C8T6的数据手册或引脚分配图来确定。上面举例的PA0/PA1是TIM2的通道1和2,是一种常见选择。
4. 软件编程:STM32 HAL库实现五态控制
我们使用STM32CubeMX进行初始化配置,然后用HAL库编写应用逻辑。这是目前最主流、最高效的开发方式。
4.1 使用STM32CubeMX进行工程初始化
- 创建项目:选择MCU型号STM32F103C8Tx。
- 配置时钟:在
RCC中,将HSE(外部高速时钟)设置为Crystal/Ceramic Resonator。然后在Clock Configuration标签页,将系统时钟源选为HSE,并适当倍频至72MHz(F103的常用最高频率)。 - 配置GPIO:
- 将
PA6,PA7,PB0,PB1设置为GPIO_Output,并给它们起个有意义的用户标签,如MOTOR_A_IN1,MOTOR_A_IN2等。
- 将
- 配置定时器产生PWM:
- 找到
TIM2(或其他你选择的定时器)。 - 将
Channel1和Channel2设置为PWM Generation CH1/CH2。 - 在下方参数设置中:
Prescaler(预分频器):设置为72-1。因为系统时钟是72MHz,分频后得到1MHz的计数器时钟。计算:72MHz / (71+1) = 1MHz。Counter Period(自动重装载值ARR):设置为1000-1。这样,PWM的频率 = 1MHz / 1000 =1kHz。这是一个对直流电机调速非常合适的频率,既能保证响应平滑,又能避免MOSFET开关损耗过大和产生人耳可闻的啸叫声。Pulse(初始脉冲宽度):先设为0。
- 找到
- 生成代码:设置好工程路径和工具链(MDK-ARM V5),点击
GENERATE CODE。
4.2 编写电机驱动与控制函数
在生成的工程中,我们主要在main.c或自己新建的motor.c文件中编写代码。
首先,定义一些宏和变量,让代码更清晰:
// motor.h 或 main.c 顶部 #define MOTOR_A_PWM &htim2, TIM_CHANNEL_1 #define MOTOR_B_PWM &htim2, TIM_CHANNEL_2 #define MOTOR_A_IN1_GPIO_Port GPIOA #define MOTOR_A_IN1_Pin GPIO_PIN_6 #define MOTOR_A_IN2_GPIO_Port GPIOA #define MOTOR_A_IN2_Pin GPIO_PIN_7 #define MOTOR_B_IN1_GPIO_Port GPIOB #define MOTOR_B_IN1_Pin GPIO_PIN_0 #define MOTOR_B_IN2_GPIO_Port GPIOB #define MOTOR_B_IN2_Pin GPIO_PIN_1 // 电机速度范围,对应PWM占空比 0~1000 #define MOTOR_MAX_SPEED 800 // 为避免全速可能导致的电流过大,设个上限 #define MOTOR_MIN_SPEED 200 // 电机有启动电压阈值,低于这个值可能转不起来然后,编写一个基础的电机控制函数:
// motor.c /** * @brief 设置单个电机的速度和方向 * @param pwm_handle: 定时器和通道句柄,如 MOTOR_A_PWM * @param in1_pin: 方向引脚1 * @param in2_pin: 方向引脚2 * @param speed: 速度值,正数正转,负数反转,绝对值大小代表速度 * @retval None */ void Motor_SetSpeed(TIM_HandleTypeDef *htim, uint32_t Channel, GPIO_TypeDef* IN1_Port, uint16_t IN1_Pin, GPIO_TypeDef* IN2_Port, uint16_t IN2_Pin, int16_t speed) { // 限制速度范围 if(speed > MOTOR_MAX_SPEED) speed = MOTOR_MAX_SPEED; if(speed < -MOTOR_MAX_SPEED) speed = -MOTOR_MAX_SPEED; // 根据速度正负设置方向 if(speed > 0) { // 正转 HAL_GPIO_WritePin(IN1_Port, IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(IN2_Port, IN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(htim, Channel, speed); // 设置PWM占空比 } else if(speed < 0) { // 反转 HAL_GPIO_WritePin(IN1_Port, IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(IN2_Port, IN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim, Channel, -speed); // 取绝对值 } else { // 停止(刹车模式,两个方向引脚同电平) HAL_GPIO_WritePin(IN1_Port, IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(IN2_Port, IN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(htim, Channel, 0); // 另一种停止方式是让两个方向引脚都为低,同时PWM为0(滑行停止)。刹车停止更迅速。 } }4.3 实现小车五态运动函数
基于上面的单电机控制函数,我们可以封装出小车整体的动作:
// car.c /** * @brief 小车前进 * @param speed: 前进速度 * @retval None */ void Car_Forward(int16_t speed) { // 两个电机都正转 Motor_SetSpeed(MOTOR_A_PWM, MOTOR_A_IN1_GPIO_Port, MOTOR_A_IN1_Pin, MOTOR_A_IN2_GPIO_Port, MOTOR_A_IN2_Pin, speed); Motor_SetSpeed(MOTOR_B_PWM, MOTOR_B_IN1_GPIO_Port, MOTOR_B_IN1_Pin, MOTOR_B_IN2_GPIO_Port, MOTOR_B_IN2_Pin, speed); } /** * @brief 小车后退 * @param speed: 后退速度 * @retval None */ void Car_Backward(int16_t speed) { // 两个电机都反转 Motor_SetSpeed(MOTOR_A_PWM, MOTOR_A_IN1_GPIO_Port, MOTOR_A_IN1_Pin, MOTOR_A_IN2_GPIO_Port, MOTOR_A_IN2_Pin, -speed); Motor_SetSpeed(MOTOR_B_PWM, MOTOR_B_IN1_GPIO_Port, MOTOR_B_IN1_Pin, MOTOR_B_IN2_GPIO_Port, MOTOR_B_IN2_Pin, -speed); } /** * @brief 小车原地左转 * @param speed: 转向速度(电机速度) * @retval None */ void Car_SpinLeft(int16_t speed) { // 左轮反转,右轮正转,实现原地旋转 Motor_SetSpeed(MOTOR_A_PWM, MOTOR_A_IN1_GPIO_Port, MOTOR_A_IN1_Pin, MOTOR_A_IN2_GPIO_Port, MOTOR_A_IN2_Pin, speed); // 右轮正转 Motor_SetSpeed(MOTOR_B_PWM, MOTOR_B_IN1_GPIO_Port, MOTOR_B_IN1_Pin, MOTOR_B_IN2_GPIO_Port, MOTOR_B_IN2_Pin, -speed); // 左轮反转 } /** * @brief 小车原地右转 * @param speed: 转向速度(电机速度) * @retval None */ void Car_SpinRight(int16_t speed) { // 左轮正转,右轮反转,实现原地旋转 Motor_SetSpeed(MOTOR_A_PWM, MOTOR_A_IN1_GPIO_Port, MOTOR_A_IN1_Pin, MOTOR_A_IN2_GPIO_Port, MOTOR_A_IN2_Pin, -speed); // 右轮反转 Motor_SetSpeed(MOTOR_B_PWM, MOTOR_B_IN1_GPIO_Port, MOTOR_B_IN1_Pin, MOTOR_B_IN2_GPIO_Port, MOTOR_B_IN2_Pin, speed); // 左轮正转 } /** * @brief 小车停止 * @retval None */ void Car_Stop(void) { Motor_SetSpeed(MOTOR_A_PWM, MOTOR_A_IN1_GPIO_Port, MOTOR_A_IN1_Pin, MOTOR_A_IN2_GPIO_Port, MOTOR_A_IN2_Pin, 0); Motor_SetSpeed(MOTOR_B_PWM, MOTOR_B_IN1_GPIO_Port, MOTOR_B_IN1_Pin, MOTOR_B_IN2_GPIO_Port, MOTOR_B_IN2_Pin, 0); }4.4 主函数逻辑与测试
在main.c的main函数中,初始化外设后,我们可以编写一个简单的测试序列:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); // PWM定时器初始化 // ... 其他初始化 // 启动PWM信号输出 HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); while (1) { Car_Forward(600); // 以60%的速度前进 HAL_Delay(2000); // 持续2秒 Car_SpinRight(400); // 右转 HAL_Delay(1000); Car_Backward(500); // 后退 HAL_Delay(2000); Car_SpinLeft(400); // 左转 HAL_Delay(1000); Car_Stop(); // 停止 HAL_Delay(2000); } }5. 调试、问题排查与进阶优化
代码烧录后,小车可能不会按预期运动。别急,这是调试的开始。
5.1 分步调试法
- 静态测试(不上电):用万用表通断档,检查所有接线是否牢固,有无虚焊、短路。重点检查电源正负极是否接反。
- 上电无动作测试:
- 先不接电机,给系统上电。
- 用万用表电压档测量驱动模块输出端(AO1/AO2)电压。当设置电机正转时,AO1应为高电平(接近VM电压),AO2应为低电平。反转时相反。停止时两者电压关系应符合真值表。这可以验证STM32的GPIO控制逻辑是否正确。
- 用示波器或逻辑分析仪(甚至一个简单的LED加电阻串联)查看PWM引脚波形,确认频率(1kHz)和占空比是否随代码改变而变化。
- 单电机测试:接上一个电机,分别测试正转、反转、停止功能是否正常。注意观察电机转向。
- 双电机联调:两个电机都接上,运行测试程序,观察小车运动。
5.2 常见问题与解决方案速查表
| 现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 电机完全不转 | 1. 电源未接通或电压不足。 2. 驱动模块使能端(STBY)未置高。 3. STM32未正常启动或程序未运行。 4. 控制IO口配置错误(如配置成了输入)。 | 1. 测量电池电压、驱动模块VM和VCC电压。 2. 检查STBY引脚是否接高电平(5V或3.3V)。 3. 检查单片机复位电路,LED闪烁测试程序是否运行。 4. 用CubeMX重新检查GPIO配置模式。 |
| 电机只朝一个方向转 | 方向控制引脚(AIN1/AIN2)电平设置错误或其中一个IO损坏。 | 用万用表或逻辑分析仪测量AIN1/AIN2引脚电平,在代码中正反转切换时,电平是否按真值表变化。 |
| 电机抖动、转速不稳或啸叫 | 1. PWM频率不合适(太低会抖动,太高可能超出驱动芯片响应能力且效率低)。 2. 电源功率不足或干扰大。 3. 电机机械负载卡顿。 | 1.调整PWM频率。尝试500Hz, 1kHz, 5kHz, 16kHz。对于TB6612,1k-5kHz是比较好的范围。避开人耳敏感频段(~1-3kHz可减少啸叫)。 2. 加强电源滤波(加大电容),检查电池电量是否充足。 3. 检查车轮、齿轮是否安装顺畅。 |
| 小车跑不直 | 两个电机的实际转速有差异(即使PWM占空比相同)。这是由于电机个体差异、轮子摩擦不同导致的。 | 1.软件校准:在代码中为两个电机设置不同的速度补偿值。例如,如果小车右偏,就稍微调低右轮的速度系数。 2.闭环控制(进阶):为电机加装编码器,使用PID算法进行精确速度闭环控制。 |
| 单片机频繁复位 | 电机启停时的大电流拉低了逻辑电源电压。 | 1.严格进行电源分离,电机电源和单片机电源从电池处就分开,通过不同的稳压模块供电。 2. 在单片机的电源入口处增加一个大电容(如100uF)缓冲。 3. 检查地线连接是否良好、够粗。 |
5.3 进阶优化方向
当基础的五态控制实现后,你可以考虑以下升级,让你的小车变得更“智能”:
- 加入遥控:最直接的是通过蓝牙模块(如HC-05/06)连接手机APP遥控,或者用红外遥控器、2.4G射频模块(如NRF24L01)实现无线控制。这需要你学习串口通信或SPI通信。
- 加入传感器:
- 超声波模块(HC-SR04):实现避障功能。当检测到前方障碍物时,自动停止或转向。
- 红外巡线模块(TCRT5000):实现巡线行驶。这是很多智能小车比赛的基础项目。
- 陀螺仪加速度计(MPU6050):获取小车姿态,可以实现更稳定的直线行驶或特定角度的精确转弯。
- 速度闭环控制:给TT电机加装霍尔编码器(很多TT电机有自带编码器的版本),通过STM32的定时器编码器接口读取转速,然后使用PID算法调节PWM占空比,让小车在不同负载、不同电量下都能保持设定速度,彻底解决“跑不直”的问题。
- 结构化你的代码:将电机驱动、小车控制、传感器处理等模块分别写成独立的
.c/.h文件,使用头文件进行接口声明。这会让你的代码更易维护和扩展,为后续添加复杂功能打下基础。
从让轮子转起来,到能按指令灵活运动,再到能感知环境、自主决策,这辆STM32智能小车就像一个微缩的机器人平台,每一步的深入都能带你学习到嵌入式开发的一个核心知识点。硬件连接是筋骨,软件逻辑是灵魂,而调试排错则是让你真正理解系统如何运作的必修课。希望这份详细的指南能帮你少走弯路,顺利启动你的嵌入式实战之旅。当你看到自己亲手打造的小车在地面上划出第一道轨迹时,那种成就感就是学习技术最好的动力。