|
| 1 | +/* |
| 2 | + * The MIT License (MIT) |
| 3 | + * |
| 4 | + * Copyright (c) 2026 Saulo Verissimo |
| 5 | + * |
| 6 | + * Permission is hereby granted, free of charge, to any person obtaining a copy |
| 7 | + * of this software and associated documentation files (the "Software"), to deal |
| 8 | + * in the Software without restriction, including without limitation the rights |
| 9 | + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
| 10 | + * copies of the Software, and to permit persons to whom the Software is |
| 11 | + * furnished to do so, subject to the following conditions: |
| 12 | + * |
| 13 | + * The above copyright notice and this permission notice shall be included in |
| 14 | + * all copies or substantial portions of the Software. |
| 15 | + * |
| 16 | + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| 17 | + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| 18 | + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
| 19 | + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| 20 | + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 21 | + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
| 22 | + * THE SOFTWARE. |
| 23 | + */ |
| 24 | + |
| 25 | +// SSD1306 OLED display driver (128x64, I2C) for MIDI 2.0 Host example. |
| 26 | +// Adafruit Feather RP2040 USB Host: I2C1 via STEMMA QT (SDA = GP2, SCL = GP3) |
| 27 | +// |
| 28 | +// Three display phases: |
| 29 | +// 1. Splash : title + credits (shown during init) |
| 30 | +// 2. Connecting : spinner animation while waiting for device |
| 31 | +// 3. Live : header + 6 scrolling log lines + status bar |
| 32 | + |
| 33 | +#include "display.h" |
| 34 | +#include <stdio.h> |
| 35 | +#include <string.h> |
| 36 | +#include "pico/stdlib.h" |
| 37 | +#include "hardware/i2c.h" |
| 38 | +#include "hardware/gpio.h" |
| 39 | + |
| 40 | +#define I2C_PORT i2c1 |
| 41 | +#define I2C_SDA 2 |
| 42 | +#define I2C_SCL 3 |
| 43 | +#define I2C_FREQ 400000 |
| 44 | +#define SSD1306_ADDR 0x3C |
| 45 | + |
| 46 | +#define SCR_W 128 |
| 47 | +#define SCR_H 64 |
| 48 | +#define PAGES (SCR_H / 8) |
| 49 | +#define CHARS_PER_LINE 21 |
| 50 | + |
| 51 | +//--------------------------------------------------------------------+ |
| 52 | +// Framebuffer |
| 53 | +//--------------------------------------------------------------------+ |
| 54 | + |
| 55 | +static uint8_t fb[SCR_W * PAGES]; |
| 56 | + |
| 57 | +//--------------------------------------------------------------------+ |
| 58 | +// Log buffer (6 lines for live view) |
| 59 | +//--------------------------------------------------------------------+ |
| 60 | + |
| 61 | +#define LOG_LINES 6 |
| 62 | +static char log_lines[LOG_LINES][CHARS_PER_LINE + 1]; |
| 63 | +static int log_count = 0; |
| 64 | + |
| 65 | +//--------------------------------------------------------------------+ |
| 66 | +// Minimal 5x7 font (ASCII 32-126) |
| 67 | +//--------------------------------------------------------------------+ |
| 68 | + |
| 69 | +#include "font5x7.h" |
| 70 | + |
| 71 | +//--------------------------------------------------------------------+ |
| 72 | +// SSD1306 I2C low-level |
| 73 | +//--------------------------------------------------------------------+ |
| 74 | + |
| 75 | +static void ssd_cmd(uint8_t cmd) { |
| 76 | + uint8_t buf[2] = { 0x00, cmd }; |
| 77 | + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, 2, false); |
| 78 | +} |
| 79 | + |
| 80 | +static void ssd_data(const uint8_t* data, size_t len) { |
| 81 | + uint8_t buf[SCR_W + 1]; |
| 82 | + buf[0] = 0x40; |
| 83 | + size_t chunk = (len > SCR_W) ? SCR_W : len; |
| 84 | + memcpy(buf + 1, data, chunk); |
| 85 | + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, chunk + 1, false); |
| 86 | +} |
| 87 | + |
| 88 | +static void ssd_flush(void) { |
| 89 | + ssd_cmd(0x21); ssd_cmd(0); ssd_cmd(127); |
| 90 | + ssd_cmd(0x22); ssd_cmd(0); ssd_cmd(7); |
| 91 | + for (int page = 0; page < PAGES; page++) { |
| 92 | + ssd_data(&fb[page * SCR_W], SCR_W); |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +//--------------------------------------------------------------------+ |
| 97 | +// Framebuffer drawing |
| 98 | +//--------------------------------------------------------------------+ |
| 99 | + |
| 100 | +static void fb_clear(void) { |
| 101 | + memset(fb, 0, sizeof(fb)); |
| 102 | +} |
| 103 | + |
| 104 | +static void fb_char(int x, int y, char c) { |
| 105 | + if (c < 32 || c > 126) c = '?'; |
| 106 | + const uint8_t* glyph = font5x7 + (c - 32) * 5; |
| 107 | + int page = y / 8; |
| 108 | + int bit_offset = y % 8; |
| 109 | + |
| 110 | + if (page >= PAGES || x + 5 > SCR_W) return; |
| 111 | + |
| 112 | + for (int col = 0; col < 5; col++) { |
| 113 | + uint8_t column_data = glyph[col]; |
| 114 | + fb[(page * SCR_W) + x + col] |= (uint8_t)(column_data << bit_offset); |
| 115 | + if (bit_offset > 0 && page + 1 < PAGES) { |
| 116 | + fb[((page + 1) * SCR_W) + x + col] |= (uint8_t)(column_data >> (8 - bit_offset)); |
| 117 | + } |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +static void fb_string(int x, int y, const char* str) { |
| 122 | + while (*str) { |
| 123 | + fb_char(x, y, *str); |
| 124 | + x += 6; |
| 125 | + if (x + 6 > SCR_W) break; |
| 126 | + str++; |
| 127 | + } |
| 128 | +} |
| 129 | + |
| 130 | +// Draw a horizontal line (1px) |
| 131 | +static void fb_hline(int x0, int x1, int y) { |
| 132 | + int page = y / 8; |
| 133 | + uint8_t mask = (uint8_t)(1 << (y % 8)); |
| 134 | + if (page >= PAGES) return; |
| 135 | + for (int x = x0; x <= x1 && x < SCR_W; x++) { |
| 136 | + fb[page * SCR_W + x] |= mask; |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +//--------------------------------------------------------------------+ |
| 141 | +// Phase 1: Splash |
| 142 | +//--------------------------------------------------------------------+ |
| 143 | + |
| 144 | +void display_init(void) { |
| 145 | + i2c_init(I2C_PORT, I2C_FREQ); |
| 146 | + gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); |
| 147 | + gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); |
| 148 | + gpio_pull_up(I2C_SDA); |
| 149 | + gpio_pull_up(I2C_SCL); |
| 150 | + |
| 151 | + sleep_ms(100); |
| 152 | + |
| 153 | + // SSD1306 init sequence |
| 154 | + ssd_cmd(0xAE); |
| 155 | + ssd_cmd(0xD5); ssd_cmd(0x80); |
| 156 | + ssd_cmd(0xA8); ssd_cmd(0x3F); |
| 157 | + ssd_cmd(0xD3); ssd_cmd(0x00); |
| 158 | + ssd_cmd(0x40); |
| 159 | + ssd_cmd(0x8D); ssd_cmd(0x14); |
| 160 | + ssd_cmd(0x20); ssd_cmd(0x00); |
| 161 | + ssd_cmd(0xA1); |
| 162 | + ssd_cmd(0xC8); |
| 163 | + ssd_cmd(0xDA); ssd_cmd(0x12); |
| 164 | + ssd_cmd(0x81); ssd_cmd(0xCF); |
| 165 | + ssd_cmd(0xD9); ssd_cmd(0xF1); |
| 166 | + ssd_cmd(0xDB); ssd_cmd(0x40); |
| 167 | + ssd_cmd(0xA4); |
| 168 | + ssd_cmd(0xA6); |
| 169 | + ssd_cmd(0xAF); |
| 170 | + |
| 171 | + // Splash screen |
| 172 | + fb_clear(); |
| 173 | + fb_string(4, 8, "TinyUSB MIDI 2.0"); |
| 174 | + fb_hline(4, 123, 18); |
| 175 | + fb_string(16, 24, "USB Host Demo"); |
| 176 | + fb_string(4, 40, "Feather RP2040"); |
| 177 | + fb_string(4, 52, "PIO-USB + SSD1306"); |
| 178 | + ssd_flush(); |
| 179 | +} |
| 180 | + |
| 181 | +//--------------------------------------------------------------------+ |
| 182 | +// Phase 2: Connecting (spinner) |
| 183 | +//--------------------------------------------------------------------+ |
| 184 | + |
| 185 | +static const char SPINNER[] = "|/-\\"; |
| 186 | + |
| 187 | +void display_connecting(uint32_t elapsed_ms) { |
| 188 | + int idx = (int)((elapsed_ms / 200) % 4); |
| 189 | + |
| 190 | + // Clear bottom 2 pages for spinner area |
| 191 | + memset(&fb[6 * SCR_W], 0, SCR_W); |
| 192 | + memset(&fb[7 * SCR_W], 0, SCR_W); |
| 193 | + |
| 194 | + char line[CHARS_PER_LINE + 1]; |
| 195 | + snprintf(line, sizeof(line), "%c Waiting device...", SPINNER[idx]); |
| 196 | + fb_string(4, 52, line); |
| 197 | + ssd_flush(); |
| 198 | +} |
| 199 | + |
| 200 | +//--------------------------------------------------------------------+ |
| 201 | +// Phase 3: Live view |
| 202 | +//--------------------------------------------------------------------+ |
| 203 | + |
| 204 | +// Layout: |
| 205 | +// y=0 : "TinyUSB MIDI 2.0 Host" (header, fixed) |
| 206 | +// y=9 : separator line |
| 207 | +// y=10 : log line 0 |
| 208 | +// y=18 : log line 1 |
| 209 | +// y=26 : log line 2 |
| 210 | +// y=34 : log line 3 |
| 211 | +// y=42 : log line 4 |
| 212 | +// y=50 : log line 5 |
| 213 | +// y=57 : separator line |
| 214 | +// y=58 : status bar |
| 215 | + |
| 216 | +static bool live_mode = false; |
| 217 | + |
| 218 | +static void draw_live_frame(void) { |
| 219 | + fb_clear(); |
| 220 | + fb_string(0, 0, "TinyUSB MIDI 2.0 Host"); |
| 221 | + fb_hline(0, 127, 9); |
| 222 | + fb_hline(0, 127, 57); |
| 223 | +} |
| 224 | + |
| 225 | +void display_live_begin(void) { |
| 226 | + live_mode = true; |
| 227 | + log_count = 0; |
| 228 | + memset(log_lines, 0, sizeof(log_lines)); |
| 229 | + |
| 230 | + draw_live_frame(); |
| 231 | + fb_string(0, 58, "Connected"); |
| 232 | + ssd_flush(); |
| 233 | +} |
| 234 | + |
| 235 | +void display_log(const char* text, uint16_t color) { |
| 236 | + (void)color; |
| 237 | + |
| 238 | + if (!live_mode) { |
| 239 | + display_live_begin(); |
| 240 | + } |
| 241 | + |
| 242 | + // Scroll up if full |
| 243 | + if (log_count >= LOG_LINES) { |
| 244 | + for (int i = 0; i < LOG_LINES - 1; i++) { |
| 245 | + strncpy(log_lines[i], log_lines[i + 1], CHARS_PER_LINE); |
| 246 | + log_lines[i][CHARS_PER_LINE] = '\0'; |
| 247 | + } |
| 248 | + log_count = LOG_LINES - 1; |
| 249 | + } |
| 250 | + |
| 251 | + strncpy(log_lines[log_count], text, CHARS_PER_LINE); |
| 252 | + log_lines[log_count][CHARS_PER_LINE] = '\0'; |
| 253 | + log_count++; |
| 254 | + |
| 255 | + // Redraw: header + log + status |
| 256 | + draw_live_frame(); |
| 257 | + for (int i = 0; i < log_count && i < LOG_LINES; i++) { |
| 258 | + fb_string(0, 10 + i * 8, log_lines[i]); |
| 259 | + } |
| 260 | + ssd_flush(); |
| 261 | +} |
| 262 | + |
| 263 | +void display_status(const char* text) { |
| 264 | + if (!live_mode) return; |
| 265 | + |
| 266 | + // Clear status area (page 7) |
| 267 | + memset(&fb[7 * SCR_W], 0, SCR_W); |
| 268 | + // Redraw separator (might have been cleared) |
| 269 | + fb_hline(0, 127, 57); |
| 270 | + |
| 271 | + char status[CHARS_PER_LINE + 1]; |
| 272 | + strncpy(status, text, CHARS_PER_LINE); |
| 273 | + status[CHARS_PER_LINE] = '\0'; |
| 274 | + fb_string(0, 58, status); |
| 275 | + ssd_flush(); |
| 276 | +} |
0 commit comments