Neste tutorial vais aprender como coordenar animações de zonas Parola num ecrã de matriz de LEDs MAX7219. A biblioteca MD_Parola é bastante popular e facilita muito a programação de animações como o scroll de texto. Também permite dividir o ecrã em várias zonas e correr diferentes animações nessas zonas.
No entanto, coordenar animações em múltiplas zonas não é fácil. Por exemplo, como reiniciar uma animação numa zona sem afetar a animação noutra zona? Ou como reiniciar todas as animações apenas quando todas tiverem terminado, mesmo que estejam a correr a velocidades diferentes?
Este tutorial vai dar-te uma estrutura de código para lidar facilmente com estes cenários de animação.
Componentes Necessários
Usei um Arduino Uno para este projeto, mas qualquer outra placa Arduino, ou uma placa ESP8266/ESP32, também funcionará. Além disso, utilizei um ecrã de matriz de LEDs MAX7219 com 4 módulos. No entanto, o código e a ligação para ecrãs com diferentes números de módulos é essencialmente igual.

Ecrã de Matriz de LEDs MAX7219

Arduino Uno

Conjunto de Cabos Dupont

Cabo USB para Arduino UNO
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.
Ligar o Ecrã LED MAX7219 ao Arduino
Neste tutorial vamos usar um ecrã de matriz de LEDs MAX7219 para mostrar animações. O controlador do ecrã MAX7219 comunica com o Arduino através de SPI (Serial Peripheral Interface). Por isso, o primeiro passo é ligar o ecrã ao Arduino usando a interface SPI.
Ligaçōes
O diagrama seguinte mostra-te as ligações entre o ecrã MAX7219 e um Arduino Uno. Certifica-te de que estás a ligar ao lado de entrada (DIN) e não ao lado de saída (DOUT) do ecrã.

Começa por ligar os pinos da interface SPI: O pino 11 do Arduino liga ao pino DIN do ecrã. Depois liga o pino 3 ao CS e o pino 13 ao CLK. Por fim, liga 5V ao VCC e GND ao GND. A tabela seguinte mostra novamente as ligações:
| Ecrã | Arduino |
|---|---|
| VCC | 5 V |
| GND | GND |
| DIN | 11 (MOSI) |
| CS | 3 (SS) |
| CLK | 13 (SCK) |
Os pinos acima são para SPI por hardware, que é mais rápido do que SPI por software, mas obriga-te a usar pinos específicos que variam entre placas. Para mais detalhes vê a MAX7219 LED dot matrix display Arduino tutorial .
De seguida, vamos escrever e correr algum código de teste para garantir que as ligações estão corretas.
Instalar as bibliotecas MD_Parola e MD_MAX72XX
Primeiro, precisas de instalar a MD_MAX72XX e a MD_Parola biblioteca. Abre o Arduino IDE e vai a Tools > Manage Libraries para abrir o Library Manager. Procura por “MD_MAX72XX” e as bibliotecas relevantes vão aparecer listadas. A imagem abaixo mostra-te as duas bibliotecas (marcadas a amarelo) depois de instaladas.

A biblioteca MD_MAX72XX é basicamente o driver para o ecrã LED MAX7219. A biblioteca MD_Parola usa-a para criar animações como scroll e efeitos de texto tipo sprite. Se quiseres saber mais sobre as animações disponíveis, vê a MAX7219 LED dot matrix display Arduino tutorial .
Código de Teste
O código seguinte é um exemplo mínimo para testar as ligações e o funcionamento do ecrã. Simplesmente faz scroll do texto “Test” no ecrã.
#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();
}
}
Se carregares e correres este código, deves ver o seguinte resultado no teu ecrã MAX7219:

Para mais detalhes sobre este código de teste, vê o tutorial Control Parola Animations on MAX7219 LED Display , onde usamos o mesmo código de teste.
Nota: se usares um tipo diferente de ecrã LED MAX7219 ou tiveres um ecrã com mais ou menos módulos, vais precisar de ajustar as seguintes definições:
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW #define MAX_DEVICES 4
Da mesma forma, se usares SPI por software em vez de SPI por hardware, vais precisar de mudar a forma como o objeto do ecrã é criado:
MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
Podes encontrar mais informações sobre isto na MAX7219 LED dot matrix display Arduino tutorial .
Nas secções seguintes, vamos analisar melhor as zonas, como criá-las e usá-las, e, mais importante, como coordenar animações que correm em diferentes zonas.
O que são Zonas Parola
A biblioteca Parola trata um ecrã LED MAX7219 como uma sequência de módulos LED 8×8, numerados de 0 até n-1 . Uma “Zona” é uma secção de módulos consecutivos. Cada zona pode ser controlada de forma independente das outras zonas. O exemplo abaixo mostra um ecrã MAX7219 com quatro módulos (0…3) e duas definições de zonas.

A Zona 0 inclui os módulos 0 e 1, e a Zona 1 inclui os módulos 2 e 3. Nota que a numeração dos módulos começa em 0 e do lado de entrada (lado direito) do ecrã. Em cada zona podes correr diferentes animações com diferentes fontes, efeitos de texto, strings de texto, velocidades e até brilhos.
As zonas não devem sobrepor-se, podem ter comprimentos diferentes e o índice da zona não tem de estar na mesma ordem sequencial dos módulos. Por exemplo, o exemplo seguinte mostra outra definição de zonas válida. Aqui temos três zonas (0,1,2) mas são de comprimentos diferentes e os índices das zonas não estão em ordem sequencial (0,2,1).

Como definir Zonas Parola
Imagina que queremos definir as seguintes zonas no ecrã. A Zona 0 vai do módulo 0 ao módulo 1, e a Zona 1 vai do módulo 2 ao módulo 3:

Na função setup, começa por chamar begin() com o número total de zonas (neste caso 2). Depois defines as zonas individuais chamando setZone(z, start, end) . O primeiro argumento z é o id da zona (0 ou 1, no nosso caso), seguido do índice do primeiro e do último módulo dessa zona.
#define MAX_ZONES 2
void setup() {
disp.begin(MAX_ZONES);
disp.setZone(0, 0, 1);
disp.setZone(1, 2, 3);
}
Como já foi referido, as zonas não devem sobrepor-se, mas podem. No entanto, se se sobrepuserem, tens de garantir que as zonas sobrepostas não correm animações ao mesmo tempo, senão vais obter artefactos estranhos.
Depois de definires as zonas, podes atribuir animações a essas zonas. Por exemplo, a linha de código seguinte define uma animação para a zona 0 que faz scroll do texto “Left” para a esquerda. Mais sobre isto na próxima secção.
disp.displayZoneText(0, "Left", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT);
Como animar Zonas Parola
A maioria das funções relacionadas com animações Parola existe em duas versões. Uma versão que funciona em todo o ecrã, e outra que funciona numa zona específica. Vê a Parola Documentation para uma visão geral.
Por exemplo, podes definir o brilho de todo o ecrã ou de uma zona específica chamando setIntensity sem ou com um índice de zona.
setIntensity(intensity); // entire display setIntensity(z, intensity); // zone z
O mesmo acontece com a maioria das outras funções para definir fontes, efeitos de texto, strings de texto, velocidades, etc. O excerto de código seguinte mostra como mostrar e animar textos diferentes em duas zonas. Na primeira zona (0) fazemos scroll do texto “0” para a direita, e na segunda zona (1), fazemos scroll do texto “1” para a esquerda.
#define SPEED 100
void setup() {
disp.begin(MAX_ZONES);
disp.setZone(0, 0, 1);
disp.setZone(1, 2, 3);
disp.displayZoneText(0, "0", PA_CENTER, SPEED, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT);
disp.displayZoneText(1, "1", PA_CENTER, SPEED, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
}
Na função loop, basta chamar o habitual displayAnimate() para saber se alguma das duas animações terminou e, se sim, reiniciamos o ecrã para todas as zonas através de displayReset() .
void loop() {
if (disp.displayAnimate()) {
disp.displayReset();
}
}
Nota que também podes reiniciar zonas individualmente chamando displayReset(z) com um identificador de zona. Vamos usar isto mais à frente.
void loop() {
if (disp.displayAnimate()) {
disp.displayReset(0);
disp.displayReset(1);
}
}
Isto funciona bem, especialmente porque ambas as animações aqui correm à mesma velocidade (100). Mas se as animações correrem a velocidades diferentes ou tiverem textos de comprimentos diferentes, então uma animação vai terminar antes da outra. Como lidamos com este caso e especificamos o que deve acontecer?
Esta coordenação entre animações em várias zonas é o tema da próxima secção.
Como coordenar Zonas Parola
Como referido acima, a chamada padrão do loop Parola chama displayAnimate() , que devolve true se alguma animação em qualquer zona estiver completa. Podes saber qual terminou chamando getZoneStatus(z) com o identificador da zona z .
Por exemplo, se tiveres duas zonas, podes reiniciar as animações em ambas as zonas quando ambas terminarem, escrevendo o seguinte código:
void loop() {
if (disp.displayAnimate()) {
if(disp.getZoneStatus(0) && disp.getZoneStatus(1)) {
disp.displayReset();
}
}
}
Isto funciona bem e é perfeito para este caso simples. Mas se tiveres cenários um pouco mais complexos ou mais zonas, rapidamente acabas com código muito confuso. Por exemplo, imagina que tens duas animações em duas zonas e uma animação é mais rápida que a outra. Isto resulta em pelo menos seis cenários de animação diferentes:
- Deixar ambas as animações correrem apenas uma vez
- Deixar a primeira animação correr uma vez, repetir a outra
- Deixar a segunda animação correr uma vez, repetir a outra
- Repetir ambas as animações quando ambas terminarem
- Repetir a animação mais rápida enquanto a mais lenta ainda está a correr
- Reiniciar a animação mais lenta quando a mais rápida terminar
Nas próximas secções vamos implementar estes seis cenários de animação. Mas para simplificar as coisas, vamos primeiro definir uma função auxiliar getStatus() .
Função getStatus
Em vez de chamar getZoneStatus(z) para cada zona, a função getStatus() devolve um vetor de bits que indica o estado de todas as zonas de uma vez.
byte getStatus() {
static byte status = 0;
disp.displayAnimate();
for (int z = 0; z < MAX_ZONES; z++) {
disp.getZoneStatus(z) ? bitSet(status, z) : bitClear(status, z);
}
return status;
}
Internamente, a função itera por todas as zonas e define o estado da zona no byte status . Isto significa que o número máximo de zonas é limitado a 8, mas normalmente é mais do que suficiente.
A imagem seguinte mostra um exemplo de vetor de bits ou vetor de estado que getStatus() devolve. Neste exemplo, as animações na zona 1 e na zona 3 estão completas:

Se chamares getStatus() no loop principal, podes facilmente imprimir o vetor de estado.
void loop() {
Serial.println(getStatus(), BIN);
}
Nota que a getStatus() internamente também chama displayAnimate() , por isso já não é preciso chamá-la na função loop.
Vamos usar getStatus() nas próximas secções para implementar os seis cenários de animação mencionados antes.
Exemplo de código com duas Zonas
Vamos começar com um exemplo de código que tem tudo exceto a implementação da função loop.
#include "MD_Parola.h"
#include "MD_MAX72xx.h"
#include "SPI.h"
#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
#define MAX_DEVICES 4
#define MAX_ZONES 2
#define CS_PIN 3
MD_Parola disp = MD_Parola(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
byte getStatus() {
static byte status = 0;
disp.displayAnimate();
for (int z = 0; z < MAX_ZONES; z++) {
disp.getZoneStatus(z) ? bitSet(status, z) : bitClear(status, z);
}
return status;
}
void setup() {
disp.begin(MAX_ZONES);
disp.setZone(0, 0, 1);
disp.setZone(1, 2, 3);
disp.setIntensity(0);
disp.displayClear();
disp.displayZoneText(0, "0", PA_CENTER, 100, 0, PA_SCROLL_RIGHT, PA_SCROLL_RIGHT);
disp.displayZoneText(1, "1", PA_CENTER, 200, 0, PA_SCROLL_LEFT, PA_SCROLL_LEFT);
}
void loop() {
// TODO
}
Neste código, primeiro incluímos as bibliotecas necessárias e definimos as constantes para o ecrã MAX7219. Se tiveres um tipo de ecrã diferente ou um número diferente de módulos, tens de alterar estas constantes.
De seguida, criamos o objeto do ecrã disp e implementamos a função getStatus() como mostrado acima.
Na função setup definimos duas animações em duas zonas. A primeira animação na zona 0 faz scroll do texto “0” para a direita. A segunda animação na zona 1 faz scroll do texto “1” para a esquerda. A imagem abaixo mostra as duas zonas com os seus índices.

Nota que a primeira animação na zona 0 corre mais rápido (velocidade=100) do que a segunda animação (velocidade=200) na zona 1. O vídeo curto abaixo mostra como isto fica.

Podemos manter o código acima igual para todos os seis cenários de animação e só precisamos de mudar a implementação da função loop.
Deixar ambas as animações correrem apenas uma vez
Vamos começar pelo caso mais simples. Queremos que ambas as animações corram apenas uma vez até terminarem. Neste caso, a função loop só tem de chamar getStatus() e não precisa de fazer mais nada.
void loop() {
getStatus();
}
Provavelmente não vais precisar disto muitas vezes, mas se implementares e correres isto, fica assim:

Nota, no entanto, que como o vídeo está em loop, parece que a animação reinicia. Na verdade, isso não acontece e a animação corre apenas uma vez e depois o ecrã fica vazio para sempre. Pode ser útil, por exemplo, para uma mensagem de arranque que aparece só uma vez.
Deixar a primeira animação correr uma vez, repetir a outra
No cenário seguinte, deixamos a primeira animação na zona 0 correr apenas uma vez, mas repetimos a segunda. Isto significa que sempre que a animação na zona 1 terminar, temos de reiniciar o ecrã para essa zona. Em baixo está o código para a função loop neste cenário.
void loop() {
if(getStatus() & 0b00000010) {
disp.displayReset(1);
}
}
Obtemos o estado e fazemos uma operação bitwise AND (&) para verificar se a animação na zona 1 terminou. Se for o caso, chamamos displayReset(1) para essa zona para repetir a animação. O vídeo seguinte mostra a animação resultante:

Mais uma vez, como o vídeo está em loop, parece que a primeira animação se repete, mas na verdade não é o caso.
Deixar a segunda animação correr uma vez, repetir a outra
No cenário seguinte trocamos os papéis da primeira e da segunda animação. Deixamos a segunda animação correr uma vez e repetimos a primeira. O código da função loop é basicamente o mesmo de antes, só mudamos a zona que estamos a verificar.
void loop() {
if(getStatus() & 0b00000001) {
disp.displayReset(0);
}
}
O vídeo abaixo mostra a animação correspondente.

Repetir ambas as animações quando ambas terminarem
Agora, vamos esperar até que ambas as animações terminem e depois reiniciar ambas. Isto é fácil de fazer. Só temos de verificar se ambos os bits de estado estão definidos. Nota que neste caso não fazemos um AND (&) mas sim uma verificação de igualdade (=)!
void loop() {
if(getStatus() == 0b00000011) {
disp.displayClear();
disp.displayReset();
}
}
No vídeo seguinte podes ver como fica a animação neste caso.

Repetir a animação mais rápida enquanto a mais lenta ainda está a correr
Como as duas animações estão a correr a velocidades diferentes, temos de decidir o que queremos fazer quando a animação mais rápida terminar. Queremos reiniciar a animação mais rápida imediatamente ou esperar até que a mais lenta termine? O caso de esperar já foi coberto na secção anterior.
Em baixo está o código que cobre o primeiro caso. Simplesmente verificamos para cada zona se terminou e, se sim, reiniciamos a animação correspondente.
void loop() {
if(getStatus() & 0b00000001) {
disp.displayReset(0);
}
if(getStatus() & 0b00000010) {
disp.displayReset(1);
}
}
Aqui está o vídeo que mostra este cenário em ação.

Reiniciar a animação mais lenta quando a mais rápida termina
O último cenário de animação é o inverso do anterior. Neste cenário queremos reiniciar a animação mais lenta assim que a mais rápida terminar. Nota que o código é bastante diferente do anterior.
void loop() {
if(getStatus() & 0b00000001) {
disp.displayReset(1);
disp.displayClear(1);
disp.displayReset(0);
}
}
Especificamente, precisamos de limpar a zona 1, já que vamos reiniciar a animação na zona 1 antes de ela terminar. Se não limpares o ecrã, vais ficar com artefactos.
O vídeo abaixo mostra esta animação. Nota que o scroll para a esquerda do “1” é interrompido e reiniciado no momento em que o scroll para a direita do “0” termina.

Conclusão
Neste tutorial aprendeste como coordenar animações Parola que correm em múltiplas zonas. Os exemplos acima usaram apenas duas zonas e são relativamente simples. No entanto, assim que tiveres mais de duas zonas, a função getStatus() vai simplificar muito o teu código.
Por exemplo, podes listar todos os estados possíveis das zonas numa instrução switch e tratá-los em conformidade. Aqui está um exemplo para duas zonas que podes facilmente adaptar para mais zonas.
void loop() {
switch (getStatus()) {
case 0b00000001:
...
break;
case 0b00000010:
...
break;
case 0b00000011:
...
break;
}
}
Não usámos esta construção nos exemplos de código acima, porque com duas zonas não simplifica muito o código, mas com mais zonas vai simplificar.
Se quiseres encadear várias animações ou controlá-las através de entradas externas, vê o tutorial Control Parola Animations on MAX7219 LED Display .
Se quiseres manipular pixels individuais da matriz de LEDs, a biblioteca Parola não é a melhor escolha e deves usar FastLED ou algo semelhante. Para um exemplo, vê o post Game of Life on a Dot Matrix Display with MAX7219 .
Espero que tenhas achado este tutorial útil. Sente-te à vontade para deixar um comentário se tiveres alguma dúvida.

