From cc4a03168e2db82419036d66352206d036b22213 Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 7 Jul 2026 09:21:49 +0200 Subject: [PATCH] Add updatable ADAU1701 program via flash partition and HTTP API. Decouple the SigmaStudio RAM download from compiled-in adau1701_program.c: DRAD v1 blobs in the dsp partition with embedded fallback, POST /api/dsp/program, and host-tested blob parse/serialize helpers. Co-authored-by: Cursor --- Software/Firmware/ADAU1701-Firmware/README.md | 16 +- .../ADAU1701-Firmware/SigmaStudioFW.h | 9 +- Software/components/core/CMakeLists.txt | 3 + .../core/include/core/DspProgram.hpp | 61 ++++++ .../core/include/core/DspProgramBlob.hpp | 71 +++++++ .../core/include/core/DspProgramError.hpp | 38 ++++ .../core/include/core/IDspProgramSource.hpp | 50 +++++ .../core/include/core/RegisterWrite.hpp | 75 +++++++ Software/components/core/src/DspProgram.cpp | 28 +++ .../components/core/src/DspProgramBlob.cpp | 185 ++++++++++++++++++ .../components/core/src/RegisterWrite.cpp | 35 ++++ Software/components/core/test/CMakeLists.txt | 7 + .../core/test/dsp_program_blob_test.cpp | 116 +++++++++++ .../drivers/adau1701/CMakeLists.txt | 6 +- .../include/adau1701/Adau1701Driver.hpp | 19 +- .../adau1701/EmbeddedDspProgramSource.hpp | 45 +++++ .../adau1701/FallbackDspProgramSource.hpp | 63 ++++++ .../adau1701/FlashDspProgramSource.hpp | 62 ++++++ .../drivers/adau1701/src/Adau1701Driver.cpp | 37 +++- .../adau1701/src/EmbeddedDspProgramSource.cpp | 60 ++++++ .../adau1701/src/FallbackDspProgramSource.cpp | 44 +++++ .../adau1701/src/FlashDspProgramSource.cpp | 88 +++++++++ .../drivers/adau1701/src/SigmaStudioFW.c | 8 +- .../drivers/adau1701/src/adau1701_program.c | 20 -- Software/components/net/CMakeLists.txt | 2 +- .../components/net/src/SetupWebServer.cpp | 74 +++++++ Software/docs/manual/ch-adau1701.tex | 7 +- Software/docs/manual/ch-api.tex | 20 ++ Software/docs/manual/ch-classes.tex | 24 +++ Software/main/hardware_bootstrap.cpp | 11 +- Software/tools/pack_dsp_program.py | 68 +++++++ 31 files changed, 1302 insertions(+), 50 deletions(-) create mode 100644 Software/components/core/include/core/DspProgram.hpp create mode 100644 Software/components/core/include/core/DspProgramBlob.hpp create mode 100644 Software/components/core/include/core/DspProgramError.hpp create mode 100644 Software/components/core/include/core/IDspProgramSource.hpp create mode 100644 Software/components/core/include/core/RegisterWrite.hpp create mode 100644 Software/components/core/src/DspProgram.cpp create mode 100644 Software/components/core/src/DspProgramBlob.cpp create mode 100644 Software/components/core/src/RegisterWrite.cpp create mode 100644 Software/components/core/test/dsp_program_blob_test.cpp create mode 100644 Software/components/drivers/adau1701/include/adau1701/EmbeddedDspProgramSource.hpp create mode 100644 Software/components/drivers/adau1701/include/adau1701/FallbackDspProgramSource.hpp create mode 100644 Software/components/drivers/adau1701/include/adau1701/FlashDspProgramSource.hpp create mode 100644 Software/components/drivers/adau1701/src/EmbeddedDspProgramSource.cpp create mode 100644 Software/components/drivers/adau1701/src/FallbackDspProgramSource.cpp create mode 100644 Software/components/drivers/adau1701/src/FlashDspProgramSource.cpp delete mode 100644 Software/components/drivers/adau1701/src/adau1701_program.c create mode 100644 Software/tools/pack_dsp_program.py diff --git a/Software/Firmware/ADAU1701-Firmware/README.md b/Software/Firmware/ADAU1701-Firmware/README.md index 1ba50cd..6766f2f 100644 --- a/Software/Firmware/ADAU1701-Firmware/README.md +++ b/Software/Firmware/ADAU1701-Firmware/README.md @@ -15,8 +15,13 @@ SigmaStudio project **DigiRadio**, IC1 = ADAU1701. Shipped with DigiRadio firmwa ## Runtime (fw 0.8.3) -- **Boot:** ESP32 replays `default_download_IC_1()` over I2C via - `adau1701::Adau1701Driver` on every power-up. +- **Boot:** ESP32 replays the active `DspProgram` over I2C via + `adau1701::Adau1701Driver` on every power-up. Source order: valid blob in + the `dsp` flash partition, else the embedded export from this folder + (`EmbeddedDspProgramSource`). +- **Program update:** `POST /api/dsp/program` with a `DRAD` v1 blob (see + `Software/tools/pack_dsp_program.py`). Device reboots and loads the new + script on next boot. - **Live control:** six-band PEQ, input mixer, master volume, stereo/bass enhancement overlays — safeload via `audio::AudioService` and REST `/api/audio/*`; profile persisted in encrypted NVS. @@ -37,6 +42,13 @@ Parameter map: `components/drivers/adau1701/include/adau1701/Adau1701ParamMap.hp Do **not** use SigmaStudio *Link Compile Download* on DigiRadio — export only; the ESP32 programs the DSP at every power-up. +## DRAD v1 blob (flash / HTTP upload) + +Layout: magic `DRAD`, version u16 (=1), write count u16, CRC32 (IEEE over +writes section), then for each write: address u16, length u16, data bytes. +Pack from JSON with `Software/tools/pack_dsp_program.py` or use +`core::serializeDspProgramBlob()` in host tests. + ## Documentation - Design: `docs/manual/ch-sigmastudio.tex` diff --git a/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h b/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h index bf7af11..1719e94 100644 --- a/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h +++ b/Software/Firmware/ADAU1701-Firmware/SigmaStudioFW.h @@ -36,19 +36,14 @@ void sigma_studio_set_device(void* i2cDevHandle); * * @param devAddress SigmaStudio device address (0x68 write addr). * @param address 16-bit target address in DSP memory map. - * @param length Payload length in bytes. + * @param length Payload length in bytes (may exceed 255). * @param pData Payload bytes. */ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, unsigned int address, - unsigned char length, + unsigned int length, ADI_REG_TYPE* pData); -/** - * @brief Run the DigiRadio SigmaStudio default download sequence. - */ -void adau1701_run_default_download(void); - /** Safeload data register base (0x0810..0x0814). */ #define ADAU1701_SAFELOAD_DATA_BASE 0x0810U /** Safeload address register base (0x0815..0x0819). */ diff --git a/Software/components/core/CMakeLists.txt b/Software/components/core/CMakeLists.txt index b7073fd..fcc386f 100644 --- a/Software/components/core/CMakeLists.txt +++ b/Software/components/core/CMakeLists.txt @@ -34,6 +34,9 @@ idf_component_register( "src/BroadcastLabel.cpp" "src/RdsMetadataAccumulator.cpp" "src/DabDynamicLabelAccumulator.cpp" + "src/DspProgram.cpp" + "src/RegisterWrite.cpp" + "src/DspProgramBlob.cpp" INCLUDE_DIRS "include" ) diff --git a/Software/components/core/include/core/DspProgram.hpp b/Software/components/core/include/core/DspProgram.hpp new file mode 100644 index 0000000..657b34b --- /dev/null +++ b/Software/components/core/include/core/DspProgram.hpp @@ -0,0 +1,61 @@ +/** + * @file DspProgram.hpp + * @brief Ordered ADAU1701 RAM download sequence (pure domain). + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/RegisterWrite.hpp" + +#include + +namespace core { + +/** + * @brief DspProgram — immutable SigmaStudio download script. + * + * @dname DspProgram + * @return n/a (type) + * @pubstate Owns writes_; built from a validated flash blob or embedded export. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class DspProgram { +public: + /** + * @brief DspProgram — construct from an ordered write list. + * + * @dname DspProgram + * @param writes Non-empty register blocks in replay order. + * @pubstate moves writes into writes_. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + explicit DspProgram(std::vector writes); + + /** + * @brief writes — read the ordered download steps. + * + * @dname writes + * @return Reference to the internal write vector. + * @pubstate reads writes_. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] const std::vector& writes() const noexcept; + +private: + std::vector writes_; +}; + +} // namespace core diff --git a/Software/components/core/include/core/DspProgramBlob.hpp b/Software/components/core/include/core/DspProgramBlob.hpp new file mode 100644 index 0000000..b1d92bd --- /dev/null +++ b/Software/components/core/include/core/DspProgramBlob.hpp @@ -0,0 +1,71 @@ +/** + * @file DspProgramBlob.hpp + * @brief Framed ADAU1701 program blob parse/serialise (pure core). + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * Blob layout (v1, little-endian): + * magic[4] = "DRAD", version u16, write_count u16, payload_crc32 u32, + * then for each write: address u16, length u16, data[length]. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/DspProgram.hpp" +#include "core/DspProgramError.hpp" + +#include +#include +#include +#include +#include + +namespace core { + +/** + * @brief parseDspProgramBlob — validate and decode a flash/HTTP blob. + * + * @dname parseDspProgramBlob + * @param blob Raw bytes from the dsp partition or POST body. + * @return DspProgram on success, or DspProgramError. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-07-07 + */ +[[nodiscard]] std::expected +parseDspProgramBlob(std::span blob); + +/** + * @brief serializeDspProgramBlob — encode a program for flash storage. + * + * @dname serializeDspProgramBlob + * @param program Ordered download script. + * @return Framed blob bytes with CRC32. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-07-07 + */ +[[nodiscard]] std::vector +serializeDspProgramBlob(const DspProgram& program); + +/** + * @brief dspProgramErrorToken — stable API/JSON error string. + * + * @dname dspProgramErrorToken + * @param error Parse or store failure. + * @return Short snake_case token. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-07-07 + */ +[[nodiscard]] const char* dspProgramErrorToken(DspProgramError error) noexcept; + +} // namespace core diff --git a/Software/components/core/include/core/DspProgramError.hpp b/Software/components/core/include/core/DspProgramError.hpp new file mode 100644 index 0000000..d5b2a0f --- /dev/null +++ b/Software/components/core/include/core/DspProgramError.hpp @@ -0,0 +1,38 @@ +/** + * @file DspProgramError.hpp + * @brief Failure causes for ADAU1701 program blob parse and load. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +namespace core { + +/** + * @brief DspProgramError — ADAU1701 program validation failures. + * + * @dname DspProgramError + * @return n/a (type) + * @pubstate n/a + * + * @author Michele Bigi + * @date 2026-07-07 + */ +enum class DspProgramError { + Empty, ///< No bytes supplied. + Truncated, ///< Header or write payload shorter than declared. + InvalidMagic, + UnsupportedVersion, + BadCrc, + TooLarge, + FlashReadFailed, + FlashWriteFailed, +}; + +} // namespace core diff --git a/Software/components/core/include/core/IDspProgramSource.hpp b/Software/components/core/include/core/IDspProgramSource.hpp new file mode 100644 index 0000000..dd1158f --- /dev/null +++ b/Software/components/core/include/core/IDspProgramSource.hpp @@ -0,0 +1,50 @@ +/** + * @file IDspProgramSource.hpp + * @brief Port for loading an ADAU1701 RAM download program. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/DspProgram.hpp" +#include "core/DspProgramError.hpp" + +#include + +namespace core { + +/** + * @brief IDspProgramSource — supplies DspProgram for Adau1701Driver::boot(). + * + * @dname IDspProgramSource + * @return n/a (type) + * @pubstate Implementations live in the adau1701 driver (embedded + flash). + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class IDspProgramSource { +public: + virtual ~IDspProgramSource() = default; + + /** + * @brief loadProgram — obtain the download script for this boot. + * + * @dname loadProgram + * @return DspProgram on success, or DspProgramError. + * @pubstate none + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] virtual std::expected + loadProgram() = 0; +}; + +} // namespace core diff --git a/Software/components/core/include/core/RegisterWrite.hpp b/Software/components/core/include/core/RegisterWrite.hpp new file mode 100644 index 0000000..895230c --- /dev/null +++ b/Software/components/core/include/core/RegisterWrite.hpp @@ -0,0 +1,75 @@ +/** + * @file RegisterWrite.hpp + * @brief One SigmaStudio SIGMA_WRITE_REGISTER_BLOCK transaction. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include +#include +#include + +namespace core { + +/** + * @brief RegisterWrite — 16-bit DSP address plus payload bytes. + * + * @dname RegisterWrite + * @return n/a (type) + * @pubstate Owns data_; replayed by Adau1701Driver via SIGMA_WRITE_REGISTER_BLOCK. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class RegisterWrite { +public: + /** + * @brief RegisterWrite — construct one download step. + * + * @dname RegisterWrite + * @param address Target address in the ADAU1701 memory map. + * @param data Payload bytes (may exceed 255; driver chunks I2C). + * @pubstate moves data into data_. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + RegisterWrite(std::uint16_t address, std::vector data); + + /** + * @brief address — read the 16-bit target address. + * + * @dname address + * @return SigmaStudio register/data address. + * @pubstate reads address_. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] std::uint16_t address() const noexcept; + + /** + * @brief data — read payload bytes. + * + * @dname data + * @return Immutable byte span of the write payload. + * @pubstate reads data_. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] std::span data() const noexcept; + +private: + std::uint16_t address_; + std::vector data_; +}; + +} // namespace core diff --git a/Software/components/core/src/DspProgram.cpp b/Software/components/core/src/DspProgram.cpp new file mode 100644 index 0000000..31889b5 --- /dev/null +++ b/Software/components/core/src/DspProgram.cpp @@ -0,0 +1,28 @@ +/** + * @file DspProgram.cpp + * @brief DspProgram implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "core/DspProgram.hpp" + +namespace core { + +DspProgram::DspProgram(std::vector writes) + : writes_(std::move(writes)) +{ +} + +const std::vector& DspProgram::writes() const noexcept +{ + return writes_; +} + +} // namespace core diff --git a/Software/components/core/src/DspProgramBlob.cpp b/Software/components/core/src/DspProgramBlob.cpp new file mode 100644 index 0000000..b30c5ef --- /dev/null +++ b/Software/components/core/src/DspProgramBlob.cpp @@ -0,0 +1,185 @@ +/** + * @file DspProgramBlob.cpp + * @brief DspProgram blob parse/serialise implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "core/DspProgramBlob.hpp" + +#include +#include + +namespace core { + +namespace { + +constexpr char kMagic[4] = {'D', 'R', 'A', 'D'}; +constexpr std::uint16_t kVersion = 1U; +constexpr std::size_t kHeaderSize = 12U; +constexpr std::size_t kMaxBlobSize = 200U * 1024U; +constexpr std::size_t kMaxWriteCount = 32U; +constexpr std::size_t kMaxWritePayload = 16U * 1024U; + +[[nodiscard]] std::uint16_t readLe16(const std::uint8_t* p) +{ + return static_cast(p[0] | (static_cast(p[1]) << 8)); +} + +[[nodiscard]] std::uint32_t readLe32(const std::uint8_t* p) +{ + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24); +} + +[[nodiscard]] std::uint32_t crc32(std::span data) +{ + std::uint32_t crc = 0xFFFFFFFFU; + for (const std::uint8_t byte : data) { + crc ^= byte; + for (int bit = 0; bit < 8; ++bit) { + const std::uint32_t mask = -(crc & 1U); + crc = (crc >> 1) ^ (0xEDB88320U & mask); + } + } + return ~crc; +} + +[[nodiscard]] bool magicMatches(std::span blob) +{ + return blob.size() >= 4U + && blob[0] == static_cast(kMagic[0]) + && blob[1] == static_cast(kMagic[1]) + && blob[2] == static_cast(kMagic[2]) + && blob[3] == static_cast(kMagic[3]); +} + +} // namespace + +const char* dspProgramErrorToken(DspProgramError error) noexcept +{ + switch (error) { + case DspProgramError::Empty: + return "empty"; + case DspProgramError::Truncated: + return "truncated"; + case DspProgramError::InvalidMagic: + return "invalid_magic"; + case DspProgramError::UnsupportedVersion: + return "unsupported_version"; + case DspProgramError::BadCrc: + return "bad_crc"; + case DspProgramError::TooLarge: + return "too_large"; + case DspProgramError::FlashReadFailed: + return "flash_read_failed"; + case DspProgramError::FlashWriteFailed: + return "flash_write_failed"; + } + return "unknown"; +} + +std::expected +parseDspProgramBlob(std::span blob) +{ + if (blob.empty()) { + return std::unexpected(DspProgramError::Empty); + } + if (blob.size() > kMaxBlobSize) { + return std::unexpected(DspProgramError::TooLarge); + } + if (blob.size() < kHeaderSize) { + return std::unexpected(DspProgramError::Truncated); + } + if (!magicMatches(blob)) { + return std::unexpected(DspProgramError::InvalidMagic); + } + if (readLe16(blob.data() + 4U) != kVersion) { + return std::unexpected(DspProgramError::UnsupportedVersion); + } + + const std::uint16_t writeCount = readLe16(blob.data() + 6U); + const std::uint32_t expectedCrc = readLe32(blob.data() + 8U); + const std::span payload = blob.subspan(kHeaderSize); + + if (writeCount == 0U || writeCount > kMaxWriteCount) { + return std::unexpected(DspProgramError::Truncated); + } + if (crc32(payload) != expectedCrc) { + return std::unexpected(DspProgramError::BadCrc); + } + + std::vector writes; + writes.reserve(writeCount); + std::size_t offset = 0U; + + for (std::uint16_t index = 0U; index < writeCount; ++index) { + if (offset + 4U > payload.size()) { + return std::unexpected(DspProgramError::Truncated); + } + const std::uint16_t address = + readLe16(payload.data() + offset); + const std::uint16_t length = + readLe16(payload.data() + offset + 2U); + offset += 4U; + + if (length == 0U || length > kMaxWritePayload + || offset + length > payload.size()) { + return std::unexpected(DspProgramError::Truncated); + } + + std::vector data(length); + std::memcpy(data.data(), payload.data() + offset, length); + offset += length; + writes.emplace_back(address, std::move(data)); + } + + if (offset != payload.size()) { + return std::unexpected(DspProgramError::Truncated); + } + + return DspProgram(std::move(writes)); +} + +std::vector serializeDspProgramBlob(const DspProgram& program) +{ + std::vector payload; + for (const RegisterWrite& write : program.writes()) { + const auto data = write.data(); + payload.push_back(static_cast(write.address() & 0xFFU)); + payload.push_back( + static_cast((write.address() >> 8) & 0xFFU)); + const std::uint16_t length = + static_cast(data.size()); + payload.push_back(static_cast(length & 0xFFU)); + payload.push_back(static_cast((length >> 8) & 0xFFU)); + payload.insert(payload.end(), data.begin(), data.end()); + } + + std::vector blob; + blob.reserve(kHeaderSize + payload.size()); + blob.insert(blob.end(), kMagic, kMagic + 4); + blob.push_back(static_cast(kVersion & 0xFFU)); + blob.push_back(static_cast((kVersion >> 8) & 0xFFU)); + const std::uint16_t writeCount = + static_cast(program.writes().size()); + blob.push_back(static_cast(writeCount & 0xFFU)); + blob.push_back(static_cast((writeCount >> 8) & 0xFFU)); + const std::uint32_t payloadCrc = crc32(payload); + blob.push_back(static_cast(payloadCrc & 0xFFU)); + blob.push_back(static_cast((payloadCrc >> 8) & 0xFFU)); + blob.push_back(static_cast((payloadCrc >> 16) & 0xFFU)); + blob.push_back(static_cast((payloadCrc >> 24) & 0xFFU)); + blob.insert(blob.end(), payload.begin(), payload.end()); + return blob; +} + +} // namespace core diff --git a/Software/components/core/src/RegisterWrite.cpp b/Software/components/core/src/RegisterWrite.cpp new file mode 100644 index 0000000..99111a5 --- /dev/null +++ b/Software/components/core/src/RegisterWrite.cpp @@ -0,0 +1,35 @@ +/** + * @file RegisterWrite.cpp + * @brief RegisterWrite implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "core/RegisterWrite.hpp" + +namespace core { + +RegisterWrite::RegisterWrite(std::uint16_t address, + std::vector data) + : address_(address) + , data_(std::move(data)) +{ +} + +std::uint16_t RegisterWrite::address() const noexcept +{ + return address_; +} + +std::span RegisterWrite::data() const noexcept +{ + return data_; +} + +} // namespace core diff --git a/Software/components/core/test/CMakeLists.txt b/Software/components/core/test/CMakeLists.txt index 7776961..6b98b21 100644 --- a/Software/components/core/test/CMakeLists.txt +++ b/Software/components/core/test/CMakeLists.txt @@ -46,11 +46,18 @@ add_library(digiradio_core STATIC "${CORE_SRC_DIR}/BroadcastLabel.cpp" "${CORE_SRC_DIR}/RdsMetadataAccumulator.cpp" "${CORE_SRC_DIR}/DabDynamicLabelAccumulator.cpp" + "${CORE_SRC_DIR}/DspProgram.cpp" + "${CORE_SRC_DIR}/RegisterWrite.cpp" + "${CORE_SRC_DIR}/DspProgramBlob.cpp" ) target_include_directories(digiradio_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include" ) +add_executable(dsp_program_blob_test dsp_program_blob_test.cpp) +target_link_libraries(dsp_program_blob_test PRIVATE digiradio_core) +add_test(NAME dsp_program_blob_test COMMAND dsp_program_blob_test) + add_executable(device_identity_test device_identity_test.cpp) target_link_libraries(device_identity_test PRIVATE digiradio_core) add_test(NAME device_identity_test COMMAND device_identity_test) diff --git a/Software/components/core/test/dsp_program_blob_test.cpp b/Software/components/core/test/dsp_program_blob_test.cpp new file mode 100644 index 0000000..7685952 --- /dev/null +++ b/Software/components/core/test/dsp_program_blob_test.cpp @@ -0,0 +1,116 @@ +/** + * @file dsp_program_blob_test.cpp + * @brief Host tests for ADAU1701 program blob parse/serialise. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "core/DspProgram.hpp" +#include "core/DspProgramBlob.hpp" +#include "core/DspProgramError.hpp" +#include "core/RegisterWrite.hpp" + +#include +#include +#include + +namespace { + +[[nodiscard]] core::DspProgram sampleProgram() +{ + return core::DspProgram(std::vector{ + core::RegisterWrite(0x081CU, {0x00U, 0x1CU}), + core::RegisterWrite(0x0400U, std::vector(16U, 0xABU)), + }); +} + +[[nodiscard]] int runRoundTripTest() +{ + const core::DspProgram original = sampleProgram(); + const std::vector blob = + core::serializeDspProgramBlob(original); + const auto parsed = core::parseDspProgramBlob(blob); + if (!parsed) { + std::cerr << "round-trip parse failed\n"; + return EXIT_FAILURE; + } + if (parsed->writes().size() != original.writes().size()) { + std::cerr << "write count mismatch\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runEmptyTest() +{ + const std::vector empty; + const auto parsed = core::parseDspProgramBlob(empty); + if (parsed || parsed.error() != core::DspProgramError::Empty) { + std::cerr << "expected empty error\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runTruncatedTest() +{ + const std::vector blob = {'D', 'R', 'A', 'D', 1, 0, 1, 0}; + const auto parsed = core::parseDspProgramBlob(blob); + if (parsed || parsed.error() != core::DspProgramError::Truncated) { + std::cerr << "expected truncated error\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runBadCrcTest() +{ + std::vector blob = core::serializeDspProgramBlob(sampleProgram()); + blob.back() ^= 0xFFU; + const auto parsed = core::parseDspProgramBlob(blob); + if (parsed || parsed.error() != core::DspProgramError::BadCrc) { + std::cerr << "expected bad_crc error\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +[[nodiscard]] int runBadMagicTest() +{ + std::vector blob = core::serializeDspProgramBlob(sampleProgram()); + blob[0] = 'X'; + const auto parsed = core::parseDspProgramBlob(blob); + if (parsed || parsed.error() != core::DspProgramError::InvalidMagic) { + std::cerr << "expected invalid_magic error\n"; + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +} // namespace + +int main() +{ + if (runRoundTripTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runEmptyTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runTruncatedTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runBadCrcTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + if (runBadMagicTest() != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/Software/components/drivers/adau1701/CMakeLists.txt b/Software/components/drivers/adau1701/CMakeLists.txt index 51c2aad..3462118 100644 --- a/Software/components/drivers/adau1701/CMakeLists.txt +++ b/Software/components/drivers/adau1701/CMakeLists.txt @@ -4,12 +4,14 @@ idf_component_register( SRCS "src/Adau1701Driver.cpp" "src/Adau1701Dsp.cpp" + "src/EmbeddedDspProgramSource.cpp" + "src/FlashDspProgramSource.cpp" + "src/FallbackDspProgramSource.cpp" "src/SigmaStudioFW.c" - "src/adau1701_program.c" INCLUDE_DIRS "include" "${ADAU_FW_DIR}" - REQUIRES core driver esp_driver_gpio esp_driver_i2c + REQUIRES core driver esp_driver_gpio esp_driver_i2c esp_partition ) target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23) diff --git a/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp b/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp index 8c2517e..acf6753 100644 --- a/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp +++ b/Software/components/drivers/adau1701/include/adau1701/Adau1701Driver.hpp @@ -19,6 +19,7 @@ #include "core/EqProfile.hpp" #include "core/FrequencyHz.hpp" #include "core/GainDb.hpp" +#include "core/IDspProgramSource.hpp" #include "core/MixSource.hpp" #include "core/MixerState.hpp" @@ -48,10 +49,11 @@ struct Adau1701Pins { * @brief Adau1701Driver — owns I2C + reset, boot, and safeload runtime. * * @dname Adau1701Driver - * @param pins Board wiring for I2C and RESET#. + * @param pins Board wiring for I2C and RESET#. + * @param programSource RAM download script (flash with embedded fallback). * @return n/a (type) * @pubstate Owns I2C bus/device handles (RAII). booted_ true after a - * successful default_download replay. + * successful program replay. * * Writes the SigmaStudio export from Firmware/ADAU1701-Firmware on every * boot (no EEPROM self-boot on DigiRadio). Runtime EQ and mixer changes @@ -63,16 +65,18 @@ struct Adau1701Pins { class Adau1701Driver { public: /** - * @brief Adau1701Driver — construct with board pin map. + * @brief Adau1701Driver — construct with board pin map and program source. * * @dname Adau1701Driver - * @param pins SDA/SCL/reset/address configuration. - * @pubstate stores pins_; not booted until boot(). + * @param pins SDA/SCL/reset/address configuration. + * @param programSource Download script provider for boot(). + * @pubstate stores pins_ and programSource_; not booted until boot(). * * @author Michele Bigi * @date 2026-07-06 */ - explicit Adau1701Driver(Adau1701Pins pins); + explicit Adau1701Driver(Adau1701Pins pins, + core::IDspProgramSource& programSource); /** * @brief ~Adau1701Driver — release I2C resources. @@ -222,8 +226,11 @@ private: unsigned paramAddr, core::GainDb gain); [[nodiscard]] std::expected safeloadFixpoint( unsigned paramAddr, std::int32_t fixpoint); + [[nodiscard]] std::expected replayProgram( + const core::DspProgram& program); Adau1701Pins pins_; + core::IDspProgramSource& programSource_; bool booted_; void* i2cBus_; void* i2cDev_; diff --git a/Software/components/drivers/adau1701/include/adau1701/EmbeddedDspProgramSource.hpp b/Software/components/drivers/adau1701/include/adau1701/EmbeddedDspProgramSource.hpp new file mode 100644 index 0000000..a67b3d6 --- /dev/null +++ b/Software/components/drivers/adau1701/include/adau1701/EmbeddedDspProgramSource.hpp @@ -0,0 +1,45 @@ +/** + * @file EmbeddedDspProgramSource.hpp + * @brief IDspProgramSource backed by the compiled SigmaStudio export. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/IDspProgramSource.hpp" + +namespace adau1701 { + +/** + * @brief EmbeddedDspProgramSource — factory default from DigiRadio_IC_1.h. + * + * @dname EmbeddedDspProgramSource + * @return n/a (type) + * @pubstate Builds the same five SIGMA_WRITE steps as default_download_IC_1(). + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class EmbeddedDspProgramSource : public core::IDspProgramSource { +public: + /** + * @brief loadProgram — return the embedded SigmaStudio download script. + * + * @dname loadProgram + * @return DspProgram mirroring default_download_IC_1(). + * @pubstate none + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] std::expected + loadProgram() override; +}; + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/include/adau1701/FallbackDspProgramSource.hpp b/Software/components/drivers/adau1701/include/adau1701/FallbackDspProgramSource.hpp new file mode 100644 index 0000000..7dc9c5a --- /dev/null +++ b/Software/components/drivers/adau1701/include/adau1701/FallbackDspProgramSource.hpp @@ -0,0 +1,63 @@ +/** + * @file FallbackDspProgramSource.hpp + * @brief Tries flash partition first, then embedded SigmaStudio export. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/IDspProgramSource.hpp" + +namespace adau1701 { + +/** + * @brief FallbackDspProgramSource — flash then embedded program loader. + * + * @dname FallbackDspProgramSource + * @return n/a (type) + * @pubstate Borrows primary and fallback sources for the process lifetime. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class FallbackDspProgramSource : public core::IDspProgramSource { +public: + /** + * @brief FallbackDspProgramSource — wire flash and embedded sources. + * + * @dname FallbackDspProgramSource + * @param primary Usually FlashDspProgramSource. + * @param fallback Usually EmbeddedDspProgramSource. + * @pubstate stores non-owning references. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + FallbackDspProgramSource(core::IDspProgramSource& primary, + core::IDspProgramSource& fallback); + + /** + * @brief loadProgram — use flash when valid, else embedded export. + * + * @dname loadProgram + * @return DspProgram from primary or fallback. + * @pubstate logs when falling back to embedded. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] std::expected + loadProgram() override; + +private: + core::IDspProgramSource& primary_; + core::IDspProgramSource& fallback_; +}; + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/include/adau1701/FlashDspProgramSource.hpp b/Software/components/drivers/adau1701/include/adau1701/FlashDspProgramSource.hpp new file mode 100644 index 0000000..4a75e92 --- /dev/null +++ b/Software/components/drivers/adau1701/include/adau1701/FlashDspProgramSource.hpp @@ -0,0 +1,62 @@ +/** + * @file FlashDspProgramSource.hpp + * @brief IDspProgramSource reading the dsp flash partition. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ +#pragma once + +#include "core/IDspProgramSource.hpp" + +#include +#include + +namespace adau1701 { + +/** + * @brief FlashDspProgramSource — loads a validated blob from partition dsp. + * + * @dname FlashDspProgramSource + * @return n/a (type) + * @pubstate Uses esp_partition API; empty/erased flash returns Empty. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +class FlashDspProgramSource : public core::IDspProgramSource { +public: + /** + * @brief loadProgram — read and parse the dsp data partition. + * + * @dname loadProgram + * @return DspProgram on success, or a parse/flash error. + * @pubstate reads the full partition into RAM once per call. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] std::expected + loadProgram() override; + + /** + * @brief storeBlob — erase and write a validated blob to partition dsp. + * + * @dname storeBlob + * @param blob Framed program bytes (already validated by caller). + * @return Ok on success, or DspProgramError::FlashWriteFailed. + * @pubstate erases the dsp partition before writing. + * + * @author Michele Bigi + * @date 2026-07-07 + */ + [[nodiscard]] static std::expected + storeBlob(std::span blob); +}; + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp index 508218f..bdad130 100644 --- a/Software/components/drivers/adau1701/src/Adau1701Driver.cpp +++ b/Software/components/drivers/adau1701/src/Adau1701Driver.cpp @@ -16,6 +16,7 @@ #include "adau1701/Adau1701ParamMap.hpp" #include "core/BiquadDesign.hpp" +#include "core/DspProgram.hpp" #include "DigiRadio_IC_1_PARAM.h" #include "SigmaStudioFW.h" @@ -28,9 +29,6 @@ extern "C" { -/** @brief SigmaStudio default program download (generated C, not part of the C++ API). */ -void adau1701_run_default_download(void); - } // extern "C" namespace adau1701 { @@ -42,8 +40,10 @@ constexpr int kI2cPort = 0; constexpr std::uint8_t kFixedHighPassBandIndex = 0U; } // namespace -Adau1701Driver::Adau1701Driver(Adau1701Pins pins) +Adau1701Driver::Adau1701Driver(Adau1701Pins pins, + core::IDspProgramSource& programSource) : pins_(pins) + , programSource_(programSource) , booted_(false) , i2cBus_(nullptr) , i2cDev_(nullptr) @@ -62,6 +62,26 @@ Adau1701Driver::~Adau1701Driver() } } +std::expected Adau1701Driver::replayProgram( + const core::DspProgram& program) +{ + const unsigned char deviceAddr = + static_cast(pins_.i2cAddr7 << 1); + for (const core::RegisterWrite& write : program.writes()) { + const auto data = write.data(); + if (data.empty()) { + return std::unexpected(Adau1701Error::DownloadFailed); + } + SIGMA_WRITE_REGISTER_BLOCK( + deviceAddr, + write.address(), + static_cast(data.size()), + const_cast( + reinterpret_cast(data.data()))); + } + return {}; +} + std::expected Adau1701Driver::boot() { if (booted_) { @@ -110,7 +130,14 @@ std::expected Adau1701Driver::boot() sigma_studio_bind_i2c(kI2cPort, static_cast(pins_.i2cAddr7)); sigma_studio_set_device(dev); - adau1701_run_default_download(); + const auto program = programSource_.loadProgram(); + if (!program) { + ESP_LOGE(kTag, "DSP program load failed"); + return std::unexpected(Adau1701Error::DownloadFailed); + } + if (auto replay = replayProgram(*program); !replay) { + return replay; + } booted_ = true; ESP_LOGI(kTag, "SigmaStudio program loaded"); diff --git a/Software/components/drivers/adau1701/src/EmbeddedDspProgramSource.cpp b/Software/components/drivers/adau1701/src/EmbeddedDspProgramSource.cpp new file mode 100644 index 0000000..f2a573d --- /dev/null +++ b/Software/components/drivers/adau1701/src/EmbeddedDspProgramSource.cpp @@ -0,0 +1,60 @@ +/** + * @file EmbeddedDspProgramSource.cpp + * @brief EmbeddedDspProgramSource implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "adau1701/EmbeddedDspProgramSource.hpp" + +#include "DigiRadio_IC_1.h" +#include "DigiRadio_IC_1_REG.h" + +#include + +namespace adau1701 { + +namespace { + +[[nodiscard]] std::vector copyBytes(const ADI_REG_TYPE* data, + std::size_t size) +{ + return std::vector( + reinterpret_cast(data), + reinterpret_cast(data) + size); +} + +} // namespace + +std::expected +EmbeddedDspProgramSource::loadProgram() +{ + std::vector writes; + writes.reserve(5U); + + writes.emplace_back( + static_cast(REG_COREREGISTER_IC_1_ADDR), + copyBytes(R0_COREREGISTER_IC_1_Default, REG_COREREGISTER_IC_1_BYTE)); + writes.emplace_back( + static_cast(PROGRAM_ADDR_IC_1), + copyBytes(Program_Data_IC_1, PROGRAM_SIZE_IC_1)); + writes.emplace_back( + static_cast(PARAM_ADDR_IC_1), + copyBytes(Param_Data_IC_1, PARAM_SIZE_IC_1)); + writes.emplace_back( + static_cast(REG_COREREGISTER_IC_1_ADDR), + copyBytes(R3_HWCONFIGURATION_IC_1_Default, R3_HWCONFIGURATION_IC_1_SIZE)); + writes.emplace_back( + static_cast(REG_COREREGISTER_IC_1_ADDR), + copyBytes(R4_COREREGISTER_IC_1_Default, REG_COREREGISTER_IC_1_BYTE)); + + return core::DspProgram(std::move(writes)); +} + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/src/FallbackDspProgramSource.cpp b/Software/components/drivers/adau1701/src/FallbackDspProgramSource.cpp new file mode 100644 index 0000000..4f8b5bf --- /dev/null +++ b/Software/components/drivers/adau1701/src/FallbackDspProgramSource.cpp @@ -0,0 +1,44 @@ +/** + * @file FallbackDspProgramSource.cpp + * @brief FallbackDspProgramSource implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "adau1701/FallbackDspProgramSource.hpp" + +#include "esp_log.h" + +namespace adau1701 { + +namespace { +constexpr char kTag[] = "DspProgram"; +} // namespace + +FallbackDspProgramSource::FallbackDspProgramSource( + core::IDspProgramSource& primary, + core::IDspProgramSource& fallback) + : primary_(primary) + , fallback_(fallback) +{ +} + +std::expected +FallbackDspProgramSource::loadProgram() +{ + if (auto primary = primary_.loadProgram(); primary) { + ESP_LOGI(kTag, "using flash DSP program"); + return primary; + } + + ESP_LOGW(kTag, "flash DSP program unavailable — using embedded export"); + return fallback_.loadProgram(); +} + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/src/FlashDspProgramSource.cpp b/Software/components/drivers/adau1701/src/FlashDspProgramSource.cpp new file mode 100644 index 0000000..f5b20c8 --- /dev/null +++ b/Software/components/drivers/adau1701/src/FlashDspProgramSource.cpp @@ -0,0 +1,88 @@ +/** + * @file FlashDspProgramSource.cpp + * @brief FlashDspProgramSource implementation. + * + * DigiRadio firmware — https://github.com/manvalan/DigiRadio + * + * Copyright 2026 Michele Bigi + * SPDX-License-Identifier: Apache-2.0 + * + * @author Michele Bigi + * @date 2026-07-07 + */ + +#include "adau1701/FlashDspProgramSource.hpp" + +#include "core/DspProgramBlob.hpp" + +#include "esp_log.h" +#include "esp_partition.h" + +#include + +namespace adau1701 { + +namespace { + +constexpr char kTag[] = "FlashDsp"; +constexpr std::uint8_t kDspPartitionSubtype = 0x40U; + +[[nodiscard]] const esp_partition_t* dspPartition() +{ + return esp_partition_find_first(ESP_PARTITION_TYPE_DATA, + kDspPartitionSubtype, + "dsp"); +} + +[[nodiscard]] bool partitionLooksEmpty(std::span data) +{ + for (const std::uint8_t byte : data) { + if (byte != 0xFFU) { + return false; + } + } + return true; +} + +} // namespace + +std::expected +FlashDspProgramSource::loadProgram() +{ + const esp_partition_t* part = dspPartition(); + if (part == nullptr) { + ESP_LOGW(kTag, "dsp partition missing"); + return std::unexpected(core::DspProgramError::FlashReadFailed); + } + + std::vector buffer(part->size); + if (esp_partition_read(part, 0, buffer.data(), part->size) != ESP_OK) { + return std::unexpected(core::DspProgramError::FlashReadFailed); + } + if (partitionLooksEmpty(buffer)) { + return std::unexpected(core::DspProgramError::Empty); + } + + return core::parseDspProgramBlob(buffer); +} + +std::expected +FlashDspProgramSource::storeBlob(std::span blob) +{ + const esp_partition_t* part = dspPartition(); + if (part == nullptr || blob.size() > part->size) { + return std::unexpected(core::DspProgramError::FlashWriteFailed); + } + + if (esp_partition_erase_range(part, 0, part->size) != ESP_OK) { + return std::unexpected(core::DspProgramError::FlashWriteFailed); + } + if (esp_partition_write(part, 0, blob.data(), blob.size()) != ESP_OK) { + return std::unexpected(core::DspProgramError::FlashWriteFailed); + } + + ESP_LOGI(kTag, "stored %u byte DSP program blob", static_cast(blob.size())); + return {}; +} + +} // namespace adau1701 diff --git a/Software/components/drivers/adau1701/src/SigmaStudioFW.c b/Software/components/drivers/adau1701/src/SigmaStudioFW.c index 26d50a1..a02e89d 100644 --- a/Software/components/drivers/adau1701/src/SigmaStudioFW.c +++ b/Software/components/drivers/adau1701/src/SigmaStudioFW.c @@ -32,7 +32,7 @@ void sigma_studio_set_device(void* i2cDevHandle) void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, unsigned int address, - unsigned char length, + unsigned int length, ADI_REG_TYPE* pData) { (void)devAddress; @@ -42,11 +42,11 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, enum { kChunk = 64U }; unsigned int addr = address; - unsigned char remaining = length; + unsigned int remaining = length; ADI_REG_TYPE* cursor = pData; while (remaining > 0U) { - const unsigned char chunk = + const unsigned int chunk = remaining > kChunk ? kChunk : remaining; unsigned char buf[2U + 64U]; buf[0] = (unsigned char)((addr >> 8) & 0xFFU); @@ -55,7 +55,7 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress, i2c_master_transmit(s_dev, buf, (size_t)(2U + chunk), 1000); addr += chunk; cursor += chunk; - remaining = (unsigned char)(remaining - chunk); + remaining -= chunk; } } diff --git a/Software/components/drivers/adau1701/src/adau1701_program.c b/Software/components/drivers/adau1701/src/adau1701_program.c deleted file mode 100644 index f7fab9f..0000000 --- a/Software/components/drivers/adau1701/src/adau1701_program.c +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @file adau1701_program.c - * @brief SigmaStudio default download for DigiRadio ADAU1701. - * - * DigiRadio firmware — https://github.com/manvalan/DigiRadio - * - * Copyright 2026 Michele Bigi - * SPDX-License-Identifier: Apache-2.0 - * - * @author Michele Bigi - * @date 2026-07-06 - */ - -#include "SigmaStudioFW.h" -#include "DigiRadio_IC_1.h" - -void adau1701_run_default_download(void) -{ - default_download_IC_1(); -} diff --git a/Software/components/net/CMakeLists.txt b/Software/components/net/CMakeLists.txt index bd28a6c..160922a 100644 --- a/Software/components/net/CMakeLists.txt +++ b/Software/components/net/CMakeLists.txt @@ -7,7 +7,7 @@ idf_component_register( "src/NetBootstrap.cpp" INCLUDE_DIRS "include" EMBED_FILES "www/index.html.gz" - REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server mdns secure_store tuner audio bluetooth station integration bt1035 + REQUIRES core esp_wifi esp_netif esp_event nvs_flash esp_http_server mdns secure_store tuner audio bluetooth station integration bt1035 adau1701 ) target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23) diff --git a/Software/components/net/src/SetupWebServer.cpp b/Software/components/net/src/SetupWebServer.cpp index 6661aa2..4942065 100644 --- a/Software/components/net/src/SetupWebServer.cpp +++ b/Software/components/net/src/SetupWebServer.cpp @@ -22,6 +22,7 @@ #include "core/AudioProfileJson.hpp" #include "core/BluetoothJson.hpp" #include "core/CompanionChipStatus.hpp" +#include "core/DspProgramBlob.hpp" #include "core/FirmwareVersion.hpp" #include "core/HealthStatus.hpp" #include "core/HealthStatusJson.hpp" @@ -37,6 +38,7 @@ #include "bluetooth/BluetoothService.hpp" #include "station/StationService.hpp" #include "integration/IntegrationService.hpp" +#include "adau1701/FlashDspProgramSource.hpp" #include "bt1035/Bt1035Error.hpp" #include "esp_http_server.h" @@ -46,7 +48,9 @@ #include "freertos/task.h" #include +#include #include +#include namespace net { @@ -667,6 +671,68 @@ esp_err_t wifiPostHandler(httpd_req_t* req) return ESP_OK; } +/** + * @brief dspProgramPostHandler — store validated ADAU program blob to flash. + * + * @dname dspProgramPostHandler + * @param req HTTP request handle (raw application/octet-stream body). + * @return ESP_OK on success, or an esp_err_t error code. + * @pubstate writes the dsp partition; schedules reboot to apply on next boot. + * + * @author Michele Bigi + * @date 2026-07-07 + */ +esp_err_t dspProgramPostHandler(httpd_req_t* req) +{ + constexpr int kMaxBlobSize = 200 * 1024; + const int contentLen = req->content_len; + if (contentLen <= 0 || contentLen > kMaxBlobSize) { + httpd_resp_set_status(req, "413 Payload Too Large"); + return httpd_resp_send(req, nullptr, 0); + } + + std::vector body(static_cast(contentLen)); + int received = 0; + while (received < contentLen) { + const int chunk = httpd_req_recv(req, + reinterpret_cast(body.data()) + + received, + contentLen - received); + if (chunk <= 0) { + httpd_resp_set_status(req, "400 Bad Request"); + return httpd_resp_send(req, nullptr, 0); + } + received += chunk; + } + + const auto parsed = core::parseDspProgramBlob(body); + if (!parsed) { + const std::string json = std::string(R"({"error":")") + + core::dspProgramErrorToken(parsed.error()) + + R"("})"; + httpd_resp_set_status(req, "400 Bad Request"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + if (auto stored = adau1701::FlashDspProgramSource::storeBlob(body); !stored) { + const std::string json = std::string(R"({"error":")") + + core::dspProgramErrorToken(stored.error()) + + R"("})"; + httpd_resp_set_status(req, "500 Internal Server Error"); + httpd_resp_set_type(req, "application/json"); + return httpd_resp_send(req, json.c_str(), json.size()); + } + + const std::string json = + std::string(R"({"status":"stored","reboot_sec":)") + + std::to_string(kRebootDelaySec) + "}"; + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json.c_str(), json.size()); + xTaskCreate(rebootTask, "reboot", 2048, nullptr, 5, nullptr); + return ESP_OK; +} + esp_err_t bluetoothStatusGetHandler(httpd_req_t* req) { auto* ctx = routeContextFrom(req); @@ -1108,6 +1174,14 @@ std::expected SetupWebServer::start( }; httpd_register_uri_handler(server_, &audioBassEnhanceUri); + const httpd_uri_t dspProgramUri = { + .uri = "/api/dsp/program", + .method = HTTP_POST, + .handler = dspProgramPostHandler, + .user_ctx = routeCtx, + }; + httpd_register_uri_handler(server_, &dspProgramUri); + const httpd_uri_t bluetoothStatusUri = { .uri = "/api/bluetooth/status", .method = HTTP_GET, diff --git a/Software/docs/manual/ch-adau1701.tex b/Software/docs/manual/ch-adau1701.tex index e66f422..e6b8a73 100644 --- a/Software/docs/manual/ch-adau1701.tex +++ b/Software/docs/manual/ch-adau1701.tex @@ -121,7 +121,7 @@ the DSP program and writes it into RAM after each reset. \node[step, fill=black!6, below=of rst] (i2c) {Create I\textsuperscript{2}C master @ 100\,kHz, address 0x34}; \node[step, fill=black!8, below=of i2c] (dl) - {\texttt{default\_download\_IC\_1()} --- control, program, param RAM}; + {Replay \texttt{DspProgram} from flash or embedded export}; \node[step, fill=black!6, below=of dl] (ready) {\texttt{Adau1701Driver::isBooted()} = true}; \node[step, fill=black!10, below=of ready] (prof) @@ -135,7 +135,10 @@ the DSP program and writes it into RAM after each reset. \end{figure} \texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} streams each write in 64-byte I\textsuperscript{2}C -chunks. After download, \texttt{loadAndApply()} restores the saved +chunks (payloads may exceed 255 bytes). The active script comes from the +\texttt{dsp} data partition when a valid \texttt{DRAD} blob is present; otherwise +the embedded SigmaStudio export is used (Section~\ref{sec:api-dsp-program}). +After download, \texttt{loadAndApply()} restores the saved \texttt{core::AudioProfile} from NVS (or factory defaults) so user settings survive power cycles without re-writing the whole program. diff --git a/Software/docs/manual/ch-api.tex b/Software/docs/manual/ch-api.tex index 4994989..512e1ca 100644 --- a/Software/docs/manual/ch-api.tex +++ b/Software/docs/manual/ch-api.tex @@ -225,6 +225,26 @@ the ADAU1701 and writes NVS key \texttt{audio\_profile\_json}. HTTP status: \textbf{200 OK}; \textbf{400} for parse/validation failures; \textbf{500} when safeload or NVS persistence fails. +\subsection{\texttt{POST /api/dsp/program}} +\label{sec:api-dsp-program} + +Uploads a framed ADAU1701 RAM download blob (\texttt{DRAD} v1, see +\texttt{core::parseDspProgramBlob()}). The body is raw bytes +(\texttt{application/octet-stream} or any content type). On success the blob +is written to the \texttt{dsp} flash partition and the device reboots; the +next boot replays the new program before \texttt{loadAndApply()}. + +\begin{drnote}[Success response] +\begin{drcode}[JSON] +{"status":"stored","reboot_sec":3} +\end{drcode} +\end{drnote} + +HTTP status: \textbf{200 OK}; \textbf{400} with \texttt{\{"error":"..."\}} +for invalid/truncated/CRC failures; \textbf{413} when the body exceeds +200\,KiB; \textbf{500} on flash write failure. Pack blobs with +\texttt{tools/pack\_dsp\_program.py} or \texttt{core::serializeDspProgramBlob()}. + \subsection{\texttt{POST /api/audio/reset}} \label{sec:api-audio-reset} diff --git a/Software/docs/manual/ch-classes.tex b/Software/docs/manual/ch-classes.tex index 89a07f8..b90bdc2 100644 --- a/Software/docs/manual/ch-classes.tex +++ b/Software/docs/manual/ch-classes.tex @@ -183,6 +183,30 @@ audio control to safeload I\textsuperscript{2}C transactions without exposing parameter RAM addresses to services (Chapter~\ref{ch:adau1701}, Section~\ref{sec:adau1701-stack}). +\section{RegisterWrite}\label{cls:RegisterWrite} +One SigmaStudio \texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} step: 16-bit address +plus payload bytes. Aggregated by \texttt{DspProgram}. + +\section{DspProgram}\label{cls:DspProgram} +Ordered ADAU1701 RAM download script (pure domain). Parsed from a framed +\texttt{DRAD} flash blob or built by \texttt{EmbeddedDspProgramSource}. + +\section{IDspProgramSource}\label{cls:IDspProgramSource} +Port supplying \texttt{DspProgram} to \texttt{Adau1701Driver::boot()}. +Implementations: flash partition, embedded export, and fallback composite. + +\section{EmbeddedDspProgramSource}\label{cls:EmbeddedDspProgramSource} +Builds the factory \texttt{DspProgram} from \texttt{DigiRadio\_IC\_1.h} +(five writes matching \texttt{default\_download\_IC\_1()}). + +\section{FlashDspProgramSource}\label{cls:FlashDspProgramSource} +Reads and parses the \texttt{dsp} flash partition; \texttt{storeBlob()} erases +and writes a validated blob from \texttt{POST /api/dsp/program}. + +\section{FallbackDspProgramSource}\label{cls:FallbackDspProgramSource} +Tries \texttt{FlashDspProgramSource} first, then +\texttt{EmbeddedDspProgramSource} when the partition is empty or invalid. + % ------------------------------------------------------------------ % Domain core — audio (Slice 5) % ------------------------------------------------------------------ diff --git a/Software/main/hardware_bootstrap.cpp b/Software/main/hardware_bootstrap.cpp index 918b020..2afdf11 100644 --- a/Software/main/hardware_bootstrap.cpp +++ b/Software/main/hardware_bootstrap.cpp @@ -15,6 +15,9 @@ #include "adau1701/Adau1701Driver.hpp" #include "adau1701/Adau1701Dsp.hpp" +#include "adau1701/EmbeddedDspProgramSource.hpp" +#include "adau1701/FallbackDspProgramSource.hpp" +#include "adau1701/FlashDspProgramSource.hpp" #include "audio/AudioService.hpp" #include "board_pins.hpp" #include "bt1035/Bt1035Driver.hpp" @@ -51,13 +54,19 @@ si4684::Si4684Driver gSi4684( gImages.fmFirmware()); si4684::Si4684Tuner gSi4684Tuner(gSi4684); +adau1701::EmbeddedDspProgramSource gEmbeddedDspProgram; +adau1701::FlashDspProgramSource gFlashDspProgram; +adau1701::FallbackDspProgramSource gDspProgramSource( + gFlashDspProgram, gEmbeddedDspProgram); + adau1701::Adau1701Driver gAdau1701( adau1701::Adau1701Pins{ .i2cSda = board::pins::Adau1701Sda, .i2cScl = board::pins::Adau1701Scl, .resetGpio = board::pins::Adau1701Reset, .i2cAddr7 = board::pins::Adau1701Addr, - }); + }, + gDspProgramSource); adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701); secure_store::NvsAudioProfileStore gAudioStore; audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore); diff --git a/Software/tools/pack_dsp_program.py b/Software/tools/pack_dsp_program.py new file mode 100644 index 0000000..18d2ff3 --- /dev/null +++ b/Software/tools/pack_dsp_program.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""pack_dsp_program.py — build a DRAD v1 ADAU1701 program blob for POST /api/dsp/program. + +Reads a JSON file describing SigmaStudio register writes: + +[ + {"address": 2076, "data": [0, 28]}, + {"address": 1024, "data": [0, 0, ...]} +] + +Usage: + python3 tools/pack_dsp_program.py writes.json -o dsp_program.bin +""" + +from __future__ import annotations + +import argparse +import json +import struct +import sys +import zlib +from pathlib import Path +from typing import Any + +MAGIC = b"DRAD" +VERSION = 1 + + +def crc32(data: bytes) -> int: + return zlib.crc32(data) & 0xFFFFFFFF + + +def pack_writes(writes: list[dict[str, Any]]) -> bytes: + payload = bytearray() + for entry in writes: + address = int(entry["address"]) + data = bytes(int(x) & 0xFF for x in entry["data"]) + if not data: + raise ValueError(f"empty data at address 0x{address:04X}") + payload += struct.pack(" int: + parser = argparse.ArgumentParser(description="Pack ADAU1701 DSP program blob") + parser.add_argument("json_file", type=Path, help="JSON array of register writes") + parser.add_argument("-o", "--output", type=Path, required=True, help="Output .bin") + args = parser.parse_args() + + writes = json.loads(args.json_file.read_text(encoding="utf-8")) + if not isinstance(writes, list) or not writes: + print("JSON must be a non-empty array", file=sys.stderr) + return 1 + + blob = pack_writes(writes) + args.output.write_bytes(blob) + print(f"Wrote {len(blob)} bytes to {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())