news 2026/10/3 7:50:25

STM32G474 HRTIM触发ADC采样:实现PWM中间时刻采样的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32G474 HRTIM触发ADC采样:实现PWM中间时刻采样的完整指南

用STM32G474做数字电源或者电机控制的朋友应该都清楚,这颗芯片最值钱的外设就是HRTIM,高分辨率定时器。它不光能输出多路带死区的PWM,还能直接作为ADC的外部触发源,让采样点精确落在PWM周期内的任意相位上。这次要记录的项目就是把这个链路完整跑通:用HRTIM触发多路ADC采样,DMA把结果搬到内存,再通过串口打印出来,最终实现PWM中间时刻采样,尽量躲开开关管动作瞬间产生的噪声尖峰。这篇文章适合正在做数字电源、逆变器、电机控制,或者被ADC采样毛刺折腾到头秃的朋友参考。

1. 为什么是STM32G474搭配HRTIM来干这件事

1.1 开关噪声从哪来,为什么不能硬扛

很多人在嵌入式采样时遇到ADC数值跳变,第一反应是加滤波电容或者改软件平均值。但如果是PWM驱动功率级的场景,问题往往出在采样时刻本身。

开关管在导通和关断的瞬间,电压和电流变化率非常高。以典型MOSFET为例,dV/dt可以到几十伏每纳秒,这个瞬态会通过寄生电容、PCB走线耦合到ADC输入引脚上。哪怕你前端加了很好的RC滤波器,在高频振铃面前,RC的截止频率也不够干净地压掉所有噪声。而且加RC太重,还会拖慢信号建立时间,导致ADC采到的根本是过期的值。

所以正确的思路不是硬扛,而是躲。让ADC采样时刻避开开关动作的高噪声窗口,这就是PWM中间时刻采样的核心出发点。在PWM脉宽中心点附近,开关管已经稳定导通或者稳定关断,di/dt和dv/dt都相对平缓,此时采集到的电流、电压信号才是真正能反映系统状态的稳态值。

1.2 HRTIM相比普通定时器的关键优势

STM32G474的HRTIM不仅仅是频率高那么简单。它内部有好几个独立的定时器单元,每个单元都能产生自己的PWM和触发事件,而且这些事件和ADC之间有一条专门的硬件触发通路。

普通定时器比如TIM1也能触发ADC,但在高频PWM场景下有明显的局限。首先是触发精度问题,普通定时器的计数时钟就是内核时钟的APB分频,最小步长是固定的,采样点如果想精确落在占空比的50%位置,往往只能取个近似值。其次是延迟问题,触发信号经过定时器输出、触发复用器、ADC等待逻辑,中间每一级都有可观的延时,在几百kHz甚至MHz级的PWM频率下,这几个时钟周期的误差已经足以让采样点偏移到噪声区。

HRTIM的触发路径要短得多,而且它内置了针对ADC触发的专门信号,可以理解为芯片级优化的“采样同步总线”。另外HRTIM的多路定时器单元天然适合做各种移相PWM,比如全桥LLC、三相逆变器,这种情况下你不仅需要产生多路PWM,还要在每一路PWM的特定相位去采样,HRTIM可以直接把不同定时器单元的比较事件配置成不同的ADC触发源,这是普通定时器很难做到的。

1.3 这次工程的完整链路

整个数据通路是这样的:HRTIM产生PWM波形,同时在预定的比较事件点产生一个触发信号,这个信号直接送到ADC的外部触发引脚。ADC收到触发后立刻开始转换规则组里的所有通道,转换完成后由DMA把数据搬到内存缓冲区。主循环里把缓冲区的原始ADC码换算成实际电压或电流值,格式化后通过串口发送出去。

这条链路里没有CPU参与ADC数据的搬移,所以即使PWM频率很高、ADC转换很快,CPU也只要负责把结果打印出来,不会因为频繁进入中断处理数据而占用大量时间。从硬件角度来看,HRTIM是发起者,ADC是执行者,DMA是搬运工,串口是观察窗口,CPU只是个打杂的。

2. 采样时机设计与触发链路搭建

2.1 中心对齐模式下的采样点选择

PWM生成方式常见有两种:边沿对齐和中心对齐。边沿对齐模式里,计数器从0加到周期值然后回0,PWM脉冲比较靠前或靠后;而中心对齐模式里,计数器先向上计数再从周期值向下返回,得到的PWM波形左右对称。

中心对齐模式在电源控制里非常常用,因为它的对称性好,谐波分量更低,而且方便设计“死区对称”的互补PWM。在这个模式下,如果设定输出在计数器的值小于比较值时拉高,那么一个完整的PWM高电平区间是由两段组成的:一段在计数器上升沿从0到比较值,另一段在计数器下降沿从比较值回到0。换句话说,高电平区间的“正中间”刚好落在计数器等于0的位置。

所以,如果你想采样MOSFET导通中段的电流,就在计数器等于0的时刻触发ADC。反过来,如果你需要采样续流阶段或者关断中段的信号,就把触发点放在计数器等于周期值的位置。这个逻辑搞清楚后,整个采样时刻设计就清晰了:不需要靠猜,而是沿着计数器的走势去选固定的相位点。

2.2 从HRTIM事件到ADC采样的触发链路

在STM32G474上,HRTIM的任意一个定时器单元都可以产生ADC触发事件。常用做法是选择定时器单元A或者单元B的比较事件,或者直接用周期事件。

这里要区分两个概念:周期事件和比较事件。周期事件是定时器计数到周期寄存器的值(或者归零)时产生的,天然适合放在中心点或者边沿点上。比较事件则更灵活,你可以把比较寄存器设成任意值,让事件精确落在PWM周期内的任意相位。

实际工程里面,我习惯的做法是这样:PWM用主定时器单元A产生,采样触发点用单元A的其中一个比较通道来实现。这样触发点和PWM本来就出自同一个定时器,不存在两个定时器之间的相位误差问题。如果要调整采样点,只需要改这个比较值的大小,逻辑非常清晰。

触发信号从HRTIM出来后,会进入ADC的外部触发输入。ADC配置成外部触发模式,上升沿有效,收到一个触发脉冲就启动一组规则通道的转换。G474有多个ADC模块,都可以配置成使用这个HRTIM触发信号,从而实现多ADC同步采样。

2.3 多路ADC的DMA传输设计

多路ADC的说法,实际上对应两种不同的需求。

第一种是单ADC多通道扫描。比如只需要采集母线电压、相电流、温度这些信号,它们不需要完全在同一时刻被采集,只要在一个触发脉冲下全部依次转换即可。这种情况下,ADC把几个通道依次转换,DMA把结果挨个搬到数组里就行,配置相对简单。

第二种是多ADC同步采样。比如三相电流采样,要求三相电流在同一时刻被采下来,这就要用到G474的多个ADC模块。HRTIM的一个触发信号到达后,ADC1、ADC2、ADC3同时开始转换各自对应的通道,每个ADC模块分别接一条DMA通道,把数据搬回不同的缓冲区。

无论哪种方式,DMA都建议配置成循环模式,也就是Circular模式。这样每次触发后,DMA自动按照固定的偏移把新数据写入缓冲区,CPU只需要维护一个简单的“当前读位置”即可。循环模式的好处是不需要频繁重启DMA,也不会因为DMA没有及时重装配置而漏掉一次转换结果。

3. 关键配置实操:从CubeMX到HAL库

3.1 时钟与HRTIM基础配置

先把时钟跑起来。我用的外部晶振是8MHz,通过PLL倍频到170MHz作为系统主时钟HCLK。HRTIM的输入时钟在G474上直接连接HCLK,所以计数频率就是170MHz,这个频率下做10kHz到100kHz的PWM都绰绰有余。

在中心对齐模式下,PWM频率和定时器周期寄存器Period之间的关系是:

f_PWM = f_HRTIM / (2 * (Period + 1))

反过来就是:

Period = f_HRTIM / (2 * f_PWM) - 1

比如目标载波频率是50kHz,那么Period = 170000000 / (2 * 50000) - 1 = 1699。比较值则根据占空比去计算,如果占空比是40%,在中心对齐模式下,比较值 = Period * (1 - 0.4) = 1019。

代码里定时器单元A的初始化大致是这样:

HRTIM_TimerConfigTypeDef htim_timerA = {0}; htim_timerA.Init.Period = 1699; // 50kHz载波,中心对齐 htim_timerA.Init.CounterMode = HRTIM_COUNTERMODE_UP_DOWN; htim_timerA.Init.Compare1 = 1019; // 40%占空比 htim_timerA.Init.Repetition = 0; htim_timerA.Init.Prescaler = HRTIM_PRESCALERRATIO_DIV1; htim_timerA.Init.UpdateTrigger = HRTIM_UPDATETRIGGER_NONE; htim_timerA.Init.ResetTrigger = HRTIM_RESETTRIGGER_NONE; htim_timerA.Init.ResetUpdate = HRTIM_RESETUPDATE_MAIN; HAL_HRTIM_WaveformTimerConfig(&hhrtim1, HRTIM_TIMERINDEX_TIMER_A, &htim_timerA);

这里Compare1就是控制PWM占空比的比较值,其他通道比如TIM_B、TIM_C可以用一样的方式初始化。

3.2 ADC触发与DMA配置

ADC部分最关键的是把外部触发源选对。G474的ADC有一个外部触发选择列表,其中就包含HRTIM1的输出触发信号。我的配置是让ADC1和ADC2都使用HRTIM1的触发信号,边沿为上升沿触发。

HAL库里的ADC配置结构体大致这样:

hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_HRTIM1_TRG1; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; hadc1.Init.ContinuousConvMode = DISABLE; hadc1.Init.DMAContinuousRequests = ENABLE; hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN; HAL_ADC_Init(&hadc1);

TriggerEdge选择上升沿,是因为HRTIM触发信号在事件产生时是一个脉冲边沿。ContinuousConvMode必须关闭,否则ADC会自己不停地转换,外部触发就失去意义了。DMAContinuousRequests打开后,DMA会一直保持请求状态,配合循环模式使用。

DMA的初始化也要注意数据宽度。ADC结果是12位的,从数据寄存器读出来用半字HalfWord刚好,DMA写入内存的数据宽度也设成HalfWord,两边匹配,传输效率最高。如果设置成Byte,高字节会被截断,数据直接错误。

双ADC同步时,需要把ADC1配置为主模式,ADC2配置为从模式,多ADC模式选择同步规则模式。此时ADC1和ADC2的规则组会同时响应同一个外部触发,每个ADC把各自的结果存到各自的DR寄存器里。DMA就是两个独立通道分别服务两个ADC。

3.3 串口打印与数据展示

数据进了缓冲区,最终还是要给人看的。串口打印这部分我用的是最朴素的方案:printf重定向到USART1,然后主循环里定时把DMA缓冲区里的值格式化输出。

重定向函数:

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }

主循环里的处理逻辑大概是:预留两个uint16_t数组,比如adc1_buf[8]和adc2_buf[8],DMA循环往里面写数据。主循环每隔一毫秒读一次标志,然后对最新的一组数据进行简单的软件滤波,比如去掉最大值最小值后取平均,最后用printf打印出来。

打印格式可以根据自己的习惯来,比如:

printf("ADC1_0:%d ADC1_1:%d ADC2_0:%d ADC1_V:%d.%03dV\r\n", adc1_buf[0], adc1_buf[1], adc2_buf[0], voltage_mv / 1000, voltage_mv % 1000);

要注意的是,在中断服务函数里不要去调用printf,哪怕是用DMA方式发送也不要。格式化字符串是很耗时的操作,放在中断里会拖慢中断响应,影响系统的实时性。我一般只在主循环里做格式化。

4. 实测现象与常见问题排查

4.1 采样毛刺严重,数值来回跳

这种情况是最常见的。一测就发现问题不是采样代码,而是采样点仍然落在了开关噪声区间。

排查方法很简单:把HRTIM的ADC触发比较值一点点往PWM周期前面或后面挪,同时用串口观察打印的数值波动情况。你会发现数值有明显的“好区”和“坏区”,在开关切换沿附近的采样结果跳动非常剧烈,而往周期中心方向移动后,数据会突然变得平稳。

如果调整触发点已经移到中心位置还是有毛刺,那就要怀疑噪声是通过地线或者电源传导进来的。这时用示波器看一下ADC引脚的波形,如果看到开关频率的高频振铃耦合到了引脚上,就是硬件问题,需要挪到第5章说的PCB布局处理。

4.2 采样结果一直是零或者不变

先确认HRTIM有没有真正在运行。如果PWM都没出来,那肯定是定时器配置问题,触发信号也没有,ADC自然一动不动。可以在初始化完HRTIM之后,先用逻辑分析仪或者示波器看PWM输出引脚。

再确认ADC的外部触发配置是否正确。很多人用CubeMX生成代码后,发现CubeMX里的外部触发外设选择列表没有HRTIM的选项,其实是需要在HRTIM配置页面里先打开ADCTrigger开关,生成代码后外部触发列表才会出现对应选项。

还有一个容易被忽略的坑:ADC的校准没做。STM32G4系列在初始化后建议马上跑一次HAL_ADCEx_Calibration_Start,不校准的话零点偏差会非常大,偶尔还会出现所有通道输出同一个固定值的情况。

4.3 DMA传输数据错位或顺序混乱

多通道扫描模式下,数据错位多数是DMA长度和实际转换通道数不匹配。比如ADC开了4个通道,但DMA配置只搬3个半字,下一次触发时第一个通道的数据会被当成上一轮的最后一个通道输出,整个数组整体错位。

解决办法很简单:确认DMA数据长度等于通道数量,且每次转换完成后把数组索引归零。如果用循环模式,注意不要让主循环的读速度超过DMA的写速度,否则会读到一半新一半旧的数据。

多ADC同步模式下还有一个隐藏问题:虽然ADC1和ADC2是同时触发的,但它们各自的转换完成时刻可能不一样。如果两个ADC的通道数量不同或者采样时间设置相差很大,DMA搬运速度就会有差异。这种情况建议把两个ADC的采样时间配成一致,让结果尽可能同步输出。

4.4 串口打印卡住,主循环跑不动

问题一般出在printf上。HAL_UART_Transmit是阻塞发送,如果发送大量格式化字符串,费时确实可观。尤其是在调试时高频打印,比如每个PWM周期都print一次,主循环基本就废了。

我常用的做法是降频打印。比如每1000个PWM周期打印一次,或者干脆主循环里加一个简单的计时器,每隔10毫秒打印一帧。这样既能看到数据变化的趋势,又不至于拖垮系统。更高阶的做法是用串口DMA加环形缓冲区,把格式化好的字符串丢进缓冲区就返回,由DMA在后台慢慢发。调试阶段先用阻塞方式,跑通了再优化也不迟。

5. 硬件与PCB层面的避坑建议

5.1 ADC前端RC滤波与参考电压处理

即使采样时刻已经尽量避开开关噪声,硬件上的辅助滤波仍然很有必要。ADC输入前端一般会加一级RC低通,这个电阻电容的取值需要平衡两个矛盾:太小了滤不掉高频噪声,太大了会拉长信号建立时间,导致采样结果偏小甚至动态失真。

经验值上,电阻取100欧姆到1千欧姆,电容取1nF到10nF比较常见。比如信号带宽不高、只是看母线电压,可以用1k欧姆加10nF,截止频率大约15.9kHz,对几十kHz以上的开关振铃已经有不错的衰减效果。如果是电流采样,信号带宽可能要几十kHz,就需要把截止频率调高,相应的电容就得用1nF甚至330pF,否则电流环的动态响应会受影响。

G474的ADC参考电压引脚VREF+也需要单独处理。不要直接和数字电源用一个引脚,而是通过一个几十欧姆的磁珠或者小电阻,再加一颗1uF和100nF的电容去耦到模拟地。参考电压的干净程度直接决定ADC转换的绝对精度,这一点经常被忽视。

5.2 采样走线的布板要点

PCB布局方面,有几条原则是长期踩坑踩出来的。

ADC输入走线要尽量远离开关管的驱动信号、栅极电阻、电感以及PWM输出走线,尤其是不要和这些高dv/dt信号平行走线超过几毫米。这些区域是辐射噪声的主要来源,哪怕ADC走线和它们隔了0.5mm,寄生电容耦合过来的噪声都可能比信号本身还明显。

模拟地和功率地要遵循单点连接的原则。最简单的做法是在PCB上把模拟地单独划一块区域,ADC、运放、VREF的去耦电容都落在上面,然后通过一个铜皮或者0欧电阻与功率地相连。不要让MOSFET的源极电流流过ADC参考地平面,地平面上的压降会产生肉眼可见的采样误差。

还有一条很多人不知道的技巧:ADC输入走线两侧尽量铺地,但不要形成很长的“地尾巴”。如果你在PCB上把ADC引脚到运放的输出走线包在两侧地层里,高频共模噪声会被明显抑制。走线如果比较长,可以在输出端和ADC输入之间串联一个小电阻,比如100欧姆,配合对地电容形成RC滤波,这比单纯加电容更不容易损坏运放输出。

6. 写在最后的一点项目心得

这整套链路跑通之后,最直观的感受是采样数据干净了不是一点半点,而是质变。之前用普通定时器触发ADC,无论怎么调软件滤波,总有一两个点跳来跳去。换成HRTIM把触发时刻挪到PWM中间后,基本不需要再做特殊的软件平滑,原始数据直接打印出来就是稳定的。

另外一个体会是,这种“躲”采样点的方法,本质上是在和系统的时间关系做设计。你不需要让ADC对抗几十兆赫兹的振铃,只需要在正确的时间点去读取正确的信号。想明白这一点,很多所谓ADC采样不准的问题都能迎刃而解。

如果你接下来要在这个基础上做闭环控制,可以把DMA缓冲区里的数据直接拿去做PID计算,串口打印只保留在调试阶段。再往后,还可以用HRTIM的另一个定时器单元产生一个移相后的触发信号,专门采样不同桥臂的电流,这就涉及到更复杂的时分复用采样策略了。先把这个基础链路吃透,后面扩展起来会顺手很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 7:50:20

工厂冷却水引入分布式能源站:余量利用与方案比选

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:50:11

SpringBoot WebSocket STOMP企业IM实战:群聊、@提醒与消息回执

在接手企业IM(即时通讯)这个需求之前,我一直以为SpringBoot里加个WebSocket就是调个API的事,真正动手做才发现,从“能连上”到“能稳定支持群聊、提醒、消息回执”之间隔着一条巨大的鸿沟。这篇文章把我在开发一个包含…

作者头像 李华
网站建设 2026/10/3 7:49:53

DRV8818PWPR+ATmega32工业级双极步进电机控制方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:48:15

Hadoop生态核心拆解:从HDFS、MapReduce到HA高可用实战指南

做了这么多年大数据相关的项目,每次被问到“想入行大数据,第一步该学什么”,我的答案几乎没变过:先把Hadoop吃透。这不是因为它最时髦,恰恰相反,Hadoop的生态组件在当下已经不算“新潮”了,但它…

作者头像 李华
网站建设 2026/10/3 7:47:51

5G+TSN融合部署实战指南:时钟同步与QoS映射配置锚点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华