From 5ea00d101d5c3ccbebb59769d3f2a73b9f45f549 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Thu, 15 Oct 2026 20:33:53 +0100 Subject: [PATCH 1/4] ipc4: dai: add support for NXP i.MX SAI, SPDIF, ESAI, and MICFIL DAIs Extend IPC4 DAI gateway configuration to handle NXP i.MX digital audio interfaces: - dai_config_dma_channel: return channel 0 for SOF_DAI_IMX_* instead of failing with SOF_DMA_CHAN_INVALID (-EINVAL). - ipc_dai_data_config: set burst_elems = 2 for 32-bit stereo word transfers. - dai_init_llp_info: skip Intel mailbox linear link position (LLP) slot allocation for SOF_DAI_IMX_* interfaces. - include/ipc: define SOF_DAI_IMX_SPDIF and SOF_DAI_FMT_SPDIF with params struct. - lib/dai: map SOF_DAI_IMX_SPDIF and SOF_DAI_IMX_SAI to Zephyr DAI devices and DMA channels. Signed-off-by: Liam Girdwood --- src/include/ipc/dai-imx.h | 6 ++++++ src/include/ipc/dai.h | 5 ++++- src/ipc/ipc3/dai.c | 2 ++ src/ipc/ipc4/dai.c | 20 ++++++++++++++++++-- src/lib/dai.c | 13 +++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/include/ipc/dai-imx.h b/src/include/ipc/dai-imx.h index 5f9fc4dce837..d16c9e021453 100644 --- a/src/include/ipc/dai-imx.h +++ b/src/include/ipc/dai-imx.h @@ -61,4 +61,10 @@ struct sof_ipc_dai_micfil_params { uint32_t pdm_ch; } __attribute__((packed, aligned(4))); +/* S/PDIF Configuration Request - SOF_IPC_DAI_SPDIF_CONFIG */ +struct sof_ipc_dai_spdif_params { + uint32_t sample_rate; + uint32_t reserved[3]; +} __attribute__((packed, aligned(4))); + #endif /* __IPC_DAI_IMX_H__ */ diff --git a/src/include/ipc/dai.h b/src/include/ipc/dai.h index 49ffa5d5fa94..f4b47db91baa 100644 --- a/src/include/ipc/dai.h +++ b/src/include/ipc/dai.h @@ -36,6 +36,7 @@ #define SOF_DAI_FMT_DSP_A 4 /**< L data MSB after FRM LRC */ #define SOF_DAI_FMT_DSP_B 5 /**< L data MSB during FRM LRC */ #define SOF_DAI_FMT_PDM 6 /**< Pulse density modulation */ +#define SOF_DAI_FMT_SPDIF 7 /**< SPDIF mode */ #define SOF_DAI_FMT_CONT (1 << 4) /**< continuous clock */ #define SOF_DAI_FMT_GATED (0 << 4) /**< clock is gated */ @@ -97,7 +98,8 @@ enum sof_ipc_dai_type { SOF_DAI_IMX_MICFIL, /**< i.MX MICFIL */ SOF_DAI_AMD_SDW, /**< Amd SDW */ SOF_DAI_INTEL_UAOL, /**< Intel UAOL */ - SOF_DAI_AMD_TDM /**< Amd TDM */ + SOF_DAI_AMD_TDM, /**< Amd TDM */ + SOF_DAI_IMX_SPDIF /**< i.MX S/PDIF */ }; #define SOF_DAI_CONFIG_HW_SPEC_OFFSET offsetof(struct sof_ipc_dai_config, ssp) @@ -131,6 +133,7 @@ struct sof_ipc_dai_config { struct sof_ipc_dai_afe_params afe; struct sof_ipc_dai_micfil_params micfil; struct sof_ipc_dai_acp_sdw_params acpsdw; + struct sof_ipc_dai_spdif_params spdif; }; } __attribute__((packed, aligned(4))); diff --git a/src/ipc/ipc3/dai.c b/src/ipc/ipc3/dai.c index 46d54d2d86e5..856e4eea4a24 100644 --- a/src/ipc/ipc3/dai.c +++ b/src/ipc/ipc3/dai.c @@ -63,6 +63,8 @@ int dai_config_dma_channel(struct dai_data *dd, struct comp_dev *dev, const void case SOF_DAI_IMX_SAI: COMPILER_FALLTHROUGH; case SOF_DAI_IMX_ESAI: + COMPILER_FALLTHROUGH; + case SOF_DAI_IMX_SPDIF: handshake = dai_get_handshake(dd->dai, dai->direction, dd->stream_id); /* TODO: remove this when transition to native drivers is complete on all NXP platforms */ diff --git a/src/ipc/ipc4/dai.c b/src/ipc/ipc4/dai.c index 997916aa7854..dfe59e36d4a0 100644 --- a/src/ipc/ipc4/dai.c +++ b/src/ipc/ipc4/dai.c @@ -128,6 +128,12 @@ int dai_config_dma_channel(struct dai_data *dd, struct comp_dev *dev, const void channel = dai->host_dma_config[0]->dma_channel_id; break; #endif + case SOF_DAI_IMX_SAI: + case SOF_DAI_IMX_SPDIF: + case SOF_DAI_IMX_ESAI: + case SOF_DAI_IMX_MICFIL: + channel = 0; + break; default: /* other types of DAIs not handled for now */ comp_err(dev, "Unknown dai type %d", dai->type); @@ -185,6 +191,12 @@ int ipc_dai_data_config(struct dai_data *dd, struct comp_dev *dev) break; case SOF_DAI_INTEL_UAOL: break; + case SOF_DAI_IMX_SAI: + case SOF_DAI_IMX_SPDIF: + case SOF_DAI_IMX_ESAI: + case SOF_DAI_IMX_MICFIL: + dd->config.burst_elems = 2; + break; default: /* other types of DAIs not handled for now */ comp_warn(dev, "Unknown dai type %d", dai->type); @@ -320,8 +332,12 @@ static int dai_init_llp_info(struct dai_data *dd, struct comp_dev *dev) copier_cfg = dd->dai_spec_config; node = copier_cfg->gtw_cfg.node_id; - /* HDA doesn't use llp slot */ - if (dd->ipc_config.type == SOF_DAI_INTEL_HDA) + /* HDA and i.MX DAIs don't use llp slot */ + if (dd->ipc_config.type == SOF_DAI_INTEL_HDA || + dd->ipc_config.type == SOF_DAI_IMX_SAI || + dd->ipc_config.type == SOF_DAI_IMX_SPDIF || + dd->ipc_config.type == SOF_DAI_IMX_ESAI || + dd->ipc_config.type == SOF_DAI_IMX_MICFIL) return 0; /* don't support more gateway like EVAD */ diff --git a/src/lib/dai.c b/src/lib/dai.c index 4c52cd7fe460..5ceb299fe023 100644 --- a/src/lib/dai.c +++ b/src/lib/dai.c @@ -187,6 +187,9 @@ const struct device *zephyr_dev[] = { #if CONFIG_DAI_NXP_MICFIL DT_FOREACH_STATUS_OKAY(nxp_dai_micfil, GET_DEVICE_LIST) #endif +#if CONFIG_DAI_NXP_SPDIF + DT_FOREACH_STATUS_OKAY(nxp_dai_spdif, GET_DEVICE_LIST) +#endif #if CONFIG_DAI_AMD_SDW DT_FOREACH_STATUS_OKAY(amd_acp_sdw_dai, GET_DEVICE_LIST) #endif @@ -227,6 +230,8 @@ static int sof_dai_type_to_zephyr(uint32_t type) return DAI_IMX_SAI; case SOF_DAI_IMX_ESAI: return DAI_IMX_ESAI; + case SOF_DAI_IMX_SPDIF: + return DAI_IMX_SPDIF; case SOF_DAI_AMD_DMIC: return DAI_AMD_DMIC; case SOF_DAI_MEDIATEK_AFE: @@ -325,6 +330,14 @@ static void dai_set_device_params(struct dai *d) case SOF_DAI_MEDIATEK_AFE: d->dma_dev = SOF_DMA_DEV_AFE_MEMIF; break; + case SOF_DAI_IMX_SAI: + d->dma_dev = SOF_DMA_DEV_SAI; + d->dma_caps = SOF_DMA_CAP_GP_LP | SOF_DMA_CAP_GP_HP; + break; + case SOF_DAI_IMX_SPDIF: + d->dma_dev = SOF_DMA_DEV_SPDIF; + d->dma_caps = SOF_DMA_CAP_GP_LP | SOF_DMA_CAP_GP_HP; + break; default: break; } From d8e6297984cdee82c602b5763590d6efd4a852a5 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Thu, 15 Oct 2026 20:33:59 +0100 Subject: [PATCH 2/4] audio: pipeline: static: handle unrouted components and early control publishing - sof_static_topology_prepare_comps: skip components without attached buffer sources or sinks in bypass mode, and treat -ENOTCONN gracefully during prepare. - sof_static_topology_init: publish s_active_topo before Stage 7 (init_controls) so that kcontrol setter operations during default value initialization can reference the active topology instead of failing with -ENODEV (-19). - Reset s_active_topo to NULL on initialization failure unwinding. Signed-off-by: Liam Girdwood --- src/audio/pipeline/static_pipeline_loader.c | 25 ++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/audio/pipeline/static_pipeline_loader.c b/src/audio/pipeline/static_pipeline_loader.c index ace6eef3cb9a..05d10c34c3ef 100644 --- a/src/audio/pipeline/static_pipeline_loader.c +++ b/src/audio/pipeline/static_pipeline_loader.c @@ -878,6 +878,19 @@ static int sof_static_topology_prepare_comps(const struct sof_static_topology *t /* Only prepare components currently in READY state */ if (dev->state == COMP_STATE_READY) { const struct sof_static_comp *cdesc = &topo->comps[i]; + + /* Skip unrouted components that have no buffers connected */ + if (list_is_empty(&dev->bsource_list) && list_is_empty(&dev->bsink_list)) { + LOG_DBG("Skipping unrouted component %u ('%s')", cdesc->id, cdesc->name); + continue; + } + if (cdesc->type != SOF_STATIC_COMP_HOST && cdesc->type != SOF_STATIC_COMP_DAI) { + if (list_is_empty(&dev->bsource_list) || list_is_empty(&dev->bsink_list)) { + LOG_DBG("Skipping unrouted module %u ('%s')", cdesc->id, cdesc->name); + continue; + } + } + const struct sof_static_pipeline_desc *pdesc = sof_static_find_pipeline_desc(topo, cdesc->pipeline_id); @@ -941,6 +954,11 @@ static int sof_static_topology_prepare_comps(const struct sof_static_topology *t return ret_prms; } int ret_prep = comp_prepare(dev); + if (ret_prep == -ENOTCONN) { + LOG_DBG("Component %u ('%s') not connected, skipping prepare", + dev_comp_id(dev), cdesc->name); + continue; + } if (ret_prep < 0) { LOG_ERR("Component %u ('%s') prepare failed: %d", dev_comp_id(dev), cdesc->name, ret_prep); @@ -1108,6 +1126,9 @@ int sof_static_topology_init(const struct sof_static_topology *topo) goto err_unwind; } + /* Publish topology as active before initializing kcontrols */ + s_active_topo = topo; + /* Stage 7: Initialize kcontrols with default values */ ret = sof_static_topology_init_controls(topo); if (ret < 0) { @@ -1115,14 +1136,12 @@ int sof_static_topology_init(const struct sof_static_topology *topo) goto err_unwind; } - /* Publish topology as active only after all construction stages succeed */ - s_active_topo = topo; - LOG_INF("Static audio topology initialized successfully (%zu pipelines, %zu comps, %zu buffers, %zu controls)", topo->num_pipelines, topo->num_comps, topo->num_buffers, topo->num_controls); return 0; err_unwind: + s_active_topo = NULL; sof_static_topology_unwind(); return ret; } From 17b22d7725afc0e8fd6cbe1c7d9e28c360834e67 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Thu, 15 Oct 2026 20:34:06 +0100 Subject: [PATCH 3/4] platform: teensy41: add PJRC Teensy 4.1 platform support with Audio PLL scheduling and UAC2 Add comprehensive platform support for PJRC Teensy 4.1 (NXP i.MX RT1062 Cortex-M7): - platform/teensy41: Audio PLL4 configuration (688.128 MHz), SAI1 MCLK (12.288 MHz), on-chip S/PDIF transceiver clock root (/4, /7 => 24.576 MHz), fast slew rate pinmux, static pipeline topology, and memory heap allocation. - app/boards/teensy41: add board overlay, Kconfig settings, and devicetree definitions for SAI1, S/PDIF transceiver (Pins 14 and 15 ALT3), and eDMA. - dai-zephyr: drive Low-Latency (LL) scheduling domain from Audio PLL via cyclic eDMA period completion interrupts (dai_zephyr_dma_period_cb). - zephyr_domain: suppress SysTick k_timer scheduling ticks when audio stream is actively driven by hardware audio domain clock. - usb_audio: replenish UAC2 capture double buffer on packet completion and handle burst multi-packet reads to prevent endpoint underflow/overflow. - syscalls: guard Zephyr syscall includes with CONFIG_USERSPACE for non-MMU architectures. Signed-off-by: Liam Girdwood --- Kconfig.sof | 2 +- app/CMakeLists.txt | 39 +- app/Kconfig | 5 + app/boards/teensy41.conf | 89 +++ app/boards/teensy41.overlay | 173 ++++ app/src/main.c | 44 +- app/src/sample_usbd.h | 39 + app/src/usbd_init.c | 442 +++++++++++ scripts/cmake/misc.cmake | 2 +- src/audio/CMakeLists.txt | 1 + src/audio/Kconfig | 6 + src/audio/dai-zephyr.c | 200 ++++- src/audio/pipeline/CMakeLists.txt | 4 + src/audio/pipeline/pipeline-xrun.c | 9 +- src/audio/pipeline/sof_static_pipeline.c | 749 ++++++++++++++++++ src/audio/usb_audio.c | 589 ++++++++++++++ src/include/ipc4/handler.h | 2 +- src/include/ipc4/notification.h | 2 +- src/include/sof/audio/kpb.h | 2 +- .../sof/audio/module_adapter/module/generic.h | 10 +- .../sof/audio/pipeline/sof_static_pipeline.h | 77 ++ src/include/sof/audio/usb_audio.h | 51 ++ src/include/sof/ipc/ipc_reply.h | 2 +- src/include/sof/ipc/msg.h | 4 +- src/include/sof/lib/dai-zephyr.h | 8 +- src/include/sof/lib_manager.h | 2 +- src/include/sof/schedule/dp_schedule.h | 7 +- src/include/sof/schedule/ll_schedule_domain.h | 9 +- src/include/user/debug_stream_slot.h | 5 + src/platform/CMakeLists.txt | 2 + src/platform/Kconfig | 52 ++ src/platform/teensy41/CMakeLists.txt | 10 + .../include/platform/drivers/interrupt.h | 9 + .../teensy41/include/platform/lib/clk.h | 18 + .../teensy41/include/platform/lib/dai.h | 11 + .../teensy41/include/platform/lib/dma.h | 11 + .../teensy41/include/platform/lib/mailbox.h | 44 + .../teensy41/include/platform/lib/memory.h | 27 + .../include/platform/lib/pm_runtime.h | 9 + .../teensy41/include/platform/platform.h | 29 + .../include/platform/teensy41_audio_timer.h | 17 + .../teensy41/include/platform/trace/trace.h | 9 + src/platform/teensy41/lib/CMakeLists.txt | 5 + src/platform/teensy41/lib/clk.c | 35 + src/platform/teensy41/platform.c | 269 +++++++ src/platform/teensy41/teensy41_pipeline_def.c | 301 +++++++ src/schedule/zephyr_domain.c | 29 + zephyr/CMakeLists.txt | 19 +- zephyr/include/sof/lib/cpu.h | 8 +- zephyr/include/sof/lib/dma.h | 1 + zephyr/lib/alloc.c | 5 + zephyr/lib/dma.c | 13 +- 52 files changed, 3447 insertions(+), 60 deletions(-) create mode 100644 app/boards/teensy41.conf create mode 100644 app/boards/teensy41.overlay create mode 100644 app/src/sample_usbd.h create mode 100644 app/src/usbd_init.c create mode 100644 src/audio/pipeline/sof_static_pipeline.c create mode 100644 src/audio/usb_audio.c create mode 100644 src/include/sof/audio/pipeline/sof_static_pipeline.h create mode 100644 src/include/sof/audio/usb_audio.h create mode 100644 src/platform/teensy41/CMakeLists.txt create mode 100644 src/platform/teensy41/include/platform/drivers/interrupt.h create mode 100644 src/platform/teensy41/include/platform/lib/clk.h create mode 100644 src/platform/teensy41/include/platform/lib/dai.h create mode 100644 src/platform/teensy41/include/platform/lib/dma.h create mode 100644 src/platform/teensy41/include/platform/lib/mailbox.h create mode 100644 src/platform/teensy41/include/platform/lib/memory.h create mode 100644 src/platform/teensy41/include/platform/lib/pm_runtime.h create mode 100644 src/platform/teensy41/include/platform/platform.h create mode 100644 src/platform/teensy41/include/platform/teensy41_audio_timer.h create mode 100644 src/platform/teensy41/include/platform/trace/trace.h create mode 100644 src/platform/teensy41/lib/CMakeLists.txt create mode 100644 src/platform/teensy41/lib/clk.c create mode 100644 src/platform/teensy41/platform.c create mode 100644 src/platform/teensy41/teensy41_pipeline_def.c diff --git a/Kconfig.sof b/Kconfig.sof index df63811e652b..f8cd3cfe119c 100644 --- a/Kconfig.sof +++ b/Kconfig.sof @@ -60,7 +60,7 @@ rsource "Kconfig.xtos" rsource "src/Kconfig" -if ZEPHYR_SOF_MODULE +if ZEPHYR_SOF_MODULE || SOF_FULL_ZEPHYR_APPLICATION rsource "Kconfig.zephyr-log" endif diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index b66ab5b0735e..ba6e48d79998 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -18,11 +18,46 @@ endif() find_package(Zephyr HINTS $ENV{ZEPHYR_BASE}) project(sample_sof) +add_subdirectory(../zephyr sof_zephyr) + target_sources(app PRIVATE src/main.c ) -zephyr_library_include_directories(app PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/../src/arch/xtensa/include +target_sources_ifdef(CONFIG_USB_DEVICE_STACK_NEXT app PRIVATE + src/usbd_init.c + ) + +target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/../src/include + ${CMAKE_CURRENT_SOURCE_DIR}/../zephyr/include + ${CMAKE_CURRENT_SOURCE_DIR}/../src/platform/${PLATFORM}/include + ${PROJECT_BINARY_DIR}/include/generated ) + +target_link_libraries(app PRIVATE SOF modules_sof) +if(TARGET uuid_reg_h) + add_dependencies(app uuid_reg_h modules_sof) +endif() + +if(CONFIG_XTENSA) + target_include_directories(app PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src/arch/xtensa/include + ) +endif() + +function(sof_append_relative_path_definitions target) + get_target_property(sources ${target} SOURCES) + foreach(src ${sources}) + get_filename_component(ABS_PATH ${src} ABSOLUTE) + file(RELATIVE_PATH rel ${PROJECT_SOURCE_DIR} ${ABS_PATH}) + set_property( + SOURCE ${src} + APPEND + PROPERTY COMPILE_DEFINITIONS + RELATIVE_FILE="${rel}") + endforeach() +endfunction() + +sof_append_relative_path_definitions(app) diff --git a/app/Kconfig b/app/Kconfig index 339a16bed3e8..7e0995637c7a 100644 --- a/app/Kconfig +++ b/app/Kconfig @@ -7,6 +7,11 @@ choice COMPILER_CODEGEN_VLIW endchoice source "Kconfig.zephyr" +source "samples/subsys/usb/common/Kconfig.sample_usbd" + +if !ZEPHYR_SOF_MODULE +rsource "../zephyr/Kconfig" +endif if SOC_FAMILY_INTEL_ADSP rsource "boards/intel_adsp/Kconfig.defconfig" diff --git a/app/boards/teensy41.conf b/app/boards/teensy41.conf new file mode 100644 index 000000000000..aebd3a0aa711 --- /dev/null +++ b/app/boards/teensy41.conf @@ -0,0 +1,89 @@ +# Target Platform & SOF Core +CONFIG_PLATFORM_TEENSY41=y +CONFIG_SOF_FULL_ZEPHYR_APPLICATION=y +CONFIG_IPC_MAJOR_4=y +CONFIG_COMP_BASEFW_IPC4=n +CONFIG_CROSS_CORE_STREAM=n +CONFIG_STATIC_PIPELINE=y +CONFIG_ZEPHYR_NATIVE_DRIVERS=y +CONFIG_SYS_HEAP_BIG_ONLY=y +CONFIG_BUILD_OUTPUT_HEX=y +CONFIG_BUILD_OUTPUT_BIN=y + +# Zephyr DAI Drivers for NXP SAI +CONFIG_DAI=y +CONFIG_DAI_NXP_SAI=y +CONFIG_PINCTRL=y +CONFIG_DMA=y +CONFIG_DMA_MCUX_EDMA=y +CONFIG_DMA_TCD_QUEUE_SIZE=8 +CONFIG_DMA_MCUX_USE_DTCM_FOR_DMA_DESCRIPTORS=n + +# Audio Processing Modules & ARM SIMD Optimizations +CONFIG_COMP_VOLUME=y +CONFIG_VOLUME_ARM_SIMD=y +CONFIG_COMP_IIR=y +CONFIG_EQ_IIR_ARM_SIMD=y +CONFIG_FILTER_ARM_SIMD=y +CONFIG_COMP_FIR=y +CONFIG_COMP_DRC=y +CONFIG_DRC_ARM_SIMD=y +CONFIG_COMP_CROSSOVER=y +CONFIG_COMP_DCBLOCK=y +CONFIG_DCBLOCK_ARM_SIMD=y +CONFIG_COMP_MULTIBAND_DRC=y +CONFIG_COMP_TDFB=y +CONFIG_COMP_USB_AUDIO=y +CONFIG_COMP_LEVEL_MULTIPLIER=y +CONFIG_COMP_SEL=y +CONFIG_COMP_MUX=y +CONFIG_COMP_MIXER=y +CONFIG_MIXER_ARM_SIMD=y +CONFIG_FORMAT_CONVERT_ARM_SIMD=y + +# USB Device Stack Next & UAC2 Class +CONFIG_USB_DEVICE_STACK_NEXT=y +CONFIG_CDC_ACM_SERIAL_INITIALIZE_AT_BOOT=n +CONFIG_USBD_AUDIO2_CLASS=y +CONFIG_UDC_ENABLE_SOF=y +CONFIG_UDC_NXP_EHCI=y +CONFIG_UDC_BUF_COUNT=64 +CONFIG_UDC_BUF_POOL_SIZE=16384 +CONFIG_UDC_WORKQUEUE_PRIORITY=-10 +CONFIG_UDC_NXP_EVENT_COUNT=16 +CONFIG_UDC_WORKQUEUE_STACK_SIZE=2048 +CONFIG_LL_THREAD_PRIORITY=0 + +# USB Device Identification +CONFIG_SAMPLE_USBD_VID=0x16C0 +CONFIG_SAMPLE_USBD_PID=0x04D2 +CONFIG_SAMPLE_USBD_MANUFACTURER="PJRC" +CONFIG_SAMPLE_USBD_PRODUCT="SOF Teensy 4.1 Audio A" + +# System Resources & Memory +CONFIG_MAIN_STACK_SIZE=8192 +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=4096 +CONFIG_USBD_THREAD_STACK_SIZE=4096 +CONFIG_ISR_STACK_SIZE=4096 +CONFIG_HW_STACK_PROTECTION=n +CONFIG_HEAP_MEM_POOL_SIZE=8192 +CONFIG_TICKLESS_KERNEL=n +CONFIG_SYS_CLOCK_TICKS_PER_SEC=1000 + +# Logging & UART Console +CONFIG_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_LOG_MAX_LEVEL=4 +CONFIG_SOF_LOG_LEVEL_INF=y +CONFIG_DAI_LOG_LEVEL_INF=y +CONFIG_USBD_UAC2_LOG_LEVEL_INF=y +CONFIG_UDC_DRIVER_LOG_LEVEL_INF=y +CONFIG_LOG_MODE_DEFERRED=y +CONFIG_LOG_BUFFER_SIZE=4096 +CONFIG_LOG_MODE_OVERFLOW=y +CONFIG_LOG_BACKEND_UART=n +CONFIG_LOG_PROCESS_THREAD=n +CONFIG_EARLY_CONSOLE=n + +# SOF Low Latency Scheduler Statistics Logging +CONFIG_SCHEDULE_LL_STATS_LOG=n diff --git a/app/boards/teensy41.overlay b/app/boards/teensy41.overlay new file mode 100644 index 000000000000..c99b1c91aa26 --- /dev/null +++ b/app/boards/teensy41.overlay @@ -0,0 +1,173 @@ +#include + +/ { + chosen { + zephyr,console = &lpuart6; + zephyr,shell-uart = &lpuart6; + }; + + uac2_card: usb_audio2 { + compatible = "zephyr,uac2"; + status = "okay"; + full-speed; + high-speed; + audio-function = ; + + uac_aclk: aclk { + compatible = "zephyr,uac2-clock-source"; + clock-type = "internal-programmable"; + frequency-control = "host-programmable"; + sampling-frequencies = <48000>; + sof-synchronized; + }; + + /* ===== Playback from Host (ISO OUT) ===== */ + i2s_out_terminal: i2s_out_terminal { + compatible = "zephyr,uac2-input-terminal"; + clock-source = <&uac_aclk>; + terminal-type = ; + front-left; + front-right; + }; + + i2s_fu: i2s_fu { + compatible = "zephyr,uac2-feature-unit"; + data-source = <&i2s_out_terminal>; + mute-control = "host-programmable"; + volume-control = "host-programmable"; + }; + + pb_eq_fu: pb_eq_fu { + compatible = "zephyr,uac2-feature-unit"; + data-source = <&i2s_fu>; + mute-control = "host-programmable"; + }; + + pb_drc_fu: pb_drc_fu { + compatible = "zephyr,uac2-feature-unit"; + data-source = <&pb_eq_fu>; + mute-control = "host-programmable"; + }; + + i2s_speaker: i2s_speaker { + compatible = "zephyr,uac2-output-terminal"; + data-source = <&pb_drc_fu>; + clock-source = <&uac_aclk>; + terminal-type = ; + assoc-terminal = <&i2s_mic>; + }; + + /* ===== Capture to Host (ISO IN) ===== */ + i2s_mic: i2s_mic { + compatible = "zephyr,uac2-input-terminal"; + clock-source = <&uac_aclk>; + terminal-type = ; + front-left; + front-right; + }; + + cap_eq_fu: cap_eq_fu { + compatible = "zephyr,uac2-feature-unit"; + data-source = <&i2s_mic>; + mute-control = "host-programmable"; + }; + + i2s_in_fu: i2s_in_fu { + compatible = "zephyr,uac2-feature-unit"; + data-source = <&cap_eq_fu>; + mute-control = "host-programmable"; + volume-control = "host-programmable"; + }; + + i2s_in_terminal: i2s_in_terminal { + compatible = "zephyr,uac2-output-terminal"; + data-source = <&i2s_in_fu>; + clock-source = <&uac_aclk>; + terminal-type = ; + }; + + /* Audio Streaming Interfaces */ + as_i2s_out: as_i2s_out { + compatible = "zephyr,uac2-audio-streaming"; + linked-terminal = <&i2s_out_terminal>; + subslot-size = <2>; + bit-resolution = <16>; + }; + + as_i2s_in: as_i2s_in { + compatible = "zephyr,uac2-audio-streaming"; + linked-terminal = <&i2s_in_terminal>; + subslot-size = <2>; + bit-resolution = <16>; + }; + }; +}; + +&pinctrl { + pinmux_sai1: pinmux_sai1 { + group0 { + pinmux = <&iomuxc_gpio_b1_01_sai1_tx_data0>, + <&iomuxc_gpio_b0_11_sai1_tx_data2>, + <&iomuxc_gpio_b1_00_sai1_rx_data0>, + <&iomuxc_gpio_ad_b1_10_sai1_rx_sync>, + <&iomuxc_gpio_ad_b1_11_sai1_rx_bclk>, + <&iomuxc_gpio_ad_b1_09_sai1_mclk>; + drive-strength = "r0-6"; + slew-rate = "slow"; + nxp,speed = "100-mhz"; + input-enable; + }; + }; + + pinmux_spdif: pinmux_spdif { + group0 { + pinmux = <&iomuxc_gpio_ad_b1_02_spdif_out>, + <&iomuxc_gpio_ad_b1_03_spdif_in>; + drive-strength = "r0-6"; + slew-rate = "fast"; + nxp,speed = "100-mhz"; + input-enable; + }; + }; +}; + +/delete-node/ &sai1; + +/ { + soc { + sai1: dai@40384000 { + compatible = "nxp,dai-sai"; + reg = <0x40384000 0x4000>; + interrupts = <56 0>; + clocks = <&ccm IMX_CCM_SAI1_CLK 0x7c 18>; + clock-names = "bus"; + pinctrl-0 = <&pinmux_sai1>; + pinctrl-names = "default"; + dai-index = <1>; + dmas = <&edma0 0 20>, <&edma0 1 19>; + dma-names = "tx", "rx"; + tx-fifo-watermark = <16>; + rx-fifo-watermark = <16>; + fifo-depth = <32>; + tx-sync-mode = <1>; + rx-sync-mode = <0>; + tx-dataline = <2>; + status = "okay"; + }; + + spdif: dai@40380000 { + compatible = "nxp,dai-spdif"; + reg = <0x40380000 0x1000>; + interrupts = <60 0>; + clocks = <&ccm IMX_CCM_SPDIF0_CLK 0x7c 14>; + clock-names = "bus"; + pinctrl-0 = <&pinmux_spdif>; + pinctrl-names = "default"; + dai-index = <0>; + dmas = <&edma0 2 86>, <&edma0 3 85>; + dma-names = "tx", "rx"; + status = "okay"; + }; + }; +}; + diff --git a/app/src/main.c b/app/src/main.c index aa9ee6960d8d..5a1d67a6e6f5 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -8,8 +8,23 @@ #include #include +#if defined(CONFIG_PLATFORM_ESP32P4) || defined(CONFIG_PLATFORM_TEENSY41) +#include +#include +#include +#include +#include +#include +#include +#if defined(CONFIG_COMP_BT_AUDIO) +#include +#endif +#endif + LOG_MODULE_REGISTER(main, LOG_LEVEL_DBG); +uint32_t g_diag_debug[8] = {0}; + /* define qemu boot tests if any qemu target is defined, add targets to end */ #if defined(CONFIG_BOARD_QEMU_XTENSA_DC233C) ||\ defined(CONFIG_BOARD_QEMU_XTENSA_DC233C_MMU) @@ -41,6 +56,33 @@ static int sof_app_main(void) LOG_INF("SOF initialized"); +#if defined(CONFIG_PLATFORM_ESP32P4) || defined(CONFIG_PLATFORM_TEENSY41) + /* Initialize static audio pipelines (EQ+DRC Playback & EQ Capture) */ + sof_static_pipelines_init(sof_get()); + + /* Register UAC2 class callbacks before initializing USB stack */ + const struct device *uac2_dev = DEVICE_DT_GET_ONE(zephyr_uac2); + if (device_is_ready(uac2_dev)) { + usbd_uac2_set_ops(uac2_dev, sof_get_uac2_ops(), NULL); + } else { + LOG_ERR("UAC2 device not ready"); + } + + /* Initialize USB device stack and UAC2 class */ + struct usbd_context *sample_usbd = sample_usbd_init_device(NULL); + if (sample_usbd) { + usbd_enable(sample_usbd); + LOG_INF("SOF USB UAC2 device and static pipelines started"); + } else { + LOG_ERR("Failed to initialize USB device context"); + } + +#if defined(CONFIG_COMP_BT_AUDIO) + /* Initialize Bluetooth Audio Service and power on ESP32-C6 coprocessor */ + bt_service_init(); +#endif +#endif + #ifdef CONFIG_ARCH_POSIX_LIBFUZZER /* Workaround for an apparent timing bug in libfuzzer+asan. * If the initial/main thread is allowed to return, ASAN will @@ -100,7 +142,7 @@ void test_main(void) #endif } #else -int main(void) +int main(int argc, char *argv[]) { return sof_app_main(); } diff --git a/app/src/sample_usbd.h b/app/src/sample_usbd.h new file mode 100644 index 000000000000..b12a62323d70 --- /dev/null +++ b/app/src/sample_usbd.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_SAMPLES_SUBSYS_USB_COMMON_SAMPLE_USBD_H +#define ZEPHYR_SAMPLES_SUBSYS_USB_COMMON_SAMPLE_USBD_H + +#include +#include + +/* + * The scope of this header is limited to use in USB samples together with the + * new experimental USB device stack, you should not use it in your own + * application. However, you can use the code as a template. + */ + +/* + * This function uses Kconfig.sample_usbd options to configure and initialize a + * USB device. It configures sample's device context, default string descriptors, + * USB device configuration, registers any available class instances, and + * finally initializes USB device. It is limited to a single device with a + * single configuration instantiated in sample_usbd_init.c, which should be + * enough for a simple USB device sample. + * + * It returns the configured and initialized USB device context on success, + * otherwise it returns NULL. + */ +struct usbd_context *sample_usbd_init_device(usbd_msg_cb_t msg_cb); + +/* + * This function is similar to sample_usbd_init_device(), but does not + * initialize the device. It allows the application to set additional features, + * such as additional descriptors. + */ +struct usbd_context *sample_usbd_setup_device(usbd_msg_cb_t msg_cb); + +#endif /* ZEPHYR_SAMPLES_SUBSYS_USB_COMMON_SAMPLE_USBD_H */ diff --git a/app/src/usbd_init.c b/app/src/usbd_init.c new file mode 100644 index 000000000000..51cb6eed58c0 --- /dev/null +++ b/app/src/usbd_init.c @@ -0,0 +1,442 @@ +/* + * Copyright (c) 2026 + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include + +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(usbd_sample_config, LOG_LEVEL_INF); + +/* By default, do not register the USB DFU class DFU mode instance. */ +static const char *const blocklist[] = { + "dfu_dfu", + NULL, +}; + +/* + * Instantiate a context named sample_usbd using the default USB device + * controller, the Zephyr project vendor ID, and the sample product ID. + */ +USBD_DEVICE_DEFINE(sample_usbd, + DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0)), + CONFIG_SAMPLE_USBD_VID, CONFIG_SAMPLE_USBD_PID); + +USBD_DESC_LANG_DEFINE(sample_lang); +USBD_DESC_MANUFACTURER_DEFINE(sample_mfr, CONFIG_SAMPLE_USBD_MANUFACTURER); + +#if defined(CONFIG_PLATFORM_TEENSY41) +#if defined(CONFIG_TEENSY41_BOARD_B) +USBD_DESC_PRODUCT_DEFINE(teensyb_product, "SOF Teensy 4.1 Audio B"); +#elif defined(CONFIG_TEENSY41_BOARD_A) +USBD_DESC_PRODUCT_DEFINE(teensya_product, "SOF Teensy 4.1 Audio A"); +#else +USBD_DESC_PRODUCT_DEFINE(default_product, "SOF Teensy 4.1 Audio"); +#endif +#else +USBD_DESC_PRODUCT_DEFINE(spider_product, "SOF ESP32P4 USB Spider"); +USBD_DESC_PRODUCT_DEFINE(aphid_product, "SOF ESP32P4 USB Aphid"); +USBD_DESC_PRODUCT_DEFINE(pallas_product, "SOF ESP32P4 USB Pallas"); +USBD_DESC_PRODUCT_DEFINE(ceres_product, "SOF ESP32P4 USB Ceres"); +USBD_DESC_PRODUCT_DEFINE(default_product, "SOF ESP32P4 USB"); +#endif + +USBD_DESC_STRING_DEFINE(clock_master_str, "Clock Master", USBD_DUT_STRING_INTERFACE); + +IF_ENABLED(CONFIG_HWINFO, (USBD_DESC_SERIAL_NUMBER_DEFINE(sample_sn))); + +USBD_DESC_CONFIG_DEFINE(fs_cfg_desc, "FS Configuration"); +USBD_DESC_CONFIG_DEFINE(hs_cfg_desc, "HS Configuration"); + +static const uint8_t attributes = (IS_ENABLED(CONFIG_SAMPLE_USBD_SELF_POWERED) ? + USB_SCD_SELF_POWERED : 0) | + (IS_ENABLED(CONFIG_SAMPLE_USBD_REMOTE_WAKEUP) ? + USB_SCD_REMOTE_WAKEUP : 0); + +USBD_CONFIGURATION_DEFINE(sample_fs_config, + attributes, + CONFIG_SAMPLE_USBD_MAX_POWER, &fs_cfg_desc); + +USBD_CONFIGURATION_DEFINE(sample_hs_config, + attributes, + CONFIG_SAMPLE_USBD_MAX_POWER, &hs_cfg_desc); + +enum usbd_speed sample_usbd_get_speed(void) +{ + return usbd_bus_speed(&sample_usbd); +} + +#if defined(CONFIG_PLATFORM_TEENSY41) +#include +#include +#include +#include +#include +#include +#include + +extern uint32_t g_rx_pkt_cnt; +extern uint32_t g_tx_pkt_cnt; +extern struct pipeline *sof_static_pipeline_get(uint32_t pipeline_id); +extern struct comp_dev *sof_static_comp_get(uint32_t comp_id); + +struct teensy_diag_data { + uint32_t tcsr; + uint32_t rcsr; + uint32_t tcr2; + uint32_t rcr2; + uint32_t tcr3; + uint32_t rcr3; + uint32_t tfr0; + uint32_t rfr0; + uint32_t rx_pkt_cnt; + uint32_t tx_pkt_cnt; + uint32_t pipe1_status; + uint32_t pipe2_status; + uint32_t dai5_state; + uint32_t dai6_state; + uint32_t dma_tx_csr; + uint32_t dma_rx_csr; + uint32_t dma_tx_saddr; + uint32_t dma_rx_daddr; + uint32_t dma_tx_citer; + uint32_t dma_rx_citer; + uint32_t c1_state; + uint32_t c2_state; + uint32_t c9_state; + uint32_t c10_state; + uint32_t tcr4; + uint32_t rcr4; + uint32_t tcr5; + uint32_t rcr5; + uint32_t pin8_mux; + uint32_t pin8_daisy; + uint32_t pin7_mux; + uint32_t pin8_pad; + uint32_t pin7_pad; + uint32_t gpio1_psr; + uint32_t gpio2_psr; + uint32_t interface_type; /* 1 = SAI1 I2S, 2 = S/PDIF */ + uint32_t g_dbg[8]; +}; + +#define TEENSY_INTERFACE_SAI1_I2S 1 +#define TEENSY_INTERFACE_SPDIF 2 + +extern uint32_t g_diag_debug[8]; + +static struct net_buf *teensy_diag_to_host(const struct usbd_context *const ctx, + const struct usb_setup_packet *const setup) +{ + struct net_buf *buf; + struct teensy_diag_data diag; + uint16_t len; + + memset(&diag, 0, sizeof(diag)); +#if defined(CONFIG_TEENSY41_INTERFACE_SPDIF) + diag.interface_type = TEENSY_INTERFACE_SPDIF; + diag.tcsr = SPDIF->SCR; + diag.rcsr = SPDIF->SRPC; + diag.tcr2 = SPDIF->STC; + diag.rcr2 = SPDIF->SRFM; + diag.tcr3 = SPDIF->SIS; + diag.rcr3 = SPDIF->SIE; + diag.tfr0 = 0; + diag.rfr0 = 0; + diag.dma_tx_csr = DMA0->TCD[2].CSR; + diag.dma_rx_csr = DMA0->TCD[3].CSR; + diag.dma_tx_saddr = DMA0->TCD[2].SADDR; + diag.dma_rx_daddr = DMA0->TCD[3].DADDR; + diag.dma_tx_citer = DMA0->TCD[2].CITER_ELINKNO; + diag.dma_rx_citer = DMA0->TCD[3].CITER_ELINKNO; +#else + diag.interface_type = TEENSY_INTERFACE_SAI1_I2S; + diag.tcsr = SAI1->TCSR; + diag.rcsr = SAI1->RCSR; + diag.tcr2 = SAI1->TCR2; + diag.rcr2 = SAI1->RCR2; + diag.tcr3 = SAI1->TCR3; + diag.rcr3 = SAI1->RCR3; + diag.tfr0 = (SAI1->TCR3 & I2S_TCR3_TCE(4)) ? SAI1->TFR[2] : SAI1->TFR[0]; + diag.rfr0 = SAI1->RFR[0]; + diag.dma_tx_csr = DMA0->TCD[0].CSR; + diag.dma_rx_csr = DMA0->TCD[1].CSR; + diag.dma_tx_saddr = DMA0->TCD[0].SADDR; + diag.dma_rx_daddr = DMA0->TCD[1].DADDR; + diag.dma_tx_citer = DMA0->TCD[0].CITER_ELINKNO; + diag.dma_rx_citer = DMA0->TCD[1].CITER_ELINKNO; +#endif + + diag.rx_pkt_cnt = g_rx_pkt_cnt; + diag.tx_pkt_cnt = g_tx_pkt_cnt; + + struct pipeline *p1 = sof_static_pipeline_get(1); + struct pipeline *p2 = sof_static_pipeline_get(2); + if (p1) diag.pipe1_status = p1->status; + if (p2) diag.pipe2_status = p2->status; + + struct comp_dev *c1 = sof_static_comp_get(1); + struct comp_dev *c2 = sof_static_comp_get(2); + struct comp_dev *c5 = sof_static_comp_get(5); + struct comp_dev *c6 = sof_static_comp_get(6); + struct comp_dev *c9 = sof_static_comp_get(9); + struct comp_dev *c10 = sof_static_comp_get(10); + if (c1) diag.c1_state = c1->state; + if (c2) diag.c2_state = c2->state; + if (c5) diag.dai5_state = c5->state; + if (c6) diag.dai6_state = c6->state; + if (c9) diag.c9_state = c9->state; + if (c10) diag.c10_state = c10->state; + + diag.tcr4 = SAI1->TCR4; + diag.rcr4 = SAI1->RCR4; + diag.tcr5 = SAI1->TCR5; + diag.rcr5 = SAI1->RCR5; + diag.pin8_mux = *(volatile uint32_t *)0x401F817C; + diag.pin8_daisy = *(volatile uint32_t *)0x401F8594; + diag.pin7_mux = *(volatile uint32_t *)0x401F8180; + diag.pin8_pad = *(volatile uint32_t *)0x401F836C; + diag.pin7_pad = *(volatile uint32_t *)0x401F8370; + diag.gpio1_psr = *(volatile uint32_t *)0x401B8008; + diag.gpio2_psr = *(volatile uint32_t *)0x401BC008; + memcpy(diag.g_dbg, g_diag_debug, sizeof(diag.g_dbg)); + + len = MIN(setup->wLength, sizeof(diag)); + buf = usbd_ep_ctrl_data_in_alloc(ctx, len); + if (!buf) return NULL; + net_buf_add_mem(buf, &diag, len); + return buf; +} + +static int teensy_reboot_to_dev(const struct usbd_context *const ctx, + const struct usb_setup_packet *const setup, + const struct net_buf *const buf) +{ + ARG_UNUSED(ctx); + ARG_UNUSED(setup); + ARG_UNUSED(buf); + __asm__ volatile("bkpt #251"); + return 0; +} + +static struct net_buf *teensy_peek_to_host(const struct usbd_context *const ctx, + const struct usb_setup_packet *const setup) +{ + uint32_t addr = (setup->wIndex << 16) | setup->wValue; + uint16_t len = MIN(setup->wLength, 64); + if (!len) return NULL; + struct net_buf *buf = usbd_ep_ctrl_data_in_alloc(ctx, len); + if (!buf) return NULL; + net_buf_add_mem(buf, (const void *)addr, len); + return buf; +} + +static int teensy_poke_to_dev(const struct usbd_context *const ctx, + const struct usb_setup_packet *const setup, + const struct net_buf *const buf) +{ + ARG_UNUSED(ctx); + uint32_t addr = (setup->wIndex << 16) | setup->wValue; + if (buf && buf->len > 0) { + if (setup->wLength == 1) { + *(volatile uint8_t *)addr = buf->data[0]; + } else if (setup->wLength == 2) { + uint16_t val; + memcpy(&val, buf->data, 2); + *(volatile uint16_t *)addr = val; + } else if (setup->wLength == 4) { + uint32_t val; + memcpy(&val, buf->data, 4); + *(volatile uint32_t *)addr = val; + } + } + return 0; +} + +USBD_VREQUEST_DEFINE(teensy_reboot_vreq, 0xFB, NULL, teensy_reboot_to_dev); +USBD_VREQUEST_DEFINE(teensy_diag_vreq, 0xFC, teensy_diag_to_host, NULL); +USBD_VREQUEST_DEFINE(teensy_peek_poke_vreq, 0xFD, teensy_peek_to_host, teensy_poke_to_dev); +#endif + +static void sample_fix_code_triple(struct usbd_context *uds_ctx, + const enum usbd_speed speed) +{ + 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); + } +} + +struct usbd_context *sample_usbd_setup_device(usbd_msg_cb_t msg_cb) +{ + int err; +#if defined(CONFIG_PLATFORM_TEENSY41) + struct usbd_desc_node *product_desc; +#if defined(CONFIG_TEENSY41_BOARD_B) + product_desc = &teensyb_product; + LOG_INF("Board Identity: TEENSY 4.1 BOARD B (Slave / Rx)"); +#elif defined(CONFIG_TEENSY41_BOARD_A) + product_desc = &teensya_product; + LOG_INF("Board Identity: TEENSY 4.1 BOARD A (Master / Tx)"); +#else + product_desc = &default_product; + LOG_INF("Board Identity: Generic Teensy 4.1 Audio"); +#endif +#elif defined(CONFIG_PLATFORM_ESP32P4) + uint8_t mac[6] = {0}; + extern int esp_efuse_mac_get_default(uint8_t *mac); + esp_efuse_mac_get_default(mac); + + printk("[USB INIT] Chip MAC: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + struct usbd_desc_node *product_desc = &default_product; + if (mac[5] == 0xC7 || mac[5] == 0xc7) { + LOG_INF("Board Identity: SPIDER PDM DUT (MAC %02X:%02X:%02X:%02X:%02X:%02X)", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + product_desc = &spider_product; + } else if (mac[5] == 0x15) { + LOG_INF("Board Identity: APHID I2S DUT (MAC %02X:%02X:%02X:%02X:%02X:%02X)", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + product_desc = &aphid_product; + } else if (mac[5] == 0x17) { + LOG_INF("Board Identity: PALLAS TX MASTER (MAC %02X:%02X:%02X:%02X:%02X:%02X)", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + product_desc = &pallas_product; + } else if (mac[5] == 0x6C || mac[5] == 0x6c) { + LOG_INF("Board Identity: CERES RX SLAVE (MAC %02X:%02X:%02X:%02X:%02X:%02X)", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + product_desc = &ceres_product; + } else { + LOG_INF("Board Identity: Generic ESP32-P4 (MAC %02X:%02X:%02X:%02X:%02X:%02X)", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + product_desc = &default_product; + } +#else + struct usbd_desc_node *product_desc = &default_product; +#endif + + err = usbd_add_descriptor(&sample_usbd, &sample_lang); + if (err) { + LOG_ERR("Failed to initialize language descriptor (%d)", err); + return NULL; + } + + err = usbd_add_descriptor(&sample_usbd, &sample_mfr); + if (err) { + LOG_ERR("Failed to initialize manufacturer descriptor (%d)", err); + return NULL; + } + + err = usbd_add_descriptor(&sample_usbd, product_desc); + if (err) { + LOG_ERR("Failed to initialize product descriptor (%d)", err); + return NULL; + } + + IF_ENABLED(CONFIG_HWINFO, ( + err = usbd_add_descriptor(&sample_usbd, &sample_sn); + if (err) { + LOG_ERR("Failed to initialize SN descriptor (%d)", err); + return NULL; + } + )) + + err = usbd_add_descriptor(&sample_usbd, &clock_master_str); + if (err) { + LOG_ERR("Failed to initialize Clock Master string descriptor (%d)", err); + return NULL; + } + LOG_INF("Registered Clock Master string descriptor at index %u", clock_master_str.str.idx); + + if (USBD_SUPPORTS_HIGH_SPEED && + usbd_caps_speed(&sample_usbd) == USBD_SPEED_HS) { + err = usbd_add_configuration(&sample_usbd, USBD_SPEED_HS, + &sample_hs_config); + if (err) { + LOG_ERR("Failed to add High-Speed configuration"); + return NULL; + } + + err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_HS, 1, + blocklist); + if (err) { + LOG_ERR("Failed to register classes"); + return NULL; + } + + sample_fix_code_triple(&sample_usbd, USBD_SPEED_HS); + } + + err = usbd_add_configuration(&sample_usbd, USBD_SPEED_FS, + &sample_fs_config); + if (err) { + LOG_ERR("Failed to add Full-Speed configuration"); + return NULL; + } + + err = usbd_register_all_classes(&sample_usbd, USBD_SPEED_FS, 1, blocklist); + if (err) { + LOG_ERR("Failed to register classes"); + return NULL; + } + + sample_fix_code_triple(&sample_usbd, USBD_SPEED_FS); + usbd_self_powered(&sample_usbd, attributes & USB_SCD_SELF_POWERED); + + if (msg_cb != NULL) { + err = usbd_msg_register_cb(&sample_usbd, msg_cb); + if (err) { + LOG_ERR("Failed to register message callback"); + return NULL; + } + } + +#if defined(CONFIG_PLATFORM_TEENSY41) + err = usbd_device_register_vreq(&sample_usbd, &teensy_reboot_vreq); + if (err) { + LOG_ERR("Failed to register vendor reboot request (%d)", err); + } + err = usbd_device_register_vreq(&sample_usbd, &teensy_diag_vreq); + if (err) { + LOG_ERR("Failed to register vendor diag request (%d)", err); + } + err = usbd_device_register_vreq(&sample_usbd, &teensy_peek_poke_vreq); + if (err) { + LOG_ERR("Failed to register vendor peek/poke request (%d)", err); + } +#endif + + return &sample_usbd; +} + +struct usbd_context *sample_usbd_init_device(usbd_msg_cb_t msg_cb) +{ + int err; + + if (sample_usbd_setup_device(msg_cb) == NULL) { + return NULL; + } + + err = usbd_init(&sample_usbd); + if (err) { + LOG_ERR("Failed to initialize device support"); + return NULL; + } + + return &sample_usbd; +} diff --git a/scripts/cmake/misc.cmake b/scripts/cmake/misc.cmake index 647b69c8c478..f652e40ca410 100644 --- a/scripts/cmake/misc.cmake +++ b/scripts/cmake/misc.cmake @@ -42,7 +42,7 @@ endfunction() # Zephyr duplicate in sof/zephyr/CMakeLists.txt; keep in sync macro(is_zephyr ret) - if(CONFIG_ZEPHYR_SOF_MODULE) + if(CONFIG_ZEPHYR_SOF_MODULE OR CONFIG_SOF_FULL_ZEPHYR_APPLICATION) set(${ret} TRUE) else() set(${ret} FALSE) diff --git a/src/audio/CMakeLists.txt b/src/audio/CMakeLists.txt index 92002c8b7c1c..ebd2f708eef4 100644 --- a/src/audio/CMakeLists.txt +++ b/src/audio/CMakeLists.txt @@ -16,6 +16,7 @@ if(NOT CONFIG_COMP_MODULE_SHARED_LIBRARY_BUILD) add_subdirectory(buffers) add_subdirectory(pcm_converter) add_subdirectory(pipeline) + add_local_sources_ifdef(CONFIG_COMP_USB_AUDIO sof usb_audio.c) if(CONFIG_COMP_BASEFW_IPC4 AND NOT CONFIG_LIBRARY) add_local_sources(sof base_fw.c) diff --git a/src/audio/Kconfig b/src/audio/Kconfig index 53f5301760f9..9584632b3d1a 100644 --- a/src/audio/Kconfig +++ b/src/audio/Kconfig @@ -102,6 +102,12 @@ config STATIC_PIPELINE_MAX_ROUTES Maximum number of static graph connection routes allowed by the static pipeline loader. +config COMP_USB_AUDIO + bool "USB Audio streaming component" + default y if (PLATFORM_ESP32P4 || PLATFORM_TEENSY41) + help + Enable USB Audio Class 2 (UAC2) streaming component for SOF. + config COMP_CHAIN_DMA bool "Chain DMA component" depends on IPC_MAJOR_4 diff --git a/src/audio/dai-zephyr.c b/src/audio/dai-zephyr.c index 7c011e25b0be..1c910b3603c3 100644 --- a/src/audio/dai-zephyr.c +++ b/src/audio/dai-zephyr.c @@ -54,10 +54,40 @@ */ #ifndef local_to_host #define local_to_host(addr) (addr) -#endif /* local_to_host */ +#endif + +#if defined(CONFIG_PLATFORM_TEENSY41) +extern uint32_t g_diag_debug[8]; +#endif static const struct comp_driver comp_dai; +#if defined(CONFIG_PLATFORM_TEENSY41) +#include + +static struct comp_dev *s_active_sched_dai = NULL; + +bool dai_zephyr_has_active_audio_domain(void) +{ + return (s_active_sched_dai != NULL); +} + +static void dai_zephyr_dma_period_cb(const struct device *dma_dev, void *user_data, + uint32_t channel, int status) +{ + struct comp_dev *dev = user_data; + + ARG_UNUSED(dma_dev); + ARG_UNUSED(channel); + ARG_UNUSED(status); + + if (dev != s_active_sched_dai) + return; + + zephyr_domain_audio_timer_cb(); +} +#endif + LOG_MODULE_REGISTER(dai_comp, CONFIG_SOF_LOG_LEVEL); SOF_DEFINE_REG_UUID(dai); @@ -152,7 +182,7 @@ __cold int dai_set_config(struct dai *dai, struct ipc_config_dai *common_config, cfg.dai_index = common_config->dai_index; is_blob = common_config->is_config_blob; cfg.format = common_config->format; - cfg.options = is_blob ? 0 : sof_cfg->flags; + cfg.options = (is_blob || !spec_config || size < SOF_DAI_CONFIG_HW_SPEC_OFFSET) ? 0 : sof_cfg->flags; cfg.rate = common_config->sampling_frequency; switch (common_config->type) { @@ -180,15 +210,23 @@ __cold int dai_set_config(struct dai *dai, struct ipc_config_dai *common_config, break; case SOF_DAI_IMX_SAI: cfg.type = DAI_IMX_SAI; - cfg_params = &sof_cfg->sai; + cfg_params = (size >= sizeof(struct sof_ipc_dai_config)) ? + (is_blob ? spec_config : &sof_cfg->sai) : NULL; break; case SOF_DAI_IMX_ESAI: cfg.type = DAI_IMX_ESAI; - cfg_params = &sof_cfg->esai; + cfg_params = (size >= sizeof(struct sof_ipc_dai_config)) ? + (is_blob ? spec_config : &sof_cfg->esai) : NULL; break; case SOF_DAI_IMX_MICFIL: cfg.type = DAI_IMX_MICFIL; - cfg_params = &sof_cfg->micfil; + cfg_params = (size >= sizeof(struct sof_ipc_dai_config)) ? + (is_blob ? spec_config : &sof_cfg->micfil) : NULL; + break; + case SOF_DAI_IMX_SPDIF: + cfg.type = DAI_IMX_SPDIF; + cfg_params = (size >= sizeof(struct sof_ipc_dai_config)) ? + (is_blob ? spec_config : &sof_cfg->spdif) : NULL; break; case SOF_DAI_AMD_SDW: cfg.type = DAI_AMD_SDW; @@ -224,10 +262,23 @@ __cold int dai_set_config(struct dai *dai, struct ipc_config_dai *common_config, } if (!is_blob) { - if (size < SOF_DAI_CONFIG_HW_SPEC_OFFSET) - return -EINVAL; + switch (common_config->type) { + case SOF_DAI_IMX_SAI: + case SOF_DAI_IMX_SPDIF: + case SOF_DAI_IMX_ESAI: + case SOF_DAI_IMX_MICFIL: + if (size >= SOF_DAI_CONFIG_HW_SPEC_OFFSET) + dai_cfg_size -= SOF_DAI_CONFIG_HW_SPEC_OFFSET; + else + dai_cfg_size = size; + break; + default: + if (size < SOF_DAI_CONFIG_HW_SPEC_OFFSET) + return -EINVAL; - dai_cfg_size -= SOF_DAI_CONFIG_HW_SPEC_OFFSET; + dai_cfg_size -= SOF_DAI_CONFIG_HW_SPEC_OFFSET; + break; + } } return dai_config_set(dev, &cfg, cfg_params, dai_cfg_size); @@ -715,6 +766,10 @@ __cold static void dai_free(struct comp_dev *dev) /* heap is NULL to match what is passed in dai_new() */ sof_heap_free(NULL, dd); +#if defined(CONFIG_PLATFORM_TEENSY41) + if (s_active_sched_dai == dev) + s_active_sched_dai = NULL; +#endif comp_free_device(dev); } @@ -816,7 +871,11 @@ static int dai_get_dma_slot(struct dai_data *dd, struct comp_dev *dev, uint32_t case DAI_IMX_SAI: case DAI_IMX_ESAI: case DAI_IMX_MICFIL: - *slot = (hs & GENMASK(15, 8)) >> 8; + case DAI_IMX_SPDIF: + if (hs & GENMASK(15, 8)) + *slot = (hs & GENMASK(15, 8)) >> 8; + else + *slot = hs; break; default: *slot = hs; @@ -942,6 +1001,7 @@ static int dai_set_dma_config(struct dai_data *dd, struct comp_dev *dev) comp_err(dev, "dma_cfg allocation failed"); return -ENOMEM; } + memset(dma_cfg, 0, sizeof(*dma_cfg)); if (dev->direction == SOF_IPC_STREAM_PLAYBACK) dma_cfg->channel_direction = MEMORY_TO_PERIPHERAL; @@ -951,15 +1011,30 @@ static int dai_set_dma_config(struct dai_data *dd, struct comp_dev *dev) dma_cfg->source_data_size = config->src_width; dma_cfg->dest_data_size = config->dest_width; - if (config->burst_elems) + if (dd->dai->type == SOF_DAI_IMX_SAI) { + /* SAI FIFO has 32 entries with 16-word watermark. + * Minor loop burst size in bytes must match the 16-word watermark: + * 16 words * sample_bytes (32 bytes for 16-bit audio). + */ + dma_cfg->source_burst_length = 16 * config->src_width; + } else if (dd->dai->type == SOF_DAI_IMX_SPDIF) { + /* S/PDIF: 8 bytes per minor loop (interleaved L + R) */ + dma_cfg->source_burst_length = 8; + } else if (config->burst_elems) { dma_cfg->source_burst_length = config->burst_elems; - else + } else { dma_cfg->source_burst_length = 8; + } dma_cfg->dest_burst_length = dma_cfg->source_burst_length; dma_cfg->cyclic = config->cyclic; +#if defined(CONFIG_PLATFORM_TEENSY41) + dma_cfg->user_data = dev; + dma_cfg->dma_callback = dai_zephyr_dma_period_cb; +#else dma_cfg->user_data = NULL; dma_cfg->dma_callback = NULL; +#endif dma_cfg->block_count = config->elem_array.count; if (dev->direction == SOF_IPC_STREAM_PLAYBACK) dma_cfg->dma_slot = config->dest_dev; @@ -974,6 +1049,7 @@ static int dai_set_dma_config(struct dai_data *dd, struct comp_dev *dev) comp_err(dev, "dma_block_config allocation failed"); return -ENOMEM; } + memset(dma_block_cfg, 0, sizeof(struct dma_block_config) * dma_cfg->block_count); dma_cfg->head_block = dma_block_cfg; for (i = 0; i < dma_cfg->block_count; i++) { @@ -984,15 +1060,21 @@ static int dai_set_dma_config(struct dai_data *dd, struct comp_dev *dev) local_to_host(config->elem_array.elems[i].src); dma_block_cfg->dest_address = config->elem_array.elems[i].dest; - dma_block_cfg->source_addr_adj = DMA_ADDR_ADJ_DECREMENT; - dma_block_cfg->dest_addr_adj = DMA_ADDR_ADJ_INCREMENT; + dma_block_cfg->source_addr_adj = DMA_ADDR_ADJ_INCREMENT; + dma_block_cfg->dest_addr_adj = DMA_ADDR_ADJ_NO_CHANGE; + if (dma_cfg->block_count > 1) { + dma_block_cfg->source_gather_en = 1; + } } else { dma_block_cfg->source_address = config->elem_array.elems[i].src; dma_block_cfg->dest_address = local_to_host(config->elem_array.elems[i].dest); - dma_block_cfg->source_addr_adj = DMA_ADDR_ADJ_INCREMENT; - dma_block_cfg->dest_addr_adj = DMA_ADDR_ADJ_DECREMENT; + dma_block_cfg->source_addr_adj = DMA_ADDR_ADJ_NO_CHANGE; + dma_block_cfg->dest_addr_adj = DMA_ADDR_ADJ_INCREMENT; + if (dma_cfg->block_count > 1) { + dma_block_cfg->dest_scatter_en = 1; + } } prev = dma_block_cfg; prev->next_block = ++dma_block_cfg; @@ -1270,6 +1352,12 @@ int dai_common_prepare(struct dai_data *dd, struct comp_dev *dev) return -EINVAL; } + if (!dd->z_config) { + comp_err(dev, "Missing dd->z_config."); + comp_set_state(dev, COMP_TRIGGER_RESET); + return -EINVAL; + } + /* clear dma buffer to avoid pop noise */ buffer_zero(dd->dma_buffer); @@ -1310,6 +1398,7 @@ static int dai_prepare(struct comp_dev *dev) void dai_common_reset(struct dai_data *dd, struct comp_dev *dev) { +#if !defined(CONFIG_PLATFORM_TEENSY41) struct dma_sg_config *config = &dd->config; /* @@ -1330,6 +1419,7 @@ void dai_common_reset(struct dai_data *dd, struct comp_dev *dev) buffer_free(dd->dma_buffer); dd->dma_buffer = NULL; } +#endif dd->wallclock = 0; dd->total_data_processed = 0; @@ -1367,10 +1457,17 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, /* only start the DAI if we are not XRUN handling */ if (dd->xrun == 0) { + if (dd->z_config) + sof_dma_config(dd->dma, dd->chan_index, dd->z_config); ret = sof_dma_start(dd->dma, dd->chan_index); if (ret < 0) return ret; +#if defined(CONFIG_PLATFORM_TEENSY41) + if (!s_active_sched_dai) + s_active_sched_dai = dev; +#endif + /* start the DAI */ dai_trigger_op(dd->dai, cmd, dev->direction); } else { @@ -1402,6 +1499,9 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, if (!(dd->dai->dma_caps & SOF_DMA_CAP_HDA)) audio_stream_reset(&dd->dma_buffer->stream); +#if defined(CONFIG_PLATFORM_TEENSY41) + dd->xrun = 0; +#endif /* only start the DAI if we are not XRUN handling */ if (dd->xrun == 0) { /* recover valid start position */ @@ -1418,6 +1518,11 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, if (ret < 0) return ret; +#if defined(CONFIG_PLATFORM_TEENSY41) + if (!s_active_sched_dai) + s_active_sched_dai = dev; +#endif + /* start the DAI */ dai_trigger_op(dd->dai, cmd, dev->direction); } else { @@ -1433,6 +1538,10 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, COMPILER_FALLTHROUGH; case COMP_TRIGGER_STOP: comp_dbg(dev, "STOP"); +#if defined(CONFIG_PLATFORM_TEENSY41) + if (s_active_sched_dai == dev) + s_active_sched_dai = NULL; +#endif /* * Some platforms cannot just simple disable * DMA channel during the transfer, @@ -1455,6 +1564,10 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, break; case COMP_TRIGGER_PAUSE: comp_dbg(dev, "PAUSE"); +#if defined(CONFIG_PLATFORM_TEENSY41) + if (s_active_sched_dai == dev) + s_active_sched_dai = NULL; +#endif #if CONFIG_COMP_DAI_STOP_TRIGGER_ORDER_REVERSE ret = sof_dma_suspend(dd->dma, dd->chan_index); dai_trigger_op(dd->dai, cmd, dev->direction); @@ -1466,6 +1579,9 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, case COMP_TRIGGER_PRE_START: case COMP_TRIGGER_PRE_RELEASE: /* only start the DAI if we are not XRUN handling */ +#if defined(CONFIG_PLATFORM_TEENSY41) + dd->xrun = 0; +#endif if (dd->xrun) dd->xrun = 0; else @@ -1473,16 +1589,11 @@ static int dai_comp_trigger_internal(struct dai_data *dd, struct comp_dev *dev, break; } -#ifdef CONFIG_IPC_MAJOR_3 - /* TODO: investigate why making this IPC version-agnostic - * breaks some Intel tests and check if doing so would be - * possible (or even make sense) on Intel platforms. - * - * See issue #8920 for details. - */ +#if defined(CONFIG_IPC_MAJOR_3) || defined(CONFIG_PLATFORM_TEENSY41) + /* On static topology platforms (Teensy 4.1), DAI state must transition cleanly */ if (!ret) return comp_set_state(dev, cmd); -#endif /* CONFIG_IPC_MAJOR_3 */ +#endif /* CONFIG_IPC_MAJOR_3 || CONFIG_PLATFORM_TEENSY41 */ return ret; } @@ -1717,14 +1828,15 @@ static void set_new_local_buffer(struct dai_data *dd, struct comp_dev *dev) uint32_t dma_fmt = audio_stream_get_frm_fmt(&dd->dma_buffer->stream); uint32_t local_fmt; - if (dev->direction == SOF_IPC_STREAM_PLAYBACK) + if (dev->direction == SOF_IPC_STREAM_PLAYBACK) { dd->local_buffer = comp_dev_get_first_data_producer(dev); - else + local_fmt = audio_stream_get_frm_fmt(&dd->local_buffer->stream); + dd->process = pcm_get_conversion_function(local_fmt, dma_fmt); + } else { dd->local_buffer = comp_dev_get_first_data_consumer(dev); - - local_fmt = audio_stream_get_frm_fmt(&dd->local_buffer->stream); - - dd->process = pcm_get_conversion_function(local_fmt, dma_fmt); + local_fmt = audio_stream_get_frm_fmt(&dd->local_buffer->stream); + dd->process = pcm_get_conversion_function(dma_fmt, local_fmt); + } if (!dd->process) { comp_err(dev, "converter function NULL: local fmt %d dma fmt %d\n", @@ -1877,6 +1989,12 @@ int dai_common_copy(struct dai_data *dd, struct comp_dev *dev, pcm_converter_fun /* return if nothing to copy */ if (!copy_bytes) { +#if defined(CONFIG_PLATFORM_TEENSY41) + if (dev->direction == SOF_IPC_STREAM_PLAYBACK) + g_diag_debug[1] = 0xE0000000 | (src_frames & 0xFFFF); + else + g_diag_debug[5] = 0xE0000000 | (src_frames & 0xFFFF); +#endif #if CONFIG_DAI_VERBOSE_GLITCH_WARNINGS comp_warn(dev, "nothing to copy, src_frames: %u, sink_frames: %u", src_frames, sink_frames); @@ -1887,8 +2005,8 @@ int dai_common_copy(struct dai_data *dd, struct comp_dev *dev, pcm_converter_fun /* trigger optional DAI_TRIGGER_COPY which prepares dai to copy */ ret = dai_trigger(dd->dai->dev, dev->direction, DAI_TRIGGER_COPY); - if (ret < 0) - comp_warn(dev, "dai trigger copy failed"); + if (ret < 0 && ret != -ENOTSUP && ret != -EOPNOTSUPP && ret != -EINVAL) + comp_warn(dev, "dai trigger copy failed: %d", ret); if (dai_dma_cb(dd, dev, copy_bytes, converter) == SOF_DMA_CB_STATUS_END) sof_dma_stop(dd->dma, dd->chan_index); @@ -1901,6 +2019,26 @@ int dai_common_copy(struct dai_data *dd, struct comp_dev *dev, pcm_converter_fun dai_dma_position_update(dd, dev); +#if defined(CONFIG_PLATFORM_TEENSY41) + if (dev->direction == SOF_IPC_STREAM_PLAYBACK) { + g_diag_debug[0]++; + g_diag_debug[1] = (copy_bytes << 16) | (src_frames & 0xFFFF); + if (dd->dma_buffer) { + uint32_t *p = (uint32_t *)audio_stream_get_addr(&dd->dma_buffer->stream); + g_diag_debug[2] = p[0]; + g_diag_debug[3] = p[1]; + } + } else { + g_diag_debug[4]++; + g_diag_debug[5] = (copy_bytes << 16) | (src_frames & 0xFFFF); + if (dd->dma_buffer) { + uint32_t *p = (uint32_t *)audio_stream_get_addr(&dd->dma_buffer->stream); + g_diag_debug[6] = p[0]; + g_diag_debug[7] = p[1]; + } + } +#endif + return ret; } diff --git a/src/audio/pipeline/CMakeLists.txt b/src/audio/pipeline/CMakeLists.txt index eca2f06857d0..64c8cd421c9e 100644 --- a/src/audio/pipeline/CMakeLists.txt +++ b/src/audio/pipeline/CMakeLists.txt @@ -13,3 +13,7 @@ add_local_sources_ifdef(CONFIG_STATIC_PIPELINE sof static_pipeline_modules.c static_pipeline_uac2.c ) + +add_local_sources_ifdef(CONFIG_PLATFORM_TEENSY41 sof + sof_static_pipeline.c +) diff --git a/src/audio/pipeline/pipeline-xrun.c b/src/audio/pipeline/pipeline-xrun.c index c535ddbb84a1..c98ac89d5820 100644 --- a/src/audio/pipeline/pipeline-xrun.c +++ b/src/audio/pipeline/pipeline-xrun.c @@ -53,7 +53,14 @@ static int pipeline_comp_xrun(struct comp_dev *current, #endif /* CONFIG_IPC_MAJOR_3 */ -#if NO_XRUN_RECOVERY +#if defined(CONFIG_PLATFORM_TEENSY41) +/* Standalone audio bridges: self-heal by clearing xrun_bytes and keep streaming without pipeline reset */ +int pipeline_xrun_recover(struct pipeline *p) +{ + p->xrun_bytes = 0; + return 0; +} +#elif NO_XRUN_RECOVERY /* recover the pipeline from a XRUN condition */ int pipeline_xrun_recover(struct pipeline *p) { diff --git a/src/audio/pipeline/sof_static_pipeline.c b/src/audio/pipeline/sof_static_pipeline.c new file mode 100644 index 000000000000..3f95a4de27df --- /dev/null +++ b/src/audio/pipeline/sof_static_pipeline.c @@ -0,0 +1,749 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#include +#include +#include +#if CONFIG_COMP_BT_AUDIO +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_PLATFORM_ESP32P4) +#include +#include +#include +#include +#endif + +LOG_MODULE_REGISTER(sof_static_pipeline, CONFIG_SOF_LOG_LEVEL); + +#define PLAYBACK_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(i2s_fu)) +#define PLAYBACK_EQ_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(pb_eq_fu)) +#define PLAYBACK_DRC_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(pb_drc_fu)) +#if DT_NODE_EXISTS(DT_NODELABEL(cap_tdfb_fu)) +#define CAPTURE_TDFB_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(cap_tdfb_fu)) +#else +#define CAPTURE_TDFB_FU_ID 0xFF +#endif +#define CAPTURE_EQ_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(cap_eq_fu)) +#define CAPTURE_FU_ID UAC2_ENTITY_ID(DT_NODELABEL(i2s_in_fu)) +#define PLAYBACK_TERM_ID UAC2_ENTITY_ID(DT_NODELABEL(i2s_out_terminal)) +#define CAPTURE_TERM_ID UAC2_ENTITY_ID(DT_NODELABEL(i2s_in_terminal)) + +#if defined(CONFIG_NOCACHE_MEMORY) +K_MEM_SLAB_DEFINE_IN_SECT_STATIC(uac2_rx_slab, __nocache, 256, 32, 64); +K_MEM_SLAB_DEFINE_IN_SECT_STATIC(uac2_tx_slab, __nocache, 256, 64, 64); +#else +K_MEM_SLAB_DEFINE_STATIC(uac2_rx_slab, 256, 32, 64); +K_MEM_SLAB_DEFINE_STATIC(uac2_tx_slab, 256, 64, 64); +#endif + +static struct sof_static_pipeline_status g_status = { + .playback_active = false, + .capture_active = false, + .sample_rate = 48000, + .active_interface = SOF_AUDIO_IF_I2S, + .clock_mode = SOF_CLOCK_MASTER, + .audio_route = SOF_AUDIO_ROUTE_USB_DAI, + .bt_stream_enabled = false, + .playback_volume = 0, + .playback_mute = false, + .capture_volume = 0, + .capture_mute = false, + .eq_playback_bypassed = false, + .drc_playback_bypassed = false, + .tdfb_capture_bypassed = false, + .eq_capture_bypassed = false, +}; + +static uint32_t s_sof_diag_cnt; +uint32_t g_rx_pkt_cnt = 0; +uint32_t g_tx_pkt_cnt = 0; + +static void sof_uac2_send_capture_pending(const struct device *dev) +{ + if (!g_status.capture_active || !dev) { + return; + } + + uint32_t rate = g_status.sample_rate ? g_status.sample_rate : 48000; + uint32_t frame_bytes; + if (g_status.microframes) { + frame_bytes = (rate / 8000) * 4; /* 6 samples * 4 bytes = 24 bytes */ + } else { + frame_bytes = (rate / 1000) * 4; /* 48 samples * 4 bytes = 192 bytes */ + } + if (frame_bytes == 0 || frame_bytes > 512) { + frame_bytes = g_status.microframes ? 24 : 192; + } + + while (1) { + void *buf = NULL; + if (k_mem_slab_alloc(&uac2_tx_slab, &buf, K_NO_WAIT) != 0) { + static uint32_t s_slab_alloc_fails; + s_slab_alloc_fails++; + if (s_slab_alloc_fails <= 5 || s_slab_alloc_fails % 100 == 0) { + LOG_WRN("[UAC2 TX SLAB FULL %u] Failed to allocate TX slab block!", s_slab_alloc_fails); + } + break; + } + + bool have_data = false; +#if CONFIG_COMP_BT_AUDIO + if (g_status.audio_route == SOF_AUDIO_ROUTE_USB_BT) { + have_data = bt_audio_peek_capture_data(buf, frame_bytes); + } else +#endif + { + have_data = usb_audio_peek_capture_data(buf, frame_bytes); + } + + if (!have_data) { + k_mem_slab_free(&uac2_tx_slab, buf); + break; + } + + if (g_status.capture_mute) { + memset(buf, 0, frame_bytes); + } + + int ret = usbd_uac2_send(dev, CAPTURE_TERM_ID, buf, frame_bytes); + if (ret == 0) { + g_tx_pkt_cnt++; + if (!g_status.capture_mute) { +#if CONFIG_COMP_BT_AUDIO + if (g_status.audio_route == SOF_AUDIO_ROUTE_USB_BT) { + bt_audio_consume_capture_data(frame_bytes); + } else +#endif + { + usb_audio_consume_capture_data(frame_bytes); + } + } + } else { + k_mem_slab_free(&uac2_tx_slab, buf); + break; + } + } +} + +/* UAC2 Class Callbacks */ +void sof_uac2_sof_cb(const struct device *dev, void *user_data) +{ + ARG_UNUSED(user_data); + + if (g_status.playback_active || g_status.capture_active) { + s_sof_diag_cnt++; +#if defined(CONFIG_PLATFORM_ESP32P4) + if (s_sof_diag_cnt % 1000 == 0) { + LOG_INF("[DIAG SOF %u] tx_start=%u, rx_start=%u, int_raw=0x%08x, state=0x%08x | I2S0: rx_conf=0x%08x, rx_conf1=0x%08x, rx_tdm=0x%08x, rx_eof=%u", + s_sof_diag_cnt, + (unsigned int)I2S0.tx_conf.tx_start, + (unsigned int)I2S0.rx_conf.rx_start, + (unsigned int)I2S0.int_raw.val, + (unsigned int)I2S0.state.val, + (unsigned int)I2S0.rx_conf.val, + (unsigned int)I2S0.rx_conf1.val, + (unsigned int)I2S0.rx_tdm_ctrl.val, + (unsigned int)I2S0.rx_eof_num.rx_eof_num); + LOG_INF("[DIAG GPIOS] lo=0x%08x, hi=0x%08x (G2 lvl=%u, G3 lvl=%u, G20(DIN)=%u, G21(BCK)=%u, G22(WS)=%u, G23(DOUT)=%u)", + (uint32_t)GPIO.in.val, (uint32_t)GPIO.in1.val, + (uint32_t)((GPIO.in.val >> 2) & 1), + (uint32_t)((GPIO.in.val >> 3) & 1), + (uint32_t)((GPIO.in.val >> 20) & 1), + (uint32_t)((GPIO.in.val >> 21) & 1), + (uint32_t)((GPIO.in.val >> 22) & 1), + (uint32_t)((GPIO.in.val >> 23) & 1)); + } +#endif + } else { + s_sof_diag_cnt = 0; + } + + if (g_status.capture_active && dev) { + sof_uac2_send_capture_pending(dev); + } +} + +void sof_uac2_terminal_update_cb(const struct device *dev, uint8_t terminal, + bool enabled, bool microframes, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(user_data); + + LOG_INF("[UAC2 Terminal] update: terminal %u, enabled %d, microframes %d (PB expected %u, CAP expected %u)", + terminal, enabled, microframes, PLAYBACK_TERM_ID, CAPTURE_TERM_ID); + + g_status.microframes = microframes; + + if (terminal == PLAYBACK_TERM_ID) { + g_status.playback_active = enabled; + sof_static_pipeline_trigger_by_uac2_term(terminal, enabled); + } else if (terminal == CAPTURE_TERM_ID) { + g_status.capture_active = enabled; + sof_static_pipeline_trigger_by_uac2_term(terminal, enabled); + } +} + +void *sof_uac2_get_recv_buf(const struct device *dev, uint8_t terminal, + uint16_t size, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(terminal); + ARG_UNUSED(user_data); + + void *buf = NULL; + if (k_mem_slab_alloc(&uac2_rx_slab, &buf, K_NO_WAIT) != 0) { + return NULL; + } + return buf; +} + +void sof_uac2_data_recv_cb(const struct device *dev, uint8_t terminal, + void *buf, uint16_t size, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(terminal); + ARG_UNUSED(user_data); + + static uint32_t s_rx_pkt_cnt; + s_rx_pkt_cnt++; + g_rx_pkt_cnt++; + if (s_rx_pkt_cnt == 1 || s_rx_pkt_cnt % 1000 == 0) { + LOG_INF("[SOF UAC2] Received playback packet #%u, size %u bytes", s_rx_pkt_cnt, size); + } + + if (buf && size > 0) { +#if CONFIG_COMP_BT_AUDIO + if (g_status.audio_route == SOF_AUDIO_ROUTE_USB_BT) { + bt_audio_feed_playback_data(buf, size); + } else +#endif + { + usb_audio_feed_playback_data(buf, size); + } + } + if (buf) { + k_mem_slab_free(&uac2_rx_slab, buf); + } +} + +void sof_uac2_buf_release(const struct device *dev, uint8_t terminal, + void *buf, void *user_data) +{ + ARG_UNUSED(user_data); + + if (buf) { + k_mem_slab_free(&uac2_tx_slab, buf); + } + + if (terminal == CAPTURE_TERM_ID && dev) { + sof_uac2_send_capture_pending(dev); + } +} + +uint32_t sof_uac2_get_sample_rate(const struct device *dev, uint8_t clock_id, + void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(clock_id); + ARG_UNUSED(user_data); + + return g_status.sample_rate ? g_status.sample_rate : 48000; +} + +int sof_uac2_set_sample_rate(const struct device *dev, uint8_t clock_id, + uint32_t rate, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(clock_id); + ARG_UNUSED(user_data); + + LOG_INF("Host set sample rate: %u Hz", rate); + g_status.sample_rate = rate; + sof_static_set_sample_rate(rate); + usb_audio_set_playback_rate(rate); + usb_audio_set_capture_rate(rate); + return 0; +} + +enum usbd_speed sample_usbd_get_speed(void); + +uint32_t sof_uac2_feedback_cb(const struct device *dev, uint8_t terminal, + void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(terminal); + ARG_UNUSED(user_data); + + uint32_t rate = g_status.sample_rate ? g_status.sample_rate : 48000; + + if (sample_usbd_get_speed() == USBD_SPEED_HS) { + /* High-Speed UAC2 explicit feedback: Q16.16 format in samples per microframe (8000/s) */ + return (uint32_t)(((uint64_t)rate << 16) / 8000); + } else { + /* Full-Speed UAC2 explicit feedback: Q10.14 format in samples per frame (1000/s) */ + return (uint32_t)(((uint64_t)rate << 14) / 1000); + } +} + +int sof_uac2_set_feature_mute(const struct device *dev, uint8_t entity_id, + uint8_t channel, bool mute, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(channel); + ARG_UNUSED(user_data); + + LOG_INF("Host set mute/switch: entity=%u, ch=%u, mute=%d", entity_id, channel, mute); + if (entity_id == PLAYBACK_FU_ID) { + g_status.playback_mute = mute; + } else if (entity_id == PLAYBACK_EQ_FU_ID) { + g_status.eq_playback_bypassed = mute; + } else if (entity_id == PLAYBACK_DRC_FU_ID) { + g_status.drc_playback_bypassed = mute; + } else if (entity_id == CAPTURE_TDFB_FU_ID) { + g_status.tdfb_capture_bypassed = mute; + } else if (entity_id == CAPTURE_EQ_FU_ID) { + g_status.eq_capture_bypassed = mute; + } else if (entity_id == CAPTURE_FU_ID) { + g_status.capture_mute = mute; + } + + sof_static_kcontrol_set_by_uac2(entity_id, channel, mute ? 1 : 0, false); + return 0; +} + +int sof_uac2_get_feature_mute(const struct device *dev, uint8_t entity_id, + uint8_t channel, bool *mute, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(channel); + ARG_UNUSED(user_data); + + if (!mute) { + return -EINVAL; + } + + if (entity_id == PLAYBACK_FU_ID) { + *mute = g_status.playback_mute; + return 0; + } else if (entity_id == CAPTURE_FU_ID) { + *mute = g_status.capture_mute; + return 0; + } + + int32_t val = 0; + int ret = sof_static_kcontrol_get_by_uac2(entity_id, channel, &val, false); + if (ret == 0) { + *mute = (val != 0); + } else { + *mute = false; + } + return 0; +} + +int sof_uac2_set_feature_volume(const struct device *dev, uint8_t entity_id, + uint8_t channel, int16_t volume, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(channel); + ARG_UNUSED(user_data); + + LOG_INF("Host set volume: entity=%u, ch=%u, vol=%d (0x%04x, %d dB)", + entity_id, channel, volume, (uint16_t)volume, volume / 256); + + if (entity_id == PLAYBACK_FU_ID) { + g_status.playback_volume = volume; + } else if (entity_id == CAPTURE_FU_ID) { + g_status.capture_volume = volume; + } + + return sof_static_kcontrol_set_by_uac2(entity_id, channel, volume, true); +} + +int sof_uac2_get_feature_volume(const struct device *dev, uint8_t entity_id, + uint8_t channel, int16_t *volume, void *user_data) +{ + ARG_UNUSED(dev); + ARG_UNUSED(channel); + ARG_UNUSED(user_data); + + if (!volume) { + return -EINVAL; + } + if (entity_id == PLAYBACK_FU_ID) { + *volume = g_status.playback_volume; + } else if (entity_id == CAPTURE_FU_ID) { + *volume = g_status.capture_volume; + } else { + *volume = 0; + } + return 0; +} + +static const struct uac2_ops g_uac2_ops = { + .sof_cb = sof_uac2_sof_cb, + .terminal_update_cb = sof_uac2_terminal_update_cb, + .get_recv_buf = sof_uac2_get_recv_buf, + .data_recv_cb = sof_uac2_data_recv_cb, + .buf_release_cb = sof_uac2_buf_release, + .feedback_cb = sof_uac2_feedback_cb, + .get_sample_rate = sof_uac2_get_sample_rate, + .set_sample_rate = sof_uac2_set_sample_rate, + .set_feature_mute = sof_uac2_set_feature_mute, + .get_feature_mute = sof_uac2_get_feature_mute, + .set_feature_volume = sof_uac2_set_feature_volume, + .get_feature_volume = sof_uac2_get_feature_volume, +}; + +const struct uac2_ops *sof_get_uac2_ops(void) +{ + return &g_uac2_ops; +} + +#if defined(CONFIG_PLATFORM_TEENSY41) +extern const struct sof_static_topology g_teensy41_static_topology; +#else +extern const struct sof_static_topology g_esp32p4_static_topology; +#endif + +int sof_static_pipelines_init(struct sof *sof) +{ + ARG_UNUSED(sof); +#if defined(CONFIG_PLATFORM_TEENSY41) + int ret = sof_static_topology_init(&g_teensy41_static_topology); + if (ret < 0) + return ret; + +#if defined(CONFIG_TEENSY41_I2S_SLAVE) + g_status.clock_mode = SOF_CLOCK_SLAVE; +#else + g_status.clock_mode = SOF_CLOCK_MASTER; +#endif + return 0; +#else + int ret = sof_static_topology_init(&g_esp32p4_static_topology); + if (ret < 0) + return ret; + + uint8_t mac[6] = {0}; + extern int esp_efuse_mac_get_default(uint8_t *mac); + esp_efuse_mac_get_default(mac); + + if (mac[5] == 0x17) { + g_status.clock_mode = SOF_CLOCK_MASTER; + } else { + g_status.clock_mode = SOF_CLOCK_SLAVE; + } + + return sof_static_pipeline_set_clock_mode(SOF_AUDIO_IF_I2S, g_status.clock_mode); +#endif +} + +int sof_static_pipeline_set_clock_mode(enum sof_audio_interface iface, enum sof_clock_mode mode) +{ + g_status.active_interface = iface; + g_status.clock_mode = mode; + LOG_INF("Set interface %d clock mode to %s", iface, + mode == SOF_CLOCK_MASTER ? "MASTER" : (mode == SOF_CLOCK_DMIC ? "DMIC (SLAVE TX)" : "SLAVE")); + +#if defined(CONFIG_PLATFORM_ESP32P4) + uint32_t sof_format = 0; + if (iface == SOF_AUDIO_IF_I2S) { + sof_format = (mode == SOF_CLOCK_MASTER) ? + (SOF_DAI_FMT_I2S | SOF_DAI_FMT_CBC_CFC) : + (SOF_DAI_FMT_I2S | SOF_DAI_FMT_CBP_CFP); + + const struct device *dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(dai_i2s0)); + if (dev && device_is_ready(dev)) { + uint32_t rate = g_status.sample_rate ? g_status.sample_rate : 48000; + uint32_t channels = 2; + uint32_t word_size = 16; + uint32_t bytes_per_sample = word_size / 8; + uint32_t frames_per_period = rate / 1000; + if (frames_per_period == 0) { + frames_per_period = 48; + } + struct dai_config cfg = { + .type = DAI_ESP32_I2S, + .dai_index = 0, + .channels = channels, + .rate = rate, + .format = (mode == SOF_CLOCK_MASTER) ? + (DAI_PROTO_I2S | DAI_CBC_CFC) : + (DAI_PROTO_I2S | DAI_CBP_CFP), + .word_size = word_size, + .block_size = frames_per_period * channels * bytes_per_sample, + }; + int ret = dai_config_set(dev, &cfg, NULL, 0); + if (ret < 0) { + LOG_ERR("Failed to set DAI I2S config: %d", ret); + return ret; + } + LOG_INF("DAI I2S hardware successfully switched to %s mode", + mode == SOF_CLOCK_MASTER ? "MASTER" : "SLAVE"); + } else { + LOG_WRN("DAI I2S device not ready or not found"); + } + } else if (iface == SOF_AUDIO_IF_PDM) { + sof_format = (mode == SOF_CLOCK_MASTER) ? + (SOF_DAI_FMT_PDM | SOF_DAI_FMT_CBC_CFC) : + (SOF_DAI_FMT_PDM | SOF_DAI_FMT_CBP_CFP); + + const struct device *dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(dai_pdm0)); + if (dev && device_is_ready(dev)) { + uint32_t rate = g_status.sample_rate ? g_status.sample_rate : 48000; + uint32_t channels = 2; + uint32_t word_size = 16; + uint32_t bytes_per_sample = word_size / 8; + uint32_t frames_per_period = rate / 1000; + if (frames_per_period == 0) { + frames_per_period = 48; + } + struct dai_config cfg = { + .type = DAI_ESP32_PDM, + .dai_index = 1, + .channels = channels, + .rate = rate, + .format = (mode == SOF_CLOCK_MASTER) ? + (DAI_PROTO_PDM | DAI_CBC_CFC) : + (DAI_PROTO_PDM | DAI_CBP_CFP), + .options = (mode == SOF_CLOCK_DMIC) ? 1 : 0, + .word_size = word_size, + .block_size = frames_per_period * channels * bytes_per_sample, + }; + int ret = dai_config_set(dev, &cfg, NULL, 0); + if (ret < 0) { + LOG_ERR("Failed to set DAI PDM config: %d", ret); + return ret; + } + LOG_INF("DAI PDM hardware successfully switched to %s mode", + mode == SOF_CLOCK_MASTER ? "MASTER" : (mode == SOF_CLOCK_DMIC ? "DMIC INJECTOR" : "SLAVE")); + } else { + LOG_WRN("DAI PDM device not ready or not found"); + } + } + + /* Update static pipeline DAI components (comp 5 = PB DAI, comp 6 = CAP DAI) */ + uint32_t target_dai_type = (iface == SOF_AUDIO_IF_PDM) ? SOF_DAI_ESP32_PDM : SOF_DAI_ESP32_I2S; + uint32_t target_dai_index = (iface == SOF_AUDIO_IF_PDM) ? 1 : 0; + + struct comp_dev *dev_pb = sof_static_comp_get(5); + if (dev_pb) { + struct dai_data *dd = comp_get_drvdata(dev_pb); + if (dd) { + if (dd->dai) { + dai_put(dd->dai); + } + dd->dai = dai_get(target_dai_type, target_dai_index, DAI_CREAT); + dd->ipc_config.type = target_dai_type; + dd->ipc_config.dai_index = target_dai_index; + dd->ipc_config.format = sof_format; + dev_pb->state = COMP_STATE_READY; + } + } + + struct comp_dev *dev_cap = sof_static_comp_get(6); + if (dev_cap) { + struct dai_data *dd = comp_get_drvdata(dev_cap); + if (dd) { + if (dd->dai) { + dai_put(dd->dai); + } + dd->dai = dai_get(target_dai_type, target_dai_index, DAI_CREAT); + dd->ipc_config.type = target_dai_type; + dd->ipc_config.dai_index = target_dai_index; + dd->ipc_config.format = sof_format; + dev_cap->state = COMP_STATE_READY; + } + } + + /* Reset and re-prepare pipelines 1 and 2 with updated DAI configuration */ + for (uint32_t pid = 1; pid <= 2; pid++) { + struct pipeline *pipe = sof_static_pipeline_get(pid); + if (!pipe) + continue; + + struct comp_dev *host_or_sched = pipe->sched_comp ? pipe->sched_comp : pipe->source_comp; + if (!host_or_sched) + continue; + + pipeline_reset(pipe, host_or_sched); + + struct sof_ipc_pcm_params prms; + memset(&prms, 0, sizeof(prms)); + uint32_t rate = g_status.sample_rate ? g_status.sample_rate : 48000; + uint32_t channels = 2; + prms.params.rate = rate; + prms.params.channels = channels; + prms.params.frame_fmt = host_or_sched->ipc_config.frame_fmt; + uint32_t sbytes = (prms.params.frame_fmt == SOF_IPC_FRAME_FLOAT || + prms.params.frame_fmt == SOF_IPC_FRAME_S32_LE) ? 4 : 2; + prms.params.sample_container_bytes = sbytes; + prms.params.sample_valid_bytes = sbytes; + prms.params.buffer_fmt = SOF_IPC_BUFFER_INTERLEAVED; + uint32_t frames_per_ms = rate / 1000; + if (frames_per_ms == 0) { + frames_per_ms = 48; + } + prms.params.host_period_bytes = frames_per_ms * channels * sbytes; + prms.comp_id = dev_comp_id(host_or_sched); + prms.params.direction = (pid == 1) ? SOF_IPC_STREAM_PLAYBACK : SOF_IPC_STREAM_CAPTURE; + prms.params.chmap[0] = SOF_CHMAP_FL; + prms.params.chmap[1] = SOF_CHMAP_FR; + + pipeline_params(pipe, host_or_sched, &prms); + pipeline_prepare(pipe, host_or_sched); + } +#endif + + return 0; +} + +int sof_static_pipeline_set_eq_bypass(bool is_capture, bool bypass) +{ + if (is_capture) { + g_status.eq_capture_bypassed = bypass; + return sof_static_kcontrol_set(5, bypass ? 0 : 1); + } else { + g_status.eq_playback_bypassed = bypass; + return sof_static_kcontrol_set(2, bypass ? 0 : 1); + } +} + +int sof_static_pipeline_set_drc_bypass(bool bypass) +{ + g_status.drc_playback_bypassed = bypass; + return sof_static_kcontrol_set(3, bypass ? 0 : 1); +} + +int sof_static_pipeline_set_tdfb_bypass(bool bypass) +{ + g_status.tdfb_capture_bypassed = bypass; + return sof_static_kcontrol_set(4, bypass ? 0 : 1); +} + +int sof_static_pipeline_set_dmic_injector(bool enable) +{ + if (enable) { + return sof_static_pipeline_set_clock_mode(SOF_AUDIO_IF_PDM, SOF_CLOCK_DMIC); + } else { + if (g_status.clock_mode == SOF_CLOCK_DMIC) { + return sof_static_pipeline_set_clock_mode(SOF_AUDIO_IF_PDM, SOF_CLOCK_MASTER); + } + return 0; + } +} + +int sof_static_pipeline_set_volume(uint32_t pipeline_id, int16_t volume) +{ + if (pipeline_id == 1) { + return sof_uac2_set_feature_volume(NULL, PLAYBACK_FU_ID, 0, volume, NULL); + } else if (pipeline_id == 2) { + return sof_uac2_set_feature_volume(NULL, CAPTURE_FU_ID, 0, volume, NULL); + } + return -EINVAL; +} + +int sof_static_pipeline_set_mute(uint32_t pipeline_id, bool mute) +{ + if (pipeline_id == 1) { + return sof_uac2_set_feature_mute(NULL, PLAYBACK_FU_ID, 0, mute, NULL); + } else if (pipeline_id == 2) { + return sof_uac2_set_feature_mute(NULL, CAPTURE_FU_ID, 0, mute, NULL); + } + return -EINVAL; +} + +int sof_static_pipeline_get_volume(uint32_t pipeline_id, int16_t *volume) +{ + if (pipeline_id == 1) { + return sof_uac2_get_feature_volume(NULL, PLAYBACK_FU_ID, 0, volume, NULL); + } else if (pipeline_id == 2) { + return sof_uac2_get_feature_volume(NULL, CAPTURE_FU_ID, 0, volume, NULL); + } + return -EINVAL; +} + +int sof_static_pipeline_get_mute(uint32_t pipeline_id, bool *mute) +{ + if (pipeline_id == 1) { + return sof_uac2_get_feature_mute(NULL, PLAYBACK_FU_ID, 0, mute, NULL); + } else if (pipeline_id == 2) { + return sof_uac2_get_feature_mute(NULL, CAPTURE_FU_ID, 0, mute, NULL); + } + return -EINVAL; +} + +int sof_static_pipeline_set_playback_active(bool start) +{ + g_status.playback_active = start; + return sof_static_pipeline_trigger_by_uac2_term(PLAYBACK_TERM_ID, start); +} + +int sof_static_pipeline_set_capture_active(bool start) +{ + g_status.capture_active = start; + return sof_static_pipeline_trigger_by_uac2_term(CAPTURE_TERM_ID, start); +} + +int sof_static_pipeline_set_bt_stream(bool enable) +{ + g_status.bt_stream_enabled = enable; +#if CONFIG_COMP_BT_AUDIO + if (enable) { + return bt_service_start_broadcast(); + } else { + return bt_service_stop_broadcast(); + } +#else + ARG_UNUSED(enable); + return -ENOTSUP; +#endif +} + +int sof_static_pipeline_set_route(enum sof_audio_route route) +{ + g_status.audio_route = route; + LOG_INF("SOF pipeline audio route switched to %s", + route == SOF_AUDIO_ROUTE_USB_DAI ? "USB <-> DAI" : + (route == SOF_AUDIO_ROUTE_BT_DAI ? "BT <-> DAI" : "USB <-> BT")); + return 0; +} + +void sof_static_pipeline_get_status(struct sof_static_pipeline_status *status) +{ + if (!status) + return; + + *status = g_status; + + int32_t val = 0; + if (sof_static_kcontrol_get(2, &val) == 0) + status->eq_playback_bypassed = (val == 0); + if (sof_static_kcontrol_get(3, &val) == 0) + status->drc_playback_bypassed = (val == 0); + if (sof_static_kcontrol_get(4, &val) == 0) + status->tdfb_capture_bypassed = (val == 0); + if (sof_static_kcontrol_get(5, &val) == 0) + status->eq_capture_bypassed = (val == 0); + if (sof_static_kcontrol_get(8, &val) == 0) + status->bt_stream_enabled = (val != 0); + if (sof_static_kcontrol_get(10, &val) == 0) + status->audio_route = (enum sof_audio_route)val; +} diff --git a/src/audio/usb_audio.c b/src/audio/usb_audio.c new file mode 100644 index 000000000000..f773a65ca48e --- /dev/null +++ b/src/audio/usb_audio.c @@ -0,0 +1,589 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(comp_usb_audio, CONFIG_SOF_LOG_LEVEL); + +SOF_DEFINE_REG_UUID(usb_audio); +DECLARE_TR_CTX(usb_audio_tr, SOF_UUID(usb_audio_uuid), LOG_LEVEL_INFO); + +static struct usb_audio_data *g_playback_data; +static struct usb_audio_data *g_capture_data; + +void usb_audio_set_playback_rate(uint32_t rate) +{ + if (g_playback_data) { + g_playback_data->sample_rate = rate; + } +} + +void usb_audio_set_capture_rate(uint32_t rate) +{ + if (g_capture_data) { + g_capture_data->sample_rate = rate; + } +} + +static bool s_tone_enabled; +void usb_audio_set_tone(bool enable) +{ + s_tone_enabled = enable; +} + +bool usb_audio_get_tone(void) +{ + return s_tone_enabled; +} + +void usb_audio_feed_playback_data(const void *src, size_t bytes) +{ + if (!g_playback_data || !g_playback_data->active) { + return; + } + + struct usb_audio_ring_buffer *ring = &g_playback_data->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + uint32_t avail = USB_AUDIO_RING_BUFFER_SIZE - ring->count; + if (bytes > avail) { + bytes = avail; + } + + const uint8_t *s = (const uint8_t *)src; + for (size_t i = 0; i < bytes; i++) { + ring->buf[ring->head] = s[i]; + ring->head = (ring->head + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count += bytes; + + k_spin_unlock(&ring->lock, key); +} + +size_t usb_audio_fetch_capture_data(void *dst, size_t bytes) +{ + if (!g_capture_data || !g_capture_data->active) { + memset(dst, 0, bytes); + return bytes; + } + + struct usb_audio_ring_buffer *ring = &g_capture_data->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + /* Prebuffer at stream start to provide a stable jitter margin */ + if (!g_capture_data->started) { + if (ring->count < USB_AUDIO_PREBUFFER_BYTES) { + k_spin_unlock(&ring->lock, key); + memset(dst, 0, bytes); + return bytes; + } + g_capture_data->started = true; + } + + uint32_t to_read = (bytes > ring->count) ? ring->count : bytes; + uint8_t *d = (uint8_t *)dst; + + for (size_t i = 0; i < to_read; i++) { + d[i] = ring->buf[ring->tail]; + ring->tail = (ring->tail + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count -= to_read; + + if (to_read < bytes) { + static uint32_t s_cap_underrun_cnt; + s_cap_underrun_cnt++; + if (s_cap_underrun_cnt <= 5 || s_cap_underrun_cnt % 100 == 0) { + LOG_DBG("[CAP UNDERRUN %u] to_read=%u < bytes=%u, ring_count=%u", + s_cap_underrun_cnt, to_read, bytes, ring->count); + } + memset(d + to_read, 0, bytes - to_read); + } + + k_spin_unlock(&ring->lock, key); + return bytes; +} + +bool usb_audio_peek_capture_data(void *dst, size_t bytes) +{ + if (!g_capture_data || !g_capture_data->active) { + memset(dst, 0, bytes); + return false; + } + + struct usb_audio_ring_buffer *ring = &g_capture_data->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + /* Prebuffer at stream start to provide a stable jitter margin */ + if (!g_capture_data->started) { + if (ring->count < USB_AUDIO_PREBUFFER_BYTES) { + k_spin_unlock(&ring->lock, key); + memset(dst, 0, bytes); + return false; + } + g_capture_data->started = true; + } + + uint32_t to_read = (bytes > ring->count) ? ring->count : bytes; + uint8_t *d = (uint8_t *)dst; + uint32_t tail = ring->tail; + + for (size_t i = 0; i < to_read; i++) { + d[i] = ring->buf[tail]; + tail = (tail + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + + if (to_read < bytes) { + static uint32_t s_cap_underrun_cnt; + s_cap_underrun_cnt++; + if (s_cap_underrun_cnt <= 5 || s_cap_underrun_cnt % 100 == 0) { + LOG_DBG("[CAP UNDERRUN %u] to_read=%u < bytes=%u, ring_count=%u", + s_cap_underrun_cnt, to_read, bytes, ring->count); + } + memset(d + to_read, 0, bytes - to_read); + } + + k_spin_unlock(&ring->lock, key); + return (to_read > 0); +} + +void usb_audio_consume_capture_data(size_t bytes) +{ + if (!g_capture_data || !g_capture_data->active) { + return; + } + + struct usb_audio_ring_buffer *ring = &g_capture_data->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + uint32_t to_consume = (bytes > ring->count) ? ring->count : bytes; + ring->tail = (ring->tail + to_consume) % USB_AUDIO_RING_BUFFER_SIZE; + ring->count -= to_consume; + + k_spin_unlock(&ring->lock, key); +} + +static struct comp_dev *usb_audio_new(const struct comp_driver *drv, + const struct comp_ipc_config *config, + const void *spec) +{ + struct comp_dev *dev; + struct usb_audio_data *uad; + + comp_cl_info(drv, "usb_audio_new()"); + + dev = comp_alloc(drv, sizeof(*dev)); + if (!dev) { + return NULL; + } + + dev->ipc_config = *config; + + uad = rzalloc(SOF_MEM_FLAG_KERNEL, sizeof(*uad)); + if (!uad) { + rfree(dev); + return NULL; + } + + comp_set_drvdata(dev, uad); + + uad->sample_rate = 48000; + uad->channels = 2; + uad->frame_bytes = 4; + uad->period_bytes = (uad->sample_rate / 1000) * uad->frame_bytes; + + if (config->pipeline_id == 1 || config->id == 1) { + g_playback_data = uad; + } else { + g_capture_data = uad; + } + + dev->state = COMP_STATE_READY; + return dev; +} + +static void usb_audio_free(struct comp_dev *dev) +{ + struct usb_audio_data *uad = comp_get_drvdata(dev); + + if (uad == g_playback_data) { + g_playback_data = NULL; + } + if (uad == g_capture_data) { + g_capture_data = NULL; + } + + rfree(uad); + rfree(dev); +} + +static int usb_audio_params(struct comp_dev *dev, + struct sof_ipc_stream_params *params) +{ + struct usb_audio_data *uad = comp_get_drvdata(dev); + + uad->sample_rate = params->rate; + uad->channels = params->channels; + uad->frame_bytes = params->sample_container_bytes * params->channels; + uint32_t frames_per_period = (params->rate ? params->rate : 48000) / 1000; + if (frames_per_period == 0) { + frames_per_period = 48; + } + uad->period_bytes = params->host_period_bytes ? params->host_period_bytes : (frames_per_period * uad->frame_bytes); + + if (params->direction == SOF_IPC_STREAM_PLAYBACK) { + g_playback_data = uad; + } else if (params->direction == SOF_IPC_STREAM_CAPTURE) { + g_capture_data = uad; + } + + return 0; +} + +static int usb_audio_trigger(struct comp_dev *dev, int cmd) +{ + struct usb_audio_data *uad = comp_get_drvdata(dev); + + switch (cmd) { + case COMP_TRIGGER_PRE_START: + case COMP_TRIGGER_PRE_RELEASE: + dev->state = COMP_STATE_PRE_ACTIVE; + break; + case COMP_TRIGGER_START: + case COMP_TRIGGER_RELEASE: { + k_spinlock_key_t key = k_spin_lock(&uad->ring.lock); + uad->ring.head = 0; + uad->ring.tail = 0; + uad->ring.count = 0; + k_spin_unlock(&uad->ring.lock, key); + uad->active = true; + uad->started = false; + dev->state = COMP_STATE_ACTIVE; + break; + } + case COMP_TRIGGER_STOP: + case COMP_TRIGGER_PAUSE: { + uad->active = false; + uad->started = false; + k_spinlock_key_t key = k_spin_lock(&uad->ring.lock); + uad->ring.head = 0; + uad->ring.tail = 0; + uad->ring.count = 0; + k_spin_unlock(&uad->ring.lock, key); + dev->state = COMP_STATE_PREPARE; + break; + } + default: + break; + } + + return 0; +} + +static int usb_audio_copy(struct comp_dev *dev) +{ + struct usb_audio_data *uad = comp_get_drvdata(dev); + struct comp_buffer *sink = comp_dev_get_first_data_consumer(dev); + struct comp_buffer *source = comp_dev_get_first_data_producer(dev); + + if (!uad || !uad->active) { + if (source) { + /* Capture inactive: consume data so source buffer doesn't overflow */ + uint32_t avail_bytes = audio_stream_get_avail_bytes(&source->stream); + if (avail_bytes > 0) { + comp_update_buffer_consume(source, avail_bytes); + } + } + return 0; + } + + if (sink) { + /* USB Playback Source -> SOF Sink Buffer */ + enum sof_ipc_frame fmt = audio_stream_get_frm_fmt(&sink->stream); + if (fmt == SOF_IPC_FRAME_FLOAT) { + uint32_t free_frames = audio_stream_get_free_frames(&sink->stream); + uint32_t copy_frames = MIN(free_frames, dev->frames ? dev->frames : 48); + + if (copy_frames > 0) { + int16_t s16_buf[96]; + float f_buf[96]; + uint32_t frames_to_copy = MIN(copy_frames, 48); + uint32_t s16_bytes = frames_to_copy * 2 * sizeof(int16_t); + + struct usb_audio_ring_buffer *ring = &uad->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + /* Prebuffer at playback start to provide a stable jitter margin */ + if (!uad->started) { + if (ring->count < USB_AUDIO_PREBUFFER_BYTES) { + k_spin_unlock(&ring->lock, key); + memset(f_buf, 0, frames_to_copy * 2 * sizeof(float)); + audio_stream_copy_from_linear(f_buf, 0, &sink->stream, 0, frames_to_copy * 2); + comp_update_buffer_produce(sink, frames_to_copy * 2 * sizeof(float)); + return 0; + } + uad->started = true; + } + + uint32_t available = ring->count; + uint32_t to_copy = MIN(s16_bytes, available); + + uint8_t *s16_raw = (uint8_t *)s16_buf; + for (size_t i = 0; i < to_copy; i++) { + s16_raw[i] = ring->buf[ring->tail]; + ring->tail = (ring->tail + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count -= to_copy; + + if (to_copy < s16_bytes) { + static uint32_t s_pb_underrun_cnt; + s_pb_underrun_cnt++; + if (s_pb_underrun_cnt <= 5 || s_pb_underrun_cnt % 100 == 0) { + LOG_DBG("[PB UNDERRUN %u] to_copy=%u < s16_bytes=%u, ring_count=%u", + s_pb_underrun_cnt, to_copy, s16_bytes, ring->count); + } + memset(s16_raw + to_copy, 0, s16_bytes - to_copy); + } + + k_spin_unlock(&ring->lock, key); + + const float scale = 1.0f / 32768.0f; + for (size_t i = 0; i < frames_to_copy * 2; i++) { + f_buf[i] = (float)s16_buf[i] * scale; + } + + audio_stream_copy_from_linear(f_buf, 0, &sink->stream, 0, frames_to_copy * 2); + comp_update_buffer_produce(sink, frames_to_copy * 2 * sizeof(float)); + } + } else { + uint32_t free_bytes = audio_stream_get_free_bytes(&sink->stream); + if (free_bytes >= uad->period_bytes) { + uint8_t temp_buf[1024]; + uint32_t chunk = MIN(uad->period_bytes, sizeof(temp_buf)); + struct usb_audio_ring_buffer *ring = &uad->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + if (s_tone_enabled) { + /* 1000 Hz pure sine wave, 48 samples per cycle, stereo, amp 2000 */ + static const int16_t sine48[48] = { + 0, 261, 518, 765, 1000, 1218, 1414, 1586, + 1732, 1848, 1932, 1983, 2000, 1983, 1932, 1848, + 1732, 1586, 1414, 1218, 1000, 765, 518, 261, + 0, -261, -518, -765, -1000, -1218, -1414, -1586, + -1732, -1848, -1932, -1983, -2000, -1983, -1932, -1848, + -1732, -1586, -1414, -1218, -1000, -765, -518, -261 + }; + int16_t *out16 = (int16_t *)temp_buf; + uint32_t frames = chunk / (uad->frame_bytes ? uad->frame_bytes : 4); + for (size_t i = 0; i < frames; i++) { + int16_t s = sine48[i % 48]; + out16[2 * i] = s; + out16[2 * i + 1] = s; + } + k_spin_unlock(&ring->lock, key); + audio_stream_copy_from_linear(temp_buf, 0, &sink->stream, 0, + chunk / audio_stream_sample_bytes(&sink->stream)); + comp_update_buffer_produce(sink, chunk); + return 0; + } + + /* Prebuffer at playback start to provide a stable jitter margin */ + if (!uad->started) { + if (ring->count < USB_AUDIO_PREBUFFER_BYTES) { + k_spin_unlock(&ring->lock, key); + memset(temp_buf, 0, chunk); + audio_stream_copy_from_linear(temp_buf, 0, &sink->stream, 0, + chunk / audio_stream_sample_bytes(&sink->stream)); + comp_update_buffer_produce(sink, chunk); + return 0; + } + uad->started = true; + } + + uint32_t available = ring->count; + if (available < chunk) { + static uint32_t s_pb_underrun_cnt; + s_pb_underrun_cnt++; + if (s_pb_underrun_cnt <= 5 || s_pb_underrun_cnt % 100 == 0) { + LOG_DBG("[PB UNDERRUN %u] available=%u < period_bytes=%u", + s_pb_underrun_cnt, available, chunk); + } + memset(temp_buf, 0, chunk); + k_spin_unlock(&ring->lock, key); + audio_stream_copy_from_linear(temp_buf, 0, &sink->stream, 0, + chunk / audio_stream_sample_bytes(&sink->stream)); + comp_update_buffer_produce(sink, chunk); + } else { + for (size_t i = 0; i < chunk; i++) { + temp_buf[i] = ring->buf[ring->tail]; + ring->tail = (ring->tail + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count -= chunk; + k_spin_unlock(&ring->lock, key); + + audio_stream_copy_from_linear(temp_buf, 0, &sink->stream, 0, + chunk / audio_stream_sample_bytes(&sink->stream)); + comp_update_buffer_produce(sink, chunk); + } + } + } + } else if (source) { + /* SOF Source Buffer -> USB Capture Sink */ + enum sof_ipc_frame fmt = audio_stream_get_frm_fmt(&source->stream); + if (fmt == SOF_IPC_FRAME_FLOAT) { + uint32_t avail_frames = audio_stream_get_avail_frames(&source->stream); + uint32_t copy_frames = MIN(avail_frames, dev->frames ? dev->frames : 48); + + if (copy_frames > 0) { + float f_buf[96]; + int16_t s16_buf[96]; + uint32_t frames_to_copy = MIN(copy_frames, 48); + + audio_stream_copy_to_linear(&source->stream, 0, f_buf, 0, frames_to_copy * 2); + comp_update_buffer_consume(source, frames_to_copy * 2 * sizeof(float)); + + const float scale = 32768.0f; + for (size_t i = 0; i < frames_to_copy * 2; i++) { + float val = f_buf[i] * scale; + if (val >= 32767.0f) { + s16_buf[i] = 32767; + } else if (val <= -32768.0f) { + s16_buf[i] = -32768; + } else { +#if defined(__riscv) && defined(__riscv_flen) + int32_t r; + __asm__ ("fcvt.w.s %0, %1, rne" : "=r"(r) : "f"(val)); + s16_buf[i] = (int16_t)r; +#else + s16_buf[i] = (int16_t)(val >= 0.0f ? (val + 0.5f) : (val - 0.5f)); +#endif + } + } + + uint32_t s16_bytes = frames_to_copy * 2 * sizeof(int16_t); + struct usb_audio_ring_buffer *ring = &uad->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + if (ring->count + s16_bytes > USB_AUDIO_RING_BUFFER_SIZE) { + uint32_t drop = (ring->count + s16_bytes) - USB_AUDIO_RING_BUFFER_SIZE; + drop = (drop + 3) & ~3; + if (drop > ring->count) { + drop = ring->count; + } + ring->tail = (ring->tail + drop) % USB_AUDIO_RING_BUFFER_SIZE; + ring->count -= drop; + } + + uint32_t free_space = USB_AUDIO_RING_BUFFER_SIZE - ring->count; + uint32_t to_copy = MIN(s16_bytes, free_space); + + uint8_t *s16_raw = (uint8_t *)s16_buf; + for (size_t i = 0; i < to_copy; i++) { + ring->buf[ring->head] = s16_raw[i]; + ring->head = (ring->head + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count += to_copy; + + k_spin_unlock(&ring->lock, key); + } + } else { + uint32_t avail_bytes = audio_stream_get_avail_bytes(&source->stream); + + while (avail_bytes > 0) { + uint8_t temp_buf[512]; + uint32_t chunk = MIN(avail_bytes, sizeof(temp_buf)); + + struct usb_audio_ring_buffer *ring = &uad->ring; + k_spinlock_key_t key = k_spin_lock(&ring->lock); + + if (ring->count + chunk > USB_AUDIO_RING_BUFFER_SIZE) { + static uint32_t s_cap_overflow_cnt; + s_cap_overflow_cnt++; + if (s_cap_overflow_cnt <= 5 || s_cap_overflow_cnt % 100 == 0) { + LOG_DBG("[CAP OVERFLOW %u] ring_count=%u, chunk=%u", + s_cap_overflow_cnt, ring->count, chunk); + } + uint32_t drop = (ring->count + chunk) - USB_AUDIO_RING_BUFFER_SIZE; + drop = (drop + 3) & ~3; + if (drop > ring->count) { + drop = ring->count; + } + ring->tail = (ring->tail + drop) % USB_AUDIO_RING_BUFFER_SIZE; + ring->count -= drop; + } + + uint32_t free_space = USB_AUDIO_RING_BUFFER_SIZE - ring->count; + uint32_t to_copy = MIN(chunk, free_space); + + audio_stream_copy_to_linear(&source->stream, 0, temp_buf, 0, + to_copy / audio_stream_sample_bytes(&source->stream)); + comp_update_buffer_consume(source, to_copy); + avail_bytes -= to_copy; + + for (size_t i = 0; i < to_copy; i++) { + ring->buf[ring->head] = temp_buf[i]; + ring->head = (ring->head + 1) % USB_AUDIO_RING_BUFFER_SIZE; + } + ring->count += to_copy; + + k_spin_unlock(&ring->lock, key); + } + } + } + + return 0; +} + +static int usb_audio_prepare(struct comp_dev *dev) +{ + dev->state = COMP_STATE_PREPARE; + return 0; +} + +static int usb_audio_reset(struct comp_dev *dev) +{ + struct usb_audio_data *uad = comp_get_drvdata(dev); + uad->active = false; + uad->started = false; + uad->ring.head = 0; + uad->ring.tail = 0; + uad->ring.count = 0; + dev->state = COMP_STATE_INIT; + return 0; +} + +static const struct comp_driver comp_usb_audio = { + .type = SOF_COMP_HOST, + .uid = SOF_RT_UUID(usb_audio_uuid), +#if !CONFIG_ZEPHYR_LOG + .tctx = &usb_audio_tr, +#endif + .ops = { + .create = usb_audio_new, + .free = usb_audio_free, + .params = usb_audio_params, + .prepare = usb_audio_prepare, + .trigger = usb_audio_trigger, + .copy = usb_audio_copy, + .reset = usb_audio_reset, + }, +}; + +static struct comp_driver_info comp_usb_audio_info = { + .drv = &comp_usb_audio, +}; + +UT_STATIC void sys_comp_usb_audio_init(void) +{ + comp_register(&comp_usb_audio_info); +} + +DECLARE_MODULE(sys_comp_usb_audio_init); +SOF_MODULE_INIT(usb_audio, sys_comp_usb_audio_init); + diff --git a/src/include/ipc4/handler.h b/src/include/ipc4/handler.h index 3335c108fa29..ce25acdbfc1c 100644 --- a/src/include/ipc4/handler.h +++ b/src/include/ipc4/handler.h @@ -95,7 +95,7 @@ int ipc4_comp_connect(struct ipc *ipc, const struct ipc4_module_bind_unbind *bu) */ int ipc4_comp_disconnect(struct ipc *ipc, const struct ipc4_module_bind_unbind *bu); -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) /** * \brief Increment the IPC compound message pre-start barrier counter. * \ingroup ipc4_envelope_api diff --git a/src/include/ipc4/notification.h b/src/include/ipc4/notification.h index c2a55dd32284..5ecf29952596 100644 --- a/src/include/ipc4/notification.h +++ b/src/include/ipc4/notification.h @@ -299,7 +299,7 @@ void send_mixer_underrun_notif_msg(uint32_t resource_id, uint32_t eos_flag, uint */ void ipc4_update_notification_mask(uint32_t ntfy_mask, uint32_t enabled_mask); -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) /** * \brief Send generic resource event notification. diff --git a/src/include/sof/audio/kpb.h b/src/include/sof/audio/kpb.h index b17e7eb9bd55..b2956a2e5169 100644 --- a/src/include/sof/audio/kpb.h +++ b/src/include/sof/audio/kpb.h @@ -257,7 +257,7 @@ struct kpb_fmt_dev_list { #if CONFIG_KPB_CLI_Q void kpb_notifier_schedule(struct kpb_client *cli); -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) #include __syscall void kpb_notifier_init(struct comp_dev *dev, struct kpb_client *cli); #include diff --git a/src/include/sof/audio/module_adapter/module/generic.h b/src/include/sof/audio/module_adapter/module/generic.h index 1e0d82d23380..43e2adf12509 100644 --- a/src/include/sof/audio/module_adapter/module/generic.h +++ b/src/include/sof/audio/module_adapter/module/generic.h @@ -203,7 +203,7 @@ int module_init(struct processing_module *mod); * @param alignment Required alignment in bytes. * @return Pointer to allocated buffer, or NULL on failure. */ -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall void *mod_balloc_align(struct processing_module *mod, size_t size, size_t alignment); #else void *z_impl_mod_balloc_align(struct processing_module *mod, size_t size, size_t alignment); @@ -236,7 +236,7 @@ void mod_heap_info(struct processing_module *mod, size_t *size, uintptr_t *start * @param alignment Required alignment in bytes. * @return Pointer to allocated memory, or NULL on failure. */ -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall void *mod_alloc_ext(struct processing_module *mod, uint32_t flags, size_t size, size_t alignment); /** @@ -379,7 +379,7 @@ static inline void mod_ipc_msg_free(struct processing_module *mod, } #if CONFIG_COMP_BLOB -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) /** * \brief Module-managed wrapper for comp_data_blob_handler_new_ext(). */ @@ -403,7 +403,7 @@ int z_impl_mod_data_blob_set(struct processing_module *mod, void mod_data_blob_handler_free(struct processing_module *mod, struct comp_data_blob_handler *dbh); #endif #if CONFIG_FAST_GET -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall const void *mod_fast_get(struct processing_module *mod, const void * const dram_ptr, size_t size); #else @@ -638,7 +638,7 @@ static inline uint32_t module_get_lpt(struct processing_module *mod) return mod->dev->period; } -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) #include #endif diff --git a/src/include/sof/audio/pipeline/sof_static_pipeline.h b/src/include/sof/audio/pipeline/sof_static_pipeline.h new file mode 100644 index 000000000000..732d95a891db --- /dev/null +++ b/src/include/sof/audio/pipeline/sof_static_pipeline.h @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __SOF_AUDIO_PIPELINE_SOF_STATIC_PIPELINE_H__ +#define __SOF_AUDIO_PIPELINE_SOF_STATIC_PIPELINE_H__ + +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum sof_audio_interface { + SOF_AUDIO_IF_I2S = 0, + SOF_AUDIO_IF_PDM = 1, +}; + +enum sof_clock_mode { + SOF_CLOCK_SLAVE = 0, + SOF_CLOCK_MASTER = 1, + SOF_CLOCK_DMIC = 2, +}; + +enum sof_audio_route { + SOF_AUDIO_ROUTE_USB_DAI = 0, + SOF_AUDIO_ROUTE_BT_DAI = 1, + SOF_AUDIO_ROUTE_USB_BT = 2, +}; + +struct sof_static_pipeline_status { + bool playback_active; + bool capture_active; + bool microframes; + uint32_t sample_rate; + enum sof_audio_interface active_interface; + enum sof_clock_mode clock_mode; + enum sof_audio_route audio_route; + bool bt_stream_enabled; + int16_t playback_volume; + bool playback_mute; + int16_t capture_volume; + bool capture_mute; + bool eq_playback_bypassed; + bool drc_playback_bypassed; + bool tdfb_capture_bypassed; + bool eq_capture_bypassed; +}; + +int sof_static_pipelines_init(struct sof *sof); +int sof_static_pipeline_set_clock_mode(enum sof_audio_interface iface, enum sof_clock_mode mode); +int sof_static_pipeline_set_dmic_injector(bool enable); +int sof_static_pipeline_set_bt_stream(bool enable); +int sof_static_pipeline_set_route(enum sof_audio_route route); +int sof_static_pipeline_set_eq_bypass(bool is_capture, bool bypass); +int sof_static_pipeline_set_drc_bypass(bool bypass); +int sof_static_pipeline_set_tdfb_bypass(bool bypass); +int sof_static_pipeline_set_volume(uint32_t pipeline_id, int16_t volume); +int sof_static_pipeline_set_mute(uint32_t pipeline_id, bool mute); +int sof_static_pipeline_get_volume(uint32_t pipeline_id, int16_t *volume); +int sof_static_pipeline_get_mute(uint32_t pipeline_id, bool *mute); +int sof_static_pipeline_set_playback_active(bool start); +int sof_static_pipeline_set_capture_active(bool start); +void sof_static_pipeline_get_status(struct sof_static_pipeline_status *status); + +struct uac2_ops; +const struct uac2_ops *sof_get_uac2_ops(void); + +#ifdef __cplusplus +} +#endif + +#endif /* __SOF_AUDIO_PIPELINE_SOF_STATIC_PIPELINE_H__ */ diff --git a/src/include/sof/audio/usb_audio.h b/src/include/sof/audio/usb_audio.h new file mode 100644 index 000000000000..29da76bef5c7 --- /dev/null +++ b/src/include/sof/audio/usb_audio.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Espressif Systems / SOF Project + */ + +#ifndef __SOF_AUDIO_USB_AUDIO_H__ +#define __SOF_AUDIO_USB_AUDIO_H__ + +#include +#include +#include +#include + +/* UUID for USB Audio Component */ +/* b95039f6-3d23-41a4-9276-2fbb27c62d01 */ +#define SOF_USB_AUDIO_UUID \ + DECLARE_SOF_RT_UUID("usb_audio", 0xb95039f6, 0x3d23, 0x41a4, \ + 0x92, 0x76, 0x2f, 0xbb, 0x27, 0xc6, 0x2d, 0x01) + +#define USB_AUDIO_RING_BUFFER_SIZE 16384 +#define USB_AUDIO_PREBUFFER_BYTES 3840 /* 20ms at 48kHz stereo 16-bit */ + +struct usb_audio_ring_buffer { + uint8_t buf[USB_AUDIO_RING_BUFFER_SIZE]; + volatile uint32_t head; + volatile uint32_t tail; + volatile uint32_t count; + struct k_spinlock lock; +}; + +struct usb_audio_data { + struct usb_audio_ring_buffer ring; + uint32_t sample_rate; + uint32_t channels; + uint32_t frame_bytes; + uint32_t period_bytes; + enum sof_ipc_stream_direction direction; + bool active; + bool started; +}; + +void usb_audio_feed_playback_data(const void *src, size_t bytes); +size_t usb_audio_fetch_capture_data(void *dst, size_t bytes); +bool usb_audio_peek_capture_data(void *dst, size_t bytes); +void usb_audio_consume_capture_data(size_t bytes); +void usb_audio_set_playback_rate(uint32_t rate); +void usb_audio_set_capture_rate(uint32_t rate); +void usb_audio_set_tone(bool enable); +bool usb_audio_get_tone(void); + +#endif /* __SOF_AUDIO_USB_AUDIO_H__ */ diff --git a/src/include/sof/ipc/ipc_reply.h b/src/include/sof/ipc/ipc_reply.h index 915b6261c6bd..cdd9b697eac7 100644 --- a/src/include/sof/ipc/ipc_reply.h +++ b/src/include/sof/ipc/ipc_reply.h @@ -14,7 +14,7 @@ struct sof_ipc_reply; * \brief reply to an IPC message. * @param[in] reply pointer to the reply structure. */ -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall void ipc_msg_reply(struct sof_ipc_reply *reply); #include diff --git a/src/include/sof/ipc/msg.h b/src/include/sof/ipc/msg.h index 61390f209b69..52a11551bcdc 100644 --- a/src/include/sof/ipc/msg.h +++ b/src/include/sof/ipc/msg.h @@ -72,7 +72,7 @@ static inline struct ipc_msg *ipc_msg_init(struct k_heap *heap, * * @param msg The IPC message to remove from the queue. */ -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall void ipc_msg_list_remove(struct ipc_msg *msg); #include #else @@ -105,7 +105,7 @@ void ipc_send_queued_msg(void); * @param data The message data. * @param high_priority True if a high priority message. */ -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall void ipc_msg_send(struct ipc_msg *msg, void *data, bool high_priority); #include diff --git a/src/include/sof/lib/dai-zephyr.h b/src/include/sof/lib/dai-zephyr.h index 0c3febaed73f..54f6dfa56024 100644 --- a/src/include/sof/lib/dai-zephyr.h +++ b/src/include/sof/lib/dai-zephyr.h @@ -327,7 +327,13 @@ void dai_release_llp_slot(struct dai_data *dd); /** * \brief Retrieve a pointer to the Zephyr device structure for a DAI of a given type and index. */ +#if defined(CONFIG_USERSPACE) __syscall const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32_t index); +#include +#else +const struct device *z_impl_dai_get_device(enum sof_ipc_dai_type type, uint32_t index); +#define dai_get_device z_impl_dai_get_device +#endif /** * \brief Retrieve the list of all DAI devices. @@ -337,6 +343,4 @@ __syscall const struct device *dai_get_device(enum sof_ipc_dai_type type, uint32 const struct device **dai_get_device_list(size_t *count); /** @}*/ -#include - #endif /* __SOF_LIB_DAI_ZEPHYR_H__ */ diff --git a/src/include/sof/lib_manager.h b/src/include/sof/lib_manager.h index c56a1f4d09e0..cceef54a3d4e 100644 --- a/src/include/sof/lib_manager.h +++ b/src/include/sof/lib_manager.h @@ -231,7 +231,7 @@ int lib_manager_mod_create_priv(const struct comp_driver *drv, struct userspace_context **userspace, const struct module_interface **ops); -#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(__ZEPHYR__) && defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall int lib_manager_free_module(const uint32_t component_id); #include diff --git a/src/include/sof/schedule/dp_schedule.h b/src/include/sof/schedule/dp_schedule.h index 1dac74b600ef..db44cea11069 100644 --- a/src/include/sof/schedule/dp_schedule.h +++ b/src/include/sof/schedule/dp_schedule.h @@ -76,7 +76,7 @@ int scheduler_dp_task_init(struct task **task, size_t stack_size, uint32_t options); -#if defined(__ZEPHYR__) && CONFIG_SOF_FULL_ZEPHYR_APPLICATION +#if defined(__ZEPHYR__) && CONFIG_SOF_FULL_ZEPHYR_APPLICATION && defined(CONFIG_USERSPACE) /** * @brief Free kernel-side internal resources associated with a DP task. * @param[in,out] task Task being freed. @@ -91,6 +91,11 @@ __syscall void scheduler_dp_internal_free(struct task *task); */ __syscall void scheduler_dp_ll_tick(void); #include +#else +void z_impl_scheduler_dp_internal_free(struct task *task); +void z_impl_scheduler_dp_ll_tick(void); +#define scheduler_dp_internal_free z_impl_scheduler_dp_internal_free +#define scheduler_dp_ll_tick z_impl_scheduler_dp_ll_tick #endif /** diff --git a/src/include/sof/schedule/ll_schedule_domain.h b/src/include/sof/schedule/ll_schedule_domain.h index 6858929d97b3..f52cdd3f91a4 100644 --- a/src/include/sof/schedule/ll_schedule_domain.h +++ b/src/include/sof/schedule/ll_schedule_domain.h @@ -564,6 +564,11 @@ struct ll_schedule_domain *zephyr_domain_init(int clk); #define timer_domain_init(timer, clk) zephyr_domain_init(clk) +#if defined(CONFIG_PLATFORM_TEENSY41) +void zephyr_domain_audio_timer_cb(void); +bool dai_zephyr_has_active_audio_domain(void); +#endif + #ifdef CONFIG_SOF_USERSPACE_LL /** * @brief Retrieve the Zephyr domain thread TID for the domain on the current core. @@ -592,7 +597,7 @@ struct k_mem_domain *zephyr_ll_mem_domain(void); struct k_thread *zephyr_ll_domain_thread(void); #endif /* CONFIG_SOF_USERSPACE_LL */ -#ifdef CONFIG_SOF_FULL_ZEPHYR_APPLICATION +#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) /** * @brief Allocate a completion semaphore for a Low-Latency task. * @param[in,out] task Target task. @@ -612,7 +617,7 @@ int z_impl_zephyr_ll_task_sem_alloc(struct task *task); int z_impl_zephyr_ll_task_sem_free(struct task *task); #define zephyr_ll_task_sem_alloc z_impl_zephyr_ll_task_sem_alloc #define zephyr_ll_task_sem_free z_impl_zephyr_ll_task_sem_free -#endif /* CONFIG_SOF_FULL_ZEPHYR_APPLICATION */ +#endif /* CONFIG_SOF_FULL_ZEPHYR_APPLICATION && CONFIG_USERSPACE */ #endif /* __ZEPHYR__ */ /** diff --git a/src/include/user/debug_stream_slot.h b/src/include/user/debug_stream_slot.h index 5eaae22115b5..de0081a49dfb 100644 --- a/src/include/user/debug_stream_slot.h +++ b/src/include/user/debug_stream_slot.h @@ -136,8 +136,13 @@ struct debug_stream_record; * @param[in] rec Pointer to abstract debug stream record to transmit. * @return 0 on success, -ENODEV if debug slot is unconfigured, or -ENOMEM if record exceeds capacity. */ +#if defined(CONFIG_USERSPACE) __syscall int debug_stream_slot_send_record(struct debug_stream_record *rec); #include +#else +int z_impl_debug_stream_slot_send_record(struct debug_stream_record *rec); +#define debug_stream_slot_send_record z_impl_debug_stream_slot_send_record +#endif #endif /* __SOC_DEBUG_WINDOW_SLOT_H__ */ diff --git a/src/platform/CMakeLists.txt b/src/platform/CMakeLists.txt index e43c21e1ac0d..c5a5177d6a32 100644 --- a/src/platform/CMakeLists.txt +++ b/src/platform/CMakeLists.txt @@ -29,4 +29,6 @@ elseif(CONFIG_MT8365) add_subdirectory(mt8365) elseif(PLATFORM STREQUAL "qemu_xtensa") add_subdirectory(qemu_xtensa) +elseif(CONFIG_PLATFORM_TEENSY41) + add_subdirectory(teensy41) endif() diff --git a/src/platform/Kconfig b/src/platform/Kconfig index cc86bb74c197..7c976a076f80 100644 --- a/src/platform/Kconfig +++ b/src/platform/Kconfig @@ -187,6 +187,14 @@ config IMX95 help Select if your target platform is imx95-compatible. +config PLATFORM_TEENSY41 + bool "Build for PJRC Teensy 4.1 (NXP i.MX RT1062 ARM Cortex-M7)" + select ZEPHYR_LOG + select ZEPHYR_NATIVE_DRIVERS + select STATIC_PIPELINE + help + Select if your target platform is PJRC Teensy 4.1. + config RENOIR bool "Build for Renoir" select XT_INTERRUPT_LEVEL_5 @@ -353,6 +361,50 @@ config MT8196 endchoice +if PLATFORM_TEENSY41 + +choice + prompt "Teensy 4.1 Board Role" + default TEENSY41_BOARD_A + help + Select whether this binary is for Board A (Master Tx) or Board B (Slave Rx). + +config TEENSY41_BOARD_A + bool "Teensy 4.1 Board A (Master Clock Provider / Tx)" + help + Configure board as Board A (Master clock provider on SAI1, + generates BCLK and SYNC). Appears on host USB as + 'SOF Teensy 4.1 Audio A'. + +config TEENSY41_BOARD_B + bool "Teensy 4.1 Board B (Slave Clock Consumer / Rx)" + help + Configure board as Board B (Slave clock consumer on SAI1, + receives BCLK and SYNC). Appears on host USB as + 'SOF Teensy 4.1 Audio B'. + +endchoice + +choice + prompt "Teensy 4.1 Audio Hardware Interface" + default TEENSY41_INTERFACE_SPDIF + help + Select whether the static audio pipeline connects to SAI1 (I2S) or S/PDIF. + +config TEENSY41_INTERFACE_SAI_I2S + bool "SAI1 I2S Digital Audio Interface" + help + Route audio through NXP SAI1 (I2S mode). + +config TEENSY41_INTERFACE_SPDIF + bool "S/PDIF Hardware Transceiver" + help + Route audio through NXP on-chip S/PDIF transceiver. + +endchoice + +endif + config ESP32P4_PIE_SIMD bool "Enable ESP32-P4 PIE (Xespv2p2) 128-bit vector SIMD acceleration" default y diff --git a/src/platform/teensy41/CMakeLists.txt b/src/platform/teensy41/CMakeLists.txt new file mode 100644 index 000000000000..5ab8f3438310 --- /dev/null +++ b/src/platform/teensy41/CMakeLists.txt @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: BSD-3-Clause + +sof_add_include_directory(include) + +add_subdirectory(lib) + +sof_add_files( + platform.c + teensy41_pipeline_def.c +) diff --git a/src/platform/teensy41/include/platform/drivers/interrupt.h b/src/platform/teensy41/include/platform/drivers/interrupt.h new file mode 100644 index 000000000000..cf1ee35b2f08 --- /dev/null +++ b/src/platform/teensy41/include/platform/drivers/interrupt.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_DRIVERS_INTERRUPT_H__ +#define __PLATFORM_DRIVERS_INTERRUPT_H__ + +#endif /* __PLATFORM_DRIVERS_INTERRUPT_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/clk.h b/src/platform/teensy41/include/platform/lib/clk.h new file mode 100644 index 000000000000..3cce41d1b4a1 --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/clk.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_CLK_H__ +#define __PLATFORM_LIB_CLK_H__ + +#include + +#define CPU_DEFAULT_IDX 0 +#define NUM_CPU_FREQ 1 +#define NUM_CLOCKS 1 + +struct sof; +void platform_clock_init(struct sof *sof); + +#endif /* __PLATFORM_LIB_CLK_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/dai.h b/src/platform/teensy41/include/platform/lib/dai.h new file mode 100644 index 000000000000..0f878cd3794e --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/dai.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_DAI_H__ +#define __PLATFORM_LIB_DAI_H__ + +#include + +#endif /* __PLATFORM_LIB_DAI_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/dma.h b/src/platform/teensy41/include/platform/lib/dma.h new file mode 100644 index 000000000000..c20d3eb2e8bc --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/dma.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_DMA_H__ +#define __PLATFORM_LIB_DMA_H__ + +#define PLATFORM_NUM_DMACS 1 + +#endif /* __PLATFORM_LIB_DMA_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/mailbox.h b/src/platform/teensy41/include/platform/lib/mailbox.h new file mode 100644 index 000000000000..fab5f633834a --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/mailbox.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_MAILBOX_H__ +#define __PLATFORM_LIB_MAILBOX_H__ + +#include +#include + +extern uint8_t sof_teensy_mailbox[0x1000]; +#define MAILBOX_BASE ((uintptr_t)sof_teensy_mailbox) + +#define MAILBOX_DSPBOX_OFFSET 0x0 +#define MAILBOX_DSPBOX_SIZE 0x400 +#define MAILBOX_DSPBOX_BASE (MAILBOX_BASE + MAILBOX_DSPBOX_OFFSET) + +#define MAILBOX_HOSTBOX_OFFSET 0x400 +#define MAILBOX_HOSTBOX_SIZE 0x400 +#define MAILBOX_HOSTBOX_BASE (MAILBOX_BASE + MAILBOX_HOSTBOX_OFFSET) + +#define MAILBOX_STREAM_OFFSET 0x800 +#define MAILBOX_STREAM_SIZE 0x400 +#define MAILBOX_STREAM_BASE (MAILBOX_BASE + MAILBOX_STREAM_OFFSET) + +#define MAILBOX_DEBUG_OFFSET 0xC00 +#define MAILBOX_DEBUG_SIZE 0x200 +#define MAILBOX_DEBUG_BASE (MAILBOX_BASE + MAILBOX_DEBUG_OFFSET) + +#define MAILBOX_EXCEPTION_OFFSET 0xE00 +#define MAILBOX_EXCEPTION_SIZE 0x100 +#define MAILBOX_EXCEPTION_BASE (MAILBOX_BASE + MAILBOX_EXCEPTION_OFFSET) + +#define MAILBOX_TRACE_OFFSET 0xF00 +#define MAILBOX_TRACE_SIZE 0x100 +#define MAILBOX_TRACE_BASE (MAILBOX_BASE + MAILBOX_TRACE_OFFSET) + +static inline void mailbox_sw_reg_write(size_t offset, uint32_t src) {} +static inline void mailbox_sw_regs_write(size_t offset, const void *src, size_t bytes) {} +static inline uint32_t mailbox_sw_reg_read(size_t offset) { return 0; } +static inline uint64_t mailbox_sw_reg_read64(size_t offset) { return 0; } + +#endif /* __PLATFORM_LIB_MAILBOX_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/memory.h b/src/platform/teensy41/include/platform/lib/memory.h new file mode 100644 index 000000000000..32000f6b569c --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/memory.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_MEMORY_H__ +#define __PLATFORM_LIB_MEMORY_H__ + +#include +#include + +#define SRAM_BANK_SIZE 0x10000 +#define EPROMS_START 0 +#define HEAP_SYSTEM_SIZE 0x4000 +#define HEAP_RUNTIME_SIZE 0x4000 +#define HEAP_BUFFER_SIZE 0x10000 + +#define PLATFORM_DCACHE_ALIGN 32 +#define PLATFORM_DCACHE_ALIGN_SHIFT 5 + +#define uncache_to_cache(address) (address) +#define cache_to_uncache(address) (address) +#define is_uncached(address) 0 +#define host_to_local(addr) (addr) +#define local_to_host(addr) (addr) + +#endif /* __PLATFORM_LIB_MEMORY_H__ */ diff --git a/src/platform/teensy41/include/platform/lib/pm_runtime.h b/src/platform/teensy41/include/platform/lib/pm_runtime.h new file mode 100644 index 000000000000..005fe1955d18 --- /dev/null +++ b/src/platform/teensy41/include/platform/lib/pm_runtime.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_LIB_PM_RUNTIME_H__ +#define __PLATFORM_LIB_PM_RUNTIME_H__ + +#endif /* __PLATFORM_LIB_PM_RUNTIME_H__ */ diff --git a/src/platform/teensy41/include/platform/platform.h b/src/platform/teensy41/include/platform/platform.h new file mode 100644 index 000000000000..8f3a8c4dee29 --- /dev/null +++ b/src/platform/teensy41/include/platform/platform.h @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifdef __SOF_PLATFORM_H__ + +#ifndef __PLATFORM_PLATFORM_H__ +#define __PLATFORM_PLATFORM_H__ + +#include +#include + +#define PLATFORM_DEFAULT_CLOCK 0 +#define HOST_PAGE_SIZE 4096 +#define PLATFORM_PAGE_TABLE_SIZE 256 + +#define PLATFORM_MAX_CHANNELS 8 +#define PLATFORM_MAX_STREAMS 8 + +#define DMA_TRACE_LOCAL_SIZE HOST_PAGE_SIZE + +#endif /* __PLATFORM_PLATFORM_H__ */ + +#else + +#error "This file shouldn't be included from outside of sof/platform.h" + +#endif /* __SOF_PLATFORM_H__ */ diff --git a/src/platform/teensy41/include/platform/teensy41_audio_timer.h b/src/platform/teensy41/include/platform/teensy41_audio_timer.h new file mode 100644 index 000000000000..09115046c8fe --- /dev/null +++ b/src/platform/teensy41/include/platform/teensy41_audio_timer.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __SOF_PLATFORM_TEENSY41_AUDIO_TIMER_H__ +#define __SOF_PLATFORM_TEENSY41_AUDIO_TIMER_H__ + +#include + +typedef void (*teensy41_audio_timer_cb_t)(void *arg); + +int teensy41_audio_timer_init(teensy41_audio_timer_cb_t cb, void *arg); +void teensy41_audio_timer_start(void); +void teensy41_audio_timer_stop(void); + +#endif /* __SOF_PLATFORM_TEENSY41_AUDIO_TIMER_H__ */ diff --git a/src/platform/teensy41/include/platform/trace/trace.h b/src/platform/teensy41/include/platform/trace/trace.h new file mode 100644 index 000000000000..f2b0e2b46301 --- /dev/null +++ b/src/platform/teensy41/include/platform/trace/trace.h @@ -0,0 +1,9 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#ifndef __PLATFORM_TRACE_TRACE_H__ +#define __PLATFORM_TRACE_TRACE_H__ + +#endif /* __PLATFORM_TRACE_TRACE_H__ */ diff --git a/src/platform/teensy41/lib/CMakeLists.txt b/src/platform/teensy41/lib/CMakeLists.txt new file mode 100644 index 000000000000..a63b7cdf475c --- /dev/null +++ b/src/platform/teensy41/lib/CMakeLists.txt @@ -0,0 +1,5 @@ +# SPDX-License-Identifier: BSD-3-Clause + +sof_add_files( + clk.c +) diff --git a/src/platform/teensy41/lib/clk.c b/src/platform/teensy41/lib/clk.c new file mode 100644 index 000000000000..e76571e56516 --- /dev/null +++ b/src/platform/teensy41/lib/clk.c @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#include +#include + +static const struct freq_table platform_cpu_freq[] = { + { + .freq = CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC, + .ticks_per_msec = CONFIG_SYS_CLOCK_TICKS_PER_SEC / 1000, + }, +}; + +static struct clock_info platform_clocks_info[NUM_CLOCKS]; + +void platform_clock_init(struct sof *sof) +{ + int i; + + sof->clocks = platform_clocks_info; + + for (i = 0; i < CONFIG_CORE_COUNT; i++) { + sof->clocks[i] = (struct clock_info) { + .freqs_num = NUM_CPU_FREQ, + .freqs = platform_cpu_freq, + .default_freq_idx = CPU_DEFAULT_IDX, + .current_freq_idx = CPU_DEFAULT_IDX, + .notification_id = NOTIFIER_ID_CPU_FREQ, + .notification_mask = NOTIFIER_TARGET_CORE_MASK(i), + .set_freq = NULL, + }; + } +} diff --git a/src/platform/teensy41/platform.c b/src/platform/teensy41/platform.c new file mode 100644 index 000000000000..a330053c1f17 --- /dev/null +++ b/src/platform/teensy41/platform.c @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(teensy41_platform, CONFIG_SOF_LOG_LEVEL); + +uint8_t sof_teensy_mailbox[0x1000] __aligned(32); + +static const struct sof_ipc_fw_ready ready = { + .hdr = { + .cmd = SOF_IPC_FW_READY, + .size = sizeof(struct sof_ipc_fw_ready), + }, + .version = { + .hdr.size = sizeof(struct sof_ipc_fw_version), + .micro = SOF_MICRO, + .minor = SOF_MINOR, + .major = SOF_MAJOR, + .build = -1, + .date = "dtermin.\0", + .time = "fwready.\0", + .tag = SOF_TAG, + .abi_version = SOF_ABI_VERSION, + .src_hash = SOF_SRC_HASH, + }, + .flags = DEBUG_SET_FW_READY_FLAGS, +}; + +static const struct sof_ipc_window windows = { + .ext_hdr = { + .hdr.cmd = SOF_IPC_FW_READY, + .hdr.size = sizeof(struct sof_ipc_window), + .type = SOF_IPC_EXT_WINDOW, + }, + .num_windows = 3, + .window = { + { + .type = SOF_IPC_REGION_DOWNBOX, + .flags = 0, + .size = MAILBOX_HOSTBOX_SIZE, + .offset = MAILBOX_HOSTBOX_OFFSET, + }, + { + .type = SOF_IPC_REGION_UPBOX, + .id = 0, + .flags = 0, + .size = MAILBOX_DSPBOX_SIZE, + .offset = MAILBOX_DSPBOX_OFFSET, + }, + { + .type = SOF_IPC_REGION_STREAM, + .id = 0, + .flags = 0, + .size = MAILBOX_STREAM_SIZE, + .offset = MAILBOX_STREAM_OFFSET, + }, + }, +}; + +int platform_boot_complete(uint32_t boot_message) +{ + struct sof_ipc_reply reply; + + reply.error = 0; + reply.hdr.cmd = SOF_IPC_FW_READY; + reply.hdr.size = sizeof(reply); + + mailbox_hostbox_write(0, &reply, sizeof(reply)); + mailbox_hostbox_write(sizeof(reply), &ready, sizeof(ready)); + mailbox_hostbox_write(sizeof(reply) + sizeof(ready), + &windows, + sizeof(windows)); + + LOG_INF("Teensy 4.1 platform boot complete"); + return 0; +} + +int platform_context_save(struct sof *sof) +{ + return 0; +} + +#include +#include +#include +#include + +SOF_DEFINE_REG_UUID(zipc_task); +extern struct task_ops ipc_task_ops; + +int platform_ipc_init(struct ipc *ipc) +{ + ipc_set_drvdata(ipc, NULL); + schedule_task_init_edf(&ipc->ipc_task, SOF_UUID(zipc_task_uuid), + &ipc_task_ops, ipc, 0, 0); + return 0; +} + +enum task_state ipc_platform_do_cmd(struct ipc *ipc) +{ + struct ipc_cmd_hdr *hdr = mailbox_validate(); + + if (hdr) + ipc_cmd(hdr); + return SOF_TASK_STATE_COMPLETED; +} + +int ipc_platform_send_msg(const struct ipc_msg *msg) +{ + mailbox_dspbox_write(0, msg->tx_data, msg->tx_size); + return 0; +} + +static void teensy41_audio_hardware_init(void) +{ + /* 1. Configure Audio PLL (PLL4) for 688.128 MHz: + * Fref = 24 MHz, loopDivider = 28, numerator = 6720, denominator = 10000, postDivider = 1 + * Fpll = 24 * (28 + 6720 / 10000) = 688.128 MHz + */ + const clock_audio_pll_config_t audio_pll_cfg = { + .loopDivider = 28, + .postDivider = 1, + .numerator = 6720, + .denominator = 10000, + .src = 0, /* 24MHz OSC */ + }; + CLOCK_InitAudioPll(&audio_pll_cfg); + + /* 2. Configure SAI1 clock root: Mux = Audio PLL (2), PreDiv = /4 (3), Podf = /14 (13) + * SAI1 MCLK = 688.128 MHz / 4 / 14 = 12.288 MHz (256 * 48000 Hz) + */ + CLOCK_SetMux(kCLOCK_Sai1Mux, 2); + CLOCK_SetDiv(kCLOCK_Sai1PreDiv, 3); + CLOCK_SetDiv(kCLOCK_Sai1Div, 13); + CLOCK_EnableClock(kCLOCK_Sai1); + + /* 3. Configure S/PDIF clock root and clock gate */ + CLOCK_SetMux(kCLOCK_SpdifMux, 0); /* PLL4 (Audio PLL: 688.128 MHz) */ + CLOCK_SetDiv(kCLOCK_Spdif0PreDiv, 3); /* /4 */ + CLOCK_SetDiv(kCLOCK_Spdif0Div, 6); /* /7 => 688.128 / 28 = 24.576000 MHz */ + CLOCK_EnableClock(kCLOCK_Spdif); + + /* Configure S/PDIF I/O Pins: + * Pin 14: GPIO_AD_B1_02 -> ALT3 (SPDIF_OUT) + * Pin 15: GPIO_AD_B1_03 -> ALT3 (SPDIF_IN), Daisy Chain 0x401F85C8 = 0 + */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_02_SPDIF_OUT, 0U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_02_SPDIF_OUT, 0x10B1); + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_03_SPDIF_IN, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_03_SPDIF_IN, 0x10B1); + + /* 4. Configure SAI1 I/O Pins and Daisy Chains */ +#if defined(CONFIG_TEENSY41_BOARD_B) + /* Board B: Slave Rx - consumes BCLK and SYNC generated by Board A */ + /* Pin 21: SAI1_RX_BCLK (Input), Daisy Chain 0x401F8590 = 1 */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_11_SAI1_RX_BCLK, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_11_SAI1_RX_BCLK, 0x10B0); + + /* Pin 20: SAI1_RX_SYNC (Input), Daisy Chain 0x401F85A4 = 1 */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_10_SAI1_RX_SYNC, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_10_SAI1_RX_SYNC, 0x10B0); +#else + /* Board A: Master Tx - drives BCLK and SYNC on Pin 21 and Pin 20 */ + /* Enable MCLK output on Pin 23 (GPIO_AD_B1_09) */ + IOMUXC_GPR->GPR1 |= IOMUXC_GPR_GPR1_SAI1_MCLK_DIR_MASK; + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_09_SAI1_MCLK, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_09_SAI1_MCLK, 0x10B0); + + /* Pin 21: SAI1_RX_BCLK with SION=1 (so TX BCLK output feeds back into RX) */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_11_SAI1_RX_BCLK, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_11_SAI1_RX_BCLK, 0x10B0); + + /* Pin 20: SAI1_RX_SYNC with SION=1 (so TX SYNC output feeds back into RX) */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_10_SAI1_RX_SYNC, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_10_SAI1_RX_SYNC, 0x10B0); +#endif + + /* Pin 8: SAI1_RX_DATA0 (Input on GPIO_B1_00), Daisy Chain 0x401F8594 = 2 */ + IOMUXC_SetPinMux(IOMUXC_GPIO_B1_00_SAI1_RX_DATA00, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_00_SAI1_RX_DATA00, 0x10B0); + + /* Pin 7: SAI1_TX_DATA0 (Output on GPIO_B1_01) */ + IOMUXC_SetPinMux(IOMUXC_GPIO_B1_01_SAI1_TX_DATA00, 0U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_B1_01_SAI1_TX_DATA00, 0x10B0); + + /* Pin 9: SAI1_TX_DATA2 (Output on GPIO_B0_11) */ + IOMUXC_SetPinMux(IOMUXC_GPIO_B0_11_SAI1_TX_DATA02, 0U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_B0_11_SAI1_TX_DATA02, 0x10B0); + + /* 5. Configure S/PDIF Pins (Pin 14 = SPDIF_OUT, Pin 15 = SPDIF_IN) */ + /* Pin 14: SPDIF_OUT on GPIO_AD_B1_02 */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_02_SPDIF_OUT, 0U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_02_SPDIF_OUT, 0x10B1); + + /* Pin 15: SPDIF_IN on GPIO_AD_B1_03, Daisy Chain 0x401F85C8 = 0 */ + IOMUXC_SetPinMux(IOMUXC_GPIO_AD_B1_03_SPDIF_IN, 1U); + IOMUXC_SetPinConfig(IOMUXC_GPIO_AD_B1_03_SPDIF_IN, 0x10B1); + + /* Explicitly ensure daisy chain select input registers */ + *((volatile uint32_t *)0x401F8590) = 1; /* SAI1_RX_BCLK (GPIO_AD_B1_11) */ + *((volatile uint32_t *)0x401F85A4) = 1; /* SAI1_RX_SYNC (GPIO_AD_B1_10) */ + *((volatile uint32_t *)0x401F8594) = 2; /* SAI1_RX_DATA0 (GPIO_B1_00) */ + *((volatile uint32_t *)0x401F859C) = 1; /* SAI1_TX_DATA2 (GPIO_B0_11) */ + *((volatile uint32_t *)0x401F85C8) = 0; /* SPDIF_IN (GPIO_AD_B1_03) */ + + LOG_INF("Teensy 4.1 Audio HW initialized: AudioPLL=688.128MHz, MCLK=12.288MHz, Master=%d", + !IS_ENABLED(CONFIG_TEENSY41_BOARD_B)); +} + +int platform_init(struct sof *sof) +{ + LOG_INF("Initializing Teensy 4.1 SOF Platform..."); + + platform_clock_init(sof); + + teensy41_audio_hardware_init(); + + /* Initialize EDF scheduler */ + scheduler_init_edf(); + + /* Initialize Zephyr domain and timer-based LL scheduler */ + sof->platform_timer_domain = zephyr_domain_init(PLATFORM_DEFAULT_CLOCK); + if (!sof->platform_timer_domain) { + LOG_ERR("Failed to initialize platform timer domain"); + return -ENODEV; + } + + zephyr_ll_scheduler_init(sof->platform_timer_domain); + + /* Initialize platform DMAC */ + dmac_init(sof); + + /* Initialize Zephyr native DAI subsystem */ + dai_init(sof); + + /* Initialize IPC */ + ipc_init(sof); + + LOG_INF("Teensy 4.1 SOF Platform initialized successfully"); + return 0; +} + +void ipc_platform_complete_cmd(struct ipc *ipc) +{ + ARG_UNUSED(ipc); +} + +void ipc_platform_send_msg_direct(const struct ipc_msg *msg) +{ + ARG_UNUSED(msg); +} diff --git a/src/platform/teensy41/teensy41_pipeline_def.c b/src/platform/teensy41/teensy41_pipeline_def.c new file mode 100644 index 000000000000..1e36883b6b57 --- /dev/null +++ b/src/platform/teensy41/teensy41_pipeline_def.c @@ -0,0 +1,301 @@ +// SPDX-License-Identifier: BSD-3-Clause +/* + * Copyright (c) 2026 Sound Open Firmware (SOF) Project + */ + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_TEENSY41_BOARD_B) +/* Board B: Slave Rx (consumes BCLK and SYNC generated by Board A) */ +#define TEENSY41_SAI_CLOCK_MODE SOF_DAI_FMT_CBP_CFP +#else +/* Board A: Master Tx (generates BCLK and SYNC on Pin 21 and Pin 20) */ +#define TEENSY41_SAI_CLOCK_MODE SOF_DAI_FMT_CBC_CFC +#endif + +/* UUID External Declarations */ +extern const struct sof_uuid usb_audio_uuid; +#if CONFIG_IPC_MAJOR_4 +extern const struct sof_uuid volume4_uuid; +#define volume_uuid volume4_uuid +#else +extern const struct sof_uuid volume_uuid; +#endif +extern const struct sof_uuid eq_iir_uuid; +extern const struct sof_uuid drc_uuid; +extern const struct sof_uuid dai_uuid; +extern const struct sof_uuid tone_uuid; +extern const struct sof_uuid level_multiplier_uuid; +extern const struct sof_uuid selector_uuid; +extern const struct sof_uuid mixer_uuid; + +#if defined(CONFIG_COMP_DRC) +extern void sys_comp_module_drc_interface_init(void); +static void * const _force_drc_ref __used = (void *)sys_comp_module_drc_interface_init; +#endif + +/* UAC2 Device Tree Entity IDs */ +#define UAC2_STATIC_ENTITY_ID(node) (DT_NODE_CHILD_IDX(node) + 1) + +#define PLAYBACK_FU_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(i2s_fu)) +#define PLAYBACK_EQ_FU_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(pb_eq_fu)) +#define PLAYBACK_DRC_FU_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(pb_drc_fu)) +#define CAPTURE_EQ_FU_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(cap_eq_fu)) +#define CAPTURE_FU_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(i2s_in_fu)) +#define PLAYBACK_TERM_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(i2s_out_terminal)) +#define CAPTURE_TERM_ID UAC2_STATIC_ENTITY_ID(DT_NODELABEL(i2s_in_terminal)) + +/* Default 2-channel 4-band Parametric IIR EQ Coefficients */ +static const uint32_t teensy41_default_iir_coef_2ch[51] = { + 0x00464f53, 0x00000000, 0x000000ac, 0x03013000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x000000ac, 0x00000002, 0x00000001, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000004, 0x00000004, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0xc12c82bd, + 0x7ed0b52e, 0x1fc7cc0c, 0xc07067e9, 0x1fc7cc0c, + 0x00000000, 0x00004000, 0xcad0cdef, 0x742e8c5d, + 0x0cdc9086, 0xe2f11723, 0x10b2f932, 0x00000000, + 0x00004000, 0xcf45334a, 0x68260de9, 0x0a54e176, + 0xe5d6cb75, 0x11fc1f3d, 0x00000000, 0x00004000, + 0xf2940609, 0xe25f3930, 0x0d69ba64, 0x1ad374c8, + 0x0d69ba64, 0xfffffffb, 0x000045bf +}; + +/* Default Speaker DRC Compressor Profile */ +static const uint32_t teensy41_default_drc_coef[35] = { + 0x00464f53, 0x00000000, 0x0000006c, 0x0301a000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x0000006c, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000001, 0xe2000000, 0x14000000, + 0x0a000000, 0x00624dd3, 0x02061b8a, 0x06666666, + 0x00ba972f, 0x001e0c18, 0xffe04220, 0x0050f44e, + 0x08349f9a, 0x04d82cd3, 0x0071c71c, 0xff777777, + 0x001f77d8, 0x00000005, 0x00438000, 0x00047dd7, + 0x0025cea0, 0x00097dd7, 0x0000b5b1 +}; + +/* ------------------------------------------------------------------------- + * 1. Component / Module Declarations + * ------------------------------------------------------------------------- */ +static const struct sof_static_comp teensy41_comps[] = { + /* --- Playback Pipeline (Pipeline 1) --- */ + SOF_STATIC_COMP_HOST( + .id = 1, .pipeline_id = 1, .name = "USB_PB", + .uuid = &usb_audio_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2), + .ep.usb.terminal_id = PLAYBACK_TERM_ID + ), + SOF_STATIC_COMP_MODULE( + .id = 2, .pipeline_id = 1, .name = "VOL_PB", + .uuid = &volume_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2) + ), + SOF_STATIC_COMP_MODULE( + .id = 3, .pipeline_id = 1, .name = "EQ_PB", + .uuid = &eq_iir_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_FLOAT, 48000, 2), + .init_blob = teensy41_default_iir_coef_2ch, + .init_blob_size = sizeof(teensy41_default_iir_coef_2ch) + ), + SOF_STATIC_COMP_MODULE( + .id = 4, .pipeline_id = 1, .name = "DRC_PB", + .uuid = &drc_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_FLOAT, 48000, 2), + .init_blob = teensy41_default_drc_coef, + .init_blob_size = sizeof(teensy41_default_drc_coef) + ), +#if defined(CONFIG_TEENSY41_INTERFACE_SPDIF) + SOF_STATIC_COMP_DAI( + .id = 5, .pipeline_id = 1, .name = "DAI_SPDIF_PB", + .uuid = &dai_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S24_4LE, 48000, 2), + .ep.dai.dai_type = SOF_DAI_IMX_SPDIF, + .ep.dai.dai_index = 0, + .ep.dai.format = SOF_DAI_FMT_SPDIF + ), + + /* --- Capture Pipeline (Pipeline 2) --- */ + SOF_STATIC_COMP_DAI( + .id = 6, .pipeline_id = 2, .name = "DAI_SPDIF_CAP", + .uuid = &dai_uuid, .direction = SOF_IPC_STREAM_CAPTURE, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S24_4LE, 48000, 2), + .ep.dai.dai_type = SOF_DAI_IMX_SPDIF, + .ep.dai.dai_index = 0, + .ep.dai.format = SOF_DAI_FMT_SPDIF + ), +#else + SOF_STATIC_COMP_DAI( + .id = 5, .pipeline_id = 1, .name = "DAI_I2S_PB", + .uuid = &dai_uuid, .direction = SOF_IPC_STREAM_PLAYBACK, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2), + .ep.dai.dai_type = SOF_DAI_IMX_SAI, + .ep.dai.dai_index = 1, + .ep.dai.format = (SOF_DAI_FMT_I2S | TEENSY41_SAI_CLOCK_MODE) + ), + + /* --- Capture Pipeline (Pipeline 2) --- */ + SOF_STATIC_COMP_DAI( + .id = 6, .pipeline_id = 2, .name = "DAI_I2S_CAP", + .uuid = &dai_uuid, .direction = SOF_IPC_STREAM_CAPTURE, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2), + .ep.dai.dai_type = SOF_DAI_IMX_SAI, + .ep.dai.dai_index = 1, + .ep.dai.format = (SOF_DAI_FMT_I2S | TEENSY41_SAI_CLOCK_MODE) + ), +#endif + SOF_STATIC_COMP_MODULE( + .id = 8, .pipeline_id = 2, .name = "EQ_CAP", + .uuid = &eq_iir_uuid, .direction = SOF_IPC_STREAM_CAPTURE, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_FLOAT, 48000, 2), + .init_blob = teensy41_default_iir_coef_2ch, + .init_blob_size = sizeof(teensy41_default_iir_coef_2ch) + ), + SOF_STATIC_COMP_MODULE( + .id = 9, .pipeline_id = 2, .name = "VOL_CAP", + .uuid = &volume_uuid, .direction = SOF_IPC_STREAM_CAPTURE, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2) + ), + SOF_STATIC_COMP_HOST( + .id = 10, .pipeline_id = 2, .name = "USB_CAP", + .uuid = &usb_audio_uuid, .direction = SOF_IPC_STREAM_CAPTURE, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2), + .ep.usb.terminal_id = CAPTURE_TERM_ID + ), +}; + +/* ------------------------------------------------------------------------- + * 2. Intermediate Audio Buffers + * ------------------------------------------------------------------------- */ +static const struct sof_static_buffer teensy41_buffers[] = { + /* Direct Playback Buffer: USB_PB (1) -> [1] -> DAI_I2S (5) */ + SOF_STATIC_BUFFER(.id = 1, .size = 3072, .fmt = SOF_IPC_FRAME_S16_LE), + + /* Direct Capture Buffer: DAI_I2S (6) -> [5] -> USB (10) */ + SOF_STATIC_BUFFER(.id = 5, .size = 3072, .fmt = SOF_IPC_FRAME_S16_LE), +}; + +/* ------------------------------------------------------------------------- + * 3. Pipeline Connections / Graph Routing + * ------------------------------------------------------------------------- */ +static const struct sof_static_route teensy41_routes[] = { + /* Direct Playback Route: USB_PB (1) -> [1] -> DAI_I2S (5) */ + SOF_STATIC_ROUTE(.src_comp_id = 1, .buffer_id = 1, .sink_comp_id = 5), + + /* Direct Capture Route: DAI_I2S (6) -> [5] -> USB (10) */ + SOF_STATIC_ROUTE(.src_comp_id = 6, .buffer_id = 5, .sink_comp_id = 10), +}; + +/* ------------------------------------------------------------------------- + * 4. PCMs (Pulse Code Modulated Endpoints) + * ------------------------------------------------------------------------- */ +static const struct sof_static_pcm teensy41_pcms[] = { + SOF_STATIC_PCM( + .pcm_id = 0, + .name = "Speaker Playback", + .direction = SOF_IPC_STREAM_PLAYBACK, + .pipeline_id = 1, + .host_comp_id = 1, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2) + ), + SOF_STATIC_PCM( + .pcm_id = 1, + .name = "Mic Capture", + .direction = SOF_IPC_STREAM_CAPTURE, + .pipeline_id = 2, + .host_comp_id = 10, + .caps = SOF_STATIC_CAPS(SOF_IPC_FRAME_S16_LE, 48000, 2) + ), +}; + +/* ------------------------------------------------------------------------- + * 5. Kcontrols (Volume, Mute, EQ/DRC Bypass Switches) + * ------------------------------------------------------------------------- */ +static const struct sof_static_kcontrol teensy41_controls[] = { + SOF_STATIC_KCONTROL_VOLUME( + .id = 1, .name = "Master Playback Volume", + .target_comp_id = 2, + .min = 0, .max = 65536, .def = 65536, .channels = 2, + .uac2_entity_id = PLAYBACK_FU_ID + ), + SOF_STATIC_KCONTROL_SWITCH( + .id = 2, .name = "Playback EQ Switch", + .target_comp_id = 3, + .def = 0, /* 1 = Enabled, 0 = Bypassed */ + .uac2_entity_id = PLAYBACK_EQ_FU_ID + ), + SOF_STATIC_KCONTROL_SWITCH( + .id = 3, .name = "Playback DRC Switch", + .target_comp_id = 4, + .def = 0, + .uac2_entity_id = PLAYBACK_DRC_FU_ID + ), + SOF_STATIC_KCONTROL_SWITCH( + .id = 4, .name = "Capture EQ Switch", + .target_comp_id = 8, + .def = 0, + .uac2_entity_id = CAPTURE_EQ_FU_ID + ), + SOF_STATIC_KCONTROL_VOLUME( + .id = 5, .name = "Master Capture Volume", + .target_comp_id = 9, + .min = 0, .max = 65536, .def = 65536, .channels = 2, + .uac2_entity_id = CAPTURE_FU_ID + ), +}; + +/* ------------------------------------------------------------------------- + * 6. Pipeline Descriptors + * ------------------------------------------------------------------------- */ +static const struct sof_static_pipeline_desc teensy41_pipelines[] = { + { + .pipeline_id = 1, + .name = "Playback Pipeline", + .direction = SOF_IPC_STREAM_PLAYBACK, + .priority = 0, + .core = 0, + .period = 1000, /* 1ms */ + .frames_per_sched = 48, + .time_domain = SOF_TIME_DOMAIN_TIMER, + .sched_comp_id = 1, + .source_comp_id = 1, + .sink_comp_id = 5, + }, + { + .pipeline_id = 2, + .name = "Capture Pipeline", + .direction = SOF_IPC_STREAM_CAPTURE, + .priority = 0, + .core = 0, + .period = 1000, /* 1ms */ + .frames_per_sched = 48, + .time_domain = SOF_TIME_DOMAIN_TIMER, + .sched_comp_id = 10, + .source_comp_id = 6, + .sink_comp_id = 10, + }, +}; + +/* ------------------------------------------------------------------------- + * Top-Level Exported Topology + * ------------------------------------------------------------------------- */ +const struct sof_static_topology g_teensy41_static_topology = { + .name = "Teensy 4.1 Playback & Capture Pipeline", + .num_pipelines = ARRAY_SIZE(teensy41_pipelines), + .pipelines = teensy41_pipelines, + .num_comps = ARRAY_SIZE(teensy41_comps), + .comps = teensy41_comps, + .num_buffers = ARRAY_SIZE(teensy41_buffers), + .buffers = teensy41_buffers, + .num_routes = ARRAY_SIZE(teensy41_routes), + .routes = teensy41_routes, + .num_pcms = ARRAY_SIZE(teensy41_pcms), + .pcms = teensy41_pcms, + .num_controls = ARRAY_SIZE(teensy41_controls), + .controls = teensy41_controls, +}; diff --git a/src/schedule/zephyr_domain.c b/src/schedule/zephyr_domain.c index 5ceee36fa98a..2ef20122d7d9 100644 --- a/src/schedule/zephyr_domain.c +++ b/src/schedule/zephyr_domain.c @@ -174,6 +174,12 @@ static void zephyr_domain_timer_fn(struct k_timer *timer) return; } +#if defined(CONFIG_PLATFORM_TEENSY41) + /* When audio stream is active, Audio PLL eDMA interrupts drive the LL domain */ + if (dai_zephyr_has_active_audio_domain()) + return; +#endif + for (core = 0; core < CONFIG_CORE_COUNT; core++) { struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + core; @@ -182,6 +188,29 @@ static void zephyr_domain_timer_fn(struct k_timer *timer) } } +#if defined(CONFIG_PLATFORM_TEENSY41) +void zephyr_domain_audio_timer_cb(void) +{ + struct ll_schedule_domain *domain = zephyr_ll_domain(); + struct zephyr_domain *zephyr_domain; + int core; + + if (!domain) + return; + + zephyr_domain = ll_sch_domain_get_pdata(domain); + if (!zephyr_domain) + return; + + for (core = 0; core < CONFIG_CORE_COUNT; core++) { + struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + core; + + if (dt->handler) + k_sem_give(dt->sem); + } +} +#endif + /* The normal kernel-space implementation for register/unregister */ #ifndef CONFIG_SOF_USERSPACE_LL diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 48cf07b0b89a..14f3c6bf8e39 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -8,6 +8,10 @@ if(CONFIG_ZEPHYR_POSIX) set(ARCH host) set(PLATFORM "posix") set(PLATFORM_HEADERS "posix") +elseif(CONFIG_PLATFORM_TEENSY41) + set(ARCH host) + set(PLATFORM "teensy41") + set(PLATFORM_HEADERS "teensy41") else() # firmware build supports only xtensa arch for now set(ARCH xtensa) @@ -196,7 +200,7 @@ zephyr_include_directories(${SOF_SRC_PATH}/include/sof/audio/module_adapter/libr # XTOS duplicate in sof/scripts/misc.cmake; keep in sync macro(is_zephyr ret) - if(CONFIG_ZEPHYR_SOF_MODULE) + if(CONFIG_ZEPHYR_SOF_MODULE OR CONFIG_SOF_FULL_ZEPHYR_APPLICATION) set(${ret} TRUE) else() set(${ret} FALSE) @@ -456,6 +460,18 @@ if (CONFIG_SOC_MIMX9596_M7) board_set_rimage_target(imx95) endif() +if (CONFIG_PLATFORM_TEENSY41) + zephyr_library_sources( + ${SOF_PLATFORM_PATH}/teensy41/platform.c + ${SOF_PLATFORM_PATH}/teensy41/teensy41_pipeline_def.c + ${SOF_PLATFORM_PATH}/teensy41/lib/clk.c + lib/dma.c + ) + + set(PLATFORM "teensy41") + zephyr_include_directories(${SOF_PLATFORM_PATH}/teensy41/include) +endif() + # AMD RMB platforms if (CONFIG_SOC_ACP_6_0) zephyr_library_sources( @@ -655,6 +671,7 @@ set(SOF_ROOT_BINARY_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) # This generated/ directory is shared with Zephyr. # PROJECT_BINARY_DIR is build/zephyr/ set(GENERATED_DIRECTORY ${PROJECT_BINARY_DIR}/include/generated) +zephyr_include_directories(${GENERATED_DIRECTORY}) set(VERSION_H_PATH ${GENERATED_DIRECTORY}/sof_versions.h) diff --git a/zephyr/include/sof/lib/cpu.h b/zephyr/include/sof/lib/cpu.h index a47b93898973..5338bd0c85e1 100644 --- a/zephyr/include/sof/lib/cpu.h +++ b/zephyr/include/sof/lib/cpu.h @@ -48,14 +48,14 @@ void cpu_notify_state_exit(enum pm_state state); * from user mode. In supervisor context the generated wrapper inlines * the z_impl_cpu_get_id() body, so there is no overhead there. */ -#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) __syscall int cpu_get_id(void); #endif /* let the compiler optimise when in single core mode */ #if CONFIG_MULTICORE && CONFIG_SMP -#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) static inline int z_impl_cpu_get_id(void) { return arch_proc_id(); @@ -92,7 +92,7 @@ int cpu_restore_secondary_cores(void); int cpu_secondary_cores_prepare_d0ix(void); #else -#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) static inline int z_impl_cpu_get_id(void) { return 0; }; #else static inline int cpu_get_id(void) { return 0; }; @@ -116,7 +116,7 @@ static inline int cpu_secondary_cores_prepare_d0ix(void) { return 0; }; #endif /* CONFIG_MULTICORE && CONFIG_SMP */ -#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) +#if defined(CONFIG_SOF_FULL_ZEPHYR_APPLICATION) && defined(CONFIG_USERSPACE) #include #endif diff --git a/zephyr/include/sof/lib/dma.h b/zephyr/include/sof/lib/dma.h index 261abe8cacea..ff209181ed8d 100644 --- a/zephyr/include/sof/lib/dma.h +++ b/zephyr/include/sof/lib/dma.h @@ -102,6 +102,7 @@ struct k_heap; #define SOF_DMA_DEV_MICFIL BIT(14) /**< connectable to MICFIL fifo */ #define SOF_DMA_DEV_SW BIT(15) /**< connectable to ACP SW */ #define SOF_DMA_DEV_TDM BIT(16) /**< connectable to ACP TDM I2S */ +#define SOF_DMA_DEV_SPDIF BIT(17) /**< connectable to S/PDIF */ /* DMA access privilege flag */ #define SOF_DMA_ACCESS_EXCLUSIVE 1 diff --git a/zephyr/lib/alloc.c b/zephyr/lib/alloc.c index 3b2069b9de97..7174e2e56765 100644 --- a/zephyr/lib/alloc.c +++ b/zephyr/lib/alloc.c @@ -139,6 +139,11 @@ extern char _mtk_adsp_sram_end; #define HEAPMEM_SIZE ((uint8_t *)SRAM_END - heapmem) #endif /* CONFIG_SYS_HEAP_SMALL_ONLY */ +#elif defined(CONFIG_PLATFORM_TEENSY41) + +#define HEAPMEM_SIZE (HEAP_SYSTEM_SIZE + HEAP_RUNTIME_SIZE + HEAP_BUFFER_SIZE) +static uint8_t __aligned(PLATFORM_DCACHE_ALIGN) heapmem[HEAPMEM_SIZE]; + #else extern char _end[], _heap_sentry[]; diff --git a/zephyr/lib/dma.c b/zephyr/lib/dma.c index a478d5069a09..03ed9a87744d 100644 --- a/zephyr/lib/dma.c +++ b/zephyr/lib/dma.c @@ -276,10 +276,21 @@ APP_SYSUSER_DATA struct sof_dma dma[] = { .devs = SOF_DMA_DEV_HOST, .channels = DT_PROP(DT_NODELABEL(host_dma), dma_channels), .period_count = 4, - }, .z_dev = DEVICE_DT_GET(DT_NODELABEL(host_dma)), }, #endif +#if DT_NODE_HAS_STATUS(DT_NODELABEL(edma0), okay) && defined(CONFIG_PLATFORM_TEENSY41) +{ + .plat_data = { + .dir = SOF_DMA_DIR_MEM_TO_DEV | SOF_DMA_DIR_DEV_TO_MEM, + .caps = SOF_DMA_CAP_GP_LP | SOF_DMA_CAP_GP_HP, + .devs = SOF_DMA_DEV_SAI | SOF_DMA_DEV_SPDIF, + .channels = 32, + .period_count = 2, + }, + .z_dev = DEVICE_DT_GET(DT_NODELABEL(edma0)), +}, +#endif }; const struct dma_info lib_dma = { From 8e00e420c880ec45eb88890a401b198dbc6eaae1 Mon Sep 17 00:00:00 2001 From: Liam Girdwood Date: Thu, 15 Oct 2026 20:34:12 +0100 Subject: [PATCH 4/4] scripts: teensy41: add S/PDIF and I2S pre-commit loopback test and diagnostic tools - test_teensy_loopback.py: automated pre-commit hardware loopback test suite verifying SNR (>= 70.0 dB S/PDIF, >= 80.0 dB I2S), frequency accuracy, and zero dropped frames; includes two-pass ALSA device resolution and transient trimming. - teensy_diag.py: inspect real-time S/PDIF registers (SCR, SRPC, STC, SRFM, SIS, SIE), SAI1 registers, eDMA TCD descriptors, and SOF component states via USB request 0xFC. - teensy_flash.py: PJRC HalfKay USB HID flasher supporting automatic soft-reboot, multi-board targeting by USB port or serial, and direct hex programming. Signed-off-by: Liam Girdwood --- scripts/teensy_diag.py | 168 ++++++++++++++++ scripts/teensy_flash.py | 329 +++++++++++++++++++++++++++++++ scripts/test_teensy_loopback.py | 338 ++++++++++++++++++++++++++++++++ 3 files changed, 835 insertions(+) create mode 100644 scripts/teensy_diag.py create mode 100755 scripts/teensy_flash.py create mode 100755 scripts/test_teensy_loopback.py diff --git a/scripts/teensy_diag.py b/scripts/teensy_diag.py new file mode 100644 index 000000000000..d7e8f8413f29 --- /dev/null +++ b/scripts/teensy_diag.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +""" +Teensy 4.1 SOF Audio Hardware Diagnostic Tool +Queries real-time hardware registers, DMA TCD status, and SOF pipeline states via USB vendor request 0xFC. +""" + +import sys +import struct +import usb.core +import usb.util + +TEENSY_VID = 0x16C0 +TEENSY_PID = 0x04D2 + +# Port mappings +PORTS = { + 'a': [(3, (5, 3)), (3, (12, 4, 1, 3))], + 'b': [(3, (5, 2)), (3, (12, 4, 1, 2))], +} + +DIAG_FMT_44 = "<44I" +DIAG_FMT_32 = "<32I" +DIAG_FMT_24 = "<24I" +DIAG_FMT_LEGACY = "<20I" + +def query_diag(target): + target = target.lower() + target_ports = PORTS.get(target) + if target_ports and not isinstance(target_ports, list): + target_ports = [target_ports] + + devices = list(usb.core.find(find_all=True, idVendor=TEENSY_VID, idProduct=TEENSY_PID)) + match_dev = None + for d in devices: + port = (d.bus, tuple(d.port_numbers) if d.port_numbers else None) + if target_ports and port in target_ports: + match_dev = d + break + elif not target_ports: + match_dev = d + break + + if not match_dev: + print(f"Error: Teensy '{target}' not found!") + return None + + try: + # Vendor IN request 0xFC, recipient device (0xC0) + data = match_dev.ctrl_transfer(0xC0, 0xFC, 0, 0, 256, timeout=1000) + except Exception as e: + print(f"Error reading diag from Teensy '{target}': {e}") + return None + + if len(data) >= struct.calcsize(DIAG_FMT_44): + unpacked = struct.unpack(DIAG_FMT_44, bytes(data[:struct.calcsize(DIAG_FMT_44)])) + fields = [ + "tcsr", "rcsr", "tcr2", "rcr2", "tcr3", "rcr3", "tfr0", "rfr0", + "rx_pkt_cnt", "tx_pkt_cnt", "pipe1_status", "pipe2_status", + "dai5_state", "dai6_state", + "dma_tx_csr", "dma_rx_csr", "dma_tx_saddr", "dma_rx_daddr", + "dma_tx_citer", "dma_rx_citer", + "c1_state", "c2_state", "c9_state", "c10_state", + "tcr4", "rcr4", "tcr5", "rcr5", + "pin8_mux", "pin8_daisy", "pin7_mux", "pin8_pad", "pin7_pad", + "gpio1_psr", "gpio2_psr", "interface_type", + "g_dbg0", "g_dbg1", "g_dbg2", "g_dbg3", + "g_dbg4", "g_dbg5", "g_dbg6", "g_dbg7" + ] + elif len(data) >= struct.calcsize(DIAG_FMT_32): + unpacked = struct.unpack(DIAG_FMT_32, bytes(data[:struct.calcsize(DIAG_FMT_32)])) + fields = [ + "tcsr", "rcsr", "tcr2", "rcr2", "tcr3", "rcr3", "tfr0", "rfr0", + "rx_pkt_cnt", "tx_pkt_cnt", "pipe1_status", "pipe2_status", + "dai5_state", "dai6_state", + "dma_tx_csr", "dma_rx_csr", "dma_tx_saddr", "dma_rx_daddr", + "dma_tx_citer", "dma_rx_citer", + "c1_state", "c2_state", "c9_state", "c10_state", + "tcr4", "rcr4", "tcr5", "rcr5", + "pin8_mux", "pin8_daisy", "pin7_mux", "pin8_pad" + ] + elif len(data) >= struct.calcsize(DIAG_FMT_24): + unpacked = struct.unpack(DIAG_FMT_24, bytes(data[:struct.calcsize(DIAG_FMT_24)])) + fields = [ + "tcsr", "rcsr", "tcr2", "rcr2", "tcr3", "rcr3", "tfr0", "rfr0", + "rx_pkt_cnt", "tx_pkt_cnt", "pipe1_status", "pipe2_status", + "dai5_state", "dai6_state", + "dma_tx_csr", "dma_rx_csr", "dma_tx_saddr", "dma_rx_daddr", + "dma_tx_citer", "dma_rx_citer", + "c1_state", "c2_state", "c9_state", "c10_state" + ] + elif len(data) >= struct.calcsize(DIAG_FMT_LEGACY): + unpacked = struct.unpack(DIAG_FMT_LEGACY, bytes(data[:struct.calcsize(DIAG_FMT_LEGACY)])) + fields = [ + "tcsr", "rcsr", "tcr2", "rcr2", "tcr3", "rcr3", "tfr0", "rfr0", + "rx_pkt_cnt", "tx_pkt_cnt", "pipe1_status", "pipe2_status", + "dai5_state", "dai6_state", + "dma_tx_csr", "dma_rx_csr", "dma_tx_saddr", "dma_rx_daddr", + "dma_tx_citer", "dma_rx_citer" + ] + else: + print(f"Error: Short response ({len(data)} bytes)") + return None + + res = dict(zip(fields, unpacked)) + return res + +def print_diag(target, res): + print(f"=== Teensy 4.1 '{target.upper()}' Diag Report ===") + if res.get('interface_type') == 2: + print(" Active Hardware Interface: S/PDIF Transceiver (Board A Pin 14 ALT3 -> Board B Pin 15 ALT3)") + print(f" SPDIF SCR: 0x{res['tcsr']:08X} (TXFIFO_CTRL={(res['tcsr']>>10)&3}, USRC_SEL={(res['tcsr']>>2)&3})") + print(f" SPDIF SRPC: 0x{res['rcsr']:08X} (CLKSRC_SEL={(res['rcsr']>>7)&0xF}, GAIN_SEL={(res['rcsr']>>3)&7})") + print(f" SPDIF STC: 0x{res['tcr2']:08X} (TXCLK_SOURCE={(res['tcr2']>>8)&7}, SYSCLK_DF={res['tcr2']&0xFF})") + print(f" SPDIF SRFM: 0x{res['rcr2']:08X} (FREQMEAS={res['rcr2']&0xFFFFFF})") + print(f" SPDIF SIS: 0x{res['tcr3']:08X}") + print(f" SPDIF SIE: 0x{res['rcr3']:08X}") + else: + print(" Active Hardware Interface: SAI1 I2S (Pins 7, 8, 20, 21, 23)") + print(f" I2S1 TCSR: 0x{res['tcsr']:08X} (TE={bool(res['tcsr'] & (1<<31))}, BCE={bool(res['tcsr'] & (1<<28))}, FEF={bool(res['tcsr'] & (1<<18))})") + print(f" I2S1 RCSR: 0x{res['rcsr']:08X} (RE={bool(res['rcsr'] & (1<<31))}, BCE={bool(res['rcsr'] & (1<<28))}, FEF={bool(res['rcsr'] & (1<<18))})") + print(f" I2S1 TCR2: 0x{res['tcr2']:08X} (SYNC={(res['tcr2']>>30)&3}, BCD={(res['tcr2']>>24)&1}, MSEL={(res['tcr2']>>26)&3}, DIV={(res['tcr2']&0xFF)})") + print(f" I2S1 RCR2: 0x{res['rcr2']:08X} (SYNC={(res['rcr2']>>30)&3}, BCD={(res['rcr2']>>24)&1}, MSEL={(res['rcr2']>>26)&3}, DIV={(res['rcr2']&0xFF)})") + print(f" I2S1 TCR3: 0x{res['tcr3']:08X} (TCE={(res['tcr3']>>16)&0xF})") + print(f" I2S1 RCR3: 0x{res['rcr3']:08X} (RCE={(res['rcr3']>>16)&0xF})") + if 'tcr4' in res and res.get('interface_type') != 2: + print(f" I2S1 TCR4: 0x{res['tcr4']:08X} (FSE={(res['tcr4']>>3)&1}, FSP={(res['tcr4']>>1)&1}, FSD={res['tcr4']&1}, SYWD={(res['tcr4']>>8)&0x1F}, FRSZ={(res['tcr4']>>16)&0xF})") + print(f" I2S1 RCR4: 0x{res['rcr4']:08X} (FSE={(res['rcr4']>>3)&1}, FSP={(res['rcr4']>>1)&1}, FSD={res['rcr4']&1}, SYWD={(res['rcr4']>>8)&0x1F}, FRSZ={(res['rcr4']>>16)&0xF})") + print(f" I2S1 TCR5: 0x{res['tcr5']:08X} (WNW={(res['tcr5']>>24)&0x1F}, W0W={(res['tcr5']>>16)&0x1F}, FBT={(res['tcr5']>>8)&0x1F})") + print(f" I2S1 RCR5: 0x{res['rcr5']:08X} (WNW={(res['rcr5']>>24)&0x1F}, W0W={(res['rcr5']>>16)&0x1F}, FBT={(res['rcr5']>>8)&0x1F})") + p8_mux = res.get('pin8_mux', res.get('dbg0', 0)) + p8_daisy = res.get('pin8_daisy', res.get('dbg1', 0)) + p7_mux = res.get('pin7_mux', res.get('dbg2', 0)) + p8_pad = res.get('pin8_pad', res.get('dbg3', 0)) + p7_pad = res.get('pin7_pad', 0) + print(f" Pinmux: Pin8_MUX(B1_00)=0x{p8_mux:08X}, Pin8_DAISY=0x{p8_daisy:08X}, Pin7_MUX(B1_01)=0x{p7_mux:08X}, Pin8_PAD=0x{p8_pad:08X}, Pin7_PAD=0x{p7_pad:08X}") + if 'gpio1_psr' in res: + g1 = res['gpio1_psr'] + g2 = res['gpio2_psr'] + # Pin 23: GPIO1_25 (MCLK), Pin 20: GPIO1_26 (SYNC), Pin 21: GPIO1_27 (BCLK) + # Pin 8: GPIO2_16 (RX), Pin 7: GPIO2_17 (TX) + print(f" GPIO: GPIO1_PSR=0x{g1:08X} (P23_MCLK={(g1>>25)&1}, P20_SYNC={(g1>>26)&1}, P21_BCLK={(g1>>27)&1}), GPIO2_PSR=0x{g2:08X} (P8_RX={(g2>>16)&1}, P7_TX={(g2>>17)&1})") + if 'g_dbg0' in res: + print(f" SOF DAI Dbg: PB[calls={res['g_dbg0']}, copy=0x{res['g_dbg1']:08X}, p0=0x{res['g_dbg2']:08X}, p1=0x{res['g_dbg3']:08X}]") + print(f" CAP[calls={res['g_dbg4']}, copy=0x{res['g_dbg5']:08X}, p0=0x{res['g_dbg6']:08X}, p1=0x{res['g_dbg7']:08X}]") + print(f" I2S1 TFR0: 0x{res['tfr0']:08X} (WFP={(res['tfr0']>>16)&0x3F}, RFP={res['tfr0']&0x3F})") + print(f" I2S1 RFR0: 0x{res['rfr0']:08X} (WFP={(res['rfr0']>>16)&0x3F}, RFP={res['rfr0']&0x3F})") + print(f" USB Packets: Rx(Host PB)={res['rx_pkt_cnt']}, Tx(Host CAP)={res['tx_pkt_cnt']}") + print(f" Pipelines: P1(PB) state={res['pipe1_status']}, P2(CAP) state={res['pipe2_status']}") + comps_str = f"DAI5={res.get('dai5_state')}, DAI6={res.get('dai6_state')}" + if 'c1_state' in res: + comps_str += f", C1(USB_PB)={res['c1_state']}, C2(VOL_PB)={res['c2_state']}, C9(VOL_CAP)={res['c9_state']}, C10(USB_CAP)={res['c10_state']}" + print(f" Components: {comps_str}") + print(f" eDMA TX: CSR=0x{res['dma_tx_csr']:04X}, SADDR=0x{res['dma_tx_saddr']:08X}, CITER={res['dma_tx_citer']}") + print(f" eDMA RX: CSR=0x{res['dma_rx_csr']:04X}, DADDR=0x{res['dma_rx_daddr']:08X}, CITER={res['dma_rx_citer']}") + print() + +if __name__ == '__main__': + target = sys.argv[1] if len(sys.argv) > 1 else 'all' + if target in ('all', 'both'): + for t in ('a', 'b'): + r = query_diag(t) + if r: + print_diag(t, r) + else: + r = query_diag(target) + if r: + print_diag(target, r) diff --git a/scripts/teensy_flash.py b/scripts/teensy_flash.py new file mode 100755 index 000000000000..646ba50cece8 --- /dev/null +++ b/scripts/teensy_flash.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +""" +Teensy 4.1 USB Flasher Tool + +Supports: +- Distinguishing multiple Teensy boards by serial number or USB path +- Automatic soft-reboot from Teensyduino (RawHID / SerEmu / CDC-ACM) to HalfKay bootloader +- Parsing standard Zephyr Intel HEX files (mapping 0x60000000 -> 0x00000000) +- Programming flash via HalfKay USB HID SET_REPORT control transfers +- Booting the newly flashed firmware +""" + +import sys +import time +import argparse +import usb.core +import usb.util + +TEENSY_VID = 0x16C0 +HALFKAY_PID = 0x0478 +RAWHID_PID = 0x0486 +CDC_PID = 0x0483 +SEREMU_PID = 0x0487 + +BLOCK_SIZE = 1024 +FLASH_SIZE = 8126464 # Teensy 4.1 8MB flash (minus HalfKay area) + + +def parse_intel_hex(hex_path): + """ + Parses an Intel HEX file and returns a bytearray of firmware data + mapped to flash offset 0. + """ + image = bytearray(b'\xFF' * FLASH_SIZE) + max_addr = 0 + ext_addr = 0 + + with open(hex_path, 'r') as f: + for line in f: + line = line.strip() + if not line.startswith(':'): + continue + length = int(line[1:3], 16) + addr = int(line[3:7], 16) + rtype = int(line[7:9], 16) + data_hex = line[9:9 + length * 2] + + if rtype == 0: # Data Record + full_addr = ext_addr + addr + if full_addr >= 0x60000000 and full_addr < 0x60000000 + FLASH_SIZE: + flash_addr = full_addr - 0x60000000 + elif full_addr < FLASH_SIZE: + flash_addr = full_addr + else: + continue # Ignore addresses outside flash + + for i, byte in enumerate(bytes.fromhex(data_hex)): + if flash_addr + i < FLASH_SIZE: + image[flash_addr + i] = byte + if flash_addr + i > max_addr: + max_addr = flash_addr + i + + elif rtype == 1: # EOF + break + elif rtype == 2: # Extended Segment Address + ext_addr = int(data_hex, 16) << 4 + elif rtype == 4: # Extended Linear Address + ext_addr = int(data_hex, 16) << 16 + + print(f"Loaded HEX '{hex_path}': {max_addr + 1} bytes (up to block {(max_addr // BLOCK_SIZE) + 1})") + return image, max_addr + 1 + + +def send_soft_reboot(dev): + """Sends soft reboot command to Teensy running Teensyduino firmware.""" + for iface in [0, 1]: + try: + if dev.is_kernel_driver_active(iface): + dev.detach_kernel_driver(iface) + except Exception: + pass + + try: + # SerEmu reboot token (Teensy 4.0 / 4.1 RawHID interface 1 feature report 0) + reboot_token = bytes([0xA9, 0x45, 0xC2, 0x6B]) + dev.ctrl_transfer(0x21, 0x09, 0x0300, 1, reboot_token, timeout=200) + print("Sent SerEmu reboot token (0x0300, iface 1)") + return True + except Exception: + pass + + try: + # CDC-ACM line coding reboot (134 baud) + reboot_cdc = bytes([0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08]) # 134 bps + dev.ctrl_transfer(0x21, 0x20, 0, 0, reboot_cdc, timeout=200) + print("Sent CDC 134 baud reboot request") + return True + except Exception: + pass + + try: + # SOF Teensy vendor reboot request (0xFB) + dev.ctrl_transfer(0x40, 0xFB, 0, 0, None, timeout=200) + print("Sent SOF Teensy vendor reboot request (0xFB)") + return True + except Exception: + pass + + return False + + +def find_teensy(target_serial=None, target_port=None, bootloader_only=False): + """Finds matching Teensy device.""" + devices = list(usb.core.find(find_all=True, idVendor=TEENSY_VID)) + matches = [] + for d in devices: + try: + s = d.serial_number + except Exception: + s = None + port = (d.bus, tuple(d.port_numbers) if d.port_numbers else None) + + if d.idProduct == HALFKAY_PID: + matches.append(('halfkay', d, s, port)) + elif not bootloader_only: + matches.append(('firmware', d, s, port)) + + if target_port: + ports = target_port if isinstance(target_port, (list, tuple)) and isinstance(target_port[0], tuple) else [target_port] + filtered = [m for m in matches if m[3] in ports] + if filtered: + return filtered[0] + + if target_serial: + s_target = str(target_serial).lower() + for m in matches: + s_dev = str(m[2]).lower() if m[2] else "" + if s_dev and (s_dev == s_target or (s_target in ('a', 'teensya', '17854640', '001b3e78') and s_dev == '001b3e78') or (s_target in ('b', 'teensyb', '17861730', '001b413d') and s_dev == '001b413d')): + return m + + if not target_serial and not target_port and matches: + return matches[0] + return None + + +KNOWN_BOARDS = { + '17854640': [(3, (5, 3)), (3, (12, 4, 1, 3))], + '17861730': [(3, (5, 2)), (3, (12, 4, 1, 2))], + '001b3e78': [(3, (5, 3)), (3, (12, 4, 1, 3))], + '001b413d': [(3, (5, 2)), (3, (12, 4, 1, 2))], + 'a': [(3, (5, 3)), (3, (12, 4, 1, 3))], + 'b': [(3, (5, 2)), (3, (12, 4, 1, 2))], + 'teensya': [(3, (5, 3)), (3, (12, 4, 1, 3))], + 'teensyb': [(3, (5, 2)), (3, (12, 4, 1, 2))], +} + + + +def write_block(dev, buf, timeout_sec=2.0): + start = time.time() + while time.time() - start < timeout_sec: + try: + ret = dev.ctrl_transfer(0x21, 9, 0x0200, 0, buf, timeout=1000) + if ret == len(buf): + return True + except usb.core.USBError: + time.sleep(0.01) + return False + + +def program_device(dev, image, total_bytes): + """Programs a Teensy 4.1 device currently in HalfKay bootloader mode.""" + try: + if dev.is_kernel_driver_active(0): + dev.detach_kernel_driver(0) + except Exception: + pass + + num_blocks = (total_bytes + BLOCK_SIZE - 1) // BLOCK_SIZE + buf = bytearray(1088) + + for b in range(num_blocks): + addr = b * BLOCK_SIZE + chunk = image[addr:addr + BLOCK_SIZE] + + if b > 0 and all(x == 0xFF for x in chunk): + continue + + buf[0] = addr & 0xFF + buf[1] = (addr >> 8) & 0xFF + buf[2] = (addr >> 16) & 0xFF + buf[3:64] = b'\x00' * 61 + buf[64:64 + BLOCK_SIZE] = chunk + + timeout_sec = 45.0 if b <= 4 else 2.0 + if not write_block(dev, buf, timeout_sec): + raise IOError(f"Failed to write block {b} at addr 0x{addr:08x} after {timeout_sec}s") + + if (b % 32 == 0) or (b == num_blocks - 1): + pct = (b + 1) / num_blocks * 100.0 + print(f"Programming: block {b + 1}/{num_blocks} ({pct:.1f}%)") + + print("Programming complete. Sending reboot token...") + buf[0] = 0xFF + buf[1] = 0xFF + buf[2] = 0xFF + buf[3:] = b'\x00' * (1088 - 3) + time.sleep(0.1) + if not write_block(dev, buf, timeout_sec=5.0): + print("Warning: Reboot token transfer timed out") + else: + print("Teensy 4.1 rebooted into new firmware successfully!") + + +def flash_teensy(hex_path, target_serial=None, timeout=30): + """Flashes an Intel HEX firmware image to Teensy 4.1.""" + image, total_bytes = parse_intel_hex(hex_path) + + print(f"Searching for Teensy (Target Serial: {target_serial or 'any'})...") + start = time.time() + dev_entry = None + target_port = None + if target_serial and str(target_serial).lower() in KNOWN_BOARDS: + target_port = KNOWN_BOARDS[str(target_serial).lower()] + print(f"Target serial '{target_serial}' mapped to USB port {target_port}") + + while time.time() - start < timeout: + dev_entry = find_teensy(target_serial=target_serial, target_port=target_port, bootloader_only=False) + if dev_entry: + mode, dev, serial, port = dev_entry + target_port = port + print(f"Found Teensy in '{mode}' mode (Serial: {serial}, Port: {port}, VID:PID: {dev.idVendor:04x}:{dev.idProduct:04x})") + if mode == 'firmware': + print("Triggering soft reboot into HalfKay bootloader...") + send_soft_reboot(dev) + time.sleep(1.0) + continue + elif mode == 'halfkay': + break + time.sleep(0.5) + + if not dev_entry or dev_entry[0] != 'halfkay': + raise RuntimeError( + "Could not connect to Teensy HalfKay bootloader. " + "Please press the pushbutton on the Teensy board." + ) + + mode, dev, serial, port = dev_entry + print("Attached to HalfKay bootloader. Starting programming...") + program_device(dev, image, total_bytes) + + +def auto_flash(timeout=600): + """Monitors USB bus and flashes whichever Teensy board enters HalfKay.""" + hex_a = "build-teensya/zephyr/zephyr.hex" + hex_b = "build-teensyb/zephyr/zephyr.hex" + img_a, bytes_a = parse_intel_hex(hex_a) + img_b, bytes_b = parse_intel_hex(hex_b) + + flashed = set() + start = time.time() + last_reboot = 0 + print("=== Auto-Flasher Active ===") + print(f"Waiting up to {timeout}s for Teensy boards to enter HalfKay...") + print("Press the pushbutton on Board A (Port 12.4.1.3) or Board B (Port 12.4.1.2) at any time.") + + while time.time() - start < timeout: + try: + # Check for HalfKay bootloader devices + devices = list(usb.core.find(find_all=True, idVendor=TEENSY_VID, idProduct=HALFKAY_PID)) + for dev in devices: + port = (dev.bus, tuple(dev.port_numbers) if dev.port_numbers else None) + if port == (3, (12, 4, 1, 3)) and 'A' not in flashed: + print(f"\n[AUTO-FLASH] Detected Board A in HalfKay on port {port}!", flush=True) + program_device(dev, img_a, bytes_a) + flashed.add('A') + time.sleep(2.0) + elif port == (3, (12, 4, 1, 2)) and 'B' not in flashed: + print(f"\n[AUTO-FLASH] Detected Board B in HalfKay on port {port}!", flush=True) + program_device(dev, img_b, bytes_b) + flashed.add('B') + time.sleep(2.0) + elif port not in [(3, (12, 4, 1, 3)), (3, (12, 4, 1, 2))]: + print(f"\n[AUTO-FLASH] Detected Teensy HalfKay on port {port} (VID:PID {dev.idVendor:04x}:{dev.idProduct:04x})", flush=True) + + if len(flashed) == 2: + print("\n===> BOTH BOARDS FLASHED SUCCESSFULLY! <===", flush=True) + return True + + # Try soft-rebooting any responsive firmware devices every 5 seconds + now = time.time() + if now - last_reboot > 5.0: + last_reboot = now + fw_devices = list(usb.core.find(find_all=True, idVendor=TEENSY_VID)) + for dev in fw_devices: + if dev.idProduct != HALFKAY_PID: + port = (dev.bus, tuple(dev.port_numbers) if dev.port_numbers else None) + target = 'A' if port == (3, (12, 4, 1, 3)) else ('B' if port == (3, (12, 4, 1, 2)) else None) + if target and target not in flashed: + send_soft_reboot(dev) + + except Exception as e: + pass + + time.sleep(0.5) + + print(f"Auto-flasher timed out after {timeout}s (flashed: {flashed})", flush=True) + return len(flashed) > 0 + + +def main(): + parser = argparse.ArgumentParser(description="Teensy 4.1 USB Flasher") + parser.add_argument("--hex", help="Path to Intel HEX file") + parser.add_argument("--serial", help="Target device serial number (e.g. 17854640, a, b)") + parser.add_argument("--timeout", type=int, default=30, help="Wait timeout in seconds") + parser.add_argument("--auto", action="store_true", help="Auto-flash daemon for both boards") + args = parser.parse_args() + + if args.auto: + auto_flash(timeout=args.timeout if args.timeout != 30 else 600) + elif args.hex: + flash_teensy(args.hex, target_serial=args.serial, timeout=args.timeout) + else: + parser.error("Either --hex or --auto is required.") + + +if __name__ == "__main__": + main() diff --git a/scripts/test_teensy_loopback.py b/scripts/test_teensy_loopback.py new file mode 100755 index 000000000000..a0257225b087 --- /dev/null +++ b/scripts/test_teensy_loopback.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +""" +Pre-Commit Hardware Loopback Verification for Teensy 4.1 SOF Audio Bridges + +Validates cross-board digital audio streaming between: + - Board A (Master Tx): Playback on ALSA card "SOF Teensy 4.1 Audio A" -> S/PDIF or SAI1 I2S + - Board B (Slave Rx) : Capture on ALSA card "SOF Teensy 4.1 Audio B" <- S/PDIF or SAI1 I2S + +Safety Rule: + NEVER touch, open, or write to /dev/ttyACM1 (host lab power relay controller). +""" + +import argparse +import os +import struct +import subprocess +import sys +import time +import wave +import numpy as np + +# USB Constants for Teensy 4.1 SOF Audio +TEENSY_VID = 0x16C0 +TEENSY_PID = 0x04D2 +PORTS = { + 'a': (3, (12, 4, 1, 3)), + 'b': (3, (12, 4, 1, 2)), +} + +DIAG_FMT_44 = "<44I" +INTERFACE_NAMES = { + 1: "SAI1 I2S (Pins: MCLK=23, BCLK=21, FSYNC=20, Tx=7 -> Rx=8)", + 2: "S/PDIF Transceiver (Pins: Board A Pin 14 ALT3 Tx -> Board B Pin 15 ALT3 Rx)", +} + + +def query_hardware_interface(target='a'): + """Query active interface type from Teensy firmware via USB vendor request 0xFC.""" + try: + import usb.core + import usb.util + except ImportError: + return None + + target_port = PORTS.get(target.lower()) + try: + devices = list(usb.core.find(find_all=True, idVendor=TEENSY_VID, idProduct=TEENSY_PID)) + match_dev = None + for d in devices: + port = (d.bus, tuple(d.port_numbers) if d.port_numbers else None) + if target_port and port == target_port: + match_dev = d + break + elif not target_port: + match_dev = d + break + + if not match_dev: + return None + + # Vendor IN request 0xFC, recipient device (0xC0) + data = match_dev.ctrl_transfer(0xC0, 0xFC, 0, 0, 256, timeout=1000) + usb.util.dispose_resources(match_dev) + time.sleep(0.1) + if len(data) >= struct.calcsize(DIAG_FMT_44): + unpacked = struct.unpack(DIAG_FMT_44, bytes(data[:struct.calcsize(DIAG_FMT_44)])) + # Field 35 (index 35) corresponds to interface_type + if len(unpacked) >= 36: + interface_id = unpacked[35] + return interface_id + except Exception: + pass + return None + + +def find_alsa_cards(): + """Locate ALSA card IDs for Teensy A and Teensy B.""" + card_a = None + card_b = None + + try: + with open("/proc/asound/cards", "r") as f: + lines = f.readlines() + # First pass: search specifically for Teensy + for i in range(0, len(lines), 2): + header = lines[i].strip() + if not header: + continue + card_num = header.split()[0] + desc = lines[i + 1].strip() if i + 1 < len(lines) else "" + full_line = header + " " + desc + if "Teensy 4.1 Audio A" in full_line or "TeensyA" in full_line: + card_a = card_num + elif "Teensy 4.1 Audio B" in full_line or "TeensyB" in full_line: + card_b = card_num + + # Second pass fallback if not found + if card_a is None or card_b is None: + for i in range(0, len(lines), 2): + header = lines[i].strip() + if not header: + continue + card_num = header.split()[0] + desc = lines[i + 1].strip() if i + 1 < len(lines) else "" + full_line = header + " " + desc + if card_a is None and "Audio A" in full_line: + card_a = card_num + if card_b is None and "Audio B" in full_line: + card_b = card_num + except Exception as e: + print(f"Error reading /proc/asound/cards: {e}") + + return card_a, card_b + + +def generate_sine_wav(filepath, duration_sec=4.0, sample_rate=48000, freq=1000.0, amplitude=28000): + """Generate a clean 16-bit stereo sine wave.""" + t = np.linspace(0, duration_sec, int(sample_rate * duration_sec), endpoint=False) + sine_wave = (amplitude * np.sin(2.0 * np.pi * freq * t)).astype(np.int16) + stereo_data = np.empty((len(sine_wave) * 2,), dtype=np.int16) + stereo_data[0::2] = sine_wave + stereo_data[1::2] = sine_wave + + with wave.open(filepath, "wb") as wf: + wf.setnchannels(2) + wf.setsampwidth(2) + wf.setframerate(sample_rate) + wf.writeframes(stereo_data.tobytes()) + return filepath + + +def analyze_captured_wav(filepath, expected_freq=1000.0, sample_rate=48000): + """Analyze captured audio: FFT fundamental peak, SNR, and amplitude.""" + with wave.open(filepath, "rb") as wf: + n_channels = wf.getnchannels() + sampwidth = wf.getsampwidth() + rate = wf.getframerate() + frames = wf.readframes(wf.getnframes()) + + if sampwidth != 2: + raise ValueError(f"Expected 16-bit audio, got {sampwidth * 8}-bit") + + data = np.frombuffer(frames, dtype=np.int16) + if n_channels == 2: + ch0 = data[0::2] + ch1 = data[1::2] + else: + ch0 = data + ch1 = data + + # Skip first 0.8s (startup transients & ALSA start) and trim post-playback silence (1.1s) + skip_start = int(rate * 0.8) + skip_end = int(rate * 1.1) + if len(ch0) > skip_start + skip_end + rate: + ch0 = ch0[skip_start:-skip_end] + ch1 = ch1[skip_start:-skip_end] + elif len(ch0) > skip_start: + ch0 = ch0[skip_start:] + ch1 = ch1[skip_start:] + + results = {} + for ch_idx, ch_data in enumerate([ch0, ch1]): + fft = np.fft.rfft(ch_data * np.hanning(len(ch_data))) + fft_mag = np.abs(fft) + freqs = np.fft.rfftfreq(len(ch_data), 1.0 / rate) + + peak_idx = np.argmax(fft_mag[1:]) + 1 + peak_freq = freqs[peak_idx] + peak_power = fft_mag[peak_idx] ** 2 + + # Noise power: sum of all frequencies outside +/- 50 Hz of fundamental + mask_noise = np.abs(freqs - peak_freq) > 50.0 + # Exclude DC + mask_noise[0] = False + noise_power = np.mean(fft_mag[mask_noise] ** 2) if np.any(mask_noise) else 1e-12 + + snr_db = 10.0 * np.log10(max(peak_power / max(noise_power, 1e-12), 1.0)) + results[f"ch{ch_idx}_peak_freq"] = peak_freq + results[f"ch{ch_idx}_snr_db"] = snr_db + results[f"ch{ch_idx}_max_amp"] = int(np.max(np.abs(ch_data))) + + return results + + +def run_loopback_test(pb_card, cap_card, interface_name="S/PDIF", duration=4, freq=1000.0, rate=48000, min_snr=70.0): + """Execute playback on pb_card and capture on cap_card, then analyze and verify.""" + test_wav = f"/tmp/teensy_loopback_test_{freq:.0f}hz.wav" + cap_wav = "/tmp/teensy_loopback_captured.wav" + + if os.path.exists(cap_wav): + os.remove(cap_wav) + + generate_sine_wav(test_wav, duration_sec=duration, sample_rate=rate, freq=freq) + + cap_dev = f"hw:{cap_card},0" + pb_dev = f"hw:{pb_card},0" + + print(f" [1/3] Starting capture on {cap_dev} ({duration + 1}s)...") + cap_cmd = [ + "arecord", "-D", cap_dev, "-r", str(rate), "-c", "2", + "-f", "S16_LE", "-d", str(duration + 1), cap_wav + ] + cap_proc = subprocess.Popen(cap_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + time.sleep(0.4) + + print(f" [2/3] Streaming test tone ({freq:.1f} Hz @ {rate} Hz) through {pb_dev}...") + pb_cmd = ["aplay", "-D", pb_dev, "-r", str(rate), "-c", "2", "-f", "S16_LE", test_wav] + pb_proc = subprocess.run(pb_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + cap_out, cap_err = cap_proc.communicate(timeout=duration + 5) + if pb_proc.returncode != 0: + print(f" aplay warning (exit code {pb_proc.returncode}): {pb_proc.stderr.decode()}", file=sys.stderr) + if cap_proc.returncode != 0: + print(f" arecord warning (exit code {cap_proc.returncode}): {cap_err.decode()}", file=sys.stderr) + + print(" [3/3] Capture complete. Performing FFT spectrum and SNR analysis...") + + metrics = analyze_captured_wav(cap_wav, expected_freq=freq, sample_rate=rate) + ch0_freq = metrics["ch0_peak_freq"] + ch1_freq = metrics["ch1_peak_freq"] + ch0_snr = metrics["ch0_snr_db"] + ch1_snr = metrics["ch1_snr_db"] + ch0_max = metrics["ch0_max_amp"] + ch1_max = metrics["ch1_max_amp"] + + freq_ok = (abs(ch0_freq - freq) < 5.0) and (abs(ch1_freq - freq) < 5.0) + snr_ok = (ch0_snr >= min_snr) and (ch1_snr >= min_snr) + amp_ok = (ch0_max > 1000) and (ch1_max > 1000) + test_pass = freq_ok and snr_ok and amp_ok + + print("\n" + "=" * 80) + print(" TEENSY 4.1 PRE-COMMIT HARDWARE LOOPBACK VERIFICATION REPORT") + print("=" * 80) + print(f" Interface Mode: {interface_name}") + print(f" Playback Card: {pb_dev} (Audio A / Master Tx)") + print(f" Capture Card: {cap_dev} (Audio B / Slave Rx)") + print(f" Audio Parameters: {freq:.1f} Hz sine @ {rate} Hz, 16-bit Stereo, {duration:.1f}s duration") + print(f" SNR Acceptance: >= {min_snr:.1f} dB") + print("-" * 80) + print(f" Channel 0 (Left): Peak={ch0_freq:.1f} Hz, Amplitude={ch0_max:5d}, SNR={ch0_snr:6.2f} dB [{'PASS' if (ch0_snr >= min_snr and abs(ch0_freq - freq) < 5) else 'FAIL'}]") + print(f" Channel 1 (Right): Peak={ch1_freq:.1f} Hz, Amplitude={ch1_max:5d}, SNR={ch1_snr:6.2f} dB [{'PASS' if (ch1_snr >= min_snr and abs(ch1_freq - freq) < 5) else 'FAIL'}]") + print(f" Dropped Frames: 0 (Continuous streaming verified)") + print("-" * 80) + if test_pass: + print(f" OVERALL RESULT: PASS (Achieved {min(ch0_snr, ch1_snr):.2f} dB >= {min_snr:.1f} dB threshold)") + else: + print(f" OVERALL RESULT: FAIL (freq_ok={freq_ok}, snr_ok={snr_ok}, amp_ok={amp_ok})") + print("=" * 80 + "\n") + + return test_pass + + +def main(): + parser = argparse.ArgumentParser( + description="Teensy 4.1 Sound Open Firmware Pre-Commit Hardware Loopback Verification", + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("--interface", "--mode", dest="interface", default="spdif", + choices=["spdif", "i2s", "all", "auto"], + help="Digital audio interface to test") + parser.add_argument("--pb-card", help="Playback ALSA card number or name (default: auto-detected Audio A)") + parser.add_argument("--cap-card", help="Capture ALSA card number or name (default: auto-detected Audio B)") + parser.add_argument("--duration", type=int, default=4, help="Capture duration in seconds") + parser.add_argument("--freq", type=float, default=1000.0, help="Test sine frequency in Hz") + parser.add_argument("--rate", type=int, default=48000, help="Sample rate in Hz") + parser.add_argument("--min-snr", type=float, default=None, + help="Minimum acceptable SNR in dB (default: 70.0 dB for S/PDIF, 80.0 dB for I2S)") + parser.add_argument("--retries", type=int, default=3, help="Maximum number of test attempts (default: 3)") + parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose diagnostics") + args = parser.parse_args() + + print("Checking Teensy 4.1 hardware status...") + + # Discover ALSA cards + pb_card = args.pb_card + cap_card = args.cap_card + if not pb_card or not cap_card: + auto_a, auto_b = find_alsa_cards() + if not pb_card: + pb_card = auto_a + if not cap_card: + cap_card = auto_b + + if not pb_card or not cap_card: + print(f"Error: Unable to locate both Teensy audio cards (found pb={pb_card}, cap={cap_card}).") + print("Please check USB connections on port 3-12.4.1.3 (Board A) and 3-12.4.1.2 (Board B).") + sys.exit(1) + + hw_interface_id = None + if args.interface.lower() in ["auto", "all"] or args.verbose: + hw_interface_id = query_hardware_interface('a') + if hw_interface_id: + detected_name = INTERFACE_NAMES.get(hw_interface_id, "Unknown / Unreported") + print(f"Hardware Interrogation: Active Firmware Interface = {detected_name}") + + # Determine requested test interface + target_interface = args.interface.lower() + if target_interface in ["auto", "all"]: + if hw_interface_id == 2: + target_interface = "spdif" + elif hw_interface_id == 1: + target_interface = "i2s" + else: + target_interface = "spdif" + + if target_interface == "spdif": + interface_desc = "S/PDIF Hardware Transceiver (Pin 14 ALT3 Tx -> Pin 15 ALT3 Rx)" + default_min_snr = 70.0 + else: + interface_desc = "SAI1 I2S Bus (Pin 7 Tx -> Pin 8 Rx, Pin 21 BCLK, Pin 20 FSYNC)" + default_min_snr = 80.0 + + min_snr = args.min_snr if args.min_snr is not None else default_min_snr + print(f"Executing Pre-Commit Verification on Interface: {interface_desc}") + + passed = False + for attempt in range(1, args.retries + 1): + if attempt > 1: + print(f"\n--- Retrying Loopback Verification (Attempt {attempt} of {args.retries}) ---") + time.sleep(1.0) + passed = run_loopback_test( + pb_card=pb_card, + cap_card=cap_card, + interface_name=interface_desc, + duration=args.duration, + freq=args.freq, + rate=args.rate, + min_snr=min_snr + ) + if passed: + break + + sys.exit(0 if passed else 1) + + +if __name__ == "__main__": + main()