☰
STM32电磁循迹小车:从能跑到跑得明白的完整工程实践
2026/10/26 11:44:32 网站建设 项目流程

简介:这份资源是面向嵌入式初学者与竞赛选手的STM32电磁循迹小车完整工程代码,在基础循迹之外集成蓝牙遥控、线路长度测量与定圈停止等扩展功能,适合用来对照学习传感器数据处理、电机PWM调速与串口通信的协同实现。压缩包共249个文件,约9MB,以c源文件与h头文件为主体,配合Keil工程文件、编译中间产物及hex、axf等可执行输出,构成可直接打开编译的完整工程目录。代码中保留详细注释,覆盖时钟与GPIO初始化、霍尔元件数据读取、蓝牙指令解析、编码器脉冲计数与圈数统计等模块,便于读者按功能逐块理解控制逻辑并移植到自己的小车平台。目前已有6729人学习下载,可作为课程设计或循迹竞赛的参考底稿,帮助快速搭建调试环境、定位问题并积累嵌入式项目经验。

1. 从“能跑就行”到“跑得明白”:一套 STM32 电磁循迹小车的完整交付思路

很多同学做电磁循迹小车,第一版代码往往是“能跑就行”:传感器读回来直接进 if-else,电机 PWM 拍脑袋给值,跑偏了就改阈值,改到最后自己都说不清哪一行在起作用。这套方案想解决的正是这个问题——把 STM32 电磁循迹小车做成一份带详细注释、结构清晰、功能可扩展的完整工程,除了基础循迹,还集成了蓝牙遥控、赛道长度测量、定圈停止三个实用小功能。它适合正在做嵌入式课程设计、电子竞赛入门训练,或者需要一个可复用底盘控制框架的读者。下面按需求、设计、实现、避坑、验收的顺序,把这份工程拆开讲透。

2. 需求与总体设计:四个功能模块如何划分职责

2.1 功能需求拆解与优先级

先把需求列清楚,再谈代码。这套小车的核心需求可以拆成四层:第一层是电磁循迹,靠电感线圈感应赛道通电导线产生的交变磁场,判断车体相对导线的偏移方向;第二层是电机闭环控制,把偏移量转换成左右轮的差速;第三层是蓝牙遥控,用手机或串口终端下发前进、后退、转向、调速指令;第四层是赛道长度测量与定圈停止,通过编码器或霍尔计数推算行驶距离,达到设定圈数后自动停车。

优先级上,循迹是主线,电机控制是执行基础,蓝牙和计圈是附加功能。很多工程翻车就翻在把附加功能塞进主循环里,导致循迹响应变慢。我的做法是:循迹与电机控制放在定时器中断里以固定周期执行,蓝牙解析放在主循环低优先级处理,计圈逻辑挂在编码器外部中断上只做计数,主循环定期读取。这样各模块的时间确定性有保障,不会因为蓝牙收包抖动影响转向。

2.2 硬件资源分配与引脚规划

STM32 型号选型上,这类项目通常用 STM32F103C8T6 或同级别芯片就够:需要至少 2 路定时器 PWM 输出驱动电机、1 路 ADC 采集电感信号(多路可用 ADC 扫描或外部多路开关)、1 路 USART 接蓝牙模块、若干 GPIO 接编码器和按键。引脚分配建议提前画一张表,避免后期改线改到怀疑人生。

资源用途典型配置
TIM1_CH1/CH2左右电机 PWM10 kHz,占空比 0~1000
ADC1_IN0~IN3四路电感信号连续扫描,DMA 搬运
USART1蓝牙串口9600 或 115200,8N1
TIM3循迹控制周期5 ms 中断
EXTI编码器脉冲计数上升沿触发
GPIO电机方向、蜂鸣器、按键推挽输出

这张表的意义在于:把“哪个外设干什么”固定下来,后面写初始化代码时按表配置,不会出现两个功能抢同一个定时器的情况。血泪经验是,PWM 和编码器计数千万别共用同一个定时器通道组,否则改一个参数另一个就乱。

2.3 软件分层:驱动层、控制层、应用层

工程按三层组织:驱动层封装电机、ADC、串口、编码器的读写;控制层实现循迹算法和速度环;应用层处理蓝牙指令和计圈状态机。分层的好处是换硬件时只改驱动层,调参数时只动控制层。目录结构大致如下:

Project/ ├── Core/ # 主函数与中断入口 ├── Drivers/ │ ├── motor.c/h # 电机 PWM 与方向控制 │ ├── adc_sensor.c/h # 电感采集与归一化 │ ├── encoder.c/h # 编码器计数 │ └── uart_bt.c/h # 蓝牙收发 ├── Control/ │ ├── track.c/h # 循迹偏差计算与 PID │ └── speed.c/h # 速度环 └── App/ ├── remote.c/h # 蓝牙指令解析 └── lap.c/h # 计圈与定圈停止

这种划分不是形式主义。调试时你可以单独给 track.c 打桩输入偏差值,观察输出是否合理,不用整车跑起来才能验证算法。后面讲避坑时还会提到,模块边界不清是参数互相干扰的根源。

3. 循迹与电机控制实现:从电感采样到差速输出

3.1 电感信号采集与偏差计算

电磁循迹的关键是把多路电感的幅值转换成“车偏左还是偏右、偏多少”。常见做法是左右各一个电感,或者左中右三个。这里用左右双电感方案:归一化后计算差值比总和,得到 -1 到 +1 之间的偏差量。

/* 读取左右电感归一化值并计算偏差,返回 -1000~+1000 */ int16_t Track_GetError(void) { uint16_t raw_l = ADC_GetValue(ADC_CH_LEFT); uint16_t raw_r = ADC_GetValue(ADC_CH_RIGHT); /* 减去静态基线,抑制环境磁场干扰 */ int32_t l = (int32_t)raw_l - g_base_left; int32_t r = (int32_t)raw_r - g_base_right; if (l < 0) l = 0; if (r < 0) r = 0; int32_t sum = l + r; if (sum < 50) { /* 信号太弱,视为丢线 */ return g_last_error; /* 保持上一次偏差,避免猛打方向 */ } /* 差值比总和,映射到 -1000~+1000 */ return (int16_t)((r - l) * 1000 / sum); }

这段代码有两个设计点值得说。第一,静态基线 g_base_left/right 要在车上电、车静止、赛道通电时采样得到,用来扣掉环境本底,否则白天黑夜读数能差一截。第二,丢线时返回上一次偏差而不是清零,是为了让车“顺着原来的方向找线”,直接清零会让车在原地犹豫甚至反向冲出赛道。参数上,sum 的阈值 50 需要根据你的 ADC 分辨率和放大电路实测调整,太小会误判丢线,太大会在弱信号区抖动。

3.2 增量式 PID 与差速输出

偏差算出来后,用增量式 PID 转成转向修正量,再叠加到基础速度上形成左右轮目标速度。增量式的好处是不需要积分饱和处理,输出平滑,适合这种周期性控制。

/* 5ms 周期调用,输入偏差,输出左右轮 PWM 目标值 */ void Track_Control(int16_t error) { static int16_t last_err = 0, prev_err = 0; int16_t delta = error - last_err; /* 增量式 PID:Kp*Δe + Ki*e + Kd*(Δe-Δe_last) */ int32_t turn = (int32_t)g_kp * delta + (int32_t)g_ki * error + (int32_t)g_kd * (delta - (last_err - prev_err)); prev_err = last_err; last_err = error; int32_t base = g_base_speed; /* 基础速度,蓝牙可调 */ int32_t left = base + turn / 100; /* 除以缩放系数防溢出 */ int32_t right = base - turn / 100; left = (left < 0) ? 0 : (left > 1000 ? 1000 : left); right = (right < 0) ? 0 : (right > 1000 ? 1000 : right); Motor_SetSpeed(MOTOR_LEFT, left); Motor_SetSpeed(MOTOR_RIGHT, right); }

参数整定顺序建议先 P 后 D 再补 I:先把 Ki、Kd 置零,调 Kp 到车能跟线但略有摆动;再加 Kd 压摆动;最后加很小的 Ki 消除直道上的稳态偏置。缩放系数 100 是为了让定点运算不溢出,实际取值和你的 Kp 量级有关,改参数时这个除数要一起看。注意左右轮的“加”和“减”方向取决于你的电机安装方向,装反了车会越修越偏,第一次上电务必架空轮子观察转向。

3.3 速度环与电机驱动细节

只有位置环没有速度环,车在弯道会因为负载变化忽快忽慢。速度环用编码器在固定周期内计数值作为实测速度,和目标速度做 PI 调节,输出叠加到 PWM 上。实现上,编码器用外部中断或定时器编码器模式计数,每 20 ms 读一次并清零。

/* 20ms 调用一次,速度环 PI */ void Speed_Loop(void) { int16_t act_l = Encoder_ReadAndClear(ENC_LEFT); int16_t act_r = Encoder_ReadAndClear(ENC_RIGHT); g_pwm_l += (g_target_l - act_l) * SPEED_KP / 100 + (g_target_l - act_l) * SPEED_KI / 1000; g_pwm_r += (g_target_r - act_r) * SPEED_KP / 100 + (g_target_r - act_r) * SPEED_KI / 1000; g_pwm_l = (g_pwm_l < 0) ? 0 : (g_pwm_l > 1000 ? 1000 : g_pwm_l); g_pwm_r = (g_pwm_r < 0) ? 0 : (g_pwm_r > 1000 ? 1000 : g_pwm_r); Motor_SetPwm(g_pwm_l, g_pwm_r); }

电机驱动芯片常见的是 L298N 或 TB6612,前者压降大、发热明显,后者效率高但电流余量要看电机堵转电流。无论哪种,PWM 频率别低于 8 kHz,否则电机会有可闻啸叫;也别太高,超过 20 kHz 部分驱动芯片开关损耗上来会烫。方向控制上,换向前建议先给 0 占空比再切方向引脚,避免瞬间反电动势冲击驱动。

4. 蓝牙遥控与计圈功能:附加功能怎么不拖累主线

4.1 蓝牙指令协议与解析

蓝牙模块(如 HC-05 类串口透传模块)本质就是一根无线串口,所以协议设计越简单越好。这里用单字节指令加可选参数的格式:0xA1 前进、0xA2 后退、0xA3 左转、0xA4 右转、0xA5 停车、0xA6 设置基础速度后跟一字节参数。解析放在主循环,用状态机收包,避免在中断里做字符串处理。

/* 主循环调用,非阻塞解析蓝牙指令 */ void Remote_Poll(void) { uint8_t ch; while (UART_ReadByte(&ch)) { switch (g_bt_state) { case BT_IDLE: if (ch >= 0xA1 && ch <= 0xA6) { g_bt_cmd = ch; g_bt_state = (ch == 0xA6) ? BT_WAIT_PARAM : BT_IDLE; if (ch != 0xA6) Remote_Execute(ch, 0); } break; case BT_WAIT_PARAM: Remote_Execute(g_bt_cmd, ch); g_bt_state = BT_IDLE; break; } } }

状态机的好处是收包不阻塞,参数没到齐也不会卡住循迹。注意蓝牙模块上电默认波特率要和代码一致,很多“收不到指令”的问题其实是波特率对不上或者 TX/RX 接反。另外,遥控指令和循迹模式要有明确的模式切换标志,别让遥控的前进指令和循迹的差速输出同时写电机,否则两个来源打架,车会抽搐。

4.2 赛道长度测量与定圈停止

长度测量用编码器累计脉冲数乘以每脉冲对应距离。每脉冲距离 = 轮周长 / 每圈脉冲数,这个系数要在直道上实测标定:让车走 5 米,读累计脉冲,反算系数。定圈停止则是一个状态机:累计距离达到单圈长度后圈数加一,圈数达到设定值就切到停车状态并锁存,直到收到复位指令。

/* 每 20ms 调用,累计距离并判断定圈 */ void Lap_Update(void) { int32_t pulses = Encoder_ReadAndClear(ENC_LEFT) + Encoder_ReadAndClear(ENC_RIGHT); g_total_pulse += pulses / 2; /* 双轮平均 */ float dist = g_total_pulse * g_mm_per_pulse; /* 单位 mm */ if (dist - g_last_lap_dist >= g_lap_length_mm) { g_last_lap_dist += g_lap_length_mm; g_lap_count++; if (g_lap_count >= g_target_lap) { g_run_state = STATE_STOP; /* 锁存停车 */ Motor_SetSpeed(MOTOR_LEFT, 0); Motor_SetSpeed(MOTOR_RIGHT, 0); } } }

这里最容易翻车的是单圈长度设得比实际短,车还没到线就提前计圈,跑几圈后误差累积直接乱套。我的习惯是单圈长度按实测值放大 2%~3% 作为容差,并且计圈判断用“跨过阈值”而不是“等于阈值”,避免浮点比较的玄学问题。定圈停车后一定要锁存状态,否则下一周期又满足条件重复触发,日志里会看到圈数疯涨。

5. 避坑与排查:这类小车最容易翻车的五个点

5.1 电感读数漂移,白天黑夜表现不一致

现象是同一套参数上午能跑、下午就冲出赛道。原因多半是环境磁场本底变化,或者电源电压波动导致 ADC 参考漂移。解决办法是每次上电静止时重新采样基线,并在运行中做慢速跟踪:车在直道稳定跟线时,用当前读数缓慢更新基线,时间常数取几秒,既跟得上温漂又不会被弯道带偏。

5.2 电机启动瞬间复位或程序跑飞

现象是给油门的瞬间单片机重启,串口打印从头开始。原因是电机启动电流大,电池电压被拉低,单片机供电跌破复位阈值,或者电机续流干扰串进了 MCU 电源。解决上,电机电源和 MCU 电源分开走线,共地单点连接,电机两端加续流二极管,MCU 电源入口加足够容量的电容。这类问题用示波器看电源纹波最直接,没有示波器就先把供电和地线理顺再调代码。

5.3 PID 参数一改就震荡,越调越乱

现象是调 Kp 时车摆,加 Kd 后高频抖,加 Ki 后直道画龙。原因通常是控制周期不固定,或者位置环和速度环互相耦合。解决上,先把速度环固定在一个合理值甚至暂时开环,只调位置环;确认控制周期严格 5 ms 不被打断;每次只改一个参数并记录,别三个参数一起动。参数记录建议直接写在注释里,附上当时的赛道和电池状态,不然一周后你完全不记得为什么是这个值。

5.4 蓝牙指令偶尔丢包或误动作

现象是遥控时好时坏,偶尔车自己动一下。原因是串口中断里做了耗时处理导致丢字节,或者无线环境干扰造成误码。解决上,中断里只把字节塞进环形缓冲区,解析全部放主循环;协议加帧头帧尾或校验字节,校验不过直接丢弃;指令执行前检查当前模式,遥控和循迹互斥。丢包本身不可怕,可怕的是半包被当成有效指令执行。

5.5 计圈误差随圈数累积

现象是跑三五圈后停车位置越来越偏。原因是每脉冲距离系数标定不准,或者双轮脉冲直接相加没有考虑转弯时内外轮差。解决上,标定系数时走尽量长的直线取平均;计圈用单侧轮或双轮平均要固定一种,别中途换算法;长距离运行可以加一个“每圈过起跑线传感器校正”的兜底,用光电或电磁特征点重置累计值。没有绝对准的里程计,只有带校正的里程计。

6. 验收与答辩:怎么证明这套小车真的跑明白了

验收这套工程,我一般分三步走,每一步都有可观察的判据,不靠“感觉还行”。

第一步是静态验收:上电不跑车,用串口打印四路电感原始值、基线值、偏差值,手动把车在赛道上左右移动,看偏差是否单调变化、符号是否正确、丢线时是否保持上次值。同时用示波器或逻辑分析仪看 PWM 频率和占空比是否随目标速度变化。这一步能挡掉大半“跑起来才暴露”的问题。

第二步是分功能验收:先关蓝牙、关计圈,只跑循迹,记录直道和弯道的通过情况;再开速度环,对比开环和闭环在同一段赛道的时间差和稳定性;然后单独测蓝牙,逐条指令验证并故意发错误帧看是否被丢弃;最后测计圈,跑固定圈数用卷尺量实际停车位置误差。建议做一张验收记录表:

验收项判据实测
循迹直道不脱线,偏差绝对值小于 200待填
循迹弯道全程不冲出赛道待填
速度环同段赛道时间波动小于 5%待填
蓝牙指令20 次下发全部正确执行待填
定圈停车3 圈停车位置误差小于 30 cm待填

第三步是答辩表达:不要从头念代码,而是按“需求—架构—关键算法—遇到的问题—怎么解决”讲。评委最爱问的是“你这个 PID 参数怎么来的”“丢线怎么处理”“计圈误差怎么控制”,提前把这三个问题的答案准备成一句话加一个数据。演示时先跑完整流程,再针对提问单独展示串口数据和参数修改效果,比单纯跑一圈更有说服力。

从那以后我每次交付这类小车工程,都强制先走一遍静态验收再上赛道,参数改动必须留注释和日期。这套习惯帮我省下了大量“跑起来才发现”的返工时间。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询