4、ADC与DMA协同工作:ADC触发DMA的配置流程、数据对齐方式与内存映射

在STM32H7系列中,ADC的采样率可达数Msps(如H743的ADC时钟最高为36MHz,采样率可达3.6Msps),若采用CPU中断方式搬运采样数据,不仅会因频繁进入中断而占用大量CPU周期,还可能导致在高频采样时因响应延迟而丢失数据。DMA(Direct Memory Access)的引入正是为了解决这一瓶颈:它允许ADC转换完成后直接触发DMA传输,将结果从ADC数据寄存器搬运至用户指定的内存区域,全程无需CPU干预。本章将深入剖析ADC与DMA协同工作的核心机制,包括触发链路、数据对齐规则、内存映射策略以及多通道场景下的配置要点。

ADC 外设域 DMA 控制器域 内存域 (SRAM) ADC 转换核心 DR 数据寄存器 (32-bit, 右对齐) DMA 通道 FIFO / 打包 ADC_Buffer[ ] 半字/字对齐 循环缓冲区 DMA 请求 (硬件触发) 总线传输 (AHB) ADC 触发 DMA 配置流程 ① 配置 ADC 通道/采样时间 ② 设置 DMA 请求模式 (单次/循环) ③ 配置 DMA 通道: 外设地址=&DR, 内存地址=&Buffer ④ 设置数据宽度 (半字/字), 内存地址增量 ⑤ 使能 DMA 通道, 使能 ADC DMA 请求 ⑥ 启动 ADC 转换 (软件/定时器触发) ⑦ 在 DMA 传输完成中断中处理数据 ⑧ 循环模式下自动回绕, 无需干预

上图展示了ADC、DMA与内存三者之间的硬件链路关系。ADC转换完成后,其数据寄存器(DR)会向DMA控制器发出请求信号;DMA控制器在收到请求后,通过AHB总线将DR中的内容搬运至SRAM中的指定缓冲区。整个过程由硬件自动完成,CPU只需在DMA传输完成中断中处理数据即可。下面我们将从配置流程、数据对齐和内存映射三个维度展开详细讲解。

4.1 ADC触发DMA的完整配置流程

4.1.1 核心原理:硬件握手机制

STM32H7的ADC与DMA之间采用“请求-应答”握手协议。当ADC完成一次转换并将结果锁存到DR寄存器时,会向DMA控制器发送一个脉冲请求信号(ADCx_DRDY)。DMA控制器在检测到该请求后,根据预先配置的传输参数(源地址、目的地址、数据宽度、传输计数等)执行一次总线传输。传输完成后,DMA会释放总线,等待下一次ADC请求。这一机制的关键在于:DMA传输的触发源是ADC的硬件事件,而非软件指令,因此CPU可以在ADC连续采样期间完全进入低功耗模式或执行其他任务。

在H7系列中,每个ADC(ADC1/ADC2/ADC3)都有独立的DMA请求线,分别映射到DMA1或DMA2的不同通道。例如,ADC1的DMA请求可以连接到DMA1的通道0或DMA2的通道0,具体取决于用户选择的DMA控制器和通道映射表。这种灵活性允许开发者根据系统总线负载情况选择最优的DMA通道。

4.1.2 操作流程:从初始化到启动

以下是一个标准的ADC+DMA初始化流程,以ADC1、DMA2通道0为例,使用HAL库函数实现:

// 1. 配置ADC句柄
ADC_HandleTypeDef hadc1;
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; // 36MHz
hadc1.Init.Resolution = ADC_RESOLUTION_16B;           // 16位分辨率
hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;            // 扫描模式
hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;           // 序列转换结束
HAL_ADC_Init(&hadc1);

// 2. 配置ADC通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_0;      // 通道0
sConfig.Rank = ADC_REGULAR_RANK_1;    // 规则组第1个
sConfig.SamplingTime = ADC_SAMPLETIME_32CYCLES_5; // 采样时间
HAL_ADC_ConfigChannel(&hadc1, &sConfig);

// 3. 配置DMA句柄
DMA_HandleTypeDef hdma_adc1;
hdma_adc1.Instance = DMA2_Stream0;
hdma_adc1.Init.Request = DMA_REQUEST_ADC1;  // 请求源为ADC1
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; // 外设地址不增量
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;     // 内存地址增量
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 16位
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc1.Init.Mode = DMA_CIRCULAR;          // 循环模式
hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_adc1);

// 4. 关联DMA与ADC
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

// 5. 启动ADC DMA传输
uint16_t adc_buffer[128];  // 采样缓冲区
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 128);

上述代码中,HAL_ADC_Start_DMA()函数会同时使能ADC和DMA,启动后ADC自动开始转换,每次转换完成都会触发DMA搬运。当DMA传输计数减至0时(即采集满128个样本),会触发DMA传输完成中断,用户可在中断回调中处理数据。

4.1.3 案例拆解:多通道顺序采样

假设我们需要采集3个模拟通道(CH0、CH1、CH2),每个通道采样100次,共300个数据点。配置要点如下:

  • 扫描模式:使能SCAN_ENABLE,并配置3个通道的Rank分别为1、2、3。
  • DMA缓冲区大小:设置为300(3通道×100次),数据类型为uint16_t。
  • 数据排列:DMA按转换顺序依次写入缓冲区,即[CH0_1, CH1_1, CH2_1, CH0_2, CH1_2, CH2_2, ...]。
  • 循环模式:若使用DMA_CIRCULAR,缓冲区填满后自动回绕覆盖,适合连续监测场景。

在实际项目中,若需要区分各通道数据,可通过索引计算:第i次采样中,CH0位于buffer[i*3],CH1位于buffer[i*3+1],CH2位于buffer[i*3+2]。这种固定排列方式便于后续AI推理时直接按通道提取特征。

4.1.4 常见误区与复盘

误区1:DMA缓冲区大小设置错误。若缓冲区大小小于ADC转换次数,DMA会提前触发完成中断,导致数据截断。务必确保缓冲区大小 ≥ 转换次数 × 通道数。

误区2:忽略DMA请求映射。H7系列中,同一DMA流可映射多个外设请求,但同一时刻只能选择一个。若ADC1的DMA请求未正确映射到DMA2_Stream0,则DMA不会响应。

误区3:数据宽度不匹配。ADC1的DR寄存器为32位,但实际有效数据仅低16位(16位分辨率时)。若DMA配置为32位宽度,则每次传输会占用4字节内存,造成浪费且可能覆盖相邻数据。

4.2 数据对齐方式与内存映射策略

4.2.1 核心原理:右对齐与左对齐

STM32H7的ADC支持两种数据对齐方式:右对齐和左对齐。右对齐是默认模式,数据存储在DR寄存器的低N位(N为分辨率位数),高位置0。例如16位分辨率下,转换值范围0~65535,存储在DR[15:0]。左对齐则将数据存储在DR的高N位,低位置0,相当于将结果左移(32-N)位。左对齐常用于需要将ADC结果直接作为定点数参与运算的场景,可简化后续的移位处理。

对于DMA传输而言,对齐方式直接影响内存中数据的解释方式。若使用右对齐,内存中的uint16_t值即为实际ADC转换值;若使用左对齐,则需右移(32-N)位才能得到真实值。在AI推理场景中,通常需要将ADC值归一化到[0,1]或[-1,1]区间,右对齐更便于直接进行浮点转换。

4.2.2 内存映射:缓冲区设计与缓存一致性

在STM32H7中,DMA可以访问SRAM1、SRAM2、SRAM3以及AXI SRAM。对于ADC采样数据,推荐使用SRAM1或SRAM2,因为它们与DMA控制器之间的总线路径较短,延迟更低。若使用AXI SRAM(0x24000000起始),需注意其与CPU缓存(D-Cache)的一致性:当DMA写入数据后,CPU读取前必须执行SCB_InvalidateDCache_by_Addr(),否则可能读到缓存中的旧数据。

内存区域起始地址大小DMA访问特性推荐场景
SRAM10x30000000128KBDMA1/DMA2均可访问,延迟低ADC采样缓冲区
SRAM20x30020000128KBDMA1/DMA2均可访问,延迟低ADC采样缓冲区
SRAM30x3004000032KB仅DMA2可访问备用缓冲区
AXI SRAM0x24000000512KBDMA2可访问,需处理缓存一致性大型数据缓冲

对于AI推理场景,建议将ADC缓冲区定义在SRAM1中,并采用双缓冲机制:DMA在采集当前缓冲区时,CPU处理上一缓冲区数据,通过DMA传输完成中断切换缓冲区指针。这种机制可避免数据覆盖冲突,提高系统吞吐量。

4.2.3 案例拆解:AI推理前的数据预处理

假设我们使用ADC采集振动传感器信号,采样率为10kHz,每次推理需要256个样本。配置如下:

#define SAMPLE_COUNT 256
#define BUFFER_SIZE  (SAMPLE_COUNT * 2)  // 双缓冲

uint16_t adc_buf[2][SAMPLE_COUNT] __attribute__((section(".ARM.__at_0x30000000")));
volatile uint8_t buf_index = 0;

// DMA传输完成中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (hadc->Instance == ADC1) {
        // 处理当前缓冲区数据
        process_ai_inference((float*)adc_buf[buf_index], SAMPLE_COUNT);
        // 切换缓冲区
        buf_index ^= 1;
        // 重新启动DMA传输到新缓冲区
        HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf[buf_index], SAMPLE_COUNT);
    }
}

在上述代码中,process_ai_inference()函数将ADC原始值转换为浮点数据并送入神经网络。由于DMA采用循环模式,每次传输完成中断后自动重新开始,配合双缓冲切换,可实现无间隙的连续采样与推理。

4.2.4 常见误区与优化建议

优化建议1:使用DMA的FIFO功能。H7的DMA支持FIFO模式,可缓存多个数据后再写入内存,减少总线占用次数。对于ADC采样,建议将FIFO阈值设置为半满(4个数据),以平衡延迟与总线效率。

优化建议2:合理选择数据宽度。若ADC分辨率为16位,DMA数据宽度应设为半字(16位),内存对齐也设为半字。若使用32位宽度,则每次传输会读取DR的完整32位,但高16位无意义,浪费带宽。

优化建议3:注意DMA传输完成中断的优先级。在实时性要求高的AI推理场景中,应将DMA中断优先级设置为高于普通外设中断,但低于实时性更强的控制中断(如定时器PWM更新中断)。

风险提示:缓存一致性问题。若将ADC缓冲区放置在AXI SRAM且使能了D-Cache,必须在CPU读取数据前执行缓存失效操作。否则,CPU可能读取到DMA写入前的旧数据,导致推理结果错误。推荐使用SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf[buf_index], SAMPLE_COUNT * 2)。

4.3 实战要点与性能调优

4.3.1 采样率与DMA带宽的匹配

STM32H7的DMA2最高支持AHB时钟频率(通常为240MHz),每次传输需要多个时钟周期。以16位数据宽度为例,DMA传输一次约需4个AHB时钟周期。若ADC采样率为3.6Msps,则DMA需每秒执行360万次传输,占用约1440万时钟周期,占AHB总带宽的6%。这一比例在可接受范围内,但若同时运行多个DMA通道(如ADC+DAC+UART),需注意总线带宽分配。

4.3.2 低功耗模式下的DMA行为

在睡眠模式下,DMA仍可正常工作,但需确保ADC时钟保持开启。若进入停止模式,ADC和DMA均停止工作,需在唤醒后重新初始化。对于电池供电的AIoT设备,建议采用定时器触发ADC采样,在采样间隙让CPU进入睡眠模式,DMA完成中断唤醒CPU处理数据。

4.3.3 案例复盘:从原始数据到AI特征

某工业设备监测项目需采集三相电流信号,每相采样率为5kHz,每次推理窗口为200ms(即1000个采样点/通道)。采用ADC1的3个通道,DMA循环模式,缓冲区大小为3000(3×1000)。在DMA传输完成中断中,将原始数据转换为浮点数组,并提取RMS、峰值、频率特征后送入轻量级神经网络。实测CPU占用率从原来的35%(中断搬运方式)降至8%,推理延迟从12ms降至5ms,系统整体功耗降低40%。

该案例的关键成功因素包括:双缓冲机制避免了数据覆盖;DMA优先级设置为高,确保采样无丢失;使用__attribute__((section))将缓冲区固定在SRAM1,减少总线冲突。

4.3.4 总结与最佳实践

  • 配置顺序:先初始化DMA,再初始化ADC,最后启动DMA传输。
  • 缓冲区管理:优先使用双缓冲+循环模式,避免数据覆盖。
  • 数据对齐:右对齐模式便于直接使用,左对齐适合定点运算。
  • 内存选择:SRAM1/2优先,AXI SRAM需处理缓存一致性。
  • 调试技巧:使用DMA的传输完成中断和错误中断(如FIFO溢出)进行状态监控。

通过合理配置ADC与DMA的协同工作,开发者可以构建高效、低延迟的数据采集链路,为后续的AI推理提供稳定、连续的数据流。在实际项目中,还需根据具体传感器特性和推理算法需求,灵活调整采样率、缓冲区大小和中断优先级,以达到系统性能的最优平衡。