├── LICENSE
├── Quadrature_Diagram.png
├── README.md
├── code_explanation.png
├── pio_rotary_encoder.cpp
├── pio_rotary_encoder.pio
└── rotary_encoder.jpg
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 GitJer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Quadrature_Diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitJer/Rotary_encoder/49a330fd747457a5bcce89127be0c5c854c102df/Quadrature_Diagram.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # This repository has been updated and moved to [here](https://github.com/GitJer/Some_RPI-Pico_stuff/tree/main/Rotary_encoder)
2 |
3 | Please use the new repository!
4 |
5 | OLD TEXT:
6 |
7 |
8 |
9 | # Rotary encoder for Raspberry Pi Pico using PIO code
10 |
11 | This software reads a rotary encoder with the Raspberry Pi Pico using PIO code.
12 | The rotary encoder that is used is an optical encoder with very clean signals on its output, called A and B, so debouncing of these signals is unnecessary.
13 |
14 |
15 |
16 | The specific encoder I use gives 600 pulses per 360 degrees rotation. For each pulse, 4 signal changes are measured, see the next figure from [wikipedia](https://en.wikipedia.org/wiki/Rotary_encoder#/media/File:Quadrature_Diagram.svg), so in total 2400 transitions are made for a full rotation.
17 |
18 |
19 |
20 | I have also tried a cheap simple rotary encoder, but only when turning very slowly does the result make any sense.
21 |
22 | ## Other code
23 | This isn't the first code to read a rotary encoder using the PIO, see e.g. [pimoroni-pico](https://github.com/pimoroni/pimoroni-pico/blob/encoder-pio/drivers/encoder-pio/encoder.pio), which does timing and rudimentary debouncing. And it lets you choose non-consecutive pins.
24 |
25 | But this code uses interrupts in the PIO code to signal rotations to the C++ code. And maybe this can best be considered as an exercise on how to make something useful with:
26 | - 19 `jmp` instructions (one hidden in a `mov exec`)
27 | - 3 `in` instructions
28 | - 2 `irq` instructions
29 | - 1 `out` instruction
30 | - 1 `mov` instruction (well ... technically 2)
31 |
32 | But seriously, I wanted to play with a jump table in PIO code. This means setting the `.origin` to make sure the jump table is at a fixed position in instruction memory (0), and setting the initial program counter with `pio_sm_init(pio, sm, 16, &c)` to start at instruction location 16, i.e. after the jump table. It also means that the `mov exec` instruction is used to make a jump to the jump table.
33 |
34 | ## Explanation of PIO code
35 | The first 15 addresses are a list of jumps, forming a jump table, that is later used to raise either an IRQ that signals a clockwise rotation or an IRQ to signal a counter clockwise rotation. See below.
36 |
37 | The important steps are explained in the figure below. There the program line number, the contents of the Output Shift Register (OSR) and the Input Shift Register (ISR) are shown.
38 |
39 | The program starts at line 16, which is only used as an initialization of the program: the two pins (whose values are denoted as A and B) of the rotary encoder are read into the ISR. The two rightmost bits are now AB. The other bits in the ISR do not play a role at this moment and are denoted as 'x'.
40 |
41 | The next step, line 17, is the actual start of the program: the content of the ISR is copied to the OSR. The two bits AB are to be used as the previous values, and are therefore denoted as A' and B'.
42 |
43 | Line 18 clears the ISR because the zero valued bits are needed later. Then, in two steps, the ISR gets shifted in the old values, A'B' from the OSR, and reads in two new readings of the rotary encoder (AB).
44 |
45 | Now, at line 20, the 16 left most bits of the ISR contain: 000000000000A'B'AB.
46 | According to the [datasheet of the rp2040](https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf) an unconditional jmp instruction without delay or side-set is, apart from the address to jump to, all zeros. And that is exactly what is now contained in the ISR: a jmp instruction to address 0A'B'AB. This is always an address in the first 16 program addresses (0-15). The PIO has an instruction to interpret the content of the ISR as an instruction and execute it: on line 20 the `mov exec ISR` does exactly this.
47 |
48 |
49 |
50 | ## Jump table
51 |
52 | The first 16 instructions form a jump table. The addresses represent the 12 legal transitions and 4 error transitions that can be found in the output of the rotary encoder. As described above, the address that is jumped to is represented by the two old values and the two current values of the output: A'B'AB. The 16 transitions are:
53 |
54 | ```
55 | A'B'A B = meaning; action
56 | --------------------------------------------------------------------------------------
57 | 0 0 0 0 = transition from 00 to 00 = no change in reading; do a jump to line 17
58 | 0 0 0 1 = transition from 00 to 01 = clockwise rotation; do a jump to CW
59 | 0 0 1 0 = transition from 00 to 10 = counter clockwise rotation; do a jump to CCW
60 | 0 0 1 1 = transition from 00 to 11 = error; do a jump to line 17
61 | 0 1 0 0 = transition from 01 to 00 = counter clockwise rotation; do a jump to CCW
62 | 0 1 0 1 = transition from 01 to 01 = no change in reading; do a jump to line 17
63 | 0 1 1 0 = transition from 01 to 10 = error; do a jump to line 17
64 | 0 1 1 1 = transition from 01 to 11 = clockwise rotation; do a jump to CW
65 | 1 0 0 0 = transition from 10 to 00 = clockwise rotation; do a jump to CW
66 | 1 0 0 1 = transition from 10 to 01 = error; do a jump to line 17
67 | 1 0 1 0 = transition from 10 to 10 = no change in reading; do a jump to line 17
68 | 1 0 1 1 = transition from 10 to 11 = counter clockwise rotation; do a jump to CCW
69 | 1 1 0 0 = transition from 11 to 00 = error; do a jump to line 17
70 | 1 1 0 1 = transition from 11 to 01 = counter clockwise rotation; do a jump to CCW
71 | 1 1 1 0 = transition from 11 to 10 = clockwise rotation; do a jump to CW
72 | 1 1 1 1 = transition from 11 to 11 = no change in reading; do a jump to line 17
73 | ```
74 | The jump to `CW` is a piece of code that sets `irq 0` and then jumps to line 17, the jump to `CCW` sets `irq 1` and then jumps to line 17.
75 | In the C++ code, the `irq 0` causes a counter called `rotation` to be increased, the `irq 1` causes it to be decreased.
76 |
--------------------------------------------------------------------------------
/code_explanation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitJer/Rotary_encoder/49a330fd747457a5bcce89127be0c5c854c102df/code_explanation.png
--------------------------------------------------------------------------------
/pio_rotary_encoder.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include "pico/stdlib.h"
4 | #include "hardware/pio.h"
5 | #include "hardware/irq.h"
6 |
7 | #include "pio_rotary_encoder.pio.h"
8 |
9 | // class to read the rotation of the rotary encoder
10 | class RotaryEncoder
11 | {
12 | public:
13 | // constructor
14 | // rotary_encoder_A is the pin for the A of the rotary encoder.
15 | // The B of the rotary encoder has to be connected to the next GPIO.
16 | RotaryEncoder(uint rotary_encoder_A)
17 | {
18 | uint8_t rotary_encoder_B = rotary_encoder_A + 1;
19 | // pio 0 is used
20 | PIO pio = pio0;
21 | // state machine 0
22 | uint8_t sm = 0;
23 | // configure the used pins as input with pull up
24 | pio_gpio_init(pio, rotary_encoder_A);
25 | gpio_set_pulls(rotary_encoder_A, true, false);
26 | pio_gpio_init(pio, rotary_encoder_B);
27 | gpio_set_pulls(rotary_encoder_B, true, false);
28 | // load the pio program into the pio memory
29 | uint offset = pio_add_program(pio, &pio_rotary_encoder_program);
30 | // make a sm config
31 | pio_sm_config c = pio_rotary_encoder_program_get_default_config(offset);
32 | // set the 'in' pins
33 | sm_config_set_in_pins(&c, rotary_encoder_A);
34 | // set shift to left: bits shifted by 'in' enter at the least
35 | // significant bit (LSB), no autopush
36 | sm_config_set_in_shift(&c, false, false, 0);
37 | // set the IRQ handler
38 | irq_set_exclusive_handler(PIO0_IRQ_0, pio_irq_handler);
39 | // enable the IRQ
40 | irq_set_enabled(PIO0_IRQ_0, true);
41 | pio0_hw->inte0 = PIO_IRQ0_INTE_SM0_BITS | PIO_IRQ0_INTE_SM1_BITS;
42 | // init the sm.
43 | // Note: the program starts after the jump table -> initial_pc = 16
44 | pio_sm_init(pio, sm, 16, &c);
45 | // enable the sm
46 | pio_sm_set_enabled(pio, sm, true);
47 | }
48 |
49 | // set the current rotation to a specific value
50 | void set_rotation(int _rotation)
51 | {
52 | rotation = _rotation;
53 | }
54 |
55 | // get the current rotation
56 | int get_rotation(void)
57 | {
58 | return rotation;
59 | }
60 |
61 | private:
62 | static void pio_irq_handler()
63 | {
64 | // test if irq 0 was raised
65 | if (pio0_hw->irq & 1)
66 | {
67 | rotation = rotation - 1;
68 | }
69 | // test if irq 1 was raised
70 | if (pio0_hw->irq & 2)
71 | {
72 | rotation = rotation + 1;
73 | }
74 | // clear both interrupts
75 | pio0_hw->irq = 3;
76 | }
77 |
78 | // the pio instance
79 | PIO pio;
80 | // the state machine
81 | uint sm;
82 | // the current location of rotation
83 | static int rotation;
84 | };
85 |
86 | // Initialize static member of class Rotary_encoder
87 | int RotaryEncoder::rotation = 0;
88 |
89 | int main()
90 | {
91 | // needed for printf
92 | stdio_init_all();
93 | // the A of the rotary encoder is connected to GPIO 16, B to GPIO 17
94 | RotaryEncoder my_encoder(16);
95 | // initialize the rotatry encoder rotation as 0
96 | my_encoder.set_rotation(0);
97 | // infinite loop to print the current rotation
98 | while (true)
99 | {
100 | printf("rotation=%d\n", my_encoder.get_rotation());
101 | }
102 | }
--------------------------------------------------------------------------------
/pio_rotary_encoder.pio:
--------------------------------------------------------------------------------
1 |
2 | .program pio_rotary_encoder
3 | .wrap_target
4 | .origin 0 ; The jump table has to start at 0
5 | ; it contains the correct jumps for each of the 16
6 | ; combination of 4 bits formed by A'B'AB
7 | ; A = current reading of pin_A of the rotary encoder
8 | ; A' = previous reading of pin_A of the rotary encoder
9 | ; B = current reading of pin_B of the rotary encoder
10 | ; B' = previous reading of pin_B of the rotary encoder
11 | jmp read ; 0000 = from 00 to 00 = no change in reading
12 | jmp CW ; 0001 = from 00 to 01 = clockwise rotation
13 | jmp CCW ; 0010 = from 00 to 10 = counter clockwise rotation
14 | jmp read ; 0011 = from 00 to 11 = error
15 |
16 | jmp CCW ; 0100 = from 01 to 00 = counter clockwise rotation
17 | jmp read ; 0101 = from 01 to 01 = no change in reading
18 | jmp read ; 0110 = from 01 to 10 = error
19 | jmp CW ; 0111 = from 01 to 11 = clockwise rotation
20 |
21 | jmp CW ; 1000 = from 10 to 00 = clockwise rotation
22 | jmp read ; 1001 = from 10 to 01 = error
23 | jmp read ; 1010 = from 10 to 10 = no change in reading
24 | jmp CCW ; 1011 = from 10 to 11 = counter clockwise rotation
25 |
26 | jmp read ; 1100 = from 11 to 00 = error
27 | jmp CCW ; 1101 = from 11 to 01 = counter clockwise rotation
28 | jmp CW ; 1110 = from 11 to 10 = clockwise rotation
29 | jmp read ; 1111 = from 11 to 11 = no change in reading
30 |
31 | pc_start: ; this is the entry point for the program
32 | in pins 2 ; read the current values of A and B and use
33 | ; them to initialize the previous values (A'B')
34 | read:
35 | mov OSR ISR ; the OSR is (after the next instruction) used to shift
36 | ; the two bits with the previous values into the ISR
37 | out ISR 2 ; shift the previous value into the ISR. This also sets
38 | ; all other bits in the ISR to 0
39 | in pins 2 ; shift the current value into the ISR
40 | ; the 16 LSB of the ISR now contain 000000000000A'B'AB
41 | ; this represents a jmp instruction to the address A'B'AB
42 | mov exec ISR ; do the jmp encoded in the ISR
43 | CW: ; a clockwise rotation was detected
44 | irq 0 ; signal a clockwise rotation via an IRQ
45 | jmp read ; jump to reading the current values of A and B
46 | CCW: ; a counter clockwise rotation was detected
47 | irq 1 ; signal a counter clockwise rotation via an IRQ
48 | ; jmp read ; jump to reading the current values of A and B.
49 | ; the jmp isn't needed because of the .wrap, and the first
50 | ; statement of the program happens to be a jmp read
51 | .wrap
--------------------------------------------------------------------------------
/rotary_encoder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GitJer/Rotary_encoder/49a330fd747457a5bcce89127be0c5c854c102df/rotary_encoder.jpg
--------------------------------------------------------------------------------