Der Blinken Lights (Part II -- Framebuffer)

Looking at the hardware from part I of this series, you may have realized that the addressing scheme of the individual pixels is likely to be a bit hinky, which is true. It's also necessary to rapidly scan through the columns in order to produce the illusion of a continuous image. The canonical way to do this is to store the image data in a frame buffer structured in such a way as to streamline writing the data out. Interface routines abstract the wiring and addressing shenanigans.

So, on to the specifics:

Column Addressing (4X4 Driver)

Each column on each panel is wired to a common anode, and all sixteen column anodes are driven by a 4X4 driver shield. It's a straightforward setup -- push the desired column configuration out to the shield (via the shiftOut() function) and be done. Sadly, the included shiftOut() function is too slow, since it uses digitalWrite(), which is a notorious pig. Since I wired the display for convenience of soldering, not addressing, I had to write a mapping function:

void selectColumn(int8_t column) {
  byte high, low;
  
  static byte colMapLo[] = {0x00, 0x00, 0x00, 0x00, 0x10, 0x20, 0x40, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x08};
  static byte colMapHi[] = {0x10, 0x20, 0x40, 0x80, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00};
  
  static uint8_t bitXLAT = digitalPinToBitMask(XLAT);
  static uint8_t portXLAT = digitalPinToPort(XLAT);
  static volatile uint8_t *outXLAT = portOutputRegister(portXLAT);
  
  if ((column > 15) || (column < 0)) {
    high = 0xff;
    low = 0xff;
  } else {
    high = colMapHi[column];
    low = colMapLo[column];
  }
  *outXLAT &= ~bitXLAT;
  fastShiftOut(SIN, SCLK, LSBFIRST, high);
  fastShiftOut(SIN, SCLK, LSBFIRST, low);
  *outXLAT |= bitXLAT;
}

What this does is, given a column selection, writes the appropriate information to the high and low bytes of the 4x4 Driver in order to illuminate the desired column on the each half of the display. The fiddling around with *outXLAT is just me poking the output latch to switch to the new row. 

There's one more wrinkle. If you look at this picture of one of the boards with the column wiring in, you will notice that they sense of the column wiring swaps between the upper and lower halves. This certainly makes the wiring neater, but it means we have to mangle what column the pixels in the lower half are assigned to in order to get the correct mapping. This makes the framebuffer itself somewhat ... interesting.

Row Addressing (I2C PWM Driver)

Each of the row drivers has sixteen outputs, so the conceptually obvious thing to do is assign one to each color. I didn't do that because it would have made the wiring more complex. 

The aforementioned strange wiring means that the row addressing is just a mess of special cases. It's mapping by if statement. Those who are more experienced programmers than I can probably point out a number of easier ways to do this in C/C++, but I wanted this code to be more or less within the Arduino framework. It ended up being wound deeply into the framebuffer.

We assign a 2D array to each driver, one dimension for the column and one for the rows. That allows us to write that array through functions that take care of the addressing and then write it out to the drivers with a simple for loop.

Building the Framebuffer

I built some functions to map cartesian coordinates onto a frame buffer that I could step through. These are ordinary enough mapping functions; here's the one for the red pixels:

void setRedPixel(uint8_t value, uint8_t column, uint8_t row) {
  uint8_t j;
  if ((row > 15) || (column > 15)) return;
  if (row < 8) {
    if (column < 8) {
      j = (7 - column);
    } else {
      j = (23 - column);
    }
  } else {
    j = column;
  }
  setRedRow(row, ~value, Abuf[j], Bbuf[j], Cbuf[j]); // we negate the value b/c value = 0x0 is full on
  return;
}

Abuf, Bbuf, and Cbuf are global buffers that hold the next set of values to write out to the three I2C PWM drivers. One thing to note is that, because of the way the drivers are built, 0x0 corresponds to full on. Therefore, we invert the desired PWM value here, just before we write it into the data structure that we send out over I2C, so that it behaves intuitively in other parts of the code. 

Note that we also mangle the column so that it goes to the correct cell in the array so it gets picked up correctly by the refresh loop.

Here's the function that it calls to modify a single row for driver A:

void setRedRow(uint8_t row, uint8_t value, byte *A, byte *B, byte *C) {
  uint8_t j;
  if (row < 4) {
    j = row + 4;
    C[j] &= 0x0f;                 // delete the existing upper nibble
    C[j] |= (value & 0x0f) << 4;  // write in the new value to the upper nibble
  } else if (row < 8) {
    j = row - 4;
    C[j] &= 0x0f;                 // delete the existing upper nibble
    C[j] |= (value & 0x0f) << 4;  // write in the new value to the upper nibble
  } else if (row < 12) {
    j = row - 4;
    A[j] &= 0x0f;                 // delete the existing upper nibble
    A[j] |= (value & 0x0f) << 4;  // write in the new value to the upper nibble
  } else if (row < 16) {
    j = row - 12;
    A[j] &= 0x0f;                 // delete the existing upper nibble
    A[j] |= (value & 0x0f) << 4;  // write in the new value to the upper nibble 
  } 
}

As you can see, it's a mess of if statements which abstract the wiring in such a way as to make the output code super easy (and fast):

// update framebuffer
column++;
if (column > 15) {
  column = 0;
  frameCnt++;
}
*outMR &= ~bitMR;
*outXLAT &= ~bitXLAT;
*outMR |= bitMR;
*outXLAT |= bitXLAT;
RowSend(ADD_A, Abuf[column]);
RowSend(ADD_B, Bbuf[column]);
RowSend(ADD_C, Cbuf[column]);
selectColumn(column);

The RowSend() function is just a bit of wrapper around some calls to the Wire library:

void RowSend(byte address, byte *column) {
  Wire.beginTransmission(address);
  Wire.write(0x10);
  Wire.write(column, 8);
  Wire.endTransmission();
}

Since I'd like to step through the framebuffer rapidly and not worry about it, I attached it to a timer interrupt. This is a demo, so I started out just calling everything from the interrupt handler. That should work, right?

Those of you with a better understanding of I2C on AVRs than I did are probably already laughing. When I tried to use this code, it hung as soon as I started the timer. This made me think there was something wrong with how I was starting the timer. After an hour or so of banging around, I went back to an old standby for debugging this sort of thing -- new blank program, add one thing at a time. What broke it was when I added back the calls to rowSend(). A little more debugging of the comment/printf() style proved that the problem was calling the Wire() library from inside interrupt context. On reflect, this is not really all that surprising. The solution is to change the interrupt handler to read as follows:

volatile byte stepFlag;
ISR (TIMER4_OVF_vect) {
  TCNT4 = TIMERVAL;
  stepFlag++;
}

And then add busy-wait loops that wait for stepFlag to be nonzero, and then write the framebuffer when it's not. And then set stepFlag back to zero, of course. It's nice because, among other things, it provides an easy way to detect overruns -- if stepFlag is ever greater than one, you know you dropped at least one step.

However, when push came to shove, I decided to just let the code free-run. Timing isn't critical, and I can go just a hair faster that way.

Much better now! The result is that I can now turn on all 256 pixels in order, one color at a time. You can download the complete code for this project to see all the ugly details, including how I did the game of life pattern. 

Make sure to check out our earlier hardware post, as well as the following products used in this demo: