Skip to content

radio_esp32::phy

Bringing the PHY up, and the calibration that goes with it.

Step 3.6. Everything else in this crate answers a call from the blob; this is the one place FlintOS calls into it, and the first place where “compiles and links” stops meaning much.

esp_phy_enable in phy_init.c, reduced to what FlintOS has:

  1. Take a reference to the shared radio clocks (DPORT_WIFI_CLK_EN).
  2. First time only: load a stored calibration, hand it and the init data to register_chipv7_phy, and store the result back if it was recalculated.
  3. Afterwards: phy_wakeup_init, which is the cheap path.

Disable runs it backwards: phy_close_rf, then the clocks.

The calibration mode is a decision, not a constant

Section titled “The calibration mode is a decision, not a constant”

esp-idf asks for PHY_RF_CAL_FULL when it has no usable stored calibration and PHY_RF_CAL_PARTIAL when it has one. The difference is hundreds of milliseconds at every boot, which is the entire reason [crate::calibration] exists.

It also uses PHY_RF_CAL_NONE after a deep-sleep wake, on the grounds that the calibration in RAM is still good. FlintOS has no deep sleep, so that case cannot arise here and is not implemented rather than guessed at.

apps/tests/radioprobe runs this on a board. On an ESP32-WROOM: register_chipv7_phy returns 0 with our init data and our eFuse MAC, a full calibration takes ~183 ms, and a re-enable takes ~250 µs.

It also found a kernel bug, since fixed: this double-faulted on every preemption, and the cause was four wrong window overflow/underflow vectors rather than anything about the blob. Nothing in FlintOS’s own Rust reaches the call8 and call12 handlers; a GCC-built blob reaches them constantly. enable is preemptible now, and there is no critical section anywhere on this path – see arch/xtensa/src/asm/vectors.S.

One thing it found that is not fixed:

  • The stored calibration currently costs more than it saves. Reading it back is ~1.36 s, against the ~183 ms of calibration it avoids, because kvstore::get scans the whole log per key and [crate::calibration] needs eighteen of them. [CalibrationMode::Partial] is the right decision and the store is the right idea; the log is the wrong shape.
enum PhyError {
Register(i32),
OutOfMemory,
}

Why the PHY could not be brought up.

Implements: Clone, Copy, Debug, Eq, PartialEq, StructuralPartialEq

unsafe fn common_clock_disable()

Release one reference to the clock gates shared by Wi-Fi and Bluetooth.

Must match a prior [common_clock_enable].

unsafe fn common_clock_enable()

Take one reference to the clock gates shared by Wi-Fi and Bluetooth.

esp-idf reference-counts wifi_bt_common_module_enable: the Wi-Fi driver takes a temporary reference of its own around the longer-lived reference held by esp_phy_enable. Treating either callback as a raw bit operation lets the temporary disable shut the PHY off while its owner is still live.

Writes DPORT on the first reference. Every call must be paired with [common_clock_disable].

unsafe fn disable(mask: u32)

Drop a reference, closing the RF when the last one goes.

Calls into the blob and writes DPORT. Must match an [enable].

unsafe fn enable(mask: u32) -> Result<(), PhyError>

Bring the PHY up, or take another reference to an already-running one.

The first call does the expensive part: it loads a stored calibration if there is a usable one, registers the PHY, and stores the result back when the PHY had to recalculate. Later calls only re-wake it.

Calls into the blob and writes DPORT. The caller must pair every enable with a [disable], and must have initialised [crate::nvs] first if the calibration is to survive a reboot – without it this still works, and simply recalibrates every time.

References: PhyError

fn refs() -> u32

How many references are outstanding. Diagnostics.

unsafe fn rf_cal_version() -> u32

The calibration layout this PHY speaks.

Calls into the blob. Cheap and side-effect-free, but it is still the blob.

unsafe fn version_str() -> &'static str

The PHY blob’s version string, as it reports itself.

Which archive is linked is otherwise invisible: make blobs fetches a pinned revision, and nothing downstream would notice if it fetched a different one.

Calls into the blob, which returns a pointer to its own static storage.