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。

- Prescaler:
(SystemCoreClock / 1000000) - 1(设成1μs节拍) - ARR(Period) : 124(每125μs触发一次)
- Update Event中断 Enable
具体配置如下图

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,如下图


关键代码
关键在两个回调函数,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()等增加延时则得不偿失。经古希腊掌管嵌入式の神季总点拨,将拉高操作放在接收回调中,以工代赈,两难自解。

浙公网安备 33010602011771号