摘要:
在电子设计竞赛的智能小车调试过程中,我遇到了一个比较典型的问题:八路灰度传感器已经完成校准,主控也能够读取数据,但小车依然无法稳定循迹,甚至出现左右摆动、偏离黑线的情况。
一开始,我把注意力放在 PID 参数和传感器阈值上。后来重新检查了传感器数据到电机控制之间的处理流程,才发现问题的关键并不只是 PID,而是校准结果没有被正确利用、黑白电平定义不一致,以及位置误差与转向控制之间缺乏统一的方向约定。
这篇文章结合原来的调试记录,介绍如何读取感为八路灰度传感器的数字量、计算黑线位置、验证差速控制方向,并给出一段可以独立运行的 C 语言示例程序。
关键词:电赛小车、八路灰度传感器、感为循迹板、I²C 通信、PID 控制、循迹算法、嵌入式开发
一、问题出现:为什么校准成功,小车还是跑偏?
做电赛小车时,我们通常会把循迹系统分成三个部分:
感知:利用灰度传感器判断黑线位置。
控制:根据黑线偏差计算转向修正量。
执行:通过电机差速调整小车运动方向。
看似简单,但真正调试时会发现,任何一个环节出现错误,最终都可能表现为“小车不会循迹”。
在这次调试中,最初出现的现象是:
灰度传感器可以正常进行校准。
主控能够读取到传感器数据。
小车仍然会在黑线附近摆动或偏离轨道。
单纯调整 PID 参数,没有从根本上解决问题。
这就产生了一个疑问:
既然传感器已经校准成功,为什么控制系统仍然不能正确识别赛道?
我后来意识到,传感器能够工作,不等于主控已经正确理解了传感器输出的数据。
一套完整的循迹控制链应该是:
八路灰度传感器 ↓ 校准与数字量输出 ↓ I²C 通信读取 ↓ 黑白状态统一转换 ↓ 黑线位置计算 ↓ 偏差计算 ↓ PID / PD 控制 ↓ 左右电机差速输出每个环节都需要验证,而不能因为最终表现为跑偏,就直接认定 PID 参数存在问题。
二、问题一:已经校准,却还在使用固定阈值
2.1 原来的处理方式
原程序使用模拟量判断黑白,例如:
if (adc_value < 50) { // 判断检测到黑线 }这里的50是固定阈值。
这种方式在特定环境下可能正常,但如果安装高度、赛道材料或环境光发生变化,原来适用的阈值就可能不再合适。
尤其对于已经提供校准和数字比较功能的循迹板,如果主控依然使用独立的固定阈值,就没有充分利用板载校准结果。
当然,并不是说模拟量循迹一定不好。
对于需要连续灰度信息、精细位置估计的系统,使用归一化模拟量反而可能获得更丰富的反馈。
这里的问题是:当前程序选择了固定阈值,却没有让主控判断逻辑与传感器已经完成的校准保持一致。
2.2 改为读取校准后的数字量
本次使用的感为八路灰度传感器支持通过 I²C 获取八路数字状态。
根据对应产品手册,读取数字量的命令字为:
0xDD一次读取获得一个 8 位数据,每一位对应一个探头。
典型读取过程可以理解为:
START ↓ 发送从设备地址 + 写 ↓ 发送数字量命令 0xDD ↓ RESTART ↓ 发送从设备地址 + 读 ↓ 读取 1 字节数字量 ↓ STOP注意:0xDD是这款设备协议中的命令字,而不是所有 I²C 灰度传感器通用的寄存器地址。
具体从机地址以及读写函数,需要依据传感器版本和主控平台适配。
感为这类带数字比较功能的产品,还内置了滞回及滤波处理,能够帮助降低部分信号抖动的影响。
但这并不意味着读取数字量之后就不需要校准和验证。
传感器与地面的距离、赛道反射率以及现场光照条件,仍然会影响识别结果。
三、问题二:黑白电平的定义可能完全相反
这是本次修改中很关键的一步。
在原来的数据处理约定中,传感器数字量按以下方式解释:
1 = 白色 0 = 黑色但后续位置计算程序希望:
1 = 检测到黑线 0 = 未检测到黑线如果这两个定义没有统一,位置计算就会出现错误。
3.1 通过按位取反统一数据
可以使用下面的方式转换:
uint8_t mask = (uint8_t)(~raw);其中:
raw:传感器输出的原始字节。mask:经过极性转换的八路黑线检测结果。
例如,假设读取到:
raw = 0xE7转换成二进制:
raw = 1110 0111 mask = 0001 1000得到:
mask = 0x18此时bit3和bit4为 1。
如果它们对应传感器中间的两个探头,就可以判断黑线位于小车中间附近。
需要注意的是,并不是所有灰度传感器都必须取反。
正确做法是先固定其他条件,将黑线分别放到每个探头下面,观察原始数据如何变化,再决定是否取反。
不要在没有验证电平含义的情况下直接复制这段代码。
四、问题三:如何把八路状态转换成黑线位置?
获得数字量之后,控制程序仍然需要知道:
黑线究竟位于小车中心的左侧还是右侧?
这时就可以使用加权平均算法。
4.1 为八个探头定义位置权重
假设从小车前进方向观察,本文约定bit0对应最左侧探头,bit7对应最右侧探头。
可以定义如下权重:
| 探头 | bit0 | bit1 | bit2 | bit3 | bit4 | bit5 | bit6 | bit7 |
|---|---|---|---|---|---|---|---|---|
| 权重 | +3.5 | +2.5 | +1.5 | +0.5 | -0.5 | -1.5 | -2.5 | -3.5 |
这里约定:
位置为正,表示黑线偏左。
位置为负,表示黑线偏右。
位置接近零,表示黑线位于中间。
实际安装时必须验证探头顺序。如果传感器方向反过来安装,或者芯片输出位序不同,就需要调整权重映射。
4.2 加权平均算法的原理
假设八个探头的检测状态分别为:
[
b_0,b_1,\ldots,b_7
]
每个探头的位置权重为:
[
w_0,w_1,\ldots,w_7
]
则黑线位置可以估算为:
[
pos=\frac{\sum_{i=0}^{7}b_iw_i}{\sum_{i=0}^{7}b_i}
]
其中,检测到黑线的探头取1,没有检测到的取0。
例如:
mask = 0x18对应bit3和bit4检测到黑线:
[
pos=\frac{0.5+(-0.5)}{2}=0
]
说明当前检测位置位于中间。
再看另一个例子:
mask = 0x60对应bit5和bit6检测到黑线:
[
pos=\frac{-1.5+(-2.5)}{2}=-2.0
]
说明黑线位于右侧。
这些权重表示相对位置,并不是厘米或毫米。若要得到实际横向距离,需要结合探头间距和安装标定。
4.3 一段可以独立运行的 C 语言代码
下面这段示例只负责数据解码和位置计算,不依赖特定 STM32 或 MSPM0 库,可以先在计算机上验证算法,再移植到 MCU。
#include <stdint.h> #include <stdbool.h> #include <stdio.h> typedef struct { uint8_t mask; bool has_line; float pos; } LineResult; LineResult decode_line(uint8_t raw) { const float weight[8] = { 3.5f, 2.5f, 1.5f, 0.5f, -0.5f, -1.5f, -2.5f, -3.5f }; LineResult r = { .mask = (uint8_t)(~raw), .has_line = false, .pos = 0.0f }; float sum = 0.0f; int count = 0; for (int i = 0; i < 8; ++i) { if (r.mask & (uint8_t)(1u << i)) { sum += weight[i]; ++count; } } if (count > 0) { r.has_line = true; r.pos = sum / count; } return r; } int main(void) { const uint8_t raw_values[] = { 0xE7, 0xEF, 0xFF, 0x00, 0x9F }; for (size_t i = 0; i < sizeof(raw_values) / sizeof(raw_values[0]); ++i) { LineResult r = decode_line(raw_values[i]); printf( "raw=%02X mask=%02X found=%d pos=%.1f\n", raw_values[i], r.mask, r.has_line, r.pos ); } return 0; }这段程序采用了前面已经说明的两个假设:
原始数据
0表示黑线,需要取反。bit0位于左侧,bit7位于右侧。
本地使用 GCC 编译并执行上述程序,输出如下:
raw=E7 mask=18 found=1 pos=0.0 raw=EF mask=10 found=1 pos=-0.5 raw=FF mask=00 found=0 pos=0.0 raw=00 mask=FF found=1 pos=0.0 raw=9F mask=60 found=1 pos=-2.0这里是算法测试输出,并非实际小车的串口采集日志。
这一步的价值在于:不需要驱动电机,就可以先检查位置计算是否正确。
4.4 两个必须单独处理的特殊情况
第一种:mask=0x00
八个探头都没有检测到黑线。
这意味着当前没有有效的黑线位置,不能直接把pos=0当作小车位于赛道中心。
第二种:mask=0xFF
所有探头都检测到黑线。
它可能对应较宽的黑色区域、路口,也可能与阈值配置或传感器异常有关。
此外,左右两端同时检测到黑线时,加权平均也可能得到pos=0,但这不意味着真实黑线在正中间。
因此,加权平均适合处理正常的单条连续黑线;对于十字路口、分叉和异常图案,还应结合检测形态和状态机判断。
五、问题四:PID 参数可能不是问题,控制方向才是
只有确认位置估算正确之后,才应该继续调试电机控制。
假设目标位置为:
target = 0定义控制误差为:
[
e=target-pos
]
先不引入积分和微分项,只使用最基础的比例控制:
[
correction=K_p e
]
其中correction表示左右轮的差速修正量。
假设本文的电机控制约定是:
left = base_speed + correction; right = base_speed - correction;5.1 一个容易被忽略的方向问题
假设检测到:
mask = 0x10 pos = -0.5根据本文的坐标定义,黑线位于小车右侧。
目标位置是零,所以:
[
e=0-(-0.5)=0.5
]
如果:
Kp = 5.0 base_speed = 35则:
[
correction=5.0\times0.5=2.5
]
左右轮指令为:
left = 37.5 right = 32.5对于正常前进的差速小车,左轮速度高于右轮速度时,车体会向右转动。
这与黑线位于右侧的情况相符。
这里的35、37.5、32.5只是控制指令的示例数值,具体是 PWM 计数值、速度指令还是其他量,取决于电机驱动程序。
不能直接把它们理解为真实车轮转速。
5.2 为什么我建议先只用 P 控制?
因为此时最需要验证的是控制方向。
如果连左右方向都反了,增加微分项甚至积分项,只会让现象更加复杂。
建议采用以下顺序:
固定较低的基础速度。
暂时设
Ki=0、Kd=0。手动把黑线移到左侧或右侧,观察左右轮目标速度。
确认车体有向黑线方向转动的趋势。
再根据实际运动情况逐步调整
Kp。如果存在明显摆动,再考虑加入
Kd抑制快速变化。
原来的参考参数为:
base_speed = 35 Kp = 5.0 Ki = 0.0 Kd = 0.2由于Ki=0,这实际上是一个 PD 控制配置。
这些数值可以保留为原项目的参考记录,但不能直接移植到其他小车。
特别是微分项:
[
D=K_d\frac{e_k-e_{k-1}}{\Delta t}
]
它与控制周期有关。如果程序改变了采样周期,或者没有正确处理时间差,原来的Kd就不一定适用。
实际控制时,还需要对左右轮指令和修正量进行限幅,防止超出电机驱动允许的范围。
六、问题五:I²C 通信失败不能被当作正常数据
还有一个容易忽略的问题:传感器读取并不一定每次成功。
例如:
I²C 从设备没有应答。
地址配置不一致。
SDA、SCL 接线或上拉存在问题。
电机运行时电源干扰影响通信。
数据读取函数失败,但上层仍然使用了旧数据或默认值。
如果程序没有检查通信返回值,小车可能会根据无效的数据做出转向决策。
我的建议是把读取流程改成:
请求读取灰度数据 ↓ I²C 是否成功? / \ 否 是 ↓ ↓ 记录错误 解析数据 ↓ ↓ 有限次重试 判断是否丢线 ↓ ↓ 仍失败则 计算位置 停止或进入 ↓ 故障状态 更新控制例如,可以在控制程序中使用下面这样的逻辑:
uint8_t raw; // 以下函数需根据实际 MCU 平台实现 if (!gray_read_raw(&raw)) { motor_stop(); return; } LineResult line = decode_line(raw); if (!line.has_line) { motor_stop(); return; } // 全部探头触发,交由特殊路况逻辑处理 if (line.mask == 0xFF) { motor_stop(); return; } // 正常情况下才进入方向控制 update_line_controller(line.pos);以上是控制流程示例,gray_read_raw()、motor_stop()和update_line_controller()需要通过实际项目中的驱动函数实现。
这里采用停止策略,是为了让最初的调试版本尽量容易验证。正式比赛时可以进一步设计丢线恢复、路口识别和故障状态机。
七、如何快速定位循迹故障?
经过这次调试,我更倾向于把循迹问题拆分成可以逐项验证的小问题。
| 故障现象 | 优先检查内容 |
|---|---|
| 校准成功,但读数异常 | I²C 通信、命令字、从机地址 |
| 黑线上显示白色 | 黑白极性、是否需要取反 |
| 黑线向左移动,位置却变负 | 探头排列与位序 |
| 位置正确,小车却越转越偏 | 电机方向、差速符号、误差定义 |
| 小车在直线左右摆动 | Kp、Kd、噪声、采样周期 |
| 小车突然大角度转向 | 瞬时读数、通信异常、丢线处理 |
| 遇到路口就判断错误 | 多探头图案识别与状态机 |
我推荐按照下面的顺序逐步调试:
第一步:只调试传感器。
先不要让电机运动,用手移动黑线,验证每个探头能否正常触发。
第二步:检查数据极性和位置。
输出raw、mask和pos,确认左、中、右三种位置的数值符合预期。
第三步:验证左右轮控制方向。
先让车轮离地,使用小幅度修正量,检查左右轮的响应方向。
第四步:低速循迹。
先在直线赛道上测试,确认可以稳定修正方向,再进行弯道测试。
第五步:最后优化 PID。
逐步提高速度,结合实际偏差、摆动和转向响应调整控制参数。
这个顺序的好处是,每次只验证一个模块。
当小车出现异常时,可以更快判断问题属于感知、通信、算法还是执行机构,而不是反复修改所有参数。
八、复盘:这次调试让我理解了什么?
这次小车循迹调试,让我印象最深的并不是 PID 的数学公式,而是一个工程开发中非常基础的原则:
必须先保证输入数据可信,再讨论控制算法是否足够优秀。
最初遇到小车跑偏时,我很容易把问题归结于 PID 参数不合适。
但重新检查之后才发现,程序需要先解决几个基础问题:
是否真正读取了传感器校准后的数据?
原始数据的每一位代表什么?
位置坐标与实际探头位置是否一致?
误差符号和左右轮差速方向是否统一?
通信异常和丢线时,控制器应该如何处理?
只有这些问题得到验证,PID 参数调整才有明确意义。
实际上,这种开发思路不仅适用于循迹小车,也可以延伸到其他嵌入式系统。
无论是摄像头识别、毫米波雷达目标跟踪,还是智能汽车中的感知与控制系统,都需要经历类似的数据链路:
数据采集 → 数据解释 → 状态估计 → 控制决策 → 执行反馈。
我认为,调试能力并不是知道多少种算法,而是遇到故障时,能够逐层定位数据和控制链路中的问题。
这也是这次电赛项目带给我的一项实际收获。
九、参考资料
感为八路灰度传感器官方产品页与用户手册
原文章:电赛小车校准成功却不会循迹?问题可能不在 PID!
后续计划:如果继续整理这套电赛小车项目,可以进一步分享八路灰度传感器与主控的 I²C 通信代码、PD/PID 参数整定过程,以及小车循迹和避障模块的系统集成经验。
欢迎交流电赛小车、嵌入式调试和智能网联汽车相关技术问题。