Rust on the Teensy 4.0 - Teensycore

2022-08-22

Rust is legendary for cross-compiling to all kinds of platforms. Variations of the ARM chipset are among the list. A while ago I set out to create a baremetal kernel for the Teensy 4.0 written in rust. I quickly realized it would be fairly trivial to extract my code into a reusable platform. Thus, teensycore was built and you too can enjoy this extremely lightweight kernel!

Teensycore is entirely open source and contributions are welcome!

Resources

In this article, I will explain some of the features that come built-in to teensycore and provide a few examples for how to use it. First, a brief overview of the functionality. Functionality

At the time of this writing, teensycore is fully equipped with the following features:

  • Native support for all 5 UART peripherals
  • Software i2c
  • Periodic timers
  • Gpio addressing based on the pinout numbers
  • Fully-functional interrupt system
  • To-the-nanosecond wait()
  • 132MHz of beautiful clock speed
  • Hardware-optimized floating point math

In addition to these hardware-level systems, teensycore sports a very lightweight “standard library” (of dubious quality), which includes data structures like:

  • Linked-list
  • BTreeMap
  • Fixed-size array with queue/stack operations
  • Variable-size vector with queue/stack operations
  • Paged memory with a rudimentary reclamation system
  • Stack-backed strings
  • And more!

The reason teensycore has these things is because it is #![no_std] compliant. So the standard libraries we know and love aren’t available.

Now on to an example. Let’s take a look at everyone’s favorite: blinky.

Blinky

We shall blink the built-in LED. No wires needed! Here’s a step-by-step guide for starting a new teensycore project from scratch.

Compiling the Project

First, you’ll need to install a few build tools. Although we’re using cargo for the heavy lifting, there is a tiny bit of c, some linker action, and a bunch of cargo flags that happen behind the scenes.

sudo apt install gcc-arm-none-eabi jq

Next, you’ll need to configure rust so it can leverage the proper toolchain.

rustup default nightly
rustup target add thumbv7em-none-eabi

You’re now ready to create a teensycore project.

cargo new blinky --lib
cd blinky

Modify your Cargo.toml file so it has this lib section and include teensycore in the dependencies.

[package]
name = "blinky"
version = "0.1.0"
edition = "2021"[lib]
crate-type = ["staticlib"]
path = "src/lib.rs"

[dependencies]
teensycore = "^0.0.11"

Most of that is all standard stuff, but the build itself is a bit odd. Unfortunately, it’s currently written in bash — so this will only work on Linux/WSL/Mac machines.

Download the build file.

curl https://raw.githubusercontent.com/SharpCoder/teensycore/main/build-template.sh > build.sh

# Add execute permission to the build
chmod +x ./build.sh

Because of the extra linker steps to get our kernel in the right spot in the file, and due to that one pesky .c file — this build.sh script was born. It essentially cargo builds your rust, then uses the arm-none-eabi-ld to hook everything up and objcopy to emit a hex file.

All that’s left is the entrypoint to teensycore itself. Here’s the code you can plop into src/lib.rs to get started.

#![feature(lang_items)]
#![no_std]
teensycore::main!({
    pin_mode(13, Mode::Output);
    loop {
        pin_out(13, Power::High);
        wait_ns(1 * S*TO_NANO);
        pin_out(13, Power::Low);
        wait_ns(1 * S_TO_NANO);
    }
});

The teensycore::main!({ /_ code here_/ }) macro includes a bunch of my standard libraries, configures the default UART, fires up the clock peripherals, and much more.

The design goal I had in mind was to make that macro get you as close to a blank Arduino project as possible. Don’t have to think about what’s happening too much, just get right down to bit banging!

pin 13 is the gpio pin on which the internal LED is wired in a teensy 4.0. So toggling this every second is pretty easy in teensycore.

Let’s explore a more advanced project.

Reading/Writing Data over I2C

Next let’s explore the i2c interface. I shamelessly modeled it after the Arduino interface because, well, that makes a lot of sense. Suppose you have an extra eeprom chip lying about (in this example, I’m using the 24LC512). Here’s how to read and write data from it…

(Note: I have the SDA line wired to pin 18, the SCL line wired to pin 19)

#![feature(lang_items)]
#![crate_type = "staticlib"]
#![no_std]

teensycore::main!({
    use teensycore::*;

    let mut wire = I2C::begin(18, 19);
    wire.set_speed(I2CSpeed::Fast400kHz); wire.begin_transmission(0x50, true);
    // First two bytes are memory address
    wire.write(&[0, 0]);
    // Next is a sequential write of data
    wire.write(b"EARTH");
    wire.end_transmission();

    // Settle time for whole-page write. Per docs.
    wait_ns(250 * MS_TO_NANO);    // Select the address we wish to read
    wire.begin_transmission(0x50, true);
    wire.write(&[0, 0]);
    // Perform read request
    wire.begin_transmission(0x50, false);

    // Use the `debug_str` functionality to output this data to the
    // TX2 UART. Pin 8 on the teensy. For debugging purposes.
    debug_str(&[
        // Send 'true' as the second parameter to include an ack
        // This tells the chip we wish to do sequential reads
        // with automatic addr incrementation.
        wire.read(true),
        wire.read(true),
        wire.read(true),
        wire.read(true),
        wire.read(true),
    ]);
    wire.end_transmission();
});

When I wrote this code originally, I had an actual arduino hooked up to pin 8 (TX2) and used the Arduino Serial Monitor to spy the eeprom results. It was a cool external validation of both the UART and the I2C systems.

Let’s break it down a bit.

let mut wire = I2C::begin(18, 19);
wire.set_speed(I2CSpeed::Fast400kHz);

Teensycore i2c supports “fast mode” and normal mode, per the i2c documentation. My i2c library is just bit banging really fast on normal gpio wires. As such, you can use it with any available GPIO pins which is pretty neat.

A transaction is completed similar to the Arduino Wire library.

wire.begin_transmission(0x50, true);
wire.write(&[0, 0]);
wire.write(b"EARTH");
wire.end_transmission();

You begin_transmission( addr, write_mode ) and then end_transmission() when complete.

If you specify false for the second parameter, you’ll be in read mode. And grabbing bytes can be done like this:wire.read(true) That parameter just tells whether to send back an ack or not.

That’s really all there is to this i2c communication library. Conclusion

We’ve covered the basics of starting a new teensycore project from scratch and leveraging the various subsystems to blink lights, communicate over i2c, and emit debug serial information. As time goes on, I will be adding to this kernel and writing more articles about how to use the new and old features alike.