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
,单击绿色的“克隆或下载”按钮,然后单击“下载 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]
表示此程序不会使用大多数 Rust 程序使用的标准 main
接口。使用 no_std
上下文中的 main
接口的主要(双关语)原因是它需要 nightly。
use panic_halt as _;
。这个库提供了一个 panic_handler
,它定义了程序的恐慌行为。我们将在本书的 恐慌 章中更详细地介绍它。
#[entry]
是由 cortex-m-rt
库提供的属性,用于标记程序的入口点。由于我们没有使用标准的 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
中有一个非本机 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 模型可以让我们捕获一些错误编译:例如,运行为 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 内核在启动时执行的内容。
请注意,在某些设置中,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 运行到我们的 main 函数,使用 continue
命令。
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