In this tutorial you will learn how to control Parola animations on a MAX7219 LED Matrix Display. We are going to chain animations, use buttons to switch animations and potentiometers to adjust the brightness and the speed of animations.
The MD_Parola library is a popular library that makes the programming of animations on a display, such as a scrolling text very easy. While single animations are straight-forward to implement, you might find it more difficult to chain or control multiple animation. Also, often we want to read sensor data without interrupting the animation. This tutorial will show you how to do this.
Required Parts
I used an Arduino Uno for this project but any other Arduino board, or an ESP8266/ESP32 board will work just as well. Also here we are using a MAX7219 LED Matrix Display composed of 4 modules. But the code and wiring for Displays with more or less modules is essentially the same.
MAX7219 LED Matrix Display
Arduino Uno
Dupont Wire Set
Breadboard
USB Cable for Arduino UNO
Push Button
Potentiometer 10KΩ
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Connecting MAX7219 LED Display with Arduino
In this tutorial we will use a MAX7219 LED Matrix Display to display animations. The MAX7219 LED display driver communicates with the Arduino through SPI (Serial Peripheral Interface). So, the first thing to do is to connect the Display to the Arduino using the SPI interface.
Wiring
The following diagram shows you the required wiring between the MAX7219 Display and an Arduino Uno. Make sure you are connecting to the input (DIN) and not the output side (DOUT) of the display.
Start by connecting the SPI interface pins: Pin 11 of the Arduino is connected to the DIN pin of the Display. Then connect Pin 3 to CS and Pin 13 to CLK. Finally, connect 5V to VCC and GND to GND. The following table shows the connections again
Display | Arduino |
---|---|
VCC | 5 V |
GND | GND |
DIN | 11 (MOSI) |
CS | 3 (SS) |
CLK | 13 (SCK) |
The pins chosen above are for hardware SPI, which is faster than software SPI but requires you to use specific pins that differ between board. For more details see the MAX7219 LED dot matrix display Arduino tutorial.
Next let’s write and run some test code to ensure that the connections are correct.
Install MD_Parola and MD_MAX72XX libraries
First, we need to install the MD_MAX72XX and the MD_Parola library. You can install these libraries via the Library Manager of the Arduino IDE. Go to Tools > Manage Libraries
which opens the Library Manager. Search for “MD_MAX72XX” and the relevant libraries will be listed. In the screenshot below you can see that I have already installed the two libraries (marked yellow):
The MD_MAX72XX library is essentially the driver software for the MAX7219 LED Display. And the MD_Parola library uses it to create animations like scrolling and sprite text effects. If you want to learn more about available animations have a look at the MAX7219 LED dot matrix display Arduino tutorial.
Test Code
The following code is a minimal example to test the wiring and function of the display. It simply shows the scrolling text “Test” on the display.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); disp.displayText("Test", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); } void loop() { if (disp.displayAnimate()) { disp.displayReset(); } }
The code starts by including the required libraries. The SPI
library is available by default, and you installed the MD_Parola
and the MD_MAX72xx
library above.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h"
Next we need to define some constants need to create the display object (disp
). Namely, we need to define the type of display, how many modules the display has and which Arduino pin is connected to the Chip Select (CS
) signal.
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
If you have a display with more or less modules then you will need to set MAX_DEVICES
accordingly. Also note that we are using hardware SPI here. For software SPI you need to define the pins used. Have a look at the MAX7219 LED dot matrix display Arduino tutorial for details.
In the setup
function, the code prepares the display, sets the brightness and clears the display. In the last step, we define an animation that scrolls the text “Test
” from left to right with a speed of 100, that is a delay of 100msec between each animation step. So lower is actually faster.
void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); disp.displayText("Test", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); }
The main loop
continuously runs the animation by calling displayAnimate()
. If this function returns true, the animation is complete and display resets for another round. Which means the scrolling of the text starts again.
void loop() { if (disp.displayAnimate()) { disp.displayReset(); } }
If you upload and run this code you should see the text “Test” scrolling from left to right on your MAX7219 Display:
If that works then congratulations! We are ready for more animations.
Chain Animations
A common question is how to build a chain or sequence of animations that are executed after each other. For example, let’s say we want to scroll the text “Left” to the left and after that scroll the text “Right” to the right and the repeat the cycle.
However, we cannot interrupt the animation loop or easily chain two loops after each other. Somehow, we have to switch animations within the loop.
void loop() { if (disp.displayAnimate()) { disp.displayReset(); } }
The following code shows you how this can be achieved:
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void switchAnimation(int animationId) { switch(animationId) { case 0: disp.displayText("Left", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); switchAnimation(0); } void loop() { static int animationId = 0; if (disp.displayAnimate()) { disp.displayReset(); switchAnimation(++animationId % 2); } }
Let’s have a closer look at this code. The include statements, the constant definitions and the creation of the MD_Parola display are the same as before.
switchAnimation function
What is new is the switchAnimation()
function. It takes the id
of an animation and sets either the scrolling left or scrolling right animation. Note that switchAnimation()
does not execute the animation. It only tells the display, which animation to execute. The actual execution of the animation is performed by displayAnimate()
in the main loop.
void switchAnimation(int animationId) { switch(animationId) { case 0: disp.displayText("Left", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } }
setup function
The setup function remains largely unchanged. As before it prepares the display, sets the brightness and clears the display. In the last step, however, we call switchAnimation(0), which set the animation with id=0. In this case, this is the scrolling of the text “Left” animation.
loop function
In the loop function we define the variable animationId
to keep track of the current animation. It is static
, which means its value is maintained during loops.
void loop() { static int animationId = 0; if (disp.displayAnimate()) { disp.displayReset(); switchAnimation(++animationId % 2); } }
Within the loop, the call of displayAnimate()
executes the current animation. When an animation is complete (text is completely scrolled), this function returns true and we enter the body of the if
-statement. There we perform a reset and then switch to the next animation.
The expression ++animationId % 2
, increments the animation id until it becomes equal to 2 and in then sets it back to 0. If you have more than 2 animations you want to chain, then you have to replace the 2 by the number animations you have. Also, these animations must be defined in the switchAnimation()
function.
Here is some template code for a sequence of three animations:
... void switchAnimation(int animationId) { switch(animationId) { case 0: disp.displayText(...); break; case 1: disp.displayText(...); break; case2: disp.displayText(...); break; } } void setup() { ... } void loop() { static int animationId = 0; if (disp.displayAnimate()) { disp.displayReset(); switchAnimation(++animationId % 3); } }
If you upload and run this code you should see the following animation:
In the next section, I show you how switch between animations based on external inputs, such as push buttons.
Switch Animations
Often you want to switch between animation based on some external input. This might be a push button, a remote control, a motion sensor or some sensor data such as temperature. In the following example, we are going to use two push buttons to activate either the “Left” or the “Right” animation.
Let’s start by adding the two push buttons to our circuit.
Wiring
Adding the two push buttons is simple. Just connect one pin of both buttons to ground (GND) and the other pin to Pin 7 and Pin 8 of the Arduino as shown below.:
Just watch out when connecting the button that you use the pins that are actually switched. Note that we don’t need any pull-up resistors, since we are going to use the internal pull-ups of the Arduino.
Code to Switch animation
The code for switching between animations is a simple extension of the code for chained animations.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 #define BTN1_PIN 6 #define BTN2_PIN 7 MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void switchAnimation(int animationId) { switch (animationId) { case 0: disp.displayText("Left", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } } void setAnimation(int btn_pin, int animationId) { if (!digitalRead(btn_pin)) { disp.displayReset(); disp.displayClear(); switchAnimation(animationId); delay(200); } } void animate() { if (disp.displayAnimate()) { disp.displayReset(); } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); setAnimation(0); pinMode(BTN1_PIN, INPUT_PULLUP); pinMode(BTN2_PIN, INPUT_PULLUP); } void loop() { setAnimation(BTN1_PIN, 0); setAnimation(BTN2_PIN, 1); animate(); }
The part that includes the required libraries, defines the constants and creates the MD_Parola display object are largely unchanged. The only difference is that we define the pins, where the two button are connected to.
#define BTN1_PIN 6 #define BTN2_PIN 7
Also the switchAnimation()
function is the same as before. But we now have a new function setAnimation()
:
void setAnimation(int btn_pin, int animationId) { if (!digitalRead(btn_pin)) { disp.displayReset(); disp.displayClear(); switchAnimation(animationId); delay(200); } }
It takes a button pin and an animation id and checks whether the button at the specified pin is pressed by calling !digitalRead(btn_pin)
. Note that the logic is inverted, since we are pulling down the GPIO pin when the button is pressed.
If the button is pressed, we reset and clear the display, and switch to the animation assigned to that button. The 200msec delay is for debouncing of the button. Note that the delay here is fine, since we just switched to a new animation and therefore do not delay a currently running animation.
Next we move the code that executes an animation from the main loop to a new function animate()
. This will make the loop function look much nicer.
void animate() { if (disp.displayAnimate()) { disp.displayReset(); } }
The setup() function is largely the same but we set the mode for the two pins the buttons are connected to. Note that we set INPUT_PULLUP
, to use the internal pull-up resistors.
void setup() { ... pinMode(BTN1_PIN, INPUT_PULLUP); pinMode(BTN2_PIN, INPUT_PULLUP); }
The loop
function is now beautifully simple. We just assign the id of an animation to the buttons via setAnimation()
and then call animate()
to execute the currently active animation. Note that it read like a little story or recipe. That is how we want our code to look like.
void loop() { setAnimation(BTN1_PIN, 0); setAnimation(BTN2_PIN, 1); animate(); }
Upload the code and give it a go! The short clips below show you the two animation you should see, when pressing the left (BTN1) or right (BTN2) button.
In the next section we are going to modify the function of the two button.
Toggle or Suspend Animations
Instead of using a separate button for each animation, we will use one button (BTN1) to toggle the animation and the other button (BTN2) to pause the currently running animation. The following code shows you how that is done.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 #define BTN1_PIN 6 #define BTN2_PIN 7 int animationId = 0; MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void switchAnimation() { switch (animationId) { case 0: disp.displayText("Left", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } } void toggleAnimation() { if (!digitalRead(BTN1_PIN)) { disp.displayReset(); disp.displayClear(); animationId = (animationId + 1) % 2; switchAnimation(); delay(200); } } void pauseAnimation() { static bool isPaused = false; if (!digitalRead(BTN2_PIN)) { isPaused = !isPaused; disp.displaySuspend(isPaused); delay(200); } } void animate() { if (disp.displayAnimate()) { disp.displayReset(); } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); switchAnimation(); pinMode(BTN1_PIN, INPUT_PULLUP); pinMode(BTN2_PIN, INPUT_PULLUP); } void loop() { toggleAnimation(); pauseAnimation(); animate(); }
There are a few changes compared to the previous code. First, the switchAnimation()
doesn’t take an animationId
as argument anymore but relies on a global variable to keep track of the currently running animation.
Secondly, we have two new functions: toggleAnimation()
and pauseAnimation()
. Let’s have a closer look at toggleAnimation()
first.
void toggleAnimation() { if (!digitalRead(BTN1_PIN)) { disp.displayReset(); disp.displayClear(); animationId = (animationId + 1) % 2; switchAnimation(); delay(200); } }
This function checks if BTN1 was pressed. If that is the case, it first resets and clears the display. Then it updates the animationId
to the next animation and calls switchAnimation()
to set the animation for that id. At the end we have the usual delay for button debouncing.
The pauseAnimation()
function checks whether BTN2 was pressed and if that is the case toggles the isPaused
flag. Based on the value of this flag the display animation is then suspended or not.
void pauseAnimation() { static bool isPaused = false; if (!digitalRead(BTN2_PIN)) { isPaused = !isPaused; disp.displaySuspend(isPaused); delay(200); } }
The setup function is essentially unchanged and the loop function again reads very nicely like a little story:
void loop() { toggleAnimation(); pauseAnimation(); animate(); }
In each iteration we handle the toggling or suspension of the animation. As you can see it is easy to change the function of buttons and keep everything neatly organized.
In the next two sections we go one step further and add a potentiometer to adjust the brightness or speed of a running animation.
Change Animation Brightness
In this part we have a look how we can adjust the brightness of the displayed text, while the animation is running.
Wiring
Let’s start by adding a potentiometer to our circuit. We will use it to adjust the brightness or intensity of the display in an analog fashion. I used a 10kΩ potentiometer but any value between 1kΩ up to 100kΩ will work as well.
Connect the middle pin of the potentiometer (POT) to the analog input A0 of the Arduino. Then connect 5V and GND to the two other pins of the potentiometer. The order doesn’t really matter but will affect if the brightness increases if you turn the potentiometer to the left or right. Just swap the 5V and GND connections at the potentiometer if you prefer a specific direction, e.g. turning right increases the brightness.
Code to change Brightness
And here is the code we used before with the additional functionality to adjust the brightness of the display based on the position of the potentiometer.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 #define BTN1_PIN 6 #define BTN2_PIN 7 int animationId = 0; MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void switchAnimation() { switch (animationId) { case 0: disp.displayText("Left", PA_CENTER, 100, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } } void toggleAnimation() { if (!digitalRead(BTN1_PIN)) { disp.displayReset(); disp.displayClear(); animationId = (animationId + 1) % 2; switchAnimation(); delay(200); } } void pauseAnimation() { static bool isPaused = false; if (!digitalRead(BTN2_PIN)) { isPaused = !isPaused; disp.displaySuspend(isPaused); delay(200); } } void adjustBrightness() { int val = analogRead(A0); int intensity = map(val, 0, 1023, 0, 15); disp.setIntensity(intensity); } void animate() { if (disp.displayAnimate()) { disp.displayReset(); } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); switchAnimation(); pinMode(BTN1_PIN, INPUT_PULLUP); pinMode(BTN2_PIN, INPUT_PULLUP); } void loop() { toggleAnimation(); pauseAnimation(); adjustBrightness(); animate(); }
Let’s have a look at the changes in code. We need only one new function that reads the potentiometer value via analogRead() maps it to an intensity value between 0 and 15 and then sets the display intensity/brightness accordingly.
void adjustIntensity() { int val = analogRead(A0); int intensity = map(val, 0, 1023, 0, 15); disp.setIntensity(intensity); }
The we need to call adjustBrightness()
in the main loop to adjust the brightness of the display, while the animation is running. And that’s it. Now we can toggle and pause animations, and can changes the brightness.
In the next section, we do something very similar but instead of adjusting the brightness of the display we adjust the speed of the animation.
Change Animation Speed
Here is the complete code to set the animation speed depending on the potentiometer position.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 #define BTN1_PIN 6 #define BTN2_PIN 7 int animationId = 0; MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void switchAnimation() { int speed = disp.getSpeed(); switch (animationId) { case 0: disp.displayText("Left", PA_CENTER, speed, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, speed, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } } void toggleAnimation() { if (!digitalRead(BTN1_PIN)) { disp.displayReset(); disp.displayClear(); animationId = (animationId + 1) % 2; switchAnimation(); delay(200); } } void pauseAnimation() { static bool isPaused = false; if (!digitalRead(BTN2_PIN)) { isPaused = !isPaused; disp.displaySuspend(isPaused); delay(200); } } void adjustSpeed() { int val = analogRead(A0); int speed = map(val, 0, 1023, 10, 1000); disp.setSpeed(speed); } void animate() { if (disp.displayAnimate()) { disp.displayReset(); } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); switchAnimation(); pinMode(BTN1_PIN, INPUT_PULLUP); pinMode(BTN2_PIN, INPUT_PULLUP); } void loop() { toggleAnimation(); pauseAnimation(); adjustSpeed(); animate(); }
This codes is a tiny bit more complex, since we have to add a new function and change the switchAnimation()
function. In the switchAnimation()
function we now get the current speed of the display and use it when we are switching to another animation:
void switchAnimation() { int speed = disp.getSpeed(); switch (animationId) { case 0: disp.displayText("Left", PA_CENTER, speed, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); break; case 1: disp.displayText("Right", PA_CENTER, speed, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT); break; } }
And we have the new function adjustSpeed()
, which is very similar to the previous adjustBrightness()
function. The adjustSpeed()
function that reads the potentiometer value via analogRead() maps it to an speed value between 10 and 1000 and then sets the display speed accordingly.
void adjustSpeed() { int val = analogRead(A0); int speed = map(val, 0, 1023, 10, 1000); disp.setSpeed(speed); }
Change Animation Content
Finally, I want to show you a simple method to update the content of an animation. You will need this if you want to display sensor data, for instance, a temperature value or time data.
Instead of using a specific sensor, in the example below we re-use the potentiometer to simulate sensor readings. But you could easily replace the potentiometer with any other sensor.
#include "MD_Parola.h" #include "MD_MAX72xx.h" #include "SPI.h" #define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4 #define CS_PIN 3 MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES); void updateAnimation() { static char buffer[32] = ""; int value = analogRead(A0); sprintf(buffer, "value=%d", value); disp.displayText(buffer, PA_CENTER, 50, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); } void animate() { if (disp.displayAnimate()) { disp.displayReset(); updateAnimation(); } } void setup() { disp.begin(); disp.setIntensity(0); disp.displayClear(); updateAnimation(); } void loop() { animate(); }
The core functionality for the animation update is in the function updateAnimation()
. It reads the current sensor value, in this example the potentiometer value, formats and writes it to a string buffer
via sprintf
, and finally updates the displayed text via displayText()
. If you upload and run the code you should see the following animation:
Note that the size of the string buffer is set to 32 characters. If you want to show longer texts, you will need to increase the buffer size.
The updateAnimation()
can easily be changed to show other data. Let’s assume instead of the potentiometer you connect a DHT11 temperature sensor. You would just change the function as follows:
float t = dht.readTemperature(); void updateAnimation() { static char buffer[32] = ""; float temp = dht.readTemperature(); sprintf(text, "Temp=%.2f", temp); disp.displayText(buffer, PA_CENTER, 50, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT); }
This assumes, of course, that you have we connected the DHT11 sensor and setup its function in the code. Have a look at the tutorial on How to use DHT11 and DHT22 Sensors with Arduino, for more information.
Note that the content update happens when a new animation starts but not during the execution of an animation. For instance, we are using the scrolling text animation and the text will not update while the text scrolls. It will update, however, when a new cycle starts. You can see this in the animate()
function, where we call updateAnimation()
, when the current animation is complete.
void animate() { if (disp.displayAnimate()) { disp.displayReset(); updateAnimation(); } }
And there you have it! Several, hopefully useful, examples on how to control various features of an animation using digital or analog inputs, without disrupting the animation. If you have any questions, feel free to leave a comment.
Conclusions
The Parola library is great for animations but it can be a bit tricky to understand how to control, change or update animations without interrupting them. I hope this tutorial gave you all the information to achieve this in your projects.
We used buttons and a potentiometer as inputs to control the animations. But it is easy to use other digital or analog inputs instead. For instance, you could control the animations via a IR remote control, or use a photoresistor to adjust the display brightness based on ambient light, and display up-to-date weather data from the internet.
Furthermore, the Parola library allows you to divide a display in multiple zones that can run different animations, e.g. one zone for a clock and the other one for weather data. If you want to learn more about that have a look at the Coordinate Parola Zone Animations on MAX7219 Display tutorial.
Finally, while we used an Arduino in this tutorial, any other common microcontroller such as ESP32 or ESP8266 will work as well.
And now enjoy animating things ; )
Stefan is a professional software developer and researcher. He has worked in robotics, bioinformatics, image/audio processing and education at Siemens, IBM and Google. He specializes in AI and machine learning and has a keen interest in DIY projects involving Arduino and 3D printing.
RAJ
Thursday 13th of February 2025
good