ADC_DMA模式理论部分

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

150.6.7 ADC_DMA模式理论部分

/*
 * 整体目的:启动 ADC1 三通道连续扫描,由 DMA 自动把每次转换结果循环搬到 RAM,
 *           主循环只读取缓冲区并通过 USART1 显示 PA0~PA2 的电压与原始码。
 * 设计思路:ADC1 按 Rank1~Rank3 转换 CH0~CH2;每次 ADC 更新同一个 DR 后,固定映射的
 *           DMA1_Channel1 立即把一个半字写入数组下一个位置,三项传完后循环回到首项。
 * 硬件依据:PA0~PA2 分别为 ADC1_IN0~IN2;ADC 为 12 位右对齐,DMA 两端均配置半字,
 *           所以 uint16_t[3] 与三个 Rank 一一匹配,ADC 时钟则由 PCLK2/6 得到 12 MHz。
 * 执行效果:DMA 在后台不断刷新数组;前台打印不决定采样时刻,也不能保证一次打印的
 *           三个元素恰好来自同一轮扫描,要求严格同步时应在传输完成回调中制作快照。
 * 代码范围:摘录 CubeIDE/12ADC_DMA 的用户代码;不展开 CubeMX 初始化或 HAL 库内部。
 */

uint16_t ADC_DMA_Buff[3] = {0U};           /* 依次保存 CH0、CH1、CH2 的最新 12 位转换结果。 */

HAL_ADCEx_Calibration_Start(&hadc1);       /* ADC1 初始化完成后、首次转换前执行一次自校准。 */
HAL_ADC_Start_DMA(                         /* 同时启动规则组转换和与 ADC1 关联的 DMA 传输。 */
    &hadc1,                                 /* 指定 ADC1,其 DMA 请求固定映射到 DMA1_Channel1。 */
    (uint32_t *)ADC_DMA_Buff,               /* HAL 参数类型为 uint32_t*,实际搬运宽度仍由 DMA 设为半字。 */
    3U);                                    /* 传输三个数据项,必须同时匹配 Rank 数和数组元素数。 */

while (1)                                   /* DMA 循环采样期间,前台持续显示缓冲区最新内容。 */
{                                           /* 一轮串口显示开始。 */
    for (uint8_t i = 0U; i < 3U; i++)      /* 依次读取数组下标 0~2,对应 PA0~PA2。 */
    {                                       /* 当前通道的数据处理开始。 */
        float ADC_Value =                   /* 保存本通道原始码换算出的估计输入电压。 */
            3.3f * (float)ADC_DMA_Buff[i] / 4096.0f; /* 假定 VDDA=3.3 V,按 1 LSB=Vref/4096 换算。 */
        printf(                              /* 通过已重定向的 USART1 输出本通道结果。 */
            "PA%u DMA_ADC_Value:%1.2fv   ADC Num: %u\n", /* 显示引脚号、电压和原始码。 */
            (unsigned int)i,                /* 下标值正好对应 PA0、PA1、PA2 的数字后缀。 */
            (double)ADC_Value,              /* float 传入可变参数时提升为 double,显式转换便于理解。 */
            (unsigned int)ADC_DMA_Buff[i]); /* 读取 DMA 最近写入的 0~4095 原始结果。 */
    }                                       /* 当前通道显示结束,继续读取下一个元素。 */
}                                           /* 返回 while 顶部;课程源码未限速,串口可能成为瓶颈。 */

本节讲解 ADC 的 DMA 采集思路和 CubeMX 配置。配套工程已给出三通道循环 DMA 的完整实现;其实际配置是 ADC1 连续扫描 CH0、CH1、CH2,而非只停留在工程创建阶段。

一、ADC检测的DMA模式

1.1 打开工程

ADC 的一次规则组转换完成后,数字结果会写入 ADC 的数据寄存器 DR。STM32F1 的 ADC 转换数据寄存器是 16 位寄存器:12 位 ADC 的有效转换结果按所选对齐方式放在其中。因此,在本工程采用右对齐时,读取到的正常原始值范围为 0 到 4095。

不使用 DMA 时,常见读取方式有两种:

方式CPU 的工作适用情况
查询(轮询)CPU 反复检查 EOC 标志,转换完成后读取 DR单次、低频或流程很简单的采样
ADC 中断转换完成触发中断,CPU 在回调/中断处理流程中读取结果需要及时处理每个转换结果,但采样率不高
DMAADC 发出 DMA 请求,DMA 控制器从 DR 读出结果并写入 RAM连续、多通道或较频繁的采样

DMA 的价值是让 CPU 不必对每一笔 ADC 结果执行“读 DR、写数组”的搬运指令。但“DMA 不占用 CPU”不能理解成完全没有处理器开销:CPU 仍要完成时钟、ADC、DMA 的初始化并启动传输;DMA 会占用片上总线;工程还启用了 DMA 中断,半传输、传输完成或错误事件会进入 DMA1_Channel1_IRQHandler(),由 HAL 调用相应回调。

本工程的模拟输入为 PA0、PA1、PA2,分别对应 ADC1 的 CH0、CH1、CH2。GPIO 必须配置为 Analog,避免数字输入缓冲带来额外功耗和干扰。外部电压必须以模拟电源地为共同参考,并保持在 ADC 允许输入范围内;若使用电位器或传感器,通常将其输出接入相应 PA 引脚、地端与开发板共地,且不要超过 VDDA。视频没有给出特定传感器电路,不能据此假定三个通道外接了相同器件。

对于多通道连续扫描,轮询尤其容易出现逻辑问题。ADC 每完成一个 Rank 都会更新同一个 DR;若程序没有在下一次转换覆盖前及时读走数据,就无法仅靠一个寄存器保留完整的 CH0、CH1、CH2 三个结果。DMA 可以在每次转换完成时立即把该次结果依序保存到数组中。

1.2 DMA模式讲解

DMA(Direct Memory Access,直接存储器访问)是 STM32 内部的独立数据搬运模块。一次 DMA 传输至少需要明确以下信息:

配置项本工程的值含义
请求源/外设地址ADC1 DRADC 转换完成时提出 DMA 请求;外设地址固定
存储器地址ADC_DMA_Buff结果写入 RAM 数组;每次传输后递增到下一个元素
传输数量3按传输单位计数,不是字节数;对应三个 uint16_t 元素
传输方向Peripheral to Memory从 ADC 数据寄存器读,写入内存
数据宽度外设半字、存储器半字每个 ADC 结果以 16 位为一个 DMA 传输单元
模式Circular传完 3 个元素后自动从数组起始处继续覆盖写入

在 STM32F103 的 DMA 请求映射中,ADC1 使用 DMA1_Channel1。因此每轮扫描的数据路径为:

ADC1 CH0 转换完成 -> ADC1_DR -> ADC_DMA_Buff[0]
ADC1 CH1 转换完成 -> ADC1_DR -> ADC_DMA_Buff[1]
ADC1 CH2 转换完成 -> ADC1_DR -> ADC_DMA_Buff[2]
下一轮 CH0 转换完成 -> 回到 ADC_DMA_Buff[0]

这里的“3”必须与规则组转换数、数组元素个数一致。若 ADC 配置为 3 个 Rank 而 DMA 长度只有 2,循环写入的数组不再代表一帧 CH0/CH1/CH2 数据;反之,长度大于实际转换数也会造成数据帧边界不符合预期。

配套工程将 DMA 的外设地址递增关闭、存储器地址递增打开。原因是所有转换结果都来自同一个 ADC1->DR,而 RAM 中要依次存放不同通道的值。外设和存储器数据宽度均为半字,与 uint16_t ADC_DMA_Buff[3] 相匹配。虽然 HAL_ADC_Start_DMA() 的第二个参数类型是 uint32_t *,该 API 是以地址整数传入 DMA 层;在本工程中把按 2 字节对齐的 uint16_t 数组强制转换后,DMA 仍按“半字”配置写入每个数组元素,不能因此把缓冲区改理解为 uint32_t[3]。

“循环模式”使 DMA 计数到零后重新装载并继续传输,适合持续观察三路电压;“普通模式”则在传满一次后停止,若要继续采样需要软件再次启动 DMA。循环模式下主循环读取数组时,DMA 可能正好在改写其中某个元素,所以三次读取未必来自同一轮完整扫描。显示电压时通常可以接受这一点;若控制算法必须取得严格同步的一帧数据,应在 DMA 传输完成回调中置标志、使用双缓冲/复制快照,或在临界区内处理已完成的一帧,不能直接把正在循环写入的数组当作永久稳定的数据。

本工程生成并启用了 DMA1_Channel1_IRQn。HAL_ADC_Start_DMA() 会配置 DMA 的完成、半完成和错误处理流程,再由 HAL_DMA_Start_IT() 开始传输;中断服务函数调用 HAL_DMA_IRQHandler(&hdma_adc1)。因此,用户若需要按“半个缓冲区”或“完整缓冲区”处理数据,可实现 HAL_ADC_ConvHalfCpltCallback()、HAL_ADC_ConvCpltCallback() 等 HAL 回调,而不应在 ADC 的 DR 上再手工轮询同一批 DMA 数据。

1.3 新建工程

视频使用 STM32CubeIDE 为 STM32F103ZET6 创建工程。其可复现的关键配置如下。

类别配置
时钟源HSE,PLL 倍频后 SYSCLK 为 72 MHz
ADC 时钟PCLK2 / 6 = 12 MHz
ADC 实例ADC1
规则组扫描模式,连续转换,3 次转换,软件触发
通道顺序CH0 / Rank 1,CH1 / Rank 2,CH2 / Rank 3
采样时间三个通道均为 239.5 个 ADC 时钟周期
GPIOPA0、PA1、PA2:模拟输入
DMADMA1 Channel1,外设到存储器,外设不递增、存储器递增,半字,循环,低优先级
串口USART1,PA9 为 TX、PA10 为 RX,用于 printf 输出

ADC 时钟必须不高于 STM32F103 规定的 14 MHz。系统时钟和 PCLK2 为 72 MHz 时,选择 /6 得到 12 MHz,满足限制;不能直接以 72 MHz 驱动 ADC。工程中对应的时钟外设配置为:

PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
PeriphClkInit.AdcClockSelection = RCC_ADCPCLK2_DIV6;
HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit);

239.5 个采样周期是 F1 ADC 可选的最长采样时间。ADC 的单次总转换时间还包括固定的 12.5 个 ADC 时钟周期,因此本配置下单通道约为:

(239.5 + 12.5) / 12 MHz = 21 us

三个通道的一轮扫描约需 63 us,未计入软件启动、DMA 仲裁和打印输出等额外时间。较长采样时间适合源阻抗较高或希望输入采样电容有充分建立时间的信号;若外部信号驱动能力强且需要更高采样率,可在确认误差允许后缩短采样时间,不能只为提高速度而盲目修改。

在 CubeMX 的 ADC 配置页添加 CH0、CH1、CH2 后,要明确设置 Rank 1、2、3 和 Number Of Conversion = 3;只添加引脚而未设置规则组顺序,DMA 缓冲区索引就没有可靠的通道含义。DMA Settings 中添加 ADC1 的 DMA 请求并选择循环模式、半字宽度;NVIC 中保持 DMA1 channel1 global interrupt 启用,才能让 HAL 的 DMA 中断流程正常运行。代码生成选项使用“每个外设生成独立 .c/.h 文件”只影响文件组织,不改变 ADC 或 DMA 的硬件行为。

启动顺序应为:完成 GPIO、DMA、ADC 初始化后,先执行 ADC 校准,再启动 DMA。配套工程使用:

HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADC_DMA_Buff, 3);

实际产品代码应检查这两个 HAL 函数的返回值,并在失败时进入可诊断的错误处理,而不是继续使用未成功启动的采样结果。DMA 正常运行时,原始值换算电压的关系为 V_in = raw / 4096 * V_ref;示例中的 3.3f 只有在 ADC 参考电压确为 3.3 V 时才成立。若 VDDA 与 3.3 V 存在偏差,显示值也会按相同比例产生误差。