
1. 为什么需要定时器触发ADCDMA采集在嵌入式开发中数据采集是个高频需求。就拿我最近做的工业温控项目来说需要同时采集8个温度传感器的数据。如果直接用轮询方式读取ADCCPU啥事都别干了光忙着采集数据就够呛。这时候定时器触发ADC配合DMA搬运的方案就派上用场了。这种方案的精妙之处在于三点首先是定时器提供精准的采样节奏比如每1ms采集一次其次是ADC负责把模拟信号转为数字量最后DMA悄无声息地把数据搬到内存全程不需要CPU插手。实测下来CPU占用率能从90%降到不足5%效果立竿见影。这里有个实际案例某电机控制系统需要采集三相电流采样率要求10kHz。用传统方式会导致系统响应迟缓改用TIM1触发ADC1DMA的方案后不仅满足了实时性要求还留出了充足CPU资源做算法运算。2. 硬件架构深度解析2.1 STM32F407的ADC模块特点F407的ADC堪称性能怪兽最高支持2.4MSPS的采样率。它有3个独立ADC单元每个单元有16个规则通道。这里有个坑我踩过规则通道虽然多但数据寄存器(DR)只有一个这意味着多通道采集时如果不及时取走数据就会被新数据覆盖。ADC的扫描模式是解决这个问题的钥匙。开启扫描模式后ADC会自动按预定顺序转换多个通道。配合DMA的循环搬运模式就能实现采集-传输-存储的自动化流水线。记得我第一次调试时忘了开扫描模式结果永远只能读到最后一个通道的数据排查了半天才发现问题。2.2 DMA控制器的工作机制DMA2控制器有8个数据流每个流有8个通道。ADC1对应DMA2的Stream0/Channel0这个映射关系一定要记牢。配置时最容易出错的是数据对齐问题当ADC设为12位分辨率时DMA的数据宽度应该设成HalfWord(16位)否则会出现数据错位。有个实用技巧使用双缓冲机制。定义两个缓冲区DMA交替填充这样处理数据时永远有一个完整的数据块可用。代码示例#define BUF_SIZE 256 uint16_t adc_buf1[BUF_SIZE], adc_buf2[BUF_SIZE]; DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)adc_buf2, DMA_Memory_1);2.3 定时器的触发原理TIM2/TIM3/TIM4都可以作为ADC的触发源。关键是要配置TRGO信号通常选择更新事件作为触发源。这里有个细节定时器的时钟源要算准。比如APB1时钟是42MHz经过预分频后定时器的计数频率为TIM_Prescaler 42000-1; // 分频后1MHz TIM_Period 1000-1; // 1kHz触发频率3. 库函数配置全流程3.1 DMA配置实战先上完整代码再拆解关键点void DMA_Config(void) { DMA_InitTypeDef DMA_InitStruct; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_InitStruct.DMA_Channel DMA_Channel_0; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)adc_buffer; DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStruct.DMA_BufferSize CHANNEL_NUM * SAMPLE_COUNT; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStruct.DMA_Mode DMA_Mode_Circular; DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_InitStruct.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, DMA_InitStruct); DMA_Cmd(DMA2_Stream0, ENABLE); }重点参数解析DMA_Mode_Circular开启循环模式缓冲区填满后自动从头开始BufferSize总传输量通道数×每个通道采样点数MemoryInc必须使能否则所有数据都会堆在同一个地址3.2 ADC配置技巧ADC的配置要特别注意触发源选择ADC_InitStruct.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_TRGO; ADC_InitStruct.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_Rising;扫描模式和多通道配置ADC_InitStruct.ADC_ScanConvMode ENABLE; ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_56Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_56Cycles);采样时间计算公式采样时间 (采样周期 12.5) / ADC时钟频率比如ADC时钟为21MHz采样周期设为56则实际采样时间(5612.5)/21≈3.26us3.3 定时器精准触发定时器配置的关键是匹配采样率需求void TIM_Config(uint32_t freq) { TIM_TimeBaseInitTypeDef TIM_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); uint32_t clock SystemCoreClock / 2; // APB1时钟 uint32_t prescaler clock / 1000000 - 1; // 分频到1MHz TIM_InitStruct.TIM_Prescaler prescaler; TIM_InitStruct.TIM_Period (1000000 / freq) - 1; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStruct); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }4. 调试经验与性能优化4.1 常见问题排查指南数据错乱按这个顺序检查确认DMA缓冲区足够大没有溢出检查ADC和DMA的数据宽度是否一致验证定时器触发频率是否超出ADC采样能力有个隐蔽的坑ADC的EOC(转换结束)标志在DMA模式下可能不会自动清除需要手动读取DR寄存器来清除。我曾在这个问题上卡了两天后来发现加上这行代码就好了volatile uint16_t dummy ADC1-DR; // 清除EOC标志4.2 性能优化实战提升采样率的三个诀窍降低ADC采样周期(但不能小于最短要求)使用ADC过采样硬件功能开启DMA突发传输模式内存优化技巧__attribute__((section(.ccmram))) uint16_t adc_buffer[SIZE];把缓冲区放到CCM RAM可以避免总线冲突实测能提升10%的传输速度。4.3 多通道数据分离技巧假设采集了4个通道数据在缓冲区中的排列是CH0,CH1,CH2,CH3,CH0,CH1...。提取特定通道数据的函数void ExtractChannel(uint16_t *dest, uint16_t *src, uint8_t ch, uint8_t total_ch) { for(int i0; iBUF_SIZE/total_ch; i) { dest[i] src[i*total_ch ch]; } }对于需要实时处理的应用建议使用RTOS的消息队列来传递数据块。FreeRTOS的实现示例xQueueSend(adcQueue, dataBlock, portMAX_DELAY);