Embedded Rust详解


✔Learning Resource


😀《The Embedded Rust Book》

简介

认识硬件

让我们熟悉一下我们将要使用的硬件。

STM32F3DISCOVERY(“F3”)

这个板子包含什么?

  • 一个STM32F303VCT6微控制器。这个微控制器有
    • 单核 ARM Cortex-M4F 处理器,硬件支持单精度浮点运算,最大时钟频率为 72 MHz。
    • 256 KiB 的“闪存”内存。(1 KiB = 10 24字节)
    • 48 KiB 内存。
    • 多种集成外设,如定时器、I2C、SPI 和 USART。
    • 通用输入输出 (GPIO) 和其他类型的引脚可通过板旁边的两排接头访问。
    • 可通过标有“USB USER”的 USB 端口访问的 USB 接口。
  • 一个加速计作为的一部分LSM303DLHC芯片。
  • 磁力作为一部分LSM303DLHC芯片。
  • 陀螺仪作为一部分L3GD20芯片。
  • 8 个用户 LED 排列成指南针形状。
  • 第二个微控制器:STM32F103。该微控制器实际上是板载编程器/调试器的一部分,并连接到名为“USB ST-LINK”的 USB 端口。

如需更详细的功能列表和电路板的进一步规格,请访问STMicroelectronics网站。

警告:如果您想将外部信号应用于电路板,请务必小心。微控制器 STM32F303VCT6 引脚的标称电压为 3.3 伏。如需更多信息,请参阅手册中的6.2 绝对最大额定值部分

一个no_std rust环境

术语嵌入式编程用于广泛的不同类别的编程。从编程只有几 KB RAM 和 ROM 的8 位 MCU(如ST72325xx),到具有 32/64 位 4 核 Cortex-A53 @的 Raspberry Pi(B 型 3+)等系统1.4 GHz 和 1GB 内存。编写代码时将应用不同的限制/限制,具体取决于您拥有的目标和用例类型。

有两种通用的嵌入式编程分类:

托管环境

这些类型的环境接近于正常的 PC 环境。这意味着您提供了一个系统接口EG POSIX ,它为您提供了与各种系统交互的原语,例如文件系统、网络、内存管理、线程等。标准库通常又依赖于这些原语来实现他们的功能。您可能还有某种 sysroot 和 RAM/ROM 使用限制,也许还有一些特殊的硬件或 I/O。总体而言,感觉就像在专用 PC 环境中编码。

裸机环境

在裸机环境中,在您的程序之前没有加载任何代码。如果没有操作系统提供的软件,我们将无法加载标准库。相反,程序及其使用的板条箱只能使用硬件(裸机)来运行。为了防止 Rust 加载标准库,请使用no_std. 标准库的平台无关部分可通过libcore 获得。libcore 还排除了在嵌入式环境中并不总是需要的东西。其中之一是用于动态内存分配的内存分配器。如果您需要此功能或任何其他功能,通常有提供这些功能的板条箱。

libstd 运行时

如前所述,使用libstd需要某种系统集成,但这不仅是因为 libstd只是提供一种访问操作系统抽象的通用方法,它还提供了一个运行时。该运行时负责设置堆栈溢出保护、处理命令行参数以及在调用程序的主函数之前生成主线程。此运行时在no_std环境中也不可用。

概括

#![no_std]是一个 crate 级别的属性,表示 crate 将链接到 core-crate 而不是 std-crate。反过来,libcore crate 是 std crate 与平台无关的子集,它不对程序将在其上运行的系统做任何假设。因此,它为浮点数、字符串和切片等语言原语提供 API,以及公开原子操作和 SIMD 指令等处理器功能的 API。然而,它缺少任何涉及平台集成的 API。由于这些属性,no_std 和libcore代码可用于任何类型的引导(阶段 0)代码,如引导加载程序、固件或内核。

概述

特征 no_std 标准
堆(动态内存) *
集合(Vec、HashMap 等) **
堆栈溢出保护
在 main 之前运行初始化代码
libstd 可用
libcore 可用
编写固件、内核或引导加载程序代码

* 仅当您使用alloccrate 并使用合适的分配器(如alloc-cortex-m )时

** 仅当您使用collectionscrate 并配置全局默认分配器时。

也可以看看

工具

处理微控制器涉及使用几种不同的工具,因为我们将处理与笔记本电脑不同的架构,我们必须在远程设备上运行和调试程序。

我们将使用下面列出的所有工具。当未指定最低版本时,任何最新版本都应该可以工作,但我们已经列出了我们测试过的版本。

  • Rust 1.31、1.31-beta 或更新的工具链加上 ARM Cortex-M 编译支持。
  • cargo-binutils ~0.1.4
  • qemu-system-arm. 测试版本:3.0.0
  • OpenOCD >=0.8。测试版本:v0.9.0 和 v0.10.0
  • 具有 ARM 支持的 GDB。强烈建议使用 7.12 或更高版本。测试版本:7.10、7.11、7.12 和 8.1
  • cargo-generategit。这些工具是可选的,但可以更轻松地跟随本书进行学习。

下面的文字解释了我们使用这些工具的原因。安装说明可以在下一页找到。

cargo-generate 或者 git

裸机程序是非标准的 ( no_std) Rust 程序,需要对链接过程进行一些调整才能使程序的内存布局正确。这需要一些额外的文件(如链接器脚本)和设置(如链接器标志)。我们为您打包了一个模板,您只需填写缺少的信息(例如项目名称和目标硬件的特性)。

我们的模板兼容cargo-generate:用于从模板创建新 Cargo 项目的 Cargo 子命令。您还可以使用下载模板gitcurlwget,或Web浏览器。

cargo-binutils

cargo-binutils是 Cargo 子命令的集合,可以轻松使用 Rust 工具链附带的 LLVM 工具。这些工具包括objdumpnm和的 LLVM 版本,size用于检查二进制文件。

与 GNU binutils 相比,使用这些工具的优势在于 (a) 安装 LLVM 工具是相同的单命令安装 ( rustup component add llvm-tools-preview),无论您的操作系统如何,以及 (b)objdump支持所有rustc支持架构的工具——从 ARM 到 x86_64—— - 因为它们共享相同的 LLVM 后端。

qemu-system-arm

QEMU 是一个模拟器。在这种情况下,我们使用可以完全模拟 ARM 系统的变体。我们使用 QEMU 在主机上运行嵌入式程序。多亏了这一点,即使您没有任何硬件,您也可以遵循本书的某些部分!

GDB

调试器是嵌入式开发的一个非常重要的组件,因为您可能并不总是能够将内容记录到主机控制台。在某些情况下,您的硬件甚至可能没有 LED 指示灯闪烁!

总的来说,LLDB 在调试方面的效果与 GDB 一样好,但我们还没有找到 GDB 的load命令的 LLDB 对应项,它将程序上传到目标硬件,因此目前我们建议您使用 GDB。

OpenOCD

GDB 无法直接与 STM32F3DISCOVERY 开发板上的 ST-Link 调试硬件通信。它需要一个转换器,而开放式片上调试器 OpenOCD 就是那个转换器。OpenOCD 是一个在您的笔记本电脑/PC 上运行的程序,可在 GDB 的基于 TCP/IP 的远程调试协议和 ST-Link 的基于 USB 的协议之间进行转换。

OpenOCD 还执行其他重要工作,作为其在 STM32F3DISCOVERY 开发板上调试基于 ARM Cortex-M 的微控制器的翻译的一部分:

  • 它知道如何与 ARM CoreSight 调试外设使用的内存映射寄存器进行交互。正是这些 CoreSight 寄存器允许:
    • 断点/观察点操作
    • 读取和写入 CPU 寄存器
    • 检测 CPU 何时因调试事件而停止
    • 遇到调试事件后继续执行 CPU
    • 等等。
  • 它还知道如何擦除和写入微控制器的 FLASH

安装

此页面包含一些工具的与操作系统无关的安装说明:

Rust 工具链

按照https://rustup.rs 上的说明安装 rustup 。

注意确保您的编译器版本等于或高于1.31. rustc -V应该返回一个比下面显示的日期新的日期。

$ rustc -V
rustc 1.31.1 (b6c32da9b 2018-12-18)

对于带宽和磁盘使用问题,默认安装仅支持本机编译。要为 ARM Cortex-M 架构添加交叉编译支持,请选择以下编译目标之一。对于本书示例中使用的 STM32F3DISCOVERY 板,请使用thumbv7em-none-eabihf目标。

Cortex-M0、M0+ 和 M1(ARMv6-M 架构):

rustup target add thumbv6m-none-eabi

Cortex-M3(ARMv7-M 架构):

rustup target add thumbv7m-none-eabi

无硬件浮点的 Cortex-M4 和 M7(ARMv7E-M 架构):

rustup target add thumbv7em-none-eabi

带有硬件浮点的 Cortex-M4F 和 M7F(ARMv7E-M 架构):

rustup target add thumbv7em-none-eabihf

Cortex-M23(ARMv8-M 架构):

rustup target add thumbv8m.base-none-eabi

Cortex-M33 和 M35P(ARMv8-M 架构):

rustup target add thumbv8m.main-none-eabi

Cortex-M33F 和 M35PF 带硬件浮点(ARMv8-M 架构):

rustup target add thumbv8m.main-none-eabihf

cargo-binutils

cargo install cargo-binutils

rustup component add llvm-tools-preview

cargo-generate

稍后我们将使用它从模板生成项目。

cargo install cargo-generate

Linux

以下是一些 Linux 发行版的安装命令。

套餐

  • Ubuntu 18.04 或更高版本/Debian 延伸版或更高版本

注意 gdb-multiarch是您将用于调试 ARM Cortex-M 程序的 GDB 命令

sudo apt install gdb-multiarch openocd qemu-system-arm
  • Ubuntu 14.04 和 16.04

注意 arm-none-eabi-gdb是您将用于调试 ARM Cortex-M 程序的 GDB 命令

sudo apt install gdb-arm-none-eabi openocd qemu-system-arm
  • Fedora 27 或更新版本

注意 arm-none-eabi-gdb是您将用于调试 ARM Cortex-M 程序的 GDB 命令

sudo dnf install arm-none-eabi-gdb openocd qemu-system-arm
  • 拱形Linux

注意 arm-none-eabi-gdb是您将用于调试 ARM Cortex-M 程序的 GDB 命令

sudo pacman -S arm-none-eabi-gdb qemu-arch-extra openocd

udev 规则

此规则允许您在没有 root 权限的情况下将 OpenOCD 与 Discovery 板一起使用。

/etc/udev/rules.d/70-st-link.rules使用如下所示的内容创建文件。

# STM32F3DISCOVERY rev A/B - ST-LINK/V2
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", TAG+="uaccess"

# STM32F3DISCOVERY rev C+ - ST-LINK/V2-1
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", TAG+="uaccess"

然后使用以下命令重新加载所有 udev 规则:

sudo udevadm control --reload-rules

如果您将电路板插入笔记本电脑,请将其拔下,然后再重新插入。

您可以通过运行以下命令来检查权限:

lsusb

哪个应该显示类似

(..)
Bus 001 Device 018: ID 0483:374b STMicroelectronics ST-LINK/V2.1
(..)

记下总线和设备编号。使用这些数字创建一个类似 /dev/bus/usb/<bus>/<device>. 然后像这样使用这个路径:

ls -l /dev/bus/usb/001/018
crw-------+ 1 root root 189, 17 Sep 13 12:34 /dev/bus/usb/001/018
getfacl /dev/bus/usb/001/018 | grep user
user::rw-
user:you:rw-

+附加到权限指示扩展的许可的存在。该getfacl命令告诉用户you可以使用该设备。

苹果系统

所有工具都可以使用Homebrew安装:

$ # GDB
$ brew install armmbed/formulae/arm-none-eabi-gcc

$ # OpenOCD
$ brew install openocd

$ # QEMU
$ brew install qemu

Windows

arm-none-eabi-gdb

ARM.exe为 Windows提供安装程序。从这里拿一个,然后按照说明操作。就在安装过程完成之前,勾选/选择“添加环境变量路径”选项。然后验证工具是否在您的%PATH%

$ arm-none-eabi-gdb -v
GNU gdb (GNU Tools for Arm Embedded Processors 7-2018-q2-update) 8.1.0.20180315-git
(..)

开放式强迫症

没有用于 Windows 的 OpenOCD 的官方二进制版本,但如果您不想自己编译它,xPack 项目提供了一个二进制发行版,请点击此处。按照提供的安装说明进行操作。然后更新您的%PATH%环境变量以包含安装二进制文件的路径。( C:\Users\USERNAME\AppData\Roaming\xPacks\@xpack-dev-tools\openocd\0.10.0-13.1\.content\bin\, 如果您一直在使用简易安装)

验证 OpenOCD 是否在您的目录%PATH%中:

$ openocd -v
Open On-Chip Debugger 0.10.0
(..)

动车组

官方网站获取QEMU 。

您还需要安装此 USB 驱动程序,否则 OpenOCD 将无法工作。按照安装程序说明进行操作,并确保安装正确版本(32 位或 64 位)的驱动程序。

验证安装

在本节中,我们检查一些必需的工具/驱动程序是否已正确安装和配置。

使用微型 USB 电缆将您的笔记本电脑/PC 连接到发现板。发现板有两个 USB 连接器;使用位于电路板边缘中心的标有“USB ST-LINK”的那个。

还要检查是否填充了 ST-LINK 标头。见下图;ST-LINK 标头以红色圈出。

现在运行以下命令:

openocd -f interface/stlink.cfg -f target/stm32f3x.cfg

注意:旧版本的 openocd,包括 2017 年的 0.10.0 版本,不包含新的(和更可取的)interface/stlink.cfg文件;相反,您可能需要使用interface/stlink-v2.cfginterface/stlink-v2-1.cfg

您应该得到以下输出,并且程序应该阻止控制台:

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.919881
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

内容可能不完全匹配,但您应该得到关于断点和观察点的最后一行。如果您得到它,则终止 OpenOCD 进程并转到下一部分

如果您没有得到“断点”行,请尝试以下命令之一。

openocd -f interface/stlink-v2.cfg -f target/stm32f3x.cfg
openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

如果其中一个命令有效,则意味着您获得了发现板的旧硬件版本。这不会成为问题,但请将此事实记入内存,因为您稍后需要以不同的方式配置内容。您可以转到 下一部分

如果没有任何命令以普通用户身份运行,则尝试以 root 权限(例如sudo openocd ..)运行它们。如果命令确实在 root 权限下工作,则检查udev 规则是否已正确设置。

入门

QUEM

我们将开始为Cortex-M3 微控制器LM3S6965编写程序。我们选择它作为我们的初始目标,因为它可以使用 QEMU进行模拟,因此您无需在本节中摆弄硬件,我们可以专注于工具和开发过程。

重要信息 在本教程中,我们将使用名称“app”作为项目名称。每当您看到“app”一词时,您都应该将其替换为您为项目选择的名称。或者,您也可以将您的项目命名为“app”并避免替换。

创建一个非标准的 Rust 程序

我们将使用cortex-m-quickstart项目模板从中生成一个新项目。创建的项目将包含一个准系统应用程序:一个新的嵌入式 Rust 应用程序的良好起点。此外,该项目将包含一个examples目录,其中包含几个单独的应用程序,突出显示了一些关键的嵌入式 Rust 功能。

使用 cargo-generate

首先安装货物生成

cargo install cargo-generate

然后生成一个新项目

cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
 Project Name: app
 Creating project called `app`...
 Done! New project created /tmp/app
cd app

使用 git

克隆存储库

git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd app

然后填写Cargo.toml文件中的占位符

[package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
version = "0.1.0"

# ..

[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "awesome-app"
test = false
bench = false

使用neither

获取cortex-m-quickstart模板的最新快照并提取它。

curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd app

或者您可以浏览到cortex-m-quickstart,单击绿色的“克隆或下载”按钮,然后单击“下载 ZIP”。

然后Cargo.toml按照“使用git”版本的第二部分中的方法填写文件中的占位符。

程序概览

为方便起见,这里是源代码中最重要的部分src/main.rs

#![no_std]
#![no_main]

use panic_halt as _;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {
        // your code goes here
    }
}

这个程序与标准的 Rust 程序有点不同,所以让我们仔细看看。

#![no_std]表示这个程序不会链接到标准包, std. 相反,它将链接到它的子集:core板条箱。

#![no_main]表示这个程序不会使用main 大多数 Rust 程序使用的标准接口。主要(没有双关语)的原因no_mainmainno_std上下文中使用界面需要每晚。

use panic_halt as _;. 这个 crate 提供了一个panic_handler定义程序的恐慌行为。

#[entry]cortex-m-rtcrate提供的一个属性,用于标记程序的入口点。由于我们没有使用标准main 接口,我们需要另一种方式来指示程序的入口点,那就是#[entry].

fn main() -> !. 我们的程序将是目标硬件上运行的唯一进程,所以我们不希望它结束!我们使用发散函数-> ! 函数签名中的位)来确保在编译时就是这种情况。

交叉编译

下一步是交叉编译 Cortex-M3 架构的程序。cargo build --target $TRIPLE如果您知道编译目标 ( $TRIPLE) 应该是什么,那么这就像运行一样简单。幸运的是,.cargo/config.toml模板中有答案:

tail -n6 .cargo/config.toml
[build]
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
target = "thumbv7m-none-eabi"    # Cortex-M3
# target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

要交叉编译 Cortex-M3 架构,我们必须使用 thumbv7m-none-eabi. 安装 Rust 工具链时不会自动安装该目标,现在是将该目标添加到工具链的好时机,如果您还没有这样做:

rustup target add thumbv7m-none-eabi

由于thumbv7m-none-eabi编译目标已在您的.cargo/config.toml文件中设置为默认值,因此下面的两个命令执行相同的操作:

cargo build --target thumbv7m-none-eabi
cargo build

检查

现在我们在target/thumbv7m-none-eabi/debug/app. 我们可以使用cargo-binutils.

随着cargo-readobj我们可以打印ELF头,以确认这是一个ARM二进制文件。

cargo readobj --bin app -- -file-headers

注意:

  • --bin app 是用于检查二进制文件的糖 target/$TRIPLE/debug/app
  • --bin app 如有必要,还将(重新)编译二进制文件
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0x0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x405
  Start of program headers:          52 (bytes into file)
  Start of section headers:          153204 (bytes into file)
  Flags:                             0x5000200
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         2
  Size of section headers:           40 (bytes)
  Number of section headers:         19
  Section header string table index: 18

cargo-size 可以打印二进制文件的链接器部分的大小。

cargo size --bin app --release -- -A

我们--release用来检查优化版本

app  :
section             size        addr
.vector_table       1024         0x0
.text                 92       0x400
.rodata                0       0x45c
.data                  0  0x20000000
.bss                   0  0x20000000
.debug_str          2958         0x0
.debug_loc            19         0x0
.debug_abbrev        567         0x0
.debug_info         4929         0x0
.debug_ranges         40         0x0
.debug_macinfo         1         0x0
.debug_pubnames     2035         0x0
.debug_pubtypes     1892         0x0
.ARM.attributes       46         0x0
.debug_frame         100         0x0
.debug_line          867         0x0
Total              14570

ELF 链接器部分的复习

  • .text 包含程序指令
  • .rodata 包含常量值,如字符串
  • .data包含初始值为零的静态分配变量
  • .bss也包含静态分配的变量,其初始值
  • .vector_table是我们用来存储向量(中断)表的非标准部分
  • .ARM.attributes并且这些.debug_*部分包含元数据,并且 在闪烁二进制文件时不会加载到目标上。

重要提示:ELF 文件包含诸如调试信息之类的元数据,因此它们在磁盘上的大小*不能准确反映程序在设备上闪烁时将占用的空间。始终*使用cargo-size来检查二进制真的有多大。

cargo-objdump 可用于反汇编二进制文件。

cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex

注意如果上面的命令抱怨Unknown command line argument看到以下错误报告:https://github.com/rust-embedded/book/issues/269

注意此输出在您的系统上可能会有所不同。新版本的 rustc、LLVM 和库可以生成不同的程序集。我们截断了一些指令以保持代码片段较小。

app:  file format ELF32-arm-little

Disassembly of section .text:
main:
     400: bl  #0x256
     404: b #-0x4 

Reset:
     406: bl  #0x24e
     40a: movw  r0, #0x0
     < .. truncated any more instructions .. >

DefaultHandler_:
     656: b #-0x4 

UsageFault:
     657: strb  r7, [r4, #0x3]

DefaultPreInit:
     658: bx  lr

__pre_init:
     659: strb  r7, [r0, #0x1]

__nop:
     65a: bx  lr

HardFaultTrampoline:
     65c: mrs r0, msp
     660: b #-0x2 

HardFault_:
     662: b #-0x4 

HardFault:
     663: 

运行

接下来,让我们看看如何在 QEMU 上运行嵌入式程序!这次我们将使用hello实际执行某些操作的 示例。

为方便起见,这里的源代码examples/hello.rs

//! Prints "Hello, world!" on the host console using semihosting

#![no_main]
#![no_std]

use panic_halt as _;

use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};

#[entry]
fn main() -> ! {
    hprintln!("Hello, world!").unwrap();

    // exit QEMU
    // NOTE do not run this on hardware; it can corrupt OpenOCD state
    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

该程序使用称为半主机的东西将文本打印到主机 控制台。当使用真正的硬件时,这需要一个调试会话,但当使用 QEMU 时,它就可以工作。

让我们从编译示例开始:

cargo build --example hello

输出二进制文件将位于 target/thumbv7m-none-eabi/debug/examples/hello.

要在 QEMU 上运行此二进制文件,请运行以下命令:

qemu-system-arm \
  -cpu cortex-m3 \
  -machine lm3s6965evb \
  -nographic \
  -semihosting-config enable=on,target=native \
  -kernel target/thumbv7m-none-eabi/debug/examples/hello
Hello, world!

打印文本后,该命令应成功退出(退出代码 = 0)。在 *nix 上,您可以使用以下命令进行检查:

echo $?
0

让我们分解一下 QEMU 命令:

  • qemu-system-arm. 这是 QEMU 模拟器。这些 QEMU 二进制文件有几个变体;这个对ARM机器进行完整的系统仿真,因此得名。
  • -cpu cortex-m3. 这告诉 QEMU 模拟 Cortex-M3 CPU。指定 CPU 型号可以让我们捕获一些错误编译错误:例如,运行为 Cortex-M4F 编译的程序,它具有硬件 FPU,在执行过程中会导致 QEMU 错误。
  • -machine lm3s6965evb. 这告诉 QEMU 模拟 LM3S6965EVB,这是一个包含 LM3S6965 微控制器的评估板。
  • -nographic. 这告诉 QEMU 不要启动它的 GUI。
  • -semihosting-config (..). 这告诉 QEMU 启用半主机。半主机允许模拟设备使用主机 stdout、stderr 和 stdin 并在主机上创建文件。
  • -kernel $file. 这会告诉 QEMU 在模拟机器上加载和运行哪个二进制文件。

输入这么长的 QEMU 命令太费力了!我们可以设置自定义运行器来简化流程。.cargo/config.toml有一个被注释掉的调用 QEMU 的运行程序;让我们取消注释:

head -n3 .cargo/config.toml
[target.thumbv7m-none-eabi]
# uncomment this to make `cargo run` execute programs on QEMU
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"

此运行程序仅适用于thumbv7m-none-eabi目标,这是我们的默认编译目标。现在cargo run将编译程序并在 QEMU 上运行它:

cargo run --example hello --release
   Compiling app v0.1.0 (file:///tmp/app)
    Finished release [optimized + debuginfo] target(s) in 0.26s
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
Hello, world!

调试

调试对于嵌入式开发至关重要。让我们看看它是如何完成的。

调试嵌入式设备涉及远程调试,因为我们要调试的程序不会在运行调试器程序(GDB 或 LLDB)的机器上运行。

远程调试涉及客户端和服务器。在 QEMU 设置中,客户端将是一个 GDB(或 LLDB)进程,而服务器将是运行嵌入式程序的 QEMU 进程。

在本节中,我们将使用hello我们已经编译的示例。

调试的第一步是在调试模式下启动 QEMU:

qemu-system-arm \
  -cpu cortex-m3 \
  -machine lm3s6965evb \
  -nographic \
  -semihosting-config enable=on,target=native \
  -gdb tcp::3333 \
  -S \
  -kernel target/thumbv7m-none-eabi/debug/examples/hello

此命令不会向控制台打印任何内容并将阻止终端。这次我们传递了两个额外的标志:

  • -gdb tcp::3333. 这告诉 QEMU 等待 TCP 端口 3333 上的 GDB 连接。
  • -S. 这告诉 QEMU 在启动时冻结机器。如果没有这个,程序将在我们有机会启动调试器之前到达 main 的末尾!

接下来我们在另一个终端中启动 GDB 并告诉它加载示例的调试符号:

gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello

注意:您可能需要另一个版本的 gdb 而不是gdb-multiarch取决于您在安装章节中安装的那个版本。这也可以是 arm-none-eabi-gdb或只是gdb.

然后在 GDB shell 中我们连接到 QEMU,它正在等待 TCP 端口 3333 上的连接。

target remote :3333
Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473     pub unsafe extern "C" fn Reset() -> ! {

您将看到该进程已停止,并且程序计数器指向名为Reset. 这就是重置处理程序:Cortex-M 内核在启动时执行的操作。

请注意,在某些设置Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473中,gdb 可能会打印一些警告,而不是显示如上所示的行:

core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254` `src/libcore/num/bignum.rs: No such file or directory.

这是一个众所周知的故障。您可以放心地忽略这些警告,您很可能在 Reset() 处。

这个重置处理程序最终会调用我们的主函数。让我们使用断点和continue命令一路跳过。要设置断点,让我们首先看看我们想要在代码中中断的位置,使用list命令。

list main

这将显示来自文件 examples/hello.rs 的源代码。

6       use panic_halt as _;
7
8       use cortex_m_rt::entry;
9       use cortex_m_semihosting::{debug, hprintln};
10
11      #[entry]
12      fn main() -> ! {
13          hprintln!("Hello, world!").unwrap();
14
15          // exit QEMU

我们想在第 13 行的“Hello, world!”之前添加一个断点。我们使用以下break命令:

break 13

我们现在可以使用以下continue命令指示 gdb 运行到我们的 main 函数:

continue
Continuing.

Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13          hprintln!("Hello, world!").unwrap();

我们现在接近打印“Hello, world!”的代码。让我们继续使用next命令。

next
16          debug::exit(debug::EXIT_SUCCESS);

此时您应该看到“Hello, world!” 打印在正在运行的终端上qemu-system-arm

$ qemu-system-arm (..)
Hello, world!

next再次调用将终止 QEMU 进程。

next
[Inferior 1 (Remote target) exited normally]

您现在可以退出 GDB 会话。

quit

Hardware(硬件)

了解硬件

在我们开始之前,您需要确定目标设备的一些特征,因为这些特征将用于配置项目:

  • ARM 核心。例如皮质-M3。
  • ARM 内核是否包含 FPU?Cortex-M4 F和 Cortex-M7 F内核可以。
  • 目标设备有多少闪存和 RAM?例如 256 KiB 的闪存和 32 KiB 的 RAM。
  • 闪存和 RAM 在地址空间中映射到哪里?例如,RAM 通常位于 address 0x2000_0000

您可以在数据表或设备的参考手册中找到此信息。

在本节中,我们将使用我们的参考硬件 STM32F3DISCOVERY。该板包含一个 STM32F303VCT6 微控制器。该微控制器具有:

  • 包含单精度 FPU 的 Cortex-M4F 内核
  • 位于地址 0x0800_0000 的 256 KiB 闪存。
  • 位于地址 0x2000_0000 的 40 KiB RAM。(还有另一个 RAM 区域,但为简单起见,我们将忽略它)。

配置

我们将从一个新的模板实例开始。请参阅 QEMU关于如何做到这一点没有复习 cargo-generate

$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
 Project Name: app
 Creating project called `app`...
 Done! New project created /tmp/app

$ cd app

第一步是在.cargo/config.toml.

tail -n5 .cargo/config.toml
# Pick ONE of these compilation targets
# target = "thumbv6m-none-eabi"    # Cortex-M0 and Cortex-M0+
# target = "thumbv7m-none-eabi"    # Cortex-M3
# target = "thumbv7em-none-eabi"   # Cortex-M4 and Cortex-M7 (no FPU)
target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)

我们将使用thumbv7em-none-eabihf它覆盖 Cortex-M4F 内核。

第二步是将内存区域信息输入到memory.x 文件中。

$ cat memory.x
/* Linker script for the STM32F303VCT6 */
MEMORY
{
  /* NOTE 1 K = 1 KiBi = 1024 bytes */
  FLASH : ORIGIN = 0x08000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 40K
}

注意:如果您memory.x在完成特定构建目标的第一次构建后出于某种原因更改了文件,请cargo clean在之前执行 cargo build,因为cargo build可能无法跟踪memory.x.

我们将再次从 hello 示例开始,但首先我们必须进行一些小的更改。

在 中examples/hello.rs,确保debug::exit()已注释掉或删除该调用。它仅用于在 QEMU 中运行。

#[entry]
fn main() -> ! {
    hprintln!("Hello, world!").unwrap();

    // exit QEMU
    // NOTE do not run this on hardware; it can corrupt OpenOCD state
    // debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

您现在可以像以前一样使用交叉编译程序cargo build 和检查二进制文件cargo-binutils。该 cortex-m-rt箱处理所有的魔法需要得到你的芯片上运行,如有益,几乎所有的Cortex-M CPU的启动以同样的方式。

cargo build --example hello

调试

调试看起来有点不同。事实上,根据目标设备的不同,第一步看起来可能会有所不同。在本节中,我们将展示调试在 STM32F3DISCOVERY 上运行的程序所需的步骤。这是为了作为参考;有关调试的设备特定信息,请查看Debugonomicon

和以前一样,我们将进行远程调试,客户端将是一个 GDB 进程。然而,这一次,服务器将是 OpenOCD。

正如在安装验证部分所做的那样,将发现板连接到您的笔记本电脑/PC 并检查 ST-LINK 标头是否已填充。

在终端上运行openocd以连接到发现板上的 ST-LINK。从模板的根目录运行此命令;openocd将拾取 openocd.cfg指示使用哪个接口文件和目标文件的文件。

cat openocd.cfg
# Sample OpenOCD configuration for the STM32F3DISCOVERY development board

# Depending on the hardware revision you got you'll have to pick ONE of these
# interfaces. At any time only one interface should be commented out.

# Revision C (newer revision)
source [find interface/stlink.cfg]

# Revision A and B (older revisions)
# source [find interface/stlink-v2.cfg]

source [find target/stm32f3x.cfg]

注意如果您在验证部分发现发现板的版本较旧,则此时应修改openocd.cfg 文件以使用interface/stlink-v2.cfg.

$ openocd
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.913879
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

在另一个终端上运行 GDB,也是从模板的根目录。

$  -q target/thumbv7em-none-eabihf/debug/examples/hello

接下来将 GDB 连接到 OpenOCD,它正在等待端口 3333 上的 TCP 连接。

(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()

现在继续使用命令将程序闪存(加载)到微控制器上 load

(gdb) load
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x1e70 lma 0x8000400
Loading section .rodata, size 0x61c lma 0x8002270
Start address 0x800144e, load size 10380
Transfer rate: 17 KB/sec, 3460 bytes/write.

程序现在已加载。该程序使用半主机,因此在我们进行任何半主机调用之前,我们必须告诉 OpenOCD 启用半主机。您可以使用命令向 OpenOCD 发送命令monitor

(gdb) monitor arm semihosting enable
semihosting is enabled

您可以通过调用monitor help命令查看所有 OpenOCD 命令。

像以前一样,我们可以一直跳过main使用断点和 continue命令。

(gdb) break main
Breakpoint 1 at 0x8000d18: file examples/hello.rs, line 15.

(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, main () at examples/hello.rs:15
15          let mut stdout = hio::hstdout().unwrap();

注意如果在您发出上述continue命令后 GDB 阻塞终端而不是点击断点,您可能需要仔细检查文件中的内存区域信息memory.x是否为您的设备正确设置(开始长度)。

推进程序next应该产生与以前相同的结果。

(gdb) next
16          writeln!(stdout, "Hello, world!").unwrap();

(gdb) next
19          debug::exit(debug::EXIT_SUCCESS);

此时您应该看到“Hello, world!” 打印在 OpenOCD 控制台上,等等。

$ openocd
(..)
Info : halted: PC: 0x08000e6c
Hello, world!
Info : halted: PC: 0x08000d62
Info : halted: PC: 0x08000d64
Info : halted: PC: 0x08000d66
Info : halted: PC: 0x08000d6a
Info : halted: PC: 0x08000a0c
Info : halted: PC: 0x08000d70
Info : halted: PC: 0x08000d72

发出另一个next将使处理器执行debug::exit。这充当断点并停止进程:

(gdb) next

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0800141a in __syscall ()

它还会导致将其打印到 OpenOCD 控制台:

$ openocd
(..)
Info : halted: PC: 0x08001188
semihosting: *** application exited ***
Warn : target not halted
Warn : target not halted
target halted due to breakpoint, current mode: Thread
xPSR: 0x21000000 pc: 0x08000d76 msp: 0x20009fc0, semihosting

但是,在微控制器上运行的进程尚未终止,您可以使用continue或类似命令恢复它。

您现在可以使用该quit命令退出 GDB 。

(gdb) quit

调试现在需要更多的步骤,因此我们将所有这些步骤打包到一个名为openocd.gdb. 该文件是在该cargo generate步骤中创建的,应该无需任何修改即可工作。让我们有一个高峰:

cat openocd.gdb
target extended-remote :3333

# print demangled symbols
set print asm-demangle on

# detect unhandled exceptions, hard faults and panics
break DefaultHandler
break HardFault
break rust_begin_unwind

monitor arm semihosting enable

load

# start the process but immediately halt the processor
stepi

现在运行<gdb> -x openocd.gdb target/thumbv7em-none-eabihf/debug/examples/hello将立即将 GDB 连接到 OpenOCD,启用半主机,加载程序并启动进程。

或者,您可以变成<gdb> -x openocd.gdb自定义运行cargo run程序来 构建程序启动 GDB 会话。此运行程序包含在.cargo/config.toml但已注释掉。

head -n10 .cargo/config.toml
[target.thumbv7m-none-eabi]
# uncomment this to make `cargo run` execute programs on QEMU
# runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# uncomment ONE of these three option to make `cargo run` start a GDB session
# which option to pick depends on your system
runner = "arm-none-eabi-gdb -x openocd.gdb"
# runner = "gdb-multiarch -x openocd.gdb"
# runner = "gdb -x openocd.gdb"
$ cargo run --example hello
(..)
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x1e70 lma 0x8000400
Loading section .rodata, size 0x61c lma 0x8002270
Start address 0x800144e, load size 10380
Transfer rate: 17 KB/sec, 3460 bytes/write.
(gdb)

Memory Mapped Registers

嵌入式系统只能通过执行普通的 Rust 代码并在 RAM 中移动数据来实现。如果我们想将任何信息输入或输出我们的系统(例如闪烁 LED、检测按钮按下或与某种总线上的片外外围设备通信),我们将不得不深入了解外设及其“内存映射寄存器”。

您可能会发现访问微控制器中的外围设备所需的代码已经编写好了,位于以下级别之一:

  • Micro-architecture Crate - 此类 crate 处理您的微控制器使用的处理器内核通用的任何有用例程,以及使用该特定类型处理器内核的所有微控制器通用的任何外围设备。例如,cortex-m crate 为您提供了启用和禁用中断的功能,这些功能对于所有基于 Cortex-M 的微控制器都是相同的。它还允许您访问所有基于 Cortex-M 的微控制器中包含的“SysTick”外设。
  • 外设访问板条箱 (PAC) - 这种板条箱是为您正在使用的微控制器的特定部件号定义的各种内存包装器寄存器的薄包装器。例如,tm4c123x适用于 Texas Instruments Tiva-C TM4C123 系列,或stm32f30x适用于 ST-Micro STM32F30x 系列。在这里,您将按照微控制器技术参考手册中给出的每个外围设备的操作说明直接与寄存器交互。
  • HAL Crate - 这些 crate 为您的特定处理器提供更加用户友好的 API,通常通过实现一些在Embedded-hal 中定义的常见特征。例如,这个 crate 可能提供一个Serial结构体,带有一个构造函数,该构造函数采用一组适当的 GPIO 引脚和波特率,并提供某种write_byte发送数据的功能。
  • Board Crate - 这些 crate 比 HAL Crate 更进一步,通过预配置各种外设和 GPIO 引脚以适合您正在使用的特定开发人员套件或板,例如stm32f3-discovery用于 STM32F3DISCOVERY 板。

板条箱

如果您不熟悉嵌入式 Rust,板条箱是完美的起点。他们很好地抽象了开始学习这个主题时可能会让人不知所措的硬件细节,并使标准任务变得简单,比如打开或关闭 LED。它们公开的功能因板而异。由于本书旨在保持与硬件无关,因此本书不会涵盖板条箱。

如果您想试用 STM32F3DISCOVERY 板,强烈建议查看stm32f3-discovery板箱,它提供使板 LED 闪烁、访问指南针、蓝牙等功能。discovery提供了一个很好的介绍使用板箱的。

但是,如果您正在使用一个还没有专用板条箱的系统,或者您需要现有板条箱未提供的功能,请继续阅读我们从底部开始的微架构板条箱。

微架构箱

让我们看看所有基于 Cortex-M 的微控制器通用的 SysTick 外设。我们可以在cortex-m crate 中找到一个非常低级的 API ,我们可以这样使用它:

#![no_std]
#![no_main]
use cortex_m::peripheral::{syst, Peripherals};
use cortex_m_rt::entry;
use panic_halt as _;

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take().unwrap();
    let mut systick = peripherals.SYST;
    systick.set_clock_source(syst::SystClkSource::Core);
    systick.set_reload(1_000);
    systick.clear_current();
    systick.enable_counter();
    while !systick.has_wrapped() {
        // Loop
    }

    loop {}
}

在功能SYST结构非常紧密映射到由ARM技术参考手册此外设定义的功能。这个 API 中没有关于“延迟 X 毫秒”的内容——我们必须自己使用while循环粗略地实现它。请注意,SYST在调用之前我们无法访问我们的结构Peripherals::take()- 这是一个特殊的例程,可保证SYST我们整个程序中只有一个结构。

使用外设访问箱 (PAC)

如果我们将自己限制在每个 Cortex-M 附带的基本外围设备上,我们的嵌入式软件开发就不会走得太远。在某些时候,我们将需要编写一些特定于我们正在使用的特定微控制器的代码。在这个例子中,我们假设我们有一个 Texas Instruments TM4C123——一个中等的 80MHz Cortex-M4,具有 256 KiB 的闪存。我们将拉入tm4c123x板条箱以使用该芯片。

#![no_std]
#![no_main]

use panic_halt as _; // panic handler

use cortex_m_rt::entry;
use tm4c123x;

#[entry]
pub fn init() -> (Delay, Leds) {
    let cp = cortex_m::Peripherals::take().unwrap();
    let p = tm4c123x::Peripherals::take().unwrap();

    let pwm = p.PWM0;
    pwm.ctl.write(|w| w.globalsync0().clear_bit());
    // Mode = 1 => Count up/down mode
    pwm._2_ctl.write(|w| w.enable().set_bit().mode().set_bit());
    pwm._2_gena.write(|w| w.actcmpau().zero().actcmpad().one());
    // 528 cycles (264 up and down) = 4 loops per video line (2112 cycles)
    pwm._2_load.write(|w| unsafe { w.load().bits(263) });
    pwm._2_cmpa.write(|w| unsafe { w.compa().bits(64) });
    pwm.enable.write(|w| w.pwm4en().set_bit());
}

我们访问的PWM0外设完全相同的方式,因为我们访问的SYST外围较早,但我们叫tm4c123x::Peripherals::take()。由于这个 crate 是使用svd2rust自动生成的,我们寄存器字段的访问函数采用闭包,而不是数字参数。虽然这看起来像很多代码,但 Rust 编译器可以使用它为我们执行一堆检查,然后生成非常接近手写汇编器的机器代码!自动生成的代码无法确定特定访问器函数的所有可能参数都有效(例如,如果 SVD 将寄存器定义为 32 位,但没有说明这些 32 位值中的某些值是否有效)有特殊意义),则该函数被标记为unsafe. 在使用该函数设置loadcompa子字段时,我们可以在上面的示例中看到这一点bits()

read()函数返回一个对象,该对象提供对该寄存器中各个子字段的只读访问,如该芯片制造商的 SVD 文件所定义。您可以Rtm4c123x 文档中找到此特定寄存器的特殊返回类型、此特定外设、此特定芯片上的所有可用函数。

if pwm.ctl.read().globalsync0().is_set() {
    // Do a thing
}

write()函数采用带有单个参数的闭包。通常我们称之为w. 然后,此参数提供对该寄存器内的各种子字段的读写访问,如该芯片制造商的 SVD 文件所定义的那样。同样,您可以在tm4c123x 文档中找到此特定寄存器、此特定外设、此特定芯片上的“w”上可用的所有功能。请注意,我们未设置的所有子字段将为我们设置为默认值 - 寄存器中的任何现有内容都将丢失。

pwm.ctl.write(|w| w.globalsync0().clear_bit());

修改

如果我们只想改变这个寄存器中的一个特定子字段而其他子字段保持不变,我们可以使用该modify函数。这个函数接受一个带有两个参数的闭包——一个用于读取,一个用于写入。通常我们分别称它们为rw。该r参数可以用来检查所述寄存器的当前内容,并且w参数可以用来修改寄存器的内容。

pwm.ctl.modify(|r, w| w.globalsync0().clear_bit());

modify函数在这里真正展示了闭包的威力。在 C 中,我们必须读入一些临时值,修改正确的位,然后将值写回。这意味着存在相当大的错误范围:

uint32_t temp = pwm0.ctl.read();
temp |= PWM0_CTL_GLOBALSYNC0;
pwm0.ctl.write(temp);
uint32_t temp2 = pwm0.enable.read();
temp2 |= PWM0_ENABLE_PWM4EN;
pwm0.enable.write(temp); // Uh oh! Wrong variable!

使用 HAL 板条箱

芯片的 HAL crate 通常通过为 PAC 公开的原始结构实现自定义 Trait 来工作。通常,此特征会定义一个函数,调用constrain()单个外设或split()具有多个引脚的 GPIO 端口之类的东西。此函数将消耗底层原始外围结构并返回具有更高级别 API 的新对象。这个 API 也可以做一些事情,比如让串口new功能需要借用一些Clock结构,只能通过调用配置 PLL 和设置所有时钟频率的函数来生成。通过这种方式,静态地不可能在没有首先配置时钟速率的情况下创建串行端口对象,或者串行端口对象将波特率错误地转换为时钟滴答。一些 crate 甚至为每个 GPIO 引脚可以处于的状态定义特殊特征,要求用户在将引脚传递到外设之前将引脚置于正确的状态(例如,通过选择适当的备用功能模式)。一切都没有运行时成本!

让我们看一个例子:

#![no_std]
#![no_main]

use panic_halt as _; // panic handler

use cortex_m_rt::entry;
use tm4c123x_hal as hal;
use tm4c123x_hal::prelude::*;
use tm4c123x_hal::serial::{NewlineMode, Serial};
use tm4c123x_hal::sysctl;

#[entry]
fn main() -> ! {
    let p = hal::Peripherals::take().unwrap();
    let cp = hal::CorePeripherals::take().unwrap();

    // Wrap up the SYSCTL struct into an object with a higher-layer API
    let mut sc = p.SYSCTL.constrain();
    // Pick our oscillation settings
    sc.clock_setup.oscillator = sysctl::Oscillator::Main(
        sysctl::CrystalFrequency::_16mhz,
        sysctl::SystemClock::UsePll(sysctl::PllOutputFrequency::_80_00mhz),
    );
    // Configure the PLL with those settings
    let clocks = sc.clock_setup.freeze();

    // Wrap up the GPIO_PORTA struct into an object with a higher-layer API.
    // Note it needs to borrow `sc.power_control` so it can power up the GPIO
    // peripheral automatically.
    let mut porta = p.GPIO_PORTA.split(&sc.power_control);

    // Activate the UART.
    let uart = Serial::uart0(
        p.UART0,
        // The transmit pin
        porta
            .pa1
            .into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
        // The receive pin
        porta
            .pa0
            .into_af_push_pull::<hal::gpio::AF1>(&mut porta.control),
        // No RTS or CTS required
        (),
        (),
        // The baud rate
        115200_u32.bps(),
        // Output handling
        NewlineMode::SwapLFtoCRLF,
        // We need the clock rates to calculate the baud rate divisors
        &clocks,
        // We need this to power up the UART peripheral
        &sc.power_control,
    );

    loop {
        writeln!(uart, "Hello, World!\r\n").unwrap();
    }
}

Semihosting(半主机)

半主机是一种让嵌入式设备在主机上进行 I/O 的机制,主要用于将消息记录到主机控制台。半主机需要一个调试会话,几乎不需要其他任何东西(没有额外的电线!)所以它使用起来非常方便。缺点是它非常慢:根据您使用的硬件调试器(例如 ST-Link),每个写入操作可能需要几毫秒。

cortex-m-semihosting箱提供了一个API做的Cortex-M的设备半主机操作。下面的程序是“Hello, world!”的半主机版本:

#![no_main]
#![no_std]

use panic_halt as _;

use cortex_m_rt::entry;
use cortex_m_semihosting::hprintln;

#[entry]
fn main() -> ! {
    hprintln!("Hello, world!").unwrap();

    loop {}
}

如果你在硬件上运行这个程序,你会看到“Hello, world!” OpenOCD 日志中的消息。

$ openocd
(..)
Hello, world!
(..)

您确实需要先从 GDB 在 OpenOCD 中启用半主机:

(gdb) monitor arm semihosting enable
semihosting is enabled

QEMU 理解半主机操作,因此上述程序也可以运行而 qemu-system-arm无需启动调试会话。请注意,您需要将-semihosting-config标志传递给 QEMU 以启用半主机支持;这些标志已经包含在.cargo/config.toml模板文件中。

$ # this program will block the terminal
$ cargo run
     Running `qemu-system-arm (..)
Hello, world!

还有一个exit半主机操作可用于终止 QEMU 进程。重要提示:千万不能使用debug::exit硬件; 此功能可能会损坏您的 OpenOCD 会话,并且在您重新启动它之前您将无法调试更多程序。

#![no_main]
#![no_std]

use panic_halt as _;

use cortex_m_rt::entry;
use cortex_m_semihosting::debug;

#[entry]
fn main() -> ! {
    let roses = "blue";

    if roses == "red" {
        debug::exit(debug::EXIT_SUCCESS);
    } else {
        debug::exit(debug::EXIT_FAILURE);
    }

    loop {}
}
$ cargo run
     Running `qemu-system-arm (..)

$ echo $?
1

最后一个提示:您可以将恐慌行为设置为exit(EXIT_FAILURE). 这将让您编写no_std可以在 QEMU 上运行的运行通过测试。

为方便起见,panic-semihosting板条箱具有“退出”功能,启用exit(EXIT_FAILURE)后会在将紧急消息记录到主机 stderr 后调用。

#![no_main]
#![no_std]

use panic_semihosting as _; // features = ["exit"]

use cortex_m_rt::entry;
use cortex_m_semihosting::debug;

#[entry]
fn main() -> ! {
    let roses = "blue";

    assert_eq!(roses, "red");

    loop {}
}
$ cargo run
     Running `qemu-system-arm (..)
panicked at 'assertion failed: `(left == right)`
  left: `"blue"`,
 right: `"red"`', examples/hello.rs:15:5

$ echo $?
1

注意:要在 上启用此功能panic-semihosting,请编辑您的 Cargo.toml依赖项部分,其中panic-semihosting指定为:

panic-semihosting = { version = "VERSION", features = ["exit"] }

VERSION想要的版本在哪里。有关依赖项功能的更多信息,请查看specifying dependenciesCargo 书的部分。

Panicking(恐慌)

恐慌是 Rust 语言的核心部分。索引等内置操作会在运行时检查内存安全。当尝试越界索引时,这会导致恐慌。

在标准库中,恐慌有一个定义的行为:它展开恐慌线程的堆栈,除非用户选择在恐慌时中止程序。

然而,在没有标准库的程序中,恐慌行为是未定义的。可以通过声明#[panic_handler]函数来选择行为。该函数必须在程序的依赖图中只出现一次,并且必须具有以下签名:fn(&PanicInfo) -> !,其中PanicInfo 是一个包含有关恐慌位置信息的结构。

鉴于嵌入式系统的范围从面向用户到安全关键(不能崩溃),没有一种适合所有恐慌行为的尺寸,但有很多常用行为。这些常见的行为已经被打包到定义#[panic_handler]函数的crate 中。一些例子包括:

  • panic-abort. 恐慌导致中止指令被执行。
  • panic-halt. 恐慌导致程序或当前线程通过进入无限循环而停止。
  • panic-itm. 使用 ITM(一种 ARM Cortex-M 特定外设)记录紧急消息。
  • panic-semihosting. 使用半主机技术将恐慌消息记录到主机。

您可以panic-handler 在 crates.io 上找到更多搜索关键字的 crate。

程序可以通过链接到相应的 crate 来选择这些行为之一。在应用程序的源代码中将恐慌行为表示为一行代码这一事实不仅可用作文档,而且还可用于根据编译配置文件更改恐慌行为。例如:

#![no_main]
#![no_std]

// dev profile: easier to debug panics; can put a breakpoint on `rust_begin_unwind`
#[cfg(debug_assertions)]
use panic_halt as _;

// release profile: minimize the binary size of the application
#[cfg(not(debug_assertions))]
use panic_abort as _;

// ..

在此示例中,panic-halt使用 dev 配置文件 ( cargo build)构建时crate 链接到crate ,但panic-abort使用 release 配置文件 ( cargo build --release)构建时链接到crate 。

语句的use panic_abort as _;形式use用于确保panic_abort恐慌处理程序包含在我们的最终可执行文件中,同时向编译器明确我们不会显式使用 crate 中的任何内容。如果没有as _重命名,编译器会警告我们有一个未使用的导入。有时候,你可能会看到extern crate panic_abort,而不是,这是2018年版的锈之前使用的旧风格,现在应该仅用于“SYSROOT”箱子(那些散发着铁锈本身),例如proc_macroallocstd,和test

一个例子

这是一个尝试对超出其长度的数组进行索引的示例。操作导致恐慌。

#![no_main]
#![no_std]

use panic_semihosting as _;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    let xs = [0, 1, 2];
    let i = xs.len() + 1;
    let _y = xs[i]; // out of bounds access

    loop {}
}

此示例选择了panic-semihosting使用半主机将紧急消息打印到主机控制台的行为。

$ cargo run
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
panicked at 'index out of bounds: the len is 3 but the index is 4', src/main.rs:12:13

您可以尝试将行为更改为panic-halt并确认在这种情况下不会打印任何消息。

Exceptions(异常)

异常和中断是处理器处理异步事件和致命错误(例如执行无效指令)的硬件机制。异常意味着抢占并涉及异常处理程序、响应触发事件的信号而执行的子程序。

cortex-m-rt箱提供了一个exception声明异常处理程序属性。

// Exception handler for the SysTick (System Timer) exception
#[exception]
fn SysTick() {
    // ..
}

除了exception属性异常处理程序看起来像普通函数之外,还有一个区别:exception处理程序不能被软件调用。按照前面的示例,该语句SysTick(); 将导致编译错误。

这种行为几乎是有意为之,它需要提供一个特性: 处理程序中static mut声明的变量可以安全使用。 exception

#[exception]
fn SysTick() {
    static mut COUNT: u32 = 0;

    // `COUNT` has transformed to type `&mut u32` and it's safe to use
    *COUNT += 1;
}

您可能知道,static mut在函数中使用变量会使其 不可重入。从多个异常/中断处理程序或从main一个或多个异常/中断处理程序直接或间接调用不可重入函数是未定义的行为 。

安全 Rust 绝不能导致未定义的行为,因此不可重入函数必须标记为unsafe. 然而我只是告诉exception处理程序可以安全地使用static mut变量。这怎么可能?这是可能的,因为 exception处理程序不能被软件调用,因此重入是不可能的。

请注意,该exception属性通过将静态变量包装到unsafe块中并为我们提供新的适当类型&mut的同名变量来转换函数内静态变量的定义。因此,我们可以通过*访问变量的值来取消引用引用,而无需将它们包装在一个unsafe块中。

一个完整的例子

这是一个使用系统计时器SysTick大约每秒引发一次异常的示例。在SysTick异常处理程序跟踪它已经多少次被称为在COUNT变量,然后打印出的值 COUNT使用半主机主机控制台。

注意:您可以在任何 Cortex-M 设备上运行此示例;你也可以在 QEMU 上运行它

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use panic_halt as _;

use core::fmt::Write;

use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

#[entry]
fn main() -> ! {
    let p = cortex_m::Peripherals::take().unwrap();
    let mut syst = p.SYST;

    // configures the system timer to trigger a SysTick exception every second
    syst.set_clock_source(SystClkSource::Core);
    // this is configured for the LM3S6965 which has a default CPU clock of 12 MHz
    syst.set_reload(12_000_000);
    syst.clear_current();
    syst.enable_counter();
    syst.enable_interrupt();

    loop {}
}

#[exception]
fn SysTick() {
    static mut COUNT: u32 = 0;
    static mut STDOUT: Option<HStdout> = None;

    *COUNT += 1;

    // Lazy initialization
    if STDOUT.is_none() {
        *STDOUT = hio::hstdout().ok();
    }

    if let Some(hstdout) = STDOUT.as_mut() {
        write!(hstdout, "{}", *COUNT).ok();
    }

    // IMPORTANT omit this `if` block if running on real hardware or your
    // debugger will end in an inconsistent state
    if *COUNT == 9 {
        // This will terminate the QEMU process
        debug::exit(debug::EXIT_SUCCESS);
    }
}
tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-rt = "0.6.3"
panic-halt = "0.2.0"
cortex-m-semihosting = "0.3.1"
$ cargo run --release
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
123456789

如果您在 Discovery 板上运行它,您将在 OpenOCD 控制台上看到输出。此外,当计数达到 9 时,程序不会停止。

默认异常处理程序

exception属性的实际作用是覆盖特定异常的默认异常处理程序。如果您不覆盖特定异常的处理程序,它将由DefaultHandler函数处理,该函数默认为:

fn DefaultHandler() {
    loop {}
}

此函数由cortex-m-rtcrate提供并标记为, #[no_mangle]以便您可以在“DefaultHandler”上放置断点并捕获 未处理的异常。

可以DefaultHandler使用exception属性覆盖它:

#[exception]
fn DefaultHandler(irqn: i16) {
    // custom default handler
}

irqn参数指示正在处理哪个异常。负值表示正在处理 Cortex-M 异常;零或正值表示正在处理设备特定异常(AKA 中断)。

硬故障处理程序

HardFault例外的情况比较特殊。当程序进入无效状态时会触发此异常,因此其处理程序无法返回,因为这可能导致未定义的行为。此外,运行时 crate 在HardFault调用用户定义的处理程序之前会做一些工作,以提高可调试性。

结果是HardFault处理程序必须具有以下签名: fn(&ExceptionFrame) -> !. 处理程序的参数是一个指向由异常压入堆栈的寄存器的指针。这些寄存器是触发异常时处理器状态的快照,可用于诊断硬故障。

这是一个执行非法操作的示例:读取不存在的内存位置。

注意:这个程序在 QEMUqemu-system-arm -machine lm3s6965evb上无法运行,即它不会崩溃,因为 它不检查内存负载,并且会很高兴地0在读取无效内存时返回。

#![no_main]
#![no_std]

use panic_halt as _;

use core::fmt::Write;
use core::ptr;

use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;

#[entry]
fn main() -> ! {
    // read a nonexistent memory location
    unsafe {
        ptr::read_volatile(0x3FFF_FFFE as *const u32);
    }

    loop {}
}

#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
    if let Ok(mut hstdout) = hio::hstdout() {
        writeln!(hstdout, "{:#?}", ef).ok();
    }

    loop {}
}

HardFault处理器打印ExceptionFrame值。如果您运行它,您将在 OpenOCD 控制台上看到类似的内容。

$ openocd
(..)
ExceptionFrame {
    r0: 0x3ffffffe,
    r1: 0x00f00000,
    r2: 0x20000000,
    r3: 0x00000000,
    r12: 0x00000000,
    lr: 0x080008f7,
    pc: 0x0800094a,
    xpsr: 0x61000000
}

pc值是异常发生时程序计数器的值,它指向触发异常的指令。

如果你看一下程序的反汇编:

$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
(..)
ResetTrampoline:
 8000942:       movw    r0, #0xfffe
 8000946:       movt    r0, #0x3fff
 800094a:       ldr     r0, [r0]
 800094c:       b       #-0x4 

您可以在反0x0800094a汇编中查找程序计数器的值。您将看到加载操作 ( ldr r0, [r0]) 导致了异常。本r0ExceptionFrame会告诉你寄存器的值r00x3fff_fffe在那个时候。

Interrupts(中断)

中断在很多方面与异常不同,但它们的操作和使用大体相似,并且它们也由相同的中断控制器处理。尽管异常由 Cortex-M 架构定义,但中断始终是供应商(通常甚至是芯片)在命名和功能方面的特定实现。

中断确实提供了很大的灵活性,在尝试以高级方式使用它们时需要考虑到这一点。我们不会在本书中介绍这些用途,但记住以下几点是个好主意:

  • 中断具有可编程的优先级,这些优先级决定了它们的处理程序的执行顺序
  • 中断可以嵌套和抢占,即中断处理程序的执行可能会被另一个更高优先级的中断中断
  • 一般需要清除导致中断触发的原因,防止无休止地重新进入中断处理程序

运行时的一般初始化步骤始终相同:

  • 设置外设以在所需场合生成中断请求
  • 在中断控制器中设置中断处理程序所需的优先级
  • 在中断控制器中启用中断处理程序

与异常类似,cortex-m-rtcrate 提供了一个interrupt 属性来声明中断处理程序。可用的中断(以及它们在中断处理程序表中的位置)通常是通过svd2rustSVD 描述自动生成的。

// Interrupt handler for the Timer2 interrupt
#[interrupt]
fn TIM2() {
    // ..
    // Clear reason for the generated interrupt request
}

中断处理程序看起来像普通函数(除了缺少参数)类似于异常处理程序。然而,由于特殊的调用约定,它们不能被固件的其他部分直接调用。然而,可以在软件中生成中断请求以触发对中断处理程序的转移。

与异常处理程序类似,也可以static mut 在中断处理程序中声明变量以保持安全状态。

#[interrupt]
fn TIM2() {
    static mut COUNT: u32 = 0;

    // `COUNT` has type `&mut u32` and it's safe to use
    *COUNT += 1;
}

IO

TODO Cover memory mapped I/O using registers.

Peripherals(外设)

什么是外设?

大多数微控制器不仅仅是一个 CPU、RAM 或闪存——它们包含硅片部分,用于与微控制器之外的系统进行交互,以及通过传感器、电机控制器直接或间接地与周围环境进行交互,或人机界面,如显示器或键盘。这些组件统称为外围设备。

这些外设很有用,因为它们允许开发人员将处理工作卸载给它们,从而避免必须在软件中处理所有事情。类似于桌面开发人员将图形处理卸载到视频卡的方式,嵌入式开发人员可以将一些任务卸载到外围设备,从而让 CPU 将时间花在做其他重要的事情上,或者什么都不做以节省电量。

如果您查看 1970 年代或 1980 年代的老式家用计算机的主电路板(实际上,昨天的台式 PC 与今天的嵌入式系统相距甚远),您会期望看到:

  • 处理器
  • 一个内存芯片
  • 一个ROM芯片
  • 一个 I/O 控制器

RAM 芯片、ROM 芯片和 I/O 控制器(该系统中的外围设备)将通过一系列称为“总线”的并行迹线连接到处理器。该总线承载地址信息,它选择处理器希望与总线上的哪个设备进行通信,以及承载实际数据的数据总线。在我们的嵌入式微控制器中,同样的原则适用 - 只是所有东西都封装在一块硅上。

但是,与通常具有 Vulkan、Metal 或 OpenGL 等软件 API 的图形卡不同,外围设备通过硬件接口暴露给我们的微控制器,该接口映射到一块内存。

线性和实内存空间

在微控制器上,将一些数据写入其他任意地址(例如0x4000_00000x0000_0000)也可能是完全有效的操作。

在桌面系统上,对内存的访问由 MMU 或内存管理单元严格控制。该组件有两个主要职责:强制执行对内存部分的访问权限(防止一个进程读取或修改另一个进程的内存);并将物理内存的段重新映射到软件中使用的虚拟内存范围。微控制器通常没有 MMU,而是仅在软件中使用真实的物理地址。

尽管 32 位微控制器具有来自0x0000_0000, 和的真实线性地址空间0xFFFF_FFFF,但它们通常只使用该范围的几百 KB 用于实际内存。这留下了大量的地址空间。在前面的章节中,我们讨论了 RAM 位于 address 0x2000_0000。如果我们的RAM为64昆明植物研究所长(即0xFFFF的最大地址),然后地址0x2000_0000,以0x2000_FFFF将对应于我们的RAM。当我们写入一个位于 address 的变量0x2000_1234时,内部发生的事情是一些逻辑检测地址的高位部分(在本例中为 0x2000),然后激活 RAM 以便它可以对地址的低位部分(0x1234在这种情况下)。在 Cortex-M 上,我们也将闪存 ROM 映射到地址0x0000_0000直到地址0x0007_FFFF(如果我们有 512 KiB 闪存 ROM)。微控制器设计者并没有忽略这两个区域之间的所有剩余空间,而是将外围设备的接口映射到某些存储器位置。这最终看起来像这样:

Nordic nRF52832 数据表 (pdf)

内存映射外设

乍一看,与这些外设的交互很简单——将正确的数据写入正确的地址。例如,通过串行端口发送 32 位字可能与将该 32 位字写入某个内存地址一样直接。然后串行端口外围设备将接管并自动发送数据。

这些外设的配置工作类似。不是调用函数来配置外设,而是公开了一块内存,用作硬件 API。写入0x8000_0000SPI 频率配置寄存器,SPI 端口将以每秒 8 兆位的速度发送数据。写入0x0200_0000相同的地址,SPI 端口将以每秒 125 千位的速度发送数据。这些配置寄存器看起来有点像这样:

Nordic nRF52832 数据表 (pdf)

无论使用何种语言,无论该语言是汇编、C 还是 Rust,此接口都是与硬件进行交互的方式。

A First Attempt

寄存器

让我们看看“SysTick”外设 - 每个 Cortex-M 处理器内核都附带一个简单的计时器。通常,您会在芯片制造商的数据表或技术参考手册中查找这些内容,但此示例适用于所有 ARM Cortex-M 内核,让我们查看ARM 参考手册。我们看到有四个寄存器:

抵消 姓名 描述 宽度
0x00 SYST_CSR 控制和状态寄存器 32位
0x04 SYST_RVR 重载值寄存器 32位
0x08 SYST_CVR 当前值寄存器 32位
0x0C SYST_CALIB 校准值寄存器 32位

C 方法

在 Rust 中,我们可以用与在 C 中完全相同的方式来表示一组寄存器 - 使用struct.

#[repr(C)]
struct SysTick {
    pub csr: u32,
    pub rvr: u32,
    pub cvr: u32,
    pub calib: u32,
}

限定符#[repr(C)]告诉 Rust 编译器像 C 编译器那样布置这个结构。这非常重要,因为 Rust 允许重新排序结构字段,而 C 则不允许。您可以想象如果这些字段被编译器默默地重新排列,我们将不得不进行调试!有了这个限定符,我们就有了与上表相对应的四个 32 位字段。但是当然,这struct本身是没有用的——我们需要一个变量。

let systick = 0xE000_E010 as *mut SysTick;
let time = unsafe { (*systick).cvr };

Volatile Accesses

现在,上述方法存在一些问题。

  1. 每次我们想访问我们的外设时,我们都必须使用 unsafe。
  2. 我们无法指定哪些寄存器是只读的或读写的。
  3. 程序中任何位置的任何代码都可以通过此结构访问硬件。
  4. 最重要的是,它实际上不起作用……

现在,问题是编译器很聪明。如果您对同一块 RAM 进行两次写入,一个接一个,编译器会注意到这一点,并且完全跳过第一次写入。在 C 中,我们可以将变量标记为volatile确保每次读取或写入都按预期进行。在 Rust 中,我们将访问标记为 volatile,而不是变量。

let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };

所以,我们已经解决了四个问题之一,但现在我们有更多的unsafe代码!幸运的是,有一个第三方板条箱可以提供帮助 - volatile_register.

use volatile_register::{RW, RO};

#[repr(C)]
struct SysTick {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

fn get_systick() -> &'static mut SysTick {
    unsafe { &mut *(0xE000_E010 as *mut SysTick) }
}

fn get_time() -> u32 {
    let systick = get_systick();
    systick.cvr.read()
}

现在,易失性访问是通过readwrite方法自动执行的。它仍然unsafe是执行写入,但公平地说,硬件是一堆可变状态,编译器无法知道这些写入是否真正安全,因此这是一个很好的默认位置。

The Rusty Wrapper

我们需要将其封装struct到一个更高层的 API 中,以便我们的用户可以安全地调用。作为驱动程序作者,我们手动验证不安全代码是否正确,然后为我们的用户提供一个安全的 API,这样他们就不必担心(前提是他们相信我们会做对!)。

一个例子可能是:

use volatile_register::{RW, RO};

pub struct SystemTimer {
    p: &'static mut RegisterBlock
}

#[repr(C)]
struct RegisterBlock {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

impl SystemTimer {
    pub fn new() -> SystemTimer {
        SystemTimer {
            p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
        }
    }

    pub fn get_time(&self) -> u32 {
        self.p.cvr.read()
    }

    pub fn set_reload(&mut self, reload_value: u32) {
        unsafe { self.p.rvr.write(reload_value) }
    }
}

pub fn example_usage() -> String {
    let mut st = SystemTimer::new();
    st.set_reload(0x00FF_FFFF);
    format!("Time is now 0x{:08x}", st.get_time())
}

现在,这种方法的问题是编译器完全可以接受以下代码:

fn thread1() {
    let mut st = SystemTimer::new();
    st.set_reload(2000);
}

fn thread2() {
    let mut st = SystemTimer::new();
    st.set_reload(1000);
}

我们&mut self对该set_reload函数的参数检查是否没有对该特定SystemTimer结构的其他引用,但它们不会阻止用户创建第二个SystemTimer指向完全相同的外围设备!如果作者足够勤奋地发现所有这些“重复”的驱动程序实例,以这种方式编写的代码将起作用,但是一旦代码分布在多个模块、驱动程序、开发人员和数天中,编写这些代码会变得越来越容易各种错误。

The Borrow Checker

可变全局状态

不幸的是,硬件基本上只是可变的全局状态,这对于 Rust 开发人员来说可能会感到非常可怕。硬件独立于我们编写的代码结构而存在,并且可以随时被现实世界修改。

我们的规则应该是什么?

我们如何才能可靠地与这些外围设备交互?

  1. 始终使用volatile读取或写入外围存储器的方法,因为它可以随时更改
  2. 在软件中,我们应该能够共享对这些外设的任意数量的只读访问
  3. 如果某些软件应该具有对外围设备的读写访问权限,则它应该持有对该外围设备的唯一引用

借用检查器

这些规则中的最后两条听起来与 Borrow Checker 已经做的很相似!

想象一下,我们是否可以传递这些外围设备的所有权,或者为它们提供不可变或可变的引用?

嗯,我们可以,但是对于 Borrow Checker,我们需要每个外设正好有一个实例,这样 Rust 才能正确处理这个问题。好吧,幸运的是在硬件中,任何给定的外围设备只有一个实例,但是我们如何在代码结构中公开它?

Singletons

在软件工程中,单例模式是一种软件设计模式,它将类的实例化限制为一个对象。

维基百科:单例模式

但是为什么我们不能只使用全局变量呢?

我们可以让一切都变成公共静态,就像这样

static mut THE_SERIAL_PORT: SerialPort = SerialPort;

fn main() {
    let _ = unsafe {
        THE_SERIAL_PORT.read_speed();
    };
}

但这有几个问题。它是一个可变的全局变量,在 Rust 中,与这些变量交互总是不安全的。这些变量在整个程序中也是可见的,这意味着借用检查器无法帮助您跟踪这些变量的引用和所有权。

我们如何在 Rust 中做到这一点?

我们不只是让我们的外设成为一个全局变量,而是决定创建一个全局变量,在这种情况下称为PERIPHERALS,它包含Option<T>我们每个外设的 。

struct Peripherals {
    serial: Option<SerialPort>,
}
impl Peripherals {
    fn take_serial(&mut self) -> SerialPort {
        let p = replace(&mut self.serial, None);
        p.unwrap()
    }
}
static mut PERIPHERALS: Peripherals = Peripherals {
    serial: Some(SerialPort),
};

这种结构使我们能够获得外围设备的单个实例。如果我们尝试take_serial()多次调用,我们的代码就会崩溃!

fn main() {
    let serial_1 = unsafe { PERIPHERALS.take_serial() };
    // This panics!
    // let serial_2 = unsafe { PERIPHERALS.take_serial() };
}

尽管与这个结构交互是unsafe,但是一旦我们包含了SerialPort它,我们就不再需要使用unsafe,或者根本不需要使用该PERIPHERALS结构。

这有一个小的运行时开销,因为我们必须将SerialPort结构包装在一个选项中,我们需要调用take_serial()一次,但是这个小的前期成本允许我们在我们程序的其余部分中利用借用检查器。

现有库支持

尽管我们在Peripherals上面创建了自己的结构,但没有必要为您的代码执行此操作。在cortex_m箱子中含有一种叫宏singleton!(),会为你执行此操作。

#[macro_use(singleton)]
extern crate cortex_m;

fn main() {
    // OK if `main` is executed only once
    let x: &'static mut bool =
        singleton!(: bool = false).unwrap();
}

cortex_m 文档

此外,如果您使用cortex-m-rtic,则定义和获取这些外设的整个过程都会为您抽象化,您将获得一个Peripherals结构,其中包含Option<T>您定义的所有项目的非版本。

// cortex-m-rtic v0.5.x
#[rtic::app(device = lm3s6965, peripherals = true)]
const APP: () = {
    #[init]
    fn init(cx: init::Context) {
        static mut X: u32 = 0;

        // Cortex-M peripherals
        let core: cortex_m::Peripherals = cx.core;

        // Device specific peripherals
        let device: lm3s6965::Peripherals = cx.device;
    }
}

但为什么?

但是这些单例如何对我们的 Rust 代码的工作方式产生显着的影响?

impl SerialPort {
    const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

    fn read_speed(
        &self // <------ This is really, really important
    ) -> u32 {
        unsafe {
            ptr::read_volatile(Self::SER_PORT_SPEED_REG)
        }
    }
}

这里有两个重要因素在起作用:

  • 因为我们使用的是单例,所以只有一种方法或地方可以获取SerialPort结构
  • 要调用该read_speed()方法,我们必须拥有所有权或对SerialPort结构的引用

这两个因素放在一起意味着只有在我们适当满足借用检查器的情况下才能访问硬件,这意味着我们在任何时候都不会对同一硬件有多个可变引用!

fn main() {
    // missing reference to `self`! Won't work.
    // SerialPort::read_speed();

    let serial_1 = unsafe { PERIPHERALS.take_serial() };

    // you can only read what you have access to
    let _ = serial_1.read_speed();
}

像对待数据一样对待硬件

此外,由于一些引用是可变的,而一些引用是不可变的,因此可以查看函数或方法是否有可能修改硬件的状态。例如,

允许更改硬件设置:

fn setup_spi_port(
    spi: &mut SpiPort,
    cs_pin: &mut GpioPin
) -> Result<()> {
    // ...
}

这不是:

fn read_button(gpio: &GpioPin) -> bool {
    // ...
}

这允许我们强制代码是否应该或不应该在编译时而不是在运行时对硬件进行更改。请注意,这通常仅适用于一个应用程序,但对于裸机系统,我们的软件将被编译为单个应用程序,因此这通常不是限制。

Static Guarantees

Rust 的类型系统在编译时防止数据竞争(请参阅SendSync特征)。类型系统还可用于在编译时检查其他属性;在某些情况下减少运行时检查的需要。

例如,当应用于嵌入式程序时,可以使用这些静态检查来强制正确完成 I/O 接口的配置。例如,可以设计一个 API,其中只能通过首先配置接口将使用的引脚来初始化串行接口。

还可以静态检查操作,例如将引脚设置为低电平,只能在正确配置的外设上执行。例如,尝试更改配置为浮动输入模式的引脚的输出状态会引发编译错误。

并且,如前一章所见,所有权的概念可以应用于外设,以确保只有程序的某些部分可以修改外设。与将外围设备视为全局可变状态的替代方案相比,这种访问控制使软件更容易推理。

Typestate Programming

typestates的概念描述了将有关对象当前状态的信息编码为该对象的类型。虽然这听起来有点神秘,但如果您在 Rust 中使用过构建器模式,那么您已经开始使用 Typestate Programming!

pub mod foo_module {
    #[derive(Debug)]
    pub struct Foo {
        inner: u32,
    }

    pub struct FooBuilder {
        a: u32,
        b: u32,
    }

    impl FooBuilder {
        pub fn new(starter: u32) -> Self {
            Self {
                a: starter,
                b: starter,
            }
        }

        pub fn double_a(self) -> Self {
            Self {
                a: self.a * 2,
                b: self.b,
            }
        }

        pub fn into_foo(self) -> Foo {
            Foo {
                inner: self.a + self.b,
            }
        }
    }
}

fn main() {
    let x = foo_module::FooBuilder::new(10)
        .double_a()
        .into_foo();

    println!("{:#?}", x);
}

在这个例子中,没有直接的方法来创建一个Foo对象。我们必须创建一个FooBuilder,并正确初始化它,然后才能获得Foo我们想要的对象。

这个最小的例子编码了两个状态:

  • FooBuilder,表示“未配置”或“配置进行中”状态
  • Foo,表示“已配置”或“准备使用”状态。

强类型

因为 Rust 有一个强类型系统,没有简单的方法可以神奇地创建 的实例Foo,或者在不调用方法FooBuilderFoo情况下将 a变成 a into_foo()。此外,调用该into_foo()方法会消耗原始FooBuilder结构,这意味着如果不创建新实例就无法重用它。

这允许我们将系统的状态表示为类型,并将状态转换的必要操作包含到将一种类型交换为另一种类型的方法中。通过创建一个FooBuilder并将其交换为一个Foo对象,我们已经完成了基本状态机的步骤。

Peripherals as State Machines

微控制器的外围设备可以被认为是一组状态机。例如,简化GPIO 引脚的配置可以表示为以下状态树:

  • 已禁用
  • 启用
    • 配置为输出
      • 输出:高
      • 输出:低
    • 配置为输入
      • 输入:高阻
      • 输入:拉低
      • 输入:拉高

如果外设在Disabled模式下启动,要移动到Input: High Resistance模式,我们必须执行以下步骤:

  1. 已禁用
  2. 启用
  3. 配置为输入
  4. 输入:高阻

如果我们想从 移动Input: High ResistanceInput: Pulled Low,我们必须执行以下步骤:

  1. 输入:高阻
  2. 输入:拉低

同样,如果我们要将 GPIO 引脚从配置为 移动Input: Pulled LowOutput: High,我们必须执行以下步骤:

  1. 输入:拉低
  2. 配置为输入
  3. 配置为输出
  4. 输出:高

硬件表示

通常,上面列出的状态是通过将值写入映射到 GPIO 外设的给定寄存器来设置的。让我们定义一个虚构的 GPIO 配置寄存器来说明这一点:

姓名 位数 价值 意义 笔记
使能够 0 0 残疾 禁用 GPIO
1 启用 启用 GPIO
方向 1 0 输入 将方向设置为输入
1 输出 将方向设置为输出
输入模式 2..3 00 高阻抗 将输入设置为高电阻
01 下拉 输入引脚被拉低
10 拉高 输入引脚被拉高
11 不适用 无效状态。不要设置
输出模式 4 0 设置低 输出引脚被驱动为低电平
1 设置高 输出引脚被驱动为高电平
输入状态 5 X 有效值 0 如果输入 < 1.5v,1 如果输入 >= 1.5v

我们可以在 Rust 中暴露以下结构来控制这个 GPIO:

/// GPIO interface
struct GpioConfig {
    /// GPIO Configuration structure generated by svd2rust
    periph: GPIO_CONFIG,
}

impl GpioConfig {
    pub fn set_enable(&mut self, is_enabled: bool) {
        self.periph.modify(|_r, w| {
            w.enable().set_bit(is_enabled)
        });
    }

    pub fn set_direction(&mut self, is_output: bool) {
        self.periph.modify(|_r, w| {
            w.direction().set_bit(is_output)
        });
    }

    pub fn set_input_mode(&mut self, variant: InputMode) {
        self.periph.modify(|_r, w| {
            w.input_mode().variant(variant)
        });
    }

    pub fn set_output_mode(&mut self, is_high: bool) {
        self.periph.modify(|_r, w| {
            w.output_mode.set_bit(is_high)
        });
    }

    pub fn get_input_status(&self) -> bool {
        self.periph.read().input_status().bit_is_set()
    }
}

但是,这将允许我们修改某些没有意义的寄存器。例如,如果我们output_mode在 GPIO 配置为输入时设置该字段会发生什么?

一般来说,使用这种结构将允许我们达到上面状态机未定义的状态:例如,输出被拉低,或输入被设置为高。对于某些硬件,这可能无关紧要。在其他硬件上,它可能会导致意外或未定义的行为!

虽然这个接口编写起来很方便,但它并没有强制执行我们的硬件实现所规定的设计契约。

Design Contracts

在上一章中,我们编写了一个强制执行设计契约的接口。让我们再看看我们想象中的 GPIO 配置寄存器:

姓名 位数 价值 意义 笔记
使能够 0 0 残疾 禁用 GPIO
1 启用 启用 GPIO
方向 1 0 输入 将方向设置为输入
1 输出 将方向设置为输出
输入模式 2..3 00 高阻抗 将输入设置为高电阻
01 下拉 输入引脚被拉低
10 拉高 输入引脚被拉高
11 不适用 无效状态。不要设置
输出模式 4 0 设置低 输出引脚被驱动为低电平
1 设置高 输出引脚被驱动为高电平
输入状态 5 X 有效值 0 如果输入 < 1.5v,1 如果输入 >= 1.5v

如果我们在使用底层硬件之前检查状态,在运行时执行我们的设计契约,我们可能会编写如下代码:

/// GPIO interface
struct GpioConfig {
    /// GPIO Configuration structure generated by svd2rust
    periph: GPIO_CONFIG,
}

impl GpioConfig {
    pub fn set_enable(&mut self, is_enabled: bool) {
        self.periph.modify(|_r, w| {
            w.enable().set_bit(is_enabled)
        });
    }

    pub fn set_direction(&mut self, is_output: bool) -> Result<(), ()> {
        if self.periph.read().enable().bit_is_clear() {
            // Must be enabled to set direction
            return Err(());
        }

        self.periph.modify(|r, w| {
            w.direction().set_bit(is_output)
        });

        Ok(())
    }

    pub fn set_input_mode(&mut self, variant: InputMode) -> Result<(), ()> {
        if self.periph.read().enable().bit_is_clear() {
            // Must be enabled to set input mode
            return Err(());
        }

        if self.periph.read().direction().bit_is_set() {
            // Direction must be input
            return Err(());
        }

        self.periph.modify(|_r, w| {
            w.input_mode().variant(variant)
        });

        Ok(())
    }

    pub fn set_output_status(&mut self, is_high: bool) -> Result<(), ()> {
        if self.periph.read().enable().bit_is_clear() {
            // Must be enabled to set output status
            return Err(());
        }

        if self.periph.read().direction().bit_is_clear() {
            // Direction must be output
            return Err(());
        }

        self.periph.modify(|_r, w| {
            w.output_mode.set_bit(is_high)
        });

        Ok(())
    }

    pub fn get_input_status(&self) -> Result<bool, ()> {
        if self.periph.read().enable().bit_is_clear() {
            // Must be enabled to get status
            return Err(());
        }

        if self.periph.read().direction().bit_is_set() {
            // Direction must be input
            return Err(());
        }

        Ok(self.periph.read().input_status().bit_is_set())
    }
}

因为我们需要对硬件实施限制,所以我们最终会进行大量的运行时检查,这会浪费时间和资源,而且这些代码对于开发人员来说使用起来会很不愉快。

类型状态

但是,如果我们使用 Rust 的类型系统来强制执行状态转换规则呢?拿这个例子:

/// GPIO interface
struct GpioConfig<ENABLED, DIRECTION, MODE> {
    /// GPIO Configuration structure generated by svd2rust
    periph: GPIO_CONFIG,
    enabled: ENABLED,
    direction: DIRECTION,
    mode: MODE,
}

// Type states for MODE in GpioConfig
struct Disabled;
struct Enabled;
struct Output;
struct Input;
struct PulledLow;
struct PulledHigh;
struct HighZ;
struct DontCare;

/// These functions may be used on any GPIO Pin
impl<EN, DIR, IN_MODE> GpioConfig<EN, DIR, IN_MODE> {
    pub fn into_disabled(self) -> GpioConfig<Disabled, DontCare, DontCare> {
        self.periph.modify(|_r, w| w.enable.disabled());
        GpioConfig {
            periph: self.periph,
            enabled: Disabled,
            direction: DontCare,
            mode: DontCare,
        }
    }

    pub fn into_enabled_input(self) -> GpioConfig<Enabled, Input, HighZ> {
        self.periph.modify(|_r, w| {
            w.enable.enabled()
             .direction.input()
             .input_mode.high_z()
        });
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Input,
            mode: HighZ,
        }
    }

    pub fn into_enabled_output(self) -> GpioConfig<Enabled, Output, DontCare> {
        self.periph.modify(|_r, w| {
            w.enable.enabled()
             .direction.output()
             .input_mode.set_high()
        });
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Output,
            mode: DontCare,
        }
    }
}

/// This function may be used on an Output Pin
impl GpioConfig<Enabled, Output, DontCare> {
    pub fn set_bit(&mut self, set_high: bool) {
        self.periph.modify(|_r, w| w.output_mode.set_bit(set_high));
    }
}

/// These methods may be used on any enabled input GPIO
impl<IN_MODE> GpioConfig<Enabled, Input, IN_MODE> {
    pub fn bit_is_set(&self) -> bool {
        self.periph.read().input_status.bit_is_set()
    }

    pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
        self.periph.modify(|_r, w| w.input_mode().high_z());
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Input,
            mode: HighZ,
        }
    }

    pub fn into_input_pull_down(self) -> GpioConfig<Enabled, Input, PulledLow> {
        self.periph.modify(|_r, w| w.input_mode().pull_low());
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Input,
            mode: PulledLow,
        }
    }

    pub fn into_input_pull_up(self) -> GpioConfig<Enabled, Input, PulledHigh> {
        self.periph.modify(|_r, w| w.input_mode().pull_high());
        GpioConfig {
            periph: self.periph,
            enabled: Enabled,
            direction: Input,
            mode: PulledHigh,
        }
    }
}

现在让我们看看使用它的代码会是什么样子:

/*
 * Example 1: Unconfigured to High-Z input
 */
let pin: GpioConfig<Disabled, _, _> = get_gpio();

// Can't do this, pin isn't enabled!
// pin.into_input_pull_down();

// Now turn the pin from unconfigured to a high-z input
let input_pin = pin.into_enabled_input();

// Read from the pin
let pin_state = input_pin.bit_is_set();

// Can't do this, input pins don't have this interface!
// input_pin.set_bit(true);

/*
 * Example 2: High-Z input to Pulled Low input
 */
let pulled_low = input_pin.into_input_pull_down();
let pin_state = pulled_low.bit_is_set();

/*
 * Example 3: Pulled Low input to Output, set high
 */
let output_pin = pulled_low.into_enabled_output();
output_pin.set_bit(true);

// Can't do this, output pins don't have this interface!
// output_pin.into_input_pull_down();

这绝对是一种存储引脚状态的便捷方式,但为什么要这样做呢?为什么这比将状态存储在enum我们的GpioConfig结构内部更好?

编译时功能安全

因为我们完全在编译时强制执行我们的设计约束,所以这不会产生运行时成本。当引脚处于输入模式时,无法设置输出模式。相反,您必须通过将其转换为输出引脚来遍历状态,然后设置输出模式。因此,在执行函数之前检查当前状态不会造成运行时损失。

此外,由于这些状态是由类型系统强制执行的,因此该接口的使用者不再有错误的余地。如果他们尝试执行非法的状态转换,代码将无法编译!

Zero Cost Abstractions

类型状态也是零成本抽象的一个很好的例子 - 将某些行为移动到编译时执行或分析的能力。这些类型状态不包含实际数据,而是用作标记。由于它们不包含数据,因此它们在运行时在内存中没有实际表示:

use core::mem::size_of;

let _ = size_of::<Enabled>();    // == 0
let _ = size_of::<Input>();      // == 0
let _ = size_of::<PulledHigh>(); // == 0
let _ = size_of::<GpioConfig<Enabled, Input, PulledHigh>>(); // == 0

零尺寸类型

struct Enabled;

像这样定义的结构称为零大小类型,因为它们不包含实际数据。尽管这些类型在编译时是“真实的”——您可以复制它们、移动它们、引用它们等等,但是优化器将完全剥离它们。

在这段代码中:

pub fn into_input_high_z(self) -> GpioConfig<Enabled, Input, HighZ> {
    self.periph.modify(|_r, w| w.input_mode().high_z());
    GpioConfig {
        periph: self.periph,
        enabled: Enabled,
        direction: Input,
        mode: HighZ,
    }
}

我们返回的 GpioConfig 在运行时从不存在。调用此函数通常归结为单个汇编指令 - 将常量寄存器值存储到寄存器位置。这意味着我们开发的类型状态接口是一种零成本抽象——它不再使用 CPU、RAM 或代码空间来跟踪 的状态GpioConfig,并呈现为与直接寄存器访问相同的机器代码。

嵌套

一般而言,这些抽象可以根据您的需要进行嵌套。只要使用的所有组件都是零尺寸类型,整个结构在运行时就不会存在。

对于复杂或深度嵌套的结构,定义所有可能的状态组合可能很乏味。在这些情况下,宏可用于生成所有实现。

Portability(可移植性)

在嵌入式环境中,可移植性是一个非常重要的话题:每个供应商甚至来自单个制造商的每个系列都提供不同的外围设备和功能,同样,与外围设备交互的方式也会有所不同。

平衡这种差异的常用方法是通过称为硬件抽象层或HAL 的层。

硬件抽象是软件中的一组例程,用于模拟某些特定于平台的细节,使程序可以直接访问硬件资源。

它们通常允许程序员通过向硬件提供标准操作系统 (OS) 调用来编写独立于设备的高性能应用程序。

维基百科:硬件抽象层

嵌入式系统在这方面有点特殊,因为我们通常没有操作系统和用户可安装的软件,而是作为一个整体编译的固件映像以及许多其他限制。因此,虽然维基百科定义的传统方法可能有效,但它可能不是确保可移植性的最有效方法。

我们如何在 Rust 中做到这一点?输入嵌入式哈

什么是嵌入式hal?

简而言之,它是一组特性,定义了HAL 实现驱动程序应用程序(或固件)之间的实现契约。这些契约包括能力(即如果为某种类型实现了特征,HAL 实现提供了某种能力)和方法(即如果您可以构造一个实现特征的类型,则保证您拥有特征中指定的方法可用的)。

典型的分层可能如下所示:

嵌入式 hal中定义的一些特征是:

  • GPIO(输入和输出引脚)
  • 串行通讯
  • I2C
  • SPI
  • 计时器/倒计时
  • 模数转换

实现和使用嵌入式 hal特征和板条箱的主要原因是为了控制复杂性。如果您认为应用程序可能必须在硬件以及应用程序和其他硬件组件的潜在驱动程序中实现对外围设备的使用,那么应该很容易看出可重用性非常有限。用数学表示,如果M是外围 HAL 实现的数量,N是驱动程序的数量,那么如果我们要为每个应用程序重新发明轮子,那么我们最终会得到M*N 个实现,同时使用嵌入式HAL提供的API特征将使实现复杂度接近M+N。当然,还有其他好处,例如由于定义明确且随时可用的 API,减少了反复试验。

嵌入式 hal 的用户

如上所述,HAL 有三个主要用户:

HAL 实施

HAL 实现提供了硬件和 HAL 特征的用户之间的接口。典型的实现包括三个部分:

  • 一种或多种硬件特定类型
  • 创建和初始化此类类型的函数,通常提供各种配置选项(速度、操作模式、使用引脚等)
  • 一个或一个以上trait impl嵌-HAL性状该类型

这样的HAL 实现可以有多种风格:

  • 通过底层硬件访问,例如通过寄存器
  • 通过操作系统,例如通过sysfs在 Linux 下使用
  • 通过适配器,例如用于单元测试的模拟类型
  • 通过硬件适配器的驱动程序,例如 I2C 多路复用器或 GPIO 扩展器

驱动

驱动程序为内部或外部组件实现一组自定义功能,连接到实现嵌入式 hal 特征的外设。此类驱动器的典型示例包括各种传感器(温度、磁力计、加速度计、光)、显示设备(LED 阵列、LCD 显示器)和执行器(电机、发射器)。

驱动程序必须使用类型实例进行初始化,该类型实例实现了特定trait的嵌入式 hal,这是通过 trait bound 确保的,并提供其自己的类型实例和一组允许与驱动设备交互的自定义方法。

应用

该应用程序将各个部分绑定在一起并确保实现所需的功能。在不同系统之间移植时,这是最需要适应工作的部分,因为应用程序需要通过 HAL 实现正确初始化真实硬件,并且不同硬件的初始化不同,有时差异很大。此外,用户的选择通常也很重要,因为组件可以物理连接到不同的终端,硬件总线有时需要外部硬件来匹配配置,或者在使用内部外设(例如多个定时器)时需要进行不同的权衡具有不同的功能可用或外围设备与其他设备冲突)。

Concurrency(并发)

当程序的不同部分可能在不同时间或无序执行时,就会发生并发。在嵌入式上下文中,这包括:

  • 中断处理程序,每当相关的中断发生时运行,
  • 各种形式的多线程,您的微处理器定期在程序的各个部分之间进行交换,
  • 在某些系统中,多核微处理器,其中每个核可以同时独立运行程序的不同部分。

由于很多嵌入式程序需要处理中断,并发通常迟早会出现,同时也是很多微妙和困难的错误发生的地方。幸运的是,Rust 提供了许多抽象和安全保证来帮助我们编写正确的代码。

无并发

嵌入式程序最简单的并发是无并发:你的软件由一个主循环组成,它一直在运行,根本没有中断。有时这非常适合手头的问题!通常,您的循环会读取一些输入、执行一些处理并写入一些输出。

#[entry]
fn main() {
    let peripherals = setup_peripherals();
    loop {
        let inputs = read_inputs(&peripherals);
        let outputs = process(inputs);
        write_outputs(&peripherals, outputs);
    }
}

由于没有并发,因此无需担心程序各部分之间的数据共享或对外围设备的同步访问。如果您可以通过这种简单的方法逃脱,这可能是一个很好的解决方案。

全局可变数据

与非嵌入式 Rust 不同,我们通常不会创建堆分配并将对该数据的引用传递给新创建的线程。相反,我们的中断处理程序可能随时被调用,并且必须知道如何访问我们正在使用的任何共享内存。在最低级别,这意味着我们必须静态分配可变内存,中断处理程序和主代码都可以引用这些内存。

在 Rust 中,这样的static mut变量读或写总是不安全的,因为如果不特别小心,你可能会触发竞争条件,你对变量的访问在中途被一个也访问该变量的中断中断。

有关此行为如何在您的代码中导致细微错误的示例,请考虑一个嵌入式程序,该程序在每个一秒周期(频率计数器)计算某些输入信号的上升沿:

static mut COUNTER: u32 = 0;

#[entry]
fn main() -> ! {
    set_timer_1hz();
    let mut last_state = false;
    loop {
        let state = read_signal_level();
        if state && !last_state {
            // DANGER - Not actually safe! Could cause data races.
            unsafe { COUNTER += 1 };
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    unsafe { COUNTER = 0; }
}

每一秒,定时器中断都会将计数器设置回 0。同时,主循环不断测量信号,并在看到从低到高的变化时递增计数器。我们不得不使用unsafe访问 COUNTER,因为它是static mut,这意味着我们向编译器保证我们不会导致任何未定义的行为。你能发现比赛条件吗?上的增量COUNTER不能保证是原子-事实上,在大多数嵌入式平台,它将被分成了负载,则增量,然后商店。如果中断在加载之后但在存储之前触发,则在中断返回后将忽略重置回 0 - 我们将在该期间计算两倍的转换次数。

临界区

那么,我们可以对数据竞争做些什么呢?一种简单的方法是使用临界区,即禁用中断的上下文。通过将COUNTERin的访问包装 main在临界区中,我们可以确保定时器中断在我们完成递增之前不会触发COUNTER

static mut COUNTER: u32 = 0;

#[entry]
fn main() -> ! {
    set_timer_1hz();
    let mut last_state = false;
    loop {
        let state = read_signal_level();
        if state && !last_state {
            // New critical section ensures synchronised access to COUNTER
            cortex_m::interrupt::free(|_| {
                unsafe { COUNTER += 1 };
            });
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    unsafe { COUNTER = 0; }
}

在此示例中,我们使用cortex_m::interrupt::free,但其他平台将具有类似的机制来执行临界区中的代码。这也与禁用中断、运行一些代码,然后重新启用中断相同。

请注意,我们不需要在定时器中断中放置临界区,原因有两个:

  • 写入 0COUNTER不会受到比赛的影响,因为我们不阅读它
  • main无论如何它永远不会被线程中断

如果COUNTER被多个可能互相抢占的中断处理程序共享,那么每个中断处理程序也 可能需要一个临界区。

这解决了我们眼前的问题,但我们仍然要编写许多需要仔细推理的不安全代码,而且我们可能会不必要地使用临界区。由于每个临界区都会临时暂停中断处理,因此会产生一些额外代码大小以及更高的中断延迟和抖动的相关成本(处理中断可能需要更长的时间,并且处理它们之前的时间将更加可变)。这是否是一个问题取决于您的系统,但总的来说,我们希望避免它。

值得注意的是,虽然临界区保证不会触发任何中断,但它不提供多核系统上的排他性保证!即使没有中断,另一个内核也可以愉快地访问与您的内核相同的内存。如果您使用多核,您将需要更强的同步原语。

原子访问

在某些平台上,可以使用特殊的原子指令,为读取-修改-写入操作提供保证。专门针对 Cortex-M:thumbv6 (Cortex-M0、Cortex-M0+)仅提供原子加载和存储指令,而thumbv7(Cortex-M3 及以上)提供完整的比较和交换(CAS)指令。这些 CAS 指令提供了对所有中断的严厉禁用的替代方案:我们可以尝试递增,它在大多数情况下都会成功,但如果被中断,它将自动重试整个递增操作。即使跨多个内核,这些原子操作也是安全的。

use core::sync::atomic::{AtomicUsize, Ordering};

static COUNTER: AtomicUsize = AtomicUsize::new(0);

#[entry]
fn main() -> ! {
    set_timer_1hz();
    let mut last_state = false;
    loop {
        let state = read_signal_level();
        if state && !last_state {
            // Use `fetch_add` to atomically add 1 to COUNTER
            COUNTER.fetch_add(1, Ordering::Relaxed);
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    // Use `store` to write 0 directly to COUNTER
    COUNTER.store(0, Ordering::Relaxed)
}

这个时间COUNTER是一个安全static变量。由于可以从中断处理程序和主线程安全地修改AtomicUsize 类型,COUNTER而无需禁用中断。如果可能,这是一个更好的解决方案 - 但您的平台可能不支持它。

注意Ordering:这会影响编译器和硬件对指令重新排序的方式,也会对缓存可见性产生影响。假设目标是单核平台Relaxed就足够了,并且在这种特殊情况下是最有效的选择。更严格的排序将导致编译器围绕原子操作发出内存屏障;根据您使用原子的内容,您可能需要也可能不需要这个!原子模型的精确细节很复杂,最好在别处进行描述。

有关原子和排序的更多详细信息,请参阅nomicon

抽象、发送和同步

上述解决方案都不是特别令人满意。它们需要unsafe 必须非常仔细地检查并且不符合人体工程学的积木。我们当然可以在 Rust 中做得更好!

我们可以将我们的计数器抽象成一个安全的接口,它可以安全地在我们代码的任何其他地方使用。对于这个例子,我们将使用临界区计数器,但你可以用原子做一些非常相似的事情。

use core::cell::UnsafeCell;
use cortex_m::interrupt;

// Our counter is just a wrapper around UnsafeCell<u32>, which is the heart
// of interior mutability in Rust. By using interior mutability, we can have
// COUNTER be `static` instead of `static mut`, but still able to mutate
// its counter value.
struct CSCounter(UnsafeCell<u32>);

const CS_COUNTER_INIT: CSCounter = CSCounter(UnsafeCell::new(0));

impl CSCounter {
    pub fn reset(&self, _cs: &interrupt::CriticalSection) {
        // By requiring a CriticalSection be passed in, we know we must
        // be operating inside a CriticalSection, and so can confidently
        // use this unsafe block (required to call UnsafeCell::get).
        unsafe { *self.0.get() = 0 };
    }

    pub fn increment(&self, _cs: &interrupt::CriticalSection) {
        unsafe { *self.0.get() += 1 };
    }
}

// Required to allow static CSCounter. See explanation below.
unsafe impl Sync for CSCounter {}

// COUNTER is no longer `mut` as it uses interior mutability;
// therefore it also no longer requires unsafe blocks to access.
static COUNTER: CSCounter = CS_COUNTER_INIT;

#[entry]
fn main() -> ! {
    set_timer_1hz();
    let mut last_state = false;
    loop {
        let state = read_signal_level();
        if state && !last_state {
            // No unsafe here!
            interrupt::free(|cs| COUNTER.increment(cs));
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    // We do need to enter a critical section here just to obtain a valid
    // cs token, even though we know no other interrupt could pre-empt
    // this one.
    interrupt::free(|cs| COUNTER.reset(cs));

    // We could use unsafe code to generate a fake CriticalSection if we
    // really wanted to, avoiding the overhead:
    // let cs = unsafe { interrupt::CriticalSection::new() };
}

我们已经将unsafe代码移到精心规划的抽象内部,现在我们的应用程序代码不包含任何unsafe块。

这种设计要求应用程序传入一个CriticalSection令牌:这些令牌仅由 安全生成interrupt::free,因此通过要求传入一个令牌,我们确保我们在临界区中操作,而不必自己实际进行锁定。这种保证是由编译器静态提供的:不会有任何与cs. 如果我们有多个计数器,它们都可以被赋予相同的cs,而不需要多个嵌套的临界区。

这也带来了 Rust 并发的一个重要主题: SendSync特征。总结一下 Rust 书,当它可以安全地移动到另一个线程时,类型是 Send,而当它可以在多个线程之间安全共享时,它是 Sync。在嵌入式上下文中,我们认为中断在与应用程序代码不同的线程中执行,因此被中断和主代码访问的变量必须是同步的。

对于 Rust 中的大多数类型,编译器会自动为您派生这两个特征。但是,因为CSCounter包含一个UnsafeCell,它不是同步的,因此我们不能使static CSCounter:static 变量必须是同步的,因为它们可以被多个线程访问。

为了告诉编译器我们已经注意到CSCounter在线程之间共享实际上是安全的,我们显式地实现了 Sync trait。与之前使用的临界区一样,这仅在单核平台上是安全的:对于多核,您需要付出更大的努力来确保安全。

互斥体

我们已经创建了一个特定于我们的计数器问题的有用抽象,但是有许多用于并发的通用抽象。

一个这样的同步原语是互斥,互斥的缩写。这些构造确保对变量的独占访问,例如我们的计数器。线程可以尝试锁定(或获取)互斥锁,并且要么立即成功,要么阻塞等待获取锁,或者返回无法锁定互斥锁的错误。当该线程持有锁时,它被授予访问受保护数据的权限。当线程完成时,它解锁(或 释放)互斥锁,允许另一个线程锁定它。在 Rust 中,我们通常会使用Droptrait 来实现解锁,以确保当互斥量超出范围时它总是被释放。

将互斥锁与中断处理程序一起使用可能会很棘手:中断处理程序阻塞通常是不可接受的,并且阻塞等待主线程释放锁将是特别灾难性的,因为我们会死锁(主线程)线程永远不会释放锁,因为执行停留在中断处理程序中)。死锁不被认为是不安全的:即使在安全的 Rust 中也是可能的。

为了完全避免这种行为,我们可以实现一个需要锁定临界区的互斥锁,就像我们的反例一样。只要临界区必须与锁一样长,我们就可以确保我们可以独占访问被包装的变量,甚至不需要跟踪互斥锁的锁定/解锁状态。

这实际上是在cortex_m板条箱中为我们完成的!我们可以用它写我们的计数器:

use core::cell::Cell;
use cortex_m::interrupt::Mutex;

static COUNTER: Mutex<Cell<u32>> = Mutex::new(Cell::new(0));

#[entry]
fn main() -> ! {
    set_timer_1hz();
    let mut last_state = false;
    loop {
        let state = read_signal_level();
        if state && !last_state {
            interrupt::free(|cs|
                COUNTER.borrow(cs).set(COUNTER.borrow(cs).get() + 1));
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    // We still need to enter a critical section here to satisfy the Mutex.
    interrupt::free(|cs| COUNTER.borrow(cs).set(0));
}

我们现在使用Cell,它和它的兄弟一起RefCell被用来提供安全的内部可变性。我们已经看到UnsafeCell了 Rust 内部可变性的底层:它允许您获得对其值的多个可变引用,但只能使用不安全的代码。ACell 类似于 anUnsafeCell但它提供了一个安全的接口:它只允许获取当前值的副本或替换它,而不是获取引用,并且由于它不是 Sync,因此不能在线程之间共享。这些约束意味着它可以安全使用,但我们不能直接在static变量中使用它, 因为static必须是 Sync。

那么为什么上面的例子有效呢?Mutex<T>为任何T发送的实现同步 - 例如Cell. 它可以安全地执行此操作,因为它仅在关键部分期间才允许访问其内容。因此,我们能够获得一个完全没有不安全代码的安全计数器!

这对于像u32我们的计数器这样的简单类型非常有用,但是对于不是 Copy 的更复杂的类型呢?嵌入式上下文中一个极其常见的示例是外设结构,它通常不是 Copy。为此,我们可以转向RefCell.

共享外设

使用svd2rust和类似抽象生成的设备箱通过强制一次只能存在一个外围结构实例来提供对外围设备的安全访问。这确保了安全性,但使得从主线程和中断处理程序访问外设变得困难。

为了安全地共享外围访问,我们可以使用Mutex我们之前看到的。我们还需要使用RefCell,它使用运行时检查来确保一次只给出一个对外围设备的引用。这比普通的有更多的开销Cell,但由于我们提供引用而不是副本,我们必须确保一次只存在一个。

最后,我们还必须考虑在主代码中初始化后以某种方式将外围设备移动到共享变量中。为此,我们可以使用Option类型,初始化None并稍后设置为外围设备的实例。

use core::cell::RefCell;
use cortex_m::interrupt::{self, Mutex};
use stm32f4::stm32f405;

static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =
    Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    // Obtain the peripheral singletons and configure it.
    // This example is from an svd2rust-generated crate, but
    // most embedded device crates will be similar.
    let dp = stm32f405::Peripherals::take().unwrap();
    let gpioa = &dp.GPIOA;

    // Some sort of configuration function.
    // Assume it sets PA0 to an input and PA1 to an output.
    configure_gpio(gpioa);

    // Store the GPIOA in the mutex, moving it.
    interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));
    // We can no longer use `gpioa` or `dp.GPIOA`, and instead have to
    // access it via the mutex.

    // Be careful to enable the interrupt only after setting MY_GPIO:
    // otherwise the interrupt might fire while it still contains None,
    // and as-written (with `unwrap()`), it would panic.
    set_timer_1hz();
    let mut last_state = false;
    loop {
        // We'll now read state as a digital input, via the mutex
        let state = interrupt::free(|cs| {
            let gpioa = MY_GPIO.borrow(cs).borrow();
            gpioa.as_ref().unwrap().idr.read().idr0().bit_is_set()
        });

        if state && !last_state {
            // Set PA1 high if we've seen a rising edge on PA0.
            interrupt::free(|cs| {
                let gpioa = MY_GPIO.borrow(cs).borrow();
                gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
            });
        }
        last_state = state;
    }
}

#[interrupt]
fn timer() {
    // This time in the interrupt we'll just clear PA0.
    interrupt::free(|cs| {
        // We can use `unwrap()` because we know the interrupt wasn't enabled
        // until after MY_GPIO was set; otherwise we should handle the potential
        // for a None value.
        let gpioa = MY_GPIO.borrow(cs).borrow();
        gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().clear_bit());
    });
}

需要考虑的内容很多,所以让我们分解重要的几行。

static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =
    Mutex::new(RefCell::new(None));

我们的共享变量现在是 aMutex周围的 a RefCell,其中包含一个 Option。在Mutex确保我们只有一个临界区中访问,并因此使变量同步,即使一个普通的RefCell不会同步。该RefCell给我们参考,我们将需要使用我们内部的可变性GPIOA。该Option让我们初始化这个变量为空的东西,后来才真正移动的变量,我们不能静态地访问外设的独生子,在运行时,所以这是必需的。

interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));

在临界区中,我们可以调用borrow()互斥锁,它为我们提供了对RefCell. 然后我们调用replace()将我们的新值移动到RefCell.

interrupt::free(|cs| {
    let gpioa = MY_GPIO.borrow(cs).borrow();
    gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());
});

最后,我们MY_GPIO以安全和并发的方式使用。临界区像往常一样阻止中断触发,并让我们借用互斥锁。在 RefCell随后给了我们一个&Option<GPIOA>,并跟踪它保持多久借来的-一旦进入参考范围的那样,RefCell将被更新,以表明它不再是借来的。

由于我们无法将GPIOA移出&Option,我们需要将其转换为&Option<&GPIOA>with as_ref(),我们最终unwrap()可以获取&GPIOA它让我们修改外围设备。

如果我们需要对共享资源的可变引用borrow_mutderef_mut 则应使用and代替。以下代码显示了使用 TIM2 定时器的示例。

use core::cell::RefCell;
use core::ops::DerefMut;
use cortex_m::interrupt::{self, Mutex};
use cortex_m::asm::wfi;
use stm32f4::stm32f405;

static G_TIM: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> =
    Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
    let mut cp = cm::Peripherals::take().unwrap();
    let dp = stm32f405::Peripherals::take().unwrap();

    // Some sort of timer configuration function.
    // Assume it configures the TIM2 timer, its NVIC interrupt,
    // and finally starts the timer.
    let tim = configure_timer_interrupt(&mut cp, dp);

    interrupt::free(|cs| {
        G_TIM.borrow(cs).replace(Some(tim));
    });

    loop {
        wfi();
    }
}

#[interrupt]
fn timer() {
    interrupt::free(|cs| {
        if let Some(ref mut tim)) =  G_TIM.borrow(cs).borrow_mut().deref_mut() {
            tim.start(1.hz());
        }
    });
}

哇!这是安全的,但也有点笨拙。我们还有什么可以做的吗?

信息中心

一种替代方案是RTIC 框架,它是实时中断驱动并发的缩写。它强制执行静态优先级并跟踪对static mut变量(“资源”)的访问,以静态地确保始终安全地访问共享资源,而无需总是进入临界区和使用引用计数(如 中所示RefCell)的开销。这有许多优点,例如保证没有死锁并提供极低的时间和内存开销。

该框架还包括其他功能,如消息传递,这减少了对显式共享状态的需求,以及安排任务在给定时间运行的能力,可用于实现周期性任务。查看文档以获取更多信息!

实时操作系统

嵌入式并发的另一个常见模型是实时操作系统 (RTOS)。虽然目前在 Rust 中的探索较少,但它们广泛用于传统的嵌入式开发。开源示例包括FreeRTOSChibiOS。这些 RTOS 支持运行多个应用程序线程,CPU 在这些线程之间进行交换,当线程让出控制权(称为协作多任务处理)或基于常规计时器或中断(抢占式多任务处理)时。RTOS 通常提供互斥锁和其他同步原语,并且经常与硬件功能(例如 DMA 引擎)进行互操作。

在撰写本文时,可以指出的 Rust RTOS 示例并不多,但这是一个有趣的领域,因此请关注此空间!

多核

在嵌入式处理器中拥有两个或更多内核变得越来越普遍,这为并发性增加了一层额外的复杂性。所有使用临界区(包括cortex_m::interrupt::Mutex)的示例都假设唯一的其他执行线程是中断线程,但在多核系统上不再如此。相反,我们需要为多核设计的同步原语(也称为 SMP,用于对称多处理)。

这些通常使用我们之前看到的原子指令,因为处理系统将确保在所有内核上保持原子性。

详细介绍这些主题目前超出了本书的范围,但一般模式与单核情况相同。

Collections

最终,您将希望在程序中使用动态数据结构(AKA 集合)。std提供了一组公共集合:VecStringHashMap等。所有实现的集合都std使用全局动态内存分配器(也称为堆)。

按照core定义,这些实现在没有内存分配的情况下不可用,但可以alloc在编译器附带的crate 中找到。

如果您需要集合,堆分配实现不是您唯一的选择。您还可以使用固定容量集合;在heaplesscrate 中可以找到一个这样的实现。

在本节中,我们将探索和比较这两种实现。

使用 alloc

alloc箱出厂时的标准锈病分布。要导入 crate,您可以直接将use其导入,而无需Cargo.toml文件中将其声明为依赖项 。

#![feature(alloc)]

extern crate alloc;

use alloc::vec::Vec;

为了能够使用任何集合,您首先需要使用该global_allocator 属性来声明您的程序将使用的全局分配器。您选择的分配器需要实现该GlobalAlloc特征。

为完整起见并保持本节尽可能独立,我们将实现一个简单的凹凸指针分配器并将其用作全局分配器。但是,我们强烈建议您在程序中使用 crates.io 中经过实战测试的分配器,而不是这个分配器。

// Bump pointer allocator implementation

extern crate cortex_m;

use core::alloc::GlobalAlloc;
use core::ptr;

use cortex_m::interrupt;

// Bump pointer allocator for *single* core systems
struct BumpPointerAlloc {
    head: UnsafeCell<usize>,
    end: usize,
}

unsafe impl Sync for BumpPointerAlloc {}

unsafe impl GlobalAlloc for BumpPointerAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        // `interrupt::free` is a critical section that makes our allocator safe
        // to use from within interrupts
        interrupt::free(|_| {
            let head = self.head.get();
            let size = layout.size();
            let align = layout.align();
            let align_mask = !(align - 1);

            // move start up to the next alignment boundary
            let start = (*head + align - 1) & align_mask;

            if start + size > self.end {
                // a null pointer signal an Out Of Memory condition
                ptr::null_mut()
            } else {
                *head = start + size;
                start as *mut u8
            }
        })
    }

    unsafe fn dealloc(&self, _: *mut u8, _: Layout) {
        // this allocator never deallocates memory
    }
}

// Declaration of the global memory allocator
// NOTE the user must ensure that the memory region `[0x2000_0100, 0x2000_0200]`
// is not used by other parts of the program
#[global_allocator]
static HEAP: BumpPointerAlloc = BumpPointerAlloc {
    head: UnsafeCell::new(0x2000_0100),
    end: 0x2000_0200,
};

除了选择全局分配器之外,用户还必须定义如何使用不稳定 alloc_error_handler属性处理内存不足 (OOM) 错误。

#![feature(alloc_error_handler)]

use cortex_m::asm;

#[alloc_error_handler]
fn on_oom(_layout: Layout) -> ! {
    asm::bkpt();

    loop {}
}

一切就绪后,用户最终可以使用alloc.

#[entry]
fn main() -> ! {
    let mut xs = Vec::new();

    xs.push(42);
    assert!(xs.pop(), Some(42));

    loop {
        // ..
    }
}

如果您使用过stdcrate 中的集合,那么这些将很熟悉,因为它们是完全相同的实现。

使用 heapless

heapless不需要设置,因为它的集合不依赖于全局内存分配器。只是use它的集合并继续实例化它们:

extern crate heapless; // v0.4.x

use heapless::Vec;
use heapless::consts::*;

#[entry]
fn main() -> ! {
    let mut xs: Vec<_, U8> = Vec::new();

    xs.push(42).unwrap();
    assert_eq!(xs.pop(), Some(42));
}

您会注意到这些集合与alloc.

首先,您必须预先声明集合的容量。heapless 集合永远不会重新分配并具有固定容量;此容量是集合类型签名的一部分。在这种情况下,我们已经声明了xs 具有 8 个元素的容量,即向量最多可以容纳 8 个元素。这由类型签名中的U8(请参阅typenum)指示。

其次,该push方法和许多其他方法都返回一个Result. 由于 heapless集合具有固定容量,因此所有将元素插入到集合中的操作都可能失败。API 通过返回一个Result指示操作是否成功来反映此问题。相比之下,alloc集合将在堆上重新分配自己以增加其容量。

从 v0.4.x 版本开始,所有heapless集合都内联存储其所有元素。这意味着像这样的操作let x = heapless::Vec::new();将在堆栈上分配集合,但也可以在static变量上分配集合,甚至在堆 ( Box<Vec<_, _>>) 上。

权衡

在堆分配、可重定位集合和固定容量集合之间进行选择时,请记住这些。

内存不足和错误处理

对于堆分配,内存不足总是可能的,并且可能发生在集合可能需要增长的任何地方:例如,所有 alloc::Vec.push调用都可能产生 OOM 条件。因此,某些操作可能会隐式失败。一些alloc集合公开了 一些try_reserve方法,可以让您在增加集合时检查潜在的 OOM 条件,但您需要主动使用它们。

如果您只使用heapless集合并且不将内存分配器用于其他任何事情,那么 OOM 条件是不可能的。相反,您必须根据具体情况处理容量不足的集合。这就是你必须处理所有Result通过类似方法返回小号 Vec.push

OOM 故障可能比unwrap对所有Result返回的 s说-ing更难调试,heapless::Vec.push因为观察到的故障位置可能 与问题原因的位置匹配。例如,vec.reserve(1)如果分配器几乎耗尽,因为其他一些集合正在泄漏内存(在安全 Rust 中可能发生内存泄漏),甚至 可以触发 OOM。

内存使用情况

对堆分配集合的内存使用情况进行推理是很困难的,因为长寿命集合的容量可能会在运行时发生变化。某些操作可能会隐式地重新分配集合以增加其内存使用量,而某些集合公开的方法shrink_to_fit可能会减少集合使用的内存——最终,由分配器决定是否实际缩小内存分配。此外,分配器可能必须处理内存碎片,这会增加 明显的内存使用量。

另一方面,如果您专门使用固定容量集合,将它们中的大部分存储在static变量中并为调用堆栈设置最大大小,那么链接器将检测您是否尝试使用比物理可用内存更多的内存。

此外,堆栈上分配的固定容量集合将通过-Z emit-stack-sizes标志报告,这意味着分析堆栈使用情况的工具(如stack-sizes)将在其分析中包括它们。

但是,固定容量集合无法收缩,这会导致负载因子(集合大小与其容量之间的比率)低于可重定位集合所能达到的负载因子。

最坏情况执行时间 (WCET)

如果您正在构建对时间敏感的应用程序或硬实时应用程序,那么您可能非常关心程序不同部分的最坏情况执行时间。

alloc集合可以重新分配,这样可以增加集合还将包括它需要重新分配的集合,它本身依赖于时间的操作的WCET运行时收集的能力。这使得很难确定例如alloc::Vec.push操作的 WCET,因为它取决于正在使用的分配器及其运行时容量。

另一方面,固定容量集合永远不会重新分配,因此所有操作都有可预测的执行时间。例如,heapless::Vec.push在恒定时间内执行。

便于使用

alloc需要设置全局分配器,heapless而不需要。但是,heapless需要您选择实例化的每个集合的容量。

alloc几乎每个 Rust 开发人员都会熟悉该API。该 heaplessAPI尝试密切模仿的allocAPI,但它永远不会是完全一样的,因为它明确的错误处理-一些开发人员可能会觉得明确错误处理过度或太麻烦了。

Design Patterns

HAL Design Patterns:

这是一组常用和推荐的模式,用于在 Rust 中为微控制器编写硬件抽象层 (HAL)。在为微控制器编写 HAL 时,除了现有的Rust API 指南之外,还打算使用这些模式。

Checklist

  • 命名

    (crate 与 Rust 命名约定一致)

  • 互操作性

    (板条箱与其他库功能很好地交互)

  • 可预测性

    (板条箱启用清晰的代码,使其看起来如何)

    • 使用构造函数代替扩展特征(C-CTOR
  • GPIO 接口

    (GPIO 接口遵循通用模式)

    • 默认情况下,引脚类型为零大小 ( C-ZST-PIN )
    • 引脚类型提供擦除引脚和端口的方法(C-ERASED-PIN
    • 引脚状态应编码为类型参数(C-PIN-STATE

Naming

箱子被适当地命名(C-CRATE-NAME)

HAL crate 应以其旨在支持的芯片或芯片系列命名。它们的名称应以 结尾,-hal以将它们与 register access crate 区分开来。名称不应包含下划线(改用破折号)。

Interoperability

包装类型提供析构方法(C-FREE)

CopyHAL 提供的任何非包装器类型都应该提供一个free方法来使用包装器并返回创建它的原始外围设备(可能还有其他对象)。

如有必要,该方法应关闭并重置外设。new 使用 返回的原始外设调用free不应由于外设的意外状态而失败。

如果 HAL 类型需要Copy构造其他非对象(例如 I/O 引脚),则任何此类对象也应被释放并返回freefree在这种情况下应该返回一个元组。

例如:

pub struct Timer(TIMER0);

impl Timer {
    pub fn new(periph: TIMER0) -> Self {
        Self(periph)
    }

    pub fn free(self) -> TIMER0 {
        self.0
    }
}

HAL 重新导出其注册访问箱 (C-REEXPORT-PAC)

HAL 可以写在svd2rust生成的 PAC 之上,或者写在提供原始寄存器访问的其他 crate 之上。HAL 应始终在其 crate 根中重新导出它们所基于的 register access crate。

PAC 应该以 name 重新导出pac,而不管 crate 的实际名称是什么,因为 HAL 的名称应该已经清楚地表明正在访问什么 PAC。

类型实现embedded-hal特征(C-HAL-TRAITS)

HAL 提供的类型应实现embedded-halcrate提供的所有适用特征 。

可以为同一类型实现多个特征。

Predictability

使用构造函数代替扩展特征(C-CTOR)

HAL 向其添加功能的所有外围设备都应包含在新类型中,即使该功能不需要其他字段。

应避免为原始外设实现的扩展特性。

方法#[inline\]在适当的地方装饰(C-INLINE)

默认情况下,Rust 编译器不会跨 crate 边界执行完全内联。由于嵌入式应用程序对意外的代码大小增加很敏感,#[inline]应使用如下指导编译器:

  • 所有“小”功能都应该被标记#[inline]。什么是“小”是主观的,但通常所有预期编译为一位数指令序列的函数都被认为是小。
  • 很可能采用常量值作为参数的函数应标记为#[inline]. 这使得编译器能够在编译时计算甚至复杂的初始化逻辑,前提是函数输入是已知的。

GPIO

默认情况下,引脚类型为零大小 (C-ZST-PIN)

HAL 公开的 GPIO 接口应为每个接口或端口上的每个引脚提供专用的零大小类型,从而在所有引脚分配都是静态已知的情况下实现零成本的 GPIO 抽象。

每个 GPIO 接口或端口都应该实现一个split方法,返回一个带有每个引脚的结构。

例子:

pub struct PA0;
pub struct PA1;
// ...

pub struct PortA;

impl PortA {
    pub fn split(self) -> PortAPins {
        PortAPins {
            pa0: PA0,
            pa1: PA1,
            // ...
        }
    }
}

pub struct PortAPins {
    pub pa0: PA0,
    pub pa1: PA1,
    // ...
}

引脚类型提供擦除引脚和端口的方法 (C-ERASED-PIN)

Pin 应该提供类型擦除方法,将它们的属性从编译时移动到运行时,并在应用程序中提供更大的灵活性。

例子:

/// Port A, pin 0.
pub struct PA0;

impl PA0 {
    pub fn erase_pin(self) -> PA {
        PA { pin: 0 }
    }
}

/// A pin on port A.
pub struct PA {
    /// The pin number.
    pin: u8,
}

impl PA {
    pub fn erase_port(self) -> Pin {
        Pin {
            port: Port::A,
            pin: self.pin,
        }
    }
}

pub struct Pin {
    port: Port,
    pin: u8,
    // (these fields can be packed to reduce the memory footprint)
}

enum Port {
    A,
    B,
    C,
    D,
}

引脚状态应编码为类型参数 (C-PIN-STATE)

根据芯片或系列的不同,引脚可以配置为具有不同特性的输入或输出。此状态应在类型系统中进行编码,以防止在错误状态下使用引脚。

附加的、特定于芯片的状态(例如驱动强度)也可以以这种方式使用附加类型参数进行编码。

应提供更改引脚状态的方法into_inputinto_output方法。

此外,with_{input,output}_state应该提供在不同状态下临时重新配置引脚而不移动它的方法。

应为每种引脚类型提供以下方法(即擦除和非擦除引脚类型应提供相同的 API):

  • pub fn into_input<N: InputState>(self, input: N) -> Pin<N>

  • pub fn into_output<N: OutputState>(self, output: N) -> Pin<N>

  • pub fn with_input_state(
        &mut self,
        input: N,
        f: impl FnOnce(&mut PA1) -> R,
    ) -> R
  • pub fn with_output_state(
        &mut self,
        output: N,
        f: impl FnOnce(&mut PA1) -> R,
    ) -> R

引脚状态应受密封特征的限制。HAL 的用户应该不需要添加他们自己的状态。这些特征可以提供实现引脚状态 API 所需的特定于 HAL 的方法。

例子:

mod sealed {
    pub trait Sealed {}
}

pub trait PinState: sealed::Sealed {}
pub trait OutputState: sealed::Sealed {}
pub trait InputState: sealed::Sealed {
    // ...
}

pub struct Output<S: OutputState> {
    _p: PhantomData<S>,
}

impl<S: OutputState> PinState for Output<S> {}
impl<S: OutputState> sealed::Sealed for Output<S> {}

pub struct PushPull;
pub struct OpenDrain;

impl OutputState for PushPull {}
impl OutputState for OpenDrain {}
impl sealed::Sealed for PushPull {}
impl sealed::Sealed for OpenDrain {}

pub struct Input<S: InputState> {
    _p: PhantomData<S>,
}

impl<S: InputState> PinState for Input<S> {}
impl<S: InputState> sealed::Sealed for Input<S> {}

pub struct Floating;
pub struct PullUp;
pub struct PullDown;

impl InputState for Floating {}
impl InputState for PullUp {}
impl InputState for PullDown {}
impl sealed::Sealed for Floating {}
impl sealed::Sealed for PullUp {}
impl sealed::Sealed for PullDown {}

pub struct PA1<S: PinState> {
    _p: PhantomData<S>,
}

impl<S: PinState> PA1<S> {
    pub fn into_input<N: InputState>(self, input: N) -> PA1<Input<N>> {
        todo!()
    }

    pub fn into_output<N: OutputState>(self, output: N) -> PA1<Output<N>> {
        todo!()
    }

    pub fn with_input_state<N: InputState, R>(
        &mut self,
        input: N,
        f: impl FnOnce(&mut PA1<N>) -> R,
    ) -> R {
        todo!()
    }

    pub fn with_output_state<N: OutputState, R>(
        &mut self,
        output: N,
        f: impl FnOnce(&mut PA1<N>) -> R,
    ) -> R {
        todo!()
    }
}

// Same for `PA` and `Pin`, and other pin types.

Tips for embedded C developers

本章收集了各种技巧,可能对希望开始编写 Rust 的有经验的嵌入式 C 开发人员有用。它将特别强调您在 C 中可能已经习惯的东西在 Rust 中的不同之处。

预处理器

在嵌入式 C 中,将预处理器用于各种目的是很常见的,例如:

  • 代码块的编译时选择 #ifdef
  • 编译时数组大小和计算
  • 用于简化常见模式的宏(以避免函数调用开销)

在 Rust 中没有预处理器,因此许多这些用例的处理方式不同。在本节的其余部分,我们将介绍使用预处理器的各种替代方法。

Compile-Time Code Selection

#ifdef ... #endif在 Rust 中最接近的匹配是Cargo features。这些比 C 预处理器更正式一些:所有可能的功能都明确列出每个 crate,并且只能打开或关闭。当您将一个 crate 列为依赖项时,功能会被打开,并且是附加的:如果您的依赖树中的任何一个 crate 为另一个 crate 启用了一个特性,那么该特性将为该 crate 的所有用户启用。

例如,您可能有一个提供信号处理原语库的 crate。每个人都可能需要一些额外的时间来编译或声明一些您希望避免的大型常量表。你可以为你的每个组件声明一个 Cargo 特性Cargo.toml

[features]
FIR = []
IIR = []

然后,在您的代码中,使用#[cfg(feature="FIR")]来控制所包含的内容。

/// In your top-level lib.rs

#[cfg(feature="FIR")]
pub mod fir;

#[cfg(feature="IIR")]
pub mod iir;

类似地,仅当某个功能启用,或者功能的任何组合已启用或未启用时,您才可以包含代码块。

此外,Rust 提供了许多您可以使用的自动设置条件,例如target_arch根据架构选择不同的代码。有关条件编译支持的完整详细信息,请参阅Rust 参考的 条件编译章节。

条件编译仅适用于下一个语句或块。如果一个块不能在当前范围内使用,那么该cfg属性将需要多次使用。值得注意的是,在大多数情况下,最好简单地包含所有代码并允许编译器在优化时删除死代码:这对您和您的用户来说更简单,并且通常编译器会很好地删除未使用的代码.

编译时大小和计算

Rust 支持const fn, 保证在编译时可评估的函数,因此可以在需要常量的地方使用,例如数组的大小。这可以与上述功能一起使用,例如:

const fn array_size() -> usize {
    #[cfg(feature="use_more_ram")]
    { 1024 }
    #[cfg(not(feature="use_more_ram"))]
    { 128 }
}

static BUF: [u32; array_size()] = [0u32; array_size()];

从 1.31 开始,这些是稳定 Rust 的新内容,因此文档仍然很少。const fn在撰写本文时,可用的功能也非常有限;在未来的 Rust 版本中,预计会扩展const fn.

Rust 提供了一个极其强大的宏系统。虽然 C 预处理器几乎直接对源代码的文本进行操作,但 Rust 宏系统在更高的级别上运行。Rust 宏有两种变体:示例*过程宏*。前者更简单也最常见;它们看起来像函数调用,可以扩展为完整的表达式、语句、项或模式。过程宏更复杂,但允许对 Rust 语言进行极其强大的添加:它们可以将任意 Rust 语法转换为新的 Rust 语法。

通常,在您可能使用过 C 预处理器宏的地方,您可能想查看一个宏示例是否可以完成这项工作。它们可以在您的 crate 中定义,并且可以由您自己的 crate 轻松使用或导出给其他用户。请注意,由于它们必须扩展为完整的表达式、语句、项或模式,因此 C 预处理器宏的某些用例将不起作用,例如扩展为变量名称的一部分或列表中不完整的项集的宏.

与 Cargo 功能一样,如果您甚至需要宏,则值得考虑。在许多情况下,常规函数更容易理解,并且会被内联到与宏相同的代码中。在#[inline]#[inline(always)] 属性 让您对这一过程的进一步控制,但应谨慎在这里拍摄,以及-编译器会从同一包装箱自动内联函数在适当情况下,所以迫使它这样做不恰当实际上可能会导致性能下降。

解释整个 Rust 宏系统超出了本提示页面的范围,因此鼓励您查阅 Rust 文档以获取完整详细信息。

构建系统

大多数 Rust crate 是使用 Cargo 构建的(尽管不是必需的)。这解决了传统构建系统的许多难题。但是,您可能希望自定义构建过程。Cargo为此提供了build.rs 脚本。它们是 Rust 脚本,可以根据需要与 Cargo 构建系统交互。

构建脚本的常见用例包括:

  • 提供构建时间信息,例如将构建日期或 Git 提交哈希静态嵌入到您的可执行文件中
  • 根据所选功能或其他逻辑在构建时生成链接器脚本
  • 更改 Cargo 构建配置
  • 添加额外的静态库来链接

目前不支持构建后脚本,您可能在传统上将其用于从构建对象自动生成二进制文件或打印构建信息等任务。

交叉编译

将 Cargo 用于您的构建系统还可以简化交叉编译。在大多数情况下,只需告诉 Cargo--target thumbv6m-none-eabi并在target/thumbv6m-none-eabi/debug/myapp.

对于 Rust 本身不支持的平台,您需要libcore 自己为该目标构建。在这样的平台上,Xargo可以用作 Cargo 的替身,它会自动libcore为您构建。

迭代器与数组访问

在 C 中,您可能习惯于通过索引直接访问数组:

int16_t arr[16];
int i;
for(i=0; i<sizeof(arr)/sizeof(arr[0]); i++) {
    process(arr[i]);
}

在 Rust 中,这是一种反模式:索引访问可能会更慢(因为它需要进行边界检查)并且可能会阻止各种编译器优化。这是一个重要的区别,值得重复:Rust 将检查手动数组索引的越界访问以保证内存安全,而 C 将很乐意在数组外进行索引。

相反,使用迭代器:

let arr = [0u16; 16];
for element in arr.iter() {
    process(*element);
}

迭代器提供了一系列强大的功能,您必须在 C 中手动实现,例如链接、压缩、枚举、查找最小值或最大值、求和等等。迭代器方法也可以链接,提供非常易读的数据处理代码。

引用 vs 指针

在 Rust 中,指针(称为原始指针)存在但仅在特定情况下使用,因为总是考虑取消引用它们unsafe——Rust 不能提供关于指针后面可能是什么的通常保证。

在大多数情况下,我们改用引用,由指定的&符号,或 可变的引用,以表示&mut。引用的行为类似于指针,因为它们可以被取消引用以访问底层值,但它们是 Rust 所有权系统的关键部分:Rust 将严格强制您可能只有一个可变引用多个非可变引用到同一个在任何给定时间的价值。

在实践中,这意味着您必须更加小心是否需要对数据进行可变访问:在 C 中,默认值是可变的,您必须明确说明const,而在 Rust 中则相反。

您可能仍然使用原始指针的一种情况是直接与硬件交互(例如,将指向缓冲区的指针写入 DMA 外设寄存器),并且它们也在幕后用于所有外设访问包,以允许您读取和写内存映射寄存器。

Volatile Access

在 C 中,个别变量可能被标记为volatile,向编译器表明变量中的值可能会在访问之间发生变化。易失性变量通常用于内存映射寄存器的嵌入式上下文中。

在 Rust 中,我们没有将变量标记为volatile,而是使用特定的方法来执行 volatile 访问:core::ptr::read_volatilecore::ptr::write_volatile。这些方法采用 a*const T或 a *mut T原始指针,如上所述)并执行易失性读取或写入。

例如,在 C 中你可以这样写:

volatile bool signalled = false;

void ISR() {
    // Signal that the interrupt has occurred
    signalled = true;
}

void driver() {
    while(true) {
        // Sleep until signalled
        while(!signalled) { WFI(); }
        // Reset signalled indicator
        signalled = false;
        // Perform some task that was waiting for the interrupt
        run_task();
    }
}

Rust 中的等价物将在每次访问时使用 volatile 方法:

static mut SIGNALLED: bool = false;

#[interrupt]
fn ISR() {
    // Signal that the interrupt has occurred
    // (In real code, you should consider a higher level primitive,
    //  such as an atomic type).
    unsafe { core::ptr::write_volatile(&mut SIGNALLED, true) };
}

fn driver() {
    loop {
        // Sleep until signalled
        while unsafe { !core::ptr::read_volatile(&SIGNALLED) } {}
        // Reset signalled indicator
        unsafe { core::ptr::write_volatile(&mut SIGNALLED, false) };
        // Perform some task that was waiting for the interrupt
        run_task();
    }
}

代码示例中有几点值得注意:

  • 我们可以传递&mut SIGNALLED到函数 requires *mut T,因为 &mut T自动转换为 a *mut T(对于*const T
  • 我们需要/方法的unsafe块,因为它们是函数。确保安全使用是程序员的责任:有关更多详细信息,请参阅方法文档。read_volatile``write_volatile``unsafe

在您的代码中直接使用这些函数是很少见的,因为它们通常会由更高级别的库为您处理。对于内存映射外设,外设访问 crate 将自动实现易失性访问,而对于并发原语,有更好的抽象可用。

压缩和对齐类型

在嵌入式 C 中,通常会告诉编译器一个变量必须有一定的对齐方式,或者一个结构必须打包而不是对齐,通常是为了满足特定的硬件或协议要求。

在 Rust 中,这由repr结构或联合上的属性控制。默认表示不提供布局保证,因此不应用于与硬件或 C 互操作的代码。编译器可能会重新排序结构成员或插入填充,并且行为可能会随着 Rust 的未来版本而改变。

struct Foo {
    x: u16,
    y: u8,
    z: u16,
}

fn main() {
    let v = Foo { x: 0, y: 0, z: 0 };
    println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z);
}

// 0x7ffecb3511d0 0x7ffecb3511d4 0x7ffecb3511d2
// Note ordering has been changed to x, z, y to improve packing.

为确保布局可与 C 互操作,请使用repr(C)

#[repr(C)]
struct Foo {
    x: u16,
    y: u8,
    z: u16,
}

fn main() {
    let v = Foo { x: 0, y: 0, z: 0 };
    println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z);
}

// 0x7fffd0d84c60 0x7fffd0d84c62 0x7fffd0d84c64
// Ordering is preserved and the layout will not change over time.
// `z` is two-byte aligned so a byte of padding exists between `y` and `z`.

要确保打包表示,请使用repr(packed)

#[repr(packed)]
struct Foo {
    x: u16,
    y: u8,
    z: u16,
}

fn main() {
    let v = Foo { x: 0, y: 0, z: 0 };
    // Unsafe is required to borrow a field of a packed struct.
    unsafe { println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z) };
}

// 0x7ffd33598490 0x7ffd33598492 0x7ffd33598493
// No padding has been inserted between `y` and `z`, so now `z` is unaligned.

请注意, usingrepr(packed)还将类型的对齐方式设置为1

最后,要指定特定的对齐方式,请使用repr(align(n)),其中n是要对齐的字节数(并且必须是 2 的幂):

#[repr(C)]
#[repr(align(4096))]
struct Foo {
    x: u16,
    y: u8,
    z: u16,
}

fn main() {
    let v = Foo { x: 0, y: 0, z: 0 };
    let u = Foo { x: 0, y: 0, z: 0 };
    println!("{:p} {:p} {:p}", &v.x, &v.y, &v.z);
    println!("{:p} {:p} {:p}", &u.x, &u.y, &u.z);
}

// 0x7ffec909a000 0x7ffec909a002 0x7ffec909a004
// 0x7ffec909b000 0x7ffec909b002 0x7ffec909b004
// The two instances `u` and `v` have been placed on 4096-byte alignments,
// evidenced by the `000` at the end of their addresses.

请注意,我们可以结合repr(C)使用repr(align(n))以获得对齐且与 C 兼容的布局。这是不允许的结合repr(align(n))repr(packed),因为repr(packed)套对齐1。一个repr(packed)类型也不允许包含一个repr(align(n))类型。

其他资源

Interoperability(互操作性)

Rust 和 C 代码之间的互操作性始终依赖于两种语言之间的数据转换。为此目的,在被stdlib调用 std::ffi和 中 有两个专用模块std::os::raw

std::os::raw 处理可以由编译器隐式转换的低级原始类型,因为 Rust 和 C 之间的内存布局足够相似或相同。

std::ffi提供了一些实用程序用于将更加复杂的类型,如弦乐器,既映射&strString 对C-类型是更容易和更安全的处理。

这些模块都没有在 中可用core,但是您可以在crate 中找到#![no_std] 兼容版本,std::ffi::{CStr,CString}以及cstr_corecrate 中的大多数std::os::raw类型cty

锈型 中间的 C型
细绳 字符串 *字符
&str CStr *常量字符
() c_void 空白
u32 或 u64 c_uint 无符号整数
等等

如上所述,基本类型可以由编译器隐式转换。

unsafe fn foo(num: u32) {
    let c_num: c_uint = num;
    let r_num: u32 = c_num;
}

与其他构建系统的互操作性

在您的嵌入式项目中包含 Rust 的一个常见要求是将 Cargo 与您现有的构建系统相结合,例如 make 或 cmake。

我们正在问题 #61 中的问题跟踪器上为此收集示例和用例 。

与 RTOS 的互操作性

将 Rust 与诸如 FreeRTOS 或 ChibiOS 之类的 RTOS 集成仍在进行中;尤其是从 Rust 调用 RTOS 函数可能会很棘手。

我们正在问题 #62 中的问题跟踪器上为此收集示例和用例 。

A little C with your Rust

在 Rust 项目中使用 C 或 C++ 包括两个主要部分:

  • 包装暴露的 C API 以与 Rust 一起使用
  • 构建您的 C 或 C++ 代码以与 Rust 代码集成

由于 C++ 没有稳定的 ABI 供 Rust 编译器作为目标,因此建议C在将 Rust 与 C 或 C++ 结合使用时使用ABI。

定义接口

在从 Rust 使用 C 或 C++ 代码之前,有必要定义(在 Rust 中)链接代码中存在哪些数据类型和函数签名。在 C 或 C++ 中,您将包含定义此数据的头 (.h.hpp) 文件。在 Rust 中,需要手动将这些定义转换为 Rust,或者使用工具生成这些定义。

首先,我们将介绍将这些定义从 C/C++ 手动转换为 Rust。

包装 C 函数和数据类型

通常,用 C 或 C++ 编写的库将提供一个定义公共接口中使用的所有类型和函数的头文件。示例文件可能如下所示:

/* File: cool.h */
typedef struct CoolStruct {
    int x;
    int y;
} CoolStruct;

void cool_function(int i, char c, CoolStruct* cs);

当转换为 Rust 时,此界面将如下所示:

/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
    pub x: cty::c_int,
    pub y: cty::c_int,
}

pub extern "C" fn cool_function(
    i: cty::c_int,
    c: cty::c_char,
    cs: *mut CoolStruct
);

让我们一次一个地看一下这个定义,以解释每个部分。

#[repr(C)]
pub struct CoolStruct { ... }

默认情况下,Rust 不保证包含在struct. 为了保证与 C 代码的兼容性,我们包含了#[repr(C)]属性,它指示 Rust 编译器始终使用 C 在结构中组织数据的相同规则。

pub x: cty::c_int,
pub y: cty::c_int,

由于 C 或 C++ 定义intor 的方式的灵活性,char建议使用 中定义的原始数据类型cty,它将类型从 C 映射到 Rust 中的类型。

pub extern "C" fn cool_function( ... );

此语句定义了使用 C ABI 的函数的签名,称为cool_function. 通过定义签名而不定义函数体,这个函数的定义需要在别处提供,或者从静态库链接到最终库或二进制文件。

    i: cty::c_int,
    c: cty::c_char,
    cs: *mut CoolStruct

与我们上面的数据类型类似,我们使用 C 兼容定义来定义函数参数的数据类型。为了清楚起见,我们还保留了相同的参数名称。

我们这里有一种新类型,*mut CoolStruct. 由于 C 没有 Rust 引用的概念,它看起来像这样:&mut CoolStruct,我们有一个原始指针。由于取消引用此指针是unsafe,并且该指针实际上可能是一个null指针,因此在与 C 或 C++ 代码交互时必须注意确保 Rust 的典型保证。

自动生成界面

有一个名为bindgen的工具可以自动执行这些转换,而不是手动生成这些接口,这可能很乏味且容易出错。有关bindgen的使用说明,请参阅bindgen 用户手册,但典型的过程包括以下内容:

  1. 收集所有 C 或 C++ 头文件,定义你想与 Rust 一起使用的接口或数据类型。
  2. 编写一个bindings.h文件,它#include "..."是您在第一步中收集的每个文件。
  3. 提供此bindings.h文件以及用于将代码编译为bindgen. 提示:使用Builder.ctypes_prefix("cty")/ --ctypes-prefix=ctyBuilder.use_core()/--use-core使生成的代码#![no_std]兼容。
  4. bindgen将生成生成的 Rust 代码到终端窗口的输出。此文件可能会通过管道传输到项目中的文件,例如bindings.rs. 你可以在你的 Rust 项目中使用这个文件来与编译和链接为外部库的 C/C++ 代码交互。提示:cty如果生成的绑定中的类型带有前缀,请不要忘记使用crate cty

构建您的 C/C++ 代码

由于 Rust 编译器不直接知道如何编译 C 或 C++ 代码(或来自任何其他语言的代码,提供 C 接口),因此有必要提前编译您的非 Rust 代码。

对于嵌入式项目,这通常意味着将 C/C++ 代码编译为静态存档(例如cool-library.a),然后可以在最后的链接步骤中将其与 Rust 代码结合。

如果您要使用的库已作为静态存档分发,则无需重新构建代码。只需如上所述转换提供的接口头文件,并在编译/链接时包含静态存档。

如果代码存在作为源项目,这将是必要编译C / C ++代码到静态库,或者通过触发现有构建系统(如makeCMake等),或通过移植必要的编译步骤,使用一种叫做cc板条箱的工具。对于这两个步骤,都需要使用build.rs脚本。

Rustbuild.rs构建脚本

一个build.rs脚本是写在鲁斯特语法的文件,那是你的编译机上运行后,你的项目的依赖已建成,但在此之前项目的生成。

完整的参考可以在这里找到。build.rs脚本对于生成代码(例如通过bindgen)、调用外部构建系统(例如Make)或通过使用cccrate直接编译 C/C++ 非常有用。

触发外部构建系统

对于具有复杂外部项目或构建系统的项目,std::process::Command通过遍历相对路径、调用固定命令(例如make library),然后将生成的静态库复制到适当的在target构建目录中的位置。

虽然您的 crate 可能针对no_std嵌入式平台,但您build.rs只能在编译 crate 的机器上执行。这意味着您可以使用将在您的编译主机上运行的任何 Rust crate。

使用cccrate构建 C/C++ 代码

对于依赖项或复杂性有限的项目,或者对于难以修改构建系统以生成静态库(而不是最终的二进制文件或可执行文件)的项目,使用cccrate可能更容易,它提供了惯用的 Rust主机提供的编译器接口。

在将单个 C 文件编译为静态库的依赖项的最简单情况下,build.rs使用cccrate的示例脚本如下所示:

extern crate cc;

fn main() {
    cc::Build::new()
        .file("foo.c")
        .compile("libfoo.a");
}

A little Rust with your C

在 C 或 C++ 项目中使用 Rust 代码主要由两部分组成。

  • 在 Rust 中创建一个 C 友好的 API
  • 将您的 Rust 项目嵌入到外部构建系统中

除了cargoand之外meson,大多数构建系统都没有原生的 Rust 支持。因此,您很可能最好仅cargo用于编译您的 crate 和任何依赖项。

设置项目

cargo像往常一样创建一个新项目。

有一些标志告诉cargo发出一个系统库,而不是它的常规 rust 目标。这也允许您为库设置不同的输出名称,如果您希望它与您的 crate 的其余部分不同。

[lib]
name = "your_crate"
crate-type = ["cdylib"]      # Creates dynamic lib
# crate-type = ["staticlib"] # Creates static lib

构建CAPI

因为 C++ 没有稳定的 ABI 供 Rust 编译器作为目标,我们C用于不同语言之间的任何互操作性。在 C 和 C++ 代码中使用 Rust 时也不例外。

#[no_mangle\]

Rust 编译器对符号名称的处理与本机代码链接器所期望的不同。因此,任何 Rust 导出以在 Rust 之外使用的函数都需要被告知不要被编译器破坏。

extern "C"

默认情况下,您在 Rust 中编写的任何函数都将使用 Rust ABI(它也不稳定)。相反,在构建面向外部的 FFI API 时,我们需要告诉编译器使用系统 ABI。

根据您的平台,您可能希望针对特定的 ABI 版本,此处记录了这些版本。


将这些部分放在一起,您会得到一个大致如下所示的函数。

#[no_mangle]
pub extern "C" fn rust_function() {

}

就像C在您的 Rust 项目中使用代码一样,您现在需要将数据从应用程序的其余部分可以理解的形式来回转换。

链接和更大的项目背景

那么,问题就解决了一半。你现在怎么用这个?

这在很大程度上取决于您的项目和/或构建系统

cargo将创建一个my_lib.so/my_lib.dllmy_lib.a文件,具体取决于您的平台和设置。这个库可以简单地由你的构建系统链接。

但是,从 C 调用 Rust 函数需要一个头文件来声明函数签名。

Rust-ffi API 中的每个函数都需要有一个对应的头函数。

#[no_mangle]
pub extern "C" fn rust_function() {}

然后会变成

void rust_function();

等等。

有一个工具可以自动执行此过程,称为cbindgen,它会分析您的 Rust 代码,然后从中生成 C 和 C++ 项目的标头。

此时,使用 C 中的 Rust 函数就像包含标头并调用它们一样简单!

#include "my-rust-project.h"
rust_function();

Optimizations: the speed size tradeoff

每个人都希望他们的程序超快和超小,但通常不可能同时拥有这两种特性。本节讨论提供的不同优化级别rustc以及它们如何影响程序的执行时间和二进制大小。

没有优化

这是默认设置。当您打电话时,cargo build您使用开发 (AKA dev) 配置文件。此配置文件针对调试进行了优化,因此它启用调试信息,但启用任何优化,即它使用-C opt-level = 0.

至少对于裸机开发而言,debuginfo 是零成本的,因为它不会占用 Flash / ROM 中的空间,因此我们实际上建议您在发布配置文件中启用 debuginfo – 默认情况下禁用它。这将允许您在调试发布版本时使用断点。

[profile.release]
# symbols are nice and they don't increase the size on Flash
debug = true

没有优化对于调试来说是很好的,因为单步调试代码感觉就像你在逐条语句执行程序,而且你可以print 在 GDB 中堆叠变量和函数参数。代码优化后,尝试打印变量会导致$0 = <value optimized out>打印。

dev配置文件的最大缺点是生成的二进制文件会很大而且很慢。大小通常更成问题,因为未优化的二进制文件可能占用数十 KiB 的闪存,而您的目标设备可能没有——结果:未优化的二进制文件不适合您的设备!

我们可以有更小的、调试器友好的二进制文件吗?是的,有一个技巧。

优化依赖

有一个名为 Cargo 的功能profile-overrides,可让您覆盖依赖项的优化级别。您可以使用该功能来优化所有依赖项的大小,同时保持 top crate 未优化和调试器友好。

下面是一个例子:

# Cargo.toml
[package]
name = "app"
# ..

[profile.dev.package."*"] # +
opt-level = "z" # +

没有覆盖:

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 9060   0x8000400
.rodata               1708   0x8002780
.data                    0  0x20000000
.bss                     4  0x20000000

使用覆盖:

$ cargo size --bin app -- -A
app  :
section               size        addr
.vector_table         1024   0x8000000
.text                 3490   0x8000400
.rodata               1100   0x80011c0
.data                    0  0x20000000
.bss                     4  0x20000000

这意味着 Flash 使用量减少了 6 KiB,而 top crate 的可调试性没有任何损失。如果您进入依赖项,那么您将<value optimized out>再次开始看到这些 消息,但通常情况下,您想要调试顶级板条箱而不是依赖项。如果您确实需要调试依赖项,那么您可以使用该profile-overrides功能从优化中排除特定的依赖项。请参阅下面的示例:

# ..

# don't optimize the `cortex-m-rt` crate
[profile.dev.package.cortex-m-rt] # +
opt-level = 0 # +

# but do optimize all the other dependencies
[profile.dev.package."*"]
codegen-units = 1 # better optimizations
opt-level = "z"

现在顶部板条箱和cortex-m-rt调试器友好!

优化速度

截至 2018 年 9 月 18 日,rustc支持三个“优化速度”级别:opt-level = 123。当您运行时,cargo build --release您使用的是默认为opt-level = 3.

opt-level = 2和都3以牺牲二进制大小为代价来优化速度,但 level3比 level 执行更多的矢量化和内联2。特别是,您会看到 atopt-level等于或大于2LLVM 将展开循环。循环展开在 Flash / ROM 方面的成本相当高(例如,从 26 字节到 194 以实现零数组循环),但也可以在给定正确条件的情况下将执行时间减半(例如,迭代次数足够大)。

目前没有办法禁用循环展开opt-level = 23所以如果你负担不起它的成本,你应该优化你的程序的大小。

优化尺寸

截至 2018 年 9 月 18 日,rustc支持两个“优化大小”级别:opt-level = "s""z". 这些名称是从 clang/LLVM 继承而来的,并不太具有描述性,但"z"旨在说明它生成的二进制文件比"s".

如果您希望针对大小优化发布二进制文件profile.release.opt-level,请Cargo.toml按如下所示更改 设置。

[profile.release]
# or "z"
opt-level = "s"

这两个优化级别大大降低了 LLVM 的内联阈值,这是一个用于决定是否内联函数的指标。Rust 原则之一是零成本抽象;这些抽象倾向于使用许多新类型和小函数来保存不变量(例如,借用内部值的函数,例如deref, as_ref),因此低内联阈值会使 LLVM 错过优化机会(例如消除死分支、内联调用闭包)。

在优化大小时,您可能想尝试增加内联阈值,看看这是否对二进制大小有任何影响。更改内联阈值的推荐方法是将-C inline-threshold标志附加到.cargo/config.toml.

# .cargo/config.toml
# this assumes that you are using the cortex-m-quickstart template
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
rustflags = [
  # ..
  "-C", "inline-threshold=123", # +
]

使用什么价值?从 1.29.0 开始,这些是不同优化级别使用的内联阈值

  • opt-level = 3 使用 275
  • opt-level = 2 使用 225
  • opt-level = "s" 使用 75
  • opt-level = "z" 使用 25

你应该尝试225275优化大小时。

Appendix A: Glossary

The embedded ecosystem is full of different protocols, hardware components and vendor-specific things that use their own terms and abbreviations. This Glossary attempts to list them with pointers for understanding them better.

BSP

A Board Support Crate provides a high level interface configured for a specific board. It usually depends on a HAL crate. There is a more detailed description on the memory-mapped registers page or for a broader overview see this video.

FPU

Floating-point Unit. A ‘math processor’ running only operations on floating-point numbers.

HAL

A Hardware Abstraction Layer crate provides a developer friendly interface to a microcontroller’s features and peripherals. It is usually implemented on top of a Peripheral Access Crate (PAC). It may also implement traits from the embedded-hal crate. There is a more detailed description on the memory-mapped registers page or for a broader overview see this video.

I2C

Sometimes referred to as I²C or Inter-IC. It is a protocol meant for hardware communication within a single integrated circuit. See here for more details

PAC

A Peripheral Access Crate provides access to a microcontroller’s peripherals. It is one of the lower level crates and is usually generated directly from the provided SVD, often using svd2rust. The Hardware Abstraction Layer would usually depend on this crate. There is a more detailed description on the memory-mapped registers page or for a broader overview see this video.

SPI

Serial Peripheral Interface. See here for more information.

SVD

System View Description is an XML file format used to describe the programmers view of a microcontroller device. You can read more about it on the ARM CMSIS documentation site.

UART

Universal asynchronous receiver-transmitter. See here for more information.

USART

Universal synchronous and asynchronous receiver-transmitter. See here for more information.


😁《Discovery》

硬件/知识要求

由于嵌入式编程的性质,理解值的二进制和十六进制表示如何工作,以及一些按位运算符的使用也将非常有帮助。例如,了解以下程序如何产生其输出会很有用。

fn main() {
    let a = 0x4000_0000 + 0xa2;

    // Use of the bit shift "<<" operation.
    let b = 1 << 5;

    // {:X} will format values as hexadecimal
    println!("{:X}: {:X}", a, b);
}

此外,要遵循此材料,您将需要以下硬件:

(有些组件是可选的,但建议使用)

(您可以从“大型”电子 供应商电子商务 网站购买此板)

  • 可选的。一个3.3V USB <-> 串行模块。为了详细说明:如果你有发现主板的最新版本中的一个(通常是给出的第一个版本的情况下在数年前发布的),那么你就不会因为主板包括板载这个功能需要这个模块。如果您有旧版电路板,那么第 10 章和第 11 章将需要此模块。为了完整起见,我们将包含使用串行模块的说明。本书将使用此特定型号,但您可以使用任何其他型号,只要它在 3.3V 下运行即可。您可以从电子商务网站购买的 CH340G 模块也可以使用,而且可能更便宜。

  • 可选的。HC-05 蓝牙模块(带接头!)。HC-06 也可以。

(与其他中国零件一样,您几乎只能在电子商务 网站上找到这些零件。(美国)电子供应商出于某种原因通常不会库存这些零件)

  • 两条 mini-B USB 电缆。需要一个才能使 STM32F3DISCOVERY 板工作。仅当您有串行 <-> USB 模块时才需要另一个。确保电缆都支持数据传输,因为某些电缆仅支持充电设备。

注意这些并不是几乎每部 Android 手机都随附的 USB 电缆;那些是微型USB 电缆。确保你有正确的东西!

  • 主要是可选的。5 根母对母、4 根公对母和 1 根公对公跳线(又名杜邦)。您很可能需要一对一女性才能使 ITM 发挥作用。仅当您使用 USB <-> 串行和蓝牙模块时才需要其他电线。

(您可以从电子供应商电子商务 网站获得这些)

FAQ : 等等,为什么我需要这个特定的硬件?

它让我和你的生活更轻松。

如果我们不必担心硬件差异,那么材料会更加平易近人。相信我这一点。

常见问题:我可以使用不同的开发板来遵循此材料吗?

也许?这主要取决于两件事:您之前使用微控制器的经验和/或是否已经存在f3用于您的开发板的高级板条箱,例如。

使用不同的开发板,如果不是所有的初学者友好性和“易于遵循”,IMO,本文将失去大部分。

如果您有不同的开发板,并且您不认为自己完全是初学者,那么最好从快速入门项目模板开始。

Setting up a development environment

搭建开发环境

处理微控制器涉及多种工具,因为我们将处理与您的计算机不同的架构,我们必须在“远程”设备上运行和调试程序。

文档

工具不是一切。没有文档,几乎不可能使用微控制器。

我们将在本书中提及所有这些文件:

注意所有这些链接都指向 PDF 文件,其中一些文件长达数百页,大小为数 MB。

*注意:较新的(从 2020/09 左右开始)探索板可能有不同的电子罗盘和陀螺仪(请参阅用户手册)。因此,第 14-16 章中的许多内容将不会按原样运行。像这样检查 github 问题。

工具

我们将使用下面列出的所有工具。在未指定最低版本的情况下,任何最新版本都可以使用,但我们列出了我们测试过的版本。

  • Rust 1.31 或更新的工具链。

  • itmdump>=0.3.1 ( cargo install itm)。测试版本:0.3.1。

  • OpenOCD >=0.8。测试版本:v0.9.0 和 v0.10.0

  • arm-none-eabi-gdb. 强烈建议使用 7.12 或更高版本。测试版本:7.10、7.11、7.12 和 8.1

  • cargo-binutils. 0.1.4 或更新版本。

  • minicom在 Linux 和 macOS 上。测试版本:2.7。读者报告这picocom也有效,但我们将minicom在本文中使用。

  • PuTTY 在 Windows 上。

如果你的电脑有蓝牙功能并且你有蓝牙模块,你可以另外安装这些工具来玩蓝牙模块。所有这些都是可选的:

  • Linux,仅当您没有像 Blueman 这样的蓝牙管理器应用程序时。
    • bluez
    • hcitool
    • rfcomm
    • rfkill

macOS / OSX / Windows 用户只需要其操作系统附带的默认蓝牙管理器。

接下来,按照与操作系统无关的安装说明安装一些工具:

rustc & cargo

按照https://rustup.rs 上的说明安装 rustup 。

如果您已经安装了 rustup,请仔细检查您是否在稳定频道上并且您的稳定工具链是最新的。rustc -V应该返回一个比下面显示的日期新的日期:

$ rustc -V
rustc 1.31.0 (abe02cefd 2018-12-04)

itmdump

cargo install itm

验证版本 >=0.3.1

$ itmdump -V
itmdump 0.3.1

cargo-binutils

安装 llvm-tools-preview

rustup component add llvm-tools-preview

安装 cargo-binutils

cargo install cargo-binutils

验证工具是否已安装

在终端运行以下命令

cargo new test-size
cd test-size
cargo run
cargo size -- -version

结果应该是这样的:

~
$ cargo new test-size
     Created binary (application) `test-size` package

~
$ cd test-size

~/test-size (main)
$ cargo run
   Compiling test-size v0.1.0 (~/test-size)
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s
     Running `target/debug/test-size`
Hello, world!

~/test-size (main)
$ cargo size -- -version
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
LLVM (http://llvm.org/):
  LLVM version 11.0.0-rust-1.50.0-stable
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: znver2

Linux

Here are the installation commands for a few Linux distributions.

REQUIRED packages

Ubuntu 18.04 or newer / Debian stretch or newer

NOTE gdb-multiarch is the GDB command you’ll use to debug your ARM Cortex-M programs

sudo apt-get install \
  gdb-multiarch \
  minicom \
  openocd

Ubuntu 14.04 and 16.04

NOTE arm-none-eabi-gdb is the GDB command you’ll use to debug your ARM Cortex-M programs

sudo apt-get install \
  gdb-arm-none-eabi \
  minicom \
  openocd

Fedora 23 or newer

sudo dnf install \
  minicom \
  openocd \
  gdb

Arch Linux

NOTE arm-none-eabi-gdb is the GDB command you’ll use to debug your ARM Cortex-M programs

sudo pacman -S \
  arm-none-eabi-gdb \
  minicom \
  openocd

Other distros

NOTE arm-none-eabi-gdb is the GDB command you’ll use to debug your ARM Cortex-M programs

For distros that don’t have packages for ARM’s pre-built toolchain, download the “Linux 64-bit” file and put its bin directory on your path. Here’s one way to do it:

mkdir -p ~/local && cd ~/local
tar xjf /path/to/downloaded/file/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2

Then, use your editor of choice to append to your PATH in the appropriate shell init file (e.g. ~/.zshrc or ~/.bashrc):

PATH=$PATH:$HOME/local/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux/bin

Optional packages

Ubuntu / Debian

sudo apt-get install \
  bluez \
  rfkill

Fedora

sudo dnf install \
  bluez \
  rfkill

Arch Linux

sudo pacman -S \
  bluez \
  bluez-utils \
  rfkill

udev rules

These rules let you use USB devices like the F3 and the Serial module without root privilege, i.e. sudo.

Create 99-openocd.rules in /etc/udev/rules.d using the idVendor and idProduct from the lsusb output.

For example, connect the STM32F3DISCOVERY to your computer using a USB cable. Be sure to connect the cable to the “USB ST-LINK” port, the USB port in the center of the edge of the board.

Execute lsusb:

lsusb | grep ST-LINK

It should result in something like:

$ lsusb | grep ST-LINK
Bus 003 Device 003: ID 0483:374b STMicroelectronics ST-LINK/V2.1

So the idVendor is 0483 and idProduct is 374b.

Create /etc/udev/rules.d/99-openocd.rules:

sudo vi /etc/udev/rules.d/99-openocd.rules

With the contents:

# STM32F3DISCOVERY - ST-LINK/V2.1
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="374b", MODE:="0666"

For older devices with OPTIONAL USB <-> FT232 based Serial Module

Create /etc/udev/rules.d/99-ftdi.rules:

sudo vi /etc/udev/rules.d/99-openocd.rules

With the contents:

# FT232 - USB <-> Serial Converter
ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE:="0666"

Reload the udev rules with:

sudo udevadm control --reload-rules

If you had any board plugged to your computer, unplug them and then plug them in again.

Windows

arm-none-eabi-gdb

ARM provides .exe installers for Windows. Grab one from here, and follow the instructions. Just before the installation process finishes tick/select the “Add path to environment variable” option. Then verify that the tools are in your %PATH%:

Verify gcc is installed:

arm-none-eabi-gcc -v

The results should be something like:

(..)
$ arm-none-eabi-gcc -v
gcc version 5.4.1 20160919 (release) (..)

OpenOCD

There’s no official binary release of OpenOCD for Windows but there are unofficial releases available here. Grab the 0.10.x zipfile and extract it somewhere in your drive (I recommend C:\OpenOCD but with the drive letter that makes sense to you) then update your %PATH% environment variable to include the following path: C:\OpenOCD\bin (or the path that you used before).

Verify OpenOCD is installed and in your %PATH% with:

openocd -v

The results should be something like:

$ openocd -v
Open On-Chip Debugger 0.10.0
(..)

PuTTY

Download the latest putty.exe from this site and place it somewhere in your %PATH%.

You’ll also need to install this USB driver or OpenOCD won’t work. Follow the installer instructions and make sure you install the right (32-bit or 64-bit) version of the driver.

macOS

All the tools can be install using Homebrew:

Install ArmMbed

brew tap ArmMbed/homebrew-formulae

Install the ARM GCC toolchain

brew install arm-none-eabi-gcc

Install minicom and OpenOCD

brew install minicom openocd

Verify the installation

Let’s verify that all the tools were installed correctly.

Linux only

Verify permissions

Connect the STM32F3DISCOVERY to your computer using an USB cable. Be sure to connect the cable to the “USB ST-LINK” port, the USB port in the center of the edge of the board.

The STM32F3DISCOVERY should now appear as a USB device (file) in /dev/bus/usb. Let’s find out how it got enumerated:

lsusb | grep -i stm

This should result in:

$ lsusb | grep -i stm
Bus 003 Device 004: ID 0483:374b STMicroelectronics ST-LINK/V2.1
$ # ^^^        ^^^

In my case, the STM32F3DISCOVERY got connected to the bus #3 and got enumerated as the device #4. This means the file /dev/bus/usb/003/004 is the STM32F3DISCOVERY. Let’s check its permissions:

$ ls -la /dev/bus/usb/003/004
crw-rw-rw-+ 1 root root 189, 259 Feb 28 13:32 /dev/bus/usb/003/00

The permissions should be crw-rw-rw-. If it’s not … then check your udev rules and try re-loading them with:

sudo udevadm control --reload-rules

For older devices with OPTIONAL USB <-> FT232 based Serial Module

Unplug the STM32F3DISCOVERY and plug the Serial module. Now, figure out what’s its associated file:

$ lsusb | grep -i ft232
Bus 003 Device 005: ID 0403:6001 Future Technology Devices International, Ltd FT232 Serial (UART) IC

In my case, it’s the /dev/bus/usb/003/005. Now, check its permissions:

$ ls -l /dev/bus/usb/003/005
crw-rw-rw- 1 root root 189, 21 Sep 13 00:00 /dev/bus/usb/003/005

As before, the permissions should be crw-rw-rw-.

Verify OpenOCD connection

Connect the STM32F3DISCOVERY using the USB cable to the USB port in the center of edge of the board, the one that’s labeled “USB ST-LINK”.

Two red LEDs should turn on right after connecting the USB cable to the board.

IMPORTANT There is more than one hardware revision of the STM32F3DISCOVERY board. For older revisions, you’ll need to change the “interface” argument to -f interface/stlink-v2.cfg (note: no -1 at the end). Alternatively, older revisions can use -f board/stm32f3discovery.cfg instead of -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg.

NOTE OpenOCD v0.11.0 has deprecated interface/stlink-v2.cfg in favor of interface/stlink.cfg which supports ST-LINK/V1, ST-LINK/V2, ST-LINK/V2-1, and ST-LINK/V3.

*Nix

FYI: The interface directory is typically located in /usr/share/openocd/scripts/, which is the default location OpenOCD expects these files. If you’ve installed them somewhere else use the -s /path/to/scripts/ option to specify your install directory.

openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

or

openocd -f interface/stlink.cfg -f target/stm32f3x.cfg

Windows

Below the references to C:\OpenOCD is the directory where OpenOCD is installed.

openocd -s C:\OpenOCD\share\scripts -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

NOTE cygwin users have reported problems with the -s flag. If you run into that problem you can add C:\OpenOCD\share\scripts\ directory to the parameters.

cygwin users:

openocd -f C:\OpenOCD\share\scripts\interface\stlink-v2-1.cfg -f C:\OpenOCD\share\scripts\target\stm32f3x.cfg

All

OpenOCD is a service which forwards debug information from the ITM channel to a file, itm.txt, as such it runs forever and does not return to the terminal prompt.

The initial output of OpenOCD is something like:

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v27 API v2 SWIM v15 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.915608
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

(If you don’t … then check the general troubleshooting instructions.)

Also, one of the red LEDs, the one closest to the USB port, should start oscillating between red light and green light.

That’s it! It works. You can now use Ctrl-c to stop OpenOCD or close/kill the terminal.

Meet your hardware

STM32F3DISCOVERY(“F3”)

我们将在整本书中将此板称为“F3”。以下是板上的许多组件中的一些:

在这些组件中,最重要的是微控制器(有时缩写为“MCU”代表“微控制器单元”),它是位于电路板中心的黑色大方块。MCU 运行您的代码。您有时可能会读到“对电路板进行编程”,而实际上我们所做的是对安装在电路板上的 MCU 进行编程。

STM32F303VCT6(“STM32F3”)

由于 MCU 如此重要,让我们仔细看看坐在我们板上的那个。

我们的 MCU 被 100 个微小的金属引脚包围着。这些引脚连接到 迹线,这些小“道路”充当将电路板上的组件连接在一起的电线。MCU 可以动态改变引脚的电气特性。这类似于改变电流流经电路的电灯开关。通过启用或禁用流过特定引脚的电流,可以打开和关闭连接到该引脚(通过走线)的 LED。

每个制造商使用不同的零件编号方案,但许多制造商允许您通过查看零件编号来确定有关组件的信息。查看我们 MCU 的部件号 ( STM32F303VCT6),ST前面的 向我们暗示这是由ST Microelectronics制造的部件。通过搜索ST 的营销材料,我们还可以了解到以下内容:

  • M32表示,这是一个基于ARM®的32位微控制器。
  • F3代表的MCU是ST的“STM32F3系列”。这是一系列基于 Cortex®-M4 处理器设计的 MCU。
  • 部件号的其余部分详细介绍了额外功能和 RAM 大小等内容,目前我们不太关心这些。

Arm? Cortex-M4?

如果我们的芯片是 ST 制造的,那么 Arm 是谁?如果我们的芯片是 STM32F3,那么 Cortex-M4 是什么?

您可能会惊讶地发现,虽然“基于 Arm”的芯片非常受欢迎,但“Arm”商标背后的公司 ( Arm Holdings ) 实际上并不制造用于购买的芯片。相反,他们的主要商业模式只是设计芯片的一部分。然后,他们会将这些设计授权给制造商,制造商又会以物理硬件的形式实施这些设计(也许通过他们自己的一些调整)然后可以出售。ARM 在这里的战略与英特尔等公司不同,后者既设计制造芯片。

Arm 许可了一系列不同的设计。他们的“Cortex-M”系列设计主要用作微控制器的核心。例如,Cortex-M0 专为低成本和低功耗而设计。Cortex-M7 成本更高,但具有更多功能和性能。我们的 STM32F3 的核心是基于 Cortex-M4,它处于中间:比 Cortex-M0 的功能和性能更多,但比 Cortex-M7 便宜。

幸运的是,您不需要为了本书而对不同类型的处理器或 Cortex 设计了解太多。但是,希望您现在对设备的术语有了更多了解。当您专门使用 STM32F3 时,您可能会发现自己正在阅读文档并使用基于 Cortex-M 的芯片的工具,因为 STM32F3 基于 Cortex-M 设计。

串行模块

如果您有旧版本的发现板,您可以使用此模块在 F3 中的微控制器和您的计算机之间交换数据。该模块将使用 USB 电缆连接到您的计算机。这个时候我就不多说了。

如果您有较新版本的电路板,则不需要此模块。ST-LINK 将兼作 USB<-> 串行转换器,通过 PC4 和 PC5 引脚连接到微控制器 USART1。

蓝牙模块

该模块与串行模块的用途完全相同,但它通过蓝牙而不是 USB 发送数据。

LED roulette(轮盘)

好的,让我们从构建以下应用程序开始:

我将给你一个高级 API 来实现这个应用程序,但别担心我们稍后会做低级的东西。本章的主要目标是熟悉刷机和调试过程。

在本文中,我们将使用发现存储库中的入门代码。确保您始终拥有最新版本的 master 分支,因为该网站会跟踪该分支。

起始代码位于该src存储库的目录中。在该目录中,还有更多以本书每一章命名的目录。这些目录中的大多数是入门 Cargo 项目。

现在,跳转到src/05-led-roulette目录。检查src/main.rs文件:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::entry;

#[entry]
fn main() -> ! {
    let _y;
    let x = 42;
    _y = x;

    // infinite loop; just so we don't leave this stack frame
    loop {}
}

微控制器程序在两个方面不同于标准程序:#![no_std]#![no_main]

no_std属性表示该程序不会使用stdcrate,它假定一个底层操作系统;该程序将改为使用corecrate,它的一个子集std可以在裸机系统上运行(即,没有像文件和套接字这样的操作系统抽象的系统)。

no_main属性表示该程序不会使用标准main接口,该接口是为接收参数的命令行应用程序量身定制的。main我们将使用crate 中的entry属性cortex-m-rt来定义自定义入口点,而不是标准。在这个程序中,我们将入口点命名为“main”,但也可以使用任何其他名称。入口点函数必须有签名fn() -> !;这种类型表示函数不能返回——这意味着程序永远不会终止。

如果您细心观察,您还会注意到.cargoCargo 项目中也有一个目录。该目录包含一个 Cargo 配置文件 ( .cargo/config),它调整链接过程以根据目标设备的要求定制程序的内存布局。这种修改后的链接过程是cortex-m-rt板条箱的要求。您还将.cargo/config在以后的部分中进行进一步的调整,以使构建和调试更容易。

好的,让我们从构建这个程序开始。

Build it

第一步是构建我们的“二进制”板条箱。因为微控制器的架构与您的计算机不同,所以我们必须进行交叉编译。在 Rust 中交叉编译就像--targetrustc或 Cargo传递一个额外的标志一样简单。复杂的部分是找出该标志的参数:目标的名称

F3 中的微控制器中有一个 Cortex-M4F 处理器。rustc知道如何交叉编译到 Cortex-M 架构,并提供 4 个不同的目标,涵盖该架构中的不同处理器系列:

  • thumbv6m-none-eabi, 对于 Cortex-M0 和 Cortex-M1 处理器
  • thumbv7m-none-eabi, 对于 Cortex-M3 处理器
  • thumbv7em-none-eabi, 适用于 Cortex-M4 和 Cortex-M7 处理器
  • thumbv7em-none-eabihf, 对于 Cortex-M4 F和 Cortex-M7 F处理器

对于 F3,我们将使用thumbv7em-none-eabihf目标。在交叉编译之前,您必须为您的目标下载标准库的预编译版本(实际上是它的简化版本)。这是使用rustup

rustup target add thumbv7em-none-eabihf

您只需要执行上述步骤一次;每当您更新工具链时,rustup都会重新安装新的标准库(rust-std组件)。

随着rust-std到位组件使用货运您现在可以交叉编译程序。

注意确保您在src/05-led-roulette目录中并运行cargo build以下命令以创建可执行文件:

cargo build --target thumbv7em-none-eabihf

在您的控制台上,您应该看到如下内容:

$ cargo build --target thumbv7em-none-eabihf
   Compiling typenum v1.12.0
   Compiling semver-parser v0.7.0
   Compiling version_check v0.9.2
   Compiling nb v1.0.0
   Compiling void v1.0.2
   Compiling autocfg v1.0.1
   Compiling cortex-m v0.7.1
   Compiling proc-macro2 v1.0.24
   Compiling vcell v0.1.3
   Compiling unicode-xid v0.2.1
   Compiling stable_deref_trait v1.2.0
   Compiling syn v1.0.60
   Compiling bitfield v0.13.2
   Compiling cortex-m v0.6.7
   Compiling cortex-m-rt v0.6.13
   Compiling r0 v0.2.2
   Compiling stm32-usbd v0.5.1
   Compiling stm32f3 v0.12.1
   Compiling usb-device v0.2.7
   Compiling cfg-if v1.0.0
   Compiling paste v1.0.4
   Compiling stm32f3-discovery v0.6.0
   Compiling embedded-dma v0.1.2
   Compiling volatile-register v0.2.0
   Compiling nb v0.1.3
   Compiling embedded-hal v0.2.4
   Compiling semver v0.9.0
   Compiling generic-array v0.14.4
   Compiling switch-hal v0.3.2
   Compiling num-traits v0.2.14
   Compiling num-integer v0.1.44
   Compiling rustc_version v0.2.3
   Compiling bare-metal v0.2.5
   Compiling cast v0.2.3
   Compiling quote v1.0.9
   Compiling generic-array v0.13.2
   Compiling generic-array v0.12.3
   Compiling generic-array v0.11.1
   Compiling panic-itm v0.4.2
   Compiling lsm303dlhc v0.2.0
   Compiling as-slice v0.1.4
   Compiling micromath v1.1.0
   Compiling accelerometer v0.12.0
   Compiling chrono v0.4.19
   Compiling aligned v0.3.4
   Compiling rtcc v0.2.0
   Compiling cortex-m-rt-macros v0.1.8
   Compiling stm32f3xx-hal v0.6.1
   Compiling aux5 v0.2.0 (~/embedded-discovery/src/05-led-roulette/auxiliary)
   Compiling led-roulette v0.2.0 (~/embedded-discovery/src/05-led-roulette)
    Finished dev [unoptimized + debuginfo] target(s) in 17.91s

注意一定要在没有优化的情况下编译这个 crate 。上面提供的 Cargo.toml 文件和构建命令将确保优化关闭。

好的,现在我们已经生成了一个可执行文件。这个可执行文件不会使任何 LED 闪烁,它只是我们将在本章后面构建的一个简化版本。作为完整性检查,让我们验证生成的可执行文件实际上是 ARM 二进制文件:

cargo readobj --target thumbv7em-none-eabihf --bin led-roulette -- --file-header

cargo readobj ..上述相当于 readelf -h target/thumbv7em-none-eabihf/debug/led-roulette 而且应该产生类似的东西:

$ cargo readobj --target thumbv7em-none-eabihf --bin led-roulette -- --file-header
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0x8000195
  Start of program headers:          52 (bytes into file)
  Start of section headers:          818328 (bytes into file)
  Flags:                             0x5000400
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         4
  Size of section headers:           40 (bytes)
  Number of section headers:         22
  Section header string table index: 20

接下来,我们将把程序烧写到我们的微控制器中。

Flash it

闪存是将我们的程序移动到微控制器(持久性)内存中的过程。一旦刷入,微控制器每次上电都会执行刷入的程序。

在这种情况下,我们的led-roulette程序将是微控制器存储器中的唯一程序。我的意思是微控制器上没有其他东西在运行:没有操作系统,没有“守护进程”,什么都没有。led-roulette可以完全控制设备。

进入实际闪烁。我们需要做的第一件事是启动 OpenOCD。我们在上一节中这样做了,但这次我们将在临时目录中运行命令(/tmp在 *nix 上; %TEMP%在 Windows 上)。

确保 F3 已连接到您的计算机并在新终端中运行以下命令。

对于 *nix 和 MacOS:

cd /tmp
openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

对于 Windows注意:替换C:实际的 OpenOCD 路径:

cd %TEMP%
openocd -s C:\share\scripts -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg

注意电路板的较旧版本需要将稍微不同的参数传递给 openocd

程序会阻塞;让那个终端保持打开状态。

现在是解释该openocd命令实际执行的操作的好时机。

我提到 STM32F3DISCOVERY(又名 F3)实际上有两个微控制器。其中之一用作程序员/调试器。用作编程器的电路板部分称为 ST-LINK(意法半导体决定这样称呼它)。此 ST-LINK 使用串行线调试 (SWD) 接口连接到目标微控制器(此接口是 ARM 标准,因此在处理其他基于 Cortex-M 的微控制器时会遇到它)。该 SWD 接口可用于闪存和调试微控制器。ST-LINK 连接到“USB ST-LINK”端口,当您将 F3 连接到计算机时,它会显示为 USB 设备。

至于 OpenOCD,它是一种提供一些服务的软件,例如在 USB 设备上提供一些服务,例如公开 SWD 或 JTAG 等调试协议的GDB 服务器

关于实际命令:.cfg我们使用的那些文件指示 OpenOCD 查找 ST-LINK USB 设备 ( interface/stlink-v2-1.cfg) 并期望 STM32F3XX 微控制器 ( target/stm32f3x.cfg) 连接到 ST-LINK。

OpenOCD 输出如下所示:

$ openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg
Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "hla_swd". To override use 'transport select '.
adapter speed: 1000 kHz
adapter_nsrst_delay: 100
Info : The selected transport took over low-level target control. The results might differ compared to plain JTAG/SWD
none separate
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : Unable to match requested speed 1000 kHz, using 950 kHz
Info : clock speed 950 kHz
Info : STLINK v2 JTAG v37 API v2 SWIM v26 VID 0x0483 PID 0x374B
Info : using stlink api v2
Info : Target voltage: 2.888183
Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints

“6 个断点,4 个观察点”部分表示处理器可用的调试功能。

保持该openocd进程运行,并在前一个终端或新终端中 确保您位于项目src/05-led-roulette/目录中

我提到 OpenOCD 提供了一个 GDB 服务器,所以让我们现在连接到它:

执行 GDB

首先,我们需要确定gdb您的哪个版本能够调试 ARM 二进制文件。

这可以是以下任一命令,请尝试每一个:

arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
gdb-multiarch -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette

失败案例

如果有warningerror在该Remote debugging using :3333行之后,您可以检测到失败案例:

$ gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
warning: Architecture rejected target-supplied description
Truncated register 16 in remote 'g' packet
(gdb)

成功案例

成功案例一:

$ arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
cortex_m_rt::Reset () at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:497
497     pub unsafe extern "C" fn Reset() -> ! {
(gdb)

成功案例2:

~/embedded-discovery/src/05-led-roulette (master)
$ arm-none-eabi-gdb -q -ex "target remote :3333" target/thumbv7em-none-eabihf/debug/led-roulette
Reading symbols from target/thumbv7em-none-eabihf/debug/led-roulette...
Remote debugging using :3333
0x00000000 in ?? ()
(gdb)

在失败和成功的情况下,您都应该在OpenOCD 终端中看到新的输出,如下所示:

 Info : stm32f3x.cpu: hardware has 6 breakpoints, 4 watchpoints
+Info : accepting 'gdb' connection on tcp/3333
+Info : device id = 0x10036422
+Info : flash size = 256kbytes

注意如果您收到类似的错误undefined debug reason 7 - target needs reset,您可以尝试monitor reset halt按照此处所述运行。

默认情况下,OpenOCD 的 GDB 服务器侦听 TCP 端口 3333(本地主机)。此命令正在连接到该端口。

更新 ../.cargo/config.toml

既然您已成功确定需要使用哪个调试器,我们需要进行更改,../.cargo/config.toml以便cargo run命令成功。

NOTE cargo是 Rust 包管理器,你可以在这里阅读它 。

回到终端提示符并查看../.cargo/config.toml

~/embedded-discovery/src/05-led-roulette
$ cat ../.cargo/config.toml
[target.thumbv7em-none-eabihf]
runner = "arm-none-eabi-gdb -q"
# runner = "gdb-multiarch -q"
# runner = "gdb -q"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

使用您喜欢的编辑器进行编辑,../.cargo/config.toml以便该 runner行包含该调试器的正确名称:

nano ../.cargo/config.toml

例如,如果您的调试器gdb-multiarch在编辑之后git diff应该是:

$ git diff ../.cargo/config.toml
diff --git a/src/.cargo/config.toml b/src/.cargo/config.toml
index ddff17f..8512cfe 100644
--- a/src/.cargo/config.toml
+++ b/src/.cargo/config.toml
@@ -1,6 +1,6 @@
 [target.thumbv7em-none-eabihf]
-runner = "arm-none-eabi-gdb -q"
-# runner = "gdb-multiarch -q"
+# runner = "arm-none-eabi-gdb -q"
+runner = "gdb-multiarch -q"
 # runner = "gdb -q"
 rustflags = [
   "-C", "link-arg=-Tlink.x",

现在您已经../.cargo/config.toml设置好了,让我们使用cargo run启动调试会话来测试它。

注:--target thumbv7em-none-eabihf定义哪些架构来构建和运行。在我们的../.cargo/config.toml文件中,我们 target = "thumbv7em-none-eabihf"实际上没有必要在--target此处指定我们这样做,只是为了让您知道可以使用命令行上的参数并且它们会覆盖config.toml文件中的参数。

cargo run --target thumbv7em-none-eabihf

结果是:

~/embedded-discovery/src/05-led-roulette
$ cargo run --target thumbv7em-none-eabihf
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...

现在发出target remote :3333连接到 OpenOCD 服务器并连接到 F3 的命令:

(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()

太棒了,我们将来会修改../.cargo/config.toml但是,由于此文件与所有章节共享,因此应考虑到这一点进行更改。如果您想要或我们需要进行仅与特定章节有关的更改,则创建.cargo/config.toml该章节目录的本地目录。

烧录设备

假设您运行了 GDB,如果没有按照上一节中的建议启动它。

现在使用load命令 ingdb将程序实际刷入设备:

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x20ec lma 0x8000194
Loading section .rodata, size 0x514 lma 0x8002280
Start address 0x08000194, load size 10132
Transfer rate: 17 KB/sec, 3377 bytes/write.

您还将在 OpenOCD 终端中看到新的输出,例如:

 Info : flash size = 256kbytes
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+adapter speed: 4000 kHz
+target halted due to breakpoint, current mode: Thread
+xPSR: 0x61000000 pc: 0x2000003a msp: 0x2000a000
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

我们的程序已经加载完毕,让我们调试一下吧!

Debug it

我们已经在调试会话中,所以让我们调试我们的程序。

load命令之后,我们的程序在它的入口点停止。这由 GDB 输出的“起始地址 0x8000XXX”部分指示。入口点是处理器/CPU 将首先执行的程序的一部分。

我提供给您的入门项目有一些在函数之前运行的额外代码main。此时,我们对“pre-main”部分不感兴趣,所以让我们直接跳到main函数的开头。我们将使用断点来做到这一点。break main(gdb)提示符下发出:

注意对于这些 GDB 命令,我通常不会提供可复制的代码块,因为它们很短,而且自己输入它们会更快。此外,大多数可以缩短。例如bforbreaksfor step,请参阅GDB 快速参考 以获取更多信息或使用 Google 查找您的其他人。此外,您可以通过键入前几个字母而不是一个选项卡来完成或两个选项卡来查看所有可能的命令来使用选项卡完成。

最后,help xxxx其中 xxxx 是命令,将提供短名称和其他信息:

(gdb) help s
step, s
Step program until it reaches a different source line.
Usage: step [N]
Argument N means step N times (or till program stops for another reason).
(gdb) break main
Breakpoint 1 at 0x80001f0: file src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

接下来发出continue命令:

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7
7       #[entry]

断点可用于停止程序的正常流程。该continue命令将使程序自由运行,直到到达断点。在这种情况下,直到它到达#[entry] main 函数的蹦床和break main设置断点的位置。

请注意,GDB 输出显示“断点 1”。请记住,我们的处理器只能使用其中的六个断点,因此最好注意这些消息。

好的。由于我们停在#[entry]并使用了,disassemble /m我们看到了进入的代码,这是一个蹦床到主。这意味着它会设置堆栈,然后main使用 ARM 分支和链接指令调用对该函数的子例程调用bl

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <_ZN12led_roulette18__cortex_m_rt_main17he61ef18c060014a5E>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

接下来,我们需要发出一个stepGDB 命令,该命令将通过语句步进到函数/过程中来推进程序语句。所以在第一个step命令之后,我们在里面main并定位在第一个可执行rust语句,第 10 行,但它 没有被执行:

(gdb) step
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:10
10          let x = 42;

接下来,我们将发出第二个step,它执行第 10 行并在第 1 行停止11 _y = x;,同样执行第 11 行。

注意我们可以在第二个(gdb)提示符下按 Enter 键,它会重新发出之前的语句step,但是为了在本教程中清晰起见,我们通常会重新键入命令。

(gdb) step
11          _y = x;

如您所见,在这种模式下,在每个step命令上,GDB 将打印当前语句及其行号。正如您稍后将在 TUI 模式中看到的那样,您将不会在命令区域中看到该语句。

我们现在“在”_y = x声明;该语句尚未执行。这意味着x 已初始化但未初始化_y。让我们使用print 命令检查那些堆栈/局部变量,p简称:

(gdb) print x
$1 = 42
(gdb) p &x
$2 = (*mut i32) 0x20009fe0
(gdb) p _y
$3 = 536870912
(gdb) p &_y
$4 = (*mut i32) 0x20009fe4

正如预期的那样,x包含值42_y,但是,包含值536870912(?)。这是因为_y尚未初始化,它包含一些垃圾值。

该命令print &x打印变量的地址x。这里有趣的一点是 GDB 输出显示了引用的类型:*mut i32,一个指向i32值的可变指针。另一个有趣的事情是,地址x_y非常接近对方:他们的地址只是4个字节分开。

也可以使用以下info locals命令,而不是一一打印局部变量:

(gdb) info locals
x = 42
_y = 536870912

好的。使用 another step,我们将在loop {}语句之上:

(gdb) step
14          loop {}

并且_y现在应该被初始化。

(gdb) print _y
$5 = 42

如果我们step再次在loop {}语句之上使用,我们会卡住,因为程序永远不会通过该语句。

注意如果您step错误地使用了或任何其他命令并且 GDB 卡住了,您可以通过按 来解除卡住Ctrl+C

如上所述,该disassemble /m命令可用于围绕您当前所在的行反汇编程序。您可能还希望对set print asm-demangle on 名称进行乱码处理,这只需要在调试会话中完成一次。稍后这个和其他命令将被放置在一个初始化文件中,这将简化启动调试会话。

(gdb) set print asm-demangle on
(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h51e7c3daad2af251E:
8       fn main() -> ! {
   0x080001f6 <+0>:     sub     sp, #8
   0x080001f8 <+2>:     movs    r0, #42 ; 0x2a

9           let _y;
10          let x = 42;
   0x080001fa <+4>:     str     r0, [sp, #0]

11          _y = x;
   0x080001fc <+6>:     str     r0, [sp, #4]

12
13          // infinite loop; just so we don't leave this stack frame
14          loop {}
=> 0x080001fe <+8>:     b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>
   0x08000200 <+10>:    b.n     0x8000200 <led_roulette::__cortex_m_rt_main+10>

End of assembler dump.

看到=>左侧的粗箭头了吗?它显示了处理器接下来将执行的指令。

此外,如上所述,如果您要执行step命令 GDB 会卡住,因为它正在执行一条指向自身的分支指令并且永远不会越过它。所以你需要使用 Ctrl+C来重新获得控制权。另一种方法是使用stepi( si) GDB 命令,该命令执行一条 asm 指令,GDB 将打印处理器接下来将执行的语句的地址行号,并且不会卡住。

(gdb) stepi
0x08000194      14          loop {}

(gdb) si
0x08000194      14          loop {}

在我们转向更有趣的事情之前的最后一个技巧。在 GDB 中输入以下命令:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) disassemble /m
Dump of assembler code for function main:
7       #[entry]
   0x080001ec <+0>:     push    {r7, lr}
   0x080001ee <+2>:     mov     r7, sp
=> 0x080001f0 <+4>:     bl      0x80001f6 <led_roulette::__cortex_m_rt_main>
   0x080001f4 <+8>:     udf     #254    ; 0xfe

End of assembler dump.

我们现在回到了开头#[entry]

monitor reset halt将重置微控制器并在程序开始时停止它。continue然后,该命令将让程序自由运行,直到它到达断点,在这种情况下,它是在 处的断点#[entry]

当您错误地跳过您有兴趣检查的程序部分时,此组合非常方便。您可以轻松地将程序的状态回滚到最初的状态。

细则:此reset命令不会清除或触摸 RAM。该内存将保留其上次运行的值。不过,这应该不是问题,除非您的程序行为取决于未初始化变量的值,但这就是未定义行为 (UB) 的定义。

我们完成了这个调试会话。你可以用quit命令结束它。

(gdb) quit
A debugging session is active.

        Inferior 1 [Remote target] will be detached.

Quit anyway? (y or n) y
Detaching from program: $PWD/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.

为了获得更好的调试体验,您可以使用 GDB 的文本用户界面 (TUI)。要进入该模式,请在 GDB shell 中输入以下命令之一:

(gdb) layout src
(gdb) layout asm
(gdb) layout split

注意向 Windows 用户道歉,GNU ARM Embedded Toolchain 附带的 GDB 可能不支持此 TUI 模式:-(

以下是layout split通过执行以下命令设置 a 的示例。正如你所看到的,我们已经放弃了传递--target参数:

$ cargo run
(gdb) target remote :3333
(gdb) load
(gdb) set print asm-demangle on
(gdb) set style sources off
(gdb) break main
(gdb) continue

这是一个以上述命令为-ex参数的命令行,以节省您的一些输入,很快我们将提供一种更简单的方法来执行初始命令集:

cargo run -- -q -ex 'target remote :3333' -ex 'load' -ex 'set print asm-demangle on' -ex 'set style sources off' -ex 'b main' -ex 'c' target/thumbv7em-none-eabihf/debug/led-roulette

结果如下:

现在我们将向下滚动顶部源窗口,以便我们看到整个文件并执行layout split,然后step

然后我们将执行一些info localsandstep的:

(gdb) info locals
(gdb) step
(gdb) info locals
(gdb) step
(gdb) info locals

您可以随时使用以下命令退出 TUI 模式:

(gdb) tui disable

注意如果您不喜欢默认的 GDB CLI,请查看gdb-dashboard。它使用 Python 将默认的 GDB CLI 转换为显示寄存器、源代码视图、程序集视图和其他内容的仪表板。

不过不要关闭 OpenOCD!以后我们会一次又一次地使用它。最好让它继续运行。

The Led and Delay abstractions

现在,我将介绍两个高级抽象,我们将使用它们来实现 LED 轮盘赌应用程序。

辅助 crateaux5公开了一个名为 的初始化函数init。调用此函数时,返回两个打包在元组中的Delay值:一个值和一个LedArray值。

Delay 可用于在指定的毫秒数内阻止您的程序。

LedArray是一个包含 8 个Leds的数组。每个Led代表 F3 板上的一个 LED,并公开两种方法:onoff可分别用于打开或关闭 LED。

让我们通过将起始代码修改为如下所示来尝试这两种抽象:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::{entry, Delay, DelayMs, LedArray, OutputSwitch};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let half_period = 500_u16;

    loop {
        leds[0].on().ok();
        delay.delay_ms(half_period);

        leds[0].off().ok();
        delay.delay_ms(half_period);
    }
}

现在构建它:

cargo build

注意*启动 GDB 会话之前忘记重新构建程序是可能的;这种遗漏会导致非常混乱的调试会话。为了避免这个问题,你可以调用 justcargo run 而不是cargo build; cargo run. 该cargo run命令将构建并*启动调试会话,确保您永远不会忘记重新编译您的程序。

现在我们将运行并重复我们在上一节中所做的闪烁过程,但使用新程序。我会让你输入cargo run这很快就会变得更容易。:)

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...

(gdb) target remote :3333
Remote debugging using :3333
led_roulette::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x52c0 lma 0x8000194
Loading section .rodata, size 0xb50 lma 0x8005454
Start address 0x08000194, load size 24484
Transfer rate: 21 KB/sec, 6121 bytes/write.

(gdb) break main
Breakpoint 1 at 0x8000202: file ~/embedded-discovery/src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline ()
    at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]

(gdb) step
led_roulette::__cortex_m_rt_main () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:9
9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

(gdb)

好的。让我们逐步完成代码。这一次,我们将使用next命令而不是step。不同之处在于该next命令将跳过函数调用而不是进入它们内部。

(gdb) next
11          let half_period = 500_u16;

(gdb) next
13          loop {

(gdb) next
14              leds[0].on().ok();

(gdb) next
15              delay.delay_ms(half_period);

执行该leds[0].on().ok()语句后,您应该看到一个红色 LED,指向北方的 LED 亮起。

让我们继续遍历程序:

(gdb) next
17              leds[0].off().ok();

(gdb) next
18              delay.delay_ms(half_period);

delay_ms调用将阻塞程序半秒,但您可能不会注意到,因为该 next命令也需要一些时间来执行。但是,在跳过leds[0].off() 语句后,您应该会看到红色 LED 熄灭。

你已经可以猜到这个程序做了什么。使用continue命令让它不间断地运行。

(gdb) continue
Continuing.

现在,让我们做一些更有趣的事情。我们将使用 GDB 修改我们程序的行为。

首先,让我们通过点击 来停止无限循环Ctrl+C。你可能最终会在里面的某个地方 Led::onLed::off或者delay_ms

^C
Program received signal SIGINT, Interrupt.
0x08003434 in core::ptr::read_volatile<u32> (src=0xe000e010)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053

在我的例子中,程序停止了它在read_volatile函数内的执行。GDB输出结果显示,一些有趣的信息:core::ptr::read_volatile (src=0xe000e010)。这意味着该函数来自corecrate 并且它是用参数调用的src = 0xe000e010

正如您所知,显示函数参数的一种更明确的方法是使用以下info args 命令:

(gdb) info args
src = 0xe000e010

无论您的程序在哪里停止,您都可以随时查看backtrace命令的输出 (bt简称)以了解它是如何到达那里的:

(gdb) backtrace
#0  0x08003434 in core::ptr::read_volatile<u32> (src=0xe000e010)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053
#1  0x08002d66 in vcell::VolatileCell<u32>::get<u32> (self=0xe000e010) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/vcell-0.1.3/src/lib.rs:33
#2  volatile_register::RW<u32>::read<u32> (self=0xe000e010) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/volatile-register-0.2.0/src/lib.rs:75
#3  cortex_m::peripheral::SYST::has_wrapped (self=0x20009fa4)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/peripheral/syst.rs:136
#4  0x08003004 in stm32f3xx_hal::delay::{{impl}}::delay_us (self=0x20009fa4, us=500000)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:58
#5  0x08002f3e in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:32
#6  0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38
#7  0x0800024c in led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:15
#8  0x08000206 in led_roulette::__cortex_m_rt_main_trampoline () at src/05-led-roulette/src/main.rs:7

backtrace 将打印从当前函数到 main 的函数调用跟踪。

回到我们的话题。为了做我们所追求的,首先,我们必须返回到main函数。我们可以使用finish命令来做到这一点。该命令恢复程序执行并在程序从当前函数返回后立即停止执行。我们将不得不多次调用它。

(gdb) finish
Run till exit from #0  0x08003434 in core::ptr::read_volatile<u32> (src=0xe000e010)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1053
cortex_m::peripheral::SYST::has_wrapped (self=0x20009fa4)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.6.4/src/peripheral/syst.rs:136
136             self.csr.read() & SYST_CSR_COUNTFLAG != 0
Value returned is $1 = 5

(..)

(gdb) finish
Run till exit from #0  0x08002f3e in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:32
0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38
38              self.delay_ms(u32(ms));

(gdb) finish
Run till exit from #0  0x08002f80 in stm32f3xx_hal::delay::{{impl}}::delay_ms (self=0x20009fa4, ms=500)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/stm32f3xx-hal-0.5.0/src/delay.rs:38
0x0800024c in led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:15
15              delay.delay_ms(half_period);

我们回来了main。我们在这里有一个局部变量:half_period

(gdb) print half_period
$3 = 500

现在,我们将使用以下set命令修改此变量:

(gdb) set half_period = 100

(gdb) print half_period
$5 = 100

如果您使用该continue命令再次让程序自由运行,您可能会看到 LED 现在将以更快的速度闪烁,但更有可能的是闪烁率没有改变。发生了什么?

让我们用 停止程序,Ctrl+C然后在 处设置一个断点main:14

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
core::cell::UnsafeCell::get (self=0x20009fa4)
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:1711
1711        pub const fn get(&self) -> *mut T {

然后在main.rs:14和设置一个断点continue

(gdb) break main.rs:14
Breakpoint 2 at 0x8000236: file src/05-led-roulette/src/main.rs, line 14.
(gdb) continue
Continuing.

Breakpoint 2, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:14
14              leds[0].on().ok();

现在打开你的终端窗口,如果可能的话,它大约有 80 行长和 170 个字符。

注意如果你不能打开那么大的终端,没问题你会看到 --Type <RET> for more, q to quit, c to continue without paging--所以只需输入 return 直到你看到(gdb)提示。然后滚动终端窗口以查看结果。

(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17h51e7c3daad2af251E:
8       fn main() -> ! {
   0x08000208 <+0>:     push    {r7, lr}
   0x0800020a <+2>:     mov     r7, sp
   0x0800020c <+4>:     sub     sp, #64 ; 0x40
   0x0800020e <+6>:     add     r0, sp, #32

9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();
   0x08000210 <+8>:     bl      0x8000302 
   0x08000214 <+12>:    b.n     0x8000216 
   0x08000216 <+14>:    add     r0, sp, #32
   0x08000218 <+16>:    add     r1, sp, #4
   0x0800021a <+18>:    ldmia.w r0, {r2, r3, r4, r12, lr}
   0x0800021e <+22>:    stmia.w r1, {r2, r3, r4, r12, lr}
   0x08000222 <+26>:    ldr     r0, [sp, #52]   ; 0x34
   0x08000224 <+28>:    ldr     r1, [sp, #56]   ; 0x38
   0x08000226 <+30>:    str     r1, [sp, #28]
   0x08000228 <+32>:    str     r0, [sp, #24]
   0x0800022a <+34>:    mov.w   r0, #500        ; 0x1f4

10
11          let half_period = 500_u16;
   0x0800022e <+38>:    strh.w  r0, [r7, #-2]

12
13          loop {
   0x08000232 <+42>:    b.n     0x8000234 
   0x08000234 <+44>:    add     r0, sp, #24
   0x08000268 <+96>:    b.n     0x8000234 

14              leds[0].on().ok();
=> 0x08000236 <+46>:    bl      0x80001ec >>>
   0x0800023a <+50>:    b.n     0x800023c 
   0x0800023c <+52>:    bl      0x8000594 ::ok<(),core::convert::Infallible>>
   0x08000240 <+56>:    b.n     0x8000242 
   0x08000242 <+58>:    add     r0, sp, #4
   0x08000244 <+60>:    mov.w   r1, #500        ; 0x1f4

15              delay.delay_ms(half_period);
   0x08000248 <+64>:    bl      0x8002f5c 
   0x0800024c <+68>:    b.n     0x800024e 
   0x0800024e <+70>:    add     r0, sp, #24

16
17              leds[0].off().ok();
   0x08000250 <+72>:    bl      0x800081a >>>
   0x08000254 <+76>:    b.n     0x8000256 
   0x08000256 <+78>:    bl      0x8000594 ::ok<(),core::convert::Infallible>>
   0x0800025a <+82>:    b.n     0x800025c 
   0x0800025c <+84>:    add     r0, sp, #4
   0x0800025e <+86>:    mov.w   r1, #500        ; 0x1f4

18              delay.delay_ms(half_period);
   0x08000262 <+90>:    bl      0x8002f5c 
   0x08000266 <+94>:    b.n     0x8000268 

End of assembler dump.

在上面的转储中,延迟没有改变的原因是因为编译器认识到 half_period 没有改变,而是在delay.delay_ms(half_period);被调用的两个地方 我们看到了mov.w r1, #500。所以改变 的值half_period没有任何作用!

   0x08000244 <+60>:    mov.w   r1, #500        ; 0x1f4

15              delay.delay_ms(half_period);
   0x08000248 <+64>:    bl      0x8002f5c 

(..)

   0x0800025e <+86>:    mov.w   r1, #500        ; 0x1f4

18              delay.delay_ms(half_period);
   0x08000262 <+90>:    bl      0x8002f5c 

该问题的一种解决方案是包装half_period在 a 中Volatile,如下所示。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use volatile::Volatile;
use aux5::{Delay, DelayMs, LedArray, OutputSwitch, entry};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let mut half_period = 500_u16;
    let v_half_period = Volatile::new(&mut half_period);

    loop {
        leds[0].on().ok();
        delay.delay_ms(v_half_period.read());

        leds[0].off().ok();
        delay.delay_ms(v_half_period.read());
    }
}

在该部分编辑Cargo.toml添加。volatile = "0.4.3"``[dependencies]

[dependencies]
aux5 = { path = "auxiliary" }
volatile = "0.4.3"

使用上面的代码,Volatile您现在可以进行更改,half_period并且可以尝试不同的值。这是命令列表和解释;# xxxx展示。

$ cargo run --target thumbv7em-none-eabihf   # Compile and load the program into gdb
(gdb) target remote :3333           # Connect to STM32F3DISCOVERY board from PC
(gdb) load                          # Flash program
(gdb) break main.rs:16              # Set breakpoint 1 at top of loop
(gdb) continue                      # Continue, will stop at main.rs:16
(gdb) disable 1                     # Disable breakpoint 1
(gdb) set print asm-demangle on     # Enable asm-demangle
(gdb) disassemble /m                # Disassemble main function
(gdb) continue                      # Led blinking on for 1/2 sec then off 1/2 sec
^C                                  # Stop with Ctrl+C
(gdb) enable 1                      # Enable breakpiont 1
(gdb) continue                      # Continue, will stop at main.rs:16
(gdb) print half_period             # Print half_period result is 500
(gdb) set half_period = 2000        # Set half_period to 2000ms
(gdb) print half_period             # Print half_period and result is 2000
(gdb) disable 1                     # Disable breakpoint 1
(gdb) continue                      # Led blinking on for 2 secs then off 2 sec
^C                                  # Stop with Ctrl+C
(gdb) quit                          # Quit gdb

关键更改位于源代码的第 13、17 和 20 行,您可以在反汇编中看到。在 13 我们创建v_half_period然后 read()它的值在第 17 和 20 行。这意味着当我们set half_period = 2000 现在 LED 将打开 2 秒然后关闭 2 秒。

$ cargo run --target thumbv7em-none-eabihf
   Compiling led-roulette v0.2.0 (~/embedded-discovery/src/05-led-roulette)
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
     Running `arm-none-eabi-gdb -q ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...

(gdb) target remote :3333
Remote debugging using :3333
led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) load
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x5258 lma 0x8000194
Loading section .rodata, size 0xbd8 lma 0x80053ec
Start address 0x08000194, load size 24516
Transfer rate: 21 KB/sec, 6129 bytes/write.

(gdb) break main.rs:16
Breakpoint 1 at 0x8000246: file src/05-led-roulette/src/main.rs, line 16.
Note: automatically using hardware breakpoints for read-only addresses.

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) disable 1

(gdb) set print asm-demangle on

(gdb) disassemble /m
Dump of assembler code for function _ZN12led_roulette18__cortex_m_rt_main17he1f2bc7990b13731E:
9       fn main() -> ! {
   0x0800020e <+0>:     push    {r7, lr}
   0x08000210 <+2>:     mov     r7, sp
   0x08000212 <+4>:     sub     sp, #72 ; 0x48
   0x08000214 <+6>:     add     r0, sp, #36     ; 0x24

10          let (mut delay, mut leds): (Delay, LedArray) = aux5::init();
   0x08000216 <+8>:     bl      0x800036a 
   0x0800021a <+12>:    b.n     0x800021c 
   0x0800021c <+14>:    add     r0, sp, #36     ; 0x24
   0x0800021e <+16>:    add     r1, sp, #8
   0x08000220 <+18>:    ldmia.w r0, {r2, r3, r4, r12, lr}
   0x08000224 <+22>:    stmia.w r1, {r2, r3, r4, r12, lr}
   0x08000228 <+26>:    ldr     r0, [sp, #56]   ; 0x38
   0x0800022a <+28>:    ldr     r1, [sp, #60]   ; 0x3c
   0x0800022c <+30>:    str     r1, [sp, #32]
   0x0800022e <+32>:    str     r0, [sp, #28]
   0x08000230 <+34>:    mov.w   r0, #500        ; 0x1f4

11
12          let mut half_period = 500_u16;
   0x08000234 <+38>:    strh.w  r0, [r7, #-6]
   0x08000238 <+42>:    subs    r0, r7, #6

13          let v_half_period = Volatile::new(&mut half_period);
   0x0800023a <+44>:    bl      0x800033e ::new<&mut u16>>
   0x0800023e <+48>:    str     r0, [sp, #68]   ; 0x44
   0x08000240 <+50>:    b.n     0x8000242 

14
15          loop {
   0x08000242 <+52>:    b.n     0x8000244 
   0x08000244 <+54>:    add     r0, sp, #28
   0x08000288 <+122>:   b.n     0x8000244 

16              leds[0].on().ok();
=> 0x08000246 <+56>:    bl      0x800032c >>>
   0x0800024a <+60>:    b.n     0x800024c 
   0x0800024c <+62>:    bl      0x80005fc ::ok<(),core::convert::Infallible>>
   0x08000250 <+66>:    b.n     0x8000252 
   0x08000252 <+68>:    add     r0, sp, #68     ; 0x44

17              delay.delay_ms(v_half_period.read());
   0x08000254 <+70>:    bl      0x800034a ::read<&mut u16,u16,volatile::access::ReadWrite>>
   0x08000258 <+74>:    str     r0, [sp, #4]
   0x0800025a <+76>:    b.n     0x800025c 
   0x0800025c <+78>:    add     r0, sp, #8
   0x0800025e <+80>:    ldr     r1, [sp, #4]
   0x08000260 <+82>:    bl      0x8002fc4 
   0x08000264 <+86>:    b.n     0x8000266 
   0x08000266 <+88>:    add     r0, sp, #28

18
19              leds[0].off().ok();
   0x08000268 <+90>:    bl      0x8000882 >>>
   0x0800026c <+94>:    b.n     0x800026e 
   0x0800026e <+96>:    bl      0x80005fc ::ok<(),core::convert::Infallible>>
   0x08000272 <+100>:   b.n     0x8000274 
   0x08000274 <+102>:   add     r0, sp, #68     ; 0x44

20              delay.delay_ms(v_half_period.read());
   0x08000276 <+104>:   bl      0x800034a ::read<&mut u16,u16,volatile::access::ReadWrite>>
   0x0800027a <+108>:   str     r0, [sp, #0]
   0x0800027c <+110>:   b.n     0x800027e 
   0x0800027e <+112>:   add     r0, sp, #8
   0x08000280 <+114>:   ldr     r1, [sp, #0]
   0x08000282 <+116>:   bl      0x8002fc4 
   0x08000286 <+120>:   b.n     0x8000288 

End of assembler dump.

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x080037b2 in core::cell::UnsafeCell::get (self=0x20009fa0) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/cell.rs:1716
1716        }

(gdb) enable 1

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) print half_period
$2 = 500

(gdb) disable 1

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x08003498 in core::ptr::read_volatile (src=0xe000e010) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1052
1052        unsafe { intrinsics::volatile_load(src) }

(gdb) enable 1

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::__cortex_m_rt_main () at src/05-led-roulette/src/main.rs:16
16              leds[0].on().ok();

(gdb) print half_period
$3 = 500

(gdb) set half_period = 2000

(gdb) print half_period
$4 = 2000

(gdb) disable 1

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x0800348e in core::ptr::read_volatile (src=0xe000e010) at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1046
1046    pub unsafe fn read_volatile(src: *const T) -> T {

(gdb) q
Detaching from program: ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

题!如果您开始降低 的值会发生什么half_period?在什么值下 half_period您不能再看到 LED 闪烁?

The challenge

你现在装备齐全,可以迎接挑战!您的任务是实现我在本章开头向您展示的应用程序。

这里又是GIF:

此外,这可能有助于:

这是时序图。它指示在任何给定时刻哪个 LED 亮起,以及每个 LED 亮起的时间。在 X 轴上,我们有以毫秒为单位的时间。时序图显示了一个周期。此模式将每 800 毫秒重复一次。Y 轴用基点标记每个 LED:北、东等。作为挑战的一部分,您必须弄清楚Leds阵列中的每个元素如何映射到这些基点(提示:)cargo doc --open ;-)

在你尝试这个挑战之前,让我给你一个额外的提示。我们的 GDB 会话总是在开始时输入相同的命令。我们可以使用一个.gdb文件在 GDB 启动后立即执行一些命令。通过这种方式,您可以省去在每个 GDB 会话中手动输入它们的工作。

事实证明,我们已经创建了../openocd.gdb,您可以看到它的功能与我们在上一节中所做的差不多,另外还有一些其他命令。查看评论以获取更多信息:

$ cat ../openocd.gdb
# Connect to gdb remote server
target remote :3333

# Load will flash the code
load

# Eanble demangling asm names on disassembly
set print asm-demangle on

# Enable pretty printing
set print pretty on

# Disable style sources as the default colors can be hard to read
set style sources off

# Initialize monitoring so iprintln! macro output
# is sent from the itm port to itm.txt
monitor tpiu config internal itm.txt uart off 8000000

# Turn on the itm port
monitor itm port 0 on

# Set a breakpoint at main, aka entry
break main

# Set a breakpiont at DefaultHandler
break DefaultHandler

# Set a breakpiont at HardFault
break HardFault

# Continue running and until we hit the main breakpoint
continue

# Step from the trampoline code in entry into main
step

现在我们需要修改../.cargo/config.toml文件来执行../openocd.gdb

nano ../.cargo/config.toml

编辑您的runner命令-x ../openocd.gdb。假设您使用arm-none-eabi-gdb的差异是:

~/embedded-discovery/src/05-led-roulette
$ git diff ../.cargo/config.toml
diff --git a/src/.cargo/config.toml b/src/.cargo/config.toml
index ddff17f..02ac952 100644
--- a/src/.cargo/config.toml
+++ b/src/.cargo/config.toml
@@ -1,5 +1,5 @@
 [target.thumbv7em-none-eabihf]
-runner = "arm-none-eabi-gdb -q"
+runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
 # runner = "gdb-multiarch -q"
 # runner = "gdb -q"
 rustflags = [

的全部内容../.cargo/config.toml,再次假设arm-none-eabi-gdb,是:

[target.thumbv7em-none-eabihf]
runner = "arm-none-eabi-gdb -q -x ../openocd.gdb"
# runner = "gdb-multiarch -q"
# runner = "gdb -q"
rustflags = [
  "-C", "link-arg=-Tlink.x",
]

[build]
target = "thumbv7em-none-eabihf"

有了它,您现在可以使用一个简单的cargo run命令来构建代码的 ARM 版本并运行gdb会话。该gdb会议会自动闪烁程序并跳转到的开始main,因为它step的通过进入蹦床:

cargo run
~/embedded-discovery/src/05-led-roulette (Update-05-led-roulette-WIP)
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q -x openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/led-roulette...
led_roulette::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x52c0 lma 0x8000194
Loading section .rodata, size 0xb50 lma 0x8005454
Start address 0x08000194, load size 24484
Transfer rate: 21 KB/sec, 6121 bytes/write.
Breakpoint 1 at 0x8000202: file ~/embedded-discovery/src/05-led-roulette/src/main.rs, line 7.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, led_roulette::__cortex_m_rt_main_trampoline ()
    at ~/embedded-discovery/src/05-led-roulette/src/main.rs:7
7       #[entry]
led_roulette::__cortex_m_rt_main () at ~/embedded-discovery/src/05-led-roulette/src/main.rs:9
9           let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

My solution

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux5::{Delay, DelayMs, LedArray, OutputSwitch, entry};

#[entry]
fn main() -> ! {
    let (mut delay, mut leds): (Delay, LedArray) = aux5::init();

    let ms = 50_u8;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().ok();
            delay.delay_ms(ms);
            leds[curr].off().ok();
            delay.delay_ms(ms);
        }
    }
}

还有一件事!检查您的解决方案在“发布”模式下编译时是否也有效:

$ cargo build --target thumbv7em-none-eabihf --release

您可以使用以下gdb命令对其进行测试:

$ # or, you could simply call `cargo run --target thumbv7em-none-eabihf --release`
$ arm-none-eabi-gdb target/thumbv7em-none-eabihf/release/led-roulette
$ #                                              ~~~~~~~

二进制大小是我们应该时刻关注的东西!你的解决方案有多大?您可以使用size发布二进制文件上的命令来检查:

$ # equivalent to size target/thumbv7em-none-eabihf/debug/led-roulette
$ cargo size --target thumbv7em-none-eabihf --bin led-roulette -- -A
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
led-roulette  :
section               size        addr
.vector_table          404   0x8000000
.text                21144   0x8000194
.rodata               3144   0x800542c
.data                    0  0x20000000
.bss                     4  0x20000000
.uninit                  0  0x20000004
.debug_abbrev        19160         0x0
.debug_info         471239         0x0
.debug_aranges       18376         0x0
.debug_ranges       102536         0x0
.debug_str          508618         0x0
.debug_pubnames      76975         0x0
.debug_pubtypes     112797         0x0
.ARM.attributes         58         0x0
.debug_frame         55848         0x0
.debug_line         282067         0x0
.debug_loc             845         0x0
.comment               147         0x0
Total              1673362


$ cargo size --target thumbv7em-none-eabihf --bin led-roulette --release -- -A
    Finished release [optimized + debuginfo] target(s) in 0.03s
led-roulette  :
section              size        addr
.vector_table         404   0x8000000
.text                5380   0x8000194
.rodata               564   0x8001698
.data                   0  0x20000000
.bss                    4  0x20000000
.uninit                 0  0x20000004
.debug_loc           9994         0x0
.debug_abbrev        1821         0x0
.debug_info         74974         0x0
.debug_aranges        600         0x0
.debug_ranges        6848         0x0
.debug_str          52828         0x0
.debug_pubnames     20821         0x0
.debug_pubtypes     18891         0x0
.ARM.attributes        58         0x0
.debug_frame         1088         0x0
.debug_line         15307         0x0
.comment               19         0x0
Total              209601

注意Cargo 项目已配置为使用 LTO 构建发布二进制文件。

知道如何读取这个输出吗?该text部分包含程序指令。就我而言,它大约为 5.25KB。另一方面,databss部分包含在 RAM 中静态分配的static变量(变量)。static正在使用一个变量aux5::init;这就是为什么它显示 4 个字节的bss.

最后一件事!我们一直在 GDB 中运行我们的程序,但我们的程序根本不依赖于 GDB。您可以确认这是关闭 GDB 和 OpenOCD,然后通过按下板上的黑色按钮重置板。LED 轮盘应用程序将在没有 GDB 干预的情况下运行。

Hello, world!

注意事项 STM32F3DISCOVERY 上的“焊桥”SB10(参见电路板背面)是使用 ITM 和iprint!如下所示的宏所必需的,默认情况下焊接(参见用户手册的第 21 页)。(更准确地说:这实际上取决于电路板版本。如果您有旧版本的电路板,如旧用户手册所述,SB10 已焊接。检查您的电路板以确定是否需要修复它。)

TL;DR您有两种选择来解决这个问题:焊接焊桥 SB10 或在 SWO 和 PB3 之间连接母对母跳线,如下图所示。


在我们开始做低级的东西之前,再多一点有用的魔法。

闪烁 LED 就像嵌入式世界的“你好,世界”。

但在本节中,我们将运行一个适当的“Hello, world”程序,将内容打印到您的计算机控制台。

进入06-hello-world目录。里面有一些入门代码:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux6::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let mut itm = aux6::init();

    iprintln!(&mut itm.stim[0], "Hello, world!");

    loop {}
}

iprintln宏将格式化消息,并将其输出到微控制器的ITM。ITM 代表 Instrumentation Trace Macrocell,它是一种基于 SWD(串行线调试)的通信协议,可用于将消息从微控制器发送到调试主机。这种通信只是一种方式:调试主机不能向微控制器发送数据。

管理调试会话的 OpenOCD 可以接收通过此 ITM通道发送的数据并将其重定向到文件。

ITM 协议处理(您可以将它们视为以太网帧)。每个帧都有一个报头和一个可变长度的有效载荷。OpenOCD 将接收这些帧并将它们直接写入文件而不进行解析。所以,如果微控制器发送字符串“Hello, world!” 使用 iprintln宏,OpenOCD 的输出文件不会完全包含该字符串。

要检索原始字符串,必须解析 OpenOCD 的输出文件。我们将使用该 itmdump程序在新数据到达时执行解析。

您应该已经itmdump在安装章节中安装了该程序。

在新终端中,/tmp如果您使用的是 *nix 操作系统,请在目录内运行此命令%TEMP%,如果您运行的是 Windows ,则从目录内运行此命令。这应该与您运行 OpenOCD 的目录相同。

注意itmdumpopenocd都从同一目录运行非常重要!

$ # itmdump terminal

$ # *nix
$ cd /tmp && touch itm.txt

$ # Windows
$ cd %TEMP% && type nul >> itm.txt

$ # both
$ itmdump -F -f itm.txt

此命令将像itmdump现在一样阻止itm.txt文件。保持这个终端打开。

确保 STM32F3DISCOVERY 板已连接到您的计算机。从/tmp目录中打开另一个终端(在 Windows 上%TEMP%)以启动 OpenOCD。

好吧。现在,让我们构建入门代码并将其闪存到微控制器中。

我们现在将构建并运行应用程序cargo run. 并使用next. 因为openocd.gdb包含OpenOCD中的monitor命令openocd.gdb会将 ITM 输出重定向到 itm.txt 并将itmdump其写入其终端窗口。此外,它设置断点并逐步通过我们在第一个可执行语句处的蹦床fn main()

~/embedded-discovery/src/06-hello-world
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `arm-none-eabi-gdb -q -x ../openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world...
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:14
14          loop {}
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x2828 lma 0x8000194
Loading section .rodata, size 0x638 lma 0x80029bc
Start address 0x08000194, load size 12276
Transfer rate: 18 KB/sec, 4092 bytes/write.
Breakpoint 1 at 0x80001f0: file ~/embedded-discovery/src/06-hello-world/src/main.rs, line 8.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x800092a: file /home/wink/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 570.
Breakpoint 3 at 0x80029a8: file /home/wink/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 560.

Breakpoint 1, hello_world::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/06-hello-world/src/main.rs:8
8       #[entry]
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          let mut itm = aux6::init();

(gdb)

现在发出一个next命令,该命令将aux6::init()在 中的下一个可执行语句处执行并停止main.rs,它将我们定位在第 12 行:

(gdb) next
12        iprintln!(&mut itm.stim[0], "Hello, world!");

然后发出另一个next将执行第 12 行的命令,执行iprintln并在第 14 行停止:

(gdb) next
14        loop {}

现在既然iprintln已经执行,itmdump 终端窗口上的输出应该是Hello, world!字符串:

$ itmdump -F -f itm.txt
(...)
Hello, world!

很棒,对吧?iprintln在接下来的部分中随意用作日志记录工具。

panic!

panic!宏也将其输出到ITM!

main函数更改为如下所示:

#[entry]
fn main() -> ! {
    panic!("Hello, world!");
}

在运行另一个建议之前,我发现退出 gdb 时必须确认很不方便。将以下文件添加到您的主目录中,~/.gdbinit以便它立即退出:

$ cat ~/.gdbinit
define hook-quit
  set confirm off
end

好的,现在使用cargo run,它停在第一行fn main()

$ cargo run
   Compiling hello-world v0.2.0 (~/embedded-discovery/src/06-hello-world)
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `arm-none-eabi-gdb -q -x ../openocd.gdb ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world`
Reading symbols from ~/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world...
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          panic!("Hello, world!");
Loading section .vector_table, size 0x194 lma 0x8000000
Loading section .text, size 0x20fc lma 0x8000194
Loading section .rodata, size 0x554 lma 0x8002290
Start address 0x08000194, load size 10212
Transfer rate: 17 KB/sec, 3404 bytes/write.
Breakpoint 1 at 0x80001f0: file ~/embedded-discovery/src/06-hello-world/src/main.rs, line 8.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 2 at 0x8000222: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 570.
Breakpoint 3 at 0x800227a: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs, line 560.

Breakpoint 1, hello_world::__cortex_m_rt_main_trampoline () at ~/embedded-discovery/src/06-hello-world/src/main.rs:8
8       #[entry]
hello_world::__cortex_m_rt_main () at ~/embedded-discovery/src/06-hello-world/src/main.rs:10
10          panic!("Hello, world!");
(gdb)

我们将使用简短的命令名称来保存输入,c然后输入EnterReturn键:

(gdb) c
Continuing.

如果一切顺利,您将在itmdump终端中看到一些新的输出。

$ # itmdump terminal
(..)
panicked at 'Hello, world!', src/06-hello-world/src/main.rs:10:5

然后键入Ctrl-cwhich 在运行时跳出循环:

^C
Program received signal SIGINT, Interrupt.
0x0800115c in panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:57
57            atomic::compiler_fence(Ordering::SeqCst);

最终,panic!这只是另一个函数调用,因此您可以看到它留下了函数调用的痕迹。这允许您使用backtrace或只是bt查看导致恐慌的调用堆栈:

(gdb) bt
#0  panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47
#1  0x080005c2 in core::panicking::panic_fmt () at library/core/src/panicking.rs:92
#2  0x0800055a in core::panicking::panic () at library/core/src/panicking.rs:50
#3  0x08000210 in hello_world::__cortex_m_rt_main () at src/06-hello-world/src/main.rs:10
#4  0x080001f4 in hello_world::__cortex_m_rt_main_trampoline () at src/06-hello-world/src/main.rs:8

我们可以做的另一件事是在记录之前捕捉恐慌。所以我们会做几件事;重置到开头,禁用断点 1,在 处设置一个新断点rust_begin_unwind,列出断点,然后继续:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

(gdb) disable 1

(gdb) break rust_begin_unwind 
Breakpoint 4 at 0x800106c: file ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs, line 47.

(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x080001f0 in hello_world::__cortex_m_rt_main_trampoline 
                                           at ~/prgs/rust/tutorial/embedded-discovery/src/06-hello-world/src/main.rs:8
        breakpoint already hit 1 time
2       breakpoint     keep y   0x08000222 in cortex_m_rt::DefaultHandler_ 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:570
3       breakpoint     keep y   0x0800227a in cortex_m_rt::HardFault_ 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:560
4       breakpoint     keep y   0x0800106c in panic_itm::panic 
                                           at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47

(gdb) c
Continuing.

Breakpoint 4, panic_itm::panic (info=0x20009fa0) at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-itm-0.4.2/src/lib.rs:47
47          interrupt::disable();

您会注意到这次itmdump控制台上没有打印任何内容。如果您使用恢复程序,continue则将打印一个新行。

在后面的部分中,我们将研究其他更简单的通信协议。

最后,输入q退出命令,它立即退出而不要求确认:

(gdb) q
Detaching from program: ~/prgs/rust/tutorial/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

作为一个更短的序列,您可以键入Ctrl-d,这消除了一次击键!

注意在这种情况下,(gdb)提示被覆盖quit)

quit)
Detaching from program: ~/prgs/rust/tutorial/embedded-discovery/target/thumbv7em-none-eabihf/debug/hello-world, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]

Registers

是时候探索LedAPI 在幕后做了什么了。

简而言之,它只是写入一些特殊的内存区域。进入07-registers目录,让我们逐句运行启动代码。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux7::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        // A magic address!
        const GPIOE_BSRR: u32 = 0x48001018;

        // Turn on the "North" LED (red)
        *(GPIOE_BSRR as *mut u32) = 1 << 9;

        // Turn on the "East" LED (green)
        *(GPIOE_BSRR as *mut u32) = 1 << 11;

        // Turn off the "North" LED
        *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);

        // Turn off the "East" LED
        *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
    }

    loop {}
}

这是什么魔法?

地址0x48001018指向一个寄存器。寄存器是控制外围设备的特殊内存区域。外围设备是一种电子元件,它紧邻微控制器封装中的处理器,为处理器提供额外的功能。毕竟,处理器本身只能做数学和逻辑。

此特定寄存器控制通用输入/输出 (GPIO)引脚(GPIO外设)并可用于将这些引脚中的每一个驱动低电平高电平

旁白:LED、数字输出和电压电平

驾驶?别针?低的?高的?

引脚是电触点。我们的微控制器有几个,其中一些连接到 LED。LED,即发光二极管,只有在施加具有特定极性的电压时才会发光。

幸运的是,微控制器的引脚以正确的极性连接到 LED。我们所要做的就是通过引脚输出一些非零电压来打开 LED。连接到 LED 的引脚配置为数字输出,只能输出两种不同的电压电平:“低”,0 伏,或“高”,3 伏。“高”(电压)电平将打开 LED,而“低”(电压)电平将关闭它。

这些“低”和“高”状态直接映射到数字逻辑的概念。“低”是0false ,“高”是1true。这就是该引脚配置被称为数字输出的原因。

RTRM: Reading The Reference Manual

我提到微控制器有几个引脚。为方便起见,这些引脚被分组 为 16 个引脚的端口。每个端口都以字母命名:端口 A、端口 B 等,每个端口内的引脚以 0 到 15 的数字命名。

我们必须找出的第一件事是哪个引脚连接到哪个 LED。此信息在 STM32F3DISCOVERY用户手册中(您下载了副本,对吗?)。在这个特定部分:

第 6.4 节 LED - 第 18 页

手册上说:

  • LD3,北 LED,连接到引脚PE9PE9是以下的缩写形式:端口 E 上的引脚 9。
  • LD7,东 LED,连接到引脚PE11

到目前为止,我们知道我们要更改引脚 PE9 和 PE11 的状态以打开/关闭北/东 LED。这些引脚是端口 E 的一部分,因此我们必须处理GPIOE 外围设备。

每个外设都有一个与之关联的寄存器。寄存器块是分配在连续内存中的寄存器集合。寄存器块开始的地址称为基地址。我们需要弄清楚GPIOE外围设备的基地址是什么。该信息位于微控制器参考手册的以下部分:

第 3.2.2 节内存映射和寄存器边界地址 - 第 51 页

该表表示GPIOE寄存器块的基地址是0x4800_1000

每个外设在文档中也有自己的部分。这些部分中的每一个都以外设寄存器块包含的寄存器表结束。对于GPIO外围设备系列,该表位于:

第 11.4.12 节 GPIO 寄存器映射 - 第 243 页

“BSRR”是我们将用于设置/复位的寄存器。它的偏移值是“GPIOE”基地址的“0x18”。我们可以在参考手册中查找 BSRR。GPIO 寄存器 -> GPIO 端口位设置/复位寄存器 (GPIOx_BSRR)。

现在我们需要跳转到该特定寄存器的文档。这是上面的几页:

第 11.4.7 节 GPIO 端口位设置/复位寄存器 (GPIOx_BSRR) - 第 240 页

最后!

这是我们要写入的寄存器。文档说了一些有趣的事情。首先,这个寄存器是只写的……所以让我们尝试读取它的值:-)

我们将使用GDB的examine命令:x

(gdb) next
16              *(GPIOE_BSRR as *mut u32) = 1 << 9;

(gdb) x 0x48001018
0x48001018:     0x00000000

(gdb) # the next command will turn the North LED on
(gdb) next
19              *(GPIOE_BSRR as *mut u32) = 1 << 11;

(gdb) x 0x48001018
0x48001018:     0x00000000

读取寄存器返回0。这与文档所说的相符。

文档说的另一件事是位 0 到 15 可用于设置相应的引脚。即位 0 设置引脚 0。这里,设置意味着在引脚上输出值。

该文档还说,第 16 到 31 位可用于重置相应的引脚。在这种情况下,第 16 位重置引脚编号 0。正如您可能猜到的,重置意味着在引脚上输出一个低值

将这些信息与我们的程序相关联,似乎都一致:

  • 1 << 9( BS9 = 1)BSRR 设置PE9 。那会使北方LED
  • 1 << 11( BS11 = 1)BSRR设置PE11 。那会使东LED
  • 1 << 25( BR9 = 1)BSRR设置PE9 。这将关闭北方 LED 。
  • 最后,将1 << 27( BR11 = 1)写入BSRR设置为PE11 low。这会关闭东 LED 。

(mis)Optimization

对寄存器的读/写非常特殊。我什至敢说它们是副作用的体现。在前面的例子中,我们将四个不同的值写入同一个寄存器。如果你不知道地址是一个寄存器,你可能已经简化了逻辑,只是将最终值1 << (11 + 16)写入寄存器。

实际上,编译器的后端/优化器 LLVM 不知道我们正在处理寄存器,并且会合并写入从而改变我们程序的行为。让我们快速检查一下。

$ cargo run --release
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:9
9           aux7::init();

(gdb) next
25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);

(gdb) disassemble /m
Dump of assembler code for function _ZN9registers18__cortex_m_rt_main17h45b1ef53e18aa8d0E:
8       fn main() -> ! {
   0x08000248 <+0>:     push    {r7, lr}
   0x0800024a <+2>:     mov     r7, sp

9           aux7::init();
   0x0800024c <+4>:     bl      0x8000260 
   0x08000250 <+8>:     movw    r0, #4120       ; 0x1018
   0x08000254 <+12>:    mov.w   r1, #134217728  ; 0x8000000
   0x08000258 <+16>:    movt    r0, #18432      ; 0x4800

10
11          unsafe {
12              // A magic address!
13              const GPIOE_BSRR: u32 = 0x48001018;
14
15              // Turn on the "North" LED (red)
16              *(GPIOE_BSRR as *mut u32) = 1 << 9;
17
18              // Turn on the "East" LED (green)
19              *(GPIOE_BSRR as *mut u32) = 1 << 11;
20
21              // Turn off the "North" LED
22              *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
23
24              // Turn off the "East" LED
25              *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
=> 0x0800025c <+20>:    str     r1, [r0, #0]
   0x0800025e <+22>:    b.n     0x800025e 

End of assembler dump.

这次 LED 的状态没有改变!该str指令是将值写入寄存器的指令。我们的调试(未优化)程序有四个,每次写入寄存器一个,但发布(优化)程序只有一个。

我们可以检查使用objdump并将输出捕获到out.asm

# same as cargo objdump -- -d --no-show-raw-insn --print-imm-hex --source target/thumbv7em-none-eabihf/debug/registers
cargo objdump --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > debug.txt

然后检查debug.txt查找main,我们看到4str条指令:

080001ec <main>:
; #[entry]
 80001ec:       push    {r7, lr}
 80001ee:       mov     r7, sp
 80001f0:       bl      #0x2
 80001f4:       trap

080001f6 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2>:
; fn main() -> ! {
 80001f6:       push    {r7, lr}
 80001f8:       mov     r7, sp
;     aux7::init();
 80001fa:       bl      #0x3e
 80001fe:       b       #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0xa>
;         *(GPIOE_BSRR as *mut u32) = 1 << 9;
 8000200:       movw    r0, #0x2640
 8000204:       movt    r0, #0x800
 8000208:       ldr     r0, [r0]
 800020a:       movw    r1, #0x1018
 800020e:       movt    r1, #0x4800
 8000212:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << 11;
 8000214:       movw    r0, #0x2648
 8000218:       movt    r0, #0x800
 800021c:       ldr     r0, [r0]
 800021e:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << (9 + 16);
 8000220:       movw    r0, #0x2650
 8000224:       movt    r0, #0x800
 8000228:       ldr     r0, [r0]
 800022a:       str     r0, [r1]
;         *(GPIOE_BSRR as *mut u32) = 1 << (11 + 16);
 800022c:       movw    r0, #0x2638
 8000230:       movt    r0, #0x800
 8000234:       ldr     r0, [r0]
 8000236:       str     r0, [r1]
;     loop {}
 8000238:       b       #-0x2 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
 800023a:       b       #-0x4 <registers::__cortex_m_rt_main::hc2e3436fa38cd6f2+0x44>
 (..)

我们如何防止 LLVM 错误优化我们的程序?我们使用volatile操作而不是普通的读/写:

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::entry;

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        // A magic address!
        const GPIOE_BSRR: u32 = 0x48001018;

        // Turn on the "North" LED (red)
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);

        // Turn on the "East" LED (green)
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);

        // Turn off the "North" LED
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));

        // Turn off the "East" LED
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
    }

    loop {}
}

release.txt使用 with--release模式生成。

cargo objdump --release --bin registers -- -d --no-show-raw-insn --print-imm-hex --source > release.txt

现在找到main例程release.txt,我们看到了 4str条指令。

0800023e <main>:
; #[entry]
 800023e:       push    {r7, lr}
 8000240:       mov     r7, sp
 8000242:       bl      #0x2
 8000246:       trap

08000248 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0>:
; fn main() -> ! {
 8000248:       push    {r7, lr}
 800024a:       mov     r7, sp
;     aux7::init();
 800024c:       bl      #0x22
 8000250:       movw    r0, #0x1018
 8000254:       mov.w   r1, #0x200
 8000258:       movt    r0, #0x4800
;         intrinsics::volatile_store(dst, src);
 800025c:       str     r1, [r0]
 800025e:       mov.w   r1, #0x800
 8000262:       str     r1, [r0]
 8000264:       mov.w   r1, #0x2000000
 8000268:       str     r1, [r0]
 800026a:       mov.w   r1, #0x8000000
 800026e:       str     r1, [r0]
 8000270:       b       #-0x4 <registers::__cortex_m_rt_main::h45b1ef53e18aa8d0+0x28>
 (..)

我们看到四个写(str指令)被保留了下来。如果您使用它运行它, gdb您还会看到我们得到了预期的行为。

注意:最后一个next将无限执行loop {},用于Ctrl-c返回(gdb)提示。

$ cargo run --release
(..)

Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:9
9       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:11
11          aux7::init();

(gdb) next
18              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);

(gdb) next
21              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);

(gdb) next
24              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));

(gdb) next
27              ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));

(gdb) next
^C
Program received signal SIGINT, Interrupt.
0x08000270 in registers::__cortex_m_rt_main ()
    at ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:1124
1124            intrinsics::volatile_store(dst, src);
(gdb) 

0xBAAAAAAD address

并非所有外围存储器都可以访问。看看这个程序。

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    aux7::init();

    unsafe {
        ptr::read_volatile(0x4800_1800 as *const u32);
    }

    loop {}
}

这个地址与GPIOE_BSRR我们之前使用的地址很接近,但是这个地址是无效的。在这个地址没有寄存器的意义上是无效的。

现在,让我们尝试一下。

$ cargo run
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:9
9       #[entry]

(gdb) continue
Continuing.

Breakpoint 3, cortex_m_rt::HardFault_ (ef=0x20009fb0)
    at ~/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-rt-0.6.13/src/lib.rs:560
560         loop {

(gdb)

我们试图做一个无效的操作,读取不存在的内存,所以处理器引发了一个 异常,一个硬件异常。

在大多数情况下,当处理器尝试执行无效操作时会引发异常。异常会破坏程序的正常流程并强制处理器执行异常处理程序,它只是一个函数/子例程。

有不同类型的例外。每种异常由不同的条件引发,每种异常由不同的异常处理程序处理。

aux7箱子依赖于cortex-m-rt它定义了默认箱 硬故障处理程序,命名HardFault,处理该“无效的内存地址”异常。openocd.gdb在 上放置一个断点HardFault;这就是调试器在执行异常处理程序时停止程序的原因。我们可以从调试器中获取有关异常的更多信息。让我们来看看:

(gdb) list
555     #[allow(unused_variables)]
556     #[doc(hidden)]
557     #[link_section = ".HardFault.default"]
558     #[no_mangle]
559     pub unsafe extern "C" fn HardFault_(ef: &ExceptionFrame) -> ! {
560         loop {
561             // add some side effect to prevent this from turning into a UDF instruction
562             // see rust-lang/rust#28728 for details
563             atomic::compiler_fence(Ordering::SeqCst);
564         }

ef是发生异常之前程序状态的快照。让我们检查一下:

(gdb) print/x *ef
$1 = cortex_m_rt::ExceptionFrame {
  r0: 0x48001800,
  r1: 0x80036b0,
  r2: 0x1,
  r3: 0x80000000,
  r12: 0xb,
  lr: 0x800020d,
  pc: 0x8001750,
  xpsr: 0xa1000200
}

这里有几个字段,但最重要的一个是pc程序计数器寄存器。该寄存器中的地址指向产生异常的指令。让我们围绕坏指令反汇编程序。

(gdb) disassemble /m ef.pc
Dump of assembler code for function core::ptr::read_volatile<u32>:
1046    pub unsafe fn read_volatile<T>(src: *const T) -> T {
   0x0800174c <+0>:     sub     sp, #12
   0x0800174e <+2>:     str     r0, [sp, #4]

1047        if cfg!(debug_assertions) && !is_aligned_and_not_null(src) {
1048            // Not panicking to keep codegen impact smaller.
1049            abort();
1050        }
1051        // SAFETY: the caller must uphold the safety contract for `volatile_load`.
1052        unsafe { intrinsics::volatile_load(src) }
   0x08001750 <+4>:     ldr     r0, [r0, #0]
   0x08001752 <+6>:     str     r0, [sp, #8]
   0x08001754 <+8>:     ldr     r0, [sp, #8]
   0x08001756 <+10>:    str     r0, [sp, #0]
   0x08001758 <+12>:    b.n     0x800175a <core::ptr::read_volatile<u32>+14>

1053    }
   0x0800175a <+14>:    ldr     r0, [sp, #0]
   0x0800175c <+16>:    add     sp, #12
   0x0800175e <+18>:    bx      lr

End of assembler dump.

异常是由ldr r0, [r0, #0]指令引起的,一条读指令。该指令试图读取r0寄存器指示地址处的内存。顺便说一句,r0是CPU(处理器)寄存器不是内存映射寄存器;它没有关联的地址,例如, GPIO_BSRR

如果我们能r0在引发异常的那一刻检查寄存器的值是正确的,那不是很好吗?好吧,我们已经做到了!我们之前打印r0ef值中的字段r0是引发异常时 register的值。又来了:

(gdb) print/x *ef
$1 = cortex_m_rt::ExceptionFrame {
  r0: 0x48001800,
  r1: 0x80036b0,
  r2: 0x1,
  r3: 0x80000000,
  r12: 0xb,
  lr: 0x800020d,
  pc: 0x8001750,
  xpsr: 0xa1000200
}

r0包含0x4800_1800我们调用read_volatile 函数的无效地址的值。

Spooky action at a distance

BSRR不是唯一可以控制端口 E 引脚的ODR寄存器。该寄存器还可以让您更改引脚的值。此外,ODR还可以让您检索端口 E 的当前输出状态。

ODR 记录在:

第 11.4.6 节 GPIO 端口输出数据寄存器 - 第 239 页

我们来看看这个程序。这个程序的关键是fn iprint_odr. 此函数将当前值打印ODRITM控制台

#![no_main]
#![no_std]

use core::ptr;

#[allow(unused_imports)]
use aux7::{entry, iprintln, ITM};

// Print the current contents of odr
fn iprint_odr(itm: &mut ITM) {
    const GPIOE_ODR: u32 = 0x4800_1014;

    unsafe {
        iprintln!(
            &mut itm.stim[0],
            "ODR = 0x{:04x}",
            ptr::read_volatile(GPIOE_ODR as *const u16)
        );
    }
}

#[entry]
fn main() -> ! {
    let mut itm= aux7::init().0;

    unsafe {
        // A magic addresses!
        const GPIOE_BSRR: u32 = 0x4800_1018;

        // Print the initial contents of ODR
        iprint_odr(&mut itm);

        // Turn on the "North" LED (red)
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 9);
        iprint_odr(&mut itm);

        // Turn on the "East" LED (green)
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << 11);
        iprint_odr(&mut itm);

        // Turn off the "North" LED
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (9 + 16));
        iprint_odr(&mut itm);

        // Turn off the "East" LED
        ptr::write_volatile(GPIOE_BSRR as *mut u32, 1 << (11 + 16));
        iprint_odr(&mut itm);
    }

    loop {}
}

如果你运行这个程序

$ cargo run
(..)
Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:22
22      #[entry]

(gdb) continue
Continuing.

您将在 itmdump 的控制台上看到:

$ # itmdump's console
(..)
ODR = 0x0000
ODR = 0x0200
ODR = 0x0a00
ODR = 0x0800
ODR = 0x0000

副作用!尽管我们多次读取同一个地址而没有实际修改它,但每次BSRR写入时我们仍然看到它的值变化。

Type safe manipulation

我们使用的最后一个寄存器ODR,在其文档中包含以下内容:

位 31:16 保留,必须保持在复位值

我们不应该写入寄存器的那些位,否则可能会发生坏事。

还有一个事实是寄存器具有不同的读/写权限。其中一些是只写的,另一些可以读取和写入,并且必须有一些是只读的。

最后,直接使用十六进制地址容易出错。您已经看到尝试访问无效的内存地址会导致异常,从而中断我们程序的执行。

如果我们有一个 API 来以“安全”的方式操作寄存器不是很好吗?理想情况下,API 应该对我提到的这三点进行编码:不要弄乱实际地址,应该尊重读/写权限,并应该防止修改寄存器的保留部分。

好吧,我们愿意!aux7::init()实际上返回一个值,该值提供了一个类型安全的 API 来操作GPIOE外围设备的寄存器 。

您可能还记得:与外围设备关联的一组寄存器称为寄存器块,它位于内存的连续区域中。在这种类型安全的 API 中,每个寄存器块都被建模为一个struct其中的每个字段代表一个寄存器。每个寄存器字段都是不同的新类型,例如u32公开以下方法的组合:readwritemodify根据其读/写权限。最后,这些方法不采用像 那样的原始值u32,而是采用另一种可以使用构建器模式构造的新类型,并防止修改寄存器的保留部分。

熟悉这个 API 的最好方法是将我们的运行示例移植到它。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux7::{entry, iprintln, ITM, RegisterBlock};

#[entry]
fn main() -> ! {
    let gpioe = aux7::init().1;

    // Turn on the North LED
    gpioe.bsrr.write(|w| w.bs9().set_bit());

    // Turn on the East LED
    gpioe.bsrr.write(|w| w.bs11().set_bit());

    // Turn off the North LED
    gpioe.bsrr.write(|w| w.br9().set_bit());

    // Turn off the East LED
    gpioe.bsrr.write(|w| w.br11().set_bit());

    loop {}
}

您注意到的第一件事:不涉及魔术地址。相反,我们使用更人性化的方式,例如gpioe.bsrrBSRRGPIOE寄存器块中引用寄存器。

然后我们有这个write接受闭包的方法。如果使用身份闭包 ( |w| w),此方法会将寄存器设置为其默认(重置)值,即在微控制器上电/重置后立即具有的值。该值0x0用于BSRR寄存器。由于我们想向寄存器写入一个非零值,我们使用像bs9和 之类的构建器方法br9来设置默认值的一些位。

让我们运行这个程序!调试程序时,我们可以做一些有趣的事情。

gpioe是对GPIOE寄存器块的引用。print gpioe将返回寄存器块的基地址。

$ cargo run
(..)

Breakpoint 1, registers::__cortex_m_rt_main_trampoline () at src/07-registers/src/main.rs:7
7       #[entry]

(gdb) step
registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:9
9           let gpioe = aux7::init().1;

(gdb) next
12          gpioe.bsrr.write(|w| w.bs9().set_bit());

(gdb) print gpioe
$1 = (*mut stm32f3::stm32f303::gpioc::RegisterBlock) 0x48001000

但是如果我们改为print *gpioe,我们将获得寄存器块的完整视图:将打印其每个寄存器的值。

(gdb) print *gpioe
$2 = stm32f3::stm32f303::gpioc::RegisterBlock {
  moder: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_MODER> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 1431633920
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_MODER>
  },
  otyper: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OTYPER> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OTYPER>
  },
  ospeedr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_OSPEEDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_OSPEEDR>
  },
  pupdr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_PUPDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_PUPDR>
  },
  idr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_IDR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 204
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_IDR>
  },
  odr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_ODR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_ODR>
  },
  bsrr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BSRR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BSRR>
  },
  lckr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_LCKR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_LCKR>
  },
  afrl: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRL> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRL>
  },
  afrh: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_AFRH> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_AFRH>
  },
  brr: stm32f3::generic::Reg<u32, stm32f3::stm32f303::gpioc::_BRR> {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0
      }
    },
    _marker: core::marker::PhantomData<stm32f3::stm32f303::gpioc::_BRR>
  }
}

所有这些新类型和闭包听起来像是会生成大型、臃肿的程序,但是,如果您在启用LTO 的情况下在发布模式下实际编译程序,您将看到它生成的指令与使用的“不安全”版本完全相同,write_volatile并且十六进制地址做到了!

cargo objdump抢的汇编代码release.txt

cargo objdump --bin registers --release -- -d --no-show-raw-insn --print-imm-hex > release.txt

然后搜索mainrelease.txt

0800023e <main>:
 800023e:          push    {r7, lr}
 8000240:          mov    r7, sp
 8000242:          bl    #0x2
 8000246:          trap

08000248 <registers::__cortex_m_rt_main::h199f1359501d5c71>:
 8000248:          push    {r7, lr}
 800024a:          mov    r7, sp
 800024c:          bl    #0x22
 8000250:          movw    r0, #0x1018
 8000254:          mov.w    r1, #0x200
 8000258:          movt    r0, #0x4800
 800025c:          str    r1, [r0]
 800025e:          mov.w    r1, #0x800
 8000262:          str    r1, [r0]
 8000264:          mov.w    r1, #0x2000000
 8000268:          str    r1, [r0]
 800026a:          mov.w    r1, #0x8000000
 800026e:          str    r1, [r0]
 8000270:          b    #-0x4 <registers::__cortex_m_rt_main::h199f1359501d5c71+0x28>

最好的部分是没有人必须编写一行代码来实现 GPIOE API。所有代码都是使用svd2rust工具从系统视图描述 (SVD) 文件自动生成的 。此 SVD 文件实际上是微控制器供应商提供的 XML 文件,其中包含其微控制器的寄存器映射。该文件包含寄存器块的布局、基地址、每个寄存器的读/写权限、寄存器的布局、寄存器是否具有保留位以及许多其他有用信息。

LEDs, again

在上一节中,我为您提供了初始化(配置)的外围设备(我在 中初始化了它们 aux7::init)。这就是为什么仅仅写入BSRR就足以控制 LED 的原因。但是,外设不会在微控制器启动后立即初始化

在本节中,您将获得更多关于寄存器的乐趣。我不会做任何初始化,您必须将配置GPIOE引脚初始化为数字输出引脚,以便您能够再次驱动 LED。

这是入门代码。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux8::entry;

#[entry]
fn main() -> ! {
    let (gpioe, rcc) = aux8::init();

    // TODO initialize GPIOE

    // Turn on all the LEDs in the compass
    gpioe.odr.write(|w| {
        w.odr8().set_bit();
        w.odr9().set_bit();
        w.odr10().set_bit();
        w.odr11().set_bit();
        w.odr12().set_bit();
        w.odr13().set_bit();
        w.odr14().set_bit();
        w.odr15().set_bit()
    });

    aux8::bkpt();

    loop {}
}

如果您运行启动代码,您将看到这次没有任何反应。此外,如果您打印GPIOE寄存器块,您会看到即使在gpioe.odr.write执行语句之后,每个寄存器的读数都为零 !

$ cargo run
Breakpoint 1, main () at src/08-leds-again/src/main.rs:9
9           let (gpioe, rcc) = aux8::init();

(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x08000f3c in __bkpt ()

(gdb) finish
Run till exit from #0  0x08000f3c in __bkpt ()
main () at src/08-leds-again/src/main.rs:25
25          aux8::bkpt();

(gdb) p/x *gpioe
$1 = stm32f30x::gpioc::RegisterBlock {
  moder: stm32f30x::gpioc::MODER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  otyper: stm32f30x::gpioc::OTYPER {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  ospeedr: stm32f30x::gpioc::OSPEEDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  pupdr: stm32f30x::gpioc::PUPDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  idr: stm32f30x::gpioc::IDR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  odr: stm32f30x::gpioc::ODR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  bsrr: stm32f30x::gpioc::BSRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  lckr: stm32f30x::gpioc::LCKR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrl: stm32f30x::gpioc::AFRL {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  afrh: stm32f30x::gpioc::AFRH {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  },
  brr: stm32f30x::gpioc::BRR {
    register: vcell::VolatileCell<u32> {
      value: core::cell::UnsafeCell<u32> {
        value: 0x0
      }
    }
  }
}

Power

事实证明,为了省电,大多数外围设备都以断电状态启动——这是它们在微控制器启动后的状态。

复位和时钟控制 ( RCC) 外设可用于打开或关闭所有其他外设。

您可以在以下位置的RCC寄存器块中找到寄存器列表:

第 9.4.14 节 - RCC 寄存器映射 - 第 166 页 - 参考手册

控制其他外设电源状态的寄存器有:

  • AHBENR
  • APB1ENR
  • APB2ENR

这些寄存器中的每一位控制单个外设的电源状态,包括GPIOE.

您在本节中的任务是打开GPIOE外围设备的电源。你必须:

  • 找出我之前提到的三个寄存器中的哪一个具有控制电源状态的位。
  • 弄清楚该位必须设置为什么值,0或者1,才能打开GPIOE外设。
  • 最后,您必须更改启动代码以修改正确的寄存器以打开 GPIOE外设。

如果成功,您将看到该gpioe.odr.write语句现在可以修改ODR寄存器的值。

请注意,这不足以实际打开 LED。

Configuration

开启GPIOE外设后,还需要进行配置。在这种情况下,我们希望将引脚配置为数字输出,以便它们可以驱动 LED;默认情况下,大多数引脚配置为数字输入

您可以在以下位置的GPIOE寄存器块中找到寄存器列表:

第 11.4.12 节 - GPIO 寄存器 - 第 243 页 - 参考手册

我们必须处理的寄存器是:MODER

本节的任务是进一步更新启动器代码以将正确的 GPIOE 引脚配置为数字输出。你必须:

  • 找出您需要将哪些引脚配置为数字输出。(提示:检查用户手册(第 18 页)的第 6.4 节 LED )。
  • 阅读文档以了解MODER寄存器中的位的作用。
  • 修改MODER寄存器以将引脚配置为数字输出。

如果成功,您将在运行程序时看到 8 个 LED 亮起。

The solution

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use aux8::entry;

#[entry]
fn main() -> ! {
    let (gpioe, rcc) = aux8::init();

    // enable the GPIOE peripheral
    rcc.ahbenr.write(|w| w.iopeen().set_bit());

    // configure the pins as outputs
    gpioe.moder.write(|w| {
        w.moder8().output();
        w.moder9().output();
        w.moder10().output();
        w.moder11().output();
        w.moder12().output();
        w.moder13().output();
        w.moder14().output();
        w.moder15().output()
    });

    // Turn on all the LEDs in the compass
    gpioe.odr.write(|w| {
        w.odr8().set_bit();
        w.odr9().set_bit();
        w.odr10().set_bit();
        w.odr11().set_bit();
        w.odr12().set_bit();
        w.odr13().set_bit();
        w.odr14().set_bit();
        w.odr15().set_bit()
    });

    aux8::bkpt();

    loop {}
}

Clocks and timers

在本节中,我们将重新实现 LED 轮盘赌应用程序。我将把Led抽象还给你, 但这次我要拿走Delay抽象:-)

这是入门代码。该delay功能未实现,因此如果您运行该程序,LED 将闪烁得如此之快,以至于它们似乎总是亮着。

#![no_main]
#![no_std]

use aux9::{entry, switch_hal::OutputSwitch, tim6};

#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    // TODO implement this
}

#[entry]
fn main() -> ! {
    let (leds, rcc, tim6) = aux9::init();
    let mut leds = leds.into_array();

    // TODO initialize TIM6

    let ms = 50;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().unwrap();
            delay(tim6, ms);
            leds[curr].off().unwrap();
            delay(tim6, ms);
        }
    }
}

for loop delays

第一个挑战是在delay不使用任何外设的情况下实现该功能,显而易见的解决方案是将其实现为for循环延迟:

#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    for _ in 0..1_000 {}
}

当然,上面的实现是错误的,因为它对于 的任何值总是产生相同的延迟ms

在本节中,您必须:

  • 修复delay函数以生成与其输入成比例的延迟ms
  • 调整该delay功能,使 LED 轮盘赌以 4 秒(800 毫秒周期)内大约 5 个周期的速度旋转。
  • 微控制器内部的处理器时钟频率为 72 MHz,并在一个“滴答”(一个时钟周期)内执行大多数指令。for认为delay函数必须执行多少 ( ) 个循环才能产生 1 秒的延迟?
  • 实际上有多少个for循环delay(1000)
  • 如果在发布模式下编译您的程序并运行它会发生什么?

NOP

如果在上一节中您在发布模式下编译程序并实际查看了反汇编,您可能会注意到该delay函数已被优化掉并且永远不会在main.

LLVM 认为该函数没有做任何有价值的事情,只是将其删除。

有一种方法可以防止 LLVM 优化for循环延迟:添加易失性汇编指令。任何指令都可以,但在这种情况下 NOP(无操作)是一个特别好的选择,因为它没有副作用。

您的for循环延迟将变为:

#[inline(never)]
fn delay(_tim6: &tim6::RegisterBlock, ms: u16) {
    const K: u16 = 3; // this value needs to be tweaked
    for _ in 0..(K * ms) {
        aux9::nop()
    }
}

delay当你在发布模式下编译你的程序时,这个时间不会被 LLVM 编译掉:

$ cargo objdump --bin clocks-and-timers --release -- -d --no-show-raw-insn
clocks-and-timers:      file format ELF32-arm-little

Disassembly of section .text:
clocks_and_timers::delay::h711ce9bd68a6328f:
 8000188:       push    {r4, r5, r7, lr}
 800018a:       movs    r4, #0
 800018c:       adds    r4, #1
 800018e:       uxth    r5, r4
 8000190:       bl      #4666
 8000194:       cmp     r5, #150
 8000196:       blo     #-14 
 8000198:       pop     {r4, r5, r7, pc}

现在,测试一下:在调试模式下编译程序并运行它,然后在发布模式下编译程序并运行它。它们之间有什么区别?您认为造成差异的主要原因是什么?你能想出一种方法让它们再次相等或至少更相似吗?

One-shot timer

我希望,到目前为止,我已经让您确信for循环延迟是实现延迟的一种糟糕方式。

现在,我们将使用硬件计时器实现延迟。(硬件)计时器的基本功能是……精确跟踪时间。定时器是另一个可供微控制器使用的外设。因此它可以使用寄存器进行控制。

我们使用的微控制器有几个(实际上超过 10 个)不同类型(基本、通用和高级定时器)的定时器可供使用。有些计时器 比其他计时器具有更高的分辨率(位数),而有些计时器不仅可以用于跟踪时间。

我们将使用其中一种基本计时器:TIM6. 这是我们微控制器中可用的最简单的定时器之一。基本计时器的文档在以下部分:

第 22 节定时器 - 第 670 页 - 参考手册

其寄存器记录在:

第 22.4.9 节 TIM6/TIM7 寄存器映射 - 第 682 页 - 参考手册

我们将在本节中使用的寄存器是:

  • SR,状态寄存器。
  • EGR,事件生成寄存器。
  • CNT,计数器寄存器。
  • PSC,预分频寄存器。
  • ARR,自动重载寄存器。

我们将使用计时器作为一次性计时器。它有点像闹钟。我们将计时器设置为在一段时间后关闭,然后我们将等到计时器关闭。该文档将这种操作模式称为单脉冲模式

以下是对基本定时器在单脉冲模式下配置时如何工作的描述:

  • 计数器由用户启用 ( CR1.CEN = 1)。
  • CNT寄存器复位其值为零,并且,在每个刻度,其值被加一。
  • 一旦CNT寄存器达到寄存器的值ARR,计数器将被硬件 ( CR1.CEN = 0)禁用并引发更新事件( SR.UIF = 1)。

TIM6由 APB1 时钟驱动,其频率不必与处理器频率匹配。也就是说,APB1 时钟可以运行得更快或更慢。但是,默认设置是 APB1 和处理器的时钟频率均为 8 MHz。

在单脉冲模式的功能描述中提到的刻度是一样的APB1时钟的一个刻度。的CNT在频率寄存器增加apb1 / (psc + 1) 每秒,其中倍apb1是APB1时钟的频率和psc在预分频寄存器的值,PSC

Initialization

与其他所有外设一样,我们必须先初始化此计时器,然后才能使用它。和上一节一样,初始化将涉及两个步骤:启动定时器,然后配置它。

启动定时器很容易:我们只需将TIM6EN位设置为 1。该位位于寄存器块APB1ENRRCC寄存器中。

    // Power on the TIM6 timer
    rcc.apb1enr.modify(|_, w| w.tim6en().set_bit());

配置部分稍微复杂一些。

首先,我们必须将定时器配置为以单脉冲模式运行。

    // OPM Select one pulse mode
    // CEN Keep the counter disabled for now
    tim6.cr1.write(|w| w.opm().set_bit().cen().clear_bit());

然后,我们希望CNT计数器以 1 KHz 的频率运行,因为我们的delay 函数将毫秒数作为参数,而 1 KHz 会产生 1 毫秒的周期。为此,我们必须配置预分频器。

    // Configure the prescaler to have the counter operate at 1 KHz
    tim6.psc.write(|w| w.psc().bits(psc));

我会让你弄清楚预分频器的值,psc. 请记住,计数器的频率是apb1 / (psc + 1)和该apb1是8MHz。

Busy waiting

计时器现在应该正确初始化。剩下的就是delay使用定时器来实现该功能。

我们要做的第一件事是设置自动重载寄存器 ( ARR) 以使计时器以ms 毫秒为单位结束。由于计数器以 1 KHz 运行,因此自动重载值将与 相同ms

    // Set the timer to go off in `ms` ticks
    // 1 tick = 1 ms
    tim6.arr.write(|w| w.arr().bits(ms));

接下来,我们需要启用计数器。它将立即开始计数。

    // CEN: Enable the counter
    tim6.cr1.modify(|_, w| w.cen().set_bit());

现在我们需要等到计数器达到自动重载寄存器的值ms,然后我们就会知道ms毫秒已经过去了。这种情况称为更新事件,它由UIF状态寄存器 ( SR)的位指示。

    // Wait until the alarm goes off (until the update event occurs)
    while !tim6.sr.read().uif().bit_is_set() {}

这种只等待直到满足某些条件(在这种情况下UIF变为1)的模式称为忙等待,您将在本文中多次看到它:-)

最后,我们必须清除(设置为0)该UIF位。如果我们不这样做,下次我们进入该delay 函数时,我们会认为更新事件已经发生并跳过忙等待部分。

    // Clear the update event flag
    tim6.sr.modify(|_, w| w.uif().clear_bit());

现在,将所有这些放在一起并检查它是否按预期工作。

Putting it all together

#![no_main]
#![no_std]

use aux9::{entry, switch_hal::OutputSwitch, tim6};

#[inline(never)]
fn delay(tim6: &tim6::RegisterBlock, ms: u16) {
    // Set the timer to go off in `ms` ticks
    // 1 tick = 1 ms
    tim6.arr.write(|w| w.arr().bits(ms));

    // CEN: Enable the counter
    tim6.cr1.modify(|_, w| w.cen().set_bit());

    // Wait until the alarm goes off (until the update event occurs)
    while !tim6.sr.read().uif().bit_is_set() {}

    // Clear the update event flag
    tim6.sr.modify(|_, w| w.uif().clear_bit());
}

#[entry]
fn main() -> ! {
    let (leds, rcc, tim6) = aux9::init();
    let mut leds = leds.into_array();

    // Power on the TIM6 timer
    rcc.apb1enr.modify(|_, w| w.tim6en().set_bit());

    // OPM Select one pulse mode
    // CEN Keep the counter disabled for now
    tim6.cr1.write(|w| w.opm().set_bit().cen().clear_bit());

    // Configure the prescaler to have the counter operate at 1 KHz
    // APB1_CLOCK = 8 MHz
    // PSC = 7999
    // 8 MHz / (7999 + 1) = 1 KHz
    // The counter (CNT) will increase on every millisecond
    tim6.psc.write(|w| w.psc().bits(7_999));

    let ms = 50;
    loop {
        for curr in 0..8 {
            let next = (curr + 1) % 8;

            leds[next].on().unwrap();
            delay(tim6, ms);
            leds[curr].off().unwrap();
            delay(tim6, ms);
        }
    }
}

Serial communication

这就是我们将要使用的。我希望你的电脑有一个!

呐,别担心。这种连接器 DE-9 很久以前在 PC 上已经过时了。它被通用串行总线 (USB) 取代。我们不会处理 DE-9 连接器本身,而是处理该电缆通常用于/通常用于的通信协议。

那么这个串行通信是什么?它是一种异步通信协议,其中两个设备使用两条数据线(加上一个公共接地)串行交换数据,就像一次一位一样。该协议是异步的,因为共享线路都不携带时钟信号。相反,双方必须在通信发生之前就沿线路发送数据的速度达成一致。该协议允许双工通信,因为数据可以同时从 A 发送到 B 和从 B 发送到 A。

我们将使用此协议在微控制器和您的计算机之间交换数据。与我们之前使用的 ITM 协议相比,通过串行通信协议,您可以将数据从计算机发送到微控制器。

您可能要问的下一个实际问题是:我们通过该协议发送数据的速度有多快?

该协议适用于帧。每个帧有一个起始位、5 到 9 位有效载荷(数据)和 1 到 2 个停止位。协议的速度称为波特率,以每秒位数 (bps) 为单位。常见的波特率有:9600、19200、38400、57600 和 115200 bps。

实际回答这个问题:使用 1 个起始位、8 个数据位、1 个停止位和 115200 bps 的波特率的常见配置,理论上可以每秒发送 11,520 帧。由于每一帧都携带一个字节的数据,因此数据速率为 11.52 KB/s。实际上,由于通信较慢端(微控制器)的处理时间,数据速率可能会较低。

今天的计算机不支持串行通信协议。所以你不能直接将计算机连接到微控制器。但这就是串行模块的用武之地。该模块将位于两者之间,并为微控制器提供一个串行接口,并为您的计算机提供一个 USB 接口。微控制器会将您的计算机视为另一个串行设备,而您的计算机会将微控制器视为虚拟串行设备。

*nix tooling

发现板的更新版本

在更新版本中,如果您将发现板连接到您的计算机,您应该会看到一个新的 TTY 设备出现在/dev.

$ # Linux
$ dmesg | tail | grep -i tty
[13560.675310] cdc_acm 1-1.1:1.2: ttyACM0: USB ACM device

这是 USB <-> 串行设备。在 Linux 上,它被命名为tty*(通常为 ttyACM*ttyUSB*)。

如果您没有看到设备出现,那么您可能有一个较旧的电路板版本;检查下一节,其中包含有关旧版本的说明。如果您有较新的修订版,请跳过下一部分并移至“minicom”部分。

发现板/外部串行模块的旧版本

将串行模块连接到您的计算机,让我们找出操作系统为其分配的名称。

注意在 Mac 上,USB 设备将命名为:/dev/cu.usbserial-*. 您不会使用 找到它dmesg,而是相应地使用ls -l /dev | grep cu.usb和调整以下命令!

$ dmesg | grep -i tty
(..)
[  +0.000155] usb 3-2: FTDI USB Serial Device converter now attached to ttyUSB0

但这是什么ttyUSB0东西?当然是文件啦!一切都是 *nix 中的文件:

$ ls -l /dev/ttyUSB0
crw-rw-rw- 1 root uucp 188, 0 Oct 27 00:00 /dev/ttyUSB0

注意如果上述权限是crw-rw----udev 规则未正确设置请参阅udev 规则

您可以通过简单地写入此文件来发送数据:

$ echo 'Hello, world!' > /dev/ttyUSB0

您应该看到串行模块上的 TX(红色)LED 闪烁一次,而且速度非常快!

所有版本:minicom

处理使用的串行设备echo远非符合人体工程学。因此,我们将使用该程序minicom 通过键盘与串行设备进行交互。

我们必须minicom在使用之前进行配置。有很多方法可以做到这一点,但我们将使用.minirc.dfl主目录中的 文件。~/.minirc.dfl使用以下内容创建一个文件:

$ cat ~/.minirc.dfl
pu baudrate 115200
pu bits 8
pu parity N
pu stopbits 1
pu rtscts No
pu xonxoff No

注意确保此文件以换行符结尾!否则,minicom将无法阅读。

该文件应该很容易阅读(除了最后两行),但让我们逐行阅读:

  • pu baudrate 115200. 将波特率设置为 115200 bps。
  • pu bits 8. 每帧 8 位。
  • pu parity N. 没有奇偶校验。
  • pu stopbits 1. 1 个停止位。
  • pu rtscts No. 没有硬件控制流。
  • pu xonxoff No. 没有软件控制流程。

一旦到位,我们就可以启动minicom.

$ # NOTE you may need to use a different device here
$ minicom -D /dev/ttyACM0 -b 115200

这告诉minicom打开串行设备/dev/ttyACM0并将其波特率设置为 115200。基于文本的用户界面 (TUI) 将弹出。

您现在可以使用键盘发送数据了!继续输入一些东西。请注意,TUI不会回显您键入的内容,但是,如果您使用的是外部模块,您可能会看到模块上的一些 LED 随每次按键而闪烁。

minicom 命令

minicom通过键盘快捷键公开命令。在 Linux 上,快捷方式以Ctrl+A. 在 Mac 上,快捷键以键开头Meta。一些有用的命令如下:

  • Ctrl+A+ Z。Minicom 命令总结
  • Ctrl+A+ C。清屏
  • Ctrl+A+ X。退出并重置
  • Ctrl+A+ Q。退出而不重置

注意mac 用户:在上面的命令中,替换Ctrl+AMeta.

Windows tooling

首先拔下您的发现板。

在插入发现板或串口模块之前,请在终端上运行以下命令:

$ mode

它将打印连接到您的计算机的设备列表。与启动的人COM在他们的名字是串口设备。这是我们将使用的设备类型。插入串行模块之前,*请记下所有COM *端口 mode输出。

现在,插入发现板并mode再次运行命令。如果您看到COM列表中出现了一个新 端口,那么您就有了一个较新的发现版本,这是分配给发现中串行功能的 COM 端口。你可以跳过下一段。

如果您没有获得新的 COM 端口,那么您可能有一个较旧的发现版本。现在插入串口模块;您应该会看到新的 COM 端口出现;那是串行模块的COM端口。

现在启动putty。将弹出一个 GUI。

在应该打开“会话”类别的启动屏幕上,选择“串行”作为“连接类型”。在“串行线路”字段中输入COM您在上一步中获得的设备,例如COM3

接下来,从左侧菜单中选择“连接/串行”类别。在这个新视图中,确保串口配置如下:

  • “速度(波特)”:115200
  • “数据位”:8
  • “停止位”:1
  • “奇偶校验”:无
  • “流量控制”:无

最后,点击打开按钮。现在会出现一个控制台:

如果您在此控制台上键入,串行模块上的 TX(红色)LED 应闪烁。每次击键都应使 LED 闪烁一次。请注意,控制台不会回显您键入的内容,因此屏幕将保持空白。

Loopbacks

我们已经测试了发送数据。是时候测试接收它了。除了没有其他设备可以向我们发送一些数据……或者有吗?

输入:环回

串行模块环回

您可以向自己发送数据!在生产中不是很有用,但对调试非常有用。

旧板修订版/外部串行模块

如上图所示,使用公对公跳线将串行模块的TXORXI引脚连接在一起。

现在在 minicom/PuTTY 中输入一些文本并观察。发生什么了?

你应该看到三件事:

  • 和以前一样,每次按下按键时 TX(红色)LED 都会闪烁。
  • 但现在 RX(绿色)LED 也会在每次按键时闪烁!这表示串口模块正在接收一些数据;它刚刚发送的那个。
  • 最后,在 minicom/PuTTY 控制台上,您应该看到您键入的内容回显到控制台。

Newer board revision

如果您有较新版本的电路板,您可以通过使用母对母跳线将 PC4 和 PC5 引脚短路来设置环回,就像您对 SWO 引脚所做的那样。

您现在应该可以向自己发送数据了。

现在尝试在 minicom/PuTTY 中输入一些文本并观察。

注意:为了排除现有固件对串行引脚(PC4 和 PC5)执行奇怪操作的可能性,我们建议您在向 minicom/PuTTY 中输入文本时按住重置按钮。

如果一切正常,您应该看到您键入的内容回显到 minicom/PuTTY 控制台。

USART

微控制器有一个称为 USART 的外设,它代表通用同步/异步接收器/发送器。该外设可以配置为使用多种通信协议,如串行通信协议。

在本章中,我们将使用串行通信在微控制器和计算机之间交换信息。但在我们这样做之前,我们必须把所有东西都连接起来。

我之前提到过,这个协议涉及两条数据线:TX 和 RX。TX代表发射器,RX代表接收器。发射器和接收器虽然是相对术语;哪条线路是发送器,哪条线路是接收器,取决于您从通信的哪一侧查看线路。

Newer board revisions

如果您有较新版本的电路板并使用板载 USB <-> 串行功能,那么auxiliary板条箱会将引脚设置PC4为 TX 线,将引脚设置PC5为 RX 线。

板上的所有东西都已经接线,因此您无需自己接线。

较旧的电路板修订版/外部串行模块

如果您使用的是外部USB < - >串行模块,那么你将需要启用adapter的特征aux11在箱子的依赖Cargo.toml

[dependencies.aux11]
path = "auxiliary"
# enable this if you are going to use an external serial adapter
features = ["adapter"] # <- uncomment this

我们将使用该引脚PA9作为微控制器的 TX 线和PA10它的 RX 线。换句话说,引脚将PA9数据输出到其线路上,而引脚PA10在其线路上侦听数据。

我们可以使用不同的引脚对作为 TX 和 RX 引脚。数据表第 44 页中有一个表格 ,列出了我们可以使用的所有其他可能的引脚。

串行模块也有 TX 和 RX 引脚。我们必须交叉这些引脚:即将微控制器的 TX 引脚连接到串行模块的 RX 引脚,将微控制器的 RX 引脚连接到串行模块的 TX 引脚。下面的接线图显示了所有必要的连接。

这些是连接微控制器和串行模块的推荐步骤:

  • 关闭 OpenOCD 和 itmdump
  • 断开 USB 电缆与 F3 和串行模块的连接。
  • 使用母对公 (F/M) 线将 F3 GND 引脚之一连接到串行模块的 GND 引脚。最好是黑色的。
  • 使用 F/M 线将 F3 背面的 PA9 引脚连接到串行模块的 RXI 引脚。
  • 使用 F/M 线将 F3 背面的 PA10 引脚连接到串行模块的 TXO 引脚。
  • 现在将 USB 电缆连接到 F3。
  • 最后将 USB 电缆连接到串行模块。
  • 重新启动 OpenOCD 和 itmdump

一切都已接好!让我们继续来回发送数据。

Send a single byte

我们的第一个任务是通过串行连接从微控制器向计算机发送一个字节。

这一次,我将为您提供一个已经初始化的 USART 外设。您只需使用负责发送和接收数据的寄存器。

进入11-usart目录,让我们在其中运行启动代码。确保您已打开 minicom/PuTTY。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // Send a single character
    usart1
        .tdr
        .write(|w| w.tdr().bits(u16::from(b'X')) );

    loop {}
}

该程序写入TDR寄存器。这会导致USART外设通过串行接口发送一个字节的信息。

在接收端,您的计算机上,您应该看到显示字符X出现在 minicom/PuTTY 的终端上。

Send a string

下一个任务是将整个字符串从微控制器发送到您的计算机。

我希望您将字符串"The quick brown fox jumps over the lazy dog."从微控制器发送到您的计算机。

轮到你编写程序了。

在调试器中逐句执行您的程序。你看到了什么?

然后再次执行该程序,但一次性使用该continue命令。这次会发生什么?

最后,在发布模式下构建程序,并再次一次性运行它。这次会发生什么?

Overruns

如果你这样写你的程序:

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        usart1
            .tdr
            .write(|w| w.tdr().bits(u16::from(*byte)));
    }

    loop {}
}

当您执行在调试模式下编译的程序时,您可能会在计算机上收到类似的信息。

$ # minicom's terminal
(..)
The uic brwn oxjums oer helaz do.

如果你在发布模式下编译,你可能只会得到这样的东西:

$ # minicom's terminal
(..)
T

什么地方出了错?

您会看到,通过线路发送字节需要相对大量的时间。我已经做了数学,所以让我引用自己的话:

使用 1 个起始位、8 个数据位、1 个停止位和 115200 bps 的波特率的常见配置,理论上可以每秒发送 11,520 帧。由于每一帧都携带一个字节的数据,因此数据速率为 11.52 KB/s

我们的 pangram 有 45 个字节的长度。这意味着45 bytes / (11,520 bytes/s) = 3,906 us发送字符串至少需要 3,900 微秒 ( )。处理器的工作频率为 8 MHz,执行一条指令需要 125 纳秒,因此很可能for 在不到 3,900 微秒的时间内完成循环。

我们实际上可以计算执行for循环所需的时间。aux11::init()返回一个 MonoTimer(单调计时器)值,该值公开一个Instant类似于 std::time.

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, mono_timer, mut itm) = aux11::init();

    let instant = mono_timer.now();
    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        usart1.tdr.write(|w| w.tdr().bits(u16::from(*byte)));
    }
    let elapsed = instant.elapsed(); // in ticks

    iprintln!(
        &mut itm.stim[0],
        "`for` loop took {} ticks ({} us)",
        elapsed,
        elapsed as f32 / mono_timer.frequency().0 as f32 * 1e6
    );

    loop {}
}

在调试模式下,我得到:

$ # itmdump terminal
(..)
`for` loop took 22415 ticks (2801.875 us)

这不到 3,900 微秒,但相距不远,这就是为什么只有几个字节的信息丢失的原因。

总之,处理器试图以比硬件实际处理速度更快的速度发送字节,这会导致数据丢失。这种情况称为缓冲区溢出

我们如何避免这种情况?状态寄存器 ( ISR) 有一个标志 ,TXE指示写入TDR寄存器是否“安全”而不会导致数据丢失。

让我们用它来减慢处理器的速度。

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, mono_timer, mut itm) = aux11::init();

    let instant = mono_timer.now();
    // Send a string
    for byte in b"The quick brown fox jumps over the lazy dog.".iter() {
        // wait until it's safe to write to TDR
        while usart1.isr.read().txe().bit_is_clear() {} // <- NEW!

        usart1
            .tdr
            .write(|w| w.tdr().bits(u16::from(*byte)));
    }
    let elapsed = instant.elapsed(); // in ticks

    iprintln!(
        &mut itm.stim[0],
        "`for` loop took {} ticks ({} us)",
        elapsed,
        elapsed as f32 / mono_timer.frequency().0 as f32 * 1e6
    );

    loop {}
}

这一次,在调试或发布模式下运行程序应该会在接收端产生一个完整的字符串。

$ # minicom/PuTTY's console
(..)
The quick brown fox jumps over the lazy dog.

for循环的时间也应该更接近理论的 3,900 微秒。下面的时间是针对调试版本的。

$ # itmdump terminal
(..)
`for` loop took 30499 ticks (3812.375 us)

uprintln!

在下一个练习中,我们将实现uprint!宏系列。你的目标是让这行代码工作:

    uprintln!(serial, "The answer is {}", 40 + 2);

其中必须"The answer is 42"通过串行接口发送字符串。

我们该怎么做?这是信息寻找到std的执行println!

// src/libstd/macros.rs
macro_rules! print {
    ($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}

到目前为止看起来很简单。我们需要内置format_args!宏(它在编译器中实现,所以我们看不到它实际做了什么)。我们必须以完全相同的方式使用该宏。这个 _print函数有什么作用?

// src/libstd/io/stdio.rs
pub fn _print(args: fmt::Arguments) {
    let result = match LOCAL_STDOUT.state() {
        LocalKeyState::Uninitialized |
        LocalKeyState::Destroyed => stdout().write_fmt(args),
        LocalKeyState::Valid => {
            LOCAL_STDOUT.with(|s| {
                if s.borrow_state() == BorrowState::Unused {
                    if let Some(w) = s.borrow_mut().as_mut() {
                        return w.write_fmt(args);
                    }
                }
                stdout().write_fmt(args)
            })
        }
    };
    if let Err(e) = result {
        panic!("failed printing to stdout: {}", e);
    }
}

看起来很复杂,但我们唯一感兴趣的部分是:w.write_fmt(args)stdout().write_fmt(args)。是什么print!最终所做的就是调用fmt::Write::write_fmt方法与输出format_args!作为它的参数。

幸运的是,我们也不必实现该fmt::Write::write_fmt方法,因为它是默认方法。我们只需要实现这个fmt::Write::write_str方法。

让我们这样做。

这就是等式的宏观方面的样子。剩下要做的就是提供write_str方法的实现。

在上面我们看到Writestd::fmt. 我们无法访问stdWrite也可以在core::fmt.

#![deny(unsafe_code)]
#![no_main]
#![no_std]

use core::fmt::{self, Write};

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln, usart1};

macro_rules! uprint {
    ($serial:expr, $($arg:tt)*) => {
        $serial.write_fmt(format_args!($($arg)*)).ok()
    };
}

macro_rules! uprintln {
    ($serial:expr, $fmt:expr) => {
        uprint!($serial, concat!($fmt, "\n"))
    };
    ($serial:expr, $fmt:expr, $($arg:tt)*) => {
        uprint!($serial, concat!($fmt, "\n"), $($arg)*)
    };
}

struct SerialPort {
    usart1: &'static mut usart1::RegisterBlock,
}

impl fmt::Write for SerialPort {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        // TODO implement this
        // hint: this will look very similar to the previous program
        Ok(())
    }
}

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    let mut serial = SerialPort { usart1 };

    uprintln!(serial, "The answer is {}", 40 + 2);

    loop {}
}

Receive a single byte

到目前为止,我们已经将数据从微控制器发送到您的计算机。是时候尝试相反的方法了:从您的计算机接收数据。

有一个RDR寄存器将填充来自 RX 线的数据。如果我们读取该寄存器,我们将检索通道另一端发送的数据。问题是:我们怎么知道我们收到了(新的)数据?状态寄存器 ,ISR,有一个用于此目的的位: RXNE。我们可以忙着等待那面旗帜。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    loop {
        // Wait until there's data available
        while usart1.isr.read().rxne().bit_is_clear() {}

        // Retrieve the data
        let _byte = usart1.rdr.read().rdr().bits() as u8;

        aux11::bkpt();
    }
}

让我们试试这个程序吧!让它自由运行continue,然后在 minicom/PuTTY 的控制台中输入一个字符。发生什么了?_byte变量的内容是什么?

(gdb) continue
Continuing.

Program received signal SIGTRAP, Trace/breakpoint trap.
0x8003d48 in __bkpt ()

(gdb) finish
Run till exit from #0  0x8003d48 in __bkpt ()
usart::main () at src/11-usart/src/main.rs:19
19              aux11::bkpt();

(gdb) p/c _byte
$1 = 97 'a'

Echo server

让我们将传输和接收合并到一个程序中并编写一个回显服务器。回显服务器将它发送的相同文本发送回客户端。对于此应用程序,微控制器将作为服务器,而您和您的计算机将作为客户端。

这应该很容易实现。(提示:逐字节执行)

Reverse a string

好的,接下来让我们让服务器更有趣,让服务器用客户端发送的文本的反向响应客户端。每次他们按下 ENTER 键时,服务器都会响应客户端。每个服务器响应将在一个新行中。

这次你需要一个缓冲区;你可以使用heapless::Vec. 这是入门代码:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};
use heapless::Vec;

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // A buffer with 32 bytes of capacity
    let mut buffer: Vec<u8, 32> = Vec::new();

    loop {
        buffer.clear();

        // TODO Receive a user request. Each user request ends with ENTER
        // NOTE `buffer.push` returns a `Result`. Handle the error by responding
        // with an error message.

        // TODO Send back the reversed string
    }
}

My solution

#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux11::{entry, iprint, iprintln};
use heapless::Vec;

#[entry]
fn main() -> ! {
    let (usart1, _mono_timer, _itm) = aux11::init();

    // A buffer with 32 bytes of capacity
    let mut buffer: Vec<u8, 32> = Vec::new();

    loop {
        buffer.clear();

        loop {
            while usart1.isr.read().rxne().bit_is_clear() {}
            let byte = usart1.rdr.read().rdr().bits() as u8;

            if buffer.push(byte).is_err() {
                // buffer full
                for byte in b"error: buffer full\n\r" {
                    while usart1.isr.read().txe().bit_is_clear() {}
                    usart1
                        .tdr
                        .write(|w| w.tdr().bits(u16::from(*byte)));
                }

                break;
            }

            // Carriage return
            if byte == 13 {
                // Respond
                for byte in buffer.iter().rev().chain(&[b'\n', b'\r']) {
                    while usart1.isr.read().txe().bit_is_clear() {}
                    usart1
                        .tdr
                        .write(|w| w.tdr().bits(u16::from(*byte)));
                }

                break;
            }
        }
    }
}

Bluetooth setup

是时候摆脱一些电线了。串行通信不仅可以在 USB 协议之上进行仿真;它也可以在蓝牙协议之上进行模拟。这种串行蓝牙协议称为 RFCOMM。

在我们将蓝牙模块与微控制器一起使用之前,让我们首先使用 minicom/PuTTY 与它进行交互。

我们需要做的第一件事是:打开蓝牙模块。我们将不得不使用以下连接向它分享一些 F3 功能:

连接它的推荐步骤是:

  • 关闭 OpenOCD 和 itmdump
  • 断开 USB 电缆与 F3 和串行模块的连接。
  • 使用母对母 (F/F) 线将 F3 的 GND 引脚连接到蓝牙的 GND 引脚。最好是黑色的。
  • 使用 F/F 线将 F3 的 5V 引脚连接到蓝牙的 VCC 引脚。最好是红色的。
  • 然后,将 USB 电缆连接回 F3。
  • 重新启动 OpenOCD 和 itmdump

在您打开 F3 板的电源后,蓝牙模块上的两个 LED(一个蓝色和一个红色)应立即开始闪烁。

接下来要做的是配对您的计算机和蓝牙模块。AFAIK、Windows 和 mac 用户可以简单地使用他们的操作系统默认蓝牙管理器进行配对。蓝牙模块默认引脚为 1234。

Linux 用户必须遵循(部分)这些说明

Linux

如果您有图形蓝牙管理器,您可以使用它来将您的计算机与蓝牙模块配对并跳过这些步骤中的大部分。

首先,您计算机的蓝牙收发器可能已关闭。检查其状态hciconfig并在必要时将其打开:

$ hciconfig
hci0:   Type: Primary  Bus: USB
        BD Address: 68:17:29:XX:XX:XX  ACL MTU: 310:10  SCO MTU: 64:8
        DOWN  <--
        RX bytes:580 acl:0 sco:0 events:31 errors:0
        TX bytes:368 acl:0 sco:0 commands:30 errors:0

$ sudo hciconfig hci0 up

$ hciconfig
hci0:   Type: Primary  Bus: USB
        BD Address: 68:17:29:XX:XX:XX  ACL MTU: 310:10  SCO MTU: 64:8
        UP RUNNING  <--
        RX bytes:1190 acl:0 sco:0 events:67 errors:0
        TX bytes:1072 acl:0 sco:0 commands:66 errors:0

然后你需要启动 BlueZ(蓝牙)守护进程:

  • 在基于 systemd 的 Linux 发行版上,使用:
$ sudo systemctl start bluetooth
  • 在 Ubuntu(或基于新贵的 Linux 发行版)上,使用:
$ sudo /etc/init.d/bluetooth start

您可能还需要解锁蓝牙,具体取决于以下内容rfkill list

$ rfkill list
9: hci0: Bluetooth
        Soft blocked: yes # <--
        Hard blocked: no

$ sudo rfkill unblock bluetooth

$ rfkill list
9: hci0: Bluetooth
        Soft blocked: no  # <--
        Hard blocked: no

扫描

$ hcitool scan
Scanning ...
        20:16:05:XX:XX:XX       Ferris
$ #                             ^^^^^^

一对

$ bluetoothctl
[bluetooth]# scan on
[bluetooth]# agent on
[bluetooth]# pair 20:16:05:XX:XX:XX
Attempting to pair with 20:16:05:XX:XX:XX
[CHG] Device 20:16:05:XX:XX:XX Connected: yes
Request PIN code
[agent] Enter PIN code: 1234

射频通信设备

我们将为我们的蓝牙模块创建一个设备文件/dev。然后我们就可以像使用/dev/ttyUSB0.

$ sudo rfcomm bind 0 20:16:05:XX:XX:XX

因为我们用作0参数bind,/dev/rfcomm0将是分配给我们的蓝牙模块的设备文件。

您可以随时使用以下命令释放(销毁)设备文件:

$ # Don't actually run this command right now!
$ sudo rfcomm release 0

Loopback, again

将您的计算机与蓝牙模块配对后,您的操作系统应该已经为您创建了一个设备文件/COM 端口。在 Linux 上,它应该是/dev/rfcomm*; 在 mac 上,应该是/dev/cu.*;在 Windows 上,它应该是一个新的 COM 端口。

我们现在可以使用 minicom/PuTTY 测试蓝牙模块。由于该模块没有像串行模块那样用于传输和接收事件的 LED 指示灯,我们将使用环回连接测试该模块:

只需使用 F/F 线将模块的 TXD 引脚连接到其 RXD 引脚即可。

现在,使用minicom/连接到设备PuTTY

$ minicom -D /dev/rfcomm0

连接后,蓝牙模块的闪烁模式应变为:长时间暂停然后快速闪烁两次。

在 minicom/PuTTY 终端内输入应该回显您输入的内容。

AT commands

蓝牙模块和 F3 需要配置为以相同的波特率进行通信。教程代码将UART1串口设备初始化为115200波特率。HC-05蓝牙模块默认配置为9600波特率。

蓝牙模块支持 AT 模式,允许您检查和更改其配置和设置。要使用 AT 模式,请将蓝牙模块连接到 F3 和 FTDI,如下图所示。

进入AT模式的推荐步骤:

  • 断开 F3 和 FTDI 与计算机的连接。
  • 使用母/母 (F/F) 线(最好是黑色的)将 F3 的 GND 引脚连接到蓝牙的 GND 引脚。
  • 使用 F/F 线(最好是红色的)将 F3 的 5V 引脚连接到蓝牙的 VCC 引脚。
  • 使用母/公 (F/M) 线将 FTDI RXI 引脚连接到蓝牙的 TXD 引脚。
  • 使用母/公 (F/M) 线将 FTDI TXO 引脚连接到蓝牙的 RXD 引脚。
  • 现在通过 USB 电缆将 FTDI 连接到您的计算机。
  • 接下来通过 USB 电缆将 F3 连接到您的计算机,同时按住蓝牙模块上的按钮(有点棘手)。
  • 现在,松开按钮,蓝牙模块将进入 AT 模式。您可以通过观察蓝牙模块上的红色 LED 缓慢闪烁(大约 1-2 秒开/关)来确认这一点。

AT 模式始终以 38400 的波特率运行,因此请为该波特率配置您的终端程序并连接到 FTDI 设备。

当您的串行连接建立后,您可能会ERROR: (0)重复显示一堆。如果发生这种情况,只需按 ENTER 即可停止错误。

完整性检查

$ at
OK
OK
(etc...)

OK重复回答,直到您再次按 ENTER。

重命名设备

$ at+name=ferris
OK

查询蓝牙模块当前波特率

at+uart?
+UART:9600,0,0
OK
+UART:9600,0,0
OK
(etc ...)

更改波特率

$ at+uart=115200,0,0
OK

Serial over Bluetooth

现在我们验证了蓝牙模块与 minicom/PuTTY 兼容,让我们将其连接到微控制器:

连接它的推荐步骤:

  • 关闭 OpenOCD 和itmdump.
  • 断开 F3 与计算机的连接。
  • 使用母对母 (F/F) 线(最好是黑色的)将 F3 的 GND 引脚连接到模块的 GND 引脚。
  • 使用 F/F 线(最好是红色的)将 F3 的 5V 引脚连接到模块的 VCC 引脚。
  • 使用 F/F 线将 F3 背面的 PA9 (TX) 引脚连接到蓝牙的 RXD 引脚。
  • 使用 F/F 线将 F3 背面的 PA10 (RX) 引脚连接到蓝牙的 TXD 引脚。
  • 现在使用 USB 电缆连接 F3 和您的计算机。
  • 重新启动 OpenOCD 和itmdump.

就是这样!您应该可以不加修改地运行您在USART章节中编写的所有程序!只要确保您打开了正确的串行设备/COM 端口。

注意如果您在与蓝牙设备通信时遇到问题,您可能需要以较低的波特率初始化 USART1。从等于115200为9600bps降低它可能会帮助,如所描述这里

I2C

我们刚刚看到了串行通信协议。它是一种广泛使用的协议,因为它非常简单,而且这种简单性使其易于在蓝牙和 USB 等其他协议之上实现。

然而,它的简单性也是一个缺点。更复杂的数据交换,如读取数字传感器,需要传感器供应商在其之上提出另一种协议。

(Un)对我们来说幸运的是,嵌入式领域还有很多其他的通信协议。其中一些被广泛用于数字传感器。

我们使用的 F3 板有三个运动传感器:加速度计、磁力计和陀螺仪。加速度计和磁力计封装在单个组件中,可通过 I2C 总线进行访问。

I2C 代表内部集成电路,是一种同步 串行通信协议。它使用两条线来交换数据:一条数据线(SDA)和一条时钟线(SCL)。因为使用时钟线来同步通信,所以这是一个同步协议。

该协议使用 模型,其中主是启动和驱动与从设备通信的设备。多个设备,包括主设备和从设备,可以同时连接到同一条总线。主设备可以通过首先向总线广播其地址来与特定从设备通信。该地址可以是 7 位或 10 位长。一旦主机开始与从机通信,在主机停止通信之前,其他设备都不能使用总线。

时钟线决定了数据交换的速度,它通常以 100 KHz(标准模式)或 400 KHz(快速模式)的频率运行。

General protocol

I2C 协议比串行通信协议更复杂,因为它必须支持多个设备之间的通信。让我们使用示例来看看它是如何工作的:

主 -> 从

如果主站要向从站发送数据:

  1. 主:广播开始
  2. M:广播从地址(7 位)+ R/W(第 8 位)设置为 WRITE
  3. 从机:响应 ACK(确认)
  4. M:发送一个字节
  5. S:响应 ACK
  6. 重复步骤 4 和 5 零次或更多次
  7. M:广播停止或(广播重新启动并返回(2))

注意从地址可以是 10 位而不是 7 位长。其他都不会改变。

主 <- 从

如果master想从slave读取数据:

  1. M:广播开始
  2. M:广播从地址(7 位)+ R/W(第 8 位)设置为 READ
  3. S:以 ACK 响应
  4. S:发送字节
  5. M:以 ACK 响应
  6. 重复步骤 4 和 5 零次或更多次
  7. M:广播停止或(广播重新启动并返回(2))

注意从地址可以是 10 位而不是 7 位长。其他都不会改变。

LSM303DLHC

*注意:较新的(从 2020/09 左右开始)探索板可能有LSM303AGR 而不是LSM303DLHC。查看像这样的 github 问题以获取更多详细信息。

F3 中的两个传感器,磁力计和加速度计,封装在单个组件中:LSM303DLHC 集成电路。这两个传感器可以通过 I2C 总线访问。每个传感器的行为类似于 I2C 从设备,并具有不同的地址。

每个传感器都有自己的存储器,用于存储感知环境的结果。我们与这些传感器的交互将主要涉及读取它们的记忆。

这些传感器的存储器被建模为字节可寻址寄存器。这些传感器也可以配置;这是通过写入他们的寄存器来完成的。因此,从某种意义上说,这些传感器与微控制器内部的外围设备非常相似。不同之处在于它们的寄存器没有映射到微控制器的存储器中。相反,它们的寄存器必须通过 I2C 总线访问。

有关 LSM303DLHC 的主要信息来源是其数据表。通读它以了解如何读取传感器的寄存器。那部分在:

第 5.1.1 节 I2C 操作 - 第 20 页 - LSM303DLHC 数据表

与本书相关的文档的另一部分是寄存器的描述。那部分在:

第 7 节寄存器描述 - 第 25 页 - LSM303DLHC 数据表

Read a single register

让我们将所有这些理论付诸实践!

就像使用 USART 外设一样,我已经负责在您到达之前初始化所有内容, main因此您只需要处理以下寄存器:

  • CR2. 控制寄存器 2。
  • ISR. 中断和状态寄存器。
  • TXDR. 发送数据寄存器。
  • RXDR. 接收数据寄存器。

这些寄存器记录在参考手册的以下部分:

第 28.7 节 I2C 寄存器 - 第 868 页 - 参考手册

我们将把I2C1外围设备与引脚PB6( SCL) 和PB7( SDA)结合使用。

这次您无需连接任何东西,因为传感器在板上并且已经连接到微控制器。但是,我建议您将串行/蓝牙模块与 F3 断开连接,以使其更易于操作。稍后,我们将移动电路板相当多。

您的任务是编写一个程序来读取磁力计IRA_REG_M寄存器的内容。该寄存器是只读的并且始终包含值0b01001000

微控制器将扮演 I2C 主设备的角色,而 LSM303DLHC 内部的磁力计将成为 I2C 从设备。

这是入门代码。你必须实现TODOs。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// Slave address
const MAGNETOMETER: u16 = 0b0011_1100;

// Addresses of the magnetometer's registers
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, _delay, mut itm) = aux14::init();

    // Stage 1: Send the address of the register we want to read to the
    // magnetometer
    {
        // TODO Broadcast START

        // TODO Broadcast the MAGNETOMETER address with the R/W bit set to Write

        // TODO Send the address of the register that we want to read: IRA_REG_M
    }

    // Stage 2: Receive the contents of the register we asked for
    let byte = {
        // TODO Broadcast RESTART

        // TODO Broadcast the MAGNETOMETER address with the R/W bit set to Read

        // TODO Receive the contents of the register

        // TODO Broadcast STOP
        0
    };

    // Expected output: 0x0A - 0b01001000
    iprintln!(&mut itm.stim[0], "0x{:02X} - 0b{:08b}", IRA_REG_M, byte);

    loop {}
}

为了给您一些额外的帮助,这些是您将要使用的确切位域:

  • CR2: SADD1, RD_WRN, NBYTES, START,AUTOEND
  • ISR: TXIS, RXNE,TC
  • TXDRTXDATA
  • RXDRRXDATA

The solution

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// Slave address
const MAGNETOMETER: u16 = 0b0011_1100;

// Addresses of the magnetometer's registers
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, _delay, mut itm) = aux14::init();

    // Stage 1: Send the address of the register we want to read to the
    // magnetometer
    {
        // Broadcast START
        // Broadcast the MAGNETOMETER address with the R/W bit set to Write
        i2c1.cr2.write(|w| {
            w.start().set_bit();
            w.sadd().bits(MAGNETOMETER);
            w.rd_wrn().clear_bit();
            w.nbytes().bits(1);
            w.autoend().clear_bit()
        });

        // Wait until we can send more data
        while i2c1.isr.read().txis().bit_is_clear() {}

        // Send the address of the register that we want to read: IRA_REG_M
        i2c1.txdr.write(|w| w.txdata().bits(IRA_REG_M));

        // Wait until the previous byte has been transmitted
        while i2c1.isr.read().tc().bit_is_clear() {}
    }

    // Stage 2: Receive the contents of the register we asked for
    let byte = {
        // Broadcast RESTART
        // Broadcast the MAGNETOMETER address with the R/W bit set to Read
        i2c1.cr2.modify(|_, w| {
            w.start().set_bit();
            w.nbytes().bits(1);
            w.rd_wrn().set_bit();
            w.autoend().set_bit()
        });

        // Wait until we have received the contents of the register
        while i2c1.isr.read().rxne().bit_is_clear() {}

        // Broadcast STOP (automatic because of `AUTOEND = 1`)

        i2c1.rxdr.read().rxdata().bits()
    };

    // Expected output: 0x0A - 0b01001000
    iprintln!(&mut itm.stim[0], "0x{:02X} - 0b{:08b}", IRA_REG_M, byte);

    loop {}
}

Read several registers

读取IRA_REG_M寄存器很好地测试了我们对 I2C 协议的理解,但该寄存器包含无趣的信息。

这一次,我们将读取实际暴露传感器读数的磁力计寄存器。涉及六个连续的寄存器,它们OUT_X_H_M以地址开头0x03

我们将修改我们之前的程序来读取这六个寄存器。只需要进行一些修改。

我们需要将我们从磁力计请求的地址从 更改IRA_REG_MOUT_X_H_M

    // Send the address of the register that we want to read: OUT_X_H_M
    i2c1.txdr.write(|w| w.txdata().bits(OUT_X_H_M));

我们必须向从站请求六个字节而不是一个字节。

    // Broadcast RESTART
    // Broadcast the MAGNETOMETER address with the R/W bit set to Read
    i2c1.cr2.modify(|_, w| {
        w.start().set_bit();
        w.nbytes().bits(6);
        w.rd_wrn().set_bit();
        w.autoend().set_bit()
    });

并填充缓冲区而不是仅读取一个字节:

    let mut buffer = [0u8; 6];
    for byte in &mut buffer {
        // Wait until we have received the contents of the register
        while i2c1.isr.read().rxne().bit_is_clear() {}

        *byte = i2c1.rxdr.read().rxdata().bits();
    }

    // Broadcast STOP (automatic because of `AUTOEND = 1`)

将它们全部放在一个循环中并延迟以降低数据吞吐量:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux14::{entry, iprint, iprintln, prelude::*};

// Slave address
const MAGNETOMETER: u16 = 0b0011_1100;

// Addresses of the magnetometer's registers
const OUT_X_H_M: u8 = 0x03;
const IRA_REG_M: u8 = 0x0A;

#[entry]
fn main() -> ! {
    let (i2c1, mut delay, mut itm) = aux14::init();

    loop {
        // Broadcast START
        // Broadcast the MAGNETOMETER address with the R/W bit set to Write
        i2c1.cr2.write(|w| {
            w.start().set_bit();
            w.sadd().bits(MAGNETOMETER);
            w.rd_wrn().clear_bit();
            w.nbytes().bits(1);
            w.autoend().clear_bit()
        });

        // Wait until we can send more data
        while i2c1.isr.read().txis().bit_is_clear() {}

        // Send the address of the register that we want to read: OUT_X_H_M
        i2c1.txdr.write(|w| w.txdata().bits(OUT_X_H_M));

        // Wait until the previous byte has been transmitted
        while i2c1.isr.read().tc().bit_is_clear() {}

        // Broadcast RESTART
        // Broadcast the MAGNETOMETER address with the R/W bit set to Read
        i2c1.cr2.modify(|_, w| {
            w.start().set_bit();
            w.nbytes().bits(6);
            w.rd_wrn().set_bit();
            w.autoend().set_bit()
        });

        let mut buffer = [0u8; 6];
        for byte in &mut buffer {
            // Wait until we have received something
            while i2c1.isr.read().rxne().bit_is_clear() {}

            *byte = i2c1.rxdr.read().rxdata().bits();
        }
        // Broadcast STOP (automatic because of `AUTOEND = 1`)

        iprintln!(&mut itm.stim[0], "{:?}", buffer);

        delay.delay_ms(1_000_u16);
    }
}

如果你运行它,你应该在itmdump‘s 的控制台中每秒打印一个 6 个字节的新数组。如果您在板上移动,数组中的值应该会发生变化。

$ # itmdump terminal
(..)
[0, 45, 255, 251, 0, 193]
[0, 44, 255, 249, 0, 193]
[0, 49, 255, 250, 0, 195]

但是这些字节没有多大意义。让我们把它们变成实际的读数:

        let x_h = u16::from(buffer[0]);
        let x_l = u16::from(buffer[1]);
        let z_h = u16::from(buffer[2]);
        let z_l = u16::from(buffer[3]);
        let y_h = u16::from(buffer[4]);
        let y_l = u16::from(buffer[5]);

        let x = ((x_h << 8) + x_l) as i16;
        let y = ((y_h << 8) + y_l) as i16;
        let z = ((z_h << 8) + z_l) as i16;

        iprintln!(&mut itm.stim[0], "{:?}", (x, y, z));

现在它应该看起来更好:

$ # `itmdump terminal
(..)
(44, 196, -7)
(45, 195, -6)
(46, 196, -9)

这是沿磁力计的 XYZ 轴分解的地球磁场。

LED compass

在本节中,我们将使用 F3 上的 LED 实现指南针。就像正确的指南针一样,我们的 LED 指南针必须以某种方式指向北方。它会通过打开八个 LED 中的一个来实现这一点。打开的 LED 应指向北方。

磁场有大小(以高斯或特斯拉为单位)和方向。F3 上的磁力计测量外部磁场的大小和方向,但它会报告所述磁场沿其轴分解

见下文,磁力计有三个与之关联的轴。

上面只显示了 X 和 Y 轴。Z 轴指向屏幕的“外部”。

让我们通过运行以下启动代码来熟悉磁力计的读数:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*};

#[entry]
fn main() -> ! {
    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        iprintln!(&mut itm.stim[0], "{:?}", lsm303dlhc.mag().unwrap());
        delay.delay_ms(1_000_u16);
    }
}

lsm303dlhc模块通过 LSM303DLHC 提供高级 API。在底层,它执行与您在上一节中实现的相同的 I2C 例程,但它在I16x3结构而不是元组中报告 X、Y 和 Z 值 。

定位北在您当前位置的位置。然后旋转电路板,使其“朝北”对齐:北方 LED (LD3) 应指向北方。

现在运行启动代码并观察输出。你看到什么 X、Y 和 Z 值?

$ # itmdump terminal
(..)
I16x3 { x: 45, y: 194, z: -3 }
I16x3 { x: 46, y: 195, z: -8 }
I16x3 { x: 47, y: 197, z: -2 }

现在将板旋转 90 度,同时保持它与地面平行。这次你看到了什么 X、Y 和 Z 值?然后再次将其旋转 90 度。你看到了什么价值观?

Take 1

我们可以实现 LED 指南针的最简单方法是什么?即使它并不完美。

首先,我们只关心磁场的 X 和 Y 分量,因为当您查看指南针时,您总是将其保持在水平位置,因此指南针位于 XY 平面内。

例如,在以下情况下您会打开什么 LED。EMF 代表地球磁场,绿色箭头表示 EMF 的方向(指向北方)。

SoutheastLED,对不对?

在这种情况下,磁场的 X 和 Y 分量有什么迹象?两者都是积极的。

如果我们只看 X 和 Y 分量的符号,我们就可以确定磁场属于哪个象限。

在前面的示例中,磁场位于第一象限(x 和 y 为正),因此打开SouthEastLED是有意义的。同样,如果磁场位于不同的象限,我们可以打开不同的 LED。

让我们试试这个逻辑。这是入门代码:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        // Look at the signs of the X and Y components to determine in which
        // quadrant the magnetic field is
        let dir = match (x > 0, y > 0) {
            // Quadrant ???
            (true, true) => Direction::Southeast,
            // Quadrant ???
            (false, true) => panic!("TODO"),
            // Quadrant ???
            (false, false) => panic!("TODO"),
            // Quadrant ???
            (true, false) => panic!("TODO"),
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(1_000_u16);
    }
}

有一个Direction在枚举led有8个变种东南西北命名的模块: NorthEastSouthwest等。这些变体代表了指南针的8个LED之一。该Leds值可以使用Direction enum;进行索引。索引的结果是指向那个的 LED Direction

Solution 1

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        // Look at the signs of the X and Y components to determine in which
        // quadrant the magnetic field is
        let dir = match (x > 0, y > 0) {
            // Quadrant I
            (true, true) => Direction::Southeast,
            // Quadrant II
            (false, true) => Direction::Northeast,
            // Quadrant III
            (false, false) => Direction::Northwest,
            // Quadrant IV
            (true, false) => Direction::Southwest,
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(1_000_u16);
    }
}

Take 2

这一次,我们将使用数学来获得磁场与磁力计的 X 轴和 Y 轴形成的精确角度。

我们将使用该atan2功能。此函数返回-PItoPI范围内的角度。下图显示了如何测量该角度:

尽管此图中未明确显示,但 X 轴指向右侧,Y 轴指向上方。

这是入门代码。theta,以弧度表示,已经被计算出来。您需要根据 的值选择要打开的 LED theta

#![deny(unsafe_code)]
#![no_main]
#![no_std]

// You'll find this useful ;-)
use core::f32::consts::PI;

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};
// this trait provides the `atan2` method
use m::Float;

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        let _theta = (y as f32).atan2(x as f32); // in radians

        // FIXME pick a direction to point to based on `theta`
        let dir = Direction::Southeast;

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(100_u8);
    }
}

建议/提示:

  • 一整圈旋转等于 360 度。
  • PI 弧度相当于 180 度。
  • 如果theta为零,你会打开什么 LED?
  • theta相反,如果非常接近于零,您会打开什么 LED?
  • 如果theta继续增加,您会在什么值下打开不同的 LED?

Solution 2

#![deny(unsafe_code)]
#![no_main]
#![no_std]

// You'll find this useful ;-)
use core::f32::consts::PI;

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, switch_hal::OutputSwitch, Direction, I16x3};
use m::Float;

#[entry]
fn main() -> ! {
    let (leds, mut lsm303dlhc, mut delay, _itm) = aux15::init();
    let mut leds = leds.into_array();

    loop {
        let I16x3 { x, y, .. } = lsm303dlhc.mag().unwrap();

        let theta = (y as f32).atan2(x as f32); // in radians

        let dir = if theta < -7. * PI / 8. {
            Direction::North
        } else if theta < -5. * PI / 8. {
            Direction::Northwest
        } else if theta < -3. * PI / 8. {
            Direction::West
        } else if theta < -PI / 8. {
            Direction::Southwest
        } else if theta < PI / 8. {
            Direction::South
        } else if theta < 3. * PI / 8. {
            Direction::Southeast
        } else if theta < 5. * PI / 8. {
            Direction::East
        } else if theta < 7. * PI / 8. {
            Direction::Northeast
        } else {
            Direction::North
        };

        leds.iter_mut().for_each(|led| led.off().unwrap());
        leds[dir as usize].on().unwrap();

        delay.delay_ms(100_u8);
    }
}

Magnitude

我们一直在研究磁场的方向,但它的实际大小是多少?magnetic_field函数报告的数字是无单位的。我们如何将这些值转换为高斯?

文档将回答这个问题。

第 2.1 节传感器特性 - 第 10 页 - LSM303DLHC 数据表

该页面中的表格显示了根据 GN 位的值具有不同值的磁增益设置。默认情况下,这些 GN 位设置为001。这意味着 X 轴和 Y 轴1100 LSB / Gauss的磁增益为 ,Z 轴的磁增益为980 LSB / Gauss。LSB 代表最低有效位,1100 LSB / Gauss数字表示读数 1100等于1 Gauss,读数2200等于 2 高斯,依此类推。

因此,我们需要做的是将传感器输出的 X、Y 和 Z 值除以其相应的 增益。然后,我们将获得以高斯为单位的磁场的 X、Y 和 Z 分量。

通过一些额外的数学运算,我们可以从 X、Y 和 Z 分量中检索磁场的大小:

let magnitude = (x * x + y * y + z * z).sqrt();

将所有这些放在一个程序中:

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, I16x3};
use m::Float;

#[entry]
fn main() -> ! {
    const XY_GAIN: f32 = 1100.; // LSB / G
    const Z_GAIN: f32 = 980.; // LSB / G

    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        let I16x3 { x, y, z } = lsm303dlhc.mag().unwrap();

        let x = f32::from(x) / XY_GAIN;
        let y = f32::from(y) / XY_GAIN;
        let z = f32::from(z) / Z_GAIN;

        let mag = (x * x + y * y + z * z).sqrt();

        iprintln!(&mut itm.stim[0], "{} mG", mag * 1_000.);

        delay.delay_ms(500_u16);
    }
}

该程序将以毫高斯 ( mG)为单位报告磁场的大小(强度)。地球磁场的幅度是在范围内250 mG,以650 mG(幅度取决于您的地理位置),所以你应该在这个范围内看到的值或接近该范围-我看到周围210毫克的幅度。

一些问题:

不移动板子,你看到了什么价值?你总是看到相同的值吗?

如果你旋转木板,幅度会改变吗?应该改变吗?

Calibration

如果我们旋转板子,地球磁场相对于磁力计的方向应该改变,但它的大小不应该!然而,磁力计表明磁场的大小会随着电路板的旋转而变化。

为什么会这样?事实证明,磁力计需要校准才能返回正确的答案。

校准涉及相当多的数学(矩阵),因此我们不会在这里介绍它,但如果您有兴趣,本 应用说明会介绍该过程。相反,我们将在本节中做的是可视化我们的状态。

让我们来试试这个实验:让我们记录磁力计的读数,同时我们慢慢地向不同方向旋转板。我们将使用iprintln宏将读数格式化为制表符分隔值 (TSV)。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux15::{entry, iprint, iprintln, prelude::*, I16x3};

#[entry]
fn main() -> ! {
    let (_leds, mut lsm303dlhc, mut delay, mut itm) = aux15::init();

    loop {
        let I16x3 { x, y, z } = lsm303dlhc.mag().unwrap();

        iprintln!(&mut itm.stim[0], "{}\t{}\t{}", x, y, z);

        delay.delay_ms(100_u8);
    }
}

您应该在控制台中获得如下所示的输出:

$ # itmdump console
-76     213     -54
-76     213     -54
-76     213     -54
-76     213     -54
-73     213     -55

您可以使用管道将其传输到文件:

$ # Careful! Exit any running other `itmdump` instance that may be running
$ itmdump -F -f itm.txt > emf.txt

在记录数据几秒钟的同时,沿许多不同的方向旋转板。

然后将该 TSV 文件导入电子表格程序(或使用如下所示的 Python 脚本)并将前两列绘制为散点图。

#!/usr/bin/python

import csv
import math
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns
import sys

# apply plot style
sns.set()

x = []
y = []

with open(sys.argv[1], 'r') as f:
    rows = csv.reader(f, delimiter='\t')

    for row in rows:
        # discard rows that are missing data
        if len(row) != 3 or not row[0] or not row[1]:
            continue

        x.append(int(row[0]))
        y.append(int(row[1]))

r = math.ceil(max(max(np.abs(x)), max(np.abs(y))) / 100) * 100

plt.plot(x, y, '.')
plt.xlim(-r, r)
plt.ylim(-r, r)
plt.gca().set_aspect(1)
plt.tight_layout()

plt.savefig('emf.svg')
plt.close

如果您在平坦的水平面上旋转电路板,磁场的 Z 分量应该保持相对恒定,并且该图应该是以原点为中心的圆周(而不是椭圆)。如果您以随机方向旋转棋盘,这就是上面的情节,那么您应该得到一个由以原点为中心的一堆点组成的圆。与圆形的偏差表明需要校准磁力计。

带回家的信息:不要只相信传感器的读数。验证它正在输出合理的值。如果不是,则校准它。

Punch-o-meter(打孔计)

在本节中,我们将使用板上的加速度计。

这次我们要建造什么?打孔计!我们将测量你的刺戳的力量。嗯,实际上你可以达到的最大加速度,因为加速度是加速度计测量的。强度和加速度是成比例的,所以这是一个很好的近似值。

加速度计也内置在 LSM303DLHC 封装内。就像磁力计一样,它也可以使用 I2C 总线进行访问。它还具有与磁力计相同的坐标系。这里又是坐标系:

就像在上一个单元中一样,我们将使用高级 API 直接获取封装良好的struct.

Gravity is up?

我们要做的第一件事是什么?

执行健全性检查!

启动器代码打印加速度计测量的加速度的 X、Y 和 Z 分量。这些值已经被“缩放”并且单位为gs。其中1 g等于重力加速度,约9.8米每秒的平方。

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux16::{entry, iprint, iprintln, prelude::*, I16x3, Sensitivity};

#[entry]
fn main() -> ! {
    let (mut lsm303dlhc, mut delay, _mono_timer, mut itm) = aux16::init();

    // extend sensing range to `[-12g, +12g]`
    lsm303dlhc.set_accel_sensitivity(Sensitivity::G12).unwrap();
    loop {
        const SENSITIVITY: f32 = 12. / (1 << 14) as f32;

        let I16x3 { x, y, z } = lsm303dlhc.accel().unwrap();

        let x = f32::from(x) * SENSITIVITY;
        let y = f32::from(y) * SENSITIVITY;
        let z = f32::from(z) * SENSITIVITY;

        iprintln!(&mut itm.stim[0], "{:?}", (x, y, z));

        delay.delay_ms(1_000_u16);
    }
}

这个程序在董事会静止时的输出将是这样的:

$ # itmdump console
(..)
(0.0, 0.0, 1.078125)
(0.0, 0.0, 1.078125)
(0.0, 0.0, 1.171875)
(0.0, 0.0, 1.03125)
(0.0, 0.0, 1.078125)

这很奇怪,因为板没有移动,但它的加速度不为零。这是怎么回事?这一定和重力有关吧?因为重力加速度是1 g。但是重力将物体向下拉,所以沿 Z 轴的加速度应该是负的而不是正的……

程序是否使 Z 轴向后?不,您可以测试旋转板以使重力与 X 或 Y 轴对齐,但加速度计测量的加速度始终指向上方。

这里发生的情况是,加速度计测量的是电路板的正确加速度,而不是观察到的加速度。这个适当的加速度是从自由落体的观察者那里看到的板的加速度。自由落体的观察者正以 的加速度向地球中心移动1g;从它的角度来看,棋盘实际上是以 的加速度向上移动(远离地球中心)1g。这就是为什么正确的加速度指向上方的原因。这也意味着如果电路板处于自由落体状态,加速度计会报告正确的零加速度。请不要在家里尝试。

The challenge

为简单起见,我们将仅在板保持水平时测量 X 轴上的加速度。这样我们就不必处理减去我们之前观察到的虚构的 1g事情,这会很困难,因为1g根据电路板的方向,这可能会有 XYZ 组件。

以下是打孔机必须执行的操作:

  • 默认情况下,应用程序不会“观察”板的加速度。
  • 当检测到显着的 X 加速度时(即加速度超过某个阈值),应用程序应开始新的测量。
  • 在该测量间隔期间,应用程序应跟踪观察到的最大加速度
  • 测量间隔结束后,应用程序必须报告观察到的最大加速度。您可以使用iprintln宏报告该值。

试一试,让我知道你能打出多用力;-)

My solution

#![deny(unsafe_code)]
#![no_main]
#![no_std]

#[allow(unused_imports)]
use aux16::{entry, iprint, iprintln, prelude::*, I16x3, Sensitivity};
use m::Float;

#[entry]
fn main() -> ! {
    const SENSITIVITY: f32 = 12. / (1 << 14) as f32;
    const THRESHOLD: f32 = 0.5;

    let (mut lsm303dlhc, mut delay, mono_timer, mut itm) = aux16::init();

    lsm303dlhc.set_accel_sensitivity(Sensitivity::G12).unwrap();

    let measurement_time = mono_timer.frequency().0; // 1 second in ticks
    let mut instant = None;
    let mut max_g = 0.;
    loop {
        let g_x = f32::from(lsm303dlhc.accel().unwrap().x).abs() * SENSITIVITY;

        match instant {
            None => {
                // If acceleration goes above a threshold, we start measuring
                if g_x > THRESHOLD {
                    iprintln!(&mut itm.stim[0], "START!");

                    max_g = g_x;
                    instant = Some(mono_timer.now());
                }
            }
            // Still measuring
            Some(ref instant) if instant.elapsed() < measurement_time => {
                if g_x > max_g {
                    max_g = g_x;
                }
            }
            _ => {
                // Report max value
                iprintln!(&mut itm.stim[0], "Max acceleration: {}g", max_g);

                // Measurement done
                instant = None;

                // Reset
                max_g = 0.;
            }
        }

        delay.delay_ms(50_u8);
    }
}

What’s left for you to explore

我们几乎没有触及表面!还有很多东西等你去探索。

注意:如果您正在阅读本文,并且希望帮助在 Discovery 书中添加以下任何项目的示例或练习,或任何其他相关的嵌入主题,我们很乐意得到您的帮助!

如果您想提供帮助,请打开一个问题,但需要帮助或指导如何将其贡献给本书,或者打开一个添加信息的拉取请求!

关于嵌入式软件的话题

这些主题讨论编写嵌入式软件的策略。尽管可以通过不同的方式解决许多问题,但这些部分讨论了一些策略,以及何时使用它们有意义(或没有意义)。

多任务处理

我们所有的程序都执行了一个任务。我们如何在没有操作系统的系统中实现多任务处理,因此没有线程。多任务处理有两种主要方法:抢占式多任务处理和协作多任务处理。

在抢占式多任务处理中,当前正在执行的任务可以在任何时间点 被另一个任务抢占(中断)。在抢占时,第一个任务将被挂起,处理器将转而执行第二个任务。在某个时候,第一个任务将恢复。微控制器以中断的形式为抢占提供硬件支持。

在协作多任务处理中,正在执行的任务将一直运行,直到到达暂停点。当处理器到达该暂停点时,它将停止执行当前任务并转而执行不同的任务。在某个时候,第一个任务将恢复。这两种多任务处理方法之间的主要区别在于,在协作多任务处理中,在已知的暂停点产生 执行控制,而不是在其执行的任何点被强行抢占。

睡眠

我们所有的程序都在不断地轮询外围设备,看看是否有什么需要做的。然而,有时没有什么可做的!在那个时候,微控制器应该“休眠”。

当处理器休眠时,它会停止执行指令,从而节省电量。节省电量几乎总是一个好主意,因此您的微控制器应该尽可能多地休眠。但是,它如何知道何时必须醒来才能执行某些操作?“中断”是唤醒微控制器的事件之一,但还有其他事件,wfi并且wfe是使处理器“休眠”的指令。

与微控制器功能相关的主题

微控制器(如我们的 STM32F3)具有许多不同的功能。然而,许多共享类似的功能,可用于解决各种不同的问题。

这些主题讨论了其中的一些功能,以及如何在嵌入式开发中有效地使用它们。

直接内存访问 (DMA)

这个外设是一种异步的 memcpy。到目前为止,我们的程序一直在逐字节地将数据泵送到 UART 和 I2C 等外设中。该 DMA 外设可用于执行批量数据传输。从 RAM 到 RAM,从外围设备(如 UART)到 RAM 或从 RAM 到外围设备。您可以安排 DMA 传输,例如从 USART1 读取 256 个字节到此缓冲区中,让它在后台运行,然后轮询某个寄存器以查看它是否已完成,以便您可以在传输过程中执行其他操作。

中断

为了与现实世界进行交互,微控制器通常需要在发生某种事件时立即做出响应。

微控制器具有被中断的能力,这意味着当某个事件发生时,它会停止当前正在执行的任何操作,转而响应该事件。当我们想在按下按钮时停止电机或在计时器完成倒计时时测量传感器时,这非常有用。

尽管这些中断可能非常有用,但它们也可能有点难以正确使用。我们希望确保快速响应事件,同时也允许其他工作继续进行。

在 Rust 中,我们对中断进行建模,类似于桌面 Rust 程序上的线程概念。这意味着我们还必须考虑 Rust 概念Send以及Sync 在我们的主应用程序和作为处理中断事件的一部分执行的代码之间共享数据时。

脉宽调制 (PWM)

简而言之,PWM 会打开某些东西,然后定期将其关闭,同时在“开启时间”和“关闭时间”之间保持一定比例(“占空比”)。当用于具有足够高频率的 LED 时,这可用于使 LED 变暗。低占空比,比如 10% 的时间和 90% 的关闭时间,会使 LED 非常暗,而高占空比,比如 90% 的时间和 10% 的关闭时间,会使 LED 更亮(几乎好像它已完全通电)。

通常,PWM 可用于控制向某些电子设备提供多少功率。通过微控制器和电动机之间的适当(功率)电子设备,PWM 可用于控制向电动机提供多少功率,从而可用于控制其扭矩和速度。然后你可以添加一个角位置传感器,你就得到了一个闭环控制器,可以控制电机在不同负载下的位置。

数字输入

我们使用微控制器引脚作为数字输出来驱动 LED。但这些引脚也可以配置为数字输入。作为数字输入,这些引脚可以读取开关(开/关)或按钮(按下/未按下)的二进制状态。

剧透阅读开关/按钮的二进制状态并不像听起来那么简单;-)

模数转换器 (ADC)

那里有很多数字传感器。您可以使用 I2C 和 SPI 之类的协议来读取它们。但模拟传感器也存在!这些传感器只是输出一个与它们感测的幅度成正比的电压电平。

ADC 外设可用于将该“模拟”电压电平(例如1.25 伏特)转换为[0, 65535]处理器可用于其计算的范围内的“数字”数字。

数模转换器 (DAC)

正如您所期望的那样,DAC 与 ADC 完全相反。您可以将一些数字值写入寄存器以在某个“模拟”引脚上产生[0, 3.3V]范围内的电压(假设有3.3V电源)。当这个模拟引脚连接到一些合适的电子设备并且寄存器以某个恒定的、快速的速率(频率)和正确的值被写入时,你可以产生声音甚至音乐!

实时时钟 (RTC)

该外围设备可用于以“人类格式”跟踪时间。秒、分钟、小时、天、月和年。这个外围设备处理从“滴答”到这些人类友好的时间单位的转换。它甚至可以为您处理闰年和夏令时!

其他通讯协议

SPI、I2S、SMBUS、CAN、IrDA、以太网、USB、蓝牙等。

不同的应用程序使用不同的通信协议。面向用户的应用程序通常有一个 USB 连接器,因为 USB 是 PC 和智能手机中无处不在的协议。而在汽车内,您会发现很多 CAN“总线”。一些数字传感器使用 SPI,其他使用 I2C,其他使用 SMBUS。

一般嵌入式相关主题

这些主题涵盖了不特定于我们的设备或其上的硬件的项目。相反,他们讨论了可用于嵌入式系统的有用技术。

陀螺仪

作为我们的 Punch-o-meter 练习的一部分,我们使用加速度计来测量三个维度的加速度变化。我们的电路板还配备了一个称为陀螺仪的传感器,它使我们能够测量三个维度的“自旋”变化。

这在尝试构建某些系统时非常有用,例如想要避免翻倒的机器人。此外,来自陀螺仪等传感器的数据也可以使用称为传感器融合的技术与来自加速度计的数据相结合(有关更多信息,请参见下文)。

伺服和步进电机

虽然有些电机主要用于向一个方向或另一个方向旋转,例如向前或向后驱动遥控车,但有时更精确地测量电机的旋转方式很有用。

我们的微控制器可用于驱动伺服或步进电机,从而可以更精确地控制电机转动的圈数,甚至可以将电机定位在一个特定位置,例如,如果我们想移动指向特定方向的时钟。

传感器融合

STM32F3DISCOVERY 包含三个运动传感器:加速度计、陀螺仪和磁力计。这些措施本身就是:(适当的)加速度、角速度和(地球的)磁场。但是这些量级可以“融合”成更有用的东西:电路板方向的“稳健”测量。具有比单个传感器更少的测量误差的稳健手段将能够做到。

这种从不同来源获取更可靠数据的想法被称为传感器融合。


那么下一步去哪里?有几种选择:

  • 您可以查看f3董事会支持箱中的示例。所有这些示例都适用于您拥有的 STM32F3DISCOVERY 板。

  • 你可以试试这个运动传感器演示这篇博文中提供了有关实现和源代码的详细信息

  • 你可以查看群众实时。一个非常高效的抢占式多任务框架,支持任务优先级和无死锁执行。

  • 您可以尝试在不同的开发板上运行 Rust。最简单的入门方法是使用cortex-m-quickstartCargo 项目模板。

  • 你可以查看这篇博客文章,它描述了 Rust 类型系统如何防止 I/O 配置中的错误。

  • 您可以查看我的博客,了解有关使用 Rust 进行嵌入式开发的其他主题。

  • 您可以查看embedded-hal旨在为微控制器上常见的所有嵌入式 I/O 功能构建抽象(特征)的项目。

  • 您可以加入Weekly driver 计划并帮助我们在embedded-hal特征之上编写通用驱动程序, 并适用于各种平台(ARM Cortex-M、AVR、MSP430、RISCV 等)

General troubleshooting

OpenOCD 问题

无法连接到 OpenOCD - “错误:打开失败”

症状

在尝试与设备建立新连接时,您会收到如下所示的错误:

$ openocd -f (..)
(..)
Error: open failed
in procedure 'init'
in procedure 'ocd_bouncer'

原因

设备未(正确)连接或未使用正确的 ST-LINK 接口配置。

使固定

Linux:

  • 使用 来检查 USB 连接lsusb
  • 您可能没有足够的权限打开设备。再试一次sudo。如果可行,您可以使用这些说明使 OpenOCD 在没有 root 权限的情况下工作。
  • 您可能为 ST-LINK 使用了错误的接口配置。尝试interface/stlink-v2.cfg代替interface/stlink-v2-1.cfg.

视窗:

  • 您可能缺少 ST-LINK USB 驱动程序。安装说明 在这里

无法连接到 OpenOCD - “X00ms 后再次轮询”

症状

在尝试与设备建立新连接时,您会收到如下所示的错误:

$ openocd -f (..)
(..)
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 100ms
Info : Previous state query failed, trying to reconnect
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 300ms
Info : Previous state query failed, trying to reconnect

原因

微控制器可能陷入了某种紧密的无限循环中,或者它可能不断引发异常,例如异常处理程序正在引发异常。

使固定

  • 关闭 OpenOCD,如果正在运行
  • 按住重置(黑色)按钮
  • 启动 OpenOCD 命令
  • 现在,松开重置按钮

OpenOCD 连接丢失 - “在 X00 毫秒内再次轮询”

症状

一个运行OpenOCD的会话突然错误有:

# openocd -f (..)
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 100ms
Info : Previous state query failed, trying to reconnect
Error: jtag status contains invalid mode value - communication failure
Polling target stm32f3x.cpu failed, trying to reexamine
Examination failed, GDB will be halted. Polling again in 300ms
Info : Previous state query failed, trying to reconnect

原因

USB 连接丢失。

使固定

  • 关闭 OpenOCD
  • 断开并重新连接 USB 电缆。
  • 重新启动 OpenOCD

无法刷新设备 - “忽略数据包错误,继续…”

症状

刷新设备时,您将获得:

$ arm-none-eabi-gdb $file
Start address 0x8000194, load size 31588
Transfer rate: 22 KB/sec, 5264 bytes/write.
Ignoring packet error, continuing...
Ignoring packet error, continuing...

原因

itmdump在“打印”到 ITM 的程序运行时关闭。当前 GDB 会话将显示正常工作,只是没有 ITM 输出,但下一个 GDB 会话将出现错误,并显示上一节中显示的消息。

或者,itmdump被称为后,monitor tpiu发出从而使 itmdump删除的文件/文件命名管道是OpenOCD的被写入。

使固定

  • 关闭/杀死 GDB、OpenOCD 和 itmdump
  • 删除itmdump正在使用的文件/命名管道(例如, itm.txt)。
  • 启动 OpenOCD
  • 然后,启动 itmdump
  • 然后,启动执行monitor tpiu命令的 GDB 会话。

无法连接到 OpenOCD - “错误:无法将 telnet 绑定到套接字:地址已在使用中”

症状

在尝试与设备建立新连接时,您会收到如下所示的错误:

$ openocd -f (..)
(..)
Error: couldn't bind telnet to socket: Address already in use

原因

OpenOCD 需要访问的一个或多个端口 3333、4444 或 6666 正被另一个进程使用。这些端口中的每一个都用于另一个方面:3333 用于 gdb,4444 用于 telnet,6666 用于 TCL 的远程过程调用 (RPC) 命令

使固定

你可以走两条路线来解决这个问题。A) 终止使用这些端口之一的任何进程。B) 指定您知道可供 OpenOCD 免费使用的不同端口。

方案一

苹果电脑:

  • 通过运行获取使用端口的进程列表 sudo lsof -PiTCP -sTCP:LISTEN
  • 通过记录它们的 pid 并kill [pid]为每个进程运行来终止阻塞关键端口的进程。(假设您可以确认他们没有在您的机器上运行任何关键任务!)

方案B

全部:

  • 启动时将配置详细信息发送到 OpenOCD,以便它对任何进程使用与默认端口不同的端口。
  • 例如,要在 4441 而不是默认的 4444 上执行其 telnet 功能,您将运行 openocd -f interface/stlink-v2-1.cfg -f target/stm32f3x.cfg -c "telnet_port 4441"
  • 更多关于 OpenOCD 配置阶段的细节可以在他们的 [official docs online] 中找到:http://openocd.org/doc/html/Server-Configuration.html

Cargo问题

“找不到Crate core

症状

   Compiling volatile-register v0.1.2
   Compiling rlibc v1.0.0
   Compiling r0 v0.1.0
error[E0463]: can't find crate for `core`

error: aborting due to previous error

error[E0463]: can't find crate for `core`

error: aborting due to previous error

error[E0463]: can't find crate for `core`

error: aborting due to previous error

Build failed, waiting for other jobs to finish...
Build failed, waiting for other jobs to finish...
error: Could not compile `r0`.

To learn more, run the command again with --verbose.

原因

您使用的工具链早于nightly-2018-04-08并且忘记调用rustup target add thumbv7em-none-eabihf.

使固定

每晚更新并安装thumbv7em-none-eabihf目标。

$ rustup update nightly

$ rustup target add thumbv7em-none-eabihf

How to use GDB

下面是一些有用的 GDB 命令,可以帮助我们调试我们的程序。这假设您已将程序刷入微控制器并连接到 OpenOCD 会话。

一般调试

注意:您在下面看到的许多命令都可以使用简短的形式执行。例如,continue可以简单地用作c,或者break $location可以用作b $location。一旦您对下面的命令有了经验,请尝试看看在 GDB 无法识别它们之前,您可以让这些命令运行多短!

处理断点

  • break $location

    : 在代码中的某个位置设置断点。的值

    $location

    可以包括:

    • break *main - 中断函数的确切地址 main
    • break *0x080012f2 - 打破确切的内存位置 0x080012f2
    • break 123 - 在当前显示文件的第 123 行中断
    • break main.rs:123 - 在文件的第 123 行中断 main.rs
  • info break: 显示当前断点

  • delete

    : 删除所有断点

    • delete $n:删除断点$nn是一个数,例如:delete $2
  • clear

    : 删除下一条指令的断点

    • clear main.rs:$function: 删除$functionin入口处的断点main.rs
    • clear main.rs:123: 删除第 123 行的断点 main.rs
  • enable

    : 启用所有设置的断点

    • enable $n: 启用断点 $n
  • disable

    : 禁用所有设置的断点

    • disable $n: 禁用断点 $n

控制执行

  • continue: 开始或继续执行你的程序

  • next

    : 执行程序的下一行

    • next $n: 重复next $n数次
  • nexti: 同,next但用机器指令代替

  • step

    : 执行下一行,如果下一行包含对另一个函数的调用,则单步执行该代码

    • step $n: 重复step $n数次
  • stepi: 同,step但用机器指令代替

  • jump $location

    :在指定位置恢复执行:

    • jump 123: 在第 123 行恢复执行
    • jump 0x080012f2: 在地址 0x080012f2 处恢复执行

印刷信息

  • print /$f $data

    - 打印变量包含的值

    $data

    。可以选择使用 格式化输出

    $f

    ,其中可以包括:

    x: hexadecimal 
    d: signed decimal
    u: unsigned decimal
    o: octal
    t: binary
    a: address
    c: character
    f: floating point
    • print /t 0xA: 将十六进制值打印0xA为二进制 (0b1010)
  • x /$n$u$f $address

    : 检查内存在

    $address

    。可选地,

    $n

    定义要显示的单位数、

    $u

    单位大小(字节、半字、字等)、上面定义的

    $f

    任何

    print

    格式

    • x /5i 0x080012c4: 打印5条机器指令盯着地址 0x080012c4
    • x/4xb $pc: 从$pc当前指向的位置开始打印 4 个字节的内存
  • disassemble $location
    • disassemble /r main:反汇编函数main/r用于显示组成每条指令的字节

查看符号表

  • info functions $regex

    : 打印匹配的函数的名称和数据类型

    $regex

    ,省略

    $regex

    打印所有函数

    • info functions main: 打印包含单词的已定义函数的名称和类型 main
  • info address $symbol

    : 打印

    $symbol

    内存中的存储位置

    • info address GPIOC: 打印变量的内存地址 GPIOC
  • info variables $regex: 打印匹配的全局变量的名称和类型$regex,省略$regex打印所有全局变量

  • ptype $data

    : 打印更多详细信息

    $data
    • ptype cp: 打印变量的详细类型信息 cp

浏览程序堆栈

  • backtrace $n

    : 打印

    $n

    帧轨迹,或省略

    $n

    打印所有帧

    • backtrace 2: 打印前 2 帧的轨迹
  • frame $n: 选择带编号或地址的帧$n,省略$n显示当前帧

  • up $n: 选择帧$n帧向上

  • down $n:$n向下选择帧帧

  • info frame $address: 描述帧处$address,省略$address当前选定的帧

  • info args: 打印选定帧的参数

  • info registers $r

    : 打印

    $r

    选定帧中寄存器的值,

    $r

    所有寄存器 省略

    • info registers $sp: 打印$sp当前帧中栈指针寄存器的值

远程控制 OpenOCD

  • monitor reset run

    :重置CPU,重新开始执行

    • monitor reset: 和上面一样
  • monitor reset init: 重置 CPU,在开始时停止执行

  • monitor targets: 显示当前目标的信息和状态

😎《The Embedonomicon》

The embedonomicon

embedonomion 将引导您完成#![no_std]从头开始创建应用程序的过程,以及为 Cortex-M 微控制器构建特定于架构的功能的迭代过程。

要求

这本书是自包含的。读者不需要熟悉 Cortex-M 架构,也不需要访问 Cortex-M 微控制器——本书中包含的所有示例都可以在 QEMU 中进行测试。但是,您需要安装以下工具来运行和检查本书中的示例:

  • 本书所有代码均使用2018版。如果您不熟悉 2018 年的功能和习惯用法,请查看edition guide.
  • Rust 1.31 或更新的工具链加上 ARM Cortex-M 编译支持。
  • cargo-binutils. v0.1.4 或更新版本。
  • cargo-edit.
  • QEMU 支持 ARM 仿真。该qemu-system-arm程序必须安装在您的计算机上。
  • 具有 ARM 支持的 GDB。

示例设置

所有操作系统通用的指令

$ # Rust toolchain
$ # If you start from scratch, get rustup from https://rustup.rs/
$ rustup default stable

$ # toolchain should be newer than this one
$ rustc -V
rustc 1.31.0 (abe02cefd 2018-12-04)

$ rustup target add thumbv7m-none-eabi

$ # cargo-binutils
$ cargo install cargo-binutils

$ rustup component add llvm-tools-preview

苹果系统

$ # arm-none-eabi-gdb
$ # you may need to run `brew tap Caskroom/tap` first
$ brew cask install gcc-arm-embedded

$ # QEMU
$ brew install qemu

Ubuntu 16.04

$ # arm-none-eabi-gdb
$ sudo apt install gdb-arm-none-eabi

$ # QEMU
$ sudo apt install qemu-system-arm

Ubuntu 18.04 或 Debian

$ # gdb-multiarch -- use `gdb-multiarch` when you wish to invoke gdb
$ sudo apt install gdb-multiarch

$ # QEMU
$ sudo apt install qemu-system-arm

视窗

从 ARM 安装工具链包(可选步骤)(在 Ubuntu 18.04 上测试)

  • 随着 2018 年底从GCC 的链接器切换 到Cortex-M 微控制器的LLD,不再需要gcc-arm-none-eabi。但是对于那些希望使用工具链的人来说,从这里安装并按照下面列出的步骤操作:
$ tar xvjf gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2
$ mv gcc-arm-none-eabi-  # optional
$ export PATH=${PATH}:/bin # add this line to .bashrc to make persisten

The smallest #![no_std] program

在本节中,我们将编写最小的#![no_std]程序来编译.

什么#![no_std]意思?

#![no_std]是一个 crate 级别属性,表示 crate 将链接到core crate 而不是stdcrate,但这对应用程序意味着什么?

std箱是除锈的标准库。它包含假定程序将在操作系统上运行而不是直接在金属上运行的功能std还假设操作系统是通用操作系统,就像在服务器和台式机中找到的操作系统一样。出于这个原因,std提供了一个标准 API,而不是通常在以下操作系统中可以找到的功能:线程、文件、套接字、文件系统、进程等。

另一方面,core包是包的子集,std它对程序将在其上运行的系统做出零假设。因此,它为浮点数、字符串和切片等语言原语提供 API,以及公开原子操作和 SIMD 指令等处理器功能的 API。然而,它缺少任何涉及堆内存分配和 I/O 的 API。

对于应用程序来说,std不仅仅是提供一种访问操作系统抽象的方法。std它还负责设置堆栈溢出保护、处理命令行参数以及在main调用程序函数之前生成主线程等。一个#![no_std] 应用程序缺乏所有标准运行,所以它必须初始化它自己的运行时,如果有需要。

由于这些属性,#![no_std]应用程序可以是系统上运行的第一个和/或唯一的代码。它可以是标准 Rust 应用程序永远无法做到的许多事情,例如:

  • 操作系统的内核。
  • 固件。
  • 一个引导程序。

编码

有了这个,我们可以继续#![no_std]编译最小的程序:

$ cargo new --edition 2018 --bin app

$ cd app
$ # modify main.rs so it has these contents
$ cat src/main.rs
#![no_main]
#![no_std]

use core::panic::PanicInfo;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

这个程序包含一些你在标准 Rust 程序中看不到的东西:

#![no_std]我们已经广泛涵盖的属性。

#![no_main]属性意味着程序不会使用标准main函数作为其入口点。在撰写本文时,Rust 的main接口对程序执行的环境做了一些假设:例如,它假设存在命令行参数,因此一般来说,它不适用于#![no_std]程序。

#[panic_handler]属性。标有此属性的函数定义了恐慌的行为,包括库级恐慌 ( core::panic!) 和语言级恐慌(越界索引)。

这个程序不会产生任何有用的东西。事实上,它会产生一个空的二进制文件。

$ # equivalent to `size target/thumbv7m-none-eabi/debug/app`
$ cargo size --target thumbv7m-none-eabi --bin app
   text       data        bss        dec        hex    filename
      0          0          0          0          0    app

在链接板条箱之前确实包含恐慌符号。

$ cargo rustc --target thumbv7m-none-eabi -- --emit=obj

$ cargo nm -- target/thumbv7m-none-eabi/debug/deps/app-*.o | grep '[0-9]* [^N] '
00000000 T rust_begin_unwind

然而,这是我们的起点。在下一节中,我们将构建一些有用的东西。但在继续之前,让我们设置一个默认构建目标,以避免必须将--target标志传递给每个 Cargo 调用。

$ mkdir .cargo

$ # modify .cargo/config so it has these contents
$ cat .cargo/config
[build]
target = "thumbv7m-none-eabi"

eh_personality

如果您的配置没有因恐慌而无条件中止,而完整操作系统的大多数目标都没有(或者如果您的自定义目标不包含 "panic-strategy": "abort"),那么您必须告诉 Cargo 这样做或添加一个eh_personality函数,这需要每晚编译器。这是 Rust 关于它的文档这里有一些关于它的讨论

在 Cargo.toml 中,添加:

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

或者,声明eh_personality函数。一个在展开时不做任何特殊事情的简单实现如下:

#![feature(lang_items)]

#[lang = "eh_personality"]
extern "C" fn eh_personality() {}

language item required, but not found: 'eh_personality'如果不包括在内,您将收到错误消息。

Memory layout

下一步是确保程序具有正确的内存布局,以便目标系统能够执行它。在我们的示例中,我们将使用虚拟 Cortex-M3 微控制器: LM3S6965。我们的程序将是设备上运行的唯一进程,因此它还必须负责初始化设备。

背景资料

Cortex-M 设备要求在其代码存储区的开头存在一个向量表。向量表是一个指针数组;启动设备需要前两个指针,其余指针与异常有关。我们暂时忽略它们。

链接器决定程序的最终内存布局,但我们可以使用链接器脚本对其进行一些控制。链接器脚本为我们提供的布局控制粒度是在部分级别。节是在连续内存中布置的符号集合。反过来,符号可以是数据(静态变量)或指令(Rust 函数)。

每个符号都有一个由编译器分配的名称。作为防锈1.28的,名称,该锈编译受让人以符号的形式为:_ZN5krate6module8function17he1dfc17c86fe16daE,其中demangles到 krate::module::function::he1dfc17c86fe16da哪里krate::module::function是函数或变量的路径和he1dfc17c86fe16da是某种散列。Rust 编译器会将每个符号放入自己唯一的部分;例如,前面提到的符号将放置在名为.text._ZN5krate6module8function17he1dfc17c86fe16daE.

这些编译器生成的符号和节名称不能保证在 Rust 编译器的不同版本中保持不变。但是,该语言允许我们通过以下属性控制符号名称和部分位置:

  • #[export_name = "foo"]将符号名称设置为foo.
  • #[no_mangle]意思是:使用函数或变量名(不是它的完整路径)作为它的符号名。 #[no_mangle] fn bar()将产生一个名为 的符号bar
  • #[link_section = ".bar"]将符号放在名为 的部分中.bar

通过这些属性,我们可以公开程序的稳定 ABI 并在链接描述文件中使用它。

The Rust side

如上所述,对于 Cortex-M 设备,我们需要填充向量表的前两个条目。第一个,堆栈指针的初始值,可以仅使用链接描述文件填充。第二个是重置向量,需要在 Rust 代码中创建并使用链接描述文件正确放置。

重置向量是指向重置处理程序的指针。复位处理程序是设备在系统复位后或第一次上电后将执行的函数。重置处理程序始终是硬件调用堆栈中的第一个堆栈帧;从它返回是未定义的行为,因为没有其他堆栈帧要返回。我们可以通过使其成为一个发散函数来强制重置处理程序永远不会返回,这是一个带有签名的函数fn(/* .. */) -> !

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

硬件在这里需要某种格式,我们坚持使用它extern "C"来告诉编译器使用 C ABI 降低函数,而不是不稳定的 Rust ABI。

要从链接描述文件中引用重置处理程序和重置向量,我们需要它们有一个稳定的符号名称,因此我们使用#[no_mangle]. 我们需要对 的位置进行精细控制RESET_VECTOR,因此我们将其放置在已知部分 中.vector_table.reset_vector。重置处理程序本身的确切位置Reset并不重要。我们只是坚持使用默认的编译器生成部分。

链接器在遍历输入目标文件列表时将忽略具有内部链接的符号(也称为内部符号),因此我们需要我们的两个符号具有外部链接。在 Rust 中将符号设为外部的唯一方法是将其对应的项目设为公开 ( pub) 且可访问(项目和 crate 的根之间没有私有模块)。

链接器脚本端

将向量表放置在正确位置的最小链接描述文件如下所示。让我们来看看它。

$ cat link.x
/* Memory layout of the LM3S6965 microcontroller */
/* 1K = 1 KiBi = 1024 bytes */
MEMORY
{
  FLASH : ORIGIN = 0x00000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 64K
}

/* The entry point is the reset handler */
ENTRY(Reset);

EXTERN(RESET_VECTOR);

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));
  } > FLASH

  .text :
  {
    *(.text .text.*);
  } > FLASH

  /DISCARD/ :
  {
    *(.ARM.exidx .ARM.exidx.*);
  }
}

MEMORY

链接描述文件的这一部分描述了目标内存块的位置和大小。定义了两个内存块:FLASHRAM; 它们对应于目标中可用的物理内存。此处使用的值对应于 LM3S6965 微控制器。

ENTRY

这里我们向链接器指示符号名称为 的重置处理程序Reset是程序的 入口点。链接器积极丢弃未使用的部分。链接器将入口点和从中调用的函数视为已使用,因此不会丢弃它们。如果没有这一行,链接器将丢弃该Reset函数以及从它调用的所有后续函数。

EXTERN

链接器是懒惰的;一旦他们找到了从入口点递归引用的所有符号,他们将停止查看输入目标文件。即使在找到所有其他引用的符号之后,也EXTERN强制链接器查找EXTERN的参数。根据经验,如果您需要一个未从入口点调用的符号始终出现在输出二进制文件中,则应EXTERNKEEP.

SECTIONS

这部分描述了输入目标文件中的节(也称为输入节)如何安排在输出目标文件的节(也称为输出节)中,或者它们是否应该被丢弃。这里我们定义了两个输出部分:

  .vector_table ORIGIN(FLASH) : { /* .. */ } > FLASH

.vector_table包含向量表并位于FLASH内存的开头。

  .text : { /* .. */ } > FLASH

.text包含程序子程序并位于FLASH. 它的起始地址没有指定,但链接器将把它放在前一个输出段之后 .vector_table

输出.vector_table部分包含:

    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

我们将(调用)堆栈放在 RAM 的末尾(堆栈已满降序;它向更小的地址增长),因此 RAM 的结束地址将用作初始堆栈指针 (SP) 值。该地址是使用我们为RAM 内存块输入的信息在链接描述文件中计算出来的。

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));

接下来,我们使用KEEP强制链接器.vector_table.reset_vector在初始 SP 值之后插入所有命名的输入节 。位于该部分的唯一符号是RESET_VECTOR,因此这将有效地RESET_VECTOR排在向量表中的第二位。

输出.text部分包含:

    *(.text .text.*);

这包括所有名为.text和的输入部分.text.*。请注意,我们KEEP 在此处不使用让链接器丢弃未使用的部分。

最后,我们使用特殊/DISCARD/部分丢弃

    *(.ARM.exidx .ARM.exidx.*);

名为.ARM.exidx.*. 这些部分与异常处理有关,但我们不会对恐慌进行堆栈展开,它们会占用闪存中的空间,因此我们只是将它们丢弃。

把这一切放在一起

现在我们可以链接应用程序。作为参考,这里是完整的 Rust 程序:

#![no_main]
#![no_std]

use core::panic::PanicInfo;

// The reset handler
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    let _x = 42;

    // can't return so we go into an infinite loop here
    loop {}
}

// The reset vector, a pointer into the reset handler
#[link_section = ".vector_table.reset_vector"]
#[no_mangle]
pub static RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

我们必须调整链接器进程以使其使用我们的链接器脚本。这是通过将-C link-arg标志传递给rustc. 这可以通过cargo-rustc或 来完成cargo-build

重要提示.cargo/config在运行此命令之前,请确保您拥有在最后一节末尾添加的文件。

使用cargo-rustc子命令:

$ cargo rustc -- -C link-arg=-Tlink.x

或者您可以设置 rustflags.cargo/config并继续使用 cargo-build子命令。我们会做后者,因为它可以更好地与 cargo-binutils.

# modify .cargo/config so it has these contents
$ cat .cargo/config
[target.thumbv7m-none-eabi]
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"

[target.thumbv7m-none-eabi]部分表示这些标志仅在交叉编译到该目标时才会使用。

检查它

现在让我们检查输出二进制文件以确认内存布局看起来像我们想要的那样(这需要cargo-binutils):

$ cargo objdump --bin app -- -d -no-show-raw-insn
app:    file format elf32-littlearm


Disassembly of section .text:

:
                   sub    sp, #4
                   movs    r0, #42
                   str    r0, [sp]
                   b    #-2 
                   b    #-4 

这是该.text部分的拆解。我们看到名为 的重置处理程序Reset位于 address 0x8

$ cargo objdump --bin app -- -s -section .vector_table
app:    file format elf32-littlearm

Contents of section .vector_table:
 0000 00000120 09000000                    ... ....

这显示了该.vector_table部分的内容。我们可以看到该部分从地址开始,0x0并且该部分的第一个字是0x2001_0000objdump输出为小端格式)。这是初始 SP 值并匹配 RAM 的结束地址。第二个字是0x9; 这是重置处理程序的拇指模式地址。当要在拇指模式下执行功能时,其地址的第一位设置为 1。

测试它

该程序是有效的LM3S6965程序;我们可以在虚拟微控制器 (QEMU) 中执行它来测试它。

$ # this program will block
$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -gdb tcp::3333 \
      -S \
      -nographic \
      -kernel target/thumbv7m-none-eabi/debug/app
$ # on a different terminal
$ arm-none-eabi-gdb -q target/thumbv7m-none-eabi/debug/app
Reading symbols from target/thumbv7m-none-eabi/debug/app...done.

(gdb) target remote :3333
Remote debugging using :3333
Reset () at src/main.rs:8
8       pub unsafe extern "C" fn Reset() -> ! {

(gdb) # the SP has the initial value we programmed in the vector table
(gdb) print/x $sp
$1 = 0x20010000

(gdb) step
9           let _x = 42;

(gdb) step
12          loop {}

(gdb) # next we inspect the stack variable `_x`
(gdb) print _x
$2 = 42

(gdb) print &_x
$3 = (i32 *) 0x2000fffc

(gdb) quit

A main interface

我们现在有一个最小的工作程序,但我们需要以最终用户可以在其上构建安全程序的方式对其进行打包。在本节中,我们将实现一个main类似于标准 Rust 程序使用的接口。

首先,我们将二进制 crate 转换为 library crate:

$ mv src/main.rs src/lib.rs

然后将其重命名为rt代表“运行时”的名称。

$ sed -i s/app/rt/ Cargo.toml

$ head -n4 Cargo.toml
[package]
edition = "2018"
name = "rt" # <-
version = "0.1.0"

第一个更改是让重置处理程序调用外部main函数:

$ head -n13 src/lib.rs
#![no_std]

use core::panic::PanicInfo;

// CHANGED!
#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

我们还删除了该#![no_main]属性,因为它对库箱没有影响。

有这么出现在这个阶段正交问题:如若rt 库提供了一个标准恐慌行为,还是应该不是提供一个 #[panic_handler]功能,让最终用户选择恐慌行为?本文档不会深入研究该问题,并且为简单起见,将#[panic_handler]rtcrate 中保留虚拟函数。但是,我们想通知读者还有其他选择。

第二个更改涉及将我们之前编写的链接描述文件提供给应用程序包。链接器将在库搜索路径 ( -L) 和调用它的目录中搜索链接器脚本。应用程序包不需要携带 的副本,link.x因此我们将rt使用构建脚本将链接器脚本放在库搜索路径中。

$ # create a build.rs file in the root of `rt` with these contents
$ cat build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    Ok(())
}

现在,用户可以编写一个应用程序来公开main符号并将其链接到rtcrate。在rt将给予该节目内存布局护理。

$ cd ..

$ cargo new --edition 2018 --bin app

$ cd app

$ # modify Cargo.toml to include the `rt` crate as a dependency
$ tail -n2 Cargo.toml
[dependencies]
rt = { path = "../rt" }
$ # copy over the config file that sets a default target and tweaks the linker invocation
$ cp -r ../rt/.cargo .

$ # change the contents of `main.rs` to
$ cat src/main.rs
#![no_std]
#![no_main]

extern crate rt;

#[no_mangle]
pub fn main() -> ! {
    let _x = 42;

    loop {}
}

反汇编将类似,但现在将包括用户main功能。

$ cargo objdump --bin app -- -d -no-show-raw-insn
app:    file format elf32-littlearm


Disassembly of section .text:

: sub sp, #4 movs r0, #42 str r0, [sp] b #-2 b #-4 : push {r7, lr} mov r7, sp bl #-18 trap

使其类型安全

main接口的工作原理,但它很容易理解错误。例如,用户可以编写main 为非发散函数,并且不会出现编译时错误和未定义的行为(编译器会错误优化程序)。

我们可以通过向用户公开宏而不是符号接口来添加类型安全性。在 rtcrate 中,我们可以编写这个宏:

$ tail -n12 ../rt/src/lib.rs
#[macro_export]
macro_rules! entry {
    ($path:path) => {
        #[export_name = "main"]
        pub unsafe fn __main() -> ! {
            // type check the given path
            let f: fn() -> ! = $path;

            f()
        }
    }
}

然后应用程序编写者可以像这样调用它:

$ cat src/main.rs
#![no_std]
#![no_main]

use rt::entry;

entry!(main);

fn main() -> ! {
    let _x = 42;

    loop {}
}

现在,如果作者将 的签名更改main为非发散函数,例如fn().

主线前的生活

rt看起来不错,但功能不完整!针对它编写的应用程序不能使用 static变量或字符串文字,因为rt的链接器脚本没有定义标准的 .bss,.data.rodata部分。让我们解决这个问题!

第一步是在链接描述文件中定义这些部分:

$ # showing just a fragment of the file
$ sed -n 25,46p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* NEW! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    *(.bss .bss.*);
  } > RAM

  .data :
  {
    *(.data .data.*);
  } > RAM

  /DISCARD/ :

他们只是重新导出输入部分并指定每个输出部分将位于哪个内存区域。

通过这些更改,将编译以下程序:

#![no_std]
#![no_main]

use rt::entry;

entry!(main);

static RODATA: &[u8] = b"Hello, world!";
static mut BSS: u8 = 0;
static mut DATA: u16 = 1;

fn main() -> ! {
    let _x = RODATA;
    let _y = unsafe { &BSS };
    let _z = unsafe { &DATA };

    loop {}
}

但是,如果您在真实硬件上运行此程序并对其进行调试,您将观察到static 变量BSSDATA没有值0并且1到了时间main。相反,这些变量将具有垃圾值。问题是在设备上电后 RAM 的内容是随机的。如果您在 QEMU 中运行程序,您将无法观察到这种效果。

事实上,如果您的程序static在执行写入之前读取任何变量,那么您的程序具有未定义的行为。让我们通过static在调用之前初始化所有变量来解决这个问题main

我们需要稍微调整链接器脚本以进行 RAM 初始化:

$ # showing just a fragment of the file
$ sed -n 25,52p ../rt/link.x
  .text :
  {
    *(.text .text.*);
  } > FLASH

  /* CHANGED! */
  .rodata :
  {
    *(.rodata .rodata.*);
  } > FLASH

  .bss :
  {
    _sbss = .;
    *(.bss .bss.*);
    _ebss = .;
  } > RAM

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))
  {
    _sdata = .;
    *(.data .data.*);
    _edata = .;
  } > RAM

  _sidata = LOADADDR(.data);

  /DISCARD/ :

让我们来看看这些变化的细节:

    _sbss = .;
    _ebss = .;
    _sdata = .;
    _edata = .;

我们将符号与.bss.data节的开始和结束地址相关联,稍后我们将在 Rust 代码中使用它们。

  .data : AT(ADDR(.rodata) + SIZEOF(.rodata))

我们将节的加载内存地址 (LMA) 设置为.data节的末尾.rodata 。该.data包含static具有非零初始值变量; 该部分的虚拟内存地址 (VMA).data位于 RAM 中的某个位置——这是static变量所在的位置。static但是,这些变量的初始值必须分配在非易失性存储器 (Flash) 中;LMA 是闪存中存储这些初始值的位置。

  _sidata = LOADADDR(.data);

最后,我们将一个符号与 的 LMA 相关联.data

在 Rust 方面,我们将.bss部分归零并初始化.data部分。我们可以从 Rust 代码中引用我们在链接描述文件中创建的符号。这些符号的地址1.bss.data段的边界。

更新后的重置处理程序如下所示:

$ head -n32 ../rt/src/lib.rs
#![no_std]

use core::panic::PanicInfo;
use core::ptr;

#[no_mangle]
pub unsafe extern "C" fn Reset() -> ! {
    // NEW!
    // Initialize RAM
    extern "C" {
        static mut _sbss: u8;
        static mut _ebss: u8;

        static mut _sdata: u8;
        static mut _edata: u8;
        static _sidata: u8;
    }

    let count = &_ebss as *const u8 as usize - &_sbss as *const u8 as usize;
    ptr::write_bytes(&mut _sbss as *mut u8, 0, count);

    let count = &_edata as *const u8 as usize - &_sdata as *const u8 as usize;
    ptr::copy_nonoverlapping(&_sidata as *const u8, &mut _sdata as *mut u8, count);

    // Call user entry point
    extern "Rust" {
        fn main() -> !;
    }

    main()
}

现在最终用户可以直接或间接地使用static变量而不会遇到未定义的行为!

在上面的代码中,我们以字节方式执行了内存初始化。可以强制将.bss.data部分对齐到 4 个字节。然后可以在 Rust 代码中使用这个事实来执行逐字初始化,同时省略对齐检查。如果您有兴趣了解如何实现这一点,请查看cortex-m-rt板条箱。

Exception handling

在“内存布局”部分,我们决定从简单开始,忽略异常处理。在本节中,我们将添加对处理它们的支持;这是如何在稳定的 Rust 中实现编译时可覆盖行为的示例(即不依赖不稳定的#[linkage = "weak"]属性,这会使符号变弱)。

背景资料

简而言之,异常是 Cortex-M 和其他架构提供的一种机制,用于让应用程序响应异步事件,通常是外部事件。大多数人都知道的最突出的异常类型是经典(硬件)中断。

Cortex-M 异常机制的工作原理是这样的:当处理器接收到与某种异常相关的信号或事件时,它会暂停当前子程序的执行(通过将状态存储在调用堆栈中),然后继续执行相应的子程序异常处理程序,另一个子程序,在一个新的堆栈帧中。在完成异常处理程序的执行(即从它返回)后,处理器恢复执行挂起的子程序。

处理器使用向量表来决定要执行的处理程序。表中的每个条目都包含一个指向处理程序的指针,每个条目对应不同的异常类型。例如,第二个条目是重置处理程序,第三个条目是 NMI(不可屏蔽中断)处理程序,依此类推。

如前所述,处理器期望向量表位于内存中的某个特定位置,并且其中的每个条目都可能在运行时被处理器使用。因此,条目必须始终包含有效值。此外,我们希望rtcrate 是灵活的,以便最终用户可以自定义每个异常处理程序的行为。最后,向量表驻留在只读内存中,或者更确切地说是在不容易修改的内存中,因此用户必须静态注册处理程序,而不是在运行时。

为了满足所有这些约束,我们将为crate 中向量表的所有条目分配一个默认rt,但使这些值有点弱,以便最终用户在编译时覆盖它们。

Rust side

让我们看看如何实现所有这些。为简单起见,我们将只处理向量表的前 16 个条目;这些条目不是特定于设备的,因此它们在任何类型的 Cortex-M 微控制器上都具有相同的功能。

我们要做的第一件事是在rtcrate 的代码中创建一个向量数组(指向异常处理程序的指针) :

$ sed -n 56,91p ../rt/src/lib.rs
pub union Vector {
    reserved: u32,
    handler: unsafe extern "C" fn(),
}

extern "C" {
    fn NMI();
    fn HardFault();
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFault },
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];

向量表中的一些条目是保留的;ARM 文档指出应该为它们分配值,0因此我们使用联合来做到这一点。必须指向处理程序的条目使用外部函数;这很重要,因为它让最终用户 提供实际的函数定义。

接下来,我们在 Rust 代码中定义一个默认的异常处理程序。最终用户未分配处理程序的异常将使用此默认处理程序。

$ tail -n4 ../rt/src/lib.rs
#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}

链接器脚本端

在链接描述文件端,我们将这些新的异常向量放在重置向量之后。

$ sed -n 12,25p ../rt/link.x
EXTERN(RESET_VECTOR);
EXTERN(EXCEPTIONS); /* <- NEW */

SECTIONS
{
  .vector_table ORIGIN(FLASH) :
  {
    /* First entry: initial Stack Pointer value */
    LONG(ORIGIN(RAM) + LENGTH(RAM));

    /* Second entry: reset vector */
    KEEP(*(.vector_table.reset_vector));

    /* The next 14 entries are exception vectors */
    KEEP(*(.vector_table.exceptions)); /* <- NEW */
  } > FLASH

我们用来PROVIDE为我们在rtNMI 和上面的其他人)中未定义的处理程序提供默认值:

$ tail -n8 ../rt/link.x
PROVIDE(NMI = DefaultExceptionHandler);
PROVIDE(HardFault = DefaultExceptionHandler);
PROVIDE(MemManage = DefaultExceptionHandler);
PROVIDE(BusFault = DefaultExceptionHandler);
PROVIDE(UsageFault = DefaultExceptionHandler);
PROVIDE(SVCall = DefaultExceptionHandler);
PROVIDE(PendSV = DefaultExceptionHandler);
PROVIDE(SysTick = DefaultExceptionHandler);

PROVIDE仅当检查所有输入目标文件后等号左侧的符号仍未定义时才生效。在这种情况下,用户没有为相应的异常实现处理程序。

测试它

就是这样!该rt箱子现在有异常处理程序的支持。我们可以使用以下应用程序对其进行测试:

注意:事实证明在 QEMU 中很难产生异常。在真实硬件上,读取无效的内存地址(即在 Flash 和 RAM 区域之外)就足够了,但 QEMU 很乐意接受该操作并返回零。陷阱指令适用于 QEMU 和硬件,但不幸的是它在稳定版上不可用,因此您必须暂时切换到 nightly 才能运行此示例和下一个示例。

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    // this executes the undefined instruction (UDF) and causes a HardFault exception
    intrinsics::abort()
}
(gdb) target remote :3333
Remote debugging using :3333
Reset () at ../rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b DefaultExceptionHandler
Breakpoint 1 at 0xec: file ../rt/src/lib.rs, line 95.

(gdb) continue
Continuing.

Breakpoint 1, DefaultExceptionHandler ()
    at ../rt/src/lib.rs:95
95          loop {}

(gdb) list
90          Vector { handler: SysTick },
91      ];
92
93      #[no_mangle]
94      pub extern "C" fn DefaultExceptionHandler() {
95          loop {}
96      }

为了完整起见,这里是程序优化版本的反汇编:

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex
app:    file format elf32-littlearm


Disassembly of section .text:

: trap trap : push {r7, lr} mov r7, sp movw r1, #0x0 movw r0, #0x0 movt r1, #0x2000 movt r0, #0x2000 subs r1, r1, r0 bl #0x34 movw r1, #0x0 movw r0, #0x0 movt r1, #0x2000 movt r0, #0x2000 subs r2, r1, r0 movw r1, #0x16c movt r1, #0x0 bl #0x8 bl #-0x40 trap $ cargo objdump --bin app --release -- -s -j .vector_table app: file format elf32-littlearm Contents of section .vector_table: 0000 00000120 45000000 83000000 83000000 ... E........... 0010 83000000 83000000 83000000 00000000 ................ 0020 00000000 00000000 00000000 83000000 ................ 0030 00000000 00000000 83000000 83000000 ................

向量表现在类似于本书迄今为止所有代码片段的结果。总结一下:

  • 在前面内存章节的

    检查

    部分中,我们了解到:

    • 向量表中的第一个条目包含堆栈指针的初始值。

    • Objdump 以little endian格式打印,因此堆栈从 0x2001_0000.

    • 第二个入口指向 address

      0x0000_0045

      ,即重置处理程序。

      • 重置处理程序的地址可以在上面的反汇编中看到,是0x44.
      • 由于对齐要求,设置为 1 的第一位不会改变地址。相反,它会导致函数以拇指模式执行。
  • 之后,可以看到在

    0x7f

    和之间交替的地址模式

    0x00

    • 看上面的反汇编,很明显0x7f指的是 DefaultExceptionHandler0x7e以拇指模式执行)。
    • 将模式与本章前面设置的向量表(参见 的定义pub static EXCEPTIONS)与Cortex-M 的向量表布局交叉引用,很明显DefaultExceptionHandler每次出现相应的处理程序条目时都会出现的地址 出现在表中。
    • 反过来,也可以看出,Rust 代码中向量表数据结构的布局与 Cortex-M 向量表中的所有保留槽对齐。因此,所有保留时隙都正确设置为零值。

覆盖处理程序

要覆盖异常处理程序,用户必须提供一个函数,其符号名称与我们在 中使用的名称完全匹配EXCEPTIONS

#![feature(core_intrinsics)]
#![no_main]
#![no_std]

use core::intrinsics;

use rt::entry;

entry!(main);

fn main() -> ! {
    intrinsics::abort()
}

#[no_mangle]
pub extern "C" fn HardFault() -> ! {
    // do something interesting here
    loop {}
}

你可以在 QEMU 中测试

(gdb) target remote :3333
Remote debugging using :3333
Reset () at /home/japaric/rust/embedonomicon/ci/exceptions/rt/src/lib.rs:7
7       pub unsafe extern "C" fn Reset() -> ! {

(gdb) b HardFault
Breakpoint 1 at 0x44: file src/main.rs, line 18.

(gdb) continue
Continuing.

Breakpoint 1, HardFault () at src/main.rs:18
18          loop {}

(gdb) list
13      }
14
15      #[no_mangle]
16      pub extern "C" fn HardFault() -> ! {
17          // do something interesting here
18          loop {}
19      }

程序现在执行用户定义的HardFault函数,而不是 DefaultExceptionHandlerrtcrate 中。

就像我们对main接口的第一次尝试一样,这个第一个实现存在没有类型安全的问题。错误输入异常的名称也很容易,但这不会产生错误或警告。相反,用户定义的处理程序被简单地忽略。这些问题可以通过象的宏被固定exception!在定义的宏cortex-m-rtv0.5.x或 exception在属性cortex-m-rtv0.6.x.

Assembly on stable

到目前为止,我们已经设法启动设备并处理中断,而无需一行汇编。这真是一个壮举!但是根据您所针对的架构,您可能需要一些组装才能达到这一点。还有一些操作,比如需要汇编的上下文切换等。

问题是内联汇编 ( asm!) 和自由形式汇编 ( global_asm!) 都不稳定,并且无法估计它们何时会稳定下来,因此您不能在 stable 上使用它们。这不是一个大问题,因为我们将在此处记录一些解决方法。

为了激发本节的内容,我们将调整HardFault处理程序以提供有关生成异常的堆栈帧的信息。

这是我们想要做的:

我们不会让用户直接将他们的HardFault处理程序放在向量表中,而是让rtcrate 将一个蹦床放到向量表中的用户定义的HardFault 处理程序中。

$ tail -n36 ../rt/src/lib.rs
extern "C" {
    fn NMI();
    fn HardFaultTrampoline(); // <- CHANGED!
    fn MemManage();
    fn BusFault();
    fn UsageFault();
    fn SVCall();
    fn PendSV();
    fn SysTick();
}

#[link_section = ".vector_table.exceptions"]
#[no_mangle]
pub static EXCEPTIONS: [Vector; 14] = [
    Vector { handler: NMI },
    Vector { handler: HardFaultTrampoline }, // <- CHANGED!
    Vector { handler: MemManage },
    Vector { handler: BusFault },
    Vector {
        handler: UsageFault,
    },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: SVCall },
    Vector { reserved: 0 },
    Vector { reserved: 0 },
    Vector { handler: PendSV },
    Vector { handler: SysTick },
];

#[no_mangle]
pub extern "C" fn DefaultExceptionHandler() {
    loop {}
}

这个蹦床将读取堆栈指针,然后调用用户HardFault 处理程序。蹦床必须用汇编编写:

  mrs r0, MSP
  b HardFault

由于 ARM ABI 的工作方式,这将主堆栈指针 (MSP) 设置为HardFault函数/例程的第一个参数。这个 MSP 值也恰好是一个指向由异常压入堆栈的寄存器的指针。通过这些更改,用户HardFault处理程序现在必须具有签名 fn(&StackedRegisters) -> !

.s 档案

稳定程序集的一种方法是将程序集写入外部文件:

$ cat ../rt/asm.s
  .section .text.HardFaultTrampoline
  .global HardFaultTrampoline
  .thumb_func
HardFaultTrampoline:
  mrs r0, MSP
  b HardFault

并使用cccrate 的构建脚本中的rtcrate 将该文件组装成目标文件 ( .o),然后组装成存档 ( .a)。

$ cat ../rt/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

use cc::Build;

fn main() -> Result<(), Box> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // assemble the `asm.s` file
    Build::new().file("asm.s").compile("asm"); // <- NEW!

    // rebuild if `asm.s` changed
    println!("cargo:rerun-if-changed=asm.s"); // <- NEW!

    Ok(())
}
$ tail -n2 ../rt/Cargo.toml
[build-dependencies]
cc = "1.0.25"

就是这样!

我们可以HardFaultTrampoline 通过编写一个非常简单的程序来确认向量表包含一个指向的指针。

#![no_main]
#![no_std]

use rt::entry;

entry!(main);

fn main() -> ! {
    loop {}
}

#[allow(non_snake_case)]
#[no_mangle]
pub fn HardFault(_ef: *const u32) -> ! {
    loop {}
}

下面是拆解。看看地址HardFaultTrampoline

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex
app:    file format elf32-littlearm


Disassembly of section .text:

:
                   b    #-0x4 

: b #-0x4
: push {r7, lr} mov r7, sp bl #-0xa trap : b #-0x4 : mrs r0, msp b #-0x18

注意:为了使这个反汇编更小,我注释掉了 RAM 的初始化

现在看看向量表。第四个条目应该是HardFaultTrampoline加一的地址 。

$ cargo objdump --bin app --release -- -s -j .vector_table
app:    file format elf32-littlearm

Contents of section .vector_table:
 0000 00000120 45000000 4f000000 51000000  ... E...O...Q...
 0010 4f000000 4f000000 4f000000 00000000  O...O...O.......
 0020 00000000 00000000 00000000 4f000000  ............O...
 0030 00000000 00000000 4f000000 4f000000  ........O...O...

.o/.a文件

使用cccrate的缺点是它需要在构建机器上安装一些汇编程序。例如,当以 ARM Cortex-M 为目标时,cccratearm-none-eabi-gcc用作汇编器。

我们可以用rtcrate运送一个预组装的文件,而不是在构建机器上组装文件。这样,构建机器上就不需要汇编程序。但是,您仍然需要在打包和发布 crate 的机器上安装一个汇编程序。

程序集 ( .s) 文件与其编译 版本之间没有太大区别:对象 ( .o) 文件。汇编器不做任何优化;它只是为目标架构选择正确的目标文件格式。

Cargo 支持将档案 ( .a) 与 crate捆绑在一起。我们可以使用该ar命令将目标文件打包成档案,然后将档案与 crate 捆绑在一起。事实上,这就是cc板条箱的作用;你可以看到它调用通过搜索指定文件中的命令outputtarget目录中。

$ grep running $(find target -name output)
running: "arm-none-eabi-gcc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-g" "-fno-omit-frame-pointer" "-mthumb" "-march=armv7-m" "-Wall" "-Wextra" "-o" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o" "-c" "asm.s"
running: "ar" "crs" "/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/libasm.a" "/home/japaric/rust-embedded/embedonomicon/ci/asm/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out/asm.o"
$ grep cargo $(find target -name output)
cargo:rustc-link-search=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out
cargo:rustc-link-lib=static=asm
cargo:rustc-link-search=native=/tmp/app/target/thumbv7m-none-eabi/debug/build/rt-6ee84e54724f2044/out

我们会做一些类似的事情来生成一个存档。

$ # most of flags `cc` uses have no effect when assembling so we drop them
$ arm-none-eabi-as -march=armv7-m asm.s -o asm.o

$ ar crs librt.a asm.o

$ arm-none-eabi-objdump -Cd librt.a
In archive librt.a:

asm.o:     file format elf32-littlearm


Disassembly of section .text.HardFaultTrampoline:

00000000 :
   0:    f3ef 8008     mrs    r0, MSP
   4:    e7fe          b.n    0 

接下来我们修改构建脚本以将此存档与rtrlib捆绑在一起。

$ cat ../rt/build.rs
use std::{
    env,
    error::Error,
    fs::{self, File},
    io::Write,
    path::PathBuf,
};

fn main() -> Result<(), Box> {
    // build directory for this crate
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());

    // extend the library search path
    println!("cargo:rustc-link-search={}", out_dir.display());

    // put `link.x` in the build directory
    File::create(out_dir.join("link.x"))?.write_all(include_bytes!("link.x"))?;

    // link to `librt.a`
    fs::copy("librt.a", out_dir.join("librt.a"))?; // <- NEW!
    println!("cargo:rustc-link-lib=static=rt"); // <- NEW!

    // rebuild if `librt.a` changed
    println!("cargo:rerun-if-changed=librt.a"); // <- NEW!

    Ok(())
}

现在我们可以用之前的简单程序测试这个新版本,我们将得到相同的输出。

$ cargo objdump --bin app --release -- -d -no-show-raw-insn -print-imm-hex
app:    file format elf32-littlearm


Disassembly of section .text:

:
                   b    #-0x4 

: b #-0x4
: push {r7, lr} mov r7, sp bl #-0xa trap : b #-0x4 : mrs r0, msp b #-0x18

注意:和之前一样,我已经注释掉了 RAM 初始化以使反汇编更小。

$ cargo objdump --bin app --release -- -s -j .vector_table
app:    file format elf32-littlearm

Contents of section .vector_table:
 0000 00000120 45000000 4f000000 51000000  ... E...O...Q...
 0010 4f000000 4f000000 4f000000 00000000  O...O...O.......
 0020 00000000 00000000 00000000 4f000000  ............O...
 0030 00000000 00000000 4f000000 4f000000  ........O...O...

运送预组装档案的缺点是,在最坏的情况下,您需要为您的库支持的每个编译目标运送一个构建工件。

Logging with symbols

本节将向您展示如何利用符号和 ELF 格式来实现超廉价的日志记录。

任意符号

每当我们需要在 crate 之间使用稳定的符号接口时,我们主要使用no_mangle属性,有时使用export_name属性。该 export_name属性采用一个字符串,该字符串成为符号的名称,而#[no_mangle]基本上是#[export_name = <item-name>].

事实证明,我们不仅限于单个单词名称;我们可以使用任意字符串,例如句子,作为export_name属性的参数。至少当输出格式是 ELF 时,任何不包含空字节的东西都可以。

让我们来看看:

$ cargo new --lib foo

$ cat foo/src/lib.rs
#[export_name = "Hello, world!"]
#[used]
static A: u8 = 0;

#[export_name = "こんにちは"]
#[used]
static B: u8 = 0;
$ ( cd foo && cargo nm --lib )
foo-d26a39c34b4e80ce.3lnzqy0jbpxj4pld.rcgu.o:
0000000000000000 r Hello, world!
0000000000000000 V __rustc_debug_gdb_scripts_section__
0000000000000000 r こんにちは

你能看出这是怎么回事吗?

编码

这是我们要做的:我们将为static每个日志消息创建一个变量,但不是将消息存储变量中,而是将消息存储在变量的符号名称中。我们将记录的不是static变量的内容,而是它们的地址。

只要static变量不是零大小,每个变量都会有不同的地址。我们在这里所做的是有效地将每条消息编码为一个唯一标识符,这恰好是变量地址。日志系统的某些部分必须将此 ID 解码回消息中。

让我们写一些代码来说明这个想法。

在这个例子中,我们需要某种方式来进行 I/O,因此我们将使用 cortex-m-semihostingcrate 来实现。半主机是一种让目标设备借用主机 I/O 功能的技术;这里的主机通常是指正在调试目标设备的机器。在我们的例子中,QEMU 支持开箱即用的半主机,因此不需要调试器。在真实设备上,您将有其他方式进行 I/O,如串行端口;在这种情况下,我们使用半主机,因为这是在 QEMU 上进行 I/O 的最简单方法。

这是代码

#![no_main]
#![no_std]

use core::fmt::Write;
use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    static A: u8 = 0;

    let _ = writeln!(hstdout, "{:#x}", &A as *const u8 as usize);

    #[export_name = "Goodbye"]
    static B: u8 = 0;

    let _ = writeln!(hstdout, "{:#x}", &B as *const u8 as usize);

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

我们还使用debug::exitAPI 来让程序终止 QEMU 进程。这很方便,因此我们不必手动终止 QEMU 进程。

dependencies是 Cargo.toml的部分:

[dependencies]
cortex-m-semihosting = "0.3.1"
rt = { path = "../rt" }

现在我们可以构建程序

$ cargo build

要运行它,我们必须将--semihosting-config标志添加到我们的 QEMU 调用中:

$ qemu-system-arm \
      -cpu cortex-m3 \
      -machine lm3s6965evb \
      -nographic \
      -semihosting-config enable=on,target=native \
      -kernel target/thumbv7m-none-eabi/debug/app
0x1fe0
0x1fe1

注意:这些地址可能不是您在本地获得的地址,因为static在更改工具链时不能保证变量的地址保持不变(例如优化可能已经改进)。

现在我们有两个地址打印到控制台。

解码

我们如何将这些地址转换为字符串?答案在 ELF 文件的符号表中。

$ cargo objdump --bin app -- -t | grep '\.rodata\s*0*1\b'
00001fe1 g       .rodata         00000001 Goodbye
00001fe0 g       .rodata         00000001 Hello, world!
$ # first column is the symbol address; last column is the symbol name

objdump -t打印符号表。该表包含所有符号,但我们只在该.rodata部分中查找大小为 1 个字节的符号(我们的变量具有 type u8)。

需要注意的是,在优化程序时,符号的地址可能会发生变化。让我们检查一下。

普罗蒂普您可以设置target.thumbv7m-none-eabi.runner从之前的长QEMU命令(qemu-system-arm -cpu (..) -kernel)的货物配置文件(中.cargo/conifg)有cargo run使用该亚军执行二进制输出。

$ head -n2 .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
$ cargo run --release
     Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/app`
0xb9c
0xb9d
$ cargo objdump --bin app --release -- -t | grep '\.rodata\s*0*1\b'
00000b9d g     O .rodata    00000001 Goodbye
00000b9c g     O .rodata    00000001 Hello, world!

因此,请确保始终在您执行的 ELF 文件中查找字符串。

当然,在 ELF 文件中查找字符串的过程可以使用解析.symtabELF 文件中包含的符号表(节)的工具来自动化。实现这样的工具超出了本书的范围,留给读者作为练习。

实现零成本

我们能做得更好吗?我们可以!

当前的实现将static变量放在 中.rodata,这意味着即使我们从不使用它们的内容,它们也占用 Flash 中的大小。使用一点链接脚本魔法,我们可以让它们在 Flash 中占据空间。

$ cat log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log);
  }
}

我们将把static变量放在这个新的输出.log部分。此链接描述文件将收集.log输入目标文件部分中的所有符号,并将它们放入输出.log部分。我们已经在内存布局章节中看到了这种模式。

这里的新(INFO)部分是零件;这告诉链接器这个部分是一个不可分配的部分。不可分配的部分作为元数据保存在 ELF 二进制文件中,但它们不会加载到目标设备上。

我们还指定了这个输出部分的起始地址:0in .log 0 (INFO)

我们可以做的另一个改进是从格式化 I/O ( fmt::Write) 切换到二进制 I/O,即将地址作为字节而不是字符串发送到主机。

二进制序列化可能很困难,但我们将通过将每个地址序列化为单个字节来使事情变得非常简单。使用这种方法,我们不必担心字节序或框架。这种格式的缺点是单个字节最多只能表示 256 个不同的地址。

让我们进行这些更改:

#![no_main]
#![no_std]

use cortex_m_semihosting::{debug, hio};

use rt::entry;

entry!(main);

fn main() -> ! {
    let mut hstdout = hio::hstdout().unwrap();

    #[export_name = "Hello, world!"]
    #[link_section = ".log"] // <- NEW!
    static A: u8 = 0;

    let address = &A as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- CHANGED!

    #[export_name = "Goodbye"]
    #[link_section = ".log"] // <- NEW!
    static B: u8 = 0;

    let address = &B as *const u8 as usize as u8;
    hstdout.write_all(&[address]).unwrap(); // <- CHANGED!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

在运行之前,您必须附加-Tlog.x到传递给链接器的参数。这可以在 Cargo 配置文件中完成。

$ cat .cargo/config
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
rustflags = [
  "-C", "link-arg=-Tlink.x",
  "-C", "link-arg=-Tlog.x", # <- NEW!
]

[build]
target = "thumbv7m-none-eabi"

现在你可以运行它了!由于输出现在具有二进制格式,我们将通过xxd命令将其通过管道将其重新格式化为十六进制字符串。

$ cargo run | xxd -p
0001

地址是0x000x01。现在让我们看看符号表。

$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log    00000001 Goodbye
00000000 g     O .log    00000001 Hello, world!

有我们的字符串。你会注意到他们的地址现在从零开始;这是因为我们为输出.log部分设置了起始地址。

每个变量的大小为 1 个字节,因为我们将u8其用作它们的类型。如果我们使用类似的东西,u16那么所有地址都会是偶数,我们将无法有效地使用所有地址空间 ( 0...255)。

包装起来

您已经注意到记录字符串的步骤总是相同的,因此我们可以将它们重构为一个存在于其自己的 crate 中的宏。此外,我们可以通过抽象特征背后的 I/O 部分来使日志库更可重用。

$ cargo new --lib log

$ cat log/src/lib.rs
#![no_std]

pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

鉴于这个库依赖于.log部分,它应该负责提供log.x链接描述文件,所以让我们实现这一点。

$ mv log.x ../log/
$ cat ../log/build.rs
use std::{env, error::Error, fs::File, io::Write, path::PathBuf};

fn main() -> Result<(), Box> {
    // Put the linker script somewhere the linker can find it
    let out = PathBuf::from(env::var("OUT_DIR")?);

    File::create(out.join("log.x"))?.write_all(include_bytes!("log.x"))?;

    println!("cargo:rustc-link-search={}", out.display());

    Ok(())
}

现在我们可以重构我们的应用程序以使用log!宏:

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{log, Log};
use rt::entry;

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    let _ = log!(logger, "Hello, world!");

    let _ = log!(logger, "Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

不要忘记更新Cargo.toml文件以依赖新的logcrate。

$ tail -n4 Cargo.toml
[dependencies]
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log    00000001 Goodbye
00000000 g     O .log    00000001 Hello, world!

和以前一样的输出!

奖励:多个日志级别

许多日志框架提供了在不同日志级别记录消息的方法。这些日志级别传达了消息的严重性:“这是一个错误”、“这只是一个警告”等。这些日志级别可用于在搜索例如错误消息时过滤掉不重要的消息。

我们可以扩展我们的日志库以支持日志级别,而不会增加其占用空间。下面是我们将如何做到这一点:

我们有一个用于消息的平面地址空间:从0255(包括)。为了简单起见,假设我们只想区分错误消息和警告消息。我们可以把在地址空间的开始所有的错误消息,并且所有的警告消息的错误消息。如果解码器知道第一条警告消息的地址,则它可以对消息进行分类。这个想法可以扩展到支持两个以上的日志级别。

让我们通过用log两个新宏替换宏来测试这个想法:error!warn!

$ cat ../log/src/lib.rs
#![no_std]

pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

/// Logs messages at the ERROR log level
#[macro_export]
macro_rules! error {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.error"] // <- CHANGED!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

/// Logs messages at the WARNING log level
#[macro_export]
macro_rules! warn {
    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log.warning"] // <- CHANGED!
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

我们通过将消息放在不同的链接部分来区分错误和警告。

接下来我们要做的是更新链接描述文件,将错误信息放在警告信息之前。

$ cat ../log/log.x
SECTIONS
{
  .log 0 (INFO) : {
    *(.log.error);
    __log_warning_start__ = .;
    *(.log.warning);
  }
}

我们还为__log_warning_start__错误和警告之间的边界命名为 。该符号的地址将是第一条警告消息的地址。

我们现在可以更新应用程序以使用这些新的宏。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{error, warn, Log};
use rt::entry;

entry!(main);

fn main() -> ! {
    let hstdout = hio::hstdout().unwrap();
    let mut logger = Logger { hstdout };

    let _ = warn!(logger, "Hello, world!"); // <- CHANGED!

    let _ = error!(logger, "Goodbye"); // <- CHANGED!

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

struct Logger {
    hstdout: HStdout,
}

impl Log for Logger {
    type Error = ();

    fn log(&mut self, address: u8) -> Result<(), ()> {
        self.hstdout.write_all(&[address])
    }
}

输出不会有太大变化:

$ cargo run | xxd -p
0100

我们仍然在输出中得到两个字节,但错误的地址为 0,警告的地址为 1,即使警告是最先记录的。

现在看看符号表。

$ cargo objdump --bin app -- -t | grep '\.log'
00000000 g     O .log    00000001 Goodbye
00000001 g     O .log    00000001 Hello, world!
00000001 g       .log    00000000 __log_warning_start__

现在__log_warning_start__,该.log部分中有一个额外的符号。这个符号的地址是第一条警告信息的地址。地址低于此值的符号是错误,其余符号是警告。

使用适当的解码器,您可以从所有这些信息中获得以下人类可读的输出:

WARNING Hello, world!
ERROR Goodbye

Global singletons(单例)

在本节中,我们将介绍如何实现全局共享单例。嵌入式 Rust 书籍涵盖了 Rust 非常独特的本地拥有的单身人士。全局单例本质上是您在 C 和 C++ 中看到的单例模式;它们并非特定于嵌入式开发,但由于它们涉及符号,因此它们似乎非常适合嵌入式开发。

TODO(资源团队)在启动时将“嵌入式 Rust 书”链接到单例部分

为了说明本节,我们将扩展我们在上一节中开发的记录器以支持全局日志记录。结果将#[global_allocator]与嵌入式 Rust 书中涵盖的功能非常相似 。

TODO(资源团队)#[global_allocator]在更稳定的位置链接到本书的收藏章节。

以下是我们想要做的总结:

在上一节中,我们创建了一个log!宏来通过特定的记录器记录消息,这是一个实现Logtrait的值。log!宏的语法是log!(logger, "String"). 我们想扩展宏,使其 log!("String")也能工作。使用logger-less 版本应该通过全局记录器记录消息;这就是std::println!工作原理。我们还需要一种机制来声明全局记录器是什么;这是类似于#[global_allocator].

可能是全局记录器是在 top crate 中声明的,也可能是全局记录器的类型是在 top crate 中定义的。在这种情况下,依赖项无法知道全局记录器的确切类型。为了支持这种情况,我们需要一些间接性。

log我们不会在crate中对全局记录器的类型进行硬编码,而是在该crate 中仅声明全局记录器的接口。这就是我们将添加一个新的特点,GlobalLoglog箱子。该log!宏还必须利用该性状。

$ cat ../log/src/lib.rs
#![no_std]

// NEW!
pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}

pub trait Log {
    type Error;

    fn log(&mut self, address: u8) -> Result<(), Self::Error>;
}

#[macro_export]
macro_rules! log {
    // NEW!
    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };

    ($logger:expr, $string:expr) => {{
        #[export_name = $string]
        #[link_section = ".log"]
        static SYMBOL: u8 = 0;

        $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
    }};
}

// NEW!
#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}

这里有很多东西要解开。

让我们从特征开始。

pub trait GlobalLog: Sync {
    fn log(&self, address: u8);
}

双方GlobalLogLog有一个log方法。不同之处在于它 GlobalLog.log采用对接收者的共享引用 ( &self)。这是必要的,因为全局记录器将是一个static变量。稍后再谈。

另一个区别是GlobalLog.log不返回 a Result。这意味着它不能向调用者报告错误。这不是对用于实现全局单例的特征的严格要求。全局单例中的错误处理很好,但是log! 宏的全局版本的所有用户都必须就错误类型达成一致。在这里,我们通过让GlobalLog实现者处理错误来稍微简化接口。

另一个区别是GlobalLog要求实现者是 Sync,即它可以在线程之间共享。这是对放置在static变量中的值的要求;它们的类型必须实现Sync trait。

在这一点上,可能不完全清楚为什么界面必须这样。板条箱的其他部分将使这一点更清楚,所以请继续阅读。

接下来是log!宏:

    ($string:expr) => {
        unsafe {
            extern "Rust" {
                static LOGGER: &'static dyn $crate::GlobalLog;
            }

            #[export_name = $string]
            #[link_section = ".log"]
            static SYMBOL: u8 = 0;

            $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
        }
    };

当没有特定$logger的调用时,宏使用一个extern static 被调用的变量LOGGER来记录消息。这个变量在别处定义的全局记录器;这就是我们使用extern块的原因。

我们需要声明一个类型,LOGGER否则代码不会进行类型检查。目前我们不知道 的具体类型,LOGGER但我们知道,或者更确切地说,它实现了GlobalLogtrait,因此我们可以在这里使用 trait 对象。

宏扩展的其余部分看起来与宏的本地版本的扩展非常相似,log!因此我不会在这里解释,因为它在前一章中解释

现在我们知道它LOGGER必须是一个 trait 对象,所以我们更清楚为什么我们ErrorGlobalLog. 如果我们没有省略,那么我们将需要ErrorLOGGER. 这就是我之前所说的“所有用户log!都需要就错误类型达成一致”的意思。

现在是最后一块:global_logger!宏。它可能是一个 proc 宏属性,但编写macro_rules!宏更容易。

#[macro_export]
macro_rules! global_logger {
    ($logger:expr) => {
        #[no_mangle]
        pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
    };
}

此宏创建使用的LOGGER变量log!。因为我们需要一个稳定的 ABI 接口,所以我们使用该no_mangle属性。这样,符号名称LOGGER将是“LOGGER”,这是log!宏所期望的。

另一个重要的一点是这个静态变量的类型必须与log!宏扩展中使用的类型完全匹配。如果它们不匹配,则会由于 ABI 不匹配而发生 Bad Stuff。

让我们编写一个使用此新全局记录器功能的示例。

$ cat src/main.rs
#![no_main]
#![no_std]

use cortex_m::interrupt;
use cortex_m_semihosting::{
    debug,
    hio::{self, HStdout},
};

use log::{global_logger, log, GlobalLog};
use rt::entry;

struct Logger;

global_logger!(Logger);

entry!(main);

fn main() -> ! {
    log!("Hello, world!");

    log!("Goodbye");

    debug::exit(debug::EXIT_SUCCESS);

    loop {}
}

impl GlobalLog for Logger {
    fn log(&self, address: u8) {
        // we use a critical section (`interrupt::free`) to make the access to the
        // `static mut` variable interrupt safe which is required for memory safety
        interrupt::free(|_| unsafe {
            static mut HSTDOUT: Option = None;

            // lazy initialization
            if HSTDOUT.is_none() {
                HSTDOUT = Some(hio::hstdout()?);
            }

            let hstdout = HSTDOUT.as_mut().unwrap();

            hstdout.write_all(&[address])
        }).ok(); // `.ok()` = ignore errors
    }
}

TODO(资源团队) 在稳定时使用cortex_m::Mutex而不是static mut变量const fn

我们必须添加cortex-m依赖项。

$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }

这是上一节中编写的示例之一的移植。输出与我们回到那里的输出相同。

$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g     O .log    00000001 Goodbye
00000000 g     O .log    00000001 Hello, world!

一些读者可能会担心全局单例的这种实现不是零成本,因为它使用涉及动态调度的 trait 对象,即通过 vtable 查找执行方法调用。

但是,LLVM 似乎足够聪明,可以在使用优化 / LTO 进行编译时消除动态调度。这可以通过在LOGGER符号表中搜索来确认 。

$ cargo objdump --bin app --release -- -t | grep LOGGER

如果static缺少,则意味着没有 vtable 并且 LLVM 能够将所有LOGGER.log调用转换为Logger.log调用。

Direct Memory Access (DMA)

本节涵盖围绕 DMA 传输构建内存安全 API 的核心要求。

DMA 外设用于与处理器的工作(主程序的执行)并行执行内存传输。DMA 传输或多或少相当于生成一个线程(请参阅thread::spawn)来执行memcpy. 我们将使用 fork-join 模型来说明内存安全 API 的要求。

考虑以下 DMA 原语:

/// A singleton that represents a single DMA channel (channel 1 in this case)
///
/// This singleton has exclusive access to the registers of the DMA channel 1
pub struct Dma1Channel1 {
    // ..
}

impl Dma1Channel1 {
    /// Data will be written to this `address`
    ///
    /// `inc` indicates whether the address will be incremented after every byte transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_destination_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// Data will be read from this `address`
    ///
    /// `inc` indicates whether the address will be incremented after every byte transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_source_address(&mut self, address: usize, inc: bool) {
        // ..
    }

    /// Number of bytes to transfer
    ///
    /// NOTE this performs a volatile write
    pub fn set_transfer_length(&mut self, len: usize) {
        // ..
    }

    /// Starts the DMA transfer
    ///
    /// NOTE this performs a volatile write
    pub fn start(&mut self) {
        // ..
    }

    /// Stops the DMA transfer
    ///
    /// NOTE this performs a volatile write
    pub fn stop(&mut self) {
        // ..
    }

    /// Returns `true` if there's a transfer in progress
    ///
    /// NOTE this performs a volatile read
    pub fn in_progress() -> bool {
        // ..
    }
}

假设Dma1Channel1静态配置为Serial1在一次性模式(即非循环模式)下与串行端口(AKA UART 或 USART)#1 一起使用。 Serial1提供以下阻塞API:

/// A singleton that represents serial port #1
pub struct Serial1 {
    // ..
}

impl Serial1 {
    /// Reads out a single byte
    ///
    /// NOTE: blocks if no byte is available to be read
    pub fn read(&mut self) -> Result<u8, Error> {
        // ..
    }

    /// Sends out a single byte
    ///
    /// NOTE: blocks if the output FIFO buffer is full
    pub fn write(&mut self, byte: u8) -> Result<(), Error> {
        // ..
    }
}

假设我们想将Serial1API扩展为 (a) 异步发送缓冲区和 (b) 异步填充缓冲区。

我们将从一个内存不安全的 API 开始,我们将对其进行迭代,直到它完全内存安全。在每个步骤中,我们将向您展示如何破坏 API,让您了解在处理异步内存操作时需要解决的问题。

A first stab

首先,让我们尝试使用Write::write_allAPI 作为参考。为了简单起见,让我们忽略所有错误处理。

/// A singleton that represents serial port #1
pub struct Serial1 {
    // NOTE: we extend this struct by adding the DMA channel singleton
    dma: Dma1Channel1,
    // ..
}

impl Serial1 {
    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<'a>(mut self, buffer: &'a [u8]) -> Transfer<&'a [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}

/// A DMA transfer
pub struct Transfer<B> {
    buffer: B,
}

impl<B> Transfer<B> {
    /// Returns `true` if the DMA transfer has finished
    pub fn is_done(&self) -> bool {
        !Dma1Channel1::in_progress()
    }

    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(self) -> B {
        // Busy wait until the transfer is done
        while !self.is_done() {}

        self.buffer
    }
}

注意: Transfer可以公开基于期货或生成器的 API,而不是上面显示的 API。这是一个 API 设计问题,与整个 API 的内存安全性关系不大,因此我们不会在本文中深入研究。

我们还可以实现Read::read_exact.

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<'a>(&mut self, buffer: &'a mut [u8]) -> Transfer<&'a mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        self.dma.start();

        Transfer { buffer }
    }
}

write_allAPI的使用方法如下:

fn write(serial: Serial1) {
    // fire and forget
    serial.write_all(b"Hello, world!\n");

    // do other stuff
}

这是使用read_exactAPI的示例:

fn read(mut serial: Serial1) {
    let mut buf = [0; 16];
    let t = serial.read_exact(&mut buf);

    // do other stuff

    t.wait();

    match buf.split(|b| *b == b'\n').next() {
        Some(b"some-command") => { /* do something */ }
        _ => { /* do something else */ }
    }
}

mem::forget

mem::forget是一个安全的 API。如果我们的 API 真的安全,那么我们应该能够同时使用两者而不会遇到未定义的行为。然而,事实并非如此。考虑以下示例:

fn unsound(mut serial: Serial1) {
    start(&mut serial);
    bar();
}

#[inline(never)]
fn start(serial: &mut Serial1) {
    let mut buf = [0; 16];

    // start a DMA transfer and forget the returned `Transfer` value
    mem::forget(serial.read_exact(&mut buf));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}

在这里,我们开始 DMA 传输 in start,以填充在堆栈上分配的数组,然后mem::forget是返回Transfer值。然后我们继续返回start并执行函数bar

这一系列操作会导致未定义的行为。DMA 传输写入堆栈内存,但该内存在start返回时被释放,然后被重新bar用于分配变量,如xy。在运行时,这可能会导致变量xy随机更改其值。DMA 传输还可以覆盖由函数的序言推入堆栈的状态(例如链接寄存器)bar

请注意,如果我们没有使用mem::forget, but mem::drop,则有可能使 makeTransfer的析构函数停止 DMA 传输,然后程序就会是安全的。但是不能依靠运行的析构函数来强制执行内存安全,因为mem::forget内存泄漏(参见 RC 循环)在 Rust 中是安全的。

我们可以通过改变缓冲区的生存期解决这方面的问题 'a,以'static在这两个API。

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact(&mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. same as before ..
    }
}

如果我们尝试复制之前的问题,我们会注意到它mem::forget不再引起问题。

#[allow(dead_code)]
fn sound(mut serial: Serial1, buf: &'static mut [u8; 16]) {
    // NOTE `buf` is moved into `foo`
    foo(&mut serial, buf);
    bar();
}

#[inline(never)]
fn foo(serial: &mut Serial1, buf: &'static mut [u8]) {
    // start a DMA transfer and forget the returned `Transfer` value
    mem::forget(serial.read_exact(buf));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}

和以前一样,DMA 传输mem::forgetTransfer 值之后继续。这一次不是问题,因为它buf是静态分配的(例如static mut变量)而不是在堆栈上。

重叠使用

我们的 API 不会阻止用户Serial在 DMA 传输过程中使用该接口。这可能会导致传输失败或数据丢失。

有几种方法可以防止重叠使用。一种方法是Transfer 取得所有权Serial1并在wait被调用时将其归还。

/// A DMA transfer
pub struct Transfer<B> {
    buffer: B,
    // NOTE: added
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    // NOTE: the return value has changed
    pub fn wait(self) -> (B, Serial1) {
        // Busy wait until the transfer is done
        while !self.is_done() {}

        (self.buffer, self.serial)
    }

    // ..
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    // NOTE we now take `self` by value
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        // .. same as before ..

        Transfer {
            buffer,
            // NOTE: added
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    // NOTE we now take `self` by value
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        // .. same as before ..

        Transfer {
            buffer,
            // NOTE: added
            serial: self,
        }
    }
}

移动语义Serial1在传输过程中静态地阻止访问。

fn read(serial: Serial1, buf: &'static mut [u8; 16]) {
    let t = serial.read_exact(buf);

    // let byte = serial.read(); //~ ERROR: `serial` has been moved

    // .. do stuff ..

    let (serial, buf) = t.wait();

    // .. do more stuff ..
}

还有其他方法可以防止重叠使用。例如,可以Cell将指示 DMA 传输是否正在进行的 ( ) 标志添加到 Serial1. 当设置标志时readwrite,read_exactwrite_all 都将Error::InUse在运行时返回错误(例如)。该标志将在使用write_all/时设置read_exact并在Transfer.wait.

编译器(错误)优化

编译器可以自由地重新排序和合并非易失性存储器操作以更好地优化程序。使用我们当前的 API,这种自由可能会导致未定义的行为。考虑以下示例:

fn reorder(serial: Serial1, buf: &'static mut [u8]) {
    // zero the buffer (for no particular reason)
    buf.iter_mut().for_each(|byte| *byte = 0);

    let t = serial.read_exact(buf);

    // ... do other stuff ..

    let (buf, serial) = t.wait();

    buf.reverse();

    // .. do stuff with `buf` ..
}

这里,编译器可自由移动buf.reverse()之前t.wait(),这会导致数据争用:两个处理器和DMA最终会修改 buf在同一时间。类似地,编译器可以将归零操作移到 after read_exact,这也会导致数据竞争。

为了防止这些有问题的重新排序,我们可以使用 compiler_fence

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact(mut self, buffer: &'static mut [u8]) -> Transfer<&'static mut [u8]> {
        self.dma.set_source_address(USART1_RX, false);
        self.dma
            .set_destination_address(buffer.as_mut_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // NOTE: added
        atomic::compiler_fence(Ordering::Release);

        // NOTE: this is a volatile *write*
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all(mut self, buffer: &'static [u8]) -> Transfer<&'static [u8]> {
        self.dma.set_destination_address(USART1_TX, false);
        self.dma.set_source_address(buffer.as_ptr() as usize, true);
        self.dma.set_transfer_length(buffer.len());

        // NOTE: added
        atomic::compiler_fence(Ordering::Release);

        // NOTE: this is a volatile *write*
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(self) -> (B, Serial1) {
        // NOTE: this is a volatile *read*
        while !self.is_done() {}

        // NOTE: added
        atomic::compiler_fence(Ordering::Acquire);

        (self.buffer, self.serial)
    }

    // ..
}

我们使用Ordering::Releaseinread_exactwrite_all来防止所有前面的内存操作被移动到之后 self.dma.start(),它执行易失性写入。

同样,我们使用Ordering::AcquireinTransfer.wait来防止所有后续内存操作被移动到之前 self.is_done(),它执行易失性读取。

为了更好地可视化围栏的效果,这里是上一节示例的稍微调整版本。我们在评论中添加了围栏及其顺序。

fn reorder(serial: Serial1, buf: &'static mut [u8], x: &mut u32) {
    // zero the buffer (for no particular reason)
    buf.iter_mut().for_each(|byte| *byte = 0);

    *x += 1;

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // NOTE: the processor can't access `buf` between the fences
    // ... do other stuff ..
    *x += 2;

    let (buf, serial) = t.wait(); // compiler_fence(Ordering::Acquire) ▼

    *x += 3;

    buf.reverse();

    // .. do stuff with `buf` ..
}

由于有 围栏,调零操作后*不能移动。同样,之前由于围栏无法移动操作。两个围栏 *之间的内存操作可以跨围栏自由重新排序,但这些操作都没有涉及,因此此类重新排序不会导致未定义的行为。 read_exact``Release``reverse wait``Acquire``buf

请注意,这compiler_fence比所需的要强一些。例如,x即使我们知道buf不与x(由于 Rust 别名规则)重叠,围栏也会阻止操作被合并。但是,不存在比compiler_fence.

我们不需要内存屏障吗?

这取决于目标架构。对于 Cortex M0 到 M4F 内核, AN321表示:

3.2 典型用法

(..)

在 Cortex-M 处理器中很少需要使用 DMB,因为它们不会重新排序内存事务。但是,如果要在其他 ARM 处理器上重用该软件,尤其是多主系统,则需要它。例如:

  • DMA 控制器配置。CPU 内存访问和 DMA 操作之间需要一个屏障。

(..)

4.18 多主系统

(..)

在第 47 页的图 41 和图 42 的示例中省略 DMB 或 DSB 指令不会导致任何错误,因为 Cortex-M 处理器:

  • 不要重新排序内存传输
  • 不允许两个写传输重叠。

其中图 41 显示了在开始 DMA 事务之前使用的 DMB(内存屏障)指令。

对于 Cortex-M7 内核,如果您使用数据缓存 (DCache),您将需要内存屏障 (DMB/DSB),除非您手动使 DMA 使用的缓冲区无效。即使禁用了数据缓存,仍可能需要内存屏障来避免在存储缓冲区中重新排序。

如果您的目标是多核系统,那么您很可能需要内存屏障。

如果确实需要内存屏障,则需要使用atomic::fence而不是compiler_fence. 这应该会在 Cortex-M 设备上生成一条 DMB 指令。

Generic buffer

我们的 API 比它需要的更具限制性。例如,以下程序即使有效也不会被接受。

fn reuse(serial: Serial1, msg: &'static mut [u8]) {
    // send a message
    let t1 = serial.write_all(msg);

    // ..

    let (msg, serial) = t1.wait(); // `msg` is now `&'static [u8]`

    msg.reverse();

    // now send it in reverse
    let t2 = serial.write_all(msg);

    // ..

    let (buf, serial) = t2.wait();

    // ..
}

为了接受这样的程序,我们可以使缓冲区参数通用。

// as-slice = "0.1.0"
use as_slice::{AsMutSlice, AsSlice};

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: B) -> Transfer<B>
    where
        B: AsMutSlice<Element = u8>,
    {
        // NOTE: added
        let slice = buffer.as_mut_slice();
        let (ptr, len) = (slice.as_mut_ptr(), slice.len());

        self.dma.set_source_address(USART1_RX, false);

        // NOTE: tweaked
        self.dma.set_destination_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    fn write_all<B>(mut self, buffer: B) -> Transfer<B>
    where
        B: AsSlice<Element = u8>,
    {
        // NOTE: added
        let slice = buffer.as_slice();
        let (ptr, len) = (slice.as_ptr(), slice.len());

        self.dma.set_destination_address(USART1_TX, false);

        // NOTE: tweaked
        self.dma.set_source_address(ptr as usize, true);
        self.dma.set_transfer_length(len);

        atomic::compiler_fence(Ordering::Release);
        self.dma.start();

        Transfer {
            buffer,
            serial: self,
        }
    }
}

注: AsRef<[u8]>AsMut<[u8]>)可能已被使用,而不是 AsSlice<Element = u8>AsMutSlice<Element = u8)。

现在该reuse计划将被接受。

不可移动的缓冲区

通过此修改,API 还将按值接受数组(例如[u8; 16])。但是,使用数组会导致指针失效。考虑以下程序。

fn invalidate(serial: Serial1) {
    let t = start(serial);

    bar();

    let (buf, serial) = t.wait();
}

#[inline(never)]
fn start(serial: Serial1) -> Transfer<[u8; 16]> {
    // array allocated in this frame
    let buffer = [0; 16];

    serial.read_exact(buffer)
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}

read_exact操作将使用函数的buffer本地 地址start。返回buffer时该本地将被释放,start并且使用的指针read_exact将失效。您最终会遇到与unsound示例类似的情况。

为了避免这个问题,我们要求与我们的 API 一起使用的缓冲区即使在移动时也保留其内存位置。该PinNEWTYPE提供这样的保证。我们可以更新我们的 API 以要求所有缓冲区首先“固定”。

注意:要编译此点以下的所有程序,您将需要 Rust >=1.33.0。截至撰写本文时 (2019-01-04),这意味着使用夜间频道。

/// A DMA transfer
pub struct Transfer<B> {
    // NOTE: changed
    buffer: Pin<B>,
    serial: Serial1,
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: bounds changed
        B: DerefMut,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: bounds changed
        B: Deref,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..
    }
}

注意:我们本可以使用StableDereftrait 而不是Pin newtype 但选择了Pin它,因为它是在标准库中提供的。

通过这个新 API,我们可以使用&'static mut引用、Box-ed 切片、Rc-ed 切片等。

fn static_mut(serial: Serial1, buf: &'static mut [u8]) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}

fn boxed(serial: Serial1, buf: Box<[u8]>) {
    let buf = Pin::new(buf);

    let t = serial.read_exact(buf);

    // ..

    let (buf, serial) = t.wait();

    // ..
}

'static 边界

固定是否让我们安全地使用堆栈分配的数组?答案是否定的。考虑以下示例。

fn unsound(serial: Serial1) {
    start(serial);

    bar();
}

// pin-utils = "0.1.0-alpha.4"
use pin_utils::pin_mut;

#[inline(never)]
fn start(serial: Serial1) {
    let buffer = [0; 16];

    // pin the `buffer` to this stack frame
    // `buffer` now has type `Pin<&mut [u8; 16]>`
    pin_mut!(buffer);

    mem::forget(serial.read_exact(buffer));
}

#[inline(never)]
fn bar() {
    // stack variables
    let mut x = 0;
    let mut y = 0;

    // use `x` and `y`
}

如前所述,由于堆栈帧损坏,上述程序会遇到未定义的行为。

该API不健全类型的缓冲区Pin<&'a mut [u8]>,其中'a不是 'static。为了防止出现问题,我们必须'static在某些地方添加一个边界。

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: added 'static bound
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        // NOTE: added 'static bound
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..
    }
}

现在有问题的程序将被拒绝。

析构函数

现在 API 接受Box-es 和其他具有析构函数的类型,我们需要决定在Transfer提前删除时做什么。

通常,Transfer使用该wait方法使用值,但也可以隐式或显式地drop在传输结束之前使用该值。例如,删除一个Transfer<Box<[u8]>>值将导致缓冲区被释放。如果传输仍在进行中,这可能会导致未定义的行为,因为 DMA 最终会写入已释放的内存。

在这种情况下,一种选择是Transfer.drop停止 DMA 传输。另一种选择是Transfer.drop等待传输完成。我们会选择前者,因为它更便宜。

/// A DMA transfer
pub struct Transfer<B> {
    // NOTE: always `Some` variant
    inner: Option<Inner<B>>,
}

// NOTE: previously named `Transfer<B>`
struct Inner<B> {
    buffer: Pin<B>,
    serial: Serial1,
}

impl<B> Transfer<B> {
    /// Blocks until the transfer is done and returns the buffer
    pub fn wait(mut self) -> (Pin<B>, Serial1) {
        while !self.is_done() {}

        atomic::compiler_fence(Ordering::Acquire);

        let inner = self
            .inner
            .take()
            .unwrap_or_else(|| unsafe { hint::unreachable_unchecked() });
        (inner.buffer, inner.serial)
    }
}

impl<B> Drop for Transfer<B> {
    fn drop(&mut self) {
        if let Some(inner) = self.inner.as_mut() {
            // NOTE: this is a volatile write
            inner.serial.dma.stop();

            // we need a read here to make the Acquire fence effective
            // we do *not* need this if `dma.stop` does a RMW operation
            unsafe {
                ptr::read_volatile(&0);
            }

            // we need a fence here for the same reason we need one in `Transfer.wait`
            atomic::compiler_fence(Ordering::Acquire);
        }
    }
}

impl Serial1 {
    /// Receives data into the given `buffer` until it's filled
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn read_exact<B>(mut self, mut buffer: Pin<B>) -> Transfer<B>
    where
        B: DerefMut + 'static,
        B::Target: AsMutSlice<Element = u8> + Unpin,
    {
        // .. same as before ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }

    /// Sends out the given `buffer`
    ///
    /// Returns a value that represents the in-progress DMA transfer
    pub fn write_all<B>(mut self, buffer: Pin<B>) -> Transfer<B>
    where
        B: Deref + 'static,
        B::Target: AsSlice<Element = u8>,
    {
        // .. same as before ..

        Transfer {
            inner: Some(Inner {
                buffer,
                serial: self,
            }),
        }
    }
}

现在 DMA 传输将在释放缓冲区之前停止。

fn reuse(serial: Serial1) {
    let buf = Pin::new(Box::new([0; 16]));

    let t = serial.read_exact(buf); // compiler_fence(Ordering::Release) ▲

    // ..

    // this stops the DMA transfer and frees memory
    mem::drop(t); // compiler_fence(Ordering::Acquire) ▼

    // this likely reuses the previous memory allocation
    let mut buf = Box::new([0; 16]);

    // .. do stuff with `buf` ..
}

概括

综上所述,实现内存安全的DMA传输需要考虑以下几点:

  • 使用不可移动的缓冲区加上间接:Pin<B>。或者,您可以使用StableDereftrait。
  • 缓冲区的所有权必须传递给 DMA : B: 'static
  • 不要依赖于存储安全运行析构函数。考虑如果mem::forget与您的 API 一起使用会发生什么。
  • 不要为它添加自定义的析构停止DMA传输,或者等待到结束。考虑如果mem::drop与您的 API 一起使用会发生什么。

本文省略了构建生产级 DMA 抽象所需的几个细节,例如配置 DMA 通道(例如流、循环与单次模式等)、缓冲区对齐、错误处理、如何制作抽象设备 -不可知论等。所有这些方面都留给读者/社区作为练习(:P)。

关于编译器支持的说明

本书使用了一个内置的编译器目标 ,thumbv7m-none-eabiRust 团队为其分发了一个rust-std组件,这是一个预编译的 crate 集合,如corestd

如果您想尝试为不同的目标架构复制本书的内容,您需要考虑 Rust 为(编译)目标提供的不同级别的支持。

LLVM 支持

从 Rust 1.28 开始,官方的 Rust 编译器rustc使用 LLVM 来生成(机器)代码。Rust 为架构提供的最低级别支持是在 rustc. 您可以rustc通过运行以下命令,通过 LLVM查看所有支持的架构:

$ # you need to have `cargo-binutils` installed to run this command
$ cargo objdump -- -version
LLVM (http://llvm.org/):
  LLVM version 7.0.0svn
  Optimized build.
  Default target: x86_64-unknown-linux-gnu
  Host CPU: skylake

  Registered Targets:
    aarch64    - AArch64 (little endian)
    aarch64_be - AArch64 (big endian)
    arm        - ARM
    arm64      - ARM64 (little endian)
    armeb      - ARM (big endian)
    hexagon    - Hexagon
    mips       - Mips
    mips64     - Mips64 [experimental]
    mips64el   - Mips64el [experimental]
    mipsel     - Mipsel
    msp430     - MSP430 [experimental]
    nvptx      - NVIDIA PTX 32-bit
    nvptx64    - NVIDIA PTX 64-bit
    ppc32      - PowerPC 32
    ppc64      - PowerPC 64
    ppc64le    - PowerPC 64 LE
    sparc      - Sparc
    sparcel    - Sparc LE
    sparcv9    - Sparc V9
    systemz    - SystemZ
    thumb      - Thumb
    thumbeb    - Thumb (big endian)
    wasm32     - WebAssembly 32-bit
    wasm64     - WebAssembly 64-bit
    x86        - 32-bit X86: Pentium-Pro and above
    x86-64     - 64-bit X86: EM64T and AMD64

如果 LLVM 支持您感兴趣的架构,但rustc构建时禁用了后端(这是 Rust 1.28 中的 AVR 的情况),那么您将需要修改 Rust 源以启用它。PR rust-lang/rust#52787的前两次提交让您了解所需的更改。

另一方面,如果 LLVM 不支持该架构,但 LLVM 的一个分支支持,则在构建rustc. Rust 构建系统允许这样做,原则上它应该只需要更改llvm子模块以指向 fork。

如果您的目标架构仅由某些供应商提供的 GCC 支持,您可以选择使用mrustc,一个非官方的 Rust 编译器,将您的 Rust 程序转换为 C 代码,然后使用 GCC 编译它。

内置目标

编译目标不仅仅是它的架构。每个目标都有一个 与其相关联的规范,其中描述了其体系结构、操作系统和默认链接器。

Rust 编译器知道几个目标。这些内置在编译器中,可以通过运行以下命令列出:

$ rustc --print target-list | column
aarch64-fuchsia                   mipsisa32r6el-unknown-linux-gnu
aarch64-linux-android             mipsisa64r6-unknown-linux-gnuabi64
aarch64-pc-windows-msvc           mipsisa64r6el-unknown-linux-gnuabi64
aarch64-unknown-cloudabi          msp430-none-elf
aarch64-unknown-freebsd           nvptx64-nvidia-cuda
aarch64-unknown-hermit            powerpc-unknown-linux-gnu
aarch64-unknown-linux-gnu         powerpc-unknown-linux-gnuspe
aarch64-unknown-linux-musl        powerpc-unknown-linux-musl
aarch64-unknown-netbsd            powerpc-unknown-netbsd
aarch64-unknown-none              powerpc-wrs-vxworks
aarch64-unknown-none-softfloat    powerpc-wrs-vxworks-spe
aarch64-unknown-openbsd           powerpc64-unknown-freebsd
aarch64-unknown-redox             powerpc64-unknown-linux-gnu
aarch64-uwp-windows-msvc          powerpc64-unknown-linux-musl
aarch64-wrs-vxworks               powerpc64-wrs-vxworks
arm-linux-androideabi             powerpc64le-unknown-linux-gnu
arm-unknown-linux-gnueabi         powerpc64le-unknown-linux-musl
arm-unknown-linux-gnueabihf       riscv32i-unknown-none-elf
arm-unknown-linux-musleabi        riscv32imac-unknown-none-elf
arm-unknown-linux-musleabihf      riscv32imc-unknown-none-elf
armebv7r-none-eabi                riscv64gc-unknown-linux-gnu
armebv7r-none-eabihf              riscv64gc-unknown-none-elf
armv4t-unknown-linux-gnueabi      riscv64imac-unknown-none-elf
armv5te-unknown-linux-gnueabi     s390x-unknown-linux-gnu
armv5te-unknown-linux-musleabi    sparc-unknown-linux-gnu
armv6-unknown-freebsd             sparc64-unknown-linux-gnu
armv6-unknown-netbsd-eabihf       sparc64-unknown-netbsd
armv7-linux-androideabi           sparc64-unknown-openbsd
armv7-unknown-cloudabi-eabihf     sparcv9-sun-solaris
armv7-unknown-freebsd             thumbv6m-none-eabi
armv7-unknown-linux-gnueabi       thumbv7a-pc-windows-msvc
armv7-unknown-linux-gnueabihf     thumbv7em-none-eabi
armv7-unknown-linux-musleabi      thumbv7em-none-eabihf
armv7-unknown-linux-musleabihf    thumbv7m-none-eabi
armv7-unknown-netbsd-eabihf       thumbv7neon-linux-androideabi
armv7-wrs-vxworks-eabihf          thumbv7neon-unknown-linux-gnueabihf
armv7a-none-eabi                  thumbv7neon-unknown-linux-musleabihf
armv7a-none-eabihf                thumbv8m.base-none-eabi
armv7r-none-eabi                  thumbv8m.main-none-eabi
armv7r-none-eabihf                thumbv8m.main-none-eabihf
asmjs-unknown-emscripten          wasm32-unknown-emscripten
hexagon-unknown-linux-musl        wasm32-unknown-unknown
i586-pc-windows-msvc              wasm32-wasi
i586-unknown-linux-gnu            x86_64-apple-darwin
i586-unknown-linux-musl           x86_64-fortanix-unknown-sgx
i686-apple-darwin                 x86_64-fuchsia
i686-linux-android                x86_64-linux-android
i686-pc-windows-gnu               x86_64-linux-kernel
i686-pc-windows-msvc              x86_64-pc-solaris
i686-unknown-cloudabi             x86_64-pc-windows-gnu
i686-unknown-freebsd              x86_64-pc-windows-msvc
i686-unknown-haiku                x86_64-rumprun-netbsd
i686-unknown-linux-gnu            x86_64-sun-solaris
i686-unknown-linux-musl           x86_64-unknown-cloudabi
i686-unknown-netbsd               x86_64-unknown-dragonfly
i686-unknown-openbsd              x86_64-unknown-freebsd
i686-unknown-uefi                 x86_64-unknown-haiku
i686-uwp-windows-gnu              x86_64-unknown-hermit
i686-uwp-windows-msvc             x86_64-unknown-hermit-kernel
i686-wrs-vxworks                  x86_64-unknown-illumos
mips-unknown-linux-gnu            x86_64-unknown-l4re-uclibc
mips-unknown-linux-musl           x86_64-unknown-linux-gnu
mips-unknown-linux-uclibc         x86_64-unknown-linux-gnux32
mips64-unknown-linux-gnuabi64     x86_64-unknown-linux-musl
mips64-unknown-linux-muslabi64    x86_64-unknown-netbsd
mips64el-unknown-linux-gnuabi64   x86_64-unknown-openbsd
mips64el-unknown-linux-muslabi64  x86_64-unknown-redox
mipsel-unknown-linux-gnu          x86_64-unknown-uefi
mipsel-unknown-linux-musl         x86_64-uwp-windows-gnu
mipsel-unknown-linux-uclibc       x86_64-uwp-windows-msvc
mipsisa32r6-unknown-linux-gnu     x86_64-wrs-vxworks

您可以使用以下命令打印这些目标之一的规范:

$ rustc +nightly -Z unstable-options --print target-spec-json --target thumbv7m-none-eabi
{
  "abi-blacklist": [
    "stdcall",
    "fastcall",
    "vectorcall",
    "thiscall",
    "win64",
    "sysv64"
  ],
  "arch": "arm",
  "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
  "emit-debug-gdb-scripts": false,
  "env": "",
  "executables": true,
  "is-builtin": true,
  "linker": "arm-none-eabi-gcc",
  "linker-flavor": "gcc",
  "llvm-target": "thumbv7m-none-eabi",
  "max-atomic-width": 32,
  "os": "none",
  "panic-strategy": "abort",
  "relocation-model": "static",
  "target-c-int-width": "32",
  "target-endian": "little",
  "target-pointer-width": "32",
  "vendor": ""
}

如果这些内置目标似乎都不适合您的目标系统,则必须通过编写自己的 JSON 格式的目标规范文件来创建自定义目标,下一节将对此进行介绍 。

rust-std 成分

对于一些内置目标,Rust 团队rust-std通过rustup. 这个组件是一个预编译的 crate 的集合,比如corestd,交叉编译需要它。

您可以通过运行以下命令找到具有rust-std可用组件的目标列表rustup

$ rustup target list | column
aarch64-apple-ios                       mipsel-unknown-linux-musl
aarch64-fuchsia                         nvptx64-nvidia-cuda
aarch64-linux-android                   powerpc-unknown-linux-gnu
aarch64-pc-windows-msvc                 powerpc64-unknown-linux-gnu
aarch64-unknown-linux-gnu               powerpc64le-unknown-linux-gnu
aarch64-unknown-linux-musl              riscv32i-unknown-none-elf
aarch64-unknown-none                    riscv32imac-unknown-none-elf
aarch64-unknown-none-softfloat          riscv32imc-unknown-none-elf
arm-linux-androideabi                   riscv64gc-unknown-linux-gnu
arm-unknown-linux-gnueabi               riscv64gc-unknown-none-elf
arm-unknown-linux-gnueabihf             riscv64imac-unknown-none-elf
arm-unknown-linux-musleabi              s390x-unknown-linux-gnu
arm-unknown-linux-musleabihf            sparc64-unknown-linux-gnu
armebv7r-none-eabi                      sparcv9-sun-solaris
armebv7r-none-eabihf                    thumbv6m-none-eabi
armv5te-unknown-linux-gnueabi           thumbv7em-none-eabi
armv5te-unknown-linux-musleabi          thumbv7em-none-eabihf
armv7-linux-androideabi                 thumbv7m-none-eabi
armv7-unknown-linux-gnueabi             thumbv7neon-linux-androideabi
armv7-unknown-linux-gnueabihf           thumbv7neon-unknown-linux-gnueabihf
armv7-unknown-linux-musleabi            thumbv8m.base-none-eabi
armv7-unknown-linux-musleabihf          thumbv8m.main-none-eabi
armv7a-none-eabi                        thumbv8m.main-none-eabihf
armv7r-none-eabi                        wasm32-unknown-emscripten
armv7r-none-eabihf                      wasm32-unknown-unknown
asmjs-unknown-emscripten                wasm32-wasi
i586-pc-windows-msvc                    x86_64-apple-darwin
i586-unknown-linux-gnu                  x86_64-apple-ios
i586-unknown-linux-musl                 x86_64-fortanix-unknown-sgx
i686-linux-android                      x86_64-fuchsia
i686-pc-windows-gnu                     x86_64-linux-android
i686-pc-windows-msvc                    x86_64-pc-windows-gnu
i686-unknown-freebsd                    x86_64-pc-windows-msvc
i686-unknown-linux-gnu                  x86_64-rumprun-netbsd
i686-unknown-linux-musl                 x86_64-sun-solaris
mips-unknown-linux-gnu                  x86_64-unknown-cloudabi
mips-unknown-linux-musl                 x86_64-unknown-freebsd
mips64-unknown-linux-gnuabi64           x86_64-unknown-linux-gnu (default)
mips64-unknown-linux-muslabi64          x86_64-unknown-linux-gnux32
mips64el-unknown-linux-gnuabi64         x86_64-unknown-linux-musl
mips64el-unknown-linux-muslabi64        x86_64-unknown-netbsd
mipsel-unknown-linux-gnu                x86_64-unknown-redox

如果rust-std您的目标没有组件,或者您使用的是自定义目标,那么您将不得不使用夜间工具链来构建标准库。

创建自定义目标

如果自定义目标三元组不适用于您的平台,您必须创建一个自定义目标文件,将您的目标描述为 rustc。

请记住,需要使用夜间编译器来构建核心库,这必须针对 rustc 未知的目标完成。

确定目标三元组

许多目标已经有一个已知的三元组来描述它们,通常采用 ARCH-VENDOR-SYS-ABI 的形式。你的目标应该是使用与LLVM相同的三元组;但是,如果您需要向 Rust 指定 LLVM 不知道的其他信息,则可能会有所不同。虽然三元组在技术上仅供人类使用,但它的独特性和描述性很重要,尤其是如果目标将来会被上游。

ARCH 部分通常只是架构名称,32 位 ARM 除外。例如,您可能会为这些处理器使用 x86_64,但要指定确切的 ARM 架构版本。典型值可能是armv7armv5tethumbv7neon。查看内置目标的名称以获得灵感。

VENDOR 部分是可选的,它描述了制造商。省略此字段与使用unknown.

SYS 部分描述了所使用的操作系统。 对于桌面平台,典型值包括win32linuxdarwinnone用于裸机使用。

ABI 部分描述了流程是如何启动的。eabi用于裸机,而gnu用于 glibc、muslmusl 等。

现在您有了一个目标三元组,创建一个包含三元组名称和.json 扩展名的文件。例如,描述的文件armv7a-none-eabi将具有文件名 armv7a-none-eabi.json

填充目标文件

目标文件必须是有效的 JSON。有两个地方描述了它的内容: Target,其中每个字段都是必需的,以及TargetOptions,每个字段都是可选的。 所有下划线都替换为连字符

推荐的方法是将目标文件基于与目标系统相似的内置目标的规范,然后调整它以匹配目标系统的属性。为此,请使用命令 rustc +nightly -Z unstable-options --print target-spec-json --target $SOME_SIMILAR_TARGET,使用 编译器中已内置的目标

您几乎可以将该输出复制到您的文件中。从一些修改开始:

  • 消除 "is-builtin": true

  • 填充llvm-target用的三倍LLVM预期

  • 决定一个恐慌的策略。裸机实现可能会使用 "panic-strategy": "abort". 如果你决定不abort恐慌,除非你告诉 Cargo每个项目,你必须定义一个eh_personality函数。

  • 配置原子。选择描述您的目标的第一个选项:

    • 我有一个单核处理器,没有线程,没有中断,或者以任何方式并行发生多种事情:如果您确定是这种情况,例如 WASM(目前),您可以设置"singlethread": true. 这将配置 LLVM 以将所有原子操作转换为使用它们的单线程对应项。如果使用线程或中断,错误地使用此选项可能会导致 UB。
    • 我有本机原子操作:设置max-atomic-width为您的目标可以原子操作的最大位类型。例如,许多 ARM 内核具有 32 位原子操作。"max-atomic-width": 32在这种情况下你可以设置。
    • 我没有本机原子操作,但我可以自己模拟它们:设置max-atomic-width为最多可以模拟 128 位的最高位数,然后将 LLVM 期望的所有原子同步功能实现 为 #[no_mangle] unsafe extern "C". 这些函数已经被gcc标准化了,所以gcc的文档可能有更多的注释。缺少函数会导致链接器错误,而错误实现的函数可能会导致 UB。例如,如果您有一个带中断的单核、单线程处理器,您可以实现这些函数来禁用中断,执行常规操作,然后重新启用它们。
    • 我没有本机原子操作:您必须做一些不安全的工作来手动确保代码中的同步。您必须设置"max-atomic-width": 0.
  • 如果与现有工具链集成,请更改链接器。例如,如果您使用的工具链使用 gcc 的自定义构建,请将

    "linker-flavor": "gcc"

    和设置

    linker

    为链接器的命令名称。如果您需要额外的链接器参数,请使用

    pre-link-args

    and

    post-link-args

    "pre-link-args": {
        "gcc": [
            "-Wl,--as-needed",
            "-Wl,-z,noexecstack",
            "-m64"
        ]
    },
    "post-link-args": {
        "gcc": [
            "-Wl,--allow-multiple-definition",
            "-Wl,--start-group,-lc,-lm,-lgcc,-lstdc++,-lsupc++,--end-group"
        ]
    }

    确保链接器类型是

    link-args

    .

  • 配置 LLVM 功能。llc -march=ARCH -mattr=help在 ARCH 是基本架构的地方运行(不包括 ARM 的版本)以列出可用功能及其描述。如果您的目标需要严格的内存对齐访问(例如armv5te),请确保启用strict-align. 要启用某个功能,请在它之前放置一个加号。同样,要禁用某个功能,请在它之前放置一个减号。功能应该像这样用逗号分隔: "features": "+soft-float,+neon. 请注意,如果 LLVM 根据提供的三元组和 CPU 对您的目标有足够的了解,则这可能没有必要。

  • 如果您知道,请配置 LLVM 使用的 CPU。这将启用特定于 CPU 的优化和功能。在上一步命令输出的顶部,有一个已知 CPU 的列表。如果您知道将针对特定 CPU,则可以cpu在 JSON 目标文件的字段中进行设置。

使用目标文件

一旦你有了一个目标规范文件,.json如果它在当前目录或$RUST_TARGET_PATH.

验证它是否可以被 rustc 读取:

❱ rustc --print cfg --target foo.json # or just foo if in the current directory
debug_assertions
target_arch="arm"
target_endian="little"
target_env=""
target_feature="mclass"
target_feature="v7"
target_has_atomic="16"
target_has_atomic="32"
target_has_atomic="8"
target_has_atomic="cas"
target_has_atomic="ptr"
target_os="none"
target_pointer_width="32"
target_vendor=""

现在,您终于可以使用它了!许多资源一直在推荐xargocargo-xbuild. 然而,它的继任者,cargo 的build-std功能,最近收到了很多工作,并且很快就达到了与其他选项相同的功能。因此,本指南将仅涵盖该选项。

从一个最低限度的no_std程序开始。现在,cargo build -Z build-std=core --target foo.json再次使用上述有关引用路径的规则运行 。希望您现在应该在目标目录中有一个二进制文件。

您可以选择将货物配置为始终使用您的目标。请参阅页面末尾关于最小no_std程序的建议。但是,您目前必须使用该标志,-Z build-std=core因为该选项不稳定。

构建额外的内置crate

在使用cargo 的build-std特性时,你可以选择在哪个 crate 中编译。默认情况下,当只传递-Z build-std, std, core, 和alloc被编译时。但是,您可能希望std在为裸机编译时排除。为此,请在 之后指定您想要的板条箱 build-std。例如,要包含corealloc,通过-Z build-std=core,alloc

故障排除

需要语言项,但未找到: eh_personality

添加"panic-strategy": "abort"到您的目标文件,或定义一个eh_personality函数。或者,告诉 Cargo 忽略它。

未定义的引用 __sync_val_compare_and_swap_#

Rust 认为您的目标具有原子指令,但 LLVM 没有。回到关于配置原子的步骤 。您将需要减少 中的数量max-atomic-width。有关更多详细信息,请参阅#58500

找不到syncalloc

与上述情况类似,Rust 不认为你有原子。你必须自己实现它们或者告诉 Rust 你有原子指令。

多重定义 __(something)

您可能会将 Rust 程序与从另一种语言构建的代码相关联,而另一种语言包括 Rust 也创建的编译器内置程序。要解决此问题,您需要告诉链接器允许多个定义。如果使用 gcc,您可以添加:

"post-link-args": {
    "gcc": [
        "-Wl,--allow-multiple-definition"
    ]
}

添加符号时出错:无法识别文件格式

切换到货物的build-std功能并更新您的编译器。这是为一些试图将内部 Rust 对象传递给外部链接器的编译器构建引入的错误

😄《MicroRust》

Getting started

好的,让我们像往常一样开始使用 Rust。

$ rustup update

让您的工具链保持最新总是好的。

现在让我们创建一个新的二进制项目。你可能不经常这样做,所以忘记是可以理解的。如果你运行$ cargo,你会得到一个提示。

Buiding

$ cargo new microrust-start
     Created binary (application) `microrust-start` project
$ cd microrust-start
Cargo.toml  src

这已经创建了一个二进制板条箱。

现在我们可以$ cargo build这样做,甚至$ cargo run可以,但是一切都在为您的计算机编译和运行。

目标

micro:bit 的架构与您的计算机不同,因此第一步将是针对 micro:bit 的架构进行交叉编译。如果您进行 Internet 搜索,您会找到Rust的平台支持列表。查看此页面,您会发现 micro:bit 的 nRF51822 Cortex-M0 微处理器:

thumbv6m-none-eabi [*] [ ] [ ] Bare Cortex-M0, M0+, M1

“thumbv6m-none-eabi”是已知的目标三元组。注意星星代表什么:

这些是裸机微控制器目标,只能访问核心库,不能访问 std。

要安装此目标:

$ rustup target add thumbv6m-none-eabi

构建 1

现在我们应该如何使用它?好吧,如果您要查看$ cargo build -h,您会尝试:

$ cargo build --target thumbv6m-none-eabi
error[E0463]: can't find crate for `std`
  |
  = note: the `thumbv6m-none-eabi` target may not be installed

error: aborting due to previous error

For more information about this error, try `rustc --explain E0463`.
error: Could not compile `microrust-start`.

To learn more, run the command again with --verbose.

帮助说明相当无用,因为我们刚刚安装了该目标。我们还注意到,thumbv6m-none-eabi 目标不包括 std,只有核心 crate,它具有与平台无关的 std 功能子集。为什么在我们构建时它仍然在寻找 std crate?

no_std

事实证明,除非明确禁用,否则 rust 将始终寻找 std crate,因此我们将添加 no_std 属性

src/main.rs
#![no_std]

fn main() {
    println!("Hello, world!");
}

构建 2

$ cargo build --target thumbv6m-none-eabi
error: cannot find macro `println!` in this scope
 --> src/main.rs:4:5
  |
4 |     println!("Hello, world!");
  |     ^^^^^^^

println是在 std crate 中找到的宏。我们目前不需要它,所以我们可以删除它并尝试重新构建。

构建 3

$ cargo build --target thumbv6m-none-eabi
error: `#[panic_handler]` function required, but not found

这个错误是因为 rustc 需要实现一个恐慌处理程序。

panic_impl

我们可以尝试自己实现 panic 宏,但是使用一个为我们完成它的 crate 更容易和更便携。

如果我们在crates.io 上查看 panic-impl 关键字,我们会找到一些例子。让我们画一个最简单的,并将它添加到我们的 Cargo.toml。如果您忘记了如何执行此操作,请尝试查看货物手册

Cargo.toml
[dependencies]
panic-halt = "~0.2"
src/main.rs
#![no_std]

extern crate panic_halt;

fn main() {
}

建造 4

$ cargo build --target thumbv6m-none-eabi
error: requires `start` lang_item

no_main

在您习惯制作的普通命令行 rust 二进制文件中,执行二进制文件通常会通过执行 C 运行时库 (crt0) 来启动操作系统。这又会调用 Rust 运行时,如start语言项所标记,后者又会调用 main 函数。

启用后no_std,因为我们的目标是微控制器,crt0 和 rust 运行时都不可用,所以即使实现start也无济于事。我们需要更换操作系统入口点。

例如,您可以在默认入口点之后命名一个函数,对于 linux 是_start,并以这种方式启动。请注意,您还需要禁用name mangling

#![no_std]
#![no_main]

#[no_mangle]
pub extern "C" fn _start() -> ! {
    loop {}
}

这是尝试让我们自己工作的道路的尽头。在这一点上,我们需要特定于板的支持箱和一些货物调整的帮助才能使其正常工作。

microbit-crate

让我们为 micro:bit 添加对板条箱的依赖。

[dependencies]
panic-halt = "~0.2"
microbit="~0.7"

microbit crate 有两个值得注意的依赖项:

embedded-hal

这个 crate 是一个 HAL 实现 crate,其中 HAL 代表硬件抽象层。随着 Rust 在嵌入式开发中变得越来越流行,希望尽可能少的硬件特定实现。

出于这个原因,embedded-hal板条箱包含一系列硬件抽象特征,可以由板特定的板条箱实现。

cortex-m-rt

这个 crate 实现了 Cortex-M 微控制器的最小启动/运行时间。除其他外,这个板条箱提供:

  • #[entry]属性,来定义程序的入口点。
  • 硬故障处理程序的定义
  • 默认异常处理程序的定义

这个箱子需要:

  • 将特定微控制器的存储器布局定义为 memory.x 文件。幸运的是,这通常由董事会支持箱提供

要使用该#[entry]属性,我们需要将其添加为依赖项。

有关详细信息,您可以使用有用的Cortex-M的,快速启动箱它的文档

cargo-config

在我们继续之前,我们将通过编辑来调整货物的配置microrust-start/.cargo/config。有关更多信息,您可以在此处阅读文档

.cargo/config

# Configure builds for our target, the micro:bit's architecture
[target.thumbv6m-none-eabi]
# Execute binary using gdb when calling cargo run
runner = "arm-none-eabi-gdb"
# Tweak to the linking process required by the cortex-m-rt crate
rustflags = [
    "-C", "link-arg=-Tlink.x",
    # The LLD linker is selected by default
    #"-C", "linker=arm-none-eabi-ld",
]

# Automatically select this target when cargo building this project
[build]
target = "thumbv6m-none-eabi"

arm-none-eabi-gdb

这是用于 ARM EABI(嵌入式应用程序二进制接口)的 gdb(GNU 调试器)版本。它将允许我们从您的计算机调试在我们的 micro:bit 上运行的代码。

构建目标

现在,您需要做的就是运行$ cargo build,cargo 会自动添加--target thumbv6m-none-eabi.

建造 5

Cargo.toml

[dependencies]
panic-halt = "~0.2"
microbit="~0.7"
cortex-m-rt="~0.6"

src/main.rs

#![no_std]
#![no_main]

extern crate panic_halt;

use cortex_m_rt::entry;

#[entry]
fn main() {
}
$ cargo build
error: custom attribute panicked
 --> src/main.rs:7:1
  |
7 | #[entry]
  | ^^^^^^^^
  |
  = help: message: `#[entry]` function must have signature `[unsafe] fn() -> !`

! 返回类型

一个鲜为人知的锈功能,所以如果你不知道这意味着什么,我会原谅你。返回类型为的!意味着函数不能返回。实现这一点的一种简单方法是使用无限循环。

src/main.rs

#![no_std]
#![no_main]

extern crate panic_halt;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    loop {}
}

建造 6

如果您现在尝试构建,您应该最终会受到欢迎Finished

$ cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s

构建完成

作为完整性检查,让我们验证生成的可执行文件实际上是 ARM 二进制文件:

$ file target/thumbv6m-none-eabi/debug/microrust-start
target/thumbv6m-none-eabi/debug/microrust-start: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
                                                                            ^^^  ^^^^^

Flashing

Flashing is the process of moving our program into the microcontroller’s (persistent) memory. Once flashed, the microcontroller will execute the flashed program every time it is powered on.

In this case, our rustled program will be the only program in the microcontroller memory. By this I mean that there’s nothing else running on the microcontroller: no OS, no daemon, nothing. rustled has full control over the device. This is what is meant by bare-metal programming.

  • OS

    operating system

  • Daemon

    program running in the background

Connect the micro:bit to your computer and run the following commands on a new terminal.

We need to give OCD the name of the interfaces we are using:

$ # All
$ # Windows: remember that you need an extra `-s %PATH_TO_OPENOCD%\\scripts`
$ openocd -f interface/cmsis-dap.cfg -f target/nrf51.cfg

The program will block; leave that terminal open.

Now it’s a good time to explain what this command is actually doing.

I mentioned that the micro:bit actually has two microcontrollers. One of them is used as a USB interface and programmer/debugger. This microcontroller is connected to the target microcontroller using a Serial Wire Debug (SWD) interface (this interface is an ARM standard so you’ll run into it when dealing with other Cortex-M based microcontrollers). This SWD interface can be used to flash and debug a microcontroller. It uses the CMSIS-DAP protocol for host debugging of application programs. It will appear as a USB device when you connect the micro:bit to your laptop.

As for OpenOCD, it’s software that provides some services like a GDB server on top of USB devices that expose a debugging protocol like SWD or JTAG.

GDB: The GNU debugger will allow us to debug our software by controlling the execution of our program. We will learn more about this a little bit later.

Onto the actual command: those .cfg files we are using instruct OpenOCD to look for

  • a CMSIS-DAP USB interface device (interface/cmsis-dap.cfg)
  • a nRF51XXX microcontroller target (target/nrf51.cfg) to be connected to the USB interface.

The OpenOCD output looks like this:

Open On-Chip Debugger 0.10.0
Licensed under GNU GPL v2
For bug reports, read
    http://openocd.org/doc/doxygen/bugs.html
Info : auto-selecting first available session transport "swd". To override use 'transport select '.
cortex_m reset_config sysresetreq
adapter speed: 1000 kHz
Info : CMSIS-DAP: SWD  Supported
Info : CMSIS-DAP: Interface Initialised (SWD)
Info : CMSIS-DAP: FW Version = 1.0
Info : SWCLK/TCK = 1 SWDIO/TMS = 1 TDI = 0 TDO = 0 nTRST = 0 nRESET = 1
Info : CMSIS-DAP: Interface ready
Info : clock speed 1000 kHz
Info : SWD DPIDR 0x0bb11477
Info : nrf51.cpu: hardware has 4 breakpoints, 2 watchpoints

The “4 breakpoints, 2 watchpoints” part indicates the debugging features the processor has available.

I mentioned that OpenOCD provides a GDB server so let’s connect to that right now:

$ arm-none-eabi-gdb -q target/thumbv6m-none-eabi/debug/rustled
Reading symbols from target/thumbv6m-none-eabi/debug/rustled...done.
(gdb)

This only opens a GDB shell. To actually connect to the OpenOCD GDB server, use the following command within the GDB shell:

(gdb) target remote :3333
Remote debugging using :3333
0x00000000 in ?? ()

By default OpenOCD’s GDB server listens on TCP port 3333 (localhost). This command is connecting to that port.

After entering this command, you’ll see new output in the OpenOCD terminal:

 Info : stm32f3x.cpu: hardware has 4 breakpoints, 2 watchpoints
+Info : accepting 'gdb' connection on tcp/3333
+Info : nRF51822-QFAA(build code: H0) 256kB Flash

Almost there. To flash the device, we’ll use the load command inside the GDB shell:

(gdb) load
Loading section .vector_table, size 0x188 lma 0x8000000
Loading section .text, size 0x38a lma 0x8000188
Loading section .rodata, size 0x8 lma 0x8000514
Start address 0x8000188, load size 1306
Transfer rate: 6 KB/sec, 435 bytes/write.

And that’s it. You’ll also see new output in the OpenOCD terminal.

 Info : flash size = 256kbytes
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target state: halted
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+Info : Unable to match requested speed 8000 kHz, using 4000 kHz
+adapter speed: 4000 kHz
+target state: halted
+target halted due to breakpoint, current mode: Thread
+xPSR: 0x61000000 pc: 0x2000003a msp: 0x2000a000
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+Info : Unable to match requested speed 1000 kHz, using 950 kHz
+adapter speed: 950 kHz
+target state: halted
+target halted due to debug-request, current mode: Thread
+xPSR: 0x01000000 pc: 0x08000194 msp: 0x2000a000

Our program is loaded, we can now run it!

(gdb) continue
Continuing.

Continue runs the program until the next breakpoint. This time it blocks, nothing happens. This is because all we have in our code is a loop!

.gdbinit

Before we move on though, we are going to add one more file to our project. This will automate the last few steps so we don’t need to repeatedly do the same actions in gdb:

.gdbinit
# Connects GDB to OpenOCD server port
target remote :3333
# (optional) Unmangle function names when debugging
set print asm-demangle on
# Load your program, breaks at entry
load
# (optional) Add breakpoint at function
break rustled::main
# Continue with execution
continue

Now we can learn how to debug code on the micro:bit.

Debugger

设置

在开始之前,让我们添加一些代码进行调试:

// -- snip --
entry!(main);
fn main() -> ! {
    let _y;
    let x = 42;
    _y = x;
    loop {}
}

GDB 会话

我们已经在调试会话中,所以让我们调试我们的程序。

load命令之后,我们的程序在它的入口点停止。这由 GDB 输出的“起始地址 0x8000XXX”部分指示。入口点是处理器/CPU 将首先执行的程序的一部分。

我提供给您的入门项目有一些在函数之前运行的额外代码main。此时,我们对“pre-main”部分不感兴趣,所以让我们直接跳到main函数的开头。我们将使用断点来做到这一点:

(gdb) break rustled::main
Breakpoint 1 at 0x8000218: file src/main.rs, line 8.

(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.

Breakpoint 1, rustled::main () at src/rustled/src/main.rs:13
13          let x = 42;

断点可用于停止程序的正常流程。该continue命令将使程序自由运行,直到到达断点。在这种情况下,直到它到达main函数,因为那里有一个断点。

请注意,GDB 输出显示“断点 1”。请记住,我们的处理器只能使用其中的四个断点,因此最好注意这些消息。

为了获得更好的调试体验,我们将使用 GDB 的文本用户界面 (TUI)。要进入该模式,请在 GDB shell 上输入以下命令:

(gdb) layout src

注意 向Windows 用户道歉。GNU ARM Embedded Toolchain 附带的 GDB 不支持此 TUI 模式:(

您可以随时使用以下命令退出 TUI 模式:

(gdb) tui disable

好的。我们现在处于main. 我们可以使用step命令逐句推进程序语句。所以让我们使用它两次来达到y = x语句。输入step一次后,您只需按 Enter 即可再次运行它。

(gdb) step
14           _y = x;

如果您没有使用 TUI 模式,则在每次step调用时 GDB 都会打印回当前语句及其行号。

我们现在“在”y = x声明;该语句尚未执行。这意味着x 已初始化但未初始化y。让我们使用以下print命令检查这些堆栈/局部变量:

(gdb) print x
$1 = 42

(gdb) print &x
$2 = (i32 *) 0x10001fdc

(gdb) print _y
$3 = 134219052

(gdb) print &_y
$4 = (i32 *) 0x10001fd8

正如预期的那样,x包含值42_y但是,包含值134219052(?)。因为_y还没有被初始化,所以包含了一些垃圾值。

该命令print &x打印变量的地址x。这里有趣的一点是 GDB 输出显示了引用的类型: i32*,一个指向i32值的指针。另一个有趣的事情是,地址x_y非常接近对方:他们的地址只是4个字节分开。

也可以使用以下info locals命令,而不是一一打印局部变量:

(gdb) info locals
x = 42
_y = 134219052

好的。使用 another step,我们将在loop {}语句之上:

(gdb) step
17          loop {}

并且_y现在应该被初始化。

(gdb) print _y
$5 = 42

如果我们step再次在loop {}语句之上使用,我们会卡住,因为程序永远不会通过该语句。相反,我们将使用layout asm 命令切换到反汇编视图,并使用 一次推进一条指令stepi

注意如果您step错误地使用了该命令并且 GDB 卡住了,您可以通过点击 来解除卡住Ctrl+C

(gdb) layout asm

如果您没有使用 TUI 模式,您可以使用该disassemble /m命令在您当前所在的行周围反汇编程序。

(gdb) disassemble /m
Dump of assembler code for function led_roulette::main:
11      fn main() -> ! {
   0x08000188 <+0>:     sub     sp, #8

12          let _y;
13          let x = 42;
   0x0800018a <+2>:     movs    r0, #42 ; 0x2a
   0x0800018c <+4>:     str     r0, [sp, #4]

14          _y = x;
   0x0800018e <+6>:     ldr     r0, [sp, #4]
   0x08000190 <+8>:     str     r0, [sp, #0]

15
16          // infinite loop; just so we don't leave this stack frame
17          loop {}
=> 0x08000192 <+10>:    b.n     0x8000194 <led_roulette::main+12>
   0x08000194 <+12>:    b.n     0x8000194 <led_roulette::main+12>

End of assembler dump.

看到=>左侧的粗箭头了吗?它显示了处理器接下来将执行的指令。

如果不在每个stepi命令的 TUI 模式内,GDB 将打印语句、行号处理器接下来将执行的指令的地址。

(gdb) stepi
0x08000194      17          loop {}

(gdb) stepi
0x08000194      17          loop {}

在我们转向更有趣的事情之前的最后一个技巧。在 GDB 中输入以下命令:

(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000188 msp: 0x10002000

(gdb) continue
Continuing.

Breakpoint 1, led_roulette::main () at src/main.rs:8
8           let x = 42;

我们现在回到了开头main

monitor reset halt将重置微控制器并在程序入口点停止它。下面的continue命令将让程序自由运行,直到它到达main有断点的函数。

当您错误地跳过您有兴趣检查的程序部分时,此组合非常方便。您可以轻松地将程序的状态回滚到最初的状态。

细则:此reset命令不会清除或触摸 RAM。该内存将保留其上次运行的值。不过,这应该不是问题,除非您的程序行为取决于未初始化变量的值,但这就是未定义行为(UB)的定义。

我们完成了这个调试会话。你可以用quit命令结束它。

(gdb) quit
A debugging session is active.

        Inferior 1 [Remote target] will be detached.

Quit anyway? (y or n) y
Detaching from program: $PWD/target/thumbv7em-none-eabihf/debug/led-roulette, Remote target
Ending remote debugging.

注意如果您不喜欢默认的 GDB CLI,请查看gdb-dashboard。它使用 Python 将默认的 GDB CLI 转换为显示寄存器、源代码视图、程序集视图和其他内容的仪表板。

但是不要关闭 OpenOCD!以后我们会一次又一次地使用它。最好让它继续运行。

Solution

这是我们迄今为止所做工作的回顾。

Cargo.toml

[package]
name = "start"
version = "0.2.0"

[dependencies]
panic-halt = "~0.2"
microbit="~0.7"
cortex-m-rt="~0.6"

Rust

#![no_std]
#![no_main]

extern crate cortex_m_rt;
extern crate microbit;
extern crate panic_halt;

use cortex_m_rt::entry;

#[entry]
fn main() -> ! {
    let _y;
    let x = 42;
    _y = x;
    loop {}
}

.cargo/config

# Configure builds for our target, the micro:bit's architecture
[target.thumbv6m-none-eabi]
# Execute binary using gdb when calling cargo run
runner = "arm-none-eabi-gdb"
# Tweak to the linking process required by the cortex-m-rt crate
rustflags = [
    "-C", "link-arg=-Tlink.x",
    # The LLD linker is selected by default
    #"-C", "linker=arm-none-eabi-ld",
]

# Automatically select this target when cargo building this project
[build]
target = "thumbv6m-none-eabi"

.gdbinit

# Connects GDB to OpenOCD server port
target remote :3333
# (optional) Unmangle function names when debugging
set print asm-demangle on
# Load your program, breaks at entry
load
# (optional) Add breakpoint at function
break main
# Continue with execution
continue

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录