简介:基于STM32F103C8T6的智能小车完整项目包,适合嵌入式入门者、高校课设与电子竞赛团队参考。压缩包内共181个文件,其中74个C源文件与78个头文件构成完整工程逻辑,另有PDF原理图、Keil工程配置文件(uvprojx/uvoptx)以及编译生成hex、map、lst等文件,整体仅2.05MB,结构清晰,可直接用Keil uVision打开并编译烧录。项目围绕寻迹与避障两条主线展开:寻迹部分通过传感器检测地面黑白线,由STM32对比信号差异后调节电机转速与转向;避障部分基于超声波模块发射与回波时间差计算障碍物距离,并设置安全阈值执行减速或转向决策。同时工程涉及PWM电机调速、外部中断响应、PID闭环控制以及OLED实时状态显示等关键知识点,配合原理图可完整还原硬件连接与信号流向。目前已有2991人学习下载,适合用于课程设计、毕业设计或竞赛方案的二次开发,也便于对照源码理解STM32外设配置与典型控制算法落地方式。
1. 基于STM32的智能小车:寻迹与避障的代码与原理图
这套资源不是那种只有main.c的残缺工程,而是把STM32小车真正跑通所需的两样东西都给了:可编译的寻迹、避障代码,以及对应的原理图。它解决的是新手最头疼的对不上账的问题——原理图画了PA0,代码里却操作PA1,最后车不跑还不知道查哪。适合正在备赛的学生,也适合想从点灯进阶到完整运动控制的开发者。如果你能看懂GPIO和定时器,但还没写过PID,这套资源能让你跳过最没有底气的硬件适配阶段,直接研究逻辑本身。
2. 硬件架构与原理图回读:主控、驱动与电源的五个关键连接
拿到原理图别急着看芯片型号,先找电源树和数据流向。这套资源的主控是STM32F103C8T6,属于最通用的蓝板小系统板,64KB Flash、20KB RAM,跑两路直流电机PWM加一路超声波测距完全够用。原理图整体分四块:电源输入、主控最小系统、电机驱动接口、传感器接口。把这四块的关系理清,代码里的外设配置就不需要瞎猜。
2.1 主控选型与固件库环境
STM32F103C8T6的主频72MHz,对于寻迹和避障来说性能是富余的。真正影响开发效率的是固件库选择:用标准外设库还是HAL库。这套资源的标准外设库版本更接近寄存器底层,适合想看清PWM和ADC是怎么配出来的人;HAL库封装更厚,适合习惯快速调通的开发者。建议拿到代码后先看头文件里包含的是stm32f10x.h还是stm32f1xx_hal.h,据此选对IDE的编译环境,否则打开工程就是几十个红叉。
2.2 电机驱动芯片的选型差异
原理图里电机驱动常见两种方案:L298N和TB6612。L298N便宜但压降大,满速时电池电压掉到5V以下很常见,而且引脚多、接线乱;TB6612体积小,低压降、发热低,适合小车这类小功率场景。这套资源的驱动模块两种都能接,但要注意共地。驱动模块的GND和单片机GND必须连在一起,否则PWM信号没有参考电平,电机要么不转要么乱转。
| 对比项 | L298N | TB6612 |
|---|---|---|
| 压降 | 约2V | 约0.5V |
| PWM频率上限 | 较低 | 较高,适合高频 |
| 引脚数量 | 多,需跳帽 | 少,接线清晰 |
| 适用电压 | 5V-35V | 2.7V-15V |
| 发热量 | 大 | 小 |
2.3 引脚分配与定时器通道绑定
这是原理图里最容易埋雷的地方。PWM信号必须接到定时器的通道引脚上,不是随便挑一个GPIO就能输出PWM。这套资源典型的引脚分配如下:
| 功能 | 引脚 | 定时器/通道 |
|---|---|---|
| 左轮PWM | PA0 | TIM2_CH1 |
| 右轮PWM | PA1 | TIM2_CH2 |
| 左轮方向 | PB0 | GPIO输出 |
| 右轮方向 | PB1 | GPIO输出 |
| 超声波Trig | PB5 | GPIO输出 |
| 超声波Echo | PA6 | TIM3_CH1捕获 |
| 舵机PWM | PA2 | TIM2_CH3 |
| 四路循迹ADC | PC0-PC3 | ADC1_IN10-13 |
如果你要改代码,先把这张表抄下来,再对照原理图走一遍网络标号。PA0和PA1在原理图上写着TIM2_CH1、TIM2_CH2,如果代码里用了TIM3_CH1,那就是资源本身标注不一致,按原理图改代码就行。
2.4 PWM频率与占空比的计算
代码里PWM的初始化参数直接关系到小车低速时是否发抖。以72MHz主频为例,定时器预分频PSC=71,重载值ARR=1999,算出来PWM频率是500Hz,周期2ms。这个频率对直流电机足够平滑,对舵机也兼容(舵机要求50Hz,但一般通过独立定时器配置)。初始化时还要注意把PWM引脚配成复用推挽输出,模式错了输出不了波形。
/* 定时器2 PWM初始化:PA0/PA1复用推挽 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; 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_TimeBaseInitTypeDef TIM_InitStructure; TIM_InitStructure.TIM_Prescaler = 71; TIM_InitStructure.TIM_Period = 1999; TIM_InitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_InitStructure);这里PSC=71和ARR=1999这两个参数决定PWM频率。PWM频率=72MHz/(PSC+1)/(ARR+1),代入就是500Hz。占空比由TIM_Pulse决定,2000对应100%占空比,跑400就是20%左右。改代码时先动Pulse值,不要动分频系数,否则转向手感全变了。
2.5 电源树与去耦电容
原理图上最容易被忽略的是电源去耦。MCU的3.3V引脚边上应该放一个0.1uF陶瓷电容,电机电源入口放470uF电解电容。堵转时电机电流能拉到2A以上,如果没有储能电容,5V会被拉穿进而破坏STM32供电。避障小车比纯寻迹小车更依赖稳定供电,因为超声波模块在电压波动时会输出异常Echo电平,直接导致测距跳变。
3. 寻迹模块与差速转向:从红外对管阈值到PWM输出
寻迹这部分资源代码的核心是四路红外对管传感器。每个传感器由一个红外发射管和一个接收管组成,黑线吸收红外光,白线反射红外光,接收管输出的模拟电压会随反射强度变化。常用方案有两种:一种是对管模块自带比较器输出数字电平,另一种是直接引出模拟电压进ADC。资源里用的是ADC采样,好处是可以在软件里做动态阈值,不用每次换场地都去拧模块上的电位器。
3.1 四路对管的布局与数据读取
四路对管一般排成一条直线,距离为1.5cm-2cm,安装高度控制在2cm左右。高度太低容易压线抖动,高度太高则黑白差异不明显。代码初始化ADC后,循环读取PC0-PC3的转换结果。读回来的原始值范围是0-4095,直接用这个值做判断容易被环境光干扰。更好的做法是先做归一化:每轮读取后取四个通道的最大值和最小值,把每个通道映射到0.0-1.0。
/* 四路ADC读取并归一化 */ uint16_t adc_raw[4]; float sensor_ratio[4]; void sensor_update(void) { uint16_t min_val = 4095, max_val = 0; for (int i = 0; i < 4; i++) { adc_raw[i] = adc_read_channel(i); if (adc_raw[i] < min_val) min_val = adc_raw[i]; if (adc_raw[i] > max_val) max_val = adc_raw[i]; } for (int i = 0; i < 4; i++) { sensor_ratio[i] = (float)(adc_raw[i] - min_val) / (max_val - min_val + 1); } }归一化的作用是让阈值不再依赖绝对电压。min_val和max_val是本轮的动态范围,如果车跑在均匀光照下,这个范围基本稳定;如果突然从室内到阳光直射,max和min会同时变化,ratio值依然能区分黑白线。加1是为了防止max等于min时除零。
3.2 动态阈值判断与出线处理
比完归一化之后,用0.5作为黑白分界。ratio大于0.5说明该路传感器大部分落在白区,小于0.5说明压到黑线。四路状态可以编码成一个4位二进制数,例如只有第二路压线就是0b0100。常见的判断逻辑是:如果中间两路有任意一路压线,车头基本对准线;如果只有左边压线,说明车往右偏了;如果只有右边压线,说明车往左偏了。
最难的场景是四个通道全部出线。这种情况往往是急弯把车甩出赛道,或者地面反光把黑线盖住了。固定写死的处理方式是原地转圈,效果很差。资源代码里的处理方式是记住上一帧的方向,继续朝那个方向转并降低速度,直到再次寻到线。
3.3 比例转向与差速控制
寻迹转向不是简单的左转右转两档,那样车会画龙。资源的控制逻辑用的是比例转向:根据压线位置算出偏差量,再按比例叠加到左右轮PWM上。偏差量计算常见做法是给四个通道加权:最左通道权重-2,中间左-1,中间右+1,最右+2,加权求和后再归一化到-1到+1。偏差为正表示车偏右,需要右轮减速、左轮加速。
/* 比例转向:偏差叠加到左右轮 */ int position_weight = -2 * (sensor_ratio[0] > 0.5) -1 * (sensor_ratio[1] > 0.5) +1 * (sensor_ratio[2] > 0.5) +2 * (sensor_ratio[3] > 0.5); int speed_diff = position_weight * 15; /* 比例系数,调试时从10开始加 */ int left_speed = base_speed + speed_diff; int right_speed = base_speed - speed_diff; set_motor_speed(0, left_speed); /* 左电机 */ set_motor_speed(1, right_speed); /* 右电机 */speed_diff的比例系数15是经验值,太小会导致转向不足冲出赛道,太大会原地打转。调试顺序建议:先固定base_speed=300(PWM占空比约15%),只调比例系数,观察车在直线和弯道的表现。系数从10起步,每加2跑一圈看效果。如果车在过弯时抖得厉害,说明比例系数过大,降低而不是强行加大。
4. 避障逻辑与状态机:超声波测距与三方向决策
避障部分采用HC-SR04超声波模块。原理是Trig引脚给一个10us以上的高电平,模块内部发送8个40kHz脉冲,Echo引脚输出高电平,高电平持续时间就是声音从发射到返回的总时间。声音在空气中速度约340m/s,往返距离除以2得到障碍物距离。资源代码里拆了两种实现方式:延时查询和输入捕获。延时查询简单但占用CPU,输入捕获更稳定。
4.1 超声波时序与距离换算
先明确时间单位。HC-SR04的Echo高电平时间与距离成比例,距离约等于高电平时间除以58。查表得知,1cm对应约58us。代码实现时可以这样写:测得时间us = 58 × 距离cm。在STM32上如果没有输入捕获,用外部中断加定时器计时也能做,但精度差一些。
/* 超声波测距:输入捕获方式 */ uint32_t echo_start, echo_end; uint32_t measure_distance_cm(void) { GPIO_SetBits(TRIG_PORT, TRIG_PIN); /* Trig拉高 */ delay_us(15); /* 至少10us */ GPIO_ResetBits(TRIG_PORT, TRIG_PIN); /* Trig拉低 */ while (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN) == RESET); echo_start = TIM_GetCounter(TIM3); while (GPIO_ReadInputDataBit(ECHO_PORT, ECHO_PIN) != RESET); echo_end = TIM_GetCounter(TIM3); return (echo_end - echo_start) / 58; }TRIG_PORT和ECHO_PORT的宏定义在头文件里,换板子时只需要改这两个宏,不用动测距函数内部。while循环等Echo上升沿和下降沿是阻塞式的,这段时间大约几十到几百微秒,对避障应用完全够用。真正要注意的是TIM3的计数频率,如果定时器没有配置成1MHz计数,这里算出的差值就不是微秒,距离会差倍。
4.2 舵机云台与三方向扫描
单超声波只能测正前方一个方向,撞到侧面的障碍物就看不到。资源里加了一路舵机云台,SG90舵机用50Hz、0.5ms-2.5ms的PWM控制角度。通过舵机把超声波转到左、中、右三个方向,每个方向测一次距离,再决策怎么走。舵机初始化后先让云台回中,也就是角度在90度左右,等待300ms稳定后再开始扫描。
转向云台的执行流程是:先转到左45度位置,延时200ms,测距;再回中,延时200ms,测距;再转右45度,测距。这个循环保证了超声波稳定输出。如果直接连续转动测距,云台还没停稳Echo就返回了,数据会飘。
4.3 避障状态机的决策逻辑
避障决策用一个状态机管理。默认状态是直行,每次测完三个方向后更新状态。判断优先级是:正前方距离小于25cm时进入转向状态;左前方最远就左转;右前方最远就右转;如果都近就后退。转向完成后恢复直行。
/* 避障状态机 */ #define NAV_FORWARD 0 #define NAV_TURN_L 1 #define NAV_TURN_R 2 #define NAV_BACKWARD 3 uint8_t nav_state = NAV_FORWARD; float dist_left, dist_front, dist_right; void obstacle_update(void) { dist_left = measure_angle(45); dist_front = measure_angle(0); dist_right = measure_angle(-45); if (dist_front < 25.0f) { if (dist_left > dist_right && dist_left > 30.0f) nav_state = NAV_TURN_L; else if (dist_right > 30.0f) nav_state = NAV_TURN_R; else nav_state = NAV_BACKWARD; } else { nav_state = NAV_FORWARD; } execute_nav(nav_state); }dist_left优先的条件是左前方距离大于30cm。30cm这个数字是经验值,太大会频繁转向,太小会撞到侧面。注意执行转向时要把车速降下来,否则状态机切换的速度跟不上小车前进的速度,容易直接撞墙。资源里转向动作是固定PWM持续一段时间,我一般会改成“转向直到前方距离大于40cm”,用超声波数据做闭环退出条件。
5. 避坑:常见问题排查与血泪经验
这套资源整体是可用的,但新手照着焊完、下完代码之后,至少有五个坑会在前十分钟内遇到。我把它们按发生频率排个序。每一条都是我在调试同类小车时真实踩过的,按现象、原因、解决三步写清楚。
5.1 上电电机就自己转
现象:刚插上电池,还没按下启动键,两个电机就吱吱转。原因有三:一是STM32的PA0、PA1在初始化前处于浮空状态,定时器还没配置时引脚电压不确定,驱动芯片检测到高电平就输出;二是方向控制引脚悬空;三是使能引脚EN被默认拉高。解决:在代码最开头、所有外设初始化之前,先把方向引脚强制拉低,使能引脚也初始化为低电平;或者干脆加一个物理拨码开关串联在电机电源上,先下完程序再推开关上电。
5.2 超声波测距突然跳0或跳几百
现象:车正前方50cm空空如也,超声波却报出5cm或者0cm,避障逻辑乱跳。原因:Trig的高电平时长不够是典型原因,HC-SR04要求至少10us,但很多人用delay函数近似,在72MHz下delay的实现差异很大,实际可能只有2us;另一个原因是近距离盲区,HC-SR04在2cm以内测不准。解决:先把Trig拉高时间改成delay_us(15),实测20us更保险;同时把超声波安装位置设计成离车头至少3cm;最后在软件里加一个滤波,连续三次测距结果相差超过20cm就丢弃重测。
5.3 ST-Link下载失败或下载后程序不跑
现象:MDK里点击下载提示找不到目标芯片,换一个下载器就能连上;或者下载成功但复位后没反应。原因:最常见的是调试器和板子没共地,SWD四线里的GND没接;还有一种情况是板载调试器的3.3V跳线帽还在,外部电源同时供电导致电压冲突;最后是BOOT0跳线被拨到1,芯片进入ISP模式,不会跑主程序。解决:先检查SWD排线,确保GND是通的;然后拔掉板载调试器的电源跳线帽,只保留SWDIO、SWCLK、GND三根线;最后确认BOOT0跳线在0侧。
5.4 白底反光导致寻迹乱拐
现象:小车在深色地毯上跑得正常,换到浅色地砖或者阳光直射的地方就疯狂摆头。原因:固定阈值判定被环境光打破。地砖反射红外光本来就强,如果阈值的固定电压选低了,白底也会被误判成黑线。解决:用上面的动态阈值归一化取代固定阈值判断;如果对管模块只输出了数字量,就在每个模块上加可调电位器并带上锂电池现场校准。资源代码如果阈值写死,改成进入主循环前做一次自校准即可。
5.5 堵转压降导致单片机复位
现象:小车爬坡或者被卡住时,屏幕闪烁,超声波数据变为0,程序重启。原因:电机堵转电流瞬间升高,电池电压被拉低,5V稳压输出跟着塌了,STM32的3.3V低于复位阈值。解决:电机电源和逻辑电源强制分开,电机从电池端取电,逻辑部分从稳压器取电;在电机电源入口并联一颗470uF电解电容;如果小车还没做PCB,直接在驱动模块的VM和GND之间飞一颗大电容,效果立竿见影。
6. 进阶:参数标定、PID调优与程序固化
到这步,你的小车已经能按套路跑起来了。如果想跑得更稳,我建议按下面这套流程做一次完整的参数标定。第一步是架空测试:把四个轮子悬空,固定PWM输出,观察左右轮转速是否一致。大多数翻车都是左右轮在相同占空比下转速不同导致的车身画龙,这一步能提前暴露问题。第二步是低速实测:把base_speed设成200,跑一圈标准赛道,记录每个弯道是入弯太慢还是出弯甩尾。第三步是加速迭代:每次加50的base_speed重跑,直到找到振荡临界点,然后退回上一档作为正式速度。
PID调优不要上来就上完整PID,先只用P项。把上面的speed_diff比例系数独立出来,调一个P值让小车不出赛道。然后加一点点D项抑制抖动。一个可复用的起点是P=1.2,D=0.4,I=0,控制周期10ms。这里P作用于位置偏差,D作用于偏差的变化率。如果P太大,小车会在赛道中线附近快速摆动;D太大,弯道入弯表现僵直。
最后是程序固化。每次调好参数后,把PWM比例系数和阈值存到Flash末尾地址,用的函数是STM32标准库的FLASH_ProgramHalfWord。上电时优先读Flash,读不到才用默认值。我习惯做一个拨码开关进入标定模式:上电时拨码在1档,小车原地扫描白底和黑线各100次,自动算出阈值并写Flash;拨回0档进入正常运行。这样换场地不用重新编译代码,拨一下开关就行。
从那以后,我每次调完寻迹小车都会强制走一遍完整流程:架空测转速、低速跑三圈、再加速度迭代,最后拨码标定收尾。这四步能避开大部分翻车现场,希望帮到你。
本文还有配套的精品资源,点击获取