diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\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\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\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..90865873a8cb1c0993a067436b75befc2d287991 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" @@ -0,0 +1 @@ +两份pr,已完成。 \ No newline at end of file diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..98b77503cbffda3bd58a627310f077cf76763c3e --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,151 @@ +# RT‑Thread 线程间同步与通信实验报告 +> 实验环境:RT‑Thread 4.1.1 +> 源代码文件:`study_sample.c` +> MSH调试命令:`parking_demo`、`ticket_demo`、`startup_demo`、`package_demo` + +## 目录 +1. [练习一:停车场车位管理(计数信号量)](#练习一停车场车位管理计数信号量) +2. [练习二:多窗口售票系统(互斥量)](#练习二多窗口售票系统互斥量) +3. [练习三:系统启动条件检查(事件集)](#练习三系统启动条件检查事件集) +4. [练习四:快递分拣中心(消息队列)](#练习四快递分拣中心消息队列) +5. [思考题汇总](#思考题汇总) +6. [实验踩坑总结](#实验踩坑总结) +7. [文件提交说明](#文件提交说明) + +## 练习一:停车场车位管理(计数信号量) +### 实验场景 +停车场仅有3个车位,存在5台车辆线程竞争车位资源。计数信号量初始值设置为3代表空闲车位。车辆申请车位2秒未获取到则打印超时信息,获取车位后模拟停留一段时间,离开时释放车位,保证停车场内同时车辆不超过3台。 + +### 使用API +- `rt_sem_create()`:创建动态计数信号量 +- `rt_sem_take(sem, tick)`:申请信号量资源,设置超时时间 +- `rt_sem_release()`:释放信号量资源 + +### 核心逻辑 +1. 创建初始值为3的计数信号量; +2. 创建5个车辆线程; +3. 线程调用`rt_sem_take`申请车位,超时时间2000ms;超时打印等待信息; +4. 获取信号量成功,打印车辆进入信息;延时模拟车辆在停车场停留; +5. 停留结束调用`rt_sem_release`释放车位,打印车辆离开; +6. 实验调试读取信号量内部`value`成员计算占用车位(仅调试,产品禁止直接访问内核结构体成员)。 + +### 输出结果 + +![image-20260819231825099](figures/image-20260819231825099.png) + +### 实验现象 +最多同时3台车辆进入停车场,满足资源上限;车辆2s无法获取车位打印超时,之后继续重试申请车位。 + +### 思考题:为什么这里使用计数信号量,而不是互斥量? +互斥量计数只能为1,具备线程所有权,用于排他访问单一临界资源。 +计数信号量支持初始值大于1,适合管理**多个相同的有限资源**。 +停车场拥有3个车位,属于多资源场景,因此选择计数信号量,互斥量只能表示1个车位,无法满足业务。 + +## 练习二:多窗口售票系统(互斥量) +### 实验场景 +两个售票窗口A、B,总票数20张。使用互斥量保护票数读‑判断‑修改完整临界区,避免多线程竞态,防止重复出票、票数小于0;票售罄之后两个线程自动退出。 + +### 使用API +- `rt_mutex_create()`:创建互斥量 +- `rt_mutex_take()`:获取互斥锁 +- `rt_mutex_release()`:释放互斥锁 + +### 核心逻辑 +1. 全局变量保存剩余票数`ticket = 20`; +2. 创建两个售票线程; +3. **读取票数、判断余票、打印票号、票数自减全部放在互斥锁保护范围内**; +4. 票数等于0时,打印售罄信息,释放互斥锁后线程退出; +5. 模拟延时放在锁的外部,不占用临界区。 + +### 输出结果 + +![image-20260819231911030](figures/image-20260819231911030.png) + +### 对比实验 +1. **去掉互斥量**:读取票数与修改票数之间加入短暂延时,会出现重复售票、票数变成负数,多线程竞态条件引发异常。 +2. **添加互斥量**:完整业务流程加锁,20张票全部正常售出,票号唯一,票数不会小于0。 + +### 思考题:为什么只给 `ticket--` 加锁仍可能不安全? +票数读取判断 `if(ticket>0)` 和自减 `ticket--` 是两条独立CPU指令。 +如果仅仅给自减操作加锁,读取判断没有保护,多个线程可以读到同一个票号,造成重复售票。 +> **临界区需要保护完整的读‑判断‑修改业务流程,而不是只保护单一行代码。** + +## 练习三:系统启动条件检查(事件集) +### 实验场景 +设备上电,需要网络模块、传感器模块、存储模块全部初始化完成,业务线程才允许启动。 +- 网络线程延时1000ms发送就绪事件; +- 传感器线程延时2000ms发送就绪事件; +- 存储线程延时5000ms发送就绪事件; +- 业务线程使用`AND`模式等待全部事件,单次最大等待3000ms;超时打印尚未就绪模块,继续循环等待。 + +事件位定义 +```c +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) +``` + +### 使用API +- `rt_event_init()`:静态初始化事件集 +- `rt_event_send()`:发送事件标志位 +- `rt_event_recv()`:等待事件;`RT_EVENT_FLAG_AND`全部位置位才返回成功;`RT_EVENT_FLAG_CLEAR`接收成功自动清除标志。 + +> RT‑Thread4.1.1注意:`RT_EVENT_FLAG_CLEAR`模式超时返回时输出参数`e=0`,需要读取`event.set`获取真实事件标志,超时不会清除事件对象内部标志。 + +### 输出结果 + +![image-20260819231939674](figures/image-20260819231939674.png) + + +### 思考题:如果用三个独立信号量实现这个功能,与使用事件集相比有什么区别? +1. **事件集**:单个对象管理多个bit标志;原生支持`AND`等待全部条件 / `OR`等待任意条件;资源占用小,代码简洁。 +2. **三个独立信号量**:需要创建3个IPC对象;RT‑Thread没有原生API等待“全部信号量到达”,需要手写业务逻辑判断;内存开销更大,代码复杂。 +> 当前场景需要等待多个独立条件全部完成,事件集是最合适组件。 + +## 练习四:快递分拣中心(消息队列) +### 实验场景 +两个发件线程,每个线程生成5条快递消息,合计10条快递;消息队列容量设置为5。分拣线程接收消息,统计总快递数量、各区域快递数量、总重量;队列满时发送线程统计失败次数,不崩溃。 + +消息结构体 +```c +typedef struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; //1华东,2华南,3华北 + rt_uint16_t weight; +}package_msg_t; +``` + +### 使用API +- `rt_mq_init()`:静态初始化消息队列 +- `rt_mq_send()`:发送消息,队列满返回`‑RT_EFULL` +- `rt_mq_recv()`:阻塞接收消息 + +### 核心逻辑 +1. 消息队列容量5,两个发送线程各生成5条快递; +2. 发送线程调用`rt_mq_send`,捕获队列满返回值,统计发送失败次数; +3. 分拣线程循环接收快递消息; +4. 统计总包裹数量、华东/华南/华北各个区域数量、全部快递总重量; +5. 接收满10条快递,打印全部统计结果。 + +### 输出结果 + +![image-20260819232008638](figures/image-20260819232008638.png) + + +## 思考题汇总 +1. **练习一:为什么这里使用计数信号量,而不是互斥量?** +互斥量用于排他访问,所有权绑定线程,计数只能为1;计数信号量可以管理N个相同资源。停车场3个车位属于多资源,适合计数信号量。 + +2. **练习二:为什么只给`ticket--`加锁仍可能不安全?** +票数读取判断和自减是两条独立指令;只保护自减,读取判断无锁,多线程会读到相同票号,发生重复售票;整个读‑判断‑修改业务流程都需要放到临界区锁内。 + +3. **练习三:如果用三个独立信号量实现这个功能,与使用事件集相比有什么区别?** +事件集单对象管理多bit,原生支持AND等待全部条件,代码简洁;3个独立信号量没有“等待全部条件”原生接口,需要手动写逻辑,资源开销更大,实现繁琐。 + +## 实验踩坑总结 +1. IPC接口超时返回值为负数,超时判断必须写`‑RT_ETIMEOUT`,不能直接写`RT_ETIMEOUT`。 +2. 函数内部**栈局部变量不会自动清零,必须手动初始化=0**,否则统计计数得到随机脏数据。 +3. `switch‑case`每一个case末尾必须添加`break`,防止case穿透,多条分支连续执行。 +4. 互斥量:`break`退出循环之前必须释放互斥锁,禁止持有锁直接跳出循环。 +5. 事件集:`RT_EVENT_FLAG_CLEAR`模式超时返回输出参数e等于0,不能用e做超时判断,读取`event.set`获取标志。 \ No newline at end of file diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231825099.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231825099.png" new file mode 100644 index 0000000000000000000000000000000000000000..08048b064d8db1edb8e92eff806a874f83dbbf71 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231825099.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231911030.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231911030.png" new file mode 100644 index 0000000000000000000000000000000000000000..851772b230738a2ead9b2598143a4933a19f1e21 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231911030.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231939674.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231939674.png" new file mode 100644 index 0000000000000000000000000000000000000000..81c4b0b6d6420081af7a2963de976d56cf2547b4 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819231939674.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819232008638.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819232008638.png" new file mode 100644 index 0000000000000000000000000000000000000000..b5ad7ab75f8a03d43de9cb61e326b6047a7b9de8 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/figures/image-20260819232008638.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/study.c" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/study.c" new file mode 100644 index 0000000000000000000000000000000000000000..104a702182c57da141711103ab1872b8283210a0 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/study.c" @@ -0,0 +1,774 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-19 yanran the first version + */ + +#include + +#define TASK1_THREAD_PRIORITY 25 +#define TASK1_THREAD_TIMESLICE 5 +static rt_sem_t task1_sem = RT_NULL; + +static char thread1_stack[1024]; +static struct rt_thread thread1; + +static void rt_thread1_entry(void *parameter) +{ + static rt_err_t result; + static rt_uint8_t number = 0; + while(1) + { + + if(0 == number) + { + + result = rt_sem_take(task1_sem, rt_tick_from_millisecond(2000)); + if (result == -RT_ETIMEOUT) + { + rt_kprintf("car1 waiting for a space\n"); + rt_thread_mdelay(1000); + } + else if (result == RT_EOK) + { + number = 1; + rt_kprintf("car1 entered, used spaces: %d\n",3 - (task1_sem->value)); + rt_thread_mdelay(2000); + } + } + + + if(number == 1) + { + number = 0; + rt_sem_release(task1_sem); + rt_kprintf("car1 left\n"); + rt_thread_mdelay(500); + } + } +} + +static char thread2_stack[1024]; +static struct rt_thread thread2; +static void rt_thread2_entry(void *parameter) +{ + static rt_err_t result; + static rt_uint8_t number = 0; + while(1) + { + + if(0 == number) + { + + result = rt_sem_take(task1_sem, rt_tick_from_millisecond(2000)); + if (result == -RT_ETIMEOUT) + { + rt_kprintf("car2 waiting for a space\n"); + rt_thread_mdelay(2000); + } + else if (result == RT_EOK) + { + number = 1; + rt_kprintf("car2 entered, used spaces: %d\n",3 - (task1_sem->value)); + rt_thread_mdelay(4000); + } + } + + if(number == 1) + { + number = 0; + rt_sem_release(task1_sem); + rt_kprintf("car2 left\n"); + rt_thread_mdelay(1000); + } + } +} + +static char thread3_stack[1024]; +static struct rt_thread thread3; +static void rt_thread3_entry(void *parameter) +{ + static rt_err_t result; + static rt_uint8_t number = 0; + while(1) + { + + if(0 == number) + { + + result = rt_sem_take(task1_sem, rt_tick_from_millisecond(2000)); + if (result == -RT_ETIMEOUT) + { + rt_kprintf("car3 waiting for a space\n"); + rt_thread_mdelay(2000); + + } + else if (result == RT_EOK) + { + number = 1; + rt_kprintf("car3 entered, used spaces: %d\n",3 - (task1_sem->value)); + rt_thread_mdelay(5000); + } + } + + if(number == 1) + { + number = 0; + rt_sem_release(task1_sem); + rt_kprintf("car3 left\n"); + rt_thread_mdelay(1000); + } + } +} +// +static char thread4_stack[1024]; +static struct rt_thread thread4; +static void rt_thread4_entry(void *parameter) +{ + static rt_err_t result; + static rt_uint8_t number = 0; + while(1) + { + + if(0 == number) + { + + result = rt_sem_take(task1_sem, rt_tick_from_millisecond(2000)); + if (result == -RT_ETIMEOUT) + { + rt_kprintf("car4 waiting for a space\n"); + rt_thread_mdelay(2000); + } + else if (result == RT_EOK) + { + number = 1; + rt_kprintf("car4 entered, used spaces: %d\n",3 - (task1_sem->value)); + rt_thread_mdelay(3000); + } + } + + + if(number == 1) + { + number = 0; + rt_sem_release(task1_sem); + rt_kprintf("car4 left\n"); + rt_thread_mdelay(1000); + } + } +} +// +static char thread5_stack[1024]; +static struct rt_thread thread5; +static void rt_thread5_entry(void *parameter) +{ + static rt_err_t result; + static rt_uint8_t number = 0; + while(1) + { + + if(0 == number) + { + + result = rt_sem_take(task1_sem, rt_tick_from_millisecond(2000)); + if (result == -RT_ETIMEOUT) + { + rt_kprintf("car5 waiting for a space\n"); + rt_thread_mdelay(2000); + } + else if (result == RT_EOK) + { + number = 1; + rt_kprintf("car5 entered, used spaces: %d\n",3 - (task1_sem->value)); + rt_thread_mdelay(4000); + } + } + + + if(number == 1) + { + number = 0; + rt_sem_release(task1_sem); + rt_kprintf("car5 left\n"); + rt_thread_mdelay(1000); + } + } +} + +/* 信号量的初始化 */ +int parking_demo() +{ + /* 创建一个动态信号量,初始值是3 */ + task1_sem = rt_sem_create("task1_sem", 3, RT_IPC_FLAG_PRIO); + if (task1_sem == RT_NULL) + { + rt_kprintf("create task1_sem semaphore failed.\n"); + return -1; + } + else + { + rt_kprintf("create done. task1_sem semaphore value = 3.\n"); + } + + rt_thread_init(&thread1, + "thread1", + rt_thread1_entry, + RT_NULL, + &thread1_stack[0], + sizeof(thread1_stack), + TASK1_THREAD_PRIORITY, TASK1_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&thread1, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&thread1); + + rt_thread_init(&thread2, + "thread2", + rt_thread2_entry, + RT_NULL, + &thread2_stack[0], + sizeof(thread2_stack), + TASK1_THREAD_PRIORITY, TASK1_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&thread2, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&thread2); + + rt_thread_init(&thread3, + "thread3", + rt_thread3_entry, + RT_NULL, + &thread3_stack[0], + sizeof(thread3_stack), + TASK1_THREAD_PRIORITY, TASK1_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&thread3, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&thread3); + + rt_thread_init(&thread4, + "thread4", + rt_thread4_entry, + RT_NULL, + &thread4_stack[0], + sizeof(thread4_stack), + TASK1_THREAD_PRIORITY, TASK1_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&thread4, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&thread4); + + rt_thread_init(&thread5, + "thread5", + rt_thread5_entry, + RT_NULL, + &thread5_stack[0], + sizeof(thread5_stack), + TASK1_THREAD_PRIORITY, TASK1_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&thread5, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&thread5); + + return 0; +} + +/* 导出到 msh 命令列表中 */ +MSH_CMD_EXPORT(parking_demo, task1 sample); + +/***************************************题目二************************************************************/ + +#define TASK2_THREAD_PRIORITY 8 +#define TASK2_THREAD_TIMESLICE 5 + +/* 指向互斥量的指针 */ +static rt_mutex_t dynamic_mutex = RT_NULL; +static rt_uint8_t ticket = 20; + +static char task2_thread1_stack[1024]; +static struct rt_thread task2_thread1; +static void task2_rt_thread_entry1(void *parameter) +{ + while (1) + { + /* 线程1获取到互斥量后,售票,然后释放互斥量 */ + rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); + + if(ticket == 0) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: 0\n"); + break; + } + + if(ticket > 0) + { + rt_kprintf("window A sold ticket %d\n",ticket); + ticket -= 1; + + } + + rt_mutex_release(dynamic_mutex); + rt_thread_mdelay(10); + } + + rt_mutex_release(dynamic_mutex); +} + +static char task2_thread2_stack[1024]; +static struct rt_thread task2_thread2; +static void task2_rt_thread_entry2(void *parameter) +{ + while (1) + { + /* 线程2获取到互斥量后,售票,然后释放互斥量 */ + rt_mutex_take(dynamic_mutex, RT_WAITING_FOREVER); + + if(ticket == 0) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: 0\n"); + break; + } + else if(ticket == 1) + { + rt_kprintf("window B sold ticket %d\n",ticket); + ticket -= 1; + + } + + else + { + rt_kprintf("window B sold ticket %d\n",ticket); + ticket -= 1; + rt_kprintf("window B sold ticket %d\n",ticket); + ticket -= 1; + + } + + rt_mutex_release(dynamic_mutex); + rt_thread_mdelay(10); + } + + rt_mutex_release(dynamic_mutex); +} + +/* 互斥量示例的初始化 */ +int ticket_demo(void) +{ + /* 创建一个动态互斥量 */ + dynamic_mutex = rt_mutex_create("dmutex", RT_IPC_FLAG_PRIO); + if (dynamic_mutex == RT_NULL) + { + rt_kprintf("create dynamic mutex failed.\n"); + return -1; + } + + rt_thread_init(&task2_thread1, + "task2_thread1", + task2_rt_thread_entry1, + RT_NULL, + &task2_thread1_stack[0], + sizeof(task2_thread1_stack), + TASK2_THREAD_PRIORITY, TASK2_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&task2_thread1, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&task2_thread1); + + rt_thread_init(&task2_thread2, + "task2_thread2", + task2_rt_thread_entry2, + RT_NULL, + &task2_thread2_stack[0], + sizeof(task2_thread2_stack), + TASK2_THREAD_PRIORITY, TASK2_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&task2_thread2, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&task2_thread2); + return 0; +} + +/* 导出到 msh 命令列表中 */ +MSH_CMD_EXPORT(ticket_demo, mutex sample); + +/***********************************************第三题*******************************************/ + +#define task3_THREAD_PRIORITY 9 +#define task3_THREAD_TIMESLICE 5 + +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) + +/* 事件控制块 */ +static struct rt_event event; + +static char network_thread_stack[1024]; +static struct rt_thread network_thread; +/* network线程入口函数 */ +static void network_send_event(void *param) +{ + rt_thread_mdelay(1000); + rt_kprintf("network ready\n"); + rt_event_send(&event, EVENT_NET_READY); +} + + +static char sensor_thread_stack[1024]; +static struct rt_thread sensor_thread; +/* sensor线程入口函数 */ +static void sensor_send_event(void *param) +{ + rt_thread_mdelay(2000); + rt_kprintf("sensor ready\n"); + rt_event_send(&event, EVENT_SENSOR_READY); +} + + +static char storage_thread_stack[1024]; +static struct rt_thread storage_thread; +/* storage线程入口函数 */ +static void storage_send_event(void *param) +{ + rt_thread_mdelay(5000); + rt_kprintf("storage ready\n"); + rt_event_send(&event, EVENT_STORAGE_READY); +} + + +static char business_thread_stack[1024]; +static struct rt_thread business_thread; +/* business线程入口函数 */ +static void business_recv_event(void *param) +{ + rt_uint32_t e; + rt_err_t event_err; + + while(1) + { + event_err = rt_event_recv(&event, (EVENT_NET_READY | EVENT_SENSOR_READY | EVENT_STORAGE_READY), + RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, + rt_tick_from_millisecond(3000), &e); + if(event_err == -RT_ETIMEOUT) + { + if(!(event.set & EVENT_NET_READY) ) + { + rt_kprintf("wait modules timeout, network is not ready\n"); + } + + if(!(event.set & EVENT_SENSOR_READY) ) + { + rt_kprintf("wait modules timeout, sensor is not ready\n"); + } + + if(!(event.set & EVENT_STORAGE_READY) ) + { + rt_kprintf("wait modules timeout, storage is not ready\n"); + } + } + + if(event_err == RT_EOK) + { + rt_kprintf("all modules are ready\n"); + rt_kprintf("business task started\n"); + rt_event_detach(&event); + break; + } + + } +} + + +int startup_demo(void) +{ + rt_err_t result; + + /* 初始化事件对象 */ + result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO); + if (result != RT_EOK) + { + rt_kprintf("init event failed.\n"); + return -1; + } + + rt_thread_init(&network_thread, + "network_thread", + network_send_event, + RT_NULL, + &network_thread_stack[0], + sizeof(network_thread_stack), + task3_THREAD_PRIORITY, task3_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&network_thread, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&network_thread); + + + rt_thread_init(&sensor_thread, + "sensor_thread", + sensor_send_event, + RT_NULL, + &sensor_thread_stack[0], + sizeof(sensor_thread_stack), + task3_THREAD_PRIORITY, task3_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&sensor_thread, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&sensor_thread); + + + rt_thread_init(&storage_thread, + "storage_thread", + storage_send_event, + RT_NULL, + &storage_thread_stack[0], + sizeof(storage_thread_stack), + task3_THREAD_PRIORITY, task3_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&storage_thread, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&storage_thread); + + + rt_thread_init(&business_thread, + "business_thread", + business_recv_event, + RT_NULL, + &business_thread_stack[0], + sizeof(business_thread_stack), + task3_THREAD_PRIORITY, task3_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&business_thread, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&business_thread); + + return 0; +} + +/* 导出到 msh 命令列表中 */ +MSH_CMD_EXPORT(startup_demo, event sample); + +/*******************************************第四题**********************************************/ + +#define task4_THREAD_PRIORITY 25 +#define task4_THREAD_TIMESLICE 10 + +//region = 1:华东 +//region = 2:华南 +//region = 3:华北 + +/* 消息队列控制块 */ +static struct rt_messagequeue mq; +/* 消息队列中用到的放置消息的内存池 */ +static rt_uint8_t msg_pool[2048]; + +typedef struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; + rt_uint16_t weight; +}package_msg_t; + + +static char msgsend_thread1_stack[1024]; +static struct rt_thread msgsend_thread1; +/* 线程1入口函数 */ +static void msgsend_thread1_entry(void *parameter) +{ + + package_msg_t packmeg[5] = {{1,1,500} , {2,2,600} , {3,3,700} , {4,1,800} , {5,2,900}}; + package_msg_t * buf = &packmeg[0]; + rt_uint8_t failcnt = 0,sendcnt = 0; + while (1) + { + int result; + /* 发送消息到消息队列中 */ + result = rt_mq_send(&mq, buf, sizeof(packmeg[0])); + if (result == -RT_EFULL) + { + failcnt++; + rt_kprintf("发送线程1发送失败次数:%d\n",failcnt); + } + if(result == RT_EOK) + { + switch(buf->region) + { + case 1: rt_kprintf("package %d -> east,weight: %d g\n",buf->id,buf->weight);break; + case 2: rt_kprintf("package %d -> south,weight: %d g\n",buf->id,buf->weight);break; + case 3: rt_kprintf("package %d -> north,weight: %d g\n",buf->id,buf->weight);break; + } + sendcnt++; + if(sendcnt == 5) + { + break; + } + buf ++; + rt_thread_mdelay(200); + } + } +} + + +static char msgsend_thread2_stack[1024]; +static struct rt_thread msgsend_thread2; +/* 线程2入口函数 */ +static void msgsend_thread2_entry(void *parameter) +{ + int result; + package_msg_t packmeg[5] = {{6,3,550} , {7,1,650} , {8,2,750} , {9,3,850} , {10,1,950}}; + package_msg_t * buf = &packmeg[0]; + rt_uint8_t failcnt = 0,sendcnt = 0; + while (1) + { + /* 发送消息到消息队列中 */ + result = rt_mq_send(&mq, buf, sizeof(packmeg[0])); + if (result == -RT_EFULL) + { + failcnt++; + rt_kprintf("发送线程1发送失败次数:%d\n",failcnt); + } + if(result == RT_EOK) + { + switch(buf->region) + { + case 1: rt_kprintf("package %d -> east,weight: %d g\n",buf->id,buf->weight);break; + case 2: rt_kprintf("package %d -> south,weight: %d g\n",buf->id,buf->weight);break; + case 3: rt_kprintf("package %d -> north,weight: %d g\n",buf->id,buf->weight);break; + } + sendcnt++; + if(sendcnt == 5) + { + break; + } + buf++; + rt_thread_mdelay(200); + } + } +} + + +static char msgrscv_thread_stack[1024]; +static struct rt_thread msgrscv_thread; +/* 分拣线程入口函数 */ +static void msgrscv_thread_entry(void *parameter) +{ + rt_uint8_t recvcnt = 0, eastcnt = 0, southcnt = 0, northcnt = 0; + rt_uint32_t totalweigh = 0; + package_msg_t packmeg; + while (1) + { + if (rt_mq_recv(&mq, &packmeg, sizeof(packmeg), RT_WAITING_FOREVER) == RT_EOK) + { + recvcnt++; + switch(packmeg.region) + { + case 1 :eastcnt++;break; + case 2 :southcnt++;break; + case 3 :northcnt++;break; + } + totalweigh += packmeg.weight; + + if(recvcnt == 10) + { + rt_kprintf("total packages: %d\n",recvcnt); + rt_kprintf("east: %d, south: %d, north: %d\n",eastcnt,southcnt,northcnt); + rt_kprintf("total weight: %d\n",totalweigh); + rt_mq_detach(&mq); + break; + } + } + } +} + + +int package_demo(void) +{ + rt_err_t result; + + /* 初始化消息队列 */ + result = rt_mq_init(&mq, + "mqt", + &msg_pool[0], /* 内存池指向msg_pool */ + sizeof(package_msg_t), /* 每个消息的大小 */ + 5 * sizeof(package_msg_t), /* 内存池的大小 */ + RT_IPC_FLAG_PRIO); /* 如果有多个线程等待,按照先来先得到的方法分配消息 */ + + if (result != RT_EOK) + { + rt_kprintf("init message queue failed.\n"); + return -1; + } + + rt_thread_init(&msgsend_thread1, + "msgsend_thread1", + msgsend_thread1_entry, + RT_NULL, + &msgsend_thread1_stack[0], + sizeof(msgsend_thread1_stack), + task4_THREAD_PRIORITY, task4_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&msgsend_thread1, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&msgsend_thread1); + + + rt_thread_init(&msgsend_thread2, + "msgsend_thread2", + msgsend_thread2_entry, + RT_NULL, + &msgsend_thread2_stack[0], + sizeof(msgsend_thread2_stack), + task4_THREAD_PRIORITY, task4_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&msgsend_thread2, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&msgsend_thread2); + + + rt_thread_init(&msgrscv_thread, + "msgrscv_thread", + msgrscv_thread_entry, + RT_NULL, + &msgrscv_thread_stack[0], + sizeof(msgrscv_thread_stack), + task4_THREAD_PRIORITY, task4_THREAD_TIMESLICE); +#ifdef RT_USING_SMP + /* 绑定线程到同一个核上,避免启用多核时的输出混乱 */ + rt_thread_control(&msgrscv_thread, RT_THREAD_CTRL_BIND_CPU, (void*)0); +#endif + rt_thread_startup(&msgrscv_thread); + + return 0; +} + +/* 导出到 msh 命令列表中 */ +MSH_CMD_EXPORT(package_demo, msgq sample); + + + + + + + + + + diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..6cb6228c15d0c85c42d2175b7d39508cf633af8d --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,150 @@ +# RT‑Thread多线程实验报告 +## Part 1 理论知识梳理 +### 1 STM32F407启动流程与RT‑Thread引导机制 +#### 1.1 从上电复位到main函数完整调用链 +STM32F407上电后执行硬件复位,硬件自动读取复位向量表,获取栈顶指针以及复位处理函数入口,跳转到`Reset_Handler`。 + +1. 汇编启动文件`startup_stm32f407xx.s`完成内存初始化:将Flash中`.data`段的初始化数据拷贝到SRAM,把`.bss`段全部清零,保证全局未初始化变量默认为0。 +2. 调用ST库的`SystemInit()`,完成FPU浮点单元使能、中断向量表VTOR配置、RCC外设时钟复位等芯片底层操作。 +3. 根据编译器不同进入对应入口函数:MDK‑ARMCC为`$Sub$$main`,IAR为`__low_level_init`,本工程使用GCC工具链,入口为`entry()`。 +4. 进入RT‑Thread内核总入口`rtthread_startup()`,首先关闭全局中断,防止内核初始化过程被中断打断。 +5. 执行`rt_hw_board_init()`板级初始化:完成RT‑Thread堆内存注册、板卡时钟配置、控制台串口设备绑定,调用`rt_components_board_init()`执行`INIT_BOARD_EXPORT`注册的板级初始化函数。 +6. 依次完成内核组件初始化:系统定时器初始化、调度器初始化、信号子系统初始化。 +7. `rt_application_init()`创建main线程,线程入口是`main_thread_entry`,并不会直接调用用户编写的`main()`。 +8. 创建系统后台线程:软件定时器线程负责处理延时、软件定时器任务;idle空闲线程,CPU无就绪任务时运行,承担资源回收、低功耗钩子执行。 +9. 调用`rt_system_scheduler_start()`正式启动调度器,系统进入多线程运行环境。 +10. main线程获取CPU之后,在`main_thread_entry`内部执行`rt_components_init()`,运行`INIT_APP_EXPORT`等应用层级注册函数,最后调用用户业务`main()`。 + +> 关键要点:调度器启动之前系统处于裸机、中断关闭状态;堆、调度器、系统线程全部准备完毕之后,才开启多任务,保证用户线程运行时内核环境完整可用。 +> +> ![image-20260818225544529](figures/image-20260818225544529.png) + +#### 1.2 自动初始化原理:INIT_BOARD_EXPORT 与 INIT_APP_EXPORT +自动初始化机制目的是避免在main函数中堆砌大量模块初始化调用。借助编译链接,把初始化函数指针存放至自定义段,内核启动阶段遍历段内指针批量执行初始化函数。 + +1. **宏封装生成函数指针** +`INIT_EXPORT`为底层基础宏,接收函数名与等级字符串。宏定义一个`const`类型函数指针变量,把目标函数地址赋值给该指针;通过`RT_SECTION`将变量放置到`.rti_fn.xx`自定义段,添加`RT_USED`属性,避免链接器判定函数未被引用而直接裁剪。 + +上层封装不同等级初始化宏: +- `INIT_BOARD_EXPORT(fn)`等级为`"1"`,用于板级初始化; +- `INIT_APP_EXPORT(fn)`等级为`"6"`,用于应用层初始化。 + +2. **链接脚本控制段排布** +链接脚本中配置 `KEEP(*(SORT(.rti_fn*)))`。`SORT`按照段名字符串排序,不同等级函数指针按照1→2→3…→6在内存中连续排布;`KEEP`保证段不会被链接器丢弃。 + +3. **内核遍历调用函数** +内核提供两组遍历接口: +- `rt_components_board_init()`:遍历等级1的段,执行`INIT_BOARD_EXPORT`注册函数。该函数在`rt_hw_board_init`内部调用,此时时钟、堆、串口硬件就绪,但调度器尚未启动,适合板卡外设、底层驱动初始化。 +- `rt_components_init()`:遍历2~6等级段,`INIT_APP_EXPORT`注册函数在此执行。运行于main线程上下文,调度器已经启动,多任务环境就绪,适合用户业务模块、线程创建等逻辑。 + +本次实验模块入口使用`INIT_APP_EXPORT`修饰,无需修改`main.c`,系统启动时自动执行,完成三条实验线程创建。 + +### 2 内核基础与对象模型 +#### 2.1 面向对象设计思想与线程控制块 +RT‑Thread使用C语言模拟面向对象思想,线程、信号量、消息队列、定时器、设备驱动全部抽象为内核对象。所有对象头部内嵌统一`struct rt_object`基结构体,包含对象名称、类型标记、标志位、双向链表节点。系统维护对象容器数组,不同类型对象挂载到对应链表,对外提供统一创建、销毁、按名字查找接口。新增内核组件仅扩展对象类型,原有管理逻辑无需大量修改,满足开闭原则。 + +线程控制块`struct rt_thread`保存线程全部描述信息,关键成员说明: + +|成员|功能说明| +| ---- | ---- | +|`stat`|线程状态标记,标记初始、就绪、运行、挂起、关闭;调度器依靠该成员判断线程是否允许参与CPU调度| +|`current_priority`|线程实际运行优先级,数值越小优先级越高,是抢占调度判断依据| +|`init_tick`|线程创建时配置的时间片;线程重新获取CPU,剩余时间片重置为此值| +|`sp`|线程栈指针,上下文切换核心;线程切出将寄存器现场压栈并保存sp;切回线程读取sp恢复寄存器| +|`stack_addr`|线程栈内存起始地址;栈溢出检测、剩余栈统计依赖该地址| +|`stack_size`|线程栈总字节大小| +|`entry`|线程入口函数地址| + +#### 2.2 静态对象与动态对象对比、高可靠场景选用静态线程原因 + +|对比维度|静态内核对象|动态内核对象| +| ---- | ---- | ---- | +|内存分配位置|TCB、线程栈定义为全局数组,编译期分配,存放于BSS段|运行时调用`rt_malloc`,从系统堆申请内存| +|创建API|`rt_thread_init()`,用户传入预先定义好的TCB、栈缓冲区|`rt_thread_create()`,内核自动分配TCB与栈内存| +|生命周期|跟随程序整个生命周期,编译期内存地址固定|运行时动态生成,支持删除销毁| +|资源释放|无释放操作,内存由链接脚本管理|调用`rt_thread_delete()`,空闲线程延迟回收,内存归还堆| +|运行风险|不存在内存申请失败,资源大小编译阶段即可确定|堆耗尽会创建失败;频繁创建销毁容易产生内存碎片| + +工业控制、汽车电子等高可靠性场景优先选用静态线程: +1. **行为可预测**:TCB、栈内存编译期确定地址,运行不依赖堆,不会出现运行时内存不足导致线程创建失败,可以做栈空间静态上限分析。 +2. **消除内存碎片**:堆反复申请释放会产生外部碎片,长时间运行可能无法分配连续大块内存;静态对象完全不依赖堆,规避碎片风险。 +3. **时序确定,故障隔离**:动态线程删除依靠idle线程延迟回收,回收时机不确定;静态对象没有销毁回收流程,时序行为稳定,便于安全认证测试。 + +### 3 线程状态机与调度算法 +#### 3.1 线程五态模型 +RT‑Thread线程分为初始态、就绪态、运行态、挂起态、关闭态,不同API触发状态迁移: + +|状态|含义|典型触发API| +| ---- | ---- | ---- | +|INIT初始态|线程完成初始化,还未加入调度器|`rt_thread_create()` / `rt_thread_init()`| +|READY就绪态|线程条件满足,等待分配CPU时间片|`rt_thread_startup()`、`rt_thread_resume()`、延时超时唤醒;被高优先级线程抢占让出CPU| +|RUNNING运行态|正在占用CPU执行代码|调度器选中最高优先级就绪线程;无直接应用API,由内核完成切换| +|SUSPEND挂起态|线程放弃CPU,不再参与调度|`rt_thread_mdelay()`延时、`rt_thread_suspend()`主动挂起、等待信号量/消息队列资源不满足| +|CLOSE关闭态|线程执行结束或者被删除,等待资源回收|`rt_thread_exit()`、`rt_thread_delete()`| + +状态迁移关键点: +1. `rt_thread_startup()`将初始态线程切换至就绪态,加入就绪链表; +2. 就绪态切换到运行态完全由调度器决定,应用代码无法直接操作; +3. `rt_thread_yield()`让出CPU,线程依旧保持就绪态,不会进入挂起; +4. 静态线程使用`rt_thread_detach()`,仅脱离对象容器,不会释放静态内存;动态线程使用`rt_thread_delete()`。 + +![image-20260818225609547](figures/image-20260818225609547.png) + +#### 3.2 调度机制剖析 +##### (1)基于优先级的抢占式调度 +RT‑Thread调度器采用优先级位图搭配双向链表,查找最高优先级就绪任务时间复杂度O(1)。 + +调度规则:系统始终选择就绪队列当中优先级数值最小(优先级最高)的线程运行。 +当高优先级线程变为就绪态,无论当前低优先级线程执行到何处,都会立刻触发任务切换,高优先级线程抢占CPU。抢占发生在两种场景: +1. 任务上下文:调用释放信号量、恢复线程等API内部主动触发调度; +2. 中断上下文:系统tick节拍中断退出前,检查就绪队列,判断是否需要抢占切换。 + +本次实验monitor线程优先级10,高于两个工作线程16;monitor一旦就绪就会抢占LED工作线程,保证状态打印按时输出。 + +##### (2)时间片轮转调度 +时间片轮转仅针对**相同优先级**的多个就绪线程。同优先级线程挂载在同一个就绪链表,每个线程拥有时间片参数`init_tick`。每一次系统tick中断,线程剩余时间片`remaining_tick`自减;计数归零,线程移动到链表尾部,让出CPU给链表下一个同优先级线程,剩余时间片重新加载初始时间片。 + +本实验`th_led1`、`th_led2`优先级均为16,时间片5ticks;但线程内部调用`rt_thread_mdelay`主动挂起,线程大部分时间处于挂起状态,实际线程切换更多由延时唤醒事件驱动。 + +## Part 2 多线程实战编程 +### 任务需求说明 +1. **工作线程1 th_led1(动态创建)** +优先级16,时间片5ticks,栈大小1024字节。线程循环翻转板载红色LED,每次循环打印计数,读取输出系统节拍`rt_tick_get()`。使用`rt_thread_create`从堆动态分配TCB与栈。 + +2. **工作线程2 th_led2(静态初始化)** +优先级16,时间片5ticks。全局定义线程控制块TCB与栈缓冲区,调用`rt_thread_init()`完成静态初始化。循环翻转板载蓝色LED,循环内部打印运行计数。 + +3. **监控控制线程 th_monitor(动态创建)** +优先级10,优先级高于两个工作线程;线程内部`rt_thread_mdelay(2000)`实现2秒周期。每次唤醒读取th_led1、th_led2线程状态、剩余栈空间并打印输出。 +板载按键触发一次,挂起th_led1;再次触发按键,恢复th_led1运行。按键使用外部中断,中断服务函数只置位标志变量,线程挂起恢复操作放在monitor线程轮询处理,禁止在中断内直接调用线程操作API。 + +4. 系统集成调试 +- 使用`INIT_APP_EXPORT()`注册模块初始化函数,模块自动启动,`main.c`不增加业务调用代码。 +- 使用`MSH_CMD_EXPORT()`导出自定义FinSH命令`thread_dump`,串口终端输入命令打印线程状态、剩余栈大小。 + +### 关键实现要点 +1. 剩余栈统计原理:动态线程创建时内核使用魔数`'#'(0x23)`填充整个栈;静态线程需要用户手动使用memset填充魔数。Cortex‑M栈向下生长,从栈起始地址向高地址扫描连续魔数,统计字节数量即为剩余栈大小。 +2. 按键中断处理:中断回调仅设置标志位,`rt_thread_suspend`、`rt_thread_resume`在线程中执行,不允许在中断服务函数直接调用。 +3. 打印格式注意:RT‑Thread的`rt_vsnprintf`与标准C库行为存在差异,`%s`后的数字代表最大输出精度,不是最小输出宽度,打印线程名称需要注意格式,避免字符串截断。 +4. monitor线程优先级高于业务线程,保证2秒周期打印不会被业务线程阻塞;同时优先级不能高于msh shell线程,防止串口shell被日志刷屏,无法输入调试命令。 + +### 实验现象 +1. 上电后红色LED周期性翻转,蓝色LED以不同周期翻转;串口输出线程计数、系统tick数值。 +2. monitor线程每2秒输出一次两个线程状态、剩余栈字节。 +3. 按下板载按键,红色LED停止翻转,monitor日志打印挂起提示;再次按下按键,红色LED恢复闪烁,日志打印恢复提示。 +4. msh串口输入`thread_dump`,终端输出表格,展示线程名称、状态、优先级、剩余栈。 + +### 实验结果分析 +1. 抢占调度验证:monitor线程优先级更高,2s定时唤醒后可以抢占业务线程,周期性日志不会丢失。打印时刻业务线程经常处于SUSPEND状态,是业务线程执行`rt_thread_mdelay`进入休眠。 +2. 同优先级轮转:两个LED线程优先级相同,因为内部延时挂起,多数时间不在就绪态,线程切换主要由延时唤醒事件驱动。 +3. 线程状态流转:LED线程在RUNNING和SUSPEND之间来回切换,符合五态模型;按键触发完成READY↔SUSPEND完整状态迁移。 +4. 栈余量:两个线程栈配置1024字节,运行后栈剩余空间充足,栈大小配置合理。 +5. 自动初始化生效:不修改main.c,系统启动自动创建全部业务线程;msh自定义命令正常调用,调试功能可用。 + +### 串口日志输出 +![image-20260818223212900](figures/image-20260818223212900.png) + + + +![image-20260818223631117](figures/image-20260818223631117.png) + diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223212900.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223212900.png" new file mode 100644 index 0000000000000000000000000000000000000000..5a0fdc675fe004547a231f00da8436b7140aaa46 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223212900.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223631117.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223631117.png" new file mode 100644 index 0000000000000000000000000000000000000000..6e8088e20afeea97f8a9b10c9909db0b9afe71f5 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818223631117.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225544529.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225544529.png" new file mode 100644 index 0000000000000000000000000000000000000000..24414822b74926a6ca0feb8d9792da13d59da4a2 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225544529.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225609547.png" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225609547.png" new file mode 100644 index 0000000000000000000000000000000000000000..7c8dabc4f9d55f57616028adc304f29d5264aaf6 Binary files /dev/null and "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/figures/image-20260818225609547.png" differ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" new file mode 100644 index 0000000000000000000000000000000000000000..2826b5cd5f2023f4aa90e5ccfd8b62d28948bb83 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-18 yanran the first version + */ + +#include +#include +#include + +/*-----------------------引脚定义-----------------------*/ +#define PIN_LED_R rt_pin_get("PF.12") +#define PIN_LED_B rt_pin_get("PF.11") +#define PIN_KEY_UP rt_pin_get("PC.5") + +/*-----------------------线程参数-----------------------*/ +#define TH_LED1_PRIORITY 16 +#define TH_LED1_TIMESLICE 5 +#define TH_LED1_STACK_SIZE 1024 + +#define TH_LED2_PRIORITY 16 +#define TH_LED2_TIMESLICE 5 +#define TH_LED2_STACK_SIZE 1024 + +#define TH_MONITOR_PRIORITY 10 +#define TH_MONITOR_TIMESLICE 5 +#define TH_MONITOR_STACK_SIZE 1024 + +#define TH_LED1_DELAY_MS 500 +#define TH_LED2_DELAY_MS 1000 +#define TH_MONITOR_PERIOD_MS 2000 + +/*-------------------------线程句柄--------------------*/ +static rt_thread_t th_led1 = RT_NULL; +static rt_thread_t th_led2 = RT_NULL; +static rt_thread_t th_monitor = RT_NULL; + + +/* th_led2 采用静态方式:TCB 与栈均在全局数据段静态定义 */ +static struct rt_thread led2_thread; +static rt_uint8_t led2_stack[TH_LED2_STACK_SIZE]; + +/* ===================== 按键与挂起控制 ===================== */ +static volatile rt_uint8_t key_pressed = 0; /* ISR 中置位,monitor 中消费 */ +static volatile rt_uint8_t led1_suspend_req = 0; /* 请求 th_led1 自行挂起 */ + +/* 按键中断回调(ISR 上下文,仅置位标志) */ +static void key_up_callback(void *args) +{ + key_pressed = 1; +} + +/* ===================== 工具函数 ===================== */ + +/* + * 计算线程剩余(未使用)栈空间。 + * RT-Thread 创建线程时用 '#'(0x23) 填充整个栈区,ARM 栈向下生长, + * 从低地址向高地址扫描连续 '#' 的字节数即为剩余栈。 + */ +static rt_uint32_t get_stack_remain(rt_thread_t thread) +{ + rt_uint32_t remain = 0; + rt_uint8_t *p = (rt_uint8_t *)thread->stack_addr; + rt_uint8_t *end = (rt_uint8_t *)((rt_ubase_t)thread->stack_addr + thread->stack_size); + + while ((p < end) && (*p == '#')) + { + remain++; + p++; + } + return remain; +} + +/* 线程状态数值 -> 可读字符串 */ +static const char *sta_to_str(rt_uint8_t stat) +{ + stat &= RT_THREAD_STAT_MASK; + switch (stat) + { + case RT_THREAD_INIT: return "INIT"; + case RT_THREAD_READY: return "READY"; + case RT_THREAD_SUSPEND: return "SUSPEND"; + case RT_THREAD_RUNNING: return "RUNNING"; + case RT_THREAD_CLOSE: return "CLOSE"; + default: return "UNKNOWN"; + } +} + +/* ===================== 工作线程 1:动态创建,翻转 LED_R ===================== */ +static void th_led1_entry(void *parameter) +{ + rt_uint32_t count = 0; + + /* 按键挂起请求由本线程自行挂起(本内核 rt_thread_suspend 仅允许挂起自身) */ + while (1) + { + if (led1_suspend_req) + { + led1_suspend_req = 0; + rt_kprintf("th_led1: self suspend\n"); + rt_thread_suspend(rt_thread_self()); + rt_schedule(); + rt_kprintf("th_led1: resumed\n"); + } + + rt_pin_write(PIN_LED_R, !rt_pin_read(PIN_LED_R)); + rt_kprintf("th_led1: count=%d, tick=%u\n", count++, (rt_ubase_t)rt_tick_get()); + rt_thread_mdelay(TH_LED1_DELAY_MS); + } +} + +/* ===================== 工作线程 2:静态初始化,翻转 LED_B ===================== */ +static void th_led2_entry(void *parameter) +{ + rt_uint32_t count = 0; + + while (1) + { + rt_pin_write(PIN_LED_B, !rt_pin_read(PIN_LED_B)); + rt_kprintf("th_led2: count=%d\n", count++); + rt_thread_mdelay(TH_LED2_DELAY_MS); + } +} + +/* ===================== 监控与控制线程:打印状态、处理按键 ===================== */ +static void th_monitor_entry(void *parameter) +{ + rt_err_t res; + + while (1) + { + rt_thread_mdelay(TH_MONITOR_PERIOD_MS); + + /* 按键单次触发:挂起/恢复 th_led1 */ + if (key_pressed) + { + key_pressed = 0; + if ((th_led1->stat & RT_THREAD_STAT_MASK) == RT_THREAD_SUSPEND) + { + res = rt_thread_resume(th_led1); + rt_kprintf("monitor: resume th_led1, ret=%d\n", res); + } + else + { + led1_suspend_req = 1; + rt_kprintf("monitor: request th_led1 suspend\n"); + } + } + + /* 打印两个工作线程的运行状态与剩余栈空间 */ + rt_kprintf("monitor: [%-8s] stat=%-8s stack_remain=%uB | [%-8s] stat=%-8s stack_remain=%uB\n", + th_led1->name, sta_to_str(th_led1->stat), get_stack_remain(th_led1), + th_led2->name, sta_to_str(th_led2->stat), get_stack_remain(th_led2)); + } +} + +/* ===================== 自定义 MSH 命令:thread_dump ===================== */ +static void thread_dump(void) +{ + rt_kprintf("--------------------------------------------------------\n"); + rt_kprintf("name stat prio remain_stack(B)\n"); + rt_kprintf("--------------------------------------------------------\n"); + rt_kprintf("%-9s %-9s %-7d %u\n", th_led1->name, sta_to_str(th_led1->stat), + th_led1->current_priority, get_stack_remain(th_led1)); + rt_kprintf("%-9s %-9s %-7d %u\n", th_led2->name, sta_to_str(th_led2->stat), + th_led2->current_priority, get_stack_remain(th_led2)); + rt_kprintf("--------------------------------------------------------\n"); +} +MSH_CMD_EXPORT(thread_dump, dump thread status and remain stack); + +/* ===================== 自动初始化(禁止在 main.c 硬编码调用) ===================== */ +static int thread_study_init(void) +{ + rt_err_t res; + + /* 板载 LED 引脚初始化 */ + rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); + rt_pin_mode(PIN_LED_B, PIN_MODE_OUTPUT); + + /* 按键中断初始化 */ + rt_pin_mode(PIN_KEY_UP, PIN_MODE_INPUT_PULLUP); + rt_pin_attach_irq(PIN_KEY_UP, PIN_IRQ_MODE_FALLING, key_up_callback, RT_NULL); + rt_pin_irq_enable(PIN_KEY_UP, PIN_IRQ_ENABLE); + + /* 动态创建 th_led1 */ + th_led1 = rt_thread_create("th_led1", + th_led1_entry, + RT_NULL, + TH_LED1_STACK_SIZE, + TH_LED1_PRIORITY, + TH_LED1_TIMESLICE); + if (th_led1 == RT_NULL) + { + rt_kprintf("create th_led1 failed\n"); + return -RT_ERROR; + } + rt_thread_startup(th_led1); + + /* 静态初始化 th_led2 */ + res = rt_thread_init(&led2_thread, + "th_led2", + th_led2_entry, + RT_NULL, + led2_stack, + sizeof(led2_stack), + TH_LED2_PRIORITY, + TH_LED2_TIMESLICE); + if (res != RT_EOK) + { + rt_kprintf("init th_led2 failed, ret=%d\n", res); + return res; + } + th_led2 = &led2_thread; + rt_thread_startup(th_led2); + + /* 动态创建 th_monitor */ + th_monitor = rt_thread_create("th_mon", + th_monitor_entry, + RT_NULL, + TH_MONITOR_STACK_SIZE, + TH_MONITOR_PRIORITY, + TH_MONITOR_TIMESLICE); + if (th_monitor == RT_NULL) + { + rt_kprintf("create th_monitor failed\n"); + return -RT_ERROR; + } + rt_thread_startup(th_monitor); + + rt_kprintf("thread_study_init done\n"); + return RT_EOK; +} +INIT_APP_EXPORT(thread_study_init); + diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..dbbb415fb0b861316395cbc9f182fb705934b227 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,48 @@ +# AHT10 采集 + MQTT 上传 + 文件存储 + 云端控制 + +## 功能概述 + +- 采集 AHT10 温湿度(I2C3),通过 `kawaii-mqtt` 发布到 EMQX +- 同步追加写入 `/fal/Data.txt`,每 10 次调用 `fsync` 落盘 +- 订阅下行主题 `rtt-sub666`,接收 JSON 指令(`{"msg":"led on/off"}`)控制 PF12 LED +- 导出 MSH 命令 `aht10_thread` 启动采集线程 + +--- + +## 依赖软件包/组件 + +| 组件 | 用途 | +|------|------| +| `aht10` | 传感器驱动(需开启 Sensor 框架) | +| `kawaii-mqtt` | MQTT 客户端(Broker 地址、端口、Client ID 需配置) | +| `cJSON` | 解析下行 JSON | +| DFS + elm-fat + FAL + SFUD | 文件系统与 Flash 分区挂载 | +| `rt_vsnprintf_full` | 支持 `%.2f` 浮点打印 | + +--- + +## 硬件引脚 + +- AHT10:I2C3 总线 +- WiFi 片选:PF10(板级初始化拉高,避免 SPI 总线冲突) +- LED:PF12(低电平点亮) + +--- + +## 配置要点 + +1. **MQTT 参数** + 在 `kawaii-mqtt` 的 Kconfig 中设置 Host、Port、Client ID(需唯一),发布主题由 `KAWAII_MQTT_PUBTOPIC` 宏定义(在 `mqttclient.h` 或配置中修改)。 + +2. **文件路径** + 代码使用 `/fal/Data.txt`,确保 `fal` 分区已挂载。若需改用 `font` 分区,修改路径并确保该分区已创建并挂载。 + +3. **下行订阅** + 回调函数 `msg_to_led` 注册到主题 `"rtt-sub666"`(需在 MQTT 初始化时调用 `mqtt_subscribe` 绑定)。 + +## 注意事项 + +- **Client ID 唯一**:多个设备使用相同 ID 会互踢。 +- **文件系统挂载**:确保 `/fal` 或 `/font` 已正确挂载,否则 `open` 失败。 +- **SPI 总线冲突**:若 W25Q64 探测失败,可增大初始化延时或调整片选时序。 +- **JSON 异常**:下行非 JSON 或缺失 `msg` 字段时,代码无处理,建议增加判空。 diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_homework.c" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_homework.c" new file mode 100644 index 0000000000000000000000000000000000000000..917ea98d5c5a131154d10cf9807493d311db5fbe --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_homework.c" @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-21 yanran the first version + */ +#include +#include +#include +#include + +#include "aht10.h" +#include "mqttclient.h" +#include + +#define PIN_LED_R GET_PIN(F, 12) // PF12 : LED_R --> LED + +#define WIFI_CS GET_PIN(F,10) +void WIFI_CS_PULL_UP(void) +{ + rt_pin_mode(WIFI_CS, PIN_MODE_OUTPUT); + rt_pin_write(WIFI_CS, PIN_HIGH); +} +INIT_BOARD_EXPORT(WIFI_CS_PULL_UP); + +/*********************************************************/ +extern mqtt_client_t *client; +int aht10_entry(void *var) +{ + aht10_device_t aht10 = aht10_init("i2c3"); + if(aht10 == RT_NULL) + { + rt_kprintf("aht10 init fail\n"); + return -RT_ERROR; + } + + rt_uint32_t count = 1; + mqtt_message_t msg; + memset(&msg, 0, sizeof(msg)); + char buf[64]; + float temp,humi; + + int fd; + fd = open("/fal/Data.txt",O_RDWR | O_CREAT); + while(1) + { + temp = aht10_read_temperature(aht10); + humi = aht10_read_humidity(aht10); + //rt_kprintf("temp = %.2f , humi = %.2f\n",temp,humi); + //rt_kprintf("num %d\n",num); + + snprintf(buf, sizeof(buf), "temp:%.2f,humi:%.2f,count: %u\r\n", temp, humi,count); + rt_kprintf("temp:%.2f,humi:%.2f,count: %u\n", temp, humi,count); + + msg.qos = QOS0; + msg.payload = (void *)buf; + mqtt_publish(client, KAWAII_MQTT_PUBTOPIC, &msg); + + if(fd >= 0) + { + write(fd,(char *)buf,strlen(buf)); + if((count %10) == 0) + { + fsync(fd); + } + + count++; + + } + rt_thread_mdelay(500); + } + close(fd); + return RT_EOK; +} + +int aht10_thread(void) +{ + rt_thread_t AHT10_thread = rt_thread_create("aht10", aht10_entry, RT_NULL, 1024*5, 10, 10); + if(AHT10_thread == RT_NULL) + { + rt_kprintf("aht10 thread create fail\n"); + return -RT_ERROR; + } + + rt_thread_startup(AHT10_thread); + return RT_EOK; +} + +MSH_CMD_EXPORT(aht10_thread,aht10_thread); +/*************************************************************************/ +void msg_to_led(message_data_t* message) +{ + + if(strcmp(message->topic_name, "rtt-sub666") == 0) + { + + rt_kprintf("topic ture\n"); + cJSON *root = cJSON_Parse((char*)message->message->payload); + cJSON *msg = cJSON_GetObjectItem(root,"msg"); + + if(msg && strcmp(msg->valuestring, "led on") == 0) + { + rt_pin_write(PIN_LED_R, PIN_LOW); + rt_kprintf("led on\n"); + } + else if(msg && strcmp(msg->valuestring, "led off") == 0) + { + rt_pin_write(PIN_LED_R, PIN_HIGH); + rt_kprintf("led off\n"); + } + } + else + { + rt_kprintf("topic error\n"); + } +} + +/*************************************************************************/ + + + + + diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000000000000000000000000000000000000..263841656b32be4ece31cf0d3170c0c248488c6e --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/README.md" @@ -0,0 +1,151 @@ + +# DAY4 虚拟 LED 设备驱动框架设计 +平台:STM32F407 星火一号 `stm32f407‑rt‑spark`,RT‑Thread Studio 工程 `04_device_sample` +提交:`README.md` + `virtu_dev.h` + `virtu_dev.c` +MSH测试命令:`virtu_dev_test` + +## 一、设计目标 +仿照 RT‑Thread PIN 设备驱动框架三层架构,实现一套自定义LED设备驱动框架。不直接使用原生`rt_device`的`read/write/control`通用接口,自定义LED专属`rt_led_ops`操作接口表,向外提供语义化用户API,复刻PIN框架的分层、接口隔离、可移植的设计模式。 + +本实现BSP层可以对接真实STM32 GPIO硬件,底层调用RTT PIN驱动接口操作引脚;框架层只负责转发,不关心底层芯片硬件细节。 + +## 二、框架架构 + +应用层 rt_led_init() / rt_led_on() / rt_led_off() / rt_led_toggle() + 用户直接调用语义化API + │ + ▼ +框架层 virtu_dev.h / virtu_dev.c(上半部分) + 定义 rt_led_ops、rt_device_led;实现注册函数、对外用户API + 连接应用层与BSP硬件层 + │ + ▼ +BSP硬件层 virtu_dev.c(下半部分) + stm32_led_init / stm32_led_on / stm32_led_off / stm32_led_toggle + _stm32_led_ops ops实例 + rt_hw_led_init() 配合INIT_BOARD_EXPORT实现上电自动注册 + +## 三、与PIN框架对应关系 +|组件|PIN框架|本LED框架(virtu_dev)| +|---|---|---| +|头文件|`pin.h`|`virtu_dev.h`| +|框架源码|`pin.c`|`virtu_dev.c`(上半部分)| +|BSP源码|`drv_gpio.c`|`virtu_dev.c`(下半部分)| +|ops结构体|`struct rt_pin_ops`|`struct rt_led_ops`| +|设备结构体|`struct rt_device_pin`|`struct rt_device_led`| +|注册函数|`rt_device_pin_register()`|`rt_device_led_register()`| +|用户API|`rt_pin_write()`|`rt_led_on()`| +|BSP实现函数|`stm32_pin_write()`|`stm32_led_on()`| +|ops实例|`_stm32_pin_ops`|`_stm32_led_ops`| +|初始化函数|`rt_hw_pin_init()`|`rt_hw_led_init()`| +|自动注册宏|`INIT_BOARD_EXPORT(rt_hw_pin_init)`|`INIT_BOARD_EXPORT(rt_hw_led_init)`| + +## 四、关键数据结构 +### 1. rt_led_ops 操作接口表 +```c +struct rt_led_ops +{ + void (*led_init)(struct rt_device *device, rt_base_t pin, rt_base_t mode); + void (*led_on)(struct rt_device *device, rt_base_t pin); + void (*led_off)(struct rt_device *device, rt_base_t pin); + void (*led_toggle)(struct rt_device *device, rt_base_t pin); +}; +``` +- `led_init`:初始化指定LED引脚,设置引脚工作模式 +- `led_on`:点亮指定LED引脚 +- `led_off`:熄灭指定LED引脚 +- `led_toggle`:LED翻转,示例中实现循环闪烁10次逻辑 + +### 2. rt_device_led 自定义设备结构体 +```c +struct rt_device_led +{ + struct rt_device parent; + const struct rt_led_ops *ops; +}; +``` +- `parent`:继承RT‑Thread标准`rt_device`,放在结构体第一个成员,用于向系统设备管理器注册 +- `ops`:保存BSP层传入的LED操作函数指针表,框架层依靠该指针间接调用底层硬件实现 + +## 五、设备注册流程 +``` +系统上电启动 + -> rt_hw_led_init() // INIT_BOARD_EXPORT自动调用 + -> rt_device_led_register("led", &_stm32_led_ops, RT_NULL) + -> 填充 _hw_led.parent 的设备基础属性,全部原生rt_device接口置RT_NULL + -> _hw_led.ops = ops; // 保存BSP传入ops函数指针表 + -> rt_device_register(&_hw_led.parent, "led", RT_DEVICE_FLAG_RDWR) + -> LED设备注册进入RT‑Thread设备管理器 +``` +注册完成后,应用层直接调用`rt_led_xxx()`系列语义API完成LED控制。 + +## 六、API调用链示例(以 rt_led_on(PIN_LED_B) 为例) +``` +rt_led_on(PIN_LED_B) // 应用层调用对外API + -> _hw_led.ops->led_on(&_hw_led.parent, pin) // 框架层,通过函数指针转发 + -> stm32_led_on(device, pin) // BSP硬件层实现 + -> rt_kprintf("led on\n"); + -> rt_pin_write(pin,PIN_LOW); // 底层调用RTT PIN框架操作真实GPIO +``` + +## 七、与通用rt_device驱动(test_drv.c)对比 +`test_drv.c`直接复用`rt_device`内置`init/read/write/control`,上层需要使用`rt_device_find` + `rt_device_write`传递缓冲区完成控制。 +本LED框架仿照PIN框架,自定义专属ops与语义API。 + +|对比项|test_drv.c(通用rt_device)|本LED自定义框架| +|---|---|---| +|ops定义|无,直接复用rt_device原生接口|自定义`struct rt_led_ops`| +|用户调用API|`rt_device_write(dev,pos,buf,size)`|`rt_led_on(pin)`语义化函数| +|参数传递|依靠buffer缓冲区+offset传递参数|直接传入pin引脚号参数| +|代码可读性|需要查阅协议文档解析buffer内容|函数名直观,参数含义明确| +|可移植性|更换硬件,应用层代码需要修改|仅需要重新实现一套`rt_led_ops`,应用层代码无需改动| + +## 八、实验验证 +### 测试代码 +```c +#define PIN_LED_B rt_pin_get("PF.11") +#define PIN_MODE PIN_MODE_OUTPUT +static void virtu_dev_test(void) +{ + rt_led_init(PIN_LED_B,PIN_MODE); + rt_led_on(PIN_LED_B); + rt_thread_mdelay(2000); + rt_led_off(PIN_LED_B); + rt_thread_mdelay(2000); + rt_led_toggle(PIN_LED_B); +} +MSH_CMD_EXPORT(virtu_dev_test, virtu_dev sample); +``` + +### 预期串口输出 +``` +msh >virtu_dev_test +led init success +led on +led off +led toggle... +count: 1 +count: 2 +count: 3 +count: 4 +count: 5 +count: 6 +count: 7 +count: 8 +count: 9 +count: 10 +``` + +### 结果分析 +1. `rt_led_init()`通过ops调用BSP层`stm32_led_init`,打印日志,调用`rt_pin_mode`完成GPIO初始化。 +2. `rt_led_on()`点亮LED,延时2秒;`rt_led_off()`熄灭LED,延时2秒。底层调用PIN框架接口操作真实PF.11引脚。 +3. `rt_led_toggle()`执行闪烁逻辑,循环10次,打印count计数。 +4. 全部上层API都通过框架层保存的ops函数指针跳转到BSP层实现;框架层完全不直接操作硬件引脚,做到软硬件解耦。 + +## 九、总结 +本项目完整复刻RT‑Thread PIN驱动框架核心思想: +1. **分层设计**:应用层‑框架层‑BSP硬件层三层分离,各层职责明确。框架层不包含硬件操作代码。 +2. **接口隔离**:框架层定义`rt_led_ops`接口契约;BSP层完成硬件实现,通过函数指针表和框架层解耦。 +3. **自动注册机制**:`INIT_BOARD_EXPORT`完成上电自动注册,BSP将ops函数指针注入框架。 +4. **语义化API**:对外提供`rt_led_on/off/init/toggle`,调用直观,相比原生`rt_device`的read/write更友好。 +5. **良好可移植性**:切换不同硬件平台时,仅需要重新实现一套`rt_led_ops`;框架层代码、上层应用测试代码完全不用修改。 diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.c" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.c" new file mode 100644 index 0000000000000000000000000000000000000000..3f5552ff8b9765108c2cb90368fcb3b6ab6e0061 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.c" @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-20 yanran the first version + */ +#include +#include "virtu_dev.h" +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +/**************************************************************************************/ +//实现stm32_led的bsp函数 +static void stm32_led_init(struct rt_device *device, rt_base_t pin, rt_base_t mode) +{ + rt_kprintf("led init success\n"); + rt_pin_mode(pin,mode); + return; +} +static void stm32_led_on (struct rt_device *device, rt_base_t pin) +{ + rt_kprintf("led on\n"); + rt_pin_write(pin,PIN_LOW); + return; +} +static void stm32_led_off (struct rt_device *device, rt_base_t pin) +{ + rt_kprintf("led off\n"); + rt_pin_write(pin,PIN_HIGH); + return; +} +static void stm32_led_toggle (struct rt_device *device, rt_base_t pin) +{ + //以闪烁10次为例子 + rt_kprintf("led toggle...\n"); + rt_uint8_t count = 0; + while(count < 10) + { + rt_pin_write(pin,PIN_LOW); + rt_thread_mdelay(500); + rt_pin_write(pin,PIN_HIGH); + rt_thread_mdelay(500); + count++; + rt_kprintf("count: %d\n",count); + } + return; +} + +//定义led的bsp驱动层ops表 +const static struct rt_led_ops _stm32_led_ops = +{ + stm32_led_init, + stm32_led_on, + stm32_led_off, + stm32_led_toggle, +}; + +/********************************************************************************************/ + +static struct rt_device_led _hw_led; + +//注册led设备 +int rt_device_led_register(const char *name, const struct rt_led_ops *ops, void *user_data) +{ + _hw_led.parent.type = RT_Device_Class_Unknown; + _hw_led.parent.rx_indicate = RT_NULL; + _hw_led.parent.tx_complete = RT_NULL; + + _hw_led.parent.init = RT_NULL; + _hw_led.parent.open = RT_NULL; + _hw_led.parent.close = RT_NULL; + _hw_led.parent.read = RT_NULL; + _hw_led.parent.write = RT_NULL; + _hw_led.parent.control = RT_NULL; + + _hw_led.ops = ops; + _hw_led.parent.user_data = user_data; + + /* register a character device */ + rt_device_register(&_hw_led.parent, name, RT_DEVICE_FLAG_RDWR); + return 0; +} + +//上电后自动完成led设备的注册,修改这里的_stm32_led_ops实现调用不同芯片的bsp函数 +int rt_hw_led_init(void) +{ + // 把BSP的硬件ops传给框架层 rt_device_led_register + rt_device_led_register("led", &_stm32_led_ops, RT_NULL); + return 0; +} +INIT_BOARD_EXPORT(rt_hw_led_init); // 系统上电自动执行! + +/***********************************************************************************/ +//RT-Thread Hardware LED APIs +//开放给rtthread使用的led api,实际上调用对应ops表指向的bsp函数 +void rt_led_on(rt_base_t pin) +{ + RT_ASSERT(_hw_led.ops != RT_NULL); + _hw_led.ops->led_on(&_hw_led.parent, pin); +} + +void rt_led_off(rt_base_t pin) +{ + RT_ASSERT(_hw_led.ops != RT_NULL); + _hw_led.ops->led_off(&_hw_led.parent, pin); +} + +void rt_led_init(rt_base_t pin, rt_base_t mode) +{ + RT_ASSERT(_hw_led.ops != RT_NULL); + _hw_led.ops->led_init(&_hw_led.parent, pin, mode); +} + +void rt_led_toggle(rt_base_t pin) +{ + RT_ASSERT(_hw_led.ops != RT_NULL); + _hw_led.ops->led_toggle(&_hw_led.parent, pin); +} +/************************************************************************/ +//自定义的led设备的测试程序 +#define PIN_LED_B rt_pin_get("PF.11") +#define PIN_MODE PIN_MODE_OUTPUT + +static void virtu_dev_test(void) +{ + rt_led_init(PIN_LED_B,PIN_MODE); + rt_led_on(PIN_LED_B); + rt_thread_mdelay(2000); + rt_led_off(PIN_LED_B); + rt_thread_mdelay(2000); + rt_led_toggle(PIN_LED_B); +} + +MSH_CMD_EXPORT(virtu_dev_test, virtu_dev sample); + diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.h" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.h" new file mode 100644 index 0000000000000000000000000000000000000000..ecc2e9ab0eb952c3fcf03f615527d1df94c6b125 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\344\275\234\344\270\232/\347\254\254\345\233\233\345\244\251\344\275\234\344\270\232/virtu_dev.h" @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2026-08-20 yanran the first version + */ +#ifndef APPLICATIONS_VIRTU_DEV_H_ +#define APPLICATIONS_VIRTU_DEV_H_ + +#include + +//自定义的led_ops表 +struct rt_led_ops +{ + void (*led_init)(struct rt_device *device, rt_base_t pin, rt_base_t mode); + void (*led_on)(struct rt_device *device, rt_base_t pin); + void (*led_off)(struct rt_device *device, rt_base_t pin); + void (*led_toggle)(struct rt_device *device, rt_base_t pin); +}; + +//定义led设备类型 +struct rt_device_led +{ + struct rt_device parent; + const struct rt_led_ops *ops; +}; + +//函数声明 +void rt_led_on(rt_base_t pin); +void rt_led_off(rt_base_t pin); +void rt_led_init(rt_base_t pin, rt_base_t mode); +void rt_led_toggle(rt_base_t pin); + + +#endif /* APPLICATIONS_VIRTU_DEV_H_ */ diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..671aa9f24aa039887c3094143dd9b9638a5bb773 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,110 @@ +# RT-Thread夏令营Day1学习笔记 + +# 一、Markdown 语法学习与实战应用 + +今日系统学习了轻量级标记语言 Markdown 的核心语法,该语法结构简洁、可读性强,非常适合用于编写技术笔记、项目README文档以及开源项目PR说明,是嵌入式开发和开源学习的基础工具。 + +## 1. 核心基础语法 + +熟练掌握了日常高频使用的语法,包含多级标题、有序/无序列表、文本样式、行内代码、代码块等,核心用法如下: + +```markdown +# 一级标题 +## 二级标题 +### 三级标题 +**加粗文本** +`行内代码` +``` +## 2. 实战使用场景 + +在RT-Thread嵌入式学习中,代码块是实用性最高的功能,可单独收纳Git指令、程序代码、操作步骤,区分普通文字与技术代码,让笔记规整且便于后续复用、复制。日常学习中可用于每日笔记整理、实验过程记录、项目文档编写等场景。 + +# 二、Git与Gitee版本控制与协作规范 + +本次学习完成了Git环境部署、账号配置、远程仓库连接,掌握了基础代码管理指令与团队协作、PR提交规范,适配开源项目多人开发模式。 + +## 1. 基础环境配置 + +完成Git全局用户信息配置,通过SSH密钥打通Gitee远程连接权限,可通过专属指令校验连接状态、查看和修改远程仓库地址,保障本地与远程仓库正常通信。 + +```bash +# 配置全局用户信息 +git config --global user.name "用户名" +git config --global user.email "邮箱" +# 查看配置信息 +git config --list +# 测试Gitee SSH连接 +ssh -T git@gitee.com +# 查看、修改远程仓库地址 +git remote -v +git remote set-url origin 仓库地址 +``` + +## 2. 日常代码提交与同步 + +掌握了仓库初始化、文件暂存、提交、拉取、推送等核心操作,形成标准化的本地代码更新、远程同步流程,可完整记录代码迭代版本。 + +```bash +git init # 初始化本地Git仓库 +git fetch origin master # 同步远程主分支信息 +git add 文件/路径 # 文件添加至暂存区 +git commit -m "提交备注" # 提交修改并备注说明 +git pull # 拉取远程最新代码 +git push origin master # 本地代码推送至远程仓库 +``` + +## 3. 分支管理与团队PR规范 + +学习了分支创建与切换操作,为独立开发、功能迭代提供保障。团队协作采用标准化流程:先Fork官方仓库,克隆至本地后,同步远程主分支最新代码,新建独立分支完成学习任务,避免代码混杂。 + +项目主分支开启保护机制,禁止直接推送代码,所有修改需通过新建分支提交PR,由专人审核合并。普通组员PR由小组长审核,组长PR需由导师复核,严格把控代码规范与目录格式,保障仓库代码整洁统一。 + +```bash +git checkout -b 分支名 # 创建并切换至新分支 +git switch master # 切换回主分支 +git pull origin master # 同步远程主分支最新代码 +``` + +# 三、RT-Thread开发环境搭建与工程调试 + +## 1. 开发环境配置 + +掌握RT-Thread完整开发环境搭建逻辑,环境由RT-Thread源码、SDK开发工具、BSP板级支持包三部分组成。其中SDK提供编译工具链、开发编译环境,BSP适配对应硬件开发板,包含启动文件、底层驱动、工程配置等核心文件。 + +## 2. 工程创建与仿真测试 + +熟悉RT-Thread Studio的工程创建、编译、下载、调试全流程。一方面基于STM32F407ZG芯片创建实体开发板工程,校验ST-Link驱动正常后,完成程序编译、下载与断点调试,可成功停留在main函数,验证工程运行正常。 + +另一方面通过QEMU模拟器搭建虚拟ARM硬件环境,创建`qemu-vexpress-a9`工程,无需实体开发板即可完成工程编译、运行,成功进入msh命令行,掌握系统基础校验方法。msh作为系统内置Shell,可输入指令查看系统状态、执行功能操作。 + +# 四、RT-Thread工程结构、驱动框架与内核原理 + +## 1. 工程核心目录结构 + +了解RT-Thread标准工程目录分工,核心包含application应用目录、drivers外设驱动目录(串口、音频、键鼠、LCD等)、components组件目录(文件系统、C++支持等)、libcpu底层跨架构接口目录、libc工具链兼容目录,各目录各司其职,保障工程模块化运行。 + +## 2. 三层驱动框架 + +RT-Thread采用分层式驱动架构,有效屏蔽硬件差异、降低应用层耦合度:第一层为统一IO设备管理层,统一管理字符、块、SPI等各类外设;第二层为通用硬件设备驱动层;第三层为厂商专属外设驱动层,适配不同硬件型号。 + +## 3. 内核核心特性 + +RT-Thread内核采用面向对象设计思想,源码涵盖内存管理、中断处理、进程通信(IPC)、对象管理等核心模块。系统调度器为优先级抢占式调度,可高效分配系统资源,保障多线程稳定运行。 + +# 五、RT-Thread系统启动流程 + +梳理出RT-Thread完整启动执行逻辑,启动流程层级清晰、循序渐进: + +1. 汇编层初始化:执行启动汇编文件,完成栈指针配置、Flash数据拷贝至SRAM、内存段清零操作,根据编译工具链跳转对应初始化入口(MDK走__main、GCC调用自定义entry); + +2. 系统底层初始化:关闭全局中断,执行板级硬件初始化,打印系统版本Logo; + +3. 内核模块初始化:依次完成系统时钟、调度器、信号量等核心模块初始化; + +4. 线程创建与调度启动:创建主线程、定时器线程、空闲线程,启动系统调度器; + +5. 最终跳转至用户main函数,执行用户应用程序。 + +# 六、项目实战:Hello RT-Thread + +基于STM32F407开发板完成入门实战项目,在RT-Thread Studio新建工程后,修改代码添加自定义打印语句,编译下载至开发板。设备正常运行后,终端成功输出“Hello RSOC 2026”与系统自带版本号,顺利完成首个RT-Thread应用程序开发与运行测试。 \ No newline at end of file diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..79dd5f03fa7c914fbd7fb006757eada7a36afe5a --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,220 @@ +# RT-Thread 夏令营 Day5 学习整理 + +## 一、软件包生态与索引机制 + +RT-Thread 的软件包是预封装的功能模块,涵盖传感器驱动、网络协议、工具库等,主线已有 800 多个包,可以像“应用商店”一样按需选用。软件包的索引通过 Kconfig 实现,该 Kconfig 位于 env 工具的 `packages` 目录下。RT-Thread Studio 会根据内核版本自动匹配 env 版本——内核 5.1.0 以上使用 env 2.0,5.1.0 以下则用 1.5.x,以保证兼容性。 + +本次实战用到的软件包主要包括: +- **AHT10**:温湿度传感器驱动,依赖 Sensor 设备框架; +- **RW007**:SPI WiFi 模块驱动; +- **kawaii-mqtt**:MQTT 客户端; +- **rt_vsnprintf_full**:支持浮点数格式化打印; +- **SFUD**:通用 SPI Flash 驱动,屏蔽不同型号 Flash 的差异。 + +--- + +## 二、AHT10 温湿度采集与调试 + +### 2.1 软件包使用流程 + +在 RT-Thread Settings 中开启 I2C3 总线(AHT10 挂在该总线上),然后添加 `aht10` 软件包。保存配置后源码会自动下载。核心接口非常简单: + +```c +#include + +aht10_device_t dev = aht10_init("i2c3"); // 传入总线名称 +float temp = aht10_read_temperature(dev); +float humi = aht10_read_humidity(dev); +``` + +### 2.2 浮点数打印问题 + +默认的 libc 不支持 `%f` 格式化,直接 `rt_kprintf("%.1f", temp)` 会输出乱码或空内容。解决办法有两个: +- 使能 `rt_vsnprintf_full` 软件包,让系统支持浮点数打印; +- 或手动拆分整数和小数部分,例如 `(int)temp` 和 `(int)(temp*10)%10`。 + +### 2.3 封装为线程并导出命令 + +建议将读取逻辑放在独立线程中,并导出为 MSH 命令,方便调试: + +```c +static void aht10_thread_entry(void *param) +{ + aht10_device_t dev = aht10_init("i2c3"); + if (dev == RT_NULL) return; + while (1) { + rt_kprintf("T: %.1f H: %.1f%%\n", + aht10_read_temperature(dev), + aht10_read_humidity(dev)); + rt_thread_mdelay(500); + } +} +static void aht10_sample(void) +{ + rt_thread_t tid = rt_thread_create("aht10", aht10_thread_entry, + RT_NULL, 1024, 10, 20); + if (tid) rt_thread_startup(tid); +} +MSH_CMD_EXPORT(aht10_sample, start aht10 sample); +``` + +烧录后终端输入 `aht10_sample` 即可看到温湿度数值,用手触摸传感器读数会明显变化,说明驱动正常。 + +--- + +## 三、RW007 WiFi 模块联网 + +### 3.1 软件包使能与引脚修正 + +在 menuconfig 中开启 RW007 软件包后,**务必对照开发板原理图修正引脚配置**,包括片选(CS)、Boot0/Boot1、中断(INT)和复位(Reset)引脚。默认引脚与实际板子不符,不改会导致模块无法工作。修正后编译下载。 + +### 3.2 联网验证 + +通过终端执行 WiFi 命令: +- `wifi scan`:扫描附近热点,可验证引脚是否正确; +- `wifi join `:连接热点; +- `ping baidu.com`:测试网络连通性。 + +能 `ping` 通即表示 RW007 已正常联网,为后续 MQTT 通信做好准备。 + +--- + +## 四、MQTT 通信(kawaii-mqtt + EMQX) + +### 4.1 MQTT 协议概念 + +MQTT 是轻量级发布/订阅协议,适合物联网场景。Broker(消息代理)负责转发主题消息,客户端之间解耦。本次使用 EMQX 提供的免费公共服务器(`broker.emqx.io`,端口 1883),并用 MQTTX 桌面工具辅助调试。 + +### 4.2 kawaii-mqtt 核心用法 + +使能 `kawaii-mqtt` 软件包并开启示例,填写配置(host、port、client_id、订阅/发布主题)。注意 `client_id` 必须全局唯一,否则会互相踢下线。核心 API 流程: + +```c +mqtt_client_t *client = mqtt_lease(); +mqtt_set_host(client, "broker.emqx.io"); +mqtt_set_port(client, "1883"); +mqtt_set_client_id(client, "your_unique_id"); +mqtt_connect(client); +// 订阅 +mqtt_subscribe(client, "sub_topic", QOS0, callback); +// 发布 +mqtt_message_t msg; +msg.qos = QOS0; +msg.payload = (void *)"hello"; +msg.payloadlen = strlen("hello"); +mqtt_publish(client, "pub_topic", &msg); +``` + +发布的消息载荷必须指定 `payloadlen`,否则会发送失败。接收采用回调驱动,当订阅主题有消息到达时,注册的回调函数会被执行。 + +### 4.3 自定义 MSH 命令 + +可封装一条 MSH 命令,通过参数灵活控制发送内容: + +```c +static void mqtt_send(int argc, char *argv[]) +{ + if (argc < 2) return; + if (mqtt_client_connected(client)) { + mqtt_message_t msg; + msg.payload = argv[1]; + msg.payloadlen = strlen(argv[1]); + mqtt_publish(client, PUB_TOPIC, &msg); + } +} +MSH_CMD_EXPORT(mqtt_send, send message); +``` + +`argc` 表示参数个数(命令本身算第一个),`argv[0]` 是命令名,`argv[1]` 开始是实际参数。 + +--- + +## 五、文件系统与外部 Flash 持久化 + +### 5.1 DFS 虚拟文件系统 + +RT-Thread 的 DFS(Device File System)提供统一的 POSIX 风格文件操作接口(`open/read/write/close`),支持多种底层介质。根目录为 `/`,可挂载不同分区。本次外部 Flash 型号为 W25Q64,通过 SFUD 驱动统一访问。 + +### 5.2 配置与挂载 + +在 menuconfig 中依次使能: +- DFS 组件; +- elm-fat 文件系统(FATFS 兼容层); +- SPI Flash 驱动; +- FAL 分区管理; +- 自动挂载选项。 + +启动流程大致为:系统初始化 DFS 注册表 → 注册 elm 和 romfs 文件系统 → SPI Flash 探测并注册块设备 → FAL 根据分区表划分区域 → 自动挂载 romfs 到 `/`,再挂载 elm 分区到 `/fal`(或 `/flash`,取决于板卡定义)。 + +### 5.3 分区与挂载点 + +外部 Flash 被划分为多个分区(固件区、WiFi 固件区、字库区、文件系统区)。文件系统区通常挂载到 `/fal`(部分板子挂载到 `/flash`)。若需要使用额外分区(如 `font`),需在 romfs 根目录中预先定义该目录项(因为 romfs 是只读的,无法运行时 `mkdir`),然后在代码中手动创建设备并挂载: + +```c +struct rt_device *dev = fal_blk_device_create("font"); +dfs_mkfs("elm", "font"); // 首次格式化 +dfs_mount("font", "/font", "elm", 0, 0); +``` + +建议采用“先尝试挂载,失败再格式化”的策略,避免每次开机都擦写 Flash。 + +### 5.4 POSIX 文件读写 + +用标准接口在 `/fal` 下追加写入 `Data.txt`: + +```c +int fd = open("/fal/Data.txt", O_WRONLY | O_CREAT | O_APPEND); +if (fd >= 0) { + write(fd, "Temp: 26.5, Humi: 45.2\n", strlen(...)); + close(fd); // close 会触发缓存落盘,不可省略 +} +``` + +注意 `write` 只是写入缓存,`close` 才真正将数据刷入 Flash。若文件内容为空,检查是否忘记 `close`。 + +### 5.5 典型问题与解决 + +| 现象 | 原因 | 解决 | +|------|------|------| +| W25Q64 探测失败 | 与 RW007 共用 SPI 总线,初始化时序冲突 | 初始化时先延时 500ms,失败后重试,同时将 WiFi 片选上拉 | +| `/fal` 下出现 BAD file 乱码 | FAT 目录项损坏(写中断或旧数据残留) | 用 `mkfs -t elm filesystem` 格式化(设备名是 `filesystem`,不是 `fal`) | +| RTC 警告刷屏 | FAT 取时间戳时找不到 RTC 设备 | 在 `ffconf.h` 中设置 `FF_FS_NORTC = 1` 关闭时间戳 | +| 中文冒号乱码 | elm-fat code page 为 437,不支持中文 | 改用 ASCII 字符(如英文冒号) | + +--- + +## 六、综合实战:采集 + MQTT 上报 + 文件记录 + +最终任务是将 AHT10 数据每 2 秒采集一次,通过 MQTT 发布到云端(MQTTX 可接收),同时追加写入 Flash 文件。 + +### 6.1 三线程 + 消息队列解耦 + +将任务拆分为三个独立线程,通过消息队列传递数据: + +- **采集线程**:每隔 3 秒读取温湿度,将数据封装后发送到两条消息队列(一条给 MQTT,一条给文件)。 +- **MQTT 上传线程**:从队列接收数据,拼装成字符串后调用 `mqtt_publish` 发送。 +- **文件保存线程**:从另一队列接收数据,以追加方式写入 `/fal/Data.txt`,并维护最近若干条记录。 + +这种设计使采集、上传、存储三者互不阻塞,便于扩展和调试。 + +### 6.2 云端控灯(下行指令) + +订阅下行主题(如 `rsoc/xxx/device/down`),在回调函数中解析载荷内容: +- 若包含 `"toggle"` 或 `"led"`,翻转板载 LED 状态; +- 若包含 `"on"`,点亮 LED; +- 若包含 `"off"`,熄灭 LED。 + +通过 MQTTX 发送对应字符串即可远程控制。 + +--- + +## 七、今日总结 + +Day5 的学习串联了软件包管理、传感器采集、WiFi 联网、MQTT 通信、文件系统持久化,构建了一个完整的“采集 → 上传 → 存储”物联网数据链路。核心体会: + +- 软件包大幅降低了驱动开发工作量,但要留意依赖关系与配置细节; +- 硬件层面(引脚、总线冲突、时序)仍需要细心对照手册调整; +- DFS 和 POSIX 接口让 Flash 操作变得像普通文件一样简单,但缓存和挂载点问题需留意; +- MQTT 发布/订阅模型适合设备与云端交互,结合回调机制可轻松实现远程控制。 + +后续可继续尝试其他软件包(如 JSON 解析、按键消抖等),进一步丰富应用功能。 \ No newline at end of file diff --git "a/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000000000000000000000000000000000000..1b1c6e904a9ec485369ee9b389683cc60b716323 --- /dev/null +++ "b/2026/\347\254\2542\347\273\204/\345\210\230\351\207\221\346\265\267/\347\254\224\350\256\260/\347\254\254\345\233\233\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,206 @@ + +# RT‑Thread 设备驱动框架学习笔记(第4天) + +## 前言 +RT‑Thread设备驱动框架核心目的是**软硬件解耦,提升代码可移植性**。上层应用不直接操作芯片寄存器、不依赖芯片原厂HAL库,全部调用操作系统提供的抽象接口;硬件相关逻辑全部下沉到驱动BSP层。更换MCU时,只修改底层驱动,业务应用代码可以最大程度复用。 +本节课主要学习通用`rt_device`设备模型、PIN引脚驱动框架完整原理,同时理解两种虚拟设备:直接复用rt_device的`test_dev`,以及仿照PIN架构自定义ops的LED虚拟驱动框架。 + +## 一、RT‑Thread通用设备基础模型 +### 1.三层整体架构 +RT‑Thread标准设备框架分为三层: + +|层级|职责|核心API/对象| +|---|---|---| +|应用层|查找、打开、操作、关闭设备业务逻辑|`rt_device_find`、`rt_device_open/read/write/control/close`| +|IO设备管理层|设备注册管理、引用计数、统一分发入口|`struct rt_device`、`rt_device_register`| +|驱动层|BSP硬件对接,实现真正硬件动作|`init/open/read/write/control`驱动实现函数| + +调用流程: +``` +应用层 + ├─ rt_device_find() 根据名字拿到设备句柄 + ├─ rt_device_open() 打开设备,引用计数+1 + └─ rt_device_read / write / control +    → rt_device内部函数指针ops +      → 底层驱动实现(真实硬件 / 虚拟模拟) +``` + +### 2.struct rt_device与函数指针ops +`struct rt_device`是所有设备的基类,属于C语言模拟面向对象的写法。 +关键成员: +- `type`:设备类型,字符设备、块设备、杂项设备等 +- `ref_count`:设备打开引用计数,多任务访问资源管理 +- `user_data`:驱动私有数据,保存设备状态信息 +- 函数指针集合:`init / open / close / read / write / control` + +驱动开发流程(通用test_dev示例): +1. 创建/定义设备实例; +2. 将自己写的驱动函数赋值给rt_device的各个函数指针; +3. 调用`rt_device_register()`把设备注册进系统设备管理器; +4. MSH执行`list_device`即可看到注册完成的设备。 + +### 3.设备完整生命周期 +1. 系统启动,驱动初始化,创建并注册设备; +2. 应用调用`rt_device_find()`获取设备指针; +3. `rt_device_open()`打开设备,引用计数自增; +4. 通过read/write/control完成业务操作; +5. `rt_device_close()`关闭设备,引用计数自减。 + +> 引用计数用来做多任务资源保护,多个线程同时操作同一个设备时,避免资源异常释放。 + +### 4.通用rt_device模型的短板 +通用设备只有固定的`read/write/control`接口,没有业务语义。 +比如控制LED、GPIO引脚,需要把参数打包放到buffer缓冲区,依靠offset偏移、自定义cmd命令完成参数传递。 +缺点: +- 代码可读性差,使用者需要记住buffer内部协议; +- 参数传递容易出错; +- 调用接口无法直观体现“点亮LED、设置引脚模式”这类业务动作。 + +> 解决思路:PIN框架给出示范,**在rt_device基础之上再封装一层,自定义专属ops函数指针表,对外提供语义化API**。 + +## 二、PIN引脚驱动框架深度分析 +PIN框架就是基于通用rt_device做二次封装的典型案例,专门用于GPIO引脚操作。 + +### 1.PIN框架三层架构 +``` +应用层 rt_pin_mode() / rt_pin_write() / rt_pin_read() / rt_pin_attach_irq() +  │ 语义化API,使用者无需关心芯片寄存器 +  ▼ +框架层 pin.h + pin.c +  │ 定义专属rt_pin_ops接口表;实现注册函数、对外高层API;只做转发,不操作硬件 +  ▼ +BSP硬件层 drv_gpio.c + 实现stm32真实硬件函数;填充ops实例;系统启动完成设备注册 +``` + +核心思想:**框架层只定义接口契约,BSP层完成接口实现;应用层通过框架层间接调用硬件函数**。更换芯片,只重写BSP层ops实现,上层应用完全不用改动。 + +### 2.关键结构体 +#### ① 自定义设备结构体 struct rt_device_pin +```c +struct rt_device_pin +{ + struct rt_device parent; // 必须放在第一个,继承通用设备,用于系统注册 + const struct rt_pin_ops *ops; // PIN框架专属操作函数指针表 +}; +``` +- `parent`:是给RT‑Thread设备管理器识别的基类; +- `ops`:保存BSP传入的硬件函数表,框架层依靠这个指针间接调用底层。 + +#### ② 专属接口表 struct rt_pin_ops +这是一份接口契约,规定BSP必须实现7个函数: +```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)(...); + rt_err_t (*pin_detach_irq)(...); + rt_err_t (*pin_irq_enable)(...); + rt_base_t (*pin_get)(const char *name); +}; +``` + +|函数|功能| +|---|---| +|pin_mode|配置引脚输入输出、上拉下拉、开漏模式| +|pin_write|输出引脚高低电平| +|pin_read|读取引脚电平状态| +|pin_attach_irq|绑定外部中断用户回调函数| +|pin_detach_irq|解绑中断回调| +|pin_irq_enable|使能/关闭引脚外部中断| +|pin_get|字符串引脚名"PA.0"转换为数字引脚编码| + +> 注意区分两套ops: +> 1. `rt_pin_ops`:PIN框架自己定义,BSP实现硬件; +> 2. parent里面read/write/control:原生rt_device接口,pin.c内部实现`_pin_read/_pin_write/_pin_control`做兼容转发。 +> 业务开发优先使用`rt_pin_xxx`高层API,极少使用`rt_device_write`操作pin设备。 + +### 3.引脚编码规则 +RT‑Thread用一个整数编码GPIO引脚,把**端口号、引脚号压缩到同一个数字中**。 +- 高4位:端口编号(A=0,B=1,C=2……) +- 低4位:引脚编号0‑15 + +相关宏: +- `PIN_NUM(port,no)`:拼接端口和引脚得到编码; +- `PIN_PORT(pin)`:从编码解析出端口; +- `PIN_NO(pin)`:从编码解析出引脚号; +- `GET_PIN(port,no)`:方便用户直接使用的宏。 + +底层再通过端口编号计算寄存器基地址,移位得到引脚位掩码,调用HAL库操作寄存器。 + +### 4.PIN设备完整注册流程 +1. BSP层`rt_hw_pin_init()`,系统上电自动执行;先开启各个GPIO时钟; +2. 调用`rt_device_pin_register("pin",&_stm32_pin_ops,RT_NULL)`,把BSP的ops传给框架层; +3. 注册函数内部: + - 挂载框架内部兼容函数`_pin_read/_pin_write/_pin_control`到父类rt_device; + - **保存BSP传入ops指针到全局`_hw_pin.ops`**; + - 调用`rt_device_register()`,将设备注册进操作系统。 +4. 注册完成,应用层即可调用`rt_pin_mode / rt_pin_write`。 + +> `_stm32_pin_ops`是BSP层的实例,把`stm32_pin_mode`、`stm32_pin_write`等真实硬件函数填入ops结构体。 + +### 5.调用链示例 rt_pin_write() +``` +rt_pin_write(pin, value) // 应用层调用高层API +  → _hw_pin.ops->pin_write(&_hw_pin.parent,pin,value) //框架层函数指针跳转 +    → stm32_pin_write(dev,pin,value) //BSP层硬件实现 +      → HAL_GPIO_WritePin() //STM32 HAL库 +        → 操作BSRR/ODR硬件寄存器 +``` + +### 6.PIN外部中断简单流程 +1. 应用调用`rt_pin_attach_irq()`绑定回调; +2. BSP内部维护一张中断回调表,保存每个引脚对应的回调函数; +3. `rt_pin_irq_enable()`开启中断,配置NVIC优先级; +4. 硬件触发EXTI中断,进入中断服务函数; +5. HAL库处理中断标志,查表取出用户回调,执行用户编写的中断处理函数。 + +> 按键中断出现两次触发,一般是机械抖动,可以软件延时消抖或者使用button软件包。 + +## 三、为什么要自定义ops函数指针表 +如果框架层直接调用BSP的`stm32_pin_write()`函数: +- 框架代码直接依赖STM32驱动,换芯片必须修改pin.c框架源码; +- 分层被打破,框架与BSP强耦合。 + +使用ops函数指针表的优势: +1. **解耦**:框架只依赖接口声明,不依赖具体芯片实现; +2. **可移植**:更换硬件,仅重新实现一份ops表,框架代码、上层业务完全不动; +3. **C语言实现面向对象虚函数表效果,接口与实现分离**。 + +## 四、两种虚拟设备对比 test_dev 与自定义LED框架 +### 1.test_dev:直接复用rt_device通用框架 +直接使用rt_device自带`init/open/read/write/control`,不自定义ops结构体。 +- 注册:`INIT_DEVICE_EXPORT`开机自动注册; +- 控制使用自定义cmd,通过control接口读写设备状态; +- 使用方式:`rt_device_find`找到设备,调用标准read/write/control; +- 缺点:没有业务语义,参数靠buffer和cmd传递。 + +### 2.仿照PIN架构自定义LED虚拟驱动框架(本次作业) +模仿PIN整套模式,自己搭建三层架构: +1. 自定义`struct rt_led_ops`接口表,定义led_init、led_on、led_off等业务函数指针; +2. 自定义设备结构体`struct rt_device_led`,第一个成员是`struct rt_device parent`,第二个保存ops指针; +3. 框架层:实现注册函数`rt_device_led_register()`,实现`rt_led_on/rt_led_off`等对外语义API,只做函数指针转发,不操作硬件; +4. BSP层:实现虚拟或者真实硬件函数,填充ops实例; +5. `rt_hw_led_init()`配合`INIT_BOARD_EXPORT`上电自动注册设备; +6. 应用层直接调用`rt_led_on(pin)`,不需要find、open设备。 + +|对比项|通用test_dev|自定义LED框架(模仿PIN)| +|---|---|---| +|ops来源|直接使用rt_device原生接口|自定义`rt_led_ops`专属接口表| +|应用API|`rt_device_write / control`|`rt_led_on(pin)`语义化接口| +|传参方式|buffer缓冲区+命令码|直接传入业务参数| +|可读性|差,需要解析buffer协议|函数名直观易懂| +|可移植性|换硬件应用层需要改动|只需重写ops,上层代码无需修改| + +> 核心模式总结: +> PIN框架、自定义LED框架,都是**在rt_device基础上再封装一层**;rt_device只是为了能注册进系统设备管理器,真正业务能力全部来自自定义ops函数指针表。 + +## 五、学习总结 +1. RT‑Thread设备框架核心就是分层+函数指针,做到接口和实现分离,追求代码可移植; +2. 通用`rt_device`是所有设备的基础,提供统一注册、生命周期管理,但是接口缺少业务语义; +3. PIN框架是典型二次封装案例:继承rt_device,新增自定义ops,对外提供友好语义API;分为应用层‑框架层‑BSP层; +4. ops函数指针表就是C语言的接口契约:框架只规定函数长什么样,BSP完成硬件实现; +5. 虚拟设备分为两类:直接复用rt_device的test_dev;仿照PIN完整架构自定义ops的LED驱动; +6. 写自定义驱动时,框架层代码严禁出现硬件操作;硬件逻辑全部放到BSP层,上层应用只调用框架对外API,禁止直接调用BSP内部static函数。 \ No newline at end of file