1. 项目概述:当STM32F103遇上智能导航
去年在指导大学生电子设计竞赛时,我带着学生用STM32F103C8T6开发了一款融合红外循迹和超声波避障的智能小车。这个看似简单的项目,实际上涉及了嵌入式系统开发的完整流程:从硬件选型、电路设计到软件编程,再到最后的调试优化。今天我就把这个项目的完整实现过程分享给大家,特别是那些刚接触STM32的新手朋友们。
这个小车的核心功能很简单:既能沿着地面画好的黑线自动行驶(循迹),又能在遇到障碍物时自动避开(避障)。但实现起来却有不少门道:
- 硬件层面:需要处理好不同模块的供电电压(3.3V、5V、12V),确保信号传输稳定
- 软件层面:要设计合理的控制逻辑,处理好传感器数据采集与电机控制的时序关系
- 调试层面:需要通过实际测试不断优化参数,比如PWM占空比、转向时间等
下面这张表是小车的主要功能模块构成:
| 功能模块 | 实现方式 | 关键参数 |
|---|---|---|
| 主控制器 | STM32F103C8T6最小系统板 | 72MHz主频,64KB Flash |
| 循迹检测 | 四路红外传感器 | 检测距离1-3cm |
| 避障检测 | HC-SR04超声波模块 | 测距范围2-400cm |
| 电机驱动 | L298N驱动板 | 最大驱动电流2A |
| 动力系统 | 直流减速电机 | 转速200RPM,12V供电 |
提示:选择STM32F103C8T6是因为它性价比高,外设丰富(有多个定时器可用于PWM生成),而且社区资源丰富,非常适合初学者。
2. 硬件设计与连接:电压匹配是关键
2.1 硬件选型清单
在开始接线前,我们需要确保所有硬件组件都准备到位。下面是我经过多次实践验证后确定的最佳硬件配置:
| 硬件名称 | 数量 | 关键参数 | 选购建议 |
|---|---|---|---|
| STM32F103C8T6最小系统板 | 1块 | 核心板需带SWD下载接口 | 建议选择带USB转串口的版本 |
| 四路红外循迹模块 | 1个 | 检测高度可调 | 选择带灵敏度电位器的型号 |
| HC-SR04超声波模块 | 1个 | 工作电压5V | 新版RCWL-1601精度更高 |
| L298N电机驱动板 | 1块 | 最大输出电流2A | 建议加装散热片 |
| 直流减速电机 | 2个 | 减速比1:48,12V供电 | 搭配橡胶轮增加摩擦力 |
| 智能小车底盘 | 1套 | 带万向轮和电机固定孔位 | 亚克力材质更轻便 |
| 12V锂电池 | 1块 | 容量2000mAh以上 | 建议选用带保护板的18650电池组 |
| 5V稳压模块 | 1块 | 输出电流≥1A | AMS1117-5.0性价比高 |
| 杜邦线 | 若干 | 建议准备20cm和10cm两种长度 | 公对公、公对母都要准备 |
2.2 电路连接详解
硬件连接是项目成功的基础,必须特别注意电压匹配问题。STM32F103的工作电压是3.3V,而红外传感器、超声波模块和L298N的逻辑端需要5V供电。以下是详细的连接方案:
2.2.1 电源系统连接
- 锂电池正极接L298N的12V输入端子
- 锂电池正极同时接5V稳压模块的输入端
- 5V稳压模块输出端:
- 接红外循迹模块的VCC
- 接超声波模块的VCC
- 接L298N的5V供电端子(如果模块有独立5V输入)
- 所有模块的GND端子必须共地:
- 锂电池负极接L298N的GND
- L298N的GND接STM32的GND
- STM32的GND接所有传感器的GND
注意:千万不要把12V直接接到STM32上!我曾有学生因此烧毁了好几块开发板。一定要确保电压匹配后再通电。
2.2.2 信号线连接
根据STM32F103C8T6的引脚分布,我设计了如下连接方案:
| 模块 | 信号线 | STM32引脚 | 配置模式 | 备注 |
|---|---|---|---|---|
| 红外循迹模块 | OUT1 | PA0 | GPIO输入(上拉) | 左外侧传感器 |
| OUT2 | PA1 | GPIO输入(上拉) | 左内侧传感器 | |
| OUT3 | PA2 | GPIO输入(上拉) | 右内侧传感器 | |
| OUT4 | PA3 | GPIO输入(上拉) | 右外侧传感器 | |
| HC-SR04 | Trig | PB0 | GPIO输出(推挽) | 触发测距信号 |
| Echo | PB1 | GPIO输入(浮空) | 回波检测 | |
| L298N | IN1 | PB5 | GPIO输出(推挽) | 左电机正转控制 |
| IN2 | PB6 | GPIO输出(推挽) | 左电机反转控制 | |
| IN3 | PB7 | GPIO输出(推挽) | 右电机正转控制 | |
| IN4 | PB8 | GPIO输出(推挽) | 右电机反转控制 | |
| ENA | PA8(TIM1_CH1) | PWM输出 | 左电机速度控制 | |
| ENB | PA9(TIM1_CH2) | PWM输出 | 右电机速度控制 |
这样设计引脚分配有以下几个考虑:
- 将相关功能集中分配(如红外传感器用PA0-PA3)
- PWM输出使用高级定时器TIM1,可产生更稳定的PWM波
- 保留USART1(PA9/PA10)用于后续扩展调试信息输出
3. 软件开发环境搭建
3.1 工具链安装
工欲善其事,必先利其器。我们需要搭建完整的STM32开发环境:
-
Keil MDK-ARM安装:
- 下载Keil5安装包(建议V5.38及以上版本)
- 安装时勾选"STM32F1xx_DFP"设备支持包
- 注册MDK-ARM(社区版有32KB代码限制)
-
STM32CubeMX配置:
bash复制# 在Linux下可通过以下命令安装(Windows用户直接下载exe) wget https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-configurators-and-code-generators/stm32cubemx.html tar -xzf en.stm32cubemx-lin-v6-9-0.zip cd stm32cubemx ./SetupSTM32CubeMX-6.9.0.linux -
ST-Link驱动安装:
- Windows用户下载ST-Link官方驱动
- Linux用户通常内核已集成驱动,只需确保权限:
bash复制sudo usermod -a -G plugdev $USER sudo echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666"' > /etc/udev/rules.d/99-stlink.rules
3.2 工程创建与配置
使用STM32CubeMX创建工程时,有几个关键配置点需要注意:
-
时钟树配置:
- 启用外部高速时钟(HSE)
- PLL倍频设为x9
- 系统时钟配置为72MHz
- APB1分频设为/2(36MHz,不超频)
-
GPIO配置:
- 红外传感器引脚设为上拉输入(GPIO_Input_PullUp)
- 超声波Trig设为推挽输出(GPIO_Output_PP)
- 电机控制IN1-IN4设为推挽输出
- PWM输出引脚设为复用推挽(GPIO_Mode_AF_PP)
-
定时器配置:
c复制// TIM1 PWM配置参数 htim1.Instance = TIM1; htim1.Init.Prescaler = 71; // 72MHz/(71+1)=1MHz htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 999; // PWM频率=1MHz/1000=1kHz htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; -
生成代码:
- 选择Toolchain为MDK-ARM V5
- 勾选"Generate peripheral initialization as a pair of '.c/.h' files"
- 建议工程目录不要有中文和空格
4. 核心代码实现
4.1 硬件抽象层实现
在开始写业务逻辑前,我们需要先实现几个底层硬件驱动函数:
4.1.1 精确延时函数
c复制// 微秒级延时(基于CPU指令周期)
void Delay_us(uint32_t us)
{
uint32_t ticks = SystemCoreClock / 1000000 * us / 5;
while(ticks--) {
__NOP();
}
}
// 毫秒级延时(基于HAL库)
void Delay_ms(uint32_t ms)
{
HAL_Delay(ms);
}
实测发现:STM32F103在72MHz下,每个__NOP()大约需要5个时钟周期。这个延时函数用于超声波测距时,误差可以控制在±1us以内。
4.1.2 电机控制函数
c复制void Motor_Control(uint8_t state, uint16_t left_speed, uint16_t right_speed)
{
// 设置PWM占空比
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, left_speed);
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_2, right_speed);
switch(state) {
case MOTOR_FORWARD:
HAL_GPIO_WritePin(L298N_PORT, L298N_IN1_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(L298N_PORT, L298N_IN2_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(L298N_PORT, L298N_IN3_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(L298N_PORT, L298N_IN4_PIN, GPIO_PIN_RESET);
break;
// 其他状态类似...
}
}
4.2 传感器驱动实现
4.2.1 超声波测距
c复制float Ultrasonic_Measure(void)
{
uint32_t start_time = 0, end_time = 0;
float distance = 0;
// 发送10us的Trig脉冲
HAL_GPIO_WritePin(ULTR_PORT, ULTR_TRIG_PIN, GPIO_PIN_SET);
Delay_us(10);
HAL_GPIO_WritePin(ULTR_PORT, ULTR_TRIG_PIN, GPIO_PIN_RESET);
// 等待Echo变高
while(HAL_GPIO_ReadPin(ULTR_PORT, ULTR_ECHO_PIN) == GPIO_PIN_RESET);
// 记录高电平开始时间
start_time = DWT->CYCCNT;
// 等待Echo变低
while(HAL_GPIO_ReadPin(ULTR_PORT, ULTR_ECHO_PIN) == GPIO_PIN_SET);
// 计算高电平持续时间(us)
end_time = DWT->CYCCNT;
uint32_t time_diff = (end_time - start_time) / (SystemCoreClock / 1000000);
// 计算距离(cm) = 时间(us) * 340m/s / 2 / 10000
distance = time_diff * 0.017f;
return distance;
}
技巧:使用DWT(Data Watchpoint and Trace)计数器可以获得更高精度的时间测量。需要在初始化时启用:
c复制CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
4.2.2 红外循迹检测
c复制uint8_t IR_Track_Detect(void)
{
uint8_t left1 = HAL_GPIO_ReadPin(IR_PORT, IR_LEFT1_PIN);
uint8_t left2 = HAL_GPIO_ReadPin(IR_PORT, IR_LEFT2_PIN);
uint8_t right1 = HAL_GPIO_ReadPin(IR_PORT, IR_RIGHT1_PIN);
uint8_t right2 = HAL_GPIO_ReadPin(IR_PORT, IR_RIGHT2_PIN);
// 检测到黑线时引脚为低电平
if(!left1 && !left2) return TRACK_LEFT_DEVIATION;
if(!right1 && !right2) return TRACK_RIGHT_DEVIATION;
if(!left2 && !right1) return TRACK_CENTER;
// 其他状态判断...
}
4.3 主控制逻辑实现
将避障和循迹功能融合的核心算法:
c复制void Car_Navigation(void)
{
static uint8_t avoid_state = 0;
float distance = Ultrasonic_Measure();
// 避障优先级最高
if(distance > 0 && distance < OBSTACLE_DISTANCE) {
switch(avoid_state) {
case 0: // 停止
Motor_Control(MOTOR_STOP, 0, 0);
avoid_state = 1;
break;
case 1: // 后退
Motor_Control(MOTOR_BACKWARD, 600, 600);
if(++avoid_state > 10) avoid_state = 2;
break;
// 其他避障状态...
}
}
else {
avoid_state = 0;
// 执行循迹逻辑
uint8_t track_state = IR_Track_Detect();
switch(track_state) {
case TRACK_CENTER:
Motor_Control(MOTOR_FORWARD, 700, 700);
break;
// 其他循迹状态...
}
}
}
5. 系统调试与优化
5.1 分模块调试技巧
-
红外传感器调试:
- 将传感器对准不同颜色的表面,用逻辑分析仪观察输出信号
- 调整传感器高度和灵敏度电位器,确保在白纸上输出高电平,在黑线上输出低电平
- 典型问题:环境光干扰大时,可在传感器上方加遮光罩
-
超声波模块调试:
c复制// 测试代码 while(1) { float dist = Ultrasonic_Measure(); printf("Distance: %.1f cm\r\n", dist); Delay_ms(200); }- 测量不同距离的物体,检查返回值是否准确
- 常见问题:Echo信号受到干扰,可尝试在Trig和Echo线上加100Ω电阻
-
电机调试:
- 单独测试每个电机的正反转
- 测量空载和负载时的电流,确保不超过L298N的额定值
- 发现电机抖动时,可尝试增加PWM频率(1kHz→5kHz)
5.2 PID算法优化循迹
基础版本的循迹控制比较简单,采用"转速差"方法。更高级的做法是使用PID控制:
c复制// PID参数
float Kp = 0.5, Ki = 0.01, Kd = 0.1;
float error = 0, last_error = 0, integral = 0;
void PID_Tracking(void)
{
uint8_t track_state = IR_Track_Detect();
// 计算误差
switch(track_state) {
case TRACK_LEFT_DEVIATION: error = -2; break;
case TRACK_LEFT_SLIGHT: error = -1; break;
case TRACK_CENTER: error = 0; break;
// 其他状态...
}
// PID计算
integral += error;
float derivative = error - last_error;
float output = Kp*error + Ki*integral + Kd*derivative;
last_error = error;
// 应用控制
Motor_Control(MOTOR_FORWARD,
700 - output, // 左轮速度
700 + output); // 右轮速度
}
调试PID参数的技巧:
- 先调Kp,使小车能快速响应轨迹变化但不过冲
- 再调Kd,抑制振荡
- 最后调Ki,消除静态误差(但循迹应用通常Ki可以设很小)
6. 项目扩展与进阶
基础功能实现后,可以考虑以下扩展方向:
-
无线遥控功能:
- 添加HC-05蓝牙模块,实现手机APP控制
- 增加手动/自动模式切换
-
多传感器融合:
c复制// 使用多个超声波传感器 #define FRONT_DIST Ultrasonic_Measure(TRIG1_PIN, ECHO1_PIN) #define LEFT_DIST Ultrasonic_Measure(TRIG2_PIN, ECHO2_PIN) #define RIGHT_DIST Ultrasonic_Measure(TRIG3_PIN, ECHO3_PIN) void Advanced_Avoidance(void) { if(FRONT_DIST < 20) { if(LEFT_DIST > RIGHT_DIST) { // 左侧空间更大,向左转 } else { // 向右转 } } } -
状态显示与调试:
- 添加0.96寸OLED显示屏,实时显示传感器数据
- 通过串口输出调试信息,方便参数调整
-
机器学习应用:
- 收集传感器数据和小车动作,训练简单的决策模型
- 实现更智能的避障策略
7. 常见问题解决方案
在项目开发过程中,我总结了以下常见问题及解决方法:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 小车循迹时频繁抖动 | 转向响应过快 | 降低P增益或增加主循环延时 |
| 超声波测距值跳动大 | 环境回声干扰 | 软件滤波(取多次测量中间值) |
| 电机启动时L298N重启 | 电源电流不足 | 增加电源电容或使用更大容量电池 |
| 红外传感器误检测 | 环境光干扰 | 调整灵敏度电位器或加遮光罩 |
| STM32偶尔死机 | 电源不稳定 | 在3.3V和GND间加100uF电容 |
| 循迹时小车冲出轨道 | 传感器间距不合适 | 调整传感器间距为轨道宽度的70%左右 |
最后分享一个调试心得:当小车行为异常时,建议按照"电源→信号→逻辑"的顺序排查:
- 先用万用表检查各点电压是否正常
- 用逻辑分析仪或示波器查看关键信号波形
- 最后再检查程序逻辑是否正确
这个项目虽然基础,但涵盖了嵌入式开发的完整流程。通过不断调试优化,我们最终实现了稳定可靠的小车导航系统。希望这篇文章能帮助大家少走弯路,快速掌握STM32在智能小车中的应用。