STM32 定时器触发 ADC + DMA 实战:把采样间隔锁死在微秒级 我早年做信号采集第一版就是图省事在main循环里HAL_Delay(5)然后读一次 ADC循环里再顺手printf打个调试、算个浮点均值。示波器上看采集到的正弦波波形是连着的我也就没当回事。直到有天想看看它的频谱把数据拖进 Matlab 做 FFT——好家伙一个干净的单频信号频谱上拉出一串旁瓣跟被狗啃过一样。后来才反应过来问题不在 ADC在采样间隔不平。我那 5ms 的间隔是名义值真跑到HAL_ADC_GetValue那一行的时候前面printf卡了 200µs、printf后面浮点运算又挤了 100µs实际间隔在 4.7ms 到 5.3ms 之间乱跳。采样时刻一抖等间隔采样这个前提就破了FFT 不漏才怪。所以这一篇就讲一件事怎么让 ADC 的每一次转换都卡在固定时间间隔上且不让 CPU 操心。答案就是定时器触发 DMA。一、为什么非得用定时器触发先说软件触发为什么不行。你在代码里调HAL_ADC_Start那一刻就是采样时刻。可主循环里永远有别的活儿——中断抢占了、某个函数比你估计的多跑了几个周期、甚至编译器优化顺序变了采样时刻就跟着飘。这种抖动是随机的、你控制不住的间隔误差轻松到几十甚至上百微秒。定时器触发TIM TRGO是另一回事。TIM 计数器每个周期溢出一次硬件自动产生一个触发信号TRGO这个信号直接喂给 ADC 的外部触发输入。整个过程不经过 CPU、不受代码影响所以采样间隔 定时器周期稳得像石英钟。整条链路是这样的TIM Update 事件 → TRGO 信号 → ADC 外部触发启动一次转换 → 转换完成 EOC → DMA 把结果搬进内存数组CPU 从头到尾没插手。转换一旦被触发就开始搬完 DMA 自动接着搬下一个采样间隔严格等于定时器周期采样间隔 定时器周期 TIM_CLK / ((PSC 1) × (ARR 1))这就是你的实际采样率fs TIM_CLK / ((PSC 1) × (ARR 1))比如 TIM 时钟 72MHz想采 10kHz(PSC1)×(ARR1) 7200就行PSC 设 71、ARR 设 99一除正好 10kHz误差是 0。再说双缓冲。单次采集你HAL_ADC_Start_DMA开个循环搬就行但你要做连续采集比如实时波形、FFT、电量计量数据像水龙头一样源源不断你处理速度跟不上 DMA 写入速度就会丢数。解决办法是 Ping-Pong把缓冲区一分为二DMA 写前半的时候你处理后半DMA 写后半的时候你处理前半两套交替谁也不挡谁。二、CubeMX 怎么配配置分三块定时器、ADC、DMA。1. 定时器 TIMx我用的 TIM3PrescalerPSC和 Counter PeriodARR按上面的公式算决定采样率。关键一项TRGO Parameters → Trigger Event Selection Update Event。意思是定时器每次溢出就从 TRGO 引脚内部信号线发一个触发脉冲给 ADC。定时器本身不需要进中断它只管计数和发触发信号。2. ADC我用的 ADC1External Trigger Conversion Source TIM3_TRGO选你刚才那个定时器。External Trigger Conversion Edge Rising Edge。★ Continuous Conversion Mode Disable必须关这一项开了ADC 会自己连续转定时器触发就被无视了。我们要的是定时器说采才采所以连续模式必须关由触发源驱动。多通道的话Scan Conversion Mode EnableRank 顺序就是数组下标顺序。DMA Continuous Requests Enable扫描多通道时每个通道转换完都申请一次 DMA。3. DMAMode Circular循环写完末尾回到开头继续。Data Width Word / Half WordADC 是 12 位存到 16 位的uint16_t最合适选 Half Word。开启Half Transfer和Transfer Complete中断——这就是 Ping-Pong 的两个开关。配完生成代码下面就是最少的驱动。三、最少的代码缓冲区开成两倍帧长前半给 DMA 写、后半你处理或者反过来。两个回调分别标记哪一半刚满#defineADC_BUF_LEN128// 一帧样本数单通道uint16_tadc_buf[2*ADC_BUF_LEN];// Ping-Pong前半 0..127后半 128..255volatileuint8_tbuf_half_ready0;// 0没满 1前半刚满 2后半刚满intmain(void){HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_DMA_Init();MX_ADC1_Init();MX_TIM3_Init();// 1) 定时器开始计数TRGOUpdate 事件自动往外发触发脉冲HAL_TIM_Base_Start(htim3);// 2) ADC 启动DMA 把每次转换结果搬进 adc_buf循环模式HAL_ADC_Start_DMA(hadc1,(uint32_t*)adc_buf,2*ADC_BUF_LEN);while(1){if(buf_half_ready1){// 前半(0..127)刚写满可以安全处理后半 DMA 正在写process_frame(adc_buf[0],ADC_BUF_LEN);buf_half_ready0;}elseif(buf_half_ready2){// 后半(128..255)刚写满前半 DMA 正在写process_frame(adc_buf[ADC_BUF_LEN],ADC_BUF_LEN);buf_half_ready0;}}}// DMA 搬满前半时触发0..ADC_BUF_LEN-1 这一段已经稳定可处理voidHAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef*hadc){if(hadc-InstanceADC1)buf_half_ready1;}// DMA 搬满整块时触发说明后半刚写完前半这段现在安全可处理voidHAL_ADC_ConvCpltCallback(ADC_HandleTypeDef*hadc){if(hadc-InstanceADC1)buf_half_ready2;}这段在干嘛HAL_TIM_Base_Start让定时器开始发触发脉冲HAL_ADC_Start_DMA让 ADC 每次被触发就转一次、转完 DMA 自动搬。DMA 走到缓冲区一半进HalfCplt回调你处理前半走到末尾进Cplt回调你处理后半。两半永远错开处理的时候那一端 DMA 不碰数据不会被覆盖。process_frame里你爱干啥干啥——甩个串口、算 RMS、攒够几帧丢给 FFT 都行。采样率是多少就是第一节那个公式算出来的fs写死成一个宏就行// 例TIM3 72MHzPSC71, ARR99 → fs 72000000/7200 10000#defineSAMPLE_RATE_HZ10000四、真正会坑你的几个点1. ADC 的 Continuous 没关。这是最高频的坑。你辛辛苦苦配了 TIM TRGO结果 ADC 自己还在连续转定时器触发根本不生效采样率对不上甚至完全失控。记住用触发源驱动Continuous 必须 Disable。2. TRGO 源选错。CubeMX 里 TRGO 能选 Update、OC1、OC2 等等。你要的是每个周期触发一次选Update Event。选成比较输出(OC)的话触发时刻跟着比较值走采样率算出来跟你的 PSC/ARR 公式对不上差得离谱。3. 双缓冲开启姿势。很多人以为HAL_ADC_Start_DMA直接给你双缓冲其实它默认是单缓冲 circular双缓冲得你自己在 DMA 配置里开 Double Buffer 模式或者像我上面那样用 Circular HT/TC 中断自己模拟 Ping-Pong。不开的话你还在处理前半DMA 已经绕回开头把前半覆盖了——数据直接错乱。4. 采样率超过 ADC 吞吐上限。ADC 一次转换要时间T_conv (采样时间 12.5) 个 ADC 周期。12 位固定 12.5 周期采样时间你配比如 1.5 / 7.5 / 239.5 周期。ADC 时钟一般最高 14MHz看型号所以最大采样率大概在 1MHz 量级封顶。你非要 2MHz 采样转换还没完下一次触发就来了结果就是丢转换、数值跳。5. 中断里活儿太多。process_frame如果你在里面搞个printf、跑个浮点大循环处理时间超过半帧时长下一帧来了你还没处理完覆盖就发生了。中断里只做拷贝置标志重活丢给主循环或 RTOS 任务。6. TIM 时钟和 ADC 时钟不是同一个源。算fs时你用的 TIM_CLK 要去时钟树里查不一定是系统主频。APB 分频后 TIM 时钟可能是 72MHz、ADC 时钟走另一路最大 14MHz 或 36MHz——两个频率混着用公式全错。7. 数据对齐没搞对。ADC 是 12 位可以左对齐或右对齐。右对齐时直接读就是 0~4095 的真实值左对齐读出来要右移 4 位。CubeMX 里默认右对齐你读adc_buf[i]直接用就行但如果你手改过对齐方式数值会差 16 倍波形看起来像被压扁。8. 双缓冲两半拼反。前半和后半顺序搞反波形整体错位半个周期做 FFT 相干采样直接废。HalfCplt 对应的是前半、Cplt 对应的是后半上面代码里buf_half_ready的赋值顺序别写反。五、完整例程骨架把前面三块拼起来就是能跑的最小系统TIM3 触发 ADC1单通道或多通道扫描DMA circular HT/TC 中断做 Ping-Pong主循环里拿到一帧就处理。多通道时把ADC_BUF_LEN换成通道数 × 每通道样本数Rank 顺序就是数组下标读的时候按通道 stride 取数即可。// 启动三连顺序无所谓但 TIM 必须 Start 否则没触发源HAL_TIM_Base_Start(htim3);HAL_ADC_Start_DMA(hadc1,(uint32_t*)adc_buf,2*ADC_BUF_LEN);// 之后主循环 两个 DMA 回调见第三节定时器定节奏、ADC 管转换、DMA 管搬运——三件套一合采样间隔误差直接归零。适合波形采集、FFT 频谱、电量/RMS 计量、振动分析、音频预处理等一切采样间隔必须准的场景。不适合超低速单次采样那种裸HAL_ADC_GetValue就够了杀鸡不用牛刀。进阶玩法注入通道Injected Channel——它能在规则通道扫描途中插队响应高优先级采样配合本篇的触发机制就能搭出一套定时采集 紧急采样的组合。