
简介基于STM32F103系统板与常见硬件模块搭建的避障小车完整项目包面向嵌入式初学者、课程设计/毕业设计学生及竞赛项目开发者。项目通过STM32库函数编写C语言程序实现电机驱动、传感器检测、避障策略等完整逻辑最终可实地运行源码结构清晰经测试可直接编译导入也可按需扩展更多功能。ZIP压缩包约92MB包含完整源码、Keil工程文件及说明文档文件清单暂无明细但压缩包目录按功能模块组织便于定位结合说明能快速完成硬件接线与参数调试。目前已有62人学习使用适合项目开发、课程设计、期末大作业、工程实训、大创及学科竞赛等场景。若遇到编译或模块调试问题可通过作者渠道获得及时解答。1. 项目概述与核心需求拆解1.1 一次典型的STM32避障小车项目到底是做什么的STM32避障小车不是什么新鲜项目了但每年依然有大量嵌入式初学者、电子竞赛选手和毕业设计学生选择它。原因很简单——它把嵌入式开发中最核心的几个点全部串联起来了GPIO控制、定时器PWM输出、外部中断或轮询检测、状态机逻辑设计以及最容易被忽视的实地调试能力。我也经历过这个阶段。当时拿到一块STM32F103C8T6最小系统板两个直流减速电机加一个L298N驱动模块一个HC-SR04超声波模块几个红外避障传感器一块18650电池组就这么一堆东西要实现小车上电后能自己走、自己拐弯、不撞墙。项目的本质其实就一句话用C语言基于STM32标准库函数编写控制逻辑让小车根据传感器数据实时决策运动方向。但这句话背后涉及的知识链条相当长从GPIO口的推挽输出配置到定时器的PWM模式从传感器数据读取到电机差速转向控制每一步都有坑。1.2 为什么用库函数而不是寄存器或HAL库先说结论标准外设库Standard Peripheral Library是学习STM32最好的中间层没有之一。很多新手第一次接触STM32会在寄存器开发、标准库、HAL库之间纠结。我做过一个不算严谨但很直观的对比开发方式代码可读性学习曲线调试便利性资料丰富度纯寄存器极差满屏位操作极陡峭非常困难少且零散标准库较好函数名即语义平缓便利极丰富HAL库好但封装过厚平缓偏曲折一般官方为主标准库的本质是什么它把寄存器操作封装成一个个函数和结构体比如GPIO_Init()这个函数你只需要填写一个GPIO_InitTypeDef结构体指定引脚号、模式、速度函数内部就会帮你完成复杂的寄存器配置。这个过程既没有丢失对硬件的控制感又不需要你死记硬背寄存器地址。我见过太多一上来就追HAL库的人CubeMX自动生成代码确实快但一旦出问题很难定位到底哪一步配置错了。而标准库的代码你是一行行自己写的每一条配置你都清楚它的用途。这就是我坚持用库函数做这个项目的核心理由。1.3 项目目标与技术路线规划在动手之前我把整个项目拆成了三个里程碑里程碑一点亮LED并实现串口打印确认开发环境、下载调试链路全部打通里程碑二电机能转通过PWM控制转速实现前进、后退、左右转里程碑三接入传感器数据实现完整的避障逻辑并通过实测验证这个拆法不是随意定的而是遵循嵌入式开发的基本规律先打通最小系统再逐模块增加功能。很多同学拿到板子就直接写避障逻辑代码结果电机不动、传感器读数为零根本没法判断是哪一层出了问题。我自己第一次做的时候就没忍住直接上了全套代码结果光排查环境问题就折腾了两天。2. 硬件选型与搭建要点2.1 主控板、电机驱动与传感器的选型逻辑先说主控。STM32F103C8T6是一块性价比极高的芯片72MHz主频、64KB Flash、20KB RAM做避障小车绰绰有余。它的Flash空间在标准库开发方式下非常充裕如果你用寄存器开发还能更省但没必要。电机驱动模块我选的L298N虽然它功耗大、效率低但控制逻辑简单极适合教学演示。它内部是两个H桥电路通过IN1-IN4引脚控制电机正反转ENA/ENB引脚接收PWM信号控制转速。后来我也试过TB6612FNG体积更小、压降更低但接线上比L298N多一些讲究。避障方案的选择值得多说几句。我当时同时用了两种传感器HC-SR04超声波模块测距范围2cm-400cm精度约3mm适合对前方障碍物进行远距离探测红外避障传感器探测距离可调2-30cm反应快适合近距离弥补超声波盲区超声波的探测角大约15度正前方可以探测到但左右前方的障碍物往往会漏掉。所以我在小车左右两侧斜前方各装了一个红外传感器这样三个传感器配合基本能覆盖小车前方180度的范围。如果你只有超声波也不是不能做但避障策略要保守很多——只能走直行-遇障-右转这种简单逻辑。2.2 供电系统的设计与实测功耗控制供电是整个项目中容易翻车的环节。我踩过最大的坑就是电机一转单片机就重启。原因很简单——电机启动瞬间电流可以达到正常工作电流的数倍瞬间拉低电源电压导致STM32的供电跌落到复位阈值以下。我当时用的是两节18650锂电池串联7.4V标称给L298N供电然后从L298N的5V输出口取电给STM32最小系统板。问题在于L298N的5V输出是线性稳压压差大、效率低大电流场景下纹波很大。后来我做了三处改进问题完全消失改用AMS1117-3.3稳压模块独立给STM32供电不跟电机共用电源在STM32电源入口并联一个470uF电解电容和104陶瓷电容做滤波电机电源线上加一个大电解电容吸收启动浪涌如果你手头只有一路电源至少要做到数字部分和功率部分在PCB或面包板上分开布线然后在单片机的VCC和GND之间并联足够容量的电容。这件事做好能少排查一半的诡异故障。2.3 电机与编码器的连接思考我第一次做的时候没用编码器直接靠PWM占空比开环控制也能跑。但如果你想做精细调速比如让小车直行时两个轮子转速一致就需要编码器了。带编码器的MG513电机是常见选择通过定时器的编码器模式读取AB相脉冲算出实际转速再做PID闭环调速。我给这个项目的定位是入门所以最终采用的是开环PWM控制——左右轮PWMA占空比相等硬件上通过同一路PWM输出给两侧电机。这种做法简单够用但有一个前提两个电机的机械特性差异不能太大。实测中我发现便宜电机的转速一致性大概有5%-10%的偏差这会导致小车不走直线需要在地面调试时微调占空比参数来解决。3. 开发环境搭建与工程初始化3.1 Keil MDK的安装与工程配置写STM32的标准库代码Keil MDK仍然是目前最顺手的IDE。安装过程比较简单需要注意两个环节安装对应芯片的器件支持包Device Family Pack比如F1系列的Keil.STM32F1xx_DFP在工程选项里配置下载器选择ST-Link或者J-Link接线方面ST-Link接SWD接口四根线即可SWDIO、SWCLK、GND、3.3V。SWD比JTAG省引脚四条线就够了强烈推荐新手用SWD。新建工程的时候一个标准的STM32F103标准库工程需要添加以下文件// 核心文件 core_cm3.c // Cortex-M3内核相关 system_stm32f10x.c // 系统时钟初始化 // 外设库源文件按需添加 stm32f10x_gpio.c // GPIO控制 stm32f10x_rcc.c // 时钟控制 stm32f10x_tim.c // 定时器 stm32f10x_usart.c // 串口 stm32f10x_exti.c // 外部中断 stm32f10x_adc.c // ADC如果用到 // 中断服务函数文件 stm32f10x_it.c工程配置里有几个容易疏忽的地方注意C/C选项卡的Define里需要填写USE_STDPERIPH_DRIVER这样编译器才会包含标准库的外设驱动头文件。漏了这一步编译会报一堆未定义类型的错误。另外芯片型号要选对。我之前用过一个国产兼容芯片型号显示为STM32F103C8T6但Flash只有64KB。如果不改Target选项卡里的Flash Size程序烧录时就会报错。保险做法是下载后用ST-Link Utility读取芯片ID确认一下参数。3.2 系统时钟初始化与标准库依赖关系从启动文件到main函数芯片上电后第一个重要动作是配置时钟。标准库工程默认从system_stm32f10x.c中的SystemInit()函数开始跑这个函数最终调用SetSysClock()把系统时钟设置到72MHz。这里有一个初学者容易忽略的点RCCReset and Clock Control是一切外设工作的前提。你用GPIO之前必须先把GPIO外设的时钟打开// 使能GPIOB和GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOA, ENABLE); // 使能定时器3的时钟 RCC_APB2PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);不打开外设时钟你对寄存器的任何写入都无效。这就像一栋楼的电闸没合上你在房间里怎么按开关灯都不会亮。时钟树是STM32最抽象也最核心的概念。标准库很贴心地用SystemInit()帮你处理好了大部分配置你只需要知道APB2是高速外设总线72MHzAPB1是低速外设总线36MHz。GPIO挂在APB2上所以GPIO的时钟使能函数参数里带APB2TIM3挂在APB1上所以带APB1。记不住的话查数据手册的框图最准确。3.3 第一个程序LED闪烁与串口调试助手我说过新工程的第一步一定要从LED闪烁开始。这不是走形式而是为了验证整个工具链是否通畅——编译、下载、运行、观测结果任何一个环节断了都能立刻暴露。GPIO输出模式的配置是第一个必须掌握的标准库用法GPIO_InitTypeDef GPIO_InitStructure; // 打开GPIOB时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 配置PB0为推挽输出速度50MHz GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // 点亮LED GPIO_SetBits(GPIOB, GPIO_Pin_0);这个结构体的每个字段都值得理解透彻GPIO_Pin指定操作哪个引脚可以是单引脚也可以多引脚按位或GPIO_Mode推挽输出Out_PP、开漏输出Out_OD、浮空输入IN_FLOATING、上拉输入IPU、下拉输入IPD、复用推挽AF_PPGPIO_SpeedGPIO翻转速度不是输出速度的最大值限制而是压摆率的配置。50MHz适合PWM这类高频信号LED闪烁用2MHz就够了LED能亮之后紧接着配置串口。USART1通过PA9TX和PA10RX输出调试信息用printf重定向到串口助手查看。这一步做不好后面传感器数据的观察会非常痛苦。4. 核心模块实现电机控制与传感器读取4.1 基于定时器的PWM输出配置流程电机调速的本质是控制平均电压。PWM脉宽调制通过固定频率下改变占空比来实现这一点——占空比高平均电压高电机转得快。我用的是定时器3的通道1和通道2分别输出PWM给左侧和右侧电机。标准库配置代码TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // TIM3时钟使能挂在APB1上 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 定时器时基配置 TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 分频72MHz/721MHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 周期1MHz/10001kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // PWM通道配置 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 500; // 初始占空比50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_Cmd(TIM3, ENABLE);这里两个关键参数要解释清楚Prescaler预分频把72MHz的系统时钟降下来我写到72-1是因为STM32的分频值是实际值减1。分频后频率是72MHz/721MHz即每微秒计数一次Period自动重装载值写到1000-1意味着计数器从0数到999总共1000次。那么PWM频率就是1MHz/10001kHz最终PWM频率1kHz对直流减速电机来说是合适的——频率太低电机会发出明显的嗡嗡声频率太高的话驱动模块的开关损耗会变大。调速的方法就是改变TIM_Pulse的值。比如50%占空比就是50025%就是250。4.2 L298N电机驱动的电平逻辑与控制表L298N的控制逻辑非常简单我直接说结果电机IN1IN2ENA动作左电机01PWM前进左电机10PWM后退左电机00任意停止右电机IN3IN4ENB动作右电机01PWM向前左转右电机10PWM向后右转选择左右轮都正转还是反转取决于你的电机接线。建议你先测试出两个轮子都让车向前走对应的电平组合再固化到代码里。这块板子的逻辑输入引脚可以直接由STM32的GPIO驱动不需要额外加电路。我实现的前进函数// 左电机前进 GPIO_ResetBits(GPIOB, GPIO_Pin_12); // IN1 0 GPIO_SetBits(GPIOB, GPIO_Pin_13); // IN2 1 // 右电机前进 GPIO_ResetBits(GPIOB, GPIO_Pin_14); // IN3 0 GPIO_SetBits(GPIOB, GPIO_Pin_15); // IN4 1 // 设置占空比 TIM_SetCompare1(TIM3, left_speed); TIM_SetCompare2(TIM3, right_speed);左右转的实现方式有两种。一种是小车原地旋转差速转向左轮正转、右轮反转车体绕中心点转动另一种是圆弧转向左轮速度慢、右轮速度快车走弧线。避障程序里我两种都会用——大幅度的转向用原地旋转小幅度的修正用差速偏转。4.3 超声波与红外传感器的数据读取超声波模块的工作原理不复杂给Trig引脚一个10us以上的高电平模块自动发射8个40kHz方波然后等待Echo引脚返回高电平。高电平的持续时间就是声波往返的时间。声速取340m/s的话距离cm的计算公式是距离(cm) 高电平时间(us) / 58推导很简单往返时间除以2是单程时间再乘以0.034cm/us换算一下就是除以58。标准库下用GPIO模拟读取Echo的高电平时长// 发送10us以上的触发高电平 GPIO_SetBits(GPIOB, GPIO_Pin_1); // TRIG delay_us(20); GPIO_ResetBits(GPIOB, GPIO_Pin_1); // 等待Echo拉高 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) 0); // 测量高电平持续时间 TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, ENABLE); while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2) 1); TIM_Cmd(TIM2, DISABLE); time_us TIM_GetCounter(TIM2); distance_cm time_us / 58.0;这里最好用定时器来计时而不是指望delay。如果主循环里有其他耗时操作delay计时会引入很大误差。红外避障模块更简单它输出的是开关量——有障碍物时输出低电平或高电平看模块版本直接读取GPIO即可。我用三个GPIO引脚分别接左、中、右三路红外传感器这样避障决策的输入信号就齐了。5. 避障逻辑设计与状态机实现5.1 状态机的设计与决策逻辑避障程序的核心是一个有限状态机。状态机这个概念听起来高级其实就是用变量记录当前状态根据条件跳转。我定义了五个状态typedef enum { STATE_FORWARD, // 直行 STATE_TURN_LEFT, // 左转 STATE_TURN_RIGHT, // 右转 STATE_BACK, // 后退 STATE_STOP // 停止 } CarState;避障决策逻辑我按优先级排列超声波距离小于20cm说明前方有近距离障碍先停车再决定方向左侧红外或右侧红外触发说明侧前方有物体往另一侧转向超声波距离在20-40cm之间说明前方即将进入危险区减速慢行超声波距离大于40cm且无红外触发安全全速直行转向的方向怎么选我采用了简单的右侧优先策略——前方有障碍时先尝试右转如果右侧也有障碍右侧红外触发再尝试左转如果两侧都不安全就后退一段距离再转向。这个决策逻辑用C语言实现就是一组if-else嵌套配合一个延时状态切换的变量防止状态跳变太快。比如右转动作需要持续500ms左右具体要看实测效果你不能每20ms就切换一次状态那样小车会变成抽搐式避障。5.2 主循环框架与状态切换时序主程序的核心是一个while(1)死循环每轮循环做三件事读传感器、做决策、执行动作。但这里有个实际问题超声波的测量周期大约60ms包含20ms触发间隔和40ms最大回波时间如果每轮循环都完整跑一遍整个循环周期会超过100ms。这在需要快速响应的避障场景下是来得及的但前提是程序里不能有阻塞式的delay。我第一次写的代码里右转就直接delay(500)这500ms内CPU完全卡死传感器数据也不更新了。后来改成非阻塞方式记录状态进入的时间每轮循环检查是否到时间uint32_t last_time 0; uint32_t interval 500; // 右转持续500ms if (current_state STATE_TURN_RIGHT) { if (GetTick() - last_time interval) { // 500ms到了切回直行 SetCarState(STATE_FORWARD); } }这里的GetTick()基于SysTick定时器实现每毫秒加一类似操作系统的tick时钟。这是一种非常实用的嵌入式编程技巧用查询的方式实现了不阻塞的定时反馈。5.3 速度控制参数的实测标定调试避障小车最花时间的环节是调整速度参数。如果你电机全速转遇到障碍再转方向车的惯性会导致转向来不及姿态已经偏了。我给速度初始化设了三档直行速度左轮PWM600右轮PWM580左轮比右轮偏快实测所以右轮多给一点转弯速度外侧轮PWM500内侧轮PWM250差速转弯不是原地旋转后退速度两轮PWM400这个参数的合适值跟你车身重量、电机型号、地面摩擦力都有关只能实测调整。我给的参考值是基于我的车模你拿自己的车要做的第一件事就是测慢速直行时左右轮占空比各配多少能走直线。注意如果PWM占空比过低小于100/1000电机可能因为静摩擦力过大而转不动这时候小车会发出嗡嗡声但不动。遇到这种情况先把最小启动占空比找出来确定电机线性调速区间的下限。6. 实地调试与常见问题排查6.1 烧录与连接相关的坑我遇到的最典型问题是ST-Link下载时报错error: no stm32 target found! if your product embeds debug authentication, please...。这个报错一般有几种原因SWDIO和SWCLK接反了这是最常见的低级错误目标板没有供电ST-Link的3.3V输出能力有限带不动整个小车系统BOOT0引脚的状态不对标准下载模式下BOOT0要拉低连接线太长或接触不良SWD信号在较长线缆上容易被干扰我建议的排查顺序是先检查线序再确认板子单独供电然后用万用表量SWDIO和SWCLK引脚在板子上的电压是否正常最后换一根短杜邦线试试。按这个顺序基本能解决90%的连接问题。还有一个Keil里的配置坑Utilities标签页的Settings里要正确选择下载器型号。如果你同时装了ST-Link和J-Link的驱动Keil可能选错导致下载失败。6.2 传感器数据和电机控制异常的处理超声波模块读数一直为0或者乱跳这个问题我问过不少人自己也踩过。90%的原因是Echo引脚接错或GPIO模式配置不对。Echo引脚需要配置为浮空输入或上拉输入如果用推挽输出去读结果是无效的。另外HC-SR04供电电压要求5V而STM32的GPIO是3.3V逻辑。Trig引脚接3.3V的高电平是否可靠实测中大多数模块能正常触发但也有个别模块对3.3V电平不敏感触发电平阈值刚好在3.3V附近临界。保险起见可以在Trig和Echo线上加一个电平转换或者选择兼容3.3V的模块版本。电机只转不走或者反转先查L298N的电源指示灯是否亮。如果灯亮但不转用万用表量PWM引脚对地电压占空比50%左右时应该量到1.65V左右的平均电压。如果测到的是0V或3.3V说明PWM没有输出检查定时器的配置。我还遇到过一种诡异情况程序下载后电机不动但只要手碰一下芯片就动了。后来定位到是复位电路的问题——复位引脚悬空电磁干扰导致芯片不断复位。在NRST引脚对地加一个104电容基本能解决。6.3 现场调试效率提升技巧避障小车的实地运行效果必须在真实地面上验证但反复弯腰捡车、摆车位很累。我后来发现一个提升效率的方法用一个纸箱或者木板搭一个高约20cm的测试台把车放上去跑这样不用弯腰就能观察和回收小车。如果空间够大在地面上用胶带贴出一块2m×2m的测试区域区域内放几个纸箱当障碍物跑完一个回合记录行为再调整参数。调试状态和数据反馈也很关键。我在程序里预留了串口调试接口用printf输出当前状态机的状态、三个传感器的数据和左右轮PWM值。跑一轮下来看串口日志能快速定位是传感器误判还是决策逻辑问题。实测中最让我印象深刻的点是室内瓷砖地面和地毯地面的摩擦系数差异极大同一组PWM参数在瓷砖上跑得好好的放到地毯上就原地打转走不动。所以参数标定最好在跟最终使用环境一致的地面上做否则调试结果没有参考意义。7. 项目复盘与扩展思考避障小车做完了回头复盘整个过程的收获我认为最核心的不是会配置GPIO、会用定时器而是建立了一种嵌入式开发的思维方式先确认环境再写代码先模块测试再系统集成先看清数据再调整参数。现在很多同学想直接用HAL库STM32CubeMX几分钟就能生成一个能跑的初始化工程但是遇到问题就手足无措因为不清楚底层发生了什么。而标准库的方式迫使你从每一位配置开始思考这种脚踏实地的过程对建立体系化的调试能力来说是不可替代的。做完基础版的避障小车还可以往里加很多功能。我后来给它加了蓝牙模块用手机App控制方向加了OLED显示屏实时显示距离和状态还尝试用编码器做了闭环速度控制直行精度提升了很多。再往深走可以用K210视觉模块做颜色识别避障或者移植FreeRTOS做多任务管理。最后再分享一个我做测试时踩过的坑锂电池用到后期电压下降电机速度会明显变慢但避障逻辑是按正常速度标定的。所以电压不足时小车的转弯动作会变形甚至撞上障碍物。这一点在做现场演示时尤其需要注意——提前检查电池电量或者程序中加入电压监测电不足时自动降低速度阈值。本文还有配套的精品资源点击获取