MCU.TEC Algoritimos Programação PIO com RP2040/RP2350: Controle Flexível e Determinístico de GPIOs

Programação PIO com RP2040/RP2350: Controle Flexível e Determinístico de GPIOs

Introdução à PIO (Programmable I/O) no RP2040/RP2350

A arquitetura dos microcontroladores RP2040 e RP2350, desenvolvidos pela Raspberry Pi Foundation, se destaca não apenas por sua dupla CPU Cortex-M0+, mas principalmente por oferecer subsistemas programáveis capazes de executar tarefas paralelas de forma determinística. Entre esses subsistemas, a PIO (Programmable I/O) é uma das inovações mais poderosas e exclusivas desses chips. Trata-se de um bloco periférico projetado para descarregar da CPU tarefas que envolvem controle de tempo preciso e manipulação direta de pinos GPIO, como geração de sinais, protocolos personalizados de comunicação, captura de dados síncronos/assíncronos, e muito mais.

Diferentemente dos periféricos tradicionais como UART, SPI ou I2C, a PIO oferece uma máquina de estados programável com sua própria linguagem de instruções, capaz de operar de forma independente da CPU principal. Com isso, é possível implementar protocolos que não são suportados nativamente pelo hardware, como WS2812 (NeoPixel), DHT11/DHT22, I2S, IR, ou até protocolos customizados de comunicação. Esse recurso permite ao engenheiro embarcado criar soluções elegantes e altamente precisas sem sobrecarregar o núcleo principal com rotinas de temporização.

Ao utilizar GPIOs via CPU, o controle sofre as limitações do sistema operacional embarcado (como o FreeRTOS ou até o superloop), interrupções, latência de execução e jitter, que dificultam a geração de sinais de tempo crítico. Já com a PIO, é possível garantir precisão de ciclo de clock com resolução de até 1 ciclo a 125 MHz, garantindo comportamento determinístico, essencial em aplicações como interfaces sensíveis ao tempo ou geração de pulsos PWM customizados.

A PIO é composta por quatro State Machines (SMs) por bloco (dois blocos no total, totalizando 8 SMs), cada uma com seu próprio conjunto de registradores, buffer FIFO de entrada e saída, e suporte a interrupções e sincronização. A linguagem PIO é propositalmente simples, com instruções como set, jmp, mov, wait, in, out, push e pull, otimizadas para controle de sinais digitais. Com elas, é possível implementar loops de comunicação com precisão de ciclo de clock, enquanto a CPU principal se dedica a tarefas mais complexas ou de alto nível.

Além disso, o RP2350 — sucessor direto do RP2040 — mantém compatibilidade com a arquitetura PIO, oferecendo melhorias em integração periférica e capacidade de processamento, mas mantendo o mesmo conceito fundamental: dar ao programador controle total do GPIO via uma linguagem dedicada e hardware altamente previsível.


Linguagem PIO: Instruções, Comportamentos e Exemplos Práticos

A linguagem PIO utilizada nos microcontroladores RP2040 e RP2350 foi projetada para ser minimalista, determinística e altamente previsível, com instruções de execução em tempo constante. Cada instrução ocupa 16 bits e executa em exatamente um ciclo de clock (exceto quando explicitamente configurada com atraso). O programador pode usar essa linguagem para construir pequenos programas que serão carregados nas máquinas de estado da PIO, executando loops, manipulando registradores e controlando diretamente os pinos GPIO.

Instruções Fundamentais da Linguagem PIO

A seguir, apresentamos as principais instruções da linguagem PIO, acompanhadas de explicações funcionais e exemplos práticos. Todas as instruções podem ser combinadas com delays explícitos (de 0 a 31 ciclos) e sideset para controlar GPIOs em paralelo.


set: Escreve valor direto em registradores ou GPIOs

set pins, 1    ; Define o GPIO como alto
set x, 5       ; Define o registrador X com valor 5

Essa instrução é usada para carregar valores fixos em registradores ou definir o estado de saída dos pinos. No contexto de controle de sinais, é comum usar set para definir níveis lógicos nos GPIOs ou resetar contadores.


jmp: Desvio condicional ou incondicional

jmp x--, loop  ; Decrementa X e salta se X não for zero
jmp pin        ; Salta para endereço conforme valor do pino
jmp always     ; Salto incondicional

O jmp é usado para construir laços e implementar lógica condicional. A versão com x-- é particularmente útil para contadores decrescentes.


mov: Move valores entre registradores, GPIOs, buffers

mov y, x       ; Copia valor de X para Y
mov isr, osr   ; Copia buffer de saída para buffer de entrada

mov é o equivalente ao assign de outras linguagens, essencial para manipulação interna de dados dentro da SM (State Machine).


wait: Aguarda condição específica (nível de pino, IRQ, etc.)

wait 1 pin 3   ; Aguarda o pino 3 ir para nível alto
wait 0 irq 4   ; Aguarda IRQ 4 ser limpo

Permite sincronização com eventos externos. Muito útil para aguardar respostas de sensores ou transições de protocolos.


in: Carrega bits externos para o registrador ISR

in pins, 1     ; Lê 1 bit do GPIO e empurra para o ISR
in x, 5        ; Lê 5 bits do registrador X

in é o principal método para capturar dados no PIO. Ele enche o ISR (Input Shift Register), que pode ser transferido para a FIFO com push.


out: Escreve bits para GPIO ou registradores

out pins, 1    ; Envia 1 bit para os GPIOs
out y, 3       ; Envia 3 bits para o registrador Y

Complementa o in, e é usado para emissão de dados ou controle de hardware. Bits são retirados do registrador OSR.


push e pull: Comunicação com a FIFO

push          ; Envia conteúdo do ISR para a FIFO de saída
pull          ; Carrega o OSR com dados da FIFO de entrada

Essas instruções fazem a ponte entre a SM e o programa em C. O pull é usado para receber comandos ou dados da CPU, enquanto o push envia dados capturados para serem lidos pela CPU.


irq: Gera ou aguarda interrupções

irq nowait 3     ; Dispara IRQ 3
irq wait 3       ; Espera IRQ 3 ser acionado

Essencial para sincronização entre a SM e a CPU, especialmente quando se deseja evitar polling contínuo.


nop: Instrução nula, usada para delay ou alinhamento

nop             ; Ocupa 1 ciclo, sem efeito

Usada quando se deseja simplesmente aguardar tempo ou preencher espaços.


Exemplo: Blink em PIO puro

.program blink
loop:
    set pins, 1   [31]
    set pins, 0   [31]
    jmp loop

Esse pequeno programa acende e apaga um LED conectado ao GPIO a cada 32 ciclos. A precisão é absoluta e independe da CPU principal.


Programando com o SDK PIO: Funções Essenciais em C

Para integrar o código PIO ao restante do firmware no RP2040 ou RP2350, utilizamos o Pico SDK, uma biblioteca C desenvolvida pela Raspberry Pi Foundation que oferece suporte direto para gerenciamento de programas PIO, controle de máquinas de estado (SMs), comunicação via FIFO e sincronização com a CPU.

O fluxo típico de uso do PIO no código C envolve:

  1. Compilar o código .pio com o utilitário pioasm.
  2. Incluir o programa PIO compilado no firmware.
  3. Inicializar o periférico PIO e a SM.
  4. Carregar o programa e configurar a SM.
  5. Ativar a máquina de estado.
  6. Trocar dados com a PIO (via FIFO ou GPIOs).

A seguir, analisamos as funções mais importantes do SDK, com exemplos e comentários.


pio_add_program()

uint offset = pio_add_program(pio0, &meu_programa_program);

Essa função carrega o programa PIO na memória do periférico. O retorno (offset) é o endereço onde o programa foi carregado — necessário para iniciar a SM. A estrutura meu_programa_program é gerada automaticamente pelo pioasm.


pio_sm_config()

pio_sm_config c = meu_programa_program_get_default_config(offset);

Retorna uma estrutura de configuração padrão para a máquina de estado. Essa estrutura é manipulada com funções auxiliares para definir GPIOs, direção dos pinos, clock, etc. É o equivalente à configuração de periféricos como SPI ou UART.


sm_config_set_clkdiv()

sm_config_set_clkdiv(&c, 1.0f);

Define o divisor de clock da SM. Um valor de 1.0 significa que a SM executa a 125 MHz, igual ao clock do sistema. Ajustar esse valor é essencial para controlar temporizações nos sinais gerados.


sm_config_set_out_pins() e similares

sm_config_set_out_pins(&c, 2, 1);   // Usa GPIO 2 para saída
sm_config_set_set_pins(&c, 2, 1);
sm_config_set_sideset_pins(&c, 2);
sm_config_set_in_pins(&c, 2);

Estas funções definem quais pinos a SM usará para cada tipo de operação (set, in, out, sideset). Também definem a direção lógica desses pinos e a quantidade de bits envolvidos.


pio_sm_init() e pio_sm_set_enabled()

pio_sm_init(pio0, 0, offset, &c);
pio_sm_set_enabled(pio0, 0, true);

Inicializa a SM com as configurações preparadas anteriormente e, em seguida, ativa a execução do programa. Cada PIO possui até 4 SMs (state machines), numeradas de 0 a 3.


pio_sm_put_blocking() e pio_sm_get()

pio_sm_put_blocking(pio0, 0, 0xA5);     // Envia dado via FIFO
uint32_t data = pio_sm_get(pio0, 0);    // Lê dado recebido

Essas funções enviam e recebem dados entre a CPU e a máquina de estado via FIFO. O envio é blocking por padrão, mas também existe a versão não bloqueante (pio_sm_put()), ideal em contextos de baixa latência.


pio_set_irq0_source_enabled() e pio_interrupt_clear()

pio_set_irq0_source_enabled(pio0, pis_interrupt_sm0, true);
irq_set_exclusive_handler(PIO0_IRQ_0, handler);
pio_interrupt_clear(pio0, 0);

Estas funções habilitam e tratam interrupções vindas da PIO, permitindo sincronização fina entre CPU e SMs, muito útil em protocolos que precisam de handshake ou aviso de dados prontos.


Com essas funções, o engenheiro tem controle total da PIO em tempo de execução. Na próxima seção, veremos como aplicar esse conhecimento na prática com implementações completas de protocolos usando PIO.


Exemplo Prático: Controle de WS2812 (NeoPixel) com PIO

Os LEDs do tipo WS2812, popularmente conhecidos como NeoPixel, exigem uma sequência de pulsos com temporização extremamente precisa para codificar bits 1 e 0. Por exemplo, para transmitir um bit ‘1’, o pino deve ficar em nível alto por aproximadamente 0,8 µs e baixo por 0,45 µs. Já o bit ‘0’ requer um nível alto de apenas 0,4 µs seguido por 0,85 µs em nível baixo. Como esses tempos são críticos e não toleram jitter, a CPU (mesmo em loop tight com interrupções desabilitadas) não garante a estabilidade necessária. É nesse cenário que o uso da PIO se torna essencial.

Diagrama de Temporização (bit WS2812)

Gerado a seguir com Python/Matplotlib para ilustrar o ciclo de um bit ‘1’ e um bit ‘0’:

import matplotlib.pyplot as plt

# Cria os sinais para 1 e 0
def pulse(bit):
    if bit == 1:
        return [1]*8 + [0]*5  # ~0.8us HIGH + ~0.45us LOW
    else:
        return [1]*4 + [0]*9  # ~0.4us HIGH + ~0.85us LOW

t = list(range(13))
plt.step(t, pulse(1), label='Bit 1')
plt.step(t, [x+1.2 for x in pulse(0)], label='Bit 0 (deslocado)')
plt.yticks([0, 1, 2.2], ['LOW', 'HIGH', ''])
plt.title("Temporização WS2812: bit 1 vs bit 0")
plt.xlabel("Tempo (ciclos normalizados)")
plt.grid(True)
plt.legend()
plt.show()

Código PIO – Transmissão de Bits WS2812

.program ws2812
.side_set 1
.wrap_target
bitloop:
    out x, 1           side 0    ; Extrai 1 bit do OSR
    jmp !x do_zero     side 1    ; Se bit = 0, vai para do_zero (curto HIGH)
    nop                side 1 [2]; Bit = 1, mantém HIGH por mais tempo
    nop                side 0 [5]; Em seguida, LOW
    jmp end
do_zero:
    nop                side 0 [2]; Bit = 0, mantém LOW após pulso curto
end:
    nop                side 0
.wrap

Este código usa sideset para controlar diretamente o nível do pino (GPIO), sem ocupar bits do OUT. O programa lê 1 bit do OSR, verifica se é 0 ou 1, e ajusta o tempo de HIGH/LOW com nop e side conforme a necessidade.

Código C – Envio de Dados RGB via PIO

#include "hardware/pio.h"
#include "ws2812.pio.h"

void ws2812_program_init(PIO pio, uint sm, uint pin, float freq_mhz) {
    uint offset = pio_add_program(pio, &ws2812_program);
    pio_sm_config c = ws2812_program_get_default_config(offset);

    sm_config_set_sideset_pins(&c, pin);
    sm_config_set_out_shift(&c, true, true, 24); // MSB first, auto-pull, 24 bits por cor

    sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
    sm_config_set_clkdiv(&c, 125.0f / freq_mhz); // Ex: 125 MHz / 8 = 15.625 MHz

    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);

    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
}

void put_pixel(PIO pio, uint sm, uint32_t rgb) {
    pio_sm_put_blocking(pio, sm, rgb << 8u); // WS2812 espera 24 bits alinhados à esquerda
}

Para cada LED WS2812, enviamos 24 bits (8 bits para G, R, B — nessa ordem). O valor é alinhado à esquerda para ocupar os bits mais significativos do registrador OSR.


Análise de Desempenho e Considerações

  • Precisão Temporal: A divisão de clock permite gerar pulsos com precisão sub-microsegundo. Um divisor de 8 produz ciclos de 0,064 µs com clock base de 125 MHz.
  • Baixa Carga na CPU: Após carregar os valores RGB na FIFO com pio_sm_put_blocking, a CPU pode continuar outras tarefas enquanto a SM transmite os bits.
  • Expansão: Protocolos similares com precisão crítica (como protocolos IR, I2S ou SPI com tempos não convencionais) podem ser implementados com a mesma abordagem.

Perfeito. Vamos agora implementar o protocolo DHT11/DHT22 usando PIO, abordando seus desafios de temporização, lógica de captura de bits e leitura do sensor com exemplo completo.


Exemplo Prático: Leitura de DHT11/DHT22 com PIO

Sensores como o DHT11 e DHT22 fornecem temperatura e umidade via um protocolo proprietário, que se baseia em pulsos PWM de tempo variável. O sinal é unidirecional (do sensor para o microcontrolador) e exige uma sequência precisa de leitura dos pulsos de dados.

Descrição do Protocolo

A comunicação com DHT11/DHT22 segue os seguintes passos:

  1. A CPU inicia a comunicação mantendo o pino em nível baixo por ~18 ms.
  2. O sensor responde com um pulso baixo (~80 µs) seguido de um pulso alto (~80 µs).
  3. Em seguida, o sensor transmite 40 bits de dados (5 bytes: umidade inteira, umidade decimal, temperatura inteira, temperatura decimal, checksum).
  4. Cada bit é codificado como:
    • 0: 50 µs LOW + ~26-28 µs HIGH
    • 1: 50 µs LOW + ~70 µs HIGH

Ou seja, a largura do pulso HIGH indica o bit, o que exige medição precisa de tempo de subida.


Código PIO – Captura de Pulsos do DHT

.program dht_read
.wrap_target
wait_low:
    wait 0 pin 0          ; Aguarda início do pulso LOW
    set x, 0              ; Zera contador de tempo
count_high:
    jmp pin count_high [1]; Enquanto pino HIGH, incrementa X
    in x, 8               ; Quando pino LOW, salva X como duração
    push                  ; Envia valor capturado para FIFO
    jmp wait_low          ; Repete para próximo bit
.wrap

Este programa aguarda uma transição para LOW, zera o registrador X e começa a contar a duração do HIGH (pulso que determina se é ‘1’ ou ‘0’). O valor é armazenado via in e push.

  • Um valor de X pequeno indica bit 0.
  • Um valor de X grande indica bit 1.

Código C – Leitura e Decodificação dos Bits

#define DHT_PIN 15
#define SM_DHT 0

void dht_pio_init(PIO pio) {
    uint offset = pio_add_program(pio, &dht_read_program);
    pio_gpio_init(pio, DHT_PIN);
    pio_sm_config c = dht_read_program_get_default_config(offset);

    sm_config_set_in_shift(&c, true, true, 8);
    sm_config_set_clkdiv(&c, 1.0f); // 125MHz para precisão máxima
    sm_config_set_jmp_pin(&c, DHT_PIN);

    pio_sm_set_consecutive_pindirs(pio, SM_DHT, DHT_PIN, 1, false); // Entrada
    pio_sm_init(pio, SM_DHT, offset, &c);
    pio_sm_set_enabled(pio, SM_DHT, true);
}

void dht_request_start() {
    gpio_set_dir(DHT_PIN, GPIO_OUT);
    gpio_put(DHT_PIN, 0);
    sleep_ms(20);                   // LOW por 18ms
    gpio_put(DHT_PIN, 1);
    busy_wait_us_32(40);           // HIGH por 20-40us
    gpio_set_dir(DHT_PIN, GPIO_IN);
}

void dht_read_data(PIO pio) {
    dht_request_start();
    dht_pio_init(pio);

    uint8_t bits[40];
    for (int i = 0; i < 40; ++i) {
        while (pio_sm_is_rx_fifo_empty(pio, SM_DHT)) tight_loop_contents();
        uint8_t pulse_len = pio_sm_get(pio, SM_DHT) & 0xFF;
        bits[i] = (pulse_len > 40) ? 1 : 0;
    }

    // Conversão dos bits para bytes
    uint8_t bytes[5] = {0};
    for (int i = 0; i < 40; i++) {
        bytes[i / 8] <<= 1;
        bytes[i / 8] |= bits[i];
    }

    if (bytes[4] == ((bytes[0] + bytes[1] + bytes[2] + bytes[3]) & 0xFF)) {
        printf("Umidade: %d.%d %%\n", bytes[0], bytes[1]);
        printf("Temperatura: %d.%d °C\n", bytes[2], bytes[3]);
    } else {
        printf("Checksum inválido.\n");
    }
}

Considerações Técnicas

  • Clock de precisão: o uso de clkdiv = 1.0 oferece o máximo de resolução temporal (8ns por ciclo com 125MHz).
  • Resiliência: pode-se ajustar o limiar entre 0 e 1 com mais robustez (por ex. pulse_len > 50).
  • Separação de tarefas: a PIO realiza a captura de pulsos; a CPU interpreta os dados, mantendo separação limpa de responsabilidades.

Exemplo Prático: Leitura de DHT11/DHT22 com PIO

Sensores como o DHT11 e DHT22 fornecem temperatura e umidade via um protocolo proprietário, que se baseia em pulsos PWM de tempo variável. O sinal é unidirecional (do sensor para o microcontrolador) e exige uma sequência precisa de leitura dos pulsos de dados.

Descrição do Protocolo

A comunicação com DHT11/DHT22 segue os seguintes passos:

  1. A CPU inicia a comunicação mantendo o pino em nível baixo por ~18 ms.
  2. O sensor responde com um pulso baixo (~80 µs) seguido de um pulso alto (~80 µs).
  3. Em seguida, o sensor transmite 40 bits de dados (5 bytes: umidade inteira, umidade decimal, temperatura inteira, temperatura decimal, checksum).
  4. Cada bit é codificado como:
    • 0: 50 µs LOW + ~26-28 µs HIGH
    • 1: 50 µs LOW + ~70 µs HIGH

Ou seja, a largura do pulso HIGH indica o bit, o que exige medição precisa de tempo de subida.


Código PIO – Captura de Pulsos do DHT

.program dht_read
.wrap_target
wait_low:
    wait 0 pin 0          ; Aguarda início do pulso LOW
    set x, 0              ; Zera contador de tempo
count_high:
    jmp pin count_high [1]; Enquanto pino HIGH, incrementa X
    in x, 8               ; Quando pino LOW, salva X como duração
    push                  ; Envia valor capturado para FIFO
    jmp wait_low          ; Repete para próximo bit
.wrap

Este programa aguarda uma transição para LOW, zera o registrador X e começa a contar a duração do HIGH (pulso que determina se é ‘1’ ou ‘0’). O valor é armazenado via in e push.

  • Um valor de X pequeno indica bit 0.
  • Um valor de X grande indica bit 1.

Código C – Leitura e Decodificação dos Bits

#define DHT_PIN 15
#define SM_DHT 0

void dht_pio_init(PIO pio) {
    uint offset = pio_add_program(pio, &dht_read_program);
    pio_gpio_init(pio, DHT_PIN);
    pio_sm_config c = dht_read_program_get_default_config(offset);

    sm_config_set_in_shift(&c, true, true, 8);
    sm_config_set_clkdiv(&c, 1.0f); // 125MHz para precisão máxima
    sm_config_set_jmp_pin(&c, DHT_PIN);

    pio_sm_set_consecutive_pindirs(pio, SM_DHT, DHT_PIN, 1, false); // Entrada
    pio_sm_init(pio, SM_DHT, offset, &c);
    pio_sm_set_enabled(pio, SM_DHT, true);
}

void dht_request_start() {
    gpio_set_dir(DHT_PIN, GPIO_OUT);
    gpio_put(DHT_PIN, 0);
    sleep_ms(20);                   // LOW por 18ms
    gpio_put(DHT_PIN, 1);
    busy_wait_us_32(40);           // HIGH por 20-40us
    gpio_set_dir(DHT_PIN, GPIO_IN);
}

void dht_read_data(PIO pio) {
    dht_request_start();
    dht_pio_init(pio);

    uint8_t bits[40];
    for (int i = 0; i < 40; ++i) {
        while (pio_sm_is_rx_fifo_empty(pio, SM_DHT)) tight_loop_contents();
        uint8_t pulse_len = pio_sm_get(pio, SM_DHT) & 0xFF;
        bits[i] = (pulse_len > 40) ? 1 : 0;
    }

    // Conversão dos bits para bytes
    uint8_t bytes[5] = {0};
    for (int i = 0; i < 40; i++) {
        bytes[i / 8] <<= 1;
        bytes[i / 8] |= bits[i];
    }

    if (bytes[4] == ((bytes[0] + bytes[1] + bytes[2] + bytes[3]) & 0xFF)) {
        printf("Umidade: %d.%d %%\n", bytes[0], bytes[1]);
        printf("Temperatura: %d.%d °C\n", bytes[2], bytes[3]);
    } else {
        printf("Checksum inválido.\n");
    }
}

Considerações Técnicas

  • Clock de precisão: o uso de clkdiv = 1.0 oferece o máximo de resolução temporal (8ns por ciclo com 125MHz).
  • Resiliência: pode-se ajustar o limiar entre 0 e 1 com mais robustez (por ex. pulse_len > 50).
  • Separação de tarefas: a PIO realiza a captura de pulsos; a CPU interpreta os dados, mantendo separação limpa de responsabilidades.


Exemplo Prático: Codificação e Decodificação Manchester com PIO

A codificação Manchester é uma técnica de modulação digital em que cada bit de dados é representado por uma transição no sinal, garantindo sincronização de clock sem um canal separado. Ela é usada em sistemas Ethernet clássicos, RFID, infravermelho e diversas interfaces de comunicação robusta com baixa taxa de erro.

Regra de Codificação Manchester

  • Bit 1 → Transição LOW → HIGH no meio do período.
  • Bit 0 → Transição HIGH → LOW no meio do período.

Isso garante que toda unidade de tempo contenha uma transição, o que facilita a sincronização mesmo em longas sequências de bits iguais.


Diagrama de Tempo: Manchester vs Dados Originais

import matplotlib.pyplot as plt

dados = [1, 0, 1, 1, 0]
manchester = []

for bit in dados:
    if bit == 1:
        manchester += [0, 1]
    else:
        manchester += [1, 0]

t = list(range(len(manchester)))
plt.step(t, manchester, where='post', label="Manchester")
plt.title("Codificação Manchester")
plt.xlabel("Tempo (unidades)")
plt.yticks([0, 1], ['LOW', 'HIGH'])
plt.grid(True)
plt.legend()
plt.show()

Código PIO – Transmissão Manchester

.program manchester_tx
.wrap_target
bitloop:
    out x, 1           ; Lê 1 bit do OSR
    jmp !x bit0
bit1:
    set pins, 0 [7]    ; LOW por metade do tempo
    set pins, 1 [7]    ; HIGH por metade do tempo
    jmp bitloop
bit0:
    set pins, 1 [7]
    set pins, 0 [7]
    jmp bitloop
.wrap

Esse programa lê um bit do registrador de saída (OSR) e gera a sequência de 2 pulsos que representam o bit via codificação Manchester, com tempos de HIGH/LOW controlados por set + delay.


Código C – Envio de Dados com Manchester

#define PIN_MANCH_TX 2
#define SM_MANCH_TX 0

void manchester_tx_init(PIO pio) {
    uint offset = pio_add_program(pio, &manchester_tx_program);
    pio_sm_config c = manchester_tx_program_get_default_config(offset);

    sm_config_set_out_shift(&c, true, true, 1); // MSB first, autopull, 1 bit por vez
    sm_config_set_clkdiv(&c, 125.0f / 1e6);     // 1 Mbps (1us por ciclo lógico)
    
    pio_gpio_init(pio, PIN_MANCH_TX);
    pio_sm_set_consecutive_pindirs(pio, SM_MANCH_TX, PIN_MANCH_TX, 1, true);

    pio_sm_init(pio, SM_MANCH_TX, offset, &c);
    pio_sm_set_enabled(pio, SM_MANCH_TX, true);
}

void manchester_tx_send(PIO pio, uint sm, uint8_t byte) {
    for (int i = 7; i >= 0; i--) {
        bool bit = (byte >> i) & 1;
        pio_sm_put_blocking(pio, sm, bit);
    }
}

Extensão: Decodificação com PIO

Embora a transmissão seja direta, a decodificação Manchester exige medição do tempo entre transições para distinguir entre 01 e 10. Uma estratégia possível:

  • Usar wait 1 pin e wait 0 pin alternados.
  • Medir o tempo entre transições (como no exemplo do DHT).
  • Interpretar a transição:
    • LOW → HIGH = 1
    • HIGH → LOW = 0

Considerações de Projeto

  • Clock determinístico: a codificação é robusta a jitter, mas exige precisão de tempo nas transições.
  • Baixa sobrecarga da CPU: transmissão inteira feita na PIO, liberando a CPU.
  • Aplicações práticas: protocolos customizados sobre fio, comunicação com RFID passivo, controle remoto, redes industriais legadas.

Considerações Finais e Recomendações de Projeto com PIO

A PIO dos microcontroladores RP2040 e RP2350 inaugura uma nova era de flexibilidade para projetos embarcados de baixo custo. Em vez de depender exclusivamente de periféricos fixos, o engenheiro passa a contar com um conjunto de máquinas de estado programáveis capazes de emular virtualmente qualquer protocolo digital. Com isso, o RP2040 torna-se um “sintetizador de periféricos”, permitindo adaptações rápidas e suporte a interfaces não previstas originalmente pelo hardware.

Ao longo deste artigo, demonstramos aplicações práticas de PIO em contextos reais, como:

  • Controle de LEDs WS2812 (NeoPixel), onde a precisão temporal é crítica;
  • Leitura do sensor DHT11/DHT22, com análise de pulsos para reconstrução dos dados;
  • Codificação Manchester, ideal para transmissões robustas com clock embutido;
  • Geração de vídeo HDMI monocromático em PIO puro, com renderização de sprites e contador via lógica dedicada em C.

Esses exemplos demonstram que, com criatividade e bom planejamento, o programador pode transformar o RP2040 em uma verdadeira central de protocolos. No entanto, é fundamental respeitar certas boas práticas:

  1. Gerenciamento de clock e delays: o uso incorreto de set, jmp, nop e delay pode quebrar a temporalidade de um protocolo. Sempre revise o tempo por instrução.
  2. FIFO e sincronização: o uso de push e pull deve considerar profundidade da FIFO (4 palavras por direção) para evitar overflow ou bloqueios.
  3. Integração com DMA: para aplicações com tráfego intensivo (como vídeo ou áudio), é recomendável usar DMA em conjunto com o PIO.
  4. Divisão de responsabilidades: mantenha a PIO focada em tarefas de tempo crítico e deixe o processamento lógico para a CPU.
  5. Depuração com logic analyzer: muitos erros em PIO só são visíveis com osciloscópio ou analisador lógico. Faça testes com sinais simples primeiro.

Além disso, considere que a linguagem PIO é pequena por projeto — apenas 32 instruções por bloco — e o número de SMs é limitado (4 por bloco, 2 blocos). Portanto, planeje seus protocolos com modularidade e reuso em mente.


Recursos Recomendados

  • 📘 RP2040 Datasheet – seção sobre PIO (capítulo 3)
  • 📘 Getting Started with PIO – por Raspberry Pi Foundation
  • 🧰 PicoDVI – exemplo avançado de HDMI via PIO
  • 🧰 ws2812-pio – exemplo oficial
  • 🎮 TinyPong – jogo Pong com RP2040 (via VGA)
0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

Diferença entre Big Endian, Little Endian e Bit EndiannessDiferença entre Big Endian, Little Endian e Bit Endianness

Artigo originalmente postado em https://carlosdelfino.eti.br/programacao/cplusplus/Diferencas_entre_BigEndian_Little_Endian_e_Bit_Endianness/ Para iniciantes, este conceito pode parecer confuso e até inútil. No entanto, para quem deseja trabalhar com microcontroladores, processadores e, principalmente, redes a nível de

0
Adoraria saber sua opinião, comente.x