Discolights
This article goes throught long time uncared-for application of traffic lights. Ambient discolights.
Realized with:
![micro:bit with STOP:bit mounted](./figure/eyelet-art/discolights/photo.jpg)
What to do? 🌐
Discolights turns on and off having some time interval for each phase. Thus some clock and light program is needed. Like struct
bellow. Program
encapsulates output pin for controlling LED light, array of intervals and field for next interval. On clock later.
struct Program<const N: usize> {
ctl: Pin<Output<PushPull>>,
schema: [i16; N],
next: usize,
}
Easy part but program needs to be initialized and driven to boot.
Preparation 🌐
Real time counter is clock of choice. Fits fine. More RTCs than one could be used if microbit::board::Board of microbit-0.13.0 would provide them. No problem. Nordic nRF52833 supports them. Image shows its diagram cut-off.
![nRF52833 cut-off](./figure/eyelet-art/discolights/diagram.png)
An of course if it was even needed.
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m::interrupt::{free as interrupt_free, Mutex};
use cortex_m_rt::entry;
use microbit::hal::gpio::{Level, Pin};
use microbit::hal::gpio::{Output, PushPull};
use microbit::hal::prelude::OutputPin;
use microbit::hal::{
pac::{interrupt, RTC0},
rtc::{Rtc, RtcInterrupt},
};
use microbit::board::Board;
use microbit::hal::clocks::Clocks;
use core::cell::RefCell;
use core::sync::atomic::{AtomicU16, Ordering};
static RED_PRG: Mutex<RefCell<Option<Program<10>>>> = Mutex::new(RefCell::new(None));
static YEL_PRG: Mutex<RefCell<Option<Program<8>>>> = Mutex::new(RefCell::new(None));
static GRE_PRG: Mutex<RefCell<Option<Program<13>>>> = Mutex::new(RefCell::new(None));
static RTC: Mutex<RefCell<Option<Rtc<RTC0>>>> = Mutex::new(RefCell::new(None));
#[entry]
fn entry() -> ! {
let brd = Board::take().unwrap();
let clocks = Clocks::new(brd.CLOCK);
clocks.start_lfclk();
const PRESCALER: u32 = 32;
let mut nvic = brd.NVIC;
let mut rtc = Rtc::new(brd.RTC0, PRESCALER).unwrap();
rtc.enable_interrupt(RtcInterrupt::Tick, Some(&mut nvic));
rtc.enable_counter();
let pins = brd.pins;
let red_ctl = pins.p0_02.into_push_pull_output(Level::Low).degrade();
let yel_ctl = pins.p0_03.into_push_pull_output(Level::Low).degrade();
let gre_ctl = pins.p0_04.into_push_pull_output(Level::Low).degrade();
let red_mix = [1000, -1200, 800, -300, 100, -100, 200, -200, 140, -134_i16];
let yel_mix = [800, -300, 330, -370, 550, -880, 123, -555_i16];
#[rustfmt::skip]
let gre_mix = [-890, 990, -1111, 876, -345, 875, -432, 777, -321, 444, -1000, 100, -100_i16,];
let red_prg = Program {
ctl: red_ctl,
schema: red_mix,
next: 0,
};
let yel_prg = Program {
ctl: yel_ctl,
schema: yel_mix,
next: 0,
};
let gre_prg = Program {
ctl: gre_ctl,
schema: gre_mix,
next: 0,
};
interrupt_free(move |cs| {
_ = RED_PRG.borrow(cs).borrow_mut().replace(red_prg);
_ = YEL_PRG.borrow(cs).borrow_mut().replace(yel_prg);
_ = GRE_PRG.borrow(cs).borrow_mut().replace(gre_prg);
_ = RTC.borrow(cs).borrow_mut().replace(rtc);
});
loop {}
}
Interesting parts:
- RTC — needs low frequency clock running, goes with prescaler to get frequency around 1000 Hz.
- Output pins — mapping of pins can be read out in micro:bit documentation, start low as push-pulls.
- Schemas — some disco-fancy timings in milliseconds, negative values are for off phase.
- RTC tick interrupt — “everything” is put into mutexes for access from interrupt handler.
1000 Hz means tick each ms. Because frequency of RTC is fixed and disco-timings are not, more RTCs would solve nothing and even would not be beneficial at all.
Drive 🌐
#[interrupt]
unsafe fn RTC0() {
static RED_COUNTDOWN: AtomicU16 = AtomicU16::new(1);
static YEL_COUNTDOWN: AtomicU16 = AtomicU16::new(1);
static GRE_COUNTDOWN: AtomicU16 = AtomicU16::new(1);
interrupt_free(|cs| {
let mut borrow = RTC.borrow(cs).borrow_mut();
let rtc = borrow.take().unwrap();
rtc.reset_event(RtcInterrupt::Tick);
borrow.replace(rtc);
});
if RED_COUNTDOWN.fetch_sub(1, Ordering::Relaxed) == 1 {
adv_prg(&RED_COUNTDOWN, &RED_PRG);
}
if YEL_COUNTDOWN.fetch_sub(1, Ordering::Relaxed) == 1 {
adv_prg(&YEL_COUNTDOWN, &YEL_PRG);
}
if GRE_COUNTDOWN.fetch_sub(1, Ordering::Relaxed) == 1 {
adv_prg(&GRE_COUNTDOWN, &GRE_PRG);
}
}
fn adv_prg<const N: usize>(countdown: &AtomicU16, prg: &Mutex<RefCell<Option<Program<N>>>>) {
let interval = interrupt_free(|cs| {
let mut borrow = prg.borrow(cs).borrow_mut();
let mut prg = borrow.take().unwrap();
let interval = adv_prg_nucleus(&prg.schema, &mut prg.next, &mut prg.ctl);
borrow.replace(prg);
interval
});
countdown.swap(interval, Ordering::Relaxed);
}
fn adv_prg_nucleus(schema: &[i16], next: &mut usize, ctl: &mut Pin<Output<PushPull>>) -> u16 {
let next_val = *next;
let mut interval = schema[next_val];
*next = (next_val + 1) % schema.len();
if interval < 0 {
_ = ctl.set_low();
interval *= -1;
} else {
_ = ctl.set_high();
}
interval as u16
}
// cargo flash --target thumbv7em-none-eabihf --chip nRF52833_xxAA --release --features panic_abort
[package]
name = "discolights"
authors = [ "software9119.technology" ]
version = "0.0.1-alpha"
edition = "2021"
license = "MIT"
[dependencies]
microbit-v2 = "0.13.0"
cortex-m = "0.7.3"
cortex-m-rt = "0.7.0"
panic-halt = "0.2.0"
Tick is generated every ms. Thus any concept of counter can be used to reach interval specified for light. Here countdown counter. Reason for adv_prg_nucleus
to exists is monomorphization. This reduces duplicates.
Abreast it nothing else extraordinary. Each counter is checked, substracted and set, next interval index is computed and light set high/low accordingly.
Outcome 🌐
![discolights show](./figure/eyelet-art/discolights/show.gif)
Now disco party can definitely begin. Working project for code of this article is available at discolights.