MCU芯片AS32X601如何使用 DMA 单次模式搬运 ADC 多通道采样数据? 1. 背景在数据采集项目中经常需要 ADC 按固定周期采集多个模拟通道并通过 DMA 自动把转换结果搬运到内存以减少 CPU 占用。AS32X601 当前 DMA 驱动没有提供可直接使用的循环模式因此 DMA 完成一次传输后需要由软件重新装载 DMA 参数。对于 ADC 多通道扫描可以采用下面的方案ADC 的规则序列配置多个通道。每轮 ADC 扫描完成后结果依次保存在 RDR[0]、RDR[1]…… 中。DMA 使用单次模式把这些结果搬入一个采样帧。DMA 完成中断中重新设置源地址、目的地址和传输数量。使用双缓冲保存数据避免 CPU 处理数据时被下一轮 DMA 覆盖。本文以 ADC1 采集 4 个通道为例介绍具体实现。流程图如下2. ADC 多通道数据排列假设 ADC 规则序列配置如下规则序号ADC 通道引脚RSEQ0ADC1_CH2PA0RSEQ1ADC1_CH3PA1RSEQ2ADC1_CH4PA2RSEQ3ADC1_CH5PA3一次规则序列转换结束后结果分别存放在ADC1-RDR[0]ADC1-RDR[1]ADC1-RDR[2]ADC1-RDR[3]因此一轮 ADC 扫描可以看作一个采样帧一帧数据 CH2 CH3 CH4 CH5DMA 每次搬运 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 多通道初始化下面以 PA0PA3 对应的 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_Channel1DMA 的源地址为 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 和 DMAvoid 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]); } } }缓冲区中的通道顺序与规则序列一致buffer[x][0] RSEQ0也就是ADC_CH2buffer[x][1] RSEQ1也就是ADC_CH3buffer[x][2] RSEQ2也就是ADC_CH4buffer[x][3] RSEQ3也就是ADC_CH510. 使用单次 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 APIDMA 写内存、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][03] 第0帧的4个通道g_adc_data[1][03] 第1帧的4个通道...g_adc_data[1023][03] 第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 多通道数据采集。