O Arduino possui diversos módulos de display para podermos escrever alguma mensagem ou desenhar uma imagem. Entretanto, é possível interfacear o Arduino com um monitor VGA para exibir informações simples sem a necessidade de módulos. E é este o assunto do post, onde controlaremos um monitor VGA com e sem biblioteca.

Para entender o procedimento a fundo, é imprescindível a leitura do post sobre como funciona o VGA.


Limitações

Antes de abordar a implementação do VGA, é importante citar as limitações de usar o Arduino para isto.

Velocidade insuficiente

No meu caso, utilizarei o Arduino UNO, o qual funciona com um clock de 16MHz. Isto quer dizer que o microcontrolador que ele utiliza (atm328p) é capaz de processar uma instrução a cada 62,5ns.

É um tempo muito pequeno, mas ainda assim não é suficiente para poder lidar com a formação correta da imagem no monitor. Isto porque, o tempo de cada pixel é de aproximadamente 40ns. Ou seja, enquanto o Arduino processaria 1 única instrução, já deu tempo do monitor desenhar quase 2 pixels.

A situação é ainda pior, pois na hora de desenhar a imagem, o microcontrolador executaria mais de 1 instrução, o que provocaria um atraso de vários pixels. Sendo assim, é inviável querer desenhar no monitor de forma completa.

Isto pode ser contornado utilizado um microcontrolador com um clock mais rápido. Outras placas Arduino que atendem à isto são: Arduino Zero, MKR1000, Arduino Due etc.

De qualquer forma, a implementação que faremos (com e sem biblioteca) apresenta esta limitação de escrita dos pixels.

Pixels

Devido ao problema anterior, a biblioteca que mostrarei cria previamente uma matriz com todas as informações necessárias para desenhar a tela. E esta matriz ocupa bastante espaço na memoria do Arduino.

Por conta disto, a resolução máxima que ela desenha é bem limitada: 120×60 pixels, podendo ser aumentada para 120×240 no Arduino Mega.

Formação das cores (sem biblioteca)

Conforme o padrão VGA, os canais das cores devem receber uma tensão de 0 a 0,7V. Mas, como o Arduino UNO não possui conversor DA (digital p/ analógico), utilizaremos um sinal de 0 a 5V. Sendo assim, teremos uma limitação nas cores exibidas.

Como temos 3 canais de cores, podemos ter 8 combinações de cores diferentes:

Vm = vermelho; Vd = verde; A = azul

  • Vm: 0, Vd: 0, A: 0 – Cor preta
  • Vm: 0, Vd: 0, A: 1 – Cor azul
  • Vm: 0, Vd: 1, A: 0 – Cor verde
  • Vm: 0, Vd: 1, A: 1 – Cor ciano
  • Vm: 1, Vd: 0, A: 0 – Cor vermelha
  • Vm: 1, Vd: 0, A: 1 – Cor magenta
  • Vm: 1, Vd: 1, A: 0 – Cor amarela
  • Vm: 1, Vd: 1, A: 1 – Cor branca

Formação das cores (com biblioteca)

No caso de utilizar a biblioteca, o mesmo problema anterior aparece, mas com um detalhe amais. Conforme disse, na biblioteca uma matriz é criada previamente para formar a imagem. E esta matriz pressupõe que cada pixel ocupa 2 bits.

Isto faz com que só seja possível utilizar 2 comandos para controlar os canais de cores. E estes 2 comandos (2 bits) são capazes de gerar apenas 4 cores, que dependem da forma como os canais são combinados (veremos mais adiante).


VGA sem biblioteca

O objetivo da implementação sem a biblioteca é apenas mostrar como a comunicação VGA pode ser feita. Não pretendo mostrar como escrever textos ou desenhar imagens, pois isto será feito com a biblioteca.

E os tempos envolvidos no funcionamento do VGA estão explicados neste post. Portanto, dê uma lida para entender o procedimento.

Circuito

Para o circuito, precisamos ligar os canais de cores um em cada pino digital do Arduino (pode ser qualquer pino digital). No caso dos pinos de sincronismo, utilizarei os timers do microcontrolador (atmega328p) para criar os pulsos via hardware.

Como os pulsos serão gerados pelo hardware interno do microcontrolador, não poderei escolher qualquer pino digital para os sincronismos. No caso, o pino de sincronismo vertical será ligado no pino digital 10 do Arduino e o horizontal no pino digital 3.

Estas ligações com o VGA precisam ser acompanhadas de alguns resistores em série. No caso dos canais de cores, recomenda-se usar resistores de 470Ω e, no caso dos sincronismos, resistores de 68Ω.

Veja como a ligação é feita com o conector VGA macho (consulte os pinos do conector VGA para saber onde ligar corretamente):

Circuito Arduino com VGA
Desenho do conector retirado daqui.

Para fazer a conexão com o conector VGA, utilizei alguns jumpers:

Conector VGA ligação com jumper

Acabou que eu não liguei todos os pinos de terra (5, 6, 7, 8 e 10) e mesmo assim consegui formar imagens.

Programação

Os tópicos adiante serão sobre a configuração dos timers internos do microcontrolador. Se você não sabe o que eles são, recomendo não dar muito enfoque para os próximos dois tópicos.

E as configurações são baseadas nos procedimentos deste artigo.

Gerando sincronismo vertical

O sinal do sincronismo vertical precisa ser um pulso com:

  • Período de 16,66ms (1/60).
  • Largura de 64μs.

Para isto, utilizarei o timer 1. E irei configurar ele no modo PWM rápido com a comutação do pino OC1B (pino digital 10) via hardware. Veja o código abaixo para configurar o timer 1 de acordo com especificado:

1
2
3
4
5
6
// Configura o timer 1 com prescaler de 1024, no modo fast PWM
TCCR1A = _BV(WGM10) | _BV(WGM11) | _BV(COM1B1);
TCCR1B = _BV(WGM12) | _BV(WGM13) | _BV(CS12) | _BV(CS10);
OCR1A = 259; // Período de 16,64‬ms
OCR1B = 0; // Largura de 64us (2 linhas)
TIMSK1 = _BV(TOIE1); // Habilita interrupção por estouro

Gerando sincronismo horizontal

O sinal do sincronismo horizontal precisa ser um pulso com:

  • Período de 31,75μs (1/(60*525)).
  • Largura de 3,8μs.

Para isto, utilizarei o timer 2. E irei configurar ele no modo PWM rápido com a comutação do pino OC2B (pino digital 3) via hardware. Veja o código abaixo para configurar o timer 2 de acordo com especificado:

1
2
3
4
5
6
// Configura o timer 2 com prescaler de 8, no modo fast PWM
TCCR2A = _BV(WGM20) | _BV(WGM21) | _BV(COM2B1);
TCCR2B = _BV(WGM22) | _BV(CS21);
OCR2A = 63; // Período de 32us
OCR2B = 7; // Largura de 4us (~96 pixels)
TIMSK2 = _BV(TOIE2); // Habilita interrupção por estouro

Função setup

Além das duas configurações anteriores, na inicialização do programa, precisamos fazer 3 outras coisas:

  • Parar o timer 0

O timer 0 no Arduino fica com a interrupção habilitada sendo executada no fundo do seu programa. E esta interrupção pode atrasar o procedimento de escrita na tela. Portanto, ela deve ser desligada.

Como não iremos utilizar o timer 2 para nada, resolvi desabilitar o timer direto (desabilitar sua contagem) com o comando:

TCCR0B = 0;

  • Configurar os pinos como saída

Como iremos precisar de economizar tempo nas instruções na hora de desenhar o vídeo, resolvi manipular os registradores de entrada/saída diretamente. Para isso, criei dois comandos (SET_BIT e CLR_BIT) no início do programa para me auxiliar com a manipulação dos registradores.

  • Habilitar as interrupções globais

Para isto, basta utilizar o comando “sei()” para habilitar as interrupções.

Função loop

O meu objetivo é criar, na função loop, uma forma de formar os pixels sem muita dor de cabeça. Para isso, criei uma variável chamada video_exibido que é 1 quando a comunicação está na região do vídeo exibido.

Além disto, criei outra variável chamada linha_atual, que informa o numero da linha atual da região do vídeo exibido.

No tópico seguinte, veremos como estas variáveis são manipuladas.

Com isto, a função loop fica com a seguinte estrutura:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void loop() {
  // Se está na região do vídeo exibido
  if (video_exibido)
  {
    video_exibido = 0;

    // Desenha a imagem aqui
  }

  // Após desenhar, limpa os canais de cores
  CLR_BIT(PORTD, pin_red);
  CLR_BIT(PORTD, pin_green);
  CLR_BIT(PORTD, pin_blue);
}

Interrupções

Estrutura de pixels do VGA
Referência do diagrama de vídeo gerado

As interrupções devem ser capazes de setar a variável video_exibido no momento em que a comunicação chegar na região de vídeo exibido. Sendo assim, criei uma variável chamada back_porch_vertical_restante que é inicializada com 35 em todo começo de tela e é decrementada a cada linha até chegar em 0.

Esta variável serve para contar a região do back porch vertical mais as 2 linhas de sincronismo vertical. Logo, quando esta variável chegar em 0, quer dizer que a comunicação saiu do back porch vertical. Após isto, a cada interrupção de linha, a variável linha_atual é incrementada, pois já estamos na região de vídeo exibido.

Com isto, se esta variável linha_atual for menor do que o tamanho vertical da tela (480), a interrupção seta a variável video_exibido. Assim, temos a funcionalidade que queríamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Interrupção que ocorre a cada inicio de tela
ISR(TIMER1_OVF_vect)
{
  // Reseta a posição da linha e do back porch vertical restante
  linha_atual = 0;
  back_porch_vertical_restante = VGA_BACK_PORCH_VERTICAL;
}

// Interrupção que ocorre a cada inicio de linha
ISR(TIMER2_OVF_vect)
{
  // Subtrai do back porch restante se ele for maior que 0
  if (back_porch_vertical_restante > 0)
  {
    back_porch_vertical_restante--;
  }
  else // Caso contrário, soma a linha atual
  {
    linha_atual++;
    // Se a linha atual estiver antes do front porch vertical, seta o video_exibido
    if (linha_atual < VGA_ALTURA)
    {
      video_exibido = 1;
    }
  }
}

Delay

Como desativamos o timer 0 do Arduino, não podemos mais utilizar as funções delay do Arduino. Para resolver este problema, podemos utilizar a instrução de “não operação” do microcontrolador (NOP). Esta instrução gasta 1 ciclo do clock (62,5ns para clock de 16MHz).

Mas ela não existe na linguagem “c” do Arduino, só na linguagem assembly. Portanto, temos que adicioná-la com a seguinte definição:

#define NOP asm (“nop\n\t”)

Após adicionar o comando acima, basta escrever “NOP” para criar um delay de 62,5ns. Além do comando acima, resolvi criar uma função para repetir este comando dentro de um for:

1
2
3
4
5
6
7
8
// Delay com a instrução de não operação do microcontrolador
void delay_nop(uint16_t tempo)
{
  for(uint16_t i=0; i<tempo; i++)
  {
    NOP;
  }
}

A função acima demora o tempo do for, mais o tempo dos comandos NOP executados, mais o retorno para o programa principal.

Desenhando algo

Com as implementações anteriores, já podemos desenhar alguma imagem no loop. Para isto, basta setar o pinos das cores correspondentes.

Como exemplo, resolvi colocar o primeiro terço da tela de verde, o segundo terço de vermelho e o último terço de azul. Consegui fazer isto com a ajuda da variável linha_atual que informa em qual linha a comunicação está. Veja o código completo para entender.

Código completo

Leia os comentários para entender o código por completo.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Define os pinos
#define pin_vsync   PB2
#define pin_hsync   PD3
#define pin_red     PD5
#define pin_green   PD6
#define pin_blue    PD7

// Comandos para facilitar a manipulação dos registradores
#define SET_BIT(PORT, PIN) PORT |= _BV(PIN)
#define CLR_BIT(PORT, PIN) PORT &= ~(_BV(PIN))

#define NOP asm ("nop\n\t")

// Parametros do padrão VGA
#define VGA_ALTURA              480
#define VGA_BACK_PORCH_VERTICAL 35


uint8_t video_exibido = 0;
volatile byte back_porch_vertical_restante;
volatile int linha_atual;

void setup()
{
  // Define os pinos como saída
  SET_BIT(DDRB, pin_vsync);
  SET_BIT(DDRD, pin_hsync);
  SET_BIT(DDRD, pin_red);
  SET_BIT(DDRD, pin_green);
  SET_BIT(DDRD, pin_blue);

  // Desabilita o clock do timer0
  TCCR0B = 0;

  // Configura o timer 1 com prescaler de 1024, no modo fast PWM
  TCCR1A = _BV(WGM10) | _BV(WGM11) | _BV(COM1B1);
  TCCR1B = _BV(WGM12) | _BV(WGM13) | _BV(CS12) | _BV(CS10);
  OCR1A = 259; // Período de 16,64‬ms
  OCR1B = 0; // Largura de 64us (2 linhas)
  TIMSK1 = _BV(TOIE1); // Habilita interrupção por estouro

  // Configura o timer 2 com prescaler de 8, no modo fast PWM
  TCCR2A = _BV(WGM20) | _BV(WGM21) | _BV(COM2B1);
  TCCR2B = _BV(WGM22) | _BV(CS21);
  OCR2A = 63; // Período de 32us
  OCR2B = 7; // Largura de 4us (~96 pixels)
  TIMSK2 = _BV(TOIE2); // Habilita interrupção por estouro

  sei(); // Habilita as interrupções
}

void loop() {
  // Se está na região do vídeo exibido
  if (video_exibido)
  {
    video_exibido = 0;

    // Desenha a imagem aqui
    if (linha_atual < VGA_ALTURA/3)
    {
      SET_BIT(PORTD, pin_green);
    }
    else if (linha_atual < 2*VGA_ALTURA/3)
    {
      SET_BIT(PORTD, pin_red);
    }
    else
    {
      SET_BIT(PORTD, pin_blue);
    }
    delay_nop(80); // Valor empirico para atrasar até o final da linha
  }
  // Após desenhar, limpa os canais de cores
  CLR_BIT(PORTD, pin_red);
  CLR_BIT(PORTD, pin_green);
  CLR_BIT(PORTD, pin_blue);
}

// Delay com a instrução de não operação do microcontrolador
void delay_nop(uint16_t tempo)
{
  for(uint16_t i=0; i<tempo; i++)
  {
    NOP;
  }
}

// Interrupção que ocorre a cada inicio de tela
ISR(TIMER1_OVF_vect)
{
  // Reseta a posição da linha e do back porch vertical restante
  linha_atual = 0;
  back_porch_vertical_restante = VGA_BACK_PORCH_VERTICAL;
}

// Interrupção que ocorre a cada inicio de linha
ISR(TIMER2_OVF_vect)
{
  // Subtrai do back porch restante se ele for maior que 0
  if (back_porch_vertical_restante > 0)
  {
    back_porch_vertical_restante--;
  }
  else // Caso contrário, soma a linha atual
  {
    linha_atual++;
    // Se a linha atual estiver antes do front porch vertical, seta o video_exibido
    if (linha_atual < VGA_ALTURA)
    {
      video_exibido = 1;
    }
  }
}

Resultados

A imagem formada no monitor VGA foi a seguinte:

Arduino controlando monitor VGA

Pode-se dizer que o procedimento deu certo. Entretanto, os últimos pixels da parte verde ficaram pretos e o mesmo ocorreu no início das demais cores. Um dos motivos deste problema são os if’s dentro do loop que atrasam a formação da imagem.

Resolvi criar outro código dentro do loop para gerar uma imagem contendo as 8 cores diferentes:

Arduino controlando monitor VGA com 8 cores

O código para gerar a imagem acima está no tópico abaixo.

Código para gerar 8 cores

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
// Define os pinos
#define pin_vsync   PB2
#define pin_hsync   PD3
#define pin_red     PD5
#define pin_green   PD6
#define pin_blue    PD7

// Comandos para facilitar a manipulação dos registradores
#define SET_BIT(PORT, PIN) PORT |= _BV(PIN)
#define CLR_BIT(PORT, PIN) PORT &= ~(_BV(PIN))

#define NOP asm ("nop\n\t")

// Parametros do padrão VGA
#define VGA_ALTURA              480
#define VGA_BACK_PORCH_VERTICAL 35


uint8_t video_exibido = 0;
volatile byte back_porch_vertical_restante;
volatile int linha_atual;

void setup()
{
  // Define os pinos como saída
  SET_BIT(DDRB, pin_vsync);
  SET_BIT(DDRD, pin_hsync);
  SET_BIT(DDRD, pin_red);
  SET_BIT(DDRD, pin_green);
  SET_BIT(DDRD, pin_blue);

  // Desabilita o clock do timer0
  TCCR0B = 0;

  // Configura o timer 1 com prescaler de 1024, no modo fast PWM
  TCCR1A = _BV(WGM10) | _BV(WGM11) | _BV(COM1B1);
  TCCR1B = _BV(WGM12) | _BV(WGM13) | _BV(CS12) | _BV(CS10);
  OCR1A = 259; // Período de 16,64‬ms
  OCR1B = 0; // Largura de 64us (2 linhas)
  TIMSK1 = _BV(TOIE1); // Habilita interrupção por estouro

  // Configura o timer 2 com prescaler de 8, no modo fast PWM
  TCCR2A = _BV(WGM20) | _BV(WGM21) | _BV(COM2B1);
  TCCR2B = _BV(WGM22) | _BV(CS21);
  OCR2A = 63; // Período de 32us
  OCR2B = 7; // Largura de 4us (~96 pixels)
  TIMSK2 = _BV(TOIE2); // Habilita interrupção por estouro

  sei(); // Habilita as interrupções
}

void loop() {
  // Se está na região do vídeo exibido
  if (video_exibido)
  {
    video_exibido = 0;

    // Desenha a imagem aqui
    delay_nop(4); // 001
    SET_BIT(PORTD, pin_blue);
    delay_nop(4); // 010
    SET_BIT(PORTD, pin_red);
    CLR_BIT(PORTD, pin_blue);
    delay_nop(4); // 011
    SET_BIT(PORTD, pin_blue);
    delay_nop(4); // 100
    SET_BIT(PORTD, pin_green);
    CLR_BIT(PORTD, pin_red);
    CLR_BIT(PORTD, pin_blue);
    delay_nop(4); // 101
    SET_BIT(PORTD, pin_blue);
    delay_nop(4); // 110
    SET_BIT(PORTD, pin_red);
    CLR_BIT(PORTD, pin_blue);
    delay_nop(4); // 111
    SET_BIT(PORTD, pin_green);
    SET_BIT(PORTD, pin_red);
    SET_BIT(PORTD, pin_blue);
    delay_nop(5);
  }
  // Após desenhar, limpa os canais de cores
  CLR_BIT(PORTD, pin_red);
  CLR_BIT(PORTD, pin_green);
  CLR_BIT(PORTD, pin_blue);
}

// Delay com a instrução de não operação do microcontrolador
void delay_nop(uint16_t tempo)
{
  for(uint16_t i=0; i<tempo; i++)
  {
    NOP;
  }
}

// Interrupção que ocorre a cada inicio de tela
ISR(TIMER1_OVF_vect)
{
  // Reseta a posição da linha e do back porch vertical restante
  linha_atual = 0;
  back_porch_vertical_restante = VGA_BACK_PORCH_VERTICAL;
}

// Interrupção que ocorre a cada inicio de linha
ISR(TIMER2_OVF_vect)
{
  // Subtrai do back porch restante se ele for maior que 0
  if (back_porch_vertical_restante > 0)
  {
    back_porch_vertical_restante--;
  }
  else // Caso contrário, soma a linha atual
  {
    linha_atual++;
    // Se a linha atual estiver antes do front porch vertical, seta o video_exibido
    if (linha_atual < VGA_ALTURA)
    {
      video_exibido = 1;
    }
  }
}

Sinais de sincronismo e de cor

As imagens dos sinais abaixo foram mostradas e explicadas no post sobre o funcionamento do VGA. Então, recomendo dar uma lida lá para entender.

Elas nada mais são do que os sinais do sincronismo vertical, horizontal e do canal de cor verde gerados pelo Arduino no primeiro código fornecido acima (3 faixas de cores).

VGA sinal real do sincronismo vertical e horizontal
VGA sinal real do sincronismo horizontal e do pixel
Pulso do sincronismo horizontal e canal da cor verde em alta durante o tempo de vídeo exibido

VGA com biblioteca

A biblioteca utilizada para controlar o VGA será esta. Basta baixar e colocar na pasta de bibliotecas do Arduino. 

Como foi dito anteriormente, a biblioteca utiliza uma matriz previamente construída para formar a imagem, e que ocupa bastante espaço na memoria. Então, os programas desenvolvidos utilizando a biblioteca devem ficar o mais enxuto possível.

Circuito

É praticamente o mesmo de antes, com as seguintes diferenças:

  • O pino de sincronismo vertical deve ser ligado no pino digital 9 do Arduino.
  • Só existem duas conexões para os canais de cores, que é a do pino 6 e 7 do Arduino. 

A ligação depende das cores que você quer que apareçam. Para saber o resultado das configurações, acesse a página da biblioteca e desça a página até a parte com o título “4 colors”.

A partir da lista de configurações, resolvi escolher a penúltima (cor preta, amarela, azul e branco):

Ligação do Arduino usando a biblioteca

Programação

A biblioteca possui um conjunto de exemplo interessantes para demonstrar como utilizar os comandos. Portanto, basta abri-los e testá-los.

Código com texto

O código abaixo apenas escreve um texto fixo na tela:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include <VGAX.h>

//font generated from BITFONZI - by Sandro Maffiodo
#define FNT_NANOFONT_HEIGHT 6
#define FNT_NANOFONT_SYMBOLS_COUNT 95
//data size=570 bytes
const unsigned char fnt_nanofont_data[FNT_NANOFONT_SYMBOLS_COUNT][1+FNT_NANOFONT_HEIGHT] PROGMEM={
{ 1, 128, 128, 128, 0, 128, 0, }, //glyph '!' code=0
{ 3, 160, 160, 0, 0, 0, 0, }, //glyph '"' code=1
{ 5, 80, 248, 80, 248, 80, 0, },  //glyph '#' code=2
{ 5, 120, 160, 112, 40, 240, 0, },  //glyph '$' code=3
{ 5, 136, 16, 32, 64, 136, 0, },  //glyph '%' code=4
{ 5, 96, 144, 104, 144, 104, 0, },  //glyph '&' code=5
{ 2, 128, 64, 0, 0, 0, 0, },  //glyph ''' code=6
{ 2, 64, 128, 128, 128, 64, 0, }, //glyph '(' code=7
{ 2, 128, 64, 64, 64, 128, 0, },  //glyph ')' code=8
{ 3, 0, 160, 64, 160, 0, 0, },  //glyph '*' code=9
{ 3, 0, 64, 224, 64, 0, 0, }, //glyph '+' code=10
{ 2, 0, 0, 0, 0, 128, 64, },  //glyph ',' code=11
{ 3, 0, 0, 224, 0, 0, 0, }, //glyph '-' code=12
{ 1, 0, 0, 0, 0, 128, 0, }, //glyph '.' code=13
{ 5, 8, 16, 32, 64, 128, 0, },  //glyph '/' code=14
{ 4, 96, 144, 144, 144, 96, 0, }, //glyph '0' code=15
{ 3, 64, 192, 64, 64, 224, 0, },  //glyph '1' code=16
{ 4, 224, 16, 96, 128, 240, 0, }, //glyph '2' code=17
{ 4, 224, 16, 96, 16, 224, 0, },  //glyph '3' code=18
{ 4, 144, 144, 240, 16, 16, 0, }, //glyph '4' code=19
{ 4, 240, 128, 224, 16, 224, 0, },  //glyph '5' code=20
{ 4, 96, 128, 224, 144, 96, 0, }, //glyph '6' code=21
{ 4, 240, 16, 32, 64, 64, 0, }, //glyph '7' code=22
{ 4, 96, 144, 96, 144, 96, 0, },  //glyph '8' code=23
{ 4, 96, 144, 112, 16, 96, 0, },  //glyph '9' code=24
{ 1, 0, 128, 0, 128, 0, 0, }, //glyph ':' code=25
{ 2, 0, 128, 0, 0, 128, 64, },  //glyph ';' code=26
{ 3, 32, 64, 128, 64, 32, 0, }, //glyph '<' code=27
{ 3, 0, 224, 0, 224, 0, 0, }, //glyph '=' code=28
{ 3, 128, 64, 32, 64, 128, 0, },  //glyph '>' code=29
{ 4, 224, 16, 96, 0, 64, 0, },  //glyph '?' code=30
{ 4, 96, 144, 176, 128, 112, 0, },  //glyph '@' code=31
{ 4, 96, 144, 240, 144, 144, 0, },  //glyph 'A' code=32
{ 4, 224, 144, 224, 144, 224, 0, }, //glyph 'B' code=33
{ 4, 112, 128, 128, 128, 112, 0, }, //glyph 'C' code=34
{ 4, 224, 144, 144, 144, 224, 0, }, //glyph 'D' code=35
{ 4, 240, 128, 224, 128, 240, 0, }, //glyph 'E' code=36
{ 4, 240, 128, 224, 128, 128, 0, }, //glyph 'F' code=37
{ 4, 112, 128, 176, 144, 112, 0, }, //glyph 'G' code=38
{ 4, 144, 144, 240, 144, 144, 0, }, //glyph 'H' code=39
{ 3, 224, 64, 64, 64, 224, 0, },  //glyph 'I' code=40
{ 4, 240, 16, 16, 144, 96, 0, },  //glyph 'J' code=41
{ 4, 144, 160, 192, 160, 144, 0, }, //glyph 'K' code=42
{ 4, 128, 128, 128, 128, 240, 0, }, //glyph 'L' code=43
{ 5, 136, 216, 168, 136, 136, 0, }, //glyph 'M' code=44
{ 4, 144, 208, 176, 144, 144, 0, }, //glyph 'N' code=45
{ 4, 96, 144, 144, 144, 96, 0, }, //glyph 'O' code=46
{ 4, 224, 144, 224, 128, 128, 0, }, //glyph 'P' code=47
{ 4, 96, 144, 144, 144, 96, 16, },  //glyph 'Q' code=48
{ 4, 224, 144, 224, 160, 144, 0, }, //glyph 'R' code=49
{ 4, 112, 128, 96, 16, 224, 0, }, //glyph 'S' code=50
{ 3, 224, 64, 64, 64, 64, 0, }, //glyph 'T' code=51
{ 4, 144, 144, 144, 144, 96, 0, },  //glyph 'U' code=52
{ 3, 160, 160, 160, 160, 64, 0, },  //glyph 'V' code=53
{ 5, 136, 168, 168, 168, 80, 0, },  //glyph 'W' code=54
{ 4, 144, 144, 96, 144, 144, 0, },  //glyph 'X' code=55
{ 3, 160, 160, 64, 64, 64, 0, },  //glyph 'Y' code=56
{ 4, 240, 16, 96, 128, 240, 0, }, //glyph 'Z' code=57
{ 2, 192, 128, 128, 128, 192, 0, }, //glyph '[' code=58
{ 5, 128, 64, 32, 16, 8, 0, },  //glyph '\' code=59
{ 2, 192, 64, 64, 64, 192, 0, },  //glyph ']' code=60
{ 5, 32, 80, 136, 0, 0, 0, }, //glyph '^' code=61
{ 4, 0, 0, 0, 0, 240, 0, }, //glyph '_' code=62
{ 2, 128, 64, 0, 0, 0, 0, },  //glyph '`' code=63
{ 3, 0, 224, 32, 224, 224, 0, },  //glyph 'a' code=64
{ 3, 128, 224, 160, 160, 224, 0, }, //glyph 'b' code=65
{ 3, 0, 224, 128, 128, 224, 0, }, //glyph 'c' code=66
{ 3, 32, 224, 160, 160, 224, 0, },  //glyph 'd' code=67
{ 3, 0, 224, 224, 128, 224, 0, }, //glyph 'e' code=68
{ 2, 64, 128, 192, 128, 128, 0, },  //glyph 'f' code=69
{ 3, 0, 224, 160, 224, 32, 224, },  //glyph 'g' code=70
{ 3, 128, 224, 160, 160, 160, 0, }, //glyph 'h' code=71
{ 1, 128, 0, 128, 128, 128, 0, }, //glyph 'i' code=72
{ 2, 0, 192, 64, 64, 64, 128, },  //glyph 'j' code=73
{ 3, 128, 160, 192, 160, 160, 0, }, //glyph 'k' code=74
{ 1, 128, 128, 128, 128, 128, 0, }, //glyph 'l' code=75
{ 5, 0, 248, 168, 168, 168, 0, }, //glyph 'm' code=76
{ 3, 0, 224, 160, 160, 160, 0, }, //glyph 'n' code=77
{ 3, 0, 224, 160, 160, 224, 0, }, //glyph 'o' code=78
{ 3, 0, 224, 160, 160, 224, 128, }, //glyph 'p' code=79
{ 3, 0, 224, 160, 160, 224, 32, },  //glyph 'q' code=80
{ 3, 0, 224, 128, 128, 128, 0, }, //glyph 'r' code=81
{ 2, 0, 192, 128, 64, 192, 0, },  //glyph 's' code=82
{ 3, 64, 224, 64, 64, 64, 0, }, //glyph 't' code=83
{ 3, 0, 160, 160, 160, 224, 0, }, //glyph 'u' code=84
{ 3, 0, 160, 160, 160, 64, 0, },  //glyph 'v' code=85
{ 5, 0, 168, 168, 168, 80, 0, },  //glyph 'w' code=86
{ 3, 0, 160, 64, 160, 160, 0, },  //glyph 'x' code=87
{ 3, 0, 160, 160, 224, 32, 224, },  //glyph 'y' code=88
{ 2, 0, 192, 64, 128, 192, 0, },  //glyph 'z' code=89
{ 3, 96, 64, 192, 64, 96, 0, }, //glyph '{' code=90
{ 1, 128, 128, 128, 128, 128, 0, }, //glyph '|' code=91
{ 3, 192, 64, 96, 64, 192, 0, },  //glyph '}' code=92
{ 3, 96, 192, 0, 0, 0, 0, },  //glyph '~' code=93
{ 4, 48, 64, 224, 64, 240, 0, },  //glyph '£' code=94
};
VGAX vga;

// Texto para escrever na tela
static const char str0[] PROGMEM="MUNDO PROJETADO!";
static const char str1[] PROGMEM="controlando o monitor VGA";
static char x=-VGAX_WIDTH;

void setup() {
  vga.begin();
  vga.clear(0b0);

  vga.clear(0b11);
  vga.printPROGMEM((byte*)fnt_nanofont_data, FNT_NANOFONT_SYMBOLS_COUNT, FNT_NANOFONT_HEIGHT, 3, 1, str0, 20, VGAX_HEIGHT/2-5, 2);
  vga.printPROGMEM((byte*)fnt_nanofont_data, FNT_NANOFONT_SYMBOLS_COUNT, FNT_NANOFONT_HEIGHT, 3, 1, str1, 7, VGAX_HEIGHT/2+4, 1);
}
void loop() {
}

Resultado:

Escrevendo um texto no monitor

Desenhando imagem personalizada

A biblioteca fornece a possibilidade de você adicionar a sua própria imagem no código. Para isso você precisa utilizar a ferramenta de conversão de imagem em bits que a biblioteca fornece.

Essa ferramenta é um arquivo html (2bitimagem.html) que fica dentro da pasta tools da biblioteca. Basta abrir o arquivo (com o navegador) e seguir os passos descritos na janela que aparecer. E a imagem utilizada para conversão deve estar já na resolução certa (120×60).

No meu caso, peguei a logo do site, redimensionei para 120×60 e mandei converter:

Ferramenta da biblioteca para converter a imagem em bits

Feito isto, bastou copiar o código gerado, colar no Arduino e desenhar a imagem. A imagem gerada pode ser vista na figura baixo:

Monitor VGA com a logo do site

É um resultado até bom se considerarmos as limitações de cores e resolução.

Enfim, o código utilizado para gerar a imagem convertida está mostrado no tópico seguinte.

Código para desenhar imagem

1
2
3
4
5
6
7
#include <VGAX.h>

//image generated from 2BITIMAGE - by Sandro Maffiodo
#define IMG_MUNDOPROJETADO_WIDTH 120
#define IMG_MUNDOPROJETADO_BWIDTH 30
#define IMG_MUNDOPROJETADO_HEIGHT 60
//data size=1800 bytes