Illuminating a shot glass tray for fun and Prosit!

2016-04-17 22:16:28

It all began with an upcoming bachelor party. A friend had the idea to create a self-made shot glass tray and asked if I could add some LEDs into it to have some nice lighting effects along with it ..and who doesn't like LEDs anyway? So I figured, sure, some PWM and multiplexing - been there, done that, don't see a problem. Let's do that. So I went on and started pondering over color patterns, when it hit me. We need a drinking game along with all that! Two people drinking against each other, randomly selected by lighting up each person's personal glass. A battle of the shot glass tray. Battletray.

Opaque lighted Battletray

Lighting it up

So as I said, the basic concept seemed straight forward, Pulse-width Modulation (PWM) to control the red, green and blue amount of each RGB LED, and Multiplexing to select each LED individually. In our case, the tray will have 10 RGB LEDs, which means

  • 3 lines for each red, green and blue
  • 10 lines for the common cathode of each LED

Using an ATmega328 in a PDIP28 package provided enough I/O pins for the task, no need for actual multiplexer components such as a 4051. Also the PWM ended up as software PWM instead of using the ATmega's hardware PWM unit (I need the timers for other tasks anyway, and admittedly, I haven't managed yet to use the hardware PWM properly for multiplexed RGB LEDs)

Hardware

As mentioned, it's all build around an ATmega328 microcontroller since this is my default go to microcontroller. Using the multiplexed design, which means only one LED is lighted up at the same time, we only need three resistors for the commonly shared red, green and blue diodes. The LED itself has a common cathode, so connecting it to ground will actually enable the LED. Leaving the line floating will turn it off.

The ATmega's I/O pins are quite limited in the amount of current it can source (i.e. ground it) and I didn't want to risk anything here. So instead of going straight to the I/O pin, I used NPN transistors to switch each LED to ground.

Other than that, it's a switch, a resistor and capacitor for hardware debouncing and the usual decouple capacitor for the microcontroller's power. Summarized, for a 10 LED version, our components are:

  • 1 ATmega328 controller
  • 10 common cathode RGB LEDs
  • 10 NPN transistors (I had some 2N3904 lying around, so I took those)
  • 2 100nF capacitors for button debounce and decoupling
  • 1 1k0 resistor for button debounce
  • 1 180R resistor for the red diode (may vary with other LEDs)
  • 2 120R resistors for the blue and green diode (again, may vary)

Battletray PCB

KiCad project and PDF schematic can be found on Github. As you can see, I used a perfboard, so naturally there's no PCB layout available, at least at this point.

Firmware

The microcontroller is mostly busy doing the PWM and multiplexing for each LED over and over again. To give a basic idea how that looks like for 10 LEDs displaying all the same color, here's some code:

#define LEDS 10
#define MAX_VALUE 64 /* PWM steps */
char rgb[3] = {0, 32, 63}; /* PWM duty cycle based on 64 steps */

for (i = 0; i < LEDS; i++) {
    led_on(i); /* turn on a single LED */
    for (value = 0; value < MAX_VALUE; value++) {
        /* simple software PWM for each color */
        red   = (rgb[0] > value) ? 1 : 0;
        green = (rgb[1] > value) ? 1 : 0;
        blue  = (rgb[2] > value) ? 1 : 0;
    }
    led_off(i); /* turn off the LED */
}

This is obviously a very simplified version, but it should give the basic idea. The full implementation can be seen in the run_light() function in the source code.

Using 64 steps for the PWM means each red, green and blue diode can have 64 different intensity levels (including being off), so a theoretical total of 64*64*64 = 256k colors can be achieved. This however is really more theoretical than anything close to the real world since you won't notice every single step difference.

The 64 itself was rather arbitrarily chosen. Using less steps will make color transitions less smooth while using more might end up in too long loop times, resulting in flickering. Since it's the laziness of our eyes that will make each LED appear to be turned on all of the time (a camera lens is more sensitive to this), the proper frame rate / loop times need to be used. Running at 8MHz, 10 LEDs with 64 steps is no problem.

Add some funky lighting

Now, displaying every LED with one single color is a bit boring - we wouldn't even need a microcontroller for that. Instead, I wanted to have different color patterns in different modes with smooth color transitions - and let's not forget about the drinking game part.

Starting slowly, let's say we want to periodically toggle between two different colors, a cyan and purple shade. We cannot really use the _delay_ms() function or its variants since this will also delay the software PWM and we end up with either flickering or no light at all. We could use a simple variable to count until a certain value is reached and toggle the color, but that's not very practical, it's a trial and error process to find the correct counter value and we need to adjust it every time we change the code.

Well, the microcontroller's timer units can practically do the same in hardware, independent of what the CPU is doing. We just need to calculate the counter value once for the toggle time we want, and let the interrupt handler do the rest.

#define COLOR_MODE_MAX 2 /* just two modes */
uint8_t color_mode;

ISR(TIMER1_COMPA_vect)
{
    /* timer1 compare match interrupt handler, called every n milliseconds */
    if (++color_mode == COLOR_MODE_MAX) {
        color_mode = 0;
    }
}

void
set_all_leds(uint8_t red, uint8_t green, uint8_t blue)
{
    /* imagine this does the whole multiplexing/PWM stuff */
}

void
set_leds(void)
{
    if (color_mode == 0) {
        set_all_leds(0, 31, 63);
    } else {
        set_all_leds(63, 0, 31);
    }
}

int
main(void)
{
    /* imagine here's all the I/O port setup, timer initialization etc. */

    while (1) {
        set_leds();
    }
}

Again, heavily simplified, but in the very big picture, this is pretty much it: a defined set of color values (or a base for it) and a timer interrupt that will set up the next color value. Though, this still sets every LED to the same color.

To light up each LED in a different color, we basically just have to add an array of values and loop through that one instead.

uint8_t colors[LEDS][3] = {
    63,  0,  0,
     0, 63,  0,
     0,  0, 63,
    63,  0, 63,
    ...
};

void
set_one_led(uint8_t led, uint8_t red, uint8_t green, uint8_t blue)
{
    /*
     * imagine again this does the whole multiplexing/PWM stuff, but this time
     * it only handles a single LED defined by the "led" parameter
     */
}

void
set_leds(void)
{
    /* called from the main loop */
    uint8_t i;

    for (i = 0; i < LEDS; i++) {
        set_one_led(i, colors[i][0], colors[i][1], colors[i][2]);
    }
}

Alright, now every LED has a different color. Let's make the colors rotate so that on every timer interrupt, the colors are shifted one position.

uint8_t offset;

ISR(TIMER1_COMPA_vect)
{
    /* timer compare match interrupt handler */
    if (++offset == LEDS) {
        offset = 0;
    }
}

void
set_leds(void)
{
    /* called from the main loop */
    uint8_t i;
    uint8_t index = 0;

    for (i = 0; i < LEDS; i++) {
        /* shift the start index according to the offset */
        index = offset + i;
        if (index >= LEDS) {
            index -= LEDS;
        }
        set_one_led(index, colors[i][0], colors[i][1], colors[i][2]);
    }
}

And this gives us the basic concept for moving color patterns.

Smoothen in up

So far, the color changes are happening drastically by simply changing the value. This can be okay, but it's also nice to have a smooth gradient transition between the color changes.

Instead of simply setting the color to the defined number of PWM steps, increasing or decreasing the value in single steps until the specified value is reached will get you there.

Pack it into modules

So, now we have a few random colors rotating around, but why limit it to one single color pattern. If we modularize the main concept, we can implement all kinds of different patterns. In the end, it's just a loop that handles the LEDs and a timer interrupt handler that adjusts the LEDs' values. Replace the direct function calls with function pointers for the setup and interrupt handling, pack it into a struct and it's done. Check the module.c / module.h code for the main concept and any of the module_*.c files for direct implementation of that.

The Button!

Right.. I haven't really mentioned the button yet. Well, since I just mentioned to concept of having different color patterns implemented, we also need a way to select them. A single button will do just that. Press it and the next defined color pattern module will be activated.

Battletray button

The button itself is connected to the ATmega's external interrupt 0 pin (INT0) and is active low, i.e. pressing it will pull it to ground. The interrupt itself is set up to be triggered on falling edge therefore.

That covers switching between the different color patterns, but what about the drinking game? Would be good to have that separated from the light modes. Well, just add another timer - the ATmega has three of them. Pressing the button will start a timer. The timer's overflow interrupt handler (triggered when the timer counted all its way up to its maximum value and overflows to zero) will check if the button is still pressed, and if so treat it as a long press which toggles between the light and the game mode. Okay, actually it uses the timer and a counter variable in this case to keep the delay for normal button presses short. Triggering the external interrupt on both edges is an alternative here.

Let the battle begin

Now after covering the basics of getting the LEDs lit up in different colors and patterns (yeah, in a quite abstract way, but just check the source code for the details), it is time to come to the drinking game - which in the end is really just another light pattern mode, only with a bit more complex timer handling.

The drinking game itself is simple - not that drinking games are generally known for their complexity anyway. Everyone gets a dedicated glass on the tray which are naturally filled with whatever you prefer. Someone, whoever wants, presses the button and the LEDs start spinning and flashing around and eventually two single LEDs light up in red and blue respectively. If it's your glass, you drink. The End.

Sometimes however a second pair of glasses will light up, and every now and then even a third pair. Same rules apply. A variation of the rule is, if you finish last, you have to drink another shot. Also, the glasses are lit up in either red or blue at this point, so teams could be formed as well this way.

How it's done

Like I said, in a way, it's just another lighting pattern. Instead of toggling through the different available pattern modules, as it's the case in the lighting main mode, pressing the button in the drinking game mode will simply restart the module.

The timer handling consists of two main stages

  1. LED spinning and flashing
  2. Displaying the contestants

The contestants are determined with a pseudo random number generated provided by the AVR C library that is very unprofessionally seeded with different combinations of timer values. In drinking game mode, another timer next to the LED handling timer and button press timer is active. In different stages and also during button press and release, the timers' values are stored and later used for seeding the number generator.

Based on which random numbers are then generated, the initial contestants are chosen and it's also defined whether there will be a second or third pair of contestants in that round.

Rather than simply using the generated random number to choose the contestant, all contestants are put in an array and the random numbers walk through the array's indices. This adds an even distribution to the selection process.

uint8_t chosen[LEDS];

uint8_t setup_next_one(void)
{
    uint8_t next = get_random_number();
    while (chosen[next] == 1) {
        next = get_random_number();
    }
    chosen[next] = 1;
    return next;
}

The actual implementation is a bit different and also adds checks to avoid this ends up in an endless loop.

And for now, this is really all to it.

What's next

The whole firmware is rather messy at the moment. There wasn't much time from idea to deadline, so my priority was in functionality over cleanness. Some parts are also rather nastily hacked together so they simply work, lots of room for improvement.

As for the general concept, some additional ideas already came up. One is to add either mechanical or optical switches to the glass holders to detect when a glass was taken out and put back in. This way the rule variation of "last one drinks again" can be enforced. Another idea is to add buttons for each glass to add extra variations in who will have to drink.

So far this was a one-time project with a single prototype that we gave away as present. But my friend is already eager to build another one and add the mentioned improvements to it. Who knows, maybe it even ends up on Kickstarter one day.

Categories

Tags