//! 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)
}
neiam
/lambent-flex-c6
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
6.6 KiB · text
History
9d79b3d