
简介一份STM32电磁循迹小车完整代码包面向嵌入式初学者与智能车竞赛爱好者提供带详细注释的最终方案包含蓝牙遥控、测线路长度、定圈停止等附加功能。代码基于库函数版例程覆盖STM32初始化配置、霍尔传感器数据读取、蓝牙通信、编码器距离计算、圈数统计与PWM电机控制等模块可帮助读者完整理解电磁循迹小车的软硬件协同逻辑。压缩包共249个文件以c/h源文件为主辅以o/d/crf编译中间文件、uvprojx工程文件、hex/axf烧录文件及map/lst调试文件整体约9MB结构清晰便于直接打开学习。目前已有6710人学习下载适合需要参考完整代码、快速上手电磁循迹项目或扩展蓝牙与测距功能的开发者。 做一个电磁循迹小车听起来好像只要把传感器架起来、把代码一烧就完事。真正动手后才发现循迹算法、电机响应、传感器标定、蓝牙干扰、计数累计误差这些东西全搅在一起每一个环节不处理好小车要么在直道上画龙要么过弯直接冲出赛道。这次要聊的是一套我反复验证过的方案STM32电磁循迹小车带详细变量注释的完整代码另外还塞进了蓝牙遥控、测线路长度、定圈停止这三个平时竞赛和课设里特别常用的扩展功能。这套代码的定位很明确给正在做智能车竞赛、STM32课程设计或者单纯想入门“电磁循迹”的人一个能直接跑通的底子。它不用摄像头、不用OpenMV核心就是三路电磁传感器配合差速控制跑的是经典的比例控制加PID修正方案。代码量不大但每个功能模块的注释都写得比较细后期改阈值、调参数、加功能都比较方便。整套工程基于STM32F103C8T6和标准外设库用Keil MDK打开就能编译。1. 项目整体设计与选型思路1.1 为什么选电磁循迹而不是摄像头或红外我见过很多新手一上来就想上摄像头理由是“摄像头识别赛道更直观”。但实际比赛和课程设计里项目时间通常只有一两个月摄像头需要处理二值化、透视变换、丢线补线一套流程走下来光调试就够喝一壶。电磁循迹的优势在于传感器输出是连续的模拟电压数据量小、响应快、对光照不敏感。只要把工字电感和运放电路调好后面控制逻辑会纯粹很多。另外从控制难度上看电磁方案天然适合差速转向。两路电机转速差的响应速度比舵机快而且在低速和中高速段都能保持不错的稳定性。用阿克曼转向的小车高速下容易出现转向不足差速小车只要PID参数不激进跑起来反而更稳。这也是为什么很多智能车竞赛的电磁组都采用两轮差速结构。1.2 系统架构与三种运行模式整套系统就是“感知-决策-执行”三层。感知层是三路电磁传感器加一路霍尔测速决策层是STM32跑状态机和PID计算执行层是TB6612驱动直流减速电机。蓝牙模块挂在串口上负责在手动遥控和自动循迹之间切换同时接收目标圈数。我把运行模式拆成三种切换逻辑全部由一个枚举变量管理模式触发方式行为遥控模式蓝牙发送 F/B/L/R/S手动控制前进、后退、左转、右转、停止循迹模式蓝牙发送 A根据电磁偏差自动调整左右轮速度定圈停止循迹模式下发送数字1~9距离累计达到目标后自动停车这三种模式在主循环里用if / else if分流就行。这样设计的好处是功能扩展时不用改动循迹核心比如后面想加蜂鸣器、OLED显示只要在主循环里加一个分支。2. 硬件组成与接线方案2.1 器件清单先列一下核心物料都是常见东西主控STM32F103C8T6最小系统板电机驱动TB6612FNGL298N也行但TB6612的内阻和体积都更适合小车传感器3个工字电感10mH左右加LMV358运放电路输出0~3.3V可调电压测速/测距2路霍尔测速模块轮子每转一圈输出固定数量的脉冲蓝牙HC-05或HC-06串口透传需要特别注意电磁传感器不是直接接ADC就能用。电感感应的信号只有几毫伏到几十毫伏必须先经过放大、检波、滤波再送进ADC。某宝上常见的“电磁循迹传感器模块”已经把集成好了输出0~3.3V直流电压直接接STM32的ADC引脚即可。如果是自己搭电路就要用运放加二极管检波再滤波否则ADC读出来会是一堆噪声。2.2 引脚分配与实际接线我的接线如下建议你尽量按这个来因为代码里的引脚定义直接对应这份表功能STM32引脚说明左电磁ADCPA0ADC1通道0中电磁ADCPA1ADC1通道1右电磁ADCPA4ADC1通道4蓝牙TXPA2USART2_TX接HC-05的RX蓝牙RXPA3USART2_RX接HC-05的TX左PWMPA8TIM1_CH1TB6612的PWMA右PWMPA9TIM1_CH2TB6612的PWMB左方向PB12/PB13TB6612的AIN1/AIN2右方向PB14/PB15TB6612的BIN1/BIN2左测速信号PB0外部中断输入右测速信号PB1外部中断输入这里有两个容易踩坑的地方。第一蓝牙模块如果用的是5V供电板RX引脚很可能是5V电平直接连到STM32的PA3有烧引脚风险最好加一个1K电阻分压或者直接用3.3V供电版本。第二霍尔测速模块输出的是开漏信号要接上拉电阻否则中断会频繁误触发测出来的距离会严重偏大。3. 核心功能与代码模块详解3.1 电磁采样与偏差计算电磁循迹的核心思想是导线通电后周围会产生交变磁场。三个电感水平放在车头分别位于赛道中心线的左、中、右。当小车中心正对导线时中间电感的感应电压最高左右两个电感电压大致相等当小车偏离时左右电压差就会变大。ADC采样部分// 读取指定ADC通道的转换结果连续采8次取平均降低噪声 uint16_t EM_ReadChannel(ADC_TypeDef* ADCx, uint8_t channel) { ADC_RegularChannelConfig(ADCx, channel, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADCx, ENABLE); while (!ADC_GetFlagStatus(ADCx, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADCx); } void EM_Update(void) { // 三路传感器原始电压范围大约0~409512位ADC left_val EM_ReadChannel(ADC1, ADC_Channel_0); mid_val EM_ReadChannel(ADC1, ADC_Channel_1); right_val EM_ReadChannel(ADC1, ADC_Channel_4); float total (float)(left_val mid_val right_val); if (total 50.0f) { return; // 信号太弱大概率是出赛道了保持上次偏差 } // 偏差归一化到 -100 ~ 100 error ((float)right_val - (float)left_val) / total * 100.0f; }归一化这步很多人会忽略直接把电压差当误差用。这样做的问题在于传感器离导线越近电压整体越高同样的偏移量在不同位置产生的电压差不一样。归一化之后偏差基本只与小车相对于导线的位置有关和整体信号强度无关PID参数才能在不同赛道上通用。那段if (total 50.0f) return;必须保留。电磁车过弯时如果车头大幅偏离赛道某一路传感器可能超出磁场范围读数掉得非常快这时候还拿算出来的error去控制方向会乱。让误差保持上一帧的数值能避免小车在出线瞬间猛打方向盘。3.2 差速控制与PID调参偏差算出来了接下来就是怎么转。我的控制方式是给定一个基础速度base_speed然后根据PID输出值调整左右轮// 位置式PID控制周期由定时器中断固定为10ms float Kp 1.6f, Ki 0.02f, Kd 2.5f; int last_error 0; float integral 0; int EM_PID_Calc(int target, int current) { int err target - current; // 目标偏差是0 integral (float)err * 0.01f; // 10ms控制周期粗略积分 // 积分限幅防止起步时积分饱和 if (integral 50) integral 50; if (integral -50) integral -50; float out Kp * err Ki * integral Kd * (err - last_error); last_error err; return (int)out; } void Track_Control(void) { int adjust EM_PID_Calc(0, (int)error); int left_pwm base_speed - adjust; int right_pwm base_speed adjust; // PWM限幅避免差速过大导致转向过头 left_pwm (left_pwm 1000) ? 1000 : left_pwm; left_pwm (left_pwm -1000) ? -1000 : left_pwm; right_pwm (right_pwm 1000) ? 1000 : right_pwm; right_pwm (right_pwm -1000) ? -1000 : right_pwm; Motor_SetSpeed(0, left_pwm); // 后两个参数内部处理方向 Motor_SetSpeed(1, right_pwm); }这里的加减号方向要和你实际电机接线一致。如果发现小车越偏越远说明adjust方向反了把左轮和右轮的符号互换即可不用改硬件。关于PID参数我的调法分三步把Kp放到一个很小的值Ki和Kd先设为0让小车能跟着弯走但会有比较明显的迟滞。逐步增大Kp直到小车在直道上开始轻微抖动的临界点然后回调20%。再加Kd抑制抖动最后加一点点Ki消除残差。新手往往死在第一步就把基础速度给太高。我建议先把base_speed定在600/1000约60%占空比验证算法再往上提速。速度越高磁场信号变化越剧烈PID参数需要重新微调。3.3 蓝牙遥控协议解析蓝牙端我用一个非常简单的字符协议不整JSON那些花活F前进B后退L左转R右转S停车A切换为自动循迹模式1~9设置目标圈数串口接收中断里做解析void USART2_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART2) 0xFF; switch (ch) { case A: mode MODE_TRACK; // 启动自动循迹 break; case F: case B: mode MODE_MANUAL; manual_dir (ch F) ? 1 : -1; break; case L: case R: mode MODE_MANUAL; manual_turn (ch L) ? -1 : 1; break; case S: mode MODE_STOP; break; default: if (ch 1 ch 9) target_laps ch - 0; break; } } }蓝牙模块配置成9600波特率、8位数据位、1位停止位。如果你买的是HC-05需要先进入AT模式把波特率改到和代码一致。这里有个常见坑HC-05默认波特率通常是38400或9600HC-06通常是9600或115200烧录代码前确定模块实际波特率否则串口助手收到的全是乱码。3.4 线路长度测量与定圈停止测线路长度我采用霍尔测速模块计脉冲而不是靠延时估算。轮子每转一圈会输出固定数量脉冲统计脉冲个数就能算出轮子走过的距离。// 外部中断统计脉冲 volatile uint32_t pulse_cntL 0; volatile uint32_t pulse_cntR 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { pulse_cntL; // 左轮测速脉冲1 EXTI_ClearITPendingBit(EXTI_Line0); } } void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) ! RESET) { pulse_cntR; EXTI_ClearITPendingBit(EXTI_Line1); } } // 根据脉冲计算平均行驶距离单位mm float Get_Travel_Distance(void) { float avg_pulse ((float)pulse_cntL (float)pulse_cntR) / 2.0f; return avg_pulse / PULSES_PER_ROUND * WHEEL_CIRCUMFERENCE_MM; }PULSES_PER_ROUND是你那个测速模块在轮子转一圈时输出的脉冲数。霍尔模块一般是“电机出轴脉冲数×减速比”比如电机出轴每圈2个脉冲、减速比30的话轮子一圈约60个脉冲。但实际可能因为减速箱误差、安装偏差而不同最好让小车在直道上跑一段准确量好的距离再反推这个参数比直接套理论值更可靠。定圈停止就很简单了float single_lap_length 1200.0f; // 单圈赛道长度单位mm现场实测 uint8_t target_laps 3; // 循迹模式下不断检查 if (mode MODE_TRACK) { if (Get_Travel_Distance() target_laps * single_lap_length) { mode MODE_STOP; Motor_Stop(); } }这种“按距离算圈数”的方式有一个前提每圈实际路径长度要相对固定否则误差会累积。如果赛道上有明显的起跑线也可以加一路巡线传感器检测起跑线每圈计数一次更精确。但我在竞赛里用距离法完全够用只要试跑阶段把单圈长度标定准确就行。4. 主程序状态机与参数配置4.1 主循环结构主循环写得很直白核心是一个状态机int main(void) { SystemInit(); GPIO_Config(); TIM1_PWM_Init(); ADC1_Init(); USART2_Init(); EXTI_Init(); Sensor_Calibrate(); // 上电校准电磁传感器零点 while(1) { if (mode MODE_TRACK) { EM_Update(); Track_Control(); if (is_running) { Check_LapStop(); // 定圈停止检测 } } else if (mode MODE_MANUAL) { Manual_Control(); } else // MODE_STOP { Motor_Stop(); } delay_ms(10); // 控制周期10ms工程里用SysTick产生标志位 } }这里delay_ms(10)只是为了展示主循环逻辑实际工程里我用SysTick产生10ms标志位主循环检测到标志位置位才执行一次控制。否则串口中断、PID计算、距离累计会互相挤占CPU时间小车运行就不稳定。is_running是一个运动开始标志用来区分“静止待命”和“运动中循迹”。比如你想在发车后先等蓝牙指令再开始计圈就通过这个标志控制。这里为了简化主流程没有展开但工程里已经保留了对应变量。4.2 上电校准和关键参数Sensor_Calibrate()的作用是上电时让小车放在赛道中心把当前三路ADC值读出来作为基准。这个校准不能省因为不同电池电压下电感模块的静态输出电压会有差异不校准的话左右偏差的零点会漂移。校准逻辑也不复杂记录左右ADC差值基准之后EM_Update得到的error再减掉这个基准偏移即可。几个关键参数建议这样设初始值参数推荐值说明base_speed600 ~ 800基础PWM满量程1000Kp1.2 ~ 2.0比例增益决定转向响应速度Kd2.0 ~ 3.5微分增益抑制直道抖动Ki0.01 ~ 0.05积分增益消除静态残差PULSES_PER_ROUND据实测每轮脉冲数single_lap_length现场测量单圈赛道长度mm这几个参数是整个项目最值得花时间的地方。参数调好后循迹部分基本不用动后续做速度规划或其他功能也只是在Track_Control()里增加速度修正项。5. 常见问题与调试实录5.1 下载程序时报 “No STM32 Target Found!”先讲最让人头疼的烧录问题。代码写好了Keil里一点下载弹出 “No STM32 Target Found!”或者后面还跟一句 “If your product embeds debug authentication...”。我遇到这个报错时按下面顺序排查基本能解决确认ST-Link/J-Link连接线是否短路或虚接SWD只需要SWDIO、SWCLK、GND、3.3V四根线不要接错。确认最小系统板供电正常。很多F103板子用3.3V供电但烧录器给目标板供电的电流有限最好外接电源后再烧。检查下载器驱动。ST-Link要在设备管理器里显示正常如果出现黄色感叹号重新装驱动。如果目标板是新买的芯片可能存在调试保护。这种情况在最新批次的F103里也有需要先通过ST-Link Utility或STM32CubeProgrammer把option bytes里的保护等级调到无再回Keil下载。第4条很多人不知道。老一批F103基本没有调试保护问题但最近有不少新板子出厂带读保护直接用Keil下载就会报这个错。先连上ST-Link用CubeProgrammer处理一下再下载就好了。5.2 直道上画龙弯道里又反应不过来画龙本质是PID参数不够匹配。直道上来回小幅摆动说明Kp太高或者Kd太低导致阻尼不够弯道里反应慢说明Kp不够或者基础速度给得过高。我的处理顺序是先收基础速度再调Kp最后调Kd。如果你发现弯道外侧轮完全不会减速八成是偏差符号反了。最快判断方法是把小车架空用手把车头偏到右侧看左轮是否加速如果加速方向错了马上交换左右轮PID输出符号。5.3 测出的距离越跑越偏霍尔模块每圈输出脉冲数存在离散性左右轮磨损、胎压不一也会导致实际周长变化。我的解决方法是取左右轮脉冲平均值并且在定圈停止前做一次“就近补偿”最后一圈如果脉冲数距离目标还差一点用较低速度滑过去而不是高速急停。如果对精度要求更高可以换编码器测速。普通霍尔模块测的是轮子转数不是轮子走过的真实距离打滑时必然有误差。竞赛里我不追求毫米级平均误差在3%以内完全可以接受。5.4 蓝牙连上但控制无效蓝牙连上了、收发也有数据但小车不动我遇到的多半是共地问题。蓝牙模块和STM32如果没有共地串口电平没有参考基准单片机能收到乱码甚至什么都收不到。接线时一定把蓝牙的GND和STM32的GND接在一起。还有一种情况是蓝牙模块一接上电机就抖。这是因为蓝牙模块和电机驱动共用5V电源时电机启动瞬间把5V拉低导致单片机复位或蓝牙掉线。解决办法是电源分开走电机电源线直接从电池取逻辑电源从降压模块单独出地线再汇合瞬间压降就不会干扰到控制电路。最后再分享一点个人体会。最开始我给小车加测距和定圈功能时以为逻辑上接一个计数就行结果跑了三五圈就开始飘查了半天发现是霍尔脉冲数没标定准PULSES_PER_ROUND填了理论值真实值差了不少。后来老老实实用卷尺量了5米直道跑了一遍反推出来的每轮脉冲数才稳定下来。所以这套代码虽然能直接跑但PULSES_PER_ROUND和single_lap_length这两项千万不要照抄拿到你的车上实测一次再填进去。这些参数就是小车的“手感”每台车都不一样标定准确了剩下的就是享受跑圈的过程了。本文还有配套的精品资源点击获取