sync
This commit is contained in:
@@ -9,9 +9,12 @@ target_sources(app PRIVATE
|
||||
src/io.c
|
||||
src/audio.c
|
||||
src/usb.c
|
||||
src/uart.c
|
||||
src/protocol.c
|
||||
src/utils.c
|
||||
src/settings.c
|
||||
)
|
||||
zephyr_include_directories(src)
|
||||
|
||||
zephyr_include_directories(include)
|
||||
|
||||
|
||||
|
||||
195
firmware/Tags.md
195
firmware/Tags.md
@@ -1,133 +1,122 @@
|
||||
# Audio-Tags Format
|
||||
# Edi's Buzzer - Metadata Tags Format
|
||||
|
||||
Dieses Dokument beschreibt das aktuelle Tag-Format für Audiodateien.
|
||||
## Architektur-Übersicht
|
||||
Die Metadaten werden transparent an das Ende der rohen Audio-Daten angehängt. Das Format basiert auf einer strikten **Little-Endian** Byte-Reihenfolge und nutzt eine erweiterbare **TLV-Struktur** (Type-Length-Value) für die eigentlichen Datenblöcke.
|
||||
|
||||
## 1) Position in der Datei
|
||||
Das physische Layout einer Datei im Flash-Speicher sieht wie folgt aus:
|
||||
`[Audio-Rohdaten] [TLV-Block 1] ... [TLV-Block N] [Footer (8 Bytes)]`
|
||||
|
||||
Die Tags stehen am Dateiende:
|
||||
---
|
||||
|
||||
`[audio_data][metadata][tag_version_u8][footer_len_le16]["TAG!"]`
|
||||
## 1. Footer-Struktur
|
||||
Der Footer liegt exakt auf den letzten 8 Bytes der Datei (EOF - 8). Er dient als Ankerpunkt für den Parser, um die Metadaten rückwärts aus der Datei zu extrahieren. Das 8-Byte-Alignment stellt speichersichere Casts auf 32-Bit-ARM-Architekturen sicher.
|
||||
|
||||
- `audio_data`: eigentliche Audiodaten
|
||||
- `metadata`: Folge von Tag-Einträgen
|
||||
- `tag_version_u8`: 1 Byte Versionsnummer des Tag-Formats
|
||||
- `footer_len_le16`: 2 Byte, Little Endian
|
||||
- `"TAG!"`: 4 Byte Magic (`0x54 0x41 0x47 0x21`)
|
||||
| Offset | Feld | Typ | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 0 | `total_size` | `uint16_t` | Gesamtgröße in Bytes (Summe aller TLV-Blöcke + 8 Bytes Footer). |
|
||||
| 2 | `version` | `uint16_t` | Format-Version. Aktuell `0x0001`. |
|
||||
| 4 | `magic` | `char[4]` | Fixe Signatur: `"TAG!"` (Hex: `54 41 47 21`). |
|
||||
|
||||
## 2) Bedeutung von `footer_len_le16`
|
||||
---
|
||||
|
||||
`footer_len_le16` ist die **Gesamtlänge des Footers**, also:
|
||||
## 2. TLV-Header (Type-Length-Value)
|
||||
Jeder Metadaten-Block beginnt mit einem exakt 4 Bytes großen Header. Unbekannte Typen können vom Controller durch einen relativen Sprung (`fs_seek` um `length` Bytes) übersprungen werden.
|
||||
|
||||
`footer_len = metadata_len + 1 + 2 + 4`
|
||||
| Offset | Feld | Typ | Beschreibung |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| 0 | `type` | `uint8_t` | Definiert den Inhalt des Blocks (siehe Typen-Definitionen). |
|
||||
| 1 | `index` | `uint8_t` | Erlaubt die Fragmentierung großer Datensätze (z.B. bei JSON > 64 KB). Standard: `0x00`. |
|
||||
| 2 | `length` | `uint16_t` | Größe der folgenden Payload in Bytes (ohne diesen Header). |
|
||||
|
||||
Damit beginnt `metadata` bei:
|
||||
---
|
||||
|
||||
`metadata_start = file_size - footer_len`
|
||||
## 3. Typen-Definitionen
|
||||
|
||||
Das passt zur aktuellen Implementierung in der Firmware.
|
||||
### Type `0x00`: Binary System Metadata
|
||||
Dieser Typ gruppiert maschinenlesbare, binäre Systeminformationen. Die Unterscheidung erfolgt über das `Index`-Feld.
|
||||
|
||||
### Tag-Version
|
||||
#### Index `0x00`: Audio Format
|
||||
Dieser Block konfiguriert den I2S-Treiber vor der Wiedergabe.
|
||||
* **Typ:** `0x00`
|
||||
* **Index:** `0x00`
|
||||
* **Länge:** `0x0008` (8 Bytes)
|
||||
* **Payload:** `[codec: 1 Byte] [bit_depth: 1 Byte] [reserved: 2 Bytes] [samplerate: 4 Bytes]`
|
||||
|
||||
- `tag_version` ist aktuell `0x01`.
|
||||
- Der Host darf nur bekannte Versionen interpretieren.
|
||||
- Bei unbekannter Version: Tag-Block ignorieren oder als "nicht unterstützt" melden.
|
||||
#### Index `0x01`: Audio CRC32
|
||||
Speichert die CRC32-Prüfsumme (IEEE) der reinen Audiodaten (vom Dateianfang bis zum Beginn des ersten TLV-Blocks). Dient Synchronisations-Tools für einen schnellen Integritäts- und Abgleich-Check, ohne die gesamte Datei neu hashen zu müssen.
|
||||
* **Typ:** `0x00`
|
||||
* **Index:** `0x01`
|
||||
* **Länge:** `0x0004` (4 Bytes)
|
||||
* **Payload:** `uint32_t` (Little-Endian)
|
||||
|
||||
## 3) Endianness und Typen
|
||||
### Type `0x10`: JSON Metadata
|
||||
Dieser Block enthält Metadaten, die primär für das Host-System (z. B. das Python-Tool) zur Verwaltung, Kategorisierung und Anzeige bestimmt sind. Der Mikrocontroller ignoriert und überspringt diesen Block während der Audiowiedergabe.
|
||||
|
||||
- Alle Multi-Byte-Werte sind **Little Endian**.
|
||||
- Tag-Einträge sind TLV-basiert:
|
||||
- `type`: `uint8_t`
|
||||
- `len`: `uint16_t`
|
||||
- `value`: `byte[len]`
|
||||
* **Typ:** `0x10`
|
||||
* **Länge:** Variabel
|
||||
* **Payload:** UTF-8-kodierter JSON-String (ohne Null-Terminator).
|
||||
|
||||
Dadurch können auch unbekannte Typen sauber übersprungen werden.
|
||||
#### Standardisierte JSON-Schlüssel
|
||||
Die nachfolgenden Schlüssel (Keys) sind im Basis-Standard definiert. Die Integration weiterer, proprietärer Schlüssel ist technisch möglich. Es wird jedoch empfohlen, dies mit Vorsicht zu handhaben, da zukünftige Standardisierungen diese Schlüsselnamen belegen könnten (Namenskollision).
|
||||
|
||||
## 4) Unterstützte Tag-Typen
|
||||
| Schlüssel | Datentyp | Beschreibung |
|
||||
| :--- | :--- | :--- |
|
||||
| `t` | String | Titel der Audiodatei |
|
||||
| `a` | String | Autor oder Ersteller |
|
||||
| `r` | String | Bemerkungen (Remarks) oder Beschreibung |
|
||||
| `c` | Array of Strings | Kategorien zur Gruppierung |
|
||||
| `dc` | String | Erstellungsdatum (Date Created), idealerweise nach ISO 8601 |
|
||||
| `ds` | String | Speicher- oder Änderungsdatum (Date Saved), idealerweise nach ISO 8601 |
|
||||
|
||||
Aktuell definierte Typen:
|
||||
**Beispiel-Payload:**
|
||||
Ein vollständiger JSON-Datensatz gemäß dieser Spezifikation hat folgendes Format:
|
||||
|
||||
- `0x00`: `DESCRIPTION` (Beschreibung des Samples)
|
||||
- `0x01`: `AUTHOR`
|
||||
- `0x10`: `CRC32_RAW`
|
||||
- `0x20`: `FILE_FORMAT` (Info für Host, Player wertet derzeit nicht aus)
|
||||
```json
|
||||
{
|
||||
"t": "Testaufnahme System A",
|
||||
"a": "Entwickler-Team",
|
||||
"r": "Überprüfung der Mikrofon-Aussteuerung.",
|
||||
"c": ["Test", "Audio", "V1"],
|
||||
"dc": "2026-03-05T13:00:00Z",
|
||||
"ds": "2026-03-05T13:10:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## 5) Value-Format pro Tag
|
||||
*(Hinweis zur Skalierbarkeit: Für zukünftige Erweiterungen können dedizierte TLV-Typen definiert werden, wie beispielsweise 0x11 für GZIP-komprimierte JSON-Daten oder 0x20 für binäre Bilddaten wie PNG-Cover).*
|
||||
|
||||
### 5.1 `0x00` DESCRIPTION
|
||||
---
|
||||
|
||||
- `value`: UTF-8-Text
|
||||
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||
## 4. Lese-Algorithmus (Parser-Logik)
|
||||
|
||||
### 5.2 `0x01` AUTHOR
|
||||
Der Controller extrahiert die Hardware-Parameter nach folgendem Ablauf:
|
||||
|
||||
- `value`: UTF-8-Text
|
||||
- `len`: Anzahl Bytes des UTF-8-Texts
|
||||
1. **Footer lokalisieren:** * Gehe zu `EOF - 8`. Lese 8 Bytes in das `tag_footer_t` Struct.
|
||||
* Validiere `magic == "TAG!"` und `version == 0x0001` (unter Berücksichtigung von Little-Endian Konvertierung via `sys_le16_to_cpu`).
|
||||
2. **Grenzen berechnen:**
|
||||
* Lese `total_size`.
|
||||
* Die reinen Audiodaten enden bei `audio_limit = EOF - total_size`.
|
||||
* Gehe zur Position `audio_limit`.
|
||||
3. **TLV-Blöcke iterieren:**
|
||||
* Solange die aktuelle Leseposition kleiner als `EOF - 8` ist:
|
||||
* Lese 4 Bytes in den `tlv_header_t`.
|
||||
* Wenn `type == 0x00`: Lese die nächsten 8 Bytes in das `tlv_audio_format_t` Struct.
|
||||
* Wenn `type != 0x00`: Führe `fs_seek(header.length, FS_SEEK_CUR)` aus.
|
||||
|
||||
### 5.3 `0x10` CRC32_RAW
|
||||
---
|
||||
|
||||
- `value`: `uint32_t crc32` (4 Byte, Little Endian)
|
||||
- `len`: **muss 4** sein
|
||||
## 5. Hex-Beispiel
|
||||
|
||||
### 5.4 `0x20` FILE_FORMAT
|
||||
Eine fiktive Datei enthält Audio-Daten. Es soll ein PCM-Mono Format (16 Bit, 16 kHz) sowie ein kurzes JSON `{"t":"A"}` (9 Bytes) angehängt werden.
|
||||
|
||||
- `value`:
|
||||
- `bits_per_sample`: `uint8_t`
|
||||
- `sample_rate`: `uint32_t` (Little Endian)
|
||||
- `len`: **muss 5** sein
|
||||
**1. TLV 0x00 (Audio Format):**
|
||||
* Header: `00 00 08 00` (Type 0, Index 0, Length 8)
|
||||
* Payload: `00 10 00 00 80 3E 00 00` (Mono, 16-Bit, Reserved, 16000 Hz)
|
||||
|
||||
Beispielwerte aktuell oft: `bits_per_sample = 16`, `sample_rate = 16000`.
|
||||
**2. TLV 0x10 (JSON):**
|
||||
* Header: `10 00 09 00` (Type 16, Index 0, Length 9)
|
||||
* Payload: `7B 22 74 22 3A 22 41 22 7D` (`{"t":"A"}`)
|
||||
|
||||
## 6) Vorkommen je Typ
|
||||
|
||||
Aktueller Stand: **jeder Tag-Typ darf maximal 1x vorkommen**.
|
||||
|
||||
Empfohlene Host-Regel:
|
||||
|
||||
- Falls ein Typ mehrfach vorkommt, letzte Instanz gewinnt (`last-wins`) und ein Warnhinweis wird geloggt.
|
||||
|
||||
## 7) Validierungsregeln (Host)
|
||||
|
||||
Beim Lesen:
|
||||
|
||||
1. Prüfen, ob Datei mindestens 7 Byte hat.
|
||||
2. Letzte 6 Byte prüfen: `footer_len_le16` + `TAG!`.
|
||||
3. `footer_len` gegen Dateigröße validieren (`6 <= footer_len <= file_size`).
|
||||
4. `tag_version` an Position `file_size - 6 - 1` lesen und validieren.
|
||||
5. Im Metadatenbereich TLV-Einträge lesen, bis Ende erreicht.
|
||||
6. Für bekannte Typen feste Längen prüfen (`CRC32_RAW=4`, `FILE_FORMAT=5`).
|
||||
7. Unbekannte Typen über `len` überspringen.
|
||||
|
||||
Beim Schreiben:
|
||||
|
||||
1. Vorhandene Tags entfernen/ersetzen (audio-Ende bestimmen).
|
||||
2. Neue TLV-Metadaten schreiben.
|
||||
3. `tag_version_u8` schreiben (`0x01`).
|
||||
4. `footer_len_le16` schreiben (inkl. 1+2+4).
|
||||
5. `TAG!` schreiben.
|
||||
5. Datei auf neue Länge truncaten.
|
||||
|
||||
## 8) Beispiel (hex)
|
||||
|
||||
Beispiel mit:
|
||||
|
||||
- DESCRIPTION = "Kick"
|
||||
- AUTHOR = "Edi"
|
||||
- CRC32_RAW = `0x12345678`
|
||||
|
||||
TLV-Daten:
|
||||
|
||||
- `00 04 00 4B 69 63 6B`
|
||||
- `01 03 00 45 64 69`
|
||||
- `10 04 00 78 56 34 12`
|
||||
|
||||
`metadata_len = 7 + 6 + 7 = 20 (0x0014)`
|
||||
|
||||
`footer_len = 20 + 1 + 2 + 4 = 27 (0x001B)`
|
||||
|
||||
Footer-Ende:
|
||||
|
||||
- `01 1B 00 54 41 47 21`
|
||||
|
||||
## 9) Hinweis zur aktuellen Firmware
|
||||
|
||||
Die Firmware verarbeitet Tag-Payload direkt binär (Chunk-Streaming über das Protokoll). Das dateiinterne Format entspricht direkt diesem Dokument.
|
||||
**3. Footer:**
|
||||
* Total Size: `2D 00` (45 Bytes = 12 Bytes Audio-TLV + 13 Bytes JSON-TLV + 12 Bytes Padding/Zusatz + 8 Bytes Footer) -> *Hinweis: Size ist in diesem Konstrukt abhängig vom genauen Payload.*
|
||||
* Version: `01 00`
|
||||
* Magic: `54 41 47 21` (`TAG!`)
|
||||
@@ -1,6 +1,6 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 2
|
||||
PATCHLEVEL = 19
|
||||
VERSION_MINOR = 3
|
||||
PATCHLEVEL = 5
|
||||
VERSION_TWEAK = 0
|
||||
#if (IS_ENABLED(CONFIG_LOG))
|
||||
EXTRAVERSION = debug
|
||||
|
||||
@@ -10,6 +10,37 @@ typedef struct slot_info_t {
|
||||
size_t size;
|
||||
} slot_info_t;
|
||||
|
||||
typedef enum {
|
||||
FS_MSG_START,
|
||||
FS_MSG_CHUNK,
|
||||
FS_MSG_EOF,
|
||||
FS_MSG_ABORT
|
||||
} fs_msg_type_t;
|
||||
|
||||
typedef struct {
|
||||
fs_msg_type_t type;
|
||||
|
||||
/* Die Union spart RAM, da Start- und Chunk-Parameter
|
||||
nie gleichzeitig im selben Message-Paket benötigt werden. */
|
||||
union {
|
||||
/* Payload für FS_MSG_START */
|
||||
struct {
|
||||
/* Der String wird sicher in die Queue kopiert */
|
||||
char filename[MAX_PATH_LEN];
|
||||
uint32_t expected_size;
|
||||
uint32_t start_position;
|
||||
} start;
|
||||
|
||||
/* Payload für FS_MSG_CHUNK */
|
||||
struct {
|
||||
void *slab_ptr;
|
||||
uint32_t chunk_size;
|
||||
} chunk;
|
||||
};
|
||||
} fs_msg_t;
|
||||
|
||||
extern struct k_msgq fs_msgq;
|
||||
|
||||
/**
|
||||
* @brief Initializes the filesystem by mounting it
|
||||
*/
|
||||
@@ -102,6 +133,21 @@ int fs_pm_mkdir(const char *path);
|
||||
*/
|
||||
int fs_pm_rename(const char *old_path, const char *new_path);
|
||||
|
||||
/**
|
||||
* @brief Recursively creates directories for the given path, ensuring the flash is active during the operation
|
||||
* @param path Path to the directory to create (can include multiple levels, e.g. "/dir1/dir2/dir3")
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_pm_mkdir_recursive(char *path);
|
||||
|
||||
/**
|
||||
* @brief Recursively removes a directory and all its contents, ensuring the flash is active during the operation
|
||||
* @param path Path to the directory to remove
|
||||
* @param max_len Maximum length of the path buffer
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_pm_rm_recursive(char *path, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Gets the length of the audio data in a file, accounting for any metadata tags
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
@@ -138,29 +184,15 @@ int fs_tag_open_read(struct fs_file_t *fp, uint8_t *version, size_t *payload_len
|
||||
ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Positions file pointer for tag payload overwrite at end of audio data.
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
* @return 0 on success, negative error code on failure
|
||||
* @brief Setzt die Synchronisation für einen neuen Dateitransfer zurück.
|
||||
*/
|
||||
int fs_tag_open_write(struct fs_file_t *fp);
|
||||
void fs_reset_transfer_sync(void);
|
||||
|
||||
/**
|
||||
* @brief Writes a raw tag payload chunk.
|
||||
* @param fp Pointer to an open fs_file_t positioned for tag payload write
|
||||
* @param buffer Source buffer
|
||||
* @param len Number of bytes to write
|
||||
* @return Number of bytes written, negative error code on failure
|
||||
* @brief Blockiert den aufrufenden Thread, bis der FS-Thread den Transfer
|
||||
* (EOF oder ABORT) vollständig auf dem Flash abgeschlossen hat.
|
||||
*/
|
||||
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Finalizes tags by appending version + footer and truncating file.
|
||||
* @param fp Pointer to an open fs_file_t structure representing the audio file
|
||||
* @param version Tag format version to write
|
||||
* @param payload_len Tag payload length in bytes
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len);
|
||||
void fs_wait_for_transfer_complete(void);
|
||||
|
||||
/**
|
||||
* @brief Retrieves information about the firmware slot, such as start address and size
|
||||
@@ -30,6 +30,14 @@ typedef enum {
|
||||
CMD_PUT_FILE = 0x20,
|
||||
CMD_PUT_FW = 0x21,
|
||||
CMD_GET_FILE = 0x22,
|
||||
CMD_PUT_TAGS = 0x24,
|
||||
CMD_GET_TAGS = 0x25,
|
||||
|
||||
CMD_PLAY = 0x30,
|
||||
CMD_STOP = 0x31,
|
||||
|
||||
CMD_SET_SETTING = 0x40,
|
||||
CMD_GET_SETTING = 0x41,
|
||||
} protocol_cmd_t;
|
||||
|
||||
typedef enum {
|
||||
28
firmware/include/settings.h
Normal file
28
firmware/include/settings.h
Normal file
@@ -0,0 +1,28 @@
|
||||
#ifndef BUZZER_SETTINGS_H
|
||||
#define BUZZER_SETTINGS_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* Struktur für den direkten Lesezugriff aus dem RAM (Zero-Latency) */
|
||||
typedef struct {
|
||||
uint8_t audio_vol; /* 0..100 */
|
||||
bool play_norepeat; /* true = 1, false = 0 */
|
||||
uint32_t storage_interval_s; /* 0..7200 Sekunden */
|
||||
} app_settings_t;
|
||||
|
||||
/* Globale Instanz für den direkten Lesezugriff */
|
||||
extern app_settings_t app_settings;
|
||||
|
||||
/* Initialisiert das Settings-Subsystem, NVS und lädt die gespeicherten Werte */
|
||||
int app_settings_init(void);
|
||||
|
||||
/* Setter: Aktualisieren den RAM-Wert und starten/verlängern den Speichern-Timer */
|
||||
void app_settings_set_audio_vol(uint8_t vol);
|
||||
void app_settings_set_play_norepeat(bool norepeat);
|
||||
void app_settings_set_storage_interval(uint32_t interval_s);
|
||||
|
||||
/* Forciert sofortiges Speichern aller anstehenden Werte (Aufruf z.B. vor CMD_REBOOT) */
|
||||
void app_settings_save_pending_now(void);
|
||||
|
||||
#endif /* BUZZER_SETTINGS_H */
|
||||
8
firmware/include/uart.h
Normal file
8
firmware/include/uart.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef _UART_H_
|
||||
#define _UART_H_
|
||||
|
||||
int uart_init(void);
|
||||
int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout);
|
||||
int uart_write_string(const char *str, k_timeout_t timeout);
|
||||
int uart_read(uint8_t *buffer, size_t max_len, k_timeout_t timeout);
|
||||
#endif /* _UART_H_ */
|
||||
8
firmware/include/usb.h
Normal file
8
firmware/include/usb.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#ifndef USB_H_
|
||||
#define USB_H_
|
||||
|
||||
int usb_init(void);
|
||||
void usb_wait_for_dtr(void);
|
||||
bool usb_dtr_active(void);
|
||||
|
||||
#endif /* USB_H_ */
|
||||
@@ -3,7 +3,7 @@ mcuboot:
|
||||
size: 0xC000
|
||||
region: flash_primary
|
||||
|
||||
# Primary Slot: Start bleibt 0xC000, Größe jetzt 200KB (0x32000)
|
||||
# Primary Slot: Start bleibt 0xC000, Größe 200KB (0x32000)
|
||||
mcuboot_primary:
|
||||
address: 0xC000
|
||||
size: 0x32000
|
||||
@@ -26,7 +26,13 @@ mcuboot_secondary:
|
||||
size: 0x32000
|
||||
region: flash_primary
|
||||
|
||||
# External Flash bleibt unverändert
|
||||
# NVS storage am Ende des Flashs, 16KB (0x4000)
|
||||
settings_storage:
|
||||
address: 0xFC000
|
||||
size: 0x4000
|
||||
region: flash_primary
|
||||
|
||||
# External Flash
|
||||
littlefs_storage:
|
||||
address: 0x0
|
||||
size: 0x800000
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# --- GPIO & Logging ---
|
||||
CONFIG_GPIO=y
|
||||
CONFIG_LOG=y
|
||||
CONFIG_POLL=y
|
||||
CONFIG_POLL=n
|
||||
|
||||
# --- Power Management (Fix für HAS_PM & Policy) ---
|
||||
# CONFIG_PM=y
|
||||
@@ -13,24 +13,25 @@ CONFIG_FLASH_MAP=y
|
||||
CONFIG_FILE_SYSTEM=y
|
||||
CONFIG_FILE_SYSTEM_LITTLEFS=y
|
||||
CONFIG_FILE_SYSTEM_MKFS=y
|
||||
CONFIG_FS_LITTLEFS_READ_SIZE=64
|
||||
CONFIG_FS_LITTLEFS_READ_SIZE=256
|
||||
CONFIG_FS_LITTLEFS_PROG_SIZE=256
|
||||
CONFIG_FS_LITTLEFS_CACHE_SIZE=512
|
||||
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=128
|
||||
CONFIG_FS_LITTLEFS_CACHE_SIZE=4096
|
||||
CONFIG_FS_LITTLEFS_LOOKAHEAD_SIZE=256
|
||||
CONFIG_FS_LITTLEFS_BLOCK_CYCLES=512
|
||||
CONFIG_MAIN_STACK_SIZE=2048
|
||||
|
||||
# --- NVS & Settings (für die Speicherung von Konfigurationen) ---
|
||||
CONFIG_NVS=y
|
||||
CONFIG_SETTINGS=y
|
||||
CONFIG_SETTINGS_NVS=y
|
||||
|
||||
# --- USB Device & CDC ACM ---
|
||||
CONFIG_USB_DEVICE_STACK=y
|
||||
CONFIG_DEPRECATION_TEST=y
|
||||
CONFIG_USB_DEVICE_MANUFACTURER="Eduard Iten"
|
||||
CONFIG_USB_DEVICE_PRODUCT="Edis Buzzer"
|
||||
CONFIG_USB_DEVICE_PID=0x0001
|
||||
CONFIG_USB_DRIVER_LOG_LEVEL_ERR=y
|
||||
CONFIG_USB_DEVICE_LOG_LEVEL_ERR=y
|
||||
CONFIG_USB_DEVICE_LOG_LEVEL_OFF=y
|
||||
CONFIG_USB_DEVICE_INITIALIZE_AT_BOOT=n
|
||||
CONFIG_USB_DEVICE_STACK_NEXT=n
|
||||
CONFIG_USB_DEVICE_STACK_NEXT=y
|
||||
CONFIG_USBD_CDC_ACM_CLASS=y
|
||||
CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n
|
||||
CONFIG_USBD_LOG_LEVEL_ERR=y
|
||||
CONFIG_UDC_DRIVER_LOG_LEVEL_ERR=y
|
||||
CONFIG_USBD_CDC_ACM_LOG_LEVEL_OFF=y
|
||||
|
||||
# --- UART (für USB-CDC) ---
|
||||
CONFIG_SERIAL=y
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <audio.h>
|
||||
#include <fs.h>
|
||||
#include <io.h>
|
||||
#include <settings.h>
|
||||
|
||||
#define AUDIO_THREAD_STACK_SIZE 2048
|
||||
#define AUDIO_THREAD_PRIORITY 5
|
||||
@@ -47,7 +48,6 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1);
|
||||
static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE);
|
||||
static const struct gpio_dt_spec amp_en_dev = GPIO_DT_SPEC_GET(AUDIO_AMP_ENABLE_NODE, gpios);
|
||||
|
||||
static volatile int current_volume = 8;
|
||||
static volatile bool abort_playback = false;
|
||||
static char next_random_filename[64] = {0};
|
||||
|
||||
@@ -57,6 +57,8 @@ static char cached_404_path[] = "/lfs/sys/404";
|
||||
static struct k_mutex i2s_lock;
|
||||
static struct k_work audio_stop_work;
|
||||
|
||||
static uint32_t last_played_index = 0xFFFFFFFF;
|
||||
|
||||
static void audio_stop_work_handler(struct k_work *work)
|
||||
{
|
||||
ARG_UNUSED(work);
|
||||
@@ -89,6 +91,52 @@ void i2s_resume(void)
|
||||
k_mutex_unlock(&i2s_lock);
|
||||
}
|
||||
|
||||
int get_random_file(char *out_filename, size_t max_len)
|
||||
{
|
||||
if (audio_file_count == 0)
|
||||
{
|
||||
/* Fallback auf System-Sound, wenn Ordner leer */
|
||||
strncpy(out_filename, cached_404_path, max_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t target_index;
|
||||
|
||||
/* Random-Index generieren mit optionalem No-Repeat-Schutz */
|
||||
if (app_settings.play_norepeat && audio_file_count > 1) {
|
||||
do {
|
||||
target_index = k_cycle_get_32() % audio_file_count;
|
||||
} while (target_index == last_played_index);
|
||||
} else {
|
||||
target_index = k_cycle_get_32() % audio_file_count;
|
||||
}
|
||||
|
||||
last_played_index = target_index;
|
||||
|
||||
struct fs_dir_t dirp;
|
||||
struct fs_dirent entry;
|
||||
uint32_t current_index = 0;
|
||||
|
||||
fs_dir_t_init(&dirp);
|
||||
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
|
||||
return -ENOENT;
|
||||
|
||||
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
||||
{
|
||||
if (entry.type == FS_DIR_ENTRY_FILE)
|
||||
{
|
||||
if (current_index == target_index)
|
||||
{
|
||||
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
|
||||
break;
|
||||
}
|
||||
current_index++;
|
||||
}
|
||||
}
|
||||
fs_pm_closedir(&dirp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_refresh_file_count(void)
|
||||
{
|
||||
static struct fs_dir_t dirp;
|
||||
@@ -112,6 +160,7 @@ void audio_refresh_file_count(void)
|
||||
fs_pm_closedir(&dirp);
|
||||
audio_file_count = count;
|
||||
LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH);
|
||||
get_random_file(next_random_filename, sizeof(next_random_filename));
|
||||
}
|
||||
|
||||
static void wait_for_i2s_drain(void)
|
||||
@@ -130,40 +179,6 @@ static void wait_for_i2s_drain(void)
|
||||
}
|
||||
}
|
||||
|
||||
int get_random_file(char *out_filename, size_t max_len)
|
||||
{
|
||||
if (audio_file_count == 0)
|
||||
{
|
||||
/* Fallback auf System-Sound, wenn Ordner leer */
|
||||
strncpy(out_filename, cached_404_path, max_len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct fs_dir_t dirp;
|
||||
struct fs_dirent entry;
|
||||
uint32_t target_index = k_cycle_get_32() % audio_file_count;
|
||||
uint32_t current_index = 0;
|
||||
|
||||
fs_dir_t_init(&dirp);
|
||||
if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0)
|
||||
return -ENOENT;
|
||||
|
||||
while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0')
|
||||
{
|
||||
if (entry.type == FS_DIR_ENTRY_FILE)
|
||||
{
|
||||
if (current_index == target_index)
|
||||
{
|
||||
snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name);
|
||||
break;
|
||||
}
|
||||
current_index++;
|
||||
}
|
||||
}
|
||||
fs_pm_closedir(&dirp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void audio_system_ready(void)
|
||||
{
|
||||
k_sem_give(&audio_ready_sem);
|
||||
@@ -250,8 +265,8 @@ void audio_thread(void *arg1, void *arg2, void *arg3)
|
||||
|
||||
bool trigger_started = false;
|
||||
int queued_blocks = 0;
|
||||
uint8_t factor = MIN(255, current_volume * 0xFF / 100);
|
||||
LOG_INF("Volume factor: %u (for volume %d%%)", factor, current_volume);
|
||||
uint8_t factor = MIN(255, app_settings.audio_vol * 0xFF / 100);
|
||||
LOG_INF("Volume factor: %u (for volume %d%%)", factor, app_settings.audio_vol);
|
||||
|
||||
while (!abort_playback)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include <zephyr/fs/littlefs.h>
|
||||
#include <zephyr/sys/byteorder.h>
|
||||
#include <zephyr/drivers/flash.h>
|
||||
#include <zephyr/storage/flash_map.h>
|
||||
#include <zephyr/dfu/flash_img.h>
|
||||
@@ -9,18 +10,23 @@
|
||||
|
||||
LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF);
|
||||
|
||||
#define FS_THREAD_STACK_SIZE 2048
|
||||
#define FS_THREAD_PRIORITY 6
|
||||
|
||||
#define FS_MSGQ_MAX_ITEMS 4
|
||||
#define FS_SLAB_BUF_SIZE 4096
|
||||
#define TAG_FORMAT_VERSION 0x0001
|
||||
#define TAG_MAGIC "TAG!"
|
||||
#define TAG_MAGIC_LEN 4U
|
||||
#define TAG_LEN_FIELD_LEN 2U
|
||||
#define TAG_VERSION_LEN 1U
|
||||
#define TAG_FOOTER_V1_LEN (TAG_VERSION_LEN + TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN)
|
||||
#define TAG_FORMAT_VERSION 0x01
|
||||
|
||||
#define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage)
|
||||
#define SLOT1_ID FIXED_PARTITION_ID(slot1_partition)
|
||||
|
||||
FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data);
|
||||
|
||||
K_MEM_SLAB_DEFINE(file_buffer_slab, FS_SLAB_BUF_SIZE, FS_MSGQ_MAX_ITEMS, 4);
|
||||
K_MSGQ_DEFINE(fs_msgq, sizeof(fs_msg_t), FS_MSGQ_MAX_ITEMS, 4);
|
||||
K_SEM_DEFINE(fs_transfer_done_sem, 0, 1);
|
||||
|
||||
#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash)
|
||||
#if !DT_NODE_EXISTS(QSPI_FLASH_NODE)
|
||||
#error "QSPI Flash alias not defined in devicetree"
|
||||
@@ -33,6 +39,8 @@ static struct k_mutex flash_pm_lock;
|
||||
static struct slot_info_t slot1_info;
|
||||
static struct flash_img_context flash_ctx;
|
||||
|
||||
extern struct k_mem_slab file_buffer_slab;
|
||||
|
||||
static struct fs_mount_t fs_storage_mnt = {
|
||||
.type = FS_LITTLEFS,
|
||||
.fs_data = &fs_storage_data,
|
||||
@@ -40,6 +48,17 @@ static struct fs_mount_t fs_storage_mnt = {
|
||||
.mnt_point = "/lfs",
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
FS_STATE_IDLE,
|
||||
FS_STATE_RECEIVING
|
||||
} fs_thread_state_t;
|
||||
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint16_t total_size;
|
||||
uint16_t version;
|
||||
uint8_t magic[4];
|
||||
} tag_footer_t;
|
||||
|
||||
int fs_init(void) {
|
||||
int rc = fs_mount(&fs_storage_mnt);
|
||||
if (rc < 0) {
|
||||
@@ -121,10 +140,7 @@ int fs_pm_close(struct fs_file_t *file)
|
||||
{
|
||||
LOG_DBG("PM Closing file");
|
||||
int rc = fs_close(file);
|
||||
if (rc == 0)
|
||||
{
|
||||
fs_pm_flash_suspend();
|
||||
}
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -144,10 +160,7 @@ int fs_pm_closedir(struct fs_dir_t *dirp)
|
||||
{
|
||||
LOG_DBG("PM Closing directory");
|
||||
int rc = fs_closedir(dirp);
|
||||
if (rc == 0)
|
||||
{
|
||||
fs_pm_flash_suspend();
|
||||
}
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -196,11 +209,159 @@ int fs_pm_rename(const char *old_path, const char *new_path)
|
||||
return rc;
|
||||
}
|
||||
|
||||
int fs_pm_rm_recursive(char *path_buf, size_t max_len)
|
||||
{
|
||||
struct fs_dirent entry;
|
||||
struct fs_dir_t dir;
|
||||
int rc;
|
||||
|
||||
fs_pm_flash_resume();
|
||||
|
||||
/* 1. Stat prüfen: Ist es eine Datei? */
|
||||
rc = fs_stat(path_buf, &entry);
|
||||
if (rc != 0) {
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Wenn es eine Datei ist, direkt löschen und beenden */
|
||||
if (entry.type == FS_DIR_ENTRY_FILE) {
|
||||
rc = fs_unlink(path_buf);
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 2. Es ist ein Verzeichnis. Schleife bis es leer ist. */
|
||||
size_t orig_len = strlen(path_buf);
|
||||
|
||||
while (1) {
|
||||
fs_dir_t_init(&dir);
|
||||
rc = fs_opendir(&dir, path_buf);
|
||||
if (rc != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
bool found_something = false;
|
||||
|
||||
/* Genau EINEN löschbaren Eintrag suchen */
|
||||
while (1) {
|
||||
rc = fs_readdir(&dir, &entry);
|
||||
if (rc != 0 || entry.name[0] == '\0') {
|
||||
break; /* Ende oder Fehler */
|
||||
}
|
||||
if (strcmp(entry.name, ".") == 0 || strcmp(entry.name, "..") == 0) {
|
||||
continue; /* Ignorieren */
|
||||
}
|
||||
|
||||
found_something = true;
|
||||
break; /* Treffer! Schleife abbrechen. */
|
||||
}
|
||||
|
||||
/* WICHTIG: Das Verzeichnis SOFORT schließen, BEVOR wir rekurieren!
|
||||
* Damit geben wir das File-Handle (NUM_DIRS) an Zephyr zurück. */
|
||||
fs_closedir(&dir);
|
||||
|
||||
if (!found_something || rc != 0) {
|
||||
break; /* Verzeichnis ist nun restlos leer */
|
||||
}
|
||||
|
||||
size_t name_len = strlen(entry.name);
|
||||
if (orig_len + 1 + name_len >= max_len) {
|
||||
rc = -ENAMETOOLONG;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Pfad für das gefundene Kindelement bauen */
|
||||
path_buf[orig_len] = '/';
|
||||
strcpy(&path_buf[orig_len + 1], entry.name);
|
||||
|
||||
/* Rekursiver Aufruf für das Kind */
|
||||
rc = fs_pm_rm_recursive(path_buf, max_len);
|
||||
|
||||
/* Puffer sofort wieder auf unser Verzeichnis zurückschneiden */
|
||||
path_buf[orig_len] = '\0';
|
||||
|
||||
if (rc != 0) {
|
||||
break; /* Abbruch, falls beim Löschen des Kindes ein Fehler auftrat */
|
||||
}
|
||||
}
|
||||
|
||||
/* 3. Das nun restlos leere Verzeichnis selbst löschen */
|
||||
if (rc == 0) {
|
||||
rc = fs_unlink(path_buf);
|
||||
}
|
||||
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
int fs_pm_mkdir_recursive(char *path)
|
||||
{
|
||||
int rc = 0;
|
||||
struct fs_dirent entry;
|
||||
char *p = path;
|
||||
|
||||
/* Führenden Slash überspringen, falls vorhanden (z. B. bei "/lfs") */
|
||||
if (*p == '/') {
|
||||
p++;
|
||||
}
|
||||
|
||||
/* Flash für den gesamten Durchlauf aktivieren */
|
||||
fs_pm_flash_resume();
|
||||
|
||||
while (*p != '\0') {
|
||||
if (*p == '/') {
|
||||
*p = '\0'; /* String temporär am aktuellen Slash terminieren */
|
||||
|
||||
/* Prüfen, ob dieser Pfadabschnitt bereits existiert */
|
||||
rc = fs_stat(path, &entry);
|
||||
|
||||
if (rc == -ENOENT) {
|
||||
/* Existiert nicht -> anlegen */
|
||||
rc = fs_mkdir(path);
|
||||
if (rc != 0) {
|
||||
*p = '/'; /* Bei Fehler Slash wiederherstellen und abbrechen */
|
||||
break;
|
||||
}
|
||||
} else if (rc == 0) {
|
||||
/* Existiert -> prüfen, ob es ein Verzeichnis ist */
|
||||
if (entry.type != FS_DIR_ENTRY_DIR) {
|
||||
rc = -ENOTDIR;
|
||||
*p = '/';
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
/* Anderer Dateisystemfehler */
|
||||
*p = '/';
|
||||
break;
|
||||
}
|
||||
|
||||
*p = '/'; /* Slash für den nächsten Schleifendurchlauf wiederherstellen */
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
/* Letztes Element verarbeiten, falls der Pfad nicht mit '/' endet */
|
||||
if (rc == 0 && p > path && *(p - 1) != '/') {
|
||||
rc = fs_stat(path, &entry);
|
||||
if (rc == -ENOENT) {
|
||||
rc = fs_mkdir(path);
|
||||
} else if (rc == 0) {
|
||||
if (entry.type != FS_DIR_ENTRY_DIR) {
|
||||
rc = -ENOTDIR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Flash am Ende wieder in den Suspend schicken */
|
||||
fs_pm_flash_suspend();
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
|
||||
size_t *audio_limit, size_t *payload_len, bool *has_tag)
|
||||
{
|
||||
uint8_t footer[6];
|
||||
uint16_t tag_len;
|
||||
tag_footer_t footer;
|
||||
|
||||
if (audio_limit == NULL || payload_len == NULL || has_tag == NULL) {
|
||||
return -EINVAL;
|
||||
@@ -210,42 +371,41 @@ static int fs_get_tag_bounds(struct fs_file_t *fp, off_t file_size,
|
||||
*audio_limit = (size_t)file_size;
|
||||
*payload_len = 0U;
|
||||
|
||||
if (file_size < (off_t)TAG_FOOTER_V1_LEN) {
|
||||
if (file_size < (off_t)sizeof(tag_footer_t)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
fs_seek(fp, -(off_t)(TAG_LEN_FIELD_LEN + TAG_MAGIC_LEN), FS_SEEK_END);
|
||||
if (fs_read(fp, footer, sizeof(footer)) != sizeof(footer)) {
|
||||
/* Den 8-Byte-Footer direkt in das Struct einlesen */
|
||||
fs_seek(fp, -(off_t)sizeof(tag_footer_t), FS_SEEK_END);
|
||||
if (fs_read(fp, &footer, sizeof(tag_footer_t)) != sizeof(tag_footer_t)) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (memcmp(&footer[2], TAG_MAGIC, TAG_MAGIC_LEN) != 0) {
|
||||
/* 1. Signatur prüfen */
|
||||
if (memcmp(footer.magic, TAG_MAGIC, 4) != 0) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
tag_len = (uint16_t)footer[0] | ((uint16_t)footer[1] << 8);
|
||||
if (tag_len > (uint16_t)file_size || tag_len < TAG_FOOTER_V1_LEN) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
uint8_t tag_version = 0;
|
||||
fs_seek(fp, -(off_t)TAG_FOOTER_V1_LEN, FS_SEEK_END);
|
||||
if (fs_read(fp, &tag_version, 1) != 1) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EIO;
|
||||
}
|
||||
/* 2. Endianness konvertieren */
|
||||
uint16_t tag_version = sys_le16_to_cpu(footer.version);
|
||||
uint16_t tag_len = sys_le16_to_cpu(footer.total_size);
|
||||
|
||||
/* 3. Version und Größe validieren */
|
||||
if (tag_version != TAG_FORMAT_VERSION) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
if (tag_len > (uint16_t)file_size || tag_len < sizeof(tag_footer_t)) {
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return -EBADMSG;
|
||||
}
|
||||
|
||||
*has_tag = true;
|
||||
*audio_limit = (size_t)file_size - tag_len;
|
||||
*payload_len = tag_len - TAG_FOOTER_V1_LEN;
|
||||
*payload_len = tag_len - sizeof(tag_footer_t);
|
||||
|
||||
fs_seek(fp, 0, FS_SEEK_SET);
|
||||
return 0;
|
||||
@@ -321,51 +481,6 @@ ssize_t fs_tag_read_chunk(struct fs_file_t *fp, void *buffer, size_t len)
|
||||
return fs_read(fp, buffer, len);
|
||||
}
|
||||
|
||||
int fs_tag_open_write(struct fs_file_t *fp)
|
||||
{
|
||||
ssize_t audio_limit = fs_get_audio_data_len(fp);
|
||||
if (audio_limit < 0) {
|
||||
return (int)audio_limit;
|
||||
}
|
||||
fs_seek(fp, audio_limit, FS_SEEK_SET);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ssize_t fs_tag_write_chunk(struct fs_file_t *fp, const void *buffer, size_t len)
|
||||
{
|
||||
return fs_write(fp, buffer, len);
|
||||
}
|
||||
|
||||
int fs_tag_finish_write(struct fs_file_t *fp, uint8_t version, size_t payload_len)
|
||||
{
|
||||
if (version != TAG_FORMAT_VERSION) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
size_t total_footer_len = payload_len + TAG_FOOTER_V1_LEN;
|
||||
if (total_footer_len > UINT16_MAX) {
|
||||
return -EFBIG;
|
||||
}
|
||||
|
||||
if (fs_write(fp, &version, 1) != 1) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
uint8_t len_bytes[2];
|
||||
len_bytes[0] = (uint8_t)(total_footer_len & 0xFFU);
|
||||
len_bytes[1] = (uint8_t)((total_footer_len >> 8) & 0xFFU);
|
||||
if (fs_write(fp, len_bytes, sizeof(len_bytes)) != sizeof(len_bytes)) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
if (fs_write(fp, TAG_MAGIC, TAG_MAGIC_LEN) != TAG_MAGIC_LEN) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
off_t current_pos = fs_tell(fp);
|
||||
return fs_truncate(fp, current_pos);
|
||||
}
|
||||
|
||||
int flash_get_slot_info(slot_info_t *info) {
|
||||
if (slot1_info.size != 0) {
|
||||
*info = slot1_info;
|
||||
@@ -492,4 +607,113 @@ size_t fs_get_internal_flash_page_size(void) {
|
||||
}
|
||||
|
||||
return info.size;
|
||||
}
|
||||
}
|
||||
|
||||
void fs_reset_transfer_sync(void)
|
||||
{
|
||||
k_sem_reset(&fs_transfer_done_sem);
|
||||
}
|
||||
|
||||
void fs_wait_for_transfer_complete(void)
|
||||
{
|
||||
k_sem_take(&fs_transfer_done_sem, K_FOREVER);
|
||||
}
|
||||
|
||||
static void fs_thread_entry(void *p1, void *p2, void *p3)
|
||||
{
|
||||
ARG_UNUSED(p1);
|
||||
ARG_UNUSED(p2);
|
||||
ARG_UNUSED(p3);
|
||||
|
||||
LOG_INF("Filesystem thread started");
|
||||
fs_thread_state_t state = FS_STATE_IDLE;
|
||||
fs_msg_t msg;
|
||||
struct fs_file_t current_file;
|
||||
fs_file_t_init(¤t_file);
|
||||
char current_filename[MAX_PATH_LEN] = {0};
|
||||
|
||||
while (1)
|
||||
{
|
||||
k_timeout_t wait_time = (state == FS_STATE_IDLE) ? K_FOREVER : K_SECONDS(1);
|
||||
int rc = k_msgq_get(&fs_msgq, &msg, wait_time);
|
||||
|
||||
if (rc == -EAGAIN)
|
||||
{
|
||||
if (state == FS_STATE_RECEIVING)
|
||||
{
|
||||
LOG_WRN("FS Transfer Timeout. Aborting and dropping file.");
|
||||
fs_pm_close(¤t_file);
|
||||
fs_pm_unlink(current_filename);
|
||||
state = FS_STATE_IDLE;
|
||||
k_sem_give(&fs_transfer_done_sem);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case FS_STATE_IDLE:
|
||||
if (msg.type == FS_MSG_START)
|
||||
{
|
||||
strncpy(current_filename, msg.start.filename, MAX_PATH_LEN - 1);
|
||||
current_filename[MAX_PATH_LEN - 1] = '\0';
|
||||
|
||||
/* Bei Position 0 (Neuer Datei-Upload) die alte Datei restlos löschen */
|
||||
if (msg.start.start_position == 0) {
|
||||
fs_pm_unlink(current_filename);
|
||||
}
|
||||
|
||||
rc = fs_pm_open(¤t_file, current_filename, FS_O_CREATE | FS_O_WRITE);
|
||||
if (rc == 0) {
|
||||
if (msg.start.start_position > 0) {
|
||||
fs_seek(¤t_file, msg.start.start_position, FS_SEEK_SET);
|
||||
}
|
||||
state = FS_STATE_RECEIVING;
|
||||
} else {
|
||||
LOG_ERR("Failed to open %s: %d", current_filename, rc);
|
||||
}
|
||||
}
|
||||
else if (msg.type == FS_MSG_CHUNK)
|
||||
{
|
||||
/* Chunks im IDLE-Status (z.B. nach Fehler) direkt verwerfen */
|
||||
if (msg.chunk.slab_ptr != NULL)
|
||||
{
|
||||
k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr);
|
||||
}
|
||||
}
|
||||
else if (msg.type == FS_MSG_EOF || msg.type == FS_MSG_ABORT)
|
||||
{
|
||||
/* Verhindert Deadlocks, falls das Öffnen fehlgeschlagen war */
|
||||
k_sem_give(&fs_transfer_done_sem);
|
||||
}
|
||||
break;
|
||||
|
||||
case FS_STATE_RECEIVING:
|
||||
if (msg.type == FS_MSG_CHUNK)
|
||||
{
|
||||
if (msg.chunk.slab_ptr != NULL)
|
||||
{
|
||||
fs_write(¤t_file, msg.chunk.slab_ptr, msg.chunk.chunk_size);
|
||||
k_mem_slab_free(&file_buffer_slab, msg.chunk.slab_ptr);
|
||||
}
|
||||
}
|
||||
else if (msg.type == FS_MSG_EOF)
|
||||
{
|
||||
fs_pm_close(¤t_file);
|
||||
state = FS_STATE_IDLE;
|
||||
k_sem_give(&fs_transfer_done_sem);
|
||||
}
|
||||
else if (msg.type == FS_MSG_ABORT)
|
||||
{
|
||||
fs_pm_close(¤t_file);
|
||||
fs_pm_unlink(current_filename);
|
||||
state = FS_STATE_IDLE;
|
||||
k_sem_give(&fs_transfer_done_sem);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
K_THREAD_DEFINE(fs, FS_THREAD_STACK_SIZE, fs_thread_entry,
|
||||
NULL, NULL, NULL, FS_THREAD_PRIORITY, 0, 0);
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <io.h>
|
||||
#include <usb.h>
|
||||
#include <utils.h>
|
||||
#include <uart.h>
|
||||
#include <settings.h>
|
||||
|
||||
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
|
||||
|
||||
@@ -38,6 +40,13 @@ int main(void)
|
||||
|
||||
int rc;
|
||||
|
||||
rc = app_settings_init();
|
||||
if (rc < 0)
|
||||
{
|
||||
LOG_ERR("Settings initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = fs_init();
|
||||
if (rc < 0)
|
||||
{
|
||||
@@ -52,13 +61,21 @@ int main(void)
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = usb_cdc_acm_init();
|
||||
rc = usb_init();
|
||||
if (rc < 0)
|
||||
{
|
||||
LOG_ERR("USB initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = uart_init();
|
||||
if (rc < 0)
|
||||
{
|
||||
LOG_ERR("UART initialization failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
|
||||
|
||||
rc = io_init();
|
||||
if (rc < 0)
|
||||
{
|
||||
@@ -81,6 +98,7 @@ int main(void)
|
||||
{
|
||||
LOG_INF("Firmware image already confirmed. No need to confirm again.");
|
||||
}
|
||||
|
||||
while (1)
|
||||
{
|
||||
k_sleep(K_FOREVER);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
166
firmware/src/settings.c
Normal file
166
firmware/src/settings.c
Normal file
@@ -0,0 +1,166 @@
|
||||
#include "settings.h"
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/settings/settings.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
LOG_MODULE_REGISTER(app_settings, LOG_LEVEL_DBG);
|
||||
|
||||
/* Initialisierung mit Standardwerten als Fallback */
|
||||
app_settings_t app_settings = {
|
||||
.audio_vol = 50,
|
||||
.play_norepeat = false,
|
||||
.storage_interval_s = 3600
|
||||
};
|
||||
|
||||
/* Flags zur Markierung ungespeicherter Änderungen */
|
||||
static bool dirty_audio_vol = false;
|
||||
static bool dirty_play_norepeat = false;
|
||||
static bool dirty_storage_interval = false;
|
||||
|
||||
/* Workqueue-Objekt für das asynchrone Speichern */
|
||||
static struct k_work_delayable save_work;
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Lese-Handler für Zephyr (Aufruf beim Booten durch settings_load) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static int audio_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||
{
|
||||
if (settings_name_steq(name, "vol", NULL)) {
|
||||
return read_cb(cb_arg, &app_settings.audio_vol, sizeof(app_settings.audio_vol));
|
||||
}
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
static int play_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||
{
|
||||
if (settings_name_steq(name, "norepeat", NULL)) {
|
||||
uint8_t val = 0;
|
||||
int rc = read_cb(cb_arg, &val, sizeof(val));
|
||||
if (rc >= 0) {
|
||||
app_settings.play_norepeat = (val > 0);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
static int sys_settings_set(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg)
|
||||
{
|
||||
if (settings_name_steq(name, "storage_interval", NULL)) {
|
||||
return read_cb(cb_arg, &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s));
|
||||
}
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
/* Registrierung der Namespaces für das automatische Laden */
|
||||
SETTINGS_STATIC_HANDLER_DEFINE(audio, "audio", NULL, audio_settings_set, NULL, NULL);
|
||||
SETTINGS_STATIC_HANDLER_DEFINE(play, "play", NULL, play_settings_set, NULL, NULL);
|
||||
SETTINGS_STATIC_HANDLER_DEFINE(sys, "settings", NULL, sys_settings_set, NULL, NULL);
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Schreib-Logik (Asynchron über System Workqueue) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
static void save_work_handler(struct k_work *work)
|
||||
{
|
||||
if (dirty_audio_vol) {
|
||||
settings_save_one("audio/vol", &app_settings.audio_vol, sizeof(app_settings.audio_vol));
|
||||
dirty_audio_vol = false;
|
||||
LOG_DBG("NVS Write: audio/vol = %d", app_settings.audio_vol);
|
||||
}
|
||||
if (dirty_play_norepeat) {
|
||||
uint8_t val = app_settings.play_norepeat ? 1 : 0;
|
||||
settings_save_one("play/norepeat", &val, sizeof(val));
|
||||
dirty_play_norepeat = false;
|
||||
LOG_DBG("NVS Write: play/norepeat = %d", val);
|
||||
}
|
||||
if (dirty_storage_interval) {
|
||||
settings_save_one("settings/storage_interval", &app_settings.storage_interval_s, sizeof(app_settings.storage_interval_s));
|
||||
dirty_storage_interval = false;
|
||||
LOG_DBG("NVS Write: settings/storage_interval = %d", app_settings.storage_interval_s);
|
||||
}
|
||||
}
|
||||
|
||||
static void schedule_save(void)
|
||||
{
|
||||
if (app_settings.storage_interval_s == 0) {
|
||||
/* Direkter Schreibvorgang, Work sofort einreihen */
|
||||
k_work_cancel_delayable(&save_work);
|
||||
k_work_submit(&save_work.work);
|
||||
} else {
|
||||
/* Timer neustarten (überschreibt laufenden Countdown) */
|
||||
k_work_reschedule(&save_work, K_SECONDS(app_settings.storage_interval_s));
|
||||
}
|
||||
}
|
||||
|
||||
void app_settings_save_pending_now(void)
|
||||
{
|
||||
struct k_work_sync sync;
|
||||
k_work_cancel_delayable_sync(&save_work, &sync);
|
||||
save_work_handler(&save_work.work);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Setter (API für das Protokoll) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
void app_settings_set_audio_vol(uint8_t vol)
|
||||
{
|
||||
if (vol > 100) vol = 100;
|
||||
|
||||
if (app_settings.audio_vol != vol) {
|
||||
app_settings.audio_vol = vol;
|
||||
dirty_audio_vol = true;
|
||||
schedule_save();
|
||||
}
|
||||
}
|
||||
|
||||
void app_settings_set_play_norepeat(bool norepeat)
|
||||
{
|
||||
if (app_settings.play_norepeat != norepeat) {
|
||||
app_settings.play_norepeat = norepeat;
|
||||
dirty_play_norepeat = true;
|
||||
schedule_save();
|
||||
}
|
||||
}
|
||||
|
||||
void app_settings_set_storage_interval(uint32_t interval_s)
|
||||
{
|
||||
if (interval_s > 7200) interval_s = 7200;
|
||||
|
||||
if (app_settings.storage_interval_s != interval_s) {
|
||||
app_settings.storage_interval_s = interval_s;
|
||||
dirty_storage_interval = true;
|
||||
schedule_save();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* Initialisierung */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
int app_settings_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
k_work_init_delayable(&save_work, save_work_handler);
|
||||
|
||||
err = settings_subsys_init();
|
||||
if (err) {
|
||||
LOG_ERR("settings_subsys_init failed (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
/* Lädt alle Werte aus dem NVS in den RAM */
|
||||
err = settings_load();
|
||||
if (err) {
|
||||
LOG_ERR("settings_load failed (err %d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
LOG_INF("Settings init ok. Vol=%d, NoRepeat=%d, Interval=%d",
|
||||
app_settings.audio_vol, app_settings.play_norepeat, app_settings.storage_interval_s);
|
||||
|
||||
return 0;
|
||||
}
|
||||
135
firmware/src/uart.c
Normal file
135
firmware/src/uart.c
Normal file
@@ -0,0 +1,135 @@
|
||||
// uart.c
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/sys/ring_buffer.h>
|
||||
|
||||
LOG_MODULE_REGISTER(uart, LOG_LEVEL_INF);
|
||||
|
||||
#define RX_RING_BUF_SIZE 1024
|
||||
#define TX_RING_BUF_SIZE 1024
|
||||
|
||||
const struct device *const uart_dev = DEVICE_DT_GET_ONE(zephyr_cdc_acm_uart);
|
||||
|
||||
RING_BUF_ITEM_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
|
||||
RING_BUF_ITEM_DECLARE(tx_ringbuf, TX_RING_BUF_SIZE);
|
||||
K_SEM_DEFINE(tx_done_sem, 0, 1);
|
||||
K_SEM_DEFINE(rx_ready_sem, 0, 1);
|
||||
|
||||
static void uart_isr(const struct device *dev, void *user_data)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
|
||||
if (!uart_irq_update(dev))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (uart_irq_rx_ready(dev))
|
||||
{
|
||||
uint8_t *data_ptr;
|
||||
uint32_t claimed_len;
|
||||
int recv_len;
|
||||
|
||||
claimed_len = ring_buf_put_claim(&rx_ringbuf, &data_ptr, RX_RING_BUF_SIZE);
|
||||
|
||||
if (claimed_len > 0)
|
||||
{
|
||||
recv_len = uart_fifo_read(dev, data_ptr, claimed_len);
|
||||
ring_buf_put_finish(&rx_ringbuf, recv_len);
|
||||
if (recv_len > 0)
|
||||
{
|
||||
k_sem_give(&rx_ready_sem);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
uart_irq_rx_disable(dev);
|
||||
}
|
||||
}
|
||||
if (uart_irq_tx_ready(dev))
|
||||
{
|
||||
uint8_t *data_ptr;
|
||||
uint32_t claim_len;
|
||||
int written;
|
||||
|
||||
claim_len = ring_buf_get_claim(&tx_ringbuf, &data_ptr, ring_buf_size_get(&tx_ringbuf));
|
||||
|
||||
if (claim_len > 0)
|
||||
{
|
||||
written = uart_fifo_fill(dev, data_ptr, claim_len);
|
||||
ring_buf_get_finish(&tx_ringbuf, written);
|
||||
}
|
||||
else
|
||||
{
|
||||
uart_irq_tx_disable(dev);
|
||||
}
|
||||
k_sem_give(&tx_done_sem);
|
||||
}
|
||||
}
|
||||
|
||||
int uart_init(void)
|
||||
{
|
||||
if (!device_is_ready(uart_dev))
|
||||
{
|
||||
LOG_ERR("UART device not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
uart_irq_callback_set(uart_dev, uart_isr);
|
||||
uart_irq_rx_enable(uart_dev);
|
||||
|
||||
LOG_INF("UART device initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int uart_write(const uint8_t *data, size_t len, k_timeout_t timeout)
|
||||
{
|
||||
size_t written_total = 0;
|
||||
k_sem_reset(&tx_done_sem);
|
||||
|
||||
while (written_total < len)
|
||||
{
|
||||
uint32_t written = ring_buf_put(&tx_ringbuf, &data[written_total], len - written_total);
|
||||
written_total += written;
|
||||
|
||||
if (written > 0)
|
||||
{
|
||||
uart_irq_tx_enable(uart_dev);
|
||||
}
|
||||
|
||||
if (written_total < len)
|
||||
{
|
||||
int ret = k_sem_take(&tx_done_sem, timeout);
|
||||
if (ret != 0)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
return written_total;
|
||||
}
|
||||
|
||||
int uart_write_string(const char *str, k_timeout_t timeout)
|
||||
{
|
||||
return uart_write((const uint8_t *)str, strlen(str), timeout);
|
||||
}
|
||||
|
||||
int uart_read(uint8_t *data, size_t len, k_timeout_t timeout)
|
||||
{
|
||||
uint32_t read_len = ring_buf_get(&rx_ringbuf, data, len);
|
||||
|
||||
if (read_len == 0 && !K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
|
||||
k_sem_reset(&rx_ready_sem);
|
||||
if (ring_buf_is_empty(&rx_ringbuf)) {
|
||||
if (k_sem_take(&rx_ready_sem, timeout) != 0) {
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
read_len = ring_buf_get(&rx_ringbuf, data, len);
|
||||
}
|
||||
if (read_len > 0) {
|
||||
uart_irq_rx_enable(uart_dev);
|
||||
}
|
||||
return read_len;
|
||||
}
|
||||
@@ -1,202 +1,181 @@
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/sys/ring_buffer.h> /* NEU */
|
||||
#include <errno.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
#include <zephyr/usb/usbd.h>
|
||||
|
||||
#include <io.h>
|
||||
#include "usb.h"
|
||||
|
||||
#define RX_RING_BUF_SIZE 1024
|
||||
#define USB_MANUFACTURER_STRING "Iten Engineering"
|
||||
#define USB_PRODUCT_STRING "Edis Buzzer"
|
||||
#define USB_DEVICE_VID 0x1209
|
||||
#define USB_DEVICE_PID 0xEDED
|
||||
|
||||
LOG_MODULE_REGISTER(usb, LOG_LEVEL_INF);
|
||||
|
||||
K_SEM_DEFINE(usb_rx_sem, 0, 1);
|
||||
K_SEM_DEFINE(usb_tx_sem, 0, 1);
|
||||
K_SEM_DEFINE(dtr_active_sem, 0, 1);
|
||||
static uint32_t dtr_active = 0U;
|
||||
|
||||
#define UART_NODE DT_ALIAS(usb_uart)
|
||||
const struct device *cdc_dev = DEVICE_DT_GET(UART_NODE);
|
||||
static volatile bool rx_interrupt_enabled = false;
|
||||
USBD_DEVICE_DEFINE(cdc_acm_serial,
|
||||
DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)),
|
||||
USB_DEVICE_VID, USB_DEVICE_PID);
|
||||
|
||||
RING_BUF_DECLARE(rx_ringbuf, RX_RING_BUF_SIZE);
|
||||
USBD_DESC_LANG_DEFINE(cdc_acm_lang);
|
||||
USBD_DESC_MANUFACTURER_DEFINE(cdc_acm_mfr, USB_MANUFACTURER_STRING);
|
||||
USBD_DESC_PRODUCT_DEFINE(cdc_acm_product, USB_PRODUCT_STRING);
|
||||
IF_ENABLED(CONFIG_HWINFO, (USBD_DESC_SERIAL_NUMBER_DEFINE(cdc_acm_sn)));
|
||||
|
||||
static void cdc_acm_irq_cb(const struct device *dev, void *user_data)
|
||||
USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration");
|
||||
USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "HS Configuration");
|
||||
|
||||
USBD_CONFIGURATION_DEFINE(fs_config, 0U, 125U, &fs_cfg_desc);
|
||||
USBD_CONFIGURATION_DEFINE(hs_config, 0U, 125U, &hs_cfg_desc);
|
||||
|
||||
static void fix_code_triple(struct usbd_context *uds_ctx, enum usbd_speed speed)
|
||||
{
|
||||
ARG_UNUSED(user_data);
|
||||
|
||||
if (!uart_irq_update(dev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (uart_irq_rx_ready(dev)) {
|
||||
uint8_t buffer[64];
|
||||
uint32_t space = ring_buf_space_get(&rx_ringbuf);
|
||||
|
||||
if (space == 0) {
|
||||
/* Backpressure anwenden: Ringpuffer ist voll.
|
||||
Interrupt deaktivieren, damit Daten im HW-FIFO bleiben
|
||||
und der USB-Stack den Host drosselt (NAK). */
|
||||
uart_irq_rx_disable(dev);
|
||||
} else {
|
||||
int to_read = MIN(sizeof(buffer), space);
|
||||
int len = uart_fifo_read(dev, buffer, to_read);
|
||||
|
||||
if (len > 0) {
|
||||
ring_buf_put(&rx_ringbuf, buffer, len);
|
||||
k_sem_give(&usb_rx_sem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uart_irq_tx_ready(dev)) {
|
||||
uart_irq_tx_disable(dev);
|
||||
k_sem_give(&usb_tx_sem);
|
||||
}
|
||||
if (IS_ENABLED(CONFIG_USBD_CDC_ACM_CLASS) ||
|
||||
IS_ENABLED(CONFIG_USBD_CDC_ECM_CLASS) ||
|
||||
IS_ENABLED(CONFIG_USBD_CDC_NCM_CLASS) ||
|
||||
IS_ENABLED(CONFIG_USBD_MIDI2_CLASS) ||
|
||||
IS_ENABLED(CONFIG_USBD_AUDIO2_CLASS) ||
|
||||
IS_ENABLED(CONFIG_USBD_VIDEO_CLASS)) {
|
||||
usbd_device_set_code_triple(uds_ctx, speed,
|
||||
USB_BCC_MISCELLANEOUS, 0x02, 0x01);
|
||||
} else {
|
||||
usbd_device_set_code_triple(uds_ctx, speed, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool usb_wait_for_data(k_timeout_t timeout)
|
||||
static void usbd_msg_cb(struct usbd_context *const ctx, const struct usbd_msg *const msg)
|
||||
{
|
||||
if (!ring_buf_is_empty(&rx_ringbuf)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Wenn der Puffer leer ist, sicherstellen, dass der RX-Interrupt
|
||||
aktiviert ist, da sonst keine neuen Daten empfangen werden können. */
|
||||
if (device_is_ready(cdc_dev)) {
|
||||
uart_irq_rx_enable(cdc_dev);
|
||||
}
|
||||
|
||||
return (k_sem_take(&usb_rx_sem, timeout) == 0);
|
||||
int err;
|
||||
|
||||
LOG_DBG("USBD message: %s", usbd_msg_type_string(msg->type));
|
||||
|
||||
if (usbd_can_detect_vbus(ctx)) {
|
||||
if (msg->type == USBD_MSG_VBUS_READY) {
|
||||
err = usbd_enable(ctx);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to enable USB device (%d)", err);
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->type == USBD_MSG_VBUS_REMOVED) {
|
||||
err = usbd_disable(ctx);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to disable USB device (%d)", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg->type == USBD_MSG_CDC_ACM_CONTROL_LINE_STATE) {
|
||||
uint32_t rts = 0U;
|
||||
uint32_t dcd = 0U;
|
||||
uint32_t dsr = 0U;
|
||||
|
||||
if (msg->dev != NULL) {
|
||||
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_RTS, &rts);
|
||||
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DTR, &dtr_active);
|
||||
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DCD, &dcd);
|
||||
(void)uart_line_ctrl_get(msg->dev, UART_LINE_CTRL_DSR, &dsr);
|
||||
LOG_DBG("CDC ACM RTS: %u, DTR: %u, DCD: %u, DSR: %u", rts, dtr_active, dcd, dsr);
|
||||
if (dtr_active) {
|
||||
k_sem_give(&dtr_active_sem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool usb_read_byte(uint8_t *c)
|
||||
int usb_init(void)
|
||||
{
|
||||
int ret = ring_buf_get(&rx_ringbuf, c, 1);
|
||||
return (ret > 0);
|
||||
int err;
|
||||
|
||||
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_lang);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add language descriptor (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_mfr);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add manufacturer descriptor (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_product);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add product descriptor (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
IF_ENABLED(CONFIG_HWINFO, (
|
||||
err = usbd_add_descriptor(&cdc_acm_serial, &cdc_acm_sn);
|
||||
))
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add serial-number descriptor (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (USBD_SUPPORTS_HIGH_SPEED && usbd_caps_speed(&cdc_acm_serial) == USBD_SPEED_HS) {
|
||||
err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_HS, &hs_config);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add HS configuration (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_HS, 1);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to register HS CDC ACM class (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
fix_code_triple(&cdc_acm_serial, USBD_SPEED_HS);
|
||||
}
|
||||
|
||||
err = usbd_add_configuration(&cdc_acm_serial, USBD_SPEED_FS, &fs_config);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to add FS configuration (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = usbd_register_class(&cdc_acm_serial, "cdc_acm_0", USBD_SPEED_FS, 1);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to register FS CDC ACM class (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
fix_code_triple(&cdc_acm_serial, USBD_SPEED_FS);
|
||||
|
||||
err = usbd_msg_register_cb(&cdc_acm_serial, usbd_msg_cb);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to register USBD callback (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = usbd_init(&cdc_acm_serial);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to initialize USBD (%d)", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!usbd_can_detect_vbus(&cdc_acm_serial)) {
|
||||
err = usbd_enable(&cdc_acm_serial);
|
||||
if (err) {
|
||||
LOG_ERR("Failed to enable USBD (%d)", err);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INF("USBD CDC ACM initialized");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int usb_read_buffer(uint8_t *buf, size_t max_len)
|
||||
void usb_wait_for_dtr(void)
|
||||
{
|
||||
int ret = ring_buf_get(&rx_ringbuf, buf, max_len);
|
||||
return ret;
|
||||
k_sem_take(&dtr_active_sem, K_FOREVER);
|
||||
}
|
||||
|
||||
void usb_resume_rx(void)
|
||||
bool usb_dtr_active(void)
|
||||
{
|
||||
if (device_is_ready(cdc_dev)) {
|
||||
uart_irq_rx_enable(cdc_dev);
|
||||
}
|
||||
}
|
||||
|
||||
void usb_write_byte(uint8_t c)
|
||||
{
|
||||
if (!device_is_ready(cdc_dev)) {
|
||||
return;
|
||||
}
|
||||
uart_poll_out(cdc_dev, c);
|
||||
}
|
||||
|
||||
int usb_write_buffer(const uint8_t *buf, size_t len)
|
||||
{
|
||||
if (!device_is_ready(cdc_dev))
|
||||
{
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
size_t written;
|
||||
while (len > 0)
|
||||
{
|
||||
written = uart_fifo_fill(cdc_dev, buf, len);
|
||||
|
||||
len -= written;
|
||||
buf += written;
|
||||
|
||||
uart_irq_tx_enable(cdc_dev);
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
|
||||
if (k_sem_take(&usb_tx_sem, K_MSEC(100)) != 0)
|
||||
{
|
||||
LOG_WRN("USB TX timeout - consumer not reading?");
|
||||
return -ETIMEDOUT;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void usb_flush_rx(void)
|
||||
{
|
||||
uint8_t dummy;
|
||||
if (!device_is_ready(cdc_dev)) return;
|
||||
|
||||
/* Hardware-FIFO leeren, falls Reste vorhanden */
|
||||
while (uart_fifo_read(cdc_dev, &dummy, 1) > 0);
|
||||
|
||||
/* Ringpuffer und Semaphore zurücksetzen */
|
||||
ring_buf_reset(&rx_ringbuf);
|
||||
k_sem_reset(&usb_rx_sem);
|
||||
}
|
||||
|
||||
static void usb_status_cb(enum usb_dc_status_code cb_status, const uint8_t *param)
|
||||
{
|
||||
switch (cb_status) {
|
||||
case USB_DC_CONNECTED:
|
||||
/* VBUS wurde vom Zephyr-Stack erkannt */
|
||||
LOG_DBG("VBUS detected, USB device connected");
|
||||
break;
|
||||
case USB_DC_CONFIGURED:
|
||||
LOG_DBG("USB device configured by host");
|
||||
io_usb_status(true);
|
||||
if (device_is_ready(cdc_dev)) {
|
||||
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DCD, 1);
|
||||
(void)uart_line_ctrl_set(cdc_dev, UART_LINE_CTRL_DSR, 1);
|
||||
|
||||
/* Interrupt-Handler binden und initial aktivieren */
|
||||
uart_irq_callback_set(cdc_dev, cdc_acm_irq_cb);
|
||||
uart_irq_rx_enable(cdc_dev);
|
||||
}
|
||||
break;
|
||||
case USB_DC_DISCONNECTED:
|
||||
/* Kabel wurde gezogen */
|
||||
LOG_DBG("VBUS removed, USB device disconnected");
|
||||
if (device_is_ready(cdc_dev)) {
|
||||
uart_irq_rx_disable(cdc_dev);
|
||||
}
|
||||
io_usb_status(false);
|
||||
break;
|
||||
case USB_DC_RESET:
|
||||
LOG_DBG("USB bus reset");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int usb_cdc_acm_init(void)
|
||||
{
|
||||
LOG_DBG("Initializing USB Stack...");
|
||||
|
||||
/* Zephyr-Treiber registrieren. Verbraucht keinen Strom ohne VBUS. */
|
||||
int ret = usb_enable(usb_status_cb);
|
||||
if (ret != 0) {
|
||||
LOG_ERR("Failed to enable USB (%d)", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
#if DT_NODE_HAS_STATUS(DT_NODELABEL(cdc_acm_uart0), okay)
|
||||
const struct device *cdc_dev = DEVICE_DT_GET(DT_NODELABEL(cdc_acm_uart0));
|
||||
|
||||
if (!device_is_ready(cdc_dev)) {
|
||||
LOG_ERR("CDC ACM device not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
#else
|
||||
LOG_ERR("CDC ACM UART device not found in devicetree");
|
||||
return -ENODEV;
|
||||
#endif
|
||||
|
||||
LOG_DBG("USB Stack enabled and waiting for VBUS in hardware");
|
||||
return 0;
|
||||
return dtr_active != 0U;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
#ifndef USB_CDC_ACM_H
|
||||
#define USB_CDC_ACM_H
|
||||
|
||||
/**
|
||||
* @brief Initializes the USB CDC ACM device
|
||||
* @return 0 on success, negative error code on failure
|
||||
*/
|
||||
int usb_cdc_acm_init(void);
|
||||
|
||||
/**
|
||||
* @brief Waits until data is available in the USB RX FIFO or the timeout expires
|
||||
* @param timeout Maximum time to wait for data. Use K_FOREVER for infinite wait.
|
||||
* @return true if data is available, false if timeout occurred
|
||||
*/
|
||||
bool usb_wait_for_data(k_timeout_t timeout);
|
||||
|
||||
/**
|
||||
* @brief Reads a single character from the USB RX FIFO
|
||||
* @param c Pointer to store the read character
|
||||
* @return true if a character was read, false if no data was available
|
||||
*/
|
||||
bool usb_read_byte(uint8_t *c);
|
||||
|
||||
/**
|
||||
* @brief Reads a block of data from the USB RX FIFO
|
||||
* @param buf Buffer to store the read data
|
||||
* @param max_len Maximum number of bytes to read
|
||||
* @return Number of bytes read
|
||||
*/
|
||||
int usb_read_buffer(uint8_t *buf, size_t max_len);
|
||||
|
||||
/**
|
||||
* @brief Resumes the USB RX interrupt when all data has been read
|
||||
*/
|
||||
void usb_resume_rx(void);
|
||||
|
||||
/**
|
||||
* @brief Writes a single character to the USB TX FIFO
|
||||
* @param c Character to write
|
||||
*/
|
||||
void usb_write_byte(uint8_t c);
|
||||
|
||||
/**
|
||||
* @brief Writes a block of data to the USB TX FIFO
|
||||
* @param buf Buffer containing the data to write
|
||||
* @param len Number of bytes to write
|
||||
*/
|
||||
int usb_write_buffer(const uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* @brief Flushes the USB RX FIFO
|
||||
*/
|
||||
void usb_flush_rx(void);
|
||||
|
||||
#endif // USB_CDC_ACM_H
|
||||
@@ -4,6 +4,8 @@
|
||||
#include <zephyr/logging/log_ctrl.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
#include <settings.h>
|
||||
|
||||
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
||||
#include <hal/nrf_power.h>
|
||||
#elif IS_ENABLED(CONFIG_SOC_SERIES_STM32G0X)
|
||||
@@ -17,6 +19,7 @@ LOG_MODULE_REGISTER(utils, LOG_LEVEL_DBG);
|
||||
|
||||
void reboot_with_status(uint8_t status)
|
||||
{
|
||||
app_settings_save_pending_now();
|
||||
#if IS_ENABLED(CONFIG_SOC_SERIES_NRF52X)
|
||||
/* Korrigierter Aufruf mit Register-Index 0 */
|
||||
nrf_power_gpregret_set(NRF_POWER, REBOOT_STATUS_REG_IDX, (uint32_t)status);
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CONFIG_LOG=y
|
||||
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
|
||||
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
|
||||
|
||||
Reference in New Issue
Block a user