Close

Software Part 1: Buttons, Lights and Configuration

A project log for Happy Clap Switch

Clap-clap, light goes on. Clap-clap, light goes off. This makes me happy.

Alan GreenAlan Green 06/07/2019 at 20:402 Comments

The clap-switch source is a single file C program, written using the Atmel START framework. This post discusses the main loop, how each of the peripherals are handled. A follow up post will discuss handling of the sound input and the actual clap detection.

If you'd like to see or use the source, it's available at github.com/alanvgreen/clap-switch

All of the interesting code is in main.c.

Main Loop and Top Level 

At the core of the software is tick_millis, a 32 bit millisecond counter. tick_millis is updated every millisecond by an interrupt from the PIT (programmable interrupt timer). 

ISR(RTC_PIT_vect)
{
    tick_millis++;

    // Clear interrupt flag to indicate that interrupt has been handled.
    RTC.PITINTFLAGS = RTC_PI_bm;
}

The timing is generated by the 3217's internal 32kHz timer, so it will only be correct with a few percent, but that's close enough.

The main loop begins by waiting for tick_millis to be different from the last time it saw tick_millis. It sleeps while waiting, and will wake when an interrupt occurs.

uint32_t last_awake = tick_millis;
while (1) {
    while (last_awake == tick_millis) {
        __builtin_avr_sleep();
    }
    last_awake = tick_millis;

    // ... rest of loop here ...
}

So long as the rest of the main loop takes less than a millisecond, then the loop will execute every millisecond.

Output: WS2812 LEDs

WS2812 LEDs are stateful: you only need to tell them what to do when you want them to change what they're doing. The code keeps track of whether the led status is up-to-date with the leds_updated variable. When that variable is true, the LEDs don't need updating. 

Sending Data to WS2812s

Following the advice from Josh Levine, clap-switch simply bit-bangs data out to the LEDs, paying more attention to the length of the high part of the cycle than the low part of the cycle. If you haven't read his article, and you think you might one day program a WS2812/NeoPixel, reading this article will pay back your time investment many-fold.

Here's the code to send one byte. We use cycle delays, knowing our CPU speed to within 20MHz, and keeping in mind that the on-off transition adds a smidgen of delay too.

void sendByte(uint8_t v) {
    // Working with PB0
    register uint8_t on = VPORTB_OUT | 1;
    register uint8_t off = VPORTB_OUT & 254;
    for (uint8_t i = 0; i < 8; i++) {
        if (v & 0x80) {
            VPORTB_OUT = on;
            __builtin_avr_delay_cycles(13); // 0.65uS
            VPORTB_OUT = off;
            __builtin_avr_delay_cycles(8); // 0.4uS
        } else {
            VPORTB_OUT = on;
            __builtin_avr_delay_cycles(6); // 0.3uS
            VPORTB_OUT = off;
            __builtin_avr_delay_cycles(15); // 0.75uS
            
        }
        v <<= 1;
    }
}

 We call this code three times per WS2812 - once each for Green, Red and Blue (yes, in that order). And we do that 8 times - once per WS2812, for a total of 24 calls to update all of the LEDs.

We calcuate the values of R, G and B to send using an HSL to RGB conversion algorithm. H (hue) and L (luminance) are set using the rotary encoders. We assume S (saturation) is 1, meaning fully saturated. In the context of a bedroom light, the result is a pleasingly useful subset of the full range of the WS2812s color output.

// v is L (because lowercase l looks like 1 and is confusing). 
// v range 0-255 instead of 0-1. 
uint16_t v = (config.brightness * config.brightness) / 16;
v = min(v, 255); // in case config.brightness == 64

// S is assumed to be 1 (range 0-1)

// Calculate chroma - 0 to 255.
uint16_t c = (255 - abs((2 * (int16_t) v) - 255)); 

// config.hue is in range 0-191
uint8_t hue_region = config.hue >> 5; // top 3 bits of region are hue range (0-5)
int8_t hue_val = config.hue & 0x1f; // bottom 5 bits are val
uint16_t xt = 8 * (hue_region & 1 ? hue_val : 32-hue_val);
uint16_t x = (c * (256 - xt)) >> 8; // scale x down to range 0 to 255
uint8_t r = 0, g = 0, b = 0;
switch (hue_region) {
case 0: r = c; g = x; break;
case 1: r = x; g = c; break;
case 2: g = c; b = x; break;
case 3: g = x; b = c; break;
case 4: r = x; b = c; break;
default: r = c; b = x;
}

uint16_t m = v - c/2;
sendLeds(min(r + m, 255), min(g + m, 255), min(b + m, 255));

This code closely follows the algorithm from Wikipedia. Note that:

Input: Button

The button reading code simply checks whether it's state is different from how it was last time the code checked, and also includes a 20ms minimum time between reporting changes, which effectively debounces the output.

Input: Rotary Encoder  

This code is based on logic from circuits at home (link not working at time of writing). Based on the current and previous states of the encoder, it returns -1 or 1, depending on the direction that the encoder was turned, or zero if there was no change.

// Look up table for intepreting encoder state transitions.
// Index is (last_encoder_reading << 2 | // current_reading)
static const int8_t enc_states [] PROGMEM = {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0};

// Returns -1 on turn one way, 1, the other way and zero for no change.
int8_t readEncoder1() {
    static uint8_t last = 0;
    uint8_t curr = (ENC_1A_get_level() ? 2 : 0) | (ENC_1B_get_level() ? 1 : 0);
    int8_t result = pgm_read_byte(&(enc_states[last << 2 | curr]));
    last = curr;
    return result;
}

 I did not find any need for debouncing logic.

Based on whether the encoder returns -1 or 1, we twiddle the brightness and hue pretty much as you would expect. Here is the code for updating brightness:

void updateBrightness(int8_t in) {
    if (in == -1 && config.brightness >= 1) {
        config.brightness--;
        configChanged();
    }
    if (in == 1 && config.brightness < MAX_BRIGHT) {
        config.brightness++;
        configChanged();
    }
}

Saving Configuration

I wanted to make sure that when the light was unplugged and then replugged, ti came back in pretty much the same state - whether the light was on or off, as well as hue and brightness. clap-switch accomplishes this by writing the configuration to 'EEPROM' (actually, it's flash) whenever the configuration is changed. 

void maybeWriteConfig() {
    if (!config_written && tick_millis > config_change_millis + CONFIG_WAIT_MS) {
        FLASH_0_write_eeprom_block(0, (uint8_t *) &config, sizeof(Eeprom));
        config_written = true;
    }
}

 Note that we wait a few tens of milliseconds after each configuration change, in case there is another change in quick succession. This saves wearing out the EEPROM, which only has a lifetime of tens of thousands of writes.

At startup, the EEPROM is read into RAM and the light put back into the same state.

Discussions

Alan Green wrote 06/26/2019 at 09:03 point

Oh, that is a great idea. I'll think about it too.

Nice instructable!

  Are you sure? yes | no

Simon Merrett wrote 06/12/2019 at 06:23 point

Hi Alan, thanks for sharing your code. I agree that if you want to drive WS2812 LEDs josh is the guy to listen to! 

I'm itching to see if the new ATtiny ICs can implement a really nice way to read encoders that is largely CPU-independent, using ACs, event sys and the CCL etc. There is an app note from microchip which does this but it uses more pins (6) than I'm willing to give over to one encoder. In the meantime, I have an instructable based on the old ATMEGA328PB to do interrupt-driven encoder reading, if it's of interest to keep your loop short. One guy in the comments managed to reduce the code to one interrupt! 

https://www.instructables.com/id/Improved-Arduino-Rotary-Encoder-Reading/

  Are you sure? yes | no