use embedded_hal::pwm::SetDutyCycle;
use crate::PolarityMode;
pub trait GammaMap {
fn map(&self, raw: u8) -> u8;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum GammaCorrection {
Linear,
CieLStar,
}
#[rustfmt::skip]
const CIE_PREFIX: [u8; 16] = [
0, 9, 18, 26, 33, 39, 44, 48,
52, 56, 60, 63, 66, 69, 72, 74,
];
#[rustfmt::skip]
const CIE_KNOTS: [u8; 16] = [
77, 107, 129, 146, 160, 173, 184, 194,
204, 212, 221, 228, 236, 242, 249, 255,
];
impl GammaMap for GammaCorrection {
#[inline]
fn map(&self, raw: u8) -> u8 {
match self {
Self::Linear => raw,
Self::CieLStar => {
if raw < 16 {
CIE_PREFIX[raw as usize]
} else if raw == 255 {
255 } else {
let idx = ((raw - 16) >> 4) as usize;
let lo = CIE_KNOTS[idx];
let diff = CIE_KNOTS[idx + 1].wrapping_sub(lo);
let frac = raw & 0x0F;
let mut offset = 0u8;
let mut lost = 0u8;
if frac & 8 != 0 {
offset += diff >> 1;
lost += diff & 1;
}
if frac & 4 != 0 {
offset += diff >> 2;
lost += (diff & 3) >> 1;
}
if frac & 2 != 0 {
offset += diff >> 3;
lost += (diff & 7) >> 2;
}
if frac & 1 != 0 {
offset += diff >> 4;
lost += (diff & 15) >> 3;
}
if lost >= 4 {
offset += 1;
} lo.wrapping_add(offset)
}
}
}
}
}
pub struct PwmLed<P: SetDutyCycle, G: GammaMap = GammaCorrection> {
pin: P,
gamma: G,
polarity: PolarityMode,
max_duty: u16,
duty_scale: u32,
brightness: u8,
}
impl<P: SetDutyCycle, G: GammaMap> PwmLed<P, G> {
pub fn new(pin: P, gamma: G, polarity: PolarityMode) -> Result<Self, P::Error> {
let max_duty = pin.max_duty_cycle();
let duty_scale = max_duty as u32 * 257;
let mut led = Self {
pin,
gamma,
polarity,
max_duty,
duty_scale,
brightness: 0,
};
led.off()?;
Ok(led)
}
#[inline]
pub fn from_pin(pin: P, gamma: G, polarity: PolarityMode) -> Self {
let max_duty = pin.max_duty_cycle();
let duty_scale = max_duty as u32 * 257;
Self {
pin,
gamma,
polarity,
max_duty,
duty_scale,
brightness: 0,
}
}
pub fn set_brightness(&mut self, raw: u8) -> Result<(), P::Error> {
let corrected = self.gamma.map(raw);
let duty_raw = self.polarity.map_duty(corrected) as u32;
let duty = ((duty_raw * self.duty_scale + 32768) >> 16) as u16;
self.pin.set_duty_cycle(duty)?;
self.brightness = raw;
Ok(())
}
#[inline]
pub fn set_brightness_percent(&mut self, pct: u8) -> Result<(), P::Error> {
let p = pct.min(100) as u32;
let x = (p << 8).wrapping_sub(p).wrapping_add(50);
let raw = (x.wrapping_add(x << 3).wrapping_add(x << 5) >> 12) as u8;
self.set_brightness(raw)
}
#[inline]
pub fn set_duty_raw(&mut self, duty: u16) -> Result<(), P::Error> {
self.pin.set_duty_cycle(duty)
}
#[inline]
pub fn off(&mut self) -> Result<(), P::Error> {
self.set_brightness(0)
}
#[inline]
pub fn on(&mut self) -> Result<(), P::Error> {
self.set_brightness(255)
}
#[inline]
pub fn brightness(&self) -> u8 {
self.brightness
}
#[inline]
pub fn is_on(&self) -> bool {
self.brightness > 0
}
#[inline]
pub fn is_off(&self) -> bool {
self.brightness == 0
}
#[inline]
pub fn gamma(&self) -> &G {
&self.gamma
}
#[inline]
pub fn polarity(&self) -> PolarityMode {
self.polarity
}
#[inline]
pub fn max_duty(&self) -> u16 {
self.max_duty
}
#[inline]
pub fn release(self) -> P {
self.pin
}
}
impl<P: SetDutyCycle, G: GammaMap> embedded_hal::pwm::ErrorType for PwmLed<P, G> {
type Error = P::Error;
}
impl<P: SetDutyCycle, G: GammaMap> SetDutyCycle for PwmLed<P, G> {
#[inline]
fn max_duty_cycle(&self) -> u16 {
self.max_duty
}
#[inline]
fn set_duty_cycle(&mut self, duty: u16) -> Result<(), Self::Error> {
self.pin.set_duty_cycle(duty)
}
}
impl<P: SetDutyCycle, G: GammaMap> core::fmt::Debug for PwmLed<P, G> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PwmLed")
.field("polarity", &self.polarity)
.field("max_duty", &self.max_duty)
.field("brightness", &self.brightness)
.finish_non_exhaustive()
}
}
#[cfg(feature = "defmt")]
impl<P: SetDutyCycle, G: GammaMap> defmt::Format for PwmLed<P, G> {
fn format(&self, fmt: defmt::Formatter) {
defmt::write!(
fmt,
"PwmLed {{ polarity: {}, max_duty: {}, brightness: {} }}",
self.polarity,
self.max_duty,
self.brightness,
)
}
}
#[cfg(test)]
include!(concat!(env!("OUT_DIR"), "/gamma_tables.rs"));
#[cfg(test)]
mod tests {
use super::*;
use crate::PolarityMode;
fn cie_reference(raw: u8) -> u8 {
CIE_LSTAR[raw as usize]
}
#[test]
fn cie_prefix_exact() {
for raw in 0..16u8 {
assert_eq!(
GammaCorrection::CieLStar.map(raw),
cie_reference(raw),
"prefix mismatch at raw={raw}"
);
}
}
#[test]
fn cie_knots_exact() {
for raw in (16..=255).step_by(16) {
let raw = raw as u8;
assert_eq!(
GammaCorrection::CieLStar.map(raw),
cie_reference(raw),
"knot mismatch at raw={raw}"
);
}
}
#[test]
fn cie_knots_endpoints() {
assert_eq!(CIE_KNOTS[0], 77);
assert_eq!(CIE_KNOTS[15], 255);
}
#[test]
fn cie_interp_max_error() {
let mut max_err = 0u16;
for raw in 16..=255u8 {
let got = GammaCorrection::CieLStar.map(raw);
let ref_val = cie_reference(raw);
let err = (got as i16 - ref_val as i16).unsigned_abs();
max_err = max_err.max(err);
}
assert!(max_err <= 4, "CIE L* interp max error {max_err} > 4");
}
#[test]
fn cie_lstar_monotonic() {
let mut prev = 0u8;
for raw in 0..=255u8 {
let val = GammaCorrection::CieLStar.map(raw);
assert!(
val >= prev,
"CIE L* non-monotonic at raw={raw}: {prev} -> {val}"
);
prev = val;
}
}
#[test]
fn cie_lstar_endpoints() {
assert_eq!(GammaCorrection::CieLStar.map(0), 0);
assert_eq!(GammaCorrection::CieLStar.map(255), 255);
}
#[test]
fn cie_lstar_at_midpoint() {
assert_eq!(GammaCorrection::CieLStar.map(128), 194);
}
#[test]
fn cie_lstar_low_end_smooth() {
let mut prev = 0u8;
for raw in 1..=10u8 {
let val = GammaCorrection::CieLStar.map(raw);
let step = val - prev;
assert!(
step <= 9,
"raw={raw}: step {step} too large (prev={prev}, val={val})"
);
prev = val;
}
assert_eq!(GammaCorrection::CieLStar.map(1), 9);
}
#[test]
fn linear_is_identity() {
for raw in 0..=255u8 {
assert_eq!(GammaCorrection::Linear.map(raw), raw);
}
}
#[test]
fn polarity_off_and_full_on_invert() {
assert_eq!(PolarityMode::ActiveLow.map_duty(0), 255);
assert_eq!(PolarityMode::ActiveLow.map_duty(255), 0);
}
use embedded_hal_mock::eh1::pwm::Mock as PwmMock;
use embedded_hal_mock::eh1::pwm::Transaction as PwmTrans;
const MAX_DUTY: u16 = 1000;
#[test]
fn new_sets_off() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
];
let led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
)
.unwrap();
assert_eq!(led.brightness(), 0);
assert!(led.is_off());
assert!(!led.is_on());
led.release().done();
}
#[test]
fn from_pin_no_touch() {
let e = [PwmTrans::max_duty_cycle(MAX_DUTY)];
let led = PwmLed::from_pin(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
);
assert_eq!(led.brightness(), 0);
led.release().done();
}
#[test]
fn set_brightness_active_high() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
PwmTrans::set_duty_cycle(MAX_DUTY),
];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
)
.unwrap();
led.set_brightness(255).unwrap();
assert_eq!(led.brightness(), 255);
assert!(led.is_on());
led.release().done();
}
#[test]
fn set_brightness_active_low_polarity_inverts_duty() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(MAX_DUTY), PwmTrans::set_duty_cycle(0), ];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveLow,
)
.unwrap();
led.on().unwrap();
assert_eq!(led.brightness(), 255);
led.release().done();
}
#[test]
fn on_and_off_track_correctly() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
PwmTrans::set_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
)
.unwrap();
assert!(led.is_off());
led.on().unwrap();
assert!(led.is_on());
led.off().unwrap();
assert!(led.is_off());
led.release().done();
}
fn expected_duty(raw: u8, polarity: PolarityMode, max_duty: u16) -> u16 {
let duty_raw = polarity.map_duty(raw) as u32;
let duty_scale = max_duty as u32 * 257;
((duty_raw * duty_scale + 32768) >> 16) as u16
}
#[test]
fn brightness_percent_rounding() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0),
PwmTrans::set_duty_cycle(expected_duty(3, PolarityMode::ActiveHigh, MAX_DUTY)),
PwmTrans::set_duty_cycle(0),
PwmTrans::set_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(expected_duty(128, PolarityMode::ActiveHigh, MAX_DUTY)),
];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
)
.unwrap();
led.set_brightness_percent(1).unwrap();
assert_eq!(led.brightness(), 3);
led.set_brightness_percent(0).unwrap();
assert_eq!(led.brightness(), 0);
led.set_brightness_percent(100).unwrap();
assert_eq!(led.brightness(), 255);
led.set_brightness_percent(50).unwrap();
assert_eq!(led.brightness(), 128);
led.release().done();
}
#[test]
fn custom_gamma_map() {
struct InvertGamma;
impl GammaMap for InvertGamma {
fn map(&self, raw: u8) -> u8 {
255 - raw
}
}
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(MAX_DUTY), PwmTrans::set_duty_cycle(0), ];
let mut led = PwmLed::new(PwmMock::new(&e), InvertGamma, PolarityMode::ActiveHigh).unwrap();
led.on().unwrap();
assert!(led.is_on()); led.release().done();
}
#[test]
fn set_duty_raw_bypasses_gamma() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0), PwmTrans::set_duty_cycle(500), ];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::CieLStar,
PolarityMode::ActiveHigh,
)
.unwrap();
led.set_duty_raw(500).unwrap();
assert_eq!(led.brightness(), 0); led.release().done();
}
#[test]
fn set_duty_cycle_trait_passthrough() {
let e = [
PwmTrans::max_duty_cycle(MAX_DUTY),
PwmTrans::set_duty_cycle(0), PwmTrans::set_duty_cycle(250),
];
let mut led = PwmLed::new(
PwmMock::new(&e),
GammaCorrection::Linear,
PolarityMode::ActiveHigh,
)
.unwrap();
<PwmLed<PwmMock> as SetDutyCycle>::set_duty_cycle(&mut led, 250).unwrap();
assert_eq!(led.max_duty_cycle(), MAX_DUTY);
led.release().done();
}
#[test]
fn accessors() {
let e = [PwmTrans::max_duty_cycle(MAX_DUTY)];
let led = PwmLed::from_pin(
PwmMock::new(&e),
GammaCorrection::CieLStar,
PolarityMode::ActiveLow,
);
assert_eq!(led.polarity(), PolarityMode::ActiveLow);
assert_eq!(led.max_duty(), MAX_DUTY);
assert_eq!(led.brightness(), 0);
led.release().done();
}
}