Fairy Light 3000 – An exercise in real time programming and over-engineering

I bought some Fairy Lights from Bunning to use in the house, but they were way too bright, so I thought to myself, how can I dim them? This led me down a quite a rabbit hole. They are high voltage ~30V, they are wired with half of them with one polarity to the others.
So I starting with doing it with passive components and then thought it would be a easier with a microcontroller, the initial tests worked well, and then feature creep started and keep on going.

My aim was to have it as slick as an apple product, everything had to be real time, smooth fades etc. As I was doing the PWM manually in the Arduino, it meant that I only had had a few ms per frame to do any work. This is quite a challenge as various things take quite a while, I could only update 1 character on the OLD per frame for example, reading the clock took a while etc.

So this was a good exercise in writing a real time app with a very small about of time to do any work, as well as very limited resources (30k for application and 2k of RAM)

Features

  • Control of 2 sets of high voltage fairy lights
  • Turbo mod – high voltage for extra bright lights
  • Multiple patterns with tweakable controls
  • Clock with multiple displays.
  • Alarm with snooze, rising alarm volume, time limit.
  • Lights can be triggered by the alarm.
  • Light level sensor
  • USB programming port / Time Debug port for timing debugging
  • One press favorite light setting

Technical Details

Internals

The Arduino Nano is the brain of the device. This is connected to all the other parts:

  • The controls inputs – the Dial and the two switches (All use analog input pins)
  • The OLED screen uses the 4bit parallel interface mode.
    • The OLED was modified to allow its brightness to be controlled, this is attached to a one of the digital output of the Nano and uses PWM to set the brightness
  • The LED driver chips (L293D) is used to drive the fairy lights.
  • The Step-Up Regulator converts the 12V input into either 29V or 31V (depending on the relay setting of the Voltage Selector board which is controlled by a pin on the Nano). This is the normal vs turbo.
  • The Real Time Clock module uses I2C to talk to the Nano
  • The Light sensor an LDR connected directly to an analog input on the Nano

Rear Panel

Power

Power is center positive 12V

Lights

This is 5 pib DIN socket.

Pins 3 and 5 connect to the 1st Fairy lights strip

Pins 1 and 4 connect to the 2nd Fairy lights strip

Pin 2 is unused

Alarm

This is a buzzer. It can get very loud!

USB

This is used for firmware updates. The device can be powered of this, but then the fairy lights do not work

Time Debug

This is used for timing the device (See details in Theory of Operation).

Sleeve is ground.

Ring is the signal which controls the polarity of the LED driver.

Tip is the active signal sent to the LED driver to enable the LEDs.

To make use of this, connected Ring and Tip to an oscilloscope.

Theory of Operation

The application runs in a loop, the first part of which is acting as a PWM timing circuit for the LEDs and the remaining time in which cycle is spent doing other tasks, such as the clock, alarm, button handling, effects updates etc.

The main loop does the following:

  • Exec chunk (each chunk is round robined across different frames)
    • Chunk 1:
      • Get Time from RTC chip (slow!)
    • Chunk 2:
      • Check Buttons
      • Check Analog inputs
    • Chunk 3:
      • Create Time/Date string
      • Sleep Checks
      • Handle button presses
      • Check Alarm Conditions
      • Fade LCD update
      • Splash screen sequencing
      • Screen manager updates
  • Play alarm if sounding
  • Update Current Effect
  • Update OLED

The screen have a screen manager which has a list of screen sets, and each screen set has screen.

A screen is defined with a draw() and tick() method and the manager controls change the screen sets, and the screen sets control the screens.

Each screen also has a list of setting, which have draw() and tick() methods

Timing

Each cycle is as long as the blue state below.

(The yellow is the active signal (tip), and blue the polarity (ring) and for the above #define SPIKE was uncommented in the code.)

Looking at the yellow trace, the solid block as the start is where the PWM phase is handled, in the above brightness is full (so about 2/3 of the full duty cycle is achievable), the remaining 1/3 is where all other operations occur.

In the code, this is broken in different chunks which can cycled through and handled on different frames (for non-time critical things)

The only things which happen per frame are the LED updates, and the updates to the OLED.

The OLED is so slow that it can’t be sent more than 1 character per frame (or for font changes, the font data is send in groups of 4 bytes per frame)

The  ‘Spikes’ visible on the yellow trace are put into the code at various places:

  • After the chunk processing
  • After the alarm/effect update
  • After The OLED update

On the left of the trace the spikes are clearly visible, and the 3rd one is almost at the start of the new cycle. This is the optimal balance point, where it almost touches. The worst thing to do is change the view on the minimal screen as this does font changes which is the slowest op.

Letting the 3rd spike bleed into the next frame will cause the whole frame to be skipped and flickering will be noticeable on the LEDS.

In BunningFairyDriver2.ino the period of the cycle can be set here:

Values much larger than this will cause visible flickering on the LEDs and strobing on the OLED backlight.

If code is added which cause flickering then the maximum time the lights can be reduced (essentially shortening the initial yellow period, which gives more time to updates etc. by decreasing the number in L293DLED.cpp The downside is the maximum brightness of the LEDs is reduced)

If possible, it is better to move code into a new chunk to spread out the load across frames.

User manual: