Introduction

WARNING: This is only published because it's convenient, don't use this for anything

(feel free to skip sections you know about)

Scope

The following topics are covered in the core chapters:

  • How to write, build and flash an embedded program.
  • Basic operation of a GPIO, ubiquitous in microcontrollers.

Non-goals

  • Teaching electric circuit theory or electronics.

    We'll cover the minimum required to understand how some devices work along the way.

  • Covering Rustic, low level details.

    We won't be talking about linker scripts, the boot process, or how to glue those two into a minimally working Rust program.

Rust

This book is meant to be understandable for non-rust users, but not sufficient for doing anything with the information contained herein. For learning rust itself, the Rust Book is often recommended. While the syntax will no be explained, a couple necessary key concepts are listed here:

  • Traits

    Traits are comparable to interfaces in other languages

  • Crates

    Crates are the compilation unit of the rust compiler and can either be runnable binaries or libraries to be used in other crates

  • no_std

    The default rust standard library std contains features that aren't necessarily available on all targets, such as memory allocation or network support. For such targets, a stripped down version, core, is provided, that only contains features that don't require os support.

Rust embedded ecosystem

The rust embedded ecosystem, as propagated by the embedded-wg, is based on several foundational crates (we're only considering arm devices here):

  • cortex-m and cortex-m-rt

    These crates provide abstractions for the ARM Cortex-M cpus and the runtime. This includes initializing ram, implementing interrupts and much more.

  • embedded-hal

    The embedded-hal crate includes various traits, that can be used by device drivers to interface with external components, without being the specific to one MCU. The traits are usually implement by a microcontroller-specific hal.

  • PACs

    The Peripheral Access Crates are used as a basic interface to the registers of the peripherals included in the microcontroller. Most are generated using svd2rust, which translated a manufacturer-provided svd file to rust code.

  • HALs

    Hardware Abstraction Libraries wrap the code generated by the PACs into something more usable and provide safe abstractions for the peripherals. These abstractions usually consist of some chip-specific setup and an implementation of the matching embedded-hal traits for actually using the peripheral

There are also some other ecosystems, which aren't discussed here, but are also useful:

Most of the crates in the rust embedded ecosystem are listed in the awesome-embedded-rust list.

Background

What's a microcontroller?

A microcontroller is a system on a chip. Whereas your laptop is made up of several discrete components: a processor, RAM sticks, a hard drive, an ethernet port, etc.; a microcontroller has all those components built into a single "chip" or package. This makes it possible to build systems with minimal part count.

What can you do with a microcontroller?

Lots of things! Microcontrollers are the central part of systems known as embedded systems. These systems are everywhere but you don't usually notice them. These systems control the brakes of your car, wash your clothes, print your documents, keep you warm, keep you cool, optimize the fuel consumption of your car, etc.

The main trait of these systems is that they operate without user intervention even if they expose a user interface like a washing machine does; most of their operation is done on their own.

The other common trait of these systems is that they control a process. And for that these systems usually have one or more sensors and one or more actuators. For example, an HVAC system has several sensors, thermometers and humidity sensors spread across some area, and several actuators as well, heating elements and fans connected to ducts.

When should I use a microcontroller?

All these application I've mentioned, you can probably implement with a Raspberry Pi, a computer that runs Linux. Why should I bother with a microcontroller that operates without an OS? Sounds like it would be harder to develop a program.

The main reason is cost. A microcontroller is much cheaper than a general purpose computer. Not only the microcontroller is cheaper; it also requires many fewer external electrical components to operate. This makes Printed Circuit Boards (PCB) smaller and cheaper to design and manufacture.

The other big reason is power consumption. A microcontroller consumes orders of magnitude less power than a full blown processor. If your application will run on batteries that makes a huge difference.

And last but not least: (hard) real time constraints. Some processes require their controllers to respond to some events within some time interval (e.g. a quadcopter/drone hit by a wind gust). If this deadline is not met, the process could end in catastrophic failure (e.g. the drone crashes to the ground). A general purpose computer running a general purpose OS has many services running in the background. This makes it hard to guarantee execution of a program within tight time constraints.

When should I not use a microcontroller?

Where heavy computations are involved. To keep their power consumption low, microcontrollers have very limited computational resources available to them. For example, some microcontrollers don't even have hardware support for floating point operations. On those devices, performing a simple addition of single precision numbers can take hundreds of CPU cycles.

What's Rust?

To quote Wikipedia: "Rust is a multi-paradigm system programming language focused on safety, especially safe concurrency. Rust is syntactically similar to C++, but is designed to provide better memory safety while maintaining high performance."

Rust is a relatively new programming language, that tries to tackle the fields long held by C/C++: Low-level programming, without much overhead. In contrast it's much more ambitious in it's memory management: It tries to do it's best to ensure that no memory corruption is possible. This includes race conditions, buffer overflows, use after free and much more.

It also has many other benefits that a lot of other newer languages share: An integrated build tool, cargo, an opinionated code formatter, rustfmt, that much of the community has converged to and dependency management, that simplifies sharing code & common interfaces.

Meet your hardware

TODO Add an annotated picture

Parts

The LPC845-BRK consists of two microcontrollers:

  • LPC845

    That's the chip you'll be programming

  • LPC11U35

    That's the chip in the upper left corner. It acts as a debug bridge, allowing you to program, debug and communicate with the LPC845. It runs a firmware that implements the CMSIS-DAP protocol.

It also has some extra things:

  • RGB Led

    This small thing contains three leds, red, green and blue

  • Buttons

    Things you can press

  • Potentiometer

    An adjustable resistor, allows you to set a voltage by using it as a voltage divider

  • Capacitive Button

    A button that can be activated by touching it. Unfortunately, getting it set up is pretty involved, so we're not going to do that.

Development environment setup

Dealing with microcontrollers involves several tools, as we'll be dealing with an architecture different than your laptop's, and we'll have to run and debug programs on a "remote" device.

Documentation

Without documentation it is pretty much impossible to work with microcontrollers.

Tools

We'll use all the tools listed below. Where a minimum version is not specified, any recent version should work but we have listed the version we have tested.

  • Cargo & rustc.
  • picocom or similar on linux and macOS
  • PuTTY on Windows.

Next, follow OS-agnostic installation instructions for a the tools:

Tools

Install rustup by following the instructions at https://rustup.rs.

Follow the instructions to get the latest stable version of rust. To make sure, just execute

rustup default stable

Then you'll need to install the thumbv6m-none-eabi target for the cortex-m0 cpu inside the lpc845:

rustup target add thumbv6m-none-eabi

Then we'll install cargo-flash using cargo:

cargo install cargo-flash

It's from the great probe-rs project, and is used for getting your code onto the microcontroller.

Linux

Unfortunately, most linux distributions don't allow non-root applications to talk directly to usb devices. This means, you'd need to use root to talk to the debug probe, but we can just use a udev rule to fix this for these devices.

You wanna add the following into a file under /etc/udev/rules.d/, for example 70-cmsis-dap.rules

# CMSIS-DAP devices
SUBSYSTEMS=="usb", ATTRS{product}=="*CMSIS-DAP*", GROUP="wheel", MODE="0660"

Now tell udev to reload the rules with

sudo udevadm control --reload

Verify your setup

First, clone the repo of the lpc8xx-hal:

git clone https://github.com/lpc-rs/lpc8xx-hal/

Enter the directory and flash the program onto your microcontroller with

cargo flash --chip LPC845M301JBD48 --example gpio_delay --release --features 845-r

Now the blue led on your board should blink

GPIO Output

Most pins in a microcontroller are usually General Purpose Input Output pins. These can either output signals or read signals applied to them. They can be used for various applications, but in this case, we're using an led connected to the P1.1 pin.

Getting to blinky

  1. Go to the quickstart folder & copy it somewhere else. This is the binary crate we're going to work with

  2. Look at what's in there

    There are already various things in the folder. For now, we can pretty much ignore every non-source file, but if you're curious, this is why they're used

    • .cargo/config

      In here, the target architecture for our program is defined. See the embedonomicon for more details on architecture

    • memory.x

      This defines the amount and the location of flash & RAM for the target.

    • openocd.cfg

      Openocd needs some configuration to work. This contains it.

So, what's in the src/main.rs?

There are a couple things needed, that aren't used in normal rust programs.

Let's go through them line-by-line:

  • #![no_std]

    The std library isn't implemented for microcontrollers, so it's disabled.

    This needs to be done in all binary & library crates

  • #![no_main]

    An embedded

  1. The first thing that's needed when starting is getting access to the peripherals.

    
    #![allow(unused_variables)]
    fn main() {
      let p = lpc8xx_hal::Peripherals::take().unwrap();
    }
    
  2. Next, the system controller SYSCON, GPIO and the switch matrix SWM, that's controlling what each pin is used for, are brought into a usable state. This means

    
    #![allow(unused_variables)]
    fn main() {
      let swm = p.SWM.split();
      let mut syscon = p.SYSCON.split();
      let gpio = p.GPIO.enable(&mut syscon.handle);
    }
    
  3. The blue led is connected to PIO1_1. To use it as a gpio pin, it needs to be configured as such in the switch matrix.

    
    #![allow(unused_variables)]
    fn main() {
      let led = swm.pins.pio1_1.into_gpio_pin(&gpio);
    }
    
  4. Now that it's a gpio pin, it can be configured to act as an output

    
    #![allow(unused_variables)]
    fn main() {
      let mut led = led.into_output();
    }
    
  5. Now all the needed setup is done, the only thing left is getting the LED to blink.

    This can be done by setting the pin high & low in a loop with the OutputPin trait. Unfortunately this is way too fast for any human to see. It can be slowed down, by repeatedly setting it high/low in a loop, since setting the pin takes some amount of time. A value of 1 000 000 works well in release mode.

    
    #![allow(unused_variables)]
    fn main() {
      loop {
          for _ in 0..1_000_000 {
              led.set_high().unwrap();
          }
          for _ in 0..1_000_000 {
              led.set_low().unwrap();
          }
      }
    }
    
  6. Last but not least, we still need to run & compile it. This can be done by executing cargo flash --release --chip LPC845M301JBD48. If everything was done correctly, the blue led should blink now.

Please note that the led is active low, it's on when the pin is set to 0V with set_low().

Now you can try a few more things with your newly gained knowledge:

  • Try blinking the other two leds, red on PIO0_2 and green on PIO0_0
  • By quickly turning the led on/off, the human eye perceives the led having a lowered brightness. This technique is called PWM and is almost always used for adjusting the brightness.

Delay

embedded-hal provides the delay trait for generating precise delays. The HAL provides an implementation which you can access by using the SYST in the Peripherals for the Delay struct.

Try using it to blink the led and check if it really delays the right amount of time.

Driver crate

We can now use other driver crates to add more functionality. For this example, we're using the embedded-morse crate to output morse messages over the led.

Add the crate to the Cargo.toml to the [dependencies] section and make an instance in the src/main.rs:


#![allow(unused_variables)]
fn main() {
let mut delay = lpc8xx_hal::delay::Delay::new(p.SYST);
// the last argument inverts the output, since the led is active-low
let mut morse = embedded_morse::Morse::new_default(delay.clone(), led, true);
}

Now output something using output_str.

The embedded-morse driver uses only traits from embedded-hal. Because of that, it will work on a large swath of microcontrollers, without needing to modify it.

GPIO Input

Until now, the GPIO pins were only used in the output state. As the name implies, they can also be used as inputs. And, as was already covered, there are multiple buttons on the board we can use.

Contact bounce

As buttons are phyiscal devices, they're unfortunately not perfect. When pressing or depressing a button it flips between the pressed & not state for a short amount of time, which needs to be kept in mind. The act of filtering this out is called debouncing, a common way to do it is to simply wait a few milliseconds after the input is changed and ignore all the changes in the meantime.

Controlling the led

You can switch gpio pin to input mode with: TODO: impl Now, the InputPin trait can be used to check the state. Try controlling the led with the button, so it's on when the button is pressed and off when not.

Next, try to switch the led state with ToggleableOutputPin. TODO: impl

Here, you'll need to consider debouncing the input signal, since otherwise the led can be toggled so fast, that you won't notice.

Serial communication

PWM

Further Ideas

  • DS18B20

Acknowledgments

This book is based in part on microrust and the discovery book.