Persist Si4684 crystal calibration (ibias/ctun/xtalFreqHz) to EEPROM
Extends the existing FM/DAB ANTCAP EEPROM persistence pattern
(Eeprom24aa::writeFmAntCap/writeDabAntCap) to the crystal trim found by
POST /api/tuner/xtal-calibrate, which previously only applied live and
was lost on every reboot.
- Eeprom24aa gains readXtalCalibration()/writeXtalCalibration() at word
addresses 0x02 (ibias), 0x03 (ctun), 0x04-0x07 (xtalFreqHz,
big-endian), right after the existing FM/DAB ANTCAP bytes.
- HardwareBootstrap::boot() now boots ADAU1701 before Si4684 (needed so
the EEPROM read, which borrows ADAU1701's I2C bus, can happen before
Si4684's boot() call, which takes the crystal trim as an argument),
loads the saved trim if present, and falls back to the compiled-in
defaults (ibias=72, ctun=0, xtalFreqHz=19199750) otherwise.
- POST /api/tuner/xtal-calibrate now persists every successful live
recalibration automatically ("persisted":true/false in the response)
via a new saveXtalCalibration()/net::AntennaCalibration::saveXtal
bridge, mirroring the ANTCAP save pattern.
Verified live: boot log confirms "Xtal not calibrated" before the first
save, "Xtal calibration loaded: ibias=72 ctun=0 xtal_freq_hz=19199750"
after, surviving a reboot; DAB/FM tuning unaffected (DAB CNR 17-19dB,
locked).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,24 @@
|
||||
|
||||
namespace eeprom24aa {
|
||||
|
||||
/**
|
||||
* @brief XtalCalibration — Si4684 crystal reference trim, EEPROM-backed.
|
||||
*
|
||||
* @dname XtalCalibration
|
||||
* @return n/a (type)
|
||||
* @pubstate Plain DTO mirroring POWER_UP ARG3/ARG8/ARG4-7 (AN649 Command
|
||||
* 0x01); persisted as a set (all three or none) since a partial
|
||||
* trim is meaningless.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
struct XtalCalibration {
|
||||
std::uint8_t ibias; ///< POWER_UP ARG3 IBIAS (0-127).
|
||||
std::uint8_t ctun; ///< POWER_UP ARG8 CTUN (0-63).
|
||||
std::uint32_t xtalFreqHz; ///< POWER_UP ARG4-7 XTAL_FREQ in Hz.
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Eeprom24aa — reads the factory EUI-48 from Microchip 24AA025E48;
|
||||
* also stores one board-specific calibration byte in the chip's
|
||||
@@ -50,6 +68,12 @@ public:
|
||||
/** Same range as kFmAntCapMax; kept as a separate name for the DAB
|
||||
* calibration byte's own doc comments below. */
|
||||
static constexpr std::uint8_t kDabAntCapMax = 128U;
|
||||
/** POWER_UP ARG3 IBIAS valid range (AN649 Command 0x01); 0xFF (blank
|
||||
* EEPROM) reads back as "never calibrated". */
|
||||
static constexpr std::uint8_t kXtalIbiasMax = 127U;
|
||||
/** POWER_UP ARG8 CTUN valid range (AN649 Command 0x01); 0xFF (blank
|
||||
* EEPROM) reads back as "never calibrated". */
|
||||
static constexpr std::uint8_t kXtalCtunMax = 63U;
|
||||
|
||||
/**
|
||||
* @brief Eeprom24aa — bind to a running I2C master bus and 7-bit addr.
|
||||
@@ -140,6 +164,40 @@ public:
|
||||
[[nodiscard]] std::expected<void, core::IdentityError>
|
||||
writeDabAntCap(std::uint8_t value);
|
||||
|
||||
/**
|
||||
* @brief readXtalCalibration — read the stored Si4684 crystal trim.
|
||||
*
|
||||
* @dname readXtalCalibration
|
||||
* @return Calibrated ibias/ctun/xtalFreqHz if all three were saved via
|
||||
* writeXtalCalibration(), nullopt if never calibrated (any of
|
||||
* the three bytes still blank/out of range), or IdentityError
|
||||
* on an I2C failure.
|
||||
* @pubstate performs one I2C read of six bytes starting at word address
|
||||
* 0x02.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
[[nodiscard]] std::expected<std::optional<XtalCalibration>, core::IdentityError>
|
||||
readXtalCalibration();
|
||||
|
||||
/**
|
||||
* @brief writeXtalCalibration — persist the Si4684 crystal trim.
|
||||
*
|
||||
* @dname writeXtalCalibration
|
||||
* @param calibration ibias (0-127), ctun (0-63), xtalFreqHz found by
|
||||
* POST /api/tuner/xtal-calibrate, not computed.
|
||||
* @return Ok on success, or IdentityError::I2cFailed.
|
||||
* @pubstate performs one I2C write of six bytes starting at word address
|
||||
* 0x02 (ibias, ctun, xtalFreqHz big-endian), then blocks for
|
||||
* the chip's write-cycle time before returning.
|
||||
*
|
||||
* @author Michele Bigi
|
||||
* @date 2026-08-24
|
||||
*/
|
||||
[[nodiscard]] std::expected<void, core::IdentityError>
|
||||
writeXtalCalibration(const XtalCalibration& calibration);
|
||||
|
||||
private:
|
||||
i2c_master_bus_handle_t bus_;
|
||||
std::uint8_t addr7_;
|
||||
|
||||
@@ -32,6 +32,11 @@ constexpr std::uint8_t kEui48WordAddress = 0xFAU;
|
||||
constexpr std::uint8_t kFmAntCapWordAddress = 0x00U;
|
||||
/** DAB ANTCAP calibration byte, next word address after the FM byte. */
|
||||
constexpr std::uint8_t kDabAntCapWordAddress = 0x01U;
|
||||
/** Si4684 crystal trim: IBIAS byte, CTUN byte, then XTAL_FREQ as 4 bytes
|
||||
* big-endian -- 6 bytes total starting right after the DAB ANTCAP byte. */
|
||||
constexpr std::uint8_t kXtalIbiasWordAddress = 0x02U;
|
||||
constexpr std::uint8_t kXtalCtunWordAddress = 0x03U;
|
||||
constexpr std::uint8_t kXtalFreqHzWordAddress = 0x04U;
|
||||
constexpr int kI2cTimeoutMs = 100;
|
||||
/** DS20001191 §"Page Write"/"Byte Write": max write cycle time after STOP
|
||||
* before the chip acknowledges further I2C traffic. */
|
||||
@@ -221,4 +226,93 @@ Eeprom24aa::writeDabAntCap(std::uint8_t value)
|
||||
return {};
|
||||
}
|
||||
|
||||
std::expected<std::optional<XtalCalibration>, core::IdentityError>
|
||||
Eeprom24aa::readXtalCalibration()
|
||||
{
|
||||
if (bus_ == nullptr) {
|
||||
return std::unexpected(core::IdentityError::I2cFailed);
|
||||
}
|
||||
|
||||
i2c_device_config_t devCfg = {};
|
||||
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||
devCfg.device_address = addr7_;
|
||||
devCfg.scl_speed_hz = 100000;
|
||||
|
||||
i2c_master_dev_handle_t dev = nullptr;
|
||||
if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) {
|
||||
ESP_LOGW(kTag, "i2c_master_bus_add_device failed");
|
||||
return std::unexpected(core::IdentityError::I2cFailed);
|
||||
}
|
||||
|
||||
const std::uint8_t wordAddress = kXtalIbiasWordAddress;
|
||||
std::array<std::uint8_t, 6> payload = {};
|
||||
const esp_err_t err = i2c_master_transmit_receive(
|
||||
dev, &wordAddress, 1U, payload.data(), payload.size(), kI2cTimeoutMs);
|
||||
|
||||
i2c_master_bus_rm_device(dev);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "Xtal calibration read failed (err=0x%x)",
|
||||
static_cast<unsigned>(err));
|
||||
return std::unexpected(core::IdentityError::ReadFailed);
|
||||
}
|
||||
|
||||
const std::uint8_t ibias = payload[0];
|
||||
const std::uint8_t ctun = payload[1];
|
||||
const std::uint32_t xtalFreqHz =
|
||||
(static_cast<std::uint32_t>(payload[2]) << 24)
|
||||
| (static_cast<std::uint32_t>(payload[3]) << 16)
|
||||
| (static_cast<std::uint32_t>(payload[4]) << 8)
|
||||
| static_cast<std::uint32_t>(payload[5]);
|
||||
|
||||
if (ibias > kXtalIbiasMax || ctun > kXtalCtunMax
|
||||
|| xtalFreqHz == 0xFFFFFFFFU) {
|
||||
return std::optional<XtalCalibration>{};
|
||||
}
|
||||
return std::optional<XtalCalibration>{
|
||||
XtalCalibration{.ibias = ibias, .ctun = ctun,
|
||||
.xtalFreqHz = xtalFreqHz}};
|
||||
}
|
||||
|
||||
std::expected<void, core::IdentityError>
|
||||
Eeprom24aa::writeXtalCalibration(const XtalCalibration& calibration)
|
||||
{
|
||||
if (bus_ == nullptr) {
|
||||
return std::unexpected(core::IdentityError::I2cFailed);
|
||||
}
|
||||
|
||||
i2c_device_config_t devCfg = {};
|
||||
devCfg.dev_addr_length = I2C_ADDR_BIT_LEN_7;
|
||||
devCfg.device_address = addr7_;
|
||||
devCfg.scl_speed_hz = 100000;
|
||||
|
||||
i2c_master_dev_handle_t dev = nullptr;
|
||||
if (i2c_master_bus_add_device(bus_, &devCfg, &dev) != ESP_OK) {
|
||||
ESP_LOGW(kTag, "i2c_master_bus_add_device failed");
|
||||
return std::unexpected(core::IdentityError::I2cFailed);
|
||||
}
|
||||
|
||||
const std::array<std::uint8_t, 7> payload = {
|
||||
kXtalIbiasWordAddress,
|
||||
calibration.ibias,
|
||||
calibration.ctun,
|
||||
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 24),
|
||||
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 16),
|
||||
static_cast<std::uint8_t>(calibration.xtalFreqHz >> 8),
|
||||
static_cast<std::uint8_t>(calibration.xtalFreqHz)};
|
||||
const esp_err_t err =
|
||||
i2c_master_transmit(dev, payload.data(), payload.size(), kI2cTimeoutMs);
|
||||
|
||||
i2c_master_bus_rm_device(dev);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "Xtal calibration write failed (err=0x%x)",
|
||||
static_cast<unsigned>(err));
|
||||
return std::unexpected(core::IdentityError::I2cFailed);
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(kI2cWriteCycleMs));
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace eeprom24aa
|
||||
|
||||
Reference in New Issue
Block a user