ADC单通道连续采样

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-08-29 08:09:36 Views: 0 Likes: 0 Comments: 0

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 1IN2 -> Rank 2IN3 -> Rank 3IN4 -> 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.ScanConvModeContinuousConvModeDiscontinuousConvModeNbrOfDiscConversionNbrOfConversion 是否与配置一致。
  • 检查四次 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 时,浮点格式化默认可能被裁剪。若 printfsprintf 不能正确显示 %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 批量处理。