Recently, I’ve been trying to learn more about electronics and embedded development. Maybe I’m just tired of operating purely in the virtual, but there’s something cool about being able to physically put together a circuit and push a button to make something happen. I went through the usual Arduino resources before seeing what Rust had to offer. I’m happy to report there’s some really good material out there.
If you’re new to embedded (but not new to Rust), I’d recommend the Discovery book as your jumping off point. It runs through the basics of how to build an embedded program, how to debug it, how to step through with GDB, how to read a data sheet, etc. After that, I’d recommend the rust embedded book, it will show you how the ecosystem is put together.
Embedded in Rust seems to really be taking off lately, there is a weekly driver initiative which I hope this crate is good enough to be considered. I’ve been hanging out in the Rust embedded matrix room if anyone wants to get at me and give me some feedback, the code that I’ll be showing is also published on github, though not yet on crates.io
.
Rust Embedded Ecosystem
It’s possible to build drivers that are hardware agnostic due to the embedded-hal
crate. It’s a collection of traits and types that abstract over specific implementations. rotary-encoder-hal
uses the InputPin trait, for example. I’ll get into the details a bit more later.
If you’re interested in how the ecosystem fits together, I recommend checking out this chapter in the embedded book.
What’s a Rotary Encoder
It’s a peripheral whose position is encoded as digital output. In other words: it’s a knob that you can turn. One nice thing about rotary encoders is they spin infinitely. The way they work is pretty simple, inside the encoder there’s two pins and a bunch of evenly spaced contacts, as you spin, the pins will touch the contacts in a definable way to create a square wave from which you can determine the direction. I apologize ahead of time for the quality of my drawings.
Then if the encoder was turned,
The resulting square wave would look like (top wave is A, bottom is B),
The key thing to notice here is that they are 90° out of step.
Implementation
The implementation is fairly straightforward. If we consider the square waves from the previous section and those that naturally follow from turning the other direction, we can construct a truth table of the current state/old state to determine whether a turn occurred and which way.
Current | Old | Direction |
---|---|---|
00 | 01 | Clockwise |
01 | 11 | Clockwise |
10 | 00 | Clockwise |
11 | 10 | Clockwise |
00 | 10 | Counter-Clockwise |
01 | 00 | Counter-Clockwise |
10 | 11 | Counter-Clockwise |
11 | 01 | Counter-Clockwise |
We can represent the complete state as a single u8
, and shift right by 2 (>> 2) to ‘move in’ the current state.
This truth table is implemented using the From
trait for u8
pub enum Direction {
Clockwise,
CounterClockwise,
None,
}
impl From<u8> for Direction {
fn from(s: u8) -> Self {
match s {
0b0001 | 0b0111 | 0b1000 | 0b1110 => Direction::Clockwise,
0b0010 | 0b0100 | 0b1011 | 0b1101 => Direction::CounterClockwise,
_ => Direction::None,
}
}
}
I’ve got a Rotary
struct that holds both pins and state.
pub struct Rotary<A, B> {
pin_a: A,
pin_b: B,
state: u8,
}
Now for the embedded-hal
part. How can we say that A
and B
are gpio pins? embedded-hal
helpfully exposes an InputPin
trait, that has methods is_low()
and is_high()
for reading the state of the pins. Using this, we can constrain the implementation of Rotary
to take two generic pins.
impl<A, B> Rotary<A, B>
where
A: InputPin,
B: InputPin,
{
pub fn new(pin_a: A, pin_b: B) -> Self {
Self {
pin_a,
pin_b,
state: 0u8,
}
}
//...
}
Rotary
exposes an update()
method, that when called reads the value for pin_a
and pin_b
into state
using bitwise ‘or’.
pub fn update(&mut self) -> Result<Direction, Either<A::Error, B::Error>> {
// use mask to get previous state value
let mut s = self.state & 0b11;
// move in the new state
if self.pin_a.is_low().map_err(Either::Left)? {
s |= 0b100;
}
if self.pin_b.is_low().map_err(Either::Right)? {
s |= 0b1000;
}
// shift new to old
self.state = s >> 2;
// and here we use the From<u8> implementation above to return a Direction
Ok(s.into())
}
I hope you liked that little foray into the world of Rust embedded! I’m a beginner in this space myself, so if anything seems out of sorts, let me know. Thanks for reading.