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:
  1. // Rotina para atualizar os LEDs  
  2. // 12 LEDs RGB, controlador WS2812B  
  3. // bit 0: 400nS high, 850nS low  
  4. // bit 1: 800nS high, 450nS low  
  5. // Vamos aproveitar as tolerâncias e trabalhar com 450/800  
  6. // A rotina abaixo é uma adaptação de código da biblioteca NeoPixel  
  7. // da Adafruit https://github.com/adafruit/Adafruit_NeoPixel/blob/master/Adafruit_NeoPixel.cpp  
  8. // Garantir mínimo de 50uSeg entre chamadas  
  9. static void refresh()  
  10. {  
  11.     volatile uint16_t i = 3*12;     // Contador de bytes  
  12.     volatile uint8_t bit = 8;       // Contador de bits  
  13.     volatile uint8_t *ptr = pixels; // Ponteiro para os bytes  
  14.     volatile uint8_t  b = *ptr++;   // Byte atual  
  15.     volatile uint8_t  hi, lo;       // Programações do port  
  16.     volatile uint8_t next;          // Próxima transição  
  17.     volatile uint8_t *port = &LED_PORT;  
  18.       
  19.     cli();      // Sem interrupções daqui para frente  
  20.     hi = *port | LED_BIT;  
  21.     lo = *port & ~LED_BIT;  
  22.     next = lo;  
  23.     if (b & 0x80)   
  24.         next = hi;      
  25.   
  26.     // Assembly para contar os ciclos  
  27.     // A 20MHz, cada ciclo dura 50nS  
  28.     // Um bit (1,25uS) corresponde a 25 cliclos  
  29.     // HHHHHHHHHxxxxxxxLLLLLLLLL  
  30.     // ^        ^      ^          mudanças em 0, 9 e 16 ciclos  
  31.     asm volatile(  
  32.          "head25:"                   "\n\t" // Clk  Pseudocode    (T =  0)  
  33.           "st   %a[port],  %[hi]"    "\n\t" // 2    PORT = hi     (T =  2)  
  34.           "sbrc %[byte],  7"         "\n\t" // 1-2  if(b & 128)  
  35.           "mov  %[next], %[hi]"      "\n\t" // 1-0   next = hi    (T =  4)  
  36.           "dec  %[bit]"              "\n\t" // 1    bit--         (T =  5)  
  37.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T =  7)  
  38.           "st   %a[port],  %[next]"  "\n\t" // 2    PORT = next   (T =  9)  
  39.           "mov  %[next] ,  %[lo]"    "\n\t" // 1    next = lo     (T =  10)  
  40.           "breq nextbyte25"          "\n\t" // 1-2  if(bit == 0) (from dec above)  
  41.           "rol  %[byte]"             "\n\t" // 1    b <<= 1       (T = 12)  
  42.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 14)  
  43.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 16)  
  44.           "st   %a[port],  %[lo]"    "\n\t" // 2    PORT = lo     (T = 18)  
  45.           "nop"                      "\n\t" // 1    nop           (T = 19)  
  46.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 21)  
  47.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 23)  
  48.           "rjmp head25"              "\n\t" // 2    -> head25 (next bit out)  
  49.             
  50.          "nextbyte25:"               "\n\t" //                    (T = 12)  
  51.           "ldi  %[bit]  ,  8"        "\n\t" // 1    bit = 8       (T = 13)  
  52.           "ld   %[byte] ,  %a[ptr]+" "\n\t" // 2    b = *ptr++    (T = 15)  
  53.           "nop"                      "\n\t" // 1    nop           (T = 16)  
  54.           "st   %a[port],  %[lo]"    "\n\t" // 2    PORT = lo     (T = 18)  
  55.           "nop"                      "\n\t" // 1    nop           (T = 19)  
  56.           "rjmp .+0"                 "\n\t" // 2    nop nop       (T = 21)  
  57.           "sbiw %[count], 1"         "\n\t" // 2    i--           (T = 23)  
  58.            "brne head25"             "\n"   // 2    if(i != 0) -> (next byte)  
  59.           : [port]  "+e" (port),  
  60.             [byte]  "+r" (b),  
  61.             [bit]   "+r" (bit),  
  62.             [next]  "+r" (next),  
  63.             [count] "+w" (i)  
  64.           : [ptr]    "e" (ptr),  
  65.             [hi]     "r" (hi),  
  66.             [lo]     "r" (lo));      
  67.       
  68.     sei();     // Permite novamente interrupções  
  69. }  
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: