diff --git a/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.cpp b/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.cpp index 3cf9f2e4..5b0f2f4b 100644 --- a/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/BlueSCSI_platform.cpp @@ -1202,6 +1202,9 @@ void platform_late_init() // Act as SCSI initiator void platform_initiator_gpio_setup() { +#ifdef ENABLE_AUDIO_OUTPUT_SPDIF + audio_setup_release(); +#endif // pin function pup pdown out state fast gpio_conf(SCSI_IN_IO, GPIO_FUNC_SIO, false, false, false, true, false); gpio_conf(SCSI_IN_MSG, GPIO_FUNC_SIO, false, false, false, true, false); diff --git a/lib/BlueSCSI_platform_RP2MCU/audio_i2s.cpp b/lib/BlueSCSI_platform_RP2MCU/audio_i2s.cpp index f0364652..45bb6902 100644 --- a/lib/BlueSCSI_platform_RP2MCU/audio_i2s.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/audio_i2s.cpp @@ -400,6 +400,10 @@ bool audio_is_active() { return !audio_idle; } +bool audio_is_paused() { + return audio_paused; +} + bool audio_is_playing(uint8_t id) { // return audio_playing; return audio_owner == (id & S2S_CFG_TARGET_ID_BITS) && audio_playing; @@ -823,7 +827,8 @@ bool audio_set_paused(uint8_t id, bool paused) { } void audio_stop(uint8_t id) { - if (id != 0xFF && (audio_idle || (id & S2S_CFG_TARGET_ID_BITS) != audio_owner)) return; + if (audio_idle) return; + if (id != 0xFF && (id & S2S_CFG_TARGET_ID_BITS) != audio_owner) return; memset(¤t_track, 0, sizeof(current_track)); memset(output_buf_a, 0, sizeof(output_buf_a)); @@ -924,5 +929,14 @@ void audio_set_file_position(uint8_t id, const CUETrackInfo *trackinfo, uint32_t setup_playback(id, lba, 0, false); } +#ifdef UNIT_TEST +/* Test accessor */ +extern "C" void i2s_test_start(uint8_t owner) +{ + audio_owner = owner; + audio_playing = true; + audio_idle = false; +} +#endif #endif // ENABLE_AUDIO_OUTPUT_SPDIF diff --git a/lib/BlueSCSI_platform_RP2MCU/audio_spdif.cpp b/lib/BlueSCSI_platform_RP2MCU/audio_spdif.cpp index d9188a1b..8d8ee4fd 100644 --- a/lib/BlueSCSI_platform_RP2MCU/audio_spdif.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/audio_spdif.cpp @@ -139,7 +139,7 @@ static uint16_t wire_buf_b[WIRE_BUFFER_SIZE]; static uint8_t snd_stop_irqs = 0; // DMA IRQs seen since audio_stop() began // tracking for audio playback -static uint8_t audio_owner; // SCSI ID or 0xFF when idle +static uint8_t audio_owner = 0xFF; // SCSI ID or 0xFF when idle static volatile bool audio_paused = false; static ImageBackingStore* audio_file; static uint64_t fpos; @@ -193,9 +193,14 @@ static uint8_t invert = 0; // biphase encode help: set if last wire bit was '1' #ifndef SPDIF_PIO_INSTANCE # ifdef BLUESCSI_MCU_RP20XX # define SPDIF_PIO_INSTANCE pio0 -# define SPDIF_PIO_SM 0 # else # define SPDIF_PIO_INSTANCE pio2 +# endif +#endif +#ifndef SPDIF_PIO_SM +# ifdef BLUESCSI_MCU_RP20XX +# define SPDIF_PIO_SM 0 +# else # define SPDIF_PIO_SM 1 # endif #endif @@ -392,11 +397,20 @@ bool audio_is_active() { return audio_owner != 0xFF && g_scsi_settings.getSystem()->enableCDAudio; } +bool audio_is_paused() { + return audio_paused; +} + bool audio_is_playing(uint8_t id) { return audio_owner == (id & S2S_CFG_TARGET_ID_BITS); } void audio_setup() { + if (platform_is_initiator_mode_enabled()) { + // the SCSI host code owns the state machine, see audio_setup_release() + audio_setup_failed = true; + return; + } if (!g_scsi_settings.getSystem()->enableCDAudio) { logmsg("Audio setup skipped, this build does not support CD Audio"); audio_setup_failed = true; @@ -416,10 +430,6 @@ void audio_setup() { logmsg("BlueSCSI CD Audio Enabled - Connect DAC to BlueSCSI or use SPDIF on I2C SCL pin"); #endif - if (platform_set_smps_pwm(true)) { - logmsg("Regulator set to PWM mode for CD audio"); - } - // Calculate clock divider, rounding up as necessary double clkdiv = ((double)(g_bluescsi_timings->clk_hz) / (double)(5644800)); @@ -453,6 +463,10 @@ void audio_setup() { already_claimed = true; } + if (platform_set_smps_pwm(true)) { + logmsg("Regulator set to PWM mode for CD audio"); + } + if (SPDIF_OUTPUT_PIN != GPIO_EXP_SPARE) { gpio_put(GPIO_EXP_SPARE, true); gpio_set_dir(GPIO_EXP_SPARE, false); @@ -474,6 +488,24 @@ void audio_setup() { #endif } +// Initiator mode: the SCSI host code takes over PIO0 on RP2040. Put the state +// machine and the output pin back as a build without audio has them. +void audio_setup_release() { + audio_stop(); + if (already_claimed) { + pio_sm_config c = pio_get_default_sm_config(); + sm_config_set_set_pins(&c, 0, 5); // PINCTRL reset value + pio_sm_init(SPDIF_PIO_UNIT, spdif_pio_sm, 0, &c); + gpio_put(SPDIF_OUTPUT_PIN, true); + gpio_set_dir(SPDIF_OUTPUT_PIN, false); + gpio_set_pulls(SPDIF_OUTPUT_PIN, true, false); + gpio_set_function(SPDIF_OUTPUT_PIN, GPIO_FUNC_I2C); + platform_set_smps_pwm(false); + already_claimed = false; + } + audio_setup_failed = true; +} + void audio_poll() { if (!audio_is_active() || !g_scsi_settings.getSystem()->enableCDAudio || audio_setup_failed) return; if (audio_paused) return; @@ -649,6 +681,7 @@ void audio_stop(uint8_t id) { if (audio_setup_failed) { return; } + if (audio_owner == 0xFF) return; if (id != 0xFF && audio_owner != (id & S2S_CFG_TARGET_ID_BITS)) return; // to help mute external hardware, send a bunch of '0' samples prior to @@ -661,9 +694,7 @@ void audio_stop(uint8_t id) { // and wait for them to shut down naturally snd_stop_irqs = 0; audio_stopping = true; - while (dma_channel_is_busy(SOUND_DMA_CHA)) tight_loop_contents(); - while (dma_channel_is_busy(SOUND_DMA_CHB)) tight_loop_contents(); - while (!pio_sm_is_tx_fifo_empty(SPDIF_PIO_UNIT, spdif_pio_sm)) tight_loop_contents(); + while (dma_channel_is_busy(SOUND_DMA_CHA) || !pio_sm_is_tx_fifo_empty(SPDIF_PIO_UNIT, spdif_pio_sm)) tight_loop_contents(); audio_stopping = false; // idle the subsystem diff --git a/lib/BlueSCSI_platform_RP2MCU/audio_spdif.h b/lib/BlueSCSI_platform_RP2MCU/audio_spdif.h index bc8d22b5..17409402 100644 --- a/lib/BlueSCSI_platform_RP2MCU/audio_spdif.h +++ b/lib/BlueSCSI_platform_RP2MCU/audio_spdif.h @@ -1,5 +1,6 @@ /** * Copyright (C) 2023 saybur + * Copyright (c) 2026 Eric Helgeson * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,6 +56,12 @@ bool audio_is_active(); */ void audio_setup(); +/** + * Hands the PIO state machine and the output pin back when initiator mode is + * enabled. Audio stays off afterwards. + */ +void audio_setup_release(); + /** * Called from platform_poll() to fill sample buffer(s) if needed. */ diff --git a/lib/BlueSCSI_platform_RP2MCU/scsi_accel_target.cpp b/lib/BlueSCSI_platform_RP2MCU/scsi_accel_target.cpp index 3cad4b28..04b8146b 100644 --- a/lib/BlueSCSI_platform_RP2MCU/scsi_accel_target.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/scsi_accel_target.cpp @@ -80,7 +80,7 @@ // B: Lookup from g_scsi_parity_check_lookup and copy to scsi_read_parity PIO // C: Addresses from scsi_accel_read PIO to lookup DMA READ_ADDR register // D: From pacer to data state machine to trigger transfers -#ifdef BLUESCSI_NETWORK +#if defined(BLUESCSI_NETWORK) || defined(ENABLE_AUDIO_OUTPUT_SPDIF) # define SCSI_DMA_CH_A 6 # define SCSI_DMA_CH_B 7 # define SCSI_DMA_CH_C 8 @@ -92,6 +92,11 @@ # define SCSI_DMA_CH_D 3 #endif +#ifdef ENABLE_AUDIO_OUTPUT_SPDIF +#define SCSI_DMA_USES(ch) ((ch) == SCSI_DMA_CH_A || (ch) == SCSI_DMA_CH_B || (ch) == SCSI_DMA_CH_C || (ch) == SCSI_DMA_CH_D) +static_assert(!SCSI_DMA_USES(SOUND_DMA_CHA) && !SCSI_DMA_USES(SOUND_DMA_CHB), "SCSI and S/PDIF audio share a DMA channel"); +#endif + static struct { uint8_t *app_buf; // Buffer provided by application uint32_t app_bytes; // Bytes available in application buffer diff --git a/lib/BlueSCSI_platform_RP2MCU/sdio.cpp b/lib/BlueSCSI_platform_RP2MCU/sdio.cpp index 5ac1ccca..b16576b5 100644 --- a/lib/BlueSCSI_platform_RP2MCU/sdio.cpp +++ b/lib/BlueSCSI_platform_RP2MCU/sdio.cpp @@ -35,6 +35,7 @@ #if defined(SD_USE_SDIO) && !defined(SD_USE_RP2350_SDIO) #include "sdio.h" +#include "sdio_timeout.h" #include "sdio_write_response.h" #include #include @@ -86,8 +87,8 @@ static struct { pio_sm_config pio_cfg_data_tx; sdio_transfer_state_t transfer_state; - uint32_t transfer_start_time; // restarted on every completed block - uint32_t burst_start_time; // start of the whole multi-block transfer + volatile uint32_t transfer_start_time; // restarted on every completed block, from the DMA IRQ + volatile uint32_t burst_start_time; // start of the whole multi-block transfer uint32_t *data_buf; uint32_t blocks_done; // Number of blocks transferred so far uint32_t total_blocks; // Total number of blocks to transfer @@ -225,7 +226,7 @@ sdio_status_t receive_status_register(uint8_t* sds) { { // SD Spec says CMD6 transaction timeout is 100ms // ACMD13 is likely similar - if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > 100) + if (sdio_deadline_passed(&g_sdio.transfer_start_time, platform_millis, 100)) { #ifdef SDIO_DEBUG @@ -792,8 +793,8 @@ sdio_status_t rp2040_sdio_rx_poll(uint32_t *bytes_complete) return SDIO_ERR_DATA_CRC; } } - else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > SDIO_BLOCK_TIMEOUT_MS || - (uint32_t)(platform_millis() - g_sdio.burst_start_time) > SDIO_BURST_TIMEOUT_MS) + else if (sdio_deadline_passed(&g_sdio.transfer_start_time, platform_millis, SDIO_BLOCK_TIMEOUT_MS) || + sdio_deadline_passed(&g_sdio.burst_start_time, platform_millis, SDIO_BURST_TIMEOUT_MS)) { sdio_log_data_timeout("rp2040_sdio_rx_poll()", SDIO_DATA_SM, g_sdio.pio_data_rx_offset); rp2040_sdio_stop(); @@ -1036,8 +1037,8 @@ sdio_status_t rp2040_sdio_tx_poll(uint32_t *bytes_complete) rp2040_sdio_stop(); return g_sdio.wr_status; } - else if ((uint32_t)(platform_millis() - g_sdio.transfer_start_time) > SDIO_BLOCK_TIMEOUT_MS || - (uint32_t)(platform_millis() - g_sdio.burst_start_time) > SDIO_BURST_TIMEOUT_MS) + else if (sdio_deadline_passed(&g_sdio.transfer_start_time, platform_millis, SDIO_BLOCK_TIMEOUT_MS) || + sdio_deadline_passed(&g_sdio.burst_start_time, platform_millis, SDIO_BURST_TIMEOUT_MS)) { sdio_log_data_timeout("rp2040_sdio_tx_poll()", SDIO_CMD_SM, g_sdio.pio_data_tx_offset); diff --git a/lib/BlueSCSI_platform_RP2MCU/sdio_timeout.h b/lib/BlueSCSI_platform_RP2MCU/sdio_timeout.h new file mode 100644 index 00000000..249a2b8c --- /dev/null +++ b/lib/BlueSCSI_platform_RP2MCU/sdio_timeout.h @@ -0,0 +1,38 @@ +/** + * BlueSCSI - Copyright (c) 2026 Eric Helgeson + * + * BlueSCSI firmware is licensed under the GPL version 3 or any later version. + * + * https://www.gnu.org/licenses/gpl-3.0.html + * ---- + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +**/ + +// Deadline check for a start time that an interrupt rewrites while the main +// loop polls. The start time is read before the clock, so an IRQ landing +// between the two reads can only make the block look younger. Reading the +// clock first lets a start time from the future wrap the subtraction to +// ~4.29e9 ms, which killed SD write bursts that were microseconds old. + +#pragma once + +#include +#include + +static inline bool sdio_deadline_passed(const volatile uint32_t *start_ms, uint32_t (*now_ms)(void), uint32_t limit_ms) +{ + uint32_t start = *start_ms; + uint32_t now = now_ms(); + return (uint32_t)(now - start) > limit_ms; +} diff --git a/src/BlueSCSI.cpp b/src/BlueSCSI.cpp index 221fac29..644be351 100644 --- a/src/BlueSCSI.cpp +++ b/src/BlueSCSI.cpp @@ -112,8 +112,8 @@ void save_logfile(bool always = false) bool every_command = g_log_debug; #ifdef ENABLE_AUDIO_OUTPUT // An SD write can stall for 100-500 ms and CD audio buffers 46 ms, - // so batch the saves while a track plays. - if (audio_is_active()) every_command = false; + // so batch the saves while a track plays. A paused track reads nothing. + if (audio_is_active() && !audio_is_paused()) every_command = false; #endif bool buffer_filling = (loglen - prev_log_pos) > LOGBUFSIZE / 2; if (always || every_command || buffer_filling || (LOG_SAVE_INTERVAL_MS > 0 && (uint32_t)(platform_millis() - prev_log_save) > LOG_SAVE_INTERVAL_MS)) diff --git a/src/BlueSCSI_audio.h b/src/BlueSCSI_audio.h index 06f2b0f1..09064b03 100644 --- a/src/BlueSCSI_audio.h +++ b/src/BlueSCSI_audio.h @@ -78,6 +78,11 @@ bool audio_is_playing(uint8_t id); */ bool audio_is_active(); +/** + * True while the active track is paused. Implemented by the platform backend. + */ +bool audio_is_paused(); + /** * Begins audio playback for a file. *