README.md hinzugefügt

This commit is contained in:
2026-09-25 07:54:14 +02:00
parent cfe3fee3c3
commit 5da29cf3ca
2 changed files with 508 additions and 87 deletions

187
README.md Normal file
View File

@@ -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

View File

@@ -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 <Adafruit_TinyUSB.h>
@@ -27,6 +35,7 @@
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <SdFat.h>
#include <lvgl.h>
#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))
{
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.");
comboActive = false;
}
lv_timer_handler();
static bool lastMscBusyShown = false;
if (mscBusy != lastMscBusyShown)
{
lastMscBusyShown = mscBusy;
lv_obj_set_style_bg_color(activityDot, mscBusy ? lv_color_hex(0x00E000) : lv_color_hex(0x333333), 0);
}
delay(5);
}