Fix ADAU1701 SerialInputRegister IBP override causing persistent hiss
The 2026-08-16 IBP=1 override (commit 6974095) was applied alongside a
separate, simultaneous fix to Si4684's PIN_CONFIG_ENABLE and credited
with turning static into audible music. SerialInputRegister (0x081F) is
a single register shared by every SDATA_INx pin on the ADAU1701, so the
override also applied to the ESP32 streaming input, not just Si4684's.
Live A/B testing today (DAB, FM, and ESP32 web-radio, all isolated via
mixer gain, with and without ADAU1701 DSP bypass, with and without BT
A2DP codec changes) narrowed the hiss to this one shared register.
Removing the override and leaving IBP at its compiled default (0x00)
resolved the hiss on both the Si4684 and ESP32 paths, confirmed by ear.
Also:
- Add a runtime HTTP API (GET/POST /api/bluetooth/a2dp-codec) to change
the BT1035 A2DP codec bitmask without reflashing, used to rule out
AAC/SBC codec choice as a contributing cause.
- Extend the ADAU1701 boot-time EQ diagnostic to read back all 6 bands
(previously band 0 only) using the correct 5.23 fixed-point format.
- Note EEPROM persistence for ANTCAP/crystal calibration as TODO.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,34 @@ struct BluetoothConnectRequest {
|
||||
[[nodiscard]] std::expected<std::uint8_t, ParseError>
|
||||
parseBluetoothAutoReconnectJson(std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief parseBluetoothA2dpCodecConfigJson — validate POST codec_mask field.
|
||||
*
|
||||
* @dname parseBluetoothA2dpCodecConfigJson
|
||||
* @param json Request body with \c codec_mask 0–63 (see AT+A2DPCFG bits).
|
||||
* @return Bitmask on success, or ParseError.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
[[nodiscard]] std::expected<std::uint8_t, ParseError>
|
||||
parseBluetoothA2dpCodecConfigJson(std::string_view json);
|
||||
|
||||
/**
|
||||
* @brief serializeBluetoothA2dpCodecJson — negotiated codec for HTTP.
|
||||
*
|
||||
* @dname serializeBluetoothA2dpCodecJson
|
||||
* @param codec Parsed negotiated codec (AT+A2DPENC reply).
|
||||
* @return JSON object \c {"codec":"..."}.
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
[[nodiscard]] std::string serializeBluetoothA2dpCodecJson(
|
||||
Bt1035A2dpCodec codec);
|
||||
|
||||
/**
|
||||
* @brief serializeBluetoothScanJson — serialise scan result list.
|
||||
*
|
||||
|
||||
@@ -40,7 +40,8 @@ enum class Bt1035AtCommand {
|
||||
Reset, ///< AT+RESET — software reset (best-effort, not gated on OK).
|
||||
Ping, ///< AT — link check.
|
||||
I2sMode, ///< AT+AUXCFG=3 — I2S input from ADAU1701 (mandatory).
|
||||
I2sSlave48k24, ///< AT+I2SCFG=35 — I2S slave 48 kHz 24-bit (§5.1.4).
|
||||
I2sSlave48k32, ///< AT+I2SCFG=67 — I2S slave 48 kHz 32-bit (§5.1.4).
|
||||
A2dpCodecConfig, ///< AT+A2DPCFG=1 — enable AAC alongside mandatory SBC (§5.3.4).
|
||||
PairDiscoverable, ///< AT+PAIR=1 — enter BR/EDR/BLE discoverable mode.
|
||||
PairHidden, ///< AT+PAIR=0 — leave discoverable mode.
|
||||
A2dpStat, ///< AT+A2DPSTAT — read A2DP link state.
|
||||
@@ -105,16 +106,40 @@ enum class Bt1035AtResponseKind {
|
||||
};
|
||||
|
||||
/** Number of commands in bootInitSequence(). */
|
||||
inline constexpr std::size_t kBt1035BootInitCommandCount = 3U;
|
||||
inline constexpr std::size_t kBt1035BootInitCommandCount = 4U;
|
||||
|
||||
/**
|
||||
* Feasycom programming guide §5.1.4: I2S slave, 48 kHz, 24-bit — matches the
|
||||
* ADAU1701 serial output word length (SigmaStudio Hardware Configuration).
|
||||
* Bit field: BIT[0]=enable(1), BIT[1]=slave(1), BIT[2]=FS 48kHz(0),
|
||||
* BIT[3]=left justified(0), BIT[4]=data 1-bit delay(0), BIT[5:6]=24-bit(01)
|
||||
* -> 1 + 2 + 32 = 35.
|
||||
* Feasycom programming guide §5.3.4: AT+A2DPCFG bit field enables optional
|
||||
* codecs on top of the mandatory baseline SBC (BIT0=AAC, BIT1=aptX,
|
||||
* BIT2=aptX-LL, BIT3=aptX-HD, BIT4=aptX-Adaptive, BIT5=LDAC; BT1035 does
|
||||
* not support BT806's FastStream). Never sent before 2026-08-24, so every
|
||||
* A2DP link negotiated SBC only regardless of what the paired speaker
|
||||
* supports. Value 1 = AAC only, matching the guide's own worked example
|
||||
* -- picked over a wider bitmask because AAC is the most broadly
|
||||
* supported optional codec on consumer speakers (native on iOS) and this
|
||||
* is the datasheet-validated example, not an untested combination.
|
||||
*/
|
||||
inline constexpr std::uint8_t kBt1035I2sSlave48k24Param = 35U;
|
||||
inline constexpr std::uint8_t kBt1035A2dpCodecConfigParam = 1U;
|
||||
|
||||
/**
|
||||
* Feasycom programming guide §5.1.4: I2S slave, 48 kHz, 32-bit -> value 67.
|
||||
* This is one of only two configurations the guide validates with explicit
|
||||
* bit-clock math (the other being value 3, 16-bit); 24-bit (35, used here
|
||||
* until 2026-08-24) is not a documented example. Matches the ADAU1701's
|
||||
* actual physical output framing: the Serial Output Control Register
|
||||
* (0x081E, R15_BCLK_FREQ/R15_LRCLK_FREQ fields in the compiled SigmaStudio
|
||||
* export) fixes BCLK=3.072 MHz / LRCLK=48 kHz regardless of the OWL
|
||||
* "word length" field -- i.e. a 64-BCLK-cycle (32-bit) frame is what's
|
||||
* physically on the wire no matter what OWL says, so 67 is the value that
|
||||
* matches reality; 35 (24-bit) silently regressed in commit 6f7b6dd
|
||||
* (2026-08-08, an unrelated large feature commit) and was found to
|
||||
* contradict every other citation of this value in the repo
|
||||
* (instructions.md, README.md, CLAUDE.md, AGENTS.md, docs/TODO.md).
|
||||
* Bit field: BIT[0]=enable(1), BIT[1]=slave(1), BIT[2]=FS 48kHz(0),
|
||||
* BIT[3]=left justified(0), BIT[4]=data 1-bit delay(0), BIT[5:6]=32-bit(10)
|
||||
* -> 1 + 2 + 64 = 67.
|
||||
*/
|
||||
inline constexpr std::uint8_t kBt1035I2sSlave48k32Param = 67U;
|
||||
|
||||
/**
|
||||
* @brief buildBt1035AtLine — serialise a command with CRLF terminator.
|
||||
@@ -133,7 +158,7 @@ inline constexpr std::uint8_t kBt1035I2sSlave48k24Param = 35U;
|
||||
* @brief bootInitSequence — mandatory bring-up commands in order.
|
||||
*
|
||||
* @dname bootInitSequence
|
||||
* @return Ping, I2sMode (AUXCFG=3), I2sSlave48k24 (I2SCFG=35).
|
||||
* @return Ping, I2sMode (AUXCFG=3), I2sSlave48k32 (I2SCFG=67).
|
||||
* @pubstate none
|
||||
*
|
||||
* @author Michele Bigi
|
||||
@@ -210,6 +235,23 @@ parseBt1035A2dpEncoderResponse(std::string_view response);
|
||||
*/
|
||||
[[nodiscard]] const char* a2dpCodecToken(Bt1035A2dpCodec codec) noexcept;
|
||||
|
||||
/**
|
||||
* @brief buildBt1035A2dpCodecConfigLine — AT+A2DPCFG with runtime bitmask.
|
||||
*
|
||||
* @dname buildBt1035A2dpCodecConfigLine
|
||||
* @param bitmask BIT0=AAC, BIT1=aptX, BIT2=aptX-LL, BIT3=aptX-HD,
|
||||
* BIT4=aptX-Adaptive, BIT5=LDAC (§5.3.4); clamped to 0-63.
|
||||
* 0 disables every optional codec (SBC-only baseline).
|
||||
* @return Full AT line including CRLF.
|
||||
* @pubstate Takes effect on the next A2DP negotiation, not an already
|
||||
* streaming link — the peer must reconnect for the change to
|
||||
* apply.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
[[nodiscard]] std::string buildBt1035A2dpCodecConfigLine(std::uint8_t bitmask);
|
||||
|
||||
/**
|
||||
* @brief buildBt1035SetAutoConnLine — AT+AUTOCONN with reconnect count.
|
||||
*
|
||||
|
||||
@@ -96,6 +96,33 @@ parseBluetoothAutoReconnectJson(std::string_view json)
|
||||
return static_cast<std::uint8_t>(raw);
|
||||
}
|
||||
|
||||
std::expected<std::uint8_t, ParseError>
|
||||
parseBluetoothA2dpCodecConfigJson(std::string_view json)
|
||||
{
|
||||
if (json.find('{') == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
const std::string needle = "\"codec_mask\":";
|
||||
const std::size_t start = json.find(needle);
|
||||
if (start == std::string_view::npos) {
|
||||
return std::unexpected(ParseError::MissingField);
|
||||
}
|
||||
char* end = nullptr;
|
||||
const unsigned long raw =
|
||||
std::strtoul(json.data() + start + needle.size(), &end, 10);
|
||||
if (end == json.data() + start + needle.size() || raw > 63U) {
|
||||
return std::unexpected(ParseError::InvalidJson);
|
||||
}
|
||||
return static_cast<std::uint8_t>(raw);
|
||||
}
|
||||
|
||||
std::string serializeBluetoothA2dpCodecJson(Bt1035A2dpCodec codec)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "{\"codec\":\"" << a2dpCodecToken(codec) << "\"}";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string serializeBluetoothScanJson(
|
||||
const std::vector<Bt1035ScannedDevice>& devices)
|
||||
{
|
||||
|
||||
@@ -106,8 +106,10 @@ std::string buildBt1035AtLine(Bt1035AtCommand command)
|
||||
return "AT\r\n";
|
||||
case Bt1035AtCommand::I2sMode:
|
||||
return "AT+AUXCFG=3\r\n";
|
||||
case Bt1035AtCommand::I2sSlave48k24:
|
||||
return "AT+I2SCFG=35\r\n";
|
||||
case Bt1035AtCommand::I2sSlave48k32:
|
||||
return "AT+I2SCFG=67\r\n";
|
||||
case Bt1035AtCommand::A2dpCodecConfig:
|
||||
return buildBt1035A2dpCodecConfigLine(kBt1035A2dpCodecConfigParam);
|
||||
case Bt1035AtCommand::PairDiscoverable:
|
||||
return "AT+PAIR=1\r\n";
|
||||
case Bt1035AtCommand::PairHidden:
|
||||
@@ -128,6 +130,14 @@ std::string buildBt1035AtLine(Bt1035AtCommand command)
|
||||
return "AT\r\n";
|
||||
}
|
||||
|
||||
std::string buildBt1035A2dpCodecConfigLine(std::uint8_t bitmask)
|
||||
{
|
||||
if (bitmask > 63U) {
|
||||
bitmask = 63U;
|
||||
}
|
||||
return "AT+A2DPCFG=" + std::to_string(bitmask) + "\r\n";
|
||||
}
|
||||
|
||||
std::string buildBt1035SetAutoConnLine(std::uint8_t times)
|
||||
{
|
||||
if (times > 15U) {
|
||||
@@ -150,7 +160,8 @@ std::array<Bt1035AtCommand, kBt1035BootInitCommandCount> bootInitSequence() noex
|
||||
return std::array<Bt1035AtCommand, kBt1035BootInitCommandCount>{
|
||||
Bt1035AtCommand::Ping,
|
||||
Bt1035AtCommand::I2sMode,
|
||||
Bt1035AtCommand::I2sSlave48k24,
|
||||
Bt1035AtCommand::I2sSlave48k32,
|
||||
Bt1035AtCommand::A2dpCodecConfig,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace {
|
||||
std::cerr << "I2S mode must be in init sequence\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k24) {
|
||||
if (sequence[2U] != core::Bt1035AtCommand::I2sSlave48k32) {
|
||||
std::cerr << "I2SCFG must be in init sequence\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
@@ -44,9 +44,19 @@ namespace {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string i2sCfg =
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k24);
|
||||
if (i2sCfg != "AT+I2SCFG=35\r\n") {
|
||||
std::cerr << "I2SCFG=35 command line mismatch\n";
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::I2sSlave48k32);
|
||||
if (i2sCfg != "AT+I2SCFG=67\r\n") {
|
||||
std::cerr << "I2SCFG=67 command line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
if (sequence[3U] != core::Bt1035AtCommand::A2dpCodecConfig) {
|
||||
std::cerr << "A2DPCFG must be in init sequence\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string a2dpCfg =
|
||||
core::buildBt1035AtLine(core::Bt1035AtCommand::A2dpCodecConfig);
|
||||
if (a2dpCfg != "AT+A2DPCFG=1\r\n") {
|
||||
std::cerr << "A2DPCFG=1 command line mismatch\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const std::string reset =
|
||||
|
||||
Reference in New Issue
Block a user