野火STM32_HAL库版课程笔记-ADC多通道采集空气、烟雾传感器(DMA传输)
摘要:本文介绍了使用DMA方式实现ADC多通道数据采集的方法。通过配置ADC1的IN4和IN5通道,结合DMA自动传输功能,实现了对MQ135空气检测模块和MQ2烟雾检测模块的数据采集。相比传统轮询/中断方式,DMA能自动将转换数据存入指定数组,减轻CPU负担。文中详细说明了CubeMX配置步骤,包括ADC采样时间设置、DMA参数配置,并提供了关键代码实现,包括DMA启动函数和转换完成回调函数。最
前置介绍
回顾, 没有 DMA 之前 ADC 采集的流程
- 启动 ADC
- 等待转换完成
- 再转换下一个通道
- 等待通道转换完成后
- 读取转换数值
在这个过程中, 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 输出电压值发生变化.
更多推荐



所有评论(0)