Programming the LED Matrix

Now that I’ve got the LED matrix wired up and working it’s time to share the code I used to make it all work. It’s really quite simple and doesn’t take much code at all. That’s the beauty of using an LED controller over trying to directly control them manually with shift registers or I/O expanders.

Setup a Pattern

The first thing to do is to set up a pattern to display. This will be a way for you to test all your LEDs and ensure you have them all wired in the correct order. I created a simple LEDPattern.h file to define a couple constant arrays. The const keyword tells the compiler that this data is a constant and will not change, with that knowledge the compiler knows to place the lr_ptn variable in program memory rather than into RAM. On most microcontrollers, and even PCs, there is typically far more storage available than RAM.

#ifndef LEDPattern_h
#define LEDPattern_h

#include <stdint.h>

/* const uint8_t lr_ptn[9][5] = {{0x40,0x40,0x40,0x40,0x00},
        {0x20,0x20,0x20,0x20,0x00},
        {0x10,0x10,0x10,0x10,0x00},
        {0x08,0x08,0x08,0x08,0x00},
        {0x04,0x04,0x04,0x04,0x00},
        {0x02,0x02,0x02,0x02,0x00},
        {0x01,0x01,0x01,0x01,0x00},
        {0x00,0x00,0x00,0x00,0xF0},
        {0x00,0x00,0x00,0x00,0x0F}}; */
                
const uint8_t lr_ptn[9][5] = {{0x7F,0x7F,0x7F,0x7F,0xFF},
           {0x00,0x00,0x00,0x00,0x00},
           {0x7F,0x7F,0x00,0x00,0x00},
           {0x00,0x00,0x7F,0x7F,0x00},
           {0x00,0x00,0x00,0x00,0xFF},
           {0x00,0x00,0x00,0x00,0x00},
           {0x00,0x00,0x00,0x00,0xFF},
           {0x00,0x00,0x7F,0x7F,0x00},
           {0x7F,0x7F,0x00,0x00,0x00}};

#endif

 

You might be wondering what the #ifndef and #define statements are at the very top of this header file. It’s basically saying if LED Pattern_h is not defined, do everything until the #endif at the end of the file. The purpose is just to ensure the file only ever gets included a single time by the compiler. If it somehow got included previously, that if statement would be “false” and it wouldn’t include it again.

Initialization

Like all peripherals, your first step in the code is to initialize them and set up your microprocessor to use them.

#include <stdint.h>
#include <i2c_t3.h>
#include <Serial.h>
#include "LEDPattern.h"

// I2C Addresses
#define LED_ADR 0x38

void setup() {
  // Start serial port
  Serial.begin(115200);
  while(!Serial);
  Serial.println("Serial port has been opened!");
  
  // Start I2C
  // I2C Master, Bus 0, External Pullups, 400 kHz bus speed
  Wire.begin(I2C_MASTER, 0x00, I2C_PINS_18_19, I2C_PULLUP_EXT, 400000);
  Wire.setDefaultTimeout(40000);
  Serial.println("I2C Bus Initialized!");
  
  // Initialize the LED Matrix
  // Minimum intensity, Scan All Digits
  // Enable Display, IRQ pin set to segment driver
  const char led_init[] = {0x01,0x00,0x00,0x03,0x01};
  Wire.beginTransmission(LED_ADR);
  Wire.write(led_init,sizeof(led_init));
  Wire.endTransmission();
  Serial.println("LED Matrix Configured!");
}

Libraries

We start with including all the libraries we’ll need. First on the list is stdint.h which contains the definitions of fixed length variables like uint8_t. I like to use these over things like char and int because then I know exactly how many bits my variables are and the compiler can optimize at will. There is no sense wasting 32-bits, which is what an int is on the Teensy 3.6, on something dumb like a counter that only goes to 4.

Next is the Teensy 3.6 replacement for the Arduino Wire library. The I2c_t3.h library is optimized and written to take advantage of the advanced features the Teensy 3.6 has, for example, multiple I2C busses, faster clock speeds, and interrupts. The use of interrupts will become very important for my pinball project. With the Arduino Wire library when you send data over the I2C bus, or request data from a slave, the processor will just sit there twiddling its thumbs while it waits for the transmission to finish. With interrupts, you’re actually loading up a buffer (or reading into a buffer) and you can go off and do other things while that processes in the background. This is called a non-blocking function. Since this example is only using the LED matrix I haven’t implemented the non-blocking routines but you’ll see that once I start integrating all the building blocks together.

Last of all I include my LED pattern I created above. You’ll notice it’s included with quotes instead of between the less than/greater than symbols like the others. The quotes simply mean the file is not a built-in library and tells the compiler to look for it in your project drive rather than the libraries folder. I also define the I2C address for the MAX6958A which is 0x38. The MAX6958 comes in two varieties, the A or B version. The only difference is the I2C address, which means you could have one of each on the same I2C bus. It would have been better for Maxim to have external inputs to set addresses rather than separate part numbers, but they probably wanted to keep the pin count as low as possible.

Setup

In the setup loop, I start with opening up a serial port to send out debug messages. You’ll see this in every example as the Arduino environment doesn’t really have any other debugging interface. The second block of code initializes the I2C bus. I’m using bus 0, the Teensy 3.6 will be a master on the bus, and it will operate at a 400 kHz frequency which is the maximum supported by the MAX6958. I also tell it to use external pull up resistors. For I2C you need pretty beefy pullups since the bus isn’t actively driven high by the ICs. As you saw in my prototype LED matrix post, I have 2.2kΩ pullups. Typically you’ll see people use 4.7kΩ resistors but on my breadboard which has lots of capacitance, I went a little higher.

MAX6958 Register Map
MAX6958 Register Map

The last block in the setup function sends out commands to the MAX6958 to configure its registers and get it ready to accept display data. The first byte sent is always the address that you want to write to, and this will auto-increment from that point if you write more than one byte. As noted in the comments I set it to use minimum intensity (so I don’t blind myself), to enable the display, set the IRQ pin as a segment driver, and to scan all four digits.

Main Loop

The main loop code is really small, really simple, and very straightforward.

void loop() {
  // Cycle Through LED Pattern
  for(uint8_t i = 0;i<9;i++){
    Wire.beginTransmission(LED_ADR);
    Wire.write(0x20);	// Segment data starts at address 0x20
    for(uint8_t k = 0;k<5;k++){
      Wire.write(lr_ptn[i][k]);
    }
    Wire.endTransmission()
    delay(200);
  }
}

Since our LEDPattern.h file defines the constant patterns in a 2D array we’ll need a pair of nested for loops to cycle through them. In this case, the i loop cycles through the rows and the k loop cycles through each column which is the digit/segment data. So, for each row we need to start an I2C transaction, send the register address for where the segment data starts (0x20), loop through the 5 columns of digit/segment data adding each to the TX buffer, then finally sending it out and ending the I2C transaction. Then I just have a 200 ms delay in there to slow it down enough our slow human eyes can see.

And that’s it, that’s all it takes to update the LED Matrix. This chip handles all of the charlieplexing for us, allowing our processor to go off and do far more important tasks for our game.

Demonstration

Here is a short video showing the LED matrix in action. You may be wondering why there are three different looking LEDs that I’m using. I bought three different viewing angles to test out which will show up best through inserts. The narrower the viewing angle, the brighter they are, but the more focused the light is. That testing will have to wait until I have inserts to test with.

Leave a Reply

Your email address will not be published. Required fields are marked *