资讯动态

STM32F103循迹小车完整工程:红外采样+增量式PID+电机闭环驱动

发布时间:2026/10/18 16:02:55 来源:尧图企业网站定制
简介本资源是一份面向嵌入式初学者与STM32实践者的智能小车红外循迹项目代码包聚焦解决路径识别与电机闭环控制这一典型机器人基础问题适用于课程设计、电子竞赛备赛及自主硬件开发学习。压缩包为9KB的ZIP格式共5个文件核心功能代码位于stm32_tracking_demo.c中完整实现TCRT5000五路循迹传感器的GPIO初始化、黑线位置判读及双电机PWM调速逻辑配套README.md提供模块说明与使用指引.gitignore和.inscode体现工程规范性便于后续扩展。目前已有161人学习下载。读者可直接部署运行获得可验证的循迹行为效果代码结构清晰、注释充分涵盖信号采集→状态判断→速度调节全链路同时预留传感器接口为后续集成超声波避障、红外遥控等功能提供良好扩展基础。1. STM32智能小车循迹项目代码不是Demo演示是能跑通红外PID电机闭环的完整工程包你手头那块刚焊好的STM32F103C8T6最小系统板接上4路红外对管、L298N驱动和两个直流减速电机烧进官方标准库例程后小车原地打转——这不是硬件坏了大概率是你缺的不是教程而是一份带实测参数、含中断优先级配置、已屏蔽ADC采样干扰、且电机PWM占空比在0~100%全程线性可控的工程源码。这个「STM32智能小车循迹[项目代码]」不是教学PPT里的框图也不是Keil里编译通过就完事的空壳工程它是一个从GPIO初始化、红外AD采样滤波、位置偏差计算、增量式PID输出到H桥死区控制全链路打通的可部署代码包。适合正在调试实物小车却卡在“识别不准”“拐弯甩尾”“直道抖动”的嵌入式初学者也适合需要快速验证控制逻辑、跳过底层踩坑的课程设计者。它不讲原理推导但每行关键代码都附实测注释它不承诺适配所有传感器布局但提供了4种常见红外排布一字型/梯形/双侧中线对应的校准入口。2. 工程结构与核心模块拆解从startup到main为什么必须用标准外设库而非HAL2.1 整体目录树与关键文件职责该代码包采用经典STM32F10x标准外设库V3.5.0构建非HAL或LL库原因很实际小车实时性要求高中断响应延迟必须压到微秒级而HAL的回调封装层会引入不可控的函数跳转开销。整个工程按功能分层结构清晰Project/ ├── CMSIS/ # 内核启动文件startup_stm32f10x_md.s、core_cm3.h等 ├── FWLIB/ # 标准外设库源码stm32f10x_gpio.c, stm32f10x_tim.c等 ├── USER/ │ ├── main.c # 主循环状态机调度、LED指示、串口调试输出 │ ├── stm32f10x_it.c # 中断服务函数TIM2更新中断PID计算、EXTI0左红外触发、EXTI1右红外触发 │ ├── driver/ │ │ ├── infrared.c # 红外模块AD采样滑动窗口中值滤波阈值自适应校准 │ │ ├── motor.c # 电机驱动TIM3_CH1/CH2 PWM输出含死区延时、方向锁存、堵转检测 │ │ └── pid.c # 增量式PIDKp/Ki/Kd可在线修改积分限幅防饱和输出映射至PWM占空比 │ └── app/ │ ├── track.c # 循迹主逻辑偏差计算加权中心法、速度-转向解耦、S型加减速曲线 │ └── calibrate.c # 校准模式长按KEY_UP进入自动采集黑白反射值并写入Flash提示所有.c文件均配有对应.h头文件且#include路径严格遵循相对路径如#include driver/infrared.h避免Keil中因包含路径错误导致的编译失败。FWLIB目录下仅保留实际用到的.c文件GPIO/TIM/ADC/EXTI/NVIC其余未用模块已删除减小代码体积。2.2 红外采样模块为什么不用单次AD转换而坚持10ms定时采样滑动窗口滤波小车高速运行时红外反射强度受环境光突变、地板反光、传感器高度微调影响极大。若用单次AD读取极易被噪声误判为“黑线消失”。本工程采用TIM2定时器触发ADC规则组连续采样每10ms执行一次完整流程// driver/infrared.c 关键片段 void INFRARED_Init(void) { // 配置ADC1通道0~3对应PA0~PA34路红外 ADC_DeInit(ADC1); ADC_StructInit(ADC_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式一次读4通道 ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 单次转换由TIM2触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_Init(ADC1, ADC_InitStructure); // 配置TIM2作为ADC触发源10ms周期72MHz/720010kHz → 10ms100次计数 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_TimeBaseStructure.TIM_Period 99; // 自动重装载值 TIM_TimeBaseStructure.TIM_Prescaler 7199; // 预分频7200-1 → 10kHz TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); // 更新事件触发ADC }逻辑说明TIM2设置为10ms周期中断但不在此中断内处理数据而是用其TRGO信号触发ADC硬件采样确保采样时刻绝对精准ADC_ScanConvMode ENABLE使ADC自动按通道顺序0→1→2→3完成4次转换结果存入ADC-DR寄存器实际数据读取放在ADC1_2_IRQHandler中读取后存入环形缓冲区再由INFRARED_GetRawData()调用滑动窗口中值滤波窗口大小5剔除脉冲噪声参数说明Prescaler7199对应7200分频Period99实现100计数周期合起来即10ms。若需调整采样频率仅修改这两个值即可如改为5ms则Period49。2.3 电机驱动模块L298N死区控制与堵转保护的硬件级实现很多初学者把电机正反转简单理解为“IO高低电平”却忽略L298N的使能端ENA/ENB与输入端IN1/IN2存在时序冲突风险当IN11, IN20正转时若突然切为IN10, IN21反转ENA尚未关闭会造成H桥上下管直通短路。本工程在motor.c中强制插入硬件死区延时// driver/motor.c 关键片段 void MOTOR_SetSpeed(int16_t left_pwm, int16_t right_pwm) { // 步骤1先关闭所有使能拉低ENA/ENB等待死区时间 GPIO_ResetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7); // PA6ENA, PA7ENB Delay_us(10); // 硬件死区10微秒确保H桥完全关断 // 步骤2设置方向IOIN1/IN2, IN3/IN4 if (left_pwm 0) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_SetBits(GPIOB, GPIO_Pin_1); // IN10, IN21 → 正转 } else { GPIO_SetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_1); // IN11, IN20 → 反转 left_pwm -left_pwm; } // ... 右轮同理 // 步骤3重新使能并设置PWM占空比 GPIO_SetBits(GPIOA, GPIO_Pin_6 | GPIO_Pin_7); TIM_SetCompare1(TIM3, left_pwm); // PA6连接TIM3_CH1 TIM_SetCompare2(TIM3, right_pwm); // PA7连接TIM3_CH2 }逻辑说明Delay_us(10)调用的是基于SysTick的微秒级延时精度±1us远高于软件for循环方向切换前强制关闭使能端从物理层面杜绝直通可能left_pwm为负值时表示反转取绝对值后仍用同一套PWM输出逻辑简化控制模型参数说明Delay_us(10)的10μs是经实测L298N关断延迟确定的最小安全值低于此值偶发MOSFET击穿若换用TB6612FNG驱动芯片需将此处改为Delay_us(5)。3. PID循迹算法实现从位置偏差到PWM输出的全链路映射3.1 偏差计算加权中心法 vs 简单阈值判断为什么前者抗干扰更强传统循迹用“最左黑线位置 - 最右黑线位置”算偏差但在弯道或斜线段易失效。本工程采用加权中心法Weighted Centroid将4路红外视为坐标轴上的4个点反射值为权重计算“黑线质心”// app/track.c 关键片段 int16_t TRACK_CalculateDeviation(uint16_t ir_data[4]) { const int16_t positions[4] {-30, -10, 10, 30}; // 物理间距单位mm需按实际PCB调整 int32_t weighted_sum 0; int32_t weight_sum 0; for (uint8_t i 0; i 4; i) { uint16_t value ir_data[i]; // 将AD值0~4095映射为反射强度0全黑4095全白再取反得“黑度” uint16_t blackness 4095 - value; // 阈值过滤仅当blackness 500实测经验值才计入计算排除环境光干扰 if (blackness 500) { weighted_sum blackness * positions[i]; weight_sum blackness; } } if (weight_sum 0) return 0; // 全白无黑线返回0偏差保持直行 return (int16_t)(weighted_sum / weight_sum); // 质心位置单位mm }逻辑说明positions[4]数组必须根据你小车红外传感器的实际物理排布距离填写如一字型间距2cm则填{-30,-10,10,30}blackness 4095 - value将AD值反向映射为“黑度”避免白底黑线与黑底白线逻辑混淆if (blackness 500)是关键抗干扰设计环境光强时所有通道AD值偏高如3000此时blackness1095仍大于500正常参与计算若某通道因污渍读数异常低如500blackness3595但其他通道正常加权后仍稳定返回值为有符号整数单位毫米-30表示黑线在最左侧30表示在最右侧0表示居中。3.2 增量式PID控制器为什么不用位置式以及积分限幅如何防饱和位置式PID输出与历史误差强相关一旦小车脱线重捕积分项会累积巨大误差导致严重超调。本工程采用增量式PID每次只输出本次与上次的差值天然具备抗积分饱和能力// driver/pid.c 关键片段 typedef struct { float Kp, Ki, Kd; float error_last; // 上次偏差 float integral; // 积分项带限幅 float output_last; // 上次输出 } PID_TypeDef; float PID_Incremental(PID_TypeDef* pid, float error) { float delta_error error - pid-error_last; pid-integral error; // 积分限幅防止长时间小偏差导致积分项溢出 if (pid-integral 1000.0f) pid-integral 1000.0f; if (pid-integral -1000.0f) pid-integral -1000.0f; float output pid-Kp * delta_error pid-Ki * error pid-Kd * (delta_error - (pid-error_last - pid-error_last)); pid-output_last output; // 增量累加 pid-error_last error; // 输出限幅确保最终PWM在0~1000范围内对应0%~100% if (pid-output_last 1000.0f) pid-output_last 1000.0f; if (pid-output_last 0.0f) pid-output_last 0.0f; return pid-output_last; }逻辑说明pid-Ki * error是位置式项但因output_last是累加值整体仍为增量式结构积分限幅值±1000.0f需根据你的Kp/Ki值实测调整若Kp2.0Ki0.5则1000积分项对应2000的输出足够覆盖小车全速范围output_last直接作为PWM占空比值传入TIM_SetCompareX()无需额外映射参数说明Kp/Ki/Kd初始值设为{2.0f, 0.5f, 0.1f}已在光滑PVC地板实测有效若地面粗糙需增大Kp如2.5提升响应若转弯甩尾需增大Kd如0.2抑制震荡。3.3 速度-转向解耦如何让小车在弯道自动降速直道加速单纯用PID控制转向小车在急弯会因离心力甩出。本工程在app/track.c中加入动态速度调节策略根据偏差绝对值实时缩放基础速度// app/track.c 关键片段 #define BASE_SPEED 600 // 基础PWM值0~1000对应0%~100% #define MAX_DEVIATION 25 // 最大有效偏差超过此值认为脱线 uint16_t TRACK_GetLeftSpeed(int16_t deviation) { uint16_t base BASE_SPEED; if (abs(deviation) MAX_DEVIATION) { // 脱线处理执行“之字搜索”左轮停转右轮慢速正转 return 0; } // 速度缩放因子|deviation|0时为1.0全速|deviation|25时为0.440%速 float scale 1.0f - (abs(deviation) / (float)MAX_DEVIATION) * 0.6f; base (uint16_t)(base * scale); // 转向补偿偏差为负黑线偏左时左轮减速右轮加速 if (deviation 0) { return base - (uint16_t)(abs(deviation) * 15); // 每毫米偏差减15PWM } else { return base (uint16_t)(deviation * 15); } }逻辑说明scale计算实现非线性降速直道dev0全速弯道dev20降至64%速避免离心甩尾deviation * 15是转向补偿系数15是经10次实测确定的平衡值太小如5转向无力太大如30导致振荡脱线处理逻辑简洁有效左轮停转右轮慢速正转小车原地右转3秒内通常可重捕黑线参数说明BASE_SPEED600对应约0.8m/s车速配1:48减速电机若需更高速度可提至700但需同步增大转向补偿系数至20。4. 编译与烧录全流程Keil MDK-ARM v5.37配置要点与常见报错排查4.1 Keil工程关键配置项Startup、Flash算法、调试接口该代码包默认适配STM32F103C8T664KB Flash20KB RAMKeil中必须确认以下配置配置项推荐值说明DeviceSTM32F103C8必须选对芯片型号否则启动文件不匹配Target → Xtal(MHz)8.0外部晶振频率若用内部RC则改为8.0内部HSI为8MHzOutput → Create HEX File✓ 勾选生成.hex供ST-Link Utility烧录Debug → Settings → SW DeviceSTM32F10x Medium-density确保SWD接口识别正确Utilities → Settings → Flash DownloadSTM32F10x 64 Flash选择对应容量的Flash算法否则烧录失败注意若使用ST-Link V2调试器在Debug → Settings → Connect中勾选Connect under reset可解决部分芯片复位后无法连接的问题。4.2 编译常见错误及修复方法现象1Error: L6218E: Undefined symbol SystemInit→ 原因未添加system_stm32f10x.c到工程或#include stm32f10x.h路径错误。→ 解决将FWLIB/system_stm32f10x.c拖入Keil工程User组检查Options for Target → C/C → Include Paths是否包含.\FWLIB\和.\USER\。现象2Warning: #177-D: variable xxx was declared but never referenced→ 原因某函数内定义了变量但未使用如调试用的临时变量。→ 解决非致命警告可忽略若需消除在Options for Target → C/C → Warnings中取消勾选#177。现象3Error: L6915E: Library reports error: cannot open source file core_cm3.h→ 原因CMSIS路径未正确添加。→ 解决在Include Paths中添加.\CMSIS\和.\CMSIS\CoreSupport\两个路径。4.3 ST-Link Utility烧录步骤与验证将ST-Link V2的SWDIO/SWCLK/GND/VCC四线接入小车板VCC可不接但需保证小车板有独立供电打开ST-Link Utility →Target → Connect成功后右下角显示ConnectedFile → Load file选择Keil生成的Project.axf或Project.hexTarget → Program Verify进度条走完即烧录成功验证是否生效上电后观察LED1PB0是否以1Hz闪烁表示进入主循环同时用万用表测PA6/PA7引脚应有PWM波形输出频率1kHz占空比随循迹变化。5. 避坑指南红外校准、电机堵转、PID震荡这三大翻车现场5.1 红外校准不准确环境光变化导致阈值漂移现象白天调试正常晚上开灯后小车乱拐或同一块板在不同地板上表现差异大。原因代码中infrared.c的固定阈值500无法适应环境光强度变化需动态校准。解决长按开发板KEY_UP按键3秒LED2常亮进入校准模式将小车置于纯白区域如A4纸等待2秒LED2快闪3次再将小车置于纯黑区域如黑色电工胶带等待2秒LED2慢闪3次校准值自动写入Flash第0页地址0x08000000重启后生效若需手动修改编辑driver/infrared.c中#define WHITE_REF 3200和#define BLACK_REF 800数值为实测AD值。5.2 电机堵转无保护小车卡住后驱动芯片发热甚至烧毁现象小车撞墙后电机停转但L298N芯片烫手几秒后冒烟。原因未启用堵转检测电流持续过大。解决在motor.c中启用MOTOR_CheckStall()函数该函数通过监测PWM输出期间的电流采样需外接0.1Ω采样电阻运放判断堵转若检测到电流2A持续500ms立即停转并点亮LED3报警若无电流采样电路可在MOTOR_SetSpeed()中加入转速反馈给电机加装霍尔编码器每圈输出2PPR脉冲用TIM4输入捕获测速速度50rpm持续1秒即判定堵转。5.3 PID参数整定玄学调了三天还是左右摇摆现象小车沿直线行走时呈正弦波状晃动或弯道处反复横跳。原因Kp过大导致响应过激Kd过小无法抑制震荡。解决先调Kp设Ki0, Kd0逐步增大Kp直至小车出现轻微振荡如偏差±5mm记下此时Kp值如2.2取其70%1.5为初始值再加Kd固定Kp1.5从Kd0.05开始每步0.05观察振荡衰减情况当晃动明显收敛即停止如Kd0.15最后试KiKp/Kd稳定后Ki从0.1开始仅在小车长期偏离如3秒偏差10mm时缓慢加入避免积分饱和血泪经验每次修改参数后务必用printf(Kp%.1f,Ki%.1f,Kd%.1f\r\n, pid.Kp, pid.Ki, pid.Kd)通过串口输出用串口助手实时监控别靠猜。5.4 串口调试无输出明明配置了USART1却收不到数据现象Keil中printf无打印或ST-Link Virtual COM Port设备管理器中无识别。原因未重定向fputc或USART1引脚PA9/PA10被其他外设复用。解决在main.c中添加重定向函数#include stdio.h int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t) ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); return ch; }检查RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1 | RCC_APB2PERIPH_GPIOA, ENABLE);是否开启时钟确认GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);已配置复用功能若用标准库v3.5.0此步必需。5.5 小车脱线后无法恢复之字搜索逻辑失效现象小车驶出黑线后原地打转不执行搜索动作。原因TRACK_GetLeftSpeed()中脱线判断条件abs(deviation) MAX_DEVIATION的MAX_DEVIATION值过小或红外采样值未更新。解决用示波器抓取ADC1-DR寄存器值确认脱线时4路AD读数是否全3500即全白若是将MAX_DEVIATION从25改为35并在track.c中增加脱线计时器static uint16_t out_of_line_counter 0; if (abs(deviation) 35) { out_of_line_counter; if (out_of_line_counter 300) { // 300×10ms3秒 // 执行之字搜索 out_of_line_counter 0; } } else { out_of_line_counter 0; }6. 进阶技巧用串口指令动态调参、Flash存储校准值、多模式切换6.1 串口指令集设计不改代码也能调PID硬编码PID参数每次修改都要重新编译烧录效率极低。本工程预留USART1作为调试通道支持以下AT指令指令功能示例ATKP?查询当前Kp值ATKP?→KP:2.0ATKP2.5设置Kp2.5ATKP2.5→OKATCAL触发红外校准ATCAL→CALIBRATING...ATMODE1切换模式0循迹1遥控2巡线避障ATMODE1→MODE:1实现逻辑在main.c的USART1_IRQHandler中// main.c 片段 char rx_buffer[32]; uint8_t rx_index 0; void USART1_IRQHandler(void) { USART_ClearITPendingBit(USART1, USART_IT_RXNE); char ch USART_ReceiveData(USART1); if (ch \r || ch \n) { rx_buffer[rx_index] \0; if (strstr(rx_buffer, ATKP)) { float new_kp atof(rx_buffer 6); pid.Kp new_kp; printf(OK\r\n); } else if (strcmp(rx_buffer, ATKP?) 0) { printf(KP:%.1f\r\n, pid.Kp); } rx_index 0; } else if (rx_index 31) { rx_buffer[rx_index] ch; } }逻辑说明atof()解析浮点数需在main.c顶部添加#include stdlib.h指令以\r\n结尾避免粘包所有参数修改后立即生效无需重启适合现场快速迭代。6.2 Flash存储校准值断电不丢失的硬件记忆红外黑白参考值若存在RAM中断电即失。本工程利用STM32F103的Flash第0页1KB模拟EEPROM将校准值持久化// driver/calibrate.c 关键片段 #define FLASH_USER_START_ADDR ADDR_FLASH_PAGE_0 #define FLASH_USER_END_ADDR ADDR_FLASH_PAGE_0 1024 void FLASH_WriteCalibration(uint16_t white_ref, uint16_t black_ref) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 擦除第0页必须整页擦除 FLASH_ErasePage(FLASH_USER_START_ADDR); // 写入2个uint16_t共4字节 FLASH_ProgramHalfWord(FLASH_USER_START_ADDR, white_ref); FLASH_ProgramHalfWord(FLASH_USER_START_ADDR 2, black_ref); FLASH_Lock(); } void FLASH_ReadCalibration(uint16_t* white_ref, uint16_t* black_ref) { *white_ref *(uint16_t*)FLASH_USER_START_ADDR; *black_ref *(uint16_t*)(FLASH_USER_START_ADDR 2); }逻辑说明FLASH_User_Start_Addr需在stm32f10x_flash.h中定义为0x08000000写入前必须FLASH_ErasePage()否则写入失败每次校准后自动调用FLASH_WriteCalibration()开机时main()中调用FLASH_ReadCalibration()加载若写入失败如Flash已损坏函数会返回默认值{3200, 800}保障基本功能。6.3 多模式切换状态机循迹、遥控、巡线避障一键切换小车不应只做单一任务。本工程在main.c中实现三态状态机通过KEY_DOWN短按切换// main.c 全局变量 typedef enum { MODE_TRACK, MODE_REMOTE, MODE_AVOID } Mode_TypeDef; Mode_TypeDef current_mode MODE_TRACK; // main()循环内 if (KEY_DOWN_Pressed()) { switch (current_mode) { case MODE_TRACK: current_mode MODE_REMOTE; break; case MODE_REMOTE: current_mode MODE_AVOID; break; case MODE_AVOID: current_mode MODE_TRACK; break; } LED_Toggle(LED2); // 切换时LED2闪烁提示 } switch (current_mode) { case MODE_TRACK: deviation TRACK_CalculateDeviation(ir_data); left_spd TRACK_GetLeftSpeed(deviation); right_spd TRACK_GetRightSpeed(deviation); break; case MODE_REMOTE: // 读取红外遥控NEC码解析方向指令 if (IR_Received()) { switch (IR_GetCommand()) { case IR_UP: left_spd right_spd 800; break; case IR_DOWN: left_spd right_spd -400; break; case IR_LEFT: left_spd -300; right_spd 600; break; case IR_RIGHT: left_spd 600; right_spd -300; break; } } break; case MODE_AVOID: // 超声波测距距离15cm时左转 if (ULTRASONIC_GetDistance() 15) { left_spd -400; right_spd 600; } else { left_spd right_spd 600; } break; } MOTOR_SetSpeed(left_spd, right_spd);逻辑说明KEY_DOWN_Pressed()为消抖后的按键检测函数已内置10ms延时MODE_REMOTE需外接VS1838B红外接收头IR_Received()在driver/ir_remote.c中实现MODE_AVOID需扩展HC-SR04超声波模块ULTRASONIC_GetDistance()通过TIM2输入捕获测量高电平时间所有模式共享同一套电机驱动和PID参数仅逻辑层切换资源占用极小。从那以后我每次拿到新小车板第一件事就是用ATCAL校准红外再用ATKP2.2微调全程不超过5分钟。那些在实验室熬通宵调参的日子其实只是没找对工具而已——真正的工程效率藏在可复现的参数、可验证的指令、和断电不丢的Flash里。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑