From 1bc3e5bd52719afc761185b5ca7f4c9ed99740c5 Mon Sep 17 00:00:00 2001 From: Justin Beaurivage Date: Sat, 7 Dec 2024 01:17:10 -0500 Subject: [PATCH] examples(pygamer): Restore neopixel examples using SPI driver --- boards/pygamer/Cargo.toml | 26 ++++ .../pygamer/examples/neopixel_adc_battery.rs | 89 ++++++++++++++ boards/pygamer/examples/neopixel_adc_light.rs | 88 ++++++++++++++ boards/pygamer/examples/neopixel_button.rs | 115 ++++++++++++++++++ boards/pygamer/examples/neopixel_easing.rs | 102 ++++++++++++++++ boards/pygamer/examples/neopixel_rainbow.rs | 84 +++++++++++++ boards/pygamer/src/pins.rs | 48 +++++++- 7 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 boards/pygamer/examples/neopixel_adc_battery.rs create mode 100644 boards/pygamer/examples/neopixel_adc_light.rs create mode 100644 boards/pygamer/examples/neopixel_button.rs create mode 100644 boards/pygamer/examples/neopixel_easing.rs create mode 100644 boards/pygamer/examples/neopixel_rainbow.rs diff --git a/boards/pygamer/Cargo.toml b/boards/pygamer/Cargo.toml index b7388f3a9f5..77cc99eb72d 100644 --- a/boards/pygamer/Cargo.toml +++ b/boards/pygamer/Cargo.toml @@ -33,6 +33,11 @@ version = "0.20.0" optional = true version = "0.3.2" +[dependencies.ws2812-spi] +version = "0.5.0" +features = ["mosi_idle_high"] +optional = true + [dev-dependencies] embedded-graphics = "0.8.1" embedded-sdmmc = "0.8.0" @@ -52,6 +57,7 @@ max-channels = ["dma", "atsamd-hal/max-channels"] panic_led = [] rt = ["cortex-m-rt", "atsamd-hal/samd51j-rt"] usb = ["atsamd-hal/usb", "usb-device"] +neopixel-spi = ["dep:ws2812-spi"] # Enable async support from atsamd-hal async = ["atsamd-hal/async"] @@ -89,3 +95,23 @@ name = "timer" [[example]] name = "usb_poll" required-features = ["usb"] + +[[example]] +name = "neopixel_adc_battery" +required-features = ["neopixel-spi"] + +[[example]] +name = "neopixel_adc_light" +required-features = ["neopixel-spi"] + +[[example]] +name = "neopixel_button" +required-features = ["neopixel-spi"] + +[[example]] +name = "neopixel_easing" +required-features = ["neopixel-spi"] + +[[example]] +name = "neopixel_rainbow" +required-features = ["neopixel-spi"] diff --git a/boards/pygamer/examples/neopixel_adc_battery.rs b/boards/pygamer/examples/neopixel_adc_battery.rs new file mode 100644 index 00000000000..0a048aa5f0e --- /dev/null +++ b/boards/pygamer/examples/neopixel_adc_battery.rs @@ -0,0 +1,89 @@ +//! Display battery percentage on the neopixels. +//! +//! Note leds may appear white during debug. Either build for release or add +//! opt-level = 2 to profile.dev in Cargo.toml + +#![no_std] +#![no_main] + +#[cfg(not(feature = "panic_led"))] +use panic_halt as _; +use pygamer::{entry, hal, pac, Pins}; + +use hal::adc::Adc; +use hal::{clock::GenericClockController, delay::Delay}; + +use pac::gclk::pchctrl::Genselect::Gclk11; + +use hal::ehal::delay::DelayNs; + +use pac::{CorePeripherals, Peripherals}; +use smart_leds::{brightness, SmartLedsWrite, RGB8}; + +#[entry] +fn main() -> ! { + let mut peripherals = Peripherals::take().unwrap(); + let core = CorePeripherals::take().unwrap(); + let mut clocks = GenericClockController::with_internal_32kosc( + peripherals.gclk, + &mut peripherals.mclk, + &mut peripherals.osc32kctrl, + &mut peripherals.oscctrl, + &mut peripherals.nvmctrl, + ); + let pins = Pins::new(peripherals.port).split(); + + let mut adc0 = Adc::adc0(peripherals.adc0, &mut peripherals.mclk, &mut clocks, Gclk11); + let mut battery = pins.battery.init(); + + // neopixels + let mut neopixel = pins.neopixel.init_spi( + &mut clocks, + // Unfortunately, the SPI driver requires a clock pin, even though it's not used by the + // neopixels. + pins.i2c.scl, + peripherals.sercom2, + &mut peripherals.mclk, + ); + + let mut delay = Delay::new(core.SYST, &mut clocks); + + //todo put this on a .. 10minute, 30min, update timer + loop { + let battery_data = battery.read(&mut adc0); + + let mut colors = [ + RGB8::default(), + RGB8::default(), + RGB8::default(), + RGB8::default(), + RGB8::default(), + ]; + + if battery_data < 3.6 { + enable_leds(1, &mut colors); + } else if (3.6..3.8).contains(&battery_data) { + enable_leds(2, &mut colors); + } else if (3.8..3.9).contains(&battery_data) { + enable_leds(3, &mut colors); + } else if (3.9..4.0).contains(&battery_data) { + enable_leds(4, &mut colors); + } else { + enable_leds(5, &mut colors); + }; + + neopixel + .write(brightness(colors.iter().cloned(), 1)) + .unwrap(); + + // Reset the LEDs + delay.delay_ms(10); + } +} + +/// Turn on the specified number of LEDs and set the color to red. +fn enable_leds(num_leds: usize, colors: &mut [RGB8]) { + for color in colors.iter_mut().take(num_leds) { + *color = RGB8::from((255, 0, 0)); + } +} diff --git a/boards/pygamer/examples/neopixel_adc_light.rs b/boards/pygamer/examples/neopixel_adc_light.rs new file mode 100644 index 00000000000..4ed277709b2 --- /dev/null +++ b/boards/pygamer/examples/neopixel_adc_light.rs @@ -0,0 +1,88 @@ +//! Display light sensor reading on the neopixels. +//! +//! Note leds may appear white during debug. Either build for release or add +//! opt-level = 2 to profile.dev in Cargo.toml + +#![no_std] +#![no_main] + +#[cfg(not(feature = "panic_led"))] +use panic_halt as _; +use pygamer::{entry, hal, pac, Pins}; + +use hal::adc::Adc; +use hal::prelude::*; +use hal::{clock::GenericClockController, delay::Delay}; +use pac::gclk::pchctrl::Genselect::Gclk11; +use pac::{CorePeripherals, Peripherals}; +use smart_leds::SmartLedsWrite; +use smart_leds::{ + hsv::{hsv2rgb, Hsv}, + RGB8, +}; + +#[entry] +fn main() -> ! { + let mut peripherals = Peripherals::take().unwrap(); + let core = CorePeripherals::take().unwrap(); + let mut clocks = GenericClockController::with_internal_32kosc( + peripherals.gclk, + &mut peripherals.mclk, + &mut peripherals.osc32kctrl, + &mut peripherals.oscctrl, + &mut peripherals.nvmctrl, + ); + let pins = Pins::new(peripherals.port).split(); + + let mut adc1 = Adc::adc1(peripherals.adc1, &mut peripherals.mclk, &mut clocks, Gclk11); + let mut light = pins.light_pin.into_alternate(); + + // neopixels + let mut neopixel = pins.neopixel.init_spi( + &mut clocks, + // Unfortunately, the SPI driver requires a clock pin, even though it's not used by the + // neopixels. + pins.i2c.scl, + peripherals.sercom2, + &mut peripherals.mclk, + ); + + let mut delay = Delay::new(core.SYST, &mut clocks); + + const NUM_LEDS: usize = 5; + let mut j: u8 = 0; + + loop { + let light_data: u16 = adc1.read(&mut light).unwrap(); + + let pos: usize = if light_data < 100 { + 0 + } else if (147..1048).contains(&light_data) { + 1 + } else if (1048..3048).contains(&light_data) { + 2 + } else if (3048..3948).contains(&light_data) { + 3 + } else { + 4 + }; + + //finally paint the one led wherever the position is + let _ = neopixel.write((0..NUM_LEDS).map(|i| { + if i == pos { + hsv2rgb(Hsv { + hue: j, + sat: 255, + val: 32, + }) + } else { + RGB8::default() + } + })); + + //incremement the hue easing + j = j.wrapping_add(1); + + delay.delay_ms(10u8); + } +} diff --git a/boards/pygamer/examples/neopixel_button.rs b/boards/pygamer/examples/neopixel_button.rs new file mode 100644 index 00000000000..ce897e23bd3 --- /dev/null +++ b/boards/pygamer/examples/neopixel_button.rs @@ -0,0 +1,115 @@ +//! Joystick y controls the color of a neopixel while Joystick x moves it +//! left and right around the center neopixel +//! Select and Start control a second neopixel left and right while it is +//! automatically rotating through the color wheel +//! When they overlap, joystick takes precedence +//! +//! Note leds may appear white during debug. Either build for release or add +//! opt-level = 2 to profile.dev in Cargo.toml + +#![no_std] +#![no_main] + +#[cfg(not(feature = "panic_led"))] +use panic_halt as _; +use pygamer::{self as bsp, entry, hal, pac, pins::Keys, Pins}; + +use bsp::util::map_from; +use hal::adc::Adc; +use hal::{clock::GenericClockController, delay::Delay}; + +use pac::gclk::pchctrl::Genselect::Gclk11; +use pac::{CorePeripherals, Peripherals}; + +use hal::ehal::delay::DelayNs; + +use smart_leds::SmartLedsWrite; +use smart_leds::{ + hsv::{hsv2rgb, Hsv}, + RGB8, +}; + +#[entry] +fn main() -> ! { + let mut peripherals = Peripherals::take().unwrap(); + let core_peripherals = CorePeripherals::take().unwrap(); + + let mut clocks = GenericClockController::with_internal_32kosc( + peripherals.gclk, + &mut peripherals.mclk, + &mut peripherals.osc32kctrl, + &mut peripherals.oscctrl, + &mut peripherals.nvmctrl, + ); + + let mut delay = Delay::new(core_peripherals.SYST, &mut clocks); + let pins = Pins::new(peripherals.port).split(); + + let mut buttons = pins.buttons.init(); + + let mut adc1 = Adc::adc1(peripherals.adc1, &mut peripherals.mclk, &mut clocks, Gclk11); + let mut joystick = pins.joystick.init(); + + // neopixels + let mut neopixel = pins.neopixel.init_spi( + &mut clocks, + // Unfortunately, the SPI driver requires a clock pin, even though it's not used by the + // neopixels. + pins.i2c.scl, + peripherals.sercom2, + &mut peripherals.mclk, + ); + + const NUM_LEDS: usize = 5; + let mut pos_button: usize = 2; + let mut color_button: u8 = 0; + loop { + let (x, y) = joystick.read(&mut adc1); + + // map up/down to control rainbow color 0-255 + let color_joy = map_from(y as i16, (0, 4095), (0, 255)) as u8; + + // map left/right to neopixel position 0-4 + // joystick is not quite linear, rests at second pixel + // shifting up by 500 seems to help + let pos_joy = map_from(x as i16 + 500, (0, 4595), (0, 4)) as usize; + + for event in buttons.events() { + match event { + Keys::SelectDown => { + pos_button = pos_button.saturating_sub(1); + } + Keys::StartDown => { + if pos_button < 4 { + pos_button += 1; + } + } + _ => {} + } + } + + //finally paint the two leds at position, accel priority + let _ = neopixel.write((0..NUM_LEDS).map(|i| { + if i == pos_joy { + hsv2rgb(Hsv { + hue: color_joy, + sat: 255, + val: 32, + }) + } else if i == pos_button { + hsv2rgb(Hsv { + hue: color_button, + sat: 255, + val: 32, + }) + } else { + RGB8::default() + } + })); + + //incremement the hue easing + color_button = color_button.wrapping_add(1); + + delay.delay_ms(5); + } +} diff --git a/boards/pygamer/examples/neopixel_easing.rs b/boards/pygamer/examples/neopixel_easing.rs new file mode 100644 index 00000000000..43982c80b83 --- /dev/null +++ b/boards/pygamer/examples/neopixel_easing.rs @@ -0,0 +1,102 @@ +//! Randomly choose and led and color to breath in and out +//! +//! Note leds may appear white during debug. Either build for release or add +//! opt-level = 2 to profile.dev in Cargo.toml + +#![no_std] +#![no_main] + +#[cfg(not(feature = "panic_led"))] +use panic_halt as _; +use pygamer::{entry, hal, pac, Pins}; + +use core::f32::consts::FRAC_PI_2; + +use hal::clock::GenericClockController; +use hal::delay::Delay; +use hal::trng::Trng; + +use pac::{CorePeripherals, Peripherals}; + +use hal::ehal::delay::DelayNs; + +use micromath::F32Ext; +use smart_leds::SmartLedsWrite; +use smart_leds::{ + hsv::{hsv2rgb, Hsv}, + RGB8, +}; + +#[entry] +fn main() -> ! { + let mut peripherals = Peripherals::take().unwrap(); + let core = CorePeripherals::take().unwrap(); + let mut clocks = GenericClockController::with_internal_32kosc( + peripherals.gclk, + &mut peripherals.mclk, + &mut peripherals.osc32kctrl, + &mut peripherals.oscctrl, + &mut peripherals.nvmctrl, + ); + + let pins = Pins::new(peripherals.port).split(); + + // neopixels + let mut neopixel = pins.neopixel.init_spi( + &mut clocks, + // Unfortunately, the SPI driver requires a clock pin, even though it's not used by the + // neopixels. + pins.i2c.scl, + peripherals.sercom2, + &mut peripherals.mclk, + ); + + let mut delay = Delay::new(core.SYST, &mut clocks); + + let trng = Trng::new(&mut peripherals.mclk, peripherals.trng); + + const NUM_LEDS: usize = 5; + + loop { + let rand = trng.random_u8(); + let pos: usize = rand.wrapping_rem(5) as usize; //random led + + //slowly enable led + for j in 0..255u8 { + let _ = neopixel.write((0..NUM_LEDS).map(|i| { + if i == pos { + hsv2rgb(Hsv { + hue: rand, + sat: 255, + val: sine_ease_in(j as f32, 0.0, 32.0, 255.0) as u8, + }) + } else { + RGB8::default() + } + })); + delay.delay_ms(5); + } + + //slowly disable led - note the reverse .rev() + for j in (0..255u8).rev() { + let _ = neopixel.write((0..NUM_LEDS).map(|i| { + if i == pos { + hsv2rgb(Hsv { + hue: rand, + sat: 255, + val: sine_ease_in(j as f32, 0.0, 32.0, 255.0) as u8, + }) + } else { + RGB8::default() + } + })); + delay.delay_ms(5); + } + } +} + +#[inline] +// current step, where oputput starts, where output ends, last step +fn sine_ease_in(t: f32, b: f32, c: f32, d: f32) -> f32 { + -c * (t / d * FRAC_PI_2).cos() + c + b +} diff --git a/boards/pygamer/examples/neopixel_rainbow.rs b/boards/pygamer/examples/neopixel_rainbow.rs new file mode 100644 index 00000000000..d31d40d222a --- /dev/null +++ b/boards/pygamer/examples/neopixel_rainbow.rs @@ -0,0 +1,84 @@ +//! Rotate all neopixel leds through a rainbow. Uses a luckily placed set of SPI +//! pins as a timer source. +//! +//! Note leds may appear white during debug. Either build for release or add +//! opt-level = 2 to profile.dev in Cargo.toml + +#![no_std] +#![no_main] + +#[cfg(not(feature = "panic_led"))] +use panic_halt as _; +use pygamer::{entry, hal, pac, Pins}; + +use hal::{clock::GenericClockController, delay::Delay}; + +use pac::{CorePeripherals, Peripherals}; + +use hal::ehal::delay::DelayNs; + +use smart_leds::hsv::{hsv2rgb, Hsv}; +use smart_leds::SmartLedsWrite; + +#[entry] +fn main() -> ! { + let mut peripherals = Peripherals::take().unwrap(); + let core = CorePeripherals::take().unwrap(); + let mut clocks = GenericClockController::with_internal_32kosc( + peripherals.gclk, + &mut peripherals.mclk, + &mut peripherals.osc32kctrl, + &mut peripherals.oscctrl, + &mut peripherals.nvmctrl, + ); + let pins = Pins::new(peripherals.port).split(); + + // neopixels + let mut neopixel = pins.neopixel.init_spi( + &mut clocks, + // Unfortunately, the SPI driver requires a clock pin, even though it's not used by the + // neopixels. + pins.i2c.scl, + peripherals.sercom2, + &mut peripherals.mclk, + ); + + let mut delay = Delay::new(core.SYST, &mut clocks); + + loop { + for j in 0..255u8 { + let colors = [ + // split the color changes across all 5 leds evenly, 255/5=51 + // and have them safely wrap over when they go above 255 + hsv2rgb(Hsv { + hue: j, + sat: 255, + val: 32, + }), + hsv2rgb(Hsv { + hue: j.wrapping_add(51), + sat: 255, + val: 32, + }), + hsv2rgb(Hsv { + hue: j.wrapping_add(102), + sat: 255, + val: 32, + }), + hsv2rgb(Hsv { + hue: j.wrapping_add(153), + sat: 255, + val: 32, + }), + hsv2rgb(Hsv { + hue: j.wrapping_add(204), + sat: 255, + val: 32, + }), + ]; + + neopixel.write(colors.iter().cloned()).unwrap(); + delay.delay_ms(5); + } + } +} diff --git a/boards/pygamer/src/pins.rs b/boards/pygamer/src/pins.rs index d6fa0363b87..fe44eb0d95a 100644 --- a/boards/pygamer/src/pins.rs +++ b/boards/pygamer/src/pins.rs @@ -150,7 +150,8 @@ pub mod aliases { name: neopixel aliases: { PushPullOutput: NeopixelPin, - Reset: NeopixelReset + Reset: NeopixelReset, + AlternateC: NeopixelPinSpi, } }, /// Digital pin 9 @@ -665,6 +666,51 @@ pub struct Neopixel { pub neopixel: NeopixelReset, } +/// Pads for the labelled SPI pins +/// +/// According to the datasheet, the combination of PA17, PB22 & PB23 shouldn't +/// work, even though it does. We have added an undocumented `UndocIoSet1` to +/// `Sercom1` for this combination. +#[cfg(feature = "neopixel-spi")] +pub type NeopixelSpiPads = spi::Pads; + +/// SPI master for the labelled pins +#[cfg(feature = "neopixel-spi")] +pub type NeopixelSpi = spi::PanicOnRead, spi::Tx>>; + +#[cfg(feature = "neopixel-spi")] +impl Neopixel { + /// Setup the SPI driver for neopixel addressable LEDs. + /// + /// Unfortunately, the SPI uses SERCOM2, which is also used by the onboard + /// I2C. Therefore the I2C cannot be used simultaneously with the neopixels + /// in SPI mode. The SPI peripheral driver also requires PA12, aka SDA to + /// drive the SCLK signal. Even though it's not used by the LEDs, it's still + /// required to provide a clock pin in order to compile. + pub fn init_spi( + self, + clocks: &mut GenericClockController, + scl: impl hal::gpio::AnyPin, + sercom2: pac::Sercom2, + mclk: &mut pac::Mclk, + ) -> ws2812_spi::Ws2812 { + use hal::fugit::RateExtU32; + + let gclk0 = clocks.gclk0(); + let clock = &clocks.sercom2_core(&gclk0).unwrap(); + let pads = spi::Pads::default() + .data_out(self.neopixel) + .sclk(scl.into()); + let spi = spi::Config::new(mclk, sercom2, pads, clock.freq()) + .spi_mode(spi::MODE_0) + .baud(3.MHz()) + .enable() + .into_panic_on_read(); + + ws2812_spi::Ws2812::new(spi) + } +} + /// SPI pins pub struct SPI { pub mosi: SpiMosiReset,