Skip to content

sigpwny/tm4c-hello-world

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cortex-m-quickstart

A template for building applications for ARM Cortex-M microcontrollers

This project is developed and maintained by the Cortex-M team.

Dependencies

To build embedded programs using this template you'll need:

  • Nightly Rust toolchain from 2018-08-28 or newer: rustup default nightly
  • The cargo generate subcommand: cargo install cargo-generate
  • rust-std components (pre-compiled core crate) for the ARM Cortex-M targets. Run:
$ rustup target add thumbv6m-none-eabi thumbv7m-none-eabi thumbv7em-none-eabi thumbv7em-none-eabihf

Non-dependencies

This section list programs that are not required to build embedded programs but are useful or necessary for embedded development.

To flash (put the firmware on the target device) and debug embedded programs you'll need these additional programs:

  • GDB with ARM support or LLDB, for debugging.
  • QEMU with ARM support, for running embedded programs on the build machine.
  • OpenOCD and similar, which make debugging possible at all.

To inspect the produced binaries you'll want cargo-binutils.

Usage

First timers

Building

If you are already familiar with the process of building and debugging embedded Rust programs feel free to skip this section!

To get you familiar with building and debugging embedded Rust programs we'll use the template with its default values to build a program for the LM3S6965, a microcontroller with a Cortex-M3 core that QEMU can emulate.

In this section we'll use a debugger (GDB or LLDB) and QEMU. Be sure to have them installed!

  1. Initialize the template
$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
  1. Build the example "Hello, world!" program
// example/hello.rs
#![no_main]
#![no_std]

#[macro_use]
extern crate cortex_m_rt;
extern crate cortex_m_semihosting as sh;
extern crate panic_semihosting;

use core::fmt::Write;

use sh::hio;

entry!(main);

fn main() -> ! {
    let mut stdout = hio::hstdout().unwrap();
    writeln!(stdout, "Hello, world!").unwrap();

    loop {}
}

We'll cross compile the program for the thumbv7m-none-eabi target. This target covers Cortex-M3 devices like the one we are targeting.

$ cargo build --target thumbv7m-none-eabi --example hello

You'll find the output binary in the following path: target/thumbv7m-none-eabi/debug/examples/hello. The output is an ELF file.

If you have file installed you can print the file type of the output to confirm it's an ELF file:

$ ( cd target/thumbv7m-none-eabi/debug/examples && file hello )
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

If you have cargo-binutils installed you can look at the ELF header of the output:

NOTE cargo-readelf will be available in a future release of binutils (v0.1.3)

$ cargo readelf --example hello -- --file-headers
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:               0x23A3
  Start of program headers:          52 (bytes into file)
  Start of section headers:          673340 (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:         21
  Section header string table index: 19

If you look at the bottom of the Cargo configuration file (.cargo/config) you'll see that the thumbv7m-none-eabi target has been set as the default compilation target. This means that you do not need to pass the --target flag to cross compile because it's already implied. So:

$ cargo build --example hello

Does the same as before.

  1. There's no step 3! Your build is done, but for completeness let's look at how to run this program on QEMU.

Running the program on QEMU

Execute this command and you are done.

$ 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 above command will block the terminal because the hello program never ends! To terminate QEMU input this in your terminal: Ctrl+A, followed by X.

Let me break down that long 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 flash (kind of) and run on the emulated machine.

Debugging

First, we launch a QEMU instance:

$ 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

Note that this command won't print anything to the console but 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 start up LLDB in another terminal:

$ lldb target/thumbv7m-none-eabi/debug/examples/hello

And tell it to connect to the GDB server that QEMU created:

(lldb) gdb-remote 3333
Process 1 stopped
* thread #1, stop reason = signal SIGTRAP
    frame #0: 0x000023a2 hello`Reset at lib.rs:475
   472
   473  #[doc(hidden)]
   474  #[no_mangle]
-> 475  pub unsafe extern "C" fn Reset() -> ! {
   476      extern "C" {
   477          // This symbol will be provided by the user via the `entry!` macro
   478          fn main() -> !;

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:

(lldb) breakpoint set -name hello::main
Breakpoint 1: where = hello`hello::main::h9cf19a1378cbd1b8 + 2 at hello.rs:20, address = 0x000006a8

(lldb) continue
Process 1 resuming
Process 1 stopped
* thread #1, stop reason = breakpoint 1.1
    frame #0: 0x000006a8 hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:20
   17   entry!(main);
   18
   19   fn main() -> ! {
-> 20       let mut stdout = hio::hstdout().unwrap();
   21       writeln!(stdout, "Hello, world!").unwrap();
   22
   23       loop {}

We are now close to the code that prints "Hello, world!". Let's move the program forward using the next command.

(lldb) next
Process 1 stopped
* thread #1, stop reason = step over
    frame #0: 0x000006be hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:21
   18
   19   fn main() -> ! {
   20       let mut stdout = hio::hstdout().unwrap();
-> 21       writeln!(stdout, "Hello, world!").unwrap();
   22
   23       loop {}
   24   }

(lldb) next
Process 1 stopped
* thread #1, stop reason = step over
    frame #0: 0x000006f6 hello`hello::main::h9cf19a1378cbd1b8 at hello.rs:23
   20       let mut stdout = hio::hstdout().unwrap();
   21       writeln!(stdout, "Hello, world!").unwrap();
   22
-> 23       loop {}
   24   }

At this point you should see "Hello, world!" printed on the terminal that's running qemu-system-arm.

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

That's it! You can now exit lldb, which will also cause QEMU to terminate.

(lldb) exit
Quitting LLDB will kill one or more processes. Do you really want to proceed: [Y/n] y

Not first timers

Building

This section explains how to configure this template to build programs for some specific Cortex-M device.

  1. Before everything else, you need to know the characteristics of the target device:
  • What's the ARM core? e.g. Cortex-M3.

  • Does the ARM core include an FPU? e.g. the Cortex-M4F does.

  • How much Flash memory and RAM does the target device has? e.g. 40 KB of RAM

  • Where is Flash memory and RAM mapped in the address space? e.g. 0x2000_0000 is common for RAM.

You should be able to find this information in the data sheet and / or reference manual of your device.

As an example, we'll use the STM32F303VCT6 microcontroller. This device 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).

  1. Initialize the template
$ cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
  1. Set the default compilation target in .cargo/config.

For the STM32F303VCT6, we pick the thumbv7em-none-eabihf target as that covers the Cortex-M4F core.

$ tail .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)
  1. Enter the memory region information into memory.x.

As we mentioned before the STM32F303VCT6 has 40 KB of RAM located at address 0x2000_0000 and 256 KB of Flash memory located at address 0x0800_0000.

$ cat memory.x
MEMORY
{
  /* NOTE K = KiBi = 1024 bytes */
  FLASH : ORIGIN = 0x08000000, LENGTH = 256K
  RAM : ORIGIN = 0x20000000, LENGTH = 40K
}
  1. Build one of the examples:
$ cargo build --example hello

You are done! You should be able to flash and run this example on your device.

Debugging

Teaching you how to flash and debug this program on your device is out of scope for this document due to the sheer variety of possible hardware / software combinations. But the steps required to flash and debug this program on the STM32F3DISCOVERY using OpenOCD are provided below as a reference.

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.

$ cat openocd.cfg
source [find interface/stlink-v2-1.cfg]
source [find target/stm32f3x.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 <transport>'.
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.

$ cat openocd.gdb
target remote :3333

# print demangled symbols
set print asm-demangle on

# detect unhandled exceptions and hard faults
break DefaultHandler
break UserHardFault

monitor arm semihosting enable

load
$ arm-none-eabi-gdb -x openocd.gdb target/thumbv7em-none-eabihf/debug/examples/hello
(..)
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x21dc lma 0x8000400
Loading section .rodata, size 0x6a4 lma 0x80025e0
Start address 0x800238c, load size 11392
Transfer rate: 17 KB/sec, 3797 bytes/write.

(gdb) list
470     #[no_mangle]
471     pub static __RESET_VECTOR: unsafe extern "C" fn() -> ! = Reset;
472
473     #[doc(hidden)]
474     #[no_mangle]
475     pub unsafe extern "C" fn Reset() -> ! {
476         extern "C" {
477             // This symbol will be provided by the user via the `entry!` macro
478             fn main() -> !;
479

The openocd.gdb script will connect GDB to OpenOCD and then flash the program into the device. After that you can do a normal debugging session.

If you continue the program past the semihosting write operation you'll see "Hello, world" printed on the OpenOCD console.

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

Next steps

TODO point the reader to embedded-hal, awesome-embedded-rust, etc.

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Code of Conduct

Contribution to this crate is organized under the terms of the Rust Code of Conduct, the maintainer of this crate, the Cortex-M team, promises to intervene to uphold that code of conduct.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published