Rotary encoder

This article implements a rotary encoder to control the brightness of an LED with PWM. It builds on previous articles pwm_led_control and pic_compiler_comparison.

The encoder used here is the PEC11-4215F-S24 (see pdf) which is an incremental encoder with a non-latching button. It turns in discrete steps, where each step produces a square-wave signal (see Figure 1).

rotary encoder signals

Figure 1: Rotary encoder signals

These pulses can then be read by an MCU and used to determine the direction of the turn and the number of steps taken. Looking closely at this figure, if D = AB, then, moving clockwise, D = 11, 01, 00, 10 in turn.


The wiring diagram for this encoder is shown in Figure 2.

rotary encoder wiring

Figure 2: Encoder wiring diagram

This circuit has one pull-down and two pull-up resistors. When active, they keep the voltage close to zero or Vcc respectively. These resistors are often needed for logic circuits to ensure that input pin values are read as 0 and 1. The resistor values here are not strict - you could probably use anything around 1kΩ.


Each encoder position was read by combining the two encoder pin values. Referring again to Figure 1, the sequences of encoder position values are

sequence direction
00, 01, 11, 10 clockwise
10, 11, 01, 00 counter-clockwise

(You might recognize these numbers as counting in gray code (see html).)

Taking any two consecutive values in these sequences can be interpreted as a step in that direction. So a clockwise step has a value of either 0001, 0111, 1110 or 1000. Similarly, a counter-clockwise step has a value of either 1011, 1101, 0100 or 0010. There are two additional cases that need to be handled in the code: a combination of two of the same position (e.g. 0000), and any other combination (e.g. 0011). Thus, the encoder routine can be written as

enc_pos = (ENC_A << 1) | ENC_B;

if (enc_pos != prev_enc_pos) {
    enc_dir = (enc_prev_pos << 2) | enc_pos;
    if (enc_dir == 0b0001 || enc_dir == 0b0111 || enc_dir == 0b1110 || enc_dir == 0b1000) {
        // clockwise step
else if (enc_dir == 0b1011 || enc_dir == 0b1101 || enc_dir == 0b0100 || enc_dir == 0b0010) {
        // counter-clockwise step

    enc_prev_pos = enc_pos;

Since every possiblity is handled, this turns out to be very robust code.


Interrupts were used to handle input from the rotary encoder. This is because encoder values change rapidly and I was worried about errors as a result of missing a step. For the pic18f4550, PORTB pins <RB7:RB4> are best suited for interrupt-on-change. Encoder pins ENC_A and ENC_B were connected to MCU pins RB6 and RB7 respectively.

Interrupt-on-change for PORTB is for input only. The weak internal pull-ups for PORTB are not enough to pull an input up to logic 1 (although they are for an unconnected pin). So, as mentioned previously, external pull-up resistors were required for the MCU to detect a change.

Now to write code that works. As always, you should start with the simplest program that works. Then, as you add code, make sure the program continues to work. So, following this approach, the code began with (pwm_led_control). Then code that toggled the LED when the encoder button was pressed was added. Finally, the encoder routine (above) was added to change the brightness of the LED.

The entire code and makefile (gz) can be downloaded for your use.


A video of the result is shown in Video 1.

Video 1: Controlling the brightness of an LED. Filmed on a potato.


While the code is robust, it is possible to get some errors by spinning the dial wildly. I'm not sure how to make the program more tolerant to this kind of error without an oscilloscope (or some other kind of diagnostic tool) that showed how the error occurred. However for the purposes of brightening an LED, this isn't a critical problem. For dead reckoning, an optical encoder would be a better choice of input.