diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/aht10_data.h" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/aht10_data.h" new file mode 100644 index 0000000000000000000000000000000000000000..dd1372a9297fcb2ce313027e494fe376c7568f44 --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/aht10_data.h" @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __AHT10_DATA_H__ +#define __AHT10_DATA_H__ + +/** + * @brief 通知其他应用模块已经完成一次温湿度采集。 + * + * @param temperature_x10 温度扩大 10 倍后的整数。 + * @param humidity_x10 湿度扩大 10 倍后的整数。 + */ +void aht10_data_notify(int temperature_x10, int humidity_x10); + +#endif /* __AHT10_DATA_H__ */ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mount_dht_mqtt_demo.c" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mount_dht_mqtt_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..4a618a608e0a86849952e8d07b24b4c248ad642c --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mount_dht_mqtt_demo.c" @@ -0,0 +1,222 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-23 24527 the first version + * 2026-08-23 24527 mount font partition and save AHT10 data + */ + +#include +#include +#include +#include +#include +#include +#include +#include "aht10_data.h" + +#define DBG_TAG "mount.demo" +#define DBG_LVL DBG_LOG +#include + +#define FONT_PARTITION_NAME "font" +#define FONT_MOUNT_PATH "/font" +#define DATA_FILE_PATH "/font/Data.txt" + +static rt_bool_t font_mounted = RT_FALSE; +static rt_uint32_t sample_count = 0; + +/** + * @brief 接收一次温湿度采样结果并追加写入 Data.txt。 + * + * mqtt_aht10.c 每完成一次 AHT10 采集后会调用本函数。 + * 因此同一份数据在上传 MQTT 的同时,也会保存到文件系统。 + * + * @param temperature_x10 温度扩大 10 倍后的整数。 + * @param humidity_x10 湿度扩大 10 倍后的整数。 + */ +void aht10_data_notify(int temperature_x10, int humidity_x10) +{ + char line[96]; + int temperature_decimal; + int humidity_decimal; + int length; + int file; + int write_size; + + sample_count++; + + if (font_mounted == RT_FALSE) + { + LOG_W("font filesystem not mounted, skip sample %u", + (unsigned int)sample_count); + return; + } + + temperature_decimal = temperature_x10 % 10; + humidity_decimal = humidity_x10 % 10; + + if (temperature_decimal < 0) + { + temperature_decimal = -temperature_decimal; + } + + if (humidity_decimal < 0) + { + humidity_decimal = -humidity_decimal; + } + + length = rt_snprintf( + line, + sizeof(line), + "Temp: %d.%d ; Humi: %d.%d ; Count: %u\r\n", + temperature_x10 / 10, + temperature_decimal, + humidity_x10 / 10, + humidity_decimal, + (unsigned int)sample_count); + + if ((length <= 0) || (length >= (int)sizeof(line))) + { + LOG_E("format data failed"); + return; + } + + file = open(DATA_FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0); + + if (file < 0) + { + LOG_E("open '%s' failed", DATA_FILE_PATH); + return; + } + + write_size = write(file, line, length); + close(file); + + if (write_size != length) + { + LOG_E("write '%s' failed", DATA_FILE_PATH); + return; + } + + LOG_D("save sample %u to %s", + (unsigned int)sample_count, + DATA_FILE_PATH); +} + +/** + * @brief 将 W25Q64 的 font 分区挂载到 /font。 + * + * 如果 FAL 尚未创建 font 块设备,则调用 + * fal_blk_device_create() 创建块设备,然后使用 + * Elm-FatFs 挂载到 /font。 + * + * 注意:如果 font 分区尚未格式化,需要先在 MSH 中执行: + * mkfs -t elm font + * + * @return RT_EOK 挂载成功。 + * @return -RT_ERROR 创建块设备或挂载失败。 + */ +static rt_err_t font_filesystem_mount(void) +{ + rt_device_t flash_dev; + int result; + + if (font_mounted == RT_TRUE) + { + return RT_EOK; + } + + fal_init(); + + flash_dev = rt_device_find(FONT_PARTITION_NAME); + + if (flash_dev == RT_NULL) + { + flash_dev = fal_blk_device_create(FONT_PARTITION_NAME); + } + + if (flash_dev == RT_NULL) + { + LOG_E("can't create block device on '%s'", + FONT_PARTITION_NAME); + return -RT_ERROR; + } + + result = dfs_mount( + flash_dev->parent.name, + FONT_MOUNT_PATH, + "elm", + 0, + RT_NULL); + + if (result != RT_EOK) + { + LOG_E("mount '%s' to '%s' failed, error = %d", + FONT_PARTITION_NAME, + FONT_MOUNT_PATH, + result); + LOG_I("if this partition is new, run: mkfs -t elm font"); + return -RT_ERROR; + } + + font_mounted = RT_TRUE; + + LOG_I("partition '%s' mounted to '%s'", + FONT_PARTITION_NAME, + FONT_MOUNT_PATH); + LOG_I("AHT10 data file: %s", DATA_FILE_PATH); + + return RT_EOK; +} + +/** + * @brief 手动挂载 font 分区。 + * + * 当自动挂载失败时,可以完成格式化后再次通过 MSH 执行本命令。 + * + * @return RT_EOK 挂载成功。 + * @return -RT_ERROR 挂载失败。 + */ +static rt_err_t mount_demo(void) +{ + rt_err_t result; + + result = font_filesystem_mount(); + + if (result != RT_EOK) + { + LOG_E("failed to initialize font filesystem"); + return result; + } + + LOG_I("font filesystem initialized successfully"); + + return RT_EOK; +} + +/** + * @brief 上电后自动尝试挂载 font 分区。 + * + * 自动挂载失败不会阻止 MQTT 模块继续启动, + * 之后可以在 MSH 中格式化并手动重新挂载。 + * + * @return RT_EOK 始终返回成功,避免阻塞其他应用初始化。 + */ +static int font_filesystem_auto_init(void) +{ + if (font_filesystem_mount() != RT_EOK) + { + LOG_W("font filesystem auto mount failed"); + LOG_W("format it if needed, then run 'mount_demo'"); + } + + return RT_EOK; +} + +INIT_APP_EXPORT(font_filesystem_auto_init); + +MSH_CMD_EXPORT(mount_demo, mount font partition to /font); diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_aht10.c" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_aht10.c" new file mode 100644 index 0000000000000000000000000000000000000000..f6b10c045e639d676dfacec8818fb81fd3d5ffbe --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_aht10.c" @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-23 RT-Thread first version + */ + +#include +#include +#include +#include +#include "aht10.h" +#include "aht10_data.h" +#include "mqttclient.h" + +/* Wi-Fi 参数,请修改成自己的热点名称和密码 */ +#define WIFI_SSID "" +#define WIFI_PASSWORD "" + +/* MQTT 参数,公共 EMQX 的 1883 端口允许匿名连接 */ +#define MQTT_URL "" +#define MQTT_PORT "" +#define MQTT_CLIENT_ID "" +#define MQTT_USERNAME "" +#define MQTT_PASSWORD "" +#define MQTT_PUBLISH_TOPIC "rtthread/aht10/data" + +/* 传感器、线程和上传参数 */ +#define AHT10_I2C_BUS_NAME "i2c3" +#define THREAD_PRIORITY 20 +#define THREAD_STACK_SIZE 4096 +#define THREAD_TIMESLICE 10 +#define WIFI_RETRY_COUNT 5 +#define UPLOAD_PERIOD_MS 5000 + +/* 线程句柄用于防止重复启动 */ +static rt_thread_t aht10_mqtt_thread = RT_NULL; + +/** + * @brief 连接 Wi-Fi 并等待 DHCP 分配网络地址。 + * + * @return RT_EOK 网络连接成功,-RT_ERROR 网络连接失败。 + */ +static rt_err_t wifi_connect(void) +{ + rt_err_t result; + int retry; + int wait_count; + + if (rt_wlan_is_ready() == RT_TRUE) + { + return RT_EOK; + } + + for (retry = 1; retry <= WIFI_RETRY_COUNT; retry++) + { + rt_kprintf("connect Wi-Fi %s, attempt %d/%d.\n", + WIFI_SSID, retry, WIFI_RETRY_COUNT); + result = rt_wlan_connect(WIFI_SSID, WIFI_PASSWORD); + + if (result == RT_EOK) + { + /* 最多等待 10 秒,直到无线网络获得 IP 地址 */ + for (wait_count = 0; wait_count < 50; wait_count++) + { + if (rt_wlan_is_ready() == RT_TRUE) + { + rt_kprintf("Wi-Fi connected.\n"); + return RT_EOK; + } + rt_thread_mdelay(200); + } + } + + rt_kprintf("Wi-Fi connect failed, error = %d.\n", result); + rt_thread_mdelay(2000); + } + + return -RT_ERROR; +} + +/** + * @brief 创建并连接 MQTT 客户端。 + * + * @return MQTT 客户端句柄,失败时返回 RT_NULL。 + */ +static mqtt_client_t *mqtt_client_connect(void) +{ + mqtt_client_t *client; + int result; + + mqtt_log_init(); + client = mqtt_lease(); + if (client == RT_NULL) + { + rt_kprintf("create MQTT client failed.\n"); + return RT_NULL; + } + + mqtt_set_host(client, MQTT_URL); + mqtt_set_port(client, MQTT_PORT); + mqtt_set_client_id(client, MQTT_CLIENT_ID); + mqtt_set_clean_session(client, 1); + + /* 公共 Broker 无需认证,自建服务器可填写这两个宏 */ + if (MQTT_USERNAME[0] != '\0') + { + mqtt_set_user_name(client, MQTT_USERNAME); + } + if (MQTT_PASSWORD[0] != '\0') + { + mqtt_set_password(client, MQTT_PASSWORD); + } + + result = mqtt_connect(client); + if (result != KAWAII_MQTT_SUCCESS_ERROR) + { + rt_kprintf("connect MQTT server failed, error = %d.\n", result); + return RT_NULL; + } + + rt_kprintf("MQTT connected: %s:%s, client id: %s.\n", + MQTT_URL, MQTT_PORT, MQTT_CLIENT_ID); + return client; +} + +/** + * @brief 采集一次温湿度并发布为 JSON 数据。 + * + * 数据格式为 {"temperature":25.3,"humidity":61.8}。 + * + * @param client MQTT 客户端句柄。 + * @param sensor AHT10 设备句柄。 + * + * @return 0 发布成功,其他值表示失败。 + */ +static int sensor_data_publish(mqtt_client_t *client, aht10_device_t sensor) +{ + mqtt_message_t message; + char payload[96]; + int temperature_x10; + int humidity_x10; + int temperature_decimal; + int humidity_decimal; + int result; + + /* + * 将浮点数据扩大 10 倍转成整数, + * 避免嵌入式 C 库没有开启浮点格式化。 + */ + humidity_x10 = (int)(aht10_read_humidity(sensor) * 10.0f); + temperature_x10 = (int)(aht10_read_temperature(sensor) * 10.0f); + + /* 通知其他应用模块已经完成一次采集 */ + aht10_data_notify(temperature_x10, humidity_x10); + + humidity_decimal = humidity_x10 % 10; + temperature_decimal = temperature_x10 % 10; + + if (humidity_decimal < 0) + { + humidity_decimal = -humidity_decimal; + } + if (temperature_decimal < 0) + { + temperature_decimal = -temperature_decimal; + } + + rt_snprintf(payload, sizeof(payload), + "{%ctemperature%c:%d.%d,%chumidity%c:%d.%d}", + 34, 34, temperature_x10 / 10, temperature_decimal, + 34, 34, humidity_x10 / 10, humidity_decimal); + + rt_memset(&message, 0, sizeof(message)); + message.qos = QOS0; + message.payload = payload; + message.payloadlen = rt_strlen(payload); + result = mqtt_publish(client, MQTT_PUBLISH_TOPIC, &message); + + if (result == KAWAII_MQTT_SUCCESS_ERROR) + { + rt_kprintf("publish success: %s\n", payload); + } + else + { + rt_kprintf("publish failed, error = %d.\n", result); + } + + return result; +} + +/** + * @brief 温湿度采集与 MQTT 上传线程入口。 + * + * 依次连接 Wi-Fi、初始化 AHT10、连接 EMQX, + * 然后每隔 5 秒采集并上传一次数据。 + * + * @param parameter 保留参数,当前未使用。 + */ +static void aht10_mqtt_thread_entry(void *parameter) +{ + aht10_device_t sensor = RT_NULL; + mqtt_client_t *client; + + RT_UNUSED(parameter); + + if (wifi_connect() != RT_EOK) + { + rt_kprintf("network is not ready, demo stopped.\n"); + goto exit; + } + + sensor = aht10_init(AHT10_I2C_BUS_NAME); + if (sensor == RT_NULL) + { + rt_kprintf("initialize AHT10 on %s failed.\n", + AHT10_I2C_BUS_NAME); + goto exit; + } + + rt_kprintf("AHT10 initialized on %s.\n", AHT10_I2C_BUS_NAME); + client = mqtt_client_connect(); + if (client == RT_NULL) + { + goto exit; + } + + while (1) + { + sensor_data_publish(client, sensor); + rt_thread_mdelay(UPLOAD_PERIOD_MS); + } + +exit: + if (sensor != RT_NULL) + { + aht10_deinit(sensor); + } + aht10_mqtt_thread = RT_NULL; +} + +/** + * @brief 启动 AHT10 温湿度采集与 MQTT 上传实验。 + * + * @return RT_EOK 线程启动成功。 + * @return -RT_EBUSY 实验线程已经在运行。 + * @return -RT_ERROR 线程创建或启动失败。 + */ +int aht10_mqtt_demo(void) +{ + rt_err_t result; + + if (aht10_mqtt_thread != RT_NULL) + { + rt_kprintf("AHT10 MQTT demo is already running.\n"); + return -RT_EBUSY; + } + + aht10_mqtt_thread = rt_thread_create( + "ahtmqtt", + aht10_mqtt_thread_entry, + RT_NULL, + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (aht10_mqtt_thread == RT_NULL) + { + rt_kprintf("create AHT10 MQTT thread failed.\n"); + return -RT_ERROR; + } + + result = rt_thread_startup(aht10_mqtt_thread); + if (result != RT_EOK) + { + rt_kprintf("startup AHT10 MQTT thread failed, error = %d.\n", + result); + rt_thread_delete(aht10_mqtt_thread); + aht10_mqtt_thread = RT_NULL; + return -RT_ERROR; + } + + return RT_EOK; +} + + +/* 导出为 MSH 命令 */ +MSH_CMD_EXPORT(aht10_mqtt_demo, collect AHT10 data and publish to EMQX); diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_led_toggle.c" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_led_toggle.c" new file mode 100644 index 0000000000000000000000000000000000000000..675d6969fd2e975eb9da3f5cdb990d836297d4cb --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\344\275\234\344\270\232/mqtt_led_toggle.c" @@ -0,0 +1,415 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-23 24527 the first version + */ + +#include +#include +#include +#include +#include +#include "mqttclient.h" + +/* Wi-Fi 参数,请修改成自己的热点名称和密码 */ +#define WIFI_SSID "" +#define WIFI_PASSWORD "" + +/* MQTT 参数,公共 EMQX 的 1883 端口允许匿名连接 */ +#define MQTT_URL "" +#define MQTT_PORT "" +#define MQTT_CLIENT_ID "" +#define MQTT_USERNAME "" +#define MQTT_PASSWORD "" + +/* 云端向开发板发送 LED 控制指令的主题 */ +#define MQTT_LED_CONTROL_TOPIC "rtthread/led/control" + +/* 线程参数 */ +#define THREAD_PRIORITY 20 +#define THREAD_STACK_SIZE 4096 +#define THREAD_TIMESLICE 10 +#define WIFI_RETRY_COUNT 5 + +/* + * 板载 LED: + * GPIO_LED_B -> PF11 + * GPIO_LED_R -> PF12 + */ +#define GPIO_LED_B GET_PIN(F, 11) +#define GPIO_LED_R GET_PIN(F, 12) + +/* + * RT-Spark 板载 LED 通常为低电平点亮,因此默认写高电平关闭。 + * 如果你的实际硬件为高电平点亮,可将这里改为 PIN_LOW。 + */ +#define LED_DEFAULT_LEVEL PIN_HIGH + +/* LED MQTT 控制线程句柄 */ +static rt_thread_t mqtt_led_thread = RT_NULL; + + +/** + * @brief 初始化板载 LED GPIO。 + * + * 将 PF11 和 PF12 设置为推挽输出模式, + * 并设置为默认关闭状态。 + */ +static void led_gpio_init(void) +{ + rt_pin_mode(GPIO_LED_B, PIN_MODE_OUTPUT); + rt_pin_mode(GPIO_LED_R, PIN_MODE_OUTPUT); + + rt_pin_write(GPIO_LED_B, LED_DEFAULT_LEVEL); + rt_pin_write(GPIO_LED_R, LED_DEFAULT_LEVEL); +} + + +/** + * @brief 翻转指定 LED 的输出电平。 + * + * 读取 LED 当前 GPIO 输出电平,并写入相反电平, + * 从而实现 LED 状态翻转。 + * + * @param pin RT-Thread PIN 编号。 + */ +static void led_toggle(rt_base_t pin) +{ + rt_base_t level; + + level = rt_pin_read(pin); + + if (level == PIN_HIGH) + { + rt_pin_write(pin, PIN_LOW); + } + else + { + rt_pin_write(pin, PIN_HIGH); + } +} + + +/** + * @brief 连接 Wi-Fi 并等待网络就绪。 + * + * 如果网络已经连接,则直接返回。 + * 否则最多进行 WIFI_RETRY_COUNT 次连接尝试。 + * + * @return RT_EOK 网络连接成功。 + * @return -RT_ERROR 网络连接失败。 + */ +static rt_err_t wifi_connect(void) +{ + rt_err_t result; + int retry; + int wait_count; + + if (rt_wlan_is_ready() == RT_TRUE) + { + return RT_EOK; + } + + for (retry = 1; retry <= WIFI_RETRY_COUNT; retry++) + { + rt_kprintf("connect Wi-Fi %s, attempt %d/%d.\n", + WIFI_SSID, + retry, + WIFI_RETRY_COUNT); + + result = rt_wlan_connect(WIFI_SSID, WIFI_PASSWORD); + + if (result == RT_EOK) + { + /* + * 最多等待 10 秒, + * 等待 DHCP 获取 IP 地址。 + */ + for (wait_count = 0; wait_count < 50; wait_count++) + { + if (rt_wlan_is_ready() == RT_TRUE) + { + rt_kprintf("Wi-Fi connected.\n"); + return RT_EOK; + } + + rt_thread_mdelay(200); + } + } + + rt_kprintf("Wi-Fi connect failed, error = %d.\n", result); + rt_thread_mdelay(2000); + } + + return -RT_ERROR; +} + + +/** + * @brief MQTT LED 控制主题消息回调。 + * + * 云端向 MQTT_LED_CONTROL_TOPIC 发布以下字符串: + * + * blue 或 B: + * 翻转 PF11 蓝灯。 + * + * red 或 R: + * 翻转 PF12 红灯。 + * + * all 或 ALL: + * 同时翻转 PF11 和 PF12。 + * + * MQTT Payload 不保证以 '\0' 结尾, + * 因此先根据 payloadlen 复制到本地缓冲区后再解析。 + * + * @param client MQTT 客户端句柄。 + * @param msg MQTT 接收到的消息数据。 + */ +static void led_control_handler(void *client, message_data_t *msg) +{ + char command[32]; + rt_size_t copy_len; + + RT_UNUSED(client); + + if ((msg == RT_NULL) || + (msg->message == RT_NULL) || + (msg->message->payload == RT_NULL)) + { + return; + } + + copy_len = msg->message->payloadlen; + + if (copy_len >= sizeof(command)) + { + copy_len = sizeof(command) - 1; + } + + rt_memcpy(command, + msg->message->payload, + copy_len); + + command[copy_len] = '\0'; + + rt_kprintf("MQTT LED command: %s\n", command); + + /* + * 蓝灯翻转命令。 + */ + if ((rt_strcmp(command, "blue") == 0) || + (rt_strcmp(command, "B") == 0)) + { + led_toggle(GPIO_LED_B); + rt_kprintf("GPIO_LED_B PF11 toggled.\n"); + } + /* + * 红灯翻转命令。 + */ + else if ((rt_strcmp(command, "red") == 0) || + (rt_strcmp(command, "R") == 0)) + { + led_toggle(GPIO_LED_R); + rt_kprintf("GPIO_LED_R PF12 toggled.\n"); + } + /* + * 两个 LED 同时翻转。 + */ + else if ((rt_strcmp(command, "all") == 0) || + (rt_strcmp(command, "ALL") == 0)) + { + led_toggle(GPIO_LED_B); + led_toggle(GPIO_LED_R); + + rt_kprintf("GPIO_LED_B PF11 and GPIO_LED_R PF12 toggled.\n"); + } + else + { + rt_kprintf("unknown LED command: %s\n", command); + } +} + + +/** + * @brief 创建、连接并订阅 MQTT LED 控制主题。 + * + * 使用与 mqtt_aht10.c 相同的 MQTT Broker, + * 但使用独立的 MQTT_CLIENT_ID。 + * + * @return MQTT 客户端句柄。 + * @return RT_NULL 创建、连接或订阅失败。 + */ +static mqtt_client_t *mqtt_led_client_connect(void) +{ + mqtt_client_t *client; + int result; + + mqtt_log_init(); + + client = mqtt_lease(); + + if (client == RT_NULL) + { + rt_kprintf("create MQTT client failed.\n"); + return RT_NULL; + } + + mqtt_set_host(client, MQTT_URL); + mqtt_set_port(client, MQTT_PORT); + mqtt_set_client_id(client, MQTT_CLIENT_ID); + mqtt_set_clean_session(client, 1); + + if (MQTT_USERNAME[0] != '\0') + { + mqtt_set_user_name(client, MQTT_USERNAME); + } + + if (MQTT_PASSWORD[0] != '\0') + { + mqtt_set_password(client, MQTT_PASSWORD); + } + + result = mqtt_connect(client); + + if (result != KAWAII_MQTT_SUCCESS_ERROR) + { + rt_kprintf("connect MQTT server failed, error = %d.\n", + result); + return RT_NULL; + } + + rt_kprintf("MQTT connected: %s:%s, client id: %s.\n", + MQTT_URL, + MQTT_PORT, + MQTT_CLIENT_ID); + + /* + * 订阅 LED 控制主题。 + * 收到消息后由 led_control_handler() 处理。 + */ + result = mqtt_subscribe(client, + MQTT_LED_CONTROL_TOPIC, + QOS0, + led_control_handler); + + if (result != KAWAII_MQTT_SUCCESS_ERROR) + { + rt_kprintf("subscribe '%s' failed, error = %d.\n", + MQTT_LED_CONTROL_TOPIC, + result); + return RT_NULL; + } + + rt_kprintf("subscribe success: %s\n", + MQTT_LED_CONTROL_TOPIC); + + return client; +} + + +/** + * @brief MQTT LED 云端控制线程入口。 + * + * 初始化 LED GPIO,连接 Wi-Fi, + * 随后连接 MQTT Broker 并订阅 LED 控制主题。 + * + * MQTT 客户端内部负责接收服务器消息, + * 收到控制命令时自动调用 led_control_handler()。 + * + * @param parameter 未使用。 + */ +static void mqtt_led_thread_entry(void *parameter) +{ + mqtt_client_t *client; + + RT_UNUSED(parameter); + + led_gpio_init(); + + if (wifi_connect() != RT_EOK) + { + rt_kprintf("network is not ready, MQTT LED demo stopped.\n"); + goto exit; + } + + client = mqtt_led_client_connect(); + + if (client == RT_NULL) + { + goto exit; + } + + rt_kprintf("MQTT LED control ready.\n"); + + /* + * Kawaii MQTT 内部线程负责 MQTT 数据接收, + * 当前线程保持运行即可。 + */ + while (1) + { + rt_thread_mdelay(1000); + } + +exit: + + mqtt_led_thread = RT_NULL; +} + + +/** + * @brief 启动 MQTT 云端 LED 翻转实验。 + * + * 创建 LED 控制线程,并通过 MQTT 订阅接收云端命令。 + * + * @return RT_EOK 线程启动成功。 + * @return -RT_EBUSY 实验已经运行。 + * @return -RT_ERROR 创建或启动线程失败。 + */ +static rt_err_t mqtt_led_toggle_demo(void) +{ + rt_err_t result; + + if (mqtt_led_thread != RT_NULL) + { + rt_kprintf("MQTT LED toggle demo is already running.\n"); + return -RT_EBUSY; + } + + mqtt_led_thread = rt_thread_create( + "mqttled", + mqtt_led_thread_entry, + RT_NULL, + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (mqtt_led_thread == RT_NULL) + { + rt_kprintf("create MQTT LED thread failed.\n"); + return -RT_ERROR; + } + + result = rt_thread_startup(mqtt_led_thread); + + if (result != RT_EOK) + { + rt_kprintf("startup MQTT LED thread failed, error = %d.\n", + result); + + rt_thread_delete(mqtt_led_thread); + mqtt_led_thread = RT_NULL; + + return -RT_ERROR; + } + + return RT_EOK; +} + + +/* 导出为 MSH 命令 */ +MSH_CMD_EXPORT(mqtt_led_toggle_demo, control PF11 and PF12 LEDs by MQTT); + diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_enable.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_enable.png" new file mode 100644 index 0000000000000000000000000000000000000000..aff71b7200de846881167df139d3b819b818764b Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_enable.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_layer.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_layer.png" new file mode 100644 index 0000000000000000000000000000000000000000..c0e0772cad4ea14dfb52d0b6274c2c21f1ddf5c7 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/dfs_layer.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/directory_api.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/directory_api.png" new file mode 100644 index 0000000000000000000000000000000000000000..9df97048841e864bb5c878e3f459055126f7540f Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/directory_api.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/elm_fatfs_enable.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/elm_fatfs_enable.png" new file mode 100644 index 0000000000000000000000000000000000000000..bcefb3442d94b41832ccb8f133d8f4e3fcbff64b Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/elm_fatfs_enable.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_api.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_api.png" new file mode 100644 index 0000000000000000000000000000000000000000..5885990fc30afe1e0bd76f100bcb514c4a627b85 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_api.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_filesystem_enable.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_filesystem_enable.png" new file mode 100644 index 0000000000000000000000000000000000000000..d64f202f52a4455777bc105d94b1cc8b5cb4ce99 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_filesystem_enable.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_framework.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_framework.png" new file mode 100644 index 0000000000000000000000000000000000000000..faceee59492ab9ef8f58b74d1aa9a90511d9c21b Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_framework.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_port.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_port.png" new file mode 100644 index 0000000000000000000000000000000000000000..35c1557a7afe5c517c59d31b8cf7247740a6b612 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fal_port.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fatfs_sector_4096.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fatfs_sector_4096.png" new file mode 100644 index 0000000000000000000000000000000000000000..7c5fb74976e38ddecb771bf140ddd256f7ed0aac Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/fatfs_sector_4096.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/file_api.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/file_api.png" new file mode 100644 index 0000000000000000000000000000000000000000..aa8505119fd2c8b0aeb5a9c1c8d57094a3da97fe Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/file_api.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_package.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_package.png" new file mode 100644 index 0000000000000000000000000000000000000000..bdd21dab8fe58228cc93e73c4f80527d4235c90b Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_package.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_test_config.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_test_config.png" new file mode 100644 index 0000000000000000000000000000000000000000..67416a7348e1efbee952a74b9ca4271522bbde80 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/kawaii_mqtt_test_config.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/mqtt_framework.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/mqtt_framework.png" new file mode 100644 index 0000000000000000000000000000000000000000..b91f4eb2bb56fccdd2698e48b9323e2be6c91cf8 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/mqtt_framework.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/package_directory.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/package_directory.png" new file mode 100644 index 0000000000000000000000000000000000000000..8d649a16e272de4020c089f6c36f08a9550346e7 Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/package_directory.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/rw007_package_config.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/rw007_package_config.png" new file mode 100644 index 0000000000000000000000000000000000000000..b280b9a726f5a4dd9e7b2e4f7a4c2e361e8fa8cf Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/rw007_package_config.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/spi_flash_fal_enable.png" "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/spi_flash_fal_enable.png" new file mode 100644 index 0000000000000000000000000000000000000000..95c72fa72fc5eefe82a489c86f86d1516010c94d Binary files /dev/null and "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\224\350\256\260/images/spi_flash_fal_enable.png" differ diff --git "a/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\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\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\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 0000000000000000000000000000000000000000..bcd0337c456850d91fa1bc8fde98920d60f5527d --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\272\273\345\263\273\350\217\201/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\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,777 @@ +# RT-Thread软件包管理、MQTT与NOR Flash学习 + +今天的内容比前几天更接近一个完整的嵌入式应用系统。前面学习线程、线程同步和线程通信时,主要是在理解RT-Thread“内核怎样管理任务”和“系统怎样管理硬件”;而今天继续往上走,开始接触**软件包管理、MQTT网络通信、FAL Flash抽象层、DFS文件系统以及NOR Flash文件系统挂载**。 + +把这些内容串起来以后,我觉得今天最大的收获不是单独学会了几个配置选项,而是开始看到RT-Thread从底层硬件到应用功能之间的完整链路: + +```text +硬件Flash / SPI / 网络设备 + ↓ +设备驱动 + ↓ +FAL / 网络协议栈 + ↓ +DFS文件系统 / MQTT客户端 + ↓ +软件包 + ↓ +应用程序 +``` + +以前写裸机或者只使用HAL时,很多功能都需要自己从底层一点一点拼起来。RT-Thread则把这些常用能力做成了组件和软件包,通过Kconfig配置以后就能比较自然地组合进工程。今天学完以后,我对“RTOS不只是一个线程调度器”这句话有了更深的理解。 + +--- + +# 一、软件包管理:让第三方功能真正成为工程的一部分 + +## 1.软件包并不是简单复制几个`.c`文件 + +以前在单片机工程里加入第三方库,最常见的方式就是: + +```text +下载源码 + ↓ +复制到工程目录 + ↓ +添加Include Path + ↓ +手动修改宏 + ↓ +解决依赖 + ↓ +参与编译 +``` + +这种方式小项目还能接受,但项目复杂以后,很容易出现版本、依赖和配置混乱。 + +RT-Thread的软件包机制给我的感觉更像是: + +> **软件包不只是源码集合,而是“源码+版本+Kconfig配置+SCons构建+依赖关系”的整体。** + +今天查看RT-Thread Studio使用的Env目录时,可以看到软件包相关目录: + +![RT-Thread Env软件包目录](./images/package_directory.png) + +从目录结构上能看到`packages`和`Kconfig`。也就是说,软件包不是完全游离在RT-Thread工程之外,而是和Env、Kconfig、构建系统结合在一起。 + +应用开发时,我们不需要每次手动把整个库复制进工程,而是可以通过RT-Thread Settings或者menuconfig选择需要的软件包,配置完成以后再由构建系统把对应源码加入工程。 + +所以我现在把RT-Thread软件包理解成: + +```text +软件包源码 ++ +Kconfig配置 ++ +SConscript构建 ++ +版本信息 ++ +依赖项 += +可重复使用的软件模块 +``` + +## 2.以kawaii-mqtt为例理解软件包配置 + +今天MQTT使用的就是`kawaii-mqtt`软件包。 + +在RT-Thread Studio的图形配置界面中,可以直接启用: + +![使能kawaii-mqtt软件包](./images/kawaii_mqtt_package.png) + +配置界面里不仅仅有一个“打开/关闭”开关,还能继续设置: + +```text +MQTT版本 +Keep Alive时间 +线程栈大小 +线程优先级 +线程时间片 +最大Topic长度 +默认Buffer大小 +命令超时时间 +``` + +这些配置把原本需要在源码里修改的宏整理成了Kconfig参数。应用层真正关心的是“我需要怎样的MQTT客户端”,而不是每次打开头文件去找几十个`#define`。 + +软件包还提供测试示例: + +![开启kawaii-mqtt测试示例](./images/kawaii_mqtt_test_config.png) + +这里可以直接设置: + +```text +MQTT Host +MQTT Port +Client ID +Username +Password +Subscribe Topic +Publish Topic +``` + +今天的配置中Broker使用`broker.emqx.io`,端口为`1883`。 + + +另外,今天还接触到了`rw007`这个SPI WiFi驱动软件包。它也很适合用来理解RT-Thread软件包不只是“下载源码”,还会把硬件接口参数一起纳入配置。 + +![RW007软件包配置](./images/rw007_package_config.png) + +从配置界面可以看到,RW007使用`spi2`作为总线,同时还需要配置CS、BOOT0、BOOT1、INT/BUSY和RESET等引脚。当前截图中的参数为: + +```text +RW007 BUS NAME:spi2 +CS pin index:90 +BOOT0 pin index:29 +BOOT1 pin index:90 +INT/BUSY pin index:107 +RESET pin index:111 +SPI Max Hz:30000000 +``` + +这一张图让我对“软件包”和“驱动框架”的关系有了更直观的认识。像RW007这种软件包,上层提供的是WiFi功能,但底层仍然要依赖前面学过的SPI设备和PIN设备。也就是说,它实际形成的是这样一条链路: + +```text +应用程序 + ↓ +RW007软件包 + ↓ +RT-Thread网络组件 + ↓ +SPI设备 + PIN设备 + ↓ +BSP驱动 + ↓ +STM32 HAL + ↓ +RW007硬件 +``` + +所以软件包并没有绕开驱动框架,而是在驱动框架之上继续向上封装。之前学PIN、SPI时看到的是“怎样把硬件接入RT-Thread”,今天看到RW007后则更像是在理解“接入系统的硬件怎样继续被上层软件包利用”。 + +这也让我觉得前几天学的内容开始真正串起来了: + +```text +PIN/SPI驱动框架 + ↓ +RW007 WiFi设备 + ↓ +网络协议栈 + ↓ +kawaii-mqtt + ↓ +MQTT应用 +``` + +从底层引脚到最后向Broker发布消息,中间其实已经跨过了好几层抽象。 + +## 3.我对RT-Thread软件包管理的一点理解 + +今天用完以后,我觉得软件包机制最大的价值不是“下载代码更方便”,而是降低功能整合成本。 + +如果后面一个工程需要: + +```text +AHT10 +MQTT +EasyFlash +WebClient +LVGL +文件系统 +OTA +``` + +如果每个模块都自己手动整理源码位置、头文件路径、依赖关系、宏配置和初始化顺序,工程很快就会变乱。 + +RT-Thread软件包体系则更像: + +```text +选择功能 + ↓ +配置参数 + ↓ +解析依赖 + ↓ +加入工程 + ↓ +编译 +``` + +这让我感觉RT-Thread已经不只是一个实时内核,而是开始提供完整的嵌入式软件生态。 + +--- + +# 二、MQTT:从“设备联网”开始理解发布/订阅模型 + +## 1.MQTT的关键不是Socket,而是Broker和Topic + +以前如果直接使用TCP Socket,我的思路通常是: + +```text +客户端 + ↓ +连接服务器IP:Port + ↓ +send() + ↓ +recv() +``` + +MQTT虽然最终也建立在网络连接之上,但它在应用层增加了一个非常重要的模型: + +```text +发布者 + ↓ +Broker + ↓ +订阅者 +``` + +课堂中的图很好地说明了这个关系: + +![MQTT发布订阅模型](./images/mqtt_framework.png) + +例如一个温度传感器节点发布: + +```text +Temperature = 21.9℃ +``` + +它并不需要知道手机、电脑或者网页客户端的IP地址,只需要把消息发布到Broker的某个Topic。其他客户端只要订阅这个Topic,Broker就会把对应消息转发过去。 + +这让我觉得MQTT一个很重要的价值是: + +> **发布者和订阅者之间是解耦的。** + +## 2.Topic把网络通信变成了“按主题组织消息” + +今天配置kawaii-mqtt时,除了Broker地址,最核心的就是: + +```text +Publish Topic +Subscribe Topic +``` + +例如: + +```text +Publish Topic:rtt-pub123 +Subscribe Topic:rtt-sub123 +``` + +这样一个RT-Thread设备既可以是发布者,也可以同时是订阅者。 + +以后做一个温湿度设备,可以设计: + +```text +device/001/sensor +device/001/status +device/001/cmd +``` + +设备周期上传: + +```text +device/001/sensor +``` + +云端下发控制命令: + +```text +device/001/cmd +``` + +设备执行以后再返回: + +```text +device/001/status +``` + +这样应用逻辑比直接维护多条TCP连接更清晰。 + +它也和前面学习的RT-Thread消息队列有一点相似: + +```text +RT-Thread Message Queue: +系统内部线程之间传消息 + +MQTT Topic: +设备和网络节点之间传消息 +``` + +虽然两者工作层级完全不同,但思想上都有一种“通过约定好的通道传递消息”的感觉。 + +## 3.软件包、网络和业务线程开始连接起来 + +今天学MQTT后,我开始能想象一个真正RT-Thread物联网程序的结构: + +```text +传感器线程 + ↓ +读取温湿度 + ↓ +消息队列/共享数据 + ↓ +MQTT线程 + ↓ +kawaii-mqtt + ↓ +TCP/IP + ↓ +网络设备 + ↓ +Broker +``` + +反过来控制命令则是: + +```text +Broker + ↓ +MQTT Subscribe + ↓ +MQTT回调 + ↓ +消息队列/事件 + ↓ +LED控制线程 + ↓ +GPIO +``` + +这其实正好把前几天学习的线程、IPC、设备驱动、软件包和MQTT串在了一起。 + +--- + +# 三、NOR Flash、FAL与DFS:把Flash变成真正可以使用的文件系统 + +## 1.FAL解决的是“不同Flash如何统一管理” + +今天NOR Flash部分首先接触的是FAL,也就是Flash Abstraction Layer。 + +课堂中的框架图: + +![FAL框架](./images/fal_framework.png) + +从图里可以看到,FAL位于应用和Flash硬件之间。 + +上层可能是: + +```text +OTA +文件系统 +EasyFlash +``` + +下面则可能有: + +```text +Flash1 +Flash2 +... +Flash n +``` + +FAL中间提供: + +```text +Flash管理 +分区管理 +``` + +所以FAL最重要的作用不是“实现文件系统”,而是: + +> **统一不同Flash设备,并在Flash之上增加分区管理。** + +FAL主要API在课堂图中也整理得很清楚: + +![FAL API](./images/fal_api.png) + +Flash设备相关: + +```c +fal_flash_device_find(); +``` + +分区相关: + +```c +fal_partition_find(); +fal_get_partition_table(); +fal_partition_read(); +fal_partition_write(); +fal_partition_erase(); +fal_partition_erase_all(); +fal_show_part_table(); +``` + +另外还可以基于分区创建BLK、MTD、CHAR设备。 + +## 2.FAL移植、本地Flash和W25Q64之间的关系 + +FAL移植的核心是建立Flash设备表和分区表。 + +课堂图: + +![FAL移植结构](./images/fal_port.png) + +可以看到: + +```text +fal_cfg.h + ↓ +设备表/分区表 + ↓ +设备1、设备2、...、设备n + ↓ +Flash1、Flash2、...、Flash n +``` + +也就是说FAL需要知道系统里有哪些Flash、每个Flash叫什么、容量多大、擦除粒度是多少,以及read/write/erase函数是什么。 + +今天配置中还使能了SPI Flash和FAL: + +![SPI Flash和FAL配置](./images/spi_flash_fal_enable.png) + +其中可以看到类似: + +```text +Enable SPI FLASH(W25Q64 spi2) +Enable File System +Enable FAL +``` + +这一步让我对“挂载NOR Flash文件系统”有了更完整的认识。 + +它其实不是直接: + +```text +W25Q64 → FatFS +``` + +而是: + +```text +W25Q64 + ↓ +SPI驱动 + ↓ +Flash驱动 + ↓ +FAL Flash Device + ↓ +FAL Partition + ↓ +Block Device + ↓ +DFS + ↓ +elm FatFS + ↓ +挂载点 +``` + +## 3.DFS和FatFS把Flash真正变成“文件” + +如果FAL负责的是Flash和分区管理,那么DFS负责的就是**文件系统统一接口**。 + +课堂中的结构图: + +![DFS文件系统层次](./images/dfs_layer.png) + +可以看到: + +```text +应用程序 + ↓ +POSIX接口层 + ↓ +DFS虚拟文件系统 + ↓ +ELM FATFS / RomFS / YAFFS2 / UFFS2 / DevFS / 网络功能 + ↓ +块设备 / FTL / 设备 + ↓ +SD Card / SPI FLASH / NAND Flash / 网卡 +``` + +上层统一通过: + +```c +open(); +close(); +read(); +write(); +``` + +访问文件。 + +文件API关系: + +![文件操作API](./images/file_api.png) + +目录操作: + +![目录操作API](./images/directory_api.png) + +包括: + +```c +mkdir(); +opendir(); +readdir(); +closedir(); +``` + +因此,当NOR Flash成功挂载以后,应用层就不再需要自己计算Flash地址、处理扇区和维护数据结构,而是可以像使用普通文件一样: + +```c +int fd = open("/data/test.txt", O_WRONLY | O_CREAT); +write(fd, buffer, len); +close(fd); +``` + +这一步实际上把开发思维从“Flash地址管理”提升到了“文件管理”。 + +--- + +# 四、今天实际配置文件系统时几个值得记住的点 + +## 1.先使能DFS和elm FatFS + +在RT-Thread Components中首先需要使能DFS: + +![使能DFS](./images/dfs_enable.png) + +然后开启elm-chan FatFS: + +![使能elm FatFS](./images/elm_fatfs_enable.png) + +这里容易混淆的一点是: + +```text +DFS != FatFS +``` + +DFS是RT-Thread的虚拟文件系统框架。 + +FatFS只是DFS下面的一种具体文件系统。 + +## 2.FatFS最大扇区大小要和底层设备匹配 + +课堂配置中把: + +```text +Maximum sector size to be handled +``` + +改成: + +```text +4096 +``` + +如图: + +![FatFS最大扇区4096](./images/fatfs_sector_4096.png) + +这里是一个很容易忽略、但实际非常关键的配置。 + +Flash通常以扇区为擦除单位,实际底层块设备也可能暴露4096字节这样的Sector Size。如果FatFS配置支持的最大Sector Size太小,就可能无法正确适配底层块设备。 + +所以这里让我意识到: + +> **文件系统参数并不是随便配置的,它必须和底层存储设备的几何特性对应起来。** + +## 3.让FAL分区成为文件系统的真正载体 + +文件系统配置里还使能: + +```text +Enable FAL filesystem partition based on W25Q64 +Enable filesystem auto mount +``` + +![基于FAL分区使能文件系统](./images/fal_filesystem_enable.png) + +这说明最终FatFS不是直接挂在“整个W25Q64”上,而是挂在FAL管理的某个分区对应设备上。 + +整个路径可以总结成: + +```text +W25Q64硬件 + ↓ +SPI Flash驱动 + ↓ +FAL Flash Device + ↓ +FAL Partition + ↓ +Block Device + ↓ +elm FatFS + ↓ +DFS Mount + ↓ +目录和文件 +``` + +--- + +# 五、把今天三部分内容放到一起以后,我的一些发现 + +今天的三个主题看起来分别是: + +```text +软件包管理 +MQTT +NOR Flash文件系统 +``` + +但把它们放在一起以后,我觉得它们其实是在教我同一个东西: + +> **复杂嵌入式系统不能一直靠应用代码直接操作底层,而需要一层一层把能力抽象和组合起来。** + +MQTT并不是应用自己重新写TCP协议,而是: + +```text +应用 +↓ +kawaii-mqtt软件包 +↓ +网络接口 +``` + +Flash文件也不是应用自己管理扇区,而是: + +```text +应用 +↓ +POSIX/DFS +↓ +FatFS +↓ +FAL +↓ +Flash驱动 +``` + +软件包管理则进一步把这些成熟组件的获取、配置、依赖和构建也统一起来。 + +所以现在我对RT-Thread整个系统的理解开始变成: + +```text + 应用 + │ + ┌──────────────┼──────────────┐ + │ │ │ + MQTT 文件操作 OTA + │ │ │ + 软件包kawaii-mqtt POSIX/DFS FAL + │ │ │ + 网络 FatFS Partition + │ │ │ + 网络设备 Block Device Flash Device + │ └──────┬───────┘ + │ │ + 硬件 SPI NOR Flash +``` + +以前做一个功能时,我常常会从“我要调用哪个函数?”开始想。 + +现在慢慢变成: + +```text +这个功能处在哪一层? +它下面依赖什么框架? +设备对象在哪里? +上层应该通过哪个统一接口访问? +``` + +我觉得这才是今天真正值得记下来的变化。 + +--- + +# 六、今天的实践流程总结 + +如果以后重新做一次“W25Q64+FatFS+MQTT”的工程,我现在会按下面的顺序检查。 + +### 第一步:确认底层设备 + +```text +SPI是否正常 +W25Q64是否识别 +网络设备是否工作 +``` + +### 第二步:使能组件和软件包 + +```text +Enable SPI FLASH +Enable FAL +Enable DFS +Enable elm FatFS +Enable kawaii-mqtt +``` + +### 第三步:检查关键参数 + +```text +Flash容量 +Flash擦除粒度 +FatFS最大Sector Size +FAL分区大小 +挂载点 +MQTT Broker +Port +Publish Topic +Subscribe Topic +``` + +### 第四步:验证FAL + +可以先检查: + +```text +Flash Device能否找到 +Partition能否找到 +分区表是否正确 +读写擦除是否正常 +``` + +### 第五步:验证文件系统 + +重点确认: + +```text +mount是否成功 +目录是否存在 +文件能否创建 +write/read是否正常 +掉电后数据是否保留 +``` + +### 第六步:验证MQTT + +确认: + +```text +Broker连接 +Subscribe成功 +Publish成功 +回调收到消息 +断线重连 +``` + +到这里,一个很基础的物联网数据链路就已经形成: + +```text +传感器采集 + ↓ +本地文件保存 + + +MQTT上传云端 +``` + +这个结构已经非常接近真正项目中的“本地存储+云端同步”模式了。