From 347379424cb6100b816478e72a7915f71718487f Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Mon, 23 Feb 2026 21:16:22 -0500 Subject: [PATCH] improved encoding --- .gitignore | 3 + CMakeLists.txt | 3 + include/EncoderHandler.h | 3 + include/globals.h | 3 + quadrature_encoder.pio | 145 +++++++++++++++++++++++++++++++++++++++ src/DisplayHandler.cpp | 43 ++++++++++-- src/EncoderHandler.cpp | 92 +++++++++++++------------ src/Gate.cpp | 6 +- src/main.cpp | 1 + 9 files changed, 250 insertions(+), 49 deletions(-) create mode 100644 .gitignore create mode 100644 quadrature_encoder.pio diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f593d29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.clangd +compile_commands.json + diff --git a/CMakeLists.txt b/CMakeLists.txt index 55cb17f..bd176ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,10 +29,13 @@ target_include_directories(clock PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib) add_subdirectory(lib/pico-ssd1306) +pico_generate_pio_header(clock ${CMAKE_CURRENT_LIST_DIR}/quadrature_encoder.pio) + # Pull in standard library and hardware abstraction target_link_libraries(clock pico_stdlib hardware_gpio + hardware_pio hardware_i2c pico_multicore pico_ssd1306 diff --git a/include/EncoderHandler.h b/include/EncoderHandler.h index d44f1ad..c3897d2 100644 --- a/include/EncoderHandler.h +++ b/include/EncoderHandler.h @@ -10,6 +10,8 @@ class EncoderHandler { private: + uint sm; + uint last_count; public: @@ -23,6 +25,7 @@ class EncoderHandler { void setup(); static void gpio_callback(uint gpio, uint32_t events); void moveCursor(bool dir = 1); + void update(); }; #endif diff --git a/include/globals.h b/include/globals.h index 478c82d..4915f1d 100644 --- a/include/globals.h +++ b/include/globals.h @@ -20,6 +20,9 @@ static constexpr uint8_t OUT_8_PIN = 14; static constexpr uint8_t SCREEN_SCL_PIN = 18; static constexpr uint8_t SCREEN_SDA_PIN = 19; +// Modify moves per detent if your encoder is acting weird +// for me, with 20 detents per full rotation, 4 works +static constexpr uint8_t TICKS_PER_DETENT = 4; static constexpr uint8_t ENCODER_CLK_PIN = 20; static constexpr uint8_t ENCODER_DT_PIN = 21; static constexpr uint8_t ENCODER_SW_PIN = 22; diff --git a/quadrature_encoder.pio b/quadrature_encoder.pio new file mode 100644 index 0000000..76966c2 --- /dev/null +++ b/quadrature_encoder.pio @@ -0,0 +1,145 @@ +; +; Copyright (c) 2023 Raspberry Pi (Trading) Ltd. +; +; SPDX-License-Identifier: BSD-3-Clause +; +.pio_version 0 // only requires PIO version 0 + +.program quadrature_encoder + +; the code must be loaded at address 0, because it uses computed jumps +.origin 0 + + +; the code works by running a loop that continuously shifts the 2 phase pins into +; ISR and looks at the lower 4 bits to do a computed jump to an instruction that +; does the proper "do nothing" | "increment" | "decrement" action for that pin +; state change (or no change) + +; ISR holds the last state of the 2 pins during most of the code. The Y register +; keeps the current encoder count and is incremented / decremented according to +; the steps sampled + +; the program keeps trying to write the current count to the RX FIFO without +; blocking. To read the current count, the user code must drain the FIFO first +; and wait for a fresh sample (takes ~4 SM cycles on average). The worst case +; sampling loop takes 10 cycles, so this program is able to read step rates up +; to sysclk / 10 (e.g., sysclk 125MHz, max step rate = 12.5 Msteps/sec) + +; 00 state + JMP update ; read 00 + JMP decrement ; read 01 + JMP increment ; read 10 + JMP update ; read 11 + +; 01 state + JMP increment ; read 00 + JMP update ; read 01 + JMP update ; read 10 + JMP decrement ; read 11 + +; 10 state + JMP decrement ; read 00 + JMP update ; read 01 + JMP update ; read 10 + JMP increment ; read 11 + +; to reduce code size, the last 2 states are implemented in place and become the +; target for the other jumps + +; 11 state + JMP update ; read 00 + JMP increment ; read 01 +decrement: + ; note: the target of this instruction must be the next address, so that + ; the effect of the instruction does not depend on the value of Y. The + ; same is true for the "JMP X--" below. Basically "JMP Y--, " + ; is just a pure "decrement Y" instruction, with no other side effects + JMP Y--, update ; read 10 + + ; this is where the main loop starts +.wrap_target +update: + MOV ISR, Y ; read 11 + PUSH noblock + +sample_pins: + ; we shift into ISR the last state of the 2 input pins (now in OSR) and + ; the new state of the 2 pins, thus producing the 4 bit target for the + ; computed jump into the correct action for this state. Both the PUSH + ; above and the OUT below zero out the other bits in ISR + OUT ISR, 2 + IN PINS, 2 + + ; save the state in the OSR, so that we can use ISR for other purposes + MOV OSR, ISR + ; jump to the correct state machine action + MOV PC, ISR + + ; the PIO does not have a increment instruction, so to do that we do a + ; negate, decrement, negate sequence +increment: + MOV Y, ~Y + JMP Y--, increment_cont +increment_cont: + MOV Y, ~Y +.wrap ; the .wrap here avoids one jump instruction and saves a cycle too + + + +% c-sdk { + +#include "hardware/clocks.h" +#include "hardware/gpio.h" + +// max_step_rate is used to lower the clock of the state machine to save power +// if the application doesn't require a very high sampling rate. Passing zero +// will set the clock to the maximum + +static inline void quadrature_encoder_program_init(PIO pio, uint sm, uint pin, int max_step_rate) +{ + pio_sm_set_consecutive_pindirs(pio, sm, pin, 2, false); + pio_gpio_init(pio, pin); + pio_gpio_init(pio, pin + 1); + + gpio_pull_up(pin); + gpio_pull_up(pin + 1); + + pio_sm_config c = quadrature_encoder_program_get_default_config(0); + + sm_config_set_in_pins(&c, pin); // for WAIT, IN + sm_config_set_jmp_pin(&c, pin); // for JMP + // shift to left, autopull disabled + sm_config_set_in_shift(&c, false, false, 32); + // don't join FIFO's + sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_NONE); + + // passing "0" as the sample frequency, + if (max_step_rate == 0) { + sm_config_set_clkdiv(&c, 1.0); + } else { + // one state machine loop takes at most 10 cycles + float div = (float)clock_get_hz(clk_sys) / (10 * max_step_rate); + sm_config_set_clkdiv(&c, div); + } + + pio_sm_init(pio, sm, 0, &c); + pio_sm_set_enabled(pio, sm, true); +} + +static inline int32_t quadrature_encoder_get_count(PIO pio, uint sm) +{ + uint ret; + int n; + + // if the FIFO has N entries, we fetch them to drain the FIFO, + // plus one entry which will be guaranteed to not be stale + n = pio_sm_get_rx_fifo_level(pio, sm) + 1; + while (n > 0) { + ret = pio_sm_get_blocking(pio, sm); + n--; + } + return ret; +} + +%} diff --git a/src/DisplayHandler.cpp b/src/DisplayHandler.cpp index 64c22fa..bbd6ed0 100644 --- a/src/DisplayHandler.cpp +++ b/src/DisplayHandler.cpp @@ -78,8 +78,41 @@ void DisplayHandler::moveCursor(bool dir) { outputs[currentOut]->modifierSelectionIndex = std::size(MOD_TYPES) - 1; } - } + } else if (currentScreen == 2) { // width control + outputs[currentOut]->editing = 1; + if (dir == 1) { + outputs[currentOut]->width++; + } else { + outputs[currentOut]->width--; + } + if (outputs[currentOut]->width > 100) { + outputs[currentOut]->width = 100; + } + + if (outputs[currentOut]->width < 1) { + outputs[currentOut]->width = 1; + } + + outputs[currentOut]->setWidth(outputs[currentOut]->width); + + } else if (currentScreen == 3) { + + outputs[currentOut]->editing = 1; + if (dir == 1) { + outputs[currentOut]->p++; + } else { + outputs[currentOut]->p--; + } + + if (outputs[currentOut]->p > 100) { + outputs[currentOut]->p = 100; + } + + if (outputs[currentOut]->p < 0) { + outputs[currentOut]->p = 0; + } + } } } else { @@ -111,16 +144,17 @@ void DisplayHandler::handleClick() { if (onOutScreen) { if (currentScreen == 0) { // exit screen - cursorPosition = currentOut; + cursorPosition = currentOut + 1; currentOut = -1; currentScreen = 0; onOutScreen = 0; + cursorClick = false; } if (currentScreen == 1 && outputs[currentOut]->editing == 1) { outputs[currentOut]->setDiv(outputs[currentOut]->modifierSelectionIndex); - cursorClick = 0; outputs[currentOut]->editing = 0; + cursorClick = false; } } else { @@ -198,7 +232,8 @@ void DisplayHandler::renderMainPage() { void DisplayHandler::renderOutPage() { - std::string title = std::to_string(currentOut) + "| " + out_pages[currentScreen]; + uint8_t visualOut = currentOut + 1; + std::string title = std::to_string(visualOut) + "| " + out_pages[currentScreen]; pico_ssd1306::drawText(display, font_12x16, title.c_str(), 1, 2); std::string param_string; diff --git a/src/EncoderHandler.cpp b/src/EncoderHandler.cpp index 97be2cb..aa000a6 100644 --- a/src/EncoderHandler.cpp +++ b/src/EncoderHandler.cpp @@ -5,6 +5,8 @@ #include "globals.h" #include #include +#include "hardware/pio.h" +#include "quadrature_encoder.pio.h" static EncoderHandler* self = nullptr; @@ -19,53 +21,59 @@ EncoderHandler::EncoderHandler(DisplayHandler* display_handler) { void EncoderHandler::gpio_callback(uint gpio, uint32_t events) { - static uint64_t last_sw_time = 0; - static uint64_t last_rotate_time = 0; - uint64_t now = to_us_since_boot(get_absolute_time()); + uint64_t now = to_us_since_boot(get_absolute_time()); + static uint64_t last_sw_time = 0; - if (gpio == ENCODER_SW_PIN) { // handle button press - if (now - last_sw_time > 200000) { - self->display_handler->handleClick(); - last_sw_time = now; - } - } else if (gpio == ENCODER_CLK_PIN) { // handle encoder turn - - if (now - last_rotate_time < 5000) return; - - if (events & GPIO_IRQ_EDGE_FALL) { - if (gpio_get(ENCODER_CLK_PIN) == 0) { - - uint16_t dt_state = gpio_get(ENCODER_DT_PIN); - - if (dt_state) { - self->display_handler->moveCursor(); - } else { - self->display_handler->moveCursor(0); - } - } - - last_rotate_time = now; - } - } + if (gpio == ENCODER_SW_PIN) { + if (now - last_sw_time > 200000) { // 200ms debounce + self->display_handler->handleClick(); + last_sw_time = now; + } + } } - void EncoderHandler::setup() { - gpio_init(ENCODER_SW_PIN); - gpio_set_dir(ENCODER_SW_PIN, GPIO_IN); - gpio_pull_up(ENCODER_SW_PIN); + self = this; - gpio_init(ENCODER_CLK_PIN); - gpio_set_dir(ENCODER_CLK_PIN, GPIO_IN); - gpio_pull_up(ENCODER_CLK_PIN); + // 1. Setup Button (Standard GPIO) + gpio_init(ENCODER_SW_PIN); + gpio_set_dir(ENCODER_SW_PIN, GPIO_IN); + gpio_pull_up(ENCODER_SW_PIN); + + // Enable IRQ only for the switch + gpio_set_irq_enabled_with_callback(ENCODER_SW_PIN, GPIO_IRQ_EDGE_FALL, true, &EncoderHandler::gpio_callback); - gpio_init(ENCODER_DT_PIN); - gpio_set_dir(ENCODER_DT_PIN, GPIO_IN); - gpio_pull_up(ENCODER_DT_PIN); + // 2. Setup Rotation (PIO) + // Note: ENCODER_CLK_PIN and ENCODER_DT_PIN must be consecutive! + PIO pio = pio0; + uint offset = pio_add_program(pio, &quadrature_encoder_program); + this->sm = pio_claim_unused_sm(pio, true); + + // Internal pull-ups still needed for the PIO pins + gpio_init(ENCODER_CLK_PIN); + gpio_pull_up(ENCODER_CLK_PIN); + gpio_init(ENCODER_DT_PIN); + gpio_pull_up(ENCODER_DT_PIN); - clk_last_state = gpio_get(ENCODER_CLK_PIN); - - gpio_set_irq_enabled_with_callback(20, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &EncoderHandler::gpio_callback); - gpio_set_irq_enabled(21, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); - gpio_set_irq_enabled(22, GPIO_IRQ_EDGE_FALL, true); + quadrature_encoder_program_init(pio, sm, ENCODER_CLK_PIN, 0); + + this->last_count = 0; +} + +// Call this in your main loop or a timer +void EncoderHandler::update() { + int32_t current_count = quadrature_encoder_get_count(pio0, this->sm); + + int32_t delta = current_count - last_count; + + if (abs(delta) >= TICKS_PER_DETENT) { + + if (delta < 0) { // Changed from > to < to reverse direction + display_handler->moveCursor(); // Clockwise + } else { + display_handler->moveCursor(0); // Counter-clockwise + } + + last_count = current_count - (delta % TICKS_PER_DETENT); + } } diff --git a/src/Gate.cpp b/src/Gate.cpp index c69bbcf..10b3ef5 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -30,19 +30,19 @@ void Gate::setLen(uint32_t currentPeriod) { void Gate::setDiv(uint8_t modifier_selecton_index) { switch(modifier_selecton_index) { case 0: // x8 (32nd triplets) - tickInterval = 8; // 96 / 12 hits per beat + tickInterval = 12; // 96 / 12 hits per beat isEnabled = true; divideMode = 0; modifier = 8; break; case 1: // x4 (16th triplets) - tickInterval = 16; // 96 / 6 hits per beat + tickInterval = 24; // 96 / 6 hits per beat isEnabled = true; divideMode = 0; modifier = 4; break; case 2: // x2 (8th triplets) - tickInterval = 32; // 96 / 3 hits per beat + tickInterval = 48; // 96 / 3 hits per beat isEnabled = true; divideMode = 0; modifier = 2; diff --git a/src/main.cpp b/src/main.cpp index 13a925c..daf883a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -123,6 +123,7 @@ int main() { update_period(); while (true) { + encoder_handler.update(); handle_outs(); } }