I've spent the last month developing the prototype. I got my boards in from OSHPark a few days ago, put one together in the lab after work and got the main function of displaying a message up and working. I'll cover the development and debugging process here.

component selection and circuit design

For the microcontroller, I picked a Microchip PIC18F2XK20. I paired it with an LIS3DH 3-axis, adjustable-range (up to ±16G) accelerometer from STMicroelectronics. The adjustable range ensured that, if I had gotten my napkin acceleration calculations wrong (as to how much acceleration the board will experience when shaken), I would still have some range to sense the motion of the card. The power comes from two CR2032 coin cells in basic SMD friction-mount battery holders. These should have enough capacity to power the card for a full hour or more of shaking (again, if my napkin calculations are correct). Since I'm not using any fancy constant-current LED drivers, and instead using resistive current limiting, I wanted to ensure that the components had a consistent operating voltage independent of the battery voltage (which will drop over the remaining charge of the cells), and for this I used a MCP1640 step-up boost converter.

Choosing components was somewhat difficult. I anticipated the final construction of the card to be a stackup of a 0.6mm PCB to hold the components, with a 1.0mm thick material stacked on top, with holes in the top layer to allow for the components on the bottom to "stick up" through the top layer. This stackup of materials gives a final thickness of 1.6mm, which would allow me to use CR2016 (20mm diameter, 1.6mm thick) batteries in the final version. These batteries can be purchased in a tab-mount package, which could be soldered onto the 0.6mmPCB and "stick up" through both layers in the stackup. -- This means that any part I use needs to be 1.0mm or thinner. Oftentimes I would find a component that was perfect in all regards, except for the package height. Of course, I could have chosen taller components for this prototype, but I didn't want to repeat the entire design process when I needed to pick thinner parts.

This endeavor gave me a lot of respect for the Coin, which is an electronic credit card with an overall thickness of 0.8mm, half as thick as what I am attempting to build. Their product uses thin-film lithium batteries, a (likely) custom E-Ink display, and some sort of flexible PCB with chip-on-board construction. Their entire package is then somehow cast into a piece of injection molded plastic. -- I've concluded that without an exponentially larger sum of money, such technology is simply out of DIY'er monetary reach (for now).

When picking the microcontroller, I made sure I had enough I/O to drive 10 LEDs (tall enough a column to allow for some different font options). I made sure it had at least one wake-on-interrupt pin. The accelerometer has an internal state machine that allows for the execution of small "programs." These can still be run in the low power, low sample rate mode, and can allow me to put the uC to sleep if the user doesn't shake it for a while and then wake on an acceleration that is large enough. This will keep the batteries alive longer if the user forgets to turn the power switch off after use. The uC also needed at least one ADC peripheral for the ambient light sensor (ALS) I placed on board (read more about the motivation behind this in the previous post). -- I decided to go with an ALS IC instead of other options due to the small amount of external circuitry required. The only consideration here was to make sure that the response time was low enough to allow it to sense rapid changes in ambient light. -- I also made sure there was an extra UART peripheral which would allow for easy debugging during board bringup.

I could have picked a lower-range PIC, I certainly don't need the amount of flash or the processing power that this PIC18 will provide, but again, since I'm hoping this is a bit of a hackable platform, I wanted to have more than just the minimum amount of power needed. I also could have chosen a different package with a smaller pin count. Three pins are used for the ICSP programming header, which don't have to be dedicated only to programming, but it gets a little hairy when trying to use them as I/O, and I wanted to avoid having to do this. I had an extra interrupt-enabled pin and was able to connect both interrupt outputs of the accelerometer to the microcontroller, which is probably not needed. Lastly, the UART for debugging was excessive (though again, consistent with the spirit of hackability).

The schematic is largely uninteresting. The only slightly interesting bit would be powering the ALS from a second toggle switch, which also asserts an I/O pin on the microcontroller, signifying that the user wishes to program a new configuration into the board.

image

board layout

This is the first board I've routed to completion. I've started a few other designs in the past but never completed them. This whole process took a little longer than I would have liked, mainly because I insisted on creating all of my own symbols and packages. While you can find libraries online for eagle that contain all of the parts I used, I wanted to make sure all of the silkscreen and footprint design was consistent, and it was faster to repeat all of the work myself then go in and modify existing work. As an added bonus, I now have an Eagle library I can call my own and fully trust. I took a while to be conscious of how I was naming footprints and packages, and some nights I sat and spun my wheels because I couldn't figure out how I wanted to name a part.

I tried routing the subsections of the board as tight as possible, as practice for the second revision where I want to fit the components into the smallest possible area. Especially difficult was the placement of the components around the boost converter. I also tried making the traces as clean as possible, something eagle makes a bit difficult. The follow-me routing is almost unusable and thus I did not use it. If I happened to place five traces next to one another, only to find that the sixth would not fit in the available space, I had to rip up the sections of the first five, move them slightly and then place the sixth trace. Also, I wanted to make sure I was using consistent 45 degree angles on every bend, something that was a bit difficult to do while routing between different grid sizes.

Here's the finished 2-side board layout. The large through-hole section on the right of the board is the debug connector. It breaks out a lot of the signals so I can probe on them to make sure stuff is working. One mistake in the layout was not rounding the corners slightly in the outline layer. This led to some sharp corners on the board. I added holes in the bottom, opposite of the LEDs as I thought I might try and see if I can swing it from some string to generate patterns as well.

image

board assembly and some interesting bringup issues

This was also the first surface mount board I have assembled. It was made somewhat painless by a microscope and hot air station in a lab at my internship I managed to get some time on. Most painful were the tiny 0402 LEDs. The polarity marking was on the bottom, so I had to flip them over, check the polarity mark, and then very carefully flip them over making sure they didn't fly out of my tweezers in the process (it also didn't help that not a single pair of tweezers in the lab had unbent tips). I eventually just noted the configuration of the bond wires to the die itself which I could see through the clear package. I used the hot air on the board in stages, which was fairly easy because the board had small groupings of components.

image

The first issue I ran into was not ordering the boost converter. -- Searching the schematic for a potential workaround, I noted that the VDD connection on the programming port was connected to the output of the on/off switch, meaning that when the switch was in the on position then the VDD pin on the programming port would have the battery voltage on it. This design decision was made such that, when the switch is in the off position, and the board is powered through the programmer, the programmer supply is not also across the batteries. However, since the boost converter is only on the board to create a stable 3.3V supply, and everything on the board will work down to the 3V the batteries supply, I could simply jump this VDD pin on the programming port to the regulator output pin. This powers the board off the batteries directly, and still allows me to use the switch to turn the board on and off until the the boost converters arrive in the mail.

After the entire board was assembled, I wrote a small program to turn all of the LEDs on the board on. Although the programmer could talk to the PIC and the program would upload, not all of the LEDs seemed to turn on. I could turn different ones on, but as soon as I modified the program to turn all of the LEDs on, the board would turn off. At first I figured that I had placed too much solder paste on the board and shorted some pins together under the PIC or the LEDs and I was creating a short through the PIC by attempting to turn specific LEDs on. To fix this, I soldered a new PIC onto the board, but was still getting the same issues.

Scratching my head at this point, I started inspecting the board carefully under the microscope, looking for any soldering issues. Doing this, I accidentally discovered that when held under the microscope (which had a very bright ring light) the board would power down. I first thought that this might be the ALS drawing too much current (it acts as a current source, drawing more current from the supply the more light it is illuminated with) causing the battery voltage to droop below 3V, so I covered the ALS with some black electrical tape and held the board under the microscope again, only for it to have the same effect. -- Feeling defeated, I decided to give the schematic one overview for any potential issues, which is when I noticed that one of the LEDs was on an I/O pin that could also function as the PIC's low voltage programming pin. Thinking this might be an issue, I re-checked the PIC configuration #pragma config lines, to discover that had not disabled low-voltage programming.

PICs come from the factory with low voltage programming enabled. Since low voltage programming does not preclude the standard programming voltages, and if your manufacturing process depends on low voltage programming, it would be impractical to have to use the regular programming voltage to enable the low-voltage programming mode.

With the one configuration line changed the board no longer powered off after being illuminated with bright light, and I was able to turn all of the LEDs on in code. My theory at the moment is that, there was a small amount of photogeneration (basically the LED acting as a PV cell) in the LED, creating a high enough voltage across the series resistor to trigger the low voltage programming mode. -- Also, it seems that if the PIC is in low voltage programming mode then it will happily put itself into programming mode if the I/O pin is brought high in software.

more board bringup

The next most important thing to check that was working was the accelerometer. The first thing I did was write some code to enable the serial port on the PIC so that I could get some data off of it to analyze. I don't really like using any libraries provided by Microchip, so I just wrote a few functions to setup the USART peripheral, and some small utility functions. Modeling the Processing approach, I like to have a setup() function that runs at the beginning of main() and calls out to other setup functions in the appropriate order.

Setting up the UART (and anything really on a microcontroller) is really just an exercise in reading the datasheet and putting 0's and 1's in all of the right memory locations. Since most of the mnemonics for the registers are sometimes impossible to remember, I like to leave comments as to why something was set to say, the value 8. Always helps down the road.

void setupUART() {
    //set the baud rate to 115.2k, using Fosc = 64MHz, the value for SPBRG is
    //taken from the datasheet
    SPBRG = 8;
    SPBRGH = 0;

    //configure the TX and RX pins as outputs (the EUSART will reconfigure them
    //automatically
    TRISCbits.TRISC6 = 1;
    TRISCbits.TRISC7 = 1;

    //configure the EUSART in async mode
    TXSTAbits.SYNC = 0;

    //enable the serial port, and enable transmission
    RCSTAbits.SPEN = 1;
    TXSTAbits.TXEN = 1;
}

From here, I wrote some simple functions to write out a character array byte by byte, and also a function to convert from integer to a character array (no printf here).

void UARTPutStr(unsigned const char *data) {
    while(*data != '\0') {
        UARTPutC(*data);
        *data++;
    }
}

char *intToStr(int num) {
    static char out[7]; //INT_MIN = -32768, so we'll need at maximum 6
                        //chars and a null termination to represent an integer
    char digits[6];     //a place to store the digits
    int digitIndex = 0; //keep track of where in the digits array we're placing
                        //digits

    int offset = 0;
    if(num < 0) {
        out[offset++] = '-';
        num = num * -1;
    }

    do {
        digits[digitIndex++] = (num % 10) + '0';
        num /= 10;
    } while(num);

    for(int i = 0; i < digitIndex + offset; i++) {
        out[i + offset] = digits[digitIndex - i - 1];
    }
    out[digitIndex + offset] = '\0';
    return out;
}

Some frustration came when attempting to read data from the accelerometer. Following the datasheet's recommendations, I configured some accelerometer registers to enable continuous block updates, set the update rate to 100Hz, enable an antialiasing filter with an 800Hz bandwidth (to reduce noise, basically a low pass filter), disabled self test, enabled 4-wire SPI mode, and set the scale to ±16G. However, the data from the accelerometer was always the same value, even when the board was violently shaken. Lots of head scratching ensued until I found an application note from ST noting that the axes are all disabled after startup. The datasheet lists the X, Y and Z enable register bits as "enabled at POR" so I figured they should be initialized after the accelerometer is powered on. However, the initialization sequence that is detailed in an application note states that the accelerometer powers up with all axes initially enabled, reads some calibration memory, does a self-calibration, and then disables all axes by modifying the appropriate register. So, while the datasheet is technically correct that at reset the axes are enabled, after the initialization sequence is completed they are not enabled. To fix this, I just had to set the enable bits to the correct value, which was simple enough. -- Again I wrote some routines to abstract away SPI operations, and then wrote a setup function for the accelerometer.

void SPIRead(unsigned char addr, int length, unsigned char *out) {
    //bring CS low to begin the cycle
    LATCbits.LATC2 = 0;

    //load and tx the address into the SPI output buffer, with the read flag set
    //[7] is the R/W bit (1 -> read), and [6:0] are the address bits
    SSPBUF = addr | 0b10000000;
    while(!SSPSTATbits.BF);

    for(int i = 0; i < length; i++) {
        //write out dummy data to clock the device
        SSPBUF = 0xFF;
        while(!SSPSTATbits.BF);
        out[i] = SSPBUF;
    }
    //bring CS high to end the cycle
    LATCbits.LATC2 = 1;
}

void SPIWrite(unsigned char addr, int length, unsigned char *in) {
    //bring CS low to begin the cycle
    LATCbits.LATC2 = 0;

    //load the address into the SPI output buffer, with the write flag set
    //[7] is the write bit (0 -> write), and [6:0] are the address bits
    //chop off the 7th bit of the address if it was set for some reason
    SSPBUF = addr & 0b01111111;
    while(!SSPSTATbits.BF);

    for(int i = 0; i < length; i++) {
        SSPBUF = in[i];
        while(!SSPSTATbits.BF);
    }

    LATCbits.LATC2 = 1;
}
void setupAccelerometer() {
    unsigned char data[2] = {
        0b01100111,     //data rate -> 100Hz (ODR = 0b0110)
                        //block update -> continuous (BDU -> 0b1)
                        //axis enable -> X, Y, Z enabled
        0b00100000      //AA filter bandwidth -> 800Hz (BW -> 0b00)
                        //scale selection -> +-16Oh G (FSCALE -> 0b011);
                        //self test disabled (ST -> 0b00)
                        //SPI mode -> 4 wire mode (SIM -> 0);
    };
    SPIWrite(0x20, 1, &data[0]);
    SPIWrite(0x24, 1, &data[1]);
}

Then, since the Y acceleration is really all I'm interested in (it's the one that's parallel with the motion of the card when shaken), I wrote a function to abstract that away too.

signed int getYAcceleration() {
    unsigned char yData[2];

    //TODO : optimize this to use the automatic address increment feature
    //       of the accelerometer's SPI interface
    SPIRead(0x2B, 1, &yData[0]);    //read the MSB 8 bits of the Y axis
    SPIRead(0x2A, 1, &yData[1]);    //read the LSB 8 bits of the Y axis

    return (yData[0] << 8) | yData[1];
}

At this point, I wanted to test my assumptions about what the acceleration would look like, as well as confirm that I was going to get a semblance of something that was periodic to actually use to time when to light the LEDs up. I wrote some code to stream the acceleration readings over the serial port. Running this code, I shook the card back and forth, collected the data, and plotted it. Here are the results of one of those tests.

image

As you can see, the periodicity is easily visible, and at this point I was pretty confident that I was going to be able to make this thing work.

using the data / timer-counter magic

Interpreting the data is a little confusing. Since the data is acceleration, not velocity or position, an initial thought would be to numerically integrate the data twice, and then use the resulting data as the position data. However, the arc the hand makes when shaking the card will have the "zero crossing" of the acceleration in the middle of the arc, it's when the hand goes from "speeding up" to "slowing down" so the acceleration should cross from positive to negative, or negative to positive, the acceleration should reach a maximum at the edges of the arc, when the direction change occurs, after which the acceleration should start to decrease in magnitude again before crossing zero again.

Mathematically, if the position is considered to be sin(t) (considering the motion as linear) then the second derivative in time will be the acceleration (which is what the accelerometer reports), which will be -sin(t). Thus, the zero crossing of position occurs at the same time as the zero crossing in acceleration as predicted. The peaks occur also at the same time, just opposite in sign. Thus, it's possible to treat the acceleration data as just negated position data (in this case).

Note that this is the acceleration in the direction of the linear velocity of the board. There will also be a perpendicular component that is also sinusoidal, which could also be used. I may look at combining the two in the future to reduce noise.

The end goal is to distribute the "rows" of the text or graphic that needs to be displayed when the card is shaken over this arc. To do this, I used two timers. One timer, running at a slower frequency, is used to measure the time between subsequent "peaks" and "troughs" in the acceleration. This time is then divided by however many columns need to be displayed, and a second timer is used to count these intervals, increment the column to display when required, and turn the LEDs on and off according to which column is currently being displayed. With that said, here's the code that searches for the peaks and troughs, and then sets some variables up for the timer interrupts to use.

int main(int argc, char** argv) {

    setup();

    //enable interrupts
    INTCONbits.GIE = 1;     //enable global interrupts
    INTCONbits.PEIE = 1;    //enable peripheral interrupts

    //setup timer 0, which times the length of one shake
    T0CONbits.T0CS = 0;     //timer 0 is driven by Fosc/4
    T0CONbits.PSA = 0;      //enable timer 0 clock prescaler

    T0CONbits.TMR0ON = 1;   //turn timer 0 on
    T0CONbits.T0PS = 0b010; //1:8 prescale value for timer 0;

    //INFO : timer 0 will overflow at 7.813KHz, meaning that one overflow of periodCount is equal to
    //       128us
    INTCONbits.TMR0IE = 1;  //enable timer 0 overflow interrupts

    //setup timer 1, which times out each column of the display
    T1CONbits.TMR1ON = 1;   //turn on timer 1
    TMR1H = 0xF0;           //set the count to 15/16ths of the overflow value
    TMR1L = 0x00;

    //INFO : since timer 1 is configured with a prescaler of 1:1, and Fosc/4 = 16MHz, it will
    //       overflow (16bits) at 244.1Hz, with a period of 4.096ms, to cause the overflow to occur
    //       faster, the counter register is preloaded with 0xF000 at every overflow, which is
    //       15/16ths the overflow value of 0xFFFF, so the counter (running at 16Mhz) only has to
    //       count 0xFFF more to overflow, this will happen at 3.907kHz, or every 256us, this is
    //       chosen so that if there are about 3 shakes in a second, the timer can still keep track
    //       of line widths of 100 or more


    signed int y;
    signed int previousY = getYAcceleration();
    float shakePeriod;
    float columnPeriod;

    while(1) {

        //get the current y acceleration
        y = getYAcceleration();

        //need to do a peak find, alternating between "peaks" and "troughs"
        //alternating between searching for these get us the left and right
        //edges of the shake
        if(posPeak && y < previousY || !posPeak && y > previousY) {
            if(posPeak && (y > THRESHOLD) || !posPeak && y < -THRESHOLD) {
                //a peak was found, now we need to search for the opposite peak
                posPeak = !posPeak;

                //now we need to setup timer 1 to generate `rowCount` pulses during one shake
                shakePeriod = periodCount * 128.0e-6;
                columnPeriod = shakePeriod/((float)columnCount);
                columnTimerPeriod = (int)(columnPeriod/(256.0e-6));

                //enable TMR1 interrupts
                PIE1bits.TMR1IE = 1;

                //start a new period
                periodCount = 0;
                columnTimerCount = 0;
                currentColumn = 0;
            }
        }
        previousY = y;
    }
}

This code first sets up two hardware timers. Timer0 is used to time the period of the acceleration "sine" wave. It's clock source and prescale value cause it to overflow and generate an interrupt at 7.813kHz. Timer1 is setup similarly, but because it is a 16 bit timer, every time the timer overflows it is reset with a value of 0xF000, to increase the frequency at which the timer overflows to around 4kHz.

In the main loop, a positive or negative peak is found by examining the current and past acceleration value, and toggling between looking for a peak and a trough ("negative" peak value). periodCount is the number of Timer0 interrupts that have occurred (it is incremented in the interrupt), this is multiplied by the period of the interrupt to get the total time since the last peak or trough. This time is then divided by how many columns need to be displayed, and then this value is then divided by the period of the "column" Timer1 interrupts. The resulting value is how many Timer1 interrupts should occur between changing the column of LEDs to the next value. The interrupt, shown below, handles this.

void interrupt isr(void) {
    if(INTCONbits.TMR0IF) {
        //this timer increments a period timer so we can count how long the last shake took
        periodCount++;
        INTCONbits.TMR0IF = 0;
    }
    if(PIR1bits.TMR1IF) {
        TMR1H = 0xF0;
        TMR1L = 0x00;
        if(columnTimerCount >= columnTimerPeriod) {
            currentColumn++;
            if(currentColumn > columnCount) {
                setLEDOutput(0b0000000000);
            } else {
                if(posPeak) {
                    setLEDOutput(image[columnCount - currentColumn - 1]);
                } else {
                    setLEDOutput(image[currentColumn]);
                }
            }
            columnTimerCount = 0;
        } else {
            columnTimerCount++;
        }
        PIR1bits.TMR1IF = 0;
    }
}

setLEDOutput is a short, unglamorous function that takes a 10 bit wide binary value and sets the status of the LEDs. The image buffer is an array of 10 bit wide values. Otherwise, this should be pretty self explanitory. The only tricky bit is figuring out if the card is moving "forwards" or "backwards" and subsequently traversing forwards through the image buffer or backwards.

static unsigned int image[XXX] = {
    0b0001111111,
    0b0000000010,
    0b0000001100,
    0b0000000010,
    0b0001111111,
    0b0000000000,
    ...
}

demo

Here's a clip of when I first got it working. Keep in mind, it looks a lot better in person, due to the aliasing the frame rate of the video causes.

generating the image buffer

Typing out 100 or so 10 bit wide fields is not a great way to spend an afternoon, so I wrote a quick script in Node.js using the aptly-named get-pixels library to take 10 pixel tall black and white image and convert it to something I could copy and paste into my code.

image

var getPixels = require('get-pixels');

getPixels("mattegan.png", function(err, pixels) {
    if(err) {
        console.log(err);
    } else {
        var w = 59;
        var h = 10;
        var lineStr;
        var pixels = pixels.data;
        for(var col = 0; col < w; col++) {
            lineStr = '\t0b';
            for(var row = 9; row >= 0; row--) {
                var pixelStartIndex = (col + row * w) * 4;
                var color;
                for(var c = 0; c < 3; c++) {
                    color = pixels[pixelStartIndex + c] << ((2 - c) * 8);
                }
                lineStr += color == 0 ? '1' : '0';
            }
            console.log(lineStr + ',');
        }
    }
})

conclusion

I'm going to focus a bit on the software now I think. I would like to clean it up and maybe use some more complex methods for ascertaining the motion of the card. I also need to try and get the "serial via light" working, as well as the low power modes. However, I would like to get working on the second revision of the hardware, a more "polished" and presentable version. This whole thing has been a lot of fun and a super valuable learning experience, and I'm excited to see where else it takes me.