diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..b37f338c33390c2862dd7eed728a85e5d25c796c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" @@ -0,0 +1,372 @@ +#include + +#define PACKAGE_QUEUE_SIZE 5 +#define PACKAGE_PER_SENDER 5 +#define PACKAGE_END_ID 0xFFFFFFFFU + +#define THREAD_STACK_SIZE 1024 +#define SORT_PRIORITY 19 +#define SEND_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +#define REGION_EAST 1 +#define REGION_SOUTH 2 +#define REGION_NORTH 3 + +struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; + rt_uint16_t weight; +}; + +struct sender_info +{ + rt_uint32_t base_id; + rt_uint8_t sender_id; +}; + +static rt_mq_t package_mq = RT_NULL; + +static rt_uint8_t demo_running = 0; +static rt_uint8_t sender_finished = 0; + +static rt_uint32_t send_failed = 0; + +static struct sender_info sender_info[2] = +{ + {1000, 1}, + {2000, 2} +}; + + +/* 获取区域名称 */ +static const char *get_region_name(rt_uint8_t region) +{ + switch (region) + { + case REGION_EAST: + return "east"; + + case REGION_SOUTH: + return "south"; + + case REGION_NORTH: + return "north"; + + default: + return "unknown"; + } +} + + +/* 发送结束消息 */ +static void send_end_message(void) +{ + struct package_msg msg; + rt_err_t result; + + msg.id = PACKAGE_END_ID; + msg.region = 0; + msg.weight = 0; + + while (1) + { + result = rt_mq_send(package_mq, + &msg, + sizeof(msg)); + + if (result == RT_EOK) + { + break; + } + + rt_thread_mdelay(20); + } +} + + +/* 快递发送线程 */ +static void sender_entry(void *parameter) +{ + struct sender_info *info; + struct package_msg msg; + + rt_uint8_t i; + rt_uint8_t finished; + rt_err_t result; + + info = (struct sender_info *)parameter; + + for (i = 1; i <= PACKAGE_PER_SENDER; i++) + { + /* 生成快递信息 */ + msg.id = info->base_id + i; + msg.region = ((i + info->sender_id - 2) % 3) + 1; + msg.weight = 700 + info->sender_id * 100 + i * 80; + + while (1) + { + /* 发送快递消息 */ + result = rt_mq_send(package_mq, + &msg, + sizeof(msg)); + + if (result == RT_EOK) + { + break; + } + + if (result == -RT_EFULL) + { + /* 记录队列满 */ + rt_enter_critical(); + send_failed++; + rt_exit_critical(); + + rt_thread_mdelay(50); + } + else + { + rt_kprintf("package %d send error: %d\n", + msg.id, + result); + + rt_thread_mdelay(50); + } + } + + rt_thread_mdelay(50); + } + + /* 记录发送线程完成 */ + rt_enter_critical(); + + sender_finished++; + finished = sender_finished; + + rt_exit_critical(); + + /* 最后一个发送线程发送结束消息 */ + if (finished == 2) + { + send_end_message(); + } +} + + +/* 快递分拣线程 */ +static void sorter_entry(void *parameter) +{ + struct package_msg msg; + + rt_uint32_t total_packages = 0; + rt_uint32_t east_count = 0; + rt_uint32_t south_count = 0; + rt_uint32_t north_count = 0; + rt_uint32_t total_weight = 0; + + rt_uint32_t failed; + rt_err_t result; + + while (1) + { + /* 等待快递消息 */ + result = rt_mq_recv(package_mq, + &msg, + sizeof(msg), + RT_WAITING_FOREVER); + + if (result != RT_EOK) + { + rt_kprintf("receive package failed: %d\n", + result); + continue; + } + + /* 检查结束消息 */ + if (msg.id == PACKAGE_END_ID) + { + break; + } + + rt_kprintf("package %d -> %s, weight: %d g\n", + msg.id, + get_region_name(msg.region), + msg.weight); + + /* 统计快递 */ + total_packages++; + total_weight += msg.weight; + + switch (msg.region) + { + case REGION_EAST: + east_count++; + break; + + case REGION_SOUTH: + south_count++; + break; + + case REGION_NORTH: + north_count++; + break; + + default: + break; + } + + /* 模拟分拣过程 */ + rt_thread_mdelay(100); + } + + rt_enter_critical(); + failed = send_failed; + rt_exit_critical(); + + /* 打印统计结果 */ + rt_kprintf("total packages: %d\n", + total_packages); + + rt_kprintf("east: %d, south: %d, north: %d\n", + east_count, + south_count, + north_count); + + rt_kprintf("total weight: %d g\n", + total_weight); + + rt_kprintf("send failed: %d\n", + failed); + + /* 删除消息队列 */ + if (package_mq != RT_NULL) + { + rt_mq_delete(package_mq); + package_mq = RT_NULL; + } + + rt_enter_critical(); + + sender_finished = 0; + send_failed = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("package demo finished\n"); +} + + +/* 快递分拣实验 */ +static void package_demo(void) +{ + rt_thread_t sender1_tid; + rt_thread_t sender2_tid; + rt_thread_t sorter_tid; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("package demo is already running\n"); + return; + } + + demo_running = 1; + sender_finished = 0; + send_failed = 0; + + rt_exit_critical(); + + /* 创建消息队列 */ + package_mq = rt_mq_create("pkg_mq", + sizeof(struct package_msg), + PACKAGE_QUEUE_SIZE, + RT_IPC_FLAG_FIFO); + + if (package_mq == RT_NULL) + { + rt_kprintf("create package queue failed\n"); + + demo_running = 0; + return; + } + + /* 创建发送线程 1 */ + sender1_tid = rt_thread_create("pkg_send1", + sender_entry, + &sender_info[0], + THREAD_STACK_SIZE, + SEND_PRIORITY, + THREAD_TIMESLICE); + + if (sender1_tid == RT_NULL) + { + rt_kprintf("create sender1 thread failed\n"); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建发送线程 2 */ + sender2_tid = rt_thread_create("pkg_send2", + sender_entry, + &sender_info[1], + THREAD_STACK_SIZE, + SEND_PRIORITY, + THREAD_TIMESLICE); + + if (sender2_tid == RT_NULL) + { + rt_kprintf("create sender2 thread failed\n"); + + rt_thread_delete(sender1_tid); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建分拣线程 */ + sorter_tid = rt_thread_create("pkg_sort", + sorter_entry, + RT_NULL, + THREAD_STACK_SIZE, + SORT_PRIORITY, + THREAD_TIMESLICE); + + if (sorter_tid == RT_NULL) + { + rt_kprintf("create sorter thread failed\n"); + + rt_thread_delete(sender1_tid); + rt_thread_delete(sender2_tid); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("package demo start\n"); + + /* 启动分拣线程 */ + rt_thread_startup(sorter_tid); + + /* 启动发送线程 */ + rt_thread_startup(sender1_tid); + rt_thread_startup(sender2_tid); +} + +MSH_CMD_EXPORT(package_demo, package message queue demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..35c7f36d68513bfb49584695abf5ee0d4526f127 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" @@ -0,0 +1,187 @@ +#include + +#define CAR_NUM 5 +#define PARKING_SPACES 3 +#define WAIT_TIME_MS 2000 +#define PARK_TIME_MS 3000 +#define RETRY_TIME_MS 500 + +static rt_sem_t parking_sem = RT_NULL; + +static rt_uint8_t used_spaces = 0; +static rt_uint8_t finished_cars = 0; +static rt_uint8_t demo_running = 0; + +static rt_uint8_t car_id[CAR_NUM] = {1, 2, 3, 4, 5}; + +/* 车辆线程 */ +static void car_entry(void *parameter) +{ + rt_uint8_t id; + rt_uint8_t spaces; + rt_uint8_t finished; + rt_err_t result; + + id = *(rt_uint8_t *)parameter; + + while (1) + { + /* 申请车位,最多等待 2 秒 */ + result = rt_sem_take(parking_sem, + rt_tick_from_millisecond(WAIT_TIME_MS)); + + if (result == RT_EOK) + { + /* 更新已使用车位 */ + rt_enter_critical(); + used_spaces++; + spaces = used_spaces; + rt_exit_critical(); + + rt_kprintf("car%d entered, used spaces: %d\n", + id, spaces); + + /* 模拟停车 */ + rt_thread_mdelay(PARK_TIME_MS); + + /* 更新已使用车位 */ + rt_enter_critical(); + used_spaces--; + spaces = used_spaces; + rt_exit_critical(); + + rt_kprintf("car%d left, used spaces: %d\n", + id, spaces); + + /* 释放车位 */ + rt_sem_release(parking_sem); + + break; + } + else if (result == -RT_ETIMEOUT) + { + rt_kprintf("car%d waiting for a space\n", id); + + /* 稍后重新申请 */ + rt_thread_mdelay(RETRY_TIME_MS); + } + else + { + rt_kprintf("car%d take semaphore failed: %d\n", + id, result); + + rt_thread_mdelay(RETRY_TIME_MS); + } + } + + /* 记录完成车辆数量 */ + rt_enter_critical(); + finished_cars++; + finished = finished_cars; + rt_exit_critical(); + + /* 最后一辆车负责释放资源 */ + if (finished == CAR_NUM) + { + rt_kprintf("all cars finished\n"); + + if (parking_sem != RT_NULL) + { + rt_sem_delete(parking_sem); + parking_sem = RT_NULL; + } + + rt_enter_critical(); + used_spaces = 0; + finished_cars = 0; + demo_running = 0; + rt_exit_critical(); + + rt_kprintf("parking demo finished\n"); + } +} + + +/* 停车场实验 */ +static void parking_demo(void) +{ + rt_thread_t tid[CAR_NUM]; + rt_uint8_t i; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("parking demo is already running\n"); + return; + } + + demo_running = 1; + used_spaces = 0; + finished_cars = 0; + + rt_exit_critical(); + + /* 创建计数信号量 */ + parking_sem = rt_sem_create("park_sem", + PARKING_SPACES, + RT_IPC_FLAG_FIFO); + + if (parking_sem == RT_NULL) + { + rt_kprintf("create parking semaphore failed\n"); + + demo_running = 0; + return; + } + + /* 创建车辆线程 */ + for (i = 0; i < CAR_NUM; i++) + { + tid[i] = rt_thread_create("car", + car_entry, + &car_id[i], + 1024, + 20, + 10); + + if (tid[i] == RT_NULL) + { + rt_uint8_t j; + + rt_kprintf("create car%d thread failed\n", + car_id[i]); + + /* 删除已创建但未启动的线程 */ + for (j = 0; j < i; j++) + { + if (tid[j] != RT_NULL) + { + rt_thread_delete(tid[j]); + } + } + + rt_sem_delete(parking_sem); + parking_sem = RT_NULL; + + demo_running = 0; + + return; + } + } + + rt_kprintf("parking demo start\n"); + rt_kprintf("total spaces: %d, total cars: %d\n", + PARKING_SPACES, CAR_NUM); + + /* 启动车辆线程 */ + for (i = 0; i < CAR_NUM; i++) + { + rt_thread_startup(tid[i]); + } +} + +MSH_CMD_EXPORT(parking_demo, parking semaphore demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..61abd8d3ab7e4ac68781a8cc507e90082b6598bb --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" @@ -0,0 +1,326 @@ +#include + +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) + +#define EVENT_ALL_READY (EVENT_NET_READY | \ + EVENT_SENSOR_READY | \ + EVENT_STORAGE_READY) + +#define THREAD_STACK_SIZE 1024 +#define INIT_PRIORITY 20 +#define BUSINESS_PRIORITY 21 +#define THREAD_TIMESLICE 10 + +#define NET_DELAY_MS 1000 +#define SENSOR_DELAY_MS 2000 +#define STORAGE_DELAY_MS 5000 +#define FIRST_WAIT_MS 3000 + +static rt_event_t startup_event = RT_NULL; + +static rt_uint32_t ready_flags = 0; +static rt_uint8_t demo_running = 0; + + +/* 记录模块状态 */ +static void set_ready_flag(rt_uint32_t flag) +{ + rt_enter_critical(); + + ready_flags |= flag; + + rt_exit_critical(); +} + + +/* 网络初始化线程 */ +static void net_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(NET_DELAY_MS); + + set_ready_flag(EVENT_NET_READY); + + result = rt_event_send(startup_event, + EVENT_NET_READY); + + if (result == RT_EOK) + { + rt_kprintf("network ready\n"); + } + else + { + rt_kprintf("network event send failed\n"); + } +} + + +/* 传感器初始化线程 */ +static void sensor_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(SENSOR_DELAY_MS); + + set_ready_flag(EVENT_SENSOR_READY); + + result = rt_event_send(startup_event, + EVENT_SENSOR_READY); + + if (result == RT_EOK) + { + rt_kprintf("sensor ready\n"); + } + else + { + rt_kprintf("sensor event send failed\n"); + } +} + + +/* 存储初始化线程 */ +static void storage_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(STORAGE_DELAY_MS); + + set_ready_flag(EVENT_STORAGE_READY); + + result = rt_event_send(startup_event, + EVENT_STORAGE_READY); + + if (result == RT_EOK) + { + rt_kprintf("storage ready\n"); + } + else + { + rt_kprintf("storage event send failed\n"); + } +} + + +/* 打印未完成模块 */ +static void print_not_ready(void) +{ + rt_uint32_t flags; + + rt_enter_critical(); + + flags = ready_flags; + + rt_exit_critical(); + + rt_kprintf("wait modules timeout"); + + if ((flags & EVENT_NET_READY) == 0) + { + rt_kprintf(", network is not ready"); + } + + if ((flags & EVENT_SENSOR_READY) == 0) + { + rt_kprintf(", sensor is not ready"); + } + + if ((flags & EVENT_STORAGE_READY) == 0) + { + rt_kprintf(", storage is not ready"); + } + + rt_kprintf("\n"); +} + + +/* 主业务线程 */ +static void business_entry(void *parameter) +{ + rt_uint32_t received; + rt_err_t result; + + /* 第一次等待,最多 3 秒 */ + result = rt_event_recv(startup_event, + EVENT_ALL_READY, + RT_EVENT_FLAG_AND, + rt_tick_from_millisecond(FIRST_WAIT_MS), + &received); + + if (result == -RT_ETIMEOUT) + { + print_not_ready(); + + /* 超时后再次等待 */ + result = rt_event_recv(startup_event, + EVENT_ALL_READY, + RT_EVENT_FLAG_AND | + RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, + &received); + } + + if (result == RT_EOK) + { + rt_kprintf("all modules are ready\n"); + rt_kprintf("business task started\n"); + } + else + { + rt_kprintf("receive startup event failed: %d\n", + result); + } + + /* 删除事件集 */ + if (startup_event != RT_NULL) + { + rt_event_delete(startup_event); + startup_event = RT_NULL; + } + + rt_enter_critical(); + + ready_flags = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("startup demo finished\n"); +} + + +/* 系统启动实验 */ +static void startup_demo(void) +{ + rt_thread_t net_tid; + rt_thread_t sensor_tid; + rt_thread_t storage_tid; + rt_thread_t business_tid; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("startup demo is already running\n"); + return; + } + + demo_running = 1; + ready_flags = 0; + + rt_exit_critical(); + + /* 创建事件集 */ + startup_event = rt_event_create("startup", + RT_IPC_FLAG_FIFO); + + if (startup_event == RT_NULL) + { + rt_kprintf("create startup event failed\n"); + + demo_running = 0; + return; + } + + /* 创建网络线程 */ + net_tid = rt_thread_create("net_init", + net_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (net_tid == RT_NULL) + { + rt_kprintf("create network thread failed\n"); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建传感器线程 */ + sensor_tid = rt_thread_create("sensor", + sensor_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (sensor_tid == RT_NULL) + { + rt_kprintf("create sensor thread failed\n"); + + rt_thread_delete(net_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建存储线程 */ + storage_tid = rt_thread_create("storage", + storage_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (storage_tid == RT_NULL) + { + rt_kprintf("create storage thread failed\n"); + + rt_thread_delete(net_tid); + rt_thread_delete(sensor_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建业务线程 */ + business_tid = rt_thread_create("business", + business_entry, + RT_NULL, + THREAD_STACK_SIZE, + BUSINESS_PRIORITY, + THREAD_TIMESLICE); + + if (business_tid == RT_NULL) + { + rt_kprintf("create business thread failed\n"); + + rt_thread_delete(net_tid); + rt_thread_delete(sensor_tid); + rt_thread_delete(storage_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("startup demo start\n"); + + /* 启动初始化线程 */ + rt_thread_startup(net_tid); + rt_thread_startup(sensor_tid); + rt_thread_startup(storage_tid); + + /* 启动业务线程 */ + rt_thread_startup(business_tid); +} + +MSH_CMD_EXPORT(startup_demo, startup event demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..f082732a85c3e33c3a508b6a03eb62a01dfccb82 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" @@ -0,0 +1,177 @@ +#include + +#define TOTAL_TICKETS 20 + +#define THREAD_STACK_SIZE 1024 +#define THREAD_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +static rt_mutex_t ticket_mutex = RT_NULL; + +static rt_int32_t ticket_count = TOTAL_TICKETS; +static rt_uint8_t finished_windows = 0; +static rt_uint8_t demo_running = 0; + +static char window_name[2] = {'A', 'B'}; + + +/* 售票线程 */ +static void ticket_entry(void *parameter) +{ + char name; + rt_int32_t ticket_no; + rt_uint8_t finished; + + name = *(char *)parameter; + + while (1) + { + /* 获取互斥量 */ + if (rt_mutex_take(ticket_mutex, RT_WAITING_FOREVER) != RT_EOK) + { + rt_kprintf("window %c take mutex failed\n", name); + break; + } + + /* 检查余票 */ + if (ticket_count <= 0) + { + rt_mutex_release(ticket_mutex); + break; + } + + /* 记录当前票号 */ + ticket_no = ticket_count; + + /* 售出一张票 */ + ticket_count--; + + rt_kprintf("window %c sold ticket %d\n", + name, + ticket_no); + + /* 释放互斥量 */ + rt_mutex_release(ticket_mutex); + + /* 模拟售票间隔 */ + rt_thread_mdelay(100); + } + + /* 记录完成窗口数量 */ + rt_enter_critical(); + + finished_windows++; + finished = finished_windows; + + rt_exit_critical(); + + /* 最后一个线程负责清理 */ + if (finished == 2) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: %d\n", + ticket_count); + + if (ticket_mutex != RT_NULL) + { + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + } + + rt_enter_critical(); + + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("ticket demo finished\n"); + } +} + + +/* 售票实验 */ +static void ticket_demo(void) +{ + rt_thread_t tid_a; + rt_thread_t tid_b; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("ticket demo is already running\n"); + return; + } + + demo_running = 1; + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + + rt_exit_critical(); + + /* 创建互斥量 */ + ticket_mutex = rt_mutex_create("ticket_mtx", + RT_IPC_FLAG_PRIO); + + if (ticket_mutex == RT_NULL) + { + rt_kprintf("create ticket mutex failed\n"); + + demo_running = 0; + return; + } + + /* 创建窗口 A 线程 */ + tid_a = rt_thread_create("win_a", + ticket_entry, + &window_name[0], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_a == RT_NULL) + { + rt_kprintf("create window A thread failed\n"); + + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建窗口 B 线程 */ + tid_b = rt_thread_create("win_b", + ticket_entry, + &window_name[1], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_b == RT_NULL) + { + rt_kprintf("create window B thread failed\n"); + + rt_thread_delete(tid_a); + + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("ticket demo start\n"); + rt_kprintf("total tickets: %d\n", TOTAL_TICKETS); + + /* 启动售票线程 */ + rt_thread_startup(tid_a); + rt_thread_startup(tid_b); +} + +MSH_CMD_EXPORT(ticket_demo, ticket mutex demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..659f25572aec14cc846feaaf90a39e73b1c2771c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" @@ -0,0 +1,144 @@ +#include + +#define TOTAL_TICKETS 20 + +#define THREAD_STACK_SIZE 1024 +#define THREAD_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +static rt_int32_t ticket_count = TOTAL_TICKETS; +static rt_uint8_t finished_windows = 0; +static rt_uint8_t demo_running = 0; + +static char window_name[2] = {'A', 'B'}; + + +/* 售票线程 */ +static void ticket_entry(void *parameter) +{ + char name; + rt_int32_t ticket_no; + rt_uint8_t finished; + + name = *(char *)parameter; + + while (1) + { + /* 检查余票 */ + if (ticket_count <= 0) + { + break; + } + + /* 读取当前票号 */ + ticket_no = ticket_count; + + /* 放大线程竞争 */ + rt_thread_mdelay(50); + + /* 修改余票 */ + ticket_count--; + + rt_kprintf("window %c sold ticket %d\n", + name, + ticket_no); + + /* 模拟售票间隔 */ + rt_thread_mdelay(100); + } + + /* 记录完成窗口数量 */ + rt_enter_critical(); + + finished_windows++; + finished = finished_windows; + + rt_exit_critical(); + + /* 最后一个线程负责结束实验 */ + if (finished == 2) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: %d\n", + ticket_count); + + rt_enter_critical(); + + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("ticket differ demo finished\n"); + } +} + + +/* 无互斥量售票实验 */ +static void ticketdiffer_demo(void) +{ + rt_thread_t tid_a; + rt_thread_t tid_b; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("ticket differ demo is already running\n"); + return; + } + + demo_running = 1; + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + + rt_exit_critical(); + + /* 创建窗口 A 线程 */ + tid_a = rt_thread_create("diff_a", + ticket_entry, + &window_name[0], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_a == RT_NULL) + { + rt_kprintf("create window A thread failed\n"); + + demo_running = 0; + return; + } + + /* 创建窗口 B 线程 */ + tid_b = rt_thread_create("diff_b", + ticket_entry, + &window_name[1], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_b == RT_NULL) + { + rt_kprintf("create window B thread failed\n"); + + rt_thread_delete(tid_a); + + demo_running = 0; + return; + } + + rt_kprintf("ticket differ demo start\n"); + rt_kprintf("total tickets: %d\n", TOTAL_TICKETS); + rt_kprintf("mutex: disabled\n"); + + /* 启动售票线程 */ + rt_thread_startup(tid_a); + rt_thread_startup(tid_b); +} + +MSH_CMD_EXPORT(ticketdiffer_demo, ticket race condition demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" new file mode 100644 index 0000000000000000000000000000000000000000..dddecbbbe28d40daae2c7db76a0dd8dcf6a117ca --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" @@ -0,0 +1,1031 @@ +# Day3 IPC 实验报告 + +## 实验一:停车场车位管理 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 计数信号量 `Semaphore`,主要涉及 `rt_sem_create()`、`rt_sem_take()`、`rt_sem_release()`、`rt_sem_delete()` 以及超时等待机制 | +| 为什么要这么做 | 停车场只有 3 个车位,但同时有 5 辆车申请进入,需要通过计数信号量限制同时获得资源的线程数量 | +| 意义是什么 | 掌握有限资源的管理方式,理解信号量如何在线程之间完成资源计数、阻塞等待、超时重试和资源释放 | + +### 1. 实验目的 + +模拟只有 3 个车位的停车场,同时创建 5 个车辆线程。每辆车进入停车场前必须申请一个车位,离开时释放车位。当停车场已满时,车辆最多等待 2 秒,超时后打印等待信息,并在稍后重新申请。 + +通过该实验验证计数信号量对有限资源数量的控制作用。 + +### 2. 实验文件 + +```text +parking_demo.c +``` + +MSH 命令: + +```text +parking_demo +``` + +### 3. 实验设计 + +停车场车位数量设置为: + +```c +#define PARKING_SPACES 3 +``` + +车辆线程数量设置为: + +```c +#define CAR_NUM 5 +``` + +创建初始值为 3 的计数信号量: + +```c +parking_sem = rt_sem_create("park_sem", + PARKING_SPACES, + RT_IPC_FLAG_FIFO); +``` + +车辆进入前使用: + +```c +rt_sem_take() +``` + +申请车位,并设置 2 秒超时时间。 + +车辆离开后使用: + +```c +rt_sem_release() +``` + +归还车位。 + +所有车辆线程结束后使用: + +```c +rt_sem_delete() +``` + +删除动态创建的信号量对象。 + +### 4. 实际运行结果 + +执行: + +```text +msh >parking_demo +``` + +实际输出: + +![524f1d54-c940-4f06-b499-3276fcf9362f](figures/524f1d54-c940-4f06-b499-3276fcf9362f.png) + +```text +parking demo start +total spaces: 3, total cars: 5 + +car5 entered, used spaces: 1 +car4 entered, used spaces: 2 +car3 entered, used spaces: 3 + +car2 waiting for a space +car1 waiting for a space + +car5 left, used spaces: 2 +car2 entered, used spaces: 3 + +car4 left, used spaces: 2 +car1 entered, used spaces: 3 + +car3 left, used spaces: 2 +car2 left, used spaces: 1 +car1 left, used spaces: 0 + +all cars finished +parking demo finished +``` + +### 5. 实验结果分析 + +实验启动后,car5、car4 和 car3 首先成功进入停车场: + +```text +car5 entered, used spaces: 1 +car4 entered, used spaces: 2 +car3 entered, used spaces: 3 +``` + +此时 3 个车位已经全部被占用,因此 car2 和 car1 无法获得信号量。在等待 2 秒后输出: + +```text +car2 waiting for a space +car1 waiting for a space +``` + +car5 离开后释放一个信号量,car2 随后成功获得车位: + +```text +car5 left, used spaces: 2 +car2 entered, used spaces: 3 +``` + +car4 离开后,car1 同样成功进入。 + +整个实验过程中: + +```text +最大同时占用车位数 = 3 +``` + +没有出现 4 辆或 5 辆车同时进入停车场的情况。 + +最终: + +```text +car1 left, used spaces: 0 +``` + +说明所有车辆均已离开,已使用车位数量恢复为 0。 + +### 6. 实验数据总结 + +| 项目 | 实际数据 | +| ---------------- | ---------------- | +| 总车位数量 | 3 | +| 车辆线程数量 | 5 | +| 首批进入车辆 | car5、car4、car3 | +| 首次等待车辆 | car2、car1 | +| 最大同时占用车位 | 3 | +| 最终占用车位 | 0 | +| 实验结果 | 正常完成 | + +### 7. 问题与解决方法 + +运行过程中出现过: + +```text +msh >car5 entered... +``` + +类似现象。 + +这是由于 MSH 命令执行完成后重新输出命令提示符,而车辆线程同时调用 `rt_kprintf()` 输出信息,导致两个输出在串口终端中发生交错。 + +该现象不会影响信号量以及线程的实际运行结果,因此无需修改核心逻辑。 + +### 8. 思考题 + +**为什么这里使用计数信号量,而不是互斥量?** + +停车场存在 3 个相同的车位资源,因此需要允许最多 3 个车辆线程同时获得资源。 + +计数信号量可以通过初始值表示可用资源数量。本实验将信号量初始值设置为 3,每辆车进入时获取一次信号量,离开时释放一次信号量,从而保证同时进入停车场的车辆数量不超过 3。 + +互斥量主要用于保护只能由一个线程独占访问的共享资源。如果使用互斥量,同一时刻通常只能有一个车辆线程获得资源,无法正确表示停车场具有 3 个车位的实际情况。 + +------ + +## 实验二:多窗口售票系统 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 互斥量 `Mutex`、临界区以及多线程访问共享变量时产生的竞态条件 | +| 为什么要这么做 | 两个售票窗口同时修改同一个 `ticket_count`,如果缺少互斥保护,线程可能同时读取相同票号并产生重复售票 | +| 意义是什么 | 理解共享资源必须进行完整的互斥保护,掌握竞态条件产生的原因以及 Mutex 对临界区的保护作用 | + +### 1. 实验目的 + +创建窗口 A 和窗口 B 两个售票线程,共同出售 20 张票。 + +首先进行无互斥量实验,在读取余票和修改余票之间加入延时,主动放大竞态条件。 + +随后使用互斥量保护完整售票过程,对比两次实验结果。 + +实验要求最终正确版本满足: + +```text +20 张票全部售出 +没有重复票号 +余票不能小于 0 +remaining tickets = 0 +``` + +### 2. 实验文件 + +正确版本: + +```text +ticket_demo.c +``` + +无互斥量对比版本: + +```text +ticketdiffer_demo.c +``` + +对应 MSH 命令: + +```text +ticket_demo +ticketdiffer_demo +``` + +------ + +### 3. 无互斥量对比实验 + +无互斥量版本中,两个线程均直接访问: + +```c +ticket_count +``` + +在读取当前票号和修改余票之间故意加入: + +```c +rt_thread_mdelay(50); +``` + +其主要过程为: + +```c +ticket_no = ticket_count; + +rt_thread_mdelay(50); + +ticket_count--; +``` + +该延时增加了两个线程同时读取相同 `ticket_count` 的概率。 + +### 4. 无互斥量实际运行结果 + +实际运行后出现大量重复票号。 + +关键输出为: + +![abd99143-5245-4c1d-a2c7-bc8249680eaf](figures/abd99143-5245-4c1d-a2c7-bc8249680eaf.png) + +```text +window B sold ticket 20 +window A sold ticket 20 + +window B sold ticket 18 +window A sold ticket 18 + +window B sold ticket 16 +window A sold ticket 16 + +window B sold ticket 14 +window A sold ticket 14 + +window B sold ticket 12 +window A sold ticket 12 + +window B sold ticket 10 +window A sold ticket 10 + +window B sold ticket 8 +window A sold ticket 8 + +window B sold ticket 6 +window A sold ticket 6 + +... +``` + +后续票号 4 和票号 2 同样出现两个窗口同时输出的情况。 + +最终: + +```text +all tickets sold +remaining tickets: 0 +ticket differ demo finished +``` + +### 5. 无互斥量结果分析 + +虽然最终: + +```text +remaining tickets: 0 +``` + +但售票过程并不正确。 + +例如初始: + +```text +ticket_count = 20 +``` + +两个线程可能执行: + +```text +窗口 A 读取 ticket_count = 20 +窗口 B 读取 ticket_count = 20 +``` + +因此两个线程保存的票号均为: + +```text +ticket_no = 20 +``` + +随后两个线程分别执行: + +```c +ticket_count--; +``` + +虽然 `ticket_count` 最终从 20 减少到 18,但是两个窗口打印出的票号均为: + +```text +ticket 20 +``` + +于是出现: + +```text +20、20 +18、18 +16、16 +... +``` + +说明“读取票号”和“修改余票”并不是一个不可分割的操作,从而产生竞态条件。 + +------ + +### 6. 加入互斥量实验 + +正确版本使用: + +```c +rt_mutex_take() +``` + +获取互斥量。 + +完整的以下过程均放入临界区: + +```text +检查余票 + ↓ +读取当前票号 + ↓ +修改剩余票数 + ↓ +打印售票结果 +``` + +完成后调用: + +```c +rt_mutex_release() +``` + +释放互斥量。 + +所有线程完成后调用: + +![e5f26e3a-f99e-46ea-bd29-4e8cdb92c3d3](figures/e5f26e3a-f99e-46ea-bd29-4e8cdb92c3d3.png) + +```c +rt_mutex_delete() +``` + +删除互斥量对象。 + +### 7. 加互斥量实际运行结果 + +实际输出: + +```text +window B sold ticket 20 +window A sold ticket 19 +window B sold ticket 18 +window A sold ticket 17 +window B sold ticket 16 +window A sold ticket 15 +window B sold ticket 14 +window A sold ticket 13 +window B sold ticket 12 +window A sold ticket 11 +window B sold ticket 10 +window A sold ticket 9 +window B sold ticket 8 +window A sold ticket 7 +window B sold ticket 6 +window A sold ticket 5 +window B sold ticket 4 +window A sold ticket 3 +window B sold ticket 2 +window A sold ticket 1 + +all tickets sold +remaining tickets: 0 +ticket demo finished +``` + +### 8. 对比结果 + +| 对比项 | 无 Mutex | 使用 Mutex | +| ---------------- | ---------------- | ----------------- | +| 初始票数 | 20 | 20 | +| 两个窗口并发运行 | 是 | 是 | +| 重复售票 | 大量出现 | 未出现 | +| 实际票号 | 20、20、18、18…… | 20、19、18、17……1 | +| 最终余票 | 0 | 0 | +| 是否存在竞态条件 | 是 | 否 | +| 售票结果 | 错误 | 正确 | + +实验说明,即使最终余票为 0,也不能说明并发程序一定正确,还必须检查每次共享数据操作是否保持一致性。 + +### 9. 问题与解决方法 + +无互斥量实验中出现了两个窗口售出相同票号的问题。 + +原因是两个线程可以在同一时刻读取同一个 `ticket_count`,随后分别修改共享变量。 + +解决方法是使用互斥量,将: + +```text +检查余票 +读取票号 +余票减 1 +记录结果 +``` + +作为一个完整的临界区进行保护。 + +### 10. 思考题 + +**为什么只给 `ticket_count--` 加锁仍可能不安全?** + +售票操作不仅包含 `ticket_count--`,还包含检查余票和读取当前票号。 + +如果两个线程在获取锁之前都执行: + +```c +ticket_no = ticket_count; +``` + +那么两个线程仍然可能读取到相同票号。 + +即使随后: + +```c +ticket_count--; +``` + +分别受到互斥保护,也已经无法避免重复售票。 + +因此需要保护完整的售票过程,而不能只保护单独的一条减法语句。 + +------ + +## 实验三:系统启动条件检查 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 事件集 `Event`、事件位、`RT_EVENT_FLAG_AND`、事件超时等待 | +| 为什么要这么做 | 系统启动需要同时满足网络、传感器和存储三个独立条件,单个条件完成时业务线程不能提前启动 | +| 意义是什么 | 掌握一个线程同时等待多个条件的方法,理解事件集在多条件同步场景中的优势 | + +### 1. 实验目的 + +模拟系统启动过程中的三个独立模块: + +```text +网络模块 +传感器模块 +存储模块 +``` + +分别定义三个事件: + +```c +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) +``` + +主业务线程使用 AND 模式等待三个事件全部完成。 + +同时设置: + +```text +网络初始化:1 秒 +传感器初始化:2 秒 +存储初始化:5 秒 +主业务线程首次最大等待:3 秒 +``` + +通过该设计验证事件集的 AND 等待以及超时机制。 + +### 2. 实验文件 + +```text +startup_demo.c +``` + +MSH 命令: + +```text +startup_demo +``` + +### 3. 实验设计 + +三个初始化线程分别完成初始化后发送对应事件: + +```c +rt_event_send() +``` + +主业务线程第一次调用: + +```c +rt_event_recv() +``` + +并设置: + +```c +RT_EVENT_FLAG_AND +``` + +要求三个事件必须全部满足。 + +首次等待时间设置为: + +```text +3 秒 +``` + +由于存储线程需要: + +```text +5 秒 +``` + +因此第一次等待必然发生超时。 + +超时后程序检查未完成模块,并再次等待三个事件全部到达。 + +### 4. 实际运行结果 + +执行: + +```text +msh >startup_demo +``` + +实际输出: + +![da3d734b-9772-4860-8a95-941c0695e303](figures/da3d734b-9772-4860-8a95-941c0695e303.png) + +```text +startup demo start + +network ready +sensor ready + +wait modules timeout, storage is not ready + +storage ready + +all modules are ready +business task started +startup demo finished +``` + +### 5. 实验结果分析 + +实验启动约 1 秒后: + +```text +network ready +``` + +说明网络初始化线程完成。 + +约 2 秒后: + +```text +sensor ready +``` + +说明传感器初始化线程完成。 + +此时存储模块尚未初始化完成,因此即使已经有两个事件到达,业务线程仍然不能启动。 + +主业务线程等待 3 秒后输出: + +```text +wait modules timeout, storage is not ready +``` + +说明第一次 `rt_event_recv()` 等待发生超时,并正确判断出存储模块尚未完成。 + +约 5 秒时: + +```text +storage ready +``` + +此时: + +```text +NETWORK + AND +SENSOR + AND +STORAGE +``` + +三个条件全部满足,因此业务线程继续执行: + +```text +all modules are ready +business task started +``` + +实验结果证明 `RT_EVENT_FLAG_AND` 可以正确实现多个条件全部满足后再继续运行的同步要求。 + +### 6. 实验数据总结 + +| 项目 | 实际结果 | +| ---------------- | ---------------- | +| 网络初始化时间 | 约 1 s | +| 传感器初始化时间 | 约 2 s | +| 存储初始化时间 | 约 5 s | +| 首次最大等待时间 | 3 s | +| 第一次等待 | 超时 | +| 未完成模块 | Storage | +| 第二次等待 | 成功 | +| 业务线程启动条件 | 三个事件全部到达 | +| 最终结果 | 正常启动 | + +### 7. 问题与解决方法 + +本实验中的首次超时属于主动设计的测试条件,并不是程序异常。 + +存储模块延时设置为 5 秒,而业务线程第一次仅等待 3 秒,因此能够稳定触发超时。 + +超时后没有直接退出业务线程,而是打印未完成模块并再次调用事件等待,使程序能够在存储模块最终完成后继续启动。 + +### 8. 思考题 + +**如果使用三个独立信号量,与使用事件集相比有什么区别?** + +如果使用三个独立信号量,需要分别创建三个信号量,并由主业务线程依次获取: + +```text +net_sem +sensor_sem +storage_sem +``` + +程序需要分别维护三个同步对象。 + +事件集可以使用不同的 bit 表示多个状态: + +```text +bit0:network +bit1:sensor +bit2:storage +``` + +再使用: + +```c +RT_EVENT_FLAG_AND +``` + +一次等待多个条件全部满足。 + +因此,对于“多个独立条件全部完成后才能继续”的场景,事件集的状态表示和等待逻辑更加集中、直观。 + +------ + +## 实验四:快递分拣中心 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 消息队列 `Message Queue`、结构体消息传递、生产者与消费者模型、队列满处理 | +| 为什么要这么做 | 两个收件线程需要把包含编号、区域和重量的完整快递信息发送给分拣线程,仅使用同步机制无法直接完成结构化数据传输 | +| 意义是什么 | 掌握线程之间传递复杂数据的方法,并理解消息缓存、发送失败、重试以及生产速度和消费速度不一致时的处理方式 | + +### 1. 实验目的 + +创建两个快递发送线程,每个线程产生 5 条快递信息。 + +快递使用以下结构: + +```c +struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; + rt_uint16_t weight; +}; +``` + +两个发送线程通过消息队列将快递信息发送给分拣线程。 + +分拣线程完成: + +```text +快递编号输出 +区域分类 +重量输出 +总数量统计 +各区域数量统计 +总重量统计 +``` + +消息队列容量限制为 5 条,并记录队列满时的发送失败次数。 + +### 2. 实验文件 + +```text +package_demo.c +``` + +MSH 命令: + +```text +package_demo +``` + +### 3. 实验设计 + +消息队列容量设置为: + +```c +#define PACKAGE_QUEUE_SIZE 5 +``` + +Sender 1 产生: + +```text +1001 ~ 1005 +``` + +Sender 2 产生: + +```text +2001 ~ 2005 +``` + +因此不存在快递编号重复问题。 + +两个发送线程使用: + +```c +rt_mq_send() +``` + +发送消息。 + +分拣线程使用: + +```c +rt_mq_recv() +``` + +接收完整的 `package_msg` 数据。 + +当消息队列已满时: + +```c +rt_mq_send() +``` + +发送失败,程序记录: + +```c +send_failed++; +``` + +随后等待一段时间并重新发送当前消息,避免快递数据丢失。 + +最后一个发送线程完成后发送结束消息,分拣线程收到结束消息后输出统计结果并退出。 + +### 4. 实际运行结果 + +执行: + +```text +msh >package_demo +``` + +实际输出: + +![b375f5f5-5d33-47d4-973c-6646adb2c1aa](figures/b375f5f5-5d33-47d4-973c-6646adb2c1aa.png) + +```text +package demo start + +package 2001 -> south, weight: 980 g +package 1001 -> east, weight: 880 g +package 2002 -> north, weight: 1060 g +package 1002 -> south, weight: 960 g +package 1003 -> north, weight: 1040 g +package 2003 -> east, weight: 1140 g +package 1004 -> east, weight: 1120 g +package 2004 -> south, weight: 1220 g +package 1005 -> south, weight: 1200 g +package 2005 -> north, weight: 1300 g + +total packages: 10 +east: 3, south: 4, north: 3 +total weight: 10900 g +send failed: 6 +package demo finished +``` + +### 5. 实验结果分析 + +两个发送线程总共产生: + +```text +5 + 5 = 10 +``` + +条快递消息。 + +实际分拣结果: + +```text +total packages: 10 +``` + +说明 10 条快递最终全部被分拣线程成功接收。 + +各区域统计为: + +```text +east: 3 +south: 4 +north: 3 +``` + +满足: + +```text +3 + 4 + 3 = 10 +``` + +总重量统计为: + +```text +10900 g +``` + +与 10 条实际快递重量之和一致。 + +实验中还得到: + +```text +send failed: 6 +``` + +说明运行过程中消息队列曾经达到容量上限。 + +由于消息队列只能存储 5 条消息,而两个发送线程同时不断产生消息,分拣线程还需要一定时间处理每一条快递,因此在部分时刻出现: + +```text +生产速度 > 消费速度 +``` + +导致: + +```c +rt_mq_send() +``` + +返回队列满错误。 + +程序没有直接丢弃该快递,而是在记录一次失败后重新尝试发送,因此虽然出现: + +```text +send failed: 6 +``` + +最终仍然完成: + +```text +total packages: 10 +``` + +说明队列满处理和重试逻辑正常。 + +### 6. 实验数据总结 + +| 项目 | 实际数据 | +| --------------- | -------- | +| 消息队列容量 | 5 | +| 发送线程数量 | 2 | +| Sender 1 消息数 | 5 | +| Sender 2 消息数 | 5 | +| 总快递数量 | 10 | +| East | 3 | +| South | 4 | +| North | 3 | +| 总重量 | 10900 g | +| 发送失败次数 | 6 | +| 最终丢失消息 | 0 | +| 实验结果 | 正常完成 | + +### 7. 问题与解决方法 + +实验中出现: + +```text +send failed: 6 +``` + +说明部分消息发送时消息队列已经达到容量上限。 + +如果发送失败后直接进入下一条消息,会导致部分快递丢失,最终: + +```text +total packages +``` + +可能小于 10。 + +因此在发送失败后采用: + +```text +记录失败次数 + ↓ +短暂延时 + ↓ +重新发送当前消息 +``` + +的方式处理。 + +最终虽然出现 6 次发送失败,但 10 条快递均成功进入消息队列并完成分拣。 + +同时运行过程中出现: + +```text +msh >package 2001... +``` + +属于 MSH 命令提示符与线程打印输出交错,对实验结果无影响。 + +------ + +# 实验总结 + +本次实验分别使用不同 IPC 机制解决不同类型的多线程问题。 + +| 实验 | IPC 机制 | 主要解决的问题 | 实际结果 | +| -------------- | ------------- | -------------------- | ------------------------------ | +| 停车场车位管理 | Semaphore | 有限资源数量控制 | 最大同时停车 3 辆 | +| 多窗口售票 | Mutex | 共享资源互斥访问 | 20 张票无重复售出 | +| 售票对比实验 | 无 Mutex | 验证竞态条件 | 出现大量重复票号 | +| 系统启动检查 | Event | 多条件同步 | 3 秒超时,5 秒后三模块全部就绪 | +| 快递分拣 | Message Queue | 线程间结构化数据通信 | 10 条全部完成,队满 6 次 | + +实验结果表明,不同 IPC 机制解决的问题并不相同: + +```text +Semaphore + ↓ +控制有限资源数量 + +Mutex + ↓ +保护共享资源 + +Event + ↓ +等待多个条件 + +Message Queue + ↓ +在线程之间传递完整消息 +``` + +通过实际的线程竞争、超时等待和队列满实验,可以更加直观地观察 RT-Thread IPC 机制在线程同步与线程通信中的作用。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" similarity index 100% rename from "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" rename to "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" similarity index 100% rename from "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" rename to "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" new file mode 100644 index 0000000000000000000000000000000000000000..1eaad937dd676fbeb19b534e72b2b7b258f06662 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" @@ -0,0 +1,15 @@ +from building import * +import os + +cwd = GetCurrentDir() +CPPPATH = [cwd] +src = Glob('*.c') + +group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH) + +list = os.listdir(cwd) +for item in list: + if os.path.isfile(os.path.join(cwd, item, 'SConscript')): + group = group + SConscript(os.path.join(item, 'SConscript')) + +Return('group') diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" new file mode 100644 index 0000000000000000000000000000000000000000..327c83c5eb2937a3fc81264c076039b2749f46b9 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" @@ -0,0 +1,54 @@ +#include +#include +#include "sensor_asair_aht10.h" + +/* + * Board BSP: + * i2c3 -> onboard AHT sensor + * SCL -> PE0 + * SDA -> PE1 + */ +#define AHT10_I2C_BUS_NAME "i2c3" + +/** + * @brief Initialize AHT10 sensor framework device + * + * After initialization, two sensor devices will be registered: + * temp_aht + * humi_aht + */ +static int aht10_port_init(void) +{ + struct rt_sensor_config cfg; + int result; + + rt_memset(&cfg, 0, sizeof(cfg)); + + /* Use onboard i2c3 bus */ + cfg.intf.dev_name = AHT10_I2C_BUS_NAME; + + /* + * AHT10 default I2C address. + * AHT10 package provides AHT10_I2C_ADDR. + */ + cfg.intf.user_data = (void *)AHT10_I2C_ADDR; + + result = rt_hw_aht10_init("aht10", &cfg); + + if (result != RT_EOK) + { + rt_kprintf("[AHT10] init failed, bus: %s, result: %d\n", + AHT10_I2C_BUS_NAME, + result); + + return -RT_ERROR; + } + + rt_kprintf("[AHT10] init success, bus: %s\n", + AHT10_I2C_BUS_NAME); + + return RT_EOK; +} + +/* Initialize automatically during RT-Thread startup */ +INIT_ENV_EXPORT(aht10_port_init); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" new file mode 100644 index 0000000000000000000000000000000000000000..c1c701386d280648a411e226d29deaaaf2021c20 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" @@ -0,0 +1,280 @@ +#include +#include + +#include + +#include +#include + +#include "day5_fs.h" + + +#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; + + +/* ============================== + * 创建 font MTD NOR Device + * ============================== */ + +static int day5_font_device_init(void) +{ + struct rt_device *font_dev; + + + fal_init(); + + + font_dev = + rt_device_find(FONT_PARTITION_NAME); + + + if (font_dev != RT_NULL) + { + return RT_EOK; + } + + + font_dev = + fal_mtd_nor_device_create( + FONT_PARTITION_NAME); + + + if (font_dev == RT_NULL) + { + rt_kprintf( + "[FS] create font MTD device failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] font MTD device created\n"); + + + return RT_EOK; +} + + +/* ============================== + * 挂载 LittleFS + * ============================== */ + +static int day5_font_mount(void) +{ + if (font_mounted) + { + rt_kprintf( + "[FS] /font already mounted\n"); + + return RT_EOK; + } + + + if (day5_font_device_init() + != RT_EOK) + { + return -RT_ERROR; + } + + + if (dfs_mount(FONT_PARTITION_NAME, + FONT_MOUNT_PATH, + "lfs", + 0, + RT_NULL) != RT_EOK) + { + rt_kprintf( + "[FS] mount /font failed\n"); + + return -RT_ERROR; + } + + + font_mounted = RT_TRUE; + + + rt_kprintf( + "[FS] LittleFS mounted to %s successfully\n", + FONT_MOUNT_PATH); + + + return RT_EOK; +} + + +/* ============================== + * 格式化 + * + * 注意: + * 只在第一次没有文件系统时使用 + * 平时不要执行 + * ============================== */ + +static int day5_font_format(void) +{ + if (font_mounted) + { + rt_kprintf( + "[FS] /font is mounted, refuse to format\n"); + + return -RT_ERROR; + } + + + if (day5_font_device_init() + != RT_EOK) + { + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] formatting font partition...\n"); + + + if (dfs_mkfs("lfs", + FONT_PARTITION_NAME) + != RT_EOK) + { + rt_kprintf( + "[FS] format failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] format success\n"); + + + return RT_EOK; +} + + +/* ============================== + * 写 Data.txt + * ============================== */ + +int day5_fs_append_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + int fd; + + int len; + + int written; + + char line[128]; + + rt_int32_t temp_decimal; + + rt_int32_t humi_decimal; + + + if (!font_mounted) + { + rt_kprintf( + "[FS] /font is not mounted\n"); + + return -RT_ERROR; + } + + + temp_decimal = temp % 10; + + humi_decimal = humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = + -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = + -humi_decimal; + } + + + len = + rt_snprintf( + line, + sizeof(line), + "Temp: %d.%d ; Humi: %d.%d ; Count: %u\r\n", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); + + + fd = + open(DATA_FILE_PATH, + O_WRONLY | + O_CREAT | + O_APPEND, + 0); + + + if (fd < 0) + { + rt_kprintf( + "[FS] open %s failed\n", + DATA_FILE_PATH); + + return -RT_ERROR; + } + + + written = + write(fd, + line, + len); + + + close(fd); + + + if (written != len) + { + rt_kprintf( + "[FS] write Data.txt failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] append success: %s", + line); + + + return RT_EOK; +} + + +/* ============================== + * Shell 初始化命令 + * ============================== */ + +static int day5_fs_init(void) +{ + return day5_font_mount(); +} + + +MSH_CMD_EXPORT(day5_fs_init, + mount font partition with littlefs); + +MSH_CMD_EXPORT(day5_font_format, + format font partition with littlefs); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" new file mode 100644 index 0000000000000000000000000000000000000000..0620eeb803a3c440f51136dd01e76670b1d40c4c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" @@ -0,0 +1,10 @@ +#ifndef __DAY5_FS_H__ +#define __DAY5_FS_H__ + +#include + +int day5_fs_append_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count); + +#endif diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" new file mode 100644 index 0000000000000000000000000000000000000000..99863b08a2c8117902a958f9363238fa565c41e4 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" @@ -0,0 +1,447 @@ +#include +#include + +#include "paho_mqtt.h" +#include "day5_mqtt.h" + + +#define MQTT_URI "tcp://broker.emqx.io:1883" + +#define MQTT_PUB_TOPIC "rsoc26/day5/kylehe/device/up" +#define MQTT_SUB_TOPIC "rsoc26/day5/kylehe/device/down" + +/* PF12 */ +#define PIN_LED_R 92 + +/* 板载 LED 低电平点亮 */ +#define LED_ON PIN_LOW +#define LED_OFF PIN_HIGH + + +static MQTTClient mqtt_client; + +static rt_bool_t mqtt_started = RT_FALSE; +static rt_bool_t mqtt_online = RT_FALSE; + +static rt_bool_t led_state = RT_FALSE; + +static char mqtt_client_id[32]; + + +/* ======================================== + * LED + * ======================================== */ + +static void day5_led_init(void) +{ + rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); + + led_state = RT_FALSE; + + rt_pin_write(PIN_LED_R, LED_OFF); + + rt_kprintf("[LED] initialized, state: OFF\n"); +} + + +static void day5_led_toggle(void) +{ + led_state = !led_state; + + if (led_state) + { + rt_pin_write(PIN_LED_R, LED_ON); + + rt_kprintf("[LED] ON\n"); + } + else + { + rt_pin_write(PIN_LED_R, LED_OFF); + + rt_kprintf("[LED] OFF\n"); + } +} + + +/* ======================================== + * MQTT callbacks + * ======================================== */ + +static void day5_mqtt_connect_callback(MQTTClient *client) +{ + rt_kprintf("[MQTT] connect callback\n"); +} + + +static void day5_mqtt_online_callback(MQTTClient *client) +{ + mqtt_online = RT_TRUE; + + rt_kprintf("[MQTT] online\n"); + rt_kprintf("[MQTT] broker: %s\n", MQTT_URI); + rt_kprintf("[MQTT] sub topic: %s\n", MQTT_SUB_TOPIC); +} + + +static void day5_mqtt_offline_callback(MQTTClient *client) +{ + mqtt_online = RT_FALSE; + + rt_kprintf("[MQTT] offline\n"); +} + + +/* MQTT 下行消息处理 */ +static void day5_mqtt_sub_callback(MQTTClient *client, + MessageData *msg_data) +{ + char payload[32]; + int len; + + len = msg_data->message->payloadlen; + + if (len >= (int)sizeof(payload)) + { + len = sizeof(payload) - 1; + } + + rt_memcpy(payload, + msg_data->message->payload, + len); + + payload[len] = '\0'; + + + /* 去掉末尾的空格、回车、换行 */ + while (len > 0) + { + if (payload[len - 1] == '\r' || + payload[len - 1] == '\n' || + payload[len - 1] == ' ') + { + payload[len - 1] = '\0'; + len--; + } + else + { + break; + } + } + + + rt_kprintf("[MQTT] recv topic: %.*s\n", + msg_data->topicName->lenstring.len, + msg_data->topicName->lenstring.data); + + rt_kprintf("[MQTT] recv payload: %s\n", + payload); + + rt_kprintf("[MQTT] payload length: %d\n", + len); + + + if ((len == 6) && + (rt_memcmp(payload, "toggle", 6) == 0)) + { + rt_kprintf("[MQTT] toggle command matched\n"); + + day5_led_toggle(); + } + else + { + rt_kprintf("[MQTT] unknown command\n"); + } +} + + +/* ======================================== + * MQTT 启动 + * ======================================== */ + +static int day5_mqtt_start(void) +{ + MQTTPacket_connectData condata = + MQTTPacket_connectData_initializer; + + + if (mqtt_started) + { + rt_kprintf("[MQTT] already started\n"); + + return -RT_ERROR; + } + + + /* 初始化 LED */ + day5_led_init(); + + + /* 清空 MQTT Client */ + rt_memset(&mqtt_client, + 0, + sizeof(mqtt_client)); + + + /* 生成客户端 ID */ + rt_snprintf(mqtt_client_id, + sizeof(mqtt_client_id), + "rsoc26-kylehe-%u", + rt_tick_get()); + + + mqtt_client.uri = MQTT_URI; + + + rt_memcpy(&mqtt_client.condata, + &condata, + sizeof(condata)); + + + mqtt_client.condata.clientID.cstring = + mqtt_client_id; + + mqtt_client.condata.keepAliveInterval = + 30; + + mqtt_client.condata.cleansession = + 1; + + mqtt_client.condata.willFlag = + 0; + + + /* MQTT 缓冲区 */ + 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_kprintf("[MQTT] buffer malloc failed\n"); + + + if (mqtt_client.buf != RT_NULL) + { + rt_free(mqtt_client.buf); + + mqtt_client.buf = RT_NULL; + } + + + if (mqtt_client.readbuf != RT_NULL) + { + rt_free(mqtt_client.readbuf); + + mqtt_client.readbuf = RT_NULL; + } + + + return -RT_ENOMEM; + } + + + /* MQTT 状态回调 */ + mqtt_client.connect_callback = + day5_mqtt_connect_callback; + + mqtt_client.online_callback = + day5_mqtt_online_callback; + + mqtt_client.offline_callback = + day5_mqtt_offline_callback; + + + /* 注册下行 Topic */ + mqtt_client.messageHandlers[0].topicFilter = + rt_strdup(MQTT_SUB_TOPIC); + + mqtt_client.messageHandlers[0].callback = + day5_mqtt_sub_callback; + + mqtt_client.messageHandlers[0].qos = + QOS0; + + + mqtt_client.defaultMessageHandler = + day5_mqtt_sub_callback; + + + rt_kprintf("[MQTT] starting...\n"); + + rt_kprintf("[MQTT] client id: %s\n", + mqtt_client_id); + + + if (paho_mqtt_start(&mqtt_client) != RT_EOK) + { + rt_kprintf("[MQTT] start failed\n"); + + return -RT_ERROR; + } + + + mqtt_started = RT_TRUE; + + + return RT_EOK; +} + + +/* ======================================== + * 温湿度 MQTT 自动上传 + * ======================================== */ + +int day5_mqtt_publish_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + char payload[128]; + + rt_int32_t temp_decimal; + rt_int32_t humi_decimal; + + + if (!mqtt_started) + { + rt_kprintf( + "[MQTT] skip sensor publish: client not started\n"); + + return -RT_ERROR; + } + + + if (!mqtt_online) + { + rt_kprintf( + "[MQTT] skip sensor publish: client offline\n"); + + return -RT_ERROR; + } + + + temp_decimal = temp % 10; + humi_decimal = humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = -humi_decimal; + } + + + rt_snprintf( + payload, + sizeof(payload), + "Temp: %d.%d ; Humi: %d.%d ; Count: %u", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); + + + /* 当前工程发布使用 QoS1 */ + if (paho_mqtt_publish(&mqtt_client, + QOS1, + MQTT_PUB_TOPIC, + payload) != RT_EOK) + { + rt_kprintf("[MQTT] sensor publish failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[MQTT] sensor publish success: %s\n", + payload); + + + return RT_EOK; +} + + +/* ======================================== + * 手动 MQTT Publish 测试 + * ======================================== */ + +static int day5_mqtt_pub(int argc, + char **argv) +{ + if (!mqtt_started) + { + rt_kprintf("[MQTT] client not started\n"); + + return -RT_ERROR; + } + + + if (!mqtt_online) + { + rt_kprintf("[MQTT] client not online yet\n"); + + return -RT_ERROR; + } + + + if (argc != 2) + { + rt_kprintf( + "usage: day5_mqtt_pub \n"); + + return -RT_ERROR; + } + + + if (paho_mqtt_publish(&mqtt_client, + QOS1, + MQTT_PUB_TOPIC, + argv[1]) != RT_EOK) + { + rt_kprintf("[MQTT] publish failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf("[MQTT] publish success\n"); + + rt_kprintf("[MQTT] topic: %s\n", + MQTT_PUB_TOPIC); + + rt_kprintf("[MQTT] data : %s\n", + argv[1]); + + + return RT_EOK; +} + + +/* ======================================== + * MSH Commands + * ======================================== */ + +MSH_CMD_EXPORT(day5_mqtt_start, + start day5 paho mqtt client); + +MSH_CMD_EXPORT(day5_mqtt_pub, + publish day5 mqtt test message); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" new file mode 100644 index 0000000000000000000000000000000000000000..4c80d9b40041ed2076d71b550f5be8a780f9908b --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" @@ -0,0 +1,10 @@ +#ifndef __DAY5_MQTT_H__ +#define __DAY5_MQTT_H__ + +#include + +int day5_mqtt_publish_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count); + +#endif diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" new file mode 100644 index 0000000000000000000000000000000000000000..617e620f2d282fd8994eb208eecb644ea4110344 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" @@ -0,0 +1,395 @@ +#include +#include + +#include "day5_mqtt.h" +#include "day5_fs.h" + + +#define TEMP_SENSOR_NAME "temp_aht10" +#define HUMI_SENSOR_NAME "humi_aht10" + +#define SAMPLE_PERIOD_MS 5000 + +#define WARMUP_COUNT 3 + + +static rt_thread_t sensor_tid = + RT_NULL; + +static rt_uint32_t sample_count = + 0; + + +/* ============================== + * 数据合法性检查 + * + * temp: + * 0.1 ℃ + * + * humi: + * 0.1 % + * ============================== */ + +static rt_bool_t sensor_data_valid( + rt_int32_t temp, + rt_int32_t humi) +{ + /* AHT 温度有效范围 */ + if (temp < -400 || + temp > 850) + { + return RT_FALSE; + } + + + /* 湿度 0 ~ 100 % */ + if (humi < 0 || + humi > 1000) + { + return RT_FALSE; + } + + + return RT_TRUE; +} + + +/* ============================== + * 读取一组数据 + * ============================== */ + +static rt_err_t sensor_read_once( + rt_device_t temp_dev, + rt_device_t humi_dev, + rt_int32_t *temp, + rt_int32_t *humi) +{ + struct rt_sensor_data temp_data; + + struct rt_sensor_data humi_data; + + + rt_memset(&temp_data, + 0, + sizeof(temp_data)); + + rt_memset(&humi_data, + 0, + sizeof(humi_data)); + + + if (rt_device_read(temp_dev, + 0, + &temp_data, + 1) != 1) + { + rt_kprintf( + "[DAY5] read temperature failed\n"); + + return -RT_ERROR; + } + + + if (rt_device_read(humi_dev, + 0, + &humi_data, + 1) != 1) + { + rt_kprintf( + "[DAY5] read humidity failed\n"); + + return -RT_ERROR; + } + + + *temp = + temp_data.data.temp; + + *humi = + humi_data.data.humi; + + + return RT_EOK; +} + + +/* ============================== + * 打印数据 + * ============================== */ + +static void sensor_print( + rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + rt_int32_t temp_decimal; + + rt_int32_t humi_decimal; + + + temp_decimal = + temp % 10; + + humi_decimal = + humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = + -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = + -humi_decimal; + } + + + rt_kprintf( + "[DAY5] Temp: %d.%d C ; Humi: %d.%d %% ; Count: %u\n", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); +} + + +/* ============================== + * Sensor Thread + * ============================== */ + +static void day5_sensor_thread( + void *parameter) +{ + rt_device_t temp_dev; + + rt_device_t humi_dev; + + rt_int32_t temp; + + rt_int32_t humi; + + int i; + + + /* 找温度设备 */ + temp_dev = + rt_device_find( + TEMP_SENSOR_NAME); + + + if (temp_dev == RT_NULL) + { + rt_kprintf( + "[DAY5] cannot find %s\n", + TEMP_SENSOR_NAME); + + return; + } + + + /* 找湿度设备 */ + humi_dev = + rt_device_find( + HUMI_SENSOR_NAME); + + + if (humi_dev == RT_NULL) + { + rt_kprintf( + "[DAY5] cannot find %s\n", + HUMI_SENSOR_NAME); + + return; + } + + + /* 打开温度设备 */ + if (rt_device_open( + temp_dev, + RT_DEVICE_FLAG_RDONLY) + != RT_EOK) + { + rt_kprintf( + "[DAY5] open %s failed\n", + TEMP_SENSOR_NAME); + + return; + } + + + /* 打开湿度设备 */ + if (rt_device_open( + humi_dev, + RT_DEVICE_FLAG_RDONLY) + != RT_EOK) + { + rt_kprintf( + "[DAY5] open %s failed\n", + HUMI_SENSOR_NAME); + + rt_device_close( + temp_dev); + + return; + } + + + rt_kprintf( + "[DAY5] AHT sensor opened successfully\n"); + + rt_kprintf( + "[DAY5] warming up sensor...\n"); + + + /* + * 舍弃启动时前 3 次数据 + * + * 防止 -50.6 ℃ 等异常初始值 + */ + for (i = 0; + i < WARMUP_COUNT; + i++) + { + sensor_read_once( + temp_dev, + humi_dev, + &temp, + &humi); + + + rt_kprintf( + "[DAY5] warmup %d/%d\n", + i + 1, + WARMUP_COUNT); + + + rt_thread_mdelay(200); + } + + + rt_kprintf( + "[DAY5] start sampling\n"); + + + /* + * 本次上电重新从 0 开始 + */ + sample_count = 0; + + + while (1) + { + if (sensor_read_once( + temp_dev, + humi_dev, + &temp, + &humi) + == RT_EOK) + { + /* + * 只有有效采样才计数 + */ + if (sensor_data_valid( + temp, + humi)) + { + sample_count++; + + + /* 串口打印 */ + sensor_print( + temp, + humi, + sample_count); + + + /* + * 写入 Flash + * + * 文件失败不会影响采集 Count + */ + day5_fs_append_sensor( + temp, + humi, + sample_count); + + + /* + * 上传 MQTT + * + * 网络失败不会影响采集 Count + */ + day5_mqtt_publish_sensor( + temp, + humi, + sample_count); + } + else + { + rt_kprintf( + "[DAY5] invalid sensor data, skip: temp=%d, humi=%d\n", + temp, + humi); + } + } + + + rt_thread_mdelay( + SAMPLE_PERIOD_MS); + } +} + + +/* ============================== + * 启动 Sensor Thread + * ============================== */ + +static int day5_sensor_start(void) +{ + if (sensor_tid != RT_NULL) + { + rt_kprintf( + "[DAY5] sensor thread already running\n"); + + return -RT_ERROR; + } + + + sensor_tid = + rt_thread_create( + "d5_sensor", + day5_sensor_thread, + RT_NULL, + 4096, + 15, + 10); + + + if (sensor_tid == RT_NULL) + { + rt_kprintf( + "[DAY5] create sensor thread failed\n"); + + return -RT_ERROR; + } + + + rt_thread_startup( + sensor_tid); + + + rt_kprintf( + "[DAY5] sensor thread started\n"); + + + return RT_EOK; +} + + +MSH_CMD_EXPORT(day5_sensor_start, + start day5 AHT sensor sampling); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" new file mode 100644 index 0000000000000000000000000000000000000000..d5ea2be8eb4e48e595d504d1239d59965a575ecb --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2023-5-10 ShiHao first version + */ + +#include +#include +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +/* 配置 LED 灯引脚 */ +#define PIN_LED_B GET_PIN(F, 11) // PF11 : LED_B --> LED +#define PIN_LED_R GET_PIN(F, 12) // PF12 : LED_R --> LED + +int main(void) +{ + + return 0; +} + diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" new file mode 100644 index 0000000000000000000000000000000000000000..e10fca7906c0793c02b53034be7b5c6dc81decaa --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" @@ -0,0 +1,964 @@ +# Day5 软件包、网络通信与文件系统 + +## 一、学习目标 + +本次学习主要围绕 RT-Thread 软件包生态、传感器采集、网络通信以及文件系统展开,完成 AHT 温湿度传感器、RW007 WiFi 模块、MQTT 通信和 Flash 文件存储的综合应用。 + +主要学习目标如下: + +1. 掌握 RT-Thread 软件包的配置和使用方法。 +2. 掌握 AHT 温湿度传感器的软件包驱动使用方法。 +3. 掌握 Sensor Framework 下传感器设备的访问方式。 +4. 掌握 RW007 WiFi 模块的初始化与联网方法。 +5. 掌握 Paho MQTT 客户端的连接、发布和订阅方法。 +6. 掌握 FAL Flash 分区管理机制。 +7. 掌握 DFS 与 LittleFS 文件系统的基本使用方法。 +8. 实现温湿度数据的本地存储和 MQTT 云端上传。 +9. 实现 MQTT 云端命令控制开发板 LED。 + +--- + +## 二、理论知识总结 + +### 2.1 RT-Thread 软件包机制 + +RT-Thread 软件包用于扩展 RT-Thread 的功能,例如传感器驱动、网络协议、文件系统、IoT 通信协议等。 + +本次实验主要使用以下软件包: + +| 软件包 | 版本 | 作用 | +| --------- | -----------: | -------------------- | +| AHT10 | v2.1.0 | AHT 温湿度传感器驱动 | +| RW007 | v2.1.0 | WiFi 网络通信 | +| Paho MQTT | 当前工程版本 | MQTT 客户端 | +| LittleFS | v2.5.0 | Flash 文件系统 | + +RT-Thread Studio 中的软件包配置最终会写入 `.config`,并在编译阶段生成对应的 `rtconfig.h`。 + +### 2.2 RT-Thread Sensor Framework + +RT-Thread Sensor Framework 对不同类型传感器进行了统一抽象。AHT 驱动初始化成功后注册两个 Sensor Device: + +```text +temp_aht10 +humi_aht10 +``` + +程序可以通过以下接口访问传感器: + +```c +rt_device_find(); +rt_device_open(); +rt_device_read(); +``` + +本实验中的温湿度数据使用整数形式保存: + +```text +温度:0.1 ℃ +湿度:0.1 % +``` + +例如 `temp = 259` 表示 `25.9 ℃`。 + +### 2.3 RW007 WiFi 模块 + +本开发板的 RW007 通过 SPI2 与 STM32 通信,实际配置为: + +| 配置项 | 参数 | +| -------- | ---: | +| SPI Bus | spi2 | +| CS | 90 | +| BOOT0 | 29 | +| BOOT1 | 90 | +| INT/BUSY | 107 | +| RESET | 111 | + +初始化成功后的日志: + +```text +[I/WLAN.dev] wlan init success +[I/WLAN.lwip] eth device init ok name:w0 +[I/WLAN.dev] wlan init success +[I/WLAN.lwip] eth device init ok name:w1 + +rw007 sn: [rw007a8a355defc584afa9f11] +rw007 ver: [RW007_2.1.0-c7747420-52] +``` + +其中 `w0` 作为 STA 网络接口连接 WiFi。 + +### 2.4 MQTT 通信 + +本实验使用公共 Broker: + +```text +broker.emqx.io:1883 +``` + +上行 Topic: + +```text +rsoc26/day5/kylehe/device/up +``` + +下行 Topic: + +```text +rsoc26/day5/kylehe/device/down +``` + +### 2.5 MQTT QoS + +| QoS | 含义 | +| ----- | ------------ | +| QoS 0 | 最多发送一次 | +| QoS 1 | 至少发送一次 | +| QoS 2 | 恰好发送一次 | + +本工程 Paho MQTT 发布时实际出现: + +```text +[E/mqtt] Not support Qos0 config, only support Qos1. +``` + +因此开发板发布消息使用 `QOS1`。 + +### 2.6 FAL Flash 抽象层 + +本开发板外部 Flash 为 W25Q64,容量 8 MB。实际 FAL 分区: + +```text +[I/FAL] ==================== FAL partition table ==================== +[I/FAL] | name | flash_dev | offset | length | +[I/FAL] ------------------------------------------------------------- +[I/FAL] | app | onchip_flash_128k | 0x00000000 | 0x00060000 | +[I/FAL] | param | onchip_flash_128k | 0x00060000 | 0x000a0000 | +[I/FAL] | easyflash | W25Q64 | 0x00000000 | 0x00080000 | +[I/FAL] | download | W25Q64 | 0x00080000 | 0x00100000 | +[I/FAL] | wifi_image | W25Q64 | 0x00180000 | 0x00080000 | +[I/FAL] | font | W25Q64 | 0x00200000 | 0x00300000 | +[I/FAL] | filesystem | W25Q64 | 0x00500000 | 0x00300000 | +``` + +本实验使用 `font` 分区,大小为 3 MB。 + +### 2.7 DFS 与 LittleFS + +本工程同时启用了 DFS、FAL、Elm-FatFs、LittleFS 和 ROMFS。本实验将 `font` 分区创建为 MTD NOR Device,并使用 LittleFS 挂载到 `/font`,最终保存文件: + +```text +/font/Data.txt +``` + +--- + +## 三、实验一:AHT 温湿度采集并通过 MQTT 上传 + +### 3.1 实验大纲 + +1. 配置 AHT 温湿度传感器软件包。 +2. 确认 AHT 实际使用的 I2C 总线。 +3. 使用 Sensor Framework 获取温湿度数据。 +4. 对启动阶段异常数据进行过滤。 +5. 配置 RW007 WiFi 模块。 +6. 获取网络 IP 地址。 +7. 配置 Paho MQTT。 +8. 连接 EMQX 公共 Broker。 +9. 实现开发板向 MQTTX 发布测试消息。 +10. 将真实温湿度数据周期性上传至 MQTTX。 +11. 使用 Count 统计本次上电后的有效采集次数。 + +实验流程: + +```text +AHT Sensor + ↓ +I2C3 + ↓ +Sensor Framework + ↓ +温湿度读取 + ↓ +有效性判断 + ↓ +Count++ + ↓ +Paho MQTT + ↓ +RW007 + ↓ +WiFi + ↓ +broker.emqx.io + ↓ +MQTTX +``` + +### 3.2 AHT 传感器配置 + +开发板 BSP 中 AHT 对应的软件 I2C 配置为: + +```text +I2C3 +SCL = PE0 +SDA = PE1 +``` + +传感器初始化使用: + +```c +#define AHT10_I2C_BUS_NAME "i2c3" +``` + +初始化成功: + +```text +[I/sensor] rt_sensor[temp_aht10] init success +[I/sensor] rt_sensor[humi_aht10] init success +[AHT10] init success, bus: i2c3 +``` + +### 3.3 传感器启动异常数据 + +最初读取温度时出现: + +```text +num 0 temp:-50.6 C +num 1 temp:-50.6 C +num 2 temp:-50.6 C +num 3 temp:27.4 C +num 4 temp:27.4 C +``` + +湿度读取: + +```text +humi:38.4% +humi:38.4% +humi:38.4% +humi:38.2% +humi:38.2% +``` + +因此程序增加预热机制: + +```c +#define WARMUP_COUNT 3 +``` + +启动后丢弃前 3 次采样: + +```text +[DAY5] warming up sensor... +[DAY5] warmup 1/3 +[DAY5] warmup 2/3 +[DAY5] warmup 3/3 +``` + +### 3.4 数据有效性判断 + +温度有效范围:`-40 ℃ ~ 85 ℃`;湿度有效范围:`0 % ~ 100 %`。 + +```c +if (temp < -400 || temp > 850) +{ + return RT_FALSE; +} + +if (humi < 0 || humi > 1000) +{ + return RT_FALSE; +} +``` + +只有有效数据才执行 `sample_count++`,因此 Count 表示本次上电后成功获取的有效数据次数。 + +### 3.5 RW007 初始化问题 + +最初执行: + +```text +wifi scan +``` + +出现: + +```text +[E/WLAN.cmd] Scan with info error:-10! +``` + +修改 RW007 参数为: + +```text +CONFIG_RW007_SPI_BUS_NAME="spi2" +CONFIG_RW007_CS_PIN=90 +CONFIG_RW007_BOOT0_PIN=29 +CONFIG_RW007_BOOT1_PIN=90 +CONFIG_RW007_INT_BUSY_PIN=107 +CONFIG_RW007_RST_PIN=111 +``` + +重新烧录后 RW007 初始化成功。 + +### 3.6 WiFi 联网测试 + +成功连接 WiFi 后: + +```text +[I/WLAN.lwip] Got IP address : 192.168.1.7 +``` + +执行 `ifconfig`: + +```text +network interface device: w0 (Default) +MTU: 1500 +MAC: fc 58 4a fa 9f 11 +FLAGS: UP LINK_UP INTERNET_UP DHCP_ENABLE ETHARP BROADCAST IGMP +ip address: 192.168.1.7 +gw address: 192.168.1.1 +net mask : 255.255.255.0 +dns server #0: 192.168.1.1 +``` + +测试 Internet: + +```text +60 bytes from 8.8.8.8 icmp_seq=0 ttl=113 time=46 ms +60 bytes from 8.8.8.8 icmp_seq=1 ttl=113 time=46 ms +60 bytes from 8.8.8.8 icmp_seq=2 ttl=113 time=43 ms +60 bytes from 8.8.8.8 icmp_seq=3 ttl=113 time=42 ms +``` + +### 3.7 MQTT Broker 配置 + +MQTT 客户端启动: + +```text +day5_mqtt_start +``` + +实际运行: + +```text +[MQTT] starting... +[MQTT] client id: rsoc26-kylehe-90392 +[MQTT] connect callback +[D/mqtt] ipv4 address port: 1883 +[D/mqtt] HOST = 'broker.emqx.io' +[I/mqtt] MQTT server connect success. +[I/mqtt] Subscribe #0 rsoc26/day5/kylehe/device/down OK! +[MQTT] online +[MQTT] broker: tcp://broker.emqx.io:1883 +[MQTT] sub topic: rsoc26/day5/kylehe/device/down +``` + +### 3.8 MQTT 发布 QoS 问题 + +第一次发布测试: + +```text +day5_mqtt_pub hello_day5 +``` + +出现: + +```text +[E/mqtt] Not support Qos0 config, only support Qos1. +[MQTT] publish failed +``` + +将 `QOS0` 修改为 `QOS1` 后发布成功,MQTTX 成功接收到: + +```text +hello_day5 +``` + +### 3.9 温湿度自动上传 + +传感器线程每 5 秒采集一次: + +```c +#define SAMPLE_PERIOD_MS 5000 +``` + +每次有效采集后上传: + +```text +Temp: XX.X ; Humi: XX.X ; Count: X +``` + +实际 MQTTX 接收到的数据包括: + +```text +Temp: 26.6 ; Humi: 39.4 ; Count: 4 +Temp: 26.5 ; Humi: 39.8 ; Count: 7 +Temp: 26.5 ; Humi: 39.8 ; Count: 8 +``` + +### 3.10 实验结果 + +实验成功实现 AHT 温湿度数据采集、异常数据过滤、有效数据 Count 计数、RW007 WiFi 联网、MQTT Broker 连接以及 MQTT 数据自动上传。 + +--- + +## 四、实验二:挂载 font 分区并保存 Data.txt + +### 4.1 实验大纲 + +1. 分析开发板 FAL Flash 分区。 +2. 确认 `font` 分区地址和大小。 +3. 分析开发板 ROMFS 根目录结构。 +4. 添加 `/font` 文件系统挂载点。 +5. 将 `font` 分区创建为 MTD NOR Device。 +6. 使用 LittleFS 格式化 `font` 分区。 +7. 将 LittleFS 挂载至 `/font`。 +8. 验证文件系统读写功能。 +9. 创建 `/font/Data.txt`。 +10. 每次有效采集后追加温湿度及 Count 数据。 + +实验流程: + +```text +W25Q64 + ↓ +FAL + ↓ +font 分区 + ↓ +MTD NOR Device + ↓ +LittleFS + ↓ +/font + ↓ +Data.txt +``` + +### 4.2 FAL 分区分析 + +```text +[I/FAL] | font | W25Q64 | 0x00200000 | 0x00300000 | +``` + +因此: + +```text +font offset = 0x00200000 +font size = 0x00300000 +``` + +### 4.3 根文件系统分析 + +执行: + +```text +ls / +``` + +最初结果: + +```text +Directory /: +fal +``` + +执行 `mkdir /font` 后再次 `ls /`,仍然只有 `fal`。分析 `drv_filesystem.c` 后发现根目录使用 ROMFS,只读,不能在运行时增加根目录。 + +### 4.4 增加 /font 挂载点 + +修改: + +```text +libraries/Board_Drivers/drv_filesystem.c +``` + +增加: + +```c +{ROMFS_DIRENT_DIR, "font", RT_NULL, 0}, +``` + +修改后: + +```c +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 + + {ROMFS_DIRENT_DIR, "font", RT_NULL, 0}, +}; +``` + +重新烧录后: + +```text +Directory /: +fal +font +``` + +### 4.5 LittleFS 挂载 + +首先创建 `font` 对应的 MTD NOR Device: + +```c +fal_mtd_nor_device_create("font"); +``` + +然后挂载: + +```c +dfs_mount("font", + "/font", + "lfs", + 0, + RT_NULL); +``` + +第一次使用时进行一次 LittleFS 格式化,之后不再重复格式化。 + +### 4.6 文件系统读写验证 + +```text +echo "hello" /font/test.txt +cat /font/test.txt +``` + +实际结果: + +```text +hello +``` + +执行: + +```text +ls /font +``` + +得到: + +```text +Directory /font: +test.txt 5 +``` + +### 4.7 Data.txt 数据保存 + +每次得到有效温湿度数据后,以 `O_WRONLY | O_CREAT | O_APPEND` 方式打开: + +```text +/font/Data.txt +``` + +数据格式: + +```text +Temp: XX.X ; Humi: XX.X ; Count: X +``` + +### 4.8 实际 Data.txt 数据 + +```text +Directory /font: +Data.txt 398 +test.txt 5 +``` + +实际数据: + +```text +Temp: 30.1 ; Humi: 42.5 ; Count: 1 +Temp: 30.1 ; Humi: 42.5 ; Count: 2 +Temp: 30.1 ; Humi: 42.8 ; Count: 3 +Temp: 30.0 ; Humi: 42.9 ; Count: 4 +Temp: 30.1 ; Humi: 42.7 ; Count: 5 +Temp: 30.0 ; Humi: 42.5 ; Count: 6 +Temp: 30.0 ; Humi: 42.4 ; Count: 7 +Temp: 29.9 ; Humi: 42.4 ; Count: 8 +Temp: 29.9 ; Humi: 42.3 ; Count: 9 +Temp: 29.9 ; Humi: 42.2 ; Count: 10 +Temp: 29.9 ; Humi: 42.2 ; Count: 11 +``` + +### 4.9 msh echo 使用问题 + +错误写法: + +```text +echo hello > /font/test.txt +``` + +提示: + +```text +Usage: echo "string" [filename] +``` + +正确命令: + +```text +echo "hello" /font/test.txt +``` + +### 4.10 实验结果 + +实验成功实现 `font` 分区的 LittleFS 挂载以及 `/font/Data.txt` 持续追加保存。 + +--- + +## 五、实验三:MQTT 云端命令控制 LED + +### 5.1 实验大纲 + +1. 配置 MQTT 下行 Topic。 +2. 在 Paho MQTT 中注册订阅回调。 +3. 从 MQTT Payload 中安全提取命令字符串。 +4. 识别 `toggle` 控制命令。 +5. 配置板载 PF12 LED。 +6. 根据 MQTT 指令切换 LED 状态。 +7. 使用 MQTTX 多次发送命令。 +8. 对比串口输出与真实 LED 状态。 + +实验流程: + +```text +MQTTX + ↓ +Publish "toggle" + ↓ +broker.emqx.io + ↓ +rsoc26/day5/kylehe/device/down + ↓ +Paho MQTT Callback + ↓ +命令解析 + ↓ +toggle + ↓ +PF12 LED + ↓ +ON / OFF +``` + +### 5.2 LED 引脚配置 + +板载红色 LED 使用 PF12。按照 RT-Thread STM32 PIN 编号: + +```text +PF12 = 5 × 16 + 12 = 92 +``` + +因此: + +```c +#define PIN_LED_R 92 +#define LED_ON PIN_LOW +#define LED_OFF PIN_HIGH +``` + +### 5.3 GET_PIN 编译问题 + +最初使用: + +```c +#define PIN_LED_R GET_PIN(F, 12) +``` + +编译出现: + +```text +warning: implicit declaration of function 'GET_PIN' +error: 'F' undeclared +``` + +最终使用: + +```c +#define PIN_LED_R 92 +``` + +编译成功。 + +### 5.4 MQTT Payload 处理 + +MQTT Payload 不保证以 `\0` 结尾,因此程序读取 `payloadlen`,限制最大长度,复制后手动补 `\0`,再去除末尾回车、换行和空格。 + +最后判断: + +```c +if ((len == 6) && + (rt_memcmp(payload, "toggle", 6) == 0)) +``` + +### 5.5 MQTT 下行测试 + +MQTTX 发布: + +```text +Topic: +rsoc26/day5/kylehe/device/down + +Payload: +toggle +``` + +开发板实际输出: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON +``` + +再次发送: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF +``` + +板载 LED 实际亮灭状态与串口日志一致。 + +### 5.6 DNS 临时解析失败 + +一次重新启动后 MQTT 出现: + +```text +[E/mqtt] getaddrinfo err: 202 'broker.emqx.io' +[E/mqtt] resolve uri err +[E/mqtt] Net connect error(-1). +[MQTT] offline +``` + +原因是 MQTT 启动时 WiFi、DHCP、DNS 尚未完全准备完成。Paho MQTT 随后自动重连: + +```text +[D/mqtt] restart! +``` + +网络恢复以后 MQTT 成功重新上线。 + +### 5.7 实验结果 + +实际运行结果: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF +``` + +实际开发板 LED 同步进行亮灭切换,说明 MQTT 下行控制功能正常。 + +--- + +## 六、线程栈溢出问题 + +完成 MQTT 和 LittleFS 整合后,传感器线程最初栈大小为 2048 Bytes,运行时出现: + +```text +[FS] append success: Temp: 25.9 ; Humi: 46.7 ; Count: 1 +thread:d5_senso stack overflow +``` + +原因是传感器线程中已经包含 AHT 读取、字符串格式化、LittleFS 写文件和 Paho MQTT 发布,调用链增加后 2048 Bytes 栈空间不足。 + +将线程栈调整为 4096 Bytes: + +```c +sensor_tid = rt_thread_create( + "d5_sensor", + day5_sensor_thread, + RT_NULL, + 4096, + 15, + 10); +``` + +重新运行后系统稳定,不再出现线程栈溢出。 + +--- + +## 七、问题及解决方法总结 + +| 问题 | 原因 | 解决方法 | +| -------------------------- | --------------------------- | ---------------------------------------- | +| `wifi scan` 返回 `-10` | RW007 没有正确初始化 | 修改 RW007 SPI 与控制引脚 | +| AHT 初始读取 `-50.6℃` | 传感器刚启动数据未稳定 | 舍弃前 3 次采样 | +| MQTT QoS0 发布失败 | 当前 Paho 配置只支持 QoS1 | 发布改为 `QOS1` | +| `mkdir /font` 后目录不存在 | `/` 为 ROMFS,只读 | 在 `drv_filesystem.c` 中添加 `font` 目录 | +| `/font` 无法保存文件 | 尚未挂载真实文件系统 | FAL MTD NOR + LittleFS 挂载 | +| `echo hello > file` 失败 | msh echo 不支持该重定向方式 | 使用 `echo "hello" filename` | +| `GET_PIN(F,12)` 编译失败 | 应用层没有对应 GET_PIN 宏 | 使用 PF12 的 RT-Thread 编号 `92` | +| MQTT 收到消息但 LED 未切换 | Payload 需要明确解析和匹配 | 使用 payloadlen + memcpy + memcmp | +| `getaddrinfo err:202` | MQTT 启动时 DNS 尚未就绪 | 等待 WiFi 获取 IP,利用 Paho 自动重连 | +| `d5_sensor stack overflow` | 文件和 MQTT 调用增加栈消耗 | 线程栈从 2048 增加到 4096 Bytes | + +--- + +## 八、综合实验架构 + +```text + AHT Sensor + │ + │ I2C3 + ▼ + Sensor Framework + │ + Temp / Humi + │ + 数据有效判断 + │ + Count++ + │ + ┌─────────────┴─────────────┐ + │ │ + ▼ ▼ + LittleFS Paho MQTT + │ │ + /font/Data.txt RW007 + │ + WiFi + │ + ▼ + broker.emqx.io + ↑ ↓ + │ │ + MQTTX MQTTX + 接收数据 发送toggle + │ + ▼ + PF12 LED + ON / OFF +``` + +--- + +## 九、系统运行流程 + +开发板重新上电后,首先等待 WiFi 获取 IP: + +```text +[I/WLAN.lwip] Got IP address : 192.168.1.x +``` + +随后依次执行: + +```text +day5_fs_init +day5_mqtt_start +``` + +等待: + +```text +[MQTT] online +``` + +最后执行: + +```text +day5_sensor_start +``` + +--- + +## 十、实验结果总结 + +### 10.1 温湿度 MQTT 上传 + +开发板能够周期性采集 AHT 温湿度数据,并通过 `rsoc26/day5/kylehe/device/up` 上传至 EMQX Broker。MQTTX 可以实时接收: + +```text +Temp: 26.6 ; Humi: 39.4 ; Count: 4 +Temp: 26.5 ; Humi: 39.8 ; Count: 7 +Temp: 26.5 ; Humi: 39.8 ; Count: 8 +``` + +### 10.2 Flash 数据保存 + +温湿度数据同时保存至: + +```text +/font/Data.txt +``` + +实际读取结果: + +```text +Temp: 30.1 ; Humi: 42.5 ; Count: 1 +Temp: 30.1 ; Humi: 42.5 ; Count: 2 +Temp: 30.1 ; Humi: 42.8 ; Count: 3 +... +Temp: 29.9 ; Humi: 42.2 ; Count: 11 +``` + +### 10.3 MQTT 控制 LED + +MQTTX 向 `rsoc26/day5/kylehe/device/down` 发送 `toggle` 后,开发板能够正确识别消息,并控制板载 LED: + +```text +[LED] ON +[LED] OFF +``` + +实际 LED 状态与程序日志一致。 + +--- + +## 十一、实验总结 + +本次实验将 RT-Thread 中多个常用组件进行了综合应用,包括 Sensor Framework、I2C、SPI、RW007、WLAN、SAL、TCP/IP、Paho MQTT、FAL、DFS、ROMFS、LittleFS、PIN 和 Thread。 + +系统最终形成完整的 IoT 数据链路: + +```text +传感器采集 + ↓ +本地 Flash 保存 + ↓ +WiFi 网络连接 + ↓ +MQTT 数据上传 + ↓ +云端命令下发 + ↓ +开发板执行控制 +``` + +通过本次实验进一步理解了 RT-Thread 的设备驱动框架、软件包机制、网络协议栈、文件系统以及线程资源管理方法,并完成了传感器、网络、存储和执行器之间的完整联动。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/6d94a05e-4868-4378-b83c-1a26c8712165.png" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/6d94a05e-4868-4378-b83c-1a26c8712165.png" new file mode 100644 index 0000000000000000000000000000000000000000..b2aed513b43ca416f6e59766874fb17398d3927d Binary files /dev/null and "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/6d94a05e-4868-4378-b83c-1a26c8712165.png" differ diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_demo.c" new file mode 100644 index 0000000000000000000000000000000000000000..ac07349ffb8d8fde04b04df5ea3a80b18ce1e846 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_demo.c" @@ -0,0 +1,197 @@ +#include +#include + +#include "test_dev.h" + + +#define TEST_REG_DEVICE_ID 0x00 +#define TEST_REG_CONFIG 0x02 + + +static void test_demo(void) +{ + rt_device_t device; + struct rt_test_device *test_dev; + + rt_uint8_t value; + rt_uint8_t write_buf[4] = {0x11, 0x22, 0x33, 0x44}; + rt_uint8_t read_buf[4] = {0}; + + rt_err_t result; + rt_size_t i; + + /* + * 查找 test_dev + */ + device = rt_device_find("test_dev"); + + if (device == RT_NULL) + { + rt_kprintf("[test_demo] test_dev not found\n"); + return; + } + + rt_kprintf("[test_demo] test_dev found\n"); + + /* + * 转换为 test_dev 设备对象 + */ + test_dev = (struct rt_test_device *)device; + + + /* + * 1. 读取 DEVICE_ID + */ + result = test_dev_read_reg(test_dev, + TEST_REG_DEVICE_ID, + &value); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] read device id failed: %d\n", + result); + return; + } + + rt_kprintf("[test_demo] device id = 0x%02X\n", + value); + + + /* + * 2. 单寄存器写入 + */ + result = test_dev_write_reg(test_dev, + TEST_REG_CONFIG, + 0x55); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] write config failed: %d\n", + result); + return; + } + + + /* + * 3. 单寄存器读回 + */ + result = test_dev_read_reg(test_dev, + TEST_REG_CONFIG, + &value); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] read config failed: %d\n", + result); + return; + } + + rt_kprintf("[test_demo] config = 0x%02X\n", + value); + + + /* + * 4. 连续写多个寄存器 + */ + result = test_dev_write_regs(test_dev, + 0x10, + write_buf, + 4); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] write regs failed: %d\n", + result); + return; + } + + + /* + * 5. 连续读多个寄存器 + */ + result = test_dev_read_regs(test_dev, + 0x10, + read_buf, + 4); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] read regs failed: %d\n", + result); + return; + } + + rt_kprintf("[test_demo] read data:"); + + for (i = 0; i < 4; i++) + { + rt_kprintf(" 0x%02X", read_buf[i]); + } + + rt_kprintf("\n"); + + + /* + * 6. 测试只读寄存器保护 + */ + rt_kprintf("[test_demo] test read-only register\n"); + + result = test_dev_write_reg(test_dev, + TEST_REG_DEVICE_ID, + 0x66); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] read-only protection works\n"); + } + else + { + rt_kprintf("[test_demo] read-only protection failed\n"); + } + + + /* + * 再次读取 DEVICE_ID, + * 确认只读寄存器没有被修改 + */ + result = test_dev_read_reg(test_dev, + TEST_REG_DEVICE_ID, + &value); + + if (result == RT_EOK) + { + rt_kprintf("[test_demo] device id after write = 0x%02X\n", + value); + } + + + /* + * 7. 测试寄存器边界保护 + * + * 从 0xFE 开始写 4 Byte: + * 0xFE + * 0xFF + * 0x100 + * 0x101 + * + * 后两个地址已经越界 + */ + rt_kprintf("[test_demo] test boundary protection\n"); + + result = test_dev_write_regs(test_dev, + 0xFE, + write_buf, + 4); + + if (result != RT_EOK) + { + rt_kprintf("[test_demo] boundary protection works\n"); + } + else + { + rt_kprintf("[test_demo] boundary protection failed\n"); + } +} + + +MSH_CMD_EXPORT(test_demo, test virtual i2c device); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\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/\344\275\225\345\230\211\344\271\220/\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 0000000000000000000000000000000000000000..977ccc5109686d501228fd890a149274e814e8e9 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\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,194 @@ +#include "test_dev.h" + + +/* + * 注册 test_dev + */ +rt_err_t test_dev_register(struct rt_test_device *dev, + const char *name, + const struct rt_test_ops *ops, + void *user_data) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(name != RT_NULL); + RT_ASSERT(ops != RT_NULL); + + /* + * 设置为通用杂项设备 + */ + dev->parent.type = RT_Device_Class_Miscellaneous; + + /* + * 当前不使用接收/发送完成回调 + */ + dev->parent.rx_indicate = RT_NULL; + dev->parent.tx_complete = RT_NULL; + +#ifdef RT_USING_DEVICE_OPS + /* + * test_dev 使用自己的 rt_test_ops, + * 不使用 rt_device 通用 ops + */ + dev->parent.ops = RT_NULL; +#else + dev->parent.init = RT_NULL; + dev->parent.open = RT_NULL; + dev->parent.close = RT_NULL; + dev->parent.read = RT_NULL; + dev->parent.write = RT_NULL; + dev->parent.control = RT_NULL; +#endif + + /* + * 保存底层操作接口 + */ + dev->ops = ops; + + /* + * 保存设备私有数据 + */ + dev->parent.user_data = user_data; + + /* + * 注册到 RT-Thread 设备管理器 + */ + return rt_device_register(&dev->parent, + name, + RT_DEVICE_FLAG_RDWR); +} + + +/* + * 初始化设备 + */ +rt_err_t test_dev_init(struct rt_test_device *dev) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(dev->ops != RT_NULL); + + if (dev->ops->init == RT_NULL) + { + return -RT_ENOSYS; + } + + return dev->ops->init(&dev->parent); +} + + +/* + * 写单个寄存器 + */ +rt_err_t test_dev_write_reg(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t value) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(dev->ops != RT_NULL); + + if (dev->ops->write_reg == RT_NULL) + { + return -RT_ENOSYS; + } + + return dev->ops->write_reg(&dev->parent, + reg, + value); +} + + +/* + * 读单个寄存器 + */ +rt_err_t test_dev_read_reg(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t *value) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(dev->ops != RT_NULL); + + if (value == RT_NULL) + { + return -RT_EINVAL; + } + + if (dev->ops->read_reg == RT_NULL) + { + return -RT_ENOSYS; + } + + return dev->ops->read_reg(&dev->parent, + reg, + value); +} + + +/* + * 连续写多个寄存器 + */ +rt_err_t test_dev_write_regs(struct rt_test_device *dev, + rt_uint8_t reg, + const rt_uint8_t *buffer, + rt_size_t size) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(dev->ops != RT_NULL); + + if (buffer == RT_NULL || size == 0) + { + return -RT_EINVAL; + } + + /* + * 防止访问超过 256 Byte 寄存器空间 + */ + if (size > (TEST_DEV_REG_SIZE - reg)) + { + return -RT_EINVAL; + } + + if (dev->ops->write_regs == RT_NULL) + { + return -RT_ENOSYS; + } + + return dev->ops->write_regs(&dev->parent, + reg, + buffer, + size); +} + + +/* + * 连续读多个寄存器 + */ +rt_err_t test_dev_read_regs(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t *buffer, + rt_size_t size) +{ + RT_ASSERT(dev != RT_NULL); + RT_ASSERT(dev->ops != RT_NULL); + + if (buffer == RT_NULL || size == 0) + { + return -RT_EINVAL; + } + + /* + * 防止访问超过 256 Byte 寄存器空间 + */ + if (size > (TEST_DEV_REG_SIZE - reg)) + { + return -RT_EINVAL; + } + + if (dev->ops->read_regs == RT_NULL) + { + return -RT_ENOSYS; + } + + return dev->ops->read_regs(&dev->parent, + reg, + buffer, + size); +} diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\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/\344\275\225\345\230\211\344\271\220/\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 0000000000000000000000000000000000000000..db2c96f49817fbf1d41eb100ab30f650d4d39855 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\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,110 @@ +#ifndef __TEST_DEV_H__ +#define __TEST_DEV_H__ + +#include +#include + + +/* + * test_dev 虚拟寄存器空间大小 + */ +#define TEST_DEV_REG_SIZE 256 + + +/* + * test_dev 操作接口 + * + * 框架层只定义接口, + * 具体实现由底层虚拟 I2C 驱动完成。 + */ +struct rt_test_ops +{ + rt_err_t (*init)(struct rt_device *dev); + + rt_err_t (*write_reg)(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t value); + + rt_err_t (*read_reg)(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t *value); + + rt_err_t (*write_regs)(struct rt_device *dev, + rt_uint8_t reg, + const rt_uint8_t *buffer, + rt_size_t size); + + rt_err_t (*read_regs)(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t *buffer, + rt_size_t size); +}; + + +/* + * test_dev 设备对象 + */ +struct rt_test_device +{ + /* + * 继承 RT-Thread 通用设备 + */ + struct rt_device parent; + + /* + * test_dev 专用操作接口 + */ + const struct rt_test_ops *ops; +}; + + +/* + * 注册 test_dev + */ +rt_err_t test_dev_register(struct rt_test_device *dev, + const char *name, + const struct rt_test_ops *ops, + void *user_data); + + +/* + * 初始化设备 + */ +rt_err_t test_dev_init(struct rt_test_device *dev); + + +/* + * 单寄存器写 + */ +rt_err_t test_dev_write_reg(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t value); + + +/* + * 单寄存器读 + */ +rt_err_t test_dev_read_reg(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t *value); + + +/* + * 连续写多个寄存器 + */ +rt_err_t test_dev_write_regs(struct rt_test_device *dev, + rt_uint8_t reg, + const rt_uint8_t *buffer, + rt_size_t size); + + +/* + * 连续读多个寄存器 + */ +rt_err_t test_dev_read_regs(struct rt_test_device *dev, + rt_uint8_t reg, + rt_uint8_t *buffer, + rt_size_t size); + + +#endif /* __TEST_DEV_H__ */ diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_i2c_virtual.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_i2c_virtual.c" new file mode 100644 index 0000000000000000000000000000000000000000..b03e055ecfff6d02977dfa1eaff233794c2ee2d7 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_i2c_virtual.c" @@ -0,0 +1,329 @@ +#include "test_dev.h" + + +/* + * 虚拟寄存器定义 + */ +#define VIRTUAL_REG_DEVICE_ID 0x00 +#define VIRTUAL_REG_STATUS 0x01 +#define VIRTUAL_REG_CONFIG 0x02 +#define VIRTUAL_REG_DATA 0x03 + + +/* + * 虚拟 I2C 设备私有数据 + */ +struct virtual_i2c_data +{ + rt_uint8_t addr; + rt_uint8_t regs[TEST_DEV_REG_SIZE]; +}; + + +/* + * test_dev 设备对象 + */ +static struct rt_test_device virtual_i2c_dev; + + +/* + * 虚拟 I2C 私有数据 + * + * 模拟一个地址为 0x50 的 I2C 从设备 + */ +static struct virtual_i2c_data virtual_i2c = +{ + .addr = 0x50, +}; + + +/* + * 虚拟 I2C 设备初始化 + */ +static rt_err_t virtual_i2c_init(struct rt_device *dev) +{ + struct virtual_i2c_data *data; + + RT_ASSERT(dev != RT_NULL); + + data = (struct virtual_i2c_data *)dev->user_data; + + if (data == RT_NULL) + { + return -RT_ERROR; + } + + /* + * 清空虚拟寄存器 + */ + rt_memset(data->regs, + 0, + sizeof(data->regs)); + + /* + * 设置默认寄存器值 + */ + data->regs[VIRTUAL_REG_DEVICE_ID] = 0xA5; + data->regs[VIRTUAL_REG_STATUS] = 0x01; + data->regs[VIRTUAL_REG_CONFIG] = 0x00; + data->regs[VIRTUAL_REG_DATA] = 0x00; + + rt_kprintf("[test_dev] virtual i2c init\n"); + rt_kprintf("[test_dev] addr = 0x%02X\n", + data->addr); + rt_kprintf("[test_dev] device id = 0x%02X\n", + data->regs[VIRTUAL_REG_DEVICE_ID]); + + return RT_EOK; +} + + +/* + * 写单个虚拟寄存器 + */ +static rt_err_t virtual_i2c_write_reg(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t value) +{ + struct virtual_i2c_data *data; + + RT_ASSERT(dev != RT_NULL); + + data = (struct virtual_i2c_data *)dev->user_data; + + if (data == RT_NULL) + { + return -RT_ERROR; + } + + /* + * DEVICE_ID 为只读寄存器 + */ + if (reg == VIRTUAL_REG_DEVICE_ID) + { + rt_kprintf("[test_dev] reg 0x%02X is read only\n", + reg); + + return -RT_ERROR; + } + + /* + * 写入虚拟寄存器 + */ + data->regs[reg] = value; + + rt_kprintf("[test_dev] write reg[0x%02X] = 0x%02X\n", + reg, + value); + + return RT_EOK; +} + + +/* + * 读单个虚拟寄存器 + */ +static rt_err_t virtual_i2c_read_reg(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t *value) +{ + struct virtual_i2c_data *data; + + RT_ASSERT(dev != RT_NULL); + + data = (struct virtual_i2c_data *)dev->user_data; + + if (data == RT_NULL) + { + return -RT_ERROR; + } + + if (value == RT_NULL) + { + return -RT_EINVAL; + } + + /* + * 读取虚拟寄存器 + */ + *value = data->regs[reg]; + + rt_kprintf("[test_dev] read reg[0x%02X] = 0x%02X\n", + reg, + *value); + + return RT_EOK; +} + + +/* + * 连续写多个虚拟寄存器 + */ +static rt_err_t virtual_i2c_write_regs(struct rt_device *dev, + rt_uint8_t reg, + const rt_uint8_t *buffer, + rt_size_t size) +{ + struct virtual_i2c_data *data; + rt_size_t i; + + RT_ASSERT(dev != RT_NULL); + + data = (struct virtual_i2c_data *)dev->user_data; + + if (data == RT_NULL) + { + return -RT_ERROR; + } + + if (buffer == RT_NULL || size == 0) + { + return -RT_EINVAL; + } + + /* + * 防止访问超过虚拟寄存器空间 + */ + if (size > (TEST_DEV_REG_SIZE - reg)) + { + return -RT_EINVAL; + } + + /* + * 连续写入寄存器 + */ + for (i = 0; i < size; i++) + { + /* + * DEVICE_ID 为只读寄存器 + */ + if ((reg + i) == VIRTUAL_REG_DEVICE_ID) + { + rt_kprintf("[test_dev] reg 0x%02X is read only\n", + (rt_uint8_t)(reg + i)); + + return -RT_ERROR; + } + + data->regs[reg + i] = buffer[i]; + } + + rt_kprintf("[test_dev] write %d regs from 0x%02X\n", + (int)size, + reg); + + return RT_EOK; +} + + +/* + * 连续读多个虚拟寄存器 + */ +static rt_err_t virtual_i2c_read_regs(struct rt_device *dev, + rt_uint8_t reg, + rt_uint8_t *buffer, + rt_size_t size) +{ + struct virtual_i2c_data *data; + rt_size_t i; + + RT_ASSERT(dev != RT_NULL); + + data = (struct virtual_i2c_data *)dev->user_data; + + if (data == RT_NULL) + { + return -RT_ERROR; + } + + if (buffer == RT_NULL || size == 0) + { + return -RT_EINVAL; + } + + /* + * 防止访问超过虚拟寄存器空间 + */ + if (size > (TEST_DEV_REG_SIZE - reg)) + { + return -RT_EINVAL; + } + + /* + * 连续读取寄存器 + */ + for (i = 0; i < size; i++) + { + buffer[i] = data->regs[reg + i]; + } + + rt_kprintf("[test_dev] read %d regs from 0x%02X\n", + (int)size, + reg); + + return RT_EOK; +} + + +/* + * 虚拟 I2C 操作接口表 + * + * 将 test_dev 框架接口 + * 与具体虚拟 I2C 实现连接起来 + */ +static const struct rt_test_ops virtual_i2c_ops = +{ + .init = virtual_i2c_init, + .write_reg = virtual_i2c_write_reg, + .read_reg = virtual_i2c_read_reg, + .write_regs = virtual_i2c_write_regs, + .read_regs = virtual_i2c_read_regs, +}; + + +/* + * 虚拟 I2C 设备注册 + */ +static int virtual_i2c_hw_init(void) +{ + rt_err_t result; + + /* + * 注册 test_dev + */ + result = test_dev_register(&virtual_i2c_dev, + "test_dev", + &virtual_i2c_ops, + &virtual_i2c); + + if (result != RT_EOK) + { + rt_kprintf("[test_dev] register failed: %d\n", + result); + + return result; + } + + /* + * 初始化虚拟 I2C 设备 + */ + result = test_dev_init(&virtual_i2c_dev); + + if (result != RT_EOK) + { + rt_kprintf("[test_dev] init failed: %d\n", + result); + + return result; + } + + rt_kprintf("[test_dev] register success\n"); + + return RT_EOK; +} + + +/* + * RT-Thread 自动初始化 + */ +INIT_DEVICE_EXPORT(virtual_i2c_hw_init); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251README.md" new file mode 100644 index 0000000000000000000000000000000000000000..5beb5d67142eb2e1486744ec4b0f2dc6e3ecb88a --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251README.md" @@ -0,0 +1,528 @@ +# Day4 实验报告:虚拟 I2C 设备驱动框架 test_dev 设计与实现 + +## 一、实验目的 + +- 理解 RT-Thread 设备驱动框架的分层设计思想 +- 理解 `struct rt_device` 在设备管理中的作用 +- 掌握 `ops` 函数指针表的设计与使用 +- 理解 `user_data` 保存设备私有数据的方法 +- 掌握 `rt_device_register()` 和 `rt_device_find()` 的设备注册与查找机制 +- 模仿 RT-Thread PIN 设备框架,自主设计一个虚拟设备驱动框架 `test_dev` +- 使用虚拟 I2C 寄存器设备验证框架层与底层驱动层之间的调用关系 + + +## 二、实验环境 + +- RT-Thread 版本:RT-Thread 4.1.1 +- 开发平台:STM32 + RT-Thread +- 开发工具:RT-Thread Studio +- 调试方式:串口 Console + msh +- 虚拟设备名称:test_dev +- 虚拟 I2C 地址:0x50 +- 虚拟寄存器空间:256 Byte + + +## 三、实验原理 + +### 3.1 RT-Thread 设备驱动分层思想 + +应用层 + ↓ +test_dev 框架层 + ↓ +ops 函数指针 + ↓ +virtual_i2c 虚拟驱动层 + ↓ +虚拟寄存器 regs[256] + + +### 3.2 test_dev 设备结构 + +重点分析: + +- `struct rt_device parent` +- `const struct rt_test_ops *ops` + +说明: + +- parent 用于接入 RT-Thread 通用设备管理框架 +- ops 用于保存 test_dev 专用设备操作函数 + + +### 3.3 rt_test_ops 操作接口 + +介绍: + +- init +- write_reg +- read_reg +- write_regs +- read_regs + +说明为什么不直接在框架层调用 virtual_i2c_xxx()。 + + +### 3.4 ops 与 user_data + +重点说明: + +ops: +用于找到“设备应该怎样操作” + +user_data: +用于找到“设备自己的数据在哪里” + +形成: + +ops -> 操作方法 +user_data -> 设备数据 + + +### 3.5 虚拟 I2C 寄存器设计 + +虚拟 I2C 地址: + +0x50 + +虚拟寄存器: + +| 地址 | 名称 | 初始值 | 属性 | +| --------- | ---------- | ------ | ------ | +| 0x00 | DEVICE_ID | 0xA5 | 只读 | +| 0x01 | STATUS | 0x01 | 可读 | +| 0x02 | CONFIG | 0x00 | 可读写 | +| 0x03 | DATA | 0x00 | 可读写 | +| 0x04~0xFF | 普通寄存器 | 0x00 | 可读写 | + + +## 四、实验内容一:设计 test_dev 框架层 + +### 实验内容大纲 + +#### 是什么知识点 + +- 自定义设备结构体 +- struct rt_device +- ops 函数指针表 +- 设备专用 API + +#### 为什么这样做 + +- 模仿 RT-Thread PIN 驱动框架 +- 将框架层与具体硬件实现分离 +- 避免应用直接依赖底层 virtual_i2c 实现 + +#### 意义是什么 + +- 提高驱动代码的模块化程度 +- 实现接口与实现解耦 +- 为不同底层实现提供统一上层 API + + +### 4.1 test_dev.h 设计 + +介绍: + +- TEST_DEV_REG_SIZE +- struct rt_test_ops +- struct rt_test_device +- test_dev_register() +- test_dev_read/write API + + +### 4.2 test_dev.c 框架实现 + +重点分析: + +test_dev_write_reg() + ↓ +dev->ops->write_reg() + +test_dev_read_reg() + ↓ +dev->ops->read_reg() + +说明框架层本身不操作虚拟寄存器。 + + +## 五、实验内容二:设计虚拟 I2C 底层驱动 + +### 实验内容大纲 + +#### 是什么知识点 + +- 设备私有数据 +- user_data +- 虚拟寄存器 +- ops 具体实现 + +#### 为什么这样做 + +- 没有依赖真实 I2C 硬件 +- 使用内存数组模拟 I2C 寄存器设备 +- 将重点放在设备驱动框架设计上 + +#### 意义是什么 + +- 可以验证完整设备驱动调用链 +- 降低硬件因素对实验的影响 +- 方便理解框架层与驱动实现层的关系 + + +### 5.1 virtual_i2c_data + +包含: + +- addr +- regs[256] + + +### 5.2 virtual_i2c_init() + +初始化: + +- I2C 地址 0x50 +- DEVICE_ID = 0xA5 +- STATUS = 0x01 +- CONFIG = 0x00 +- DATA = 0x00 + + +### 5.3 单寄存器读写 + +实现: + +- virtual_i2c_write_reg() +- virtual_i2c_read_reg() + + +### 5.4 连续寄存器读写 + +实现: + +- virtual_i2c_write_regs() +- virtual_i2c_read_regs() + + +### 5.5 保护机制 + +实现: + +- DEVICE_ID 只读保护 +- 256 Byte 寄存器边界保护 + + +## 六、实验内容三:ops 接口绑定与设备注册 + +### 实验内容大纲 + +#### 是什么知识点 + +- 函数指针表 +- rt_device_register() +- INIT_DEVICE_EXPORT() +- RT-Thread 自动初始化机制 + +#### 为什么这样做 + +- 需要将 test_dev 框架定义的接口和 virtual_i2c 的具体实现连接 +- 需要把 test_dev 加入 RT-Thread 设备管理器 + +#### 意义是什么 + +- 实现框架层和驱动层的动态绑定 +- 使设备能够通过设备名称进行统一管理 +- 体现 RT-Thread 驱动注册机制 + + +### 6.1 virtual_i2c_ops + +建立: + +rt_test_ops + ↓ +virtual_i2c_ops + ├── virtual_i2c_init + ├── virtual_i2c_write_reg + ├── virtual_i2c_read_reg + ├── virtual_i2c_write_regs + └── virtual_i2c_read_regs + + +### 6.2 test_dev_register() + +注册: + +virtual_i2c_dev + + +virtual_i2c_ops + + +virtual_i2c + ↓ +test_dev_register() + ↓ +rt_device_register() + + +### 6.3 自动初始化 + +INIT_DEVICE_EXPORT(virtual_i2c_hw_init) + +启动输出: + +[test_dev] virtual i2c init +[test_dev] addr = 0x50 +[test_dev] device id = 0xA5 +[test_dev] register success + + +## 七、实验内容四:应用层功能测试 + +### 实验内容大纲 + +#### 是什么知识点 + +- rt_device_find() +- 设备对象类型转换 +- 设备专用 API 调用 +- 驱动功能验证 + +#### 为什么这样做 + +- 验证 test_dev 是否真正加入 RT-Thread 设备管理器 +- 验证应用层是否能够经过框架层访问虚拟 I2C 设备 + +#### 意义是什么 + +- 验证整个设备驱动框架是否真正打通 +- 验证框架抽象不是停留在代码结构上,而是可以实际运行 + + +### 7.1 查找设备 + +rt_device_find("test_dev") + +真实结果: + +[test_demo] test_dev found + + +### 7.2 DEVICE_ID 读取测试 + +读取: + +0x00 + +真实结果: + +[test_dev] read reg[0x00] = 0xA5 +[test_demo] device id = 0xA5 + + +### 7.3 单寄存器读写测试 + +写入: + +CONFIG(0x02) = 0x55 + +真实结果: + +[test_dev] write reg[0x02] = 0x55 +[test_dev] read reg[0x02] = 0x55 +[test_demo] config = 0x55 + + +### 7.4 连续寄存器读写测试 + +写入: + +0x10 -> 0x11 +0x11 -> 0x22 +0x12 -> 0x33 +0x13 -> 0x44 + +真实结果: + +[test_dev] write 4 regs from 0x10 +[test_dev] read 4 regs from 0x10 +[test_demo] read data: 0x11 0x22 0x33 0x44 + + +## 八、实验内容五:异常访问与保护测试 + +### 实验内容大纲 + +#### 是什么知识点 + +- 参数合法性检查 +- 只读寄存器 +- 地址越界保护 +- 驱动健壮性 + +#### 为什么这样做 + +- 驱动程序不能只处理正常输入 +- 错误访问如果不进行检查可能导致寄存器数据异常或内存越界 + +#### 意义是什么 + +- 提高驱动程序可靠性 +- 验证设备框架具有基本的错误处理能力 + + +### 8.1 DEVICE_ID 只读测试 + +尝试: + +DEVICE_ID = 0x66 + +真实结果: + +[test_demo] test read-only register +[test_dev] reg 0x00 is read only +[test_demo] read-only protection works +[test_dev] read reg[0x00] = 0xA5 +[test_demo] device id after write = 0xA5 + +结论: + +DEVICE_ID 没有被修改,只读保护正常。 + + +### 8.2 地址越界测试 + +从: + +0xFE + +连续写: + +4 Byte + +实际需要访问: + +0xFE +0xFF +0x100 +0x101 + +其中 0x100、0x101 已超出 256 Byte 寄存器空间。 + +真实结果: + +[test_demo] test boundary protection +[test_demo] boundary protection works + +结论: + +寄存器边界保护正常。 + + +## 九、完整调用链分析 + +### 9.1 单寄存器写调用链 + +test_demo + ↓ +test_dev_write_reg() + ↓ +dev->ops->write_reg() + ↓ +virtual_i2c_write_reg() + ↓ +dev->user_data + ↓ +virtual_i2c.regs[reg] = value + + +### 9.2 单寄存器读调用链 + +test_demo + ↓ +test_dev_read_reg() + ↓ +dev->ops->read_reg() + ↓ +virtual_i2c_read_reg() + ↓ +dev->user_data + ↓ +value = virtual_i2c.regs[reg] + + +## 十、与 RT-Thread PIN 驱动框架对比 + +| PIN 框架 | test_dev | +| ------------------------ | ----------------------- | +| struct rt_device_pin | struct rt_test_device | +| struct rt_pin_ops | struct rt_test_ops | +| _stm32_pin_ops | virtual_i2c_ops | +| rt_device_pin_register() | test_dev_register() | +| rt_pin_write() | test_dev_write_reg() | +| stm32_pin_write() | virtual_i2c_write_reg() | +| GPIO 硬件 | regs[256] 虚拟寄存器 | + +说明: + +test_dev 并不是复制 PIN 驱动,而是将 PIN 框架中的设备抽象、ops、注册机制和分层思想迁移到新的虚拟设备中。 + + +## 十一、实验中出现的问题及解决方法 + +### 11.1 virtual_i2c_ops 未声明 + +编译错误: + +virtual_i2c_ops undeclared + +同时出现: + +virtual_i2c_init defined but not used +virtual_i2c_write_reg defined but not used +... + +#### 原因: + +缺少 virtual_i2c_ops 函数指针表。 + +#### 解决: + +定义: + +static const struct rt_test_ops virtual_i2c_ops + +并将 5 个 virtual_i2c_xxx() 函数绑定到对应接口。 + + +## 十二、实验结果 + +汇总真实实验结果: + +![image-20260820204023110](figures/image-20260820204023110.png) + +- test_dev 注册成功 +- rt_device_find() 查找成功 +- DEVICE_ID = 0xA5 +- CONFIG 写入并读回 0x55 +- 连续读写结果为 0x11 0x22 0x33 0x44 +- DEVICE_ID 只读保护正常 +- 寄存器越界保护正常 + + +## 十三、理论总结 + +重点总结: + +1. RT-Thread 设备驱动的分层思想 +2. struct rt_device 的作用 +3. ops 函数指针表的作用 +4. user_data 的作用 +5. 设备注册与查找机制 +6. 框架层与底层实现解耦 +7. 虚拟驱动对于理解真实设备驱动框架的意义 + + +## 十四、实验总结 + +总结本次从 PIN 驱动框架源码分析,到自主设计 test_dev,再到完成虚拟 I2C 底层驱动、设备注册、功能测试和异常保护的完整过程。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..8f5ebf2945090f1d8dcfdf1df204e6b788f983ce --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1842 @@ +# Day3 + +## IPC —— 线程间同步与通信 + +在多线程实时系统中,一个任务通常需要多个线程相互配合完成。不同线程之间可能会访问相同的资源,也可能需要按照一定的先后顺序执行,因此需要使用 **IPC(Inter-Process Communication,线程间通信机制)** 来完成线程之间的同步与数据交换。 + +RT-Thread 中常用的 IPC 机制主要包括: + +### 线程同步 + +- 信号量 `Semaphore` +- 互斥量 `Mutex` +- 事件集 `Event` + +### 线程间通信 + +- 邮箱 `Mailbox` +- 消息队列 `Message Queue` + +其中,信号量、互斥量和事件集主要用于控制多个线程之间的执行顺序以及共享资源访问;邮箱和消息队列主要用于在线程之间传递数据。RT-Thread 的 IPC 对象支持按照 **FIFO** 或 **线程优先级** 管理等待线程。 + +常用 IPC 等待标志: + +```c +RT_IPC_FLAG_FIFO +RT_IPC_FLAG_PRIO +``` + +含义: + +```text +RT_IPC_FLAG_FIFO +按照先进先出顺序唤醒等待线程。 + +RT_IPC_FLAG_PRIO +按照线程优先级排列等待线程, +优先级较高的线程优先获得资源。 +``` + +常用等待时间: + +```c +RT_WAITING_FOREVER +RT_WAITING_NO +``` + +其中: + +```text +RT_WAITING_FOREVER = -1 +一直阻塞,直到获得资源。 + +RT_WAITING_NO = 0 +不等待,如果资源当前不可用立即返回。 +``` + +------ + +## 线程同步 + +线程同步指多个线程通过一定的机制控制执行顺序,使线程按照预期的次序运行。 + +例如: + +```text +线程1:采集传感器数据 + ↓ + 写入共享内存 + ↓ +线程2:读取共享内存 + ↓ + 显示数据 +``` + +如果线程 1 还没有完成数据写入,线程 2 就开始读取数据,可能导致读取到不完整的数据。 + +因此多个线程访问同一个共享资源时,需要对共享资源进行保护。 + +多个线程共同访问的代码区域或资源称为: + +```text +临界区(Critical Section) +``` + +同一时刻通常只允许一个线程进入临界区。 + +RT-Thread 中常见的线程同步方式包括: + +```text +信号量 +互斥量 +事件集 +``` + +除此之外,还可以通过: + +```c +rt_enter_critical(); +rt_exit_critical(); +``` + +进入和退出临界区。RT-Thread 官方文档将线程同步描述为通过互斥量、事件对象、临界区等机制建立线程之间的执行顺序关系。 + +------ + +# 信号量 Semaphore + +## 信号量基本概念 + +信号量是一种轻量级的内核对象,可以用于解决线程之间的: + +```text +同步 ++ +互斥 +``` + +问题。 + +可以将信号量理解为一个 **资源计数器**。 + +例如停车场共有 5 个停车位: + +```text +信号量初值 = 5 +``` + +一辆车进入停车场: + +```text +获取一个信号量 + +5 → 4 +``` + +一辆车离开停车场: + +```text +释放一个信号量 + +4 → 5 +``` + +如果: + +```text +信号量 = 0 +``` + +表示当前已经没有可用资源。 + +此时继续申请信号量的线程会进入等待状态,直到其他线程释放信号量。RT-Thread 中每个信号量都包含一个信号量值以及一个等待线程队列。 + +------ + +## 信号量控制块 + +RT-Thread 使用: + +```c +struct rt_semaphore +``` + +管理信号量。 + +信号量句柄类型为: + +```c +rt_sem_t +``` + +其本质是一个指向信号量控制块的指针。 + +```c +typedef struct rt_semaphore *rt_sem_t; +``` + +------ + +## 信号量的基本操作 + +信号量的生命周期可以概括为: + +```text +创建 + ↓ +获取 + ↓ +使用资源 + ↓ +释放 + ↓ +删除 +``` + +即: + +```c +rt_sem_create() + ↓ +rt_sem_take() + ↓ + 临界区 + ↓ +rt_sem_release() + ↓ +rt_sem_delete() +``` + +对于静态信号量则为: + +```c +rt_sem_init() + ↓ +rt_sem_take() + ↓ +rt_sem_release() + ↓ +rt_sem_detach() +``` + +官方手册明确将信号量操作划分为创建/初始化、获取、释放以及删除/脱离。 + +------ + +## 创建信号量 + +动态创建信号量: + +```c +rt_sem_t rt_sem_create(const char *name, + rt_uint32_t value, + rt_uint8_t flag); +``` + +参数: + +| 参数 | 说明 | +| ------- | ------------------ | +| `name` | 信号量名称 | +| `value` | 信号量初始值 | +| `flag` | 等待线程的排队方式 | + +其中 `flag` 可以为: + +```c +RT_IPC_FLAG_FIFO +``` + +或者: + +```c +RT_IPC_FLAG_PRIO +``` + +例如: + +```c +rt_sem_t sem; + +sem = rt_sem_create("sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +这里: + +```text +名称 = sem +初值 = 0 +等待方式 = FIFO +``` + +创建成功返回信号量句柄,创建失败返回: + +```c +RT_NULL +``` + +------ + +## 获取信号量 + +线程通过: + +```c +rt_sem_take() +``` + +获取信号量。 + +基本形式: + +```c +rt_sem_take(sem, timeout); +``` + +例如: + +```c +rt_sem_take(sem, RT_WAITING_FOREVER); +``` + +表示: + +```text +如果当前存在信号量: + 信号量值 -1 + 线程继续运行 + +如果当前信号量为 0: + 当前线程挂起 + 一直等待其他线程释放信号量 +``` + +也可以使用: + +```c +rt_sem_take(sem, RT_WAITING_NO); +``` + +表示: + +```text +不等待。 + +如果当前无法获得信号量, +立即返回。 +``` + +------ + +## 释放信号量 + +线程完成操作之后,可以释放信号量: + +```c +rt_sem_release(sem); +``` + +例如: + +```c +rt_sem_release(sem); +``` + +其作用可以理解为: + +```text +信号量值 +1 +``` + +如果当前已经有线程因为等待这个信号量而挂起,则释放信号量后,系统会唤醒相应的等待线程。 + +因此一种非常常见的线程同步方式为: + +```text +线程 A + | +完成某项工作 + | +rt_sem_release() + | + V +信号量 + | +rt_sem_take() + | +线程 B + | +继续执行 +``` + +------ + +## 删除信号量 + +使用: + +```c +rt_sem_create() +``` + +动态创建的信号量,在不再使用时需要调用: + +```c +rt_sem_delete(); +``` + +函数: + +```c +rt_err_t rt_sem_delete(rt_sem_t sem); +``` + +例如: + +```c +rt_sem_delete(sem); +``` + +删除信号量后,系统会释放对应的内存资源。 + +如果此时仍有线程等待该信号量,RT-Thread 会先唤醒这些线程,这些等待线程得到: + +```c +-RT_ERROR +``` + +随后再释放信号量占用的内存。 + +------ + +## 静态信号量 + +如果不希望动态申请内存,也可以使用静态信号量。 + +首先定义: + +```c +static struct rt_semaphore sem; +``` + +初始化: + +```c +rt_sem_init(&sem, + "sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +使用结束后: + +```c +rt_sem_detach(&sem); +``` + +因此: + +```text +动态对象 + +rt_sem_create() +rt_sem_delete() +``` + +对应: + +```text +静态对象 + +rt_sem_init() +rt_sem_detach() +``` + +------ + +## 信号量同步示例 + +假设: + +```text +线程1负责采集数据 +线程2负责处理数据 +``` + +线程 2 必须等线程 1 采集完成后才能运行。 + +可以建立: + +```c +static rt_sem_t sem; +``` + +创建: + +```c +sem = rt_sem_create("sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +线程 1: + +```c +void thread1_entry(void *parameter) +{ + while (1) + { + /* 采集数据 */ + + rt_kprintf("thread1: data ready\n"); + + rt_sem_release(sem); + + rt_thread_mdelay(1000); + } +} +``` + +线程 2: + +```c +void thread2_entry(void *parameter) +{ + while (1) + { + rt_sem_take(sem, RT_WAITING_FOREVER); + + rt_kprintf("thread2: process data\n"); + } +} +``` + +执行过程: + +```text +thread2 + | +rt_sem_take() + | +信号量 = 0 + | +线程挂起 + | +thread1 + | +采集完成 + | +rt_sem_release() + | +唤醒 thread2 + | +thread2 继续运行 +``` + +------ + +# 互斥量 Mutex + +## 互斥量基本概念 + +互斥量主要用于: + +```text +保护共享资源 +``` + +例如两个线程同时操作串口: + +```text +Thread 1 ----\ + ---> UART +Thread 2 ----/ +``` + +如果两个线程同时输出数据,可能导致输出内容混乱。 + +因此可以添加互斥量: + +```text +Thread 1 + | +获取 Mutex + | +使用 UART + | +释放 Mutex + +Thread 2 + | +等待 Mutex + | +使用 UART +``` + +从而保证: + +```text +同一时刻只有一个线程访问 UART +``` + +------ + +## 互斥量和信号量的区别 + +互斥量可以看作一种特殊的二值信号量,但它具有: + +```text +所有权 +递归获取 +优先级继承 +``` + +等特点。 + +RT-Thread 官方文档指出,互斥量可以避免实时系统中常见的 **优先级翻转问题**。 + +------ + +## 优先级翻转 + +假设三个线程: + +```text +Thread A 高优先级 +Thread B 中优先级 +Thread C 低优先级 +``` + +Thread C 先获得共享资源: + +```text +C 获取 Mutex +``` + +此时高优先级 Thread A 也需要这个资源: + +```text +A 等待 C +``` + +但是 Thread B 又抢占了 C: + +```text +B 抢占 C +``` + +最终出现: + +```text +高优先级 A + +却必须等待 + +中优先级 B +``` + +这就是: + +```text +优先级翻转 +``` + +RT-Thread 的互斥量使用 **优先级继承**: + +```text +C 原本:低优先级 + +A 等待 C 后: + +C 临时继承 A 的高优先级 + +C 快速执行完临界区 + ↓ +释放 Mutex + ↓ +优先级恢复 +``` + +从而降低优先级翻转造成的影响。 + +------ + +## 创建互斥量 + +```c +rt_mutex_t rt_mutex_create(const char *name, + rt_uint8_t flag); +``` + +例如: + +```c +rt_mutex_t mutex; + +mutex = rt_mutex_create("mutex", + RT_IPC_FLAG_PRIO); +``` + +创建失败返回: + +```c +RT_NULL +``` + +------ + +## 获取互斥量 + +```c +rt_mutex_take(mutex, + RT_WAITING_FOREVER); +``` + +获取成功后: + +```text +当前线程成为 Mutex 的拥有者 +``` + +其他线程如果再次申请: + +```text +进入等待状态 +``` + +但是拥有 Mutex 的线程本身可以再次获取同一个 Mutex,即支持: + +```text +递归获取 +``` + +------ + +## 释放互斥量 + +```c +rt_mutex_release(mutex); +``` + +使用原则: + +```c +rt_mutex_take() + ↓ + 临界区 + ↓ +rt_mutex_release() +``` + +互斥量获取后应该尽快释放,避免长时间占用共享资源。RT-Thread 官方还特别指出,在持有互斥量期间不应修改持有线程的优先级。 + +------ + +## 删除互斥量 + +动态创建: + +```c +rt_mutex_create() +``` + +对应删除: + +```c +rt_mutex_delete(mutex); +``` + +函数: + +```c +rt_err_t rt_mutex_delete(rt_mutex_t mutex); +``` + +删除时如果还有线程等待该互斥量,等待线程会被唤醒并返回: + +```c +-RT_ERROR +``` + +------ + +## 静态互斥量 + +静态初始化: + +```c +rt_mutex_init(); +``` + +例如: + +```c +static struct rt_mutex mutex; + +rt_mutex_init(&mutex, + "mutex", + RT_IPC_FLAG_PRIO); +``` + +不再使用: + +```c +rt_mutex_detach(&mutex); +``` + +因此: + +```text +动态: +create → delete + +静态: +init → detach +``` + +------ + +## 互斥量注意事项 + +互斥量不能用于: + +```text +中断服务程序 ISR +``` + +因为互斥量具有线程所有权和阻塞等待机制。 + +因此: + +```c +/* 不要在 ISR 中这样做 */ +rt_mutex_take(...); +``` + +RT-Thread 官方明确规定 Mutex 不能在中断服务程序中使用。 + +------ + +# 事件集 Event + +## 事件集基本概念 + +信号量通常用于: + +```text +等待一个条件 +``` + +而事件集可以让线程同时等待: + +```text +多个事件 +``` + +RT-Thread 中一个事件集使用一个: + +```c +rt_uint32_t +``` + +来保存事件,因此一个事件集可以表示: + +```text +32 个事件 +``` + +每一个 bit 表示一个事件: + +```text +bit0 → Event 0 +bit1 → Event 1 +bit2 → Event 2 + +... + +bit31 → Event 31 +``` + +例如: + +```c +#define EVENT_KEY1 (1 << 0) +#define EVENT_KEY2 (1 << 1) +#define EVENT_UART (1 << 2) +``` + +------ + +## 事件触发方式 + +事件集支持: + +```c +RT_EVENT_FLAG_OR +``` + +和: + +```c +RT_EVENT_FLAG_AND +``` + +### OR + +```c +RT_EVENT_FLAG_OR +``` + +表示: + +```text +任意一个指定事件发生 +即可唤醒线程。 +``` + +例如: + +```text +等待: + +EVENT1 | EVENT2 + +只要: + +EVENT1 + +或者: + +EVENT2 + +任意一个发生即可。 +``` + +------ + +### AND + +```c +RT_EVENT_FLAG_AND +``` + +表示: + +```text +所有指定事件全部发生后 +才唤醒线程。 +``` + +例如: + +```text +EVENT1 ++ +EVENT2 +``` + +必须两个事件都已经发生才能满足条件。 + +RT-Thread 还支持: + +```c +RT_EVENT_FLAG_CLEAR +``` + +表示线程成功接收到事件以后: + +```text +自动清除对应事件标志位 +``` + +------ + +## 创建事件集 + +```c +rt_event_t rt_event_create(const char *name, + rt_uint8_t flag); +``` + +例如: + +```c +rt_event_t event; + +event = rt_event_create("event", + RT_IPC_FLAG_FIFO); +``` + +------ + +## 发送事件 + +使用: + +```c +rt_event_send(); +``` + +例如: + +```c +rt_event_send(event, EVENT_KEY1); +``` + +表示: + +```text +EVENT_KEY1 发生 +``` + +可以一次发送多个事件: + +```c +rt_event_send(event, + EVENT_KEY1 | EVENT_KEY2); +``` + +------ + +## 接收事件 + +使用: + +```c +rt_event_recv(); +``` + +典型形式: + +```c +rt_event_recv(event, + EVENT_KEY1 | EVENT_KEY2, + RT_EVENT_FLAG_OR | + RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, + &received); +``` + +含义: + +```text +等待 EVENT_KEY1 或 EVENT_KEY2 + +任意一个发生即可唤醒 + +接收成功后自动清除事件 + +如果没有事件则一直等待 +``` + +------ + +## 删除事件集 + +动态创建: + +```c +rt_event_create() +``` + +对应: + +```c +rt_event_delete(event); +``` + +静态事件集则: + +```c +rt_event_init(); +``` + +最终使用: + +```c +rt_event_detach(); +``` + +官方事件集管理接口包括创建/初始化、发送、接收和删除/脱离。 + +------ + +# 线程之间的通信 + +线程同步主要解决: + +```text +什么时候执行? +谁先执行? +谁能访问资源? +``` + +线程通信主要解决: + +```text +线程之间传递什么数据? +``` + +RT-Thread 中常用: + +```text +Mailbox +Message Queue +``` + +即: + +```text +邮箱 +消息队列 +``` + +------ + +# 邮箱 Mailbox + +## 邮箱基本原理 + +邮箱是一种开销较低、效率较高的线程通信机制。 + +基本结构: + +```text +Thread 1 + | +发送邮件 + | + V ++---------+ +| Mailbox | ++---------+ + | +接收邮件 + | +Thread 2 +``` + +RT-Thread 的邮箱中: + +```text +每封邮件固定为 4 字节 +``` + +在 32 位系统中: + +```text +一个指针通常也是 4 字节 +``` + +因此邮箱除了发送: + +```text +整数 +状态 +命令 +``` + +也可以发送: + +```text +数据指针 +``` + +------ + +## 创建邮箱 + +```c +rt_mailbox_t rt_mb_create(const char *name, + rt_size_t size, + rt_uint8_t flag); +``` + +其中: + +```text +name +邮箱名称 + +size +邮箱容量,即可以保存多少封邮件 + +flag +FIFO / PRIO +``` + +例如: + +```c +rt_mailbox_t mb; + +mb = rt_mb_create("mb", + 8, + RT_IPC_FLAG_FIFO); +``` + +表示: + +```text +邮箱最多保存 8 封邮件 +``` + +每封邮件为 4 字节。 + +------ + +## 发送邮件 + +普通发送: + +```c +rt_mb_send(mb, value); +``` + +例如: + +```c +rt_mb_send(mb, 100); +``` + +如果邮箱未满: + +```text +数据写入邮箱 +``` + +邮箱非阻塞发送可以安全地用于中断服务程序。 + +------ + +## 等待发送 + +也可以: + +```c +rt_mb_send_wait(mb, + value, + timeout); +``` + +如果邮箱已经满了,可以等待邮箱出现空闲空间。 + +------ + +## 接收邮件 + +```c +rt_mb_recv(mb, + &value, + timeout); +``` + +例如: + +```c +rt_ubase_t value; + +rt_mb_recv(mb, + &value, + RT_WAITING_FOREVER); +``` + +如果邮箱为空: + +```text +当前线程挂起 +``` + +收到新邮件后: + +```text +线程被唤醒 +``` + +如果设置了超时时间,并且一直没有邮件,则返回超时错误。 + +------ + +## 删除邮箱 + +动态邮箱: + +```c +rt_mb_delete(mb); +``` + +完整生命周期: + +```text +rt_mb_create() + ↓ +rt_mb_send() + ↓ +rt_mb_recv() + ↓ +rt_mb_delete() +``` + +静态邮箱: + +```text +rt_mb_init() + ↓ +发送 / 接收 + ↓ +rt_mb_detach() +``` + +动态创建的邮箱不再使用时应通过 `rt_mb_delete()` 释放对应系统资源。 + +------ + +# 消息队列 Message Queue + +## 消息队列基本概念 + +消息队列也是一种常见的线程间通信方式,可以认为是邮箱的扩展。 + +邮箱: + +```text +固定 4 字节 +``` + +消息队列: + +```text +可以传输长度大于 4 字节的消息 +``` + +因此消息队列更适合传输: + +```text +结构体 +传感器数据 +UART 数据 +协议数据包 +复杂消息 +``` + +RT-Thread 消息队列可以接收来自: + +```text +线程 +中断服务程序 +``` + +发送的消息,并将这些消息缓存到内部存储空间。普通消息按照 FIFO 顺序传递。 + +------ + +## 创建消息队列 + +```c +rt_mq_t rt_mq_create(const char *name, + rt_size_t msg_size, + rt_size_t max_msgs, + rt_uint8_t flag); +``` + +例如: + +```c +rt_mq_t mq; + +mq = rt_mq_create("mq", + sizeof(struct msg), + 10, + RT_IPC_FLAG_FIFO); +``` + +参数: + +| 参数 | 含义 | +| ---------- | ---------------------- | +| `name` | 消息队列名称 | +| `msg_size` | 单条消息最大长度 | +| `max_msgs` | 最多能够存储多少条消息 | +| `flag` | FIFO 或 PRIO | + +------ + +## 发送消息 + +使用: + +```c +rt_mq_send(); +``` + +例如: + +```c +struct msg +{ + rt_uint32_t id; + rt_uint32_t value; +}; + +struct msg data; + +data.id = 1; +data.value = 100; + +rt_mq_send(mq, + &data, + sizeof(data)); +``` + +消息的数据本身会被复制进消息队列。 + +------ + +## 等待发送 + +如果需要在消息队列满时等待: + +```c +rt_mq_send_wait(); +``` + +可以指定等待时间。 + +------ + +## 紧急消息 + +消息队列还支持: + +```c +rt_mq_urgent(); +``` + +普通消息: + +```text +插入队列尾部 +``` + +紧急消息: + +```text +插入队列头部 +``` + +所以紧急消息会比普通消息更早被接收。 + +------ + +## 接收消息 + +```c +rt_mq_recv(); +``` + +例如: + +```c +struct msg data; + +rt_mq_recv(mq, + &data, + sizeof(data), + RT_WAITING_FOREVER); +``` + +如果当前队列为空: + +```text +接收线程挂起 +``` + +当新的消息进入: + +```text +线程被唤醒 +``` + +然后读取消息并继续执行。 + +------ + +## 删除消息队列 + +动态创建: + +```c +rt_mq_create() +``` + +对应: + +```c +rt_mq_delete(mq); +``` + +完整过程: + +```text +rt_mq_create() + ↓ +rt_mq_send() + ↓ +rt_mq_recv() + ↓ +rt_mq_delete() +``` + +如果使用静态消息队列: + +```text +rt_mq_init() + ↓ +发送 / 接收 + ↓ +rt_mq_detach() +``` + +RT-Thread 官方将消息队列的主要操作归纳为创建、发送、接收和删除,并为静态对象提供 `rt_mq_init()`。 + +------ + +# 邮箱与消息队列的区别 + +| 对比 | 邮箱 Mailbox | 消息队列 Message Queue | +| ---------------------- | ------------------ | ---------------------- | +| 单条消息大小 | 固定 4 字节 | 可自定义 | +| 数据类型 | 整数、状态、指针等 | 数组、结构体、数据块等 | +| 内存开销 | 较小 | 相对较大 | +| 效率 | 较高 | 相对较低 | +| 是否缓存消息 | 是 | 是 | +| 普通发送是否可用于 ISR | 可以 | 可以 | +| 接收是否可阻塞 | 可以 | 可以 | +| 是否支持紧急消息 | 否 | 是,`rt_mq_urgent()` | + +因此: + +```text +简单状态、命令、指针 + ↓ + Mailbox +``` + +而: + +```text +结构体、数据包、较大的消息 + ↓ + Message Queue +``` + +------ + +# IPC 使用场景总结 + +## 信号量 + +主要解决: + +```text +线程同步 +资源计数 +``` + +典型场景: + +```text +采集线程完成 + ↓ +release semaphore + ↓ +处理线程开始运行 +``` + +------ + +## 互斥量 + +主要解决: + +```text +共享资源互斥访问 +``` + +典型场景: + +```text +UART +I2C +SPI +共享数据结构 +文件系统资源 +``` + +------ + +## 事件集 + +主要解决: + +```text +一个线程等待多个条件 +``` + +例如: + +```text +网络连接成功 + AND +设备初始化完成 + AND +传感器准备完成 +``` + +满足全部条件后线程开始工作。 + +------ + +## 邮箱 + +主要解决: + +```text +小数据、高效率线程通信 +``` + +例如: + +```text +按键状态 +命令 +ADC 数值 +对象指针 +``` + +------ + +## 消息队列 + +主要解决: + +```text +较复杂的数据传输 +``` + +例如: + +```text +传感器数据结构体 +UART 数据包 +控制命令结构体 +协议消息 +``` + +------ + +# IPC 生命周期总结 + +这是使用 RT-Thread IPC 时非常重要的一组对应关系: + +| IPC | 动态创建 | 动态删除 | 静态初始化 | 静态脱离 | +| -------- | ------------------- | ------------------- | ----------------- | ------------------- | +| 信号量 | `rt_sem_create()` | `rt_sem_delete()` | `rt_sem_init()` | `rt_sem_detach()` | +| 互斥量 | `rt_mutex_create()` | `rt_mutex_delete()` | `rt_mutex_init()` | `rt_mutex_detach()` | +| 事件集 | `rt_event_create()` | `rt_event_delete()` | `rt_event_init()` | `rt_event_detach()` | +| 邮箱 | `rt_mb_create()` | `rt_mb_delete()` | `rt_mb_init()` | `rt_mb_detach()` | +| 消息队列 | `rt_mq_create()` | `rt_mq_delete()` | `rt_mq_init()` | `rt_mq_detach()` | + +可以统一记忆为: + +```text +动态对象: + +create + ↓ +使用 + ↓ +delete +``` + +静态对象: + +```text +init + ↓ +使用 + ↓ +detach +``` + +------ + +# 常用 API 总结 + +## Semaphore + +```c +rt_sem_create() +rt_sem_delete() + +rt_sem_init() +rt_sem_detach() + +rt_sem_take() +rt_sem_release() +``` + +------ + +## Mutex + +```c +rt_mutex_create() +rt_mutex_delete() + +rt_mutex_init() +rt_mutex_detach() + +rt_mutex_take() +rt_mutex_release() +``` + +------ + +## Event + +```c +rt_event_create() +rt_event_delete() + +rt_event_init() +rt_event_detach() + +rt_event_send() +rt_event_recv() +``` + +------ + +## Mailbox + +```c +rt_mb_create() +rt_mb_delete() + +rt_mb_init() +rt_mb_detach() + +rt_mb_send() +rt_mb_send_wait() +rt_mb_recv() +``` + +------ + +## Message Queue + +```c +rt_mq_create() +rt_mq_delete() + +rt_mq_init() +rt_mq_detach() + +rt_mq_send() +rt_mq_send_wait() +rt_mq_urgent() +rt_mq_recv() +``` + +------ + +# Day3 理论总结 + +RT-Thread 的 IPC 机制主要解决两个问题: + +```text +同步 ++ +通信 +``` + +其中: + +```text +Semaphore + ↓ +控制线程执行顺序、资源数量 + +Mutex + ↓ +保护共享资源 + +Event + ↓ +等待一个或多个事件条件 + +Mailbox + ↓ +传递简单的 4 字节消息 + +Message Queue + ↓ +传递较复杂的数据 +``` + +可以进一步记忆为: + +```text + RT-Thread IPC + | + +-------+-------+ + | | + 同步 通信 + | | + +---+---+ +--+--+ + | | | | | + Sem Mutex Event Mailbox MQ +``` + +信号量重点记住: + +```c +rt_sem_create() +rt_sem_take() +rt_sem_release() +rt_sem_delete() +``` + +互斥量重点记住: + +```c +rt_mutex_create() +rt_mutex_take() +rt_mutex_release() +rt_mutex_delete() +``` + +事件集重点记住: + +```c +RT_EVENT_FLAG_OR +RT_EVENT_FLAG_AND +RT_EVENT_FLAG_CLEAR +``` + +线程通信重点区分: + +```text +Mailbox + 固定 4 字节 + 开销小、效率高 + +Message Queue + 消息长度可配置 + 可以传递结构体等复杂数据 +``` + +在实际 RT-Thread 程序中,应根据不同应用需求选择合适的 IPC 机制,而不是仅使用全局变量在线程之间交换信息。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..ff3c6b18e24798d864617b18008189fbd37f0723 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1451 @@ +# Day5 + +## 1. RT-Thread 软件包生态 + +RT-Thread 除了提供线程调度、IPC、设备驱动等基础功能外,还提供了较完整的**软件包生态系统**。开发者可以直接使用社区维护的软件包快速集成传感器、网络协议、文件系统、图形界面等功能,从而减少重复开发。 + +常见的软件包类型包括: + +- 传感器驱动软件包 +- WiFi 驱动软件包 +- MQTT 通信软件包 +- 文件系统相关软件包 +- 网络协议软件包 +- GUI 图形界面软件包 + +软件包机制的核心思想是: + +```text +选择软件包 + ↓ +配置软件包 + ↓ +下载软件包 + ↓ +参与工程编译 + ↓ +调用软件包 API +``` + +相比直接操作底层寄存器,使用软件包可以提高代码复用率,也能够降低应用开发难度。 + +### 1.1 软件包索引 + +RT-Thread 并不会将所有软件包的源码全部存放在内核仓库中,而是通过独立的软件包索引记录各个软件包的信息。 + +软件包索引通常包含: + +```text +软件包名称 +软件包仓库地址 +软件包版本 +软件包依赖关系 +Kconfig 配置 +软件包描述 +``` + +因此,软件包索引可以理解为一个统一的软件包目录。 + +整体关系如下: + +```text +RT-Thread + │ + ↓ +软件包索引 + │ + ├── Sensor 软件包 + ├── WiFi 软件包 + ├── MQTT 软件包 + ├── 文件系统软件包 + └── 其他软件包 +``` + +开发者在 `menuconfig` 中选择某个软件包后,env 工具会根据软件包索引找到对应的软件仓库并下载源码。 + +### 1.2 env 工具 + +`env` 是 RT-Thread 提供的开发辅助环境,其中包含: + +- menuconfig 配置工具 +- Kconfig 配置环境 +- SCons 构建环境 +- 软件包管理工具 + +RT-Thread Studio 内部同样集成了 env 环境。 + +典型的软件包配置流程为: + +```text +menuconfig + ↓ +RT-Thread online packages + ↓ +选择软件包 + ↓ +保存配置 + ↓ +更新软件包 + ↓ +下载源码 +``` + +常见的软件包更新命令为: + +```bash +pkgs --update +``` + +该命令会根据当前工程配置下载或更新对应的软件包。 + +### 1.3 Kconfig 配置机制 + +RT-Thread 使用 Kconfig 描述不同功能的配置选项及依赖关系。 + +例如一个传感器软件包可能依赖: + +```text +I2C +Sensor Framework +libc +``` + +Kconfig 中可以定义: + +- 软件包是否启用 +- 软件包版本 +- 功能选项 +- 默认参数 +- 软件包依赖关系 + +因此,`menuconfig` 实际上是 Kconfig 配置系统的菜单界面。 + +整体关系如下: + +```text +Kconfig + ↓ +menuconfig + ↓ +配置文件 + ↓ +rtconfig.h + ↓ +SCons + ↓ +决定参与编译的源码 +``` + +### 1.4 软件包版本兼容 + +不同 RT-Thread 内核版本之间可能存在: + +- API 变化 +- Kconfig 变化 +- 组件目录变化 +- 软件包依赖关系变化 + +因此,软件包需要考虑不同内核版本之间的兼容问题。 + +RT-Thread Studio 会针对不同版本提供不同的 env 配置。 + +课程中涉及的版本关系可以理解为: + +```text +RT-Thread 5.1.0 及以上 + ↓ + 新版 env + +较旧 RT-Thread 版本 + ↓ + 1.5.x env +``` + +这样可以减少由于内核版本和软件包索引版本不匹配引起的配置或编译错误。 + +### 1.5 软件包维护方式 + +RT-Thread 软件包生态由社区共同维护。 + +开发一个新的软件包后,通常需要: + +```text +开发软件包 + ↓ +上传代码仓库 + ↓ +添加软件包索引 + ↓ +提交 Pull Request + ↓ +审核 + ↓ +合并 +``` + +软件包索引主要记录软件包地址等信息,而软件包源码可以独立存储在对应仓库中。 + +这种模式能够使: + +- RT-Thread 内核独立维护 +- 软件包独立升级 +- 社区开发者共同参与维护 + +------ + +## 2. HT10 温湿度传感器 + +HT10 是一种数字温湿度传感器,可以通过数字总线读取环境温度和相对湿度。 + +在 RT-Thread 中,HT10 的使用并不一定需要应用程序直接操作寄存器,而可以通过软件包以及 Sensor Framework 提供的接口完成初始化和数据读取。 + +整体结构可以表示为: + +```text +应用程序 + ↓ +HT10 软件包 API + ↓ +Sensor Framework + ↓ +I2C 设备框架 + ↓ +I2C 控制器驱动 + ↓ +HT10 传感器 +``` + +### 2.1 I2C 通信 + +HT10 使用 I2C 总线进行通信。 + +I2C 是一种常见的同步串行通信协议,通常由两根信号线组成: + +| 信号 | 作用 | +| ---- | ---------- | +| SDA | 串行数据线 | +| SCL | 串行时钟线 | + +I2C 总线通常采用主从结构: + +```text +MCU + │ + ├── SCL + └── SDA + │ + ├── Device 1 + ├── Device 2 + └── Device 3 +``` + +不同从设备通过不同的 I2C 地址进行区分。 + +在 RT-Thread 中,I2C 控制器会注册为一个设备,例如: + +```text +i2c1 +i2c2 +``` + +上层驱动可以根据总线名称查找对应的 I2C 设备。 + +### 2.2 RT-Thread Sensor Framework + +Sensor Framework 是 RT-Thread 提供的统一传感器设备框架。 + +它的作用是屏蔽不同传感器之间的差异,为应用程序提供统一的访问方式。 + +例如不同类型的传感器可能包括: + +```text +温度传感器 +湿度传感器 +加速度传感器 +陀螺仪 +光照传感器 +气压传感器 +``` + +虽然底层芯片型号不同,但是通过 Sensor Framework 可以统一管理。 + +其基本层次为: + +```text +应用层 + ↓ +Sensor Framework + ↓ +具体 Sensor Driver + ↓ +I2C / SPI + ↓ +Sensor +``` + +这样可以减少应用层对具体硬件型号的依赖。 + +### 2.3 软件包依赖关系 + +HT10 软件包正常工作通常需要相关组件支持。 + +主要依赖包括: + +```text +HT10 软件包 + │ + ├── I2C Device Framework + │ + ├── Sensor Framework + │ + └── libc / printf +``` + +如果 Sensor Framework 没有开启,而软件包内部又引用了相关接口,编译时就可能出现: + +```text +undefined reference +未定义符号 +结构体或函数不存在 +``` + +因此在使用软件包前,需要先确认其依赖是否已经使能。 + +### 2.4 HT10 初始化流程 + +HT10 软件包通常会对底层操作进行封装。 + +初始化过程可以抽象为: + +```text +指定 I2C 总线 + ↓ +查找 I2C 设备 + ↓ +初始化 HT10 + ↓ +返回设备句柄 + ↓ +判断是否初始化成功 +``` + +应用程序通常只需要传入对应的 I2C 总线名称,而无需自己实现完整的寄存器初始化逻辑。 + +### 2.5 温湿度读取 + +HT10 可以分别获取: + +- 温度 +- 相对湿度 + +基本读取过程为: + +```text +HT10 + ↓ +读取原始数据 + ↓ +软件包内部转换 + ↓ +温度值 +湿度值 + ↓ +应用程序 +``` + +软件包通常已经完成: + +- I2C 命令发送 +- 数据接收 +- 原始值计算 +- 单位转换 + +应用层只需要调用相应 API。 + +### 2.6 浮点数打印 + +温度和湿度数据经常以浮点数表示,例如: + +```text +Temperature: 26.35 ℃ +Humidity: 55.42 % +``` + +但在嵌入式系统中,为了减小固件体积,默认的 `printf` 实现可能没有启用浮点格式化功能。 + +此时: + +```c +printf("%.2f", value); +``` + +可能无法正常打印。 + +原因不是传感器读取失败,而是当前 C 库的格式化输出功能没有包含浮点支持。 + +因此需要在配置中开启相应的浮点格式化支持。 + +------ + +## 3. AW007 WiFi 模块 + +AW007 是用于实现无线网络连接的 WiFi 模块。 + +在 RT-Thread 中,AW007 可以通过对应的软件包接入网络协议栈,使嵌入式设备获得 WiFi 网络通信能力。 + +整体结构为: + +```text +应用程序 + ↓ +Socket / MQTT + ↓ +TCP/IP 网络协议栈 + ↓ +AW007 软件包 + ↓ +SPI + ↓ +AW007 WiFi 模块 + ↓ +无线网络 +``` + +### 3.1 SPI 通信 + +AW007 与 MCU 之间通常通过 SPI 总线通信。 + +SPI 是一种同步串行通信协议,常见信号包括: + +| 信号 | 作用 | +| ---- | ------------------ | +| SCK | 串行时钟 | +| MOSI | 主机发送,从机接收 | +| MISO | 从机发送,主机接收 | +| CS | 从设备片选 | + +SPI 通常采用主从结构: + +```text + ┌── SPI Device 1 +MCU ── SPI ──┼── SPI Device 2 + └── SPI Device 3 +``` + +其中 SCK、MOSI 和 MISO 可以共享,而不同设备通常使用独立的 CS 信号。 + +### 3.2 AW007 控制引脚 + +除了 SPI 总线信号外,WiFi 模块通常还需要一些额外控制信号,例如: + +```text +CS +BOOT +INT +RESET +``` + +这些引脚分别负责: + +- 选择 SPI 从设备 +- 控制模块启动模式 +- 向 MCU 产生中断 +- 复位 WiFi 模块 + +软件包中的默认引脚配置不一定与实际开发板完全一致。 + +因此实际使用时,需要根据开发板原理图确认真实引脚连接。 + +### 3.3 原理图与软件配置 + +硬件驱动中的宏定义本质上必须与真实硬件连接一致。 + +关系为: + +```text +开发板原理图 + ↓ +确定实际 GPIO + ↓ +修改 BSP / 软件包配置 + ↓ +驱动操作 GPIO + ↓ +控制真实硬件 +``` + +如果软件配置中的 CS、BOOT、INT 或 RESET 引脚与原理图不一致,即使程序能够成功编译,也可能无法正常初始化 WiFi 模块。 + +因此: + +> 原理图是确认硬件引脚映射的重要依据。 + +### 3.4 WiFi 扫描 + +WiFi 模块初始化成功后,可以扫描附近的无线网络。 + +扫描过程本质上是: + +```text +启动扫描 + ↓ +搜索附近 AP + ↓ +获取 SSID + ↓ +获取信号信息 + ↓ +输出扫描结果 +``` + +其中: + +**SSID** 是无线网络名称。 + +### 3.5 WiFi 连接 + +连接无线网络通常需要: + +```text +SSID +Password +``` + +基本过程为: + +```text +选择热点 + ↓ +发送连接请求 + ↓ +完成认证 + ↓ +获取网络参数 + ↓ +加入局域网 +``` + +网络连接成功并不意味着整个互联网通信链路一定正常,因此还需要进一步进行网络测试。 + +### 3.6 Ping 测试 + +`ping` 是网络开发中常用的连通性测试工具。 + +基本原理是通过 ICMP 协议发送请求并等待响应。 + +过程为: + +```text +设备 + ↓ +ICMP Echo Request + ↓ +目标主机 + ↓ +ICMP Echo Reply + ↓ +设备 +``` + +如果能够成功收到响应,说明至少以下部分基本正常: + +```text +WiFi 驱动 +网络协议栈 +IP 配置 +路由 +网络收发 +``` + +------ + +## 4. MQTT 通信 + +MQTT 是一种轻量级的消息传输协议,在物联网设备中应用非常广泛。 + +MQTT 的特点包括: + +- 协议开销较小 +- 支持发布/订阅模式 +- 适合低带宽网络 +- 设备之间耦合度低 +- 易于扩展多个客户端 + +MQTT 通常工作在 TCP 之上。 + +协议层次可以表示为: + +```text +应用程序 + ↓ +MQTT + ↓ +TCP + ↓ +IP + ↓ +WiFi / Ethernet +``` + +### 4.1 MQTT 发布/订阅模型 + +MQTT 与传统的客户端直接通信不同。 + +客户端之间通常并不直接发送数据,而是通过 MQTT Broker 转发。 + +基本结构为: + +```text +Publisher + │ + │ Publish + ↓ +MQTT Broker + │ + │ Push + ↓ +Subscriber +``` + +其中: + +- Publisher:发布者 +- Subscriber:订阅者 +- Broker:消息服务器 + +### 4.2 MQTT Broker + +Broker 是 MQTT 系统的核心服务器。 + +主要负责: + +```text +接收消息 +管理客户端 +管理 Topic +保存订阅关系 +转发消息 +``` + +例如: + +```text +设备 A + ↓ publish + +MQTT Broker + + ↓ message +设备 B +设备 C +设备 D +``` + +发送设备不需要知道具体有哪些设备接收消息。 + +### 4.3 Topic + +MQTT 使用 Topic 对消息进行分类。 + +例如: + +```text +sensor/temperature +sensor/humidity +home/light +device/status +``` + +Publisher 向某个 Topic 发布数据: + +```text +publish + ↓ +sensor/temperature + ↓ +26.5 +``` + +Subscriber 订阅该 Topic: + +```text +subscribe sensor/temperature +``` + +之后 Broker 收到这个主题的新消息时,就会转发给所有订阅该主题的客户端。 + +### 4.4 Publisher + +Publisher 是消息发布方。 + +例如温度传感器节点可以发布: + +```text +Topic: +sensor/temperature + +Payload: +26.5 +``` + +发布过程为: + +```text +生成数据 + ↓ +指定 Topic + ↓ +调用 publish + ↓ +发送到 Broker +``` + +### 4.5 Subscriber + +Subscriber 是消息订阅方。 + +例如: + +```text +subscribe: +sensor/temperature +``` + +订阅成功后: + +```text +Broker 收到新消息 + ↓ +匹配 Topic + ↓ +发送给 Subscriber + ↓ +触发回调函数 +``` + +因此 MQTT 客户端通常需要设置消息回调函数处理接收到的数据。 + +### 4.6 Client ID + +MQTT Broker 需要通过 Client ID 区分不同客户端。 + +结构为: + +```text +Broker + │ + ├── Client A + ├── Client B + └── Client C +``` + +不同设备应尽量使用不同的 Client ID。 + +如果多个设备使用完全相同的 Client ID,部分 Broker 可能认为新的连接替代了旧连接,从而导致客户端互相掉线。 + +### 4.7 MQTT 连接流程 + +一个典型 MQTT 客户端的执行流程为: + +```text +网络准备完成 + ↓ +初始化 MQTT + ↓ +创建客户端 + ↓ +配置 Broker 地址 + ↓ +配置端口 + ↓ +设置 Client ID + ↓ +连接 Broker + ↓ +订阅 Topic + ↓ +发布 / 接收消息 +``` + +MQTT 常见的未加密 TCP 端口为: + +```text +1883 +``` + +基于 TLS 的 MQTT 通常使用其他端口,例如常见的: + +```text +8883 +``` + +### 4.8 回调机制 + +MQTT 接收消息通常采用回调方式。 + +执行逻辑为: + +```text +Broker + ↓ +消息到达 + ↓ +MQTT Client + ↓ +识别 Topic + ↓ +调用 Callback + ↓ +应用程序处理 Payload +``` + +回调机制可以避免应用程序持续主动查询是否有新消息。 + +------ + +## 5. DFS 虚拟文件系统 + +DFS 的全称是: + +```text +Device File System +``` + +它是 RT-Thread 提供的虚拟文件系统组件。 + +DFS 的主要目标是: + +> 为不同类型的文件系统和存储设备提供统一的文件访问接口。 + +例如: + +```text +应用程序 + ↓ +open / read / write / close + ↓ +DFS + ↓ +文件系统 + ↓ +Block Device + ↓ +Flash / SD Card +``` + +应用层不需要直接关心底层究竟是 SD 卡还是 SPI Flash。 + +### 5.1 虚拟文件系统思想 + +不同存储设备之间的底层驱动差异很大。 + +例如: + +```text +SD Card +SPI Flash +NOR Flash +NAND Flash +RAM Disk +``` + +如果应用程序直接针对每种存储介质开发,就需要编写大量不同的代码。 + +VFS 的作用就是在中间增加统一抽象层: + +```text + 应用程序 + ↓ + VFS + ┌────────┼────────┐ + ↓ ↓ ↓ + FATFS LittleFS ... + ↓ ↓ + SD卡 Flash +``` + +因此应用程序可以使用统一接口操作不同文件系统。 + +### 5.2 POSIX 接口 + +DFS 支持常见的 POSIX 风格文件接口,例如: + +```c +open() +read() +write() +close() +``` + +这些接口与 Linux 中的文件操作方式类似。 + +一个典型文件操作流程为: + +```text +open + ↓ +获得文件描述符 + ↓ +read / write + ↓ +close +``` + +这种设计可以提高代码的可移植性。 + +### 5.3 文件描述符 + +调用: + +```c +open() +``` + +成功后通常会返回一个整数。 + +这个整数称为: + +```text +File Descriptor +``` + +简称: + +```text +fd +``` + +例如: + +```text +fd = open(...) +``` + +后续读写时通过 fd 表示已经打开的文件。 + +基本关系为: + +```text +文件路径 + ↓ +open() + ↓ +fd + ↓ +read / write + ↓ +close(fd) +``` + +------ + +## 6. Flash 存储体系 + +在嵌入式设备中,Flash 是常见的非易失性存储介质。 + +所谓非易失性,是指: + +> 断电之后,数据仍然能够保存。 + +常见 Flash 可以分为: + +```text +内部 Flash +外部 Flash +``` + +### 6.1 内部 Flash + +内部 Flash 一般位于 MCU 芯片内部。 + +常用于存放: + +```text +程序固件 +Bootloader +配置数据 +参数 +``` + +### 6.2 外部 Flash + +如果内部 Flash 容量不足,可以通过 SPI 等接口连接外部 Flash。 + +例如: + +```text +W25Q64 +``` + +属于常见的 SPI NOR Flash。 + +整体结构为: + +```text +MCU + ↓ +SPI + ↓ +W25Q64 +``` + +外部 Flash 可以用于: + +```text +文件 +日志 +图片 +字体 +配置 +历史数据 +``` + +------ + +## 7. FAL Flash 抽象层 + +FAL 的全称是: + +```text +Flash Abstraction Layer +``` + +即 Flash 抽象层。 + +它主要用于统一管理不同类型的 Flash 设备。 + +没有抽象层时: + +```text +应用 + ↓ +内部 Flash API +``` + +或: + +```text +应用 + ↓ +W25Q64 API +``` + +不同 Flash 的接口可能完全不同。 + +加入 FAL 后: + +```text +应用层 + ↓ +FAL + ↓ +┌───────────────┐ +│ Internal Flash│ +│ SPI Flash │ +│ Other Flash │ +└───────────────┘ +``` + +应用层就可以通过统一方式访问不同 Flash。 + +### 7.1 Flash 分区 + +FAL 还提供 Flash 分区管理功能。 + +一个完整 Flash 可以划分为多个区域: + +```text +Flash +│ +├── Bootloader +├── Application +├── Download +├── File System +└── User Data +``` + +每个分区都有: + +```text +名称 +起始地址 +长度 +所属 Flash 设备 +``` + +通过分区机制可以避免不同功能的数据互相覆盖。 + +------ + +## 8. SFUD + +SFUD 的全称是: + +```text +Serial Flash Universal Driver +``` + +即串行 Flash 通用驱动。 + +它主要用于解决不同 SPI Flash 芯片之间的兼容问题。 + +不同厂商的 SPI Flash: + +```text +W25Qxx +MX25Lxx +GD25Qxx +... +``` + +在: + +- 容量 +- JEDEC ID +- 擦除方式 +- 指令 +- 特性 + +等方面可能存在差异。 + +SFUD 在中间提供统一驱动层: + +```text +上层程序 + ↓ +SFUD + ↓ +SPI Flash +``` + +这样应用层无需针对每一种 Flash 芯片重新编写完整驱动。 + +### 8.1 FAL 与 SFUD 的关系 + +FAL 与 SFUD 的作用不同。 + +可以简单理解为: + +```text +FAL + ↓ +负责 Flash 抽象与分区 + +SFUD + ↓ +负责具体 SPI Flash 驱动 +``` + +组合后的结构可以表示为: + +```text +应用程序 + ↓ +文件系统 + ↓ +FAL + ↓ +SFUD + ↓ +SPI + ↓ +W25Q64 +``` + +------ + +## 9. DFS 与 Flash 的关系 + +DFS 本身不是 Flash 驱动。 + +DFS 主要负责: + +```text +文件访问统一接口 +``` + +而 FAL、SFUD 等组件主要负责: + +```text +Flash 设备访问 +``` + +整体软件层次可以表示为: + +```text +应用程序 + ↓ +open / write / close + ↓ +DFS + ↓ +具体文件系统 + ↓ +块设备 / MTD 设备 + ↓ +FAL + ↓ +SFUD + ↓ +SPI + ↓ +W25Q64 +``` + +因此: + +- DFS 解决“如何以文件形式访问” +- FAL 解决“如何统一管理 Flash” +- SFUD 解决“如何驱动不同 SPI Flash” + +------ + +## 10. 文件系统挂载 + +文件系统在使用之前通常需要挂载。 + +挂载可以理解为: + +> 将一个实际存储设备上的文件系统映射到系统目录树中的某个路径。 + +例如: + +```text +W25Q64 + ↓ +文件系统 + ↓ +挂载 + ↓ +/flash +``` + +之后应用程序就可以通过: + +```text +/flash/test.txt +``` + +访问 Flash 中的文件。 + +整体过程为: + +```text +Flash 初始化 + ↓ +注册 Flash 设备 + ↓ +初始化文件系统 + ↓ +mount + ↓ +建立挂载点 + ↓ +应用程序访问文件 +``` + +### 10.1 文件写入流程 + +使用 POSIX 接口进行文件写入时,整体过程为: + +```text +open() + ↓ +打开或创建文件 + ↓ +write() + ↓ +写入数据 + ↓ +close() + ↓ +关闭文件 +``` + +应用层使用标准接口后,不需要关心底层 Flash 的页编程和扇区擦除等细节。 + +------ + +## 11. SPI 总线共享 + +在嵌入式系统中,一条 SPI 总线可以同时连接多个从设备。 + +例如: + +```text + ┌── AW007 WiFi +MCU ─── SPI ────┤ + └── W25Q64 Flash +``` + +通常: + +```text +SCK 共享 +MOSI 共享 +MISO 共享 +CS 独立 +``` + +通过 CS 信号决定当前与哪个设备进行通信。 + +### 11.1 CS 片选信号 + +CS 的作用是选择 SPI 从设备。 + +例如: + +```text +CS_WIFI = 0 +CS_FLASH = 1 +``` + +表示当前选择 WiFi。 + +反之: + +```text +CS_WIFI = 1 +CS_FLASH = 0 +``` + +表示当前选择 Flash。 + +大多数 SPI 设备的 CS 为低电平有效。 + +### 11.2 SPI 总线冲突 + +如果多个 SPI 从设备同时被选中,就可能产生总线冲突。 + +例如: + +```text +AW007 CS = 0 +W25Q64 CS = 0 +``` + +这时两个设备可能同时驱动 MISO 信号,导致: + +```text +数据异常 +通信失败 +Flash 读取错误 +WiFi 工作异常 +``` + +因此需要保证: + +> 任意时刻只有需要通信的 SPI 从设备处于选中状态。 + +### 11.3 软件层协调 + +在使用某个 SPI 设备前,可以先保证其他设备的 CS 处于非选中状态。 + +例如: + +```text +访问 Flash 前 + ↓ +拉高 WiFi CS + ↓ +拉低 Flash CS + ↓ +执行 Flash 通信 + ↓ +释放 Flash CS +``` + +这也是多设备共享 SPI 总线时非常重要的基本原则。 + +------ + +## 12. Day5 知识体系总结 + +Day5 的主要内容可以按照“传感器、网络、通信协议、文件系统、Flash”几个部分进行理解。 + +整体软件结构如下: + +```text + RT-Thread 应用层 + │ + ┌──────────────┼──────────────┐ + │ │ │ + ↓ ↓ ↓ + HT10 Sensor MQTT DFS + │ │ │ + ↓ ↓ ↓ + Sensor Framework TCP/IP 文件系统 + │ │ │ + ↓ ↓ ↓ + I2C AW007 WiFi FAL + │ │ + ↓ ↓ + SPI SFUD + │ + ↓ + SPI + │ + ↓ + W25Q64 +``` + +各组件的主要作用如下: + +| 组件 | 主要作用 | +| ---------------- | ----------------------------------- | +| RT-Thread 软件包 | 管理和复用第三方功能组件 | +| env | 软件包管理及配置环境 | +| Kconfig | 描述功能选项和依赖关系 | +| I2C | HT10 与 MCU 之间的数据通信 | +| Sensor Framework | 提供统一传感器访问框架 | +| AW007 | 为设备提供 WiFi 网络连接能力 | +| SPI | MCU 与 WiFi、Flash 等设备通信 | +| MQTT | 实现基于 Topic 的发布/订阅通信 | +| Broker | MQTT 消息接收和转发中心 | +| DFS | 提供统一的文件系统访问接口 | +| POSIX | 提供 `open/read/write/close` 等接口 | +| FAL | Flash 抽象和分区管理 | +| SFUD | 通用 SPI Flash 驱动 | +| W25Q64 | 外部非易失性存储设备 | + +Day5 的核心知识链路可以总结为: + +```text +RT-Thread 软件包 + ↓ +快速集成外设和组件 + ↓ +┌─────────────────────────────────┐ +│ │ +HT10 AW007 +│ │ +I2C SPI +│ │ +Sensor Framework WiFi + │ + ↓ + TCP/IP + │ + ↓ + MQTT +``` + +存储部分则为: + +```text +应用程序 + ↓ +POSIX API + ↓ +DFS + ↓ +文件系统 + ↓ +FAL + ↓ +SFUD + ↓ +W25Q64 Flash +``` + +通过 Day5 的学习,可以进一步理解 RT-Thread 并不仅仅是一个实时操作系统内核,还提供了从**设备驱动、软件包管理、网络通信到文件系统和存储管理**的一整套嵌入式软件开发框架。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..26a896057f38d75e89536de58eaeba2e035b3a8c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1214 @@ +# Day4 + +## PIN 设备驱动框架 + +第四天主要学习 RT-Thread 的 **PIN 设备驱动框架**,通过分析 PIN 设备从应用层到底层 STM32 GPIO 的调用过程,理解 RT-Thread 设备驱动的分层设计、设备注册机制以及 `ops` 函数指针的作用。 + +RT-Thread 中,应用程序通常不会直接操作 STM32 的 GPIO 寄存器,而是通过统一的 PIN 设备接口完成 GPIO 配置和控制。 + +基本关系可以理解为: + +```text +应用程序 + | + | rt_pin_mode() + | rt_pin_write() + | rt_pin_read() + v +PIN 设备框架 + | + | rt_pin_ops + v +BSP GPIO 驱动 + | + | STM32 HAL + v +GPIO 硬件 +``` + +这种设计将应用代码与具体芯片隔离。 + +当底层 MCU 从 STM32 更换为其他芯片时,只需要重新实现对应的底层 PIN 驱动,上层调用方式通常不需要改变。 + +------ + +## RT-Thread 设备驱动框架 + +RT-Thread 提供了统一的 I/O 设备管理框架。 + +设备驱动完成注册后,可以通过设备名称查找,并使用统一的设备接口进行访问。 + +常见通用设备接口包括: + +```c +rt_device_find(); +rt_device_open(); +rt_device_close(); +rt_device_read(); +rt_device_write(); +rt_device_control(); +``` + +其中: + +```c +rt_device_find("pin"); +``` + +可以用于查找已经注册的 PIN 设备。 + +PIN 设备是在 RT-Thread 通用设备框架基础上增加的一层针对 GPIO 操作的封装。 + +因此既具备通用 `rt_device` 的设备管理能力,又提供: + +```c +rt_pin_mode(); +rt_pin_write(); +rt_pin_read(); +rt_pin_attach_irq(); +rt_pin_irq_enable(); +``` + +等更加符合 GPIO 使用习惯的接口。 + +------ + +## PIN 设备结构 + +PIN 设备需要保存两个重要部分: + +```c +struct rt_device_pin +{ + struct rt_device parent; + const struct rt_pin_ops *ops; +}; +``` + +其中: + +### parent + +```c +struct rt_device parent; +``` + +表示 PIN 设备继承 RT-Thread 的通用设备结构。 + +通过这一部分,PIN 设备可以加入 RT-Thread 的设备管理系统。 + +例如: + +```c +rt_device_register(); +``` + +注册完成后,可以通过: + +```c +rt_device_find("pin"); +``` + +查找到该设备。 + +### ops + +```c +const struct rt_pin_ops *ops; +``` + +用于保存 PIN 设备对应的硬件操作函数。 + +可以把 `ops` 理解成一张: + +> GPIO 操作函数表。 + +框架层只规定需要哪些操作,而真正操作 STM32 GPIO 的代码由 BSP 层实现。 + +------ + +## rt_pin_ops + +PIN 驱动中非常重要的数据结构是: + +```c +struct rt_pin_ops +{ + void (*pin_mode)(struct rt_device *device, + rt_base_t pin, + rt_base_t mode); + + void (*pin_write)(struct rt_device *device, + rt_base_t pin, + rt_base_t value); + + int (*pin_read)(struct rt_device *device, + rt_base_t pin); + + rt_err_t (*pin_attach_irq)(struct rt_device *device, + rt_int32_t pin, + rt_uint32_t mode, + void (*hdr)(void *args), + void *args); + + rt_err_t (*pin_detach_irq)(struct rt_device *device, + rt_int32_t pin); + + rt_err_t (*pin_irq_enable)(struct rt_device *device, + rt_base_t pin, + rt_uint32_t enabled); + + rt_base_t (*pin_get)(const char *name); +}; +``` + +这些成员并不是普通变量,而是**函数指针**。 + +| 接口 | 作用 | +| ---------------- | ------------------------- | +| `pin_mode` | 设置 GPIO 工作模式 | +| `pin_write` | 设置 GPIO 输出电平 | +| `pin_read` | 读取 GPIO 输入电平 | +| `pin_attach_irq` | 注册 GPIO 中断回调 | +| `pin_detach_irq` | 解除 GPIO 中断回调 | +| `pin_irq_enable` | 使能或关闭 GPIO 中断 | +| `pin_get` | 将 PIN 名称转换为内部编号 | + +这里可以看出: + +```text +PIN 框架负责定义接口 + + ↓ + +BSP 驱动负责实现接口 +``` + +这也是 RT-Thread 驱动框架中非常重要的设计思想。 + +------ + +## GPIO 工作模式 + +RT-Thread 将常见 GPIO 模式进行了统一定义。 + +例如: + +```c +#define PIN_LOW 0x00 +#define PIN_HIGH 0x01 +``` + +表示: + +```text +PIN_LOW -> 低电平 +PIN_HIGH -> 高电平 +``` + +常见 GPIO 模式: + +```c +#define PIN_MODE_OUTPUT 0x00 +#define PIN_MODE_INPUT 0x01 +#define PIN_MODE_INPUT_PULLUP 0x02 +#define PIN_MODE_INPUT_PULLDOWN 0x03 +#define PIN_MODE_OUTPUT_OD 0x04 +``` + +对应关系: + +| 宏 | 作用 | +| ------------------------- | -------- | +| `PIN_MODE_OUTPUT` | 普通输出 | +| `PIN_MODE_INPUT` | 输入 | +| `PIN_MODE_INPUT_PULLUP` | 上拉输入 | +| `PIN_MODE_INPUT_PULLDOWN` | 下拉输入 | +| `PIN_MODE_OUTPUT_OD` | 开漏输出 | + +因此应用程序不需要直接使用 STM32 HAL 中的: + +```c +GPIO_MODE_OUTPUT_PP +GPIO_MODE_INPUT +GPIO_PULLUP +GPIO_PULLDOWN +``` + +而是可以统一使用 RT-Thread 的 PIN API。 + +例如: + +```c +rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT); +``` + +------ + +## PIN 中断模式 + +RT-Thread 同样对 GPIO 外部中断进行了统一封装。 + +常见模式包括: + +```c +PIN_IRQ_MODE_RISING +PIN_IRQ_MODE_FALLING +PIN_IRQ_MODE_RISING_FALLING +PIN_IRQ_MODE_HIGH_LEVEL +PIN_IRQ_MODE_LOW_LEVEL +``` + +例如: + +```c +rt_pin_attach_irq(KEY_PIN, + PIN_IRQ_MODE_FALLING, + key_callback, + RT_NULL); +``` + +表示: + +```text +KEY_PIN + ↓ +下降沿 + ↓ +发生 GPIO 中断 + ↓ +执行 key_callback() +``` + +完成回调绑定后,还需要: + +```c +rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE); +``` + +才能真正使能中断。 + +------ + +## BSP 层的 PIN 实现 + +以 STM32 BSP 为例,底层 GPIO 驱动一般位于: + +```text +drv_gpio.c +``` + +BSP 层会实现一组 STM32 GPIO 操作函数,例如: + +```c +stm32_pin_mode() +stm32_pin_write() +stm32_pin_read() +stm32_pin_attach_irq() +stm32_pin_detach_irq() +stm32_pin_irq_enable() +stm32_pin_get() +``` + +然后将这些函数放入一个 `rt_pin_ops` 结构体。 + +例如: + +```c +static const struct rt_pin_ops _stm32_pin_ops = +{ + stm32_pin_mode, + stm32_pin_write, + stm32_pin_read, + stm32_pin_attach_irq, + stm32_pin_detach_irq, + stm32_pin_irq_enable, + stm32_pin_get, +}; +``` + +这里完成的是: + +```text +RT-Thread 规定接口 + ↓ +STM32 实现接口 +``` + +例如: + +```c +pin_write +``` + +最后对应: + +```c +stm32_pin_write +``` + +------ + +## PIN 设备注册 + +实现 `_stm32_pin_ops` 后,还需要把它注册到 RT-Thread PIN 框架中。 + +STM32 BSP 初始化时会调用类似: + +```c +rt_device_pin_register("pin", + &_stm32_pin_ops, + RT_NULL); +``` + +其中: + +```text +"pin" +``` + +是设备名称。 + +```c +&_stm32_pin_ops +``` + +是 STM32 GPIO 对 PIN 接口的具体实现。 + +注册过程可以理解为: + +```text +STM32 GPIO 驱动 + | + | _stm32_pin_ops + v +rt_device_pin_register() + | + v +PIN 设备框架 + | + v +RT-Thread 设备管理器 +``` + +注册成功后,系统中就存在一个名为: + +```text +pin +``` + +的设备。 + +------ + +## rt_device_pin_register 的作用 + +`rt_device_pin_register()` 是连接 PIN 框架与具体 BSP 驱动的重要函数。 + +主要完成两部分工作。 + +第一部分是保存 BSP 提供的: + +```c +ops +``` + +例如: + +```c +_hw_pin.ops = ops; +``` + +这样框架层以后执行: + +```c +_hw_pin.ops->pin_write(...) +``` + +时,就能够跳转到 STM32 的: + +```c +stm32_pin_write(...) +``` + +第二部分是将 PIN 设备加入 RT-Thread 设备管理系统: + +```c +rt_device_register(); +``` + +因此: + +```text +rt_device_pin_register() +``` + +可以理解为: + +```text +保存 GPIO 操作方法 + + +注册 PIN 设备 +``` + +------ + +## rt_pin_write 调用过程 + +学习 PIN 框架时,最重要的是理解应用程序中的一条简单代码最终是如何控制硬件的。 + +例如: + +```c +rt_pin_write(LED_PIN, PIN_HIGH); +``` + +应用层看到的只是这一行代码。 + +但内部实际经历多层调用。 + +### 第一层:应用 API + +应用调用: + +```c +rt_pin_write(pin, value); +``` + +PIN 框架内部会通过: + +```c +_hw_pin.ops->pin_write(...) +``` + +调用真正的 GPIO 实现。 + +此时 `_hw_pin.ops` 保存的是: + +```c +&_stm32_pin_ops +``` + +因此: + +```c +_hw_pin.ops->pin_write +``` + +实际指向: + +```c +stm32_pin_write +``` + +------ + +## STM32 GPIO 写操作 + +进入 BSP 后执行类似: + +```c +static void stm32_pin_write(rt_device_t dev, + rt_base_t pin, + rt_base_t value) +{ + GPIO_TypeDef *gpio_port; + uint16_t gpio_pin; + + gpio_port = PIN_STPORT(pin); + gpio_pin = PIN_STPIN(pin); + + HAL_GPIO_WritePin(gpio_port, + gpio_pin, + (GPIO_PinState)value); +} +``` + +这里主要完成两件事情: + +```text +PIN 编号 + ↓ +解析 GPIO 端口 + +PIN 编号 + ↓ +解析 GPIO 引脚号 +``` + +然后调用 STM32 HAL: + +```c +HAL_GPIO_WritePin(); +``` + +最终 HAL 操作 STM32 GPIO 寄存器。 + +因此完整过程可以表示为: + +```text +rt_pin_write() + ↓ +_hw_pin.ops->pin_write() + ↓ +stm32_pin_write() + ↓ +HAL_GPIO_WritePin() + ↓ +GPIO 寄存器 + ↓ +引脚电平发生变化 +``` + +这就是从 RT-Thread 应用 API 到实际 GPIO 硬件之间的完整调用链。 + +------ + +## GET_PIN + +在 STM32 BSP 中,经常使用: + +```c +GET_PIN(port, pin) +``` + +获得 RT-Thread 使用的 PIN 编号。 + +例如: + +```c +#define LED_PIN GET_PIN(F, 12) +``` + +表示: + +```text +GPIOF +PIN 12 +``` + +之前在线程实验中已经使用过: + +```c +GET_PIN(F, 12) +``` + +同时需要注意 BSP 中宏定义所在的头文件。 + +例如 STM32 BSP 中常见: + +```c +#include "drv_common.h" +``` + +否则可能出现: + +```text +implicit declaration of function 'GET_PIN' +``` + +或者: + +```text +'F' undeclared +``` + +之类的编译错误。 + +------ + +## PIN 编码思想 + +RT-Thread BSP 并不会直接把: + +```text +GPIOC +GPIO_PIN_5 +``` + +作为应用层的 PIN 参数传递。 + +通常会将: + +```text +端口号 + 引脚号 +``` + +编码成一个整数。 + +例如一种常见编码方式: + +```c +#define PIN_NUM(port, no) \ + ((((port) & 0x0F) << 4) | ((no) & 0x0F)) +``` + +可以理解为: + +```text +高 4 bit:GPIO 端口 +低 4 bit:GPIO 引脚号 +``` + +例如: + +```c +GET_PIN(C, 5) +``` + +其中: + +```text +A = 0 +B = 1 +C = 2 +``` + +因此: + +```text +port = 2 +pin = 5 +``` + +编码: + +```text +(2 << 4) | 5 +``` + +得到: + +```text +0x25 +``` + +十进制为: + +```text +37 +``` + +所以应用程序中的一个 PIN,本质上可以使用一个整数保存。 + +------ + +## PIN 编号解码 + +BSP 在真正访问 GPIO 时,需要重新解析这个 PIN 编号。 + +通常会提供类似: + +```c +PIN_PORT(pin) +``` + +获得 GPIO 端口编号。 + +以及: + +```c +PIN_NO(pin) +``` + +获得 GPIO 引脚号。 + +例如: + +```text +pin = 0x25 +``` + +解析后: + +```text +PIN_PORT(0x25) = 2 +PIN_NO(0x25) = 5 +``` + +即: + +```text +GPIOC PIN5 +``` + +------ + +## GPIO 端口地址 + +STM32 不同 GPIO 端口在内存中的寄存器地址通常具有固定规律。 + +因此 BSP 可以根据: + +```c +PIN_PORT(pin) +``` + +计算出对应 GPIO 控制器地址。 + +例如概念上: + +```text +GPIOA +GPIOB +GPIOC +GPIOD +... +``` + +按照固定地址间隔排列。 + +因此: + +```c +PIN_STPORT(pin) +``` + +可以根据端口编号得到: + +```c +GPIO_TypeDef * +``` + +之后即可交给 STM32 HAL 使用。 + +------ + +## GPIO 位掩码 + +STM32 HAL 使用: + +```c +GPIO_PIN_0 +GPIO_PIN_1 +GPIO_PIN_2 +... +``` + +这样的位掩码表示 PIN。 + +因此 BSP 还需要把引脚编号转换为: + +```c +1 << pin_no +``` + +例如: + +```text +PIN5 +``` + +对应: + +```text +0000 0000 0010 0000 +``` + +即: + +```text +0x0020 +``` + +这一步完成 RT-Thread PIN 编号与 STM32 HAL GPIO PIN 参数之间的转换。 + +------ + +## PIN 中断 + +PIN 驱动不仅可以控制 GPIO 输入输出,还可以处理 GPIO 外部中断。 + +首先注册中断回调: + +```c +rt_pin_attach_irq(pin, + mode, + callback, + args); +``` + +然后: + +```c +rt_pin_irq_enable(pin, PIN_IRQ_ENABLE); +``` + +使能对应中断。 + +整个过程可以理解为: + +```text +应用程序 + | + | rt_pin_attach_irq() + v +PIN 框架 + | + | ops->pin_attach_irq() + v +STM32 GPIO BSP + | + | 配置 EXTI + | 保存 callback + v +NVIC / EXTI +``` + +当 GPIO 中断发生后: + +```text +GPIO 电平变化 + ↓ +EXTI 产生中断 + ↓ +进入 IRQHandler + ↓ +HAL GPIO EXTI 处理 + ↓ +查找已经注册的回调函数 + ↓ +执行用户 callback +``` + +因此应用程序只需要关心: + +```c +callback() +``` + +而不需要直接处理 STM32 EXTI 寄存器。 + +------ + +## 为什么使用 ops + +PIN 驱动框架中的核心是: + +```c +struct rt_pin_ops +``` + +这种设计本质上是通过**函数指针实现接口与实现分离**。 + +如果没有 `ops`,可能会变成: + +```c +void rt_pin_write(...) +{ + stm32_pin_write(...); +} +``` + +这样 PIN 框架直接依赖 STM32。 + +如果以后更换为: + +```text +GD32 +NXP +CH32 +``` + +框架代码本身也需要修改。 + +使用 `ops` 后: + +```c +_hw_pin.ops->pin_write(...) +``` + +框架根本不需要知道下面是什么芯片。 + +STM32 可以注册: + +```text +_stm32_pin_ops +``` + +其他 MCU 可以注册: + +```text +_gd32_pin_ops +``` + +或者其他实现。 + +应用程序仍然调用: + +```c +rt_pin_write(); +``` + +因此获得了较好的可移植性。 + +------ + +## ops 与面向对象思想 + +虽然 RT-Thread 主要使用 C 语言开发,但这种写法与面向对象中的: + +```text +接口 ++ +接口实现 +``` + +非常相似。 + +可以简单理解为: + +```text +struct rt_pin_ops + ↓ +接口定义 + +_stm32_pin_ops + ↓ +STM32 对接口的实现 +``` + +而: + +```c +_hw_pin.ops->pin_write(); +``` + +类似于通过接口调用具体实现。 + +因此函数指针表是嵌入式 C 中实现驱动抽象的重要方法。 + +------ + +## PIN 专用 API 与通用设备 API + +PIN 设备实际上存在两种访问思路。 + +### PIN 专用 API + +例如: + +```c +rt_pin_mode(pin, PIN_MODE_OUTPUT); +rt_pin_write(pin, PIN_HIGH); +rt_pin_read(pin); +``` + +这种方式最常用。 + +优点是: + +```text +简单 +直观 +符合 GPIO 操作习惯 +``` + +### rt_device 通用 API + +PIN 设备注册进入设备框架后,也可以通过: + +```c +rt_device_find("pin"); +``` + +找到设备。 + +再使用通用: + +```c +rt_device_read(); +rt_device_write(); +rt_device_control(); +``` + +访问。 + +不过通用设备 API 的参数形式需要适配: + +```text +buffer +size +pos +``` + +GPIO 操作使用起来并不直观。 + +因此 RT-Thread 又在通用设备框架之上提供了: + +```c +rt_pin_write() +rt_pin_read() +rt_pin_mode() +``` + +这些语义化 API。 + +日常 GPIO 开发一般优先使用 PIN 专用 API。 + +------ + +## PIN 驱动框架整体流程 + +整个 PIN 驱动框架可以整理为: + +```text + 系统启动 + | + v + STM32 BSP 初始化 + | + v + 创建 _stm32_pin_ops + | + v + rt_device_pin_register() + | + +---------+---------+ + | | + v v + 保存 PIN ops 注册 "pin" 设备 + | + v +----------------------------------------- + 应用运行阶段 +----------------------------------------- + | + v +rt_pin_mode / write / read + | + v + _hw_pin.ops + | + v +stm32_pin_mode/write/read + | + v + STM32 HAL + | + v + GPIO 寄存器 + | + v + 硬件 PIN +``` + +------ + +## 学习总结 + +通过 PIN 驱动框架源码分析,可以看到 RT-Thread 驱动设计中的几个核心思想。 + +### 1. 分层 + +整体结构为: + +```text +应用层 + ↓ +设备驱动框架 + ↓ +BSP + ↓ +HAL + ↓ +硬件 +``` + +每一层只负责自己的工作。 + +### 2. 设备抽象 + +不同芯片的 GPIO 操作最终都可以统一成: + +```c +rt_pin_mode(); +rt_pin_write(); +rt_pin_read(); +``` + +应用程序不需要直接依赖 STM32 HAL。 + +### 3. ops 函数指针 + +通过: + +```c +struct rt_pin_ops +``` + +将: + +```text +接口定义 +``` + +与: + +```text +硬件实现 +``` + +分离。 + +这是 PIN 框架最值得理解的部分。 + +### 4. 注册机制 + +BSP 实现完成后,并不是应用直接调用 BSP 函数,而是通过: + +```c +rt_device_pin_register(); +``` + +把实现注册到 PIN 框架。 + +### 5. 可移植性 + +应用程序只依赖: + +```text +RT-Thread PIN API +``` + +而不直接依赖: + +```text +STM32 GPIO +``` + +因此更换 MCU 后,上层代码能够尽可能保持不变。 + +------ + +## 官方资料 + +本部分学习主要参考 RT-Thread 官方资料: + +- RT-Thread 官方 PIN API 参考手册 +- RT-Thread 官方设备子系统 API +- RT-Thread 官方 GitHub/Gitee 源码 +- PIN 设备框架源码 +- STM32 BSP `drv_gpio.c` +- RT-Thread `rt_device` 设备管理源码 + +阅读源码时重点关注以下函数: + +```c +rt_device_pin_register() +rt_pin_mode() +rt_pin_write() +rt_pin_read() +rt_pin_attach_irq() +rt_pin_irq_enable() +rt_device_register() +rt_device_find() +``` + +以及以下数据结构: + +```c +struct rt_device +struct rt_device_pin +struct rt_pin_ops +``` \ No newline at end of file