From bbd34f76643ce0d16274f41e6568cfb69eefe594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B4=BA=E8=BE=BE?= Date: Sun, 23 Aug 2026 12:33:20 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BD=9C=E4=B8=9A=E6=8F=90=E4=BA=A4]=20?= =?UTF-8?q?=E8=B4=BA=E8=BE=BE=E7=AC=AC4=E3=80=815=E5=A4=A9=E4=BD=9C?= =?UTF-8?q?=E4=B8=9A=E4=B8=8E=E5=AD=A6=E4=B9=A0=E7=AC=94=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../README.md" | 240 +++++++++ .../day5_app.c" | 510 ++++++++++++++++++ .../drv_filesystem.c" | 123 +++++ .../README.md" | 221 ++++++++ .../test_dev.c" | 284 ++++++++++ .../test_dev.h" | 42 ++ ...24\345\244\251\347\254\224\350\256\260.md" | 204 +++++++ ...33\345\244\251\347\254\224\350\256\260.md" | 203 +++++++ 8 files changed, 1827 insertions(+) create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_app.c" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/drv_filesystem.c" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.c" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.h" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" create mode 100644 "2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000..4234d40 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,240 @@ +# 第五天作业:AHT10、MQTT 与文件系统联调 + +## 1. 实验目标 + +- 使用 AHT10 软件包通过 `i2c3` 采集温湿度; +- 通过 RW007 连接无线网络,将数据发布到 RK3399 上的 Mosquitto; +- 将 W25Q64 的 `font` 分区格式化为 FAT,并挂载到 `/fal`; +- 将每次有效采样追加写入 `/fal/Data.txt`; +- 订阅控制主题,由 MQTT 指令翻转开发板红色 LED; +- 熟悉 AHT10、Kawaii MQTT、RW007、FAL、DFS 和 FatFs 等软件包及组件。 + +## 2. 提交文件 + +```text +第五天作业/ +├── README.md +├── day5_app.c +└── drv_filesystem.c +``` + +`day5_app.c` 包含传感器采集、Wi-Fi、MQTT、文件追加和 LED 控制逻辑;`drv_filesystem.c` 将自动挂载目标由原来的 `filesystem` 分区改为 `font` 分区。 + +## 3. 系统结构 + +```text +AHT10 --I2C3--> RT-Spark --RW007/Wi-Fi--> Mosquitto(RK3399) + | | + | +--> rsoc26/heda/aht10 + | +--> rsoc26/heda/led/set + | + +--> FAL font 分区 --> FatFs /fal --> Data.txt + +--> PF12 红色 LED +``` + +MQTT Broker 地址为 `10.113.209.208:1883`。账号信息和 Wi-Fi 信息不写入源码,运行时通过 MSH 参数传入。 + +## 4. RT-Thread 配置 + +在 `menuconfig` 中启用以下功能: + +```text +RT_USING_WIFI +RT_USING_DFS +RT_USING_DFS_ELMFAT +RT_USING_FAL +RT_USING_SAL +SAL_USING_LWIP +SAL_USING_POSIX +BSP_USING_AHT21 +BSP_USING_RW007_WLAN +BSP_USING_FS +BSP_USING_FLASH_FATFS +BSP_USING_FLASH_FS_AUTO_MOUNT +PKG_USING_AHT10 +PKG_USING_KAWAII_MQTT +KAWAII_MQTT_NETSOCKET_USE_SAL +``` + +SPI NOR Flash 擦除扇区为 4096 字节,因此设置: + +```text +RT_DFS_ELM_MAX_SECTOR_SIZE=4096 +``` + +RW007 使用与 RT-Spark 板卡一致的引脚: + +| 信号 | 引脚 | PIN 编号 | +| --- | --- | ---: | +| CS / BOOT1 | PF10 | 90 | +| BOOT0 / SPI2_SCK | PB13 | 29 | +| INT/BUSY | PG11 | 107 | +| RESET | PG15 | 111 | + +## 5. 文件系统启动流程 + +本工程的启动过程如下: + +```text +SPI2 驱动初始化 + -> W25Q64 设备注册 + -> FAL 初始化并读取分区表 + -> fal_blk_device_create("font") + -> 注册 font 块设备 + -> DFS 注册 elm 文件系统 + -> 根目录挂载 ROMFS + -> 将 font 以 elm 类型挂载到 /fal + -> day5 应用打开 /fal/Data.txt +``` + +若板级自动挂载尚未生效,应用会主动执行一次 `dfs_mount("font", "/fal", "elm", ...)`。若分区没有 FAT 文件系统,可执行一次: + +```text +day5_format_font CONFIRM +``` + +该命令只格式化 `font` 分区。格式化会清除该分区原有内容,因此仅在首次使用或文件系统损坏时执行。 + +## 6. 编译与烧录结果 + +2026-08-23 完成 SCons 构建: + +```text +Memory region Used Size Region Size %age Used +CODE 375312 B 1 MB 35.79% +RAM1 66912 B 128 KB 51.05% + +text data bss dec +375312 1652 65256 442220 +scons: done building targets. +``` + +重新生成 MDK5 工程后,Keil 5.43 / Arm Compiler 6.24 构建与下载成功: + +```text +Program Size: Code=246064 RO-data=34380 RW-data=1196 ZI-data=64836 +0 Error(s), 7 Warning(s). + +Erase Done. +Programming Done. +Verify OK. +Application running ... +``` + +7 条警告来自 RT-Thread 及软件包的既有代码,本次 `day5_app.c` 没有产生编译警告。 + +## 7. 实验步骤 + +### 7.1 检查设备和无线网络 + +串口参数为 `115200-8-N-1`。进入 MSH 后执行: + +```text +list device +wifi scan +``` + +实测设备列表中包含 `i2c3`、`font`、`W25Q64`、`wspi`、`wlan0` 和 `wlan1`,扫描结果能够发现目标热点。 + +### 7.2 启动采集任务 + +```text +day5_start +``` + +程序等待 DHCP 完成后连接 MQTT,每 5 秒采样一次。AHT10 的异常哨兵值会被丢弃,不增加 Count,也不会写文件或上传云端。 + +### 7.3 RK3399 订阅数据 + +在 RK3399 上执行: + +```bash +mosquitto_sub -h 127.0.0.1 -p 1883 \ + -u -P '' \ + -t 'rsoc26/heda/#' -v +``` + +### 7.4 查看本地文件 + +```text +cat /fal/Data.txt +``` + +### 7.5 云端翻转 LED + +在 RK3399 上执行: + +```bash +mosquitto_pub -h 127.0.0.1 -p 1883 \ + -u -P '' \ + -t 'rsoc26/heda/led/set' -m 'toggle' +``` + +停止采集命令为: + +```text +day5_stop +``` + +## 8. 实测数据 + +### 8.1 串口采集与发布 + +```text +[day5/fs] /fal/Data.txt is ready +[day5/aht10] initialized on i2c3 +[I/WLAN.mgnt] wifi connect success ssid:dgzn123 +[I/WLAN.lwip] Got IP address : 10.113.209.221 +[day5/wifi] connected and network ready +[day5/mqtt] connected to 10.113.209.208:1883 +[day5/mqtt] subscribed: rsoc26/heda/led/set +[day5/mqtt] published {"temperature":32.65,"humidity":56.38,"count":1} +[day5/mqtt] published {"temperature":32.75,"humidity":56.43,"count":2} +[day5/mqtt] published {"temperature":32.73,"humidity":56.40,"count":3} +``` + +### 8.2 Data.txt + +```text +Temp:32.65 ; Humi:56.38 ; Count: 1 +Temp:32.75 ; Humi:56.43 ; Count: 2 +Temp:32.73 ; Humi:56.40 ; Count: 3 +Temp:32.74 ; Humi:56.28 ; Count: 4 +Temp:32.75 ; Humi:56.30 ; Count: 5 +``` + +实测文件连续记录到 Count 20。串口采集工具按 ASCII 解码时会把 UTF-8 全角冒号显示成三个问号,但写入源码和文件中的实际字节为 UTF-8 `EF BC 9A`。 + +### 8.3 MQTT Broker 接收 + +```text +rsoc26/heda/aht10 {"temperature":32.79,"humidity":56.43,"count":13} +rsoc26/heda/aht10 {"temperature":32.80,"humidity":56.46,"count":14} +rsoc26/heda/aht10 {"temperature":32.79,"humidity":56.52,"count":15} +rsoc26/heda/led/set toggle +rsoc26/heda/aht10 {"temperature":32.79,"humidity":56.47,"count":17} +``` + +### 8.4 LED 控制 + +```text +[day5/mqtt] received rsoc26/heda/led/set: toggle +[day5/led] red LED is on +``` + +## 9. 验收结果 + +| 项目 | 实测结果 | 状态 | +| --- | --- | --- | +| AHT10 采集 | I2C3 初始化成功,连续获得有效温湿度 | 通过 | +| Wi-Fi | RW007 扫描、关联、DHCP 均成功 | 通过 | +| MQTT 上传 | RK3399 Mosquitto 收到 JSON 数据 | 通过 | +| 文件系统 | `font` 以 FAT 挂载到 `/fal` | 通过 | +| 数据文件 | `Data.txt` 连续记录温度、湿度和 Count | 通过 | +| 云端控制 | MQTT `toggle` 指令成功翻转红色 LED | 通过 | +| SCons 构建 | 生成 ELF、HEX、BIN | 通过 | +| Keil 构建与烧录 | 0 Error,Verify OK | 通过 | + +## 10. 实验结论 + +本实验完成了从传感器采集、文件持久化到 MQTT 云端通信的完整数据链路。RK3399 可以稳定承担局域网 MQTT Broker,RT-Spark 能通过 RW007 联网并在 DHCP 就绪后建立 MQTT 会话。`font` 分区完成 FAT 挂载,采样结果同时写入 `Data.txt` 和上传 Broker;反向控制主题也能驱动 LED 翻转,全部作业要求均得到实机验证。 diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_app.c" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_app.c" new file mode 100644 index 0000000..d0b449a --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_app.c" @@ -0,0 +1,510 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "aht10.h" +#include "mqttclient.h" + +#define DAY5_BROKER_HOST "10.113.209.208" +#define DAY5_BROKER_PORT "1883" +#define DAY5_CLIENT_ID "rsoc26-heda-spark" +#define DAY5_TOPIC_DATA "rsoc26/heda/aht10" +#define DAY5_TOPIC_LED "rsoc26/heda/led/set" + +#define DAY5_I2C_BUS "i2c3" +#define DAY5_FONT_DEVICE "font" +#define DAY5_MOUNT_POINT "/fal" +#define DAY5_DATA_FILE "/fal/Data.txt" +#define DAY5_LED_PIN GET_PIN(F, 12) +#define DAY5_SAMPLE_PERIOD_MS 5000 + +struct day5_credentials +{ + char wifi_ssid[33]; + char wifi_password[65]; + char mqtt_username[33]; + char mqtt_password[65]; +}; + +static struct day5_credentials day5_credentials; +static mqtt_client_t *day5_mqtt_client = RT_NULL; +static rt_thread_t day5_thread = RT_NULL; +static rt_uint32_t day5_sample_count = 0; +static rt_bool_t day5_led_on = RT_FALSE; +static rt_bool_t day5_running = RT_FALSE; + +static void day5_copy_text(char *destination, + rt_size_t destination_size, + const char *source) +{ + rt_size_t length; + + length = rt_strlen(source); + if (length >= destination_size) + { + length = destination_size - 1; + } + + rt_memcpy(destination, source, length); + destination[length] = '\0'; +} + +static void day5_float_parts(float value, int *whole, int *fraction) +{ + int scaled; + + scaled = (int)(value * 100.0f); + *whole = scaled / 100; + *fraction = scaled % 100; + if (*fraction < 0) + { + *fraction = -*fraction; + } +} + +static void day5_led_init(void) +{ + rt_pin_mode(DAY5_LED_PIN, PIN_MODE_OUTPUT); + day5_led_on = RT_FALSE; + rt_pin_write(DAY5_LED_PIN, PIN_HIGH); +} + +static void day5_led_toggle(void) +{ + day5_led_on = day5_led_on ? RT_FALSE : RT_TRUE; + rt_pin_write(DAY5_LED_PIN, day5_led_on ? PIN_LOW : PIN_HIGH); + rt_kprintf("[day5/led] red LED is %s\n", + day5_led_on ? "on" : "off"); +} + +static void day5_led_message(void *client, message_data_t *message_data) +{ + char payload[64]; + rt_size_t length; + + (void)client; + + if ((message_data == RT_NULL) || + (message_data->message == RT_NULL) || + (message_data->message->payload == RT_NULL)) + { + return; + } + + length = message_data->message->payloadlen; + if (length >= sizeof(payload)) + { + length = sizeof(payload) - 1; + } + + rt_memcpy(payload, message_data->message->payload, length); + payload[length] = '\0'; + + rt_kprintf("[day5/mqtt] received %s: %s\n", + message_data->topic_name, + payload); + + if ((rt_strcmp(payload, "toggle") == 0) || + (rt_strstr(payload, "led_toggle") != RT_NULL)) + { + day5_led_toggle(); + } + else + { + rt_kprintf("[day5/mqtt] ignored command\n"); + } +} + +static rt_err_t day5_connect_wifi(void) +{ + rt_err_t result; + rt_uint32_t waited_ms = 0; + + rt_kprintf("[day5/wifi] connecting to %s...\n", + day5_credentials.wifi_ssid); + + result = rt_wlan_connect(day5_credentials.wifi_ssid, + day5_credentials.wifi_password); + if (result != RT_EOK) + { + rt_kprintf("[day5/wifi] connect failed: %d\n", result); + return result; + } + + while (!rt_wlan_is_ready() && (waited_ms < 30000)) + { + rt_thread_mdelay(200); + waited_ms += 200; + } + + if (!rt_wlan_is_ready()) + { + rt_kprintf("[day5/wifi] DHCP timeout\n"); + return -RT_ETIMEOUT; + } + + rt_kprintf("[day5/wifi] connected and network ready\n"); + return RT_EOK; +} + +static rt_err_t day5_connect_mqtt(void) +{ + int result; + + mqtt_log_init(); + day5_mqtt_client = mqtt_lease(); + if (day5_mqtt_client == RT_NULL) + { + return -RT_ENOMEM; + } + + mqtt_set_host(day5_mqtt_client, DAY5_BROKER_HOST); + mqtt_set_port(day5_mqtt_client, DAY5_BROKER_PORT); + mqtt_set_client_id(day5_mqtt_client, DAY5_CLIENT_ID); + mqtt_set_user_name(day5_mqtt_client, day5_credentials.mqtt_username); + mqtt_set_password(day5_mqtt_client, day5_credentials.mqtt_password); + mqtt_set_clean_session(day5_mqtt_client, 1); + mqtt_set_keep_alive_interval(day5_mqtt_client, 30); + + result = mqtt_connect(day5_mqtt_client); + if (result != 0) + { + rt_kprintf("[day5/mqtt] connect failed: %d\n", result); + return -RT_ERROR; + } + + result = mqtt_subscribe(day5_mqtt_client, + DAY5_TOPIC_LED, + QOS0, + day5_led_message); + if (result != 0) + { + rt_kprintf("[day5/mqtt] subscribe failed: %d\n", result); + return -RT_ERROR; + } + + rt_kprintf("[day5/mqtt] connected to %s:%s\n", + DAY5_BROKER_HOST, + DAY5_BROKER_PORT); + rt_kprintf("[day5/mqtt] subscribed: %s\n", DAY5_TOPIC_LED); + return RT_EOK; +} + +static rt_err_t day5_check_storage(void) +{ + int file; + + file = open(DAY5_DATA_FILE, O_WRONLY | O_CREAT | O_APPEND, 0); + if (file < 0) + { + rt_kprintf("[day5/fs] mounting %s at %s\n", + DAY5_FONT_DEVICE, + DAY5_MOUNT_POINT); + if (dfs_mount(DAY5_FONT_DEVICE, + DAY5_MOUNT_POINT, + "elm", + 0, + RT_NULL) == RT_EOK) + { + file = open(DAY5_DATA_FILE, + O_WRONLY | O_CREAT | O_APPEND, + 0); + } + + if (file < 0) + { + rt_kprintf("[day5/fs] %s is not writable\n", DAY5_MOUNT_POINT); + rt_kprintf("[day5/fs] run: day5_format_font CONFIRM\n"); + return -RT_ERROR; + } + } + + close(file); + rt_kprintf("[day5/fs] %s is ready\n", DAY5_DATA_FILE); + return RT_EOK; +} + +static rt_err_t day5_append_file(float temperature, + float humidity, + rt_uint32_t count) +{ + char line[96]; + int temperature_whole; + int temperature_fraction; + int humidity_whole; + int humidity_fraction; + int file; + int length; + int written; + + day5_float_parts(temperature, + &temperature_whole, + &temperature_fraction); + day5_float_parts(humidity, &humidity_whole, &humidity_fraction); + + length = rt_snprintf(line, + sizeof(line), + "Temp\xEF\xBC\x9A%d.%02d ; Humi\xEF\xBC\x9A%d.%02d ; Count\xEF\xBC\x9A %u\r\n", + temperature_whole, + temperature_fraction, + humidity_whole, + humidity_fraction, + count); + if ((length <= 0) || (length >= (int)sizeof(line))) + { + return -RT_ERROR; + } + + file = open(DAY5_DATA_FILE, O_WRONLY | O_CREAT | O_APPEND, 0); + if (file < 0) + { + return -RT_ERROR; + } + + written = write(file, line, length); + close(file); + + return (written == length) ? RT_EOK : -RT_ERROR; +} + +static rt_err_t day5_publish(float temperature, + float humidity, + rt_uint32_t count) +{ + char payload[128]; + int temperature_whole; + int temperature_fraction; + int humidity_whole; + int humidity_fraction; + int length; + mqtt_message_t message; + + day5_float_parts(temperature, + &temperature_whole, + &temperature_fraction); + day5_float_parts(humidity, &humidity_whole, &humidity_fraction); + + length = rt_snprintf(payload, + sizeof(payload), + "{\"temperature\":%d.%02d,\"humidity\":%d.%02d,\"count\":%u}", + temperature_whole, + temperature_fraction, + humidity_whole, + humidity_fraction, + count); + if ((length <= 0) || (length >= (int)sizeof(payload))) + { + return -RT_ERROR; + } + + rt_memset(&message, 0, sizeof(message)); + message.qos = QOS0; + message.payload = payload; + message.payloadlen = length; + + if (mqtt_publish(day5_mqtt_client, DAY5_TOPIC_DATA, &message) != 0) + { + return -RT_ERROR; + } + + rt_kprintf("[day5/mqtt] published %s\n", payload); + return RT_EOK; +} + +static void day5_worker(void *parameter) +{ + aht10_device_t sensor; + float temperature; + float humidity; + + (void)parameter; + + day5_led_init(); + + if (day5_check_storage() != RT_EOK) + { + goto exit; + } + + sensor = aht10_init(DAY5_I2C_BUS); + if (sensor == RT_NULL) + { + rt_kprintf("[day5/aht10] init failed on %s\n", DAY5_I2C_BUS); + goto exit; + } + rt_kprintf("[day5/aht10] initialized on %s\n", DAY5_I2C_BUS); + + if (day5_connect_wifi() != RT_EOK) + { + aht10_deinit(sensor); + goto exit; + } + + if (day5_connect_mqtt() != RT_EOK) + { + aht10_deinit(sensor); + goto exit; + } + + while (day5_running) + { + temperature = aht10_read_temperature(sensor); + humidity = aht10_read_humidity(sensor); + + if ((temperature < -40.0f) || (temperature > 85.0f) || + (humidity < 0.0f) || (humidity > 100.0f)) + { + rt_kprintf("[day5/aht10] discarded invalid sample: temp=%d, humi=%d\n", + (int)temperature, + (int)humidity); + rt_thread_mdelay(DAY5_SAMPLE_PERIOD_MS); + continue; + } + + day5_sample_count++; + + if (day5_append_file(temperature, + humidity, + day5_sample_count) != RT_EOK) + { + rt_kprintf("[day5/fs] append failed at count %u\n", + day5_sample_count); + } + + if (day5_publish(temperature, + humidity, + day5_sample_count) != RT_EOK) + { + rt_kprintf("[day5/mqtt] publish failed at count %u\n", + day5_sample_count); + } + + rt_thread_mdelay(DAY5_SAMPLE_PERIOD_MS); + } + + mqtt_disconnect(day5_mqtt_client); + mqtt_release(day5_mqtt_client); + day5_mqtt_client = RT_NULL; + aht10_deinit(sensor); + +exit: + day5_running = RT_FALSE; + day5_thread = RT_NULL; + rt_kprintf("[day5] stopped\n"); +} + +static int day5_start(int argc, char **argv) +{ + if (argc != 5) + { + rt_kprintf("usage: day5_start \n"); + return -RT_EINVAL; + } + + if (day5_running) + { + rt_kprintf("[day5] already running\n"); + return -RT_EBUSY; + } + + if ((rt_strlen(argv[1]) >= sizeof(day5_credentials.wifi_ssid)) || + (rt_strlen(argv[2]) >= sizeof(day5_credentials.wifi_password)) || + (rt_strlen(argv[3]) >= sizeof(day5_credentials.mqtt_username)) || + (rt_strlen(argv[4]) >= sizeof(day5_credentials.mqtt_password))) + { + rt_kprintf("[day5] credential is too long\n"); + return -RT_EINVAL; + } + + day5_copy_text(day5_credentials.wifi_ssid, + sizeof(day5_credentials.wifi_ssid), + argv[1]); + day5_copy_text(day5_credentials.wifi_password, + sizeof(day5_credentials.wifi_password), + argv[2]); + day5_copy_text(day5_credentials.mqtt_username, + sizeof(day5_credentials.mqtt_username), + argv[3]); + day5_copy_text(day5_credentials.mqtt_password, + sizeof(day5_credentials.mqtt_password), + argv[4]); + + day5_sample_count = 0; + day5_running = RT_TRUE; + day5_thread = rt_thread_create("day5", + day5_worker, + RT_NULL, + 6144, + 15, + 10); + if (day5_thread == RT_NULL) + { + day5_running = RT_FALSE; + rt_kprintf("[day5] thread creation failed\n"); + return -RT_ENOMEM; + } + + rt_thread_startup(day5_thread); + rt_kprintf("[day5] worker started\n"); + return RT_EOK; +} +MSH_CMD_EXPORT(day5_start, start AHT10 MQTT filesystem and LED demo); + +static int day5_stop(void) +{ + if (!day5_running) + { + rt_kprintf("[day5] is not running\n"); + return RT_EOK; + } + + day5_running = RT_FALSE; + rt_kprintf("[day5] stop requested\n"); + return RT_EOK; +} +MSH_CMD_EXPORT(day5_stop, stop the day5 worker); + +static int day5_format_font(int argc, char **argv) +{ + if ((argc != 2) || (rt_strcmp(argv[1], "CONFIRM") != 0)) + { + rt_kprintf("usage: day5_format_font CONFIRM\n"); + rt_kprintf("warning: this erases the complete '%s' partition\n", + DAY5_FONT_DEVICE); + return -RT_EINVAL; + } + + if (day5_running) + { + rt_kprintf("[day5/fs] stop the worker before formatting\n"); + return -RT_EBUSY; + } + + if (dfs_mkfs("elm", DAY5_FONT_DEVICE) != RT_EOK) + { + rt_kprintf("[day5/fs] format failed\n"); + return -RT_ERROR; + } + + if (dfs_mount(DAY5_FONT_DEVICE, + DAY5_MOUNT_POINT, + "elm", + 0, + RT_NULL) != RT_EOK) + { + rt_kprintf("[day5/fs] mount failed after format\n"); + return -RT_ERROR; + } + + rt_kprintf("[day5/fs] font partition mounted at %s\n", + DAY5_MOUNT_POINT); + return RT_EOK; +} +MSH_CMD_EXPORT(day5_format_font, format font partition after explicit confirmation); diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/drv_filesystem.c" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/drv_filesystem.c" new file mode 100644 index 0000000..56caa74 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/drv_filesystem.c" @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2018-12-13 balanceTWK add sdcard port file + * 2021-05-10 Meco Man fix a bug that cannot use fatfs in the main thread at starting up + * 2021-07-28 Meco Man implement romfs as the root filesystem + */ + +#include +#include +#include +#include + +#if DFS_FILESYSTEMS_MAX < 4 +#error "Please define DFS_FILESYSTEMS_MAX more than 4" +#endif +#if DFS_FILESYSTEM_TYPES_MAX < 4 +#error "Please define DFS_FILESYSTEM_TYPES_MAX more than 4" +#endif + +#define DBG_TAG "app.filesystem" +#define DBG_LVL DBG_INFO +#include + +#ifdef BSP_USING_FS_AUTO_MOUNT +#ifdef BSP_USING_SDCARD_FATFS +static int onboard_sdcard_mount(void) +{ + if (dfs_mount("sd", "/sdcard", "elm", 0, 0) == RT_EOK) + { + LOG_I("SD card mount to '/sdcard'"); + } + else + { + LOG_E("SD card mount to '/sdcard' failed!"); + } + + return RT_EOK; +} +#endif /* BSP_USING_SDCARD_FATFS */ +#endif /* BSP_USING_FS_AUTO_MOUNT */ + +#ifdef BSP_USING_FLASH_FS_AUTO_MOUNT +#ifdef BSP_USING_FLASH_FATFS +#define FS_PARTITION_NAME "font" + +static int onboard_fal_mount(void) +{ + /* 初始化 fal 功能 */ + extern int fal_init(void); + extern struct rt_device *fal_blk_device_create(const char *parition_name); + fal_init(); + /* 在 spi flash 中名为 "font" 的分区上创建一个块设备 */ + struct rt_device *flash_dev = fal_blk_device_create(FS_PARTITION_NAME); + if (flash_dev == NULL) + { + LOG_E("Can't create a block device on '%s' partition.", FS_PARTITION_NAME); + } + else + { + LOG_D("Create a block device on the %s partition of flash successful.", FS_PARTITION_NAME); + } + + /* 挂载 spi flash 中名为 "filesystem" 的分区上的文件系统 */ + if (dfs_mount(flash_dev->parent.name, "/fal", "elm", 0, 0) == 0) + { + LOG_I("Filesystem initialized!"); + } + else + { + LOG_E("Failed to initialize filesystem!"); + LOG_D("You should create a filesystem on the block device first!"); + } + + return RT_EOK; +} +#endif /*BSP_USING_FLASH_FATFS*/ +#endif /*BSP_USING_FLASH_FS_AUTO_MOUNT*/ + + +const struct romfs_dirent _romfs_root[] = +{ +#ifdef BSP_USING_SDCARD_FATFS + {ROMFS_DIRENT_DIR, "sdcard", RT_NULL, 0}, +#endif + +#ifdef BSP_USING_FLASH_FATFS + {ROMFS_DIRENT_DIR, "fal", RT_NULL, 0}, +#endif +}; + +const struct romfs_dirent romfs_root = +{ + ROMFS_DIRENT_DIR, "/", (rt_uint8_t *)_romfs_root, sizeof(_romfs_root) / sizeof(_romfs_root[0]) +}; + +static int filesystem_mount(void) +{ + +#ifdef BSP_USING_FS + if (dfs_mount(RT_NULL, "/", "rom", 0, &(romfs_root)) != 0) + { + LOG_E("rom mount to '/' failed!"); + } + + /* 确保块设备注册成功之后再挂载文件系统 */ + rt_thread_delay(500); +#endif +#ifdef BSP_USING_FS_AUTO_MOUNT + onboard_sdcard_mount(); +#endif /* BSP_USING_FS_AUTO_MOUNT */ + +#ifdef BSP_USING_FLASH_FS_AUTO_MOUNT + onboard_fal_mount(); +#endif + + return RT_EOK; +} +INIT_APP_EXPORT(filesystem_mount); diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000..0d9a0c5 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,221 @@ +# 第四天作业:虚拟 LED 设备驱动框架 + +## 1. 作业目标 + +仿照 RT-Thread PIN 设备的分层方式,设计一个名为 `test_dev` 的虚拟 LED 设备框架。实验不直接驱动 GPIO,而是用内存保存 4 个 LED 的开关与亮度状态,重点验证以下机制: + +- 通用 `rt_device` 对象的注册与查找; +- 专用 `ops` 函数表的定义、注入与调用; +- 应用层、设备框架层和虚拟 BSP 层之间的解耦; +- 对无效 LED 编号、空指针和越界亮度的错误处理。 + +## 2. 提交文件 + +```text +第四天作业/ +├── README.md +├── test_dev.h +└── test_dev.c +``` + +| 文件 | 内容 | +| --- | --- | +| `test_dev.h` | 专用 ops、设备结构和应用 API 声明 | +| `test_dev.c` | 框架转发、虚拟 BSP 实现、设备注册和 MSH 测试命令 | + +## 3. 三层结构 + +```text +应用层 +test_dev_demo / rt_test_dev_set_state / rt_test_dev_toggle + │ + ▼ +设备框架层 +struct rt_device_test + struct rt_test_dev_ops +rt_device_test_register() + 参数检查 + ops 转发 + │ + ▼ +虚拟 BSP 层 +virtual_led_init / set_state / get_state / toggle / set_brightness + │ + ▼ +virtual_led_state(用内存模拟硬件状态) +``` + +框架层只规定“设备能够做什么”,虚拟 BSP 层决定“操作具体怎样完成”。以后换成真实 LED 时,只需重新实现 `rt_test_dev_ops`,应用 API 无需改变。 + +## 4. 核心数据结构 + +设备结构在通用设备对象之后增加专用接口表: + +```c +struct rt_device_test +{ + struct rt_device parent; + const struct rt_test_dev_ops *ops; +}; +``` + +`parent` 放在首成员位置,因此 `struct rt_device_test *` 与其内嵌的 `struct rt_device *` 具有相同起始地址,可以交给 RT-Thread 通用设备管理层注册。 + +专用接口表定义为: + +```c +struct rt_test_dev_ops +{ + rt_err_t (*init)(struct rt_device *device); + rt_err_t (*set_state)(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t enabled); + rt_err_t (*get_state)(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t *enabled); + rt_err_t (*toggle)(struct rt_device *device, rt_uint8_t led_id); + rt_err_t (*set_brightness)(struct rt_device *device, + rt_uint8_t led_id, + rt_uint8_t brightness); +}; +``` + +这些接口比通用 `read/write/control` 更贴近 LED 的业务语义,调用方不需要约定 `buffer` 内部格式。 + +## 5. 注册过程 + +系统在设备初始化阶段执行: + +```text +INIT_DEVICE_EXPORT(rt_hw_test_dev_init) + │ + ▼ +rt_device_test_register("test_dev", &_virtual_led_ops, &_virtual_state) + │ + ├── 检查名称、ops 与同名设备 + ├── 初始化 struct rt_device_test + ├── 保存 ops 和 user_data + └── rt_device_register(..., RT_DEVICE_FLAG_RDWR) +``` + +注册成功后,`list_device` 应能看到 `test_dev`。`user_data` 指向虚拟 LED 的状态数组,BSP 回调通过它访问私有数据。 + +## 6. 调用链分析 + +以 `rt_test_dev_toggle(0)` 为例: + +```text +test_dev_demo() + -> rt_test_dev_toggle(0) 应用 API + -> 检查 LED 编号与回调是否存在 框架层 + -> _test_device.ops->toggle(..., 0) ops 间接调用 + -> virtual_led_toggle(..., 0) 虚拟 BSP + -> 修改 enabled[0] + -> 输出状态日志 +``` + +这种写法的关键不是日志本身,而是调用关系中不存在应用层到某个具体芯片 HAL 函数的直接依赖。 + +## 7. 与通用字符设备的比较 + +| 比较项 | 通用字符设备 | 本作业的专用设备框架 | +| --- | --- | --- | +| 应用 API | `rt_device_read/write/control` | `rt_test_dev_toggle/set_brightness` | +| 参数表达 | `pos + buffer + size` | LED 编号、状态、亮度 | +| ops 来源 | `rt_device` 标准回调 | 自定义 `rt_test_dev_ops` | +| 优点 | 接口统一,适用面广 | 语义明确,易于移植具体实现 | +| 代价 | 需要约定缓冲区格式 | 需要额外维护专用框架 API | + +两种方式不是互相替代:简单字节流设备适合通用接口,而 GPIO、SPI、传感器等具有明显领域操作的设备,常在通用设备对象上增加专用 ops 和 API。 + +## 8. 测试方法 + +将 `test_dev.c` 和 `test_dev.h` 加入 BSP 的 `applications` 目录并重新构建、下载。进入 MSH 后执行: + +```text +list_device +test_dev_demo +``` + +### 8.1 构建结果 + +2026-08-23 已将源码加入 `D:\Code\MCU_Projects\RT-Spark\Empty\applications`,并使用 Arm GNU Toolchain 完成 SCons 构建: + +```text +CC build\applications\test_dev.o +LINK rt-thread.elf + +Memory region Used Size Region Size %age Used +CODE 95788 B 1 MB 9.14% +RAM1 5624 B 128 KB 4.29% + +text data bss dec +95788 1228 4392 101408 +scons: done building targets. +``` + +构建成功生成 `rt-thread.elf`、`rtthread.hex` 和 `rtthread.bin`。 + +同日重新生成 MDK5 工程后,又使用 Keil 5.43 / Arm Compiler 6.24 完成交叉构建与下载: + +```text +compiling test_dev.c... +linking... +Program Size: Code=61122 RO-data=10590 RW-data=772 ZI-data=3748 +0 Error(s), 2 Warning(s). + +Erase Done. +Programming Done. +Verify OK. +Application running ... +``` + +两条警告来自 RT-Thread 自带的 `cwchar.c`,本作业源码没有产生编译警告。Keil 于 11:27:41 完成擦除、编程和校验,固件随后正常启动。 + +### 8.2 设备注册结果 + +开发板连接 COM3,串口参数为 `115200-8-N-1`。执行 `list_device` 后,设备列表如下: + +```text +msh >list_device +device type ref count +-------- -------------------- --------- +test_dev Miscellaneous Device 0 +uart1 Character Device 2 +pin Pin Device 0 +msh > +``` + +`test_dev` 已作为 `Miscellaneous Device` 注册,引用计数为 0,说明 `INIT_DEVICE_EXPORT(rt_hw_test_dev_init)` 在系统启动阶段正确执行,设备已经进入 RT-Thread 设备管理器。 + +### 8.3 功能实测结果 + +执行 `test_dev_demo`,完整串口输出如下: + +```text +msh >test_dev_demo +[test_dev] virtual LED device initialized +[test_dev] LED0 state=on +[test_dev] LED0 brightness=50% +[test_dev] LED0 toggled to off +[test_dev] LED0 final state=off +[test_dev] invalid brightness result=-5 (expected -5) +[test_dev] invalid LED result=-5 (expected -5) +msh > +``` + +正常操作路径依次完成初始化、点亮 LED0、设置 50% 亮度、翻转为关闭状态和状态读回,最终状态为 `off`。亮度 101 和 LED 编号 4 两项越界输入均返回 `-5`,与当前工程中的 `-RT_EINVAL` 一致,说明参数校验生效。 + +## 9. 验收结果 + +| 验收项目 | 实测结果 | 状态 | +| --- | --- | --- | +| 源码完整性 | 头文件、框架层、虚拟 BSP 和 MSH 命令齐全 | 通过 | +| SCons 构建 | 生成 ELF、HEX 和 BIN,源码无编译警告 | 通过 | +| Keil 构建 | Arm Compiler 6.24,0 Error | 通过 | +| 固件下载 | Erase、Programming、Verify 全部成功 | 通过 | +| 设备注册 | `list_device` 显示 `test_dev`,类型为 Miscellaneous | 通过 | +| 正常操作 | LED0 完成 on、50% 亮度、toggle 和状态读回 | 通过 | +| 越界亮度 | 亮度 101 返回 `-RT_EINVAL` | 通过 | +| 越界编号 | LED 编号 4 返回 `-RT_EINVAL` | 通过 | + +## 10. 实验结论 + +本实验完整走通了“自动初始化、设备注册、应用 API、ops 转发、虚拟 BSP、私有状态”的调用链。`test_dev` 能被通用设备管理器查找到,同时通过专用 API 表达 LED 的开关、状态查询、翻转和亮度设置,验证了与 PIN 设备相同的分层与接口注入思想。正常路径和非法参数路径均符合设计,SCons、Keil 构建以及目标板运行验证全部通过。 diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.c" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.c" new file mode 100644 index 0000000..d18ba86 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.c" @@ -0,0 +1,284 @@ +#include "test_dev.h" + +struct virtual_led_state +{ + rt_bool_t enabled[TEST_DEV_LED_COUNT]; + rt_uint8_t brightness[TEST_DEV_LED_COUNT]; +}; + +static struct rt_device_test _test_device; +static struct virtual_led_state _virtual_state; + +static rt_err_t test_dev_check_led(rt_uint8_t led_id) +{ + if (led_id >= TEST_DEV_LED_COUNT) + { + return -RT_EINVAL; + } + + return RT_EOK; +} + +rt_err_t rt_device_test_register(const char *name, + const struct rt_test_dev_ops *ops, + void *user_data) +{ + rt_err_t result; + + if ((name == RT_NULL) || (ops == RT_NULL)) + { + return -RT_EINVAL; + } + + if (rt_device_find(name) != RT_NULL) + { + return -RT_EBUSY; + } + + rt_memset(&_test_device, 0, sizeof(_test_device)); + _test_device.parent.type = RT_Device_Class_Miscellaneous; + _test_device.parent.user_data = user_data; + _test_device.ops = ops; + + result = rt_device_register(&_test_device.parent, + name, + RT_DEVICE_FLAG_RDWR); + if (result != RT_EOK) + { + _test_device.ops = RT_NULL; + } + + return result; +} + +rt_err_t rt_test_dev_init(void) +{ + if ((_test_device.ops == RT_NULL) || + (_test_device.ops->init == RT_NULL)) + { + return -RT_ENOSYS; + } + + return _test_device.ops->init(&_test_device.parent); +} + +rt_err_t rt_test_dev_set_state(rt_uint8_t led_id, rt_bool_t enabled) +{ + if (test_dev_check_led(led_id) != RT_EOK) + { + return -RT_EINVAL; + } + + if ((_test_device.ops == RT_NULL) || + (_test_device.ops->set_state == RT_NULL)) + { + return -RT_ENOSYS; + } + + return _test_device.ops->set_state(&_test_device.parent, + led_id, + enabled ? RT_TRUE : RT_FALSE); +} + +rt_err_t rt_test_dev_get_state(rt_uint8_t led_id, rt_bool_t *enabled) +{ + if ((test_dev_check_led(led_id) != RT_EOK) || (enabled == RT_NULL)) + { + return -RT_EINVAL; + } + + if ((_test_device.ops == RT_NULL) || + (_test_device.ops->get_state == RT_NULL)) + { + return -RT_ENOSYS; + } + + return _test_device.ops->get_state(&_test_device.parent, + led_id, + enabled); +} + +rt_err_t rt_test_dev_toggle(rt_uint8_t led_id) +{ + if (test_dev_check_led(led_id) != RT_EOK) + { + return -RT_EINVAL; + } + + if ((_test_device.ops == RT_NULL) || + (_test_device.ops->toggle == RT_NULL)) + { + return -RT_ENOSYS; + } + + return _test_device.ops->toggle(&_test_device.parent, led_id); +} + +rt_err_t rt_test_dev_set_brightness(rt_uint8_t led_id, + rt_uint8_t brightness) +{ + if ((test_dev_check_led(led_id) != RT_EOK) || (brightness > 100U)) + { + return -RT_EINVAL; + } + + if ((_test_device.ops == RT_NULL) || + (_test_device.ops->set_brightness == RT_NULL)) + { + return -RT_ENOSYS; + } + + return _test_device.ops->set_brightness(&_test_device.parent, + led_id, + brightness); +} + +static struct virtual_led_state *virtual_led_get_data(struct rt_device *device) +{ + return (struct virtual_led_state *)device->user_data; +} + +static rt_err_t virtual_led_init(struct rt_device *device) +{ + struct virtual_led_state *state = virtual_led_get_data(device); + + if (state == RT_NULL) + { + return -RT_ERROR; + } + + rt_memset(state, 0, sizeof(*state)); + rt_kprintf("[test_dev] virtual LED device initialized\n"); + return RT_EOK; +} + +static rt_err_t virtual_led_set_state(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t enabled) +{ + struct virtual_led_state *state = virtual_led_get_data(device); + + if (state == RT_NULL) + { + return -RT_ERROR; + } + + state->enabled[led_id] = enabled; + rt_kprintf("[test_dev] LED%u state=%s\n", + led_id, + enabled ? "on" : "off"); + return RT_EOK; +} + +static rt_err_t virtual_led_get_state(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t *enabled) +{ + struct virtual_led_state *state = virtual_led_get_data(device); + + if (state == RT_NULL) + { + return -RT_ERROR; + } + + *enabled = state->enabled[led_id]; + return RT_EOK; +} + +static rt_err_t virtual_led_toggle(struct rt_device *device, + rt_uint8_t led_id) +{ + struct virtual_led_state *state = virtual_led_get_data(device); + + if (state == RT_NULL) + { + return -RT_ERROR; + } + + state->enabled[led_id] = state->enabled[led_id] ? RT_FALSE : RT_TRUE; + rt_kprintf("[test_dev] LED%u toggled to %s\n", + led_id, + state->enabled[led_id] ? "on" : "off"); + return RT_EOK; +} + +static rt_err_t virtual_led_set_brightness(struct rt_device *device, + rt_uint8_t led_id, + rt_uint8_t brightness) +{ + struct virtual_led_state *state = virtual_led_get_data(device); + + if (state == RT_NULL) + { + return -RT_ERROR; + } + + state->brightness[led_id] = brightness; + rt_kprintf("[test_dev] LED%u brightness=%u%%\n", + led_id, + brightness); + return RT_EOK; +} + +static const struct rt_test_dev_ops _virtual_led_ops = +{ + virtual_led_init, + virtual_led_set_state, + virtual_led_get_state, + virtual_led_toggle, + virtual_led_set_brightness, +}; + +static int rt_hw_test_dev_init(void) +{ + return rt_device_test_register(TEST_DEV_NAME, + &_virtual_led_ops, + &_virtual_state); +} +INIT_DEVICE_EXPORT(rt_hw_test_dev_init); + +static int test_dev_demo(void) +{ + rt_device_t device; + rt_bool_t enabled = RT_FALSE; + rt_err_t result; + + device = rt_device_find(TEST_DEV_NAME); + if (device == RT_NULL) + { + rt_kprintf("[test_dev] device not found\n"); + return -RT_ERROR; + } + + result = rt_test_dev_init(); + if (result != RT_EOK) + { + rt_kprintf("[test_dev] init failed: %d\n", result); + return result; + } + + if ((rt_test_dev_set_state(0, RT_TRUE) != RT_EOK) || + (rt_test_dev_set_brightness(0, 50) != RT_EOK) || + (rt_test_dev_toggle(0) != RT_EOK) || + (rt_test_dev_get_state(0, &enabled) != RT_EOK)) + { + rt_kprintf("[test_dev] normal operation failed\n"); + return -RT_ERROR; + } + + rt_kprintf("[test_dev] LED0 final state=%s\n", + enabled ? "on" : "off"); + + result = rt_test_dev_set_brightness(0, 101); + rt_kprintf("[test_dev] invalid brightness result=%d (expected %d)\n", + result, + -RT_EINVAL); + + result = rt_test_dev_toggle(TEST_DEV_LED_COUNT); + rt_kprintf("[test_dev] invalid LED result=%d (expected %d)\n", + result, + -RT_EINVAL); + + return RT_EOK; +} +MSH_CMD_EXPORT(test_dev_demo, test the virtual LED device framework); diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.h" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.h" new file mode 100644 index 0000000..a9bca07 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev.h" @@ -0,0 +1,42 @@ +#ifndef TEST_DEV_H__ +#define TEST_DEV_H__ + +#include +#include + +#define TEST_DEV_NAME "test_dev" +#define TEST_DEV_LED_COUNT 4U + +struct rt_test_dev_ops +{ + rt_err_t (*init)(struct rt_device *device); + rt_err_t (*set_state)(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t enabled); + rt_err_t (*get_state)(struct rt_device *device, + rt_uint8_t led_id, + rt_bool_t *enabled); + rt_err_t (*toggle)(struct rt_device *device, rt_uint8_t led_id); + rt_err_t (*set_brightness)(struct rt_device *device, + rt_uint8_t led_id, + rt_uint8_t brightness); +}; + +struct rt_device_test +{ + struct rt_device parent; + const struct rt_test_dev_ops *ops; +}; + +rt_err_t rt_device_test_register(const char *name, + const struct rt_test_dev_ops *ops, + void *user_data); + +rt_err_t rt_test_dev_init(void); +rt_err_t rt_test_dev_set_state(rt_uint8_t led_id, rt_bool_t enabled); +rt_err_t rt_test_dev_get_state(rt_uint8_t led_id, rt_bool_t *enabled); +rt_err_t rt_test_dev_toggle(rt_uint8_t led_id); +rt_err_t rt_test_dev_set_brightness(rt_uint8_t led_id, + rt_uint8_t brightness); + +#endif /* TEST_DEV_H__ */ diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000..f88227b --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,204 @@ +# 【RSOC26】+ 第五天:AHT10、MQTT 与文件系统联调 + +今天把 RT-Spark 的传感器、无线网络、MQTT 和文件系统串成了一条完整链路:AHT10 通过 I2C 采集温湿度,RW007 连接 Wi-Fi,数据一份追加到 SPI Flash 的 `Data.txt`,另一份发布到 RK3399 上运行的 Mosquitto;RK3399 再通过 MQTT 主题向开发板发送命令,实现 LED 翻转。 + +## 一、整体方案 + +```text +AHT10 --I2C3--> RT-Spark --RW007--> Wi-Fi --> Mosquitto(RK3399) + | | + | +--> 温湿度主题 + | +--> LED 控制主题 + | + +--> W25Q64/font --> FatFs --> /fal/Data.txt +``` + +本次使用的主题: + +| 主题 | 方向 | 数据 | +| --- | --- | --- | +| `rsoc26/heda/aht10` | 开发板 → Broker | 温度、湿度、采样次数 JSON | +| `rsoc26/heda/led/set` | Broker → 开发板 | `toggle` | + +## 二、RK3399 作为 MQTT 服务器 + +RK3399 Linux 开发板完全可以作为局域网 MQTT Broker。本次在 Debian 系统上安装 Mosquitto 2.0.21,并把配置、持久化数据和日志统一放到 `/home/pi/Mosquitto`。Broker 监听局域网的 1883 端口,关闭匿名访问,使用密码文件认证。 + +相比直接连接公网服务器,局域网 Broker 的优点是搭建和抓包方便、延迟低,断开互联网后局域网实验仍能继续。实际使用时应避免匿名写入,并通过防火墙限制访问范围;如果跨公网使用,还应启用 TLS。 + +服务端验证命令: + +```bash +systemctl status mosquitto +ss -lntp | grep 1883 +mosquitto_sub -h 127.0.0.1 -p 1883 \ + -u <用户名> -P '<密码>' -t 'rsoc26/heda/#' -v +``` + +## 三、AHT10 软件包 + +AHT10 软件包封装了传感器初始化和数据换算。应用只需指定 I2C 总线: + +```c +sensor = aht10_init("i2c3"); +temperature = aht10_read_temperature(sensor); +humidity = aht10_read_humidity(sensor); +``` + +实测发现,上电后的第一次读取可能得到 `-50.00/0.00` 这样的无效哨兵值。因此不能拿到 `float` 后立即计数和上传,而应先做量程判断:温度限制在 `-40~85 ℃`,湿度限制在 `0~100 %RH`。无效样本直接丢弃,Count 只统计真正写入文件并尝试上传的数据。 + +## 四、RW007 与网络 ready 时序 + +RW007 使用 SPI2,板级引脚必须与硬件一致: + +| 信号 | 引脚 | +| --- | --- | +| CS / BOOT1 | PF10 | +| BOOT0 / SCK | PB13 | +| INT/BUSY | PG11 | +| RESET | PG15 | + +只启用软件包但引脚错误时,系统里不会出现 `wlan0/wlan1`,`wifi scan` 会返回错误。引脚修正后,`list device` 能看到 `wspi`、`wlan0` 和 `wlan1`,并能扫描到目标热点。 + +另一个容易忽略的问题是“关联成功”不等于“IP 已就绪”。`rt_wlan_connect()` 可能在 DHCP 获取地址前返回。如果此时马上建立 MQTT TCP 连接,会出现连接失败。正确做法是连接后继续等待: + +```c +while (!rt_wlan_is_ready() && waited_ms < 30000) +{ + rt_thread_mdelay(200); + waited_ms += 200; +} +``` + +实测日志顺序如下: + +```text +[I/WLAN.mgnt] wifi connect success ssid:dgzn123 +[I/WLAN.lwip] Got IP address : 10.113.209.221 +[day5/wifi] connected and network ready +[day5/mqtt] connected to 10.113.209.208:1883 +``` + +## 五、MQTT 数据上传与命令订阅 + +温湿度采用 JSON 上传,便于 Broker 后续接入数据库或可视化平台: + +```json +{"temperature":32.75,"humidity":56.43,"count":2} +``` + +Kawaii MQTT 客户端设置 Broker、端口、Client ID、用户名和密码后建立连接,再订阅 LED 控制主题。回调中的 payload 不保证以 `\0` 结尾,所以要根据 `payloadlen` 复制到本地缓冲区后再比较,避免越界读取。 + +RK3399 下发命令: + +```bash +mosquitto_pub -h 127.0.0.1 -p 1883 \ + -u <用户名> -P '<密码>' \ + -t 'rsoc26/heda/led/set' -m 'toggle' +``` + +开发板实测输出: + +```text +[day5/mqtt] received rsoc26/heda/led/set: toggle +[day5/led] red LED is on +``` + +## 六、文件系统启动流程 + +RT-Thread 文件系统不是直接操作 Flash 地址,而是逐层完成注册和挂载: + +```text +SPI2 驱动 + -> W25Q64 SPI 设备 + -> FAL Flash 设备与分区表 + -> font 块设备 + -> DFS 的 elm/FatFs 文件系统 + -> /fal 挂载点 + -> POSIX open/write/close +``` + +关键组件的职责: + +- FAL:统一 Flash 访问,并按分区表找到 `font`; +- `fal_blk_device_create`:把 Flash 分区包装成 RT-Thread 块设备; +- DFS:管理文件系统类型、挂载点和文件描述符; +- elm/FatFs:解释 FAT 磁盘格式; +- POSIX 层:提供 `open/write/close` 等接口。 + +仅打开 `BSP_USING_FLASH_FATFS` 不代表 elm 组件一定已经启用。本次曾遇到 `File system (elm) was not found`,补开 `RT_USING_DFS_ELMFAT` 后才可以格式化和挂载。W25Q64 的擦除扇区为 4096 字节,因此 `RT_DFS_ELM_MAX_SECTOR_SIZE` 也设置为 4096。 + +首次格式化: + +```text +day5_format_font CONFIRM +[day5/fs] font partition mounted at /fal +``` + +格式化会擦除整个 `font` 分区,只应在首次使用或文件系统损坏时执行。普通重启只需要挂载,不应重复格式化。 + +## 七、Data.txt 追加写入 + +文件以追加方式打开,每次采样写一行: + +```c +file = open("/fal/Data.txt", O_WRONLY | O_CREAT | O_APPEND, 0); +write(file, line, length); +close(file); +``` + +实测内容: + +```text +Temp:32.65 ; Humi:56.38 ; Count: 1 +Temp:32.75 ; Humi:56.43 ; Count: 2 +Temp:32.73 ; Humi:56.40 ; Count: 3 +Temp:32.74 ; Humi:56.28 ; Count: 4 +Temp:32.75 ; Humi:56.30 ; Count: 5 +``` + +使用 `O_APPEND` 可以确保每次写入都从文件尾部开始。每条数据写完立即关闭文件,效率不如长期保持打开,但遇到掉电时更容易保留已经完成的记录,适合本次 5 秒一次的低频采集。 + +## 八、常用 Shell 命令 + +本次实验实际使用了这些命令: + +| 命令 | 用途 | +| --- | --- | +| `help` | 查看已注册 MSH 命令 | +| `list device` | 查看 I2C、SPI、块设备和 WLAN 设备 | +| `wifi scan` | 扫描附近热点 | +| `wifi status` | 查看连接状态 | +| `ifconfig` | 查看 IP、网关和网络接口 | +| `ls /fal` | 查看挂载目录 | +| `cat /fal/Data.txt` | 查看采集记录 | +| `mount font /fal elm` | 手动挂载 font 分区 | +| `mkfs -t elm font` | 使用 Shell 格式化块设备 | +| `ps` | 查看线程状态 | +| `free` | 查看内存占用 | +| `ping 10.113.209.208` | 验证到 Broker 的网络连通性 | + +## 九、实测结果 + +开发板连续发布: + +```text +[day5/mqtt] published {"temperature":32.65,"humidity":56.38,"count":1} +[day5/mqtt] published {"temperature":32.75,"humidity":56.43,"count":2} +[day5/mqtt] published {"temperature":32.73,"humidity":56.40,"count":3} +``` + +RK3399 订阅端收到: + +```text +rsoc26/heda/aht10 {"temperature":32.79,"humidity":56.43,"count":13} +rsoc26/heda/aht10 {"temperature":32.80,"humidity":56.46,"count":14} +rsoc26/heda/led/set toggle +rsoc26/heda/aht10 {"temperature":32.79,"humidity":56.47,"count":17} +``` + +Keil 编译为 0 Error,下载日志显示 `Erase Done`、`Programming Done` 和 `Verify OK`。温湿度采集、文件追加、MQTT 上传和 MQTT 控灯均在实机上验证成功。 + +## 十、总结 + +这次实验的重点不是分别调用几个软件包 API,而是处理组件之间的依赖和启动时序:驱动要先注册设备,FAL 要先得到 Flash 分区,DFS 要先注册 elm,网络要等 DHCP ready,MQTT 才能建立连接。把每层状态通过设备列表、Shell 命令和日志逐项确认后,复杂的联网采集问题就能被拆成若干可验证的小问题。最终 RK3399 稳定承担局域网 MQTT Broker,RT-Spark 完成了“采集—本地保存—云端上传—远程控制”的双向闭环。 diff --git "a/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000..503fe64 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\350\264\272\350\276\276/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,203 @@ +# 第四天学习笔记:RT-Thread I/O 设备框架 + +## 1. 为什么需要 I/O 设备框架 + +不同 MCU 厂商对 GPIO、UART、I2C、SPI 等外设提供的寄存器和 HAL API 并不相同。如果应用层直接调用厂商函数,业务代码会与芯片绑定,移植时需要大面积修改。 + +RT-Thread 在应用和硬件之间增加 I/O 设备管理层: + +```text +应用层:统一 API 或设备专用 API + │ +I/O 设备层:设备对象、注册、查找、生命周期与调用转发 + │ +驱动/BSP 层:实现 ops,对接 HAL 或寄存器 + │ +硬件外设 +``` + +分层后的核心收益是把“接口”和“实现”分开。应用依赖稳定接口,BSP 负责消化芯片差异。 + +## 2. `rt_device` 的作用 + +`struct rt_device` 是 RT-Thread 设备模型的基础对象,包含设备类型、标志、引用计数、回调和私有数据等信息。常见设备类型包括字符设备、块设备、网络接口、PIN、I2C 总线和 SPI 设备。 + +驱动管理接口: + +| 接口 | 作用 | +| --- | --- | +| `rt_device_create` | 动态创建设备对象 | +| `rt_device_register` | 用名称把设备加入设备管理器 | +| `rt_device_unregister` | 从设备管理器注销 | +| `rt_device_destroy` | 释放动态设备对象 | +| `rt_device_find` | 按名称查找设备 | + +应用访问接口: + +| 接口 | 作用 | +| --- | --- | +| `rt_device_open` | 按指定模式打开设备 | +| `rt_device_close` | 关闭设备并更新引用状态 | +| `rt_device_read` | 从设备读取数据 | +| `rt_device_write` | 向设备写入数据 | +| `rt_device_control` | 执行配置或设备专用控制命令 | + +典型生命周期是 `find -> open -> read/write/control -> close`。驱动必须在应用访问前完成注册。 + +## 3. ops:用 C 实现接口与多态 + +ops 是一组函数指针。框架规定函数签名,BSP 填入具体实现,运行时通过函数指针完成转发。例如: + +```c +struct rt_test_dev_ops +{ + rt_err_t (*init)(struct rt_device *device); + rt_err_t (*toggle)(struct rt_device *device, rt_uint8_t led_id); +}; +``` + +框架调用的是 `device->ops->toggle()`,并不知道它最后操作 STM32 GPIO、其他芯片的 GPIO,还是仅修改一段模拟数据。这和面向对象语言中的接口或虚函数表作用相似。 + +设计 ops 时要注意: + +- 接口只表达稳定能力,不暴露芯片寄存器细节; +- 每个可选回调在调用前检查是否为 `RT_NULL`; +- 返回值使用 RT-Thread 错误码; +- 驱动私有状态通过 `user_data` 或包含 `rt_device` 的扩展结构保存; +- 注册失败要返回真实错误,动态分配的对象还要释放。 + +## 4. 初始化导出机制 + +RT-Thread 使用初始化导出宏把函数放入特定链接段,系统启动时按阶段执行。常见顺序可概括为: + +```text +BOARD -> PREV -> DEVICE -> COMPONENT -> ENV -> APP +``` + +- `INIT_BOARD_EXPORT`:板级硬件早期初始化; +- `INIT_DEVICE_EXPORT`:设备驱动初始化; +- `INIT_COMPONENT_EXPORT`:组件初始化; +- `INIT_APP_EXPORT`:应用初始化。 + +设备注册通常应发生在使用它的应用启动之前。具体选择哪个阶段,还要结合驱动所依赖的时钟、总线和组件是否已经就绪。 + +## 5. PIN 设备的三层对接 + +PIN 是专用设备框架的典型实现: + +```text +应用层 +rt_pin_mode / rt_pin_write / rt_pin_read / rt_pin_attach_irq + │ + ▼ +PIN 框架层 +struct rt_device_pin + struct rt_pin_ops +rt_device_pin_register() + rt_pin_* 转发 + │ + ▼ +STM32 BSP +stm32_pin_mode / write / read / attach_irq / irq_enable + │ + ▼ +HAL_GPIO_* / EXTI / NVIC +``` + +`struct rt_device_pin` 在通用设备对象基础上保存 `rt_pin_ops`: + +```c +struct rt_device_pin +{ + struct rt_device parent; + const struct rt_pin_ops *ops; +}; +``` + +应用调用 `rt_pin_write(pin, value)` 时,框架通过 `_hw_pin.ops->pin_write()` 跳转到 BSP。换用不同 MCU 时,只需要提供新的 ops 实现。 + +## 6. 引脚编号与 GET_PIN + +STM32 BSP 通常把端口号和引脚号编码到一个整数中。以 `GET_PIN(C, 5)` 为例,端口 C 编号为 2,引脚号为 5,常见编码结果为: + +```text +(2 << 4) | 5 = 0x25 = 37 +``` + +BSP 再从编号中拆出端口和位号,得到 GPIO 基地址与位掩码。应用只保存逻辑引脚编号,不需要接触寄存器地址。 + +## 7. PIN 中断链路 + +应用侧的基本过程: + +```c +rt_pin_mode(key_pin, PIN_MODE_INPUT_PULLUP); +rt_pin_attach_irq(key_pin, PIN_IRQ_MODE_FALLING, key_irq, RT_NULL); +rt_pin_irq_enable(key_pin, PIN_IRQ_ENABLE); +``` + +中断到达后的调用链: + +```text +GPIO 边沿 + -> EXTIx_IRQHandler + -> HAL_GPIO_EXTI_IRQHandler + -> BSP 查找该 EXTI 对应的 PIN 回调记录 + -> 执行应用注册的 key_irq(args) +``` + +中断回调应尽量短,不执行长延时、复杂日志或可能阻塞的操作。需要较多处理时,可在 ISR 中释放信号量或发送事件,再由线程完成后续工作。机械按键还需要考虑硬件或软件消抖。 + +## 8. I2C 设备访问要点 + +I2C 使用 SDA 和 SCL 两根信号线,支持多设备寻址。常见寄存器读取由两条消息组成:先写寄存器地址,再读数据。 + +```c +struct rt_i2c_msg msgs[2]; + +msgs[0].addr = slave_addr; +msgs[0].flags = RT_I2C_WR; +msgs[0].buf = ® +msgs[0].len = 1; + +msgs[1].addr = slave_addr; +msgs[1].flags = RT_I2C_RD; +msgs[1].buf = data; +msgs[1].len = data_len; + +result = rt_i2c_transfer(bus, msgs, 2); +``` + +成功条件通常是返回实际完成的消息数量,此例应为 2,不能只判断是否为 `RT_EOK`。设备地址通常填写 7 位地址,不要重复拼入读写位。 + +## 9. SPI 总线与设备 + +SPI 将总线控制器和片选设备分开管理。一条 SPI 总线可以挂接多个设备,每个设备拥有独立片选和通信配置。 + +常见流程: + +1. 把 SPI 设备挂到指定总线并指定 CS 引脚; +2. 使用 `rt_spi_configure` 设置位宽、模式、位序和最大频率; +3. 使用 `rt_spi_transfer`、`rt_spi_send_then_recv` 等接口传输; +4. 检查实际传输长度。 + +SPI 模式由 CPOL 和 CPHA 共同决定。配置不匹配时,即使有时钟和片选,也可能读到错误数据。 + +## 10. 第四天作业中的实践 + +本次 `test_dev` 选择虚拟 LED 作为对象,定义 `init/set_state/get_state/toggle/set_brightness` 五个 ops。框架负责校验参数和转发,虚拟 BSP 用数组保存 4 个 LED 的开关及亮度。 + +与直接把回调挂到通用 `rt_device` 的 `read/write/control` 相比,专用 API 的参数语义更清晰,也更接近 PIN 框架。代价是需要额外维护一组接口,因此只有当设备具备稳定且明确的领域操作时才值得增加专用层。 + +## 11. 调试检查清单 + +- `list_device` 是否出现目标设备名; +- 初始化导出阶段是否早于应用访问; +- 设备名是否重复注册; +- `ops` 和具体回调是否为空; +- `open/read/write/control` 的返回值语义是否理解正确; +- I2C 返回消息数、SPI 返回字节数是否满足预期; +- 中断回调中是否存在阻塞操作; +- 设备私有状态是否有明确的生命周期和并发保护策略。 + +## 12. 小结 + +第四天的核心不是记忆某个 HAL 函数,而是理解从应用 API 到实际硬件的完整调用链。`rt_device` 提供统一管理入口,专用 ops 描述设备能力,BSP 实现具体操作,初始化导出机制把设备注册进系统。掌握这一模型后,GPIO、I2C、SPI 和传感器驱动都可以按相同思路拆分和定位问题。 -- Gitee