首页 / 资讯中心 / 文章详情

STM32电磁循迹小车完整代码解析:从电感采集到定圈停车

STM32电磁循迹小车完整代码解析:从电感采集到定圈停车 ★ FEATURED ARTICLE
简介这份资源是面向嵌入式初学者与竞赛选手的STM32电磁循迹小车完整工程代码附带详尽中文注释并集成蓝牙遥控、线路长度测量、定圈停止等扩展功能可帮助读者理解传感器数据采集、电机PWM调速与转向控制、UART蓝牙通信及编码器里程计算等核心环节适合作为课程设计或智能车比赛的参考方案。压缩包共249个文件约9MB以c源文件与h头文件为主体配合Keil工程文件、编译中间产物及hex、axf等可执行输出目录结构完整便于直接打开工程对照注释逐模块研读。目前已有6729人学习下载读者可从中获得循迹算法实现思路、蓝牙指令解析流程、圈数统计逻辑与调试排错经验为后续嵌入式项目开发积累可复用的代码基础。1. 从一堆能跑的代码说起这套 STM32 电磁循迹小车到底解决了什么拿到一份“能跑但看不懂”的单片机工程比拿到一份跑不起来的工程更让人难受。这份 STM32 电磁循迹小车完整代码最直接的价值不是“又一个循迹 Demo”而是把电感采集、舵机打角、电机差速、蓝牙遥控、赛道长度累计、定圈停车这些散落在各处的功能全部塞进一个带详细注释的工程里。它适合两类人一类是刚接触 STM32 和电磁循迹、想照着接线就能让车动起来的新手另一类是已经能跑基础循迹、但卡在“怎么加遥控、怎么算里程、怎么定圈停”的二次开发者。下面按“能干什么 → 怎么跑 → 怎么改”的顺序把这份代码拆开讲。2. 工程结构与技术选型从电感信号到轮速输出的完整链路2.1 硬件组成与信号流这套小车的核心是“电磁感应 差速转向”。车头布置若干工字电感赛道中心线通有特定频率的交流信号电感感应出的电压幅值随车头偏离中心线的距离变化。主控用 STM32 的 ADC 采集这些电感信号经过滤波和归一化后算出偏差再通过 PID 或分段打角输出舵机 PWM同时根据偏差和速度目标调整左右轮 PWM 占空比。蓝牙模块挂在 USART 上用来接收遥控指令和回传状态测线路长度靠编码器或电机霍尔脉冲累计定圈停止则是在累计里程达到设定圈长后切断电机输出。常见做法是把整个工程分成Core主逻辑、Hardware外设初始化、Algorithm循迹与 PID、App蓝牙、里程、定圈几层。这份代码的注释密度比较高基本每个函数入口都写了“输入什么、输出什么、什么时候调用”对新手比较友好。2.2 目录结构速览一个典型的工程目录大致如下具体文件名可能略有差异但分层思路一致Project/ ├── Core/ │ ├── main.c // 主循环、初始化调度 │ ├── stm32f1xx_it.c // 中断服务函数 │ └── system_stm32f1xx.c // 系统时钟 ├── Hardware/ │ ├── adc.c // 电感信号采集 │ ├── pwm.c // 舵机与电机 PWM │ ├── encoder.c // 编码器/霍尔测速 │ └── usart.c // 蓝牙串口 ├── Algorithm/ │ ├── filter.c // 滑动平均/中值滤波 │ ├── pid.c // 位置式/增量式 PID │ └── track.c // 偏差计算与打角策略 ├── App/ │ ├── bluetooth.c // 遥控指令解析 │ ├── odometer.c // 里程累计 │ └── lap_stop.c // 定圈停车判断 └── Drivers/ // 标准外设库或 HAL 库提示不同版本的工程可能用标准库也可能用 HAL 库接线和引脚定义以自己板子的原理图为准不要直接照搬引脚号。2.3 关键外设初始化顺序上电后的初始化顺序很关键顺序错了会出现“电机先转、电感还没稳定”的翻车现场。常见顺序是时钟 → GPIO → ADC含校准→ PWM 定时器 → 编码器定时器 → USART → 中断优先级 → 使能输出。下面是一段简化的初始化骨架注释里标了每一步的作用int main(void) { SystemClock_Config(); // 配置系统时钟决定后续定时器频率 MX_GPIO_Init(); // 引脚复用与上下拉 MX_ADC1_Init(); // 电感采集通道 MX_TIM1_Init(); // 舵机 PWM通常 50Hz MX_TIM2_Init(); // 电机 PWM频率可高一些 MX_TIM3_Init(); // 编码器接口模式 MX_USART1_UART_Init(); // 蓝牙串口常见 9600 或 115200 ADC_Calibrate(); // ADC 自校准减少零点漂移 Motor_Stop(); // 先确保电机不动 Bluetooth_SendString(READY\r\n); while (1) { Track_Task(); // 采集、算偏差、打角 Speed_Task(); // 测速与差速调整 App_Task(); // 蓝牙解析、里程、定圈判断 } }这段代码的逻辑是“先让所有外设进入确定状态再进主循环”。ADC_Calibrate()不能省电磁信号本身幅值不大零点漂移会直接让车往一边偏。Motor_Stop()放在初始化末尾是为了避免定时器刚启动时占空比寄存器残留值导致车突然窜出去。主循环里三个任务按固定节奏调用实际工程里常用定时器中断打标志位来保证采样周期稳定而不是靠while空转。3. 核心模块拆解电感采集、偏差计算与舵机打角3.1 电感信号采集与滤波电感信号是整套循迹的“眼睛”。ADC 采回来的原始值会随电源波动、赛道信号强度变化而抖动直接拿去算偏差车会像喝多了一样左右摆。常见做法是每个通道连续采多次去掉最大最小后取平均再做一阶低通。下面是一段可抄的采集函数#define SAMPLE_NUM 8 uint16_t ADC_Read_Filtered(uint8_t ch) { uint16_t buf[SAMPLE_NUM]; uint32_t sum 0; uint16_t max 0, min 0xFFFF; for (uint8_t i 0; i SAMPLE_NUM; i) { buf[i] ADC_Read_Channel(ch); // 单通道单次转换 sum buf[i]; if (buf[i] max) max buf[i]; if (buf[i] min) min buf[i]; } // 去掉一个最大值和一个最小值抑制尖峰干扰 return (uint16_t)((sum - max - min) / (SAMPLE_NUM - 2)); }SAMPLE_NUM取 8 是采样速度和稳定性的折中取太大主循环周期会被拉长取太小滤波效果有限。ADC_Read_Channel内部要保证采样时间足够电感信号经过运放后输出阻抗不低采样时间太短会采不准。滤波后的值再送进归一化用“当前值减最小值除以最大值减最小值”把不同通道拉到同一量纲否则左右电感灵敏度不一致偏差永远算不对。3.2 偏差计算与打角策略偏差计算常见有两种一种是“左右电感差值除以和值”得到 -1 到 1 之间的归一化偏差另一种是多电感加权适合电感数量多、赛道曲率变化大的场景。这份代码里用的是差值归一化加分段打角逻辑直观调参也容易。核心片段如下float Get_Track_Error(void) { uint16_t l ADC_Read_Filtered(ADC_CH_LEFT); uint16_t r ADC_Read_Filtered(ADC_CH_RIGHT); float err; if ((l r) 100) { // 两个电感都没信号可能脱线 return last_error; // 保持上一次偏差避免猛打角 } err ((float)l - (float)r) / ((float)l (float)r); last_error err; return err; } void Track_Task(void) { float err Get_Track_Error(); int16_t angle PID_Calc(steer_pid, err); // 输出舵机打角增量 Servo_Set_Angle(angle); }Get_Track_Error里那个“信号太弱就保持上次偏差”的判断很关键脱线瞬间如果直接按 0 处理舵机会回正车反而冲出去。PID_Calc的输入是归一化偏差输出限幅到舵机机械行程内常见是 -30 到 30 度。PID 参数我一般先只上 P让车能大致跟线再加 D 抑制摆动I 项在循迹里用得少除非赛道有持续单边坡度。调参时把车架空手动把车头往左往右偏看舵机响应是否跟手比直接下地跑效率高得多。3.3 电机差速与速度闭环舵机负责方向电机负责速度。差速的常见做法是“基础速度 偏差修正”偏差为正时左轮减速右轮加速偏差为负时反过来。如果只做开环占空比电池电压一下降车就变慢循迹手感会飘。所以工程里通常还会用编码器做速度闭环下面是一个增量式 PID 的速度环片段int16_t Speed_PID(int16_t target, int16_t actual) { int16_t err target - actual; static int16_t last_err 0; int16_t p KP * err; int16_t d KD * (err - last_err); last_err err; return p d; // 输出叠加到基础占空比上 }target来自“基础速度 偏差修正”actual来自编码器在固定周期内读到的脉冲数。KP、KD的量纲要和脉冲数量级匹配直接抄别人的参数大概率翻车因为编码器线数和采样周期不同。实际调试时先把KD置 0调KP到轮速能跟上目标且不持续震荡再加KD压超调。4. 附加功能实现蓝牙遥控、里程累计与定圈停车4.1 蓝牙串口指令解析蓝牙模块一般透传单片机收到的就是一串字符。常见协议是单字符指令比如F前进、B后退、L左转、R右转、S停止、A切自动循迹。解析时不要在主循环里死等用中断接收加环形缓冲更稳。下面是一个简化的解析函数void Bluetooth_Parse(uint8_t ch) { switch (ch) { case F: mode MODE_MANUAL; manual_speed 600; break; case B: mode MODE_MANUAL; manual_speed -600; break; case L: manual_steer -20; break; case R: manual_steer 20; break; case S: mode MODE_STOP; manual_speed 0; break; case A: mode MODE_AUTO; break; // 切回自动循迹 default: break; } }mode是全局状态机手动模式下循迹任务不输出舵机避免两边抢控制权。manual_speed的正负代表方向实际写进 PWM 时要取绝对值并设置方向引脚。串口中断里只做“收字节进缓冲”解析放到主循环这样不会因为解析耗时导致丢字节。4.2 里程累计与定圈判断测线路长度有两种常见来源编码器脉冲累计或者按“速度 × 时间”估算。前者准但需要编码器后者简单但受打滑影响。代码里如果是编码器方案通常在一个固定周期中断里把左右轮脉冲累加再乘以轮周长换算成距离void Odometer_Update(int16_t left_pulse, int16_t right_pulse) { float dist_l left_pulse * MM_PER_PULSE; float dist_r right_pulse * MM_PER_PULSE; total_distance (dist_l dist_r) * 0.5f; // 取平均抵消差速 } uint8_t Lap_Check(float lap_length) { if (total_distance lap_length * lap_count) { lap_count; return 1; // 到达一圈 } return 0; }MM_PER_PULSE等于轮周长除以每圈脉冲数这个值必须实测让车直线走一段已知距离读累计脉冲反推比按理论值算靠谱。Lap_Check返回 1 时上层把电机占空比清零并进停车状态。定圈数lap_count的目标值建议做成宏或可配置变量方便赛前改。4.3 状态机把功能串起来蓝牙、循迹、里程、定圈这几个功能如果各写各的if很容易互相打架。常见做法是用一个顶层状态机IDLE、MANUAL、AUTO、STOP。自动模式下才跑循迹和里程累计手动模式下只响应遥控停车状态屏蔽所有电机输出。状态切换集中在蓝牙解析和定圈判断两处逻辑清晰也方便后面加“暂停”“重新计圈”之类的功能。5. 避坑与常见问题调试现场最容易翻车的几件事5.1 车上电就猛冲现象一给电电机直接满速转。原因多半是 PWM 定时器初始化后比较寄存器默认值不为 0或者方向引脚初始电平不对。解决初始化最后统一调用Motor_Stop()把占空比写 0、方向引脚拉低再进主循环。5.2 循迹左右摆个不停现象车能跟线但持续画龙。原因通常是滤波不足、PID 的 D 项过大或者左右电感灵敏度不一致。解决先确认归一化做了没有再把 D 项减半试跑最后检查两个电感在中心线正上方时读数是否接近。5.3 蓝牙遥控时好时坏现象按了没反应或者连按几次才动。原因常见是串口中断里做了耗时解析导致丢字节或者蓝牙模块波特率和代码不一致。解决中断只收字节进环形缓冲解析放主循环用串口助手先确认模块波特率再对代码里的BaudRate。5.4 里程和实际距离对不上现象定圈总是提前或延后停。原因多是MM_PER_PULSE用了理论值或者差速时只取了单侧脉冲。解决实测标定该系数累计时取左右平均如果赛道有打滑适当留一点余量。5.5 脱线后车不减速反而冲出去现象车头偏出赛道速度不降。原因是没有做“信号丢失”判断偏差保持逻辑让舵机维持旧角度。解决在Get_Track_Error里加信号强度阈值连续多次低于阈值就进保护状态降速或停车。6. 二次开发怎么改从参数标定到功能扩展的实操思路拿到这份代码后我建议先别急着加功能先把三个基础量标定准电感归一化区间、MM_PER_PULSE、舵机中位与限幅。这三个不准后面加什么功能都是空中楼阁。标定电感时把车放在赛道中心线上记录各通道读数再分别把车头左偏、右偏到极限位置记录读数用这些值做归一化上下限。舵机中位用“给固定角度看轮子是否回正”的方式确认限幅要留机械余量别让舵机顶死发出嗡嗡声。功能扩展上比较有价值的改法有三个方向。第一把分段打角换成完整 PID 加前馈弯道提前打角直道更稳第二把定圈停车扩展成“分段限速”比如第一圈慢速热身、第二圈提速用一个圈数计数器驱动目标速度表第三把蓝牙协议从单字符升级成带校验的短帧比如$F,600#方便上位机调参。改协议时注意接收缓冲要能容纳整帧解析用状态机而不是strstr硬找。验证方法上我习惯分三层架空测舵机和电机响应低速下地测循迹再逐步提速测定圈。每改一个参数只动一个变量改完记录一次现象不然出了问题根本不知道是哪次改动引入的。下面这张表是我常用的调试记录格式可以直接抄改动项改动前改动后现象结论舵机 KP812直道更跟手弯道略抖保留补 D速度 KP35起步更冲回退到 4滤波窗口48摆动减小响应变慢保留最后说个血泪经验这份代码注释再全也别跳过“先架空、再低速、后提速”的顺序。我有一次图省事直接满速下地结果脱线保护没生效车一头撞上桌腿舵机齿轮当场扫齿。从那以后我每次改完参数都强制走一遍架空验证确认舵机和电机响应正常才放地上跑。希望这份拆解能帮你少走几个弯路把这份代码真正跑成自己的车。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站