STM32外设AD-DMA+定时读取模板

发布于:2025-05-17 ⋅ 阅读:(12) ⋅ 点赞:(0)

一,方法引入

轮询法虽然简单,但 CPU 一直在忙着等待,效率太低。为了让 CPU 能在 ADC 转换的同时处理其他任务,我们可以请出强大的帮手——DMA (Direct Memory Access)。

这种方法的思路是:

  1. 配置 ADC 工作在连续转换模式。
  2. 配置 DMA 通道,让它像一个勤劳的"搬运工",在 ADC 每完成一次转换后,自动将结果从 ADC 数据寄存器搬运到内存中的一个指定缓冲区(比如一个数组)。
  3. DMA 通常配置为循环模式 (Circular Mode),这样当缓冲区写满后,它会自动回到缓冲区的开头继续写入,覆盖旧的数据。
  4. 不启用 ADC 或 DMA 的中断来触发数据处理。
  5. CPU 在自己的主循环或一个定时任务中,定期地去读取这个 DMA 缓冲区的内容。
  6. 为了获得更稳定的读数,通常会对缓冲区中的多个样本值求平均。

类比: 就像你在门口放了一个大容量的自动收信篮 (DMA 缓冲区)。邮递员 (ADC) 不停地把信投进去,篮子满了就从头开始覆盖旧信 (循环模式)。你 (CPU) 不需要每次来信都跑去看,而是每隔一段时间(比如每天下午)去信箱里翻看一下最近的一批信件 (读取缓冲区),然后根据这些信件综合判断情况 (求平均)。

这种方式相比纯轮询,大大降低了 CPU 占用,因为等待和数据搬运都由 DMA 在后台完成了。CPU 只需在需要数据时去读取即可。

二,CubeMX配置

使能循环转换和DMA请求
在这里插入图片描述
在DMA设置中,选择循环模式,Word字节
在这里插入图片描述

三,变量声明

uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];

“DMA 自动收信篮”: DMA 持续写入 ADC 转换结果的内存区域。CPU 会定期读取这里的数据。

__IO uint32_t adc_val;

“平均读数”: 用于存储通过对 DMA 缓冲区数据求平均计算得到的 ADC 值。

__IO float voltage;

“平均电压”: 用于存储根据平均 ADC 值计算得到的电压。

优点与缺点: 相比轮询,极大降低了 CPU 占用。实现相对简单,无需处理中断。但缺点是数据处理不是实时的,存在一定的延迟(取决于处理任务的调用周期);直接读取循环缓冲区进行平均,混合了新旧数据,可能不够精确;如果需要精确控制采样和处理的同步,这种方法不够灵活。

四,代码实现 (单通道)

假设我们只关心一个 ADC 通道的连续采样,并希望获得一个平均值。



// --- 全局变量 --- 
#define ADC_DMA_BUFFER_SIZE 32 // DMA缓冲区大小,可以根据需要调整
uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; // DMA 目标缓冲区
__IO uint32_t adc_val;  // 用于存储计算后的平均 ADC 值
__IO float voltage; // 用于存储计算后的电压值

// --- 初始化 (通常在 main 函数或外设初始化函数中调用一次) ---
void adc_dma_init(void)
{
    // 启动 ADC 并使能 DMA 传输
    // hadc1: ADC 句柄
    // (uint32_t*)adc_dma_buffer: DMA 目标缓冲区地址 (HAL库通常需要uint32_t*)
    // ADC_DMA_BUFFER_SIZE: 本次传输的数据量 (缓冲区大小)
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE);
}

// --- 处理任务 (在主循环或定时器回调中定期调用) ---
void adc_task(void)
{
    uint32_t adc_sum = 0;
    
    // 1. 计算 DMA 缓冲区中所有采样值的总和
    //    注意:这里直接读取缓冲区,可能包含不同时刻的采样值
    for(uint16_t i = 0; i < ADC_DMA_BUFFER_SIZE; i++)
    {
        adc_sum += adc_dma_buffer[i];
    }
    
    // 2. 计算平均 ADC 值
    adc_val = adc_sum / ADC_DMA_BUFFER_SIZE; 
    
    // 3. (可选) 将平均数字值转换为实际电压值
    voltage = ((float)adc_val * 3.3f) / 4096.0f; // 假设12位分辨率, 3.3V参考电压

    // 4. 使用计算出的平均值 (adc_val 或 voltage)
    // my_printf(&huart1, "Average ADC: %lu, Voltage: %.2fV\n", adc_val, voltage);
}

逻辑分解:

  1. 定义缓冲区: uint16_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE]; 创建一个数组作为 DMA 的目标地址。大小 ADC_DMA_BUFFER_SIZE 决定了我们一次平均多少个采样点。
  2. 启动 DMA: HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE); 这是关键函数。它会配置 ADC 开始连续转换,并指示 DMA 控制器将每次转换的结果自动存入 adc_dma_buffer。因为 DMA 配置为循环模式,这个过程会一直进行下去,无需再次调用 Start。
  3. 定时处理 (adc_task): 这个函数需要被定期调用。
    3.1 求和: 遍历整个 DMA 缓冲区,将所有值累加到 adc_sum
    3.2 求平均: 用总和除以缓冲区大小,得到平均 ADC 读数 adc_val
    3.3 计算电压: (可选) 根据平均 ADC 值计算出对应的电压。
    3.4 使用结果: 将计算得到的 adc_valvoltage 用于后续逻辑。

思考时间
为何是 (uint32_t*)adc_dma_buffer变量是32位 ,CubeMX配置为Word 传输?
您可能注意到,即使我们的 ADC 缓冲区 adc_dma_buffer 定义为 uint16_t 类型(因为 12 位或 16 位 ADC 的结果适合用 16 位整数存储),在调用 HAL_ADC_Start_DMA 时(形参adc_dma_buffer 是32位,我们却将其强制转换为了 uint32_t*。同时,在 CubeMX 中配置 DMA 时,内存和外设的数据宽度 (Memory/Peripheral Data Width) 通常都设置为 Word (32位)。这是为什么呢?
1,ADC 数据寄存器是 32 位的: 关键在于,STM32 的 ADC 数据寄存器 (DR) 本身通常是一个 32 位宽的寄存器。虽然实际有效的转换结果可能只占低 12 位或 16 位,但硬件层面,它位于一个 32 位的"容器"中。
2,DMA 传输效率与对齐: DMA 控制器按配置的数据宽度进行传输。将外设和内存的数据宽度都设置为 Word (32位),可以简化 DMA 配置,并且通常能更好地利用总线带宽和保证内存对齐,从而提高传输效率。DMA 每次直接从 32 位的 ADC 数据寄存器搬运一个 32 位的数据到内存中的 32 位对齐地址。
3,HAL 函数形参的统一性: HAL 库的 DMA 相关函数为了通用性,其缓冲区指针参数通常设计为 uint32_t* 类型。因此,即使我们的目标是存储 16 位的数据,也需要进行类型转换 (uint32_t*)adc_dma_buffer 来匹配函数形参。
4,数据如何存储? 当 DMA 以 Word 宽度传输时,它会将 ADC 的 32 位数据寄存器内容完整地搬运到 adc_dma_buffer 数组的每个 uint16_t 元素所占的内存空间。由于我们定义的是 uint16_t 数组,实际上 DMA 每次写入会覆盖两个相邻的 uint16_t 元素(因为一个 32 位 Word 包含两个 16 位 Half-Word)。但是,因为 ADC 的有效数据通常在低 16 位,所以我们访问 adc_dma_buffer[i] 时,通常能正确获取到需要的转换结果(存储在内存的低 16 位部分)。
更推荐的做法: 为了避免潜在的混乱和未定义行为(特别是在不同架构或编译器优化下),更清晰和安全的方法是将 DMA 缓冲区也定义为 uint32_t 类型:

uint32_t adc_dma_buffer[ADC_DMA_BUFFER_SIZE];
// ...
HAL_ADC_Start_DMA(&hadc1, adc_dma_buffer, ADC_DMA_BUFFER_SIZE);
// ...
// 处理时,仍然只关心低16位
adc_sum += (uint16_t)adc_dma_buffer[i];

或者,在 CubeMX 中将 DMA 的内存数据宽度 (Memory Data Width) 配置为 Half Word (16位),与外设宽度(如果也配置为 Half Word)或实际数据大小匹配。这样,DMA 每次只传输 16 位,可以与 uint16_t 缓冲区完美对应,也无需类型转换(但需确认 HAL 函数是否仍需 uint32_t* 并相应调整)。不过,将外设和内存宽度都设为 Word 是更常见且通常推荐的配置。

结论: 将 DMA 配置为 Word 传输并使用 (uint32_t*) 转换是为了匹配硬件寄存器宽度、提高效率和满足 HAL 函数签名。虽然直接使用 uint16_t 缓冲区在很多情况下能工作,但定义为 uint32_t 缓冲区或精确配置 DMA 宽度为 Half Word 是更严谨的做法。

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_DMA_BUFFER_SIZE);的实参ADC_DMA_BUFFER_SIZE是否可以换成sizeof(adc_dma_buffer))


网站公告

今日签到

点亮在社区的每一天
去签到