Thursday, January 26, 2023

0000 0000 1011 1010

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.



Friday, January 20, 2023

0000 0000 1011 1001

3D LED Candle

PCBWay recently and very kindly sent me some PCBs that I designed to give a more realistic candle effect.

I made a video and a blog about this PCB in another context, using it to make a solar temperature indicator.

The theory is based on separation - a "single point" candle is only going to vary light strength (and possibly frequency), but it cannot give that "side to side" movement that a real flame experiences as it sways in the vortex of it's own gases.

And so I thought as the Padauk PFS154 has three available PWM channels I would make a large-ish LED PCB that fits three lights and perhaps would give that "real world" swaying  3D effect missing from all of my previous incarnarnations.


Very simple to solder up, the PCB works a treat - but will it be candley enough for me in this configuration?

The code to drive this simulation is constantly being tinkered with, but basically this version is as follows:

/*
  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(1, 3);
}

// 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(2 + 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) {

  // AUTO_INIT_SYSCLOCK();

  PDK_SET_SYSCLOCK(SYSCLOCK_ILRC_DIV4);

  // AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV);

  EASY_PDK_CALIBRATE_ILRC(19000, 4000);

  return 0;
}

Finally I made up a Padauk based stable joule thief PCB to link to the LEDs as a solar based power source, and here's the result.

Let me know if you want any gerber files for these boards, and I'd love some feedback on the whole concept of a fake 3D candle!

Comments under the YT video shown below would be awesome.



Friday, January 13, 2023

0000 0000 1011 1000

A trickle of joy

Fresh from re-entry are a few singed parcels. Let's hope that what is inside has survived the trip!



Friday, January 6, 2023

0000 0000 1011 0111

NiMH Solar Trickle Charger

Awhile ago Big Clive posted a video and some gerber files based around a design for a trickle charger suitable for Nickel Metal Hydride rechargeable batteries (and I think NiCads as well).


One interesting comment during the build from Clive was around the use of a solar panel instead of a 5V source (e.g. via 5V regulator and USB connector).

So it came to pass that I ordered the board and decided to put one of these together and try a 5V solar panel for the current source.

The result is a VERY long rambling video (37 minutes as opposed to Clive's original 23 minutes!) which outlines the process of putting this thing together and testing it. Enjoy!