简介:面向嵌入式开发工程师与智能车爱好者,这是一份基于STM32与51单片机的AGV智能小车寻迹避障系统设计资料包,系统讲解从硬件搭建到软件实现的完整流程。资源以STM32C8T6为核心控制板,配套L298N电机驱动、直流电机、TCRT5000红外寻迹模块、HC-SR04超声波测距模块以及蜂鸣器LED声光报警单元,硬件上采用双层亚克力底盘布局;压缩包共1个PDF文档,大小4.29MB,内容紧凑、目录结构分明。目前已有186人浏览学习。软件部分详解PID路径跟踪、超声波避障算法、蓝牙无线控制与状态显示逻辑,并给出硬件连接示意图、可运行源码和51单片机版本的设计源码说明;同时结合AGV小车在智能物流、工业自动化、无人驾驶等场景中的实际应用,介绍红外检测黑色轨迹、超声波避障触发、PID调速等关键原理,对调试中的常见问题与排错思路也有涉及,适合用于课程设计、毕业设计、电子竞赛及工程预研,能为读者提供从硬件接线到代码移植、参数调试的清晰对照。 做AGV智能小车,尤其是寻迹避障这个方向,算是嵌入式入门到进阶之间最经典的一个综合项目了。我当年也是从51单片机点亮LED开始,一路折腾到STM32,最后才把这套系统跑通。这次直接把从硬件选型、原理分析到源码实现、调参避坑的完整过程整理出来,工程目录和核心代码我都会拆开讲,想拿来做课程设计、毕业设计或者自己练手的读者,跟着这条线走,能省不少弯路。
先交代一下这套系统是什么:整车用一块STM32F103C8T6做主控,负责避障决策和串口通信;一块STC89C52单片机负责快速巡检循迹传感器,直接控制电机驱动。两块MCU之间通过UART通信协同工作,实现小车沿黑色引导线行驶,遇到障碍物自动停车绕行后继续寻迹。这个方案特别适合刚入门的群体——51写循迹逻辑足够简单,STM32能跑更复杂的避障状态机,两块芯片分工协作,学习曲线比一开始就上RTOS要平缓得多。
1. 项目整体设计与双MCU分工思路
1.1 为什么选STM32 + 51双主控
很多读者第一个问题肯定是:一颗STM32都能干完的活,为什么要塞两块芯片?这个设计看起来多此一举,但其实是我把两种方案都做了一遍之后才定下来的。
先说51单片机的短板:STC89C52的IO翻转速度和处理能力都有限,跑一个简单的循迹PWM控制逻辑没问题,但一旦要同时处理超声波触发、多个回波定时器捕获,再加串口收发,主循环的时序很容易混乱。而STM32F103C8T6是72MHz主频的Cortex-M3内核,定时器、USART、ADC外设非常丰富,处理这些任务绰绰有余。
但反过来说,循迹控制本身是一个高频实时任务,需要对传感器状态快速响应,如果让一个主控同时干循迹和避障,避障的超声波测距过程(尤其是我最初用阻塞方式写)会打断循迹巡检的节奏,导致小车在过弯时反应迟钝。
所以双MCU架构的核心价值在这几个地方:
- 任务分区明确:51跑高频实时循迹控制环,STM32跑低频避障决策环,两者互不阻塞。
- 学习和调试成本低:51部分逻辑极其直观,适合快速验证传感器和电机驱动;STM32部分则专注在状态机、串口协议和定时器捕获上,知识边界清晰。
- 容错性好:即使STM32因为避障临时占用大量时间,51依然能维持小车基本运动状态,不会直接失控。
- 答辩/汇报有亮点:双MCU协同工作比单芯片方案看起来更有系统设计感,也顺便覆盖了更多嵌入式知识点。
1.2 系统工作流程与信号走向
整个系统的工作流程我从上电开始理一遍,这样后面看代码会更有全局感。
上电后,51单片机先完成IO初始化和PWM定时器配置,然后进入主循环不断读取循迹传感器模块输出的数字电平。默认情况下,STM32同样完成初始化,并通过串口向51发送“前进”控制指令。运行过程中,STM32每隔大约60ms触发一次HC-SR04超声波测距,当检测到正前方障碍物距离小于设定阈值(我一般设为20cm)时,程序会进入避障状态机,通过串口向51发送“左转”或“右转”的指令字。51收到指令后,根据转向指令调整左右轮PWM占空比,直到STM32测到前方距离恢复安全值,再发指令恢复寻迹。
这里面有一个关键设计:循迹传感器本身实时监测黑线偏差,51在每个控制周期都会根据偏差计算差速转向;而STM32的避障指令是作为一个“高优先级覆盖”事件插入到控制环节当中。也就是说,51的默认行为是沿黑线走,但收到避障指令时会临时切换运动模式。这种“默认+覆盖”的架构,比把所有判断逻辑都堆在一个主循环里要干净得多,信号流向也清晰。
2. 硬件选型、传感器原理与电路要点
2.1 核心硬件选型表与理由
选型这部分我直接给出一版稳定跑通的配置,都是市面上比较容易买到、价格合适、资料多的型号。
| 部件 | 选型 | 选型理由 |
|---|---|---|
| 主控(决策端) | STM32F103C8T6 | Cortex-M3内核,72MHz主频,USART/TIM资源充足,LQFP48封装焊接友好 |
| 主控(循迹端) | STC89C52RC | 经典51内核,IO直接驱动逻辑简单,支持串口ISP下载,成本低 |
| 电机驱动 | TB6612FNG | 压降小、体积小、支持PWM调速;L298N更皮实但功耗和发热较大 |
| 循迹传感器 | TCRT5000红外对管 ×4 | 单路模块带电压比较器,输出数字高低电平,灵敏度可调 |
| 避障传感器 | HC-SR04超声波 ×1 | 测距3cm到400cm,时序简单,抗光线干扰能力强 |
| 电池供电 | 两节18650串联(7.4V) | 电机驱动直接取电;逻辑电源通过降压模块单独供给 |
我在初期版本用过L298N,但发现它的压降太大,两节18650供电时电机端电压掉得厉害,而且模块体积大,在小车底盘上不好固定。换成TB6612之后,同样占空比下整车动力明显提升,板载稳压也稳定了很多。如果手头正好只有L298N,也不是不能用,关键是逻辑电源和电机电源要分开。
2.2 循迹模块原理、布局与阈值调节
TCRT5000是我在市面小车上见得最多的一种循迹传感器,核心原理是红外反射式检测。它由一枚红外发射管和一枚红外接收管组成,发射管持续发出红外光,光线照射到地面后反射到接收管。白色地面反射率高,接收管接收的光强较大,输出低电平;黑色引导线吸收红外光,接收管收到的光弱,输出高电平。所以循迹这个动作,本质上就是不断读取这组高低电平的分布,判断黑线相对车身的位置。
传感器布局上我建议至少用4路,最好5路。4路的话中间两路对准黑线,外侧两路负责检测大角度偏差;5路时中间一路可以作为“完全居中”的精确标志。我之前贪快,用的两路方案,小车只要偏一点就直接丢线,后来换成4路才真正稳定。安装高度控制在1.5到2.5cm之间,太低容易受到地面起伏影响,太高红外反射信号衰减严重,黑色和白色的输出电平差会变小。
每个TCRT5000模块上都有一个旋转电位器,用来调节比较器阈值。调的时候有个笨但有效的方法:把模块分别放在黑色引导线和白色地面上,用万用表测输出引脚,旋转电位器直到黑线时输出高电平、白底时输出低电平,并保留足够余量。注意,不要在阳光直射或强光环境下调节,环境红外光干扰会让阈值判断不稳定,实测在室内正常灯光下调节即可。
2.3 超声波测距原理与定时器捕获
避障部分我用的HC-SR04,这是入门测距最常用的一款模块,原理其实非常简单:向Trig引脚发送至少10us的高电平,模块内部自动发出8个40kHz的超声波脉冲,同时Echo引脚变为高电平;当模块收到回波后,Echo引脚恢复低电平。这个高电平持续时间,就是超声波往返的总时间。
距离计算公式很简单:
距离(cm) = 高电平时间(us) / 58
也可以写成:距离 = 时间 × 0.017cm/us。因为声速约340m/s,除以2就是单程距离。
代码上我最初用了最简单的阻塞等待方式:
HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_SET); delay_us(10); HAL_GPIO_WritePin(TRIG_PORT, TRIG_PIN, GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(ECHO_PORT, ECHO_PIN) == GPIO_PIN_SET) { cnt++; delay_us(1); } distance = cnt / 58;实测下来,这种写法在遮挡物比较近、回波正常时没问题,但一旦传感器正对斜面或者被测物体吸音效果太强,Echo引脚一直不跳变,整个while循环就会卡死,导致避障状态机直接“假死”。所以后来我改用了输入捕获的方式:Trig送完脉冲后,用定时器捕获Echo高电平时间,同时设置一个超时保护。工程里已经用HAL库的输入捕获实现,代码会放在stm32f1xx_it.c中断回调里,不占用主循环,稳定性好很多。
3. 软件分层、源码结构与核心代码解析
3.1 51端寻迹算法与差速控制
51端最核心的任务是实时确定黑线偏差,并转换成左右轮的差速控制。我这里的传感器接在P2口低5位,每路传感器输出0表示检测到黑线,输出1表示未检测到黑线。
最基本的逻辑可以用查表法实现:
u8 sensor = P2 & 0x1F; switch (sensor) { case 0x04: // 00100 中间检测到黑线,直行 LeftMotorPWM = SPEED_STRAIGHT; RightMotorPWM = SPEED_STRAIGHT; break; case 0x02: // 00010 黑线偏右 LeftMotorPWM = SPEED_TURN_HIGH; RightMotorPWM = SPEED_TURN_LOW; break; case 0x08: // 01000 黑线偏左 LeftMotorPWM = SPEED_TURN_LOW; RightMotorPWM = SPEED_TURN_HIGH; break; default: break; }这只是一版最基础的实现,实际跑起来会发现问题:如果直接把占空比从一个值跳到另一个值,小车在过弯时会一顿一顿,非常生硬。我后来把“直接赋值”改成了“按步长递进”,让占空比平滑过渡,走线就顺滑了很多。
更进一步的优化是用P值比例控制。假设4路循迹传感器从左到右编号为1到4,可以定义当前偏差error为各个传感器位置的加权和。中间两路权重低,外侧权重高,例如:
int error = -3*sensor1 - 1*sensor2 + 1*sensor3 + 3*sensor4;然后把error乘一个比例系数Kp,直接作为左右轮的差速基准值:
int diff = Kp * error; LeftMotorPWM = BASE_SPEED + diff; RightMotorPWM = BASE_SPEED - diff;这个方法比查表灵活得多,Kp调好以后,小车过弯是平滑过渡而不是阶梯跳变。Kp太大会导致左右PWM频繁摆动,Kp太小则转向不足冲出线。我在四路布局下,误差范围大概是-6到+6,Kp取2到3之间比较合适,具体可以根据车重和轮胎摩擦力微调。
3.2 STM32端避障状态机
STM32端的核心是避障决策逻辑。不能用简单的“探测到障碍就转弯,转完就前进”这种无脑逻辑,否则会出现震荡:小车左转后如果障碍物还在侧前方,马上又认为前方不安全,再次转弯,形成死循环。
我用的方案是有限状态机,把运行过程划分为几个明确状态:
typedef enum { STATE_FORWARD, STATE_TURN_LEFT, STATE_TURN_RIGHT, STATE_STOP } AvoidState; AvoidState state = STATE_FORWARD; while (1) { distance = MeasureDistance(); switch (state) { case STATE_FORWARD: if (distance < 20) { state = STATE_STOP; SendCommand(CMD_STOP); HAL_Delay(200); state = STATE_TURN_LEFT; SendCommand(CMD_TURN_LEFT); } break; case STATE_TURN_LEFT: // 持续检测,前方距离恢复后再前进 if (distance > 35) { SendCommand(CMD_FORWARD); state = STATE_FORWARD; } break; default: break; } HAL_Delay(60); }注意几个细节:转弯前先发一个停车指令,稍等片刻再发转向指令,让车有足够时间响应,不然高速状态下直接转向容易甩尾;转弯状态下,前方距离恢复的阈值要比触发阈值高,我触发是20cm,恢复是35cm,这个差值相当于迟滞,可以避免小车在障碍物边缘反复横跳。
避障方向我是固定左转,如果想让小车更智能,可以在车头左右各装一个超声波模块,哪个方向探测距离远就优先往哪个方向转。这个改进在工程里是预留了接口的,拓展起来不需要改状态机主体。
3.3 双MCU串口通信协议设计
两块单片机之间通信最省事的方式就是UART,波特率我用的9600——这个速率对51来说依然很稳定,STM32也足够。真正需要设计的是帧协议,不能直接裸发一个字节就完事,否则干扰或者掉字节时系统状态容易错乱。
我定义了一个最简协议帧:
| 帧头1 | 帧头2 | 指令字 | 数据 | 累加和 | 结束符 |
|---|---|---|---|---|---|
| 0xAA | 0x55 | CMD | DATA | SUM | 0x0D 0x0A |
累加和SUM为指令字、数据、帧头所有字节之和的低8位。接收端收到0xAA 0x55后进入解析状态,根据长度接收数据,计算校验,只有校验通过才更新控制标志位。如果校验失败,整帧丢弃,不会影响当前运行状态。
指令字定义如下:
| 指令 | 含义 | 数据 |
|---|---|---|
| 0x01 | 前进 | 无 |
| 0x02 | 停车 | 无 |
| 0x03 | 左转 | 转向时间或角度 |
| 0x04 | 右转 | 转向时间或角度 |
51端的串口接收用中断方式,每收到一个字节进入中断,按接收状态机填充缓冲区,这里给一个51端收数据的核心思路:
void UART_ISR() interrupt 4 { u8 dat; if (RI) { RI = 0; dat = SBUF; // 按状态推进:找帧头 -> 收长度/指令/数据 -> 校验 -> 置标志 } }实际调试中,我发现乱码有九成原因是两块板子没有共地。双机之间没有连GND的话,电平参考点不一致,串口电平判断就会错乱。所以接线时除了TX接RX、RX接TX,一定要在STM32的GND和51的GND之间额外接一根线。
3.4 工程文件组织与Keil环境配置注意
整个工程的源码我分成了两套独立的Keil工程,原因是STM32和51的编译器完全不同。Keil5如果想同时编译这两种芯片,需要分别装MDK-ARM和C51两个编译器支持包,每次打开工程时工程文件类型不同,IDE会自动切换。如果出现打开工程后编译按钮灰色、提示选择编译器的现象,多半就是对应支持包没装好。
推荐的目录结构是这样:
Project/ ├── Docs/ │ ├── 接线表.md │ └── 原理图.pdf ├── STM32_F103/ │ ├── Core/Inc/ │ ├── Core/Src/ │ │ ├── main.c │ │ ├── hcsr04.c │ │ ├── avoid.c │ │ └── uart_protocol.c │ └── MDK-ARM/ └── STC_89C52/ ├── Source/ │ ├── main.c │ ├── motor.c │ ├── line_sensor.c │ └── uart_receive.c └── Output/这样组织的好处很明显:51的代码和STM32的代码完全隔离,各自调试互不影响。我在实际开发中就是先把51的循迹单独调通,再把STM32的避障单独用串口助手模拟测距输出,最后才联调通信,问题定位会快很多。
STM32端建议直接用STM32CubeMX生成初始化代码,因为定时器输入捕获、串口DMA这些配置手写寄存器太费时间,而且容易踩坑。生成之后再把业务逻辑加到生成的框架里。
4. 调试实录:常见问题与排查速查
4.1 上电前的检查清单
每次连上新硬件,我不会急着上电下载代码,而是先按固定顺序检查一遍。这些习惯能帮你省下大量排查时间。
- 电压确认:先用万用表测电源模块输出,逻辑部分要稳定在5V,电机供电按电池电压实测记录。不要直接接在降压模块输出端测,要测板子电源引脚处的实际电压。
- BOOT0引脚:STM32的BOOT0要接GND,确保从Flash启动。否则明明下载成功,一上电程序就是不跑。
- 电机接线:确认左右电机的A、B相顺序一致,否则一个轮子前行一个轮子后退,小车直接原地打转。
- 传感器供电:TCRT5000模块供电要接在干净的5V上,不要从单片机IO直接供电。
- 下载通路:ST-LINK的SWDIO、SWCLK、GND三根线先接好再上电,避免热插拔导致连接异常。
4.2 高频问题与排查思路
这个项目的每个环节我都踩过不少坑,挑几个出现频率最高的问题列成速查表,遇到类似现象可以直接对照。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| ST-LINK下载报错“No STM32 Target Found” | BOOT0接错、SWD接线松动、供电不足 | 检查BOOT0是否接GND;重新插拔SWD线;先用USB供电下载 |
| 电机不转或只有一个转 | 驱动板使能脚EN没接高、PWM引脚错位 | 把EN接VCC;对照原理图逐脚测量PWM输出 |
| 小车走不直,总是跑偏 | 左右轮PWM基准不一致、减速比有差异 | 调低速度逐轮测试,软件里给慢轮加补偿 |
| 循迹在小转弯处冲出线 | 速度过快、传感器数量不足、Kp太小 | 降速试验;把PWM响应改为步进式;增大Kp |
| 超声波测距偶尔返回0 | 阻塞等待超时、测量周期太短 | 改用定时器输入捕获;加超时保护;测量间隔大于50ms |
| 双机串口接收乱码 | 波特率不匹配、没共地、电源噪声 | 统一波特率;两块板子接GND;电机和逻辑分开供电 |
| 电池电压一低就复位 | 电机启动瞬间拉低系统电压 | 电源输入端并接470uF以上电解电容;电机和逻辑地分开布线 |
4.3 调试顺序与参数微调心得
我强烈建议按“先单机、再联机、先低速、再高速”的顺序来调,不要一上来就全功能放开,否则哪里出错都分不清。
第一步是软硬件自检:单独给51下载一个电机正反转测试程序,确认左右电机方向正确;给STM32下载一个超声波测距程序,用串口助手观察距离读数是否和实际值吻合。这两个测试通过了,才说明底层硬件是可信的。
第二步是调循迹。让小车在直线黑线上低速走,观察传感器输出的跳变是否稳定,调整电位器让输出余量足够。然后调Kp值,从较小的值开始逐步增加,找到一个过弯不抖、直线不摆的平衡点。这个阶段我建议把速度控制在最大占空比的30%到40%,超过这个范围,过弯时惯性太大,算法再好也救不回来。
第三步才接入避障。STM32先单独工作,通过串口发指令控制51动作,观察避障行为是否正确。避障逻辑稳定后再把两个系统合起来,进行全流程测试。
最后分享一个经验:电机电源和逻辑电源一定要分开供电。一开始图省事共用一个电源,结果电机一转,STM32的ADC波形就严重跳动,超声波偶尔还会误触发。后来我在电机电源引脚并联大电容,同时让逻辑电源走独立的降压模块,问题立刻消失。嵌入式系统里,电源的干净程度决定了系统稳定性的下限。
这套系统跑通之后,可玩性还很高。比如加上蓝牙模块,就可以通过手机App手动遥控,观察串口上报的状态数据;换上OpenMV摄像头,就能把寻迹从“反射式红外”升级成“视觉巡线”;或者把无障碍路径规划做成固定轨迹记忆回放。这次整理的完整工程资料和源码已经在对应开源仓库中打包好,关于代码实现方式、传感器选型、工程环境配置,所有疑问都可以在评论区直接提,我尽量及时回复。
本文还有配套的精品资源,点击获取