
简介这是一份STM32智能小车PID闭环速度控制程序源代码基于标准库函数开发主要面向嵌入式入门学习者、电子设计竞赛队伍以及智能小车爱好者帮助解决直流减速电机的测速反馈与闭环调速问题。工程基于KEIL环境配套Keil.STM32F1xx_DFP.2.3.0.pack库文件适配STM32F103C8T6芯片也较易移植到其他STM32F1系列源码涵盖L293D电机驱动、1602液晶显示、测速模块信号采集并通过K1按键启动闭环控制K3/K4按键降低或提高目标速度整体逻辑清晰作者已在智能小车上实测通过。压缩包共包含172个文件除37个.h头文件和36个.c源文件外还有Keil工程配置.uvprojx、编译中间文件.o/.crf、最终烧录用的.hex/.axf以及map映射、调试脚本等完整还原工程构建过程包体仅3.44MB。目前已有4145人学习使用适合想快速理解PID比例积分微分运算、PWM调速与编码器测速配合或用此工程作为课程设计、竞赛作品基础的读者。1. 为什么PID闭环速度控制是智能小车的必修课很多朋友拿到“STM32智能小车PID闭环速度控制基于库函数程序源代码.rar”这个压缩包时第一反应是赶紧解压、下载、烧录看看小车能不能转。这个思路没错但如果你只是把它当成一个“能跑的程序”来用那真的太浪费了。我建议你先花十分钟想清楚一件事这个项目到底解决了什么问题以及它和你之前玩过的普通小车有什么本质区别。1.1 从“能走”到“走得稳”的跨越先说说很多入门玩家踩过的坑。你之前做的小车大概率是开环控制——给电机一个固定PWM占空比比如70%然后小车就往前冲。但实际跑起来你会发现电池电压从8.4V降到7.2V同样70%的PWM车速明显变慢地面从瓷砖换成地毯摩擦力变大速度又开始掉转弯的时候负载变化左右轮转速不一致小车跑的路线歪得没法看。这是因为直流电机的转速受负载和供电电压影响非常大开环控制根本没法保证“我想要多少速度它就真的有多少速度”。而PID闭环控制做的事就是实时测量电机实际转速和目标速度做比较根据误差动态调整PWM输出让电机转速始终贴在目标值附近。这就是“能走”和“走得稳”的本质区别。1.2 为什么选择STM32 库函数方案这个项目选型很有代表性主控用STM32F103系列开发方式用标准外设库也就是常说的“库函数”速度反馈用编码器控制算法用增量式PID。这个组合在2024年仍然值得学原因有三点。第一STM32F103是市面上资料最全、最经典的单片机不管是江科大原野火的教程还是正点原子的视频覆盖量都极其庞大遇到问题几乎一定能搜到答案。第二标准外设库虽然是“老技术”但它的代码可读性比HAL库更直观适合理解底层寄存器操作逻辑对初学者来说反而比HAL库更好上手。第三增量式PID代码量小、无积分累积过深的问题、执行效率高非常适合8位/32位MCU上的电机控制场景。提示如果你的开发环境比较新可能标准外设库在最新版Keil里会报一些兼容性警告这不是代码问题是编译器版本导致的后面我会讲怎么处理。1.3 这个源码包能帮你解决什么问题从实际价值来看这个项目至少覆盖了几个高频需求毕业设计STM32小车是电类专业经典选题、竞赛前期验证智能车/电子设计竞赛的基础闭环控制、以及想从“点亮LED”进阶到“真正控制一个物理对象”的自学者。拿到源码后你应该重点看五个模块定时器编码器模式的初始化与读取、PWM输出配置、PID计算函数、电机驱动逻辑、以及主循环或中断里的速度控制调度。这五个模块理解了整个项目就等于拿下了80%。接下来的内容我会按这几个模块逐个拆解把代码背后为什么这么写讲清楚。2. 核心硬件方案与选型思路很多初学者容易忽视硬件选型和代码之间的因果关系。其实代码怎么写很大程度上是由硬件方案决定的。这个项目里的关键硬件选型如下主控STM32F103C8T6蓝丸板、电机驱动TB6612或L298N、直流减速电机带霍尔编码器如常见的MG310或JGA25-370、电源方案两节18650或7.4V锂电池组。2.1 编码器与测速原理编码器是这个项目中最关键的传感器。常见的光电霍尔编码器装在电机尾部电机每转一圈编码器会输出一定数量的脉冲。比如常见的减速电机减速比1:30电机本体每转一圈输出13个脉冲霍尔编码器常见13PPR那么输出轴转一圈的脉冲数就是13×30390个。你在代码里看到的TIM_GetCounter或者编码器计数读取实际上就是读取定时器在编码器模式下自动累计的脉冲数。STM32的定时器编码器模式非常方便它通过TIMx_CH1和TIMx_CH2两路输入捕获自动判断正反转并向上/向下计数。这意味着硬件上就把方向判断和脉冲计数一起解决了不用你在中断里手动判断A相B相的先后顺序。有一点必须提醒编码器脉冲计数读取后要转化为实际速度。常见的做法是在一个固定时间窗口内读取计数器增量然后换算成“每秒多少转”或“每秒多少厘米”。公式是速度(转/秒) (脉冲增量 / 编码器总分辨率) / 时间间隔(秒)其中编码器总分辨率 编码器线数(PPR) × 减速比如果开启定时器4倍频模式还需要再乘以4。这就是为什么这类代码里你总能看到一个类似Encoder_Get_Speed()的函数内部做了一堆除法运算——它不是为了炫技是真的要换算成物理单位。2.2 电机驱动与PWM的配合有了速度反馈还得有执行机构。电机驱动芯片负责将单片机的PWM信号放大成功率信号驱动电机。TB6612和L298N的区别在于TB6612功耗低、体积小、PWM频率响应更好适合智能小车这种电池供电的场景L298N压降大、发热大但胜在便宜、皮实。在代码层面PWM配置有几个关键参数频率、占空比分辨率、以及方向引脚的控制逻辑。对于常见的直流减速电机PWM频率建议设置在10kHz到20kHz之间。频率太低电机会发出刺耳的啸叫声人也听得见频率太高MOS管开关损耗加大驱动芯片发热。选择15kHz左右基本不会错。占空比分辨率则取决于定时器重装值ARR一般设置为999或9999对应PWM分辨率为0.1%或0.01%。电机正反转控制靠两个方向引脚如AIN1、AIN2的电平组合。注意方向切换时最好先让PWM输出为0再切换方向引脚否则容易烧驱动芯片或产生大的冲击电流。这个细节在很多源码里都不会专门写但却是实际调试中最容易烧硬件的坑。2.3 电源系统的隐藏细节这个项目里还有一个很多人容易忽视的地方——电源。STM32需要3.3V编码器需要5V或3.3V电机需要7.4V左右。靠谱的做法是电池直接给电机驱动板供电驱动板上带5V稳压输出给单片机系统或者用独立的降压模块给逻辑电路供电。绝对不要用单片机的3.3V去驱动电机也不要把电机电源和逻辑电源直接并在同一根线上否则电机启停瞬间的电压跌落会把单片机搞复位。我见过太多“小车跑着跑着就重启”“速度一快就死机”的案例查到最后都是电源问题。代码写得再对电源不干净什么都白搭。3. PID算法实现细节与关键代码解析PID是整个项目的灵魂。这个源码包里用的是增量式PID相比位置式PID它的优势是输出的是控制量的增量没有误差累积项而且天然带有抗积分饱和的特性非常适合嵌入式环境。3.1 位置式与增量式的选择逻辑简单回顾一下两个公式。位置式PID输出的是绝对的PWM占空比u(k) Kp*e(k) Ki*Σe(i) Kd*(e(k)-e(k-1))增量式PID输出的是PWM的增量Δu(k) Kp*(e(k)-e(k-1)) Ki*e(k) Kd*(e(k)-2*e(k-1)e(k-2))两者的差异在实际工程中很明显。位置式的积分项需要累加所有历史误差一旦长时间运行计算量增大且可能出现积分饱和——误差一直存在积分项疯狂累积最终导致输出一直在最大值调不回来。增量式只计算最近三次误差输出的是增量天然规避了这个麻烦。在智能小车这个场景控制频率速度环通常在50到200Hz之间。增量式PID的计算量小占用资源少而且不需要对积分项做特殊的限幅保护代码实现简单可靠。所以这个项目选增量式是合理的建议你不要轻易改成位置式除非有特殊需求。3.2 核心代码结构逐行解析抛开具体文件名整个项目的核心代码逻辑基本可以抽象为以下几个模块编码器读取模块int16_t Encoder_Get_Count(void) { int16_t count; count (int16_t)TIM2-CNT; // 读取计数器值 TIM2-CNT 0; // 清零准备下一次计数 return count; }这里需要注意两个细节。第一TIM2-CNT一定要清零。不清零的话下一次读到的就是累计值而不是增量速度计算直接就错了。第二计数器的类型是有符号的int16_t而不是无符号uint16_t因为编码器模式会向上或向下计数需要能表达负数。这两个细节是这个模块最容易出错的地方。PID控制模块typedef struct { float Kp; float Ki; float Kd; float target; // 目标值 float actual; // 实际值 float err; // 当前误差 float err_last; // 上一次误差 float err_prev; // 上上次误差 float output; // PID输出增量 } PID_TypeDef; void PID_Calc(PID_TypeDef *pid) { pid-err pid-target - pid-actual; pid-output pid-Kp * (pid-err - pid-err_last) pid-Ki * pid-err pid-Kd * (pid-err - 2*pid-err_last pid-err_prev); pid-err_prev pid-err_last; pid-err_last pid-err; }这段代码很典型。每次调用PID_Calc就是做一次增量式计算输出output是PWM的增量最终PWM占空比是current_pwm output。注意需要手动判断current_pwm是否超限例如限制在0到1000之间对应ARR999这个限幅操作一般放在调用PID的函数里而不是放在PID计算函数里。速度控制调度速度环的定时器中断里大概每5到10毫秒执行一次读取编码器计数换算成实际速度调用PID_Calc更新PWM占空比。中断频率太低控制滞后明显中断频率太高PID输出变化过于频繁电机反而抖动。5ms到20ms即50Hz到200Hz是一个比较稳妥的范围我一般先用100Hz起步。3.3 调参方法论从P到I再到D很多人拿到源码后最关心的问题就是“PID参数调多少合适”这个真没有标准答案但有一套方法论可以参考。我推荐用“临界比例度法”配合“试凑法”组合操作。先把Ki和Kd设为0只保留Kp。从小到大慢慢加Kp观察小车速度响应如果实际速度有稳态误差、响应慢继续加Kp如果出现等幅振荡说明Kp已经偏大回退到振荡前值的60%-80%作为基础Kp。然后在基础Kp上把Ki从0开始往上加用来消除稳态误差。注意Ki过大同样会引发振荡而且振荡周期往往比Kp引起的更长。最后加Kd用来抑制超调和振荡但Kd对噪声敏感如果编码器信号有干扰Kd太大会导致输出抖动。实测下来常见的速度环参数范围大致是Kp在0.1到2.0之间Ki在0.01到0.5之间Kd在0.001到0.1之间具体和PWM分辨率、速度单位、编码器分辨率有关。但这个范围只能作为起点真正合适的值必须针对自己的小车去调。注意调参时一定要让目标速度发生变化来测试。比如先给一个中速目标稳定后再突然给一个高速目标观察响应曲线。如果从低速切到高速时出现过大的超调说明Kp或Ki偏大如果响应太慢、半天爬不到目标速度说明Kp或Ki偏小。4. 实操过程与核心环节实现前面讲了原理和代码逻辑这一节我按实际操作顺序把从解压源码到小车跑起来的过程完整走一遍标出哪些坑值得特别注意。4.1 环境准备与工程导入首先准备开发环境。建议使用Keil MDK 5.36以上版本再安装STM32F1系列的器件支持包DFP也就是你之前可能搜过的“STM32芯片包安装”。标准外设库StdPeriph Library在Keil老版本如5.23里体验最好新版本会有几个警告不影响下载运行但如果你有强迫症可以把编译器版本切换为AC5ARM Compiler 5警告就基本消除了。解压源码包后不要直接打开先看文件夹结构。一般会分为USER包含main.c、stm32f10x_it.c、系统文件、HARDWARE各外设驱动的.c和.h、SYSTEM延时、串口等公共模块、CORE启动文件、OBJ编译输出。双击USER目录下的.uvprojx或.uvproj文件即可打开工程。打开后做一次“清理-编译”。按下编译快捷键F7前先检查三件事魔术棒Options for Target里的Device是否选的是你实际用的芯片型号C8T6还是C6T6C/C选项卡里的Define是否包含USE_STDPERIPH_DRIVER,STM32F10X_MDOutput选项卡里的Create HEX File是否勾选。这三个配置错了编译必报错或者烧录后没反应。4.2 硬件接线与烧录验证硬件接线以最常见的C8T6蓝丸板为例。电机驱动模块和MCU的IO对应关系需要和源码里的宏定义保持一致比如信号端口说明PWM_LPA0/TIM2_CH1左电机PWMPWM_RPA1/TIM2_CH2右电机PWMAIN1/DIR_LPB0左电机方向AIN2PB1左电机反向BIN1/DIR_RPB10右电机方向BIN2PB11右电机反向ENC_L_APA6/TIM3_CH1左编码器A相ENC_L_BPA7/TIM3_CH2左编码器B相ENC_R_APB4/TIM3_CH1或PB6右编码器A相ENC_R_BPB5/TIM3_CH2或PB7右编码器B相提示不同开发板的丝印不一样主控引脚复用也不一样。务必对照原理图确认不要盲信网上的接线表。接错引脚轻则没反应重则烧IO口。烧录方式推荐用ST-Link或J-Link用SWD接口只需要接SWDIO、SWCLK、GND、3V3四根线。如果你用的是串口ISP下载记得BOOT0要拉高下载完再拉低复位。烧录成功后如果程序里有LED闪烁或者OLED显示初始化应该能立刻看到效果。4.3 关键指标实测与效果评估程序烧录成功后还不能高兴得太早要验证闭环是否真的生效。我建议做一个简单但效果明显的测试把小车架起来让轮子悬空通过串口上位机或板载按键设定一个目标速度。如果PID生效你会听到电机声音快速变化轮子转速稳定在目标值附近如果PID没调好你会看到轮子要么忽快忽慢要么震荡到极限。另一个建议是给小车设一个突然变化的目标速度观察是否能快速跟随。短时间内如果你看到轮子像“抽搐”一样抖动多半是Kp过大如果轮子过很久才达到目标速度可能是Ki太小或Kp太小。这些表现比你用示波器看波形更直观对新手也更友好。我试过的调试顺序是先只调Kp目标速度恒定观察稳态误差和稳定性稳定后再加Ki消除稳态误差最后加Kd抑制超调。整个调参过程大概需要一两个小时要有耐心。调完一轮参数手感上你就对PID“身体记忆”了。4.4 常见问题速查表我把实际调试中经常遇到的问题整理成一个速查表遇到问题可以快速定位。现象可能原因解决方案编译报错“core_cm3.h: No such file”工程缺少CMSIS路径在C/C Include Paths中添加CORE目录电机不转PWM通道或方向引脚接线错误检查IO映射和驱动芯片供电轮子能转但速度显示为0编码器接线错误或定时器配置不对检查编码器A/B相是否接对模式是否配置为编码器模式速度波动大噪声严重Kd过大或编码器信号受干扰降低Kd给编码器线加滤波电容电机啸叫PWM频率太低增大PWM频率至10k-15kHz小车跑偏左右轮PID参数不一致或编码器分辨率不同左右轮分别单独标定和调参一键下载后运行不了BOOT0跳线没归位下载后复位或把BOOT0拉低还有一个非常典型的坑使用标准库的延时函数Delay_ms()时如果定时器配置和默认SysTick冲突程序会卡死在延时里。这个往往是SYSTEM文件夹里的SysTick_Config配置与你自己的定时器初始化有冲突导致的。排查方法是单步调试看卡在哪一行。4.5 我的一些独家调试技巧分享三个能直接提升效率的小经验。第一善用串口打印。把目标速度、实际速度、PID输出值通过串口输出用上位机虚拟示波器比如VOFA、匿名上位机画曲线比看逻辑分析仪直观得多。串口波特率建议115200输出频率和控制频率保持一致比如100Hz这样画的曲线才有参考价值。第二用静态测试代替动态测试。调参时不要总在跑动中观察把车架起来调好空载参数再放到地面做负载修正。空载下小车的响应特性和地面满载差别很大直接地面调你会分不清到底哪个参数有问题。第三如果左右轮驱动结构完全对称但跑起来还是有微小跑偏可以用软件补偿。例如在直行时给某一侧电机加一个小的占空比偏置比如2%到5%。这个叫做“死区补偿”或“零位校准”在很多工业控制里也是这样做的。5. 从速度环到更复杂的控制结构很多同学做完速度闭环后会觉得“就这样了”其实不是。速度环只是第一步在这个基础上还能扩展出很多更有意思的控制架构。5.1 双环控制位置外环 速度内环最常见的是在速度环外面再加一个位置环构成串级PID也叫级联PID也就是你搜索时看到的“pid双环控制”、“级联pid控制”。位置环的输出作为速度环的目标值速度环的输出控制PWM。这样做的好处是能实现“精确走直线多少厘米”“自动停到某个位置”这类功能是循迹小车、智能车库、机械臂控制的基础结构。实现双环时的关键原则是外环位置环的响应频率要低于内环速度环。通常速度环100Hz到200Hz位置环50Hz左右否则两个环互相抢资源系统容易不稳定。5.2 模糊PID和自适应控制如果你觉得定参数的PID调着费劲可以了解模糊PID。模糊PID的本质是用模糊规则表根据当前误差和误差变化率在线调整Kp、Ki、Kd。在小车高速跑直线、突然转弯、爬坡等工况变化很大的场景模糊PID有更好的适应性。但学习成本也更高需要建立模糊规则、隶属度函数等建议先把定参数PID吃透再考虑别一上来就搞复杂结构。5.3 多机通信与上位机联调STM32小车做到后期经常要和K210、OpenMV这类视觉模块通信。这也解释了热搜词里为什么有“k210与stm32通讯”。串口通信是首选方案STM32的USART接收视觉模块发来的坐标或循迹数据再把位置信息转换成速度环的目标速度这样就组成了一套完整的视觉伺服系统。通信协议建议使用帧头数据校验的简单格式比如0xAA 0x55 len data checksum。串口中断接收时要用环形缓冲或者状态机解析防止数据粘包和丢帧。这个能力在后续做比赛和毕业设计里都是高频考点。关于“error: no stm32 target found”这个报错如果你遇到的是ST-Link连接失败基本是接线松了或者驱动没装好但如果是在CubeMX生成工程后编译板子不识别注意检查调试接口的引脚是否被复用成了GPIO。复用后ST-Link的SWD引脚没法用就回不去下载模式了这时你需要按住复位键再点下载同时连按复位放掉多试几次就能连上。我在实际做项目时发现把速度环调试好之后整个代码的可扩展性会变得非常强。很多看起来复杂的功能比如自动循迹、避障、定位停车本质上都是在目标速度的生成上做文章而底层的执行机制还是那套PID闭环。这大概就是这类项目最值得学习的地方——它不是孤立的知识点而是一个打通“传感器-算法-执行器”全流程的经典案例。最后再分享一个我自己的小习惯每次调完一组参数我都会把当时的波形截图或者把参数记在代码注释里附带调参日期、小车状态空载/满载、电源电压。这样下次不管是自己调优还是帮别人复现都有数据可查不用从头再来。这个习惯看起来简单但坚持下来你会发现它帮你省下的时间绝对超出预期。本文还有配套的精品资源点击获取