资讯详情

智能车程序实战:灰度循迹、超声波避障与PID调速全解析

📅 2026/10/29 17:16:46 | 华诺云谱 👁 阅读
智能车程序实战:灰度循迹、超声波避障与PID调速全解析
简介面向智能车竞赛四轮组的完整工程资源聚焦基础巡线、环岛识别、十字路口识别三大核心任务适合备赛学生与嵌入式算法爱好者从零搭建或优化自有方案。压缩包共622个文件约46.57MB以C/C源码、IAR工程配置、编译中间文件及调试辅助文件为主其中h/c对应核心算法ewp/eww是IAR工程结构axf/hex为可直接烧录的镜像pbi/browse等则保留调试信息。源码目录按传感器数据处理、PID控制器、图像识别、决策制定、导航和主程序划分模块边界清晰便于逐段研读或迁移复用。已有950人学习适合希望深入理解智能车感知-决策-控制链路、并需要工程模板快速上手的开发者。资源完整呈现从赛道信息采集、环岛/路口判定到电机控制的执行流程对PID调参、视觉识别和多模块协作均有实际参考价值。1. 咱们先聊聊为什么你写的智能车程序一下地就跑偏晚上带着刚焊好的车模去走廊试跑轮子一转就冲着墙冲差点撞到人。第一次跑智能车的同学十有八九都经历过这个瞬间明明是照着教程把 SmartCar 程序烧进去了拨码开关也设对了可车就是不走直线、过不了弯、进出车库全靠手推。问题多半不在焊接而在程序里对传感器、电机和逻辑的配合处理得太粗糙。本文写的就是一套能落地的智能车程序方案涵盖主控选型、灰度循迹、超声波避障、电机调速与 PID 整定以及调试和排错的具体做法。适合正在做全国大学生智能车竞赛、嵌入式课程设计或者想自己从零拼一台循迹避障车的人看。看完之后你至少能把手上的车从「能走」调到「能按你的意图走」而不是靠调阈值碰运气。实际动手前想清楚一点智能车程序并不神秘本质上就是「感知 → 决策 → 执行」的循环。把这三件事的函数写清楚车辆行为就稳定写乱了换再多传感器也白搭。下面按一条常规的开发路线逐步给你拆开讲。2. 硬件与程序骨架先定平台再谈控制一台智能车能不能按程序跑一半取决于机械和硬件接线是否干净另一半取决于主循环的结构是否合理。很多小白一上来就写几十个中断和全局变量结果电机一发抖摄像头数据就失真后边怎么调都不对。2.1 常见的主控与传感器选型我在学校实验室和工程实践中常用的方案是 STM32F103C8T6 做主控成本低、库资料全、带定时器和 ADC 够用车模底盘用常见的二驱或四驱小车架电机是 N20 减速电机或 370 电机驱动用 TB6612FNG比 L298N 效率高发热也小。传感器方面循迹用灰度传感器或红外对管阵列通常布置成 4 路或 5 路。4 路适合简单跑道5 路更适合有十字、环岛的赛道。避障超声波模块 HC-SR04 足够测距范围 2cm 到 400cm用在室内赛道里绰绰有余。如果后面想加摄像头做视觉循迹再买 OV7725 或总钻风不过那是进阶事这里先不展开。选型的核心逻辑是不要把主控带的能力当成选型加分项而是看你手头传感器的输出是模拟量、数字量还是 PWM。只要主控的 IO 数量、ADC 通道、定时器数量能满足需求就可以定案。很多同学一开始就追求「强大的主控」结果大部分引脚空着布线乱、调试难得不偿失。2.2 最小接线与 IO 分配表先画一张 IO 分配表再动焊枪。这不是形式主义是为了后面写程序时不用反复查原理图也方便排查虚焊或者接反。功能模块信号线主控引脚说明电机驱动 TB6612PWMA / AIN1 / AIN2PA8 / PB12 / PB13左电机电机驱动 TB6612PWMB / BIN1 / BIN2PA9 / PB14 / PB15右电机灰度传感器左 1OUT1PA0 (ADC)模拟量输入灰度传感器左 2OUT2PA1 (ADC)模拟量输入灰度传感器右 1OUT3PA2 (ADC)模拟量输入灰度传感器右 2OUT4PA3 (ADC)模拟量输入超声波 HC-SR04Trig / EchoPB0 / PB1触发与回波输入蓝牙/串口调试TX / RXPA9 / PA10需要接串口转 USB上电之前先用万用表量一遍正负极和信号电压确保没有接到 3.3V 和 5V 混用的地方。STM32 的 GPIO 最高容忍 5V但不代表你随便接都安全能避免的误差尽量避免。2.3 初始化与主循环怎么搭才不会乱程序骨架不建议一上来就写死在一整个 while 循环里。我一般会分三层初始化层、逻辑层、执行层。初始化层负责配置时钟、GPIO、ADC、PWM 定时器和串口逻辑层处理传感器采集和状态判断执行层调用电机驱动函数输出 PWM。下面给一段最小框架#include stm32f1xx_hal.h #define SPEED_DEFAULT 7000 // PWM 占空比初值范围 0-9999 #define SPEED_SLOW 4500 #define SPEED_TURN 6000 static void SystemClock_Config(void); static void GPIO_Init(void); static void TIM_PWM_Init(void); static void ADC_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); GPIO_Init(); TIM_PWM_Init(); ADC_Init(); uint16_t sensor[4] {0}; uint16_t distance_cm 0; while (1) { // 1. 感知读灰度与超声波的原始数据 sensor[0] read_ADC(PA0); sensor[1] read_ADC(PA1); sensor[2] read_ADC(PA2); sensor[3] read_ADC(PA3); distance_cm read_ultrasonic_cm(); // 2. 决策根据传感器状态调用方向与速度处理 intelligence_handle(sensor, distance_cm, calc_pwm_left, calc_pwm_right); // 3. 执行输出到 TB6612 set_motor(LEFT_MOTOR, calc_pwm_left, MOTOR_FORWARD); set_motor(RIGHT_MOTOR, calc_pwm_right, MOTOR_FORWARD); HAL_Delay(5); } }这段代码的逻辑很清晰每次主循环先采集 4 路灰度值和超声波距离然后传给intelligence_handle()做判断最后更新左右电机 PWM。HAL_Delay(5)是 5ms 的主循环节拍相当于把控制频率定在 200Hz 左右对室内低速车足够。如果你用的传感器需要更短的采样间隔就把延时降到 1ms但要注意 OLED 显示和串口打印不要写在控制循环里否则会拖慢周期。这个框架最大的价值是把「采集在哪、决策在哪、输出在哪」分得清清楚楚。后面你调 PID 也好改避障逻辑也好都是在intelligence_handle()里局部改动不会牵扯到初始化代码排查问题也快。3. 循迹程序灰度传感器状态怎么读车身才会跟着线走循迹是智能车程序的第一个核心功能。大部分同学的翻车现场都集中在循迹上不是车冲出赛道就是过十字路口时原地打转。这背后的原因是把循迹理解成了「检测到某一路就转某一边」而正确的做法应当是先理解传感器排列和赛道路面的映射关系。3.1 四路灰度传感器检测逻辑是怎么运作的灰度传感器的输出是模拟量黑线附近反射率低输出电压高白色赛道反射率高输出电压低。把这个模拟量跟阈值比较转成 0 或 1然后拼成一个状态码再去做转向判断。四路传感器从车身左到右依次记为 S1、S2、S3、S4排列间距大约 1.2cm刚好覆盖一条 2cm 宽的黑线。当车身在线上跑时典型状态如下完全直线S2 和 S3 压线状态码是 0110车身偏右左侧传感器 S1 压线状态码是 1000车身偏左右侧传感器 S4 压线状态码是 0001冲出赛道或完全没检测线状态码是 0000 或 1111。注意状态码的含义取决于你用的传感器是「压线输出高电平」还是「压线输出低电平」。有的模块带反相器逻辑跟上面相反你拿到手以后第一件事是用串口把四个通道的原始 ADC 值打出来确认实际电平映射关系。很多同学照着教程写方向反了还以为是传感器坏了白排查半小时。3.2 状态机转向逻辑代码状态机是循迹程序里最常用的写法。它根据当前状态码查找对应的转向策略。转向策略用左右电机的速度差来实现速度差越大转向越激进同时也是车辆过弯不稳的主要原因因此转向量要分档而不是一刀切打死方向。typedef struct { uint8_t sensor_state; uint8_t direction; int16_t left_offset; int16_t right_offset; } track_event_t; track_event_t track_table[] { {0b0110, DIR_FORWARD, 0, 0 }, {0b0100, DIR_FORWARD, 400, 0 }, {0b1100, DIR_FORWARD, 300, 0 }, {0b1000, DIR_TURN_LEFT, 1200, 400 }, {0b0010, DIR_FORWARD, 0, 400 }, {0b0011, DIR_FORWARD, 0, 300 }, {0b0001, DIR_TURN_RIGHT,400, 1200}, {0b0000, DIR_SEARCH, 800, 800 }, {0b1111, DIR_STOP, 0, 0 } }; void intelligence_handle(const uint16_t *sensor, uint16_t dist, int16_t *pwm_l, int16_t *pwm_r) { uint8_t state make_state(sensor); // 把四路 ADC 转成 0b0000~0b1111 track_event_t *event find_event(track_table, state); if (event-direction DIR_FORWARD) { *pwm_l SPEED_DEFAULT event-left_offset; *pwm_r SPEED_DEFAULT event-right_offset; } else if (event-direction DIR_TURN_LEFT) { *pwm_l SPEED_TURN event-left_offset; *pwm_r SPEED_TURN event-right_offset; } // DIR_SEARCH 和 DIR_STOP 的分支略 }这段状态表的核心是在find_event()里做查表匹配。你可以直接用 if-else 写但查表的好处是事件多了以后容易维护后续要增加弯道角度、十字路口直行策略只需要在表里加一行不用改主循环。参数说明里最值得关注的是left_offset和right_offset。直线时 offset 为 0微偏时用 300 左右严重偏离用 1200。这个量不是绝对的取决于你电机的额定转速和 PWM 范围。我把 PWM 设成 0-9999直线速度 7000弯道内侧 600012007200外侧 60004006400差量在 800 左右实测转弯比较顺。如果你发现车在弯道上转不过去优先加大外侧速度而不是减小内侧速度因为内侧轮减速太多会直接停车外侧提速反而能维持整车的通过性。3.3 阈值标定与丢线处理阈值标定的做法是把车放在赛道上用串口打印四个通道的原始 ADC 值黑线处记录一个值白色处记录一个值取中值作为初始阈值。然后跑一圈看状态码切换处是否靠近线边缘。如果切得太早就把阈值往黑线方向的 ADC 值靠一靠切得太晚就往白色方向靠一靠。这个「取中值」只是第一步后面跑起来会遇到阳光、灯光角度变化导致 ADC 漂移的问题。比较稳妥的做法是每次启动后在白色赛道上自动采集一轮基准值把它存成动态阈值。很多竞赛团队的经验是阈值不是定死的而是每 500ms 更新一次滑动平均值以防止灯光缓慢变化带来的读数漂移。丢线处理是循迹里最容易翻车的点。当四个传感器全部为 0 时有可能是车已经冲出赛道也可能是进入十字或环岛的瞬间。正确响应是保持上一次有效状态的方向继续前进一段距离同时降低速度而不是原地打转或猛打方向。状态表里的DIR_SEARCH就是干这个用的左右轮都设定为 800 偏量让车缓慢向前搜索重新捕获黑线后再恢复循迹。这种做法在多数室内赛道上是可靠的比加陀螺仪转向简单得多。4. 电机速度环 PID直线不稳、弯道甩尾怎么治很多人的智能车程序里电机控制只是「给 PWM 就走不给就停」。这套逻辑拍摄演示够用但真正上赛道之后你会发现电池电压一波动、地面摩擦一变车速就不一致直道跑成 S 形弯道甩尾。问题就出在没有速度闭环。4.1 为什么速度要闭环而不是直接调 PWM直流减速电机的转速受负载影响明显。同样占空比下电池满电和亏电时转速能差 20% 以上新轮胎和旧轮胎的摩擦系数也不一样。如果你的程序只输出固定 PWM那车速就是开环的跑快了过弯翻车跑慢了直道被超。速度闭环的核心是让程序实时读编码器反馈算目标速度和实际速度的误差再用 PID 输出修正占空比。编码器通常装在电机尾部常见型号是霍尔式或光电式的STM32 用定时器的编码器模式直接读 AB 相不用自己写退耦逻辑。4.2 增量式 PID 代码模板与参数说明低转速智能车场景下增量式 PID 比位置式 PID 更适合单片机上跑。它每次输出的是 PWM 增量不会对占空比造成剧烈的冲击起步和停车都更平顺。typedef struct { int16_t target_speed; // 目标速度单位编码器脉冲数 per 10ms int16_t actual_speed; int16_t err; int16_t err_last; int16_t err_prev; float kp; float ki; float kd; int16_t output; } pid_controller_t; static pid_controller_t pid_left { .target_speed 40, .kp 1.2f, .ki 0.05f, .kd 0.3f, }; int16_t pid_update(pid_controller_t *pid, int16_t target, int16_t actual) { pid-target_speed target; pid-actual_speed actual; pid-err pid-target_speed - pid-actual_speed; int16_t delta (int16_t)(pid-kp * (pid-err - pid-err_last) pid-ki * pid-err pid-kd * (pid-err - 2 * pid-err_last pid-err_prev)); pid-err_prev pid-err_last; pid-err_last pid-err; pid-output delta; // 输出限幅防止 PID 把占空比推到超限值 if (pid-output 9000) pid-output 9000; if (pid-output 1000) pid-output 1000; return pid-output; }参数注释里得解释一下target_speed的单位这里是每 10ms 采集到的编码器脉冲数。因为主循环是 5ms我通常累积两个周期的脉冲总数再做 PID 计算这样既避免了过短的采样周期导致的微分噪声也不会让响应慢到跟不上赛道节奏。三个参数调整的经验是先调 kp 让车开始出现轻微震荡然后退回去 0.7 倍再加 ki 消除静差但要小心低速时的积分饱和——如果车被按住动不了积分项会一路增长一松手车就猛蹿。解决方式是加抗积分饱和判断输出已经到达限幅时不再累加误差。kd 在这类低速电机上作用不明显我一般只调到刚好让曲线平滑不会过度依赖。在实际调车时你会发现「PID 参数是玄学」这话真有道理。因为电机、电池、负载、轮胎都影响最终表现同一组参数换块电池就完全不是一回事。所以我建议你做一个串口小工具把目标速度、实际速度、PID 输出周期性地发到电脑上绘图观察曲线再改参数而不是改了就跑一圈看感觉。4.3 速度环与循迹转向的配合顺序最后要聊的是速度环和转向环的耦合问题。很多同学的代码里循迹状态直接设置目标 PWM然后 PID 又去追这个 PWM两套逻辑互不知道对方结果车在弯道上内外轮的差速被 PID 拉平了转向失效。正确的配合法是循迹逻辑输出的是「左右轮目标速度」而不是最终 PWM。速度环根据目标速度分别计算两路 PWM。这样循迹只管方向和期望速度速度环管实际执行职责不重叠。示例流程是intelligence_handle()里根据状态表算出左右轮目标速度比如直线 40、微偏 35、急弯内侧 25 外侧 45然后分别调用pid_update()得到占空比最后写入 TB6612。这种结构的另一个好处是后面你要加蓝牙遥控、视觉巡线只需要换决策层底层速度环和驱动层完全不动。5. 智能车程序常见问题排查跑偏、抖动、突然停车怎么办调试智能车最难的还不是写代码而是出了问题不知道从哪查。下面写几条我在带竞赛和课程设计时反复看到的典型故障每条都按「现象 → 原因 → 解决」来梳理照着逐步排查能省很多时间。现象 1四个轮子都能转但车一上赛道就往一个方向偏。原因左右电机实际转速不一致。常见根源不是程序而是两个电机的磨损程度或负载阻力不同导致同一 PWM 下转速有差异还有可能是 TB6612 两路供电电压因线径不同产生压降差。解决先用编码器读取两路实际转速如果差异超过 5%在程序里给较慢一侧增加一个固定补偿值比如 200 PWM。如果用了 PID 仍偏检查电机接线端子是否接触不良重新插拔并拧紧螺丝往往比调参数更快解决问题。现象 2直道上车来回摆头像在画龙。原因灰度阈值太敏感或循迹转向量过大车检测到微小的偏差就猛打方向产生震荡。另一个可能原因是主循环周期不稳定采集和决策之间插入大量串口打印导致控制周期跳动。解决把主循环里所有串口打印挪到单独的调试函数中只在需要时开启。转向量分档减小观察摆头幅度是否下降。如果还是摆检查传感器间距是否太窄过小的间距会让检测逻辑对车身姿态极其敏感。现象 3过弯时外侧轮冲出赛道或整车甩尾。原因入弯速度太快或者转向差速不足。很多程序只转了内侧轮的减速没有提高外侧轮的速度导致整车转向力矩不够。解决按前面说的原则增大外侧目标速度而不是一味缩小内侧。同时控制在入弯前提前降速可以做一个简单的横向加速度估计弯道状态出现时把目标速度整体降到直线速度的 60%出弯后恢复。现象 4超声波避障时车突然原地停下然后反复前进后退。原因超声波读到的距离出现跳变比如回波误检或车自身抖动产生反馈程序误判为前方有障碍物。还有可能是避障逻辑挡住了循迹逻辑在无障碍物时让车保持 STOP 状态。解决对超声波数据做连续三次采样取中值或者加一个简单的滞回比较只有连续两次距离小于阈值才触发避障连续两次大于阈值加 10cm 才解除避障避免边界抖动。同时检查 HC-SR04 的供电电压低于 5V 时测距容易乱跳。现象 5电池快没电的时候车明显变慢弯道完全过不去。原因电机转速随电压下降而 PID 的限幅上限是 9000输出电压已经饱和但仍达不到目标速度。解决把目标速度的设定和实时电池电压关联起来做一个简单的电压监测电量下降时整体降低所有目标速度。更彻底的办法是换用带升压功能的稳压模块将电池电压稳定在 7.4V 或 12V 再供给电机驱动。这个坑在比赛后半程最常见拿万用表量一下电压就能确认。6. 调试技巧进阶用串口和虚拟示波器把车调到能跑进 50 秒调车不能靠猜。把运行数据可视化是效率最高的手段也是从「新手调车」到「熟手调车」的分水岭。这里分享一套我常用的调试方法工具要求不高一根 USB 转 TTL 线加一个免费的串口绘图软件就能搞定。先在 STM32 上把一组结构体存起来目标速度、实际速度、左右轮 PWM、当前状态码、超声波距离。然后用串口中断或定时器把这些数据组帧发出。常见的做法是每一帧以帧头加 8 个字节数据加帧尾的方式发送波特率设为 921600这样绘图刷新率能到 100Hz 以上。上位机接收后按协议解析并绘图。绘图观察的重点有两个第一实际速度是否快速跟随目标速度。如果跟随太慢增大 kp如果出现等幅震荡减小 kp 和 ki。第二状态码切换是否连续。如果从 0110 直接跳到 0001说明传感器分布和线宽不匹配或者阈值标定有偏差应该在硬件层面调整传感器间距而不是硬调代码。另一个技巧是录制赛道数据。把一次完整的跑圈数据记录下来然后离线回放逐段查看哪里速度掉得太多、哪里转向过度。这比现场肉眼观察精细得多。具体做法是把串口收到的数据存到 CSV 文件导入到 Python 或 Excel 里画速度曲线和状态码时间线。你很快就会发现90% 的问题都集中在入弯和出弯的两个点上而不是直道。最后我还想提醒一点调整参数前先确认机械结构没有松动。我见过太多人花大半天调 PID结果发现是轮胎顶丝松了。我的习惯是每次调车前先检查一遍螺丝、传感器支架和编码器联轴器顺手把赛道上的灰尘擦干净这个习惯至少帮我省了三分之一的时间。车要是真不听话先从机械找原因再怀疑代码。希望这些经验能帮你少走点弯路。调车是个特别磨人的过程但每一步踩实了程序就稳了。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑