☰
ROBOTC巡线小车实战:光感归一化标定与PID整定
2026/10/29 14:49:21 网站建设 项目流程

简介:面向乐高EV3平台初学者及课程实验、竞赛入门者的RobotC巡线小车实践文档,围绕用C语言控制双轮小车沿黑线行驶展开,重点解决电机驱动、传感器读取与程序结构组织等入门问题。压缩包仅含1个PDF文件,约756KB,以标准实验报告形式编排,涵盖实验原理、代码实例与现象说明,便于按知识点查阅和课堂复现。内容从motor[]转速控制与差速转向讲起,延伸到触碰、光电、超声波传感器的初始化、SensorValue读取及阈值判断,并给出while循环、wait1Msec延时和巡线摆动策略的示例。已有580人学习,适合希望把编程思维落到硬件控制、补齐RobotC基础操作与实验报告写作的学习者参考。

1. 从一条黑线说起:为什么巡线小车的成败在传感器读数而不是电机

同一套乐高 ROBOTC 巡线小车(C 语言)的代码,在两张不同的场地上跑,结果能差出十万八千里:一张地上稳得像粘在线上,换个教室、换块底板就开始画龙、切弯、原地打转。很多人第一反应是 PID 参数没调好,于是把 Kp 从 0.5 改到 3,改到车抖成筛子又退回来,折腾一晚上。我踩过最深的坑恰恰相反——电机和算法都没问题,是光感读数压根没标定,阈值是拍脑袋写的 45,而那块场地的黑线实测只有 32,白底却有 78,整条控制环建立在一个错的输入上。乐高 ROBOTC 巡线小车这件事,硬件是现成的积木件,C 语言是现成的编译器,真正决定成败的是你能不能把「传感器读数」这一层做实。这篇写给已经拿到控制器、装好车轮、准备写第一行 task 的人,也写给已经能跑但跑不快的人。

2. 光电传感器读数与阈值标定:把 0~100 的玄学变成可复现的归一化值

2.1 反射式光感到底读到了什么

巡线用的光感本质上是一个红外发射管加一个光敏接收管,封装在同一个塑料壳里。发射管持续打出一束红外光,接收管测量反射回来的强度,控制器再把这个强度量化成一个 0 到 100 左右的整数。白纸反光强,读数偏高;黑线吸光,读数偏低。就这么简单,也正因为太简单,很多人直接拿这个原始值去判断黑白,然后就翻车了。

问题在于这个数值不是物理量,而是「相对量」。它同时受四件事影响:发射管的亮度、接收管的灵敏度、传感器离地高度、环境光里有多少红外成分。阳光里的红外成分远高于室内日光灯,所以同一辆车在窗边和走廊里读数能差出二十几个点。电池从满电掉到七成,发射管亮度下降,黑线读数会整体上移。这就解释了为什么很多人的车「上午调好下午就废」——他调的不是阈值,是电池电压。

ROBOTC 里配置光感有两种常见模式:一种带主动照明(发射管常开),一种被动接收环境光。巡线场景几乎都该用主动照明模式,因为它对外界光的依赖小得多。配置写法通常长这样,不同固件版本的类型名略有差别,按你手上编译器的传感器类型下拉框里能选到的名字来:

#pragma config(Sensor, S1, leftEye, sensorLightActive) #pragma config(Sensor, S2, rightEye, sensorLightActive) #pragma config(Motor, motorB, leftMotor, tmotorNormal, openLoop) #pragma config(Motor, motorC, rightMotor, tmotorNormal, openLoop)

这段 pragma 块看着像注释,实际是 ROBOTC 用来告诉固件「哪个口接了什么东西」的声明,编译时会写进程序开头。写错了端口号,程序能编译通过,跑起来却完全没反应——这是新手第一个卡点。我一般习惯把传感器端口和电机端口在纸上一一对应写下来,再抄进 pragma,别凭记忆。

2.2 现场标定:黑白取样与归一化映射

标定的目标只有一个:把「这个场地上这个高度下」的原始读数,映射成一个跟场地无关的 0 到 100。做法是分别采黑线和白底的样本,各取平均值,作为两个端点,中间的读数线性插值。

// 在指定端口上连续采样 times 次,返回平均值 // times 取 8~16 比较合适,太少受噪声影响,太多拖慢启动 int sampleAvg(tSensors port, int times) { long sum = 0; for (int i = 0; i < times; i++) { sum += SensorValue[port]; wait1Msec(5); // 给传感器刷新留时间 } return (int)(sum / times); } // 把原始读数归一化到 0(纯黑) ~ 100(纯白) int normalize(int raw, int black, int white) { int span = white - black; if (span < 8) span = 8; // 防止极端情况下除零或除小数 int v = (raw - black) * 100 / span; if (v < 0) v = 0; if (v > 100) v = 100; return v; }

逻辑很直白,但有两个细节必须处理。第一,(raw - black) * 100在 raw 最大 100、black 最小 0 的情况下最多 10000,用 int 完全够,但如果你的固件读数范围更大(有些颜色传感器原始值到 1023),就要先转 long 再乘,否则溢出成负数,车会突然发疯。第二,span的保护不能省。曾经有一块深灰色底板,黑白差值只有 6,程序里直接除,结果得到一堆几百的异常值,输出限幅之后车的行为变成了「要么全速左要么全速右」,看起来像 PID 参数炸了,实际是标定数据本身没意义。

标定的流程我固定成三步,每次换场地都重做一遍,耗时不超过 30 秒:

  1. 把车手动摆到纯黑线上,两个传感器都压住线,读十次平均值,记下black。
  2. 把车摆到纯白区域,读十次平均值,记下white。
  3. 把这两个数写进程序的常量区,或者用板载按钮做一次运行时标定,把值存进变量。

第三步能做到运行时标定是最好的,因为它能顺带把电池电压的影响吃掉。做法很简单:上电后让车原地等待三秒,这三秒里把传感器按在黑线上读一遍、按在白底上读一遍,按键确认。多写二十行代码,换来的是「换场地不用重新编译」,值。

提示:标定时手不要挡住传感器上方的环境光,也不要把车悬空——离地高度变了,读数就变了,标出来的是废数据。

2.3 单光感、双光感、三光感的选型对比

方案传感器数量能拿到什么信息典型问题适用场景
单光感1只有「偏左还是偏右」的二值方向无法判断偏了多少,直道易画龙教学演示、低速循迹
双光感2左右各一路,可算连续偏差量两路一致性差会引入系统性偏置主流方案,PID 巡线首选
三光感及以上3~5可预判弯道曲率、可识别路口布线与端口占用增加,标定工作量翻倍竞速、复杂赛道

双光感是绝大多数人的最优解:信息量刚好够算出连续偏差,硬件成本和多出来的调试量都在可接受范围。三光感的价值在竞速场景才体现得出来——最外侧那一路可以在车还没进弯时就给出「弯来了」的信号,让你提前减速。如果只是把车跑稳、跑完一圈,双光感足够了,多加的传感器只会让标定表变成一张需要维护的表格。

2.4 阈值法的边界:什么时候必须上 PID

二值化阈值法就是「读数大于阈值算白,小于算黑」,然后按左右黑白的四种组合决定转向。它的优点是代码短、调试直观、不挑硬件;缺点是输出是离散的,车永远在「左满舵」和「右满舵」之间来回切,直道上必然画龙。低速(电机功率 30 以下)时画龙幅度小,看着还能接受;一旦把基础功率提到 50 以上,车身质量带来的惯性会让它直接甩出线外。

判断该不该上 PID 有个很土但好用的标准:如果车在直道上的横向摆动幅度超过线宽的一半,阈值法就到头了。这时候不是加传感器就能解决的,因为你缺的是「偏差的连续值」和「偏差的变化趋势」,而 PID 恰好就是拿这两样东西做文章。下一章先不管 PID,把最小可跑的骨架立起来,因为一个连开环都跑不直的车,谈闭环调参是没有意义的。

3. C 语言最小闭环:三十分钟让车沿黑线跑起来的第一版代码

3.1 ROBOTC 的 C 语言骨架:pragma 配置、task main 与电机接口

ROBOTC 的 C 语言不是标准 C,它砍掉了标准库的一大部分,换成了机器人专用的全局数组和函数。写巡线程序,用得到的核心接口就这几组:

  • motor[portName] = power;直接给电机功率,范围 -100 到 100,负数反转。
  • SensorValue[portName]读传感器,整数。
  • nMotorEncoder[portName]读编码器累计脉冲,用于测速和算里程。
  • wait1Msec(n)阻塞等待,单位毫秒。
  • clearTimer(T1)和time1[T1]做非阻塞计时。

程序结构上,ROBOTC 用task main()作为入口,这一点和标准 C 的int main()不同。它还支持多任务,task关键字可以定义并行执行的函数,但初学者用多了容易把共享变量写成竞态,我的建议是先用单任务把逻辑跑通,需要显示调试信息时再考虑加一个低优先级任务。

电机控制模式在 pragma 里就要定下来,这一条后面会专门讲坑:openLoop表示你给多少功率就是多少功率,不做速度闭环;PIDControl表示固件内部会自建一个速度环,把功率换算成目标转速去维持。巡线的时候外层本来就在做位置闭环,内层再套速度环,两个环会互相打架。所以巡线车的驱动电机,我一般统一用开环,把控制权完整留给自己的代码。

3.2 第一版二值化巡线代码:能跑、能停、能调

#pragma config(Sensor, S1, leftEye, sensorLightActive) #pragma config(Sensor, S2, rightEye, sensorLightActive) #pragma config(Motor, motorB, leftMotor, tmotorNormal, openLoop) #pragma config(Motor, motorC, rightMotor, tmotorNormal, openLoop) // 这三个数必须按你的场地实测改,不要抄别人的 int BLACK_L = 30, WHITE_L = 82; int BLACK_R = 30, WHITE_R = 82; int THRESH = 55; // 归一化后的分界,白底上的线一般在 40~60 之间 int normalize(int raw, int black, int white) { int span = white - black; if (span < 8) span = 8; int v = (raw - black) * 100 / span; if (v < 0) v = 0; if (v > 100) v = 100; return v; } task main() { int basePower = 40; // 直道巡航功率,先小后大 int turnPower = 25; // 修正时的差速幅度 int l, r; while (true) { l = normalize(SensorValue[leftEye], BLACK_L, WHITE_L); r = normalize(SensorValue[rightEye], BLACK_R, WHITE_R); if (l > THRESH && r > THRESH) { // 两边都白:线在中间偏某侧,直行 motor[leftMotor] = basePower; motor[rightMotor] = basePower; } else if (l <= THRESH && r > THRESH) { // 左边压线:车偏右了,向左修 motor[leftMotor] = basePower - turnPower; motor[rightMotor] = basePower + turnPower; } else if (l > THRESH && r <= THRESH) { // 右边压线:车偏左了,向右修 motor[leftMotor] = basePower + turnPower; motor[rightMotor] = basePower - turnPower; } else { // 两边都黑:多半是压到横线或冲出赛道 motor[leftMotor] = 0; motor[rightMotor] = 0; } wait1Msec(10); } }

代码写完之后先说逻辑。四路分支对应四种传感器黑白组合,第一种「两边都白」在双光感贴线布局下其实意味着两个传感器都压在黑线两侧的白区,车是正的,直行;第二、三种是偏移修正;第四种「两边都黑」是异常态,直接停机是最保守的处理,等你看清楚它到底压到了什么再改成别的策略。

参数上只有三个值得动的:basePower决定整体速度,turnPower决定修正力度,THRESH决定黑白分界。调的顺序永远是先THRESH再basePower最后turnPower。THRESH怎么定:把车摆在线上,看归一化后的读数在直道上的波动范围,取中间值;如果两边读数在直道上一个 35 一个 68,说明两个传感器的高度或者一致性有问题,先机械调平,别急着改代码。

wait1Msec(10)这一行看起来可有可无,实际上是整个循环的心跳。去掉它,循环会以微秒级的速度空转,把 CPU 占满,读到的传感器值来不及刷新,你得到的是同一份旧数据反复参与判断,车的行为会变得完全不可预测。10 毫秒对应 100Hz 的控制频率,对巡线来说够用;电池电压低的时候我甚至会放宽到 20 毫秒,牺牲一点响应换取读数的稳定性。

3.3 代码逐段说明与三个必须改的参数

normalize的四个参数里,black和white是场地相关的,raw是实时读数。函数把「原始值」翻译成「相对反射率」,好处是后面所有的阈值和增益都不再依赖某一块具体的板子。这是整个程序里最值得写对的一个函数,因为它的输出直接喂给控制器,它歪了,后面所有调参都是白费。

BLACK_L和BLACK_R分开定义而不是共用一个值,是给硬件误差留的口子。同一批光感装在同一个高度上,读数也可能差三到五个点,机械装配的微小倾角就足够了。分开标定能把这份系统性偏差吃掉,代价只是多写一行常量。

THRESH用归一化后的 55 而不是原始值 45,是因为归一化之后这个数在不同场地上有可比性。我见过的做法里,有人把阈值直接写成(black + white) / 2运行时算出来,这更稳,因为标定数据一更新,阈值自动跟着走。

3.4 从能跑到跑稳:加一个调试显示

车跑起来之后,第一个想知道的是「它到底看到了什么」。NXT 平台用nxtDisplayTextLine,EV3 平台用displayTextLine,两者参数形式类似,第一个是行号,后面是格式串:

// 放在 while 循环末尾,实时输出归一化读数和当前电机输出 displayTextLine(0, "L:%d R:%d", l, r); displayTextLine(1, "M:%d %d", motor[leftMotor], motor[rightMotor]);

它的价值在于把「车的行为」和「传感器的认知」对齐。车往左冲,屏幕上却显示右边压线,那就是符号写反了;车在直道上抖,屏幕上两个数在阈值附近来回跳,那就是阈值选在了噪声带上。我调车的时候基本全程开着这个显示,肉眼盯着数字比盯着车可靠得多——车会骗你,数字不会。

这里有个细节值得提前说:显示函数本身有开销,刷得太快会拖慢主循环。如果加了显示之后车的行为明显变了,把刷新放到单独的task里,或者每隔五次循环刷一次。别小看这点开销,控制周期的稳定性比显示实时性重要得多。

4. 把 P 改成 PID:巡线小车的误差、采样周期与三个参数怎么定

4.1 巡线的误差怎么定义:从二值判断到连续偏差

二值化的问题在于信息丢失。读数 56 和读数 99 都算「白」,控制器看到的完全一样,可实际上前者是刚压到线边,后者已经跑到赛道外面去了。PID 的第一步就是把这份被扔掉的信息捡回来,定义一个有符号的连续偏差。

双光感最常用的偏差定义是归一化读数之差:

// dev > 0 表示左边更白(左传感器偏离黑线更多),车体偏右 int dev = l - r; // 范围约 -100 ~ +100

这个定义的好处是不需要额外标定,两个传感器的一致性误差会自动抵消掉一部分。它的物理含义是「车身相对黑线的横向偏移」,量纲是反射率百分比,和速度无关。也有用(l - r) / (l + r)做归一化的写法,好处是能抵消整体光照变化,坏处是分母在两侧压线时会接近零,产生尖峰,需要额外限幅。我一般先用差值,跑通了再看要不要上比值。

符号方向一定要在实车上验一遍。把车头往左掰一点,看dev是正还是负,然后确认电机转向和这个符号是对应的。这一步大概花二十秒,但不做的话,后面调参调到天亮也调不出来——因为负反馈写成了正反馈,车会越修越偏,最后冲出赛道。这是最容易翻车的地方,我见过不止一个人在这儿耗掉半个下午。

4.2 增量式 PID 代码与参数整定顺序

float Kp = 0.80; // 比例,先只用它,能把车拉回线上就行 float Ki = 0.00; // 积分,巡线一般用不上,直道有恒定偏置时才加 float Kd = 4.00; // 微分,抑制画龙的关键 int basePower = 45; int deadband = 2; // 偏差死区,小于这个值认为车是正的,别修 float integral = 0, lastDev = 0; int dev, turn, lp, rp; while (true) { int l = normalize(SensorValue[leftEye], BLACK_L, WHITE_L); int r = normalize(SensorValue[rightEye], BLACK_R, WHITE_R); dev = l - r; if (dev > -deadband && dev < deadband) dev = 0; // 死区,防止直道微抖 integral += dev; if (integral > 800) integral = 800; // 抗积分饱和 if (integral < -800) integral = -800; float deriv = dev - lastDev; lastDev = dev; turn = (int)(Kp * dev + Ki * integral + Kd * deriv); lp = basePower + turn; rp = basePower - turn; if (lp > 100) lp = 100; if (lp < -100) lp = -100; if (rp > 100) rp = 100; if (rp < -100) rp = -100; motor[leftMotor] = lp; motor[rightMotor] = rp; wait1Msec(10); // 采样周期必须固定,这是微分的隐含前提 }

turn是修正量,lp和rp是左右轮最终功率。这里用减法是因为符号约定:dev > 0表示车偏右,需要向左修,左轮减速右轮加速,所以左轮减去修正量。如果你的车是反的,把lp和rp后面那两个加减号对调即可。

整定顺序不能乱,这是血泪经验:

  1. 先把Ki和Kd都置 0,只留Kp。从 0.3 开始,每次加 0.2,观察直道行为。Kp太小车不修,偏出去就一路偏出去;太大车会在直道上高频抖。找到「能纠正但不抖」的那个值,记下来。
  2. 加Kd。这是让车不画龙的关键。从Kp的 3 到 5 倍开始试,比如Kp = 0.8就试Kd = 3~4。Kd的作用是看到「偏差正在快速变大」时提前反向拉一把,所以它天然抑制过冲。Kd太小的车在弯道出口会来回摆三四个周期,太大则对传感器噪声极其敏感,车会抖得像筛糠。
  3. 最后才考虑Ki。大多数巡线场景根本不需要积分项,因为赛道不会给你一个恒定的单向偏置,除非两个传感器高度装得一高一低,形成了系统性偏差——那种情况正确的解法是修机械,不是加积分。

注意:Kd项对读数噪声极其敏感。如果你发现Kd一加车就抖,先回去看传感器离地高度是不是太高(读数飘)或者采样周期是不是不稳定,别急着降Kd。

4.3 Kp/Ki/Kd 三个参数的实际取值范围与调法

参数典型范围太小的表现太大的表现调整步长建议
Kp0.4 ~ 1.5弯道修正不足,出弯后一路偏直道高频抖动,车头左右碎晃±0.2
Ki0 ~ 0.02直道存在恒定单侧偏置积分饱和,出弯后长时间朝一侧拽±0.005
Kd2 ~ 8弯道出口来回摆三四个周期对噪声过敏,车体高频抖动±0.5

范围是量纲决定的:偏差dev最大到 100,turn要落在 ±60 左右才能有效修正,所以Kp自然落在 1 附近;Kd乘的是相邻两次采样的差值,正常情况下这个差值只有几个单位,所以Kd要大出一个量级才起得到作用。看到别人写的Kd = 0.05就觉得自己的 4 太大,是没搞清量纲——他的偏差可能是归一化到 ±1 的,你的偏差是 ±100,参数根本没有可比性。

4.4 采样周期、死区与输出限幅

采样周期是 PID 里最容易被忽略的隐含参数。微分项算的是dev - lastDev,这个值只有在两次采样的时间间隔恒定时才有「变化率」的含义。中间插入一个显示刷新、一次编码器读取,周期从 10 毫秒变成 14 毫秒,微分项的物理意义就变了,表现为车偶尔莫名其妙地抖一下。

固定周期的做法有两种:一是用wait1Msec死等,简单但会被慢操作拖偏;二是用定时器做节拍控制,记下本次循环开始的时间,算完控制量后用while (time1[T1] < 10) {}补齐。前者够用,后者更稳。

死区的作用是滤掉直道上的微小偏差。没有死区时,两个传感器读数差一两个点,控制器就当成偏差去修,车在直道上会持续做肉眼几乎看不出但确实存在的微调,累积起来就是轻微的画龙。deadband = 2到3是比较好的起点,太大则弯道入口响应变钝。

输出限幅是安全网。basePower + turn在急弯时轻易超过 100,而motor[]的合法输入是 -100 到 100,超出的部分固件会怎么处理取决于实现,有的截断有的回绕,回绕就直接反转了,车会突然倒着冲出去。所以必须自己限,不要寄希望于固件替你兜底。

5. 巡线小车常见问题排查:五条踩坑记录与对应解法

5.1 车在原地高频抖动不走:电机速度环和外层 PID 打架

现象:程序烧进去,车通电后轮子发出「哒哒哒」的高频抖动,声音很尖,但车几乎不前进,或者前进时一冲一顿。

原因:电机在 pragma 里配置成了带内部速度闭环的模式。外层代码在算位置偏差,内层固件在根据编码器维持目标转速,两个环的带宽接近,互相抢控制权,形成振荡。

解决:把 pragma 里电机的配置改回开环,让motor[]的赋值直接就是功率百分比。如果确实需要速度闭环(比如要求两个轮子严格同速走直线),那就让内层跑速度环、外层只做转向修正,且外层的输出周期要明显慢于内层。

5.2 换个教室就冲出弯道:环境光把阈值带偏

现象:在实验桌上跑得好好的,搬到走廊或者靠窗的位置,直道还行,一进弯就切出去。代码一个字没改。

原因:光感的原始读数里混进了环境红外成分。窗边阳光和室内灯管下,同一条黑线的读数可以差二十个点,原本的阈值变成了噪声带里的一条线,二值判断开始随机翻转。

解决:短期把标定流程重跑一遍,把新的黑白值写进去;长期改成上电运行时标定,用按键触发,三秒完成。再进一步,把传感器往下压,离地高度从 15 毫米降到 8 毫米左右,反射信号占比提高,环境光的相对影响就被压缩了。这个改动比改任何参数都有效。

5.3 直道上左右画龙:采样周期抖动与积分饱和

现象:直道上车头以固定频率左右摆动,频率大概两三赫兹,摆幅不大但一直不停。把Kd加上去之后短暂好转,过一会儿又开始。

原因:两种可能。一是主循环里有耗时操作(比如每圈都刷新屏幕),采样周期不稳定,微分项失真;二是曾经加过Ki并且没有做抗饱和,弯道里积分项累积到很大,出弯后还在往同一侧拽。

解决:先把显示刷新挪到独立任务或者降低刷新频率,让主循环的周期稳定在 10 毫秒左右;然后把integral的上下限收紧到 ±800 以内,并且在偏差进入死区时停止积分累积。做完这两步,大多数画龙会直接消失。

5.4 直角弯必丢线:单光感的物理盲区

现象:连续弯和缓弯都没问题,一遇到直角弯或者断线,车直接冲出去,屏幕上两个读数都变成白底值。

原因:两个传感器之间的间距大于线宽,车头摆到某个角度时,黑线正好从两个传感器中间穿过去,两侧读到的都是白色。二值逻辑里这对应「两边都白,直行」,于是车就自信地直着开出去了。

解决:把「两边都白」从「直行」改成「保持上一次的转向,同时降速」。因为丢线的瞬间车一定是在转弯过程中,保持上一次的转向至少能让它继续绕着弯走一小段,配合降速就有机会重新压到线。更好的做法是加第三路传感器,或者上状态机做丢线搜索,这部分放在最后一章展开。

5.5 程序跑几十秒就停住:任务结构与阻塞调用

现象:刚上电一切正常,跑了二三十秒之后车突然停住,电机输出归零,重新上电又能跑一会儿。

原因:主循环里某处有个可能长时间阻塞的调用(比如等待按键、等待某个传感器就绪),或者干脆是程序跑飞进了默认的异常处理。还有一种情况是程序里用了while死等某个条件,而这个条件在电池电压下降后不再成立。

解决:把主循环里所有可能无限等待的调用都改成带超时的轮询,超时后走保底逻辑(停机或者低速直线)。同时给主循环加一个「心跳」变量,每圈自增,用另一个任务监视它,超过 200 毫秒没变化就强制把所有电机置零。这套保护写起来不到二十行,但能让你在调试时不至于追着车满屋子跑。

6. 速度上去之后:曲率前馈、丢线恢复与调参习惯

基础功率提到 60 以上,PID 就有点不够用了。原因不在参数,在信息——比例项是「看到偏差才修」,等它看到偏差时,车已经偏出去了,高速下这段延迟足够让车压到线外。解决办法是给控制量加一个前馈项:用左右传感器的读数差的变化率估计弯道曲率,在偏差还没变大之前就先把差速打出去。

// 前馈:用偏差的变化速度预判弯道,提前给出转向 float curvature = (dev - lastDev); // 和微分项同源,但系数独立 float feedforward = Kff * curvature; // Kff 从 0.3 开始试 turn = (int)(Kp * dev + Kd * deriv + feedforward);

注意前馈和微分项用的是同一个信号,只是系数分开。这不是重复,微分项的作用是抑制过冲,前馈的作用是提前量,两者在时域上的效果完全相反,所以必须分开调。调法也简单:先把Kff设为 0,把Kd调好,然后逐步加Kff,直到车在弯道入口明显「抢」了一下方向,再回退一点。

丢线恢复我习惯写成一个三状态机,而不是在判断分支里打补丁:

状态触发条件动作退出条件
TRACKING正常压线PID 巡线两侧读数持续超阈值 30 毫秒
SEARCHING判定丢线保持上次转向,功率降到 30任一侧重新压线
RECOVER搜索超时 800 毫秒原地小角度扫线压线或人工复位

状态机的好处是每个状态的行为都是明确的,不会出现「一边搜线一边还在跑 PID」这种逻辑打架的情况。我一般把状态存在一个枚举变量里,主循环开头先做状态迁移判断,再按状态分发执行,代码结构清楚,加新状态也方便。

最后说个习惯。我现在每调一次参数,都会在纸上记一行:日期、场地、电池状态、Kp、Kd、basePower、现象。听起来很笨,但巡线这事儿的参数是强场地相关的,换块板子全部作废,唯一能复用的就是你记录下来的「这块场地上什么参数有效、为什么」。没有这份记录,第二次去同一个场地你还是从零开始。这套东西不难,难的是别指望一次性调对——把标定做实、把周期稳住、把参数一次只改一个,剩下的就是耐心。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询