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
.
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 thatOption<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
The
reset_handler
function is going to callmain
immediately.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.
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.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.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:
rawhw
contains the aforementionedregister_block
definitions and therefore exports unsafe APIs only.devices
contains code to interface with various devices on a high level. This code uses the functions provided byrawhw
and exposes a safe interface to talk to the hardware units. The code in this module is highly unsafe and not threading aware, because the CPU does not have threads. The exposed safe interfaces are still a small lie, because ISRs are technically able to call them without an unsafe block. But since ISRs are unsafe code by definition I think this tradeoff is acceptable.app
contains the logic of the application. This includes a small DST-aware date-time structure and functions to rasterize the graphics. Programming this part felt like programming normal Rust, with the exception that I had to provide safe wrappers to call the unsafe floating point intrinsics .Insider tip: Implementing triangle rasterization is a very tedious and ugly task.
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.
- The language feels very good. No implicit integer conversions. No integer promotion.
I like the way how macros work. Feature flags instead if
#ifdef
s are great. Defined overflows are good, although I am not 100 % sold on the idea that overflows panic in debug builds. - The tooling could be better, but I am spoiled by Visual Studio, therefore it is going to be very difficult to meet my expectations. Although Visual Studio Code with the Rusty Code extension works well, the auto-completion is severely lacking. If I had to choose a single criticism from this list, it would be this one.
- cargo & xargo and the way building works is great but the functionality of xargo should probably be merged into cargo and build times are slow.
- I really like that tests can be written directly underneath the module and that testing works so flawlessly.
The unused-warnings are way too eager, especially in combination with test builds and feature flags. But I have no idea how to improve this. Maybe it would be better not to issue an unused-warning if there is at least one code path in any feature flag combination that uses the variable/module/parameter.
For example, in this application I have a logging macro that compiles to a no-op in release builds. But this leads to, arguably spurious, unused-variable-warnings.