Módulo 02: Representação de Dados e Aritmética Computacional — Resumo

Este resumo condensa o Módulo 02 em uma leitura de revisão. Para o aprofundamento completo, com demonstrações, exemplos adicionais e contexto histórico estendido, recorra a 02_material.qmd e a 02_livro.qmd. Use estas páginas para repassar os conceitos na véspera da prova, antes da tutoria do Projeto Integrador ou durante deslocamentos.

O Computador É Uma Máquina Aritmética Imperfeita

Abra um console e digite 0.1 + 0.2. A resposta 0.30000000000000004 não é bug do navegador nem da minha máquina: é o padrão IEEE 754 funcionando como foi escrito em 1985. Por que esse 4 aparece em qualquer linguagem, em qualquer arquitetura, inclusive no seu PIC18F4550? E, no extremo oposto da curva, como um chip de 8 bits soma inteiros de 16 bits sem ter uma instrução nativa para essa largura? Eu quero que você guarde essas duas perguntas; ao final deste resumo elas terão respostas precisas. O caminho passa pelos sistemas posicionais, atravessa o complemento de dois, mergulha no IEEE 754 e desemboca nos cinco flags do registrador STATUS do PIC18.

flowchart TD
    A["Sistemas de<br/>Numeração"] --> B["Inteiros<br/>sem Sinal"]
    B --> C["Inteiros<br/>com Sinal"]
    C --> D["Ponto Flutuante<br/>IEEE 754"]
    D --> E["Circuitos<br/>Aritméticos"]
    E --> F["Aritmética no<br/>PIC18F4550"]
    F --> G["Projeto<br/>Integrador"]

Os blocos conceituais do módulo, da base teórica ao Projeto Integrador.

Sistemas Posicionais e Conversões Entre Bases

A base 2 não venceu por elegância matemática — projetos ternários como o SETUN soviético chegaram a existir, e a base e é teoricamente ótima — mas por robustez tecnológica: transistores em corte ou saturação distinguem dois níveis de tensão com folga. Um sistema posicional em base b representa o inteiro não negativo N pela soma N = \sum_{i=0}^{n-1} d_i \cdot b^i, com cada d_i \in \{0, \ldots, b-1\}. A unicidade dessa decomposição é o que torna o algoritmo de divisões sucessivas pela base bem definido.

O hexadecimal (b = 16) sobrevive por uma propriedade preciosa: 16 = 2^4, então cada dígito corresponde a um quarteto de bits e a conversão binário-hexadecimal é mero agrupamento. Daí datasheets, listagens do MPLAB X e dumps preferirem \texttt{A35C}_{16} a \texttt{1010 0011 0101 1100}_2. O octal sobrevive nas permissões Unix e nas constantes C com prefixo 0 — atenção: 010 em C vale decimal 8, fonte recorrente de bugs sutis.

flowchart LR
    DEC["173 base 10"] -->|"divisões<br/>sucessivas por 2"| BIN["10101101 base 2"]
    BIN -->|"agrupar 4 bits"| HEX["AD base 16"]
    HEX -->|"expandir nibble"| BIN
    BIN -->|"soma posicional"| DEC

Fluxos de conversão entre bases que precisam virar automáticos.

Da base b para decimal, somam-se as contribuições posicionais. De decimal para base b, faz-se divisões sucessivas lendo os restos do último ao primeiro. Para frações, multiplicações sucessivas, lendo a parte inteira a cada passo. Aqui aparece o fato que sustenta metade dos mistérios do ponto flutuante: 0{,}1_{10} em binário é a dízima 0{,}0\overline{0011}_2. O número que você escreve com despreocupação em C não tem representação binária exata.

Inteiros sem Sinal e Aritmética Modular

Quando o processador soma dois inteiros sem sinal de n bits em um registrador de n bits, ele opera no anel \mathbb{Z}/2^n\mathbb{Z}. Some \texttt{0xFF} com \texttt{0x01} na ULA de 8 bits do PIC18: o resultado matemático é 256, não cabe, o registrador recebe \texttt{0x00} e o flag C do STATUS é setado. A faixa representável é [0, 2^n - 1].

Como o PIC18 tem ULA de 8 bits, declarar uint8_t em C emite uma instrução por operação; uint16_t encadeia duas, propagando carry; uint32_t quatro; uint64_t oito. O custo cresce linearmente com a largura, e essa é uma das primeiras decisões de eficiência conscientes do código embarcado. Prefira sempre tipos de largura explícita (uint8_t, int16_t) a int ou long, que mudam de tamanho entre o PIC e o seu notebook.

Voltando à primeira pergunta de abertura: o compilador soma uint16_t no PIC18 emitindo ADDWF para os bytes baixos (que atualiza C) seguida de ADDWFC para os bytes altos, incluindo o flag C herdado. O carry propagado manualmente entre as duas instruções constrói, em duas etapas, o equivalente a uma soma de 16 bits. Essa é a aritmética de múltipla precisão: o mesmo flag C que detectava transbordo estende indefinidamente a largura, byte a byte, sem alterar um único transistor.

Inteiros com Sinal: O Vencedor É o Complemento de Dois

Três representações para inteiros com sinal disputaram espaço historicamente. O sinal-magnitude usa o bit mais significativo como sinal, mas tem zero duplicado (+0 e -0) e exige somador-subtrator condicional — sobrevive hoje apenas na mantissa do IEEE 754. O complemento de um nega invertendo bits, mas mantém zero duplicado e exige correção end-around carry. O complemento de dois domina todas as arquiteturas comerciais modernas.

flowchart TB
    P["Como representar<br/>números negativos?"]
    P --> SM["Sinal-Magnitude<br/>2 zeros<br/>somador duplo"]
    P --> C1["Complemento de Um<br/>2 zeros<br/>end-around carry"]
    P --> C2["Complemento de Dois<br/>zero único<br/>somador único"]
    SM --> X1["Abandonado<br/>para inteiros"]
    C1 --> X2["Abandonado"]
    C2 --> V["Vencedor universal"]

As três representações históricas e o desfecho industrial.

A definição associa à sequência (b_{n-1}, \ldots, b_0) o valor N = -b_{n-1} \cdot 2^{n-1} + \sum_{i=0}^{n-2} b_i \cdot 2^i, com o bit mais significativo de peso negativo. A faixa [-2^{n-1}, +2^{n-1}-1] é assimétrica e o zero é único. Três consequências decisivas. Primeira: comparações por padrão de bits são triviais. Segunda: a negação é “inverte os bits e soma um”, regra que decorre da congruência -N \equiv 2^n - N \pmod{2^n}. Terceira, decisiva: subtração e soma usam o mesmo somador físico, porque A - B = A + (-B). A instrução SUBWF do PIC18 reaproveita exatamente o silício que executa ADDWF. Esse aproveitamento liberou transistores e foi o motivo principal da hegemonia do esquema.

Truque mental: para descobrir o valor de um padrão em complemento de dois cujo bit mais significativo é 1, subtraia 2^n do valor lido como unsigned. Em 8 bits, \texttt{1111 1011} vale 251 unsigned; em complemento de dois, 251 - 256 = -5.

Ao estender int8_t para int16_t, o bit de sinal precisa ser propagado para os novos bits à esquerda; para unsigned, basta preencher com zeros. A confusão clássica que peço que você evite é tratar carry-out e overflow como sinônimos. Em \texttt{0xFF} + \texttt{0x01} lido como complemento de dois, temos -1 + 1 = 0: nenhum overflow, mas C = 1. Em \texttt{0x7F} + \texttt{0x01}, temos +127 + 1 = -128 truncado: OV = 1, mas C = 0. A regra elegante: overflow ocorre se e somente se os operandos têm o mesmo sinal e o resultado tem sinal oposto, ou seja, \text{overflow} = (s_a \oplus s_r) \wedge (s_b \oplus s_r). O flag OV do STATUS é gerado por essa expressão.

O trecho a seguir, parte da segunda tarefa do Projeto Integrador, implementa a soma com detecção explícita de overflow encadeando dois somadores de 8 bits via flag C:

#include <xc.h>
#include <stdint.h>

uint8_t soma16_ov(int16_t a, int16_t b, int16_t *r) {
    uint16_t ua = (uint16_t)a, ub = (uint16_t)b;
    uint16_t res = ua + ub;
    *r = (int16_t)res;
    uint8_t sa = (ua >> 15) & 1, sb = (ub >> 15) & 1, sr = (res >> 15) & 1;
    return (sa == sb) && (sa != sr);
}
; A em A_H:A_L, B em B_H:B_L; resultado em R_H:R_L
soma16:
    MOVF    A_L, W, ACCESS
    ADDWF   B_L, W, ACCESS    ; soma bytes baixos, atualiza C
    MOVWF   R_L, ACCESS
    MOVF    A_H, W, ACCESS
    ADDWFC  B_H, W, ACCESS    ; soma altos com carry; OV em 16 bits
    MOVWF   R_H, ACCESS
    RETURN

Ponto Flutuante e o Padrão IEEE 754

Inteiros, mesmo em 64 bits, são insuficientes para problemas científicos: a massa de uma molécula é 10^{-26} kg e a do Sol é 10^{30} kg. A solução é a notação científica em base 2: três campos — sinal s, mantissa m \in [1, 2) e expoente e — com N = (-1)^s \cdot m \cdot 2^e. Variar e desloca a janela de precisão; m codifica o número com aproximadamente o mesmo número de algarismos significativos.

flowchart LR
    subgraph FP32["IEEE 754 binário32"]
        S["s<br/>1 bit"] --- E["E<br/>8 bits<br/>bias 127"] --- F["F<br/>23 bits<br/>fração"]
    end
    S --> V["valor = (-1)^s × (1 + F·2^-23) × 2^(E-127)"]
    E --> V
    F --> V

Layout do formato binário32 do IEEE 754.

No formato binário32, uma palavra de 32 bits aloca 1 bit para o sinal, 8 bits para o expoente armazenado E (com bias 127) e 23 bits para a fração F. Para números normais (1 \le E \le 254), o valor é N = (-1)^s \cdot (1 + F \cdot 2^{-23}) \cdot 2^{E - 127}, com o bit implícito 1 à esquerda da vírgula. O binário64 usa 11 bits de expoente (bias 1023) e 52 bits de fração, atingindo precisão relativa 2^{-52} \approx 2{,}2 \times 10^{-16} contra 2^{-23} \approx 1{,}2 \times 10^{-7} da precisão simples.

Três decisões do padrão merecem atenção. O expoente com bias em vez de complemento de dois preserva ordenação por comparação inteira — William Kahan defendeu o esquema porque um comparador único para inteiros e floats valia mais que a simetria estética. O bit implícito da mantissa evita armazenar o 1 inicial obrigatório da forma normalizada, ganhando um bit de precisão; a exceção são os denormais (E = 0), que usam bit implícito 0 e permitem perda gradual de precisão perto de zero. Os valores especiais completam o quadro: E = 0 e F = 0 codificam zero com sinal; E = 255 e F = 0, \pm\infty; E = 255 e F \neq 0, NaN, que propaga em qualquer operação e satisfaz \text{NaN} \neq \text{NaN} por especificação — daí if (x != x) ser a forma canônica de detectá-lo em C.

Os números representáveis não estão uniformemente distribuídos na reta real: a distância entre dois floats consecutivos em torno de 1{,}0 é \approx 1{,}19 \times 10^{-7}; em torno de 10^6, perto de 0{,}12; em torno de 10^{30}, da ordem de 10^{23}. Precisão relativa constante, precisão absoluta variável — exatamente o que ponto flutuante promete entregar. Como 0{,}1 é dízima em binário, o IEEE 754 arredonda para o representável mais próximo, e a soma de dois arredondados acumula erro. Por isso 0{,}1 + 0{,}2 \neq 0{,}3: o terceiro padrão de bits difere do esperado por um único bit no fim da mantissa.

Nunca compare floats por igualdade; compare por tolerância, fabs(a-b) < eps. E reorganize algoritmos numéricos para evitar cancelamento subtrativo, em que dois números próximos em magnitude perdem todos os algarismos significativos ao serem subtraídos.

Repito o conselho que sempre dou: ponto flutuante não é a representação certa para tudo. Para valores monetários é desastroso; sistemas financeiros sérios usam inteiros em centavos. No PIC18, sem FPU, cada soma em float pode consumir 200 ciclos da biblioteca do XC8 — em código crítico, represente grandezas em escalas fixas (centésimos de grau, décimos de milímetro) e converta para exibição só na hora de mostrar no LCD.

Circuitos Aritméticos e a ULA do PIC18F4550

Toda essa aritmética é realizada, no silício, por arranjos simples de portas lógicas. O somador completo de um bit tem três entradas (a, b, c_{in}) e duas saídas dadas por s = a \oplus b \oplus c_{in} e c_{out} = (a \cdot b) + (c_{in} \cdot (a \oplus b)) — cerca de doze transistores em CMOS. Conectar n desses em cadeia, com carry-out alimentando carry-in do próximo, produz o somador ripple-carry.

flowchart LR
    A0["a0,b0"] --> FA0["FA"]
    A1["a1,b1"] --> FA1["FA"]
    A2["a2,b2"] --> FA2["FA"]
    A3["a3,b3"] --> FA3["FA"]
    CIN["cin=0"] --> FA0
    FA0 -->|"carry"| FA1
    FA1 -->|"carry"| FA2
    FA2 -->|"carry"| FA3
    FA3 --> COUT["carry-out<br/>flag C"]
    FA0 --> S0["s0"]
    FA1 --> S1["s1"]
    FA2 --> S2["s2"]
    FA3 --> S3["s3"]

Somador ripple-carry — padrão da ULA do PIC18.

A virtude do ripple-carry é a simplicidade; o vício é a latência sequencial. Processadores rápidos adotam carry-lookahead, que calcula todos os carries em paralelo com latência logarítmica ao custo de área. Para o PIC18, com somador de 8 bits e clock até 12 MHz internos, o ripple-carry basta. A subtração reaproveita o mesmo somador: inverte-se o subtraendo e força-se c_{in} = 1, implementando “inverte e soma um” sem hardware adicional. A multiplicação elementar é shift-and-add, mas o PIC18 traz MULWF, que executa multiplicação 8 \times 8 em um ciclo via matriz combinacional, com resultado em PRODH:PRODL. Para arquiteturas maiores, o algoritmo de Booth examina três bits do multiplicador por vez, unificando o tratamento de unsigned e complemento de dois. A divisão é a operação mais cara; o PIC18 não a tem em hardware, o XC8 implementa em software por subtrações iterativas.

A ULA do PIC18 atualiza cinco flags do STATUS que merecem ser memorizados. C (carry-out do bit 7), DC (carry-out do bit 3, útil para BCD), Z (resultado zero), N (bit 7 do resultado, sinal em complemento de dois) e OV (overflow em complemento de dois). A combinação desses flags com BC/BNC/BZ/BNZ/BN/BNN/BOV/BNOV implementa toda a lógica de controle baseada em comparações.

flowchart TB
    OP["Operação ALU<br/>ADDWF / SUBWF / ..."] --> ST["Registrador STATUS"]
    ST --> C["C: carry-out<br/>unsigned"]
    ST --> DC["DC: digit carry<br/>nibble (BCD)"]
    ST --> Z["Z: resultado zero"]
    ST --> N["N: bit 7<br/>(negativo c2)"]
    ST --> OV["OV: overflow<br/>complemento de 2"]

Os cinco flags do STATUS atualizados pelas operações aritméticas.

Uma armadilha clássica: no PIC18, C = 1 após SUBWF significa sem empréstimo, oposto do x86. Ao escrever assembly com comparações, lembre-se de que BC após SUBWF salta quando minuendo \geq subtraendo. Erros de off-by-one em laços derivam frequentemente dessa inversão.

Pergunta para o intervalo: em um sistema de dosagem de medicamentos em décimos de miligrama, em que representação você armazenaria a dose acumulada — float, int32_t em décimos de mg, ou outra? Justifique pensando em faixa dinâmica, precisão relativa e custo computacional no PIC18.

Aplicação no Projeto Integrador

Três tarefas materializam, no kit ACEPIC PRO V8.2 com o LCD Sunstar 2004A, os blocos teóricos do módulo. A primeira pede que você implemente uma rotina de conversão binário-decimal para exibição no LCD, via divisões sucessivas por 10, com cuidado especial para INT16_MIN (-32\,768), cuja negação direta produziria overflow — a solução é converter para uint16_t, manipular a magnitude como unsigned e prefixar o sinal ao final.

flowchart TB
    N["valor uint16_t"] --> D1["N mod 10 → dígito 0<br/>N := N / 10"]
    D1 --> D2["N mod 10 → dígito 1<br/>N := N / 10"]
    D2 --> D3["... até N = 0"]
    D3 --> INV["lê dígitos<br/>na ordem inversa"]
    INV --> LCD["string para o LCD<br/>Sunstar 2004A"]

Conversão binário-decimal via divisões sucessivas por 10.

A segunda tarefa é a soma de 16 bits com detecção de overflow já apresentada. A entrega pedagógica não está no código, mas no relatório: execute o programa com pares que exercitem os quatro casos da tabela abaixo e confronte C com OV. É nessa tabela que o complemento de dois finalmente se torna operacional na sua cabeça.

Operandos (hex) Leitura unsigned Leitura c2 C OV Comentário
0xFFFF + 0x0001 65535 + 1 -1 + 1 = 0 1 0 carry sem overflow
0x7FFF + 0x0001 32767 + 1 +32767 + 1 erra 0 1 overflow sem carry
0x8000 + 0xFFFF 32768 + 65535 -32768 + (-1) erra 1 1 ambos os flags
0x1000 + 0x2000 4096 + 8192 +4096 + 8192 0 0 sem condição excepcional

A terceira tarefa é experimental: executar 0{,}1 + 0{,}2 e (1 + \epsilon) - 1 no próprio PIC18, exibindo o resultado no LCD e registrando no relatório a representação hexadecimal dos quatro valores envolvidos (0{,}1, 0{,}2, 0{,}3 e a soma) como sequências de 4 bytes em IEEE 754 binário32, inspecionadas no simulador do MPLAB X. Você verá com os próprios olhos que os padrões de 0{,}1 e 0{,}2 não correspondem aos racionais exatos e que sua soma difere do padrão de 0{,}3 por um único bit no fim da mantissa.

flowchart TB
    L1["ADDWF<br/>(byte baixo)"] -->|"propaga<br/>flag C"| L2["ADDWFC<br/>(byte alto)"]
    L2 --> R["Resultado 16 bits"]
    L2 --> FLAGS["C: carry final<br/>OV: overflow 16 bits"]

Aritmética de múltipla precisão — soma de 16 bits a partir de duas instruções de 8 bits.

Síntese

flowchart LR
    A["Aritmética<br/>modular"] --> B["Complemento<br/>de Dois"]
    A --> C["Unsigned<br/>n bits"]
    B --> D["Somador único<br/>(ADDWF + sinal)"]
    C --> D
    D --> E["Aritmética de<br/>múltipla precisão<br/>(ADDWFC)"]
    E --> F["Módulo 03:<br/>conjunto de<br/>instruções"]
    G["IEEE 754"] --> H["Software<br/>no PIC18"]
    H --> I["Ponto fixo<br/>preferível"]

Os três blocos conceituais e suas conexões com o Módulo 03.

As duas perguntas de abertura agora se respondem com precisão. O 4 no fim de 0{,}1 + 0{,}2 aparece porque 0{,}1 e 0{,}2 são dízimas periódicas em binário, arredondadas para o float mais próximo; a soma desses arredondados difere do float que melhor aproxima 0{,}3 por um bit no fim da mantissa, e o IEEE 754 entrega esse bit sem dó. O PIC18 soma uint16_t em duas instruções — ADDWF e ADDWFC — com o flag C propagando o carry entre os bytes, técnica que se estende a qualquer largura sem alterar uma porta lógica do silício. Três blocos ficam consolidados: a aritmética modular unifica unsigned e complemento de dois e cristaliza “inverte e soma um” como mecanismo único de negação; o IEEE 754 emerge como decisão de engenharia motivada pela comparação via inteiros, pelo bit implícito e pelos valores especiais; o somador completo, o ripple-carry e a ULA do PIC18 com seus cinco flags fecham o circuito entre teoria e silício. No Módulo 03 abrimos o conjunto de instruções do PIC18, os modos de endereçamento e a controvérsia RISC versus CISC — e as instruções de soma e subtração que usaremos lá são exatamente aquelas cujos flags discutimos esta semana.