STM32电磁寻迹与电磁炮二合一控制实战解析
2026/9/16 16:50:44 网站建设 项目流程

简介:这份STM32电磁炮与循迹小车工程源码,适合电子工程、嵌入式开发及智能车竞赛方向的读者参考。项目以STM32微控制器为核心,将寻迹小车的传感器采集、路径识别、电机PID调速,与电磁炮的线圈电流精确控制整合在同一套工程中,覆盖GPIO、定时器、ADC、PWM、DAC、外部中断、串口与多任务调度等关键点。压缩包共244个文件,以C语言源码和头文件为主,包含Keil工程配置、编译中间文件、hex烧录文件,及调试辅助的map/lst/htm记录,整体约8.19MB,目录规整,便于按模块对照学习。目前已有1573人下载学习。通过源码可重点研习PWM/DAC调流实现电磁线圈发射控制、PID闭环循迹算法、中断服务函数设计、过流与过热保护逻辑,以及完整的嵌入式工程组织方式;无论备赛智能车还是入门STM32实战,都是可直接参考的完整样例。

1. STM32电磁寻迹与电磁炮:同一个单片机上的两套控制逻辑

这个标题把两个经常出现在大学生电子设计竞赛和课程设计里的东西放到了一起:电磁循迹小车和电磁炮。很多人第一反应是“这两个项目各做各的,拼在一起有什么意义”,实际恰恰相反。电磁循迹小车做的是连续控制,传感器以几十微秒的周期读电感电压,实时调整转向和速度;电磁炮做的是离散控制,按键触发之后升压、充电、等待电压达标、放电,整个过程是状态机。把两套逻辑放在同一个STM32上跑,真正要解决的其实是任务调度和资源复用,循迹小车的那套差比和算法并不会更长,真正决定车跑得稳不稳的是信号链路从电感到ADC再到PWM的整体延迟。

这篇写的是这种组合项目的完整落地路径:电磁寻迹的传感器怎么选、电感怎么摆、差比和怎么调,到电机用什么方式驱动,再到电磁炮的升压和触发电路怎么用单片机接,最后落到一个快速验证传感器状态的调试技巧。适合手里已经有STM32最小系统板、想自己做一辆电磁循迹车然后把电磁炮作为第二执行负载的开发者。整个过程不依赖某个特定的成品源码头文件,直接面向寄存器和标准外设库来说明,放到HAL库也一样用。

2. 电磁寻迹的传感链路:电感布局、谐振频率和检测方案

2.1 工字电感与并联电容:为什么电磁循迹要用LC谐振

电磁循迹车跑的路面下埋的是一条通有20kHz左右交变电流的导线,导线周围会产生交变磁场。车上检测这个磁场的方式不是直接用霍尔元件,而是用工字电感。工字电感在交变磁场中会产生感应电动势,电动势的大小和电感与导线的相对位置有关,这就是最原始的循迹信号来源。

单个电感直连ADC的话,感应的信号只有毫伏级别,而且环境噪声会直接叠加进去,非常难处理。常见做法是给工字电感并联一个谐振电容,让电感和电容组成LC并联谐振回路,谐振频率对准发射导线的载波频率。在谐振点上,回路阻抗最大,感应电压会被抬升到ADC可以稳定采样的水平。这个选型是整个电磁寻迹项目里最容易被忽略但影响最大的环节。

电感量和电容量的搭配决定了谐振频率。公式是 f = 1 / (2π√(LC))。如果发射频率是20kHz,电感选10mH,算出谐振电容的经验范围是6.8nF到10nF。实际装车之后要微调,因为工字电感的实际电感量会受磁芯、绕线分布电容和周围金属件影响,比标称值偏大或偏小都正常。

我的习惯是先用LCR表实测每个电感的实际电感量,再反推电容值,这样谐振点更准。没有LCR表就用示波器看信号幅度,把电容从小到大换,信号幅度最大的那组就是接近谐振的组合。3D打印的外壳里如果嵌了金属螺母,位置不要挨着工字电感,涡流会让感应信号畸变。

2.2 比较器阈值方案与ADC幅值方案的选择

传感器的输出有两种送进单片机的方式。

第一种是电感信号经过运放整流滤波后,接到比较器上,比较器输出高低电平,单片机读数字输入。这种方案的优点是主控完全不参与信号解算,响应极快,程序里只有一个GPIO读操作。缺点是丢失了信号强度信息,只能判断“有没有越过导线”,离导线的距离、导线在哪一侧,都判断不出来。窄赛道直道可以跑,但S弯和十字交叉基本没法处理。

第二种是电感信号经过精密整流和高频滤波后变成直流电压,直接接STM32的ADC引脚,程序里把几路电压数字化。这个方案能保留信号的强度信息,用差比和算法算出导线相对于车体中线的偏差值。缺点是对ADC采样稳定性和参考电压有要求,如果直接用VDD做参考,电池电压波动会直接反映在采样值上。

做电磁循迹车推荐用ADC方案,哪怕是入门版本也要用ADC。比较器方案只适合那种“只要车不冲出去就行”的最低要求演示。ADC方案里的三个信号在stm32上对应三路ADC通道,采样周期要和DMA循环配合起来,不要用阻塞式逐通道读取,否则电机PWM更新会被传感器采样卡住。

2.3 电感排布与差比和的物理意义

三电感是电磁循迹的最低配置。左右两个电感隔着车体中心线对称安装,中间电感装在正前方。驱动导线在当前时刻的正下方时,中间电感感应电压最高,左右两个电压不对称,这种不对称性就是转向的依据。

差比和公式写为:

int16_t error = (100 * (left_adc - right_adc)) / (left_adc + right_adc + middle_adc);

注意这里用的是左减右,不是右减左。方向取决于你的电机安装朝向和转向控制环路的正负反馈极性。代码里这个error是后续PID控制器的输入,正值表示车身偏左还是偏右,由你的电感安装方向和转向定义决定,第一次跑之前需要实际验证一下极性。

这个公式分母加上中间电感的值,好处是两个场景:一是左右电感同时进入强磁场,分子变大但分母也变大,输出不会爆炸;二是某个电感接近饱和拐点、数值变化剧烈时,差比和依然保持相对平滑。分母里的middle_adc不建议去掉,没有它的话,车正对导线时左右电压都很高,差比和反而会不稳定。

电感的横向间距,三轮车常见的是10cm到14cm。间距太小,差比和输出范围窄,直线段的灵敏度不够;间距太大,过弯时外侧电感很容易丢信号,进入饱和区,输出就不再线性。参数表格如下:

参数推荐值说明
发射频率20kHz(也可做28kHz)和LC谐振频率保持一致
工字电感10mH,直径7mm兼顾灵敏度和抗饱和能力
谐振电容6.8nF~10nF实测调整,以信号幅度最大为准
电感间距10cm~14cm三轮车架常用范围
电感离地高度10mm~25mm越低信号越强,但越容易被路面电磁噪声干扰

3. 在STM32上把电磁寻迹跑通:从ADC采集到电机PWM

3.1 用ADC+DMA循环采集多路电感信号

先把传感器路径整清楚,才能写程序。三路电感电压进入单片机之前,经过整流变成直流,直接连到STM32的ADC引脚,比如PC0、PC1、PC2。程序里用ADC的扫描模式配合DMA循环传输,refills三个通道的采样值到数组里。下面是基于标准外设库的初始化片段:

ADC_InitTypeDef adc_init; DMA_InitTypeDef dma_init; uint16_t adc_buf[3]; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); dma_init.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma_init.DMA_MemoryBaseAddr = (uint32_t)adc_buf; dma_init.DMA_DIR = DMA_DIR_PeripheralSRC; dma_init.DMA_BufferSize = 3; dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable; dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma_init.DMA_Mode = DMA_Mode_Circular; dma_init.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &dma_init); DMA_Cmd(DMA1_Channel1, ENABLE); adc_init.ADC_Mode = ADC_Mode_Independent; adc_init.ADC_ScanConvMode = ENABLE; adc_init.ADC_ContinuousConvMode = ENABLE; adc_init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; adc_init.ADC_DataAlign = ADC_DataAlign_Right; adc_init.ADC_NbrOfChannel = 3; ADC_Init(ADC1, &adc_init); ADC_RegularChannelConfig(ADC1, ADC_Channel_10, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_11, 2, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_12, 3, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE);

这段代码做的事情是让ADC1自动循环扫描三个通道,每次转换完结果由DMA搬运到adc_buf数组,环形传输,所以主循环拿到的始终是最新数据。采样时间用较长的239.5周期,在20kHz的电磁信号环境下,能起到硬件低通滤波的效果。ADC_RegularChannelConfig里的三个通道要和你实际接线的引脚对应,别照抄通道编号,这个项目最常见的接线错误就是代码和电路图通道号错位。

主循环里直接读adc_buf,不用等待转换完成位。读取速度远快于20kHz的载波频率,对差比和计算来说完全够用。如果要进一步提升稳定性,可以对adc_buf做滑动平均,取最近4次采样的均值再做差比和,开销只有三次加法和一次移位。

3.2 电机驱动与转向控制的最小实现

循迹小车电机驱动常用TB6612或者DRV8833,控制方式都是PWM调速加方向引脚。STM32上用一个定时器的两个通道输出PWM,分别控制左右电机。转向不需要额外舵机,差速就能转弯。

转向控制用位置式PD即可,积分项在电磁循迹里不是必须的,因为差比和本身是一个稳定的偏差信号,不会像编码器测速那样有静差问题。加积分容易造成弯道处转向过量。PD参数表按这个范围作为起点调试:

参数起步值调整方向
Kp3.0~5.0直道抖动大就减小
Kd8.0~15.0弯道甩尾就增大
基础PWM25%~35%必须能让车克服静止摩擦起步

核心控制代码放在定时器中断里,中断频率设1kHz,每毫秒执行一次,保证控制周期稳定,不受主循环里电磁炮状态机的影响:

void TIM2_IRQHandler(void) { static int16_t last_error = 0; int16_t error, p, d, turn, motor_l, motor_r; if (TIM_GetITStatus(TIM2, TIM_IT_Update) == RESET) return; error = get_track_error(); p = KP * error; d = KD * (error - last_error); turn = p + d; last_error = error; motor_l = BASE_SPEED - turn; motor_r = BASE_SPEED + turn; if (motor_l > PWM_MAX) motor_l = PWM_MAX; if (motor_r > PWM_MAX) motor_r = PWM_MAX; if (motor_l < 0) motor_l = 0; if (motor_r < 0) motor_r = 0; set_motor_duty(motor_l, motor_r); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); }

get_track_error()内部执行差比和公式,motor_l和motor_r是左右轮的PWM占空比。BASE_SPEED减去turn是内侧轮减速,加上turn是外侧轮加速,实现差速转向。这套逻辑如果发现转向反了,把motor_l和motor_r的表达式交换,或者在差比和公式里把left减right换成right减left。

控制中断的优先级要有讲究。让此时基中断优先级高于串口和ADC事件,低于任何一个不可屏蔽的系统异常。同时电磁炮的触发相关操作,如果涉及GPIO翻转,不要放在比这个更高的中断优先级里做长延时,只需要翻转引脚,具体时序交给外部硬件去执行。

3.3 串口打点与标定

跑起来之前先做一次标定。把车悬空,手机横着缓慢扫过电感下方,模拟从左侧到右侧的移动,同时用串口把left_adc、right_adc、middle_adc和差比和error打出来。正常现象是:电感对称时error接近0,扫到右边时error平稳变化到正数或负数。

这一步能发现三个问题:某一路ADC读数恒为4095(接线断路或通道配置错)、差比和突变跳变(谐振电容偏了导致信号进入非线性区)、数值在某个位置停留不变化(运放输出饱和,需要调低放大倍数或增大电感离地高度)。标定通过再上赛道,不然上赛道之后拿激光笔和示波器去查信号链路非常浪费时间。

void send_debug(void) { printf("L:%d R:%d M:%d Err:%d\r\n", adc_buf[0], adc_buf[1], adc_buf[2], get_track_error()); }

dcb上跑起来之后串口打印会占用不少时间,调试完毕之后把send_debug关掉,只保留get_track_error。不关也行,但波特率115200,持续打印会有约3%的周期占用,对1kHz控制中断影响不大,可读性和稳定性之间做个取舍。

4. 电磁炮单片机控制:升压、电容充放电与触发时序

4.1 电磁炮的功率链路和单片机接口

电磁炮在课程设计语境下,通常是电容储能放电型。用升压电路把电池电压升到数十伏甚至上百伏给电容充电,储到设定电压后,通过可控硅或者IGBT瞬间放电,电流流过线圈产生脉冲磁场,把铁磁性弹丸加速推出去。

STM32在这一环里做的不是功率变换本身,而是状态管理。它控制三点:升压电路的开关(PWM或者使能脚)、电容电压的采样(电阻分压接ADC)、放电管的触发(GPIO输出脉冲给驱动电路)。

电路基本结构是:电池 → 升压模块 → 限流电阻 → 储能电容;电容两端并接分压电阻网络 → ADC通道;电容正极 → 放电线圈 → 可控硅阳极,可控硅阴极接地,控制极接单片机的触发驱动。RCC配置里要额外把DMA1或DMA2的时钟打开,ADC采样通道和循迹传感器通道分开配置。

安全设计这里必须注意:放电管关断之后,储能电容可能还留存电荷。需要并联一个泄放电阻,典型取100kΩ到220kΩ,功率选1W以上。这个电阻让电容在上电后10秒内放到安全电压以下,防止误触线圈造成伤害。

4.2 电容电压采集与充放电状态机

电容电压不能用单片机直接测,高压会把ADC引脚击穿。用电阻分压,比如两个电阻按1:100分压,电压降到原来的百分之一。选择电阻时要注意分压电阻本身的耐压和功耗,至少两个电阻串联再分压,不能只靠一个分压。

电压ADC采样和循迹电感的ADC采样要分开看待,因为它们的时间尺度完全不同。循迹用DMA循环搬运,几乎每个控制周期都有新数据。电容电压只需每20ms~50ms采样一次,用定时器触发单次转换即可。状态机代码:

typedef enum { GUN_IDLE, GUN_CHARGING, GUN_READY, GUN_FIRE, GUN_DUMP } gun_state_t; gun_state_t gun_state = GUN_IDLE; void gun_update(void) { uint16_t v_adc = gun_get_voltage_adc(); uint32_t v_mv = (uint32_t)v_adc * 3300 / 4096 * 100; /* 乘分压比 */ switch (gun_state) { case GUN_IDLE: if (fire_button_pressed()) { gun_state = GUN_CHARGING; boost_enable(1); } break; case GUN_CHARGING: if (v_mv >= TARGET_VOLTAGE_MV) { boost_enable(0); gun_state = GUN_READY; } if (charge_timeout()) { gun_state = GUN_DUMP; } break; case GUN_READY: if (fire_button_released()) { gun_state = GUN_FIRE; trigger_fire(); gun_state = GUN_IDLE; } break; case GUN_FIRE: /* 触发后立即回到IDLE */ break; default: boost_enable(0); gun_state = GUN_IDLE; break; } }

注意这个状态机里每个状态转移都有明确条件,没有用延时阻塞。充电过程中boost_enable是拉高一个GPIO还是打开PWM输出,取决于升压模块的使能接口类型。带软启动的升压模块通常给一个高电平使能即可。触发后电容电压会瞬间跌到很低,所以触发完立刻回到IDLE,不要停在READY判断电压。

TARGET_VOLTAGE_MV根据电容耐压和线圈参数来确定,不要超过电容标称耐压的80%。同时要在充电路径上加限流,否则电容短路或者误接时,升压模块会被瞬间拉到极限甚至损坏。

4.3 与循迹任务共享STM32的实时性协调

两个任务在同一颗主控上跑,最容易出问题的不是计算量,而是两个不同的实时性要求互相挤占。电磁循迹的控制中断固定1kHz,不能因为主循环在做充电电压判断就被拖延。解决办法是分层:把循迹控制放定时器中断里,把电磁炮状态机放主循环里。因为电磁炮状态机本身不要求微秒级响应,几十毫秒的延迟无关紧要。

而电磁炮的触发输出是整个项目里唯一要求硬实时的地方。触发信号由状态机置位,但实际引脚翻转放在定时器中断里执行一次,确保可控硅的触发时刻相对于电机PWM周期是确定的。以下代码放在和循迹控制相同的中断里,每毫秒检查一次标志位:

uint8_t fire_request = 0; void TIM2_IRQHandler(void) { if (fire_request) { fire_request = 0; GPIO_SetBits(GUN_TRIGGER_GPIO, GUN_TRIGGER_PIN); delay_us_pin(20); /* 触发脉冲宽度 */ GPIO_ResetBits(GUN_TRIGGER_GPIO, GUN_TRIGGER_PIN); } /* 循迹PD控制的代码也在这里 */ }

触发脉冲宽度通常10到20微秒就足够,太宽会使可控硅控制极功耗增大。这段里面用delay_us_pin而不是普通Delay,因为标准库自带的微秒延时函数是靠死循环延时的,问题在于编译器开O2优化之后延时时间会缩短,需要校准。实际工程中更稳妥的做法是把这个触发操作外移到一个专用的单次定时器里,设定定时器延时到20微秒后产生更新事件,在中断里复位引脚。忌用毫秒级的软件延时,那会把循迹控制周期彻底打乱。

电容充电期间也并没有占用任何单片机中断资源,升压模块完全依靠硬件闭环工作,MCU只在需要判断“是否达到目标电压”时读一次ADC。所以整车的转向性能不会因为带了电磁炮模块而明显下降,这是这种主控方案的最大优势。

5. 调试技巧:用串口看差比和的分段特征,快速定位丢线问题

5.1 差比和输出幅值的三个区间

把车放在赛道上,上电之后串口打印差比和数值,观察它在不同位置的输出。正常信号应该呈现三个分段:车在导线正右方时输出是较大的饱和值,正上方时输出稳定在0附近,正左方时输出是反向饱和值。这三个分段的中间过渡区越陡峭,说明传感器对位置的灵敏度越高,但过渡区一般不希望过窄,否则PID一碰就发散。

过渡区的数据特征如果出现断崖式跳跃,比如从+50直接跳到-50,中间没有连续值,说明两侧电感的感应范围重叠不足,两个电感之间的间距过大,或者谐振频率没对准,左右电感的灵敏度不一致。把采样到的左路和右路分别打印出来,两侧同时移动时数值是否同步升降,立刻能看出差异。这两行串口数据比任何理论分析都有用。

5.2 丢线判定和信号饱和的边界

差比和的分母降到很低,说明车已经离线远,三路信号都弱了。此时PID会把转向输出打到极限,常见行为是车身猛打方向冲出去。解决办法不是调Kp,而是给丢线状态加一个独立逻辑。

int16_t get_track_error(void) { uint32_t sum = adc_buf[0] + adc_buf[1] + adc_buf[2]; if (sum < LOST_THRESHOLD) { return (last_error > 0) ? LOST_TURN_RIGHT : LOST_TURN_LEFT; } return update_last_error(calculate_error()); }

丢线时让转向保持上一次的有效方向而不是归零,这是电磁寻迹车上最实用的一招。常见错误是丢线时直接返回0,车会沿直线冲出赛道。LOST_THRESHOLD需要和运放的静态输出电压配合调,推车横扫赛道时观察串口数据确认最小值,留出20%的裕量。

最后一层的验证方法是,把示波器的两个探头分别接左右电感的整流输出,手动推车过弯,观察波形峰值变化是否平滑。如果某一侧波形到弯道顶点出现削顶,说明运放饱和,需要减小放大倍数。这个检查和调PID参数同样关键,削顶会让ADC读丢信息,再好的控制参数也救不回来。

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

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

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

立即咨询