QEMU

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

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

创建一个非标准的 Rust 程序

我们将使用 cortex-m-quickstart 项目模板从中生成一个新项目。创建的项目将包含一个最基本的应用程序:这是新的嵌入式 rust 应用程序的良好起点。此外,该项目还将包含一个 examples 目录,其中包含几个单独的应用程序,重点介绍一些关键的嵌入式 rust 功能。

使用 cargo-generate

首先安装 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}}" -> "app"
version = "0.1.0"

# ..

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

两者都不使用

获取 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,单击绿色的 “Clone or download” 按钮,然后单击 “Download ZIP”。

然后像 “使用 git” 版本的第二部分那样填写 Cargo.toml 文件中的占位符。

程序概述

为方便起见,这里是 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] 表示该程序不会链接到标准 crate std。相反,它将链接到它的子集:core crate。

#![no_main] 表示该程序不会使用大多数 Rust 程序使用的标准 main 接口。使用 no_main 的主要 (没有双关的意思) 原因是,在 no_std 上下文中使用 main 接口需要 nightly。

use panic_halt as _;。此 crate 提供了一个 panic_handler,用于定义程序的 panic 行为。我们将在本书的 Panicking 章节中更详细地介绍这一点。

#[entry]cortex-m-rt crate 提供的属性,用于标记程序的入口点。由于我们不使用标准的 main 接口,因此我们需要另一种方法来指示程序的入口点,那就是 #[entry]

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

交叉编译

下一步是为 Cortex-M3 架构交叉编译程序。如果您知道编译目标 ($TRIPLE) 应该是什么,那么只需运行 cargo build --target $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 中有一个非本地 ELF 二进制文件。我们可以使用 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 <main+0x4>

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

DefaultHandler_:
     656: b #-0x4 <DefaultHandler_>

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_>

HardFault_:
     662: b #-0x4 <HardFault_>

HardFault:
     663: <unknown>

运行

接下来,让我们看看如何在 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 模型可以让我们捕获一些错误编译错误:例如,运行为具有硬件 FPU 的 Cortex-M4F 编译的程序将使 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 内核在启动时执行的内容。

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

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

这是一个已知的小故障。您可以安全地忽略这些警告,您很可能位于 Reset()。

此重置处理程序最终将调用我们的 main 函数。让我们使用断点和 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

我们想在 “Hello, world!” 之前添加一个断点,该断点位于第 13 行。我们使用 break 命令执行此操作

break 13

现在,我们可以指示 gdb 使用 continue 命令运行到我们的 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);

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

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

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

next
[Inferior 1 (Remote target) exited normally]

您现在可以退出 GDB 会话。

quit