Rust on ESP32, part 3: Writing a simple driver for an old soviet switch

Tuomas Louekari
3 min readJun 12, 2024

--

I found this thing in a Latvian flea market. It’s a peculiar switch with one toggle position (top in this picture) and two positions (bottom) that return to neutral acting more like buttons.

First of all, I’d like to point out that many of the concepts and practices related to this project are still out of my comfort zone so regard this article as more of a hobbyist’s ramblings rather than a tutorial.

Let’s start with the repo, it can be found here. The code is divided into lib.rs (the driver itself) and main.rs, acting as an example of the usage. The entire little project leans heavily to this example and the Embedded Rust Book chapter on concurrency.

As for the hardware, if you build something similar, make sure that the pins you’re using are interrupt enabled. Refer to the pinout of your board. In the case of ESP32C3, all GPIO pins can be used for interrupts.

Perhaps the most important new concepts in this project were mutex and critical section. Mutex is basically an object that prevents multiple threads accessing the same shared resource. In this case, this resource is the OLD_SOVIET_SWITCH in the top of the example. Critical section, in turn, is a section of code during which the interrupt won’t fire.

Another interesting problem was to determine which parts of the code belong to the driver and which to the example. At first, I tried to have the static item inside the driver, but that would mean that I could never have more than one instance of the switch. This is how I ended up dividing the code:

lib.rs:

#![no_std]
#![no_main]

use esp32c3_hal::{
gpio::{
Event, InputPin,
},
interrupt,
peripherals::{self},
};
use esp_backtrace as _;

pub struct OldSovietSwitch<T1, T2, T3>
where
T1: InputPin,
T2: InputPin,
T3: InputPin,
{
pub pin1_main: T1,
pub pin2_bottom_left: T2,
pub pin3_bottom_right: T3,
}

pub struct OldSovietSwitchState {
pub pin1_main_high: bool,
pub pin2_bottom_left_high: bool,
pub pin3_bottom_right_high: bool,
}

impl <T1, T2, T3> OldSovietSwitch<T1, T2, T3>
where
T1: InputPin,
T2: InputPin,
T3: InputPin,
{
pub fn new(
pin1_main: T1,
pin2_bottom_left: T2,
pin3_bottom_right: T3,
) -> Self {
let mut instance = Self {
pin1_main,
pin2_bottom_left,
pin3_bottom_right,
};

instance.setup();
instance
}

pub fn setup(&mut self) {
self.pin1_main.listen(Event::FallingEdge);
self.pin2_bottom_left.listen(Event::FallingEdge);
self.pin3_bottom_right.listen(Event::FallingEdge);
interrupt::enable(peripherals::Interrupt::GPIO, interrupt::Priority::Priority3).unwrap();
}
pub fn read_state(&mut self) -> OldSovietSwitchState {
self.pin1_main.clear_interrupt();
self.pin2_bottom_left.clear_interrupt();
self.pin3_bottom_right.clear_interrupt();
OldSovietSwitchState {
pin1_main_high: self.pin1_main.is_input_high(),
pin2_bottom_left_high: self.pin2_bottom_left.is_input_high(),
pin3_bottom_right_high: self.pin3_bottom_right.is_input_high(),
}
}
}

main.rs:

#![no_std]
#![no_main]

use core::cell::RefCell;
use critical_section::Mutex;
use esp32c3_hal::{
clock::ClockControl,
gpio::{Gpio4, Gpio5, Gpio6, Input, PullDown, IO},
peripherals::Peripherals,
prelude::*,
Delay,
};
use esp_backtrace as _;
use esp_println::println;
use old_soviet_switch::*;

static OLD_SOVIET_SWITCH: Mutex<RefCell<Option<OldSovietSwitch<
Gpio6<Input<PullDown>>,
Gpio5<Input<PullDown>>,
Gpio4<Input<PullDown>>
>>>> = Mutex::new(RefCell::new(None));

#[entry]
fn main() -> ! {
let peripherals = Peripherals::take();
let system = peripherals.SYSTEM.split();
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let main_switch = io.pins.gpio6.into_pull_down_input();
let bottom_left = io.pins.gpio5.into_pull_down_input();
let bottom_right = io.pins.gpio4.into_pull_down_input();

let clocks = ClockControl::max(system.clock_control).freeze();
let mut delay = Delay::new(&clocks);
let ivan = OldSovietSwitch::new(
main_switch,
bottom_left,
bottom_right,
);
critical_section::with(|cs| OLD_SOVIET_SWITCH.borrow_ref_mut(cs).replace(ivan));

println!("Old soviet switch says hi.");
loop {
delay.delay_ms(500u32);
println!("Loop..");
}
}

#[interrupt]

fn GPIO() {
critical_section::with(|cs| {
let switch_states = OLD_SOVIET_SWITCH.borrow_ref_mut(cs).as_mut().unwrap().read_state();
println!("1 High: {:?}", switch_states.pin1_main_high);
println!("2 High: {:?}", switch_states.pin2_bottom_left_high);
println!("3 High: {:?}", switch_states.pin3_bottom_right_high);
});
}

The switch is simple enough that I could’ve achieved the same result by simply listening to the pins without the driver. However, this example could easily be expanded to accommodate a more complex physical device, and then the need for abstraction would be obvious.

--

--