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:
- Compilar o código
.pio
com o utilitáriopioasm
. - Incluir o programa PIO compilado no firmware.
- Inicializar o periférico PIO e a SM.
- Carregar o programa e configurar a SM.
- Ativar a máquina de estado.
- 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:
- A CPU inicia a comunicação mantendo o pino em nível baixo por ~18 ms.
- O sensor responde com um pulso baixo (~80 µs) seguido de um pulso alto (~80 µs).
- Em seguida, o sensor transmite 40 bits de dados (5 bytes: umidade inteira, umidade decimal, temperatura inteira, temperatura decimal, checksum).
- Cada bit é codificado como:
0
: 50 µs LOW + ~26-28 µs HIGH1
: 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:
- A CPU inicia a comunicação mantendo o pino em nível baixo por ~18 ms.
- O sensor responde com um pulso baixo (~80 µs) seguido de um pulso alto (~80 µs).
- Em seguida, o sensor transmite 40 bits de dados (5 bytes: umidade inteira, umidade decimal, temperatura inteira, temperatura decimal, checksum).
- Cada bit é codificado como:
0
: 50 µs LOW + ~26-28 µs HIGH1
: 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
ewait 0 pin
alternados. - Medir o tempo entre transições (como no exemplo do DHT).
- Interpretar a transição:
LOW → HIGH
= 1HIGH → 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:
- Gerenciamento de clock e delays: o uso incorreto de
set
,jmp
,nop
edelay
pode quebrar a temporalidade de um protocolo. Sempre revise o tempo por instrução. - FIFO e sincronização: o uso de
push
epull
deve considerar profundidade da FIFO (4 palavras por direção) para evitar overflow ou bloqueios. - 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.
- Divisão de responsabilidades: mantenha a PIO focada em tarefas de tempo crítico e deixe o processamento lógico para a CPU.
- 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)