简介:这是一套基于STM32芯片的循迹避障小车完整资料源码,主要面向正在准备毕业设计、课程设计或期末大作业的计算机、电子类专业学生,也适合希望动手实践嵌入式开发的学习者。资源以高分毕业设计为背景,评审达到99分,代码完整、可直接运行,能够帮助读者快速理解循迹与避障功能的实现思路;小车包含循迹传感器与避障检测逻辑,源码中模块划分明确,便于定位与修改。压缩包共包含322个文件,主要有C语言源码、H头文件、Keil工程配置文件、原理图与PCB设计文档、PDF说明以及3D模型等,覆盖从硬件设计到软件调试的完整链路,总大小约64.22MB。目前已有164人学习下载。资料内目录结构清晰,文件类型覆盖工程源码、硬件图纸、编译输出与说明文档,既可作为项目参考,也可直接用于二次开发,对想完整跑通一套STM32小车项目的初学者尤为友好。
1. 循迹避障小车最难的不是算法,是两套控制逻辑的切换
课设答辩时最容易出现的场面:小车在白色跑道上能沿黑线走,一遇障碍就原地打转或直接撞上去。追问代码才发现,while(1) 里把每个传感器读了一遍,再把电平硬塞给电机,根本分不清当前是在循迹还是避障。基于 STM32 的循迹避障小车资料源码,分值差距几乎都卡在这:循迹与避障两套逻辑没有放进同一个状态框架。其实底盘构成很固定:两轮差速转向、三至五路灰度感知、一个超声波避障通道,再加一个能切换逻辑的小状态机,把“什么时候循迹、什么时候避障”的优先级先定死,后面才拿得到高分。下面直接拆到可以复制进工程的程度。
2. STM32循迹避障小车的差速模型与传感器选型
2.1 两轮差速转向与转向力度换算
先不碰代码,把小车底层的运动关系讲清楚。最常见结构是左轮、右轮各由一个直流电机驱动,转向靠两个轮子的速度差完成。设左轮线速度 vL、右轮线速度 vR、轮距 L,小车整体前进速度 v=(vL+vR)/2,旋转角速度 ω=(vR-vL)/L,转向半径 R=v/ω。展开后得到:
R = L × (vL + vR) / (2 × (vR - vL))
这个公式的意义不在精确计算半径,而在于让你理解:左右轮速差越大,转弯半径越小;差速为零,走直线。实际代码里 vL 和 vR 直接用 PWM 占空比替换,占空比差就是转向“力度”。我一般把占空比差分成三档,直线全速 70%,轻微纠偏只差 10%,直角弯差 40%,程序里用宏定义,方便答辩时当场调节。但注意转向力度不是越大越好,差速过大会造成小车原地甩头,灰度传感器扫过黑线后反而丢线。
2.2 循迹传感器:数字量输出还是模拟量ADC
循迹部分的感知方案,网上资料里最常见的是 TCRT5000 或带比较器的灰度模块。这类模块有两种输出形式,直接决定算法复杂度。
| 输出类型 | 信号含义 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数字量 | 黑线/白底二值电平 | 程序简单,抗干扰 | 无法感知部分遮挡 | 三路快速循迹 |
| 模拟量 | 反射电压随灰度变化 | 可加权定位线中心 | 需要ADC和校准 | PID平滑循迹 |
| 摄像头/K210串口 | 输出线坐标或赛道类别 | 扩展性强 | 成本和开发量高 | 加分演示项目 |
重点说前两种。数字量模块内部比较器把反射电压和电位器阈值比较,输出直接接 STM32 的 GPIO;模拟量模块则把反射电压引到 ADC 引脚。选型时有一个容易被忽略的点:模拟量模块的反射电压范围一般在 0~3.3V,但不同颜色赛道、不同安装高度的静态电压差异很大,程序必须留一个校准常量。若源码里直接用固定阈值判断黑白,换到深绿色地毯或反光很强的白纸就会误判,这是第一处需要适配的地方。
2.3 超声波避障的距离测定与换算
避障部分最常见是 HC-SR04,原理不复杂:给 Trig 脚一个 10us 高电平脉冲,模块发出 8 个 40kHz 声波;障碍物反射后 Echo 脚输出高电平,高电平持续时间等于声波往返时间。声速取 340m/s,即 0.0344cm/us,往返距离要除以 2,所以距离 cm = 高电平时间(us) / 58.2。
| 高电平时间 us | 距离 cm | 说明 |
|---|---|---|
| 116 | 2 | 接近盲区 |
| 580 | 10 | 触发避障的常用阈值 |
| 1450 | 25 | 开始预减速的距离 |
| 5800 | 100 | 模块量程上限附近 |
这个换算没必要在中断里做浮点除法,保存 us 数,需要判断时与阈值整数比较即可。真正要注意的是测量一次大约要 5~20ms,如果每轮主循环都阻塞等待 Echo,循迹 PWM 的更新频率会被拖低,小车转弯会变“肉”。第三章的源码组织会专门处理这件事。
3. STM32源码工程结构:外设初始化与状态机组织
3.1 先配好时钟、PWM 和 GPIO,再把源码装进 CubeMX 骨架
拿到一份循迹避障小车源码,第一步不是看 main.c 里的 while(1),而是打开工程先确认时钟树和外设映射。常见组合是 STM32CubeMX 生成 HAL 库工程,配合 Keil5 编译;芯片包安装有问题时先在 CubeMX 的 Help 菜单核对固件包版本。
我一般建议的引脚划分是:TIM2_CH1 输出左电机 PWM、TIM2_CH2 输出右电机 PWM、TIM3_CH1 输入捕获接超声波 Echo,PB4 接 Trig。PWM 频率设成 10kHz,这个值在直流电机上声音小、调速线性度好;过低会听到明显啸叫,过高则驱动板开关损耗大。以下是一段电机接口头文件示意:
/* motor.h */ #ifndef __MOTOR_H #define __MOTOR_H #include "main.h" #define MOTOR_PWM_FREQ 10000 /* 10kHz,避免啸叫 */ #define MOTOR_DEAD_ZONE 50 /* 占空比死区,低于此值电机不转 */ #define PWM_MAX_PERCENT 100 #define PWM_STRAIGHT_PERCENT 70 /* 直线速度,答辩现场可调 */ #define PWM_CORRECT_DELTA 10 /* 循迹轻微纠偏差速 */ #define PWM_TURN_DELTA 40 /* 急转差速 */ void Motor_Init(void); void Motor_SetLeftDuty(uint8_t percent); void Motor_SetRightDuty(uint8_t percent); void Motor_Stop(void); #endif这段头文件看起来只定义了几个宏,其实是整个源码调参的入口。PWM 频率、死区、直线速度和转向力度全部集中在这里,比散落在 main.c 里到处改数字好讲。死区是直流电机特性造成的,占空比太低时电机克服不了静摩擦力,会出现 PWM 输出占空比 30% 还没动的现象,所以要单独处理:低于阈值直接输出 0,高于阈值再从 0~100 线性映射到 ARR 比较值。
3.2 用小状态机管理“循迹 / 避障 / 停车”三件事
循迹避障合体小车经常出问题的根源,是两套行为同时抢电机。我的做法是把运行状态拆成四个:ST_IDLE(待命)、ST_LINE(循迹)、ST_AVOID(避障)、ST_HOLD(遇障等待)。状态迁移条件只有三个:开始命令、超声波距离小于阈值、避障完成。用枚举和 switch 实现,源码结构非常清楚。
/* main.c 状态机骨架 */ typedef enum { ST_IDLE = 0, ST_LINE, ST_AVOID, ST_HOLD } run_state_t; volatile run_state_t g_state = ST_IDLE; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); /* 电机PWM */ MX_TIM3_Init(); /* 超声波回波计时 */ MX_USART1_Init(); /* 调试串口 */ Motor_Init(); Ultrasonic_Init(); while (1) { switch (g_state) { case ST_IDLE: Motor_Stop(); if (uart_cmd == 'z') g_state = ST_LINE; /* 串口收到启动命令 */ break; case ST_LINE: LineFollow_Tick(); if (ultrasonic_cm <= 15) g_state = ST_AVOID; break; case ST_AVOID: Avoid_Tick(); if (avoid_finish) g_state = ST_LINE; break; case ST_HOLD: Motor_Stop(); break; } } }这段代码的价值不在于功能完整,而在于把优先级写成了结构。超声波距离小于 15cm 时,不管循迹当前状态如何,都会切到避障;避障完成后回到循迹。串口收到的 ‘z’ 命令只触发启动,不会在运行中打扰路径。注意 g_state 被中断和主循环共享,正式工程要加 volatile;若还出现状态错乱,再补一个简单的临界区保护即可。
3.3 电机驱动抽象层:隔离硬件差异
源码里电机驱动部分差别很大,有 L298N、TB6612、DRV8833,还有集成 MOS 的驱动板。接口却可以统一:两个方向引脚加一个 PWM 引脚。Motor_SetLeftDuty 内部只控制占空比,方向在 Motor_Init 里按驱动板硬件接法决定。换驱动板时只改初始化函数,主逻辑不用动,这是源码里保留一个抽象层的原因。
| 驱动板 | 电源要求 | 方向控制 | 备注 |
|---|---|---|---|
| L298N | 12V 输入,5V 输出可给 STM32 | IN1/IN2 | 压降大,发热快 |
| TB6612 | 2.7~5.5V 逻辑,电机电压独立 | AIN1/AIN2 | 效率高,常用 |
| DRV8833 | 2.7~10.8V | xIN1/xIN2 | 适合小车 |
还要注意电机电源和 STM32 电源的共地问题。驱动板上的 GND 必须和单片机 GND 接在一起,否则 PWM 控制信号没有参考地,电机要么不转要么乱转。这个老生常谈的问题在课设现场出现频率极高,后面第六章还会再提一次。
4. 循迹算法从开环比较到加权 PID 转向
4.1 三路灰度传感器的基准校准
循迹算法的输入是灰度传感器的反射电压。先写一个校准流程放在上电阶段:小车放在黑线上,串口打印左、中、右三路 ADC 原始值;再放到白底上,各打印一次。把两组数据平均后得到每条线的黑/白中间值,作为判断阈值。
#define TRACK_ADC_BLACK_L 2800 /* 左传感器黑线ADC值 */ #define TRACK_ADC_WHITE_L 900 /* 左传感器白底ADC值 */ int32_t Track_Threshold(int32_t black, int32_t white) { return (black + white) / 2; } uint8_t Track_IsBlack(uint8_t ch) { uint32_t adc = Track_ReadADC(ch); int32_t th = Track_Threshold(g_track_black[ch], g_track_white[ch]); return adc > th ? 1 : 0; }阈值不取固定值而是每次开机重新标定,能吸收传感器老化、电池电压下降和环境光线变化。电池电压从 8.4V 掉到 7.2V 时,反射电压会整体变化,固定阈值很容易误判。Track_ReadADC 用阻塞式单次转换即可,三个通道轮流读,一次循环增加几十微秒,对 10ms 级控制周期没有压力。
4.2 三路数字量快速转向的简单实现
如果只拿到三路数字量输出,最简单可靠的算法是把三个电平编码成 3 位掩码,再用 switch 查表转向。
uint8_t mask = (left << 2) | (middle << 1) | right; switch (mask) { case 0x04: /* 只有左线 */ Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT - PWM_CORRECT_DELTA); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT); break; case 0x02: /* 中间 */ Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT); break; case 0x01: /* 只有右线 */ Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT - PWM_CORRECT_DELTA); break; case 0x06: /* 左偏 */ Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT - PWM_TURN_DELTA * 2 / 3); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT); break; case 0x03: /* 右偏 */ Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT - PWM_TURN_DELTA * 2 / 3); break; default: /* 丢线 */ Motor_Stop(); break; }注意 0x04 表示左传感器压线、中右两个没压,车应该往左转,所以左侧减速、右侧保持,车头向左偏。推荐用这种“减速转弯”而不是“加速外侧轮”,因为加速会让车身甩尾,灰度传感器容易冲出赛道。default 里的丢线处理,答辩演示时可以改成“保持上次转向继续走一小段”,避免在弯道全丢时急停,但基础版先停车更安全。
4.3 模拟量加权循迹与 PID 调试参数
为了提高平滑度,我一般把中间三路模拟量转成线位置。把每个传感器的“压线量”当作权重,求得加权位置 pos,范围在 -100~100 之间表示线在左边还是右边。压线量用白底静态值减去当前 ADC 值,差值越大说明压在黑线上的面积越多:
pos = (leftValue * (-100) + midValue * 0 + rightValue * 100) / (leftValue + midValue + rightValue)
得到 pos 后,转向增量由 PID 算出来:
pid_out = Kp * pos + Ki * pos_integral + Kd * (pos - pos_last); pid_out = clamp(pid_out, -PWM_TURN_DELTA, PWM_TURN_DELTA); Motor_SetLeftDuty(PWM_STRAIGHT_PERCENT + pid_out); Motor_SetRightDuty(PWM_STRAIGHT_PERCENT - pid_out);pos 为正表示线偏右,pid_out 为正时左轮加速、右轮减速,车头向右修正。先给一组能起跑的初始参数:Kp=1.2、Ki=0.02、Kd=0.6,控制周期 10ms。若小车左右抖动,先把 Kd 加大;若转弯太慢冲出赛道,先加 Kp;Ki 只用来消除静态偏差,黑线连续偏移时不要调大,否则会震荡。
| 现象 | 调整项 | 调法 |
|---|---|---|
| 左右摆动 | Kd | 每次 +0.2 |
| 弯道冲出 | Kp | 每次 +0.3 |
| 长期贴线一侧 | Ki | 每次 +0.01 |
5. 避障逻辑实现:测距、决策与让行的优先级
5.1 HC-SR04 测距时序与定时器实现
避障测距不推荐用阻塞式 delay 读 Echo 高电平,因为主循环还要处理循迹。常见方案是用 TIM3 输入捕获:Trig 发 10us 脉冲后,Echo 高电平到来时捕获计数,Echo 结束后再捕获一次,两次计数差乘以定时器分辨率就是时间。定时器配成 1MHz 时,计数值直接等于微秒数。下面是一个简化版串行测量代码,适合低速小车:
void Ultrasonic_SendTrig(void) { HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); } uint32_t Ultrasonic_ReadEchoUs(void) { uint32_t timeout = 30000; /* 等待Echo变高 */ while (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_RESET) if (--timeout == 0) return 0; __HAL_TIM_SET_COUNTER(&htim3, 0); timeout = 30000; while (HAL_GPIO_ReadPin(ECHO_GPIO_Port, ECHO_Pin) == GPIO_PIN_SET) if (__HAL_TIM_GET_COUNTER(&htim3) > timeout) return 0; return __HAL_TIM_GET_COUNTER(&htim3); /* 单位us */ }这段代码用轮询加超时保护实现,输入捕获中断版只是把等待逻辑搬进回调。t2 就是高电平持续微秒数。要留意一个细节:读 Echo 电平前先把对应引脚确认为输入模式,部分资料源码会把 Echo 误配成输出,导致电平读不到。delay_us 由 SysTick 或 DWT 实现。每次触发到测量完成约 10ms,控制周期按 50ms 设计不会让避障反应迟钝。
5.2 遇障后的状态决策
避障不能只做“见障就转”,要考虑小车当前在哪条赛道上。比如左转避障后,小车可能已经偏离黑线,直接回循迹状态会丢线。通常会加一个固定时长的避障动作序列:发现障碍后先减速、左转固定毫秒数、直行、右转回正,再用循迹姿态找线。这个动作序列用子状态表示比用时间硬编码更好用。
typedef enum { AVOID_START = 0, AVOID_TURN_LEFT, AVOID_FORWARD, AVOID_TURN_RIGHT, AVOID_DONE } avoid_substate_t; void Avoid_Tick(void) { static avoid_substate_t sub = AVOID_START; switch (sub) { case AVOID_START: Motor_SetLeftDuty(40); Motor_SetRightDuty(40); /* 减速 */ sub = AVOID_TURN_LEFT; break; case AVOID_TURN_LEFT: Motor_SetLeftDuty(20); Motor_SetRightDuty(70); /* 左转 */ if (--avoid_left_ticks <= 0) sub = AVOID_FORWARD; break; case AVOID_FORWARD: Motor_SetLeftDuty(60); Motor_SetRightDuty(60); if (--avoid_fwd_ticks <= 0) sub = AVOID_TURN_RIGHT; break; case AVOID_TURN_RIGHT: Motor_SetLeftDuty(70); Motor_SetRightDuty(20); /* 右转找线 */ if (--avoid_right_ticks <= 0) sub = AVOID_DONE; break; case AVOID_DONE: g_state = ST_LINE; /* 回到循迹 */ sub = AVOID_START; break; } }这里的 left、forward、right 对应的 tick 数不能凭空定。我一般先用串口打印串级日志,测出小车转 90 度需要的毫秒数,再除以 50ms 控制周期得到 ticks。源码里的数值要允许在编译期修改,不要在函数内部写魔法数字;avoid_left_ticks 这些变量在进入 AVOID_START 时统一赋值。
5.3 与循迹模块的耦合
避障过程中灰度传感器仍在工作,如果避障子状态机每帧都调用 LineFollow_Tick,就会把避障转向指令覆盖掉。常见做法是:在 ST_AVOID 状态下不再执行循迹 PID,只保留一个 Line_IsLost 检测函数扫描传感器有没有重新压线。一旦检测到中或侧方压线,说明车头已经回到赛道附近,立刻退出避障序列。这个耦合逻辑比固定避障时间更鲁棒,但需要在场地实测。
注意:避障优先级高于循迹,但不高于手动急停。调试串口收到停止命令时,应无条件返回 ST_IDLE,避免小车在验证过程中冲下桌面。
6. 调参与验证:把循迹避障小车源码变成能答辩的作品
6.1 用串口把内部状态“看”出来
高分项目和能跑项目的分界线在可观测性。给源码加一个简单的调试串口输出,每个控制周期打印一次状态编码、三路灰度 ADC、超声波距离和左右占空比。用格式化字符串发送,串口助手或 STM32CubeMonitor 都能解析。注意日志帧率不要超过 20Hz,否则串口中断占用太大会干扰 PWM 更新。看到一帧数据后,就能判断小车丢线到底是阈值错误还是速度过快。
6.2 影响稳定性的物理细节
这里列几个我在调这类小车时最先检查的点,每个都能让“源码没变但现场翻车”。
- 电池电压变化:满电 8.4V 和低电 7.2V 下,电机转速差异明显,灰度阈值也要重标定。
- PWM 频率:低于 5kHz 时电机啸叫,高于 20kHz 部分驱动板发热,10kHz 是保守值。
- 传感器安装高度:TCRT5000 反射距离建议在 5~15mm,太高会把黑线识别成白底。
- 共地问题:驱动板 GND 和 STM32 GND 断开时,电机抖动且 SWD 可能报 no target found。
- 车头朝向:超声波探头要装在车头最前方,否则侧面碰撞先到。
6.3 让答辩组看到扩展性的几个加分点
想要把这类题目做成高分项目,代码稳定只能拿基础分。可以加一两个低风险扩展。最简单的是慢入慢出:启动时把占空比从 0 逐步升到目标值,停车时反向渐变,既减少打滑也显得专业。其次是事件日志:把每次避障的位置和时间戳存进一个环形缓冲区,结束后通过串口回放,能直接回答“刚才为什么在这里转弯”。还有同学在车头加 K210 视觉模块,通过串口向 STM32 发送线坐标或色块,主控这边解析一帧 8 字节 UART 报文即可,这属于视觉循迹的典型扩展路径,讲清楚数据帧格式就是很好的答辩素材。这些改动不需要重写架构,只在现有状态机上加一个扩展节点。先保证循迹算法丢线后能在 1 秒内回中,再谈视觉和日志,顺序不能反。
本文还有配套的精品资源,点击获取