MCU芯片AS32X601如何使用 DMA 单次模式搬运 ADC 多通道采样数据?

📅 2026/7/24 22:56:26 👁️ 阅读次数 📝 编程学习
MCU芯片AS32X601如何使用 DMA 单次模式搬运 ADC 多通道采样数据?

1. 背景

在数据采集项目中,经常需要 ADC 按固定周期采集多个模拟通道,并通过 DMA 自动把转换结果搬运到内存,以减少 CPU 占用。

AS32X601 当前 DMA 驱动没有提供可直接使用的循环模式,因此 DMA 完成一次传输后,需要由软件重新装载 DMA 参数。

对于 ADC 多通道扫描,可以采用下面的方案:

  1. ADC 的规则序列配置多个通道。
  2. 每轮 ADC 扫描完成后,结果依次保存在 RDR[0]、RDR[1]…… 中。
  3. DMA 使用单次模式,把这些结果搬入一个采样帧。
  4. DMA 完成中断中重新设置源地址、目的地址和传输数量。
  5. 使用双缓冲保存数据,避免 CPU 处理数据时被下一轮 DMA 覆盖。

本文以 ADC1 采集 4 个通道为例,介绍具体实现。流程图如下


2. ADC 多通道数据排列

假设 ADC 规则序列配置如下:

规则序号

ADC 通道

引脚

RSEQ0

ADC1_CH2

PA0

RSEQ1

ADC1_CH3

PA1

RSEQ2

ADC1_CH4

PA2

RSEQ3

ADC1_CH5

PA3

一次规则序列转换结束后,结果分别存放在:

ADC1->RDR[0]

ADC1->RDR[1]

ADC1->RDR[2]

ADC1->RDR[3]

因此,一轮 ADC 扫描可以看作一个采样帧:

一帧数据 = CH2 + CH3 + CH4 + CH5

DMA 每次搬运 4 个数据,即可得到一帧完整的多通道采样结果。


3. 数据结构设计

先定义通道数量和双缓冲区:

#define ADC_CHANNEL_COUNT 4U volatile uint16_t g_adc_buffer[2][ADC_CHANNEL_COUNT]; volatile uint8_t g_adc_dma_index = 0U; volatile uint8_t g_adc_ready_index = 0U; volatile uint8_t g_adc_frame_ready = 0U;

两个缓冲区交替工作:

  • DMA 正在写缓冲区 0 时,CPU 可以处理缓冲区 1。
  • DMA 正在写缓冲区 1 时,CPU可以处理缓冲区 0。

这种方式通常称为乒乓缓冲或双缓冲。


4. ADC 多通道初始化

下面以 PA0~PA3 对应的 ADC1 通道为例:

void User_ADC1_Init(void) { ADC_InitTypeDef adc; ADC_ChannelTypeDef channel; GPIO_InitTypeDef gpio; ADC1_CLK_ENABLE(); GPIOA_CLK_ENABLE(); /* PA0、PA1、PA2、PA3配置为模拟输入 */ gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3; gpio.GPIO_Mode = GPIO_Mode_AN; gpio.GPIO_IType = GPIO_IN_FLOATING; gpio.GPIO_OStrength = GPIO_OStrength_4_5mA; GPIO_Init(GPIOA, &gpio); ADC_StructInit(&adc); adc.ADC_ClockDivide = ADC_CLK_DIVIDE_4; adc.ADC_Resolution = ADC_RESOLUTION_12BIT; adc.ADC_Alignment = ADC_DATA_ALIGN_RIGHT; adc.ADC_Symbol = ADC_DATA_SELDO_UNSIGNED; /* * 若由软件启动连续转换,选择 INTERNAL。 * 若由 TIM0 周期触发,则改为对应的外部触发源。 */ adc.ADC_RegularTrigger = ADC_TRIGGER_INTERNAL; adc.ADC_RegularEnable = ENABLE; adc.ADC_InjectEnable = DISABLE; adc.ADC_DmaEnable = ENABLE; /* 多通道规则序列必须开启扫描模式 */ adc.ADC_ScanModeEn = ENABLE; /* * 软件连续采集时可设置为 ENABLE。 * 如果使用定时器周期触发,一般设置为 DISABLE。 */ adc.ADC_ContinuousModeEn = ENABLE; adc.ADC_RegularDiscontinuousModeEn = DISABLE; adc.ADC_InjectDiscontinuousModeEn = DISABLE; adc.ADC_InjectAutoModeEn = DISABLE; adc.ADC_IntervalModeEn = DISABLE; adc.ADC_RegularSequenceLength = ADC_CHANNEL_COUNT; adc.ADC_InjectSequenceLength = 0U; adc.ADC_PowerEn = ENABLE; adc.ADC_PowerRegain = ENABLE; ADC_Init(ADC1, &adc); channel.ADC_SampleTime = ADC_SPT_CLK_640; channel.ADC_InterruptEn = DISABLE; channel.ADC_Channel = ADC_CH_2; ADC_ConfigChannel(ADC1, ADC_RSEQ_0, &channel); channel.ADC_Channel = ADC_CH_3; ADC_ConfigChannel(ADC1, ADC_RSEQ_1, &channel); channel.ADC_Channel = ADC_CH_4; ADC_ConfigChannel(ADC1, ADC_RSEQ_2, &channel); channel.ADC_Channel = ADC_CH_5; ADC_ConfigChannel(ADC1, ADC_RSEQ_3, &channel); }

这里最重要的是:

adc.ADC_ScanModeEn = ENABLE;

adc.ADC_RegularSequenceLength = ADC_CHANNEL_COUNT;

规则序列长度必须与实际配置的通道数量一致。


5. DMA 单次模式初始化

定义 ADC 使用的 DMA 通道:

#define ADC_DMA_CHANNEL DMA0_Channel1

DMA 的源地址为 ADC1->RDR[0],目的地址为当前 DMA 缓冲区。

void User_ADC1_DMA_Init(void) { DMA_InitTypeDef dma; PLIC_InitTypeDef plic; DMA0_CLK_ENABLE(); DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); DMA_DeInit(ADC_DMA_CHANNEL); DMA_ClearFlag(DMA0, DMA_FLAG_FTF1); DMA_ClearFlag(DMA0, DMA_FLAG_ERR1); DMA_StructInit(&dma); dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->RDR[0]; dma.DMA_MemoryBaseAddr = (uint32_t)&g_adc_buffer[0][0]; dma.DMA_DIR = DMA_DIR_PeripheralSRC; /* 一次只搬运一帧,即4个通道 */ dma.DMA_BufferSize = ADC_CHANNEL_COUNT; /* * RDR[0]~RDR[3]地址连续,因此外设地址递增。 */ dma.DMA_PeripheralInc = DMA_PeripheralInc_Enable; /* 目标数组地址递增 */ dma.DMA_MemoryInc = DMA_MemoryInc_Enable; /* * ADC的RDR寄存器是32位寄存器, * 内存仅保存12位转换结果,因此使用16位数组。 */ dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Priority = DMA_Priority_VeryHigh; dma.DMA_M2M = DMA_M2M_Disable; DMA_Init(ADC_DMA_CHANNEL, &dma); /* 将DMA通道连接到ADC1请求 */ DMA_PeriphIF(ADC_DMA_CHANNEL, ADC1_IF); DMA_ITConfig(ADC_DMA_CHANNEL, DMA_FTF_IT, ENABLE); DMA_ITConfig(ADC_DMA_CHANNEL, DMA_ERR_IT, ENABLE); plic.PLIC_IRQChannel = DMA0_IRQn; plic.PLIC_IRQPriority = 2U; plic.PLIC_IRQChannelCmd = ENABLE; PLIC_Init(&plic); }

为什么 DMA 每次只搬运一个采样帧?

如果外设地址递增,DMA 会依次读取:

RDR[0] → RDR[1] → RDR[2] → RDR[3]

但是 DMA 不会自动把外设地址恢复为 RDR[0]。如果直接把传输数量设置为多帧数据量,那么完成第一帧后,源地址还会继续递增,最终访问到 ADC 结果寄存器区域以外的地址。

因此,正确做法是:

• 每次DMA搬运4个通道

• 产生完成中断

• 重新装载RDR[0]和下一块缓冲区地址

• 开始下一轮搬运


6. DMA 重新装载

DMA 重新装载时应先关闭通道,然后依次设置计数器、外设地址和内存地址:

static void User_ADC1_DMA_Arm(uint8_t buffer_index) { DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); DMA_ClearFlag(DMA0, DMA_FLAG_FTF1); DMA_ClearFlag(DMA0, DMA_FLAG_ERR1); ADC_DMA_CHANNEL->CHCNT = ADC_CHANNEL_COUNT; ADC_DMA_CHANNEL->CHPADDR = (uint32_t)&ADC1->RDR[0]; ADC_DMA_CHANNEL->CHMADDR = (uint32_t)&g_adc_buffer[buffer_index][0]; DMA_Cmd(ADC_DMA_CHANNEL, ENABLE); }

这里需要同时恢复三个关键参数:

CHCNT /* 本次传输数量 */

CHPADDR /* 外设起始地址 */

CHMADDR /* 内存起始地址 */

只恢复 CHCNT 是不够的,因为上一轮传输后,DMA 的源地址和目的地址通常已经发生变化。


7. 启动 ADC 和 DMA

void User_ADC1_Start(void) { g_adc_dma_index = 0U; g_adc_frame_ready = 0U; User_ADC1_DMA_Arm(g_adc_dma_index); /* * AS32X601带Cache。 * DMA启动前应根据芯片Cache接口处理缓冲区一致性。 */ ClearCache(); ADC1->CTRL0 |= ADC_CTRL0_TRIGEN | ADC_CTRL0_DMAEN; ADC_SoftRegularConvert(ADC1); }

如果采用 TIM0 外部触发,则应先准备好 DMA,再启动定时器:

• 初始化ADC

• 初始化DMA

• 装载并使能DMA

• 使能ADC外部触发

• 启动TIM0

这个顺序可以避免第一次 ADC 转换发生时 DMA 尚未准备好。


8. DMA 完成中断

void DMA0_IRQ_Handler(void) { uint8_t completed_index; if (DMA_GetITStatus(DMA0, DMA_IT_FTF1) != RESET) { DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); DMA_ClearITPendingBit(DMA0, DMA_IT_FTF1); completed_index = g_adc_dma_index; /* 切换到另一个DMA缓冲区 */ g_adc_dma_index ^= 1U; /* * 先重新装载DMA,尽量缩短DMA未就绪的时间。 */ User_ADC1_DMA_Arm(g_adc_dma_index); /* * 根据具体Cache接口,使CPU能够看到DMA写入的数据。 * 如果芯片只提供整个Cache维护接口,可使用ClearCache(); * 若提供按地址失效接口,优先只处理完成缓冲区。 */ ClearCache(); g_adc_ready_index = completed_index; g_adc_frame_ready = 1U; } if (DMA_GetITStatus(DMA0, DMA_IT_ERR1) != RESET) { DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); DMA_ClearITPendingBit(DMA0, DMA_IT_ERR1); /* * 根据应用需求记录错误, * 然后重新装载DMA或停止ADC。 */ } }

中断函数中不要进行串口打印、滤波或浮点运算。中断只负责:

  1. 清除中断标志。
  2. 切换 DMA 缓冲区。
  3. 重新装载 DMA。
  4. 通知主循环处理已经完成的缓冲区。

9. 主循环处理数据

int main(void) { uint8_t index; Systemclock_Init(); delay_init(160); User_Print_Init(921600); User_ADC1_Init(); User_ADC1_DMA_Init(); User_ADC1_Start(); while (1) { if (g_adc_frame_ready != 0U) { /* * 简单应用中可短暂关中断保护这两个共享变量。 * 更高采样率场景建议使用帧计数或消息队列。 */ index = g_adc_ready_index; g_adc_frame_ready = 0U; printf("CH2=%u, CH3=%u, CH4=%u, CH5=%u\r\n", (unsigned int)g_adc_buffer[index][0], (unsigned int)g_adc_buffer[index][1], (unsigned int)g_adc_buffer[index][2], (unsigned int)g_adc_buffer[index][3]); } } }

缓冲区中的通道顺序与规则序列一致:

buffer[x][0] = RSEQ0,也就是ADC_CH2

buffer[x][1] = RSEQ1,也就是ADC_CH3

buffer[x][2] = RSEQ2,也就是ADC_CH4

buffer[x][3] = RSEQ3,也就是ADC_CH5


10. 使用单次 DMA 时的关键问题

10.1 必须重新装载 DMA 地址

DMA 完成后,不能只重新使能 DMA。因为:

  • CHCNT 已经减到 0。
  • 外设地址已经从 RDR[0] 移动到最后一个结果寄存器之后。
  • 内存地址也已经移动到缓冲区末尾。

• 下一轮开始前必须重新设置:

ADC_DMA_CHANNEL->CHCNT;

ADC_DMA_CHANNEL->CHPADDR;

ADC_DMA_CHANNEL->CHMADDR;

10.2 注意下一次 ADC 触发间隔

从 DMA 完成到重新使能 DMA 之间存在一个很短的中断处理窗口。如果下一个 ADC 触发在这个窗口内到来,可能出现:

  • ADC DMA 请求丢失;
  • 通道数据错位;
  • 一帧数据不完整;
  • DMA 溢出或 ADC overrun。

因此必须保证:

ADC触发周期 > 一帧转换时间 + DMA完成中断重新装载时间

如果采样频率很高,可以考虑:

  • 降低 ADC 触发频率;
  • 提高 DMA 中断优先级;
  • 缩短中断函数;
  • 使用 ADC 序列完成信号控制下一次触发;
  • 在重新装载 DMA 期间暂时屏蔽 ADC 触发;

10.3 软件连续转换不一定适合高速多通道采集

如果配置:

ADC_ContinuousModeEn = ENABLE;

ADC 可能在 DMA 完成中断尚未重新装载时就开始下一轮转换。对于采样时序要求较高的应用,建议使用 TIM0 周期触发:

ADC_ContinuousModeEn = DISABLE;

ADC_RegularTrigger = ADC_TRIGGER_EXTERNAL_x;

这样可以获得稳定的采样周期,并为 DMA 重装留下确定的时间。

10.4 注意 Cache 一致性

AS32X601 的 CPU 带有 Cache,而 DMA 直接访问物理内存。DMA 已经写完数据,并不代表 CPU 立即能够从 Cache 中读取到最新值。

因此需要根据平台提供的 Cache API:

  • DMA 写内存、CPU 读内存:读取前失效对应 Cache。
  • CPU 写内存、DMA 读内存:启动 DMA 前清理对应 Cache。
  • DMA 缓冲区最好按 Cache Line 对齐。
  • 如果支持非缓存区,DMA 缓冲区可放入非缓存内存段。

不要仅依靠 volatile。volatile 只能限制编译器优化,不能解决 CPU Cache 与 DMA 之间的数据一致性问题。

10.5 不要在 DMA 中断中打印

串口打印耗时通常远大于 DMA 重装操作。如果在中断中调用 printf(),很容易错过下一次 ADC 触发。

DMA 中断应尽可能短,打印和数据处理放到主循环或任务中完成。


11. 如果需要采集多帧数据

假设需要一次采集 1024 帧,每帧包含 4 个通道,可以定义:

#define ADC_CHANNEL_COUNT 4U #define ADC_FRAME_COUNT 1024U volatile uint16_t g_adc_data[ADC_FRAME_COUNT][ADC_CHANNEL_COUNT]; volatile uint32_t g_adc_frame_count = 0U;

DMA 每次仍然只传输一帧,但目的地址指向不同的数组行:

static void ADC_DMA_ArmFrame(uint32_t frame) { DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); ADC_DMA_CHANNEL->CHCNT = ADC_CHANNEL_COUNT; ADC_DMA_CHANNEL->CHPADDR = (uint32_t)&ADC1->RDR[0]; ADC_DMA_CHANNEL->CHMADDR = (uint32_t)&g_adc_data[frame][0]; DMA_Cmd(ADC_DMA_CHANNEL, ENABLE); }

完成中断中推进帧号:

void DMA0_IRQ_Handler(void) { if (DMA_GetITStatus(DMA0, DMA_IT_FTF1) != RESET) { DMA_Cmd(ADC_DMA_CHANNEL, DISABLE); DMA_ClearITPendingBit(DMA0, DMA_IT_FTF1); ++g_adc_frame_count; if (g_adc_frame_count < ADC_FRAME_COUNT) { ADC_DMA_ArmFrame(g_adc_frame_count); } else { ADC1->CTRL0 &= ~(ADC_CTRL0_TRIGEN | ADC_CTRL0_DMAEN); ClearCache(); g_adc_frame_ready = 1U; } } }

这样最终得到的数据排列为:

g_adc_data[0][0~3] 第0帧的4个通道

g_adc_data[1][0~3] 第1帧的4个通道

...

g_adc_data[1023][0~3] 第1023帧的4个通道


12. 总结

AS32X601 的 DMA 没有可直接使用的循环模式时,仍然可以完成 ADC 多通道连续采集。核心方法是:

• ADC扫描多个规则通道

• DMA单次搬运一帧数据

• DMA完成中断

• 恢复CHCNT、CHPADDR和CHMADDR

• 切换缓冲区

• 继续下一轮采集

实现时需要特别注意以下几点:

  • ADC 必须开启扫描模式。
  • 规则序列长度必须等于通道数量。
  • DMA 外设地址从 RDR[0] 开始,并开启外设地址递增。
  • DMA 每次只搬运一轮规则序列。
  • DMA 完成后必须恢复计数、源地址和目的地址。
  • 高采样率场景建议使用 TIM0 等定时器触发 ADC。
  • 中断中只进行 DMA 重装和状态更新。
  • 必须正确处理 CPU Cache 与 DMA 的一致性。
  • 使用双缓冲可以减少数据覆盖风险。

通过软件重装 DMA,可以在不依赖硬件循环模式的情况下,实现稳定、低 CPU 占用的 ADC 多通道数据采集。