├── 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 --------------------------------------------------------------------------------