Or an 840 Segment display (960 if you count the decimal point)
Now, I wonder if I can get an old calculator to play the accompanying 4 track MOD?
What’s going on here then?
It all started with an eBay purchase of some cheap Chinese 7 segment displays.
A month or two later and bag of 30 arrived! Did I really buy 30? I left them in a drawer and forgot all about them. Eventually, I dug them out to play with but soon realised that these 7 segment displays were common anode and wouldn’t work nicely with my old friend the Max7219. I eventually figured it out [common anode displays on the Max7219 and an Arduino] and, another eBay purchase later, I had a bag full of Max7219s too.
I then designed a PCB to drive 2 of these 4 digit packages. That’s 64 LEDs, the total number a single Max7219 can drive. The PCB needed to be smaller than the 2 displays if I wanted to stick these modules together into one big array. And I did.
Daisy chaining multiple Max7219s
One of the great things about the Max7219 is that they can be chained together. Data Out (pin 24) carries across to the Data In (pin 1) of another max7219 and so on. Doing this means I can control many more than 64 LEDs. I designed the PCB with this in mind with the idea that they would be beautifully modular and slot together effortlessly. My first version didn’t quite work and was misaligned. Nothing a soldering iron and some wire couldn’t fix.
I’d never coded for multiple Max7219s before and strangely couldn’t find much online.
A standard function to send data to a Max7219 would look something like this :
void MAX7219Send(uint8_t address, uint8_t value)
{
// Ensure LOAD/CS is LOW
digitalWrite(chip_select, LOW);
// Send the register address
SPI.transfer(address);
// Send the value
SPI.transfer(value);
// Tell chip to load in data
digitalWrite(chip_select, HIGH);
}
After some experimenting I discovered it was pretty easy to address multiple Max7219s from my Arduino Pro Micro clone. To write to three would look something like :
void MAX7219Send(uint8_t address_A, uint8_t value_A, uint8_t address_B, uint8_t value_B, uint8_t address_C, uint8_t value_C)
{
// Ensure LOAD/CS is LOW
digitalWrite(chip_select, LOW);
// Send the register address
SPI.transfer(address_A);
// Send the value
SPI.transfer(value_A);
// Send the register address
SPI.transfer(address_B);
// Send the value
SPI.transfer(value_B);
// Send the register address
SPI.transfer(address_C);
// Send the value
SPI.transfer(value_C);
// Tell chip to load in data
digitalWrite(chip_select, HIGH);
}
At this point, I’ve got a few connected together and I’m beginning to think about how I’m going to display bitmaps, or single ‘pixels’, on this thing.
Mapping pixels to segments
I ordered more PCBs, this 7 segment folly was becoming pretty expensive. In total I’d have a bank of 5 x 3 modules, that’s 20 x 6 digits. If each 7 segment digit corresponds to a 2 x 4 pixel block then that would give me a virtual screen size of 40 x 30 pixels. A single 8 digit module would map to a 8 x 10 pixel area, like this :
Notice there’s a lot of pixels here that don’t map to any segments, 30% that will never be displayed.
I created a 40 x 30 1 bpp screen buffer and will use standard routines to put pixels, draw lines, copy bitmaps etc. into this buffer before, somehow, translating that lot to the 7 segment displays.
The data structure for my screen buffer looks like :
byte screen_buffer[30][5] =
{
// bank 0 bank 1 bank 2 bank 3 bank 4
// top
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
// lower
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
// bank 0 bank 1 bank 2 bank 3 bank 4
// top
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
// lower
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
// bank 0 bank 1 bank 2 bank 3 bank 4
// top
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
// lower
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,},
{ 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}
};
Standard putPixel(x,y) and removePixel(x,y) routines are pretty straightforward, just a matter of turning bits on and off in the screen buffer :
void removePixel(byte x, byte y)
{
byte bank = x / 8;
byte by = y;
byte thebit = 0b10000000 >> ((x % 8)); // bit to turn off
screen_buffer[by][bank] &= ~thebit;
}
void putPixel(byte x, byte y)
{
byte bank = x / 8;
byte by = y;
byte thebyte = 0b10000000 >> ((x % 8));
screen_buffer[by][bank] |= thebyte;
}
Displaying the screen buffer on the 7 segment array is a little more complicated, partly because the Max7219s are wired in reverse (the common anode problem). This made my code slightly more weird than it would've been if I was using common cathode displays.
In short, the usual digit parameter of a Max7219 command now refers to the segment and the data refers to the digit, or any number of the 8 addressable digits.
Normally, to set the C segment on digit 1 you would use the command :
MAX7219Send(1,0b00010000);
But with my reversed, common anode, wiring to set the C segment on digit 1 it's :
MAX7219Send(3,0b00000001);
Or to set the C segment on all digits :
MAX7219Send(3,0b01111111);
My idea then, to loop through 7 segments and build the bit pattern to send for each digit of a module / bank, for each of the 15 modules.
Given the digit (0 to 7) and the bank(0 to 14) I have to grab and convert the pixel data from the screen buffer for each digit and bank I'm looking at.
Anyway, here's the confusing code :
// returns current screen buffer segment bit state for given bank and digit
byte get_screen_buffer(byte digit, byte bank)
{
// x,y into screen buffer from digit, bank
byte bx = (bank % 5);
byte by = ((digit / 4) ) * 5 + ((bank / 5) * 10);
byte segs = 0;
byte shift;
if (digit == 0) shift = 6;
if (digit == 1) shift = 4;
if (digit == 2) shift = 2;
if (digit == 3) shift = 0;
if (digit == 4) shift = 6;
if (digit == 5) shift = 4;
if (digit == 6) shift = 2;
if (digit == 7) shift = 0;
bool bittest;
/*
pixel to digit mapping :
A,x,
F,B,
G,x,
E,C,
D,x
0,0 = A
0,1 = F
1,1 = B
0,2 = G
0,3 = E
1,3 = C
0,4 = D
*/
//A
bittest = ( ( (screen_buffer[by][bx]) & (0b00000010 > shift );
segs = segs | ( (bittest * 255) & 0b1000000);
//B
bittest = ( ( (screen_buffer[by + 1][bx]) & (0b00000001 > shift );
segs = segs | ((bittest * 255) & 0b0100000);
//C
bittest = ( ( (screen_buffer[by + 3][bx]) & (0b00000001 > shift );
segs = segs | ((bittest * 255) & 0b0010000);
//D
bittest = ( ( (screen_buffer[by + 4][bx]) & (0b00000010 > shift );
segs = segs | ((bittest * 255) & 0b0001000);
//E
bittest = ( ( (screen_buffer[by + 3][bx]) & (0b00000010 > shift );
segs = segs | ((bittest * 255) & 0b0000100);
//F
bittest = ( ( (screen_buffer[by + 1][bx]) & (0b00000010 > shift );
segs = segs | ((bittest * 255) & 0b0000010);
//G
bittest = ( ( (screen_buffer[by + 2][bx]) & (0b00000010 > shift );
segs = segs | ((bittest * 255) & 0b0000001);
return segs;
}
#define DIGIT1 0b01000000
#define DIGIT2 0b00100000
#define DIGIT3 0b00010000
#define DIGIT4 0b00001000
#define DIGIT5 0b00000100
#define DIGIT6 0b00000010
#define DIGIT7 0b00000001
#define DIGIT8 0b10000000
byte display_digits[]
{
DIGIT1,
DIGIT2,
DIGIT3,
DIGIT4,
DIGIT5,
DIGIT6,
DIGIT7,
DIGIT8
};
byte bank[15];
byte segment_lookup[] =
{
0b1000000, //A
0b0100000, //B
0b0010000, //C
0b0001000, //D
0b0000100, //E
0b0000010, //F
0b0000001 //G
};
void display_screen_buffer()
{
// Each max7219 controls 1 bank of 8 digits.
// for each 7-seg segment (A to F) of a digit
for (byte segments = 0; segments
And here's the full code :
#include
#define LOAD_PIN 10 byte screen_buffer[30][5] = { // bank 0 bank 1 bank 2 bank 3 bank 4 // top { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, // lower { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, // bank 0 bank 1 bank 2 bank 3 bank 4 // top { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, // lower { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, // bank 0 bank 1 bank 2 bank 3 bank 4 // top { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, // lower { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,}, { 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,} }; // returns current screen buffer segment bit state for given bank and digit byte get_screen_buffer(byte digit, byte bank) { // x,y into screen buffer from digit, bank byte bx = (bank % 5); byte by = ((digit / 4) ) * 5 + ((bank / 5) * 10); byte segs = 0; byte shift; if (digit == 0) shift = 6; if (digit == 1) shift = 4; if (digit == 2) shift = 2; if (digit == 3) shift = 0; if (digit == 4) shift = 6; if (digit == 5) shift = 4; if (digit == 6) shift = 2; if (digit == 7) shift = 0; bool bittest; /* pixel to digit mapping : A,x, F,B, G,x, E,C, D,x 0,0 = A 0,1 = F 1,1 = B 0,2 = G 0,3 = E 1,3 = C 0,4 = D */ //A bittest = ( ( (screen_buffer[by][bx]) & (0b00000010 > shift ); segs = segs | ( (bittest * 255) & 0b1000000); //B bittest = ( ( (screen_buffer[by + 1][bx]) & (0b00000001 > shift ); segs = segs | ((bittest * 255) & 0b0100000); //C bittest = ( ( (screen_buffer[by + 3][bx]) & (0b00000001 > shift ); segs = segs | ((bittest * 255) & 0b0010000); //D bittest = ( ( (screen_buffer[by + 4][bx]) & (0b00000010 > shift ); segs = segs | ((bittest * 255) & 0b0001000); //E bittest = ( ( (screen_buffer[by + 3][bx]) & (0b00000010 > shift ); segs = segs | ((bittest * 255) & 0b0000100); //F bittest = ( ( (screen_buffer[by + 1][bx]) & (0b00000010 > shift ); segs = segs | ((bittest * 255) & 0b0000010); //G bittest = ( ( (screen_buffer[by + 2][bx]) & (0b00000010 > shift ); segs = segs | ((bittest * 255) & 0b0000001); return segs; } void putpixel(byte x, byte y) { // bounds check if ((x>=0) && (x=0) && (y> ((x % 8)); screen_buffer[by][bank] |= thebyte; } } bool getpixel(byte x, byte y, bool setclear) { // which bank byte bank = x / 8; byte by = y; byte thebit = 0b10000000 >> ((x % 8)); byte check = screen_buffer[by][bank] & thebit; bool result = check > ((x % 8)); // bit to turn off screen_buffer[by][bank] &= ~thebit; } #define DIGIT1 0b01000000 #define DIGIT2 0b00100000 #define DIGIT3 0b00010000 #define DIGIT4 0b00001000 #define DIGIT5 0b00000100 #define DIGIT6 0b00000010 #define DIGIT7 0b00000001 #define DIGIT8 0b10000000 byte display_digits[] { DIGIT1, DIGIT2, DIGIT3, DIGIT4, DIGIT5, DIGIT6, DIGIT7, DIGIT8 }; byte bank[15]; byte segment_lookup[] = { 0b1000000, //A 0b0100000, //B 0b0010000, //C 0b0001000, //D 0b0000100, //E 0b0000010, //F 0b0000001 //G }; void display_screen_buffer() { // Each max7219 controls 1 bank of 8 digits. // for each 7-seg segment (A to F) of a digit for (byte segments = 0; segments x2 ) dx = x1-x2; else dx = x2-x1; if ( y1 > y2 ) dy = y1-y2; else dy = y2-y1; if ( dy > dx ) { swapxy = 1; tmp = dx; dx =dy; dy = tmp; tmp = x1; x1 =y1; y1 = tmp; tmp = x2; x2 =y2; y2 = tmp; } if ( x1 > x2 ) { tmp = x1; x1 =x2; x2 = tmp; tmp = y1; y1 =y2; y2 = tmp; } err = dx >> 1; if ( y2 > y1 ) ystep = 1; else ystep = -1; y = y1; for( x = x1; x See also : How to drive common anode displays with the Max7219 and an Arduino