From 963506628f7ba6ece11285492ebb355f79b46f36 Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Sat, 8 Sep 2018 22:30:35 +0200 Subject: [PATCH] writing your first embedded Rust program cc #4 --- src/blinky/blinky.md | 794 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 792 insertions(+), 2 deletions(-) diff --git a/src/blinky/blinky.md b/src/blinky/blinky.md index 16786bfb..5d4cb29b 100644 --- a/src/blinky/blinky.md +++ b/src/blinky/blinky.md @@ -1,3 +1,793 @@ -# Blinking your first LED +# Writing your first embedded Rust program -> ❌: This section has not yet been written. Please refer to [embedded-wg#117](https://github.com/rust-lang-nursery/embedded-wg/issues/117) for discussion of this section. \ No newline at end of file +> ❌: This section is work in progress. Please refer to +> [embedded-wg#117](https://github.com/rust-lang-nursery/embedded-wg/issues/117) +> for discussion of this section. + +In this section we'll walk you through the process of writing, building, +flashing and debugging an embedded Rust program. + +## Emulated device + +We'll start writing a program for the [LM3S6965], a Cortex-M3 microcontroller. +We have chosen this as our initial target because it can be emulated using QEMU +so you don't need to fiddle with hardware in this section and we can focus on +the tooling and the development process. + +[LM3S6965]: http://www.ti.com/product/LM3S6965 + +### A non standard Rust program + +We'll use the [`cortex-m-quickstart`] project template so go generate a new +project from it. + +[`cortex-m-quickstart`]: https://github.com/rust-embedded/cortex-m-quickstart#cortex-m-quickstart + +``` console +$ 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 +``` + +**IMPORTANT** We'll use the name "app" for the project name in this tutorial. +Whenever you see the word "app" you should replace it with the name you selected +for your project. Or, you could also name your project "app" and avoid the +substitutions. + +For convenience here's the source code of `src/main.rs`: + +``` console +$ cat src/main.rs +``` + +``` rust +#![no_std] +#![no_main] + +// pick a panicking behavior +extern crate panic_halt; // you can put a breakpoint on `rust_begin_unwind` to catch panics +// extern crate panic_abort; // requires nightly +// extern crate panic_itm; // logs messages over ITM; requires ITM support +// extern crate panic_semihosting; // logs messages to the host stderr; requires a debugger + +use cortex_m_rt::entry; + +#[entry] +fn main() -> ! { + loop { + // your code goes here + } +} +``` + +This program is a bit different from a standard Rust program so let's take a +closer look. + +`#![no_std]` indicates that this program will *not* link to the standard crate, +`std`. Instead it will link to its subset: the `core` crate. + +`#![no_main]` indicates that this program won't use the standard `main` +interface that most Rust programs use. The main (no pun intended) reason to go +with `no_main` is that using the `main` interface in `no_std` context requires +nightly. + +`extern crate panic_halt;`. This crate provides a `panic_handler` that defines +the panicking behavior of the program. More on this later on. + +[`#[entry]`] is an attribute provided by the [`cortex-m-rt`] crate that's used +to mark the entry point of the program. As we are not using the standard `main` +interface we need another way to indicate the entry point of the program and +that'd be `#[entry]`. + +[`#[entry]`]: https://rust-embedded.github.io/cortex-m-rt/0.6.1/cortex_m_rt_macros/fn.entry.html +[`cortex-m-rt`]: https://crates.io/crates/cortex-m-rt + +`fn main() -> !`. Our program will be the *only* process running on the target +hardware so we don't want it to end! We use a divergent function (the `-> !` +bit in the function signature) to ensure at compile time that'll be the case. + +### Cross compiling + +The next step is to *cross* compile the program for the Cortex-M3 architecture. +That's as simple as running `cargo build --target $TRIPLE` if you know what the +compilation target (`$TRIPLE`) should be. Luckily, the `.cargo/config` in the +template has the answer: + +``` console +$ 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) +``` + +To cross compile for the Cortex-M3 architecture we have to use +`thumbv7m-none-eabi`. This compilation target has been set as the default so the +two commands below do the same: + +``` console +$ cargo build --target thumbv7m-none-eabi + +$ cargo build +``` + +### Inspecting + +Now we have a non-native ELF binary in `target/thumbv7m-none-eabi/debug/app`. We +can inspect it using `cargo-binutils`. + +With `cargo-readobj` we can print the ELF headers to confirm that this is an ARM +binary. + +``` console +$ # `--bin app` is sugar for inspect the binary at `target/$TRIPLE/debug/app` +$ # `--bin app` will also (re)compile the binary, if necessary + +$ cargo readobj --bin app -- -file-headers +``` + +``` text +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` can print the size of the linker sections of the binary. + + + +``` console +$ # we use `--release` to inspect the optimized version + +$ cargo size --bin app --release -- -A +``` + +``` text +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 files contain metadata like debug information so their *size +on disk* does *not* accurately reflect the space the program will occupy when +flashed on a device. *Always* use `cargo-size` to check how big a binary really +is. + +`cargo-objdump` can be used to disassemble the binary. + +``` console +$ cargo objdump --bin app --release -- -disassemble -no-show-raw-insn -print-imm-hex +``` + + + +``` text +app: file format ELF32-arm-little + +Disassembly of section .text: +Reset: + 400: bl #0x36 + 404: movw r0, #0x0 + 408: movw r1, #0x0 + 40c: movt r0, #0x2000 + 410: movt r1, #0x2000 + 414: bl #0x2c + 418: movw r0, #0x0 + 41c: movw r1, #0x45c + 420: movw r2, #0x0 + 424: movt r0, #0x2000 + 428: movt r1, #0x0 + 42c: movt r2, #0x2000 + 430: bl #0x1c + 434: b #-0x4 + +UserHardFault_: + 436: b #-0x4 + +UsageFault: + 438: b #-0x4 + +__pre_init: + 43a: bx lr + +HardFault: + 43c: mrs r0, msp + 440: bl #-0xe + +__zero_bss: + 444: movs r2, #0x0 + 446: b #0x0 <__zero_bss+0x6> + 448: stm r0!, {r2} + 44a: cmp r0, r1 + 44c: blo #-0x8 <__zero_bss+0x4> + 44e: bx lr + +__init_data: + 450: b #0x2 <__init_data+0x6> + 452: ldm r1!, {r3} + 454: stm r0!, {r3} + 456: cmp r0, r2 + 458: blo #-0xa <__init_data+0x2> + 45a: bx lr +``` + +### Running + +Next, let's see how to run an embedded program on QEMU! This time we'll use the +`hello` example which actually does something. + +For convenience here's the source code of `src/main.rs`: + +``` console +$ cat examples/hello.rs +``` + +``` rust +//! Prints "Hello, world!" on the host console using semihosting + +#![no_main] +#![no_std] + +extern crate panic_halt; + +use core::fmt::Write; + +use cortex_m_rt::entry; +use cortex_m_semihosting::{debug, hio}; + +#[entry] +fn main() -> ! { + let mut stdout = hio::hstdout().unwrap(); + writeln!(stdout, "Hello, world!").unwrap(); + + // exit QEMU or the debugger section + debug::exit(debug::EXIT_SUCCESS); + + loop {} +} +``` + +This program uses something called semihosting to print text to the *host* +console. When using real hardware this requires a debug session but when using +QEMU this Just Works. + +Let's start by compiling the example: + +``` console +$ cargo build --example hello +``` + +The output binary will be located at +`target/thumbv7m-none-eabi/debug/examples/hello`. + +To run this binary on QEMU run the following command: + +``` console +$ 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! +``` + +The command should successfully exit (exit code = 0) after printing the text. On +*nix you can check that with the following command: + +``` console +$ echo $? +0 +``` + +Let me break down that long QEMU command for you: + +- `qemu-system-arm`. This is the QEMU emulator. There are a few variants of + these QEMU binaries; this one does full *system* emulation of *ARM* machines + hence the name. + +- `-cpu cortex-m3`. This tells QEMU to emulate a Cortex-M3 CPU. Specifying the + CPU model lets us catch some miscompilation errors: for example, running a + program compiled for the Cortex-M4F, which has a hardware FPU, will make QEMU + error during its execution. + +- `-machine lm3s6965evb`. This tells QEMU to emulate the LM3S6965EVB, a + evaluation board that contains a LM3S6965 microcontroller. + +- `-nographic`. This tells QEMU to not launch its GUI. + +- `-semihosting-config (..)`. This tells QEMU to enable semihosting. Semihosting + lets the emulated device, among other things, use the host stdout, stderr and + stdin and create files on the host. + +- `-kernel $file`. This tells QEMU which binary to load and run on the emulated + machine. + +Typing out that long QEMU command is too much work! We can set a custom runner +to simplify the process. `.cargo/config` has a commented out runner that invokes +QEMU; let's uncomment it: + +``` console +$ 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"] +``` + +This runner only applies to the `thumbv7m-none-eabi` target, which is our +default compilation target. Now `cargo run` will compile the program and run it +on QEMU: + +``` console +$ 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! +``` + +### Debugging + +Debugging is critical to embedded development. Let's see how it's done. + +Debugging an embedded device involves *remote* debugging as the program that we +want to debug won't be running on the machine that's running the debugger +program (GDB or LLDB). + +Remote debugging involves a client and a server. In a QEMU setup, the client +will be a GDB (or LLDB) process and the server will be the QEMU process that's +also running the embedded program. + +In this section we'll use the `hello` example we already compiled. + +The first debugging step is to launch QEMU in debugging mode: + +``` console +$ 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 +``` + +This command won't print anything to the console and will block the terminal. We +have passed two extra flags this time: + +- `-gdb tcp::3333`. This tells QEMU to wait for a GDB connection on TCP + port 3333. + +- `-S`. This tells QEMU to freeze the machine at startup. Without this the + program would have reached the end of main before we had a chance to launch + the debugger! + +Next we launch GDB in another terminal and tell it to load the debug symbols of +the example: + +``` console +$ $GDB -q target/thumbv7m-none-eabi/debug/examples/hello +``` + +**NOTE**: $GDB represents a GDB program capable of debugging ARM binaries. This +could be `gdb`, `arm-none-eabi-gdb` or `gdb-multiarch` depending on your +installation. + +Then within the GDB shell we connect to QEMU, which is waiting for a connection +on TCP port 3333. + +``` console +(gdb) 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() -> ! { +``` + +You'll see that the process is halted and that the program counter is pointing +to a function named `Reset`. That is the reset handler: what Cortex-M cores +execute upon booting. + +This reset handler will eventually call our main function. Let's skip all the +way there using a breakpoint and the `continue` command: + +``` console +(gdb) break main +Breakpoint 1 at 0x400: file examples/panic.rs, line 29. + +(gdb) continue +Continuing. + +Breakpoint 1, main () at examples/hello.rs:17 +17 let mut stdout = hio::hstdout().unwrap(); +``` + +We are now close to the code that prints "Hello, world!". Let's move forward +using the `next` command. + +``` console +(gdb) next +18 writeln!(stdout, "Hello, world!").unwrap(); + +(gdb) next +20 debug::exit(debug::EXIT_SUCCESS); +``` + +At this point you should see "Hello, world!" printed on the terminal that's +running `qemu-system-arm`. + +``` console +$ qemu-system-arm (..) +Hello, world! +``` + +Calling `next` again will terminate the QEMU process. + +``` console +(gdb) next +[Inferior 1 (Remote target) exited normally] +``` + +You can now exit the GDB session. + +``` console +(gdb) quit +``` + +## Real hardware + +By now you should be somewhat familiar with the tooling and the development +process. In this section we'll switch to real hardware; the process will remain +largely the same. Let's dive in. + +### Know your hardware + +Before we begin you need to identify some characteristics of the target device +as these will be used to configure the project: + +- The ARM core. e.g. Cortex-M3. + +- Does the ARM core include an FPU? Cortex-M4**F** and Cortex-M7**F** cores do. + +- How much Flash memory and RAM does the target device has? e.g. 40 KB of RAM + +- Where are Flash memory and RAM mapped in the address space? e.g. RAM is + commonly located at address `0x2000_0000`. + +You can find this information in the data sheet or the reference manual of your +device. + +In this section we'll be using our reference hardware, the STM32F3DISCOVERY. +This board contains an STM32F303VCT6 microcontroller. This microcontroller has: + +- A Cortex-M4F core that includes a single precision FPU + +- 256 KB of Flash located at address 0x0800_0000. + +- 40 KB of RAM located at address 0x2000_0000. (There's another RAM region but + for simplicity we'll ignore it). + +### Configuring + +We'll start from scratch with a fresh template instance: + +``` console +$ 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 +``` + +Step number one is to set a default compilation target in `.cargo/config`. + +``` console +$ 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) +``` + +We'll use `thumbv7em-none-eabihf` as that covers the Cortex-M4F core. + +The second step is to enter the memory region information into the `memory.x` +file. + +``` console +$ 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 +} +``` + +There's no step three. You can now cross compile programs using `cargo build` +and inspect the binaries using `cargo-binutils` as you did before. + +``` console +$ cargo build --example hello +``` + +### Debugging + +Debugging will look a bit different. In fact, the first steps can look different +depending on the target device. In this section we'll show the steps required to +debug a program running on the STM32F3DISCOVERY. This is meant to serve as a +reference; for device specific about debugging check out the [New Book +(temporary name)](https://github.com/rust-embedded/new-book). + +As before we'll do remote debugging and the client will be a GDB process. This +time, however, the server will be OpenOCD. + +On a terminal run `openocd` to connect to the ST-LINK on the discovery board. +Run this command from the root of the template; `openocd` will pick up the +`openocd.cfg` file which indicates which interface file and target file to use. + +``` console +$ cat openocd.cfg +``` + +``` text +source [find interface/stlink-v2-1.cfg] +source [find target/stm32f3x.cfg] +``` + +``` console +$ 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 +``` + +On another terminal run GDB, also from the root of the template. + +``` console +$ $GDB -q target/thumbv7em-none-eabihf/debug/examples/hello +``` + +Next connect GDB to OpenOCD, which is waiting for a TCP connection on port 3333. + +``` console +(gdb) target remote :3333 +Remote debugging using :3333 +0x00000000 in ?? () +``` + +Now proceed to *flash* (load) the program onto the microcontroller using the +`load` command. + +``` console +(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. +``` + +The program is now loaded. This program uses semihosting so before we do any +semihosting call we have to tell OpenOCD to enable semihosting. You can send +commands to OpenOCD using the `monitor` command. + +``` console +(gdb) monitor arm semihosting enable +semihosting is enabled +``` + +> You can see all the OpenOCD commands by invoking the `monitor help` command. + +Like before we can skip all the way to `main` using a breakpoint and the +`continue` command. + +``` console +(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(); +``` + +Advancing the program with `next` should produce the same results as before. + +``` console +(gdb) next +16 writeln!(stdout, "Hello, world!").unwrap(); + +(gdb) next +19 debug::exit(debug::EXIT_SUCCESS); +``` + +At this point you should see "Hello, world!" printed on the OpenOCD console, +among other stuff. + +``` console +$ 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 +``` + +Issuing another `next` will make the processor execute `debug::exit`. This acts +as a breakpoint and halts the process: + +``` console +(gdb) next + +Program received signal SIGTRAP, Trace/breakpoint trap. +0x0800141a in __syscall () +``` + +It also causes this to be printed to the OpenOCD console: + +``` console +$ 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 +``` + +However, the process running on the microcontroller has not terminated and you +can resume it using `continue` or a similar command. + +You can now exit GDB using the `quit` command. + +``` console +(gdb) quit +``` + +Debugging now requires a few more steps so we have packed all those steps into a +single GDB script named `openocd.gdb`. + +``` console +$ cat openocd.gdb +``` + +``` text +target remote :3333 + +# print demangled symbols +set print asm-demangle on + +# detect unhandled exceptions, hard faults and panics +break DefaultHandler +break UserHardFault +break rust_begin_unwind + +monitor arm semihosting enable + +load + +# start the process but immediately halt the processor +stepi +``` + +Now running `$GDB -x openocd.gdb $program` will immediately connect GDB to +OpenOCD, enable semihosting, load the program and start the process. + +Alternatively, you can turn `$GDB -x openocd.gdb` into a custom runner to make +`cargo run` build a program *and* start a GDB session. This runner is included +in `.cargo/config` but it's commented out. + +``` console +$ head -n8 .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 this to make `cargo run` start a GDB session +# NOTE: you may need to change `arm-none-eabi-gdb` +runner = "arm-none-eabi-gdb -x openocd.gdb" # <- +``` + +``` console +$ 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) +``` + +## I/O + +> **TODO**