
简介一份基于STM32F103C8T6的智能小车完整工程资源涵盖寻迹与避障两大功能适合嵌入式初学者、电子设计竞赛参与者及想动手实践STM32开发的读者。资源包含硬件原理图与软件代码硬件部分涉及超声波模块、OLED显示、电机驱动等软件部分基于Keil uVision与STM32固件库实现了PWM调速、中断响应、PID控制等关键逻辑可帮助理解从传感器采集到电机控制的完整闭环设计。压缩包共181个文件以C语言源文件.c和头文件.h为主另有原理图PDF、Keil工程配置.uvprojx、.uvoptx、编译生成的hex与map文件等包体仅2.05MB轻量易下载。目前已有2977人学习浏览。代码结构清晰文件分类完整既可直接烧录运行验证效果也可作为二次开发基础是学习STM32嵌入式系统与自动控制应用的实用参考资料。 最近总有人拿着编译通过的代码来找我说小车下地之后要么原地打转要么直直冲到墙上去问是不是芯片坏了。其实这种“基于STM32的智能小车寻迹避障”项目难的不是点灯和写电机测试而是把传感器、控制逻辑和机械结构真正捏合到一起。我自己从最早用51单片机做过后来转向STM32帮人调过不少板子和代码这中间积累了不少经验。这篇就围绕寻迹、避障这两个功能把原理图设计和代码实现从头到尾捋一遍包括模块选型、电路细节、调参思路还有那些报错和坑。这套东西特别适合三类人准备电子竞赛或者做课程设计的学生刚接触STM32想做个能跑能转的小东西练手的入门者以及需要快速出demo的硬件工程师。如果你已经有了STM32F103C8T6的最小系统板再买几个传感器模块理论上一个下午就能让小车跑起来。但要想稳定还是得把原理、逻辑、参数挨个吃透。1. 项目整体设计与需求拆解1.1 寻迹和避障的本质是什么先说寻迹。常见方案是红外反射式传感器比如TCRT5000。传感器向下发射红外光黑线吸收光线白色地面反射光线模块上的比较器把反射强度转成高低电平。于是赛道上的“黑线”就变成了单片机引脚上的0和1。小车要做的事情就是不断读这些电平然后决定两个电机怎么转。差速转向是核心左轮快、右轮慢车就往右偏反之往左偏。所以寻迹算法本质上是一个感知-判断-执行的快速循环循环频率越高转向越平滑。避障是另一套逻辑。模块换成超声波测距传感器典型的是HC-SR04。单片机发出一个短脉冲然后测量回波时间用声速乘以时间除以2就算出了前方障碍物距离。避障策略并不复杂当距离小于设定阈值时让小车暂停、转向、再前进绕过障碍物。难的是这个转向动作要“恰到好处”转多了偏离赛道转少了撞上去。所以避障程序里很少用裸的if逻辑而是用状态机来管理“直行-检测-转弯-回正”这个过程。这两个功能单独做都不难难在同一个程序里同时跑还要保证响应速度。STM32F103的主频72MHz跑这点逻辑绰绰有余真正的瓶颈反而在传感器采样频率和电机机械响应上。1.2 方案选型用哪颗STM32、哪些传感器最省心芯片我首推STM32F103C8T6也就是大家常说的“蓝板”。这颗芯片虽然便宜但资源足够64KB Flash、20KB RAM、3个定时器、10个ADC通道、4路USART做寻迹避障绰绰有余。如果手里有stm32f103zet6这种大容量芯片板子大了一点引脚多用起来更舒服但其实功能上没有本质区别。关键是开发资料漫天飞出任何问题都能搜到答案。传感器配置上我建议这样选模块型号作用备注寻迹模块TCRT50003路或5路检测黑线数字输出阈值可调超声波模块HC-SR04测距避障波束角大注意安装角度电机驱动TB6612或L298N驱动直流电机TB6612效率高L298N简单粗暴电机4个直流减速电机或2个带编码器提供动力带编码器后续可以闭环电源18650电池组或7.4V锂电池供电必须和逻辑电源分开规划为什么不推荐更贵的灰度传感器或者激光雷达因为入门项目追求的是“快速跑通”TCRT5000数字输出直接接GPIO代码里读引脚就行HC-SR04测距逻辑简单示例代码一抓一把。等你这套玩熟了再升级到20路灰度或者OpenMV也不迟。2. 原理图设计与硬件搭建要点2.1 最小系统板与电机驱动电路原理图要点如果你打算自己做板子而不是买现成的核心板那原理图里必须包含STM32最小系统电源、复位、时钟、启动模式设置和下载调试接口。电源部分VDD每个引脚旁边都配一个100nF去耦电容这个不能省。很多自制板子跑飞或烧不进程序就是因为电源引脚附近没有走星型接地或者电容放得太远。芯片的VDDA引脚需要额外加滤波通常串联一个10Ω电阻再接DVDD同一端再放一个1uF电容。复位电路用一个10kΩ上拉电阻加一个0.1uF电容到地再接NRST引脚。时钟用8MHz无源晶振两个20pF负载电容夹在晶振两端。启动模式BOOT0、BOOT1要用电阻/跳线固定正常从Flash启动就都拉低。下载接口建议引出SWD的四根线SWDIO、SWCLK、GND、3.3V再多引一个NRST遇到无法连接时能派上用场。电机驱动部分要单独说。如果用L298N原理图上就是两个H桥结构IN1~IN4控制方向ENA和ENB接单片机的PWM引脚通过PWM占空比调速。L298N上有两个电源输入一个是电机电源比如12V另一个是逻辑电源5V。很多新手直接把12V接到单片机引脚上结果板子瞬间烧掉。一定要看清L298N模块的VCC和VSS分别是给谁供电。如果是TB6612它需要更高的输入电压VM和逻辑电压VCC3.3V到5V都可以输出接电机AIN1/AIN2/BIN1/BIN2控制方向PWMA/PWMB调速。TB6612比L298N的内阻小发热少用3.7V电池组就能跑我比较推荐。还有一点必须注意电机和单片机共地。不管用哪种驱动板电机驱动的GND必须和STM32的GND连在一起否则PWM信号没有参考地电机根本不会转或者乱转。2.2 传感器接口与电源分配TCRT5000模块一般是VCC、GND、DO三个引脚少数有AO模拟输出。DO输出的是经过比较器处理的数字电平黑线上为低电平或高电平具体看模块白底为相反电平。VCC可以直接接5V因为模块内部有比较器输出高电平是VCCSTM32F103的GPIO大多数能容忍5V电平但稳妥起见可以在DO引脚串联一个1kΩ电阻再进MCU避免万一引脚配置错误产生电流倒灌。HC-SR04的接口是VCC、Trig、Echo、GND。VCC一般接5VTrig引脚可以接3.3V单片机的GPIO因为它内部有电平转换或者当输入时判定阈值是2V以上3.3V足够。但Echo输出的是5V回波信号直接接到3.3V的PA0引脚长期运行有风险。按我习惯用一个10kΩ和5.1kΩ电阻组成分压电路把Echo的5V降到大约3.3V再进单片机。如果你用的是STM32F103C8T6引脚标明“容忍5V”的可以直连但不要依赖这一点加个分压电阻成本几乎为零。电源分配是整个原理图里最需要花心思的地方。我见过的小车异常复位七成以上都是电源问题。电机启动瞬间电流可能达到1A甚至更高会导致电压跌落。正确的分配思路是电池电压先进入电机驱动模块驱动模块直接给电机供电同时用驱动模块内部的5V输出或者单独用LM2596降压到5V给单片机和传感器供电。一定不要把单片机、传感器和电机电源放在同一条母线上否则电机一转单片机就复位。如果模块比较多可以加一个大电容比如470uF在电池端并联吸收尖峰电流。如果你自己画PCB还需要注意传感器安装位置。寻迹模块的间距要和赛道黑线宽度匹配一般中线模块对准车头中心两边模块间距20到30毫米。超声波模块尽量固定在前方支架上离地面8厘米以上不然会收到地杂波。3. 核心代码逻辑与工程实现3.1 工程结构初始化定时器PWM、GPIO、超声波测距我用的是标准外设库虽然HAL库更新但标准库的代码量大、直接适合理解底层。工程里主要做这几件初始化GPIO配置、定时器配置、串口配置。GPIO配置里电机方向引脚设置为推挽输出PWM引脚设置为复用推挽寻迹模块引脚设置为输入模式超声波的Trig设置为输出Echo设置为输入捕获或者普通输入。PWM用通用定时器TIM2产生配置成向上计数输出通道为PWM1模式。举个简化例子两路PWM分别控制左轮和右轮void TIM2_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr - 1; TIM_TimeBaseStructure.TIM_Prescaler psc - 1; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, ENABLE); TIM_Cmd(TIM2, ENABLE); }超声波测距最简单的实现是“拉高Trig延时10us再拉低然后等待Echo输入上升沿记录时间再等下降沿计算出时间差”。用普通延时函数就能做但效率低因为阻塞等待会浪费CPU。更好的方式是用定时器输入捕获或者在Echo引脚触发外部中断后开启定时器计时。入门先用第一种等代码跑通再优化不迟。算距离的公式float GetDistance(void) { uint32_t time; float distance; GPIO_SetBits(GPIOA, GPIO_Pin_2); // Trig delay_us(10); GPIO_ResetBits(GPIOA, GPIO_Pin_2); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) RESET); // 等待Echo变高 TIM_Cmd(TIM3, ENABLE); TIM_SetCounter(TIM3, 0); while (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_3) ! RESET); // 等待Echo变低 time TIM_GetCounter(TIM3); TIM_Cmd(TIM3, DISABLE); distance time * 340.0f / 20000.0f; // 按定时器单位换算 return distance; }这里有个细节超声波模块建议每次测量间隔至少60ms因为声波余振会影响第二次回波。连续测量会导致读数忽远忽近。3.2 寻迹算法差速转向与阈值处理寻迹最基础的逻辑是判断传感器组合。以三路寻迹为例左、中、右三个探头读回来三个电平可以归纳成下面这张表左中右小车状态101直行中探头压线001左偏需要右转回正100右偏需要左转回正000可能出了线或来到断线处111十字路口保持直行注意这里“1”和“0”的含义要根据模块特性调整有些模块黑线输出低电平所以要读代码和实测对齐。更平滑的算法是加权偏差。比如三路传感器的权重分别为-1、0、1把每个传感器检测到的结果乘上权重再加起来得到偏差error。直行时error0偏左时error-1偏右时error1。然后根据error调整左右轮占空比。如果加个简单的P控制器转向会更平滑int error 0; int left_speed BASE_SPEED; int right_speed BASE_SPEED; error -left_sensor right_sensor; // 左偏则为负右偏为正 left_speed BASE_SPEED - Kp * error; right_speed BASE_SPEED Kp * error;如果传感器路数更多比如5路权重可以设成-2、-1、0、1、2再乘上一个系数就能得到更细的偏差值。对于入门P控制足够。PD控制主要为了抑制振荡如果小车高速运行加上微分项会让转向更稳定但D项系数不能过大否则在传感器跳变瞬间会产生很大的抖动。还有一个关键阈值处理传感器并不是每次都能读到稳定的地线反射。比如在强光线下白底反射率降低模块可能误判。所以寻迹模块上都有一个可调电位器用来调节比较器阈值。调试的时候把小车放到黑线上观察模块指示灯亮灭再放到白底看指示灯是否翻转。反复调整电位器找到两边都稳定判别的中间点。3.3 避障策略超声波测距与状态机切换避障程序如果用最简单的if写法会出现这种情况超声波突然误报一个近距离小车就猛地转弯转过一点又误报远距离又急忙回正最后原地抽风。解决思路就是引入状态机。我常用的是一个简单四状态机状态行为跳转条件FORWARD直行距离 20cm 时切换到 DETECTDETECT停车并扫描连续3次距离小于阈值进入TURN否则回FORWARDTURN原地旋转或转弯旋转时间达到设定值进入FORWARDBACK后退有需要时后方有障碍时使用实际代码中用一个枚举类型定义状态typedef enum { FORWARD, DETECT, TURN_LEFT, TURN_RIGHT, } CarState; CarState state FORWARD; void AvoidLoop(void) { uint16_t dist; static uint8_t detect_count 0; switch (state) { case FORWARD: SetMotor(LEFT_PWM, RIGHT_PWM); dist GetDistance(); if (dist 25) { detect_count; if (detect_count 3) { state DETECT; detect_count 0; } } else { detect_count 0; } break; case DETECT: SetMotor(0, 0); delay_ms(200); // 简单策略默认向左转 state TURN_LEFT; break; case TURN_LEFT: SetMotor(-LEFT_PWM, RIGHT_PWM); // 左轮反转右轮正转原地左转 delay_ms(400); state FORWARD; break; default: state FORWARD; break; } }连续3次检测的目的就是滤波去掉超声波模块偶发的毛刺。转向时间需要根据实际机械结构调试通常200到600毫秒。如果车是两轮差速结构原地旋转角度和PWM占空比、时间基本成正比可以先试转一个固定时间测量实际转过多少度再反推。更优化的避障策略是左右两侧加装额外测距传感器检测前方障碍时判断左侧还是右侧空间更大然后向空间大的一侧绕行。这个升级只需要多两个超声波模块代码里加一个比较逻辑就行但对提升通过率非常明显。4. 调试方法与常见问题排查4.1 下载失败 “no stm32 target found” 的处理这个报错几乎是STM32新手必遇的问题很多时候并不是代码问题而是调试器连不上芯片。常见原因和排查顺序如下检查调试器接线。SWDIO接PA13SWCLK接PA14GND必须和板子共地。有些ST-Link需要3.3V供电如果板子没有独立电源用ST-Link的3.3V给板子供电也可以但要确保板子电流不大。检查驱动。在电脑的设备管理器里看ST-Link是否识别如果出现黄色感叹号重新安装驱动。在Keil的Options for Target里选择正确的调试器。CMSIS-DAP还是ST-Link不能选错SW方式选SWD模式速度尝试降低到1MHz以下。有些杜邦线太长或者质量差高频模式下SWD信号容易失真。按住板子复位键保持不放点击下载然后瞬间松开复位键。这个方法我调试自制板子时屡试不爽因为芯片有可能已经跑飞在复位瞬间恢复调试接口。如果还不行检查BOOT0引脚。把BOOT0拉高到3.3V然后再上电。这样芯片不运行Flash中的程序SWD调试接口更容易连接。连上后改回BOOT0拉低重新下载。检查芯片VBAT是否需要接3.3V。有些最小系统板VBAT悬空也能工作但自制板子上VBAT浮空可能导致内部复位失效。如果显示stm32 target not found但是能识别调试器有可能是芯片锁死。用STM32 ST-LINK Utility里的Connect Under Reset选项连接如果连上全擦除后就可以重新下载了。这算是STM32开发里的经典问题处理多了就顺手了。4.2 寻迹不稳定的几个坑寻迹小车跑偏、冲线、在弯道处甩出去这类问题最容易让人头大。我总结下来无非这几种原因首先是传感器阈值没调好。寻迹模块的灵敏度直接决定输入信号质量阈值调太灵敏会在线边缘反复跳变调太迟钝遇到弯道可能根本检测不到黑线。调试时建议用一张白纸画一条2厘米宽的黑胶带模拟赛道先把模块放到黑线上再放到白纸上观察输出电平是否稳定然后用小螺丝刀微调电位器直到边界清晰。其次是传感器布局和车体机械结构不匹配。如果两个寻迹模块间距小于轮距小车在蛇形前进时容易来回摆动如果间距太大在急弯时会间歇性丢线。一般三路寻迹的间距建议略小于车身宽度的一半五路寻迹则更密集适合复杂赛道。第三是车速太快。电机PWM占空比太大会让小车在弯道上离心甩出。解决办法是加PID或者限速可以在转弯状态强制将速度降为直行的60%到70%。这里有个小技巧直行速度用BASE_SPEED左右轮转入弯道时内侧轮不一定要降到零反而保留一个正转的小速度这样转弯更流畅不会原地打转。最后是电池电量影响。电池电压下降后电机转速明显变低之前调的PID参数可能不适用了。如果发现小车跑一段时间性能衰退先检查电池电压再调参数。4.3 避障误判的调参心得超声波模块的“误判”比寻迹更让人头疼因为它测的是一个锥形区域不是一条直线。HC-SR04的波束角大概15度左右加上墙壁回波经常会同时接收到多个回波信号导致距离读数忽大忽小。我实测下来最有效的方法是多次采样取中值。比如连续读取5次距离去掉最大值最小值剩下的取平均或取中值能过滤掉大部分随机毛刺。同时每次触发间隔保持在60ms以上不然接收电路余振会影响下一次测量。安装位置也很关键。超声波模块不要紧贴地面优选25cm到40cm的高度向前略微俯视5度左右。如果太低地面反射会先于障碍物回波到达接收头测出来的距离会偏近如果太高近距离小障碍物又测不到。如果你把它固定在一个可以调整角度的支架上调试时省事很多。避障阈值不要设太大。如果前方有2米空间却把避障阈值设成50cm小车会在没需要绕行的地方频繁转向。我一般设成20到25cm这个距离对应小车从检测到障碍到停下所需要的刹车距离。接近开关逻辑加状态机滤波之后误报率可以降到很低。写在最后最后说点个人经验。智能小车这个项目代码量不大原理图也谈不上复杂但它是少数能把“硬件设计-底层驱动-控制算法-调试排障”完整串起来的入门项目。我每次带新人都坚持一个顺序先点亮LED再让电机转起来然后接一个传感器测试最后才把它们合在一起。千万别一上来就烧完整程序否则出了问题你根本不知道是硬件还是代码的锅。踩过几次坑之后我自己的习惯是在电路板上预留尽量多的测试点传感器接口全用排针引出方便用示波器和万用表量信号。代码里每加一个功能就编译下载测试一次不要攒一堆代码再统一调。这个习惯看似笨但在做寻迹避障这种多模块协同的项目上真的能省下大把时间。这个项目后续扩展空间也很大比如加蓝牙模块改成手机遥控把红外寻迹换成摄像头循迹或者在避障逻辑里加入更复杂的路径规划。但无论如何先把引脚配置、定时器、PWM、状态机这些基本功练扎实后续所有花活都建立在这上面。本文还有配套的精品资源点击获取