乐高RobotC巡线小车:先跑通差速与光电阈值再上PID

发布时间:2026/10/29 14:52:18
乐高RobotC巡线小车:先跑通差速与光电阈值再上PID

简介:面向乐高EV3教育平台学习者与机器人课程入门教学的RobotC巡线小车实验文档,以标准实验报告的体例展开,帮助读者在C语言环境下完成从电机驱动到传感器巡线的完整实践。内容围绕电机转速与转角控制、双轮差速转向的三种实现方式、触碰、光电、超声波三类传感器的初始化与SensorValue读取、while循环与wait1Msec延时等知识点组织,并给出光电阈值判断下沿黑线边沿往复修正的巡线原理,可直接对照搭建与调试。资源包共1个文件,为1份756KB的PDF文档,含实验原理、示例代码与传感器配置说明,便于打印或平板查阅。已有580人学习下载,适合初学者按章节复现代码、理解传感器工作机制并积累排错经验。

1. 乐高 RobotC 巡线小车:先别急着写 PID,把差速和阈值跑通

很多人拿到乐高 RobotC 巡线小车套件,第一反应是直接抄一段 PID,结果车在线上左右画龙,调三天还在怀疑代码。反直觉的地方在于,巡线翻车多数不是算法不够高级,而是光电阈值没校准、左右电机不对称、传感器装得太靠前。这份课程实验报告把电机转速控制、转角控制、触碰/光电/超声波传感器、LCD 在线调试和 10.03 秒的巡线基线串在一起,价值在于给出一条能复现的最小路径:先让差速底盘走直线,再把光电读值压成三区阈值,最后才谈修正力度。它适合刚拿到 NXT/EV3 类控制器、卡在端口配置和阈值上的新手,也适合要交课程设计、需要把参数讲清楚的熟手。

2. 差速底盘与电机控制:从 power 到编码器增量的四个参数

巡线车的底层不是“巡线算法”,而是差速底盘能不能按你的意志走。左右轮各给一个功率值,车体就会围绕某个瞬时圆心转,功率差越大转得越急;但motor[]里的 100 不是 100 cm/s,它只是占空比意义上的功率指令,电池电压、地面摩擦、轮子打滑都会把这个映射拉歪。常见做法是先把车架空,确认正负方向、左右轮映射和死区功率,再落地跑直线。这个顺序省不掉,省掉就会在后面把机械误差当成算法误差。

2.1 差速转向的几何关系与功率映射

设左右轮线速度为vL、vR,轮距为L,差速底盘的瞬时转向半径可以写成R = L * (vL + vR) / (2 * (vR - vL))。这个式子不需要背,但要理解三个边界:当vL = vR时分母为 0,车走直线;当一侧为 0、另一侧为正时,转弯半径约为L/2;当vL = -vR时,转弯半径为 0,也就是原地转向。巡线时真正用的是小功率差:外侧轮多给 10 到 20 个功率单位,内侧轮少给同样的量,车头就会向黑线方向偏。功率差给太大,车会在黑线两侧来回甩,光电读值还没稳定,车已经冲出去了。

motor[]的取值通常是 -100 到 100。负值反转,正值正转,0 是停。低速段有死区,给 5 或 10 时电机可能只响不转,尤其是电量下降后更明显。我一般会让巡线基础功率落在 35 到 55 之间,转向内侧轮不低于 15,否则一遇阻力就停,车身会“点头”。如果套件用的是 NXT 电机,内置编码器能读到角度;如果是普通直流电机,就只能靠时间和电压估。实验报告里提到浮动模式和制动模式的差别:制动模式下功率和速度的线性关系更好,浮动模式下差不多 40 以上才比较线性。巡线要频繁启停和修正,优先用制动模式,别让车在 0 功率时滑行太远。

提示:先把车架起来,给motor[motorA] = 30和motor[motorB] = 30,看两个轮子是不是同向、是不是同一侧。左右装反是最高频的翻车点,代码里调半天不如架起来看十秒。

2.2 转速控制与转角控制:什么时候用 motor[],什么时候用编码器目标

motor[]是开环功率控制,适合巡线这种连续修正场景:每 10 到 20 毫秒读一次光电,算一次左右功率,立刻写回电机。它不保证走多远,只保证“现在给多少力”。nMotorEncoderTarget[]是增量目标控制,适合定距前进、直角转弯、让车精确走 360 度这类动作。它的关键细节是:设置目标增量不会自动启动电机,后面必须跟一句motor[]给功率;到达目标后电机自动停,但停得准不准受功率、制动模式和地面影响。

下面这段代码把车按 6 度一个小步往前挪,每步停 600 毫秒,方便观察编码器增量和实际位移的对应关系。它来自实验报告里的角度控制思路,我把它压缩成可复现的最小例:

task main() { int nMoveSize = 6; // 每次目标增量,单位是编码器度数 int nSpeed = 50; // 启动功率,太低会停在死区 int nDelay = 600; // 每步后的停顿,给机械结构稳定时间 bFloatDuringInactiveMotorPWM = false; // 制动模式,停车不滑行 nMotorEncoder[motorA] = 0; // 编码器清零 nMotorEncoderTarget[motorA] = 0; // 目标增量先归零 while (true) { nMotorEncoderTarget[motorA] = nMoveSize; // 设增量,不会自己转 motor[motorA] = nSpeed; // 必须再给功率才启动 wait1Msec(nDelay); // 等这一步走完并稳定 } }

逻辑说明:nMotorEncoderTarget[motorA]设定的是“从当前点再走多少度”,不是绝对角度;nMotorEncoder[motorA]是累计读数,用来观察有没有过冲。wait1Msec(nDelay)在这里不是控制电机走多久,而是给每一步之间留观察窗口。参数说明:nMoveSize从 6 度开始比较安全,改到 30 度以上时车会明显一顿一顿;nSpeed低于 30 可能克服不了静摩擦,高于 60 又容易在目标点附近冲过头;nDelay太短会看不到稳定过程,太长会让循环变得很慢。巡线主循环不要用这种“走一步停一下”的结构,它只适合标定和直角弯。

如果要等电机真正到达目标,可以用运行状态轮询,而不是拍一个固定延时:

task main() { bFloatDuringInactiveMotorPWM = false; nMotorEncoder[motorA] = 0; nMotorEncoderTarget[motorA] = 360; // 目标增量 360 度 motor[motorA] = 35; while (nMotorRunState[motorA] != runStateIdle) { wait1Msec(10); // 10ms 轮询一次,别空转占满 CPU } motor[motorA] = 0; }

逻辑说明:nMotorRunState从运行态回到runStateIdle才说明目标动作结束,这比wait1Msec(3000)可靠。参数说明:360 度是编码器度数,不是轮子转一圈的行驶距离;轮径、齿轮比、打滑都会让实际距离偏离。巡线车上我更愿意用时间片加光电反馈,不用角度目标硬压,因为黑线不会等你走完 360 度再拐弯。

2.3 端口初始化与最小可跑程序

RobotC 的传感器初始化有两种写法:一种用#pragma config(Sensor, S1, light1, sensorLightActive),另一种用const tSensors light1 = (tSensors) S1;。课程实验报告里两种都出现了,但同一个工程里最好统一用#pragma,端口、名称、类型在一处看全,后面读SensorValue[light1]不容易把 S1 和 S2 搞混。端口分配建议固定成表,不要今天光电插 S1、明天插 S3,否则调试记录没有可比性。

端口设备建议名称说明
S1右侧光电lightRight靠近黑线右侧,读值用于右偏判断
S2左侧光电lightLeft双传感器夹线时使用
S3触碰传感器touchFront碰撞急停或触发反转
S4超声波sonarFront20 英寸内减速或停车
motorA左轮电机leftMotor正转对应车前进
motorB右轮电机rightMotor正转对应车前进

下面是最小可跑程序,只做一件事:两个轮子低速同向转 1.2 秒,然后停。别小看这一步,它能同时验证端口、方向、电池和固件下载:

#pragma config(Sensor, S1, lightRight, sensorLightActive) #pragma config(Sensor, S2, lightLeft, sensorLightActive) task main() { bFloatDuringInactiveMotorPWM = false; // 制动模式,停车不滑行 nMotorEncoder[motorA] = 0; nMotorEncoder[motorB] = 0; motor[motorA] = 40; // 左轮低速正转 motor[motorB] = 40; // 右轮低速正转 wait1Msec(1200); // 固定跑 1.2 秒,观察直线偏移 motor[motorA] = 0; motor[motorB] = 0; wait1Msec(300); // 留一点时间看是否滑行 }

逻辑说明:先清编码器,再给功率,再用固定延时停车,目的是把“能跑”和“能停”分开看。参数说明:40 是低速档,适合在桌面上看方向;如果车不动,先查电池和电机线,再把功率加到 55 试一次。wait1Msec(1200)不是精确行驶时间,只用来做重复性观察。跑完这一步,如果车往左偏,不要急着改代码,先交换左右电机线或检查轮子阻力;机械不对称靠软件补,会把巡线阈值一起带偏。实验报告里 10.03 秒的完成时间是在特定结构、特定电量和特定阈值下得到的,只能当基线,不能直接抄成目标。

3. 光电巡线闭环:单传感器边沿跟随与双传感器纠偏的代码落地

光电传感器是巡线车的眼睛,但它读到的是一个受高度、环境光、地面材质和电池电压影响的反射强度,不是干净的“黑/白”布尔值。实验报告里用 45 作为分界,让光值大于 45 时前进、小于等于 45 时停止,这个例子适合讲原理,不适合直接上线跑。黑线会吸光,白底反光强,所以黑线读数低、白底读数高;但传感器离地高度变一点,整段读值就会平移,原来 45 的阈值可能变成 38 或 52。先校准,再谈控制。

3.1 光电读值不是“黑=0/白=100”,先做三点校准

校准不要只测黑线中心和白纸中心两个点,要测三个区间:黑线中心的最低值、黑线边缘的过渡值、白底的高值。把车拿在手里,让传感器慢慢从白底推过黑线,同时把SensorValue[lightRight]打到 LCD 调试页,记录最小值、最大值和抖动范围。常见做法是取黑线最大值和白底最小值的中间做阈值,再留 3 到 5 个单位的滞回,避免车在边界上反复触发。实验报告里提到的 45 只能当起点,真正上线要用你自己的地面和高度重新测。

校准项记录内容用法
黑线中心连续 10 次读值的最小/最大判断黑线判定上限
白底连续 10 次读值的最小/最大判断白底判定下限
过渡区手推车过线时的跳变范围设置滞回,防止抖
电池电压满电和跑 10 分钟后的读值判断阈值是否需要补偿

我一般会把阈值写成两个而不是一个:blackLine表示低于它就算压到黑线,whiteFloor表示高于它才算回到白底,中间是灰区。灰区里不做急转,保持上一拍或直行,能明显减少画龙。这个做法比单阈值多两个参数,但调试时能看清楚车到底是“没看到线”还是“在线上抖”。如果 LCD 上读值跳动超过 8 个单位,先查传感器高度和环境光,不要靠软件滤波硬压,滤得太狠会让车反应慢半拍。

3.2 单传感器边沿跟随与双传感器纠偏的取舍

单传感器边沿跟随的结构最简单:传感器放在黑线右侧,车不断在“压到黑线”和“回到白底”之间切换。压到黑线时让左轮慢、右轮快,车头向右修正;回到白底时让左轮快、右轮慢,车头向左修正。车就是这样贴着黑线边沿往前走。它的优点是少一个传感器、少一组阈值,窄线和小场地也能跑;缺点是车身会周期性摆动,速度越快摆得越明显。

双传感器夹线更适合稳定巡航:左传感器压黑线就向右修,右传感器压黑线就向左修,两个都白就直行,两个都黑就当成横线或急弯直冲。它的逻辑清晰,但两个传感器的间距、高度和一致性要求更高。实验报告的改进建议里提到,一开始用两个光电传感器反而走了弯路,后来把传感器移到离轮轴更近的位置才好转。这不是说双传感器不好,而是说结构没定之前,双传感器的两个阈值会互相打架。我的建议是:先用单传感器把差速和基础功率跑通,再用双传感器做稳定版。

策略优点边界适合场景
单传感器边沿跟随结构简单,调试参数少车身摆动,速度上限低窄线、短圈、快速验证
双传感器夹线直线稳定,纠偏方向明确间距和高度敏感标准黑线、可重复圈速
双传感器加灰区抗抖动更好参数多,需要 LCD 记录课程设计、需要讲清调参过程

3.3 巡线主循环代码:三区阈值、差速修正和采样周期

先看单传感器版本。它把读值分成黑线区、灰区、白底区,三个区给不同的左右轮功率。代码里的端口名和阈值都来自前面的校准,不要直接抄 38 和 52 就上车:

#pragma config(Sensor, S1, lightEdge, sensorLightActive) int blackLine = 38; // 低于它判为黑线,按实测改 int whiteFloor = 52; // 高于它判为白底,按实测改 int baseSpeed = 42; // 巡航基础功率 int turnInner = 18; // 修正时内侧轮功率 void drive(int left, int right) { motor[motorA] = left; motor[motorB] = right; } task main() { wait1Msec(200); // 上电稳定,避免第一拍读到异常值 while (true) { int v = SensorValue[lightEdge]; if (v < blackLine) { drive(turnInner, baseSpeed); // 压到黑线,向右修正 } else if (v > whiteFloor) { drive(baseSpeed, turnInner); // 回到白底,向左修正 } else { drive(baseSpeed, baseSpeed); // 灰区直行,减少抖动 } wait1Msec(15); // 采样周期,15ms 左右比较跟手 } }

逻辑说明:drive()只是把左右功率写进电机,避免主循环里重复两行。黑线区让左轮慢、右轮快,车头向右;白底区反过来。灰区不急着修正,是为了让车在边界上有一点“记忆”。参数说明:blackLine和whiteFloor必须用 LCD 读值校准;baseSpeed = 42对应中低速,场地小就降到 35,直道多可以试 50;turnInner = 18不能太低,低于 15 时内侧轮可能被地面阻力卡住;wait1Msec(15)是控制周期,改到 5 毫秒会让修正过于敏感,改到 40 毫秒以上又容易冲过线。

双传感器版本把方向判断写得更直白,适合夹线跑:

#pragma config(Sensor, S1, lightRight, sensorLightActive) #pragma config(Sensor, S2, lightLeft, sensorLightActive) int threshold = 42; // 两个传感器共用一个阈值前,先确认一致性 int fast = 55; // 外侧轮功率 int slow = 20; // 内侧轮功率 void setMotor(int l, int r) { motor[motorA] = l; motor[motorB] = r; } task main() { wait1Msec(200); while (true) { int leftBlack = SensorValue[lightLeft] < threshold; int rightBlack = SensorValue[lightRight] < threshold; if (leftBlack && !rightBlack) { setMotor(slow, fast); // 左压线,向右修 } else if (!leftBlack && rightBlack) { setMotor(fast, slow); // 右压线,向左修 } else if (leftBlack && rightBlack) { setMotor(fast, fast); // 双黑,横线或急弯,直冲 } else { setMotor(fast, fast); // 双白,直行找回黑线 } wait1Msec(10); } }

逻辑说明:leftBlack和rightBlack是每一拍的布尔判断,四个分支覆盖左压线、右压线、双黑、双白。双黑时直冲不是万能解,遇到连续横线要加状态机或延时退出,否则车会一直冲。参数说明:threshold两个传感器共用一个值之前,先把两个传感器放在同一块白底和同一段黑线上对比,差值超过 6 个单位就分别设阈值;fast = 55、slow = 20的差值决定修正力度,差值太小转不动,太大就画龙。采样周期 10 毫秒比单传感器版本更紧,因为双传感器需要更快地判断左右状态。

注意:代码里的motorA默认是左轮、motorB默认是右轮。如果你的车架反了,不要改四个分支,先把setMotor()里的两个赋值交换,保持逻辑层方向一致。

4. 避坑与排查:巡线车抖、冲出线、读值乱跳的现场记录

巡线车的故障很少是单点故障,通常是传感器、电机、结构、程序四层叠在一起。下面五条记录来自课程实验和后续复现里最常见的翻车场景,每条按“现象 → 原因 → 解决”拆开,方便对着车逐项排除。排查顺序建议从传感器读值开始,再看电机方向,最后才改控制参数;反过来做,很容易把机械问题调成代码问题。

4.1 传感器层:读值乱跳与过弯丢线

现象一:LCD 上光电读值在 20 到 70 之间乱跳,车一上线就左右猛打。原因通常不是传感器坏了,而是安装高度太高或太低。太高时光斑散开,黑线边缘的反射光混进白底;太低时传感器几乎贴地,地面反光和轮子震动都会放大。实验报告里特别提到,光电传感器离地高度不同,车体直立和同样倾角下采集的数值不同,控制器参数也要跟着变。解决方法是把传感器固定在离地 8 到 15 毫米的区间内逐档试,每档记录黑线低值、白底高值和抖动幅度,选抖动最小的那一档,再把阈值写成黑线上限和白底下限两个值。

现象二:直道很稳,一进弯就丢线,车冲出去后原地转。原因是传感器装得离轮轴太远。车转向时传感器所在位置的旋转半径大、线速度高,读值到执行之间有了延迟,等电机响应时车头已经过了黑线。实验报告的改进建议里写得很直白:光电传感器在 y 轴上与轮轴距离过远,会导致转向时传感器旋转半径大、线速度过大,信号延迟误差过大,最后转向频繁且卡顿;把传感器移到离轮轴更近的位置后好转。解决方法是先改结构,把传感器往轮轴方向挪,再降基础功率,最后才调转向内侧轮功率。结构不改,参数只能压一时。

4.2 电机与供电:起步不走与左右不对称

现象三:代码给 30 的功率,轮子只响不转,给到 50 才走;跑几分钟后同样的 50 又走不动。原因是电机低速死区和电池电压下降叠加。motor[]的功率不是恒定转速,电量低时占空比对应的扭矩下降,静摩擦一顶就停。解决方法是把巡线基础功率留出余量,别贴着死区跑;满电时标定的阈值和功率,在电量下降后要复测一次。我一般会在 LCD 上同时显示光电值和当前功率,跑三圈后看功率有没有被程序自动拉高;如果没有,就手动把baseSpeed加 5 再试。别用“满电参数”跑完整节课,后半段翻车多半是电压问题。

现象四:车直行时慢慢往左偏,代码里左右功率明明一样。原因是左右电机内阻、齿轮箱阻力、轮子摩擦不一致,开环功率补不了。解决方法是先交换左右电机线,确认偏移跟着电机走还是跟着车架走;跟着电机走就换电机或加补偿,跟着车架走就查轮子是否卡、轴是否弯。软件补偿只能在直行段加一个小偏置,比如motor[motorA] = base + 2; motor[motorB] = base;,但这个偏置会污染转向逻辑,能机械解决就不要用软件硬补。实验报告里 10.03 秒的圈速是在特定车况下得到的,换一套轮子或换一块电池,参数就要重标。

4.3 结构与程序:传感器位置、循环周期和调试占用

现象五:车在线上高频抖动,转向分支每秒切换几十次,电机发出咔哒声。原因可能是循环周期太短,也可能是灰区太窄。wait1Msec(5)配上单阈值,光电读值的噪声会直接变成电机指令;灰区只有 2 个单位时,车在边界上每一拍都换方向。解决方法是把采样周期放到 10 到 20 毫秒,把黑线阈值和白底阈值拉开 8 到 12 个单位,灰区里保持直行或保持上一拍方向。双传感器版本还要检查两个传感器的安装间距:间距太窄,两个都容易同时压黑线,分支会频繁进“双黑直冲”;间距太宽,车已经偏出线了还没触发修正。先把结构间距定死,再调阈值,顺序不能反。

还有一个容易被忽略的坑:在线调试窗口和 LCD 刷新会占用主循环时间。调试时车看起来反应慢,不一定是代码逻辑慢,可能是每拍都在刷屏。解决方法是把显示刷新降到每 5 到 10 拍一次,或者只在停车时打印;巡线主循环里只保留必要的传感器读取和电机写入。课程实验报告里把 LCD 显示给定值和反馈数据作为扩展要求,这个功能对调参很有用,但要让它服务于调试,不要让刷新拖慢控制周期。

5. 进阶整定与验证:用 LCD 调试窗口把 10 秒级圈速压稳

固定差速修正跑通后,可以把“压线就固定慢内侧轮”换成一个小 P 控制:用左右光电读值差算误差,再把误差乘系数变成转向量。它不需要完整 PID,先把 P 调稳,积分和微分在巡线这种短周期场景里反而容易引入新问题。下面这段代码保留三区阈值做安全边界,只在灰区附近做比例修正,避免直线段被噪声带偏:

#pragma config(Sensor, S1, lightRight, sensorLightActive) #pragma config(Sensor, S2, lightLeft, sensorLightActive) int baseSpeed = 45; float kp = 0.8; // 先小后大,0.4 起步更稳 task main() { wait1Msec(200); while (true) { int l = SensorValue[lightLeft]; int r = SensorValue[lightRight]; int error = r - l; // 左右读值差,方向按实际安装调 int turn = (int)(kp * error); if (turn > 18) turn = 18; // 限幅,防止猛打 if (turn < -18) turn = -18; motor[motorA] = baseSpeed + turn; motor[motorB] = baseSpeed - turn; wait1Msec(15); } }

逻辑说明:error是两个光电读值的差,turn是转向修正量,左右轮一加一减形成差速。限幅 18 是防止某一拍读到异常值时电机猛打。参数说明:kp从 0.4 开始,车摆得厉害就降到 0.2,弯道跟不上就升到 1.0 左右;baseSpeed先固定在 40 到 45,调好kp再加速度。这个结构比固定差速柔和,但前提是两个光电传感器一致性够好,否则error的零点会漂。

验证不要只看一圈。我一般会按同一块场地、同一块电池、同一组阈值跑三次,记录圈速、丢线次数和最大修正量,取中位数。下表是一次典型整定记录,数值只作为记录格式参考,实际以你的车为准:

轮次baseSpeedkp采样周期圈速丢线次数备注
1400.415ms12.8s1弯道偏慢
2450.615ms11.2s0直道轻微摆
3450.815ms10.6s2急弯修正过猛
4420.615ms10.9s0稳定优先

最后说一个血泪经验:不要凭手感改阈值。每次改完blackLine、whiteFloor或kp,都强制跑三圈并记录 LCD 上的光电值和修正量;如果三圈里有两圈丢线,这次参数就不算通过。从那以后我每次动巡线参数都先架空空转看方向,再低速跑一圈,最后才上正常速度,省下来的返工时间比调参时间多得多。需要对照原始实验报告、完整工程和接线记录的话,按资源页的文件结构取用即可;希望帮到你。

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