diff --git a/tools/topology/topology2/CMakeLists.txt b/tools/topology/topology2/CMakeLists.txt index 2a1ee5483e7e..de152f1526d2 100644 --- a/tools/topology/topology2/CMakeLists.txt +++ b/tools/topology/topology2/CMakeLists.txt @@ -7,9 +7,11 @@ add_custom_target(topologies2 ALL # Create directory structure to be provided for deployment COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/target/sof-ipc4-tplg COMMAND ${CMAKE_COMMAND} -E create_symlink sof-ipc4-tplg ${CMAKE_CURRENT_BINARY_DIR}/target/sof-ace-tplg + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/target/sof-tplg COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/target/development # copy the topology files only to target COMMAND ${CMAKE_COMMAND} -E copy_if_different production/*.tplg ${CMAKE_CURRENT_BINARY_DIR}/target/sof-ipc4-tplg/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different production/ipc3/*.tplg ${CMAKE_CURRENT_BINARY_DIR}/target/sof-tplg/ COMMAND ${CMAKE_COMMAND} -E copy_if_different development/*.tplg ${CMAKE_CURRENT_BINARY_DIR}/target/development/ ) @@ -37,6 +39,18 @@ add_custom_target(abi_target DEPENDS abi.conf ) +# Atom platforms use IPC3 even when the source topology is written using the +# topology2 object model. Keep a separate ABI manifest so IPC3 and IPC4 output +# can be built together without changing the existing production topologies. +add_custom_command(OUTPUT abi-ipc3.conf + COMMAND > abi-ipc3.conf ${CMAKE_CURRENT_SOURCE_DIR}/get_abi.sh + ${SOF_ROOT_SOURCE_DIRECTORY} "ipc3" + DEPENDS ${SOF_ROOT_SOURCE_DIRECTORY}/src/include/kernel/abi.h +) +add_custom_target(abi_ipc3_target + DEPENDS abi-ipc3.conf +) + add_dependencies(topologies2 topology2_dev topology2_prod) add_subdirectory(development) add_subdirectory(production) diff --git a/tools/topology/topology2/README.md b/tools/topology/topology2/README.md index 759867a47d1c..6d5c99e0eff8 100644 --- a/tools/topology/topology2/README.md +++ b/tools/topology/topology2/README.md @@ -371,6 +371,7 @@ variables such as `DMIC_DRIVER_VERSION`, `SSP_BLOB_VERSION`, and `NUM_HDMIS`. Supported platforms: +* `cht` — Intel Cherry Trail / Atom (IPC3; currently Yoga Book RT5677 only) * `tgl` — Intel Tiger Lake / Alder Lake (CAVS 2.5) * `mtl` — Intel Meteor Lake (ACE 1.x) * `lnl` — Intel Lunar Lake (ACE 2.x) @@ -397,10 +398,32 @@ Select the cmake file matching the target platform generation: | Platform | CMake Target File | |---|---| +| Cherry Trail IPC3 | `tplg-targets-ipc3.cmake` | | Tiger Lake / Alder Lake | `tplg-targets-cavs25.cmake` | | Meteor Lake | `tplg-targets-ace1.cmake` | | Lunar Lake | `tplg-targets-ace2.cmake` | | Panther Lake | `tplg-targets-ace3.cmake` | | HDA generic | `tplg-targets-hda-generic.cmake` | +### Cherry Trail IPC3 + +Topology2 describes the source format and does not require the target firmware +to use IPC4. Cherry Trail firmware uses IPC3, so its targets are built with a +separate IPC3 ABI manifest and installed under `target/sof-tplg` instead of the +IPC4/ACE topology directory. + +The Yoga Book topology can be built directly with: + +```bash +cmake --build . --target topology2_prod_ipc3_sof-cht-rt5677 +python3 ../tools/topology/topology2/verify-cht-rt5677.py \ + topology/topology2/production/ipc3/sof-cht-rt5677.tplg +``` + +`sof-cht-rt5677.tplg` configures SSP2 for DSP_B with an inverted bit clock and +non-inverted frame clock, 48 kHz stereo S24_LE, four 25-bit TDM slots, a +4.8 MHz bit clock, and a 19.2 MHz codec MCLK. These values must stay aligned +with the Linux `cht-yogabook` machine driver. The verifier parses the compiled +artifact and rejects changes to the SSP link or the PCM0/PCM1 stereo contract. + Development and testing topologies go in `development/tplg-targets.cmake`. diff --git a/tools/topology/topology2/cht-rt5677.conf b/tools/topology/topology2/cht-rt5677.conf new file mode 100644 index 000000000000..96c18861a78b --- /dev/null +++ b/tools/topology/topology2/cht-rt5677.conf @@ -0,0 +1,331 @@ +# SPDX-License-Identifier: BSD-3-Clause + + + + + + + + + + + + + + + + + + + + + + + + +Define { + # Cherry Trail uses the first-generation SSP configuration blob and IPC3. + SSP_BLOB_VERSION $SSP_BLOB_VERSION_1_0 +} + +# The machine driver connects the RT5677 AIF routes through these platform +# widgets. They mirror the widgets exposed by the legacy Cherry Trail topology. +Object.Widget.virtual [ + { name "codec_out0" type "output" index 1 } + { name "codec_out1" type "output" index 2 } + { name "codec_in0" type "input" index 3 } + { name "codec_in1" type "input" index 4 } + { name "ssp2 Rx" type "input" index 5 } + { name "ssp2 Tx" type "output" index 6 } +] + +# SSP2 carries the RT5677 AIF1 stream. The bit and frame clocks are driven by +# the Cherry Trail SSP; the codec consumes both clocks. +Object.Dai.SSP [ + { + id 0 + dai_index 2 + direction "duplex" + name "SSP2-Codec" + default_hw_config_id 0 + sample_bits 24 + + Object.Base.hw_config.1 { + name "SSP2" + id 0 + format "DSP_B" + mclk "codec_mclk_in" + mclk_freq 19200000 + bclk "codec_consumer" + bclk_freq 4800000 + # Match SND_SOC_DAIFMT_IB_NF in the machine driver: + # invert BCLK while leaving the frame clock non-inverted. + bclk_invert "true" + fsync "codec_consumer" + fsync_freq 48000 + fsync_invert "false" + tdm_slots 4 + tdm_slot_width 25 + tx_slots 3 + rx_slots 3 + } + } +] + +Object.Pipeline { + ipc3-mixer-volume-dai-playback.1 { + index 1 + format "s24le" + channels 2 + dai_type "SSP" + dai_index 2 + periods 2 + # Two 1 ms periods at 48 kHz, stereo, four bytes per sample. + size 768 + + Object.Widget.pipeline.1 { + stream_name "dai.SSP.2.playback" + priority 1 + core 0 + frames 0 + mips 5000 + } + Object.Widget.dai.playback { + format "s24le" + stream_name "SSP2-Codec" + core_id 0 + } + Object.Widget.pga.1 { + ramp_step_ms 20 + Object.Control.mixer.1 { + name "1 Master Playback Volume" + Object.Base.tlv.vtlv_m64s2 { + Object.Base.scale.m64s2 {} + } + } + } + Object.Base.route.4 { + source "buffer.1.2" + sink "dai.SSP.2.playback" + } + } + + ipc3-volume-dai-capture.2 { + index 2 + format "s32le" + channels 2 + dai_type "SSP" + dai_index 2 + periods 2 + # Two 1 ms periods at 48 kHz, stereo, four bytes per sample. + size 768 + + Object.Widget.pipeline.1 { + stream_name "dai.SSP.2.capture" + priority 0 + core 0 + frames 0 + mips 5000 + } + Object.Widget.dai.capture { + format "s24le" + stream_name "SSP2-Codec" + core_id 0 + } + Object.Widget.host.capture { + stream_name "Low Latency Capture 0" + core_id 0 + } + Object.Widget.pga.1 { + ramp_step_ms 250 + Object.Control.mixer.1 { + name "2 PCM 0 Capture Volume" + max 40 + Object.Base.channel.1 { + name "fl" + reg 0 + shift 0 + } + Object.Base.channel.2 { + name "fr" + reg 0 + shift 1 + } + Object.Base.tlv.vtlv_m64s2 { + Object.Base.scale.m64s2 {} + } + } + } + Object.Base.route.4 { + source "dai.SSP.2.capture" + sink "buffer.2.1" + } + } + + volume-playback.3 { + index 3 + format "s32le" + channels 2 + period 1000 + time_domain "dma" + + Object.Widget.pipeline.1 { + priority 0 + core 0 + frames 0 + mips 100000 + stream_name "dai.SSP.2.playback" + } + Object.Widget.host.playback { + stream_name "Playback 0" + core_id 0 + period_sink_count 0 + period_source_count 2 + } + Object.Widget.pga.1 { + ramp_step_ms 20 + period_sink_count 2 + period_source_count 2 + Object.Control.mixer.1 { + name "3 Playback Volume" + Object.Base.tlv.vtlv_m64s2 { + Object.Base.scale.m64s2 {} + } + } + } + Object.Widget.buffer.1 { + caps "ram_dma_cache" + # IPC3 needs SOF_TKN_BUF_SIZE serialized explicitly. An object + # value takes precedence over the class's automatic size value. + size 768 + } + Object.Widget.buffer.2 { + caps "ram_dma_cache" + size 768 + } + } + + volume-playback.4 { + index 4 + format "s32le" + channels 2 + period 5000 + time_domain "dma" + + Object.Widget.pipeline.1 { + priority 0 + core 0 + frames 0 + mips 100000 + stream_name "dai.SSP.2.playback" + } + Object.Widget.host.playback { + stream_name "Playback 1" + core_id 0 + period_sink_count 0 + period_source_count 2 + } + Object.Widget.pga.1 { + ramp_step_ms 20 + period_sink_count 2 + period_source_count 2 + Object.Control.mixer.1 { + name "4 Playback Volume" + Object.Base.tlv.vtlv_m64s2 { + Object.Base.scale.m64s2 {} + } + } + } + Object.Widget.buffer.1 { + caps "ram_dma_cache" + # Five milliseconds at 48 kHz, two channels and four bytes per + # sample, with two periods: 5 ms * 48 * 2 * 4 * 2 = 3840. + size 3840 + } + Object.Widget.buffer.2 { + caps "ram_dma_cache" + size 3840 + } + } +} + +# Both playback front ends feed the low-latency SSP2 mixer. +Object.Base.route [ + { source "buffer.3.2" sink "ipc3-mixer.1.1" } + { source "buffer.4.2" sink "ipc3-mixer.1.1" } +] + +Object.PCM.pcm [ + { + name "PCM" + id 0 + direction "duplex" + + Object.Base.fe_dai.1 { + name "PCM 0" + } + Object.PCM.pcm_caps.playback { + direction "playback" + name "Playback 0" + formats "S16_LE,S24_LE,S32_LE" + rates "48000" + rate_min 48000 + rate_max 48000 + channels_min 2 + channels_max 2 + # Match the proven Cherry Trail IPC3 topology instead of the + # much larger generic topology2 PCM capability defaults. + periods_min 2 + periods_max 16 + period_size_min 192 + period_size_max 16384 + buffer_size_min 65536 + buffer_size_max 65536 + } + Object.PCM.pcm_caps.capture { + direction "capture" + name "Low Latency Capture 0" + formats "S16_LE,S24_LE,S32_LE" + rates "48000" + rate_min 48000 + rate_max 48000 + channels_min 2 + channels_max 2 + periods_min 2 + # The low-latency capture path supports fewer queued periods + # than playback in the legacy Cherry Trail topology. + periods_max 4 + period_size_min 192 + period_size_max 16384 + buffer_size_min 65536 + buffer_size_max 65536 + } + } + { + name "PCM Deep Buffer" + id 1 + direction "playback" + + Object.Base.fe_dai.1 { + name "PCM Deep Buffer 1" + } + Object.PCM.pcm_caps.playback { + direction "playback" + name "Playback 1" + formats "S16_LE,S24_LE,S32_LE" + rates "48000" + rate_min 48000 + rate_max 48000 + channels_min 2 + channels_max 2 + # Keep userspace constraints identical for the low-latency and + # deep-buffer playback PCMs, as on the legacy CHT topology. + periods_min 2 + periods_max 16 + period_size_min 192 + period_size_max 16384 + buffer_size_min 65536 + buffer_size_max 65536 + } + } +] diff --git a/tools/topology/topology2/include/components/buffer.conf b/tools/topology/topology2/include/components/buffer.conf index 32dfcebd29f8..66a03a683458 100644 --- a/tools/topology/topology2/include/components/buffer.conf +++ b/tools/topology/topology2/include/components/buffer.conf @@ -60,12 +60,16 @@ Class.Widget."buffer" { "host" "pass" "comp" + # RAM | DMA | CACHE, without the HP capability required by + # newer Intel platforms. + "ram_dma_cache" ] !tuple_values [ 113 113 113 65 + 97 ] } } diff --git a/tools/topology/topology2/include/components/ipc3/mixer.conf b/tools/topology/topology2/include/components/ipc3/mixer.conf new file mode 100644 index 000000000000..e04596960b4d --- /dev/null +++ b/tools/topology/topology2/include/components/ipc3/mixer.conf @@ -0,0 +1,28 @@ +## \struct ipc3_mixer +## \brief IPC3 low-latency mixer widget. + +Class.Widget."ipc3-mixer" { + DefineAttribute."index" {} + DefineAttribute."instance" {} + + + + attributes { + !constructor [ + "index" + "instance" + ] + !mandatory [ + "format" + ] + !immutable [ + "type" + "uuid" + ] + unique "instance" + } + + type "mixer" + uuid "37:c0:06:bc:aa:12:7c:41:9a:97:89:28:2e:32:1a:76" + no_pm "true" +} diff --git a/tools/topology/topology2/include/components/widget-common.conf b/tools/topology/topology2/include/components/widget-common.conf index 14ccd8fcf86a..a132110805fb 100644 --- a/tools/topology/topology2/include/components/widget-common.conf +++ b/tools/topology/topology2/include/components/widget-common.conf @@ -68,6 +68,19 @@ DefineAttribute."core_id" { token_ref "comp.word" } +## Number of periods available at the component's sink. +## +## These IPC3 attributes are optional so IPC4 objects remain unchanged unless +## a topology explicitly sets them. +DefineAttribute."period_sink_count" { + token_ref "comp.word" +} + +## Number of periods available at the component's source. +DefineAttribute."period_source_count" { + token_ref "comp.word" +} + ## number of periods to preload DefineAttribute."preload_count" { # Token set reference name and type diff --git a/tools/topology/topology2/include/pipelines/ipc3/mixer-volume-dai-playback.conf b/tools/topology/topology2/include/pipelines/ipc3/mixer-volume-dai-playback.conf new file mode 100644 index 000000000000..0c4c31cc694a --- /dev/null +++ b/tools/topology/topology2/include/pipelines/ipc3/mixer-volume-dai-playback.conf @@ -0,0 +1,96 @@ +## \struct ipc3_mixer_volume_dai_playback +## \brief IPC3 mixer, volume and DAI playback pipeline. +## +## Multiple host pipelines can connect to the mixer endpoint. The pipeline is +## DMA scheduled by its backend DAI and uses a PGA immediately before that DAI. + + + + + + + + +Class.Pipeline."ipc3-mixer-volume-dai-playback" { + + + # The backend type/index and buffer geometry are inherited by the DAI and + # buffer child objects. The topology connects the final buffer to the + # selected backend endpoint outside this reusable class. + DefineAttribute."dai_type" { + type "string" + } + DefineAttribute."dai_index" {} + DefineAttribute."periods" {} + DefineAttribute."size" {} + + attributes { + !constructor [ + "index" + ] + !mandatory [ + "format" + "dai_type" + "dai_index" + "periods" + "size" + ] + !immutable [ + "direction" + ] + unique "instance" + } + + Object.Widget { + pipeline."1" {} + + ipc3-mixer."1" { + period_sink_count 2 + period_source_count 2 + } + + buffer."1" { + caps "comp" + } + + pga."1" { + period_sink_count 2 + period_source_count 2 + ramp_step_ms 20 + } + + buffer."2" { + caps "comp" + } + + dai."playback" { + type "dai_in" + direction "playback" + period_sink_count 0 + period_source_count 2 + } + } + + Object.Base { + route."1" { + source "ipc3-mixer.$index.1" + sink "buffer.$index.1" + } + route."2" { + source "buffer.$index.1" + sink "pga.$index.1" + } + route."3" { + source "pga.$index.1" + sink "buffer.$index.2" + } + } + + direction "playback" + time_domain "dma" + period 1000 + priority 1 + core 0 + frames 0 + mips 5000 +} diff --git a/tools/topology/topology2/include/pipelines/ipc3/volume-dai-capture.conf b/tools/topology/topology2/include/pipelines/ipc3/volume-dai-capture.conf new file mode 100644 index 000000000000..aa96e70bee38 --- /dev/null +++ b/tools/topology/topology2/include/pipelines/ipc3/volume-dai-capture.conf @@ -0,0 +1,98 @@ +## \struct ipc3_volume_dai_capture +## \brief IPC3 DAI, volume and host capture pipeline. +## +## The backend DAI schedules the complete capture path. Keeping the DAI and host +## widgets in one pipeline avoids creating two scheduler widgets with the same +## pipeline ID, as was possible with the legacy m4 topology macros. + + + + + + + + +Class.Pipeline."ipc3-volume-dai-capture" { + + + # The backend type/index and buffer geometry are inherited by the DAI and + # buffer child objects. The topology connects the selected backend endpoint + # to the first buffer outside this reusable class. + DefineAttribute."dai_type" { + type "string" + } + DefineAttribute."dai_index" {} + DefineAttribute."periods" {} + DefineAttribute."size" {} + + attributes { + !constructor [ + "index" + ] + !mandatory [ + "format" + "dai_type" + "dai_index" + "periods" + "size" + ] + !immutable [ + "direction" + ] + unique "instance" + } + + Object.Widget { + pipeline."1" {} + + dai."capture" { + type "dai_out" + direction "capture" + period_sink_count 2 + period_source_count 0 + } + + buffer."1" { + caps "ram_dma_cache" + } + + pga."1" { + period_sink_count 2 + period_source_count 2 + ramp_step_ms 250 + } + + buffer."2" { + caps "ram_dma_cache" + } + + host."capture" { + type "aif_out" + period_sink_count 2 + period_source_count 0 + } + } + + Object.Base { + route."1" { + source "buffer.$index.1" + sink "pga.$index.1" + } + route."2" { + source "pga.$index.1" + sink "buffer.$index.2" + } + route."3" { + source "buffer.$index.2" + sink "host.$index.capture" + } + } + + direction "capture" + time_domain "dma" + period 1000 + priority 0 + core 0 + frames 0 + mips 5000 +} diff --git a/tools/topology/topology2/include/pipelines/pipeline-common.conf b/tools/topology/topology2/include/pipelines/pipeline-common.conf index 37a34066d4df..260088de45a3 100644 --- a/tools/topology/topology2/include/pipelines/pipeline-common.conf +++ b/tools/topology/topology2/include/pipelines/pipeline-common.conf @@ -46,7 +46,8 @@ DefineAttribute."period" { token_ref "scheduler.word" constraints { min 333 - max 1000 + # The upper bound is platform-specific. IPC3 topologies commonly + # use periods longer than the 1 ms typical of IPC4 pipelines. } } diff --git a/tools/topology/topology2/platform/intel/cht-hw-config.conf b/tools/topology/topology2/platform/intel/cht-hw-config.conf new file mode 100644 index 000000000000..3db7ffb7ccd8 --- /dev/null +++ b/tools/topology/topology2/platform/intel/cht-hw-config.conf @@ -0,0 +1,73 @@ +## \struct hw_config +## \brief Cherry Trail SSP hardware configuration. +## +## Cherry Trail supports SSP clocks outside the cardinal-clock frequencies +## accepted by the modern CAVS hardware configuration class. Device-specific +## clocking and slot geometry belong in the topology object that uses this +## class. + +Class.Base."hw_config" { + DefineAttribute."id" {} + DefineAttribute."instance" {} + + DefineAttribute."name" { + type "string" + } + + DefineAttribute."format" { + type "string" + constraints { + !valid_values [ + "I2S" + "DSP_A" + "DSP_B" + ] + } + } + + DefineAttribute."mclk" { + type "string" + } + DefineAttribute."mclk_freq" {} + DefineAttribute."bclk" { + type "string" + } + DefineAttribute."bclk_freq" {} + DefineAttribute."bclk_invert" { + type "string" + constraints { + !valid_values [ + "true" + "false" + ] + } + } + DefineAttribute."fsync" { + type "string" + } + DefineAttribute."fsync_freq" {} + DefineAttribute."fsync_invert" { + type "string" + constraints { + !valid_values [ + "true" + "false" + ] + } + } + DefineAttribute."tdm_slots" {} + DefineAttribute."tdm_slot_width" {} + DefineAttribute."tx_slots" {} + DefineAttribute."rx_slots" {} + + attributes { + !constructor [ + "id" + ] + !mandatory [ + "name" + ] + unique "instance" + } + +} diff --git a/tools/topology/topology2/production/CMakeLists.txt b/tools/topology/topology2/production/CMakeLists.txt index c2cb86400e4b..06cf2b5a30b6 100644 --- a/tools/topology/topology2/production/CMakeLists.txt +++ b/tools/topology/topology2/production/CMakeLists.txt @@ -8,6 +8,7 @@ include(tplg-targets-ace2.cmake) include(tplg-targets-ace3.cmake) include(tplg-targets-ace4.cmake) include(tplg-targets-imx8.cmake) +include(tplg-targets-ipc3.cmake) add_custom_target(topology2_prod) @@ -32,3 +33,26 @@ foreach(tplg ${TPLGS}) add_custom_target(topology2_prod_${output} DEPENDS ${output}.tplg) add_dependencies(topology2_prod topology2_prod_${output}) endforeach() + +# IPC3 topologies are kept in a separate build subdirectory so the deployment +# step cannot accidentally install them as IPC4/ACE topology files. +file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/ipc3) + +foreach(tplg ${IPC3_TPLGS}) + set(defines "") + list(LENGTH tplg length) + list(GET tplg 0 input) + list(GET tplg 1 output) + + math(EXPR last_index "${length}-1") + if (${last_index} EQUAL 2) + list(GET tplg ${last_index} defines) + endif() + + add_alsatplg2_command("${CMAKE_CURRENT_BINARY_DIR}/../abi-ipc3.conf" abi_ipc3_target + "${CMAKE_CURRENT_SOURCE_DIR}/../${input}" "ipc3/${output}" + "${CMAKE_CURRENT_SOURCE_DIR}/../" "${defines}") + + add_custom_target(topology2_prod_ipc3_${output} DEPENDS ipc3/${output}.tplg) + add_dependencies(topology2_prod topology2_prod_ipc3_${output}) +endforeach() diff --git a/tools/topology/topology2/production/tplg-targets-ipc3.cmake b/tools/topology/topology2/production/tplg-targets-ipc3.cmake new file mode 100644 index 000000000000..b7949b9bd70f --- /dev/null +++ b/tools/topology/topology2/production/tplg-targets-ipc3.cmake @@ -0,0 +1,7 @@ +# SPDX-License-Identifier: BSD-3-Clause + +# Topology2 source targeting firmware that uses the SOF IPC3 protocol. +set(IPC3_TPLGS + # Lenovo Yoga Book YB1-X91F/L: Cherry Trail SSP2 with RT5677 AIF1. + "cht-rt5677\;sof-cht-rt5677" +) diff --git a/tools/topology/topology2/verify-cht-rt5677.py b/tools/topology/topology2/verify-cht-rt5677.py new file mode 100755 index 000000000000..2a309ea3c77f --- /dev/null +++ b/tools/topology/topology2/verify-cht-rt5677.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: BSD-3-Clause +"""Verify the compiled Cherry Trail RT5677 topology contract.""" + +import argparse +import struct +import sys +from pathlib import Path + + +TPLG_MAGIC = 0x41536F43 +TPLG_ABI = 5 +TYPE_PCM = 7 +TYPE_BACKEND_LINK = 10 +HEADER = struct.Struct("<9I") +STREAM_CAPS_SIZE = 104 +PCM_SIZE = 912 +LINK_SIZE = 1656 +HW_CONFIG_SIZE = 120 + + +class VerificationError(Exception): + """Report a topology contract violation.""" + + +def u32(data: bytes, offset: int) -> int: + """Read one little-endian unsigned 32-bit value.""" + return struct.unpack_from(" str: + """Read one fixed-width, NUL-terminated topology string.""" + return data[offset : offset + size].split(b"\0", 1)[0].decode("ascii") + + +def blocks(data: bytes): + """Yield validated topology block headers and payloads.""" + offset = 0 + while offset < len(data): + if len(data) - offset < HEADER.size: + raise VerificationError(f"truncated block header at offset {offset}") + magic, abi, _, block_type, size, _, payload_size, _, count = HEADER.unpack_from( + data, offset + ) + if magic != TPLG_MAGIC: + raise VerificationError(f"invalid topology magic at offset {offset}") + if abi != TPLG_ABI: + raise VerificationError(f"expected topology ABI {TPLG_ABI}, found {abi}") + if size != HEADER.size: + raise VerificationError(f"unexpected block header size {size}") + payload_start = offset + size + payload_end = payload_start + payload_size + if payload_end > len(data): + raise VerificationError(f"truncated block payload at offset {offset}") + yield block_type, count, data[payload_start:payload_end] + offset = payload_end + + if offset != len(data): + raise VerificationError("topology has trailing bytes") + + +def records(payload: bytes, count: int, fixed_size: int): + """Yield records whose private data follows the fixed topology structure.""" + offset = 0 + for _ in range(count): + if len(payload) - offset < fixed_size: + raise VerificationError("truncated topology record") + record_size = u32(payload, offset) + if record_size != fixed_size: + raise VerificationError( + f"unexpected topology record size {record_size}, expected {fixed_size}" + ) + private_size = u32(payload, offset + fixed_size - 4) + end = offset + fixed_size + private_size + if end > len(payload): + raise VerificationError("truncated topology private data") + yield payload[offset:end] + offset = end + + if offset != len(payload): + raise VerificationError("topology block contains trailing record data") + + +def verify_pcm(record: bytes, expected): + """Verify a PCM identity, directions, and its active stream capabilities.""" + pcm_name, pcm_id, playback, capture = expected + actual_name = c_string(record, 4) + actual_id = u32(record, 92) + actual_playback = u32(record, 100) + actual_capture = u32(record, 104) + if (actual_name, actual_id, actual_playback, actual_capture) != expected: + raise VerificationError( + f"unexpected PCM: {(actual_name, actual_id, actual_playback, actual_capture)}" + ) + + for direction, enabled, offset in ( + ("playback", playback, 692), + ("capture", capture, 692 + STREAM_CAPS_SIZE), + ): + caps_size = u32(record, offset) + if not enabled: + if caps_size != 0: + raise VerificationError(f"{pcm_name} unexpectedly enables {direction}") + continue + if caps_size != STREAM_CAPS_SIZE: + raise VerificationError(f"{pcm_name} has invalid {direction} capabilities") + rate_min = u32(record, offset + 60) + rate_max = u32(record, offset + 64) + channels_min = u32(record, offset + 68) + channels_max = u32(record, offset + 72) + if (rate_min, rate_max, channels_min, channels_max) != (48000, 48000, 2, 2): + raise VerificationError( + f"{pcm_name} {direction} is not fixed at 48 kHz stereo" + ) + + +def verify_link(record: bytes): + """Verify the single SSP2 hardware configuration.""" + if c_string(record, 8) != "SSP2-Codec": + raise VerificationError(f"unexpected backend link {c_string(record, 8)!r}") + if u32(record, 1636) != 1: + raise VerificationError("SSP2-Codec must contain exactly one hardware configuration") + if u32(record, 1640) != 0: + raise VerificationError("SSP2-Codec default hardware configuration must be ID 0") + + hw = record[676 : 676 + HW_CONFIG_SIZE] + if u32(hw, 0) != HW_CONFIG_SIZE or u32(hw, 4) != 0: + raise VerificationError("invalid SSP2 hardware configuration header") + actual = { + "format": u32(hw, 8), + "invert_bclk": hw[13], + "invert_fsync": hw[14], + "bclk_provider": hw[15], + "fsync_provider": hw[16], + "mclk_direction": hw[17], + "mclk_rate": u32(hw, 20), + "bclk_rate": u32(hw, 24), + "fsync_rate": u32(hw, 28), + "tdm_slots": u32(hw, 32), + "tdm_slot_width": u32(hw, 36), + "tx_slots": u32(hw, 40), + "rx_slots": u32(hw, 44), + } + expected = { + "format": 5, # SND_SOC_DAI_FORMAT_DSP_B + "invert_bclk": 1, + "invert_fsync": 0, + "bclk_provider": 1, + "fsync_provider": 1, + "mclk_direction": 1, + "mclk_rate": 19_200_000, + "bclk_rate": 4_800_000, + "fsync_rate": 48_000, + "tdm_slots": 4, + "tdm_slot_width": 25, + "tx_slots": 0x3, + "rx_slots": 0x3, + } + if actual != expected: + differences = ", ".join( + f"{key}={actual[key]!r} (expected {value!r})" + for key, value in expected.items() + if actual[key] != value + ) + raise VerificationError(f"invalid SSP2 hardware configuration: {differences}") + + private_size = u32(record, LINK_SIZE - 4) + private = record[LINK_SIZE : LINK_SIZE + private_size] + offset = 0 + sample_bits = [] + while offset < len(private): + if len(private) - offset < 12: + raise VerificationError("truncated SSP2 vendor array") + array_size, tuple_type, count = struct.unpack_from(" len(private): + raise VerificationError("invalid SSP2 vendor array size") + element_size = 48 if tuple_type == 1 else 8 + if 12 + count * element_size != array_size: + raise VerificationError("invalid SSP2 vendor array element count") + for element in range(count): + element_offset = offset + 12 + element * element_size + token = u32(private, element_offset) + if token == 502: + sample_bits.append(u32(private, element_offset + 4)) + offset += array_size + if sample_bits != [24]: + raise VerificationError(f"SSP2 valid sample bits must be 24, found {sample_bits}") + + +def verify(path: Path) -> None: + """Verify all Yoga Book topology invariants in a compiled artifact.""" + data = path.read_bytes() + if not data: + raise VerificationError("topology artifact is empty") + + pcm_records = [] + link_records = [] + for block_type, count, payload in blocks(data): + if block_type == TYPE_PCM: + pcm_records.extend(records(payload, count, PCM_SIZE)) + elif block_type == TYPE_BACKEND_LINK: + link_records.extend(records(payload, count, LINK_SIZE)) + + if len(link_records) != 1: + raise VerificationError( + f"expected one SSP2-Codec backend link, found {len(link_records)}" + ) + if len(pcm_records) != 2: + raise VerificationError(f"expected two PCMs, found {len(pcm_records)}") + + verify_link(link_records[0]) + verify_pcm(pcm_records[0], ("PCM", 0, 1, 1)) + verify_pcm(pcm_records[1], ("PCM Deep Buffer", 1, 1, 0)) + + +def main() -> int: + """Parse arguments and report a concise verification result.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("topology", type=Path, help="compiled sof-cht-rt5677.tplg") + args = parser.parse_args() + try: + verify(args.topology) + except (OSError, UnicodeDecodeError, struct.error, VerificationError) as error: + print(f"FAIL: {error}", file=sys.stderr) + return 1 + print(f"PASS: {args.topology} matches the Yoga Book SSP2 and stereo PCM contract") + return 0 + + +if __name__ == "__main__": + sys.exit(main())