Decoding an EC11 Rotary Encoder over RS485
Decoding an EC11 Rotary Encoder over RS485#
Today I worked on decoding an EC11 rotary encoder, sending the result over RS485, and displaying it on a receiver setup.
The receiver setup consists of:
- 1 x Raspberry Pi Pico 2 W
- 1 x RS485 receiver board
- 1 x SH1106-based OLED display
The transmitter setup consists of:
- 1 x EC11 rotary encoder on a small prototype PCB
- 1 x Digital IO Expander Board
- 1 x Raspberry Pi Pico 2 W
- 1 x RS485 transmitter board
The basic idea was to read the A and B pins from the rotary encoder, decode the direction of travel on the transmitter side, send a simple message over the RS485 link, and then display the result on the receiver.
The protocol is still very rudimentary, but it works well enough for my current testing.
Reading the Encoder#
Decoding the signals from an EC11 rotary encoder is a common enough task, but it is still worth documenting how I handled it in this setup.
The encoder has three pins for the rotary section:
- A
- B
- Common, usually marked C
It also has two additional pins for the push switch, usually marked D and E.
In my test setup, pin C is connected to 3.3 V. The A and B pins are connected to inputs on the Digital IO Expander Board, which are read through 74HC165 shift registers. This board features built-in pull-down resistors, keeping the lines from floating when the encoder switches are open.
With this arrangement, the A and B inputs normally sit low. When the encoder is rotated, A and B change state in sequence. The order they change in tells you the direction of rotation.
When the encoder is turned clockwise, A changes first, followed by B. The signals are offset from each other, giving the usual quadrature-style pattern.
For clockwise rotation, the sequence is:
| Pin C State | Pin A State | Pin B State | Notes |
|---|---|---|---|
| 1 | 0 | 0 | Default / stationary |
| 1 | 1 | 0 | First step |
| 1 | 1 | 1 | Second step |
| 1 | 0 | 1 | Third step |
| 1 | 0 | 0 | Back to default / stationary |
If A is treated as the upper bit and B as the lower bit, this gives the following sequence:
0, 2, 3, 1, 0
When the encoder is turned counter-clockwise, B changes first, followed by A.
For counter-clockwise rotation, the sequence is:
| Pin C State | Pin A State | Pin B State | Notes |
|---|---|---|---|
| 1 | 0 | 0 | Default / stationary |
| 1 | 0 | 1 | First step |
| 1 | 1 | 1 | Second step |
| 1 | 1 | 0 | Third step |
| 1 | 0 | 0 | Back to default / stationary |
This gives the following sequence:
0, 1, 3, 2, 0
The useful part is that the two sequences are different. By keeping track of the previous state and the current state, the direction of rotation can be determined.
Decoding the Direction#
The method I am currently using is based on comparing the previous A/B state with the current A/B state.
The valid clockwise transitions are:
00 -> 10
10 -> 11
11 -> 01
01 -> 00
The valid counter-clockwise transitions are:
00 -> 01
01 -> 11
11 -> 10
10 -> 00
So rather than only checking which bit changed, the decoder needs to check the transition from the previous state to the current state.
The logic can be expressed roughly as:
previous = 0
loop forever:
sleep for 250 us
current = read encoder A/B state
if current is different from previous:
transition = previous -> current
if transition is a valid clockwise transition:
send "CW"
remember last direction as clockwise
else if transition is a valid counter-clockwise transition:
send "CCW"
remember last direction as counter-clockwise
else:
the encoder may have moved too quickly,
bounced, or a reading may have been missed
if there is a known previous direction:
optionally assume the same direction continued
otherwise:
ignore the transition
previous = current
For now, this seems to work well enough for testing.
The encoder is not being read directly from GPIO interrupts. Instead, the current input state is being polled from the Digital IO Expander Board through the 74HC165 shift registers.
That means the sampling rate matters. It needs to be fast enough to catch the state changes as the encoder is turned, but not so fast that it wastes unnecessary processing time.
My current polling delay is:
250 us
or:
0.25 ms
That seems to work well for my current setup. If I increase that delay too much, I run the risk of missing transitions as the encoder is turned.
An elegant side effect of this state-machine approach is that it provides a useful amount of software filtering. Because the logic checks for valid quadrature transitions, some noisy or invalid state changes can be ignored rather than being treated as real direction updates.
On a higher powered MCU like the RP2350 used on the Raspberry Pi Pico 2 W, this polling overhead is probably not much of a concern. On a smaller device, such as an ATtiny1616, the trade-off may become more important.
Processing on the Transmitter#
I also decided to process the encoder direction on the transmitter side rather than send all raw A/B state changes across the RS485 link.
That feels closer to how I would probably use this in a real project.
Instead of the receiver needing to understand the raw encoder transitions, the transmitter can simply send higher-level messages such as:
CW
CCW
For this prototype, these updates are sent over the RS485 link as simple ASCII strings at 115200 baud. Offloading the decoding to the transmitter keeps the implementation dead simple, minimizes bus overhead, and reduces the processing work required by the receiver.
That may become more useful later if I attach multiple transmitter boards to the same RS485 bus. In that situation, I would rather have each transmitter board do its own local input processing and only send meaningful events when something has actually changed.
Current Result#
At this point, the test setup is working.
The rotary encoder is connected to the Digital IO Expander Board. The input state is read by a Raspberry Pi Pico 2 W, decoded locally, sent over RS485, received by another Raspberry Pi Pico 2 W, and displayed on an OLED screen.
It is still a simple test, but it is a useful one.
It proves that the Digital IO Expander Board can be used for more than simple button or switch inputs, and that the RS485 board can be used to move higher-level input events between boards.
It also gives me another small building block for future control panels, remote inputs, menu controls, and distributed interface boards.
If you are looking for reusable open hardware modules to speed up your bench testing, or need support designing custom breakout circuits, check out our catalog at Protoworks (NI).
- 2026-10-17 Refining the Protoworks service offerings Lab
- 2026-09-27 Belfast Tech Week Lab
- 2026-09-19 Refining the UCMOD Architecture R&D
- 2026-09-06 Giving AI Agents a Way to Talk to Protoworks Software
- 2026-09-03 A Morning at a Manufacturing Trade Show Lab
- 2026-09-01 Debugging 921600 Baud on the ATtiny1614 R&D