巡线小车稳定循迹:从传感器标定到PID整定实战
1. 巡线小车真正的难点,从来不是“让它动起来”
很多人第一次做巡线小车,卡住的地方都非常相似:面包板插好、传感器接上、电机一转,小车在地上歪歪扭扭跑了两米,然后就再也推不动了。这个阶段的人往往会本能地怀疑硬件——是不是传感器不灵、是不是电机没劲、是不是电池不够。但把同样的硬件换一套代码,它又能跑得挺像回事。这就说明,巡线小车的瓶颈几乎从来不在“能不能动”,而在“能不能稳定地把位置误差变成正确的转向修正”。换句话说,这是一道控制题,不是一道接线题。
我这些年带过不少做循迹小车的同学,最常见的认知偏差是把巡线理解成一串 if-else:左边压线就往右拐,右边压线就往左拐。这种写法在低速下确实能跑,但一旦速度提上来,你就会看到小车在直道上左右画龙,在弯道口直接冲出去。原因很朴素——开关量只有“黑”和“白”两种状态,它丢掉了“偏了多少”这个信息。控制器的输入如果是离散的、跳变的,输出就必然是抖动的。所以从第一行代码开始,就应该把巡线当成一个连续的误差闭环来对待,传感器给出的是位置偏移量,控制器给出的是转向修正量,中间那段才是真正需要花时间打磨的东西。
这篇内容面向的是已经把车搭起来、或者正准备动手搭一台的人,也适合那些车能跑但想跑得更快更稳的人。我会把整条链路拆开讲:从主控和传感器的选型依据,到动力系统的估算方法,再到怎么把五路开关量变成连续误差,最后是 PID 的具体调试顺序和实测排错。每一个选择我都会说清楚为什么这么选,因为照着别人的清单买件,最后大概率还是调不出来,只有理解了取舍逻辑,你才能在缺件、换件、改赛道的时候自己拿主意。
先交代一下我这次用的配置基线,后面所有的参数都是围绕它来的:两轮差速驱动加一个万向轮,底盘尺寸大约 18cm×12cm,五路红外反射式传感器横向排布,主控用常见的 Arduino 兼容板,电机是带减速箱的直流减速电机,驱动用双路 H 桥模块,电源是两节锂电芯串联。这套配置不算高端,但足够把 PID 的每一个细节都跑明白,而且成本可控,改起来也不心疼。
2. 主控与传感器的选型,要从“误差分辨率”往回推
2.1 主控板选型里最容易被忽略的三个实际约束
选主控的时候,新手最容易只看“引脚够不够”。引脚当然要看,但真正决定你调试体验的是另外三件事。第一是模数转换的通道数和精度,如果你打算用模拟量输出的传感器做归一化处理,那至少要留出五路 ADC;十位精度意味着把参考电压切成 1024 份,在 5V 参考下每份大约 4.9mV,对于反射式传感器来说这个分辨率是够用的,但如果你的传感器输出摆幅只有 1V 左右,那实际有效分辨率就掉到两百多份,噪声就会很明显。第二是是否支持硬件串口和外部中断,串口用来实时回传误差曲线,中断用来接编码器测速,这两个在后期提速时是刚需。第三是供电与逻辑电平的匹配,很多板子标称输入 7 到 12V,但板载稳压芯片在电机启动瞬间的压降会让主控复位,这个坑后面会单独讲。
如果你的目标只是跑通巡线和 PID,常见的八位主控完全够用,因为循迹的运算量非常小:一次循环就是五路采样、一次加权、一次 PID 计算、两路 PWM 输出,整个周期控制在 2 到 5 毫秒毫无压力。真正吃资源的是附加功能,比如编码器测速、OLED 显示、蓝牙调参。我的建议是先按最小系统把车跑稳,附加功能一律留到参数定型之后再加,否则你调参的时候永远分不清是参数不对还是显示刷新拖慢了主循环。
2.2 红外反射式传感器的工作边界与布置间距
红外反射式传感器的原理很简单:一对红外发射管和接收管朝下,遇到白底反射强、接收管导通程度高,遇到黑线反射弱、接收管几乎不导通。它的优点是便宜、响应快、电路简单,缺点是对环境和高度极其敏感。我在室内白炽灯下和窗边自然光下测同一块场地,同一组阈值能差出 200 多个计数,这就是为什么很多人换个地方车就不好使了。
安装高度是第一个必须实测的量。以常见的 TCRT5000 类模块为例,标称检测距离是毫米级,我实测下来 4 到 6mm 是一个比较舒服的区间,低于 3mm 容易刮地,高于 8mm 之后黑白差值会明显缩小,抗干扰能力直线下降。你可以这样定:把车放在黑线上,串口打印每一路的原始值,然后一点点垫高传感器,观察黑白差值,选一个差值最大且车体不会磕地的高度锁死。这个动作花十分钟,能省掉后面几小时的玄学调参。
横向间距要跟你的赛道线宽匹配。常见黑线宽度是 18 到 20mm,如果五路探头等间距排布,我一般让相邻探头中心距在 8 到 10mm,这样总跨度大约 32 到 40mm,正常压在线上时中央那一路必然读到黑,稍微偏一点就变成两路同时读到黑,误差信号不会出现“全白”的空档。间距太大,中间会出现整段全白的死区,车一走直线就抖;间距太小,五路区分度不够,误差分辨率白白浪费。这里有个简单的验证方法:把车横着放在黑线上,手动左右平移,同时看串口输出的位置误差,理想情况是误差随平移单调、连续地变化,如果中间出现跳变或者平台,就说明间距或者阈值需要调。
2.3 数字量输出与模拟量输出的取舍
很多模块同时提供数字输出和模拟输出。数字输出省事,板载电位器调好阈值,读到高电平就是白、低电平就是黑。但我在实际调试里几乎不用数字输出,原因有两个:一是数字输出的阈值是全局共享的,五路模块各自调电位器,一致性很难保证,稍微有点灰尘或者光照变化,某一路就比别人早触发;二是数字输出丢掉了“黑的程度”这个中间信息,做不了归一化和自适应阈值。
模拟输出虽然需要多占 ADC 通道,但它能让你在软件里做很多补救。比如可以给每一路单独标定黑白基准值,做 min-max 归一化,把不同批次、不同老化程度的传感器拉到同一个尺度上。再比如可以计算整条传感器组的“总黑度”,用它来判断当前是压线、半压线还是完全丢线。这些在只有开关量的时候是无从下手的。所以我的一般做法是:买带模拟输出的模块,把数字输出脚悬空不用,全部走 ADC。多花的那点接线时间,在调参阶段会成倍还回来。
2.4 传感器前瞻距离对操控稳定性的影响
传感器的纵向位置(也就是前瞻距离)经常被忽视,但它对高速稳定性影响很大。传感器装在车头最前端,车看到弯道的时候已经贴到弯道口了,转向来不及;装在太靠后的位置,前瞻足够但转向的几何关系会变得别扭。我的经验是让传感器排的中心线到驱动轮轴心的水平距离落在 60 到 90mm 之间,具体数值取决于你的车长和最高速度。
可以用一个粗略的估算来理解:假设车速是 0.8m/s,主循环周期 5ms,那么每个控制周期车前进 4mm。如果前瞻是 70mm,从“看到弯”到“轮子到达弯道”大约有 17 个控制周期,这给了控制器足够的反应窗口。如果前瞻只有 30mm,那只有 7 个周期,PID 的微分项还没来得及起作用车就冲出去了。当然前瞻也不是越大越好,太大之后传感器容易同时压到相邻赛道的线,尤其在密集弯道里会误判。
3. 动力系统:先用估算把电机的量级定下来
3.1 用中学物理估算需要的扭矩
选电机不要凭感觉,一个很粗的估算就能帮你避开大坑。假设整车质量 0.5kg,驱动轮直径 42mm(半径 0.021m),你希望小车从静止开始能有 1m/s² 的加速度。根据 F=ma,需要的推力是 0.5N,两轮分担,每轮 0.25N。轮上扭矩等于力乘半径,也就是 0.25×0.021≈0.00525 N·m,约等于 5.25 mN·m。而常见的带减速箱直流电机,输出扭矩通常在 15 到 30 mN·m 这个量级,看起来余量很大。
但别忘了两个现实因素:一是电机标称扭矩往往是堵转或者接近堵转时的值,实际工作点远低于它;二是巡线小车需要频繁加减速和差速转向,转向时一侧轮子要反转,负载会明显上升。所以我的经验是按估算值的 3 到 5 倍留余量,也就是选 20 到 25 mN·m 左右的减速电机比较稳妥。减速比方面,巡线不需要很高的极速,但需要好的低速线性,减速比在 1:30 到 1:50 之间是比较常见的甜点区,转速大约几百转每分钟,配合 42mm 轮径,线速度在 0.5 到 1.2m/s,正好是室内赛道的合适范围。
3.2 驱动模块选型看三个硬指标
驱动板的选择上,我只看三件事。第一是持续电流能力,注意是持续不是峰值,很多模块标着峰值 2A 但持续只有 1A 且不带散热片,跑几分钟就烫手降频。你的电机堵转电流大概是多少,一定要查一下,驱动板的持续电流至少要是它的 1.5 倍。第二是是否支持 PWM 调速且频率合理,PWM 频率太低电机会啸叫、低速抖动明显,太高又有开关损耗,常见的几 kHz 到十几 kHz 都可以接受。第三是控制逻辑是否清晰,有没有独立的使能脚、方向脚,能不能直接吃 3.3V 或 5V 的逻辑电平。
我个人偏向用双路 H 桥集成模块,接线少、体积小,但一定要看清它有没有内置续流二极管和过流保护。如果没有,电机启停瞬间的反电动势很容易把驱动打穿,尤其是在你反复快速改变占空比的时候。这一条我吃过亏,后面会讲。
3.3 供电、共地与那次烧板的复盘
这一段我讲个真实的教训。早期我做的一台车,主控和电机共用一组电池,主控通过板载稳压取电,电机直接接电池。静态测试一切正常,一上赛道跑十几秒主控就复位,车像抽风一样乱冲。当时怀疑过程序跑飞、怀疑过传感器干扰,最后用示波器看电源轨才发现:电机启动瞬间电池电压从 8.2V 掉到 6V 出头,板载稳压的输出跟着塌了一下,主控欠压复位。
解决思路很直接,但要按顺序来。第一,主控和电机尽量分开供电,比如电机用一组电池,主控单独用一组或者经过一个独立的升压/降压模块。第二,如果必须共用,一定要加大容量的电解电容做储能缓冲,我在电机电源两端并了 470μF 加上 0.1μF 的陶瓷电容,电压跌落明显变缓。第三,共地必须做,但要走对路径,所有地线最终汇聚到一个点,电机的大电流回路不要和传感器的信号地串在同一条细线上,否则电机的开关噪声会通过地线串进 ADC,读数乱跳。第四,给电机两端各并一个 0.1μF 的去耦电容,能明显压掉换向火花带来的高频干扰。
另外提醒一句,接线的时候务必断电操作,尤其是带电插拔电机线。我有一次在通电状态下拔电机插头,电感断路的瞬间反压直接把驱动模块送走了。这类小事故看着不起眼,但它会打断你整个调试节奏。
4. 把五路开关量变成一个连续误差量
4.1 为什么必须先做归一化
传感器读回来的原始值受很多因素影响:每个探头的发光强度不完全一致、探头离地高度有微小差异、地面材质和光照在变。如果直接拿原始值算误差,就等于把所有这些不一致都算进了控制器的输入里,PID 调出来的参数只对这一台车这一个环境有效。
归一化的做法很朴素:对每一路单独标定一个“全白值”和一个“全黑值”,然后把当前读数映射到 0 到 1000 的区间。全白值怎么取?把车放在纯白地面上,读若干次取平均;全黑值怎么取?把车放在黑线上让探头正对黑线,读平均。实际做的时候还要留一点余量,比如全白值往上加 5%,全黑值往下减 5%,避免边界抖动。
// 每一路单独标定,数组下标对应传感器编号 const int WHITE_VAL[5] = { 812, 798, 823, 806, 815 }; const int BLACK_VAL[5] = { 118, 132, 105, 126, 112 }; int normalize(int idx, int raw) { int v = (raw - BLACK_VAL[idx]) * 1000L / (WHITE_VAL[idx] - BLACK_VAL[idx]); if (v < 0) v = 0; if (v > 1000) v = 1000; return v; // 0 表示全黑,1000 表示全白 }标定这一步强烈建议做成一次性工具:串口打印每一路的黑白值,你把车举起来在白纸和黑线上各放一次,把数字抄下来填进数组。花五分钟,换来的是后面所有参数的可移植性。
4.2 加权平均法:从五路读数算出位置偏差
归一化之后,把每一路的“黑度”当成权重,乘以它对应的位置系数,再除以权重总和,得到的就是传感器组相对于黑线的质心位置。位置系数的取法有讲究,我用的是对称的等距取值,比如 {-2000, -1000, 0, 1000, 2000},单位是“微位置”,实际物理意义就是探头间距的千分之一。误差为正表示线在车的右侧,误差为负表示线在左侧。
const int POS_WEIGHT[5] = { -2000, -1000, 0, 1000, 2000 }; float readLineError(bool &lost) { long sumW = 0, sumWV = 0; for (int i = 0; i < 5; i++) { int raw = analogRead(SENSOR_PIN[i]); int dark = 1000 - normalize(i, raw); // 转成黑度:0 白,1000 黑 sumW += dark; sumWV += (long)dark * POS_WEIGHT[i]; } if (sumW < 800) { // 总黑度太低,判定丢线 lost = true; return 0; } lost = false; return (float)sumWV / sumW; }这个写法的好处是误差连续。当黑线正好压在中间探头上,误差接近 0;当黑线移到中间和右二之间,两路同时有读数,误差就会平滑地在 0 和 1000 之间滑动。这正是 PID 需要的输入形态。相比之下,if-else 写法给出的永远是三类离散值,微分项根本没法算。
4.3 总黑度这个副产物其实很有用
上面代码里的sumW就是我说的总黑度,它是一个非常好用的辅助信号。正常压线时它大概在 1000 到 2500 之间(取决于线宽和探头间距),完全偏离赛道掉到全白时会掉到几百以下,遇到十字路口或者起跑线这种大面积黑区时会飙到 4000 以上。你可以用它做三件事:判断丢线、识别路口、以及在起跑线做停车或者计数。
丢线处理是新手最容易写崩的地方。很多人的做法是丢线就原地停车,结果车在弯道出口稍微偏一点就停死,比赛里直接报废。我的做法是丢线时保持上一次的转向方向继续修,同时降低速度,给车一个“找回来”的机会。更稳妥一点的做法是记住丢线前最后一次有效的误差符号,让车朝那个方向多打一点舵,相当于凭记忆找线。
float lastError = 0; // ... float err = readLineError(lost); if (lost) { err = (lastError > 0) ? 2500 : -2500; // 朝丢线方向满舵找线 baseSpeed = baseSpeed * 0.6; // 同时降速 } else { lastError = err; }5. PID 调试:两小时能走完的实操流程
5.1 位置式 PID 的代码骨架
巡线用的是位置式 PID,输出的是转向修正量。控制目标是让位置误差趋近于零,也就是让黑线的质心回到传感器组的中心。代码结构很标准,但有几个细节必须注意:积分项要限幅并且做抗饱和,微分项要用误差的差分而不是测量值的差分(这里两者等价,因为设定值恒为 0),输出要给一个明确的上下限,最后左右轮的速度要分别限幅到 PWM 的合法区间。
float Kp = 0.35, Ki = 0.0, Kd = 2.5; float integral = 0, lastErr = 0; const float I_LIMIT = 3000; int computeSteer(float err, float dt) { integral += err * dt; if (integral > I_LIMIT) integral = I_LIMIT; if (integral < -I_LIMIT) integral = -I_LIMIT; float derivative = (err - lastErr) / dt; lastErr = err; float out = Kp * err + Ki * integral + Kd * derivative; if (out > 255) out = 255; if (out < -255) out = -255; return (int)out; }注意dt必须是真实的循环周期,不能当成常数 1 随便写。如果你在主循环里加了串口打印或者显示刷新,周期就会变,微分的量纲跟着变,参数变得毫无意义。我的做法是用millis()算实际间隔,同时把控制循环和串口打印解耦,比如每 10 个控制周期才打印一次。
左右轮的赋值也很关键,我一般用left = base - steer, right = base + steer的对称差速,然后对两轮分别做 0 到 255 的限幅。如果不限幅,转向修正大的时候一侧轮子会算出负数,PWM 写入负值的行为依赖库实现,很容易出现“该反转却停住”的怪现象。
5.2 先 P、再 D、最后才考虑 I 的顺序不能乱
调参顺序是这件事里最重要的经验,没有之一。我见过太多人一上来三个参数一起试,结果调了两天也不知道是哪个在起作用。正确的顺序是这样的:
第一步,只留 P,让 Ki=0、Kd=0。从小往大加 Kp,观察小车的反应。Kp 太小,车会反应迟钝,过弯时贴着外沿慢慢蹭出去;Kp 逐渐加大,车开始能咬住线,但直道上会左右小幅摆动;继续加大,摆动幅度变大、频率变高,最后变成剧烈画龙甚至原地抖。记住那个刚开始出现明显摆动的 Kp 值,然后把 Kp 退回到它的 60% 到 70%,这就是你的 P 基线。
第二步,加 D,抑制摆动。Kd 的作用是“看到误差变化太快就提前刹车”。从 0 开始一点点加,你会看到直道上的画龙幅度逐渐变小,车变得“稳”。但 Kd 加过头会有副作用:车对噪声变得极其敏感,传感器读数稍微抖一下,输出就猛跳,表现为电机发出高频的“哒哒”声、车身轻微震颤。听到这个声音就说明 Kd 太大了,往回退。
第三步,I 一般不用。巡线这个场景里,误差的来源主要是几何关系,不是稳态偏差,所以积分项在绝大多数情况下只会帮倒忙——它会在长弯道里累积,导致出弯时严重过冲。我个人的默认设置就是 Ki=0,只有在赛道有长距离缓弯、而且你确认车存在持续的固定偏置(比如某个电机天生慢一点)时,才考虑加一点点 I,而且要配上严格的限幅。
| 阶段 | 目标参数 | 观察现象 | 调整动作 |
|---|---|---|---|
| P 粗调 | Ki=0, Kd=0 | 直道轻微摆动 | Kp 加到明显摆动后退回 65% |
| D 抑制 | Kp 固定 | 摆动收敛但出现震颤 | Kd 加到震颤临界后退回 70% |
| I 微调 | Kp、Kd 固定 | 长缓弯有固定偏差 | Ki 从极小值起,严格限幅 |
| 整体复核 | 三者固定 | 全赛道跑三圈 | 记录脱线点,针对性微调 |
5.3 用串口曲线代替肉眼判断
肉眼看车摆动只能看出“晃得厉害”或者“还行”,判断不了相位和趋势。真正有效的做法是把误差和输出打到串口,用绘图工具看波形。我在循环里每 20ms 输出一行误差,转向量,左轮,右轮,然后看三条曲线的相位关系。
健康的状态是:误差曲线在零线附近小幅波动,转向量曲线与误差曲线同相但略微超前(这是 D 项在起作用)。如果误差曲线的摆动幅度越来越大,说明系统在发散,Kp 或 Kd 有一个过头了;如果误差曲线始终偏向一侧不回来,说明存在稳态偏差,要么是机械不对中,要么是传感器标定有问题;如果转向量曲线出现高频毛刺,说明 D 项放大了噪声,需要降低 Kd 或者给误差加一个简单的一阶低通滤波。
一阶滤波的实现很简单,一行代码的事:filtErr = filtErr * 0.7 + err * 0.3;。系数越大越平滑但延迟越大,0.6 到 0.8 是比较常用的范围。加滤波之后 PID 用的是滤波后的误差,但要记得lastErr也要跟着用滤波值,否则微分项会前后不一致。
5.4 常见波形与对应病灶的对照
调参调不下去的时候,对着波形找病灶比盲试快得多。我整理了几种典型现象和对应的处理方向,这张表在我自己的调试笔记里翻用率最高。
| 波形/现象 | 最可能的原因 | 优先处理 |
|---|---|---|
| 误差在零附近高频抖动 | 传感器噪声或阈值临界 | 加滤波、重标定黑白值 |
| 误差缓慢单向漂移 | 两电机特性不一致 | 单独做速度补偿系数 |
| 过弯时误差冲过零后大幅反弹 | Kd 不足或 Kp 过大 | 先加 Kd,再回头降 Kp |
| 直道周期性左右摆 | Kp 接近临界振荡 | Kp 退回 60% |
| 出弯后长时间不回中 | 积分饱和 | 关掉 Ki 或减小限幅 |
| 输出饱和但误差不降 | 基础速度过高 | 降速或加大转向权重 |
6. 实车跑起来之后的四类疑难杂症排查链路
6.1 直道画龙:先分清是机械问题还是参数问题
车在直道上左右摆,第一反应往往是去调 Kp,但我在实际排查里发现,有相当一部分画龙是机械原因。判断方法很简单:把车架空,让两轮以相同占空比转,看转速是否一致;再把车放在直线上,把 PID 关掉、只给固定直行速度,看它跑一段之后偏向哪边。如果固定直行就明显跑偏,那说明两轮的有效转速不同,这时候你把 Kp 调到天亮也调不好。
机械层面的原因主要有三个:轮胎和地面的摩擦不均匀、某一侧减速箱阻力偏大、整车重心没有落在两轮中轴线上。前两个可以通过给慢的那一侧乘一个补偿系数解决,比如rightPWM = rightPWM * 0.96,系数靠实测直行偏移量反复微调。第三个要动结构,把电池往中间挪,或者给重的一侧加配重。
确认机械没问题之后,再回到参数。直道画龙的典型特征是摆动频率固定、幅度稳定不发散,这通常是 Kp 偏高但还没到振荡临界,配合 Kd 增加阻尼就能压下去。如果摆动幅度在逐渐变大,那就是真的在发散,必须立刻降 Kp。
6.2 出弯脱线:多半是前瞻和速度不匹配
车能进弯但出不来,是最让人抓狂的一种。我的排查顺序是这样的:先看是不是丢线判定太激进。很多人的丢线阈值设得太高,车在弯道里稍微偏一点,总黑度掉到阈值以下,控制器直接进入丢线保护或者停车。把阈值调低,让它在半压线时仍然能算出有效的质心误差,往往就能救回来。
再看是不是速度太快。用一个很笨但有效的办法:把基础速度降到原来的一半,如果出弯正常了,那就是速度问题。这时候有两个方向,一是继续降速,二是加弯道降速逻辑——用误差绝对值或者总黑度判断当前是不是在弯里,弯里自动把基础速度打个七折,出弯再恢复。这个逻辑比硬调 PID 有效得多,也是实际比赛里最常用的手段。
最后才怀疑 PID。出弯脱线如果伴随明显的过冲(车冲到弯道外侧再往回拉),那是 Kp 太大或者 Kd 不够;如果是慢吞吞地跟不上弯,那是 Kp 太小。这两种在波形上很好区分。
6.3 两个电机不一致带来的隐性偏移
前面提过电机补偿,这里展开说。即使是同批次电机,减速箱的摩擦、齿轮间隙、绕组电阻都会有差异,表现出来就是同样的 PWM 下转速不同。这种差异在低速时影响不大,但速度提上来之后会变成一个明显的恒定偏置,逼着 PID 的积分项去补,而积分项一补就容易在弯道里过冲。
我的处理办法是在底层做一次标定:让车直行 2 米,量它偏离中线的距离,然后按比例调整补偿系数,反复两三次把偏移压到 1cm 以内。这个动作是一次性的,做完之后 PID 的负担会明显减轻。补偿系数不要设得太极端,超过 0.9 到 1.1 这个范围就说明电机差异太大了,换电机比硬调更省事。
// 标定得到的左右轮补偿系数 const float TRIM_L = 1.00; const float TRIM_R = 0.96; int lp = constrain((int)((base - steer) * TRIM_L), 0, 255); int rp = constrain((int)((base + steer) * TRIM_R), 0, 255);6.4 电池电压下来之后参数全变了
这个坑非常隐蔽。满电的时候车跑得好好的,跑三五分钟之后开始画龙、开始脱线,你以为是参数漂了,其实是因为电池电压下降,同样的 PWM 占空比对应的实际转速变了,整车的动态特性跟着变。镍氢和锂电的放电曲线不同,锂电在前段比较平,到后面掉得快。
应对手段有两个层面。硬件上,尽量用稳压输出给电机驱动,或者至少在电池电压范围内做一次软件补偿——用分压电阻测电池电压,把 PWM 输出按电压比例缩放,让“有效驱动电压”保持恒定。软件上,把基础速度设得保守一点,留出足够的余量,让参数在电压波动范围内都能工作。我个人的习惯是把满电时的速度设为赛道需要的 85%,这样掉压之后仍然够用,参数漂移也不明显。
7. 速度提上去之后,还需要补的三件事
7.1 转向量的限幅比想象中重要
PID 输出直接给差速,在误差很大的时候(比如刚起跑偏了一大截,或者刚找回线)转向量会顶到限幅,一侧轮子接近停转,车会猛地一顿。这个突变对机械结构和传感器读数都不友好,容易引发二次摆动。解决办法是给转向量加一个变化率限制,也就是限幅斜率:每个周期允许的转向量变化不超过某个值,让车“平滑地打舵”。
int steerTarget = computeSteer(err, dt); static int steerNow = 0; const int SLEW = 12; // 每周期最大变化量 if (steerTarget - steerNow > SLEW) steerNow += SLEW; else if (steerTarget - steerNow < -SLEW) steerNow -= SLEW; else steerNow = steerTarget;SLEW 的取值跟你的控制周期有关,周期 5ms 的话 10 到 15 比较合适,太大起不到平滑作用,太小会让车反应变迟钝。
7.2 用误差大小做速度分级
恒定速度跑全赛道是很吃亏的,直道上浪费了时间,弯道里又不够稳。我一般做三档:误差绝对值小于 500 认为是直道,给满速;500 到 1500 是中弯,给 80%;大于 1500 或者判定丢线,给 60%。这个分级不需要很精确,但它能显著降低脱线率,同时把平均圈速提上来。
要注意的是速度切换本身也要平滑,不能一下子从 100% 跳到 60%,否则车会点头。用一个简单的斜坡函数,每个周期最多降 3 个 PWM 单位,加速同理。
7.3 参数是跟赛道绑定的,别指望一套打天下
最后说个心态问题。PID 参数没有“最优解”,只有“对当前这台车、这条赛道、这个速度最优”。线宽从 18mm 换成 25mm、赛道从哑光纸换成亮面布、环境光从暗变亮,参数都得重新看一遍。所以我建议你把标定流程固化成一套动作:先标传感器黑白值,再标电机补偿,然后 P、D 顺序走一遍,最后用串口波形复核。这套流程走完大概二十分钟,比抱着旧参数硬调快得多。
我自己的习惯是在代码里留一组宏,把不同赛道的参数分文件存着,换场地直接改宏定义重新编译。听上去有点土,但比赛现场那种环境下,能少一次现场调参就少一次翻车的机会。
最后分享一个我踩过好几次坑才养成的习惯:每次改参数只改一个,改完立刻跑三圈,把圈速和脱线位置记在纸上。凭记忆调参是这件事里最大的敌人,因为人对“上一版好像更好一点”的判断极不可靠。有了记录,你至少能保证每次都在往好的方向走,而不是在两个参数之间来回打转。