STM32定时器硬件触发ADC实现毫秒级稳定采样
简介本资源是一份基于STM32F103系列MCU的定时器触发ADC采集完整工程实现面向嵌入式初学者与进阶开发者解决模拟信号高精度、周期性采样的核心需求适用于传感器数据采集、实时监控及控制系统等典型场景。压缩包含1026个文件主体为561个C源码与245个头文件h辅以汇编启动文件s、链接脚本sct、IAR工程配置icf、编译中间产物o/d/crf及可执行镜像axf/hex总大小15.29MB结构完整覆盖LL底层库全链路配置。已有6546人学习下载体现其在实战教学与项目移植中的广泛认可。读者可直接获取TIM2/TIM3触发ADC的初始化代码、中断服务例程、LL库关键调用如LL_ADC_REG_SetTriggerSource、LL_TIM_EnableIT_UPDATE、数学库依赖arm_cortexM3l_math.a等及多通道采样时序控制逻辑代码注释详尽便于理解定时器-ADC硬件联动机制并快速复用于自定义项目。1. 定时器ADC联动不是“配个中断就完事”一个毫秒级稳定采样系统的真实落地场景某高校嵌入式课程设计里学生用STM32F103做振动信号采集最初直接调HAL_ADC_Start()HAL_ADC_PollForConversion()结果采样间隔抖动超±8msFFT频谱全糊换成HAL_ADC_Start_IT()后中断响应受其他外设抢占影响仍无法满足1kHz等间隔采样要求。直到把TIM2配置为触发源、ADC1设为硬件触发模式才真正跑出标准偏差12μs的稳定序列——这项目就是干这个的用定时器TRGO信号硬连线触发ADC规则通道转换绕过CPU干预实现确定性、低抖动、可复现的周期性模拟量采集。它不依赖HAL_Delay或SysTick软延时不靠中断服务函数调度而是把时序控制权交给APB1总线上的硬件协同逻辑。适合做电机电流环采样、音频前端预处理、传感器同步读取等对时序敏感的工业级应用。如果你正被“采样不准”“FFT变形”“波形跳变”困扰又卡在“查了手册还是连不上触发线”这份资源就是为你拆解从寄存器映射到实测波形的完整链路。2. 硬件触发链路拆解TIM2-TRGO如何精准“敲响”ADC1的转换门2.1 触发路径物理连接从定时器输出引脚到ADC内部触发线STM32F103的定时器触发ADC并非GPIO引脚直连而是芯片内部总线级硬连线。关键路径是TIM2_CR1寄存器的CEN位启动计数 → 计数器溢出/更新事件 → 触发TRGO信号默认为Update Event→ 该信号经AHB总线路由至ADC1的EXTSEL[2:0]位 → ADC1检测到上升沿即启动一次规则通道转换。这个过程完全在芯片内部完成无需PC6/TIM2_CH1等GPIO参与那些是对外输出PWM用的。你看到的“PA0接ADC1_IN0”只是模拟输入通道和触发信号走的是两条独立通路。很多初学者误以为要“把TIM2_CH1接到ADC1的某个引脚”这是典型误解——F103没有这种外部触发引脚所有触发都是寄存器配置的内部信号路由。提示F103系列中只有TIM1/TIM2/TIM3/TIM4支持作为ADC触发源且每个定时器对应固定ADC如TIM2→ADC1TIM3→ADC2不能随意交叉。手册RM0008第11.3.3节明确列出触发映射表务必核对。2.2 核心寄存器配置四步法避开HAL库封装直击硬件本质我们不用HAL库的HAL_TIM_Base_Start()和HAL_ADC_Start()组合而是手动配置关键寄存器确保每一步都可控// 步骤1配置TIM2为向上计数模式自动重装载值7199 → 72MHz/(71991)10kHz触发频率 RCC-APB1ENR | RCC_APB1ENR_TIM2EN; // 使能TIM2时钟 TIM2-PSC 7199; // 预分频器72MHz / (71991) 10kHz TIM2-ARR 99; // 自动重装载值10kHz / (991) 100Hz采样率可调 TIM2-CR1 TIM_CR1_CEN; // 启动计数器 // 步骤2配置ADC1时钟与通道注意ADC时钟必须≤14MHz RCC-APB2ENR | RCC_APB2ENR_ADC1EN; // 使能ADC1时钟 RCC-CFGR ~RCC_CFGR_ADCPRE; // 清除ADC预分频位 RCC-CFGR | RCC_CFGR_ADCPRE_1; // APB272MHz → ADCCLK72MHz/612MHz满足≤14MHz // 步骤3设置ADC1规则序列单通道PA0 ADC1-SQR3 0; // 清空SQR3 ADC1-SQR3 0; // 选择通道0PA0作为第一个也是唯一转换通道 ADC1-SMPR2 0x00000007; // 通道0采样时间239.5周期最大值适配高阻传感器 // 步骤4启用硬件触发并选择TIM2_TRGO ADC1-CR2 ~ADC_CR2_EXTSEL; // 清除原有触发源选择 ADC1-CR2 | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0; // 0b11 → TIM2 TRGO ADC1-CR2 | ADC_CR2_EXTTRIG; // 使能外部触发 ADC1-CR2 | ADC_CR2_ADON; // 开启ADC注意必须在触发配置后开启这段代码的关键在于执行顺序不可颠倒必须先配好TIM2再设ADC时钟分频最后才开ADC并启用触发。如果先开ADC再配触发源ADC会进入“等待触发”状态但永远等不到信号ADC1-SR的EOC位永远不会置位。2.3 触发时序验证用示波器抓取TRGO与EOC信号确认硬件握手光看寄存器不保险必须用示波器验证真实时序。将TIM2的TRGO信号引出到GPIO需配置TIM2_CH3为OC模式输出TRGO// 在TIM2初始化后追加将TRGO映射到PB1TIM2_CH3 RCC-APB2ENR | RCC_APB2ENR_IOPBEN; GPIOB-CRH ~GPIO_CRH_CNF1; // PB1复用推挽 GPIOB-CRH | GPIO_CRH_MODE1_1; // 输出模式50MHz TIM2-CCMR2 | TIM_CCMR2_CC3S_0; // CH3作为输出比较 TIM2-CCER | TIM_CCER_CC3E; // 使能CH3输出 TIM2-BDTR | TIM_BDTR_MOE; // 主输出使能否则OC无效然后用示波器同时接PB1TRGO和PA0ADC输入或ADC_DR寄存器读取点需软件打点你会看到TRGO脉冲宽度 ARR1个定时器时钟周期本例中为100μs宽方波每个TRGO上升沿后ADC_DR寄存器在固定延迟约3.5μs后更新新值此为ADC采样转换固有延迟两次DR更新间隔严格等于TRGO周期10ms100Hz无任何抖动这个实测波形才是触发成功的铁证。我曾帮A同学调试时发现他TRGO波形正常但ADC无响应最后查出是RCC-CFGR中ADC预分频配置错误导致ADC时钟超频锁死——没有示波器验证这种问题会卡死三天。3. 软件数据流设计DMA搬运双缓冲规避采样中断丢失3.1 为什么不用ADC中断——中断响应不确定性是硬伤假设你设1kHz采样率TRGO周期1ms每次转换完成后进ADC_IRQHandler。但F103中断响应时间受以下因素影响当前执行指令是否为多周期指令如LDMIA是否有更高优先级中断正在执行如USB中断NVIC抢占优先级设置是否合理实测表明在开启FreeRTOS且运行多个任务时ADC中断延迟波动可达20~80μs导致采样点实际时间戳失真。而DMA搬运是总线级操作只要配置正确数据搬移完全不经过CPU时序由硬件仲裁器保证。3.2 DMA双缓冲配置无缝衔接采样与处理避免数据覆盖本项目采用DMA循环模式双缓冲让硬件自动在两个内存区之间切换// 定义双缓冲区各1000个uint16_t对应1s数据 __attribute__((aligned(4))) uint16_t adc_buffer_a[1000]; __attribute__((aligned(4))) uint16_t adc_buffer_b[1000]; // 配置DMA1 Channel1ADC1专用通道 RCC-AHBENR | RCC_AHBENR_DMA1EN; DMA1_Channel1-CPAR (uint32_t)ADC1-DR; // 外设地址 DMA1_Channel1-CMAR (uint32_t)adc_buffer_a; // 内存地址初始指向buffer_a DMA1_Channel1-CNDTR 1000; // 传输数量 DMA1_Channel1-CCR DMA_CCR_EN | DMA_CCR_DIR | DMA_CCR_CIRC | DMA_CCR_MINC | // 循环模式内存增量 DMA_CCR_TCIE | DMA_CCR_HTIE; // 全传输完成半传输中断使能 // 启动DMA此时ADC已使能触发 DMA1_Channel1-CCR | DMA_CCR_EN;关键点在于DMA_CCR_HTIE当DMA搬完前500个数据buffer_a的前半段时触发HTIF标志此时可在中断中开始处理前500点当搬完全部1000点TCIF时DMA自动将CMAR切回buffer_a起始地址并开始向buffer_b写入下一组数据。这样处理线程永远有500点“安全缓冲区”绝不会因处理慢而丢数据。3.3 双缓冲中断服务函数标记状态而非搬运数据volatile uint8_t current_buffer 0; // 0buffer_a, 1buffer_b volatile uint8_t buffer_ready 0; // 0未就绪, 1半满, 2全满 void DMA1_Channel1_IRQHandler(void) { if (DMA1-ISR DMA_ISR_HTIF1) { // 半传输完成 DMA1-IFCR DMA_IFCR_CHTIF1; // 清中断标志 buffer_ready 1; current_buffer 0; // 此时buffer_a前500点就绪 } if (DMA1-ISR DMA_ISR_TCIF1) { // 全传输完成 DMA1-IFCR DMA_IFCR_CTCIF1; buffer_ready 2; current_buffer 1; // 切换到buffer_b全满 } } // 主循环中处理非阻塞 if (buffer_ready 1) { process_samples(adc_buffer_a, 500); // 处理前半段 buffer_ready 0; } else if (buffer_ready 2) { process_samples(adc_buffer_b, 1000); // 处理整段buffer_b buffer_ready 0; }这里刻意避免在中断里做FFT等耗时操作——中断只做状态标记计算全放在主循环。这是嵌入式开发血泪经验中断函数必须短小精悍否则会拖垮整个实时性。4. 常见问题排查五条踩坑记录每条都来自真实翻车现场4.1 现象ADC_DR寄存器始终读出0x0000示波器测TRGO正常原因ADC时钟配置错误。F103的ADCCLK必须≤14MHz但很多开发者直接用APB272MHz作为ADC时钟导致ADC模块锁死。解决检查RCC-CFGR的ADCPR位bit14-15必须设为0b10分频6或0b11分频8确保ADCCLK72MHz/612MHz或72MHz/89MHz。4.2 现象第一次TRGO后ADC有数据后续TRGO无响应原因ADC1-CR2的ADON位在触发配置前就被置位。ADC开启后若未配置触发源会进入“等待触发”状态并拒绝后续配置。解决严格按顺序执行——先配TIM2再配ADC时钟/通道/触发源最后ADC1-CR2 | ADC_CR2_ADON。可用while(!(ADC1-SR ADC_SR_ADON))轮询确认ADC真正就绪。4.3 现象DMA传输数量不对buffer_a只收到前100个数据就停止原因DMA1_Channel1-CNDTR寄存器在DMA使能后不可写。若先写CNDTR1000再置CCR.EN1但中途被中断打断CNDTR可能被意外改写。解决在置位CCR.EN前用__disable_irq()关全局中断配置完立即__enable_irq()。或者更稳妥配置CCR时先清EN位配完所有参数再置EN。4.4 现象双缓冲切换混乱有时buffer_a和buffer_b数据混在一起原因未启用DMA循环模式CCR.CIRC0。非循环模式下DMA传完1000次后自动停机CMAR不再自动重载下次触发时数据会覆盖原buffer起始位置。解决确认DMA1_Channel1-CCR | DMA_CCR_CIRC且CNDTR值必须与buffer大小严格一致1000不能写成999。4.5 现象采样值随电源电压漂移同一信号读数每天差20LSB原因未启用ADC内部参考电压校准。F103的VREFINT出厂校准值存在批次差异需在初始化时执行一次校准。解决在ADC1-CR2 | ADC_CR2_ADON后插入校准流程ADC1-CR2 | ADC_CR2_CAL; // 开始校准 while(ADC1-CR2 ADC_CR2_CAL); // 等待校准结束约6个ADC时钟周期校准后ADC精度可从±10LSB提升至±2LSB25°C时。5. 二次开发实战从100Hz到10kHz采样率的三步改造与FFT验证5.1 采样率动态调节修改ARR/PSC实现10Hz~10kHz无级变速原始代码中TIM2-PSC7199分频7200、TIM2-ARR99重载100固定为100Hz。要升到10kHz只需两处改动// 方案1保持PSC719910kHz基准ARR0 → 10kHz最小ARR0对应1个计数周期 TIM2-ARR 0; // 注意ARR0时更新事件在计数器从0→0时触发即每个时钟周期一次 // 方案2PSC0不分频ARR7199 → 72MHz/(71991)10kHz更易理解 TIM2-PSC 0; TIM2-ARR 7199; // 方案3动态计算推荐封装为函数 void set_adc_sampling_rate(uint32_t target_hz) { uint32_t timer_clk 72000000; // TIM2挂载在APB172MHz uint32_t period timer_clk / target_hz; TIM2-PSC 0; // 先设PSC0简化计算 TIM2-ARR period - 1; // ARR period - 1 TIM2-EGR TIM_EGR_UG; // 重新初始化计数器 }调用set_adc_sampling_rate(10000)即可切到10kHz。注意ARR最小值为0最大值为0xFFFF65535因此理论可调范围为1.1Hz ~ 72MHz但受限于ADC转换时间F103单次转换需12.5个ADC时钟周期实际最高采样率12MHz/12.5≈960kHz需超频ADC不推荐。工程中10kHz是安全上限。5.2 实时FFT验证用ARM CMSIS-DSP库跑通1024点FFT采样率提上去后必须验证数据有效性。我们用CMSIS-DSP的arm_cfft_f32()做实时频谱分析#include arm_math.h #define FFT_SIZE 1024 float32_t fft_input[FFT_SIZE]; // 存放ADC转换后的浮点数据 float32_t fft_output[FFT_SIZE*2]; // CMSIS要求复数格式实部虚部交替 arm_cfft_instance_f32 fft_inst; // 初始化FFT仅需一次 arm_cfft_init_f32(fft_inst, FFT_SIZE); // 在主循环中当buffer_b满1000点时取前1024点做FFT for(int i0; iFFT_SIZE; i) { fft_input[i] (float32_t)adc_buffer_b[i] * 3.3f / 4095.0f; // 转电压值 } arm_cfft_f32(fft_inst, fft_input, 0, 1); // 0正向变换1不缩放 arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE); // 计算幅值谱 // fft_output[0..1023]即为各频点幅值索引i对应频率 i * 10kHz / 1024 ≈ i*9.77Hz实测在10kHz采样率下STM32F103C8T6主频72MHz完成一次1024点FFT耗时约8.2ms完全满足实时性102.4ms采集1024点留足94ms处理余量。5.3 工业级加固添加过压保护与温度补偿的代码模板真实工业场景中传感器可能遭遇浪涌或温漂。我们在ADC读取后插入简单保护#define ADC_VREF 3.3f #define ADC_RES 4095.0f #define MAX_VOLTAGE 3.6f // 传感器最大输出电压 #define MIN_VOLTAGE 0.0f uint16_t raw_value *(uint16_t*)0x20000000; // 从DMA buffer读取 float voltage (raw_value * ADC_VREF) / ADC_RES; // 过压保护超过3.6V则标记异常并钳位 if (voltage MAX_VOLTAGE) { voltage MAX_VOLTAGE; error_flag | OVER_VOLTAGE; } // 温度补偿假设使用NTC热敏电阻查表法 extern const float ntc_table[100]; // 预存100点温度-阻值表 int temp_index (int)(voltage * 30.0f); // 简化映射实际需查表插值 if (temp_index 0 temp_index 100) { voltage * (1.0f 0.00385f * (ntc_table[temp_index] - 25.0f)); // 线性补偿系数 }这段代码虽简但体现了工业代码思维先保安全过压钳位再求准确温度补偿。很多开源项目只做校准不做保护一上电就烧ADC——这是新手和老手的本质区别。从那以后我每次做ADC项目都会在main()开头强制插入ADC1-CR2 | ADC_CR2_CAL和while(ADC1-SR ADC_SR_ADON)轮询哪怕文档说“可选”。因为校准失败的ADC就像没校准的示波器探头你永远不知道读出来的是真相还是幻觉。希望帮到你。本文还有配套的精品资源点击获取