STM32 Embedded Rust
Introduction
For some time now I have been interested in improving my Rust skills. It just so happens that the other day I saw an article on Embedded Rust. Reading through the documentation for the stmrs project, I was surprised at how simplistic well organized the HAL was.
Wanting to experiment, I installed the required Cargo dependencies and created a project. Since most of this is documented in the embedded rust book, I wont go into detail about setting up the environment.
The Code
Below is the code I wrote for this mini project. It contains only the main function and a single ISR for processing rx interrupts from the USART peripheral. While it is very simplistic code, blinking an led and echoing the serial port, it was sufficient to allow me to try out the language.
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
#![no_main]
#![no_std]
use core::fmt::Write;
use cortex_m_rt::entry;
use stm32f4xx_hal::interrupt;
use stm32f4xx_hal::{self as hal};
use crate::hal::{pac, prelude::*, serial::{Config, Serial}};
use cortex_m::peripheral::NVIC;
use cortex_m::interrupt::Mutex;
use heapless::{String, spsc::Queue};
use core::cell::RefCell;
use panic_probe as _; // panic handler
const BUF_SIZE : usize = 256;
static BUFFER: Mutex<RefCell<Option<String<BUF_SIZE>>>> = Mutex::new(RefCell::new(None));
static USART_QUEUE: Mutex<RefCell<Option<Queue<u8, BUF_SIZE>>>> = Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
if let (Some(dp), Some(cp)) = (
pac::Peripherals::take(),
cortex_m::peripheral::Peripherals::take(),
) {
// Set up the LED
let gpiob = dp.GPIOB.split();
let mut led2 = gpiob.pb7.into_push_pull_output();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.sysclk(180.MHz()).freeze();
let mut delay = cp.SYST.delay(&clocks);
let gpiod = dp.GPIOD.split();
let tx_pin = gpiod.pd8.into_alternate();
let rx_pin = gpiod.pd9.into_alternate();
// Set up USART3 for serial communication
let mut serial = Serial::new(
dp.USART3,
(tx_pin, rx_pin),
Config::default().baudrate(9600.bps()).wordlength_8(),
&clocks,
)
.unwrap();
// Enable the RX interrupt for USART3
serial.listen(stm32f4xx_hal::serial::Event::RxNotEmpty);
// Enable USART3 interrupt in NVIC
unsafe {
NVIC::unmask(pac::Interrupt::USART3);
}
// Initialize the buffer and queue for storing received characters
cortex_m::interrupt::free(|cs| {
BUFFER.borrow(cs).replace(Some(String::new()));
USART_QUEUE.borrow(cs).replace(Some(Queue::new()));
});
loop {
// Toggle the LED
led2.toggle();
delay.delay_ms(500);
// Check if we have a complete string in the buffer
cortex_m::interrupt::free(|cs| {
if let Some(buffer) = BUFFER.borrow(cs).borrow_mut().as_mut() {
if buffer.ends_with('\n') {
// Print the complete message and clear the buffer
write!(serial, "Received: {}", buffer).unwrap();
buffer.clear();
}
}
});
}
}
loop {}
}
// USART3 interrupt handler
#[interrupt]
fn USART3() {
cortex_m::interrupt::free(|cs| {
let usart3 = unsafe { &*pac::USART3::ptr() };
if usart3.sr.read().rxne().bit_is_set() {
// Read the received character
let received = usart3.dr.read().dr().bits() as u8;
// Access buffer and add the received character
if let Some(buffer) = BUFFER.borrow(cs).borrow_mut().as_mut() {
// Only add character if there’s space in the buffer
if buffer.push(received as char).is_err() {
buffer.clear(); // Clear if buffer overflows
}
}
}
});
}
Problems & Evaluation
Overall, this was a fairly easy project. I was able to get an led blinking in under an hour (including installing the tools) and it only took me another 35 minutes to get USART working. That being said, I do have a few notes.
Problems
Since there are too many to go into depth about, I’m just going give a list:
- No easy way to view RAM/FLASH usage on the microcontroller
- Using the HAL results in no control over chip startup
- No FreeRTOS equivalent: RTIC does not have preemption
- Unsafe Blocks: Why would I use Rust if everything ends up in an unsafe block
- Complex Interrupts
- Generally complex code
I will admit that most of these are not valid reasons, but are opinions.
Evaluation
The purpose of this project was to experience what it was like to write embedded code in rust. While I did enjoy the project, I’m still unsure of how this would look on a larger scale. I’ve had numerous issues in the past with global variables in Rust. In C, this can be solved with wrapping the variables in a mutex or semaphore. When I know that this is unnecessary, I don’t have to include it. Whereas in Rust, the compiler will complain.
Given my past experience, I think that I will be sicking to C for the time being. I have put a lot of time and effort into my C setup/environment, I enjoy using it and working in C. I also enjoy making my own HAL and drivers. Attempting to transfer my work to a different language would be less learning and more repetition.