2025-07-17 DMA读取ADC

需求

当前呼吸机采用外挂AD7689用于比例阀AD反馈、氧电池、压力监测,需要每个通道1ms更新一次。AD7689共8个通道,因而整体采样率需8KHz。传统实现HAL_SPI_TransmitReceive​为阻塞式,8KHz采样率下对MCU占用较高,考虑DMA方式。

实现方法

STM32F429 外挂 AD7689 ADC 芯片,使用 DMA 方式采集 ADC 数据,这里的 DMA 并不是直接对 AD7689 的模拟输入进行DMA采集,而是用于 SPI 通信中搬运 AD7689 转换结果

  • STM32 作为 SPI 主机
  • AD7689 作为 SPI 从机
  • 每次转换通过 SPI 收发16位数据,获取ADC结果
  • STM32 使用 DMA 自动收发 SPI 数据,减轻 CPU 负担

DMA 在这个系统中是如何使用的?

步骤 功能 说明
1 STM32 启动 CNV 控制 GPIO 产生上升沿,触发 AD7689 转换
2 启动 SPI DMA 传输 使用HAL_SPI_TransmitReceive_DMA()​向 AD7689 发送配置字并接收数据
3 DMA 完成后中断回调处理 HAL_SPI_TxRxCpltCallback()​中保存 ADC 数据,并准备下一通道
4 重复上述操作,轮询通道 每125μs采集一个通道,8个通道合计1ms,满足每通道1kHz采样率

DMA用法说明

STM32 HAL中,可以使用以下API实现:

HAL_SPI_TransmitReceive_DMA(&hspi1, tx_buf, rx_buf, 2); // 每次2字节

其中:

  • tx_buf​:存储配置字(控制 AD7689 通道、工作模式)
  • rx_buf​:接收AD7689输出的采样值

DMA传输完成后,回调函数 HAL_SPI_TxRxCpltCallback​ 会被调用。


工作流程图(简略)

         STM32(主机)                    AD7689(从机)
        ----------------                -----------------
   定时器中断/软件触发
           ↓
   CNV 拉低      
           ↓
   SPI_DMA发送配置字 + 接收         <=>   SPI回应采样数据
           ↓
   DMA中断完成 => 存储数据 => CNV拉高
           ↓
   准备下一个通道配置(轮询)

✅ DMA方式采集AD7689的优势

方式 优点
SPI + DMA 低CPU占用,传输快,易于扩展
轮询方式 简单,但CPU频繁处理中断开销大

❗ 注意事项

  • DMA一次传输16位(2字节):AD7689输出的是16位数据
  • 转换前必须设置CNV脉冲,CNV时序必须满足 datasheet
  • 每次转换都需发送 配置字,选择通道
  • AD7689不是自动轮询通道的,需由主控 MCU 切换配置字

实现细节

定时器TIM7

定时器需要满足8KHz,选择TIM6或TIM7. 这两个定时器对应APB1,时钟频率为84MHz。

image

  • Prescaler: (SystemCoreClock / 1000000) - 1​(设成1μs节拍)
  • ARR(Period) : 124(每125μs触发一次)
  • Update Event中断 Enable

具体配置如下图

image

NVIC Settings中使能TIM7 global interrupt

注意,生成代码中需要手动添加HAL_TIM_Base_Start_IT(&htim7);

完整初始化代码如下

static void MX_TIM7_Init(void)
{

  /* USER CODE BEGIN TIM7_Init 0 */

  /* USER CODE END TIM7_Init 0 */

  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM7_Init 1 */

  /* USER CODE END TIM7_Init 1 */
  htim7.Instance = TIM7;
  htim7.Init.Prescaler = 84-1;
  htim7.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim7.Init.Period = 125-1;
  htim7.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  if (HAL_TIM_Base_Init(&htim7) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim7, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM7_Init 2 */
  HAL_TIM_Base_Start_IT(&htim7);
  /* USER CODE END TIM7_Init 2 */

}

SPI1

SPI参数配置与普通模式一致,只需额外使能SPI中断与DMA,如下图

image

image

关键代码

关键在两个回调函数,HAL_TIM_PeriodElapsedCallback​定时器回调,每125us触发一次,在该回调中通过HAL_SPI_TransmitReceive_DMA​启动DMA传输;DMA传输完成后,触发HAL_SPI_TxRxCpltCallback​回调,在此处处理接收到的数据。

HAL_TIM_PeriodElapsedCallback

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM6) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */
  if (htim->Instance == TIM7) {
    ad7689_read_raw_dma();
  }
  /* USER CODE END Callback 1 */
}
uint8_t g_ad_channel_index = 0;
uint8_t g_tx_buf[2] = {0};
uint8_t g_rx_buf[8][2] = {0};
int ad7689_read_raw_dma()
{
    HAL_GPIO_WritePin(ADC_SPI_CS_GPIO_Port, ADC_SPI_CS_Pin, GPIO_PIN_RESET);
    uint16_t reg = (1 << 13) | (7 << 10) | (g_ad_channel_index << 7) | (0 << 6) | (1 << 3) | (0 << 1) | (0 << 0);
    reg <<= 2;
    g_tx_buf[0] = (uint8_t)(reg >> 8);
    g_tx_buf[1] = (uint8_t)(reg & 0xFF);
    
    int ret = HAL_SPI_TransmitReceive_DMA(&hspi1, g_tx_buf, g_rx_buf[g_ad_channel_index], 2);
    if (ret != 0)
    {
        ++g_ad_read_err_count[g_ad_channel_index];
    }
    return ret;
}

HAL_SPI_TxRxCpltCallback

void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI1)
    {
        uint16_t result = (g_rx_buf[g_ad_channel_index][0] << 8) | g_rx_buf[g_ad_channel_index][1];

        int value_index = (g_ad_channel_index + E_AD_AD_CHNNEL_NUM_MAX - 2) % E_AD_AD_CHNNEL_NUM_MAX;

        g_ad_raw_ad_value[value_index][g_ad_value_index[value_index]] = result;

        ++g_ad_value_index[value_index];
        if (g_ad_value_index[value_index] >= AD_RAW_DATA_BUFFER_COUNT)
        {
            g_ad_value_index[value_index] = 0;
        }

        ++g_ad_channel_index;
        if (g_ad_channel_index >= E_AD_AD_CHNNEL_NUM_MAX)
        {
            g_ad_channel_index = 0;
        }
        HAL_GPIO_WritePin(ADC_SPI_CS_GPIO_Port, ADC_SPI_CS_Pin, GPIO_PIN_SET);
    }
}

重点

ad7689原先实现如下

    HAL_GPIO_WritePin(ADC_SPI_CS_GPIO_Port, ADC_SPI_CS_Pin, GPIO_PIN_RESET);
    ret = HAL_SPI_TransmitReceive(&hspi1, txbuf, rxbuf, sizeof(txbuf), 20);
    HAL_GPIO_WritePin(ADC_SPI_CS_GPIO_Port, ADC_SPI_CS_Pin, GPIO_PIN_SET);

在该实现中,HAL_SPI_TransmitReceive​为阻塞接口,本身有耗时。而HAL_SPI_TransmitReceive_DMA​为非阻塞接口,AD7689 CNV引脚拉低与拉高之间需要10ns延时,若直接使用__NOP()等增加延时则得不偿失。经古希腊掌管嵌入式の神季总点拨,将拉高操作放在接收回调中,以工代赈,两难自解​。

posted @ 2025-07-17 20:31  thatdor  阅读(66)  评论(0)    收藏  举报