STM32F407 TIMER3 TRGO触发ADC+DMA连续采样实战 简介本资源是一份面向STM32嵌入式开发者的实战型代码工程聚焦于高精度、低CPU占用的定时触发AD采集方案适用于电机控制、传感器实时监测等对采样时序与效率要求较高的工业与物联网场景。资源完整实现了STM32F1系列中TIMER3的TRGO信号精准触发ADC转换并通过DMA自动将多通道采样结果连续搬移至双缓冲区避免中断频繁响应显著提升系统实时性与稳定性。压缩包共6个源码文件3个.c实现模块化初始化与中断处理3个.h提供函数声明与寄存器配置接口结构清晰、注释规范涵盖timer.c/adc.c/dma.c核心驱动及对应头文件总大小仅3KB轻量易集成。目前已有4104人学习下载读者可直接复用该工程框架快速掌握TRGO-ADC-DMA协同配置的关键时序关系、DMA双缓存切换逻辑及中断服务程序设计要点是深入理解STM32外设联动机制的优质入门与进阶参考。1. STM32 TIMER3 TRGO触发AD采集并自动DMA存入缓存为什么你调通了定时器和ADC却还是采不到连续波形很多做电机控制、音频前端或振动信号采集的开发者都卡在这样一个看似“全链路打通”实则“数据静默”的玄学现场TIMER3配置成主模式TRGO信号也确认能从CH2引脚测到方波ADC设置为外部触发、规则通道、连续转换DMA也启用了循环模式——但最终缓存里要么全是0要么是某几个固定值反复刷屏根本看不到随时间变化的真实模拟量。这不是硬件坏点也不是寄存器写错位而是触发时序、时钟域对齐、DMA缓冲区管理这三座大山没被真正踩实。本文不讲教科书式寄存器映射只聚焦于用STM32F407以最常见型号为例把TIMER3的TRGO作为ADC启动脉冲并让DMA在无CPU干预下稳定填满双缓冲区的完整落地路径。适合已能点亮LED、会用HAL库但对底层触发链路仍感黑匣子的嵌入式工程师以及需要在资源受限MCU上实现10kHz以上等间隔采样的实时系统开发者。2. 触发链路怎么搭从TIMER3主模式到ADC外部触发的硬连接逻辑要让ADC真正“听”TIMER3的话不能只靠库函数调用顺序必须理解物理信号流TIMER3产生TRGO → 这个信号被路由到ADC的EXTSEL → ADC检测到上升沿后启动一次规则组转换 → 转换完成EOC再通知DMA搬运结果。整个过程跨三个外设时钟域APB1 for TIM, APB2 for ADC, AHB for DMA任何一环时序失配都会导致丢点或锁死。2.1 配置TIMER3为主模式TRGO必须来自更新事件UG很多人误以为TRGO可以随便选一个事件输出比如CC1IF或COM。但ADC外部触发要求的是严格周期性、无抖动、与计数器重载完全同步的脉冲。只有更新事件UG满足这个条件——它在ARR重载瞬间产生且不受输入捕获、比较输出等干扰。因此必须将TIM3_CR2的MMS位设为010bUpdate event// 手动配置TIM3为主模式绕过HAL的封装陷阱 RCC-APB1ENR | RCC_APB1ENR_TIM3EN; // 使能时钟 TIM3-PSC 83; // 84MHz / (831) 1MHz 计数频率 TIM3-ARR 99; // 1MHz / (991) 10kHz 更新频率 TIM3-CR1 TIM_CR1_CEN; // 先不启动 TIM3-CR2 TIM_CR2_MMS_1; // MMS 010b → TRGO Update Event TIM3-EGR TIM_EGR_UG; // 手动触发一次更新确保TRGO初始态有效提示HAL_TIMEx_MasterConfigSynchronization()默认用的是TIM_CLOCKSOURCE_ETRMODE1这会把TRGO连到ETR引脚而非内部UG务必手动改写CR2寄存器。这是第一个血泪经验——别迷信HAL封装。2.2 ADC外部触发源选择EXTSEL必须对应TIM3_TRGOSTM32F4的ADC1/2/3共用一套EXTSEL编码但TIM3_TRGO在ADC1的EXTSEL表中编号为1011b即11不是常见的0100bTIM2_TRGO。查RM0090第13.4.15节EXTSEL编码表可确认若选错会导致ADC永远等不到触发信号// 配置ADC1为外部触发非软件触发 ADC1-CR2 ~ADC_CR2_EXTEN; // 清除原有触发使能 ADC1-CR2 | ADC_CR2_EXTEN_0; // 上升沿触发必须下降沿不工作 ADC1-CR2 ~ADC_CR2_EXTSEL; // 清除原有EXTSEL字段4位 ADC1-CR2 | (0b1011 ADC_CR2_EXTSEL_Pos); // EXTSEL 11 → TIM3_TRGO注意ADC_CR2_EXTEN_0是上升沿ADC_CR2_EXTEN_1是下降沿。实测中若用下降沿ADC可能只在第一次触发后响应后续脉冲失效——这是硬件设计限制不是bug。2.3 启动顺序决定成败先启DMA再启ADC最后启TIMER这是最容易翻车的时序坑。如果先启动TIMER3TRGO脉冲会立刻发出但此时ADC还没准备好接收DMA也没分配好地址结果就是前N个脉冲被丢弃缓存起始位置错乱。正确顺序必须是初始化DMA通道指定内存地址、传输长度、循环模式启动ADC仅使能不开始转换最后启动TIMER3CEN1// DMA2_Stream0 配置为循环模式ADC1使用此通道 RCC-AHB1ENR | RCC_AHB1ENR_DMA2EN; DMA2_Stream0-CR 0; // 先清零 DMA2_Stream0-PAR (uint32_t)ADC1-DR; // 外设地址ADC数据寄存器 DMA2_Stream0-M0AR (uint32_t)adc_buffer; // 内存地址你的uint16_t缓存数组 DMA2_Stream0-NDTR BUFFER_SIZE; // 传输数量如1024 DMA2_Stream0-CR | DMA_SxCR_MINC | // 内存地址自增 DMA_SxCR_PSIZE_0 | // 外设宽度16bit DMA_SxCR_MSIZE_0 | // 内存宽度16bit DMA_SxCR_DIR_0 | // 外设→内存 DMA_SxCR_CIRC | // 循环模式关键 DMA_SxCR_EN; // 使能DMA此时ADC未启不传数据 // ADC1使能不启动转换 ADC1-CR2 | ADC_CR2_ADON; delay_us(3); // 等待ADC稳压手册要求最小3us // 最后启动TIMER3 TIM3-CR1 | TIM_CR1_CEN;3. DMA缓冲区怎么管双缓冲模式下如何避免数据覆盖与指针错位单缓冲模式Circular在高速采集时有个致命问题当CPU正在读取缓冲区A的中间位置时DMA可能已把新数据写入A的开头造成读写冲突。双缓冲Double Buffer通过两个独立内存块自动切换机制解决此问题但HAL库的HAL_ADC_Start_DMA()默认不启用双缓冲必须手动配置DMA寄存器。3.1 双缓冲硬件原理M0AR/M1AR与CT位协同工作DMA2_Stream0支持双缓冲其核心是两个内存地址寄存器M0AR/M1AR和一个CT位Current Target。当CT0时DMA向M0AR写当缓冲区填满DMA自动切到M1AR并置CT1再次填满再切回M0AR并清CT0。CPU只需轮询CT位即可知道当前哪个缓冲区是“满的可读”哪个是“空的可写”。// 启用双缓冲需同时设置M0AR、M1AR并开启DBM位 uint16_t buffer_a[BUFFER_SIZE]; uint16_t buffer_b[BUFFER_SIZE]; DMA2_Stream0-M0AR (uint32_t)buffer_a; DMA2_Stream0-M1AR (uint32_t)buffer_b; DMA2_Stream0-CR | DMA_SxCR_DBM; // 双缓冲使能关键 // 注意NDTR仍为单缓冲长度双缓冲总长2*NDTR3.2 CPU读取策略用CT位判断当前有效缓冲区HAL库没有提供直接读CT位的API必须查寄存器。每次读取前检查DMA_SxCR_CT位0表示M0AR已满1表示M1AR已满// 在主循环中安全读取 if (DMA2_Stream0-CR DMA_SxCR_CT) { // M1AR已满 → 处理buffer_b process_samples(buffer_b, BUFFER_SIZE); } else { // M0AR已满 → 处理buffer_a process_samples(buffer_a, BUFFER_SIZE); } // 处理完后无需清CT位DMA硬件自动维护提示不要在中断里处理大量数据建议DMA传输完成中断TCIF只做标志置位主循环轮询处理。否则高采样率下中断频繁会挤占CPU。3.3 缓冲区大小怎么定避开DMA传输延迟导致的丢点理论采样率TIMER3更新频率但实际能稳定采集的最高频率受DMA建立时间限制。测试发现当BUFFER_SIZE 32时即使TIMER3设为100kHzADC也会因DMA来不及切换缓冲而丢点。经验值BUFFER_SIZE ≥ 64且必须是2的幂次便于地址对齐。例如TIMER3更新频率推荐BUFFER_SIZE实测最大稳定采样点数/秒10 kHz12810,000 × 128 1.28M/s50 kHz25650,000 × 256 12.8M/s100 kHz512100,000 × 512 51.2M/s注意51.2M/s已接近F407的AHB总线带宽极限168MHz/2≈84MB/s但需预留其他外设占用建议留30%余量。4. 常见问题排查5条真实踩坑记录与当场验证法这一章不讲“可能的原因”只列我亲手复现过的5个具体现象、根因定位方法和一行代码级解决方案。每条都能在1分钟内验证是否命中你的问题。4.1 现象缓存里全是0x0000用示波器测TRGO有方波ADC_DR读出来也是0原因ADC时钟未使能或分频比过大导致ADC内核根本没工作。验证读RCC-CR2的ADC123EN位bit23是否为1读RCC-CFGR的ADCPRE字段是否为00b2分频或01b4分频。F407最大ADC时钟为36MHz若APB284MHz则ADCPRE必须为01b84/421MHz。解决RCC-CR2 | RCC_CR2_ADC123EN; // 使能ADC123时钟 RCC-CFGR ~RCC_CFGR_ADCPRE; // 清除原分频 RCC-CFGR | RCC_CFGR_ADCPRE_1; // 设为4分频01b4.2 现象缓存前半段是有效数据后半段突然变成0xFFFF反复出现原因DMA传输数量NDTR设得比实际缓冲区小DMA写越界后覆盖了其他变量。验证在process_samples()开头加断点观察buffer_a[0]到buffer_a[BUFFER_SIZE-1]是否连续再看buffer_a[BUFFER_SIZE]是否被篡改。解决确认DMA2_Stream0-NDTR BUFFER_SIZE且BUFFER_SIZE定义为编译期常量#define BUFFER_SIZE 256而非变量。4.3 现象用HAL_ADC_GetValue()读ADC_DR得到非零值但DMA缓存仍是0原因ADC配置了扫描模式SCAN但未设置多通道或规则序列长度SQR1::L为0。验证读ADC1-SQR1的L字段bit23:20必须≥1若只采1个通道L0b00000通道错应为0b0000表示1个通道手册P312L0 → 1 conversion。解决ADC1-SQR1 0; // 清零SQR1 ADC1-SQR3 ADC_CHANNEL_0; // 仅采CH0 ADC1-SQR1 | (0b0000 ADC_SQR1_L_Pos); // L0 → 1 conversion4.4 现象TIMER3的TRGO测出来是10kHz方波但ADC采样率只有5kHz原因ADC配置了“连续转换模式”CONT1导致ADC在一次触发后自行连续转换不再等待下一个TRGO。验证读ADC1-CR2的CONT位bit1若为1则错误。外部触发时CONT必须为0。解决ADC1-CR2 ~ADC_CR2_CONT; // 关闭连续模式必须为04.5 现象双缓冲模式下CT位始终为0M1AR从未被写入原因DMA的双缓冲使能DBM位未设置或M1AR地址未初始化。验证调试器查看DMA2_Stream0-CR的DBM位bit16是否为1查看DMA2_Stream0-M1AR是否为非零有效地址。解决DMA2_Stream0-CR | DMA_SxCR_DBM; // 必须显式置位 DMA2_Stream0-M1AR (uint32_t)buffer_b; // 必须赋值不能留05. 进阶技巧用ADC注入通道同步采集参考电压实现温度漂移实时校准单纯用TIMER3 TRGO触发主规则通道只能解决“等间隔采样”问题但工业场景常需同时采集传感器信号和片内温度传感器TS且要求两者严格时间对齐——比如每10次主通道采样后第11次必须是TS值用于实时计算ADC偏移补偿。这时就得用注入通道Injected Channel配合相同的TRGO触发因为注入组也有自己的外部触发源JEXTSEL且可与规则组共享同一TRGO。5.1 注入通道触发配置复用同一TRGO但用不同EXTSEL编码ADC注入组的JEXTSEL字段与规则组EXTSEL独立但TIM3_TRGO在JEXTSEL表中编号为0111b即7不是规则组的11。这意味着你可以让规则通道CH0和注入通道CH16即TS在同一TRGO脉冲下先后启动// 规则组CH0EXTSEL11如前 ADC1-SQR3 ADC_CHANNEL_0; ADC1-SQR1 (0b0000 ADC_SQR1_L_Pos); // 注入组TSCH16JEXTSEL7且必须使能注入转换 ADC1-JSQR (ADC_CHANNEL_16 ADC_JSQR_JSQ1_Pos) | // 第1个注入通道 (0b0000 ADC_JSQR_JL_Pos) | // JL0 → 1注入通道 (0b0111 ADC_JSQR_JEXTSEL_Pos); // JEXTSEL7 → TIM3_TRGO ADC1-CR1 | ADC_CR1_JAW; // 使能注入通道 ADC1-CR2 | ADC_CR2_JEXTEN_0; // 注入上升沿触发5.2 时间对齐保障注入转换在规则转换结束后立即执行关键点在于ADC的转换时序当TRGO到来ADC先启动规则组转换CH0待其结束EOC自动紧接着启动注入组转换TS整个过程无CPU干预时间差仅由ADC时钟周期决定通常1μs。这样每10个TRGO脉冲后你就能在注入数据寄存器ADC1-JDR1里拿到一个精准对应的温度值。5.3 实时校准代码框架用滑动窗口动态更新零点偏移假设你已通过双缓冲获得buffer_a[]主通道和周期性获取的ts_value注入通道校准逻辑如下#define CALIB_WINDOW 64 static uint16_t ts_history[CALIB_WINDOW]; static uint8_t ts_idx 0; static int16_t zero_offset 0; void update_calibration(uint16_t ts_raw) { // TS原始值转摄氏度F407公式T(℃) (V25 - VSENSE)/Avg_Slope 25 float v_sense ((float)ts_raw * 3.3f) / 4095.0f; float temp_c (1.43f - v_sense) / 0.0043f 25.0f; // 滑动窗口均值滤波 ts_history[ts_idx] (uint16_t)temp_c; ts_idx (ts_idx 1) % CALIB_WINDOW; // 计算当前温度下的理论零点假设-40~125℃线性漂移 // 此处简化每℃漂移0.1LSB则offset (temp_c - 25) * 0.1 zero_offset (int16_t)((temp_c - 25.0f) * 0.1f); } // 在process_samples()中应用校准 void process_samples(uint16_t *buf, uint16_t len) { for (uint16_t i 0; i len; i) { int32_t calibrated (int32_t)buf[i] - zero_offset; if (calibrated 0) calibrated 0; if (calibrated 4095) calibrated 4095; buf[i] (uint16_t)calibrated; } }我一般会在项目启动时先采集1000个TS样本做初始校准之后每100ms更新一次zero_offset。这个技巧让某振动监测设备在-20℃到70℃环境下的ADC零点漂移从±12LSB压到±2LSB以内。希望帮到你。本文还有配套的精品资源点击获取