commit cfe3fee3c3760a7ef3e38d249e0990d5f9b94e02 Author: Dany Thinnes Date: Wed Sep 23 17:29:55 2026 +0200 Lauffähiger Floppinator-Code diff --git a/floppinator/floppinator.ino b/floppinator/floppinator.ino new file mode 100644 index 0000000..f4dd7a1 --- /dev/null +++ b/floppinator/floppinator.ino @@ -0,0 +1,410 @@ +/* + Hirnfrei Floppinator - Standalone-Version (Pico only) + + 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). + + Pinbelegung SD-Modul -> Pico (SPI0, alternatives Pinset): + 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. + + Das Display haengt weiterhin an SPI1 wie bisher, unveraendert. + Serial1 / UART wird nicht mehr gebraucht und ist komplett entfernt. +*/ + +#include +#include +#include +#include +#include + +#define TFT_CS_PIN 9 +#define TFT_DC_PIN 8 +#define TFT_RST_PIN 12 +#define TFT_BL_PIN 13 + +#define BTN_PREV_PIN 14 +#define BTN_NEXT_PIN 15 +#define BTN_SEL_PIN 20 +#define DEBOUNCE_MS 50 + +#define SD_CS_PIN 5 +#define SD_MISO_PIN 4 +#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 + +#if DEBUG_ENABLED + #define DBG_PRINT(...) do { if (Serial) { Serial.print(__VA_ARGS__); } } while (0) + #define DBG_PRINTLN(...) do { if (Serial) { Serial.println(__VA_ARGS__); } } while (0) +#else + #define DBG_PRINT(...) do {} while (0) + #define DBG_PRINTLN(...) do {} while (0) +#endif + +#define MAX_IMAGES 16 + +struct FloppyImage +{ + char name[32]; +}; + +FloppyImage images[MAX_IMAGES]; +uint8_t imageCount = 0; + +int8_t browseIndex = 0; +int8_t mountedIndex = -1; + +Adafruit_USBD_MSC usb_msc; + +SdFat32 sd; +File32 currentFile; +String currentFilename = ""; + +class TftDisplay : public Adafruit_ST7735 +{ +public: + TftDisplay(SPIClass *spiClass, int8_t cs, int8_t dc, int8_t rst) + : Adafruit_ST7735(spiClass, cs, dc, rst) {} + using Adafruit_ST77xx::setColRowStart; +}; + +TftDisplay tft(&SPI1, TFT_CS_PIN, TFT_DC_PIN, TFT_RST_PIN); + +struct Button +{ + uint8_t pin; + bool lastReading = HIGH; + bool stableState = HIGH; + uint32_t lastChangeMs = 0; +}; + +Button btnPrev { BTN_PREV_PIN }; +Button btnNext { BTN_NEXT_PIN }; +Button btnSel { BTN_SEL_PIN }; + +bool buttonPressed(Button &b) +{ + bool reading = digitalRead(b.pin); + if (reading != b.lastReading) + { + b.lastChangeMs = millis(); + b.lastReading = reading; + } + if ((millis() - b.lastChangeMs) > DEBOUNCE_MS && reading != b.stableState) + { + b.stableState = reading; + if (b.stableState == LOW) + { + return true; + } + } + return false; +} + +// --- USB MSC Callbacks: jetzt direkt auf die SD-Karte --- + +int32_t msc_read_cb(uint32_t lba, void *buffer, uint32_t bufsize) +{ + if (!currentFile) + { + return -1; + } + + if (!currentFile.seekSet((uint32_t)lba * 512UL)) + { + DBG_PRINT("SEEK FEHLER (read) lba="); + DBG_PRINTLN(lba); + return -1; + } + + int n = currentFile.read(buffer, bufsize); + if (n != (int)bufsize) + { + DBG_PRINT("READ FEHLER lba="); + DBG_PRINT(lba); + DBG_PRINT(" n="); + DBG_PRINTLN(n); + return -1; + } + return bufsize; +} + +int32_t msc_write_cb(uint32_t lba, uint8_t *buffer, uint32_t bufsize) +{ + if (!currentFile) + { + return -1; + } + + if (!currentFile.seekSet((uint32_t)lba * 512UL)) + { + DBG_PRINT("SEEK FEHLER (write) lba="); + DBG_PRINTLN(lba); + return -1; + } + + int n = currentFile.write(buffer, bufsize); + if (n != (int)bufsize) + { + DBG_PRINT("WRITE FEHLER lba="); + DBG_PRINTLN(lba); + return -1; + } + return bufsize; +} + +void msc_flush_cb(void) +{ + if (currentFile) + { + currentFile.sync(); + } +} + +// --- Display --- + +void updateDisplay() +{ + tft.fillScreen(ST77XX_BLACK); + + tft.setTextSize(1); + tft.setTextColor(ST77XX_WHITE); + tft.setCursor(4, 4); + tft.println("Floppy A:"); + + tft.setCursor(4, 24); + if (mountedIndex >= 0) + { + tft.setTextColor(ST77XX_GREEN); + tft.println(images[mountedIndex].name); + } + else + { + tft.setTextColor(ST77XX_RED); + tft.println("-- leer --"); + } + + tft.setCursor(4, 55); + tft.setTextColor(ST77XX_YELLOW); + tft.print("> "); + if (imageCount > 0) + { + tft.println(images[browseIndex].name); + } + else + { + tft.println("(Suche .img...)"); + } +} + +// --- SD-Verzeichnis einlesen --- + +void scanImages() +{ + imageCount = 0; + + File32 root = sd.open("/"); + DBG_PRINT("root.open('/') Ergebnis: "); + DBG_PRINTLN(root ? "OK" : "FEHLGESCHLAGEN"); + + if (!root) + { + DBG_PRINTLN("-> FEHLER: Root-Verzeichnis '/' konnte nicht geoeffnet werden!"); + return; + } + + File32 file = root.openNextFile(); + while (file) + { + char nameBuf[32]; + file.getName(nameBuf, sizeof(nameBuf)); + + if (!file.isDirectory()) + { + String lowerName = String(nameBuf); + lowerName.toLowerCase(); + + if (lowerName.endsWith(".img") && imageCount < MAX_IMAGES) + { + strncpy(images[imageCount].name, nameBuf, sizeof(images[imageCount].name) - 1); + images[imageCount].name[sizeof(images[imageCount].name) - 1] = '\0'; + imageCount++; + } + } + file.close(); + file = root.openNextFile(); + } + root.close(); + + DBG_PRINT(imageCount); + DBG_PRINTLN(" .img-Datei(en) gefunden."); + for (uint8_t i = 0; i < imageCount; i++) + { + DBG_PRINT(" ["); DBG_PRINT(i); DBG_PRINT("] "); + DBG_PRINTLN(images[i].name); + } +} + +void mountImage(int8_t idx) +{ + if (idx < 0 || idx >= imageCount) + { + return; + } + + String fname = String(images[idx].name); + if (!fname.startsWith("/")) + { + fname = "/" + fname; + } + + DBG_PRINT("Mounte: '"); + DBG_PRINT(fname); + DBG_PRINTLN("'"); + + // USB-Verbindung kurz trennen, damit das Betriebssystem das Medium neu einliest + usb_msc.setUnitReady(false); + TinyUSBDevice.detach(); + delay(200); + + if (currentFile) + { + currentFile.close(); + } + + currentFile = sd.open(fname.c_str(), O_RDWR); + bool ok = (bool)currentFile; + + DBG_PRINT("-> Mount "); + DBG_PRINTLN(ok ? "OK" : "FEHLGESCHLAGEN"); + + if (!ok) + { + TinyUSBDevice.attach(); + return; + } + + currentFilename = fname; + mountedIndex = idx; + + // 1.44 MB Standard Disketten-Geometrie (2880 Sektoren à 512 Byte) + usb_msc.setCapacity(2880, 512); + usb_msc.setUnitReady(true); + + TinyUSBDevice.attach(); +} + +void setup() +{ + Serial.begin(115200); +#if DEBUG_ENABLED + delay(2000); // Zeit, um den Seriellen Monitor rechtzeitig zu oeffnen +#endif + DBG_PRINTLN(); + DBG_PRINTLN("=== Bochser Floppy-Emulator (standalone) startet ==="); + + pinMode(BTN_PREV_PIN, INPUT_PULLUP); + pinMode(BTN_NEXT_PIN, INPUT_PULLUP); + pinMode(BTN_SEL_PIN, INPUT_PULLUP); + + pinMode(TFT_BL_PIN, OUTPUT); + digitalWrite(TFT_BL_PIN, HIGH); + + DBG_PRINTLN("Initialisiere Display..."); + SPI1.setRX(28); + SPI1.setSCK(10); + SPI1.setTX(11); + + tft.initR(INITR_BLACKTAB); + tft.setColRowStart(1, 0); + tft.setRotation(1); + + updateDisplay(); + DBG_PRINTLN("Display initialisiert."); + + DBG_PRINTLN("Initialisiere SD-Karte (SPI0)..."); + SPI.setRX(SD_MISO_PIN); + SPI.setTX(SD_MOSI_PIN); + SPI.setSCK(SD_SCK_PIN); + + if (!sd.begin(SD_CS_PIN, SD_SCK_MHZ(4))) + { + DBG_PRINTLN("SD-Karte nicht gefunden!"); + } + else + { + DBG_PRINTLN("SD-Karte OK."); + 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); + usb_msc.setCapacity(2880, 512); + usb_msc.setUnitReady(false); + usb_msc.begin(); + DBG_PRINTLN("=== Setup abgeschlossen ==="); +} + +void loop() +{ + if (imageCount == 0) + { + return; + } + + if (buttonPressed(btnPrev)) + { + browseIndex = (browseIndex - 1 + imageCount) % imageCount; + if (Serial) + { + DBG_PRINT("Zurueck -> Index "); DBG_PRINT(browseIndex); + DBG_PRINT(" ("); DBG_PRINT(images[browseIndex].name); DBG_PRINTLN(")"); + } + updateDisplay(); + } + + if (buttonPressed(btnNext)) + { + browseIndex = (browseIndex + 1) % imageCount; + if (Serial) + { + DBG_PRINT("Weiter -> Index "); DBG_PRINT(browseIndex); + DBG_PRINT(" ("); DBG_PRINT(images[browseIndex].name); DBG_PRINTLN(")"); + } + updateDisplay(); + } + + if (buttonPressed(btnSel)) + { + DBG_PRINT("Auswahl gedrueckt, Index "); DBG_PRINTLN(browseIndex); + if (browseIndex != mountedIndex) + { + mountImage(browseIndex); + updateDisplay(); + } + else + { + DBG_PRINTLN("Bereits gemountet, keine Aktion."); + } + } +}