diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex1_parking.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex1_parking.c" new file mode 100644 index 0000000000000000000000000000000000000000..37b08647c34e8ee2d2a922423cb496ce633b236c --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex1_parking.c" @@ -0,0 +1,60 @@ +/* + * 练习一:停车场车位管理(计数信号量) + * 与 ex2/ex3/ex4 互斥,applications/ 下同一时刻只留一个。 + */ +#include + +#define PARK_TOTAL 3 +#define CAR_NUM 5 +#define CAR_STAY_TICK rt_tick_from_millisecond(3000) +#define CAR_WAIT_TICK rt_tick_from_millisecond(2000) + +static rt_sem_t park_sem = RT_NULL; + +static void car_entry(void *param) +{ + rt_uint32_t car_no = (rt_uint32_t)param; + rt_uint32_t value; + + while (1) + { + if (rt_sem_take(park_sem, CAR_WAIT_TICK) == RT_EOK) + { + rt_sem_getvalue(park_sem, &value); + rt_kprintf("car%u entered, used spaces: %u\n", car_no, PARK_TOTAL - value); + rt_thread_mdelay(CAR_STAY_TICK); + rt_kprintf("car%u left\n", car_no); + rt_sem_release(park_sem); + break; + } + else + { + rt_kprintf("car%u waiting for a space\n", car_no); + rt_thread_mdelay(rt_tick_from_millisecond(500)); + } + } +} + +static int parking_init(void) +{ + rt_thread_t tid; + rt_uint32_t i; + char name[8]; + + park_sem = rt_sem_create("park", PARK_TOTAL, RT_IPC_FLAG_FIFO); + if (park_sem == RT_NULL) + { + rt_kprintf("park sem create failed\n"); + return -RT_ENOMEM; + } + + for (i = 1; i <= CAR_NUM; i++) + { + rt_snprintf(name, sizeof(name), "car%u", i); + tid = rt_thread_create(name, car_entry, (void *)i, 1024, 20, 5); + if (tid) rt_thread_startup(tid); + else rt_kprintf("%s create failed\n", name); + } + return RT_EOK; +} +INIT_APP_EXPORT(parking_init); diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex2_tickets.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex2_tickets.c" new file mode 100644 index 0000000000000000000000000000000000000000..ef11b379a97a681329c55513691c1cd73b0d3b34 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex2_tickets.c" @@ -0,0 +1,66 @@ +/* + * 练习二:多窗口售票系统(互斥量) + * 与 ex1/ex3/ex4 互斥,applications/ 下同一时刻只留一个。 + * 对比实验:把 USE_MUTEX 改为 0,观察无保护时的竞争。 + */ +#include + +#define USE_MUTEX 1 /* 对比实验:改为 0,观察无保护时的竞争 */ + +static rt_int32_t ticket_count = 20; +static rt_mutex_t ticket_mutex = RT_NULL; + +static void window_entry(void *param) +{ + const char *name = (const char *)param; + rt_int32_t sold; + rt_bool_t last; + + while (1) + { +#if USE_MUTEX + rt_mutex_take(ticket_mutex, RT_WAITING_FOREVER); +#endif + if (ticket_count > 0) + { + sold = ticket_count; +#if !USE_MUTEX + rt_thread_mdelay(10); /* 读票之后、减票之前留出竞争窗口 */ +#endif + ticket_count--; + last = (ticket_count == 0); +#if USE_MUTEX + rt_mutex_release(ticket_mutex); +#endif + rt_kprintf("window %s sold ticket %d\n", name, sold); + if (last) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: %d\n", ticket_count); + } + rt_thread_mdelay(50); + } + else + { +#if USE_MUTEX + rt_mutex_release(ticket_mutex); +#endif + break; + } + } +} + +static int tickets_init(void) +{ + rt_thread_t ta, tb; + + ticket_mutex = rt_mutex_create("ticket", RT_IPC_FLAG_FIFO); + if (ticket_mutex == RT_NULL) return -RT_ENOMEM; + + ta = rt_thread_create("winA", window_entry, (void *)"A", 1024, 15, 5); + tb = rt_thread_create("winB", window_entry, (void *)"B", 1024, 15, 5); + if (ta) rt_thread_startup(ta); + if (tb) rt_thread_startup(tb); + return RT_EOK; +} +INIT_APP_EXPORT(tickets_init); diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex3_startup.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex3_startup.c" new file mode 100644 index 0000000000000000000000000000000000000000..4526c7d741d458752e199775f5948f3c3df7e8d0 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex3_startup.c" @@ -0,0 +1,83 @@ +/* + * 练习三:系统启动条件检查(事件集) + * 与 ex1/ex2/ex4 互斥,applications/ 下同一时刻只留一个。 + */ +#include + +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) +#define EVENT_ALL (EVENT_NET_READY | EVENT_SENSOR_READY | EVENT_STORAGE_READY) + +static rt_event_t init_event = RT_NULL; +static volatile rt_uint32_t g_ready = 0; /* 记录已就绪模块,供超时打印用 */ + +static void mark_ready(rt_uint32_t flag) +{ + rt_base_t level = rt_hw_interrupt_disable(); + g_ready |= flag; + rt_hw_interrupt_enable(level); +} + +static void net_entry(void *p) +{ + rt_thread_mdelay(1000); + rt_event_send(init_event, EVENT_NET_READY); + mark_ready(EVENT_NET_READY); + rt_kprintf("network ready\n"); +} + +static void sensor_entry(void *p) +{ + rt_thread_mdelay(1500); + rt_event_send(init_event, EVENT_SENSOR_READY); + mark_ready(EVENT_SENSOR_READY); + rt_kprintf("sensor ready\n"); +} + +static void storage_entry(void *p) +{ + rt_thread_mdelay(5000); /* 超时测试:故意设长,让业务先超时 */ + rt_event_send(init_event, EVENT_STORAGE_READY); + mark_ready(EVENT_STORAGE_READY); + rt_kprintf("storage ready\n"); +} + +static void business_entry(void *p) +{ + rt_uint32_t recved; + rt_err_t err; + + /* 先等 3 秒(AND 模式,三个事件都到才算数) */ + err = rt_event_recv(init_event, EVENT_ALL, + RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, + rt_tick_from_millisecond(3000), &recved); + if (err != RT_EOK) + { + if (!(g_ready & EVENT_NET_READY)) rt_kprintf("wait modules timeout, network is not ready\n"); + if (!(g_ready & EVENT_SENSOR_READY)) rt_kprintf("wait modules timeout, sensor is not ready\n"); + if (!(g_ready & EVENT_STORAGE_READY)) rt_kprintf("wait modules timeout, storage is not ready\n"); + /* storage 还在初始化,继续等到它完成 */ + rt_event_recv(init_event, EVENT_ALL, + RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, &recved); + } + + rt_kprintf("all modules are ready\n"); + rt_kprintf("business task started\n"); +} + +static int startup_init(void) +{ + rt_thread_t t; + + init_event = rt_event_create("init", RT_IPC_FLAG_FIFO); + if (init_event == RT_NULL) return -RT_ENOMEM; + + t = rt_thread_create("net", net_entry, RT_NULL, 1024, 10, 5); if (t) rt_thread_startup(t); + t = rt_thread_create("sensor", sensor_entry, RT_NULL, 1024, 10, 5); if (t) rt_thread_startup(t); + t = rt_thread_create("storage", storage_entry, RT_NULL, 1024, 10, 5); if (t) rt_thread_startup(t); + t = rt_thread_create("business", business_entry, RT_NULL, 1024, 8, 5); if (t) rt_thread_startup(t); + return RT_EOK; +} +INIT_APP_EXPORT(startup_init); diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex4_parcel.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex4_parcel.c" new file mode 100644 index 0000000000000000000000000000000000000000..e5bdbb121da07933eff09526b62996c51dee1938 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ex4_parcel.c" @@ -0,0 +1,114 @@ +/* + * 练习四:快递分拣中心(消息队列) + * 与 ex1/ex2/ex3 互斥,applications/ 下同一时刻只留一个。 + */ +#include + +#define MQ_CAPACITY 5 +#define SENDER_NUM 2 +#define PKG_PER_SENDER 5 +#define REGION_END 0 /* 结束消息:region=0 表示某发件线程已发完 */ + +struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; /* 1 华东, 2 华南, 3 华北 */ + rt_uint16_t weight; +}; + +static rt_mq_t pkg_mq = RT_NULL; +static rt_mutex_t sf_mutex = RT_NULL; /* 保护 send_failed 计数 */ +static rt_uint32_t send_failed = 0; + +static const char *region_str(rt_uint8_t r) +{ + switch (r) + { + case 1: return "east"; + case 2: return "south"; + case 3: return "north"; + default: return "?"; + } +} + +static void sender_entry(void *param) +{ + rt_uint32_t sender = (rt_uint32_t)param; + rt_uint32_t base = (sender == 1) ? 1000 : 2000; + struct package_msg pkg; + rt_uint32_t i; + + for (i = 1; i <= PKG_PER_SENDER; i++) + { + pkg.id = base + i; + pkg.region = (rt_uint8_t)((i % 3) + 1); /* 简单分布 1/2/3 */ + pkg.weight = (rt_uint16_t)(800 + (sender * 100) + i * 37); /* 任意重量 */ + + if (rt_mq_send(pkg_mq, &pkg, sizeof(pkg)) != RT_EOK) + { + /* 队列满,记一次失败,原地重试本条(i-- 保证同一条只成功发一次) */ + rt_mutex_take(sf_mutex, RT_WAITING_FOREVER); + send_failed++; + rt_mutex_release(sf_mutex); + i--; + rt_thread_mdelay(100); + } + } + + /* 本线程所有快递发完,再发一条结束消息 */ + pkg.id = 0; pkg.region = REGION_END; pkg.weight = 0; + while (rt_mq_send(pkg_mq, &pkg, sizeof(pkg)) != RT_EOK) + rt_thread_mdelay(100); +} + +static void sorter_entry(void *param) +{ + struct package_msg pkg; + rt_uint32_t total = 0, east = 0, south = 0, north = 0, weight = 0; + rt_uint32_t end_count = 0; + + while (1) + { + if (rt_mq_recv(pkg_mq, &pkg, sizeof(pkg), RT_WAITING_FOREVER) != RT_EOK) + continue; + + if (pkg.region == REGION_END) + { + end_count++; + if (end_count >= SENDER_NUM) break; /* 两个发件线程都结束 */ + continue; + } + + total++; + weight += pkg.weight; + if (pkg.region == 1) east++; + else if (pkg.region == 2) south++; + else if (pkg.region == 3) north++; + + rt_kprintf("package %u -> %s, weight: %u g\n", pkg.id, region_str(pkg.region), pkg.weight); + } + + rt_kprintf("total packages: %u\n", total); + rt_kprintf("east: %u, south: %u, north: %u\n", east, south, north); + rt_kprintf("total weight: %u g\n", weight); + rt_mutex_take(sf_mutex, RT_WAITING_FOREVER); + rt_kprintf("send failed: %u\n", send_failed); + rt_mutex_release(sf_mutex); +} + +static int parcel_init(void) +{ + rt_thread_t t; + + pkg_mq = rt_mq_create("pkg", sizeof(struct package_msg), MQ_CAPACITY, RT_IPC_FLAG_FIFO); + if (pkg_mq == RT_NULL) return -RT_ENOMEM; + + sf_mutex = rt_mutex_create("sf", RT_IPC_FLAG_FIFO); + if (sf_mutex == RT_NULL) return -RT_ENOMEM; + + t = rt_thread_create("sender1", sender_entry, (void *)1, 1024, 15, 5); if (t) rt_thread_startup(t); + t = rt_thread_create("sender2", sender_entry, (void *)2, 1024, 15, 5); if (t) rt_thread_startup(t); + t = rt_thread_create("sorter", sorter_entry, RT_NULL, 2048, 10, 5); if (t) rt_thread_startup(t); + return RT_EOK; +} +INIT_APP_EXPORT(parcel_init); diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/Pasted image 20260818214306.png" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/Pasted image 20260818214306.png" new file mode 100644 index 0000000000000000000000000000000000000000..0a5302ee0aebc006fb281985392f156af4ed218c Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/Pasted image 20260818214306.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/app_led_threads.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/app_led_threads.c" new file mode 100644 index 0000000000000000000000000000000000000000..ac47cb100658b73d0695ddc8dc585ed32772861f --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/app_led_threads.c" @@ -0,0 +1,214 @@ +/* + * app_led_threads.c —— 星火一号 (STM32F407ZGT6) 多线程实战示例 + * ---------------------------------------------------------------------------- + * 板载资源(已核对星火一号原理图/BSP): + * LED_R(红) = PF12 共阳极:低电平点亮 / 高电平熄灭 + * LED_B(蓝) = PF11 共阳极:低电平点亮 / 高电平熄灭 + * KEY_LEFT = PC0 按下为低电平(下降沿触发) + * + * 线程规划: + * - 工作线程1 th_led1 : 动态创建(rt_thread_create),优先级16/时间片5/栈1024 + * - 工作线程2 th_led2 : 静态初始化(rt_thread_init),全局 TCB + 全局栈 + * - 监控线程 th_monitor : 优先级10(更高),周期 rt_thread_mdelay(2000), + * 打印 th_led1/th_led2 的状态与剩余栈;按键挂起/恢复 th_led1 + * - INIT_APP_EXPORT() 自动加载(不碰 main.c) + * - MSH_CMD_EXPORT() 导出命令 thread_dump + * + * 用法:把本文件放进 <星火一号工程>/applications/ 目录,编译下载即可。 + * 注意:星火一号模板 main.c 里默认有一段闪烁 LED 的 demo,请把它注释掉, + * 否则会和本文件的线程同时操作 PF11/PF12,造成竞争。 + * ---------------------------------------------------------------------------- + */ +#include +#include +#include /* 星火一号 BSP 提供 GET_PIN */ + +/* ============== 1. 星火一号 板级引脚 ============== */ +#define LED_R_PIN GET_PIN(F, 12) /* 红色 LED */ +#define LED_B_PIN GET_PIN(F, 11) /* 蓝色 LED */ +#define KEY_PIN GET_PIN(C, 0) /* KEY_LEFT,按下为低电平 */ +#define KEY_IRQ_MODE PIN_IRQ_MODE_FALLING + +/* ============== 2. 线程参数 ============== */ +#define LED_PRIORITY 16 +#define LED_TICK 5 +#define LED_STACK_SIZE 1024 + +#define MON_PRIORITY 10 +#define MON_TICK 5 +#define MON_STACK_SIZE 1024 + +/* ============== 3. 静态线程 th_led2 的 TCB 与栈(全局) ============== */ +static struct rt_thread th_led2; +static rt_uint8_t led2_stack[LED_STACK_SIZE]; + +/* ============== 4. 全局句柄、计数与按键去抖 ============== */ +static rt_thread_t th_led1 = RT_NULL; +static rt_thread_t th_monitor = RT_NULL; +static volatile rt_uint32_t led1_count = 0; +static volatile rt_uint32_t led2_count = 0; +static volatile rt_bool_t led1_suspended = RT_FALSE; +static rt_tick_t last_key_tick = 0; /* 按键软件去抖用 */ + +/* ============== 5. 状态 → 中文 工具函数 ============== */ +static const char *stat2str(rt_uint8_t stat) +{ + switch (stat & RT_THREAD_STAT_MASK) + { + case RT_THREAD_INIT: return "初始态"; + case RT_THREAD_READY: return "就绪态"; + case RT_THREAD_RUNNING: return "运行态"; + case RT_THREAD_SUSPEND: return "挂起态"; + case RT_THREAD_CLOSE: return "关闭态"; + default: return "未知态"; + } +} + +/* ============== 6. 工作线程1:动态创建 th_led1 ============== */ +/* 星火一号 LED 为共阳:PIN_LOW 点亮,PIN_HIGH 熄灭;翻转即亮灭交替 */ +static void led1_entry(void *parameter) +{ + rt_pin_mode(LED_R_PIN, PIN_MODE_OUTPUT); + + while (1) + { + led1_count++; + rt_pin_write(LED_R_PIN, (led1_count % 2) ? PIN_HIGH : PIN_LOW); + rt_kprintf("th_led1 count=%u, tick=%u\n", led1_count, rt_tick_get()); + rt_thread_mdelay(500); + } +} + +/* ============== 7. 工作线程2:静态初始化 th_led2 ============== */ +static void led2_entry(void *parameter) +{ + rt_pin_mode(LED_B_PIN, PIN_MODE_OUTPUT); + + while (1) + { + led2_count++; + rt_pin_write(LED_B_PIN, (led2_count % 2) ? PIN_HIGH : PIN_LOW); + rt_kprintf("th_led2 count=%u\n", led2_count); + rt_thread_mdelay(500); + } +} + +/* ============== 8. 监控线程 th_monitor(优先级10,高于工作线程) ============== */ +static void monitor_entry(void *parameter) +{ + while (1) + { + rt_thread_mdelay(2000); /* 周期 2s */ + + rt_kprintf("==== monitor @ tick=%u ====\n", rt_tick_get()); + + if (th_led1 != RT_NULL) + rt_kprintf("th_led1 stat=%s free_stack=%lu\n", + stat2str(th_led1->stat), + (unsigned long)rt_thread_get_free_stack_size(th_led1)); + + rt_kprintf("th_led2 stat=%s free_stack=%lu\n", + stat2str(th_led2.stat), + (unsigned long)rt_thread_get_free_stack_size(&th_led2)); + + rt_kprintf("th_led1_suspended=%s\n", led1_suspended ? "yes" : "no"); + } +} + +/* ============== 9. 按键中断回调:单次挂起 / 再次恢复 th_led1 ============== */ +static void key_callback(void *args) +{ + /* 中断上下文不能 sleep,用 tick 做 200ms 软件去抖 */ + rt_tick_t now = rt_tick_get(); + if (now - last_key_tick < rt_tick_from_millisecond(200)) + return; + last_key_tick = now; + + if (!led1_suspended) + { + if (th_led1 != RT_NULL) + rt_thread_suspend(th_led1); /* 中断里挂起“另一个”线程是允许的 */ + led1_suspended = RT_TRUE; + rt_kprintf("[key] th_led1 suspended\n"); + } + else + { + if (th_led1 != RT_NULL) + rt_thread_resume(th_led1); + led1_suspended = RT_FALSE; + rt_kprintf("[key] th_led1 resumed\n"); + } +} + +/* ============== 10. MSH 命令 thread_dump ============== */ +static void cmd_thread_dump(int argc, char **argv) +{ + rt_kprintf("---- thread_dump ----\n"); + if (th_led1 != RT_NULL) + rt_kprintf("th_led1 stat=%s free_stack=%lu count=%u\n", + stat2str(th_led1->stat), + (unsigned long)rt_thread_get_free_stack_size(th_led1), + led1_count); + rt_kprintf("th_led2 stat=%s free_stack=%lu count=%u\n", + stat2str(th_led2.stat), + (unsigned long)rt_thread_get_free_stack_size(&th_led2), + led2_count); + if (th_monitor != RT_NULL) + rt_kprintf("th_monitor stat=%s free_stack=%lu\n", + stat2str(th_monitor->stat), + (unsigned long)rt_thread_get_free_stack_size(th_monitor)); +} +MSH_CMD_EXPORT(thread_dump, dump th_led1/th_led2/th_monitor thread info); + +/* ============== 11. 自动初始化入口(app 级,禁止在 main.c 硬编码) ============== */ +static int led_app_init(void) +{ + rt_err_t ret; + + /* 引脚初始化 */ + rt_pin_mode(LED_R_PIN, PIN_MODE_OUTPUT); + rt_pin_mode(LED_B_PIN, PIN_MODE_OUTPUT); + rt_pin_mode(KEY_PIN, PIN_MODE_INPUT_PULLUP); /* 上拉:按下为低 */ + + /* ① 动态创建 th_led1 */ + th_led1 = rt_thread_create("th_led1", led1_entry, RT_NULL, + LED_STACK_SIZE, LED_PRIORITY, LED_TICK); + if (th_led1 != RT_NULL) + rt_thread_startup(th_led1); + else + { + rt_kprintf("th_led1 create failed!\n"); + return -RT_ENOMEM; + } + + /* ② 静态初始化 th_led2 */ + ret = rt_thread_init(&th_led2, "th_led2", led2_entry, RT_NULL, + &led2_stack[0], sizeof(led2_stack), + LED_PRIORITY, LED_TICK); + if (ret == RT_EOK) + rt_thread_startup(&th_led2); + else + { + rt_kprintf("th_led2 init failed!\n"); + return (int)ret; + } + + /* ③ 动态创建 th_monitor(优先级10 < 16,更高) */ + th_monitor = rt_thread_create("th_monitor", monitor_entry, RT_NULL, + MON_STACK_SIZE, MON_PRIORITY, MON_TICK); + if (th_monitor != RT_NULL) + rt_thread_startup(th_monitor); + else + { + rt_kprintf("th_monitor create failed!\n"); + return -RT_ENOMEM; + } + + /* ④ 按键中断:KEY_LEFT 单次挂起 / 再次恢复 */ + rt_pin_attach_irq(KEY_PIN, KEY_IRQ_MODE, key_callback, RT_NULL); + rt_pin_irq_enable(KEY_PIN, PIN_IRQ_ENABLE); + + rt_kprintf("led_app_init ok (INIT_APP_EXPORT) on 星火一号\n"); + return RT_EOK; +} +INIT_APP_EXPORT(led_app_init); diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/rtthread-day2-work.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/rtthread-day2-work.md" new file mode 100644 index 0000000000000000000000000000000000000000..6b4e9a3c5ba258489c9751f5b68d1d819b4b1a3e --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/rtthread-day2-work.md" @@ -0,0 +1,258 @@ +# 1.STM32F407 启动流程与 RT-Thread 引导机制 + +## 1.1 为何需要自动化初始化 +传统 RTOS/裸机工程里,驱动初始化通常靠人工在 `main()` 里逐个调用,需要用脑子记忆,新增驱动要改 main。而在rtthread内有两个函数`INIT_BOARD_EXPORT` 与 `INIT_APP_EXPORT` `声明及注册`,只要把初始化函数用宏标注,链接器在编译期就把它排进一个特定段,运行期由内核自动遍历执行,main 里什么都不用写。 + +--- +### 1.2 宏展开机制 +所有 `INIT_*_EXPORT` 宏都源于同一个底层宏 `INIT_EXPORT`: +```c +/* rtdef.h —— 六级初始化优先级,数字越小越靠前执行 */ +#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "board") /* 板级:最早 */ +#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "pre") +#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "device") +#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "component") +#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "env") +#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "app") /* 应用:最晚 */ + +/* 真正的"魔法"定义 */ +#define INIT_EXPORT(fn, level) \ + RT_USED const rt_init_fn_t __rt_init_##fn##_##level \ + SECTION("rt_init_fn." level) = fn +``` +**其中**,INIT_BOARD_EXPORT(fn)` 做的事情,就是把 `fn` 这个函数指针,塞进 Flash 里一个叫 `rt_init_fn.board` 的小抽屉,和所有同级别的函数指针排在一起。 + +--- + +### 1.3 链接脚本布局(段的物理排序) + +链接脚本(`link.lds` / `rtthread.lds`)负责把这些分散在各 `.o` 文件里的 `rt_init_fn.*` 段**聚到一起并按级别排序** + +链接器还会生成两个边界符号:`__rt_init_fn_start`(段首)和 `__rt_init_fn_end`(段尾)。**这两个符号之间的内存,本质上就是一个"函数指针数组"** + +--- +### 1.4 运行期遍历(谁来"按抽屉执行") +```c +/* components.c */ + +/* 入口 A:只跑 board 段,在 rt_hw_board_init() 内部、调度器启动之前 */ +void rt_components_board_init(void) +{ + volatile const rt_init_fn_t *fn_ptr; + + for (fn_ptr = &__rt_init_fn_board_start; + fn_ptr < &__rt_init_fn_board_end; + fn_ptr++) + { + (*fn_ptr)(); /* 逐个取出函数指针并调用 */ + } +} + +/* 入口 B:跑剩下五段(pre→device→component→env→app),在 main 线程入口里、用户 main 之前 */ +void rt_components_init(void) +{ + volatile const rt_init_fn_t *fn_ptr; + + for (fn_ptr = &__rt_init_fn_pre_start; + fn_ptr < &__rt_init_fn_app_end; + fn_ptr++) + { + (*fn_ptr)(); + } +} +``` +![[Pasted image 20260818214306.png|394]] + + +--- +### 1.5 INIT_BOARD_EXPORT 与 INIT_APP_EXPORT 的本质区别 + +| 维度 | `INIT_BOARD_EXPORT` | `INIT_APP_EXPORT` | +|---|---|---| +| 所属段 | `rt_init_fn.board` | `rt_init_fn.app` | +| 调用入口 | `rt_components_board_init()` | `rt_components_init()` | +| 调用时机 | **调度器启动之前**(中断仍关闭) | **调度器启动之后**、用户 `main()` **之前** | +| 线程环境 | 无线程概念,不能阻塞/睡眠 | 已是线程上下文,可用 `rt_thread_mdelay`、信号量等 | +| 典型用途 | 时钟、GPIO pinmux、早期 UART | 网络协议栈、文件系统、应用级组件 | +| 失败后果 | 系统根本起不来 | 仅该应用组件异常,系统仍在跑 | +**执行顺序保证**:因为 `board` 段在 Flash 中排在 `component` 段之前,且 `rt_components_board_init` 先于 `rt_components_init` 被调用,所以 `rt_hw_usart_init` 一定先于 `rt_hw_console_init` 执行。 + +所以,他们的本质区别是中断的时机不同,而这也决定了他们能做什么,能不能使用线程。 + +--- +## 2. 内核基础与对象模型 +RT-Thread 用 **纯 C 语言**模拟面向对象,核心为”对象头 + 类型标签“: +- 所有内核对象(线程、信号量、互斥量、定时器、设备、邮箱……)的 `struct` **第一个成员都是 `struct rt_object`**,提供统一的名字、类型、链表节点。 +- 通过 `type` 字段区分对象种类;通过 `list` 双向链表节点,把对象挂进"**对象容器**"(内核用 `rt_object_container` 数组按类型管理,共 `RT_Object_Class_Unknown` 到各类别),实现类似"基类容器"的效果。 +- 访问对象时先检查 `type`,再按具体类型强转回派生结构体 +```c +/* rtdef.h:所有内核对象的公共头部 */ +struct rt_object +{ + char name[RT_NAME_MAX]; /* 对象名字 */ + rt_uint8_t type; /* 对象类型(枚举) */ + rt_uint8_t flag; /* 对象标志 */ + rt_list_t list; /* 挂到对象容器的链表节点 */ +}; + +/* 线程控制块:第一成员就是 rt_object,等价于"继承" */ +struct rt_thread +{ + struct rt_object parent; /* 继承的公共头部 */ + rt_list_t tlist; /* 调度器用的就绪/挂起链表节点 */ + /* ... 线程私有字段见 2.2 ... */ +}; +``` + +--- + +### 2.2 线程控制块 `struct rt_thread` 关键成员 + +| 成员 | 类型 | 作用 | +|---|---|---| +| `stat` | `rt_uint8_t` | **线程状态机核心**:`RT_THREAD_INIT`(初始) / `RT_THREAD_READY`(就绪) / `RT_THREAD_RUNNING`(运行) / `RT_THREAD_SUSPEND`(挂起) / `RT_THREAD_CLOSE`(关闭)。调度器据此决定是否参与调度 | +| `current_priority` | `rt_uint8_t` | **当前优先级**(0 最高)。调度时比较此值选最高优先级线程运行;可被 `rt_thread_control` 动态修改 | +| `init_priority` | `rt_uint8_t` | 初始优先级,作为"基准"。当优先级被临时提升(如优先级翻转防护)后,可恢复回此值 | +| `init_tick` | `rt_uint32_t` | **时间片初始值**(单位:tick)。同优先级线程轮转时,每次获得的 CPU 时间长度 | +| `remaining_tick` | `rt_uint32_t` | **剩余时间片**。SysTick 每 tick 递减;归零后线程被移到同优先级就绪队尾,并重置为 `init_tick` | +| `sp` | `void *` | **当前栈指针**。上下文切换时保存/恢复现场的关键——指向该线程在被切出时各寄存器压栈的位置 | +| `entry` | `void *` | 线程入口函数指针 | +| `parameter` | `void *` | 传给 `entry` 的参数 | +| `stack_addr` / `stack_size` | `void *` / `rt_uint32_t` | 线程栈的起址与大小(静态或动态分配,见 2.3) | +| `tlist` | `rt_list_t` | 调度器链表节点:线程在**就绪表/挂起表/定时器超时表**中的挂接点 | +| `thread_timer` | `struct rt_timer` | 线程内置定时器:用于 `rt_thread_delay()` / `rt_thread_suspend` 的超时唤醒 | +| `error` | `rt_err_t` | 最近一次操作的错误码 | +| `cleanup` | 函数指针 | 线程退出时(状态变 `CLOSE`)的清理回调 | +> **调度器视角**:`stat` 决定"是否就绪"、`current_priority` 决定"被谁抢占"、`init_tick`/`remaining_tick` 决定"同优先级能跑多久"、`sp` 决定"切回来时从哪恢复现场"——这四个字段是调度算法直接读写的对象。 + +--- + +### 2.3 静态对象 vs 动态对象 + +#### 1. 静态线程创建(编译期确定内存) + +```c +/* 1. 定义线程控制块(TCB),由编译器在静态区分配内存 */ +static struct rt_thread led_thread; + +/* 2. 定义线程栈,大小为 512 字节,同样在静态区 */ +static rt_uint8_t led_stack[512]; + +/* 3. 初始化线程(此时线程处于“初始态”,不会立刻运行) */ +rt_thread_init( + &led_thread, /* 线程控制块指针 */ + "led", /* 线程名称,字符串,用于调试识别 */ + led_entry, /* 线程入口函数,即线程要做的事情 */ + RT_NULL, /* 传递给入口函数的参数(无参数则填 RT_NULL) */ + &led_stack[0], /* 栈起始地址(数组首元素地址) */ + sizeof(led_stack), /* 栈大小(单位:字节) */ + RT_THREAD_PRIORITY_MAX - 2, /* 线程优先级:数字越小越高,这里设为次低优先级 */ + 20 /* 时间片:同优先级轮转时,每次运行 20 个 tick */ +); + +/* 4. 启动线程:将线程从“初始态”转为“就绪态”,等待调度器调度 */ +rt_thread_startup(&led_thread); +``` + +--- + +### 2. 动态线程创建(运行期从堆分配内存) + +c + +```c +/* 1. 创建线程(系统自动从堆中分配控制块和栈空间) */ +rt_thread_t tid = rt_thread_create( + "led", /* 线程名称 */ + led_entry, /* 线程入口函数 */ + RT_NULL, /* 传递给入口函数的参数 */ + 512, /* 栈大小(单位:字节) */ + RT_THREAD_PRIORITY_MAX - 2, /* 线程优先级:次低 */ + 20 /* 时间片:20 个 tick */ +); + +/* 2. 检查线程是否创建成功 */ +if (tid != RT_NULL) +{ + /* 3. 启动线程(转为就绪态) */ + rt_thread_startup(tid); +} +else +{ + /* 创建失败,通常是堆内存不足 */ + rt_kprintf("create led thread failed!\n"); +} +``` + +| 维度 | 静态对象 | 动态对象 | +|---|---|---| +| 分配位置 | 编译期确定的 `.bss`/全局区(或你指定的静态数组) | 运行时从**内存堆(heap)** 动态申请 | +| 典型 API | `rt_thread_init()`、`rt_sem_init()`(对象由调用者提供) | `rt_thread_create()`、`rt_sem_create()`(内核帮你 malloc) | +| 生命周期 | 与程序同寿,或手动 `rt_thread_detach()` 脱离 | `rt_thread_delete()` / `rt_sem_delete()` 释放回堆 | +| 释放机制 | 无(或 detach 仅从容器摘除,内存不回收) | 释放回堆,可被后续分配复用 | +| 失败风险 | 编译期就占好内存,**无运行时分配失败** | 堆不足时返回 `RT_NULL`,需判空 | +| 内存碎片 | 无 | 长期运行可能产生碎片 | + +--- + +### 2.4 实践建议 + 实践建议:关键任务线程(控制环、看门狗、通信)、中断相关对象用静态;非关键、数量随业务波动的临时线程可用动态。 + +在工控、轨交、医疗、航天等**高可靠(safety-critical)场景**,通常禁止动态分配,原因: + +1. **确定性(Determinism)**:静态对象在编译/链接期就占好固定内存,**运行期分配不会失败**。动态 `malloc` 在堆紧张时可能返回 `NULL`,而关键控制线程若在启动时才失败,系统就起不来了。 +2. **无内存碎片**:长期 7×24 运行的设备,动态分配/释放会逐步产生碎片,最终无连续块可用。静态分配一次性划定,永不产生碎片。 +3. **无释放类 Bug**:动态对象有 `use-after-free`、`double-free`、`泄漏` 等风险;静态对象生命周期与系统一致,规避整类错误。 +4. **启动顺序与可验证性**:静态对象地址固定,可静态分析内存布局、做 MISRA/功能安全审计;动态堆的行为难以在出厂前完全验证。 +5. **最坏执行时间(WCET)可控**:动态分配的时间不确定,静态分配的执行路径可预测——这对实时性认证(如 IEC 61508、ISO 26262)是硬要求。 + +## 3. 线程状态机与调度算法 + +### 3.1 rtt线程状态描述及转换api + +|**

状态名称

**|**

宏定义(在 `rtdef.h` 中)

**|**

描述

**| +|---|---|---| +|**初始态**​|`RT_THREAD_INIT`|线程刚被创建,系统为其分配了控制块和栈,但还未就绪运行。| +|**就绪态**​|`RT_THREAD_READY`|线程已经具备运行条件,正在等待 CPU 资源(排在就绪列表中)。| +|**运行态**​|`RT_THREAD_RUNNING`|线程当前正占用 CPU 正在执行。| +|**挂起态**​|`RT_THREAD_SUSPEND`|线程因为等待某个事件(如延时到期、信号量、互斥量等)而主动让出 CPU。| +|**关闭态**​|`RT_THREAD_CLOSE`|线程执行完毕或主动退出,等待系统回收资源(由 idle 线程完成最后清理)。| + +![[五态关系图.png]] + +| 起点 → 终点 | 触发 API / 事件 | +| --------- | --------------------------------------------------------------------------------------- | +| 初始态 → 就绪态 | `rt_thread_startup()` | +| 就绪态 → 运行态 | 调度器在节拍/中断返回时选中(无显式 API,由 `rt_schedule()` 驱动) | +| 运行态 → 就绪态 | `rt_thread_yield()`(主动让出)、被高优先级线程抢占 | +| 运行态 → 挂起态 | `rt_thread_suspend()`、`rt_thread_delay()`、`rt_sem_take()`(资源不可用)、`rt_mb_recv()` 等阻塞 API | +| 挂起态 → 就绪态 | `rt_thread_resume()`、等待的信号量/邮箱到位、`rt_timer` 超时唤醒 | +| * → 关闭态 | `rt_thread_delete()`(动态)/ `rt_thread_detach()`(静态)或线程入口函数 `return` | +>在其中程序未完成而发生的“抢占”行为不是线程自己调的,而是调度器在中断返回或资源释放点强制发生的**。 + +--- + +### 3.3 抢占式调度(Preemptive Scheduling) + +RT-Thread 默认是**基于优先级的全抢占式**调度: + +- **核心规则**:就绪队列中,**优先级数值最小(0 最高)的线程永远先跑**。任何时刻只要有更高优先级线程进入就绪态,当前运行线程立即被抢占。 +- **抢占发生的时机(调度点)**: + 1. **中断返回**:ISR 里释放了资源(如 `rt_sem_release`),退出中断前调用 `rt_schedule()`,发现更高优先级就绪 → 切换。 + 2. **主动释放资源**:线程调用 `rt_sem_release()` / `rt_mb_send()` 等,内部触发 `rt_schedule()`。 + 3. **主动让出**:线程调 `rt_thread_yield()`(让同优先级或更高优先级先跑)。 + 4. **线程挂起/延时**:`rt_thread_delay()` 当前线程进入 SUSPEND,调度器选下一个就绪线程。 +- **实现支撑**:调度器用"优先级位图(bitmap)+ 每优先级一个就绪链表"的结构,找最高优先级线程是 O(1);`current_priority` 字段即比较依据。 + +--- + +### 3.4 时间片轮转(Round-Robin) + +抢占式解决"谁先跑(优先级)",时间片轮转解决"**同优先级多个线程怎么公平分 CPU**": + +- **作用范围**:**仅在相同 `current_priority` 的线程之间**生效。不同优先级仍走抢占式。 +- **原理**: + - 每个线程有 `init_tick`(时间片长度)和 `remaining_tick`(剩余)。 + - 系统节拍(SysTick)每触发一次,`remaining_tick--`。 + - 递减到 0 时:该线程被移到**同优先级就绪队列的队尾**,`remaining_tick` 重置为 `init_tick`,调度器在同优先级里选下一个。 +- 线程也可主动 `rt_thread_yield()` 立即让出(不等时间片耗尽)。 diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/\344\272\224\346\200\201\345\205\263\347\263\273\345\233\276.png" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/\344\272\224\346\200\201\345\205\263\347\263\273\345\233\276.png" new file mode 100644 index 0000000000000000000000000000000000000000..74c23fe4985959319d0c31949aa68baecd03c7b5 Binary files /dev/null and "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/\344\272\224\346\200\201\345\205\263\347\263\273\345\233\276.png" differ diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev-design.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev-design.md" new file mode 100644 index 0000000000000000000000000000000000000000..5929bb3e333b41156df3357ac71caaf62006da4d --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev-design.md" @@ -0,0 +1,138 @@ +# test_dev —— 一个自包含的虚拟设备驱动框架层 + +## 一、为什么要有"框架层" + +Day4 讲 IO 设备驱动框架时,重点就是下面这条分层链: + +``` +应用层 API + ↓ +IO 设备管理层(rt_device_xxx) + ↓ +设备驱动框架层(SPI / I2C / GPIO / Pin) ← test_dev 要扮演的就是这一层 + ↓ +设备驱动层(HAL / LL) + ↓ +硬件 +``` + +框架层的核心价值只有一句话:**让"管理层"只认一套统一接口,让"驱动层"只管具体硬件**,两边互不关心对方长什么样。test_dev 把这一层单独抽出来,做成不依赖 RT-Thread、不依赖真实硬件、纯 C 就能编译运行的最小例子,专门用来看清"框架层到底干了什么"。 + +## 二、test_dev 的整体设计 + +### 2.1 分层映射 + +| 真实 RT-Thread 里的角色 | test_dev 里的对应 | +|---|---| +| IO 设备管理层(rt_device_xxx) | `test_dev` 框架本身:register / find / init / open / read / write / control / close | +| 设备驱动框架层(夹在中间那层) | 这里就是 test_dev 演示的对象 | +| 设备驱动层 / 硬件 | `test_dev_driver_ops` 回调 + 一个具体驱动(虚拟 LED) | + +> 真实 RT-Thread 里框架层还会再细分 SPI / I2C / Pin,各自有独立的 ops。test_dev 用"一个统一框架 + 可换的 driver ops"把同一套机制一次性演示清楚。 + +### 2.2 核心数据结构 + +`test_dev_t` —— 框架层统一的设备对象(类比 `struct rt_device`): + +```c +struct test_dev { + const char *name; /* 设备名,应用层按名字查找 */ + test_dev_type_t type; /* 设备类型(LED / KEY / SENSOR...) */ + uint8_t ref_count; /* 打开计数,open/close 配对 */ + uint8_t initialized; /* 是否已 init */ + void *user_data; /* 驱动私有数据(虚拟寄存器/状态) */ + const test_dev_driver_ops_t *dops; /* 指向具体驱动的回调表 */ +}; +``` + +`test_dev_driver_ops_t` —— 框架层对驱动层要求的统一回调(类比 `struct rt_device_ops`): + +```c +struct test_dev_driver_ops { + int (*init) (test_dev_t *dev); + int (*deinit) (test_dev_t *dev); + int (*read) (test_dev_t *dev, void *buf, size_t len); + int (*write) (test_dev_t *dev, const void *buf, size_t len); + int (*control)(test_dev_t *dev, int cmd, void *args); +}; +``` + +解耦的关键就在 `dops` 这个指针:框架层只调用 `dev->dops->xxx`,永远不直接碰硬件。换一个设备,只是换一张 `dops` 表。 + +### 2.3 生命周期与 API + +``` +create → 分配设备对象(含私有数据结构) +register→ 挂到全局设备表(名字 -> 设备) +find → 应用层按名字拿到设备句柄 +init/open/close/read/write/control → 统一入口,内部分发到 driver ops +``` + +`open` 里做了"懒初始化":首次 open 若未 init 会自动 init;`close` 在引用计数归零时才调驱动的 `deinit`。这和 RT-Thread 里 `rt_device_open` 的引用计数行为一致。 + +## 三、调用链走读(以点亮 LED 为例) + +应用层一句: + +```c +test_dev_write(dev, &on, 1); /* on = 1,点亮 */ +``` + +实际发生的调用栈: + +``` +test_dev_write(dev, &on, 1) // 框架层统一入口 + └─ dev->dops->write(dev, &on, 1) // 分发到驱动回调 + └─ led_write(dev, &on, 1) // 具体驱动实现 + └─ hw->on = 1; // 改写"虚拟寄存器" + printf(... "ON") // 模拟硬件动作 +``` + +应用层从头到尾只认 `test_dev_write` 这个框架 API,既不知道背后是 LED 还是按键,也不知道 `led_write` 里发生了什么。这就是框架层存在的意义。 + +## 四、与 RT-Thread 真实框架对照 + +| test_dev | RT-Thread | 说明 | +|---|---|---| +| `test_dev_t` | `struct rt_device` | 设备对象 | +| `test_dev_driver_ops_t` | `struct rt_device_ops` | 统一回调接口 | +| `test_dev_register` | `rt_device_register` | 注册到系统 | +| `test_dev_find` | `rt_device_find` | 按名查找 | +| `test_dev_read/write/control` | `rt_device_read/write/control` | 管理层 API | +| `led_ops`(具体驱动回调) | `rt_pin_ops` / `rt_spi_ops` | 框架/驱动层的具体实现 | +| `virt_led_hw_t` | 寄存器基址 / HAL 句柄 | 硬件状态载体 | + +一一对应,只是 test_dev 把"管理层 + 框架层"合成了一层来简化演示。 + +## 五、编译运行(虚拟 LED 例子) + +代码全在 `test_dev/` 目录,纯标准 C99,无第三方依赖。本地用任意 C 编译器: + +```bash +cd test_dev +gcc test_dev.c test_dev_led.c main.c -o test_dev_demo +./test_dev_demo +``` + +预期输出: + +``` +[test_dev] register 'led0' (type=0) +[virt_led] led0 init: off, brightness=100% +[virt_led] led0 -> ON +app read back: led is ON +[virt_led] led0 brightness=40% +[virt_led] led0 toggle -> OFF +[virt_led] led0 deinit +``` + +## 六、如何扩展第二个设备(验证框架价值) + +框架层"加设备不改应用"的价值,靠扩展来验证。照着 `test_dev_led.c` 再写一个 `test_dev_key.c`(虚拟按键): + +1. 定义 `virt_key_hw_t`(记录按键电平、是否按下); +2. 实现 `key_init / key_read / key_control` 填进一张 `key_ops`; +3. `test_dev_key_create` 时把 `type` 设成 `TEST_DEV_TYPE_KEY`; +4. `test_dev_register` 后,应用层用**完全相同的** `test_dev_find / test_dev_read` 就能操作按键。 + +应用层代码一行都不用改——这正是 Day4 说的"框架层解耦"的真正含义。 diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.c" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.c" new file mode 100644 index 0000000000000000000000000000000000000000..f336458529846dff9b102047b14453b4d2e300c7 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.c" @@ -0,0 +1,83 @@ +#include "test_dev_led.h" +#include +#include + +/* + * 虚拟 LED 的"硬件寄存器"——纯内存模拟,不碰任何真实外设。 + * 在真实 RT-Thread 里,这里会是 GPIO 寄存器基址 / PWM 句柄。 + */ +typedef struct { + uint8_t on; /* 0=灭 1=亮,模拟 GPIO 输出位 */ + uint8_t brightness; /* 0~100,模拟 PWM 占空比 */ +} virt_led_hw_t; + +static int led_init(test_dev_t *dev) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)dev->user_data; + hw->on = 0; + hw->brightness = 100; + printf("[virt_led] %s init: off, brightness=100%%\n", dev->name); + return 0; +} + +static int led_deinit(test_dev_t *dev) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)dev->user_data; + hw->on = 0; + printf("[virt_led] %s deinit\n", dev->name); + return 0; +} + +static int led_read(test_dev_t *dev, void *buf, size_t len) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)dev->user_data; + if (len < sizeof(uint8_t)) + return -1; + *(uint8_t *)buf = hw->on; + return sizeof(uint8_t); +} + +static int led_write(test_dev_t *dev, const void *buf, size_t len) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)dev->user_data; + if (len < sizeof(uint8_t)) + return -1; + hw->on = (*(const uint8_t *)buf) ? 1 : 0; + printf("[virt_led] %s -> %s\n", dev->name, hw->on ? "ON" : "OFF"); + return sizeof(uint8_t); +} + +static int led_control(test_dev_t *dev, int cmd, void *args) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)dev->user_data; + switch (cmd) { + case TEST_LED_CMD_SET_BRIGHTNESS: + hw->brightness = *(const uint8_t *)args; + printf("[virt_led] %s brightness=%u%%\n", dev->name, hw->brightness); + return 0; + case TEST_LED_CMD_TOGGLE: + hw->on = !hw->on; + printf("[virt_led] %s toggle -> %s\n", dev->name, hw->on ? "ON" : "OFF"); + return 0; + default: + return -1; + } +} + +/* 驱动层实现:把上面的函数填进回调表 */ +static const test_dev_driver_ops_t led_ops = { + .init = led_init, + .deinit = led_deinit, + .read = led_read, + .write = led_write, + .control= led_control, +}; + +/* 创建虚拟 LED:分配自己的私有硬件状态,再交给框架层托管 */ +test_dev_t *test_dev_led_create(const char *name) +{ + virt_led_hw_t *hw = (virt_led_hw_t *)calloc(1, sizeof(virt_led_hw_t)); + if (!hw) + return NULL; + return test_dev_create(name, TEST_DEV_TYPE_LED, &led_ops, hw); +} diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.h" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.h" new file mode 100644 index 0000000000000000000000000000000000000000..c71c82cc6aaa2cc4e30bb50baf83e362bfefb547 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/test_dev_led.h" @@ -0,0 +1,13 @@ +#ifndef TEST_DEV_LED_H +#define TEST_DEV_LED_H + +#include "test_dev.h" + +/* 驱动层创建并注册一个虚拟 LED 设备 */ +test_dev_t *test_dev_led_create(const char *name); + +/* 虚拟 LED 的控制命令 */ +#define TEST_LED_CMD_SET_BRIGHTNESS 0x10 /* args: uint8_t* (0~100) */ +#define TEST_LED_CMD_TOGGLE 0x11 /* args: NULL */ + +#endif /* TEST_DEV_LED_H */ diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/Git \345\270\270\347\224\250\345\221\275\344\273\244\351\200\237\346\237\245\350\241\250.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/Git \345\270\270\347\224\250\345\221\275\344\273\244\351\200\237\346\237\245\350\241\250.md" new file mode 100644 index 0000000000000000000000000000000000000000..56d5a6d5fa73a9db164464647bd399180c920273 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/Git \345\270\270\347\224\250\345\221\275\344\273\244\351\200\237\346\237\245\350\241\250.md" @@ -0,0 +1,143 @@ +# Git 常用命令速查表 + +> 桌面速查参考,按使用场景分组。尖括号 `< >` 中的内容请按需替换,例如 `` = 远程仓库地址,`` = 分支名,`` = 文件路径。 + +--- + +## 一、仓库初始化与克隆 + +| 命令 | 说明 | +| :-- | :-- | +| `git init` | 在当前目录初始化一个新的 Git 仓库 | +| `git clone ` | 克隆远程仓库到本地 | +| `git clone ` | 克隆到指定目录 `` | +| `git config --global user.name ""` | 设置全局提交用户名 | +| `git config --global user.email ""` | 设置全局提交邮箱 | +| `git config --list` | 查看所有 Git 配置 | + +--- + +## 二、文件暂存与提交 + +| 命令 | 说明 | +| :-- | :-- | +| `git status` | 查看工作区与暂存区状态 | +| `git add ` | 将指定文件加入暂存区 | +| `git add .` | 将所有修改/新增文件加入暂存区(不含删除) | +| `git add -A` / `git add --all` | 将所有改动(含删除)加入暂存区 | +| `git commit -m ""` | 提交暂存区,并附提交说明 | +| `git commit -am ""` | 跳过 `git add`,直接提交已跟踪文件的修改 | +| `git commit --amend` | 修改最后一次提交(补充内容或改写说明) | +| `git stash` | 临时保存当前工作区修改 | +| `git stash pop` | 恢复最近一次 stash 并删除该记录 | +| `git stash list` | 查看所有 stash 记录 | + +--- + +## 三、分支管理 + +| 命令 | 说明 | +| :-- | :-- | +| `git branch` | 列出所有本地分支(当前分支前带 `*`) | +| `git branch ` | 基于当前分支创建新分支 | +| `git branch -d ` | 删除已合并的分支 | +| `git branch -D ` | 强制删除分支(未合并也可用) | +| `git checkout ` | 切换到指定分支 | +| `git checkout -b ` | 创建并切换到新分支 | +| `git switch ` | 切换到指定分支(Git 2.23+ 推荐) | +| `git switch -c ` | 创建并切换到新分支(Git 2.23+ 推荐) | +| `git merge ` | 将 `` 合并进当前分支 | +| `git rebase ` | 将当前分支的提交变基到 `` 之上 | + +--- + +## 四、合并与变基 + +| 命令 | 说明 | +| :-- | :-- | +| `git merge ` | 生成一次合并提交(保留完整历史) | +| `git merge --no-ff ` | 禁用快进合并,强制保留合并提交 | +| `git rebase ` | 将当前分支提交"重放"到目标分支末尾,历史更线性 | +| `git rebase -i ` | 交互式变基,可合并/改写提交 | +| `git rebase --abort` | 中止正在进行的变基 | +| `git rebase --continue` | 解决冲突后继续变基 | +| `git merge --abort` | 中止正在进行的合并 | + +> 冲突发生后:手动编辑冲突文件,保留需要的内容,再 `git add ` 标记已解决,最后 `git commit`(merge)或 `git rebase --continue`(rebase)。 + +--- + +## 五、远程仓库操作 + +| 命令 | 说明 | +| :-- | :-- | +| `git remote -v` | 查看远程仓库的 fetch / push 地址 | +| `git remote add origin ` | 关联本地仓库到远程 `origin` | +| `git remote set-url origin ` | 修改远程仓库地址 | +| `git remote rename ` | 重命名远程仓库 | +| `git remote rm ` | 移除远程仓库 | +| `git fetch` | 拉取所有远端更新到本地(不合并) | +| `git fetch ` | 同步指定远端分支到本地 | +| `git pull` | 拉取当前分支对应的远端并合并(`fetch` + `merge`) | +| `git pull --rebase` | 以变基方式拉取(fetch + rebase) | +| `git push` | 推送当前分支到对应远端 | +| `git push -u origin ` | 推送并关联远端分支(首次推送推荐) | +| `git push --force` | 强制推送(慎用,会覆盖远端历史) | +| `git push --tags` | 推送本地标签到远端 | + +--- + +## 六、查看状态与日志 + +| 命令 | 说明 | +| :-- | :-- | +| `git status` | 查看工作区与暂存区状态 | +| `git log` | 查看提交历史(按时间倒序) | +| `git log --oneline` | 每个提交显示一行(简洁) | +| `git log -n ` | 只显示最近 `` 条提交 | +| `git log --graph --all` | 图形化展示分支与合并历史 | +| `git log --author=""` | 按作者筛选提交 | +| `git show ` | 查看某次提交的详细改动 | +| `git diff` | 查看工作区与暂存区的差异 | +| `git diff --cached` | 查看暂存区与最新提交的差异 | +| `git diff ` | 比较两个分支的差异 | +| `git blame ` | 查看文件每行最后修改的提交与作者 | + +--- + +## 七、撤销与回退 + +| 命令 | 说明 | +| :-- | :-- | +| `git restore ` | 丢弃工作区对 `` 的修改(Git 2.23+) | +| `git restore --staged ` | 将 `` 从暂存区移出(取消 `add`) | +| `git checkout -- ` | 丢弃工作区对 `` 的修改(旧写法) | +| `git reset ` | 取消暂存(保留工作区修改) | +| `git reset --soft ` | 回退到指定提交,保留暂存区与工作区 | +| `git reset --mixed ` | 回退到指定提交,保留工作区(默认) | +| `git reset --hard ` | 彻底回退到指定提交(丢弃所有后续改动,**慎用**) | +| `git revert ` | 生成一次新提交来撤销某次提交(不改写历史,推荐) | +| `git clean -fd` | 删除未跟踪的文件与目录(**慎用**) | + +> **reset vs revert**:`reset` 改写历史,适合本地未推送的提交;`revert` 保留历史、新增撤销提交,适合已推送的提交。 + +--- + +## 附录:日常典型工作流 + +```bash +# 1. 克隆并切换到开发分支 +git clone +git switch -c feature/my-work + +# 2. 开发完成后暂存并提交 +git add . +git commit -m "完成某某功能" + +# 3. 同步远端最新代码(推荐变基方式) +git fetch origin +git rebase origin/master + +# 4. 推送到远端并发起 PR +git push -u origin feature/my-work +``` diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/rtthread-day1-notes.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/rtthread-day1-notes.md" new file mode 100644 index 0000000000000000000000000000000000000000..6cc75fd3ac51ee19542f9cef76aeaa6a31970800 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260/rtthread-day1-notes.md" @@ -0,0 +1,242 @@ +# 2026 RT-Thread 大学生夏令营 Day1 学习笔记 + +--- + +## 一、工程文件树要点 + +| 目录/模块 | 作用 | +| :-- | :-- | +| `application/main.c` | 程序主入口(如 hello 打印) | +| `drivers/` | 底层驱动适配(LCD/串口/音频等) | +| `components/` | 核心组件:C++、文件系统 DFS、finish 终端等 | +| `libc` | C 库兼容层(armcc/gcc/clang) | +| `src/` | 内核:时钟、设备管理、idle 线程、IPC(进程间通信) | +| `drivers` 框架 | 三层:IO 设备管理 → 驱动框架(串口/SPI/I2C/GPIO)→ 厂商驱动实现 | + +- **finish 组件**:`component/finish`,可查看系统资源(内存、线程状态);`ps` 查看线程,`tab` 补全命令;`idle` 线程是 RTOS 最小运行实体。 + +--- + +## 二、Markdown 语法(笔记/作业统一格式) + +- 标题:`#` 一级、`##` 二级、`###` 三级……(井号数量 = 层级)。 +- 代码块:三个反引号 ``` 包裹,起始处标注语言(如 ```c)实现高亮。 +- 行内代码/高亮:一对反引号 `code`。 +- 加粗:`**文字**`;斜体:`*文字*`。 +- 表格:用 `|` 分隔列,第二行用 `|---|` 分隔表头与内容。 +- 插入图片(语法示例,非本文实际引用):`![描述](图片路径)`;建议把剪贴板图片保存到工程下 `figures/` 等相对路径目录,避免迁移丢图。 +- 学习要求:每日用 Markdown 整理笔记 → 提交到个人 Git 仓库 + 发布到社区论坛。 + +--- + +## 三、Git 基础操作与 PR 贡献流程 + +### 3.1 全局设置 + +第一次使用 Git 时,先配置用户名和邮箱,这些信息会记录到每一次提交中: + +```bash +git config --global user.name "" +git config --global user.email "" +``` + +### 3.2 基础工作流(本地 → 远端) + +从零开始创建并推送一个仓库的最小流程: + +```bash +mkdir my_project +cd my_project +git init # 初始化当前目录为 Git 仓库 +touch README.md +git add README.md # 加入暂存区(受 git 保护) +git commit -m "first commit" # 提交,附说明 +git remote add origin # 关联远端仓库 +git push -u origin master # 本地推送到远端 +``` + +> **说明** +> +> - `git add `:将指定文件或目录加入暂存区(stage/index),加入后文件即进入 Git 管理、受版本控制保护。 +> - `git commit -m ""`:把暂存区的改动生成一次提交,并填写提交日志。 +> - `git push`:本地 → 远端(推送);`git pull`:远端 → 本地(拉取)。 + +提交成功后的终端输出示例: + +```text +[master 8de35a] first commit + 1 file changed, 3 insertions(+) + create mode 100644 README.md +``` + +如果当前工作区没有待提交内容,执行 `git commit` 会提示: + +```text +On branch master +Your branch is up to date with 'origin/master'. + +nothing to commit, working tree clean +``` + +需要临时保存当前修改(稍后恢复)时可用: + +```bash +git stash +``` + +输出示例: + +```text +Saved working directory and index state WIP on master: Initial commit +``` + +### 3.3 远程仓库管理 + +| 命令 | 作用 | +| :-- | :-- | +| `git remote add origin ` | 关联本地仓库到远端 | +| `git remote set-url origin ` | 修改当前仓库的远端地址(如仓库迁移) | +| `git remote -v` | 查看远端仓库的 fetch / push 地址 | +| `git fetch origin master` | 同步远端分支到本地(不自动合并) | + +修改远端地址示例: + +```bash +git remote set-url origin <新的仓库地址> +``` + +> 注意:`<新的仓库地址>` 请替换为你自己的实际仓库地址,例如 `git@example.com:/.git`。 + +查看远端地址: + +```bash +git remote -v +``` + +输出示例: + +```text +origin (fetch) +origin (push) +``` + +同步远端分支: + +```bash +git fetch origin master +``` + +输出示例: + +```text +From + * branch master -> FETCH_HEAD +``` + +### 3.4 分支与协作 + +| 命令 | 作用 | +| :-- | :-- | +| `git checkout -b ` | 基于当前分支新建并切换到新分支 | +| `git switch ` | 切换到指定分支(Git 2.23+ 推荐) | +| `git pull origin master` | 拉取远程 `master` 分支到本地 | + +```bash +git checkout -b test # 基于当前分支新建并切换到 test 分支 +git switch master # 切换到 master 分支 +git fetch origin master # 同步远端分支到本地 +git remote -v # 查看远端仓库 +git remote set-url origin # 修改远端仓库地址 +``` + +`git checkout -b test` 执行成功后会提示: + +```text +Switched to a new branch 'test' +``` + +### 3.5 提交新分支并创建 PR + +在 `test` 分支完成修改后,按以下步骤提交并推送到远端: + +```bash +git add README.md +git add ./ +git commit -m "second commit" +git push origin test +``` + +`git push origin test` 的输出示例: + +```text +Enumerating objects: 5, done. +Counting objects: 100% (5/5), done. +Delta compression using up to 16 threads +Compressing objects: 100% (3/3), done. +Writing objects: 100% (3/3), 324 bytes | 324.00 KiB/s, done. +Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) +remote: Powered by GITEE.COM +remote: Create a pull request for 'test' on Gitee by visiting: +remote: https://gitee.com///pull/new/:test...:master +To + * [new branch] test -> test +``` + +推送完成后,在 Gitee 仓库页面创建 Pull Request: + +1. **切换到目标分支**:在仓库页面顶部的分支下拉框中选择 `test`。 +2. **点击“贡献代码”**:页面会提示“当前分支比 master 领先 1 次提交”。 +3. **点击“创建 Pull Request”**:填写标题和描述后提交,等待仓库维护者审查合并。 + +页面中可看到该分支的提交记录,示例如下: + +| 提交人 | 提交信息 | Commit ID | 时间 | +| :-- | :-- | :-- | :-- | +| 你(或贡献者) | second commit | `71eebe9` | 2 分钟前 | + +README 预览中可能显示: + +```text +这是我的 git 测试仓库 +这是我的第二次修改 +``` + +### 3.6 PR(Pull Request)规范流程 + +1. **Fork** 统一仓库到个人账号,clone 到本地。 +2. 在**个人仓库**新建分支,完成当天笔记/作业(建 `笔记/` 与 `作业/` 文件夹,内容 Markdown 格式)。 +3. 提交并推送到个人仓库对应分支,发起 **PR** 到统一仓库。 +4. **小组长/导师审核** → 审核通过后合并分支。 +5. **冲突解决**:在冲突分支执行 `git merge` / `git rebase` 同步最新远端,手动选择保留哪一侧修改后继续提交。 + +### 3.7 如何解决 Git 冲突 + +在多人协作中,冲突通常发生在以下场景: + +**冲突产生的原因(时间线示意)** + +- **t1 时刻**:`master`、`fork1`、`fork2` 都基于同一份代码,状态为 t1。 +- **t2 时刻**: + - `fork1` 修改后合并到 `master`,`master` 由 t1 更新为 t2。 + - `fork2` 仍保留 t1 时刻的内容。 + +此时 `fork2` 向 `master` 创建 Pull Request,会发现目标分支 `master` 已经更新到 t2,而 `fork2` 自己的提交仍然基于旧的 t1。由于两者的修改基础不同,就会产生冲突。 + +**解决冲突的步骤** + +1. **Fork 仓库后同步上游**:定期将上游(upstream)仓库的最新改动同步到自己的 Fork。 +2. 切换到产生冲突的分支,执行 `git merge` 或 `git rebase`,把远端的最新修改同步到当前分支: + + ```bash + git switch your-branch + git merge master # 或 git rebase master + ``` + +3. 根据提示手动解决冲突文件,选择保留旧时间线或新时间线的修改。 +4. 解决后继续提交并推送: + + ```bash + git add . + git commit -m "resolve conflict" + git push origin your-branch + ``` diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\347\254\224\350\256\260/rtthread-day3-notes.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\347\254\224\350\256\260/rtthread-day3-notes.md" new file mode 100644 index 0000000000000000000000000000000000000000..87f5cabd1370aba34f1401a3d2caab5b27dbd82f --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\347\254\224\350\256\260/rtthread-day3-notes.md" @@ -0,0 +1,231 @@ +# RT-Thread 线程间同步与通信 学习笔记 + +> 涵盖多线程协作中为什么需要同步、进入/退出临界区的四种方式、核心三件套(信号量 / 互斥量 / 事件集)、线程间通信(邮箱 / 消息队列)、ISR 使用限制、对象销毁,以及重点速记。 + +--- + +## 一、为什么需要线程间同步 + +多线程系统中,多个线程协作时,若不加保护地访问共享资源,会导致数据错乱或损坏。 + +- **临界区(Critical Section)**:被多个线程同时访问、必须通过互斥保护的代码段或资源。 +- **全局变量不能直接当作 IPC 媒介**:无法解决竞态条件(race condition)、缓存一致性、原子性问题。 + +**同步 vs 互斥** + +| 概念 | 含义 | 例子 | +| :-- | :-- | :-- | +| 同步 Synchronization | 线程按预定先后次序运行 | A 先采集,B 再处理 | +| 互斥 Mutual Exclusion | 任意时刻最多一个线程访问临界区 | 共享内存只能被一人写 | + +> 互斥可以看作一种特殊的同步。 + +--- + +## 二、进入/退出临界区的四种方式(从重到轻) + +| 方式 | 说明 | 关键 API | +| :-- | :-- | :-- | +| 1. 关闭全局中断 | 最重,屏蔽所有中断,仅适合**极短**临界区 | `rt_base_t level = rt_hw_interrupt_disable(); ... rt_hw_interrupt_enable(level);` | +| 2. 调度器锁 | 较重,阻止线程调度但中断仍可响应;**可嵌套**(嵌套深度用 `rt_critical_level()` 查询) | `rt_enter_critical()` / `rt_exit_critical()` | +| 3. 信号量 Semaphore | 轻量级内核对象,见第三节 | `rt_sem_take()` / `rt_sem_release()` | +| 4. 互斥量 Mutex | 特殊的二值信号量,解决优先级反转,见第三节 | `rt_mutex_take()` / `rt_mutex_release()` | + +> 方式 1、2 属于**底层(硬件 / 调度器级)**的临界区保护:屏蔽或暂停的是"执行权"本身,系统基本不为线程服务。方式 3、4 属于**软件级(内核 IPC 对象)**的资源保护:线程只是被挂起等待,系统仍在正常调度运行。前者最"重"、开销与风险最大,后者更轻量、可控,是日常多用的方案。 + +--- + +## 三、核心三件套:信号量 / 互斥量 / 事件集 + +### 3.1 信号量(Semaphore) + +- **本质**:基于计数器的同步机制,每个对象有一个计数值 `value` 和一个线程等待队列。 +- **特性** + - 计数值范围:**0 ~ 65535** + - 支持阻塞 / 非阻塞获取 + - 等待模式:`RT_IPC_FLAG_FIFO`(先进先出)、`RT_IPC_FLAG_PRIO`(按优先级) + - **允许在中断上下文释放**(与互斥量的关键区别) +- **停车场模型** + - `value = N` → 有 N 个空位,线程可进入 + - `value = 0` → 已满,新线程挂起到等待队列 + - 线程离开(释放)→ `value++`,唤醒等待队列中的一个线程 +- **核心 API** + + ```c + rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag); + + rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time); + /* time: RT_WAITING_FOREVER 永久阻塞 / RT_WAITING_NO 非阻塞立即返回 / N 等待 N 个 tick */ + + rt_err_t rt_sem_release(rt_sem_t sem); + ``` + +- **四大应用场景** + 1. **线程同步** — 线程 A 完成工作后 `release`,线程 B `take` 等待。 + 2. **锁** — 二值信号量(`value=0/1`)保护临界区(不推荐,优先用互斥量)。 + 3. **中断与线程同步** — ISR 中 `rt_sem_release()`,线程中 `rt_sem_take()`。 + 4. **资源计数** — `value=N` 表示 N 个同类资源可用。 + +### 3.2 互斥量(Mutex)—— 解决优先级反转 + +- **本质**:一种特殊的二值信号量,但有三个关键差异: + +| 特性 | 信号量 | 互斥量 | +| :-- | :-- | :-- | +| 计数值 | 0 ~ 65535 | 仅 0 或 1 | +| 释放者 | 任何线程都可释放 | 必须由持有线程释放 | +| 递归获取 | 不支持(会死锁) | 支持(持有线程可重复获取) | +| 优先级反转 | 存在此问题 | 有优先级继承机制解决 | + +- **优先级继承原理** + 1. 低优先级线程 A 持有锁。 + 2. 高优先级线程 C 请求锁 → 被挂起。 + 3. 系统临时将 A 的优先级提升到 C 的级别 → 防止中优先级线程 B 抢占 A。 + 4. A 尽快执行完临界区并释放锁 → 恢复原始优先级,C 获得锁。 +- **核心 API** + + ```c + rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag); + + rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time); + + rt_err_t rt_mutex_release(rt_mutex_t mutex); /* 必须由持有者调用 */ + ``` + +- **使用原则**:保护临界区、防止优先级反转 → 用互斥量;资源计数、ISR 通知线程 → 用信号量。 + +### 3.3 事件集(Event) + +- **本质**:一个 32 位无符号整数,每一位代表一个事件,可实现**一对多、多对多**的同步。 +- **特性** + - 一个线程可以等"任意一个事件"(逻辑**或**)或"全部事件都到达"(逻辑**与**)。 + - 一个事件可以唤醒多个等待线程。 + - 适合多事件组合触发场景。 +- **核心 API** + + ```c + rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set); + + rt_err_t rt_event_recv(rt_event_t event, + rt_uint32_t set, + rt_uint8_t option, + rt_int32_t timeout, + rt_uint32_t *recved); + /* option: RT_EVENT_FLAG_AND / RT_EVENT_FLAG_OR / RT_EVENT_FLAG_CLEAR */ + ``` + +- **示例(多事件组合)** + - 线程 3:每计数 10 次发送信号量。 + - 线程 4:收到信号量后对 `number+1`,加到 10 后亮蓝灯并发送**事件 5**。 + - 线程 5:用 `rt_event_recv()` 同时等**事件 3 和事件 5 都到达**,才报告 `"all over"`。 + +--- + +## 四、延伸:线程间通信(传数据) + +### 4.1 邮箱(Mailbox) + +- 专为传递小规模数据(通常是一个 `unsigned long`)设计。 +- 内部由**环形缓冲区 + 等待链表 + 超时定时器**实现。 +- **容量由 `rt_mb_create(name, size, flag)` 的 `size` 参数指定**(即邮箱可容纳的邮件封数),并非固定 5 个;每封邮件固定为 4 字节(一个 `unsigned long`)。 +- 例:`rt_mb_create("mbox", 5, ...)` 创建 5 封容量的邮箱,也可设为 32 等任意值,看应用需要。 +- **核心 API** + + ```c + rt_mailbox_t rt_mb_create(const char *name, rt_size_t size, rt_uint8_t flag); + + rt_err_t rt_mb_send(rt_mailbox_t mb, rt_ubase_t value); /* 发送一封邮件(value 即 4 字节数据) */ + rt_err_t rt_mb_send_wait(rt_mailbox_t mb, rt_ubase_t value, rt_int32_t timeout); /* 满时按超时等待 */ + rt_err_t rt_mb_recv(rt_mailbox_t mb, rt_ubase_t *value, rt_int32_t timeout); /* 接收一封邮件 */ + ``` + +### 4.2 消息队列(Message Queue) + +- 由固定数量的**消息块**组成,每块存固定长度数据(`msg_size`)。 +- 维护两个独立等待链表:`Send List`(等写入)和 `Receive List`(等读取)。 +- 写操作:写入**队尾**;读操作:从**队首**读取。`rt_mq_urgent()` 可把消息插到**队首**(优先被取走)。 +- 多线程竞争时支持 **FIFO 或 PRIO** 策略。 +- 唤醒机制:写入时唤醒 `Receive List` 队首线程;读出时唤醒 `Send List` 队首线程。 +- **核心 API** + + ```c + rt_mq_t rt_mq_create(const char *name, rt_size_t msg_size, rt_size_t max_msgs, rt_uint8_t flag); + /* msg_size: 单条消息字节数;max_msgs: 队列可容纳的消息条数 */ + + rt_err_t rt_mq_send(rt_mq_t mq, const void *buffer, rt_size_t size); /* 写入队尾,size <= msg_size */ + rt_err_t rt_mq_send_wait(rt_mq_t mq, const void *buffer, rt_size_t size, rt_int32_t timeout); + rt_err_t rt_mq_urgent(rt_mq_t mq, const void *buffer, rt_size_t size); /* 插到队首 */ + rt_err_t rt_mq_recv(rt_mq_t mq, void *buffer, rt_size_t size, rt_int32_t timeout); /* 从队首读取 */ + ``` + +### 4.3 邮箱 vs 消息队列:何时用哪个 + +| 维度 | 邮箱 Mailbox | 消息队列 Message Queue | +| :-- | :-- | :-- | +| 数据单元 | 固定 4 字节(`unsigned long`,可存指针 / 小整数 / 标志) | 可配置大小(创建时 `msg_size`),适合多字节 / 结构体 | +| 底层结构 | 环形缓冲区存 `value` | 多个固定大小消息块 | +| 典型用途 | 事件通知、传递句柄 / 地址 / 计数 | 传感器数据帧、命令包、变长数据 | +| 容量含义 | `rt_mb_create` 的 `size`(邮件封数) | `rt_mq_create` 的 `max_msgs`(消息条数) | + +> 经验法则:只传一个"信号 + 一个指针/编号"用邮箱最省事;要传一段连续数据或多字节结构体,用消息队列。 + +--- + +## 五、重点速记 + +**同步 vs 互斥** + +> 同步 = 控制执行顺序;互斥 = 临界区排他访问(互斥是一种特殊同步)。 + +**三件套选择** + +| 机制 | 适用场景 | +| :-- | :-- | +| 信号量 | 资源计数 / 线程同步 / ISR→线程通知 | +| 互斥量 | 保护临界区 / 防优先级反转(支持递归、仅持有者释放) | +| 事件集 | 一对多、多对多同步(32 位事件位图,AND/OR 逻辑) | + +**关键数字** + +| 项 | 值 | +| :-- | :-- | +| 信号量计数值范围 | 0 ~ 65535 | +| 事件集 | 32 位,每位一个事件 | +| 邮箱 | 每封 4 字节(`unsigned long`);容量由 `rt_mb_create` 的 `size` 参数指定(非固定 5) | + +**等待模式** + +- `RT_IPC_FLAG_FIFO` → 先进先出 +- `RT_IPC_FLAG_PRIO` → 高优先级线程先获取 + +**超时参数** + +- `RT_WAITING_FOREVER` → 永久阻塞 +- `RT_WAITING_NO` → 非阻塞立即返回 +- `N` → 等待 N 个 tick + +**ISR(中断上下文)使用限制** + +> 中断服务程序里**只能用"非阻塞的发送 / 释放"类接口**(接收方在普通线程里等);任何会挂起等待的"接收 / 获取"接口都**不能**在 ISR 中调用。 + +| 机制 | ISR 内可 send / release? | ISR 内可 recv / take? | 说明 | +| :-- | :-- | :-- | :-- | +| 信号量 | 可以(`rt_sem_release`) | 不可以(`rt_sem_take` 会阻塞) | release 无等待,可用 | +| 互斥量 | 完全不能 | 完全不能 | 互斥量依赖线程优先级继承,ISR 无线程上下文 | +| 事件集 | 可以(`rt_event_send`) | 不可以(`rt_event_recv` 会阻塞) | | +| 邮箱 | 可以(`rt_mb_send`,可带超时) | 不可以(`rt_mb_recv` 会阻塞) | | +| 消息队列 | 可以(`rt_mq_send`,可带超时) | 不可以(`rt_mq_recv` 会阻塞) | | + +**对象销毁(delete / detach)** + +- **动态创建**(`*_create`)配 `rt_xxx_delete`:释放内核对象并回收其占用的内存。 +- **静态初始化**(`*_init`)配 `rt_xxx_detach`:仅把对象从内核脱离,**不**释放静态内存(内存由调用者管理)。 +- 对应 API:`rt_sem_delete` / `rt_mutex_delete` / `rt_event_delete` / `rt_mb_delete` / `rt_mq_delete`(以及同名 `_detach` 版本,如 `rt_sem_detach`)。 + +**易错点** + +1. 信号量可由**任何线程**释放,互斥量只能由**持有线程**释放。 +2. 互斥量支持**递归获取**,信号量不支持(递归获取会死锁)。 +3. 信号量可在**中断中释放**,互斥量**不能**(ISR 中完全不可用互斥量)。 +4. 全局变量**不能**直接用于 IPC——无法解决竞态条件。 +5. **多把互斥量必须按统一顺序加锁**,否则易出现死锁(例如线程 1 持有 A 等 B、线程 2 持有 B 等 A,互相等待)。 diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\347\254\224\350\256\260/rtthread-day2-notes 1.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\347\254\224\350\256\260/rtthread-day2-notes 1.md" new file mode 100644 index 0000000000000000000000000000000000000000..b741cc45ea6db9f8549e8c6a76877cf95640e275 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\347\254\224\350\256\260/rtthread-day2-notes 1.md" @@ -0,0 +1,302 @@ + + +## 一、裸机开发模式及其缺陷 + +- **前后台模式** + - **后台**:主循环(如 `while(1)`)按顺序执行任务(读取 ADC、刷新 LCD 等)。 + - **前台**:中断处理(如 SPI、串口接收中断),打断后台执行。 +- **主要缺陷** + - **并发性**:缺乏真正的并发处理能力,阻塞操作会卡住整个系统。 + - **实时性**:后台任务阻塞(如 `delay`)会严重影响系统响应。 + - **可维护性**:代码与硬件强绑定,移植困难。 + - **可重用性**:模块化程度低,重复造轮子。 + +--- + +## 二、RTOS 引入及线程概念 + +- **引入目的**:通过线程(任务)机制解决裸机缺陷。 +- **核心思想**:**分而治之**——将复杂任务拆分为多个独立线程,实现并发执行。 +- **示例**:ADC 读取、SPI 通信等可拆分为独立线程,避免相互阻塞。 + +--- + +## 三、RTOS 启动流程 + +1. **芯片上电**:执行启动文件(如 `startup.s`)中的 `Reset_Handler`。 +2. **底层初始化**:清 BSS 段、初始化 SRAM 等芯片级操作。 +3. **工具链层**: + - 关闭硬件总中断,保证启动稳定。 + - 调用入口函数(如 `entry`),完成系统初始化。 +4. **关键初始化步骤**: + - 初始化 UART(用于终端打印)。 + - 初始化定时器(提供延时计时)。 + - 初始化调度器(管理线程运行)。 + - 可选:信号初始化。 + - 应用线程初始化。 +5. **自动初始化**:RT-Thread 通过自动初始化机制(`INIT_*_EXPORT` 宏 + 初始化函数表,由 `rt_components` 组件负责扫描执行)在 `main` 函数之前自动调用各组件的初始化函数,无需手动调用。 +6. **调度器启动**:系统开始按优先级调度线程。 + +> **自动初始化宏分级**(按执行先后):`INIT_BOARD_EXPORT` → `INIT_DEVICE_EXPORT` → `INIT_COMPONENT_EXPORT` → `INIT_ENV_EXPORT` → `INIT_APP_EXPORT`。板级(board)最早执行,应用层(app)最后执行。 + +--- + +## 四、线程控制块(TCB) + +- **作用**:线程的"身份证"和"档案袋",记录线程所有关键信息。 +- **关键成员**: + - `sp`(栈指针):指向线程独立栈空间,记录栈大小和起始地址。 + - `entry`(入口函数):线程执行的起点。 + - `parameter`:传递给入口函数的参数。 + - 错误代码、运行状态、优先级、退出钩子函数、定时器等。 +- **面向对象思想**:用 C 语言模拟继承,`rt_thread` 结构体嵌套 `rt_object` 基类,提供统一属性(name、type、flag 等)。 + +--- + +## 五、线程栈与上下文切换 + +- **独立栈空间**:每个线程拥有自己的栈,用于保存局部变量和函数调用。 +- **栈溢出**:RTOS 中特指线程栈溢出,需注意栈大小分配(栈太小会越界破坏其他内存)。 +- **上下文切换**: + - 切换时保存当前线程现场(寄存器、程序计数器等)到自身栈中。 + - 恢复下一个线程的上下文,实现无缝接续执行。 + +--- + +## 六、线程状态及生命周期 + +- **五种状态**: + 1. **初始态**:线程刚被创建,未启动。 + 2. **就绪态**:等待 CPU 资源。 + 3. **运行态**:正在占用 CPU 执行。 + 4. **挂起态**:因延时、信号量等待等暂时让出 CPU。 + 5. **关闭态**:线程结束,等待资源回收。 +- **状态转换**: + - 创建 → 初始态 → 调用 `rt_thread_startup()` → 就绪态 → 调度运行 → 运行态。 + - 运行态可通过 `delay`、信号量获取等进入挂起态,挂起结束后恢复就绪态。 + - 线程退出后进入关闭态,由 **idle 线程** 回收资源。 +- **idle 线程(空闲线程)**: + - 最低优先级线程,系统"兜底"线程。默认 256 级优先级时为 **255**,裁剪为 32 级时对应 **31**;该优先级固定保留,**用户线程不可使用**。 + - 永远处于就绪态,仅当无其他就绪线程时运行。 + - 负责资源回收(清理关闭态线程)、低功耗管理、喂狗等。 + +--- + +## 七、线程调度策略 + +- **抢占式优先级调度** + - 优先级数字越小等级越高:**0 为最高,255 为最低**。 + - 高优先级线程一旦就绪,立即抢占低优先级线程。 + - 典型默认优先级:main 线程(如 10)、timer 线程(如 4)、idle 线程(最低,固定 255)。 +- **时间片轮转** + - 仅适用于**同优先级**线程。 + - 每个线程分配时间片(如 10ms),超时后让出 CPU 给同优先级下一个线程,实现公平分时。 +- **调度触发** + - 调度器启动后,从就绪列表选取最高优先级线程运行。 + - 实际线程切换由 **PendSV 异常**完成(在中断退出时触发,保证不会打断其他中断)。 + +### 7.1 优先级与时间片数值详解 + +- **优先级范围**:系统支持 0~255 共 256 个优先级级别,**0 最高、255 最低**;可通过 `rtconfig.h` 裁剪为 **8 级或 32 级**,以节省资源,配置灵活。 +- **idle 独占最低优先级**:最低优先级固定保留给 idle 线程,用户线程不可占用。 +- **抢占规则**:高优先级就绪即抢占。例:任务 A 优先级 0、任务 B 优先级 10,若 A 就绪,B 会被立刻换出;只有 A 执行完且无更高优先级任务时,B 才能继续。 +- **时间片单位 tick**:1 tick 默认 **10ms**(可在 `rtconfig.h` 改为 1ms 等)。 +- **同优先级轮转**:优先级相同的线程按各自时间片轮转。例:线程 A、B 优先级均为 10,A 时间片 20 tick、B 时间片 100 tick,则 CPU 按 `A→B→A→B…` 轮流分配。时间片设置过小会导致**频繁切换**反而降低效率,需权衡。 + +--- + +## 八、线程创建方式 + +RT-Thread 提供两种创建方式,本质都是分配"线程控制块 + 栈空间",区别仅在内存来源。 + +- **静态创建**(`rt_thread_init`): + - 预先分配线程控制块和栈空间(如全局数组)。 + - 优点:内存分配确定、无碎片,适合资源受限或高安全场景(如军工)。 +- **动态创建**(`rt_thread_create`): + - 从系统堆中动态申请控制块和栈。 + - 优点:灵活,但存在内存碎片风险与不确定性。 +- **创建后**:线程处于初始态,必须调用 `rt_thread_startup()` 进入就绪态。 + +### 8.1 代码示例 + +动态创建(从堆分配): + +```c +#include + +static void thread_entry(void *parameter) +{ + rt_uint32_t count = 0; + while (1) + { + rt_kprintf("dynamic thread count: %d\n", count++); + rt_thread_mdelay(500); /* 延时 500ms,单位毫秒 */ + } +} + +int main(void) +{ + rt_thread_t tid; + + tid = rt_thread_create("dynamic", /* 线程名称(用于调试显示) */ + thread_entry, /* 线程入口函数 */ + RT_NULL, /* 入口参数 */ + 512, /* 线程栈大小(字节) */ + 10, /* 优先级:数字越小越高 */ + 10); /* 时间片(单位 tick) */ + + if (tid != RT_NULL) + rt_thread_startup(tid); /* 启动线程,进入就绪态 */ + else + rt_kprintf("create thread failed!\n"); + + return 0; +} +``` + +静态创建(使用预先分配的数组): + +```c +#include + +#define THREAD_STACK_SIZE 512 +#define THREAD_PRIORITY 10 +#define THREAD_TIMESLICE 10 + +static struct rt_thread static_thread; /* 预先分配的控制块 */ +static rt_uint8_t static_thread_stack[THREAD_STACK_SIZE]; /* 预先分配的栈 */ + +static void thread_entry(void *parameter) +{ + rt_uint32_t count = 0; + while (1) + { + rt_kprintf("static thread count: %d\n", count++); + rt_thread_mdelay(500); + } +} + +int main(void) +{ + rt_err_t result; + + result = rt_thread_init(&static_thread, /* 线程控制块 */ + "static", /* 线程名称 */ + thread_entry, /* 入口函数 */ + RT_NULL, /* 入口参数 */ + static_thread_stack, /* 栈起始地址 */ + THREAD_STACK_SIZE, /* 栈大小 */ + THREAD_PRIORITY, /* 优先级 */ + THREAD_TIMESLICE); /* 时间片 */ + + if (result == RT_EOK) + rt_thread_startup(&static_thread); + else + rt_kprintf("init thread failed!\n"); + + return 0; +} +``` + +> 注意:静态方式要求栈数组为**全局/静态**变量(生命周期与线程一致);动态方式需保证系统堆(`RT_HEAP_SIZE`)足够。 + +### 8.2 常用线程管理 API + +| API | 作用 | +| :-- | :-- | +| `rt_thread_create()` | 动态创建线程(从堆分配) | +| `rt_thread_init()` | 静态初始化线程(使用指定内存) | +| `rt_thread_startup()` | 启动线程,使其进入就绪态 | +| `rt_thread_delete()` | 删除动态线程(释放堆内存) | +| `rt_thread_detach()` | 脱离静态线程(不释放内存) | +| `rt_thread_yield()` | 主动让出 CPU,切换到同优先级其他线程 | +| `rt_thread_mdelay(ms)` | 毫秒级延时(进入挂起态) | +| `rt_thread_sleep(tick)` | tick 级延时 | +| `rt_thread_self()` | 获取当前线程句柄 | +| `rt_thread_control()` | 控制线程(如修改优先级、挂起/恢复) | +| `rt_thread_suspend()` / `rt_thread_resume()` | 挂起 / 恢复线程 | + +--- + +## 九、SCons 构建系统 + +- **简介**:基于 Python 的构建工具,用于编译 RT-Thread 项目,替代繁琐的 Makefile。 +- **核心文件** + - `SConstruct`:顶层构建脚本。 + - `SConscript`:分散在各源码目录,控制当前目录的构建行为。 +- **常用命令** + - `scons`:编译项目。 + - `scons -jN`:多线程加速(如 `-j20`)。 + - `scons -c`:清理编译产物。 + - `scons --target=mdk` / `--target=cmake`:生成 IDE 工程文件。 +- **关键语法** + - `glob`:匹配源文件。 + - `DefineGroup`:添加文件分组,精细控制参与编译的文件。 + - `depend`:依赖宏控制脚本是否生效(宏需在 `rtconfig.h` 中启用)。 + - `rtconfig.h`:全局配置,由 Kconfig 生成,集中管理所有宏。 + +### 9.1 实战避坑指南 + +- **工具链环境变量 `RTT_EXEC_PATH`**:SCons 通过该变量定位编译器。使用非 ARM 平台或自定义编译器时需手动指定: + - Windows(CMD):`set RTT_EXEC_PATH=编译器路径` 后执行 `scons`。 + - Linux/macOS(bash):`export RTT_EXEC_PATH=编译器路径`。 + - 未正确设置会报"找不到编译器"等错误。 +- **`rtconfig.py` 切换编译器**:编译器和参数统一在此配置,需按工具链(GCC / Keil AC5/AC6 / IAR / LLVM)设置 `ARCH`(架构)、`CPU`(指令集)、`FPU`(浮点类型)等,否则编译结果不正确。 +- **修改配置后先清理再编译**:改了 `rtconfig.h` 或 `SConscript` 后若出现"奇怪的错误"或配置未生效,先 `scons -c` 清理旧产物再 `scons` 重编,通常能解决。 +- **生成 IDE 工程**:`--target=mdk` 生成 Keil 工程;`--target=cmake` 生成 CMake 脚本可导入 VS Code / VS / CLion;生成 IAR 工程需本机已装 IAR。 +- **跨平台路径**:Windows 用 `\`、Linux/macOS 用 `/`;在 `SConscript` 中建议用 Python `os.path` 处理路径。Linux 下注意脚本可执行权限。 + +--- + +## 十、开发环境与实践要点 + +- **RT-Thread Studio**:集成开发环境,支持图形化配置、软件包管理。 +- **工程配置**:通过 `rtconfig.h` 和 `menuconfig` 调整功能。 +- **软件包**:云端拉取,如 `Kernel Simple`,丰富外设驱动和组件。 +- **调试技巧**: + - 使用 `list_thread` 或 `ps` 命令查看线程状态。 + - Tab 键补全命令。 + - 注意下载器(ST-Link / J-Link)和串口的匹配。 + +### 10.1 GPIO 设备模型与 MSH 命令导出(LED 实战示例) + +翻转 LED 本质是操作引脚设备。RT-Thread 提供统一的 PIN 设备接口,无需直接操作寄存器: + +```c +#include +#include +#include + +/* 以 STM32 为例,PC13 接 LED;请按开发板原理图替换为实际引脚编号 */ +#define RED_LED_PIN GET_PIN(C, 13) + +/* 动态线程:翻转红色 LED(对应作业"优先级 16") */ +static void red_led_entry(void *parameter) +{ + rt_pin_mode(RED_LED_PIN, PIN_MODE_OUTPUT); /* 配置为推挽输出 */ + while (1) + { + rt_pin_write(RED_LED_PIN, PIN_HIGH); /* 点亮 */ + rt_thread_mdelay(500); + rt_pin_write(RED_LED_PIN, PIN_LOW); /* 熄灭 */ + rt_thread_mdelay(500); + } +} + +int led_sample(void) +{ + rt_thread_t tid = rt_thread_create("red_led", red_led_entry, + RT_NULL, 512, 16, 10); + if (tid != RT_NULL) + rt_thread_startup(tid); + return 0; +} + +/* 导出为 MSH 命令:在终端输入 led_sample 即可运行 */ +MSH_CMD_EXPORT(led_sample, toggle red led by thread); +``` + +> - 静态线程(作业中的"蓝色 LED")只需把上例的 `rt_thread_create` 换成 `rt_thread_init` + 预分配栈即可。 +> - `GET_PIN(port, pin)` 是 RT-Thread 的引脚编号宏;`PIN_HIGH`/`PIN_LOW`/`PIN_MODE_OUTPUT` 为设备框架定义的常量。 +> - 除 MSH 导出外,也可通过 `INIT_APP_EXPORT(led_sample)` 让函数在开机时自动执行。 + diff --git "a/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260/rtthread-day4-notes.md" "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260/rtthread-day4-notes.md" new file mode 100644 index 0000000000000000000000000000000000000000..ae1ee82644510d8d59872aec3e75ba4390d3bb99 --- /dev/null +++ "b/2026/\347\254\2541\347\273\204/\350\265\265\345\276\267\346\254\242/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260/rtthread-day4-notes.md" @@ -0,0 +1,239 @@ +# RT-Thread 大学生夏令营 Day4 · 设备驱动框架学习笔记 + +## 一、IO 设备驱动框架 + +### 1.1 背景与动机 + +- 不同厂商的 SPI / I2C 等接口 **API 不统一**,更换芯片时需重复开发,降低效率、难以复用。 +- RT-Thread 通过**抽象层将上层应用与底层硬件驱动解耦**:上层用统一 API(`open / read / write / control / close`)操作设备,底层适配各厂商驱动。 +- 核心优势:上层开发成本低、易跨 MCU 移植、驱动可复用、减少"造轮子"。 +- 代价:框架更复杂、Flash 占用略有增加——但优点远大于缺点。 + +### 1.2 框架的分层架构 + +``` +应用层 API + ↓ +IO 设备管理层(rt_device_xxx) + ↓ +设备驱动框架层(SPI / I2C / GPIO / Pin ...) + ↓ +设备驱动层(各厂商 HAL / LL 库实现) + ↓ +硬件 +``` + +- 所有设备继承自 `rt_device` 基类,采用面向对象思路。 +- **核心纽带是 `rt_device_ops` 结构体**:封装 `init / open / read / write / control / close` 等函数指针,下层驱动实现这些接口,上层通过统一 API 调用——它是"上层统一调用、下层各自实现"的解耦点。 +- 演进:从早期仅统一 SPI API → 统一为 IO 设备层 → 形成 SPI / I2C / GPIO 等多设备并行的架构。 +- **并行结构**:既可走统一 IO 层调用,也可直接使用各设备专用接口(如 SPI 专用读写 / 传输函数)。 + +### 1.3 设备创建、注册与使用流程 + +| 步骤 | 关键 API / 操作 | +| :-- | :-- | +| 创建设备 | `rt_device_create(type, ...)` | +| 注册到 IO 管理系统 | `rt_device_register(device, "设备名", 打开标志)` | +| 查找设备 | `rt_device_find("设备名")` → 返回设备句柄 | +| 初始化 | `rt_device_init(handle)` | +| 打开设备 | `rt_device_open(handle, 读写模式)` | +| 读写 | `rt_device_read / write(handle, ...)` | +| 控制 | `rt_device_control(handle, ...)` | +| 关闭 | `rt_device_close(handle)` | + +- 注册成功后,终端执行 `list device` 可查看已注册字符设备。 +- **自动初始化机制**:系统在启动时自动执行被 `INIT_*` 宏标记的设备初始化函数,无需手动调用。 +- **错误码规范**:成功返回 `RT_OK`(正数),失败返回负值错误码。 + +### 1.4 关键机制与踩坑要点 + +- **回调机制**:可通过 `rt_device_set_xxx_callback` 注册接收 / 发送回调函数,用于中断 / DMA 非阻塞场景下的数据通知与"发送完成"回执。 +- **打开模式**:若需接收回调,必须以**中断或 DMA 方式**打开设备,避免阻塞。 +- **隐式初始化**:系统会检测设备初始化标志位,未初始化时自动触发 `init` 并置位,防止重复初始化(防重入安全锁)。 +- **典型坑**:读写失败常因"未先 `open` 设备"导致引用计数未增加、设备未真正打开——可先 `list device` 确认状态,补上 `open` 后重新编译下载即可打通。 +- **日志级别**:调试时切勿把普通信息误用 `LOG_E`(会被识别为错误、干扰排查),应使用 `LOG_I`;**中断上下文中禁用阻塞式 `LOG_I`**,须开启 `ulog` 的 ISR / 异步输出功能。 + +--- + +## 二、GPIO 设备驱动开发实践 + +### 2.1 GPIO 基本特性 + +- MCU 引脚具备**功能复用**特性,可配置为电源、时钟、通用 IO 等。 +- 支持多种中断触发模式(上升沿 / 下降沿 / 双边沿等)。 +- 引脚模式多种(输出、输入上拉 / 下拉、开漏输出等),需根据电路特性精准选择。 + +### 2.2 GPIO 主要 API + +通过设备管理接口操作 GPIO(`rt_pin_xxx`): + +| 函数 | 作用 | +| :-- | :-- | +| `rt_pin_mode(pin, mode)` | 设置引脚模式(输入 / 输出 / 上拉 / 下拉 / 开漏) | +| `rt_pin_read(pin)` | 读引脚电平(仅高 / 低两种状态) | +| `rt_pin_write(pin, value)` | 写引脚电平 | +| `rt_pin_attach_irq(pin, trigger, callback, args)` | 绑定中断回调函数 | +| `rt_pin_detach_irq(pin)` | 解绑中断回调 | + +> `rt_pin_mode` 常用模式宏:`PIN_MODE_OUTPUT`(推挽输出)、`PIN_MODE_INPUT`(浮空输入)、`PIN_MODE_INPUT_PULLUP`(上拉输入)、`PIN_MODE_INPUT_PULLDOWN`(下拉输入)、`PIN_MODE_OUTPUT_OD`(开漏输出)。 + +### 2.3 按键中断实验要点 + +- 按键按下为**低电平**,故配置引脚为**上拉输入模式**,确保未按下时读取高电平。 +- 注册回调函数并设定**下降沿触发**,实现按键按下响应。 +- **按键消抖**:硬件通常无消抖,建议通过软件延时或引入 RT-Thread 的 `button` 软件包(支持单击 / 双击等),参考其 README / 示例即可快速上手。 + +### 2.4 引脚编号计算 + +- 每组 GPIO 含 16 个引脚;如引脚编号 `37` = 前两组(32 个)+ C 组第 5 个引脚(GPIOC5)。 +- 通用公式:**`pin = 组号 × 16 + 组内序号`**(A 组 = 0,B 组 = 1,C 组 = 2 …),例如 C5 → `2 × 16 + 5 = 37`。 +- 采用通用 API 编写应用,可轻松迁移至其他 MCU。 + +### 2.5 PIN 设备与驱动框架的对接关系(调用链) + +RT-Thread 把 GPIO 也纳入了 IO 设备驱动框架,以"Pin 设备"的形式统一管理。其对接分三层: + +| 层级 | 角色 | 关键结构 / 函数 | +| :-- | :-- | :-- | +| 应用层 | 应用调用统一的 Pin API | `rt_pin_mode` / `rt_pin_write` / `rt_pin_read` / `rt_pin_attach_irq` | +| Pin 框架层 | 中转并调用底层实现 | `struct rt_device_pin`(内含 `struct rt_pin_ops *ops`) | +| 板级驱动层 | 实现具体硬件操作 | `drv_gpio.c` 中实现 `rt_pin_ops` 各函数 → HAL / LL 库 | + +- **对接核心**:板级驱动用 `rt_device_pin_register("pin", &pin_ops, ...)` 把实现了 `rt_pin_ops` 函数指针集合的驱动注册为名为 `"pin"` 的设备;`rt_pin_ops` 包含 `pin_mode` / `pin_write` / `pin_read` / `pin_attach_irq` / `pin_detach_irq` / `pin_irq_enable` 等函数指针。 +- **调用链(以设置引脚模式为例)**: + + ```text + rt_pin_mode(pin, mode) + → Pin 框架查找 "pin" 设备(全局句柄) + → 调用 device->ops->pin_mode(device, pin, mode) + → 板级 stm32_pin_mode() // drv_gpio.c + → HAL_GPIO_Init() / LL 库 + → 硬件寄存器 + ``` + +- **中断对接**:`rt_pin_attach_irq()` 把用户回调登记到 Pin 框架的中断回调表;当 GPIO 外部中断(EXTI)触发时,板级 `GPIO_IRQHandler` 进入中断上下文,经 `rt_interrupt_enter()` 后由框架调用已登记的回调函数,实现"按键按下 → 回调响应"的闭环。 + +> 这正是"统一设备 API → 框架层 ops → 驱动层 HAL"解耦思想的体现:应用只认 `rt_pin_xxx`,换 MCU 只需重写板级 `drv_gpio.c` 的 `rt_pin_ops` 实现,上层代码无需改动。 + +--- + +## 三、I2C 外设开发 + +### 3.1 I2C 协议要点(飞利浦 1980 年提出) + +- 三种速率模式:低速 / 标准 / 快速模式。 +- **起始位**:SCL 高电平时 SDA 由高跳低;**停止位**:SCL 高电平时 SDA 由低跳高。 +- **应答机制**:从机拉低 SDA 表示应答(ACK),否则为非应答(NACK)。 +- **地址**:一个字节中前 7 位为设备地址,最低 1 位为读写标志(0 写 / 1 读);常见为 7 位地址,也存在 10 位扩展。 + +### 3.2 RT-Thread 中的 I2C 使用 + +- I2C 为总线结构,一条总线可挂载多个设备(地址不冲突即可),`list device` 可查看 I2C / SPI 总线设备。 +- 访问流程: + 1. `rt_device_find("i2c总线名")` 查找总线设备; + 2. 构建 `struct rt_i2c_msg` 消息结构体(含设备地址、读写标志、数据长度、缓冲区指针); + 3. 调用传输函数完成读写(地址会自动左移 1 位并补读写位)。 +- 读写通过修改消息结构体的 `flag`(读 / 写模式)实现;单字节长度设为 1,多字节传结构体数组。 +- 注意事项:读写操作互斥,不可同时进行;可设置"忽略应答"等标志。 + +`rt_i2c_msg` 结构体关键字段: + +| 字段 | 含义 | +| :-- | :-- | +| `addr` | 从机 7 位地址(框架自动左移并补读写位,应用时填 7 位即可) | +| `flags` | 读写标志(`RT_I2C_RD` 读 / `RT_I2C_WR` 写) | +| `len` | 数据字节数 | +| `buf` | 数据缓冲区指针 | + +### 3.3 硬件 I2C vs 软件模拟 I2C + +- 软件模拟通常带 `sw` 前缀;硬件 I2C 需先配置对应 GPIO 引脚再启用。 +- 当前部分芯片支持包版本较老可能不支持硬件 I2C,可先用软件模拟兜底;启用硬件 I2C 需在 Kconfig / board.h 中开启对应支持。 +- 软件 I2C 驱动已简化:只要设备对接了驱动框架,一般无需额外编写软件 I2C 驱动。 + +--- + +## 四、SPI 外设开发 + +### 4.1 SPI 特点 + +- 同步、全双工通信,速率较高。 +- 系统分为**总线控制器**与挂载的**子设备**,每个设备有独立片选(CS)引脚。 +- 使用流程:先注册 SPI 总线 → 再挂载 SPI 设备 → `rt_device_find` 查找 → 打开 → 调用框架 API 收发。 + +### 4.2 设备命名规则 + +- 总线:`spix`(如 `spi2`); +- 挂载设备:`spi x.y`(x = 总线编号,y = 设备序号),如 `spi2.0` 表示 SPI2 总线上的第 0 号设备; +- 片选引脚信息通过 `user_data` 参数传递(记录具体 GPIO 编号,如 PB12)。 + +### 4.3 配置与使用要点 + +- 获取 SPI 句柄后需配置:工作模式、数据宽度、最大频率(Hz)、字节序(MSB / LSB)、主从模式、时钟相位(采样边沿)、片选极性(默认低有效,部分器件需高有效)。 +- **四种工作模式**(由时钟极性 CPOL 与时钟相位 CPHA 组合),常用**模式 0 与模式 2**(均在时钟第一个边沿采样数据): + +| 模式 | CPOL | CPHA | 采样边沿 | +| :-- | :-- | :-- | :-- | +| 0 | 0 | 0 | 第一个边沿(常用) | +| 1 | 0 | 1 | 第二个边沿 | +| 2 | 1 | 0 | 第一个边沿(常用) | +| 3 | 1 | 1 | 第二个边沿 | + +- SPI 全双工本质:每次发送同时接收数据;`rt_spi_send` / `rt_spi_receive` 底层均封装了双向传输。 +- 三线模式可简化传输(仅需三条线)。 + +--- + +## 五、工程与工具链要点 + +### 5.1 两种工程创建方式对比 + +| 维度 | 基于主线导入 | 基于芯片创建 | +| :-- | :-- | :-- | +| 配置方式 | 直接在 config 中配置 | 需新建工程并手动拷贝引脚初始化函数 | +| SPI 等外设 | 自动配置 | 须在 library / menuconfig 中手动使能 | +| 开发体验 | 更省事 | 链路更长,手动搬运易埋隐患 | + +### 5.2 CubeMX / Qmax 工具 + +- 在 Qmax 中配置引脚后,会自动生成对应引脚初始化代码,设备驱动框架直接调用这些生成的函数(最终调到 HAL 库)。 +- 图形化界面降低手动编写底层代码出错概率;配置 CubeMX 路径本质是让系统识别 Qmax 安装位置(类似环境变量)。 +- 工具会生成大量文件,实际只需关注核心配置文件(如时钟、SPI / ADC 引脚初始化相关)。 + +### 5.3 CubeMX 与 RT-Thread Studio 的两种转换方法 + +RT-Thread Studio 内置了对 STM32CubeMX 的支持。所谓"两种转换方法",本质是把 CubeMX 的图形化配置与 RT-Thread Studio 工程互相打通的两条路径——二者底层机制相同(都由 CubeMX 生成 `cubemx/` 代码,再由 RT-Thread 的构建系统编译链接),区别仅在**起点**。 + +| 方法 | 起点 → 终点 | 典型场景 | +| :-- | :-- | :-- | +| 方法一:CubeMX → Studio | 已有 `.ioc`,搬进 Studio 接管 | 手头有 CubeMX 工程,想接入 RT-Thread | +| 方法二:Studio → CubeMX | 已有 Studio 工程,用 CubeMX 插件增改 | 从 Studio 起步,中途要加 / 改外设 | + +**方法一:CubeMX 工程导入 RT-Thread Studio** + +1. 在 Studio 中新建 / 打开 RT-Thread 工程(基于芯片或 BSP),把 `.ioc` 放到 `board/CubeMX_Config/`(BSP 通常已自带默认 `.ioc`)。 +2. 在 Studio 设置里配置 CubeMX 可执行文件路径(首次需指定,相当于把 CubeMX 安装位置告诉 Studio)。 +3. 右键 `.ioc` → **Open with CubeMX**(或 Studio 的 CubeMX 菜单)启动 CubeMX,图形化配置引脚、时钟、外设。 +4. 点击 **Generate Code**:CubeMX 把代码生成到工程的 `cubemx/`(含 `main.c`、`gpio.c`、`spi.c`、`MX_GPIO_Init()` / `MX_SPI2_Init()` 等 `HAL_*` 初始化函数)。 +5. Studio 的构建系统(SConscript)自动把 `cubemx/` 纳入编译链接。**注意**:CubeMX 生成的 `main()` 不是系统入口(RT-Thread 用自己的入口),真正被调用的是那些 `MX_*` 初始化函数——它们在 `rt_hw_board_init()`(或 `board.c`)中被调用。 +6. 初始化完成后,上层即可用 `rt_pin_*` / `rt_device_*` 设备框架操作外设。 + +**方法二:在 RT-Thread Studio 工程内用 CubeMX 插件增改** + +1. 在 Studio 工程里找到 `board/CubeMX_Config/` 下的 `.ioc`(Studio 建 BSP 工程时会预置)。 +2. 右键 `.ioc` → **Open with CubeMX** 启动 CubeMX。 +3. 勾选 / 修改外设引脚与时钟 → **Generate Code** → 关闭 CubeMX。 +4. 回到 Studio 直接 **Build**,构建系统重新编译 `cubemx/` 新生成的 `MX_*` 初始化。 +5. 在应用 / board 初始化里调用新增的 `MX_*` 函数,并用设备框架注册、使用。 + +> 两种方法共用同一套机制:CubeMX 只负责生成"底层 HAL 初始化",RT-Thread 设备框架叠加其上;改引脚 / 时钟只需重开 CubeMX 重新 Generate,无需手写 `board.h`。与笔记 5.2 的 Qmax 工具作用等价(Qmax 是 RT-Thread 自家的图形化引脚配置工具,可作为 CubeMX 替代);若 CubeMX 不可用,可退回"手动在 `board.h` 写 `MX_*` / HAL 初始化"或改用软件模拟。 + +### 5.4 弱符号机制 + +- 部分底层函数采用**弱符号(`__weak`)**定义,链接器会优先使用用户 / board.h 中的重定义实现。 +- 即在 board.h 中重新实现该函数,最终会调用 board.h 内版本——框架已预留接口,开发者按需重写即可定制。 + +### 5.5 自动初始化机制 + +- 系统在 Flash 特定地址写入初始化函数指针,启动时遍历执行,统一收口管理(空间换时间)。 +- 串口、Pin 脚等需提前初始化以供中断使用;SPI 等其余外设依赖该自动初始化机制。