news 2026/9/10 9:20:41

STM32循迹避障小车源码解析:从外设分配到PD控制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32循迹避障小车源码解析:从外设分配到PD控制

简介:基于STM32的循迹避障小车毕设源码包,面向计算机、电子类专业正在准备毕业设计或课程项目的学生,也适合需要完整嵌入式实战案例的进阶学习者。项目源自大四高分毕设,评审99分,经导师认可,代码完整可运行,硬件原理图、PCB、结构模型与源码均整理在内,能够直接用于功能演示与二次开发。包内共322个文件,以C语言源码、STM32标准外设库头文件及编译链接产物(.o/.crf/.axf/.hex)为主,同时包含Keil工程配置、原理图与PCB文档(.schdoc/.pcbdoc)、3D模型文件(.stl/.sldprt)和说明文档(.pdf/.md),从代码到实物层次完整;压缩包整体64.22MB,结构清晰,便于按模块查阅。已有164人学习下载,适合拿来作为期末大作业、课程设计或毕设参考,也可为循迹、避障、电机驱动等STM32典型应用提供可直接运行的示例。

1. 为什么这个STM32循迹避障小车值得拆开看

把一份完整跑通、评过99分的STM32循迹避障小车源码当研究对象,比看零散教程有价值得多。这套资料不是随手拼的:stm32f10x_adc.c 负责循迹灰度采样,stm32f10x_tim.c 同时承担电机PWM和超声波输入捕获,stm32f10x_flash.c 把校准阈值掉电保存,连 i2c.c、can.c、fsmc.c 都是模板工程自带、按需裁剪的外设驱动,说明作者保留了扩展接口而不是把删不掉的代码硬塞进去。对于要做毕业设计、课程设计或期末大作业的人,最值得学的不是寻线算法本身,而是"一个完整工程里外设怎么分配、模块怎么组织、哪里留扩展"。这篇把硬件资源分配、循迹与避障的代码实现、拿到工程后的编译烧录排错完整过一遍,适合在校生,也适合刚接触标准外设库的嵌入式新人。

2. 硬件选型与STM32外设资源分配

2.1 为什么是F103ZET6 + 标准外设库

循迹避障小车是 STM32 毕设里出现频率最高的选题,但很多人的代码只能在实验室跑通、换块板子就废。这套源码选的是 STM32F103ZET6,72MHz 主频、512KB Flash、64KB SRAM,资源对小车主控来说溢出不少,这不是浪费,而是刻意留了余量:循迹、避障跑完之后,还能挂 OLED、ESP8266 甚至跑 Freemodbus。库用的是标准外设库而不是 HAL,原因很实际——大量课程设计和毕业设计资料都基于标准库,遇到问题搜"GPIO_Init"比搜"MX_GPIO_Init"能多找到十倍的结果,而且标准库的结构对理解寄存器映射更友好。

工程里 system_stm32f10x.c + stm32f10x_rcc.c 决定了整个芯片的时钟走向,这里值得先弄清楚,因为后面所有外设的波特率、PWM频率、采样周期都挂在时钟树上。默认配置是外部 8MHz 晶振,PLL 9 倍频到 72MHz,APB1 分频到 36MHz(TIM2/3/4/5 的时钟源),APB2 保持 72MHz(GPIO、ADC1、TIM1 的时钟源)。改外置晶振数值时,不要把 system_stm32f10x.c 里的 PLL_M、PLL_N 孤立地调,要连 HSE_VALUE 宏一起改,否则串口波特率会整体偏移,这是老手也容易忽略的地方。

2.2 整车模块引脚分配

拿到工程第一步不是看 main.c,而是先建一张引脚占用表,确认各模块没有抢占同一组外设。下面是这套小车源码中推荐的一套分配方案,与标准库工程的默认配置对应,改动最少:

模块引脚/外设功能说明
左电机PWMPA6 / TIM3_CH1左轮速度,10kHz
右电机PWMPA7 / TIM3_CH2右轮速度,10kHz
电机方向PC0~PC3 GPIO四路IN引脚,控制正反转
五路循迹PC4~PC8 GPIO/ADCTCRT5000传感器阵列
超声波TrigPA2 GPIOHC-SR04触发引脚
超声波EchoPA3 / TIM2_CH4回波输入捕获
OLED显示PB6/PB7 I2C1运行状态显示
调试串口PA9/PA10 USART1115200-8N1,打印日志

这里有个容易被新手踩到的坑:PA2/PA3 同时也是 USART2 的 TX/RX,如果想把超声波和蓝牙模块一起挂上,就会冲突。代码里超声波占用了 PA3 的 TIM2_CH4 复用功能,蓝牙只能换到 USART1(PA9/PA10),串口调试和蓝牙共用一个串口时,用跳线切换,不要同时接。FSMC 和 I2C 在这个项目里处于"模板保留"状态:I2C1 给了 OLED,FSMC 预留了外扩 TFT-LCD,CAN 接口在驱动代码里初始化了但没有接总线,后续要做多机通信或 Freemodbus 移植可以直接用上。

提示:GPIO 配置时,PWM 引脚要开复用推挽(GPIO_Mode_AF_PP),方向引脚用推挽输出(GPIO_Mode_Out_PP),循迹输入引脚用浮空输入即可,不要开上拉——上拉会直接影响灰度传感器的电平翻转点。

2.3 电机驱动与TIM3 PWM初始化

电机驱动常见两种方案:L298N 和 TB6612。L298N 便宜但压降大(约2V),电池电压 7.4V 经驱动后到电机可能只剩 5V 多,爬坡和转向时会明显乏力;TB6612 管压降只有 0.5V 左右,1.2A 连续电流够用,发热低,这套源码对应的驱动板方案也是兼容 TB6612FNG 的。选型时还要注意:TB6612 的 VM 接电池正极,VCC 接 3.3V 或 5V 逻辑电源,两者不能接反,否则芯片直接冒烟。

PWM 初始化是独立封装的函数,不依赖其他模块,可以直接抄下来做电机起转测试:

void TIM3_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); tim.TIM_Prescaler = psc; // 72MHz / (psc+1) 得到计数频率 tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_Period = arr; // PWM 频率 = 计数频率 / (arr+1) tim.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, &tim); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; // 初始占空比 0,防止上电猛冲 oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM3, &oc); TIM_OC2Init(TIM3, &oc); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }

初始化时把 preload 打开,占空比更新会在计数周期边界同步生效,不会出现运动中突然顿挫。调用时用TIM3_PWM_Init(7199, 0),PWM 频率就是 72MHz / 7200 = 10kHz。10kHz 对直流减速电机来说是合适的频率——低于 1kHz 能听到明显的电磁噪声且扭矩波动大,高于 20kHz 驱动芯片的开关损耗上升但扭矩无明显提升。如果后期换了大功率电机,这个频率可能需要下调到 5kHz。

注意:电池给电机供电时,GND 必须和 STM32 开发板的 GND 相连(共地),否则 PWM 信号没有参考电平,电机轻则抖动不转,重则 GPIO 口被击穿。这是小车不上电转、上电乱转的头号原因。

3. 循迹模块:ADC灰度采样与PD转向修正

3.1 TCRT5000 循迹原理与两种读取方式

TCRT5000 是光电反射式传感器,内部一个红外发射管加一个光电接收管。红外光照射到白色地面反射强,接收管导通压降低;照射到黑色胶带时大部分红外被吸收,接收管截止电压升高。把接收端电压接到比较器或直接接 MCU,就能区分黑白线。

读取传感器有两种典型方式。一种是模块自带比较器,输出数字电平(黑线低电平/白线高电平),GPIO 直接读引脚即可,优点是代码简单、抗干扰强,适合实验室白底黑线这种对比清晰的场景。另一种是模块只引出模拟电压(ADC 采样),MCU 通过 ADC 判断明暗程度,可以输出 0~4095 的灰度值,适合地砖颜色不统一或赛道有阴影的场地。这套源码里保留的是 ADC 方案,对应 stm32f10x_adc.c,因为 ADC 方案能拿到连续灰度值,便于做阈值自适应,这是评分时的一个加分点。

3.2 ADC采样与阈值校准

ADC 部分用 ADC1 的规则通道组连续采样。循迹传感器阵列的模拟输出接入的是 PC4/PC5 等引脚,其中 PC4 对应 ADC1_IN14、PC5 对应 ADC1_IN15,可以用独立模式单通道依次转换,也可以开扫描模式一次把五路全部采完。注意:ADC 引脚采样时间不能太短,TCRT5000 的响应时间是微秒级的,采样时间设置低于 55.5 周期时,在高速行驶中会采到毛刺值。工程里的配置是采样时间 239.5 周期,配合 12MHz ADC 时钟,单通道采样率约 50kHz,对循迹场景绰绰有余。

读取五路灰度值的核心逻辑:

#define SENSOR_NUM 5 u16 sensor_adc[SENSOR_NUM]; // 五路原始ADC值 u16 threshold_low[SENSOR_NUM]; // 每路独立阈值 void read_sensors(void) { // 使用ADC1的注入组别,一次触发依次转换五通道 // 注入组转换结果锁存,读取期间不会被规则组打断 ADC_SoftwareStartInjectConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_JEOC) == RESET); sensor_adc[0] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1); sensor_adc[1] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_2); sensor_adc[2] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_3); sensor_adc[3] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_4); sensor_adc[4] = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_5); }

注入组通道需要预先用ADC_InjectedChannelConfig()完成映射,转换顺序和read_sensors()中的读取顺序要保持一致。使用注入组而不是规则组的原因:PWM 和超声波中断会频繁抢占外设总线,规则组 ADC 的转换结果寄存器有限,转换慢时易被覆盖;注入组有独立的结果寄存器,适合在时序关键的主循环里"一次性快照"五路信号。

阈值怎么定?黑线和白地之间的 ADC 差值通常在 800~1500 之间,工程里用每路独立阈值threshold_low[i],而不是统一用一个全局阈值。原因很实际:五个传感器的安装高度、红外管衰减程度不可能完全一致,统一阈值会出现"中间三路正常、边缘两路永远误判"的问题。校准时把车放在赛道上,分别记录压黑线和压白地的值,取中值作为初始阈值,然后把这个值存进 Flash(见 3.5 节),下次开机免重复校准。

3.3 五路加权偏差计算

拿到五路二值化结果后,要把"哪一路看到黑线"换算成一个连续的偏差量,喂给 PD 控制器。最常用的是加权平均法:中间传感器权重 0,左侧为负,右侧为正,按激活状态加权平均得到归一化位置。

// 将ADC值与阈值比较,得到五路逻辑值 // 0=白底,1=压线 static u8 sensor_binary[SENSOR_NUM]; static const s8 weight[SENSOR_NUM] = {-2, -1, 0, 1, 2}; s16 get_line_position(void) { s32 sum = 0, active = 0; for (int i = 0; i < SENSOR_NUM; i++) { sensor_binary[i] = (sensor_adc[i] < threshold_low[i]) ? 1 : 0; sum += sensor_binary[i] * weight[i]; active += sensor_binary[i]; } if (active == 0) { // 全部白底:说明冲出赛道,返回一个极值让小车紧急寻线 return (last_position > 0) ? 100 : -100; } last_position = (s16)(sum * 100 / active); return last_position; }

这段代码的关键在active == 0的分支:五路全白意味着车已经脱离黑线,此时不能返回 0(会让小车笔直开走),而是沿用上一次的偏移方向返回极值,让转向逻辑把小车的弯打回去寻线。sum * 100 / active把位置归一化到 -100~+100,100倍是为了让 PD 的 P 项系数不至于太小,调参时看着更直观。

3.4 PD转向控制与参数调节

PWM 差速转向的实现思路:把位置偏差换算成左右轮速度差,偏差越大转向越猛。单纯 P 控制的问题是高速入弯时转向滞后,车身会冲过弯心;加上微分项 D 后,偏差变化速率被提前感知,入弯前就开始收油。

static float kp = 1.8f, kd = 0.6f; static s16 last_pos = 0; void motor_control_by_line(void) { s16 pos = get_line_position(); float diff = kp * pos + kd * (pos - last_pos); last_pos = pos; s16 speed_l = BASE_SPEED + (s16)diff; // 左轮 s16 speed_r = BASE_SPEED - (s16)diff; // 右轮 // 限幅保护:堵转电流过大会拉低电池电压 if (speed_l > MAX_SPEED) speed_l = MAX_SPEED; if (speed_r > MAX_SPEED) speed_r = MAX_SPEED; if (speed_l < -MAX_SPEED) speed_l = -MAX_SPEED; if (speed_r < -MAX_SPEED) speed_r = -MAX_SPEED; TIM_SetCompare1(TIM3, abs(speed_l)); TIM_SetCompare2(TIM3, abs(speed_r)); // 方向引脚根据speed_l/speed_r符号置高或置低 set_motor_dir(LEFT, speed_l >= 0); set_motor_dir(RIGHT, speed_r >= 0); }

调参顺序建议从纯 P 开始:先设 kd=0,kp 从 0.5 逐步增大,直到小车在直道上不摆头、弯道上不冲出赛道,此时记下 kp;再累加 kd,每次加 0.2,观察入弯是否更平滑。如果小车在直道上左右震荡,说明 kp 偏大或 kd 偏小;如果入弯后转向不足冲出去,说明 kd 过大把转向动作"提前削减"了。BASE_SPEED 的典型范围是 300~500(占空比 0~7199 时的数值),低于 300 时电机接近堵转临界点,高于 550 时大部分赛道的 U 形弯会甩尾。

现象调整参数
直道抖动、蛇形kp 减小 20% 或 kd 增大 0.2
入弯转向不足、冲出赛道kd 减小或 kp 增大
弯道内侧轮反转限幅值设太高,改回 500 基准
循线稳定但响应迟钝kp 增大 10%,观察两圈

PD 参数不是固定的,电池电压从 8.4V 掉到 7.2V 时电机响应变慢,同一套参数表现会差不少。正式调试时用充满电的电池调完,比赛或演示前再充满电,避免参数漂移。

3.5 阈值掉电保存到Flash

每次开机都要把车放赛道上重新校准一次,在答辩演示时非常尴尬。工程里把五路阈值写进了 stm32f10x_flash.c 驱动的功能范围:启动时先用一小段内嵌逻辑检测 Flash 里是否有有效校准数据,有就直接加载;没有才进入校准模式。这里注意 F103ZET6 的 Flash 按扇区管理,前 4 个扇区各 16KB,写之前必须先擦除整扇区:

#define CALIB_FLAG_ADDR 0x0807F000 // 最后64KB扇区起始 #define CALIB_DATA_OFFSET 0x10 void threshold_save_to_flash(u16 *th, u8 len) { u32 addr = CALIB_FLAG_ADDR; FLASH_Unlock(); FLASH_ErasePage(CALIB_FLAG_ADDR); // 擦除整页 // 先写标志:0xA55A 表示阈值有效 FLASH_ProgramHalfWord(addr, 0xA55A); addr += 2; for (u8 i = 0; i < len; i++) { FLASH_ProgramHalfWord(addr, th[i]); addr += 2; } FLASH_Lock(); }

0x0807F000位于 F103ZET6 用户 Flash 的最末段,放阈值不会跟代码区冲突,但要注意:程序本身编译后如果超过了这个地址,烧录时会把这段覆盖掉,导致校准数据被破坏。检查方法很简单,编译后看 Keil 的 Build Output,确认 Code 区的结束地址小于 0x0807F000。掉电保存逻辑可以延伸到行驶里程、最高速度记录等参数,作为一个低成本数据记录思路。

4. 超声波避障:输入捕获测距与状态机切换

4.1 HC-SR04 测距原理为什么选输入捕获

HC-SR04 的工作时序:Trig 引脚拉高 10us 以上触发测距,模块自动发射 8 个 40kHz 超声波脉冲,然后 Echo 引脚输出一个高电平脉冲,脉冲宽度等于超声波从发射到接收的往返时间。距离 = 高电平时间 × 声速 / 2,即每微秒对应 0.017cm。

测距实现有两种常见方案。轮询延时方案用while(GPIO_ReadPin()==SET)死等加delay_us()计时,代码简单但有两个致命问题:一是主循环被阻塞,测距期间电机 PWM 无法更新,小车会一卡一卡地走;二是delay_us()本身依赖 SysTick,与中断嵌套叠加时误差很大。这套工程用的是定时器输入捕获:TIM2_CH4 配置为捕获模式,自动记录 Echo 引脚上高电平的上升沿和下降沿时刻,两个时刻的差值就是脉冲宽度,全程 0 CPU 占用,测距的同时循迹逻辑照常运行。

4.2 TIM2 输入捕获初始化

Echo 引脚接在 PA3(TIM2_CH4),必须开启 GPB 复用时钟和 TIM2 时钟。输入捕获的滤波器参数值得单独说:HC-SR04 在复杂环境下 Echo 信号偶尔会有前沿抖动,不滤波时会捕获到错误起始时刻,测距值跳变几十厘米。

void TIM2_IC_Init(void) { GPIO_InitTypeDef gpio; TIM_ICInitTypeDef ic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin = GPIO_Pin_3; // PA3: TIM2_CH4 gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); ic.TIM_Channel = TIM_Channel_4; ic.TIM_ICPolarity = TIM_ICPolarity_Rising; // 先捕获上升沿 ic.TIM_ICSelection = TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler = TIM_ICPSC_DIV1; ic.TIM_ICFilter = 0x0F; // 8个时钟周期采样滤波 TIM_ICInit(TIM2, &ic); TIM_Cmd(TIM2, ENABLE); }

配置成上升沿捕获后,还要处理下降沿。标准库的输入捕获一次只能配置一种极性,所以工程里采用"捕获上升沿 -> 记录时刻 -> 重配为下降沿 -> 再捕获"的切换方式。重配极性用TIM_ICInit直接再调一次即可,不需要关闭定时器。这里有个细节:TIM_ICFilter = 0x0F意味着信号需要连续 8 个时钟周期保持同一电平才算有效,滤波太强会丢失超声波的短脉冲,太弱又会引入毛刺,0x0F 对 40kHz 超声波场景是合理的默认值。

4.3 往返时间读取与距离换算

完整的测距函数包含触发、捕获上升沿、捕获下降沿三部分。触发超声波之前,先清一次捕获标志,防止上一次残留的中断标志干扰本次测量:

float ultrasonic_get_cm(void) { u32 t1, t2; // 清标志,避免残留捕获值影响本次计算 TIM_ClearFlag(TIM2, TIM_FLAG_CC4); GPIO_SetBits(GPIOA, GPIO_Pin_2); // Trig 拉高 delay_us(15); // 至少10us,取15us留余量 GPIO_ResetBits(GPIOA, GPIO_Pin_2); // Trig 拉低 // 等待上升沿捕获 while (TIM_GetFlagStatus(TIM2, TIM_FLAG_CC4) == RESET); t1 = TIM_GetCapture4(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC4); // 切换为下降沿捕获 TIM_ICInit(TIM2, &falling_ic_init); while (TIM_GetFlagStatus(TIM2, TIM_FLAG_CC4) == RESET); t2 = TIM_GetCapture4(TIM2); TIM_ClearFlag(TIM2, TIM_FLAG_CC4); // 恢复上升沿模式 TIM_ICInit(TIM2, &rising_ic_init); // 溢出处理:超出40ms未捕获到下降沿,视为超量程 if ((t2 < t1) || (t2 - t1 > 40000)) return -1; return (float)(t2 - t1) * 0.017f; }

逻辑说明:TIM2 计数频率 72MHz 会溢出(65535 个计数约 0.91ms 翻一次),所以测距函数里不能直接用 32 位计数器的值算,而是必须在两次捕获之间切换极性并不断清标志。t2 - t1 > 40000对应高电平时间超过 40 毫秒,即距离超过 6.8 米,HC-SR04 量程只有 4 米左右,超限直接返回 -1 交给上层处理,避免小车把无效距离当成障碍物急停。

4.3 循迹与避障的状态机切换

避障的决策逻辑用状态机组织,比在 main 里堆 if-else 清晰得多。系统只有三个状态:正常循迹、避障转向、急停等待。状态之间的转换条件以距离阈值为准:

工况前向距离动作
正常行驶大于 30cm循迹优先
遇障预警20~30cm降速到 60%,继续循迹
强制避障小于 20cm停车,原地转向 90°
脱困恢复侧向距离大于 35cm回正,恢复循迹

状态切换的判定逻辑:超声波数据是异步更新的(每 50ms 触发一次测距),但状态机只在主循环里查询最新值,不在中断里切换——中断里只负责更新距离变量。这样避免了一个经典问题:Echo 捕获中断是否处理完成和状态切换无关,状态机永远读到的是上一次完整测距的结果。

typedef enum { STATE_FOLLOW_LINE, STATE_SLOW_DOWN, STATE_AVOID_TURN, STATE_RECOVER } CarState; CarState car_state = STATE_FOLLOW_LINE; float obstacle_dis = 100.0f; void state_machine_update(float dis) { switch (car_state) { case STATE_FOLLOW_LINE: if (dis < 20.0f) { car_state = STATE_AVOID_TURN; avoid_turn_start(1); // 传入方向参数,优先往左 set_base_speed(0); // 先停车 } else if (dis < 30.0f) { car_state = STATE_SLOW_DOWN; set_base_speed(BASE_SPEED * 0.6f); } break; case STATE_SLOW_DOWN: if (dis >= 35.0f) { car_state = STATE_FOLLOW_LINE; // 障碍物已移开 set_base_speed(BASE_SPEED); } else if (dis < 18.0f) { car_state = STATE_AVOID_TURN; } break; case STATE_AVOID_TURN: // 转向动作持续1.5秒后重新检测 if (avoid_turn_timeout()) { car_state = STATE_RECOVER; set_base_speed(BASE_SPEED); } break; case STATE_RECOVER: // 转向回正期间保持低速 if (dis > 25.0f) { car_state = STATE_FOLLOW_LINE; set_base_speed(BASE_SPEED); } break; } }

避障和循迹的仲裁逻辑在 main 循环里体现:超声波距离大于 30cm 时,电机控制函数只调用motor_control_by_line();进入避障状态后,电机控制切换到固定 PWM 差速旋转。这里要注意avoid_turn_start的方向参数要根据障碍物位置动态决定——常见做法是检测到障碍物后,先原地左转 90°,测距,左转后仍然小于 20cm 就说明左侧也不通,再回正右转 180°直接掉头。整个状态机设计的关键是不信任单次测距结果,连续两次距离都越界才切换状态,避免超声波传感器受到声波干扰时小车神经质一样急停。

5. Keil工程导入、car.axf与四个容易踩的坑

5.1 car.axf 文件在工程里扮演什么角色

下载资料解压后,car.axf是最容易引起困惑的文件。axf 是 ARM ELF 格式的可执行镜像,由 Keil 在编译链接后自动生成,它和 hex、bin 的区别在于:hex 是纯代码镜像用于烧录,axf 额外包含调试符号表和源码路径信息,所以它既是可执行文件也是调试文件。用 ST-LINK 在线调试时,Keil 实际加载的就是 axf;离线烧录用 hex 或 bin 更稳妥,也可以用 Keil 自带的 fromelf 工具把 axf 转成 hex:

fromelf --i32 --output=car.hex car.axf

拿到资料后建议这么做:先不要改代码,直接用 Keil 打开工程编译一遍,确认编辑环境和你本地的芯片包、编译器版本兼容,再把 car.axf 烧到板子上验证硬件是否正常。如果板子型号和原工程一致(STM32F103ZET6),axf 是可以直接跑的;如果换了型号,比如从 ZET6 换成 C8T6,axf 就不能用,必须重新编译。

5.2 环境准备:芯片包与ST-LINK

打开工程前先确认三件事:Keil 版本、设备芯片包、调试器驱动。工程基于标准外设库,MDK 4.7 以上或 MDK5 都能编译,但 MDK5 需要安装 STM32F1xx 的 Device Family Pack;ST-LINK 驱动注意区分旧版 ST-Link Utility 和新版 STM32CubeProgrammer,两套驱动不能同时混用,装完重启 Keil。给经常在 C51 和 STM32 两个平台间切换的人一句实在话:两个环境的芯片包互不干扰,不用卸了重装,MDK5 装完 STM32F1 支持包后打开 C51 工程会自动切换工具链。

5.3 error: no stm32 target found 排查流程

这个报错在插上 ST-LINK、点下载后弹窗出现,原因排在前四位的依次是:芯片供电异常、SWD 引脚被占用、ST-LINK 固件太老、芯片被锁死。小车项目里最容易犯的是电池组只给电机驱动供电,没给开发板供电,ST-LINK 的 3.3V 输出能力又不够,导致识别失败。排查顺序:先用万用表量板子 3.3V;再在 Keil 的 Debug 设置里把 ST-LINK 的 SW 模式波特率从 4MHz 降到 1MHz;如果 SWD 被重映射到了别的功能,按住复位键的同时点下载,等烧录进度条出现再松开复位。芯片锁死用 ST-LINK Utility 全片擦除即可解。

5.4 串口打印验证传感器是否正常

小车不上赛道时,肉眼看不到传感器状态,用串口打印是最快的验证手段。工程里 USART1 初始化在 usart.c 中,波特率 115200,重定向fputc后即可用printf打印日志:

// 主循环低速轮询打印 printf("dis=%5.1fcm adc[0..4]=%d %d %d %d %d state=%d\n", obstacle_dis, sensor_adc[0], sensor_adc[1], sensor_adc[2], sensor_adc[3], sensor_adc[4], car_state);

把车悬空用手在黑线和白地之间滑动,串口助手里 adc 值应该有明显跳变;超声波对着 30cm 处的手掌,距离值应该稳定在 29~31cm 之间。这个技巧在答辩现场特别实用:屏幕上实时滚动数据比口头解释"这块代码能测距"有说服力得多。

5.5 基于这套源码扩展的三个方向

如果要把成绩再往上推,可以在现有工程上做三个方向的扩展,每个都只需少量代码改动:一是给循迹模块加模糊控制或 BP 网络替代固定 PD,评分时"智能算法"是明显加分标签;二是预留的 USART1 接 ESP8266 或蓝牙模块,把车体姿态和距离数据推送到手机或云平台,注意热点里常提到的"stm32+8266 宿舍控制"类项目就是同一条技术路线;三是预留的 CAN 接口接第二块板子做主从编队,这是把单个小车提升到"多机协作"课题的最短路径。三个方向都不需要动当前的主控板和驱动板,原工程的引脚分配和代码组织足够支撑。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 9:18:33

滑动窗口+字符频次:高效破解字母异位词查找的经典算法

1. 从一道面试题说起&#xff1a;为什么“找字母异位词”值得认真对待 如果你刷过一段时间算法题&#xff0c;大概率会遇到这道经典的 Find All Anagrams in a String 。题目本身不长&#xff1a;给定两个字符串 s 和 p &#xff0c;在 s 中找到所有 p 的字母异位词的…

作者头像 李华
网站建设 2026/9/10 9:18:00

本地中小企业SEO实战指南:从地图标注到转化落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/10 9:16:38

基于S7-300和组态王的恒压供水系统设计与PID控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华