做数字电源和电机控制的朋友应该都经历过这种事:PWM驱动桥臂,ADC老老实实放在定时器中断里触发,结果采样回来的电压电流波形边缘全是毛刺,开关管动作瞬间的高频噪声全部灌进了采样结果。我以前在F103上调这个,用的是“触发后延时再开ADC”的土办法,延时长短全凭手感,换个负载波形就歪。后来换了STM32G474,才真正把这个痛点解决掉。
STM32G474这颗MCU,真正值钱的地方不只是170MHz主频,也不只是5路独立ADC,而是它那颗可以做高分辨率输出的HRTIM。把HRTIM的触发事件和ADC采样时刻对齐,就能实现真正意义上的PWM中间时刻采样——也就是让ADC在每个PWM开关周期内,挑最干净的时间点去转换,而不是被动地在开关沿附近硬扛噪声。这篇文章我会把整套方案的原理、CubeMX配置、代码实现和调试经验完整讲一遍,适合正在被开关噪声困扰、或者刚上手G4系列做电源/电机控制的开发者参考。
1. 方案拆解:为什么非要用HRTIM触发ADC
1.1 它到底解决了什么痛点
很多人在做DC-DC电源、电机FOC或者逆变器时,都希望采样输出电流和母线电压。PWM信号驱动半桥,开关管开通关断的瞬间,电流变化率di/dt和电压变化率dv/dt都非常大,这些突变电流流过PCB走线寄生电感、MOSFET寄生电容时,会产生一串高频振荡。这个振荡会通过多种方式耦合到ADC采样电路里,比如地弹噪声、参考电压抖动、采样保持电容上的电荷耦合。
只要ADC的采样保持窗口落在开关沿附近,采到的值就不是“当前时刻的真实物理量”,而是叠加了振铃噪声的信号。特别是电流采样,如果用的是采样电阻或电流互感器,开关沿后几百纳秒内信号都在震荡,这个区间你去触发ADC,每个开关周期采到的噪声相位还不一样,结果就是波形毛刺大、电流环数据跳动。
所谓的PWM中间时刻采样,思路很朴素:把ADC触发点放在PWM周期里距离两个开关沿都比较远的“中间安全区”,让采样保持电容在信号已经稳定的区间捕获电压。这个思路实现的前提是,触发时序必须和PWM边沿严格对齐。
1.2 为什么不推荐只用TIM1/TIM8
可能有人会说,STM32F4的TIM1/TIM8也能用更新事件或比较事件触发ADC,用起来也一样吧。确实可以,但有几个现实问题。
第一,高级定时器的更新事件触发ADC,触发点通常对应PWM周期起点或者终点,这个位置恰恰是开关动作位置,触发间隔和开关沿太近,噪声最脏。除非你用另一个通道的比较事件做触发,但高级定时器的比较事件就是产生PWM的边缘,触发点等于占空比切换点本身,想额外找一个和PWM比较值无关的触发点,资源上比较紧张。
第二,高级定时器PWM本身输出的死区精度、频率精度没问题,但它的比较通道是0~65535的16位计数器,在170MHz主频下,要输出几十kHz的PWM时,计数器周期值会分到一个相对较小的值,想同时兼顾“高PWM频率”和“精确触发点”就受限制。
HRTIM不一样。它本质上是一个带有多个高分辨率单元的复杂定时器,每个子定时器都有一组独立比较寄存器,可以同时做占空比控制事件和ADC触发事件,两者互不干扰。它还能以极小的步进调整事件边沿的相位,对高频电源应用来说,这个能力是决定性的。
1.3 G474的ADC和HRTIM触发链路是怎么接的
G474的ADC是完全独立于定时器的外设,但它有一个关键的属性:规则组可以配置为外部触发启动。触发源里有一类就是HRTIM产生的ADC触发信号,命名一般是HRTIM_ADCTRG1~HRTIM_ADCTRG10。
触发链路是这样的:HRTIM内部的子定时器计数器在运行过程中,每当达到特定的比较值或周期值,产生一个匹配事件,这个事件被HRTIM的输出触发器打包成ADC触发脉冲,送给对应的ADC外设,ADC收到脉冲后启动规则组的扫描转换,转换完一个序列后输出EOC信号,DMA再把数据搬走。
整条链路全部由硬件完成,CPU不用管,时序也是固定的,不会因为中断响应延迟导致采样时刻抖动。这正是电源控制里非常在意的点:ADC触发是异步于软件执行流,时刻由硬件保证。
2. 中间时刻采样背后的关键原理
2.1 开关噪声是怎么混进ADC结果的
我简单画过一个典型半桥电路,用示波器抓过开关节点电压,开通瞬间在几百ns内会出现一个高频振铃,幅值可能叠加一两伏,频率在几十MHz到上百MHz。这种噪声要进入ADC结果,路径很多。
一是地弹。功率回路的大电流从电源地回流到驱动地,PCB上的地平面电位瞬时波动,ADC的参考电压是相对地的,地一抖,转换结果就跟着抖。二是电磁耦合。开关节点和采样线如果平行走线,高频振铃会通过寄生电容耦合到采样保持电容上。三是参考源抖动。如果MCU的VDDA和功率地共用地线,开关噪声会直接污染模拟电源。
所以单纯依赖MCU端软件滤波是治标不治本的,通过调整采样时刻,主动避开噪声频段最活跃的时间窗口,比在软件里加再多滤波都有效。
2.2 中心对齐模式下“中点”到底在哪里
先明确一个概念。PWM有两种常见的计数模式,边沿对齐和中心对齐。边沿对齐的计数器从0数到PERIOD后立刻归零,PWM周期内高电平宽度由比较值决定,中间点就是PERIOD/2附近。这种模式下,要用CMP2额外产生一个“周期中点”触发事件,比较值设为PERIOD/2,这个值不参与PWM占空比控制。
中心对齐则是对称的三角形计数,计数器从0升到PERIOD,再降到0,通常一个完整的PWM周期包含一段上升和一段下降。这种模式下有一个非常巧妙的特点:PWM输出高电平段的中心点正好对应计数器到达PERIOD的时刻,也就是Period事件;低电平段的中心点对应计数器降回0的时刻,也就是Reset/Zero事件。
我实际做20kHz电机电流采样时,就喜欢把HRTIM子定时器配置为中心对齐模式。假设HRTIM时钟170MHz,期望PWM频率20kHz,那么计数周期就是170MHz/20kHz=8500个时钟周期,中心对齐模式下PERIOD设为4250,计数器从0到4250再到0,正好一个完整PWM周期20kHz。占空比由CMP1控制,而ADC触发用Period事件或者Reset事件,这两个事件和CMP1完全独立。
2.3 占空比对安全窗口的影响和触发点选择
这里有一个值得注意的细节:高电平中点和低电平中点不一定都安全,要看占空比。
占空比接近100%时,高电平时间很长,Period事件两侧离开关沿很远,采样非常安全;但低电平时间很短,Reset事件就在两个开关沿之间,几乎没有安全窗口。反之占空比接近0%时,Reset事件更安全,Period事件不安全。
所以实际项目中,触发点应该根据占空比范围动态选择。如果PWM占空比经常在50%上下,Period和Reset都行;如果长期高占空比,用Period事件采样高电平中段;如果长期低占空比,用Reset事件采样低电平中段。
要想获得最大的抗噪窗口,也可以配合比较事件自己计算。比如在边沿对齐模式下,一个PWM周期内,如果把ADC触发点设置在高电平开始后延迟一段时间的位置,可以避开刚开通的振铃;具体延迟多少,可以用示波器观察开关节点振铃时间,算出安全窗口后换算成HRTIM的计数值。
2.4 HRTIM的触发事件如何精确定位
HRTIM每个子定时器都有多个比较寄存器CMP1~CMP4等,还有周期事件、复位事件。配置为ADC触发时,可以在HRTIM的ADC Trigger控制器里指定这些事件源,输出给ADC外部触发。
HRTIM的高分辨率特性在这个场景有实际意义。它的比较事件可以专门用于触发ADC,不受PWM输出比较值的约束,你可以在一个PWM周期内放置多个不同的触发点,比如高电平中点触发一次、低电平中点触发一次,甚至放在死区结束之后某个精确时刻。这种灵活度,传统高级定时器很难做到。
HRTIM内部还有高分辨率延时单元,默认情况下HRTIM时钟170MHz,比较事件的分辨率是一个时钟周期约5.88ns。提一下这个精度已经足够绝大多数应用,真正要ns级精度的场景,可以启用HRTIM DLL校准的高分辨率模式,事件可以再细分成更小的步进,不过配置会复杂一些,常规工程用默认精度就够了。
3. 实操:CubeMX一步步配置
3.1 时钟树的基础配置
新建G474工程时,先把时钟树配好。HRTIM需要比较高的时钟频率,一般直接用系统时钟170MHz作为HRTIM内核时钟。ADC的时钟建议单独分频,G4系列ADC外设本身性能很强,但ADCCLK不能太高,通常我会把ADCCLK配在40MHz左右,这样12位分辨率下单个通道转换时间大约在几百ns级别。
具体在CubeMX的Clock Configuration页面里,把PLL配置为170MHz,然后设置ADCCLK分频,让ADCCLK在40MHz上下,具体值以CubeMX提示合法范围为准。注意ADC参考电压如果内部不好用,外接基准源到VREF+,采样精度会好不少。
3.2 HRTIM的PWM和ADC触发配置
打开HRTIM外设,选择TimerA作为PWM输出定时器。TimerA配置为中心对齐模式PWM,时钟分频可保持默认。周期值设成按上文计算的4250,Period寄存器就是周期计数目标。CMP1寄存器设置占空比比较值。
PWM输出通道选TA1,输出极性可以根据驱动电路决定,通常先用高电平有效。死区根据你的驱动芯片参数来,我习惯先设一个参考值,后续再调整。
关键的ADC触发配置在HRTIM的界面里有ADC Triggers选项。选择一个触发通道,比如ADCTrigger1,使能后,Event Source选择TimerA的Period事件,或者根据占空比情况选择TimerA Reset事件。这样HRTIM会在每个PWM周期产生一个与PWM中点对齐的ADC触发脉冲。
如果占空比范围需要自适应,你也可以在同一个HRTIM里配置两组ADC触发器,一组用Period事件,一组用Reset事件,运行中通过寄存器切换触发源,或者把两个信号分别送给ADC1和ADC2,这在一些高端双采样控制里常用。
3.3 ADC扫描模式和DMA配置
ADC1配置为规则组扫描模式,启用外部触发,触发源选择HRTIM的ADC触发信号。扫描序列里可以加多个通道,比如我加了3个采样通道,序列长度设为3,通道顺序按需求排列。
打开ADC中断里的DMA请求,并开启DMA的循环模式。这里有一个很关键的选项叫Continuous Requests Mode,翻译过来是“连续请求模式”,这个选项要特别留意,必须打开。否则DMA在搬完一轮数据后就会停下来,下一次HRTIM触发不会再产生DMA请求,现象就是只采到一批数据,后面数据不更新。
DMA配置时,外设地址选ADC1的数据寄存器,内存地址指向一个uint16_t数组,传输方向外设到内存,数据宽度半字,内存地址递增。传输运行模式设为循环模式。有些CubeMX版本里DMA事务长度填ADC通道数量,比如3个通道就填3,这样每完成一轮扫描,DMA会产生传输完成中断。
3.4 串口打印配置
串口UART配置为115200、8N1即可。打印有两种方式,阻塞发送和DMA发送。阻塞发送简单,HAL_UART_Transmit一把梭,但是会占用CPU等待,如果打印数据量大,会影响主循环。DMA发送需要自己管理缓冲区,发送完成前不能再次往同一个缓冲区里写新数据,否则数据会错乱。
我实际项目中打印频率不会太高,通常每个采样周期都更新数据,但只有积累一定采样次数后才打印一次,比如每200次采样打印一次,也就是20kHz PWM下每秒打印100帧左右,这个频率不影响实时控制。串口发送可以用DMA,也可以直接阻塞发送,因为间隔足够宽。
4. 代码实现与关键点解析
4.1 初始化顺序和完整框架
初始化顺序上,优先初始化DMA、ADC、UART、HRTIM这些外设,然后先启动ADC的DMA模式,再启动HRTIM定时器,最后启动PWM输出。先后顺序要注意,先启动ADC,再启动HRTIM,可以防止HRTIM启动后第一个触发脉冲来临而ADC还没准备好转换的情况。
核心代码框架大概是这样的:
#define ADC_CH_NUM 3 uint16_t adc_raw[ADC_CH_NUM]; volatile uint8_t dma_flag = 0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_USART1_UART_Init(); MX_ADC1_Init(); MX_HRTIM_Init(); // 先启动ADC的DMA采集 HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_raw, ADC_CH_NUM); // 再启动HRTIM,开始产生PWM和触发信号 HAL_HRTIM_SimpleBaseStart(&hhrtim, HRTIM_TIMERINDEX_MASTER); HAL_HRTIM_SimpleBaseStart(&hhrtim, HRTIM_TIMERINDEX_TIMER_A); HAL_HRTIM_WaveformCounterStart(&hhrtim, HRTIM_TIMERINDEX_TIMER_A); HAL_HRTIM_WaveformOutputStart(&hhrtim, HRTIM_TIMERINDEX_TIMER_A); while (1) { if (dma_flag) { dma_flag = 0; // 打印逻辑也可以放这里 } } }4.2 ADC转换完成回调
每个PWM周期HRTIM触发ADC,ADC扫描完3个通道后,DMA搬运完成,产生传输完成中断,HAL库会调用HAL_ADC_ConvCpltCallback。在这个回调里,你可以设置打印标志,或者直接记录采样次数。
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { // 表示最新一轮采样数据已经在adc_raw数组中 dma_flag = 1; } }4.3 打印显示处理
打印时,把uint16_t原始码值换算为实际电压。如果ADC参考电压是3.3V,分辨率12位,公式为电压=adc_raw*3.3/4095。串口打印可以用printf重定向,也可以自己拼字符串。
我在g474工程里常用重定向到串口,然后:
char buf[128]; snprintf(buf, sizeof(buf), "CH0:%d(%d.%03dV) CH1:%d(%d.%03dV) CH2:%d(%d.%03dV)\r\n", adc_raw[0], adc_raw[0]*3300/4095/1000, adc_raw[0]*3300/4095%1000, adc_raw[1], adc_raw[1]*3300/4095/1000, adc_raw[1]*3300/4095%1000, adc_raw[2], adc_raw[2]*3300/4095/1000, adc_raw[2]*3300/4095%1000); HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, strlen(buf));打印频率由主循环里的计数来控制,不要每周期都打印,否则DMA发送可能来不及。
4.4 关于DMA循环模式和“只能采一次”的现象
我在实际调试中遇到过好几次“ADC只出第一批数据就停了”的情况,基本原因都是ADC的Continuous Requests Mode没有打开。这个模式的含义是,DMA搬运完一次后,是否立刻重新准备接收下一轮转换数据。如果不开,DMA在完成一次传输后就处于空闲状态,即使ADC又完成新的转换,数据也不会被搬运到内存里。
打开这项后,DMA循环搬运,adc_raw数组里的数据每隔一个PWM周期就会刷新一轮。这才是“持续采样”的正确姿势。
5. 常见问题与排查技巧
5.1 数据整体错位怎么办
如果打印出来的各通道数据和实际通道顺序对不上,比如通道1的数据出现在通道0的位置,多半是扫描序列顺序和DMA传输顺序不一致。
G4的规则组扫描是固定顺序的,从Rank1开始,依次转换Rank2、Rank3……转换完一个通道,DMA搬运一次数据到内存,所以adc_raw[0]对应的是Rank1,adc_raw[1]对应Rank2。如果CubeMX里配置Rank顺序和你想采样顺序相反,输出自然反了。检查时直接改CubeMX里的Rank顺序就行,比在代码里手动交换数组更直观。
5.2 HRTIM不触发ADC,数据永远不更新
这个问题也比较常见。先确认HRTIM的计数器有没有跑起来,用调试器看HRTIM寄存器,TimerA的计数器值是否在变化。如果静止不动,检查HRTIM WaveformCounterStart有没有调用。
再检查ADC的外部触发源是否选对了。G4的ADC外部触发列表里,HRTIM相关的触发源有很多个,比如HRTIM_ADCTRG1、HRTIM_ADCTRG2等,而CubeMX里HRTIM的ADCTrigger设置和你给ADC选择的触发源必须对应。比如HRTIM里配置的ADCTrigger1,那ADC1外部触发源要选HRTIM_ADCTRG1,选成2、3都不会触发。
另外一定要检查HRTIM的时间基有没有使能,有些模式下HRTIM子定时器默认没有运行时钟,需要在HRTIM配置里把Timer A的时钟设置为内部时钟170MHz,否则计数器永远不跑。
5.3 采样到的数据噪声还是大,怎么判断是采样点问题
如果你把采样点设置在中间区域,数据依然有毛刺,就要分两步排查。
第一步,观察开关节点波形,确定振铃持续时间。如果振铃能持续数微秒,但PWM周期本身只有50us,中间区域还是有足够安全时间;如果振铃持续超过一半的周期,那采样点自然没办法完全避开,只能从硬件上削减振铃,比如增加栅极电阻、优化PCB布局、在采样输入端加RC滤波。
第二步,利用HRTIM的灵活性,动态改变触发点位置,观察ADC数据的标准差变化。你可以同时配置两个ADC,一个用Period事件触发,一个用Reset事件触发,对比不同时刻采样数据的波动情况。哪个数据波动小,说明哪个位置更“干净”。这个实验用代码就能做,非常方便。
5.4 串口DMA发送第二次就卡死
串口用DMA发送时,有一个高频坑:上一次发送还没有完成,你又调用了HAL_UART_Transmit_DMA,就会返回HAL_BUSY,后续打印全部失效。严格来说这不影响采样,但会让人误以为整个工程挂了。
解决方法是维护一个发送完成标志。在HAL_UART_TxCpltCallback中置位标志,只有上一次发送完成后才允许发送下一帧。
volatile uint8_t tx_done = 1; void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { tx_done = 1; } }打印前检查tx_done,如果为1则开始发送并清零标志,否则跳过本次打印。虽然会丢几帧,但保证了系统不死。
5.5 硬件侧最容易忽略的两件事
软件方案再好,硬件底板太差也会白搭。采样电路在PCB布局上至少要注意两点:一是功率地、模拟地单点连接,ADC采样回路尽量短,采样电阻和MCU的VSSA不要隔太远;二是ADC输入引脚上保留RC滤波,一般100Ω电阻加1nF电容组合,既能滤高频,又不会明显影响信号带宽。RC的截止频率按你实际被采信号的最高频率来算,比如电流环带宽10kHz以内,RC截止频率放到几百kHz,影响很小,但振铃衰减明显。
还有个容易被忽略的细节是,ADC参考电压引脚附近一定要放去耦电容,至少一个1uF和一个100nF,紧贴引脚。参考电压不稳,中间时刻采样做得再好,转换结果的信噪比也会受限。
最后说一个我自己的习惯。所有G4的电源类项目,我都不会在主循环里做任何改变采样时序的操作。HRTIM触发ADC、DMA搬数据、串口打印,三个环节相互独立,HRTIM负责时序,DMA负责搬运,主循环只做显示和逻辑,这样逻辑怎么写都不会破坏采样时刻。这个习惯帮我避免了很多怪问题,建议你也可以这么试。