
简介在嵌入式数据采集与电机控制中ADC采样率提升常导致CPU被中断拖累。利用定时器硬件触发ADC转换再配合DMA双缓冲乒乓缓冲搬运数据可让采样链路几乎不占用CPU资源。STM32H743基于异步时钟域的ADC与高级定时器TRGO事件为实现稳定触发频率和低抖动采样提供了硬件基础。通过CubeMX配置TIM1触发源、ADC通道与循环DMA并在半传输/全传输中断中交替消费缓冲区即可构建高速、低延迟的数据流。该方案适用于振动采集、音频分析以及FOC电流环等场景是H7系列工程中兼顾吞吐与实时性的典型实践。1. STM32H743 定时器触发 ADC DMA 双缓冲让高速采样不再拖死 CPU做电机 FOC 电流环或者振动采集时最怕的就是采样点来了CPU 还在处理上一轮的算法。用 HAL 库默认的轮询方式读 ADC看起来逻辑简单实际上一旦采样率提到几百 kHz主循环几乎被 EOC 等待占满稍微有点中断抖动就会丢数。stm32h743 单片机开发里常用的解法是把定时器触发、ADC 转换和 DMA 搬运串成一条硬件链路定时器到点ADC 自动启动转换完成DMA 自动把结果搬进内存搬满一半或搬完一整块再通过中断通知 CPU 来消费。CPU 该算算法算算法该跑通信跑通信采样这件事几乎不占资源。这份源码就是一个可以照着改的完整工程适合做数据采集、电机控制、音频分析也适合刚上手 H7 双缓冲 DMA 的新手拿来当模板。2. H743 的 ADC 触发链路与 DMA 双缓冲原理、选型与两条实现路径2.1 三种 ADC 启动方式为什么非要定时器触发ADC 启动这件事HAL 库给了三种常规思路轮询、中断、定时器触发。轮询是 while 循环里反复查转换完成标志适合低频、单通道、对 CPU 占用不敏感的场景中断是每次转换完成进一次中断在中断里把数据读走适合中等频率但中断频率一高上下文切换的开销就很可观定时器触发则完全不同ADC 的启动信号来自定时器硬件事件转换结果由 DMA 搬走整个过程 CPU 只在 DMA 半满或全满时被打断一次。在 H743 上定时器触发不是可选项而是必选项原因在于 H7 的 ADC 是挂在异步时钟域上的。CubeMX 里 ADC 的 Clock Prescaler 作用于 adc_ker_ck这颗时钟可以独立配置不跟 APB2 完全绑定。只要定时器 TRGO 事件能稳定产生ADC 就能在完全确定的时间点启动转换采样时刻的抖动只取决于定时器计数精度而不是中断响应延迟。这个特性对计算 ADC 信噪比非常关键——采样抖动直接决定高频信号采出来的 SNR 上限CPU 中断响应里的几十纳秒随机延迟足够把信噪比拉低好几个 dB。我一般会把「定时器触发 DMA 搬运」当成 H7 采样工程的默认架构轮询和中断只留作调试兜底。触发源常用 TIM1 或 TIM8这两路高级定时器能直接向 ADC 输出 TRGO 事件不需要额外飞线。选择哪个定时器主要看它是否同时承担 PWM 输出任务。电机 FOC 场景下 PWM 波形和定时器本来就共用 TIM1让 ADC 在 PWM 中心点触发采样时刻恰好避开开关噪声这比单独开一个定时器做触发更有工程价值。2.2 双缓冲的两种实现硬件 DBUF 与半传/全传乒乓DMA 双缓冲在 H743 上有两条实现路径很多人第一次接触时会被命名搞混。一条是寄存器级的硬件双缓冲DBUFDMA 控制器内部维护两个内存地址每次传输完成自动切换到另一块另一条是用 DMA 循环模式配合半传输、全传输两个中断自己搭乒乓逻辑业内也习惯叫它软双缓冲。硬件 DBUF 在 STM32H7 的 DMA_SxCR 寄存器里有专门的 DBUF 位开启后 DMA 会在 M0AR 和 M1AR 两个地址之间来回切换。听起来很方便但 HAL 库对它的封装并不完整不像 F4 系列那样有现成的 MultiBuffer 接口工程里直接用的反而不多。真正主流的是第二种把一块连续内存切成前后两半DMA 开循环模式先写前半块写满触发半传输中断接着写后半块写满触发全传输中断。这样从 CPU 视角看DMA 正在写后半块时前半块可以安全消费反过来也一样形成无缝交替。下面这张表列出两者的差异方便按场景选型对比项硬件 DBUF半传/全传乒乓配置复杂度需要手写寄存器HAL 支持弱CubeMX 生成后少量改回调即可中断次数每满一块一次半满、全满各一次CPU 消费窗口一个整块的处理时间半个块的处理时间延迟更低适用场景内存带宽紧张、追求极致吞吐绝大多数工程含电机控制与采集双缓冲的核心收益在于生产者与消费者并行。DMA 是生产者不断往内存写CPU 是消费者在处理算法时不需要等采样停下。没有双缓冲时DMA 写完一整块才通知 CPU这期间 CPU 可能在空转等数据有了双缓冲CPU 在处理前半块时 DMA 已经在写后半块流水线不空转。后面要讲的代码就按乒乓路径展开这也是这份源码实际采用的方案。3. 定时器、ADC、DMA 三端打通CubeMX 参数与逐行初始化代码3.1 TIM1 触发源配置通用采集与电机 FOC 两种做法定时器触发 ADC 的配置点核心在 TRGO 事件的选择。CubeMX 里 TIM1 的配置界面有几个关键项Clock Source 选 Internal ClockMode 里按需要使能 PWM Generation CH1Trigger Output (TRGO) 选 Update Event。选 Update Event 的触发频率由定时器时钟和重载值决定适合做普通数据采集。假设 APB2 定时器时钟是 240MHz想要 600kHz 的触发频率PSC 设为 0ARR 设为 399频率就是 240M / (399 1) 600kHz。这个触发频率可以稳定驱动单通道 ADC 跑到 600kSPS对大多数振动和音频采集都足够。电机 FOC 的场景要换一种配置。FOC 里 PWM 载波通常用中心对齐模式PWM 波形关于计数周期中点对称开关管在计数值等于比较值的时刻动作会有明显的 dv/dt 噪声。ADC 采样要刻意避开这个时刻采在 PWM 中间才最干净。配置方法是 TIM1 的 Counter Mode 选 Center Aligned ModeTrigger Output 不选 Update Event而是选 OC1REF也就是比较脉冲。CubeMX 里 TRGO 选项较多FOC 工程里常见的是用 TIM1 的 TRGO2它能独立选择比较事件作为触发源这样触发时刻精确落在 PWM 中间相位。通用采集和 FOC 两种方案CubeMX 参数差异我用一张表列出来配置项通用采集电机 FOC计数模式Up CountingCenter Aligned Mode 1频率/周期ARR399PSC0按载波频率定 ARR如 20kHz 载波 ARR5999TRGO 事件Update EventOC1REF 或 TRGO2 比较脉冲触发相位每次计数溢出时PWM 中间点避开开关噪声适用通道ADC1 单通道ADC1 三路电流或母线电压实际配置时我习惯先在 CubeMX 里把 TIM1 调出 PWM 波形再用逻辑分析仪确认边沿周期最后才接 ADC 触发。跳过这一步直接进代码调出问题时很难分清是定时器的问题还是 ADC 配置的问题。3.2 ADC 初始化与 DMA 请求绑定ADC 侧的初始化代码分三块外设基本参数、校准、DMA 请求绑定。H7 的校准函数和 F4 不一样多了偏移校准参数不调用校准直接采样零点误差会偏大新手容易在这翻车。/* ADC1 外设配置 */ hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; /* adc_ker_ck 四分频 */ hadc1.Init.Resolution ADC_RESOLUTION_12B; /* 12 位分辨率H7 可配到 16 位 */ hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.ScanConvMode ADC_SCAN_ENABLE; /* 多通道扫描必须开 */ hadc1.Init.EOCSelection ADC_EOC_SEQ_CONV; /* 一个序列全部转换完再置 EOC */ hadc1.Init.ExternalTrigConv ADC_EXTERNAL_TRIGCONV_T1_TRGO; /* TIM1 TRGO 触发 */ hadc1.Init.ExternalTrigConvEdge ADC_EXTERNAL_TRIGCONV_EDGE_RISING; /* 上升沿启动 */ hadc1.Init.ContinuousConvMode DISABLE; /* 禁止连续转换由定时器决定节奏 */ hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.NbrOfConversion 3; /* 3 个通道依次转换 */ HAL_ADC_Init(hadc1);H7 的转换时钟分频和 F4 语义不同这里 ClockPrescaler 分的是 ADC 异步时钟不是 APB2 时钟。H743 的 ADC 时钟上限大约 36MHz如果配得太高转换结果会出现偶发的符号位跳动采样值看着是 12 位但低两位不稳定。分辨率选 12 位还是 16 位要权衡16 位模式实际上是硬件过采样转换时间明显变长对高采样率场景未必划算。通道配置是重复动作三个通道写三遍每个通道单独设置采样周期。采样周期直接影响 ADC 的输入阻抗匹配和信噪比信号源内阻大时采样周期要放宽但放宽意味着单次转换时间变长限制了最高触发频率。sConfig.Channel ADC_CHANNEL_0; sConfig.SamplingTime ADC_SAMPLETIME_8CYCLES_5; /* 采样周期 8.5 个 ADC 时钟 */ sConfig.SingleDiff ADC_SINGLE_ENDED; /* 单端输入 */ sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; HAL_ADC_ConfigChannel(hadc1, sConfig);DMA 请求绑定放在 MspInit 里CubeMX 会自动生成骨架但有几个参数要手动确认DMA 方向是外设到内存内存递增开外设地址递增关数据宽度半字。ADC 的结果寄存器是 16 位有效用半字搬运既能省一半总线带宽也避免高字节无效数据混进缓冲区。hdma_adc1.Instance DMA1_Stream0; hdma_adc1.Init.Request DMA_REQUEST_ADC1; /* DMAMUX 把 ADC1 请求映射到该流 */ hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; /* 外设地址固定为 ADC DR */ hdma_adc1.Init.MemInc DMA_MINC_ENABLE; /* 内存地址递增 */ hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; /* 循环模式双缓冲的前提 */ hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1);循环模式里 DMA 会持续响应外设请求相当于一种连续请求模式。H7 的 DMA 支持突发传输可以把几个请求合并成一次总线突发减少总线仲裁次数但突发长度配得过大、内存端口带宽不足时反而会阻塞其他 DMA 通道工程上先用 SINGLE 跑通再调 BURST 更稳。3.3 双缓冲缓冲区布局与启动代码缓冲区布局是双缓冲工程的核心我一般把两个子缓冲定义成一个二维数组DMA 从首地址开始循环写入前半块视为 BUF_A后半块视为 BUF_B。数组必须 32 字节对齐否则后面做 Cache 操作会出问题。#define CH_NUM 3 /* ADC 通道数 */ #define BLOCK_SIZE 512 /* 每个通道每个缓冲区的采样点数 */ __attribute__((aligned(32))) uint16_t adc_buf[2][CH_NUM * BLOCK_SIZE]; /* 前半 BUF_A后半 BUF_B */ #define BUF_A adc_buf[0] #define BUF_B adc_buf[1]启动代码比预期简单真正的工作全在中断回调里HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, CH_NUM * BLOCK_SIZE);第三个参数是 DMA 要搬运的次数而不是缓冲区字节数这里写 1536对应 3 通道乘 512 点。DMA 搬完前半块触发半传输中断搬完整个数组触发全传输中断。中间不需要任何手动参与定时器只要不停止DMA 就会按触发节奏持续填数。有一点要特别说清楚这份资源里的「双缓冲」不是指 HAL_ADC_Start_DMA 传了两个缓冲地址而是依靠循环 DMA 天然地把一块连续内存分成前后两半来交替使用。这两半的边界由 DMA 数据长度决定前半块填满时 DMA 自动回到起始地址继续写不会等你处理完。这就是双缓冲并行性的来源。4. 乒乓缓冲与数据消费让每个采样周期都被完整接管4.1 半传输与全传输回调双缓冲的「换班」时机DMA 循环模式下半传输中断对应前半块写满全传输中断对应整块写满也就是后半块写满。两个回调就是双缓冲的换班信号。HAL 库的默认回调是弱函数直接在用户文件里重写即可。volatile uint8_t buf_a_ready 0; volatile uint8_t buf_b_ready 0; void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { buf_a_ready 1; /* BUF_A 已填满可以消费 */ } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { buf_b_ready 1; /* BUF_B 已填满可以消费 */ } }回调里只置标志位不做数据处理。原因有两条一是中断里跑算法会拉长中断时间其他高优先级中断可能被堵住二是数据处理耗时不确定万一处理时间超过 DMA 填满一块的周期DMA 已经开始覆盖缓冲区数据就毁了。标志位由主循环消费这个模式是嵌入式里最常见的生产者-消费者模型。每次回调进来CPU 拿到的是一整块连续的三通道数据。通道排列顺序由 ADC 序列寄存器决定第一个转换的通道 0 在最前接着通道 1、通道 2三个一组循环。消费端取数据时要按这个交错结构解析。void process_adc_block(uint16_t *buf, uint32_t total_samples) { uint32_t i, group_count total_samples / CH_NUM; uint32_t ch0_sum 0; for (i 0; i group_count; i) { ch0_sum buf[i * CH_NUM]; /* 每组第一个是通道 0 */ } ch0_avg ch0_sum / group_count; /* 均值滤波简单去噪 */ }参数上注意 total_samples 传入的是 CH_NUM * BLOCK_SIZE循环里按 CH_NUM 步进才能保证每个通道的数据都被正确取到。这块代码如果循环步长写错最常见的现象就是三个通道的数据互相串位看着像通道间相位偏移实际是解析逻辑错了。4.2 消费数据的同步标志位、临界区与主循环主循环的消费逻辑要处理好两个问题一是同一块数据不能被消费两次二是 DMA 正在写的块不能被消费。乒乓结构能解决这两个问题但标志位的清空时机必须小心。while (1) { if (buf_a_ready) { buf_a_ready 0; /* 先清标志再处理 */ process_adc_block(BUF_A, CH_NUM * BLOCK_SIZE); } if (buf_b_ready) { buf_b_ready 0; process_adc_block(BUF_B, CH_NUM * BLOCK_SIZE); } }先清标志再处理这个顺序是我的习惯可以把临界区压到最小。如果先处理再清标志处理过程中 DMA 又触发了一次同类型中断标志位会被重新置上等处理完再清会把新数据就绪信号误删导致丢一块数据。虽然 DMA 循环模式要过一整轮才会再次触发相同中断但半传输和全传输回调彼此独立临界问题依然存在养成先清后用的习惯能少踩很多坑。主循环里还要留意处理耗时。单块处理时间必须低于 DMA 填满一块的时间。以 600kHz 触发率、每块 512 点、三通道为例一块数据是 1536 次转换DMA 填满一块约 2.56ms半块约 1.28ms。如果你的处理循环里跑了浮点 FFT一次耗时超过这个数DMA 就会覆盖仍在消费的数据。此时有两个方向一是把 BLOCK_SIZE 加大延长单块时间二是把处理任务拆到不同优先级线程用 RTOS 的信号量来做同步而不是裸奔主循环。这套源码里裸奔主循环的写法适合学习上产品时我会建议换 RTOS 版本。5. 常见问题与排查触发断链、缓存一致性与通道错位的坑5.1 触发类问题采样率不对、DMA 中断不进现象 1实际采样频率是预期的一半或两倍。原因通常是中心对齐模式引发的。中心对齐模式下定时器先向上计数再向下计数一个完整周期里 Update 事件会出现两次如果 TRGO 选的是 Update EventADC 触发频率就是预期值的两倍。解决方法是把 TRGO 改成比较事件或者在计算 ARR 时主动把触发频率除 2。我曾经在 FOC 调试时遇到过电流波形周期比设定载波高一倍查了半天 PWM最后发现是 TRGO 事件选错。现象 2定时器在跑PWM 波形正常但 ADC 数据纹丝不动。原因大概率是外部触发边沿没配。HAL 库里 ExternalTrigConvEdge 默认是 None必须显式配置成 Rising 或 Falling。配置寄存器之后确认接线ADC 的外部触发来自定时器内部事件不需要 GPIO 连接CubeMX 里如果看不到 TIM1 的触发选项检查定时器的 Trigger Output 有没有在 Mode 里使能。现象 3DMA 中断一直不进程序卡在 HAL_ADC_Start_DMA 后的等待里。原因多半是 NVIC 没使能 DMA 中断。CubeMX 生成代码时如果 MX_DMA_Init 之后没有看到 HAL_NVIC_SetPriority 和 HAL_NVIC_EnableIRQ 调用DMA 传输完成不会产生中断回调。解决方法是手动补上中断使能代码并确认中断优先级比主循环里其他快速中断低或相当避免回调被无限抢占导致标志位长期不更新。5.2 数据类问题缓存不一致、通道错位、Overrun现象 4打开 D-Cache 后ADC 数据偶发旧值波形出现周期性毛刺。这是 H743 特有的坑本质是缓存一致性问题。DMA 写内存是绕过 CPU 缓存的数据到达物理内存后D-Cache 里可能还存着旧值CPU 读缓冲时命中的是 Cache 里的过期数据。现象像玄学波形大部分时间正常偶尔冒出一个旧数据点。解决方法是把缓冲区声明改成 32 字节对齐并且在消费之前调用 Cache 失效操作。SCB_InvalidateDCache_by_Addr((uint32_t *)BUF_A, (int32_t)sizeof(BUF_A)); SCB_InvalidateDCache_by_Addr((uint32_t *)BUF_B, (int32_t)sizeof(BUF_B));注意这里用的是 Invalidate 而不是 Clean因为 DMA 写内存方向是外设到内存CPU 只需要丢弃旧缓存不需要把缓存里的脏数据写回。如果缓冲区没对齐这个函数会报错或无效。我一般会在工程初始化里把 D-Cache 的启用和缓冲区对齐做成编译期断言防止后续加代码时新缓冲区没对齐就上线。现象 5三通道数据整体错位通道 1 的数据显示在通道 0 的位置。原因在 ADC 序列配置的 Rank 顺序和 DMA 数据解析不一致。HAL_ADC_ConfigChannel 里每个通道都有 Channel 和 Rank 两个参数Rank 决定转换顺序DMA 缓冲区里数据就按 Rank 排列。如果通道配置顺序是 Rank1通道1、Rank2通道0缓冲区里第一个位置就是通道 1 的样本主循环按 CH_NUM 步进解析时自然会错。检查方法很简单把三个输入分别接固定电平采集后看缓冲区前三个数的排列能直接确认 Rank 顺序。现象 6ADC 错误回调里报 Overrun采样值卡在 0 或满量程。Overrun 表示 ADC 转换完成时DMA 还没把上一次数据搬走数据寄存器被新值覆盖。常见原因有两个ADC 时钟配置过高导致转换时间短于 DMA 响应时间或者 DMA 优先级太低被其他高优先级 DMA 流抢占。解决方法是先降低 ADC 时钟分频系数把转换时间拉长同时把 DMA 优先级提到 High。如果还在报 Overrun检查 DMAMUX 的请求映射是否配到了低速总线端口。6. 验证与进阶测触发频率、算 ADC 信噪比、试试多 ADC 交叉采样双缓冲工程跑通之后验证不能只看波形像不像要量化触发频率和 ADC 信噪比。先验证触发链路TIM1 的 PWM 输出直接接到另一个定时器的输入捕获通道比如 TIM2_CH1测一下 PWM 频率是否符合设定值。触发频率验证这一步做扎实后面所有采样率推断都建立在这个数字上。我的习惯是每次改 ARR 或 PSC 后都强制用输入捕获或频率计测一遍而不是心算完就信了。接着验证 ADC 链路质量。给一个通道输入标准正弦波采集一段数据在 PC 端做 FFT观察频谱里有没有明显杂散。12 位 ADC 的理论信噪比大约是 74dB工程上能跑到 60dB 以上说明链路基本健康。如果 FFT 里出现很多与输入信号无关的尖峰优先检查采样触发抖动和 D-Cache 一致性处理这两个是 H7 采集系统里最常见的杂散来源。/* FFT 输入整理示例取 BUF_A 中通道 0 的数据 */ for (int i 0; i BLOCK_SIZE; i) { fft_input[i] BUF_A[i * CH_NUM]; }进阶方向有三条。一条是把缓冲加深到四块做「四缓冲」消费延迟更低代价是内存占用线性增加。另一条是把这套乒乓思路平移到串口 DMA日志输出同样可以做到不阻塞主循环。第三条更有价值H743 有两个 ADC配成双 ADC 交叉采样模式两个 ADC 交替转换同一个通道在相同的定时器触发频率下把有效采样率翻倍。配置时需要给两个 ADC 设置不同的相位偏移CubeMX 里能直接生成但双缓冲的回调要同时处理两个 ADC 的中断标志位要区分 adc1_ready 和 adc2_ready逻辑比单 ADC 稍微绕一点。从那以后我每次在新板子上调试 ADC都强制自己按固定顺序过一遍先测定时器触发频率再确认 DMA 半传/全传回调进入次数然后接标准信号看 SNR最后才碰控制算法。这套流程帮我把采样链路的变量隔离得干干净净排查问题再也不用靠猜。希望帮到你。本文还有配套的精品资源点击获取