The perpetual QX5252 (Kelly's Light update)
Sometime ago now I made a "perpetual light" for my dog Kelly which used a QX5252 in SOT23-5 form ( blog and video). The idea was to make a random 4-light LED show which went day and night powered by a single 1.2V NiMH battery recharged daily by a small solar panel.
Although I don't like to dwell too much on this, these random happy perpetual lights are part of a memorial to a very special sweet puppy who left us under tragic circumstance way too early. We miss you Kelly!
By rights a single 1.2(ish)V battery should not be able to drive a microcontroller such as the ATTiny13 or the PFS154, let alone some LEDs as well! That's the joy of the Stable Joule Thief project which uses the QX5252 plus some other bits and pieces to provide a stable power output (at say 4V) for the microcontroller to do it's thing.
For a crazy idea years ago to be made flesh in these wonderful PCBs is extremely satisfying - they work better than I ever expected.
All was going well with "Kelly's Light" until the new puppies decided that munching on the old dog's light would be a great use of their growing teeth!
Exit one much loved project and enter the opportunity to update the concept to include a PFS154 Stable Joule Thief PCB that has recently arrived.
Originally I did not think that I would be able to use the PFS154 as the microcontroller for this project because I couldn't see how to make it sleep between blinks (important for lowering the power usage).
I looked at some old code and had a bit of a go at modifying this using the datasheet as a guide, and in the end I was able to make it work quite well!
There were also some issues associated with the GPIO naming not being conducive to easy manipulation using the old ATTiny13 code, and I solved this by making an array of values for the purpose of accessing all 16 values of LED combinations.
When a "random" number is generated, a light combo from the array is chosen and displayed - a bit clunky but it works well.
Here is the code for the original ATTiny13 version:
// -----------------------------------------------------------------
// Description: A lightshow built on four "random" LEDs which flick
// on or off for a random amount of time depending on which state
// is selected.
//
// Author: OneCircuit Date: 28/01/2021
// -----------------------------------------------------------------
//
// MicroChip ATTINY13 μC
//
// +-\/-+
// RESET--ACD0--5/A0--PCINT5--PB5 1| |8 VCC
// CLKI--ACD3--3/A3--PCINT3--PB3 2| |7 PB2--PCINT2--2/A1--SCK--ADC1
// ACD2--4/A2--PCINT4--PB4 3| |6 PB1--PCINT1---1---MISO--OCOB--INT0*
// GND 4| |5 PB0--PCINT0---0---MOSI--OCOA*
// +----+
// * indicates PWM port
//
#include <avr/sleep.h> // the sleep routines
byte getleds = 0b00000000; // initial all leds off
byte sleeptime = 0; // initial timer = 0
uint16_t myrand = 2901; // initial seed -> happy birthday
void setup() {
DDRB = 0b00001111; // only four outputs
PORTB = 0b11110000; // the rest input pullup
myrand = myrand+analogRead(A2); // reset the seed
}
ISR(WDT_vect) {
}
// generate a "random" number between small and big
uint16_t gimmerand(uint16_t small, uint16_t big) {
myrand ^= (myrand << 13);
myrand ^= (myrand >> 9);
myrand ^= (myrand << 7);
if (abs(myrand) % 13 == 0) {
myrand = myrand - 23;
}
if (abs(myrand) % 17 == 0) {
myrand = myrand + 11;
}
return abs(myrand) % 23 * (big - small) / 23 + small;
}
void powerDown(void)
{
MCUCR = 0b00110000; // see datasheet
WDTCR = 0b01000010; // 64ms
ADCSRA &= ~(1 << ADEN); // turn off ADC
ACSR |= (1 << ACD); // turn off Analog comparator.
sei(); // enable global interrupts
set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep deeply little one
sleep_enable(); // enable sleep mode
cli(); // Disable BOD steps
BODCR = (1 << BODSE) | (1 << BODS);
BODCR = (1 << BODS);
sei();
sleep_cpu();
sleep_disable(); // ISR routine returns here so wake up
ADCSRA |= (1 << ADEN); // turn on ADC
ACSR = (0 << ACD); // turn on Analog comparator.
delay(10); // settle time
}
void loop() {
getleds = gimmerand(0, 16); // returns 0..15
sleeptime = gimmerand(1, 25);
PORTB = getleds;
for (int howmanysleeps = 0; howmanysleeps < sleeptime; howmanysleeps++) {
powerDown();
}
}
And here is the updated (?) code for the PFS154:
/*
Candle with Three PWM
Pseudo-random flickering to simulate a candle. Output is
via 3xPWM channels, variables can be changed to
alter the simulation
Tue 17 Jan 2023 13:50:35 AEDT
*/
#include <stdint.h>
#include <stdlib.h>
#include "../device.h"
#include "../easy-pdk/calibrate.h"
#include "../auto_sysclock.h"
#include "../delay.h"
#include <stdbool.h>
#define LED4_BIT 4
#define LED0_BIT 0
#define LED3_BIT 3
uint16_t myrand = 2901; // happy birthday
uint8_t slowcounter = 0;
uint8_t medcounter = 0;
uint8_t fastcounter = 0;
uint8_t slowstart = 0;
uint8_t slowend = 0;
uint8_t medstart = 0;
uint8_t medend = 0;
uint8_t faststart = 0;
uint8_t fastend = 0;
uint8_t faster = 0;
uint8_t waveslow[] = {20, 30, 70, 100};
uint8_t wavemed[] = {25, 40, 100, 120};
uint8_t wavefast[] = {20, 30, 120, 140};
bool fastup = true;
bool slowup = true;
bool medup = true;
void mydelay(uint8_t counter) {
for (uint8_t thiscount = 0; thiscount <= counter; thiscount++) {
_delay_us(1);
}
}
uint16_t gimmerand(uint16_t small, uint16_t big) {
myrand ^= (myrand << 13);
myrand ^= (myrand >> 9);
myrand ^= (myrand << 7);
if (abs(myrand) % 13 == 0) {
myrand = myrand - 23;
}
if (abs(myrand) % 17 == 0) {
myrand = myrand + 11;
}
return abs(myrand) % 23 * (big - small) / 23 + small;
}
void getnewslow() {
slowstart = gimmerand(waveslow[0], waveslow[1]);
slowend = gimmerand(waveslow[2], waveslow[3]);
}
void getnewmed() {
medstart = gimmerand(wavemed[0], wavemed[1]);
medend = gimmerand(wavemed[2], wavemed[3]);
}
void getnewfast() {
faststart = gimmerand(wavefast[0], wavefast[1]);
fastend = gimmerand(wavefast[2], wavefast[3]);
faster = gimmerand(2, 6);
}
// Main program
void main() {
PAC |= (1 << LED4_BIT) | (1 << LED0_BIT) | (1 << LED3_BIT);
// see datasheet
PWMG1DTL = 0x00;
PWMG1DTH = 0x00;
PWMG1CUBL = 0xff;
PWMG1CUBH = 0xff;
PWMG1C = 0b10100111;
PWMG1S = 0b00000000;
PWMG0DTL = 0x00;
PWMG0DTH = 0x00;
PWMG0CUBL = 0xff;
PWMG0CUBH = 0xff;
PWMG0C = 0b10100111;
PWMG0S = 0b00000000;
PWMG2DTL = 0x00;
PWMG2DTH = 0x00;
PWMG2CUBL = 0xff;
PWMG2CUBH = 0xff;
PWMG2C = 0b10100111;
PWMG2S = 0b00000000;
getnewfast();
getnewslow();
getnewmed();
slowcounter = slowstart;
fastcounter = faststart;
medcounter = medstart;
// Main processing loop
while (1) {
// ramp up slow
if (slowup) {
slowcounter++;
if (slowcounter > slowend) { // ramp finished so switch boolean
slowup = !slowup;
}
}
else {
// ramp down slow
slowcounter--;
if (slowcounter < slowstart) { // ramp finished so switch boolean
slowup = !slowup;
getnewslow();
}
}
// ramp up med
if (medup) {
medcounter++;
if (medcounter > medend) { // ramp finished so switch boolean
medup = !medup;
}
}
else {
// ramp down med
medcounter--;
if (medcounter < medstart) { // ramp finished so switch boolean
medup = !medup;
getnewmed();
}
}
// ramp up fast
if (fastup) {
fastcounter = fastcounter + faster;
if (fastcounter > fastend) { // ramp finished so switch boolean
fastup = !fastup;
}
}
else {
// ramp down fast
fastcounter = fastcounter - faster;
if (fastcounter < faststart) { // ramp finished so switch boolean
fastup = !fastup;
getnewfast();
}
}
// delay + a re-purposed random for ramp speeds
mydelay(15 + faster);
PWMG1DTL = slowcounter & 255;
PWMG1DTH = slowcounter;
PWMG0DTL = fastcounter & 255;
PWMG0DTH = fastcounter;
PWMG2DTL = medcounter & 255;
PWMG2DTH = medcounter;
}
}
// Startup code - Setup/calibrate system clock
unsigned char _sdcc_external_startup(void) {
CLKMD = CLKMD_ILRC | CLKMD_ENABLE_ILRC | CLKMD_ENABLE_IHRC;
CLKMD = CLKMD_ILRC | CLKMD_ENABLE_ILRC;
PDK_SET_SYSCLOCK(SYSCLOCK_ILRC);
EASY_PDK_CALIBRATE_ILRC(50000, 4000);
return 0;
}
The soldering up of the lights and the board was so trivial compared to the earlier version.
Also, check out the "magnetic soldering" technique used around the 12 minute mark of the video below!
It all went well and the new (slightly expanded) version of the light is now sitting on the windowsill generating random combinations of LEDS night and day - "Kelly's Light" continues to shine.