本文可解决什么问题:如果你正在调 STM32 循迹小车,遇到"疯狂左转""直线画龙""直角弯冲线""丢线冲出赛道"等问题,本文用 20 小时实测记录,帮你快速定位并解决这些调参痛点,附完整可运行的 PD + 丢线旋转代码。

最终成果:S=40 速度下,7 个 360° 急弯的赛道只有 1 个极小弯(直径不到车身的 1/5)会出错,其余全程稳定跑圈。


一、写在前面

做这辆循迹小车之前,我只学过 PWM 和串口(HAL库),PID 对我来说就是"三个系数+一个玄学公式"。这篇文章不是教科书式的 PID 推导,而是我调参 20 个小时踩出来的真实记录——包括一度让车"疯狂左转"的 bug、让我想摔车的直线蛇形,以及最后怎么一步步让车从"画龙"变成"完美跑圈"。

如果你也在调循迹车,希望这篇能帮你少走我走过的弯路。

我的最终成果:S=40 速度下,7 个 360° 急弯的赛道只有 1 个极小弯(直径不到车身的 1/5)会出错,其余全程稳定跑圈。


二、循迹控制的基本框架

循迹小车 = 传感器("看"线在哪里)→ 控制器(算该往哪修)→ 电机(执行转向)。拆开就三件事:

5路红外传感器 → 加权误差 e → PD控制器 → 差速(左轮+Δ, 右轮-Δ) → 车转向追线

核心变量:误差 e

  • e = 0:车正压在线中间 ✅
  • e < 0:线在左边(偏左)
  • e > 0:线在右边(偏右)

误差算得准不准,决定控制好不好。我的 5 路传感器从左到右加权 {-2, -1, 0, 1, 2},哪个传感器压线就把它的权重加进误差:

/* 读取5路循迹传感器,返回加权误差: 偏左为负,偏右为正,居中为0 */
static int8_t read_sensors(void)
{
    GPIO_TypeDef* ports[5]   = {IR_L_GPIO_Port, IR_LM_GPIO_Port, IR_C_GPIO_Port, IR_RM_GPIO_Port, IR_R_GPIO_Port};
    const uint16_t     pins[5]   = {IR_L_Pin, IR_LM_Pin, IR_C_Pin, IR_RM_Pin, IR_R_Pin};
    const int8_t      weights[5] = {-2, -1, 0, 1, 2};   /* 左->右加权 */
    int8_t error = 0;
    uint8_t active = 0;

    for (uint8_t i = 0; i < 5; i++)
    {
        if (HAL_GPIO_ReadPin(ports[i], pins[i]) == LINE_ACTIVE_LEVEL)
        {
            error += weights[i];
            active++;
        }
    }
    /* 丢线处理:见后面"丢线旋转找线"章节 */
    if (active == 0)
    {
        lost_cycles++;
        if (lost_cycles <= LOST_SPIN_DELAY) {
            line_lost = 0;
            error = last_error * 2;                 /* 短暂丢线: 沿上次方向加强修正 */
        } else {
            line_lost = 1;                          /* 持续丢线: 交给原地旋转找线 */
            error = 0;
        }
    } else {
        last_error = error;
        lost_cycles = 0;
        line_lost = 0;
    }
    return error;
}

【配图占位:整车照片】(此处插入整车照片,展示 5 路红外传感器布局与电机安装位置)

踩坑 1:传感器极性(LINE_ACTIVE_LEVEL)。我的模块是"白板触发"型:白板亮灯/DO输出低,黑线熄灯/DO输出高。所以黑线=高电平=检测到,要设 LINE_ACTIVE_LEVEL = GPIO_PIN_SET。不同模块极性可能相反,一定要实测——对着黑线白纸各看一次串口 IR 值,反了就改这一个宏。我一开始用反了,车把白板当线,满场乱跑。

IR 状态表(黑线=1,白板=0,从左到右 5 位):

【配图占位:IR状态表截图】(此处插入串口打印的 IR 状态表截图,便于对照验证传感器极性)

状态IR
悬空/全黑11111
偏左01000 / 01100
居中00100
偏右00010 / 00110
大左偏11100 / 10100
大右偏00101 / 00111

三、P 控制:让车"转向线"

最简单的控制器:修正量 = KP × 误差。

int16_t corr = kp_x10 * e / 10;   /* 误差 -4..4 → 修正量 */
change1(base_speed + corr);        /* 电机1 = 左轮 */
change2(base_speed - corr);        /* 电机2 = 右轮 */

线偏右(e>0)→ 左轮加速、右轮减速 → 车右转追线 ✅

踩坑 2:转向方向反了(疯狂左转)。我一开始写的是 change1(base - corr),物理上电机1驱动左轮,线偏右时左轮减速右轮加速 = 车左转 = 越转越偏 → 疯狂左转。这个 bug 我调了很久才发现,因为"代码注释写的是电机1=左轮",但接线实际是电机1=左轮……方向对不对,别信注释,架空车轮 T 命令实测(发 T 1 40 看哪个轮转、往哪转)。

KP 实测记录(SP=40,其他参数不变):

KP现象
10直线蛇形严重(画龙)
8直线可接受 + 弯道跟得上(甜点)
4直线稳,但弯道转不过来(冲线)

【配图占位:蛇形与完美对比】(此处插入直线蛇形与调参后走直的轨迹对比图)

结论:KP 太小 → 跟不上弯;KP 太大 → 直线过冲蛇形。先调 P 到"直线勉强不画龙 + 弯道勉强能过",再上 D 项。


四、D 项:直线蛇形的解药

蛇形的本质是修正过冲:车偏一点 → P 猛修 → 修过头 → 偏另一边 → 再猛修……无限循环。P 控制看不到"车正在以多快的速度偏",所以刹不住。

D 项(微分)= 按误差变化速度给阻尼:误差正在增大(车在偏离)→ 阻尼对抗;误差正在减小(车在回正)→ 阻尼让它别冲过头。

int16_t d_term = (int16_t)(e - last_e);                    /* 误差变化率 */
int16_t corr = kp_x10 * e / 10 + kd_x10 * d_term / 10; /* P项 + D项 */
last_e = e;

KD 实测:KD=30(配合 KP=8、20ms 控制周期),直线从"画龙"直接变"走直"——这是整个调参过程里最爽的一次质变。

踩坑 3:控制周期不是越快越好。我把控制周期从 20ms 改到 10ms 想"响应更快",结果车一步一甩头。原因:传感器只有 5 档离散值(-4..+4),10ms 采样时误差跳变更频繁,D 项把离散跳变当"剧烈变化"放大 → 疯狂抖动。回退 20ms 后完美。数字传感器 + D 项,控制周期别太短。


五、丢线处理:比 PID 更重要的救场

直角弯、锐角弯时,线会瞬间从传感器视野里消失(00000)。P 控制这时候是瞎的,处理不好就是冲出赛道。

我的方案是两段式丢线处理:

  1. 短暂丢线(前 2 个控制周期):沿上次误差方向加强修正 error = last_error * 2——应对传感器瞬间没压上
  2. 持续丢线:原地旋转找线——朝上次偏差的方向原地打转(一前一后),直到重新看到线
/* 持续丢线: 原地旋转找线(先朝上次偏差方向转) */
if (line_lost)
{
    if (lost_cycles > LOST_MAX_CYCLES) {
        change1(0); change2(0);                     /* 找线超时(1.5s): 停车防跑飞 */
    } else if (last_error >= 0) {
        change1(LOST_SPIN_SPEED);                   /* 线之前在右 → 右转找线 */
        change2(-LOST_SPIN_SPEED);
    } else {
        change1(-LOST_SPIN_SPEED);                  /* 线之前在左 → 左转找线 */
        change2(LOST_SPIN_SPEED);
    }
    return;
}

效果:直角弯从"冲出去回不来"变成"原地转一下,找到线继续跑"。我的赛道 7 个 360° 弯,靠它只错 1 个极小弯。


六、I 项:我帮你踩过的坑

教科书说 PID 三项俱全,但循迹车急弯多,我实测 I 项弊大于利:

  • I 项治"恒定偏角"(车一直偏同一侧拉不回来),适合长直线
  • 我试 KI=0.2:误差在弯道里持续偏一侧 → 积分疯涨 → 丢线时积分没复位 → 车原地转圈
  • 结论:急弯赛道别上 I 项。除非你加"抗积分饱和 + 丢线时积分清零",否则纯 PD 更稳

七、完整 line_follow 代码(PD + 丢线旋转)

/* 循迹差速控制: 每20ms调用一次 */
static void line_follow(int16_t base_speed)
{
    int8_t e = read_sensors();

    /* 持续丢线: 原地旋转找线 */
    if (line_lost)
    {
        if (lost_cycles > LOST_MAX_CYCLES) {
            change1(0); change2(0);
        } else if (last_error >= 0) {
            change1(LOST_SPIN_SPEED);
            change2(-LOST_SPIN_SPEED);
        } else {
            change1(-LOST_SPIN_SPEED);
            change2(LOST_SPIN_SPEED);
        }
        return;
    }

    int16_t d_term = (int16_t)(e - last_e);
    int16_t corr = kp_x10 * e / 10 + kd_x10 * d_term / 10;  /* PD */
    last_e = e;

    if (corr > 99)  corr = 99;
    if (corr < -99) corr = -99;

    int16_t m1 = base_speed + corr;  /* 电机1 = 左轮 */
    int16_t m2 = base_speed - corr;  /* 电机2 = 右轮 */
    if (m1 > 99) m1 = 99;  if (m1 < -99) m1 = -99;
    if (m2 > 99) m2 = 99;  if (m2 < -99) m2 = -99;
    change1(m1);
    change2(m2);
}

参数默认值(写死在固件里):KP=8, KD=30, SP=40——断电重启直接是最优配置,比赛现场不用调。


八、调参方法论总结(我的整定顺序)

  1. 先验证传感器:串口看 IR,黑=1 白=0,5 路一致,左右不反(这一步错了后面全白调)
  2. 先验证转向方向:架空车轮,单电机测试,确认"线偏右→车右转"
  3. 先 P 后 D:P 调到"直线勉强不画龙 + 弯道勉强能过",再上 D 项压蛇形
  4. D 项从中间值开始:我 KD=30(配合 KP=8);蛇形残留→加,弯道迟钝→减
  5. I 项急弯赛道直接跳过
  6. 控制周期 20ms 别乱改
  7. 最后调速度:S 从 40 起步,稳了再加;冲到 S=50 直角弯开始冲线,就定格在 40

真实数据:S=40 时 1.9m 直线 3.56s(约 0.53 m/s),全程赛道稳定跑圈。


九、附录:其他真实踩坑(每个都能省你几小时)

  1. 串口只发不收:USART1 的 RX 引脚(PA10)被配成了推挽输出(AF_PP),输入信号被顶住。改成输入模式就好了。CubeMX 重新生成代码会把这个修复冲掉,注意
  2. 霍尔测速恒为 0:EXTI 回调里用 == 判断 GPIO_PIN_11|GPIO_PIN_12 的合并掩码,永远不相等。改成按位与 &,且中断入口拆成单脚调用
  3. 电机时转时不转:电机输出线虚焊,扯线测试才暴露。焊完一定要做拉力测试
  4. 电机响但轮子不动:锂电池没电了(电压不足)。调试/比赛前先确认电池满电
  5. PCB 打样:封装尺寸务必对照实物核对(我的驱动板就因封装问题只能外接);电源线加粗(VM≥40mil);信号线远离晶振和电机线

十,完整踩坑清单(按阶段)

一、画板子(PCB 设计)

#坑一句话解决
1封装尺寸没核对实物驱动板画小了放不进去 → 只能外接。打样前拿卡尺量实物
2核心板正反面/脚位方向没标逻辑脚位反了 → 只能焊在板底。原理图标注脚位方向
3横向排针易断路横向受力方向差,纵向排针稳。选型时优先纵向
4排针针数"待确认"就下单排母长度 = 针数×2.54,先数实物针数
55V 信号接了非 FT 脚(PA0~PA7/PB0/PB1)会烧单片机,只能接 FT 脚
6TB6612 的 STBY 悬空悬空=输出关闭电机不转,必须拉 3.3V
7TB6612 的 VCC 接 5V逻辑阈值 3.5V > STM32 的 3.3V 会偶发不转,接 3.3V
8电源线太细VM/5V/GND ≥40mil,VM 越粗越好
9晶振离信号线/电机线太近干扰导致复位/乱码,晶振紧贴 PD0/PD1
103.3V 总线宽度30mil(紧张 20mil),瓶颈在排针接触电阻(3/4 针并联)
11接线表左右写反(文档笔误)我们画板手册 P2-2 就写反过一次,拉线前对照检查
12布局不分区电源/驱动/传感器混放 → 干扰,按功能分区+接插件放板边

二、焊接

#坑一句话解决
13虚焊(最大坑)左轮电机线虚焊 → 时转时不转+疯狂左转,焊完必做拉力测试
14不上锡电机接口表面氧化/有漆,刮一下/用助焊剂/铜线绑着焊
15焊点不保护用热缩管或热熔胶包焊点,防短路防拉扯
16模块引脚顺序想当然霍尔模块是 D0/GND/VCC/A0(VCC 不在第一脚),看丝印/说明
17焊完不测万用表通断 + 上电量电压再装

三、接线

#坑一句话解决
18USB-TTL 收发接反串口没数据的第一嫌疑,RXD←PA9、TXD→PA10
19不共地电机 VM 接了不转 = 电源模块 GND 没和 MCU 连,一根共地线
20电机 VM 接 ST-Link 5V电流不够会烧 ST-Link,VM 走电源模块 VADJ
21霍尔磁铁距离不均触发时间短=漏脉冲=转速读数虚低,距离<5mm 且两轮一致
22磁铁极性不对3144 单极触发,翻面才触发,装错面=没信号
23串口延长线串联接头多=易断+端口号变,接头处胶带固定/换成品长线
24电机输出线绑不牢跑动甩松,焊+绑+胶带+拉力测试四件套

四、电源

#坑一句话解决
25锂电池没电="电机响但轮子不动"电压不足,比赛/调试前先量 VM 或充满电
26VADJ 电压和电机额定不匹配N20 上 7.4V 会过热,按电机额定调(N20=6V)
27全车断电重启参数丢失参数回默认,把最优参数写进固件默认值

五、软件/调车(最多)

#坑一句话解决
28串口只发不收PA10 被 CubeMX 配成 AF_PP 推挽输出顶住信号 → 改输入模式(重生成会冲掉!)
29HAL_UART_Receive_IT 死锁ORE 溢出后 HAL 关中断不恢复 → 寄存器级自己清标志继续收
30电机方向反方向电平反了 → 实测后对调 DIR 电平
31霍尔 RPM 恒 0EXTI 回调 == 判断合并掩码永不相等 → 改按位与 &
32RPM1 恒等于 RPM2中断入口合并传掩码 → 拆成单脚调用 HAL_GPIO_EXTI_IRQHandler
33转向逻辑反(疯狂左转)m1/m2 加减反 → 线偏右却左转,架空 T 命令实测方向
34传感器极性反(LINE_ACTIVE_LEVEL)模块"白板触发"型 → 黑线=高电平 → GPIO_PIN_SET
35P=10 直线蛇形修正过冲 → 降 KP(8 甜点)
36P=4 弯道转不动KP 太小 → 回升
37蛇形压不住上 D 项(KD=30),直线从画龙变走直
38I 项实验失败急弯积分错乱+丢线不复位 → 转圈,急弯赛道别上 I 项
39控制周期 10ms 反而蛇形离散传感器跳变被 D 项放大 → 回退 20ms
40丢线跑飞last_error*2 猜方向不靠谱 → 丢线原地旋转找线
41丢线旋转停不下来加超时(1.5s)自动停车
42断电重启参数丢默认值写成最优(KP=8/KD=30/SP=40)
43S=50 直角弯冲线速度上限,定格 S=40(除非加出弯降速)
44左右电机 RPM 差 44%电机个体差,P 控制能修正,可加单边补偿
45磁铁偏心惯性力高速振动,换轻磁铁/对称布置
46S=20 低速弯道停PWM 死区堵转,N20 扭矩大没这问题
47悬空 IR:11111 误判"坏了"无反射=像黑线,正常现象
48桌面深色/杂物误判传感器把桌面当线 → 白板环境测试/调电位器
49串口端口号变重插 USB-TTL 后 COM 号会变 → 重新枚举
50main.c 编码错位编辑工具把 MX_GPIO_Init 拆行 → 改完必编译

十一、总结与互动

做这辆车的最大感受:PID 不是数学题,是"看现象-改参数-看现象"的工程循环。真正让我车变稳的,不是理解了公式,而是搞懂了"蛇形是过冲、丢线要旋转、方向要先验证"这些工程直觉。

如果你也在调循迹车,卡住了欢迎评论区交流。完整工程(CubeMX 工程 + 全套文档)整理好后会开源,关注不迷路。

下一篇预告:【STM32循迹小车】从洞洞板到PCB打样全流程与翻车点


原创文章,欢迎转载,注明出处。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐