⚙️ Aula 02 — Linguagem C Aplicada à Engenharia: Tipos Primitivos, Modificadores e Operadores Bitwise

Navegação Rápida


📌 Sumário da Aula

  1. 🧠 1. Tipos de Dados com Largura Fixa e o Padrão ISO C99
  2. 📐 2. Representação Numérica: Complemento de Dois e IEEE 754
  3. ⚡ 3. O Modificador Crítico volatile e Otimizações de Compilador
  4. 🛡️ 4. Modificadores de Armazenamento e Escopo: const, static e extern
  5. 🎛️ 5. Álgebra Booleana em Registradores: Idiomas Canônicos Bitwise
  6. 📊 6. Diagrama de Tipos Primitivos, Volatile e Manipulação Bitwise
  7. 📝 7. Questões de Concursos e Problemas Avançados Resolvidos
  8. 🔗 8. Conexões e Referências Cruzadas

🧠 1. Tipos de Dados com Largura Fixa e o Padrão ISO C99

No padrão clássico ANSI C (C89/C90), os tipos inteiros primitivos (short, int, long) possuem larguras de bits dependentes da arquitetura do compilador e do hardware da CPU:

  • Em processadores de 8 bits (AVR ATmega), int possui frequentemente 16 bits;
  • Em processadores de 32 bits (ARM Cortex-M), int possui 32 bits;
  • Em estações de 64 bits (x86_64), long pode ter 32 ou 64 bits.

Em sistemas de controle de potência e telemetria elétrica, essa ambiguidade é inaceitável. O protocolo Modbus, por exemplo, exige estritamente registradores de 16 bits não assinados, enquanto os conversores analógico-digitais (ADC) geram amostras de 10, 12 ou 16 bits alinhadas à direita.

O padrão ISO/IEC 9899:1999 (C99) resolveu definitivamente esse problema introduzindo o cabeçalho padrão <stdint.h>, que define tipos inteiros com número exato de bits:

Tipo Definido em <stdint.h>Largura (Bits / Bytes)Faixa Numérica VálidaAplicação Típica em Sistemas Elétricos
uint8_t8 bits (1 Byte)Registradores de configuração I2C, SPI e portas de saída
int8_t8 bits (1 Byte)Offset de calibração térmica e sensores com sinal
uint16_t16 bits (2 Bytes)Amostras brutas de conversores ADC e registradores Modbus
int16_t16 bits (2 Bytes)Sinais de corrente e tensão alternada senoidais instantâneos
uint32_t32 bits (4 Bytes)Endereços de barramento de memória, contadores de tempo e millis
int32_t32 bits (4 Bytes)Acumuladores de produto escalar e filtros IIR/FIR em ponto fixo
uint64_t64 bits (8 Bytes)Medição acumulada de energia elétrica em Joules ou Watt-hora

Regra de Ouro da Engenharia de Firmware: Nunca utilize o tipo int puro em código embarcado crítico; utilize sempre os tipos explícitos de <stdint.h> para garantir portabilidade matemática determinística entre diferentes arquiteturas de hardware.


📐 2. Representação Numérica: Complemento de Dois e IEEE 754

2.1 Aritmética de Inteiros com Sinal: Complemento de Dois

Todos os tipos inteiros com sinal (int8_t, int16_t, int32_t) são codificados na representação de Complemento de Dois. Em uma palavra de bits com padrão binário , o valor decimal correspondente é dado por:

O bit mais significativo () atua como bit de sinal com peso negativo . Vantagens computacionais do Complemento de Dois:

  1. Zero Único: Não existe ambiguidade de e (como ocorre na representação em magnitude e sinal);
  2. Aritmética Unificada: Os circuitos somadores da Unidade Lógica e Aritmética (ALU) executam a subtração realizando a simples soma binária , dispensando circuitos de subtração dedicados.

2.2 Ponto Flutuante IEEE 754

Para representar números reais de faixa dinâmica ampla (como impedâncias em ou fasores de corrente), utiliza-se o padrão IEEE 754:

  1. Precisão Simples (float - 32 bits):
    • 1 bit de sinal ();
    • 8 bits de expoente () com viés de 127;
    • 23 bits de mantissa fracionária (), garantindo dígitos decimais significativos de precisão.
  2. Precisão Dupla (double - 64 bits):
    • 1 bit de sinal ();
    • 11 bits de expoente () com viés de 1023;
    • 52 bits de mantissa fracionária (), garantindo dígitos decimais significativos de precisão.

Impacto no Tempo Real: Processadores ARM Cortex-M0/M3 não possuem unidade de hardware para ponto flutuante (FPU). Operações com float nesses núcleos são emuladas por dezenas ou centenas de instruções de software na biblioteca libgcc, tornando o cálculo de RMS inviável em frequências elevadas. Já o Cortex-M4F e Cortex-M7 possuem FPU de hardware nativa, executando somas e multiplicações de ponto flutuante em apenas de clock.


⚡ 3. O Modificador Crítico volatile e Otimizações de Compilador

Em programas de desktop tradicionais, uma variável só muda de valor quando o próprio fluxo de execução do código escreve expressamente nela. Em sistemas embarcados e periféricos elétricos, o estado da memória física pode ser alterado de maneira completamente assíncrona por:

  1. Registradores de periféricos mapeados em memória cujo hardware atualiza os bits autonomamente (ex: o registrador de status de uma UART quando um novo byte serial chega da subestação);
  2. Variáveis globais compartilhadas entre a rotina principal main() e uma Rotina de Serviço de Interrupção (ISR);
  3. Sistemas multitarefa em tempo real (RTOS) onde múltiplas threads leem e gravam na mesma posição de RAM.

3.1 A Catástrofe da Otimização sem volatile

Considere o seguinte laço de espera por transmissão serial USART implementado sem o modificador volatile:

/* Erro Comum de Programação em Firmware */
uint32_t *reg_status = (uint32_t *) 0x40013800; // Endereço físico do USART1->SR
 
while (!(*reg_status & (1U << 7))) {
    // Aguarda o hardware periférico setar o bit 7 (TXE - Transmit Data Register Empty)
}

Quando esse código é compilado com as flags de otimização ativadas (-O2 ou -O3), o compilador GCC executa a análise de fluxo de dados. Ele observa que o laço while não altera o valor apontado por *reg_status em seu corpo. Concluindo erroneamente que o valor na memória jamais mudará por conta própria, o compilador carrega o valor da memória uma única vez para um registrador interno da CPU (como r0) antes do laço e transforma o teste em:

; Código assembly gerado incorretamente pelo compilador (-O2 sem volatile)
LDR  r0, =0x40013800 ; Carrega o endereço
LDR  r1, [r0]        ; Lê da memória UMA ÚNICA VEZ
loop_infinito:
TST  r1, #128        ; Testa o bit no registrador r1
BEQ  loop_infinito   ; Se r1 era zero, entra em loop infinito e TRAVA o microcontrolador!

O microcontrolador entra em travamento permanente, pois a CPU jamais lê a memória física novamente para ver se o periférico concluiu a transmissão.

3.2 A Correção com volatile

A declaração formal correta informa ao compilador que a leitura e a escrita no endereço físico possuem efeitos colaterais de hardware e não podem sofrer caching ou eliminação de código morto:

/* Declaração Canônica e Segura */
volatile uint32_t * const reg_status = (volatile uint32_t *) 0x40013800;
 
while (!(*reg_status & (1U << 7))) {
    // Compilador é OBRIGADO a emitir uma instrução LDR na memória física em TODO ciclo!
}

🛡️ 4. Modificadores de Armazenamento e Escopo: const, static e extern

4.1 O Modificador const

Indica que a variável é de somente leitura após a inicialização.

  • Em sistemas embarcados, variáveis globais const são automaticamente alocadas na memória Flash ROM (seção .rodata), poupando os escassos bytes de memória SRAM;
  • Na passagem de parâmetros de funções, o uso de ponteiros constantes (void processar_amostras(const float *buffer, uint16_t n)) protege os dados de entrada contra modificações acidentais e permite ao compilador gerar instruções vetorizadas mais agressivas.

4.2 O Modificador static

Possui dois significados semânticos distintos na linguagem C:

  1. No Escopo Global (em nível de arquivo): Restringe a visibilidade da variável ou função exclusivamente ao arquivo .c onde foi declarada (Internal Linkage), impedindo colisões de nomes com outros módulos do projeto e promovendo encapsulamento.
  2. No Escopo Local (dentro de uma função): Faz com que a variável local preserve seu valor entre invocações sucessivas da função, sendo alocada estaticamente na SRAM (seção .data ou .bss) em vez de ser destruída na Stack ao término da função. Excelente para implementar integradores e contadores de faltas.

4.3 O Modificador extern

Informa ao compilador que a variável ou função existe e foi definida em outro módulo do projeto (External Linkage). A resolução do endereço absoluto de memória é delegada para o Linker.


🎛️ 5. Álgebra Booleana em Registradores: Idiomas Canônicos Bitwise

O controle de periféricos de hardware na Engenharia Elétrica (acionamento de bobinas de disparo de disjuntores, leitura de sensores de presença de tensão e habilitação de relógios de periféricos) é realizado por operações sobre bits individuais em registradores de 8, 16 ou 32 bits.

Os seis operadores bitwise fundamentais da linguagem C são:

  • & : E Lógico bit a bit (AND)
  • | : OU Lógico bit a bit (OR)
  • ^ : OU Exclusivo bit a bit (XOR)
  • ~ : Negação / Inversão bit a bit (NOT)
  • << : Deslocamento de bits para a esquerda (Shift Left)
  • >> : Deslocamento de bits para a direita (Shift Right)
flowchart TD
    Op["Operações Bitwise Canônicas em Registradores"] --> S["1. Setar Bit n (Forçar 1):<br/>REG |= (1U << n)"]
    Op --> C["2. Limpar Bit n (Forçar 0):<br/>REG &= ~(1U << n)"]
    Op --> T["3. Inverter Bit n (Toggle):<br/>REG ^= (1U << n)"]
    Op --> Q["4. Testar Bit n (Query/Polling):<br/>if (REG & (1U << n))"]

5.1 Os Quatro Idiomas Fundamentais

1. Setar um Bit Específico ()

Para ligar uma flag ou acionar uma saída sem modificar o estado dos demais bits do registrador: Exemplo: Para acionar o comando de fechamento do disjuntor mapeado no bit 5: GPIO_ODR |= (1U << 5);

2. Limpar um Bit Específico ()

Para desligar uma flag forçando o bit para zero, preservando intactos os demais bits: Exemplo: Para desenergizar a bobina do disjuntor no bit 5: GPIO_ODR &= ~(1U << 5);

3. Alternar / Inverter um Bit Específico ()

Para inverter o estado lógico do bit (de 0 para 1 ou de 1 para 0), fundamental para gerar pulsos de clock ou piscar LEDs de pulso de vida (Heartbeat): Exemplo: GPIO_ODR ^= (1U << 13);

4. Testar o Estado de um Bit ()

Para ler o estado lógico de um pino de entrada ou flag de interrupção: Exemplo: if (EXTI_PR & (1U << 0)) { /* Tratar interrupção de sobrecorrente */ }

5.2 Manipulação de Campos de Múltiplos Bits (Bitmasking)

Frequentemente, um conjunto contíguo de bits define a configuração de um periférico (por exemplo, os bits 2, 3 e 4 do registrador de controle selecionam a escala do conversor ADC de 12 bits): Para gravar com segurança o valor binário 0b101 (5 decimal) no campo de 3 bits com deslocamento de 2 posições:

#define ADC_SCALE_POS    2U
#define ADC_SCALE_MASK   (0x07U << ADC_SCALE_POS) // Máscara: 0b00011100
 
// Passo 1: Limpar os bits do campo alvo;
// Passo 2: Mascarar o novo valor e deslocá-lo para a posição;
// Passo 3: Escrever o novo valor com OU lógico.
ADC_CR = (ADC_CR & ~ADC_SCALE_MASK) | ((5U << ADC_SCALE_POS) & ADC_SCALE_MASK);

📊 6. Diagrama de Tipos Primitivos, Volatile e Manipulação Bitwise

O diagrama abaixo apresenta a estrutura comparativa dos tipos de dados de largura exata, a atuação do qualificador volatile e a mecânica das operações bitwise:


📝 7. Questões de Concursos e Problemas Avançados Resolvidos

Questão 1 (Banca CESGRANRIO - Engenheiro de Equipamentos Eletrônica / Petrobras)

Em um sistema de telemetria e supervisão de uma planta de compressão de gás, o registrador de controle de um atuador eletro-hidráulico de 16 bits encontra-se mapeado na memória. O engenheiro necessita acionar simultaneamente o bit 3 (habilitação da válvula proporcional) e o bit 8 (partida da bomba de óleo), assegurando que nenhum outro bit do registrador seja alterado. Qual instrução em linguagem C realiza essa operação de forma atômica e segura?

Alternativas: A) REG_CTRL = (1U << 3) + (1U << 8);
B) REG_CTRL |= (1U << 3) | (1U << 8);
C) REG_CTRL &= (1U << 3) & (1U << 8);
D) REG_CTRL ^= (1U << 3) | (1U << 8);
E) REG_CTRL = ~( (1U << 3) | (1U << 8) );

Gabarito Comentado:

  • Alternativa correta: B.
  • Justificativa Detalhada:
    • A alternativa A atribui diretamente (=), o que sobrescreveria todo o registrador, apagando (zerando) qualquer outro bit de controle previamente ativado.
    • A alternativa B utiliza o operador de atribuição composto OU bit a bit (|=). A expressão (1U << 3) | (1U << 8) gera a máscara binária contendo 1 estritamente nas posições 3 e 8 (0x0108 em hexadecimal). Ao realizar o OR lógico com o conteúdo existente do registrador, os bits 3 e 8 passam para nível alto 1, enquanto todos os bits onde a máscara possui 0 mantêm rigorosamente os seus valores anteriores.

Questão 2 (Banca VUNESP - Engenheiro Eletricista / Concurso Público)

Considere uma variável do tipo int8_t (inteiro com sinal de 8 bits em complemento de dois) declarada e inicializada em um firmware de um medidor de energia: int8_t temperatura = 120; Durante a execução de um ensaio de aquecimento, o código executa a seguinte operação aritmética simples: temperatura = temperatura + 15; Qual será o valor numérico decimal armazenado na variável temperatura após essa operação e como esse fenômeno é denominado em computação?

Resolução Passo a Passo:

  1. Faixa Numérica do int8_t: Um inteiro com sinal de 8 bits em complemento de dois opera estritamente na faixa:

  2. Representação Binária de 120:

  3. Representação Binária de 15:

  4. Soma Binária de 8 bits:

    0111\,1000 & (120) \\ + 0000\,1111 & (15) \\ \hline 1000\,0111 & (\text{Resultado em binário}) \end{array}$$
  5. Decodificação do Resultado em Complemento de Dois: O bit mais significativo é , indicando um número negativo. O valor decimal é:

Análise de Engenharia: A soma de dois números positivos () ultrapassou o teto máximo representável em 8 bits (), invadindo o bit de sinal com peso . O sistema passa a reportar uma temperatura negativa espúria de , o que desligaria indevidamente o alarme de sobreaquecimento térmico. A solução correta é utilizar int16_t para o acumulador de soma.


Questão 3 (Problema Prático de Engenharia - Decodificação de Telemetria Modbus)

Um medidor inteligente de energia envia via barramento serial uma palavra de status de 16 bits (uint16_t status_word). A especificação técnica define que:

  • Os bits 0 a 3 contêm o código de falha da instalação (0 a 15);
  • O bit 7 indica se o disjuntor principal está aberto (1) ou fechado (0);
  • Os bits 12 a 15 contêm a classe tarifária ativa (0 a 15).

Dado que o medidor reportou a palavra hexadecimal 0xA089:

  1. Escreva a expressão em linguagem C para extrair o código de falha e determine o seu valor decimal.
  2. Determine se o disjuntor está aberto ou fechado.
  3. Escreva a expressão em C para extrair a classe tarifária e determine o seu valor decimal.

Resolução Passo a Passo:

O valor recebido é 0xA089. Em binário de 16 bits:

  1. Extração do Código de Falha (Bits 0 a 3): A máscara para os 4 bits inferiores é 0x000F (0b0000000000001111):

    uint8_t codigo_falha = status_word & 0x000F;

    Operação binária:

  2. Estado do Disjuntor (Bit 7):

    bool disjuntor_aberto = (status_word & (1U << 7)) != 0;

    No número 0xA089, o bit 7 é avaliado: O bit 7 é exatamente 1.

  3. Extração da Classe Tarifária (Bits 12 a 15): Os bits estão no topo da palavra. Deslocam-se 12 posições para a direita e aplica-se a máscara de 4 bits 0x0F:

    uint8_t classe_tarifaria = (status_word >> 12) & 0x0F;

    Deslocando 12 bits para a direita: Valor decimal: .


🔗 8. Conexões e Referências Cruzadas

Disciplinas Vinculadas:

Materiais Didáticos Oficiais da Disciplina:

  • Arquivo de Curso: 01 - Bases Para Programação de Sistemas Embarcados.pdf
  • Arquivo de Curso: 02 - Linguagem C Recursos Básicos.pdf
  • Arquivo de Apoio: 00 - Plano de Ensino.pdf