Bits and Droids logo
Mail icon

Radio with a MAX7219, 8 digit, 7 segment display

3/30/2024 | by Bits and Droids

Sometimes, the TM1637 6 digit 7 segment displays can be hard to source locally. Luckily, we have many options, including the MAX7219 LED driver chip. This chip can achieve the same results in a different package. The code involved isn't hard, but it enables us to use more advanced tricks and techniques. I'll try to break it down, and hopefully, you'll understand how the code moves after this article. We will display our data using functions, loops, and basic algorithms. Now, don't be daunted. An algorithm sounds intimidating but is nothing more than a chain of logical steps to solve a problem.

Components

Libraries used:

Bits and Droids flight connector

https://www.bitsanddroids.com/downloads

LedControl

https://github.com/wayoda/LedControl

Parts used:

these are affiliate links that help support this content.

Wiring

The MAX7219 displays have a slightly different pinout than the screens we've used. They don't utilize the I2C protocol but rather use the SPI interface of the Arduino. SPI is a communication protocol that allows us only to use 3 pins to drive our display. Looking at the back of your 7-segment display, you notice 24 solder joints. If we drive the displays without any chip, we'd need all 24 pins. By utilizing the SPI interface, we save ourselves a whopping 21 pins. Another neat feature is that we can daisy chain the displays after each other. This will save us 21 + 24 pins (45 pins total).

I won't fully detail how this protocol works, but we usually need certain dedicated SPI pins to fully utilize the interface. It's important to note that the LedControl library does not utilize this feature. In short, we can use any 3 pins we desire on our Arduino. The downside is that we lose some speed that we'd normally gain by using an approach that uses the interface to its fullest.

In our example, we've hooked up the display to pins 12 (DATA-IN or DIN), 11 (CLK), and 10 (LOAD or CS). The displays usually have 2 sides. 1 side has a DIN pin, while the other has a DOUT pin. To keep things simple, DIN is for incoming data. A good example would be the data coming from our Arduino towards our display. The DOUT is for outgoing data. In our case, we use this pin to send data to our second screen. The DOUT of the first screen is hooked up to the DIN pin of the second screen. We could match the first pin of the first screen to go to the first pin of the second screen and the second to the second, etcetera.

7 segment display pins
7 segment display pinout

Soldering

It's not necessary to solder anything in this project. It's optional, but instead of wiring each screen to the 5V and ground line separately, you can pass the current from the first screen to the second. We need to remove the diode located at the D1 mark and bridge the traces with some wire or solder.

The contacts to bridge

Multiple LedControl instances in our code can also be created using 3 pins, each per display. For poopies and giggles, I'm going to show the daisy chain method and the code involved in this approach.

The full code

Let's first take a look at the full code. It may seem daunting, but we will go through it step by step afterward.

cpp
#include<BitsAndDroidsFlightConnector.h> #include<LedControl.h> BitsAndDroidsFlightConnector connector = new BitsAndDroidsFlightConnector(); LedControl segmentDisplay = LedControl(12,11,10,2); int bufferArray[6]; long activeCom1 = 666666L; long oldActiveCom1= 0L; long standbyCom1 = 666666L; long oldStandbyCom1= 0L; void printNumbers(long numberToPrint, int screen){ /* Our frequency has 6 digits there we loop through this code block as long as: i (our counter) is smaller than 6. This is because an array starts counting at 0, in our case frequency = 6 digits in our array they would be stored in 0, 1, 2, 3, 4, 5. Therefor < 6 */ for (int i = 0; i < 6; i++) { bufferArray[i] = numberToPrint % 10; if(i == 3){ segmentDisplay.setDigit(screen, i, bufferArray[i], true); } else{ segmentDisplay.setDigit(screen, i, bufferArray[i], false); } numberToPrint /= 10; } /*Divide the numberToPrint to ensure the next loop through this block checks the next tenfold. In this example 123456 / 10 leaves us with 12345 (since it's an int the decimal gets ommited). The next loop we encounter 12345 / 10 = 1234, etc. etc. By calculating the remainder we can loop through each character per loop*/ } void setup() { //We start our Serial connection Serial.begin(115200); //How long the Arduino waits before moving on to the next incomming value Serial.setTimeout(15); //This block enables our screen, segmentDisplay.shutdown(0,false); //Sets the intensity to 8(a medium brightness) segmentDisplay.setIntensity(0,4); //And clear any possible characters that are displayed at the start segmentDisplay.clearDisplay(0); //repeat the block above for the second screen segmentDisplay.shutdown(1,false); segmentDisplay.setIntensity(1,4); segmentDisplay.clearDisplay(1); } void loop() { connector.dataHandling(); activeCom1 = connector.getActiveCom1(); //Check if the incoming frequency differs from the current one displayed if(activeCom1 != oldActiveCom1){ //call our function passing the number to display and on which screen we want to display it (0 is screen 1 and 1 is screen 2) printNumbers(activeCom1, 0); oldActiveCom1 = activeCom1; } standbyCom1 = connector.getStandbyCom1(); if(standbyCom1 != oldStandbyCom1){ printNumbers(standbyCom1, 1); oldStandbyCom1 = standbyCom1; } }

I like to relate computer logic to real-world objects to understand what is happening fully. In our case, the 7-segment displays have their own Max7219 chip that drives the LEDs. The chip can address each LED individually. The easiest way to visualize this would be a row of drawers in a cupboard. Each drawer contains a single item. We have 2 cupboards with 8 drawers each (2, 7 segment modules containing 8 digits each). To change a certain number, we must select which cupboard to open and which drawer to open. A number can reference our cupboards and drawers. Or, in our case, our digits can be referenced by a number, just like the screen itself. Computers start counting at 0, so our first screen would be 0 while our second is 1. The same applies to the numbers. We have 8 digits per screen. Starting at 0, it will give us a range of 0 - 7. If we want to alter the first value of the second screen, we need to address 1,0 (1 for the second screen and 0 for the first character).

7 segment layout

LedControl library

We could write the logic from scratch, but the LedControl library is widely used and easily implemented. We start by including the library at the top of the file. While we are there, we will include the Bits and Droids library. For both instances, we need to create an object. Since we use 2 MAX7219 chips, it's important to tell the library this when creating a new instance.

cpp
#include <BitsAndDroidsFlightConnector.h> #include <LedControl.h> /*the parameters required for the LedControl library are the 3 pins and the amount of 7219 chips in our circuit. We daisy chaned 2 displays granting us 2 chips in our circuit*/ LedControl displays = new LedControl(12,11,10,2); //Here we just create a new connector instance; head over to the docs to learn more BitsAndDroidsFlightConnector connector(false);

Ints and Longs

By default, an Int is a 16-bit variable on our Arduino. A 16-bit byte can hold a value ranging from -32768 to 32767. Our frequency usually contains 6 numbers. 100.000 would be the lowest possible, containing 6 digits, meaning that it would be too big for an Int. If we define our int as a long int, we double the bits to 32, giving us a range of -2,147,483,648 to 2,147,483,647 (this will fit just fine for our frequency). It's not advised to convert every int too long, but this could be a workable solution sometimes. It's good only to choose this when the situation calls for it.

Displaying data on our 7-segment display can be quite resource-intensive. It's advisable only to update the screen when the data has changed. To achieve this, we create 2 variables, a new and old frequency. We can then check if the new frequency is different from the old. Only if that is the case can we update the display. The variables will remain, and only the value will change. That is why we add them to the top of our file underneath the 2 library objects.

Here, we also create our first drawer system, which is called an array in code. An Array is a container that holds a certain type of data. In our case, we want to store an array of bytes (the smallest size possible). It's also important to define the size of the array. In our case, it will hold the frequency with a single number in each drawer.

cpp
//#includ<..... //... //..connector(false) //------------------------- long activeCom1 = 666666L; long oldActiveCom1= 0L; long standbyCom1 = 666666L; long oldStandbyCom1= 0L; int bufferArray[6];

The setup

In our setup block, we mainly initiate a Serial line and boot up our displays.

cpp
void setup() { //We start our Serial connection Serial.begin(115200); //How long the Arduino waits before moving on to the next incomming value Serial.setTimeout(15); //This block enables our screen, segmentDisplay.shutdown(0,false); //Sets the intensity to 8(a medium brightness) segmentDisplay.setIntensity(0,4); //And clear any possible characters that are displayed at the start segmentDisplay.clearDisplay(0); /*repeat the block above for the second screen Our first display was number 0 while this display is number 1*/ segmentDisplay.shutdown(1,false); segmentDisplay.setIntensity(1,4); segmentDisplay.clearDisplay(1); }

Functions and our algorithm

This will be the slightly harder part: adding our first simple algorithm to the code. We first need a game plan. The LedControl library wants us to specify where we want to put each character. We can't just pass the number 100. The number 100 needs to be broken down into single digits. Number 1 goes to screen 0 digit 0, number 2 to screen 0 digits 1, and number 0 to screen 0 digits 2.

Another factor is the'.'. After 3 characters, we want to display a '.' to represent the separation between Mhz and Khz.

It's wise to create a function because we want to apply the same steps each time we update the screen. A function is a block of code that we can call upon, which executes each time we call his name. Our setup block and loop block are also functions. The setup only gets called once, while the loop block gets executed constantly. In our loop block, we want to implement a check telling us if the incoming frequency differs from the current frequency. Suppose that is the case; we want to call upon our function to handle the logic of splitting our numbers and displaying them on the screen of our liking. Our function, therefore, requires 2 parameters: the number to display and the screen where we want to display it.

cpp
void printNumbers(long numberToPrint, int screen){ /* Our frequency has 6 digits there we loop through this code block as long as: i (our counter) is smaller than 6. This is because an array starts counting at 0, in our case frequency = 6 digits in our array they would be stored in 0, 1, 2, 3, 4, 5. Therefor < 6 */ for (int i = 0; i < 6; i++) { bufferArray[i] = numberToPrint % 10; if(i == 3){ segmentDisplay.setDigit(screen, i, bufferArray[i], true); } else{ segmentDisplay.setDigit(screen, i, bufferArray[i], false); } numberToPrint /= 10; } /*Divide the numberToPrint to ensure the next loop through this block checks the next tenfold. In this example, 123456 / 10 leaves us with 12345 (since it's an int the decimal gets omitted). The next loop we encounter 12345 / 10 = 1234, etc. etc. By calculating the remainder we can loop through each character per loop*/ }

% Remainder

The remainder (%) is a mathematical operation that returns the remainder when we divide a value by another value. 100 % 10 will return 0. 101 % 10 will return the value 1 (10 can fit 10 times into 100; 101 - 10 times 10 leaves us with 1; in this example, 1 is the remainder). If we take our long 6-digit number to apply % 10, we can isolate the last digit. 124.850 % 10 = 0. The next step would be to divide the new value by 10. 124.850 / 10 = 12485. Again, we can apply the remainder, which returns the value 5 (12485 % 10 = 5). If we repeat these steps for each digit, we can split our 6-digit value into 6 single digits without too much effort. We store each of our single digits in the Array, and we have a handy dandy cupboard that contains each of our numbers.

123450% 10012345% 1051234% 104123% 10312% 102

Remainder example

Some food for thought

The buffer is overkill for those of you who have a keen eye. Each time we calculate our remainder, we store it in our array. We then immediately print that number to our screen before calculating the next. We could theoretically print the digit and obliterate the buffer.

I've also manually set each value to 6. But what would happen if we want to use all 8 digits? Do we need to copy our code? If we fine-tune our code, we'd probably include a variable at the top of the file determining the number of characters to process. This way, we could change the value once, and our logic would handle the rest. One of the benefits of using a function is that our code becomes very modular. We can pass different values to the same function without retaking the code.