简介:面向智能车竞赛四轮组的完整工程资源,聚焦基础巡线、环岛识别、十字路口识别三大核心任务,适合备赛学生与嵌入式算法爱好者从零搭建或优化自有方案。压缩包共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 分配表,再动焊枪。这不是形式主义,是为了后面写程序时不用反复查原理图,也方便排查虚焊或者接反。
| 功能模块 | 信号线 | 主控引脚 | 说明 |
|---|---|---|---|
| 电机驱动 TB6612 | PWMA / AIN1 / AIN2 | PA8 / PB12 / PB13 | 左电机 |
| 电机驱动 TB6612 | PWMB / BIN1 / BIN2 | PA9 / PB14 / PB15 | 右电机 |
| 灰度传感器左 1 | OUT1 | PA0 (ADC) | 模拟量输入 |
| 灰度传感器左 2 | OUT2 | PA1 (ADC) | 模拟量输入 |
| 灰度传感器右 1 | OUT3 | PA2 (ADC) | 模拟量输入 |
| 灰度传感器右 2 | OUT4 | PA3 (ADC) | 模拟量输入 |
| 超声波 HC-SR04 | Trig / Echo | PB0 / PB1 | 触发与回波输入 |
| 蓝牙/串口调试 | TX / RX | PA9 / 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,弯道内侧 6000+1200=7200,外侧 6000+400=6400,差量在 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,结果发现是轮胎顶丝松了。我的习惯是每次调车前先检查一遍螺丝、传感器支架和编码器联轴器,顺手把赛道上的灰尘擦干净,这个习惯至少帮我省了三分之一的时间。车要是真不听话,先从机械找原因,再怀疑代码。
希望这些经验能帮你少走点弯路。调车是个特别磨人的过程,但每一步踩实了,程序就稳了。
本文还有配套的精品资源,点击获取