diff --git "a/2026/\347\254\2544\347\273\204/\351\207\221\351\230\263\351\230\263/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/task5.md" "b/2026/\347\254\2544\347\273\204/\351\207\221\351\230\263\351\230\263/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/task5.md" new file mode 100644 index 0000000000000000000000000000000000000000..a9b7f980e9f8f3a879aa10f7e808c8198bcfaa12 --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\207\221\351\230\263\351\230\263/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/task5.md" @@ -0,0 +1,738 @@ +## 一、项目概述 + +本工程基于 RT-Thread 操作系统运行在 STM32F407(Spark One 开发板)上,实现了完整的物联网数据采集与远程控制功能: + +- **温湿度采集**:AHT10 传感器挂接在软件 I2C3 总线(PE0: SCL, PE1: SDA),每 5 秒采集一次 +- **本地存储**:采集数据写入 SPI Flash (W25Q64) 上的 `/font/Data.txt` 文件 +- **远程上报**:通过 MQTT 协议将数据发布到 `spark/aht10/data` 主题 +- **远程控制**:订阅 `spark/aht10/cmd/led` 主题,支持 `toggle`/`on`/`off` 命令控制板载红色 LED (PF12) +- **无线通信**:通过 RW007 SPI WiFi 模块接入网络 + +## 二、项目结构 + +``` +05_task/ +├── applications/ # 应用层代码 +│ ├── main.c # 主程序(核心业务逻辑) +│ ├── aht10.h # AHT10 头文件包装 +│ ├── paho_mqtt.h # MQTT 头文件包装 +│ ├── MQTTPacket.h # MQTTPacket 头文件包装 +│ └── SConscript # 构建脚本 +├── board/ # 板级配置 +│ ├── board.c # 系统时钟配置 +│ ├── board.h # 板级头文件 +│ └── CubeMX_Config/ # CubeMX 配置 +├── middleware_aht10/ # AHT10 传感器中间件 +├── middleware_pahomqtt/ # Paho MQTT 中间件 +├── middleware_rw007/ # RW007 WiFi 中间件 +├── rtconfig.h # RT-Thread 配置 +└── SConstruct # 顶层构建脚本 +``` + +## 三、核心代码 + +### 3.1 主应用层 main.c(applications/main.c) + +```c +#include +#include +#include +#include +#include +#include + +#include "aht10.h" +#include "paho_mqtt.h" + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +#define PIN_LED_R GET_PIN(F, 12) +#define AHT10_I2C_BUS "i2c3" + +#define TELEMETRY_STACK_SIZE 2048 +#define TELEMETRY_PRIORITY 25 +#define TELEMETRY_TIMESLICE 5 +#define TELEMETRY_PERIOD_MS 5000 + +#define DATA_FILE_PATH "/font/Data.txt" +#define MQTT_DATA_TOPIC "spark/aht10/data" +#define MQTT_LED_TOPIC "spark/aht10/cmd/led" +#define MQTT_LED_STATUS_TOPIC "spark/aht10/status/led" + +static MQTTClient mqtt_client; +static volatile rt_bool_t mqtt_started; +static volatile rt_bool_t led_is_on; +static char mqtt_uri[128]; +static char mqtt_username[64]; +static char mqtt_password[64]; +static char mqtt_client_id[32]; + +/* LED 控制:板载 RGB LED 为低电平点亮 */ +static void led_set(rt_bool_t on) +{ + led_is_on = on; + rt_pin_write(PIN_LED_R, on ? PIN_LOW : PIN_HIGH); +} + +static void led_toggle(void) +{ + led_set(!led_is_on); + LOG_I("red led: %s", led_is_on ? "on" : "off"); +} + +/* 数据格式化:将温湿度转为 "Time: ... ; Temp: xx.x ; Humi: xx.x ; Count: n\r\n" */ +static void telemetry_format(char *buffer, rt_size_t size, + float temperature, float humidity, + rt_uint32_t count) +{ + time_t now; + struct tm time_info; + rt_int32_t temperature_x10 = (rt_int32_t)(temperature * 10.0f); + rt_int32_t humidity_x10 = (rt_int32_t)(humidity * 10.0f); + rt_int32_t temperature_abs = temperature_x10 < 0 ? -temperature_x10 : temperature_x10; + rt_int32_t humidity_abs = humidity_x10 < 0 ? -humidity_x10 : humidity_x10; + + now = time(RT_NULL); + localtime_r(&now, &time_info); + + rt_snprintf(buffer, size, + "Time: %04d-%02d-%02d %02d:%02d:%02d ; Temp: %s%ld.%ld ; Humi: %s%ld.%ld ; Count: %lu\r\n", + time_info.tm_year + 1900, time_info.tm_mon + 1, time_info.tm_mday, + time_info.tm_hour, time_info.tm_min, time_info.tm_sec, + temperature_x10 < 0 ? "-" : "", + (long)(temperature_abs / 10), (long)(temperature_abs % 10), + humidity_x10 < 0 ? "-" : "", + (long)(humidity_abs / 10), (long)(humidity_abs % 10), + (unsigned long)count); +} + +/* 数据落盘:以追加方式写入 /font/Data.txt */ +static void telemetry_store(const char *record) +{ + int fd; + int length = rt_strlen(record); + + fd = open(DATA_FILE_PATH, O_WRONLY | O_CREAT | O_APPEND, 0); + if (fd < 0) + { + LOG_E("open %s failed; format and mount the font partition first", DATA_FILE_PATH); + return; + } + + if (write(fd, record, length) != length) + { + LOG_E("write %s failed", DATA_FILE_PATH); + } + + close(fd); +} + +/* MQTT 文本发布工具函数 */ +static void mqtt_publish_text(const char *topic, const char *text) +{ + MQTTMessage message; + + if (!mqtt_client.isconnected) + { + return; + } + + rt_memset(&message, 0, sizeof(message)); + message.qos = QOS1; + message.payload = (void *)text; + message.payloadlen = rt_strlen(text); + + if (MQTTPublish(&mqtt_client, topic, &message) != PAHO_SUCCESS) + { + LOG_W("MQTT status publish failed"); + } +} + +/* LED 命令回调:处理 toggle / on / off 指令 */ +static void mqtt_led_command(MQTTClient *client, MessageData *message_data) +{ + const char *payload = (const char *)message_data->message->payload; + size_t length = message_data->message->payloadlen; + char status[16]; + + (void)client; + + if (length == 6 && rt_memcmp(payload, "toggle", 6) == 0) + { + led_toggle(); + } + else if (length == 2 && rt_memcmp(payload, "on", 2) == 0) + { + led_set(RT_TRUE); + } + else if (length == 3 && rt_memcmp(payload, "off", 3) == 0) + { + led_set(RT_FALSE); + } + else + { + LOG_W("unsupported LED command: %.*s", (int)length, payload); + mqtt_publish_text(MQTT_LED_STATUS_TOPIC, "error: use toggle, on, or off"); + return; + } + + rt_snprintf(status, sizeof(status), "red led: %s", led_is_on ? "on" : "off"); + LOG_I("MQTT command accepted, %s", status); + mqtt_publish_text(MQTT_LED_STATUS_TOPIC, status); +} + +/* MQTT 上线回调 */ +static void mqtt_online(MQTTClient *client) +{ + (void)client; + LOG_I("MQTT online, publish: %s, subscribe: %s", MQTT_DATA_TOPIC, MQTT_LED_TOPIC); + mqtt_publish_text(MQTT_LED_STATUS_TOPIC, "red led: online"); +} + +/* 温湿度采集线程:每 5 秒读取一次并存储 / 发布 */ +static void telemetry_thread_entry(void *parameter) +{ + aht10_device_t aht10; + rt_uint32_t count = 0; + + (void)parameter; + + aht10 = aht10_init(AHT10_I2C_BUS); + if (aht10 == RT_NULL) + { + LOG_E("AHT10 init failed on %s", AHT10_I2C_BUS); + return; + } + + while (RT_TRUE) + { + char record[96]; + float humidity = aht10_read_humidity(aht10); + float temperature = aht10_read_temperature(aht10); + + count++; + telemetry_format(record, sizeof(record), temperature, humidity, count); + telemetry_store(record); + LOG_I("%s", record); + + if (mqtt_started && mqtt_client.isconnected) + { + if (paho_mqtt_publish(&mqtt_client, QOS1, MQTT_DATA_TOPIC, record) != PAHO_SUCCESS) + { + LOG_W("MQTT publish failed"); + } + } + + rt_thread_mdelay(TELEMETRY_PERIOD_MS); + } +} + +/* MSH 命令:启动 MQTT 客户端 */ +static int iot_mqtt_start(int argc, char **argv) +{ + MQTTPacket_connectData connect_data = MQTTPacket_connectData_initializer; + + if (argc != 2 && argc != 4) + { + rt_kprintf("iot_mqtt_start [username password]\n"); + return -RT_EINVAL; + } + + if (mqtt_started) + { + rt_kprintf("MQTT client is already running.\n"); + return -RT_EBUSY; + } + + rt_strncpy(mqtt_uri, argv[1], sizeof(mqtt_uri) - 1); + mqtt_uri[sizeof(mqtt_uri) - 1] = '\0'; + rt_snprintf(mqtt_client_id, sizeof(mqtt_client_id), "spark%08lx", + (unsigned long)rt_tick_get()); + + rt_memset(&mqtt_client, 0, sizeof(mqtt_client)); + mqtt_client.uri = mqtt_uri; + mqtt_client.condata = connect_data; + mqtt_client.condata.clientID.cstring = mqtt_client_id; + mqtt_client.condata.keepAliveInterval = 30; + mqtt_client.condata.cleansession = 1; + + if (argc == 4) + { + rt_strncpy(mqtt_username, argv[2], sizeof(mqtt_username) - 1); + mqtt_username[sizeof(mqtt_username) - 1] = '\0'; + rt_strncpy(mqtt_password, argv[3], sizeof(mqtt_password) - 1); + mqtt_password[sizeof(mqtt_password) - 1] = '\0'; + mqtt_client.condata.username.cstring = mqtt_username; + mqtt_client.condata.password.cstring = mqtt_password; + } + + mqtt_client.buf_size = 1024; + mqtt_client.readbuf_size = 1024; + mqtt_client.buf = rt_calloc(1, mqtt_client.buf_size); + mqtt_client.readbuf = rt_calloc(1, mqtt_client.readbuf_size); + if (mqtt_client.buf == RT_NULL || mqtt_client.readbuf == RT_NULL) + { + rt_free(mqtt_client.buf); + rt_free(mqtt_client.readbuf); + rt_kprintf("No memory for MQTT buffers.\n"); + return -RT_ENOMEM; + } + + mqtt_client.online_callback = mqtt_online; + /* Paho 在断开时会释放 topicFilter,必须用堆内存 */ + mqtt_client.messageHandlers[0].topicFilter = rt_strdup(MQTT_LED_TOPIC); + if (mqtt_client.messageHandlers[0].topicFilter == RT_NULL) + { + rt_free(mqtt_client.buf); + rt_free(mqtt_client.readbuf); + rt_kprintf("No memory for MQTT topic.\n"); + return -RT_ENOMEM; + } + mqtt_client.messageHandlers[0].callback = mqtt_led_command; + mqtt_client.messageHandlers[0].qos = QOS1; + + if (paho_mqtt_start(&mqtt_client) != PAHO_SUCCESS) + { + rt_free(mqtt_client.messageHandlers[0].topicFilter); + mqtt_client.messageHandlers[0].topicFilter = RT_NULL; + rt_free(mqtt_client.buf); + rt_free(mqtt_client.readbuf); + rt_kprintf("MQTT client start failed.\n"); + return -RT_ERROR; + } + + mqtt_started = RT_TRUE; + rt_kprintf("MQTT worker started.\n"); + return RT_EOK; +} +MSH_CMD_EXPORT(iot_mqtt_start, start MQTT: iot_mqtt_start [user password]); + +/* MSH 命令:查询 MQTT 状态 */ +static void iot_mqtt_status(void) +{ + rt_kprintf("mqtt started=%d connected=%d\n", mqtt_started, mqtt_client.isconnected); +} +MSH_CMD_EXPORT(iot_mqtt_status, show MQTT state); + +/* MSH 命令:手动切换 LED */ +MSH_CMD_EXPORT(led_toggle, toggle the red LED); + +/* main 函数:初始化 LED 并启动采集线程 */ +int main(void) +{ + rt_thread_t telemetry_thread; + + rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); + led_set(RT_FALSE); + + telemetry_thread = rt_thread_create("telemetry", + telemetry_thread_entry, + RT_NULL, + TELEMETRY_STACK_SIZE, + TELEMETRY_PRIORITY, + TELEMETRY_TIMESLICE); + if (telemetry_thread == RT_NULL) + { + LOG_E("telemetry thread creation failed"); + return -RT_ENOMEM; + } + + rt_thread_startup(telemetry_thread); + return 0; +} +``` + +### 3.2 AHT10 传感器驱动核心代码(middleware_aht10/aht10.c.reference 关键片段) + +```c +#define AHT10_ADDR 0x38 // I2C 设备地址(GND 连接) +#define AHT10_CALIBRATION_CMD 0xE1 // 校准命令 +#define AHT10_NORMAL_CMD 0xA8 // 普通模式命令 +#define AHT10_GET_DATA 0xAC // 读取数据命令 + +/* I2C 写寄存器 */ +static rt_err_t write_reg(struct rt_i2c_bus_device *bus, rt_uint8_t reg, rt_uint8_t *data) +{ + rt_uint8_t buf[3]; + buf[0] = reg; + buf[1] = data[0]; + buf[2] = data[1]; + + if (rt_i2c_master_send(bus, AHT10_ADDR, 0, buf, 3) == 3) + return RT_EOK; + else + return -RT_ERROR; +} + +/* I2C 读数据 */ +static rt_err_t read_regs(struct rt_i2c_bus_device *bus, rt_uint8_t len, rt_uint8_t *buf) +{ + struct rt_i2c_msg msgs; + msgs.addr = AHT10_ADDR; + msgs.flags = RT_I2C_RD; + msgs.buf = buf; + msgs.len = len; + + if (rt_i2c_transfer(bus, &msgs, 1) == 1) + return RT_EOK; + else + return -RT_ERROR; +} + +/* 传感器初始化:先发普通命令,再发校准命令 */ +static rt_err_t sensor_init(aht10_device_t dev) +{ + rt_uint8_t temp[2] = {0, 0}; + + write_reg(dev->i2c, AHT10_NORMAL_CMD, temp); + rt_thread_delay(rt_tick_from_millisecond(500)); + + temp[0] = 0x08; + temp[1] = 0x00; + write_reg(dev->i2c, AHT10_CALIBRATION_CMD, temp); + rt_thread_delay(rt_tick_from_millisecond(450)); + + return RT_EOK; +} + +/* 校准使能检测 */ +static rt_uint8_t calibration_enabled(aht10_device_t dev) +{ + rt_uint8_t val = 0; + read_regs(dev->i2c, 1, &val); + + if ((val & 0x68) == 0x08) + return RT_EOK; + else + return RT_ERROR; +} + +/* 读取温度(带互斥锁保护) */ +static float read_hw_temperature(aht10_device_t dev) +{ + rt_uint8_t temp[6]; + float cur_temp = -50.0; + rt_err_t result; + + RT_ASSERT(dev); + + result = rt_mutex_take(dev->lock, RT_WAITING_FOREVER); + if (result == RT_EOK) + { + rt_uint8_t cmd[2] = {0, 0}; + write_reg(dev->i2c, AHT10_GET_DATA, cmd); + + result = calibration_enabled(dev); + if (result != RT_EOK) + { + rt_thread_mdelay(1500); + sensor_init(dev); + LOG_E("The aht10 is under an abnormal status. Please try again"); + } + else + { + read_regs(dev->i2c, 6, temp); + /* 温度数据转换公式:原始值 * 200 / 2^20 - 50 */ + cur_temp = ((temp[3] & 0xf) << 16 | temp[4] << 8 | temp[5]) * 200.0 / (1 << 20) - 50; + } + } + rt_mutex_release(dev->lock); + + return cur_temp; +} + +/* 读取湿度(带互斥锁保护) */ +static float read_hw_humidity(aht10_device_t dev) +{ + rt_uint8_t temp[6]; + float cur_humi = 0.0; + rt_err_t result; + + RT_ASSERT(dev); + + result = rt_mutex_take(dev->lock, RT_WAITING_FOREVER); + if (result == RT_EOK) + { + rt_uint8_t cmd[2] = {0, 0}; + write_reg(dev->i2c, AHT10_GET_DATA, cmd); + + result = calibration_enabled(dev); + if (result != RT_EOK) + { + rt_thread_mdelay(1500); + sensor_init(dev); + LOG_E("The aht10 is under an abnormal status. Please try again"); + } + else + { + read_regs(dev->i2c, 6, temp); + /* 湿度数据转换公式:原始值 * 100 / 2^20 */ + cur_humi = (temp[1] << 12 | temp[2] << 4 | (temp[3] & 0xf0) >> 4) * 100.0 / (1 << 20); + } + } + rt_mutex_release(dev->lock); + + return cur_humi; +} + +/* AHT10 设备初始化入口 */ +aht10_device_t aht10_init(const char *i2c_bus_name) +{ + aht10_device_t dev; + RT_ASSERT(i2c_bus_name); + + dev = rt_calloc(1, sizeof(struct aht10_device)); + if (dev == RT_NULL) + { + LOG_E("Can't allocate memory for aht10 device on '%s' ", i2c_bus_name); + return RT_NULL; + } + + dev->i2c = rt_i2c_bus_device_find(i2c_bus_name); + if (dev->i2c == RT_NULL) + { + LOG_E("Can't find aht10 device on '%s' ", i2c_bus_name); + rt_free(dev); + return RT_NULL; + } + + dev->lock = rt_mutex_create("mutex_aht10", RT_IPC_FLAG_FIFO); + if (dev->lock == RT_NULL) + { + LOG_E("Can't create mutex for aht10 device on '%s' ", i2c_bus_name); + rt_free(dev); + return RT_NULL; + } + + sensor_init(dev); + return dev; +} +``` + +### 3.3 RW007 WiFi STM32 移植代码(rw007_stm32_port.c) + +```c +#include + +#ifdef RW007_USING_STM32_DRIVERS +#include +#include +#include +#include + +extern void spi_wifi_isr(int vector); + +/* RW007 GPIO 初始化:复位、就绪引脚配置 */ +static void rw007_gpio_init(void) +{ + rt_pin_mode(RW007_RST_PIN, PIN_MODE_OUTPUT); + rt_pin_mode(RW007_INT_BUSY_PIN, PIN_MODE_INPUT_PULLDOWN); + + /* 复位 rw007 并配置模式 */ + rt_pin_write(RW007_RST_PIN, PIN_LOW); + rt_thread_delay(rt_tick_from_millisecond(100)); + rt_pin_write(RW007_RST_PIN, PIN_HIGH); + + /* 等待 rw007 就绪(退出 busy 状态) */ + while(!rt_pin_read(RW007_INT_BUSY_PIN)) + { + rt_thread_delay(5); + } + + rt_thread_delay(rt_tick_from_millisecond(200)); + rt_pin_mode(RW007_INT_BUSY_PIN, PIN_MODE_INPUT_PULLUP); +} + +/* WiFi SPI 设备初始化 */ +int wifi_spi_device_init(void) +{ + char sn_version[32]; + + GPIO_TypeDef *cs_gpiox; + uint16_t cs_pin; + + cs_gpiox = (GPIO_TypeDef *)((rt_base_t)GPIOA + (rt_base_t)(RW007_CS_PIN / 16) * 0x0400UL); + cs_pin = (uint16_t)(1 << RW007_CS_PIN % 16); + + rw007_gpio_init(); + rt_hw_spi_device_attach(RW007_SPI_BUS_NAME, "wspi", cs_gpiox, cs_pin); + rt_hw_wifi_init("wspi"); + + /* 配置 STA + AP 双模式 */ + rt_wlan_set_mode(RT_WLAN_DEVICE_STA_NAME, RT_WLAN_STATION); + rt_wlan_set_mode(RT_WLAN_DEVICE_AP_NAME, RT_WLAN_AP); + + rw007_sn_get(sn_version); + rt_kprintf("\nrw007 sn: [%s]\n", sn_version); + rw007_version_get(sn_version); + rt_kprintf("rw007 ver: [%s]\n\n", sn_version); + + return 0; +} +INIT_APP_EXPORT(wifi_spi_device_init); + +/* SPI WiFi 中断处理 */ +static void int_wifi_irq(void * p) +{ + ((void)p); + spi_wifi_isr(0); +} + +void spi_wifi_hw_init(void) +{ + rt_pin_attach_irq(RW007_INT_BUSY_PIN, PIN_IRQ_MODE_FALLING, int_wifi_irq, 0); + rt_pin_irq_enable(RW007_INT_BUSY_PIN, RT_TRUE); +} + +#endif /* RW007_USING_STM32_DRIVERS */ +``` + +### 3.4 板级时钟配置(board/board.c) + +```c +#include "board.h" + +/* STM32F407 系统时钟配置:HSE + PLL = 168MHz */ +void SystemClock_Config(void) +{ + RCC_OscInitTypeDef RCC_OscInitStruct = {0}; + RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; + RCC_PeriphCLKInitTypeDef PeriphClkInitStruct = {0}; + + __HAL_RCC_PWR_CLK_ENABLE(); + __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1); + + /* 振荡器配置:HSE + LSE + LSI */ + RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI|RCC_OSCILLATORTYPE_HSE + |RCC_OSCILLATORTYPE_LSE; + RCC_OscInitStruct.HSEState = RCC_HSE_ON; + RCC_OscInitStruct.LSEState = RCC_LSE_ON; + RCC_OscInitStruct.LSIState = RCC_LSI_ON; + RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; + RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; + RCC_OscInitStruct.PLL.PLLM = 4; + RCC_OscInitStruct.PLL.PLLN = 168; + RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; + RCC_OscInitStruct.PLL.PLLQ = 7; + if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) + { + Error_Handler(); + } + + /* 时钟分频:AHB=/1, APB1=/4, APB2=/2 */ + RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK + |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2; + RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; + RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; + RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; + RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; + + if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK) + { + Error_Handler(); + } + /* RTC 时钟源选择 LSE */ + PeriphClkInitStruct.PeriphClockSelection = RCC_PERIPHCLK_RTC; + PeriphClkInitStruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE; + if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInitStruct) != HAL_OK) + { + Error_Handler(); + } +} +``` + +### 3.5 应用层头文件包装(applications/*.h) + +```c +/* applications/aht10.h */ +#ifndef APPLICATION_AHT10_WRAPPER_H +#define APPLICATION_AHT10_WRAPPER_H +#include "../packages/aht10-latest/inc/aht10.h" +#endif + +/* applications/paho_mqtt.h */ +#ifndef APPLICATION_PAHO_MQTT_WRAPPER_H +#define APPLICATION_PAHO_MQTT_WRAPPER_H +#include "../middleware_pahomqtt/MQTTClient-RT/paho_mqtt.h" +#endif + +/* applications/MQTTPacket.h */ +#ifndef APPLICATION_MQTT_PACKET_WRAPPER_H +#define APPLICATION_MQTT_PACKET_WRAPPER_H +#include "../middleware_pahomqtt/MQTTPacket/src/MQTTPacket.h" +#endif +``` + +## 四、关键运行流程 + +### 4.1 启动流程 + +1. **系统时钟**:`SystemClock_Config()` 将 STM32F407 配置为 168MHz +2. **WiFi 初始化**:`INIT_APP_EXPORT(wifi_spi_device_init)` 自动注册 RW007 SPI 设备 +3. **文件系统**:`filesystem_mount()` 挂载 ROMFS 与 FAL 分区(filesystem / font) +4. **主线程**:`main()` 配置 LED 引脚并启动 `telemetry` 线程 + +### 4.2 数据流 + +``` +AHT10 传感器 + │ (I2C3, 每 5 秒) + ▼ +telemetry_thread + ├──► /font/Data.txt (本地存储) + └──► MQTT publish (spark/aht10/data 远程上报) + │ + ▼ + EMQX Broker → MQTTX 客户端 +``` + +### 4.3 控制流 + +``` +MQTTX 发布 "toggle" 到 spark/aht10/cmd/led + │ + ▼ +mqtt_led_command 回调 + │ + ▼ +led_toggle() → PF12 电平翻转 → 红色 LED 切换 + │ + ▼ +mqtt_publish_text() 上报状态到 spark/aht10/status/led +``` + +## 五、常用 MSH 命令 + +| 命令 | 说明 | +| --- | --- | +| `wifi join ` | 连接 WiFi | +| `wifi status` | 查询 WiFi 状态 | +| `ifconfig` | 查询网络接口 | +| `mount` | 挂载文件系统 | +| `ls /font` | 列出 /font 目录 | +| `cat /font/Data.txt` | 查看采集数据 | +| `mkfs -t elm font` | 格式化 font 分区(首次使用) | +| `iot_mqtt_start tcp://host:1883` | 启动 MQTT 客户端 | +| `iot_mqtt_start tcp://host:1883 ` | 启动带认证的 MQTT | +| `iot_mqtt_status` | 查看 MQTT 状态 | +| `led_toggle` | 手动切换 LED | + +## 六、MQTT 主题约定 + +| 主题 | 方向 | Payload | 说明 | +| --- | --- | --- | --- | +| `spark/aht10/data` | 设备 → 云 | `Time: ... ; Temp: ... ; Humi: ... ; Count: ...` | 温湿度上报 | +| `spark/aht10/cmd/led` | 云 → 设备 | `toggle` / `on` / `off` | LED 控制命令 | +| `spark/aht10/status/led` | 设备 → 云 | `red led: on/off` / `red led: online` | LED 状态反馈 | + +## 七、硬件资源占用 + +| 资源 | 引脚/总线 | 用途 | +| --- | --- | --- | +| PF12 | GPIO 输出 | 红色 LED(低电平点亮) | +| PE0 / PE1 | 软件 I2C3 | AHT10 传感器 (SCL / SDA) | +| SPI2 | 硬件 SPI | W25Q64 外部 Flash | +| SPI | RW007 CS/INT/RST | RW007 WiFi 模块 | +| LSE | 32.768kHz | RTC 时钟源 | \ No newline at end of file diff --git "a/2026/\347\254\2544\347\273\204/\351\207\221\351\230\263\351\230\263/\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\207\221\351\230\263\351\230\263/\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..c71c0885c4e4cd6bd835a07d0fc068eaf8f76cbf --- /dev/null +++ "b/2026/\347\254\2544\347\273\204/\351\207\221\351\230\263\351\230\263/\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,415 @@ +# 第五天笔记:从温湿度采集到云端控制 + +> 用软件包 + FinSH + DFS 串起一条完整 IoT 链路 +> **采集(AHT10)→ 存储(FatFS)→ 上传(MQTT)→ 控制(云端指令翻转 LED)** +> +> 平台:星火一号(STM32F407ZG)|环境:RT-Thread Studio|工程:`05_task` +> 参考:官方文档 [FinSH 控制台](https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/programming-manual/finsh/finsh) + +--- + +## 一、FinSH 控制台 + +### 1.1 概念 + +FinSH 是 RT-Thread 的命令行组件(Shell)。它内部有一个 `tshell` 线程:**从串口读一行 → 查命令表 → 执行函数 → 输出结果**,循环往复。系统调试时它就是第一入口——看线程、看设备、读写文件、触发自定义函数,全在命令行完成。 + +``` +你敲的文本 ──▶ tshell 线程 ──▶ 命令表匹配 ──▶ 执行对应函数 ──▶ rt_kprintf 回显 +``` + +### 1.2 两种模式 + +| 模式 | 提示符 | 风格 | 特点 | +| --------------- | --------- | ----------- | ------------------------------------------------------------ | +| **MSH**(默认) | `msh />` | 类 dos/bash | Tab 补全、↑↓ 历史,省内存,**日常都用它** | +| C-Style | `finsh>>` | C 表达式 | 函数带括号 `list_device()`,可定义变量 `var_i=1`,`list_var()` 查看 | + +MSH 下敲 `finsh` 进入 C-Style,敲 `exit()` 返回。 + +### 1.3 导出自定义命令:无参数版 + +把函数变成可敲的命令,一行宏即可: + +```c +void led_toggle(void) +{ + /* 翻转 LED */ +} +MSH_CMD_EXPORT(led_toggle, toggle the red LED); +``` + +下载后终端敲 `led_toggle` 即执行。**调试心得:给每个硬件动作先导出一个 MSH 命令**,用终端验证硬件没问题后,再接入 MQTT 等复杂逻辑——"先本地、后云端"。 + +### 1.4 带参数的命令:argc/argv(练习工程设计) + +MQTT 连接参数(服务器地址、账号密码)**运行时才输入**,所以导出的是带参数命令: + +```c +static int iot_mqtt_start(int argc, char **argv) +{ + if (argc != 2 && argc != 4) /* 参数个数校验 */ + { + rt_kprintf("iot_mqtt_start [username password]\n"); + return -RT_EINVAL; + } + rt_strncpy(mqtt_uri, argv[1], sizeof(mqtt_uri) - 1); /* argv[1] = URI */ + if (argc == 4) { /* 可选账号密码 */ } + + paho_mqtt_start(&mqtt_client); /* 启动 MQTT 线程 */ + return RT_EOK; +} +MSH_CMD_EXPORT(iot_mqtt_start, start MQTT: iot_mqtt_start [user password]); +``` + +三个要点: + +1. **`argc` 是参数个数**(含命令本身),`argv[0]` 是命令名,`argv[1]` 起才是参数; +2. **先校验参数**,非法时打印用法提示并返回错误码——这是 shell 命令的规范做法; +3. 用 MSH 命令做"运行期配置":换 Broker 只需重敲命令,**不用改代码重编译**。 + +### 1.5 导出原理:零注册的段机制 + +宏展开后生成 `{命令名, 描述, 函数指针}` 结构体,用 `__attribute__((section("FSymTab")))` 放入**独立代码段**,名字字符串放入 `.rodata.name` 段;链接脚本圈定段的起止地址。启动时 FinSH **遍历这段内存区间**就拿到了全部命令表。 + +> 所以"导出一个命令" = "往特殊段里塞一个结构体",编译器+链接器自动收集,**不需要任何注册代码**。命令名上限 `FINSH_NAME_MAX`(16 字节),超长用 `MSH_CMD_EXPORT_ALIAS`。 + +### 1.6 常用内置命令 + +| 命令 | 作用 | +| ------------------------------------------------------------ | ----------------------------------------------------- | +| `help` | 列出全部可用命令 | +| `ps` / `list_thread` | 查看线程状态、栈、优先级 | +| `list_device` | **调试第一步**:确认设备注册名(如 `i2c3`、`W25Q64`) | +| `list_timer` / `list_sem` / `list_mutex` / `list_event` / `list_mailbox` / `list_msgqueue` / `list_mempool` | 查看对应内核对象 | +| `list` | 对象总览 | +| `free` / `version` / `clear` | 内存 / 版本 / 清屏 | + +--- + +## 二、软件包 + +### 2.1 概念与价值 + +软件包 = **封装好驱动/协议/算法的功能模块**,应用层只调 API。价值:**省时间**(不必重写 I2C 时序、TCP 状态机)、**更可靠**(社区验证过的代码)、**可裁剪**(Kconfig 按需开关,控制 ROM/RAM)。 + +### 2.2 使用流程 + +``` +RT-Thread Settings 勾选 → 保存(自动下载源码到 packages/)→ 编译(自动加入构建) +``` + +> **依赖问题**:软件包可能依赖框架(如 AHT10 依赖 Sensor 框架),勾选时缺依赖会编译报错,按报错提示补开即可。 + +### 2.3 本课软件包 + +| 软件包 | 用途 | +| ---------------------------------------------- | ---------------------------- | +| `aht10` | AHT10/AHT21 温湿度传感器驱动 | +| `rw007` | SPI 接口 WiFi 模块驱动 | +| `paho-mqtt`(练习)/ `kawaii-mqtt`(课堂演示) | MQTT 客户端 | + +--- + +## 三、RT-Thread 中的组件 + +### 3.1 组件是什么?和软件包怎么区分 + +RT-Thread 系统架构分三层:**内核(Kernel)→ 组件与服务层(Components)→ 软件包(Packages)**。今天用的很多功能其实都来自"组件"这一层,容易和软件包混淆,一张表分清: + +| | 组件(Components) | 软件包(Packages) | +| ------ | ----------------------- | ----------------------- | +| 来源 | **官方随内核发布** | 社区/第三方贡献 | +| 位置 | `rt-thread/components/` | `packages/` | +| 开关宏 | `RT_USING_XXX` | `PKG_USING_XXX` | +| 例子 | FinSH、DFS、lwIP、PIN | aht10、rw007、paho-mqtt | + +> 💡 关键关系:**软件包通常"站在组件的肩膀上"**——aht10 包调用 I2C 组件,paho-mqtt 包调用 lwIP 组件。组件是官方地基,软件包是社区盖的楼。 + +### 3.2 本工程启用的组件一览(对照 05_task/rtconfig.h) + +| 宏 | 组件 | 作用 | 本工程角色 | +| -------------------------------------- | ------------------ | ---------------------- | ----------------------------- | +| `RT_USING_FINSH` + `RT_USING_MSH` | FinSH | 命令行调试 | 敲命令、导出 `iot_mqtt_start` | +| `RT_USING_DEVICE` + `RT_USING_PIN` | 设备驱动框架 / PIN | 统一设备模型、引脚 API | `rt_pin_write` 翻转 LED | +| `RT_USING_I2C` + `RT_USING_I2C_BITOPS` | I2C(软件模拟) | I2C 总线 | AHT10 挂的 `i2c3` | +| `RT_USING_SFUD` | SFUD | 通用 SPI Flash 驱动 | W25Q64 | +| `RT_USING_FAL` | FAL | Flash 分区管理 | `font` 分区 | +| `RT_USING_DFS` + `RT_USING_DFS_ELMFAT` | DFS + FatFS | 虚拟文件系统 | 挂载 `/font`、`/fal` | +| `RT_USING_SAL` + `RT_USING_LWIP` | SAL / lwIP | TCP/IP 协议栈 | MQTT 的 socket 通信 | +| `RT_USING_WIFI` | WiFi 框架 | 统一 WiFi 接口 | `wifi join` 命令 | + +### 3.3 组件之间的协作 + +``` +PIN/I2C ──▶ AHT10 软件包 ──▶ 遥测线程(应用层) +SFUD → FAL → DFS(FatFS) ──▶ open()/write() 写 Data.txt +WiFi 框架 ← RW007 软件包 ──▶ lwIP/SAL ──▶ paho-mqtt ──▶ EMQX +FinSH ────── 贯穿全程:调试每一个环节 ────── +``` + +看懂这张图,就明白了为什么 `list_device` 里同时能看到 `i2c3`、`W25Q64`、`wlan0`——它们分别是 I2C、SFUD、WiFi 三个组件各自注册的设备。 + +--- + +## 四、AHT10 温湿度传感器 + +### 4.1 硬件连接 + +板载 AHT21 挂在**软件 I2C3**:SCL = PE0,SDA = PE1,设备地址 `0x38`。 + +### 4.2 软件包三件套 + +```c +aht10_device_t dev = aht10_init("i2c3"); // 初始化:传总线名,拿设备句柄 +float temp = aht10_read_temperature(dev); // 读温度 +float humi = aht10_read_humidity(dev); // 读湿度 +``` + +| | 手写驱动 | 软件包 | +| ------ | ------------------------------------------------------------ | -------- | +| 要做的 | 查总线、构造 `rt_i2c_msg`、发命令 `0xAC`、等转换、解析 6 字节、套公式 | 3 行 | +| 适合 | 学习 I2C 时序原理 | 项目开发 | + +### 4.3 两个易错点 + +- **`%f` 打印**:默认 `rt_kprintf` 不支持浮点。两个办法:启用 `rt_vsprintf_full` 软件包;或拆整数小数 `"%d.%d", (int)temp, (int)(temp*10)%10`(练习工程用后者,省 ROM)。 +- **首次读数 -50.0**:传感器校准过程,正常现象,下一拍恢复。 + +--- + +## 五、Wi-Fi 与 RW007 + +- RW007 是 **SPI 接口** WiFi 模块,与 W25Q64 **共用 SPI2 总线**。 +- **引脚必须按原理图核对**(软件包默认可能与板子不符):CS = PF10、RST = PG15、INT = PG11、BOOT0 = PB13。引脚错了 `list_device` 里根本没有 `wlan0`。 +- 命令是 **`wifi join <密码>`**(`join` 不是 `connect`)。 +- 验证顺序:`wifi status` → `ifconfig`(有 IP 才算连上)→ `ping`(验证外网与 DNS)。 +- 共用总线需在初始化时**拉高 WiFi 片选**,否则干扰 Flash 读写。 + +--- + +## 六、MQTT 协议 + +### 6.1 核心模型:发布/订阅 + +``` +发布者(板卡) ──publish──▶ Broker(EMQX) ──按主题转发──▶ 订阅者(MQTTX) +``` + +**精髓是解耦**:发布者与订阅者互不认识,只约定主题,Broker 负责转发。 + +| 概念 | 含义 | +| ------------------- | ------------------------------------- | +| Client | 客户端,可发布、订阅、断开 | +| Broker | 服务端,维护连接、按主题转发 | +| Topic | 主题,分层命名,如 `spark/aht10/data` | +| Publish / Subscribe | 发布 / 订阅 | + +### 6.2 QoS 三档 + +| 等级 | 语义 | 典型场景 | +| ----- | ---------------------------- | -------------------- | +| QoS 0 | 最多一次,可能丢 | 高频环境数据 | +| QoS 1 | 至少一次,可能重复(需去重) | 报警、控制命令 | +| QoS 2 | 恰好一次,开销最大 | 关键且不能重复的消息 | + +### 6.3 主题设计 + +- 区分**大小写**,多写/漏写 `/` 都会对不上; +- 通配符:`+` 匹配单层,`#` 匹配多层; +- 公共 Broker 消息**人人可见**:主题别用 `test/hello`,加唯一后缀;**Client ID 也必须唯一**(同名会互相顶下线)。 + +### 6.4 调试顺序(方法论) + +> **先联网 → 再 MQTT → 后业务**。不要一上来就集成传感器数据,先把 MQTTX 与板卡的双向收发打通。 + +MQTTX 连接参数:`broker.emqx.io` / `1883` / MQTT 3.1.1 / 唯一 Client ID。验证时记住:**板卡的发布主题 = MQTTX 的订阅主题,反之亦然**。 + +--- + +## 七、文件系统 DFS + +### 7.1 四层架构 + +``` +应用层 open() / read() / write() / close() ← POSIX 接口,写法与 Linux 一致 + │ +DFS 虚拟文件系统:统一 "/" 路径与文件接口 + │ +FatFS 在分区上实现 FAT 格式 + │ +FAL 把 Flash 切成逻辑分区,向上提供"块设备" + │ +SFUD 通用串行 Flash 驱动(屏蔽芯片差异) + │ +SPI 控制器 → W25Q64(8MB) +``` + +### 7.2 分工 + +| 组件 | 职责 | +| ----- | ---------------------------- | +| SFUD | "怎么**和 Flash 芯片通信**" | +| FAL | "怎么**把 Flash 切成分区**" | +| FatFS | "分区上**跑 FAT 文件系统**" | +| DFS | "向上**统一路径和文件接口**" | + +### 7.3 分区表(W25Q64) + +| 分区名 | 偏移 | 大小 | 用途 | +| ---------- | ----- | ----- | --------------------------- | +| easyflash | 0 | 512KB | 参数存储 | +| download | 512KB | 1MB | OTA 下载区 | +| wifi_image | 1.5MB | 512KB | WiFi 固件 | +| **font** | 2MB | 3MB | **本课挂载点,存 Data.txt** | +| filesystem | 5MB | 3MB | 通用文件系统 | + +### 7.4 挂载流程 + +```c +fal_init(); // 1.FAL 分区管理 +fal_blk_device_create("font"); // 2.分区 → 块设备 +dfs_mount("font", "/font", "elm", 0, 0); // 3.挂 FatFS 到 /font +``` + +**新 Flash 首次挂载必然失败**(分区还没有文件系统),格式化一次即可: + +```text +msh />mkfs -t elm font # 格式化(只做一次,会清空数据) +msh />mount font /font elm # 手动挂载 +``` + +本工程已配置**开机自动挂载**(`INIT_APP_EXPORT` 阶段执行),之后代码里直接 `open("/font/Data.txt")`。 + +### 7.5 POSIX 接口速查 + +| API | 用途 | +| -------------------------------------------- | ---------------------------- | +| `open` / `close` | 打开/关闭,返回整数句柄 `fd` | +| `read` / `write` | 读写;`O_APPEND` 实现追加写 | +| `lseek` / `rename` / `unlink` | 定位 / 重命名 / 删除 | +| `stat` | 文件状态 | +| `mkdir` / `opendir` / `readdir` / `closedir` | 目录操作 | + +> `open()` 成功后**所有退出路径都要 `close()`**,否则长时间运行耗尽文件描述符。 + +--- + +## 八、练习实现思路(05_task) + +### 8.1 工程 + +``` +05_task/ +├─ applications/main.c ← 全部业务逻辑(单文件,好读) +├─ packages/aht10-latest/ ← 软件包:AHT10 驱动 +├─ middleware_pahomqtt/ ← paho-mqtt 移植(MQTT 客户端) +├─ middleware_rw007/ ← RW007 WiFi 驱动 +├─ libraries/Board_Drivers/ +│ ├─ spi_flash_init.c ← W25Q64 挂到 SPI2(SFUD) +│ ├─ drv_filesystem.c ← 开机自动挂载 /font、/fal +│ └─ fal/fal_cfg.h ← 分区表 +└─ board/ ← BSP:时钟、引脚 +``` + +### 8.2 总体设计:三层解耦 + +> **"驱动管硬件、软件包管协议、应用层只管业务"**——采集、存储、上传互不拖累。 + +### 8.3 遥测线程:先落盘,后上云 + +```c +while (1) +{ + 读温湿度 → count++ → 格式化记录 + open("/font/Data.txt") 追加写入 /* 永远执行,不依赖网络 */ + if (mqtt_started && isconnected) + paho_mqtt_publish(...) /* 在线才发,离线静默跳过 */ + 延时 5 秒 +} +``` + +设计要点: + +1. **main() 只做装配**:初始化 LED + 创建线程,不阻塞不轮询; +2. **采集与上传解耦**(核心):断网不丢数据,文件记录永远在进行; +3. **单一数据源**:一条记录只在一处格式化,文件和 MQTT 共用同一份字符串,本地与云端必然一致;`Count` 上电从 1 起; +4. **LED 控制复用**:`led_toggle()` 同时被本地 MSH 命令和 MQTT 回调调用——本地验证通过 = 云端功能可用。 + +### 8.4 MQTT 启动命令的健壮性设计 + +`iot_mqtt_start` 除了参数校验(见 1.4),还有三道防线: + +- **幂等保护**:`mqtt_started` 标志防止重复启动(重复调用返回 `-RT_EBUSY`); +- **失败路径逐级释放**:buf/readbuf/topicFilter 谁申请了谁负责在失败时 free,不留泄漏; +- **topicFilter 用 `rt_strdup` 堆分配**:paho 断开连接时会 free 主题串,若传字符串常量,二次连接直接崩溃——这是文档里查不到、只能靠读源码发现的细节。 + +### 8.5 联调四步法 + +``` +① 上电 → list_device 确认 i2c3/W25Q64 都在 +② 本地验证:ls /font、cat Data.txt(不需要网络) +③ 联网:wifi join → ifconfig → ping 223.5.5.5 +④ 上云:iot_mqtt_start → MQTTX 订阅收数 → MQTTX 发 toggle 翻灯 +``` + +每一步都有独立验证手段,出问题能立刻定位是哪一环——这是本次练习最大的体会。 + +### 8.6 我踩过的坑:DNS 202 + +`iot_mqtt_start` 报 `getaddrinfo err: 202`——**是 DNS 解析失败(lwIP `EAI_FAIL`),不是 MQTT 问题**。排查链: + +```text +ifconfig # ① WiFi 有没有拿到 IP? +ping 223.5.5.5 # ② 外网通不通? +dns 223.5.5.5 # ③ 外网通但域名解析失败 → 手动指定 DNS +ping broker.emqx.io # ④ 验证域名解析 +``` + +教训:**网络问题按"IP → 外网 → DNS"逐层排查**,不要盯着 MQTT 报错本身。 + +--- + +## 九、踩坑速查 + +| 现象 | 原因 | 解决 | +| -------------------------------------------- | ----------------------------- | -------------------------------------------- | +| `list_device` 无 `wlan0`,`wifi scan` 报 -10 | RW007 引脚与原理图不符 | 改为 CS=PF10、RST=PG15、INT=PG11、BOOT0=PB13 | +| 挂载报 `File system (elm) was not found` | FatFS 源码没进构建 | 确认 `RT_USING_DFS_ELMFAT` 开启且源码被编译 | +| 温湿度打印异常 | 默认不支持 `%f` | `rt_vsprintf_full` 或拆整数小数 | +| 首次读数 -50.0 | 传感器校准过程 | 正常,等下一拍 | +| 挂载失败 | 新分区未格式化 | `mkfs -t elm font` 一次 | +| Flash 读写异常 | WiFi 与 Flash 共 SPI 总线冲突 | 初始化时拉高 WiFi 片选 | +| `getaddrinfo err: 202` | DNS 解析失败 | 按 8.6 排查,`dns 223.5.5.5` | +| 全角冒号乱码 | 编码不一致 | 代码用 GBK `\xa3\xba` 或改半角冒号 | + +--- + +## 十、命令速查卡 + +| 命令 | 用途 | +| ------------------------------------------------------- | ------------------------------- | +| `help` / `ps` / `free` | 命令列表 / 线程 / 内存 | +| `list_device` | 查看设备(调试第一步) | +| `wifi scan` / `wifi join ` / `wifi status` | WiFi 扫描 / 连接 / 状态 | +| `ifconfig` / `ping ` / `dns ` | 网络 / 连通性 / DNS | +| `fal probe` | 查看 FAL 分区 | +| `mkfs -t elm font` | 格式化 font 分区(首次一次) | +| `mount font /font elm` / `mount` | 挂载分区 / 查看挂载表 | +| `df` / `ls /font` / `cat /font/Data.txt` / `rm ` | 容量 / 列目录 / 看文件 / 删文件 | +| `iot_mqtt_start tcp://broker.emqx.io:1883` | 启动 MQTT(练习工程自定义) | +| `iot_mqtt_status` / `led_toggle` | MQTT 状态 / 本地翻转 LED | + +--- + +## 十一、扩展思考(后续可以做) + + + +1. **JSON 上报**:用 `cJSON` 包把温湿度组织成 `{"temp":25.3,"humi":61.8,"count":1}`,云端解析更规范; +2. **断线补传**:MQTT 重连后把离线期间落盘的数据补发到云端; +3. **自动连网**:把 `wifi join` + `iot_mqtt_start` 写进代码,上电全自动; +4. **存储防写满**:Data.txt 滚动保留最近 N 条或按天分文件。 + +> 组件是 RT-Thread 自带的**地基**(FinSH、DFS、FAL、lwIP),软件包是社区盖的**楼**(aht10、rw007、paho-mqtt)——本课站在两者之上;三行代码读传感器、Flash 像文件一样读写、MQTT 按主题解耦通信、FinSH 贯穿调试。四者组合,就是"采集 → 存储 → 上传 → 控制"的完整 IoT 链路,也是 OTA 升级、云平台对接等进阶内容的基础。 +> \ No newline at end of file \ No newline at end of file