From 8064e2649708be9425ea8d778703c47622784eaf Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Mon, 17 Aug 2026 16:02:28 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E6=A8=A1=E6=9D=BFCOMMIT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...47\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" | 0 ...47\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" new file mode 100644 index 0000000..e69de29 diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" new file mode 100644 index 0000000..e69de29 -- Gitee From d07af8db7c79c3b0fa4c758f5e3935c435bdac22 Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Tue, 18 Aug 2026 10:59:13 +0800 Subject: [PATCH 2/8] day1 commit --- ...00\345\244\251\344\275\234\344\270\232.md" | 1 + ...00\345\244\251\347\254\224\350\256\260.md" | 2113 +++++++++++++++++ 2 files changed, 2114 insertions(+) diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" index e69de29..5e681e6 100644 --- "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\200\345\244\251\344\275\234\344\270\232.md" @@ -0,0 +1 @@ +已完成 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" index e69de29..d5877d3 100644 --- "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\200\345\244\251\347\254\224\350\256\260.md" @@ -0,0 +1,2113 @@ +# Day 1 + +## Task 1 开始 RT-Thread + +### 1. RT-Thread 简介 + +RT-Thread 是一款面向嵌入式设备的实时操作系统(RTOS)。在 RT-Thread 的学习和开发过程中,一个基本的程序开发流程可以分为: + +**编码 → 编译 → 烧录 → 运行 → 调试** + +### 2. 编码 + +首先根据实验要求编写程序代码。 + +在 RT-Thread 中,可以通过创建线程、使用延时函数以及调用系统提供的 API 来实现不同的功能。 + +例如,一个简单的线程程序可以按照以下结构编写: + +```c +#include + +int main(void) +{ + rt_kprintf("Hello RT-Thread!\n"); + + while (1) + { + rt_thread_mdelay(1000); + } + + return 0; +} +``` + +其中: + +- `rt_kprintf()`:用于向 Console 输出调试信息。 +- `rt_thread_mdelay()`:使当前线程延时指定的毫秒数。 +- `while (1)`:使程序持续运行。 + +### 3. 编译 + +完成代码编写后,需要对工程进行编译。 + +编译的主要作用是将编写的 C/C++ 源代码转换为 MCU 可以运行的机器代码。 + +基本过程为: + +```text +源代码 + ↓ +预处理 + ↓ +编译 + ↓ +汇编 + ↓ +链接 + ↓ +生成可执行文件 +``` + +编译成功后,一般会生成用于烧录的程序文件,例如: + +```text +.elf +.bin +.hex +``` + +如果编译过程中出现 Error,需要根据控制台中的错误信息检查代码、头文件、驱动以及工程配置。 + +### 4. 烧录 + +编译成功后,将生成的程序下载到开发板 MCU 的 Flash 中。 + +常见烧录方式包括: + +- ST-Link +- J-Link +- USB +- 串口 Bootloader +- STM32CubeProgrammer + +烧录完成后,对开发板重新上电或复位,MCU 就会开始运行 Flash 中的程序。 + +### 5. 运行与 Console + +程序烧录成功后,可以通过串口 Console 查看 RT-Thread 的运行信息。 + +例如: + +```text + \ | / +- RT - Thread Operating System + / | \ + +Hello RT-Thread! +``` + +Console 是嵌入式开发过程中非常重要的调试工具,可以通过打印信息判断: + +- 程序是否正常启动 +- 线程是否正常运行 +- 函数是否被执行 +- 变量值是否正确 +- 程序运行到了哪个位置 + +### 6. 调试与修改 + +如果程序运行结果与预期不一致,需要重新检查程序并进行修改。 + +因此,RT-Thread 实际开发通常是一个不断循环的过程: + +```text +编码 + ↓ +编译 + ↓ +烧录 + ↓ +运行 + ↓ +观察 Console + ↓ +发现问题 + ↓ +修改代码 + ↓ +重新编译 +``` + +通过不断重复这一过程,最终完成程序功能。 + +------ + +## Task 2 MD 文档编写 + +Markdown(`.md`)是一种轻量级标记语言,可以使用简单的符号完成标题、代码、表格以及文字格式的编写。 + +### 1. 标题 + +Markdown 使用 `#` 表示标题,不同数量的 `#` 代表不同级别的标题。 + +```text +# 一级标题 + +## 二级标题 + +### 三级标题 + +#### 四级标题 + +##### 五级标题 + +###### 六级标题 +``` + +标题级别越高,使用的 `#` 数量越少。 + +------ + +### 2. 代码段 + +Markdown 可以使用三个反引号创建代码块。 + +例如 C 语言代码: + +```c +#include + +int main() +{ + return 0; +} +``` + +在三个反引号后添加语言名称,例如 `c`、`cpp`、`python`、`java` 等,可以让 Markdown 编辑器自动进行代码语法高亮。 + +例如: + +~~~text +```c +#include + +int main() +{ + return 0; +} +``` +~~~ + +------ + +### 3. 表格 + +Markdown 可以通过 `|` 和 `-` 创建表格。 + +例如: + +| t1 | t2 | +| ---- | ---- | +| 1 | 2 | + +对应的 Markdown 语法为: + +```text +| t1 | t2 | +| ---- | ---- | +| 1 | 2 | +``` + +表格可以用于记录实验参数、测试结果和不同配置之间的对比。 + +------ + +### 4. 行内代码与高亮 + +使用一对反引号可以标记代码、变量名、命令或者需要特别强调的内容。 + +例如: + +``` +高亮语法 +``` + +还可以用于表示函数名: + +``` +rt_kprintf() +``` + +文件名: + +``` +main.c +``` + +或者命令: + +``` +make +``` + +这种方式非常适合在技术文档中标记代码相关内容。 + +------ + +### 5. 加粗字体 + +使用两个星号包围文字可以实现加粗: + +**加粗字体** + +对应语法: + +```text +**加粗字体** +``` + +加粗通常用于强调: + +**重要步骤** + +**注意事项** + +**关键参数** + +------ + +## Task 3 开始 Spark1 最小示例 + +### 1. 创建 Project + +开始创建 Spark1 最小示例工程。 + +Project 名称: + +```text +01_hello +``` + +### 2. Project 参数选择 + +具体选择: + +![image-20260817133918955](figures/image-20260817133918955.png) + +### 3. 编写最小示例 + +程序代码: + +```c +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +int main(void) +{ + int count = 1; + + rt_kprintf("hello RSOC2026!\n"); + + while (count++) + { + LOG_D("Hello RT-Thread!"); + rt_thread_mdelay(1000); + } + + return RT_EOK; +} +``` + +### 4. 编译 + +编译操作: + +```text +点击编译按钮开始编译 +``` + +编译结果: + +```text +Build Finished. 0 errors, 0 warnings. (took 657ms) +``` + +### 5. 烧录 + +烧录方式: + +```text +点击下载按钮 +``` + +烧录结果: + +```text +Download in Progress: +Progress: 100% +File download complete +Time elapsed during download operation: 00:00:01.683 +Hard reset is performed +RUNNING Program ... + Address: : 0x8000000 +Application is running, Please Hold on... +Start operation achieved successfully +执行完毕, 耗时:1904ms. +``` + +### 6.连接串口 + +连接配置: + +![image-20260817134312360](figures/image-20260817134312360.png) + +### 7. 运行结果 + +程序烧录完成后的 Console 输出: + +```text + \ | + \ | / +- RT - Thread Operating System + / | \ 4.1.1 build Aug 17 2026 10:47:44 + 2006 - 2022 Copyright by RT-Thread team +hello RSOC2026! +``` + +### 7. 遇到的问题 + +问题描述: + +```text +debug无法通过 +``` + +问题原因: + +```text +st-link的版本过低 +``` + +解决方法: + +```text +找到stm32官方升级程序升级st-link的版本 +``` + +### 8. Task 3 总结 + +通过 Spark1 最小示例,完成了从 **Project 创建 → 参数配置 → 编码 → 编译 → 烧录 → 运行** 的基本开发流程。 + +## Git 入门 + +本文以 **Gitee + Git Bash + Windows** 为例,记录从创建仓库、拉取代码、配置 Git、配置 SSH,到日常开发、分支管理以及最终提交 Pull Request(PR)的完整流程。 + +示例仓库: + +```text +https://gitee.com/kylehe233/rt-thread2026.git +``` + +SSH 地址: + +```text +git@gitee.com:kylehe233/rt-thread2026.git +``` + +------ + +### 1. 在 Gitee 创建仓库 + +首先登录 Gitee,新建一个仓库,例如: + +```text +rt-thread2026 +``` + +创建完成后,可以看到两种常用的仓库地址。 + +HTTPS: + +```text +https://gitee.com/kylehe233/rt-thread2026.git +``` + +SSH: + +```text +git@gitee.com:kylehe233/rt-thread2026.git +``` + +两种方式都可以用于 `clone / pull / push`。 + +推荐后续使用 **SSH**,配置完成之后不需要频繁输入账号密码。 + +------ + +### 2. 将远程仓库拉取到本地 + +进入希望存放项目的目录: + +```bash +cd /d/rt_thread2026/git +``` + +使用 HTTPS 克隆: + +```bash +git clone https://gitee.com/kylehe233/rt-thread2026.git +``` + +例如: + +```text +28431@KyleHe MINGW64 /d/rt_thread2026/git +$ git clone https://gitee.com/kylehe233/rt-thread2026.git + +Cloning into 'rt-thread2026'... +remote: Enumerating objects: 4, done. +remote: Counting objects: 100% (4/4), done. +remote: Compressing objects: 100% (4/4), done. +remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0) +Receiving objects: 100% (4/4), done. +``` + +克隆完成后进入仓库: + +```bash +cd rt-thread2026 +``` + +也可以直接使用 SSH: + +```bash +git clone git@gitee.com:kylehe233/rt-thread2026.git +``` + +前提是已经完成 SSH Key 配置。 + +------ + +### 3. 配置 Git 用户信息 + +第一次使用 Git 时,需要配置用户名和邮箱。 + +```bash +git config --global user.name "kylehe233-ui" +git config --global user.email "kylehe233@gmail.com" +``` + +其中: + +```text +--global +``` + +表示该配置应用于当前电脑上的所有 Git 仓库。 + +查看当前 Git 配置: + +```bash +git config --list +``` + +例如: + +```text +user.name=kylehe233-ui +user.email=kylehe233@gmail.com +``` + +也可以分别查看: + +```bash +git config user.name +git config user.email +``` + +------ + +### 4. 配置 Gitee SSH Key + +为了避免每次 Push 都进行 HTTPS 身份验证,可以配置 SSH。 + +#### 4.1 生成 SSH Key + +执行: + +```bash +ssh-keygen -t rsa -b 4096 -C "kylehe@gmail.com" +``` + +例如: + +```text +Generating public/private rsa key pair. + +Enter file in which to save the key (/c/Users/28431/.ssh/id_rsa): +Enter passphrase for "/c/Users/28431/.ssh/id_rsa" (empty for no passphrase): +Enter same passphrase again: +``` + +如果不需要额外密码,可以直接连续按 Enter。 + +生成完成: + +```text +Your identification has been saved in /c/Users/28431/.ssh/id_rsa +Your public key has been saved in /c/Users/28431/.ssh/id_rsa.pub +``` + +会生成两个文件: + +```text +id_rsa +id_rsa.pub +``` + +其中: + +```text +id_rsa +``` + +是 **私钥**,不要发送给任何人。 + +```text +id_rsa.pub +``` + +是 **公钥**,需要添加到 Gitee。 + +------ + +#### 4.2 查看 SSH 公钥 + +Git Bash 中执行: + +```bash +cat ~/.ssh/id_rsa.pub +``` + +复制完整内容。 + +一般类似: + +```text +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... kylehe@gmail.com +``` + +------ + +#### 4.3 将公钥添加到 Gitee + +进入: + +```text +Gitee +→ 设置 +→ SSH 公钥 +→ 添加公钥 +``` + +将刚才 `id_rsa.pub` 中的内容粘贴进去并保存。 + +------ + +#### 4.4 测试 SSH + +执行: + +```bash +ssh -T git@gitee.com +``` + +第一次连接可能出现: + +```text +The authenticity of host 'gitee.com (...)' can't be established. +Are you sure you want to continue connecting (yes/no/[fingerprint])? +``` + +输入: + +```text +yes +``` + +如果出现: + +```text +Hi kylehe233-ui(@kylehe233)! You've successfully authenticated, but GITEE.COM does not provide shell access. +``` + +说明: + +```text +SSH 配置成功 +``` + +其中: + +```text +GITEE.COM does not provide shell access +``` + +不是报错,只是表示 Gitee 不提供 SSH Shell 登录。 + +Git 的: + +```text +clone +pull +fetch +push +``` + +都可以正常使用。 + +------ + +### 5. Git 仓库初始化 + +如果项目已经通过: + +```bash +git clone +``` + +拉取下来,**不需要再执行 `git init`**。 + +如果是一个本地已有项目,需要自己创建 Git 仓库,则进入项目目录执行: + +```bash +git init +``` + +例如: + +```bash +cd /d/rt_thread2026/my_project +git init +``` + +此时当前目录会生成: + +```text +.git +``` + +目录,表示当前文件夹已经成为 Git 仓库。 + +------ + +### 6. 添加远程仓库 + +如果是通过 `git clone` 获取的项目,通常已经自动配置了: + +```text +origin +``` + +如果是自己执行: + +```bash +git init +``` + +创建的本地仓库,需要手动添加远程仓库: + +```bash +git remote add origin git@gitee.com:kylehe233/rt-thread2026.git +``` + +查看远程仓库: + +```bash +git remote -v +``` + +例如: + +```text +origin git@gitee.com:kylehe233/rt-thread2026.git (fetch) +origin git@gitee.com:kylehe233/rt-thread2026.git (push) +``` + +------ + +### 7. 修改远程仓库 URL + +如果最开始使用的是 HTTPS: + +```text +https://gitee.com/kylehe233/rt-thread2026.git +``` + +后来希望改为 SSH,可以执行: + +```bash +git remote set-url origin git@gitee.com:kylehe233/rt-thread2026.git +``` + +再次检查: + +```bash +git remote -v +``` + +这个命令的作用可以理解为: + +```text +设置当前 Git 仓库的远程仓库地址 +``` + +以后: + +```bash +git pull +git fetch +git push +``` + +都会使用新的地址。 + +------ + +## Git 基本操作 + +### 8. 查看仓库状态 + +这是日常使用频率最高的命令之一: + +```bash +git status +``` + +可以查看: + +- 哪些文件被修改 +- 哪些文件还没有加入暂存区 +- 哪些文件已经加入暂存区 +- 当前所在分支 + +建议每次: + +```text +add +commit +push +``` + +之前都可以执行一次: + +```bash +git status +``` + +------ + +### 9. 拉取远程代码 + +在开始修改代码之前,建议先同步远程仓库。 + +```bash +git pull origin master +``` + +含义: + +```text +从 origin 远程仓库 +拉取 master 分支 +并合并到当前本地分支 +``` + +如果当前分支已经和远程分支建立关联,也可以直接: + +```bash +git pull +``` + +------ + +### 10. 添加修改到暂存区 + +添加指定文件: + +```bash +git add 文件路径 +``` + +例如: + +```bash +git add README.md +``` + +或者: + +```bash +git add src/main.c +``` + +添加整个目录: + +```bash +git add src/ +``` + +添加当前目录下全部修改: + +```bash +git add . +``` + +检查: + +```bash +git status +``` + +------ + +### 11. 提交修改 + +提交到本地 Git 仓库: + +```bash +git commit -m "message" +``` + +例如: + +```bash +git commit -m "add uart driver" +``` + +或者: + +```bash +git commit -m "fix serial configuration" +``` + +一个比较推荐的提交信息格式是: + +```text +add: 新增功能 +fix: 修复问题 +update: 更新功能 +docs: 修改文档 +refactor: 重构代码 +test: 测试相关修改 +``` + +例如: + +```bash +git commit -m "fix: repair uart initialization" +``` + +------ + +### 12. 推送到 Gitee + +推送当前提交到远程 `master`: + +```bash +git push origin master +``` + +![0e2c3d99-08de-41e0-b846-3c7a02cdf538](figures/0e2c3d99-08de-41e0-b846-3c7a02cdf538.png) + +如果本地分支第一次推送,可以使用: + +```bash +git push -u origin master +``` + +其中: + +```text +-u +``` + +会建立: + +```text +本地 master → origin/master +``` + +的跟踪关系。 + +以后可以直接: + +```bash +git push +``` + +------ + +## Git 远程同步 + +### 13. fetch 获取远程更新 + +执行: + +```bash +git fetch origin master +``` + +作用是: + +```text +获取远程 master 分支的最新信息 +``` + +但 **不会自动把代码合并到当前工作分支**。 + +因此: + +```bash +git fetch +``` + +和: + +```bash +git pull +``` + +并不完全一样。 + +可以简单理解为: + +```text +git fetch = 下载远程更新,但暂时不合并 +git pull = 下载远程更新 + 合并 +``` + +实际上: + +```bash +git pull +``` + +通常相当于: + +```text +git fetch ++ +git merge +``` + +------ + +### 14. 查看远程仓库 + +执行: + +```bash +git remote -v +``` + +用于查看当前仓库配置的远程地址。 + +例如: + +```text +origin git@gitee.com:kylehe233/rt-thread2026.git (fetch) +origin git@gitee.com:kylehe233/rt-thread2026.git (push) +``` + +其中: + +```text +origin +``` + +只是远程仓库的默认名字。 + +------ + +## Git 分支 + +### 15. 查看当前分支 + +执行: + +```bash +git branch +``` + +例如: + +```text +* master +``` + +`*` 表示当前所在分支。 + +查看所有本地和远程分支: + +```bash +git branch -a +``` + +------ + +### 16. 创建新的开发分支 + +例如需要创建: + +```text +test +``` + +分支。 + +执行: + +```bash +git checkout -b test +``` + +![image-20260817153038429](figures/image-20260817153038429.png) + +这个命令相当于: + +```bash +git branch test +git checkout test +``` + +即: + +```text +创建 test 分支 ++ +切换到 test 分支 +``` + +再次查看: + +```bash +git branch +``` + +应该看到: + +```text +master +* test +``` + +------ + +### 17. 切换分支 + +切换回: + +```text +master +``` + +执行: + +```bash +git checkout master +``` + +切换到: + +```text +test +``` + +执行: + +```bash +git checkout test +``` + +新版 Git 也可以使用: + +```bash +git switch master +``` + +以及: + +```bash +git switch test +``` + +创建并切换: + +```bash +git switch -c test +``` + +------ + +## 推荐的实际开发流程 + +假设: + +```text +master = 稳定主分支 +test = 开发测试分支 +``` + +开发时不要直接在: + +```text +master +``` + +中大量修改代码。 + +推荐: + +```text +master + │ + └── test + │ + ├── 修改代码 + ├── commit + ├── push + │ + └── PR + │ + ▼ + master +``` + +------ + +### 18. 第一次创建 test 分支 + +首先确保当前 `master` 是最新状态: + +```bash +git checkout master +``` + +然后: + +```bash +git pull origin master +``` + +创建: + +```text +test +``` + +分支: + +```bash +git checkout -b test +``` + +将 `test` 推送到 Gitee: + +```bash +git push -u origin test +``` + +此时 Gitee 上就会同时存在: + +```text +master +test +``` + +两个分支。 + +------ + +### 19. 在 test 分支开发 + +确认当前分支: + +```bash +git branch +``` + +应该显示: + +```text +master +* test +``` + +然后正常修改代码。 + +查看修改: + +```bash +git status +``` + +加入暂存区: + +```bash +git add . +``` + +提交: + +```bash +git commit -m "add: implement new feature" +``` + +上传到远程 `test`: + +```bash +git push origin test +``` + +![2e9a5471-cfdf-4477-bdc8-cc09204a69db](figures/2e9a5471-cfdf-4477-bdc8-cc09204a69db.png) + +如果已经建立 upstream: + +```bash +git push +``` + +即可。 + +------ + +## Gitee PR 流程 + +### 20. 将 test 合并到 master + +当: + +```text +test +``` + +分支中的代码已经开发、编译、测试完成之后,不建议直接在本地强行修改 `master`。 + +推荐通过 Gitee: + +```text +Pull Request +``` + +进行合并。 + +完整关系: + +```text +test + │ + │ push + ▼ +origin/test + │ + │ Pull Request + ▼ +origin/master +``` + +------ + +### 21. 创建 Pull Request + +首先确保最新代码已经提交: + +```bash +git status +``` + +然后: + +```bash +git add . +``` + +提交: + +```bash +git commit -m "test: complete feature validation" +``` + +推送: + +```bash +git push origin test +``` + +然后进入 Gitee 仓库页面。 + +进入: + +```text +Pull Requests +``` + +创建新的 Pull Request。 + +选择: + +```text +源分支:test + +目标分支:master +``` + +也就是: + +```text +test + ↓ +master +``` + +填写 PR 标题,例如: + +```text +Merge test branch into master +``` + +![image-20260817153225643](figures/image-20260817153225643.png) + +PR 描述可以说明: + +```text +1. 新增了哪些功能 +2. 修改了哪些文件 +3. 修复了哪些问题 +4. 是否已经编译通过 +5. 是否已经在开发板上测试 +``` + +确认无误后创建 Pull Request。 + +------ + +### 22. 检查并合并 PR + +在 Gitee 上检查: + +```text +代码差异 +提交记录 +文件修改 +是否存在冲突 +``` + +确认代码没有问题后: + +```text +Merge +``` + +将: + +```text +test +``` + +合并到: + +```text +master +``` + +最终: + +```text +test + │ + │ Pull Request + ▼ +master +``` + +合并完成。 + +------ + +### 23. PR 合并后同步本地 master + +Gitee 上完成合并之后,本地的: + +```text +master +``` + +并不会自动更新。 + +首先切换: + +```bash +git checkout master +``` + +然后: + +```bash +git pull origin master +``` + +此时本地: + +```text +master +``` + +就和 Gitee 上最新的: + +```text +master +``` + +保持一致。 + +------ + +### 24. 继续下一轮开发 + +如果继续使用: + +```text +test +``` + +分支进行开发,建议先让它同步最新的: + +```text +master +``` + +切换到: + +```bash +git checkout test +``` + +然后: + +```bash +git merge master +``` + +此时: + +```text +test +``` + +就包含最新: + +```text +master +``` + +代码。 + +之后继续开发: + +```bash +git add . +git commit -m "add: next feature" +git push origin test +``` + +开发完成后再次: + +```text +test → PR → master +``` + +------ + +## 常用 Git 命令速查 + +| 作用 | 命令 | +| -------------- | --------------------------------------------------------- | +| 初始化仓库 | `git init` | +| HTTPS 克隆仓库 | `git clone https://gitee.com/kylehe233/rt-thread2026.git` | +| SSH 克隆仓库 | `git clone git@gitee.com:kylehe233/rt-thread2026.git` | +| 查看状态 | `git status` | +| 查看分支 | `git branch` | +| 查看所有分支 | `git branch -a` | +| 创建并切换分支 | `git checkout -b test` | +| 切换分支 | `git checkout test` | +| 拉取远程代码 | `git pull origin master` | +| 获取远程信息 | `git fetch origin master` | +| 添加单个文件 | `git add file` | +| 添加所有修改 | `git add .` | +| 提交修改 | `git commit -m "message"` | +| 推送 master | `git push origin master` | +| 推送 test | `git push origin test` | +| 查看远程仓库 | `git remote -v` | +| 添加远程仓库 | `git remote add origin URL` | +| 修改远程 URL | `git remote set-url origin URL` | +| 测试 Gitee SSH | `ssh -T git@gitee.com` | +| 查看提交历史 | `git log` | +| 简洁查看提交 | `git log --oneline` | +| 查看代码修改 | `git diff` | + +------ + +## 日常开发最简流程 + +如果当前已经在: + +```text +test +``` + +分支开发,一般每天最常用的就是: + +```bash +git pull +``` + +修改代码后: + +```bash +git status +``` + +然后: + +```bash +git add . +``` + +提交: + +```bash +git commit -m "fix: description" +``` + +最后: + +```bash +git push +``` + +也就是: + +```text +拉取最新代码 + ↓ +修改代码 + ↓ +git status + ↓ +git add . + ↓ +git commit + ↓ +git push +``` + +------ + +## 一个完整示例 + +第一次获取项目: + +```bash +git clone git@gitee.com:kylehe233/rt-thread2026.git +``` + +进入项目: + +```bash +cd rt-thread2026 +``` + +拉取最新主分支: + +```bash +git checkout master +git pull origin master +``` + +创建测试分支: + +```bash +git checkout -b test +``` + +上传分支: + +```bash +git push -u origin test +``` + +开发完成: + +```bash +git status +git add . +git commit -m "add: complete uart demo" +git push origin test +``` + +然后在 Gitee: + +```text +test +→ Pull Request +→ master +→ Merge +``` + +PR 合并完成之后: + +```bash +git checkout master +git pull origin master +``` + +此时整个开发闭环完成。 + +## RT-Thread 的启动流程 + +RT-Thread 的启动过程主要完成 **硬件初始化、内核初始化、系统线程创建、组件初始化以及用户 `main()` 函数运行** 等工作。 + +![rtt_startup](notes/figures/rtt_startup.png) + +根据不同的编译器,程序从启动文件 `startup_xx.S` 进入 RT-Thread 的方式有所不同: + +```text +startup_xx.S + │ + ├── MDK → $Sub$$main() + ├── IAR → __low_level_init() + └── GCC → entry() + │ + ↓ + rtthread_startup() +``` + +虽然不同编译器的入口函数不同,但最终都会进入: + +```c +rtthread_startup(); +``` + +`rtthread_startup()` 是 RT-Thread 系统启动的核心函数,负责依次完成 RT-Thread 内核和相关组件的初始化。 + +### 1. 关闭中断 + +系统首先执行: + +```c +rt_hw_interrupt_disable(); +``` + +在系统初始化完成之前暂时关闭中断,避免初始化过程中被外部中断打断,从而保证启动过程能够按照预定顺序执行。 + +启动流程: + +```text +rtthread_startup() + ↓ +rt_hw_interrupt_disable() +``` + +------ + +### 2. 板级硬件初始化 + +随后执行: + +```c +rt_hw_board_init(); +``` + +该函数主要用于完成与开发板硬件相关的初始化,例如: + +- 系统时钟初始化 +- SysTick 初始化 +- UART 初始化 +- GPIO 初始化 +- Heap 初始化 +- 其他 BSP 相关硬件初始化 + +在 `rt_hw_board_init()` 过程中,还会调用: + +```c +rt_components_board_init(); +``` + +用于执行注册到 **Board Init** 阶段的初始化函数。 + +流程可以表示为: + +```text +rt_hw_board_init() + │ + └── rt_components_board_init() + │ + └── board init functions +``` + +因此,这一阶段的主要作用就是为 RT-Thread 内核运行准备最基本的硬件环境。 + +------ + +### 3. 显示 RT-Thread 版本信息 + +硬件初始化完成后执行: + +```c +rt_show_version(); +``` + +该函数会通过 Console 输出 RT-Thread 的版本以及系统启动 Logo。 + +例如: + +```text + \ | / +- RT - Thread Operating System + / | \ +``` + +如果能够在串口终端看到这些信息,说明 RT-Thread 已经完成了部分基础初始化,并且 Console 可以正常工作。 + +------ + +### 4. 初始化系统定时器 + +接下来执行: + +```c +rt_system_timer_init(); +``` + +用于初始化 RT-Thread 的系统定时器管理模块。 + +RT-Thread 中的定时器可以用于实现: + +- 软件定时 +- 周期任务 +- 超时处理 +- 延时操作 + +此时主要完成定时器相关数据结构的初始化。 + +------ + +### 5. 初始化系统调度器 + +随后执行: + +```c +rt_system_scheduler_init(); +``` + +该函数用于初始化 RT-Thread 的线程调度系统。 + +调度器负责管理系统中不同线程的运行状态,并根据线程的: + +- 优先级 +- 就绪状态 +- 时间片 + +决定 CPU 当前应该执行哪个线程。 + +可以将其理解为 RTOS 的核心功能之一。 + +------ + +### 6. 初始化 Signal 系统 + +接下来执行: + +```c +rt_system_signal_init(); +``` + +用于初始化 RT-Thread 的线程信号机制。 + +Signal 可以用于线程之间的异步通知和事件处理。 + +如果当前 RT-Thread 配置没有启用 Signal 功能,则这一部分可能不会被编译进入最终程序。 + +------ + +### 7. 创建 Main 线程 + +系统继续执行: + +```c +rt_application_init(); +``` + +该函数会创建 RT-Thread 中非常重要的 **Main Thread**。 + +内部会通过类似下面的方式初始化线程: + +```c +rt_thread_init(main_thread_entry); +``` + +因此: + +```text +rt_application_init() + │ + └── rt_thread_init(main_thread_entry) +``` + +需要注意,此时只是完成 Main Thread 的**创建和初始化**,并不意味着 `main_thread_entry()` 已经立即开始运行。 + +线程真正运行需要等到后面的调度器启动。 + +------ + +### 8. 创建 Timer 线程 + +随后执行: + +```c +rt_system_timer_thread_init(); +``` + +用于创建系统软件定时器线程: + +```c +rt_thread_init(rt_thread_timer_entry); +``` + +流程为: + +```text +rt_system_timer_thread_init() + │ + └── rt_thread_init(rt_thread_timer_entry) +``` + +该线程主要负责处理软件定时器相关任务。 + +------ + +### 9. 创建 Idle 线程 + +接下来执行: + +```c +rt_thread_idle_init(); +``` + +创建 RT-Thread 的空闲线程: + +```c +rt_thread_init(rt_thread_idle_entry); +``` + +流程为: + +```text +rt_thread_idle_init() + │ + └── rt_thread_init(rt_thread_idle_entry) +``` + +Idle Thread 是系统中优先级最低的线程。 + +当没有其他线程需要运行时,CPU 会运行 Idle Thread。 + +Idle Thread 还可以用于完成一些系统后台工作,例如: + +- 线程资源回收 +- CPU 空闲处理 +- 低功耗处理 + +------ + +### 10. 启动线程调度器 + +所有基础初始化和系统线程创建完成后,执行: + +```c +rt_system_scheduler_start(); +``` + +此时 RT-Thread 正式启动线程调度。 + +```text +rt_system_scheduler_start() + │ + ↓ + 根据调度规则执行 + │ + ┌──────┼────────────┐ + ↓ ↓ ↓ +main_thread timer_thread idle_thread + │ + └── 其他线程 +``` + +从这一刻开始,程序不再按照普通裸机程序单一的顺序执行,而是由 RT-Thread 调度器根据线程优先级和运行状态决定 CPU 执行哪个线程。 + +------ + +### 11. Main Thread 运行 + +调度器启动后,Main Thread 会执行: + +```c +main_thread_entry(); +``` + +`main_thread_entry()` 中一个非常重要的工作就是调用: + +```c +rt_components_init(); +``` + +用于初始化 RT-Thread 的各种组件。 + +流程为: + +```text +main_thread_entry() + │ + ↓ +rt_components_init() +``` + +`rt_components_init()` 会按照规定的初始化顺序依次执行不同阶段的自动初始化函数,例如: + +```text +rt_components_init() + │ + ├── pre-initialization functions + │ + ├── device init functions + │ + ├── components init functions + │ + ├── environment init functions + │ + └── applications init functions +``` + +也就是说,通过 RT-Thread 自动初始化机制注册的设备、组件和应用,会在这一阶段被依次初始化。 + +------ + +### 12. 进入用户 main() 函数 + +组件初始化完成后,最终进入用户编写的: + +```c +main(); +``` + +以 MDK 为例,图中通过: + +```c +$Super$$main(); +``` + +进入原始的 `main()` 函数。 + +因此可以简化理解为: + +```text +main_thread_entry() + │ + ├── rt_components_init() + │ + └── main() +``` + +我们平时在: + +```c +int main(void) +{ + /* 用户程序 */ + + return 0; +} +``` + +中编写的代码,实际上已经是在 **RT-Thread Main Thread 中运行**。 + +------ + +### 13. RT-Thread 完整启动流程 + +整个启动过程可以总结为: + +```text +startup_xx.S + ↓ +编译器入口 + ↓ +rtthread_startup() + ↓ +rt_hw_interrupt_disable() + ↓ +rt_hw_board_init() + │ + └── rt_components_board_init() + ↓ +rt_show_version() + ↓ +rt_system_timer_init() + ↓ +rt_system_scheduler_init() + ↓ +rt_system_signal_init() + ↓ +rt_application_init() + │ + └── 创建 Main Thread + ↓ +rt_system_timer_thread_init() + │ + └── 创建 Timer Thread + ↓ +rt_thread_idle_init() + │ + └── 创建 Idle Thread + ↓ +rt_system_scheduler_start() + ↓ +启动线程调度 + ↓ +main_thread_entry() + ↓ +rt_components_init() + ↓ +各种组件自动初始化 + ↓ +main() + ↓ +执行用户程序 +``` + +因此,可以将 RT-Thread 的启动过程概括为: + +**启动文件 → RT-Thread 启动函数 → 硬件初始化 → 内核初始化 → 创建系统线程 → 启动调度器 → 组件初始化 → 进入 `main()` 用户程序。** + +其中最重要的几个函数可以重点记忆: + +| 函数 | 作用 | +| ------------------------------- | ------------------------ | +| `rtthread_startup()` | RT-Thread 系统启动入口 | +| `rt_hw_board_init()` | 开发板硬件初始化 | +| `rt_components_board_init()` | Board 阶段组件初始化 | +| `rt_system_timer_init()` | 系统定时器初始化 | +| `rt_system_scheduler_init()` | 调度器初始化 | +| `rt_application_init()` | 创建 Main Thread | +| `rt_system_timer_thread_init()` | 创建 Timer Thread | +| `rt_thread_idle_init()` | 创建 Idle Thread | +| `rt_system_scheduler_start()` | 启动线程调度 | +| `main_thread_entry()` | Main Thread 入口 | +| `rt_components_init()` | RT-Thread 组件自动初始化 | +| `main()` | 用户程序入口 | \ No newline at end of file -- Gitee From a963748af0f81d1ec458091c45953fcc09aa47d7 Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Tue, 18 Aug 2026 11:09:29 +0800 Subject: [PATCH 3/8] day1 commit --- "2026/\347\254\2543\347\273\204/README.md" | 1 - 1 file changed, 1 deletion(-) delete mode 100644 "2026/\347\254\2543\347\273\204/README.md" diff --git "a/2026/\347\254\2543\347\273\204/README.md" "b/2026/\347\254\2543\347\273\204/README.md" deleted file mode 100644 index 655bbb2..0000000 --- "a/2026/\347\254\2543\347\273\204/README.md" +++ /dev/null @@ -1 +0,0 @@ -# 第3组 \ No newline at end of file -- Gitee From d620c7418cbc5d1a8bb4c21e52d2dfb2f80c9226 Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Wed, 19 Aug 2026 02:57:51 +0800 Subject: [PATCH 4/8] day2 --- .../\344\275\234\344\270\232/README.md" | 1090 +++++++++++++++++ .../\344\275\234\344\270\232/thread_study.c" | 418 +++++++ 2 files changed, 1508 insertions(+) create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" new file mode 100644 index 0000000..4b56ab5 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" @@ -0,0 +1,1090 @@ +# Day2:RT-Thread 内核基础与多线程编程 + +## Part 1:理论知识总结 + +### 1. STM32F407 启动流程与 RT-Thread 引导机制 + +#### 1.1 STM32F407 与 RT-Thread 启动流程 + +STM32F407 上电或复位后,并不会直接执行用户编写的 `main()`,而是先完成 MCU 启动、C 运行环境初始化以及 RT-Thread 内核初始化。 + +整体启动流程如下: + +```text +STM32F407 上电 / 复位 + ↓ +读取中断向量表 // 获取初始 MSP 和 Reset_Handler 地址 + ↓ +设置 MSP // 设置 Cortex-M4 主栈指针 + ↓ +Reset_Handler // 进入复位处理函数 + ↓ +SystemInit() // 完成 MCU 基础系统初始化 + ↓ +初始化 .data / .bss // 建立 C 语言运行环境 + ↓ +rtthread_startup() // 进入 RT-Thread 启动流程 + ↓ +rt_hw_interrupt_disable() // 暂时关闭中断 + ↓ +rt_hw_board_init() // 初始化板级硬件 + ↓ +rt_system_timer_init() // 初始化系统定时器 + ↓ +rt_system_scheduler_init() // 初始化线程调度器 + ↓ +rt_application_init() // 创建 main 线程 + ↓ +rt_thread_idle_init() // 创建 idle 空闲线程 + ↓ +rt_system_scheduler_start() // 启动线程调度器 + ↓ +main_thread_entry() // main 线程入口 + ↓ +rt_components_init() // 执行组件自动初始化 + ↓ +INIT_APP_EXPORT() 注册函数 // 执行 APP 级自动初始化函数 + ↓ +main() // 进入用户程序 +``` + +其中,`Reset_Handler` 是 MCU 复位后的软件入口。启动阶段会完成 `.data` 数据复制和 `.bss` 清零,为后续 C 程序建立正确的运行环境。 + +RT-Thread 启动后通过 `rt_application_init()` 创建 Main Thread,再由调度器调度 `main_thread_entry()`,最终执行用户编写的 `main()`。因此,RT-Thread 中的 `main()` 实际运行在线程环境中。 + +------ + +#### 1.2 Main Thread 与 Idle Thread + +RT-Thread 启动过程中会自动创建多个系统线程,其中比较重要的是 Main Thread 和 Idle Thread。 + +Main Thread 用于执行用户程序: + +```text +rtthread_startup() + ↓ +rt_application_init() + ↓ +创建 Main Thread + ↓ +启动调度器 + ↓ +main_thread_entry() + ↓ +main() +``` + +Idle Thread 是系统中优先级最低的线程,当没有其他就绪线程时运行。 + +```text +没有其他 READY 线程 + ↓ +Idle Thread +``` + +Idle Thread 还可用于线程资源回收、空闲 Hook 和低功耗处理等后台任务。 + +------ + +#### 1.3 RT-Thread 自动初始化机制 + +RT-Thread 提供: + +```c +INIT_BOARD_EXPORT() +INIT_APP_EXPORT() +``` + +等自动初始化宏,使模块可以在系统启动过程中自动执行,而不需要在 `main()` 中手动调用。 + +其基本原理可以概括为: + +```text +初始化函数 + ↓ +INIT_BOARD_EXPORT(fn) +或 INIT_APP_EXPORT(fn) + ↓ +INIT_EXPORT(fn, level) + ↓ +生成初始化函数指针 + ↓ +放入指定 .rti_fn.x section + ↓ +链接器将同一级函数组织起来 + ↓ +系统启动 + ↓ +遍历初始化函数表 + ↓ +依次执行初始化函数 +``` + +`INIT_BOARD_EXPORT()` 与 `INIT_APP_EXPORT()` 的实现原理基本相同,主要区别在于初始化级别和执行时机不同。 + +| 宏 | 初始化阶段 | 典型用途 | +| --------------------- | ------------------------ | ------------------ | +| `INIT_BOARD_EXPORT()` | Board 级初始化阶段 | 板级硬件、底层驱动 | +| `INIT_APP_EXPORT()` | Application 级初始化阶段 | 用户应用、业务线程 | + +例如: + +```c +static int thread_study_init(void) +{ + /* 创建应用线程 */ + return 0; +} + +INIT_APP_EXPORT(thread_study_init); +``` + +系统运行到组件初始化阶段时,会自动调用 `thread_study_init()`,因此无需在 `main()` 中再次手动调用。 + +这种机制可以降低不同模块与 `main()` 之间的耦合,提高代码的模块化程度。 + +------ + +### 2. RT-Thread 内核基础与对象模型 + +#### 2.1 RT-Thread 对象模型 + +RT-Thread 使用 C 语言实现,但内核中采用了类似面向对象的设计思想。 + +线程、信号量、互斥量、事件、定时器和设备等都可以作为 RT-Thread 内核对象进行统一管理。 + +```text + RT-Thread Object + │ + ┌──────────────┼──────────────┐ + │ │ │ + Thread Semaphore Timer +``` + +线程对象基于: + +```c +struct rt_thread +``` + +进行描述,而 `struct rt_thread` 又包含 RT-Thread 基础对象相关信息。 + +因此可以将线程控制块理解为 RT-Thread 为每一个线程建立的一份“线程档案”。 + +------ + +#### 2.2 线程控制块 TCB + +线程控制块 TCB(Thread Control Block)用于保存线程的各种运行信息。 + +RT-Thread 中通过: + +```c +struct rt_thread +``` + +定义线程控制块。 + +其中包含: + +```text +线程名称 +线程状态 +线程优先级 +线程入口函数 +线程栈 +时间片 +线程错误码 +调度相关信息 +定时器 +用户数据 +``` + +一个线程可以简单理解为: + +```text +Thread + │ + ├── 线程入口函数 + ├── 线程栈 + └── TCB + ├── stat + ├── current_priority + ├── init_tick + ├── sp + └── 其他线程信息 +``` + +作业中需要重点理解以下成员: + +| 成员 | 作用 | +| ------------------ | -------------------------------------------- | +| `stat` | 保存线程当前运行状态 | +| `current_priority` | 保存线程当前优先级 | +| `init_tick` | 保存线程初始化时设置的时间片 | +| `sp` | 保存线程当前栈指针,用于线程上下文保存与恢复 | + +------ + +#### 2.3 线程栈 + +每个线程都有自己独立的线程栈。 + +线程栈主要用于保存: + +- CPU 上下文 +- 函数局部变量 +- 函数调用信息 +- 寄存器现场 +- 函数返回地址 + +线程切换时,可以简单理解为: + +```text +线程 A RUNNING + ↓ +保存 A 的上下文到线程 A 栈 + ↓ +加载线程 B 栈中的上下文 + ↓ +线程 B RUNNING +``` + +因此线程栈是实现线程上下文切换的重要基础。 + +如果栈空间设置过小,可能导致 Stack Overflow,因此创建线程时需要根据函数调用深度和局部变量大小合理设置线程栈。 + +------ + +#### 2.4 静态线程与动态线程 + +RT-Thread 主要支持两种线程创建方式: + +```text +线程 + │ + ├── 静态线程 + └── 动态线程 +``` + +两者最主要的区别是线程控制块和线程栈的内存来源不同。 + +##### 静态线程 + +静态线程的 TCB 和线程栈由程序提前定义: + +```c +static struct rt_thread thread; + +static rt_uint8_t thread_stack[1024]; +``` + +然后使用: + +```c +rt_thread_init(); +``` + +进行初始化。 + +##### 动态线程 + +动态线程使用: + +```c +rt_thread_create(); +``` + +创建,由系统从动态内存中申请线程控制块和线程栈。 + +两种方式对比如下: + +| 对比项 | 动态线程 | 静态线程 | +| -------------- | -------------------- | ------------------ | +| API | `rt_thread_create()` | `rt_thread_init()` | +| TCB | 系统动态分配 | 用户提前定义 | +| 线程栈 | 系统动态分配 | 用户提前定义 | +| 内存来源 | Heap | 静态存储区 | +| 灵活性 | 较高 | 相对较低 | +| 资源确定性 | 较低 | 较高 | +| 运行时动态申请 | 需要 | 不需要 | + +动态线程更加灵活,但依赖运行时动态内存,可能受到 Heap 空间不足和内存碎片的影响。 + +静态线程的 TCB 和栈空间在程序运行前基本已经确定,因此: + +```text +内存大小确定 +内存位置确定 +资源占用确定 +运行行为更加可预测 +``` + +所以在工业控制、汽车电子以及对可靠性和实时确定性要求较高的系统中,通常更加倾向使用静态线程。 + +------ + +### 3. 线程状态机与调度算法 + +#### 3.1 RT-Thread 线程五态模型 + +RT-Thread 中线程主要存在以下五种状态: + +| 状态 | 宏定义 | 含义 | +| ------ | ------------------- | ---------------------------- | +| 初始态 | `RT_THREAD_INIT` | 线程已经创建,但还未参与调度 | +| 就绪态 | `RT_THREAD_READY` | 已具备运行条件,等待 CPU | +| 运行态 | `RT_THREAD_RUNNING` | 当前正在 CPU 上执行 | +| 挂起态 | `RT_THREAD_SUSPEND` | 暂时不能运行,不参与正常调度 | +| 关闭态 | `RT_THREAD_CLOSE` | 线程生命周期已经结束 | + +线程状态变化可以表示为: + +```text + rt_thread_startup() + INIT ----------------------------> READY + │ + │ Scheduler + ↓ + RUNNING + / \ + / \ + delay / suspend / IPC / \ 线程结束 + ↓ ↓ + SUSPEND CLOSE + │ + │ resume + │ timeout + │ 条件满足 + ↓ + READY +``` + +需要注意: + +**READY 并不代表线程正在运行。** + +READY 只表示线程已经满足运行条件,仍需等待调度器分配 CPU。 + +------ + +#### 3.2 常用线程状态转换 API + +| API / 事件 | 典型状态变化 | 作用 | +| --------------------- | ----------------- | ---------------- | +| `rt_thread_create()` | → INIT | 动态创建线程 | +| `rt_thread_init()` | → INIT | 静态初始化线程 | +| `rt_thread_startup()` | INIT → READY | 启动线程 | +| 调度器 Scheduler | READY → RUNNING | 分配 CPU | +| `rt_thread_suspend()` | RUNNING → SUSPEND | 挂起线程 | +| `rt_thread_resume()` | SUSPEND → READY | 恢复线程 | +| `rt_thread_mdelay()` | RUNNING → SUSPEND | 当前线程主动延时 | +| Timeout | SUSPEND → READY | 延时结束 | +| 线程结束 | → CLOSE | 结束线程生命周期 | + +其中: + +```c +rt_thread_mdelay(ms); +``` + +不仅表示延时,同时还会使当前线程暂时退出调度。 + +```text +RUNNING + ↓ +rt_thread_mdelay() + ↓ +SUSPEND + ↓ +延时到期 + ↓ +READY +``` + +这使 CPU 可以在当前线程等待期间运行其他线程。 + +------ + +#### 3.3 线程优先级 + +RT-Thread 根据线程优先级决定线程被调度的先后顺序。 + +规则为: + +```text +优先级数字越小 + ↓ +实际线程优先级越高 +``` + +例如: + +```text +Thread A:priority = 10 +Thread B:priority = 20 +``` + +当 A 和 B 同时处于 READY 状态时: + +```text +Thread A +``` + +优先获得 CPU。 + +------ + +#### 3.4 抢占式调度 + +RT-Thread 支持基于优先级的抢占式调度。 + +如果当前正在运行一个低优先级线程,而此时一个更高优先级线程进入 READY: + +```text +低优先级线程 RUNNING + ↓ +高优先级线程 READY + ↓ +调度器比较优先级 + ↓ +高优先级线程抢占 + ↓ +高优先级线程 RUNNING +``` + +例如本实验线程配置: + +```text +th_monitor = priority 10 + +th_led1 = priority 16 +th_led2 = priority 16 +``` + +因此 `th_monitor` 在延时结束重新进入 READY 后,其优先级高于两个 LED 工作线程,可以优先获得 CPU。 + +这种机制能够保证更重要、更紧急的线程及时得到响应。 + +------ + +#### 3.5 时间片轮转 + +当多个线程具有相同优先级时,RT-Thread 可以使用时间片轮转机制让这些线程轮流使用 CPU。 + +例如: + +```text +Thread A +priority = 16 +time slice = 5 ticks + +Thread B +priority = 16 +time slice = 5 ticks +``` + +在两个线程持续处于可运行状态时,可以按照时间片进行轮转: + +```text +Thread A +运行一个时间片 + ↓ +Thread B +运行一个时间片 + ↓ +Thread A + ↓ +... +``` + +时间片单位为: + +```text +Tick +``` + +其实际时间长度由系统 Tick 频率决定。 + +例如系统配置为: + +```text +RT_TICK_PER_SECOND = 1000 +``` + +则: + +```text +1 tick = 1 ms +``` + +但如果 Tick 频率不同,实际时间也会发生变化,因此不能简单认为: + +```text +1 tick 永远等于 1 ms +``` + +需要根据实际系统配置确定。 + +------ + +#### 3.6 抢占、时间片和主动阻塞的区别 + +线程切换可能由不同原因产生。 + +```text +1. 高优先级线程进入 READY + ↓ + 抢占式调度 + +2. 同优先级线程时间片耗尽 + ↓ + 时间片轮转 + +3. 当前线程调用 delay / 等待 IPC + ↓ + 主动阻塞,让出 CPU +``` + +例如: + +```c +rt_thread_mdelay(500); +``` + +会使当前线程主动进入挂起状态。 + +这种线程切换与“时间片耗尽”不是同一个概念。 + +因此在多线程实验中,如果线程主动调用 `rt_thread_mdelay()`,看到线程交替运行时不能简单认为一定是时间片轮转造成的。 + +------ + +### 4. Part 1 小结 + +RT-Thread 启动后会完成板级初始化、内核初始化、系统线程创建以及调度器启动,最终由 Main Thread 执行用户 `main()`。 + +RT-Thread 使用统一的对象模型管理线程等内核对象,线程的运行信息保存在 `struct rt_thread` 线程控制块中,其中状态、优先级、时间片和栈指针是线程调度的重要依据。 + +线程可以通过动态方式和静态方式创建。动态线程使用方便、灵活,但依赖运行时动态内存;静态线程资源提前分配,确定性和可预测性更高。 + +RT-Thread 线程在 INIT、READY、RUNNING、SUSPEND 和 CLOSE 五种状态之间转换。调度器首先根据线程优先级进行抢占式调度,对于相同优先级的线程则可以使用时间片轮转机制共享 CPU。 + +这些机制共同构成了 RT-Thread 多线程运行和实时调度的基础。 + +--- + +## Part 2:多线程实战实验报告 + +### 1. 实验目的 + +本实验基于 STM32F407 星火 1 号开发板和 RT-Thread,实现动态线程、静态线程和监控线程,学习线程的创建、运行、挂起、恢复以及线程状态监控方法。 + +实验主要完成以下功能: + +- 动态创建 `th_led1`,控制红色 LED 周期翻转,并打印运行计数和系统 Tick。 +- 静态初始化 `th_led2`,控制蓝色 LED 周期翻转,并打印运行计数。 +- 动态创建高优先级监控线程 `th_monitor`,周期查看两个工作线程的运行状态和剩余栈空间。 +- 使用 KEY0 控制 `th_led1` 的挂起与恢复。 +- 使用 `INIT_APP_EXPORT()` 自动加载实验模块。 +- 使用 `MSH_CMD_EXPORT()` 导出 `thread_dump` 调试命令。 + +--- + +### 2. 实验环境 + +- 开发板:STM32F407 星火 1 号 +- 操作系统:RT-Thread +- 红色 LED:PF12 +- 蓝色 LED:PF11 +- KEY0:PC0 +- GPIO 引脚定义头文件: + +```c +#include "drv_common.h" +``` + +板载 LED 为低电平点亮: + +```text +PIN_LOW -> LED 点亮 +PIN_HIGH -> LED 熄灭 +``` + +--- + +### 3. 线程设计 + +本实验共创建 3 个线程: + +| 线程名称 | 创建方式 | 优先级 | 栈大小 | 时间片 | 功能 | +| ------------ | ---------- | -----: | ---------: | ------: | ------------------------------ | +| `th_led1` | 动态创建 | 16 | 1024 Bytes | 5 ticks | 翻转 LED_R,打印计数和 Tick | +| `th_led2` | 静态初始化 | 16 | 1024 Bytes | 5 ticks | 翻转 LED_B,打印计数 | +| `th_monitor` | 动态创建 | 10 | 1024 Bytes | 5 ticks | 按键检测、线程状态及栈空间监控 | + +其中: + +```text +th_monitor priority = 10 +th_led1 priority = 16 +th_led2 priority = 16 +``` + +RT-Thread 中优先级数字越小,实际优先级越高,因此 `th_monitor` 的优先级高于两个 LED 工作线程。 + +--- + +### 4. 自动初始化实验 + +实验模块通过: + +```c +INIT_APP_EXPORT(thread_study_init); +``` + +注册为 APP 级自动初始化函数,没有在 `main()` 中手动调用。 + +系统启动后的实际输出: + +```text +thread_study_init success! +th_led1 create success! +th_led2 init success! +th_monitor create success! +``` + +结果表明: + +- `INIT_APP_EXPORT()` 自动初始化成功。 +- `th_led1` 动态创建成功。 +- `th_led2` 静态初始化成功。 +- `th_monitor` 动态创建成功。 + +--- + +### 5. 动态线程 th_led1 实验 + +`th_led1` 使用: + +```c +rt_thread_create() +``` + +动态创建,参数为: + +```text +栈大小:1024 Bytes +优先级:16 +时间片:5 ticks +``` + +线程循环翻转红色 LED,并通过: + +```c +rt_tick_get() +``` + +读取当前系统 Tick。 + +部分实际串口数据: + +```text +[th_led1] count = 1, tick = 7 +[th_led1] count = 2, tick = 510 +[th_led1] count = 3, tick = 1013 +[th_led1] count = 4, tick = 1516 +[th_led1] count = 5, tick = 2019 +[th_led1] count = 6, tick = 2522 +``` + +相邻 Tick 差值: + +```text +510 - 7 = 503 ticks +1013 - 510 = 503 ticks +1516 - 1013 = 503 ticks +``` + +线程中设置: + +```c +rt_thread_mdelay(500); +``` + +实际运行中相邻输出约相差 503 ticks,与设置的 500 ms 周期基本一致。多出的少量 Tick 主要来自 LED 操作、串口打印和线程调度等程序执行时间。 + +实验过程中红色 LED 能够持续周期翻转,`count` 和 Tick 均持续增加,说明动态线程运行正常。 + +--- + +### 6. 静态线程 th_led2 实验 + +`th_led2` 使用静态方式初始化: + +```c +static struct rt_thread th_led2; +static rt_uint8_t th_led2_stack[1024]; +``` + +通过: + +```c +rt_thread_init() +``` + +完成线程初始化。 + +运行时实际输出: + +```text +[th_led2] count = 1 +[th_led1] count = 1, tick = 11 + +[th_led2] count = 2 +[th_led1] count = 2, tick = 514 + +[th_led2] count = 3 +[th_led1] count = 3, tick = 1017 + +[th_led2] count = 4 +[th_led1] count = 4, tick = 1520 +``` + +两个线程均能够持续运行,红色 LED 和蓝色 LED 均正常周期翻转。 + +`th_led1` 和 `th_led2` 的优先级均为 16,时间片均设置为 5 ticks。 + +需要注意的是,两个线程内部都调用了: + +```c +rt_thread_mdelay(500); +``` + +因此当前实验中两个线程的交替运行主要包含主动延时让出 CPU 的影响,不能仅通过当前串口输出直接证明 5 ticks 时间片轮转。 + +--- + +### 7. th_monitor 线程实验 + +`th_monitor` 使用动态方式创建: + +```text +优先级:10 +栈大小:1024 Bytes +时间片:5 ticks +``` + +监控线程周期获取 `th_led1` 和 `th_led2` 的: + +- 当前线程状态 +- 剩余栈空间 + +实际输出: + +```text +----- thread monitor ----- +th_led1: state=SUSPEND, stack_free=788 Bytes +th_led2: state=SUSPEND, stack_free=792 Bytes +-------------------------- +``` + +运行一段时间后: + +```text +----- thread monitor ----- +th_led1: state=SUSPEND, stack_free=780 Bytes +th_led2: state=SUSPEND, stack_free=776 Bytes +-------------------------- +``` + +后续剩余栈空间基本稳定在: + +```text +th_led1:约 780 Bytes +th_led2:约 776 Bytes +``` + +说明两个线程当前 1024 Bytes 的栈空间均具有较充足余量。 + +两个 LED 线程在监控时经常显示: + +```text +SUSPEND +``` + +这是因为两个工作线程均周期执行: + +```c +rt_thread_mdelay(500); +``` + +线程在延时期间会暂时处于挂起状态,因此该现象属于正常运行结果。 + +--- + +### 8. KEY0 挂起与恢复实验 + +KEY0 用于控制 `th_led1` 的运行状态。 + +第一次按键后实际输出: + +![image-20260819024604667](figures/image-20260819024604667.png) + +```text +[th_led1] count = 12, tick = 5551 +[KEY] request th_led1 suspend +[th_led1] suspend self +[th_led2] count = 13 +``` + +随后只有 `th_led2` 继续运行: + +```text +[th_led2] count = 14 +[th_led2] count = 15 +[th_led2] count = 16 +[th_led2] count = 17 +``` + +监控线程显示: + +```text +----- thread monitor ----- +th_led1: state=SUSPEND, stack_free=780 Bytes +th_led2: state=SUSPEND, stack_free=776 Bytes +-------------------------- +``` + +此时 `th_led1` 的计数停止在: + +```text +count = 12 +``` + +说明 `th_led1` 已成功挂起,而 `th_led2` 不受影响,继续正常运行。 + +再次按下 KEY0 后: + +![image-20260819024732173](figures/image-20260819024732173.png) + +```text +[KEY] th_led1 resume request +[th_led1] resumed +[th_led1] count = 13, tick = 10298 +``` + +随后: + +```text +[th_led1] count = 14, tick = 10802 +[th_led1] count = 15, tick = 11305 +[th_led1] count = 16, tick = 11808 +``` + +可以看到挂起前: + +```text +count = 12 +``` + +恢复后: + +```text +count = 13 +``` + +线程从原有执行位置继续运行,而不是重新开始计数。 + +该实验验证了: + +```text +RUNNING + ↓ +rt_thread_suspend() + ↓ +SUSPEND + +SUSPEND + ↓ +rt_thread_resume() + ↓ +READY + ↓ +Scheduler + ↓ +RUNNING +``` + +--- + +### 9. MSH 自定义命令实验 + +使用: + +```c +MSH_CMD_EXPORT(thread_dump, show thread status and stack usage); +``` + +将 `thread_dump` 导出为 MSH 命令。 + +在 FinSH/MSH 中执行: + +```text +msh > thread_dump +``` + +实际运行结果: + +![image-20260819024513682](figures/image-20260819024513682.png) + +```text +========== thread dump ========== +th_led1: + state : SUSPEND + priority : 16 + stack free : 780 Bytes + +th_led2: + state : SUSPEND + priority : 16 + stack free : 776 Bytes +================================= +``` + +结果表明 `thread_dump` 命令能够正常执行,并正确读取: + +- 线程状态 +- 当前优先级 +- 剩余栈空间 + +说明 `MSH_CMD_EXPORT()` 导出功能正常。 + +--- + +### 10. 实验中出现的问题及解决方法 + +#### 10.1 `GET_PIN` 未定义 + +首次编译时出现: + +```text +warning: implicit declaration of function 'GET_PIN' +error: 'F' undeclared +``` + +原因是当前 BSP 中 `GET_PIN` 宏对应的头文件没有包含。 + +解决方法: + +```c +#include "drv_common.h" +``` + +加入后重新编译,GPIO 引脚宏可以正常使用。 + +--- + +#### 10.2 `rt_thread_suspend()` 触发断言 + +最初尝试在 `th_monitor` 中直接执行: + +```c +rt_thread_suspend(th_led1); +``` + +运行时出现: + +```text +(thread == rt_thread_self()) assertion failed +at function: rt_thread_suspend +``` + +当前工程所使用的 RT-Thread 实现要求线程通过 `rt_thread_suspend()` 挂起自身。 + +解决方法: + +按键线程只设置暂停标志: + +```text +KEY0 + ↓ +设置暂停请求 +``` + +随后由 `th_led1` 自身执行: + +```c +rt_thread_suspend(rt_thread_self()); +rt_schedule(); +``` + +恢复时由监控线程执行: + +```c +rt_thread_resume(th_led1); +``` + +修改后挂起和恢复功能运行正常。 + +--- + +#### 10.3 多线程串口打印出现交叉 + +运行过程中出现过: + +```text +[th_led1] count = 5, tick[th_led2] count = 5 + = 2026 +``` + +以及: + +```text +0th_led1] count = 15, tick = 706[th_led2] count = 15 +``` + +这是因为多个线程同时使用: + +```c +rt_kprintf() +``` + +输出数据,线程切换过程中不同线程的串口字符串发生交叉。 + +该问题不会影响线程本身的正常运行。 + +实验中通过观察完整周期输出以及 `thread_dump` 独立结果判断线程状态。 + +--- + +### 11. 实验结果分析 + +本实验成功实现了一个动态工作线程、一个静态工作线程以及一个高优先级监控线程。 + +`th_led1` 使用 `rt_thread_create()` 动态创建,其 TCB 和线程栈由系统动态分配;`th_led2` 使用 `rt_thread_init()` 静态初始化,其 TCB 和 1024 Bytes 栈空间由程序提前定义,两种线程均能够正常运行。 + +两个 LED 工作线程优先级均为 16,监控线程优先级为 10,因此监控线程具有更高调度优先级。监控线程可以周期读取两个工作线程的状态和剩余栈空间。 + +KEY0 实验中,`th_led1` 挂起后计数停止,而 `th_led2` 继续运行;恢复后 `th_led1` 从原有计数继续执行,验证了线程挂起、恢复以及上下文保持过程。 + +通过 `thread_dump` 命令可以在 MSH 中手动读取线程状态、优先级和栈空间,验证了 RT-Thread Shell 命令导出机制。 + +--- + +### 12. 实验结论 + +本实验完成了 RT-Thread 多线程编程的主要功能: + +- 动态线程创建与启动 +- 静态线程初始化与启动 +- 多线程并发运行 +- 高优先级监控线程 +- 线程状态监控 +- 线程剩余栈空间检测 +- 按键控制线程挂起和恢复 +- `INIT_APP_EXPORT()` 自动初始化 +- `MSH_CMD_EXPORT()` 自定义 Shell 命令 + +通过实际运行结果,对 RT-Thread 的线程创建方式、线程状态变化、优先级调度和线程控制机制有了进一步理解。 + diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" new file mode 100644 index 0000000..30d5d0c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" @@ -0,0 +1,418 @@ +/* + * @file thread_study.c + * @author Kyle He + * @date 2026-08-19 + * @brief RT-Thread Day2 multithread study + */ + +#include +#include +#include "drv_common.h" + +/* ==================== 硬件引脚定义 ==================== */ + +/* 星火 1 号红色 LED:PF12,低电平点亮 */ +#define PIN_LED_R GET_PIN(F, 12) + +/* 星火 1 号蓝色 LED:PF11,低电平点亮 */ +#define PIN_LED_B GET_PIN(F, 11) + +/* 星火 1 号按键 KEY0:PC0,按下为低电平 */ +#define PIN_KEY0 GET_PIN(C, 0) + + +/* ==================== th_led1:动态线程 ==================== */ + +/* 动态线程句柄 */ +static rt_thread_t th_led1 = RT_NULL; + +/* LED1 手动挂起标志 */ +static volatile rt_bool_t led1_suspended = RT_FALSE; + + +/* LED1 线程入口 */ +static void led1_entry(void *parameter) +{ + rt_uint32_t count = 0; + rt_uint8_t led_state = PIN_HIGH; + + /* 配置红色 LED 为输出模式 */ + rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); + + /* 默认关闭红色 LED */ + rt_pin_write(PIN_LED_R, PIN_HIGH); + + while (1) + { + /* + * 检查挂起请求 + * 当前线程主动挂起自身 + */ + if (led1_suspended == RT_TRUE) + { + rt_kprintf("[th_led1] suspend self\n"); + + /* RUNNING -> SUSPEND */ + rt_thread_suspend(rt_thread_self()); + + /* 主动触发调度 */ + rt_schedule(); + + /* 被 resume 后从这里继续运行 */ + rt_kprintf("[th_led1] resumed\n"); + } + + /* 翻转红色 LED */ + led_state = !led_state; + rt_pin_write(PIN_LED_R, led_state); + + count++; + + /* 输出计数与系统 Tick */ + rt_kprintf("[th_led1] count = %d, tick = %d\n", + count, + rt_tick_get()); + + /* 主动延时 500 ms */ + rt_thread_mdelay(500); + } +} + + +/* ==================== th_led2:静态线程 ==================== */ + +/* 静态线程控制块 */ +static struct rt_thread th_led2; + +/* 静态线程栈:1024 Bytes */ +static rt_uint8_t th_led2_stack[1024]; + + +/* LED2 线程入口 */ +static void led2_entry(void *parameter) +{ + rt_uint32_t count = 0; + rt_uint8_t led_state = PIN_HIGH; + + /* 配置蓝色 LED 为输出模式 */ + rt_pin_mode(PIN_LED_B, PIN_MODE_OUTPUT); + + /* 默认关闭蓝色 LED */ + rt_pin_write(PIN_LED_B, PIN_HIGH); + + while (1) + { + /* 翻转蓝色 LED */ + led_state = !led_state; + rt_pin_write(PIN_LED_B, led_state); + + count++; + + /* 输出执行次数 */ + rt_kprintf("[th_led2] count = %d\n", count); + + /* 主动延时 500 ms */ + rt_thread_mdelay(500); + } +} + + +/* ==================== 线程状态工具函数 ==================== */ + +/* 将线程状态转换为字符串 */ +static const char *thread_state_str(rt_thread_t thread) +{ + rt_uint8_t state; + + state = thread->stat & RT_THREAD_STAT_MASK; + + switch (state) + { + case RT_THREAD_INIT: + return "INIT"; + + case RT_THREAD_READY: + return "READY"; + + case RT_THREAD_RUNNING: + return "RUNNING"; + + case RT_THREAD_SUSPEND: + return "SUSPEND"; + + case RT_THREAD_CLOSE: + return "CLOSE"; + + default: + return "UNKNOWN"; + } +} + + +/* + * 估算线程剩余栈空间 + * RT-Thread 初始化线程栈时通常使用 0x23 ('#') 填充 + */ +static rt_uint32_t thread_stack_free(rt_thread_t thread) +{ + rt_uint8_t *ptr; + rt_uint32_t free_size = 0; + + ptr = (rt_uint8_t *)thread->stack_addr; + + while ((free_size < thread->stack_size) && + (*ptr == '#')) + { + ptr++; + free_size++; + } + + return free_size; +} + + +/* ==================== KEY0 按键检测 ==================== */ + +static void key_scan(void) +{ + /* 检测按键按下 */ + if (rt_pin_read(PIN_KEY0) == PIN_LOW) + { + /* 软件消抖 */ + rt_thread_mdelay(50); + + if (rt_pin_read(PIN_KEY0) == PIN_LOW) + { + if (led1_suspended == RT_FALSE) + { + /* + * 第一次按键: + * 请求 th_led1 挂起自身 + */ + led1_suspended = RT_TRUE; + + rt_kprintf("[KEY] request th_led1 suspend\n"); + } + else + { + /* + * 第二次按键: + * 恢复 th_led1 + */ + led1_suspended = RT_FALSE; + + /* SUSPEND -> READY */ + rt_thread_resume(th_led1); + + rt_kprintf("[KEY] th_led1 resume request\n"); + + /* 重新调度 */ + rt_schedule(); + } + + /* + * 等待按键释放 + * 防止长按被识别为多次触发 + */ + while (rt_pin_read(PIN_KEY0) == PIN_LOW) + { + rt_thread_mdelay(10); + } + } + } +} + + +/* ==================== th_monitor:监控线程 ==================== */ + +/* 动态监控线程句柄 */ +static rt_thread_t th_monitor = RT_NULL; + + +/* monitor 线程入口 */ +static void monitor_entry(void *parameter) +{ + rt_tick_t last_monitor_tick; + + last_monitor_tick = rt_tick_get(); + + while (1) + { + /* 高频扫描按键 */ + key_scan(); + + /* 每 2 秒打印一次线程状态 */ + if ((rt_tick_get() - last_monitor_tick) >= + rt_tick_from_millisecond(2000)) + { + last_monitor_tick = rt_tick_get(); + + rt_kprintf("\n"); + rt_kprintf("----- thread monitor -----\n"); + + rt_kprintf("th_led1: state=%s, stack_free=%d Bytes\n", + thread_state_str(th_led1), + thread_stack_free(th_led1)); + + rt_kprintf("th_led2: state=%s, stack_free=%d Bytes\n", + thread_state_str(&th_led2), + thread_stack_free(&th_led2)); + + rt_kprintf("--------------------------\n"); + } + + /* monitor 扫描周期约 10 ms */ + rt_thread_mdelay(10); + } +} + + +/* ==================== MSH 命令 ==================== */ + +/* + * thread_dump + * 手动查看工作线程状态、优先级和剩余栈 + */ +static void thread_dump(void) +{ + rt_kprintf("\n"); + rt_kprintf("========== thread dump ==========\n"); + + rt_kprintf("th_led1:\n"); + rt_kprintf(" state : %s\n", + thread_state_str(th_led1)); + rt_kprintf(" priority : %d\n", + th_led1->current_priority); + rt_kprintf(" stack free : %d Bytes\n", + thread_stack_free(th_led1)); + + rt_kprintf("\n"); + + rt_kprintf("th_led2:\n"); + rt_kprintf(" state : %s\n", + thread_state_str(&th_led2)); + rt_kprintf(" priority : %d\n", + th_led2.current_priority); + rt_kprintf(" stack free : %d Bytes\n", + thread_stack_free(&th_led2)); + + rt_kprintf("=================================\n"); +} + + +/* 导出 MSH 命令 */ +MSH_CMD_EXPORT(thread_dump, show thread status and stack usage); + + +/* ==================== 模块初始化 ==================== */ + +static int thread_study_init(void) +{ + rt_err_t result; + + rt_kprintf("thread_study_init success!\n"); + + + /* ---------- 初始化 KEY0 ---------- */ + + /* KEY0 上拉输入,按下为低电平 */ + rt_pin_mode(PIN_KEY0, PIN_MODE_INPUT_PULLUP); + + + /* ---------- 动态创建 th_led1 ---------- */ + + /* + * th_led1 + * 栈大小:1024 Bytes + * 优先级:16 + * 时间片:5 ticks + */ + th_led1 = rt_thread_create("th_led1", + led1_entry, + RT_NULL, + 1024, + 16, + 5); + + if (th_led1 != RT_NULL) + { + /* INIT -> READY */ + rt_thread_startup(th_led1); + + rt_kprintf("th_led1 create success!\n"); + } + else + { + rt_kprintf("th_led1 create failed!\n"); + } + + + /* ---------- 静态初始化 th_led2 ---------- */ + + /* + * th_led2 + * 静态 TCB + * 静态栈:1024 Bytes + * 优先级:16 + * 时间片:5 ticks + */ + result = rt_thread_init(&th_led2, + "th_led2", + led2_entry, + RT_NULL, + th_led2_stack, + sizeof(th_led2_stack), + 16, + 5); + + if (result == RT_EOK) + { + /* INIT -> READY */ + rt_thread_startup(&th_led2); + + rt_kprintf("th_led2 init success!\n"); + } + else + { + rt_kprintf("th_led2 init failed!\n"); + } + + + /* ---------- 动态创建 th_monitor ---------- */ + + /* + * th_monitor + * 栈大小:1024 Bytes + * 优先级:10 + * 时间片:5 ticks + * + * 优先级 10 高于两个 LED 线程的 16 + */ + th_monitor = rt_thread_create("th_monitor", + monitor_entry, + RT_NULL, + 1024, + 10, + 5); + + if (th_monitor != RT_NULL) + { + /* INIT -> READY */ + rt_thread_startup(th_monitor); + + rt_kprintf("th_monitor create success!\n"); + } + else + { + rt_kprintf("th_monitor create failed!\n"); + } + + + return 0; +} + + +/* + * 自动初始化 + */ +INIT_APP_EXPORT(thread_study_init); -- Gitee From a37130fb79450e5ac649105a1739548b304218c0 Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Wed, 19 Aug 2026 02:59:19 +0800 Subject: [PATCH 5/8] day2 notes commit --- ...46\344\271\240\347\254\224\350\256\260.md" | 1590 +++++++++++++++++ 1 file changed, 1590 insertions(+) create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..02733fb --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\214\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1590 @@ +# Day 2 + +## Day 2 总结 + +Day 2 主要学习了 RT-Thread 中两个重要内容: + +```text +Day 2 + │ + ├── Thread + │ + │ ├── 线程控制块 + │ ├── 线程栈 + │ ├── 线程状态 + │ ├── 优先级 + │ ├── 时间片 + │ ├── 线程入口函数 + │ ├── Main Thread + │ ├── Idle Thread + │ ├── rt_thread_init() + │ ├── rt_thread_create() + │ ├── rt_thread_startup() + │ └── rt_thread_mdelay() + │ + └── SCons + ├── SConstruct + ├── SConscript + ├── DefineGroup() + ├── scons + ├── scons -c + ├── menuconfig + ├── pkgs --update + └── scons --target=mdk5 +``` + +其中线程部分最核心的是理解: + +**RT-Thread 通过调度器管理多个线程,线程依靠优先级、状态和时间片决定何时获得 CPU。** + +SCons 部分最核心的是理解: + +**SCons 根据 RT-Thread 的配置和 SConscript 脚本决定哪些源码参与工程构建,并能够自动编译或生成 MDK/IAR 工程。** + +--- + +## RT-Thread 的启动流程 + +RT-Thread 的完整启动流程已经在 Day 1 中进行了整理。 + +具体内容见: + +**Day 1 → RT-Thread 的启动流程** + +主要流程可以简单回顾为: + +```text +启动文件 + ↓ +rtthread_startup() + ↓ +硬件初始化 + ↓ +内核初始化 + ↓ +创建系统线程 + ↓ +启动调度器 + ↓ +main_thread_entry() + ↓ +rt_components_init() + ↓ +main() +``` + +本节不再重复展开,详细内容见 Day 1 笔记。 + +--- + +## 从裸机到多线程系统 + +在学习线程之前,首先需要理解为什么嵌入式系统需要 RTOS。 + +传统裸机程序通常采用 **前后台系统** 的方式运行。 + +后台程序一般是一个无限循环: + +```c +int main(void) +{ + init(); + + while (1) + { + ADC_Read(); + SPI_Read(); + USB_Packet(); + LCD_Update(); + Audio_Decode(); + File_Write(); + } +} +``` + +前台则主要由各种中断服务程序组成,例如: + +```c +void USB_ISR(void) +{ + Clear_Interrupt(); + Read_Packet(); +} +``` + +这种结构比较简单,但是随着系统功能增加,会逐渐暴露出一些问题: + +- 并发工作效率较低 +- 系统实时性难以保证 +- 代码之间耦合严重 +- 系统维护困难 +- 模块化程度较低 +- 软件复用能力较差 + +因此,在复杂嵌入式系统中,可以使用 RTOS 将整个系统拆分成多个相对独立的任务。 + +RT-Thread 中,任务的执行实体就是: + +**线程(Thread)** + +可以将复杂系统理解为: + +```text +复杂系统 + ↓ +拆分任务 + ↓ +任务 1 → 线程 1 +任务 2 → 线程 2 +任务 3 → 线程 3 +任务 4 → 线程 4 +``` + +这种方式体现了“**分而治之**”的思想。 + +--- + +## RTOS 的多任务机制 + +RTOS 的一个核心特点就是: + +**多任务运行。** + +例如,一个系统中可能同时存在: + +```text +线程 1:采集传感器数据 + +线程 2:更新 LCD + +线程 3:串口通信 + +线程 4:处理网络数据 +``` + +从用户角度来看,这些任务似乎是在“同时运行”。 + +但是对于单核 MCU 来说,CPU 在某一个瞬间实际上只能执行一个线程。 + +RTOS 通过快速切换线程,使多个线程轮流占用 CPU: + +```text +时间 → + +线程1 ███ + 线程2 ███ + 线程1 ███ + 线程3 ███ +``` + +由于 CPU 的执行速度非常快,因此从人的观察角度来看,这些线程就像是在并行运行。 + +RT-Thread 的调度器负责决定: + +**当前 CPU 应该运行哪个线程。** + +--- + +## 线程控制块 + +RT-Thread 内核需要保存每个线程的相关信息。 + +这些信息存放在线程控制块中。 + +线程控制块由: + +```c +struct rt_thread +``` + +定义。 + +线程控制块可以理解为 RT-Thread 内核为每个线程建立的一份“线程档案”。 + +其中会保存: + +- 线程名称 +- 线程状态 +- 线程优先级 +- 线程入口函数 +- 线程栈地址 +- 线程栈大小 +- 时间片 +- 线程错误码 +- 线程相关链表 +- 定时器 +- 用户数据等 + +RT-Thread 内核通过线程控制块对所有线程进行管理。 + +可以简单理解为: + +```text +线程 + │ + └── struct rt_thread + │ + ├── 线程名称 + ├── 线程状态 + ├── 优先级 + ├── 时间片 + ├── 线程栈 + ├── 入口函数 + └── 其他信息 +``` + +`struct rt_thread` 继承自 RT-Thread 的基础对象: + +```c +struct rt_object +``` + +因此线程本身也是 RT-Thread 的一种内核对象。 + +--- + +## 线程属性 + +一个线程需要多个属性才能被操作系统正确管理。 + +比较重要的属性包括: + +```text +线程 + │ + ├── 线程栈 + ├── 状态 + ├── 优先级 + ├── 时间片 + └── 入口函数 +``` + +下面分别进行说明。 + +--- + +## 线程栈 + +RT-Thread 中的每个线程都有自己独立的线程栈。 + +线程栈主要用于保存: + +- CPU 上下文 +- 函数局部变量 +- 函数调用信息 +- 寄存器信息 + +当线程发生切换时,RT-Thread 会将当前线程的运行现场保存在线程栈中。 + +当线程重新获得 CPU 后,再从线程栈中恢复运行现场。 + +因此可以将线程切换理解为: + +```text +线程 A 正在运行 + ↓ +保存线程 A 上下文到线程 A 栈 + ↓ +加载线程 B 栈中的上下文 + ↓ +线程 B 继续运行 +``` + +这也是 RTOS 能够让多个线程交替运行的重要基础。 + +线程栈还会存储函数中的局部变量。 + +因此,线程栈设置得太小可能会导致: + +```text +Stack Overflow +``` + +即栈溢出。 + +所以创建线程时需要根据线程中函数调用的复杂程度合理设置栈大小。 + +--- + +## 线程状态 + +RT-Thread 中线程主要存在以下几种状态: + +| 状态 | 宏定义 | 含义 | +| ---- | ---- | ---- | +| 初始状态 | `RT_THREAD_INIT` | 线程刚创建完成,还没有开始运行 | +| 就绪状态 | `RT_THREAD_READY` | 线程已经可以运行,正在等待 CPU | +| 运行状态 | `RT_THREAD_RUNNING` | 当前正在 CPU 上运行 | +| 挂起状态 | `RT_THREAD_SUSPEND` | 暂时不能运行,不参与调度 | +| 关闭状态 | `RT_THREAD_CLOSE` | 线程已经结束 | + +线程创建完成后首先处于: + +```text +INIT +``` + +调用启动函数后进入: + +```text +READY +``` + +当调度器选择该线程运行时: + +```text +READY + ↓ +RUNNING +``` + +如果线程主动延时或者等待某个资源: + +```text +RUNNING + ↓ +SUSPEND +``` + +等待条件满足后: + +```text +SUSPEND + ↓ +READY +``` + +线程执行结束后: + +```text +RUNNING + ↓ +CLOSE +``` + +因此线程的基本状态变化可以表示为: + +```text + 创建 + ↓ + INIT + ↓ + startup + ↓ + READY + ↓ + 调度器 + ↓ + RUNNING + ↙ ↘ + SUSPEND CLOSE + ↓ + READY +``` + +需要特别注意: + +**就绪状态并不代表线程当前正在执行。** + +就绪状态只表示: + +> 线程已经具备运行条件,正在等待调度器分配 CPU。 + +--- + +## 线程优先级 + +RT-Thread 使用线程优先级决定线程被调度的优先程度。 + +规则非常重要: + +**优先级数字越小,线程优先级越高。** + +例如: + +```text +优先级 5 → 高 + +优先级 10 → 中 + +优先级 20 → 低 +``` + +如果系统配置支持 256 个优先级,则优先级范围为: + +```text +0 ~ 255 +``` + +其中: + +```text +0 +``` + +表示最高优先级。 + +例如: + +```text +线程 A:priority = 10 + +线程 B:priority = 20 +``` + +如果 A 和 B 同时处于 READY 状态,则: + +```text +线程 A +``` + +会优先运行。 + +如果当前正在运行线程 B,此时线程 A 突然进入 READY: + +```text +线程 B:priority = 20 + ↓ +线程 A READY:priority = 10 + ↓ +线程 A 优先级更高 + ↓ +抢占线程 B + ↓ +线程 A RUNNING +``` + +这种机制称为: + +**抢占式调度。** + +--- + +## 线程时间片 + +每个线程在创建时还可以设置时间片: + +```text +tick +``` + +时间片的单位为: + +**操作系统时钟节拍 OSTick。** + +需要注意: + +**时间片主要用于相同优先级线程之间的调度。** + +例如: + +```text +线程 A +priority = 10 +tick = 10 + +线程 B +priority = 10 +tick = 5 +``` + +由于 A 和 B 的优先级相同,所以系统会采用时间片轮转的方式运行: + +```text +A 运行 10 tick + ↓ +B 运行 5 tick + ↓ +A 运行 10 tick + ↓ +B 运行 5 tick + ↓ +... +``` + +可以简单理解为: + +```text +优先级不同 + ↓ +优先级决定谁先运行 + +优先级相同 + ↓ +时间片决定轮流运行时间 +``` + +--- + +## 线程入口函数 + +线程实际执行的功能由线程入口函数实现。 + +线程入口函数的基本格式为: + +```c +void thread_entry(void *parameter) +{ +} +``` + +标准形式: + +```c +void (*entry)(void *parameter) +``` + +一个常见线程可以写成: + +```c +static void thread1_entry(void *parameter) +{ + rt_uint32_t count = 0; + + while (1) + { + rt_kprintf("thread1 count: %d\n", count++); + + rt_thread_mdelay(500); + } +} +``` + +其中: + +```c +rt_thread_mdelay(500); +``` + +表示线程延时 500 ms。 + +线程入口函数中通常需要存在让出 CPU 的操作。 + +例如: + +```c +rt_thread_mdelay(); +``` + +否则,如果一个高优先级线程一直: + +```c +while (1) +{ +} +``` + +并且从不主动阻塞或让出 CPU,那么低优先级线程可能一直无法获得运行机会。 + +因此: + +```text +线程执行任务 + ↓ +完成本次处理 + ↓ +delay / wait + ↓ +让出 CPU + ↓ +其他线程运行 +``` + +是多线程程序中常见的执行方式。 + +--- + +## 系统线程 + +RT-Thread 在启动过程中会自动创建一些系统线程。 + +其中比较重要的是: + +- 主线程 +- 空闲线程 +- 定时器线程 + +--- + +## 主线程 Main Thread + +RT-Thread 中: + +```c +main() +``` + +本身运行在一个线程中。 + +这个线程称为: + +**Main Thread(主线程)** + +系统启动时会创建 main 线程,其入口函数为: + +```c +main_thread_entry() +``` + +运行流程为: + +```text +rtthread_startup() + ↓ +rt_application_init() + ↓ +创建 Main Thread + ↓ +启动调度器 + ↓ +main_thread_entry() + ↓ +main() +``` + +也就是说,我们编写: + +```c +int main(void) +{ + rt_kprintf("hello world!\n"); + + return 0; +} +``` + +实际上这些代码是在 Main Thread 中执行的。 + +--- + +## 空闲线程 Idle Thread + +空闲线程是 RT-Thread 中优先级最低的线程。 + +当系统中没有其他就绪线程时: + +```text +没有其他 READY Thread + ↓ + Idle Thread +``` + +空闲线程的特点: + +- 优先级最低 +- 系统始终存在 +- 通常一直保持 READY +- 不能被挂起 +- 通常内部是一个无限循环 + +空闲线程还有一些特殊作用。 + +### 回收线程资源 + +动态线程运行结束后,并不会立刻完成全部资源回收。 + +线程结束后会进入: + +```text +rt_thread_defunct +``` + +僵尸线程队列。 + +之后由 Idle Thread 回收这些线程占用的资源。 + +### 执行 Idle Hook + +RT-Thread 允许给空闲线程设置 Hook 函数。 + +因此可以在 Idle Thread 中完成: + +- 功耗管理 +- 看门狗喂狗 +- CPU 空闲处理 + +等后台工作。 + +--- + +## 线程管理 API + +RT-Thread 提供两种主要方式创建线程: + +```text +线程 + │ + ├── 静态线程 + │ + └── 动态线程 +``` + +两种方式最大的区别在于: + +**线程控制块和线程栈由谁分配。** + +--- + +## 静态线程 + +静态线程的: + +- 线程控制块 +- 线程栈 + +由用户自己定义。 + +一般设置为: + +```c +static struct rt_thread thread1; + +static rt_uint8_t thread1_stack[1024]; +``` + +静态线程使用: + +```c +rt_thread_init(); +``` + +进行初始化。 + +函数形式为: + +```c +rt_err_t rt_thread_init( + struct rt_thread *thread, + const char *name, + void (*entry)(void *parameter), + void *parameter, + void *stack_start, + rt_uint32_t stack_size, + rt_uint8_t priority, + rt_uint32_t tick +); +``` + +主要参数如下: + +| 参数 | 含义 | +| ---- | ---- | +| `thread` | 线程控制块 | +| `name` | 线程名称 | +| `entry` | 线程入口函数 | +| `parameter` | 入口函数参数 | +| `stack_start` | 线程栈起始地址 | +| `stack_size` | 线程栈大小 | +| `priority` | 线程优先级 | +| `tick` | 时间片 | + +例如: + +```c +ALIGN(RT_ALIGN_SIZE) +static char thread2_stack[1024]; + +static struct rt_thread thread2; + +static void thread2_entry(void *parameter) +{ + while (1) + { + rt_kprintf("thread2\n"); + + rt_thread_mdelay(500); + } +} +``` + +初始化: + +```c +rt_thread_init( + &thread2, + "thread2", + thread2_entry, + RT_NULL, + &thread2_stack[0], + sizeof(thread2_stack), + 20, + 10 +); +``` + +但是此时线程仍然只是: + +```text +INIT +``` + +并没有真正参与调度。 + +--- + +## 动态线程 + +动态线程使用: + +```c +rt_thread_create(); +``` + +创建。 + +函数会从动态内存堆中自动申请: + +- 线程控制块 +- 线程栈 + +基本形式: + +```c +rt_thread_t rt_thread_create( + const char *name, + void (*entry)(void *parameter), + void *parameter, + rt_uint32_t stack_size, + rt_uint8_t priority, + rt_uint32_t tick +); +``` + +例如: + +```c +rt_thread_t tid; + +tid = rt_thread_create( + "thread1", + thread1_entry, + RT_NULL, + 1024, + 20, + 10 +); +``` + +可以简单对比: + +| 类型 | API | 内存来源 | +| ---- | ---- | ---- | +| 静态线程 | `rt_thread_init()` | 用户提供 | +| 动态线程 | `rt_thread_create()` | 系统动态分配 | + +--- + +## 启动线程 + +不管通过: + +```c +rt_thread_init() +``` + +还是: + +```c +rt_thread_create() +``` + +创建线程以后,线程最初都处于: + +```text +RT_THREAD_INIT +``` + +因此还需要调用: + +```c +rt_thread_startup(); +``` + +让线程进入就绪状态。 + +例如: + +```c +rt_thread_startup(&thread2); +``` + +或者动态线程: + +```c +rt_thread_startup(tid); +``` + +完整流程: + +```text +定义入口函数 + ↓ +创建 / 初始化线程 + ↓ +INIT + ↓ +rt_thread_startup() + ↓ +READY + ↓ +调度器 + ↓ +RUNNING +``` + +需要注意: + +如果启动的新线程优先级比当前线程高,那么线程进入 READY 后可能立即抢占当前线程运行。 + +--- + +## 线程延时与 CPU 使用权 + +在抢占式 RTOS 中,线程不需要使用 CPU 时应该主动让出 CPU。 + +RT-Thread 提供了线程延时 API。 + +以 Tick 为单位: + +```c +rt_thread_delay(tick); +``` + +以毫秒为单位: + +```c +rt_thread_mdelay(ms); +``` + +例如: + +```c +while (1) +{ + rt_kprintf("thread running\n"); + + rt_thread_mdelay(1000); +} +``` + +运行过程: + +```text +线程运行 + ↓ +打印信息 + ↓ +rt_thread_mdelay(1000) + ↓ +线程进入挂起状态 + ↓ +CPU 运行其他线程 + ↓ +1000 ms 到达 + ↓ +线程重新进入 READY + ↓ +等待再次调度 +``` + +所以: + +```c +rt_thread_mdelay(); +``` + +不仅仅是简单的“延时”,它还会使当前线程暂时不参与调度,将 CPU 使用权交给其他线程。 + +--- + +## 线程学习小结 + +RT-Thread 的线程可以总结为: + +```text +线程 + │ + ├── 线程控制块 + │ └── struct rt_thread + │ + ├── 线程栈 + │ └── 保存上下文和局部变量 + │ + ├── 状态 + │ ├── INIT + │ ├── READY + │ ├── RUNNING + │ ├── SUSPEND + │ └── CLOSE + │ + ├── 优先级 + │ └── 数字越小优先级越高 + │ + ├── 时间片 + │ └── 同优先级线程轮转 + │ + └── 入口函数 + └── thread_entry() +``` + +线程最基本的使用流程: + +```text +编写线程入口函数 + ↓ +创建线程 + ↓ +设置栈、优先级、时间片 + ↓ +启动线程 + ↓ +进入 READY + ↓ +等待调度器调度 + ↓ +RUNNING + ↓ +delay / wait + ↓ +让出 CPU +``` + +--- + +## SCons 构建系统 + +RT-Thread 使用: + +```text +SCons +``` + +作为工程构建工具。 + +SCons 是基于 Python 的自动化构建工具。 + +RT-Thread 可以通过 SCons 实现: + +- 编译工程 +- 管理源码文件 +- 管理头文件路径 +- 管理宏定义 +- 管理不同模块 +- 根据配置裁剪代码 +- 生成 MDK / IAR 工程 + +因此,RT-Thread 的工程并不是单纯依赖 Keil 工程文件进行管理,而是可以通过 SCons 脚本描述整个工程结构。 + +常见相关文件有: + +```text +SConstruct + +SConscript +``` + +可以简单理解为: + +```text +SConstruct + ↓ +工程顶层构建脚本 + ↓ +调用各目录中的 SConscript + ↓ +添加源码、头文件和宏 + ↓ +SCons 根据配置完成构建 +``` + +--- + +## SConstruct + +`SConstruct` 通常是整个工程最上层的 SCons 构建文件。 + +可以将它理解为: + +**整个工程构建的入口脚本。** + +当执行: + +```bash +scons +``` + +时,SCons 会首先读取: + +```text +SConstruct +``` + +然后根据 SConstruct 中定义的内容继续执行其他构建脚本。 + +典型关系: + +```text +Project + │ + ├── SConstruct + │ + ├── applications + │ └── SConscript + │ + ├── drivers + │ └── SConscript + │ + └── rt-thread + └── ... +``` + +其中: + +```text +SConstruct +``` + +负责整个工程, + +各目录中的: + +```text +SConscript +``` + +负责当前模块。 + +--- + +## SConscript + +`SConscript` 用于描述某一个模块: + +**哪些源文件应该参与编译,以及这些文件需要哪些头文件和宏定义。** + +例如一个简单的目录: + +```text +applications + │ + ├── main.c + ├── led.c + ├── led.h + └── SConscript +``` + +其 SConscript 可以类似: + +```python +from building import * + +cwd = GetCurrentDir() + +src = Glob('*.c') + +CPPPATH = [cwd] + +group = DefineGroup( + 'Applications', + src, + depend = [''], + CPPPATH = CPPPATH +) + +Return('group') +``` + +这里几个重要内容分别表示: + +```python +cwd = GetCurrentDir() +``` + +获取当前目录。 + +```python +src = Glob('*.c') +``` + +获取当前目录中的 C 文件。 + +```python +CPPPATH = [cwd] +``` + +添加头文件搜索路径。 + +最后通过: + +```python +DefineGroup() +``` + +把这些文件组成一个编译组。 + +因此可以简单理解: + +```text +SConscript + ↓ +告诉 SCons: + +哪些 .c 要编译 + +哪些 include 目录要加入 + +需要哪些宏 + +满足什么条件才编译 +``` + +--- + +## DefineGroup + +RT-Thread 的 SCons 脚本中经常可以看到: + +```python +DefineGroup() +``` + +例如: + +```python +group = DefineGroup( + 'Applications', + src, + depend = [''], + CPPPATH = CPPPATH +) +``` + +可以把它理解为: + +**定义一个工程代码组。** + +第一个参数: + +```python +'Applications' +``` + +表示组名称。 + +第二个参数: + +```python +src +``` + +表示需要编译的源文件。 + +还可以指定: + +```python +CPPPATH +``` + +头文件搜索路径。 + +以及: + +```python +CPPDEFINES +``` + +宏定义。 + +例如: + +```python +group = DefineGroup( + 'Drivers', + src, + depend = ['BSP_USING_UART1'], + CPPPATH = CPPPATH +) +``` + +表示: + +只有: + +```text +BSP_USING_UART1 +``` + +被使能时,这一组代码才会加入工程。 + +这就是 RT-Thread 可以根据配置自动裁剪工程的重要原因之一。 + +--- + +## SCons 基本命令 + +进入 RT-Thread BSP 或工程目录后,可以直接使用: + +```bash +scons +``` + +编译工程。 + +基本流程: + +```text +工程目录 + ↓ +scons + ↓ +读取 SConstruct + ↓ +加载 SConscript + ↓ +分析工程配置 + ↓ +编译源码 + ↓ +链接 + ↓ +生成最终程序 +``` + +如果代码没有发生变化,SCons 会根据依赖关系判断哪些文件需要重新编译,而不是每次都重新编译整个工程。 + +--- + +## 清理工程 + +如果希望删除已经生成的编译文件,可以使用: + +```bash +scons -c +``` + +即: + +```text +scons -c + ↓ +Clean + ↓ +清理工程编译产生的文件 +``` + +通常在工程配置发生较大变化或者需要完全重新构建时使用。 + +--- + +## 生成 Keil MDK5 工程 + +RT-Thread 可以通过 SCons 自动生成 Keil 工程。 + +对于 MDK5: + +```bash +scons --target=mdk5 +``` + +执行后 SCons 会根据当前工程中的: + +```text +SConstruct +SConscript +rtconfig.h +``` + +等配置重新生成 MDK5 工程。 + +因此如果修改了: + +- SConscript +- 软件包 +- menuconfig 配置 +- BSP 配置 + +通常需要重新执行: + +```bash +scons --target=mdk5 +``` + +让 Keil 工程同步最新配置。 + +其他常见目标还有: + +```bash +scons --target=mdk4 +``` + +生成 MDK4 工程。 + +```bash +scons --target=iar +``` + +生成 IAR 工程。 + +可以总结为: + +| 命令 | 作用 | +| ---- | ---- | +| `scons` | 编译工程 | +| `scons -c` | 清理工程 | +| `scons --target=mdk5` | 生成 MDK5 工程 | +| `scons --target=mdk4` | 生成 MDK4 工程 | +| `scons --target=iar` | 生成 IAR 工程 | + +--- + +## menuconfig 与 SCons + +RT-Thread 可以使用: + +```bash +menuconfig +``` + +对系统功能进行配置。 + +例如: + +```text +RT-Thread Kernel +RT-Thread Components +Hardware Drivers +Network +File System +Packages +``` + +配置完成后会生成或修改相关配置内容,例如: + +```text +.config +``` + +以及: + +```text +rtconfig.h +``` + +一个典型配置流程为: + +```text +menuconfig + ↓ +选择需要的功能 + ↓ +保存配置 + ↓ +更新 rtconfig.h + ↓ +SCons 根据宏判断代码是否参与编译 + ↓ +生成新的工程 +``` + +因此: + +```text +menuconfig +``` + +负责“选择功能”, + +而: + +```text +SCons +``` + +负责“根据这些功能构建工程”。 + +--- + +## pkgs --update + +如果工程使用了 RT-Thread 软件包,修改配置后通常需要执行: + +```bash +pkgs --update +``` + +它会根据当前配置更新对应的软件包。 + +常见流程: + +```text +menuconfig + ↓ +选择软件包 + ↓ +保存 + ↓ +pkgs --update + ↓ +下载 / 更新软件包 + ↓ +scons --target=mdk5 + ↓ +重新生成 Keil 工程 +``` + +因此,在使用 RT-Thread 软件包时,可以记住下面这组命令: + +```bash +menuconfig + +pkgs --update + +scons --target=mdk5 +``` + +--- + +## SCons 与 RT-Thread 工程的关系 + +整个构建流程可以理解为: + +```text +menuconfig + ↓ +决定系统需要什么功能 + ↓ +.config / rtconfig.h + ↓ +SConstruct + ↓ +调用各模块 SConscript + ↓ +根据宏决定哪些源码参与编译 + ↓ +SCons + ↓ +编译 / 生成 IDE 工程 +``` + +例如: + +```text +menuconfig 中开启 UART + ↓ +BSP_USING_UART = y + ↓ +生成对应宏 + ↓ +SConscript 检查 depend + ↓ +UART 驱动加入工程 + ↓ +编译 UART 驱动 +``` + +所以 SCons 并不是简单地“代替 Keil 点一下 Build”。 + +它实际上承担了: + +**整个 RT-Thread 工程代码组织和自动构建的工作。** + -- Gitee From c6f7d89286f351ca6bce4a20224f5178493e067c Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Thu, 20 Aug 2026 20:19:49 +0800 Subject: [PATCH 6/8] fix 2 --- .../README.md" | 0 .../thread_study.c" | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" => "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" (100%) rename "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" => "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" (100%) diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" similarity index 100% rename from "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/README.md" rename to "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/README.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" similarity index 100% rename from "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/thread_study.c" rename to "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\214\345\244\251\344\275\234\344\270\232/thread_study.c" -- Gitee From daada18f2a04caeacbc4579240013b94f0eb7a78 Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Thu, 20 Aug 2026 20:22:18 +0800 Subject: [PATCH 7/8] day3 --- .../package_demo.c" | 372 ++++ .../parking_demo.c" | 187 ++ .../startup_demo.c" | 326 +++ .../ticket_demo.c" | 177 ++ .../ticketdiffer_demo.c" | 144 ++ ...\254\254\344\270\211\345\244\251README.md" | 1031 +++++++++ ...46\344\271\240\347\254\224\350\256\260.md" | 1842 +++++++++++++++++ 7 files changed, 4079 insertions(+) create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" new file mode 100644 index 0000000..b37f338 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/package_demo.c" @@ -0,0 +1,372 @@ +#include + +#define PACKAGE_QUEUE_SIZE 5 +#define PACKAGE_PER_SENDER 5 +#define PACKAGE_END_ID 0xFFFFFFFFU + +#define THREAD_STACK_SIZE 1024 +#define SORT_PRIORITY 19 +#define SEND_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +#define REGION_EAST 1 +#define REGION_SOUTH 2 +#define REGION_NORTH 3 + +struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; + rt_uint16_t weight; +}; + +struct sender_info +{ + rt_uint32_t base_id; + rt_uint8_t sender_id; +}; + +static rt_mq_t package_mq = RT_NULL; + +static rt_uint8_t demo_running = 0; +static rt_uint8_t sender_finished = 0; + +static rt_uint32_t send_failed = 0; + +static struct sender_info sender_info[2] = +{ + {1000, 1}, + {2000, 2} +}; + + +/* 获取区域名称 */ +static const char *get_region_name(rt_uint8_t region) +{ + switch (region) + { + case REGION_EAST: + return "east"; + + case REGION_SOUTH: + return "south"; + + case REGION_NORTH: + return "north"; + + default: + return "unknown"; + } +} + + +/* 发送结束消息 */ +static void send_end_message(void) +{ + struct package_msg msg; + rt_err_t result; + + msg.id = PACKAGE_END_ID; + msg.region = 0; + msg.weight = 0; + + while (1) + { + result = rt_mq_send(package_mq, + &msg, + sizeof(msg)); + + if (result == RT_EOK) + { + break; + } + + rt_thread_mdelay(20); + } +} + + +/* 快递发送线程 */ +static void sender_entry(void *parameter) +{ + struct sender_info *info; + struct package_msg msg; + + rt_uint8_t i; + rt_uint8_t finished; + rt_err_t result; + + info = (struct sender_info *)parameter; + + for (i = 1; i <= PACKAGE_PER_SENDER; i++) + { + /* 生成快递信息 */ + msg.id = info->base_id + i; + msg.region = ((i + info->sender_id - 2) % 3) + 1; + msg.weight = 700 + info->sender_id * 100 + i * 80; + + while (1) + { + /* 发送快递消息 */ + result = rt_mq_send(package_mq, + &msg, + sizeof(msg)); + + if (result == RT_EOK) + { + break; + } + + if (result == -RT_EFULL) + { + /* 记录队列满 */ + rt_enter_critical(); + send_failed++; + rt_exit_critical(); + + rt_thread_mdelay(50); + } + else + { + rt_kprintf("package %d send error: %d\n", + msg.id, + result); + + rt_thread_mdelay(50); + } + } + + rt_thread_mdelay(50); + } + + /* 记录发送线程完成 */ + rt_enter_critical(); + + sender_finished++; + finished = sender_finished; + + rt_exit_critical(); + + /* 最后一个发送线程发送结束消息 */ + if (finished == 2) + { + send_end_message(); + } +} + + +/* 快递分拣线程 */ +static void sorter_entry(void *parameter) +{ + struct package_msg msg; + + rt_uint32_t total_packages = 0; + rt_uint32_t east_count = 0; + rt_uint32_t south_count = 0; + rt_uint32_t north_count = 0; + rt_uint32_t total_weight = 0; + + rt_uint32_t failed; + rt_err_t result; + + while (1) + { + /* 等待快递消息 */ + result = rt_mq_recv(package_mq, + &msg, + sizeof(msg), + RT_WAITING_FOREVER); + + if (result != RT_EOK) + { + rt_kprintf("receive package failed: %d\n", + result); + continue; + } + + /* 检查结束消息 */ + if (msg.id == PACKAGE_END_ID) + { + break; + } + + rt_kprintf("package %d -> %s, weight: %d g\n", + msg.id, + get_region_name(msg.region), + msg.weight); + + /* 统计快递 */ + total_packages++; + total_weight += msg.weight; + + switch (msg.region) + { + case REGION_EAST: + east_count++; + break; + + case REGION_SOUTH: + south_count++; + break; + + case REGION_NORTH: + north_count++; + break; + + default: + break; + } + + /* 模拟分拣过程 */ + rt_thread_mdelay(100); + } + + rt_enter_critical(); + failed = send_failed; + rt_exit_critical(); + + /* 打印统计结果 */ + rt_kprintf("total packages: %d\n", + total_packages); + + rt_kprintf("east: %d, south: %d, north: %d\n", + east_count, + south_count, + north_count); + + rt_kprintf("total weight: %d g\n", + total_weight); + + rt_kprintf("send failed: %d\n", + failed); + + /* 删除消息队列 */ + if (package_mq != RT_NULL) + { + rt_mq_delete(package_mq); + package_mq = RT_NULL; + } + + rt_enter_critical(); + + sender_finished = 0; + send_failed = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("package demo finished\n"); +} + + +/* 快递分拣实验 */ +static void package_demo(void) +{ + rt_thread_t sender1_tid; + rt_thread_t sender2_tid; + rt_thread_t sorter_tid; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("package demo is already running\n"); + return; + } + + demo_running = 1; + sender_finished = 0; + send_failed = 0; + + rt_exit_critical(); + + /* 创建消息队列 */ + package_mq = rt_mq_create("pkg_mq", + sizeof(struct package_msg), + PACKAGE_QUEUE_SIZE, + RT_IPC_FLAG_FIFO); + + if (package_mq == RT_NULL) + { + rt_kprintf("create package queue failed\n"); + + demo_running = 0; + return; + } + + /* 创建发送线程 1 */ + sender1_tid = rt_thread_create("pkg_send1", + sender_entry, + &sender_info[0], + THREAD_STACK_SIZE, + SEND_PRIORITY, + THREAD_TIMESLICE); + + if (sender1_tid == RT_NULL) + { + rt_kprintf("create sender1 thread failed\n"); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建发送线程 2 */ + sender2_tid = rt_thread_create("pkg_send2", + sender_entry, + &sender_info[1], + THREAD_STACK_SIZE, + SEND_PRIORITY, + THREAD_TIMESLICE); + + if (sender2_tid == RT_NULL) + { + rt_kprintf("create sender2 thread failed\n"); + + rt_thread_delete(sender1_tid); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建分拣线程 */ + sorter_tid = rt_thread_create("pkg_sort", + sorter_entry, + RT_NULL, + THREAD_STACK_SIZE, + SORT_PRIORITY, + THREAD_TIMESLICE); + + if (sorter_tid == RT_NULL) + { + rt_kprintf("create sorter thread failed\n"); + + rt_thread_delete(sender1_tid); + rt_thread_delete(sender2_tid); + + rt_mq_delete(package_mq); + package_mq = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("package demo start\n"); + + /* 启动分拣线程 */ + rt_thread_startup(sorter_tid); + + /* 启动发送线程 */ + rt_thread_startup(sender1_tid); + rt_thread_startup(sender2_tid); +} + +MSH_CMD_EXPORT(package_demo, package message queue demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" new file mode 100644 index 0000000..35c7f36 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/parking_demo.c" @@ -0,0 +1,187 @@ +#include + +#define CAR_NUM 5 +#define PARKING_SPACES 3 +#define WAIT_TIME_MS 2000 +#define PARK_TIME_MS 3000 +#define RETRY_TIME_MS 500 + +static rt_sem_t parking_sem = RT_NULL; + +static rt_uint8_t used_spaces = 0; +static rt_uint8_t finished_cars = 0; +static rt_uint8_t demo_running = 0; + +static rt_uint8_t car_id[CAR_NUM] = {1, 2, 3, 4, 5}; + +/* 车辆线程 */ +static void car_entry(void *parameter) +{ + rt_uint8_t id; + rt_uint8_t spaces; + rt_uint8_t finished; + rt_err_t result; + + id = *(rt_uint8_t *)parameter; + + while (1) + { + /* 申请车位,最多等待 2 秒 */ + result = rt_sem_take(parking_sem, + rt_tick_from_millisecond(WAIT_TIME_MS)); + + if (result == RT_EOK) + { + /* 更新已使用车位 */ + rt_enter_critical(); + used_spaces++; + spaces = used_spaces; + rt_exit_critical(); + + rt_kprintf("car%d entered, used spaces: %d\n", + id, spaces); + + /* 模拟停车 */ + rt_thread_mdelay(PARK_TIME_MS); + + /* 更新已使用车位 */ + rt_enter_critical(); + used_spaces--; + spaces = used_spaces; + rt_exit_critical(); + + rt_kprintf("car%d left, used spaces: %d\n", + id, spaces); + + /* 释放车位 */ + rt_sem_release(parking_sem); + + break; + } + else if (result == -RT_ETIMEOUT) + { + rt_kprintf("car%d waiting for a space\n", id); + + /* 稍后重新申请 */ + rt_thread_mdelay(RETRY_TIME_MS); + } + else + { + rt_kprintf("car%d take semaphore failed: %d\n", + id, result); + + rt_thread_mdelay(RETRY_TIME_MS); + } + } + + /* 记录完成车辆数量 */ + rt_enter_critical(); + finished_cars++; + finished = finished_cars; + rt_exit_critical(); + + /* 最后一辆车负责释放资源 */ + if (finished == CAR_NUM) + { + rt_kprintf("all cars finished\n"); + + if (parking_sem != RT_NULL) + { + rt_sem_delete(parking_sem); + parking_sem = RT_NULL; + } + + rt_enter_critical(); + used_spaces = 0; + finished_cars = 0; + demo_running = 0; + rt_exit_critical(); + + rt_kprintf("parking demo finished\n"); + } +} + + +/* 停车场实验 */ +static void parking_demo(void) +{ + rt_thread_t tid[CAR_NUM]; + rt_uint8_t i; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("parking demo is already running\n"); + return; + } + + demo_running = 1; + used_spaces = 0; + finished_cars = 0; + + rt_exit_critical(); + + /* 创建计数信号量 */ + parking_sem = rt_sem_create("park_sem", + PARKING_SPACES, + RT_IPC_FLAG_FIFO); + + if (parking_sem == RT_NULL) + { + rt_kprintf("create parking semaphore failed\n"); + + demo_running = 0; + return; + } + + /* 创建车辆线程 */ + for (i = 0; i < CAR_NUM; i++) + { + tid[i] = rt_thread_create("car", + car_entry, + &car_id[i], + 1024, + 20, + 10); + + if (tid[i] == RT_NULL) + { + rt_uint8_t j; + + rt_kprintf("create car%d thread failed\n", + car_id[i]); + + /* 删除已创建但未启动的线程 */ + for (j = 0; j < i; j++) + { + if (tid[j] != RT_NULL) + { + rt_thread_delete(tid[j]); + } + } + + rt_sem_delete(parking_sem); + parking_sem = RT_NULL; + + demo_running = 0; + + return; + } + } + + rt_kprintf("parking demo start\n"); + rt_kprintf("total spaces: %d, total cars: %d\n", + PARKING_SPACES, CAR_NUM); + + /* 启动车辆线程 */ + for (i = 0; i < CAR_NUM; i++) + { + rt_thread_startup(tid[i]); + } +} + +MSH_CMD_EXPORT(parking_demo, parking semaphore demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" new file mode 100644 index 0000000..61abd8d --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/startup_demo.c" @@ -0,0 +1,326 @@ +#include + +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) + +#define EVENT_ALL_READY (EVENT_NET_READY | \ + EVENT_SENSOR_READY | \ + EVENT_STORAGE_READY) + +#define THREAD_STACK_SIZE 1024 +#define INIT_PRIORITY 20 +#define BUSINESS_PRIORITY 21 +#define THREAD_TIMESLICE 10 + +#define NET_DELAY_MS 1000 +#define SENSOR_DELAY_MS 2000 +#define STORAGE_DELAY_MS 5000 +#define FIRST_WAIT_MS 3000 + +static rt_event_t startup_event = RT_NULL; + +static rt_uint32_t ready_flags = 0; +static rt_uint8_t demo_running = 0; + + +/* 记录模块状态 */ +static void set_ready_flag(rt_uint32_t flag) +{ + rt_enter_critical(); + + ready_flags |= flag; + + rt_exit_critical(); +} + + +/* 网络初始化线程 */ +static void net_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(NET_DELAY_MS); + + set_ready_flag(EVENT_NET_READY); + + result = rt_event_send(startup_event, + EVENT_NET_READY); + + if (result == RT_EOK) + { + rt_kprintf("network ready\n"); + } + else + { + rt_kprintf("network event send failed\n"); + } +} + + +/* 传感器初始化线程 */ +static void sensor_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(SENSOR_DELAY_MS); + + set_ready_flag(EVENT_SENSOR_READY); + + result = rt_event_send(startup_event, + EVENT_SENSOR_READY); + + if (result == RT_EOK) + { + rt_kprintf("sensor ready\n"); + } + else + { + rt_kprintf("sensor event send failed\n"); + } +} + + +/* 存储初始化线程 */ +static void storage_init_entry(void *parameter) +{ + rt_err_t result; + + rt_thread_mdelay(STORAGE_DELAY_MS); + + set_ready_flag(EVENT_STORAGE_READY); + + result = rt_event_send(startup_event, + EVENT_STORAGE_READY); + + if (result == RT_EOK) + { + rt_kprintf("storage ready\n"); + } + else + { + rt_kprintf("storage event send failed\n"); + } +} + + +/* 打印未完成模块 */ +static void print_not_ready(void) +{ + rt_uint32_t flags; + + rt_enter_critical(); + + flags = ready_flags; + + rt_exit_critical(); + + rt_kprintf("wait modules timeout"); + + if ((flags & EVENT_NET_READY) == 0) + { + rt_kprintf(", network is not ready"); + } + + if ((flags & EVENT_SENSOR_READY) == 0) + { + rt_kprintf(", sensor is not ready"); + } + + if ((flags & EVENT_STORAGE_READY) == 0) + { + rt_kprintf(", storage is not ready"); + } + + rt_kprintf("\n"); +} + + +/* 主业务线程 */ +static void business_entry(void *parameter) +{ + rt_uint32_t received; + rt_err_t result; + + /* 第一次等待,最多 3 秒 */ + result = rt_event_recv(startup_event, + EVENT_ALL_READY, + RT_EVENT_FLAG_AND, + rt_tick_from_millisecond(FIRST_WAIT_MS), + &received); + + if (result == -RT_ETIMEOUT) + { + print_not_ready(); + + /* 超时后再次等待 */ + result = rt_event_recv(startup_event, + EVENT_ALL_READY, + RT_EVENT_FLAG_AND | + RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, + &received); + } + + if (result == RT_EOK) + { + rt_kprintf("all modules are ready\n"); + rt_kprintf("business task started\n"); + } + else + { + rt_kprintf("receive startup event failed: %d\n", + result); + } + + /* 删除事件集 */ + if (startup_event != RT_NULL) + { + rt_event_delete(startup_event); + startup_event = RT_NULL; + } + + rt_enter_critical(); + + ready_flags = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("startup demo finished\n"); +} + + +/* 系统启动实验 */ +static void startup_demo(void) +{ + rt_thread_t net_tid; + rt_thread_t sensor_tid; + rt_thread_t storage_tid; + rt_thread_t business_tid; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("startup demo is already running\n"); + return; + } + + demo_running = 1; + ready_flags = 0; + + rt_exit_critical(); + + /* 创建事件集 */ + startup_event = rt_event_create("startup", + RT_IPC_FLAG_FIFO); + + if (startup_event == RT_NULL) + { + rt_kprintf("create startup event failed\n"); + + demo_running = 0; + return; + } + + /* 创建网络线程 */ + net_tid = rt_thread_create("net_init", + net_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (net_tid == RT_NULL) + { + rt_kprintf("create network thread failed\n"); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建传感器线程 */ + sensor_tid = rt_thread_create("sensor", + sensor_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (sensor_tid == RT_NULL) + { + rt_kprintf("create sensor thread failed\n"); + + rt_thread_delete(net_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建存储线程 */ + storage_tid = rt_thread_create("storage", + storage_init_entry, + RT_NULL, + THREAD_STACK_SIZE, + INIT_PRIORITY, + THREAD_TIMESLICE); + + if (storage_tid == RT_NULL) + { + rt_kprintf("create storage thread failed\n"); + + rt_thread_delete(net_tid); + rt_thread_delete(sensor_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建业务线程 */ + business_tid = rt_thread_create("business", + business_entry, + RT_NULL, + THREAD_STACK_SIZE, + BUSINESS_PRIORITY, + THREAD_TIMESLICE); + + if (business_tid == RT_NULL) + { + rt_kprintf("create business thread failed\n"); + + rt_thread_delete(net_tid); + rt_thread_delete(sensor_tid); + rt_thread_delete(storage_tid); + + rt_event_delete(startup_event); + startup_event = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("startup demo start\n"); + + /* 启动初始化线程 */ + rt_thread_startup(net_tid); + rt_thread_startup(sensor_tid); + rt_thread_startup(storage_tid); + + /* 启动业务线程 */ + rt_thread_startup(business_tid); +} + +MSH_CMD_EXPORT(startup_demo, startup event demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" new file mode 100644 index 0000000..f082732 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticket_demo.c" @@ -0,0 +1,177 @@ +#include + +#define TOTAL_TICKETS 20 + +#define THREAD_STACK_SIZE 1024 +#define THREAD_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +static rt_mutex_t ticket_mutex = RT_NULL; + +static rt_int32_t ticket_count = TOTAL_TICKETS; +static rt_uint8_t finished_windows = 0; +static rt_uint8_t demo_running = 0; + +static char window_name[2] = {'A', 'B'}; + + +/* 售票线程 */ +static void ticket_entry(void *parameter) +{ + char name; + rt_int32_t ticket_no; + rt_uint8_t finished; + + name = *(char *)parameter; + + while (1) + { + /* 获取互斥量 */ + if (rt_mutex_take(ticket_mutex, RT_WAITING_FOREVER) != RT_EOK) + { + rt_kprintf("window %c take mutex failed\n", name); + break; + } + + /* 检查余票 */ + if (ticket_count <= 0) + { + rt_mutex_release(ticket_mutex); + break; + } + + /* 记录当前票号 */ + ticket_no = ticket_count; + + /* 售出一张票 */ + ticket_count--; + + rt_kprintf("window %c sold ticket %d\n", + name, + ticket_no); + + /* 释放互斥量 */ + rt_mutex_release(ticket_mutex); + + /* 模拟售票间隔 */ + rt_thread_mdelay(100); + } + + /* 记录完成窗口数量 */ + rt_enter_critical(); + + finished_windows++; + finished = finished_windows; + + rt_exit_critical(); + + /* 最后一个线程负责清理 */ + if (finished == 2) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: %d\n", + ticket_count); + + if (ticket_mutex != RT_NULL) + { + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + } + + rt_enter_critical(); + + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("ticket demo finished\n"); + } +} + + +/* 售票实验 */ +static void ticket_demo(void) +{ + rt_thread_t tid_a; + rt_thread_t tid_b; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("ticket demo is already running\n"); + return; + } + + demo_running = 1; + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + + rt_exit_critical(); + + /* 创建互斥量 */ + ticket_mutex = rt_mutex_create("ticket_mtx", + RT_IPC_FLAG_PRIO); + + if (ticket_mutex == RT_NULL) + { + rt_kprintf("create ticket mutex failed\n"); + + demo_running = 0; + return; + } + + /* 创建窗口 A 线程 */ + tid_a = rt_thread_create("win_a", + ticket_entry, + &window_name[0], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_a == RT_NULL) + { + rt_kprintf("create window A thread failed\n"); + + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + demo_running = 0; + + return; + } + + /* 创建窗口 B 线程 */ + tid_b = rt_thread_create("win_b", + ticket_entry, + &window_name[1], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_b == RT_NULL) + { + rt_kprintf("create window B thread failed\n"); + + rt_thread_delete(tid_a); + + rt_mutex_delete(ticket_mutex); + ticket_mutex = RT_NULL; + demo_running = 0; + + return; + } + + rt_kprintf("ticket demo start\n"); + rt_kprintf("total tickets: %d\n", TOTAL_TICKETS); + + /* 启动售票线程 */ + rt_thread_startup(tid_a); + rt_thread_startup(tid_b); +} + +MSH_CMD_EXPORT(ticket_demo, ticket mutex demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" new file mode 100644 index 0000000..659f255 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/ticketdiffer_demo.c" @@ -0,0 +1,144 @@ +#include + +#define TOTAL_TICKETS 20 + +#define THREAD_STACK_SIZE 1024 +#define THREAD_PRIORITY 20 +#define THREAD_TIMESLICE 10 + +static rt_int32_t ticket_count = TOTAL_TICKETS; +static rt_uint8_t finished_windows = 0; +static rt_uint8_t demo_running = 0; + +static char window_name[2] = {'A', 'B'}; + + +/* 售票线程 */ +static void ticket_entry(void *parameter) +{ + char name; + rt_int32_t ticket_no; + rt_uint8_t finished; + + name = *(char *)parameter; + + while (1) + { + /* 检查余票 */ + if (ticket_count <= 0) + { + break; + } + + /* 读取当前票号 */ + ticket_no = ticket_count; + + /* 放大线程竞争 */ + rt_thread_mdelay(50); + + /* 修改余票 */ + ticket_count--; + + rt_kprintf("window %c sold ticket %d\n", + name, + ticket_no); + + /* 模拟售票间隔 */ + rt_thread_mdelay(100); + } + + /* 记录完成窗口数量 */ + rt_enter_critical(); + + finished_windows++; + finished = finished_windows; + + rt_exit_critical(); + + /* 最后一个线程负责结束实验 */ + if (finished == 2) + { + rt_kprintf("all tickets sold\n"); + rt_kprintf("remaining tickets: %d\n", + ticket_count); + + rt_enter_critical(); + + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + demo_running = 0; + + rt_exit_critical(); + + rt_kprintf("ticket differ demo finished\n"); + } +} + + +/* 无互斥量售票实验 */ +static void ticketdiffer_demo(void) +{ + rt_thread_t tid_a; + rt_thread_t tid_b; + + /* 防止重复启动 */ + rt_enter_critical(); + + if (demo_running) + { + rt_exit_critical(); + + rt_kprintf("ticket differ demo is already running\n"); + return; + } + + demo_running = 1; + ticket_count = TOTAL_TICKETS; + finished_windows = 0; + + rt_exit_critical(); + + /* 创建窗口 A 线程 */ + tid_a = rt_thread_create("diff_a", + ticket_entry, + &window_name[0], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_a == RT_NULL) + { + rt_kprintf("create window A thread failed\n"); + + demo_running = 0; + return; + } + + /* 创建窗口 B 线程 */ + tid_b = rt_thread_create("diff_b", + ticket_entry, + &window_name[1], + THREAD_STACK_SIZE, + THREAD_PRIORITY, + THREAD_TIMESLICE); + + if (tid_b == RT_NULL) + { + rt_kprintf("create window B thread failed\n"); + + rt_thread_delete(tid_a); + + demo_running = 0; + return; + } + + rt_kprintf("ticket differ demo start\n"); + rt_kprintf("total tickets: %d\n", TOTAL_TICKETS); + rt_kprintf("mutex: disabled\n"); + + /* 启动售票线程 */ + rt_thread_startup(tid_a); + rt_thread_startup(tid_b); +} + +MSH_CMD_EXPORT(ticketdiffer_demo, ticket race condition demo); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" new file mode 100644 index 0000000..dddecbb --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251\344\275\234\344\270\232/\347\254\254\344\270\211\345\244\251README.md" @@ -0,0 +1,1031 @@ +# Day3 IPC 实验报告 + +## 实验一:停车场车位管理 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 计数信号量 `Semaphore`,主要涉及 `rt_sem_create()`、`rt_sem_take()`、`rt_sem_release()`、`rt_sem_delete()` 以及超时等待机制 | +| 为什么要这么做 | 停车场只有 3 个车位,但同时有 5 辆车申请进入,需要通过计数信号量限制同时获得资源的线程数量 | +| 意义是什么 | 掌握有限资源的管理方式,理解信号量如何在线程之间完成资源计数、阻塞等待、超时重试和资源释放 | + +### 1. 实验目的 + +模拟只有 3 个车位的停车场,同时创建 5 个车辆线程。每辆车进入停车场前必须申请一个车位,离开时释放车位。当停车场已满时,车辆最多等待 2 秒,超时后打印等待信息,并在稍后重新申请。 + +通过该实验验证计数信号量对有限资源数量的控制作用。 + +### 2. 实验文件 + +```text +parking_demo.c +``` + +MSH 命令: + +```text +parking_demo +``` + +### 3. 实验设计 + +停车场车位数量设置为: + +```c +#define PARKING_SPACES 3 +``` + +车辆线程数量设置为: + +```c +#define CAR_NUM 5 +``` + +创建初始值为 3 的计数信号量: + +```c +parking_sem = rt_sem_create("park_sem", + PARKING_SPACES, + RT_IPC_FLAG_FIFO); +``` + +车辆进入前使用: + +```c +rt_sem_take() +``` + +申请车位,并设置 2 秒超时时间。 + +车辆离开后使用: + +```c +rt_sem_release() +``` + +归还车位。 + +所有车辆线程结束后使用: + +```c +rt_sem_delete() +``` + +删除动态创建的信号量对象。 + +### 4. 实际运行结果 + +执行: + +```text +msh >parking_demo +``` + +实际输出: + +![524f1d54-c940-4f06-b499-3276fcf9362f](figures/524f1d54-c940-4f06-b499-3276fcf9362f.png) + +```text +parking demo start +total spaces: 3, total cars: 5 + +car5 entered, used spaces: 1 +car4 entered, used spaces: 2 +car3 entered, used spaces: 3 + +car2 waiting for a space +car1 waiting for a space + +car5 left, used spaces: 2 +car2 entered, used spaces: 3 + +car4 left, used spaces: 2 +car1 entered, used spaces: 3 + +car3 left, used spaces: 2 +car2 left, used spaces: 1 +car1 left, used spaces: 0 + +all cars finished +parking demo finished +``` + +### 5. 实验结果分析 + +实验启动后,car5、car4 和 car3 首先成功进入停车场: + +```text +car5 entered, used spaces: 1 +car4 entered, used spaces: 2 +car3 entered, used spaces: 3 +``` + +此时 3 个车位已经全部被占用,因此 car2 和 car1 无法获得信号量。在等待 2 秒后输出: + +```text +car2 waiting for a space +car1 waiting for a space +``` + +car5 离开后释放一个信号量,car2 随后成功获得车位: + +```text +car5 left, used spaces: 2 +car2 entered, used spaces: 3 +``` + +car4 离开后,car1 同样成功进入。 + +整个实验过程中: + +```text +最大同时占用车位数 = 3 +``` + +没有出现 4 辆或 5 辆车同时进入停车场的情况。 + +最终: + +```text +car1 left, used spaces: 0 +``` + +说明所有车辆均已离开,已使用车位数量恢复为 0。 + +### 6. 实验数据总结 + +| 项目 | 实际数据 | +| ---------------- | ---------------- | +| 总车位数量 | 3 | +| 车辆线程数量 | 5 | +| 首批进入车辆 | car5、car4、car3 | +| 首次等待车辆 | car2、car1 | +| 最大同时占用车位 | 3 | +| 最终占用车位 | 0 | +| 实验结果 | 正常完成 | + +### 7. 问题与解决方法 + +运行过程中出现过: + +```text +msh >car5 entered... +``` + +类似现象。 + +这是由于 MSH 命令执行完成后重新输出命令提示符,而车辆线程同时调用 `rt_kprintf()` 输出信息,导致两个输出在串口终端中发生交错。 + +该现象不会影响信号量以及线程的实际运行结果,因此无需修改核心逻辑。 + +### 8. 思考题 + +**为什么这里使用计数信号量,而不是互斥量?** + +停车场存在 3 个相同的车位资源,因此需要允许最多 3 个车辆线程同时获得资源。 + +计数信号量可以通过初始值表示可用资源数量。本实验将信号量初始值设置为 3,每辆车进入时获取一次信号量,离开时释放一次信号量,从而保证同时进入停车场的车辆数量不超过 3。 + +互斥量主要用于保护只能由一个线程独占访问的共享资源。如果使用互斥量,同一时刻通常只能有一个车辆线程获得资源,无法正确表示停车场具有 3 个车位的实际情况。 + +------ + +## 实验二:多窗口售票系统 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 互斥量 `Mutex`、临界区以及多线程访问共享变量时产生的竞态条件 | +| 为什么要这么做 | 两个售票窗口同时修改同一个 `ticket_count`,如果缺少互斥保护,线程可能同时读取相同票号并产生重复售票 | +| 意义是什么 | 理解共享资源必须进行完整的互斥保护,掌握竞态条件产生的原因以及 Mutex 对临界区的保护作用 | + +### 1. 实验目的 + +创建窗口 A 和窗口 B 两个售票线程,共同出售 20 张票。 + +首先进行无互斥量实验,在读取余票和修改余票之间加入延时,主动放大竞态条件。 + +随后使用互斥量保护完整售票过程,对比两次实验结果。 + +实验要求最终正确版本满足: + +```text +20 张票全部售出 +没有重复票号 +余票不能小于 0 +remaining tickets = 0 +``` + +### 2. 实验文件 + +正确版本: + +```text +ticket_demo.c +``` + +无互斥量对比版本: + +```text +ticketdiffer_demo.c +``` + +对应 MSH 命令: + +```text +ticket_demo +ticketdiffer_demo +``` + +------ + +### 3. 无互斥量对比实验 + +无互斥量版本中,两个线程均直接访问: + +```c +ticket_count +``` + +在读取当前票号和修改余票之间故意加入: + +```c +rt_thread_mdelay(50); +``` + +其主要过程为: + +```c +ticket_no = ticket_count; + +rt_thread_mdelay(50); + +ticket_count--; +``` + +该延时增加了两个线程同时读取相同 `ticket_count` 的概率。 + +### 4. 无互斥量实际运行结果 + +实际运行后出现大量重复票号。 + +关键输出为: + +![abd99143-5245-4c1d-a2c7-bc8249680eaf](figures/abd99143-5245-4c1d-a2c7-bc8249680eaf.png) + +```text +window B sold ticket 20 +window A sold ticket 20 + +window B sold ticket 18 +window A sold ticket 18 + +window B sold ticket 16 +window A sold ticket 16 + +window B sold ticket 14 +window A sold ticket 14 + +window B sold ticket 12 +window A sold ticket 12 + +window B sold ticket 10 +window A sold ticket 10 + +window B sold ticket 8 +window A sold ticket 8 + +window B sold ticket 6 +window A sold ticket 6 + +... +``` + +后续票号 4 和票号 2 同样出现两个窗口同时输出的情况。 + +最终: + +```text +all tickets sold +remaining tickets: 0 +ticket differ demo finished +``` + +### 5. 无互斥量结果分析 + +虽然最终: + +```text +remaining tickets: 0 +``` + +但售票过程并不正确。 + +例如初始: + +```text +ticket_count = 20 +``` + +两个线程可能执行: + +```text +窗口 A 读取 ticket_count = 20 +窗口 B 读取 ticket_count = 20 +``` + +因此两个线程保存的票号均为: + +```text +ticket_no = 20 +``` + +随后两个线程分别执行: + +```c +ticket_count--; +``` + +虽然 `ticket_count` 最终从 20 减少到 18,但是两个窗口打印出的票号均为: + +```text +ticket 20 +``` + +于是出现: + +```text +20、20 +18、18 +16、16 +... +``` + +说明“读取票号”和“修改余票”并不是一个不可分割的操作,从而产生竞态条件。 + +------ + +### 6. 加入互斥量实验 + +正确版本使用: + +```c +rt_mutex_take() +``` + +获取互斥量。 + +完整的以下过程均放入临界区: + +```text +检查余票 + ↓ +读取当前票号 + ↓ +修改剩余票数 + ↓ +打印售票结果 +``` + +完成后调用: + +```c +rt_mutex_release() +``` + +释放互斥量。 + +所有线程完成后调用: + +![e5f26e3a-f99e-46ea-bd29-4e8cdb92c3d3](figures/e5f26e3a-f99e-46ea-bd29-4e8cdb92c3d3.png) + +```c +rt_mutex_delete() +``` + +删除互斥量对象。 + +### 7. 加互斥量实际运行结果 + +实际输出: + +```text +window B sold ticket 20 +window A sold ticket 19 +window B sold ticket 18 +window A sold ticket 17 +window B sold ticket 16 +window A sold ticket 15 +window B sold ticket 14 +window A sold ticket 13 +window B sold ticket 12 +window A sold ticket 11 +window B sold ticket 10 +window A sold ticket 9 +window B sold ticket 8 +window A sold ticket 7 +window B sold ticket 6 +window A sold ticket 5 +window B sold ticket 4 +window A sold ticket 3 +window B sold ticket 2 +window A sold ticket 1 + +all tickets sold +remaining tickets: 0 +ticket demo finished +``` + +### 8. 对比结果 + +| 对比项 | 无 Mutex | 使用 Mutex | +| ---------------- | ---------------- | ----------------- | +| 初始票数 | 20 | 20 | +| 两个窗口并发运行 | 是 | 是 | +| 重复售票 | 大量出现 | 未出现 | +| 实际票号 | 20、20、18、18…… | 20、19、18、17……1 | +| 最终余票 | 0 | 0 | +| 是否存在竞态条件 | 是 | 否 | +| 售票结果 | 错误 | 正确 | + +实验说明,即使最终余票为 0,也不能说明并发程序一定正确,还必须检查每次共享数据操作是否保持一致性。 + +### 9. 问题与解决方法 + +无互斥量实验中出现了两个窗口售出相同票号的问题。 + +原因是两个线程可以在同一时刻读取同一个 `ticket_count`,随后分别修改共享变量。 + +解决方法是使用互斥量,将: + +```text +检查余票 +读取票号 +余票减 1 +记录结果 +``` + +作为一个完整的临界区进行保护。 + +### 10. 思考题 + +**为什么只给 `ticket_count--` 加锁仍可能不安全?** + +售票操作不仅包含 `ticket_count--`,还包含检查余票和读取当前票号。 + +如果两个线程在获取锁之前都执行: + +```c +ticket_no = ticket_count; +``` + +那么两个线程仍然可能读取到相同票号。 + +即使随后: + +```c +ticket_count--; +``` + +分别受到互斥保护,也已经无法避免重复售票。 + +因此需要保护完整的售票过程,而不能只保护单独的一条减法语句。 + +------ + +## 实验三:系统启动条件检查 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 事件集 `Event`、事件位、`RT_EVENT_FLAG_AND`、事件超时等待 | +| 为什么要这么做 | 系统启动需要同时满足网络、传感器和存储三个独立条件,单个条件完成时业务线程不能提前启动 | +| 意义是什么 | 掌握一个线程同时等待多个条件的方法,理解事件集在多条件同步场景中的优势 | + +### 1. 实验目的 + +模拟系统启动过程中的三个独立模块: + +```text +网络模块 +传感器模块 +存储模块 +``` + +分别定义三个事件: + +```c +#define EVENT_NET_READY (1U << 0) +#define EVENT_SENSOR_READY (1U << 1) +#define EVENT_STORAGE_READY (1U << 2) +``` + +主业务线程使用 AND 模式等待三个事件全部完成。 + +同时设置: + +```text +网络初始化:1 秒 +传感器初始化:2 秒 +存储初始化:5 秒 +主业务线程首次最大等待:3 秒 +``` + +通过该设计验证事件集的 AND 等待以及超时机制。 + +### 2. 实验文件 + +```text +startup_demo.c +``` + +MSH 命令: + +```text +startup_demo +``` + +### 3. 实验设计 + +三个初始化线程分别完成初始化后发送对应事件: + +```c +rt_event_send() +``` + +主业务线程第一次调用: + +```c +rt_event_recv() +``` + +并设置: + +```c +RT_EVENT_FLAG_AND +``` + +要求三个事件必须全部满足。 + +首次等待时间设置为: + +```text +3 秒 +``` + +由于存储线程需要: + +```text +5 秒 +``` + +因此第一次等待必然发生超时。 + +超时后程序检查未完成模块,并再次等待三个事件全部到达。 + +### 4. 实际运行结果 + +执行: + +```text +msh >startup_demo +``` + +实际输出: + +![da3d734b-9772-4860-8a95-941c0695e303](figures/da3d734b-9772-4860-8a95-941c0695e303.png) + +```text +startup demo start + +network ready +sensor ready + +wait modules timeout, storage is not ready + +storage ready + +all modules are ready +business task started +startup demo finished +``` + +### 5. 实验结果分析 + +实验启动约 1 秒后: + +```text +network ready +``` + +说明网络初始化线程完成。 + +约 2 秒后: + +```text +sensor ready +``` + +说明传感器初始化线程完成。 + +此时存储模块尚未初始化完成,因此即使已经有两个事件到达,业务线程仍然不能启动。 + +主业务线程等待 3 秒后输出: + +```text +wait modules timeout, storage is not ready +``` + +说明第一次 `rt_event_recv()` 等待发生超时,并正确判断出存储模块尚未完成。 + +约 5 秒时: + +```text +storage ready +``` + +此时: + +```text +NETWORK + AND +SENSOR + AND +STORAGE +``` + +三个条件全部满足,因此业务线程继续执行: + +```text +all modules are ready +business task started +``` + +实验结果证明 `RT_EVENT_FLAG_AND` 可以正确实现多个条件全部满足后再继续运行的同步要求。 + +### 6. 实验数据总结 + +| 项目 | 实际结果 | +| ---------------- | ---------------- | +| 网络初始化时间 | 约 1 s | +| 传感器初始化时间 | 约 2 s | +| 存储初始化时间 | 约 5 s | +| 首次最大等待时间 | 3 s | +| 第一次等待 | 超时 | +| 未完成模块 | Storage | +| 第二次等待 | 成功 | +| 业务线程启动条件 | 三个事件全部到达 | +| 最终结果 | 正常启动 | + +### 7. 问题与解决方法 + +本实验中的首次超时属于主动设计的测试条件,并不是程序异常。 + +存储模块延时设置为 5 秒,而业务线程第一次仅等待 3 秒,因此能够稳定触发超时。 + +超时后没有直接退出业务线程,而是打印未完成模块并再次调用事件等待,使程序能够在存储模块最终完成后继续启动。 + +### 8. 思考题 + +**如果使用三个独立信号量,与使用事件集相比有什么区别?** + +如果使用三个独立信号量,需要分别创建三个信号量,并由主业务线程依次获取: + +```text +net_sem +sensor_sem +storage_sem +``` + +程序需要分别维护三个同步对象。 + +事件集可以使用不同的 bit 表示多个状态: + +```text +bit0:network +bit1:sensor +bit2:storage +``` + +再使用: + +```c +RT_EVENT_FLAG_AND +``` + +一次等待多个条件全部满足。 + +因此,对于“多个独立条件全部完成后才能继续”的场景,事件集的状态表示和等待逻辑更加集中、直观。 + +------ + +## 实验四:快递分拣中心 + +### 实验大纲 + +| 项目 | 内容 | +| -------------- | ------------------------------------------------------------ | +| 是什么知识点 | 消息队列 `Message Queue`、结构体消息传递、生产者与消费者模型、队列满处理 | +| 为什么要这么做 | 两个收件线程需要把包含编号、区域和重量的完整快递信息发送给分拣线程,仅使用同步机制无法直接完成结构化数据传输 | +| 意义是什么 | 掌握线程之间传递复杂数据的方法,并理解消息缓存、发送失败、重试以及生产速度和消费速度不一致时的处理方式 | + +### 1. 实验目的 + +创建两个快递发送线程,每个线程产生 5 条快递信息。 + +快递使用以下结构: + +```c +struct package_msg +{ + rt_uint32_t id; + rt_uint8_t region; + rt_uint16_t weight; +}; +``` + +两个发送线程通过消息队列将快递信息发送给分拣线程。 + +分拣线程完成: + +```text +快递编号输出 +区域分类 +重量输出 +总数量统计 +各区域数量统计 +总重量统计 +``` + +消息队列容量限制为 5 条,并记录队列满时的发送失败次数。 + +### 2. 实验文件 + +```text +package_demo.c +``` + +MSH 命令: + +```text +package_demo +``` + +### 3. 实验设计 + +消息队列容量设置为: + +```c +#define PACKAGE_QUEUE_SIZE 5 +``` + +Sender 1 产生: + +```text +1001 ~ 1005 +``` + +Sender 2 产生: + +```text +2001 ~ 2005 +``` + +因此不存在快递编号重复问题。 + +两个发送线程使用: + +```c +rt_mq_send() +``` + +发送消息。 + +分拣线程使用: + +```c +rt_mq_recv() +``` + +接收完整的 `package_msg` 数据。 + +当消息队列已满时: + +```c +rt_mq_send() +``` + +发送失败,程序记录: + +```c +send_failed++; +``` + +随后等待一段时间并重新发送当前消息,避免快递数据丢失。 + +最后一个发送线程完成后发送结束消息,分拣线程收到结束消息后输出统计结果并退出。 + +### 4. 实际运行结果 + +执行: + +```text +msh >package_demo +``` + +实际输出: + +![b375f5f5-5d33-47d4-973c-6646adb2c1aa](figures/b375f5f5-5d33-47d4-973c-6646adb2c1aa.png) + +```text +package demo start + +package 2001 -> south, weight: 980 g +package 1001 -> east, weight: 880 g +package 2002 -> north, weight: 1060 g +package 1002 -> south, weight: 960 g +package 1003 -> north, weight: 1040 g +package 2003 -> east, weight: 1140 g +package 1004 -> east, weight: 1120 g +package 2004 -> south, weight: 1220 g +package 1005 -> south, weight: 1200 g +package 2005 -> north, weight: 1300 g + +total packages: 10 +east: 3, south: 4, north: 3 +total weight: 10900 g +send failed: 6 +package demo finished +``` + +### 5. 实验结果分析 + +两个发送线程总共产生: + +```text +5 + 5 = 10 +``` + +条快递消息。 + +实际分拣结果: + +```text +total packages: 10 +``` + +说明 10 条快递最终全部被分拣线程成功接收。 + +各区域统计为: + +```text +east: 3 +south: 4 +north: 3 +``` + +满足: + +```text +3 + 4 + 3 = 10 +``` + +总重量统计为: + +```text +10900 g +``` + +与 10 条实际快递重量之和一致。 + +实验中还得到: + +```text +send failed: 6 +``` + +说明运行过程中消息队列曾经达到容量上限。 + +由于消息队列只能存储 5 条消息,而两个发送线程同时不断产生消息,分拣线程还需要一定时间处理每一条快递,因此在部分时刻出现: + +```text +生产速度 > 消费速度 +``` + +导致: + +```c +rt_mq_send() +``` + +返回队列满错误。 + +程序没有直接丢弃该快递,而是在记录一次失败后重新尝试发送,因此虽然出现: + +```text +send failed: 6 +``` + +最终仍然完成: + +```text +total packages: 10 +``` + +说明队列满处理和重试逻辑正常。 + +### 6. 实验数据总结 + +| 项目 | 实际数据 | +| --------------- | -------- | +| 消息队列容量 | 5 | +| 发送线程数量 | 2 | +| Sender 1 消息数 | 5 | +| Sender 2 消息数 | 5 | +| 总快递数量 | 10 | +| East | 3 | +| South | 4 | +| North | 3 | +| 总重量 | 10900 g | +| 发送失败次数 | 6 | +| 最终丢失消息 | 0 | +| 实验结果 | 正常完成 | + +### 7. 问题与解决方法 + +实验中出现: + +```text +send failed: 6 +``` + +说明部分消息发送时消息队列已经达到容量上限。 + +如果发送失败后直接进入下一条消息,会导致部分快递丢失,最终: + +```text +total packages +``` + +可能小于 10。 + +因此在发送失败后采用: + +```text +记录失败次数 + ↓ +短暂延时 + ↓ +重新发送当前消息 +``` + +的方式处理。 + +最终虽然出现 6 次发送失败,但 10 条快递均成功进入消息队列并完成分拣。 + +同时运行过程中出现: + +```text +msh >package 2001... +``` + +属于 MSH 命令提示符与线程打印输出交错,对实验结果无影响。 + +------ + +# 实验总结 + +本次实验分别使用不同 IPC 机制解决不同类型的多线程问题。 + +| 实验 | IPC 机制 | 主要解决的问题 | 实际结果 | +| -------------- | ------------- | -------------------- | ------------------------------ | +| 停车场车位管理 | Semaphore | 有限资源数量控制 | 最大同时停车 3 辆 | +| 多窗口售票 | Mutex | 共享资源互斥访问 | 20 张票无重复售出 | +| 售票对比实验 | 无 Mutex | 验证竞态条件 | 出现大量重复票号 | +| 系统启动检查 | Event | 多条件同步 | 3 秒超时,5 秒后三模块全部就绪 | +| 快递分拣 | Message Queue | 线程间结构化数据通信 | 10 条全部完成,队满 6 次 | + +实验结果表明,不同 IPC 机制解决的问题并不相同: + +```text +Semaphore + ↓ +控制有限资源数量 + +Mutex + ↓ +保护共享资源 + +Event + ↓ +等待多个条件 + +Message Queue + ↓ +在线程之间传递完整消息 +``` + +通过实际的线程竞争、超时等待和队列满实验,可以更加直观地观察 RT-Thread IPC 机制在线程同步与线程通信中的作用。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..8f5ebf2 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\270\211\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1842 @@ +# Day3 + +## IPC —— 线程间同步与通信 + +在多线程实时系统中,一个任务通常需要多个线程相互配合完成。不同线程之间可能会访问相同的资源,也可能需要按照一定的先后顺序执行,因此需要使用 **IPC(Inter-Process Communication,线程间通信机制)** 来完成线程之间的同步与数据交换。 + +RT-Thread 中常用的 IPC 机制主要包括: + +### 线程同步 + +- 信号量 `Semaphore` +- 互斥量 `Mutex` +- 事件集 `Event` + +### 线程间通信 + +- 邮箱 `Mailbox` +- 消息队列 `Message Queue` + +其中,信号量、互斥量和事件集主要用于控制多个线程之间的执行顺序以及共享资源访问;邮箱和消息队列主要用于在线程之间传递数据。RT-Thread 的 IPC 对象支持按照 **FIFO** 或 **线程优先级** 管理等待线程。 + +常用 IPC 等待标志: + +```c +RT_IPC_FLAG_FIFO +RT_IPC_FLAG_PRIO +``` + +含义: + +```text +RT_IPC_FLAG_FIFO +按照先进先出顺序唤醒等待线程。 + +RT_IPC_FLAG_PRIO +按照线程优先级排列等待线程, +优先级较高的线程优先获得资源。 +``` + +常用等待时间: + +```c +RT_WAITING_FOREVER +RT_WAITING_NO +``` + +其中: + +```text +RT_WAITING_FOREVER = -1 +一直阻塞,直到获得资源。 + +RT_WAITING_NO = 0 +不等待,如果资源当前不可用立即返回。 +``` + +------ + +## 线程同步 + +线程同步指多个线程通过一定的机制控制执行顺序,使线程按照预期的次序运行。 + +例如: + +```text +线程1:采集传感器数据 + ↓ + 写入共享内存 + ↓ +线程2:读取共享内存 + ↓ + 显示数据 +``` + +如果线程 1 还没有完成数据写入,线程 2 就开始读取数据,可能导致读取到不完整的数据。 + +因此多个线程访问同一个共享资源时,需要对共享资源进行保护。 + +多个线程共同访问的代码区域或资源称为: + +```text +临界区(Critical Section) +``` + +同一时刻通常只允许一个线程进入临界区。 + +RT-Thread 中常见的线程同步方式包括: + +```text +信号量 +互斥量 +事件集 +``` + +除此之外,还可以通过: + +```c +rt_enter_critical(); +rt_exit_critical(); +``` + +进入和退出临界区。RT-Thread 官方文档将线程同步描述为通过互斥量、事件对象、临界区等机制建立线程之间的执行顺序关系。 + +------ + +# 信号量 Semaphore + +## 信号量基本概念 + +信号量是一种轻量级的内核对象,可以用于解决线程之间的: + +```text +同步 ++ +互斥 +``` + +问题。 + +可以将信号量理解为一个 **资源计数器**。 + +例如停车场共有 5 个停车位: + +```text +信号量初值 = 5 +``` + +一辆车进入停车场: + +```text +获取一个信号量 + +5 → 4 +``` + +一辆车离开停车场: + +```text +释放一个信号量 + +4 → 5 +``` + +如果: + +```text +信号量 = 0 +``` + +表示当前已经没有可用资源。 + +此时继续申请信号量的线程会进入等待状态,直到其他线程释放信号量。RT-Thread 中每个信号量都包含一个信号量值以及一个等待线程队列。 + +------ + +## 信号量控制块 + +RT-Thread 使用: + +```c +struct rt_semaphore +``` + +管理信号量。 + +信号量句柄类型为: + +```c +rt_sem_t +``` + +其本质是一个指向信号量控制块的指针。 + +```c +typedef struct rt_semaphore *rt_sem_t; +``` + +------ + +## 信号量的基本操作 + +信号量的生命周期可以概括为: + +```text +创建 + ↓ +获取 + ↓ +使用资源 + ↓ +释放 + ↓ +删除 +``` + +即: + +```c +rt_sem_create() + ↓ +rt_sem_take() + ↓ + 临界区 + ↓ +rt_sem_release() + ↓ +rt_sem_delete() +``` + +对于静态信号量则为: + +```c +rt_sem_init() + ↓ +rt_sem_take() + ↓ +rt_sem_release() + ↓ +rt_sem_detach() +``` + +官方手册明确将信号量操作划分为创建/初始化、获取、释放以及删除/脱离。 + +------ + +## 创建信号量 + +动态创建信号量: + +```c +rt_sem_t rt_sem_create(const char *name, + rt_uint32_t value, + rt_uint8_t flag); +``` + +参数: + +| 参数 | 说明 | +| ------- | ------------------ | +| `name` | 信号量名称 | +| `value` | 信号量初始值 | +| `flag` | 等待线程的排队方式 | + +其中 `flag` 可以为: + +```c +RT_IPC_FLAG_FIFO +``` + +或者: + +```c +RT_IPC_FLAG_PRIO +``` + +例如: + +```c +rt_sem_t sem; + +sem = rt_sem_create("sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +这里: + +```text +名称 = sem +初值 = 0 +等待方式 = FIFO +``` + +创建成功返回信号量句柄,创建失败返回: + +```c +RT_NULL +``` + +------ + +## 获取信号量 + +线程通过: + +```c +rt_sem_take() +``` + +获取信号量。 + +基本形式: + +```c +rt_sem_take(sem, timeout); +``` + +例如: + +```c +rt_sem_take(sem, RT_WAITING_FOREVER); +``` + +表示: + +```text +如果当前存在信号量: + 信号量值 -1 + 线程继续运行 + +如果当前信号量为 0: + 当前线程挂起 + 一直等待其他线程释放信号量 +``` + +也可以使用: + +```c +rt_sem_take(sem, RT_WAITING_NO); +``` + +表示: + +```text +不等待。 + +如果当前无法获得信号量, +立即返回。 +``` + +------ + +## 释放信号量 + +线程完成操作之后,可以释放信号量: + +```c +rt_sem_release(sem); +``` + +例如: + +```c +rt_sem_release(sem); +``` + +其作用可以理解为: + +```text +信号量值 +1 +``` + +如果当前已经有线程因为等待这个信号量而挂起,则释放信号量后,系统会唤醒相应的等待线程。 + +因此一种非常常见的线程同步方式为: + +```text +线程 A + | +完成某项工作 + | +rt_sem_release() + | + V +信号量 + | +rt_sem_take() + | +线程 B + | +继续执行 +``` + +------ + +## 删除信号量 + +使用: + +```c +rt_sem_create() +``` + +动态创建的信号量,在不再使用时需要调用: + +```c +rt_sem_delete(); +``` + +函数: + +```c +rt_err_t rt_sem_delete(rt_sem_t sem); +``` + +例如: + +```c +rt_sem_delete(sem); +``` + +删除信号量后,系统会释放对应的内存资源。 + +如果此时仍有线程等待该信号量,RT-Thread 会先唤醒这些线程,这些等待线程得到: + +```c +-RT_ERROR +``` + +随后再释放信号量占用的内存。 + +------ + +## 静态信号量 + +如果不希望动态申请内存,也可以使用静态信号量。 + +首先定义: + +```c +static struct rt_semaphore sem; +``` + +初始化: + +```c +rt_sem_init(&sem, + "sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +使用结束后: + +```c +rt_sem_detach(&sem); +``` + +因此: + +```text +动态对象 + +rt_sem_create() +rt_sem_delete() +``` + +对应: + +```text +静态对象 + +rt_sem_init() +rt_sem_detach() +``` + +------ + +## 信号量同步示例 + +假设: + +```text +线程1负责采集数据 +线程2负责处理数据 +``` + +线程 2 必须等线程 1 采集完成后才能运行。 + +可以建立: + +```c +static rt_sem_t sem; +``` + +创建: + +```c +sem = rt_sem_create("sem", + 0, + RT_IPC_FLAG_FIFO); +``` + +线程 1: + +```c +void thread1_entry(void *parameter) +{ + while (1) + { + /* 采集数据 */ + + rt_kprintf("thread1: data ready\n"); + + rt_sem_release(sem); + + rt_thread_mdelay(1000); + } +} +``` + +线程 2: + +```c +void thread2_entry(void *parameter) +{ + while (1) + { + rt_sem_take(sem, RT_WAITING_FOREVER); + + rt_kprintf("thread2: process data\n"); + } +} +``` + +执行过程: + +```text +thread2 + | +rt_sem_take() + | +信号量 = 0 + | +线程挂起 + | +thread1 + | +采集完成 + | +rt_sem_release() + | +唤醒 thread2 + | +thread2 继续运行 +``` + +------ + +# 互斥量 Mutex + +## 互斥量基本概念 + +互斥量主要用于: + +```text +保护共享资源 +``` + +例如两个线程同时操作串口: + +```text +Thread 1 ----\ + ---> UART +Thread 2 ----/ +``` + +如果两个线程同时输出数据,可能导致输出内容混乱。 + +因此可以添加互斥量: + +```text +Thread 1 + | +获取 Mutex + | +使用 UART + | +释放 Mutex + +Thread 2 + | +等待 Mutex + | +使用 UART +``` + +从而保证: + +```text +同一时刻只有一个线程访问 UART +``` + +------ + +## 互斥量和信号量的区别 + +互斥量可以看作一种特殊的二值信号量,但它具有: + +```text +所有权 +递归获取 +优先级继承 +``` + +等特点。 + +RT-Thread 官方文档指出,互斥量可以避免实时系统中常见的 **优先级翻转问题**。 + +------ + +## 优先级翻转 + +假设三个线程: + +```text +Thread A 高优先级 +Thread B 中优先级 +Thread C 低优先级 +``` + +Thread C 先获得共享资源: + +```text +C 获取 Mutex +``` + +此时高优先级 Thread A 也需要这个资源: + +```text +A 等待 C +``` + +但是 Thread B 又抢占了 C: + +```text +B 抢占 C +``` + +最终出现: + +```text +高优先级 A + +却必须等待 + +中优先级 B +``` + +这就是: + +```text +优先级翻转 +``` + +RT-Thread 的互斥量使用 **优先级继承**: + +```text +C 原本:低优先级 + +A 等待 C 后: + +C 临时继承 A 的高优先级 + +C 快速执行完临界区 + ↓ +释放 Mutex + ↓ +优先级恢复 +``` + +从而降低优先级翻转造成的影响。 + +------ + +## 创建互斥量 + +```c +rt_mutex_t rt_mutex_create(const char *name, + rt_uint8_t flag); +``` + +例如: + +```c +rt_mutex_t mutex; + +mutex = rt_mutex_create("mutex", + RT_IPC_FLAG_PRIO); +``` + +创建失败返回: + +```c +RT_NULL +``` + +------ + +## 获取互斥量 + +```c +rt_mutex_take(mutex, + RT_WAITING_FOREVER); +``` + +获取成功后: + +```text +当前线程成为 Mutex 的拥有者 +``` + +其他线程如果再次申请: + +```text +进入等待状态 +``` + +但是拥有 Mutex 的线程本身可以再次获取同一个 Mutex,即支持: + +```text +递归获取 +``` + +------ + +## 释放互斥量 + +```c +rt_mutex_release(mutex); +``` + +使用原则: + +```c +rt_mutex_take() + ↓ + 临界区 + ↓ +rt_mutex_release() +``` + +互斥量获取后应该尽快释放,避免长时间占用共享资源。RT-Thread 官方还特别指出,在持有互斥量期间不应修改持有线程的优先级。 + +------ + +## 删除互斥量 + +动态创建: + +```c +rt_mutex_create() +``` + +对应删除: + +```c +rt_mutex_delete(mutex); +``` + +函数: + +```c +rt_err_t rt_mutex_delete(rt_mutex_t mutex); +``` + +删除时如果还有线程等待该互斥量,等待线程会被唤醒并返回: + +```c +-RT_ERROR +``` + +------ + +## 静态互斥量 + +静态初始化: + +```c +rt_mutex_init(); +``` + +例如: + +```c +static struct rt_mutex mutex; + +rt_mutex_init(&mutex, + "mutex", + RT_IPC_FLAG_PRIO); +``` + +不再使用: + +```c +rt_mutex_detach(&mutex); +``` + +因此: + +```text +动态: +create → delete + +静态: +init → detach +``` + +------ + +## 互斥量注意事项 + +互斥量不能用于: + +```text +中断服务程序 ISR +``` + +因为互斥量具有线程所有权和阻塞等待机制。 + +因此: + +```c +/* 不要在 ISR 中这样做 */ +rt_mutex_take(...); +``` + +RT-Thread 官方明确规定 Mutex 不能在中断服务程序中使用。 + +------ + +# 事件集 Event + +## 事件集基本概念 + +信号量通常用于: + +```text +等待一个条件 +``` + +而事件集可以让线程同时等待: + +```text +多个事件 +``` + +RT-Thread 中一个事件集使用一个: + +```c +rt_uint32_t +``` + +来保存事件,因此一个事件集可以表示: + +```text +32 个事件 +``` + +每一个 bit 表示一个事件: + +```text +bit0 → Event 0 +bit1 → Event 1 +bit2 → Event 2 + +... + +bit31 → Event 31 +``` + +例如: + +```c +#define EVENT_KEY1 (1 << 0) +#define EVENT_KEY2 (1 << 1) +#define EVENT_UART (1 << 2) +``` + +------ + +## 事件触发方式 + +事件集支持: + +```c +RT_EVENT_FLAG_OR +``` + +和: + +```c +RT_EVENT_FLAG_AND +``` + +### OR + +```c +RT_EVENT_FLAG_OR +``` + +表示: + +```text +任意一个指定事件发生 +即可唤醒线程。 +``` + +例如: + +```text +等待: + +EVENT1 | EVENT2 + +只要: + +EVENT1 + +或者: + +EVENT2 + +任意一个发生即可。 +``` + +------ + +### AND + +```c +RT_EVENT_FLAG_AND +``` + +表示: + +```text +所有指定事件全部发生后 +才唤醒线程。 +``` + +例如: + +```text +EVENT1 ++ +EVENT2 +``` + +必须两个事件都已经发生才能满足条件。 + +RT-Thread 还支持: + +```c +RT_EVENT_FLAG_CLEAR +``` + +表示线程成功接收到事件以后: + +```text +自动清除对应事件标志位 +``` + +------ + +## 创建事件集 + +```c +rt_event_t rt_event_create(const char *name, + rt_uint8_t flag); +``` + +例如: + +```c +rt_event_t event; + +event = rt_event_create("event", + RT_IPC_FLAG_FIFO); +``` + +------ + +## 发送事件 + +使用: + +```c +rt_event_send(); +``` + +例如: + +```c +rt_event_send(event, EVENT_KEY1); +``` + +表示: + +```text +EVENT_KEY1 发生 +``` + +可以一次发送多个事件: + +```c +rt_event_send(event, + EVENT_KEY1 | EVENT_KEY2); +``` + +------ + +## 接收事件 + +使用: + +```c +rt_event_recv(); +``` + +典型形式: + +```c +rt_event_recv(event, + EVENT_KEY1 | EVENT_KEY2, + RT_EVENT_FLAG_OR | + RT_EVENT_FLAG_CLEAR, + RT_WAITING_FOREVER, + &received); +``` + +含义: + +```text +等待 EVENT_KEY1 或 EVENT_KEY2 + +任意一个发生即可唤醒 + +接收成功后自动清除事件 + +如果没有事件则一直等待 +``` + +------ + +## 删除事件集 + +动态创建: + +```c +rt_event_create() +``` + +对应: + +```c +rt_event_delete(event); +``` + +静态事件集则: + +```c +rt_event_init(); +``` + +最终使用: + +```c +rt_event_detach(); +``` + +官方事件集管理接口包括创建/初始化、发送、接收和删除/脱离。 + +------ + +# 线程之间的通信 + +线程同步主要解决: + +```text +什么时候执行? +谁先执行? +谁能访问资源? +``` + +线程通信主要解决: + +```text +线程之间传递什么数据? +``` + +RT-Thread 中常用: + +```text +Mailbox +Message Queue +``` + +即: + +```text +邮箱 +消息队列 +``` + +------ + +# 邮箱 Mailbox + +## 邮箱基本原理 + +邮箱是一种开销较低、效率较高的线程通信机制。 + +基本结构: + +```text +Thread 1 + | +发送邮件 + | + V ++---------+ +| Mailbox | ++---------+ + | +接收邮件 + | +Thread 2 +``` + +RT-Thread 的邮箱中: + +```text +每封邮件固定为 4 字节 +``` + +在 32 位系统中: + +```text +一个指针通常也是 4 字节 +``` + +因此邮箱除了发送: + +```text +整数 +状态 +命令 +``` + +也可以发送: + +```text +数据指针 +``` + +------ + +## 创建邮箱 + +```c +rt_mailbox_t rt_mb_create(const char *name, + rt_size_t size, + rt_uint8_t flag); +``` + +其中: + +```text +name +邮箱名称 + +size +邮箱容量,即可以保存多少封邮件 + +flag +FIFO / PRIO +``` + +例如: + +```c +rt_mailbox_t mb; + +mb = rt_mb_create("mb", + 8, + RT_IPC_FLAG_FIFO); +``` + +表示: + +```text +邮箱最多保存 8 封邮件 +``` + +每封邮件为 4 字节。 + +------ + +## 发送邮件 + +普通发送: + +```c +rt_mb_send(mb, value); +``` + +例如: + +```c +rt_mb_send(mb, 100); +``` + +如果邮箱未满: + +```text +数据写入邮箱 +``` + +邮箱非阻塞发送可以安全地用于中断服务程序。 + +------ + +## 等待发送 + +也可以: + +```c +rt_mb_send_wait(mb, + value, + timeout); +``` + +如果邮箱已经满了,可以等待邮箱出现空闲空间。 + +------ + +## 接收邮件 + +```c +rt_mb_recv(mb, + &value, + timeout); +``` + +例如: + +```c +rt_ubase_t value; + +rt_mb_recv(mb, + &value, + RT_WAITING_FOREVER); +``` + +如果邮箱为空: + +```text +当前线程挂起 +``` + +收到新邮件后: + +```text +线程被唤醒 +``` + +如果设置了超时时间,并且一直没有邮件,则返回超时错误。 + +------ + +## 删除邮箱 + +动态邮箱: + +```c +rt_mb_delete(mb); +``` + +完整生命周期: + +```text +rt_mb_create() + ↓ +rt_mb_send() + ↓ +rt_mb_recv() + ↓ +rt_mb_delete() +``` + +静态邮箱: + +```text +rt_mb_init() + ↓ +发送 / 接收 + ↓ +rt_mb_detach() +``` + +动态创建的邮箱不再使用时应通过 `rt_mb_delete()` 释放对应系统资源。 + +------ + +# 消息队列 Message Queue + +## 消息队列基本概念 + +消息队列也是一种常见的线程间通信方式,可以认为是邮箱的扩展。 + +邮箱: + +```text +固定 4 字节 +``` + +消息队列: + +```text +可以传输长度大于 4 字节的消息 +``` + +因此消息队列更适合传输: + +```text +结构体 +传感器数据 +UART 数据 +协议数据包 +复杂消息 +``` + +RT-Thread 消息队列可以接收来自: + +```text +线程 +中断服务程序 +``` + +发送的消息,并将这些消息缓存到内部存储空间。普通消息按照 FIFO 顺序传递。 + +------ + +## 创建消息队列 + +```c +rt_mq_t rt_mq_create(const char *name, + rt_size_t msg_size, + rt_size_t max_msgs, + rt_uint8_t flag); +``` + +例如: + +```c +rt_mq_t mq; + +mq = rt_mq_create("mq", + sizeof(struct msg), + 10, + RT_IPC_FLAG_FIFO); +``` + +参数: + +| 参数 | 含义 | +| ---------- | ---------------------- | +| `name` | 消息队列名称 | +| `msg_size` | 单条消息最大长度 | +| `max_msgs` | 最多能够存储多少条消息 | +| `flag` | FIFO 或 PRIO | + +------ + +## 发送消息 + +使用: + +```c +rt_mq_send(); +``` + +例如: + +```c +struct msg +{ + rt_uint32_t id; + rt_uint32_t value; +}; + +struct msg data; + +data.id = 1; +data.value = 100; + +rt_mq_send(mq, + &data, + sizeof(data)); +``` + +消息的数据本身会被复制进消息队列。 + +------ + +## 等待发送 + +如果需要在消息队列满时等待: + +```c +rt_mq_send_wait(); +``` + +可以指定等待时间。 + +------ + +## 紧急消息 + +消息队列还支持: + +```c +rt_mq_urgent(); +``` + +普通消息: + +```text +插入队列尾部 +``` + +紧急消息: + +```text +插入队列头部 +``` + +所以紧急消息会比普通消息更早被接收。 + +------ + +## 接收消息 + +```c +rt_mq_recv(); +``` + +例如: + +```c +struct msg data; + +rt_mq_recv(mq, + &data, + sizeof(data), + RT_WAITING_FOREVER); +``` + +如果当前队列为空: + +```text +接收线程挂起 +``` + +当新的消息进入: + +```text +线程被唤醒 +``` + +然后读取消息并继续执行。 + +------ + +## 删除消息队列 + +动态创建: + +```c +rt_mq_create() +``` + +对应: + +```c +rt_mq_delete(mq); +``` + +完整过程: + +```text +rt_mq_create() + ↓ +rt_mq_send() + ↓ +rt_mq_recv() + ↓ +rt_mq_delete() +``` + +如果使用静态消息队列: + +```text +rt_mq_init() + ↓ +发送 / 接收 + ↓ +rt_mq_detach() +``` + +RT-Thread 官方将消息队列的主要操作归纳为创建、发送、接收和删除,并为静态对象提供 `rt_mq_init()`。 + +------ + +# 邮箱与消息队列的区别 + +| 对比 | 邮箱 Mailbox | 消息队列 Message Queue | +| ---------------------- | ------------------ | ---------------------- | +| 单条消息大小 | 固定 4 字节 | 可自定义 | +| 数据类型 | 整数、状态、指针等 | 数组、结构体、数据块等 | +| 内存开销 | 较小 | 相对较大 | +| 效率 | 较高 | 相对较低 | +| 是否缓存消息 | 是 | 是 | +| 普通发送是否可用于 ISR | 可以 | 可以 | +| 接收是否可阻塞 | 可以 | 可以 | +| 是否支持紧急消息 | 否 | 是,`rt_mq_urgent()` | + +因此: + +```text +简单状态、命令、指针 + ↓ + Mailbox +``` + +而: + +```text +结构体、数据包、较大的消息 + ↓ + Message Queue +``` + +------ + +# IPC 使用场景总结 + +## 信号量 + +主要解决: + +```text +线程同步 +资源计数 +``` + +典型场景: + +```text +采集线程完成 + ↓ +release semaphore + ↓ +处理线程开始运行 +``` + +------ + +## 互斥量 + +主要解决: + +```text +共享资源互斥访问 +``` + +典型场景: + +```text +UART +I2C +SPI +共享数据结构 +文件系统资源 +``` + +------ + +## 事件集 + +主要解决: + +```text +一个线程等待多个条件 +``` + +例如: + +```text +网络连接成功 + AND +设备初始化完成 + AND +传感器准备完成 +``` + +满足全部条件后线程开始工作。 + +------ + +## 邮箱 + +主要解决: + +```text +小数据、高效率线程通信 +``` + +例如: + +```text +按键状态 +命令 +ADC 数值 +对象指针 +``` + +------ + +## 消息队列 + +主要解决: + +```text +较复杂的数据传输 +``` + +例如: + +```text +传感器数据结构体 +UART 数据包 +控制命令结构体 +协议消息 +``` + +------ + +# IPC 生命周期总结 + +这是使用 RT-Thread IPC 时非常重要的一组对应关系: + +| IPC | 动态创建 | 动态删除 | 静态初始化 | 静态脱离 | +| -------- | ------------------- | ------------------- | ----------------- | ------------------- | +| 信号量 | `rt_sem_create()` | `rt_sem_delete()` | `rt_sem_init()` | `rt_sem_detach()` | +| 互斥量 | `rt_mutex_create()` | `rt_mutex_delete()` | `rt_mutex_init()` | `rt_mutex_detach()` | +| 事件集 | `rt_event_create()` | `rt_event_delete()` | `rt_event_init()` | `rt_event_detach()` | +| 邮箱 | `rt_mb_create()` | `rt_mb_delete()` | `rt_mb_init()` | `rt_mb_detach()` | +| 消息队列 | `rt_mq_create()` | `rt_mq_delete()` | `rt_mq_init()` | `rt_mq_detach()` | + +可以统一记忆为: + +```text +动态对象: + +create + ↓ +使用 + ↓ +delete +``` + +静态对象: + +```text +init + ↓ +使用 + ↓ +detach +``` + +------ + +# 常用 API 总结 + +## Semaphore + +```c +rt_sem_create() +rt_sem_delete() + +rt_sem_init() +rt_sem_detach() + +rt_sem_take() +rt_sem_release() +``` + +------ + +## Mutex + +```c +rt_mutex_create() +rt_mutex_delete() + +rt_mutex_init() +rt_mutex_detach() + +rt_mutex_take() +rt_mutex_release() +``` + +------ + +## Event + +```c +rt_event_create() +rt_event_delete() + +rt_event_init() +rt_event_detach() + +rt_event_send() +rt_event_recv() +``` + +------ + +## Mailbox + +```c +rt_mb_create() +rt_mb_delete() + +rt_mb_init() +rt_mb_detach() + +rt_mb_send() +rt_mb_send_wait() +rt_mb_recv() +``` + +------ + +## Message Queue + +```c +rt_mq_create() +rt_mq_delete() + +rt_mq_init() +rt_mq_detach() + +rt_mq_send() +rt_mq_send_wait() +rt_mq_urgent() +rt_mq_recv() +``` + +------ + +# Day3 理论总结 + +RT-Thread 的 IPC 机制主要解决两个问题: + +```text +同步 ++ +通信 +``` + +其中: + +```text +Semaphore + ↓ +控制线程执行顺序、资源数量 + +Mutex + ↓ +保护共享资源 + +Event + ↓ +等待一个或多个事件条件 + +Mailbox + ↓ +传递简单的 4 字节消息 + +Message Queue + ↓ +传递较复杂的数据 +``` + +可以进一步记忆为: + +```text + RT-Thread IPC + | + +-------+-------+ + | | + 同步 通信 + | | + +---+---+ +--+--+ + | | | | | + Sem Mutex Event Mailbox MQ +``` + +信号量重点记住: + +```c +rt_sem_create() +rt_sem_take() +rt_sem_release() +rt_sem_delete() +``` + +互斥量重点记住: + +```c +rt_mutex_create() +rt_mutex_take() +rt_mutex_release() +rt_mutex_delete() +``` + +事件集重点记住: + +```c +RT_EVENT_FLAG_OR +RT_EVENT_FLAG_AND +RT_EVENT_FLAG_CLEAR +``` + +线程通信重点区分: + +```text +Mailbox + 固定 4 字节 + 开销小、效率高 + +Message Queue + 消息长度可配置 + 可以传递结构体等复杂数据 +``` + +在实际 RT-Thread 程序中,应根据不同应用需求选择合适的 IPC 机制,而不是仅使用全局变量在线程之间交换信息。 \ No newline at end of file -- Gitee From b57dd2b9c24722a1a38c416485405b485cfe0e7b Mon Sep 17 00:00:00 2001 From: kylehe233-ui Date: Sat, 22 Aug 2026 23:35:18 +0800 Subject: [PATCH 8/8] day5 --- .../SConscript" | 15 + .../aht10_port.c" | 54 + .../day5_fs.c" | 280 ++++ .../day5_fs.h" | 10 + .../day5_mqtt.c" | 447 +++++ .../day5_mqtt.h" | 10 + .../day5_sensor.c" | 395 +++++ .../main.c" | 28 + ...\254\254\344\272\224\345\244\251README.md" | 964 +++++++++++ ...46\344\271\240\347\254\224\350\256\260.md" | 1451 +++++++++++++++++ 10 files changed, 3654 insertions(+) create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" create mode 100644 "2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" new file mode 100644 index 0000000..1eaad93 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/SConscript" @@ -0,0 +1,15 @@ +from building import * +import os + +cwd = GetCurrentDir() +CPPPATH = [cwd] +src = Glob('*.c') + +group = DefineGroup('Applications', src, depend = [''], CPPPATH = CPPPATH) + +list = os.listdir(cwd) +for item in list: + if os.path.isfile(os.path.join(cwd, item, 'SConscript')): + group = group + SConscript(os.path.join(item, 'SConscript')) + +Return('group') diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" new file mode 100644 index 0000000..327c83c --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/aht10_port.c" @@ -0,0 +1,54 @@ +#include +#include +#include "sensor_asair_aht10.h" + +/* + * Board BSP: + * i2c3 -> onboard AHT sensor + * SCL -> PE0 + * SDA -> PE1 + */ +#define AHT10_I2C_BUS_NAME "i2c3" + +/** + * @brief Initialize AHT10 sensor framework device + * + * After initialization, two sensor devices will be registered: + * temp_aht + * humi_aht + */ +static int aht10_port_init(void) +{ + struct rt_sensor_config cfg; + int result; + + rt_memset(&cfg, 0, sizeof(cfg)); + + /* Use onboard i2c3 bus */ + cfg.intf.dev_name = AHT10_I2C_BUS_NAME; + + /* + * AHT10 default I2C address. + * AHT10 package provides AHT10_I2C_ADDR. + */ + cfg.intf.user_data = (void *)AHT10_I2C_ADDR; + + result = rt_hw_aht10_init("aht10", &cfg); + + if (result != RT_EOK) + { + rt_kprintf("[AHT10] init failed, bus: %s, result: %d\n", + AHT10_I2C_BUS_NAME, + result); + + return -RT_ERROR; + } + + rt_kprintf("[AHT10] init success, bus: %s\n", + AHT10_I2C_BUS_NAME); + + return RT_EOK; +} + +/* Initialize automatically during RT-Thread startup */ +INIT_ENV_EXPORT(aht10_port_init); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" new file mode 100644 index 0000000..c1c7013 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.c" @@ -0,0 +1,280 @@ +#include +#include + +#include + +#include +#include + +#include "day5_fs.h" + + +#define FONT_PARTITION_NAME "font" +#define FONT_MOUNT_PATH "/font" +#define DATA_FILE_PATH "/font/Data.txt" + + +static rt_bool_t font_mounted = RT_FALSE; + + +/* ============================== + * 创建 font MTD NOR Device + * ============================== */ + +static int day5_font_device_init(void) +{ + struct rt_device *font_dev; + + + fal_init(); + + + font_dev = + rt_device_find(FONT_PARTITION_NAME); + + + if (font_dev != RT_NULL) + { + return RT_EOK; + } + + + font_dev = + fal_mtd_nor_device_create( + FONT_PARTITION_NAME); + + + if (font_dev == RT_NULL) + { + rt_kprintf( + "[FS] create font MTD device failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] font MTD device created\n"); + + + return RT_EOK; +} + + +/* ============================== + * 挂载 LittleFS + * ============================== */ + +static int day5_font_mount(void) +{ + if (font_mounted) + { + rt_kprintf( + "[FS] /font already mounted\n"); + + return RT_EOK; + } + + + if (day5_font_device_init() + != RT_EOK) + { + return -RT_ERROR; + } + + + if (dfs_mount(FONT_PARTITION_NAME, + FONT_MOUNT_PATH, + "lfs", + 0, + RT_NULL) != RT_EOK) + { + rt_kprintf( + "[FS] mount /font failed\n"); + + return -RT_ERROR; + } + + + font_mounted = RT_TRUE; + + + rt_kprintf( + "[FS] LittleFS mounted to %s successfully\n", + FONT_MOUNT_PATH); + + + return RT_EOK; +} + + +/* ============================== + * 格式化 + * + * 注意: + * 只在第一次没有文件系统时使用 + * 平时不要执行 + * ============================== */ + +static int day5_font_format(void) +{ + if (font_mounted) + { + rt_kprintf( + "[FS] /font is mounted, refuse to format\n"); + + return -RT_ERROR; + } + + + if (day5_font_device_init() + != RT_EOK) + { + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] formatting font partition...\n"); + + + if (dfs_mkfs("lfs", + FONT_PARTITION_NAME) + != RT_EOK) + { + rt_kprintf( + "[FS] format failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] format success\n"); + + + return RT_EOK; +} + + +/* ============================== + * 写 Data.txt + * ============================== */ + +int day5_fs_append_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + int fd; + + int len; + + int written; + + char line[128]; + + rt_int32_t temp_decimal; + + rt_int32_t humi_decimal; + + + if (!font_mounted) + { + rt_kprintf( + "[FS] /font is not mounted\n"); + + return -RT_ERROR; + } + + + temp_decimal = temp % 10; + + humi_decimal = humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = + -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = + -humi_decimal; + } + + + len = + rt_snprintf( + line, + sizeof(line), + "Temp: %d.%d ; Humi: %d.%d ; Count: %u\r\n", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); + + + fd = + open(DATA_FILE_PATH, + O_WRONLY | + O_CREAT | + O_APPEND, + 0); + + + if (fd < 0) + { + rt_kprintf( + "[FS] open %s failed\n", + DATA_FILE_PATH); + + return -RT_ERROR; + } + + + written = + write(fd, + line, + len); + + + close(fd); + + + if (written != len) + { + rt_kprintf( + "[FS] write Data.txt failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[FS] append success: %s", + line); + + + return RT_EOK; +} + + +/* ============================== + * Shell 初始化命令 + * ============================== */ + +static int day5_fs_init(void) +{ + return day5_font_mount(); +} + + +MSH_CMD_EXPORT(day5_fs_init, + mount font partition with littlefs); + +MSH_CMD_EXPORT(day5_font_format, + format font partition with littlefs); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" new file mode 100644 index 0000000..0620eeb --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_fs.h" @@ -0,0 +1,10 @@ +#ifndef __DAY5_FS_H__ +#define __DAY5_FS_H__ + +#include + +int day5_fs_append_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count); + +#endif diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" new file mode 100644 index 0000000..99863b0 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.c" @@ -0,0 +1,447 @@ +#include +#include + +#include "paho_mqtt.h" +#include "day5_mqtt.h" + + +#define MQTT_URI "tcp://broker.emqx.io:1883" + +#define MQTT_PUB_TOPIC "rsoc26/day5/kylehe/device/up" +#define MQTT_SUB_TOPIC "rsoc26/day5/kylehe/device/down" + +/* PF12 */ +#define PIN_LED_R 92 + +/* 板载 LED 低电平点亮 */ +#define LED_ON PIN_LOW +#define LED_OFF PIN_HIGH + + +static MQTTClient mqtt_client; + +static rt_bool_t mqtt_started = RT_FALSE; +static rt_bool_t mqtt_online = RT_FALSE; + +static rt_bool_t led_state = RT_FALSE; + +static char mqtt_client_id[32]; + + +/* ======================================== + * LED + * ======================================== */ + +static void day5_led_init(void) +{ + rt_pin_mode(PIN_LED_R, PIN_MODE_OUTPUT); + + led_state = RT_FALSE; + + rt_pin_write(PIN_LED_R, LED_OFF); + + rt_kprintf("[LED] initialized, state: OFF\n"); +} + + +static void day5_led_toggle(void) +{ + led_state = !led_state; + + if (led_state) + { + rt_pin_write(PIN_LED_R, LED_ON); + + rt_kprintf("[LED] ON\n"); + } + else + { + rt_pin_write(PIN_LED_R, LED_OFF); + + rt_kprintf("[LED] OFF\n"); + } +} + + +/* ======================================== + * MQTT callbacks + * ======================================== */ + +static void day5_mqtt_connect_callback(MQTTClient *client) +{ + rt_kprintf("[MQTT] connect callback\n"); +} + + +static void day5_mqtt_online_callback(MQTTClient *client) +{ + mqtt_online = RT_TRUE; + + rt_kprintf("[MQTT] online\n"); + rt_kprintf("[MQTT] broker: %s\n", MQTT_URI); + rt_kprintf("[MQTT] sub topic: %s\n", MQTT_SUB_TOPIC); +} + + +static void day5_mqtt_offline_callback(MQTTClient *client) +{ + mqtt_online = RT_FALSE; + + rt_kprintf("[MQTT] offline\n"); +} + + +/* MQTT 下行消息处理 */ +static void day5_mqtt_sub_callback(MQTTClient *client, + MessageData *msg_data) +{ + char payload[32]; + int len; + + len = msg_data->message->payloadlen; + + if (len >= (int)sizeof(payload)) + { + len = sizeof(payload) - 1; + } + + rt_memcpy(payload, + msg_data->message->payload, + len); + + payload[len] = '\0'; + + + /* 去掉末尾的空格、回车、换行 */ + while (len > 0) + { + if (payload[len - 1] == '\r' || + payload[len - 1] == '\n' || + payload[len - 1] == ' ') + { + payload[len - 1] = '\0'; + len--; + } + else + { + break; + } + } + + + rt_kprintf("[MQTT] recv topic: %.*s\n", + msg_data->topicName->lenstring.len, + msg_data->topicName->lenstring.data); + + rt_kprintf("[MQTT] recv payload: %s\n", + payload); + + rt_kprintf("[MQTT] payload length: %d\n", + len); + + + if ((len == 6) && + (rt_memcmp(payload, "toggle", 6) == 0)) + { + rt_kprintf("[MQTT] toggle command matched\n"); + + day5_led_toggle(); + } + else + { + rt_kprintf("[MQTT] unknown command\n"); + } +} + + +/* ======================================== + * MQTT 启动 + * ======================================== */ + +static int day5_mqtt_start(void) +{ + MQTTPacket_connectData condata = + MQTTPacket_connectData_initializer; + + + if (mqtt_started) + { + rt_kprintf("[MQTT] already started\n"); + + return -RT_ERROR; + } + + + /* 初始化 LED */ + day5_led_init(); + + + /* 清空 MQTT Client */ + rt_memset(&mqtt_client, + 0, + sizeof(mqtt_client)); + + + /* 生成客户端 ID */ + rt_snprintf(mqtt_client_id, + sizeof(mqtt_client_id), + "rsoc26-kylehe-%u", + rt_tick_get()); + + + mqtt_client.uri = MQTT_URI; + + + rt_memcpy(&mqtt_client.condata, + &condata, + sizeof(condata)); + + + mqtt_client.condata.clientID.cstring = + mqtt_client_id; + + mqtt_client.condata.keepAliveInterval = + 30; + + mqtt_client.condata.cleansession = + 1; + + mqtt_client.condata.willFlag = + 0; + + + /* MQTT 缓冲区 */ + mqtt_client.buf_size = + 1024; + + mqtt_client.readbuf_size = + 1024; + + + mqtt_client.buf = + rt_calloc(1, + mqtt_client.buf_size); + + mqtt_client.readbuf = + rt_calloc(1, + mqtt_client.readbuf_size); + + + if ((mqtt_client.buf == RT_NULL) || + (mqtt_client.readbuf == RT_NULL)) + { + rt_kprintf("[MQTT] buffer malloc failed\n"); + + + if (mqtt_client.buf != RT_NULL) + { + rt_free(mqtt_client.buf); + + mqtt_client.buf = RT_NULL; + } + + + if (mqtt_client.readbuf != RT_NULL) + { + rt_free(mqtt_client.readbuf); + + mqtt_client.readbuf = RT_NULL; + } + + + return -RT_ENOMEM; + } + + + /* MQTT 状态回调 */ + mqtt_client.connect_callback = + day5_mqtt_connect_callback; + + mqtt_client.online_callback = + day5_mqtt_online_callback; + + mqtt_client.offline_callback = + day5_mqtt_offline_callback; + + + /* 注册下行 Topic */ + mqtt_client.messageHandlers[0].topicFilter = + rt_strdup(MQTT_SUB_TOPIC); + + mqtt_client.messageHandlers[0].callback = + day5_mqtt_sub_callback; + + mqtt_client.messageHandlers[0].qos = + QOS0; + + + mqtt_client.defaultMessageHandler = + day5_mqtt_sub_callback; + + + rt_kprintf("[MQTT] starting...\n"); + + rt_kprintf("[MQTT] client id: %s\n", + mqtt_client_id); + + + if (paho_mqtt_start(&mqtt_client) != RT_EOK) + { + rt_kprintf("[MQTT] start failed\n"); + + return -RT_ERROR; + } + + + mqtt_started = RT_TRUE; + + + return RT_EOK; +} + + +/* ======================================== + * 温湿度 MQTT 自动上传 + * ======================================== */ + +int day5_mqtt_publish_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + char payload[128]; + + rt_int32_t temp_decimal; + rt_int32_t humi_decimal; + + + if (!mqtt_started) + { + rt_kprintf( + "[MQTT] skip sensor publish: client not started\n"); + + return -RT_ERROR; + } + + + if (!mqtt_online) + { + rt_kprintf( + "[MQTT] skip sensor publish: client offline\n"); + + return -RT_ERROR; + } + + + temp_decimal = temp % 10; + humi_decimal = humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = -humi_decimal; + } + + + rt_snprintf( + payload, + sizeof(payload), + "Temp: %d.%d ; Humi: %d.%d ; Count: %u", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); + + + /* 当前工程发布使用 QoS1 */ + if (paho_mqtt_publish(&mqtt_client, + QOS1, + MQTT_PUB_TOPIC, + payload) != RT_EOK) + { + rt_kprintf("[MQTT] sensor publish failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf( + "[MQTT] sensor publish success: %s\n", + payload); + + + return RT_EOK; +} + + +/* ======================================== + * 手动 MQTT Publish 测试 + * ======================================== */ + +static int day5_mqtt_pub(int argc, + char **argv) +{ + if (!mqtt_started) + { + rt_kprintf("[MQTT] client not started\n"); + + return -RT_ERROR; + } + + + if (!mqtt_online) + { + rt_kprintf("[MQTT] client not online yet\n"); + + return -RT_ERROR; + } + + + if (argc != 2) + { + rt_kprintf( + "usage: day5_mqtt_pub \n"); + + return -RT_ERROR; + } + + + if (paho_mqtt_publish(&mqtt_client, + QOS1, + MQTT_PUB_TOPIC, + argv[1]) != RT_EOK) + { + rt_kprintf("[MQTT] publish failed\n"); + + return -RT_ERROR; + } + + + rt_kprintf("[MQTT] publish success\n"); + + rt_kprintf("[MQTT] topic: %s\n", + MQTT_PUB_TOPIC); + + rt_kprintf("[MQTT] data : %s\n", + argv[1]); + + + return RT_EOK; +} + + +/* ======================================== + * MSH Commands + * ======================================== */ + +MSH_CMD_EXPORT(day5_mqtt_start, + start day5 paho mqtt client); + +MSH_CMD_EXPORT(day5_mqtt_pub, + publish day5 mqtt test message); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" new file mode 100644 index 0000000..4c80d9b --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_mqtt.h" @@ -0,0 +1,10 @@ +#ifndef __DAY5_MQTT_H__ +#define __DAY5_MQTT_H__ + +#include + +int day5_mqtt_publish_sensor(rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count); + +#endif diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" new file mode 100644 index 0000000..617e620 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/day5_sensor.c" @@ -0,0 +1,395 @@ +#include +#include + +#include "day5_mqtt.h" +#include "day5_fs.h" + + +#define TEMP_SENSOR_NAME "temp_aht10" +#define HUMI_SENSOR_NAME "humi_aht10" + +#define SAMPLE_PERIOD_MS 5000 + +#define WARMUP_COUNT 3 + + +static rt_thread_t sensor_tid = + RT_NULL; + +static rt_uint32_t sample_count = + 0; + + +/* ============================== + * 数据合法性检查 + * + * temp: + * 0.1 ℃ + * + * humi: + * 0.1 % + * ============================== */ + +static rt_bool_t sensor_data_valid( + rt_int32_t temp, + rt_int32_t humi) +{ + /* AHT 温度有效范围 */ + if (temp < -400 || + temp > 850) + { + return RT_FALSE; + } + + + /* 湿度 0 ~ 100 % */ + if (humi < 0 || + humi > 1000) + { + return RT_FALSE; + } + + + return RT_TRUE; +} + + +/* ============================== + * 读取一组数据 + * ============================== */ + +static rt_err_t sensor_read_once( + rt_device_t temp_dev, + rt_device_t humi_dev, + rt_int32_t *temp, + rt_int32_t *humi) +{ + struct rt_sensor_data temp_data; + + struct rt_sensor_data humi_data; + + + rt_memset(&temp_data, + 0, + sizeof(temp_data)); + + rt_memset(&humi_data, + 0, + sizeof(humi_data)); + + + if (rt_device_read(temp_dev, + 0, + &temp_data, + 1) != 1) + { + rt_kprintf( + "[DAY5] read temperature failed\n"); + + return -RT_ERROR; + } + + + if (rt_device_read(humi_dev, + 0, + &humi_data, + 1) != 1) + { + rt_kprintf( + "[DAY5] read humidity failed\n"); + + return -RT_ERROR; + } + + + *temp = + temp_data.data.temp; + + *humi = + humi_data.data.humi; + + + return RT_EOK; +} + + +/* ============================== + * 打印数据 + * ============================== */ + +static void sensor_print( + rt_int32_t temp, + rt_int32_t humi, + rt_uint32_t count) +{ + rt_int32_t temp_decimal; + + rt_int32_t humi_decimal; + + + temp_decimal = + temp % 10; + + humi_decimal = + humi % 10; + + + if (temp_decimal < 0) + { + temp_decimal = + -temp_decimal; + } + + + if (humi_decimal < 0) + { + humi_decimal = + -humi_decimal; + } + + + rt_kprintf( + "[DAY5] Temp: %d.%d C ; Humi: %d.%d %% ; Count: %u\n", + temp / 10, + temp_decimal, + humi / 10, + humi_decimal, + count); +} + + +/* ============================== + * Sensor Thread + * ============================== */ + +static void day5_sensor_thread( + void *parameter) +{ + rt_device_t temp_dev; + + rt_device_t humi_dev; + + rt_int32_t temp; + + rt_int32_t humi; + + int i; + + + /* 找温度设备 */ + temp_dev = + rt_device_find( + TEMP_SENSOR_NAME); + + + if (temp_dev == RT_NULL) + { + rt_kprintf( + "[DAY5] cannot find %s\n", + TEMP_SENSOR_NAME); + + return; + } + + + /* 找湿度设备 */ + humi_dev = + rt_device_find( + HUMI_SENSOR_NAME); + + + if (humi_dev == RT_NULL) + { + rt_kprintf( + "[DAY5] cannot find %s\n", + HUMI_SENSOR_NAME); + + return; + } + + + /* 打开温度设备 */ + if (rt_device_open( + temp_dev, + RT_DEVICE_FLAG_RDONLY) + != RT_EOK) + { + rt_kprintf( + "[DAY5] open %s failed\n", + TEMP_SENSOR_NAME); + + return; + } + + + /* 打开湿度设备 */ + if (rt_device_open( + humi_dev, + RT_DEVICE_FLAG_RDONLY) + != RT_EOK) + { + rt_kprintf( + "[DAY5] open %s failed\n", + HUMI_SENSOR_NAME); + + rt_device_close( + temp_dev); + + return; + } + + + rt_kprintf( + "[DAY5] AHT sensor opened successfully\n"); + + rt_kprintf( + "[DAY5] warming up sensor...\n"); + + + /* + * 舍弃启动时前 3 次数据 + * + * 防止 -50.6 ℃ 等异常初始值 + */ + for (i = 0; + i < WARMUP_COUNT; + i++) + { + sensor_read_once( + temp_dev, + humi_dev, + &temp, + &humi); + + + rt_kprintf( + "[DAY5] warmup %d/%d\n", + i + 1, + WARMUP_COUNT); + + + rt_thread_mdelay(200); + } + + + rt_kprintf( + "[DAY5] start sampling\n"); + + + /* + * 本次上电重新从 0 开始 + */ + sample_count = 0; + + + while (1) + { + if (sensor_read_once( + temp_dev, + humi_dev, + &temp, + &humi) + == RT_EOK) + { + /* + * 只有有效采样才计数 + */ + if (sensor_data_valid( + temp, + humi)) + { + sample_count++; + + + /* 串口打印 */ + sensor_print( + temp, + humi, + sample_count); + + + /* + * 写入 Flash + * + * 文件失败不会影响采集 Count + */ + day5_fs_append_sensor( + temp, + humi, + sample_count); + + + /* + * 上传 MQTT + * + * 网络失败不会影响采集 Count + */ + day5_mqtt_publish_sensor( + temp, + humi, + sample_count); + } + else + { + rt_kprintf( + "[DAY5] invalid sensor data, skip: temp=%d, humi=%d\n", + temp, + humi); + } + } + + + rt_thread_mdelay( + SAMPLE_PERIOD_MS); + } +} + + +/* ============================== + * 启动 Sensor Thread + * ============================== */ + +static int day5_sensor_start(void) +{ + if (sensor_tid != RT_NULL) + { + rt_kprintf( + "[DAY5] sensor thread already running\n"); + + return -RT_ERROR; + } + + + sensor_tid = + rt_thread_create( + "d5_sensor", + day5_sensor_thread, + RT_NULL, + 4096, + 15, + 10); + + + if (sensor_tid == RT_NULL) + { + rt_kprintf( + "[DAY5] create sensor thread failed\n"); + + return -RT_ERROR; + } + + + rt_thread_startup( + sensor_tid); + + + rt_kprintf( + "[DAY5] sensor thread started\n"); + + + return RT_EOK; +} + + +MSH_CMD_EXPORT(day5_sensor_start, + start day5 AHT sensor sampling); diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" new file mode 100644 index 0000000..d5ea2be --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/main.c" @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2006-2021, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + * + * Change Logs: + * Date Author Notes + * 2023-5-10 ShiHao first version + */ + +#include +#include +#include + +#define DBG_TAG "main" +#define DBG_LVL DBG_LOG +#include + +/* 配置 LED 灯引脚 */ +#define PIN_LED_B GET_PIN(F, 11) // PF11 : LED_B --> LED +#define PIN_LED_R GET_PIN(F, 12) // PF12 : LED_R --> LED + +int main(void) +{ + + return 0; +} + diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" new file mode 100644 index 0000000..e10fca7 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251\344\275\234\344\270\232/\347\254\254\344\272\224\345\244\251README.md" @@ -0,0 +1,964 @@ +# Day5 软件包、网络通信与文件系统 + +## 一、学习目标 + +本次学习主要围绕 RT-Thread 软件包生态、传感器采集、网络通信以及文件系统展开,完成 AHT 温湿度传感器、RW007 WiFi 模块、MQTT 通信和 Flash 文件存储的综合应用。 + +主要学习目标如下: + +1. 掌握 RT-Thread 软件包的配置和使用方法。 +2. 掌握 AHT 温湿度传感器的软件包驱动使用方法。 +3. 掌握 Sensor Framework 下传感器设备的访问方式。 +4. 掌握 RW007 WiFi 模块的初始化与联网方法。 +5. 掌握 Paho MQTT 客户端的连接、发布和订阅方法。 +6. 掌握 FAL Flash 分区管理机制。 +7. 掌握 DFS 与 LittleFS 文件系统的基本使用方法。 +8. 实现温湿度数据的本地存储和 MQTT 云端上传。 +9. 实现 MQTT 云端命令控制开发板 LED。 + +--- + +## 二、理论知识总结 + +### 2.1 RT-Thread 软件包机制 + +RT-Thread 软件包用于扩展 RT-Thread 的功能,例如传感器驱动、网络协议、文件系统、IoT 通信协议等。 + +本次实验主要使用以下软件包: + +| 软件包 | 版本 | 作用 | +| --------- | -----------: | -------------------- | +| AHT10 | v2.1.0 | AHT 温湿度传感器驱动 | +| RW007 | v2.1.0 | WiFi 网络通信 | +| Paho MQTT | 当前工程版本 | MQTT 客户端 | +| LittleFS | v2.5.0 | Flash 文件系统 | + +RT-Thread Studio 中的软件包配置最终会写入 `.config`,并在编译阶段生成对应的 `rtconfig.h`。 + +### 2.2 RT-Thread Sensor Framework + +RT-Thread Sensor Framework 对不同类型传感器进行了统一抽象。AHT 驱动初始化成功后注册两个 Sensor Device: + +```text +temp_aht10 +humi_aht10 +``` + +程序可以通过以下接口访问传感器: + +```c +rt_device_find(); +rt_device_open(); +rt_device_read(); +``` + +本实验中的温湿度数据使用整数形式保存: + +```text +温度:0.1 ℃ +湿度:0.1 % +``` + +例如 `temp = 259` 表示 `25.9 ℃`。 + +### 2.3 RW007 WiFi 模块 + +本开发板的 RW007 通过 SPI2 与 STM32 通信,实际配置为: + +| 配置项 | 参数 | +| -------- | ---: | +| SPI Bus | spi2 | +| CS | 90 | +| BOOT0 | 29 | +| BOOT1 | 90 | +| INT/BUSY | 107 | +| RESET | 111 | + +初始化成功后的日志: + +```text +[I/WLAN.dev] wlan init success +[I/WLAN.lwip] eth device init ok name:w0 +[I/WLAN.dev] wlan init success +[I/WLAN.lwip] eth device init ok name:w1 + +rw007 sn: [rw007a8a355defc584afa9f11] +rw007 ver: [RW007_2.1.0-c7747420-52] +``` + +其中 `w0` 作为 STA 网络接口连接 WiFi。 + +### 2.4 MQTT 通信 + +本实验使用公共 Broker: + +```text +broker.emqx.io:1883 +``` + +上行 Topic: + +```text +rsoc26/day5/kylehe/device/up +``` + +下行 Topic: + +```text +rsoc26/day5/kylehe/device/down +``` + +### 2.5 MQTT QoS + +| QoS | 含义 | +| ----- | ------------ | +| QoS 0 | 最多发送一次 | +| QoS 1 | 至少发送一次 | +| QoS 2 | 恰好发送一次 | + +本工程 Paho MQTT 发布时实际出现: + +```text +[E/mqtt] Not support Qos0 config, only support Qos1. +``` + +因此开发板发布消息使用 `QOS1`。 + +### 2.6 FAL Flash 抽象层 + +本开发板外部 Flash 为 W25Q64,容量 8 MB。实际 FAL 分区: + +```text +[I/FAL] ==================== FAL partition table ==================== +[I/FAL] | name | flash_dev | offset | length | +[I/FAL] ------------------------------------------------------------- +[I/FAL] | app | onchip_flash_128k | 0x00000000 | 0x00060000 | +[I/FAL] | param | onchip_flash_128k | 0x00060000 | 0x000a0000 | +[I/FAL] | easyflash | W25Q64 | 0x00000000 | 0x00080000 | +[I/FAL] | download | W25Q64 | 0x00080000 | 0x00100000 | +[I/FAL] | wifi_image | W25Q64 | 0x00180000 | 0x00080000 | +[I/FAL] | font | W25Q64 | 0x00200000 | 0x00300000 | +[I/FAL] | filesystem | W25Q64 | 0x00500000 | 0x00300000 | +``` + +本实验使用 `font` 分区,大小为 3 MB。 + +### 2.7 DFS 与 LittleFS + +本工程同时启用了 DFS、FAL、Elm-FatFs、LittleFS 和 ROMFS。本实验将 `font` 分区创建为 MTD NOR Device,并使用 LittleFS 挂载到 `/font`,最终保存文件: + +```text +/font/Data.txt +``` + +--- + +## 三、实验一:AHT 温湿度采集并通过 MQTT 上传 + +### 3.1 实验大纲 + +1. 配置 AHT 温湿度传感器软件包。 +2. 确认 AHT 实际使用的 I2C 总线。 +3. 使用 Sensor Framework 获取温湿度数据。 +4. 对启动阶段异常数据进行过滤。 +5. 配置 RW007 WiFi 模块。 +6. 获取网络 IP 地址。 +7. 配置 Paho MQTT。 +8. 连接 EMQX 公共 Broker。 +9. 实现开发板向 MQTTX 发布测试消息。 +10. 将真实温湿度数据周期性上传至 MQTTX。 +11. 使用 Count 统计本次上电后的有效采集次数。 + +实验流程: + +```text +AHT Sensor + ↓ +I2C3 + ↓ +Sensor Framework + ↓ +温湿度读取 + ↓ +有效性判断 + ↓ +Count++ + ↓ +Paho MQTT + ↓ +RW007 + ↓ +WiFi + ↓ +broker.emqx.io + ↓ +MQTTX +``` + +### 3.2 AHT 传感器配置 + +开发板 BSP 中 AHT 对应的软件 I2C 配置为: + +```text +I2C3 +SCL = PE0 +SDA = PE1 +``` + +传感器初始化使用: + +```c +#define AHT10_I2C_BUS_NAME "i2c3" +``` + +初始化成功: + +```text +[I/sensor] rt_sensor[temp_aht10] init success +[I/sensor] rt_sensor[humi_aht10] init success +[AHT10] init success, bus: i2c3 +``` + +### 3.3 传感器启动异常数据 + +最初读取温度时出现: + +```text +num 0 temp:-50.6 C +num 1 temp:-50.6 C +num 2 temp:-50.6 C +num 3 temp:27.4 C +num 4 temp:27.4 C +``` + +湿度读取: + +```text +humi:38.4% +humi:38.4% +humi:38.4% +humi:38.2% +humi:38.2% +``` + +因此程序增加预热机制: + +```c +#define WARMUP_COUNT 3 +``` + +启动后丢弃前 3 次采样: + +```text +[DAY5] warming up sensor... +[DAY5] warmup 1/3 +[DAY5] warmup 2/3 +[DAY5] warmup 3/3 +``` + +### 3.4 数据有效性判断 + +温度有效范围:`-40 ℃ ~ 85 ℃`;湿度有效范围:`0 % ~ 100 %`。 + +```c +if (temp < -400 || temp > 850) +{ + return RT_FALSE; +} + +if (humi < 0 || humi > 1000) +{ + return RT_FALSE; +} +``` + +只有有效数据才执行 `sample_count++`,因此 Count 表示本次上电后成功获取的有效数据次数。 + +### 3.5 RW007 初始化问题 + +最初执行: + +```text +wifi scan +``` + +出现: + +```text +[E/WLAN.cmd] Scan with info error:-10! +``` + +修改 RW007 参数为: + +```text +CONFIG_RW007_SPI_BUS_NAME="spi2" +CONFIG_RW007_CS_PIN=90 +CONFIG_RW007_BOOT0_PIN=29 +CONFIG_RW007_BOOT1_PIN=90 +CONFIG_RW007_INT_BUSY_PIN=107 +CONFIG_RW007_RST_PIN=111 +``` + +重新烧录后 RW007 初始化成功。 + +### 3.6 WiFi 联网测试 + +成功连接 WiFi 后: + +```text +[I/WLAN.lwip] Got IP address : 192.168.1.7 +``` + +执行 `ifconfig`: + +```text +network interface device: w0 (Default) +MTU: 1500 +MAC: fc 58 4a fa 9f 11 +FLAGS: UP LINK_UP INTERNET_UP DHCP_ENABLE ETHARP BROADCAST IGMP +ip address: 192.168.1.7 +gw address: 192.168.1.1 +net mask : 255.255.255.0 +dns server #0: 192.168.1.1 +``` + +测试 Internet: + +```text +60 bytes from 8.8.8.8 icmp_seq=0 ttl=113 time=46 ms +60 bytes from 8.8.8.8 icmp_seq=1 ttl=113 time=46 ms +60 bytes from 8.8.8.8 icmp_seq=2 ttl=113 time=43 ms +60 bytes from 8.8.8.8 icmp_seq=3 ttl=113 time=42 ms +``` + +### 3.7 MQTT Broker 配置 + +MQTT 客户端启动: + +```text +day5_mqtt_start +``` + +实际运行: + +```text +[MQTT] starting... +[MQTT] client id: rsoc26-kylehe-90392 +[MQTT] connect callback +[D/mqtt] ipv4 address port: 1883 +[D/mqtt] HOST = 'broker.emqx.io' +[I/mqtt] MQTT server connect success. +[I/mqtt] Subscribe #0 rsoc26/day5/kylehe/device/down OK! +[MQTT] online +[MQTT] broker: tcp://broker.emqx.io:1883 +[MQTT] sub topic: rsoc26/day5/kylehe/device/down +``` + +### 3.8 MQTT 发布 QoS 问题 + +第一次发布测试: + +```text +day5_mqtt_pub hello_day5 +``` + +出现: + +```text +[E/mqtt] Not support Qos0 config, only support Qos1. +[MQTT] publish failed +``` + +将 `QOS0` 修改为 `QOS1` 后发布成功,MQTTX 成功接收到: + +```text +hello_day5 +``` + +### 3.9 温湿度自动上传 + +传感器线程每 5 秒采集一次: + +```c +#define SAMPLE_PERIOD_MS 5000 +``` + +每次有效采集后上传: + +```text +Temp: XX.X ; Humi: XX.X ; Count: X +``` + +实际 MQTTX 接收到的数据包括: + +```text +Temp: 26.6 ; Humi: 39.4 ; Count: 4 +Temp: 26.5 ; Humi: 39.8 ; Count: 7 +Temp: 26.5 ; Humi: 39.8 ; Count: 8 +``` + +### 3.10 实验结果 + +实验成功实现 AHT 温湿度数据采集、异常数据过滤、有效数据 Count 计数、RW007 WiFi 联网、MQTT Broker 连接以及 MQTT 数据自动上传。 + +--- + +## 四、实验二:挂载 font 分区并保存 Data.txt + +### 4.1 实验大纲 + +1. 分析开发板 FAL Flash 分区。 +2. 确认 `font` 分区地址和大小。 +3. 分析开发板 ROMFS 根目录结构。 +4. 添加 `/font` 文件系统挂载点。 +5. 将 `font` 分区创建为 MTD NOR Device。 +6. 使用 LittleFS 格式化 `font` 分区。 +7. 将 LittleFS 挂载至 `/font`。 +8. 验证文件系统读写功能。 +9. 创建 `/font/Data.txt`。 +10. 每次有效采集后追加温湿度及 Count 数据。 + +实验流程: + +```text +W25Q64 + ↓ +FAL + ↓ +font 分区 + ↓ +MTD NOR Device + ↓ +LittleFS + ↓ +/font + ↓ +Data.txt +``` + +### 4.2 FAL 分区分析 + +```text +[I/FAL] | font | W25Q64 | 0x00200000 | 0x00300000 | +``` + +因此: + +```text +font offset = 0x00200000 +font size = 0x00300000 +``` + +### 4.3 根文件系统分析 + +执行: + +```text +ls / +``` + +最初结果: + +```text +Directory /: +fal +``` + +执行 `mkdir /font` 后再次 `ls /`,仍然只有 `fal`。分析 `drv_filesystem.c` 后发现根目录使用 ROMFS,只读,不能在运行时增加根目录。 + +### 4.4 增加 /font 挂载点 + +修改: + +```text +libraries/Board_Drivers/drv_filesystem.c +``` + +增加: + +```c +{ROMFS_DIRENT_DIR, "font", RT_NULL, 0}, +``` + +修改后: + +```c +const struct romfs_dirent _romfs_root[] = +{ +#ifdef BSP_USING_SDCARD_FATFS + {ROMFS_DIRENT_DIR, "sdcard", RT_NULL, 0}, +#endif + +#ifdef BSP_USING_FLASH_FATFS + {ROMFS_DIRENT_DIR, "fal", RT_NULL, 0}, +#endif + + {ROMFS_DIRENT_DIR, "font", RT_NULL, 0}, +}; +``` + +重新烧录后: + +```text +Directory /: +fal +font +``` + +### 4.5 LittleFS 挂载 + +首先创建 `font` 对应的 MTD NOR Device: + +```c +fal_mtd_nor_device_create("font"); +``` + +然后挂载: + +```c +dfs_mount("font", + "/font", + "lfs", + 0, + RT_NULL); +``` + +第一次使用时进行一次 LittleFS 格式化,之后不再重复格式化。 + +### 4.6 文件系统读写验证 + +```text +echo "hello" /font/test.txt +cat /font/test.txt +``` + +实际结果: + +```text +hello +``` + +执行: + +```text +ls /font +``` + +得到: + +```text +Directory /font: +test.txt 5 +``` + +### 4.7 Data.txt 数据保存 + +每次得到有效温湿度数据后,以 `O_WRONLY | O_CREAT | O_APPEND` 方式打开: + +```text +/font/Data.txt +``` + +数据格式: + +```text +Temp: XX.X ; Humi: XX.X ; Count: X +``` + +### 4.8 实际 Data.txt 数据 + +```text +Directory /font: +Data.txt 398 +test.txt 5 +``` + +实际数据: + +```text +Temp: 30.1 ; Humi: 42.5 ; Count: 1 +Temp: 30.1 ; Humi: 42.5 ; Count: 2 +Temp: 30.1 ; Humi: 42.8 ; Count: 3 +Temp: 30.0 ; Humi: 42.9 ; Count: 4 +Temp: 30.1 ; Humi: 42.7 ; Count: 5 +Temp: 30.0 ; Humi: 42.5 ; Count: 6 +Temp: 30.0 ; Humi: 42.4 ; Count: 7 +Temp: 29.9 ; Humi: 42.4 ; Count: 8 +Temp: 29.9 ; Humi: 42.3 ; Count: 9 +Temp: 29.9 ; Humi: 42.2 ; Count: 10 +Temp: 29.9 ; Humi: 42.2 ; Count: 11 +``` + +### 4.9 msh echo 使用问题 + +错误写法: + +```text +echo hello > /font/test.txt +``` + +提示: + +```text +Usage: echo "string" [filename] +``` + +正确命令: + +```text +echo "hello" /font/test.txt +``` + +### 4.10 实验结果 + +实验成功实现 `font` 分区的 LittleFS 挂载以及 `/font/Data.txt` 持续追加保存。 + +--- + +## 五、实验三:MQTT 云端命令控制 LED + +### 5.1 实验大纲 + +1. 配置 MQTT 下行 Topic。 +2. 在 Paho MQTT 中注册订阅回调。 +3. 从 MQTT Payload 中安全提取命令字符串。 +4. 识别 `toggle` 控制命令。 +5. 配置板载 PF12 LED。 +6. 根据 MQTT 指令切换 LED 状态。 +7. 使用 MQTTX 多次发送命令。 +8. 对比串口输出与真实 LED 状态。 + +实验流程: + +```text +MQTTX + ↓ +Publish "toggle" + ↓ +broker.emqx.io + ↓ +rsoc26/day5/kylehe/device/down + ↓ +Paho MQTT Callback + ↓ +命令解析 + ↓ +toggle + ↓ +PF12 LED + ↓ +ON / OFF +``` + +### 5.2 LED 引脚配置 + +板载红色 LED 使用 PF12。按照 RT-Thread STM32 PIN 编号: + +```text +PF12 = 5 × 16 + 12 = 92 +``` + +因此: + +```c +#define PIN_LED_R 92 +#define LED_ON PIN_LOW +#define LED_OFF PIN_HIGH +``` + +### 5.3 GET_PIN 编译问题 + +最初使用: + +```c +#define PIN_LED_R GET_PIN(F, 12) +``` + +编译出现: + +```text +warning: implicit declaration of function 'GET_PIN' +error: 'F' undeclared +``` + +最终使用: + +```c +#define PIN_LED_R 92 +``` + +编译成功。 + +### 5.4 MQTT Payload 处理 + +MQTT Payload 不保证以 `\0` 结尾,因此程序读取 `payloadlen`,限制最大长度,复制后手动补 `\0`,再去除末尾回车、换行和空格。 + +最后判断: + +```c +if ((len == 6) && + (rt_memcmp(payload, "toggle", 6) == 0)) +``` + +### 5.5 MQTT 下行测试 + +MQTTX 发布: + +```text +Topic: +rsoc26/day5/kylehe/device/down + +Payload: +toggle +``` + +开发板实际输出: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON +``` + +再次发送: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF +``` + +板载 LED 实际亮灭状态与串口日志一致。 + +### 5.6 DNS 临时解析失败 + +一次重新启动后 MQTT 出现: + +```text +[E/mqtt] getaddrinfo err: 202 'broker.emqx.io' +[E/mqtt] resolve uri err +[E/mqtt] Net connect error(-1). +[MQTT] offline +``` + +原因是 MQTT 启动时 WiFi、DHCP、DNS 尚未完全准备完成。Paho MQTT 随后自动重连: + +```text +[D/mqtt] restart! +``` + +网络恢复以后 MQTT 成功重新上线。 + +### 5.7 实验结果 + +实际运行结果: + +```text +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] ON + +[MQTT] recv topic: rsoc26/day5/kylehe/device/down +[MQTT] recv payload: toggle +[MQTT] payload length: 6 +[MQTT] toggle command matched +[LED] OFF +``` + +实际开发板 LED 同步进行亮灭切换,说明 MQTT 下行控制功能正常。 + +--- + +## 六、线程栈溢出问题 + +完成 MQTT 和 LittleFS 整合后,传感器线程最初栈大小为 2048 Bytes,运行时出现: + +```text +[FS] append success: Temp: 25.9 ; Humi: 46.7 ; Count: 1 +thread:d5_senso stack overflow +``` + +原因是传感器线程中已经包含 AHT 读取、字符串格式化、LittleFS 写文件和 Paho MQTT 发布,调用链增加后 2048 Bytes 栈空间不足。 + +将线程栈调整为 4096 Bytes: + +```c +sensor_tid = rt_thread_create( + "d5_sensor", + day5_sensor_thread, + RT_NULL, + 4096, + 15, + 10); +``` + +重新运行后系统稳定,不再出现线程栈溢出。 + +--- + +## 七、问题及解决方法总结 + +| 问题 | 原因 | 解决方法 | +| -------------------------- | --------------------------- | ---------------------------------------- | +| `wifi scan` 返回 `-10` | RW007 没有正确初始化 | 修改 RW007 SPI 与控制引脚 | +| AHT 初始读取 `-50.6℃` | 传感器刚启动数据未稳定 | 舍弃前 3 次采样 | +| MQTT QoS0 发布失败 | 当前 Paho 配置只支持 QoS1 | 发布改为 `QOS1` | +| `mkdir /font` 后目录不存在 | `/` 为 ROMFS,只读 | 在 `drv_filesystem.c` 中添加 `font` 目录 | +| `/font` 无法保存文件 | 尚未挂载真实文件系统 | FAL MTD NOR + LittleFS 挂载 | +| `echo hello > file` 失败 | msh echo 不支持该重定向方式 | 使用 `echo "hello" filename` | +| `GET_PIN(F,12)` 编译失败 | 应用层没有对应 GET_PIN 宏 | 使用 PF12 的 RT-Thread 编号 `92` | +| MQTT 收到消息但 LED 未切换 | Payload 需要明确解析和匹配 | 使用 payloadlen + memcpy + memcmp | +| `getaddrinfo err:202` | MQTT 启动时 DNS 尚未就绪 | 等待 WiFi 获取 IP,利用 Paho 自动重连 | +| `d5_sensor stack overflow` | 文件和 MQTT 调用增加栈消耗 | 线程栈从 2048 增加到 4096 Bytes | + +--- + +## 八、综合实验架构 + +```text + AHT Sensor + │ + │ I2C3 + ▼ + Sensor Framework + │ + Temp / Humi + │ + 数据有效判断 + │ + Count++ + │ + ┌─────────────┴─────────────┐ + │ │ + ▼ ▼ + LittleFS Paho MQTT + │ │ + /font/Data.txt RW007 + │ + WiFi + │ + ▼ + broker.emqx.io + ↑ ↓ + │ │ + MQTTX MQTTX + 接收数据 发送toggle + │ + ▼ + PF12 LED + ON / OFF +``` + +--- + +## 九、系统运行流程 + +开发板重新上电后,首先等待 WiFi 获取 IP: + +```text +[I/WLAN.lwip] Got IP address : 192.168.1.x +``` + +随后依次执行: + +```text +day5_fs_init +day5_mqtt_start +``` + +等待: + +```text +[MQTT] online +``` + +最后执行: + +```text +day5_sensor_start +``` + +--- + +## 十、实验结果总结 + +### 10.1 温湿度 MQTT 上传 + +开发板能够周期性采集 AHT 温湿度数据,并通过 `rsoc26/day5/kylehe/device/up` 上传至 EMQX Broker。MQTTX 可以实时接收: + +```text +Temp: 26.6 ; Humi: 39.4 ; Count: 4 +Temp: 26.5 ; Humi: 39.8 ; Count: 7 +Temp: 26.5 ; Humi: 39.8 ; Count: 8 +``` + +### 10.2 Flash 数据保存 + +温湿度数据同时保存至: + +```text +/font/Data.txt +``` + +实际读取结果: + +```text +Temp: 30.1 ; Humi: 42.5 ; Count: 1 +Temp: 30.1 ; Humi: 42.5 ; Count: 2 +Temp: 30.1 ; Humi: 42.8 ; Count: 3 +... +Temp: 29.9 ; Humi: 42.2 ; Count: 11 +``` + +### 10.3 MQTT 控制 LED + +MQTTX 向 `rsoc26/day5/kylehe/device/down` 发送 `toggle` 后,开发板能够正确识别消息,并控制板载 LED: + +```text +[LED] ON +[LED] OFF +``` + +实际 LED 状态与程序日志一致。 + +--- + +## 十一、实验总结 + +本次实验将 RT-Thread 中多个常用组件进行了综合应用,包括 Sensor Framework、I2C、SPI、RW007、WLAN、SAL、TCP/IP、Paho MQTT、FAL、DFS、ROMFS、LittleFS、PIN 和 Thread。 + +系统最终形成完整的 IoT 数据链路: + +```text +传感器采集 + ↓ +本地 Flash 保存 + ↓ +WiFi 网络连接 + ↓ +MQTT 数据上传 + ↓ +云端命令下发 + ↓ +开发板执行控制 +``` + +通过本次实验进一步理解了 RT-Thread 的设备驱动框架、软件包机制、网络协议栈、文件系统以及线程资源管理方法,并完成了传感器、网络、存储和执行器之间的完整联动。 \ No newline at end of file diff --git "a/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 0000000..ff3c6b1 --- /dev/null +++ "b/2026/\347\254\2543\347\273\204/\344\275\225\345\230\211\344\271\220/\347\254\224\350\256\260/\347\254\254\344\272\224\345\244\251\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,1451 @@ +# Day5 + +## 1. RT-Thread 软件包生态 + +RT-Thread 除了提供线程调度、IPC、设备驱动等基础功能外,还提供了较完整的**软件包生态系统**。开发者可以直接使用社区维护的软件包快速集成传感器、网络协议、文件系统、图形界面等功能,从而减少重复开发。 + +常见的软件包类型包括: + +- 传感器驱动软件包 +- WiFi 驱动软件包 +- MQTT 通信软件包 +- 文件系统相关软件包 +- 网络协议软件包 +- GUI 图形界面软件包 + +软件包机制的核心思想是: + +```text +选择软件包 + ↓ +配置软件包 + ↓ +下载软件包 + ↓ +参与工程编译 + ↓ +调用软件包 API +``` + +相比直接操作底层寄存器,使用软件包可以提高代码复用率,也能够降低应用开发难度。 + +### 1.1 软件包索引 + +RT-Thread 并不会将所有软件包的源码全部存放在内核仓库中,而是通过独立的软件包索引记录各个软件包的信息。 + +软件包索引通常包含: + +```text +软件包名称 +软件包仓库地址 +软件包版本 +软件包依赖关系 +Kconfig 配置 +软件包描述 +``` + +因此,软件包索引可以理解为一个统一的软件包目录。 + +整体关系如下: + +```text +RT-Thread + │ + ↓ +软件包索引 + │ + ├── Sensor 软件包 + ├── WiFi 软件包 + ├── MQTT 软件包 + ├── 文件系统软件包 + └── 其他软件包 +``` + +开发者在 `menuconfig` 中选择某个软件包后,env 工具会根据软件包索引找到对应的软件仓库并下载源码。 + +### 1.2 env 工具 + +`env` 是 RT-Thread 提供的开发辅助环境,其中包含: + +- menuconfig 配置工具 +- Kconfig 配置环境 +- SCons 构建环境 +- 软件包管理工具 + +RT-Thread Studio 内部同样集成了 env 环境。 + +典型的软件包配置流程为: + +```text +menuconfig + ↓ +RT-Thread online packages + ↓ +选择软件包 + ↓ +保存配置 + ↓ +更新软件包 + ↓ +下载源码 +``` + +常见的软件包更新命令为: + +```bash +pkgs --update +``` + +该命令会根据当前工程配置下载或更新对应的软件包。 + +### 1.3 Kconfig 配置机制 + +RT-Thread 使用 Kconfig 描述不同功能的配置选项及依赖关系。 + +例如一个传感器软件包可能依赖: + +```text +I2C +Sensor Framework +libc +``` + +Kconfig 中可以定义: + +- 软件包是否启用 +- 软件包版本 +- 功能选项 +- 默认参数 +- 软件包依赖关系 + +因此,`menuconfig` 实际上是 Kconfig 配置系统的菜单界面。 + +整体关系如下: + +```text +Kconfig + ↓ +menuconfig + ↓ +配置文件 + ↓ +rtconfig.h + ↓ +SCons + ↓ +决定参与编译的源码 +``` + +### 1.4 软件包版本兼容 + +不同 RT-Thread 内核版本之间可能存在: + +- API 变化 +- Kconfig 变化 +- 组件目录变化 +- 软件包依赖关系变化 + +因此,软件包需要考虑不同内核版本之间的兼容问题。 + +RT-Thread Studio 会针对不同版本提供不同的 env 配置。 + +课程中涉及的版本关系可以理解为: + +```text +RT-Thread 5.1.0 及以上 + ↓ + 新版 env + +较旧 RT-Thread 版本 + ↓ + 1.5.x env +``` + +这样可以减少由于内核版本和软件包索引版本不匹配引起的配置或编译错误。 + +### 1.5 软件包维护方式 + +RT-Thread 软件包生态由社区共同维护。 + +开发一个新的软件包后,通常需要: + +```text +开发软件包 + ↓ +上传代码仓库 + ↓ +添加软件包索引 + ↓ +提交 Pull Request + ↓ +审核 + ↓ +合并 +``` + +软件包索引主要记录软件包地址等信息,而软件包源码可以独立存储在对应仓库中。 + +这种模式能够使: + +- RT-Thread 内核独立维护 +- 软件包独立升级 +- 社区开发者共同参与维护 + +------ + +## 2. HT10 温湿度传感器 + +HT10 是一种数字温湿度传感器,可以通过数字总线读取环境温度和相对湿度。 + +在 RT-Thread 中,HT10 的使用并不一定需要应用程序直接操作寄存器,而可以通过软件包以及 Sensor Framework 提供的接口完成初始化和数据读取。 + +整体结构可以表示为: + +```text +应用程序 + ↓ +HT10 软件包 API + ↓ +Sensor Framework + ↓ +I2C 设备框架 + ↓ +I2C 控制器驱动 + ↓ +HT10 传感器 +``` + +### 2.1 I2C 通信 + +HT10 使用 I2C 总线进行通信。 + +I2C 是一种常见的同步串行通信协议,通常由两根信号线组成: + +| 信号 | 作用 | +| ---- | ---------- | +| SDA | 串行数据线 | +| SCL | 串行时钟线 | + +I2C 总线通常采用主从结构: + +```text +MCU + │ + ├── SCL + └── SDA + │ + ├── Device 1 + ├── Device 2 + └── Device 3 +``` + +不同从设备通过不同的 I2C 地址进行区分。 + +在 RT-Thread 中,I2C 控制器会注册为一个设备,例如: + +```text +i2c1 +i2c2 +``` + +上层驱动可以根据总线名称查找对应的 I2C 设备。 + +### 2.2 RT-Thread Sensor Framework + +Sensor Framework 是 RT-Thread 提供的统一传感器设备框架。 + +它的作用是屏蔽不同传感器之间的差异,为应用程序提供统一的访问方式。 + +例如不同类型的传感器可能包括: + +```text +温度传感器 +湿度传感器 +加速度传感器 +陀螺仪 +光照传感器 +气压传感器 +``` + +虽然底层芯片型号不同,但是通过 Sensor Framework 可以统一管理。 + +其基本层次为: + +```text +应用层 + ↓ +Sensor Framework + ↓ +具体 Sensor Driver + ↓ +I2C / SPI + ↓ +Sensor +``` + +这样可以减少应用层对具体硬件型号的依赖。 + +### 2.3 软件包依赖关系 + +HT10 软件包正常工作通常需要相关组件支持。 + +主要依赖包括: + +```text +HT10 软件包 + │ + ├── I2C Device Framework + │ + ├── Sensor Framework + │ + └── libc / printf +``` + +如果 Sensor Framework 没有开启,而软件包内部又引用了相关接口,编译时就可能出现: + +```text +undefined reference +未定义符号 +结构体或函数不存在 +``` + +因此在使用软件包前,需要先确认其依赖是否已经使能。 + +### 2.4 HT10 初始化流程 + +HT10 软件包通常会对底层操作进行封装。 + +初始化过程可以抽象为: + +```text +指定 I2C 总线 + ↓ +查找 I2C 设备 + ↓ +初始化 HT10 + ↓ +返回设备句柄 + ↓ +判断是否初始化成功 +``` + +应用程序通常只需要传入对应的 I2C 总线名称,而无需自己实现完整的寄存器初始化逻辑。 + +### 2.5 温湿度读取 + +HT10 可以分别获取: + +- 温度 +- 相对湿度 + +基本读取过程为: + +```text +HT10 + ↓ +读取原始数据 + ↓ +软件包内部转换 + ↓ +温度值 +湿度值 + ↓ +应用程序 +``` + +软件包通常已经完成: + +- I2C 命令发送 +- 数据接收 +- 原始值计算 +- 单位转换 + +应用层只需要调用相应 API。 + +### 2.6 浮点数打印 + +温度和湿度数据经常以浮点数表示,例如: + +```text +Temperature: 26.35 ℃ +Humidity: 55.42 % +``` + +但在嵌入式系统中,为了减小固件体积,默认的 `printf` 实现可能没有启用浮点格式化功能。 + +此时: + +```c +printf("%.2f", value); +``` + +可能无法正常打印。 + +原因不是传感器读取失败,而是当前 C 库的格式化输出功能没有包含浮点支持。 + +因此需要在配置中开启相应的浮点格式化支持。 + +------ + +## 3. AW007 WiFi 模块 + +AW007 是用于实现无线网络连接的 WiFi 模块。 + +在 RT-Thread 中,AW007 可以通过对应的软件包接入网络协议栈,使嵌入式设备获得 WiFi 网络通信能力。 + +整体结构为: + +```text +应用程序 + ↓ +Socket / MQTT + ↓ +TCP/IP 网络协议栈 + ↓ +AW007 软件包 + ↓ +SPI + ↓ +AW007 WiFi 模块 + ↓ +无线网络 +``` + +### 3.1 SPI 通信 + +AW007 与 MCU 之间通常通过 SPI 总线通信。 + +SPI 是一种同步串行通信协议,常见信号包括: + +| 信号 | 作用 | +| ---- | ------------------ | +| SCK | 串行时钟 | +| MOSI | 主机发送,从机接收 | +| MISO | 从机发送,主机接收 | +| CS | 从设备片选 | + +SPI 通常采用主从结构: + +```text + ┌── SPI Device 1 +MCU ── SPI ──┼── SPI Device 2 + └── SPI Device 3 +``` + +其中 SCK、MOSI 和 MISO 可以共享,而不同设备通常使用独立的 CS 信号。 + +### 3.2 AW007 控制引脚 + +除了 SPI 总线信号外,WiFi 模块通常还需要一些额外控制信号,例如: + +```text +CS +BOOT +INT +RESET +``` + +这些引脚分别负责: + +- 选择 SPI 从设备 +- 控制模块启动模式 +- 向 MCU 产生中断 +- 复位 WiFi 模块 + +软件包中的默认引脚配置不一定与实际开发板完全一致。 + +因此实际使用时,需要根据开发板原理图确认真实引脚连接。 + +### 3.3 原理图与软件配置 + +硬件驱动中的宏定义本质上必须与真实硬件连接一致。 + +关系为: + +```text +开发板原理图 + ↓ +确定实际 GPIO + ↓ +修改 BSP / 软件包配置 + ↓ +驱动操作 GPIO + ↓ +控制真实硬件 +``` + +如果软件配置中的 CS、BOOT、INT 或 RESET 引脚与原理图不一致,即使程序能够成功编译,也可能无法正常初始化 WiFi 模块。 + +因此: + +> 原理图是确认硬件引脚映射的重要依据。 + +### 3.4 WiFi 扫描 + +WiFi 模块初始化成功后,可以扫描附近的无线网络。 + +扫描过程本质上是: + +```text +启动扫描 + ↓ +搜索附近 AP + ↓ +获取 SSID + ↓ +获取信号信息 + ↓ +输出扫描结果 +``` + +其中: + +**SSID** 是无线网络名称。 + +### 3.5 WiFi 连接 + +连接无线网络通常需要: + +```text +SSID +Password +``` + +基本过程为: + +```text +选择热点 + ↓ +发送连接请求 + ↓ +完成认证 + ↓ +获取网络参数 + ↓ +加入局域网 +``` + +网络连接成功并不意味着整个互联网通信链路一定正常,因此还需要进一步进行网络测试。 + +### 3.6 Ping 测试 + +`ping` 是网络开发中常用的连通性测试工具。 + +基本原理是通过 ICMP 协议发送请求并等待响应。 + +过程为: + +```text +设备 + ↓ +ICMP Echo Request + ↓ +目标主机 + ↓ +ICMP Echo Reply + ↓ +设备 +``` + +如果能够成功收到响应,说明至少以下部分基本正常: + +```text +WiFi 驱动 +网络协议栈 +IP 配置 +路由 +网络收发 +``` + +------ + +## 4. MQTT 通信 + +MQTT 是一种轻量级的消息传输协议,在物联网设备中应用非常广泛。 + +MQTT 的特点包括: + +- 协议开销较小 +- 支持发布/订阅模式 +- 适合低带宽网络 +- 设备之间耦合度低 +- 易于扩展多个客户端 + +MQTT 通常工作在 TCP 之上。 + +协议层次可以表示为: + +```text +应用程序 + ↓ +MQTT + ↓ +TCP + ↓ +IP + ↓ +WiFi / Ethernet +``` + +### 4.1 MQTT 发布/订阅模型 + +MQTT 与传统的客户端直接通信不同。 + +客户端之间通常并不直接发送数据,而是通过 MQTT Broker 转发。 + +基本结构为: + +```text +Publisher + │ + │ Publish + ↓ +MQTT Broker + │ + │ Push + ↓ +Subscriber +``` + +其中: + +- Publisher:发布者 +- Subscriber:订阅者 +- Broker:消息服务器 + +### 4.2 MQTT Broker + +Broker 是 MQTT 系统的核心服务器。 + +主要负责: + +```text +接收消息 +管理客户端 +管理 Topic +保存订阅关系 +转发消息 +``` + +例如: + +```text +设备 A + ↓ publish + +MQTT Broker + + ↓ message +设备 B +设备 C +设备 D +``` + +发送设备不需要知道具体有哪些设备接收消息。 + +### 4.3 Topic + +MQTT 使用 Topic 对消息进行分类。 + +例如: + +```text +sensor/temperature +sensor/humidity +home/light +device/status +``` + +Publisher 向某个 Topic 发布数据: + +```text +publish + ↓ +sensor/temperature + ↓ +26.5 +``` + +Subscriber 订阅该 Topic: + +```text +subscribe sensor/temperature +``` + +之后 Broker 收到这个主题的新消息时,就会转发给所有订阅该主题的客户端。 + +### 4.4 Publisher + +Publisher 是消息发布方。 + +例如温度传感器节点可以发布: + +```text +Topic: +sensor/temperature + +Payload: +26.5 +``` + +发布过程为: + +```text +生成数据 + ↓ +指定 Topic + ↓ +调用 publish + ↓ +发送到 Broker +``` + +### 4.5 Subscriber + +Subscriber 是消息订阅方。 + +例如: + +```text +subscribe: +sensor/temperature +``` + +订阅成功后: + +```text +Broker 收到新消息 + ↓ +匹配 Topic + ↓ +发送给 Subscriber + ↓ +触发回调函数 +``` + +因此 MQTT 客户端通常需要设置消息回调函数处理接收到的数据。 + +### 4.6 Client ID + +MQTT Broker 需要通过 Client ID 区分不同客户端。 + +结构为: + +```text +Broker + │ + ├── Client A + ├── Client B + └── Client C +``` + +不同设备应尽量使用不同的 Client ID。 + +如果多个设备使用完全相同的 Client ID,部分 Broker 可能认为新的连接替代了旧连接,从而导致客户端互相掉线。 + +### 4.7 MQTT 连接流程 + +一个典型 MQTT 客户端的执行流程为: + +```text +网络准备完成 + ↓ +初始化 MQTT + ↓ +创建客户端 + ↓ +配置 Broker 地址 + ↓ +配置端口 + ↓ +设置 Client ID + ↓ +连接 Broker + ↓ +订阅 Topic + ↓ +发布 / 接收消息 +``` + +MQTT 常见的未加密 TCP 端口为: + +```text +1883 +``` + +基于 TLS 的 MQTT 通常使用其他端口,例如常见的: + +```text +8883 +``` + +### 4.8 回调机制 + +MQTT 接收消息通常采用回调方式。 + +执行逻辑为: + +```text +Broker + ↓ +消息到达 + ↓ +MQTT Client + ↓ +识别 Topic + ↓ +调用 Callback + ↓ +应用程序处理 Payload +``` + +回调机制可以避免应用程序持续主动查询是否有新消息。 + +------ + +## 5. DFS 虚拟文件系统 + +DFS 的全称是: + +```text +Device File System +``` + +它是 RT-Thread 提供的虚拟文件系统组件。 + +DFS 的主要目标是: + +> 为不同类型的文件系统和存储设备提供统一的文件访问接口。 + +例如: + +```text +应用程序 + ↓ +open / read / write / close + ↓ +DFS + ↓ +文件系统 + ↓ +Block Device + ↓ +Flash / SD Card +``` + +应用层不需要直接关心底层究竟是 SD 卡还是 SPI Flash。 + +### 5.1 虚拟文件系统思想 + +不同存储设备之间的底层驱动差异很大。 + +例如: + +```text +SD Card +SPI Flash +NOR Flash +NAND Flash +RAM Disk +``` + +如果应用程序直接针对每种存储介质开发,就需要编写大量不同的代码。 + +VFS 的作用就是在中间增加统一抽象层: + +```text + 应用程序 + ↓ + VFS + ┌────────┼────────┐ + ↓ ↓ ↓ + FATFS LittleFS ... + ↓ ↓ + SD卡 Flash +``` + +因此应用程序可以使用统一接口操作不同文件系统。 + +### 5.2 POSIX 接口 + +DFS 支持常见的 POSIX 风格文件接口,例如: + +```c +open() +read() +write() +close() +``` + +这些接口与 Linux 中的文件操作方式类似。 + +一个典型文件操作流程为: + +```text +open + ↓ +获得文件描述符 + ↓ +read / write + ↓ +close +``` + +这种设计可以提高代码的可移植性。 + +### 5.3 文件描述符 + +调用: + +```c +open() +``` + +成功后通常会返回一个整数。 + +这个整数称为: + +```text +File Descriptor +``` + +简称: + +```text +fd +``` + +例如: + +```text +fd = open(...) +``` + +后续读写时通过 fd 表示已经打开的文件。 + +基本关系为: + +```text +文件路径 + ↓ +open() + ↓ +fd + ↓ +read / write + ↓ +close(fd) +``` + +------ + +## 6. Flash 存储体系 + +在嵌入式设备中,Flash 是常见的非易失性存储介质。 + +所谓非易失性,是指: + +> 断电之后,数据仍然能够保存。 + +常见 Flash 可以分为: + +```text +内部 Flash +外部 Flash +``` + +### 6.1 内部 Flash + +内部 Flash 一般位于 MCU 芯片内部。 + +常用于存放: + +```text +程序固件 +Bootloader +配置数据 +参数 +``` + +### 6.2 外部 Flash + +如果内部 Flash 容量不足,可以通过 SPI 等接口连接外部 Flash。 + +例如: + +```text +W25Q64 +``` + +属于常见的 SPI NOR Flash。 + +整体结构为: + +```text +MCU + ↓ +SPI + ↓ +W25Q64 +``` + +外部 Flash 可以用于: + +```text +文件 +日志 +图片 +字体 +配置 +历史数据 +``` + +------ + +## 7. FAL Flash 抽象层 + +FAL 的全称是: + +```text +Flash Abstraction Layer +``` + +即 Flash 抽象层。 + +它主要用于统一管理不同类型的 Flash 设备。 + +没有抽象层时: + +```text +应用 + ↓ +内部 Flash API +``` + +或: + +```text +应用 + ↓ +W25Q64 API +``` + +不同 Flash 的接口可能完全不同。 + +加入 FAL 后: + +```text +应用层 + ↓ +FAL + ↓ +┌───────────────┐ +│ Internal Flash│ +│ SPI Flash │ +│ Other Flash │ +└───────────────┘ +``` + +应用层就可以通过统一方式访问不同 Flash。 + +### 7.1 Flash 分区 + +FAL 还提供 Flash 分区管理功能。 + +一个完整 Flash 可以划分为多个区域: + +```text +Flash +│ +├── Bootloader +├── Application +├── Download +├── File System +└── User Data +``` + +每个分区都有: + +```text +名称 +起始地址 +长度 +所属 Flash 设备 +``` + +通过分区机制可以避免不同功能的数据互相覆盖。 + +------ + +## 8. SFUD + +SFUD 的全称是: + +```text +Serial Flash Universal Driver +``` + +即串行 Flash 通用驱动。 + +它主要用于解决不同 SPI Flash 芯片之间的兼容问题。 + +不同厂商的 SPI Flash: + +```text +W25Qxx +MX25Lxx +GD25Qxx +... +``` + +在: + +- 容量 +- JEDEC ID +- 擦除方式 +- 指令 +- 特性 + +等方面可能存在差异。 + +SFUD 在中间提供统一驱动层: + +```text +上层程序 + ↓ +SFUD + ↓ +SPI Flash +``` + +这样应用层无需针对每一种 Flash 芯片重新编写完整驱动。 + +### 8.1 FAL 与 SFUD 的关系 + +FAL 与 SFUD 的作用不同。 + +可以简单理解为: + +```text +FAL + ↓ +负责 Flash 抽象与分区 + +SFUD + ↓ +负责具体 SPI Flash 驱动 +``` + +组合后的结构可以表示为: + +```text +应用程序 + ↓ +文件系统 + ↓ +FAL + ↓ +SFUD + ↓ +SPI + ↓ +W25Q64 +``` + +------ + +## 9. DFS 与 Flash 的关系 + +DFS 本身不是 Flash 驱动。 + +DFS 主要负责: + +```text +文件访问统一接口 +``` + +而 FAL、SFUD 等组件主要负责: + +```text +Flash 设备访问 +``` + +整体软件层次可以表示为: + +```text +应用程序 + ↓ +open / write / close + ↓ +DFS + ↓ +具体文件系统 + ↓ +块设备 / MTD 设备 + ↓ +FAL + ↓ +SFUD + ↓ +SPI + ↓ +W25Q64 +``` + +因此: + +- DFS 解决“如何以文件形式访问” +- FAL 解决“如何统一管理 Flash” +- SFUD 解决“如何驱动不同 SPI Flash” + +------ + +## 10. 文件系统挂载 + +文件系统在使用之前通常需要挂载。 + +挂载可以理解为: + +> 将一个实际存储设备上的文件系统映射到系统目录树中的某个路径。 + +例如: + +```text +W25Q64 + ↓ +文件系统 + ↓ +挂载 + ↓ +/flash +``` + +之后应用程序就可以通过: + +```text +/flash/test.txt +``` + +访问 Flash 中的文件。 + +整体过程为: + +```text +Flash 初始化 + ↓ +注册 Flash 设备 + ↓ +初始化文件系统 + ↓ +mount + ↓ +建立挂载点 + ↓ +应用程序访问文件 +``` + +### 10.1 文件写入流程 + +使用 POSIX 接口进行文件写入时,整体过程为: + +```text +open() + ↓ +打开或创建文件 + ↓ +write() + ↓ +写入数据 + ↓ +close() + ↓ +关闭文件 +``` + +应用层使用标准接口后,不需要关心底层 Flash 的页编程和扇区擦除等细节。 + +------ + +## 11. SPI 总线共享 + +在嵌入式系统中,一条 SPI 总线可以同时连接多个从设备。 + +例如: + +```text + ┌── AW007 WiFi +MCU ─── SPI ────┤ + └── W25Q64 Flash +``` + +通常: + +```text +SCK 共享 +MOSI 共享 +MISO 共享 +CS 独立 +``` + +通过 CS 信号决定当前与哪个设备进行通信。 + +### 11.1 CS 片选信号 + +CS 的作用是选择 SPI 从设备。 + +例如: + +```text +CS_WIFI = 0 +CS_FLASH = 1 +``` + +表示当前选择 WiFi。 + +反之: + +```text +CS_WIFI = 1 +CS_FLASH = 0 +``` + +表示当前选择 Flash。 + +大多数 SPI 设备的 CS 为低电平有效。 + +### 11.2 SPI 总线冲突 + +如果多个 SPI 从设备同时被选中,就可能产生总线冲突。 + +例如: + +```text +AW007 CS = 0 +W25Q64 CS = 0 +``` + +这时两个设备可能同时驱动 MISO 信号,导致: + +```text +数据异常 +通信失败 +Flash 读取错误 +WiFi 工作异常 +``` + +因此需要保证: + +> 任意时刻只有需要通信的 SPI 从设备处于选中状态。 + +### 11.3 软件层协调 + +在使用某个 SPI 设备前,可以先保证其他设备的 CS 处于非选中状态。 + +例如: + +```text +访问 Flash 前 + ↓ +拉高 WiFi CS + ↓ +拉低 Flash CS + ↓ +执行 Flash 通信 + ↓ +释放 Flash CS +``` + +这也是多设备共享 SPI 总线时非常重要的基本原则。 + +------ + +## 12. Day5 知识体系总结 + +Day5 的主要内容可以按照“传感器、网络、通信协议、文件系统、Flash”几个部分进行理解。 + +整体软件结构如下: + +```text + RT-Thread 应用层 + │ + ┌──────────────┼──────────────┐ + │ │ │ + ↓ ↓ ↓ + HT10 Sensor MQTT DFS + │ │ │ + ↓ ↓ ↓ + Sensor Framework TCP/IP 文件系统 + │ │ │ + ↓ ↓ ↓ + I2C AW007 WiFi FAL + │ │ + ↓ ↓ + SPI SFUD + │ + ↓ + SPI + │ + ↓ + W25Q64 +``` + +各组件的主要作用如下: + +| 组件 | 主要作用 | +| ---------------- | ----------------------------------- | +| RT-Thread 软件包 | 管理和复用第三方功能组件 | +| env | 软件包管理及配置环境 | +| Kconfig | 描述功能选项和依赖关系 | +| I2C | HT10 与 MCU 之间的数据通信 | +| Sensor Framework | 提供统一传感器访问框架 | +| AW007 | 为设备提供 WiFi 网络连接能力 | +| SPI | MCU 与 WiFi、Flash 等设备通信 | +| MQTT | 实现基于 Topic 的发布/订阅通信 | +| Broker | MQTT 消息接收和转发中心 | +| DFS | 提供统一的文件系统访问接口 | +| POSIX | 提供 `open/read/write/close` 等接口 | +| FAL | Flash 抽象和分区管理 | +| SFUD | 通用 SPI Flash 驱动 | +| W25Q64 | 外部非易失性存储设备 | + +Day5 的核心知识链路可以总结为: + +```text +RT-Thread 软件包 + ↓ +快速集成外设和组件 + ↓ +┌─────────────────────────────────┐ +│ │ +HT10 AW007 +│ │ +I2C SPI +│ │ +Sensor Framework WiFi + │ + ↓ + TCP/IP + │ + ↓ + MQTT +``` + +存储部分则为: + +```text +应用程序 + ↓ +POSIX API + ↓ +DFS + ↓ +文件系统 + ↓ +FAL + ↓ +SFUD + ↓ +W25Q64 Flash +``` + +通过 Day5 的学习,可以进一步理解 RT-Thread 并不仅仅是一个实时操作系统内核,还提供了从**设备驱动、软件包管理、网络通信到文件系统和存储管理**的一整套嵌入式软件开发框架。 \ No newline at end of file -- Gitee