Monday, 27 March 2017

Recreating MAX7219 functionality with an Arduino

In recent weeks a few of our older projects have been resurrected and we've had a few emailed questions about them. Number one tends to be "you built this with a PIC can you send me the Arduino code?"

The short answer is "no". The longer answer is "well, maybe, one day, when one of us needs something similar for some project".

The latest project  to garner interest is our "dartsboard scorer" using some massive 7-segment LEDs. It's been in a box for the last six months, but having recently got the workshop bungalow into some kind of useable state, I thought I might set up the dartboard and get it out again.

One thing that always bugged me (and everyone else who used it if I'm honest) is that the brightness of the LEDs fluctuates depending on the number of segments lit up. It's all because we couldn't use our MAX7219 chips as the supply voltage needs to be in the region of 9v-12v and the LEDs are common anode types (the max7219 chips work best with common cathode displays).

So I figured it's time we put the display right. And in doing so, maybe answering a couple of questions about the darts scorer - mostly "can you do it on an Arduino?" So here goes - we're going to be multiplexing the segments of the display(s) so that each segment draws the same amount of current and stays lit for the same duration; in theory that should make each segment appear with the same brightness.



As before, we're using a ULN2803A sink array. Each 7-segment display will get it's own (Arduino) controller chip (bare ATMega328 chips) and we'll tell it which number to display by sending data to it over a simple three-wire SPI/I2C connector.

We'll store the value we want to display in a variable. Then create a "pattern" to display on the 7-segment LED. So if we wanted to show the value 4:
We'd want to light up segments 2, 3, 6 and 7.
So our pattern (reading right-to-left) would be 01100110.
Similarly to display the number 7 we'd light up 1,2,3 and 6.
The pattern for number seven would be 00100111.

So in the main loop of our code, we'll look at bits 0-7 of our pattern variable and illuminate the appropriate LED segment (or not as necessary). All other segments will be turned off - so only one segment is lit at any one time and all active segments are illuminated for the same duration.


int k=2;
int last_k=2;
int mask_pattern = B00001101;
int value_to_display=0;

int byte_received=0;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;

int last_cs=1;
int last_data=1;
int last_clk=1;
int byte_buffer=0;
int bits_received=0;

int a;
int b;
int c;
int d;


void setMaskPattern(int p){
   switch(p){
      case 0:
      mask_pattern = B00111111; break;

      case 1:
      mask_pattern = B00000110; break;

      case 2:
      mask_pattern = B01011011; break;
                           
      case 3:
      mask_pattern = B01001111; break;

      case 4:
      mask_pattern = B01100110; break;

      case 5:
      mask_pattern = B01101101; break;

      case 6:
      mask_pattern = B01111101; break;

      case 7:
      mask_pattern = B00100111; break;

      case 8:
      mask_pattern = B01111111; break;

      case 9:
      mask_pattern = B01101111; break;

      case 99:
      mask_pattern = 0x00; break;
      
   }

}

void clearBuffer(){
   byte_buffer=0;
   bits_received=0;
}

void setup() {
   for(int i=2; i<=8; i++){
      pinMode(i,OUTPUT);
      digitalWrite(i,LOW);
   }

   pinMode(spi_cs,INPUT_PULLUP);
   pinMode(spi_data,INPUT_PULLUP);
   pinMode(spi_clk,INPUT_PULLUP);

   clearBuffer();
   setMaskPattern(99);
   
}

void loop() {
   
   
   // --------------------------------------------------
   // multiplex the LEDs as fast as we can
   // (up to 3ms is ok, 5ms creates a visible flicker)
   // --------------------------------------------------
   digitalWrite(last_k,LOW);
   k=k+1;
   if(k>8){ k=2;}

   b=1 << (k-2);
   if(mask_pattern & b){
      digitalWrite(k,HIGH);
   }      
   last_k=k;
   

   // ---------------------------------------------
   // if we've received any data on the SPI bus,
   // update the number to display
   // ---------------------------------------------
   b=digitalRead(spi_cs);
   if(b==LOW){
      if(last_cs!=LOW){
         // this is a falling edge - prepare the data values      
         clearBuffer();
      }else{

         // monitor the CLK line
         a=digitalRead(spi_clk);
         if(a==LOW){
            
            if(last_clk!=LOW){
               // this is a falling edge, get the data
               c=digitalRead(spi_data);
               if(c==HIGH){               
                  d = 1;
                  d = d << bits_received;
                  byte_buffer = byte_buffer + d;
               }
               bits_received++;
               
            }                        
         }
         last_clk=a;
         
      }
      
   }else{

       if(last_cs==LOW){
         // this is releasing the CS line, so put the value
         // from the buffer onto the display
               
         if(bits_received > 3){      
            value_to_display = byte_buffer;
            if(value_to_display > 9){ value_to_display=99;}
            setMaskPattern(value_to_display);
         }
      }
      
   }

   last_cs=b;
   
}

We've also got a bit of "pin polling" going on, looking for data coming in over I2C on pins 8,9 and 10. So when our CS line goes low, we reset everything, read some incoming data and when CS drifts high, choose a new pattern to make different segments of the LED display to light up. Of course this could (should?) be put onto an interrupt, but as the controller has nothing else to do, it won't hurt to poll the pins.




Which means we need a "controller" to send data to the display - the following code simply increments a counter from zero through to ten and displays the appropriate digit on the 7-segment LED.


int byte_to_send;
int spi_cs=10;
int spi_data=11;
int spi_clk=12;

int mask=0;
int k;

void sendByte(int byte_value){
// we assert the CS line (pull it low) to tell
// the target to start listening - but first need
// to make sure that the clock line is also idle

digitalWrite(spi_clk,HIGH);
digitalWrite(spi_cs,LOW);

// give it a moment
delay(1);

// now we set the data pin to indicate each bit
// in the value we want to send
for(int i=0; i<8; i++){
   mask = 1;
   mask = mask << i;
   k = byte_value & mask;
   if(k==0){
    digitalWrite(spi_data,LOW);   
   }else{
    digitalWrite(spi_data,HIGH);   
   }

   // give it a moment
   delay(1);
   
   // drive the clock line low
   digitalWrite(spi_clk,LOW);

   // give it a moment
   delay(2);

   // return the clock line to idle
   digitalWrite(spi_clk,HIGH);
   
}

// release the CS line (send it high)
digitalWrite(spi_cs,HIGH);

}

void setup() {
// put your setup code here, to run once:
pinMode(spi_cs,OUTPUT);
pinMode(spi_data,OUTPUT);
pinMode(spi_clk,OUTPUT);

// we're using pull-ups on the other end
// so everything should idle high
digitalWrite(spi_cs,HIGH);
digitalWrite(spi_data,HIGH);
digitalWrite(spi_clk,HIGH);

}

void loop() {
// put your main code here, to run repeatedly:

byte_to_send++;
if(byte_to_send > 10){ byte_to_send=0;}
sendByte(byte_to_send);
delay(2000);

}


The result looks something like this:



Now obviously this isn't the full code for our updated Darts Scorer, but should be enough to get your own project off the ground - swapping out a MAX7219 with an atmega328 and a ULN2803A transistor array (if you don't use the decimal point you can get away with a ULN2003).