STM32F4x双ADC通道不同采样率配置:仅ECK通道启用DMA降CPU负载
STM32F4x ADC双通道差异化配置方案(DMA+非DMA)
嘿,刚好做过类似的STM32F4 ADC多通道差异化配置需求,给你详细说说怎么实现——既要让ECK通道用DMA减轻CPU负载,又让Vbat通道保持低采样率且不占用DMA资源,其实核心就是把两个通道的转换逻辑分开处理,利用STM32 ADC的灵活配置特性来实现。
整体思路
- ECK通道(1kHz采样):用定时器触发+DMA循环传输,让ADC自动完成采样并把数据传到内存,全程无需CPU干预,只在DMA缓存满时(或半满时)通过中断处理数据,彻底降低CPU负载。
- Vbat通道(低采样率):用单次转换+软件/低频率定时器触发,因为采样频率极低(比如1Hz),哪怕用轮询或中断读取转换结果,对CPU的开销也可以忽略不计,完全不需要DMA。
- 两个通道共用同一个ADC,通过动态切换ADC的规则序列来实现不同通道的转换,因为Vbat采样频率低,切换的开销几乎可以忽略。
具体代码实现(基于HAL库)
1. 基础硬件初始化(GPIO、ADC、DMA、时钟)
首先开启相关外设时钟,并配置GPIO为模拟输入:
#include "stm32f4xx_hal.h" ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; TIM_HandleTypeDef htim3; // 用于触发ECK通道1kHz采样 // GPIO初始化:假设Vbat接PA0(ADC通道0),ECK接PA1(ADC通道1) void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // 配置PA0、PA1为模拟输入 GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; GPIO_InitStruct.Mode = GPIO_MODE_ANALOG; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); }
2. DMA配置(仅针对ECK通道)
配置DMA为外设到内存的循环传输,对应ADC1的DMA通道(STM32F4中ADC1对应DMA2 Stream0 Channel0):
void MX_DMA_Init(void) { __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; 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; // ADC输出12位,用半字存储 hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式,持续采样覆盖旧数据 hdma_adc1.Init.Priority = DMA_PRIORITY_MEDIUM; hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; HAL_DMA_Init(&hdma_adc1); // 将DMA与ADC关联 __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 开启DMA中断(可选,用于缓存满时处理数据) HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); }
3. ADC配置(支持双通道切换)
配置ADC为外部触发模式(定时器触发),开启DMA连续请求:
void MX_ADC1_Init(void) { ADC_ChannelConfTypeDef sConfig = {0}; __HAL_RCC_ADC1_CLK_ENABLE(); hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 保证ADC时钟≤36MHz(F4最大ADC时钟) hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = DISABLE; // 关闭扫描模式,单通道转换 hadc1.Init.ContinuousConvMode = DISABLE; // 由定时器触发单次转换,配合DMA循环实现连续采样 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING; // 上升沿触发 hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO; // 用定时器3的TRGO信号触发 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; // 开启DMA连续请求 hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV; HAL_ADC_Init(&hadc1); // 先默认配置ECK通道(ADC通道1) sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES; // 根据信号特性调整采样时间 sConfig.Offset = 0; HAL_ADC_ConfigChannel(&hadc1, &sConfig); }
4. 定时器配置(实现ECK通道1kHz采样触发)
配置定时器3生成1kHz的触发信号,触发ADC采样:
void MX_TIM3_Init(void) { TIM_MasterConfigTypeDef sMasterConfig = {0}; __HAL_RCC_TIM3_CLK_ENABLE(); // 假设系统时钟为84MHz,计算得到1kHz触发频率 htim3.Instance = TIM3; htim3.Init.Prescaler = 83; // 84MHz / (83+1) = 1MHz htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 999; // 1MHz / (999+1) = 1kHz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_Base_Init(&htim3); // 配置定时器更新事件为TRGO触发信号 sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig); }
5. 启动ECK的DMA采样与Vbat的低频率采样
uint16_t eck_adc_buffer[100]; // 缓存100个ECK采样值,循环覆盖 int main(void) { HAL_Init(); SystemClock_Config(); // 自行实现系统时钟配置(比如84MHz) MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); MX_TIM3_Init(); // 启动ECK通道的DMA采样 HAL_TIM_Base_Start(&htim3); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)eck_adc_buffer, 100); while (1) { // 读取Vbat值(比如每1秒读取一次) uint16_t vbat_val = read_vbat(); // 处理Vbat数据:滤波、转换为实际电压、存储等 HAL_Delay(1000); } } // 读取Vbat的函数:动态切换ADC通道为Vbat(通道0) uint16_t read_vbat(void) { ADC_ChannelConfTypeDef sConfig = {0}; // 重新配置ADC规则序列为Vbat通道 sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = ADC_REGULAR_RANK_1; sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES; // 直流采样用较长时间提高精度 sConfig.Offset = 0; HAL_ADC_ConfigChannel(&hadc1, &sConfig); // 启动单次转换并读取结果 HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); return HAL_ADC_GetValue(&hadc1); } // DMA传输完成中断回调(可选) void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc->Instance == ADC1) { // 处理满缓存的100个ECK采样值,比如滤波、分析等 // 注意:中断函数要尽量简短,避免阻塞 } } // DMA半传输完成中断回调(可选) void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc->Instance == ADC1) { // 处理前50个ECK采样值 } }
关键注意事项
- ADC时钟限制:STM32F4的ADC最大时钟为36MHz,所以配置
ClockPrescaler时要保证ADC时钟不超过这个值(比如系统时钟84MHz时,分频4得到21MHz,符合要求)。 - 通道切换开销:因为Vbat采样频率极低,每次切换ADC通道的开销对CPU几乎没有影响,如果觉得动态切换麻烦,也可以用两个独立ADC(如果硬件允许),但F4大部分型号有3个ADC,不过单ADC切换足够满足需求。
- DMA缓存大小:根据你的数据处理需求调整缓存大小,循环模式下会自动覆盖旧数据,CPU可以在空闲时或中断里处理缓存数据。
- 采样时间调整:ECK通道的采样时间要根据信号带宽调整,保证采样精度;Vbat通道是直流信号,可以用较长的采样时间提高精度。
内容的提问来源于stack exchange,提问作者user9512877




