GPIO e Perifericos com Zig: UART, SPI e I2C em Sistemas Embarcados

Apos configurar o ambiente, e hora de interagir com o hardware. GPIO (General Purpose Input/Output) e perifericos de comunicacao como UART, SPI e I2C sao a interface entre o microcontrolador e o mundo externo. Zig brilha aqui gracas ao sistema de tipos, que permite criar abstracoes de hardware seguras e verificadas em tempo de compilacao.

Acesso a Registradores com Type Safety

Em C embarcado, registradores sao tipicamente acessados via ponteiros para enderecos magicos. Zig permite fazer isso de forma muito mais segura:

Abordagem C (insegura)

// Facil errar enderecos, sem verificacao de tipos
#define GPIOA_MODER (*(volatile uint32_t*)0x48000000)
GPIOA_MODER = 0x01; // O que 0x01 significa? Nenhuma pista.

Abordagem Zig (type-safe)

const Gpio = packed struct {
    moder: u32,    // Mode register
    otyper: u32,   // Output type
    ospeedr: u32,  // Speed
    pupdr: u32,    // Pull-up/pull-down
    idr: u32,      // Input data
    odr: u32,      // Output data
    bsrr: u32,     // Bit set/reset
    lckr: u32,     // Lock
    afrl: u32,     // Alternate function low
    afrh: u32,     // Alternate function high
};

const GPIOA: *volatile Gpio = @ptrFromInt(0x48000000);
const GPIOB: *volatile Gpio = @ptrFromInt(0x48000400);
const GPIOC: *volatile Gpio = @ptrFromInt(0x48000800);

const PinMode = enum(u2) {
    input = 0b00,
    output = 0b01,
    alternate = 0b10,
    analog = 0b11,
};

fn configurarPino(gpio: *volatile Gpio, pino: u4, modo: PinMode) void {
    const shift = @as(u5, pino) * 2;
    gpio.moder &= ~(@as(u32, 0b11) << shift);
    gpio.moder |= (@as(u32, @intFromEnum(modo)) << shift);
}

fn escreverPino(gpio: *volatile Gpio, pino: u4, valor: bool) void {
    if (valor) {
        gpio.bsrr = @as(u32, 1) << pino; // Set
    } else {
        gpio.bsrr = @as(u32, 1) << (@as(u5, pino) + 16); // Reset
    }
}

fn lerPino(gpio: *volatile Gpio, pino: u4) bool {
    return (gpio.idr & (@as(u32, 1) << pino)) != 0;
}

Abstracao de GPIO Generica com Comptime

O poder do comptime de Zig permite criar abstracoes de hardware sem custo em runtime:

fn Pin(comptime porta: *volatile Gpio, comptime numero: u4) type {
    return struct {
        const Self = @This();

        pub fn configurar(modo: PinMode) void {
            configurarPino(porta, numero, modo);
        }

        pub fn set() void {
            escreverPino(porta, numero, true);
        }

        pub fn clear() void {
            escreverPino(porta, numero, false);
        }

        pub fn toggle() void {
            porta.odr ^= (@as(u32, 1) << numero);
        }

        pub fn ler() bool {
            return lerPino(porta, numero);
        }
    };
}

// Uso - zero overhead em runtime!
const led = Pin(GPIOC, 13);
const botao = Pin(GPIOA, 0);

pub fn main() void {
    led.configurar(.output);
    botao.configurar(.input);

    while (true) {
        if (botao.ler()) {
            led.set();
        } else {
            led.clear();
        }
    }
}

Toda a logica de selecao de porta e pino e resolvida em tempo de compilacao. O binario gerado e identico ao acesso direto a registradores.

UART - Comunicacao Serial

UART e o protocolo de comunicacao mais fundamental em embedded, usado para debug, logging e comunicacao com modulos.

Configuracao de UART

const Usart = packed struct {
    sr: u32,    // Status register
    dr: u32,    // Data register
    brr: u32,   // Baud rate register
    cr1: u32,   // Control register 1
    cr2: u32,   // Control register 2
    cr3: u32,   // Control register 3
    gtpr: u32,  // Guard time/prescaler
};

const USART1: *volatile Usart = @ptrFromInt(0x40013800);

fn uartInit(baud_rate: u32) void {
    const clock_freq: u32 = 72_000_000; // 72 MHz

    // Habilitar clock
    const rcc_apb2 = @as(*volatile u32, @ptrFromInt(0x40021018));
    rcc_apb2.* |= (1 << 14); // USART1EN

    // Configurar baud rate
    USART1.brr = clock_freq / baud_rate;

    // Habilitar TX, RX e USART
    USART1.cr1 = (1 << 13) | // UE (USART Enable)
        (1 << 3) | // TE (Transmit Enable)
        (1 << 2); // RE (Receive Enable)
}

fn uartEnviarByte(byte: u8) void {
    // Aguardar TX buffer vazio
    while (USART1.sr & (1 << 7) == 0) {}
    USART1.dr = byte;
}

fn uartEnviarString(str: []const u8) void {
    for (str) |byte| {
        uartEnviarByte(byte);
    }
}

fn uartReceberByte() u8 {
    // Aguardar dados disponiveis
    while (USART1.sr & (1 << 5) == 0) {}
    return @truncate(USART1.dr);
}

Writer compativel com std.fmt

const std = @import("std");

const UartWriter = struct {
    pub const Writer = std.io.Writer(*UartWriter, error{}, write);

    pub fn writer(self: *UartWriter) Writer {
        return .{ .context = self };
    }

    fn write(_: *UartWriter, bytes: []const u8) error{}!usize {
        for (bytes) |b| uartEnviarByte(b);
        return bytes.len;
    }
};

var uart_writer = UartWriter{};

pub fn main() void {
    uartInit(115200);

    var w = uart_writer.writer();

    // Agora podemos usar formatacao!
    w.print("Temperatura: {d:.1} C\n", .{23.5}) catch {};
    w.print("Sensor ID: 0x{x:0>4}\n", .{0xABCD}) catch {};
}

SPI - Comunicacao Serial Sincrona

SPI (Serial Peripheral Interface) e usado para comunicacao rapida com sensores, displays e memoria flash.

const Spi = packed struct {
    cr1: u32,
    cr2: u32,
    sr: u32,
    dr: u32,
    crcpr: u32,
    rxcrcr: u32,
    txcrcr: u32,
};

const SPI1: *volatile Spi = @ptrFromInt(0x40013000);

fn spiInit() void {
    // Configurar como master, clock /16, CPOL=0, CPHA=0
    SPI1.cr1 = (1 << 2) | // MSTR (Master)
        (0b011 << 3) | // BR = /16
        (1 << 6); // SPE (Enable)
}

fn spiTransferir(dado: u8) u8 {
    // Aguardar TX buffer vazio
    while (SPI1.sr & (1 << 1) == 0) {}
    SPI1.dr = dado;

    // Aguardar RX completo
    while (SPI1.sr & (1 << 0) == 0) {}
    return @truncate(SPI1.dr);
}

fn spiEnviarBuffer(dados: []const u8) void {
    for (dados) |byte| {
        _ = spiTransferir(byte);
    }
}

fn spiLerRegistrador(reg: u8) u8 {
    _ = spiTransferir(reg | 0x80); // Bit de leitura
    return spiTransferir(0x00); // Dummy byte para gerar clock
}

I2C - Barramento de Dois Fios

I2C usa apenas dois fios (SDA e SCL) para comunicar com multiplos dispositivos:

const I2c = packed struct {
    cr1: u32,
    cr2: u32,
    oar1: u32,
    oar2: u32,
    dr: u32,
    sr1: u32,
    sr2: u32,
    ccr: u32,
    trise: u32,
};

const I2C1: *volatile I2c = @ptrFromInt(0x40005400);

fn i2cInit() void {
    // Configurar clock e timing para 100kHz (Standard Mode)
    const pclk: u32 = 36_000_000; // 36 MHz
    I2C1.cr2 = pclk / 1_000_000; // Frequencia em MHz
    I2C1.ccr = pclk / (2 * 100_000); // 100kHz
    I2C1.trise = pclk / 1_000_000 + 1;
    I2C1.cr1 = 1; // PE (Peripheral Enable)
}

fn i2cEscrever(endereco: u7, dados: []const u8) !void {
    // Start condition
    I2C1.cr1 |= (1 << 8); // START
    while (I2C1.sr1 & (1 << 0) == 0) {} // SB

    // Enviar endereco + Write
    I2C1.dr = @as(u32, endereco) << 1;
    while (I2C1.sr1 & (1 << 1) == 0) {} // ADDR
    _ = I2C1.sr2; // Limpar ADDR lendo SR2

    // Enviar dados
    for (dados) |byte| {
        while (I2C1.sr1 & (1 << 7) == 0) {} // TXE
        I2C1.dr = byte;
    }

    while (I2C1.sr1 & (1 << 2) == 0) {} // BTF

    // Stop condition
    I2C1.cr1 |= (1 << 9); // STOP
}

Exemplo: Lendo Sensor de Temperatura (LM75)

fn lerTemperatura() !f32 {
    // LM75 endereco padrao: 0x48
    const ENDERECO_LM75: u7 = 0x48;

    // Enviar ponteiro para registro de temperatura
    try i2cEscrever(ENDERECO_LM75, &.{0x00});

    // Ler 2 bytes de temperatura
    var dados: [2]u8 = undefined;
    try i2cLer(ENDERECO_LM75, &dados);

    // Converter para temperatura (9 bits, 0.5 C resolucao)
    const raw: i16 = (@as(i16, dados[0]) << 8) | dados[1];
    return @as(f32, @floatFromInt(raw >> 7)) * 0.5;
}

pub fn main() void {
    uartInit(115200);
    i2cInit();

    var w = uart_writer.writer();

    while (true) {
        const temp = lerTemperatura() catch {
            w.print("Erro ao ler sensor\n", .{}) catch {};
            continue;
        };

        w.print("Temperatura: {d:.1} C\n", .{temp}) catch {};
        delay(1_000_000);
    }
}

Exercicios

  1. LED RGB: Controle um LED RGB via PWM, mudando cores suavemente.

  2. Display OLED: Comunique-se com um display SSD1306 via I2C e exiba texto.

  3. Data logger: Leia dados de um sensor via I2C e armazene em memoria flash via SPI.


Proximo Artigo

No proximo artigo, exploramos interrupts e timers para programacao event-driven em embedded.

Conteudo Relacionado


Duvidas sobre perifericos com Zig? Junte-se a comunidade Zig Brasil!

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.