简介:一份STM32巡线小车PID算法代码工程,以STM32F103C8T6为主控,结合L298N电机驱动与三路反射式红外传感器完成路径识别,并扩展了超声波测距、LCD显示、舵机等模块,面向智能车竞赛入门者及嵌入式PID控制学习者。程序采用C语言编写,数据流依次为传感器、ADC、DMA、RAM、PID控制器、PWM、L298N、直流电机,清晰呈现从采集到闭环控制的完整链路。压缩包共85个文件,以28个c源码、22个h头文件为主,辅以o/d等工程构建文件及hex烧录文件,整体约610KB,目录划分了Core、Lib、List等模块,便于分段阅读与复用代码。资源包含完整Keil工程和烧录镜像,可对照学习PID参数调节、红外采样与主控初始化细节。目前已有5976人学习下载,适合需要参考实际巡线小车实现方案、快速上手STM32电机控制的开发者。 不少同学在STM32学习路上的第一个“真正能跑”的项目,就是巡线小车。你学会了点灯、按键、串口,觉得单片机也不过如此,然后开始做小车,结果发现:车是能走了,但走不直,过弯就冲出去,要么原地转圈,要么像喝醉了一样左右猛摆。这时候你才意识到,光会配置GPIO和PWM根本不够,真正让小车站稳、跑顺、过弯不飘的,是一套可靠的PID算法。
这篇文章我就围绕“STM32巡线小车PID算法代码”这个项目,把传感器选型、PID原理、代码实现、参数整定、常见坑一次性讲透。内容偏向实战,大部分代码我都实际烧录跑过,你照着搭也能复现。适合正在做课程设计、毕业设计,或者纯粹想在电子DIY上更进一步的朋友参考。
1. 项目整体设计与方案选型
1.1 硬件构成与选型理由
巡线小车这套东西看起来简单,但硬件选型直接影响后续调试难度。先说我的推荐配置,也是目前学生项目和开源社区里最主流的组合:
- 主控:STM32F103C8T6,就是俗称的“蓝丸”核心板,性价比极高,性能完全够用。
- 传感器:8路灰度传感器模块,或者4路也行。底层逻辑是红外发射管配合光敏接收管,黑线吸收红外光,白底反射红外光,输出电压/数字量随之变化。
- 电机驱动:TB6612FNG。比L298N压降小、发热低,用在5V电机上明显更稳。
- 电机:N20微型减速电机或者TT马达,前者精度高、速度快,后者便宜好买。
- 电池:两节18650串联给电机和主控供电,注意共地和降压。
这套配置在淘宝上几十到一百多块就能搞定。关键不是贵,而是每个模块的接口都很标准,灰度传感器输出信号可以直接接STM32的ADC引脚,或者干脆用数字量版本,省掉采样麻烦。
选型背后的一个考量和常见失败原因:很多人一上来就买那种最便宜的“两路红外对管巡线模块”,但两路传感器提供的信息量太少了。小车稍微跑快一点,传感器摆到两线中间,控制逻辑就会进入“猜”的状态。所以我一直建议至少四路起步,八路最好。能不能稳定巡线,往往在你选传感器的那一刻就决定了。
1.2 为什么巡线小车非要用PID
很多人都见过最简版巡线代码:左边传感器检测到黑线就右转,右边检测到黑线就左转,传感器全白说明出线了,疯狂掉头找线。这种“开关式控制”的问题在于,它只有“转”和“不转”两个状态,缺乏“偏了多少就修正多少”的比例关系。小车在直道上会左右反复横跳,走S形,速度一快就直接飞出去。
PID做的事情,本质上是把“黑线出现在传感器阵列的哪个位置”换算成一个偏差值,然后根据偏差的大小、变化趋势甚至历史累积,动态调整两个电机的转速差。这就好比开车时你不会等车头偏了90度才猛打方向盘,而是眼睛看到偏移几厘米就微调一下方向。偏差大就多打一点方向,偏差小就少打一点,看到车正在回正就提前松一松方向盘——这就是P、D、I三个环节在物理世界里的对应。
对小车的实际控制效果而言,用了PID之后最直观的变化是:直道上能稳定走直线,弯道能平滑降速通过,而不是蛇形走位。能做到这一步,你的小车才算从“会动”变成了“能跑”。
2. PID算法的原理与关键代码实现
2.1 位置式PID与增量式PID怎么选
PID控制算法分位置式和增量式两大流派,网上一搜一大把公式,这里我用自己的话给你说清楚。
位置式PID的输出是一个绝对值,直接代表你期望的控制量。公式长这样:
u(k) = Kp * e(k) + Ki * Σe(k) + Kd * (e(k) - e(k-1))它的问题在于,公式里的“Σe(k)”是误差从开始到现在的累加和。如果传感器一个瞬间miss掉线,产生一个超大的尖峰误差,这个累计项可能会在接下来很长一段时间里持续输出一个大值,小车直接抽风。另外,你要给电机PWM值,位置式输出还需要额外做大量程限制和保护。
增量式PID输出的是控制量的增量,换句话说它告诉你的不是“现在应该给多大PWM”,而是“在上一刻的基础上,PWM还要加多少或者减多少”。公式核心部分是对误差做差分:
Δu(k) = Kp * (e(k) - e(k-1)) + Ki * e(k) + Kd * (e(k) - 2*e(k-1) + e(k-2))巡线小车我推荐增量式,原因很实在:
- 不需要做复杂的积分限幅和抗积分饱和处理,代码量小,逻辑容易看懂,适合学生项目。
- 输出是增量,就算某次传感器读数异常,最多是这一拍的PWM多加了一点,下一拍会快速恢复,不会产生长期失控。
- 单片机里实现起来只需要保存三个误差历史值,资源占用极低。
你要说位置式完全不能用?也不是。但它在巡线这种“偏差频繁突变”的场景下,调参难度和出错概率明显更高。没必要跟自己过不去,小车就用增量式。
2.2 传感器加权位置计算
PID要工作,前提是得有一个“数值化”的误差输入。多路灰度传感器返回的是一组离散信号,你怎么把这些信号变成连续的偏差值?这里用到一个经典方法:加权量化。
原理其实很简单。假设你有8路传感器,给每一路分配一个位置权重,比如从左到右依次是 -4、-3、-2、-1、1、2、3、4(中间位置设为0)。每次采集后,把所有检测到黑线的传感器的权重值求加权平均,就得到了当前黑线相对小车中线的偏差:
sum_weight = 0 sum_value = 0 for i in range(8): if sensor[i] == 0: # 0代表检测到黑线,具体看模块有效电平 sum_value += sensor_value[i] * position_weight[i] sum_weight += sensor_value[i] if sum_weight != 0: error = sum_value / sum_weight else: # 丢线处理,如果上次偏差为负则给一个最大左偏差,否则给最大右偏差 if last_error < 0: error = -5 else: error = 5用加权平均的好处在于:黑线不是只在某一个传感器下面,而是横跨两三个传感器时,算出来的偏差是连续的、渐变的,能告诉PID“线其实在中间偏左一点点,大概偏了0.4个单位”。这才是有意义的误差信号。
这里有一件事必须提醒你:权重值的分配直接决定小车跑线的手感。权重跨度调大,相当于把偏差放大,P响应会变敏感,容易抖;权重跨度调小,系统反应迟钝,弯道转不过去。我建议初始拿 -4到4这个区间,后续根据赛道实际再微调。
2.3 增量式PID核心代码逐段讲解
下面这段代码是我巡线小车项目里的核心部分,按增量式PID写的,可以直接复制到你自己的工程里参考。
首先是PID结构体和误差历史变量的定义:
typedef struct { float Kp; float Ki; float Kd; float error_last; // e(k-1) float error_prev; // e(k-2) float output; // 当前输出增量 } IncrementalPID_t; IncrementalPID_t pid = { .Kp = 32.0f, // 初始参数,后面调 .Ki = 0.0f, // 巡线一般先用不到积分 .Kd = 6.0f, // 微分先用小值 .error_last = 0.0f, .error_prev = 0.0f, .output = 0.0f };然后是核心计算函数:
float PID_Calculate(IncrementalPID_t *pid, float target, float actual) { float error = target - actual; // 目标偏差是0,也就是希望黑线在中间 // 增量式PID公式 float increment = pid->Kp * (error - pid->error_last) + pid->Ki * error + pid->Kd * (error - 2.0f * pid->error_last + pid->error_prev); pid->error_prev = pid->error_last; pid->error_last = error; pid->output += increment; // 输出限幅,防止PWM溢出或者变化过猛 if (pid->output > 100.0f) pid->output = 100.0f; if (pid->output < -100.0f) pid->output = -100.0f; return pid->output; }注意,这个函数输出的已经是一个“附加修正量”的概念。它不是一个绝对的PWM值,而是在基础速度之上应该加的修正。如果输出是正的,表示需要向右修正(具体方向和你的电机安装、传感器排列有关,得根据实际测试确认正负)。
再看巡线主循环里怎么用这个PID输出:
int baseSpeed = 60; // 基础PWM速度,0-100 int correction = 0; // 实时采集传感器加权位置,得到偏差 float position = GetLinePosition(&sensor_data); // 注意:这里PID调用时把期望位置设为0,目标就是“让黑线回到传感器阵列中央” correction = (int)PID_Calculate(&pid, 0.0f, position); // 差速输出 int speedL = baseSpeed + correction; int speedR = baseSpeed - correction; // 限幅保护 speedL = Clamp(speedL, 0, 100); speedR = Clamp(speedR, 0, 100); Motor_SetSpeed(speedL, speedR);这段代码的逻辑很清晰:传感器算出当前黑线位置偏差,PID根据偏差给出修正值,然后把修正值叠加到左右电机的基础速度上,形成差速。左电机加速的同时右电机减速,小车就会向黑线所在的那一侧转向。
但这里有一个新手很容易搞反的坑:如果小车跑起来发现越偏越厉害,甚至朝着错误方向疯狂转弯,请把修正量叠加的符号反过来,也就是int speedL = baseSpeed - correction; int speedR = baseSpeed + correction;。正负号不是数学推导能推导出来的,它取决于你的电机安装方向和传感器排列方向,实测为准。我当年就在这里卡了一天,最后发现只是符号写反,差点把车拆了。
3. 硬件接线与PID参数整定实操
3.1 接线要点与供电设计
接线看起来简单,但供电问题能毁掉整个项目。巡线小车里有两个电源域:逻辑电源和控制电源。STM32核心板一般用5V供电,电机驱动需要比单片机更高的电流。如果电池直接同时给主控和电机供电,电机启动瞬间的大电流会把主控电压拉低,导致STM32重启。你会在调试时看到小车一启动,屏幕就花掉或者干脆死机,多数都是供电惹的祸。
我的接线方案是这样的:
- 两节18650电池串联,大概7.4V到8.4V电压。
- 电池正极先到TB6612驱动板的VM引脚,为电机供电。
- TB6612板载5V稳压输出给STM32核心板和灰度传感器供电。
- 所有模块的GND必须连在一起,这是电气基准,绝不能让地线各走各的。
信号线方面,灰度传感器的数字输出接STM32的GPIO输入引脚,如果是模拟量输出就接ADC引脚。TB6612的PWMA和PWMB接STM32的定时器PWM输出引脚,AIN1/AIN2控制左电机方向,BIN1/BIN2控制右电机方向。STM32F103C8T6的定时器1和定时器2都能输出PWM,注意别把PWM通道和调试端口冲突了。
另外一个细节:TB6612的逻辑电源VCC接5V,STBY引脚必须拉高,否则电机驱动芯片进入待机模式,你以为代码错了实际是硬件没使能。这个引脚好多初学者会漏接,百思不得其解,查半天才发现芯片还睡着。
3.2 参数整定三步走
PID光有代码不调参等于白写。参数没调好,误差信号一大,小车就会在地上跳摇摆舞,看起来还挺有节奏感,实际上完全没在巡线。我分享一套我常用的整定流程,从零开始大概半小时到一小时能调到能跑的状态。
第一步,Kp和Kd先设小值,Ki设0。比如Kp=10、Kd=1、Ki=0。跑一下,观察小车直道上的表现。如果它走线时左右摆动并且幅度越震荡越大,说明Kp偏高,按比例降低到能稳定跟线为止。如果小车反应迟钝,过弯时眼睁睁看着线从传感器底下溜走,说明Kp偏低,加大。
第二步,Kp确定得差不多之后,开始加Kd。Kd的作用是抑制变化趋势,相当于阻尼。直道摆动的频率明显变高变密,就适当加Kd。一次加一点,每次加完跑一圈看效果。我常用的步进值是0.5到1,加多了小车会变得僵硬,过弯时提前转、转弯不足或者转向后无法快速恢复。
第三步,Ki最后再碰,甚至可以一直保持0。巡线小车在这种短时间、快速响应的场景下,积分项的实际作用非常有限,反而容易带来超调和不稳定。除非你发现小车在某个直道段始终稳定地朝同侧偏移,才考虑加一点点Ki,比如0.1级别开始试。
整个调参过程的核心原则就是“每次只动一个参数,改动幅度要小,改完必须跑完整圈赛道观察”。你同时调Kp和Kd,出了问题根本不知道是哪个参数导致的。这个习惯也是工程调试通用的思考方式,值得养成。
3.3 差速控制与PWM输出细节
巡线小车本质上靠左右轮差速转向。差速控制上有一个容易忽略的点:左右电机的机械特性不可能完全一致,哪怕你用同一个型号,哪怕左右PWM值完全相同,实际速度也会有差异。结果就是小车在平坦直道也会向某一侧偏。这是PID的误差来源之一,但它其实不用你去“修电机”,因为PID的纠偏能力天然会把这种系统偏差吃掉——只要偏差还在传感器可检测范围内。
不过,基础速度的选取很关键。baseSpeed设太高,PID修正幅度跟不上,尤其急弯处必然冲出赛道。设太低,小车虽然稳定但毫无意义,像一个蜗牛在蠕动。我的经验是:先把baseSpeed调到35到50,稳定巡线后再逐步往上加。每加一次速度,通常都要把Kp和Kd重新微调一遍,因为系统动态特性变了,参数需要重新适配。
PWM分辨率方面,我用100作为满量程。如果你用定时器ARR是999,那PWM比较值就对应0到999,直接把PID输出乘上一个系数映射过去即可,逻辑上是一样的。重点不是具体满量程数值,而是保证整个平滑控制链路的数据一致。
4. 常见问题与排查技巧实录
4.1 高频问题排查速查表
这部分我把做巡线小车最常遇到的故障按症状整理成了一张速查表。建议你调试前先把这张表通读一遍,很多现象其实是同一个根因,自己对号入座会快很多。
| 症状 | 可能原因 | 排查方向 |
|---|---|---|
| 小车完全不动 | STBY没拉高、PWM引脚接错、电机线松 | 先量电机驱动输出电压,再查STM32的PWM输出 |
| 小车启动后反复重启 | 供电不足,电机启动拉低主控电压 | 用独立电源或者5V稳压模块,检查共地 |
| 直道上走S形 | Kp过小或Kd过小 | 按前面整定流程逐步调整 |
| 剧烈抖动、高频震荡 | Kp过大或者Kd过大 | 先降Kp,Kd也适当回落 |
| 弯道冲出去 | Kp不够、baseSpeed过高或传感器个数不足 | 先降速验证,再逐步提Kp |
| 传感器明明压线但程序没反应 | 分辨率/电平逻辑配反 | 用串口打印每个传感器的裸数据,确认0和1的对应关系 |
| 只有一个方向能转向 | 差速修正符号错误或者单侧电机驱动没接通 | 检查PID修正量叠加的符号,测试单侧电机正反转 |
| 过弯后找不到线乱转 | 丢线处理逻辑有问题 | 检查丢线时error赋值方向、last_error是否保存 |
这表里的每一行都是我实际遇到或者帮别人排查过的。其中“串口打印传感器原始值”这一步一定要做,它能把所有“我以为”变成“我确定”。很多同学上来就调PID,最后发现传感器数据本身就是错的,那调参根本没有意义。
4.2 丢线处理与环境干扰的独家经验
丢线,也就是所有传感器都没检测到黑线,是巡线小车里最棘手的情况。要区分两种丢线场景:一种是车在直道上跑飞了,传感器阵列整体偏离赛道;另一种是过弯道时,传感器阵列进入曲率很大的弯心,黑线瞬间从检测范围里消失。
我用的处理逻辑前面代码里已经提到了:看上一拍的误差符号。如果上一拍误差为正,也就是线在左边,小车正在向右偏出,这时丢线就假设“偏差继续保持为一个比较大的值”,比如-5或者+5,让小车继续朝找回黑线的方向转。这个逻辑简单有效,缺点是误差跳变比较猛,如果Kp很大,丢线瞬间会产生一个很大的修正冲击。
更平滑的变形是:丢线时不对PID输入一个设定好的大误差,而是保持上一拍的error不变,直到传感器重新看到线。这样修正不会剧烈跳变,但代价是反应变慢。两种策略我都试过,最后固定用“设定超大误差”策略,因为巡线场景里及时把车拉回来通常比平滑更重要。
环境干扰这块,最要命的是光线变化。灰度传感器的红外发射管对环境光很敏感,强光直射下,白底反射和黑线吸收的对比度会明显下降。所以我建议:
- 优先选带遮光罩的传感器模块,或者自己用热缩管、黑胶带挡一下环境光。
- 跑赛道前先打印一次所有传感器数值,确认黑色和白色的读数区分度足够大。
- 在代码里对传感器阈值做“在线校准”,启动时自动扫描一遍当前环境光的基准值,再动态决定黑白的判定阈值。这个方法本质上是把硬件的锅转给了软件,却能省掉很多环境适配的麻烦。
另外,如果数字量传感器老是误触发,可以加一个简单的软件滤波:连续采样三次,取两次以上相同的值作为最终结果。STM32跑这个量级的传感器滤波几乎不占用CPU周期,但能显著减少偶发抖动带来的误判。
最后再分享一个实际调试经验:在PID参数已经调得比较顺的情况下,如果小车在某些速度段仍然不稳定,先别急着继续加参数,把基础速度降低10看看。很多时候是轮胎打滑或者电机响应延迟造成的物理限制,PID参数怎么调都救不回来。接受小车物理性能的边界,在它能做到的范围内把参数调到最好,这才是务实的态度。巡线小车的乐趣也在于此,它不是一道纯数学题,而是一门权衡的艺术。
本文还有配套的精品资源,点击获取