前置介绍

回顾, 没有 DMA 之前 ADC 采集的流程
  1. 启动 ADC
  2. 等待转换完成
  3. 再转换下一个通道
  4. 等待通道转换完成后
  5. 读取转换数值

在这个过程中, CPU 一直在忙于转换和读取.

而 DMA 可以代替 CPU 的读取过程中

DMA (Direct Memory Access) 直接存储访问, 可以自动将转换后的数据送到 (指定的) 内存数据 /数组 中.

为什么要用 DMA?

非常适合:多通道、连续转换等大量数据场景。

MQ2 烟雾检测模块

MQ135 空气检测模块

ADC+DMA 读取 HAL 库函数

HAL_ADC_Start_DMA():

pData : 转换后的数据存储的位置, 例如数组

Length : 转移 2 个数据, 长度就要设置为 2.

HAL_ADC_ConvCpltCallback():

在转换完成的回调函数中将数值打印出来.

这一步相较于前两节的轮询/中断方式, 唯一的区别就是这里不需要主动读取 ADC 的数值并存储.

即由 DMA 负责完成 ADC 数值的读取和转运存储 (存储到指定的地方 (数组) 中)

项目配置

ADC1

勾选 IN4, IN5.

设置转换通道数为 2 (设置完成后, 上面的 Scan Conversion Mode 扫描模式即自动被使能)

指定对应通道和采样时间.

Sampling Time 采样时间

采样时间越长, 采集到的数值越准确.

这里将其配置为 55.5 Cycles

DMA Settings DMA 设置

点击左下方 "Add" 添加模块, 在最左侧栏选择选择对应的 ADC (ADC1) (即 选择触发 DMA 的外设)

Channel : DMA 的通道

在 STM32F103 中, ADC1 默认使用的就是通道 1, 不需要变.

Direction : 数据搬运的方向.

因为这里是外设触发, 所以默认只能选择外设搬运到内存, 不需要变

Priority : 优先级

默认为低, 还可以选择正常, 高 或者 非常高.

但是现在并没有意义, 保持默认即可.

DMA Request Settings : DMA 的请求设置
Mode : 请求模式
  • Normal : 正常模式, 传输固定长度后, DMA 就停止传输.
  • Circular : 循环模式, 也就是不停地去传输数据.

因为这里需要手动读取 ADC, 这里选择正常模式, 即读取一次, 传输一次.

Increment Address : 地址递增
  • Peripheral : 外设地址递增
  • Memory : 内存地址递增

例如: 在第一个数据传输之后, 是否要传输到这个地址的下一个地址.

在这里, 规划使用数组存储数据, 所以需要勾选 内存地址递增, 因为第一次读取的是 PA4 , 就要把 PA4 的值放在数组的第一个位置, 而第二次读取的是 PA5 , 就要把 PA5 放在数组的第二位.

Data Width : 数据位宽

即分别对应 从外设读取的 数据位宽 和 传输到内存中的 数据位宽.

  • Byte (字节): 8 位
  • Half Word (半字): 16 位 / 32 位
  • Word (全字): 32 位 / 64 位

半字和字有两种大小, 具体的位数取决于处理器架构, 这里均为前者.

我们的 ADC 位数为 12 位, 所以选择 16 位 (Half Word 半字) 即可.

ADC1 - NVIC 配置 (仅查看)

当配置完, 刚刚的 DMA Settings 之后, NVIC 这里就会多出一个 DMA1 Channel1 的全局中断.

其默认为打开状态, 并且不能关闭.

可以在 Sys - NVIC 中设置 DMA1 Channel1 的中断优先级, 这里我就不给出了.

GPIO 配置 引脚名称

代码部分

记得勾选 Use MicroLIB

/* USER CODE BEGIN Includes */

#include <stdio.h>  // 使用 printf 函数
#include <string.h> // 使用 strncmp

/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

uint16_t adc_val[2];  // 存储传感器 ADC 数值
float voltage[2];     // 存储传感器 ADC 电压

/* USER CODE END PV */
  /* USER CODE BEGIN 2 */
  
  // ADC 校准
  HAL_ADCEx_Calibration_Start(&hadc1);

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    // 启动 ADC + DMA, adc_val 是数组(地址), 在 CubeMX 中配置了内存地址递增, 所以这里只需要给这个地址就可以了. 
    HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_val, 2);
    
    // 每 500ms 转换一次. 
    HAL_Delay(500);
    
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
/* USER CODE BEGIN 4 */

/**
  * @brief  ADC + DMA 转换完成回调函数
  * @param  hadc: 指向 ADC 句柄的指针
  * @retval 无
  * @note   当 ADC1 的规则通道转换完成并且 DMA 传输完成后被自动调用, 用于计算电压值并打印显示. 
  */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  // 该回调函数是在 CPU 执行完转换 和 DMA执行完传输之后才会触发的中断. 
  
  // 判断 ADC 来源, 是否为 ADC1
  if (hadc -> Instance == ADC1)
  {
    
    // 将 ADC 数值转换为电压 (假设参考电压为 3.3V, 12位精度)
    voltage[0] = (float)adc_val[0] / 4095 * 3.3f;
    voltage[1] = (float)adc_val[1] / 4095 * 3.3f;
    
    printf("MQ135 空气检测模块(IN4): %.3f V, MQ2 烟雾检测模块(IN5): %.3f V \r\n", 
            voltage[0], voltage[1]);
    
    // 可以看到, 相较于之前非DMA读取的方式, 不再需要先通过 GetValue 来读取 ADC 的数值了, 
    // ADC 的数值已经被存储在对应的内存地址中了(adc_val 数组中)
  }
}

/**
  * @brief  重定向 printf 的输出到串口
  * @param  ch: 要发送的字符
  * @param  f:  文件指针 (标准库要求的参数, 一般不使用)
  * @retval 返回发送的字符
  */
int fputc(int ch, FILE *f)
{
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
  return ch;
}

/* USER CODE END 4 */

硬件连接

可以参考一下我的连接方式, 将单片机 5V 和 GND 连接到上方横排.

然后从上方横排给两个模块接 5V 和 GND, 再用两个单独线将模块的 AO 引脚与 ADC 输入引脚相连接.

程序现象

通过向两个模块吹气, 即可观察到 ADC 测得的模块 AO 输出电压值发生变化.

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐