update-encoder-logic #2

Merged
dominic merged 3 commits from update-encoder-logic into master 2026-02-24 18:54:01 -05:00
9 changed files with 250 additions and 49 deletions
Showing only changes of commit 347379424c - Show all commits

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.clangd
compile_commands.json

View file

@ -29,10 +29,13 @@ target_include_directories(clock PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/lib)
add_subdirectory(lib/pico-ssd1306) add_subdirectory(lib/pico-ssd1306)
pico_generate_pio_header(clock ${CMAKE_CURRENT_LIST_DIR}/quadrature_encoder.pio)
# Pull in standard library and hardware abstraction # Pull in standard library and hardware abstraction
target_link_libraries(clock target_link_libraries(clock
pico_stdlib pico_stdlib
hardware_gpio hardware_gpio
hardware_pio
hardware_i2c hardware_i2c
pico_multicore pico_multicore
pico_ssd1306 pico_ssd1306

View file

@ -10,6 +10,8 @@
class EncoderHandler { class EncoderHandler {
private: private:
uint sm;
uint last_count;
public: public:
@ -23,6 +25,7 @@ class EncoderHandler {
void setup(); void setup();
static void gpio_callback(uint gpio, uint32_t events); static void gpio_callback(uint gpio, uint32_t events);
void moveCursor(bool dir = 1); void moveCursor(bool dir = 1);
void update();
}; };
#endif #endif

View file

@ -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_SCL_PIN = 18;
static constexpr uint8_t SCREEN_SDA_PIN = 19; 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_CLK_PIN = 20;
static constexpr uint8_t ENCODER_DT_PIN = 21; static constexpr uint8_t ENCODER_DT_PIN = 21;
static constexpr uint8_t ENCODER_SW_PIN = 22; static constexpr uint8_t ENCODER_SW_PIN = 22;

145
quadrature_encoder.pio Normal file
View file

@ -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--, <next addr>"
; 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;
}
%}

View file

@ -78,8 +78,41 @@ void DisplayHandler::moveCursor(bool dir) {
outputs[currentOut]->modifierSelectionIndex = std::size(MOD_TYPES) - 1; 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 { } else {
@ -111,16 +144,17 @@ void DisplayHandler::handleClick() {
if (onOutScreen) { if (onOutScreen) {
if (currentScreen == 0) { // exit screen if (currentScreen == 0) { // exit screen
cursorPosition = currentOut; cursorPosition = currentOut + 1;
currentOut = -1; currentOut = -1;
currentScreen = 0; currentScreen = 0;
onOutScreen = 0; onOutScreen = 0;
cursorClick = false;
} }
if (currentScreen == 1 && outputs[currentOut]->editing == 1) { if (currentScreen == 1 && outputs[currentOut]->editing == 1) {
outputs[currentOut]->setDiv(outputs[currentOut]->modifierSelectionIndex); outputs[currentOut]->setDiv(outputs[currentOut]->modifierSelectionIndex);
cursorClick = 0;
outputs[currentOut]->editing = 0; outputs[currentOut]->editing = 0;
cursorClick = false;
} }
} else { } else {
@ -198,7 +232,8 @@ void DisplayHandler::renderMainPage() {
void DisplayHandler::renderOutPage() { 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); pico_ssd1306::drawText(display, font_12x16, title.c_str(), 1, 2);
std::string param_string; std::string param_string;

View file

@ -5,6 +5,8 @@
#include "globals.h" #include "globals.h"
#include <string> #include <string>
#include <cstdlib> #include <cstdlib>
#include "hardware/pio.h"
#include "quadrature_encoder.pio.h"
static EncoderHandler* self = nullptr; static EncoderHandler* self = nullptr;
@ -19,53 +21,59 @@ EncoderHandler::EncoderHandler(DisplayHandler* display_handler) {
void EncoderHandler::gpio_callback(uint gpio, uint32_t events) { 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 (gpio == ENCODER_SW_PIN) {
if (now - last_sw_time > 200000) { if (now - last_sw_time > 200000) { // 200ms debounce
self->display_handler->handleClick(); self->display_handler->handleClick();
last_sw_time = now; 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;
}
}
}
void EncoderHandler::setup() { void EncoderHandler::setup() {
self = this;
// 1. Setup Button (Standard GPIO)
gpio_init(ENCODER_SW_PIN); gpio_init(ENCODER_SW_PIN);
gpio_set_dir(ENCODER_SW_PIN, GPIO_IN); gpio_set_dir(ENCODER_SW_PIN, GPIO_IN);
gpio_pull_up(ENCODER_SW_PIN); gpio_pull_up(ENCODER_SW_PIN);
gpio_init(ENCODER_CLK_PIN); // Enable IRQ only for the switch
gpio_set_dir(ENCODER_CLK_PIN, GPIO_IN); gpio_set_irq_enabled_with_callback(ENCODER_SW_PIN, GPIO_IRQ_EDGE_FALL, true, &EncoderHandler::gpio_callback);
gpio_pull_up(ENCODER_CLK_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_init(ENCODER_DT_PIN);
gpio_set_dir(ENCODER_DT_PIN, GPIO_IN);
gpio_pull_up(ENCODER_DT_PIN); gpio_pull_up(ENCODER_DT_PIN);
clk_last_state = gpio_get(ENCODER_CLK_PIN); quadrature_encoder_program_init(pio, sm, ENCODER_CLK_PIN, 0);
gpio_set_irq_enabled_with_callback(20, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, &EncoderHandler::gpio_callback); this->last_count = 0;
gpio_set_irq_enabled(21, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true); }
gpio_set_irq_enabled(22, GPIO_IRQ_EDGE_FALL, true);
// 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);
}
} }

View file

@ -30,19 +30,19 @@ void Gate::setLen(uint32_t currentPeriod) {
void Gate::setDiv(uint8_t modifier_selecton_index) { void Gate::setDiv(uint8_t modifier_selecton_index) {
switch(modifier_selecton_index) { switch(modifier_selecton_index) {
case 0: // x8 (32nd triplets) case 0: // x8 (32nd triplets)
tickInterval = 8; // 96 / 12 hits per beat tickInterval = 12; // 96 / 12 hits per beat
isEnabled = true; isEnabled = true;
divideMode = 0; divideMode = 0;
modifier = 8; modifier = 8;
break; break;
case 1: // x4 (16th triplets) case 1: // x4 (16th triplets)
tickInterval = 16; // 96 / 6 hits per beat tickInterval = 24; // 96 / 6 hits per beat
isEnabled = true; isEnabled = true;
divideMode = 0; divideMode = 0;
modifier = 4; modifier = 4;
break; break;
case 2: // x2 (8th triplets) case 2: // x2 (8th triplets)
tickInterval = 32; // 96 / 3 hits per beat tickInterval = 48; // 96 / 3 hits per beat
isEnabled = true; isEnabled = true;
divideMode = 0; divideMode = 0;
modifier = 2; modifier = 2;

View file

@ -123,6 +123,7 @@ int main() {
update_period(); update_period();
while (true) { while (true) {
encoder_handler.update();
handle_outs(); handle_outs();
} }
} }