STM32智能小车入门实战:从硬件选型到PWM电机控制

📅 2026/7/30 9:32:43 👁️ 阅读次数 📝 编程学习
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 动力心脏:电机与驱动模块选型

这是决定小车动力和操控性的关键。

  1. 电机类型:最常用的是TT减速直流电机。它集成了直流电机和减速齿轮箱,输出扭矩大,转速适中(通常空载转速在200RPM左右),价格低廉。注意查看工作电压(常用3-6V)和空载电流。
  2. 驱动模块:单片机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 电源系统设计:稳定大于一切

小车跑起来一窜一窜的,或者单片机莫名复位,八成是电源问题。

  1. 双电源供电强烈建议电机驱动电源和单片机逻辑电源分开。电机启停时会产生很大的电流波动和电压毛刺,如果共用电源,会严重干扰单片机工作。通常方案是:
    • 一块7.4V或12V的锂电池组(如18650两串)作为电机驱动电源,直接接入驱动模块的VM(电机电源)输入端。
    • 驱动模块上的5V输出(或独立的5V降压模块,如LM2596)作为单片机和其它逻辑器件(如后续的传感器)的电源。TB6612的VCC引脚(逻辑供电)也接这个5V。
  2. 滤波电容:在电机驱动模块的电源输入端口附近,并联一个100uF以上的电解电容和一个0.1uF的瓷片电容。这是吸收低频和高频干扰的经典做法,成本极低但效果显著。
  3. 电压监测:为后续升级考虑,可以用STM32的ADC监测电池电压,避免电池过放。

3. 硬件连接与原理图详解

我们以STM32F103C8T6搭配TB6612FNG驱动模块两个TT减速电机为例,进行连接。

3.1 控制逻辑剖析:H桥与PWM

要想理解接线,必须先明白驱动芯片如何控制一个电机的正反转和调速。

TB6612控制一路电机需要三个信号:

  • AIN1/AIN2(或BIN1/BIN2):这两个是方向控制引脚,决定电流方向。
  • PWMA/PWMB:这是PWM调速引脚,输入不同占空比的方波,来控制电机的平均电压,从而实现调速。

其真值表如下(以A路为例):

STBYAIN1AIN2PWMA电机A状态
100X停止(刹车)
110PWM正转(速度由PWM占空比决定)
101PWM反转(速度由PWM占空比决定)
111X停止(刹车)
0XXX待机(所有输出关断)

核心原理:AIN1和AIN2的电平组合,控制了芯片内部H桥四个MOSFET的导通状态,从而改变了电机两端的电压极性。PWM信号则快速开关对应的MOSFET,通过改变一个周期内导通时间的比例(占空比)来改变电机两端的平均电压,实现调速。

3.2 具体接线步骤

请务必在断电状态下操作!

  1. 电源部分
    • 将锂电池正负极接至TB6612的VMGND
    • 将TB6612的VCC(逻辑电压)和GND接至STM32的5VGND确保共地!
    • 将TB6612的STBY引脚接高电平(可直接接VCC或STM32的某个置高的IO口)。
  2. 信号控制部分:我们使用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)
  3. 电机输出部分
    • 将电机A的两根线接至TB6612的AO1AO2
    • 将电机B的两根线接至TB6612的BO1BO2。如果电机转向与预期相反,只需对调这两根线即可。

重要提示:PWM引脚必须连接到STM32上具有定时器输出比较(OC)功能的引脚上。你需要查阅STM32F103C8T6的数据手册或引脚分配图来确定。上面举例的PA0/PA1是TIM2的通道1和2,是一种常见选择。

4. 软件编程:STM32 HAL库实现五态控制

我们使用STM32CubeMX进行初始化配置,然后用HAL库编写应用逻辑。这是目前最主流、最高效的开发方式。

4.1 使用STM32CubeMX进行工程初始化

  1. 创建项目:选择MCU型号STM32F103C8Tx。
  2. 配置时钟:在RCC中,将HSE(外部高速时钟)设置为Crystal/Ceramic Resonator。然后在Clock Configuration标签页,将系统时钟源选为HSE,并适当倍频至72MHz(F103的常用最高频率)。
  3. 配置GPIO
    • PA6,PA7,PB0,PB1设置为GPIO_Output,并给它们起个有意义的用户标签,如MOTOR_A_IN1,MOTOR_A_IN2等。
  4. 配置定时器产生PWM
    • 找到TIM2(或其他你选择的定时器)。
    • Channel1Channel2设置为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。
  5. 生成代码:设置好工程路径和工具链(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.cmain函数中,初始化外设后,我们可以编写一个简单的测试序列:

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 分步调试法

  1. 静态测试(不上电):用万用表通断档,检查所有接线是否牢固,有无虚焊、短路。重点检查电源正负极是否接反。
  2. 上电无动作测试
    • 先不接电机,给系统上电。
    • 用万用表电压档测量驱动模块输出端(AO1/AO2)电压。当设置电机正转时,AO1应为高电平(接近VM电压),AO2应为低电平。反转时相反。停止时两者电压关系应符合真值表。这可以验证STM32的GPIO控制逻辑是否正确。
    • 用示波器或逻辑分析仪(甚至一个简单的LED加电阻串联)查看PWM引脚波形,确认频率(1kHz)和占空比是否随代码改变而变化。
  3. 单电机测试:接上一个电机,分别测试正转、反转、停止功能是否正常。注意观察电机转向。
  4. 双电机联调:两个电机都接上,运行测试程序,观察小车运动。

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 进阶优化方向

当基础的五态控制实现后,你可以考虑以下升级,让你的小车变得更“智能”:

  1. 加入遥控:最直接的是通过蓝牙模块(如HC-05/06)连接手机APP遥控,或者用红外遥控器、2.4G射频模块(如NRF24L01)实现无线控制。这需要你学习串口通信或SPI通信。
  2. 加入传感器
    • 超声波模块(HC-SR04):实现避障功能。当检测到前方障碍物时,自动停止或转向。
    • 红外巡线模块(TCRT5000):实现巡线行驶。这是很多智能小车比赛的基础项目。
    • 陀螺仪加速度计(MPU6050):获取小车姿态,可以实现更稳定的直线行驶或特定角度的精确转弯。
  3. 速度闭环控制:给TT电机加装霍尔编码器(很多TT电机有自带编码器的版本),通过STM32的定时器编码器接口读取转速,然后使用PID算法调节PWM占空比,让小车在不同负载、不同电量下都能保持设定速度,彻底解决“跑不直”的问题。
  4. 结构化你的代码:将电机驱动、小车控制、传感器处理等模块分别写成独立的.c/.h文件,使用头文件进行接口声明。这会让你的代码更易维护和扩展,为后续添加复杂功能打下基础。

从让轮子转起来,到能按指令灵活运动,再到能感知环境、自主决策,这辆STM32智能小车就像一个微缩的机器人平台,每一步的深入都能带你学习到嵌入式开发的一个核心知识点。硬件连接是筋骨,软件逻辑是灵魂,而调试排错则是让你真正理解系统如何运作的必修课。希望这份详细的指南能帮你少走弯路,顺利启动你的嵌入式实战之旅。当你看到自己亲手打造的小车在地面上划出第一道轨迹时,那种成就感就是学习技术最好的动力。