简介:面向基于STM32H743的嵌入式开发者,这份资源是《STM32CubeMX配置教程(十二)》的配套工程包,围绕定时器触发固定频率ADC采样并通过DMA搬运数据的常见需求,提供从CubeMX初始化到Keil编译的完整代码框架。压缩包共163个文件,体积约8.56MB,其中包含59个头文件和28个C源文件,覆盖STM32H7系列HAL库的定时器、ADC、DMA等外设驱动;另有Keil MDK工程文件(uvprojx/uvoptx)、CubeMX的ioc配置、编译生成的axf/hex烧录文件、链接脚本及调试配置等,可直接打开工程学习或烧录验证。已有1553人学习下载,适合正在学习STM32CubeMX自动生成代码、需要快速理解外设联动配置的初中级开发者。拿到手后既能参考定时器触发采样的时钟与参数设置,也能对照HAL驱动函数追踪DMA传输的完整流程,还可依据完整工程结构举一反三,节省排查配置问题的时间。整体目录结构清晰,便于按模块阅读与二次开发。 有个工程压缩包,名字叫“STM32H743_TIM_ADC_DMA.rar”,乍一看像是一堆寄存器配置的拼凑,但懂行的人都知道,这个组合是高性能嵌入式里最经典的“采样铁三角”:定时器负责精准触发,ADC负责把模拟量变成数字量,DMA负责在后台搬运数据,全程不需要CPU干预。这篇博文就把这套组合彻底掰开揉碎,从原理到实操,再从踩坑到排查,一次讲透。
我最早接触这个组合是做一个三相电机电流采样项目,主控是STM32H743,要求20kHz的采样率,三相电流加母线电压一共四个通道,每个PWM周期都要同步采样。如果用定时器中断里启动ADC再读结果,20kHz的中断频率加上ADC转换时间,CPU光应付采集就得吃掉三成以上的性能,更别说还得跑FOC算法了。后来改成TIM触发ADC、DMA自动搬运,CPU只在一个PWM周期结束后去内存里拿结果,负载直接降到5%以内,整个系统瞬间就清爽了。这套方案放在任何需要高频、高精度采样的场景里,都值得优先考虑。
1. 整体设计思路拆解:为什么非要用TIM+ADC+DMA这个组合
在动手写代码之前,先得把设计逻辑理清楚,否则就是照着别人的工程抄,出了问题根本不知道怎么排查。
1.1 采样任务的本质:精确、连续、不打扰CPU
任何一个采样系统,本质就三件事:什么时候采、怎么采、采完的数据怎么拿走。多数人第一步会想到用定时器中断,进入中断后启动ADC,ADC转换完成后再进一次中断读数据。这条路在采样率低、通道少的场合完全够用,比如1kHz采一个温度。但只要频率往上走,中断嵌套、现场保护、进出栈这些开销就会成倍增长,而且每次采样都有一个“启动-等待-读取”的过程,相位误差是固定的,想要多个通道严格同步,还得费劲去对齐。
TIM+ADC+DMA这套组合把这三件事彻底解耦。TIM负责产生一个精确到微秒级的触发脉冲,这个脉冲直接通过硬件连接送到ADC的触发输入端,ADC收到脉冲后自动启动转换,转换完成的结果自动触发DMA请求,DMA把数据搬移到内存数组里,整个过程没有一条指令是CPU执行的。CPU只在数据搬完一帧后收到一个“半传输中断”或者“传输完成中断”,过来把数组拷走就行。
有些同学会问,ADC不是有扫描模式,可以自己连续扫多个通道吗,为什么还需要TIM来凑热闹?连续扫描模式确实能按顺序扫完所有通道,但这个“顺序”是ADC内部自动进行的,通道切换和转换开始之间没有精确的相位关系,而且它是“满了就扫”,做不到和外部事件同步。TIM触发则是让所有通道都从同一个“起跑线”开始转换,进样时刻完全对齐,这在电机控制、并网逆变这些场景里是刚需。
1.2 为什么STM32H743特别适合干这件事
H743这颗芯片在STM32家族里属于旗舰级,Cortex-M7内核跑到480MHz,片上集成了3个独立的ADC、2个高级定时器、10个通用定时器,DMA控制器有2个,其中DMA1有8个流,DMA2有8个流,每个流8个通道。ADC的转换速率最高能做到3.6Msps,16位分辨率,而且三个ADC还支持注入组同步采样模式。
这里的核心优势是外设资源充足,可以做到真正的并行。比如三个ADC可以分别挂在三个不同的定时器触发源上,每个ADC管一组物理量,互不干扰。另外一个容易被忽略的点是H743的DMA支持循环模式加双缓冲,这意味着数据可以在内存里形成“乒乓结构”,DMA往Buffer A写的时候,CPU处理Buffer B,写完自动切到Buffer A处理Buffer B,采样的连续性完全不受CPU处理速度影响。这在做连续数据流的系统里是神技,比如振动分析、音频采集。
1.3 这套方案解决的核心痛点
直接说结论,这套方案解决的核心痛点是相位确定性和CPU占用率。相位确定性体现在:TIM触发ADC是由纯硬件完成的,从触发脉冲到ADC开始采样的延迟是固定的、可计算、可补偿的(手册里叫tADCTRG),不会像软件触发那样受中断响应时间抖动的影响。CPU占用率的改善前面也提了,数据搬运这种高频脏活累活全交给DMA,CPU只需要在帧边界上做一次轻量级的处理。
2. 核心细节解析与实操要点
工程结构和原理说完了,接下来进入真正写代码的阶段。这一部分把几个关键的配置点逐一拆开,每一个都值得单独讲清楚,因为它们直接影响采样数据的准确性。
2.1 ADC时钟树配置:分频系数决定采样精度上限
ADC的时钟源头是PLL2P或者PLL3P,经过ADC分频器后送给ADC内核。H743的ADC最高工作频率是50MHz(具体要看数据手册的电气特性表),分频系数可选1/2/4/6/8/10/12/16等。很多人在这里犯的错误是把ADC时钟拉到最高,觉得越快越好,实际不是这样。
ADC的转换时间 = 采样周期 + 转换周期(固定)。比如采样周期设为8.5个周期,转换周期固定为8.5个周期,总共17个周期。如果ADC时钟是50MHz,那么一次转换就是340ns。但采样周期太短会导致采样电容没有充分充电,尤其是信号源阻抗较高的时候,采样值会偏低。所以正确的做法是:先估算信号源阻抗,再定采样周期,最后反推ADC时钟。信号源内阻大,采样周期就得拉长,比如32.5或64.5个周期,这时即使ADC时钟是50MHz,也会因为采样周期太长降低了实际吞吐率。
具体到H743,它的ADC在50MHz时钟下,最快转换时间约3.3Msps(12位精度),16位精度下约3.6Msps。我的建议是:把ADC时钟设为50MHz,采样周期根据信号源阻抗来选,从8.5开始往上试,直到数据稳定。如果信号源阻抗本身就很低(<100Ω),8.5个周期完全够用。
2.2 TIM触发源选择:TRGO事件的来龙去脉
TIM要触发ADC,核心是把TIM产生的事件引到TRGO(Trigger Output)引脚上,再把TRGO接到ADC的触发输入。H743的ADC触发源分两组:规则组和注入组,每组有不同的触发源列表。常用的做法是用TIM1或者TIM8的更新事件作为触发,也可以用TIM2/3/4/5的输出比较事件。
这里有一个细节:更新事件和输出比较事件在时间精度上有区别。PWM的更新事件发生在计数器溢出时,对应PWM周期的起点或终点;输出比较事件则发生在计数器值和比较寄存器值相等的瞬间。如果要做“PWM中点采样”,也就是在PWM载波波峰或波谷处进样,采样时刻对应的是计数器的顶点,这时候通常用更新事件更合适,因为向上计数模式下顶点也是计数值溢出点。
在CubeMX里,要开启TIM的“Trigger Output (TRGO)”功能,并在ADC的触发源里选择对应的“Timer X Capture Compare X event”或“Timer X Update event”。H743的ADC1规则组触发源表里有TIM1_CC1、TIM1_CC2、TIM1_CC3、TIM2_CC1、TIM2_CC2、TIM3_CC1等一堆选项,选错了通道就触发不了,这是新手最容易卡住的地方。
2.3 DMA搬运配置:循环模式与内存对齐的讲究
DMA这块是整个工程最容易出玄学问题的地方。先说基本配置:ADC的DMA请求挂在DMA1或者DMA2的某个Stream上,方向是外设到内存(PeripheralToMemory),外设地址固定为ADC_DR寄存器,内存地址是自增的。关键在模式选择:Circular模式和Normal模式的取舍。
如果只需要采一轮数据,比如按键扫描,用Normal模式,传输完自动停止。如果做连续数据流,必须用Circular模式,这样DMA会一直循环写内存,写满了自动从头开始。但Circular模式有个隐患:如果你不知道DMA当前写到哪里,读数据的时候可能读到半新半旧的数据。解决办法有两个:一是用DMA的半传输中断和传输完成中断,在中断里把Buffer分成两半来读;二是用H743的双缓冲模式(Double Buffer),DMA自动在两个Buffer之间切换,一个被DMA占用时,另一个对CPU开放。
内存对齐方面,如果开了D-Cache,就必须注意缓存一致性问题。H743的DMA是直接访问SRAM的,不会经过CPU的Cache。如果CPU把数据写在Cache里还没回写SRAM,DMA读到的就是旧数据;反过来,DMA写进SRAM的数据,CPU读的时候可能命中的是Cache里的旧值。解决办法是在CubeMX里把对应的内存区域配置为“Non-cacheable”,或者手动做Cache Clean和Invalidate操作。很多人在H743上跑ADC+DMA,发现数据一直是0或者乱跳,八成就是Cache一致性的锅。
实践经验:在H743的工程里,我习惯把DMA缓冲数组定义在单独的DTCM RAM或者SRAM区域,并在MPU配置里把这块区域设置为Non-cacheable。如果要用D2域(SRAM1/2/3),要注意总线矩阵的延迟,配合Cache操作才能保证数据一致性。
3. 实操过程与核心环节实现
现在进入实战环节,用HAL库配合CubeMX演示一套完整的配置流程。这套配置我实际验证过,可以直接作为模板,替换成自己的引脚和参数就能用。
3.1 CubeMX配置步骤详解
第一步,选芯片,STM32H743VIT6或者根据你的板子来。时钟树这里有个容易错的点:H743的ADC时钟来自PLL2P,需要手动在Clock Configuration页面里把PLL2使能,并把PLL2P设为50MHz。很多人的CubeMX配置不完整,导致ADC时钟不对,采样值明显异常。
第二步,配TIM。我用TIM1做PWM输出和ADC触发,频率20kHz,周期50µs。PWM频率设为20kHz后,在“Trigger Output (TRGO)”里选择“Update Event”。TIM1的频率计算:定时器时钟是240MHz(APB2 Timer Clock),Prescaler设为0,Period设为11999,这样240MHz/12000=20kHz,正好。注意period单位是定时器计数周期,不是微秒。
第三步,配ADC。ADC1开启两个通道:IN0和IN1。在“ADC_Regular_ConversionMode”里,Number Of Conversion设为2,分别把两个Channel的Rank设为1和2,采样时间设为“8.5 Cycles”起步。在“ADC_Regular_ConversionMode”里,External Trigger Conversion Source选择“Timer 1 Trigger Out event”,External Trigger Conversion Edge选择“Rising Edge”(上升沿触发)。连续转换模式(Continuous Conversion Mode)选Disabled,因为是外部触发,不是自己连续转。DMA Continuous Requests选Enabled,这个选项的含义是:每次外部触发事件到来,ADC就自动启动一次转换,而不是只转一次就停,启用后DMA才会持续搬数。
第四步,配DMA。Add DMA for ADC1,选择DMA1 Stream0(具体看CubeMX支持情况),模式Circular,数据宽度Word(因为H743的ADC是16位数据,但存储器建议按Word对齐),内存地址增量开启。中断里勾上Half Transfer和Transfer Complete中断。
3.2 HAL库代码逻辑与关键函数说明
CubeMX生成代码后,核心逻辑其实就几行。首先初始化:
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // 启动PWM,同时启动TRGO触发 HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 128); // 启动ADC DMA传输HAL_ADC_Start_DMA这个函数的作用是使能ADC并启动DMA传输,DMA会持续往adc_buffer搬运数据,每个PWM周期触发一次ADC转换,每次转换完触发一次DMA传输。这里的128是DMA传输的半字/字数量,如果ADC配置了2个通道,128个数据就是64次PWM周期。
数据回调函数:
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { // 整个Buffer填满了,处理数据 process_audio_frame(adc_buffer, 128); } } void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { // 前一半数据准备好了,处理前半部分 process_audio_frame(adc_buffer, 64); } }这两个回调函数是顺序执行的,处理完前半部分再处理后半部分。注意回调函数里不要做耗时操作,比如浮点运算、打印、动态内存分配,否则DMA回调会堆积,导致数据丢失。处理数据的耗时一定要小于半个Buffer的填充时间,否则就追不上了。
在实际工程里,我建议用“存两次算一次”的方式:开2倍长度的Buffer,每个半传输中断里把数据拷到独立的处理数组,然后在一个低优先级任务里做计算,把采样的实时性要求和处理的算法耗时彻底分开。
3.3 一个完整的2通道音频采集示例
举个例子,用TIM1 48kHz触发ADC1的两个通道采集模拟麦克风信号,直接跑PDM或者模拟量输入。CubeMX配置和前面一致,只是PWM频率改为48kHz,Period = 240MHz/48000 = 5000。
DMA缓冲数组和回调逻辑如下:
#define ADC_BUF_LEN 512 volatile uint32_t adc_buf[ADC_BUF_LEN]; volatile uint32_t adc_buf_half[ADC_BUF_LEN/2]; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc->Instance == ADC1) { memcpy((void*)adc_buf_half, (const void*)adc_buf, (ADC_BUF_LEN/2) * sizeof(uint32_t)); } }注意这里用volatile修饰跨越中断和主循环共享的数组,避免编译器过度优化导致数据不同步。memcpy在这种快速场景下可能有点贵,但对一个64个uint32_t的拷贝,M7核只要几十个周期,完全可接受。
3.4 触发频率与采样率的对应关系
不少人混淆了PWM频率和采样率。在这套配置里,PWM的更新事件频率就是ADC的触发频率,也就是采样率。但要注意:如果一次触发只转换一个通道,那么采样率 = 触发频率;如果一次触发连续扫描多个通道,那么每个通道的采样率 = 触发频率,但总的数据率 = 触发频率 × 通道数。DMA的Buffer大小必须覆盖这个总数,不然数据会溢出丢失。
举例:两个通道,触发频率48kHz,一秒钟产生96k个采样数据,如果Buffer长度是128,那么DMA大约每1.33ms产生一次传输完成中断。如果要保证每个PWM周期都有独立的采样点,必须让DMA的Buffer能够容纳一个触发周期的数据。
4. 常见问题排查与心得
这一部分把常见的坑和排查思路过一遍,每一个都是实操中实际遇到过的。
4.1 两个特别像的“疑难杂症”的典型表现和排查
现象一:ADC数据全是0,或者偶尔有值但不更新。常规排查顺序是这样:先查ADC是否真正被触发。引脚上用示波器量有没有触发脉冲不好量,更实用的方法是:程序里单步执行,停到断点后看DMA的NDTR寄存器,如果一直在变,说明DMA在工作;如果NDTR不变,说明DMA没启动或者传输完了没收到事件。再查ADC的ISR寄存器,看EOC标志有没有置位。如果EOC一直不置位,大概率是触发源没配上,最常见的就是CubeMX里External Trigger Conversion Source选错了,或者TRGO没有开启。
现象二:数据有,但是明显错误,比如整体偏移、波形失真、有周期性毛刺。这个是Cache一致性问题最典型的症状,在H743上非常容易出现。解决方法:把DMA缓冲数组定位到非Cacheable区域。可以参考前面的MPU配置示例。还有一种情况是数据是反的或者错位的,比如通道A的值跑到通道B的位置,这通常是DMA数据宽度设置不对,ADC是半字对齐的,但DMA按字搬,导致两个通道的数据交错错位。解决方法是把数据宽度设为半字(Half Word),或者把Buffer类型改成uint16_t,并在ADC初始化时把数据对齐方式改为右对齐。
4.2 采样率达不到预期,一直卡在某一个上限
这个问题通常出在两个地方:一是ADC时钟没到50MHz,二是PWM的更新事件频率本身就被限定了。先用CubeMX的时钟树确认ADC时钟确实是50MHz,然后确认APB2的定时器时钟是不是240MHz。如果这些都正常,再看有没有在中断里做了太多事,导致采样任务被“拖累”。DMA的搬运本身不占CPU,但中断回调处理数据如果太慢,下一次中断来时CPU还在忙,就会丢数据或延迟。在这点上,我通常用逻辑分析仪或者示波器测PWM的输出频率,直接确认触发频率本身对不对,从源头排除更快。
4.3 数据跳变,是信号问题还是代码问题
在ADC采集里,数据跳变是最难排查的,因为它可能来自信号、电源、布局、代码、配置等多个方面。建议按这个优先级排查:先用内部基准电压(比如H743的VREFINT通道)测一遍,如果内部基准的值也在跳,说明是代码/配置问题,和外部信号无关。如果内部基准稳定,那问题大概率出在外部信号链路上,查信号线、参考电压、退耦电容,必要时用示波器看信号本身有没有毛刺。
我的排查经验是:永远先怀疑配置和代码,再怀疑硬件。因为代码可以逐步验证,硬件问题往往要动烙铁,成本更高。最快验证代码的方式是开一个固定电压的通道,比如内部基准,直接读它的值,如果稳定,说明整个DMA链路是通的。
4.4 多通道数据错位与DMA循环模式的坑
多通道扫描时,ADC的顺序是Rank1、Rank2、Rank3……DMA按照这个顺序把数据依次搬到Buffer里。如果DMA被配置成Circular模式,数据始终按顺序写,理论上不错位。但有个坑:如果使用HAL_ADC_Start_DMA在每次传输完成后重新启动DMA,这个重新启动的瞬间如果恰好ADC又触发了一次,DMA的地址指针会重置,导致数据错位。解决方法是:不要在传输完成中断里频繁调用HAL_ADC_Start_DMA,而是让Circular模式自己循环,只在初始化时调用一次。
5. 扩展思路:这套组合还能怎么玩
TIM+ADC+DMA的玩法远不止数据采集,几个常用的高阶扩展:
PWM中点采样:做逆变器或者电机控制时,通常需要在PWM载波的中点采样电流,这个时刻干扰最小。用TIM1做PWM输出,把TRGO设为更新事件,上面已经实现了“周期末采样”;如果要在中间采样,就用TIM的中央对齐模式,更新事件发生在计数值等于0和等于Period的时刻,这样采样点就对齐到PWM波形的中心和边界。
多ADC同步采样:H743有3个ADC,可以用TIM1的CC1触发ADC1,同时用TIM1的CC2触发ADC2,实现两个ADC同时采集。这在做三相电流+母线电压时特别有用,把采样时刻精确对齐到同一个微秒以内。
注入组采样:规则组是持续高频采样的通道,注入组可以理解为“高优先级的中断采样”,由外部事件(比如故障信号)触发,插在规则组采样的间隙里。这个特性非常灵活,比如用TIM的另一个比较事件去触发注入组,实现一个低频的“慢速通道”和规则组的“快速通道”并存。
双缓冲加N点平均:把DMA的Buffer开成4段,每次半传输中断里对一段做累加平均,4段平均完再输出一次结果。相当于在不占用额外CPU粗率中断的情况下,把采样率提升了4倍再做了平均滤波,采样噪声能压得很低。
我实际用这个思路在H743上做过一个16位、20kHz采样率的高精度数据采集系统,整体SNR可以达到80dB以上,CPU负载不到20%,效果非常理想。这套方案在电机控制、电源控制、音频采集、振动监测、传感器数据采集等领域都能直接落地。
本文还有配套的精品资源,点击获取