Skip to content

Commit c3b6951

Browse files
committed
docs: refresh content and sync English pages
1 parent 2436ea8 commit c3b6951

114 files changed

Lines changed: 9296 additions & 1179 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/about.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ sidebar_position: 12
88

99
XRobot前身为[qdu-rm-mcu](https://gitee.com/qsheeeeen/qdu-rm-mcu.git),在2019年1月19日由[@qsheeeeen](https://gitee.com/qsheeeeen)创建。至今一直作为青岛大学Robomaster未来战队的电控框架,同时被多所高校和公司使用。耗时多年,XRobot从一个基于Keil MDK的战车MCU代码,成长为一个跨平台的嵌入式软件框架,再进化为覆盖完整嵌入式开发流程的“神兵利器”。历经风风雨雨,XRobot2.0正式发布,希望与你一同前进。
1010

11+
## 当前贡献者
12+
13+
以下名单按人工维护,不包含 bot 账号。
14+
15+
- <img src="https://github.com/Jiu-xiao.png?size=48" width="24" height="24" style={{borderRadius: '50%', verticalAlign: 'middle'}} alt="@Jiu-xiao avatar" /> [@Jiu-xiao](https://github.com/Jiu-xiao) - 框架设计者,`STM32 / CH32 / Linux` 驱动开发
16+
- <img src="https://github.com/molqzone.png?size=48" width="24" height="24" style={{borderRadius: '50%', verticalAlign: 'middle'}} alt="@molqzone avatar" /> [@molqzone](https://github.com/molqzone) - `MSPM0``DAPLink`
17+
- <img src="https://github.com/CaFeZn.png?size=48" width="24" height="24" style={{borderRadius: '50%', verticalAlign: 'middle'}} alt="@CaFeZn avatar" /> [@CaFeZn](https://github.com/CaFeZn) - `HPM` 系列
18+
- <img src="https://github.com/llLeo306.png?size=48" width="24" height="24" style={{borderRadius: '50%', verticalAlign: 'middle'}} alt="@llLeo306 avatar" /> [@llLeo306](https://github.com/llLeo306) - 控制与实时系统
19+
1120
合作或其他问题请随时联系我们
1221

1322
Github主页: [@xiao](https://github.com/Jiu-xiao)

docs/adv_coding/README.md

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@ sidebar_position: 10
66

77
# 进阶编程
88

9-
本章将深入介绍更为底层和高阶的编程主题,包括:
9+
## 目录
1010

11-
- 驱动开发的底层实现
12-
- IO 与通信机制的原理与优化
13-
- 无锁/高性能数据结构设计
14-
- 代码性能与实时性调优方法
15-
16-
无论你是想开发自己的高性能模块、调试复杂外设,还是对系统底层原理感兴趣,都可以在本章找到实用的技术细节和最佳实践建议。
11+
- [核心机制](./core/README.md)
12+
- [中间件实现](./middleware/README.md)
13+
- [驱动开发](./driver/README.md)

docs/adv_coding/core/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
id: adv-coding-core
3+
title: 核心机制
4+
sidebar_position: 1
5+
---
6+
7+
# 核心机制
8+
9+
## 目录
10+
11+
- [IO 完成语义与 Port 状态机](./rw_semantics.md)
12+
- [ISR、回调与线程边界](./isr_thread_boundary.md)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
id: adv-coding-core-isr-thread-boundary
3+
title: ISR、回调与线程边界
4+
sidebar_position: 2
5+
---
6+
7+
# ISR、回调与线程边界
8+
9+
基础说明见 [设计思想](/docs/concept)[异步任务](/docs/basic_coding/system/async)[Semaphore](/docs/basic_coding/system/semaphore)。下文直接收束实现里几条最容易跑偏的边界。
10+
11+
## 单核 MCU 里的并发来源
12+
13+
`LibXR` 里这个问题的关键从来不在“有没有多核”,而在于数据和状态怎样在 `ISR`、回调、线程之间交接。单核 MCU 同样有并发:任务会互相被调度打断,ISR 会抢占线程,DMA 和外设也会按自己的节奏推进。只要一条高频路径把等待、长临界区或者调度顺序依赖混进去,最后看到的通常就是时延抖动、状态错位,以及 timeout、reset 和 late completion 互相踩踏。
14+
15+
## ISR 负责什么
16+
17+
因此更稳妥的默认原则一直是:ISR 负责交接和推进,线程负责展开后续业务。所谓交接,指的是缓冲切换、计数和时间戳更新、端点 rearm、DMA 续传、把数据推进软件队列,以及唤醒 waiter 或 worker。这些动作只要边界明确、耗时可估计,就应该尽量在 ISR 或 callback 中完成。真正不该放进去的,是阻塞等待、复杂协议解析、资源申请,或者任何依赖线程唤醒顺序才能正确收尾的逻辑。
18+
19+
## 为什么默认不用 `mutex`
20+
21+
很多人第一反应会是 `mutex`,但它并不是 ISR-task 交换的默认原语。原因很直接:ISR 必须保持 non-blocking,而很多 RTOS 根本不允许在 ISR 中获取 `mutex`。即使勉强用临界区包装,最后也常常退化成关中断时间不可控的问题。所以更实际的默认选择是:task-task 之间用 `mutex` 或有界临界区;ISR-task 之间优先用 `FromISR` 原语、`SPSC ring`、mailbox 或 sequence;只有在测量证明值得的情况下,才上更重的 CAS/lock-free 结构。
22+
23+
## 原子操作的角色
24+
25+
这也引出另一个常见误解:`CAS` 和原子操作不是 SMP 专属。在单核 MCU 上,它们一样有价值,因为它们解决的是 ISR 抢占线程时的 read-modify-write 竞态、ownership 状态 claim,以及 `busy/pending/detached` 这类轻量状态交接。单核不等于没有竞态,只是竞态发生在上下文切换之间,而不是多个核心同时执行。
26+
27+
## `FromCallback` 和 ISR 不是一回事
28+
29+
回调上下文和 ISR 上下文也不能混为一谈。`FromCallback` 的意思是“当前需要 callback-safe 语义”,并不自动等于“此刻就在硬中断里”。这也是为什么现在更强调 `ASSERT_FROM_CALLBACK(...)``PostFromCallback(in_isr)``ActiveFromCallback(..., in_isr)`,而不是把所有 callback-safe 路径都粗暴地等同于 ISR。两者一旦混成一类,接口语义很快就会漂移:本来只需要 callback-safe 的路径,会被迫套上更严格的 ISR 约束;而真正只能在 ISR 里做的动作,也会因为边界不清而被错误地下沉到普通回调里。
30+
31+
## 为什么 `BLOCK` 不能进 ISR
32+
33+
`BLOCK` 在 ISR 中同样应该被视为硬错误,而不是“风格不好”。`BLOCK` 最终一定会走到 `sem->Wait(...)` 一类等待路径;一旦把这件事塞进 ISR,后果不是接口不优雅,而是系统直接失去边界。所以更合理的做法始终是:ISR 里只投递、切状态、post,真正等待结果的动作只留给线程上下文。`Operation::UpdateStatus()` 能通过 `PostFromCallback(in_isr)` 让完成通知 callback-safe,并不意味着 `BLOCK` 本身在 ISR 中也是合法的。
34+
35+
## freshness-first 场景
36+
37+
对控制和状态估计类场景来说,另一个很容易选错的地方是“是否该上深队列”。如果系统真正关心的是最新值,而不是保存每一个旧样本,那么默认更合适的结构通常是 `latest + seq` 或单槽 mailbox,而不是深队列。深队列适合完整保留样本、允许消费延迟的场景;在 freshness-first 路径里,它反而会把“旧但合法”的数据拖进系统。因此这类场景最好把 contract 直接写出来,例如“不得使用超过 2 个控制周期前的数据”“允许 `drop_oldest`”“必须暴露 overflow / drop 计数”。
38+
39+
## `ASync` 在这套边界里的位置
40+
41+
`ASync` 也应该放在这个边界里理解。它不是一个“任何耗时工作都能往里扔”的免责桶,而更像一个统一提交接口:在 callback 或 ISR 里只做短交接,再把后续可以在线程执行的工作延后。不过这里还要记住一个现实约束:无线程实现下,`ASync` 当前会退化为同步直调包装。换句话说,它可以统一提交语义,但不保证所有系统里都真的存在一条独立后台线程。
42+
43+
## 选择原则
44+
45+
如果要在工程里先快速做判断,可以记住一条简单标准:先问这一步是不是硬件交接的一部分,它的执行时间是不是短且可界定,以及它是否需要等待、分配资源或依赖调度顺序。前两条为“是”、第三条为“否”,通常就适合 ISR 或 callback;反过来,就应该尽量下沉到线程。
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
---
2+
id: adv-coding-core-rw-semantics
3+
title: IO 完成语义与 Port 状态机
4+
sidebar_position: 1
5+
---
6+
7+
# IO 完成语义与 Port 状态机
8+
9+
基础 API 见 [IO 读写抽象](/docs/basic_coding/core/core-rw)[Operation 操作模型](/docs/basic_coding/core/core-op)。下文直接讨论这套 I/O 完成模型为什么这样组织。
10+
11+
`LibXR` 当前这套 I/O 完成模型可以看成三层:`Operation` 描述这次操作完成后该怎样反馈,`ReadPort / WritePort` 负责队列、busy 状态和完成交接,具体驱动只负责把硬件推进到“接受请求”或“完成请求”这两个边界。真正复杂的状态机留在 `Port`,而不是绑在 `Operation` 上;这不是偶然,而是设计本意。
12+
13+
`Operation` 故意被做成体积很小、可平凡拷贝的对象,只携带完成方式本身:`CALLBACK``BLOCK``POLLING``NONE`,外加对应的小型数据载体。这样做的目的,就是避免把复杂生命周期、waiter 归属、timeout 后交接之类的状态机塞进 `Operation` 本体。否则它一旦变成重对象,不仅复制和传递成本会上升,驱动层和端口层的边界也会开始混乱。
14+
15+
`BLOCK` 来说,这一点尤其重要。`Operation::UpdateStatus()``BLOCK` 模式下只负责 `sem->PostFromCallback(in_isr)`,并不会把最终 `ErrorCode` 本身塞进信号量语义里。最终结果仍由端口侧的 `block_result_` 交接。换句话说,信号量在这里表达的是“可以醒来检查归属了”,而不是“这次操作必然已经合法完成了”。这正是为了把最危险的问题挡住:一个已经 timeout 返回的 waiter,后面又被迟到完成再次 `post`,于是同一个 semaphore 出现多次唤醒,旧 token 被新调用误认成当前完成。`BLOCK timeout` 首先要防的,就是这种“post 了已经超时返回的信号量”的情况。
16+
17+
## 1. `ReadPort` 的状态机
18+
19+
`ReadPort` 当前的核心状态有:
20+
21+
| 状态 | 含义 |
22+
| ---- | ---- |
23+
| `IDLE` | 没有挂起读,也没有待交接完成 |
24+
| `PENDING` | 请求已交给底层推进,等待队列侧完成 |
25+
| `BLOCK_CLAIMED` | 当前 `BLOCK` 唤醒已经归这次 waiter 所有 |
26+
| `BLOCK_DETACHED` | timeout 或 reset 已把 waiter 分离,完成侧必须静默 |
27+
| `EVENT` | 数据先到、waiter 还没挂起;下一次调用必须先重查队列 |
28+
29+
这里最容易被误解的是 `EVENT`
30+
31+
它不是“读完成”,而是:
32+
33+
- 数据先进入了软件队列
34+
- 但当时还没有可认领的挂起读请求
35+
36+
所以后续调用方必须重新检查一次 `queue_data_`,而不是盲目重新下发底层读。
37+
38+
---
39+
40+
## 2. `WritePort` 的状态机
41+
42+
`WritePort` 的状态机和 `ReadPort` 不同,因为写路径多了“提交队列所有权”这件事。
43+
44+
核心状态有:
45+
46+
| 状态 | 含义 |
47+
| ---- | ---- |
48+
| `IDLE` | 没有活动提交者,也没有挂起中的 `BLOCK` waiter |
49+
| `LOCKED` | 当前提交路径占有队列修改权 |
50+
| `BLOCK_WAITING` | 一个 `BLOCK` waiter 已经挂起,但完成还没 claim |
51+
| `BLOCK_CLAIMED` | 最终唤醒已经归当前 waiter 所有 |
52+
| `BLOCK_DETACHED` | timeout/reset 已把 waiter 分离,完成侧不能再 post |
53+
54+
这里的关键点是:
55+
56+
- 多线程并发写安全,并不是“无锁队列自己解决了一切”
57+
- 真正的外层安全边界是 `busy_` 这道原子门
58+
59+
也就是说,多个线程不会同时直接冲进驱动 `WriteFun()` 抢底层硬件;只有拿到 `LOCKED` 的那一条路径,才拥有这次提交的队列修改权和 kickoff 权。
60+
61+
---
62+
63+
## 3. 端口眼里的“成功”是什么意思
64+
65+
端口层里最重要的一条约定是:
66+
67+
- 驱动返回 `PENDING`:表示“底层已接受,完成以后再交接”
68+
- 驱动返回非 `PENDING`:表示“这次调用已经终结”
69+
70+
这就是当前契约里“non-`PENDING` is terminal”的含义。
71+
72+
它直接带来两个后果:
73+
74+
1. 如果驱动声称自己没进入 `PENDING`,端口不会再替它维护后续完成语义
75+
2. 如果驱动在返回非 `PENDING` 后,底层其实还在后台继续跑,就会出现语义错位
76+
77+
因此驱动设计里必须非常明确:什么时刻算“已经交给硬件”,什么时刻又只是暂时 busy、还没真正接单。`PENDING` 和 non-`PENDING` 的边界一旦划错,就会出现很难看的语义错位:端口以为这次调用已经终结,但底层还在后台继续推进,结果 `BUSY / TIMEOUT / late completion` 全部缠在一起。很多看上去像队列 bug 的问题,本质上都是这里没有钉牢。
78+
79+
## 4. `BLOCK` timeout 不是取消
80+
81+
`ReadOperation(sem, timeout)` / `WriteOperation(sem, timeout)`
82+
`timeout` 都是:
83+
84+
- 相对等待时长
85+
- 传给 `Semaphore::Wait(timeout)` 的参数
86+
87+
它不是绝对 deadline,也不自动意味着底层取消。
88+
89+
因此 `BLOCK timeout` 的真实含义是:它只限制这次同步等待窗口,并不保证已经被底层接受的操作会被撤销。timeout 之后要做的关键工作,不是“把硬件立刻停掉”,而是把完成归属处理对。只要底层已经启动,迟到完成就仍然可能发生;如果这时候还允许它继续 `post` 旧的 semaphore,就会出现重复唤醒。`BLOCK_DETACHED` 一类状态存在的意义,就是告诉完成路径:这次 waiter 已经不属于原调用者了,后续只能静默收尾,不能再把唤醒投给它。
90+
91+
## 5. `Reset()` 为什么也走 detach 模型
92+
93+
`Reset()` 当前不是简单粗暴地把状态直接清成 `IDLE`
94+
95+
`BLOCK` 路径来说,它和 timeout 的处理模型是一致的:
96+
97+
1. 先把当前 waiter 分离
98+
2. 让迟到完成保持静默
99+
3. 等旧交接彻底排空后再重新开放端口
100+
101+
这样做的目的,是避免下面这类竞态:
102+
103+
- 调用者已经 timeout / reset 返回
104+
- 底层旧完成稍后到来
105+
- 老完成又把新的调用错误唤醒
106+
107+
---
108+
109+
所以 `Reset()` 和 timeout 在这里其实是同一类问题:都要先分离当前 waiter,再等旧交接彻底排空,而不是假装世界已经恢复干净。
110+
111+
## 6. `AsyncBlockWait` 的位置
112+
113+
`AsyncBlockWait` 不是给 `ReadPort / WritePort` 自己用的状态机替代品,它更像是:
114+
115+
- 给具体驱动内部“同步外观 + 异步硬件”这类路径准备的共享 waiter handoff
116+
117+
它的状态非常直接:
118+
119+
| 状态 | 含义 |
120+
| ---- | ---- |
121+
| `IDLE` | 当前没有活跃等待者 |
122+
| `PENDING` | waiter 已挂起,等待完成 |
123+
| `CLAIMED` | 完成已经认领当前 waiter |
124+
| `DETACHED` | timeout 已分离 waiter,完成只能静默回收 |
125+
126+
适合它的场景通常是:
127+
128+
- 驱动本身没有完整走 `ReadPort / WritePort`
129+
- 但仍然需要“同步等待一个异步完成”
130+
131+
例如某些 `SPI / I2C` 驱动里的 `BLOCK` 事务。
132+
133+
---
134+
135+
## 7. 读这套状态机时的总规则
136+
137+
可以把 `Port` 看成“完成所有权转移器”。
138+
139+
它真正管理的不是:
140+
141+
- 数据从哪里来
142+
- 数据发到哪里去
143+
144+
而是:
145+
146+
- 现在这次完成到底属于谁
147+
- timeout 之后谁还可以说话
148+
- 迟到完成应该唤醒谁,还是必须静默
149+
150+
如果按这个视角去读 `libxr_rw.*`,很多状态名就会顺很多。

docs/adv_coding/driver/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
---
22
id: adv-coding-drv
33
title: 驱动开发
4-
sidebar_position: 1
4+
sidebar_position: 4
55
---
66

77
# 驱动开发
88

9-
理解双缓冲、异步和零拷贝等概念,学习如何写出高效易用的驱动程序。
9+
## 目录
10+
11+
- [双缓冲区](./dbf.md)
12+
- [串口驱动设计](./uart_driver.md)
13+
- [BLOCK 超时与完成交接](./block_timeout_semantics.md)

0 commit comments

Comments
 (0)