简介:本资源是一套基于STM32F103VET6主控芯片的智能小车完整嵌入式开发工程,面向嵌入式初学者、电子设计竞赛备赛学生及智能硬件开发者,解决多模态运动控制(避障/循迹/遥控)系统集成与调试难题。压缩包共163个文件,含42个.h头文件(定义外设驱动与功能接口)、38个.c源文件(实现超声波测距、蓝牙指令解析、PID循迹算法、TIM PWM电机调速等核心逻辑),以及.o、.d、.axf、.hex等编译中间与可执行文件,配套Keil工程(.uvprojx)、调试配置(.dbgconf)、Android遥控APK及一键清理脚本(keilkilll.bat),整体6.04MB,结构规范,便于工程复现与模块化学习。已有640人学习下载,读者可直接导入Keil uVision或STM32CubeIDE编译烧录,快速掌握传感器融合、无线通信协议栈应用、实时电机闭环控制等关键技术,是理解智能小车软硬协同设计的典型实践案例。
1. 这不是玩具遥控车:STM32F103VET6智能小车程序包,是能跑通PID调速、红外循迹、超声避障三合一的硬核工程快照
你手头那块带USB转串口、4个直流电机接口、左右红外对管+前方超声模块的蓝色PCB小车底板,很可能就是基于STM32F103VET6——这颗LQFP100封装、72MHz主频、512KB Flash、64KB RAM的Cortex-M3芯片,至今仍是高校电赛、毕业设计、嵌入式入门最扛造的“练手标兵”。而这个名为stm32f103vet6小车程序2.zip的压缩包,不是Demo代码,不是裸机点灯,它是一套已烧录验证过的完整固件工程:从GPIO初始化、SysTick精准延时、PWM输出驱动L298N、EXTI捕获红外反射信号,到TIM2编码器测速、TIM3定时触发超声测距、PID位置式算法闭环控制左右轮速差——所有.c/.h文件结构清晰,注释覆盖关键寄存器位(比如TIMx->CCMR1 |= TIM_CCMR1_OC1M_2 | TIM_CCMR1_OC1M_1为什么必须这样置位),连keil uVision5的.uvprojx工程配置都保留着标准CMSIS启动文件路径。适合刚焊完小车、对着原理图发懵的A同学;也适合想快速验证自己PID参数是否合理的某导师;更适合作为某高校嵌入式课程设计的“可运行基线”,省去从零搭外设驱动的三天调试时间。它不承诺AI视觉或WiFi图传,但保证——上电即走,改两行宏定义就能换传感器型号。
2. 工程解压与Keil环境复现:从压缩包到.hex烧录的六步闭环
2.1 解压结构解析:识别核心源码与硬件映射关系
解压stm32f103vet6小车程序2.zip后,你会看到标准的Keil工程目录树:
stm32f103vet6_car/ ├── CMSIS/ # 标准ARM Cortex-M3内核支持包(无需修改) ├── FWLIB/ # ST官方标准外设库v3.5.0(关键!版本错则编译报错) ├── USER/ │ ├── main.c # 主循环:红外循迹状态机 + 超声距离判断 + PID调速入口 │ ├── motor.c/h # L298N双H桥驱动:含PWM占空比设置、方向IO控制、刹车逻辑 │ ├── infrared.c/h # 左右红外对管采样:ADC通道配置(PA0/PA1)、阈值滤波、黑白线判据 │ ├── ultrasonic.c/h # HC-SR04驱动:PB0触发脉冲、PB1输入捕获高电平时间、距离换算 │ ├── pid.c/h # 位置式PID实现:含积分限幅、输出饱和处理、采样周期宏定义 │ └── sys.c/h # 系统级:SysTick初始化、delay_ms/us、NVIC分组设置 ├── OUTPUT/ # 编译输出目录(首次打开工程时为空) ├── STM32F103VE_FLASH.ini # Flash算法配置(指定VET6芯片Flash地址0x08000000~0x0807FFFF) └── stm32f103vet6.uvprojx # Keil v5工程文件(双击即可打开)提示:
FWLIB文件夹内必须是ST官方发布的STM32F10x_StdPeriph_Lib_V3.5.0,若用HAL库或LL库会因寄存器操作差异直接编译失败。该工程未使用任何第三方GUI或RTOS,纯裸机C语言,所有外设均通过标准库函数操作(如GPIO_Init()、TIM_TimeBaseInit())。
2.2 Keil uVision5环境配置四要点
在Keil中成功编译此工程,需确认以下四点(缺一不可):
2.2.1 Device与Pack选择
打开工程后,点击Project → Options for Target 'Target 1' → Device,确保:
Device下拉框选中STM32F103VE(注意是VE,非VC/VG);Use MicroLIB必须勾选(因工程中使用了printf重定向至串口,MicroLIB体积小且兼容性好);Pack选项卡中,CMSIS和Device两个Pack需显示为绿色对勾(若为灰色,点击Manage Run-Time Environment安装对应STM32F103系列Pack)。
2.2.2 Output与Debug配置
在Output选项卡:
Create HEX File必须勾选(生成.hex供ST-Link烧录);Browse Information勾选(便于后续调试查看变量);
在Debug选项卡:Use选择ST-Link Debugger(非ULINK或J-Link);Settings → SW Device中确认识别到STM32F103VE芯片(若显示Unknown Device,检查SWD接线:SWCLK-PB13、SWDIO-PB14、GND、3.3V)。
2.2.3 C/C++预处理器宏定义
在C/C++选项卡的Define栏,必须包含以下宏(决定外设使能与引脚映射):
USE_STDPERIPH_DRIVER,STM32F10X_MD_VL,ARM_MATH_CM3其中STM32F10X_MD_VL表示中密度VL系列(VET6属于此分类),若误写为MD(中密度)会导致Flash大小计算错误,链接时报regionFLASH' overflowed`。
2.2.4 Flash下载算法绑定
在Utilities → Settings → Flash Download中:
- 点击
Add按钮,从FWLIB/Project/STM32F10x_StdPeriph_Examples/FLASH/Flash_Program/路径下选择STM32F103VE_FLASH.ini; - 确保
Reset and Run勾选(烧录后自动复位运行)。
完成上述配置后,点击Build(F7),应看到0 Error(s), 0 Warning(s)。若出现undefined reference to 'SystemInit',说明startup_stm32f10x_md_vl.s未被正确包含进工程——右键Source Group 1 → Add Existing Files to Group,添加该启动文件(位于CMSIS/Startup/目录下)。
2.3 串口重定向与调试信息抓取
工程中printf("Speed_L: %d, Speed_R: %d\r\n", speed_l, speed_r);被重定向至USART1(PA9/PA10),用于实时监控左右轮速。要看到该输出:
- 将小车底板的
USART1_TX(PA9)接入USB转TTL模块的RX; - 在Keil中点击
View → Serial Windows → UART#1,设置波特率115200; - 烧录后复位,窗口将滚动显示类似:
Speed_L: 182, Speed_R: 179 Speed_L: 183, Speed_R: 180 ...参数说明:
speed_l/speed_r是PID输出经motor_set_speed()转换后的PWM占空比值(0~1000),数值越接近说明循迹偏差越小。若发现两轮速度持续相差>15,需检查红外传感器阈值或PID比例系数Kp。
3. 核心功能模块拆解:循迹、避障、调速的寄存器级实现逻辑
3.1 红外循迹:ADC采样+状态机判定黑线位置
小车采用左右双红外对管(TCRT5000)检测地面黑白线,其输出为模拟电压(白=高电平≈3.3V,黑=低电平≈0.2V)。工程中通过ADC1通道0(PA0)和通道1(PA1)同步采样:
// infrared.c 关键代码段 void infrared_init(void) { ADC_InitTypeDef ADC_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式 GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式(同时采PA0/PA1) ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_Init(ADC1, &ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // PA0 第1序列 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); // PA1 第2序列 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } uint16_t infrared_read_left(void) { return ADC_GetConversionValue(ADC1, 0); } // PA0 uint16_t infrared_read_right(void) { return ADC_GetConversionValue(ADC1, 1); } // PA1逻辑说明:ADC采样值范围0~4095(12位精度)。工程中设定阈值
INFRARED_THRESHOLD = 2000(白线典型值2800+,黑线典型值800-),infrared_get_state()函数根据左右值组合返回5种状态:
INFRARED_LEFT_ONLY(左黑右白)→ 小车偏右,需左轮减速/右轮加速;INFRARED_RIGHT_ONLY(左白右黑)→ 小车偏左,需左轮加速/右轮减速;INFRARED_BOTH_BLACK(双黑)→ 到达终点或十字路口;INFRARED_BOTH_WHITE(双白)→ 脱线,执行“原地旋转找线”策略;INFRARED_NORMAL(一黑一白)→ 直行微调。
3.2 超声避障:输入捕获测距与距离分级响应
HC-SR04模块通过PB0输出10μs高电平触发信号,PB1捕获Echo引脚高电平持续时间。工程使用TIM2的输入捕获功能(IC2通道)精确计时:
// ultrasonic.c 关键代码段 void ultrasonic_init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // TRIG GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; // ECHO GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入 GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 自动重装载值(65535) TIM_TimeBaseStructure.TIM_Prescaler = 72-1; // 72MHz/72 = 1MHz计数频率(1us精度) TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; // PB1 = CH2 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿触发 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter = 0x0; TIM_ICInit(TIM2, &TIM_ICInitStructure); TIM_Cmd(TIM2, ENABLE); TIM_ITConfig(TIM2, TIM_IT_CC2, ENABLE); // 开启CH2捕获中断 } // 在TIM2_IRQHandler中处理捕获事件 void TIM2_IRQHandler(void) { static uint16_t rising_time = 0, falling_time = 0; static uint8_t state = 0; // 0=等待上升沿, 1=等待下降沿 if (TIM_GetITStatus(TIM2, TIM_IT_CC2) != RESET) { if (state == 0) { rising_time = TIM_GetCapture2(TIM2); // 记录上升沿时刻 TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; TIM_ICInit(TIM2, &TIM_ICInitStructure); state = 1; } else { falling_time = TIM_GetCapture2(TIM2); // 记录下降沿时刻 uint32_t high_time_us = (falling_time > rising_time) ? (falling_time - rising_time) : (0xFFFF - rising_time + falling_time); distance_cm = high_time_us / 58; // 声速340m/s → 34000cm/1000000us ≈ 1cm/58us state = 0; TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; TIM_ICInit(TIM2, &TIM_ICInitStructure); } TIM_ClearITPendingBit(TIM2, TIM_IT_CC2); } }参数说明:
distance_cm为计算出的实际距离(单位cm)。工程中定义三级响应:
DISTANCE_NEAR < 15cm:立即刹车(motor_stop());15cm ≤ DISTANCE_MID < 30cm:降低速度至50%并尝试转向;DISTANCE_FAR ≥ 30cm:正常循迹。
若实测距离跳变大(如20cm→5cm→35cm),需检查Echo信号是否受干扰——常见做法是在ultrasonic_trigger()函数中增加10ms延时后再读取,避开触发脉冲噪声。
3.3 PID调速:位置式算法与电机PWM输出映射
小车左右轮速由TIM3_CH1(PA6)、TIM3_CH2(PA7)输出PWM控制。pid.c实现位置式PID,核心公式:output = Kp * error + Ki * ∑error + Kd * (error - error_last)
// pid.c 关键代码段 typedef struct { float Kp, Ki, Kd; float integral, last_error; float output_max, output_min; } PID_TypeDef; PID_TypeDef pid_left = {1.2, 0.05, 0.3, 0, 0, 0, 0}; // 左轮PID参数 PID_TypeDef pid_right = {1.2, 0.05, 0.3, 0, 0, 0, 0}; // 右轮PID参数 float pid_calculate(PID_TypeDef* pid, float setpoint, float feedback) { float error = setpoint - feedback; pid->integral += error; // 积分限幅防饱和 if (pid->integral > pid->output_max) pid->integral = pid->output_max; if (pid->integral < pid->output_min) pid->integral = pid->output_min; float derivative = error - pid->last_error; float output = pid->Kp * error + pid->Ki * pid->integral + pid->Kd * derivative; // 输出限幅 if (output > pid->output_max) output = pid->output_max; if (output < pid->output_min) output = pid->output_min; pid->last_error = error; return output; } // 在main.c主循环中调用 void motor_pid_control(void) { static uint16_t target_speed = 300; // 目标PWM占空比(0~1000) int16_t speed_l = encoder_read_left(); // 编码器读取实际转速(需自行实现) int16_t speed_r = encoder_read_right(); float out_l = pid_calculate(&pid_left, target_speed, speed_l); float out_r = pid_calculate(&pid_right, target_speed, speed_r); motor_set_speed(MOTOR_LEFT, (int16_t)out_l); motor_set_speed(MOTOR_RIGHT, (int16_t)out_r); }逻辑说明:
target_speed是期望的PWM值(非物理转速),encoder_read_xxx()需用户自行实现编码器计数(通常用TIM4编码器接口)。若无编码器,则用infrared_get_state()输出的状态间接估算偏差,此时feedback可设为0,setpoint改为状态权重值(如INFRARED_LEFT_ONLY时setpoint=200强制右轮加速)。PID参数Kp/Ki/Kd存储在pid.c顶部,调整原则:先调Kp使响应快速但不过冲,再加Kd抑制振荡,最后用Ki消除静差。
4. 避坑指南:六个真实翻车现场与血泪修复方案
4.1 现象:编译报错Error: L6218E: Undefined symbol SystemInit
原因:Keil工程未包含startup_stm32f10x_md_vl.s启动文件,或该文件中SystemInit函数被注释/删除。标准库依赖此函数初始化时钟系统(HSI/PLL/HSE)。
解决:
- 确认
CMSIS/Startup/目录下存在startup_stm32f10x_md_vl.s; - 右键Keil工程 →
Add Existing Files to Group→ 选中该文件; - 检查文件内第123行是否为
IMPORT SystemInit(非IMPORT SystemInit_); - 若仍报错,在
main.c顶部手动添加声明:extern void SystemInit(void);。
4.2 现象:小车上电后原地打转,红外读数全为0或全为4095
原因:红外传感器供电异常(VCC未接3.3V或GND虚焊),或ADC参考电压未启用(ADC_TripleModeCmd(ADC1, DISABLE)被误开启)。
解决:
- 用万用表量测TCRT5000 VCC引脚电压是否为3.3V;
- 检查
infrared_init()中是否遗漏ADC_Cmd(ADC1, ENABLE); - 在
ADC_Init()后添加强制校准代码:ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1));
4.3 现象:超声模块Echo无响应,distance_cm恒为0
原因:HC-SR04触发脉冲宽度不足10μs,或PB1引脚未配置为浮空输入(被上拉/下拉干扰)。
解决:
- 修改
ultrasonic_trigger()函数,用GPIO_SetBits(GPIOB, GPIO_Pin_0)后立即delay_us(15),再GPIO_ResetBits(GPIOB, GPIO_Pin_0); - 确认
GPIO_Init()中PB1的GPIO_Mode为GPIO_Mode_IN_FLOATING(非GPIO_Mode_IPU); - 示波器抓PB0波形,确保高电平宽度≥10μs。
4.4 现象:PID调速后左右轮速严重不一致,小车画圆
原因:TIM3 PWM通道极性配置错误(TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High应为Low),导致占空比与实际输出反相。
解决:
- 查
motor.c中motor_pwm_init()函数; - 定位
TIM_OCInitStructure结构体,将TIM_OCPolarity字段统一改为TIM_OCPolarity_Low; - 同时检查
motor_set_speed()中TIM_SetCompare1()与TIM_SetCompare2()是否分别对应左/右轮(PA6/PA7)。
4.5 现象:烧录后小车不动,但串口有打印(如Speed_L: 0, Speed_R: 0)
原因:L298N驱动芯片未供电(VM引脚需接7~12V,非3.3V),或电机接口接反(IN1/IN2与OUT1/OUT2交叉)。
解决:
- 用万用表量测L298N VM引脚电压,必须≥7V;
- 断电状态下,用导线短接L298N的
ENA与VM,再短接IN1与5V、IN2与GND,观察电机是否正转; - 若反转,则交换
OUT1/OUT2接线;若不转,检查ENA是否悬空(需接高电平使能)。
5. 进阶技巧:三步验证法与参数调优工作流
5.1 验证硬件链路:用示波器抓四个关键波形
不要依赖“代码没报错就等于硬件OK”——这是新手最大幻觉。我每次拿到新小车板,必做以下四点波形验证(用普通20MHz示波器即可):
| 测试点 | 预期波形 | 异常表现 | 排查方向 |
|---|---|---|---|
| PA6 (TIM3_CH1) | 1kHz方波,占空比随motor_set_speed()变化 | 无波形/频率错误 | 检查RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE)是否执行 |
| PB0 (TRIG) | 10μs高电平脉冲,间隔≥60ms | 脉宽<5μs或无脉冲 | delay_us()精度不足,改用__nop()循环 |
| PB1 (ECHO) | 高电平宽度=2×距离(cm)×58μs | 恒定高电平/无响应 | HC-SR04损坏或供电不足 |
| PA0 (IR_LEFT) | 白线2800~4095,黑线0~1000 | 全0或全4095 | 传感器VCC/GND虚焊或ADC通道未使能 |
操作细节:抓PA6波形时,将示波器探头接地夹接GND,探针接PA6;触发模式设为
Normal,时基调至1ms/div。若看到波形但占空比不随motor_set_speed(500)改变,说明TIM_SetCompare1(TIM3, 500)未生效——检查TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable)是否开启。
5.2 PID参数调优:从“暴力试凑”到“Ziegler-Nichols经验法”
直接改Kp/Ki/Kd像蒙眼射箭。我习惯用三步法收敛参数:
步骤1:仅启用P控制,找到临界振荡点
- 将
Ki=Kd=0,Kp从0.1开始,每次×2增大(0.1→0.2→0.4→0.8…); - 当小车在直线上出现等幅振荡(左右摇摆幅度不变)时,记录此时
Kp_critical(如Kp=2.4)和振荡周期T_critical(用示波器测PA6波形周期,如T=1.2s); - 此时
Kp = 0.6 × Kp_critical(即1.44),Ki=0,Kd=0,小车应能稳定循迹但有静差。
步骤2:加入I控制消除静差
- 保持
Kp=1.44,Kd=0,Ki从0.01开始,每次+0.01; - 当小车从脱线恢复后不再在黑线边缘反复横跳,且直行时左右速度差<5,即为合适
Ki(如Ki=0.07)。
步骤3:用D控制抑制超调
- 固定
Kp=1.44,Ki=0.07,Kd从0.1开始,每次+0.1; - 当小车过弯时不再“冲出赛道”,且直线段速度波动<3%,即为合适
Kd(如Kd=0.5)。
血泪经验:若调参后小车反应迟钝,优先降
Kp而非升Kd;若频繁抖动,立刻降Kd并检查机械结构(轮胎打滑、轴向间隙过大)。
5.3 红外阈值自适应:应对不同光照环境
固定阈值2000在阴天可用,强光下会失效。我在infrared.c中加了一段自适应逻辑:
// 在main.c循环中每5秒执行一次 void infrared_auto_threshold(void) { static uint16_t white_max = 0, black_min = 4095; static uint8_t sample_count = 0; uint16_t left = infrared_read_left(); uint16_t right = infrared_read_right(); if (left > white_max) white_max = left; if (right > white_max) white_max = right; if (left < black_min) black_min = left; if (right < black_min) black_min = right; sample_count++; if (sample_count >= 50) { // 采样50次 INFRARED_THRESHOLD = (white_max + black_min) / 2; sample_count = 0; white_max = 0; black_min = 4095; } }效果:小车静止时按住任意按键3秒,自动采集当前环境下的白/黑基准值,后续循迹全程使用动态阈值。从那以后我每次调试新场地,都强制走一遍这个自适应流程——哪怕多花5秒,也比赛后因光线变化脱线强十倍。希望帮到你。
本文还有配套的精品资源,点击获取