1. 背景
在数据采集项目中,经常需要 ADC 按固定周期采集多个模拟通道,并通过 DMA 自动把转换结果搬运到内存,以减少 CPU 占用。
AS32X601 当前 DMA 驱动没有提供可直接使用的循环模式,因此 DMA 完成一次传输后,需要由软件重新装载 DMA 参数。
对于 ADC 多通道扫描,可以采用下面的方案:
- ADC 的规则序列配置多个通道。
- 每轮 ADC 扫描完成后,结果依次保存在 RDR0、RDR1...... 中。
- DMA 使用单次模式,把这些结果搬入一个采样帧。
- DMA 完成中断中重新设置源地址、目的地址和传输数量。
- 使用双缓冲保存数据,避免 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->RDR0
ADC1->RDR1
ADC1->RDR2
ADC1->RDR3
因此,一轮 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->RDR0,目的地址为当前 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 会依次读取:
RDR0 → RDR1 → RDR2 → RDR3
但是 DMA 不会自动把外设地址恢复为 RDR0。如果直接把传输数量设置为多帧数据量,那么完成第一帧后,源地址还会继续递增,最终访问到 ADC 结果寄存器区域以外的地址。
因此,正确做法是:
• 每次DMA搬运4个通道
• 产生完成中断
• 重新装载RDR0和下一块缓冲区地址
• 开始下一轮搬运
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。
*/
}
}
中断函数中不要进行串口打印、滤波或浮点运算。中断只负责:
- 清除中断标志。
- 切换 DMA 缓冲区。
- 重新装载 DMA。
- 通知主循环处理已经完成的缓冲区。
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]);
}
}
}
缓冲区中的通道顺序与规则序列一致:
bufferx0 = RSEQ0,也就是ADC_CH2
bufferx1 = RSEQ1,也就是ADC_CH3
bufferx2 = RSEQ2,也就是ADC_CH4
bufferx3 = RSEQ3,也就是ADC_CH5
10. 使用单次 DMA 时的关键问题
10.1 必须重新装载 DMA 地址
DMA 完成后,不能只重新使能 DMA。因为:
- CHCNT 已经减到 0。
- 外设地址已经从 RDR0 移动到最后一个结果寄存器之后。
- 内存地址也已经移动到缓冲区末尾。
• 下一轮开始前必须重新设置:
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_data00~3 第0帧的4个通道
g_adc_data10~3 第1帧的4个通道
...
g_adc_data10230~3 第1023帧的4个通道
12. 总结
AS32X601 的 DMA 没有可直接使用的循环模式时,仍然可以完成 ADC 多通道连续采集。核心方法是:
• ADC扫描多个规则通道
• DMA单次搬运一帧数据
• DMA完成中断
• 恢复CHCNT、CHPADDR和CHMADDR
• 切换缓冲区
• 继续下一轮采集
实现时需要特别注意以下几点:
- ADC 必须开启扫描模式。
- 规则序列长度必须等于通道数量。
- DMA 外设地址从 RDR0 开始,并开启外设地址递增。
- DMA 每次只搬运一轮规则序列。
- DMA 完成后必须恢复计数、源地址和目的地址。
- 高采样率场景建议使用 TIM0 等定时器触发 ADC。
- 中断中只进行 DMA 重装和状态更新。
- 必须正确处理 CPU Cache 与 DMA 的一致性。
- 使用双缓冲可以减少数据覆盖风险。
通过软件重装 DMA,可以在不依赖硬件循环模式的情况下,实现稳定、低 CPU 占用的 ADC 多通道数据采集。