STM32H743定时器触发ADC与DMA实现1MSPS采样详解
发布时间:2026/10/20 10:39:10 锦皓数字建站

简介面向使用STM32CubeMX与HAL库的嵌入式开发者这份RAR压缩包给出STM32H743定时器触发固定频率ADC采样、并通过DMA自动搬运数据的完整工程参考。示例对应《STM32CUBEMX配置教程十二》工程内可清楚看到TIM触发源选择、ADC采样通道配置、DMA循环传输模式等关键初始化代码以及HAL_TIM、HAL_ADC、HAL_DMA等外设驱动模块的调用关系并包含RCC、PWR等底层时钟与电源支持代码。包内共163个文件以C源文件、头文件与编译目标文件为主同时包含IOC/MXPROJECT的CubeMX配置、UVPROJX/UVOPTX的Keil工程文件以及AXF/HEX等可直接验证的映像整体仅8.56MB。已有1558人学习下载适合刚接触H7系列定时器与DMA协同采样或希望对照HAL库源码理解自动生成代码结构的嵌入式开发者进行学习与验证。1. 定时器触发 ADC一个工程让 1MSPS 采样不再吃 CPU把 STM32H743_TIM_ADC_DMA 这个工程完整跑通之后我对 32 位 MCU 做数据采集的印象刷新了一次原来 1MSPS 的连续采样可以完全不让 CPU 参与。定时器产生触发脉冲ADC 按固定间隔转换DMA 把结果自动搬进内存主循环甚至可以去做别的活。这个压缩包里的工程干的正是这件事它绕开了「查询标志位 中断读寄存器」这种老办法把 H743 上三个外设串成了一条完整流水线。它适合两类人。一类是做信号采集、振动监测、电力参数测量的开发者需要稳定且高速的采样又不想让采样占用太多 CPU 资源另一类是刚接触 H7 系列想弄明白外设之间怎么协同工作的同学。工程体积不大但触发、转换、搬运、回调这一整条链路非常完整拿来当模板改比自己从零写省太多时间。2. 工程链路拆解从 TRGO 脉冲到 DMA 落内存的完整通路2.1 为什么是 TIM1 ADC1 DMA1 这个组合在 H743 上做高速采集外设组合的选择基本决定了工程的上限。定时器选 TIM1因为它挂在高级定时器这一路触发输出 TRGO 可以直接连到 ADC 的外部触发引脚而且支持更新事件、比较事件多种触发源采样率可以算得很准。ADC 选 ADC1H743 有三个独立 ADCADC1 的 DMA 请求通道在 DMAMUX 里映射最灵活触发源和 DMA 请求互不干扰。DMA 选 DMA1 的 Stream 0原因比较简单这是 CubeMX 默认生成的位置请求映射由 DMAMUX 决定你把它换到 DMA2 上效果一样。这个组合能成为经典核心原因是分工清晰定时器只管「什么时刻触发」ADC 只管「触发后转一次」DMA 只管「转换完把数据搬走」。CPU 在中间的每个环节都不需要停下手头的事去等结果。相比 F1 时代用定时器中断里去启动转换的写法这套组合把采样抖动从微秒级降到了纳秒级因为触发脉冲是硬件产生的不经过中断响应。实际项目里采样率的稳定性往往比转换速度更值钱用 H743 做这件事算是最稳妥的起步方案。2.2 工程目录与三个关键文件用 CubeMX 生成的标准目录长这样STM32H743_TIM_ADC_DMA ├── Core │ ├── Inc │ │ ├── main.h │ │ ├── adc.h │ │ ├── tim.h │ │ ├── dma.h │ │ └── gpio.h │ └── Src │ ├── main.c │ ├── adc.c │ ├── tim.c │ ├── dma.c │ └── gpio.c ├── Drivers │ ├── CMSIS │ └── HAL 驱动库目录 └── MDK-ARM ├── 工程文件.uvprojx └── 启动文件与链接脚本这个结构是 CubeMX 的默认产物绝大多数人拿到的工程都是这个骨架。真正需要动的地方其实只有三个main.c 里的主循环逻辑、adc.c 里的采样参数、tim.c 里的触发频率。dma.c 和 gpio.c 生成后基本不用改。如果你打开工程发现文件结构有明显差异那大概率是用了老版本的固件包不影响理解但建议先核对 HAL 版本再编译避免某些 API 名称不一样。有个习惯建议从一开始就养成修改代码只写在由/* USER CODE BEGIN */和/* USER CODE END */包裹的区域里。CubeMX 重新生成项目时会保留这些区间的代码写到区间外面一次重新生成就把你的改动抹掉了。不少初级开发者在这上面吃过亏改了一大段代码回 CubeMX 调一下引脚回来发现代码全没了。2.3 一次触发到一次搬移的完整时序把这一整条链路拆成步骤看其实每一步都有明确的标志位TIM1 自由运行计数器从 0 数到 ARR 产生更新事件更新事件经过 TRGO 路由在 TRGO 引脚或内部连线上输出一个触发脉冲ADC1 收到外部触发启动一次规则组转换转换完成ADC 数据寄存器写入结果EOC 置位ADC 的 DMA 请求信号拉高DMA1 Stream 0 响应请求DMA 把数据寄存器里的半字搬到内存缓冲区内存地址自增当 DMA 搬完设定的数据量触发传输完成中断进入 HAL 的回调函数。这个链路里最容易踩坑的是第 2 步到第 3 步的衔接。ADC 的外部触发源有个触发边沿选择如果配成下降沿触发但定时器输出的是上升沿脉冲ADC 永远不会启动。CubeMX 里 ADC 的 External Trigger Edge 选项有 Rising Edge、Falling Edge、Both Edges 三种实际工程里绝大多数配置都选 Rising Edge这一点在改触发源时特别容易忽略。第 5 步也需要注意。ADC 的 DMA 请求不是所有模式下都会持续产生的如果把 ADC 配置成单次转换DMA 搬完设定的次数后就不再出发了。要想实现「一直采不停搬」ADC 侧要开启 DMA Continuous RequestsDMA 侧要选择 Circular 循环模式两个开关缺一个数据流都会中断。3. CubeMX 初始化实战时钟树、触发源与 DMA 通道的三个关键配置3.1 时钟树先弄清楚定时器和 ADC 各吃哪路时钟H743 的时钟树比 F103 复杂得多第一次用容易直接愣住。它的 CPU、总线、外设各自有独立的时钟源不是像老芯片那样一根总线挂所有。ADC 的时钟来自 ADCCLK定时器的时钟来自 TIMx 所挂总线的定时器倍频器两者的分频配置在 CubeMX 的 Clock Configuration 页面里是完全独立的两块。如果上来就先调 PLL 让主频跑到 480MHzADC 和定时器的时钟未必是你以为的值。我的做法是先在 CubeMX 的 Clock Configuration 页面里把目光聚焦到两个角落左上角的 ADC 分频器以及 TIM1 所挂总线的定时器时钟。H743 的 ADC 时钟一般给到几十 MHz 级别分频值从 2、4、8、16 里选具体看外设频率目标TIM1 的计数时钟通常能达到频率很高的总线定时器时钟。这个数值影响后面对定时器分频和重载值的计算所以先把它记下来。时钟配置做完之后会自动生成 SystemClock_Config 函数里面是一串 HAL_RCC_xxx_Config 调用这段一般不需要手动改但要看一眼 ADC 和定时器时钟的最终频率。如果后面发现采样率不对回到这里核对是最快的排查方式。3.2 定时器配置PSC、ARR 与 TRGO 的计算定时器在本工程里的角色不是产生中断而是产生触发脉冲所以配置上只需要关心三件事计数时钟、更新频率、触发输出。CubeMX 里 TIM1 的 Clock Source 选择 Internal ClockPrescaler 和 Counter Period 按目标采样率计算Trigger Output Trigger 选择 Update Event这样每次计数器溢出都会在 TRGO 引脚产生一个事件。采样率的计算公式是采样率 定时器计数时钟 / (PSC 1) / (ARR 1)假设你在时钟树里看到 TIM1 的计数时钟是 240MHz想要 1MSPS 的采样率常见做法是 PSC 设为 0ARR 设为 239算出来正好是 240MHz / 240 1MHz。如果你的目标采样率和时钟频率除不净先调整 PSC 让计数时钟能被整除再算 ARR这样采样间隔不会出现周期性的抖动。定时器的初始化代码一般是这样的CubeMX 生成后基本不需要动static void MX_TIM1_Init(void) { TIM_MasterConfigTypeDef sMasterConfig {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; htim1.Init.CounterMode TIM_COUNTERMODE_UP; htim1.Init.Period 239; // 1MSPS 对应的重载值 htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(htim1); sMasterConfig.MasterOutputTrigger TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(htim1, sMasterConfig); }代码逻辑说明Prescaler 是预分频值实际分频系数是 PSC 加 1Period 是自动重载寄存器 ARR计数器数到它就会溢出并产生更新事件。TIM_TRGO_UPDATE把更新事件路由到 TRGO这是整个触发链路的源头。AutoReloadPreload开启预装载表示修改 ARR 后要等当前计数周期结束才生效避免运行中改采样率时出现半个长周期半个短周期的情况。3.3 ADC 与 DMA触发边沿、半字宽度与启动顺序ADC 的配置里和定时器联动的关键参数都在 CubeMX 的 ADC1 设置页面上。Resolution 选 12 bitScan Mode 保持 DisableContinuous Conversion 必须关掉因为采样节奏由定时器管ADC 自己连续转换会打乱节奏。External Trigger Source 选择 TIM1 的 TRGO 事件External Trigger Edge 选 Rising Edge采样时间按你的信号源阻抗来定阻抗大就选长一点一般从 16.5 cycles 起步。DMA 部分ADC1 的 DMA 请求在 CubeMX 里添加到 DMA Settings 后默认会生成到 DMA1 Stream 0。Direction 为 Peripheral To MemoryMode 选 Circular外设数据宽度和内存数据宽度都设成 Half Word因为 12 位 ADC 的结果用一个 16 位无符号整数就能装。这里宽度设置不一致搬出来的数据就会错位后面避坑章节还会提到。启动代码是这套工程里最关键的一段// ADC 校准H7 系列建议上电后做一次 HAL_ADCEx_Calibration_Start(hadc1); // 以 DMA 方式启动 ADC1 转换 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE); // 启动 TIM1 基本定时器开始产生触发脉冲 HAL_TIM_Base_Start(htim1);逻辑说明第一步做 ADC 校准H7 的 ADC 内置线性校准逻辑不做的话转换结果整体偏一个值第二步把 ADC 和 DMA 接起来最后一个参数是转换次数也就是 DMA 一次传输的元素个数不是字节数第三步启动定时器触发链路开始跑。注意第二和第三个函数的调用顺序不能反先启动定时器再启动 DMA 会导致前面的触发事件丢失。HAL 库对这种顺序的错误没有报错机制表现出来就是什么事情都没发生很难查。4. 核心代码逐段拆解双缓冲回调与数据换算的实现细节4.1 缓冲区与标志定义工程代码最顶部的定义区决定了整个数据搬运的骨架。见过不少工程直接只用一个数组DMA 满就中断处理处理完再覆盖这样数据一忙就丢。这个工程的做法是把缓冲区拆成前半段和后半段配合 DMA 传输完成中断交替处理。#define ADC_BUFFER_SIZE 1024 // 每次 DMA 传输的转换次数 static uint16_t adc_buffer[ADC_BUFFER_SIZE]; static uint16_t adc_half_buffer[ADC_BUFFER_SIZE / 2]; volatile uint8_t adc_full_flag 0; // 后半段已填满 volatile uint8_t adc_half_flag 0; // 前半段已填满参数说明adc_buffer 是 DMA 搬运的目标地址DMA 会从缓冲区的开头往里写写到尾部再从开头重新写adc_half_buffer 是给应用层取数据的暂存区分成两半是为了让处理逻辑和数据采集错开。两个 flag 是主循环和中断回调之间的通信渠道用 volatile 修饰避免编译器优化导致主循环读不到最新值。4.2 主循环里只管启动后面交给硬件主函数的逻辑简单到不像一个高速采集工程int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_TIM1_Init(); HAL_ADCEx_Calibration_Start(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buffer, ADC_BUFFER_SIZE); HAL_TIM_Base_Start(htim1); while (1) { if (adc_half_flag) { adc_half_flag 0; process_samples(adc_half_buffer, ADC_BUFFER_SIZE / 2); } if (adc_full_flag) { adc_full_flag 0; process_samples(adc_half_buffer, ADC_BUFFER_SIZE / 2); } } }这段代码最核心的设计是初始化顺序DMA 先于 ADCADC 先于定时器。MX_DMA_Init 必须在 MX_ADC1_Init 前面因为 HAL_ADC_Init 内部需要把 DMA 句柄关联到 ADC 的 DMA 请求上如果此时 DMA 还没初始化句柄就是 NULL链路就断了。主循环里没有采样相关的代码只检查两个标志位这样的好处是采样频率完全由硬件决定主循环里哪怕加了几行耗时的浮点计算也不会影响采样的时间轴。4.3 中断回调半满与全满的交替处理DMA 在循环模式下每搬完半段缓冲区就会触发一次中断HAL 库把这些中断统一转成两个回调函数代码里只需要重写它们void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_half_flag 1; } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { adc_full_flag 1; } }逻辑说明DMA 传输半满时触发 ConvHalfCplt 回调5 传输全满时触发 ConvCplt 回调。因为缓冲区有 1024 个元素半满是 512全满是 1024真实含义是前半段写完了或者后半段写完了。主循环拿到 flag 后要尽快处理对应的半段数据因为 DMA 还在以循环模式继续写缓冲区处理慢了新写入的数据就会覆盖还没算完的数据。这里有个细节要注意回调里只置标志位不做数据处理。中断回调里做乘除运算会拖长中断时间如果 DMA 传输在一段时间内没有被及时处理数据就会被覆盖表现为波形出现周期性的毛刺。4.4 数据换算从原始值到电压和有效值ADC 转换出来的原始值是 0 到 4095 之间的整数要变成有物理意义的电压需要一个简单的换算float adc_raw_to_voltage(uint16_t raw) { return raw * 3.3f / 4095.0f; } float calc_rms(const uint16_t *buf, uint32_t len) { uint64_t sum 0; for (uint32_t i 0; i len; i) { int32_t v buf[i] - 2048; // 去掉直流分量再平方 sum (uint64_t)v * v; } return sqrtf((float)sum / len) * 3.3f / 4095.0f; }逻辑说明raw_to_voltage 假设参考电压是 3.3V 的典型接法如果你的板载参考电压不是 3.3V把 3.3 换成实际值即可calc_rms 先把每个采样点减去 2048还原成交流信号相对零点的偏移再平方求和取平均开根号就是标准的均方根算法。这里用了 uint64_t 累加防止 1024 个点的平方和超过 32 位表达范围。如果你做的是 DMA 多通道采样raw 数组里会交替出现不同通道的数据就不能用这个函数了需要先按通道把数据拆开再逐通道做换算。5. 避坑记录采样率上不去、数据错位和抖动噪声的三个真实案例5.1 采样率上不去改了 ARR 也没用现象定时器周期怎么调实际采样率都停在某个值用逻辑分析仪看触发波形频率和设定值对不上。我刚开始也以为是 ARR 算错了反复把计算公式套了好几遍依然对不上。原因定时器的计数时钟不是我以为的 240MHz。H743 的 APB 总线有预分频定时器输入时钟往往是 APB 时钟的两倍但前提是总线分了频。如果我拿到 CubeMX 默认配置直接改 ARR没看 TIM1 实际挂的时钟频率算出来当然不准。另外一个隐藏限制是 ADC 转换时间单次转换需要的总周期是采样时间加上固定周期如果定时器触发的频率高于 ADC 的转换速度ADC 根本没转完就收到下一个触发采样率就被 ADC 的转换时间卡住了。解决先回到 CubeMX 的 Clock Configuration 页面记下 TIM1 的实际时钟再重新算 PSC 和 ARR。同时把 ADC 的采样时间调短比如从 64.5 cycles 改到 16.5 cycles再用公式验证一遍触发频率是否小于 ADC 最大转换频率。这两个都确认了采样率才能上去。5.2 DMA 搬来的数据错位波形像被切碎了现象把缓冲区数据打印出来波形每隔一段就出现一次跳变像是把两个通道的数据混在了一起单通道采集也这样。原因DMA 数据宽度配置和外设寄存器不匹配。ADC 数据寄存器是 16 位DMA 外设端如果配成了 Word 宽度它每次会读走 32 位把相邻的两个转换结果拉在一起拼成一个乱七八糟的半字。更隐蔽的一个原因是 DMAMUX 的请求映射被 CubeMX 重新生成时改掉了ADC1 的请求被映射到了别的 DMA Stream地址对不上搬出来的数据自然不对。解决检查 DMA 配置外设端和内存端数据宽度都选 Half Word。然后核对 DMAMUX 里 ADC1 对应的请求映射是 ADC1 的通道别的 ADC 的请求映射到同一个 Stream 上结果互相覆盖也会出现类似错位。H743 的外设多了这种配置错误在 CubeMX 里不仔细看很难发现我一般会同时在调试器里看 DMA 的 Current Data Counter和缓冲区长度对不上说明配置被改过。5.3 转换值噪声大信号里叠了一层毛刺现象采集 50Hz 正弦波波形外围能看到一层粗颗粒理想情况下应该是一条平滑的曲线。FFT 之后高频部分多了很多乱七八糟的谱线。原因这里有两种可能。第一是 ADC 采样时间设太短内部采样电容没有完全充满到输入电压转换结果里带上了电容充放电的误差第二是模拟参考电压不稳VREF 引脚或者 VDDA 上的纹波直接耦合进了转换结果这种情况下无论怎么改采样参数都没有用。解决先把采样时间从几 cycle 改到 32.5 或者 64.5 cycles差值减下来再说。如果还不行用万用表看 VDDA 引脚电压是不是稳定纹波大就在 VDDA 和 VSSA 之间加一颗高质量的磁珠和一两个去耦电容参考电压要求更高的话可以外接独立的基准源。做信号采集的板子模拟电源和数字电源的隔离问题迟早要面对。5.4 定时器正常触发但 DMA 中断一次都不进现象定时器在跑ADC 也在转换理论上 DMA 应该持续搬运但断点跟踪发现 HAL_ADC_ConvCpltCallback 从未被调用缓冲区里一直是 0。原因这是初始化顺序和中断优先级双重翻车造成的。如果 MX_DMA_Init 在 MX_ADC1_Init 之后执行ADC 初始化时拿到的 DMA 句柄是空的HAL_ADC_Start_DMA 实际上没有启动任何搬运。另外DMA 的传输完成中断虽然默认使能但如果 NVIC 里对应 Stream 的中断没有打开或者优先级配得和同中断组里的其他中断冲突回调函数就不会被调用。解决强制在 main 函数里把 MX_DMA_Init 提到 MX_ADC1_Init 之前顺序错了其他全对也白搭。然后在 NVIC 设置里打开 DMA1_Stream0_IRQn优先级可以给一个较高的等级但不能和 SysTick 同级否则 tick 中断和 DMA 中断互相抢占系统会出现诡异卡顿。这两个检查完中断基本就能进到了。6. 二次开发思路把固定 1MSPS 采样改成可变速多通道方案6.1 采样率改成运行时可调固定 1MSPS 在调试阶段够用真做产品往往需要在运行中切换采样率比如启动时用低采样率判断信号类型判断完切到高速采集。实现方式是在运行中修改定时器的 ARR 寄存器因为开启预装载后新值会在下一轮溢出时生效不会破坏正在进行的采样周期。void set_sample_rate(uint32_t sample_rate) { uint32_t timer_clock TIM1_CLOCK_HZ; // 在时钟树里确认后的值 __HAL_TIM_SET_AUTORELOAD(htim1, timer_clock / sample_rate - 1); }注意 sample_rate 不能超过 ADC 的最大转换频率切换后最好重新读取一次实际 ARR确认寄存器写入生效。6.2 多通道与注入组需要同时采多路信号时把 ADC 的 Scan Mode 打开在 CubeMX 里配置通道数量和每个通道的采样时间。DMA 缓冲区长度改成 通道数乘以每通道点数数据在缓冲区里按通道顺序交替存放换算前先按通道拆开。如果各路信号的重要性不一样可以用注入组实现「规则组在采注入组插队」的效果注入组的触发源独立配置转换完成也走独立的回调互不打扰。6.3 用内部参考做简单校准H743 内置了 VREFINT 参考电压通道和校准数据上电时读取 TS_CAL1 这类出厂校准值可以修正 ADC 的增益误差。做法是在初始化阶段把内部参考通道配置成 ADC 的一个输入采集一段数据求出实际增益系数之后所有通道的换算都乘上这个系数。校准的数据放在芯片的 OTP 区读出来直接用就行不需要自己标定。这个工程我前后复现过几次每次换一块新的 H743 板子都强制要求自己先从时钟树看起再顺一遍 DMA 初始化顺序最后才敢往上接信号源。这套流程能省下大把排查问题的时间也让我少踩了好几次「配置没问题但就是不出数据」的坑。希望帮你少走这些弯路。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。