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

Bem-vindo ao segundo módulo. Eu termino o Módulo 01 com a arquitetura do PIC18F4550 ainda fresca na sua memória e abro este capítulo pedindo-lhe um pequeno gesto de humildade intelectual: aceitar que o computador, contra o que sua intuição talvez sugira, é uma máquina aritmética imperfeita. Esta semana você vai entender, com a profundidade que cabe a um curso de arquitetura, por que essa imperfeição existe e como conviver com ela.

O Problema do Patriot, do JavaScript e do Seu PIC

Durante a Guerra do Golfo, um míssil Patriot falhou em interceptar um Scud por causa de um erro de arredondamento de poucos décimos de segundo, acumulado ao longo de cem horas de operação do relógio de bordo. O relatório do GAO americano documenta o caso. Não é folclore: é um bit de mantissa cuja imprecisão se propagou até virar tragédia. Você talvez nunca projete mísseis, mas vai escrever código que mede temperatura de um forno industrial, calcula posição de um servo ou valida saldo bancário — e em qualquer um desses contextos a aritmética computacional vai trair sua expectativa se você não souber em que circunstâncias ela trai.

Abra um console JavaScript e digite 0.1 + 0.2. Você lê 0.30000000000000004. Não é bug; é o IEEE 754 funcionando como foi especificado em 1985. Por que esse 4 aparece, no Chrome, no Firefox e no seu PIC18F4550? A resposta percorre um caminho que parte dos sistemas posicionais, atravessa o complemento de dois, mergulha no IEEE 754 e desemboca no registrador STATUS do PIC18 com seus cinco flags aritméticos.

Pergunta para o intervalo: por que um chip de 8 bits, como o PIC18F4550, consegue somar inteiros de 16 bits sem instruções nativas para essa largura? Mantenha a pergunta em mente; ao final do módulo você terá o vocabulário para respondê-la com precisão.

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"]

Panorama dos oito blocos do módulo, encadeados da base teórica ao Projeto Integrador.

Sistemas de Numeração Posicionais Revisitados

A maioria dos meus alunos chega aqui com a impressão de que sistemas de numeração são assunto resolvido. Sabem converter binário para decimal, riem de quanto vale 101010_2. A familiaridade é real, mas operacional, sem o respaldo conceitual que justificaria por que a base 2, e não a 3 ou a 10, tornou-se padrão universal. A escolha do binário é tecnológica: transistores em corte ou saturação distinguem com naturalidade dois níveis de tensão; distinguir três exigiria tolerâncias bem menores. Houve projetos ternários (o SETUN soviético dos anos 1960) e resultados teóricos apontando a base e como ótimo matemático, mas a robustez industrial do binário inviabilizou qualquer alternativa.

Um sistema posicional em base b representa um inteiro não negativo N como uma sequência de dígitos (d_{n-1}, \ldots, d_0) com cada d_i \in \{0, \ldots, b-1\}, sujeita à identidade abaixo:

N = \sum_{i=0}^{n-1} d_i \cdot b^i.

A unicidade dessa representação, desde que se proíbam zeros à esquerda redundantes, decorre do teorema da divisão euclidiana e é o que torna o algoritmo de “divisões sucessivas pela base” um procedimento bem definido. A notação científica usual — escrever 6{,}022 \times 10^{23} — é o mesmo esquema aplicado de forma híbrida, e essa observação vai voltar com força quando entrarmos no IEEE 754, que é exatamente notação científica em base 2 com convenções específicas.

O hexadecimal (b = 16, dígitos 0 a 9 seguidos de A a F) tem a propriedade 16 = 2^4: cada dígito corresponde a um quarteto de bits, e a conversão binário-hexadecimal é mero agrupamento. Em vez de \texttt{1010 0011 0101 1100}_2, você escreve \texttt{A35C}_{16}. Datasheets, listagens de assembly e dumps do MPLAB X preferem hexadecimal por essa razão. O octal (b = 8, três bits por dígito) sobrevive nas permissões do Unix e nas constantes C com prefixo 0 — cuidado, 010 em C é o decimal 8, não 10, uma das fontes mais sutis de bugs em programas C.

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

Os três fluxos de conversão entre bases que você precisa internalizar até o ponto da automaticidade.

Conversão entre bases é como tabuada: precisa virar automática. Da base b para decimal, soma das contribuições posicionais: \texttt{1011 0101}_2 = 128+32+16+4+1 = 181_{10}. De decimal para base b, divisões sucessivas lendo 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 o Anel dos Inteiros Módulo 2^n

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}. Não é formalismo: some \texttt{0xFF} com \texttt{0x01} na ULA de 8 bits do PIC18; o resultado matemático é 256, não cabe em 8 bits, o registrador recebe \texttt{0x00} e o flag C do STATUS é setado para sinalizar o transbordo. A faixa representável em n bits é [0, 2^n - 1]: [0, 255] para n=8, [0, 65\,535] para n=16, mais de quatro bilhões para n=32.

O PIC18F4550 tem ULA de 8 bits, e toda operação aritmética nativa produz resultados de 8 bits. Se você declara uint8_t em C, o XC8 emite uma única instrução por operação; se declara uint16_t, ele encadeia operações byte a byte propagando carry; uint32_t custa quatro bytes, uint64_t oito. O custo em ciclos cresce linearmente com a largura, e essa é uma das primeiras decisões de eficiência conscientes do código embarcado.

Hábito a adotar: prefira sempre uint8_t, int16_t e companhia a int, unsigned, long. A largura explícita torna previsível o número de instruções geradas e protege seu código contra surpresas de portabilidade entre o PIC e o seu notebook, onde int tem larguras diferentes.

Voltando à pergunta de abertura: como o PIC18, sendo de 8 bits, soma uint16_t? O compilador emite duas instruções. ADDWF soma os bytes baixos e atualiza C. Em seguida ADDWFC (add with carry) soma 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 técnica é a aritmética de múltipla precisão: o mesmo flag C que detectava transbordo em unsigned é agora o mecanismo que estende indefinidamente a largura aritmética, byte a byte, sem alterar uma única porta lógica do silício.

Inteiros com Sinal: Três Candidatos, Um Vencedor

Como representar números negativos com bits? A pergunta parece simples, mas tem três respostas históricas distintas: sinal-magnitude, complemento de um e complemento de dois. Cada uma ilumina uma propriedade diferente da aritmética binária, e a escolha entre elas não é convenção estética — ela determina a complexidade do silício que realiza a aritmética.

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 para inteiros com sinal e o desfecho industrial.

A representação sinal-magnitude usa o bit mais significativo como sinal (0 positivo, 1 negativo) e os demais bits como magnitude. Em 8 bits, \texttt{0000 0101} é +5 e \texttt{1000 0101} é -5. Duas falhas inviabilizam o esquema: o zero tem duas representações (+0 e -0), arruinando comparações bit a bit; e a soma de sinais opostos exige hardware condicional que identifique a maior magnitude e atribua sinal — somador e subtrator viram circuitos diferentes. Curiosamente, o sinal-magnitude sobrevive em um único lugar moderno: a mantissa do IEEE 754.

O complemento de um nega invertendo todos os bits: +5 = \texttt{0000 0101}, -5 = \texttt{1111 1010}. Resolve parcialmente o somador único, mas exige correção end-around carry (o carry-out do bit mais significativo é somado de volta no menos significativo) e mantém os dois zeros. UNIVAC 1100 e CDC 6600 o adotaram; a indústria seguiu adiante.

O complemento de dois domina, sem exceção relevante, todas as arquiteturas comerciais modernas. PIC18, x86, ARM, RISC-V, MIPS: todos representam inteiros com sinal em complemento de dois. A definição associa à sequência (b_{n-1}, \ldots, b_0) o valor abaixo, com bit mais significativo de peso negativo:

N = -b_{n-1} \cdot 2^{n-1} + \sum_{i=0}^{n-2} b_i \cdot 2^i.

A faixa [-2^{n-1}, +2^{n-1}-1] é assimétrica — há um negativo a mais que positivo — e o zero é único. Três consequências decisivas. Primeira: zero único, comparações por padrão de bits ficam triviais. Segunda: a negação é “inverte os bits e soma 1”, regra que decorre da congruência -N \equiv 2^n - N \pmod{2^n}, em que 2^n - 1 - N é a inversão bit a bit. Terceira, e decisiva: subtração e soma usam o mesmo somador físico. Para calcular A - B, basta somar A com -B, e -B vem da regra “inverte e soma um”. A instrução SUBWF do PIC18 implementa exatamente isso reaproveitando o mesmo silício que realiza ADDWF. Esse aproveitamento liberou transistores para outras funções 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.

A leitura mais profunda é entender o complemento de dois como identificação concreta do anel \mathbb{Z}/2^n\mathbb{Z}: a mesma classe de equivalência módulo 2^n admite dois representantes canônicos, um em [0, 2^n-1] (sem sinal) e outro em [-2^{n-1}, 2^{n-1}-1] (com sinal). O hardware não distingue a leitura; quem escolhe qual flag do STATUS examinar é o programador.

Ao estender int8_t para int16_t, o bit de sinal precisa ser propagado para os novos bits à esquerda. O valor -5 em 8 bits (\texttt{1111 1011}) vira \texttt{1111 1111 1111 1011} em 16 bits; preencher com zeros daria 251. Para unsigned, basta preencher com zeros. O XC8 cuida disso automaticamente em casts, mas vale inspecionar o .lst para ver as instruções geradas.

A soma em complemento de dois usa o mesmo somador que opera unsigned. Em unsigned, transbordo é o carry-out, flag C. Em complemento de dois, o carry-out costuma ser irrelevante; importa o overflow: resultado fora de [-2^{n-1}, 2^{n-1}-1]. A regra elegante: overflow ocorre se e somente se os operandos têm o mesmo sinal e o resultado tem sinal oposto. Em forma booleana, com s_a, s_b, s_r os bits de sinal,

\text{overflow} = (s_a \oplus s_r) \wedge (s_b \oplus s_r).

O flag OV do STATUS do PIC18 é gerado exatamente por essa expressão a cada operação aritmética. O código a seguir, parte das tarefas do Projeto Integrador, implementa essa detecção explicitamente em C e em assembly, e é nele que você verá, na sequência ADDWF seguida de ADDWFC, a construção de uma soma de 16 bits encadeando dois somadores de 8 bits via flag C:

02_soma_overflow.c
/*
 * Modulo 02 - Tarefa 2: soma com deteccao de overflow em complemento de dois.
 * Plataforma: PIC18F4550.
 *
 * Implementa soma de inteiros de 16 bits com sinal usando aritmetica
 * em complemento de dois e detecta overflow analisando os bits de sinal
 * dos operandos e do resultado, tecnica independente de hardware.
 */

#include <stdint.h>

typedef struct {
    int16_t valor;
    uint8_t overflow;   // 1 se a soma estourou a faixa de int16_t
} resultado_t;

resultado_t soma_segura_i16(int16_t a, int16_t b)
{
    resultado_t r;
    // A soma e realizada em 32 bits para preservar o valor matematico.
    int32_t soma32 = (int32_t)a + (int32_t)b;

    r.valor = (int16_t)soma32;

    // Overflow em complemento de dois ocorre se e somente se os dois
    // operandos tem o mesmo sinal e o resultado tem sinal contrario.
    // Equivalente a (a XOR resultado) & (b XOR resultado) < 0 no bit
    // de sinal.
    uint16_t sa = (uint16_t)a;
    uint16_t sb = (uint16_t)b;
    uint16_t sr = (uint16_t)r.valor;
    r.overflow = (uint8_t)((((sa ^ sr) & (sb ^ sr)) >> 15) & 1u);
    return r;
}
02_soma_overflow.asm
; Modulo 02 - Tarefa 2: soma de 16 bits com deteccao de overflow.
; Plataforma: PIC18F4550 (pic-as).
;
; Soma (B_H:B_L) em (A_H:A_L), deixando o resultado em (A_H:A_L) e
; atualizando a flag OV do STATUS para sinalizar overflow em
; complemento de dois. A propria instrucao ADDWFC do PIC18 atualiza
; OV ao operar sobre o byte mais significativo, dispensando o calculo
; manual feito na versao em C.

    PROCESSOR 18F4550
    #include <xc.inc>

    PSECT   udata_acs
A_L:    DS 1
A_H:    DS 1
B_L:    DS 1
B_H:    DS 1
FLAGS:  DS 1            ; bit 0 = OV no resultado final

    PSECT   code
GLOBAL  soma_i16

soma_i16:
    ; Soma byte baixo: A_L = A_L + B_L (atualiza C).
    MOVF    B_L, W, A
    ADDWF   A_L, F, A
    ; Soma byte alto com carry: A_H = A_H + B_H + C (atualiza OV e N).
    MOVF    B_H, W, A
    ADDWFC  A_H, F, A
    ; Salva o bit OV do STATUS em FLAGS.0 para uso do chamador.
    CLRF    FLAGS, A
    BTFSC   STATUS, 3, A   ; bit OV = 3 no STATUS do PIC18
    BSF     FLAGS, 0, A
    RETURN

    END

A confusão clássica que peço que você evite é tratar carry-out e overflow como sinônimos. Eles não são. Em \texttt{0xFF} + \texttt{0x01} em 8 bits lido como complemento de dois, temos -1 + 1 = 0: nenhum overflow ocorre, mas C é setado. Já em \texttt{0x7F} + \texttt{0x01}, temos +127 + 1 = -128 truncado: há overflow (OV = 1), mas C = 0. Confundir os dois é uma das fontes mais frequentes de erro entre programadores que estudaram complemento de dois superficialmente.

Ponto Flutuante e o Padrão IEEE 754

Os inteiros, mesmo em 64 bits, são insuficientes para muitos problemas científicos. A massa de uma molécula é 10^{-26} kg; a do Sol é 10^{30} kg. Não interessa saber a massa do Sol com erro de um quilograma; interessa saber com erro relativo da ordem de 10^{-7} ou 10^{-15}. 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.

Antes de 1985, cada fabricante implementava ponto flutuante a seu modo: IBM hexadecimal, DEC com VAX F/G/H, CDC com o seu próprio. O padrão IEEE 754, publicado em 1985 e revisado em 2008, unificou a indústria. Todo processador comercial moderno o segue, incluindo o XC8 ao emular ponto flutuante em software no PIC18.

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 (precisão simples).

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 é dado abaixo, com o bit implícito 1 à esquerda da vírgula:

N = (-1)^s \cdot (1 + F \cdot 2^{-23}) \cdot 2^{E - 127}.

O binário64 (precisão dupla) usa 1 bit de sinal, 11 bits de expoente (bias 1023) e 52 bits de fração. A precisão relativa é de 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 comentário. A primeira é o expoente com bias em vez de complemento de dois. A motivação: permitir que a comparação de magnitudes entre dois floats use o mesmo comparador de inteiros. Com o bias, expoente -126 vira E = 1 e expoente +127 vira E = 254, preservando a ordenação. William Kahan, prêmio Turing de 1989, defendeu a escolha porque um comparador único para inteiros e floats valia mais do que a simetria estética do complemento de dois.

A segunda é o bit implícito da mantissa. Em forma normalizada, todo número não nulo tem dígito 1 antes da vírgula em base 2 — constante. Armazená-lo seria desperdício; o padrão omite-o e o reintroduz na fórmula, ganhando um bit de precisão. A exceção é o caso denormal (E = 0): a fórmula passa a N = (-1)^s \cdot F \cdot 2^{-126} com bit implícito 0, permitindo representar valores muito próximos de zero com perda gradual de precisão.

A terceira é o conjunto de valores especiais. Para E = 0 e F = 0, zero com sinal (+0 e -0 têm bits distintos, valores matemáticos iguais). Para E = 0 e F \neq 0, denormal. Para E = 255 e F = 0, \pm\infty, resultante de overflow ou de a/0. Para E = 255 e F \neq 0, NaN, vindo de 0/0, \infty - \infty, \sqrt{-1}. NaNs propagam: qualquer operação com NaN produz NaN, e basta verificar o resultado final para saber se houve indefinição. E \text{NaN} \neq \text{NaN} por especificação, o que torna if (x != x) a maneira canônica de detectar NaN em C.

Conjunto não uniforme

Os números representáveis em IEEE 754 não estão uniformemente distribuídos na reta real. Próximo de zero estão muito próximos uns dos outros; em magnitudes elevadas, distanciam-se exponencialmente. A distância entre dois floats consecutivos em torno de 1{,}0 é 2^{-23} \approx 1{,}19 \times 10^{-7}; em torno de 10^6, fica 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.

A maior fonte de perplexidade em ponto flutuante é a perda de precisão. Como 0{,}1 é dízima periódica em binário, o IEEE 754 arredonda para o número binário mais próximo representável, introduzindo erro da ordem de 2^{-24}. Somando dois números arredondados, o erro acumula. 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. Esse fenômeno é o tema da terceira tarefa do Projeto Integrador, e o código abaixo o evidencia rodando no próprio PIC18, com a aritmética implementada em software pela biblioteca do XC8:

02_float_imprecisao.c
/*
 * Modulo 02 - Tarefa 3: evidencia empirica de imprecisao em ponto flutuante.
 * Plataforma: PIC18F4550 (XC8 implementa float em 32 bits IEEE 754).
 *
 * Demonstra dois efeitos:
 *   1. 0.1 + 0.2 != 0.3 (representacao binaria nao exata de 0.1 e 0.2).
 *   2. Perda de precisao por cancelamento subtrativo: (1 + eps) - 1 != eps
 *      quando eps for muito pequeno em relacao a 1.
 *
 * A saida e enviada ao LCD via funcoes do driver do kit. Aqui mantemos
 * apenas a logica de comparacao e a producao de um codigo de erro
 * legivel por outro modulo.
 */

#include <stdint.h>

typedef struct {
    uint8_t soma_01_02_difere_03;   // 1 se 0.1 + 0.2 != 0.3
    uint8_t cancelamento_perde;     // 1 se (1+eps) - 1 != eps
    float   diferenca_observada;    // (0.1+0.2) - 0.3, para exibicao
} relatorio_float_t;

relatorio_float_t experimento_float(void)
{
    relatorio_float_t r;
    volatile float a = 0.1f;
    volatile float b = 0.2f;
    volatile float c = 0.3f;
    volatile float eps = 1e-8f;
    volatile float um = 1.0f;

    float soma = a + b;
    r.diferenca_observada = soma - c;
    r.soma_01_02_difere_03 = (soma != c) ? 1u : 0u;

    float um_mais_eps = um + eps;
    float diff = um_mais_eps - um;
    r.cancelamento_perde = (diff != eps) ? 1u : 0u;

    return r;
}
02_float_imprecisao.asm
; Modulo 02 - Tarefa 3: imprecisao em ponto flutuante.
; Plataforma: PIC18F4550 (pic-as).
;
; O PIC18F4550 nao possui FPU. Operacoes em float sao implementadas
; pela biblioteca do XC8 atraves de chamadas a rotinas em software
; que manipulam diretamente os 32 bits do formato IEEE 754 de precisao
; simples (1 bit de sinal, 8 bits de expoente com bias 127, 23 bits
; de mantissa). Por essa razao, uma versao didatica em assembly puro
; e essencialmente uma sequencia de chamadas ao runtime do compilador.
;
; Este arquivo registra o esqueleto da chamada, deixando a aritmetica
; com a biblioteca, e expoe o resultado em variaveis globais para
; inspecao no simulador.

    PROCESSOR 18F4550
    #include <xc.inc>

    PSECT   udata
A_F:    DS 4            ; 0.1f em IEEE 754 (4 bytes)
B_F:    DS 4            ; 0.2f
C_F:    DS 4            ; 0.3f esperado
R_F:    DS 4            ; (0.1 + 0.2) calculado
D_F:    DS 4            ; (0.1 + 0.2) - 0.3 observado

    PSECT   code
GLOBAL  experimento_float_asm
EXTRN   _fadd, _fsub    ; rotinas do runtime XC8 (nomes ilustrativos)

experimento_float_asm:
    ; Carrega ponteiro de A_F em FSR0 e de B_F em FSR1.
    ; A convencao exata de passagem de argumentos depende da versao
    ; do XC8; aqui ilustramos a estrutura algoritmica.
    LFSR    0, A_F
    LFSR    1, B_F
    CALL    _fadd           ; R_F = A_F + B_F
    LFSR    0, R_F
    LFSR    1, C_F
    CALL    _fsub           ; D_F = R_F - C_F
    RETURN

    END

Há um segundo fenômeno, mais traiçoeiro, chamado cancelamento subtrativo: ao subtrair dois números próximos em magnitude, os algarismos significativos se cancelam e o resultado preserva apenas os últimos, que carregam o erro de arredondamento. A lição prática é dupla. Nunca compare floats por igualdade; compare por tolerância, fabs(a-b) < eps. E reorganize algoritmos numéricos para evitar subtrações de quantidades próximas, somando termos pequenos antes de termos grandes.

A comparação if (0.1 + 0.2 == 0.3) é sempre falsa em IEEE 754 binário. Isso não é defeito; é especificação. Programadores que descobrem o fato pela primeira vez acusam o compilador de bug; o compilador está correto, e a expectativa intuitiva é que estava errada. Trate todos os seus floats com essa humildade.

O padrão define cinco modos de arredondamento; o usado por padrão é “ao mais próximo, com desempate para o par” (banker’s rounding), que cancela vieses estatísticos de longo prazo. Os outros — para zero (truncamento, usado em conversões float → int), para +\infty (teto) e para -\infty (piso) — têm aplicações específicas em aritmética intervalar. Ainda há um conselho que repito sempre: ponto flutuante não é a representação certa para tudo. Para valores monetários, é desastroso — sistemas financeiros sérios usam inteiros em centavos. Para o PIC18, é caro: o chip não tem FPU, e cada soma em float pode consumir 200 ciclos da biblioteca de runtime do XC8. Em código crítico, a prática é representar grandezas como inteiros em escalas fixas — temperatura em centésimos de grau, distância em décimos de milímetro — e converter para exibição apenas na hora de mostrar no LCD.

Circuitos Aritméticos no Silício

Toda essa aritmética é realizada, no silício, por arranjos relativamente simples de portas lógicas. A unidade básica é o somador completo de um bit (full adder): três entradas (a, b, carry-in c_{in}) e duas saídas (bit de soma s, carry-out c_{out}), dadas pelas equações abaixo.

s = a \oplus b \oplus c_{in}, \qquad c_{out} = (a \cdot b) + (c_{in} \cdot (a \oplus b)).

Duas portas XOR, duas AND e uma OR — cerca de doze transistores em CMOS. Conectar n desses em cadeia, com o carry-out de cada estágio alimentando o carry-in do seguinte, produz o somador ripple-carry de n bits. O bit menos significativo recebe c_{in} = 0; o carry-out final é o flag C do STATUS. A ULA de 8 bits do PIC18 contém um ripple-carry desses, mais lógica adicional para AND/OR/XOR/deslocamentos.

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 de 4 bits — exemplo redutível do padrão usado na ULA do PIC18.

A virtude do ripple-carry é a simplicidade; o vício é a latência. O carry propaga-se sequencialmente do bit menos significativo ao mais significativo. Em 32 bits, são até 32 atrasos de porta, dominando o ciclo em frequências de gigahertz. A solução adotada por processadores rápidos é o carry-lookahead: arranjo combinacional que calcula todos os carries em paralelo, com latência logarítmica em n ao custo de mais área. Para o PIC18, com clock interno de até 12 MHz e somador de 8 bits, o ripple-carry é absolutamente adequado.

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 instrução SUBWF faz exatamente isso internamente. A multiplicação é mais cara; o algoritmo elementar (shift-and-add) executa n somas condicionais para multiplicar duas palavras de n bits. O PIC18 tem uma peculiaridade arquitetural notável: a instrução MULWF realiza multiplicação 8 \times 8 em um único ciclo via matriz combinacional, produzindo resultado de 16 bits em PRODH:PRODL. Para multiplicações com sinal em arquiteturas maiores, o algoritmo de Booth (e a sua variante moderna radix-4) examina três bits do multiplicador por vez, escolhendo entre cinco operações (0, \pm M, \pm 2M), reduzindo o número de somas pela metade e unificando o tratamento de unsigned e complemento de dois. Multiplicadores de processadores comerciais quase sempre implementam alguma variante do Booth. A divisão, por sua vez, é a operação mais cara: o algoritmo restoring division gasta uma comparação e uma subtração condicional por bit do quociente. O PIC18 não tem divisão em hardware; o XC8 implementa em software.

A ULA do PIC18F4550 e o Registrador STATUS

A ULA do PIC18 opera sobre palavras de 8 bits e oferece somas e subtrações em complemento de dois (que são também as operações em unsigned), AND/OR/XOR bit a bit, deslocamentos com e sem rotação pelo carry, e a multiplicação 8 \times 8. Toda outra operação aritmética — soma de 16 bits, divisão, ponto flutuante — é construída em software a partir dessas primitivas.

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 registrador STATUS atualizados pelas operações aritméticas e lógicas.

Os cinco flags do STATUS merecem ser memorizados. C (Carry) recebe o carry-out do bit 7 em adições e subtrações sobre unsigned; em subtrações, segue a convenção PIC (C = 1 significa sem empréstimo, oposta à do x86). DC (Digit Carry) recebe o carry-out do bit 3, útil para BCD. Z (Zero) é setado quando o resultado é zero. N (Negative) é o bit 7 do resultado: sinal em complemento de dois. OV (Overflow) é setado quando uma soma ou subtração em complemento de dois produz resultado fora de [-128, +127]. A combinação desses flags com as instruções BC/BNC/BZ/BNZ/BN/BNN/BOV/BNOV permite implementar toda a lógica de controle baseada em comparações.

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 construída a partir de duas instruções de 8 bits.

Toda operação aritmética em larguras maiores que 8 bits é construída no PIC18 encadeando operações de 8 bits via ADDWFC (add with carry) e SUBWFB (subtract with borrow). Em uma soma de uint16_t, o byte baixo é somado por ADDWF, que atualiza C; o byte alto, por ADDWFC, que soma os dois bytes altos mais o flag C herdado. O flag OV após essa segunda instrução é o overflow de 16 bits, gratuito. Em 32 bits, são quatro instruções; em 64 bits, oito. Volte ao listing de 02_soma_overflow.asm e observe a sequência ADDWF / ADDWFC — é exatamente esse padrão que faz um chip de 8 bits somar inteiros de 16 bits sem instruções nativas para essa largura, respondendo à pergunta de abertura.

A convenção do flag C em subtrações no PIC18 é uma armadilha clássica para quem vem do x86. No PIC18, C = 1 após SUBWF significa sem empréstimo: minuendo \geq subtraendo. Isso é o oposto do x86, em que C = 1 indica empréstimo. Ao escrever assembly que envolve comparações, lembre-se de que BC após SUBWF salta quando minuendo \geq subtraendo — não menor. Erros de off-by-one em laços derivam frequentemente dessa inversão.

Regra prática para float no PIC18: use float apenas quando a faixa dinâmica dos dados exceder 10^4, o número de operações em float por segundo for menor que algumas centenas e a precisão exigida for relativa. Em qualquer outra situação, prefira ponto fixo com largura inteira adequada. Uma soma em float consome cerca de 100 μs em 8 MHz — eternidade em tempo real.

Aplicação no Projeto Integrador

Você tem três tarefas neste módulo, todas rodando no kit ACEPIC PRO V8.2 com o módulo de LCD Sunstar 2004A, e cada uma materializa um dos blocos teóricos.

A primeira tarefa pede que você implemente rotinas que recebem um valor binário interno e produzem sua representação decimal formatada para o LCD. O algoritmo é divisão sucessiva por 10, espelhando a conversão decimal-binário mas no sentido inverso. Para int16_t, há um cuidado especial com INT16_MIN (-32\,768): a negação direta produziria overflow, porque +32\,768 não cabe em int16_t. A solução é converter para uint16_t via cast, manipular a magnitude como unsigned e, ao final, prefixar o sinal. O código de referência abaixo evidencia, na versão assembly, um custo escondido: o PIC18 não tem divisão em hardware, e N/10 é implementado por subtrações iterativas. A versão otimizada — multiplicação pelo recíproco — fica como exercício de leitura para o 02_livro.qmd.

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"]

Algoritmo de conversão binário-decimal via divisões sucessivas por 10.

02_conv_decimal.c
/*
 * Modulo 02 - Tarefa 1: conversao binario -> decimal para o LCD.
 * Plataforma: PIC18F4550 + KIT ACEPIC PRO V8.2.
 * Recebe um inteiro de 16 bits com sinal e produz a string decimal
 * correspondente em um buffer fornecido pelo chamador.
 *
 * Esta versao didatica privilegia clareza algoritmica sobre eficiencia:
 * usa divisao sucessiva por 10 (algoritmo padrao) e tratamento explicito
 * do sinal. Em modulos posteriores voce vera versoes que evitam a divisao
 * de 16 bits, custosa no PIC18 (que possui ULA de 8 bits).
 */

#include <stdint.h>
#include <stddef.h>

/* Converte 'valor' (int16_t) em string decimal terminada em '\0'.
 * 'buf' deve ter pelo menos 7 bytes: sinal opcional + 5 digitos + '\0'.
 * Retorna o numero de caracteres escritos (sem contar o '\0'). */
uint8_t int16_para_decimal(int16_t valor, char *buf)
{
    char tmp[6];           // 5 digitos + sentinela
    uint8_t i = 0, n = 0;
    uint16_t mag;
    uint8_t negativo = 0;

    if (valor < 0) {
        negativo = 1;
        // Magnitude em complemento de dois: para INT16_MIN
        // (-32768) a negacao em int16_t causaria overflow.
        // Por isso convertemos via unsigned, que e seguro.
        mag = (uint16_t)(-(int32_t)valor);
    } else {
        mag = (uint16_t)valor;
    }

    // Geracao reversa dos digitos: divisao sucessiva por 10.
    do {
        tmp[i++] = (char)('0' + (mag % 10u));
        mag /= 10u;
    } while (mag != 0u);

    if (negativo) {
        buf[n++] = '-';
    }
    while (i > 0) {
        buf[n++] = tmp[--i];
    }
    buf[n] = '\0';
    return n;
}
02_conv_decimal.asm
; Modulo 02 - Tarefa 1: conversao binario -> decimal (versao didatica)
; Plataforma: PIC18F4550 (pic-as).
;
; Esboco em assembly do laco principal de divisao sucessiva por 10.
; O divisor de 16 bits e implementado por subtracoes condicionais,
; uma vez que o PIC18 nao possui instrucao DIV nativa para 16 bits.
; Esta versao didatica foca em legibilidade, nao em ciclos minimos.

    PROCESSOR 18F4550
    #include <xc.inc>

    PSECT   udata_acs
NUM_L:  DS 1            ; byte baixo do dividendo (mag)
NUM_H:  DS 1            ; byte alto do dividendo (mag)
RESTO:  DS 1            ; resto de cada divisao (0..9)
QTD:    DS 1            ; quantidade de digitos gerados

    PSECT   code
GLOBAL  div10_passo

; div10_passo: divide o valor de 16 bits em (NUM_H:NUM_L) por 10,
; deixando o quociente em (NUM_H:NUM_L) e o resto em RESTO.
; Estrategia: subtracao iterativa de 10. Didatica, nao otimizada.
div10_passo:
    CLRF    RESTO, A
    ; Caso o numero seja menor que 10, RESTO recebe NUM_L e quociente = 0.
    MOVF    NUM_H, W, A
    BNZ     div10_loop
    MOVF    NUM_L, W, A
    SUBLW   10
    BC      div10_termina   ; NUM_L < 10 -> nao da uma iteracao
div10_loop:
    MOVLW   10
    SUBWF   NUM_L, F, A
    MOVLW   0
    SUBWFB  NUM_H, F, A
    INCF    RESTO, F, A     ; conta uma iteracao
    ; Continua enquanto (NUM_H:NUM_L) >= 10.
    MOVF    NUM_H, W, A
    BNZ     div10_loop
    MOVF    NUM_L, W, A
    SUBLW   10
    BNC     div10_loop
div10_termina:
    ; Ao final, RESTO contem o digito decimal (0..9) e
    ; (NUM_H:NUM_L) contem o quociente para a proxima iteracao externa.
    RETURN

    END

A segunda tarefa é a soma com detecção de overflow, cujo código já apareceu. A entrega pedagógica não está no código — qualquer dupla competente o escreve em meia hora — e sim no relatório. Sua dupla deve executar o código com pares escolhidos para exercitar quatro casos: positivo + positivo com overflow (\texttt{0x7FFF} + \texttt{0x0001}); negativo + negativo com overflow (\texttt{0x8000} + \texttt{0xFFFF}); positivo + positivo sem overflow (\texttt{0x1000} + \texttt{0x2000}); positivo + negativo, sempre sem overflow. Para cada caso, registre os padrões, valor esperado, valor obtido e estado dos flags. O resultado mais ilustrativo é confrontar C e OV: em \texttt{0xFFFF} + \texttt{0x0001}, C = 1 e OV = 0; em \texttt{0x7FFF} + \texttt{0x0001}, C = 0 mas OV = 1. Essa tabela é o momento em que o estudante entende operacionalmente o complemento de dois.

Operandos (hex) Leitura unsigned Leitura c2 Flag C Flag 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 = 12288 0 0 sem condição excepcional

A terceira tarefa é mais experimental do que algorítmica. O programa executa 0{,}1 + 0{,}2 e (1 + \epsilon) - 1 e exibe os resultados no LCD. O objetivo é tornar visível, com o próprio chip, o fenômeno descrito em livros há semanas. O relatório deve incluir a representação hexadecimal dos quatro valores (0{,}1, 0{,}2, 0{,}3 e 0{,}1 + 0{,}2) como sequências de 4 bytes em IEEE 754 binário32, inspecionadas no simulador do MPLAB X. Você verá que 0{,}1 e 0{,}2 têm padrões que não correspondem a 1/10 e 2/10 exatos, e que a soma desses padrões difere do padrão de 0{,}3 por um único bit no fim da mantissa.

Pergunta de reflexão para o diário de projeto: se o seu kit estivesse controlando 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 miligrama, ou outra? Justifique com base no que aprendeu sobre faixa dinâmica, precisão relativa e custo computacional no PIC18.

O entregável do módulo são três blocos de código rodando no kit, um relatório de experimentação com os quatro casos de overflow e os padrões hexadecimais dos floats, e o diário de projeto atualizado registrando as escolhas de representação. Ao final você terá a chance de ver com os próprios olhos o que muitos programadores experientes nunca viram: o pulso do carry-out, o flag OV erguido pelo silício, o último bit da mantissa que separa 0{,}3 exato do 0{,}3 arredondado.

Síntese e o Caminho para o Módulo 03

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 grandes blocos conceituais do módulo e suas conexões com o que vem adiante.

Você percorreu, nesta semana, um caminho que partiu dos sistemas posicionais e desembocou no flag OV erguido em uma soma de 16 bits no PIC18. Três blocos ficam consolidados. Em representação de inteiros, a aritmética modular unifica unsigned e complemento de dois, com “inverte e soma um” cristalizando a negação e permitindo que soma e subtração compartilhem o mesmo somador físico. Em ponto flutuante, o IEEE 754 emerge não como enumeração de campos, mas como decisão de engenharia motivada pela comparação de magnitudes via inteiros, pelo bit implícito e pelos valores especiais que propagam erros. Em aritmética em hardware, o somador completo é o bloco básico, o ripple-carry sua extensão natural, e a ULA do PIC18 com seus cinco flags é a instância concreta dessas ideias.

A pergunta de abertura agora se responde com precisão. O PIC18 soma uint16_t em duas instruções, ADDWF e ADDWFC, com o flag C propagando o carry. A mesma técnica estende-se a qualquer largura, sem alteração no silício.

Antes de virar a página, faça uma autoavaliação honesta. Você deve converter sem hesitação entre bases 2, 10 e 16, aplicar “inverte e soma um” em qualquer largura justificando com aritmética modular, distinguir carry-out de overflow, descrever os 32 bits de um float IEEE 754 e identificar \pm 0, \pm \infty e NaN, compreender por que 0{,}1 + 0{,}2 \neq 0{,}3, descrever o somador completo em portas lógicas, e listar os cinco flags do STATUS dizendo em que condições cada um é alterado. Se algum desses pontos parece nebuloso, retorne à seção correspondente.

No Módulo 03 abriremos o conjunto de 75 instruções do PIC18, os modos de endereçamento e a controvérsia RISC versus CISC. As instruções de soma e subtração que usaremos lá são exatamente aquelas cujos flags discutimos esta semana. Última provocação: por que o PIC18 codifica todas as instruções em exatamente 16 bits, em vez de tamanhos variáveis como o x86? Que ganhos arquiteturais essa decisão produz, e que limites ela impõe? Formule uma hipótese; o próximo módulo abre com essa pergunta.