QEMU

我们要开始给LM3S6965编程, LM3S6965是一个Cortex-M3微控制器. 我们选择这个作为开始是应为它能被QEMU模拟,所以你不必在本部分玩弄硬件,专心与工具与编程.

Important 在本教程中我们使用"app"作为项目名. 当你看到"app"这个词的时候,你应该把它换成你给你自己的项目起的名. 或者,你就可以把你的项目名设成"app".

创建一个不含标准库的Rust程序

我们会使用cortex-m-quickstart项目模板来生成一个新项目. 新创建的项目会包含一个基础结构: 一个对嵌入式Rust程序的好的开始. 这个项目还额外包括一个有着几个不同例子的example文件夹.

使用 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}}" -> "awesome-app"
version = "0.1.0"

# ..

[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "awesome-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"中修改占位符.

程序概览

为了方便,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_main主要原因是在no_std中使用main需要每夜版的Rust.

use panic_halt as _;.这个库提供一个定义panic行为的panic_handler. 我们会在Panicing章节中讨论更多细节.

#[entry]是一个由cortex-m-rt提供的属性,用来标记程序的入口. 当我们不用标准的main入口我们就需要其他的方式声明程序的入口,就是#[entry].

fn main() -> !.我们的程序只会运行在目标硬件上,所以我们不希望他停止! 我们使用一个发散函数 (->!符号)来确保编译期不会出问题.

交叉编译

下一步是为Cortex-M3架构进行交叉编译. 在知道编译目标时($TRIPLE)使用cargo build --target $TRIPLE会很方便. 很幸运,模板中的.cargo/config已经提供了答案.

tail -n6 .cargo/config
[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. 这个编译目标并不是自带的,如果你没有的话,现在就装:

rustup target add thumbv7m-none-eabi

如果thumbv7m-none-eabi已经.cargo/config中设为默认值,那下面这两条命令是一样的

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

A refresher on ELF linker sections

  • .text contains the program instructions
  • .rodata contains constant values like strings
  • .data contains statically allocated variables whose initial values are not zero
  • .bss also contains statically allocated variables whose initial values are zero
  • .vector_table is a non-standard section that we use to store the vector (interrupt) table
  • .ARM.attributes and the .debug_* sections contain metadata and will not be loaded onto the target when flashing the binary.

IMPORTANT: ELF文件包含了类似Debug信息等等元数据,所以他们的在磁盘上的大小不能 准确的反映烧录在硬件上的大小.通常使用cargo-size来检查二进制文件真正的大小

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

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

NOTE 如果以上命令报错Unknown command line argument, 可以看看这个bug: https://github.com/rust-embedded/book/issues/269

NOTE 这个根据不同系统有所区别.新版本的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这个例子来搞事.

为了方便,如下是example/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 {}
}

这个程序使用一个叫semihosting的东西来打印信息到宿主机. 等到了真正的硬件上,就需要一个调试会话才能用.

让我们来开始编译这个例子:

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!

这条命令应该在输出信息后成功推出(exit code = 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启动semihosting. Semihosting允许仿真设备使用主机的stdout, stderr和stdin,并且在主机上创建文件

  • -kernel $file.这告诉QEMU运行哪个二进制文件

输入这么长的命令太麻烦了!我们可以在.cargo/config中配置一个自定义的运行指令. 去掉注释:

head -n3 .cargo/config
[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 or 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在开始时冻结机器.如果没有这条命令, 还没等我们打开调试器,程序就已经运行到了末尾.

下一步我们在另一个终端中启动GDB,并让它加载示例的调试符:

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

注意取决于你在安装章节安装了哪一个,你可能需要其他版本的gdb而不是gdb-multiarch. 这可能是arm-none-eabi-gdb或就是gdb.

然后在GDB Shell中连接到QEMU,它正在TCP3333上等待连接.

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的函数. 那就是reset handler,MCU在启动时执行的.

注意在某些设置中,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()处

这个reset handler最终调用我们的main函数.让我们使用断点与continue跳过.要设置断点的话,首先让我们用list看一下在哪断点.

list main

这会展示example/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