├── .gitignore
├── src
├── start
│ ├── semihosting
│ ├── io.md
│ ├── index.md
│ ├── interrupts.md
│ ├── panicking.md
│ ├── semihosting.md
│ ├── exception.md
│ ├── registers.md
│ ├── harware.md
│ └── qemu.md
├── assets
│ ├── f3.jpg
│ ├── crates.png
│ ├── verify.jpeg
│ ├── nrf52-memory-map.png
│ ├── nrf52-spi-frequency-register.png
│ └── rust_layers.svg
├── intro
│ ├── install
│ │ ├── macos.md
│ │ ├── windows.md
│ │ ├── verify.md
│ │ └── linux.md
│ ├── hardware.md
│ ├── install.md
│ ├── tooling.md
│ ├── no-std.md
│ └── index.md
├── peripherals
│ ├── borrowck.md
│ ├── index.md
│ ├── singletons.md
│ └── a-first-attempt.md
├── static-guarantees
│ ├── index.md
│ ├── zero-cost-abstractions.md
│ ├── typestate-programming.md
│ ├── state-machines.md
│ └── design-contracts.md
├── portability
│ └── index.md
├── SUMMARY.md
├── collections
│ └── index.md
└── concurrency
│ └── index.md
├── noun.md
├── book.toml
├── README.md
└── .github
└── workflows
└── main.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/src/start/semihosting:
--------------------------------------------------------------------------------
1 | # Semihosting
2 |
--------------------------------------------------------------------------------
/src/start/io.md:
--------------------------------------------------------------------------------
1 | # IO
2 |
3 | > **TODO** 使用寄存器覆盖内存映射IO.
4 |
--------------------------------------------------------------------------------
/noun.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | 未能确定翻译的名词
4 |
5 | - [ ] bump pointer allocator
6 |
--------------------------------------------------------------------------------
/src/assets/f3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logiase/The-Embedded-Rust-Book-CN/HEAD/src/assets/f3.jpg
--------------------------------------------------------------------------------
/src/assets/crates.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logiase/The-Embedded-Rust-Book-CN/HEAD/src/assets/crates.png
--------------------------------------------------------------------------------
/src/assets/verify.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logiase/The-Embedded-Rust-Book-CN/HEAD/src/assets/verify.jpeg
--------------------------------------------------------------------------------
/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Logiase"]
3 | language = "en"
4 | multilingual = false
5 | src = "src"
6 | title = "嵌入式Rust之书"
7 |
--------------------------------------------------------------------------------
/src/assets/nrf52-memory-map.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logiase/The-Embedded-Rust-Book-CN/HEAD/src/assets/nrf52-memory-map.png
--------------------------------------------------------------------------------
/src/assets/nrf52-spi-frequency-register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Logiase/The-Embedded-Rust-Book-CN/HEAD/src/assets/nrf52-spi-frequency-register.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 嵌入式Rust之书
2 |
3 | 在线阅读地址[嵌入式Rust之书](https://logiase.github.io/The-Embedded-Rust-Book-CN/)
4 |
5 | 本书为[The embedded Rust book](https://rust-embedded.github.io/book/#introduction)的中文翻译
6 |
7 | ## 目前仍在施工
8 |
9 | :hammer: :hammer:
10 |
--------------------------------------------------------------------------------
/src/start/index.md:
--------------------------------------------------------------------------------
1 | # 开始
2 |
3 | 在这一部分, 我们会带你写代码, 编译, 烧录, 调试嵌入式程序.
4 | 我们会教你QEMU的基础, 一个开源的硬件模拟器, 因此你能不用硬件来运行大部分例子.
5 | 很自然的, 唯一需要硬件的章节就是[硬件](./hardware.md), 在这部分我们使用OpenOCD在[STM32F3DISCOVERY]
6 |
7 | [STM32F3DISCOVERY]: http://www.st.com/en/evaluation-tools/stm32f3discovery.html
8 |
--------------------------------------------------------------------------------
/src/intro/install/macos.md:
--------------------------------------------------------------------------------
1 | # macOS
2 |
3 | 所有工具都可以用[Homebrew]安装:
4 |
5 | [Homebrew]: http://brew.sh/
6 |
7 | ``` console
8 | $ # GDB
9 | $ brew install armmbed/formulae/arm-none-eabi-gcc
10 |
11 | $ # OpenOCD
12 | $ brew install openocd
13 |
14 | $ # QEMU
15 | $ brew install qemu
16 | ```
17 |
18 | 这就是全部了, [下一部分].
19 |
20 | [下一部分]: verify.md
--------------------------------------------------------------------------------
/src/peripherals/borrowck.md:
--------------------------------------------------------------------------------
1 | ## 可变全局状态
2 |
3 | 很不幸, 硬件基本是就是可变的全局状态, 这对于 Rust 开发者来说非常恐怖. 硬件独立于我们写的代码而存在, 并且可以随时被现实世界改变状态.
4 |
5 | ## 我们的规则?
6 |
7 | 我们如何和这些外设进行可靠的交互?
8 |
9 | 1. 始终使用 `volatile` 方法读取或写入外设内存, 因为它随时可能发生变化
10 | 2. 在软件中, 我们应该只共享这些外设的不可变引用
11 | 3. 如果某些软件需要对外设进行读写, 则应该保留对该外设的唯一引用
12 |
13 | ## 引用检查器
14 |
15 | 这些规则的最后两条听起来很想引用检查器已经在做的事情!
16 |
17 | 想象一下我们是否可以放弃对这些外设的所有权, 或者只是用可变或者不可变的引用?
18 |
19 | 好吧, 我们可以, 但是对于引用检查器, 我们需要每个外设都存在一个唯一实例, 来让 Rust 正确处理引用检查. 幸运的是, 在硬件中任何外设都只有一个实例, 但是如何在代码中展示出来呢?
20 |
--------------------------------------------------------------------------------
/src/static-guarantees/index.md:
--------------------------------------------------------------------------------
1 | # 静态保证
2 |
3 | Rust 的类型系统可以防止在编译时发生数据竞争(参考[`Send`]和[`Sync`] traits). 类型系统还可以用来检查编译时的其他属性; 减少了对运行时检查的需求.
4 |
5 | [`Send`]: https://doc.rust-lang.org/core/marker/trait.Send.html
6 | [`Sync`]: https://doc.rust-lang.org/core/marker/trait.Sync.html
7 |
8 | 当应用于嵌入式程序时, 可以使用下面这些*静态检查*, 例如, 强制完成I/O的正确配置. 例如, 可以设计一种API, 在该API中只能先配置该就扣用到的引脚来初始化串口.
9 |
10 | 人们还可以静态检查操作, 像是只能通过正确配置的外设来让一个引脚为低. 例如, 改变浮动输入模式的引脚的输出状态会产生编译错误.
11 |
12 | 而且, 像上一章说的, 所有权的概念可以应用于外设, 以确保只有程序的某些部分才能修改外设. 与将外设设置为全局变量的方法相比, 这种*访问控制*的方法使应用更容易理解.
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Update Book
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Setup mdBook
16 | uses: peaceiris/actions-mdbook@v1
17 | with:
18 | mdbook-version: 'latest'
19 |
20 | - run: mdbook build
21 |
22 | - name: deploy ghpages
23 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
24 | uses: peaceiris/actions-gh-pages@v3
25 | with:
26 | github_token: ${{ secrets.GH_TOKEN }}
27 | publish_dir: ./book
28 |
--------------------------------------------------------------------------------
/src/intro/install/windows.md:
--------------------------------------------------------------------------------
1 | # Windows
2 |
3 | ## `arm-none-eabi-gdb`
4 |
5 | ARM为Windows提供了`.exe`安装器. 从这[openocd]: 下载[here][gcc], 然后跟着说i名安装.
6 | 在安装结束之前选择"添加到环境变量"选项. 然后验证工具已经在`%PATH%`中:
7 |
8 | ``` console
9 | $ arm-none-eabi-gdb -v
10 | GNU gdb (GNU Tools for Arm Embedded Processors 7-2018-q2-update) 8.1.0.20180315-git
11 | (..)
12 | ```
13 |
14 | [gcc]: https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads
15 |
16 | ## OpenOCD
17 |
18 | 对Windows现在还没有官方的二进制文件, 但是如果你不想自己编译, xPack项目提供了一个二进制文件. [here][openocd]. 跟着安装说明. 然后更新你的`%PATH%`环境变量. (如果你使用快速安装`C:\Users\USERNAME\AppData\Roaming\xPacks\@xpack-dev-tools\openocd\0.10.0-13.1\.content\bin\`)
19 |
20 | [openocd]: https://xpack.github.io/openocd/
21 |
22 | 验证OpenOCD已经在`%PATH%`中:
23 |
24 | ``` console
25 | $ openocd -v
26 | Open On-Chip Debugger 0.10.0
27 | (..)
28 | ```
29 |
30 | ## QEMU
31 |
32 | 从[官方网站][qemu]获取QEMU.
33 |
34 | [qemu]: https://www.qemu.org/download/#windows
35 |
36 | ## ST-LINK USB驱动
37 |
38 | 你还需要安装[USB驱动], 否则OpenOCD不能工作. 跟着安装说明并且保证你安装了正确的版本(32位或64位).
39 |
40 | [USB驱动]: http://www.st.com/en/embedded-software/stsw-link009.html
41 |
42 | 这就是全部了, [下一部分]
43 |
44 | [下一部分]: verify.md
45 |
--------------------------------------------------------------------------------
/src/static-guarantees/zero-cost-abstractions.md:
--------------------------------------------------------------------------------
1 | # 零成本抽象
2 |
3 | 类型状态是零成本抽象的一个非常棒的例子, 将一些确定的行为转移到编译步骤. 这些状态不包含实际数据, 而是用来标记. 因为他们不包含数据, 所以在运行时, 他们在内存中并不存在.
4 |
5 | ```rust
6 | use core::mem::size_of;
7 |
8 | let _ = size_of::(); // == 0
9 | let _ = size_of::(); // == 0
10 | let _ = size_of::(); // == 0
11 | let _ = size_of::>(); // == 0
12 | ```
13 |
14 | ## 零大小类型
15 |
16 | ```rust
17 | struct Enabled;
18 | ```
19 |
20 | 像这样定义的类型叫做 Zero Sized Types (零大小类型), 因为他们并不包含实际的数据. 尽管这些类型在编译时实际存在, 你能复制, 移动, 引用他们. 但是优化器会完完全全的删掉他们.
21 |
22 | 在这段代码中:
23 |
24 | ```rust
25 | pub fn into_input_high_z(self) -> GpioConfig {
26 | self.periph.modify(|_r, w| w.input_mode().high_z());
27 | GpioConfig {
28 | periph: self.periph,
29 | enabled: Enabled,
30 | direction: Input,
31 | mode: HighZ,
32 | }
33 | }
34 | ```
35 |
36 | 我们返回的 `GpioConfig` 在运行时是不存在的. 调用此函数通常会简化为单个汇编指令 - 把一个固定的值写入一个固定的寄存器. 这意味着我们开发的状态类型接口是一个零成本抽象, 他不占用 CPU, RAM, 或代码空间来追踪 `GpioConfig` 的状态然后呈现为与直接访问寄存器相同的汇编代码.
37 |
38 | ## 嵌套
39 |
40 | 通常来说, 这些抽象你可以随便套, 只要使用的都是零大小类型, 整个结构在运行时就不会存在.
41 |
42 | 对于复杂或深度嵌套的结构, 定义所有的状态组合会非常麻烦, 在这种情况下, 使用宏会很舒服.
43 |
--------------------------------------------------------------------------------
/src/intro/hardware.md:
--------------------------------------------------------------------------------
1 | # 硬件
2 |
3 | 先让我们熟悉一下陪我们的开发板
4 |
5 | ## STM32F3DISCOVERY ("F3")
6 |
7 |
8 |
9 |
10 |
11 | 这块板子上都有什么?
12 |
13 | - [STM32F303VCT6](https://www.st.com/en/microcontrollers/stm32f303vc.html) mcu. 这块芯片有:
14 | - 支持单精度浮点的单核ARM Cortex-M4F处理器
15 | - 256 KiB 闪存 (1 KiB = 10**24** bytes)
16 | - 48 KiB RAM
17 | - 很多集成外设, 如 计时器, I2C, SPI, USART
18 | - 通过标有"USB USER"的USB接口
19 | - 一个[加速度传感器](https://en.wikipedia.org/wiki/Accelerometer) [LSM303DLHC](https://www.st.com/en/mems-and-sensors/lsm303dlhc.html)
20 | - 一个[磁强计](https://en.wikipedia.org/wiki/Magnetometer) [LSM303DLHC](https://www.st.com/en/mems-and-sensors/lsm303dlhc.html)
21 | - 一个[陀螺仪](https://en.wikipedia.org/wiki/Gyroscope) [L3GD20](https://www.pololu.com/file/0J563/L3GD20.pdf)
22 | - 8个呈指南针排列的LED
23 | - 第二个mcu [STM32F103](https://www.st.com/en/microcontrollers/stm32f103cb.html). 实际上是片上编程\调试器ST-LINK的一部分
24 |
25 | 关于这块板子更进一步的详细信息, 请参阅[STMicroelectronics](https://www.st.com/en/evaluation-tools/stm32f3discovery.html)
26 |
27 | 警告!: 如果你相对板子施加外部信号, 一定要小心! STM32F303VCT6引脚能承受的电压为3.3V. 更多有关信息, 请参阅用户手册中[6.2 Absolute maximum ratings section in the manual](https://www.st.com/resource/en/datasheet/stm32f303vc.pdf)
28 |
--------------------------------------------------------------------------------
/src/start/interrupts.md:
--------------------------------------------------------------------------------
1 | # 中断
2 |
3 | 中断在许多方面与异常有区别, 但是他们的操作与使用又在很大程度上相同, 并且他们也由同一中断控制器控制.
4 | 尽管异常是由Cortex-M架构定义的, 但是在中断的命名和功能上确实供应商(或芯片)确定的.
5 |
6 | 中断有很大的灵活性, 在尝试使用高级方法使用他们前, 应该考虑这些灵活性.
7 | 在本书中我们不会介绍用法, 但请牢记下面几点:
8 |
9 | - 中断具有可编程的优先级, 可以用来确定他们处理程序的顺序.
10 | - 中断可以嵌套, 可以抢占, 就是中断处理过程可能被另一个更高优先级的中断打断.
11 | - 通常需要处理掉触发中断的原因, 以防止无限进入中断.
12 |
13 | 运行时的常规初始化步骤始终相同:
14 |
15 | - 设置外设来启用中断.
16 | - 在中断处理器中设置优先级.
17 | - 在终端控制器中启用中断函数.
18 |
19 | 和异常一样, `cortex-m-rt`提供了一个[`interrupt`]属性来定义中断处理函数.
20 | 可用的中断(还有中断向量表中的位置)通常使用`svd2rust`由SVD文件自动生成.
21 |
22 | [`interrupt`]: https://docs.rs/cortex-m-rt-macros/0.1.5/cortex_m_rt_macros/attr.interrupt.html
23 |
24 | ``` rust,ignore
25 | // Interrupt handler for the Timer2 interrupt
26 | #[interrupt]
27 | fn TIM2() {
28 | // ..
29 | // Clear reason for the generated interrupt request
30 | }
31 | ```
32 |
33 | 中断处理函数看起来就像普通函数一样(除了缺少参数).
34 | 但是由于特殊的调用约定, 他们不能直接被固件的其他部分直接调用.
35 | 但是可以在软件中生成中断请求, 以触发对中断函数的转移.
36 |
37 | 与异常处理类似, 也可以在中断处理函数中定义`static mut`变量来保持状态*安全*.
38 |
39 | ``` rust,ignore
40 | #[interrupt]
41 | fn TIM2() {
42 | static mut COUNT: u32 = 0;
43 |
44 | // `COUNT` has type `&mut u32` and it's safe to use
45 | *COUNT += 1;
46 | }
47 | ```
48 |
49 | 有关此机制的更加详细的说明, 请参考[异常].
50 |
51 | [异常]: ./exceptions.md
--------------------------------------------------------------------------------
/src/intro/install.md:
--------------------------------------------------------------------------------
1 | # 安装工具
2 |
3 | 此页包含与操作系统无关的工具安装说明:
4 |
5 | ### Rust 工具链
6 |
7 | 按照[https://rustup.rs](https://rustup.rs)的教程安装rustup.
8 |
9 | **NOTE** 确保你有`1.31`或以上版本的编译器. `rustc -V`应该返回一个更新的版本.
10 |
11 | ``` console
12 | $ rustc -V
13 | rustc 1.31.1 (b6c32da9b 2018-12-18)
14 | ```
15 |
16 | 为了带宽和磁盘的使用情况, 默认安装只支持本机编译.
17 | 要想添加ARM Cortex-M的交叉编译支持, 应该选择下面其一的编译目标.
18 | 对于STM32F3DISCOVERY开发板, 应该使用`thumbv7em-none-eabihf`
19 |
20 | Cortex-M0, M0+, M1 (ARMv6-M 架构):
21 | ``` console
22 | $ rustup target add thumbv6m-none-eabi
23 | ```
24 |
25 | Cortex-M3 (ARMv7-M 架构):
26 | ``` console
27 | $ rustup target add thumbv7m-none-eabi
28 | ```
29 |
30 | 没有硬浮点的Cortex-M4 and M7 (ARMv7E-M 架构):
31 | ``` console
32 | $ rustup target add thumbv7em-none-eabi
33 | ```
34 |
35 | 有硬浮点的Cortex-M4F and M7F (ARMv7E-M 架构):
36 | ``` console
37 | $ rustup target add thumbv7em-none-eabihf
38 | ```
39 |
40 | Cortex-M23 (ARMv8-M 架构):
41 | ``` console
42 | $ rustup target add thumbv8m.base-none-eabi
43 | ```
44 |
45 | Cortex-M33 and M35P (ARMv8-M 架构):
46 | ``` console
47 | $ rustup target add thumbv8m.main-none-eabi
48 | ```
49 |
50 | 有硬浮点的Cortex-M33F and M35PF (ARMv8-M 架构):
51 | ``` console
52 | $ rustup target add thumbv8m.main-none-eabihf
53 | ```
54 |
55 | ### `cargo-binutils`
56 |
57 | ``` console
58 | $ cargo install cargo-binutils
59 |
60 | $ rustup component add llvm-tools-preview
61 | ```
62 |
63 | ### `cargo-generate`
64 |
65 | 我们后面用这个来生成项目
66 |
67 | ``` console
68 | $ cargo install cargo-generate
69 | ```
70 |
71 | ### OS相关安装
72 |
73 | 现在跟着这些教程安装:
74 |
75 | - [Linux](install/linux.md)
76 | - [Windows](install/windows.md)
77 | - [macOS](install/macos.md)
78 |
--------------------------------------------------------------------------------
/src/static-guarantees/typestate-programming.md:
--------------------------------------------------------------------------------
1 | # 状态机编程
2 |
3 | [typestate] 的概念描述了将当前的状态编码为一个类型. 尽管这听起来不可思议, 但如果你在 Rust 中使用了 [Builder Pattern] (建造者模式), 你就已经在用状态机编程了!
4 |
5 | [typestates]: https://en.wikipedia.org/wiki/Typestate_analysis
6 | [Builder Pattern]: https://doc.rust-lang.org/1.0.0/style/ownership/builders.html
7 |
8 | ```rust
9 | pub mod foo_module {
10 | #[derive(Debug)]
11 | pub struct Foo {
12 | inner: u32,
13 | }
14 |
15 | pub struct FooBuilder {
16 | a: u32,
17 | b: u32,
18 | }
19 |
20 | impl FooBuilder {
21 | pub fn new(starter: u32) -> Self {
22 | Self {
23 | a: starter,
24 | b: starter,
25 | }
26 | }
27 |
28 | pub fn double_a(self) -> Self {
29 | Self {
30 | a: self.a * 2,
31 | b: self.b,
32 | }
33 | }
34 |
35 | pub fn into_foo(self) -> Foo {
36 | Foo {
37 | inner: self.a + self.b,
38 | }
39 | }
40 | }
41 | }
42 |
43 | fn main() {
44 | let x = foo_module::FooBuilder::new(10)
45 | .double_a()
46 | .into_foo();
47 |
48 | println!("{:#?}", x);
49 | }
50 | ```
51 |
52 | 在这个例子中, 没有一个直接创建 `Foo` 对象的方法. 我们必须先创建一个 `FooBuilder`, 然后正确的初始化它才能得到我们想要的 `Foo` 对象.
53 |
54 | 这个简单的小例子编码了两种状态:
55 |
56 | - `FooBuilder` 代表了一个"未配置", "在配置中"的状态
57 | - `Foo` 代表了一个"配置完成", "准备使用"的状态
58 |
59 | ## 强类型
60 |
61 | 因为 Rust 有一个 [Strong Type System] (强类型系统), 所以没有什么花里胡哨的办法去直接创建一个 `Foo` 的实例, 或者不用 `into_foo()` 方法把一个 `FooBuilder` 转变为 `Foo`. 另外, 调用 `into_foo()` 方法会消费掉原来的 `FooBuilder`, 意思是你没法再用它去创建一个新实例.
62 |
63 | [Strong Type System]: https://en.wikipedia.org/wiki/Strong_and_weak_typing
64 |
65 | 强类型系统让我们能把我们的系统状态表示为类型, 并且可以用方法来把由一种状态到另一种状态所需的步骤描述出来. 通过创建一个 `FooBuilder`, 并把它转变为 `Foo` 对象, 我们完成了一个基本的状态机.
66 |
--------------------------------------------------------------------------------
/src/peripherals/index.md:
--------------------------------------------------------------------------------
1 | # 外设
2 |
3 | ## 什么是外设?
4 |
5 | 很多微控制器不仅仅只有 CPU, RAM, 或 FLASH闪存 - 他们是用与微控制器之外的系统进行交互, 即通过传感器, 马达, 或者像是显示器或键盘这样的人机界面直接的或简洁的与周围交互. 这些部件就叫做外设.
6 |
7 | 这些外设很有用, 因为他们能够让开发者把要干的事丢给它们, 这样就不用让软件来处理所有的事情.
8 | 就像是桌面开发者把图像处理的部分丢给显卡一样, 嵌入式开发者能够把一些任务放在外围设备上, 这样就可以让CPU有时间去干更重要的事, 或者什么都不干来省电.
9 |
10 | 如果你看一下上个世纪70年代到80年代的旧型号家用电脑(上个世代的电脑和微控制器没差多少), 你可以发现:
11 |
12 | * 一个处理器
13 | * 一个 RAM 芯片
14 | * 一块 ROM 芯片
15 | * 一个 I/O 控制器
16 |
17 | RAM, ROM芯片还有I/O控制器(系统中的外设)会通过一系列并行接口,又叫做'总线'参与处理器的工作. 总线带有地址信息, 用来在总线上选择处理器想与那个外设通信. 在我们的嵌入式处理器中, 也是同样的规则 - 只不过是它们都被集成在了一块硅片上.
18 |
19 | ## 线性实际内存空间
20 |
21 | 在微控制器, 向某些地址写入数据, 如 `0x4000_0000` 或 `0x0000_0000` , 也会是一个完全有效的操作.
22 |
23 | 在一个桌面系统上, 对内存的读取写入操作要经过 MMU, Memory Management Unit. 它有两个主要功能: 限制对内存的访问(防止一个进程读取或者修改另一个程序的内存); 并且将物理内存重新映射到虚拟内存中. 微控制器一般没有 MMU , 作为代替它们在软件中使用真实的内存地址.
24 |
25 | 尽管32位微控制器有一个实际的线性内存地址, 起始 `0x0000_0000` 至 `0xFFFF_FFFF`, 它们通常只使用该范围内的几百KB作为实际内存. 留下了很多内存空间. 在前面的章节中, 我们说过 RAM 位于地址 `0x2000_0000`. 如果我们的 RAM 有64KB 大小(最大地址0xFFFF), 那实际内存空间就是从 `0x2000_0000` 到 `0x2000_FFFF`. 当我们向 `0x2000_1234` 写入时, 实际上内部发生的是一些逻辑检测到我们用到地址的上半部分(这里是 0x2000 ), 然后激活RAM, 以便寻找下半部分地址(这里是 0x1234 ). 在 Cortex-M 上, 我们将ROM映射到 `0x0000_0000` 上, 假设你有 512KB ROM, 那就是从 `0x0000_0000` 到 `0x0007_FFFF`. 微控制器的设计人员没有忽略这两部分之间的剩余地址, 而是在他们上面映射了外设的接口. 最后看起来像这样:
26 |
27 | 
28 |
29 | [Nordic nRF52832 Datasheet (pdf)]
30 |
31 | ## 内存映射外设
32 |
33 | 乍一看, 与外设的交互十分简单, 将正确的数据写入到正确的内存地址上. 例如, 通过串口发送32位的数据就和直接向一个地址写入32位数据一样简单, 串口就会自动读取然后发送数据.
34 |
35 | 配置这些外设的工作都很相似. 不是调用用于配置它们的函数, 而是直接公开一块内存用于硬件API. 将 `0x8000_0000` 写入配置寄存器, SPI端口便会以 8Mb/s 的速度发送数据. 将 `0x2000_0000` 写入相同地址, SPI就会以 125kb/s 的速度发送数据. 这些配置寄存器看起来和这个一样:
36 |
37 | 
38 |
39 | [Nordic nRF52832 Datasheet (pdf)]
40 |
41 | 无论使用哪种语言, C, 汇编还是Rust, 都是像这样直接与硬件交互.
42 |
43 | [Nordic nRF52832 Datasheet (pdf)]: http://infocenter.nordicsemi.com/pdf/nRF52832_PS_v1.1.pdf
44 |
--------------------------------------------------------------------------------
/src/intro/install/verify.md:
--------------------------------------------------------------------------------
1 | # 验证安装
2 |
3 | 在这一部分我们检查需要的工具/驱动是否被正确安装.
4 |
5 | 把Discovery开发板连接到电脑. Discovery有两个USB口, 使用在板子中间标着"USB ST-LINK"的口.
6 |
7 | 也检查ST-LINK的接口是否被污染. 看如下图片, ST-LINK接口被红线圈中.
8 |
9 |
10 |
11 |
12 |
13 | 现在运行如下命令:
14 |
15 | ``` console
16 | $ openocd -f interface/stlink.cfg -f target/stm32f3x.cfg
17 | ```
18 |
19 | 你应该会得到如下输出并且命令行被阻塞:
20 |
21 | ``` text
22 | Open On-Chip Debugger 0.10.0
23 | Licensed under GNU GPL v2
24 | For bug reports, read
25 | http://openocd.org/doc/doxygen/bugs.html
26 | Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
27 | adapter speed: 1000 kHz
28 | adapter_nsrst_delay: 100
29 | Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
30 | none separate
31 | Info : Unable to match requested speed 1000 kHz, using 950 kHz
32 | Info : Unable to match requested speed 1000 kHz, using 950 kHz
33 | Info : clock speed 950 kHz
34 | Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
35 | Info : using stlink api v2
36 | Info : Target voltage: 2.919881
37 | Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints
38 | ```
39 |
40 | 内容并不会完全一样, 但你应该会看到有关断点和观察点的最后一行.
41 | 如果没什么问题那就关掉OpenOCD然后到[下一部分].
42 |
43 | [下一部分]: ../../start/index.md
44 |
45 | 如果你没看到 "断点" 这一行, 那试试如下命令.
46 |
47 | ``` console
48 | $ openocd -f interface/stlink-v2.cfg -f target/stm32f3x.cfg
49 | ```
50 |
51 | ``` console
52 | $ openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg
53 | ```
54 |
55 | 如果有一条命令成功了, 这意味这你手上的是个旧版本Discovery. 那不会有什么问题, 除了内存设置会在后面有些不同, 到[下一部分].
56 |
57 | 如果这些命令都用不了, 那就试试使用root权限(像是`sudo openocd ...`). 如果命令能够执行, 那么检查一下[udev规则]是否正确.
58 |
59 | [udev规则]: linux.md#udev规则
60 |
61 | 如果到这, 你的OpenOCD还是不能用, 那就来发个[issue]然后我们来帮你!
62 |
63 | [issue]: https://github.com/rust-embedded/book/issues
64 |
--------------------------------------------------------------------------------
/src/intro/tooling.md:
--------------------------------------------------------------------------------
1 | # 工具
2 |
3 | 处理微控制器涉及到使用集中不同的工具, 因为我们要处理一个与你电脑架构不同的架构, 我们必须要在远程设备上来运行和调试程序.
4 |
5 | 我们会使用下面列出的工具. 没指定最低版本时, 按理说任何最新版本都能用, 但是我们也列出了经过测试的版本.
6 |
7 | - Rust 1.31, 1.31-beta, 或带有 ARM Cortex-M 编译器的更新的工具链
8 | - [`cargo-binutils`](https://github.com/rust-embedded/cargo-binutils) ~0.1.4
9 | - [`qemu-system-arm`](https://www.qemu.org/). 测试版本: 3.0.0
10 | - OpenOCD >=0.8. 测试版本: v0.9.0 and v0.10.0
11 | - GDB with ARM support. 7.12或更高版本. 测试版本: 7.10, 7.11, 7.12 and 8.1
12 | - [`cargo-generate`](https://github.com/ashleygwilliams/cargo-generate) 或 `git`.
13 | 这个工具可选但是会让你学习本书更加轻松.
14 |
15 | 下面来讲为什么我们需要这些工具. 安装说明会在下一页提及.
16 |
17 | ## `cargo-generate` 或 `git`
18 |
19 | 裸金属应用是不标准(`no_std`)的Rust程序, 需要对链接过程做出一些调整, 以使程序的内存布局正确. 这需要一些额外的文件(像是链接器脚本)和设置(像是连接器参数).
20 | 我们已经把这些打包成了一个模板, 这样你就只需要填写确实的信息就行(就想项目名称和目标硬件型号).
21 |
22 | 我们的模板与`cargo-generate`兼容, `cargo-generate`是Cargo的一个子命令, 用来从模板创建新的Cargo项目. 你也可以使用`git`, `curl`, `wget`或浏览器来下载模板.
23 |
24 | ## `cargo-binutils`
25 |
26 | `cargo-binutils`是Cargo的一系列子命令, 用来更轻松的配合Rust工具链使用LLVM工具. 这些工具包含LLVM版本的`objdump`, `nm`和`size`, 用来检查二进制产物.
27 |
28 | 与GNU binmutils相比, 使用这些工具的优势在于, (a) 可以无视系统一键安装LLVM工具(`rustup component add llvm-tools-preview`), (b) 像`objdump`这样的工具支持所有`rustc`支持的所有架构, 从 ARM 到 x86_64 应为他们都使用了相同的LLVM后端.
29 |
30 | ## `qemu-system-arm`
31 |
32 | QEMU是个模拟器. 在本书中, 我们使用能够模拟各种ARM系统的变体.
33 | 我们使用QEMU来在电脑上运行嵌入式程序.
34 | 多亏这个, 你能在没有硬件的情况下学习本书.
35 |
36 | ## GDB
37 |
38 | 调试器是嵌入式开发中非常重要的一个组件, 因为你并不总是有足够的空间去把气质打到控制台上.
39 | 某些情况下, 你的硬件上甚至都没有LED可以闪(呜呜呜).
40 |
41 | 通常情况下, 涉及到调试的时候, LLDB和GDB差不多, 但是我们还没找到一个与GDB的`load`命令相同功能的LLDB指令, 这个命令把程序加载到硬件上, 所以我们建议你使用GDB.
42 |
43 | ## OpenOCD
44 |
45 | GDB现在还不能直接通过ST-Link调试器和你的STM32F3DISCOVERY开发板沟通.
46 | 他需要一个翻译器, Open On-Chip Debugger 缩写 OpenOCD 就是这个翻译器.
47 | OpenOCD是在你电脑上运行的, 可以在GDB基于TCP/IP的远程调试协议和ST-LINK基于USB的协议之间进行转换.
48 |
49 | OpenOCD还执行其他工作, 作为翻译的一部分, 用于调试STM32F3DISCOVERY开发板上的ARM Cortex-M处理器.
50 |
51 | - 他知道如何与ARM CoreSight调试外围设备使用的内存映射寄存器沟通.正是这些CoreSight寄存器允许:
52 | - 断电/观察点操作
53 | - 读写CPU寄存器
54 | - 检测CPU何时银调试而暂停
55 | - 在调试结束后继续CPU执行
56 | - 更多.
57 | - 它还知道如何擦除和覆写mcu的flash
58 |
--------------------------------------------------------------------------------
/src/portability/index.md:
--------------------------------------------------------------------------------
1 | # 可移植性
2 |
3 | 在嵌入式环境中,可移植性是一个非常重要的指标:每家厂商或一家厂商中的不同系列都提供不同的外设与功能,并且交互的方法也不同。
4 |
5 | 一个普遍的方法是通过硬件抽象层,或者说是 **HAL** 解决。
6 |
7 | > 硬件抽象是软件中的一组程序,他们模拟某些平台特定的操作细节,来让程序可以直接访问硬件资源。
8 | >
9 | > 他们提供对于硬件的标准 OS 接口可以让程序员来写出不依赖于设备,高性能的程序。
10 | >
11 | > *Wikipedia: [Hardware Abstraction Layer]*
12 |
13 | [Hardware Abstraction Layer]: https://en.wikipedia.org/wiki/Hardware_abstraction
14 |
15 | 嵌入式系统在这方面有些特殊,因为我们通常没有操作系统,也不能安装软件,固件是作为一个整体来编译的,有着许多其他限制。
16 | 因此,尽管 Wikipedia 所定义的传统方法可能有用,但是并不是一个最有效的方法来确保可移植性。
17 |
18 | 我们怎么在 Rust 中做?来看看 **embedded-hal**...
19 |
20 | ## 什么是 **embedded-hal**?
21 |
22 | 简而言之,它是一组 trait ,它定义了 **HAL 的实现**,**驱动**还有**应用(或固件)**。这些约定包括能力(例如,如果你为某些类型实现了 trait ,那 **HAL** 就为它提供了一系列功能)和方法(例如,如果你构建了一个实现 trait 的类型,那就可以使用他的方法)。
23 |
24 | 典型的分型可能如下所示:
25 |
26 | 
27 |
28 | **embedded-hal** 中定义的一些 trait 如下:
29 |
30 | * GPIO (输入输出引脚)
31 | * 串行通信
32 | * I2C
33 | * SPI
34 | * 计数器、定时器
35 | * 数模转换
36 |
37 | 设计 **embedded-hal** trait 与 crate 并使用他们的原因是为了控制复杂性。
38 | 考虑一下,如果某一个应用必须实现硬件外设的使用方法,并且可能使用其他驱动,那么保持可移植性是很难的。
39 | 用数学一点的方法来表示,如果 **M** 是外设HAL实现的数量, **N** 是驱动的数量,那么如果我们为每一个应用重新造轮子,那么最终会得到 **M * N** 种实现,但是使用 **embedded-hal** trait 提供的 *API* 会把复杂度降低到 **M + N**。当然也有其他好处,例如这些定义良好、反复测试过的易用 API 。
40 |
41 | ## embedded-hal 的应用场合
42 |
43 | 如前所述, HAL 的应用场合如下:
44 |
45 | ### HAL 实现
46 |
47 | HAL 实现提供了硬件与 HAL traits 用户的交互。典型的实现包括三个部分:
48 |
49 | * 一个或多个硬件具体类型
50 | * 提供多个配置(速度,操作方式,引脚等)来创建或初始化一个类型的函数
51 | * 一个或多个 **embedded-hal** 中 `trait` 的 `impl`
52 |
53 | 这样的一个 **HAL 实现**有如下几种实现形式:
54 |
55 | * 通过低级硬件全是先,如寄存器
56 | * 通过操作系统,如在linux下通过 `sysfs`
57 | * 通过适配器,如单元测试中的 mock
58 | * 通过硬件适配器驱动,如 I2C 多路复用 或 GPIO 拓展
59 |
60 | ### 驱动
61 |
62 | 一个驱动为一个内部或外部的连接到实现了 embedded-hal traits 的组件实现一系列功能。
63 | 典型的例子包括各种传感器(温度,磁场,加速器,光),显示设备( LED , LCD 显示器)还有执行器(马达,发送器)。
64 |
65 | 一个驱动需要一个实现了 embedded-hal `trait` 的类型来初始化,这通过类型绑定来保证,并且为它自身提供一系列方法来让使用者与被驱动的设备来交互。
66 |
67 | ### 应用
68 |
69 | 应用将各个部分组合在一起来实现所需功能。在不同系统间移植时需要花费大量精力,因为应用程序需要通过 HAL 来正确的初试话实际硬件,并且不同的硬件之间初始化方法也有不同。此外,用户的决定通常也有着很大的影响,因为硬件可以物理的连接到不同位置,硬件有时候需要外部硬件才能正确配置,或者在使用内部设备时也面临着不同的选择(例如,多个定时器有着不同的功能并且可能有冲突)
70 |
--------------------------------------------------------------------------------
/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | - [引言](./intro/index.md)
4 | - [硬件](./intro/hardware.md)
5 | - [`no_std`](./intro/no-std.md)
6 | - [工具](./intro/tooling.md)
7 | - [安装](./intro/install.md)
8 | - [Linux](./intro/install/linux.md)
9 | - [MacOS](./intro/install/macos.md)
10 | - [Windows](./intro/install/windows.md)
11 | - [验证安装](./intro/install/verify.md)
12 | - [开始](./start/index.md)
13 | - [QEMU](./start/qemu.md)
14 | - [硬件](./start/harware.md)
15 | - [内存映射寄存器](./start/registers.md)
16 | - [Semihosting](./start/semihosting)
17 | - [Panicking](./start/panicking.md)
18 | - [异常](./start/exception.md)
19 | - [中断](./start/interrupts.md)
20 | - [IO](./start/io.md)
21 | - [外设](./peripherals/index.md)
22 | - [Rust的第一次尝试](./peripherals/a-first-attempt.md)
23 | - [引用检查](./peripherals/borrowck.md)
24 | - [单例](./peripherals/singletons.md)
25 | - [静态检查](./static-guarantees/index.md)
26 | - [状态机编程](./static-guarantees/typestate-programming.md)
27 | - [外设状态机](./static-guarantees/state-machines.md)
28 | - [设计合同](./static-guarantees/design-contracts.md)
29 | - [零成本抽象](./static-guarantees/zero-cost-abstractions.md)
30 | - [可移植性](./portability/index.md)
31 | - [并发](./concurrency/index.md)
32 | - [容器](./collections/index.md)
33 | - [设计模式](./design-patterns/index.md)
34 | - [硬件抽象层](./design-patterns/hal/index.md)
35 | - [Checklist](./design-patterns/hal/checklist.md)
36 | - [Naming](./design-patterns/hal/naming.md)
37 | - [Interoperability](./design-patterns/hal/interoperability.md)
38 | - [Predictability](./design-patterns/hal/predictability.md)
39 | - [GPIO](./design-patterns/hal/gpio.md)
40 | - [Tips for embedded C developers](./c-tips/index.md)
41 |
42 | - [Interoperability](./interoperability/index.md)
43 | - [A little C with your Rust](./interoperability/c-with-rust.md)
44 | - [A little Rust with your C](./interoperability/rust-with-c.md)
45 | - [Unsorted topics](./unsorted/index.md)
46 | - [Optimizations: The speed size tradeoff](./unsorted/speed-vs-size.md)
47 | - [Performing Math Functionality](./unsorted/math.md)
48 |
49 | ---
50 |
51 | [Appendix A: Glossary](./appendix/glossary.md)
--------------------------------------------------------------------------------
/src/intro/no-std.md:
--------------------------------------------------------------------------------
1 | # 一个 `no_std` 的Rust环境
2 |
3 | 嵌入式编程一词用于很多不同种类的涵义. 从只有几KB大小RAM与ROM的8位MCU, 到像是树莓派这样有32/64位四核Cortex-A53 cpu与1GB内存的设备.
4 | 编写代码时, 对于不同设备会有不同的限制.
5 |
6 | 有两种通用的嵌入式变成分类:
7 |
8 | ## 托管环境
9 |
10 | 这种环境与正常的PC环境相似. 这意味这你能够使用系统级接口, 类似POSIX这样的能提供给你与系统交互的原语, 像是文件系统, 网络, 内存管理, 线程等等.
11 | 你可能还会有些sysroot和RAM/ROM的限制, 可能还会有些特殊的硬件或I/O. 简而言之, 这类似在一台特殊用途的PC环境上编程.
12 |
13 | ## 裸金属
14 |
15 | 在一个裸金属环境中, 在你的程序开始之前不会有任何代码被加载.
16 | 没有OS提供我们没法使用标准库.
17 | 相反, 程序和它使用的库(Crates)可以只使用硬件(裸金属)来运行.
18 | 为了防止rust使用标准库, 我们使用`no_std`.
19 | 标准库中与平台无关的部分可以通过[libcore](https://doc.rust-lang.org/core/)获取.
20 | libcore中也排除了在嵌入式环境中并不总是理想的东西.
21 | 这其中之一就是用于动态内存分配的内存分配器.
22 | 如果你需要这个或是其他功能, 会有库(Crates)提供.
23 |
24 | ### libstd运行时
25 |
26 | 像前面说的, 使用[libstd](https://doc.rust-lang.org/std/)需要系统支持, 但是这并不只是因为[libstd](https://doc.rust-lang.org/std/)至提供了访问OS的通用的抽象的方法, 而且它还提供了一个运行时.
27 | 这个运行时, 除了其他事情外, 还负责设置对战一处保护, 处理命令行参数还有在调用程序的main函数之前创建主线程. 这个运行时在`no_std`环境中不可用.
28 |
29 | ## 总结
30 |
31 | `#![no_std]`是一个声明这个crate不会连接到std-crate二十core-crate的crate级别的属性.
32 | [libcore](https://doc.rust-lang.org/core/)是std-crate的一个与平台无关的子集, 对程序将要运行在的系统上没有任何假设(需求).
33 | 因此, 它为语言原语,像是float, string和slices等提供api, 和开放的处理器特性, 像是原子操作与SIMD指令.
34 | 然而他缺少任何设计平台集成的API.
35 | 由于这些属性, no\_std与[libcore](https://doc.rust-lang.org/core/)写成的代码能不能够用于任何类型的引导(stage 0)像是加载程序, 固件还有内核.
36 |
37 | ### 概述
38 |
39 | | feature | no\_std | std |
40 | |-----------------------------------------------------------|--------|-----|
41 | | 堆 (动态内存) | * | ✓ |
42 | | 集合 (Vec, HashMap, etc) | ** | ✓ |
43 | | 堆栈溢出保护 | ✘ | ✓ |
44 | | 初始化函数 | ✘ | ✓ |
45 | | libstd 可用 | ✘ | ✓ |
46 | | libcore 可用 | ✓ | ✓ |
47 | | 编写 固件, 内核, 引导加载器 | ✓ | ✘ |
48 |
49 | \* 只有当你使用 `alloc` crate并且选择一个合适的分配器, 像是[alloc-cortex-m]才可用.
50 |
51 | \** 只有当你使用 `collections` crate 并且配置一个全局默认的分配器才可用
52 |
53 | [alloc-cortex-m]: https://github.com/rust-embedded/alloc-cortex-m
54 |
55 | ## See Also
56 |
57 | * [RFC-1184](https://github.com/rust-lang/rfcs/blob/master/text/1184-stabilize-no_std.md)
58 |
--------------------------------------------------------------------------------
/src/intro/install/linux.md:
--------------------------------------------------------------------------------
1 | # Linux
2 |
3 | 如下是对几种发行版的安装命令
4 |
5 | ## 包
6 |
7 | - Ubuntu 18.04 及以上 / Debian stretch 及以上
8 |
9 | > **NOTE** `gdb-mutliarch` 是你debug你的ARM Cortex-M程序的GDB命令
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ``` console
22 | sudo apt install gdb-multiarch openocd qemu-system-arm
23 | ```
24 |
25 | - Ubuntu 14.04 , 16.04
26 |
27 | > **NOTE** `arm-none-eabi-gdb` 是你debug你的ARM Cortex-M程序的GDB命令
28 |
29 |
30 |
31 |
32 |
33 |
34 | ``` console
35 | sudo apt install gdb-arm-none-eabi openocd qemu-system-arm
36 | ```
37 |
38 | - Fedora 27 及以上
39 |
40 | > **NOTE** `arm-none-eabi-gdb` 是你debug你的ARM Cortex-M程序的GDB命令
41 |
42 |
43 |
44 |
45 |
46 |
47 | ``` console
48 | sudo dnf install arm-none-eabi-gdb openocd qemu-system-arm
49 | ```
50 |
51 | - Arch Linux
52 |
53 | > **NOTE** `arm-none-eabi-gdb` 是你debug你的ARM Cortex-M程序的GDB命令
54 |
55 | ``` console
56 | sudo pacman -S arm-none-eabi-gdb qemu-arch-extra openocd
57 | ```
58 |
59 | ## udev规则
60 |
61 | 这条规则让你可以使用OpenOCD而不要root权限.
62 |
63 | 在`/etc/udev/rules.d/70-st-link.rules`创建文件, 并写入以下内容.
64 |
65 | ``` text
66 | # STM32F3DISCOVERY rev A/B - ST-LINK/V2
67 | ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", TAG+="uaccess"
68 |
69 | # STM32F3DISCOVERY rev C+ - ST-LINK/V2-1
70 | ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", TAG+="uaccess"
71 | ```
72 |
73 | 然后重新加载udev规则
74 |
75 | ``` console
76 | sudo udevadm control --reload-rules
77 | ```
78 |
79 | 如果你把板子连接到了电脑, 重新连接.
80 |
81 | 使用如下命令检查权限:
82 |
83 | ``` console
84 | lsusb
85 | ```
86 |
87 | 你应该看到如下内容:
88 |
89 | ```text
90 | (..)
91 | Bus 001 Device 018: ID 0483:374b STMicroelectronics ST-LINK/V2.1
92 | (..)
93 | ```
94 |
95 | 记一下总线设备号. 用这些数字来创建如下目录`/dev/bus/usb//`. 然后链接目录:
96 |
97 | ``` console
98 | ls -l /dev/bus/usb/001/018
99 | ```
100 |
101 | ```text
102 | crw-------+ 1 root root 189, 17 Sep 13 12:34 /dev/bus/usb/001/018
103 | ```
104 |
105 | ```console
106 | getfacl /dev/bus/usb/001/018 | grep user
107 | ```
108 |
109 | ```text
110 | user::rw-
111 | user:you:rw-
112 | ```
113 |
114 | 附加在权限后面的`+`表示存在扩展权限。`getfacl`命令告诉用户`您`可以使用这个设备。
115 |
116 | 现在阅读[下一部分]
117 |
118 | [下一部分]: verify.md
119 |
--------------------------------------------------------------------------------
/src/start/panicking.md:
--------------------------------------------------------------------------------
1 | # 恐慌
2 |
3 | Panicking是Rust语言的核心一部分.
4 | 像是索引一样的内置操作会在运行时检查安全性.
5 | 当尝试超出索引范围时, 结果就会导致panic.
6 |
7 | 在标准库之中, 恐慌有一个定义的行为: 除非用户选择在出现恐慌时结束程序, 否则它将展开出现恐慌行为的线程的堆栈.
8 |
9 | 然而, 在没有使用标准库的程序中, 恐慌行为没有定义.
10 | 可以使用`#[panic_handler]`来指定恐慌行为.
11 | 这个函数在整个程序的语法树中只能出现*一次*, 并且必须有如下标志: `fn(&PanicInfo) -> !`, [`PanicInfo`]是一个包含了恐慌位置信息的结构体.
12 |
13 | [`PanicInfo`]: https://doc.rust-lang.org/core/panic/struct.PanicInfo.html
14 |
15 | 鉴于嵌入式系统的范围从面型用户到所以安全至关重要(不崩溃), 所以没有任何一种适用于全部情况的恐慌行为, 但是有很多经常使用的.
16 | 这些库中已经定义了`#[panic_handler]`函数. 这有几个例子:
17 |
18 | - [`panic-abort`]. 恐慌使指令停止运行.
19 | - [`panic-halt`]. 恐慌使当前程序或线程进入一个无限循环.
20 | - [`panic-itm`]. 使用ITM(Cortex-M的一种外设)记录恐慌消息.
21 | - [`panic-semihosting`]. 使用semihosting技术将恐慌信息输出到主机上.
22 |
23 |
24 | [`panic-abort`]: https://crates.io/crates/panic-abort
25 | [`panic-halt`]: https://crates.io/crates/panic-halt
26 | [`panic-itm`]: https://crates.io/crates/panic-itm
27 | [`panic-semihosting`]: https://crates.io/crates/panic-semihosting
28 |
29 | 你可以在crates.io上使用关键词[`panic-handler`]搜索到更多库.
30 |
31 | [`panic-handler`]: https://crates.io/keywords/panic-handler
32 |
33 | 程序可以通过简单的链接到其中一个库来选择一个恐慌行为.
34 | 恐慌行为在应用程序的源代码中表示为一行代码这一事实不仅可用作文档, 而且还可以根据编译配置文件用于更改恐慌行为.
35 | 例如:
36 |
37 | ``` rust,ignore
38 | #![no_main]
39 | #![no_std]
40 |
41 | // dev profile: easier to debug panics; can put a breakpoint on `rust_begin_unwind`
42 | #[cfg(debug_assertions)]
43 | use panic_halt as _;
44 |
45 | // release profile: minimize the binary size of the application
46 | #[cfg(not(debug_assertions))]
47 | use panic_abort as _;
48 |
49 | // ..
50 | ```
51 |
52 | 在这个例子中我们选择在开发时(`cargo build`)我们选择`panic-halt`, 但在发布时(`cargo build --release`)我们选择`panic-abort`.
53 |
54 | > `use panic_abort as _;`使用`use`语句来确保在最终二进制产物中`panic_abort`被包含进去, 同时也让编译器知道我们不会使用其中的任何内容.
55 | > 没有`as _`的话, 编译器会给我们一个Warn来告诉我们有个没有使用的导入库.
56 | > 又是你会看见`extern crate panic_abort`, 这是一个在2018版本之前的旧版本的写法, 现在仅仅应用于"sysroot"库(那些随着Rust一起发布的库), 像是 `proc_macro`, `alloc`, `std`, 还有 `test`.
57 |
58 | ## 一个例子
59 |
60 | 这有一个试图越界数组的例子.
61 | 最终结果会导致恐慌.
62 |
63 | ```rust,ignore
64 | #![no_main]
65 | #![no_std]
66 |
67 | use panic_semihosting as _;
68 |
69 | use cortex_m_rt::entry;
70 |
71 | #[entry]
72 | fn main() -> ! {
73 | let xs = [0, 1, 2];
74 | let i = xs.len() + 1;
75 | let _y = xs[i]; // out of bounds access
76 |
77 | loop {}
78 | }
79 | ```
80 |
81 | 这个例子选择使用semihosting技术输出信息到主机的`panic-semihosting`.
82 |
83 | ``` console
84 | $ cargo run
85 | Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
86 | panicked at 'index out of bounds: the len is 3 but the index is 4', src/main.rs:12:13
87 | ```
88 |
89 | 你可以试着把恐慌行为改成`panic-halt`来确认一下还会不会有信息输出.
90 |
--------------------------------------------------------------------------------
/src/start/semihosting.md:
--------------------------------------------------------------------------------
1 | # Semihosting
2 |
3 | Semihosting是一种让嵌入式设备的能在宿主机上IO的机制,主要用来在主机控制台上log.
4 | Semihosting需要一个调试会话,剩下什么都不要(甚至不需要额外的线!),所以它很方便.
5 | 下行速度非常长非常慢:取决于硬件调试器(例如ST-LINK),每个写操作都可能要几毫秒.
6 |
7 | [`cortex-m-semihosting`]库提供一个在Cortex-M的设备上使用semihosting的API.
8 | 下面的程序是"Hello, world!"的semihosting版本:
9 |
10 | [`cortex-m-semihosting`]: https://crates.io/crates/cortex-m-semihosting
11 |
12 | ```rust,ignore
13 | #![no_main]
14 | #![no_std]
15 |
16 | use panic_halt as _;
17 |
18 | use cortex_m_rt::entry;
19 | use cortex_m_semihosting::hprintln;
20 |
21 | #[entry]
22 | fn main() -> ! {
23 | hprintln!("Hello, world!").unwrap();
24 |
25 | loop {}
26 | }
27 | ```
28 |
29 | 如果你在这个硬件上运行程序,你会在OpenOCD日志上看到"Hello, world!".
30 |
31 | ``` console
32 | $ openocd
33 | (..)
34 | Hello, world!
35 | (..)
36 | ```
37 |
38 | 你首先要从GDB中启动semihosting:
39 |
40 | ``` console
41 | (gdb) monitor arm semihosting enable
42 | semihosting is enabled
43 | ```
44 |
45 | QEMU理解semihosting操作,所以以前的程序可以在`qemu-system-arm`上运行,而不需要启动一个调试会话.注意你需要传递`-semihosting-config`参数来让QEMU启用semihosting支持;这些参数已经包含在`.cargo/config`文件中.
46 |
47 | ``` console
48 | $ # this program will block the terminal
49 | $ cargo run
50 | Running `qemu-system-arm (..)
51 | Hello, world!
52 | ```
53 |
54 | 还有一个`exit`的semihosting操作, 用来结束QEMU进程.
55 | 重要提示: 请**不要**在硬件上使用`debug::exit`, 此函数会破坏OpenOCD会话, 只有重启才能继续调试.
56 |
57 | ```rust,ignore
58 | #![no_main]
59 | #![no_std]
60 |
61 | use panic_halt as _;
62 |
63 | use cortex_m_rt::entry;
64 | use cortex_m_semihosting::debug;
65 |
66 | #[entry]
67 | fn main() -> ! {
68 | let roses = "blue";
69 |
70 | if roses == "red" {
71 | debug::exit(debug::EXIT_SUCCESS);
72 | } else {
73 | debug::exit(debug::EXIT_FAILURE);
74 | }
75 |
76 | loop {}
77 | }
78 | ```
79 |
80 | ``` console
81 | $ cargo run
82 | Running `qemu-system-arm (..)
83 |
84 | $ echo $?
85 | 1
86 | ```
87 |
88 | 最后一个提示: 你可以将panic行为设置为`exit(EXIT_FAILURE)`. 这可以让你写的`no_std`测试在QEMU上运行.
89 |
90 | 为了方便, `panic-semihosting`有一个"exit"的feature, 启用它可以在`exit(EXIT_FAILURE)`后把panic信息输出到主机stderr上.
91 |
92 | ```rust,ignore
93 | #![no_main]
94 | #![no_std]
95 |
96 | use panic_semihosting as _; // features = ["exit"]
97 |
98 | use cortex_m_rt::entry;
99 | use cortex_m_semihosting::debug;
100 |
101 | #[entry]
102 | fn main() -> ! {
103 | let roses = "blue";
104 |
105 | assert_eq!(roses, "red");
106 |
107 | loop {}
108 | }
109 | ```
110 |
111 | ``` console
112 | $ cargo run
113 | Running `qemu-system-arm (..)
114 | panicked at 'assertion failed: `(left == right)`
115 | left: `"blue"`,
116 | right: `"red"`', examples/hello.rs:15:5
117 |
118 | $ echo $?
119 | 1
120 | ```
121 |
122 | **注意**: 编辑你的`Cargo.toml`以启用`panic-semihosting`的该特性:
123 |
124 | ``` toml
125 | panic-semihosting = { version = "VERSION", features = ["exit"] }
126 | ```
127 |
128 | 有关feature的更多信息请参考[`specifying dependencies`]
129 |
130 | [`specifying dependencies`]:
131 | https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
--------------------------------------------------------------------------------
/src/static-guarantees/state-machines.md:
--------------------------------------------------------------------------------
1 | # 外设状态机
2 |
3 | MCU 的外设可以被看作是一种状态机. 例如, 简化的 [GPIO Pin] (GPIO 引脚) 的配置可以表示为如下状态树:
4 |
5 | [GPIO pin]: https://en.wikipedia.org/wiki/General-purpose_input/output
6 |
7 | * Disabled (禁用)
8 | * Enabled (启用)
9 | * Configured as Output (输出)
10 | * Output: High (高电平输出)
11 | * Output: Low (低电平输出)
12 | * Configured as Input (输入)
13 | * Input: High Resistance (高阻)
14 | * Input: Pulled Low (拉低)
15 | * Input: Pulled High (拉高)
16 |
17 | 如果外设开始是 `Disabled` 状态, 为了把它转换到 `Input: High Resistance` 模式, 我们要这么做:
18 |
19 | 1. Disabled
20 | 2. Enabled
21 | 3. Configured as Input
22 | 4. Input: High Resistance
23 |
24 | 如果我们想从 `Input: High Resistance` 状态到 `Input: Pulled Low` 状态, 我们需要:
25 |
26 | 1. Input: High Resistance
27 | 2. Input: Pulled Low
28 |
29 | 同样的, 如果我们想把一个 GPIO 从 `Input: Pulled Low` 设置到 `Output: High`, 我们需要:
30 |
31 | 1. Input: Pulled Low
32 | 2. Configured as Input
33 | 3. Configured as Output
34 | 4. Output: High
35 |
36 | ## 硬件表示
37 |
38 | 通常, 上面列出来的状态是将给定的值写入到 GPIO 外设的寄存器上来实现的. 让我们来定义一个虚构的 GPIO 寄存器来说明一下:
39 |
40 | | Name | Bit Number(s) | Value | Meaning | Notes |
41 | | ---: | ------------: | ----: | ------: | ----: |
42 | | enable | 0 | 0 | disabled | Disables the GPIO |
43 | | | | 1 | enabled | Enables the GPIO |
44 | | direction | 1 | 0 | input | Sets the direction to Input |
45 | | | | 1 | output | Sets the direction to Output |
46 | | input_mode | 2..3 | 00 | hi-z | Sets the input as high resistance |
47 | | | | 01 | pull-low | Input pin is pulled low |
48 | | | | 10 | pull-high | Input pin is pulled high |
49 | | | | 11 | n/a | Invalid state. Do not set |
50 | | output_mode | 4 | 0 | set-low | Output pin is driven low |
51 | | | | 1 | set-high | Output pin is driven high |
52 | | input_status | 5 | x | in-val | 0 if input is < 1.5v, 1 if input >= 1.5v |
53 |
54 | 我们可以在 Rust 中公开这个结构体来展示这个寄存器结构:
55 |
56 | ```rust
57 | /// GPIO interface
58 | struct GpioConfig {
59 | /// GPIO Configuration structure generated by svd2rust
60 | periph: GPIO_CONFIG,
61 | }
62 |
63 | impl GpioConfig {
64 | pub fn set_enable(&mut self, is_enabled: bool) {
65 | self.periph.modify(|_r, w| {
66 | w.enable().set_bit(is_enabled)
67 | });
68 | }
69 |
70 | pub fn set_direction(&mut self, is_output: bool) {
71 | self.periph.modify(|_r, w| {
72 | w.direction().set_bit(is_output)
73 | });
74 | }
75 |
76 | pub fn set_input_mode(&mut self, variant: InputMode) {
77 | self.periph.modify(|_r, w| {
78 | w.input_mode().variant(variant)
79 | });
80 | }
81 |
82 | pub fn set_output_mode(&mut self, is_high: bool) {
83 | self.periph.modify(|_r, w| {
84 | w.output_mode.set_bit(is_high)
85 | });
86 | }
87 |
88 | pub fn get_input_status(&self) -> bool {
89 | self.periph.read().input_status().bit_is_set()
90 | }
91 | }
92 | ```
93 |
94 | 但是, 这样做会让我们能够修改其他寄存器. 例如, 如果当 GPIO 实际处于输入状态时, 我们将模式设置为输出会发生什么?
95 |
96 | 通常来说, 使用此结构体可以让我们达到状态机没有定义的状态: 例如, 被拉低的输出, 或者一个被设置为高电平的输入. 对于某些硬件, 这些可能不会起作用. 在其他硬件上, 这可能会导致 exception 或未定义行为.
97 |
98 | 尽管这个接口很方便, 但并不满足我们的设计.
99 |
--------------------------------------------------------------------------------
/src/intro/index.md:
--------------------------------------------------------------------------------
1 | # 引言
2 |
3 | 欢迎阅读嵌入式Rust之书, 本书是使用Rust在如微控制器(MCU)的"裸金属"嵌入式系统上编程的引导
4 |
5 | ## 谁应使用Rust进行嵌入式开发
6 |
7 | 嵌入式Rust为任何想要在嵌入式系统上享受Rust提供的高级功能及安全性的人所提供.
8 | (也可以看看[Who Rust Is For](https://doc.rust-lang.org/book/ch00-00-introduction.html))
9 |
10 | ## 概览
11 |
12 | 这本书的目标是:
13 |
14 | * 让开发者快速上手Rust嵌入式开发. 例如, 如何建立开发环境
15 |
16 | * 分享*当前*使用Rust进行嵌入式开发的最佳实践. 例如, 如何最好地使用Rust编写更加正确的嵌入式应用
17 |
18 | * 在某些情况下提供一个开发指南. 例如, 如何在一个项目中混用C与Rust.
19 |
20 | 本书试着尽可能涵盖各种体系, 但是为了让读者与作者~~还有翻译~~更轻松, 在所有实例中都是用ARM Cortex-M架构.
21 | 但是, 本书并不建立在读者熟悉该架构的基础上, 会在需要的地方解释架构的细节.
22 |
23 | ## 这本书适合谁
24 |
25 | 本书面向具有一定嵌入式背景或者对Rust熟悉的人, 但是我们相信每个对嵌入式Rust编程感兴趣的人都可以从本书中学到东西.
26 | 对于那些没有任何经验知识的人, 建议您阅读 "先决条件" 部分并且补全缺少的知识, 以便从书中获得更多知识并且提升阅读体验.
27 | 你可以查看 "其他资源" 部分来查找你想获得的知识对应资源.
28 |
29 | ### 先决条件
30 |
31 | * 你对使用Rust很熟悉, 并且在桌面环境Rust程序写过, 跑过, 捉过虫.
32 | 对Rust2018版本熟悉, 应为本书使用Rust 2018
33 |
34 | * 熟悉使用其他语言, 如C, C++, Ada开发调试嵌入式系统, 熟悉如以下概念:
35 | * 交叉编译
36 | * 内存映射外设
37 | * 中断
38 | * 通用接口, 如I2C, SPI, 串口等
39 |
40 | ### 其他资源
41 |
42 | 如果你对上面提到的东西不熟, 或者你想对本书提到的一个概念有更加深刻的了解, 你可以看看下面这些资源, 会很有用.
43 |
44 | | Topic | Resource | Description |
45 | |--------------|----------|-------------|
46 | | Rust | [Rust Book](https://doc.rust-lang.org/book/) | If you are not yet comfortable with Rust, we highly suggest reading this book. |
47 | | Rust, Embedded | [Discovery Book](https://docs.rust-embedded.org/discovery/) | If you have never done any embedded programming, this book might be a better start |
48 | | Rust, Embedded | [Embedded Rust Bookshelf](https://docs.rust-embedded.org) | Here you can find several other resources provided by Rust's Embedded Working Group. |
49 | | Rust, Embedded | [Embedonomicon](https://docs.rust-embedded.org/embedonomicon/) | The nitty gritty details when doing embedded programming in Rust. |
50 | | Rust, Embedded | [embedded FAQ](https://docs.rust-embedded.org/faq.html) | Frequently asked questions about Rust in an embedded context. |
51 | | Interrupts | [Interrupt](https://en.wikipedia.org/wiki/Interrupt) | - |
52 | | Memory-mapped IO/Peripherals | [Memory-mapped I/O](https://en.wikipedia.org/wiki/Memory-mapped_I/O) | - |
53 | | SPI, UART, RS232, USB, I2C, TTL | [Stack Exchange about SPI, UART, and other interfaces](https://electronics.stackexchange.com/questions/37814/usart-uart-rs232-usb-spi-i2c-ttl-etc-what-are-all-of-these-and-how-do-th) | - |
54 |
55 | ## 怎么看这本书
56 |
57 | 这本书默认你从头看到尾. 后面的章节建立在前面的基础上, 并且前面的章节不会深挖某个细节部分, 在后面会重新探讨这个问题
58 |
59 | 这本书使用ST公司的[STM32F3DISCOVERY]开发板作为例子.
60 | 这个开发板时ARM Cortex-M架构, 尽管基于该架构的大多数CPU的基本功能都是相似的, 但是不同供应商之间的MCU的外设与其他市县细节是不同的, 并且同意供应商之间的MCU也往往有所不同.
61 |
62 | 出于这个原因, 我们建议你买一块[STM32F3DISCOVERY]开发板来跟着学习本书中的例子.
63 |
64 | [STM32F3DISCOVERY]: http://www.st.com/en/evaluation-tools/stm32f3discovery.html
65 |
66 | ## 为本书做贡献
67 |
68 | 本书在[this repository]一起编写并且主要由[resources team]编写
69 |
70 | [this repository]: https://github.com/rust-embedded/book
71 | [resources team]: https://github.com/rust-embedded/wg#the-resources-team
72 |
73 | 如果你跟不住本书或是发现本书中某些部分不够清晰明白或者很难学习, 拿着就是一个BUG并且应该在[the issue tracker]被汇报
74 |
75 | [the issue tracker]: https://github.com/rust-embedded/book/issues/
76 |
77 | 欢迎修改文字错误或是增加内容
78 |
79 | ## 中文翻译
80 |
81 | 本书为作者抽空翻译,可能有语义不通顺,如有不明白的地方也请参考[英文原版](https://rust-embedded.github.io/book/#introduction)
82 |
83 | 如果有勘误, 欢迎提出你的想法
84 |
85 | ~~同时也复习考研英语~~
86 |
87 | 本书[仓库](https://github.com/Logiase/The-Embedded-Rust-Book-CN)
88 |
89 | 时刻欢迎批评与建议
90 |
91 | ## 重用本书资源
92 |
93 | 本书在以下LICENSES下发布
94 |
95 | * 代码示例与Cargo项目均在[MIT License]与[Apache License v2.0]下发布
96 |
97 | * 本书的文字内容, 图片与图标均根据[CC-BY-SA v4.0]条款获得许可
98 |
99 | [MIT License]: https://opensource.org/licenses/MIT
100 | [Apache License v2.0]: http://www.apache.org/licenses/LICENSE-2.0
101 | [CC-BY-SA v4.0]: https://creativecommons.org/licenses/by-sa/4.0/legalcode
102 |
103 | 太长别看系列: 如果你想在你的作品中使用我们的文字或图片, 你应该:
104 |
105 | * 加个提醒, 像是提一下本书, 再加个链接
106 | * 提供[CC-BY-SA v4.0]的链接
107 | * 说明你是否对内容进行了修改, 并且用相同的协议对进行更改
108 |
109 | 另外请一定让我们知道这本书帮了你 :gift:
110 |
--------------------------------------------------------------------------------
/src/peripherals/singletons.md:
--------------------------------------------------------------------------------
1 | # 单例
2 |
3 | > 在软件工程中, 单例模式是一种限制一个类只能存在一种的设计模式
4 | >
5 | > *Wikipedia: [Singleton Pattern]*
6 |
7 | [Singleton Pattern]: https://en.wikipedia.org/wiki/Singleton_pattern
8 |
9 | ## 但是我们为什么直接用全局变量?
10 |
11 | 我们可以把任何都设成一个全局变量, 像这样:
12 |
13 | ```rust
14 | static mut THE_SERIAL_PORT: SerialPort = SerialPort;
15 |
16 | fn main() {
17 | let _ = unsafe {
18 | THE_SERIAL_PORT.read_speed();
19 | };
20 | }
21 | ```
22 |
23 | 但是这有一些问题. 在 Rust 中, 与全局变量交互是 `unsafe` 的. 这些变量始终对你的程序可见, 这意味这引用检查器不能帮你追踪引用与所有权.
24 |
25 | ## 我们在 Rust 中怎么做?
26 |
27 | 代替将外设做为全局变量, 我们决定创建一个叫做 `PERIPHERALS` 的全局变量, 它包含我们每个外设的可空引用 `Option`.
28 |
29 | ```rust,ignore
30 | struct Peripherals {
31 | serial: Option,
32 | }
33 | impl Peripherals {
34 | fn take_serial(&mut self) -> SerialPort {
35 | let p = replace(&mut self.serial, None);
36 | p.unwrap()
37 | }
38 | }
39 | static mut PERIPHERALS: Peripherals = Peripherals {
40 | serial: Some(SerialPort),
41 | };
42 | ```
43 |
44 | 这个结构允许我们获取每个外设的单一实例. 如果我们多次使用 `take_serail()` , 我们的程序就会 panic !
45 |
46 | ```rust,ignore
47 | fn main() {
48 | let serial_1 = unsafe { PERIPHERALS.take_serial() };
49 | // This panics!
50 | // let serial_2 = unsafe { PERIPHERALS.take_serial() };
51 | }
52 | ```
53 |
54 | 尽管这么交互还是 `unsafe` , 但是我们一旦拿到它持有的 `SerialPort` , 我们就不需要再使用 `unsafe` 或者 `PERIPHERALS` 了.
55 |
56 | 这有很小的运行时开销, 因为我们必须将 `SerialPort` 包装在一个 `Option` 中, 并且需要调用一次 `take_serial()`, 但是, 这一点点成本能够让我们在剩余所有过程中使用引用检查器来检查我们的程序.
57 |
58 | ## 已有的库支持
59 |
60 | 尽管我们在前面创建了我们自己的 `Peripherals` , 但是你没必要再自己的代码中这么些, `cortex-m` 库中包含了一个叫 `singleton!()` 的宏, 它会帮你.
61 |
62 | ```rust
63 | #[macro_use(singleton)]
64 | extern crate cortex_m;
65 |
66 | fn main() {
67 | // OK if `main` is executed only once
68 | let x: &'static mut bool =
69 | singleton!(: bool = false).unwrap();
70 | }
71 | ```
72 |
73 | [cortex_m docs](https://docs.rs/cortex-m/latest/cortex_m/macro.singleton.html)
74 |
75 | 另外, 如果你使用[`cortex-m-rtic`](https://github.com/rtic-rs/cortex-m-rtic), 那它会帮你抽象这个定义和获取外围设备的步骤, 直接给你外设, 而不是你定义的 `Option`.
76 |
77 | ```rust
78 | // cortex-m-rtic v0.5.x
79 | #[rtic::app(device = lm3s6965, peripherals = true)]
80 | const APP: () = {
81 | #[init]
82 | fn init(cx: init::Context) {
83 | static mut X: u32 = 0;
84 |
85 | // Cortex-M peripherals
86 | let core: cortex_m::Peripherals = cx.core;
87 |
88 | // Device specific peripherals
89 | let device: lm3s6965::Peripherals = cx.device;
90 | }
91 | }
92 | ```
93 |
94 | ## 但是为什么?
95 |
96 | 但是这些单例如何在我们的代码中产生明显的不同?
97 |
98 | ```rust
99 | impl SerialPort {
100 | const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
101 |
102 | fn read_speed(
103 | &self // <------ This is really, really important
104 | ) -> u32 {
105 | unsafe {
106 | ptr::read_volatile(Self::SER_PORT_SPEED_REG)
107 | }
108 | }
109 | }
110 | ```
111 |
112 | 这有两个重要因素:
113 |
114 | * 因为我们在使用单例, 所以我们只有一种方法来获取一个 `SerialPort`
115 | * 为了使用 `read_speed()` 函数, 我们必须有 `SerialPort`的所有权或他的引用
116 |
117 | 这两个因素加在一起意味着我们只有在满足条件的情况下才能访问硬件, 意味着我们在任何时候都不能对同一硬件有多个可变引用!
118 |
119 | ```rust
120 | fn main() {
121 | // missing reference to `self`! Won't work.
122 | // SerialPort::read_speed();
123 |
124 | let serial_1 = unsafe { PERIPHERALS.take_serial() };
125 |
126 | // you can only read what you have access to
127 | let _ = serial_1.read_speed();
128 | }
129 | ```
130 |
131 | ## 把你的硬件看成数据
132 |
133 | 另外, 由于某些引用是可变的, 有些是不可变的, 因此可以查看某个函数或方法时候有潜在的可能修改硬件的状态. 例如:
134 |
135 | 这允许修改硬件设置:
136 |
137 | ```rust
138 | fn setup_spi_port(
139 | spi: &mut SpiPort,
140 | cs_pin: &mut GpioPin
141 | ) -> Result<()> {
142 | // ...
143 | }
144 | ```
145 |
146 | 这不允许:
147 |
148 | ```rust,ignore
149 | fn read_button(gpio: &GpioPin) -> bool {
150 | // ...
151 | }
152 | ```
153 |
154 | 这能够让我们在**编译时**(而不是运行时)确定代码是否能够修改硬件状态. 需要注意的是, 这通常仅仅在一个应用中可行, 但是对于裸金属系统, 我们的代码通常只会编译为一个应用, 所以不受限制.
155 |
--------------------------------------------------------------------------------
/src/peripherals/a-first-attempt.md:
--------------------------------------------------------------------------------
1 | # 第一次尝试
2 |
3 | ## 寄存器
4 |
5 | 我们来看看 `SysTick` 这个外设-每个 Cortex-M 处理器内核都有的简单计时器. 通常情况下, 你可以在芯片制造商的*数据手册*中找到这些信息, 但是此示例对于所有的 Arm Cortex-M 内核都是通用的, 让我们在[ARM参考手册]中进行查找. 我们看到有四个寄存器:
6 |
7 | [ARM参考手册]: http://infocenter.arm.com/help/topic/com.arm.doc.dui0553a/Babieigh.html
8 |
9 | | Offset | Name | Description | Width |
10 | |--------|-------------|-----------------------------|--------|
11 | | 0x00 | SYST_CSR | Control and Status Register | 32 bits|
12 | | 0x04 | SYST_RVR | Reload Value Register | 32 bits|
13 | | 0x08 | SYST_CVR | Current Value Register | 32 bits|
14 | | 0x0C | SYST_CALIB | Calibration Value Register | 32 bits|
15 |
16 | ## C怎么做
17 |
18 | 在Rust中, 我们可以使用与 C 完全相同的方式来表示寄存器的合集-使用 `struct` .
19 |
20 | ```rust
21 | #[repr(C)]
22 | struct SysTick {
23 | pub csr: u32,
24 | pub rvr: u32,
25 | pub cvr: u32,
26 | pub calib: u32,
27 | }
28 | ```
29 |
30 | 限定符 `#[repr(C)]` 告诉 Rust 编译器像 C 一样对这个 struct 布局. 这非常重要, 因为 Rust 会对 struct 的字段进行重新排序, 而 C 不会. 你可以想象一下, 如果 Rust 对其进行了重新排序, 我们进行调试找 BUG 会多难! 使用这个限定符之后, 我们就有四个32位字段, 它们与上表相对应. 但是, 当然仅有这个 `struct` 是不够的, 我们还需要一个变量.
31 |
32 | ```rust
33 | let systick = 0xE000_E010 as *mut SysTick;
34 | let time = unsafe { (*systick).cvr };
35 | ```
36 |
37 | ## 有序访问
38 |
39 | 现在, 我们碰到了一堆问题.
40 |
41 | 1. 我们需要用到 unsafe 来访问我们的外设.
42 | 2. 我们没有办法确定那个寄存器是只读的或可读可写的.
43 | 3. 代码中的任何部分都可以通过这个结构来访问硬件.
44 | 4. 最重要的, 现在它还不能用...
45 |
46 | 现在的问题是编译器很聪明. 如果你向同一块内存做两次写入, 一前一后, 编译器会注意到这个操作, 然后优化掉第一次写入. 在 C 中, 我们可以把这个变量标记为 `volatile` 来确保每次读写操作都会准确发生. 在 Rust 中, 我们将 *指针* 标记为 volatile, 而不是变量.
47 |
48 | ```rust
49 | let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
50 | let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };
51 | ```
52 |
53 | 所以, 我们修复了上面四个问题之一, 但是我们却写出了更 `unsafe` 的代码! 幸运的是, 这由第三方的库能帮我们 - [`volatile_register`].
54 |
55 | [`volatile_register`]: https://crates.io/crates/volatile_register
56 |
57 | ```rust
58 | use volatile_register::{RW, RO};
59 |
60 | #[repr(C)]
61 | struct SysTick {
62 | pub csr: RW,
63 | pub rvr: RW,
64 | pub cvr: RW,
65 | pub calib: RO,
66 | }
67 |
68 | fn get_systick() -> &'static mut SysTick {
69 | unsafe { &mut *(0xE000_E010 as *mut SysTick) }
70 | }
71 |
72 | fn get_time() -> u32 {
73 | let systick = get_systick();
74 | systick.cvr.read()
75 | }
76 | ```
77 |
78 | 现在, 读取写入都通过 `read` 和 `write` 安排妥当了. 虽然写入还是 `unsafe` , 不过现在, 硬件现在是一堆可变的状态, 编译器没法知道这些操作是不是安全的, 所以这是一个不错的默认位置.
79 |
80 | ## Rust风格的外壳
81 |
82 | 我们需要用一个更高级的 API 来封装一下这个 `struct` 来让能让我们安全使用. 作为驱动作者, 我们人工确定 `unsafe` 代码是否正确, 然后给我们的用户提供一个 `safe` 的 API来让我们的用户不去担心这些.
83 |
84 | 一个栗子:
85 |
86 | ```rust
87 | use volatile_register::{RW, RO};
88 |
89 | pub struct SystemTimer {
90 | p: &'static mut RegisterBlock
91 | }
92 |
93 | #[repr(C)]
94 | struct RegisterBlock {
95 | pub csr: RW,
96 | pub rvr: RW,
97 | pub cvr: RW,
98 | pub calib: RO,
99 | }
100 |
101 | impl SystemTimer {
102 | pub fn new() -> SystemTimer {
103 | SystemTimer {
104 | p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
105 | }
106 | }
107 |
108 | pub fn get_time(&self) -> u32 {
109 | self.p.cvr.read()
110 | }
111 |
112 | pub fn set_reload(&mut self, reload_value: u32) {
113 | unsafe { self.p.rvr.write(reload_value) }
114 | }
115 | }
116 |
117 | pub fn example_usage() -> String {
118 | let mut st = SystemTimer::new();
119 | st.set_reload(0x00FF_FFFF);
120 | format!("Time is now 0x{:08x}", st.get_time())
121 | }
122 | ```
123 |
124 | 现在的问题是, 如下这样的代码是被编译器完完全全接受的:
125 |
126 | ```rust
127 | fn thread1() {
128 | let mut st = SystemTimer::new();
129 | st.set_reload(2000);
130 | }
131 |
132 | fn thread2() {
133 | let mut st = SystemTimer::new();
134 | st.set_reload(1000);
135 | }
136 | ```
137 |
138 | 我们对 `set_reload` 函数的 `&mut self` 参数可以确保没有其他对*这个*特定的 `SystemTimer` 的引用, 但是它们并不阻止用户创建第二个指向同一个外设的变量! 如果作者很努力的发现所有这样"重复"的却动, 那这样的写法会有作用, 但是一旦代码分散到多个模块, 驱动, 开发者之中, 那出现错误会越来越容易.
139 |
--------------------------------------------------------------------------------
/src/collections/index.md:
--------------------------------------------------------------------------------
1 | # 容器
2 |
3 | 我们经常会在我们的应用中使用动态的数据结构(或者叫集合、容器)。
4 | `std` 为我们提供了一系列常用的容器:[`Vec`], [`String`], [`HashMap`]等等。
5 | 这些在`std`中实现的容器都用了全局的内存分配器。
6 |
7 | [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html
8 | [`String`]: https://doc.rust-lang.org/std/string/struct.String.html
9 | [`HashMap`]: https://doc.rust-lang.org/std/collections/struct.HashMap.html
10 |
11 | 但是在`Core`的定义中没有内存分配器的实现,它们随编译器`alloc`库中附带。
12 |
13 | 如果你需要使用容器,除了自己实现一个内存分配器外,也可以考虑一下*固定容量*的容器,比如[`heapless`]
14 |
15 | [`heapless`]: https://crates.io/crates/heapless
16 |
17 | 在本章节中,我们会介绍并对比这两种实现方式。
18 |
19 | ## 使用`alloc`
20 |
21 | `alloc`默认随Rust附带,不用在`Cargo.toml`中声明就可以使用。
22 |
23 | ```rust,ignore
24 | #![feature(alloc)]
25 |
26 | extern crate alloc;
27 |
28 | use alloc::vec::Vec;
29 | ```
30 |
31 | 为了使用这些容器你首先需要使用`global_allocator`来给你的程序标记使用的全局内存分配器,分配器需要实现 [`GlobalAlloc`] trait。
32 |
33 | [`GlobalAlloc`]: https://doc.rust-lang.org/core/alloc/trait.GlobalAlloc.html
34 |
35 | 为了使本章节尽量保持完整清晰,让我们实现一个简单的bump pointer allocator作为全局内存分配器。
36 | 但是*强烈*建议你在你的程序中使用一个经过实际测试的库来替代这个简陋的分配器。
37 |
38 | ``` rust,ignore
39 | // Bump pointer allocator implementation
40 | extern crate cortex_m;
41 | use core::alloc::GlobalAlloc;
42 | use core::ptr;
43 | use cortex_m::interrupt;
44 | // Bump pointer allocator for *single* core systems
45 | struct BumpPointerAlloc {
46 | head: UnsafeCell,
47 | end: usize,
48 | }
49 | unsafe impl Sync for BumpPointerAlloc {}
50 | unsafe impl GlobalAlloc for BumpPointerAlloc {
51 | unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
52 | // `interrupt::free` is a critical section that makes our allocator safe
53 | // to use from within interrupts
54 | interrupt::free(|_| {
55 | let head = self.head.get();
56 | let size = layout.size();
57 | let align = layout.align();
58 | let align_mask = !(align - 1);
59 | // move start up to the next alignment boundary
60 | let start = (*head + align - 1) & align_mask;
61 | if start + size > self.end {
62 | // a null pointer signal an Out Of Memory condition
63 | ptr::null_mut()
64 | } else {
65 | *head = start + size;
66 | start as *mut u8
67 | }
68 | })
69 | }
70 | unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
71 | // this allocator never deallocates memory
72 | }
73 | }
74 | // Declaration of the global memory allocator
75 | // NOTE the user must ensure that the memory region `[0x2000_0100, 0x2000_0200]`
76 | // is not used by other parts of the program
77 | #[global_allocator]
78 | static HEAP: BumpPointerAlloc = BumpPointerAlloc {
79 | head: UnsafeCell::new(0x2000_0100),
80 | end: 0x2000_0200,
81 | };
82 | ```
83 |
84 | 除了选择全局内存分配器,我们还需要使用*不稳定标签*`alloc_error_handler`定义内存溢出错误的处理办法。
85 |
86 | ``` rust,ignore
87 | #![feature(alloc_error_handler)]
88 | use cortex_m::asm;
89 | #[alloc_error_handler]
90 | fn on_oom(_layout: Layout) -> ! {
91 | asm::bkpt();
92 | loop {}
93 | }
94 | ```
95 |
96 | 这些都完成之后,我们就可以使用在`alloc`中定义的容器了。
97 |
98 | ```rust,ignore
99 | #[entry]
100 | fn main() -> ! {
101 | let mut xs = Vec::new();
102 | xs.push(42);
103 | assert!(xs.pop(), Some(42));
104 | loop {
105 | // ..
106 | }
107 | }
108 | ```
109 |
110 | 这些容器和`std`中的定义完全一样。
111 |
112 | ## 使用`heapless`
113 |
114 | `heapless`不需要初始化因为它并不依赖全局内存分配器,直接使用就可以:
115 |
116 | ```rust,ignore
117 | extern crate heapless; // v0.4.x
118 | use heapless::Vec;
119 | use heapless::consts::*;
120 | #[entry]
121 | fn main() -> ! {
122 | let mut xs: Vec<_, U8> = Vec::new();
123 | xs.push(42).unwrap();
124 | assert_eq!(xs.pop(), Some(42));
125 | }
126 | ```
127 |
128 | 你需要注意这两种容器的区别(`heapless`和`alloc`)。
129 |
130 | 首先你需要定义容器的最大容量,`heapless`不会重新分配内存并且有固定大小的容量,并且容量是类型的一部分。
131 | 上面的例子中,我们定义了一个只能容纳8个元素的向量`xs`(容量为8)。这个容量由`U8`(参见[`typenum`])确定。
132 |
133 | [`typenum`]: https://crates.io/crates/typenum
134 |
135 | 并且,`push`还有其他许多方法会返回一个`Result`。因为`heapless`容器有固定的容量,因此所有插入元素的操作都有失败的可能性。
136 | `heapless` API通过返回一个`Result`来表示是否成功。使用`alloc`的情况下,容器会在容量满时重新分配内存。
137 |
138 | `heapless` v0.4.x版本的容器会直接存储他们的元素,这意味着如`let x = heapless::Vec::new();`这样的操作会直接把容器分配到栈上(全在栈上),
139 | 不过想要把他们分配到`static`或者堆上(`Box>`)也是可行的。
140 |
141 | ## 使用权衡
142 |
143 | 在选择使用堆分配自动扩容的容器或固定容量的容器时记住一下几条内容。
144 |
145 | ### 内存溢出错误处理(Out of Memory)
146 |
147 | 在容器自动扩容的时候有可能发生OOM错误:比如`alloc::Vec.push`。一些扩容操作可能悄悄失败,
148 | 一些`alloc`的容器会提供一个`try_reserve`的方法来让你检查扩容是否会失败,但你必须主动调用。
149 |
150 | 如果你只使用`heapless`容器,并且不用内存分配器那就永远不会出现OOM错误。
151 | 但是你需要处理容器容量不够的问题(指你需要手动处理所有类似`Vec.push`返回的`Result`)
152 |
153 | OOM错误处理比`heapless`的Result难得多(`Result`只要`unwrap`就行),因为出现错误的位置可能和引发错误的位置不同。
154 | 比如`vec.reserve(1)`失败了,但真正的原因是其他容器一直在内存泄漏导致没法继续分配(safe Rust中也是可以出现内存泄漏的)。
155 |
156 | ### 内存使用
157 |
158 | 堆内存的使用情况进行分析时很难的,因为长期存活的容器容量是可以在运行时变化的。
159 | 一些操作可以增大内存占用,也有操作可以减少内存占用(比如`shrink_to_fit`)。
160 | 另外,分配器也要处理内存分片的问题来增加*可用*内存。
161 |
162 | 另一方面,如果你只用固定容量的容器,把它们存在`static`变量中然后设置一个容量,连接器会自动为你检查是否会容量不够。
163 |
164 | 另外,分配在栈上的固定大小容器可以在栈分析工具(如 [`stack-sizes`])使用 [`-Z emit-stack-sizes`] 来分析报告。
165 |
166 | [`-Z emit-stack-sizes`]: https://doc.rust-lang.org/beta/unstable-book/compiler-flags/emit-stack-sizes.html
167 | [`stack-sizes`]: https://crates.io/crates/stack-sizes
168 |
169 | 然而,固定容量的容器不能被压缩大小,相比自动扩容的容器有更低的内存使用效率(实际使用内存与占用内存的比值)。
170 |
171 | ### 最慢执行时间 (WCET)
172 |
173 | 如果你在做一个时间敏感型应用,或者对时间有很高要求,那么你需要关心一下你应用不同部分的最慢执行时间。
174 |
175 | `alloc`内的容器会重新分配内存,所以最慢执行时间会包括容器在*运行时*重新分配内存的过程。
176 | 这让最慢执行时间难以估计,例如`alloc::Vec.push`会根据已有容量和运行时容量进行扩容。
177 |
178 | 另一方面,固定容量容器从来不会重新分配内存,所以时间是可估计的。例如`heapless::Vec.push`有一个固定的时间。
179 |
180 | ### 便于使用
181 |
182 | `alloc`需要设置一个全局内存分配器,而`heapless`就不需要。然而`heapless`需要你在初始化时指定容量。
183 |
184 | `alloc`的API对几乎任何Rust开发者都很熟悉,`heapless`的API已经尽量和`alloc`保持一致了,但是因为它的错误处理还是会有不同,
185 | 一些开发者会觉得显式的错误处理太麻烦太多了。
186 |
--------------------------------------------------------------------------------
/src/start/exception.md:
--------------------------------------------------------------------------------
1 | # 异常
2 |
3 | 异常与中断是一种硬件机制, 处理器通过该机制来来异步处理事件或错误(例如执行无效指令).
4 | 异常意味着抢占, 涉及异常处理程序, 这些子处理程序使为了响应触发事件的信号而执行的子线程.
5 |
6 | `cortex-m-rt`库提供了一个[`exception`]这个属性用来定义异常处理函数.
7 |
8 | [`exception`]: https://docs.rs/cortex-m-rt-macros/latest/cortex_m_rt_macros/attr.exception.html
9 |
10 | ``` rust,ignore
11 | // Exception handler for the SysTick (System Timer) exception
12 | #[exception]
13 | fn SysTick() {
14 | // ..
15 | }
16 | ```
17 |
18 | 除了`exception`这个属性, 这个函数看着就像一个普通函数, 但有一点不同: `exception`处理函数不能被普通程序调用.
19 | 跟着前一个例子, 调用`SysTick`会导致编译错误.
20 |
21 | 此行为是可以预期的, 并且需要提供一个特性: 定义在`exception`处理中的`static mut`变量必须能够*安全*使用.
22 |
23 | ``` rust,ignore
24 | #[exception]
25 | fn SysTick() {
26 | static mut COUNT: u32 = 0;
27 |
28 | // `COUNT` has transformed to type `&mut u32` and it's safe to use
29 | *COUNT += 1;
30 | }
31 | ```
32 |
33 | 和你知道的一样, 在函数中使用`static mut`变量让其[*不可重入*](https://en.wikipedia.org/wiki/Reentrancy_(computing)).
34 | 从多个异常或中断函数中, 或从`main`和一个或多个异常或中断处理函数中直接或间接调用可重入函数是未定义行为.
35 |
36 | 安全Rust必须不能导致未定义行为, 所以可重入函数必须要标记为`unsafe`.
37 | 但是我只是告诉我们的`exception`处理函数可以安全的使用`static mut`变量.
38 | 这怎么可能? 但这是可能的, 因为`exception`处理不能被软件调用, 所以也就不可能重入.
39 |
40 | > 注意, `exception`属性将函数中的静态变量定义包装到`unsafe`中, 并为我们提供了具有相同名称的`&mut`引用, 从而在函数内部转换他们.
41 | > 因此, 我们可以使用`*`解引用, 来访问变量的值, 而无需把他们放在`unsafe`中.
42 |
43 | ## 一个完整的例子
44 |
45 | 这是一个使用系统计时器每秒来触发一个`SysTick`异常的例子.
46 | `SysTick`异常处理函数使用`COUNT`变量追踪一共触发了多少次, 并且用`semihosting`把`COUNT`的值输出到主机上.
47 |
48 | > **NOTE** 你可以在任何Cortex-M设备上运行, 也可以在QEMU上运行.
49 |
50 | ```rust,ignore
51 | #![deny(unsafe_code)]
52 | #![no_main]
53 | #![no_std]
54 |
55 | use panic_halt as _;
56 |
57 | use core::fmt::Write;
58 |
59 | use cortex_m::peripheral::syst::SystClkSource;
60 | use cortex_m_rt::{entry, exception};
61 | use cortex_m_semihosting::{
62 | debug,
63 | hio::{self, HStdout},
64 | };
65 |
66 | #[entry]
67 | fn main() -> ! {
68 | let p = cortex_m::Peripherals::take().unwrap();
69 | let mut syst = p.SYST;
70 |
71 | // configures the system timer to trigger a SysTick exception every second
72 | syst.set_clock_source(SystClkSource::Core);
73 | // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
74 | syst.set_reload(12_000_000);
75 | syst.clear_current();
76 | syst.enable_counter();
77 | syst.enable_interrupt();
78 |
79 | loop {}
80 | }
81 |
82 | #[exception]
83 | fn SysTick() {
84 | static mut COUNT: u32 = 0;
85 | static mut STDOUT: Option = None;
86 |
87 | *COUNT += 1;
88 |
89 | // Lazy initialization
90 | if STDOUT.is_none() {
91 | *STDOUT = hio::hstdout().ok();
92 | }
93 |
94 | if let Some(hstdout) = STDOUT.as_mut() {
95 | write!(hstdout, "{}", *COUNT).ok();
96 | }
97 |
98 | // IMPORTANT omit this `if` block if running on real hardware or your
99 | // debugger will end in an inconsistent state
100 | if *COUNT == 9 {
101 | // This will terminate the QEMU process
102 | debug::exit(debug::EXIT_SUCCESS);
103 | }
104 | }
105 | ```
106 |
107 | ``` console
108 | $ tail -n5 Cargo.toml
109 | ```
110 |
111 | ``` toml
112 | [dependencies]
113 | cortex-m = "0.5.7"
114 | cortex-m-rt = "0.6.3"
115 | panic-halt = "0.2.0"
116 | cortex-m-semihosting = "0.3.1"
117 | ```
118 |
119 | ``` console
120 | $ cargo run --release
121 | Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
122 | 123456789
123 | ```
124 |
125 | 如果你使用Discovery开发板, 你会在OpenOCD上看到输出. 另外, 直到计数达到9才会停止.
126 |
127 | ## 默认异常处理
128 |
129 | `exception`属性实际上做的是用一个特定的异常处理函数覆盖默认的异常处理函数.
130 | 如果你不覆盖的话, 异常处理程序是`DefaultHandler`, 内容如下:
131 |
132 | ``` rust,ignore
133 | fn DefaultHandler() {
134 | loop {}
135 | }
136 | ```
137 |
138 | 这个函数是`cortex-m-rt`提供的, 并且被标记为`#[no_mangle]`, 所以你可以在"DefaultHandler"中打一个断点来捕获*未处理*的异常.
139 |
140 | 也使用`exception`属性来覆盖`DefaultHandler`
141 |
142 | ``` rust,ignore
143 | #[exception]
144 | fn DefaultHandler(irqn: i16) {
145 | // custom default handler
146 | }
147 | ```
148 |
149 | `irqn`参数是正在处理的异常.
150 | 负数表示正在处理的是Cortex-M异常.
151 | 0或正数表示正在处理设备特定的异常, 又被叫做中断.
152 |
153 | ## 硬件故障处理
154 |
155 | `HardFault`有一点特殊.
156 | 当程序进入无效状态的时候, 会引发此异常, 所以这个函数不会*return*, 因为这可能会导致未定义行为.
157 | 另外, 在调用用户定义的`HardFault`函数时, 运行时会会做一些方便debug的工作.
158 |
159 | 结果是`HardFault`函数必须要像这样: `fn(&ExceptionFrame) -> !`.
160 | 函数的参数是一个指向由异常入栈的寄存器的指针.
161 | 这些寄存器是出现异常时处理器状态的快照, 可以用来诊断故障.
162 |
163 | 这有一个产生非法操作的例子: 读取不存在的内存地址.
164 |
165 | > **NOTE** 该程序在QEMU上不起作用, 不会崩溃, 因为`qemu-system-arm -machine lm3s6965evb`不会检查内存, 并且会在访问无效地址时返回一个0.
166 |
167 | ```rust,ignore
168 | #![no_main]
169 | #![no_std]
170 |
171 | use panic_halt as _;
172 |
173 | use core::fmt::Write;
174 | use core::ptr;
175 |
176 | use cortex_m_rt::{entry, exception, ExceptionFrame};
177 | use cortex_m_semihosting::hio;
178 |
179 | #[entry]
180 | fn main() -> ! {
181 | // read a nonexistent memory location
182 | unsafe {
183 | ptr::read_volatile(0x3FFF_FFFE as *const u32);
184 | }
185 |
186 | loop {}
187 | }
188 |
189 | #[exception]
190 | fn HardFault(ef: &ExceptionFrame) -> ! {
191 | if let Ok(mut hstdout) = hio::hstdout() {
192 | writeln!(hstdout, "{:#?}", ef).ok();
193 | }
194 |
195 | loop {}
196 | }
197 | ```
198 |
199 | `HardFault`函数打印`ExceptionFrame`的值.
200 | 如果你运行它, 会在OpenOCD上看到:
201 |
202 | ``` console
203 | $ openocd
204 | (..)
205 | ExceptionFrame {
206 | r0: 0x3ffffffe,
207 | r1: 0x00f00000,
208 | r2: 0x20000000,
209 | r3: 0x00000000,
210 | r12: 0x00000000,
211 | lr: 0x080008f7,
212 | pc: 0x0800094a,
213 | xpsr: 0x61000000
214 | }
215 | ```
216 |
217 | `pc`是当前程序计数器发生异常时的值, 指向触发异常的指令.
218 |
219 | 如果你看看程序的反汇编:
220 |
221 | ``` console
222 | $ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
223 | (..)
224 | ResetTrampoline:
225 | 8000942: movw r0, #0xfffe
226 | 8000946: movt r0, #0x3fff
227 | 800094a: ldr r0, [r0]
228 | 800094c: b #-0x4
229 | ```
230 |
231 | 你可以在反汇编中找到程序计数器`0x0800094a`的值.
232 | 看这, 有个加载行为 (`ldr r0, [r0]` ) 导致异常.
233 | `ExceptionFrame`中`r0`字段将告诉你此时寄存器`r0`的值为`0x3fff_fffe`
234 |
--------------------------------------------------------------------------------
/src/start/registers.md:
--------------------------------------------------------------------------------
1 | # 内存映射寄存器
2 |
3 | 嵌入式系统只能通过执行常规的Rust代码并在RAM中移动数据来达到目标.
4 | 如果我们想从外部读取信息到系统,或从系统中获取信息(例如,点亮LED,检测到按钮按下,或者与总线上某种设备进行通信),那我们必须要接触外设和"内存映射寄存器"
5 |
6 | 您可能会发现,已经在以下级别之一编写了访问微控制器外围设备所需的代码:
7 |
8 |
9 |
10 |
11 |
12 | - Micro-architecture Crate - 这种库可处理你使用的mcu的通用部分,以及使用该内核的所有mcu的通用的外设.例如,[cortex-m]可以提供启用禁用中断的功能,这些功能对所有Cortex-m处理器都适用.他还可以让你访问所有基于Cortex-m微控制器所带的'SysTick'外设.
13 | - Peripheral Access Crate (PAC) - 这种库是根据你的mcu型号来提供一个对内存包装寄存器的简单包装.例如[tm4c123x]对应Texas Instruments Tiva-C TM4C123系列,[stm32f30x]对应ST-Micro STM32F30x系列.在这里你将按照mcu的参考手册中给出的每个外设的操作说明直接操作寄存器.
14 | - HAL Crate - 这些库给实现[embedded-hal]的一些trait,来为你的mcu提供一个更加用户友好的API.例如,这些库可能会提供一个`Serial` Struct,它的构造函数使用适当GPIO与波特率,并提供某种`write_byte`函数来发送数据.有关嵌入式HAL的更多信息,请参考[移植]
15 | - Board Crate - 这些库通过预先配置好的外设和GPIO引脚来让你使用特定的开发板,像是[stm32f3-discovery]对STM32F3DISCOVERY,这些库比HAL库更进一步.
16 |
17 | [cortex-m]: https://crates.io/crates/cortex-m
18 | [tm4c123x]: https://crates.io/crates/tm4c123x
19 | [stm32f30x]: https://crates.io/crates/stm32f30x
20 | [embedded-hal]: https://crates.io/crates/embedded-hal
21 | [移植]: ../portability/index.md
22 | [stm32f3-discovery]: https://crates.io/crates/stm32f3-discovery
23 | [Discovery]: https://rust-embedded.github.io/discovery/
24 |
25 | ## Board Crate
26 |
27 | 如果你在嵌入式系统方面是个萌新,拿使用Board Crate是一个很好的起点.
28 | 他们很好的抽象了我们在学习过程中会遇到的硬件细节,并简化像是开关LED的操作.
29 | 他们暴露的函数在不同开发板之间差别很大.由于本书旨在不涉及硬件的细节,所以本书不会使用board crate.
30 |
31 | 如果你想使用STM32F3DISCOVERY进行试验,那很推荐你去看一看[stm32f3-discovery] board crate,这个库提供了一些列功能,包括开关LED,使用指南针,蓝牙等.
32 | [Discovery]这本书提供了一个使用这个board crate很好的介绍.
33 |
34 | 但是如果你使用一个没有board crate的系统,或者你需要使用现有board crate没有提供的功能,请从底部开始阅读micro-architecture.
35 |
36 | ## Micro-architecture crate
37 |
38 | 让我们看一下所有基于Cortex-M的微控制器共有的SysTick外设.我们可以在[cortex-m]中找到一个非常非常低级的API,我们能这么用:
39 |
40 | ```rust,ignore
41 | #![no_std]
42 | #![no_main]
43 | use cortex_m::peripheral::{syst, Peripherals};
44 | use cortex_m_rt::entry;
45 | use panic_halt as _;
46 |
47 | #[entry]
48 | fn main() -> ! {
49 | let peripherals = Peripherals::take().unwrap();
50 | let mut systick = peripherals.SYST;
51 | systick.set_clock_source(syst::SystClkSource::Core);
52 | systick.set_reload(1_000);
53 | systick.clear_current();
54 | systick.enable_counter();
55 | while !systick.has_wrapped() {
56 | // Loop
57 | }
58 |
59 | loop {}
60 | }
61 | ```
62 |
63 | SYST struct的函数与ARM Technical Reference Manual定义的很相似.
64 | 此API中没有没有关于`延迟X毫秒`的函数 - 我们得使用`while`循环
65 | 来大致的实现这个功能.注意,我们在调用`Peripherals::take()`函数前,
66 | 我们没法使用`SYST` - 这是一个特殊的历程,可以确保整个程序中只有一个`SYST`.
67 | 关于更多,可以参考[Peripherals]章节
68 |
69 | [Peripherals]: ../peripherals/index.md
70 |
71 | ## 使用Peripheral Access Crate (PAC)
72 |
73 | 如果我们把自己束缚在Cortex-M自带的基本外设上,那注定我们的嵌入式之路是走不远的.
74 | 在某个时候,我们需要编写一些特定于我们正在使用的硬件的代码.
75 | 在这个实例中,先假设我们有一个德州仪器(TI)的TM4C123,一个有256KiB闪存,80MHz的中等的Cortex-M4微控制器.
76 | 我们打算使用[tm4c123x]库来玩这块芯片.
77 |
78 | ```rust,ignore
79 | #![no_std]
80 | #![no_main]
81 |
82 | use panic_halt as _; // panic handler
83 |
84 | use cortex_m_rt::entry;
85 | use tm4c123x;
86 |
87 | #[entry]
88 | pub fn init() -> (Delay, Leds) {
89 | let cp = cortex_m::Peripherals::take().unwrap();
90 | let p = tm4c123x::Peripherals::take().unwrap();
91 |
92 | let pwm = p.PWM0;
93 | pwm.ctl.write(|w| w.globalsync0().clear_bit());
94 | // Mode = 1 => Count up/down mode
95 | pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
96 | pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
97 | // 528 cycles (264 up and down) = 4 loops per video line (2112 cycles)
98 | pwm._2_load.write(|w| unsafe { w.load().bits(263) });
99 | pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
100 | pwm.enable.write(|w| w.pwm4en().set_bit());
101 | }
102 |
103 | ```
104 |
105 | 除了我们调用`tm4c123x::Peripherals::take()`外,我们使用`PWM0`外设的方法是和`SYST`相同的.
106 | 因为此库是使用[svd2rust]自动生成的,所以我们访问寄存器需要闭包参数,而不是数字参数.
107 | 尽管这看起来很多,但是rust编译器会执行一堆检查,然后生成的机器码与我们手写的汇编非常接近!
108 | 自动生成的代码无法确定特定寄存器的所有参数(例如,如果SVD定义寄存器有32bit,但并没有说明其中的某些位有什么特殊功能),所以被标记为`unsafe`.
109 | 我们可以在上面这个例子中看到如何使用`bits()`的子函数`load`,`compa`.
110 |
111 | ### 读取
112 |
113 | `read()`函数会返回一个包含有制造商SVD文件定义的寄存器各个子段的只读权限的对象.
114 | 你可以在[tm4c123x documentation][tm4c123x documentation R]中特定外设,特定寄存器的特殊`R`返回值类型中的所有可用函数.
115 |
116 | ```rust,ignore
117 | if pwm.ctl.read().globalsync0().is_set() {
118 | // Do a thing
119 | }
120 | ```
121 |
122 | ### 写入
123 |
124 | `write()`函数需要一个只有一个参数的闭包参数.我们叫他`w`.
125 | 这个参数有该设备制造商SVD文件定义的寄存器所有子段的读写权限.
126 | 你也可以在[tm4c123x documentation][tm4c123x Documentation W]中找到针对该芯片该外设该寄存器`w`的所有函数.
127 | 请注意,我们未设置的所有子字段都将被设置为我们的默认值-寄存器中的所有现有内容都将丢失.
128 |
129 | ```rust,ignore
130 | pwm.ctl.write(|w| w.globalsync0().clear_bit());
131 | ```
132 |
133 | ### 修改
134 |
135 | 如果我们想修改寄存器中某一子段的值而不修改其他的,我们可以使用`modify()`函数.
136 | 该函数需要一个包括两个参数的闭包参数,一个用来读,一个用来写.我们经常叫`r`和`w`.
137 | `r`可以用来查看当前寄存器中的内容,`w`可以用来修改寄存器中的值.
138 |
139 | ```rust,ignore
140 | pwm.ctl.modify(|r, w| w.globalsync0().clear_bit());
141 | ```
142 |
143 | `modify`函数在这真的展现了闭包的强大.在`C`中,我们先要把值读取到几个临时变量中,然后做修改,然后再写回去.这意味着会存在很大错误范围:
144 |
145 | ```C
146 | uint32_t temp = pwm0.ctl.read();
147 | temp |= PWM0_CTL_GLOBALSYNC0;
148 | pwm0.ctl.write(temp);
149 | uint32_t temp2 = pwm0.enable.read();
150 | temp2 |= PWM0_ENABLE_PWM4EN;
151 | pwm0.enable.write(temp); // Uh oh! Wrong variable!
152 | ```
153 |
154 | [svd2rust]: https://crates.io/crates/svd2rust
155 | [tm4c123x documentation R]: https://docs.rs/tm4c123x/0.7.0/tm4c123x/pwm0/ctl/struct.R.html
156 | [tm4c123x documentation W]: https://docs.rs/tm4c123x/0.7.0/tm4c123x/pwm0/ctl/struct.W.html
157 |
158 | ## 使用 HAL(硬件抽象层) 库
159 |
160 | 芯片的HAL库通常通过为PAC暴露的原始结构来实现自定义trait.通常这个trait会为单独的外设定义一个叫`constrain()`的函数,为类似GPIO这样有多个引脚的外设定义`split()`函数.此函数包装最原始的结构,然后提供拥有一个高级的API的对象.
161 | 这个API可以做很多事情,例如串口的`new`需要借用`Clock`结构,`Clock`只能通过配置PLL设置时钟频率获得.
162 | 通过这种方法,在没有创建配置时钟或没法将波特率与始终速率对应起来之前没法创建一个串口对象.
163 | 一些库甚至为GPIO引脚定义了特殊的trait,需要用户选择引脚的正确状态(或者说,选择合适的复用功能).都不要运行时花销.
164 |
165 | 让我们看个例子:
166 |
167 | ```rust,ignore
168 | #![no_std]
169 | #![no_main]
170 |
171 | use panic_halt as _; // panic handler
172 |
173 | use cortex_m_rt::entry;
174 | use tm4c123x_hal as hal;
175 | use tm4c123x_hal::prelude::*;
176 | use tm4c123x_hal::serial::{NewlineMode, Serial};
177 | use tm4c123x_hal::sysctl;
178 |
179 | #[entry]
180 | fn main() -> ! {
181 | let p = hal::Peripherals::take().unwrap();
182 | let cp = hal::CorePeripherals::take().unwrap();
183 |
184 | // Wrap up the SYSCTL struct into an object with a higher-layer API
185 | let mut sc = p.SYSCTL.constrain();
186 | // Pick our oscillation settings
187 | sc.clock_setup.oscillator = sysctl::Oscillator::Main(
188 | sysctl::CrystalFrequency::_16mhz,
189 | sysctl::SystemClock::UsePll(sysctl::PllOutputFrequency::_80_00mhz),
190 | );
191 | // Configure the PLL with those settings
192 | let clocks = sc.clock_setup.freeze();
193 |
194 | // Wrap up the GPIO_PORTA struct into an object with a higher-layer API.
195 | // Note it needs to borrow `sc.power_control` so it can power up the GPIO
196 | // peripheral automatically.
197 | let mut porta = p.GPIO_PORTA.split(&sc.power_control);
198 |
199 | // Activate the UART.
200 | let uart = Serial::uart0(
201 | p.UART0,
202 | // The transmit pin
203 | porta
204 | .pa1
205 | .into_af_push_pull::(&mut porta.control),
206 | // The receive pin
207 | porta
208 | .pa0
209 | .into_af_push_pull::(&mut porta.control),
210 | // No RTS or CTS required
211 | (),
212 | (),
213 | // The baud rate
214 | 115200_u32.bps(),
215 | // Output handling
216 | NewlineMode::SwapLFtoCRLF,
217 | // We need the clock rates to calculate the baud rate divisors
218 | &clocks,
219 | // We need this to power up the UART peripheral
220 | &sc.power_control,
221 | );
222 |
223 | loop {
224 | writeln!(uart, "Hello, World!\r\n").unwrap();
225 | }
226 | }
227 | ```
228 |
--------------------------------------------------------------------------------
/src/static-guarantees/design-contracts.md:
--------------------------------------------------------------------------------
1 | # 设计合同
2 |
3 | 再上一章, 我们写了一个*不符合*设计合同的接口. 让我们再看一下我们假设的 GPIO 寄存器配置:
4 |
5 | | Name | Bit Number(s) | Value | Meaning | Notes |
6 | | ---: | ------------: | ----: | ------: | ----: |
7 | | enable | 0 | 0 | disabled | Disables the GPIO |
8 | | | | 1 | enabled | Enables the GPIO |
9 | | direction | 1 | 0 | input | Sets the direction to Input |
10 | | | | 1 | output | Sets the direction to Output |
11 | | input_mode | 2..3 | 00 | hi-z | Sets the input as high resistance |
12 | | | | 01 | pull-low | Input pin is pulled low |
13 | | | | 10 | pull-high | Input pin is pulled high |
14 | | | | 11 | n/a | Invalid state. Do not set |
15 | | output_mode | 4 | 0 | set-low | Output pin is driven low |
16 | | | | 1 | set-high | Output pin is driven high |
17 | | input_status | 5 | x | in-val | 0 if input is < 1.5v, 1 if input >= 1.5v |
18 |
19 | 如果我们改为在使用硬件前先检查状态, 在运行时强制执行我们的设计合同, 我们可能会写出如下的替代:
20 |
21 | ```rust
22 | /// GPIO interface
23 | struct GpioConfig {
24 | /// GPIO Configuration structure generated by svd2rust
25 | periph: GPIO_CONFIG,
26 | }
27 |
28 | impl GpioConfig {
29 | pub fn set_enable(&mut self, is_enabled: bool) {
30 | self.periph.modify(|_r, w| {
31 | w.enable().set_bit(is_enabled)
32 | });
33 | }
34 |
35 | pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> {
36 | if self.periph.read().enable().bit_is_clear() {
37 | // Must be enabled to set direction
38 | return Err(());
39 | }
40 |
41 | self.periph.modify(|r, w| {
42 | w.direction().set_bit(is_output)
43 | });
44 |
45 | Ok(())
46 | }
47 |
48 | pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> {
49 | if self.periph.read().enable().bit_is_clear() {
50 | // Must be enabled to set input mode
51 | return Err(());
52 | }
53 |
54 | if self.periph.read().direction().bit_is_set() {
55 | // Direction must be input
56 | return Err(());
57 | }
58 |
59 | self.periph.modify(|_r, w| {
60 | w.input_mode().variant(variant)
61 | });
62 |
63 | Ok(())
64 | }
65 |
66 | pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> {
67 | if self.periph.read().enable().bit_is_clear() {
68 | // Must be enabled to set output status
69 | return Err(());
70 | }
71 |
72 | if self.periph.read().direction().bit_is_clear() {
73 | // Direction must be output
74 | return Err(());
75 | }
76 |
77 | self.periph.modify(|_r, w| {
78 | w.output_mode.set_bit(is_high)
79 | });
80 |
81 | Ok(())
82 | }
83 |
84 | pub fn get_input_status(&self) -> Result {
85 | if self.periph.read().enable().bit_is_clear() {
86 | // Must be enabled to get status
87 | return Err(());
88 | }
89 |
90 | if self.periph.read().direction().bit_is_set() {
91 | // Direction must be input
92 | return Err(());
93 | }
94 |
95 | Ok(self.periph.read().input_status().bit_is_set())
96 | }
97 | }
98 | ```
99 |
100 | 因为我们给硬件加了强制约束, 所以在结束的时候要进行大量的运行时检查, 这浪费时间又浪费性能, 并且让人看着难受.
101 |
102 | ## 状态类型
103 |
104 | 但是如果反过来, 我们使用 Rust 的类型系统来执行转换规则的话, 看看这个例子:
105 |
106 | ```rust
107 | /// GPIO interface
108 | struct GpioConfig {
109 | /// GPIO Configuration structure generated by svd2rust
110 | periph: GPIO_CONFIG,
111 | enabled: ENABLED,
112 | direction: DIRECTION,
113 | mode: MODE,
114 | }
115 |
116 | // Type states for MODE in GpioConfig
117 | struct Disabled;
118 | struct Enabled;
119 | struct Output;
120 | struct Input;
121 | struct PulledLow;
122 | struct PulledHigh;
123 | struct HighZ;
124 | struct DontCare;
125 |
126 | /// These functions may be used on any GPIO Pin
127 | impl GpioConfig {
128 | pub fn into_disabled(self) -> GpioConfig {
129 | self.periph.modify(|_r, w| w.enable.disabled());
130 | GpioConfig {
131 | periph: self.periph,
132 | enabled: Disabled,
133 | direction: DontCare,
134 | mode: DontCare,
135 | }
136 | }
137 |
138 | pub fn into_enabled_input(self) -> GpioConfig {
139 | self.periph.modify(|_r, w| {
140 | w.enable.enabled()
141 | .direction.input()
142 | .input_mode.high_z()
143 | });
144 | GpioConfig {
145 | periph: self.periph,
146 | enabled: Enabled,
147 | direction: Input,
148 | mode: HighZ,
149 | }
150 | }
151 |
152 | pub fn into_enabled_output(self) -> GpioConfig {
153 | self.periph.modify(|_r, w| {
154 | w.enable.enabled()
155 | .direction.output()
156 | .input_mode.set_high()
157 | });
158 | GpioConfig {
159 | periph: self.periph,
160 | enabled: Enabled,
161 | direction: Output,
162 | mode: DontCare,
163 | }
164 | }
165 | }
166 |
167 | /// This function may be used on an Output Pin
168 | impl GpioConfig {
169 | pub fn set_bit(&mut self, set_high: bool) {
170 | self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
171 | }
172 | }
173 |
174 | /// These methods may be used on any enabled input GPIO
175 | impl GpioConfig {
176 | pub fn bit_is_set(&self) -> bool {
177 | self.periph.read().input_status.bit_is_set()
178 | }
179 |
180 | pub fn into_input_high_z(self) -> GpioConfig {
181 | self.periph.modify(|_r, w| w.input_mode().high_z());
182 | GpioConfig {
183 | periph: self.periph,
184 | enabled: Enabled,
185 | direction: Input,
186 | mode: HighZ,
187 | }
188 | }
189 |
190 | pub fn into_input_pull_down(self) -> GpioConfig {
191 | self.periph.modify(|_r, w| w.input_mode().pull_low());
192 | GpioConfig {
193 | periph: self.periph,
194 | enabled: Enabled,
195 | direction: Input,
196 | mode: PulledLow,
197 | }
198 | }
199 |
200 | pub fn into_input_pull_up(self) -> GpioConfig {
201 | self.periph.modify(|_r, w| w.input_mode().pull_high());
202 | GpioConfig {
203 | periph: self.periph,
204 | enabled: Enabled,
205 | direction: Input,
206 | mode: PulledHigh,
207 | }
208 | }
209 | }
210 | ```
211 |
212 | 现在让我们看看这段代码怎么用:
213 |
214 | ```rust
215 | /*
216 | * Example 1: Unconfigured to High-Z input
217 | */
218 | let pin: GpioConfig = get_gpio();
219 |
220 | // Can't do this, pin isn't enabled!
221 | // pin.into_input_pull_down();
222 |
223 | // Now turn the pin from unconfigured to a high-z input
224 | let input_pin = pin.into_enabled_input();
225 |
226 | // Read from the pin
227 | let pin_state = input_pin.bit_is_set();
228 |
229 | // Can't do this, input pins don't have this interface!
230 | // input_pin.set_bit(true);
231 |
232 | /*
233 | * Example 2: High-Z input to Pulled Low input
234 | */
235 | let pulled_low = input_pin.into_input_pull_down();
236 | let pin_state = pulled_low.bit_is_set();
237 |
238 | /*
239 | * Example 3: Pulled Low input to Output, set high
240 | */
241 | let output_pin = pulled_low.into_enabled_output();
242 | output_pin.set_bit(true);
243 |
244 | // Can't do this, output pins don't have this interface!
245 | // output_pin.into_input_pull_down();
246 | ```
247 |
248 | 这绝对是存储引脚状态的方便方法, 但是我们为什么要这么做? 为什么这么做比我们写一个 `GpioConfig` 的 `enum` 要好?
249 |
250 | ## 编译时函数安全
251 |
252 | 因为我们在编译时完全执行设计约束, 所以不会产生运行时成本. 当引脚处于输入状态时, 无法设置输出模式. 相反, 你必须通过改变状态来把它转换为输出引脚, 然后设置输出模式. 正因为如此, 在编译时检查状态, 不会造成运行时的性能损失.
253 |
254 | 而且, 因为这些状态是由类型系统强制约束的, 所以使用者不会出错, 如果他们尝试做一些非法的状态转换, 编译就无法通过!
255 |
--------------------------------------------------------------------------------
/src/start/harware.md:
--------------------------------------------------------------------------------
1 | # 硬件
2 |
3 | 现在你应该熟悉了工具与开发过程.
4 | 在本节我们来试试真正的硬件.
5 | 该过程基本不变.让我们开始:
6 |
7 | ## 了解你的硬件
8 |
9 | 在我们开始之前你需要了解硬件的特点以便于配置项目:
10 |
11 | - ARM内核. e.g. Cortex-M3.
12 | - ARM内核有FPU吗? Cortex-M4**F**和Cortex-M7**F**有.
13 | - 目标设备有多大闪存和RAM? e.g. 256KiB闪存32KiB内存.
14 | - 闪存和RAM在的地址在多少? e.g. RAM通常位于`0x2000_0000`.
15 |
16 | 你可以在用户手册和数据手册中找到这些信息.
17 |
18 | 在本届我们使用我们的参考硬件STM32F3DISCOVERY.
19 | 这块板子有一个STM32F303VCT6.这块MCU有:
20 |
21 | - 一个带有单精度FPU的Cortex-M4F内核
22 | - 位于0x0800_0000的256KiB闪存
23 | - 位于0x2000_0000的40KiB内存(还有另一个RAM区域,为了简单我们忽略)
24 |
25 | ## 配置
26 |
27 | 我们从一个新的模板实例开始.
28 | 关于如何使用`cargo-generate`请参考[上一章节QEMU]
29 |
30 | [上一章节QEMU]: qemu.md
31 |
32 | ``` console
33 | $ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
34 | Project Name: app
35 | Creating project called `app`...
36 | Done! New project created /tmp/app
37 |
38 | $ cd app
39 | ```
40 |
41 | 第一步是在`.cargo/config`中设置默认编译目标.
42 |
43 | ``` console
44 | $ tail -n5 .cargo/config
45 | ```
46 |
47 | ``` toml
48 | # Pick ONE of these compilation targets
49 | # target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
50 | # target = "thumbv7m-none-eabi" # Cortex-M3
51 | # target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
52 | target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
53 | ```
54 |
55 | 我们使用`thumbv7em-none-eabihf`,因为它包含Cortex-M4F内核.
56 |
57 | 第二步是把内存区域信息输入到`memory.x`中.
58 |
59 | ``` console
60 | $ cat memory.x
61 | /* Linker script for the STM32F303VCT6 */
62 | MEMORY
63 | {
64 | /* NOTE 1 K = 1 KiBi = 1024 bytes */
65 | FLASH : ORIGIN = 0x08000000, LENGTH = 256K
66 | RAM : ORIGIN = 0x20000000, LENGTH = 40K
67 | }
68 | ```
69 |
70 | > **NOTE**如果你因为某些原因修改了`memory.x`,并且之前做过了编译,
71 | > 那你需要执行`cargo clean`再执行`cargo build`,因为`cargo build`并不会追踪`memory.x`的变化.
72 |
73 | 我们还用hello这个例子, 但是首先先做一点小改动.
74 |
75 | 在`examples/hello.rs`中,确保`debug::exit()`被注释掉或者删掉.它只是为了运行QEMU而存在的.
76 |
77 | ```rust,ignore
78 | #[entry]
79 | fn main() -> ! {
80 | hprintln!("Hello, world!").unwrap();
81 |
82 | // exit QEMU
83 | // NOTE do not run this on hardware; it can corrupt OpenOCD state
84 | // debug::exit(debug::EXIT_SUCCESS);
85 |
86 | loop {}
87 | }
88 | ```
89 |
90 | 现在你可以用`cargo build`进行交叉编译,并且像之前一样用`cargo-binutils`查看信息.
91 | `cortex-m-rt`这个库包含了一切能让你芯片运行的魔法,它很有帮助,因为几乎所有的Cortex-M CPU都可以用相同的方式引导.
92 |
93 | ``` console
94 | $ cargo build --example hello
95 | ```
96 |
97 | ## 调试
98 |
99 | 调试过程看起来有些不同了.事实上,第一步根据目标设备不同也有不同.这一节中我们会展示在STM32DISCOBVERY上debug的步骤.这仅供参考,有关设备的调试请参考[the Debugonomicon](https://github.com/rust-embedded/debugonomicon).
100 |
101 | 和以前一样,我们进行远程调试,客户端是GDB.但是这次服务端则是OpenOCD.
102 |
103 | 像之前在[验证安装]中所做的一样,将板子连接到电脑,然后检查ST-LINK.
104 |
105 | [验证安装]: ../intro/install/verify.md
106 |
107 | 在终端上运行OpenOCD以连接到ST-LINK.从模板的根目录运行此命令;`OpenOCD`会使用`openocd.cfg`,这里面声明了使用什么接口,连接什么设备.
108 |
109 | ``` console
110 | $ cat openocd.cfg
111 | ```
112 |
113 | ``` text
114 | # Sample OpenOCD configuration for the STM32F3DISCOVERY development board
115 |
116 | # Depending on the hardware revision you got you'll have to pick ONE of these
117 | # interfaces. At any time only one interface should be commented out.
118 |
119 | # Revision C (newer revision)
120 | source [find interface/stlink.cfg]
121 |
122 | # Revision A and B (older revisions)
123 | # source [find interface/stlink-v2.cfg]
124 |
125 | source [find target/stm32f3x.cfg]
126 | ```
127 |
128 | > **NOTE** 如果你在用旧版本的DISCOVERY板子,你应该修改一下`openocd.cfg`来使用`interface/stlink-v2.cfg`
129 |
130 | ``` console
131 | $ openocd
132 | Open On-Chip Debugger 0.10.0
133 | Licensed under GNU GPL v2
134 | For bug reports, read
135 | http://openocd.org/doc/doxygen/bugs.html
136 | Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
137 | adapter speed: 1000 kHz
138 | adapter_nsrst_delay: 100
139 | Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
140 | none separate
141 | Info : Unable to match requested speed 1000 kHz, using 950 kHz
142 | Info : Unable to match requested speed 1000 kHz, using 950 kHz
143 | Info : clock speed 950 kHz
144 | Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
145 | Info : using stlink api v2
146 | Info : Target voltage: 2.913879
147 | Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints
148 | ```
149 |
150 | 同样在根目录下另起一个终端运行GDB.
151 |
152 | ``` console
153 | $ -q target/thumbv7em-none-eabihf/debug/examples/hello
154 | ```
155 |
156 | 先一步连接GDB到OpenOCD.
157 |
158 | ``` console
159 | (gdb) target remote :3333
160 | Remote debugging using :3333
161 | 0x00000000 in ?? ()
162 | ```
163 |
164 | 现在使用`load`命令*烧录*程序到mcu.
165 |
166 | ``` console
167 | (gdb) load
168 | Loading section .vector_table, size 0x400 lma 0x8000000
169 | Loading section .text, size 0x1e70 lma 0x8000400
170 | Loading section .rodata, size 0x61c lma 0x8002270
171 | Start address 0x800144e, load size 10380
172 | Transfer rate: 17 KB/sec, 3460 bytes/write.
173 | ```
174 |
175 | 现在程序被加载了.之前程序使用semihosting,因此在我们进行任何semihosting操作时,应该先告诉OpenOCD启用semihosting.可以使用`monitor`命令.
176 |
177 | ``` console
178 | (gdb) monitor arm semihosting enable
179 | semihosting is enabled
180 | ```
181 |
182 | > 你也可以使用`monitor help`查看所用OpenOCD命令.
183 |
184 | 前之前那样给`main`加断点并执行`continue`
185 |
186 | ``` console
187 | (gdb) break main
188 | Breakpoint 1 at 0x8000d18: file examples/hello.rs, line 15.
189 |
190 | (gdb) continue
191 | Continuing.
192 | Note: automatically using hardware breakpoints for read-only addresses.
193 |
194 | Breakpoint 1, main () at examples/hello.rs:15
195 | 15 let mut stdout = hio::hstdout().unwrap();
196 | ```
197 |
198 | > **NOTE** 如果在发出上面的`continue`命令后GDB阻塞了终端而不是到达断点,则你可能要仔细检查一下是否已为您的设备正确设置了`memory.x`文件中的存储区域信息(起始位置和长度).
199 |
200 | 使用`next`继续程序,应该会有和之前相同的结果.
201 |
202 | ``` console
203 | (gdb) next
204 | 16 writeln!(stdout, "Hello, world!").unwrap();
205 |
206 | (gdb) next
207 | 19 debug::exit(debug::EXIT_SUCCESS);
208 | ```
209 |
210 | 在这我们应该看到在OpenOCD的控制台上出现了"Hello, world!"
211 |
212 | ``` console
213 | $ openocd
214 | (..)
215 | Info : halted: PC: 0x08000e6c
216 | Hello, world!
217 | Info : halted: PC: 0x08000d62
218 | Info : halted: PC: 0x08000d64
219 | Info : halted: PC: 0x08000d66
220 | Info : halted: PC: 0x08000d6a
221 | Info : halted: PC: 0x08000a0c
222 | Info : halted: PC: 0x08000d70
223 | Info : halted: PC: 0x08000d72
224 | ```
225 |
226 | 发出另一个`next`命令会使程序执行`debug::exit()`.这会触发断点并终止程序:
227 |
228 | ``` console
229 | (gdb) next
230 |
231 | Program received signal SIGTRAP, Trace/breakpoint trap.
232 | 0x0800141a in __syscall ()
233 | ```
234 |
235 | 这也会使以下内容出现在OpenOCD控制台上:
236 |
237 | ``` console
238 | $ openocd
239 | (..)
240 | Info : halted: PC: 0x08001188
241 | semihosting: *** application exited ***
242 | Warn : target not halted
243 | Warn : target not halted
244 | target halted due to breakpoint, current mode: Thread
245 | xPSR: 0x21000000 pc: 0x08000d76 msp: 0x20009fc0, semihosting
246 | ```
247 |
248 | 但是,mcu上的进程不没有终止,你可以使用`continue`或类似的命令恢复进程.
249 |
250 | 你现在可以用`quit`来退出GDB
251 |
252 | ``` console
253 | (gdb) quit
254 | ```
255 |
256 | 现在调试需要更多的步骤了,那让我们来把这些步骤打包成一个叫`openocd.gdb`的GDB脚本.
257 | 这个文件在`cargo generate`步骤中已经生成了,按理说不用做修改就能用.让我们看一下:
258 |
259 | ``` console
260 | $ cat openocd.gdb
261 | ```
262 |
263 | ``` text
264 | target extended-remote :3333
265 |
266 | # print demangled symbols
267 | set print asm-demangle on
268 |
269 | # detect unhandled exceptions, hard faults and panics
270 | break DefaultHandler
271 | break HardFault
272 | break rust_begin_unwind
273 |
274 | monitor arm semihosting enable
275 |
276 | load
277 |
278 | # start the process but immediately halt the processor
279 | stepi
280 | ```
281 |
282 | 现在运行` -x openocd.gdb target/thumbv7em-none-eabihf/debug/examples/hello`会自动连接GDB到OpenOCD,启动semihosting,然后烧录程序并启动.
283 |
284 | ``` console
285 | $ head -n10 .cargo/config
286 | ```
287 |
288 | ``` toml
289 | [target.thumbv7m-none-eabi]
290 | # uncomment this to make `cargo run` execute programs on QEMU
291 | # runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
292 |
293 | [target.'cfg(all(target_arch = "arm", target_os = "none"))']
294 | # uncomment ONE of these three option to make `cargo run` start a GDB session
295 | # which option to pick depends on your system
296 | runner = "arm-none-eabi-gdb -x openocd.gdb"
297 | # runner = "gdb-multiarch -x openocd.gdb"
298 | # runner = "gdb -x openocd.gdb"
299 | ```
300 |
301 | ``` console
302 | $ cargo run --example hello
303 | (..)
304 | Loading section .vector_table, size 0x400 lma 0x8000000
305 | Loading section .text, size 0x1e70 lma 0x8000400
306 | Loading section .rodata, size 0x61c lma 0x8002270
307 | Start address 0x800144e, load size 10380
308 | Transfer rate: 17 KB/sec, 3460 bytes/write.
309 | (gdb)
310 | ```
311 |
--------------------------------------------------------------------------------
/src/start/qemu.md:
--------------------------------------------------------------------------------
1 | # QEMU
2 |
3 | 我们要开始给[LM3S6965]编程, [LM3S6965]是一个Cortex-M3微控制器.
4 | 我们选择这个作为开始是应为它能被QEMU[模拟](https://wiki.qemu.org/Documentation/Platforms/ARM#Supported_in_qemu-system-arm),所以你不必在本部分玩弄硬件,专心与工具与编程.
5 |
6 | [LM3S6965]: http://www.ti.com/product/LM3S6965
7 |
8 | **Important**
9 | 在本教程中我们使用"app"作为项目名.
10 | 当你看到"app"这个词的时候,你应该把它换成你给你自己的项目起的名.
11 | 或者,你就可以把你的项目名设成"app".
12 |
13 | ## 创建一个不含标准库的Rust程序
14 |
15 | 我们会使用[`cortex-m-quickstart`]项目模板来生成一个新项目.
16 | 新创建的项目会包含一个基础结构:
17 | 一个对嵌入式Rust程序的好的开始.
18 | 这个项目还额外包括一个有着几个不同例子的`example`文件夹.
19 |
20 | [`cortex-m-quickstart`]: https://github.com/rust-embedded/cortex-m-quickstart
21 |
22 | ### 使用 `cargo-generate`
23 |
24 | 首先安装 cargo-generate
25 |
26 | ``` console
27 | cargo install cargo-generate
28 | ```
29 |
30 | 然后生成一个新项目
31 |
32 | ```console
33 | cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
34 | ```
35 |
36 | ```text
37 | Project Name: app
38 | Creating project called `app`...
39 | Done! New project created /tmp/app
40 | ```
41 |
42 | ```console
43 | cd app
44 | ```
45 |
46 | ### 使用`git`
47 |
48 | 克隆仓库
49 |
50 | ```console
51 | git clone https://github.com/rust-embedded/cortex-m-quickstart app
52 | cd app
53 | ```
54 |
55 | 然后修改`Cargo.toml`中的占位符
56 |
57 | ```toml
58 | [package]
59 | authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
60 | edition = "2018"
61 | name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
62 | version = "0.1.0"
63 |
64 | # ..
65 |
66 | [[bin]]
67 | name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
68 | test = false
69 | bench = false
70 | ```
71 |
72 | ### 两者都不用
73 |
74 | 下载最新的`cortex-m-quickstart`然后解压
75 |
76 | ```console
77 | curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
78 | unzip master.zip
79 | mv cortex-m-quickstart-master app
80 | cd app
81 | ```
82 |
83 | 或者你可以打开[`cortex-m-quickstart`],然后点绿色的"Clone or download",然后选择"Download ZIP".
84 |
85 | 然后按照第二部分"使用git"中修改占位符.
86 |
87 | ## 程序概览
88 |
89 | 为了方便,`src/main.rs`中已经有了很重要的部分:
90 |
91 | ```rust,ignore
92 | #![no_std]
93 | #![no_main]
94 |
95 | use panic_halt as _;
96 |
97 | use cortex_m_rt::entry;
98 |
99 | #[entry]
100 | fn main() -> ! {
101 | loop {
102 | // your code goes here
103 | }
104 | }
105 | ```
106 |
107 | 这个和标准的Rust程序不太一样,咱们来凑近一点看看.
108 |
109 | `#![no_std]`声明这个程序*不会*连接到`std`标准库.
110 | 作为代替会连接到`core`
111 |
112 | `#![no_main]`声明这个程序不会使用大部分Rust使用的main函数接口.
113 | 使用`no_main`主要原因是在`no_std`中使用`main`需要每夜版的Rust.
114 |
115 | `use panic_halt as _;`.这个库提供一个定义panic行为的`panic_handler`.
116 | 我们会在[Panicing](panicing.md)章节中讨论更多细节.
117 |
118 | [`#[entry]`][entry]是一个由[`cortex-m-rt`]提供的属性,用来标记程序的入口.
119 | 当我们不用标准的`main`入口我们就需要其他的方式声明程序的入口,就是`#[entry]`.
120 |
121 | [entry]: https://docs.rs/cortex-m-rt-macros/latest/cortex_m_rt_macros/attr.entry.html
122 | [`cortex-m-rt`]: https://crates.io/crates/cortex-m-rt
123 |
124 | `fn main() -> !`.我们的程序*只会*运行在目标硬件上,所以我们不希望他停止!
125 | 我们使用一个[发散函数](https://doc.rust-lang.org/rust-by-example/fn/diverging.html) (`->!`符号)来确保编译期不会出问题.
126 |
127 | ## 交叉编译
128 |
129 | 下一步是为Cortex-M3架构进行*交叉*编译.
130 | 在知道编译目标时(`$TRIPLE`)使用`cargo build --target $TRIPLE`会很方便.
131 | 很幸运,模板中的`.cargo/config`已经提供了答案.
132 |
133 | ```console
134 | tail -n6 .cargo/config
135 | ```
136 |
137 | ```toml
138 | [build]
139 | # Pick ONE of these compilation targets
140 | # target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+
141 | target = "thumbv7m-none-eabi" # Cortex-M3
142 | # target = "thumbv7em-none-eabi" # Cortex-M4 and Cortex-M7 (no FPU)
143 | # target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
144 | ```
145 |
146 | 为了给Cortex-M3架构交叉编译,我们要使用`thumbv7m-none-eabi`.
147 | 这个编译目标并不是自带的,如果你没有的话,现在就装:
148 |
149 | ``` console
150 | rustup target add thumbv7m-none-eabi
151 | ```
152 |
153 | 如果`thumbv7m-none-eabi`已经`.cargo/config`中设为默认值,那下面这两条命令是一样的
154 |
155 | ```console
156 | cargo build --target thumbv7m-none-eabi
157 | cargo build
158 | ```
159 |
160 | ## 检查
161 |
162 | 现在我们在`target/thumbv7m-none-eabi/debug/app`有一个非本机的ELF二进制文件.
163 | 我们可以用`cargo-binutils`来检查它.
164 |
165 | 使用`cargo-readobj`来查看ELF头来确认这是个给ARM的二进制文件.
166 |
167 | ``` console
168 | cargo readobj --bin app -- -file-headers
169 | ```
170 |
171 | 注意:
172 |
173 | * `--bin app`是个`target/$TRIPLE/debug/app`的语法糖
174 | * `--bin app`如果需要的话会重新编译
175 |
176 | ``` text
177 | ELF Header:
178 | Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
179 | Class: ELF32
180 | Data: 2's complement, little endian
181 | Version: 1 (current)
182 | OS/ABI: UNIX - System V
183 | ABI Version: 0x0
184 | Type: EXEC (Executable file)
185 | Machine: ARM
186 | Version: 0x1
187 | Entry point address: 0x405
188 | Start of program headers: 52 (bytes into file)
189 | Start of section headers: 153204 (bytes into file)
190 | Flags: 0x5000200
191 | Size of this header: 52 (bytes)
192 | Size of program headers: 32 (bytes)
193 | Number of program headers: 2
194 | Size of section headers: 40 (bytes)
195 | Number of section headers: 19
196 | Section header string table index: 18
197 | ```
198 |
199 | `cargo-size`可以打印二进制文件中连接器的部分.
200 |
201 | ```console
202 | cargo size --bin app --release -- -A
203 | ```
204 |
205 | 我们使用`--release`来获取优化的版本.
206 |
207 | ``` text
208 | app :
209 | section size addr
210 | .vector_table 1024 0x0
211 | .text 92 0x400
212 | .rodata 0 0x45c
213 | .data 0 0x20000000
214 | .bss 0 0x20000000
215 | .debug_str 2958 0x0
216 | .debug_loc 19 0x0
217 | .debug_abbrev 567 0x0
218 | .debug_info 4929 0x0
219 | .debug_ranges 40 0x0
220 | .debug_macinfo 1 0x0
221 | .debug_pubnames 2035 0x0
222 | .debug_pubtypes 1892 0x0
223 | .ARM.attributes 46 0x0
224 | .debug_frame 100 0x0
225 | .debug_line 867 0x0
226 | Total 14570
227 | ```
228 |
229 | > A refresher on ELF linker sections
230 | >
231 | > - `.text` contains the program instructions
232 | > - `.rodata` contains constant values like strings
233 | > - `.data` contains statically allocated variables whose initial values are
234 | > *not* zero
235 | > - `.bss` also contains statically allocated variables whose initial values
236 | > *are* zero
237 | > - `.vector_table` is a *non*-standard section that we use to store the vector
238 | > (interrupt) table
239 | > - `.ARM.attributes` and the `.debug_*` sections contain metadata and will
240 | > *not* be loaded onto the target when flashing the binary.
241 |
242 | **IMPORTANT**: ELF文件包含了类似Debug信息等等元数据,所以他们的在*磁盘上的大小*并*不能*
243 | 准确的反映烧录在硬件上的大小.*通常*使用`cargo-size`来检查二进制文件真正的大小
244 |
245 | `cargo-objdump`可用于反汇编二进制文件.
246 |
247 | ```console
248 | cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex
249 | ```
250 |
251 | > **NOTE** 如果以上命令报错`Unknown command line argument`,
252 | > 可以看看这个bug:
253 | >
254 | > **NOTE** 这个根据不同系统有所区别.新版本的rustc,LLVM还有库会生成不同的二进制文件.
255 | > 我们删节了一些说明,意识代码段变小.
256 |
257 | ```text
258 | app: file format ELF32-arm-little
259 |
260 | Disassembly of section .text:
261 | main:
262 | 400: bl #0x256
263 | 404: b #-0x4
264 |
265 | Reset:
266 | 406: bl #0x24e
267 | 40a: movw r0, #0x0
268 | < .. truncated any more instructions .. >
269 |
270 | DefaultHandler_:
271 | 656: b #-0x4
272 |
273 | UsageFault:
274 | 657: strb r7, [r4, #0x3]
275 |
276 | DefaultPreInit:
277 | 658: bx lr
278 |
279 | __pre_init:
280 | 659: strb r7, [r0, #0x1]
281 |
282 | __nop:
283 | 65a: bx lr
284 |
285 | HardFaultTrampoline:
286 | 65c: mrs r0, msp
287 | 660: b #-0x2
288 |
289 | HardFault_:
290 | 662: b #-0x4
291 |
292 | HardFault:
293 | 663:
294 | ```
295 |
296 | ## 运行
297 |
298 | 下一步我们要在QEMU上运行我们的嵌入式程序!
299 | 这次我们使用`hello`这个例子来搞事.
300 |
301 | 为了方便,如下是`example/hello.rs`的源码
302 |
303 | ```rust,ignore
304 | //! Prints "Hello, world!" on the host console using semihosting
305 |
306 | #![no_main]
307 | #![no_std]
308 |
309 | use panic_halt as _;
310 |
311 | use cortex_m_rt::entry;
312 | use cortex_m_semihosting::{debug, hprintln};
313 |
314 | #[entry]
315 | fn main() -> ! {
316 | hprintln!("Hello, world!").unwrap();
317 |
318 | // exit QEMU
319 | // NOTE do not run this on hardware; it can corrupt OpenOCD state
320 | debug::exit(debug::EXIT_SUCCESS);
321 |
322 | loop {}
323 | }
324 | ```
325 |
326 | 这个程序使用一个叫semihosting的东西来打印信息到*宿主机*.
327 | 等到了真正的硬件上,就需要一个调试会话才能用.
328 |
329 | 让我们来开始编译这个例子:
330 |
331 | ```console
332 | cargo build --example hello
333 | ```
334 |
335 | 产出的二进制文件在`target/thumbv7m-none-eabi/debug/examples/hello`.
336 |
337 | 为了在QEMU上运行这个应使用如下命令:
338 |
339 | ```console
340 | qemu-system-arm \
341 | -cpu cortex-m3 \
342 | -machine lm3s6965evb \
343 | -nographic \
344 | -semihosting-config enable=on,target=native \
345 | -kernel target/thumbv7m-none-eabi/debug/examples/hello
346 | ```
347 |
348 | ```text
349 | Hello, world!
350 | ```
351 |
352 | 这条命令应该在输出信息后成功推出(exit code = 0).
353 | 在\*nix上你可以用如下命令确认:
354 |
355 | ```console
356 | echo $?
357 | ```
358 |
359 | ```text
360 | 0
361 | ```
362 |
363 | 让我们来破解QEMU命令:
364 |
365 | - `qemu-system-arm`.这是QEMU模拟器.有几种不同的QEMU二进制文件;这个能对ARM机器进行完整的系统仿真
366 |
367 | - `-cpu cortex-m3`.这告诉QEMU去模拟一个Cortex-M3 CPU.指定CPU型号可以让我们捕获一些编译错误:
368 | 例如,运行为带有硬件FPU的Cortex-M4F编译的程序回事QEMU执行过程中出错.
369 |
370 | - `-machine lm3s6965evb`.这告诉QEMU去模拟LM3S6965EVB,一个包含LM3S6965的评估开发板
371 |
372 | - `-nographic`.这告诉QEMu不要去启动GUI.
373 |
374 | - `-semihosting-config (..)`.这让QEMU启动semihosting. Semihosting允许仿真设备使用主机的stdout, stderr和stdin,并且在主机上创建文件
375 |
376 | - `-kernel $file`.这告诉QEMU运行哪个二进制文件
377 |
378 | 输入这么长的命令太麻烦了!我们可以在`.cargo/config`中配置一个自定义的运行指令.
379 | 去掉注释:
380 |
381 | ```console
382 | head -n3 .cargo/config
383 | ```
384 |
385 | ```toml
386 | [target.thumbv7m-none-eabi]
387 | # uncomment this to make `cargo run` execute programs on QEMU
388 | runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
389 | ```
390 |
391 | 这个运行器只针对`thumbv7m-none-eabi`,现在执行`cargo run`会编译程序并且使用QEMU运行.
392 |
393 | ```console
394 | cargo run --example hello --release
395 | ```
396 |
397 | ```text
398 | Compiling app v0.1.0 (file:///tmp/app)
399 | Finished release [optimized + debuginfo] target(s) in 0.26s
400 | Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
401 | Hello, world!
402 | ```
403 |
404 | ## 调试
405 |
406 | 调试对于嵌入式开发至关重要.让我们看看它是如何完成的.
407 |
408 | 调试嵌入式设备设计*远程*调试,因为我们要调试的程序不会运行在运行调试器(GDB or LLDB)所在的机器上
409 |
410 | 远程调试涉及客户端与服务端.在QEMU设置中,客户端是GDB(LLDB)进程,服务端则是运行嵌入式应用的QEMU程序.
411 |
412 | 在本节中,我们使用已经编译好的`hello`例子.
413 |
414 | 调试的第一步是以调试模式启动QEMU:
415 |
416 | ```console
417 | qemu-system-arm \
418 | -cpu cortex-m3 \
419 | -machine lm3s6965evb \
420 | -nographic \
421 | -semihosting-config enable=on,target=native \
422 | -gdb tcp::3333 \
423 | -S \
424 | -kernel target/thumbv7m-none-eabi/debug/examples/hello
425 | ```
426 |
427 | 这条命令不会在控制台上打印任何内容并会阻塞终端.这次我们额外传递两个命令行参数:
428 |
429 | - `-gdb tcp::3333`.这条命令告诉QEMU在TCP 3333上等待GDB链接
430 |
431 | - `-S`.这条命令告诉QEMU在开始时冻结机器.如果没有这条命令,
432 | 还没等我们打开调试器,程序就已经运行到了末尾.
433 |
434 | 下一步我们在另一个终端中启动GDB,并让它加载示例的调试符:
435 |
436 | ```console
437 | gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello
438 | ```
439 |
440 | **注意**取决于你在安装章节安装了哪一个,你可能需要其他版本的gdb而不是`gdb-multiarch`.
441 | 这可能是`arm-none-eabi-gdb`或就是`gdb`.
442 |
443 | 然后在GDB Shell中连接到QEMU,它正在TCP3333上等待连接.
444 |
445 | ```console
446 | target remote :3333
447 | ```
448 |
449 | ```text
450 | Remote debugging using :3333
451 | Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
452 | 473 pub unsafe extern "C" fn Reset() -> ! {
453 | ```
454 |
455 | 你会看到该过程已暂停,并且程序计数器指向一个名为`Reset`的函数.
456 | 那就是reset handler,MCU在启动时执行的.
457 |
458 | > 注意在某些设置中,gdb可能会提示如下信息,而不是显示`Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473`
459 | >
460 | >`core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254`
461 | > ` src/libcore/num/bignum.rs: No such file or directory.`
462 | >
463 | > 这是一个已知的故障,你可以放心的忽略它,最有可能出现在Reset()处
464 |
465 | 这个reset handler最终调用我们的main函数.让我们使用断点与continue跳过.要设置断点的话,首先让我们用`list`看一下在哪断点.
466 |
467 | ```console
468 | list main
469 | ```
470 |
471 | 这会展示`example/hello.rs`的源码
472 |
473 | ```text
474 | 6 use panic_halt as _;
475 | 7
476 | 8 use cortex_m_rt::entry;
477 | 9 use cortex_m_semihosting::{debug, hprintln};
478 | 10
479 | 11 #[entry]
480 | 12 fn main() -> ! {
481 | 13 hprintln!("Hello, world!").unwrap();
482 | 14
483 | 15 // exit QEMU
484 | ```
485 |
486 | 我们想要在第13行,"Hello world!"后加一个断点.我们可以使用`break`命令
487 |
488 | ```console
489 | break 13
490 | ```
491 |
492 | 现在,我们可以使用`continue`命令让GDB运行我们的main函数
493 |
494 | ```console
495 | continue
496 | ```
497 |
498 | ```text
499 | Continuing.
500 |
501 | Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
502 | 13 hprintln!("Hello, world!").unwrap();
503 | ```
504 |
505 | 我们现在很接近输出"Hello, world!"的那一行代码.让我们用`next`继续.
506 |
507 | ``` console
508 | next
509 | ```
510 |
511 | ```text
512 | 16 debug::exit(debug::EXIT_SUCCESS);
513 | ```
514 |
515 | 这时你应该看到"Hello, world!"已经在运行`qemu-system-arm`的终端上被打印出来了.
516 |
517 | ```text
518 | $ qemu-system-arm (..)
519 | Hello, world!
520 | ```
521 |
522 | 继续使用`next`会结束QEMU进程.
523 |
524 | ```console
525 | next
526 | ```
527 |
528 | ```text
529 | [Inferior 1 (Remote target) exited normally]
530 | ```
531 |
532 | 现在你可以退出GDB会话.
533 |
534 | ``` console
535 | quit
536 | ```
537 |
--------------------------------------------------------------------------------
/src/assets/rust_layers.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/concurrency/index.md:
--------------------------------------------------------------------------------
1 | # 并发
2 |
3 | 并发发生在你程序的不同的部分在不同时间发生或者无需执行时。
4 | 在嵌入式开发中,包括:
5 |
6 | * 中断处理,当相关中断发生时
7 | * 各种多线程,当你的微处理器交换线程时
8 | * 在某些系统中,多核处理器中每个核都可以独立运行
9 |
10 | 由于许多嵌入式应用都需要处理中断,所以并发迟早都会发生,同时也最容易出现许多奇怪难懂的 BUG 。
11 | 幸运的是, Rust 提供了许多抽象与安全保证来帮助我们写出正确的代码。
12 |
13 | ## 无并发
14 |
15 | 最简单的并发就是没有并发:你的应用就一个循环一直在运行,也没有中断。
16 | 有时候这就足够解决手头上的问题了!
17 | 典型的情况时是你的循环读取一些输入然后做一些处理进行输出。
18 |
19 | ```rust, ignore
20 | #[entry]
21 | fn main() {
22 | let peripherals = setup_peripherals();
23 | loop {
24 | let inputs = read_inputs(&peripherals);
25 | let outputs = process(inputs);
26 | write_outputs(&peripherals, outputs);
27 | }
28 | }
29 | ```
30 |
31 | 因为没有并发,所以你也没必要担心在程序的不同部分分享数据或是同步外设的访问权限。
32 | 如果你能用这种方法解决问题那很好。
33 |
34 | ## 全局可变数据
35 |
36 | 不像非嵌入式的 Rust ,我们通常不会创建堆然后把对数据的引用传递给新建的线程。
37 | 相反我们的中断处理函数可能在任意时刻被调用,并且必须知道如何访问我们正在使用的内存。
38 | 在底层上这意味着我们必须静态分配可变内存,让这块内存可以被中断和主程序引用。
39 |
40 | 在 Rust 中,像 [`static mut`] 这样的变量是读写不安全的,因为没有特殊照顾的情况下,这可能会出现竞态,
41 | 即你对数据的访问在半路上被同样要访问该数据的中断打断。
42 |
43 | [`static mut`]: https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable
44 |
45 | 举个例子,设想一下有个应用,它用一个计数器测量在一秒内一个信号有多少上升沿(频率计):
46 |
47 | ```rust,ignore
48 | static mut COUNTER: u32 = 0;
49 |
50 | #[entry]
51 | fn main() -> ! {
52 | set_timer_1hz();
53 | let mut last_state = false;
54 | loop {
55 | let state = read_signal_level();
56 | if state && !last_state {
57 | // DANGER - Not actually safe! Could cause data races.
58 | unsafe { COUNTER += 1 };
59 | }
60 | last_state = state;
61 | }
62 | }
63 |
64 | #[interrupt]
65 | fn timer() {
66 | unsafe { COUNTER = 0; }
67 | }
68 | ```
69 |
70 | 定时器中断每秒把计数器归零。
71 | 同时主循环还在不停的测量信号,有一个上升沿就 +1 。
72 | 我们使用 `unsafe` 来操作 `COUNTER` ,因为它是个 `static mut` ,这意味着我们向编译器保证我们不会做任何未定义行为。
73 | 你能看出来这有竞态吗?
74 | `COUNTER` 的增加 _不是_ 原子的 -- 事实上,在绝大多数嵌入式平台上,这个操作会被分成读取、增加、保存三个步骤。
75 | 如果中断发生在读取之后,保存之前,那清零的操作就会被忽略,我们便会一个周期计两次。
76 |
77 | ## 临界区
78 |
79 | 那么,我们要怎么做?一个简单的方法是使用 _临界区_ ,在这中断被关闭。
80 | 通过在 `main` 中使用一个临界区来包裹 `COUNTER` 我们可以保证在我们完成增加 `COUNTER` 前定时器中断不会触发。
81 |
82 | ```rust,ignore
83 | static mut COUNTER: u32 = 0;
84 |
85 | #[entry]
86 | fn main() -> ! {
87 | set_timer_1hz();
88 | let mut last_state = false;
89 | loop {
90 | let state = read_signal_level();
91 | if state && !last_state {
92 | // New critical section ensures synchronised access to COUNTER
93 | cortex_m::interrupt::free(|_| {
94 | unsafe { COUNTER += 1 };
95 | });
96 | }
97 | last_state = state;
98 | }
99 | }
100 |
101 | #[interrupt]
102 | fn timer() {
103 | unsafe { COUNTER = 0; }
104 | }
105 | ```
106 |
107 | 在这个例子中,我们使用 `cortex_m::interrupt::free` ,其他平台也有类似的步骤。
108 | 这等效于禁用中断,执行代码,重启中断。
109 |
110 | 注意我们不需要在中断中使用临界区,因为:
111 |
112 | * 对 `COUNTER` 写0不会导致竞态,因为我们没读它
113 | * 它不可能被 `main` 中断
114 |
115 | 如果 `COUNTER` 被多个中断处理函数 _共用_ ,那么每个中断可能都需要一个临界区。
116 |
117 | 这解决了我们眼前的问题,但是我们还是得写一堆需要仔细考虑的不安全代码,而且也有可能写了没必要的临界区。
118 | 因为每个临界区都暂时的停止了中断,所以会有一些额外的代码大小,还增加了中断的延迟与中断处理的时间。
119 | 这是不是个问题取决于你的系统,但我们应该避免。
120 |
121 | 需要注意,虽然临界区保证不会发生中断,但是它并不能在多核系统上做出同样的保证!
122 | 即使没有中断,其他的核心也可以访问你操作的核的内存。如果你使用多核系统,那么你需要更强的同步原语。
123 |
124 | ## 原子操作
125 |
126 | 在一些平台上,我们可以使用特殊的原子指令,为读取-修改-保存操作提供保证。
127 | 针对 Cortex-M: `thumbv6`(Cortex-M0, Cortex-M0+) 只提供原子读和原子写, `thumbv7`(Cortex-M3 及以上 ) 提供完整的比较交换(CAS)操作。
128 | 这些 CAS 指令提供了消耗严重的禁用中断的替代方法:我们直接增加,大多数时候会成功,但如果被中断,它会自动尝试重新增加。
129 | 即使是多核系统,这些操作仍然是安全的。
130 |
131 | ```rust,ignore
132 | use core::sync::atomic::{AtomicUsize, Ordering};
133 |
134 | static COUNTER: AtomicUsize = AtomicUsize::new(0);
135 |
136 | #[entry]
137 | fn main() -> ! {
138 | set_timer_1hz();
139 | let mut last_state = false;
140 | loop {
141 | let state = read_signal_level();
142 | if state && !last_state {
143 | // Use `fetch_add` to atomically add 1 to COUNTER
144 | COUNTER.fetch_add(1, Ordering::Relaxed);
145 | }
146 | last_state = state;
147 | }
148 | }
149 |
150 | #[interrupt]
151 | fn timer() {
152 | // Use `store` to write 0 directly to COUNTER
153 | COUNTER.store(0, Ordering::Relaxed)
154 | }
155 | ```
156 |
157 | 这次 `COUNTER` 是一个安全的 `static` 变量。多亏 `AtomicUsize` 类型, `COUNTER` 能从中断和主循环中不停用中断安全修改。
158 | 如果可行的话这是个更好的方法 -- 但它取决于你的系统支不支持。
159 |
160 | 关于 [`Ordering`] 的说明:
161 | 这影响编译器和硬件对指令的重新排序方式,也会对缓存产生影响。
162 | 如果单核的话, `Relaxed` 就够了,也是效率最高的方法。
163 | 更严格的排序会让编译器围绕原子操作生成内存屏障;
164 | 根据你使用的原子操作的对象选择是否使用。原子模型很复杂,在这里不做介绍。
165 |
166 | 如果想了解更多有关原子与排序的内容,请看 [nomicon] 。
167 |
168 | [`Ordering`]: https://doc.rust-lang.org/core/sync/atomic/enum.Ordering.html
169 | [nomicon]: https://doc.rust-lang.org/nomicon/atomics.html
170 |
171 | ## 抽象、发送和同步
172 |
173 | 上面的方法都不是很让人满意。他们需要使用 `unsafe` ,所以我们得非常仔细的检查,很反人类。
174 | 在 Rust 中我们有更好的解决办法!
175 |
176 | 我们可以把 `COUNTER` 抽象成一个我们可以在哪都能用的安全的接口。
177 | 在这个例子中,我们使用临界区,但你也可以用原子操作做到相同的功能。
178 |
179 | ```rust,ignore
180 | use core::cell::UnsafeCell;
181 | use cortex_m::interrupt;
182 |
183 | // Our counter is just a wrapper around UnsafeCell, which is the heart
184 | // of interior mutability in Rust. By using interior mutability, we can have
185 | // COUNTER be `static` instead of `static mut`, but still able to mutate
186 | // its counter value.
187 | struct CSCounter(UnsafeCell);
188 |
189 | const CS_COUNTER_INIT: CSCounter = CSCounter(UnsafeCell::new(0));
190 |
191 | impl CSCounter {
192 | pub fn reset(&self, _cs: &interrupt::CriticalSection) {
193 | // By requiring a CriticalSection be passed in, we know we must
194 | // be operating inside a CriticalSection, and so can confidently
195 | // use this unsafe block (required to call UnsafeCell::get).
196 | unsafe { *self.0.get() = 0 };
197 | }
198 |
199 | pub fn increment(&self, _cs: &interrupt::CriticalSection) {
200 | unsafe { *self.0.get() += 1 };
201 | }
202 | }
203 |
204 | // Required to allow static CSCounter. See explanation below.
205 | unsafe impl Sync for CSCounter {}
206 |
207 | // COUNTER is no longer `mut` as it uses interior mutability;
208 | // therefore it also no longer requires unsafe blocks to access.
209 | static COUNTER: CSCounter = CS_COUNTER_INIT;
210 |
211 | #[entry]
212 | fn main() -> ! {
213 | set_timer_1hz();
214 | let mut last_state = false;
215 | loop {
216 | let state = read_signal_level();
217 | if state && !last_state {
218 | // No unsafe here!
219 | interrupt::free(|cs| COUNTER.increment(cs));
220 | }
221 | last_state = state;
222 | }
223 | }
224 |
225 | #[interrupt]
226 | fn timer() {
227 | // We do need to enter a critical section here just to obtain a valid
228 | // cs token, even though we know no other interrupt could pre-empt
229 | // this one.
230 | interrupt::free(|cs| COUNTER.reset(cs));
231 |
232 | // We could use unsafe code to generate a fake CriticalSection if we
233 | // really wanted to, avoiding the overhead:
234 | // let cs = unsafe { interrupt::CriticalSection::new() };
235 | }
236 | ```
237 |
238 | 我们把 `unsafe` 的代码移到了我们精心设计好的抽象中,现在我们的应用不包含任何 `unsafe` 的部分。
239 |
240 | 这个设计要求我们传入一个 `CriticalSection` 标记:这些标记只能由 `interrupt::free` 安全生成,
241 | 所以通过要求传入一个标志,我们保证这个操作实在临界区执行的,而不用自己去操作。
242 | 这个保证由编译器提供:不会在运行时有任何关于 `cs` 的开销。
243 | 如果我们有多个计数器,它们也可以使用相同的 `cs` ,不用嵌套多个临界区。
244 |
245 | 这也引出了 Rust 中一个重要的话题: [`Send` and `Sync`] traits 。
246 | 总结一下, 实现 Send 的类型可以被安全的转移到另一个线程,
247 | 而实现 Sync 的可以安全的在多个线程中共用。
248 | 在嵌入式开发中,我们把中断视为新开线程,所以主代码块与中断共用的变量一定实现 Sync 。
249 |
250 | [`Send` and `Sync`]: https://doc.rust-lang.org/nomicon/send-and-sync.html
251 |
252 | 对于 Rust 中的大多数类型,这两个 traits 通常由编译器自动派生。
253 | 然而,因为 `CSCounter` 包含一个 [`UnsafeCell`] ,它并不 Sync ,
254 | 所以我们没法声明一个 `static CSCounter` : `static` _一定_ 是修饰 Sync 的,因为能被多线程共用。
255 |
256 | [`UnsafeCell`]: https://doc.rust-lang.org/core/cell/struct.UnsafeCell.html
257 |
258 | 为了让编译器知道 `CSCounter` 事实上多线程共用是安全的,我们主动为它加上 Sync 。
259 | 与之前用的临界区一样,它只在单核系统上安全。
260 |
261 | ## 互斥量
262 |
263 | 我们针对计数器问题创造了一种抽象,同时还有很多用于并发的通用的抽象。
264 |
265 | 一种 _同步原语_ 叫互斥(mutex), mutual exclusion 的缩写。
266 | 这种结构确保对变量的独占访问,如我们的计数器。
267 | 一个线程可以尝试去 _锁_ (或 _需求_ )这个互斥锁,然后要么马上成功,要么等锁被用完,要么返回一个没法上锁的错误。
268 | 当该线程持有这个锁时,它能够访问这个受保护的数据。
269 | 当线程结束时,它 _解锁_ (或 _释放_ )这个互斥锁,以便让其他线程上锁。
270 | 在 Rust 里,我们通常使用 [`Drop`] trait 来修饰 Unlock ,以确保互斥量超出作用域时能正确释放锁。
271 |
272 | [`Drop`]: https://doc.rust-lang.org/core/ops/trait.Drop.html
273 |
274 | 把中断和互斥量用在一起可能有点难:中断中通常来说都不能阻塞,并且在中断中阻塞等待主循环解锁是不可能的,
275 | 会发生 _死锁_ (主线程因为等待中断结束而不会解锁)。
276 | 死锁是不安全的,即使在没有 `unsafe` 的 Rust 中也有可能发生。
277 |
278 | 为了避免这种情况的发生,我们可以实现一个需要临界区来上锁的互斥量,就像例子一样。
279 | 只要临界区和锁生命周期一样我们就可以保证我们独占被包装的变量,甚至不需要管互斥量锁没锁。
280 |
281 | `cortex-m` 库已经帮我们完成了这些!我们可以用这种方法来写我们的计数器:
282 |
283 | ```rust,ignore
284 | use core::cell::Cell;
285 | use cortex_m::interrupt::Mutex;
286 |
287 | static COUNTER: Mutex> = Mutex::new(Cell::new(0));
288 |
289 | #[entry]
290 | fn main() -> ! {
291 | set_timer_1hz();
292 | let mut last_state = false;
293 | loop {
294 | let state = read_signal_level();
295 | if state && !last_state {
296 | interrupt::free(|cs|
297 | COUNTER.borrow(cs).set(COUNTER.borrow(cs).get() + 1));
298 | }
299 | last_state = state;
300 | }
301 | }
302 |
303 | #[interrupt]
304 | fn timer() {
305 | // We still need to enter a critical section here to satisfy the Mutex.
306 | interrupt::free(|cs| COUNTER.borrow(cs).set(0));
307 | }
308 | ```
309 |
310 | 我们现在使用 `Cell` ,它与他的兄弟 `RefCell` 共同提供安全的内部可变性。
311 | 我们已经见过 `UnsafeCell` 了,他是 Rust 内部可变性的最底层:它允许你获取多个它的可变引用,但只能在 unsafe 中。
312 | 一个 `Cell` 和 `UnsafeCell` 差不多,但是它提供一个安全的接口:
313 | 它只允许获取当前值的一个复制或者替换它,而不允许引用,并且因为它不 Sync ,它没法在线程中共用。
314 | 这些特性意味着我们能安全使用,但我们没法直接用 `static` 修饰它,因为 `static` 只能用在 Sync 身上。
315 |
316 | [`Cell`]: https://doc.rust-lang.org/core/cell/struct.Cell.html
317 |
318 | 那为什么上面的例子能用? `Mutex` 为任何实现 Send 的 `T` 实现 Sync。
319 | 这么做是安全的因为它只在临界区中允许访问它的内容。
320 | 因此我们能得到一个完全不用 unsafe 的安全计数器。
321 |
322 | 对于像 `u32` 这样的简单结构很棒,但是不能 Copy 的复杂类型呢?
323 | 嵌入式开发中一个很常见的示例是外设,它通常是不能 Copy 的。
324 | 因此我们可以使用 `RefCell` 。
325 |
326 | ## 分享外设
327 |
328 | 使用 `svd2rust` 和相关抽象生成设备库通过强制外设只有一个实例保证了安全。
329 | 但是也给从主线程与中断中操作外设造成了困难。
330 |
331 | 为了安全的分享外设权限,我们可以像之前一样使用 `Mutex` 。
332 | 我们还要用到 [`RefCell`] ,它有一个运行时检查来确保一次只给出一个外设的引用。
333 | 相比普通的 `Cell` 有着更多的开销,但因为我们提供引用而不是副本,我们必须确保同时只能存在一个。
334 |
335 | [`RefCell`]: https://doc.rust-lang.org/core/cell/struct.RefCell.html
336 |
337 | 最后,我们还需要考虑怎么在主线程初始化后把外设移动到共享变量中。
338 | 为此我们可以使用 `Option` 类型,初始化为 `None` 然后再设置为外设的实例。
339 |
340 | ```rust,ignore
341 | use core::cell::RefCell;
342 | use cortex_m::interrupt::{self, Mutex};
343 | use stm32f4::stm32f405;
344 |
345 | static MY_GPIO: Mutex>> =
346 | Mutex::new(RefCell::new(None));
347 |
348 | #[entry]
349 | fn main() -> ! {
350 | // Obtain the peripheral singletons and configure it.
351 | // This example is from an svd2rust-generated crate, but
352 | // most embedded device crates will be similar.
353 | let dp = stm32f405::Peripherals::take().unwrap();
354 | let gpioa = &dp.GPIOA;
355 |
356 | // Some sort of configuration function.
357 | // Assume it sets PA0 to an input and PA1 to an output.
358 | configure_gpio(gpioa);
359 |
360 | // Store the GPIOA in the mutex, moving it.
361 | interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
362 | // We can no longer use `gpioa` or `dp.GPIOA`, and instead have to
363 | // access it via the mutex.
364 |
365 | // Be careful to enable the interrupt only after setting MY_GPIO:
366 | // otherwise the interrupt might fire while it still contains None,
367 | // and as-written (with `unwrap()`), it would panic.
368 | set_timer_1hz();
369 | let mut last_state = false;
370 | loop {
371 | // We'll now read state as a digital input, via the mutex
372 | let state = interrupt::free(|cs| {
373 | let gpioa = MY_GPIO.borrow(cs).borrow();
374 | gpioa.as_ref().unwrap().idr.read().idr0().bit_is_set()
375 | });
376 |
377 | if state && !last_state {
378 | // Set PA1 high if we've seen a rising edge on PA0.
379 | interrupt::free(|cs| {
380 | let gpioa = MY_GPIO.borrow(cs).borrow();
381 | gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
382 | });
383 | }
384 | last_state = state;
385 | }
386 | }
387 |
388 | #[interrupt]
389 | fn timer() {
390 | // This time in the interrupt we'll just clear PA0.
391 | interrupt::free(|cs| {
392 | // We can use `unwrap()` because we know the interrupt wasn't enabled
393 | // until after MY_GPIO was set; otherwise we should handle the potential
394 | // for a None value.
395 | let gpioa = MY_GPIO.borrow(cs).borrow();
396 | gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().clear_bit());
397 | });
398 | }
399 | ```
400 |
401 | 需要考虑的内容很多,让我们来挑出重要的几行。
402 |
403 | ```rust,ignore
404 | static MY_GPIO: Mutex>> =
405 | Mutex::new(RefCell::new(None));
406 | ```
407 |
408 | 我们的共享变量现在是一个 `Mutex` 套娃 `RefCell` 套娃 `Option` 。
409 | `Mutex` 确保我们仅能够在临界区有访问权限,来让本来不 Sync 的 `RefCell` Sync 。
410 | `RefCell` 通过引用为我们提供了内部可变性,让我们能够用我们的 `GPIOA` 。
411 | `Option` 让我们能够初始化一个空值然后再把我们的变量塞进去。
412 | 我们没法静态访问外设实例,只有在运行时可以,所以这是必须的。
413 |
414 | ```rust,ignore
415 | interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
416 | ```
417 |
418 | 在临界区中,我们对互斥量使用 `borrow()` ,让我们拿到一个 `RefCell` 的引用。
419 | 使用 `replace()` 来替换 `RefCell` 中的值。
420 |
421 | ```rust,ignore
422 | interrupt::free(|cs| {
423 | let gpioa = MY_GPIO.borrow(cs).borrow();
424 | gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
425 | });
426 | ```
427 |
428 | 最后我们能够安全并发使用 `MY_GPIO` 。临界区防止中断发生,让我们解锁互斥量。
429 | `RefCell` 给我们一个 `&Option` ,
430 | 并且跟踪它的生命周期 -- 一旦生命周期结束, `RefCell` 将被更新以表示它不再被使用。
431 |
432 | 因为我们没法把 `GPIOA` 移出 `&Option` ,我们需要使用 `as_ref()` 转换成 `&Option<&GPIOA>` ,
433 | 让我们最终能 `unwarp()` 出能操作外设的 `&GPIOA` 。
434 |
435 | 如果我们需要一个共享资源的可变引用,那使用 `borrow_mut` 和 `deref_mut` 来替代。
436 | 下面的例子使用 TIM2 来展示。
437 |
438 | ```rust,ignore
439 | use core::cell::RefCell;
440 | use core::ops::DerefMut;
441 | use cortex_m::interrupt::{self, Mutex};
442 | use cortex_m::asm::wfi;
443 | use stm32f4::stm32f405;
444 |
445 | static G_TIM: Mutex>>> =
446 | Mutex::new(RefCell::new(None));
447 |
448 | #[entry]
449 | fn main() -> ! {
450 | let mut cp = cm::Peripherals::take().unwrap();
451 | let dp = stm32f405::Peripherals::take().unwrap();
452 |
453 | // Some sort of timer configuration function.
454 | // Assume it configures the TIM2 timer, its NVIC interrupt,
455 | // and finally starts the timer.
456 | let tim = configure_timer_interrupt(&mut cp, dp);
457 |
458 | interrupt::free(|cs| {
459 | G_TIM.borrow(cs).replace(Some(tim));
460 | });
461 |
462 | loop {
463 | wfi();
464 | }
465 | }
466 |
467 | #[interrupt]
468 | fn timer() {
469 | interrupt::free(|cs| {
470 | if let Some(ref mut tim)) = G_TIM.borrow(cs).borrow_mut().deref_mut() {
471 | tim.start(1.hz());
472 | }
473 | });
474 | }
475 |
476 | ```
477 |
478 | 哇!这很安全,但也有点憨批。我们还有什么可以做的吗?
479 |
480 | ## RTIC
481 |
482 | 一种替代是 [RTIC framework] ( Real Time Interrupt-driven Concurrency )。
483 | 它强制执行静态优先级并跟踪对 `static mut` 变量(“资源”)的访问,以确保共享资源始终安全访问,
484 | 而不用进入临界区和使用引用计数(如在 `RefCell` 中)的开销。
485 | 它有许多优点,例如保证没有死锁并提供极快的时间和内存开销。
486 |
487 | [RTIC framework]: https://github.com/rtic-rs/cortex-m-rtic
488 |
489 | 该框架还提供了许多其他功能,如消息传递,能减少对显式共享状态的需求,还有能在指定时间调度任务的能力,可以用来实现周期性任务。
490 | 查看 [the documentation] 获取更多信息!
491 |
492 | [the documentation]: https://rtic.rs
493 |
494 | ## 实时操作系统
495 |
496 | 嵌入式并发的另一种常见方法是实时操作系统( RTOS )。
497 | 虽然在 Rust 中发展还不是很好,但他们广泛应用于传统嵌入式开发。
498 | 开源项目包括 [FreeRTOS] 和 [ChibiOS] 。
499 | 这些实时操作系统为运行多个线程提供 CPU 调度的支持,包括线程让出控制(协作多任务)与基于常规计时器与中断(抢占式任务)。
500 | RTOS 通常提供互斥量与其他同步原语,并且经常与硬件引擎(如 DMA 控制器)进行互操作。
501 |
502 | [FreeRTOS]: https://freertos.org/
503 | [ChibiOS]: http://chibios.org/
504 |
505 | 在本文撰写时,还没有许多 Rust 的 RTOS 例子,但请仍然关注。
506 |
507 | ## 多核
508 |
509 | 在嵌入式系统中,多核系统越来越普遍,这给并发又增加了难度与复杂程度。
510 | 所有使用临界区的例子(包括 `cortex_m::interrupt::Mutex` )都假设唯一能打断的线程是中断,
511 | 但在多核系统上不是这样。
512 | 相反我们需要为多核系统设计的同步原语(也叫 SMP ,symmetric multi-processing )。
513 |
514 | 这些通常使用我们之前看到的原子指令,因为处理系统将确保在所有内核上保持原子性。
515 |
516 | 详细介绍这些主题目前超出了本书的范围,但一般模式与单核情况相同。
--------------------------------------------------------------------------------
|