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
- MISRA C:2012 Guidelines — Conjunto de regras para desenvolvimento seguro em C, amplamente adotado em sistemas embarcados críticos.
- GCC ARM Embedded Documentation — Documentação oficial do GCC para ARM, incluindo atributos de compilador e intrínsecos.
- Embedded C Coding Standard (Barr Group) — Guia prático de boas práticas para programação C em sistemas embarcados.
- Using the GNU Compiler Collection (GCC): Attributes — Referência completa sobre atributos GCC como
packed,section,interrupt. - C Standard Library: stdint.h — Documentação oficial dos tipos de inteiros de tamanho fixo definidos em C99.
- ARM Cortex-M Programming Guide — Guia da ARM sobre programação para Cortex-M, incluindo tratamento de interrupções e barreiras de memória.