刚接触智能循迹小车那会儿,我把大半时间都花在调赛道上。代码里用了一堆if-else判断赛道类型,直道、弯道、十字、起跑线都能识别,小车也能跑起来,但走线奇丑无比——左一下右一下,整个人坐在地上看它跑都觉得上头,整个轨迹像条蛇在游。直到把控制部分从离散的阈值判断换成PID控制算法,车子才第一次做到了丝滑过弯。
这篇文章就把我在三轮电磁循迹小车上,从if-else迁移到PID闭环、再到把参数调到稳定跑完全程的完整过程记录下来,包括电感怎么排、偏差怎么算、增量式PID怎么写、双闭环怎么搭、参数怎么调,以及那些让我反复返工的坑。适合准备校内智能车竞赛、或者自己DIY循迹小车的朋友参考,哪怕你用的是红外循迹或摄像头循迹,控制层的思路也通用。
我写它的另一个原因,是发现很多人卡在同一个地方:PID公式背得滚瓜烂熟,但一上车就不知道怎么调,要么Kp拧太大整车抖成筛子,要么Kd给太猛电机啸叫。其实PID在循迹车上没有那么多玄学,它就是一套“根据偏差算控制量”的规则,你把它放到车的模型里去理解,参数自然就有谱了。
1. 先把控制对象想清楚:循迹小车到底在控什么
1.1 循迹系统是典型的负反馈结构
智能循迹小车的本质是一个移动机器人,放在控制论的框架里看,它是一个非常标准的负反馈系统。传感器负责测量“我离目标线有多远”,控制器负责根据这个偏差决定“往哪边打多少方向”,执行机构(电机和车轮)负责把方向指令转成真实的转向运动。
这里最关键的一点是:控制量必须是连续的。你不可能在高速状态下用三个离散挡位把车稳在赛道中央,就像开车时你不会把方向盘只握在“正、左满、右满”三个挡位一样。电磁三轮车的执行端是两个驱动电机加一个万向轮,靠左右轮差速实现转向。所谓“控制”,本质上就是在实时计算两轮之间的转速差,让车绕着一个假想的转向中心平滑地拐弯。
很多新手把重点放在“识别赛道”上,花大量时间优化图像识别或阈值判断,结果车还是跑不稳。原因就在于,识别只是感知层的事情,真正决定走线质量的是决策层的算法。识别做得再好,控制量的输出是跳变的,车身姿态就一定会跟着跳变。
1.2 阈值判断为什么不行:离散控制的典型缺陷
如果用一句话总结阈值控制的缺陷,就是“控制量不够细腻”。三段式控制大概长这样:
if (error > 0.2) { set_steer(LEFT_FULL); } else if (error < -0.2) { set_steer(RIGHT_FULL); } else { set_steer(STRAIGHT); }这段代码跑起来,车会不停地跨越中线,形成S形走线。原因很简单:当偏差从0.19变成0.21,控制量瞬间从“直行”跳到“左满转”,车身会猛地向左;然后偏差变成负的,又瞬间右满转。车的运动惯性没有变,控制量却在一格一格地跳,走线自然没法看。
高速状态下这个问题会被无限放大。车速越快,同样的控制周期内车走过的距离越长,状态切换的滞后越明显。你在低速时感觉还凑合,一旦把PWM拉上去,车几乎是必飞的。
对比一下离散控制和连续控制:
| 方案 | 使用的偏差信息 | 输出特征 | 典型效果 |
|---|---|---|---|
| 阈值判断 | 只有左/中/右几个区间 | 离散多档 | 蛇形走线,高速必飞 |
| 比例控制 | 连续偏差值 | 连续比例输出 | 平滑,但可能振荡 |
| PID控制 | 偏差及变化趋势 | 连续且带阻尼 | 平滑且稳定 |
1.3 为什么PID是循迹小车最合适的起点
PID适用于单输入单输出、可以被近似为线性的被控对象。电磁循迹小车的横向偏差和转向力矩之间,恰好可以近似成这种线性关系。更关键的是,PID不需要你精确建立车辆动力学模型,只需要根据实际响应去调三个参数,就能得到一个工程上足够好的控制器。
在智能车竞赛圈里,高阶的赛道队伍最终会转向LQR、模型预测控制或者模糊PID,但这些都建立在你对车辆模型足够理解的基础上。对绝大多数人,尤其是起步阶段的同学,PID就是性价比最高的方案:它只靠误差的现在、过去、未来三个信息,就能完成稳定的闭环控制。先把PID玩明白,后面想上更复杂的控制,你也有对比的基准。
2. 电磁三轮组的“感官系统”:电感布局与偏差计算
2.1 电磁检测原理:20kHz交变磁场下的感应电动势
电磁循迹的思路和红外循迹完全不同。赛道下方埋的是一根通有20kHz交变电流的导线,电流会在这根导线周围产生同频率的交变磁场。车头上安装的电感线圈相当于一个磁场探头,导线中的电流变化时,穿过线圈的磁通量也跟着变化,线圈两端就会感应出同频的电动势。
感应电动势的大小和线圈与导线的距离相关,离得越近信号越强。这个强度-距离关系大致呈1/d甚至1/d²的形式衰减,也就是说,电感离导线远一点,信号会迅速掉下来,这给“根据信号强度判断横向位置”提供了基础。
电感输出的原始信号是一个20kHz的交流小信号,电压幅度通常在几毫伏到几十毫伏,必须经过运放放大,再通过峰值检波电路整成直流电平,然后才能送进MCU的ADC采样。如果你直接在程序里采20kHz的交流波,采样率不够是采不到有效幅值的,所以硬件上一定补一个“信号调理”环节。这一步如果不做,后面算法再好也是白搭。
2.2 电感布局:水平三电感和八字方案怎么选
常见的电磁电感布局大概有三种:
- 水平三电感:车头中间一个、左右各一个,形成一组可以算左右偏差的三元组。结构最简单,适合入门。
- 水平五电感:在左右两边再各加一个更外侧的电感,提供更大的线性检测区间,同时方便做十字路口、起跑线等特殊元素的识别。
- 八字电感:把电感斜向放置(斜45度),利用磁场水平分量,可以在更高速度下提前感知弯道趋势,相当于给控制系统提供了一个“前瞻量”。
我自己用下来最顺手的是“水平三电感做主偏差计算 + 垂直八字电感做前瞻”的混合方案。水平三电感负责生成主控用的连续偏差,垂直电感的信号则被用来做弯道预判——在高速入弯前提前得到一个较小的修正量,让转向环提前动作,而不是到了弯心才反应。
这里有个特别重要的物理规律:电感信号强度随距离衰减极快,所以车上电感的安装位置稍微差一厘米,归一化之后的偏差曲线就会差不少。装车时固定好之后,轻易不要动机械结构,否则参数全部重新调。
2.3 偏差归一化:不做好这步,PID参数全是废的
拿到三个电感的ADC值之后,最大的忌讳就是直接拿原始值做差。为什么?因为电感的一致性、运放的放大倍数、电池电压波动,都会让原始ADC值在一个很宽的范围内变化。你今天在满电状态下标定好的阈值,跑半小时后电压掉了,同样的物理位置读出来的ADC可能差了20%,偏差量也跟着变了,PID参数自然不再匹配。
归一化的核心公式是:
err = (R - L) / (L + M + R);这个式子用四个通道总和做分母,把左右差变成“占总信号的百分比”。电池电压变化、整体信号同时增强或减弱时,偏差值基本保持不变。这样的偏差天然落在[-1,1]区间,负值偏左,正值偏右,后面PID参数的物理意义也就一目了然。
实际代码里要加一个保护:
float get_lateral_error(void) { uint16_t L = adc_read(CH_L); uint16_t M = adc_read(CH_M); uint16_t R = adc_read(CH_R); float sum = (float)L + M + R; if (sum < 1.0f) { return 0.0f; // 信号丢失时回中,避免除零 } float err = ((float)R - L) / sum; return err; }很多从阈值判断转过来的同学容易忽略归一化,直接把左右差值丢给PID,结果换了一组电池参数就变了。这个坑我踩过,印象极深。
2.4 执行端:两轮差速转向的映射关系
三轮电磁小车一般是一个万向轮加两个驱动轮,转向靠左右轮速差实现。假设转向环输出是steer,范围在[-1,1],那么映射到PWM上就是:
pwm_left = base_speed + steer * delta_max; pwm_right = base_speed - steer * delta_max;左边轮子快、右边轮子慢,车就往右偏,反之亦然。这里base_speed是基准速度,delta_max是最大差速量,具体数值取决于你的电机和驱动器的响应范围。后面说PID参数的时候,会再展开讲这个映射关系的标定。
3. PID三种作用在赛道上的真实行为:P、I、D分别什么时候出力
3.1 比例项:整个控制的“骨架”
比例项的输出是Kp乘以当前偏差,意思很直白:偏差大,就打大方向;偏差小,就轻打。这个环节决定了一个系统的响应速度。Kp太小,小车入弯时方向打得不够,会直直地冲出赛道;Kp太大,偏差只是稍微越线就会猛打方向,结果就是整车左右摇晃,跑起来像抽筋。
我手上那辆三轮车,归一化偏差范围在±1之间,转向输出叠加到PWM上的量级需要到±800左右。初期我把Kp设到2,直道没问题,入弯直接冲出去;调大到10,直道开始剧烈抖动;最后落在6左右才勉强能跑。这个例子说明,Kp的合适区间和你的执行机构行程有直接关系,不能照搬别人的参数。
比例项的缺点是它始终滞后于偏差:先有偏差,才有输出。想让车提前对弯道作出反应,就得靠微分项。
3.2 微分项:它不是在“刹车”,而是在“预判”
微分项看的是偏差的变化率,也就是e(t)比e(t-1)变化了多少。入弯时偏差开始变大,微分项给出一个“阻止偏差继续变大”的作用,看起来就像在踩刹车。出弯时偏差开始变小,微分项又会让转向平滑地回中,避免冲过头越过中线。
用开车来类比,比例项是你看到车偏了再打方向盘,微分项是你感觉到方向盘有自动回正的力矩,提前稳住方向盘。它给了系统阻尼,这也是为什么微分能显著抑制振荡。
但微分项有个臭名昭著的毛病:它是高频噪声放大器。传感器ADC值只要抖一点点,计算出的变化率就可能很大,转向输出跟着狂跳,电机发出细细的啸叫。所以D往往要配合滤波一起用,比如先对偏差做滑动平均,再进PID。控制周期也要稳定,否则上一次和这一次的时间间隔不是常数,求出来的变化率毫无意义。
3.3 积分项:在循迹小车上要省着用
积分项处理的是历史累计误差,把过去所有偏差加起来乘以Ki。它主要对付系统的固定偏置,比如传感器装歪了、一边轮子打滑率稍高,导致车一直偏着开,比例项拉不回来。
循迹小车有没有稳态误差?严格说长直道上可能会有。但从实际跑赛道的角度看,积分项带来的麻烦常常比好处大。为什么?因为积分是对整个过程的累积。入弯时偏差持续为0.5,积分器攒了一笔输出;出弯后偏差回到0附近,积分值还在逼着车继续转向,这就是过冲的来源。
所以我的做法是给积分加限制:一是积分限幅,只让积分输出不超过某个小值;二是条件积分,只有偏差绝对值小于某个阈值时才累计,大偏差时直接短路积分器。代码里看起来就是:
if (fabs(err) < 0.3f) { integral += err * dt; } else { integral = 0.0f; }顺便说个关联话题:同样的PID放在温度控制里,积分就变得极其重要。恒温箱控温时,如果偏差持续存在,积分就会一直加,直到加热功率把温度顶上去。温度系统是大惯性、大滞后对象,比例项单独工作会产生固定的稳态误差,只有靠积分才能消除。但循迹小车是一个快速运动的低惯性对象,它的误差一直在变,积分不是主角。同一个PID算法,在不同被控对象上,参数的权重完全不同,这才是“调参”背后的本质。
3.4 位置式还是增量式:我为什么选增量式
位置式PID每次输出的是绝对的控制器输出值。增量式PID每次输出的是在上一个控制量基础上的变化量。对电机这种“带积分性质”的执行机构来说,增量式更自然:我们只需要告诉电机“你再偏快一点”,而不是“你必须转到这个PWM值”。
增量式的核心代码不复杂:
typedef struct { float Kp, Ki, Kd; float err_last; float err_pre_last; float output; } IncrementalPID; float incremental_pid_update(IncrementalPID *pid, float err, float dt) { float delta = pid->Kp * (err - pid->err_last) + pid->Ki * err + pid->Kd * (err - 2.0f * pid->err_last + pid->err_pre_last); pid->output += delta; if (pid->output > LIMIT_MAX) pid->output = LIMIT_MAX; else if (pid->output < LIMIT_MIN) pid->output = LIMIT_MIN; pid->err_pre_last = pid->err_last; pid->err_last = err; return pid->output; }dt没有单独写进公式,是因为我把Kp、Ki、Kd都做成了“已经包含dt”的系数,固定控制周期运行时不需要每次再除一遍。增量式天然有抗饱和的好处:如果执行机构已经到头了,之后的增量不会让输出无限制增长。这也是它在电机控制里特别受欢迎的原因。
4. 双闭环架构:转向环和速度环应该先调谁
4.1 为什么单靠转向环过不了高速弯
只用转向环时,车速是开环的:给一个固定PWM,车速完全由电池电压和负载决定。问题是,同样的PWM,在直道上能跑出3m/s,在弯道上因为转向阻力,车速自己会掉下来。更关键的是,如果速度不随弯道曲率调整,转向环输出得再准,过弯时的横向加速度也会把车甩出去。
所以我的做法是加一个速度环,根据当前偏差实时调整目标速度:偏差大说明接近弯道,就减速;偏差小说明在直道,就恢复高速。这本质上是一个两级控制:外环决定“该跑多快”,内环决定“怎么跑到这个速度”。
4.2 速度环的反馈:编码器测速
三轮电磁车一般使用带编码器的直流减速电机。编码器输出的脉冲信号进MCU定时器,最常见的是M法测速:在固定时间窗口内统计脉冲个数,再换算成转速。
伪代码如下:
volatile uint16_t left_pulse_cnt; volatile uint16_t right_pulse_cnt; void TIM_MS_ISR(void) { left_speed_rpm = (uint32_t)left_pulse_cnt * 60000.0f / (1000 / TIM_MS) / PULSE_PER_REV; right_speed_rpm = (uint32_t)right_pulse_cnt * 60000.0f / (1000 / TIM_MS) / PULSE_PER_REV; left_pulse_cnt = 0; right_pulse_cnt = 0; }测速窗口需要跟控制周期匹配。我把速度环控制周期设在20ms,窗口太短时低速状态下窗口内可能只有几个脉冲,算出来的速度跳来跳去,速度环反而成了扰动源。测量数据稳定比实时性重要,这是速度环调好的前提。
4.3 目标速度随偏差动态调整
目标速度的生成逻辑不复杂,但需要实际标定:
target_speed = SPEED_MAX - k * fabs(lateral_error); if (target_speed < SPEED_MIN) target_speed = SPEED_MIN;k值的标定方法:让车在直道上跑到最大速度,然后在发卡弯前观察它降到多少才不会甩尾。我这边常用的配置是最大速度2.5m/s,偏差绝对值最大约1.0,k取0.8,弯道最低限速1.0m/s。如果你发现某个弯还是甩,就把SPEED_MIN再往下压。
4.4 级联PID的调参顺序:为什么先内环后外环
很多人一上来就调转向环,结果怎么调都感觉转向“发不上力”,其实原因是速度环还没收敛,车速本身就在振荡。正确顺序是先内环后外环。
先给速度环一组稳定的参数,让车速在阶跃目标下能快速平稳到达。在此基础上再调转向环,调起来会轻松很多。我在调速度环时有个技巧:把目标速度设成方波,通过串口把实际速度发到上位机,看它在100ms左右能不能跟住、有没有超调。速度环稳了,转向环的烦恼会消失一大半。
5. 参数整定实操:从振荡发散到丝滑过弯的完整过程
5.1 临界比例度法:先找到Kp的极限
PID调参第一步永远是只保留P。把Ki和Kd设为0,只加Kp,然后让车在长直道上跑,逐步增加Kp,观察车是否开始等幅振荡。找到临界点后,记录此时的Kp作为临界增益Ku,同时用上位机测量振荡周期Tu。
根据Ziegler-Nichols经验公式,可以得到一组初始参数:
| 控制器 | Kp | Ki | Kd |
|---|---|---|---|
| P | 0.5Ku | - | - |
| PI | 0.45Ku | 0.54Ku/Tu | - |
| PID | 0.6Ku | 1.2Ku/Tu | 0.075Ku*Tu |
注意这套经验公式来自过程控制,用在循迹小车上只是一个起点。我在车上按公式得到的参数通常会再缩小30%到50%,因为小车的被控对象比恒温箱轻快得多,反应也快得多。
5.2 用上位机波形做微调:偏差曲线是照妖镜
调参过程强烈建议把偏差和控制量通过串口实时发到上位机画波形。没有波形,全靠肉眼看车跑,基本等于盲调。
几个典型的排查方向:
- 偏差波形收敛过慢:加大Kp。
- 过中线后明显超调:加大Kd。
- 长时间等幅振荡:Kp太大,或者Kd不足。
- 波形上有高频毛刺:D太强,或ADC信号没滤波。
- 直线稳定但有固定偏移:加少量Ki,或者检查机械安装是否对称。
5.3 不同赛道元素的专项调参
直道的要求是“别乱动”,这一段偏差只有微小噪声,转向输出应该接近零。发卡弯是极限工况:偏差快速增大,P项要大胆输出,D项要足够压制回中过冲,同时速度环要提前降速。S弯的节奏很快,转向环必须响应灵敏,但又不能陷入振荡,最考验D的微调。
调S弯的时候,我经常用蓝牙把偏差波形传到手机上看,寻找那个“刚好平滑、没有毛刺”的D值。调完之后记住这组参数的感觉:直道安静、大弯有力、S弯轻快不抖,基本就是及格线。
5.4 一次完整的标定记录
拿我当时的整车参数举例:增量式PID,控制周期10ms,偏差范围[-1,1],转向输出范围限定在[-600,600],基准PWM为4000。
先只加P,Kp从1.0开始。1.5时直道正常但入弯冲线,2.5时直道开始摇晃,3.0时出现明显的等幅振荡。于是记录Ku=3.0,Tu=0.45s。按Ziegler-Nichols公式推出来Kp=1.8,实际再降一点到1.6。接着加Kd,从0.2开始试,到0.8时发现高频噪声被放大,退回0.6后弯道回中非常平滑。最后Ki只给了0.01,并且只在偏差小于0.3的时候才累计,用来纠正轻微的机械偏置。
这套参数最终跑下来的效果是:直道不抖、大弯不甩、S弯勉强稳定,圈速比之前if-else时代快了近一倍。这个提升主要不是参数有多神奇,而是连续控制本身比离散控制高一个维度。
6. 调试现场最常见的坑与我的排查记录
6.1 直道上的PWM毛刺
现象是直道明明应该直直地开,电机声音却忽高忽低。我先怀疑是电机驱动问题,换了一个还是这样。后来用上位机看转向输出,发现波形上叠着一层高频毛刺,判断是ADC信号抖动被D项放大。
解决办法分两步走:先对归一化偏差做一次滑动平均,窗口取5;再把D值从0.8降到0.6。毛刺立刻消失,走线也稳了。这个案例的教训是:D项要和信号滤波成对出现,否则它就是在帮你放大噪声。
6.2 出弯回不到中线:积分饱和背锅
有一次跑发卡弯,出弯之后车总是偏向外侧,要过好一阵才回来。我把偏差曲线和积分项一并导出才发现,入弯时偏差长时间为0.7,积分器攒了一大笔输出。出弯后偏差变成0.2,比例项已经要求小角度转向,积分项却还在逼着车继续打方向,结果车冲到外道。
解决方式是给积分输出限幅,限制在±30以内,并且只在偏差小于0.3时累计。这种条件下Ki才能稳定地发挥作用,而不是帮倒忙。
6.3 低速转向没反应、高速突然猛转:电机死区
直流电机存在死区:PWM低于某个阈值时,驱动力矩不足以克服静摩擦,车轮根本不转。转向输出叠加到基准PWM后,如果基准太小,左右轮差速在低速段根本拉不出有效转向;一旦越过死区,转速又会对PWM变化非常敏感,于是出现“没反应—突然猛转”的跳变。
处理办法有两个方向:一是把基准PWM保持在死区之上,宁可用速度环的限速去压低平均速度,也不要让PWM落入死区;二是在死区附近做分段补偿,给PWM加一个小的跳变补偿量,把非线性尽量修正成线性。
6.4 电池电压下降导致参数漂移
最容易被忽略的坑是电池电压下降。刚充满电跑得很好,跑了五分钟之后走线变差,这不是心理作用,而是锂电放电过程中电压下降,同样的PWM占空比下电机实际功率减了不少。开环PWM控制系统里,等效的“系统增益”变了,PID参数自然就不再最优。
解决这个问题的最优解是速度环闭环,让速度环自己通过编码器反馈去补偿电压波动。如果暂时没上速度环,可以做电压前馈补偿:采集电池电压,按V_ref/V_bat比例修正目标PWM。但说到底,双闭环才是根治办法,这也是我一再强调速度环重要性的原因。
最后再说一个习惯:每次调参之前,先把车架空垫起来,用上位机观察各个传感器信号和输出曲线是否合理,确认传感器是干净的、PID输出是光滑的,再下地跑。这样可以排除掉大量软硬件干扰。智能循迹小车这东西说难不难,说简单也不简单,关键是把每个环节拆开来看清楚。信号对了,模型清楚了,剩下的就是花点时间耐心拧参数。希望这篇记录能帮你少走弯路,早点让你的三轮车丝滑地跑起来。