Before dedicating myself to the task of soldering 64 LEDs to make my 4x4x4 LED cube, I decided to build a life counter for the card game Magic: the Gathering. Basically, it is a two-digit 7-segment display that will begin by displaying 20 (the amount of "life" that you start with in the game). There will be two buttons that allow you to increment or decrement the counter as the game progresses.

Parts list:

Building the circuit from scratch

I wired it up according to the schematic in the Max 7219 datasheet.

I had to use two 4.7kΩ resitors in series rather than a single 9.53kΩ resistor.

Unfortunately, both of the Max7219 libraries that I tried for the Arduino failed to work properly. I was able to get one of them to flash some digits, but only by wiring them wrong. I don't know if maybe these libraries are for a specific model of Arduino (with a different clock speed) or if my ICs are bad.

I had to go lower level and write code against the SPI library to drive the Max7219. I wrote the code to keep up with player one's life total. When the buttons are pressed, it will increment and decrement the life total and then call updatePlayerOneLife() and display it on the two LED digits that I have wired up. I use the modulo operator to divide up the digits. I had to refer to the Max7219 datasheet to know which address and data value to use with the SPI library. I've done this before using my Mojo FPGA. Using the Arduino SPI library made it a little bit easier than the FPGA implementation. I wired the Max7219 SPI pins to the SPI pins on the Arduino.

I'm using an Arduino Pro Micro. Here's my pinout diagram:

Arduino Pro Micro Pinout Diagram

Pin 12 on the Max7219 is connected to Pin 6 on the Arduino; pin 13 on the Max7219 is connected to pin 15 (SCLK); pin 1 on the Max7219 is connected to pin 16 on the Arduino (MOSI).

Coding Session

Here is the source code for the magic life counter (it only covers a single player for now, but adding an additional player is an easy exercise).

/*
Copyright (c) 2018 Michael Earls (https://cerkit.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
 */

#include <SPI.h>
const int slaveSelectPin = 6;
// define 0-9 for the LED segments
const int digits[] = { 0x7e, 0x30, 0x6d, 0x79, 0x33, 0x5b, 0x5f, 0x70, 0x7f, 0x7b };

const int playerOneIncrementButtonPin = 2;
const int playerOneDecrementButtonPin = 3;

int playerOneIncrementButtonState = 0;
int playerOneDecrementButtonState = 0;

int playerOneLife = 20;
int playerTwoLife = 20;

void setup() {  
  // set the slaveSelectPin as an output:
  pinMode(slaveSelectPin, OUTPUT);
  pinMode(playerOneIncrementButtonPin, INPUT);
  pinMode(playerOneDecrementButtonPin, INPUT);
  
  digitalWrite(slaveSelectPin, HIGH);
  SPI.begin();
  initializeMax7219();
  updatePlayerOneLife();
}

void loop() {
  playerOneIncrementButtonState = digitalRead(playerOneIncrementButtonPin);
  playerOneDecrementButtonState = digitalRead(playerOneDecrementButtonPin);

  if(playerOneIncrementButtonState == HIGH)
  {
    playerOneLife += 1;
    updatePlayerOneLife();
    delay(500);
  }

  if(playerOneDecrementButtonState == HIGH)
  {
    playerOneLife -= 1;
    updatePlayerOneLife();
    delay(500);
  }
}

void initializeMax7219()
{
  // turn off test mode
  sendMessage(0x0F, 0);
  // set operation mode to normal
  sendMessage(0x0C, 1); 
  // set scan limit (number of digits)
  sendMessage(0x0B, 0x01); // use two digits
  // set brightness to 100%
  sendMessage(0x0A, 0x0F);
  // set no decode mode
  sendMessage(0x09, 0);
}

void updatePlayerOneLife()
{
  // get our individual digit values
  int ones = playerOneLife % 10;
  int tens = playerOneLife / 10  % 10;

  // update the first digit with our ones place value
  sendMessage(0x01, digits[ones]);

  // update our second digit with our tens place value
  sendMessage(0x02, digits[tens]);
}

void sendMessage(int address, int data)
{
  digitalWrite(slaveSelectPin, LOW);
  //  send in the address and value via SPI:
  SPI.transfer(address);
  SPI.transfer(data);
  // take the SS pin high to de-select the chip:
  digitalWrite(slaveSelectPin, HIGH);
}

Instead of using SPI.beginTransaction(), I simply used SPI.begin(). It worked just fine. I got this idea from the SPI digital pot example in the Arduino IDE.

My breadboard configuration. Only one LED display is connected to the Max7219