É 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)
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:
Postar um comentário