├── .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 | ![nrf52-memory-map](../assets/nrf52-memory-map.png) 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 | ![nrf52-spi-frequency-register](../assets/nrf52-spi-frequency-register.png) 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 | ![](../assets/rust_layers.svg) 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 | Peripheral Access CrateHardware Abstraction Layer ImplDriverDriverApplicationBoard Support CrateUsesImplementsHardware Abstraction Layer TraitsMicrocontroller -------------------------------------------------------------------------------- /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 | 详细介绍这些主题目前超出了本书的范围,但一般模式与单核情况相同。 --------------------------------------------------------------------------------