README.md hinzugefügt
This commit is contained in:
187
README.md
Normal file
187
README.md
Normal 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
|
||||
@@ -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))
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user