terça-feira, maio 10, 2016

Geração de Vídeo com Microcontroladores AVR - Parte 2

Vamos começar os nossos experimentos com algo bem básico: gerar um sinal de vídeo composto branco e preto com um mínimo de hardware. A base para o código são os exemplos da Cornell.



Entendendo o Vídeo Composto B&P

O sinal de vídeo composto utiliza três níveis de tensão: 1V corresponde a branco, 0,3V corresponde a preto e 0V indica um sinal de sincronismo. Tons de cinza podem ser representados por tensões entre 0,3 e 1V.

A coisa complica um pouco quando começamos a falar nas temporizações, já que existem vários padrões. O Brasil segue as temporizações de sincronismo do padrão americano, o NTSC. No caso de vídeo branco e preto, o nosso sistema é igual ao americano. Para vídeo colorido o Brasil fez uma mescla entre o padrão NTSC (para ficar compatível com as TV b&p já existentes) e o padrão PAL (que possui uma codificação melhor para cores), criando o PAL-M.Como vamos ficar no b&p, podemos usar informações sobre o sistema americano, como aqui e aqui.

Uma tela é dividida em 525 linhas, que são enviadas em dois frames de 262,5 linhas a uma taxa de 60 frames por segundo. Na TV temos o chamado vídeo entrelaçado, onde um frame envia as linhas pares e o seguinte as linhas ímpares. Vamos trabalhar com vídeo não entrelaçado, enviando sempre as mesmas 262 linhas em cada frame.Destas 262 linhas, apenas as 20 últimas correspondem ao retraço vertical (quando o feixe está se movimentando rapidamente de baixo para cima).

O sinal para as primeiras 242 linhas começa com um pulso de sincronismo horizontal (sinal em 0V) com duração de 4,7 uS. Em seguida temos o "back porch", um período de 5,9 uS onde o sinal deve ser mantido em 0,3V. Chegamos então à parte visível da linha, com 51.5 uS, onde o sinal pode variar ente 0,3 e 1V. Fechando a linha temos o "front porch" onde o sinal deve ser mantido em 0,3V por 1.4uS. Somando todos estes tempos obtemos 63,5 uS. Na prática a nossa imagem deve ocupar da linha 30 até a linha 230, deixando margens apagadas em cima e em baixo.

Durante o retraço vertical o sinal de vídeo corresponde a linhas apagadas "inventidas": 4,7 uS no nível 0,3V seguindo de 58,8 uS no nível 0V.

Hardware

Para obter um sinal preciso é recomendado usar um clock de 20 MHz dividido por um timer de 16 bits. Com isto podemos gerar uma base de tempo de 63,55 dividindo o clock por 1271 (este tempo multiplicado por 262 ficará extremamente próximo dos 60 frames por segundo.

A minha ideia inicial era usar um ATtinyx5 (de 8 pinos), porém ele não tem o timer de 16 bits. Optei então por um ATtiny44.



O sinal de vídeo é gerado por duas saídas digitais (PA0 e PA1) ligadas através de diodos a um divisor resistivo. Com isto conseguimos gerar os três níveis de interesse:
  • 0V: PA0 e PA em 0
  • 0,3V: PA0 em 1 e PA1 em 0
  • 1,0V: PA0 e PA em 1

    Software

    A ideia básica é programar o timer para interromper a cada 63,5 uS; o sinal de vídeo será controlado dentro da rotina de interrupção. O primeiro passo da rotina é gerar o pulso de sincronismo horizontal. Se estivermos fora da região visível podemos encerrar a interrupção, caso contrário vamos gerar o sinal de vídeo conforme os pixels a apresentar (o que será feito em assembler para ter a velocidade e precisão necessárias).

    Para que o timing seja preciso e a imagem não tenha interferências, as interrupções durante as linhas visíveis deve acontecer quando o processador estiver dormindo. Com isto sobra só uma parte do tempo do "front porch" nas primeiras 242 linhas. Durante o retraço vertical podemos relaxar um pouco; teremos um pouco menos de 20 * (63,5 - 4,7)  para gerar o próximo frame (ou seja, cerca de 1,1 ms a cada 16,7 ms).

    O ATtiny44 não tem Ram suficiente para guardar uma imagem da tela. O exemplo da Cornell implementa uma resolução de 144 x 200 pontos, o que exige 3600 bytes. Para ficar no simples, vamos construir nossa imagem utilizando somente duas linhas repetidas várias vezes.

    Para gerar um pixel, vamos manter PA0 em 1 e posicionar PA1 em 0 (pixel apagado) ou em 1 (pixel aceso). A sequência de instruções assembler gera o sinal correspondente ao bit b do byte contido no registrador R4, supondo que R30 contenha 1:

    BST R4,b
    BLD R30,1
    OUT 0x1B,R30
    NOP
    NOP
    

    A primeira instrução (bit store) move o bit b para o flag T. A segunda (bit load) move o flag T para o vbit de R30. A instrução OUT escreve o valor de R30 no registrador PORTA. Os dois NOPs finais são para dar o tempo exato de um pixel.

    A macro videobits contem oito vezes esta sequência, para gerar o video correspondente a os oito bits de R4. A rotina byteblast utiliza 18 vezes a macro videobits para gerar o sinal de uma linha. Desta forma é obtida a velocidade e precisão necessárias, ao custo de um código longo (o meu programa ocupou 3262 bytes dos 4K disponíveis).

    A rotina de interrupção ficou assim:
    1. ISR (TIM1_COMPA_vect)  
    2. {  
    3.     static uint8_t syncON = VID_0V;  
    4.     static uint8_t syncOFF = VID_03V;  
    5.       
    6.     // Gera o pulso de sync  
    7.     VID_PORT = syncON;  
    8.     linhaAtual++;  
    9.     if (linhaAtual == iniSyncV)  
    10.     {  
    11.         // Sync vertical é invertido  
    12.         syncON = VID_03V;  
    13.         syncOFF = VID_0V;  
    14.     }  
    15.     if (linhaAtual == fimSyncV)  
    16.     {  
    17.         // Voltar ao sync normal  
    18.         syncON = VID_0V;  
    19.         syncOFF = VID_03V;  
    20.     }  
    21.  if (linhaAtual == fimFrame)  
    22.     {  
    23.         // Fim do frame  
    24.   linhaAtual = 1;  
    25.     }  
    26.  _delay_us(2);   // Aguarda fim do tempo do pulso  
    27.  VID_PORT = syncOFF;  
    28.   
    29.     // Gerar a imagem  
    30.  if ((linhaAtual <= ultLinha) && (linhaAtual >= primLinha))  
    31.     {  
    32.         pontLinha = linhas[(linhaAtual-primLinha) & 3];  
    33.   
    34.   _delay_us(12);  // tempinho para centrar a linha  
    35.   
    36.   byteblast();  
    37.  }        
    38. }  
    O programa principal é bem simples, já que a imagem é estática:
    1. int main(void)  
    2. {  
    3.     uint8_t i;  
    4.       
    5.     // inicia as e/s digitais  
    6.     VID_DDR  = VID_PINOS;  
    7.     VID_PORT = VID_0V;  
    8.       
    9.     // começar na linha 1  
    10.     linhaAtual = 1;  
    11.       
    12.     // prepara as nossas duas linhas  
    13.     for (i = 0; i < nBytesLinha; i++)  
    14.     {  
    15.         linha1[i] = 0xFF;  
    16.         linha2[i] = 0x81;  
    17.     }  
    18.       
    19.     // inicia o timer1 para interromper a cada linha  
    20.     // clock máximo, zera contador e interrompe quando   
    21.     // atinge a contagem em OCR1A  
    22.     OCR1A = tempoLinha;  
    23.     TCCR1B = 0x09;  
    24.     TCCR1A = 0x00;  
    25.     TIMSK1 = 0x02;  
    26.       
    27.     // Habilitar interrupções e preparar para dormir  
    28.     sei();  
    29.     set_sleep_mode(SLEEP_MODE_IDLE);  
    30.     sleep_enable();  
    31.   
    32.     // O loop abaixo executa uma vez para cada linha  
    33.     for(;;)   
    34.     {  
    35.         // Precisa estar dormindo quando chegar a interrupção  
    36.         sleep_cpu();  
    37.         if (linhaAtual == (ultLinha+2))   
    38.         {   
    39.             // Retraço vertical  
    40.             // aqui temos um tempinho para gerar a próxima tela  
    41.         }  
    42.     }  
    43. }  
    O projeto completo pode ser baixado dos arquivos do blog, está em VidBP.zip.

    Um comentário:

    Alexandre Souza - PU2SEX disse...

    Ainda nem li o artigo, mas curti o tv :)