ADC单通道连续采样
145.6.2 ADC单通道连续采样
/* 以下是视频在 main.c 用户代码区加入的采样片段,ADC1 初始化由 CubeMX 生成。 */
#include <string.h> // 声明 strlen(),用于计算串口实际发送的字符串长度
uint16_t AD_Buf[4]; // 依次保存 Rank 1~4 对应的四路 12 位 ADC 结果
uint8_t i = 0; // 四路采样循环的下标,取值为 0~3
uint8_t Send_Buf[50]; // 保存格式化后的通道号、电压和 ADC 原始值
HAL_ADCEx_Calibration_Start(&hadc1); // ADC1 初始化后、首次转换前执行校准
while (1) // 持续循环采集并通过串口输出四路数据
{
for (i = 0; i < 4; i++) // 间断长度为 1,每轮循环读取下一个 Rank
{
HAL_ADC_Start(&hadc1); // 软件触发一次规则组转换
HAL_ADC_PollForConversion(&hadc1, 10); // 最多等待 10 ms,直到本次转换结束
AD_Buf[i] = HAL_ADC_GetValue(&hadc1); // 读取 DR,并按 Rank 顺序存入数组
sprintf(Send_Buf, "Channel:%d: V:%1.2f Value:%d\n\r", // 格式化通道号、电压和原始值
i + 1, (float)(AD_Buf[i]) * 3.3f / 4096.0, AD_Buf[i]); // 组合显示文本
HAL_UART_Transmit(&huart1, Send_Buf, strlen(Send_Buf), 100); // 阻塞发送本行文本
}
}
一、代码
- 查询模式可分为单通道和多通道。单通道只转换一路模拟输入;多通道按配置的转换顺序依次转换多路输入。
- 本实验实际使用四路规则组通道,采用软件触发、非连续转换、轮询等待转换结束的方式读取数据。
- 掌握多通道的通道配置、Rank 顺序和结果保存后,单通道只需把通道数和循环次数改为 1,流程相同。
- 代码中的
AD_Buf[4]用于按通道顺序保存四个原始 ADC 值,Send_Buf[50]用于存放待发送的串口字符串。
二、查询模式分多通道和单通道
- 新建 STM32F103ZET6 工程,使用 ADC1 的通道 1 到通道 4,对应 PA1、PA2、PA3、PA4。
- 本实验四路输入的连接关系如下:
光敏模块 AO ----------------------> PA1 / ADC1_IN1
3.3 V 已知电压 --------------------> PA2 / ADC1_IN2
GND 已知电压 -----------------------> PA3 / ADC1_IN3
3.3 V 已知电压 --------------------> PA4 / ADC1_IN4
光敏模块 VCC ----------------------> 3.3 V
光敏模块 GND ----------------------> STM32 GND
- PA1 到 PA4 都是“测量端”,配置为模拟输入,不向外输出高低电平。PA2、PA4 接 3.3 V 用于验证满量程读数,PA3 接地用于验证零点,PA1 接光敏模块 AO 用于观察随光照变化的电压。
- ADC 测量的是“输入引脚相对模拟地的电压”。因此光敏模块与 STM32 必须共地;没有公共地线时,AO 电压没有共同参考,结果会不稳定或完全错误。
- ADC 输入电压必须处于
VSSA~VDDA范围内。本例 MCU 和传感器使用 3.3 V 供电,AO 的高电平不能超过 3.3 V;不可把 5 V 模块的 AO 直接接到 ADC 引脚。 - 系统时钟配置为 72 MHz。STM32F1 的 ADC 时钟不能超过 14 MHz,因此将
PCLK2=72 MHz分频为PCLK2/6=12 MHz。
三、思维导图回顾
- ADC1 的通道 1、2、3、4 分别映射到 PA1、PA2、PA3、PA4。使用 CubeMX 选择 ADC 通道后,工具会自动标记对应引脚;使用寄存器或标准库配置时,必须查阅芯片数据手册中的通道映射表。
- 一个 ADC 控制器可以转换多个通道,但转换结果寄存器
DR只有一个。软件必须按已配置的转换顺序及时读取结果并保存,否则后续转换会覆盖前一次结果。 - 本例的规则序列为:Rank 1 转换 IN1,Rank 2 转换 IN2,Rank 3 转换 IN3,Rank 4 转换 IN4。数组下标
0~3与这四个 Rank 一一对应。
四、ADC配置
4.1 扫描模式
- ADC1 采用独立模式,数据右对齐,使用软件触发规则组转换。
- 扫描模式设为 Enable,表示规则组中存在多个 Rank,需要按照 Rank 顺序转换。
- 连续转换模式设为 Disable。每次转换由
HAL_ADC_Start()发起,不会在完成后自动无限循环。 - 间断转换模式设为 Enable,
NbrOfDiscConversion=1。四个 Rank 组成完整规则序列,但每次软件触发只完成其中一个转换;下一次触发继续推进到下一个 Rank,完成 Rank 4 后再次从 Rank 1 开始。 - 看门狗、DMA、外部触发和注入中断不是本实验的必需配置。
4.2 规则转化
- 规则转换组是普通 ADC 采样使用的转换序列。它最多可配置 16 个外部或内部通道,并按照 Rank 从小到大的顺序转换。
- 本例规则组长度为 4,依次配置:
IN1 -> Rank 1、IN2 -> Rank 2、IN3 -> Rank 3、IN4 -> Rank 4。 - 每一次转换完成后,结果写入
DR数据寄存器。轮询方式下应在下一次启动转换前用HAL_ADC_GetValue()读取当前结果。
4.3 注入转化
- 注入转换组是相对规则组而言的另一组转换序列,最多可配置 4 个转换。它可在规则组执行期间插入更高优先级的采样任务。
- 注入组有独立的数据寄存器和触发配置,适合需要优先采样的场景;本实验只使用规则组,不配置注入组。
- 多通道采样通常优先使用规则组配合 Rank。只有存在明确的优先级和时序需求时,才需要使用注入组。
4.4 规则/注入同步模式
- 多 ADC 同步模式用于 ADC1、ADC2、ADC3 协同采样,本实验只使用 ADC1 的独立模式,因此不需要配置双 ADC 或三 ADC 同步。
- 规则组与注入组可以共存,但它们各自使用不同的序列、触发源和结果寄存器。初学阶段先把规则组的通道、Rank 和读取顺序对应起来。
五、ADC配置结束
5.1 扫描模式与持续转换模式配置
- 在 CubeMX 的 ADC 参数页中设置 Scan Conversion Mode 为 Enable,Continuous Conversion Mode 为 Disable,Data Alignment 为 Right。
- 规则组转换数设为 4;分别在 Rank 1 到 Rank 4 选择 Channel 1 到 Channel 4。
- 连续转换使能后,ADC 会在完成序列后自动开始下一序列;本例禁用连续转换,是为了由程序明确控制每次转换时刻。
- 间断模式开启并将转换数设为 1,配合循环中每次调用一次
HAL_ADC_Start(),正好得到四路按 Rank 顺序的结果。
5.2 代码配置与检查
- 保存
.ioc后生成代码,检查hadc1.Init.ScanConvMode、ContinuousConvMode、DiscontinuousConvMode、NbrOfDiscConversion和NbrOfConversion是否与配置一致。 - 检查四次
HAL_ADC_ConfigChannel()调用中的 Channel 和 Rank 是否一一对应;通道排序不正确会导致数组元素对应了错误的物理引脚。 - ADC GPIO 初始化中,PA1 到 PA4 应同时设置为
GPIO_MODE_ANALOG。模拟输入模式会关闭数字输入路径,避免数字电路带来的额外干扰和功耗。
5.3 时钟配置问题与解决
- 选择外部 8 MHz 晶振并经 PLL 倍频后可得到 72 MHz 系统时钟;时钟树中的
PCLK2为 72 MHz。 - ADC 分频必须选择
/6,得到 12 MHz。使用/4会得到 18 MHz,超过 STM32F1 ADC 的允许时钟范围。 - 修改时钟或
.ioc参数后先保存并重新生成代码,再编译下载,避免运行的代码与界面配置不一致。
5.4 外部时钟与分频设置
- HSE 是外部高速时钟源,常见开发板使用 8 MHz 晶振;PLL 将它倍频为系统时钟。
- ADC 使用 APB2 时钟的分频结果,不是直接使用系统时钟。
72 MHz / 6 = 12 MHz满足 ADC 时钟限制。 - 生成工程时启用生成外设
.c/.h文件的选项,便于在adc.c中检查并维护 ADC 初始化代码。
5.5 串口配置与代码生成
- 启用 USART1,用于输出每个通道的电压和原始 ADC 值。串口参数采用常用的 115200 bit/s、8 位数据位、1 位停止位、无校验。
- 保存 CubeMX 配置并生成代码后,应用层在
main.c中调用 ADC 和 UART 的 HAL 接口完成采样与输出。
六、主函数配置
- ADC 初始化函数将 CubeMX 中的配置写入 ADC 寄存器;GPIO 初始化由
HAL_ADC_MspInit()完成,其中 PA1 到 PA4 被配置为模拟输入。 HAL_ADCEx_Calibration_Start(&hadc1)应在 ADC 初始化后、首次转换前调用。校准用于减小 ADC 内部偏差造成的误差。HAL_ADC_Start(&hadc1)通过软件触发开始规则转换。由于连续转换模式禁用,循环中每读取一个通道都要调用一次。HAL_ADC_PollForConversion(&hadc1, 10)等待规则转换结束标志 EOC;10是超时上限,单位为毫秒,实际 ADC 转换时间通常是微秒级。HAL_ADC_GetValue(&hadc1)读取当前一次转换的原始值。没有等到 EOC 就读取,可能获得上次残留值或不完整的采样结果。- 视频中的代码片段直接调用轮询函数,未检查它的返回值;在本课 12 MHz ADC 时钟和 239.5 周期采样配置下,正常转换时间远小于 10 ms,所以演示能够正常工作。这里保留课程写法,只说明超时情况下的实际行为,不把通用容错代码强行补进本课源码。
七、ADC初始化函数讲解
7.1 ADC的MSPInit
HAL_ADC_MspInit()开启 ADC1 与 GPIOA 时钟,并将 PA1、PA2、PA3、PA4 配置为GPIO_MODE_ANALOG。- ADC 模拟输入不使用上拉、下拉和输出速度配置。外部模拟信号从引脚进入 ADC 采样保持电路后再进行量化。
7.1.1 ADC控制寄存器1
- STM32F1 的 ADC 控制寄存器 1(CR1)包含扫描模式、间断模式、看门狗、双 ADC 模式和中断等配置。
- 本例中,扫描模式使能以支持四个规则 Rank;间断规则转换使能且间断长度为 1,使每次软件触发只转换一个 Rank。
- 看门狗、双 ADC 模式和 ADC 中断未用于本实验。
7.1.2 ADC控制寄存器2
- ADC 控制寄存器 2(CR2)包含软件启动、连续转换、数据对齐、DMA、校准和外部触发等控制位。
- 本例数据右对齐,连续转换关闭,使用软件触发;校准函数在首次采样前执行。
HAL_ADC_PollForConversion()轮询的是规则组转换结束标志 EOC。EOC 置位表明当前转换结果已写入DR,此时才能安全读取。
7.1.3 配置通道
7.1.3.1 变量解析
sConfig.Channel指定要转换的 ADC 通道,sConfig.Rank指定它在规则组内的转换次序,sConfig.SamplingTime指定该通道的采样时间。- 本例将 Channel 1 配为 Rank 1,Channel 2 配为 Rank 2,Channel 3 配为 Rank 3,Channel 4 配为 Rank 4。Rank 是“转换顺序”,不是引脚编号。
- 规则组中通道和 Rank 的对应关系必须唯一。将同一通道配置到多个 Rank 会多次采样该通道;跳过 Rank 会导致序列不连续或结果难以对应。
7.1.3.2 规则序列寄存器3
- 规则序列长度由 SQR1 的
L[3:0]字段决定,寄存器中存放的是“转换次数减 1”。四次转换对应L=3。 - SQR3 配置 Rank 1 到 Rank 6,SQR2 配置 Rank 7 到 Rank 12,SQR1 还配置 Rank 13 到 Rank 16。
- 本例只使用四个 Rank,因此通道顺序都配置在 SQR3 中。Rank 1 到 Rank 4 的内容决定每次
HAL_ADC_Start()后接下来要转换哪个通道。
八、思维导图总结
- 四路轮询的关键顺序是:ADC 初始化、校准、启动一次转换、等待 EOC、读取
DR、保存到对应数组元素、格式化并发送串口数据。 - Rank 规定多通道规则组的转换顺序;
DR只有一个,因此读取顺序必须与 Rank 一致。 - 本实验选择非连续转换和间断长度 1,使每次
HAL_ADC_Start()只产生一个可立即读取的结果,便于理解多通道轮询流程。
九、ADC自动校准
- ADC 初始化完成后调用:
HAL_ADCEx_Calibration_Start(&hadc1);
- 校准应在 ADC 时钟和初始化完成之后、首次转换之前执行。校准用于补偿 ADC 内部偏差,不会替代对
VDDA、传感器和外部电路误差的处理。 - 更换 ADC 配置、复位芯片或重新初始化 ADC 后,按工程初始化流程再次校准。
十、配置ADC
10.1 配置各种参数
- 配置 ADC1、PA1 到 PA4、四个规则 Rank、右对齐、软件触发、扫描模式和非连续转换后,生成代码并在
main.c中添加采样逻辑。 NbrOfConversion=4表示完整规则组包含四次转换;NbrOfDiscConversion=1表示每次触发只执行一项转换。- 采样时间可设置为 239.5 个 ADC 周期。对于光敏模块等慢变化、输出阻抗可能较高的信号,较长采样时间能提高读数稳定性。
10.2 变量定义与类型选择
- ADC 为 12 位,原始值范围为
0~4095。使用uint16_t保存单次采样值足够,AD_Buf[4]用于保存四路数据。 - 循环变量
i的范围为0~3,显示通道号时使用i+1;发送缓冲区使用字符数组,容量必须覆盖最长格式化字符串及结尾的\0。 strlen()需要包含<string.h>。sprintf()不检查缓冲区容量,实际工程中更建议使用snprintf()限制写入长度。
10.3 例题1:ADC数据采集与串口发送
- 四路采样循环中的
HAL_ADC_Start()、HAL_ADC_PollForConversion()和HAL_ADC_GetValue()必须保持顺序。 sprintf()将通道号、电压和原始值组成字符串;HAL_UART_Transmit()发送该字符串,strlen(Send_Buf)使发送长度随字符串实际长度变化。- 视频中的
Send_Buf从 30 字节调整为 50 字节后恢复正常输出,说明格式化字符串的长度必须小于缓冲区容量。缓冲区越界会破坏内存并导致程序停止或输出异常。
10.4 编译错误处理与头文件包含
- 使用
strlen()时包含<string.h>;sprintf()的标准声明位于<stdio.h>。本页仅呈现视频添加到用户代码区的片段,完整工程通过其他已包含的工程头文件获得sprintf()声明,不能据此认为<string.h>会声明sprintf()。 - 变量必须在使用前定义,且变量类型、数组长度与格式化参数匹配。
%d对应默认提升后的整型数值,%1.2f对应浮点数。 - 先执行编译检查,确认没有语法错误、未声明函数或未定义变量,再下载到开发板观察串口数据。
十一、编译并烧写程序
- 在 STM32CubeIDE 使用 newlib-nano 时,浮点格式化默认可能被裁剪。若
printf或sprintf不能正确显示%f,在工程属性的 C/C++ Build -> Settings -> MCU GCC Linker -> Miscellaneous -> Other flags 中添加:
-u _printf_float
- 应用设置后重新编译并下载。该选项会增加代码体积,只在确实需要格式化输出浮点数时启用。
十二、配置串口打印浮点数
12.1 代码文件与串口调试助手介绍
- 下载程序后,通过串口调试助手观察四路通道的编号、电压和原始值。串口输出是验证 ADC 通道顺序和接线的直接手段。
- 光敏传感器的电压变化应主要出现在接 AO 的通道,接 3.3 V 和 GND 的通道分别提供满量程、零点参照。
12.2 串口数据采集与问题分析
- 四路都显示接近 3.3 V 时,先检查 PA1 到 PA4 的实际接线、模拟输入模式和规则序列设置;不要只检查字符串格式。
- 正常示例中,光敏通道约为 1.84 V,两个接 3.3 V 的通道接近 3.3 V,接地通道接近 0 V。光敏通道会随环境光改变。
12.3 配置与间断模式设置
- 间断模式启用且
NbrOfDiscConversion=1时,一次HAL_ADC_Start()只转换一个规则 Rank。循环内调用四次HAL_ADC_Start(),依次得到四路结果。 - 若设为 2,则一次触发执行连续的两个 Rank;轮询读取逻辑也必须随之调整,确保每个转换结果都在被下一次覆盖前读出。
- 对本实验,间断长度设为 1 最容易将“第几次启动”与“第几个 Rank”对应起来。
12.4 代码修改与生成
- 修改
.ioc的间断模式或 Rank 配置后,保存并重新生成代码,再检查adc.c中的初始化值是否正确。 - 不要直接依赖界面显示结果;以生成的
hadc1.Init配置和各次HAL_ADC_ConfigChannel()调用为最终依据。
12.5 串口打印问题排查
- 串口只打印一两次后停止时,检查发送缓冲区越界、
sprintf()格式串、串口重定向和供电复位情况。 - 可以先发送固定短字符串验证 UART 正常,再逐步恢复 ADC 数值和浮点格式化输出,定位问题范围。
12.6 缓冲区大小调整
Send_Buf[30]无法可靠容纳包含通道号、浮点电压、原始值和换行符的完整字符串。使用Send_Buf[50]可满足本实验的输出长度。- 更稳妥的写法是使用
snprintf():
snprintf((char *)Send_Buf, sizeof(Send_Buf),
"Channel:%d: V:%1.2f Value:%u\r\n",
i + 1, voltage, AD_Buf[i]);
snprintf()即使输出被截断也不会越过数组边界;仍应根据返回值判断字符串是否完整。
12.7 代码分析与数据解释
Channel:%d显示的是规则序列位置,V:%1.2f是换算电压,Value:%d是 ADC 原始码值。- 电压值是由原始码值计算得到的显示值;用于控制逻辑时,可直接使用原始值做阈值判断,避免不必要的浮点运算。
12.8 ADC值获取与转换
HAL_ADC_GetValue()返回右对齐的 12 位原始码值,范围是0~4095,不是直接的伏特值。- 一个理想量化步长为:
$ V_{LSB}=\frac{VDDA}{4096} $
- 视频中的显示换算使用:
$ V=\frac{ADC_{raw}}{4096}\times3.3\ \text{V} $
- 该式按一个 LSB 换算。若希望
ADC_{raw}=4095显示为精确的VDDA,可使用端点换算V=ADC_{raw}\times VDDA/4095.0f。实际VDDA可能不等于 3.3 V,精度要求较高时应使用实测值。
十三、采样精度和转换时间
13.1 例题1: 采样精度计算与代码分析
- 12 位 ADC 一共有 $2^{12}=4096$ 个量化等级,原始码值从 0 到 4095。以 3.3 V 为参考时,分辨率约为 $3.3/4096\approx0.806\ \text{mV}$。
- 为避免整数除法,应让表达式中至少一个操作数为浮点数,例如
(float)AD_Buf[i] * 3.3f / 4096.0f。 - ADC 转换时间由采样时间与固定 12.5 个 ADC 时钟周期组成:
$ T_{conv}=\frac{N_{sample}+12.5}{f_{ADC}} $
- ADC 时钟为 12 MHz、采样时间为 239.5 周期时,单次 ADC 核心转换时间约为 $21\ \mu s$;这不包含轮询、格式化和串口发送耗时。
13.2 例题2: 轮询与串口打印浮点数据配置
- 轮询模式通过
HAL_ADC_PollForConversion()等待 EOC 标志。正常返回后再调用HAL_ADC_GetValue(),读取的是当前 Rank 的结果。 - 超时参数 10 ms 只是最长等待限制,实际转换通常远小于该值。若超时,应检查 ADC 时钟、触发方式、通道配置和硬件连接。
- 当前课程片段没有根据
HAL_ADC_PollForConversion()的返回值分支处理,因此即使发生超时也会继续读取DR;正常硬件配置下这一点不影响演示,排查异常读数时则必须把它考虑进去。 - 将
HAL_ADC_Start()移到四次读取循环之外,只会启动一次转换;随后连续读取四次DR会得到相同值。下一次启动才推进到下一个 Rank,因此每个 Rank 都需要一次启动、等待和读取。 - 顶部课程片段没有在
while (1)末尾加刷新延时,所以会连续采样并连续发送串口文本。若只是观察慢变化的光照,可在完整四路采样结束后增加适当延时来降低刷新频率;这与HAL_ADC_PollForConversion()等待一次转换完成不是同一种等待。 - 轮询期间 CPU 会一直等待,适合低速、少通道、便于学习的场景。多通道连续采样或更高采样率应使用 DMA,使 ADC 结果自动搬运到 RAM,再由 CPU 批量处理。