📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列(共 12 篇)第 5 篇。
上一篇《电机驱动与电源地基》解决的是”小车的腿能不能听话地动起来”。可问题来了:腿动得再稳,要是脑子不知道”我现在偏左了还是偏右了””我跑多快””车头朝哪个方向”,那也只是一辆瞎跑的车。
这一篇我们就来给小车装上四样”感官”——灰度、电磁、编码器、IMU。它们的共同任务只有一个:给后面的控制环(PID)送上一份又干净又准确的反馈信号。打个比方,PID 是司机,感知层就是司机的眼睛、耳朵和平衡感。眼睛进了沙子,再老练的司机也得撞墙。所以这一篇虽然不讲控制算法,却是整车成败的地基里非常硬核的一块。
我们分四块讲:循迹的两条路线(灰度 / 电磁)、测速度的编码器、测姿态的 IMU。每一块都给你能照抄的算法和代码,最后说一句”路上怎么在它们之间切换”。
一、循迹路线一:灰度——小车最常用的”看路”方式
灰度循迹是绝大多数电赛/智能车小车的第一选择:在车头底部横排一组朝下的光电/灰度传感器,地面是白底黑线,传感器看到黑线就报告”我这儿压线了”。把所有传感器的信息汇总,就能算出”黑线现在偏车身中线多远”。这个偏差,就是喂给转向 PID 的核心输入。
数字量阵列:加权位置法(行业标准算法)
最经典、被全网开源项目反复抄的算法,叫加权位置法,来自 Pololu 的 QTR 传感器库 readLine 算法。它的思路特别好懂:
🧩 比喻:同学按学号站一排报到
把 8 个传感器想象成班里 8 个同学,按学号(0~7)站成一排。谁看到黑线谁”举手”,举手的力度(传感器读数)当权重。我们把”学号 × 力度”全加起来,再除以”总力度”,得到的就是一个平均学号——黑线现在大概压在第几号同学附近。比起只数”有没有人举手”,这能算出黑线卡在两个同学中间的位置,精细得多。
写成公式就是:
$$\text{position} = \frac{\sum_{i} (i \times 1000) \times value_i}{\sum_{i} value_i}$$
其中 $i$ 是传感器索引。对 8 路传感器,这个值的范围是 $0 \sim 7000$,正中心就是 $(8-1)\times500 = 3500$。这个数随黑线左右移动单调变化,直接拿来当 PID 输入:设定值 setpoint = 3500,误差 error = position − 3500。
下面是改写自 QTR 库的核心代码,数字量、模拟量阵列都能用:
// N 路灰度阵列加权位置法,返回 0~(N-1)*1000,中心 (N-1)*500
// value[i]: 第 i 路校准后读数(压线=高值),已做归一化
uint16_t readLinePosition(uint16_t *value, uint8_t N) {
uint32_t avg = 0; // 加权和 Σ(i*1000*value)
uint32_t sum = 0; // 权重和 Σ(value)
static uint16_t lastPos = 0;
bool onLine = false;
for (uint8_t i = 0; i < N; i++) {
uint16_t v = value[i];
if (v > 200) onLine = true; // 判定压线
if (v > 50) { // 噪声门限,>50 才计入加权
avg += (uint32_t)v * (i * 1000);
sum += v;
}
}
if (!onLine) { // 全丢线:朝上次方向找线(下面细讲)
if (lastPos < (uint32_t)(N - 1) * 1000 / 2) return 0;
else return (N - 1) * 1000;
}
lastPos = avg / sum;
return lastPos; // PID 输入:error = lastPos - (N-1)*500
}
注意里面两个门限:读数 >50 才计入加权和(滤掉噪声),>200 才算”真的压线了”。这是 QTR 库的经验值,能让位置计算更干净。
数字量 vs 模拟量:要不要那么精细?
刚才代码里的 value[i] 可以是两种来源:
| 类型 | 怎么读 | 优点 | 缺点 |
|---|---|---|---|
| 数字量 | 每路经比较器输出 0/1(压线/没压) | 简单、直接读 IO、抗环境光稍好、运算量小 | 分辨率低,偏差呈阶梯状 |
| 模拟量 | 直接读 ADC 原值 | 分辨率高,能算出黑线在两路中间的”亚像素”位置 | 运算量大,更怕环境光和反光 |
💡 比喻
数字量像只问”踩到线了吗?是/否”;模拟量像问”你离线多近?打个 0~100 分”。后者能算出更细的偏差,但也更容易被路灯、反光骗到。
模拟量有个必做的预处理:归一化。每一路传感器单独记下自己在纯黑、纯白时的最大最小值,运行时把读数折算到 0~100。这能消除各路传感器的个体差异,换场地也只要重测一遍。
// 标定阶段:扫黑白记录每路 min[i]/max[i]
// 运行阶段:归一化到 0~100
uint8_t normalize(uint16_t raw, uint16_t mn, uint16_t mx) {
if (raw <= mn) return 0;
if (raw >= mx) return 100;
return (uint8_t)((uint32_t)(raw - mn) * 100 / (mx - mn));
}
工程四件套:”实验室能跑、赛场翻车”的分水岭
灰度循迹真正拉开差距的,不是那个加权公式,而是下面这四件事。
① 黑白阈值,一定要现场标定。 别用上次的、别用实验室的。赛场的光照、地面材质、纸张反光都和你家不一样,固定阈值一换场就误判。
⚠️ 2022 年那届的"翻车现场"
那年好几支队伍的小车在赛场识别/循迹翻车——实验室明明能跑,一到赛场就不行。根因几乎都是同一个:赛场环境和实验室差太多,参数没现场重标。(来源)
基础做法:上电后让阵列扫过真实赛道的纯黑线和纯白底,采集每路 min/max,阈值取中值。
进阶一点,可以上 OTSU 大津法做动态阈值,免去手动标定:
// 非线性映射,增强黑线区分度(减小传感器个体差异)
adc[a] = pow((adc[a] / 10.0), 3) / 15000;
// OTSU:遍历灰度级找"最大类间方差"对应的灰度作阈值
// 类间方差 = p0 * p1 * (m0 - m1)^2 (p0/p1 两类概率,m0/m1 两类均值)
// 阈值限幅 100 <= threshold <= 800
output[i] = (input[i] <= threshold) ? 1 : 0; // 二值化
有作者用 OTSU + 三次方非线性映射,在 9 片不同材质、不同光照、带污渍褶皱的场地上测试全部正常,还省掉了上电黑白标定这一步(来源)。
💡 比喻:标定就像考试前定及格线
不同试卷(场地)难度不同,你得先看看这次黑题白题大概多少分,把及格线划在中间,而不是死守上次的 60 分。OTSU 就是自动帮你找”最能把黑、白分成两堆”的那条线。
② 全丢线,绝不能把 error 清零。 这是新手最容易踩的命案。
🔥 全丢线清零 = 直冲出弯
丢线大多发生在急弯切出赛道的那一瞬间。此时最后那个偏差方向,恰恰就是你该回去找线的方向。如果把 error 归零,车就以为”前方笔直”,一头冲出去。
正确做法(就是上面代码里那段):丢线时记住上一周期的偏差极性,朝原来弯道的方向打死方向去重新抓线。再补一条超时保护:长时间持续丢线就减速甚至停车倒找,别让车原地打转。
🧩 比喻
好比你过弯时眼睛突然闭了一下,正确做法是”刚才在往左拐就继续往左找路”,而不是”看不见了就直着冲”——直冲必然冲出赛道。
③ 十字/直角路口,必须特判。 经过十字时多路传感器会同时压线,加权位置会突然跳变、把偏差算飞,车就猛打一下方向。处理办法:8 路里有 ≥3 路同时压线就判为路口,进入”路口模式”——这时候不用常规偏差了,而是靠状态机让车直行/盲走固定一段(配合编码器或惯导计程)穿过去,过完再恢复巡线。一位省赛一等奖选手就是这么干的,并强调”换赛道、换车都得重标重调,没有万能参数”(来源)。
④ 安装高度与前瞻,是个三方妥协。 前瞻 = 传感器有效检测点离车头多远。
- 前瞻远 → 高速直道更稳,能提前预判弯道,但急弯容易提前切弯、切到弯内;
- 前瞻近 → 急弯跟得紧,但高速时容易发抖。
经验做法:直道用远前瞻,弯道用近前瞻,可以动态切换(灰度阵列靠”用哪几路算偏差”来实现切换,切换点要带迟滞,否则在直弯交界来回切会抖)。
⚠️ 恩智浦智能车光电组的纠结
一份技术报告里,底盘高度反复纠结:太低上坡蹭赛道、过障被顶住;太高重心高、高速过弯侧翻;前瞻调太远又有噪点。最后只能在”不蹭赛道”的前提下尽量压低,再配俯仰角控制前瞻。(来源)
一句话:高度和前瞻都不是越大越好,是重心稳定性、过障能力、识别准确率三者的折中。
关于 TCRT5000 红外光电
入门玩具车上最常见的就是 TCRT5000 这种离散红外反射光电。它是红外发射二极管(波长 950nm) + 光敏三极管,集成了日光阻断滤光片(Vishay 官方手册)。关键参数:
- 峰值工作距离 2.5mm,有效范围 0.2~15mm,最佳 0.2~6.5mm;
- 供电 3.3~5V,配 LM393 比较器整形输出数字量。
🔥 TCRT5000 必须贴地,这是它最大的硬约束
离地太远,反射光变弱,会把白地误判成”黑”。所以循迹时模块要贴到 2~8mm(贴近峰值 2.5mm 最好)。调试口诀:没遇黑线时指示灯应长亮,一旦灯灭就是遇到黑线了。 安装高度不一致会导致各路灵敏度不同,要逐路调电位器。
💡 比喻
TCRT5000 就像用手电照地面看反光,离太远光散了,白地黑地都看不清,必须凑很近(几毫米)才分得清黑白。
选型小结:TCRT5000 离散红外便宜、易上手,但单点少、贴地要求严、抗环境光差,适合入门;集成灰度阵列(如 8 路 IIC 数字阵列、模拟量阵列)一致性好、分辨率高、部分带自标定,是电赛/智能车的主流;摄像头方案前瞻最远、能识别十字数字等复杂元素,但算力和调试成本高(留到《K230 视觉与通信协议》一篇展开)。2024/2025 电赛 H 题(自动行驶小车)的主流一等奖方案,就是 8 路灰度 + 惯导(主控 MSPM0G3507),不是电磁,这点后面会再强调。
灰度循迹的调参口诀(转向环)
偏差信号算出来后,喂给转向 PID。这里先给经验,完整的调参流程见《PID 调参实战》一篇。
💡 灰度转向调参铁律
- 先把基础速度降到很低(最大的 30~50%),Ki = Kd = 0,只加 Kp 直到能跟线;
- 升速,加 Kd 抑制振荡和过弯超调;
- Ki 在循迹转向环通常保持 0——转向是”位置跟随”,P 就能消静差,Ki 多余且容易引发振荡和过冲。
有个 Arduino 5 路 PD 循迹的实测起点可以参考:Kp=15、Ki=0、Kd=200、基础速度 200(PWM 0~255),5.14m 的赛道跑约 5 秒(来源)。
还有一条容易被忽视:偏差信号要先低通滤波再算微分。灰度原始读数有抖动,直接微分会被 Kd 放大,车就发抖。经验做法:
adc_err = 0.4 * err + 0.6 * adc_err; // 一阶低通,新值权重 α≈0.3~0.5
注意滤波别太狠:α 太小会引入滞后,让弯道响应变慢——这是前瞻和滤波之间的又一个权衡。
🔥 灰度循迹必查的几个坑
- 偏差极性 / 电机加减号接反 → 车不回正反而越跑越偏,直接冲出去。上电第一件事:手推车体,确认 error 符号和 left/right 混合方向匹配。
- 用实验室/上次的阈值上赛场 → 误判。每次新场地重标或用 OTSU。
- 十字不特判 → 偏差算飞,急打方向。
- 偏差不滤波直接微分 → 高速发抖。
- Ki 盲目加在转向环 → 过弯过冲、画龙。
二、循迹路线二:电磁——蒙眼跟着 20kHz 小调走
有些赛题(尤其智能车竞赛电磁组)不是白底黑线,而是在赛道中心埋一根漆包线,通上 20kHz、约 100mA 的正弦交变电流,产生交变磁场。车上用工字电感去感应这个磁场——离导线越近,感应出的电压越大。
🧩 比喻:蒙眼跟着哼歌的人走
地上那根线一直”哼着”20kHz 的歌,车的两只”耳朵”(左右电感)谁听得响就说明离谁近,靠左右耳的响度差,就能判断车往哪边偏了。
电磁循迹在电赛里用得不算多(主要是智能车竞赛电磁组的常态),但思路很经典,而且 TI MSPM0 芯片在这上面有独特优势,值得了解。
信号链:从 30mV 微弱交流到一个能用的数
单个 10mH 工字电感在离线上方 15~20cm 处感应出的原始信号,只有大约 30mV 的 20kHz 小交流——这么弱、这么高频,单片机根本没法直接读。必须走完整的四级信号链:

- LC 并联谐振选频:只放大 20kHz,抑制其它频率的干扰。10mH 电感配 6.8nF 电容,谐振频率约 19.3kHz(理论上 20kHz 对应 6.33nF,工程上取就近标称值 6.8nF,刻意略低一点适配寄生电容与公差)。
- 运放放大约 100 倍:把 30mV 抬到伏级(STM32 不超 3.3V)。
- 二极管检波(常用肖特基 BAT54S):把 20kHz 交流的包络/峰值整流成直流。
- RC 低通滤波:平滑后送 ADC。
💡 两个比喻串起来
- LC 谐振选频像调收音机台:周围一堆杂音(各种磁场),LC 只把 20kHz 这个台调清楚放大,别的拧小声。
- 检波整流像把抖动的水面读成水位:电感信号是 20kHz 高速抖动的交流(像水波纹),二极管 + 电容只看波纹有多高(包络),变成稳定的”水位”给单片机读。
核心算法:差比和 (L−R)/(L+R)
电感读到值之后,最关键、最该记住的算法就是差比和:
$$\text{偏差} = \frac{L – R}{L + R}$$
为什么不直接用 L−R?因为整体场强会漂移——电流波动、不同赛道,会让所有电感的读数整体涨 10%,这时候位置没变,L−R 却跟着变了。而差比和的分子分母同比例缩放,比值不变,所以偏差只反映位置,不受幅值影响。
🧩 比喻:按比例打分,而不是绝对分
全班整体多考了 10 分(信号变强),但你比同桌高出的相对名次没变。除以总分看相对位置,就不会被整体高低带偏——这就是抗幅值漂移。
工程上还会先把每个通道归一化到 0~100(value/max×100),方便统一设阈值、识别环岛/十字等特殊元素。下面是真实电磁组代码里的双重差比和:
// 归一化(每通道 value/max*100)
Left_Adc = (adc_deal_last[LEFT_1]*100) / adc_max[0];
Right_Adc = (adc_deal_last[RIGHT_1]*100) / adc_max[1];
// 双重差比和求偏差 AD_Bias
AD_Bias = ((Left_Adc - Middle_Adc)*100/(Left_Adc + Middle_Adc))
- ((Right_Adc - Middle_Adc)*100/(Right_Adc + Middle_Adc));
⚠️ 标定别贪满幅
12 位 ADC(0~4096)实测一般不超 3600。标定时让电感最大值落在量程的 70~90%,绝不要调到满幅——满幅会削顶饱和、丢分辨率,还给特殊元素的极值留不出空间。
电感布局:成绩的隐形变量
电感怎么摆,直接影响直道稳不稳、弯道灵不灵:
| 布局 | 特点 |
|---|---|
| 一字横排 | 简单、直道稳,但对弯道/特殊元素弱 |
| 八字向外 V | 两端差异大,对弯道敏感,外侧可取极值识别元素 |
| 双横 + 竖直(双 T) | 综合直弯,竖直电感对正上方磁场敏感,用于环岛/坡道识别 |
| 双 45° | 对称性好,但要求左右运放/电感一致性极高,否则跑偏 |
布局要点:相邻电感间距 ≥2cm,避免互感串扰。
⚠️ 第 16 届电磁越野组的教训
木地板测试时编码器脉冲不稳,根因竟是机械连杆张力过大 + 电机振动没约束——软件再调参也救不回,这是硬件缺陷。该队还放弃了 45° 布局,因为硬件对称性不足、左右运放灵敏度不一致导致左右不对称。(来源)
一句话:机械固定、左右对称性、硬件一致性是地基。先把电感装牢、左右配对一致,再谈调参。
MSPM0 的独门优势:片上运放
前面那条信号链里,”运放放大”通常要在板外焊 LM358/OPA4377 之类的运放。但 TI MSPM0 芯片内部就自带运放——比如 MSPM0G3507 含 2 个 OPA,MSPM0L13xx 含 2 个零漂斩波运放 + 1 个 GPAMP,可编程增益(PGA)1~32 倍(单级不够可两级 cascade),输出能直连片上高速 12 位 ADC。
🧩 比喻
MSPM0 的片上运放就像芯片自带放大镜:本来要在板外焊运放当放大镜放大微弱信号,现在 MCU 内部就有 2 个高质量(零漂、抗噪)放大镜,信号不出芯片就放大完直接给 ADC——省料、省地方,还更干净。
这意味着电磁前端的选频信号放大可以做进芯片里,省掉外置运放,减 BOM、减焊接、减板上噪声。零漂斩波运放尤其适合这种微弱信号(低失调、低 1/f 噪声)。配置用 TI 的 SysConfig 图形化完成,官方有 opa_signal_chain_to_adc 等例程。不过要注意:MSPM0 跑纯电磁的完整开源还比较少(多为灰度),建议自己验证片内运放增益够不够,不够就级联或再外置一级。
❗ 别把 2024 电赛 H 题当电磁准备
H 题”自动行驶小车”限定用 MSPM0(常见 G3507),主流一等奖方案是 8 路灰度 + MPU6050 + 位置式 PID,不是电磁循迹。 电磁 + 差比和 + 工字电感主要是智能汽车竞赛电磁组的玩法,别搞混了准备方向。
电磁的几个高频坑
🔥 危险
- 电机 PWM 频率别落在 20kHz 附近——开关噪声会被 LC 选频电路一并放大,污染信号。取 13~19kHz 错开,或给模拟前端独立 LDO 供电 + 屏蔽 + 物理远离电机。
- 不做归一化直接用 L−R → 信号涨落时偏差漂移,不同赛道表现不一(差比和就是解决这个)。
- 相邻电感太近(<2cm) → 互感串扰。
- 舵机环加大 I → 积分饱和、弯道滞后画龙;微分项不滤波会放大 ADC 噪声抖舵。
💡 排查电磁链路:用示波器自下而上
很多”算法跑不动”其实是前端没出波形(电感虚焊/谐振没对/运放没起振)。正确顺序是:先确认信号源和示波器正常,把电感贴近电磁线量 10mH 引脚看有没有 20kHz 正弦,再逐级往后量放大、检波的输出。先怀疑硬件波形,别一上来怀疑代码。 (来源)
三、编码器:知道自己跑多快
循迹解决了”往哪走”,编码器解决”跑多快”。增量编码器输出 A、B 两路正交方波(相位差 90°),转得越快、脉冲越密;正转反转,A、B 的超前关系相反。
首选读法:定时器编码器模式
STM32 的定时器有个硬件”编码器模式”,强烈推荐用它:硬件自动加减计数、自动判方向,CPU 几乎零开销。
在 CubeMX 里把 TIMx 的 Encoder Mode 设成 “TI1 and TI2”(即 4 倍频,Encoder Mode 3),Prescaler = 0,ARR 设 65535(16 位)或 0xFFFFFFFF(32 位定时器,强烈推荐用 32 位的 TIM2/TIM5 防溢出)。
🧩 比喻:4 倍频就像数火车数得更细
只数车头(2 倍频 = A、B 各数一边)不如车头车尾都数(4 倍频 = A、B 的上升沿 + 下降沿都数)。同样一列火车,你数出 4 倍的格子,测得更精细。
读速度的代码非常简洁——每个固定控制周期(常用 5ms 或 10ms)读一次计数差值:
// CubeMX: TIMx -> Encoder Mode = "TI1 and TI2"(4倍频), Prescaler=0, ARR=65535/0xFFFFFFFF
HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL); // 启动编码器接口
// 每个固定控制周期(5/10ms)调用一次,返回"单位周期脉冲数"作速度反馈
int16_t Encoder_GetSpeed(void) {
static int16_t last = 0;
int16_t cur = (int16_t)__HAL_TIM_GET_COUNTER(&htim2); // 读计数
int16_t delta = cur - last; // signed 差值自动吃掉 65535/0 回绕(单周期增量<32767)
last = cur;
return delta; // 正/负即正反转,数值即速度反馈
}
关键心法:单位周期脉冲数,直接当速度用
很多新手卡在”怎么把脉冲换算成 m/s”。其实不用换算——那个 delta(单位周期内的脉冲数)本身就能直接当速度反馈喂进 PID。
💡 比喻:数电线杆
不用换算成公里每小时,就像你每 10 秒数一次窗外过了多少根电线杆,杆数本身就代表快慢——数越多车越快,直接拿这个数去踩油门/松油门就行。
如果报告里需要物理速度,再换算也不迟:
$$\text{RPM} = \frac{\Delta CNT \times (1000 / \text{period\_ms})}{\text{PPR} \times 4 \times \text{减速比}}$$
线速度再乘轮周长即可。采样周期是个权衡:太短,ΔCNT 太小,量化噪声大;太长,响应慢。智能车常用 5ms 或 10ms(弱一点的电机推荐 10ms)。
⚠️ 16 位定时器溢出,靠 signed 差值法自动解决
CNT 到 65535 会回绕到 0。代码里 int16_t delta = cur - last; 这一句,利用了无符号回绕的特性,即便跨越 65535/0 边界也能给出正确的带符号增量——只要单周期增量不超过 ±32767。 长距离里程累加要用 int32/int64。
还有一种读法是用 EXTI 外部中断在 A/B 边沿软件计数(M 法数脉冲 / T 法测间隔),更灵活,但占 CPU、高速易丢脉冲。结论:有硬件编码器接口时优先用定时器模式,EXTI 只在引脚/定时器不够时备选。
🔥 编码器必查
- A/B 接反或电机正负接反 → 反馈方向和控制方向相反,闭环变正反馈,越控越偏。先开环验证:手转轮子正转,读数应增大。
- dt 不固定(用 HAL_Delay 或主循环计时) → 速度 = Δ脉冲/dt 和积分项全失真,PID 怎么调都不稳。编码器采样和 PID 控制周期,要用同一个硬件定时中断严格定时。 中断里只采集 + 置标志,别做浮点/打印,计算放主循环。
四、IMU:给小车装上平衡感和方向感
IMU(常用 MPU6050)是六轴传感器:三轴加速度计 + 三轴陀螺仪。它能告诉小车两件事——姿态(Pitch/Roll,平衡车直立要用)和航向(Yaw,走直线锁方向要用)。但这两件事的难度天差地别。
姿态融合:互补滤波(必会)
加速度计和陀螺仪各有各的毛病,得互补着用:
- 加速度计:能算出绝对的倾角,但噪声大、抖;
- 陀螺仪:积分出来的角度很平滑,但会慢慢漂移。
🧩 比喻:开会时该多听谁
加速度计像个诚实但话痨手抖的人(说的方向对,但一直抖);陀螺仪像个嗓门稳但会慢慢跑题的人(短期稳,但越说越偏)。互补滤波就是:开会时多听那个稳的人(权重 0.98),偶尔用诚实的人把话题拉回正轨(0.02),综合出又稳又准的答案。
互补滤波公式:
$$\text{angle} = \alpha \times (\text{angle} + \text{gyro} \times dt) + (1-\alpha) \times \text{acc\_angle}$$
$\alpha$ 取 0.95~0.98(陀螺权重高)。加速度算角:$\text{pitch} = \text{atan2}(a_y, \sqrt{a_x^2 + a_z^2})$。代码:
// 一阶互补滤波:融合加速度(绝对但抖)与陀螺(平滑但漂),求 Pitch/Roll
// alpha=0.98(陀螺权重),dt=控制周期(秒)
float comp_pitch = 0;
void IMU_Update(float ax, float ay, float az, float gy, float dt) {
float acc_pitch = atan2f(ay, sqrtf(ax*ax + az*az)) * 57.2958f; // 加速度算角(度)
comp_pitch = 0.98f*(comp_pitch + gy*dt) + 0.02f*acc_pitch; // 互补融合
}
💡 α 怎么取
α 越大越信任陀螺(更平滑但跟随慢),越小越信任加速度(更跟手但更抖)。小车机械振动大就适当加大 α 滤掉加速度噪声。一般直接取 0.95~0.98 就行。dt 千万记得用秒(0.005),别用 ms 或采样次数,否则融合比例完全错。
进阶可以上一阶卡尔曼滤波(平衡车经典 TKJ 版),效果更平滑,标准参数 Q_angle=0.001、Q_bias=0.003、R_measure=0.03、dt=0.005~0.01。它本质上就是”会自动调权重的互补滤波”——发现加速度计这会儿可信就多听它,发现它在抖就少听它。代价是计算量大。对资源受限的单片机,互补滤波通常已经够用,追求极致平滑再上卡尔曼。
💡 卡尔曼 = 自适应增益的互补滤波
两者本质相似。互补滤波省、收敛快、对小车足够;卡尔曼平滑但费算力。别一上来就堆卡尔曼,先把互补滤波吃透。”什么时候才值得上进阶滤波”,留到《进阶控制算法》一篇细聊。
Yaw(航向):六轴 IMU 的一个大坑
姿态(Pitch/Roll)有加速度计当绝对参考,能纠偏。但 Yaw 不行——水平面内的旋转,加速度计提供不了任何参考(重力始终竖直)。所以 Yaw 只能靠陀螺 Z 轴积分:yaw += gyro_z * dt,必然随时间漂移。这是六轴 IMU 的固有局限(想要绝对航向,得上九轴加磁力计,或者用外部基准纠偏)。
🧩 比喻:闭着眼睛数步子估方向
陀螺仪测的是转得多快(角速度),要靠不停累加才知道现在朝哪。就像闭着眼睛靠数步子估自己面朝哪个方向,走久了一点点误差攒起来,最后以为朝北其实朝东北——这就是漂移。
对策有两步。
第一步:开机静止采零偏。 陀螺仪静止时本该读 0,但实际总有个固定的偏心读数。上电后让车绝对静止,采 N 次(常 500~2000,1000 次很常见)陀螺 Z 读数求平均当零偏,运行时每个读数都减掉它。
🧩 比喻
就像体重秤没人站时也显示 0.3kg。你先记住这个 0.3,以后每次读数都减掉它,才是真实角速度。
// 开机静止采陀螺 Z 轴零偏 + 运行时积分 Yaw(相对航向)
float gz_bias = 0, yaw = 0;
void Gyro_CalibZ(void) { // 上电后车保持静止时调用
float sum = 0;
for (int i = 0; i < 1000; i++){ sum += MPU_GetGyroZ(); HAL_Delay(1); }
gz_bias = sum / 1000.0f; // 零偏 = 静止读数均值
}
void Yaw_Update(float dt){ // 每个控制周期调用
float gz = MPU_GetGyroZ() - gz_bias; // 去零偏
yaw += gz * dt; // 积分得相对航向(短时漂移可忽略)
}
🔥 采零偏时车必须绝对静止
有人手扶、地面震动,都会把运动量误算进零偏,运行时车就持续缓慢自转/跑偏。而且采一次零偏也只能减小、不能消除漂移(温漂、随机游走仍在),长时间运行 Yaw 仍会缓慢漂。
第二步:走直线用”相对航向”,随时重置。 既然绝对 Yaw 会漂,那就不依赖它的绝对值——每段直线起步时把当前 yaw 清零当目标,航向误差 err = 0 − yaw 做 PID,输出叠加到左右轮差速:
// 走直线锁航向:起步时 yaw=0(清零当目标),err = 0 - yaw,差速纠偏
// out = Kp*err - Kd*gz; pwm_L = base + out; pwm_R = base - out;
注意上面那行的 D 项直接用了陀螺角速度 gz(对测量微分,比对误差微分更干净、抗扰)。转 90°:把目标航向加/减 90,闭环自动转到位后再清零,进入下一段直线。再高级一点可以做串级:外环用航向角锁朝向、内环用角速度求跟手——不过对入门小车,上面这套单环 PD 已经够稳。
🧩 比喻
不管罗盘(绝对 Yaw)准不准,起步时直接把现在朝向定为 0°当基准,之后谁偏了就往回掰——就像走路时盯着正前方一个目标走,而不是依赖会跑偏的指南针。
⚠️ 2024 电赛 H 题的航向实战
团队发现单靠 MPU6050 积分航向 + 编码器里程,长距离会累积误差,温漂还让 Yaw 越漂越大。解决:用实时温度算比例系数做动态温漂补偿,把漂移压到 ±1 以内,并用 8 路灰度纠偏兜底。有的队甚至换成维特智能陀螺仪(约 0.05° 精度)替代 MPU6050(约 2°/s 零漂)来提升转向精度。(来源)
一句话:纯惯性导航(IMU + 编码器)长距离必然累积误差,要么加外部基准(灰度/视觉)纠偏,要么温漂补偿 + 定期重置,别指望积分一路准到底。
🔥 IMU 必查
- 零偏采集没静止 → 运行时持续自转。
- 以为采一次零偏就永久消漂 → 温漂仍在,必须相对航向 + 重置。
- dt 单位忘换成秒 → 融合/积分比例全错。
- 方向环只用绝对 Yaw 单环且不重置 → 漂移被当成真实偏差,车被慢慢带偏。
五、把感官接到一起:分工、串级与切换
四样感官各管一摊,整车里它们这样配合:

记住两条分工原则(串级双环的落地代码与调参顺序,见《PID 进阶》和《状态机与整车软件》两篇):
- 速度环(编码器反馈)用增量式 PID——只输出 ΔPWM、自带积分、无饱和切换冲击,适合连续调速;
- 方向/航向环(灰度/电磁/IMU 反馈)用位置式 PID(实践中多为 PD)——适合”锁住某个朝向/位置”。
🧩 比喻:开车时手脚分工
方向环是你的手(管方向、外环、慢一点),速度环是你的脚(管油门、内环、快一点)。手脚分工又配合,车才能又快又直。
至于”什么路段用哪个感官”,核心思路是分工 + 兜底:
- 巡线主要靠灰度/电磁给偏差;
- 直道锁航向用 IMU 的相对航向,防止灰度细微抖动累积成跑偏;
- 十字/丢线时灰度偏差不可信,切到 IMU + 编码器”盲走”几格穿过去;
- 一旦灰度长时间丢线,有外部基准(重新抓到的灰度线)就纠偏、没有就靠惯导兜底——绝不让 error 清零直冲。
✅ 这一篇你该带走的
- 灰度循迹核心 = 加权位置法 + 工程四件套(现场标定 / 全丢线记忆方向 / 十字特判 / 前瞻高度妥协)。
- 电磁循迹核心 = LC 选频→放大→检波→ADC 信号链 + 差比和 (L−R)/(L+R) 抗幅值漂移;MSPM0 片上运放能省外置前端。
- 编码器 = 定时器编码器模式 4 倍频,单位周期脉冲数直接当速度,signed 差值吃掉溢出。
- IMU = 加速度 + 陀螺互补滤波求姿态;Yaw 必漂,靠开机采零偏 + 相对航向 + 随时重置来锁直线。
- 不管哪种感官,dt 要严格定时、极性先开环验证、参数现场重标并留约 20% 裕度——这是”实验室能跑、赛场也能跑”的分水岭。
感官齐了、反馈干净了,下一步就是真正让控制环动起来——从最基础的”把一个电机的速度调稳”开始,一次搞懂 P、I、D 各自在管什么。我们下一篇 PID 入门见。
📚 本文是 「从 0 到 1 带你打电赛 · 小车电控篇」 系列(共 12 篇)第 5 篇。




