Controlando uma fita LED RGB com o Raspberry Pico Deixe um comentário

As fitas de LED RGB “endereçável”, baseadas no chip WS2812B, são bastante populares para a geração de efeitos luminosos. Estas fitas estão disponíveis em versões para operação a 5 ou 12V, com ou sem proteção para uso externo e em diferentes densidades (número de LEDs por metro).

Do ponto de vista de programação, a interface destas fitas apresenta alguns desafios, principalmente para microcontroladores mais modestos. É utilizado um único sinal de dados, com a definição dos bits e seus valores sendo feita por temporizações bastante curtas. A rigor, os LEDs não são endereçáveis: não é possível atualizar um LED isoladamente. Para atualizar um LED é preciso atualizar também todos os LEDs anteriores a ele na fita, o que aumenta a quantidade de bytes a enviar. Considerando que são 3 bytes para cada LED (um para cada cor), a memória para armazenar o estado de toda uma fita com 100 ou mais LEDs pode ser significativa.

Neste post vamos ver como alguns recursos do Raspberry Pi Pico facilitam o controle de uma fita de LED RGB.

Tratando o Protocolo de Comunicação do LED WS2812B

Como dito na introdução, o envio de cada bit exige temporizações relativamente curtas:

  • O início da sequência de bits é marcado por um nível baixo de pelo menos 50 microsegundos.
  • O envio de um bit “0” é composto de 400 nanosegundos de nível alto seguido de 850 nanosegundos de nível baixo.
  • O envio de um bit “1” é composto de 850 nanosegundos de nível alto seguido de 400 nanosegundos de nível baixo.
  • Os tempos dos bits precisam ter uma precisão de 150 nanosegundos.

As bibliotecas mais populares para uso com a IDE Arduino recorrem a códigos em assembly (com interrupções inibidas) para conseguir gerar estes tempos em microcontroladores ATmega como os usados no Arduino Uno. Para microcontroladores mais rápidos, elas utilizam interrupções de relógio, o que causa a pausa frequente da execução do programa principal.

Tipicamente é armazenada uma única cópia do estado de todos os LEDs. Para produzir um efeito ou animação, esta cópia é alterada e depois enviada à fita. Ao final do envio o processo se repete.

Em que o Raspberry Pi Pico pode melhorar isso?

Em primeiro lugar, ele dispõe do recurso de I/O Programado (o PIO). O PIO pode ser programado para gerar automaticamente o sinal com os tempos corretos a partir da escrita dos três bytes, sem interferência no processamento. O programa para isso é um dos exemplos fornecido pela Raspberry Pi Foundation e pode ser visto no github oficial: https://github.com/raspberrypi/pico-examples/blob/master/pio/ws2812/ws2812.pio (se o código arecer confuso, a documentação do SDK explica linha a linha a programação da PIO).

A carga dos bytes no PIO pode ser também feita sem interferência direta do processador, através do recurso de Acesso Direto à Memória (DMA). O DMA é programado com o endereço inicial e o tamanho dos dados a transferir e faz a transferência dos dados em paralelo à execução normal. O final da transferência pode ser sinalizado por uma interrupção.

Como o Raspberry Pi Pico possui generosos 264k de Ram, podemos guardar duas cópias da imagem dos LEDs. Enquanto atualizamos uma cópia, a outra é enviada à fita (via DMA+PIO). Com isso a geração do próximo estado e o envio à fita são feitos em paralelo, possibilitando uma atualização mais frequente.

Um Exemplo Prático

Neste exemplo vamos usar uma fita com 150 LEDs RGB, com alimentação de 5V. A ligação da fita à alimentação e ao Raspberry Pi Pico é a seguinte:

Vamos usar uma alimentação separada de 5V para a fita ao invés de usar os 5V da USB devido ao consumo dos LEDs. O PiPico deve ser alimentado normalmente pela USB.

O nosso programa vai ser escrito em C. A programação da Raspberry Pi Pico em C é descrita no manual do SDK (que você baixa aqui); a preparação do ambiente é descrita neste artigo da Rosana Guse (e na documentação oficial).

O programa (fitaLED.c) é o seguinte:

/**
 * Controle de LEDs RGB usando PIO e DMA, com duplo buffer
 * Baseado nos exemplos do SDK da Raspberry Pi Pico
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>

#include "pico/stdlib.h"
#include "hardware/pio.h"
#include "hardware/dma.h"
#include "hardware/clocks.h"
#include "ws2812.pio.h"

// GPIO conectado à fita
const int PIN_TX = 0;

// Número de LEDs na fita
#define NLEDS 150

// PIO e state machine para controlar os LEDs
static PIO pio;
static int sm;

// Imagens dos LEDs
static uint32_t fitaEd[NLEDS];  // imagem para edição
static uint32_t fitaAt[NLEDS];  // imagem para atualização

// Monta um valor de 32 bits com as intensidades das cores
// ggggggggrrrrrrrrbbbbbbbb00000000
static inline uint32_t urgb_u32(uint8_t r, uint8_t g, uint8_t b) {
    return
            ((uint32_t) (r) << 16) |
            ((uint32_t) (g) << 24) |
            ((uint32_t) (b) << 8);
}

// Atualiza os LEDs na fita
static void atualizaFita() {
    uint dma_chan = 0;

    // Copia para a área que será enviada
    memcpy (fitaAt, fitaEd, sizeof(fitaAt));

    // garante que a transferência anterior terminou
    dma_channel_wait_for_finish_blocking(dma_chan);

    // espera pio ter tratado tudo 
    while (!pio_sm_is_tx_fifo_empty(pio, sm)) {    
        sleep_us(10);
    }
    // para a state machine
    pio_sm_set_enabled(pio, sm, false); 

    // Configura o DMA
    // - incrementar endereço origem (memória)
    // - não incrementar endereço destino (fila da PIO)
    // - PIO indica quando fazer a transferência
    dma_channel_config c = dma_channel_get_default_config(dma_chan);
    channel_config_set_read_increment(&c, true);
    channel_config_set_write_increment(&c, false);
    channel_config_set_dreq(&c, pio_get_dreq(pio, sm, true));
    dma_channel_configure(dma_chan, &c,
        &pio->txf[sm],              // destino
        fitaAt,                     // origem
        NLEDS,                      // tamanho
        true                        // começar imediatamente
    );

    // Dispara a transferência
    sleep_us(300);   // Para indicar inicio
    pio_sm_set_enabled(pio, sm, true); // liga a state machine
}

// Limpa a imagem dos leds
static void apagaLEDS() {
    memset (fitaEd, 0, sizeof(fitaEd));
    atualizaFita();
}

// Inicia os LEDs
void iniciaLEDs() {
    // Cria um techo em cor verde/azulada
    int i = 0;
    fitaEd[i++] = urgb_u32(0, 5, 2);
    fitaEd[i++] = urgb_u32(0, 10, 4);
    fitaEd[i++] = urgb_u32(0, 20, 8);
    fitaEd[i++] = urgb_u32(0, 40, 16);
    fitaEd[i++] = urgb_u32(0, 20, 8);
    fitaEd[i++] = urgb_u32(0, 10, 4);
    fitaEd[i++] = urgb_u32(0, 5, 2);

    // O resto fica em cor vermelha clara
    while (i < NLEDS) {
        fitaEd[i++] = urgb_u32(3, 0, 1);
    }

// Roda os LEDs
void rodaLEDs() {
    uint32_t aux = fitaEd[0];
    for (int i = 1; i < NLEDS; i++) {
        fitaEd[i-1] = fitaEd[i];
    }
    fitaEd[NLEDS-1] = aux;
}

// Programa principal
int main() {
    // Aloca e inicia uma PIO
    pio = pio0;
    sm = 0;
    uint offset = pio_add_program(pio, &ws2812_program);
    ws2812_program_init(pio, sm, offset, PIN_TX, 800000, false);
    apagaLEDS();

    // Apresenta a animação
    iniciaLEDs();
    while (1) {
        rodaLEDs();
        atualizaFita();
        sleep_ms(10);    // uma pequena pausa para ficar mais visível
    }
}

As informações para compilar este programa serão colocadas em um arquivo CMakeLists.txt:

cmake_minimum_required(VERSION 3.13)

include(pico_sdk_import.cmake)

project(fitaLED_project)

pico_sdk_init()

add_executable(fitaLED
    fitaLED.c
)

pico_generate_pio_header(fitaLED ${CMAKE_CURRENT_LIST_DIR}/ws2812.pio)

target_link_libraries(fitaLED PRIVATE
    pico_stdlib
    hardware_pio
    hardware_dma
)

pico_add_extra_outputs(fitaLED)

As instruções abaixo para gerar o executável usando a linha de comando são para Linux (veja mais detalhes nas referências acima) e supõem que você instalou o SDK dentro do seu ‘home’, num diretório chamado pico. Crie debaixo deste diretório pico o diretório fitaLED e coloque dentro dele os arquivos fitaLED.c e CMakeLists.txt listados acima e execute os seguintes comandos:

cd ~/pico/fitaLED
cp ~/pico/pico-sdk/external/pico_sdk_import.cmake .
cp ~/pico//pico-examples/pio/ws2812/ws2812.pio .
mkdir build
cd build
export PICO_SDK_PATH=../../pico-sdk
cmake ..
make

Ao final terá sido criado no diretório build, entre outros, o arquivo fitaLED.uf2. Aperte o botão BOOT da Pi Pico, conecte ao micro e solte o botão. O micro vai reconhecer a placa como um pendrive. Copie o arquivo fitaLED.uf2 para este drive, a placa irá reiniciar e executar o programa.

O vídeo abaixo mostra o resultado.

Conclusão

Os recursos de PIO e DMA do Raspberry Pi Pico permitem realizar a atualização dos LEDs RGB sem um consumo elevado de processamento da CPU. Com isto a CPU pode ser usada para o que realmente importa: gerar a próxima imagem. O resultado são animações mais sofisticadas com altas taxas de atualização.

Estas técnicas podem também ser usadas com outros arranjos de LEDs WS2812B, como anéis e painéis.

Então, você vai usar isso no seu próximo projeto? Se sim, mostre ele para gente na Comunidade Maker no Facebook.

Dúvidas ou comentários? É só colocar aqui embaixo!

Faça seu comentário

Acesse sua conta e participe