RTT_ADC
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 关注的是“输入电压在参考范围内处于什么位置”。
一个直观的转换过程如下:
- 采样:在某一时刻读取 PA5 上的输入电压。
- 保持:内部采样保持电容短暂保存该电压,使后续转换期间输入近似不变。
- 量化:把参考电压范围切分为有限个区间,判断输入落在哪个区间。
- 编码:把区间编号输出为二进制数,本例最终得到
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 上限 |
课程中的完整操作顺序是:
- 在
applications下新建adc_test.c,粘贴官方 ADC 示例。 - 首次编译出现
unknown type name 'rt_adc_device_t'时,打开 RT-Thread Settings,搜索 ADC,启用 ADC device driver,按Ctrl+S保存并等待配置刷新。 - 在
drivers/board.h的 ADC 配置区启用BSP_USING_ADC1,因为本例使用 ADC1 通道 5。 - 在 CubeMX 中选择
ADC1_IN5,它会占用 PA5;把 ADC 时钟预分频改为 6,并确认系统时钟恢复为 72 MHz。 - 生成代码后,从 CubeMX 的
cubemx/Src/adc.c取得HAL_ADC_MspInit(),复制到drivers/board.c的文件作用域,不能粘进rt_hw_board_init()内部。 - 确认活动的
cubemx/Inc/stm32f1xx_hal_conf.h已定义HAL_ADC_MODULE_ENABLED。 - 重新编译、下载、复位,在 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 时完成三件事:
- 打开 ADC1 外设时钟;没有时钟,ADC 寄存器和转换电路不能工作。
- 打开 GPIOA 外设时钟;没有 GPIO 时钟,PA5 无法正确配置。
- 将
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 校准函数,适合演示流程,不应直接当作精密电压表。
代码执行顺序
rt_device_find("adc1")从统一设备容器取得 ADC1 句柄。drv_adc.c根据 ADC1 实例构造并注册这个名称。rt_adc_enable()经过设备框架调用 STM32 驱动;该版本驱动虽然接收通道参数,但实际使能整个 ADC1。rt_adc_read(..., 5)配置通道 5,启动软件转换,轮询完成后返回 12 位原始码。value × 330 / 4096把原始码转换成以 0.01 V 为单位的整数,再拆成整数和两位小数打印。rt_adc_disable()关闭 ADC1,命令结束。