diff --git a/README.md b/README.md new file mode 100644 index 0000000..55d8a4e --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +# Hirnfrei Floppinator + +USB-Floppy-Emulator auf Basis eines Raspberry Pi Pico. Zeigt `.img`- und +`.adf`-Dateien von einer SD-Karte als USB-Massenspeicher an, Auswahl über +Display + 3 Taster, Oberfläche mit LVGL. + +## Features + +- Erscheint am Host als normales USB-Massenspeichergerät (kein Treiber nötig) +- Unterstützt `.img` (PC/generisch) und `.adf` (Amiga) — Kapazität wird + automatisch aus der Dateigröße ermittelt, kein festes Format nötig +- 160×128-TFT-Display mit LVGL-Oberfläche (Liste, Statuszeile, Aktivitätsanzeige) +- Alphabetisch sortierte Image-Liste +- Manuelles Neuladen der Liste per Tastenkombination (z.B. nach SD-Kartenwechsel) +- Aktivitäts-LED (Onboard-LED) leuchtet bei Lese-/Schreibzugriffen +- Debug-Ausgaben über USB-Seriell, zentral abschaltbar + +## Hardware + +- Raspberry Pi Pico (RP2040) + ([Berrybase Pico Light Starter Kit](https://www.berrybase.de/raspberry-pi-pico-light-starter-kit-bestehend-aus-pico-offiziellem-kabel-und-stiftleisten)) +- 1,8" SPI-TFT-Display, 160×128, ST7735S-Controller + ([Berrybase 1,8" LCD-Anzeigemodul](https://www.berrybase.de/1-8-zoll-lcd-anzeigemodul-fuer-raspberry-pi-pico-65k-farben-160x128-spi)) +- Micro-SD-Kartenmodul, SPI, 3,3V (6-Pin: 3V3/CS/MOSI/CLK/MISO/GND) + ([Berrybase Micro-SD-Card-Reader-Modul, kompakt](https://www.berrybase.de/micro-sd-card-reader-modul-mit-spi-schnittstelle-kompakt)) +- 3× Kurzhubtaster, 6×6mm, vertikale Printmontage, H=5,0mm + ([Berrybase Kurzhubtaster](https://www.berrybase.de/kurzhubtaster-vertikale-printmontage-6x6mm-h-5-0mm)) +- microSD-Karte (FAT16/FAT32 formatiert) + +### Schaltplan / Verkabelung + +``` + Raspberry Pi Pico + ┌──────────────────┐ + TFT-Display │ │ + ────────── │ │ + CS ───────────────┤ GP9 │ + DC ───────────────┤ GP8 │ + RST ───────────────┤ GP12 │ + BL ───────────────┤ GP13 │ + SCK ───────────────┤ GP10 (SPI1 SCK) │ + MOSI ───────────────┤ GP11 (SPI1 TX) │ + MISO ───────────────┤ GP28 (SPI1 RX) │ + VCC ───────────────┤ 3V3(OUT) │ + GND ───────────────┤ GND │ + │ │ + SD-Kartenmodul │ │ + ────────────── │ │ + CS ───────────────┤ GP5 │ + MOSI ───────────────┤ GP7 (SPI0 TX) │ + CLK ───────────────┤ GP6 (SPI0 SCK) │ + MISO ───────────────┤ GP4 (SPI0 RX) │ + 3V3 ───────────────┤ 3V3(OUT) │ + GND ───────────────┤ GND │ + │ │ + Taster │ │ + ────── │ │ + Zurück ───────────────┤ GP14 (INPUT_PULLUP, gegen GND) │ + Weiter ───────────────┤ GP15 (INPUT_PULLUP, gegen GND) │ + Auswahl ──────────────┤ GP20 (INPUT_PULLUP, gegen GND) │ + │ │ + Aktivitäts-LED │ Onboard-LED │ + (keine Verkabelung │ (LED_BUILTIN) │ + nötig) │ │ + └──────────────────┘ +``` + +Alle Taster schalten gegen GND (interner Pull-Up wird im Code aktiviert, +kein externer Widerstand nötig). + +**Wichtig:** SD-Modul und Display hängen an unterschiedlichen SPI-Bussen +(SD an SPI0, Display an SPI1) und benutzen absichtlich *nicht* die +RP2040-Standard-SPI0-Pins (GP16-19), da diese im ursprünglichen +Prototyp-Aufbau elektrisch beansprucht waren. Bei einem Neuaufbau von +Grund auf können auch die Standard-Pins verwendet werden — einfach die +`#define SD_*_PIN`-Zeilen im Sketch anpassen. + +### Pin-Übersicht + +| Funktion | Pico-Pin | Bus/Modus | +|-------------------|----------|-------------------| +| TFT CS | GP9 | SPI1 | +| TFT DC | GP8 | SPI1 | +| TFT RST | GP12 | SPI1 | +| TFT Backlight | GP13 | digital OUT | +| TFT SCK | GP10 | SPI1 | +| TFT MOSI | GP11 | SPI1 | +| TFT MISO | GP28 | SPI1 | +| SD CS | GP5 | SPI0 | +| SD MOSI | GP7 | SPI0 | +| SD CLK | GP6 | SPI0 | +| SD MISO | GP4 | SPI0 | +| Taster Zurück | GP14 | INPUT_PULLUP | +| Taster Weiter | GP15 | INPUT_PULLUP | +| Taster Auswahl | GP20 | INPUT_PULLUP | +| Aktivitäts-LED | LED_BUILTIN (GP25) | digital OUT | + +## Abhängigkeiten (Arduino Libraries) + +Über den Arduino Library Manager installieren: + +| Library | Autor | Version | Zweck | +|---------------------------|---------------|---------|----------------------------------| +| Adafruit GFX Library | Adafruit | aktuell | Basis-Grafikbibliothek | +| Adafruit ST7735 and ST7789 Library | Adafruit | aktuell | Display-Treiber | +| SdFat | Bill Greiman | 2.x | SD-Karten-/FAT-Dateisystemzugriff | +| Adafruit TinyUSB Library | Adafruit | aktuell | USB-Massenspeicher-Gerät (MSC) | +| lvgl | kisvegabor | 9.x | Grafische Oberfläche | + +## Arduino IDE Einstellungen + +Board-Paket: **Raspberry Pi Pico/RP2040** (von Earle Philhower, +`arduino-pico`) über den Boardverwalter installieren. + +Unter `Werkzeuge`: + +| Einstellung | Wert | +|---------------------|-------------------------------| +| Board | Raspberry Pi Pico | +| Flash Size | 2MB (no FS) | +| CPU Speed | 200 MHz (oder Standard 133 MHz) | +| Optimize | Small (-Os) (Standard) | +| **USB Stack** | **Adafruit TinyUSB** ⚠️ zwingend erforderlich | +| Upload Method | Default (UF2) | + +**Wichtig:** Ohne `USB Stack: Adafruit TinyUSB` funktioniert weder die +USB-Massenspeicher-Funktion noch `Serial` (da beides über TinyUSB läuft). +Reine Test-/Diagnose-Sketches ohne TinyUSB (z.B. isolierte SPI-Tests) +brauchen stattdessen `USB Stack: Pico SDK`, damit `Serial` ohne +`Adafruit_TinyUSB.h`-Include funktioniert. + +## LVGL-Konfiguration (`lv_conf.h`) + +LVGL benötigt eine aktive `lv_conf.h`. Vorgehen: + +1. Die Vorlage `lv_conf_template.h` aus dem `lvgl`-Bibliotheksordner + eine Ebene höher kopieren, z.B. nach: + ``` + Arduino/libraries/lv_conf.h (Ebene über .../libraries/lvgl/) + ``` +2. In der kopierten Datei ganz oben `#if 0` zu `#if 1` ändern, damit sie + aktiv ist. +3. Ansonsten reicht die Standard-Vorlage unverändert — Farbformat + (RGB565) und Schriftgröße (Montserrat 14, in der Vorlage standardmäßig + aktiv) werden bereits im Sketch selbst passend gesetzt/verwendet. + +## Display-Kalibrierung + +Das verwendete ST7735S-Panel benötigt einen manuellen Adressierungs-Offset +(`tft.setColRowStart(2, 1)`), der empirisch für das konkret verbaute +Panel-Exemplar ermittelt wurde. Bei einem anderen Panel/Batch kann dieser +Wert leicht abweichen. Symptom bei falschem Offset: ein bunter, +"verrauschter" Rand am Bildschirmrand (Zeilen-/Spaltenversatz zwischen +GRAM und sichtbarem Glas). Falls das auftritt: Werte wie `(0, 0)`, +`(1, 2)` oder `(0, 32)` ausprobieren. + +## Bedienung + +| Aktion | Tasten | +|-----------------------------------|------------------------------| +| In der Liste navigieren | Zurück / Weiter | +| Fokussiertes Image mounten | Auswahl | +| Image-Liste neu laden (z.B. nach SD-Kartenwechsel) | Zurück + Weiter gleichzeitig | + +Das SD-Modul hat keinen Card-Detect-Pin — ein Kartenwechsel im laufenden +Betrieb wird daher **nicht automatisch** erkannt (frühere Versuche mit +automatischer Erkennung per periodischem Polling haben sich als +unzuverlässig erwiesen). Nach einem Kartenwechsel einfach kurz +Zurück+Weiter drücken. + +## Bekannte Einschränkungen + +- Kein automatisches Remounten des zuletzt gewählten Images nach einem + Kartenwechsel (bewusst, um kein Image von einer inzwischen anderen + Karte "weiterlaufen" zu lassen) +- Debounce-basierte Tastenerkennung, kein Auto-Repeat bei gehaltener Taste +- `MAX_IMAGES` ist aktuell auf 16 begrenzt (Konstante im Sketch anpassbar) + +## Debug-Ausgaben + +Über `Serial` (115200 Baud), zentral steuerbar über die Konstante +`DEBUG_ENABLED` im Sketch (`true`/`false`). Bei `false` entfällt auch der +Start-Delay auf den seriellen Monitor. + +## Kontakt + +Fragen, Anregungen oder Bugs? Discord: https://discord.projekt-hirnfrei.de \ No newline at end of file diff --git a/floppinator/floppinator.ino b/floppinator/floppinator.ino index f4dd7a1..ff32a0e 100644 --- a/floppinator/floppinator.ino +++ b/floppinator/floppinator.ino @@ -1,25 +1,33 @@ /* - Hirnfrei Floppinator - Standalone-Version (Pico only) + Hirnfrei Floppinator - LVGL-Version - Der Arduino Mega samt UART-Backend ist raus. Die SD-Karte haengt jetzt - direkt per SPI0 am Pico (das neue Modul mit 3V3/CS/MOSI/CLK/MISO/GND). + Backend (SD-Karte, USB-MSC, Rescan, ADF/IMG-Support, Aktivitaets-LED) + ist identisch zur reinen Adafruit-GFX-Version. Neu ist ausschliesslich + die Oberflaeche: LVGL v9 uebernimmt Display-Rendering und + Tasten-Navigation (Rauf/Runter/Auswahl -> LVGL-Keypad-Eingabegeraet). Pinbelegung SD-Modul -> Pico (SPI0, alternatives Pinset): - 3V3 -> 3V3 (out) - GND -> GND - CS -> GP5 - MOSI -> GP7 - CLK -> GP6 - MISO -> GP4 + 3V3 -> 3V3 (out), GND -> GND + CS -> GP5, MOSI -> GP7, CLK -> GP6, MISO -> GP4 - Hinweis: GP16-19 (Standard-SPI0-Pins) wurden bewusst NICHT verwendet. - GP16/17 waren im Frankenstein-Aufbau als UART-Verbindung zum Mega im - Einsatz (5V-Logik!) und haben sich beim Testen als unzuverlaessig fuer - SD-Kommunikation erwiesen - vermutlich durch die 5V-Belastung in - Mitleidenschaft gezogen. GP4-7 sind unbelastete, funktionierende Pins. + Display: 160x128 ST7735S SPI-TFT an SPI1 (unveraendert). - Das Display haengt weiterhin an SPI1 wie bisher, unveraendert. - Serial1 / UART wird nicht mehr gebraucht und ist komplett entfernt. + WICHTIG - Vorbereitung, bevor das hier kompiliert: + 1. Library "lvgl" (von kisvegabor) Version 9.x ueber den Library + Manager installieren. + 2. lv_conf.h bereitstellen: die Datei liegt als Vorlage + "lv_conf_template.h" im lvgl-Bibliotheksordner. Diese Vorlage + eine Ebene HOEHER als der "lvgl"-Ordner ablegen, z.B.: + Arduino/libraries/lv_conf.h (Ebene ueber .../libraries/lvgl/) + In der kopierten Datei ganz oben "#if 0" zu "#if 1" aendern, + damit sie ueberhaupt aktiv ist. Ansonsten reicht die Standard- + Vorlage unveraendert - Farbformat und Schriftgroesse (14) werden + im Code selbst erzwungen/verwendet, keine weiteren Anpassungen + an lv_conf.h noetig. + 3. Display-Offset (tft.setColRowStart) ist fuer dieses konkrete + Panel empirisch auf (2, 1) kalibriert. Bei einem anderen Panel- + Exemplar/Batch kann das abweichen - falls nach dem Flashen ein + bunter Rand am Bildschirmrand auftaucht, hier nachjustieren. */ #include @@ -27,6 +35,7 @@ #include #include #include +#include #define TFT_CS_PIN 9 #define TFT_DC_PIN 8 @@ -43,11 +52,9 @@ #define SD_MOSI_PIN 7 #define SD_SCK_PIN 6 -// Debug-Schalter: true = Serial-Ausgaben aktiv, false = komplett -// abgeschaltet (keine Ausgaben, kein Warten auf den seriellen Monitor -// beim Start). Das USB-CDC-Interface selbst bleibt so oder so im -// USB-Deskriptor bestehen, das liegt an der TinyUSB-Konfiguration. -#define DEBUG_ENABLED false +#define ACTIVITY_LED_PIN LED_BUILTIN + +#define DEBUG_ENABLED true #if DEBUG_ENABLED #define DBG_PRINT(...) do { if (Serial) { Serial.print(__VA_ARGS__); } } while (0) @@ -58,6 +65,8 @@ #endif #define MAX_IMAGES 16 +#define SCREEN_W 160 +#define SCREEN_H 128 struct FloppyImage { @@ -66,8 +75,6 @@ struct FloppyImage FloppyImage images[MAX_IMAGES]; uint8_t imageCount = 0; - -int8_t browseIndex = 0; int8_t mountedIndex = -1; Adafruit_USBD_MSC usb_msc; @@ -76,6 +83,8 @@ SdFat32 sd; File32 currentFile; String currentFilename = ""; +volatile bool mscBusy = false; + class TftDisplay : public Adafruit_ST7735 { public: @@ -98,6 +107,8 @@ Button btnPrev { BTN_PREV_PIN }; Button btnNext { BTN_NEXT_PIN }; Button btnSel { BTN_SEL_PIN }; +bool comboActive = false; + bool buttonPressed(Button &b) { bool reading = digitalRead(b.pin); @@ -117,12 +128,69 @@ bool buttonPressed(Button &b) return false; } -// --- USB MSC Callbacks: jetzt direkt auf die SD-Karte --- +// --- LVGL: Display-Flush + Tasten-Eingabegeraet --- + +static lv_display_t *lvDisplay; +static lv_indev_t *lvKeypad; +static lv_group_t *lvGroup; + +static lv_obj_t *imageList; +static lv_obj_t *statusLabel; +static lv_obj_t *activityDot; +static lv_obj_t *listButtons[MAX_IMAGES]; + +void disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map) +{ + uint32_t w = area->x2 - area->x1 + 1; + uint32_t h = area->y2 - area->y1 + 1; + + tft.drawRGBBitmap(area->x1, area->y1, (uint16_t *)px_map, w, h); + + lv_display_flush_ready(disp); +} + +void keypad_read_cb(lv_indev_t *indev, lv_indev_data_t *data) +{ + static uint32_t lastKey = 0; + + if (buttonPressed(btnPrev)) + { + lastKey = LV_KEY_PREV; + data->state = LV_INDEV_STATE_PRESSED; + } + else if (buttonPressed(btnNext)) + { + lastKey = LV_KEY_NEXT; + data->state = LV_INDEV_STATE_PRESSED; + } + else if (buttonPressed(btnSel)) + { + lastKey = LV_KEY_ENTER; + data->state = LV_INDEV_STATE_PRESSED; + } + else + { + data->state = LV_INDEV_STATE_RELEASED; + } + + data->key = lastKey; +} + +// --- USB MSC Callbacks --- + +inline void setMscBusy(bool busy) +{ + mscBusy = busy; + digitalWrite(ACTIVITY_LED_PIN, busy ? HIGH : LOW); +} int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) { + setMscBusy(true); + if (!currentFile) { + setMscBusy(false); return -1; } @@ -130,6 +198,7 @@ int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) { DBG_PRINT("SEEK FEHLER (read) lba="); DBG_PRINTLN(lba); + setMscBusy(false); return -1; } @@ -140,15 +209,20 @@ int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) DBG_PRINT(lba); DBG_PRINT(" n="); DBG_PRINTLN(n); + setMscBusy(false); return -1; } + setMscBusy(false); return bufsize; } int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) { + setMscBusy(true); + if (!currentFile) { + setMscBusy(false); return -1; } @@ -156,6 +230,7 @@ int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) { DBG_PRINT("SEEK FEHLER (write) lba="); DBG_PRINTLN(lba); + setMscBusy(false); return -1; } @@ -164,8 +239,10 @@ int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) { DBG_PRINT("WRITE FEHLER lba="); DBG_PRINTLN(lba); + setMscBusy(false); return -1; } + setMscBusy(false); return bufsize; } @@ -177,40 +254,128 @@ void msc_flush_cb(void) } } -// --- Display --- +// --- UI-Aufbau --- -void updateDisplay() +void buildUi() { - tft.fillScreen(ST77XX_BLACK); + lv_obj_t *scr = lv_scr_act(); + lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), 0); - tft.setTextSize(1); - tft.setTextColor(ST77XX_WHITE); - tft.setCursor(4, 4); - tft.println("Floppy A:"); + // Kopfzeile + lv_obj_t *header = lv_obj_create(scr); + lv_obj_remove_style_all(header); + lv_obj_set_size(header, SCREEN_W, 24); + lv_obj_set_pos(header, 0, 0); + lv_obj_set_style_bg_color(header, lv_color_hex(0x1b1f3a), 0); + lv_obj_set_style_bg_opa(header, LV_OPA_COVER, 0); + + lv_obj_t *title = lv_label_create(header); + lv_label_set_text(title, LV_SYMBOL_SD_CARD " FLOPPINATOR"); + lv_obj_set_style_text_color(title, lv_color_hex(0xFFC107), 0); + lv_obj_set_style_text_font(title, &lv_font_montserrat_14, 0); + lv_obj_center(title); + + // Liste + imageList = lv_list_create(scr); + lv_obj_set_size(imageList, SCREEN_W, 86); + lv_obj_set_pos(imageList, 0, 24); + lv_obj_set_style_bg_color(imageList, lv_color_hex(0x101010), 0); + lv_obj_set_style_border_width(imageList, 0, 0); + lv_obj_set_style_pad_all(imageList, 2, 0); + lv_obj_set_style_pad_row(imageList, 2, 0); + + // Fusszeile + lv_obj_t *footer = lv_obj_create(scr); + lv_obj_remove_style_all(footer); + lv_obj_set_size(footer, SCREEN_W, 18); + lv_obj_set_pos(footer, 0, 110); + lv_obj_set_style_bg_color(footer, lv_color_hex(0x1b1f3a), 0); + lv_obj_set_style_bg_opa(footer, LV_OPA_COVER, 0); + + statusLabel = lv_label_create(footer); + lv_label_set_text(statusLabel, "kein Medium"); + lv_obj_set_style_text_color(statusLabel, lv_color_hex(0xAAAAAA), 0); + lv_obj_set_style_text_font(statusLabel, &lv_font_montserrat_14, 0); + lv_obj_align(statusLabel, LV_ALIGN_LEFT_MID, 4, 0); + + activityDot = lv_obj_create(footer); + lv_obj_remove_style_all(activityDot); + lv_obj_set_size(activityDot, 8, 8); + lv_obj_set_style_radius(activityDot, LV_RADIUS_CIRCLE, 0); + lv_obj_set_style_bg_color(activityDot, lv_color_hex(0x333333), 0); + lv_obj_set_style_bg_opa(activityDot, LV_OPA_COVER, 0); + lv_obj_align(activityDot, LV_ALIGN_RIGHT_MID, -4, 0); +} + +void listButtonEventCb(lv_event_t *e) +{ + lv_obj_t *btn = (lv_obj_t *)lv_event_get_target(e); + int idx = (int)(uintptr_t)lv_obj_get_user_data(btn); + + if (idx != mountedIndex) + { + mountImage(idx); + } +} + +void refreshStatusBar() +{ + static char buf[40]; - tft.setCursor(4, 24); if (mountedIndex >= 0) { - tft.setTextColor(ST77XX_GREEN); - tft.println(images[mountedIndex].name); + snprintf(buf, sizeof(buf), LV_SYMBOL_OK " %s", images[mountedIndex].name); } else { - tft.setTextColor(ST77XX_RED); - tft.println("-- leer --"); + snprintf(buf, sizeof(buf), "kein Medium"); + } + lv_label_set_text(statusLabel, buf); + + for (uint8_t i = 0; i < imageCount; i++) + { + if (listButtons[i] == NULL) + { + continue; + } + if ((int8_t)i == mountedIndex) + { + lv_obj_set_style_bg_color(listButtons[i], lv_color_hex(0x1b3a1b), 0); + lv_obj_set_style_bg_opa(listButtons[i], LV_OPA_COVER, 0); + } + else + { + lv_obj_set_style_bg_color(listButtons[i], lv_color_hex(0x202020), 0); + lv_obj_set_style_bg_opa(listButtons[i], LV_OPA_COVER, 0); + } + } +} + +void rebuildImageList() +{ + lv_obj_clean(imageList); + for (uint8_t i = 0; i < MAX_IMAGES; i++) + { + listButtons[i] = NULL; } - tft.setCursor(4, 55); - tft.setTextColor(ST77XX_YELLOW); - tft.print("> "); - if (imageCount > 0) + if (imageCount == 0) { - tft.println(images[browseIndex].name); + lv_list_add_text(imageList, "Keine Images gefunden"); + return; } - else + + for (uint8_t i = 0; i < imageCount; i++) { - tft.println("(Suche .img...)"); + lv_obj_t *btn = lv_list_add_btn(imageList, LV_SYMBOL_FILE, images[i].name); + lv_obj_set_user_data(btn, (void *)(uintptr_t)i); + lv_obj_add_event_cb(btn, listButtonEventCb, LV_EVENT_CLICKED, NULL); + lv_group_add_obj(lvGroup, btn); + listButtons[i] = btn; } + + lv_group_focus_obj(listButtons[0]); + refreshStatusBar(); } // --- SD-Verzeichnis einlesen --- @@ -240,7 +405,7 @@ void scanImages() String lowerName = String(nameBuf); lowerName.toLowerCase(); - if (lowerName.endsWith(".img") && imageCount < MAX_IMAGES) + if ((lowerName.endsWith(".img") || lowerName.endsWith(".adf")) && imageCount < MAX_IMAGES) { strncpy(images[imageCount].name, nameBuf, sizeof(images[imageCount].name) - 1); images[imageCount].name[sizeof(images[imageCount].name) - 1] = '\0'; @@ -252,13 +417,67 @@ void scanImages() } root.close(); + sortImages(); + DBG_PRINT(imageCount); - DBG_PRINTLN(" .img-Datei(en) gefunden."); + DBG_PRINTLN(" .img/.adf-Datei(en) gefunden."); for (uint8_t i = 0; i < imageCount; i++) { DBG_PRINT(" ["); DBG_PRINT(i); DBG_PRINT("] "); DBG_PRINTLN(images[i].name); } + + rebuildImageList(); +} + +void sortImages() +{ + for (uint8_t i = 1; i < imageCount; i++) + { + FloppyImage key = images[i]; + int8_t j = i - 1; + + while (j >= 0 && strcasecmp(images[j].name, key.name) > 0) + { + images[j + 1] = images[j]; + j--; + } + images[j + 1] = key; + } +} + +void rescanSdCard() +{ + if (mscBusy) + { + return; + } + + DBG_PRINTLN("Manueller Rescan angefordert..."); + + if (currentFile) + { + currentFile.close(); + } + usb_msc.setUnitReady(false); + + bool ok = sd.begin(SD_CS_PIN, SD_SCK_MHZ(4)); + + mountedIndex = -1; + + if (ok) + { + DBG_PRINTLN("SD-Karte OK, Liste wird aktualisiert."); + scanImages(); // ruft rebuildImageList() intern auf + } + else + { + DBG_PRINTLN("SD-Karte nicht gefunden!"); + imageCount = 0; + rebuildImageList(); + } + + refreshStatusBar(); } void mountImage(int8_t idx) @@ -278,7 +497,6 @@ void mountImage(int8_t idx) DBG_PRINT(fname); DBG_PRINTLN("'"); - // USB-Verbindung kurz trennen, damit das Betriebssystem das Medium neu einliest usb_msc.setUnitReady(false); TinyUSBDevice.detach(); delay(200); @@ -303,21 +521,26 @@ void mountImage(int8_t idx) currentFilename = fname; mountedIndex = idx; - // 1.44 MB Standard Disketten-Geometrie (2880 Sektoren à 512 Byte) - usb_msc.setCapacity(2880, 512); + uint32_t numSectors = currentFile.fileSize() / 512UL; + DBG_PRINT("Dateigroesse: "); DBG_PRINT(currentFile.fileSize()); + DBG_PRINT(" Bytes -> "); DBG_PRINT(numSectors); DBG_PRINTLN(" Sektoren"); + + usb_msc.setCapacity(numSectors, 512); usb_msc.setUnitReady(true); TinyUSBDevice.attach(); + + refreshStatusBar(); } void setup() { Serial.begin(115200); #if DEBUG_ENABLED - delay(2000); // Zeit, um den Seriellen Monitor rechtzeitig zu oeffnen + delay(2000); #endif DBG_PRINTLN(); - DBG_PRINTLN("=== Bochser Floppy-Emulator (standalone) startet ==="); + DBG_PRINTLN("=== Bochser Floppy-Emulator (LVGL) startet ==="); pinMode(BTN_PREV_PIN, INPUT_PULLUP); pinMode(BTN_NEXT_PIN, INPUT_PULLUP); @@ -326,18 +549,40 @@ void setup() pinMode(TFT_BL_PIN, OUTPUT); digitalWrite(TFT_BL_PIN, HIGH); + pinMode(ACTIVITY_LED_PIN, OUTPUT); + digitalWrite(ACTIVITY_LED_PIN, LOW); + DBG_PRINTLN("Initialisiere Display..."); SPI1.setRX(28); SPI1.setSCK(10); SPI1.setTX(11); tft.initR(INITR_BLACKTAB); - tft.setColRowStart(1, 0); + tft.setColRowStart(2, 1); // Empirisch ermittelter Offset für dieses Panel tft.setRotation(1); - - updateDisplay(); + tft.fillScreen(ST77XX_BLACK); DBG_PRINTLN("Display initialisiert."); + DBG_PRINTLN("Initialisiere LVGL..."); + lv_init(); + lv_tick_set_cb(millis); + + static uint16_t lvBuf[SCREEN_W * SCREEN_H]; // explizit 16-bit RGB565, unabhaengig von lv_color_t + lvDisplay = lv_display_create(SCREEN_W, SCREEN_H); + lv_display_set_color_format(lvDisplay, LV_COLOR_FORMAT_RGB565); + lv_display_set_flush_cb(lvDisplay, disp_flush); + lv_display_set_buffers(lvDisplay, lvBuf, NULL, sizeof(lvBuf), LV_DISPLAY_RENDER_MODE_PARTIAL); + + lvKeypad = lv_indev_create(); + lv_indev_set_type(lvKeypad, LV_INDEV_TYPE_KEYPAD); + lv_indev_set_read_cb(lvKeypad, keypad_read_cb); + + lvGroup = lv_group_create(); + lv_indev_set_group(lvKeypad, lvGroup); + + buildUi(); + DBG_PRINTLN("LVGL initialisiert."); + DBG_PRINTLN("Initialisiere SD-Karte (SPI0)..."); SPI.setRX(SD_MISO_PIN); SPI.setTX(SD_MOSI_PIN); @@ -346,6 +591,7 @@ void setup() if (!sd.begin(SD_CS_PIN, SD_SCK_MHZ(4))) { DBG_PRINTLN("SD-Karte nicht gefunden!"); + rebuildImageList(); } else { @@ -353,9 +599,6 @@ void setup() scanImages(); } - updateDisplay(); - - // USB MSC konfigurieren DBG_PRINTLN("Initialisiere USB-MSC..."); usb_msc.setID("Hirnfrei", "Bochser", "1.0"); usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb); @@ -367,44 +610,35 @@ void setup() void loop() { - if (imageCount == 0) + bool prevRaw = digitalRead(BTN_PREV_PIN) == LOW; + bool nextRaw = digitalRead(BTN_NEXT_PIN) == LOW; + + if (prevRaw && nextRaw) { + if (!comboActive) + { + comboActive = true; + rescanSdCard(); + + btnPrev.stableState = LOW; btnPrev.lastReading = LOW; btnPrev.lastChangeMs = millis(); + btnNext.stableState = LOW; btnNext.lastReading = LOW; btnNext.lastChangeMs = millis(); + } + lv_timer_handler(); return; } - - if (buttonPressed(btnPrev)) + else { - browseIndex = (browseIndex - 1 + imageCount) % imageCount; - if (Serial) - { - DBG_PRINT("Zurueck -> Index "); DBG_PRINT(browseIndex); - DBG_PRINT(" ("); DBG_PRINT(images[browseIndex].name); DBG_PRINTLN(")"); - } - updateDisplay(); + comboActive = false; } - if (buttonPressed(btnNext)) + lv_timer_handler(); + + static bool lastMscBusyShown = false; + if (mscBusy != lastMscBusyShown) { - browseIndex = (browseIndex + 1) % imageCount; - if (Serial) - { - DBG_PRINT("Weiter -> Index "); DBG_PRINT(browseIndex); - DBG_PRINT(" ("); DBG_PRINT(images[browseIndex].name); DBG_PRINTLN(")"); - } - updateDisplay(); + lastMscBusyShown = mscBusy; + lv_obj_set_style_bg_color(activityDot, mscBusy ? lv_color_hex(0x00E000) : lv_color_hex(0x333333), 0); } - if (buttonPressed(btnSel)) - { - DBG_PRINT("Auswahl gedrueckt, Index "); DBG_PRINTLN(browseIndex); - if (browseIndex != mountedIndex) - { - mountImage(browseIndex); - updateDisplay(); - } - else - { - DBG_PRINTLN("Bereits gemountet, keine Aktion."); - } - } + delay(5); }