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 <cursoragent@cursor.com>
This commit is contained in:
@@ -15,8 +15,13 @@ SigmaStudio project **DigiRadio**, IC1 = ADAU1701. Shipped with DigiRadio firmwa
|
|||||||
|
|
||||||
## Runtime (fw 0.8.3)
|
## Runtime (fw 0.8.3)
|
||||||
|
|
||||||
- **Boot:** ESP32 replays `default_download_IC_1()` over I2C via
|
- **Boot:** ESP32 replays the active `DspProgram` over I2C via
|
||||||
`adau1701::Adau1701Driver` on every power-up.
|
`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
|
- **Live control:** six-band PEQ, input mixer, master volume, stereo/bass
|
||||||
enhancement overlays — safeload via `audio::AudioService` and REST
|
enhancement overlays — safeload via `audio::AudioService` and REST
|
||||||
`/api/audio/*`; profile persisted in encrypted NVS.
|
`/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;
|
Do **not** use SigmaStudio *Link Compile Download* on DigiRadio — export only;
|
||||||
the ESP32 programs the DSP at every power-up.
|
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
|
## Documentation
|
||||||
|
|
||||||
- Design: `docs/manual/ch-sigmastudio.tex`
|
- Design: `docs/manual/ch-sigmastudio.tex`
|
||||||
|
|||||||
@@ -36,19 +36,14 @@ void sigma_studio_set_device(void* i2cDevHandle);
|
|||||||
*
|
*
|
||||||
* @param devAddress SigmaStudio device address (0x68 write addr).
|
* @param devAddress SigmaStudio device address (0x68 write addr).
|
||||||
* @param address 16-bit target address in DSP memory map.
|
* @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.
|
* @param pData Payload bytes.
|
||||||
*/
|
*/
|
||||||
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
||||||
unsigned int address,
|
unsigned int address,
|
||||||
unsigned char length,
|
unsigned int length,
|
||||||
ADI_REG_TYPE* pData);
|
ADI_REG_TYPE* pData);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Run the DigiRadio SigmaStudio default download sequence.
|
|
||||||
*/
|
|
||||||
void adau1701_run_default_download(void);
|
|
||||||
|
|
||||||
/** Safeload data register base (0x0810..0x0814). */
|
/** Safeload data register base (0x0810..0x0814). */
|
||||||
#define ADAU1701_SAFELOAD_DATA_BASE 0x0810U
|
#define ADAU1701_SAFELOAD_DATA_BASE 0x0810U
|
||||||
/** Safeload address register base (0x0815..0x0819). */
|
/** Safeload address register base (0x0815..0x0819). */
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ idf_component_register(
|
|||||||
"src/BroadcastLabel.cpp"
|
"src/BroadcastLabel.cpp"
|
||||||
"src/RdsMetadataAccumulator.cpp"
|
"src/RdsMetadataAccumulator.cpp"
|
||||||
"src/DabDynamicLabelAccumulator.cpp"
|
"src/DabDynamicLabelAccumulator.cpp"
|
||||||
|
"src/DspProgram.cpp"
|
||||||
|
"src/RegisterWrite.cpp"
|
||||||
|
"src/DspProgramBlob.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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 <vector>
|
||||||
|
|
||||||
|
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<RegisterWrite> 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<RegisterWrite>& writes() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<RegisterWrite> writes_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <expected>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<DspProgram, DspProgramError>
|
||||||
|
parseDspProgramBlob(std::span<const std::uint8_t> 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<std::uint8_t>
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -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 <expected>
|
||||||
|
|
||||||
|
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<DspProgram, DspProgramError>
|
||||||
|
loadProgram() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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<std::uint8_t> 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<const std::uint8_t> data() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::uint16_t address_;
|
||||||
|
std::vector<std::uint8_t> data_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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<RegisterWrite> writes)
|
||||||
|
: writes_(std::move(writes))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<RegisterWrite>& DspProgram::writes() const noexcept
|
||||||
|
{
|
||||||
|
return writes_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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 <array>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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<std::uint16_t>(p[0] | (static_cast<std::uint16_t>(p[1]) << 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t readLe32(const std::uint8_t* p)
|
||||||
|
{
|
||||||
|
return static_cast<std::uint32_t>(p[0])
|
||||||
|
| (static_cast<std::uint32_t>(p[1]) << 8)
|
||||||
|
| (static_cast<std::uint32_t>(p[2]) << 16)
|
||||||
|
| (static_cast<std::uint32_t>(p[3]) << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t crc32(std::span<const std::uint8_t> 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<const std::uint8_t> blob)
|
||||||
|
{
|
||||||
|
return blob.size() >= 4U
|
||||||
|
&& blob[0] == static_cast<std::uint8_t>(kMagic[0])
|
||||||
|
&& blob[1] == static_cast<std::uint8_t>(kMagic[1])
|
||||||
|
&& blob[2] == static_cast<std::uint8_t>(kMagic[2])
|
||||||
|
&& blob[3] == static_cast<std::uint8_t>(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<DspProgram, DspProgramError>
|
||||||
|
parseDspProgramBlob(std::span<const std::uint8_t> 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<const std::uint8_t> 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<RegisterWrite> 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<std::uint8_t> 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<std::uint8_t> serializeDspProgramBlob(const DspProgram& program)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
for (const RegisterWrite& write : program.writes()) {
|
||||||
|
const auto data = write.data();
|
||||||
|
payload.push_back(static_cast<std::uint8_t>(write.address() & 0xFFU));
|
||||||
|
payload.push_back(
|
||||||
|
static_cast<std::uint8_t>((write.address() >> 8) & 0xFFU));
|
||||||
|
const std::uint16_t length =
|
||||||
|
static_cast<std::uint16_t>(data.size());
|
||||||
|
payload.push_back(static_cast<std::uint8_t>(length & 0xFFU));
|
||||||
|
payload.push_back(static_cast<std::uint8_t>((length >> 8) & 0xFFU));
|
||||||
|
payload.insert(payload.end(), data.begin(), data.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> blob;
|
||||||
|
blob.reserve(kHeaderSize + payload.size());
|
||||||
|
blob.insert(blob.end(), kMagic, kMagic + 4);
|
||||||
|
blob.push_back(static_cast<std::uint8_t>(kVersion & 0xFFU));
|
||||||
|
blob.push_back(static_cast<std::uint8_t>((kVersion >> 8) & 0xFFU));
|
||||||
|
const std::uint16_t writeCount =
|
||||||
|
static_cast<std::uint16_t>(program.writes().size());
|
||||||
|
blob.push_back(static_cast<std::uint8_t>(writeCount & 0xFFU));
|
||||||
|
blob.push_back(static_cast<std::uint8_t>((writeCount >> 8) & 0xFFU));
|
||||||
|
const std::uint32_t payloadCrc = crc32(payload);
|
||||||
|
blob.push_back(static_cast<std::uint8_t>(payloadCrc & 0xFFU));
|
||||||
|
blob.push_back(static_cast<std::uint8_t>((payloadCrc >> 8) & 0xFFU));
|
||||||
|
blob.push_back(static_cast<std::uint8_t>((payloadCrc >> 16) & 0xFFU));
|
||||||
|
blob.push_back(static_cast<std::uint8_t>((payloadCrc >> 24) & 0xFFU));
|
||||||
|
blob.insert(blob.end(), payload.begin(), payload.end());
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -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<std::uint8_t> data)
|
||||||
|
: address_(address)
|
||||||
|
, data_(std::move(data))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint16_t RegisterWrite::address() const noexcept
|
||||||
|
{
|
||||||
|
return address_;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::span<const std::uint8_t> RegisterWrite::data() const noexcept
|
||||||
|
{
|
||||||
|
return data_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
@@ -46,11 +46,18 @@ add_library(digiradio_core STATIC
|
|||||||
"${CORE_SRC_DIR}/BroadcastLabel.cpp"
|
"${CORE_SRC_DIR}/BroadcastLabel.cpp"
|
||||||
"${CORE_SRC_DIR}/RdsMetadataAccumulator.cpp"
|
"${CORE_SRC_DIR}/RdsMetadataAccumulator.cpp"
|
||||||
"${CORE_SRC_DIR}/DabDynamicLabelAccumulator.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
|
target_include_directories(digiradio_core PUBLIC
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/../include"
|
"${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)
|
add_executable(device_identity_test device_identity_test.cpp)
|
||||||
target_link_libraries(device_identity_test PRIVATE digiradio_core)
|
target_link_libraries(device_identity_test PRIVATE digiradio_core)
|
||||||
add_test(NAME device_identity_test COMMAND device_identity_test)
|
add_test(NAME device_identity_test COMMAND device_identity_test)
|
||||||
|
|||||||
@@ -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 <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] core::DspProgram sampleProgram()
|
||||||
|
{
|
||||||
|
return core::DspProgram(std::vector<core::RegisterWrite>{
|
||||||
|
core::RegisterWrite(0x081CU, {0x00U, 0x1CU}),
|
||||||
|
core::RegisterWrite(0x0400U, std::vector<std::uint8_t>(16U, 0xABU)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] int runRoundTripTest()
|
||||||
|
{
|
||||||
|
const core::DspProgram original = sampleProgram();
|
||||||
|
const std::vector<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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<std::uint8_t> 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;
|
||||||
|
}
|
||||||
@@ -4,12 +4,14 @@ idf_component_register(
|
|||||||
SRCS
|
SRCS
|
||||||
"src/Adau1701Driver.cpp"
|
"src/Adau1701Driver.cpp"
|
||||||
"src/Adau1701Dsp.cpp"
|
"src/Adau1701Dsp.cpp"
|
||||||
|
"src/EmbeddedDspProgramSource.cpp"
|
||||||
|
"src/FlashDspProgramSource.cpp"
|
||||||
|
"src/FallbackDspProgramSource.cpp"
|
||||||
"src/SigmaStudioFW.c"
|
"src/SigmaStudioFW.c"
|
||||||
"src/adau1701_program.c"
|
|
||||||
INCLUDE_DIRS
|
INCLUDE_DIRS
|
||||||
"include"
|
"include"
|
||||||
"${ADAU_FW_DIR}"
|
"${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)
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include "core/EqProfile.hpp"
|
#include "core/EqProfile.hpp"
|
||||||
#include "core/FrequencyHz.hpp"
|
#include "core/FrequencyHz.hpp"
|
||||||
#include "core/GainDb.hpp"
|
#include "core/GainDb.hpp"
|
||||||
|
#include "core/IDspProgramSource.hpp"
|
||||||
#include "core/MixSource.hpp"
|
#include "core/MixSource.hpp"
|
||||||
#include "core/MixerState.hpp"
|
#include "core/MixerState.hpp"
|
||||||
|
|
||||||
@@ -48,10 +49,11 @@ struct Adau1701Pins {
|
|||||||
* @brief Adau1701Driver — owns I2C + reset, boot, and safeload runtime.
|
* @brief Adau1701Driver — owns I2C + reset, boot, and safeload runtime.
|
||||||
*
|
*
|
||||||
* @dname Adau1701Driver
|
* @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)
|
* @return n/a (type)
|
||||||
* @pubstate Owns I2C bus/device handles (RAII). booted_ true after a
|
* @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
|
* Writes the SigmaStudio export from Firmware/ADAU1701-Firmware on every
|
||||||
* boot (no EEPROM self-boot on DigiRadio). Runtime EQ and mixer changes
|
* boot (no EEPROM self-boot on DigiRadio). Runtime EQ and mixer changes
|
||||||
@@ -63,16 +65,18 @@ struct Adau1701Pins {
|
|||||||
class Adau1701Driver {
|
class Adau1701Driver {
|
||||||
public:
|
public:
|
||||||
/**
|
/**
|
||||||
* @brief Adau1701Driver — construct with board pin map.
|
* @brief Adau1701Driver — construct with board pin map and program source.
|
||||||
*
|
*
|
||||||
* @dname Adau1701Driver
|
* @dname Adau1701Driver
|
||||||
* @param pins SDA/SCL/reset/address configuration.
|
* @param pins SDA/SCL/reset/address configuration.
|
||||||
* @pubstate stores pins_; not booted until boot().
|
* @param programSource Download script provider for boot().
|
||||||
|
* @pubstate stores pins_ and programSource_; not booted until boot().
|
||||||
*
|
*
|
||||||
* @author Michele Bigi
|
* @author Michele Bigi
|
||||||
* @date 2026-07-06
|
* @date 2026-07-06
|
||||||
*/
|
*/
|
||||||
explicit Adau1701Driver(Adau1701Pins pins);
|
explicit Adau1701Driver(Adau1701Pins pins,
|
||||||
|
core::IDspProgramSource& programSource);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief ~Adau1701Driver — release I2C resources.
|
* @brief ~Adau1701Driver — release I2C resources.
|
||||||
@@ -222,8 +226,11 @@ private:
|
|||||||
unsigned paramAddr, core::GainDb gain);
|
unsigned paramAddr, core::GainDb gain);
|
||||||
[[nodiscard]] std::expected<void, Adau1701Error> safeloadFixpoint(
|
[[nodiscard]] std::expected<void, Adau1701Error> safeloadFixpoint(
|
||||||
unsigned paramAddr, std::int32_t fixpoint);
|
unsigned paramAddr, std::int32_t fixpoint);
|
||||||
|
[[nodiscard]] std::expected<void, Adau1701Error> replayProgram(
|
||||||
|
const core::DspProgram& program);
|
||||||
|
|
||||||
Adau1701Pins pins_;
|
Adau1701Pins pins_;
|
||||||
|
core::IDspProgramSource& programSource_;
|
||||||
bool booted_;
|
bool booted_;
|
||||||
void* i2cBus_;
|
void* i2cBus_;
|
||||||
void* i2cDev_;
|
void* i2cDev_;
|
||||||
|
|||||||
@@ -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<core::DspProgram, core::DspProgramError>
|
||||||
|
loadProgram() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace adau1701
|
||||||
@@ -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<core::DspProgram, core::DspProgramError>
|
||||||
|
loadProgram() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::IDspProgramSource& primary_;
|
||||||
|
core::IDspProgramSource& fallback_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace adau1701
|
||||||
@@ -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 <cstdint>
|
||||||
|
#include <span>
|
||||||
|
|
||||||
|
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<core::DspProgram, core::DspProgramError>
|
||||||
|
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<void, core::DspProgramError>
|
||||||
|
storeBlob(std::span<const std::uint8_t> blob);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace adau1701
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "adau1701/Adau1701ParamMap.hpp"
|
#include "adau1701/Adau1701ParamMap.hpp"
|
||||||
|
|
||||||
#include "core/BiquadDesign.hpp"
|
#include "core/BiquadDesign.hpp"
|
||||||
|
#include "core/DspProgram.hpp"
|
||||||
|
|
||||||
#include "DigiRadio_IC_1_PARAM.h"
|
#include "DigiRadio_IC_1_PARAM.h"
|
||||||
#include "SigmaStudioFW.h"
|
#include "SigmaStudioFW.h"
|
||||||
@@ -28,9 +29,6 @@
|
|||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
|
||||||
/** @brief SigmaStudio default program download (generated C, not part of the C++ API). */
|
|
||||||
void adau1701_run_default_download(void);
|
|
||||||
|
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
|
|
||||||
namespace adau1701 {
|
namespace adau1701 {
|
||||||
@@ -42,8 +40,10 @@ constexpr int kI2cPort = 0;
|
|||||||
constexpr std::uint8_t kFixedHighPassBandIndex = 0U;
|
constexpr std::uint8_t kFixedHighPassBandIndex = 0U;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Adau1701Driver::Adau1701Driver(Adau1701Pins pins)
|
Adau1701Driver::Adau1701Driver(Adau1701Pins pins,
|
||||||
|
core::IDspProgramSource& programSource)
|
||||||
: pins_(pins)
|
: pins_(pins)
|
||||||
|
, programSource_(programSource)
|
||||||
, booted_(false)
|
, booted_(false)
|
||||||
, i2cBus_(nullptr)
|
, i2cBus_(nullptr)
|
||||||
, i2cDev_(nullptr)
|
, i2cDev_(nullptr)
|
||||||
@@ -62,6 +62,26 @@ Adau1701Driver::~Adau1701Driver()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::expected<void, Adau1701Error> Adau1701Driver::replayProgram(
|
||||||
|
const core::DspProgram& program)
|
||||||
|
{
|
||||||
|
const unsigned char deviceAddr =
|
||||||
|
static_cast<unsigned char>(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<unsigned int>(data.size()),
|
||||||
|
const_cast<ADI_REG_TYPE*>(
|
||||||
|
reinterpret_cast<const ADI_REG_TYPE*>(data.data())));
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
std::expected<void, Adau1701Error> Adau1701Driver::boot()
|
std::expected<void, Adau1701Error> Adau1701Driver::boot()
|
||||||
{
|
{
|
||||||
if (booted_) {
|
if (booted_) {
|
||||||
@@ -110,7 +130,14 @@ std::expected<void, Adau1701Error> Adau1701Driver::boot()
|
|||||||
sigma_studio_bind_i2c(kI2cPort, static_cast<unsigned char>(pins_.i2cAddr7));
|
sigma_studio_bind_i2c(kI2cPort, static_cast<unsigned char>(pins_.i2cAddr7));
|
||||||
sigma_studio_set_device(dev);
|
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;
|
booted_ = true;
|
||||||
ESP_LOGI(kTag, "SigmaStudio program loaded");
|
ESP_LOGI(kTag, "SigmaStudio program loaded");
|
||||||
|
|||||||
@@ -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 <vector>
|
||||||
|
|
||||||
|
namespace adau1701 {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
[[nodiscard]] std::vector<std::uint8_t> copyBytes(const ADI_REG_TYPE* data,
|
||||||
|
std::size_t size)
|
||||||
|
{
|
||||||
|
return std::vector<std::uint8_t>(
|
||||||
|
reinterpret_cast<const std::uint8_t*>(data),
|
||||||
|
reinterpret_cast<const std::uint8_t*>(data) + size);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::expected<core::DspProgram, core::DspProgramError>
|
||||||
|
EmbeddedDspProgramSource::loadProgram()
|
||||||
|
{
|
||||||
|
std::vector<core::RegisterWrite> writes;
|
||||||
|
writes.reserve(5U);
|
||||||
|
|
||||||
|
writes.emplace_back(
|
||||||
|
static_cast<std::uint16_t>(REG_COREREGISTER_IC_1_ADDR),
|
||||||
|
copyBytes(R0_COREREGISTER_IC_1_Default, REG_COREREGISTER_IC_1_BYTE));
|
||||||
|
writes.emplace_back(
|
||||||
|
static_cast<std::uint16_t>(PROGRAM_ADDR_IC_1),
|
||||||
|
copyBytes(Program_Data_IC_1, PROGRAM_SIZE_IC_1));
|
||||||
|
writes.emplace_back(
|
||||||
|
static_cast<std::uint16_t>(PARAM_ADDR_IC_1),
|
||||||
|
copyBytes(Param_Data_IC_1, PARAM_SIZE_IC_1));
|
||||||
|
writes.emplace_back(
|
||||||
|
static_cast<std::uint16_t>(REG_COREREGISTER_IC_1_ADDR),
|
||||||
|
copyBytes(R3_HWCONFIGURATION_IC_1_Default, R3_HWCONFIGURATION_IC_1_SIZE));
|
||||||
|
writes.emplace_back(
|
||||||
|
static_cast<std::uint16_t>(REG_COREREGISTER_IC_1_ADDR),
|
||||||
|
copyBytes(R4_COREREGISTER_IC_1_Default, REG_COREREGISTER_IC_1_BYTE));
|
||||||
|
|
||||||
|
return core::DspProgram(std::move(writes));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace adau1701
|
||||||
@@ -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<core::DspProgram, core::DspProgramError>
|
||||||
|
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
|
||||||
@@ -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 <vector>
|
||||||
|
|
||||||
|
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<const std::uint8_t> data)
|
||||||
|
{
|
||||||
|
for (const std::uint8_t byte : data) {
|
||||||
|
if (byte != 0xFFU) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::expected<core::DspProgram, core::DspProgramError>
|
||||||
|
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<std::uint8_t> 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<void, core::DspProgramError>
|
||||||
|
FlashDspProgramSource::storeBlob(std::span<const std::uint8_t> 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<unsigned>(blob.size()));
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace adau1701
|
||||||
@@ -32,7 +32,7 @@ void sigma_studio_set_device(void* i2cDevHandle)
|
|||||||
|
|
||||||
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
||||||
unsigned int address,
|
unsigned int address,
|
||||||
unsigned char length,
|
unsigned int length,
|
||||||
ADI_REG_TYPE* pData)
|
ADI_REG_TYPE* pData)
|
||||||
{
|
{
|
||||||
(void)devAddress;
|
(void)devAddress;
|
||||||
@@ -42,11 +42,11 @@ void SIGMA_WRITE_REGISTER_BLOCK(unsigned char devAddress,
|
|||||||
|
|
||||||
enum { kChunk = 64U };
|
enum { kChunk = 64U };
|
||||||
unsigned int addr = address;
|
unsigned int addr = address;
|
||||||
unsigned char remaining = length;
|
unsigned int remaining = length;
|
||||||
ADI_REG_TYPE* cursor = pData;
|
ADI_REG_TYPE* cursor = pData;
|
||||||
|
|
||||||
while (remaining > 0U) {
|
while (remaining > 0U) {
|
||||||
const unsigned char chunk =
|
const unsigned int chunk =
|
||||||
remaining > kChunk ? kChunk : remaining;
|
remaining > kChunk ? kChunk : remaining;
|
||||||
unsigned char buf[2U + 64U];
|
unsigned char buf[2U + 64U];
|
||||||
buf[0] = (unsigned char)((addr >> 8) & 0xFFU);
|
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);
|
i2c_master_transmit(s_dev, buf, (size_t)(2U + chunk), 1000);
|
||||||
addr += chunk;
|
addr += chunk;
|
||||||
cursor += chunk;
|
cursor += chunk;
|
||||||
remaining = (unsigned char)(remaining - chunk);
|
remaining -= chunk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ idf_component_register(
|
|||||||
"src/NetBootstrap.cpp"
|
"src/NetBootstrap.cpp"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
EMBED_FILES "www/index.html.gz"
|
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)
|
target_compile_features(${COMPONENT_LIB} PUBLIC cxx_std_23)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include "core/AudioProfileJson.hpp"
|
#include "core/AudioProfileJson.hpp"
|
||||||
#include "core/BluetoothJson.hpp"
|
#include "core/BluetoothJson.hpp"
|
||||||
#include "core/CompanionChipStatus.hpp"
|
#include "core/CompanionChipStatus.hpp"
|
||||||
|
#include "core/DspProgramBlob.hpp"
|
||||||
#include "core/FirmwareVersion.hpp"
|
#include "core/FirmwareVersion.hpp"
|
||||||
#include "core/HealthStatus.hpp"
|
#include "core/HealthStatus.hpp"
|
||||||
#include "core/HealthStatusJson.hpp"
|
#include "core/HealthStatusJson.hpp"
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
#include "bluetooth/BluetoothService.hpp"
|
#include "bluetooth/BluetoothService.hpp"
|
||||||
#include "station/StationService.hpp"
|
#include "station/StationService.hpp"
|
||||||
#include "integration/IntegrationService.hpp"
|
#include "integration/IntegrationService.hpp"
|
||||||
|
#include "adau1701/FlashDspProgramSource.hpp"
|
||||||
#include "bt1035/Bt1035Error.hpp"
|
#include "bt1035/Bt1035Error.hpp"
|
||||||
|
|
||||||
#include "esp_http_server.h"
|
#include "esp_http_server.h"
|
||||||
@@ -46,7 +48,9 @@
|
|||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace net {
|
namespace net {
|
||||||
|
|
||||||
@@ -667,6 +671,68 @@ esp_err_t wifiPostHandler(httpd_req_t* req)
|
|||||||
return ESP_OK;
|
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<std::uint8_t> body(static_cast<std::size_t>(contentLen));
|
||||||
|
int received = 0;
|
||||||
|
while (received < contentLen) {
|
||||||
|
const int chunk = httpd_req_recv(req,
|
||||||
|
reinterpret_cast<char*>(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)
|
esp_err_t bluetoothStatusGetHandler(httpd_req_t* req)
|
||||||
{
|
{
|
||||||
auto* ctx = routeContextFrom(req);
|
auto* ctx = routeContextFrom(req);
|
||||||
@@ -1108,6 +1174,14 @@ std::expected<void, NetError> SetupWebServer::start(
|
|||||||
};
|
};
|
||||||
httpd_register_uri_handler(server_, &audioBassEnhanceUri);
|
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 = {
|
const httpd_uri_t bluetoothStatusUri = {
|
||||||
.uri = "/api/bluetooth/status",
|
.uri = "/api/bluetooth/status",
|
||||||
.method = HTTP_GET,
|
.method = HTTP_GET,
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ the DSP program and writes it into RAM after each reset.
|
|||||||
\node[step, fill=black!6, below=of rst] (i2c)
|
\node[step, fill=black!6, below=of rst] (i2c)
|
||||||
{Create I\textsuperscript{2}C master @ 100\,kHz, address 0x34};
|
{Create I\textsuperscript{2}C master @ 100\,kHz, address 0x34};
|
||||||
\node[step, fill=black!8, below=of i2c] (dl)
|
\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)
|
\node[step, fill=black!6, below=of dl] (ready)
|
||||||
{\texttt{Adau1701Driver::isBooted()} = true};
|
{\texttt{Adau1701Driver::isBooted()} = true};
|
||||||
\node[step, fill=black!10, below=of ready] (prof)
|
\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}
|
\end{figure}
|
||||||
|
|
||||||
\texttt{SIGMA\_WRITE\_REGISTER\_BLOCK} streams each write in 64-byte I\textsuperscript{2}C
|
\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
|
\texttt{core::AudioProfile} from NVS (or factory defaults) so user settings
|
||||||
survive power cycles without re-writing the whole program.
|
survive power cycles without re-writing the whole program.
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
HTTP status: \textbf{200 OK}; \textbf{400} for parse/validation failures;
|
||||||
\textbf{500} when safeload or NVS persistence fails.
|
\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}}
|
\subsection{\texttt{POST /api/audio/reset}}
|
||||||
\label{sec:api-audio-reset}
|
\label{sec:api-audio-reset}
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,30 @@ audio control to safeload I\textsuperscript{2}C transactions without exposing
|
|||||||
parameter RAM addresses to services (Chapter~\ref{ch:adau1701},
|
parameter RAM addresses to services (Chapter~\ref{ch:adau1701},
|
||||||
Section~\ref{sec:adau1701-stack}).
|
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)
|
% Domain core — audio (Slice 5)
|
||||||
% ------------------------------------------------------------------
|
% ------------------------------------------------------------------
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
#include "adau1701/Adau1701Driver.hpp"
|
#include "adau1701/Adau1701Driver.hpp"
|
||||||
#include "adau1701/Adau1701Dsp.hpp"
|
#include "adau1701/Adau1701Dsp.hpp"
|
||||||
|
#include "adau1701/EmbeddedDspProgramSource.hpp"
|
||||||
|
#include "adau1701/FallbackDspProgramSource.hpp"
|
||||||
|
#include "adau1701/FlashDspProgramSource.hpp"
|
||||||
#include "audio/AudioService.hpp"
|
#include "audio/AudioService.hpp"
|
||||||
#include "board_pins.hpp"
|
#include "board_pins.hpp"
|
||||||
#include "bt1035/Bt1035Driver.hpp"
|
#include "bt1035/Bt1035Driver.hpp"
|
||||||
@@ -51,13 +54,19 @@ si4684::Si4684Driver gSi4684(
|
|||||||
gImages.fmFirmware());
|
gImages.fmFirmware());
|
||||||
si4684::Si4684Tuner gSi4684Tuner(gSi4684);
|
si4684::Si4684Tuner gSi4684Tuner(gSi4684);
|
||||||
|
|
||||||
|
adau1701::EmbeddedDspProgramSource gEmbeddedDspProgram;
|
||||||
|
adau1701::FlashDspProgramSource gFlashDspProgram;
|
||||||
|
adau1701::FallbackDspProgramSource gDspProgramSource(
|
||||||
|
gFlashDspProgram, gEmbeddedDspProgram);
|
||||||
|
|
||||||
adau1701::Adau1701Driver gAdau1701(
|
adau1701::Adau1701Driver gAdau1701(
|
||||||
adau1701::Adau1701Pins{
|
adau1701::Adau1701Pins{
|
||||||
.i2cSda = board::pins::Adau1701Sda,
|
.i2cSda = board::pins::Adau1701Sda,
|
||||||
.i2cScl = board::pins::Adau1701Scl,
|
.i2cScl = board::pins::Adau1701Scl,
|
||||||
.resetGpio = board::pins::Adau1701Reset,
|
.resetGpio = board::pins::Adau1701Reset,
|
||||||
.i2cAddr7 = board::pins::Adau1701Addr,
|
.i2cAddr7 = board::pins::Adau1701Addr,
|
||||||
});
|
},
|
||||||
|
gDspProgramSource);
|
||||||
adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701);
|
adau1701::Adau1701Dsp gAdau1701Dsp(gAdau1701);
|
||||||
secure_store::NvsAudioProfileStore gAudioStore;
|
secure_store::NvsAudioProfileStore gAudioStore;
|
||||||
audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore);
|
audio::AudioService gAudioService(gAdau1701Dsp, &gAudioStore);
|
||||||
|
|||||||
@@ -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("<HH", address, len(data))
|
||||||
|
payload += data
|
||||||
|
|
||||||
|
header = bytearray()
|
||||||
|
header += MAGIC
|
||||||
|
header += struct.pack("<HH", VERSION, len(writes))
|
||||||
|
header += struct.pack("<I", crc32(payload))
|
||||||
|
return bytes(header + payload)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> 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())
|
||||||
Reference in New Issue
Block a user