STM32实战如何用定时器触发ADC实现SimpleFOC电流检测附完整代码最近在折腾无刷电机控制特别是基于SimpleFOC开源框架的项目发现电流环的精度和实时性直接决定了整个系统的性能上限。很多朋友在移植SimpleFOC时卡在了电流检测这一环——软件触发ADC采样太慢时序难以对齐PWM导致电流波形失真闭环效果大打折扣。其实STM32内置的硬件联动功能比如用定时器直接触发ADC正是解决这个痛点的利器。这篇文章我就结合自己最近在一个小型云台电机项目上的实战经验聊聊如何从硬件配置到代码实现搭建一个稳定可靠的定时器触发ADC电流采样方案让你在SimpleFOC上实现媲美商业驱动器的电流控制效果。1. 理解定时器触发ADC的核心价值与SimpleFOC需求在无刷电机BLDC/PMSM的磁场定向控制FOC中电流检测是闭环控制的基石。SimpleFOC算法需要实时、同步地获取两相电流通常是Ib和Ic用于计算Clarke和Park变换进而生成正确的电压矢量。如果采样时刻与PWM开关时刻不同步采样的电流值可能包含巨大的开关噪声甚至是母线电压突变时的毛刺这会让整个电流环变得不稳定。软件触发ADC的局限性时序抖动在中断服务程序里手动启动ADC转换会受中断响应延迟、其他高优先级中断的影响采样点无法精确固定在PWM周期的特定时刻。CPU开销大高频的ADC采样通常与PWM频率同步如20kHz会持续占用CPU资源。同步性差难以确保在PWM开关管处于“开”状态的中点进行采样以获取准确的相电流平均值对于采用单/双电阻采样方案尤其关键。相比之下定时器触发ADC将整个采样流程硬件化提示STM32的定时器可以配置在特定事件如更新事件UP、比较匹配事件时自动产生一个触发信号TRGO给ADCADC接收到此硬件信号后立即启动转换无需CPU干预。这种机制实现了纳秒级的精确同步和零CPU开销的采样触发。对于SimpleFOC的电流检测接口我们需要提供两个核心函数一个用于硬件初始化配置_configureADCInline另一个用于读取转换结果_readADCVoltageInline。我们的目标就是用定时器触发ADC的硬件链路来高效、精准地服务于这两个函数。2. 硬件外设的协同配置以STM32CubeMX为例我手头的项目基于STM32G4系列但配置思路对于F1、F3、F4、H7等系列大同小异。核心是让定时器1生成PWM和ADC1 ADC2采样电流联动起来。2.1 定时器TIM1的配置要点TIM1作为高级控制定时器是生成三相PWM波的核心。我们需要它同时充当ADC的“指挥家”。关键配置步骤时钟与预分频根据你的电机控制频率设置定时器时钟。例如若期望PWM频率为20kHz系统时钟为170MHz则预分频器PSC可设置为170MHz / (20000 * 计数器周期)。将计数器周期ARR设为固定值如999这样PWM频率约为170MHz / (1000 * 预分频)。PWM生成模式选择“PWM模式1”或“PWM模式2”并启用对应通道CH1, CH2, CH3的输出设置为互补输出如果使用半桥驱动或独立输出。触发输出TRGO设置这是连接ADC的桥梁。在“主模式”Master Mode或“触发输出”设置中将“主模式选择”Master Mode Selection设置为“更新事件”Update Event。这意味着每次定时器计数器溢出或更新时都会从TIM1的TRGO引脚输出一个触发脉冲。为什么是更新事件在中心对齐计数模式下更新事件发生在计数器从ARR值向下计数到0的瞬间或反之这个时刻通常对应PWM波形的“中点”是采样相电流的理想时刻能有效避开开关噪声。下面是一个典型的CubeMX中TIM1参数配置示意表格参数项配置值说明时钟源Internal Clock使用内部时钟预分频器 (PSC)84依据系统时钟计算得出计数器模式Center Aligned Mode 1中心对齐模式利于对称PWM生成计数器周期 (ARR)999决定PWM分辨率及频率自动重载预装载Enable确保参数更新在下次更新事件生效PWM生成模式PWM Mode 1标准PWM模式CH极性High有效电平为高主模式选择Update Event关键将更新事件作为TRGO输出源配置完成后生成的代码会自动将TIM1的TRGO信号路由到内部ADC触发线。2.2 ADC的注入组与外部触发配置为了同步采样两路电流我们需要使用ADC的注入组功能。注入组可以打断常规转换序列以更高的优先级执行一组预定义的转换非常适合对实时性要求极高的电机电流采样。ADC1采样Ib和ADC2采样Ic的并行配置启用注入组在ADC配置中找到“Injected Conversion”并启用它。设置注入通道为注入组添加通道。例如将ADC1的注入通道1Rank1分配给采样Ib的GPIO如PA0ADC2的注入通道1分配给采样Ic的GPIO如PA1。设置合适的采样时间Sample Time对于电流采样通常1-2个ADC时钟周期即可但若信号源阻抗较大需适当延长。配置外部触发源在“Injected Conversion”设置中将“External Trigger Source”设置为“Timer 1 Trigger Out event”。这告诉ADC等待来自TIM1 TRGO的硬件信号。触发边沿选择“上升沿触发”Rising Edge。当TIM1的更新事件发生时TRGO产生一个上升沿ADC捕获到这个边沿后立即启动注入组转换。扫描模式与间断模式对于注入组单次转换通常禁用扫描模式Scan Conversion Disable。确保“Discontinuous Conversion Mode”也被禁用我们期望每次触发都完成整个注入序列虽然这里只有1个通道。一个容易忽略的细节ADC时钟与同步。如果使用双ADCADC1和ADC2同步采样需要配置ADC工作在“双重模式”Dual Mode下并由ADC1作为主设备ADC2作为从设备这样同一个触发信号可以同时启动两个ADC的转换。在我们的场景中由于SimpleFOC库的接口设计也可以将两个ADC独立配置由同一个TIM1 TRGO触发在中断中分别读取这在代码层面更灵活。3. 程序实现中断、读取与SimpleFOC接口对接硬件配置好后我们需要编写代码来“激活”这条硬件链路并将数据提供给SimpleFOC库。3.1 初始化与中断使能在_configureADCInline函数中此函数由SimpleFOC的InlineCurrentSense类调用我们需要完成以下几件事保存参数存储采样电阻、运放增益等换算参数。启动ADC注入组并使能中断让ADC在每次转换结束后产生中断以便我们及时读取数据。void* _configureADCInline(const void* driver_params, const int pinA, const int pinB, const int pinC) { // pinA, pinB, pinC 在此硬件方案中仅作为标识实际GPIO已在CubeMX中配置 _UNUSED(driver_params); _UNUSED(pinA); // 可能未使用 // 创建并初始化参数结构体 Stm32CurrentSenseParams* params new Stm32CurrentSenseParams; // 假设我们只用B、C相对应ADC1和ADC2 params-pins[0] 0; // 未使用 params-pins[1] pinB; // 标识符例如2 params-pins[2] pinC; // 标识符例如3 // 计算电压转换系数Vref / ADC分辨率 params-adc_voltage_conv 3.3f / 4096.0f; // 对于3.3V参考电压和12位ADC // 关键步骤使能ADC的注入转换结束中断并启动注入组 // 注意HAL_ADCEx_InjectedStart会启动注入组并等待外部触发 __HAL_ADC_ENABLE_IT(hadc1, ADC_IT_JEOC); // 使能ADC1注入组转换结束中断 HAL_ADCEx_InjectedStart(hadc1); __HAL_ADC_ENABLE_IT(hadc2, ADC_IT_JEOC); // 使能ADC2注入组转换结束中断 HAL_ADCEx_InjectedStart(hadc2); // 启动定时器1它将开始产生PWM和触发信号 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_3); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1); // 如果需要互补通道 return (void*)params; }3.2 中断服务程序中读取ADC值当ADC注入组转换完成时会进入相应的中断服务程序。我们需要在这里快速读取转换值并存储到全局变量中供_readADCVoltageInline函数查询。// 定义全局变量存储原始ADC值 volatile uint32_t adc_raw_ib 0; volatile uint32_t adc_raw_ic 0; // ADC1和ADC2的注入组转换结束中断回调函数在stm32g4xx_it.c中重写或使用HAL回调 void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 读取ADC1注入组通道1的转换结果 adc_raw_ib HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1); } else if (hadc-Instance ADC2) { // 读取ADC2注入组通道1的转换结果 adc_raw_ic HAL_ADCEx_InjectedGetValue(hadc, ADC_INJECTED_RANK_1); } // 注意这里没有复杂的计算仅快速读取确保中断执行时间最短。 }注意确保在CubeMX中开启了ADC1和ADC2的全局中断并且其中断优先级设置合理通常高于SysTick等系统中断但低于紧急故障保护中断。3.3 实现SimpleFOC电流读取接口最后我们需要实现_readADCVoltageInline函数它将存储的原始ADC值转换为电压值。float _readADCVoltageInline(const int pin_index, const void* cs_params) { uint32_t raw_adc 0; const Stm32CurrentSenseParams* params (Stm32CurrentSenseParams*)cs_params; // 根据传入的pin_index对应初始化时的pinB, pinC返回相应的ADC值 switch(pin_index) { case 1: // 对应pinA本例未使用 break; case 2: // 对应pinB即Ib raw_adc adc_raw_ib; break; case 3: // 对应pinC即Ic raw_adc adc_raw_ic; break; default: break; } // 将ADC原始值转换为电压值 raw * (Vref / 2^分辨率) return (float)raw_adc * params-adc_voltage_conv; }至此硬件触发、中断读取、接口提供的完整链路就打通了。SimpleFOC库会周期性地调用_readADCVoltageInline来获取最新的电流采样电压然后结合采样电阻和运放增益计算出实际的相电流。4. 调试技巧与常见问题排查配置完成后上电测试可能不会一次成功。下面分享几个关键的调试步骤和踩过的坑。调试第一步验证PWM和触发信号使用示波器观察电机驱动板的PWM输出在未接电机时确认频率和占空比符合预期。更关键的是探测ADC的触发输入引脚如果芯片有引出或者使用STM32的调试功能如ITM或SEGGER SystemView来验证TIM1的更新事件是否定期发生。可以在更新事件中断里翻转一个GPIO来辅助观察。调试第二步验证ADC是否被触发在HAL_ADCEx_InjectedConvCpltCallback中断回调函数里设置一个断点或翻转一个GPIO调试引脚。如果这个GPIO能随着PWM频率规律地翻转说明ADC确实被定时器成功触发并完成了转换。如果中断不进检查CubeMX中ADC的“注入转换结束中断”是否使能。NVIC里对应ADC的中断是否开启并设置了合适的优先级。HAL_ADCEx_InjectedStart是否在_configureADCInline中被正确调用。调试第三步校准与读数验证在电机静止时给电流采样电路输入一个已知的直流电压例如用可调电源模拟采样电阻两端的压降。通过SimpleFOC的current_sense.getPhaseCurrents()函数读取电流值或者直接打印adc_raw_ib和adc_raw_ic看其是否与输入电压成比例变化。注意偏置电压运放和ADC本身可能存在零点偏移。可以在电机不通电、电流为零时读取一组ADC值作为“零偏”Offset在软件中减去这个值。常见问题清单电流读数噪声大、跳动剧烈检查PCB布局电流采样走线是否远离功率回路是否使用了差分走线和适当的滤波电容。尝试增加ADC的采样保持时间让ADC有更充分的时间对信号采样。在软件中对ADC读数进行简单的滑动平均滤波但要注意引入的延迟。采样值与PWM占空比不同步确认定时器是否工作在中心对齐模式以及TRGO源是否设置为“更新事件”。检查ADC触发边沿设置是否正确。双ADC读数不同步如果要求Ib和Ic严格同步采样需配置ADC1和ADC2工作在“双重同步模式”下并由ADC1的JEOC事件触发ADC2。这需要在CubeMX的ADC“多模式”设置中进行配置代码会更复杂一些。如果对同步性要求不极端当前独立触发、独立中断读取的方案在大多数应用中是足够的。一个进阶优化为了减少中断延迟和抖动对电流环的影响可以考虑使用DMA来搬运ADC数据。将ADC配置为注入组转换完成后通过DMA自动将数据搬运到指定的内存数组双缓冲或循环缓冲然后在主循环或一个定时中断中安全地读取这些数据。这样可以将中断服务程序简化到只处理DMA传输完成中断甚至完全不用ADC中断。整个移植和调试过程其实就是将硬件特性与软件框架进行精准匹配的过程。当你看到串口打印出的三相电流波形平滑且与施加的负载相匹配时那种成就感是实实在在的。接下来你就可以在SimpleFOC Studio里愉快地调试PID参数让电机安静、有力、精准地转动起来了。