ADC多通道查询采集
147.6.4 ADC多通道查询采集
/* 10ADC_3/Core/Src/main.c 的主循环示例;CubeMX 生成的初始化函数略去。 */
#include "main.h"
#include "adc.h"
#include "usart.h"
#include "gpio.h"
uint32_t ADC_Num;
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART1_UART_Init();
HAL_ADCEx_Calibration_Start(&hadc1); // ADC 初始化后、首次 Start 前校准
while (1)
{
HAL_ADC_Start(&hadc1); // 第 1 次触发:Rank1 / PA0
if (HAL_ADC_PollForConversion(&hadc1, 2000) == HAL_OK)
{
ADC_Num = HAL_ADC_GetValue(&hadc1);
float ADC_Value = 3.3 * ADC_Num / 4096;
printf("PA0 ADC_Value:%1.2fv ADC Num: %lu\n", ADC_Value, ADC_Num);
}
else
{
printf("PA0 ADC Error\n");
}
HAL_ADC_Start(&hadc1); // 第 2 次触发:Rank2 / PA1
if (HAL_ADC_PollForConversion(&hadc1, 2000) == HAL_OK)
{
ADC_Num = HAL_ADC_GetValue(&hadc1);
float ADC_Value = 3.3 * ADC_Num / 4096;
printf("PA1 ADC_Value:%1.2fv ADC Num: %lu\n", ADC_Value, ADC_Num);
}
else
{
printf("PA1 ADC Error\n");
}
HAL_ADC_Start(&hadc1); // 第 3 次触发:Rank3 / PA2
if (HAL_ADC_PollForConversion(&hadc1, 2000) == HAL_OK)
{
ADC_Num = HAL_ADC_GetValue(&hadc1);
float ADC_Value = 3.3 * ADC_Num / 4096;
printf("PA2 ADC_Value:%1.2fv ADC Num: %lu\n", ADC_Value, ADC_Num);
}
else
{
printf("PA2 ADC Error\n");
}
}
}
一、校准功能
- STM32F1 的 ADC 内置自校准功能,用于减小 ADC 内部模拟电路(包括采样电容等)造成的零点/增益误差。校准不是把外部传感器的电压“校准成 3.3 V”,也不会替代电压基准的测量。
- 调用位置必须在
MX_ADC1_Init()完成之后、第一次HAL_ADC_Start()之前。每次上电或复位重新初始化 ADC 后执行一次即可;不能在 ADC 正在转换时插入校准。 - 视频中把校准语句从上一节工程复制到约第 94 行、
adc start前面。若从连续采样工程复制,还要删除不适合当前查询模式的启动代码(例如HAL_ADC_Start_DMA),只保留本课需要的HAL_ADCEx_Calibration_Start。 - 校准同样适用于单通道、多通道、连续和间断配置;区别只在于校准前后的启动方式,不在于通道数量。
1.1 ADC校准
- 数据手册将校准过程放在 ADC 章节(视频提到的 11.4),ADC 控制寄存器
ADC_CR2的CAL位为 bit 2,置 1 会启动自校准;RSTCAL位为 bit 3,用于先复位校准寄存器。 - HAL 的
HAL_ADCEx_Calibration_Start()并不是简单地写一位:它会停止/关闭 ADC,重新使能并等待至少两个 ADC 时钟周期,置位RSTCAL并等待硬件清零,再置位CAL并等待校准结束。通过 IDE 跳转到函数实现,可以把 API 调用和寄存器动作对应起来。 - 校准完成前不要读取 ADC 结果;若 HAL 返回
HAL_ERROR或超时,应进入错误处理。示例代码省略了返回值判断,正式程序建议检查返回值。 - 查看寄存器时要区分“寄存器名”和“位宏”:
ADC_CR2是寄存器,ADC_CR2_CAL是其中 bit 2 的宏,宏最终展开为对该位的置位操作。
二、ADC多通道检测
本课的“循环模式”指主循环反复执行三次查询采样,不是 DMA 的 Circular 模式。每次只触发一个转换,CPU 等待完成后读取结果,再触发下一次。
2.1 新建工程
- 视频在 KIDE/CubeIDE 中新建 STM32F103ZET6(ZET6)工程,示例工程目录为
CubeIDE/10ADC_3,工程名为10ADC_3。 - 实验使用 ADC1 的三个规则组通道:ADC1_IN0、ADC1_IN1、ADC1_IN2,对应 PA0、PA1、PA2。STM32F103 的具体封装和芯片型号会影响可用引脚,不能只凭“通道号”猜引脚,必须查该型号的数据手册或 CubeMX 引脚图。
- 视频的接线意图是:PA0 接光敏传感器的 AO 模拟输出,PA1 接板上 3V3,PA2 接板上 GND。PA1 和 PA2 被配置为模拟输入时,输入端呈高阻状态,可以测量固定电位;不要在它们仍配置为推挽输出时直接接电源或地,否则可能造成短路。
- 这三个输入必须与单片机共地,模拟电压应处于 0~VDDA 范围内(工程假定 VDDA=3.3 V)。ADC 测的是引脚相对 GND 的电压,不是“检测电源是否存在”;超过 VDDA 或低于 VSSA 的输入可能损坏器件并产生无效结果。
2.2 常规操作
- 先在 RCC 中启用外部高速时钟(HSE),在 SYS 中启用调试/系统功能,再配置 ADC1、USART1 和 GPIO。外设时钟必须先存在,后续初始化才有意义。
- 在 ADC1 中使能 IN0、IN1、IN2,并把 PA0、PA1、PA2 设为
GPIO_MODE_ANALOG。模拟模式会关闭数字输入路径,降低漏电和数字噪声。 - ADC 通道号与 GPIO 引脚的对应关系随 MCU 型号和封装而定。本工程的对应关系来自生成的
adc.c:ADC_CHANNEL_0 -> PA0、ADC_CHANNEL_1 -> PA1、ADC_CHANNEL_2 -> PA2;学习重点是会查表验证,而不是死记一张通用表。
2.3 回到工程
- 先添加 ADC1,再修改 ADC 时钟分频。工程系统时钟为 72 MHz,APB2 为 72 MHz,ADC 选择 APB2/6,得到 12 MHz(
RCC_ADCPCLK2_DIV6)。先使能 ADC 后再改分频,是为了让 CubeMX 根据已启用外设给出合法配置;若先改时钟导致界面报错,应重新检查外设启用状态。 - Code Generator 选择生成
.c/.h文件后,CubeMX 会生成adc.c、usart.c等初始化代码,但不会替用户写三次HAL_ADC_Start和数据处理逻辑。 Regular Conversion下的Number of Conversion表示整个规则序列的通道总数。本工程设置为 3,表示一个完整序列包含三个 Rank;它不是“每次 Start 转换几个”的参数。- 当规则序列多于一路时,CubeMX 会启用
ScanConvMode。这只是让 ADC 按 Rank 依次扫描多个通道;扫描本身不决定触发次数。 Rank是序列位置:Rank1 先转换、Rank2 次之、Rank3 最后。工程分别设置为 Channel 0、Channel 1、Channel 2,因此一次完整序列的顺序为 PA0 → PA1 → PA2。- 采样时间默认为 1.5 周期,视频建议改大或选中间值。采样时间越长,采样保持电容越充分地跟随输入,适合传感器等高源阻抗信号,但单次转换时间也会增加;“最小采样时间有时出问题”是经验现象,不能代替对源阻抗和时序的分析。
- 工程最终选择
ContinuousConvMode = DISABLE、DiscontinuousConvMode = ENABLE,并设置NbrOfDiscConversion = 1。这里的两个数量必须分清:规则序列总数NbrOfConversion=3,每次触发实际转换的数量NbrOfDiscConversion=1。
2.4 之前工程代码复制
- 上一节单通道查询代码可以复制作为串口和查询流程模板,但必须同步修改通道标签、Rank 配置和循环逻辑。只复制代码并不能自动让一个
HAL_ADC_Start读出三路数据。 - 在间断模式下,每次软件触发只转换一个 Rank;下一次
HAL_ADC_Start继续序列中的下一个 Rank,完成 Rank3 后再从 Rank1 循环。这个行为由NbrOfDiscConversion=1和硬件规则序列共同决定。 - 不要把
Number of Conversion改成 1 来表示“每次采一个通道”。正确配置是:规则序列总数保持 3,间断转换数设为 1。若把规则序列总数也改成 1,ADC 就只剩一个规则通道,无法得到三路扫描。 - 视频提到的 P10、P11、P12 是字幕识别错误;本工程实际使用 PA0、PA1、PA2。通道顺序必须与
adc.c中的 Rank 配置一致。 - 复制工程时要一并检查 USART1 初始化和
printf重定向定义。只复制main.c而没有串口配置,会出现未定义huart1、未声明printf或链接错误;打开多个工程时关闭无关编辑页可以减少复制错文件的风险。
2.5 思维导图
- 思维导图中串口部分提供的是代码放置位置和重定向思路,不是唯一实现。常见做法是在
syscalls.c或用户代码区实现_write(),调用HAL_UART_Transmit()将printf输出到 USART1。 - 添加变量定义时,应放在文件作用域或函数合适的用户代码区。
uint32_t ADC_Num;是当前工程的全局变量,用来接收HAL_ADC_GetValue()返回的 12 位无符号结果;变量类型大于 12 位不会提高 ADC 分辨率,只是方便与 HAL 返回值匹配。 - 思维导图中显示的“右对齐”对应
ADC_DATAALIGN_RIGHT:12 位结果放在 16 位数据的低位,读取后可直接按无符号整数使用。
2.6 编译问题解决
- 编译报错应先看完整的文件名、行号和错误类别,再修改对应文件。视频中的 “ADC number 没有定义” 应规范理解为
ADC_Num未声明,而不是一个叫 “a dc number” 的特殊 API。 - 定义方式可以是
uint32_t ADC_Num;,但要保证使用处可见,并且格式串匹配:uint32_t在该工程中用%lu输出(工程 ABI 下对应 unsigned long)。若改成uint16_t,应改用与编译器匹配的格式或显式转换,避免格式串类型不匹配。 - 每次只改一个问题后重新编译,确认没有新的警告;编译通过只说明语法和链接通过,还需要烧写后结合串口和已知电压验证功能。
- 烧写前确认当前选择的是
10ADC_3工程,关闭旧工程的串口窗口和编辑页,避免把旧固件、旧串口配置和新源码混在一起。
2.7 打开串口
- 工程 USART1 配置为 115200、8 数据位、无校验、1 停止位,PA9 为 TX、PA10 为 RX。USB-UART 模块要与 PA9/PA10 交叉连接并共地:模块 RX 接 PA9,模块 TX 接 PA10。
%f小数不显示通常是 newlib-nano 没有链接浮点 printf 支持。视频中通过 Project Properties 的 MCU/MCU GCC Linker 相关设置启用浮点格式;不同 CubeIDE 版本名称可能略有差异,GCC 工程常见等效选项为-u _printf_float。该选项会增加 Flash 占用。- PA0 接传感器 AO 时,电压会随光照变化;PA1 直接接 3.3 V 理论上应接近满量程;PA2 接 GND 理论上应接近 0 V。实际会受到 VDDA 误差、传感器输出范围、接线压降和噪声影响,不能要求每次都精确显示 3.300 V 或 0.000 V。
- 电压换算采用
V = VDDA × ADC_Num / 4096,其中 12 位原始码范围为 0~4095,工程代码假定 VDDA=3.3 V。若要将码 4095 映射到 VDDA,可统一改用/4095.0f;两种口径都可以,但必须在同一项目内保持一致。
2.8 代码规范性改进
- 视频先复制一次单通道代码,观察到输出仍按 PA0、PA1、PA2 的顺序变化;随后把采样和打印代码复制三份,分别改成 PA0、PA1、PA2 标签。这种写法直观,适合第一次理解 Rank 和间断触发的关系。
- 三份代码的共同流程是:
HAL_ADC_Start软件触发 →HAL_ADC_PollForConversion阻塞等待 →HAL_ADC_GetValue读取 DR → 换算并打印。PollForConversion的超时参数为 2000 ms,远大于本工程一次 239.5 周期采样所需时间,主要用于防止异常时永久阻塞。 - 更易维护的写法是用数组保存通道标签并用
for循环复用流程,但必须确保 ADC 的 Rank 顺序与数组下标一致。课程保留三份代码,是为了让初学者先看到每次 Start 对应一个 Rank。 - 每个分支都应检查
HAL_ADC_Start和HAL_ADC_PollForConversion的返回值。当前源码只检查了 Poll 的返回值,且错误分支会打印PAx ADC Error;正式代码还应处理 Start 失败并记录 HAL 状态。
2.9 多通道现象解释
NbrOfConversion=3表示规则组中有三个通道;NbrOfDiscConversion=1表示一次触发只完成一个通道。二者共同产生“每个 Start 采一个、三次 Start 轮完一组”的现象。- 第一次 Start 转换 Rank1/PA0,第二次 Start 转换 Rank2/PA1,第三次 Start 转换 Rank3/PA2;下一个 Start 又回到 Rank1。
HAL_ADC_GetValue读取的是当前完成转换写入的 ADC DR。 - PA1 接 3.3 V、PA2 接 GND 时,串口输出应分别接近 3.3 V 和 0 V;PA0 传感器输出则随光照变化。手触摸悬空模拟输入会引入人体耦合噪声,读数变化不代表传感器一定发生了对应的物理变化。
- 查询方式是 CPU 主动等待,三路采样之间还夹有浮点换算和串口发送时间,因此三路并非同一时刻采集的“同步快照”。需要同步采样时应使用同一触发下的规则扫描,再用 DMA 或结束中断一次性取走结果。
2.10 代码分析
- 初始化中的关键字段为:
ScanConvMode=ADC_SCAN_ENABLE、ContinuousConvMode=DISABLE、DiscontinuousConvMode=ENABLE、NbrOfDiscConversion=1、NbrOfConversion=3,以及 Rank1~3 对应 Channel 0~2。 - 代码中三次
HAL_ADC_Start并不是“打开三个 ADC 外设”;工程只有一个 ADC1,三次调用是对同一个规则序列进行三次软件触发。视频中“打开三个 adc”的说法应改为“触发同一个 ADC1 的三个 Rank”。 - 每次
HAL_ADC_PollForConversion成功后,HAL_ADC_GetValue返回本次转换结果;失败时不能继续把旧的ADC_Num当成新数据打印。 - 本节目标是掌握多通道、Rank、间断转换和查询读取的基本关系。下一节进入 ADC 中断模式时,核心变化是由中断通知转换完成,CPU 不再主动轮询等待;通道序列和 Rank 的概念仍然相同。
三、电路与安全基础
- PA1 接 3V3:3V3 是开发板的 3.3 V 电源轨,PA1 配成模拟输入后只测量电压,理论值接近 VDDA。接线时使用一根杜邦线把 PA1 与 3V3 排针相连,不能把 PA1 配成输出高/低电平后再硬接电源。
- PA2 接 GND:GND 是电路的参考零电位,ADC 的输入电压就是 PA2 相对 GND 的电压。PA2 接 GND 后,理想码为 0;必须与传感器和 USB-UART 共地,否则各设备的“0 V”不在同一参考点,读数会漂移。
- PA0 接 AO:光敏传感器的 AO 是模拟输出,可能由传感器内部分压或运算放大器产生。确认 AO 的最大电压不超过 3.3 V;若传感器由 5 V 供电但 AO 可达到 5 V,必须先用分压或电平转换,不能直接接 PA0。
- ADC 输入不是电源输出端,不能从 PA0/PA1/PA2 给传感器供电,也不能把两个不同电压源直接短接。测量前先断电检查跳线,避免把 3V3 与 GND 误连。
四、查询模式的边界与扩展
| 方式 | 转换完成的处理 | CPU 占用 | 适合场景 |
|---|---|---|---|
| 查询(本课) | PollForConversion 阻塞等待后读取 DR | 高 | 低速、通道少、需要直观验证 |
| 中断 | 转换完成触发 ISR/回调 | 中等 | 需要及时响应、数据量中等 |
| DMA | 硬件自动搬运,批次完成再通知 | 低 | 连续高速采样、大批量数据 |
- 查询模式中的串口打印本身可能远慢于 ADC 转换,因此它适合教学验证,不适合直接作为高速采样方案。若要固定采样率,应减少打印、改用定时器触发,或把结果批量交给 DMA。
- ADC 原始值通常不必保存为
float;可以先保存整数码,显示或控制时再换算电压,减少浮点运算和printf浮点链接的开销。 - 对传感器输出阻抗较高的情况,应根据数据手册选择采样时间,并在 PA0 前增加缓冲运放或 RC 滤波,而不是只依赖“最大采样时间”。滤波电容要保证不超过 ADC 输入允许的电压和外部驱动能力。