Embedded Systems

In this assignment you are going to build a small program running inside an emulator which emulates a Stellaris LM3S6965 microcontroller with an ARM M3 core.

You will be building a kind of GPS step counter that records changes in position, counts steps and displays some information in a graphical interface.

You will design a protocol to communicate with your system and implement a UART driver which provides a channel to communicate over.

Installation Requirements

You need to install two programs, namely QEMU for ARM (emulates ARM devices) and some tools to build a valid program to run inside the emulator

  • for QEMU: qemu-system-arm (this is the name of the binary, which might not work on some Linux distros)
  • for the tooling: cargo install cargo-binutils (or the equivalent with your package manager if that's supported)

A note on Operating Systems

This assignment is only well tested on Linux. We are pretty sure this does not work on Windows, possible with some hassle on WSL2. For MacOS we have heard people being succesful but we have no personal experience with it

Furthermore, you will be compiling programs to run in an ARM emulator. That means that the Rust compiler will be tasked to generate ARM assembly code. This is called cross-compilation, and is quite easy with Rust. You simply have to run the following command:

rustup target add thumbv7m-none-eabi

This command will download all the required files to generate thumbv7m assembly code (ARM). After this, rust will know how to compile arm programs. You can take a look at other supported targets with rustup target list. Make sure that thumbv7 is highlighted in this list, marking it as installed.

During labs, we've seen that some people have some issues installing some components related to cargo-binutils. What seems to solve this, is after adding the arm target to the compiler running

#![allow(unused)]
fn main() {
rustup update
rustup component add llvm-tools-preview
}

And to make sure that your path variable (run echo $PATH) contains /home/yourname/.cargo/bin

Description of the assignment

This assignment is split up in two parts.

Part 1: UART

First, you'll implement a UART driver for the Stellaris LM3S6965. A rough template is already provided.

The manual for the Stellaris LM3S6965 can be found here. It is very useful, and contains all the information you need, make sure to read it carefully. Additionally, the PAC crate we include can help you out. While implementing your UART driver, very carefully think about which kinds of operations might be undefined behaviour.

The driver

In the template we already provide, what we call, the runner. This is a program that starts the qemu emulator of the Stellaris LM3S6965 with your program running inside it through a library we provide. Afterwards, you can communicate with the program running inside the emulator over simulated UART serial, just as if the Stellaris board was attached to your computer through a wire, but without actually needing the hardware.

To test your UART driver, we already provide a basic skeleton in the runner that simply prints any bytes it receives over serial. Your driver should in the end be capable of both sending and receiving messages to the emulated Stellaris board.

Abstractions

For this assignment, carefully think about what abstractions you need to ensure that your UART driver has no undefined behaviour. Also recall what you learned about this in the lectures. To write your code, you will need to use unsafe code. However, keep in mind that for every use of an unsafe block in your code, you are expected to explain why that block is necessary there, and what pre-conditions, post-conditions and/or invariants need to hold for that unsafe block to be sound, and show that those conditions are actually checked, to make the code indeed sound. Note that this may be more verbose than you will make your explanations in the field (though we do recommend it), but this explanation is part of your assessment, so it needs to be complete.

Part 2: Protocol

Once you have built a driver for the UART peripheral of the Stellaris LM3S6965, you should be able to exchange simple byte-sized messages. However, almost any real-world application requires a more elaborate communication protocol than this. In the second part of this assignment, you will be designing such a protocol to communicate with the board, and to send it higher level commands to draw things on the screen.

Next quarter, during the Embedded Systems Lab, you will be communicating with a flying drone similarly, over UART. Therefore, you can see this as a practice assignment for that course. We encourage you to reflect on what you made, and to see what you can improve for next quarter if you did not like how you did it in this assignment.

Creating a shared library

The types that will be serialized will most likely be the same for both the runner and the board. It may be useful to create a library of code that is shared between the two (i.e. compiled both for the runner and the server). That way, if you add fields to structs, they are updated both for the runner and server.

You can just create it using cargo new --lib library_name, and then add library_name to members the root Cargo.toml.

We also recommend adding the following to the top of the lib.rs to enable the standard library while testing this shared code.

#![cfg_attr(not(test), no_std)]
#[cfg(test)]
extern crate std;
// your code here

Communicating reliably

Because we use an emulator, communication is 100% reliable. But that's not generally true in the real world. What if a byte is dropped? To make sure your system works when the connection is unreliable, you should add checksums to all communication. Similarly, you must make sure that if bytes are lost from a single message, future messages are readable again.

Learning Objectives

In this assignment, the goal is to learn about:

  • Programming without an operating system
  • Creating safe abstractions over unsafe code
  • Using peripheral/hardware abstraction libraries
  • UART and serial communication
  • Creating a simple, fault-tolerant message protocol

Requirements

Note that in the template, signatures are a guideline to help you, but may need to be changed to support for example error handling.

Note that there are a number of "TODO"s in the template code. You should resolve these.

You will be building a tiny application that processes messages and displays information on the screen.

External Library Policy

Explicitly allowed:

  • Logging libraries
  • Serialization libraries
  • Data structure libraries
  • Data integrity libraries
  • User parsing libraries
  • Framebuffer drawing libraries

Explicitly not allowed:

  • UART Libraries
  • Mutex Library

UART driver

  1. You must create a safe interface through which to send and receive UART messages
  2. You must provide a safe abstraction for global mutable state (Mutex)
    • You must preventing two threads executing at once. Of course there are no threads in the system you are implementing the driver for, but concurrency is still an issue.
    • This may need unsafe code, you must add an explanation on why your implementation is still sound.
  3. You must provide buffering such that long messages can be sent and received.
    • The buffer must be fixed-size, and must be sized appropriately for the messages that are sent over them.
    • If the buffer fills up, the program must not crash.

Communication

While the emulator runs, it should keep track of a "location" and amount of steps.

  1. You should be able to record changes in position (dx/dy in meters) and remember that state in the program.
  2. You should be able to record "a step" that is also kept track by the program running in the emulator
  3. You should be able to send a message to the program to change the currently viewed page
  4. You should use checksums to verify the integrity of your messages
  5. If messages are dropped, or corrupted, you should make sure that your system can continue reading future messages. Test this.
  6. You must use the serde library

Interface

In this section, some choices are left up to you. You can be a bit creative here.

  1. The Graphical Interface must contain two views
    • A map of where the user has walked, 1 pixel = 1 meter. You can assume the user won't walk far enough to leave the screen boundaries.
    • A simple counter view which shows total number of steps taken
  2. The runner should get a simple interactive interface in which you can simulate sending messages to the system by typing commands. You should be to send messages corresponding to the following actions:
    • A single step being taken, and the corresponding dx/dy movement.
    • A help page (for us) so we know what to type
    • A command to request the current total number of steps recorded by the Stellaris board
    • A reset command, that resets the state of the Stellaris board to its initial state, so 0 steps taken

General

  1. Every use of unsafe code must be sound (i.e. we will assess your use of unsafe code and check that what you are doing is sound).
  2. Every use of unsafe code should have a comment explain the entire reasoning required to understand why that block of unsafe code is indeed sound.

Grading

You should implement each of the numbered requirements above as specified. Your grade starts at a 10, and for each requirement you don't implement to spec, a point is removed. Partial points may be subtracted for partially correct implementations.

Getting started

Look in the uart.rs file. Some hints and function signatures are already given for you to implement. If you have any questions about them, feel free to come to the lab and ask them to one of the TAs.

Debugging

The Runner::new function takes two parameters: the binary to run in the emulator, and a boolean flag: wait_for_debugger. If you pass true here, and run your code, the emulator seems to get stuck while booting. However, it is actually just waiting for you to attach a debugger, with which you can step through and debug the code while it is running in the emulator. To do this, get a copy of gdb that works form ARM (arm-none-eabi-gdb or gdb-multiarch), and start the emulator to a point where it's waiting for a debugger to attach. Now, in a separate terminal, run arm-none-eabi-gdb. To attach to the emulator, use target remote localhost:1234, and then to get source debugging, use: symbol-file target/path/to/the/binary/with/embedded/code.

I usually like to do this directly from the commandline with:

arm-none-eabi-gdb \
  -ex "target remote localhost:1234" \
  -ex "symbol-file target/thumbv7m-none-eabi/debug/embedded-assignment" \
  -ex "layout source" \ # also see the source code
  -ex "focus cmd" \ # but put the focus on the command input
  -ex "b main" \ # short for breakpoint
  -ex "c" # short for continue to next breakpoint (so to main)

You can also write a .gdbinit file in the root of your project, with these commands in it.