CM Storm Devastator Keyboard Mod

So Christmas has come and gone, and somewhere in the midst of all of it, I came into possession of a shiny new keyboard. Shiny being the important word:

The Cooler Master Devastator. It’s a nice keyboard, and a welcome upgrade to my existing Logitech desktop keyboard. I especially love the media keys (really, though. I had a macro-programmable mouse and used all of the programmable keys as media keys)

As is quite clear from the picture, it is backlit which is a nice touch. The etching on the keys is quite dark, though, so you can’t see the keys with the back-light off. That’s cool, though. We’ll just keep the back-light on. There’s a key to toggle it, but wait! What is this? The key has 2 functions. Scroll lock and back-light toggle.

Shiny!

The problem with this was apparent pretty quickly, as scroll lock needs to be on in order for one to see the letters on the keys. This isn’t an issue for most people (I guess?) but I use Synergy on my desktop machine and scroll lock locks the focus to the current screen. So in short, I don’t want scroll lock to be on, but I do want the backlight to be on. Problem.

I opened up the keyboard and found, as expected, the back-light is powered by a transistor, the base of which is connected to the scroll lock LED (to avoid loading the keyboard IC directly). Can’t say I approve of the design choice but I suppose it was an inexpensive option. Now the easy way to fix this would be to connect the LED strip directly to power, but that would be too easy. What if I want to switch the back-light off? What if I wanted to do something else? I’ve been looking for a microcontroller project, here it is!

The hardware

My plan was to have 3 modes: on, off and a breathing effect. I needed a microcontroller with a pin change interrupt, PWM and a single IO for each function. The ATTiny10 was a perfect fit – SOT23-6 package (size of 2 ants sitting side-by-side), 3 IO pins, ADC, PWM, interrupts and 5V operation and internal clock. Plus I had one lying around.

I cut the trace on the PCB between the scroll lock LED and the resistor going to the transistor and soldered jumper wires to VCC and GND, so now the wires going to the LED strip carried power so I didn’t have to run any more wire and I could work in the palm rest where there’s more space to work.
Then I desoldered the wires to the LED strip and connected them to my microcontroller.
The LED strip is connected to the PWM output compare pin via a PN2222A transistor. The button is connected to in pin change interrupt pin and internally pulled-up. This means a component count of 4 (ATTiny, button, transistor and resistor)

The keyboard PCB with power routed to the LED strip JST header
Trace cut so that transistor stays hard-off. Before, transistor base current was supplied by the scroll lock LED.
The breakout holding the ATTiny10. 1/4W resistor for scale.

To keep it discreet, I drilled a small hole in the bottom of the keyboard and put some glue around the button. I also put sticky foam on the inside of the keyboard case to keep pressure on the back of the button. I destroyed 2 buttons before this because the super glue ran into the gaps and glued the button in place. Epoxy worked better but was still a bust. In the end, I waited for the epoxy to become gooey then used it sparingly and relied primarily on the sticky foam.

The full circuit, all 4 pieces
The button on the bottom of the keyboard. This was the most discreet place to put it and I don’t plan on changing mode often.
It works!

The code

The on/off modes are simple. Pin high, pin low. The breathing effect was a bit more complex. Firstly I needed a curve of human breathing to make it look natural. From paying attention to my own breathing, I observed that it was about 1/3 inhale, 1/3 exhale and 1/3 idle. We slow down at the peak of inhalation and exhalation, which is more or less the same as the first 90 degrees of a sine wave. So my curve ended up being a piecewise function.

"Breathing" curve
“Breathing” curve

First 90 degrees of sine, inverted first 90 degrees of sine, then a constant, modified to have a maximum value of 255, minimum of 100 and a total period of 255.

I put these values in an array of length 256 – reason being that I could traverse through it with an 8bit counter variable and have it wrap-around automatically when the variable overflowed. The value itself was put in the timer’s output compare register in order to change the PWM duty cycle. From past experience, I’ve learnt that you can get a pretty smooth sine wave from a 128 point array, so 256 is perfect.

A device with 32 bytes of RAM cannot hold a 256 byte array in memory, for obvious reasons. Since I didn’t need to modify the array, I stored it in flash instead as there was plenty of free space (flash is 1kb)

Debouncing was achieved with a 200ms delay inside the ISR.

#define F_CPU 1000000UL

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/delay.h>

uint8_t mode, count;

//LUT stored in array in flash as the whole thing is 8x the size of RAM
const uint8_t breathe[] PROGMEM = {100, 103, 106, 109, 112, 114, 117, 120, 123, 126, 129, 132, 134, 137,
    140, 143, 146, 148, 151, 154, 157, 159, 162, 165, 167, 170, 172, 175, 178, 180, 182, 185, 187, 190,
    192, 194, 197, 199, 201, 203, 205, 208, 210, 212, 214, 216, 217, 219, 221, 223, 225, 226, 228, 230,
    231, 233, 234, 236, 237, 238, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 250, 251, 252,
    252, 253, 253, 254, 254, 254, 255, 255, 255, 255, 255, 252, 249, 246, 243, 241, 238, 235, 232, 229,
    226, 223, 221, 218, 215, 212, 209, 207, 204, 201, 198, 196, 193, 190, 188, 185, 183, 180, 177, 175,
    173, 170, 168, 165, 163, 161, 158, 156, 154, 152, 150, 147, 145, 143, 141, 139, 138, 136, 134, 132,
    130, 129, 127, 125, 124, 122, 121, 119, 118, 117, 115, 114, 113, 112, 111, 110, 109, 108, 107, 106,
    105, 105, 104, 103, 103, 102, 102, 101, 101, 101, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
    100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
    100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
    100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100,
    100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100};

ISR(PCINT0_vect)
{
    if((PINB & 2) == 0) //Falling edge only
    {
        if(mode == 0)
        {
            //Drive pin high, disable timer
            PORTB |= (1 << 0);
            TCCR0A = 0x00;
            TCCR0B = 0x00;
            mode = 1;
        }
        else if(mode == 1)
        {
            //Start timer from peak of wave for seamless transition
            count = 80;
            OCR0AL = 255;
            TCCR0A = (1 << COM0A1) | (1 << WGM00);
            TCCR0B = (1 << CS01);
            mode = 2;
        }
        else if(mode == 2)
        {
            //Drive pin low, disable timer
            PORTB &= ~(1 << 0);
            TCCR0A = 0x00;
            TCCR0B = 0x00;
            mode = 0;
        }
        else mode = 0; //Safety net
        _delay_ms(200); //Dirty debouncing
    }
}

int main(void)
{
    mode = 1;
    count = 0;
    PORTB |= (1 << 0); //Switch on light
    DDRB |= (1 << 0); //Set LED pin as an output
    PUEB |= (1 << PUEB1); //Enable pull-up on the button pin
    
    PCMSK |= (1 << PCINT1); //Configure timer
    PCICR |= (1 << PCIE0);
    sei();
    
    while(1)
    {
        OCR0AL = pgm_read_byte(&breathe[count++]); //Adjust the duty cycle
        _delay_ms(12); //12ms * 256 = 3.072s
    }
}

Here’s a (bad) video of the resulting effect: