quinta-feira, março 10, 2016

Lanterna com Anel de LEDs RGB - Firmware

Fechando esta série veremos a alma do projeto - o firmware.

É difícil colocar uma ilustração do firmware...

No projeto da Make foi usada uma biblioteca da Adafruit para controle dos LEDs.  Eu me debrucei sobre os fontes e gerei uma versão especializada para a operação a 20MHz.

Como falei antes, os tempos são bem apertados e é preciso apelar para assembly. O código da biblioteca é muito elegante e parte da ideia que, no envio de um bit, existem três pontos onde o sinal pode ser alterado:
  • no início do bit o sinal é colocado em nível alto (H)
  • após 450ns o sinal é alterado para nível baixo se for um bit "0" ou mantido em nível alto se for um bit "1" (x)
  • após outros 350nS o sinal sempre deve ser colocado em nível baixo (L)
Trabalhando a 20MHz, cada ciclo do processador dura 50ns, o que nos leva à seguinte representação:

HHHHHHHHHxxxxxxxLLLLLLLLL
^        ^      ^

Indicando que o sinal deve alterado a 0, 9 e 16 ciclos do início. Aí é hora de contar os ciclos de cada instrução, lembrando que um desvio gasta 1 ou 2 ciclos e tomando o cuidado das contagens serem constantes em todos os caminhos possíveis. Um truque adicional, para conseguir a velocidade necessária, é pré-computar as programações para sinal alto e baixo. O resultado, na horrível notação para código assemby do avr-gcc, é isto aqui:
// Rotina para atualizar os LEDs
// 12 LEDs RGB, controlador WS2812B
// bit 0: 400nS high, 850nS low
// bit 1: 800nS high, 450nS low
// Vamos aproveitar as tolerâncias e trabalhar com 450/800
// A rotina abaixo é uma adaptação de código da biblioteca NeoPixel
// da Adafruit https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp
// Garantir mínimo de 50uSeg entre chamadas
static void refresh()
{
    volatile uint16_t i = 3*12;     // Contador de bytes
    volatile uint8_t bit = 8;       // Contador de bits
    volatile uint8_t *ptr = pixels; // Ponteiro para os bytes
    volatile uint8_t  b = *ptr++;   // Byte atual
    volatile uint8_t  hi, lo;       // Programações do port
    volatile uint8_t next;          // Próxima transição
    volatile uint8_t *port = &LED_PORT;
    
    cli();      // Sem interrupções daqui para frente
    hi = *port | LED_BIT;
    lo = *port & ~LED_BIT;
    next = lo;
    if (b & 0x80) 
        next = hi;    

    // Assembly para contar os ciclos
    // A 20MHz, cada ciclo dura 50nS
    // Um bit (1,25uS) corresponde a 25 cliclos
    // HHHHHHHHHxxxxxxxLLLLLLLLL
    // ^        ^      ^          mudanças em 0, 9 e 16 ciclos
    asm volatile(
         "head25:"                   "\n\t" // Clk  Pseudocode    (T =  0)
          "st   %a[port],  %[hi]"    "\n\t" // 2    PORT = hi     (T =  2)
          "sbrc %[byte],  7"         "\n\t" // 1-2  if(b & 128)
          "mov  %[next], %[hi]"      "\n\t" // 1-0   next = hi    (T =  4)
          "dec  %[bit]"              "\n\t" // 1    bit--         (T =  5)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T =  7)
          "st   %a[port],  %[next]"  "\n\t" // 2    PORT = next   (T =  9)
          "mov  %[next] ,  %[lo]"    "\n\t" // 1    next = lo     (T =  10)
          "breq nextbyte25"          "\n\t" // 1-2  if(bit == 0) (from dec above)
          "rol  %[byte]"             "\n\t" // 1    b <<= 1       (T = 12)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 14)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 16)
          "st   %a[port],  %[lo]"    "\n\t" // 2    PORT = lo     (T = 18)
          "nop"                      "\n\t" // 1    nop           (T = 19)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 21)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 23)
          "rjmp head25"              "\n\t" // 2    -> head25 (next bit out)
          
         "nextbyte25:"               "\n\t" //                    (T = 12)
          "ldi  %[bit]  ,  8"        "\n\t" // 1    bit = 8       (T = 13)
          "ld   %[byte] ,  %a[ptr]+" "\n\t" // 2    b = *ptr++    (T = 15)
          "nop"                      "\n\t" // 1    nop           (T = 16)
          "st   %a[port],  %[lo]"    "\n\t" // 2    PORT = lo     (T = 18)
          "nop"                      "\n\t" // 1    nop           (T = 19)
          "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 21)
          "sbiw %[count], 1"         "\n\t" // 2    i--           (T = 23)
           "brne head25"             "\n"   // 2    if(i != 0) -> (next byte)
          : [port]  "+e" (port),
            [byte]  "+r" (b),
            [bit]   "+r" (bit),
            [next]  "+r" (next),
            [count] "+w" (i)
          : [ptr]    "e" (ptr),
            [hi]     "r" (hi),
            [lo]     "r" (lo));    
    
    sei();     // Permite novamente interrupções
}
Reparem que as interrupções precisam ser desabilitadas durante este trecho crítico. Feito isto, o resto parece muito fácil:
  • O programa principal fica em loop processando os potenciômetros e atualizando os LEDs.
  • O processamento dos potenciômetros consiste em amostrar com o ADC e considerar os valores em faixas:
    • Para o modo, existem quatro faixas (apagado, pisca, roda e aceso). Nos modos pisca e roda a posição dentro da faixa determina a velocidade.
    • Para a cor temos seis faixas (apagado, vermelho, verde, azul, "cinza" e branco). Para vermelho, verde, azul e "cinza" a posição dentro da faixa determina a intensidade
  • A atualização dos LEDs consiste em atualizar um vetor com os bytes a serem enviados para os LEDs. A atualização considera o modo atual, a velocidade é implementada através de delays gerados com a interrupção do timer.


O código completo pode ser examinado nos arquivos do blog, em Lanterna.zip.

Nenhum comentário: