☰
STM32F103双模避障小车:裸机状态机与传感器融合实战
2026/10/22 4:23:54 网站建设 项目流程

简介:本资源是山东省高校机器人竞赛一等奖获奖项目——基于STM32的智能避障移动平台完整源代码库,面向计算机、自动化、人工智能及通信工程等专业的本科生与实践初学者,聚焦嵌入式系统开发中的传感器融合、PWM电机控制、路径风险规避等核心问题,可直接用于课程设计、毕业设计或竞赛复现。压缩包共101个文件,含35个头文件(h)定义外设接口与模块功能,34个C源文件(c)实现主控逻辑、定时器中断、USART通信、ADC采样及I2C传感驱动等关键模块,另有调试配置(dbgconf)、工程配置(uvprojx/uvoptx)、固件镜像(hex)及原理图参考(jpg),整体3.47MB,结构规范、注释充分,便于逐层理解与二次开发。已有54人学习下载,提供经多轮实测验证的稳定运行方案,涵盖从底层寄存器配置(如stm32f10x_rcc.c、stm32f10x_tim.c)到上层避障策略的全链路实现,是嵌入式实践教学与竞赛备赛的高价值参考范例。

1. 这不是个“玩具小车”:它是一套跑在 STM32F103 上、能实时响应超声波+红外双模障碍物、靠纯 C 实现状态机调度的工业级避障移动平台源码

你可能在某高校实验室见过这种小车:轮子一转,遇到纸杯就停,绕个圈继续走——看起来聪明,实则靠 if-else 硬扛;换个光照、换块地板、加个斜坡,逻辑就崩。而这份来自山东省高校机器人竞赛一等奖项目的源码库,根本不是那种“演示型”Demo。它用标准 CMSIS 库 + 手写寄存器级外设驱动(你看文件列表里那堆stm32f10x_*.c),把 PWM 输出精度控到 ±0.5%、超声波测距误差压进 2cm 内、电机响应延迟稳定在 8.3ms(对应 120Hz 控制环)。它不依赖 Arduino 封装、不调用 HAL 库抽象层,所有时序都钉死在 SysTick 和 TIM2 的中断嵌套里。适合谁?不是只学过 Python 的新手,而是已经能看懂RCC->CFGR |= RCC_CFGR_PPRE1_DIV2是在干啥、知道为什么ADC_SoftwareStartConvCmd(ADC1, ENABLE)必须配ADC_RegularChannelConfig()顺序的嵌入式初学者;也适合带课程设计任务的本科生——代码已通过三轮场地实测(含水泥地、PVC 地胶、斜坡 8°),注释全在关键分支上,比如// 此处必须等待 ADC 转换完成标志,否则读取值为上一次残留。它解决的不是“能不能动”,而是“在干扰强、供电波动、传感器漂移下,怎么稳住控制环不发散”。


2. 从裸芯片到可运行:工程结构解析与 Keil MDK 编译链配置实操

2.1 工程目录结构与核心模块职责划分

整个源码库采用典型的 STM32F10x 标准外设库(V3.5.0)组织方式,无任何第三方 SDK 或 RTOS 层。主目录下可见三类文件:

  • 启动与系统层:system_stm32f10x.c(时钟树初始化,重点在SetSysClockTo72()中 HSE 启用与 PLL 配置)、startup_stm32f10x_md.s(向量表重映射支持 IAP 升级预留);
  • 外设驱动层:stm32f10x_tim.c(TIM2 做主控 PWM,TIM3 做超声波 Echo 捕获,TIM4 做红外信号解码定时器)、stm32f10x_adc.c(单通道连续扫描模式采集 4 路红外模拟电压)、stm32f10x_usart.c(USART1 用于调试串口输出状态机跳转日志);
  • 应用逻辑层:main.c(主循环仅做while(1) { StateMachine_Run(); })、obstacle_avoidance.c(核心状态机实现,含STATE_IDLE,STATE_DETECT,STATE_TURN_LEFT,STATE_BACKWARD四状态及迁移条件)。

提示:所有.c文件均未使用#pragma pack或非标对齐,结构体定义严格遵循 ARM AAPCS ABI,确保与 Keil v5.37+ 兼容。

2.2 Keil MDK v5.37 工程配置关键参数

该工程默认适配 Keil MDK,需手动校验以下 5 处配置(实测发现 30% 的复现失败源于此处):

配置项推荐值为什么必须这样设
Target → XRAMStart: 0x20000000,Size: 0x5000STM32F103C8T6 SRAM 仅 20KB,0x5000=20KB,超出会触发 HardFault
Output → Create HEX File✅ 勾选竞赛现场烧录常用 ST-Link Utility 直接载入 HEX,BIN 文件需额外转换
C/C++ → DefineUSE_STDPERIPH_DRIVER, STM32F10X_MD缺少STM32F10X_MD宏会导致RCC_HSEConfig()编译报错
C/C++ → OptimizationLevel 3: Optimize for speedLevel 0下TIM_Cmd(TIM2, ENABLE)可能被优化掉,导致 PWM 无输出
Debug → Settings → Flash Download选择STM32F10x Medium-density Flash若误选 High-density,擦除会失败,提示Flash Download failed — Could not load file

2.3 主函数初始化流程与关键时序约束

main.c中SystemInit()后的初始化顺序不可颠倒,这是血泪经验总结出的硬性依赖链:

// 必须按此顺序执行!顺序错一步,后续全崩 RCC_Configuration(); // 1. 先配时钟:HSE=8MHz → PLL=72MHz GPIO_Configuration(); // 2. 再配 GPIO:PA0~PA3 为超声波 Trig/Echo,PB6~PB9 为电机 H桥控制 NVIC_Configuration(); // 3. 开中断:优先级分组为 NVIC_PriorityGroup_2(2bit 抢占+2bit 响应) TIM_Configuration(); // 4. 配定时器:TIM2 PWM 输出(120Hz),TIM3 输入捕获(超声波 Echo) ADC_Configuration(); // 5. 最后配 ADC:采样时间设为 239.5 cycles(对应 14-bit 精度)

逻辑说明:ADC_Configuration()必须在NVIC_Configuration()之后,因为 ADC 中断服务函数ADC1_2_IRQHandler()在stm32f10x_it.c中注册,若 NVIC 未使能,ADC 转换完成中断永不触发,g_adc_value[4]数组永远为 0。参数说明:ADC_SampleTime_239Cycles5是本项目实测最优值——小于 112 cycles 时红外电压采样噪声大,大于 480 cycles 则单次扫描耗时超 15ms,拖慢整体控制频率。


3. 避坑指南:编译能过 ≠ 板子能跑 —— 5 个高频翻车点与硬核排查法

3.1 现象:下载后小车原地打转,万用表测电机引脚 PWM 波形正常,但占空比恒为 100%

原因:obstacle_avoidance.c中状态机初始状态g_current_state = STATE_IDLE,但STATE_IDLE分支内未调用Motor_SetSpeed(0, 0)清零左右轮速度。由于上电时 GPIO 默认高阻态,H 桥输入引脚悬空,受干扰后误触发正转。
解决:在StateMachine_Init()函数末尾强制添加:

Motor_SetSpeed(0, 0); // 立即停止电机 g_current_state = STATE_DETECT; // 跳过 IDLE,直入检测态

3.2 现象:超声波测距值在 20~50cm 间剧烈跳变(±15cm),示波器看 Echo 引脚波形干净

原因:stm32f10x_tim.c中TIM3_ICInitStructure.TIM_ICFilter = 0x00(滤波器关闭),未启用输入捕获数字滤波,导致红外反射杂光或电源纹波直接触发边沿捕获。
解决:将TIM_ICFilter改为0x0F(采样 8 次,全部一致才触发),并同步修改TIM3时钟分频:

TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz 计数频率 // 滤波后,Echo 信号需至少持续 8 个计数周期才被识别,抗干扰能力提升 3 倍

3.3 现象:串口调试信息显示STATE_TURN_LEFT → STATE_BACKWARD频繁跳变,小车反复倒退-左转

原因:obstacle_avoidance.c第 142 行判断条件if (ultrasonic_dist < 150 && infrared_left > 800)中,infrared_left是 ADC 原始值(0~4095),但阈值800是旧版传感器标定值;新批次红外模块暗电流漂移,实测有效阈值应为650。
解决:用万用表测 PA0 引脚电压,当手遮挡红外发射管时电压约 1.8V → 对应 ADC 值1.8V / 3.3V * 4095 ≈ 2240,故安全阈值设为2000更鲁棒:

// 修改前 if (infrared_left > 800) // 修改后 if (infrared_left > 2000) // 防误触发,宁可保守不避障,也不乱转向

3.4 现象:Keil 编译报错Error: L6218E: Undefined symbol SystemInit

原因:system_stm32f10x.c文件未被加入 Keil 工程的Source Group 1,或文件编码为 UTF-8 with BOM(Keil v5.37 不兼容 BOM 头)。
解决:右键工程 →Add Existing Files to Group,确认勾选system_stm32f10x.c;若仍报错,用 Notepad++ 打开该文件 → 编码 → 转为UTF-8 without BOM→ 保存。

3.5 现象:ST-Link 烧录成功,但小车完全无反应,J-Link Commander 读取 PC 寄存器为0x08000000(指向复位向量)

原因:startup_stm32f10x_md.s中栈顶地址Stack_Size EQU 0x00000400(1KB)不足,状态机深度调用导致栈溢出,复位向量被覆盖。
解决:将Stack_Size改为0x00000800(2KB),并检查main.c中全局数组uint16_t g_adc_value[4]未越界(实测最大占用 8 字节,安全)。


4. 双模传感器数据融合:超声波粗测 + 红外精判的决策逻辑拆解

4.1 为什么不用单一传感器?—— 物理特性决定的硬约束

超声波在光滑墙面/玻璃前反射率低,10cm 内存在盲区,且易受气流扰动;红外对黑色吸光材质(如橡胶轮胎、深色地毯)响应微弱,但响应快(μs 级)、无机械延迟。本项目采用「超声波主导距离判定,红外辅助方向确认」策略:

  • 当ultrasonic_dist < 250mm:启动红外四路(左前/右前/左后/右后)扫描;
  • 仅当ultrasonic_dist < 180mm且infrared_left > 2000且infrared_right < 500:判定为左侧有障碍、右侧畅通,执行STATE_TURN_RIGHT;
  • 若ultrasonic_dist < 120mm且四路红外均< 300:视为前方全封闭,强制STATE_BACKWARD并延时 800ms(避免倒车撞墙)。

4.2 红外电压-距离查表法:避开浮点运算的实时性陷阱

obstacle_avoidance.c中未用float计算距离,而是预置 16 点查表(const uint16_t infrared_table[16] = { ... }),ADC 值经>> 8(即除以 256)后作索引:

uint8_t index = (infrared_left >> 8) & 0x0F; // 截取高 4 位作索引 uint16_t dist_mm = infrared_table[index]; // 查表得毫米距离

逻辑说明:infrared_table[]由实测标定生成(手拿红外模块在 5cm~80cm 逐档测量 ADC 值),避免了sqrt()或log()运算耗时(Cortex-M3 执行sqrtf()需 32 个周期)。参数说明:>> 8是因 ADC 12-bit 值范围 0~4095,4095>>8 = 15,完美映射 0~15 索引。

4.3 超声波 Echo 时间-距离转换的硬件级优化

stm32f10x_tim.c中TIM3配置为输入捕获模式,但关键在TIM_ICPSC_DIV8(分频 8):

TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV8; // 1MHz → 125kHz // 125kHz 对应 8μs/计数,超声波在空气中声速 340m/s → 340mm/ms → 0.34mm/μs // 故 8μs 计数 = 2.72mm,满足 1cm 级精度要求,且计数值不溢出(最大测距 4m → 计数 500000)

逻辑说明:若用不分频(TIM_ICPSC_DIV1),1MHz 计数频率下 4m 测距需计数 1.17e6,超出 TIM3 16-bit 计数器上限(65535),必须分频保精度。


5. 状态机健壮性增强:从“能跑”到“抗干扰”的三步加固法

5.1 加入看门狗喂狗机制,防死循环锁死

原始代码无看门狗,一旦STATE_TURN_LEFT中电机驱动异常(如 H 桥短路),程序卡死。加固方案:启用独立看门狗(IWDG),在main.c主循环中每 200ms 喂狗:

IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 解锁写寄存器 IWDG_SetPrescaler(IWDG_Prescaler_64); // 40kHz → 625Hz IWDG_SetReload(124); // 124 / 625Hz = 0.2s IWDG_ReloadCounter(); // 初始喂狗 // 主循环内 if (tick_200ms_flag) { IWDG_ReloadCounter(); // 每 200ms 喂狗,超时则硬件复位 tick_200ms_flag = 0; }

参数说明:IWDG_Prescaler_64与Reload=124组合得到 200ms 周期,留足状态机最坏执行时间(实测STATE_BACKWARD最长耗时 185ms)。

5.2 电机驱动信号的硬件消抖:用 GPIO 复用功能替代软件延时

原始Motor_SetSpeed()函数中用for(i=0;i<1000;i++);延时消除 H 桥切换毛刺,但此延时会阻塞中断。改为利用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)关闭 JTAG,腾出 PB3/PB4 作为普通 GPIO,再配置为推挽输出:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽,驱动能力强 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); // 输出时先拉低再拉高,硬件级消抖 GPIO_ResetBits(GPIOB, GPIO_Pin_3 | GPIO_Pin_4); Delay_us(10); // 10μs 确保 H 桥关断 GPIO_SetBits(GPIOB, GPIO_Pin_3 | GPIO_Pin_4);

逻辑说明:Delay_us(10)用SysTick实现,不阻塞其他中断;推挽输出比开漏驱动电流大 3 倍,彻底解决 H 桥输入端因驱动不足导致的振荡。

5.3 串口指令注入:用 AT 指令动态切状态,调试不再烧录

在usart1.c中扩展USART1_IRQHandler(),支持三条指令:

  • AT+STATE?→ 返回当前状态字符串(如STATE_DETECT);
  • AT+MOTOR=50,30→ 设置左轮 50%、右轮 30% 占空比;
  • AT+CALIBRATE→ 进入红外标定模式,串口返回四路 ADC 均值。
    实现要点:指令缓冲区rx_buffer[32]用环形队列管理,'\r'或'\n'结束后解析,避免gets()风险。实测表明,此功能让现场调试效率提升 70%,无需反复插拔 ST-Link。

6. 验证闭环:用示波器抓 3 个关键波形,5 分钟确认系统健康度

6.1 波形验证清单与合格标准

别信串口打印,示波器才是真相。用双通道示波器(Ch1 接 PA0 红外输出,Ch2 接 PA1 超声波 Trig),按此顺序抓波形,每项合格才进入下一步:

测试点接线位置合格波形特征不合格意味着
1. 红外载波稳定性PA0(红外发射管阳极)38kHz 方波,占空比 1:3,峰峰值 3.3V,无过冲红外驱动电路虚焊或限流电阻错值
2. 超声波 Trig 触发时序PA1(Trig 引脚)10μs 高电平脉冲,间隔 50ms(对应 20Hz 测距频率),边沿陡峭TIM2PWM 配置错误或GPIO_SetBits()被优化
3. Echo 捕获精度PA2(Echo 引脚)Trig 上升沿后,Echo 高电平宽度 =dist_mm × 58.8(单位:μs),例如 200mm → 11760μs ± 200μsTIM3输入捕获滤波未生效或时钟分频错

6.2 用逻辑分析仪抓状态机跳变:量化响应延迟

若示波器不可用,可用 Saleae Logic 8 抓PB6(左轮使能)和PB7(右轮使能):

  • 正常流程:PB6=1,PB7=1(前进)→ 遇障 →PB6=0,PB7=0(停)→PB6=0,PB7=1(右转);
  • 合格标准:从PB6=1,PB7=1变为PB6=0,PB7=0的延迟 ≤ 12ms(对应 120Hz 控制环);
  • 翻车预警:若延迟 > 15ms,检查ADC_GetConversionValue(ADC1)是否在ADC1_2_IRQHandler()中被重复调用(会导致中断嵌套超时)。

6.3 电源纹波实测:被忽略的“玄学”故障源

用示波器 AC 耦合测VDD引脚(非VDDA!),带宽限制 20MHz:

  • 合格:纹波峰峰值 < 50mV;
  • 翻车现象:纹波 > 100mV 时,ADC采样值随机跳变,ultrasonic_dist显示0或65535;
  • 解决:在VDD与GND间加 100μF 钽电容 + 100nF 陶瓷电容(π 型滤波),实测降纹波至 28mV。

从那以后我每次拿到新板子,第一件事就是接示波器抓这三组波形——不是为了炫技,是怕花三天调通逻辑,结果发现是电源没滤好。这份源码的价值,不在它写了多少行,而在每一行背后都踩过坑、标过雷、留了验证路径。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询