Back to “Welcome to pushrax.com.”

Back to “E-Ink Clock.”

E-Ink Clock: Software

The LPC1227 is programmed using the Rust programming language. I have eyeballed with Rust since it was in beta but up until now I have not had the opportunity to build something interesting with it. Therefore the decision to go with Rust was not a technical one, but purely out of interest.

This article is not a tutorial or step by step description of how to use Rust with such a microcontroller, but merely a collection of things that I think are worthy to be documented. You can see the full code in the GitHub repository.

Toolchain

To build the project I use Rust nightly:

rustup 1.2.0 (70faf07 2017-04-08)
rustc 1.18.0-nightly (63c77214c 2017-04-24)
cargo 0.19.0-nightly (8326a3683 2017-04-19)
xargo 0.3.6
gcc version 5.4.1 20160609 (release)
    [ARM/embedded-5-branch revision 237715]
    (GNU Tools for ARM Embedded Processors)

I used xargo, a cargo drop-in replacement, to do the cross-compilation from my Windows 10 AMD64 machine to the ARMv6 (Thumb) target processor. Installing xargo via cargo and working with xargo is easy and works flawlessly. One just has to type xargo instead of cargo and specify the target platform.

Apropos target platform. To get the program building I had to create my own target specification, mostly because I wanted to control the linker flags in detail. Creating a target specification is not so easy because it is not documented well. Most of the required information can be found in an internal module of rustc.

If this file is called lpc1227-none-eabi.json the build can be invoked by executing xargo build --target lpc1227-none-eabi --release.

{
    "linker": "arm-none-eabi-gcc",
    "linker-is-gnu": true,
    "linker-flavor": "gcc",
    "pre-link-args": {
        "gcc": ["-nostartfiles", "-nodefaultlibs", "-nostdlib", "-mthumb", "-mcpu=cortex-m0", "-mfloat-abi=soft", "-Tlinker.ld"]
    },
    "post-link-args": {
        "gcc": ["-lm", "-lgcc"]
    },
    "executables": true,
    "panic-strategy": "abort",
    "relocation-model": "static",
    "llvm-target": "thumbv6m-none-eabi",
    "target-endian": "little",
    "target-pointer-width": "32",
    "data-layout": "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64",
    "arch": "arm",
    "os": "none",
    "env": "",
    "vendor": "",
    "features": "+strict-align",
    "max-atomic-width": 0,
    "cpu": "cortex-m0"
}

Most of these things are copied from the original thumbv6m specification. The only interesting thing are the linker arguments. Since the project does need a C-runtime library I disabled it in the linker flags. I do still need some intrinsics, e.g., division, and therefore have to link against libgcc. Additionally I link against libm because I need trigonometric functions.

Since this is an embedded target and there is no out-of-the-box embedded Rust IDE that is going to do this for me I had to provide my own linker script. Like most linker scripts it is pure magic. Basically I divide the RAM (8 KiB) in four sections: 2 KiB stack space, a data and BSS section and the rest for the heap. The flash memory contains all static data and the ISR pointers at offset 0.

Microcontroller Startup in Rust

Describing all language features and code lines required to execute Rust code on the microcontroller is way too much for this article, but I will try to give an overview of all required steps in the startup sequence. Most things described here happen in timekeeper.rs.

  1. After power up, the microcontroller looks in the ISR-table for the pointer to the startup function and the initial stack-pointer. Both addresses are provided by Rust using a static variable that has a special link_section which is eventually referenced by the linker script. Interesting to note here is that Rust guarantees that Option<unsafe extern fn()> is either a valid address or a null-pointer, i.e., there is no “tagged union overhead.”

    #[cfg(not(test))]
    #[link_section = ".isr_vectors"]
    #[no_mangle]
    pub static ISR_VECTORS: [Option<unsafe extern fn()>; 47] = [
        Some(__stack_end__),  // Initial SP
        Some(reset_handler),  // Reset handler
        Some(nmi_handler),  // NMI handler
        Some(hard_fault_handler),  // Hard fault handler
    
  2. The reset_handler function is going to call main immediately.

  3. The first thing I have to do is to disable the Watchdog Timer, otherwise the CPU is going to reset itself after some time. After that I initialize critical hardware like the E-Ink power supplies and the CPU itself.

  4. Until now no global data structures have been initialized, each access to a static variable could lead to a crash or dangerous behavior. Since there is no C-runtime involved I have to manually copy the initial static data from the flash memory into the RAM and zero the BSS segment in the RAM. This is both done now:

    // Zero .bss
    let bss_size = (&__bss_end__ as *const u32 as usize) - (&__bss_start__ as *const u32 as usize);
    ptr::write_bytes(&mut __bss_start__, 0, bss_size / 4);
    
    // Copy .data
    let data_size = (&__data_end__ as *const u32 as usize) - (&__data_start__ as *const u32 as usize);
    ptr::copy_nonoverlapping(&__data_source_start__, &mut __data_start__, data_size / 4);
    

    The __*_start__ and __*_end__ variables are exported by the linker script.

  5. Since there is no C-runtime there is no possibility to dynamically allocate memory. To overcome this I used an existing memory allocator, wrapped it into an #![allocator] crate, and referenced this crate from the main crate. The only thing left to do now was to call its initialization function.

  6. After all these steps I initialize the rest of the hardware.

Additionally I had to provide some intrinsics. I defined them in Rust as well and delegated as much as possible to rlibc. To make this work I also had to add #![feature(core_intrinsics)] to the file. This is to avoid that the compiler optimizes my implementation of e.g. memcpy with a call to memcpy, thus leading to an endless recursion. Here is an example of such an intrinsic:

#[cfg(not(test))]
#[no_mangle]
pub unsafe extern fn __aeabi_memcpy4(dest: *mut u8, src: *const u8, n: usize) -> *mut u8 {
    rlibc::memcpy(dest, src, n)
}

Talking With the Hardware

The ARM architecture does not have special I/O-instructions but works with mapped memory. This makes interfacing the hardware easy, because I just have to obtain a pointer to the correct location and perform a volatile read or write. Since most hardware registers are split in fields, I defined a macro that adds some convenience when interfacing with such registers. Thanks to this abstraction, I never had to use bitwise arithmetic when working with the hardware registers.

For example the a part of the registers concerning the SSP controller looks like this:

register_block! {
    cr0 0x40040000 => {
        0,3, dss, u8;
        4,5, frf, FrameFormat;
        6, cpol, bool;
        7, cpha, bool;
        8,15, scr, u8;
    }
    cr1 0x40040004, andmask 0b1111 => {
        0, lbm, bool;
        1, sse, bool;
        2, ms, bool;
        3, sod, bool;
    }

This macro defines unsafe functions to access the fields, for example this allows me to invoke rawhw::ssp::cr1::dss::get() or ::set(12). The only issue I had with this abstraction is that there are some registers that have a side-effect on reading. Since set has to read the register to set a field this can lead to very confusing side effects.

This could be improved by having read-only, write-only and read-write registers and by introducing some kind of transactional semantic to avoid unnecessary reads when setting a field.

Talking With the Debugger

Cortex CPUs have a very convenient feature called semihosting that allows the embedded program to print directly to the host PC if the debugging interface is attached. To use this feature one has to prepare certain registers and then invoke a special instruction. Thanks to inline assembly this was easy to achieve in Rust.

#[cfg(feature = "semihosted")]
pub unsafe fn write0(ptr: *const u8) {
    // See newlib for details (libgloss/arm/swi.h)
    asm!(
        "bkpt 0xAB"
        :  // Outputs
        : "{r0}"(0x04), "{r1}"(ptr)  // Inputs
        : "r0", "r1", "r2", "r3", "ip", "lr", "memory", "cc"  // Clobbers
    );
}

I was not able to achieve a pleasant debugging experience, but in theory it should be possible to attach to the GDB server that for example LPCXpresso (an Eclipse based IDE for LPC microcontrollers) starts when flashing and debugging the target device. At least I was able to trick LPCXpresso into placing breakpoints by forcing it to open .rs files as C source code files.

A Short Summary of the Architecture

The project is split in three layers:

Conclusion About Rust

Please take these thoughts with a grain of salt. As we all know, it is much easier to criticize than to actually improve something.