Arduino LED Display (8×3) 8×8 LED Módulos controlados via MAX722xx

Tempo de leitura: 11 minutes

Este projeto contém driver para Módulos LED 8×8 controlados via MAX722xx. Ele permite que você crie uma exibição de tamanho personalizado que é limitada apenas pelo próprio hardware. Os tamanhos vertical e horizontal podem conter até 256 módulos, mas antes de atingir esse limite você ficaria sem linhas Slave Select para controlar os chips MAX ou ficaria limitado pela quantidade de RAM. O fato é: você pode controlar uma quantidade razoável de chips MAX e criar uma exibição de tamanho personalizado;)

Eu testei toda a ideia em um display que consiste em 8 módulos de LED na horizontal e 3 na posição vertical. Isso nos dá 24 módulos que contêm 1536 LEDs (88 * 38).

Hardware

Primeiro vamos começar com o controlador, na verdade qualquer Arduino funcionará, usei o Mega devido ao grande número de pinos de saída digital. Você também pode usar o Nano com o registrador de deslocamento e alterar a forma de endereçar as linhas Select Slave em Display: send (…).

Você precisará de uma fonte de alimentação extra para acionar os LEDs – presumindo que você usará mais de uma matriz de LED.

Módulo de matriz de LED único de condução

Eu usei o MAX7219 e o 788BS LED Matrix, este é o único com ânodo comum. O esquema abaixo ilustra a fiação dos LEDs, MAX e Arduino:

Este é equivalente, mas em vez de LEDs únicos, temos layout de PIN do Módulo de LED:

Pode acontecer, que o seu Módulo LED tenha cátodo comum, neste caso você tem que religar a conexão ao chip MAX. Você apenas tem que ter em mente que o MAX tem dois conjuntos de pinos, que são relevantes aqui: Dig0-Dig7 devem ser conectados aos cátodos (-) e SegA-SegG aos ânodos (+). Além disso, essa mudança irá trocar linhas com colunas dentro do módulo de LED de seno.

Conectando todos os LEDs Matrix juntos

No capítulo anterior, vimos como conectar um módulo de LED único com o chip MAX. Agora, conectaremos vários módulos de LED em um grande display. Abaixo está a tela física que usei para testes e exemplos. Cada Módulo de LED possui uma etiqueta indicando sua posição e a linha Selecionar Escravo.

Aqui está a ligação:

Cada conector de 3 pinos no esquema acima simboliza um módulo descrito no capítulo anterior (LED Matrix + MAX72xx), agora conectamos todos esses módulos juntos.

Todos os chips MAX722xx compartilham linhas MOSI e SCK comuns, MISO não é usado, cada chip ocupa uma linha Slave Select separada.

A posição da matriz de LED no esquema acima corresponde diretamente à sua localização no display físico que usei para teste. Adicionalmente cada módulo possui uma descrição indicando sua posição e linha Select Slave, então por exemplo: (2,1) SS: 35 nos dá o segundo módulo na terceira linha (contando a partir do zero) e PIN: 35 no Arduino para a linha Select Slave.

Compilando

Estamos usando bibliotecas padrão do Arduino, então elas já estão disponíveis, a única exceção é o ArdLog. Você deve importar este LIB para o seu IDE. Basicamente, isso significa que você deve baixar a versão correta: https://github.com/maciejmiklas/ArdLog/releases/tag/v1.0.0 e descompactá-la na pasta, onde normalmente coloca as bibliotecas externas.

Comunicação com MAX72xxx

Estamos usando a biblioteca SPI padrão e a linha Select Slave no chip MAX para endereçamento. MAX é configurado no modo LED Matrix – portanto, não há nada de especial. O método de configuração pode ser encontrado em: Display::setup ()

Configurando as coisas

A principal classe de nosso interesse será o Display – é responsável pela configuração dos chips MAX e fornece API para pintura.

Antes de começarmos a pintar, é necessário configurar as coisas. O código abaixo cria um array 2D contendo linhas Select Slave e inicializa a exibição. O display em si consiste em 3 linhas, cada uma com 8 módulos de LED. Obviamente, você pode escolher qualquer tamanho responsável, mas vou ficar com este.

O layout da matriz 2D mencionada corresponde à exibição física: cada Módulo de LED tem um chip MAX dedicado e cada chip tem uma linha Select Slave dedicada. A primeira dimensão do nosso array indica a linha física no display, a segunda dimensão indica o Módulo LED dentro desta linha e o valor em si contém o número PIN para a linha Select Slave.

#include <Display.h>

Display *disp;

/**
 * Orientação dos kits de LED (matriz de 8x8 LED) no visor que usei para teste.
 * Os números indicam a linha Select Slave de MAX7219. * 48, 46, 49, 47, 45, 43, 41, 39
 * 36, 34, 32, 30, 28, 26, 24, 22
 * 37, 35, 33, 31, 29, 27, 25, 23
 */
ss_t **ss;
ss_t** createSS() {
  ss_t **ss = alloc2DArray8(3, 8);

  // primeira fila
  ss[0][0] = 48;
  ss[0][1] = 46;
  ss[0][2] = 49;
  ss[0][3] = 47;
  ss[0][4] = 45;
  ss[0][5] = 43;
  ss[0][6] = 41;
  ss[0][7] = 39;

  // segunda fila
  ss[1][0] = 36;
  ss[1][1] = 34;
  ss[1][2] = 32;
  ss[1][3] = 30;
  ss[1][4] = 28;
  ss[1][5] = 26;
  ss[1][6] = 24;
  ss[1][7] = 22;

  // terceira fila
  ss[2][0] = 37;
  ss[2][1] = 35;
  ss[2][2] = 33;
  ss[2][3] = 31;
  ss[2][4] = 29;
  ss[2][5] = 27;
  ss[2][6] = 25;
  ss[2][7] = 23;

  return ss;
}

void setup() {
  util_setup();
  log_setup();
  
  ss = createSS();
  
  // Teste de exibição consiste em 8x3 módulos de LED (3 linhas, cada uma com 8 módulos)
  disp = new Display(8, 3, ss);
  disp->setup();
}

Há mais um método que vale a pena mencionar: log_setup(). Todo o projeto tem um registrador silencioso e preciso – para que você possa ver o que está realmente acontecendo. Por padrão está desabilitado, para habilitá-lo verifique sua documentação: https://github.com/maciejmiklas/ArdLog

Pintura na vitrine

Há mais um método que vale a pena mencionar: log_setup(). Todo o projeto tem um registrador silencioso e preciso – para que você possa ver o que está realmente acontecendo. Por padrão está desabilitado, para habilitá-lo verifique sua documentação: https://github.com/maciejmiklas/ArdLog

Pintura na vitrine

A exibição consiste em alguns módulos de LED, mas da perspectiva da API, eles são conectados em uma tela contínua. Você pode colocar neste bitmap de tela em qualquer posição fornecida por tais coordenadas:

(0,0) -----------------------------> (x)
      |
      |
      |
      |
      |
      |
      |                            (x max, y max)
      v (y)

O método paint tem a seguinte sintaxe: paint (pixel_t x, pixel_t y, pixel_t width, pixel_t height, uint8_t data). Ele permite que você pinte um bitmap em determinadas coordenadas com largura e altura limitadas. Portanto, você pode, por exemplo, pintar um bitmap em (3,4) que tenha 25×3 pixels. Ele pode ser maior do que o tamanho real da tela – neste caso, será cortado.

Isso é óbvio e simples, mas há um problema – você deve fornecer os dados corretos. Esta é a matriz 2D, onde a primeira dimensão indica a posição vertical e a segunda posição horizontal na tela. Tecnicamente falando, os dados são uma matriz plana de ponteiros e cada ponteiro aponta para uma matriz que representa uma linha horizontal no visor.

Mover a primeira dimensão de dados atravessa as linhas da tela. A segunda dimensão de dados representa pixels horizontais em uma única linha, onde cada byte representa 8 pixels. Como nossa tela consiste em LEDs simples, eles podem estar no estado ligado ou desligado, então cada pixel não está sendo representado por um byte, mas por um bit. Para cobrir 16 pixels na posição horizontal, precisamos de dois bytes, 24 pixels precisam de 3 bytes e assim por diante.

Por exemplo, para cobrir totalmente a tela que consiste em kits de LED de 8×3 (um usado em nossos exemplos), precisaríamos de dados [3] [8]. Normalmente, você pegará uma matriz pequena o suficiente para caber no bitmap e não uma que cubra a tela inteira.

O método paint (…) atualiza o buffer interno, a fim de enviar o conteúdo desse buffer aos chips MAX, você deve chamar flush (). A ideia por trás disso é dar a você a possibilidade de exibir alguns bitmaps na tela e depois pintar o resultado. Você pode programar algumas rotinas independentes, que irão atualizar diferentes partes da tela e liberar todas as alterações de uma vez.

A comunicação com os chips MAX não é muito rápida e o envio de conteúdo de todo o display a cada flush () é demorado. Você pode ser capaz de acelerar esse processo habilitando o buffer duplo (defina DEOUBLE_BUFFER em Display.h como verdadeiro). Nesse caso, o método flush () enviará apenas os bytes que foram alterados, portanto, você pode chamar flush () com cada loop e não precisa se preocupar em perder desempenho. A única desvantagem é o aumento do uso de RAM: estamos criando um array 2D que aloca 8 bytes por cada Kit de LED, mais alguns ponteiros que geralmente são necessários para manter os arrays.

Os arrays 2D neste projeto reduziram o consumo de memória, porque para criar um array 2D dinâmico, estamos criando, na verdade, 2 arrays com deslocamento calculado (consulte: alloc2DArray8 (….) em Util.h).

Requer Libs

Exemplos são o uso de ArdLog, então você deve importar esta biblioteca para o Arduino IDE. Aqui estão as instruções: https://github.com/maciejmiklas/ArdLog

Bitmap simples

Aqui está o sketch do Arduino: Bitmap simples, agora vamos examiná-lo:

#include <Display.h>

Display *disp;

/**
 * Orientação dos kits de LED (matriz de 8x8 LED) no visor que usei para teste.
 * Os números indicam a linha Select Slave de MAX7219. * 48, 46, 49, 47, 45, 43, 41, 39
 * 36, 34, 32, 30, 28, 26, 24, 22
 * 37, 35, 33, 31, 29, 27, 25, 23
 */
ss_t **ss;

uint8_t ** data;

ss_t** createSS() {
  ss_t **ss = alloc2DArray8(3, 8);

  // primeira linha
  ss[0][0] = 48;
  ss[0][1] = 46;
  ss[0][2] = 49;
  ss[0][3] = 47;
  ss[0][4] = 45;
  ss[0][5] = 43;
  ss[0][6] = 41;
  ss[0][7] = 39;

  // segunda linha
  ss[1][0] = 36;
  ss[1][1] = 34;
  ss[1][2] = 32;
  ss[1][3] = 30;
  ss[1][4] = 28;
  ss[1][5] = 26;
  ss[1][6] = 24;
  ss[1][7] = 22;

  // terceira linha
  ss[2][0] = 37;
  ss[2][1] = 35;
  ss[2][2] = 33;
  ss[2][3] = 31;
  ss[2][4] = 29;
  ss[2][5] = 27;
  ss[2][6] = 25;
  ss[2][7] = 23;

  return ss;
}

void setup() {
  dutil_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  data = alloc2DArray8(8, 2);
  data[0][0] = B01100001; data[0][1] = B10000000;
  data[1][0] = B01100001; data[1][1] = B10000000;
  data[2][0] = B01100001; data[2][1] = B10000000;
  data[3][0] = B01100001; data[3][1] = B10000000;
  data[4][0] = B01100001; data[4][1] = B10000000;
  data[5][0] = B00110011; data[5][1] = B00000000;
  data[6][0] = B00011110; data[6][1] = B00000000;
  data[7][0] = B00001100; data[7][1] = B00000000;

  disp->paint(27, 9, 9, 8, data);
}

void loop() {
  dutil_cycle();
  log_cycle();
  
  // O método Paint atualiza apenas o buffer interno,
  // para enviar dados aos chips MAX, você deve limpar o display.
  disp->flush();
  delay(100000);
}

Primeiro, temos que inicializar a exibição, como fizemos acima, no capítulo ‘Configurando as coisas‘. Em seguida, temos que criar dados que possam conter nosso bitmap – ele terá 8×2 bytes. Isso nos dá até 8 linhas e 16 pixels horizontais. Mas o tamanho do nosso bitmap é 9×8 pixels (largura x altura) e este será também o tamanho do retângulo pintado. Deve ser o menor possível, para que você possa colocar outro bitmap ao lado dele.

O display obviamente pintará apenas o retângulo dado pela largura/altura e não a matriz de dados inteira. Isso é normal, esse array de dados pode conter mais pixels do que o tamanho acumulado do bitmap de saída, porque o tamanho dos dados é uma multiplicação de 8 e o bitmap não é necessário.

void setup() {
  util_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  data = alloc2DArray8(8, 2);
  data[0][0] = B01100001; data[0][1] = B10000000;
  data[1][0] = B01100001; data[1][1] = B10000000;
  data[2][0] = B01100001; data[2][1] = B10000000;
  data[3][0] = B01100001; data[3][1] = B10000000;
  data[4][0] = B01100001; data[4][1] = B10000000;
  data[5][0] = B00110011; data[5][1] = B00000000;
  data[6][0] = B00011110; data[6][1] = B00000000;
  data[7][0] = B00001100; data[7][1] = B00000000;

  disp->paint(27, 9, 9, 8, data);
}

void loop() {
  util_cycle();
  log_cycle();
  
  // O método Paint atualiza apenas o buffer interno, a fim de enviar dados para
  // MAX fichas que você precisa liberar o display.
  disp->flush();
  
  delay(100000);
}

Texto Estático

Agora exibiremos texto estático; na verdade, essas serão duas linhas independentes.

Aqui você pode encontrar o sketch do Arduino contendo o exemplo completo: StaticText.

#include <Display.h>
#include <StaticText8x8.h>

Display *disp;
StaticText8x8 *sta1;
StaticText8x8 *sta2;
/**
 * Orientação dos kits de LED (matriz de 8x8 LED) no visor que usei para teste.
 * Os números indicam a linha Select Slave de MAX7219. * 48, 46, 49, 47, 45, 43, 41, 39
 * 36, 34, 32, 30, 28, 26, 24, 22
 * 37, 35, 33, 31, 29, 27, 25, 23
 */
ss_t **ss;

ss_t** createSS() {
  ss_t **ss = alloc2DArray8(3, 8);

  // primeira linha
  ss[0][0] = 48;
  ss[0][1] = 46;
  ss[0][2] = 49;
  ss[0][3] = 47;
  ss[0][4] = 45;
  ss[0][5] = 43;
  ss[0][6] = 41;
  ss[0][7] = 39;

  // segunda linha
  ss[1][0] = 36;
  ss[1][1] = 34;
  ss[1][2] = 32;
  ss[1][3] = 30;
  ss[1][4] = 28;
  ss[1][5] = 26;
  ss[1][6] = 24;
  ss[1][7] = 22;

  // terceira linha
  ss[2][0] = 37;
  ss[2][1] = 35;
  ss[2][2] = 33;
  ss[2][3] = 31;
  ss[2][4] = 29;
  ss[2][5] = 27;
  ss[2][6] = 25;
  ss[2][7] = 23;

  return ss;
}


void setup() {
  dutil_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  sta1 = new StaticText8x8(disp, 64);
  sta1->box(14, 2, "Olá");

  sta2 = new StaticText8x8(disp, 64);
  sta2->box(5, 15, "Mundo!");
}

void loop() {
  dutil_cycle();
  log_cycle();
  disp->flush();
  
  delay(100000);
}

Seu sketch precisa do método de configuração como já vimos acima (capítulo: Configurando as coisas), portanto, não o discutiremos novamente.

Para exibir o texto, você deve usar StaticText8x8.

A fonte é definida em: Font8x8, cada caractere tem 8×8 pixels.

Seu código pode ser parecido com este (mais as coisas de inicialização em Configurando as coisas):

StaticText8x8 *sta1;
StaticText8x8 *sta2;

void setup() {
  util_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  sta1 = new StaticText8x8(disp, 64);
  sta1->box(14, 2, "Hello");

  sta2 = new StaticText8x8(disp, 64);
  sta2->box(5, 15, "World !");
}

void loop() {
  util_cycle();
  log_cycle();
  disp->flush();
  
  delay(100000);
}

Criamos duas áreas de texto, cada uma contendo um texto diferente e sendo exibida uma abaixo da outra.

Texto de rolagem única

Desta vez, vamos exibir uma área contendo texto que irá rolar da esquerda para a direita.

Para que não analise o código

Display *disp;

ScrollingText8x8 *message;
const char *textMessage;

void setup() {
  util_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  message = new ScrollingText8x8(disp, 48, 50, 5);
  message->init();
  textMessage = "Este é um exemplo de várias áreas de rolagem ;)";
  message->scroll(8, 8, ScrollingText8x8::LOOP, textMessage);
}

void loop() {
  util_cycle();
  log_cycle();

  message->cycle();
  
  disp->flush();
}

A inicialização do display é igual à dos exemplos acima, portanto, é omitida aqui.

Para exibir o texto de rolagem, estamos usando ScrollingText8x8. Em setup (), estamos criando uma instância dessa classe e chamando o método scroll (…). Esta parte apenas inicializa a rolagem, mas não reproduz a animação em si. Para reproduzir a animação, você deve chamar cycle () e flush () no loop principal e não deve haver atrasos adicionais, caso contrário, poderá obter animação irregular.

Durante a criação de ScrollingText8x8, fornecemos a velocidade da animação – na verdade, é um atraso de 50 ms por quadro. Agora, chamar cycle() no loop principal produzirá quadros de animação de acordo com o atraso fornecido. Quando chegar a hora, o método cycle() atualizará a exibição e, finalmente, o método flush () enviará o conteúdo atualizado para os chips MAX.

Toda a implementação de ScrollingText8x8 não bloqueia e consome CPU apenas quando há algo a ser feito. Internamente, ele está usando uma máquina de estado simples.

Há uma última coisa: você deve manter o texto usado para animação em uma variável global para evitar a coleta de lixo. Não está sendo copiado em scroll() para evitar a fragmentação da memória.

Texto de rolagem misto

Este exemplo é semelhante ao anterior, mas desta vez exibiremos várias áreas de rolagem

Este código é semelhante a um com uma área de rolagem, mas desta vez temos alguns:

void setup() {
  util_setup();
  log_setup();
  
  ss = createSS();

  disp = new Display(8, 3, ss);
  disp->setup();

  uint8_t borderSpeed = 20;
  textUpDown = "* * * * * ";
  up = new ScrollingText8x8(disp, 64, borderSpeed, 1);
  up->init();
  up->scroll(0, 0, ScrollingText8x8::CONTINOUS_LOOP, textUpDown);
  
  down = new ScrollingText8x8(disp, 64, borderSpeed, 2);
  down->init();
  down->scroll(0, 16, ScrollingText8x8::CONTINOUS_LOOP, textUpDown);
 
  textLeftRight = "* ";
  left = new ScrollingText8x8(disp, 8, borderSpeed, 3);
  left->init();
  left->scroll(0, 8, ScrollingText8x8::CONTINOUS_LOOP, textLeftRight);
   
  right = new ScrollingText8x8(disp, 8, borderSpeed, 4);
  right->init();
  right->scroll(56, 8, ScrollingText8x8::CONTINOUS_LOOP, textLeftRight);

  message = new ScrollingText8x8(disp, 48, 50, 5);
  message->init();
  textMessage = "Este é um exemplo de várias áreas de rolagem;)";
  message->scroll(8, 8, ScrollingText8x8::LOOP, textMessage);
}

void loop() {
  util_cycle();
  log_cycle();

  up->cycle();
  down->cycle();
  right->cycle();
  message->cycle();
  left->cycle();
  
  
  disp->flush();
}

Criamos algumas instâncias de ScrollingText8x8, cada uma contendo textos e posições diferentes na tela. Para reproduzir a animação, você deve chamar cycle() em cada instância, mas deve chamar apenas uma vez flush(). Cada chamada em cycle() atualizará sua parte do display e flush enviará display alterado para MAX chips.

Projeto completo do LEDdisplay (Baixe)