Firmware
The firmware is written in AVR-GCC and used about 60% of the available program space of 2KB in the ATtiny2313.
It is really quite simple piece of software. Basically all it does it to receive command packets on the serial port of the Tiny2313, check if the command is for this display, and if it is decode the command and store the bitpattern into the memory. If the command is for another display it will just pass it on to the next display.
The firmware can be download both as source code and as as precompiled hexfile from the download section.
Message format
The messages between the Windows Service and the microncontrollers on the displays are formatted in rather compact 10 byte packets. In order to be able to relaibly detect the beginning of each packet the most significant bit [MSB] (bit 7 that is) in the first byte is set to zero, the rest of the bytes are set to one.
Since each digit has eight leds (the seven segments plus the decimal point [DP]) that needs to be controlled we can't just map each digit onto a single byte beacuse that would get the MSB set to zero (the start of packet indicator) if the decimal point was turned off. So the DPs gets its own byte, but since we have eight digits, and thus eight DPs the last DP must be put somewhere else or we'll get into the same problem again. The last digits DP is put on the first byte at bit six..
The first byte is is in addition to signalling the packet start and keeping the last digits DP also holds four bits (bit 3 to 0) that indicates which display (unit) this message is for. The first display is numbered 0 the next 1 and the last display is number 15. When the first display in a chain receives a message it checks the unit number and sees if it's 0. If it is the message is processed there. If it's not then the display decrements the value in the unit bits and passes the entire message along to the next display in the chain. That display also checks the bits for zero and acts accordingly.
Bit no
| 7 6 5 4 3 2 1 0
-------+---------------------------------------------
Byte 0 | 0 d7p x x u3 u2 u1 u0
1 | 1 G F E D C B A (digit 0)
2 | 1 G F E D C B A (digit 1)
3 | 1 G F E D C B A (digit 2)
4 | 1 G F E D C B A (digit 3)
5 | 1 G F E D C B A (digit 4)
6 | 1 G F E D C B A (digit 5)
7 | 1 G F E D C B A (digit 6)
8 | 1 G F E D C B A (digit 7)
9 | 1 d6p d5p d4p d3p d2p d1p d0p
The table above shows the format of the message.
- d0p is the Digit 0 decimal Point, d1p is the Digit1 decimal point and so on...
- u0, u1, u2, u3 are the bits that tells the unit number that the message is intended for.
- A,B,C,D,E,F,G are the segments of each digit.
- x is an unused bit for future expansion.
Command receiving
Reciving the data from the PC is done wih interrupts. Each incomming character generats an interrupt and gets handled by the function below. It collects ten characters in a buffer, checks if the command is for this display and decodes it and stores it if it is. If the message is for another display it decremets the destinavtion nybble and send the entire message to the next display in the chain in the hope that a display later on will be there to handle the message.
//
// This is the interrupt handler for the UART data received.
//
// Collect all incomming characters in the rxBuf -buffer until
// we got all ten charachets of the message
// Then check if the message is for this unit. If it is, decode the
// data and put it in the display array. Othervise pass it on to the
// next display in the chain.
//
ISR(SIG_USART0_RX) {
unsigned char rx;
rx = UDR; // Get the incomming data
// If the MSB if the character is not set then it's a start byte
if (rx < 128) {
rxPtr = 0; // So reset the buffer pointer to zero
}
rxBuf[rxPtr++] = rx; // Store the character in the buffer
// If we got the entire message then process it.
if (rxPtr == MSG_LENGTH) {
// Check If the message is for us
if ((rxBuf[0] & 0x0F)== 0) {
// Yes, decode and store in memory
for (rx=0; rx<8; rx++) {
digits[rx] = (rxBuf[rx+1] & 0x7F);
}
if (bit_is_set(rxBuf[0],6)) digits[7] |= 0x80;
for (rx=0; rx<7; rx++) {
if (bit_is_set(rxBuf[MSG_LENGTH-1],rx)) digits[rx] |= 0x80;
}
} else {
// Not for us, so we just pass it on
rxBuf[0]--; // Decrement the unit-counter
// Copy the entire message to the transmit buffer
for (int i=0; i
txBuf[i] = rxBuf[i];
}
txPtr = 1; // The next character to be sent by the ISR
UDR = txBuf[0]; // Send the first character manually to trigger the ISR
}
rxPtr = 0; // Reset the receive buffer pointer to zero
}
// Make sure that the pointer can't point outside the buffer
if (rxPtr >= MSG_LENGTH) rxPtr = MSG_LENGTH - 1;
}
Digit multiplexing
Since the digits are multiplexed (only one of the digits are powered at a time) the firmware continously lights up digit after digit for a few milliseconds each in so the eye/brain beleives that all digits are on at the same time,.
To save I/O-pins on the microcontroller a special multiplexing technique called Charlieplexing is used. It drastically reduces the number of needed pins on the microcontroller but requires a more complex software controlling it.
Timer0 is used to generate interrupts for the digit multiplexing
// Define the i/o pins that are connected to the charlieplexed
// display. The suffix B or D makes it easier to keep track
// of the PORT the bit belongs to.
#define CHARLIE_1_B PB3
#define CHARLIE_2_B PB1
#define CHARLIE_3_D PD5
#define CHARLIE_4_D PD3
#define CHARLIE_5_D PD4
#define CHARLIE_6_B PB0
#define CHARLIE_7_B PB4
#define CHARLIE_8_B PB2
#define CHARLIE_9_D PD6
uint8_t digits[8]; // The array containing the digits to be shown on the display
//
// This is the Interrupt Service Routine that need to get called
// often enough to not show any visible flickering on the displays.
//
ISR (SIG_OVERFLOW0) {
static uint8_t currentdigit=0; // Keeps track of the digit to be shown
uint8_t segments; // The bitpattern of the current digit
TCNT0 = 128; // Reset TCNT0 halfway to get a higher freq
// Get the bit pattern of the current digit
segments=digits[currentdigit];
// Set all LED-portpins as inputs so the corresponding
// segments of the displays will be off
DDRB &= ~(
_BV(CHARLIE_1_B) |
_BV(CHARLIE_2_B) |
_BV(CHARLIE_6_B) |
_BV(CHARLIE_7_B) |
_BV(CHARLIE_8_B)
);
DDRD &= ~(
_BV(CHARLIE_3_D) |
_BV(CHARLIE_4_D) |
_BV(CHARLIE_5_D) |
_BV(CHARLIE_9_D)
);
// Set low level on all LED-port pins. This prepares the segments
// to be lit if the pin is changed to output at a later stage. The
// displays ar of common anode-type (common positive) so the segments
// needs to be sunk to ground to be turned on.
PORTB &= ~(
_BV(CHARLIE_1_B) |
_BV(CHARLIE_2_B) |
_BV(CHARLIE_6_B) |
_BV(CHARLIE_7_B) |
_BV(CHARLIE_8_B)
);
PORTD &= ~(
_BV(CHARLIE_9_D) |
_BV(CHARLIE_3_D) |
_BV(CHARLIE_4_D) |
_BV(CHARLIE_5_D)
);
// Set portpins as output for segments to be lit
// We just assume that each segment has it's standard
// place in the connections. The special case is handled
// in the switch-statement below.
if (bit_is_set(segments,0)) DDRB |= _BV(CHARLIE_1_B);
if (bit_is_set(segments,1)) DDRB |= _BV(CHARLIE_2_B);
if (bit_is_set(segments,2)) DDRD |= _BV(CHARLIE_3_D);
if (bit_is_set(segments,3)) DDRD |= _BV(CHARLIE_4_D);
if (bit_is_set(segments,4)) DDRD |= _BV(CHARLIE_5_D);
if (bit_is_set(segments,5)) DDRB |= _BV(CHARLIE_6_B);
if (bit_is_set(segments,6)) DDRB |= _BV(CHARLIE_7_B);
if (bit_is_set(segments,7)) DDRB |= _BV(CHARLIE_8_B);
// Here we do the bit-fiddling thats neccesary to charlieplex.
// Since one of the segments (each different) of each display
// is moved to the 9'th connection we need to take care of that.
//
// Depending on which digit that are active now we need to handle
// the situation in its own unique way.
//
// The A segment on the first digit is moved from the 1'th line
// to the 9'th line so be basically to the same thing as in the bunch
// of tests above, but only test for the A segment and if it's lit
// we turn on the 9'th line instead of the first line.
// We then need to activate the transistor that handles the common
// anode for the first digit. The transistor for the first display
// is connected to the 1'th line (where the A-segment usualy go).
// so we turn on the output for that pin and set it high.
//
// The next time this routine is called we do the same thing with
// the second digit. But we then check for the B-segment and so on...
switch (currentdigit) {
case 0:
if (segments & 0b00000001) DDRD |= _BV(CHARLIE_9_D);
DDRB |= _BV(CHARLIE_1_B);
PORTB |= _BV(CHARLIE_1_B);
break;
case 1:
if (segments & 0b00000010) DDRD |= _BV(CHARLIE_9_D);
DDRB |= _BV(CHARLIE_2_B);
PORTB |= _BV(CHARLIE_2_B);
break;
case 2:
if (segments & 0b00000100) DDRD |= _BV(CHARLIE_9_D);
DDRD |= _BV(CHARLIE_3_D);
PORTD |= _BV(CHARLIE_3_D);
break;
case 3:
if (segments & 0b00001000) DDRD |= _BV(CHARLIE_9_D);
DDRD |= _BV(CHARLIE_4_D);
PORTD |= _BV(CHARLIE_4_D);
break;
case 4:
if (segments & 0b00010000) DDRD |= _BV(CHARLIE_9_D);
DDRD |= _BV(CHARLIE_5_D);
PORTD |= _BV(CHARLIE_5_D);
break;
case 5:
if (segments & 0b00100000) DDRD |= _BV(CHARLIE_9_D);
DDRB |= _BV(CHARLIE_6_B);
PORTB |= _BV(CHARLIE_6_B);
break;
case 6:
if (segments & 0b01000000) DDRD |= _BV(CHARLIE_9_D);
DDRB |= _BV(CHARLIE_7_B);
PORTB |= _BV(CHARLIE_7_B);
break;
case 7:
if (segments & 0b10000000) DDRD |= _BV(CHARLIE_9_D);
DDRB |= _BV(CHARLIE_8_B);
PORTB |= _BV(CHARLIE_8_B);
break;
}
// Show next digit at the next interrupt. This can't be done
// at the top of the ISR since the digit variable is needed
// in the switch-statement later on.
currentdigit++;
if (currentdigit>7) currentdigit=0;
}