6.6 KiB · text History dee327e
//! Bit-banged single-pixel WS2812 driver.
//!
//! Exists because the C6 has only two RMT TX channels and both are spoken for.
//! Verified against `soc/esp32c6/include/soc/soc_caps.h`:
//!
//! ```c
//! #define SOC_RMT_TX_CANDIDATES_PER_GROUP 2
//! #define SOC_RMT_CHANNELS_PER_GROUP      4   /* 4 total, only 2 transmit */
//! ```
//!
//! The two strands take `channel0` and `channel1`, so the on-board indicator
//! LED (GPIO8 on the C6-DevKitC-1) has no channel left and has to be driven by
//! hand. See [`Ws2812BitBang::set`] for the cost of that.

use anyhow::{anyhow, Result};
use esp_idf_svc::hal::delay::FreeRtos;
use esp_idf_svc::hal::gpio::{AnyOutputPin, Output, PinDriver};
use esp_idf_svc::hal::interrupt;
use esp_idf_svc::sys::esp_timer_get_time;

pub use rgb::RGB8;

/// `DR_REG_GPIO_BASE` from `soc/esp32c6/include/soc/reg_base.h`.
const GPIO_BASE: u32 = 0x6009_1000;
/// `GPIO_OUT_W1TS_REG` — write-1-to-set, from `soc/esp32c6/include/soc/gpio_reg.h`.
const GPIO_OUT_W1TS: *mut u32 = (GPIO_BASE + 0x8) as *mut u32;
/// `GPIO_OUT_W1TC_REG` — write-1-to-clear.
const GPIO_OUT_W1TC: *mut u32 = (GPIO_BASE + 0xc) as *mut u32;

/// Performance-counter CSRs. The C6 is PLIC, not CLIC
/// (`SOC_INT_PLIC_SUPPORTED 1`), so `rv_utils_get_cycle_count()` reads
/// `CSR_PCCR_MACHINE` rather than `mcycle` — see `riscv/rv_utils.h:86`.
const CSR_PCER: u32 = 0x7e0;
const CSR_PCMR: u32 = 0x7e1;

// WS2812B bit timings in nanoseconds. One bit is always ~1.25us; only the
// high/low split encodes the value.
const T0H_NS: u32 = 350;
const T1H_NS: u32 = 700;
const BIT_NS: u32 = 1250;
/// Latch/reset gap floor. Newer WS2812B parts want >280us, not the 50us of the
/// original datasheet. The code waits 1ms, comfortably above this.
const _RESET_US_MIN: u32 = 300;

/// Calibration window. Long enough to average out timer granularity, short
/// enough that busy-waiting on it cannot upset the task watchdog.
const CALIBRATE_US: i64 = 1000;

#[inline(always)]
fn cycles() -> u32 {
    let c: u32;
    // CSR_PCCR_MACHINE (0x7e2).
    unsafe { core::arch::asm!("csrr {0}, 0x7e2", out(reg) c, options(nomem, nostack)) };
    c
}

pub struct Ws2812BitBang {
    /// Held for ownership and pin configuration; levels are driven through the
    /// registers below, because `PinDriver::set_high` is a call into
    /// `gpio_set_level` and far too slow and jittery for a 350ns pulse.
    _pin: PinDriver<'static, Output>,
    mask: u32,
    t0h: u32,
    t1h: u32,
    t0l: u32,
    t1l: u32,
}

impl Ws2812BitBang {
    /// `gpio` is the pin number; the caller is responsible for it being free.
    pub fn new(pin: AnyOutputPin<'static>, gpio: i32) -> Result<Self> {
        if !(0..=30).contains(&gpio) {
            return Err(anyhow!("indicator gpio {gpio} out of range for the C6 (0-30)"));
        }

        let mut driver = PinDriver::output(pin)?;
        driver.set_low()?;

        let per_us = measure_cycles_per_us()?;
        let ns = |n: u32| (n * per_us) / 1000;

        Ok(Self {
            _pin: driver,
            mask: 1u32 << gpio,
            t0h: ns(T0H_NS),
            t1h: ns(T1H_NS),
            t0l: ns(BIT_NS - T0H_NS),
            t1l: ns(BIT_NS - T1H_NS),
        })
    }

    /// Shift out one 24-bit GRB frame.
    ///
    /// **Blocks interrupts for the duration** — 24 bits at 1.25us is about
    /// 30us. That is short enough not to upset wifi at the rates this is poked
    /// at, but it is the reason this must not be called in a tight loop.
    pub fn set(&mut self, rgb: RGB8) {
        let grb =
            ((rgb.g as u32) << 16) | ((rgb.r as u32) << 8) | rgb.b as u32;

        interrupt::free(|| {
            for i in (0..24).rev() {
                let (high, total) = if grb & (1 << i) != 0 {
                    (self.t1h, self.t1h + self.t1l)
                } else {
                    (self.t0h, self.t0h + self.t0l)
                };

                // Anchor both edges to one start stamp so per-iteration
                // overhead cannot accumulate across the frame.
                let start = cycles();
                unsafe { GPIO_OUT_W1TS.write_volatile(self.mask) };
                while cycles().wrapping_sub(start) < high {}
                unsafe { GPIO_OUT_W1TC.write_volatile(self.mask) };
                while cycles().wrapping_sub(start) < total {}
            }
        });

        // Latch. Outside the critical section: it is idle time, and holding
        // interrupts off for this long would be antisocial. `FreeRtos::delay_us`
        // is private, and the gap only has to be *at least* RESET_US, so round
        // up to the 1ms tick.
        FreeRtos::delay_ms(1);
    }

    pub fn off(&mut self) {
        self.set(RGB8::new(0, 0, 0));
    }
}

/// Derive the cycle counter's rate empirically rather than trusting a hardcoded
/// 160MHz, and prove the counter is actually running.
///
/// The performance counter is not obviously enabled during ESP-IDF startup, and
/// a stopped counter would make every delay loop above spin forever. Enabling
/// PCER/PCMR is idempotent, and the cross-check against `esp_timer` turns a
/// silent hang into a startup error.
fn measure_cycles_per_us() -> Result<u32> {
    unsafe {
        // PCER bit 0 selects the cycle event; PCMR bit 0 runs the counter.
        core::arch::asm!("csrw {0}, {1}", const CSR_PCER, in(reg) 1u32, options(nomem, nostack));
        core::arch::asm!("csrw {0}, {1}", const CSR_PCMR, in(reg) 1u32, options(nomem, nostack));
    }

    // Busy-wait rather than sleep. `FreeRtos::delay_ms` parks the task, the
    // idle task halts the core in WFI, and the cycle counter stops with it —
    // calibrating across that measured ~21 cycles/us instead of ~160, because
    // only the awake ~13% was counted. Spinning keeps the core running, which
    // is also the state the bit-bang loop actually cares about.
    let (t0, c0) = (unsafe { esp_timer_get_time() }, cycles());
    while unsafe { esp_timer_get_time() } - t0 < CALIBRATE_US {}
    let (t1, c1) = (unsafe { esp_timer_get_time() }, cycles());

    let elapsed_us = (t1 - t0) as u32;
    if elapsed_us == 0 {
        return Err(anyhow!("esp_timer did not advance; cannot calibrate indicator"));
    }
    let per_us = c1.wrapping_sub(c0) / elapsed_us;

    // A C6 runs at 80-160MHz. Anything outside this means the counter is
    // stopped or reading something else, and bit-banging would emit garbage.
    if !(40..=400).contains(&per_us) {
        return Err(anyhow!(
            "cycle counter reads {per_us} cycles/us, expected ~160; \
             CSR_PCCR_MACHINE is probably not counting"
        ));
    }
    log::info!("indicator: cycle counter calibrated at {per_us} cycles/us");
    Ok(per_us)
}