This exploration is about connecting a rotary encoder switch for use as an infinitely adustable up/down controller. The basic idea is that there are two switches inside, which generate pulses. In the simple rotary knobs, these two switches are open in the “click” position, and closed in a very specific way in between click positions (also known as “detents”).
Note that this article isn’t about “here’s a library, plug it in and you’re done”, but a summary of the steps taken to make it work in Forth on a JeeNode Zero, gradually improving the code and making it do more things. In short: it’s about the journey!
The unit below has a common middle connection, with the switches on the left and right pins:
In Forth code, this can be represented as:
PA5 constant ENC-A
PA3 constant ENC-B
PA4 constant ENC-C \ common
The trick is to tie the C pin to ground, and enable internal pull-ups on the A and B pins:
IMODE-HIGH ENC-A io-mode!
IMODE-HIGH ENC-B io-mode!
OMODE-PP ENC-C io-mode! ENC-C ioc!
And of course the first thing to try, is simply to verify that something is
happening. So let’s write a small loop which reads out the A and B pins
periodically and prints them out. In Mecrisp Forth, loops need to be inside a
definition, which we’ll call it read-enc
here:
: read-enc
begin
cr ENC-A io@ . ENC-B io@ .
500 ms
again ;
Now we can type read-enc
to start the loop, and very slowly turn the knob,
given that the switch changes only appear between the clicks:
-1 -1
-1 -1
-1 0
0 0
0 0
0 0
0 -1
-1 -1
-1 -1
Hey, look, there’s something going on! Looks like this hookup is working!
To understand the logic of this, we need to look at the description in the datasheet:
This particular encoder has 24 detents per full rotation (others may have 12), and we can see that there are four edges between each detent / click. Most importantly, the direction of the rotation determines the pattern:
- when rotated clockwise, the A pin pulses low before the B pin
- when rotated counter-clockwise, the B pin pulses low before the A pin
And that’s exactly the point of this encoding. Note also that only one pin changes at a time, no matter which way the knob is turned, or how fast. This is called Gray code, and it’s extremely useful in real-world signal processing, because it eliminates nasty non-deterministic timing problems (a bit like race conditions in software).
Enough theory. We need to turn these pulses into counter changes, because the real goal is to use the knob to increase or decrease a software counter when turned.
There are many different ways to do this, but let’s start as simple as possible:
a continuous loop, checking the pin states, and a little lookup, based on
previous and current values of the A and B pins. Here’s a modified version
of read-enc
, which uses some bit shifting tricks:
: read-enc
%11 \ previous state, stays on the stack
begin
2 lshift \ prev pins in bits 3 and 2
ab-pins tuck \ new pins, also save as previous for next cycle
or \ combines prev-a/prev-b/curr-a/curr-b into a 4-bit value
\ process this 4-bit value and leave only prev state on stack
case
%0001 of -1 step endof
%0010 of 1 step endof
%0100 of 1 step endof
%0111 of -1 step endof
%1000 of -1 step endof
%1011 of 1 step endof
%1101 of 1 step endof
%1110 of -1 step endof
endcase
again ;
On each iteration through the loop, we move the A & B bits 2 to the left, then put the new values of the A & B pins into bits 1 and 0, respectively. This is based on some extra code:
1000 variable counter
: ab-pins ( -- n ) \ read current A & B pin state as bits 1 and 0
ENC-A io@ %10 and ENC-B io@ %01 and or ;
: step ( n -- ) counter +! cr counter @ . ;
As Forth is strictly bottom up, counter
, ab-pins
, and step
must be defined
beforerun-enc
.
The step
word will increment or decrement the counter, and print it out. So
now, read-enc
will print changing counter
values when the knob is rotated,
and stay silent otherwise. We’ve just implemented the basics of a rotary
switch decoder!
But that’s still a bit boring, isn’t it? Let’s hook up an OLED and display the counter:
This requires some extra code (and the flib/any/digits.fs
font for the nice
large bitmaps). Only the step
word needs to be changed (the full code can be
found on
GitHub):
: step ( n -- ) counter +! counter @ shownum ;
To start things up properly, we now need to type in a few more commands:
lcd-init clear display read-enc
And that’s it. Working code for each of these can be found in the
rot1.fs,
rot2.fs,
and
rot3.fs
source files on GitHub. If you try this out, you’ll need to resend core.fs
before the rot3.fs
rotary encoder demo can be used, since it was modified to
include the digits.fs
bitmaps.
Unfortunately, there’s a problem when using the OLED: the knob appears to work, but only when turned very slowly. Normal turn rates seem to cause only erratic changes in the counter value. The reason for this is quite simple - it will be explained (and fixed) in the next article…
PS. Due to the way the pixels are mapped, the exact same code also works on 128x32 OLEDs:
(apologies for the low image quality: these were taken without flash, using a compact camera)