RTT_ADC

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

270.3.6RTT_ADC

#include <rtthread.h>                                      // 引入 RT-Thread 内核接口,本例使用错误码、整数类型、打印函数和 MSH 导出宏。
#include <rtdevice.h>                                      // 引入 RT-Thread 设备接口,本例使用 ADC 设备类型及操作函数。

#define ADC_DEV_NAME        "adc1"                        // 指定要查找的 ADC 设备注册名为 adc1。
#define ADC_DEV_CHANNEL     5                             // 指定读取 ADC1 的通道 5;在 STM32F103ZE 上对应 PA5。
#define REFER_VOLTAGE       330                           // 用“百分之一伏”为单位表示 3.30 V,以便只用整数计算。
#define CONVERT_BITS        (1 << 12)                     // 12 位 ADC 共有 2^12=4096 个量化区间。

static int adc_vol_sample(int argc, char *argv[])          // 定义 MSH 命令函数;本例没有使用 argc 和 argv。
{                                                         // adc_vol_sample 函数体开始。
    rt_adc_device_t adc_dev;                              // 定义 ADC 设备句柄,用于保存查找到的 adc1 设备。
    rt_uint32_t value, vol;                               // value 保存原始转换码,vol 保存以 0.01 V 为单位的电压整数。
    rt_err_t ret = RT_EOK;                                // 定义返回状态并先设为成功。

    adc_dev = (rt_adc_device_t)rt_device_find(ADC_DEV_NAME); // 按注册名查找设备,并把通用设备指针转换为 ADC 设备句柄。
    if (adc_dev == RT_NULL)                               // 判断系统中是否没有注册名为 adc1 的设备。
    {                                                     // 设备查找失败分支开始。
        rt_kprintf("adc sample run failed! can't find %s device!\n", ADC_DEV_NAME); // 打印找不到目标 ADC 设备的错误信息。
        return RT_ERROR;                                  // 立即返回错误,不再访问空设备句柄。
    }                                                     // 设备查找失败分支结束。

    ret = rt_adc_enable(adc_dev, ADC_DEV_CHANNEL);         // 使能 adc1;本 STM32 BSP 实际使能的是整个 ADC1 外设。

    value = rt_adc_read(adc_dev, ADC_DEV_CHANNEL);         // 对 ADC1 通道 5 启动一次转换并取得 0~4095 的原始值。
    rt_kprintf("the value is :%d \n", value);            // 在控制台打印本次 ADC 原始转换码。

    vol = value * REFER_VOLTAGE / CONVERT_BITS;            // 按 value×3.30/4096 换算,并用整数保留到 0.01 V。
    rt_kprintf("the voltage is :%d.%02d \n", vol / 100, vol % 100); // 分别打印整数部分和两位小数部分。

    ret = rt_adc_disable(adc_dev, ADC_DEV_CHANNEL);        // 采样结束后关闭 ADC1;返回值会覆盖前面的使能结果。

    return ret;                                           // 将关闭 ADC 时得到的状态返回给 MSH。
}                                                         // adc_vol_sample 函数体结束。
MSH_CMD_EXPORT(adc_vol_sample, adc voltage convert sample); // 导出可在控制台执行的 adc_vol_sample 命令。

本节先在 13IO_DEVICE 上操作,因 RT-Thread Studio 没有显示 CubeMX 生成的文件而临时重建了 14IO_DEVICE2;课程归档中的最终实验代码位于 14IO_DEVICE2/applications/adc_test.c。上方代码保持配套源码的有效语句、数值、函数名和字符串原样,只移除旧注释并补充逐行说明。旧注释称命令为 adc_sample,但 MSH_CMD_EXPORT 实际导出的命令是 adc_vol_sample。

一、ADC设备使用

1.1 ADC简介

ADC(Analog-to-Digital Converter,模数转换器)把连续变化的模拟电压转换为 CPU 可以处理的离散数字码。串口负责按通信协议收发数字数据,PIN 设备负责读写数字高低电平,而 ADC 关注的是“输入电压在参考范围内处于什么位置”。

一个直观的转换过程如下:

  1. 采样:在某一时刻读取 PA5 上的输入电压。
  2. 保持:内部采样保持电容短暂保存该电压,使后续转换期间输入近似不变。
  3. 量化:把参考电压范围切分为有限个区间,判断输入落在哪个区间。
  4. 编码:把区间编号输出为二进制数,本例最终得到 0~4095。

几个概念不能混为一谈:

概念含义本实验中的对应值
分辨率可区分的量化等级数量12 位,即 4096 个区间
理想量化步距一个最低有效位代表的电压假设参考电压为 3.3 V,则约为 3.3/4096≈0.806 mV
精度实际值与理想值的接近程度还受参考电压、偏置/增益误差、非线性、噪声、布线等影响
转换速率单位时间可以完成多少次转换由 ADC 时钟、采样周期、转换周期和软件调用开销共同决定

因此,“12 位”只说明数字分辨率,并不保证测量能精确到 0.806 mV。课程代码还只打印两位小数,显示步进为 0.01 V,远粗于 ADC 的理想 LSB。

1.2 模拟信号与数字信号转换

本节从应用到硬件共有四层配置,缺少任何一层都可能出现“能编译但找不到设备”或“采样不正确”:

层级本节设置作用
RT-Thread 设备框架在 RT-Thread Settings 中启用 ADC,生成 RT_USING_ADC编译 ADC 类型、API 和设备框架;未启用时会出现 rt_adc_device_t 未知等编译错误
STM32 BSP 实例在 drivers/board.h 定义 BSP_USING_ADC1编译 ADC1 的板级驱动对象,并使其注册为 adc1
HAL 与引脚硬件保证 HAL_ADC_MODULE_ENABLED 有效,并在 board.c 提供 HAL_ADC_MspInit()编译 HAL ADC 模块,打开 ADC1/GPIOA 时钟并把 PA5 配为模拟输入
时钟CubeMX 中将 ADC 时钟设为 PCLK2 六分频本工程 PCLK2 为 72 MHz,因此 ADC 时钟为 12 MHz,不超过 STM32F103 的 14 MHz 上限

课程中的完整操作顺序是:

  1. 在 applications 下新建 adc_test.c,粘贴官方 ADC 示例。
  2. 首次编译出现 unknown type name 'rt_adc_device_t' 时,打开 RT-Thread Settings,搜索 ADC,启用 ADC device driver,按 Ctrl+S 保存并等待配置刷新。
  3. 在 drivers/board.h 的 ADC 配置区启用 BSP_USING_ADC1,因为本例使用 ADC1 通道 5。
  4. 在 CubeMX 中选择 ADC1_IN5,它会占用 PA5;把 ADC 时钟预分频改为 6,并确认系统时钟恢复为 72 MHz。
  5. 生成代码后,从 CubeMX 的 cubemx/Src/adc.c 取得 HAL_ADC_MspInit(),复制到 drivers/board.c 的文件作用域,不能粘进 rt_hw_board_init() 内部。
  6. 确认活动的 cubemx/Inc/stm32f1xx_hal_conf.h 已定义 HAL_ADC_MODULE_ENABLED。
  7. 重新编译、下载、复位,在 MSH 中执行 adc_vol_sample。

CubeMX 界面中无需把普通转换模式、采样时间等参数当作本实验的最终运行配置。cubemx/Src/adc.c 在本工程中被排除出构建;真正的 drivers/drv_adc.c 使用 BSP 的 ADC1_CONFIG 初始化 ADC,并在每次读取通道 5 时将 STM32F1 的采样时间设为 55.5 个 ADC 周期。CubeMX 在这里主要帮助生成时钟、引脚和 MSP 初始化代码。

1.3 问题答疑

  • 为什么重新建了 14IO_DEVICE2? 视频中 adc.c 已经存在于磁盘,但一度没有出现在工程树中;刷新也没有解决,所以老师临时重建工程继续演示。这是 IDE 显示/过滤问题,不是使用 ADC 必须新建工程。
  • 带斜杠或灰色显示是什么意思? 表示该资源被排除出当前构建。课程归档的 .cproject 明确排除了 cubemx/Src/adc.c、gpio.c、usart.c 等文件,避免 CubeMX 初始化与 RT-Thread BSP 重复编译。
  • 为什么生成 adc.c 却不编译? 它提供 CubeMX 对硬件配置的参考代码。本节只取其中的 MSP 硬件初始化;ADC 运行参数由 RT-Thread 的 STM32 驱动管理。
  • 为什么启用 RT-Thread ADC 后仍提示找不到 adc1? RT_USING_ADC 只编译通用框架;还必须定义 BSP_USING_ADC1,使 BSP 创建并注册具体设备。
  • 为什么浮空 PA5 也会读到 2864、约 2.30 V? 浮空模拟输入没有确定电位,会受采样电容残留、电路泄漏和环境噪声影响。这个数字没有可靠的物理意义,不能用来判断 ADC 是否准确。
  • 能否直接给 PA5 接 5 V? 不能。本实验按 3.3 V 参考范围工作,模拟输入应保持在允许的 VSSA~VREF+ 范围内。课程用同一开发板的 3.3 V 接 PA5;若使用外部信号源,还必须与开发板共地,并先确认电压不越界。
  • 传感器输出电压合法就一定能测准吗? 不一定。ADC 采样瞬间要给内部采样保持电容充电;信号源阻抗过高时,电容可能来不及充到输入电压。本驱动使用 55.5 个 ADC 周期的采样时间,ST 数据手册在 14 MHz ADC 时钟下给出的相应外部输入阻抗上限为 50 kΩ。课程直接连接板载 3.3 V,信号源阻抗很低;接高阻传感器时则要查手册,并视情况延长采样时间或加运放缓冲。

二、ADC配置

2.1 构建配置

CubeMX 生成的源文件与“是否参与 RT-Thread 工程构建”是两件事。课程归档的构建清单只编译 cubemx/Src/main.c 和 stm32f1xx_hal_msp.c,而 cubemx/Src/adc.c 被排除;所以灰色或带斜杠的 adc.c 不是“私有文件”,也不是损坏文件,只是当前构建不会把它编译成目标文件。

这种安排避免两套初始化路径冲突:

  • CubeMX 的 MX_ADC1_Init() 不执行;ADC 模式由 drivers/include/config/adc_config.h 中的 ADC1_CONFIG 提供。
  • RT-Thread 自动初始化 drivers/drv_adc.c 中的 stm32_adc_init(),后者调用 HAL_ADC_Init() 并把设备注册为 adc1。
  • HAL_ADC_Init() 会进入复制到 board.c 的 HAL_ADC_MspInit(),从而完成 ADC1 时钟、GPIOA 时钟和 PA5 模拟模式配置。

2.2 自动构建

Eclipse/RT-Thread Studio 的 Build Automatically 影响工作区变更后的自动构建与界面刷新,不负责生成 ADC 业务代码。ADC 相关文件由 CubeMX 生成,是否编译由工程的 source exclusion 配置决定。视频后半段重新显示出 adc.c,只能说明工程树的显示状态恢复,不能据此推断该文件已经加入编译。

排查类似问题时,应依次确认:文件是否真实存在 → 工程树是否启用了过滤器 → 文件是否被排除出构建 → 实际构建清单中是否出现对应 .c。仅根据图标颜色判断容易误解。

2.3 adc.c文件

本工程里与 ADC 相关的几个文件职责不同:

文件是否参与本实验构建职责
applications/adc_test.c是导出命令并完成一次采样、换算和打印
drivers/drv_adc.c是STM32 BSP 驱动,配置通道、启动转换、轮询完成并注册 adc1
rt-thread/components/drivers/misc/adc.c是RT-Thread 通用 ADC API 与设备框架
cubemx/Src/adc.c否CubeMX 生成的参考初始化代码,提供需复制的 MSP 函数
2.3.1 adc.c代码

课程从 CubeMX 生成的 adc.c 中复制的是 HAL_ADC_MspInit(),而不是整个 MX_ADC1_Init()。该函数在传入句柄属于 ADC1 时完成三件事:

  1. 打开 ADC1 外设时钟;没有时钟,ADC 寄存器和转换电路不能工作。
  2. 打开 GPIOA 外设时钟;没有 GPIO 时钟,PA5 无法正确配置。
  3. 将 GPIO_PIN_5 配为 GPIO_MODE_ANALOG;模拟模式断开不需要的数字输入路径,PA5 连接到 ADC1_IN5。

ADC 的扫描、连续转换、数据对齐和软件触发等设置来自 BSP 的 ADC1_CONFIG;通道号和采样时间则在调用 rt_adc_read() 时由 drv_adc.c 配置。因此原稿所说“所有模式、速度都由 adc_test.c 传给内核”并不准确:应用只传设备句柄和通道 5,主要 ADC 模式由 BSP 驱动预设。

2.4 adc_test.c文件

adc_test.c 是应用层示例,不包含硬件时钟和 PA5 模式初始化。它只定义设备名、通道、假定参考电压和分辨率,再依次调用:

rt_device_find() → rt_adc_enable() → rt_adc_read() → 整数电压换算 → rt_adc_disable()。

在本 STM32 BSP 中,rt_adc_read(adc1, 5) 最终会配置 ADC 通道 5、使用软件触发启动一次转换、轮询转换完成并取回结果。rt_device_find() 能同时查找 UART、ADC 等不同设备,是因为这些具体设备都以 struct rt_device 为公共基类注册到同一个 RT-Thread 设备对象容器中;名称由各 BSP 注册时决定,不是 rt_device_find() 猜出来的。

2.5 board.c文件

HAL_ADC_MspInit() 必须放在 board.c 的函数外部。C 语言标准不支持在函数体内再定义普通函数;误粘进 rt_hw_board_init() 会导致编译错误或产生非标准代码。

本节复制 MSP 函数而不复制整个 CubeMX MX_ADC1_Init(),是因为 RT-Thread BSP 已负责 ADC 运行配置,但仍需要板级代码告诉 HAL“ADC1 使用哪个时钟和 GPIO”。这正是 MSP(MCU Support Package)回调的职责。

原稿把修改 HAL 配置宏也归到 board.c,这是文件职责混淆:board.c 放 MSP 函数;BSP_USING_ADC1 在 board.h;HAL_ADC_MODULE_ENABLED 在 stm32f1xx_hal_conf.h。

2.6 stm32f1xx_hal_conf.h文件

CubeMX 生成代码后,本工程实际使用的配置文件是 cubemx/Inc/stm32f1xx_hal_conf.h,其中已定义 HAL_ADC_MODULE_ENABLED 并包含 stm32f1xx_hal_adc.h。原来的 RT-Thread 配置文件被改名为 drivers/stm32f1xx_hal_conf_bak.h,bak 表示备份,不再是活动配置。

所以这里的正确操作不是修改原稿误识别出的陌生宏,而是确认以下事实:编译器包含路径会找到 CubeMX 的 Inc 目录、活动配置中已启用 HAL ADC 模块、旧文件只作为备份保留。课程使用 CubeMX 后这些修改由工具自动完成,通常无需手工重复编辑。

2.7 adc_test.c文件

初次编译的三个错误来自通用 ADC 驱动未启用,而不是 adc_test.c 语法本身。启用 RT-Thread Settings 中的 ADC 后,rtconfig.h 出现 RT_USING_ADC,rtdevice.h 才会包含 ADC 类型与函数声明。

课程代码有两个适合后续工程化时注意的地方,但精校稿不改动原代码:

  • rt_adc_enable() 的返回值写入 ret 后没有立即检查,后面的 rt_adc_disable() 又会覆盖它,因此命令最终只返回关闭操作的结果。
  • argc/argv 没有使用,所以 adc_vol_sample 不接受设备名、通道号或参考电压参数;这些值都由宏固定。

排查时应区分编译错误和运行错误:类型未知通常说明框架宏未开启;能够编译但打印 can't find adc1 device,通常说明 BSP_USING_ADC1、HAL 支持或设备初始化/注册链路不完整。

2.8 main.c文件

工程中确实存在两个 main():

  • applications/main.c 提供正常的强符号 main(),这是 RT-Thread 启动后实际运行的应用入口。
  • cubemx/Src/main.c 的入口被 RT-Thread Studio 生成为 __WEAK int main(void);链接时会被应用的强符号覆盖,所以它内部的 MX_ADC1_Init() 等调用不会执行。

但是不能据此认为整个 CubeMX main.c 都没用。drivers/drv_clk.c 的 clk_init() 明确调用其中的 SystemClock_Config(),所以该文件仍参与编译,其系统时钟和 ADC 六分频配置仍会生效。归档中 SystemClock_Config() 把系统/PCLK2 配为 72 MHz、ADC 时钟设为 PCLK2/6,即 12 MHz。

视频暂时关闭应用 main.c 中的周期打印,只是为了让 ADC 命令输出更清楚,不是 ADC 能否工作的必要条件。

2.9 adc_test.c文件

实验接线与结果
  • 本实验采样脚是 PA5,也就是 ADC1_IN5。使用开发板自己的 3.3 V 测试时,用杜邦线把 3.3 V 与 PA5 相连;二者本来就共用板上地。
  • 若使用外部可调电源或传感器,必须将外部 GND 与开发板 GND 相连,否则 ADC 没有共同的零电位参考。
  • 不要让输入低于地或高于允许的模拟参考范围,更不能因为某些数字 IO 标有 5 V 容忍就向 ADC 输入 5 V。
  • 下载并复位后执行 adc_vol_sample。每执行一次只采样一次,不会持续刷新。

视频把 PA5 接到板载 3.3 V 后读到约 4029~4031。以 4029 为例,课程代码执行的是整数运算:

4029 × 330 ÷ 4096 = 324,随后按 324/100 和 324%100 打印为 3.24 V。

读数没有达到 4095、打印值也不是精确 3.30 V,并不矛盾。代码把参考电压固定假设为 3.30 V,而实际 VREF+、3.3 V 电源、导线压降、ADC 未校准误差和噪声都会影响结果;整数除法还会直接舍去不足 0.01 V 的部分。本课程代码没有显式调用 STM32F1 的 ADC 校准函数,适合演示流程,不应直接当作精密电压表。

代码执行顺序
  1. rt_device_find("adc1") 从统一设备容器取得 ADC1 句柄。drv_adc.c 根据 ADC1 实例构造并注册这个名称。
  2. rt_adc_enable() 经过设备框架调用 STM32 驱动;该版本驱动虽然接收通道参数,但实际使能整个 ADC1。
  3. rt_adc_read(..., 5) 配置通道 5,启动软件转换,轮询完成后返回 12 位原始码。
  4. value × 330 / 4096 把原始码转换成以 0.01 V 为单位的整数,再拆成整数和两位小数打印。
  5. rt_adc_disable() 关闭 ADC1,命令结束。

参考资料

Prev: RTT_PIN Next: RTT_PWM