拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32F407智能小车实战:电机控制、PWM调速、红外遥控与PID闭环

简介本资源是一套基于STM32F407微控制器的智能小车嵌入式控制程序面向嵌入式初学者与智能硬件开发者解决电机驱动、红外遥控解析与多方向运动控制等典型实践问题。压缩包共180个文件含32个C源文件实现主控逻辑、电机PWM驱动、红外指令解码、33个头文件外设配置与函数声明、33个编译中间文件.o/.d/.crf及工程配置文件uvproj、sct、axf、hex等完整覆盖从初始化、定时器PWM生成、GPIO控制到红外通信闭环的全流程代码结构包体大小为3.7MB。已有382人学习下载。读者可直接导入Keil uVision或STM32CubeIDE运行调试获得可实操的前进/后退/转向功能模块、L298N/TB6612FNG驱动适配代码、红外NEC协议解析逻辑以及基于HAL库的标准化外设封装特别适合用于课程设计、电子竞赛备赛或自主机器人项目原型开发。 做小车做了三年多从最早的循迹避障一路做到带遥控、带闭环调速中间换过好几块主控。这次整理的是一个基于STM32F407的平台小车程序核心功能涵盖电机控制、红外遥控也就是项目名里那个irctrol、PWM调速以及后续扩展出来的PID闭环。如果你正准备用F407做一辆能跑、能遥控、速度可控的小车或者手里已经有一堆模块但不知道怎么整合成一套完整的程序这篇内容应该能帮你少走不少弯路。先把话说在前面网上关于STM32F407小车的代码很多但大部分是零散例程要么是正点原子风格的外设demo要么是某个模块的单独测试。真正能直接拼成一台完整小车的其实不多。我这套程序最开始的目的就是“把F407的电机控制、红外解码、编码器测速、PID闭环这几大块用一套代码串起来”同时尽量把每一块的原理讲明白这样你拿到手之后既能直接跑也知道该怎么改。1. 硬件选型为什么是STM32F4071.1 芯片选型背后的考量先说主控。做小车的主控方案其实非常多51、Arduino、ESP32、STM32F103几乎都有人用。那为什么我最终选了STM32F407核心原因就三个主频够高、定时器够多、有余量扩展。F407的内核是Cortex-M4带FPU主频168MHz相比F103的72MHz在主频上翻了一倍多。对于小车这种应用单说PWM和GPIO控制F103也确实够用但一旦涉及到编码器正交解码、多个定时器同步输出PWM、红外解码时要精确测量微秒级时间差F103的定时器资源就会显得捉襟见肘。F407有14个定时器其中高级定时器TIM1/TIM8可以输出互补PWM和刹车控制通用定时器TIM2到TIM5是32位的做高频测速计数非常方便。这意味着你可以在不牺牲PWM通道的情况下预留出足够的定时器给编码器反馈。另外一点是F407带了DSP指令集和FPU。虽然小车控制用不上复杂的浮点运算但如果你后面想做卡尔曼滤波、做循迹图像处理、做声控识别F407的算力余量会让你舒服很多。我见过不少人在F103上跑PID还要用定点数优化其实没必要这么憋屈自己。F407直接上浮点PID算起来毫无压力。那为什么不直接上F429或者H7说实话F407和F429在核心算力上差别不大F429多了TFT-LCD控制器和SDRAM接口但这两个东西对小车来说都是“锦上添花”。H7系列性能更强但开发成本、PCB设计难度、甚至芯片价格都上去了对于一台学习型小车来说属于杀鸡用牛刀。F407正好卡在“够用且好用”的平衡点上这也是为什么现在市面上很多教学板和开源小车项目都选它。1.2 电机驱动模块的选择电机驱动方案我试过好几种包括L293D、TB6612、分立MOS管搭H桥还接过现成的DRV8833模块。这里直接给大家对比一下驱动方案最大持续电流导通压降发热情况外围复杂度适用场景L293D600mA较大约1.2V~2V很烫低电流极小的玩具电机TB6612FNG1.2A较小约0.5V正常低多数N20减速电机、TT马达分立MOS H桥如LR7843SI23025A以上极小需要散热设计高大功率直流电机、无人机云台电机DRV88331.5A较小正常低双路电机驱动、锂电池供电对于标准小车项目我推荐TB6612。原因很简单它内部集成了两个H桥不需要你额外搭MOS管电路一个模块就能驱动两路电机体积小、发热低逻辑电压兼容3.3V可以直接接F407的GPIO。L293D的压降太大电机端实际电压比电源低一截转速上不去而且很容易烫到手。TB6612在2.5V到13.5V供电范围内都能工作基本覆盖了常见的2S锂电池供电场景。如果你要做大功率电机那就得考虑分立MOS H桥。这里顺便说一个很多人第一次做H桥时容易踩的坑N沟道MOS管做下桥臂很方便因为栅源电压容易满足但上桥臂如果也用N沟道就需要自举电路把栅极电压抬高否则MOS管没法完全导通。很多人在H桥上桥用P沟道、下桥用N沟道来避免自举但P沟道要注意导通条件栅极电压必须比源极低足够多比如IRF4905需要Vgs在-4V到-10V之间才能充分导通而且P沟道MOS内阻通常比同等级的N沟道大大电流场景发热会比较明显。要完全避免这些麻烦用专用的半桥驱动芯片如IR2104配合N沟道MOS是最省心的路线。2. 电机控制核心PWM与GPIO的配合2.1 定时器PWM的基本配置F407定时器资源非常多我用了TIM4生成两路PWM控制左电机和右电机你也可以用TIM1/TIM8这种高级定时器。只要能出PWM的定时器其实都行关键不是用哪个定时器而是PWM的频率选多少。小车电机PWM频率一般推荐10kHz到20kHz。为什么是这个范围两个原因。第一这个频段超出了人耳可闻范围20Hz到20kHz不会听到电机发出刺耳的啸叫声。第二对大多数电机驱动芯片来说10k到20k的开关频率在功耗和纹波之间是个平衡点——频率太低电流纹波大电机转速抖动明显还会发出“哒哒”声频率太高MOS管的开关损耗上升驱动芯片发热增大。用标准外设库配置PWM比较简单。以TIM4的CH1输出到PA6为例void TIM4_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM4); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; TIM_TimeBaseStructure.TIM_Prescaler psc; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM4, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM4, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM4, ENABLE); TIM_Cmd(TIM4, ENABLE); }如果PWM频率选10kHz系统时钟168MHz分频系数设为168-1那么arr 168000000 / 168 / 10000 - 1 99。这样得到的就是10kHz、0到100级占空比可调的PWM。如果你想让分辨率更高可以把arr设大一点PWM频率会相应降低4000Hz左右时占空比可以做到0到4200级。实测下来电机对占空比的分辨率敏感度其实没那么高100级足够平滑了。2.2 方向控制与刹车逻辑PWM只负责“调速度”方向控制要靠另外两个GPIO。以TB6612为例它的AIN1和AIN2引脚决定A路电机的方向AIN1AIN2电机状态00短路刹车01反转10正转11停止滑行这个逻辑表一定要记清楚。刹车和滑行是两种完全不同的状态——AIN1和AIN2同为高电平时电机两个端子都被拉到高电平电机相当于断电可以自由转动也就是滑行同为低电平时两个端子都接地相当于短路电机立刻被刹住。在代码里我习惯把每个电机的控制封装成一个结构体包含方向引脚和PWM通道typedef struct { GPIO_TypeDef *in1_port; uint16_t in1_pin; GPIO_TypeDef *in2_port; uint16_t in2_pin; TIM_TypeDef *tim; uint8_t channel; } Motor_t; void Motor_SetSpeed(Motor_t *motor, int speed) { if (speed 100) speed 100; if (speed -100) speed -100; if (speed 0) { Motor_SetDirection(motor, MOTOR_FORWARD); TIM_SetCompare(motor-tim, motor-channel, speed); } else { Motor_SetDirection(motor, MOTOR_BACKWARD); TIM_SetCompare(motor-tim, motor-channel, -speed); } }Motor_SetDirection本质就是拉高拉低两个GPIO。这里有一个很重要的细节初始化时一定要先把方向和PWM输出设置成安全状态再使能电机驱动模块。如果上电瞬间TB6612的输入引脚悬空芯片内部逻辑可能会误判成任一状态导致电机突然转一下。我踩过这个坑后来所有项目都严格要求先拉低方向引脚再把PWM占空比清零最后才把模块的STBY引脚拉高使能。在写基础驱动层的时候还有一个容易被忽略的点PWM输出极性。如果用TIM_OCPolarity_High占空比越大电机转得越快但如果用错了极性占空比越大反而越慢而且初始状态下占空比为0电机是全速运转的。这个在调试初期最迷惑人一旦发现电机不受控地狂转先检查PWM极性。3. 红外遥控最省钱也最折腾的无线方案3.1 NEC协议解码原理项目名里的irctrol我猜大概率是 IR Control 的拼写变体也就是红外遥控。为什么选红外因为它便宜、稳定、代码量适中非常适合作为小车的无线控制方案。相比蓝牙模块动不动就要二十多块一个红外接收头加一个遥控器十块钱以内搞定而且红外不存在配对问题上电就能用。红外遥控最常用的是NEC协议核心参数就几个载波频率38kHz引导码9ms高电平 4.5ms低电平逻辑0560us高电平 560us低电平逻辑1560us高电平 1.69ms低电平重复码9ms高电平 2.25ms低电平解码的关键就是测量高电平和低电平的时间宽度。有两种做法一是用定时器输入捕获二是用外部中断加微秒级时间戳。我采用的是第二种因为F407的SysTick可以轻松提供微秒级计时外部中断配置也简单。解码的核心思想是状态机。先用引导码触发状态复位然后连续测量32位数据8位地址码、8位地址反码、8位命令码、8位命令反码每一位通过高电平之后低电平的长度来判断是0还是1。这里要注意NEC协议的位信息其实在低电平长度上高电平宽度永远是560us。当然实际上因为发射管和接收头的响应延迟高电平不会严格等于560us所以判断时只需要测低电平宽度即可高电平作为“读取点”的触发信号。我用标准库实现的简化版解码逻辑大概长这样uint8_t IR_RX_State 0; // 解码状态 uint32_t IR_RX_Data 0; // 数据暂存 uint8_t IR_RX_BitCount 0; uint64_t IR_RX_LastTime 0; void IR_RX_ExtiHandler(void) { uint32_t now SysTick_GetMicros(); uint32_t width now - IR_RX_LastTime; IR_RX_LastTime now; if (width 12000) { // 超时当作新一轮引导码 IR_RX_State 0; IR_RX_BitCount 0; IR_RX_Data 0; return; } switch (IR_RX_State) { case 0: // 等待引导码 if (width 8000 width 11000) { IR_RX_State 1; IR_RX_BitCount 0; IR_RX_Data 0; } break; case 1: // 等待引导码后的低电平结束 if (width 3500 width 5500) { IR_RX_State 2; } else if (width 2000 width 2500) { // 重复码直接忽略或在主循环处理 IR_RX_State 0; } break; case 2: // 读取数据位 if (width 1000 width 1800) { IR_RX_Data (IR_RX_Data 1) | 1; // 逻辑1 } else if (width 400 width 900) { IR_RX_Data (IR_RX_Data 1) | 0; // 逻辑0 } else { IR_RX_State 0; // 非法宽度重新等待 return; } IR_RX_BitCount; if (IR_RX_BitCount 32) { IR_RX_State 0; IR_RX_CompleteFlag 1; } break; } }这个解码里有几个细节值得注意。第一判断阈值必须加容差范围因为红外接收头的实际输出宽度和理论值有偏差。第二每次中断都要读取当前时间并计算宽度所以外部中断引脚必须独立不能跟其他功能共用。第三如果解码失败率很高建议在中断里加上GPIO状态判断只有下降沿才触发中断——通常红外接收头空闲时输出高电平收到信号时输出脉冲所以用下降沿触发最合适。有一点要特别提一下实测下来有些遥控器在按键按住不放时会周期性发送重复码。如果你在小车主循环里发现“按键一直有效”就是因为没有处理重复码。所以收到引导码之后要判断一下是完整数据码还是重复码重复码一般用于音量连续调节但对于小车控制我建议忽略重复码这样只有新按键才会触发动作避免长按导致小车反复执行同一个操作。3.2 按键映射与小车控制策略解码成功之后下一步就是把遥控器按键映射成小车动作。我用的遥控器是一个二十键的NEC遥控器键值表大概是按键键值小车动作CH-0x45停车CH0x46切换速度档位CH0x47切换速度档位左箭头0x44左转右箭头0x40右转上箭头0x43前进下箭头0x15后退0~90x16~0x0D直接设速度百分比这个映射我写在了一个结构体数组里主循环检测到IR_RX_CompleteFlag之后就在数组里查表执行对应动作。要注意的是不同品牌遥控器键值可能不一样最好的方式是把遥控器每个键的实际解码值打印到串口建立自己遥控器的键值表再拿去映射动作。我见过很多人直接拿网上的键值表套自己的遥控器结果按键对不上还以为是解码程序写错了。小车的转向控制也要考虑速度差。最简单的做法是左转时左电机停转、右电机全速但这样转得过猛小车容易原地打转。更柔和的做法是“差速转向”左电机设为一个低速值右电机保持当前速度这样转弯半径大一些手感更好。我给前进和后退设计了一个基础速度然后左右转时在基础速度上加减一个偏置实测效果非常顺滑。4. PID闭环让小车走直线不靠玄学4.1 为什么需要PID先问一个问题开环PWM调速够不够用如果小车只是在平地上跑几秒钟开环确实能用。但一旦电池电量从8.4V掉到7.2V同样的PWM占空比电机转速会下降非常多。你会发现刚充完电的小车跑得飞快玩十分钟之后明显变慢了。还有更隐蔽的问题左右两个电机的特性不可能完全一致即使给一样的PWM两个轮子的转速也有差异小车会往一边偏。开环状态调整方向只能靠手动补偿非常麻烦。PID闭环的思路就是用编码器测出当前实际转速跟目标转速比较把误差交给PID控制器输出一个修正后的PWM值。这样不管电池电压怎么波动、负载怎么变化实际转速总能贴合目标值。数字PID的离散化公式是u(k) Kp * e(k) Ki * sum(e(j)) Kd * (e(k) - e(k-1))这里e(k)是当前转速误差sum(e(j))是误差累积e(k)-e(k-1)是误差变化率。对于嵌入式舵机控制、电机调速这类场景我推荐用增量式PID。它输出的是PWM修正量而不是绝对PWM值delta_u(k) Kp * (e(k) - e(k-1)) Ki * e(k) Kd * (e(k) - 2*e(k-1) e(k-2))增量式的好处有几个第一它不保存积分累积值不会出现积分饱和导致输出卡死的问题第二输出的修正量天然带限幅只要把delta_u限制在±100以内就不会出现PWM瞬间跳变第三断电重启后不需要恢复历史状态因为输出一定是从当前值附近开始的。4.2 编码器测速基础PID要有传感器反馈对电机调速来说就是编码器。我用的是带霍尔编码器的N20减速电机这种电机本体转速高、扭矩小通过齿轮箱减速后输出到轮子编码器测的是电机轴减速前的转速。N20电机的霍尔编码器常见规格是11线/转也有13线的意思就是电机轴每转一圈A相或B相输出11个脉冲。减速比常见有1:30、1:48、1:90等。比如减速比1:30的电机轮子转一圈编码器会输出11×30330个脉冲。如果对AB两相做4倍频利用A和B的上升沿和下降沿每个完整周期计4个数那么轮子转一圈能测到1320个计数。F407的定时器支持编码器模式根本不需要你手动去判断相位。以TIM2为例配置成编码器模式后它会自动根据A/B相的超前滞后关系加计数或减计数非常省心TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_SetCounter(TIM2, 0); // 在周期定时中断里读取 int16_t cnt TIM_GetCounter(TIM2); TIM_SetCounter(TIM2, 0);读取频率我用的是10ms一次。因为F407的TIM2是32位定时器理论上不会溢出但为了计算转速方便还是建议周期性读取并清零否则计数值累积起来没有意义。换算成转速的公式rpm (cnt / 4倍频系数 / 编码器线数 / 减速比) * (1000ms / 10ms) * 60举个例子10ms内读到132个计数编码器11线4倍频后每圈1320个计数那么每10ms是0.1圈即每秒10圈也就是600rpm。4.3 PID实现与整定PID代码本身不难难的是参数整定。先贴核心实现typedef struct { float Kp; float Ki; float Kd; float integral; float last_error; float pre_last_error; float output_limit; } PID_t; float PID_Calculate(PID_t *pid, float target, float current) { float error target - current; float p_out pid-Kp * (error - pid-last_error); float i_out pid-Ki * error; float d_out pid-Kd * (error - 2 * pid-last_error pid-pre_last_error); float delta p_out i_out d_out; if (delta pid-output_limit) delta pid-output_limit; if (delta -pid-output_limit) delta -pid-output_limit; pid-pre_last_error pid-last_error; pid-last_error error; return delta; }整定步骤我总结成一个口诀先P后I再DP给基础D防抖I补静差防饱和。第一步把Ki和Kd设为0Kp从0.1开始慢慢加。在小车上给一个目标转速观察PWM输出有没有明显跟随目标。如果PWM总是落后目标就加大Kp如果PWM震荡、电机“嗡嗡”响、转速来回跳说明Kp太大了往回退一点。第二步加I。维持一个恒定的负载比如让小车开在一个有摩擦力的表面如果最终稳定转速和目标转速之间有一个固定偏差这就是静差。慢慢加Ki直到偏差消失。Ki不要太大否则容易出现积分饱和——就是误差累积过大导致实际转速超出目标后又需要很长时间才能回落。第三步加D。如果小车在加减速时有明显超调转速冲过头再回落就加Kd。这里注意电机编码器测速本身就带有一定噪声Kd过大会把噪声放大导致PWM高频抖动反而让电机发热。实测流程一般是先让小车空转记录编码器值再用一个串口或者OLED显示目标转速和实际转速。我习惯用串口把数据发到电脑上用串口绘图工具看曲线这样比肉眼看转速靠谱得多。曲线出现震荡就降Kp曲线有静差就加Ki曲线超调就加Kd比盲目试参数快太多。5. 常见问题排查实录5.1 问题排查速查表做小车的过程就是不断填坑的过程。我把这几年遇到过的高频问题整理成了表格如果你调试时卡住可以按这个顺序排查现象可能原因排查方法电机完全不动驱动模块未使能STBY没拉高检查STBY引脚电平电机完全不动PWM极性配置错误初始化时设占空比50%看电机是否半速转动电机完全不动电源功率不足用手电筒照电机如果转一下就停多半是电池内阻过大只有一边电机转GPIO复用冲突检查方向引脚是否被其他外设占用小车跑偏左右电机特性不一致用编码器对比两边转速在PID目标值上做偏移补偿红外遥控时灵时不灵红外接收头供电不足或受阳光干扰加一个10uF电容到接收头VCC和GND之间红外解码一直失败外部中断触发方式不对确认是下降沿触发接收头空闲时输出高电平电机转速抖动编码器测速窗口太短把测速周期从5ms改成20ms获得更多脉冲积累PID参数怎么调都震荡PWM频率过高或者编码器方向反了先确认编码器计数值方向和实际转动方向一致再调Kp小车跑一会儿就死机电机启动瞬间拉低单片机供电加一个大容量电解电容1000uF以上在总电源端5.2 几个容易被忽略的坑第一个坑是编码器方向反了。PID闭环最怕传感器方向接反——你原以为是右侧速度偏慢想给右侧加点PWM结果编码器计数方向相反PID反而以为右侧速度太快于是减小PWM导致右侧越来越慢直到小车原地打转。排查方法很简单手动推一下小车看编码器计数值是正还是负跟预期方向对比。第二个坑是电机启动瞬间的电流尖峰。电机启动电流可以达到正常工作电流的5到8倍如果用2S锂电池直接供整个系统瞬间拉低的电压可能让单片机复位。解决方法是给电机单独加一个大电容储能同时尽量避免电池一上电就同时启动两个电机。程序里可以让两个电机启动错开几十毫秒实测下来对系统稳定性改善非常明显。第三个坑是PWM频率不能太低。最常见的问题是电机发出“咕噜咕噜”的声音其实那是PWM频率落在人耳可闻范围内了。如果频率在1kHz以下电机还会出现明显的顿挫感。至少要升到8kHz以上推荐10kHz到20kHz。但也不要超过30kHz否则驱动芯片开关损耗急剧上升摸起来烫手。第四个坑是红外接收头的位置。红外遥控对方向非常敏感接收头如果被外壳挡住或者安装在车尾遥控距离会急剧缩短。实测红外接收头的接收角度大概是正前方±45度超过这个范围基本收不到信号。如果小车做的是竞速方向建议接收头伸出车体指向斜上方这样无论小车朝哪个方向跑只要遥控器大致对准车体就能收到信号。5.3 调试顺序建议最后分享一套我用了很久的调试顺序。很多人拿到代码就急着烧录然后对着小车猛按遥控器这是效率最低的方式。第一步先测电源。确保单片机供电正常编码器供电正常驱动模块的VM端电压符合预期。第二步只测PWM输出。让代码输出一个固定占空比比如50%用万用表或者示波器看PWM输出引脚是否正常。没有示波器的话接一个LED看亮度变化也行。第三步测电机正反转。手动调用方向控制函数确认电机能正转、反转、刹车、滑行。这一步不要接PID纯开环。第四步测编码器。用手转动轮子看计数是否变化、方向是否正确。第五步开环遥控。把红外解码跑通确认按键能控制电机动作。第六步接PID。先用小Kp跑起来再逐步优化参数。这套顺序可以帮你把“电机问题”“编码器问题”“遥控问题”“PID问题”分开独立排查不会出现四者混合在一起改了这边那边又坏了的情况。从开环PWM跑到PID闭环是我个人觉得做小车最有成就感的一个阶段。最开始只是追求“小车能动”后来变成追求“小车能走直线”“转速稳定不漂”。中间因为编码器接反、PID参数乱调、红外干扰等各种问题几乎每天都要推翻一次代码但每次定位到一个问题并解决对底层原理的理解就会深一层。按照这套顺序做下来你应该也能少走不少弯路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门