Rotary Encoder & Push Switch Reference¶
The front-panel rotary encoder on the Gamma synth features a mechanical quadrature encoder with an integrated push switch.
1. Pin Assignment & Electrical Characteristics¶
| Signal | Daisy Seed Pin | STM32 Pin | Configuration | Notes |
|---|---|---|---|---|
| Phase A | seed::D15 |
PC0 |
GPIO In, Pull-Up | Quadrature channel A |
| Phase B | seed::D16 |
PA3 |
GPIO In, Pull-Up | Quadrature channel B |
| Click Switch | seed::D28 |
PA2 |
GPIO In, Pull-Up | Active-Low (0 = Pressed) |
| Ground | N/A | GND | Common return | Shared ground wire |
2. Mechanical Detents & Quadrature State Machine¶
- Detent Resolution: The encoder produces 4 Gray code state transitions per mechanical detent click, resting stably at
(1, 1)at every detent. - Why Sub-step Accumulators Fail: Simple step accumulation (
s_accum += step) with arbitrary thresholding introduces contact bounce hysteresis and deadband when reversing direction, causing direction-change steps to be dropped. - Finite State Machine (FSM): A 7-state finite state machine (Buxton algorithm) tracks full Gray code paths and triggers
DIR_CWorDIR_CCWupon returning to(1, 1), providing zero hysteresis and inherent contact debouncing.
State Transition Matrix (Full-Cycle Detents at 11)¶
#define R_START 0x0
#define R_CW_FINAL 0x1
#define R_CW_BEGIN 0x2
#define R_CW_NEXT 0x3
#define R_CCW_BEGIN 0x4
#define R_CCW_FINAL 0x5
#define R_CCW_NEXT 0x6
#define DIR_CW 0x10
#define DIR_CCW 0x20
static const uint8_t kStateTable[7][4] = {
// 00 01 10 11
{R_START, R_CW_BEGIN, R_CCW_BEGIN, R_START},
{R_CW_NEXT, R_START, R_CW_FINAL, R_START | DIR_CW},
{R_CW_NEXT, R_CW_BEGIN, R_START, R_START},
{R_CW_NEXT, R_CW_BEGIN, R_CW_FINAL, R_START},
{R_CCW_NEXT, R_START, R_CCW_BEGIN, R_START},
{R_CCW_NEXT, R_CCW_FINAL, R_START, R_START | DIR_CCW},
{R_CCW_NEXT, R_CCW_FINAL, R_CCW_BEGIN, R_START},
};
3. The Display Blocking Hazard & 1 kHz Timer ISR¶
[!WARNING] Dropped Clicks Hazard: An OLED update (
oled.Update()) via I2C blocks the processor for 9 to 23 ms. Polling the rotary encoder in the main application loop will drop up to 10–20 quadrature transitions while the screen redraws.Requirement: Sample the encoder in a 1 kHz hardware timer interrupt (e.g.,
TIM5) or in the audio DMA callback (AudioCallback).
#include "daisy_seed.h"
#include "gamma_pins.h"
using namespace daisy;
static GPIO g_enc_gpio_a;
static GPIO g_enc_gpio_b;
static Switch g_enc_click;
static TimerHandle g_timer;
static volatile int32_t g_enc_pos = 0;
static volatile int g_last_inc = 0;
void TimerCallback(void* data)
{
// 1. Read Raw GPIOs
uint8_t a = g_enc_gpio_a.Read();
uint8_t b = g_enc_gpio_b.Read();
// 2. State Machine Rotary Encoder Decoder (zero hysteresis, detents at 11)
static uint8_t s_enc_state = R_START;
static uint8_t s_prev_quad = 0x03;
uint8_t curr_quad = (a << 1) | b;
if(curr_quad != s_prev_quad)
{
s_prev_quad = curr_quad;
s_enc_state = kStateTable[s_enc_state & 0x0F][curr_quad];
uint8_t result = s_enc_state & 0x30;
if(result == DIR_CW)
{
g_enc_pos++;
g_last_inc = 1;
}
else if(result == DIR_CCW)
{
g_enc_pos--;
g_last_inc = -1;
}
}
// 3. Debounce Click Switch
g_enc_click.Debounce();
}
4. Hardware Timer Configuration (TIM5 @ 1 kHz)¶
On the STM32H750 running at 480 MHz, the timer peripheral bus clock (APB1 timer clock) runs at 240 MHz.
- Prescaler:
240 - 1-> \(240\text{ MHz} / 240 = 1\text{ MHz}\) tick frequency. - Period:
1000-> \(1\text{ MHz} / 1000 = 1\text{ kHz}\) interrupt frequency (1 ms period).
void InitEncoderTimer()
{
// Initialize pins with pullups
g_enc_gpio_a.Init(gamma_pins::encoder::pin_a, GPIO::Mode::INPUT, GPIO::Pull::PULLUP);
g_enc_gpio_b.Init(gamma_pins::encoder::pin_b, GPIO::Mode::INPUT, GPIO::Pull::PULLUP);
g_enc_click.Init(gamma_pins::encoder::pin_click, 1000.0f);
// Configure TIM5
TimerHandle::Config tim_cfg;
tim_cfg.periph = TimerHandle::Config::Peripheral::TIM_5;
tim_cfg.dir = TimerHandle::Config::CounterDir::UP;
tim_cfg.enable_irq = true;
tim_cfg.period = 1000;
g_timer.Init(tim_cfg);
g_timer.SetPrescaler(240 - 1);
g_timer.SetCallback(TimerCallback, nullptr);
g_timer.Start();
}
5. Standard Push Switch Gestures (Short Click vs. Long Press)¶
Across Gamma firmware (e.g. phase5_audio, drum_synth), the encoder push switch follows a standardized two-tier gesture convention:
| Gesture | Threshold | Action | UI / Visual Feedback |
|---|---|---|---|
| Short Click | Release before \(400\text{ ms}\) | Toggle Speaker Mute (PC3 / seed::D32) |
Header badge toggles SPK \(\leftrightarrow\) MUT |
| Long Press | Hold \(\ge 2000\text{ ms}\) | Reboot into DFU Bootloader (System::ResetToBootloader) |
Screen shows HOLD FOR UPDATE progress bar |
Implementation Pattern (Timer ISR + Main Loop)¶
static constexpr uint32_t kDfuHoldMs = 2000; // Hold time to trigger bootloader
static constexpr uint32_t kDfuShowMs = 400; // Show progress bar on OLED
static constexpr uint32_t kClickMaxMs = 400; // Release before this = short click
static volatile bool g_enc_click_event = false;
static volatile bool g_reboot_bootloader = false;
static volatile uint32_t g_enc_hold_ms = 0;
// Inside 1 kHz TimerCallback:
void HandleEncoderSwitch()
{
static uint32_t s_held_ms = 0;
static bool s_dfu_fired = false;
g_enc_click.Debounce();
if(g_enc_click.Pressed())
{
if(s_held_ms < 0xFFFF)
s_held_ms++;
if(!s_dfu_fired && s_held_ms >= kDfuHoldMs)
{
s_dfu_fired = true;
g_reboot_bootloader = true;
}
}
else
{
if(s_held_ms > 0 && s_held_ms < kClickMaxMs)
g_enc_click_event = true;
s_held_ms = 0;
s_dfu_fired = false;
}
g_enc_hold_ms = s_held_ms;
}
// Inside Main Loop:
if(g_enc_click_event)
{
g_enc_click_event = false;
g_speaker_enabled = !g_speaker_enabled;
g_spk_en.Write(g_speaker_enabled);
UsbLog::PrintLine("[SPK] Speaker %s", g_speaker_enabled ? "ON" : "MUTED");
}
if(g_reboot_bootloader)
{
g_spk_en.Write(false); // Mute speaker amplifier before reset
hw.StopAudio();
ShowBootloaderScreen();
System::Delay(200);
System::ResetToBootloader(System::DAISY_INFINITE_TIMEOUT);
}