
用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加到周期值然后回0PWM脉冲比较靠前或靠后而中心对齐模式里计数器先向上计数再从周期值向下返回得到的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的另一个定时器单元产生一个移相后的触发信号专门采样不同桥臂的电流这就涉及到更复杂的时分复用采样策略了。先把这个基础链路吃透后面扩展起来会顺手很多。