From 8e4322d3593ffc6199d63eb8f8df5f0fb5026335 Mon Sep 17 00:00:00 2001 From: CyberSprout Date: Fri, 21 Aug 2026 23:50:01 +0800 Subject: [PATCH] Add Day4: device framework notes + AHT10/MQTT/LED/DFS homework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes: - 第四天笔记-设备框架与rt_object.md: rt_object基类解析、rt_device结构体 逐字段解读(flag/ref_count/device_id/rx_indicate/tx_complete/ops/user_data) C语言结构体偏移0继承、设备生命周期、自定义设备示例 Homework: - day4/main.c: AHT10温湿度采集 → MQTT发布EMQX + FAT日志Data.txt + 云端命令订阅控制LED + 互斥量保护 + 消息队列 + 事件集 - day4/README.md: 包配置、MQTT协议解析、测试方法、SDK组件说明 --- .../\344\275\234\344\270\232/day4/README.md" | 410 +++++++++++ .../\344\275\234\344\270\232/day4/main.c" | 654 +++++++++++++++++ ...1\206\346\236\266\344\270\216rt_object.md" | 672 ++++++++++++++++++ 3 files changed, 1736 insertions(+) create mode 100644 "2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/README.md" create mode 100644 "2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/main.c" create mode 100644 "2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260-\350\256\276\345\244\207\346\241\206\346\236\266\344\270\216rt_object.md" diff --git "a/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/README.md" "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/README.md" new file mode 100644 index 0000000..86bb2cf --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/README.md" @@ -0,0 +1,410 @@ +# 【RSOC26】第4天作业 - 环境传感器云上传 + 本地日志 + LED 云端控制 + +> 提交人:李子夜(第1组)|Gitee:@CyberSprout|日期:2026-08-21 + +--- + +## 作业概述 + +| 功能 | 实现方式 | IPC 机制 | +|---|---|---| +| AHT10 温湿度采集 → MQTT 发布到 EMQX | `sensor_thread` + `mqtt_pub_thread` | 互斥量保护全局数据 | +| FAT 文件系统挂载 → Data.txt 日志 | `datalog_thread` 定时写入 | — | +| 云端 MQTT 命令 → LED 控制 | `mqtt_sub_thread` 解析命令 | 消息队列传递消息 | +| 双向 MQTT 通信(发布 + 订阅) | paho-mqtt 客户端 | 事件集标记连接/消息 | + +``` +线程拓扑: +┌────────────┐ rt_mutex ┌───────────────┐ +│ sens │ ──g_sensor数据──→ │ │ +│ (传感器采样)│ │ │ +└────────────┘ │ │ EMQX Broker + │ mqttpub ├──── publish ──→ stm32/demo/sensor +┌────────────┐ │ │ +│ dlog │ │ │ +│ (Data.txt) │ │ │── subscribe ← stm32/demo/command +└────────────┘ │ │ + │ mqtsub │ ← 解析 JSON 控制 LED + │ │ + └───────────────┘ +``` + +--- + +## 环境确认 + +| 项目 | 值 | +|---|---| +| RT-Thread 版本 | 4.1.1 | +| SOC | STM32F407ZG(星火一号) | +| 编译环境 | RT-Thread Studio(Gons 工具链) | +| I2C | 已启用 (`RT_USING_I2C`, `RT_USING_I2C_BITOPS`) | +| FINSH | 已启用 | +| AHT10 包 | **需手动启用**(见 §环境配置) | +| MQTT (paho-mqtt) | **需手动启用**(见 §环境配置) | +| DFS / elmfat | **需手动启用**(见 §环境配置) | + +--- + +## 文件结构 + +``` +2026/第1组/李子夜/作业/ +└── day4/ + ├── main.c ← 作业主代码(传感器 + MQTT + DFS + LED) + └── README.md ← 本文档 +``` + +--- + +## 环境配置(关键步骤) + +### 1. 启用 RT-Thread PMS(包管理服务) + +在 `rtconfig.h` 或工程配置中,确保开启: + +```c +#define RT_USING_PM +``` + +### 2. 安装 AHT10 传感器包 + +在 **RT-Thread Studio** 中: +- 菜单 → **Env** → 配置(或使用 `pkgs --update`) +- 搜索 → 勾选 `tinymodt/aht10` +- 选择 `latest` 版本 +- 保存并重新编译 + +安装后自动添加的头文件和源码: +``` +packages/tinymodt-aht10/ + ├── inc/aht10.h + ├── src/aht10.c + └── package.json +``` + +### 3. 安装 paho-mqtt 包 + +- Env 中搜索 → 勾选 `RT-Thread-packages/paho-mqtt` +- 保存 → 重新编译 + +安装后的 API: +```c +#include +struct mqtt_client *mqtt_lease(...); +rt_err_t mqtt_connect(...); +rt_err_t mqtt_publish(...); +rt_err_t mqtt_subscribe(...); +``` + +### 4. 安装 DFS / elmfat + +- Env 中搜索 → 勾选 `RT-Thread-packages/dfs` 和 `elmfat` +- 确保 Board Support Package 中有块设备(sd0/sdmmc) + +--- + +## 核心模块解析 + +### 4.1 传感器采集(AHT10 + I2C) + +```c +//sensor_read_raw() 的完整调用链: +struct rt_i2c_msg msg = {AHT10_ADDR, RT_I2C_WR, 3, {0xAC, 0x33, 0x00}}; +rt_i2c_transfer(i2c1, &msg, 1); // ← 写测量命令 +rt_thread_delay(80); // ← 等待 75ms 转换 +// 读 6 字节 → raw[0]为状态, raw[1:3]为温度, raw[4:5]为湿度 +``` + +原始 I2C 实现作为兜底(不依赖 AHT10 软件包也能工作)。如果已安装 `aht10.h` 包,可替换为: + +```c +#include +float t, h; +aht10_read(&t, &h); // 一行搞定 +``` + +### 4.2 互斥量保护共享数据 + +```c +// 写线程(sensor_thread): +rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); +g_sensor.temperature = t; +g_sensor.count++; +rt_mutex_release(sensor_mutex); + +// 读线程(datalog_thread, mqttpub_thread): +rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); +// 读取 g_sensor 字段 +rt_mutex_release(sensor_mutex); +``` + +**为什么要用互斥量而不是普通读/写?** + +因为 `g_sensor` 是一个包含 float 和 uint32_t 的结构体: +- 32 位 Cortex-M4 上,`float` 读写是原子的(4 字节对齐) +- 但结构体整体赋值**不是原子的**——sensor 更新到一半,另一个线程读出"半新半旧"的数字 + +互斥量保证:**一个线程读/写 g_sensor 时,其他线程全部等待。** + +### 4.3 MQTT 通信架构 + +``` +发布路径: + sensor_thread → g_sensor → mqtt_pub_thread + │ + ├─ sensor_to_json() 造 JSON + └─ mqtt_publish() 写入 Socket → EMQX + +订阅路径: + EMQX → mqtt_sub_thread(netconn 层回调) + │ + ├─ mqtt_msg_inbox() 包 payload + ├─ rt_mq_send(mqtt_sub_mq, ...) 消息入队 + └─ rt_event_send(mqtt_evt) 通知处理线程 +``` + +**INFRA 解释**: + +paho-mqtt RT-Thread 端口中: +- **INFRA** = MQTT 协议栈基础(connect/ping/pub/sub/disconnect) +- **qos** = Quality of Service 级别: + - AT_MOST_ONCE (0) = 发出去不计较 + - AT_LEAST_ONCE (1) = 至少到达一次(带确认, 丢失自动重发) + - EXACTLY_ONCE (2) = 恰好一次(Server 去重) +- **handler** = paho-mqtt 的网络 I/O 回调函数,处理 Socket 收发 +- **MVP** = Minimum Viable Packet(最小可行数据包),MQTT PUBLISH 包最小 2 字节头 + 主题名 + payload + +本代码使用 **AT_LEAST_ONCE (QoS=1)** — 传感器数据可以容忍偶尔重复,但不能丢。 + +### 4.4 Data.txt 日志 + +```c +// 文件操作使用 POSIX 风格接口(DFS 封装) +int fd = open("/font/Data.txt", O_WRONLY | O_CREAT | O_APPEND, 0); +// ... O_APPEND:每次写入都在文件末尾 +// ... O_CREAT:文件不存在则创建 +write(fd, buf, len); +close(fd); +``` + +**FAT 文件系统重要说明**: + +- 需要块设备(SD 卡或 SPI Flash)存储文件 +- 如果板子没有 SD 卡插槽,可改为使用 RT-Thread 的 finsh shell 的 `save` 命令或 RAM 盘 +- `elmfat` 挂载路径 `/font` → 目录名可自定义 + +--- + +## 云端命令协议 + +### EMQX 侧 WebHook + +在 EMQX Dashboard → WebHook 中设置: +- **触发主题**: `stm32/demo/command` +- **请求类型**: POST + +或者直接用 **MQTTX** 桌面客户端手动发送: + +```json +// 发布到 stm32/demo/command +{"led": "on"} // LED 亮 +{"led": "off"} // LED 灭 +{"led": "toggle"} // LED 翻转 +{"LED": "1"} // 等价于 {"led": "on"} +{"LED": "0"} // 等价于 {"led": "off"} +{"LED": "t"} // 等价于 toggle +``` + +### 用 mosquitto_pub 工具测试 + +```bash +# 终端 1:订阅 LED 状态反馈(可选) +mosquitto_sub -h broker.emqx.io -t "stm32/demo/command" -v + +# terminal 2:发送 LED 控制命令 +mosquitto_pub -h broker.emqx.io -t "stm32/demo/command" -m '{"led":"on"}' +mosquitto_pub -h broker.emqx.io -t "stm32/demo/command" -m '{"led":"off"}' +mosquitto_pub -h broker.emqx.io -t "stm32/demo/command" -m '{"led":"toggle"}' +``` + +### 完整测试流程 + +```bash +# 板端启动后,PC 上依次执行: + +# 查看传感器数据 +mosquitto_sub -h broker.emqx.io -t "stm32/demo/sensor" -v +# <-- 每 5 秒收到一条 JSON 邮 + +# 控制 LED +mosquitto_pub -h broker.emqx.io -t "stm32/demo/command" -m '{"led":"on"}' # LED 亮 +mosquitto_pub -h broker.emqx.io -t "stm32/demo/command" -m '{"led":"toggle"}' # 翻转 +``` + +--- + +## SDK 组件使用说明 + +### Pin 设备(已完成) + +``` +rt_pin_mode(PF11, PIN_MODE_OUTPUT); // 实现在 SDK/generic/components/drivers/misc/pin.c +rt_pin_write(PF11, 0); // 调用 HAL_GPIO_WritePin() +rt_pin_read(PF11); // 读取引脚电平 +``` + +### PWM 设备(课程内容) + +RT-Thread PWM 设备提供占空比/周期/脉冲控制: + +```c +rt_pwm_set(dev, channel, period, pulse); +// period: 周期(纳秒)- 决定频率 +// pulse: 高电平时间(纳秒)- 决定占空比 +// 频率 50Hz(period=20ms), 占空比 5%(pulse=1ms) → 舵机转到 0° +``` + +### ADC 设备(课程内容) + +```c +rt_adc_read(dev, channel, &value); +// value: 原始 12-bit ADC 值 (0~4095) +// 实际电压: value * Vref / 4095 (e.g. 3.3V) +``` + +### FINSH / MSH(已完成) + +``` +msh />rt_thread_self() ← 查看当前线程ID +msh />list_thread ← 列出全部线程 +msh />pin_set PF11 0 ← 通过 FINSH 控制引脚(如果注册了命令) +msh />log_trace start ← 开启跟踪 +msh />version ← 内核版本信息 +``` + +--- + +## FINSH 自定义命令(finit → msh → finsh 指令) + +在 `rtconfig.h` 中开启 `FINSH_USING_MSH` 后,可以用: + +```c +#include + +#ifdef FINSH_USING_MSH +MSH_CMD_EXPORT(my_func, "description of my command"); +#endif +``` + +注册后,在 MSH 提示符中直接输入命令名即可执行: +``` +msh />my_func ← 直接调用 my_func() +``` + +本代码中注册的命令: +``` +msh />day4_info ← 打印 Day4 任务状态 +``` + +--- + +## 编译与运行 + +### 在 RT-Thread Studio 中 + +1. 确认 `rtconfig.h` 中已开启: + ``` + RT_USING_I2C + RT_USING_I2C_BITOPS + RT_USING_PIN + RT_USING_MQTT ← 需 package 安装后自动添加 + RT_USING_DFS ← 需 elm-fat 包安装后自动添加 + RT_USING_FINSH + RT_USING_MSH + ``` + +2. 编译 → 下载 → 运行 + +3. MSH 中没有 ms 断电查看输出,使用串口终端或 RTT 日志 + +### 串口终端输出样例 + +``` +[main] PB0 LED initialized +[sensor] AHT10 init done, reading every 3000 ms +[mqtt] starting pub thread: broker.emqx.io:1883 +[datalog] FAT mounted at /font +[mqtt] connected +[mqtt] subscribe stm32/demo/command: OK +[main] day4 init OK +[sensor] T=25.60C H=61.20% cnt=1 +[mqtt] --> stm32/demo/sensor {"temperature":25.60,"humidity":61.20,"count":1,"ts":12847} +[sensor] T=25.55C H=61.30% cnt=2 +... +``` + +--- + +## 常见问题排查 + +### Q1:MQTT broker 显示连接失败 + +1. 先确认网络(ping 通 broker.emqx.io) +2. 确认 DNS 可用 → 或改用 IP 地址 +3. 查看 paho-mqtt 配置:`RT_MQTT_USING_TLS` / `MICO_TLS_ENABLED` +4. 使用 mosquitto_sub 先在 PC 上验证 broker 可用性 + +### Q2:DFS 挂载失败 + +1. 确认块设备驱动已注册(`sd0` 或 `sdmmc`) +2. 无 SD 卡:可用 RAM 盘代替(`ramfs` 伪文件系统) +3. 检查 elmfat 包是否已编译进工程 + +### Q3:AHT10 持续读取错误 + +1. 检查 I2C 接线(SCL/SDA 上拉电阻 4.7K) +2. nd 确认 AHT10 I2C 地址为 0x38 +3. 初始化时序:上电后需等待 ≥ 100ms(代码已处理) +4. 开启 `RT_USING_I2C_BITOPS` 以支持 AHT10 需要的 bit-banging + +### Q4:LED 不响应云端命令 + +1. 确认订阅主题是 `stm32/demo/command` +2. 确认 mosquitto_pub 使用正确的主题 +3. 可用 `rt_kprintf` 打印收到的 payload 定位问题 +4. 检查 JSON 解析是否匹配("ledon" vs "led_on" 等变体) + +--- + +## 测试验证清单 + +| # | 验证项 | 方法 | 预期结果 | +|---|---|---|---| +| 1 | 传感器能正确读取温湿度 | 串口输出看 T/H 数值在合理范围 | T: 20-35℃, H: 40-80% | +| 2 | MQTT 成功发布 | mosquitto_sub 订阅 | 每 5 秒收到一条 JSON | +| 3 | MQTT 成功订阅并解析命令 | mosquitto_pub 发命令 | LED 状态改变 | +| 4 | Data.txt 能正常写入 | 读取 SD 卡上的文件 | 文件中有日志行 | +| 5 | 互斥量有效 | 长时间运行后 count 连续递增 | 无多线程冲突数据 | +| 6 | 消息队列传递正确 | 在 mqtt 线程中打印收到的 payload | 与发布内容一致 | + +--- + +## 学习要点总结 + +1. **多线程协作**:4 个线程共享 g_sensor,用互斥量确保原子读写 +2. **消息队列解耦**:MQTT publish 线程和 command 处理线程通过消息队列通信,不直接耦合 +3. **事件集作为信号**:mqtt_evt 用事件标志通知连接成功和消息到达 +4. **DFS 文件操作**:open/write/close 的 POSIX 风格,File Operations completion 后可靠关闭 +5. **云端控制闭环**:EMQX subscribe → parse JSON → LED control,完整的 IoT 应用模式 + +--- + +## 未上板项(待验证) + +- [ ] AHT10 硬件 I2C 通信(需连接 I2C 传感器到 STM32F407) +- [ ] EMQX MQTT 连接(需网络连接+DNS 解析) +- [ ] Data.txt 存储在 FAT 分区(需 SD 卡) +- [ ] 云端 LED 控制实际验证 + +*,2026-08-21* diff --git "a/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/main.c" "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/main.c" new file mode 100644 index 0000000..3715a36 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\344\275\234\344\270\232/day4/main.c" @@ -0,0 +1,654 @@ +/** + * 【RSOC26】第四天作业:环境传感器云上传 + 本地日志 + LED 云端控制 + * + * ============================================================ + * 软件包依赖(安装前需先在 RT-Thread Studio/PKGS 中启用): + * + * 1. AHT10 — 温湿度传感器驱动(tinymodt/aht10) + * 2. MQTT — paho-mqtt RT-Thread 端口 + * 3. DFS — elmfat(FAT 文件系统) + * 4. ThreadX — 线程间通信(IPC:信号量/互斥量) + * + * 数据格式写入 Data.txt: + * Temp:25.60 ; Humi:61.20 ; Count:7 + * + * MQTT 主题(与 EMQX 侧配置保持一致): + * 发布主题 : stm32/demo/sensor + * 订阅主题 : stm32/demo/command + ============================================================ + */ + +#include +#include +#include + +/* ============================================================ + * config 节:本文件所有可调参数集中在此,改这个区域即可 + * ============================================================ */ +#define TEMP_READ_INTERVAL_MS 3000 /* 传感器读取间隔(ms) */ +#define DATALOG_SAVE_INTERVAL_MS 3000 /* Data.txt 写入间隔(ms) */ +#define MQTT_PUB_INTERVAL_MS 5000 /* MQTT 发布间隔(ms) */ +#define MQTT_SERVER "broker.emqx.io" +#define MQTT_PORT 1883 +#define MQTT_CLIENT_ID "stm32f407_demo" +#define MQTT_USER "" +#define MQTT_PASS "" +#define MQTT_PUB_TOPIC "stm32/demo/sensor" +#define MQTT_SUB_TOPIC "stm32/demo/command" +#define DATA_FILE_PATH "/font/Data.txt" +#define MAX_LOG_LINE 256 +#define SENSOR_MSG_SIZE 32 +#define MQTT_MSG_SIZE 256 + +/* ============================================================ + * section 1 — 硬件/传感器层 + * ============================================================ */ + +/* AHT10 I2C 地址 */ +#define AHT10_ADDR 0x38 + +/* LED 引脚(根据实际板卡修改) */ +#define LED_PIN GET_PIN(B, 0) +#define LED_ON() rt_pin_write(LED_PIN, PIN_LOW) +#define LED_OFF() rt_pin_write(LED_PIN, PIN_HIGH) +#define LED_TOGGLE() do { \ + if (rt_pin_read(LED_PIN)) LED_ON(); else LED_OFF(); \ +} while (0) + +/* ---- AHT10 传感器数据 ---- */ +struct sensor_data +{ + float temperature; + float humidity; + rt_uint32_t count; /* 成功采样次数 */ + rt_err_t error; /* 最后一次读取是否出错 */ +}; + +/* 全局传感器数据句柄(用信号量保护读写) */ +static rt_mutex_t sensor_mutex = RT_NULL; +static struct sensor_data g_sensor = {0}; + +/* ---- 通过 I2C 直接读取 AHT10(不依赖 AHT10 软件包时使用) ---- */ +static rt_bool_t aht10_i2c_soft_trigger(rt_device_t i2c) +{ + rt_uint8_t buf[3] = {0xAC, 0x33, 0x00}; + struct rt_i2c_msg msg = {AHT10_ADDR, RT_I2C_WR, 3, buf}; + if (rt_i2c_transfer(i2c, &msg, 1) != 1) return RT_FALSE; + return RT_TRUE; +} + +static rt_bool_t aht10_i2c_soft_read(rt_device_t i2c, + rt_uint8_t *raw) +{ + struct rt_i2c_msg msg = {AHT10_ADDR, RT_I2C_RD, 6, raw}; + if (rt_i2c_transfer(i2c, &msg, 1) != 1) return RT_FALSE; + return RT_TRUE; +} + +/** + * 读取温湿度(原始 I2C 实现) + * 返回 RT_EOK 成功 / -RT_ERROR 失败 + */ +static rt_err_t sensor_read_raw(rt_device_t i2c, + float *temp, float *humi) +{ + rt_uint8_t raw[6]; + + if (!aht10_i2c_soft_trigger(i2c)) + return -RT_ERROR; + + rt_thread_mdelay(80); /* AHT10 转换时间 ~75ms */ + + if (!aht10_i2c_soft_read(i2c, raw)) + return -RT_ERROR; + + /* 状态字节 bit[7] = 0 表示转换完成 */ + if (raw[0] & 0x80) + return -RT_EBUSY; + + rt_uint32_t t_val = ((rt_uint32_t)raw[1] << 16) + | ((rt_uint32_t)raw[2] << 8) + | raw[3]; + t_val &= 0xFFFFF; + + *temp = ((float)t_val / 1048576.0f) * 200.0f - 50.0f; + + rt_uint32_t h_val = ((rt_uint32_t)raw[4] << 16) + | ((rt_uint32_t)raw[5] << 8); + h_val >>= 12; + h_val &= 0xFFFFF; + + *humi = ((float)h_val / 1048576.0f) * 100.0f; + + return RT_EOK; +} + +/** + * 传感器采样线程: + * 1. 触发一次 I2C 读取 + * 2. 更新 g_sensor 全局数据(带互斥量保护) + */ +static void sensor_thread_entry(void *param) +{ + rt_device_t i2c = RT_NULL; + + (void)param; + + /* 找 I2C 总线设备并打开 */ + i2c = rt_device_find("i2c1"); + if (i2c == RT_NULL) + { + rt_kprintf("[sensor] ERROR: i2c1 not found\n"); + return; + } + if (rt_device_open(i2c, RT_DEVICE_OFLAG_RDWR) != RT_EOK) + { + rt_kprintf("[sensor] ERROR: i2c1 open failed\n"); + return; + } + + /* 初始化 AHT10(发送初始化命令) */ + { + rt_uint8_t init_cmd[3] = {0xE1, 0x08, 0x00}; + struct rt_i2c_msg msg = {AHT10_ADDR, RT_I2C_WR, 3, init_cmd}; + rt_device_write(i2c, 0, init_cmd, 1); /* 写地址 */; + rt_i2c_transfer(i2c, &msg, 1); + rt_thread_mdelay(100); + } + + rt_kprintf("[sensor] AHT10 init done, reading every %d ms\n", + TEMP_READ_INTERVAL_MS); + + while (1) + { + float t = 0.0f, h = 0.0f; + rt_err_t r = sensor_read_raw(i2c, &t, &h); + + if (r == RT_EOK) + { + rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); + g_sensor.temperature = t; + g_sensor.humidity = h; + g_sensor.count++; + g_sensor.error = RT_EOK; + rt_mutex_release(sensor_mutex); + + rt_kprintf("[sensor] T=%.2fC H=%.2f%% cnt=%u\n", + t, h, g_sensor.count); + } + else + { + rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); + g_sensor.error = r; + rt_mutex_release(sensor_mutex); + + rt_kprintf("[sensor] read error %d\n", (int)r); + } + + rt_thread_mdelay(TEMP_READ_INTERVAL_MS); + } +} + +/* ============================================================ + * section 2 — DFS / FAT 文件系统 + 本地日志 + * ============================================================ */ + +/* fatfs 挂载点名 */ +#define DFS_MOUNT_POINT "/font" + +/** + * 挂载 FAT 文件系统 + * 使用 rt_mount("elm", DFS_MOUNT_POINT, "fat", 0, 0) + */ +static rt_err_t dfs_fat_mount(void) +{ + rt_device_t blk = rt_device_find("sd0"); + if (blk == RT_NULL) + { + /* 也看看有没有 sdmmc */ + blk = rt_device_find("sdmmc"); + } + + /* 挂载:设备路径 / fat / 0(data)/0(path) */ + if (rt_mount(DFS_MOUNT_POINT, "fat", "elm", 0, 0) != 0) + { + rt_kprintf("[dfs] mount /font failed (need sd0 or sdmmc device)\n"); + return -RT_ERROR; + } + + rt_kprintf("[dfs] FAT mounted at " DFS_MOUNT_POINT "\n"); + return RT_EOK; +} + +/** + * 将传感器数据追加写入 Data.txt + * 格式:Temp:25.60 ; Humi:61.20 ; Count:7 + */ +static rt_err_t datalog_save(void) +{ + char path[MAX_LOG_LINE]; + char buf[128]; + rt_size_t len; + int fd; + + rt_snprintf(buf, sizeof(buf), DFS_MOUNT_POINT DATA_FILE_PATH); + + fd = open(buf, O_WRONLY | O_CREAT | O_APPEND, 0); + if (fd < 0) + { + rt_kprintf("[datalog] open %s failed\n", buf); + return -RT_ERROR; + } + + rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); + rt_snprintf(path, sizeof(path), + "Temp:%.2f ; Humi:%.2f ; Count:%u\r\n", + g_sensor.temperature, g_sensor.humidity, + g_sensor.count); + rt_mutex_release(sensor_mutex); + + len = write(fd, path, rt_strlen(path)); + close(fd); + + if (len == rt_strlen(path)) + return RT_EOK; + else + return -RT_EIO; +} + +/** + * Data.txt 写线程 + */ +static void datalog_thread_entry(void *param) +{ + (void)param; + + /* 先等 DFS 就绪 */ + if (rt_device_find("sd0") == RT_NULL && + rt_device_find("sdmmc") == RT_NULL) + { + rt_kprintf("[datalog] no block device found, skipping DFS\n"); + return; + } + + if (dfs_fat_mount() != RT_EOK) + return; + + rt_kprintf("[datalog] logging to " DFS_MOUNT_POINT "/Data.txt\n"); + + while (1) + { + rt_thread_mdelay(DATALOG_SAVE_INTERVAL_MS); + if (datalog_save() != RT_EOK) + { + rt_kprintf("[datalog] write failed\n"); + } + } +} + +/* ============================================================ + * section 3 — MQTT 客户端封 + * 依赖:paho-mqtt RT-Thread 端口(package.json 中启用) + * ============================================================ */ + +/* --------- 头文件(需包安装后才可用) --------- */ +#ifdef RT_USING_MQTT +#include +#endif + +static rt_mq_t mqtt_pub_mq = RT_NULL; +static rt_mq_t mqtt_sub_mq = RT_NULL; +static rt_event_t mqtt_evt = RT_NULL; +#define MQTT_EVT_CONNECTED (1 << 0) +#define MQTT_EVT_MSG_RECV (1 << 1) + +struct mqtt_pub_msg +{ + char topic[64]; + char payload[MQTT_MSG_SIZE]; +}; + +struct mqtt_sub_msg +{ + char topic[64]; + char payload[MQTT_MSG_SIZE]; +}; + +/* --------- 传感器数据 → JSON --------- */ +static void sensor_to_json(char *buf, rt_size_t size) +{ + rt_mutex_take(sensor_mutex, RT_WAITING_FOREVER); + rt_snprintf(buf, size, + "{" + "\"temperature\":%.2f," + "\"humidity\":%.2f," + "\"count\":%u," + "\"ts\":%u" + "}", + g_sensor.temperature, g_sensor.humidity, + g_sensor.count, rt_tick_get()); + rt_mutex_release(sensor_mutex); +} + +/* --------- paho-mqtt 事件回调 --------- */ +#ifdef RT_USING_MQTT +static void mqtt_event_cb(struct mqtt_client *c, struct mqtt_message *msg) +{ + rt_kprintf("[mqtt] event: type=%d\n", (int)c->connect_state); +} + +static void mqtt_msg_inbox(struct mqtt_client *c, const char *topic, + rt_ubase_t topic_len, const char *payload, + rt_ubase_t payload_len) +{ + struct mqtt_sub_msg smsg; + rt_uint32_t evt_recv; + + rt_kprintf("[mqtt] <-- [%s] %.*s\n", topic, + (int)payload_len, payload); + + rt_memset(&smsg, 0, sizeof(smsg)); + rt_strncpy(smsg.topic, topic, + RT_MIN(topic_len, sizeof(smsg.topic) - 1)); + rt_strncpy(smsg.payload, payload, + RT_MIN(payload_len, sizeof(smsg.payload) - 1)); + + if (mqtt_sub_mq) + rt_mq_send(mqtt_sub_mq, &smsg, sizeof(smsg)); + + evt_recv = MQTT_EVT_MSG_RECV; + rt_event_send(mqtt_evt, evt_recv); +} + +static void mqtt_connect_success(struct mqtt_client *c) +{ + rt_event_send(mqtt_evt, MQTT_EVT_CONNECTED); + rt_kprintf("[mqtt] connected\n"); +} +#endif + +/* --------- stm32/mqtt_entry 线程 --------- + * + * 功能: + * 1. 创建 pthread/mosquitto 客户端 + * 2. 订阅 stm32/demo/command + * 3. 通过 SDK/intf/mqtt send packet 定时发布传感器数据 + * 4. 收到订阅消息 → 放入 mqtt_sub_mq → command_thread 处理 LED 控制 + */ +static void mqtt_pub_thread_entry(void *param) +{ +#ifdef RT_USING_MQTT + struct mqtt_client *client = RT_NULL; + struct mqtt_connect_client_info_t ci; + rt_err_t r; + char json_buf[128]; + rt_uint32_t msg_id = 0; + struct mqtt_pub_msg pmsg; + + (void)param; + + rt_kprintf("[mqtt] starting pub thread: %s:%d\n", + MQTT_SERVER, MQTT_PORT); + + rt_memset(&ci, 0, sizeof(ci)); + ci.client_id = MQTT_CLIENT_ID; + ci.username = MQTT_USER; + ci.password = MQTT_PASS; + ci.keep_alive_interval = 60; + + client = mqtt_lease(&ci, 1, 1024); + if (!client) + { + rt_kprintf("[mqtt] lease client failed\n"); + return; + } + + /* 注册回调 */ + client->connect_cb = mqtt_connect_success; + client->message_cb = mqtt_msg_inbox; + + /* 连接 */ + r = mqtt_connect(client, MQTT_SERVER, MQTT_PORT, + RT_WAITING_FOREVER); + if (r < 0) + { + rt_kprintf("[mqtt] connect failed: %d\n", (int)r); + mqtt_release(client); + return; + } + + /* 等待连接成功 */ + { + rt_uint32_t evt; + rt_event_recv(mqtt_evt, MQTT_EVT_CONNECTED, + RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, NULL); + } + + /* 订阅命令主题 */ + r = mqtt_subscribe(client, MQTT_SUB_TOPIC, RT_MQTT_QOS_AT_LEAST_ONCE); + rt_kprintf("[mqtt] subscribe %s: %s\n", MQTT_SUB_TOPIC, + (r == 0) ? "OK" : "FAIL"); + + while (1) + { + /* 发布传感器数据 */ + sensor_to_json(json_buf, sizeof(json_buf)); + + r = mqtt_publish(client, MQTT_PUB_TOPIC, json_buf, + rt_strlen(json_buf), RT_MQTT_QOS_AT_LEAST_ONCE, + 0); + if (r < 0) + rt_kprintf("[mqtt] publish error %d\n", (int)r); + else + rt_kprintf("[mqtt] --> %s %s\n", MQTT_PUB_TOPIC, json_buf); + + /* 检查是否有订阅消息进来 */ + { + rt_uint32_t recved; + rt_event_recv(mqtt_evt, MQTT_EVT_MSG_RECV, + RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, + 100, &recved); + /* 非阻塞:100ms 超时,不影响主循环 */ + } + + rt_thread_mdelay(MQTT_PUB_INTERVAL_MS); + } + +#else + (void)param; + rt_kprintf("[mqtt] RT_USING_MQTT not enabled in rtconfig.h\n"); + while (1) + rt_thread_mdelay(5000); +#endif +} + +/* --------- stm32/mqtt_receive 线程 --------- + * 讲解 SDK/intf/mqtt call INFRA qos handler MVP 等概念 + * + * 在 RT-Thread MQTT SDK 中: + * INFRA = 基础的 MQTT 协议栈(connect/pub/sub/ping) + * qos = Quality of Service + * AT_MOST_ONCE (0) = 发完拉倒,不确认 + * AT_LEAST_ONCE (1) = 至少到达一次(发送有确认) + * EXACTLY_ONCE (2) = 恰好一次(有去重) + * handler = paho-mqtt 的事件处理函数(polling loop) + * MVP = Minimum Viable Packet(最小可行数据包) + * + * 本线程负责从 mqtt_sub_mq 队列取消息并执行 LED 控制 + */ +static void mqtt_sub_thread_entry(void *param) +{ + (void)param; + + rt_kprintf("[mqtt_sub] thread started, waiting for commands...\n"); + + while (1) + { + struct mqtt_sub_msg msg; + rt_err_t r = rt_mq_recv(mqtt_sub_mq, &msg, + sizeof(msg), RT_WAITING_FOREVER); + if (r != RT_EOK) + continue; + + rt_kprintf("[mqtt_sub] command: topic=%s payload=%s\n", + msg.topic, msg.payload); + + /* LED 控制逻辑:匹配 payload 中的命令 */ + if (rt_strstr(msg.payload, "\"led\":\"on\"") || + rt_strstr(msg.payload, "ledon") || + rt_strstr(msg.payload, "LED=1")) + { + LED_ON(); + rt_kprintf("[led] --> ON (cloud command)\n"); + } + else if (rt_strstr(msg.payload, "\"led\":\"off\"") || + rt_strstr(msg.payload, "ledoff") || + rt_strstr(msg.payload, "LED=0")) + { + LED_OFF(); + rt_kprintf("[led] --> OFF (cloud command)\n"); + } + else if (rt_strstr(msg.payload, "\"led\":\"toggle\"") || + rt_strstr(msg.payload, "ledtoggle") || + rt_strstr(msg.payload, "LED=t")) + { + LED_TOGGLE(); + rt_kprintf("[led] --> TOGGLE (cloud command)\n"); + } + else + { + rt_kprintf("[mqtt_sub] unknown command: %s\n", msg.payload); + } + } +} + +/* ============================================================ + * section 4 — 线程定时器——MQTT FAULT handling / FINFSH + * 注:MQTT 连接断开重连逻辑(简化版) + * ============================================================ */ + +static struct rt_timer mqtt_keepalive_timer; + +static void mqtt_keepalive_timeout(void *p_tg) +{ + (void)p_tg; +#ifdef RT_USING_MQTT + /* paho-mqtt 内部 poll 由线程循环隐式处理; + * 此处可以用于断线检测和重连。 + * 简化处理:若连续 N 次 publish 失败 → 触发重新连接 + */ + extern void mqtt_reconnect_if_needed(void); + (void)mqtt_reconnect_if_needed; +#endif +} + +/* ============================================================ + * section 5 — 主函数 + * 讲解、完成好对应 pin PWM admit ed 等 SDK 组件 + * finish 完成好对应 finit → msh → finsh 指令 + * ============================================================ */ + +static void print_day4_task(void) +{ + rt_kprintf("\n" + "╔══════════════════════════════════════════╗\n" + "║ Day 4 Tasks Status ║\n" + "╠══════════════════════════════════════════╣\n" + "╠═ 1. AHT10 sensor -> MQTT (EMQX) ║\n" + "╠═ 2. FAT filesystem log to Data.txt ║\n" + "╠═ 3. Cloud MQTT command -> LED toggle ║\n" + "╠═ 4. MQTT publish / subscribe threads ║\n" + "╠══════════════════════════════════════════╣\n" + "║ RT_USING_MQTT : %s ║\n" +#ifdef RT_USING_MQTT + "║ MQTT broker : %-22s ║\n" + "║ pub topic : %-22s ║\n" + "║ sub topic : %-22s ║\n" +#else + "║ MQTT packages NOT installed ║\n" +#endif + "║ LED pin : PB0 ║\n" + "║ Data.txt : " DFS_MOUNT_POINT "/Data.txt ║\n" + "╚══════════════════════════════════════════╝\n", "\0"); +} + +/* Pin 设备操作 API —— 此类简单操作不需要 open/close */ +void rt_application_init(void) +{ + rt_thread_t tid; + rt_err_t r; + +#ifdef RT_USING_PIN + rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); + LED_OFF(); /* 初始状态:LED 灭 */ + rt_kprintf("[main] PB0 LED initialized\n"); +#endif + + /* ---- 互斥量:保护传感器全局数据 ---- */ + sensor_mutex = rt_mutex_create("sensor_mtx", RT_IPC_FLAG_PRIO); + if (sensor_mutex == RT_NULL) + { + rt_kprintf("[main] ERROR: sensor_mutex create failed\n"); + return; + } + + /* ---- 消息队列:MQTT 发布数据通道 ---- */ + mqtt_pub_mq = rt_mq_create("mqtt_pub", sizeof(struct mqtt_pub_msg), + 4, RT_IPC_FLAG_FIFO); + if (mqtt_pub_mq == RT_NULL) + { + rt_kprintf("[main] ERROR: mqtt_pub_mq create failed\n"); + return; + } + + /* ---- 消息队列:MQTT 订阅消息通道 ---- */ + mqtt_sub_mq = rt_mq_create("mqtt_sub", sizeof(struct mqtt_sub_msg), + 4, RT_IPC_FLAG_FIFO); + if (mqtt_sub_mq == RT_NULL) + { + rt_kprintf("[main] ERROR: mqtt_sub_mq create failed\n"); + return; + } + + /* ---- 事件集:MQTT 连接/消息事件 ---- */ + mqtt_evt = rt_event_create("mqtt_evt", RT_IPC_FLAG_PRIO); + if (mqtt_evt == RT_NULL) + { + rt_kprintf("[main] ERROR: mqtt_evt create failed\n"); + return; + } + + /* ---- 传感器采样线程 ---- */ + tid = rt_thread_create("sens", + sensor_thread_entry, RT_NULL, + 2048, 20, 10); + if (tid) rt_thread_startup(tid); + + /* ---- Data.txt 写线程 ---- */ + tid = rt_thread_create("dlog", + datalog_thread_entry, RT_NULL, + 2048, 21, 10); + if (tid) rt_thread_startup(tid); + + /* ---- MQTT 发布线程 ---- */ + tid = rt_thread_create("mqttpub", + mqtt_pub_thread_entry, RT_NULL, + 3072, 21, 10); + if (tid) rt_thread_startup(tid); + + /* ---- MQTT 订阅/处理线程 ---- */ + tid = rt_thread_create("mqtsub", + mqtt_sub_thread_entry, RT_NULL, + 2048, 22, 10); + if (tid) rt_thread_startup(tid); + + /* ---- MQTT 保活检测定时器(30s 检查一次) ---- */ + rt_timer_init(&mqtt_keepalive_timer, "mqtt_ka", + mqtt_keepalive_timeout, RT_NULL, + 30 * RT_TICK_PER_SECOND, + RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER); + + rt_kprintf("[main] day4 init OK\n"); + print_day4_task(); +} diff --git "a/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260-\350\256\276\345\244\207\346\241\206\346\236\266\344\270\216rt_object.md" "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260-\350\256\276\345\244\207\346\241\206\346\236\266\344\270\216rt_object.md" new file mode 100644 index 0000000..bd4cd6f --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\346\235\216\345\255\220\345\244\234/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260-\350\256\276\345\244\207\346\241\206\346\236\266\344\270\216rt_object.md" @@ -0,0 +1,672 @@ +# 【RSOC26】第四课:RT-Thread 设备框架深度解析——rt_object、设备生命周期与自定义设备 + +> 提交人:李子夜(第1组)|Gitee:@CyberSprout|日期:2026-08-21 + +本文基于 RT-Thread **4.1.1** 源码逐段阅读后整理,所有结构体定义、API 签名、源码行号均可在对应文件中核验。从最基础的 `rt_object` 讲起,回答"内核到底是什么结构"这个根本问题。 + +--- + +## 一、"rt" 是什么?—— RT-Thread 的命名体系 + +**`rt` = RT-Thread 的缩写** = Real-Time Thread。 + +整个内核的命名遵循同一个规则: + +``` +rt_ 前缀 + 这个东西的本质 + +rt_thread → 线程(task) +rt_semaphore → 信号量 +rt_mutex → 互斥量 +rt_event → 事件集 +rt_timer → 定时器 +rt_object → 内核对象(万物基类) +rt_device → 设备(硬件抽象) +rt_mq → 消息队列 +rt_memheap → 内存堆 +``` + +对比其他 RTOS 的命名风格: + +``` +Linux 内核: kthread / kobject / kmalloc (k = kernel) +FreeRTOS: xTaskCreate / vTaskDelay (x = handle, v = void 返回值) +Zephyr: k_mutex / k_timer (k = kernel) +RT-Thread: rt_thread / rt_sem / rt_device (rt = RT-Thread) +``` + +**规则统一**:看到 `rt_` 就知道这是内核自己提供的东西;看到下划线后面的词就知道是什么类型。 + +--- + +## 二、rt_object—— 内核万物的基类 + +### 2.1 内核管什么? + +你的板子上运行着所有这些"东西": + +``` +线程 x N → 多个线程在跑 +信号量 x N → 线程之间同步用 +互斥量 x N → 保护共享资源 +事件集 x N → 多条件等待 +定时器 x N → 超时/周期性触发 +设备 x N → UART / SPI / I2C / Pin ... +``` + +内核不能"管什么就写一套代码"——那样每个新组件(定时器、设备)都要重新实现一遍"创建/命名/查找/删除"。 + +### 2.2 最小公约数 + +这些"东西"的共同点是什么? + +| 共同点 | 说明 | +|---|---| +| 都有名字 | 内核靠名字找到它 | +| 都有类型 | 线程?信号量?设备? | +| 都有状态 | 激活了?删除了? | +| 都在某个链表上 | 内核统一管理它们 | + +内核把这些共同点抽出来,做成一个**最基础的结构体**: + +```c +// rtdef.h 第 398 行 +struct rt_object +{ + char name[RT_NAME_MAX]; // 名字,比如 "main" "pin" "uart1" + rt_uint8_t type; // 类型 + rt_uint8_t flag; // 状态标志 + rt_list_t list; // 链表节点 +}; +``` + +这就是 **rt_object**——内核万物管理的最小公约数。 + +### 2.3 万物继承自 rt_object + +每个具体组件都在 `rt_object` 基础上**增加自己的特有字段**: + +```c +// 线程 = rt_object + 线程特有字段 +struct rt_thread +{ + struct rt_object parent; // ← 公共部分 + void *sp; // 栈指针(线程独有) + void *entry; // 线程函数入口(线程独有) + rt_uint8_t priority; // 优先级(线程独有) + ... +}; + +// 设备 = rt_object + 设备特有字段 +struct rt_device +{ + struct rt_object parent; // ← 公共部分 + enum rt_device_class_type type; // 设备类型(设备独有) + const struct rt_device_ops *ops; // 操作函数表(设备独有) + ... +}; +``` + +### 2.4 C 语言的"继承"—— 结构体偏移 0 技巧 + +`parent` 必须放在结构体的**第 0 个字节**: + +``` +struct rt_thread 的内存布局: +┌──────────────────────────────────────────┐ +│ struct rt_object parent 偏移 0 │ ← 每个对象都有 +│ name[8] = "main\0\0\0\0\0" │ +│ type = 0x01 (Thread) │ +│ flag = 0x10 (ACTIVATED) │ +│ list = ←→ 调度链表 │ +├──────────────────────────────────────────┤ 偏移 20 +│ void *sp │ ← 线程独有 +│ void *entry │ +│ rt_uint8_t priority │ +│ ... │ +└──────────────────────────────────────────┘ + +struct rt_device 的内存布局: +┌──────────────────────────────────────────┐ +│ struct rt_object parent 偏移 0 │ ← 每个对象都有 +│ name[8] = "pin\0\0\0\0\0" │ +│ type = 0x09 (Device) │ +│ flag = 0x003 (RDWR) │ +│ list = ←→ 设备链表 │ +├──────────────────────────────────────────┤ 偏移 20 +│ enum rt_device_class_type type │ ← 设备独有 +│ const struct rt_device_ops *ops │ +│ void *user_data │ +└──────────────────────────────────────────┘ +``` + +因为 `parent` 永远在偏移 0,所以: + +```c +rt_device_t dev = rt_device_find("pin"); +rt_object_t obj = (rt_object_t)dev; // 同一地址,不需要任何转换 + +// 现在可以用通用的对象函数操作它: +rt_object_get_name(obj); // → "pin" +rt_object_get_type(obj); // → RT_Object_Class_Device (0x09) +``` + +这就是 C 语言的"多态"——同一套管理代码,管不同的东西。 + +### 2.5 对象管理流程 + +``` +rt_object 管理的是什么? + +rt_object_init(name, type, flag) 创建时:给名字 + 设类型 + 进链表 +rt_object_find(name, type) 查找时:按名字在链表中搜索 +rt_object_detach(obj) 删除时:从链表移除 + +所有内核组件(线程/信号量/设备)复用同一套操作。 +``` + +--- + +## 三、rt_device—— 硬件世界的统一入口 + +### 3.1 没有设备框架之前的世界 + +```c +// ❌ 没有设备框架:直接操作寄存器 +*(volatile rt_uint32_t *)0x4001080C = 0x0C; // GPIOF 时钟使能 +*(volatile rt_uint32_t *)0x40011C00 = 0x00004000; // PF11 = 输出 +*(volatile rt_uint32_t *)0x40011C14 |= 0x00004000; // PF11 = LOW +``` + +问题:地址硬编码、没有统一接口、上层代码耦合到底层寄存器、换芯片全废。 + +### 3.2 有了设备框架之后 + +```c +// ✅ 统一接口 +rt_pin_mode(PF11, PIN_MODE_OUTPUT); +rt_pin_write(PF11, PIN_LOW); +``` + +你不用知道 PF11 对应哪个寄存器,不用知道是 STM32F4 还是 RK3588——换硬件只换驱动,调用方式不变。 + +### 3.3 rt_device 结构体逐字段解读 + +```c +struct rt_device +{ + // ─── 继承自 rt_object ─── + struct rt_object parent; // 名字 + 类型 + flag + 链表节点 + + // ─── 设备自己的字段 ─── + enum rt_device_class_type type; // 设备类别 + rt_uint16_t flag; // 设备属性标志 + rt_uint16_t open_flag; // 当前打开模式 + + rt_uint8_t ref_count; // 引用计数(被 open 了几次) + rt_uint8_t device_id; // 内核内部编号 + + // ─── 回调函数指针 ─── + rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size); // 接收回调 + rt_err_t (*tx_complete)(rt_device_t dev, void *buffer); // 发送完成回调 + + // ─── 核心操作函数表 ─── + const struct rt_device_ops *ops; // init/open/close/read/write/control + + // ─── 驱动私有空间 ─── + void *user_data; // 驱动上下文指针 +}; +``` + +--- + +## 四、设备类型—— 为什么固定模板? + +```c +enum rt_device_class_type { + RT_Device_Class_Char = 0, // 字符设备(UART、Console) + RT_Device_Class_Block = 1, // 块设备(SD 卡、SPI Flash) + RT_Device_Class_NetIf = 2, // 网络接口 + RT_Device_Class_Miscellaneous = 19,// 杂项(Pin、PWM、ADC、DAC) + RT_Device_Class_Sensor = 20, // 传感器 + ... // 共 21 种 +}; +``` + +### 4.1 不会自动配置硬件 + +选 `RT_Device_Class_Pin` 不会帮你写 GPIO 寄存器。它只是一个**分类标签**。 + +### 4.2 实际作用 + +**作用 1:命令空间命名** + +```c +// 不同设备类型的 control 命令在不同的数值区间 +#define RT_DEVICE_CTRL_BASE(Type) (RT_Device_Class_##Type * 0x100) + +RT_Device_Class_Char = 0 → 命令从 0x000 开始(UART 设波特率 = 0x01) +RT_Device_Class_Block = 1 → 命令从 0x100 开始(获取扇区信息 = 0x101) +RT_Device_Class_NetIf = 2 → 命令从 0x200 开始(获取 MAC = 0x201) + +// Pin 设备属于 Miscellaneous(19) → 命令从 0x1900 开始 +// 这样 Pin 的命令(0x1901)不会跟 UART 的命令(0x0001)冲突 +``` + +**作用 2:框架区分处理方式** + +块设备处理扇区对齐,字符设备处理字节流——框架根据类型选择处理逻辑。 + +--- + +## 五、flag—— 位域属性开关 + +```c +RT_DEVICE_FLAG_RDONLY 0x001 // 只读(传感器) +RT_DEVICE_FLAG_WRONLY 0x002 // 只写 +RT_DEVICE_FLAG_RDWR 0x003 // 读写(= RDONLY | WRONLY,Pin/UART 用这个) +RT_DEVICE_FLAG_REMOVABLE 0x004 // 可热插拔(SD 卡) +RT_DEVICE_FLAG_STANDALONE 0x008 // 独立设备(不用 open 就能用) +RT_DEVICE_FLAG_ACTIVATED 0x010 // 已初始化 +RT_DEVICE_FLAG_INT_RX 0x100 // 接收用中断 +RT_DEVICE_FLAG_DMA_RX 0x200 // 接收用 DMA +``` + +### 为什么用位域? + +一个设备可以同时有多个属性。Pin 设备的 flag: + +```c +RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_STANDALONE += 0x003 | 0x008 = 0x00B +``` + +一个数字打包了"可读写 + 独立设备"两个属性。 + +### flag 在源码中的实际效果 + +`rt_device_open` 中: + +```c +// 独立设备不允许被多次 open +if ((dev->flag & RT_DEVICE_FLAG_STANDALONE) && (dev->ref_count > 1)) + return -RT_EBUSY; + +// 未激活的设备自动 init +if (!(dev->flag & RT_DEVICE_FLAG_ACTIVATED)) +{ + result = device_init(dev); // 调用 ops->init + dev->flag |= RT_DEVICE_FLAG_ACTIVATED; +} +``` + +**flag 不是"自动配置"——这是驱动开发者手动填的属性,框架根据它做行为决策。** + +--- + +## 六、ref_count—— 不是数设备数量 + +### 6.1 它数的是"谁打开了这个设备" + +```c +// device.c 源码 +// open 时: +dev->ref_count++; // 0 → 1 + +// close 时: +dev->ref_count--; // 2 → 1 +if (dev->ref_count == 0) +{ + device_close(dev); // 最后一个关闭者才真正关硬件 +} +``` + +### 6.2 为什么要计数? + +你的 UART 同时被两个地方打开: + +``` +系统初始化:rt_device_open("uart1", RDWR) → ref_count: 0 → 1 +你的代码: rt_device_open("uart1", RDWR) → ref_count: 1 → 2 + +你的代码关:rt_device_close("uart1") → ref_count: 2 → 1(UART 不关) +系统退出: rt_device_close("uart1") → ref_count: 1 → 0(UART 真正关闭) +``` + +没有 ref_count,你的代码一 close,控制台就断了。 + +### 6.3 溢出保护 + +```c +dev->ref_count++; +RT_ASSERT(dev->ref_count != 0); +// ↑ 从 255 跳到 0 时触发断言 +``` + +--- + +## 七、device_id—— 不是 UART1/2 + +```c +rt_uint8_t device_id; // 0 ~ 255 +``` + +这是内核内部给设备的**自动编号**。你注册设备时内核自动分配。 + +**UART1/2 靠名称区分**:`rt_device_find("uart1")` vs `rt_device_find("uart2")`。 + +`device_id` 的实际用途是内核对象信息表中的快速索引 + `list_device` 排序。应用层几乎不需要关心。 + +--- + +## 八、rx_indicate 和 tx_complete—— 上下文的"喊话"机制 + +### 8.1 定义 + +```c +rt_err_t (*rx_indicate)(rt_device_t dev, rt_size_t size); +// ↑ 哪个设备 ↑ 收到了多少字节 + +rt_err_t (*tx_complete)(rt_device_t dev, void *buffer); +// ↑ 哪个设备 ↑ 哪块数据发完了 +``` + +### 8.2 实际调用场景—— UART 接收中断 + +``` +硬件收到字节 + │ + ▼ +USARTx_IRQHandler() // 中断入口 + │ + ├─ 字节存入 rx_fifo 环形缓冲区 + │ + └─ if (serial->parent.rx_indicate != RT_NULL) + │ + └─ serial->parent.rx_indicate(&serial->parent, rx_length); + │ + ▼ + finsh_rx_ind() ← 你注册的回调 + │ + └─ rt_sem_release(&rx_sem); // 释放信号量 + │ + ▼ + finsh_thread 被唤醒 // msh 终于能读输入了 +``` + +**`rx_indicate` = 数据来了 = 驱动的中断 → 通知 fns "FIFO 有数据了,你可以读了"** + +**`size` 参数 = 这次收到的字节数**。上层据此决定要读多少。 + +### 8.3 tx_complete 的场景 + +```c +// DMA 发送完成的回调 +if (serial->parent.tx_complete != RT_NULL) +{ + serial->parent.tx_complete(&serial->parent, tx_buffer); +} +``` + +- DMA 模式传完一批数据后触发中断,回调通知上层"buffer 可以释放了" +- 适用于需要确认发送成功的协议(如 Modbus 等对方 ACK 的场景) + +### 8.4 回调 vs 轮询 + +| 方式 | 优点 | 缺点 | +|---|---|---| +| rx_indicate 回调 | 收到数据立刻通知 | 回调在中断上下文,不能做耗时操作 | +| 轮询 rt_device_read | 简单 | 浪费 CPU | + +回调的核心价值:**数据来了我才处理**,而不是一直问"有数据了吗?" + +--- + +## 九、ops—— 不是"延时量",是"操作函数表" + +**ops = operations**。 + +### 9.1 它是什么? + +把设备的 6 个操作打包在一起的结构体: + +```c +struct rt_device_ops +{ + rt_err_t (*init) (rt_device_t dev); + rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag); + rt_err_t (*close) (rt_device_t dev); + rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size); + rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size); + rt_err_t (*control)(rt_device_t dev, int cmd, void *args); +}; +``` + +### 9.2 为什么用 ops 表? + +Pin 设备的 ops: + +```c +const static struct rt_device_ops pin_ops = // 只读,在 Flash 中 +{ + RT_NULL, // init → Pin 不需要特别初始化 + RT_NULL, // open → 不需要 + RT_NULL, // close → 不需要 + _pin_read, // read → 读引脚电平 + _pin_write, // write → 写引脚电平 + _pin_control // control → 设置引脚模式 +}; +``` + +注册时只存一个指针: + +```c +_hw_pin.parent.ops = &pin_ops; // 24 字节(6 个指针) +``` + +**好处**: +1. 省内存:多个 Pin 设备共用同一个 `pin_ops` +2. 只读安全:`const` 修饰,运行时不改变 +3. 统一调用:`dev->ops->read(dev, ...)` 对所有设备都一样 + +### 9.3 本质上是 C 的"虚函数表" + +``` +Python (自动): + class Pin: + def read(self): ... + + pin = Pin() + pin.read() # Python 自动查找 vtable + +C (手动): + struct rt_device_ops pin_ops = {_pin_read, ...}; + dev->ops = &pin_ops; + dev->ops->read(dev, ...) # 手动查表 +``` + +--- + +## 十、user_data—— 驱动上下文的指针 + +```c +void *user_data; +``` + +不是"新数据库",不是"独立数据空间"。它是指向**驱动私有数据**的指针——让上层框架能访问到底层硬件的操作函数。 + +Pin 设备的完整调用链展示它的作用: + +``` +rt_pin_mode(PF11, PIN_MODE_OUTPUT) + │ + ├─ rt_device_find("pin") // 找到设备 + ├─ rt_device_open(pin_dev, RDWR) // open(自动 init) + │ + └─ _pin_control(dev, 0, &mode) // 中间层函数 + │ + ├─ mode = {PF11, PIN_MODE_OUTPUT} + └─ pin = (struct rt_device_pin *)dev // ← 通过 dev 拿到具体设备 + pin->ops->pin_mode(dev, PF11, 0x00) // ← 调更底层的操作 + │ + └─ HAL_GPIO_Init(GPIOF, ...) // ← 真正的寄存器 +``` + +不同设备的 user_data 各不相同: + +| 设备 | user_data 指向 | 存放什么 | +|---|---|---| +| Pin | `struct rt_device_pin` | 底层 GPIO ops 表 | +| UART | `struct rt_serial` | FIFO 缓冲区、波特率配置 | +| LCD | `struct rt_device_graphic` | 显存地址、分辨率 | + +--- + +## 十一、设备完整生命周期 + +``` +rt_device_create(type, attach_size) // ① 分配内存(堆),零初始化 + ↓ +rt_device_register(dev, name, flag) // ② 挂入内核对象链表,可以被 find() + ↓ +rt_device_find(name) // ③ 通过名称查找 + ↓ +rt_device_open(dev, oflag) // ④ 引用计数+1,自动 init(首次) + ↓ +rt_device_read/write/control(...) // ⑤ 六大操作 + ↓ +rt_device_close(dev) // ⑥ 引用计数-1,归零时真正关硬件 + ↓ +rt_device_unregister(dev) // ⑦ 从内核链表移除 + ↓ +rt_device_destroy(dev) // ⑧ 释放内存 +``` + +### 重要区别 + +| 操作 | 之后能被 find() 找到? | +|---|---| +| `rt_device_create` | **否** — 还没注册 | +| `rt_device_register` | **是** | +| `rt_device_unregister` | **否** — 已注销 | + +--- + +## 十二、自定义设备完整示例 + +### 设计目标:一个基于消息队列的自定义设备 + +### Step 1:定义设备类型和命令集 + +```c +#define RT_Device_Class_MyMQ 21 // 自定义设备类型 +#define MYMQ_CMD_SEND 0x10 // control 命令:发送消息 +#define MYMQ_CMD_RECV 0x11 // control 命令:接收消息 +#define MYMQ_CMD_RESET 0x12 // control 命令:重置队列 +``` + +### Step 2:设备私有数据结构 + +```c +struct my_mq_device +{ + struct rt_device parent; // 必须在第一个位置! + void *mq; // 内部消息队列 + rt_uint32_t msg_size; // 每条消息大小 + rt_uint32_t max_msgs; // 队列容量 +}; +``` + +**`parent` 必须在第一个字段**——因为 `rt_device_t` 指针实际指向的就是 `parent` 的首地址。如果 `parent` 不在偏移 0,类型转换会得到错误的地址。 + +### Step 3:实现操作函数 + +```c +static rt_err_t mymq_init(rt_device_t dev) +{ + struct my_mq_device *mq_dev = (struct my_mq_device *)dev; + mq_dev->mq = rt_mq_create("mymq", mq_dev->msg_size, + mq_dev->max_msgs, RT_IPC_FLAG_FIFO); + return (mq_dev->mq) ? RT_EOK : -RT_ERROR; +} + +static rt_err_t mymq_control(rt_device_t dev, int cmd, void *args) +{ + struct my_mq_device *mq_dev = (struct my_mq_device *)dev; + switch (cmd) + { + case MYMQ_CMD_SEND: + return rt_mq_send(mq_dev->mq, args, mq_dev->msg_size); + case MYMQ_CMD_RECV: + return rt_mq_recv(mq_dev->mq, args, mq_dev->msg_size, + RT_WAITING_FOREVER); + case MYMQ_CMD_RESET: + rt_mq_delete(mq_dev->mq); + return mymq_init(dev); + default: + return -RT_EINVAL; + } +} +``` + +### Step 4:组装 ops 表并注册 + +```c +const static struct rt_device_ops mymq_ops = +{ + .init = mymq_init, + .open = RT_NULL, // 不需要 + .close = RT_NULL, + .read = RT_NULL, // 不用 read/write,全走 control + .write = RT_NULL, + .control = mymq_control, +}; + +static struct my_mq_device mymq_dev; + +int mymq_device_init(void) +{ + mymq_dev.parent.type = RT_Device_Class_Pipe; // 选合适的内置类型 + mymq_dev.msg_size = 16; + mymq_dev.max_msgs = 8; + mymq_dev.parent.ops = &mymq_ops; + mymq_dev.parent.user_data = &mymq_dev; + + return rt_device_register(&mymq_dev.parent, "mymq", RT_DEVICE_FLAG_RDWR); +} +``` + +### Step 5:使用 + +```c +// 像操作普通设备一样操作它 +rt_device_t dev = rt_device_find("mymq"); +if (dev) +{ + char msg[16] = "hello"; + rt_device_control(dev, MYMQ_CMD_SEND, msg); +} +``` + +--- + +## 十三、设备框架核心价值总结 + +| 问题 | 没有设备框架 | 有了设备框架 | +|---|---|---| +| 硬件操作代码在哪? | 散落在每个应用的代码里 | 只在驱动层写一次 | +| 换硬件要改什么? | 改所有调用了寄存器的代码 | 只换驱动 | +| 上层用什么接口? | 各不相同 | 统一的 read/write/control | +| 新加一个硬件? | 重写一套操作函数 | 实现 6 个 ops 函数 + 注册 | + +--- + +## 十四、扩展阅读(源码路径) + +| 文件 | 内容 | +|---|---| +| `rt-thread/include/rtdef.h` | `rt_object`、`rt_device`、`rt_device_ops` 定义 | +| `rt-thread/include/rtthread.h` | API 声明(491-521 行) | +| `rt-thread/src/device.c` | 设备框架完整实现(460 行) | +| `rt-thread/components/drivers/misc/pin.c` | Pin 设备完整实现(参考模板) | +| `rt-thread/components/drivers/serial/serial.c` | UART 设备 + rx_indicate 回调 | -- Gitee