RTT_IO设备框架复盘
273.3.9RTT_IO设备框架复盘
#include <rtdevice.h> // 引入 RT-Thread 设备接口,提供 PWM 设备类型和 rt_device_find() 声明。
#define PWM_DEV_NAME "pwm3" // 定义应用要查找的设备名;它必须与驱动注册时使用的名称一致。
struct rt_device_pwm *pwm_dev; // 定义 PWM 设备句柄,用于保存设备管理器返回的对象地址。
pwm_dev = (struct rt_device_pwm *)rt_device_find(PWM_DEV_NAME); // 在 pwm_led_sample() 中按名称查找已注册设备并转换为 PWM 设备指针。
本节没有新建或修改业务功能,而是从上一节的
pwm_test.c追踪设备名与注册流程。上方只摘录视频实际分析的应用层语句,均保持课程源码原样并重新逐行注释;drv_pwm.c、rt_drv_pwm.c、device.c等属于 BSP、设备类框架或内核源码,按要求不复制到文首。完整 PWM 业务代码见第 271 节。
一、设备模型框架复盘
本节围绕两个问题展开:
- 在
board.h中打开BSP_USING_PWM3到底产生了什么作用? - 应用只写字符串
"pwm3",为什么rt_device_find()就能返回 TIM3 对应的 PWM 设备?
先给出结论:RT-Thread 并不会在启动时自动扫描开发板、识别“实际接了什么硬件”。工程配置使相应驱动代码参与编译,驱动初始化函数再创建并注册一个带名字的软件设备对象;应用查找的是这个已注册对象,而不是直接搜索物理引脚或寄存器。
以本工程的 PWM3 通道 4 为例,注册与使用是两条方向相反的路径:
| 注册路径(启动阶段) | 使用路径(应用运行阶段) |
|---|---|
| 配置宏选择 TIM3 与通道 4 | 应用以 "pwm3" 调用 rt_device_find() |
| BSP 创建并初始化 PWM 对象 | 设备管理器按名称返回已注册对象 |
| PWM 类注册接口包装设备能力 | 应用调用 rt_pwm_set()、rt_pwm_enable() |
通用 rt_device_register() 把对象加入设备对象容器 | PWM 类接口经通用 control 接口回调 STM32 驱动 |
因此,“配置”“注册”“查找”“使能输出”是四个不同动作,不能混成一句“打开宏后设备就工作”。
1.1 使用VS Code辅助研究
视频用 VS Code 全局搜索宏和函数,沿引用关系逆向追踪。更准确的操作方式是:
- 用 VS Code 打开完整工程目录,而不是只打开单个
.c文件。 - 按
Ctrl+Shift+F打开全局搜索,复制符号后用Ctrl+V粘贴并回车。 - 对函数或宏使用“转到定义/查看引用”,逐步记录定义者、调用者和编译条件。
- 同时在 RT-Thread Studio 中对照实际工程配置;编辑器的着色只能辅助阅读,不能代替编译器结果。
视频中 #ifdef BSP_USING_PWM3 下的代码在 VS Code 里没有高亮,并不表示构建时一定被排除。通常是 VS Code 的 C/C++ 索引器没有取得工程的包含路径、预定义宏或编译参数。应以 rtconfig.h、board.h、构建日志及预处理结果为准;若希望着色准确,可给 VS Code 配置与工程一致的 includePath、defines 或 compile_commands.json。
1.1.1 查找设备
本节不是“复制宏后按 Ctrl+V 就搜索”,而是先进入全局搜索框,再粘贴符号。推荐从几个唯一性较强的标识开始:
- 搜索
BSP_USING_PWM3,确认谁决定 PWM3 配置是否参与编译; - 搜索
PWM3_CONFIG,确认配置宏在哪里展开为对象; - 搜索
stm32_pwm_init,确认对象何时初始化和注册; - 搜索
rt_device_pwm_register,确认 PWM 类设备怎样接入通用设备管理器; - 搜索
rt_device_find,确认应用最终按什么条件取得对象。
仅搜索字符串 pwm3 容易得到配置、日志、示例和生成文件等大量结果。沿“宏定义 → 宏使用 → 初始化函数 → 注册接口 → 查找接口”的顺序,能把同名字符串之间的关系变成可验证的调用链。
1.1.2 PWM设备模型框架复盘
本项目使用 RT-Thread 5.0.2。与 PWM 相关的关键文件各有不同职责:
| 文件 | 所在层 | 本节关注点 |
|---|---|---|
drivers/board.h | 板级配置 | 选择 PWM3 和通道 4 |
drivers/include/config/pwm_config.h | STM32 PWM 配置 | 把 PWM3 映射为 TIM3 和设备名 pwm3 |
drivers/drv_pwm.c | STM32 BSP 驱动 | 创建对象、初始化 TIM、注册 PWM 设备 |
rt-thread/components/drivers/misc/rt_drv_pwm.c | PWM 设备类 | 提供 PWM 类注册及 rt_pwm_* 接口 |
rt-thread/components/drivers/core/device.c | 通用设备管理 | 注册、注销、查找、初始化、打开、关闭、读写和控制设备 |
applications/pwm_test.c | 应用层 | 按 pwm3 查找对象并设置 PWM |
1.1.2.1 PWM配置文件
实际文件路径是 drivers/include/config/pwm_config.h。当 board.h 定义 BSP_USING_PWM3 时,预处理器才保留 PWM3 对应分支,并定义 PWM3_CONFIG。该宏展开后是一个结构体初始化器,包含三个关键字段:
| 字段 | 本工程的值 | 含义 |
|---|---|---|
tim_handle.Instance | TIM3 | 指向 TIM3 外设寄存器映射的实例指针 |
name | "pwm3" | 注册给 RT-Thread 设备管理器的名字 |
channel | 0 | 初始通道位图;稍后由通道宏补充 |
TIM3 不是“寄存器里的一个数值参数”,而是 STM32 CMSIS/HAL 头文件提供的外设实例地址宏,可理解为指向 TIM3 寄存器块的类型化指针。驱动通过这个指针操作 TIM3 的 PSC、ARR、CCR 等寄存器。
pwm_config.h 里只负责“实例与名字”的静态映射,PB1 复用输出、TIM3 时钟和 HAL TIM 模块是否启用,仍由 board.c 的 MSP 回调和 HAL 配置共同决定。
1.1.2.2 PWM测试文件
applications/pwm_test.c 把 PWM_DEV_NAME 定义为 "pwm3",再调用 rt_device_find(PWM_DEV_NAME)。这里没有把字符串直接转换为 TIM3 地址,也没有因为字符串包含数字 3 就自动推导外设编号。
名称关联来自两端使用同一字符串:驱动注册时把 stm32_pwm_obj[i].name 传入注册接口,该字段来自 PWM3_CONFIG 的 "pwm3";应用查找时也传入 "pwm3"。若任一端改成不同名称,查找就返回 RT_NULL。当前工程的 RT_NAME_MAX 为 8,pwm3 没有超过设备对象名称上限。
视频把文件名说成了 pwm3_test.c;课程归档中的真实文件名是 pwm_test.c。
1.1.2.3 分析PWM宏定义
C 预处理器在编译前执行条件编译和文本替换。这里有两类宏,作用不同:
BSP_USING_PWM3是条件开关:决定PWM3_CONFIG和 PWM3 数组元素是否存在。PWM3_CONFIG是结构体初始化器宏:在数组中被替换为 TIM3、pwm3和初始通道位图。BSP_USING_PWM3_CH4是通道开关:在pwm_get_channel()中把 PWM3 对象的第 3 位设为 1,即形成通道 4 的位图0b1000。
drv_pwm.c 中的 stm32_pwm_obj[] 是 struct stm32_pwm 类型的静态数组。每个元素同时保存:
- 一个通用 PWM 类对象
pwm_device; - 一个 HAL 定时器句柄
tim_handle; - 可用通道位图
channel; - 注册名称
name。
在当前工程只打开 BSP_USING_PWM3 的情况下,数组只包含 PWM3 这一项。静态数组的生命周期贯穿程序运行,注册进设备管理器的是其中嵌入的设备对象地址,不是临时局部变量。
视频提出“驱动代码从下往上看”,这是一种寻找入口的阅读技巧,不是 C 语言的执行顺序。更准确的方法是先找到初始化导出宏或公开 API,再沿调用链向内追踪;当定位到目标函数后,仍按函数实际控制流阅读。
1.1.2.4 分析PWM驱动文件
stm32_pwm_init() 是 STM32 PWM 驱动的注册入口,主要过程如下:
pwm_get_channel()根据BSP_USING_PWMx_CHy宏填充每个对象的通道位图。- 使用
sizeof(stm32_pwm_obj) / sizeof(stm32_pwm_obj[0])得到静态数组元素数。 - 遍历每个元素,调用
stm32_hw_pwm_init()初始化定时器和已选通道。 - 初始化成功后调用
rt_device_pwm_register(),传入 PWM 类对象、设备名、操作函数表和 HAL 定时器句柄。 - 若硬件初始化或注册失败,记录错误并返回失败状态。
sizeof(数组) / sizeof(数组首元素) 只有在数组尚未退化为指针的作用域内才能得到元素数量;把数组作为函数参数传递后再套用这条公式,结果会变成指针大小相关值。
PWM 类注册函数不会重新猜测名字。它设置设备类型、PWM 操作表和 user_data,再调用通用 rt_device_register()。最终对象的嵌套关系可理解为:
| 外层对象 | 内嵌对象 | 提供的信息 |
|---|---|---|
struct stm32_pwm | struct rt_device_pwm | TIM3 HAL 句柄、通道位图、板级驱动能力 |
struct rt_device_pwm | struct rt_device parent | PWM 类操作表以及通用设备接口 |
struct rt_device | struct rt_object parent | 名称、对象类型、对象容器连接等内核对象信息 |
这种“把父结构体作为成员嵌入子结构体”的写法,是 C 语言模拟继承和多态的常用方式。各层通过首成员地址转换和函数指针,把通用调用分派到具体 STM32 驱动。
1.1.2.5 设备管理层
真正把对象加入 RT-Thread 管理体系的是 rt_device_register()。在本工程版本中,它会:
- 检查设备指针是否为空;
- 调用
rt_device_find(name),拒绝重复名称; - 通过
rt_object_init()把对象初始化为RT_Object_Class_Device,同时记录设备名; - 保存读写标志并清零引用计数、打开状态。
因此设备管理器并不是一张额外的硬件扫描表,而是建立在 RT-Thread 内核对象管理器上的“设备类对象容器”。device.c 中的注册、注销、查找、初始化、打开、关闭、读、写和控制接口,提供了面向所有设备的统一入口;PWM、ADC、UART 等设备类再把具体操作接到这些入口上。
启用堆后,device.c 还提供 rt_device_create() 与 rt_device_destroy(),用于动态分配和销毁通用设备控制块。create 只负责分配对象,并不会自动注册;destroy 只适用于已经脱离对象管理器的动态对象。本节 PWM 对象来自静态数组,不走这套动态内存流程。
注销 rt_device_unregister() 的含义是把已经注册的设备对象从管理体系中移除,使其无法再通过名称查到;它本身不等同于关闭硬件,也不负责释放静态驱动对象占用的内存。
1.1.2.6 应用层代码
rt_device_find("pwm3") 内部最终调用 rt_object_find(name, RT_Object_Class_Device),在设备类对象中按名称查找。成功时返回同一个已注册对象的地址,失败时返回 RT_NULL。它不会执行以下动作:
- 不扫描 TIM3 或 PB1;
- 不自动初始化尚未注册的驱动;
- 不打开或使能 PWM 输出;
- 不验证外部舵机、LED 是否真的接线。
应用取得句柄后,rt_pwm_set() 会把通道、周期和脉宽装入 PWM 配置结构,再调用通用 rt_device_control();PWM 类的 control 适配函数继续调用 STM32 驱动的 drv_pwm_control(),后者才根据命令设置 TIM3。rt_pwm_enable() 也沿同一分派链路启动指定通道。
可把一次设置操作概括为:
应用 rt_pwm_set() → PWM 类接口 → rt_device_control() → PWM 类 control 适配 → STM32 drv_pwm_control() → HAL/TIM3
这正是设备模型的价值:应用只依赖稳定的设备类 API,具体寄存器操作封装在 BSP 驱动中。
1.1.2.7 使用方法
几个相近术语需要分开:
| 动作 | 含义 | 本节对应位置 |
|---|---|---|
| 驱动初始化 | 准备 HAL 句柄、定时器及通道 | stm32_hw_pwm_init() |
| 设备注册 | 把软件设备对象加入设备管理器 | rt_device_pwm_register() → rt_device_register() |
| 设备查找 | 按名称取得已注册对象的句柄 | rt_device_find("pwm3") |
| 设备打开 | 执行通用设备的 open 语义并增加引用计数 | rt_device_open();本 PWM 示例没有调用 |
| 设置/使能 | 通过 PWM 类接口配置并启动输出 | rt_pwm_set()、rt_pwm_enable() |
| 设备注销 | 从对象管理器移除注册关系 | rt_device_unregister();本示例没有调用 |
因此,“找到设备”不等于“设备已输出 PWM”,“注册设备”也不等于“外部负载存在”。课程 PWM 类把 init/open 回调设为空,示例直接使用 PWM 类设置和使能接口,这是该设备类的实际使用方式。
RT-Thread 内核知道哪些设备对象应该存在,依据的是开发者选择的配置和被编译进固件的驱动,而不是通用的即插即用探测。对 TIM3 这种 MCU 内部外设,芯片型号决定硬件资源是否存在;对板外传感器,即使软件对象注册成功,也仍可能因为未接线、地址错误或供电异常而通信失败。
1.1.2.8 分析rtconfig.h
本项目形成完整 PWM 链路至少涉及以下开关:
| 配置 | 来源 | 作用 |
|---|---|---|
RT_USING_DEVICE | RT-Thread 基础配置 | 编译通用 I/O 设备框架 |
RT_USING_PWM | RT-Thread Settings | 编译 PWM 类接口及受其保护的 STM32 PWM 驱动代码 |
BSP_USING_PWM3 | drivers/board.h | 把 TIM3 配置对象加入 STM32 PWM 对象数组 |
BSP_USING_PWM3_CH4 | drivers/board.h | 将 PWM3 通道 4 标为可用 |
HAL_TIM_MODULE_ENABLED | HAL 配置头文件 | 编译 STM32 HAL TIM 模块 |
| TIM3/PB1 MSP 配置 | drivers/board.c | 打开时钟并把 PB1 配为 TIM3_CH4 复用输出 |
RT-Thread Settings 保存后生成 rtconfig.h,本工程其中确实有 RT_USING_PWM 和 RT_USING_ADC。不建议手工修改生成的 rtconfig.h,因为下一次保存图形配置时可能被覆盖;内核/组件选项应从 Settings 修改,具体 BSP 实例则按 board.h 注释配置。
drv_pwm.c 外层受 RT_USING_PWM 条件控制;没有它,即使 board.h 定义 PWM3,主体驱动也不会参与编译。反过来,只定义 RT_USING_PWM 而没有 BSP_USING_PWM3,通用框架虽然存在,也不会凭空生成名为 pwm3 的 STM32 实例。
1.1.2.9 判断设备是否注册
stm32_pwm_init() 没有在用户 main() 中显式调用,原因是文件末尾使用了 INIT_DEVICE_EXPORT(stm32_pwm_init)。这个宏本身并不是立即调用函数,而是把函数指针放入具有初始化级别的链接段;系统启动时遍历相应区间,按级别执行函数。
RT-Thread 5.0.2 在本工程中的初始化级别依次为:
INIT_BOARD_EXPORT:非常早的板级初始化,调度器尚未启动;INIT_PREV_EXPORT:前置软件初始化;INIT_DEVICE_EXPORT:设备驱动初始化;INIT_COMPONENT_EXPORT:文件系统、网络栈等组件初始化;INIT_ENV_EXPORT:系统环境初始化;INIT_APP_EXPORT:应用初始化。
课程工程的实际时序是:
rtthread_startup()先调用rt_hw_board_init()。- 板级初始化完成时,
rt_components_board_init()遍历INIT_BOARD_EXPORT项;ADC 的stm32_adc_init()在此阶段注册,因为它使用INIT_BOARD_EXPORT。 - 调度器启动后进入 main 线程,先调用
rt_components_init();PWM 的stm32_pwm_init()作为INIT_DEVICE_EXPORT项在这里执行。 - 组件自动初始化结束后,main 线程才调用用户
main()。
所以 ADC 与 PWM 都能“上电自动初始化”,但它们的执行阶段并不相同。归档中的 ADC 函数真实名称是 stm32_adc_init(),末尾为 INIT_BOARD_EXPORT(stm32_adc_init);原稿中的 rt_hw_adc_init 不是本工程这一处的函数名。
判断 pwm3 是否真正注册,不能只看宏是否定义。可以同时检查:编译配置是否包含驱动、启动日志是否出现初始化/注册错误,以及 MSH 的设备列表中是否存在 pwm3;应用中 rt_device_find("pwm3") 返回非空也是直接证据。
1.1.2.10 串口初始化
串口走的是另一条更早的路径。drivers/drv_usart.c 提供 rt_hw_usart_init(),但文件末尾没有 INIT_DEVICE_EXPORT 或 INIT_BOARD_EXPORT。本工程在 drivers/drv_common.c 的 hw_board_init() 中按 RT_USING_SERIAL 条件直接调用它;PIN 驱动也以相同方式调用 rt_hw_pin_init()。
更完整的启动链为:
复位/启动代码 → RT-Thread entry → rtthread_startup() → rt_hw_board_init() → hw_board_init() → rt_hw_pin_init()/rt_hw_usart_init()
随后 rt_hw_board_init() 才设置控制台设备并调用 rt_components_board_init()。串口必须较早注册,才能让系统控制台和启动日志使用它。视频把原因概括为“PIN 和串口每块板都会用”,并不严格;源码实际仍受 RT_USING_PIN、RT_USING_SERIAL 条件控制,某些无串口或不用 PIN 框架的板级工程完全可以关闭它们。
UART 注册也遵循相同的设备模型思想:rt_hw_usart_init() 遍历 UART 对象并调用 rt_hw_serial_register(),串口设备类最终同样接入通用设备管理器。不同之处主要是初始化入口和设备类接口,而不是另有一套名称查找机制。
二、总结
本节可以压缩为两条主线:
- 注册线:配置宏选择驱动对象,自动初始化机制调用 BSP 初始化函数,设备类注册接口再把对象交给通用设备管理器。
- 访问线:应用按注册名找到对象,再通过设备类 API 和函数指针逐层调用具体驱动。
PWM3 的完整关系是:
RT_USING_PWM + BSP_USING_PWM3 + BSP_USING_PWM3_CH4 → PWM3_CONFIG → stm32_pwm_obj[] → stm32_pwm_init() → rt_device_pwm_register(..., "pwm3", ...) → rt_device_register() → rt_device_find("pwm3") → rt_pwm_set()/rt_pwm_enable()
设备管理层解决的是统一组织、命名、查找和调用接口的问题;它不会代替板级时钟、引脚复用、供电和接线。理解这一区别后,再阅读 ADC、UART、I²C 等驱动时,可以复用同一方法:先找配置对象与注册入口,再看设备类包装,最后回到应用如何查找和调用。
课程后续项目预告为人脸识别智能门禁,并计划组合语音模块、红外温度传感器和屏幕。无论外设数量多少,都可用本节方法判断每个模块走的是 RT-Thread 设备框架、软件包适配层,还是应用直接操作 GPIO,避免只凭文件名猜调用关系。