Uso Prático de Estruturas em Protocolos de Comunicação

Ainda preenchendo buffers de dados byte a byte? Os desenvolvedores experientes utilizam estruturas para uma abordagem mais eficiente e robusta.

Esta técnica avançada transforma o código em sistemas embarcados, como STM32, de funcional para altamente otimizado, especialmente na implementação de protocolos de comunicação.

O Problema: Manipulação Direta de Arrays

Considere a transmissão de um pacote de dados de um senser de temperatura e umidade com o seguinte protocolo:

Campo Tamanho Descrição
Cabeçalho 2 bytes 0xAA55
Temperatura 2 bytes Inteiro, multiplicado por 10
Umidade 2 bytes Inteiro, multiplicado por 10
Verificação 2 bytes CRC16

Implementação convencional com array:

uint8_t pacote_tx[8];
pacote_tx[0] = 0xAA;
pacote_tx[1] = 0x55;
pacote_tx[2] = (temperatura >> 8) & 0xFF; // Byte alto
pacote_tx[3] = temperatura & 0xFF;       // Byte baixo
pacote_tx[4] = (umidade >> 8) & 0xFF;
pacote_tx[5] = umidade & 0xFF;
uint16_t checksum = CalcularCRC(pacote_tx, 6);
pacote_tx[6] = (checksum >> 8) & 0xFF;
pacote_tx[7] = checksum & 0xFF;
EnviarDados_UART(pacote_tx, 8);

Desvantagens:

  1. Código verboso: Cada campo adicional requer múltiplas linhas de manipulação de bits.
  2. Susceptível a erros: Erros de índice são difíceis de depurar.
  3. Manutenção complexa: Alterações no protocolo exigem recálculo de todos os índices.

A Solução: Mpaeamento com Estruturas

A estratégia consiste em definir uma estrutura cuja disposição na memória corresponda exatamente ao fluxo de bytes do pacote de protocolo.

Definição da estrutura do pacote:

// Utilização de atributo para eliminar preenchimento de alinhamento.
typedef struct __attribute__((packed))
{
    uint16_t cabecalho; // 0xAA55
    int16_t  temperatura; // Valor * 10
    int16_t  umidade;    // Valor * 10
    uint16_t crc;       // Campo de verificação
} PacoteSensor;

Código de transmissão otimizado:

PacoteSensor dados;
dados.cabecalho = 0xAA55;
dados.temperatura = (int16_t)(ler_sensor_temperatura() * 10);
dados.umidade = (int16_t)(ler_sensor_umidade() * 10);
dados.crc = 0; // Inicializa com zero

// Calcula CRC sobre os primeiros campos (exclui o próprio CRC).
dados.crc = CalcularCRC((uint8_t*)&dados, sizeof(dados) - 2);

// Envia a estrutura diretamente. O tamanho é derivado do tipo.
EnviarDados_UART((uint8_t*)&dados, sizeof(dados));

Vantagens:

  • Redução significativa de código: A lógica fica mais clara e concisa.
  • Acesso direto à memória: Elimina a necessidade de cópias intermediárias.
  • Segurança de tipos: O compilador ajuda a prevenir atribuições de tipo incorretas.

Armadilhas em Sistemas ARM Cortex-M

Embora converter ponteiros de estrutura seja eficiennte, em plataformas como STM32, três problemas críticos devem ser endereçados.

1. Falha por Alinhamento de Memória

Sintoma: O código falha em tempo de execução com uma HardFault.

Causa: O acesso a tipos como uint16_t em endereços não alinhados (não múltiplos de 2) é proibido em muitas CPUs. Uma estrutura 'packed' pode resultar em campos com endereços ímpares.

Solução: Dados devem ser copiados por software para uma variável local alinhada ao serem recebidos.

// Método inseguro (pode causar exceção):
PacoteSensor *ptr_invalido = (PacoteSensor*)buffer_recepcao;
int16_t valor_temp = ptr_invalido->temperatura; // Risco de falha!

// Método seguro (via cópia):
PacoteSensor dados_recebidos;
memcpy(&dados_recebidos, buffer_recepcao, sizeof(PacoteSensor));
int16_t valor_seguro = dados_recebidos.temperatura;

2. Incompatibilidade de Ordem de Bytes (Endianness)

Sintoma: Os bytes dos campos multi-byte chegam invertidos no destino.

Causa: Arquiteturas como ARM Cortex-M utilizam little-endian, enquanto muitos protocolos de rede são big-endian.

Solução: Aplicar conversão de ordem de bytes antes da transmissão e após a recepção.

// Macro para converter de host (little-endian) para rede (big-endian).
#define HTONS(valor) ((((valor) & 0xFF00) >> 8) | (((valor) & 0x00FF) << 8))

dados.cabecalho = HTONS(0xAA55);
dados.temperatura = HTONS(calcular_temperatura());

3. Otimização Excessiva do Compilador

Sintoma: Dados enviados por DMA estão desatualizados ou incorretos.

Causa: O compilador, ao otimizar, pode manter variáveis em registradores, não garantindo que o DMA leia os valores mais recentes da memória.

Solução: Utilizar o qualificador volatile para indicar que o conteúdo da memória pode ser alterado por agentes externos (como hardware).

// Estrutura marcada como volatile para uso com DMA.
volatile PacoteSensor pacote_dma;

Aplicação em Protocolos Reais: Exemplo com Modbus RTU

A técnica é amplamente aplicável. Veja a estrutura para uma requisição Modbus RTU (Função 03):

typedef struct __attribute__((packed))
{
    uint8_t  endereco_escravo;
    uint8_t  codigo_funcao; // 0x03
    uint16_t reg_inicial;
    uint16_t quantidade_regs;
    uint16_t crc;
} RequisicaoLeituraModbus;

void SolicitarRegistrosHolding(uint8_t escravo, uint16_t reg_inicio, uint16_t qtd)
{
    volatile RequisicaoLeituraModbus pedido;
    pedido.endereco_escravo = escravo;
    pedido.codigo_funcao = 0x03;
    pedido.reg_inicial = HTONS(reg_inicio);
    pedido.quantidade_regs = HTONS(qtd);
    pedido.crc = 0;
    // Calcula CRC sobre os 6 primeiros bytes.
    pedido.crc = Modbus_CRC((uint8_t*)&pedido, 6);
    pedido.crc = HTONS(pedido.crc); // Converte o próprio CRC.

    IniciarTransmissaoDMA(&pedido, sizeof(pedido));
}

A organização de campos variáveis em uma comunicação usando estruturas frequentemente segue um padrão: definir uma estrutura para o cabeçalho fixo e, em seguida, tratar o restante do buffer de dados de forma flexível.

Tags: STM32 estruturas-em-c protocolos-de-comunicação alinhamento-de-memória endianness

Publicado em 6-6 17:39 por Thomas