/**
* Mixer Test Firmware - RP2040 (Pico SDK)
*
* Tests all hardware components:
* - MCP3208 ADC (4 slide potentiometers) -> Serial output
* - SSD1306 OLED 128x32 (vertical orientation) -> Button/encoder states
* - KY-040 Rotary Encoder -> Direction + button on display
* - 2 Push buttons (A, B) -> State on display
* - WS2812 LED strips (22 LEDs total):
* - ledStatus (2 LEDs): blue blink + white on encoder press
* - vu_Left (10 LEDs): VU meter simulation
* - vu_Right (10 LEDs): VU meter simulation
*
* Pin mapping:
* GP0 = I2C0 SDA (OLED)
* GP1 = I2C0 SCL (OLED)
* GP2 = SPI CLK (MCP3208)
* GP3 = SPI MOSI (MCP3208 Din)
* GP4 = SPI MISO (MCP3208 Dout)
* GP5 = SPI CS (MCP3208 nCS)
* GP6 = Encoder CLK
* GP7 = Encoder DT
* GP8 = Encoder SW (button)
* GP9 = Button A
* GP10 = Button B
* GP11 = WS2812 DIN (22 LEDs chain)
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "pico/stdlib.h"
#include "hardware/spi.h"
#include "hardware/i2c.h"
#include "hardware/gpio.h"
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "ws2812.pio.h"
#include "ssd1306.h"
#include <stdlib.h>
// ============================================================
// Pin Definitions
// ============================================================
// I2C (OLED)
#define PIN_SDA 0
#define PIN_SCL 1
// SPI (MCP3208)
#define PIN_SPI_CLK 2
#define PIN_SPI_MOSI 3
#define PIN_SPI_MISO 4
#define PIN_SPI_CS 5
// Encoder
#define PIN_ENC_CLK 6
#define PIN_ENC_DT 7
#define PIN_ENC_SW 8
// Buttons
#define PIN_BTN_A 9
#define PIN_BTN_B 10
// WS2812
#define PIN_WS2812 11
// ============================================================
// Constants
// ============================================================
#define NUM_LEDS_TOTAL 22
#define NUM_LEDS_STATUS 2
#define NUM_LEDS_VU 10
#define MCP3208_NUM_CH 4 // we only use CH0-CH3
// VU meter simulation speed
#define VU_UPDATE_MS 30
// blink interval for status LED
#define BLINK_INTERVAL_MS 500
// display refresh interval
#define DISPLAY_UPDATE_MS 50
// ADC read interval
#define ADC_READ_MS 100
// ============================================================
// Global State
// ============================================================
// LED pixel buffer (GRB format for WS2812)
static uint32_t led_pixels[NUM_LEDS_TOTAL];
// MCP3208 readings (12-bit, 0-4095)
static uint16_t adc_values[MCP3208_NUM_CH] = {0};
// encoder state
static int encoder_position = 0;
static int encoder_direction = 0; // -1=CCW, 0=none, 1=CW
static bool encoder_btn_pressed = false;
static uint8_t last_enc_clk = 1;
// button states (active low - pulled to GND)
static bool btn_a_pressed = false;
static bool btn_b_pressed = false;
// status LED blink
static bool status_led_on = false;
// VU meter levels (0.0 to 1.0)
static float vu_level_left = 0.0f;
static float vu_level_right = 0.0f;
static float vu_target_left = 0.0f;
static float vu_target_right = 0.0f;
// OLED display
static ssd1306_t display;
// PIO for WS2812
static PIO pio_ws = pio0;
static uint sm_ws = 0;
// ============================================================
// WS2812 LED Functions
// ============================================================
static inline uint32_t urgb_u32(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)(r) << 8) |
((uint32_t)(g) << 16) |
((uint32_t)(b) << 0);
}
static inline void put_pixel(uint32_t pixel_grb) {
pio_sm_put_blocking(pio_ws, sm_ws, pixel_grb << 8u);
}
static void ws2812_show(void) {
for (int i = 0; i < NUM_LEDS_TOTAL; i++) {
put_pixel(led_pixels[i]);
}
}
static void ws2812_clear_all(void) {
memset(led_pixels, 0, sizeof(led_pixels));
ws2812_show();
}
// set a VU LED color based on position (0-9)
// 0-4 = green, 5-7 = yellow, 8-9 = red
static uint32_t vu_color_for_index(int idx, bool lit) {
if (!lit) return urgb_u32(0, 0, 0);
if (idx < 5) {
// green LEDs (bright green)
return urgb_u32(0, 180, 0);
} else if (idx < 8) {
// yellow LEDs
return urgb_u32(180, 160, 0);
} else {
// red LEDs
return urgb_u32(200, 0, 0);
}
}
// ============================================================
// MCP3208 SPI Functions
// ============================================================
static void mcp3208_init(void) {
spi_init(spi0, 1000000); // 1 MHz SPI clock
gpio_set_function(PIN_SPI_CLK, GPIO_FUNC_SPI);
gpio_set_function(PIN_SPI_MOSI, GPIO_FUNC_SPI);
gpio_set_function(PIN_SPI_MISO, GPIO_FUNC_SPI);
// CS pin as GPIO output (active low)
gpio_init(PIN_SPI_CS);
gpio_set_dir(PIN_SPI_CS, GPIO_OUT);
gpio_put(PIN_SPI_CS, 1); // deselected
}
static uint16_t mcp3208_read(uint8_t channel) {
// MCP3208 single-ended read via Wokwi custom chip
// the custom chip state machine processes 1 byte at a time:
// Byte 0 in: start + config bits -> chip returns dummy
// Byte 1 in: more config bits -> chip returns 0x08
// Byte 2 in: dummy -> chip returns MSB nibble
// Byte 3 in: dummy -> chip returns LSB byte
// so we need 4 bytes to get the full 12-bit result.
uint8_t tx[4], rx[4];
tx[0] = 0x06 | ((channel & 0x04) >> 2); // start + SGL + D2
tx[1] = (channel & 0x03) << 6; // D1, D0
tx[2] = 0x00; // clock out MSB
tx[3] = 0x00; // clock out LSB
gpio_put(PIN_SPI_CS, 0);
spi_write_read_blocking(spi0, tx, rx, 4);
gpio_put(PIN_SPI_CS, 1);
// result: MSB nibble in rx[2], LSB byte in rx[3]
uint16_t result = ((rx[2] & 0x0F) << 8) | rx[3];
return result;
}
static void read_all_adc(void) {
for (int ch = 0; ch < MCP3208_NUM_CH; ch++) {
adc_values[ch] = mcp3208_read(ch);
}
}
static void print_adc_values(void) {
printf("ADC | CH0: %4d | CH1: %4d | CH2: %4d | CH3: %4d\n",
adc_values[0], adc_values[1], adc_values[2], adc_values[3]);
}
// ============================================================
// Encoder Functions
// ============================================================
static void encoder_init(void) {
gpio_init(PIN_ENC_CLK);
gpio_set_dir(PIN_ENC_CLK, GPIO_IN);
gpio_pull_up(PIN_ENC_CLK);
gpio_init(PIN_ENC_DT);
gpio_set_dir(PIN_ENC_DT, GPIO_IN);
gpio_pull_up(PIN_ENC_DT);
gpio_init(PIN_ENC_SW);
gpio_set_dir(PIN_ENC_SW, GPIO_IN);
gpio_pull_up(PIN_ENC_SW);
last_enc_clk = gpio_get(PIN_ENC_CLK);
}
static void encoder_update(void) {
uint8_t clk = gpio_get(PIN_ENC_CLK);
uint8_t dt = gpio_get(PIN_ENC_DT);
// detect falling edge on CLK
if (clk != last_enc_clk && clk == 0) {
if (dt != clk) {
encoder_position++;
encoder_direction = 1; // CW
} else {
encoder_position--;
encoder_direction = -1; // CCW
}
}
last_enc_clk = clk;
// encoder button (active low)
encoder_btn_pressed = !gpio_get(PIN_ENC_SW);
}
// ============================================================
// Button Functions
// ============================================================
static void buttons_init(void) {
gpio_init(PIN_BTN_A);
gpio_set_dir(PIN_BTN_A, GPIO_IN);
gpio_pull_up(PIN_BTN_A);
gpio_init(PIN_BTN_B);
gpio_set_dir(PIN_BTN_B, GPIO_IN);
gpio_pull_up(PIN_BTN_B);
}
static void buttons_update(void) {
btn_a_pressed = !gpio_get(PIN_BTN_A);
btn_b_pressed = !gpio_get(PIN_BTN_B);
}
// ============================================================
// VU Meter Simulation (random values)
// ============================================================
static uint32_t vu_rng_state = 12345;
static float vu_random(void) {
// simple xorshift PRNG returning 0.0 to 1.0
vu_rng_state ^= vu_rng_state << 13;
vu_rng_state ^= vu_rng_state >> 17;
vu_rng_state ^= vu_rng_state << 5;
return (float)(vu_rng_state % 1000) / 1000.0f;
}
static void vu_meter_update(void) {
// random VU targets (simulating audio signal)
vu_target_left = vu_random();
vu_target_right = vu_random();
// smooth approach (fast attack, slow decay like real VU)
float attack = 0.4f;
float decay = 0.08f;
if (vu_target_left > vu_level_left)
vu_level_left += (vu_target_left - vu_level_left) * attack;
else
vu_level_left += (vu_target_left - vu_level_left) * decay;
if (vu_target_right > vu_level_right)
vu_level_right += (vu_target_right - vu_level_right) * attack;
else
vu_level_right += (vu_target_right - vu_level_right) * decay;
}
// ============================================================
// LED Strip Update
// ============================================================
static void update_leds(bool blink_state) {
// --- LED 0: Blue blink (status power indicator) ---
if (blink_state) {
led_pixels[0] = urgb_u32(0, 0, 120); // Blue
} else {
led_pixels[0] = urgb_u32(0, 0, 0); // Off
}
// --- LED 1: White when encoder button pressed ---
if (encoder_btn_pressed) {
led_pixels[1] = urgb_u32(120, 120, 120); // White
} else {
led_pixels[1] = urgb_u32(0, 0, 0); // Off
}
// --- LEDs 2-11: VU Left (10 LEDs) ---
int num_lit_left = (int)(vu_level_left * 10.0f + 0.5f);
if (num_lit_left > 10) num_lit_left = 10;
for (int i = 0; i < NUM_LEDS_VU; i++) {
led_pixels[NUM_LEDS_STATUS + i] = vu_color_for_index(i, i < num_lit_left);
}
// --- LEDs 12-21: VU Right (10 LEDs) ---
int num_lit_right = (int)(vu_level_right * 10.0f + 0.5f);
if (num_lit_right > 10) num_lit_right = 10;
for (int i = 0; i < NUM_LEDS_VU; i++) {
led_pixels[NUM_LEDS_STATUS + NUM_LEDS_VU + i] = vu_color_for_index(i, i < num_lit_right);
}
ws2812_show();
}
// ============================================================
// Display Update
// ============================================================
static void update_display(void) {
ssd1306_clear(&display);
// logical space: 32 cols × 128 rows (vertical display)
// horizontal strings fit ~5 chars (32px width)
// rows advance top-to-bottom along 128px height
char line[8];
// title
ssd1306_string_h(&display, 0, 0, "MIXER");
// buttons
snprintf(line, sizeof(line), "A:%s", btn_a_pressed ? "ON" : "--");
ssd1306_string_h(&display, 0, 10, line);
snprintf(line, sizeof(line), "B:%s", btn_b_pressed ? "ON" : "--");
ssd1306_string_h(&display, 0, 19, line);
// encoder button
snprintf(line, sizeof(line), "SW:%c", encoder_btn_pressed ? '*' : '.');
ssd1306_string_h(&display, 0, 28, line);
// separator line
for (int r = 37; r < 39; r++)
for (int c = 0; c < OLED_W; c++)
ssd1306_pixel(&display, c, r, true);
// encoder direction
const char *dir_str = "---";
if (encoder_direction > 0) dir_str = "CW ";
else if (encoder_direction < 0) dir_str = "CCW";
snprintf(line, sizeof(line), "E:%s", dir_str);
ssd1306_string_h(&display, 0, 41, line);
snprintf(line, sizeof(line), "P:%d", encoder_position);
ssd1306_string_h(&display, 0, 50, line);
// separator line
for (int r = 59; r < 61; r++)
for (int c = 0; c < OLED_W; c++)
ssd1306_pixel(&display, c, r, true);
// fader labels (centered above each bar)
ssd1306_char(&display, 2, 63, '1');
ssd1306_char(&display, 10, 63, '2');
ssd1306_char(&display, 18, 63, '3');
ssd1306_char(&display, 26, 63, '4');
// 4 vertical bars showing slider values
// each bar: 6px wide, 50px tall, 2px gap between
// cols: 1-6, 9-14, 17-22, 25-30
#define BAR_W 6
#define BAR_H 50
#define BAR_ROW 73
ssd1306_vbar(&display, 1, BAR_W, BAR_ROW, BAR_H, adc_values[0], 4095);
ssd1306_vbar(&display, 9, BAR_W, BAR_ROW, BAR_H, adc_values[1], 4095);
ssd1306_vbar(&display, 17, BAR_W, BAR_ROW, BAR_H, adc_values[2], 4095);
ssd1306_vbar(&display, 25, BAR_W, BAR_ROW, BAR_H, adc_values[3], 4095);
ssd1306_update(&display);
}
// ============================================================
// Main
// ============================================================
int main(void) {
// init stdio (USB serial)
stdio_init_all();
// wait a bit for USB to enumerate
sleep_ms(1000);
printf("\n===========================\n");
printf(" Mixer Test - RP2040\n");
printf("===========================\n\n");
// ----- I2C Init (OLED) -----
i2c_init(i2c0, 400000); // 400 kHz
gpio_set_function(PIN_SDA, GPIO_FUNC_I2C);
gpio_set_function(PIN_SCL, GPIO_FUNC_I2C);
gpio_pull_up(PIN_SDA);
gpio_pull_up(PIN_SCL);
ssd1306_init(&display, i2c0);
ssd1306_clear(&display);
ssd1306_string_h(&display, 1, 10, "MIXER");
ssd1306_string_h(&display, 0, 20, "TEST");
ssd1306_update(&display);
sleep_ms(1000);
// ----- SPI Init (MCP3208) -----
mcp3208_init();
printf("[OK] MCP3208 ADC initialized (SPI0)\n");
// ----- Encoder Init -----
encoder_init();
printf("[OK] Encoder initialized (GP6/GP7/GP8)\n");
// ----- Buttons Init -----
buttons_init();
printf("[OK] Buttons initialized (GP9/GP10)\n");
// ----- WS2812 Init (PIO) -----
uint offset = pio_add_program(pio_ws, &ws2812_program);
ws2812_program_init(pio_ws, sm_ws, offset, PIN_WS2812, 800000, false);
ws2812_clear_all();
printf("[OK] WS2812 LEDs initialized (GP11, %d LEDs)\n", NUM_LEDS_TOTAL);
printf("\n--- Starting main loop ---\n\n");
// timing trackers
absolute_time_t last_blink = get_absolute_time();
absolute_time_t last_vu = get_absolute_time();
absolute_time_t last_display = get_absolute_time();
absolute_time_t last_adc = get_absolute_time();
// direction display timeout
absolute_time_t last_enc_event = get_absolute_time();
while (true) {
absolute_time_t now = get_absolute_time();
// ----- Read Encoder (poll frequently) -----
encoder_update();
// clear direction indicator after 500ms of no activity
if (encoder_direction != 0 &&
absolute_time_diff_us(last_enc_event, now) > 500000) {
encoder_direction = 0;
}
if (encoder_direction != 0) {
last_enc_event = now;
}
// ----- Read Buttons -----
buttons_update();
// ----- Read ADC periodically -----
if (absolute_time_diff_us(last_adc, now) >= ADC_READ_MS * 1000) {
last_adc = now;
read_all_adc();
print_adc_values();
}
// ----- Update VU meter -----
if (absolute_time_diff_us(last_vu, now) >= VU_UPDATE_MS * 1000) {
last_vu = now;
vu_meter_update();
}
// ----- Blink Status LED -----
if (absolute_time_diff_us(last_blink, now) >= BLINK_INTERVAL_MS * 1000) {
last_blink = now;
status_led_on = !status_led_on;
}
// ----- Update LEDs -----
update_leds(status_led_on);
// ----- Update Display -----
if (absolute_time_diff_us(last_display, now) >= DISPLAY_UPDATE_MS * 1000) {
last_display = now;
update_display();
}
sleep_ms(1);
}
return 0;
}