八路循迹模块的 DO:先让小车沿着线跑起来

经典三问

Q: 循迹模块的 DO 是什么?

A: DO 是模块比较器根据反射光强输出的数字电平,只表示当前是否超过设定阈值。

Q: 为什么先用 DO 做循迹?

A: 它把复杂的光学变化压成容易读取的状态,能先验证传感器排布、方向判断和基本差速动作。

Q: DO 循迹常见使用场景?

A: 黑线跟随、边缘检测、全黑停车、全白丢线保护和低速路径验证。


把八路模块放到白底上,8 个 DO 各自保持稳定电平;黑线从探头下方经过,某一路的指示灯亮起,DO 也随之翻转。STM32 接收到的不是连续的灰度,而是模块上的比较器根据电位器阈值作出的二选一结果:黑,或者白;1,或者 0。原始反射光强的细节被留在了比较器之前,但这个简化后的结果已经足够让小车沿着线跑起来。

接线时把车头朝前,从左到右将探头编号为 S0 到 S7。代码约定黑线为 1、白底为 0;全黑停车,整排白底视为丢线,保持上一次转向方向低速找线。至于黑线到底对应高电平还是低电平,以实际读到的状态为准,统一在配置项中处理。

DO 不是"数字版 ADC"

很多常见 MH-Sensor-Series、TCRT5000 类模块都有 VCC、GND、DO 和 AO。其中 AO 是传感头此刻反射光强对应的模拟电压;DO 则经过板上的比较器。你拧动电位器,其实是在移动比较器的分界线:高过线的一边输出一种电平,低过线的另一边输出另一种电平。

text 复制代码
红外发射 + 接收 -> 反射强弱 -> 模拟电压 AO -> 比较器 + 电位器 -> DO: 0 或 1

所以 DO 的优势和代价都很直接:

  • 它能让 STM32 只读 GPIO,不需要先写 ADC、平均、阈值。
  • 模块上已经先定好了"黑"和"白"的边界;环境一变,电位器就得重新调。
  • 当它误判时,STM32 只看见一个 0 或 1,很难知道原始反射值离边界到底有多远。

刚开始做完整低速循迹时,我会先用 DO,因为黑线位置、转向和差速之间的关系能马上落到轮子上;需要更细的边缘位置、调试环境光或建立更稳定的判断时,再回头用 AO。

八个模块怎么接,不靠一张万能线序图

8 路模块都需要 VCC 和 GND,每个 DO 各接一根 STM32 输入线。模块常见支持 3.3V 到 5V 供电,但仍然应当以买到的模块资料为准。我使用 5V 供电,DO 输出通常可以直接给 3.3V MCU 读取。

我暂定的 GPIO 是:

text 复制代码
S0 -> PA4
S1 -> PA5
S2 -> PA8
S3 -> PA11
S4 -> PA12
S5 -> PA15
S6 -> PB0
S7 -> PB5

PA15 需要关闭 JTAG 后才能当普通 GPIO 使用,工程会在初始化时处理这一点。你的循迹板如果排针顺序不同,只改引脚表,传感器从左到右的权重顺序不要跟着物理线束乱掉。

车头朝前看时,S0 是最左侧,S7 是最右侧。八个模块必须排在同一条横线上,离地高度尽量一致。一个模块抬得更高,另一个贴得更近,代码再整齐,读到的也不是同一条黑线。

先把电位器调到黑白交界,而不是随便拧

先不上车跑。给模块供电,把其中一个探头依次放到白底和黑线下方,观察模块板上的比较器指示灯或者 OLED 显示。慢慢调电位器,找到一个位置:白底稳定输出 0,黑线稳定输出 1。

不是所有模块都按这套电平方向工作。工程把这个约定写成一个宏:

c 复制代码
#define LINE_SENSOR_BLACK_IS_HIGH  1

若你实际测到黑线是 0、白底是 1,不要急着把八根 DO 线交换,也不需要把所有判断条件里的 if 逐行取反,只要改为:

c 复制代码
#define LINE_SENSOR_BLACK_IS_HIGH  0

模块指示灯亮不亮也不必和"黑线为 1"强行绑定。灯接在比较器的哪一侧、板子是否低电平点亮,都取决于模块电路。相信你在 OLED 里实测到的逻辑值。

八个 0/1 先装进一个字节

定时器每 10ms 读取一次传感器。bit0 放 S0,一直到 bit7 放 S7,这样 8 路状态只占一个 uint8_t:

text 复制代码
S7 S6 S5 S4 S3 S2 S1 S0
 0  0  0  1  1  0  0  0     二进制:00011000

工程在 LineSensor_ReadMask() 里把它们收起来:

c 复制代码
if ((GPIO_ReadInputDataBit(port, pin) != Bit_RESET) == LINE_SENSOR_BLACK_IS_HIGH)
{
    mask |= (uint8_t)(1u << index);
}

这里不需要中断。DO 是已经稳定的数字状态,不是编码器那种每一个边沿都不能漏的高速脉冲。每 10ms 看一次,对低速循迹已经足够快,也让控制节拍固定下来。

10ms 控制任务为什么放在定时器里

这辆教学小车里,TIM1 每 1ms 进入一次中断。计数满 10 次后,直接调用传感器和电机更新:

c 复制代码
void TIM1_UP_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
        system_ms++;
        control_divider++;

        if (control_divider >= 10)
        {
            control_divider = 0;
            LineSensor_Update();
            Motor_Update();
        }
    }
}

LineSensor_Update() 只是读取 8 路 GPIO、算一个很小的固定差速;Motor_Update() 只是写方向脚和 TIM2 的比较值。这些动作短、时间稳定,放进 10ms 中断能保证控制周期不被主循环里多一条调试代码打乱。

Motor_Update() 写的是 Motor_SetTargetPercent() 存下来的两个目标值。这个接口就是前面 PWM 那篇里单轮版本的两轮版:

c 复制代码
void Motor_SetTargetPercent(int16_t left, int16_t right)
{
    left_target = Motor_LimitPercent(left);
    right_target = Motor_LimitPercent(right);
}

Motor_LimitPercent() 做的是同一件事:把越界的数夹回 -100 到 100。单轮调用现在只是一个薄壳:

c 复制代码
void Motor_SetPercent(int16_t percent)
{
    Motor_SetTargetPercent(percent, percent);
}

OLED 刷屏仍然放在 main(),每 100ms 才做一次:

c 复制代码
if (Timer_Expired(&report_time, 100))
{
    OLED_ShowNum(0, 0, LineSensor_GetMask(), 3, OLED_6X8);
}

把一整屏 OLED 数据塞进 10ms 中断,刚刚获得的稳定节拍又会被自己砸掉。控制任务和调试任务都需要时间,但不该抢同一段时间。

从"哪边压线"到固定差速

八路传感器不是八句独立的 if。代码给每个位置一份权重:

c 复制代码
static const int8_t weights[8] = {-7, -5, -3, -1, 1, 3, 5, 7};

黑线被 S0、S1 看到,平均位置偏左;被 S6、S7 看到,平均位置偏右;中间的 S3/S4 命中,位置接近 0。这里算出来的 position 只是"线偏在哪一边"的离散描述,不是任何神秘控制量。

默认直走给:

text 复制代码
left = 30, right = 30

线偏左,左轮慢、右轮快,小车就往左收回来:

text 复制代码
轻微左偏:left = 24, right = 36
明显左偏:left = 18, right = 42
严重左偏:left = 10, right = 50

线偏右时反过来。工程把这些差速写成 6、12、20 三档,不会假装它们对每个底盘都正确:

c 复制代码
if (position == 0)
{
    correction = 0;
}
else if ((position <= -5) || (position >= 5))
{
    correction = 20;
}
else if ((position <= -2) || (position >= 2))
{
    correction = 12;
}
else if (position != 0)
{
    correction = 6;
}

LINE_BASE_SPEED 默认是 30。某些电机带着车身低于 50% 就压不过静摩擦,30/10/50 在它们身上不会换来漂亮的渐变转向,只会换来停、抖、突然窜出去。那不是 8 路循迹逻辑错了,是电机、减速箱、供电、地面和占空比门槛没有一起考虑。先架空观察方向,再从实际能稳定起转的最低速度往上调。

两个特殊画面,必须单独写

全黑:11111111

八个模块同时看到黑线,工程默认停车:

c 复制代码
if (sensor_mask == 0xFF)
{
    Motor_SetTargetPercent(0, 0);
    return;
}

它可能意味着终点横线、加宽的标记、传感器贴得太低,也可能只是一张整块黑纸。默认将它作为最保守的安全行为;赛道把全黑定义为"掉头"或"停止线"时,再将这个状态映射到相应动作。

全白:00000000

八个模块都没看到黑线时,不让车直接停住。它保留最后一次偏转方向,低速往那边找:

c 复制代码
if (sensor_mask == 0x00)
{
    if (last_turn < 0)
    {
        Motor_SetTargetPercent(10, 35);
    }
    else
    {
        Motor_SetTargetPercent(35, 10);
    }
    return;
}

最后一次线偏左,就继续往左寻找;最后一次偏右,就继续往右寻找。这个策略不保证每一种赛道都能找回线,它只是比"丢线以后随机直走"更有依据。若你只用 2 路传感器,很容易在一个急弯处直接把线丢掉;8 路排在车头前方,能给这段寻找动作多留一些位置线索。

先在 OLED 里确认,再放到地上

主循环每 100ms 更新调试信息。OLED 三行依次显示:

text 复制代码
第一行:8 路掩码的十进制值
第二行:left 目标速度
第三行:right 目标速度

先把一小段黑线从 S0 缓慢扫到 S7,看掩码和左右目标速度怎样变化。再把整排放到白底,确认它进入上一次转向的找线逻辑;最后用整块黑卡纸盖住,确认两轮目标都变成 0。

确认这些之后才接电机。先架空,让左/右轮执行的方向与画面中"线偏左就向左修正"一致;若实际转向反了,通常是电机安装方向、驱动板方向脚或左右电机定义和代码约定相反。先统一这些基础方向,不要把 8 路权重全部颠倒来掩盖一根电机线接反的问题。

放到地面后,环境光一变、黑线褪色、白底反光、模块高度不齐、两个同型号模块电位器位置不一样,都可能让 DO 在边缘附近来回跳。重新逐个调电位器,把黑白切换点留在稳定范围中间;真需要知道每个模块离阈值还有多远,就进入 AO 模拟输出篇。

最后橘猫说

现在的转向是固定三档差速:看到线在左边就给左慢右快,看到线在右边就反过来。急弯、不同速度、不同电机启动门槛和不同赛道,都会让同一张表出现过转或转不过去。

但它有一个好处:每一个动作都能从 mask、position、左右目标速度直接追出来。你把黑线挪到 S0,OLED 显示更大的左偏转向;把黑线挪回中间,两个目标又回到同一速度。传感器、差速和轮子之间的因果链没有被参数遮住。

DO 模式给小车的是一排已经越过阈值的决定。把这些决定接到可靠的固定节拍和低速差速上,小车就能沿线走;需要观察比较器作出决定前的电压时,再使用 AO。

相关推荐
新晨单片机设计7 小时前
S017C-基于STM32汽车防酒驾系统(心率、血氧、短信)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·防酒驾报警系统
fly_sunnn8 小时前
STM32-GPIO输出以及按键控制LED&光敏传感器控制蜂鸣器
stm32·单片机·嵌入式硬件
fly_sunnn11 小时前
STM32-LED闪烁·LED流水灯·蜂鸣器
stm32·单片机·嵌入式硬件
新晨单片机设计11 小时前
S022A-基于STM32单片机音乐盒(数码管显示)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·音乐盒
深圳老胡12 小时前
STM32 MCU 国产替代型号简介
笔记·stm32·单片机·代码规范
殷忆枫12 小时前
基于STM32的独居老人语音/一键双模紧急呼救系统设计与实现
stm32·嵌入式硬件·xcode
暖焰核心12 小时前
USART 概念篇——从高低电平到数据帧
stm32
深圳老胡14 小时前
STM32 在 VSCode 下 -O0 与 -Os 编译条件的区别
笔记·stm32·单片机·代码规范
新晨单片机设计20 小时前
S021A-基于STM32单片机自动追光系统(步进电机)【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·自动追光系统