
1. 从“来回摆头”到“贴线行驶”只差一个PID如果你亲手装过一辆循迹小车大概率见过这个画面传感器明明能准确识别黑线电机转速也正常可小车就是不走直线——要么冲过弯道直接飞出赛道要么在黑线两侧疯狂摆头像喝醉了酒一样扭着前进。很多人第一反应是“传感器装歪了”或者“电机不行”折腾半天也修不好。先说结论八成以上的蛇形问题根子不在硬件而在控制逻辑。大多数入门教程里用的循迹方式是“阈值开关法”——黑线偏左就右转黑线偏右就左转看起来逻辑没毛病但你想想开车时的操作逻辑你看到车偏右了会猛打方向盘吗不会你会根据偏离的多少适当修正一点。小车如果只有“偏左猛打”和“偏右猛打”两档输出结果必然是越过中线、冲到另一侧、再猛打回来形成来回震荡。这就是为什么做循迹小车绕不开PID算法。它解决的核心问题是根据偏离程度输出一个平滑的、成比例的修正量让小车在大直道上接近走直线在弯道里又能提前带角度切入。这篇文章我按自己做车的经验从误差怎么算、PID三个参数各自管什么、代码怎么写、参数怎么调一直讲到十字路口和电磁循迹的扩展方案争取一篇讲透。适合看这篇的人单片机刚入门、手里有套件但跑不直的朋友已经能跑但速度一快就飞线的朋友以及准备参加智能车竞赛、想从“开关循迹”升级到“PID循迹”的朋友。下面直接进入正题。2. 位置式PID与增量式PID循迹小车该用哪种2.1 PID公式首先得看懂它在算什么PID三个字母分别对应比例Proportional、积分Integral、微分Derivative。用一句大白话概括比例管现在积分管过去微分管未来。位置式PID的输出公式长这样u(k) Kp * e(k) Ki * Σe(i) Kd * [e(k) - e(k-1)]其中e(k)是当前时刻的误差e(k-1)是上一时刻的误差Σe(i)是从启动到现在所有误差的累加。积分项把历史上所有误差攒到一块儿这个在工程上很容易出问题。小车跑一圈如果积了一堆误差输出可能会爆掉所以实际做循迹时我通常用增量式PID输出的是“这次该在上次基础上加多少”Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)]最后真正给到执行器的是u(k) u(k-1) Δu(k)。增量式的好处是它输出的是变化量天然带有“记忆”而且积分饱和的风险小很多。循迹小车这种系统控制周期短、电机响应快增量式是绝对的主流。你如果看到某份开源代码里PID输出直接乘以系数就是最终PWM那大概率就是增量式。2.2 为什么位置式在赛车上容易出问题位置式不是不能用但需要额外做积分限幅、输出限幅、积分分离处理起来绕。举个例子小车停在起点线上检测到的误差一直是-2位置式的积分项会从0开始不断累加如果启动前忘了清零一上电就是满偏输出小车直接“射出去”。增量式就没有这个问题它的输出和“最近几个周期的误差变化”有关历史误差不会无限堆积。加上循迹小车的控制周期通常只有10到20毫秒增量式的“无静差”特性足够用。所以我的建议非常直接循迹小车优先用增量式PID别去折腾位置式。竞赛里我见过用位置式调得很好的队伍但他们一定额外做了抗积分饱和处理——新手阶段完全没必要给自己加这个难度。3. 误差的计算方式决定了PID能跑多快3.1 两路灰度传感器的“开关式”误差先看最基础的方案两个灰度传感器并排装在小车前方探头对准黑线两侧。读取结果是{0, 1, 0}这种格式1代表压到黑线。两路传感器只有三种组合左边压线、右边压线、都在线上。误差函数简单粗暴左压线误差 1说明车偏右了要往左修。右压线误差 -1说明车偏左了要往右修。都在线误差 0。这个方案能跑但误差分辨率太低。你会发现PID几乎退化成开关控制因为误差只有三个值比例项输出的修正量也只有三档蛇形问题依然存在只是幅度小了一点而已。3.2 五路灰度传感器的加权平均误差想体验PID的真正威力至少用五路灰度传感器。五路探头的布局是一条横排中间一路对准黑线左右各两路对称分布。我用的误差计算公式是error (-2 * s0) (-1 * s1) (0 * s2) (1 * s3) (2 * s4)s0到s4分别是最左到最右的传感器状态压线为1否则为0。这个公式的物理意义是传感器组的“重心”偏离黑线中心的加权距离。如果只有s1压线error是-1说明车稍微偏右只有s0压线error是-2说明偏得比较厉害。这样误差就从3个离散值变成了-2到2之间的5个离散值舵机或差速的修正量也平滑得多。更进一步如果把每个传感器的模拟量读出来灰度传感器输出0到1023的ADC值还能做亚传感器级的位置插值。公式变成error (0*s0 1*s1 2*s2 3*s3 4*s4) / (s0s1s2s3s4) - 2这个公式算出的是黑线中心在传感器组里的“亚像素”位置误差能精细到0.1级别高速过弯时优势非常明显——刚才还是5档误差现在变成了几十档PID终于有“比例”的感觉了。3.3 误差方向要和你自己的坐标系统一新手最容易翻车的点是误差正负方向和转向方向搞反。比如我习惯把“车偏右、需要左转”定义为正误差那么转向函数里误差 0时就要让左轮减速、右轮加速。这个方向在代码里只差一个正负号但装车后表现却是“越修正越偏”最后直接冲出去。建议你拿到代码后先做一个开环测试用手把小车放在黑线右边看屏幕上打出来的error是不是正值然后把左轮和右轮的PWM差值和error的符号逻辑捋一遍确认无误再上电跑。4. 增量式PID的代码骨架与关键参数4.1 一个可以直接抄的C语言实现以下是我实际在STM32和Arduino上都在用的增量式PID骨架代码逻辑完全一致// 控制器结构体 typedef struct { float Kp; float Ki; float Kd; float err_now; // 当前误差 float err_last; // 上一次误差 float err_prev; // 上上次误差 float out; // 输出值 } PID_t; // 增量式PID计算 float PID_Calc(PID_t *pid, float err) { pid-err_prev pid-err_last; pid-err_last pid-err_now; pid-err_now err; float delta pid-Kp * (pid-err_now - pid-err_last) pid-Ki * pid-err_now pid-Kd * (pid-err_now - 2 * pid-err_last pid-err_prev); pid-out delta; // 增量累加 return pid-out; }在主循环里调用float err Get_Line_Error(); // 读取传感器并计算误差 float out PID_Calc(track_pid, err); // 计算PID输出 // 输出限幅 if (out MAX_OUT) out MAX_OUT; if (out -MAX_OUT) out -MAX_OUT; // 差速转向base_speed为直行基础速度 int left_pwm base_speed out; int right_pwm base_speed - out; // 限制PWM范围 left_pwm constrain(left_pwm, 0, 255); right_pwm constrain(right_pwm, 0, 255); Set_Motor(left_pwm, right_pwm);MAX_OUT是输出限幅我一般设为基础速度的40%左右。比如base_speed是150MAX_OUT就是60意思是转向修正量最大能让内外轮差120。设小了的话急弯转不过来设大了直道上反而容易抖。4.2 为什么需要乘-1方向坑再强调一遍上面这个差速逻辑是“误差为正时out为正左轮增速、右轮减速车往左转”。但你的传感器布局如果和我相反比如你习惯把右偏定义成负值跑起来方向就是反的。最快的排查方法是把小车悬空手动压住传感器模拟偏右看轮子是不是朝左修正。不是的话在你的PID_Calc调用处改成PID_Calc(track_pid, -err)就行。4.3 控制周期要固定否则积分项全乱套PID的Kp、Ki、Kd是在特定控制频率下调出来的。如果你主循环是while(1)直接跑循环时间会随传感器读取、PWM设置、屏幕打印等操作波动同样的参数在不同负载下表现会差很多。正确做法是用定时器中断固定每10ms执行一次PID计算和输出。在Arduino上可以用Millis()判断在STM32上直接放定时器回调里。我自己实测过同样的Kp、Kd控制周期从10ms波动到25ms直道就开始抖弯道甚至会甩尾。控制周期不固定后面调参基本白调。5. 三个参数到底怎么调先P、再D、后I5.1 调参前的硬性准备把小车架起来轮子悬空先确认几个事两侧电机在相同PWM下转速一致不一致就先转速校准电池电量在正常区间快没电的电池会让电机响应明显变慢传感器读取的误差值在直线上确实接近0。这些不确认下面调参你永远分不清问题是出在PID还是硬件上。5.2 Kd试出直道稳定性Kp决定弯道响应很多人只知道“调PID就是调三个数”但不知道先调哪个。正确的顺序是先把Ki设为0Kp设一个较小的值比如0.05Kd先给0跑一圈试试。如果小车反应迟钝、过弯冲出去说明Kp偏小翻倍。如果开始走S型、左右震荡说明Kp偏大减半。Kp调到直道小抖、弯道勉强能过之后开始加Kd。微分项的作用是“预判误差变化趋势”误差快速变大时给一个反向的抑制力可以大幅抑制蛇形。Kd从小往大加每次加Kp的3到5倍直到直道不再抖。注意Kd太大会导致高频抖动——小车不是缓慢摆头而是“嗦嗦嗦”小幅度高频抖动而且电机和舵机会发烫。Ki在循迹小车里用的场景不多因为比例项已经能消除动态误差。但如果你发现小车过弯后总是“差一点点”才回正长期存在很小的静态误差可以给一点Ki比如0.01起步。给多了会原地画圈或者过冲。以我个人经验大部分循迹场景Ki可以保持为0如果Kp和Kd调好了还跑不直优先检查机械结构而不是加积分项。5.3 场地速度的匹配低速调的参数高速一定会翻车这点必须单独说在速度A下调出来的参数直接套到速度B上是不行的。速度提高传感器采样间隔内小车走过的距离更长同样的误差变化率会更大微分项的输出会被放大系统稳定性下降。所以我建议先把所有参数在期望的最高速度下调试。先从1m/s左右把直道跑稳再逐步加速每次加速都要重新微调Kd通常是增大。如果你在低速下把Kd调得刚刚好上高速后大概率会抖到时候别怀疑硬件先怀疑参数和速度不匹配。6. 传感器布局与机械结构对PID的隐性影响6.1 传感器离地面高度误差计算的“隐形开关”灰度传感器离地太高检测不到黑线离地太低偶尔蹭到地面ADC值会毛刺不断。我踩过最离谱的坑是传感器支架松动导致小车跑着跑着探头角度变化误差值整体偏移PID输出始终有个直流分量表现为小车持续往一侧贴线。建议传感器离地高度控制在10到15毫米并加装固定支架。每次比赛前用一张白纸和一段黑胶带校准一下传感器阈值确认五个探头的检测状态一致。6.2 重心位置决定高速过弯的极限PID调的是控制逻辑但机械重心的位置直接决定小车在高速过弯时物理上能不能稳住。电池如果放得太靠后重心靠后前轮抓地力不足过弯总是要甩出去——这时候你调再多的Kd也救不回来因为那不是控制问题是物理极限到了。把电池尽量放低、居中车身的重量分布做到前后5050左右能显著提高过弯速度上限。底盘越低越好重心越低侧倾越小传感器离地高度也越稳定。6.3 舵机中值和电机死区如果你用的是舵机转向而不是差速转向还有一个重要的坑舵机中值不是90度至少我手里这颗舵机的90度并不是在直行位置。需要实测标定给舵机发一个PWM调整到轮子完全笔直把这个值存为中值然后把PID输出映射到这个中值上。电机也有“死区”问题——低PWM值下电机可能根本不转从0到某个阈值之间是无效的。所以如果你用差速转向下限不能只设0要设到电机刚好能转的最小值否则低速修正时一个小车是“走走停停”的控制效果会非常差。7. 实战调参一场真实跑车过程全记录7.1 初始参数与第一轮观察我以一辆典型的Arduino 五路灰度传感器 差速驱动小车为例把一次完整的调参过程复盘给大家。初始参数是Kp0.1Ki0Kd0base_speed120PWM范围0到255。第一圈跑下来现象是直道勉强能走但方向修正很迟钝过第一个弯直接冲出赛道。误差已经到-2了但输出量只有0.1 * 2 0.2换算到PWM上才20对于120的基础速度来说这个修正量确实不够。于是Kp翻倍到0.2。7.2 围绕Kp和Kd的两个小时Kp0.2之后直道能走了但出现明显的S形摆动幅度大概有半个车身。这说明P增益已经偏大系统开始震荡。这时候我没有急于降Kp而是先把Kd加上去从0.8开始试。Kd0.8的第一圈直道摆动明显收敛但高速弯道出弯时有些“抢方向”我觉得是Kd偏大导致的过抑制。把Kd降到0.5过弯顺畅了很多。接着又发现一个大问题当小车走到十字路口四个传感器全压线时误差为0PID输出为0小车直接直行——这在普通路径上是没问题的但如果需要转弯的路径正好在十字路口逻辑就错了。这个在两轮差速小车上很好解决检测到全黑线时根据前后逻辑做一个延时直行或者设置一个“路口状态机”。如果是电磁车十字路口的信号处理又不一样后面我会单独讲。7.3 最终参数与稳定性验证最终这辆车在2m/s的直道上跑得很稳1.5m/s过90度弯也没问题参数定格在Kp0.18Ki0Kd0.45。关键是验证过程一定要多跑几圈——同一个赛道连续跑五圈每一圈走线都要一致说明参数稳定如果同一圈路线都飘来飘去说明某个环节还在变电池电压、传感器干扰、机械松旷都可能导致。8. 从灰度循迹到电磁循迹和摄像头循迹8.1 电磁循迹小车原理相同信号源不同说到电磁循迹很多玩摄像头车的朋友觉得是另一个技术路线其实只是误差的计算方式换了。电磁循迹通过电感线圈感应赛道中线下方导线的交变电流磁场读取ADC值来得到小车相对赛道中线的偏移量。误差计算的思路从“传感器压线状态”变成了“左右电感电压差”。核心公式一般是这样的error (left_adc - right_adc) / (left_adc right_adc)归一化之后误差范围在-1到1之间比灰度传感器的-2到2更平滑特别适合高速场景。调参逻辑和灰度车完全一样Kp给基础响应Kd抑制摆动Ki基本用不到。电磁循迹最大的坑在于感应电压的滤波和归一化。左右电感如果参数不完全一致差值本身就有偏移表现为小车在直道上也贴着一边走。解决办法是每次启动时做一个零点校准把直道上的误差偏移量记下来在误差计算里减掉。8.2 摄像头循迹小车把图像中线的偏差喂给PID摄像头循迹更偏计算机视觉但PID的部分反而更纯粹摄像头拍到的图像经过二值化、提取赛道边缘、计算中线得到“图像中线”和“图像中心”的像素差这个像素差就是PID的误差。误差值的单位从“传感器格数”变成了“像素数”。一套好的PID代码可以直接从灰度小车平移过来只需要把Get_Line_Error()改成Get_Camera_Error()就行。摄像头车的重点在于图像处理的帧率控制——如果处理一帧要80msPID控制周期最长只能到80ms那小车跑快了根本反应不过来。这时候要优先优化图像处理代码而不是调PID参数。8.3 起点线、十字路口和环岛PID不是万能的PID能解决“平滑跟随”的问题但解决不了“路径记忆”的问题。十字路口怎么选择直行还是转弯环岛进去容易出不来需要特殊逻辑。这些东西和PID无关属于状态机和路径规划层面的内容要在PID外层单独写逻辑。我的做法是维护一个TrackPhase枚举包含LINE_FOLLOW正常循迹、CROSS_INTERSECTION过十字路口、STOP_AT_START到起点线停车等状态。只有处在LINE_FOLLOW状态时才调用PID其他状态用预先写好的开环动作比如直行延时、强制转90度。9. 硬件选型和电源问题一次说清9.1 主控选型随传感器复杂度走纯灰度循迹Arduino Nano级别的板子就足够I/O口占用少AD采集也用不上。要做电磁循迹ADC通道至少需要一个采集速度最好能到1kHz以上STM32F103系列用起来很舒服。摄像头循迹就复杂了——如果只是简单的线性CCDSTM32勉强能带要是用OV7725这类摄像头输出图像强烈建议上带DSP的芯片或者直接用树莓派、OpenMV这类带图像处理的平台。9.2 电源是大家最爱忽略的坑电机驱动和单片机必须分开供电这是老生常谈但实际操作中还是很多人图省事用同一块电池。电机启动瞬间电流冲击大会让电压跌落单片机和传感器同时趴窝表现是大脚油门瞬间误差跳变、PID输出忽大忽小。我常用的方案是两节18650锂电串联7.4V给电机驱动板供电再经5V稳压模块给单片机、传感器单独供电。两个地线在驱动板附近单点连接能有效降低干扰。别小看这个细节它往往能解决你调参时遇到的“灵异现象”。9.3 传感器阈值校准的标准化流程灰度传感器在不同光线下、不同赛道上阈值完全不一样。到场地后第一步不是调PID而是校准传感器。标准流程是把小车放在黑线上记录五个ADC值。把小车放在白底上记录五个ADC值。阈值取两者的中间值或者存下每组黑白值在误差计算时做映射。这个校准一般在代码里用一个Calibrate()函数完成上电后小车自动转一圈采集数据再进入正常运行模式。自动化校准能省掉很多手忙脚乱。10. 关于PID调参这件事最后唠叨几句做了这么久的循迹小车我越来越觉得调PID像是跟车“对话”。每个参数调下去车都会用实际跑姿回应你——直道稳了还是抖了弯道顺了还是甩了都是信息。遇到问题先别慌着头疼医头把传感器、电源、机械、控制周期这些前置条件逐一确认了再回到参数上找原因。如果你刚开始接触建议用我的顺序先固定控制周期用Kp找到基本方向再用Kd消掉直道抖动最后才考虑加Ki。每调一轮都改一个参数、跑一圈、记录现象不要一次动两个参数否则出了问题你都不知道是哪个改坏的。调好一台车之后你收获的不只是“小车能跑了”这个结果更重要的是你真正理解了负反馈是怎么把一个不稳定的系统按在期望轨道上的。这套思路放到无人机、平衡车、工业控制上完全通用。这大概也是PID让这么多人着迷的原因——它简单到几行代码就能写出来又深刻到贯穿整个自动控制领域。