Replica prop with multiplexed LED array

This is a circuit I built for a replica of a PKE meter from the cartoon Extreme Ghostbusters.

The array has eight 10-segment bargraphs, of which 8 leds in each are used. One bargraph is lit at a time in this setup, so you can think of each bargraph as a row in your typical multiplexing setup.

The array is updated at 70hz, and as you may notice some of the leds are brighter than others. That's because I'm not simply multiplexing the array. I'm also doing a form of pulse width modulation across the entire array.

At 70hz, each column is lit for 1/640th of a second. That's 1792 microseconds, which divided by 256, is approximately 7 microseconds. Multipying 7 microseconds by the brightness level I want then gives me the length of time each led should be on during that 1792 mirosecond period in which a column may be lit.

In order to update all of this fast enough, I had to write my own slimmed down version of the digitalwrite function. Then, before updating each bargraph I put the time at which each of the 8 leds needs to turn on into an array and sort them from shortest to longest, so I don't have to loop through all of them checking to see if it is their time to shine.

I turn them on at the end of their cycle rather than the beginning because I found it gave better results as far as getting a consitent brightness with the dimmest leds. However, as you can see above, there is a bit of a gradient on each bargraph at the lowest brightness level.

That's because I made a mistake when designing the cicuit, and thought I was going to be able to put 160mA through the array. It turns out, the number was closer to 50mA, which was way less than I needed. I managed to bypas the Pro Mini's voltage regulato with minimal modifications to the board, but I then hit the limit on what the AtMega could handle, and in the end, even after shaving off a few mA here and there, I still only managed to get 100mA through the display. Which was enough to light it nicely, but not enough to get really good reproduction of 256 different brightness levels.

However, it turned out that the lowest brightness level created a nice color gradient due to how I update only one led in the bargraph each time through my loop that checks when it's time for the leds to turn on, and that looked cool enough, even if it wasn't entirely accurate and you couldn't really see the fade-in as well as I'd hoped.

Oh well, lesson learned! I spent four months designing this with little to no experience desgining circuits previously, so I can be forgiven if I made one or two little oversights but still ended up with something that looks way better than the dude that paid for it thought he was gonna get. :slight_smile:

Oh, and if you'd like to take a look at the method I used to update the array, here's the code for that. Feel free to use it however you wish:

// LED array variables:

  const int rows = 8;                                   // Number of rows in array. 
  const int columns = 8;                                // Number of columns in array. (My array is arranged such that one whole column is illuminated at a time, rather than one whole row, as you might have in a standard array.)

  byte matrix[rows][columns];                           // Data for array.  Brightness of each led in array 0..255.   = {{1, 3, 7, 15, 31, 63, 127, 255},{0}}; 
  
  const int rowPin0 = 0;                                // First row pin.  
  const int colPin0 = 10;                               // First column pin. (Not used at this time.)
    
  const int hz = 70;                                    // Number of times per second display is updated.  Changing this value will not affect the rate at which the display is updated, it is simply a reflection of a 2048 microsecond per-column duty cycle.  61hz was default.
                                                      
  const int columnduty = 1792;                          // 1000000.0 / hz / columns - The duty cycle for each column.  The number of microseconds for which leds in it will be lit before we move onto the next column.  2048 = 61hz. 1792 = 70hz.  1536 = 81hz.  
  const float dutyscale = columnduty / 256.0;           // Compute factor by which brightness should be scaled by to bring the duty cycles for the leds in line with the duty cycle of each column.
  
  unsigned int dutycycle[256] = {0};                    // Duty cycle lookup table.  Index is LED brightness 0..255.  Data is duty cycle for LED in microseconds.

  float fade = 1.0;                                     // This is an overall scalar for the display brightness.  Setting it to 0 will render a completely dark display, setting it to 0.5 will set the display to 50% brightness.
  

// Sensor variables:
  
  const byte potPin = 5;                                 // Analog pin the potentiometer is on.
  float pot[1] = {0};                                    // Current value of potentiometer 0..1.


// VU meter bars:

  float barValue[4] = {0};                               // Current value of bars on display.
  const byte barMax = 16;                                // Maximum height for a bar.
  const byte barMinAverage = 2;                          // Minimum average bar position which can be set with potentiometer.  Setting this to greater than 0 allows the bars to move up and down a bit when speed at it's lowest value.
  const byte barMaxAverage = 16;                         // Maximum average bar position which can be set with potentiometer.  Setting this to less than 16 allows for more movement of bars when speed is maxxed out. (12 was originally default.)
  const float barVariance = 0.75;                        // Percentage which bar's value can deviate from average position defined by potentiometer setting. 
                                                         // Ie, if barMaxAverage is 12, and barVariance is 0.5, then when speed is maxxed out bars can range in value from 6..18.

// State data:
 
  int state = 0;
  unsigned long lastStateChange;                         // Time of last state change, in milliseconds.    

  int mode = 0;
  const int MODE1 = LOW;
  const int MODE2 = HIGH;
  
  int soundState = 0;                                     // Used by sound update function and wing update function.
  int beep = 0;  
  
// Timing:

   unsigned long time;
   unsigned long oldTime;
   unsigned long timeDelta;
   float timeDeltaSec;    


/* ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */

  void setup() {                
    
    for (int x = rowPin0; x < (rowPin0+8); x++) { // Set row pins to output mode.
      pinMode(x, OUTPUT);
    }
    
    for (int y = colPin0; y < (colPin0+8); y++) { // Set col pins to output mode.
      pinMode(y, OUTPUT);
    }
    
    pinMode(9, OUTPUT);
    
    //digitalHIGH(colPin0);
    
    for (int x = 0; x < 256; x++) { // Create lookup table to convert brightnesses to duty cycles. (Duty cycles are LOW period!)
      dutycycle[x] = columnduty - x*dutyscale; // Linear brightness. (127 does not look half as bright as 255.)
      //dutycycle[x] = columnduty - pow(x/255.0, 2)*255.0*dutyscale; // Logarithmic brightness. (127 looks as you would expect it to.)
    }  
    
  }
// These functions set pins faster than the digitalWrite() function, and record their current state in the pinState[] array.
// They do not check to make sure you have attempted to set a valid pin, nor do they make sure the pin is not set to PWM mode before attempting to set it.
 
#include "pins_arduino.h"

//byte pinState[20] = {0}; // The current state of each pin, if set with the digitalHIGH() and digitalLOW() functions.
 
void digitalHIGH(uint8_t pin)
{
        uint8_t bit = digitalPinToBitMask(pin);
      uint8_t port = digitalPinToPort(pin);
      volatile uint8_t *out;

      out = portOutputRegister(port);

      *out |= bit;

        //pinState[pin] = HIGH;
}  

void digitalLOW(uint8_t pin)
{
      uint8_t bit = digitalPinToBitMask(pin);
      uint8_t port = digitalPinToPort(pin);
      volatile uint8_t *out;

      out = portOutputRegister(port);

      *out &= ~bit;

        //pinState[pin] = LOW;
}
void updatematrix() {
  
  byte index;  
  unsigned long columnstart;
  int timeelapsed;
  int rowpin[rows];
  int rowduty[rows];
  int nextduty;
  
  for (int x = 0; x < columns; x++) { // Step through each column.

    // Turn on this column.
      digitalHIGH(colPin0+x);
      
    // if (x < 1) { // Do not update any columns after the first. *TEST CODE*

      // Illuminate column:
        
        // Store pin numbers and their coinciding duty cycles in two arrays:
          for (int y = 0; y < rows; y++) {          
            rowpin[y] = (rowPin0+(rows-1)) - y;
            rowduty[y] = dutycycle[int(matrix[x][y]*fade)]; 
          }

        //  Sort duty cycle list, keeping pin numbers synchronized with it. (Bubble sort)
        
          register int i, j, temp;
           
          for (i = (rows - 1); i >= 0; i--) {
            for (j = 1; j <= i; j++) {
              if (rowduty[j-1] > rowduty[j]) {
                temp = rowduty[j-1];
                rowduty[j-1] = rowduty[j];
                rowduty[j] = temp;
                temp = rowpin[j-1];
                rowpin[j-1] = rowpin[j];
                rowpin[j] = temp;
              }
            }
          }
     
        index = 0;   
        timeelapsed = 0; 
        nextduty = rowduty[index];
        columnstart = micros(); 
                
        while (timeelapsed < columnduty) { // Wait for this column's duty cycle to complete.
          while (timeelapsed >= nextduty) { // If timelapsed is greater than or equal to the time at which the next brightest led in this column should turn on...
            digitalHIGH(rowpin[index]);
            index++;
            if (index < rows) { nextduty = rowduty[index]; } else { nextduty = columnduty; }
          }   
          timeelapsed = micros()-columnstart; 
        } 

        
      // Blank column:
      
        for (int y = 0; y < rows; y++) {
          //if (pinState[rowPin0 + y] == HIGH) { 
            digitalLOW(rowPin0 + y);
          //}
        }

      // Turn off this column.
        digitalLOW(colPin0+x);
      
    //}
///    else {
//      delayMicroseconds(columnduty); // Removing this delay when testing would make the leds brighter when fewer are lit, and could burn them out if we are pulsing them at higher than their rated current.
//    }  
    
  }
  
}