You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

STM32F4x双ADC通道不同采样率配置:仅ECK通道启用DMA降CPU负载

STM32F4x ADC双通道差异化配置方案(DMA+非DMA)

嘿,刚好做过类似的STM32F4 ADC多通道差异化配置需求,给你详细说说怎么实现——既要让ECK通道用DMA减轻CPU负载,又让Vbat通道保持低采样率且不占用DMA资源,其实核心就是把两个通道的转换逻辑分开处理,利用STM32 ADC的灵活配置特性来实现。

整体思路

  1. ECK通道(1kHz采样):用定时器触发+DMA循环传输,让ADC自动完成采样并把数据传到内存,全程无需CPU干预,只在DMA缓存满时(或半满时)通过中断处理数据,彻底降低CPU负载。
  2. Vbat通道(低采样率):用单次转换+软件/低频率定时器触发,因为采样频率极低(比如1Hz),哪怕用轮询或中断读取转换结果,对CPU的开销也可以忽略不计,完全不需要DMA。
  3. 两个通道共用同一个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采样值
  }
}

关键注意事项

  1. ADC时钟限制:STM32F4的ADC最大时钟为36MHz,所以配置ClockPrescaler时要保证ADC时钟不超过这个值(比如系统时钟84MHz时,分频4得到21MHz,符合要求)。
  2. 通道切换开销:因为Vbat采样频率极低,每次切换ADC通道的开销对CPU几乎没有影响,如果觉得动态切换麻烦,也可以用两个独立ADC(如果硬件允许),但F4大部分型号有3个ADC,不过单ADC切换足够满足需求。
  3. DMA缓存大小:根据你的数据处理需求调整缓存大小,循环模式下会自动覆盖旧数据,CPU可以在空闲时或中断里处理缓存数据。
  4. 采样时间调整:ECK通道的采样时间要根据信号带宽调整,保证采样精度;Vbat通道是直流信号,可以用较长的采样时间提高精度。

内容的提问来源于stack exchange,提问作者user9512877

火山引擎 最新活动