简介这是一套基于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(vLvR)/2旋转角速度 ω(vR-vL)/L转向半径 Rv/ω。展开后得到R L × (vL vR) / (2 × (vR - vL))这个公式的意义不在精确计算半径而在于让你理解左右轮速差越大转弯半径越小差速为零走直线。实际代码里 vL 和 vR 直接用 PWM 占空比替换占空比差就是转向“力度”。我一般把占空比差分成三档直线全速 70%轻微纠偏只差 10%直角弯差 40%程序里用宏定义方便答辩时当场调节。但注意转向力度不是越大越好差速过大会造成小车原地甩头灰度传感器扫过黑线后反而丢线。2.2 循迹传感器数字量输出还是模拟量ADC循迹部分的感知方案网上资料里最常见的是 TCRT5000 或带比较器的灰度模块。这类模块有两种输出形式直接决定算法复杂度。输出类型信号含义优点缺点适用场景数字量黑线/白底二值电平程序简单抗干扰无法感知部分遮挡三路快速循迹模拟量反射电压随灰度变化可加权定位线中心需要ADC和校准PID平滑循迹摄像头/K210串口输出线坐标或赛道类别扩展性强成本和开发量高加分演示项目重点说前两种。数字量模块内部比较器把反射电压和电位器阈值比较输出直接接 STM32 的 GPIO模拟量模块则把反射电压引到 ADC 引脚。选型时有一个容易被忽略的点模拟量模块的反射电压范围一般在 03.3V但不同颜色赛道、不同安装高度的静态电压差异很大程序必须留一个校准常量。若源码里直接用固定阈值判断黑白换到深绿色地毯或反光很强的白纸就会误判这是第一处需要适配的地方。2.3 超声波避障的距离测定与换算避障部分最常见是 HC-SR04原理不复杂给 Trig 脚一个 10us 高电平脉冲模块发出 8 个 40kHz 声波障碍物反射后 Echo 脚输出高电平高电平持续时间等于声波往返时间。声速取 340m/s即 0.0344cm/us往返距离要除以 2所以距离 cm 高电平时间(us) / 58.2。高电平时间 us距离 cm说明1162接近盲区58010触发避障的常用阈值145025开始预减速的距离5800100模块量程上限附近这个换算没必要在中断里做浮点除法保存 us 数需要判断时与阈值整数比较即可。真正要注意的是测量一次大约要 520ms如果每轮主循环都阻塞等待 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 输入捕获接超声波 EchoPB4 接 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高于阈值再从 0100 线性映射到 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 里按驱动板硬件接法决定。换驱动板时只改初始化函数主逻辑不用动这是源码里保留一个抽象层的原因。驱动板电源要求方向控制备注L298N12V 输入5V 输出可给 STM32IN1/IN2压降大发热快TB66122.75.5V 逻辑电机电压独立AIN1/AIN2效率高常用DRV88332.710.8VxIN1/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范围在 -100100 之间表示线在左边还是右边。压线量用白底静态值减去当前 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 为正时左轮加速、右轮减速车头向右修正。先给一组能起跑的初始参数Kp1.2、Ki0.02、Kd0.6控制周期 10ms。若小车左右抖动先把 Kd 加大若转弯太慢冲出赛道先加 KpKi 只用来消除静态偏差黑线连续偏移时不要调大否则会震荡。现象调整项调法左右摆动Kd每次 0.2弯道冲出Kp每次 0.3长期贴线一侧Ki每次 0.015. 避障逻辑实现测距、决策与让行的优先级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 反射距离建议在 515mm太高会把黑线识别成白底。共地问题驱动板 GND 和 STM32 GND 断开时电机抖动且 SWD 可能报 no target found。车头朝向超声波探头要装在车头最前方否则侧面碰撞先到。6.3 让答辩组看到扩展性的几个加分点想要把这类题目做成高分项目代码稳定只能拿基础分。可以加一两个低风险扩展。最简单的是慢入慢出启动时把占空比从 0 逐步升到目标值停车时反向渐变既减少打滑也显得专业。其次是事件日志把每次避障的位置和时间戳存进一个环形缓冲区结束后通过串口回放能直接回答“刚才为什么在这里转弯”。还有同学在车头加 K210 视觉模块通过串口向 STM32 发送线坐标或色块主控这边解析一帧 8 字节 UART 报文即可这属于视觉循迹的典型扩展路径讲清楚数据帧格式就是很好的答辩素材。这些改动不需要重写架构只在现有状态机上加一个扩展节点。先保证循迹算法丢线后能在 1 秒内回中再谈视觉和日志顺序不能反。本文还有配套的精品资源点击获取