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\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\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