diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt index 63019d5..82f9832 100644 --- a/firmware/CMakeLists.txt +++ b/firmware/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(app PRIVATE src/main.c src/fs.c src/io.c + src/audio.c src/usb.c src/protocol.c ) diff --git a/firmware/boards/nrf52840dk_nrf52840.overlay b/firmware/boards/nrf52840dk_nrf52840.overlay index d7f6c57..6a4d353 100644 --- a/firmware/boards/nrf52840dk_nrf52840.overlay +++ b/firmware/boards/nrf52840dk_nrf52840.overlay @@ -7,6 +7,7 @@ buzzer-button = &button0; audio-i2s = &i2s0; usb-uart = &cdc_acm_uart0; + qspi-flash = &mx25r64; }; chosen { diff --git a/firmware/src/audio.c b/firmware/src/audio.c index b416293..cdd8012 100644 --- a/firmware/src/audio.c +++ b/firmware/src/audio.c @@ -2,19 +2,24 @@ #include #include #include +#include #include #include #include #include -LOG_MODULE_REGISTER(audio, LOG_LEVEL_INF); +LOG_MODULE_REGISTER(audio, LOG_LEVEL_DBG); + +/* Dauer eines Blocks in ms (4096 Bytes / (16kHz * 2 Kanäle * 2 Bytes)) = 64 ms */ +#define BLOCK_DURATION_MS 64 +#define MAX_WAIT_TIME_MS (3 * BLOCK_DURATION_MS) /* Slab für I2S. Keine weiteren Queues oder Threads nötig. */ K_MEM_SLAB_DEFINE(audio_slab, AUDIO_BLOCK_SIZE, AUDIO_BLOCK_COUNT, 4); -/* Message Queue für Play-Kommandos (Pfade zu Dateien, max 64 Zeichen) */ -K_MSGQ_DEFINE(audio_play_msgq, 64, 4, 4); +/* Message Queue für Play-Kommandos (auf 10 erhöht für Aneinanderreihung) */ +K_MSGQ_DEFINE(audio_play_msgq, 64, 10, 4); /* Startup-Sicherung */ K_SEM_DEFINE(audio_ready_sem, 0, 1); @@ -25,48 +30,84 @@ K_SEM_DEFINE(audio_ready_sem, 0, 1); #endif static const struct device *const i2s_dev = DEVICE_DT_GET(I2S_NODE); +static volatile bool abort_playback = false; +static char next_random_filename[64] = {0}; -int get_random_file(const char *path, char *out_filename, size_t max_len) +static uint32_t audio_file_count = 0; +static char cached_404_path[] = "/lfs/sys/404"; + +void audio_refresh_file_count(void) { struct fs_dir_t dirp; struct fs_dirent entry; - int file_count = 0; - int rc; + uint32_t count = 0; fs_dir_t_init(&dirp); - rc = fs_opendir(&dirp, path); - if (rc < 0) - return rc; - - while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') + if (fs_pm_opendir(&dirp, AUDIO_PATH) < 0) { - if (entry.type == FS_DIR_ENTRY_FILE) - file_count++; + audio_file_count = 0; + return; } - fs_closedir(&dirp); - if (file_count == 0) - return -ENOENT; - - uint32_t random_index = k_cycle_get_32() % file_count; - rc = fs_opendir(&dirp, path); - if (rc < 0) - return rc; - - int current_index = 0; while (fs_readdir(&dirp, &entry) == 0 && entry.name[0] != '\0') { if (entry.type == FS_DIR_ENTRY_FILE) { - if (current_index == random_index) + count++; + } + } + fs_pm_closedir(&dirp); + audio_file_count = count; + LOG_INF("Audio cache refreshed: %u files found in %s", count, AUDIO_PATH); +} + +static void wait_for_i2s_drain(void) +{ + /* Maximale Wartezeit berechnen (8 Blöcke * 64ms = 512ms + Toleranz) */ + int64_t deadline = k_uptime_get() + (AUDIO_BLOCK_COUNT * BLOCK_DURATION_MS) + 100; + + while (k_mem_slab_num_free_get(&audio_slab) < AUDIO_BLOCK_COUNT) + { + if (k_uptime_get() >= deadline) + { + LOG_WRN("Timeout waiting for I2S drain"); + break; + } + k_sleep(K_MSEC(10)); + } +} + +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", path, entry.name); + snprintf(out_filename, max_len, "%s/%s", AUDIO_PATH, entry.name); break; } current_index++; } } - fs_closedir(&dirp); + fs_pm_closedir(&dirp); return 0; } @@ -75,115 +116,189 @@ void audio_system_ready(void) k_sem_give(&audio_ready_sem); } +void audio_stop(void) +{ + LOG_DBG("Playback abort requested"); + abort_playback = true; + k_msgq_purge(&audio_play_msgq); + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); +} + +void audio_play(const char *filename) +{ + char buf[64] = {0}; + if (filename != NULL) + { + strncpy(buf, filename, sizeof(buf) - 1); + } + + if (k_msgq_put(&audio_play_msgq, &buf, K_NO_WAIT) < 0) + { + LOG_WRN("Audio queue full, dropping request"); + } +} + void audio_thread(void *arg1, void *arg2, void *arg3) { LOG_DBG("Audio thread started"); k_sem_take(&audio_ready_sem, K_FOREVER); + /* Ersten zufälligen Dateinamen beim Booten vorab cachen */ + get_random_file(next_random_filename, sizeof(next_random_filename)); + char filename[64]; while (1) { - /* 1. Auf Play-Kommando warten */ - k_msgq_get(&audio_play_msgq, &filename, K_FOREVER); - - /* Sicherstellen, dass die I2S-Hardware nach einem vorherigen DRAIN - oder bei extrem schnellem Neudrücken garantiert gestoppt und leer ist. */ - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); - - /* 2. Datei bestimmen */ - if (filename[0] == '\0') + if (k_msgq_get(&audio_play_msgq, &filename, K_FOREVER) == 0) { - if (get_random_file(AUDIO_PATH, filename, sizeof(filename)) < 0) + fs_pm_flash_resume(); + + abort_playback = false; + + /* 2. Datei bestimmen (aus Cache oder synchron als Fallback) */ + if (filename[0] == '\0') { - LOG_ERR("No file found in %s", AUDIO_PATH); + if (next_random_filename[0] != '\0') + { + /* Cache Hit: Sofort in den lokalen Puffer übernehmen */ + strncpy(filename, next_random_filename, sizeof(filename)); + next_random_filename[0] = '\0'; /* Cache als 'leer' markieren */ + } + else + { + /* Cache Miss (z.B. bei extrem schnellem Dauerfeuer): Synchron suchen */ + if (get_random_file(filename, sizeof(filename)) < 0) + { + LOG_ERR("No file found in %s", AUDIO_PATH); + continue; + } + } + } + + struct fs_file_t file; + fs_file_t_init(&file); + if (fs_open(&file, filename, FS_O_READ) < 0) + { + LOG_ERR("Failed to open %s", filename); continue; } - } - struct fs_file_t file; - fs_file_t_init(&file); - if (fs_open(&file, filename, FS_O_READ) < 0) - { - LOG_ERR("Failed to open %s", filename); - continue; - } + LOG_INF("Playing: %s", filename); + io_status(true); - LOG_INF("Playing: %s", filename); - io_status(true); - bool i2s_started = false; - bool aborted = false; + bool trigger_started = false; + int queued_blocks = 0; - /* 3. Synchrone Lese- und Abspiel-Schleife */ - while (1) - { - /* WICHTIG: Prüfen, ob während des Abspielens ein neues Kommando in die Queue gelegt wurde */ - if (k_msgq_num_used_get(&audio_play_msgq) > 0) + while (!abort_playback) { - LOG_DBG("New play request received, aborting current playback"); - aborted = true; - break; + void *block; + /* Self-Healing Timeout bei I2S-Hängern */ + if (k_mem_slab_alloc(&audio_slab, &block, K_MSEC(MAX_WAIT_TIME_MS)) < 0) + { + LOG_ERR("I2S stall or slab timeout - resetting I2S"); + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + audio_init(); // Setzt Hardware hart zurück + break; + } + + if (abort_playback) + { + k_mem_slab_free(&audio_slab, &block); + break; + } + + ssize_t bytes_read = fs_read(&file, block, AUDIO_BLOCK_SIZE / 2); + + if (bytes_read <= 0) + { + k_mem_slab_free(&audio_slab, &block); + break; + } + + /* In-Place Konvertierung Mono -> Stereo */ + int16_t *samples = (int16_t *)block; + int samples_read = bytes_read / 2; + + for (int i = samples_read - 1; i >= 0; i--) + { + int16_t sample = samples[i]; + samples[i * 2] = sample; + samples[i * 2 + 1] = sample; + } + + /* Bei partiellem Block (Dateiende) den Rest mit Stille füllen */ + if (bytes_read < (AUDIO_BLOCK_SIZE / 2)) + { + size_t valid_bytes = bytes_read * 2; + memset((uint8_t *)block + valid_bytes, 0, AUDIO_BLOCK_SIZE - valid_bytes); + } + + /* Block in die DMA-Queue schieben */ + if (i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE) < 0) + { + k_mem_slab_free(&audio_slab, &block); + break; + } + + /* HIER werden die Variablen verwendet: */ + queued_blocks++; + + /* Regulärer Start: Erst wenn 2 Blöcke in der DMA-Queue liegen */ + if (!trigger_started && queued_blocks >= 2) + { + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); + trigger_started = true; + LOG_DBG("I2S transmission started after queuing %d blocks", queued_blocks); + } + + /* Kurze Datei (kleiner als 1 Block): Sofort starten und Schleife verlassen */ + if (bytes_read < (AUDIO_BLOCK_SIZE / 2)) + { + if (!trigger_started) + { + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); + trigger_started = true; + LOG_DBG("I2S transmission started for short file (bytes read: %zd)", bytes_read); + } + break; + } } - void *block; - if (k_mem_slab_alloc(&audio_slab, &block, K_FOREVER) != 0) + if (abort_playback) { - break; + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); + LOG_DBG("Playback aborted via audio_stop()"); + } + else + { + if (k_msgq_num_used_get(&audio_play_msgq) > 0) + { + LOG_DBG("Play request pending, skipping DRAIN"); + } + else + { + LOG_DBG("Sample finished, starting DRAIN"); + i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); + wait_for_i2s_drain(); + } } - ssize_t bytes_read = fs_read(&file, block, AUDIO_BLOCK_SIZE / 2); - - if (bytes_read <= 0) + fs_close(&file); + fs_pm_flash_suspend(); + if (k_msgq_num_used_get(&audio_play_msgq) == 0) { - k_mem_slab_free(&audio_slab, &block); - break; /* EOF oder Fehler */ + io_status(false); } - /* In-Place Konvertierung Mono -> Stereo */ - int16_t *samples = (int16_t *)block; - int samples_read = bytes_read / 2; - - for (int i = samples_read - 1; i >= 0; i--) + if (k_msgq_num_used_get(&audio_play_msgq) == 0 && next_random_filename[0] == '\0') { - int16_t sample = samples[i]; - samples[i * 2] = sample; - samples[i * 2 + 1] = sample; - } - - if (bytes_read < (AUDIO_BLOCK_SIZE / 2)) - { - size_t valid_bytes = bytes_read * 2; - memset((uint8_t *)block + valid_bytes, 0, AUDIO_BLOCK_SIZE - valid_bytes); - } - - if (i2s_write(i2s_dev, block, AUDIO_BLOCK_SIZE) < 0) - { - k_mem_slab_free(&audio_slab, &block); - break; - } - - if (!i2s_started) - { - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_START); - i2s_started = true; + if (get_random_file(next_random_filename, sizeof(next_random_filename)) == 0) + { + LOG_DBG("Pre-cached next random file: %s", next_random_filename); + } } } - - /* 4. Aufräumen */ - if (aborted) - { - /* Hart abbrechen, Puffer sofort verwerfen */ - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DROP); - } - else - { - /* Sauber ausklingen lassen, bis der letzte I2S-Puffer leer ist */ - i2s_trigger(i2s_dev, I2S_DIR_TX, I2S_TRIGGER_DRAIN); - } - - fs_close(&file); - io_status(false); - LOG_DBG("Playback finished or aborted"); } } @@ -210,16 +325,7 @@ int audio_init(void) if (ret < 0) return ret; + audio_refresh_file_count(); LOG_INF("Audio initialized: %u bits, %u.%03u kHz", config.word_size, config.frame_clk_freq / 1000, config.frame_clk_freq % 1000); return 0; -} - -void audio_play(const char *filename) -{ - char buf[64] = {0}; - if (filename != NULL) - { - strncpy(buf, filename, sizeof(buf) - 1); - } - k_msgq_put(&audio_play_msgq, &buf, K_NO_WAIT); } \ No newline at end of file diff --git a/firmware/src/audio.h b/firmware/src/audio.h index 2706a74..2c47101 100644 --- a/firmware/src/audio.h +++ b/firmware/src/audio.h @@ -10,8 +10,8 @@ #define AUDIO_THREAD_PRIORITY 5 #define AUDIO_EVENTS_MASK (AUDIO_EVENT_PLAY | AUDIO_EVENT_STOP | AUDIO_EVENT_SYNC) -#define AUDIO_BLOCK_SIZE 1024 -#define AUDIO_BLOCK_COUNT 4 +#define AUDIO_BLOCK_SIZE 4096 +#define AUDIO_BLOCK_COUNT 8 #define AUDIO_WORD_WIDTH 16 #define AUDIO_SAMPLE_RATE 16000 @@ -27,7 +27,7 @@ int audio_init(void); * * @param filename The path to the audio file to play */ -void audio_play(const char *filename) +void audio_play(const char *filename); /** * @brief Stops the currently playing audio @@ -39,4 +39,14 @@ void audio_stop(void); */ void audio_system_ready(void); +/** + * @brief Refreshes the count of available audio files + */ +void audio_refresh_file_count(void); + +/** + * @brief Puts the QSPI flash into deep sleep mode to save power + */ +void flash_deep_sleep(void); + #endif // AUDIO_H \ No newline at end of file diff --git a/firmware/src/fs.c b/firmware/src/fs.c index cb50124..f4d5626 100644 --- a/firmware/src/fs.c +++ b/firmware/src/fs.c @@ -1,10 +1,21 @@ #include +#include +#include #include -LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_INF); +LOG_MODULE_REGISTER(buzz_fs, LOG_LEVEL_DBG); #define STORAGE_PARTITION_ID FIXED_PARTITION_ID(littlefs_storage) FS_LITTLEFS_DECLARE_DEFAULT_CONFIG(fs_storage_data); +#define QSPI_FLASH_NODE DT_ALIAS(qspi_flash) +#if !DT_NODE_EXISTS(QSPI_FLASH_NODE) +#error "QSPI Flash alias not defined in devicetree" +#endif + +static const struct device *flash_dev = DEVICE_DT_GET(QSPI_FLASH_NODE); +static volatile uint32_t open_count = 0; +static struct k_mutex flash_pm_lock; + static struct fs_mount_t fs_storage_mnt = { .type = FS_LITTLEFS, .fs_data = &fs_storage_data, @@ -18,6 +29,90 @@ int fs_init(void) { LOG_ERR("Error mounting filesystem: %d", rc); return rc; } + k_mutex_init(&flash_pm_lock); LOG_DBG("Filesystem mounted successfully"); return 0; -} \ No newline at end of file +} + +int fs_pm_flash_suspend(void) +{ + if (!device_is_ready(flash_dev)) { + return -ENODEV; + } + + k_mutex_lock(&flash_pm_lock, K_FOREVER); + + if (open_count > 0) { + open_count--; + if (open_count == 0) { + int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_SUSPEND); + if (rc < 0) { + LOG_WRN("Could not suspend flash: %d", rc); + } else { + LOG_DBG("Flash entered deep power-down"); + } + } + } + + k_mutex_unlock(&flash_pm_lock); + return 0; +} + +int fs_pm_flash_resume(void) +{ + if (!device_is_ready(flash_dev)) return -ENODEV; + + k_mutex_lock(&flash_pm_lock, K_FOREVER); + + if (open_count == 0) { + int rc = pm_device_action_run(flash_dev, PM_DEVICE_ACTION_RESUME); + if (rc == 0) { + k_busy_wait(50); // t-exit-dpd + LOG_DBG("Flash resumed"); + } + } + open_count++; + + k_mutex_unlock(&flash_pm_lock); + return 0; +} + +int fs_pm_open(struct fs_file_t *file, const char *path, fs_mode_t mode) +{ + int rc = fs_open(file, path, mode); + if (rc == 0) + { + fs_pm_flash_resume(); + } + return rc; +} + +int fs_pm_close(struct fs_file_t *file) +{ + int rc = fs_close(file); + if (rc == 0) + { + fs_pm_flash_suspend(); + } + return rc; +} + +int fs_pm_opendir(struct fs_dir_t *dirp, const char *path) +{ + int rc = fs_opendir(dirp, path); + if (rc == 0) + { + fs_pm_flash_resume(); + } + return rc; +} + +int fs_pm_closedir(struct fs_dir_t *dirp) +{ + int rc = fs_closedir(dirp); + if (rc == 0) + { + fs_pm_flash_suspend(); + } + return rc; +} \ No newline at end of file diff --git a/firmware/src/fs.h b/firmware/src/fs.h index 7591ea7..272d19e 100644 --- a/firmware/src/fs.h +++ b/firmware/src/fs.h @@ -7,4 +7,20 @@ * @brief Initializes the filesystem by mounting it */ int fs_init(void); + +/** + * @brief Puts the QSPI flash into deep sleep mode to save power + */ +int fs_pm_flash_suspend(void); + +/** + * @brief Resumes the QSPI flash from deep sleep mode + */ +int fs_pm_flash_resume(void); + + +int fs_pm_open(struct fs_file_t *file, const char *path, fs_mode_t mode); +int fs_pm_close(struct fs_file_t *file); +int fs_pm_opendir(struct fs_dir_t *dirp, const char *path); +int fs_pm_closedir(struct fs_dir_t *dirp); #endif // FS_H \ No newline at end of file diff --git a/firmware/src/io.c b/firmware/src/io.c index 664431a..774b1b4 100644 --- a/firmware/src/io.c +++ b/firmware/src/io.c @@ -3,7 +3,7 @@ #include #include -LOG_MODULE_REGISTER(io, LOG_LEVEL_INF); +LOG_MODULE_REGISTER(io, LOG_LEVEL_DBG); #define STATUS_LED_NODE DT_ALIAS(status_led) #define USB_LED_NODE DT_ALIAS(usb_led) @@ -20,6 +20,7 @@ void button_isr(const struct device *dev, struct gpio_callback *cb, uint32_t pin gpio_pin_interrupt_configure_dt(&button_spec, GPIO_INT_DISABLE); LOG_DBG("Button pressed, triggering audio play"); + audio_stop(); audio_play(NULL); k_work_reschedule(&debounce_work, K_MSEC(50)); diff --git a/firmware/src/main.c b/firmware/src/main.c index fa590a1..3285a56 100644 --- a/firmware/src/main.c +++ b/firmware/src/main.c @@ -75,8 +75,4 @@ int main(void) LOG_INF("All subsystems initialized. Starting application threads."); audio_system_ready(); - audio_play("/lfs/sys/404"); - while (1) { - k_sleep(K_FOREVER); - } } \ No newline at end of file diff --git a/firmware/src/protocol.c b/firmware/src/protocol.c index 0422c0b..7ebce5f 100644 --- a/firmware/src/protocol.c +++ b/firmware/src/protocol.c @@ -7,6 +7,7 @@ #include #include +#include #define PROTOCOL_VERSION 1 @@ -44,7 +45,7 @@ int send_ls(const char *path) const char *ls_path = (path == NULL || path[0] == '\0') ? "/" : path; fs_dir_t_init(&dirp); - if (fs_opendir(&dirp, ls_path) < 0) + if (fs_pm_opendir(&dirp, ls_path) < 0) { LOG_ERR("Failed to open directory '%s'", ls_path); return ENOENT; @@ -57,7 +58,7 @@ int send_ls(const char *path) usb_write_buffer((const uint8_t *)tx_buffer, strlen(tx_buffer)); } - fs_closedir(&dirp); + fs_pm_closedir(&dirp); return 0; } @@ -88,7 +89,7 @@ int put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_cr fs_file_t_init(&file); fs_unlink(filename); LOG_DBG("Opening file '%s' for writing (expected size: %zd bytes, expected CRC32: 0x%08x)", filename, filesize, expected_crc32); - rc = fs_open(&file, filename, FS_O_CREATE | FS_O_WRITE); + rc = fs_pm_open(&file, filename, FS_O_CREATE | FS_O_WRITE); if (rc < 0) { LOG_ERR("Failed to open file '%s' for writing: %d", filename, rc); @@ -109,7 +110,7 @@ int put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_cr if (read < 0) { LOG_ERR("Error reading from USB: %d", read); - fs_close(&file); + fs_pm_close(&file); return -read; } else if (read == 0) @@ -117,7 +118,7 @@ int put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_cr if (retry_count >= 10) { LOG_ERR("No data received from USB after multiple attempts"); - fs_close(&file); + fs_pm_close(&file); return -ETIMEDOUT; } @@ -147,7 +148,7 @@ int put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_cr if (written < 0) { LOG_ERR("Error writing to file '%s': %d", filename, (int)written); - fs_close(&file); + fs_pm_close(&file); return (int)written; } @@ -163,7 +164,7 @@ int put_binary_file(const char *filename, ssize_t filesize, uint32_t expected_cr uint32_t duration = k_uptime_get_32() - start; uint32_t kb_per_s = (filesize * 1000) / (duration * 1024 + 1); LOG_DBG("Received file '%s' (%zd bytes) in %u ms (%u kb/s), CRC32: 0x%08x", filename, filesize, duration, kb_per_s, running_crc32); - fs_close(&file); + fs_pm_close(&file); LOG_DBG("Closed file '%s' after writing", filename); if (running_crc32 != expected_crc32) { @@ -224,6 +225,7 @@ void execute_current_command(void) if (rc == 0) { send_ok(); + audio_refresh_file_count(); // Nach erfolgreichem Upload die Anzahl der verfügbaren Audiodateien aktualisieren } else {