Skip to content

Complete Integration Architecture & Main Loop Blueprint

This blueprint demonstrates how to combine the OLED display, 14 tactile keypad switches, rotary encoder, potentiometers, and joysticks into a responsive, non-blocking firmware architecture.


1. System Threading & Timing Model

flowchart TD
    subgraph ISR ["1 kHz Hardware Timer ISR (TIM5)"]
        T1["Sample Encoder GPIOs"] --> T2["Gray Code State Machine"]
        T2 --> T3["Update volatile g_enc_pos"]
        T4["Debounce 14 Keys + Click"] --> T5{"Edge Detected?"}
        T5 -- "Yes" --> T6["Push to Lock-Free Ring Buffer"]
        T5 -- "No" --> T7["Return"]
    end

    subgraph AppLoop ["Application Main Loop (while(1))"]
        M1["Drain Ring Buffer Key Events"] --> M2["Dispatch Note/Chord Events"]
        M2 --> M3["Check Encoder Delta"]
        M3 --> M4["Poll ADC Knobs & Joysticks"]
        M4 --> M5{"Screen Refresh Due? (~30 Hz)"}
        M5 -- "Yes" --> M6["Draw Buffer & oled.Update() (blocks 9ms)"]
        M5 -- "No" --> M7["System::Delay(1)"]
    end

    T6 -.-> M1

2. Integrated Implementation Skeleton

#include "daisy_seed.h"
#include "dev/oled_ssd130x.h"
#include "util/oled_fonts.h"
#include "gamma_pins.h"
#include <cmath>

using namespace daisy;

DaisySeed hw;

// OLED
using MyOled = OledDisplay<SSD130xI2c128x64Driver>;
static MyOled oled;

// Digital Peripherals
static Switch      g_note_keys[gamma_pins::note_keys::COUNT];
static Switch      g_chord_keys[gamma_pins::chord_keys::COUNT];
static Switch      g_enc_click;
static GPIO        g_enc_gpio_a;
static GPIO        g_enc_gpio_b;
static TimerHandle g_timer;

// Ring Buffer
struct KeyEvent { uint8_t id; bool pressed; };
static constexpr size_t kEventQueueSize = 32;
static KeyEvent         g_event_queue[kEventQueueSize];
static volatile size_t  g_eq_head = 0;
static volatile size_t  g_eq_tail = 0;

static volatile int32_t g_enc_pos  = 0;
static volatile int     g_last_inc = 0;

// Analog State
static float g_knobs[gamma_pins::knobs::COUNT]       = {0.0f};
static float g_sticks[gamma_pins::joysticks::COUNT] = {0.5f, 0.5f, 0.5f, 0.5f};

static inline void EnqueueEvent(uint8_t id, bool pressed)
{
    size_t next = (g_eq_head + 1) % kEventQueueSize;
    if(next != g_eq_tail)
    {
        g_event_queue[g_eq_head].id      = id;
        g_event_queue[g_eq_head].pressed = pressed;
        g_eq_head                        = next;
    }
}

// 1 kHz Timer ISR
void TimerCallback(void* data)
{
    // Encoder decoding
    uint8_t a = g_enc_gpio_a.Read();
    uint8_t b = g_enc_gpio_b.Read();
    // Rotary Encoder State Machine (zero hysteresis, detents at 11)
    static uint8_t s_enc_state = 0;
    static uint8_t s_prev_quad = 0x03;
    uint8_t curr_quad = (a << 1) | b;
    if(curr_quad != s_prev_quad)
    {
        static const uint8_t kStateTable[7][4] = {
            {0x0, 0x2, 0x4, 0x0},
            {0x3, 0x0, 0x1, 0x0 | 0x10},
            {0x3, 0x2, 0x0, 0x0},
            {0x3, 0x2, 0x1, 0x0},
            {0x6, 0x0, 0x4, 0x0},
            {0x6, 0x5, 0x0, 0x0 | 0x20},
            {0x6, 0x5, 0x4, 0x0},
        };
        s_prev_quad = curr_quad;
        s_enc_state = kStateTable[s_enc_state & 0x0F][curr_quad];
        uint8_t result = s_enc_state & 0x30;
        if(result == 0x10)      { g_enc_pos++; g_last_inc = 1;  }
        else if(result == 0x20) { g_enc_pos--; g_last_inc = -1; }
    }

    // Encoder Click
    g_enc_click.Debounce();
    if(g_enc_click.RisingEdge())       EnqueueEvent(14, true);
    else if(g_enc_click.FallingEdge()) EnqueueEvent(14, false);

    // Note Keys
    for(size_t i = 0; i < gamma_pins::note_keys::COUNT; i++)
    {
        g_note_keys[i].Debounce();
        if(g_note_keys[i].RisingEdge())       EnqueueEvent(i, true);
        else if(g_note_keys[i].FallingEdge()) EnqueueEvent(i, false);
    }

    // Chord Keys
    for(size_t i = 0; i < gamma_pins::chord_keys::COUNT; i++)
    {
        g_chord_keys[i].Debounce();
        if(g_chord_keys[i].RisingEdge())       EnqueueEvent(7 + i, true);
        else if(g_chord_keys[i].FallingEdge()) EnqueueEvent(7 + i, false);
    }
}

int main(void)
{
    hw.Init();
    // Gamma USB-C is on the Seed's external USB port: use Logger<LOGGER_EXTERNAL>, not hw.StartLog()
    // (declare once at file scope: using UsbLog = Logger<LOGGER_EXTERNAL>;)
    UsbLog::StartLog(false);

    // 1. Speaker Amplifier Enable (PC3) - Hold LOW at startup to prevent pop
    GPIO spk_en;
    spk_en.Init(gamma_pins::system_pins::pin_speaker_en, GPIO::Mode::OUTPUT, GPIO::Pull::NOPULL);
    spk_en.Write(false); // Held LOW during hardware init; drive HIGH after hw.StartAudio()

    // 2. Display Init (with bus reset)
    __HAL_RCC_I2C1_FORCE_RESET();
    System::Delay(2);
    __HAL_RCC_I2C1_RELEASE_RESET();

    __HAL_RCC_GPIOB_CLK_ENABLE();
    GPIO_InitTypeDef gpio_init;
    gpio_init.Mode      = GPIO_MODE_AF_OD;
    gpio_init.Pull      = GPIO_PULLUP;
    gpio_init.Speed     = GPIO_SPEED_FREQ_HIGH;
    gpio_init.Alternate = GPIO_AF4_I2C1;
    gpio_init.Pin       = (1U << gamma_pins::display::pin_scl.pin) | (1U << gamma_pins::display::pin_sda.pin);
    HAL_GPIO_Init(GPIOB, &gpio_init);

    MyOled::Config disp_cfg;
    disp_cfg.driver_config.transport_config.i2c_address               = gamma_pins::display::i2c_address;
    disp_cfg.driver_config.transport_config.i2c_config.periph         = I2CHandle::Config::Peripheral::I2C_1;
    disp_cfg.driver_config.transport_config.i2c_config.speed          = I2CHandle::Config::Speed::I2C_1MHZ;
    disp_cfg.driver_config.transport_config.i2c_config.mode           = I2CHandle::Config::Mode::I2C_MASTER;
    disp_cfg.driver_config.transport_config.i2c_config.pin_config.scl = gamma_pins::display::pin_scl;
    disp_cfg.driver_config.transport_config.i2c_config.pin_config.sda = gamma_pins::display::pin_sda;
    oled.Init(disp_cfg);

    // 3. ADC Init
    constexpr size_t num_adc = gamma_pins::knobs::COUNT + gamma_pins::joysticks::COUNT + 1;
    AdcChannelConfig adc_cfg[num_adc];
    for(size_t i = 0; i < gamma_pins::knobs::COUNT; i++)
        adc_cfg[i].InitSingle(gamma_pins::knobs::pins[i]);
    for(size_t i = 0; i < gamma_pins::joysticks::COUNT; i++)
        adc_cfg[gamma_pins::knobs::COUNT + i].InitSingle(gamma_pins::joysticks::pins[i]);
    adc_cfg[gamma_pins::knobs::COUNT + gamma_pins::joysticks::COUNT].InitSingle(gamma_pins::aux_adc::pin);
    hw.adc.Init(adc_cfg, num_adc);
    hw.adc.Start();

    // 4. Keys, Encoder & Timer Init
    for(size_t i = 0; i < gamma_pins::note_keys::COUNT; i++)
        g_note_keys[i].Init(gamma_pins::note_keys::pins[i], 1000.0f);
    for(size_t i = 0; i < gamma_pins::chord_keys::COUNT; i++)
        g_chord_keys[i].Init(gamma_pins::chord_keys::pins[i], 1000.0f);
    g_enc_click.Init(gamma_pins::encoder::pin_click, 1000.0f);
    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);

    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();

    uint32_t last_screen_time = System::GetNow();
    int32_t  last_enc_pos     = 0;

    while(1)
    {
        uint32_t now = System::GetNow();

        // Drain key events
        while(g_eq_tail != g_eq_head)
        {
            KeyEvent ev = g_event_queue[g_eq_tail];
            g_eq_tail   = (g_eq_tail + 1) % kEventQueueSize;
            // Handle ev.id (0..6 note, 7..13 chord, 14 enc click) and ev.pressed
        }

        // Encoder delta
        int32_t cur_pos = g_enc_pos;
        if(cur_pos != last_enc_pos)
        {
            int32_t delta = cur_pos - last_enc_pos;
            last_enc_pos = cur_pos;
            // Handle delta (+1 CW, -1 CCW)
        }

        // Read Knobs
        for(size_t i = 0; i < gamma_pins::knobs::COUNT; i++)
        {
            float raw = hw.adc.GetFloat(i);
            if(gamma_pins::knobs::invert[i]) raw = 1.0f - raw;
            raw = fmaxf(0.0f, fminf(1.0f, raw));
            if(fabsf(raw - g_knobs[i]) > 0.005f) g_knobs[i] = raw;
        }

        // Read Joysticks
        for(size_t i = 0; i < gamma_pins::joysticks::COUNT; i++)
        {
            float raw = hw.adc.GetFloat(gamma_pins::knobs::COUNT + i);
            if(gamma_pins::joysticks::invert[i]) raw = 1.0f - raw;
            raw = fmaxf(0.0f, fminf(1.0f, raw));
            g_sticks[i] += gamma_pins::joysticks::iir_coefficient * (raw - g_sticks[i]);
        }

        // Display refresh (~33 Hz)
        if(now - last_screen_time >= 30)
        {
            last_screen_time = now;
            oled.Fill(false);
            oled.SetCursor(0, 0);
            oled.WriteString("GAMMA SYNTH", Font_6x8, true);
            oled.Update();
        }

        System::Delay(1);
    }
}