ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Arduino小车闭环控制:编码器测速+PID+超声波避障实战

Arduino小车闭环控制:编码器测速+PID+超声波避障实战 很多 Arduino 小车教程停在“会用 L298N 控制正反转”再往后就是一坨互相覆盖的 delay 和 if小车要么撞墙要么越跑越快要么根本走不直。真正要把“能遥控的小车”升级成“能自己导航的小车”绕不开五件事编码器测速、中断计数、滤波、PID 闭环、超声波避障。把这些一个个落地到 Arduino Uno R3 上就是《一个项目速通单片机》第 2 章的主线。这一章的“导航”不是 GPS、不是地图、也不是 SLAM而是让小车具备最基本的自主运动能力知道自己轮子转得对不对、能不能稳定到达目标速度、前方有障碍会不会转向、给一个距离能不能准确停下来。换句话说是从“开环遥控”跨到“闭环控制”。这篇文章不打算只给你贴一套完整代码因为不同商家的小车电机减速比、码盘线数、轮径都不一样照抄一定跑不对。我会把整条控制链路拆开讲引脚资源怎么分配、编码器脉冲怎么读取、滤波为什么必须做、增量式 PID 怎么写、超声波云台和状态机怎么组织、速度规划与位置控制怎么落到定时中断里。你看完拿自己的 Arduino Uno R3 小车照着标定流程走一遍应该能自己调出一台“走直线不偏、遇障碍会拐、停 1 米误差小于几厘米”的小车。1. 本章核心能力速览能力项说明主控平台Arduino Uno R3ATmega328P16MHz测速方案电机自带增量式编码器 外部中断脉冲计数闭环算法增量式 PID固定周期定时中断执行感知方案HC-SR04 超声波 舵机云台扫描运动规划梯形/斜坡速度规划脉冲累计位置控制自动避障状态机决策根据左/中/右距离选择转向输出控制双路直流减速电机 PWM 调速差速转向调试手段串口打印 Serial Plotter 观察目标速度与实际速度本章终点现象小车能稳定巡航、自动避开纸箱、按设定距离停下这一章会把 Arduino Uno R3 上十分紧张的中断、定时器资源都用起来。UNO 只有 D2、D3 两个外部中断脚刚好接左右两个编码器定时器一共三个还要兼顾 millis、PWM 和舵机库分配错了代码能编译但功能就是不对。第 2 章“速通”的重点不是抄代码而是把单片机资源如何分配、任务如何分时调度讲清楚。2. 先理清控制链路从开环到闭环如果你还在用“给固定 PWM车就往前跑”的思路这就是典型的开环控制。开环的问题不是小车不动而是小车不“知道”自己动了多少。电池电压高一点PWM 100 时轮子转得飞快电压低一点同一串 PWM 转速立刻掉下来。再加上地面摩擦、转弯负载变化车速根本不是一个固定 PWM 能压住的。要让小车“自己导航”必须先有反馈。反馈的钥匙就是直流减速电机后面那个小码盘。码盘跟随电机旋转霍尔元件或光电对管会输出方波脉冲脉冲数正比于电机转过的角度。Arduino 用外部中断数这个脉冲就知道轮子实际转了多少。测出实际转速后把“目标速度”和“实际速度”的误差交给 PIDPID 输出新的 PWM 值反复纠正这就是闭环。避障和导航属于更高一层的行为规划。它的输入来自超声波测距或者说是“前方有没有障碍、障碍在左侧还是右侧”。但这层规划不能直接去操作电机它应该只决定“目标速度设为多少、目标转向角度是多少”具体的转速稳定交给底层速度环处理。所以整章代码建议分成三层硬件中断层编码器脉冲计数。定时中断层每 10ms 计算实际速度执行 PID刷新电机 PWM。主循环层超声波扫描、避障状态机、速度目标更新不阻塞底层控制。这个层次理清了后面写代码就不会乱。3. Arduino Uno R3 引脚与定时器资源分配Arduino Uno R3 的 ATmega328P 是 8 位单片机RAM 只有 2KB没法像树莓派那样直接把传感器数据全部缓存下来做高级滤波。设计小车程序时必须节省资源尤其是定时器。先看外部中断UNO 只支持 D2、D3 两个外部中断恰好对应左右两个驱动轮的编码器 A 相。编码器 B 相可以不接中断把它接到普通 IO 上用于判断方向。做速度闭环时只数 A 相脉冲就够了做更精细的方向识别时再通过 B 相电平判断正反转。再看定时器Timer0 被 Arduino 的 millis() 占用Timer1 被 Servo 库默认占用Timer2 可以被 MsTimer2 等定时器库占用。PWM 引脚和定时器是绑定的Timer0 控制 D5、D6Timer1 控制 D9、D10Timer2 控制 D3、D11。换句话说某个引脚一旦被 Servo 或定时器库占用它的 analogWrite 就可能失效。下面给一套常见接线方案。不同电机驱动板引脚定义不同接线前一定要对照你自己的原理图。外设引脚建议占用的单片机资源左编码器 A 相D2外部中断 INT0右编码器 A 相D3外部中断 INT1电机 PWMD5、D6Timer0电机方向D4、D7、D8、D12普通 GPIO舵机信号线D9Servo 库默认占用 Timer1超声波 TrigD11普通 GPIO超声波 EchoD10普通 GPIO舵机尽量接到 D9因为 Arduino Servo 库默认使用 Timer1。如果你把舵机接在其他脚库仍然占用 Timer1所以 D9、D10 的 analogWrite 就不能再当普通 PWM 用了。电机调速避开 D9、D10使用 D5、D6 更稳妥。定时中断如果使用 MsTimer2它会占用 Timer2此时 D3、D11 的 analogWrite 也会受影响。电源是另一个很容易翻车的地方。两个电机同时启动电流很大SG90 舵机堵转电流也可以到几百毫安不要直接从 Arduino 的 5V 引脚给舵机和超声波模块供电。推荐用 7.4V 锂电池给电机驱动板供电再通过驱动板的 5V 输出或者独立的 BEC 稳压给 Arduino、舵机和超声波模块供电同时把电源地线连在一起。4. 编码器测速与外部中断计数先写一个最原始的脉冲计数程序验证编码器能不能被 Arduino 正确读到。// 编码器脉冲计数测试程序 const int ENC_LEFT_PIN 2; // 左轮编码器 A 相接 D2 volatile long leftPulseCount 0; // 中断里修改必须 volatile void leftEncoderISR() { leftPulseCount; // 只做计数不打印、不延时 } void setup() { Serial.begin(115200); pinMode(ENC_LEFT_PIN, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_LEFT_PIN), leftEncoderISR, RISING); } void loop() { Serial.print(left pulse ); Serial.println(leftPulseCount); delay(500); }这段程序里有两个必须注意的地方。第一leftPulseCount加了volatile因为它在中断里被修改主循环要读取不加 volatile 编译器可能优化出错误结果。第二ISR 里只能做“计数”这一类最短操作不能在中断里调用Serial.print、delay这类耗时函数否则会抢占主程序并且可能引起数据错乱。让小车轮子悬空手动转轮子或者给电机一个固定 PWM如果串口打印的数字在增加说明编码器接线正确。如果始终是 0先检查 A 相是否接在支持外部中断的 D2/D3再看信号线是否共地最后检查编码器供电电压。编码器脉冲数本身没有意义要把它换算成转速。小车电机资料里一般会有两个参数码盘每转输出多少脉冲以及减速比是多少。比如某个电机内部码盘每转输出 20 个脉冲减速比 30那么输出轴转一圈对应约 600 个脉冲。这个数字必须按你自己的电机实际查表填写不能照抄。// 编码器测速换算示例 #define PULSES_PER_REV 600.0f // 编码器输出轴每转脉冲数必须按电机资料改 #define CONTROL_PERIOD_MS 10.0f // 控制周期10ms volatile long leftPulseCount 0; long lastLeftPulse 0; // 在 10ms 定时中断里调用一次 float getLeftSpeedRPM() { long delta leftPulseCount - lastLeftPulse; lastLeftPulse leftPulseCount; float revPerSec (float)delta / PULSES_PER_REV / (CONTROL_PERIOD_MS / 1000.0f); float rpm revPerSec * 60.0f; return rpm; }如果你以后想做双向计数、判断电机正反转可以把 B 相也接进普通 GPIO在 A 相上升沿中断里读取 B 相电平。B 相为高说明正转为低说明反转并决定脉冲计数加一还是减一。但单纯的速度闭环场景先做单相脉冲计数更简单可靠。5. 滤波读取到的速度为什么一直在跳把上面的测速值通过串口打印出来你会看到转速永远在一个区间里跳比如 500 RPM 在 490 到 515 之间不断波动。这个波动不完全是 PID 没调好更多是编码器码盘遮挡不均匀、齿轮间隙、脉冲丢失、控制周期固定但脉冲相位非整数等原因造成的量化噪声。直接拿这个抖动严重的速度去做 PID微分项会被噪声放大PWM 输出会跟着发抖电机声音会变得很难听。所以在速度环前面加一级滤波非常必要。最简单的做法是滑动平均滤波用一个固定长度窗口每次取最近 N 个测量值的平均值。N 太小滤波效果差N 太大响应变慢一般取 5 到 10 比较合适。// 滑动平均滤波窗口 8 #define FILTER_SIZE 8 float speedBuf[FILTER_SIZE]; int bufIndex 0; float bufSum 0; bool bufFull false; float smoothSpeed(float rawSpeed) { bufSum - speedBuf[bufIndex]; speedBuf[bufIndex] rawSpeed; bufSum rawSpeed; bufIndex (bufIndex 1) % FILTER_SIZE; if (bufIndex 0) bufFull true; if (bufFull) { return bufSum / (float)FILTER_SIZE; } else { return rawSpeed; // 窗口未满时直接返回原始值 } }更进一步的方案是限幅滤波加滑动平均。先判断当前测量值和上一次测量值相差是否超过限制超过则认为这次测量是异常跳变丢弃或替换成上一次值然后再进入滑动平均。这样既能抑制偶发的脉冲丢失也不会把相邻两次真实变化完全削平。滤波会影响 PID 的响应速度。滤波太狠你会发现速度环反应迟钝起步、刹车都有明显滞后。建议把滤波放在测速之后、PID 计算之前同时控制周期保持固定这样滤波器相位滞后才可知、可补偿。6. PID 速度闭环让 PWM 自己跟着目标走PID 是这一章真正的主角。单片机知识如果不和 PID 结合起来你很难理解“控制算法”四个字到底改变了什么。速度 PID 的工作流程如下定时器每 10ms 触发一次算出当前实际速度用目标速度减去实际速度得到误差PID 根据误差历史计算出应该输出的 PWM再写给电机驱动。增量式 PID 比位置式 PID 更适合这种资源少、输出是 PWM 的场合。位置式 PID 需要累计所有历史误差容易积分饱和而且一旦误差累计值异常输出会大幅跳变。增量式 PID 只计算本次输出相对上一次输出的增量然后叠加到上一次输出上天然适合增量控制的电机调速。// 增量式 PID 结构体和更新函数 typedef struct { float kp; // 比例系数 float ki; // 积分系数 float kd; // 微分系数 float err; // 当前误差 float lastErr; // 上一次误差 float preErr; // 上上次误差 float output; // PID 输出即 PWM 值 float outputLimit; // 输出限幅 } IncrementalPID; float incrementalPIDUpdate(IncrementalPID *pid, float target, float current) { pid-err target - current; float dErr pid-err - pid-lastErr; float ddErr pid-err - 2.0f * pid-lastErr pid-preErr; pid-output pid-kp * dErr pid-ki * pid-err pid-kd * ddErr; // 限幅防止 PWM 输出超过电机安全范围 if (pid-output pid-outputLimit) pid-output pid-outputLimit; if (pid-output -pid-outputLimit) pid-output -pid-outputLimit; pid-preErr pid-lastErr; pid-lastErr pid-err; return pid-output; }这段代码里的输出既包含正 PWM 也包含负 PWM。正数代表小车前进方向负数代表后退方向实际调用时写电机函数需要把 PWM 取绝对值再根据正负号设置方向引脚。PID 的周期很重要。增量式 PID 的“增量”必须建立在固定采样周期上因为你每次更新的是误差变化量如果两次计算间隔忽长忽短KP、KI 参数的效果就会完全乱掉。所以速度环不要放在使用 delay 的主循环里要放进定时器中断推荐 10ms 到 20ms。这个时间足够 ATmega328P 执行几次浮点运算又不会把电机响应拖慢。在定时中断里调用 PID 的示例#include MsTimer2.h IncrementalPID speedPid; volatile float targetSpeed 0.0f; // 目标速度由主循环修改 float currentSpeed 0.0f; float pwmOut 0.0f; void controlTimer10ms() { // 1. 读取编码器增量并计算转速 currentSpeed getLeftSpeedRPM(); currentSpeed smoothSpeed(currentSpeed); // 2. 只对左轮做速度环演示右轮同理 pwmOut incrementalPIDUpdate(speedPid, targetSpeed, currentSpeed); // 3. 写电机驱动方向根据 pwmOut 正负决定 setMotorPWM((int)fabs(pwmOut), pwmOut 0 ? DIR_FORWARD : DIR_BACKWARD); } void setup() { speedPid.kp 1.5f; // 需要现场整定这里只是初始值 speedPid.ki 0.05f; speedPid.kd 0.0f; speedPid.output 0.0f; speedPid.lastErr 0.0f; speedPid.preErr 0.0f; speedPid.outputLimit 200.0f; // 不给满 PWM避免起步打滑 MsTimer2::set(10, controlTimer10ms); MsTimer2::start(); }调参顺序比参数本身更重要。先把 KI、KD 设为 0只留 KP设定一个固定目标速度通过 Serial Plotter 观察实际速度曲线。当小车出现轻微连续震荡时记下此时 KP把它降回 60% 左右再缓慢增加 KI让稳态误差消失。KD 在测速噪声大时容易出现反效果很多电机小车场景干脆不加 KD。调参时最好把车轮架起来悬空空转避免小车在桌子上乱冲调试安全比参数精度重要。7. 速度规划起步不猛冲停车不点头PID 能稳住速度但不能决定“速度怎么变化”。如果你一上来就把目标速度设成 500 RPM车会瞬间满 PWM 加速轮子很容易打滑编码器计数会丢失PID 还会误以为速度不够继续加大输出形成恶性循环。所以导航类小车还必须做速度规划。最简单的速度规划是斜坡加速float desiredSpeed 260.0f; // 用户希望到达的速度 float targetSpeed 0.0f; // PID 实际使用的目标速度 float accelStep 8.0f; // 每个控制周期目标速度增加量 // 在主循环里周期调用比如每 50ms void updateSpeedPlan() { if (targetSpeed desiredSpeed) { targetSpeed accelStep; if (targetSpeed desiredSpeed) targetSpeed desiredSpeed; } else if (targetSpeed desiredSpeed) { targetSpeed - accelStep; if (targetSpeed desiredSpeed) targetSpeed desiredSpeed; } }如果想让车更柔和可以做成三段式梯形规划先按固定斜率加速达到预定最大速度后匀速接近目标位置或停车点时再按斜率减速。复杂一点的 S 型曲线在小车场景里并不必须因为 UNO 的运算能力和内存有限梯形规划已经能解决大部分起步打滑和停车过冲问题。速度规划对超声波避障也有实际意义。超声波检测到前方障碍后不要直接从 500 RPM 刹到 0那样小车会因为惯性继续滑行实际刹车距离难以预测。更稳的做法是先把目标速度规划到低速比如 100 RPM再根据超声波距离判断是否停车和转向。这样底层的速度环始终是平滑的避障行为不会让 PWM 突然跳变。8. 超声波避障云台扫描与状态机决策测速解决的是“动得稳不稳”避障解决的是“该不该转向”。本章避障方案使用 HC-SR04 超声波测距模块配合舵机云台让超声波模块可以旋转测量左、中、右三个方向的距离再做出转向决策。超声波测距需要用到pulseIn阻塞测量在单片机小车里最稳妥的做法是把测距放在主循环中控制中断仍然由定时器负责二者互不阻塞。测量代码很简单// HC-SR04 测距超时返回 -1 const int TRIG_PIN 11; const int ECHO_PIN 10; long getDistanceCM() { digitalWrite(TRIG_PIN, LOW); delayMicroseconds(2); digitalWrite(TRIG_PIN, HIGH); delayMicroseconds(10); digitalWrite(TRIG_PIN, LOW); long duration pulseIn(ECHO_PIN, HIGH, 30000); // 30ms 超时 if (duration 0) return -1; return duration / 58; // 声速往返换算成 cm }注意不要只测一次就信这个值。超声波对软海绵、倾斜表面、细线、玻璃等物体可能误判偶尔会出现一个特别近或特别远的值。建议对同一个角度连续测 3 次取中值或距离最近的一次作为安全判断依据。避障策略不需要做成复杂的路径规划用一个简单的有限状态机就能跑通。判断逻辑可以这样写小车直行每 100ms 测一次正前方距离。正前方距离大于安全阈值比如 25cm继续直行。正前方距离小于阈值停车并让舵机云台转向左侧测左方距离。舵机云台再转向右侧测右方距离。如果左侧距离明显更大就左转否则右转。转向完成后舵机云台回正目标速度重新开始斜坡加速。这个判断逻辑会带来一个常见问题小车在狭窄区域可能左右摇摆、反复横跳。解决办法是加上“转向锁定时间”也就是每次避障转向后至少保持新方向 500ms不立刻重新触发避障同时把安全返回阈值设置得比触发阈值更大防止在两个阈值之间反复切换。舵机云台扫描要避免在主循环里连续使用多个 delay。舵机从 0 度转到 180 度需要几百毫秒如果每一步都阻塞在 delay 上避障流程会变得非常卡顿。建议用毫秒级时间戳的非阻塞方式实现或者接受避障时短暂停车的现象。对于教学车来说停车后再扫描方向比边跑边转更容易理解和排查。9. 位置控制按里程停到目标点这一章的“位置控制”不是地图坐标定位而是通过累计编码器脉冲控制小车实际行驶的距离。基础原理是把目标距离换算成目标脉冲数编码器每加一个脉冲就让累计值加一当累计值达到目标脉冲数时停车。位置控制不能单独存在它必须建立在速度环之上。如果只有位置控制没有速度环你很难同时保证“平均速度合适”和“终点精确刹车”。正确的做法是目标距离未到之前速度环先把实际速度稳定在规划速度附近距离接近目标时再平滑降低目标速度最后发出停车指令。// 位置控制伪代码按实际电机参数替换常量 const float PULSES_PER_REV 600.0f; // 每转脉冲数 const float WHEEL_DIAMETER_MM 65.0f; // 轮径需实际测量 const float WHEEL_CIRCUM_MM PI * WHEEL_DIAMETER_MM; const float PULSES_PER_MM PULSES_PER_REV / WHEEL_CIRCUM_MM; long targetDistancePulses (long)(1000.0f * PULSES_PER_MM); // 走 1 米 long startLeftPulse 0; long travelPulse 0; // 主循环或控制状态机中调用 void runGoStraightDistance() { targetDistancePulses (long)(1000.0f * PULSES_PER_MM); startLeftPulse leftPulseCount; targetSpeed 220.0f; // 开始位置控制时给目标速度 while (travelPulse targetDistancePulses) { travelPulse leftPulseCount - startLeftPulse; if (rightPulseOffsetTooLarge()) { // 走偏处理根据左右轮脉冲差修正目标速度 fixDirectionByPulseDiff(); } delay(5); } targetSpeed 0.0f; // 停车交给速度环 }直行不走直线是差速小车最常见的问题。两边电机转速标定不同、轮子直径差异、地面摩擦力不同都会导致小车跑偏。简单的纠偏方法是比较左右轮脉冲累计值把它们之差乘上一个很小的比例系数加在转弯方向一侧的速度上。比如左轮脉冲总是比右轮多说明车在往右偏那就把左侧目标速度降低一点或右侧目标速度提高一点。这个修正量应该限制在总速度的 10% 以内否则会变成明显转弯。位置控制的停车精度受很多因素影响包括响应延迟、刹车惯性、轮胎打滑、目标速度过高。所以位置控制场景里速度规划更重要目标距离还剩最后 20cm 时就应该把目标速度降到 100 RPM 以下避免高速下 PID 已经停了但车还在滑。拿一把卷尺实际测一下停车位置把误差计算进程序里是工程上非常正常的校准方式。10. 把整个系统组织进 Arduino 主程序Arduino Uno R3 的资源有限程序一旦把避障、PID、舵机、测距全部堆在一起没有清晰调度必定出问题。建议把整个系统按时间片分层组织。任务执行频率所在位置编码器脉冲计数硬件中断触发ISRD2/D3编码器数据与 PID 计算每 10msTimer2 中断避障状态机运行每 50ms 到 100ms主循环中按 millis 判断超声波测距需要时调用主循环做超时保护舵机云台扫描避障状态机内部主循环非阻塞方式主循环不能再用无尽 delay 写流程。超声波测距天然带阻塞时间舵机转动也有等待时间所以更好的模式是状态机配时间戳。主循环每次都检查当前时间是否到了任务周期而不是死等某个事件完成。底层的 PID 在定时器中断里独立运行即使主循环正在测距和转舵机轮速也能保持稳定。这里提醒一个所有 Arduino 新手都会踩的坑ISR 里绝对不能出现Serial.print、delay、动态内存分配、过于复杂的数学运算。测速代码里经常有人为了方便调试在中断里直接打印脉冲数结果程序跑着跑着就死机。正确做法是先让中断把计数存到全局变量主循环或另一个低频定时任务再去打印、显示。11. 功能测试与效果验证11.1 编码器测速测试先把小车架起来四个轮子或两个驱动轮离地然后手动转动车轮观察串口打印的脉冲数。确认脉冲数稳定增长后给电机一个固定 PWM比如 120分别打印左右轮速度。左右速度相差很大是正常的因为电机和机械结构不可能完全一致这正是后面 PID 和差速纠偏要解决的问题。判断标准是脉冲数单调增加、无跳变归零、速度值不会每隔几秒突然变成 0。11.2 PID 闭环测试把两块纸板夹在车轴附近或者让车悬空用 Serial Plotter 显示目标速度和实际速度两条曲线。先只加 KP目标速度设置到 250 RPM。观察实际速度是否能在 1 秒内追到目标附近如果持续震荡说明 KP 太大。把 KP 调稳后慢慢加 KI观察稳态误差能否消除。判断成功的最直观指标是两条曲线在 Serial Plotter 上几乎重合而 PWM 输出没有频繁大幅抖动。11.3 超声波避障测试在车前方大约 20cm 处放一个纸箱。正确的现象是小车在接近纸箱后减速停车舵机云台转到左侧测距再转到右侧测距随后向空旷一侧转向回正后继续直行。测试时不要堵在悬崖或者台阶边上因为超声波对向下倾斜的楼梯测量结果不可靠防跌落不是这个功能的目标。11.4 位置控制测试用胶带在地上画一条 1m 的直线把小车的起点对准线头目标距离设成 1m。观察小车起步、匀速、减速、停车的全过程。停车后再用卷尺量误差。如果车停过了就降低接近终点时的目标速度如果没走够检查 PULSES_PER_REV、轮径等常量是否标定正确。位置误差在 5cm 以内是正常可接受的水平。12. 常见问题与排查方法问题现象可能原因排查方式解决方案编码器脉冲始终为 0A 相没接 D2/D3、信号线没共地、编码器供电异常检查接线电压和接线图重新接线并共地外部中断改到 D2 或 D3左右轮速度差过大电机差异、轮径不同、码盘线数不同打印两边脉冲和速度做对比用 PID 闭环再做差分纠偏实际速度在目标速度附近震荡KP 过大或滤波太弱看 Serial Plotter 曲线振幅降低 KP适当加大滤波窗口稳稳停在低于目标的值但消除不了误差缺少积分作用KI 为 0观察稳态误差缓慢增大 KI起步瞬间猛冲或打滑没有做速度规划目标速度突然给满查看目标速度曲线增加斜坡加速逻辑PWM 输出总是顶到限幅值PID 输出饱和、目标速度太高或电池电压不足查看 output 是否一直等于 outputLimit降低目标速度检查电源设置合理限幅舵机抖动或位置不准确舵机供电不足、和共用 PWM 定时器冲突检查供电查看程序是否占用了 Timer1单独供电确保 PWM 引脚不冲突超声波距离偶尔跳变材质吸声、角度倾斜、测量次数太少多次打印回传值观察规律连续测 3 次取中值或最近值小车一直左右反复转圈避障状态机没有锁定时间、阈值设置不当观察哪个状态反复切换增加转向锁定时间增大返回安全阈值加入 PID 后程序死机ISR 中调用阻塞函数、数组越界、全局变量未用 volatile检查中断代码和编译警告中断里只做赋值计数调试代码移到主循环13. 最佳实践先把项目车安全调稳这一章代码量看起来不大但和 Arduino 基础 GPIO 控制完全是两个难度。工程实践上有几条建议能减少很多返工。先把硬件问题消灭在调参之前。电池要充满接线要牢靠编码器插针要接触良好轮子要锁紧。很多“PID 调不好”其实不是算法问题而是某个轮子机械打滑导致速度测量不稳定。整车悬空测试时记得在车底下垫东西或者用车架支撑不要让轮子空转时车身乱动。调参时不要一上来追求完美曲线。先让车在低目标速度下稳定运行比如 200 RPM观察 PID 三个参数对系统的影响再逐步提升到 300、400 RPM。每改一次参数都要记录现象和参数值不要凭感觉乱改。这里推荐一款常见做法Kp 引发的震荡频率和幅度记录下来KI 引发的低频漂移单独观察KD 在小车这种测速噪声大的场景里尽量后置。安全边界必须明确。带电机、带舵机、带超声波的项目本质上是会移动的机电设备。第一次落地测试必须在小范围、无障碍物的室内场地进行脚下留出立即断电的余量。超声波避障只能作为教学实验功能它不是真正的安全避障系统对细线、台阶、玻璃、低矮障碍物都可能失效。不要在开放道路、楼梯口、有人员走动的公共区域让小车自主巡航。同时在合规与使用边界上也要注意如果你的小车后续加入摄像头、人脸识别、远程遥控等内容涉及到人物肖像、位置隐私、公共区域采集数据时必须确保获得授权并且仅在可控测试环境内运行。这既是技术责任也是基本的安全边界。14. 下一步可以继续扩展的方向第 2 章的核心成果是让小车从“能遥控”变成“能闭环自稳”。当你把编码器测速、中断、PID、超声波避障、速度规划和位置控制串起来以后你会发现 Arduino Uno R3 的 2KB SRAM 很快会被占满程序结构也对调度提出了更高要求。后续可以继续在这台小车上做三件事第一
返回列表