FriendlyWire.com


beginner-friendly electronics tutorials and projects


[[slogan]]

Recreating a retro flashing Christmas star

December 20, 2025 project

When I think about Christmas and the 90's (a.k.a. my childhood), one thing comes to mind: colorful, flashing Christmas decoration. And: no LEDs! Everything glows incandescently, and is surrounded by loads of tinsel. For some reason, some 30 years later, these decorations are no longer being made. So this year, let's make our own and recreate a retro flashing Christmas star!

Inspiration

This is an original Christmas star from the 90's (photo courtesy of “Der Kirmesfan”, see also this video):

When I was a kid, and I grew up in the 90s, these things here were all the rage. They came in different colors, sizes, and shapes, and, most importantly, they blink. A lot. And how they blink is quite interesting, too, because instead of electronics they use bimetallic switches that are embedded inside the lightbulbs:

But unfortunately these incandescent lights can break, and replacements are quite expensive. And not just the bulbs, the stars themselves are expensive, too, because they are no longer being made. So for the last project of the year I thought I'd invite you on my journey to recreate one of these Christmas stars with everyday materials.

What you need

But before we get into the details of the design materials, let's first make sure the electronics part is taken care of. First of all, you need a 5V WS2811 “NeoPixel” LED strip with 50 LEDs:

Next, you also need an ordinary 5V USB power supply. And other than that, here is what you need:

  • First, we need a USB adapter to connect power.
  • . The PIC16F1455 microcontroller, with a 14-pin socket;
  • . a MAX7219 7-segment LED module; . and a rotary encoder. . The DS1302 real-time clock IC, together with an 8-pin socket, a coin cell battery holder and a 32.768 kHz watch crystal, which you can all find on a single board for very cheap, . and, finally, a 4.7 kilo ohms resistor, a 100 microfarad capacitor, and two 100 nF capacitors.

And, as always, you can find links on where to buy all of these components in the components box.

Schematic

And this is the main idea of the schematic:

The PIC controls our LED strip by sending out the red, green and blue color data, and we control the settings with a rotary encoder that also has a built-in pushbutton. All settings are shown on the eight-digit 7-segment display. The real-time clock IC provides the time information for the timer that turns our star on and off, and because that chip has a built-in battery we can also use it to store all settings. And we will use the Snap or the PICkit3 as a programmer to flash our program that we will write later onto the PIC microcontroller.

And we actually know all of these components already, because we have covered all of these topics in previous videos:

If you are interested, here are the links to the relevant articles:

So, this was the main idea, and here is the actual schematic:

We have already talked about the main idea, but here are some additional details:

  • Both the PIC16F1455 and the DS1302 get a 100nF bypass capacitor close to them to remove possible noise on the power supply, and we also add a chunky 100μF capacitor to help prevent power dips when a lot of LEDs turn on all at once.
  • The 4.7kΩ resistor at the rotary encoder's pushbutton acts as a pullup resistor. For the pins A and B of the rotary encoder we don't need it, because the PIC has built-in pullups on the pins RA4 and RA5. It also has one on RA3, which we could have used, but because in that case we could not use the Snap to flash our PIC I wanted to avoid it and decided to connect the pushbutton to RA1 instead and add the pullup resistor manually, oh well.
  • At the real time clock IC, G1 is the backup battery, and a simple CR2032 coin cell battery will last many years, and Q1 is the clock crystal that generates the 1Hz timebase for our clock.

Now I usually build these circuits on breadboards, but for this project I wanted everything to be a bit more stable and portable, and for that reason I mounted everything on perfboards and actually soldered everything by hand, and this is how it looks like:

The USB adapter is at the bottom right, and the three-terminal cable is where we connect the WS2811 LED strip. The rotary encoder and the 7-segment displays are on separate boards, and the main board holds the PIC, the real-time clock, and the coin cell battery. I glued the wires in place with hot glue to act as a strain relief because I did not want any of the wires to break.

Software

Now of course on its own this circuit doesn't do anything, we need to program it first. And the main job of the code is of course to replicate the appearance of the flashing stars. When I looked at the video of one of the stars I noticed that there is no fixed pattern, but instead the groups of lights turn ON and OFF for a fixed amount of time:

But those ON and OFF times are not the same across the groups, and that is what creates these interesting patterns. In the code there are two modes, a fast mode and a slow mode, so: two sets of time variables. You can find them here:

        // *****
        // animation
        
        // set the animation speed for each LED ring
        // (ring #1 = innermost LED, ring #4 = outermost LEDs)
        // (time conversion: 1 second = appr. 225 ticks)
        
        // slow settings
        if (speed_factor == 0) {
            
            R1on =  693; R1off = 489;
            R2on = 1201; R2off = 433;
            R3on =  989; R3off = 327;
            R4on = 1695; R4off = 234;
            
        // fast settings
        } else {
            
            R1on = 330; R1off = 120;
            R2on = 330; R2off = 120;
            R3on = 248; R3off = 110;
            R4on = 128; R4off = 165;
            
        }

The way that I programmed it, a time of 225 corresponds to one second, give or take. And that's the nice thing: when you can program the controller yourself, you can adjust these times exactly how you like :)

The source code for this project ended up being more than a thousand lines of code, and you can download the main.c file in the resources box and have a look at it in the appendix. But we don't actually have to go through all of this because a lot of it is from previous projects:

As you can see by the colors, only the magenta part is responsible for turning the LEDs on and off.

The main idea of this code is very close to that of the improved binary clock, so that is a good starting point if you want to understand the details.

A major difficulty for me was that the PIC16F1455's memory space is rather limited, and the LED color calculations and also the dimming required floating point multiplication inside the code, which is very memory-intensive on a simple 8-bit processor. Viewed from that perspective, including the code for the different components from the previous projects was not difficult at all. The challenge was making sure that we didn't run out of memory.

As always you can find the code in the appendix and I added many comments inside of it, but if you have any questions about it, please feel free to reach out to me and I will do my best to get back to you :)

Flashing

But now we can take this code and turn it into a .hex-file inside the MPLAB IDE, or you can also download the .hex-file directly from the resources box.

To flash this .hex-file onto the PIC16F1455 with the MPLAB Snap or the PICkit3, place the PIC it on a small breadboard, and connect the MPLAB Snap with the lines VDD, GND, Master Clear, Program Data, and Program Clock:

Then don't forget to added a 4.5V battery power supply because the Snap doesn't have built-in one. Inside the MPLAB IPE, we select the .hex-file we just created or downloaded and flash it onto the PIC16F1455:

And then, believe it or not, we are done with the electronics part of the project!

Construction

But, honestly, at this point all we have done is build overly complicated fairy lights. The main appeal of the Christmas stars comes, yes, from the blinking colors, but also from the materials used.

One main ingredient is Christmas tinsel. This is easy to find around Christmas time in just about any decorating store, just make sure you get non-metallic one that is non-conducting. I went for a silver one that works well with virtually any colors:

But the other ingredient are the specially made starburst reflectors that notoriously go missing on used stars that you can still buy nowadays. They are made of clear plastic and have a really unique shape, and they are what makes the stars really sparkle and shine:

And if you don't have a resin 3D printer, like me, that looks like a problem.

But around that time I was looking at Christmas tree ornaments, and I realized that the Christmas ornament caps themselves, which are made of very thin sheet metal, are perfect for this job!

All we need is punch a hole into them with a leather hole punch:

Then we bend them a bit, and then place them on top of the WS2811 NeoPixel LEDs. And here is a small test I made with just three LEDs, but I think you can see this has potential:

Now I only had to make 48 more of them. Well, actually only 47, because for the LED in the middle I decided to use a metal Christmas candle holder, which is a little bit bigger and should work well as a centerpiece:

Fair warning, though, these Christmas tree holders are tough to work with, because they are made of steel, so you definitely need a Dremel or something like that to cut off the parts we don't need and somehow drill a hole into it.

But at that point I got very excited, because it looked like the project was going to be a success. Now, the original Christmas stars have all lights mounted on a specially made frame, but I always found that a bit flimsy, and I did not know how to fabricate one. But then I discovered that an ordinary acrylic cake plate is the perfect choice, and they even come with nice patterns built in:

So I recreated the traditional arrangement, even though this cake plate is 30 cm or 12 inches, which is bigger than the original stars:

Next, I drilled a 3mm hole in the center, and then stole our cat's toy string to mark three concentric circles:

This is acrylic so you have to be careful when drilling it so that it does not crack, but I found that a step drill works great if you take your time.

But yeah, this makes quite a mess:

And then it was time to glue in the LEDs.

I used hot glue for this, and it took forever because the holes were 13mm and the LEDs are 12mm. But this was not a mistake, we actually need the LEDs to stick out of the plate far enough so that later they don't get covered by the tinsel.

They did not all come out perfectly straight, but this kind of project is very forgiving with such things.

And at that point I couldn't wait any longer and placed the reflector caps on the LEDs to get some idea how it would look like.

The 12mm LED diameter was a snug fit for most of them, but I may go back later and fix them in place with superglue just to be safe.

And then it was time to decorate and wrap everything in tinsel, which actually gets held in place by the reflector caps and does not need any extra glue, just the end needs to be tucked away neatly. This takes some time to do, because some of the tinsel will cover the reflector caps, but it was a strangely satisfying experience so I didn't mind. And it really creates that 90's look:

The middle part was still looking a bit empty, so I cut three strips of silver ribbon...

...placed them on top of the center LED, and then fastened it in place with the reflector cap. Again, this holds well without glue but I may add a drop of superglue later just in case.

Then it was time to mount the electronics, and I used M3 brass standoffs for that:

I attached some hardware designed to hang picture frames...

...and then the star was done!

And here it is, in all its glory. I am super happy with how it turned out, it is exactly how I remember them from my childhood. And for me the best part is the functionality.

The built-in menu

But speaking of functionality, how do we actually adjust the colors? The star has a built-in menu:

For each menu item you adjust the value with the rotary encoder, and push it to get to the next menu item.

  1. When you plug in the star, it just shows the current time, so it kind of doubles as a clock.
  2. But when you turn the button, you can select between the four available styles, and confirm your choice by pressing the button.
  3. Next are the four colors, 1 is the middle LED, and 4 is the outer ring. You can cycle through all available colors by turning the button, and then confirm each color by pressing it.
  4. Then you can adjust the time when the star turns on, that's why it says “on” on the left. Select the hours, press the button, and then select the minutes and press the button again.
  5. Then, you guessed it, we adjust when the star turns off in the same way.
  6. And then, after confirming, we can set the current time, and you only have to do that when you power it up for the first time.
  7. And that's the whole menu, and after pressing the button for a final time we are back in the style menu from the beginning. Here we just let it sit, and eventually it will go back to just showing the time.

So I hope this one-button menu style is not too confusing :) And you can really be creative, there are so many color combinations, muted and tacky, see below!

YouTube video

But before we take a look at the final result, I just wanted to let you know that as always I covered this entire project in a dedicated YouTube video:

Final thoughts

And here are some of our favorite color combinations. The traditional green-yellow-red combo that may or may not be considered a bit tacky...

...and then a slightly colder blue-ish purple palette...

but of course you can also choose all colors to be the same:

And the best part: if you unplug it after this year's Christmas, it will still remember the setting next year around the same time :)

I really love how this star turned out, it may be my favorite project to date, and I hope I could inspire you today to go and build something yourself. Thank you so much for being here, and let me know if you have any questions. Happy holidays!

Appendix: the full source code

Here you can find the full source code. The code (as well as the .hex file) are also available for download in the resources box.

/*
 * File:   main.c
 * Author: boos
 *
 * Created on November 1, 2025, 3:00 PM
 */

// CONFIG1
#pragma config FOSC = INTOSC    // Oscillator Selection Bits (INTOSC oscillator: I/O function on CLKIN pin)
#pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
#pragma config MCLRE = ON       // MCLR Pin Function Select (MCLR/VPP pin function is MCLR)
#pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
#pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
#pragma config IESO = OFF       // Internal/External Switchover Mode (Internal/External Switchover Mode is disabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

// CONFIG2
#pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
#pragma config CPUDIV = NOCLKDIV// CPU System Clock Selection Bit (NO CPU system divide)
#pragma config USBLSCLK = 48MHz // USB Low Speed Clock Selection bit (System clock expects 48 MHz, FS/LS USB CLKENs divide-by is set to 8.)
#pragma config PLLMULT = 3x     // PLL Multipler Selection Bit (3x Output Frequency Selected)
#pragma config PLLEN = ENABLED  // PLL Enable Bit (3x or 4x PLL Enabled)
#pragma config STVREN = OFF     // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will not cause a Reset)
#pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
#pragma config LPBOR = OFF      // Low-Power Brown Out Reset (Low-Power BOR is disabled)
#pragma config LVP = ON         // Low-Voltage Programming Enable (Low-voltage programming enabled)

#include <xc.h>

// allows the use of __delay_ms()
#define _XTAL_FREQ 48000000


// rotary encoder

// connections
#define ENC_SW      (!RA1)
#define ENC_A       (!RA5)
#define ENC_B       (!RA4)
#define ENC_SW_TRIS TRISA1
#define ENC_A_TRIS  TRISA5
#define ENC_B_TRIS  TRISA4

// global variables used for the rotary encoder (we add "volatile" wherever they are used and updated in the ISR)
volatile int rotary_pos, rotary_pos_before, rotary_diff;
volatile int rotary_value = 0;
int rotary_value_old = 0, rotary_value_change = 0;

// global debounce variable for the rotary encoder's pushbutton
// (is altered in the ISR, so we make it "volatile")
volatile int sw_buffer;

// function that converts Gray code into a binary number (for rotary encoder)
int gray_to_binary (void);


// DS1302 real-time clock IC

// connections
#define DS1302_CLK      RC3
#define DS1302_IO       RC4
#define DS1302_CE       RC5
#define DS1302_CLK_TRIS TRISC3
#define DS1302_IO_TRIS  TRISC4
#define DS1302_CE_TRIS  TRISC5

// functions for the DS1302 real-time clock
void DS1302_send (int value);
unsigned char DS1302_get (void);
void DS1302_set_bcd (int address, int data);
int DS1302_get_bcd (int address);
void DS1302_set_seconds (int data);
void DS1302_set_minutes (int data);
void DS1302_set_hours (int data);
int DS1302_get_seconds (void);
int DS1302_get_minutes (void);
int DS1302_get_hours (void);
void DS1302_start (void);
void DS1302_stop (void);
void DS1302_set24 (void);


// WS2812 LED

// connection
#define WS2812_DATA         RC2
#define WS2812_DATA_TRIS    TRISC2

// functions to control the WS2812 NeoPixel LEDs
#define WS2812_send_bit(b) WS2812_DATA=1; NOP(); NOP(); NOP(); WS2812_DATA=b; NOP(); NOP(); NOP(); NOP(); WS2812_DATA=0; NOP(); NOP(); NOP(); NOP();
void WS2812_send_byte (unsigned char b);
void WS2812_send_RGB (unsigned char r, unsigned char g, unsigned char b);


// MAX7219

// connections
#define MAX7219_CLK         RC3
#define MAX7219_DATA        RC0
#define MAX7219_LOAD        RC1
#define MAX7219_CLK_TRIS    TRISC3
#define MAX7219_DATA_TRIS   TRISC0
#define MAX7219_LOAD_TRIS   TRISC1

// define MAX7219 register addresses (for convenience)
#define MAX7219_MODE_NOP       0b00000000
#define MAX7219_MODE_DECODE    0b00001001
#define MAX7219_MODE_INTENSITY 0b00001010
#define MAX7219_MODE_SCANLIMIT 0b00001011
#define MAX7219_MODE_SHUTDOWN  0b00001100
#define MAX7219_MODE_TEST      0b00001111

// define MAX7219 commands (for convenience)
#define MAX7219_NO_DECODE      0b00000000
#define MAX7219_7SEG_DECODE    0b11111111

// define functions
void MAX7219_send (unsigned char a, unsigned char d);
void MAX7219_update (void);

// 7-segment code for the numbers 0-9
//                            .abcdefg
unsigned char get7seg[] = { 0b01111110,
                            0b00110000,
                            0b01101101,
                            0b01111001,
                            0b00110011,
                            0b01011011,
                            0b01011111,
                            0b01110000,
                            0b01111111,
                            0b01111011 };


// Christmas star-specific

// helpful function that allows us to sweep over a lot of colors with a single variable h (taking values from 0..255)
// (inspired by the curves presented on https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors)
// Results are stored in the varibales rr, gg, and bb after calling calc_hsv(hue);
void calc_hsv (int h);
int rr, gg, bb;

// variables to control the menu
volatile int menu_timeout = 0;

// time information is global for simplicity
int hours, minutes, seconds;
int hours_on, minutes_on, hours_off, minutes_off;


// *****
// main function
void main (void) {
    
    
    // *****
    // set up internal oscillator to run at 48 MHz
    
    // set the SCS settings (system clock select)
    // to use the frequency specified in configuration word (line 22)
    SCS0 = 0;
    SCS1 = 0;
    
    // set to 16 MHz configuration
    IRCF0 = 1;
    IRCF1 = 1;
    IRCF2 = 1;
    IRCF3 = 1;
    
    // enable PLL to result in 48MHz
    // (PLL is set to 3x in line 23)
    SPLLEN = 1;
    
    
    // *****
    // set up peripherals
    
    // WS2812_DATA is an output
    WS2812_DATA_TRIS = 0;
    
    // DS1302_CLK is an output
    DS1302_CLK_TRIS = 0;
    
    // DS1302_IO is usually an input
    // (unless we are reading in data)
    DS1302_IO_TRIS = 1;
    
    // DS1302_CE is an output
    DS1302_CE_TRIS = 0;
    
    // MAX7219 CLK is shared with DS1302
    MAX7219_CLK_TRIS = 0;
    
    // MAX7219 DATA is an output
    MAX7219_DATA_TRIS = 0;
    
    // MAX7219 LOAD is an output
    MAX7219_LOAD_TRIS = 0;
    
    // turn off all analog to digital converters
    ANSA4 = 0;
    ANSC0 = 0;
    ANSC1 = 0;
    ANSC2 = 0;
    ANSC3 = 0;
    
    // the rotary encoder pins are all inputs
    // (RA1 can only be an input, so we don't have to set its tristate register)
    ENC_A_TRIS = 1;
    ENC_B_TRIS = 1;
    
    // enable the weak internal pullup resistors on the rotary encoder inputs RA4, RA5 (RA1 needs an external pullup resistor)
    WPUA4 = 1;
    WPUA5 = 1;
    nWPUEN = 0;

    
    // *****
    // configure TIMER0 (we use it for reading out the rotary encoder)
	// (see Sec. 19 in the PIC16F1455 datasheet for specifications)
    TMR0CS = 0;                 // internal oscillator (Fosc/4)
    PSA = 0;                    // prescaler ON
    PS2 = 0; PS1 = 1; PS0 = 1;  // set to 1:16
    TMR0IE = 1;                 // enable interrupt on overflow
    GIE = 1;                    // enable global interrupts
    

    // *****
    // variables
    
    
    // 16 LED brightness levels
    // (We use this as a lookup table, feel free to change your brightness levels between 0 and 1.)
    float brightness_table[] = {0, 0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.08, 0.1, 0.19, 0.25, 0.34, 0.42, 0.52, 0.7, 1};
    unsigned char brightness_max = 15;
    
    // variable for the menu
    unsigned char menu_index = 0;
    
    // variables for the color information
    unsigned char hue1, hue2, hue3, hue4, mode_index;
    unsigned char brightness_factor = 0, speed_factor = 0;
    unsigned char R1red = 0, R1green = 0, R1blue = 0;
    unsigned char R2red = 0, R2green = 0, R2blue = 0;
    unsigned char R3red = 0, R3green = 0, R3blue = 0;
    unsigned char R4red = 0, R4green = 0, R4blue = 0;
    unsigned char dimmedR1red = 0, dimmedR1green = 0, dimmedR1blue = 0;
    unsigned char dimmedR2red = 0, dimmedR2green = 0, dimmedR2blue = 0;
    unsigned char dimmedR3red = 0, dimmedR3green = 0, dimmedR3blue = 0;
    unsigned char dimmedR4red = 0, dimmedR4green = 0, dimmedR4blue = 0;

    // variables for the animation
    unsigned char animation_on = 1;
    int R1timebase = 0, R1on, R1off, R1brightness_index;
    int R2timebase = 0, R2on, R2off, R2brightness_index;
    int R3timebase = 0, R3on, R3off, R3brightness_index;
    int R4timebase = 0, R4on, R4off, R4brightness_index;
    
    
    // *****
    // set up DS1302 real-time clock IC
    
    // clear the WRITE PROTECTION bit
    DS1302_CE = 1;
    DS1302_send(0x8e);
    DS1302_send(0);
    DS1302_CE = 0;
    
    // read stored brightness preference from DS1302 custom memory
    DS1302_CE = 1;
    DS1302_send(0xc1);
    mode_index = DS1302_get();
    DS1302_CE = 0;
    rotary_value = 4*mode_index;
    menu_timeout = 0;
    
    // read stored hue preferences from DS1302 custom memory
    DS1302_CE = 1;
    DS1302_send(0xc3);
    hue1 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc5);
    hue2 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc7);
    hue3 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xc9);
    hue4 = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcb);
    hours_on = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcd);
    minutes_on = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xcf);
    hours_off = DS1302_get();
    DS1302_CE = 0;
    
    DS1302_CE = 1;
    DS1302_send(0xd1);
    minutes_off = DS1302_get();
    DS1302_CE = 0;
    
    // always set DS1302 to 24-hour mode
    // (the conversion to 12-hour mode, if required, is done by us in the code.)
    DS1302_set24();
    
    
    // *****
    // set up MAX7219 in 7-segment module
	
    // scan all eight rows
    MAX7219_send(MAX7219_MODE_SCANLIMIT, 7);
    MAX7219_update();
    
    // set MAX7219 to no-decoding mode
	// (we are specifying the pattern manually)
    MAX7219_send(MAX7219_MODE_DECODE, MAX7219_NO_DECODE);
    MAX7219_update();
    
    // set MAX7219 brightness to minimum
    // (any number from 0-15 works)
    MAX7219_send(MAX7219_MODE_INTENSITY, 1);
    MAX7219_update();
    
    // turn ON
    MAX7219_send(MAX7219_MODE_SHUTDOWN, 1);
    MAX7219_update();
    
    
    // *****
    // main loop
    while (1) {
        
        
        // *****
        // extract 'style' parameters for the animnation mode
        brightness_factor = (unsigned char) ((mode_index & 1) << 1);
        speed_factor = (mode_index >> 1);

        
        // *****
        // animation
        
        // set the animation speed for each LED ring
        // (ring #1 = innermost LED, ring #4 = outermost LEDs)
        // (time conversion: 1 second = appr. 225 ticks)
        
        // slow settings
        if (speed_factor == 0) {
            
            R1on =  693; R1off = 489;
            R2on = 1201; R2off = 433;
            R3on =  989; R3off = 327;
            R4on = 1695; R4off = 234;
            
        // fast settings
        } else {
            
            R1on = 330; R1off = 120;
            R2on = 330; R2off = 120;
            R3on = 248; R3off = 110;
            R4on = 128; R4off = 165;
            
        }

        // run the animation
        if (animation_on == 1) {
        
            R1timebase++;
            R2timebase++;
            R3timebase++;
            R4timebase++;
            
            if (speed_factor == 0) {
                
                if (R1timebase < R1on) {
                    if ((R1brightness_index < brightness_max) && (R1timebase % 2 == 0)) {
                        R1brightness_index++;
                    }
                } else if (R1timebase > R1on + R1off)  {
                    R1timebase = 0;
                } else if (R1timebase > R1on)  {
                    if ((R1brightness_index > 0) && (R1timebase % 2 == 0)) {
                        R1brightness_index--;
                    }
                }
                if (R2timebase < R2on) {
                    if ((R2brightness_index < brightness_max) && (R2timebase % 2 == 0)) {
                        R2brightness_index++;
                    }
                } else if (R2timebase > R2on + R2off)  {
                    R2timebase = 0;
                } else if (R2timebase > R2on)  {
                    if ((R2brightness_index > 0) && (R2timebase % 2 == 0)) {
                        R2brightness_index--;
                    }
                }
                if (R3timebase < R3on) {
                    if ((R3brightness_index < brightness_max) && (R3timebase % 2 == 0)) {
                        R3brightness_index++;
                    }
                } else if (R3timebase > R3on + R3off)  {
                    R3timebase = 0;
                } else if (R3timebase > R3on)  {
                    if ((R3brightness_index > 0) && (R3timebase % 2 == 0)) {
                        R3brightness_index--;
                    }
                }
                if (R4timebase < R4on) {
                    if ((R4brightness_index < brightness_max) && (R4timebase % 2 == 0)) {
                        R4brightness_index++;
                    }
                } else if (R4timebase > R4on + R4off)  {
                    R4timebase = 0;
                } else if (R4timebase > R4on)  {
                    if ((R4brightness_index > 0) && (R4timebase % 2 == 0)) {
                        R4brightness_index--;
                    }
                }
                
            } else {

                if (R1timebase > R1on + R1off) {
                    R1brightness_index = 0;
                    R1timebase = 0;
                } else if (R1timebase > R1on) {
                    R1brightness_index = brightness_max;
                }
                if (R2timebase > R2on + R2off) {
                    R2brightness_index = 0;
                    R2timebase = 0;
                } else if (R2timebase > R2on) {
                    R2brightness_index = brightness_max;
                }
                if (R3timebase > R3on + R3off) {
                    R3brightness_index = 0;
                    R3timebase = 0;
                } else if (R3timebase > R3on) {
                    R3brightness_index = brightness_max;
                }
                if (R4timebase > R4on + R4off) {
                    R4brightness_index = 0;
                    R4timebase = 0;
                } else if (R4timebase > R4on) {
                    R4brightness_index = brightness_max;
                }
                
            }
        
        // no animation when we are in one of the menus
        } else {
         
            R1brightness_index = brightness_max;
            R2brightness_index = brightness_max;
            R3brightness_index = brightness_max;
            R4brightness_index = brightness_max;
            
        }
        
        
        // *****
        // read out data from DS1302
        
        // retrieve time
        minutes = DS1302_get_minutes();
        hours = DS1302_get_hours();
        
        
        // *****
        // Did the rotary encoder turn?
        // (We check for this so that we only update the information stored
        // in the DS1302 whenever something actually has been changed. This
        // reduces the amount of unnecessary write operations to the DS1302.)
        if (rotary_value != rotary_value_old) {
            rotary_value_change = 1;
            rotary_value_old = rotary_value;
        } else {
            rotary_value_change = 0;
        }
        
        
        // *****
        // menu functionality
        
        // standard operation, rotary encoder changes operation mode
        if (menu_index == 0) {
            
            // rotary encoder value is mapped to 0..3
            mode_index = (unsigned char) (rotary_value / 4) % 4;
            if (mode_index < 0) {
                mode_index = 3;
                rotary_value = 4*mode_index;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc0);
                DS1302_send(mode_index);
                DS1302_CE = 0;
                menu_timeout = 15000;
            }

            // show the time in normal operation
            if (menu_timeout == 0) {
            
                //                .abcdefg
                MAX7219_send(8, 0); MAX7219_update();
                MAX7219_send(7, 0); MAX7219_update();
                MAX7219_send(6, get7seg[hours / 10]); MAX7219_update();
                MAX7219_send(5, 0b10000000 | get7seg[hours % 10]); MAX7219_update();
                MAX7219_send(4, get7seg[minutes / 10]); MAX7219_update();
                MAX7219_send(3, get7seg[minutes % 10]); MAX7219_update();
                MAX7219_send(2, 0); MAX7219_update();
                MAX7219_send(1, 0); MAX7219_update();
            
            // otherwise, show the style menu
            } else {
                
                //                .abcdefg
                MAX7219_send(8, 0b01011011); MAX7219_update();
                MAX7219_send(7, 0b00001111); MAX7219_update();
                MAX7219_send(6, 0b00111011); MAX7219_update();
                MAX7219_send(5, 0b00001110); MAX7219_update();
                MAX7219_send(4, 0b01001111); MAX7219_update();
                MAX7219_send(3, 0); MAX7219_update();
                if (mode_index == 0) {
                    MAX7219_send(2, 0b01011011); MAX7219_update();
                    MAX7219_send(1, 0b00110111); MAX7219_update();
                } else if (mode_index == 1) {
                    MAX7219_send(2, 0b01011011); MAX7219_update();
                    MAX7219_send(1, 0b00001110); MAX7219_update();
                } else if (mode_index == 2) {
                    MAX7219_send(2, 0b01000111); MAX7219_update();
                    MAX7219_send(1, 0b00110111); MAX7219_update();
                } else {
                    MAX7219_send(2, 0b01000111); MAX7219_update();
                    MAX7219_send(1, 0b00001110); MAX7219_update();
                }
                    
                
            }
        
        // rotary encoder sets color #1
        } else if (menu_index == 1) {
            
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue1 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc2);
                DS1302_send(hue1);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[1]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #2
        } else if (menu_index == 2) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue2 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc4);
                DS1302_send(hue2);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[2]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #3
        } else if (menu_index == 3) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue3 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc6);
                DS1302_send(hue3);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[3]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
            
        // rotary encoder sets color #4
        } else if (menu_index == 4) {
        
            // rotary encoder value is mapped to 0..255
            // (if rotary encoder value becomes negative, reset values)
            hue4 = (unsigned char) (rotary_value / 2) % 256;
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xc8);
                DS1302_send(hue4);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001101); MAX7219_update();
            MAX7219_send(7, 0b00011101); MAX7219_update();
            MAX7219_send(6, 0b00000110); MAX7219_update();
            MAX7219_send(5, 0b00011101); MAX7219_update();
            MAX7219_send(4, 0b00000101); MAX7219_update();
            MAX7219_send(3, get7seg[4]); MAX7219_update();
            MAX7219_send(2, 0); MAX7219_update();
            MAX7219_send(1, 0); MAX7219_update();
        
        // rotary encoder sets ON time (hours)
        } else if (menu_index == 5) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours_on = (rotary_value % 96) / 4;
            if (hours_on < 0) {
                hours_on = 23;
                rotary_value = 4*hours_on;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xca);
                DS1302_send(hours_on);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b00010101); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours_on / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours_on % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes_on / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes_on % 10]); MAX7219_update();
            
        // rotary encoder sets ON time (minutes)
        } else if (menu_index == 6) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes_on = (rotary_value % 240) / 4;
            if (minutes_on < 0) {
                minutes_on = 59;
                rotary_value = 4*minutes_on;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xcc);
                DS1302_send(minutes_on);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b00010101); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours_on / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours_on % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes_on / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes_on % 10]); MAX7219_update();
            
        // rotary encoder sets OFF time (hours)
        } else if (menu_index == 7) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours_off = (rotary_value % 96) / 4;
            if (hours_off < 0) {
                hours_off = 23;
                rotary_value = 4*hours_off;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xce);
                DS1302_send(hours_off);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b01000111); MAX7219_update();
            MAX7219_send(6, 0b01000111); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours_off / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours_off % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes_off / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes_off % 10]); MAX7219_update();
            
        // rotary encoder sets OFF time (minutes)
        } else if (menu_index == 8) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes_off = (rotary_value % 240) / 4;
            if (minutes_off < 0) {
                minutes_off = 59;
                rotary_value = 4*minutes_off;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_CE = 1;
                DS1302_send(0xd0);
                DS1302_send(minutes_off);
                DS1302_CE = 0;
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00011101); MAX7219_update();
            MAX7219_send(7, 0b01000111); MAX7219_update();
            MAX7219_send(6, 0b01000111); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours_off / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours_off % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes_off / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes_off % 10]); MAX7219_update();
        
        // rotary encoder sets time (hours)
        } else if (menu_index == 9) {
        
            // rotary encoder value is mapped to 0..23
            // (if rotary encoder value becomes negative, reset values)
            hours = (rotary_value % 96) / 4;
            if (hours < 0) {
                hours = 23;
                rotary_value = 4*hours;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_set_hours(hours);
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001111); MAX7219_update();
            MAX7219_send(7, get7seg[0]); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, 0b10000000 | get7seg[hours / 10]); MAX7219_update();
            MAX7219_send(3, 0b10000000 | get7seg[hours % 10]); MAX7219_update();
            MAX7219_send(2, get7seg[minutes / 10]); MAX7219_update();
            MAX7219_send(1, get7seg[minutes % 10]); MAX7219_update();
            
        // rotary encoder sets time (minutes)
        } else if (menu_index == 10) {
        
            // rotary encoder value is mapped to 0..59
            // (if rotary encoder value becomes negative, reset values)
            minutes = (rotary_value % 240) / 4;
            if (minutes < 0) {
                minutes = 59;
                rotary_value = 4*minutes;
            }
            
            // update value if rotary encoder has been turned
            if (rotary_value_change) {
                DS1302_set_minutes(minutes);
            }
            
            //                .abcdefg
            MAX7219_send(8, 0b00001111); MAX7219_update();
            MAX7219_send(7, get7seg[0]); MAX7219_update();
            MAX7219_send(6, 0); MAX7219_update();
            MAX7219_send(5, 0); MAX7219_update();
            MAX7219_send(4, get7seg[hours / 10]); MAX7219_update();
            MAX7219_send(3, get7seg[hours % 10]); MAX7219_update();
            MAX7219_send(2, 0b10000000 | get7seg[minutes / 10]); MAX7219_update();
            MAX7219_send(1, 0b10000000 | get7seg[minutes % 10]); MAX7219_update();
            
        }
        
        
        // *****
        // react to the rotary encoder's pushbutton
        if (ENC_SW && (sw_buffer == 0)) {
        
            // refill debounce buffer
            sw_buffer = 500;
            
            // advance through menu options
            menu_index += 1;
            if (menu_index > 10) {
                menu_index = 0;
            }
            
            // initialize normal mode
            if (menu_index == 0) {
                
                // reset seconds (convenient after setting the time)
                seconds = 0;
                DS1302_set_seconds(seconds);
                
                // restart the clock
                DS1302_start();
                
                // reset menu timeout
                menu_timeout = 15000;
                
                // resume animations
                animation_on = 1;
                
                // prime the rotary encoder value to currently selected dimming level
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*mode_index;
            
            // initialize "set color" mode
            } else if (menu_index == 1) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue1;
                
            // initialize "set color" mode
            } else if (menu_index == 2) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue2;
                
            // initialize "set color" mode
            } else if (menu_index == 3) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue3;
                
            // initialize "set color" mode
            } else if (menu_index == 4) {

                // pause animation and reset timebases
                animation_on = 0; R1timebase = 0; R2timebase = 0; R3timebase = 0; R4timebase = 0;
                
                // prime the rotary encoder to currently selected color
                rotary_value = 2*hue4;
                
            // initialize "set ON hours" mode
            } else if (menu_index == 5) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours_on;
                
            // initialize "set ON minutes" mode
            } else if (menu_index == 6) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes_on;
                
            // initialize "set OFF hours" mode
            } else if (menu_index == 7) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours_off;
                
            // initialize "set OFF minutes" mode
            } else if (menu_index == 8) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes_off;
                
            // initialize "set hours" mode
            } else if (menu_index == 9) {

                // prime the rotary encoder value to current hours
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*hours;
                
            // initialize "set minutes" mode
            } else if (menu_index == 10) {

                // prime the rotary encoder value to current minutes
                // (multiply by 4 because our rotary encoder has 4 increments per tactile "step")
                rotary_value = 4*minutes;
                
            }

        }

        
        // *****
        // calculate LED RGB colors from their hue
        calc_hsv(hue1); R1red = (unsigned char) rr; R1green = (unsigned char) gg; R1blue = (unsigned char) bb;
        calc_hsv(hue2); R2red = (unsigned char) rr; R2green = (unsigned char) gg; R2blue = (unsigned char) bb;
        calc_hsv(hue3); R3red = (unsigned char) rr; R3green = (unsigned char) gg; R3blue = (unsigned char) bb;
        calc_hsv(hue4); R4red = (unsigned char) rr; R4green = (unsigned char) gg; R4blue = (unsigned char) bb;
        
        // Should the LEDs be ON?
        if ( (menu_index >= 1) || (menu_timeout > 0) || ( ((hours == hours_on && minutes >= minutes_on) || (hours > hours_on)) && ((hours == hours_off && minutes < minutes_off) || (hours < hours_off)) )) {
            
            dimmedR1red = (unsigned char) (R1red*brightness_table[R1brightness_index]) >> brightness_factor; dimmedR1green = (unsigned char) (R1green*brightness_table[R1brightness_index]) >> brightness_factor; dimmedR1blue = (unsigned char) (R1blue*brightness_table[R1brightness_index]) >> brightness_factor;
            dimmedR2red = (unsigned char) (R2red*brightness_table[R2brightness_index]) >> brightness_factor; dimmedR2green = (unsigned char) (R2green*brightness_table[R2brightness_index]) >> brightness_factor; dimmedR2blue = (unsigned char) (R2blue*brightness_table[R2brightness_index]) >> brightness_factor;
            dimmedR3red = (unsigned char) (R3red*brightness_table[R3brightness_index]) >> brightness_factor; dimmedR3green = (unsigned char) (R3green*brightness_table[R3brightness_index]) >> brightness_factor; dimmedR3blue = (unsigned char) (R3blue*brightness_table[R3brightness_index]) >> brightness_factor;
            dimmedR4red = (unsigned char) (R4red*brightness_table[R4brightness_index]) >> brightness_factor; dimmedR4green = (unsigned char) (R4green*brightness_table[R4brightness_index]) >> brightness_factor; dimmedR4blue = (unsigned char) (R4blue*brightness_table[R4brightness_index]) >> brightness_factor;
        
        // turn all LEDs off
        } else {
            
            dimmedR1red = 0; dimmedR1green = 0; dimmedR1blue = 0;
            dimmedR2red = 0; dimmedR2green = 0; dimmedR2blue = 0;
            dimmedR3red = 0; dimmedR3green = 0; dimmedR3blue = 0;
            dimmedR4red = 0; dimmedR4green = 0; dimmedR4blue = 0;
            
        }
        
        // *****
        // send time data to WS2812 LEDs
        // (we do this without loops because it would delay execution time, and this part is time critical)
        
        // we need to turn interrupts OFF during data transmission because this part is time critical
        GIE = 0;
        
        // innermost LED
        WS2812_send_RGB(dimmedR1red, dimmedR1green, dimmedR1blue);
        
        // rings #1 (16 LEDs)
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue); WS2812_send_RGB(dimmedR2red, dimmedR2green, dimmedR2blue);
        
        // ring #2 (16 LEDs)
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue); WS2812_send_RGB(dimmedR3red, dimmedR3green, dimmedR3blue);
        
        // ring #3 (16 LEDs)
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue); WS2812_send_RGB(dimmedR4red, dimmedR4green, dimmedR4blue);
        
        // now it is safe to turn interrupts back on
        GIE = 1;
        
        
    }
    
    return;
    
}

// interrupt service routine (is called approximately 2930 times per second)
// (48MHz/4 clock speed, 8-bit counter, with 1:16 prescaler = 2930 Hz)
void __interrupt () isr (void) {  
 
    // timer overflow?
    if (TMR0IF) {
        
        // read out the current rotary encoder position
        rotary_pos = gray_to_binary();
        
        // calculate the difference to previous position
        rotary_diff = rotary_pos_before - rotary_pos;

        // turned clockwise
        if (((rotary_diff == -1) || (rotary_diff == 3))) {
            rotary_pos_before = rotary_pos;
            rotary_value++;
            
        // turned counter-clockwise
        } else if (((rotary_diff == 1) || (rotary_diff == -3))) {
            rotary_pos_before = rotary_pos;
            rotary_value--;
        
        // in this case, a step has been missed
        // (best practice: ignore it!)
        } else if ((rotary_diff == 2) || (rotary_diff == -2)) {
            
        }
        
        // clear the debounce buffer for the rotary encoder pushbutton,
        // but only if the pushbutton is not pressed
        if ((sw_buffer > 0) && !ENC_SW) {
            sw_buffer--;
        }
        
        // clear the menu timeout buffer
        if (menu_timeout > 0) {
            menu_timeout--;
        }
        
        // clear timer flag
        TMR0IF = 0;
        
    }
    
}

// convert Gray code into a binary number
// 00 -> 0, 01 -> 1, 11 -> 2, 10 -> 3
int gray_to_binary () {
 
    if (ENC_A == 0 && ENC_B == 0) {
        return 0;
    } else if (ENC_A == 0 && ENC_B == 1) {
        return 1;
    } else if (ENC_A == 1 && ENC_B == 1) {
        return 2;
    } else {
        return 3;
    }
    
}

// send the byte "value" to the DS1302
void DS1302_send (int value) {
    
    // set data port as output
    DS1302_IO_TRIS = 0;
    
    // auxiliary index variable
	int n = 0;
    
    // loop over all eight bits
	for (n = 0; n < 8; n++) {
        DS1302_IO = (value >> n) & 1;
        NOP(); // important!
        DS1302_CLK = 1;
        DS1302_CLK = 0;
    }

}

// receive a byte from the DS1302
unsigned char DS1302_get (void) {
    
    // set data port as input
    DS1302_IO_TRIS = 1;
    
    // dummy variables
    unsigned char value = 0;
    char n = 0;
    
    // collect all eight bits
    for (n = 0; n < 8; n++) {
        NOP(); // important!
        value |= (DS1302_IO << n);
        DS1302_CLK = 1;
        DS1302_CLK = 0;
    }
    return value;

}

// convert decimal value "data" into BCD
// and store it in DS1302 at "address"
void DS1302_set_bcd (int address, int data) {
    
    int data10 = data / 10;
    int data1 = data % 10;
    DS1302_CE = 1;
    DS1302_send(address);
    DS1302_send((data10 << 4) | data1);
    DS1302_CE = 0;
    
}

// read a BCD value from DS1302 from "address"
// and convert it back into decimal
int DS1302_get_bcd (int address) {
    
    int tmp = 0, data10 = 0, data1 = 0;
    DS1302_CE = 1;
    DS1302_send(address);
    tmp = DS1302_get();
    DS1302_CE = 0;
    data10 = (tmp & 0b01110000) >> 4;
    data1  = tmp & 0b00001111;
    return 10*data10 + data1;
    
}

// short-hand functions that set the time directly
void DS1302_set_hours   (int data) { DS1302_set_bcd(0x84, data); }
void DS1302_set_minutes (int data) { DS1302_set_bcd(0x82, data); }
void DS1302_set_seconds (int data) { DS1302_set_bcd(0x80, data); }

// short-hand functions that read the time
int DS1302_get_hours   (void) { return DS1302_get_bcd(0x85); }
int DS1302_get_minutes (void) { return DS1302_get_bcd(0x83); }
int DS1302_get_seconds (void) { return DS1302_get_bcd(0x81); }

// initialize the DS1302
void DS1302_start (void) {
    
    // get the current seconds
    // seconds = DS1302_get_seconds();
    
    // make sure DS1302 is running by clearing the CLOCK HALT signal
    // at bit 7 in the "seconds" register
    // (we need to extract the seconds first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x80);
    // DS1302_send(0<<7 | ((seconds / 10) << 4) | (seconds % 10));
    DS1302_send(0);
    DS1302_CE = 0;
    
}

// stop the DS1302
void DS1302_stop (void) {
    
    // get the current seconds
    seconds = DS1302_get_seconds();
    
    // make sure DS1302 is stopped by setting the CLOCK HALT signal
    // at bit 7 in the "seconds" register
    // (we need to extract the seconds first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x80);
    DS1302_send(1<<7 | ((seconds / 10) << 4) | (seconds % 10));
    DS1302_CE = 0;
    
}

// set the DS1302 to 24-hour mode
void DS1302_set24 (void) {
    
    // get the current hours
    hours = DS1302_get_hours();
    
    // make sure DS1302 is running in 24-hour mode by clearing bit 7 
    // in the "hours" register
    // (we need to extract the hours first so they don't get deleted)
    DS1302_CE = 1;
    DS1302_send(0x84);
    DS1302_send(((hours / 10) << 4) | (hours % 10) );
    DS1302_CE = 0;
    
}

// send out a byte b in WS2812 protocol
void WS2812_send_byte (unsigned char b) {

    if (b & 0b10000000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b01000000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00100000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00010000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00001000) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000100) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000010) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    if (b & 0b00000001) { WS2812_send_bit(1); } else { WS2812_send_bit(0); }
    
}

// send red, green, and blue values in WS2812 protocol
void WS2812_send_RGB (unsigned char r, unsigned char g, unsigned char b) {

    WS2812_send_byte(g);
    WS2812_send_byte(r);
    WS2812_send_byte(b);
    
}

// This function sends out the address byte "a" and the data byte "d" in the MAX7219 format
// (Sequence of bits is a7-a6-a5-a4-a3-a2-a1-a0-d7-d6-d5-d4-d3-d2-d1-d0.)
void MAX7219_send (unsigned char a, unsigned char d) {

	// send out address byte, start with most significant bit and work backwards
    for (int i=7; i>=0; i--) {
        MAX7219_DATA = (a >> i) & 1;
        MAX7219_CLK = 1;
        MAX7219_CLK = 0;
    }
	
	// send out data byte, start with most significant bit and work backwards
    for (int i=7; i>=0; i--) {
       MAX7219_DATA = (d >> i) & 1;
       MAX7219_CLK = 1;
       MAX7219_CLK = 0;
    }
	
	// reset the data pin back to zero
	// (so that it is not left ON if the last sent bit was a 1)
    MAX7219_DATA = 0;

}

// This function pulls the LOAD pin high and then back to zero, so that the transmitted data
// appears in the MAX7219's output stage.
void MAX7219_update (void) {
	
    MAX7219_LOAD = 1;
    MAX7219_LOAD = 0;
	
}

// This function takes a hue value (variable h) and computes the corresponding red, green, and blue values.
// This is inspired by the curves presented on https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors .
void calc_hsv (int h) {
 
    // make sure the hue is between 0..255
    h = h & 0xff;
    
    // expensive floating point computations
    int v2 = (int) (5.31*h);
    int v1 = v2 / 2;
    
    // red channel
    if ((h >= 0) && (h <= 32)) {
        rr = (int) (255 - v1);
    } else if (h <= 64) {
        rr = 170;
    } else if (h <= 96) {
        rr = (int) (510 - v2);
    } else if (h <= 160) {
        rr = 0;
    } else {
        rr = (int) (v1 - 429);
    }
    
    // green channel
    if ((h >= 0) && (h <= 96)) {
        gg = (int) v1;
    } else if (h <= 128) {
        gg = (int) (510 - v1);
    } else if (h <= 160) {
        gg = (int) (850 - v2);
    } else {
        gg = 0;
    }
    
    // blue channel
    if ((h >= 0) && (h <= 96)) {
        bb = 0;
    } else if (h <= 128) {
        bb = (int) (v1 - 255);
    } else if (h <= 160) {
        bb = (int) (v2 - 595);
    } else {
        bb = (int) (681 - v1);
    }
    
}

About FriendlyWire

Beginner-friendly electronics tutorials and projects. Discover the joy of electronics! Keep reading.

Components Needed

1×5V WS2811 “NeoPixel” string light with 50 LEDs (kit)
1×USB power supply
1×USB type A adapter (link)
1×PIC16F1455 (link)
1×MAX7219 7-segment module (link)
1×CR2032 battery holder* (link)
1×CR2032 3V battery
1×DS1302 real-time clock IC* (link)
1×32.768kHz crystal* (6pF load capacitance) (link)
1×4.7kΩ resistor (link, kit)
1×100μF electrolytic capacitor (link, kit)
2×100nF ceramic capacitor (link, kit)
1×rotary encoder & knob (kit)

*I recommend to purchase a set of DS1302 real-time clock modules (link) instead of the individual parts, because it is cheaper.

Click on the items above to learn more.

Tools Needed

1×PICkit3 or MPLAB Snap programming adapter
1×side cutter
1×pliers

Resources

Let's build a community

How did you get interested in electronics? What do you want to learn? Connect and share your story!

Also consider signing up for the monthly email list :)

Tag Cloud

  • Christmas decoration
  • flashing Christmas star
  • WS2811 NeoPixel LEDs
  • MAX7219 7-segment module
  • DS1302 real-time clock
  • PIC microcontroller
  • PIC16F1455
  • beginner-friendly
  • project