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
LED RGB: Controle um LED RGB via PWM, mudando cores suavemente.
Display OLED: Comunique-se com um display SSD1306 via I2C e exiba texto.
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
- Artigo anterior: Setup Embedded
- Zig Comptime e Reflection — Abstracoes zero-cost
- Zig e Interoperabilidade com C — Drivers existentes
Duvidas sobre perifericos com Zig? Junte-se a comunidade Zig Brasil!