This blog post is available on my website
I recently was given an arduino build kit and I've been having a lot of fun experimenting with embedded systems in Rust for the first time.I started by just getting an 8x8 grid of LEDs to display anything I wanted, essentially an embedded "hello world" project. After that, I tried to get a rotary module hooked up to the board so I could select different patterns to display by turning the dial. This was much more challenging than I expected.
8x8 LEDs
The development environment
First, I had to setup an environment where I could flash and run Rust code on my MEGA2560 board. This was simple enough, all I had to do was follow the tutorial for installing
avr-hal
, which is an abstraction for AVR microcontrollers. The tutorial was fairly simple, and can be read here.
In short, these were the steps to setting up:
Set up the rust project for my board
This was done for me by running `cargo generate --git https://github.com/Rahix/avr-hal-template.git` and then following the prompts for building the project with my board.
Ensuring I was using the correct compiler
This was also fairly easy, I just ran `rustup toolchain install nightly`, since embedded Rust is not supported by the current stable compiler.
Get the port my board was connected to and configure the library I was using to connect to that port.
Start flashing the board and running code!
Translating Ascii patterns to binary
My goal for the 8x8 was to be able to display any pattern I drew in ascii, such as:
________
_oo__oo_
oooooooo
oooooooo
_oooooo_
__oooo__
___oo___
________
The 8x8 grid has 8 bytes, one for each column of LEDs. So, in order to display the above pattern I would have to write the following bytes to the LED module:
00000000
01100110
11111111
11111111
01111110
00111100
00011000
00000000
This meant writing a function that could accurately translate ascii patterns to 8 bytes. This took longer than I hoped, but I finally ended up with this function:
const MATRIX_SIZE: usize = 8;
fn text_into_bytes_array(text:&str) -> [u8; MATRIX_SIZE] {
let mut bytes: [u8;MATRIX_SIZE ]= [0x00;MATRIX_SIZE ];
for (i, filter) in text.trim().lines().enumerate().map(|(i, l)| (i, l.chars().filter(|c|!c.is_whitespace()))){
for (k, ch) in filter.into_iter().enumerate() {
let bit = {
if ch == 'o'{1} else {0}
};
bytes[i] |= bit << (7 - k);
}
}
bytes
}
Let me walk through the steps of this function:
A sized bytes array is initialized. Since we're working with an 8x8 matrix, the array has 8 items, each are inialized as `0x00`, or just 0.
I iterate through each line in the input text, ignoring any whitespace.
I iterate through each character in the current line.
I then use the bitwise 'or' operator (`|=`) to assign the byte associated with the current line of text to the same byte left shifted based on the current char's position in the current line.
After all the iteration, I return the `bytes` variable.
After writing this fucntion, I could, in theory, write any ascii pattern I wanted to the LED grid module. But there was a problem... I didn't know how to write anything to the module.
The LED Module
My first instinct was to just use the `embedded-hal-bus` crate's `spi` module to instantiate an `SpiDevice` trait object. As I am brand new to embedded programming, this proved more challenging than was neccessary for my very first embedded project. So, after a few hours of struggling to essentially create my own driver for my LED matrix module, I found a crate specifically written for the module.Eventually I think it would be great for me to write my own driver for a module, but I don't want to drown my motivation by jumping in at the deep end of embedded systems before I've even writtenwhat is essentially a "hello world" program for an arduino. After searching the name of the LED module I was using, I found this crate, which has a handy MAX7219 struct that makes it easy to write bytes to the LED display. Now I could easily display any pattern to the 8x8 grid with the following code:
// Of course first I have to instantiate the struct for the device
let led_din = pins.d12.into_output();
let led_cs = pins.d11.into_output();
let led_clk = pins.d10.into_output();
let mut led_device = max7219::MAX7219::from_pins(1, led_din, led_cs, led_clk).unwrap();
// Assuming 'pattern' is a string slice of a valid pattern.
let bytes = text_into_bytes_array(pattern);
for (i, byte) in bytes.into_iter().enumerate(){
led_device.write_raw_byte(0, 0x01 + i as u8, byte).unwrap();
}
The `write_raw_byte` method on the LED device takes 3 arguments:
the address of the display to write to, which in this case is always 0 because I only have a single 8x8 grid attached to the board
the register to write the value to, which is between `0x01` and `0x08`, because there is one register for each row of 8 LEDs
the bytes to write to the register
So, if I wanted to display the heart pattern:
00000000
01100110
11111111
11111111
01111110
00111100
00011000
00000000
I would write 00000000
to 0x01
, 01100110
to 0x02
, 11111111
to 0x03
and so on..
Rotary Module
In case you don't know what a rotary is, it's basically a dial.
My vision for the rotary was to use it to flip though an array of patterns. Which would give me the ability to change the pattern displayed by the 8x8 grid while the arduino was running.
Interrupts
After a little bit of research, it became clear to me that I would have to use what is called an _interrupt_. Interrupts are used in embedded programming to 'interrupt' the usual loop running on a
device when a specific event happens. The event that triggers an interrupt is defined by the programmer, but each board has predefined registers which correspond to specific pins triggering interrupts.
I've been working through a book which has all the code for writing to my arduino, but the code is in C. So I've been learning by translating the embedded C to embedded Rust.
I ran into an issue when it came to interrupts though... Take a look at the C code where the interrupt for the rotary module is defined:
attachInterrupt (0,isr,FALLING);
This function takes 3 arguments:
The interrupt number of a target pin. This is essentially a 'magic number' that is specific to the board and the pin you are trying to attach an interrupt to.
The function that is called when the interrupt is triggered
The 'Mode' which the interrupt is triggered.
Can be any of the following:
Low: triggers when the pin is low
Change: triggers when the pin changes values
Rising: triggers when the pin goes from low to high
Falling: triggers when the pin goes from high to low
In case your wondering, a pin being low or high is just a fancy way of saying the pin's value is a 0 (low) or 1 (high)
Now this C function is fairly straightforward, but I had no idea how to translate it to Rust. After a little sleuthing I found this example defining an interrupt using a different board from me:
#[avr_device::interrupt(atmega328p)]
#[allow(non_snake_case)]
fn PCINT2() {
PIN_CHANGED.store(true, Ordering::SeqCst);
}
#[arduino_hal::entry]
fn main() -> ! {
// CODE ABOVE....
// Enable the PCINT2 pin change interrupt
dp.EXINT.pcicr.write(|w| unsafe { w.bits(0b100) });
// Enable pin change interrupts on PCINT18 which is pin PD2 (= d2)
dp.EXINT.pcmsk2.write(|w| w.bits(0b100));
//From this point on an interrupt can happen
unsafe { avr_device::interrupt::enable() };
// CODE BELOW....
}
Rust's way of defining an interrupt seemed like it had two parts; The definition of a function using the `#[avr_device::interrupt(<board name>)]` macro, and the esoteric calls to
dp.EXINT.pcicr.write()
and dp.EXINT.pcmsk2.write()
.
At a loss, I checked out my board's pinout diagram:
I quickly noticed the key of the diagram references interrupt as the blue parts of the diagram. Following this I saw the interrupt I was trying to assign was on the
INT4
register (pin d2). So, I could define my function that triggered when the interrupt I wanted as:
#[avr_device::interrupt(atmega2560)]
fn INT4() {
}
I still wasn't sure what to put in the function, but adding this to my file didn't keep my project from compiling, a good sign!Now that I had figured out the `avr_device::interrupt` macro, I just had to tackle `dp.EXINT.pcicr.write()` and `dp.EXINT.pcmsk2.write()`. After consulting the docs for my board, I found that the `EXINT` field on my boards peripherals (`dp` in the example code), referred to the External Interrupts in the board. I knew that the fields following `EXINT` in the example code would differ from what I would have to write because the example code was using a different board than me. I decided to consult my board's datasheet. I eventually found External Interrupt Control Register A and B as well as the External Interrupt Mask Register.
External Interrupt Control Register B handles all interrupts from INT4 - INT7. The register has 8 bits, 2 for each of the interrupts. So, bits 1 and 2 correspond with INT7, bits 3 and 4 correspond with INT6, and so on.. If you look at table 15-3, you might notice that the four descriptions map perfectly to the four possible modes that can be passed in as the third argument in the `C` function `attachInterrupt`. So, If I wanted to have the INT4 interrupt trigger when it's value goes from 0 to 1, I would have to write a 1 to the first bit associated with INT4, and 0 to the second. As code this would look like this:
dp.EXINT.eicrb.modify(|_, w| unsafe{ w.bits(0b00000010) });
All I had to do now was write the correct bits to the External Interrupt Mask Register. This register simply has one bit for each of the INT interrupts (INT0 - INT7) in reverse order. To enable interrupts from INT4, all I would need to do would be to write a 1 to the 4th bit in the register:
dp.EXINT.eimsk.modify(|_, w| w.bits(0b00010000) );
The current code
After figuring out how to use the 2 modules and working out how to use interrupts, I have written a working version with the following behaviors:
Clicking the rotary module down toggles the LED display
Turning the rotary module changes the displayed pattern
I ended up with the following code for the `INT4` interrupt:
static ROTARY_UPDATED: AtomicBool = AtomicBool::new(true);
#[avr_device::interrupt(atmega2560)]
fn INT4() {
interrupt::free(|cs| {
if let Some(rt_dt) = SHARED_RT_DT_PIN.borrow(cs).borrow().as_ref() {
if rt_dt.is_high() {
counter_up(patterns().len() - 1);
} else {
counter_down(patterns().len() - 1);
}
ROTARY_UPDATED.store(true, Ordering::SeqCst);
}
})
}
`counter_up` and `counter_down` are simply helper functions for changing the `COUNTER` static variable, and ensuring it stays within 0 and the length of available patterns returned by `patterns`. Here is my main loop:
#[arduino_hal::entry]
fn main() -> ! {
let dp = arduino_hal::Peripherals::take().unwrap();
let pins = arduino_hal::pins!(dp);
dp.EXINT.eicrb.modify(|_, w| unsafe{ w.bits(0b00000010) });
dp.EXINT.eimsk.modify(|_, w| w.bits(0b00010000) );
unsafe { avr_device::interrupt::enable() };
let mut serial = arduino_hal::default_serial!(dp, pins, 57600);
let led_din = pins.d12.into_output();
let led_cs = pins.d11.into_output();
let led_clk = pins.d10.into_output();
let mut led_device = max7219::MAX7219::from_pins(1, led_din, led_cs, led_clk).unwrap();
let mut display_on = false;
let mut prev_display_status = None;
let rt_sw = pins.d4.into_pull_up_input();
let rt_dt = pins.d3.into_floating_input().downgrade();
avr_device::interrupt::free(|cs| {
SHARED_RT_DT_PIN.borrow(cs).replace(Some(rt_dt));
});
led_device.clear_display(0).unwrap();
led_device.set_intensity(0, 0x07).unwrap();
loop {
if !rt_sw.is_high() {
interrupt::free(|_| {
delay_ms(1);
if !rt_sw.is_high() {
prev_display_status = Some(display_on);
display_on = !display_on;
}
})
}
if let Some(prev) = prev_display_status {
if display_on != prev {
if display_on {
ufmt::uwriteln!(&mut serial, "turning on display").unwrap_infallible();
led_device.power_on().unwrap();
} else {
ufmt::uwriteln!(&mut serial, "turning off display").unwrap_infallible();
led_device.power_off().unwrap();
}
prev_display_status = Some(display_on);
}
}
if display_on {
if ROTARY_UPDATED.load(Ordering::SeqCst) {
let pattern = patterns()[COUNTER.load(Ordering::SeqCst) as usize];
let bytes = text_into_bytes_array(pattern);
ufmt::uwriteln!(&mut serial, "displaying pattern").unwrap_infallible();
for (i, byte) in bytes.into_iter().enumerate(){
led_device.write_raw_byte(0, 0x01 + i as u8, byte).unwrap();
}
ROTARY_UPDATED.store(false, Ordering::SeqCst)
}
}
}
}
I'm super happy with this project and had a great time jumping into embedded rust! Thanks for reading this post, until next time :)