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):
Para fazer a conexão com o conector VGA, utilizei alguns jumpers:
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,64ms 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
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,64ms 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:
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:
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,64ms 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 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):
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:
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:
Feito isto, bastou copiar o código gerado, colar no Arduino e desenhar a imagem. A imagem gerada pode ser vista na figura baixo:
É 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 |
Fantástico, obrigado por compartilhar mais este conhecimento. Nem imaginava ser possível o arduino fazer esta tarefa.
Eu é que agradeço pelo seu retorno positivo! O Arduino é até bem versátil, só precisamos descobrir como implementar as ferramentas nele!
Muito legal, me fez recordar o projeto que fiz lá pelos anos 80, apenas por brincadeira, de gerar também uma imagem de vídeo utilizando apenas CIs discretos (um oscilador e contadores e uma mémória EPROM) em dois protoboards! Naquela época tínhamos computadores pessoais baseador no Z80 (CISC e com clock bem menor) que liam teclado, geram vídeo e ainda processavam nossos programas!
Que bacana, Marcos! Consigo só imaginar o trabalhão que era montar um projeto destes. Mas com certeza aprendia bastante assim né. O bom é que hoje em dia fica fácil pro pessoal mais novo entrar neste mundo da eletrônica, graças aos facilitadores, como é o caso do Arduino. Abraço.
me ajuda?
eu não consegui fazer com que as imagens apaream 🙁 tem algum modo no qual preciso colocar o monitor???
não consegui exibir as imagens 🙁
Olá, Guilherme. Ajudo sim! No meu caso não precisei colocar o monitor em nenhum modo, funcionou diretamente. Um possível problema é de ligação, então é sempre bom conferir o circuito várias vezes para ter certeza que não há mal contato ou fios ligados em pinos diferentes. E, por acaso, você está utilizando a biblioteca ou o código que desenvolvi?
era um problema de ligação kkkk muito obrigado pela atenção e pelo site que me ajudou demais
agradeço desde já.
obrigado.
Menos mal então kkk. Por nada, Guilherme. Espero que os demais conteúdos do site possam te ajudar também!
Me tira uma duvida, por acaso tu sabe como posso posicionar alguma imagem em diferentes pontos do monitor utilizando a biblioteca?
Olá, Matheus. Dei uma lida aqui na biblioteca, mas não vi um jeito de fazer isso. Acho que dá pra fazer uma gambiarra e colocar sua imagem deslocada dentro de outra imagem de tamanho 120×60 (tamanho total da tela). Não sei se deu pra entender.
tu não conjuga na terceira pessoa…
Seria possível obter uma saída de 24 linhas e 80 colunas?
Tenho um cenário de um terminal/console TELNET com as seguintes resoluções de tela em (LINHAS, COLUNAS):
(24 x 80)
(32 x 80)
(43 x 80)
Se eu conseguir uma saída de pelo menos 24×80, posso reescrever o protocolo do TELNET para usar um Arduino como terminal.