flowchart TD
A["Sistemas de Numeração<br/>(Binário, Hex, Octal)"]
B["Representação de Inteiros<br/>(Sem sinal, Sinal-Magnitude,<br/>Complemento de 1, Complemento de 2)"]
C["Aritmética Inteira<br/>(Adição, Subtração, Multiplicação)"]
D["Overflow e Underflow<br/>Detecção e Tratamento"]
E["Ponto Flutuante<br/>IEEE 754"]
F["Erros de Arredondamento<br/>e Limitações"]
G["Circuitos Aritméticos<br/>Somador, Carry-Lookahead"]
H["Projeto Integrador<br/>Conversão para LCD<br/>Operações Seguras<br/>Experimentos com Float"]
A --> B
B --> C
C --> D
B --> E
E --> F
C --> G
D --> H
F --> H
G --> H
Módulo 2: Representação de Dados e Aritmética Computacional
Seja bem-vindo ao segundo módulo da disciplina de Arquitetura e Organização de Computadores. Neste material você vai descobrir como o computador representa números internamente — e por que essa representação tem consequências práticas diretas no código que você escreve. Estude com atenção, anote suas dúvidas e explore os exemplos: eles são o coração deste módulo.
Por Que Isso Importa Para Você?
Imagine que você está desenvolvendo um sistema embarcado para monitorar temperatura ambiente. Seu sensor retorna valores que podem variar de −40 °C a +125 °C. Você escreve o código, testa no computador, tudo parece correto. Então grava o firmware no PIC18F4550, liga o equipamento e… o display mostra valores completamente absurdos. O que aconteceu?
Em muitos casos, o culpado não é um erro lógico no algoritmo, mas uma falha na compreensão de como o processador armazena e manipula números. Talvez você tenha usado um tipo de dado de 8 bits sem sinal para guardar valores negativos, ou multiplicou dois números de 8 bits sem perceber que o resultado necessitava de 16 bits. Esses problemas têm nome: overflow, underflow e erros de representação.
Este módulo existe exatamente para que você nunca seja surpreendido por esses fenômenos. Ao final, você saberá como os números inteiros e em ponto flutuante são armazenados na memória, como as operações aritméticas são realizadas em hardware, e como identificar e tratar situações em que os resultados extrapolam os limites de representação.
Além disso, no Projeto Integrador, a tarefa prática deste módulo exige que você implemente rotinas de conversão numérica para exibição no display LCD do kit ACEPIC PRO V8.2. Para isso, você precisará dominar os conceitos apresentados aqui.
O diagrama a seguir mostra como os temas deste módulo se conectam entre si e com o restante da disciplina.
Sistemas de Numeração: a Linguagem do Hardware
Antes de entender como o computador representa números, você precisa estar fluente nos sistemas de numeração que ele utiliza. Provavelmente você já estudou este assunto em algum momento do curso, mas aqui vamos revisitá-lo com foco direto em como essas representações aparecem no dia a dia de quem programa sistemas embarcados.
Por Que Binário?
O sistema decimal que utilizamos no cotidiano possui dez dígitos (0 a 9) porque — segundo a história — os humanos aprenderam a contar nos dedos das mãos. O sistema binário possui apenas dois dígitos (0 e 1) porque os circuitos eletrônicos digitais são construídos com elementos que possuem dois estados estáveis: tensão alta (representando 1) e tensão baixa (representando 0).
Transistores, os blocos construtivos fundamentais de qualquer processador moderno, funcionam essencialmente como chaves eletrônicas: ligado ou desligado. Esta natureza binária não é uma escolha arbitrária dos engenheiros — é uma consequência direta da física dos materiais semicondutores. Um transistor em saturação (ligado) e um em corte (desligado) são estados muito distintos e estáveis, imunes a pequenas variações de tensão. Isso torna os circuitos digitais extremamente robustos e confiáveis.
Portanto, quando o PIC18F4550 armazena um byte na memória RAM, ele está, fisicamente, mantendo oito transistores em estados ligado ou desligado. O padrão desses estados é o que chamamos de representação binária do valor.
O Sistema Binário em Detalhe
No sistema posicional binário, cada dígito (chamado de bit, do inglês binary digit) representa uma potência de 2. O bit mais à direita tem peso 2^0 = 1, o próximo tem peso 2^1 = 2, depois 2^2 = 4, e assim por diante.
Definição: Sistema Posicional
Em um sistema de numeração posicional de base b, um número formado pelos dígitos d_{n-1} d_{n-2} \ldots d_1 d_0 tem valor decimal:
V = \sum_{i=0}^{n-1} d_i \cdot b^i
No sistema binário, b = 2 e cada d_i \in \{0, 1\}. No decimal, b = 10 e d_i \in \{0, 1, \ldots, 9\}.
Considere o número binário 1011_2. Seu valor decimal é calculado como:
1011_2 = 1 \cdot 2^3 + 0 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0 = 8 + 0 + 2 + 1 = 11_{10}
Para converter um número decimal para binário, utiliza-se o método das divisões sucessivas: divide-se repetidamente o número por 2 e registra-se o resto de cada divisão. Lendo os restos de baixo para cima, obtém-se a representação binária.
O Sistema Hexadecimal: Binário Compacto
Trabalhar com longas sequências de zeros e uns é tedioso e propenso a erros. Por isso, programadores e documentações técnicas de hardware usam o sistema hexadecimal (base 16) como atalho. Cada dígito hexadecimal representa exatamente quatro bits (um nibble), tornando a conversão binário-hexadecimal imediata.
Os dígitos hexadecimais são: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F. As letras A a F representam, respectivamente, os valores decimais 10 a 15.
Tabela de Conversão Rápida: Nibble para Hexadecimal
| Binário | Decimal | Hexadecimal |
|---|---|---|
| 0000 | 0 | 0 |
| 0001 | 1 | 1 |
| 0010 | 2 | 2 |
| 0011 | 3 | 3 |
| 0100 | 4 | 4 |
| 0101 | 5 | 5 |
| 0110 | 6 | 6 |
| 0111 | 7 | 7 |
| 1000 | 8 | 8 |
| 1001 | 9 | 9 |
| 1010 | 10 | A |
| 1011 | 11 | B |
| 1100 | 12 | C |
| 1101 | 13 | D |
| 1110 | 14 | E |
| 1111 | 15 | F |
Para converter o byte 10110111_2 para hexadecimal, basta dividir em dois grupos de quatro bits:
1011 \; 0111_2 = \text{B} \; 7_{16} = \text{0xB7}
Essa notação compacta é usada extensivamente em documentações de microcontroladores. O datasheet do PIC18F4550, por exemplo, especifica endereços de registradores e valores de configuração em hexadecimal. Quando você lê que o registrador TRISD está no endereço 0xF95, isso significa 1111 \; 1001 \; 0101_{16} em binário — mais claro em hexadecimal, não é?
O Sistema Octal: Presença Histórica
O sistema octal (base 8) utiliza dígitos de 0 a 7. Cada dígito octal representa três bits. Embora menos comum hoje, ainda aparece em contextos específicos como as permissões de arquivos no Unix/Linux, onde chmod 755 significa, em binário, 111 101 101 — leitura, escrita e execução para o dono; leitura e execução para o grupo e outros.
No contexto do PIC18F4550, o octal raramente aparece, mas você precisa reconhecê-lo caso encontre na literatura.
Conversões Entre Bases: Desenvolvendo Fluência
A fluência em conversões entre bases é uma habilidade prática que você usará com frequência ao depurar código de baixo nível, analisar dumps de memória e interpretar documentação de hardware. O objetivo não é memorizar tabelas, mas desenvolver o reflexo de raciocinar em múltiplas representações.
flowchart LR
D["Decimal"] -- "Divisões sucessivas" --> B["Binário"]
B -- "Leitura direta por nibbles" --> H["Hexadecimal"]
H -- "Expansão de nibbles" --> B
B -- "Soma de potências de 2" --> D
D -- "Divisões por 16" --> H
H -- "Multiplicação por potências de 16" --> D
O caminho mais eficiente é sempre passar pelo binário. Decimal → Binário usa divisões por 2; Binário → Hexadecimal agrupa de 4 em 4 bits; Hexadecimal → Decimal expande nibbles e soma potências de 16.
Representação de Números Inteiros
Com o sistema binário em mente, a pergunta natural é: como o computador representa números inteiros? Para números positivos, a resposta é direta — use os bits para representar a magnitude em binário puro. Para números negativos, a situação é mais interessante.
Inteiros Sem Sinal
O caso mais simples é o de inteiros sem sinal (unsigned integers). Com n bits, você pode representar 2^n valores distintos, na faixa [0,\; 2^n - 1].
Para o PIC18F4550, que trabalha nativamente com dados de 8 bits, um uint8_t pode representar valores de 0 a 255. Um uint16_t (16 bits) representa de 0 a 65.535.
Faixa de Representação de Inteiros Sem Sinal
Com n bits, a faixa de valores representáveis é:
[0,\; 2^n - 1]
O valor de um número binário sem sinal b_{n-1} b_{n-2} \ldots b_1 b_0 é:
V = \sum_{i=0}^{n-1} b_i \cdot 2^i
Exemplos práticos no PIC18F4550:
| Tipo | Bits | Faixa |
|---|---|---|
| uint8_t | 8 | 0 a 255 |
| uint16_t | 16 | 0 a 65.535 |
| uint32_t | 32 | 0 a 4.294.967.295 |
A Necessidade de Representar Números Negativos
Números inteiros sem sinal são perfeitos para quantidades que jamais assumem valores negativos: contadores, endereços de memória, comprimentos de strings. Mas logo você precisará representar temperaturas, velocidades relativas, erros de posição — valores que podem ser negativos.
Historicamente, três abordagens foram desenvolvidas para resolver esse problema. Conhecê-las não é apenas curiosidade histórica: compreender por que as duas primeiras foram abandonadas ilumina profundamente por que a terceira — o complemento de dois — tornou-se padrão universal.
Representação Sinal-Magnitude
A ideia mais intuitiva é reservar um bit para indicar o sinal (0 para positivo, 1 para negativo) e usar os demais bits para a magnitude do número. Com 8 bits, o bit mais significativo (MSB, Most Significant Bit) seria o bit de sinal.
Por exemplo:
- +5 = 0\;000\;0101_2
- -5 = 1\;000\;0101_2
Esta representação é exatamente como os humanos pensam em números negativos. Mas ela tem dois problemas sérios.
Problema 1: Dois zeros. O valor zero pode ser representado de duas formas: 0\;000\;0000_2 (zero positivo) e 1\;000\;0000_2 (zero negativo). Isso complica comparações — para verificar se um valor é zero, o hardware precisaria verificar ambas as representações.
Problema 2: Aritmética complicada. Tente somar +3 e -5 em sinal-magnitude. Você não pode simplesmente somar os bits — precisa comparar os sinais, determinar qual magnitude é maior, subtrair as magnitudes e aplicar o sinal do maior. Isso exige circuitos aritméticos muito mais complexos.
flowchart LR
A["0 0000101<br/>(+5 em sinal-magnitude)"]
B["1 0000101<br/>(-5 em sinal-magnitude)"]
C["Zero positivo: 0 0000000<br/>Zero negativo: 1 0000000<br/>(ambiguidade!)"]
A --- C
B --- C
Complemento de Um
O complemento de um de um número binário é obtido invertendo todos os seus bits: cada 0 vira 1 e cada 1 vira 0. Representa-se o número negativo -x pelo complemento de um de x.
Por exemplo:
- +5 = 0000\;0101_2
- -5 = 1111\;1010_2 (inversão bit a bit de 0000\;0101)
Esta representação resolve parcialmente o problema da aritmética: somar em complemento de um é mais simples que em sinal-magnitude. Entretanto, o problema dos dois zeros persiste:
- Zero positivo: 0000\;0000_2
- Zero negativo: 1111\;1111_2
Além disso, operações em complemento de um exigem uma correção adicional conhecida como end-around carry (carry circular): quando a soma produz um carry no bit de sinal, esse carry deve ser somado de volta ao resultado. Esse ajuste, embora possível, complica o hardware.
Complemento de Dois: O Padrão Universal
O complemento de dois é a representação usada em todos os processadores modernos — incluindo o PIC18F4550 — para inteiros com sinal. Ele resolve todos os problemas das abordagens anteriores com elegância matemática.
O complemento de dois de um número x de n bits é definido como 2^n - x. Na prática, você calcula o complemento de dois de duas formas equivalentes:
Método 1: Inverta todos os bits (complemento de um) e some 1 ao resultado.
Método 2: A partir da direita, copie todos os bits até encontrar o primeiro 1 (inclusive), e inverta todos os bits restantes à esquerda.
Definição: Complemento de Dois
Para um número x representado com n bits, seu complemento de dois é:
\overline{x} = 2^n - x
A representação em complemento de dois de um inteiro com sinal x em n bits é:
\text{rep}(x) = \begin{cases} x & \text{se } 0 \leq x \leq 2^{n-1} - 1 \\ 2^n + x & \text{se } -2^{n-1} \leq x < 0 \end{cases}
Com n bits, a faixa representável é [-2^{n-1},\; 2^{n-1} - 1].
Para 8 bits: faixa de -128 a +127. Para 16 bits: faixa de -32.768 a +32.767.
Vamos verificar com um exemplo. Para representar -5 em complemento de dois com 8 bits:
- Representação de +5: 0000\;0101_2
- Inverta todos os bits: 1111\;1010_2 (complemento de um)
- Some 1: 1111\;1011_2
Portanto, -5 = 1111\;1011_2 em complemento de dois de 8 bits.
Verificação: 1111\;1011_2 + 0000\;0101_2 = 1\;0000\;0000_2. O resultado é 256 = 2^8, o que confirma que 1111\;1011_2 é o complemento de dois de 0000\;0101_2.
A propriedade fundamental que torna o complemento de dois poderoso é esta: a subtração se reduz à adição do complemento. Para calcular A - B, basta calcular A + (-B), onde -B é o complemento de dois de B. Isso significa que o mesmo circuito somador serve tanto para adições quanto para subtrações, sem necessitar de hardware dedicado para subtrator. Esta propriedade foi a razão pela qual o complemento de dois substituiu todas as alternativas.
Outra propriedade valiosa: há apenas um zero. O número zero é representado unicamente por 0000\;0000_2, eliminando a ambiguidade das representações anteriores.
Vantagens do Complemento de Dois em Resumo
O complemento de dois tornou-se padrão universal porque:
- Representação única do zero: existe apenas um padrão de bits para zero.
- Subtração como adição: A - B = A + \text{comp2}(B), simplificando drasticamente o hardware.
Entendendo a Faixa Assimétrica
Você deve ter notado que com 8 bits em complemento de dois, a faixa vai de -128 a +127 — um número negativo a mais que positivos. Por quê?
Porque o padrão de bits 1000\;0000_2 precisava ser atribuído a algum valor. Em complemento de dois, esse padrão representa -128. Não existe +128 em 8 bits com sinal. Isso tem uma consequência prática importante: se você tentar calcular o complemento de dois de -128, aplicando a inversão de bits e somando 1, obtém novamente 1000\;0000_2. Em outras palavras, -128 é seu próprio complemento de dois — e é por isso que deve ser tratado com cuidado em código.
Extensão de Sinal
Frequentemente você precisa converter um número de n bits para uma representação mais ampla de m > n bits. Para inteiros sem sinal, isso é simples: basta preencher os bits à esquerda com zeros. Para inteiros com sinal em complemento de dois, você deve replicar o bit de sinal (MSB) em todos os novos bits à esquerda.
Exemplo: -5 em 8 bits é 1111\;1011_2. Em 16 bits, é 1111\;1111\;1111\;1011_2. Perceba que todos os 8 bits novos são 1, que é o valor do MSB original.
Se você cometesse o erro de preencher com zeros (0000\;0000\;1111\;1011_2), obteria o valor +251 em vez de -5 — um bug sutil e devastador.
Aritmética em Complemento de Dois
Agora que você entende como os números são representados, vamos ver como as operações aritméticas funcionam sobre eles. O ponto central é que, em complemento de dois, a adição funciona diretamente sobre os padrões de bits, independentemente do sinal.
Adição
Somar dois números em complemento de dois é idêntico a somar dois números binários sem sinal. Você simplesmente soma bit a bit, propagando o carry. O resultado, interpretado em complemento de dois, será correto desde que não haja overflow.
Exemplo: +7 + (-3):
\begin{array}{r} 0000\;0111_2 \quad (+7)\\ + 1111\;1101_2 \quad (-3)\\ \hline 0000\;0100_2 \quad (+4) \end{array}
O carry final (que seria 1 além do 8º bit) é simplesmente descartado. O resultado 0000\;0100_2 = +4 está correto.
Subtração
Como mencionado, subtração é adição do complemento de dois. Para calcular A - B:
- Calcule o complemento de dois de B
- Some A ao complemento de dois de B
Exemplo: +5 - (+8):
- Complemento de dois de +8 = 0000\;1000_2: inverte → 1111\;0111_2, soma 1 → 1111\;1000_2 (que representa -8)
- Some: 0000\;0101 + 1111\;1000 = 1111\;1101_2
O resultado 1111\;1101_2 em complemento de dois de 8 bits vale -3. E 5 - 8 = -3. Correto!
Overflow: Quando o Resultado Não Cabe
O overflow ocorre quando o resultado de uma operação aritmética excede a faixa representável com o número de bits disponível. Em complemento de dois de 8 bits, a faixa é [-128, +127]. Se você somar +100 e +50, o resultado matemático correto é +150, mas isso não cabe em 8 bits com sinal — o resultado armazenado será incorreto.
Definição: Overflow em Complemento de Dois
O overflow ocorre em uma soma quando:
- Os dois operandos são positivos e o resultado tem MSB igual a 1 (parece negativo), ou
- Os dois operandos são negativos e o resultado tem MSB igual a 0 (parece positivo).
Em outros termos: há overflow se e somente se os sinais dos dois operandos são iguais mas o sinal do resultado é diferente.
Formalmente, para A + B = S em n bits, overflow ocorre quando:
\text{overflow} = (a_{n-1} = b_{n-1}) \; \wedge \; (s_{n-1} \neq a_{n-1})
onde a_{n-1}, b_{n-1} e s_{n-1} são os bits de sinal de A, B e S, respectivamente.
Um detalhe importante: o carry out (o bit que “sobra” além do bit mais significativo) não indica overflow em complemento de dois. O carry out pode ocorrer sem overflow e vice-versa. Não confunda os dois!
Exemplo de overflow positivo: +100 + +50 em 8 bits: 0110\;0100 + 0011\;0010 = 1001\;0110_2 = -106
O resultado esperado (+150) não cabe em 8 bits com sinal. O hardware calculou corretamente em aritmética modular \pmod{256}, mas o resultado interpretado em complemento de dois é -106 — que é claramente errado.
O PIC18F4550 possui um bit de status chamado OV (Overflow) no registrador STATUS. Este bit é automaticamente setado quando uma operação aritmética produz overflow em complemento de dois. Seu código deve verificar este flag após operações críticas.
Multiplicação
A multiplicação de dois números de n bits pode produzir um resultado com até 2n bits. Isso é diferente da adição, onde o resultado em n bits é suficiente (com detecção de overflow). Para a multiplicação, você precisa alocar espaço suficiente para o resultado ou utilizar tipos de dados mais largos.
O PIC18F4550 possui uma instrução de multiplicação sem sinal de 8 bits (MULWF) que armazena o resultado de 16 bits nos registradores PRODH:PRODL. Se você multiplicar dois uint8_t, o resultado deve ser armazenado em uint16_t para não perder bits.
O Padrão IEEE 754: Representação de Ponto Flutuante
Números inteiros são suficientes para muitos problemas, mas considere os seguintes casos: calcular a constante \pi = 3,14159\ldots, medir uma resistência de 0,0047\;\Omega, ou processar o número de Avogadro 6,022 \times 10^{23}. Para representar valores muito grandes, muito pequenos ou com partes fracionárias, os computadores utilizam a notação de ponto flutuante.
Motivação: Notação Científica Binária
Você já usa notação científica no cotidiano sem perceber. Quando um físico escreve 6,022 \times 10^{23}, ele está separando o número em uma mantissa (6,022) e um expoente (23). Esta representação permite cobrir amplitudes de valores enormes com precisão relativa constante.
O mesmo princípio se aplica em binário. Um número em ponto flutuante binário é representado na forma:
\pm \; m \; \times \; 2^e
onde m é a mantissa (também chamada de significando ou fração) e e é o expoente.
O padrão IEEE 754, publicado em 1985 e revisado em 2008, define exatamente como essa representação deve ser codificada em bits. Ele é o padrão utilizado em virtualmente todo hardware de computação moderno, desde microcontroladores até supercomputadores.
Estrutura do IEEE 754 Precisão Simples (32 bits)
O formato de precisão simples (float em C) usa 32 bits divididos em três campos:
flowchart LR
S["Sinal<br/>1 bit<br/>(bit 31)"]
E["Expoente<br/>8 bits<br/>(bits 30-23)"]
M["Mantissa<br/>23 bits<br/>(bits 22-0)"]
S --- E --- M
Definição: IEEE 754 Precisão Simples
Um número em ponto flutuante de precisão simples é representado por:
(-1)^s \times 1,f \times 2^{e - 127}
onde:
- s: bit de sinal (0 = positivo, 1 = negativo)
- e: campo do expoente de 8 bits (valor armazenado, sem sinal)
- f: campo da fração de 23 bits
- e - 127: expoente real (o valor 127 é chamado de bias ou viés)
- 1,f: o “1” implícito antes da vírgula não é armazenado (normalização)
A faixa de expoentes regulares é e \in [1, 254], correspondendo a expoentes reais de -126 a +127.
Vamos decompor o número -6,75 em IEEE 754 de precisão simples:
Sinal: -6,75 é negativo, portanto s = 1.
Converter 6,75 para binário:
- Parte inteira: 6 = 110_2
- Parte fracionária: 0,75 \times 2 = 1,5 → bit 1; 0,5 \times 2 = 1,0 → bit 1
- Portanto: 6,75 = 110,11_2
Normalizar: 110,11_2 = 1,1011_2 \times 2^2 (deslocamos a vírgula 2 posições à esquerda)
Expoente armazenado: e = 2 + 127 = 129 = 1000\;0001_2
Fração: a parte após “1,” é 1011, completada com zeros até 23 bits: 10110000000000000000000
Resultado final: \underbrace{1}_{s}\;\underbrace{10000001}_{e}\;\underbrace{10110000000000000000000}_{f} Em hexadecimal:
0xC0D80000
O Papel do Bias (Viés)
Por que o expoente é armazenado com bias de 127 em vez de usar complemento de dois? A razão é prática: com bias, o expoente armazenado é sempre um número sem sinal (de 0 a 255). Isso permite comparar dois números em ponto flutuante simplesmente comparando seus padrões de bits inteiros, da esquerda para direita, sem precisar tratar o sinal do expoente separadamente. É um truque engenhoso que simplifica tanto o hardware quanto o software de comparação.
O “1” Implícito: Ganhando Precisão Gratuitamente
Em um número binário normalizado, a parte inteira é sempre 1 (nunca 0, nunca 2 ou mais). Por exemplo: 1,101_2 \times 2^3. Como a parte inteira é sempre 1, ela não precisa ser armazenada — o hardware assume que está lá. Esse “1 implícito” efetivamente nos dá 24 bits de precisão na mantissa, mesmo usando apenas 23 bits de armazenamento.
Valores Especiais do IEEE 754
O padrão reserva padrões de bits especiais para representar casos limites. Conhecê-los é importante para escrever código robusto.
Valores Especiais no IEEE 754 (Precisão Simples)
| Condição | Expoente (e) | Fração (f) | Valor |
|---|---|---|---|
| Zero positivo | 0 | 0 | +0 |
| Zero negativo | 0 | 0 (bit s=1) | -0 |
| Subnormal | 0 | \neq 0 | 0,f \times 2^{-126} |
| Infinito positivo | 255 | 0 | +\infty |
| Infinito negativo | 255 | 0 (bit s=1) | -\infty |
| NaN (Not a Number) | 255 | \neq 0 | Indefinido |
NaN ocorre em operações matematicamente indefinidas: \sqrt{-1}, 0/0, \infty - \infty. Infinito resulta de operações que excedem o maior representável: 1,0 / 0,0 em ponto flutuante não é erro — é +\infty.
Os números subnormais (também chamados de desnormalizados) permitem representar valores muito próximos de zero, menores que o mínimo normal 2^{-126}, ao custo de precisão reduzida. Eles evitam o chamado “underflow abrupto” — onde um resultado muito pequeno simplesmente se torna zero — substituindo-o por um “underflow gradual”.
IEEE 754 Precisão Dupla (64 bits)
O tipo double em C usa 64 bits: 1 bit de sinal, 11 bits de expoente (bias = 1023) e 52 bits de fração. Isso permite representar valores com muito mais precisão e em faixas maiores. No PIC18F4550, que é um processador de 8 bits sem FPU (Floating-Point Unit), operações com double são implementadas em software pelo compilador XC8 e são significativamente mais lentas que operações com float.
Erros de Arredondamento e Limitações do Ponto Flutuante
Este é o tema que mais frequentemente causa bugs sutis e difíceis de diagnosticar em sistemas que utilizam ponto flutuante. A raiz do problema é simples: nem todo número decimal tem representação binária exata com finitos bits.
O Problema do 0,1 Decimal
O número 0,1 decimal parece simples, mas em binário ele é uma dízima periódica:
0,1_{10} = 0,0\overline{0011}_2 = 0,000110011001100110011\ldots_2
Armazenado com 23 bits de fração (precisão simples), ele é arredondado para o valor mais próximo representável, que é aproximadamente 0,100000001490116\ldots. O erro é da ordem de 10^{-8} — pequeno, mas não zero.
A consequência prática mais famosa é que 0.1 + 0.2 != 0.3 em ponto flutuante:
Comparações de Igualdade com Ponto Flutuante
Uma regra fundamental: nunca compare valores em ponto flutuante com ==. Em vez disso, verifique se a diferença absoluta é menor que uma tolerância (épsilon):
Acumulação de Erros
Em cálculos iterativos — como um loop que soma muitos pequenos valores — os erros de arredondamento se acumulam. Uma soma de 1.000 termos, cada um com erro de 10^{-8}, pode resultar em erro total de 10^{-5}. Dependendo da aplicação, isso pode ser aceitável ou catastrófico.
Em sistemas embarcados como o PIC18F4550, onde o processamento de ponto flutuante é feito em software e é muito mais lento que operações inteiras, a recomendação geral é: prefira aritmética inteira quando possível, usando ponto fixo (fixed-point) para valores fracionários.
A representação em ponto fixo usa inteiros com um fator de escala implícito. Por exemplo, para representar temperaturas com precisão de 0,1 °C, você pode usar um int16_t com escala de 10 — o valor 237 representaria 23,7 °C. Operações em ponto fixo são rápidas no PIC e sem erros de arredondamento de ponto flutuante.
Circuitos Aritméticos: Do Software ao Hardware
Até aqui você viu como números são representados. Agora vamos entender como o hardware realiza as operações aritméticas — porque isso afeta diretamente o desempenho do seu código e as limitações do processador.
O Somador de Um Bit (Half Adder e Full Adder)
A unidade mais básica da aritmética digital é o somador de 1 bit. Um half adder (meio somador) recebe dois bits A e B e produz a soma S e o carry C_{out}:
| A | B | S | C_{out} |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
As equações booleanas são: S = A \oplus B (XOR) e C_{out} = A \cdot B (AND).
Um full adder (somador completo) adiciona um terceiro input: o carry de entrada C_{in}. Isso permite encadear somadores de 1 bit para formar somadores de múltiplos bits.
flowchart LR
A["A"]
B["B"]
Cin["C_in"]
FA["Full Adder"]
S["Soma (S)"]
Cout["Carry Out (C_out)"]
A --> FA
B --> FA
Cin --> FA
FA --> S
FA --> Cout
As equações do full adder são:
S = A \oplus B \oplus C_{in} C_{out} = (A \cdot B) + (A \cdot C_{in}) + (B \cdot C_{in})
Ripple Carry Adder: Simples, Mas Lento
Para somar dois números de n bits, podemos encadear n full adders em série, onde o carry out de cada estágio alimenta o carry in do próximo. Esta configuração é chamada de Ripple Carry Adder (somador com propagação de carry).
flowchart LR
FA0["FA<br/>bit 0"] -- "C1" --> FA1["FA<br/>bit 1"]
FA1 -- "C2" --> FA2["FA<br/>bit 2"]
FA2 -- "C3" --> FA3["FA<br/>bit 3"]
A0["A0, B0, C_in=0"] --> FA0
A1["A1, B1"] --> FA1
A2["A2, B2"] --> FA2
A3["A3, B3"] --> FA3
FA0 --> S0["S0"]
FA1 --> S1["S1"]
FA2 --> S2["S2"]
FA3 --> S3["S3"]
FA3 --> Cout["C_out"]
O problema do Ripple Carry Adder é a latência: o carry precisa se propagar em série do bit 0 até o bit n-1. Para um somador de 32 bits, o carry pode precisar passar por 32 estágios antes que o resultado final seja válido. Em hardware com portas lógicas reais, cada porta introduz um atraso de propagação, e 32 atrasos em série somam-se para criar um gargalo de velocidade.
Carry-Lookahead: Paralelismo na Aritmética
O Carry-Lookahead Adder (somador com antecipação de carry) resolve este problema calculando os carries de todos os bits em paralelo, em vez de em série. A ideia central é que o carry para o bit i pode ser determinado diretamente a partir dos bits de entrada, sem esperar que o carry se propague bit a bit.
Define-se dois sinais auxiliares para cada bit i:
- Geração: G_i = A_i \cdot B_i — indica que o carry é gerado neste bit (ambas as entradas são 1)
- Propagação: P_i = A_i \oplus B_i — indica que o carry de entrada seria propagado para a saída
Com estes sinais, os carries podem ser calculados diretamente:
C_1 = G_0 + P_0 \cdot C_0 C_2 = G_1 + P_1 \cdot G_0 + P_1 \cdot P_0 \cdot C_0 C_3 = G_2 + P_2 \cdot G_1 + P_2 \cdot P_1 \cdot G_0 + P_2 \cdot P_1 \cdot P_0 \cdot C_0
Todos os carries podem ser calculados em paralelo, com no máximo 3 níveis de lógica — independentemente do número de bits. Isso reduz drasticamente a latência do somador, ao custo de maior complexidade de hardware.
A ULA do PIC18F4550 usa somador com antecipação de carry para realizar operações de 8 bits em um único ciclo de clock, independentemente dos padrões de bits.
Multiplicação Binária: Shifts e Somas
A multiplicação binária sem sinal segue o mesmo algoritmo que a multiplicação longa que você aprendeu na escola primária, mas com base 2 — o que simplifica os cálculos, já que multiplicar por 0 ou por 1 é trivial.
Para multiplicar A \times B:
Para cada bit b_i de B:
- Se b_i = 1: some A deslocado i posições à esquerda ao resultado parcial
- Se b_i = 0: contribuição zero
Exemplo: 5 \times 3 em binário:
\begin{array}{r} 0101 \quad (5)\\ \times\; 0011 \quad (3)\\ \hline 0101 \quad (5 \times 2^0)\\ 1010 \quad (5 \times 2^1)\\ \hline 1111 \quad = 15 \end{array}
O algoritmo de Booth estende este conceito para multiplicação com sinal em complemento de dois, codificando a multiplicação de forma a reduzir o número de somas parciais necessárias. Embora mais complexo, o algoritmo de Booth é importante porque permite construir multiplicadores de hardware eficientes que operam com números negativos sem conversão prévia.
O PIC18F4550 suporta multiplicação de 8 bits sem sinal (instrução MULWF), produzindo resultado de 16 bits em PRODH:PRODL. Para multiplicações com sinal ou de maior precisão, você precisará implementar o algoritmo em software.
Conexão com o Projeto Integrador
Neste módulo, as tarefas do Projeto Integrador exigem que você aplique diretamente os conceitos estudados. Vamos examinar cada uma delas com atenção para que você chegue às sessões de tutoria bem preparado.
Tarefa 1: Conversão Numérica para o Display LCD
O display LCD Sunstar 2004A conectado ao kit ACEPIC PRO V8.2 exibe caracteres ASCII. Para mostrar um número, você precisa converter o valor binário interno para uma representação de dígitos decimais que o display entenda. Isso envolve o algoritmo de divisões sucessivas.
O algoritmo básico funciona assim: dado um valor inteiro V, divida-o repetidamente por 10, coletando os restos. Cada resto é um dígito decimal, do menos significativo ao mais significativo. Ao final, reverta a ordem e adicione ‘0’ (ASCII 48) a cada dígito para obter o caractere correspondente.
Para números negativos em complemento de dois, você deve primeiro verificar o sinal, tomar o valor absoluto, executar a conversão e prepender o caractere ‘-’.
Tarefa 2: Detecção de Overflow nas Operações Aritméticas
Sua segunda tarefa exige implementar rotinas de adição, subtração e multiplicação que detectam e sinalizam overflow. A detecção de overflow em adição de complemento de dois, como vimos, baseia-se na comparação dos sinais dos operandos com o sinal do resultado.
Tarefa 3: Experimentos com Ponto Flutuante
A terceira tarefa é um exercício de investigação: você criará um programa que demonstra, de forma concreta, as limitações da aritmética em ponto flutuante. O objetivo é observar e documentar casos onde o comportamento do computador diverge do que a matemática exata preveria.
Alguns experimentos sugeridos:
Experimento 1: Verifique se 0.1f + 0.2f == 0.3f. Você descobrirá que não é. Exiba no LCD a diferença.
Experimento 2: Some repetidamente 0,1f por 10 iterações. Compare o resultado com 10 \times 0,1f. Os dois deveriam ser 1,0f, mas podem diferir.
Experimento 3: Cancamento subtrativo. Calcule (x + \delta) - x para um valor pequeno de \delta e um x grande. Se x for suficientemente grande em relação a \delta, x + \delta arredondará para x, e o resultado será 0 em vez de \delta.
Experimento 4: Demonstre que a ordem das operações importa. Para x, y, z com x muito maior que y e z: (x + y) + z pode dar resultado diferente de x + (y + z).
Esses experimentos não são bugs no seu código — são comportamentos previsíveis e documentados do padrão IEEE 754. Documentar esses resultados no relatório e no diário do projeto é o objetivo central da tarefa.
Síntese: Conectando os Conceitos
Você estudou neste módulo uma cadeia de conceitos que se apoia continuamente. O sistema binário é a linguagem natural do hardware. As representações de inteiros (especialmente o complemento de dois) são a forma como esse hardware lida com números negativos de maneira eficiente. A aritmética inteira — adição, subtração, multiplicação — é implementada diretamente em circuitos digitais construídos com somadores. O ponto flutuante IEEE 754 estende a representabilidade para valores muito grandes, muito pequenos e fracionários, ao custo de precisão limitada e erros de arredondamento.
O diagrama abaixo resume as relações entre esses conceitos e sua ligação com o PIC18F4550:
flowchart TB
subgraph Hardware["Hardware PIC18F4550"]
direction TB
ULA["ULA de 8 bits<br/>(Carry-Lookahead)"]
MULWF["Instrução MULWF<br/>(8x8 → 16 bits)"]
STATUS["Registrador STATUS<br/>(bits C, Z, OV, N)"]
PRODHL["PRODH:PRODL<br/>(resultado 16 bits)"]
end
subgraph Software["Software do Projeto"]
direction TB
CONV["Rotinas de Conversão<br/>para Display LCD"]
ARITH["Operações Seguras<br/>com Detecção de Overflow"]
FLOAT["Experimentos com<br/>Ponto Flutuante"]
end
C2["Complemento de Dois<br/>Int8, Int16"] --> ULA
IEEE754["IEEE 754<br/>Float 32 bits"] -- "Software em XC8" --> CONV
ULA --> STATUS
MULWF --> PRODHL
C2 --> ARITH
IEEE754 --> FLOAT
CONV --> Software
ARITH --> Software
FLOAT --> Software
Registrador STATUS do PIC18F4550 e os Flags Aritméticos
O registrador STATUS do PIC18F4550 contém bits que registram o resultado das operações da ULA. Os mais relevantes para a aritmética são:
- C (Carry): setado quando há carry out do bit 7 na adição, ou borrow na subtração.
- Z (Zero): setado quando o resultado da operação é zero.
- OV (Overflow): setado quando há overflow em aritmética de complemento de dois.
- N (Negative): setado quando o bit 7 do resultado é 1 (resultado parece negativo em complemento de dois).
- DC (Digit Carry): carry do nibble inferior para o superior (bit 3 para bit 4). Útil para aritmética BCD.
Seu código deve consultar estes flags após operações aritméticas para verificar condições de overflow e outros erros.
Representação BCD: Quando o Decimal Precisa Ser Exato
Existe uma forma alternativa de codificar números decimais em binário que merece atenção: o BCD (Binary Coded Decimal), ou decimal codificado em binário. Na representação BCD, cada dígito decimal é codificado separadamente em 4 bits (um nibble). O número decimal 2025, por exemplo, seria representado como:
2 \quad 0 \quad 2 \quad 5 0010 \; 0000 \; 0010 \; 0101_{\text{BCD}}
Em binário puro, 2025 = 0111\;1110\;1001_2. As duas representações usam quantidades diferentes de bits e têm propriedades completamente distintas.
O BCD tem uma vantagem específica: conversão para display decimal é trivial. Cada nibble já é um dígito decimal, bastando adicionar ‘0’ (ASCII 48) para obter o caractere correspondente. Não há algoritmo de divisões sucessivas necessário. Por isso, algumas calculadoras financeiras e displays de relógio usam BCD internamente.
A desvantagem do BCD é a ineficiência: com 4 bits, você poderia representar 16 valores (0 a 15), mas o BCD usa apenas os 10 primeiros (0 a 9). Os 6 padrões restantes (1010 a 1111) são inválidos em BCD. Isso significa desperdício de capacidade de armazenamento e operações aritméticas mais complexas.
O PIC18F4550 possui suporte nativo a operações BCD através do flag DC (Digit Carry) no registrador STATUS e da instrução DAA (Decimal Adjust After Addition), que ajusta o resultado de uma adição binária para a representação BCD correta. Para projetos que precisam exibir valores decimais com precisão exata sem erros de arredondamento — como medidores de precisão, sistemas de faturamento — o BCD pode ser a escolha certa.
Representação de Caracteres e Dados Não Numéricos
Embora o foco deste módulo seja a representação numérica, vale entender brevemente como caracteres de texto são representados, pois isso aparecerá naturalmente no Projeto Integrador quando você enviar strings para o display LCD.
O padrão ASCII (American Standard Code for Information Interchange) mapeia 128 caracteres (letras, dígitos, pontuação e caracteres de controle) para valores de 7 bits (0 a 127). No PIC18F4550, caracteres são tipicamente armazenados como uint8_t ou char (8 bits), usando os 7 bits inferiores para o código ASCII.
Os dígitos ‘0’ a ‘9’ têm códigos ASCII 48 a 57. Por isso, para converter um dígito decimal d \in [0, 9] para seu caractere ASCII, basta calcular d + 48 ou d + \text{'0'}. Esta operação aparece com frequência nas rotinas de conversão que você implementará.
As letras maiúsculas ‘A’ a ‘Z’ têm códigos 65 a 90, e as minúsculas ‘a’ a ‘z’ têm códigos 97 a 122. A diferença entre maiúscula e minúscula é sempre 32 — o que significa que você pode alternar entre maiúscula e minúscula simplesmente setando ou limpando o bit 5 do código ASCII. Isso é um reflexo da elegância da representação binária.
A Aritmética no Contexto do PIC18F4550
O PIC18F4550 é um microcontrolador de 8 bits, o que significa que sua ULA opera nativamente com palavras de 8 bits. Vamos examinar como isso se manifesta na prática e quais cuidados você deve ter ao escrever código C para este processador.
Promoção de Tipos em C
O compilador MPLAB XC8, ao compilar código C para o PIC18F4550, segue as regras de promoção de tipos do padrão C. Uma das regras mais importantes é que operações aritméticas em tipos menores que int são realizadas após promoção para int. No XC8 configurado para o PIC18, int pode ter 16 bits.
Isso tem implicações: se você somar dois uint8_t, o resultado intermédio é calculado como int (16 bits) antes de ser truncado de volta para uint8_t. Em muitos casos, isso é o comportamento desejado, pois evita overflow não intencional. Mas pode ser fonte de confusão se você esperar que a soma aconteça em 8 bits.
Considere este caso: dois valores uint8_t com valor 200 cada. A soma matemática é 400. Em uint8_t, 400 mod 256 = 144 — overflow. Mas como o cálculo é promovido para int16_t, o resultado intermediário é 400, e apenas ao atribuir de volta para uint8_t ocorre o truncamento para 144. O comportamento é o mesmo na prática, mas compreender por que a soma ocorre “em 16 bits” internamente ajuda a evitar suposições incorretas.
Custo das Operações no PIC18F4550
No PIC18F4550, diferentes operações aritméticas têm custos muito diferentes em termos de ciclos de clock:
| Operação | Tipo | Ciclos Aproximados |
|---|---|---|
| Adição/Subtração 8 bits | int8_t, uint8_t |
1 |
| Adição/Subtração 16 bits | int16_t, uint16_t |
2–4 |
| Adição/Subtração 32 bits | int32_t, uint32_t |
4–8 |
| Multiplicação 8×8 bits | uint8_t × uint8_t |
1 (MULWF) |
| Multiplicação 16×16 bits | Rotina software | ~20–40 |
| Divisão 16 bits | Rotina software | ~50–100 |
Operação float |
Biblioteca software | ~100–1000 |
Esta tabela ilustra por que a escolha do tipo de dado tem impacto direto no desempenho do seu firmware. Para um sistema de aquisição de dados que deve processar centenas de amostras por segundo, a diferença entre usar float e usar inteiros com ponto fixo pode ser a diferença entre um sistema funcional e um que não consegue acompanhar o ritmo dos dados.
Divisão e o Problema com Números Negativos
A divisão inteira em C para números negativos pode surpreender. A linguagem C define que a divisão trunca em direção ao zero (truncation toward zero). Portanto:
- 7 / 2 = 3 (truncamento de 3,5)
- -7 / 2 = -3 (truncamento de -3,5 em direção ao zero, não -4)
Em linguagem assembly, operações de deslocamento à direita (>>) são frequentemente usadas como atalho para divisão por potências de 2. Para inteiros sem sinal, x >> k é equivalente a x / 2^k. Para inteiros com sinal, o comportamento depende do compilador: em C, o deslocamento aritmético à direita (que preenche com o bit de sinal) é implementado-defined para valores negativos. No XC8 para PIC18, o comportamento típico é o deslocamento aritmético, mas você deve verificar a documentação para código crítico.
Esta distinção entre divisão truncada e divisão por deslocamento de inteiros negativos é outra fonte potencial de bugs sutis. Sempre que possível, prefira / 2 a >> 1 em código legível, e deixe o compilador otimizar se necessário.
Números em Ponto Fixo: A Alternativa Prática para o PIC
Dado o custo elevado de operações em ponto flutuante no PIC18F4550 (sem FPU dedicada), a técnica de ponto fixo (fixed-point arithmetic) é amplamente usada em sistemas embarcados de baixo custo. Entender ponto fixo completa o quadro de representação numérica que você precisa dominar neste módulo.
Na representação em ponto fixo, você usa um inteiro para representar um valor fracionário, com um fator de escala implícito. Por convenção, define-se que os últimos f bits do inteiro representam a parte fracionária. Uma notação comum é Qn.f, onde n é o número de bits inteiros (incluindo o sinal) e f é o número de bits fracionários.
Por exemplo, um número em formato Q8.8 usa 16 bits no total: os 8 bits superiores para a parte inteira com sinal e os 8 bits inferiores para a parte fracionária. Para representar o valor 3,5 em Q8.8, calculamos 3,5 \times 256 = 896 = 0x0380. Para representar -1,25 em Q8.8: -1,25 \times 256 = -320, que em complemento de dois de 16 bits é 0xFF40.
Aritmética em Ponto Fixo
Para dois números em ponto fixo com escala 2^f:
Adição/Subtração: direta, pois ambos os operandos têm a mesma escala.
A + B = (a \cdot 2^f) + (b \cdot 2^f) = (a + b) \cdot 2^f
Multiplicação: o resultado tem escala 2^{2f}, portanto deve-se deslocar f bits à direita para retornar à escala 2^f.
A \times B = (a \cdot 2^f) \times (b \cdot 2^f) = (a \times b) \cdot 2^{2f} \text{Resultado escalado corretamente:} \quad \frac{A \times B}{2^f} = (a \times b) \cdot 2^f
Conversão para inteiro: deslocar f bits à direita (ou dividir por 2^f).
Conversão de inteiro para ponto fixo: deslocar f bits à esquerda (ou multiplicar por 2^f).
A grande vantagem do ponto fixo é que toda a aritmética é feita com operações inteiras — que no PIC18F4550 são rápidas, especialmente para 8 e 16 bits. A desvantagem é que você precisa gerenciar manualmente o fator de escala e garantir que o resultado não ultrapasse os limites do tipo de dado.
Um exemplo prático: no Projeto Integrador, se você precisar calcular uma média de leituras de temperatura com uma casa decimal, poderia usar int16_t com escala 10: o valor 237 representaria 23,7 °C. Somar 10 leituras e dividir por 10 resultaria em uma média com precisão de 0,1 °C, sem usar float.
Tabela Comparativa: Representações Numéricas
Para consolidar seu entendimento, a tabela a seguir compara as principais representações numéricas estudadas neste módulo, com foco em como elas se aplicam ao contexto do PIC18F4550 e do Projeto Integrador.
| Representação | Tipo C | Bits | Faixa | Precisão | Custo no PIC | Uso Típico |
|---|---|---|---|---|---|---|
| Inteiro sem sinal | uint8_t |
8 | 0 a 255 | Exata | Muito baixo | Contadores, endereços |
| Inteiro sem sinal | uint16_t |
16 | 0 a 65535 | Exata | Baixo | Timers, somas |
| Inteiro com sinal | int8_t |
8 | -128 a +127 | Exata | Muito baixo | Temperatura, erros |
| Inteiro com sinal | int16_t |
16 | -32768 a +32767 | Exata | Baixo | Medições amplas |
| Ponto fixo Q8.8 | int16_t |
16 | -128 a ~127,996 | 1/256 ≈ 0,004 | Baixo | Frações sem FPU |
| Ponto flutuante | float |
32 | ~±3,4×10³⁸ | ~7 dígitos | Alto (software) | Cálculos gerais |
| Ponto flutuante | double |
64 | ~±1,8×10³⁰⁸ | ~15 dígitos | Muito alto (software) | Precisão máxima |
Esta tabela deve guiar suas decisões de design no Projeto Integrador. Para cada variável do seu sistema, pergunte-se: qual é a faixa de valores possível? Qual precisão é necessária? Posso usar inteiros? Se a resposta para a última pergunta for sim, use inteiros.
Overflow e Underflow: Uma Análise Profunda
Agora que você tem uma visão completa das representações numéricas, vale aprofundar o estudo de dois fenômenos que causam erros silenciosos e difíceis de rastrear em sistemas embarcados: o overflow e o underflow.
Overflow em Inteiros Sem Sinal
Em inteiros sem sinal, o overflow é definido como a situação em que o resultado de uma operação excede o valor máximo representável. Para um uint8_t, o máximo é 255. Se você somar 200 + 100, o resultado matemático é 300. Em representação de 8 bits sem sinal, o resultado armazenado é 300 \mod 256 = 44.
Este comportamento é chamado de aritmética modular ou wraparound. É completamente determinístico — não há indefinição — mas raramente é o que o programador quer. A linguagem C define que overflow em inteiros sem sinal é comportamento bem-definido (wraparound), ao contrário de inteiros com sinal, cujo overflow é comportamento indefinido (undefined behavior ou UB) em C.
Isso tem implicações práticas: o compilador C assume que seu código não produz UB. Se você escrever código que faz overflow de inteiros com sinal, o compilador tem permissão para otimizá-lo de formas que podem parecer absurdas, mas são tecnicamente corretas dado que o comportamento é indefinido. Em microcontroladores, onde o compilador costuma gerar código mais conservador, isso raramente causa problemas práticos — mas em compiladores modernos altamente otimizados (como GCC com -O2 ou -O3), UB pode levar a resultados completamente imprevisíveis.
A recomendação prática: sempre use inteiros sem sinal (uint8_t, uint16_t) quando souber que os valores jamais serão negativos, e verifique o overflow explicitamente quando necessário.
Underflow em Ponto Flutuante
O underflow em ponto flutuante ocorre quando o resultado de uma operação é menor em magnitude que o menor número positivo normal representável. Para float (IEEE 754 precisão simples), o menor número positivo normal é aproximadamente 1,18 \times 10^{-38}.
Quando um cálculo produz um resultado menor que este limite, duas coisas podem acontecer:
Underflow gradual: o resultado é representado como um número subnormal, com expoente e = 0 e sem o “1 implícito”. Números subnormais têm menos bits de precisão que números normais, mas evitam que o resultado seja simplesmente zerado. A faixa de números subnormais vai de \approx 1,4 \times 10^{-45} (para float) até o mínimo normal.
Underflow para zero: se o resultado é menor que o menor número subnormal representável, ele é arredondado para zero (positivo ou negativo, dependendo do sinal).
No contexto do PIC18F4550, onde operações de ponto flutuante são implementadas em software pela biblioteca do XC8, o tratamento de underflow segue as regras do IEEE 754 mas com custo computacional adicional. Para a maioria das aplicações embarcadas com o PIC, o underflow de ponto flutuante é raro, mas você deve estar ciente de que pode acontecer ao trabalhar com valores muito pequenos.
Overflow em Ponto Flutuante
O overflow de ponto flutuante ocorre quando o resultado de uma operação excede o maior número representável positivo (para float, aproximadamente 3,4 \times 10^{38}). O resultado, conforme o IEEE 754, é +\infty ou -\infty dependendo do sinal.
Diferentemente do overflow inteiro, o overflow de ponto flutuante não causa “wraparound” — o resultado é infinito, e operações subsequentes com infinito seguem as regras do IEEE 754 (infinito + qualquer número finito = infinito, por exemplo). Isso pode propagar um erro de overflow por toda uma cadeia de cálculos, eventualmente produzindo NaN em alguma operação que combine infinito positivo com infinito negativo.
Em sistemas embarcados críticos, recomenda-se verificar resultados de ponto flutuante com funções como isinf() e isnan() da biblioteca <math.h> após operações que possam produzir valores extremos.
A Representação em Outros Contextos: BCD Empacotado e Não-Empacotado
O BCD que apresentamos anteriormente usa 4 bits por dígito. Existem duas variantes práticas que você pode encontrar:
O BCD não-empacotado (unpacked BCD) armazena um dígito decimal por byte, usando os 4 bits inferiores para o valor (0–9) e os 4 bits superiores como zero ou como o código ASCII da zona (0x30, no caso de dígitos ASCII). Esta representação é fácil de usar com displays e interfaces seriais, mas desperdicia metade dos bits de cada byte.
O BCD empacotado (packed BCD) armazena dois dígitos decimais por byte, usando os 4 bits superiores para o dígito mais significativo e os 4 bits inferiores para o menos significativo. Para representar 73, por exemplo, o byte seria 0111\;0011_2 = 0x73. Note como o valor hexadecimal e o valor BCD “parecem” o mesmo número — essa é a propriedade que torna o BCD empacotado tão útil em aplicações com displays de 7 segmentos e relógios de tempo real.
O PIC18F4550 inclui suporte à instrução DAA (Decimal Adjust After Addition) especificamente para facilitar a adição de números BCD empacotados. Após uma adição binária de dois bytes BCD, DAA ajusta o resultado para que cada nibble contenha um dígito decimal válido.
Lógica Booleana e Sua Relação com a Aritmética
A conexão entre lógica booleana e aritmética binária é mais profunda do que parece à primeira vista. O mesmo hardware que implementa portas lógicas AND, OR e XOR pode ser reconfigurado para realizar adições. De fato, as equações do somador completo que vimos — S = A \oplus B \oplus C_{in} e C_{out} = (A \cdot B) + (B \cdot C_{in}) + (A \cdot C_{in}) — são equações booleanas puras.
Isso tem uma implicação fundamental para a compreensão do hardware: a distinção entre “operação lógica” e “operação aritmética” existe no nível do software e da semântica, não necessariamente no nível do hardware. A ULA do PIC18F4550 realiza tanto operações lógicas (AND, OR, XOR, NOT) quanto aritméticas (adição, subtração) usando circuitos construídos com os mesmos tipos de portas lógicas. O que muda é a combinação específica de portas e como os sinais de controle da unidade de controle ativam diferentes caminhos.
Quando você escreve ANDWF PORTD, W em assembly PIC, ou PORTD &= 0x0F em C, você está utilizando a parte lógica da ULA. Quando escreve ADDWF contador, F, usa a parte aritmética. Mas em ambos os casos, são transistores e portas lógicas executando operações sobre os bits — a distinção é apenas de configuração.
As operações lógicas bit a bit têm usos muito práticos em sistemas embarcados. Você as usará constantemente no Projeto Integrador:
Mascaramento (AND): para isolar bits específicos. valor & 0x0F extrai os 4 bits inferiores.
Setagem de bits (OR): para forçar bits a 1 sem alterar os demais. registrador |= 0x01 seta o bit 0.
Limpeza de bits (AND NOT): para forçar bits a 0. registrador &= ~0x01 limpa o bit 0.
Inversão de bits (XOR): para alternar (toggle) bits. registrador ^= 0x01 inverte o bit 0.
Compreender que AND corresponde à multiplicação booleana, OR à adição booleana e XOR à adição módulo 2 conecta a lógica que você viu em Fundamentos de Computação com a aritmética que você estuda aqui.
Exercitando a Intuição: Estimativas Rápidas em Binário
Um programador fluente em sistemas embarcados desenvolve, com o tempo, a capacidade de fazer estimativas rápidas sem recorrer a calculadoras. Algumas regras práticas que valem a pena memorizar:
2^{10} = 1024 \approx 10^3 (um quilo). Esta aproximação é a base da confusão entre kilobyte (1024 bytes) e kilobyte SI (1000 bytes), mas é útil para estimativas rápidas.
2^{20} \approx 10^6 (um mega), 2^{30} \approx 10^9 (um giga).
Para um uint8_t (8 bits), 2^8 = 256 valores possíveis. Para uint16_t (16 bits), 2^{16} = 65.536. Para uint32_t (32 bits), 2^{32} \approx 4,3 \times 10^9.
O bit de sinal “custa” um bit de magnitude: um int8_t vai de -128 a +127, enquanto uint8_t vai de 0 a 255. A faixa total tem o mesmo número de valores (256), apenas distribuída diferentemente.
Um hexadecimal equivale a quatro bits. Dois hexadecimais equivalem a um byte. Quando você lê 0xFF, sabe imediatamente que são 8 bits todos em 1, valor decimal 255. Quando lê 0x80, é 1000\;0000_2, valor decimal 128, e em complemento de dois de 8 bits é -128.
Desenvolver esse tipo de intuição não requer memorização forçada — vem naturalmente com a prática de escrever código de baixo nível e depurar valores em dumps de memória. As sessões do Projeto Integrador serão oportunidades perfeitas para exercitar e consolidar essa fluência.
Você pode se perguntar: por que dedicar tanto tempo a representações numéricas e circuitos aritméticos em 2024, quando compiladores e bibliotecas cuidam automaticamente de todos esses detalhes? A resposta tem várias dimensões.
Primeiro, os compiladores tomam decisões de representação baseadas nas declarações de tipo que você faz. Se você declara uint8_t quando deveria usar int16_t, o compilador faz exatamente o que você pediu — e o overflow que resulta é culpa sua, não do compilador. Entender representações numéricas é pré-requisito para fazer escolhas conscientes de tipo.
Segundo, em sistemas embarcados críticos — controle de aeronaves, dispositivos médicos, automação industrial — erros de overflow ou arredondamento podem ter consequências catastróficas. O acidente com o foguete Ariane 5 em 1996, que custou centenas de milhões de dólares, foi causado por um erro de overflow: um valor de 64 bits em ponto flutuante foi convertido para um inteiro de 16 bits sem verificação de overflow. O resultado excedia 2^{15} e a exceção de hardware resultante derrubou o sistema de orientação do foguete.
Terceiro, compreender como a aritmética funciona em hardware é base para tópicos que você estudará nos próximos módulos: design de ULA (Módulo 4), pipeline e hazards de dados (Módulos 6 e 7), e otimização de código. Você não pode entender por que certas sequências de instruções são mais eficientes que outras sem saber como as operações são executadas nos circuitos.
Finalmente, no contexto do microcontrolador PIC18F4550 do Projeto Integrador, você está trabalhando diretamente com hardware que não tem as camadas de abstração de um computador pessoal. Não há sistema operacional gerenciando exceções de ponto flutuante, não há runtime de Java detectando overflow automaticamente. O que você escreve é o que executa, com todas as consequências das suas escolhas de representação.
Para Aprofundar: Tópicos de Consulta
Se você quiser explorar além do material deste módulo, estes são os tópicos que valem seu tempo:
O padrão IEEE 754-2008 completo está disponível para download gratuito em alguns repositórios acadêmicos. O documento original de William Kahan, criador do padrão, discute a motivação por trás de cada decisão de projeto.
O manual do compilador MPLAB XC8 documenta como os tipos int8_t, int16_t, int32_t, float e double são implementados no PIC18F4550, incluindo as convenções de chamada e os tipos de dado disponíveis.
O datasheet do PIC18F4550 (DS39632E), capítulo “Instruction Set Summary”, descreve o comportamento exato de cada instrução aritmética, incluindo quais bits do registrador STATUS são afetados.
O livro de Patterson e Hennessy, “Organização e Projeto de Computadores”, cobre esses temas com profundidade matemática adicional e exemplos de arquiteturas MIPS e ARM.
Preparação para as aulas presenciais: Antes de vir para a aula, certifique-se de que você consegue, sem consultar o material: converter qualquer número de 8 bits entre decimal, binário e hexadecimal; obter o complemento de dois de qualquer número de 8 bits; e identificar os três campos (sinal, expoente, fração) em um número IEEE 754 de 32 bits. Esses são os fundamentos que o professor assumirá que você já domina ao chegar.