基于STM32的智能循迹避障小车设计实践
在高校电子信息类课程中,总有一些项目能真正让学生把“纸上谈兵”变成“真枪实弹”——智能小车就是其中之一。它不像单纯的LED闪烁实验那样简单,也不像无人机飞控那样遥不可及,而是一个刚刚好的“中间态”:既有硬件搭建的乐趣,又有算法调试的挑战,还能看到自己的代码让机器动起来。
我们最近完成的这款基于STM32F103C8T6的智能小车,集成了红外循迹与超声波避障功能。从原理图绘制到代码烧录,从传感器校准到路径决策逻辑优化,整个过程几乎涵盖了嵌入式开发的所有核心环节。更重要的是,它的成本控制在200元以内,完全适合教学批量部署。
为什么选STM32F103C8T6?
说到主控芯片,很多人第一反应是51或Arduino,但当我们需要处理多任务、高实时性控制时,这些8位单片机就显得力不从心了。STM32F103C8T6虽然只是“蓝丸”开发板上的那颗不起眼的小芯片,但它基于ARM Cortex-M3内核,主频高达72MHz,自带64KB Flash和20KB RAM,还配备了丰富的定时器、ADC和通信接口资源。
实际使用中最让人安心的是它的外设成熟度。比如我们要用PWM驱动电机,直接调用TIM3输出即可;要读取多个红外传感器状态?PA0~PA3一组GPIO输入搞定;测距需要精确计时?TIM2配合输入捕获模式轻松实现微秒级精度。更别说Keil、STM32CubeIDE、PlatformIO等工具链的支持,让开发效率大幅提升。
下面这段初始化LED的代码,看似简单,却是所有调试工作的起点:
void LED_Init(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
}
PC13接了一个共阴极LED,程序运行时通过
GPIO_ResetBits(GPIOC, GPIO_Pin_13)
点亮作为运行指示。别小看这个灯,它能在系统卡死时帮你判断是否进入了死循环,或者中断有没有正常触发。
红外循迹:低成本路径识别的可靠方案
市面上做循迹小车最常用的还是红外传感器阵列。我们用了四路数字型红外模块(如TCRT5000),并排安装在车体底部,间距约1.8cm,刚好略小于黑线宽度(通常为2cm)。这种布局既能保证在直线上有足够覆盖范围,又能在转弯时提供明确的方向偏差信号。
工作原理其实很直观:红外发射管照向地面,黑色胶带吸光强、反射弱,白色地板则相反。接收端根据反射强度输出高低电平——压在线上为低,偏离为高(具体逻辑取决于比较器电路设计)。
关键在于如何快速准确地读取这四个传感器的状态,并做出转向决策。我们采用了一种紧凑的数据打包方式:
uint16_t Read_Track_Sensors(void) {
uint16_t sensor_val = 0;
sensor_val |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) << 0);
sensor_val |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) << 1);
sensor_val |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) << 2);
sensor_val |= (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) << 3);
return sensor_val;
}
返回值是一个4位二进制数,代表当前各传感器的状态。例如:
-
0b0110
→ 中间两个压线,说明处于直线段;
-
0b0011
→ 右侧两个压线,说明路径向右偏,应左转纠正;
-
0b1100
→ 左侧两个压线,需右转;
-
0b0000
→ 所有传感器都在白区,可能已脱轨,进入搜索模式。
这里有个工程经验:不要一检测到状态变化就立即转向。现实中地面反光不均、车身震动都会导致误判。我们在软件中加入了简单的延时滤波机制——连续两次采样结果一致才执行动作,有效减少了“蛇形走位”。
超声波避障:不只是测个距离那么简单
HC-SR04几乎是每个初学者都会接触的超声波模块,便宜、易用、资料多。但真正在动态环境中稳定工作,远没有想象中那么简单。
其基本流程是:给Trig引脚一个≥10μs的高脉冲,模块自动发出40kHz超声波;遇到障碍物后回波被接收,Echo引脚拉高,持续时间即为往返时间。距离计算公式为:
$$
\text{Distance (cm)} = \frac{\text{Echo高电平时间 (μs)} \times 0.034}{2}
$$
我们最初用轮询方式实现测距:
float Get_Distance(void) {
float distance = 0;
GPIO_SetBits(GPIOB, GPIO_Pin_1); // Trig
Delay_us(10);
GPIO_ResetBits(GPIOB, GPIO_Pin_1);
while (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)); // 等待上升沿
TIM_Cmd(TIM2, ENABLE);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0)); // 等待下降沿
TIM_Cmd(TIM2, DISABLE);
uint32_t time_us = TIM_GetCounter(TIM2);
distance = (float)(time_us * 0.034 / 2);
TIM_SetCounter(TIM2, 0);
return distance;
}
这种方式简单直接,但在主循环中频繁调用会阻塞其他任务。后来我们改用定时器输入捕获+中断的方式,大大提升了系统的响应能力和测量精度。
不过更大的问题是 误检 。比如斜面墙壁会导致回波偏移,无法返回;软质材料(如布料沙发)吸音严重,测不出有效距离;甚至有时地面反射也会造成干扰。为此我们做了几点改进:
- 设置最小有效距离阈值(如15cm) ,低于该值才认为是真实障碍;
- 多次测量取平均 ,剔除异常值;
- 增加探测频率 ,每100ms测一次,避免因单次误差导致急停;
- 结合运动状态判断 ,若小车静止时距离突变,优先怀疑传感器异常而非环境变化。
电机驱动:L298N真的够用吗?
L298N作为一款经典双H桥驱动芯片,虽然封装老旧、发热较大,但在教学场景下依然表现出色。它支持最高35V供电,每通道持续电流2A,足以带动常见的TT马达减速电机。
连接方式也很清晰:
- IN1/IN2 控制左电机正反转
- IN3/IN4 控制右电机
- ENA/ENB 接PWM信号实现调速
我们使用TIM3_CH1(PB6)输出PWM来调节速度:
void PWM_Motor_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 99; // 100步周期
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz → 1MHz
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 50; // 初始占空比50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
TIM_Cmd(TIM3, ENABLE);
}
通过修改
TIM_Pulse
值即可动态调整占空比,实现无级调速。但我们发现一个问题:电机启动瞬间电流冲击大,容易打滑。于是加入了渐变加速策略——每次增加5%占空比,间隔50ms,直到达到目标速度。同理,停车时也逐步降速,显著提升了行驶平稳性。
电源方面,采用两节18650串联提供7.4V,经LM2596降压至5V供给STM32和传感器。特别注意的是,L298N的逻辑供电必须独立稳定,否则可能导致控制信号紊乱。
系统整合:从模块拼接到闭环控制
当各个模块单独测试都正常后,真正的挑战才开始:如何让它们协同工作?
我们的主控逻辑采用状态机结构,默认进入“循迹优先”模式。每50ms执行一次传感器采集:
while (1) {
uint16_t track_state = Read_Track_Sensors();
float dist = Get_Distance();
if (dist < 20.0f) {
// 进入避障模式
Stop();
Backward(800); // 后退800ms
TurnLeft(500); // 尝试左转
delay_ms(100);
} else {
// 继续循迹
Track_Control(track_state);
}
}
Track_Control()
函数根据传感器组合决定动作:
-
0110
:直行
-
0011
:左转(右轮快)
-
1100
:右转(左轮快)
-
0000
:原地旋转查找路径
为了防止在十字路口或断线处无限打转,我们加入了超时机制:若连续10次未能恢复轨迹,则自动切换至随机探索模式,尝试重新定位路线。
调试过程中最有效的手段是串口输出。我们将传感器原始数据、当前状态、电机指令等信息通过USART发送到PC端,用串口助手实时监控,极大加快了问题排查速度。比如有一次发现小车总是往右偏,查看日志才发现右侧红外传感器灵敏度偏低,重新调节电位器后恢复正常。
遇到的问题与实战经验
任何项目都不可能一帆风顺,以下是几个典型问题及解决方案:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 循迹时左右晃动严重 | 传感器响应延迟 + 控制过于激进 | 加入状态滤波,限制转向角度 |
| 超声波偶尔返回0或超大量程 | 回波未接收到或中断丢失 | 增加重试机制,三次无效则取上次有效值 |
| 电池电压下降后传感器误判 | 供电波动影响比较器阈值 | 使用独立稳压模块,确保传感器电压稳定 |
| 电机启停抖动大 | PWM跳变更剧烈 | 实现软启动/停止,逐步增减占空比 |
还有一些细节值得注意:
- 机械结构上,红外传感器尽量靠近前轮轴线,减少转向滞后;
- 超声波模块居中安装,避免车身遮挡形成盲区;
- 所有引脚连线做好标记,避免插错烧芯片;
- 关键变量添加注释,方便后期维护。
教学之外的价值:不止是一次课程设计
这套系统看似简单,却完整体现了嵌入式系统的三大核心环节: 感知—决策—执行 。学生不仅能掌握GPIO、ADC、PWM、定时器等底层操作,更能理解如何将多个模块整合成一个协同工作的整体。
更重要的是,它具备极强的可扩展性:
- 换上蓝牙模块(HC-05),就能实现手机遥控;
- 加装编码器,结合PID算法,实现恒速巡航;
- 替换为OpenMV摄像头,迈入图像识别领域;
- 多台小车联网,探索群体协作行为。
甚至可以作为毕业设计的基础平台,进一步引入惯性导航、SLAM建图、ROS通信等功能。
这种高度集成且成本可控的设计思路,正体现了现代嵌入式教育的一个趋势:不再追求“炫技”,而是强调“可用、可靠、可演进”。让学生从动手实践中建立系统思维,才是这类项目真正的价值所在。
如今,这辆小车已经跑过了实验室的每一条预设轨道,也经历过无数次撞墙、脱轨、重启。但它每一次平稳地沿着黑线前进,或是灵巧地绕开前方障碍,都在无声诉说着一段关于代码、电路与控制逻辑的故事——而这,正是嵌入式世界的魅力所在。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
1万+

被折叠的 条评论
为什么被折叠?



