Embedded C: restrições e boas práticas

1. O Ambiente Embarcado e suas Limitações

Programar para sistemas embarcados em C exige uma mudança de mentalidade em relação ao desenvolvimento para desktop. Microcontroladores típicos possuem entre 2 KB e 512 KB de RAM, e Flash de 32 KB a 2 MB. A stack, frequentemente limitada a poucos kilobytes, não tolera alocações profundas ou recursão excessiva.

Processadores embarcados operam entre 8 MHz e 200 MHz, muitas vezes sem FPU (Unidade de Ponto Flutuante). Operações com float ou double são simuladas por software, consumindo centenas de ciclos. A ausência de sistema operacional — ou a presença de um RTOS mínimo — transfere para o programador a responsabilidade pelo gerenciamento de tempo, memória e concorrência.

// Exemplo: cálculo de ponto flutuante em MCU sem FPU
float sensor_value = 3.14159f;
float result = sensor_value * 2.0f;  // centenas de ciclos sem FPU

2. Gerenciamento de Memória em Sistemas Embarcados

O uso de malloc e free é amplamente desaconselhado em sistemas embarcados críticos. A alocação dinâmica introduz fragmentação, latência imprevisível e risco de falha em tempo de execução. Padrões como MISRA-C proíbem explicitamente seu uso.

A abordagem recomendada é alocação estática em tempo de compilação. Para estruturas de tamanho variável, utiliza-se pools de memória fixa (memory pools), onde blocos de tamanho pré-definido são gerenciados manualmente.

// Pool de memória fixa para buffers de 64 bytes
#define POOL_SIZE 10
#define BLOCK_SIZE 64

static uint8_t pool[POOL_SIZE][BLOCK_SIZE];
static uint8_t pool_used[POOL_SIZE] = {0};

void* pool_alloc(void) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!pool_used[i]) {
            pool_used[i] = 1;
            return pool[i];
        }
    }
    return NULL;  // pool esgotado
}

void pool_free(void* ptr) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (pool[i] == ptr) {
            pool_used[i] = 0;
            return;
        }
    }
}

3. Tipos de Dados e Portabilidade

O padrão C não define tamanhos fixos para int, long ou short. Em sistemas embarcados, onde registros de hardware têm tamanhos exatos (8, 16 ou 32 bits), isso é inaceitável. A solução é utilizar stdint.h.

#include <stdint.h>

uint8_t  reg_byte;   // exatamente 8 bits
uint16_t reg_word;   // exatamente 16 bits
uint32_t reg_dword;  // exatamente 32 bits

Endianness é outro ponto crítico. Microcontroladores ARM são little-endian por padrão, enquanto redes e alguns periféricos usam big-endian. O alinhamento de memória também difere entre arquiteturas — acessar um uint32_t em endereço não alinhado pode causar exceção de hardware.

// Conversão segura de endianness
uint16_t swap16(uint16_t val) {
    return (val << 8) | (val >> 8);
}

uint32_t swap32(uint32_t val) {
    return ((val << 24) |
            ((val << 8)  & 0x00FF0000) |
            ((val >> 8)  & 0x0000FF00) |
            (val >> 24));
}

4. Controle de Fluxo e Otimizações

Em interrupções (ISRs), a eficiência é crucial. switch geralmente gera tabela de jump, sendo mais rápido que cadeias de if-else. Para variáveis modificadas por hardware, o modificador volatile impede que o compilador otimize acessos, forçando leitura/escrita real a cada acesso.

// Registro de status de hardware
volatile uint32_t* status_reg = (uint32_t*)0x40004000;

// ISR eficiente com switch
void TIMER_IRQHandler(void) {
    uint32_t status = *status_reg;
    switch (status & 0x0F) {
        case TIMER_OVF:
            timer_overflow_handler();
            break;
        case TIMER_CMP:
            timer_compare_handler();
            break;
        default:
            break;
    }
}

Loops bloqueantes como for(i=0; i<100000; i++); desperdiçam ciclos e impedem outras tarefas. Prefira máquinas de estado acionadas por timers.

// Máquina de estado sem delays bloqueantes
typedef enum { IDLE, MEASURE, SEND, WAIT } state_t;
static state_t state = IDLE;
static uint32_t timer = 0;

void system_tick(void) {
    timer++;
}

void state_machine(void) {
    switch (state) {
        case IDLE:
            if (timer >= 1000) {
                timer = 0;
                state = MEASURE;
            }
            break;
        case MEASURE:
            read_sensor();
            state = SEND;
            break;
        case SEND:
            send_data();
            timer = 0;
            state = WAIT;
            break;
        case WAIT:
            if (timer >= 100) {
                timer = 0;
                state = IDLE;
            }
            break;
    }
}

5. Interrupções e Concorrência

ISRs devem ser curtas e previsíveis. A regra de ouro: nunca chame funções que possam bloquear ou alocar memória dentro de uma ISR. Para proteger seções críticas compartilhadas entre ISR e código principal, desabilite interrupções seletivamente.

// Seção crítica com desabilitação de interrupções
volatile uint32_t shared_counter = 0;

void increment_counter(void) {
    __disable_irq();
    shared_counter++;
    __enable_irq();
}

void TIMER_IRQHandler(void) {
    shared_counter++;  // ISR não precisa proteger
}

Variáveis compartilhadas entre ISR e contexto principal devem ser volatile. O compilador, sem volatile, pode manter o valor em registro e nunca ler a versão atualizada pela ISR.

6. Acesso a Hardware e Periféricos

Registros de periféricos são mapeados em endereços fixos de memória. A forma mais legível e segura de acessá-los é através de structs com ponteiros.

// Definição do registro UART
typedef struct {
    volatile uint32_t DR;      // Data Register
    volatile uint32_t SR;      // Status Register
    volatile uint32_t CR;      // Control Register
} UART_Type;

#define UART0 ((UART_Type*)0x40001000)

void uart_send(char c) {
    while (UART0->SR & (1 << 7));  // aguarda TX ready
    UART0->DR = c;
}

Para operações atômicas em sistemas sem suporte nativo, use intrínsecos do compilador ou assembly inline.

// Operação atômica com __sync_fetch_and_add (GCC)
uint32_t atomic_increment(uint32_t* var) {
    return __sync_fetch_and_add(var, 1);
}

// Barreira de memória com assembly inline
#define MEMORY_BARRIER() __asm volatile("dmb" ::: "memory")

O atributo __attribute__((packed)) é útil para structs que representam pacotes de dados ou registros, eliminando padding entre campos.

// Pacote de comunicação sem padding
typedef struct __attribute__((packed)) {
    uint8_t  id;
    uint16_t value;
    uint8_t  crc;
} Packet_t;

7. Compilação, Depuração e Manutenibilidade

Atributos do compilador GCC permitem controle fino sobre posicionamento e comportamento.

// Colocar função em RAM para execução rápida
void __attribute__((section(".ramfunc"))) fast_isr(void) {
    // código crítico em RAM
}

// Declarar handler de interrupção
void __attribute__((interrupt)) TIMER_IRQHandler(void) {
    // tratamento da interrupção
}

Modularização é essencial. Headers enxutos com apenas declarações necessárias, funções static para escopo de arquivo e inline para funções pequenas chamadas frequentemente.

// timer.h - header enxuto
#ifndef TIMER_H
#define TIMER_H

#include <stdint.h>

void timer_init(uint32_t period_us);
void timer_start(void);
void timer_stop(void);

#endif

Para depuração, asserts em tempo de desenvolvimento e logs condicionais via UART são práticas comuns.

#ifdef DEBUG
    #define ASSERT(cond) if (!(cond)) { \
        uart_send_string("Assert failed: " #cond "\r\n"); \
        while(1); \
    }
#else
    #define ASSERT(cond)
#endif

Referências