☰
STM32G474 HRTIM触发ADC同步采样:攻克开关电源采样噪声
2026/10/3 8:07:09 网站建设 项目流程

做双向DC-DC或者BLDC电机控制的时候,我估计不少人都遇到过这个现象:电流环明明算得好好的,示波器抓ADC输出却总在开关边沿附近跳出一堆毛刺,数值忽高忽低,看着像随机噪声,其实是采样点刚好砸在了开关管导通/关断的振铃窗口上。我这次用STM32G474把HRTIM当作触发源,带动三路ADC同步采样,再用DMA把结果搬到内存、串口打印出来,算是把这个问题彻底捋顺了,整个过程值得整理成一篇笔记。

先说结论:G474的HRTIM加ADC触发链路,配合精心设计的中间时刻采样点,能把开关噪声的影响压到很小,同时采到的电感电流还在纹波中点附近,对于峰值电流控制和平均电流控制都有参考意义。这篇文章不打算讲虚的,直接从为什么HRTIM干这个活最合适、中间时刻怎么算、CubeMX怎么配、代码怎么写、实测遇到什么坑这五条线展开,想把这一套链路复现出来的朋友,照着往下走就行。

1. 为什么非要用HRTIM:普通定时器和它差距在哪

1.1 高分辨率时钟背后的采样精度意义

STM32G474的HRTIM(高分辨率定时器)最直观的优势是时钟分辨率。系统主频跑到170MHz时,HRTIM的计数时钟可以倍频到680MHz(主频的4倍),对应的分辨率大约1.47ns。这是个什么概念?我在早期方案里试过用普通高级定时器TIM1的中心对齐模式触发ADC,它挂在170MHz的APB总线上,计数周期就是5.88ns。表面看5.88ns和1.47ns好像都够快,但在高频PWM场景下这个差距会被放大。比如开关频率500kHz,周期2us,普通定时器量化误差5.88ns,相位误差约0.3%,看起来也不大;可一旦涉及多路ADC的触发延时一致性,每路转换启动时刻可能会有几十纳秒的偏差,这对于想要稳定采样电流纹波的场景来说,已经足够让数据散开。

HRTIM的1.47ns分辨率带来的不仅是边沿精度,更重要的是事件发生器的灵活度。你可以在一个PWM周期内产生多个ADC触发事件,每个事件对应不同的比较寄存器,触发点任意平移,同时不影响PWM本身的占空比输出。这点普通定时器很难做到——要么占用比较通道影响PWM输出,要么触发通道数量不够,限制很大。

1.2 事件发生器:让ADC在精准时刻“自动开工”

HRTIM内部有五个独立的定时器单元(A/B/C/D/E),每个单元拥有自己的周期寄存器和四个比较寄存器CMP1到CMP4。比较匹配之后可以产生内部事件,这些事件经过一个可编程的事件发生器(Event Generator),最终输出到ADC的触发输入。

G474有两路ADC,分别是ADC1和ADC2,每路ADC可以通过外部触发源开启转换。HRTIM可以同时给两路ADC提供触发信号,配合ADC的扫描模式,一次触发就能把分配给这路ADC的多个通道按顺序转换完。我这次是三个采样点(后面会细说是哪三路),把其中两路挂到ADC1的扫描通道里,一路挂到ADC2,两个ADC并行工作,DMA分别搬运结果,效率和时序都能兼顾。

这里有个很多人容易忽略的点:HRTIM的事件发生器里,每个ADC触发源都要单独使能。你在HRTIM定时器单元里设置了CMP匹配产生事件,不代表ADC那边就会自动收到触发信号,必须通过HRTIM的EECR寄存器,把对应的事件映射到ADC1触发或ADC2触发线上,同时在ADC的CR2寄存器里配置好EXTSEL选择对应的HRTIM事件源。这一套链路打通了,ADC才会在指定的比较时刻准时启动。

1.3 普通定时器方案的实际局限

如果手头只有TIM1这样的高级定时器,硬要做到同样的功能也不是不行,但有几个绕不开的问题。

  • 高级定时器的比较通道通常直接用于产生PWM输出,想在同一定时器里找一个不干扰输出的比较事件来触发ADC,通道数很紧张。
  • 触发ADC的时刻一般锁定在周期上溢/下溢这些固定节点,无法做到“脉宽范围内任意时刻触发”。想要开通过程中点采样,就得用另外的定时器配合,时序同步又变复杂。
  • 多路ADC触发的一致性、时延补偿也没有HRTIM做得干净,实测下来数据抖动会明显一些。

所以结论比较明确:G474这颗芯片既然内置了HRTIM,做高频电源控制就不该绕路。它是为这类场景专门设计的,用对了地方能省掉很多外部逻辑和调试时间。

2. 中间时刻采样:从原理到采样点计算

2.1 开关噪声为什么集中在那几个微妙的时间点

先看一个基本事实:MOSFET开通和关断不是瞬间完成的。栅极驱动电流给米勒电容充放电需要时间,这个过程中漏源电压和电流交叠,会产生开关损耗和振荡。再加上PCB寄生电感、续流二极管反向恢复,开关边沿之后通常会有一段高频振铃,幅度可能达到几十毫伏甚至几百毫伏。这段振铃通过接地回路、电磁耦合进入ADC输入引脚,如果采样时刻正好落在振铃窗口内,转换结果就会叠加一个不确定的噪声分量。

这里可能有人问:ADC输入前端不是有RC滤波吗?不能滤掉吗?可以滤掉一部分,但RC滤波器会引入延时,对电流环的相位裕度有影响。想要高带宽的电流环路,RC截止频率又不能压太低。所以从源头避开噪声窗口,比单纯靠滤波更可取。中间时刻采样就是这个思路——把采样点放在开关动作已经结束、信号恢复平稳的时间段里。

2.2 边沿对齐模式和中心对齐模式下“中间”的含义

PWM有两大类:边沿对齐(递增计数)和中心对齐(增减计数)。采样点的几何位置取决于这两个模式。

  • 边沿对齐模式:周期从0递增到周期值(比如PERIOD),然后立刻归零。开关管在计数器为0时导通,到比较值CMP时关断。这个模式下的“中间时刻”是指导通脉宽的中点。比如占空比50%,脉宽中点就在PERIOD/2的位置,这时候开关管已经稳定导通了一段时间,电流纹波经过上升段后正好处于平均值附近。
  • 中心对齐模式:计数器从0递增到PERIOD再递减回0。在递增段的CMP处产生一次比较匹配,递减段的另一个CMP匹配产生第二次事件。这个模式下,PWM的“中间”天然就是计数器的中点(PERIOD/2),此时不管占空比是多少,开关管都稳定处于某个确定状态。

我在这个项目里用的是中心对齐模式。原因是中心对齐PWM在周期中点时,电流纹波正好处于波峰或波谷的对称位置,平均电流值就在这个中点附近,采样点作为“准平均电流”的代表误差很小。而边沿对齐模式下,中间时刻只有在占空比恰好50%时才和纹波中点重合,占空比偏移时采样得到的电流会更接近峰值或谷值,这取决于采样点放在脉宽中点的位置。

2.3 比较事件的计算公式与触发点设置

假设中心对齐模式,周期寄存器PERIOD决定PWM频率:

  • 频率 = HRTIM时钟频率 / (2 × PERIOD)。HRTIM时钟是680MHz,想跑500kHz的话,PERIOD = 680MHz / (2 × 500kHz) = 680。
  • 中间时刻的计数器值就是PERIOD/2 = 340。

我们把其中一个比较寄存器(比如CMP2)设为340,配置它在计数递增阶段匹配时产生ADC触发事件,这样每次PWM周期走到中点,ADC就会自动开始转换。这个比较点不参与PWM输出占空比的生成,只负责触发采样,互不干扰。

公式化的设置可以这样写:

  • 设PWM周期计数范围是[0, PERIOD],中心对齐模式。
  • 中间采样点计数器值:CMP_ADC = PERIOD / 2。
  • 如果想要微调采样点避开残余噪声,可以在PERIOD/2附近增减几十个计数,对应的时间偏移大约是增减值 × 1.47ns。

实测中我把触发点放在PERIOD/2往后偏移约25个计数(约37ns),能进一步避开开关边沿的残余振铃尾巴。这个偏移量不是固定的,和PCB布局、驱动电阻、MOSFET型号都有关系,现场调试时可以通过串口打印的数据波动幅度来反推最优位置。

2.4 采样保持时间与ADC转换时钟的配合

光有好的触发点还不够,ADC本身转换需要时间。G474的ADC是逐次逼近型,工作在最高170MHz时钟时,采样时间可以配置为2.5到640.5个ADC时钟周期,转换时间通常是固定的12.5个周期(12位分辨率)。

设ADC时钟为170MHz,采样时间配置为最大(640.5周期),那么一次完整转换大约是653个时钟周期,换算下来约3.84us。中心对齐PWM周期为2us(500kHz),这个时间显然不够在一次周期内完成全部三路通道的转换。所以要么降低ADC时钟到合理范围,要么把采样时间缩短,要么把多路通道转换分配到多个PWM周期内完成。

我实测后采用的方案:

  • ADC时钟配置为展频后约85MHz(APB2分频得到)
  • 采样时间设为8.5个周期
  • 转换时间12.5个周期
  • 单通道一次转换总耗时约21个ADC时钟周期,换算成约247ns
  • 三通道扫描一次大约740ns,远小于2us的PWM周期

所以即使扫描多路,也能保证在中间时刻这一个“窗口”内完成全部转换,不会拖到下一个开关周期。这里有个隐藏约束:中间时刻窗口的“宽度”是有限的,它等于从触发点开始到下一次开关动作之间的时间。占空比接近极端值时,这个窗口会被压缩,需要实时调整采样点或者放宽ADC采样时间。

3. 三路ADC采样链路的硬件规划与CubeMX配置

3.1 引脚分配与触发源的匹配

我的电路板需要采样三路模拟量:一路是电感电流(通过采样电阻放大后的电压),一路是输入母线电压(电阻分压),一路是输出电容电压(电阻分压)。三路信号都经过RC低通滤波后送入MCU的ADC引脚。

触发源规划:

  • ADC1挂载电感电流和输入电压两个通道,由HRTIM的事件发生器A(Timer A)产生触发。
  • ADC2挂载输出电压一个通道,由HRTIM的事件发生器C(Timer C)产生触发。

这样两个ADC由同一个时间基准控制,同时启动转换,能保证三路数据的相位一致性。虽然三路信号的性质不同(电流变化快,电压变化慢),但统一采样时刻点对系统辨识和控制算法有好处。

引脚上的注意事项:模拟输入引脚远离PWM输出引脚,避免数字开关噪声耦合到模拟通道。如果板子空间紧凑,至少保证输入信号的参考地是干净的模拟地,不能和功率地直接混用一个过孔。

3.2 CubeMX里HRTIM的配置要点

在STM32CubeMX中启用HRTIM后,我做了这些关键配置:

  • HRTIM时钟源选择内部时钟,并确认HRTIM内核时钟为680MHz。
  • 配置Timer A为中心对齐模式,周期设为680,产生500kHz PWM,用于一路功率开关的控制信号。
  • 将Timer A的CMP2设为340,作为ADC触发点。
  • 在“Event Generator”标签页里,把Timer A的CMP2事件使能,映射到ADC1触发器(具体来说是HRTIM_ADC1_TRG),同时映射到ADC2触发器(HRTIM_ADC2_TRG)。

很多人会在这一步漏掉“映射到ADC2”的选项,导致ADC2始终无法被HRTIM触发。CubeMX里实际上是支持一个事件同时触发两个ADC的,勾选对应选项就行。

另外,HRTIM的比较事件CMP2默认是PWM模式输出的一部分,如果不需要CMP2引脚输出PWM波形,可以在输出比较设置里把CMP2对应的输出通道禁止,只保留事件功能。这样就不会因为CMP2的比较匹配而意外改变PWM引脚状态。

3.3 ADC+DMA的配置细节

ADC1和ADC2都配置为:

  • 分辨率12位
  • 扫描模式启用
  • 连续转换禁用(用外部触发单次扫描,防止ADC自己连续跑把DMA缓冲区刷爆)
  • 外部触发源选择HRTIM事件输出(具体的EXTSEL值根据CubeMX下拉框选择对应HRTIM事件)
  • 采样时间按之前计算的8.5个周期设置

DMA配置:

  • ADC1使用DMA1 Channel 1,数据方向从外设到内存,数据宽度半字(16位),循环模式开启。
  • ADC2使用DMA1 Channel 2,同样的配置。
  • 内存缓冲区定义为一个全局数组,大小等于ADC1扫描通道数加ADC2扫描通道数。

这里有一个容易踩的坑:DMA的“循环模式”开启后,DMA传输完一轮会重新从内存起始地址继续覆盖写入。如果配合CMSIS-DSP库做滤波算法,缓冲区里的数据是不断被覆盖的,必须配合DMA半传输中断或传输完成中断来做数据处理,不能直接在主循环里轮询数组——因为你读到一半时DMA可能已经写入了新数据,造成数据撕裂。我这次的数据量不大(三路),用了一个简单的办法:DMA循环写满4组数据后,由定时器中断触发一次软件拷贝,把当前快照复制到另一块缓冲区供串口使用。

4. 代码实现:初始化顺序与数据链路

4.1 HRTIM启动和ADC触发的先后顺序

代码初始化的顺序有一点讲究。如果先启动HRTIM,再启动ADC,可能丢失第一个触发事件。反过来先启动ADC,HRTIM还没输出PWM,又会产生一段无触发状态。比较稳妥的顺序是:

  1. 先配置并启动DMA和ADC,让ADC处于“待触发”状态。
  2. 启动HRTIM,让它产生PWM和ADC触发事件。
  3. 通过一个全局标志位确认至少收到一次DMA传输完成中断后,再使能串口打印。

核心配置代码简写如下:

// ADC1 DMA初始化 __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); HAL_DMA_Init(&hdma_adc1); HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc1_buf, ADC1_CHANNEL_COUNT); // ADC2 DMA初始化 __HAL_LINKDMA(&hadc2, DMA_Handle, hdma_adc2); HAL_DMA_Init(&hdma_adc2); HAL_ADC_Start_DMA(&hadc2, (uint32_t *)adc2_buf, ADC2_CHANNEL_COUNT); // HRTIM启动(Master Timer必须先于Timer A启动) HAL_HRTIM_MasterStart(&hhrtim, HRTIM_MASTER_TIMEREN_MASK); HAL_HRTIM_WaveformCounterStart(&hhrtim, HRTIM_TIMERID_MASTER | HRTIM_TIMERID_TIMER_A); HAL_HRTIM_WaveformOutputStart(&hhrtim, HRTIM_TIMERID_TIMER_A);

注意HRTIM的启动有两个层面:计数器启动和波形输出启动。计数器不转,比较事件不会产生;波形输出不使能,PWM引脚不会有信号,但ADC触发事件其实是由计数器比较产生的,所以即便PWM输出引脚没使能,只要计数器在跑,触发事件照样能发。我一开始没想通这点,把波形输出关了却发现ADC照样触发,一度以为是配置错误。

4.2 DMA数据的解析与校验

DMA把ADC结果寄存器里的16位数据搬进数组后,需要做几步处理:

  • 确认数据右对齐:12位分辨率下,数据范围是0到4095,直接除以4095乘以参考电压得到电压值。
  • 通道顺序匹配:扫描模式下,DMA缓冲区第一个元素对应CubeMX里配置的第一个通道,第二个元素对应第二个通道,以此类推。如果CubeMX里调整过通道顺序,数组下标顺序会跟着变,不能写死。

我把adc1_buf设计成长度为8的数组(两个通道、四轮扫描),读取时用DMA半传输中断和全传输中断分别处理前4个和后4个元素。每轮转换完成后,计算平均值作为当前采样值,这样可以进一步滤掉单次转换的随机噪声。

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { snapshot_idx++; for (int i = 0; i < ADC1_CHANNEL_COUNT; i++) { adc1_snapshot[snapshot_idx][i] = adc1_buf[i]; } } }

4.3 串口打印的时序配合

串口打印用DMA发送,避免阻塞CPU。但有个细节:如果直接在回调函数里调用HAL_UART_Transmit_DMA,前一次发送还没结束就启动下一次,会返回HAL_BUSY,数据丢帧。我用的方案是“定时打印”,用另一个定时器产生100ms中断,在中断里拷贝当前快照到打印缓冲区,然后通过DMA发送一次。100ms内三次采样快照都完成了,所以打印频率远低于采样频率,总线不会冲突。

打印格式我选了最直观的CSV格式,方便直接导入Excel或者MATLAB里看波形:

voltage_in,current_load,voltage_out 12.012,1.234,5.001 12.013,1.235,5.002

用官方串口助手或者Python的pyserial读数都很好解析。实测波特率460800,115200也行,但高频调试时460800能少一点数据延时。

5. 实测中的坑与调试手段

5.1 现象:第一帧DMA数据错位,差了一个通道

我最初把ADC1配置成“注入通道”加“规则通道”混合模式,想着能更灵活,结果DMA搬运顺序完全不是我预期的。现象是电流数据出现在数组下标1,电压数据出现在下标0,逻辑上乱了。

排查过程:

  1. 把DMA缓冲区打印出来,发现相邻两帧数据对调,确定是扫描顺序问题。
  2. 打开CubeMX的“ADC regular sequence”视图,发现通道顺序是按我添加通道的先后排列的,而不是按通道号排序。
  3. 修改CubeMX里的扫描顺序,或者代码里按实际顺序解析数组,二选一。我选择了修改代码解析顺序,因为CubeMX里的顺序调整有时会影响DMA请求的触发位置。

另外一个小坑:G474 ADC的DMA数据寄存器读取,如果用字节(8位)宽度,会因为地址对齐问题读到高低字节错乱的数据。必须把DMA数据宽度设为半字。

5.2 采样值在占空比变化时跳水

母线电压通道的数据在输入电压稳定时很平稳,但电流通道偶尔会跳变。我怀疑是采样点窗口太窄,占空比跑偏后采样点落在了开关边沿附近。通过串口观察,出现跳变的时间点和PWM占空比超过70%之后高度重合——因为占空比越大,开通过程中点越靠近周期起点,而我的固定采样点在PERIOD/2,两者在时间上的距离被压缩,采样点逐渐靠近关断边沿的振铃区。

解决方法是把采样点改成动态跟随占空比。好在HRTIM的CMP寄存器支持在运行时修改,我写了一个简单的PID调节器计算占空比,同时把“中间时刻”采样点同步更新为目标值的一半,也就是把CMP2设置为(PERIOD × 占空比 / 2),这样采样点始终跟随脉宽的中点。

实际效果对比:

状态采样点位置数据波动范围
固定中点采样占空比50%±5 LSB
固定中点采样占空比80%±28 LSB
动态跟随采样占空比80%±8 LSB
动态跟随采样占空比15%±6 LSB

这个对比非常直观,占空比偏离50%越多,固定中点采样的劣势越明显。如果你的PWM占空比经常在较大范围内变化,建议不要用固定中点,动态跟随虽然代码多一点,但效果差得很清楚。

5.3 PCB布局上的三个要点

除了MCU配置,PCB布局对ADC采样质量的影响也很大。这方面我吃过亏,总结几个实用要点:

  • ADC前端RC滤波的电容地,必须单独走线回到MCU的模拟地引脚,不能和功率MOSFET的源极地层共用,否则开关电流会在公共阻抗上产生压降,直接叠加进采样值。
  • ADC输入走线尽量短,并且远离HRTIM输出的PWM驱动信号线。如果交叉不可避免,垂直交叉优于平行走线。
  • 在ADC引脚旁边放一个0.1uF的MLCC到模拟地,能提供高频电流的退耦路径,对抑制共模噪声有明显帮助。

另外如果采样的是高频开关电源里的电流信号,建议在运放输出和ADC引脚之间加一个二阶低通滤波,截止频率设在开关频率的1/10到1/5。RC取值时要注意运放输出阻抗和ADC输入阻抗对分压精度的影响,必要时加一级电压跟随器。

5.4 调试时一个很实用的小工具函数

调试这个项目时我写了一个简单的上位机绘图辅助脚本,用Python的pyserial读取串口CSV数据,实时画三个通道的波形图。配合串口打印,能做到“数据链路全打通”的快速视觉确认。打印频率100ms,波特率460800,CPU占用很低。

import serial import matplotlib.pyplot as plt import numpy as np ser = serial.Serial('COM5', 460800, timeout=0.5) fig, ax = plt.subplots(3, 1, sharex=True) data = {k: [] for k in ['vin', 'iload', 'vout']} plt.ion() while True: line = ser.readline().decode().strip() parts = line.split(',') if len(parts) == 3: vin, iload, vout = map(float, parts) data['vin'].append(vin) data['iload'].append(iload) data['vout'].append(vout) for ax_i, key in zip(ax, data.keys()): ax_i.plot(data[key], label=key) ax_i.legend() plt.draw() plt.pause(0.01)

串口示波器虽然不是实时性最强的工具,但在初期验证DMA数据是否正确、采样点是否合理这两个问题上,足够用了。等逻辑理顺,再上真正的示波器抓波形也不迟。

6. 从这套链路延伸出去:还能怎么扩展

做完这套HRTIM+ADC+DMA的链路后,我发现它其实是个通用的“采样骨架”,可以套到不少场景里:

  • 多路并联DC-DC的均流控制:每路输出电流一路ADC,HRTIM的不同定时器单元分别触发各路采样,保持相位一致。
  • PFC电路中的母线电压和输入电流同步采样:不需要额外加采样保持芯片,靠HRTIM的精密触发就能在电网周期内精确锁定采样点。
  • 电机控制的相电流重构:利用PWM中间时刻的窗口特性,在有效矢量时间段内采样两相电流,重构三相电流。

动态调整CMP2跟随占空比的做法,也可以推广成一种通用的“采样点自适应算法”,只需要在占空比更新时同步更新ADC触发比较值,代码逻辑并不复杂。

最后一个小经验:调试这套东西时,串口打印的内容尽量精简。我最初把DMA状态、HRTIM计数器值、每个通道的原始ADC值全部打出来,数据量太大,反而掩盖了真正的问题。后来只在异常分支打印状态,正常运行时只输出三路转换后的物理量,问题定位速度明显变快。希望这套链路和排查思路对正在做类似项目的朋友有帮助。

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

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

立即咨询