STM32F0xx Development – Part 2: Software

Overview

As mentioned in STM32F0xx DigitalIO / AnalogIn PCB, one of the main motivations behind this project was to learn more Rust.

If Rust turns out to be genuinely useful in an embedded setting, I definitely won’t be subjecting myself to C again anytime soon. Just kidding — I like you, C… …nah, not really. 😄

Setup

Hardware

  1. Target MCU – STM32F030

    This is a solid Cortex-M0 device. The variant I’m using offers around 35 usable GPIO pins, 64KB of flash, and 8KB of RAM. The real selling point, though, is the price — roughly $1 USD in low quantities.

  2. Programmer – ST-Link V2

    The inexpensive AliExpress clone, which can be picked up for around $3 USD.

Software

  1. IDE – VS Code

    I’ve been using VS Code for a while now and it’s my daily driver for Python, C#, and Rust development.

  2. OS – Ubuntu KDE 20.04 (Kubuntu)

    Recently upgraded from Kubuntu 18.04. It’s been mostly stable, although I’ve had a few interesting issues when docking and undocking.

Getting Started Resources

There are a few additional tools that need to be installed (Rust, GDB, OpenOCD, etc.). The following resources are absolutely worth checking out:

  1. The Rust Embedded Book
  2. Rust Embedded Discovery Book
  3. Cortex-M Quickstart

Debugging and Testing

After installing and setting everything up, we can finally start writing code.

…Hang on. How do we actually test that code?

Hardware-Level Debugging

The resources above walk through setting up a debug environment in VS Code. This allows you to step through code while it executes on the MCU.

Below is one of the debugger configurations from my launch.json file, with a few custom tweaks:

Unit Testing.

Can we run unit tests on the host instead of the target MCU?

Yes — but it requires some conditional compilation using cfg_attr.

When not running unit tests, the code is compiled with no_std and no_main, and certain modules are excluded:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#![cfg_attr(test, allow(unused_imports))]
#![cfg_attr(not(test), no_std)]
#![cfg_attr(not(test), no_main)]

// set the panic handler
extern crate cortex_m_semihosting;
#[cfg(not(test))]
extern crate panic_semihosting;

#[cfg(not(test))]
use cortex_m_semihosting::hprintln;
use panic_semihosting as _;

This setup allows logic to be tested on the host while still compiling cleanly for the embedded target.

Code Size While Debugging

At around 1–1.5k lines of code, I was already running out of space on the 64KB flash.

That raised a big question:

If this is the size of a small Rust codebase, is Rust actually usable in embedded systems?

The Solution

Enable optimization.

By default, debug builds include a huge amount of metadata. Adding the following to Cargo.toml drastically reduced the binary size:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[profile.dev]
opt-level = "z"
debug = true
debug-assertions = true
overflow-checks = true
lto = false
panic = 'unwind'
incremental = true
codegen-units = 256
rpath = false

Normally, opt-level defaults to 0. Switching to “z” made a massive difference.

RTOS and Embedded Libraries

RTOS

For this project, I used RTIC, a real-time concurrency framework written entirely in Rust.

While it’s not as mature as something like FreeRTOS, it fits my use case perfectly. One of its biggest advantages is safe data sharing between priority-based interrupts — something that’s notoriously easy to get wrong in traditional embedded systems.

Check it out here: RTIC

Embedded Libraries: Embedded Libs

Actual Code Example

Below is an example of the serial module used in this project.

You’ll notice heavy use of traits and trait bounds, which are conceptually similar to interfaces in C#.

The mod tests section contains all unit tests for this module.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use embedded_hal::{digital, serial};

use crate::buf;
use buf::{Buf, IBuf};

use crate::dev;
use dev::IDev;

use crate::error;
use error::BufErr;
use error::DevErr;

use nb::block;

/// Serial Device
pub struct DevSerial<USART, TXEN, RXEN, SENDBUF, RECVBUF> {
    interface: USART,
    tx_en: TXEN,
    tx_buf: SENDBUF,
    rx_en: RXEN,
    rx_buf: RECVBUF,
}

impl<USART, TXEN, RXEN, SENDBUF, RECVBUF> DevSerial<USART, TXEN, RXEN, SENDBUF, RECVBUF>
where
    USART: serial::Write<u8> + serial::Read<u8>,
    TXEN: digital::v2::OutputPin,
    RXEN: digital::v2::OutputPin,
    SENDBUF: IBuf<u8>,
    RECVBUF: IBuf<u8>,
{
    pub fn new(interface: USART, tx_en: TXEN, tx_buf: SENDBUF, rx_en: RXEN, rx_buf: RECVBUF, ) -> Self {
        DevSerial {
            interface: interface,
            tx_en: tx_en,
            tx_buf: tx_buf,
            rx_en: rx_en,
            rx_buf: rx_buf,
        }
    }
}

impl<USART, TXEN, RXEN, SENDBUF, RECVBUF> IDev for DevSerial<USART, TXEN, RXEN, SENDBUF, RECVBUF>
where
    USART: serial::Write<u8> + serial::Read<u8>,
    TXEN: digital::v2::OutputPin,
    RXEN: digital::v2::OutputPin,
    SENDBUF: IBuf<u8>,
    RECVBUF: IBuf<u8>,
{
    /// Receive Byte
    fn get_recv_byte_mut(&mut self) -> Result<(), DevErr> {
        //Read serial port
        let data = block!(self.interface.read()).map_err(|_| DevErr::SerialErr)?;

        //Check if buffer is overflowing if it is then clear the buffer
        //This should not happen often unless we get lots of garbage
        //on the comms line
        match self.rx_buf.write_val_mut(data) {
            Ok(_) => {}
            Err(BufErr::Overflow) => { self.rx_buf.clear_mut()?; }
            _ => panic!("Empty"), //This should never happen
        };

        Ok(())
    }

    /// Return receive buffer as slice
    fn get_recv_buf_as_slice(&self) -> Result<&[u8], DevErr> {
        Ok(self.rx_buf.get_as_slice()?)
    }

    fn clear_recv_buf_from_first_to_val_mut(&mut self, val: u16) -> Result<(), DevErr> {
        self.rx_buf.clear_from_first_to_val_mut(val)?;

        Ok(())
    }

    /// Clear receive buffer
    fn clear_recv_buf_mut(&mut self) -> Result<(), DevErr> {
        self.rx_buf.clear_mut()?;

        Ok(())
    }

    /// Transmit a single u8 from buffer this will
    /// remove the transmitted byte from the buf
    /// This is useful for interupts
    fn transmit_single_u8_mut(&mut self) -> Result<(), DevErr> {
        block!(self.interface.write(self.tx_buf.get_first_val_mut()?))
            .map_err(|_| DevErr::SerialErr)?;

        Ok(())
    }

    /// Write a single u8 into the send buffer
    fn write_u8_to_send_buf_mut(&mut self, data: u8) -> Result<(), DevErr> {
        self.tx_buf.write_val_mut(data)?;

        Ok(())
    }

    /// Return send buffer as mut slice
    fn get_send_buf_as_mut_slice_mut(&mut self) -> Result<&mut [u8], DevErr> {
        Ok(self.tx_buf.get_as_mut_slice_mut()?)
    }

    /// Clear send buffer
    fn clear_send_buf_mut(&mut self) -> Result<(), DevErr> {
        self.tx_buf.clear_mut()?;

        Ok(())
    }
}

#[cfg(test)]
mod tests {

    #![allow(non_snake_case)]

    use super::*;
    use embedded_hal::digital::v2::{InputPin, OutputPin};
    use embedded_hal_mock::pin::{
        Mock as PinMock, State as PinState, Transaction as PinTransaction,
    };
    use embedded_hal_mock::MockError;

    use embedded_hal::blocking::serial::Write;
    use embedded_hal::serial::Read;
    use embedded_hal_mock::serial::{Mock as SerialMock, Transaction as SerialTransaction};

    use heapless::consts::{U128, U16, U256, U32, U64}; // type level integer used to specify capacity

    #[test]
    fn callGetRecvByteMut_set3ElementReturn3_expect3ElementsInOrder() {
        // Setup
        let dio_expectations = [PinTransaction::set(PinState::High)];

        // Create pin
        let tx_en_mock = PinMock::new(&dio_expectations);
        let rx_en_mock = PinMock::new(&dio_expectations);

        let first = 10;
        let second = 20;
        let third = 30;

        // Configure expectations
        let serial_expectations = [SerialTransaction::read_many([first, second, third])];

        let serial_mock = SerialMock::new(&serial_expectations);

        let tx_buf = buf::Buf::<u8, U16>::new();
        let rx_buf = buf::Buf::<u8, U16>::new();

        let mut rs485 = DevSerial {
            interface: serial_mock,
            tx_en: tx_en_mock,
            tx_buf: tx_buf,
            rx_en: rx_en_mock,
            rx_buf: rx_buf,
        };

        rs485.get_recv_byte_mut().unwrap();
        rs485.get_recv_byte_mut().unwrap();
        rs485.get_recv_byte_mut().unwrap();

        //println!("{:?}", rs485.get_recv_buf_as_slice().unwrap());

        //Test
        assert_eq!(first, rs485.get_recv_buf_as_slice().unwrap()[0]);
        assert_eq!(second, rs485.get_recv_buf_as_slice().unwrap()[1]);
        assert_eq!(third, rs485.get_recv_buf_as_slice().unwrap()[2]);
    }

    #[test]
    fn callClearRecvBufMut_set3ElementsThenClear_expectEmptyBuf() {
        // Setup
        let dio_expectations = [PinTransaction::set(PinState::High)];

        // Create pin
        let tx_en_mock = PinMock::new(&dio_expectations);
        let rx_en_mock = PinMock::new(&dio_expectations);

        let first = 10;
        let second = 20;
        let third = 30;

        // Configure expectations
        let serial_expectations = [SerialTransaction::read_many([first, second, third])];

        let serial_mock = SerialMock::new(&serial_expectations);

        let tx_buf = buf::Buf::<u8, U16>::new();
        let rx_buf = buf::Buf::<u8, U16>::new();

        let mut rs485 = DevSerial {
            interface: serial_mock,
            tx_en: tx_en_mock,
            tx_buf: tx_buf,
            rx_en: rx_en_mock,
            rx_buf: rx_buf,
        };

        rs485.get_recv_byte_mut().unwrap();
        rs485.get_recv_byte_mut().unwrap();
        rs485.get_recv_byte_mut().unwrap();

        rs485.clear_recv_buf_mut().unwrap();

        //println!("{:?}", rs485.get_recv_buf_as_slice().unwrap());

        let test: &[u8] = &[];

        //Test
        assert_eq!(test, rs485.get_recv_buf_as_slice().unwrap());
    }
}