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:
    ISR (TIM1_COMPA_vect)
    {
        static uint8_t syncON = VID_0V;
        static uint8_t syncOFF = VID_03V;
        
        // Gera o pulso de sync
        VID_PORT = syncON;
        linhaAtual++;
        if (linhaAtual == iniSyncV)
        {
            // Sync vertical é invertido
            syncON = VID_03V;
            syncOFF = VID_0V;
        }
        if (linhaAtual == fimSyncV)
        {
            // Voltar ao sync normal
            syncON = VID_0V;
            syncOFF = VID_03V;
        }
     if (linhaAtual == fimFrame)
        {
            // Fim do frame
      linhaAtual = 1;
        }
     _delay_us(2);   // Aguarda fim do tempo do pulso
     VID_PORT = syncOFF;
    
        // Gerar a imagem
     if ((linhaAtual <= ultLinha) && (linhaAtual >= primLinha))
        {
            pontLinha = linhas[(linhaAtual-primLinha) & 3];
    
      _delay_us(12);  // tempinho para centrar a linha
    
      byteblast();
     }      
    }
    O programa principal é bem simples, já que a imagem é estática:
    int main(void)
    {
        uint8_t i;
        
        // inicia as e/s digitais
        VID_DDR  = VID_PINOS;
        VID_PORT = VID_0V;
        
        // começar na linha 1
        linhaAtual = 1;
        
        // prepara as nossas duas linhas
        for (i = 0; i < nBytesLinha; i++)
        {
            linha1[i] = 0xFF;
            linha2[i] = 0x81;
        }
        
        // inicia o timer1 para interromper a cada linha
        // clock máximo, zera contador e interrompe quando 
        // atinge a contagem em OCR1A
        OCR1A = tempoLinha;
        TCCR1B = 0x09;
        TCCR1A = 0x00;
        TIMSK1 = 0x02;
        
        // Habilitar interrupções e preparar para dormir
        sei();
        set_sleep_mode(SLEEP_MODE_IDLE);
        sleep_enable();
    
        // O loop abaixo executa uma vez para cada linha
        for(;;) 
        {
            // Precisa estar dormindo quando chegar a interrupção
            sleep_cpu();
            if (linhaAtual == (ultLinha+2)) 
            { 
                // Retraço vertical
                // aqui temos um tempinho para gerar a próxima tela
            }
        }
    }
    O projeto completo pode ser baixado dos arquivos do blog, está em VidBP.zip.

    Um comentário:

    Alexandre Souza - PU1BZZ disse...

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