Módulo 03: Conjunto de Instruções e Modos de Endereçamento — Resumo

Esta é a versão de revisão do Módulo 03, condensada para leitura rápida antes da prova ou na véspera de uma tutoria do Projeto Integrador. Mantenho aqui o esqueleto conceitual; quando precisar de demonstrações, exemplos adicionais ou contexto histórico ampliado, consulte 03_material.qmd e, para o tratamento mais profundo, 03_livro.qmd.

Vou começar com uma provocação que percorre todo o módulo: dois processadores podem ter ISAs completamente diferentes e ainda assim entregarem desempenho indistinguível num benchmark típico, ao passo que o PIC18F4550 oferece setenta e cinco instruções e um Intel Core moderno passa de mil e quinhentas. Como isso é possível sem dizer que um é mais poderoso que o outro? Guarde a pergunta — retomo no fim.

flowchart TB
    subgraph CONC["Fundamentos conceituais"]
        ISA["ISA<br/>contrato HW/SW"]
        INST["Anatomia da instrução<br/>opcode + operandos"]
        MODOS["Modos de<br/>endereçamento"]
    end

    subgraph FIL["Filosofias"]
        CR["CISC × RISC<br/>convergência moderna"]
        PIC["75 instruções<br/>do PIC18F4550"]
    end

    subgraph PRAT["Prática (Projeto Integrador)"]
        T1["Tarefa 1<br/>Análise do ASM<br/>gerado pelo XC8"]
        T2["Tarefa 2<br/>Função crítica<br/>otimizada em ASM"]
        T3["Tarefa 3<br/>Documentação<br/>dos modos usados"]
    end

    CONC --> FIL --> PRAT
    PRAT --> M4["Módulo 04:<br/>Caminho de Dados"]
Figura 1: Mapa do módulo: da ISA como contrato até as três tarefas do Projeto Integrador.

ISA como contrato entre software e hardware

A engenharia de computação moderna separa o que a máquina faz do como ela faz. O conjunto de operações executáveis, o significado de cada uma e os recursos visíveis ao programador formam a Instruction Set Architecture, ou ISA. Tudo abaixo disso é microarquitetura, invisível ao programador. A palavra contrato é deliberada: o software se compromete a emitir apenas sequências definidas pela ISA, e o hardware se compromete a executá-las com a semântica documentada, independentemente de como a microarquitetura esteja organizada em uma geração específica. É esse contrato que permite que um binário compilado em 2003 para x86 ainda execute num chip de 2026.

Formalizo a ISA como tupla \mathcal{I} = (\Sigma, \mathcal{F}, \mathcal{R}, \mathcal{M}, \mathcal{A}, \mathcal{X}, \sigma), em que \Sigma é o conjunto finito de instruções, \mathcal{F} mapeia padrões binários em operações abstratas, \mathcal{R} são os registradores visíveis, \mathcal{M} é o modelo de memória, \mathcal{A} os modos de endereçamento, \mathcal{X} os mecanismos de exceção e \sigma a transição de estado. A consequência prática é a compatibilidade binária, princípio inaugurado pela IBM em 1964 com o System/360. Há um custo: decisões originais ficam cravadas no contrato, motivo pelo qual o x86 ainda carrega aritmética decimal dos tempos do 8086.

Se a Microchip lançar amanhã um PIC18F4550 fabricado em 28 nm e com pipeline de quatro estágios, o seu código existente continuaria executando sem alteração? A resposta está justamente na noção de contrato arquitetural.

Anatomia de uma instrução

Tome ADDWF f, d, a no PIC18F4550. Trata-se de uma palavra única de dezesseis bits dividida em quatro campos: seis bits de opcode (0010 01), um bit de destino d, um bit de seleção de banco a e oito bits para o endereço f do File Register. Os seis bits mais significativos identificam univocamente a operação; os dez restantes especificam os operandos. Essa partição rígida entre opcode e operandos é a estrutura tipológica de qualquer instrução de máquina em qualquer ISA, e o orçamento de bits entre os dois campos é a decisão central de todo arquiteto.

flowchart LR
    subgraph WORD["Palavra de 16 bits da instrução ADDWF f, d, a"]
        OP["opcode<br/>0010 01<br/>(6 bits)"]
        D["d<br/>(1 bit)<br/>destino"]
        A["a<br/>(1 bit)<br/>banco"]
        F["f<br/>(8 bits)<br/>endereço do registrador"]
    end

    OP --> DECODE["Decodificador<br/>identifica<br/>ADDWF"]
    D --> CTRL["Sinais de<br/>controle"]
    A --> CTRL
    F --> ULA["Operando<br/>na ULA"]
Figura 2: Anatomia da instrução ADDWF em 16 bits.

Há uma classificação clássica pelo número de operandos explícitos. Máquinas três-operandos (ADD R3, R1, R2) seguem o padrão RISC moderno; máquinas dois-operandos fundem segundo fonte com destino, como no x86; máquinas de acumulador trabalham com um único operando explícito. O PIC18F4550 é máquina de um operando e meio: possui um único acumulador W, mas o bit d decide se o resultado volta para W ou é escrito no próprio File Register. Outra decisão fundamental é comprimento fixo versus variável. O PIC18 é exemplo canônico de comprimento fixo, com pouquíssimas exceções de dupla palavra (CALL, GOTO longo, MOVFF) tratadas pelo pipeline como NOP no segundo ciclo. Fique atento a um detalhe semântico que costuma gerar bugs sutis: muitas instruções alteram o registrador STATUS como efeito colateral, atualizando os flags C, DC, Z, OV e N. O idioma MOVF f, F parece não fazer nada, mas atualiza Z e funciona como teste-de-zero em um ciclo.

Modos de endereçamento

A pergunta operacional é simples: de onde vem o dado sobre o qual a instrução opera? Cada resposta possível é um modo de endereçamento, e a fluência nesses modos é o que separa quem lê assembly de quem realmente o compreende.

flowchart TD
    INST["Campo de<br/>operando na<br/>instrução"]

    INST --> IMED["Imediato<br/>valor está na<br/>própria instrução"]
    INST --> REG["Por registrador<br/>operando vive<br/>na CPU"]
    INST --> DIR["Direto<br/>endereço de RAM<br/>está na instrução"]
    INST --> IND["Indireto<br/>via FSR<br/>(ponteiro)"]
    INST --> IDX["Indexado<br/>FSR + W<br/>(arrays)"]
    INST --> TAB["Tabela<br/>via TBLPTR<br/>(Flash)"]

    IMED -->|"MOVLW 0x2A"| OUT1["Constantes<br/>em tempo de<br/>compilação"]
    REG -->|"ADDWF f, d"| OUT2["Variáveis<br/>locais e SFRs"]
    IND -->|"POSTINC0"| OUT3["Travessia<br/>de buffers,<br/>strings"]
    IDX -->|"PLUSW0"| OUT4["Acesso a<br/>elementos<br/>de array"]
    TAB -->|"TBLRD*+"| OUT5["Constantes<br/>grandes<br/>em Flash"]
Figura 3: Modos de endereçamento do PIC18F4550 e seus usos típicos.

No modo imediato o operando está embutido na instrução, como em MOVLW 0x2A; toda atribuição x = 42 em C produz, com altíssima probabilidade, um imediato. No endereçamento por registrador o operando reside em um registrador identificado por um campo da instrução; W é a única exceção do PIC18, pois não tem endereço acessível por outras instruções. No endereçamento direto a instrução contém o endereço completo do operando, com a peculiaridade do PIC18 de usar oito bits de endereço complementados pelo BSR, com o bit a controlando o uso do Access Bank (que reúne os SFRs e a faixa rápida de RAM em um único banco virtual).

O modo que abre as portas para ponteiros e estruturas dinâmicas é o indireto, implementado pelos três File Select Registers (FSR0, FSR1, FSR2) e por cinco registradores virtuais associados a cada um — INDFn, POSTINCn, POSTDECn, PREINCn, PLUSWn. A correspondência com C é direta: INDF0 corresponde a *p, POSTINC0 a *p++, PREINC0 a *++p e PLUSW0 a *(p + w).

Por que o indireto é o modo decisivo

Todas as estruturas de dados dinâmicas, todas as travessias de arrays e todas as passagens por referência são implementadas, no fim, por endereçamento indireto. Sem ele, linguagens com ponteiros não seriam implementáveis com a eficiência que conhecemos.

O endereçamento indexado, ausente como modo puro no PIC18, é realizado por PLUSWn, que calcula *(FSRn + W) em uma instrução — equivalente ao base-deslocamento. E, finalmente, o endereçamento por tabela em memória de programa é feito via TBLPTRU:TBLPTRH:TBLPTRL mais as instruções TBLRD*, com auto-incremento opcional; é como o XC8 acessa variáveis declaradas const na Flash, exatamente o caso da tabela de caracteres do display Sunstar 2004A do seu projeto.

CISC, RISC e a convergência moderna

As máquinas CISC dos anos 1960 e 1970 nasceram sob restrições concretas: memória cara, memória lenta em relação ao processador e ausência de compiladores otimizadores. A resposta natural foi instruções de comprimento variável, muitos modos de endereçamento, operações complexas em uma única instrução e aproximação semântica com Pascal e Algol — a chamada semantic gap closure. Por volta de 1980, pesquisadores da IBM, de Berkeley e de Stanford mostraram que apenas uma fração pequena dessas instruções era de fato usada pelos compiladores, e que instruções de duração variável tornavam o pipeline praticamente inviável. Surgiu o RISC, com instruções de comprimento fixo, arquitetura load-store, poucos modos de endereçamento e regularidade de decodificação.

A surpresa histórica veio nos anos 1990: a Intel, em vez de abandonar o x86, passou a traduzir internamente as instruções CISC em micro-operações RISC (\muops) executadas por um núcleo internamente regular, mantendo externamente o contrato x86. Foi simultaneamente capitulação ao mérito do RISC e vitória do princípio contratual da ISA. O PIC18F4550 é inequivocamente RISC, com setenta e cinco instruções, comprimento fixo e princípio load-store, mas faz concessões CISC-like típicas de microcontroladores: o bit $dfundindo destinos, instruções compostas comoDECFSZ` e modos indiretos com auto-incremento embutido.

Famílias de instruções do PIC18F4550

Você não precisa decorar as setenta e cinco instruções; precisa reconhecer os mnemônicos, entender a estrutura de cada família e saber consultar o datasheet. A família de movimentação reúne MOVF, MOVWF, MOVLW, MOVFF e LFSR, esta última carregando doze bits no FSR de forma atômica em dupla palavra. A família aritmética inclui ADDWF, ADDWFC (essencial para somas de múltiplas palavras), SUBWF, SUBFWB, INCF, DECF, MULWF e MULLW; o PIC18 multiplica em um ciclo mas não divide em hardware, motivo pelo qual algoritmos de divisão devem ser repensados ou substituídos por deslocamentos e tabelas. A família lógica espelha a aritmética com ANDWF, IORWF, XORWF, COMF e SWAPF, esta troca os nibbles em um ciclo e é útil em conversões BCD/ASCII; deslocamentos são feitos por RLCF, RRCF, RLNCF e RRNCF.

A família de manipulação de bit é central em microcontroladores: BCF, BSF, BTG, BTFSC e BTFSS fazem em um ciclo o que sem instrução dedicada exigiria três. A família de controle de fluxo inclui desvios incondicionais BRA (relativo) e GOTO (absoluto, dupla palavra), os condicionais BZ, BNZ, BC, BNC, BOV, BNOV, BN, BNN, chamadas CALL e RCALL, retornos RETURN e RETLW, além das skip combinadas como DECFSZ, que decrementa e pula numa única instrução — todo laço de N iterações tende a tomar essa forma. A família de propósito específico agrupa SLEEP, RESET, CLRWDT, NOP (útil em timings finos do LCD) e as quatro TBLRD para acesso à Flash.

flowchart TB
    PIC18["Conjunto de instruções<br/>do PIC18F4550<br/>(75 instruções)"]

    PIC18 --> MOV["Movimentação<br/>MOVF, MOVWF,<br/>MOVLW, MOVFF, LFSR"]
    PIC18 --> ARIT["Aritméticas<br/>ADDWF, SUBWF,<br/>INCF, DECF, MULWF"]
    PIC18 --> LOG["Lógicas e shift<br/>ANDWF, IORWF, XORWF,<br/>COMF, SWAPF, RLCF, RRCF"]
    PIC18 --> BIT["Manipulação de bit<br/>BCF, BSF, BTG,<br/>BTFSC, BTFSS"]
    PIC18 --> FLOW["Controle de fluxo<br/>BRA, GOTO, CALL,<br/>RETURN, BZ/BNZ/BC...,<br/>DECFSZ, INCFSZ"]
    PIC18 --> ESP["Propósito específico<br/>SLEEP, RESET, CLRWDT,<br/>NOP, TBLRD, TBLWT"]
Figura 4: As seis famílias funcionais das 75 instruções do PIC18F4550.

Exemplo emblemático: checksum de oito bytes

O exemplo canônico do módulo é o cálculo de um checksum de oito bits sobre um vetor em RAM. A versão otimizada concentra três técnicas: contagem decrescente até zero, ponteiro avançando diretamente em vez de indexação por variável, e laço do-while que elimina o teste redundante na primeira iteração. O coração do código é uma instrução só: ADDWF POSTINC0, W, ACCESS lê o byte apontado por FSR0, soma a W e auto-incrementa FSR0 em um único ciclo. Em RISC puro seriam duas ou três instruções. O resultado é um corpo de laço de três instruções e ganho típico de quinze a vinte por cento sobre a versão didática — mensurável quando o checksum executa milhares de vezes por segundo no seu Projeto Integrador.

#include <xc.h>

unsigned char checksum(const unsigned char *buf,
                       unsigned char n) {
    unsigned char soma = 0;
    for (unsigned char i = 0; i < n; i++) {
        soma += buf[i];
    }
    return soma;
}
; FSR0 -> buffer, contador em CNT, soma em W
    LFSR    0, buffer
    MOVLW   .8
    MOVWF   CNT, ACCESS
    CLRF    WREG, ACCESS
loop:
    ADDWF   POSTINC0, W, ACCESS
    DECFSZ  CNT, F, ACCESS
    BRA     loop
    MOVWF   SOMA, ACCESS

Pergunta para fixar: qual recurso da ISA do PIC18 a versão otimizada explora que a versão didática descarta? Identificar essa diferença é o teste de que você compreendeu o modo indireto com pós-incremento.

Do C ao assembly e quando descer ao baixo nível

O XC8 atravessa cinco fases — análise lexical e sintática, análise semântica, geração de código intermediário, otimização e geração de código de máquina. Você influencia parte das decisões pelo nível de otimização, pelos qualificadores register, volatile, const e pela forma de escrever o algoritmo; o restante é heurística interna. Alguns padrões são úteis de reconhecer: a = b; entre bytes vira MOVF b, W seguido de MOVWF a; a = b + c; vira MOVF b, W, ADDWF c, W, MOVWF a; if (n == 0) vira o idioma MOVF n, F seguido de BNZ; e laços for crescentes costumam ser convertidos pelo compilador em contagem decrescente para usar DECFSZ.

flowchart LR
    C["Código C<br/>a = b + c;"] --> LEX["Análise<br/>lexical e<br/>sintática"]
    LEX --> SEM["Análise<br/>semântica<br/>(tipos)"]
    SEM --> IR["Código<br/>intermediário"]
    IR --> OPT["Otimização<br/>(constantes, inline,<br/>laços, código morto)"]
    OPT --> GEN["Geração de<br/>código de<br/>máquina"]
    GEN --> ASM["Assembly PIC18<br/>MOVF b, W<br/>ADDWF c, W<br/>MOVWF a"]
Figura 5: Fases do compilador XC8: do C ao assembly do PIC18.

A regra prática sobre quando escrever assembly à mão é clara: na maioria dos casos, não vale a pena, porque o XC8 com otimização moderada produz código equivalente ao que um humano escreveria em horas. As três exceções legítimas são laços críticos que dominam o tempo total, rotinas com timing preciso (bit-banging serial, varredura de teclado, controle de displays como o Sunstar 2004A) e acesso a recursos que o C não expõe. Fora disso, escreva em C, meça com o Stopwatch do MPLAB X, e só desça para assembly com evidência empírica.

Síntese e conexão com o Projeto Integrador

Transformamos a noção informal de instrução em conceito rigoroso: palavra binária estruturada em campos, codificada por \mathcal{F} e executada como transição de estado \sigma. Esse contrato — a ISA — é o que dura entre software e hardware. Abrimos uma instrução do PIC18 e examinamos seus campos, catalogamos os modos de endereçamento, confrontamos CISC e RISC, organizamos as setenta e cinco instruções em famílias e atravessamos a fronteira do C para o assembly. O Plano de Aulas vincula tudo isso a três tarefas no kit ACEPIC PRO V8.2: analisar sistematicamente o assembly gerado pelo XC8 para pelo menos três funções já implementadas, reimplementar uma função crítica em assembly puro medindo o ganho com o Stopwatch, e documentar em tabela os modos de endereçamento usados ao longo do projeto, correlacionando padrões do seu C com modos e instruções resultantes.

Retomo as provocações de abertura. ISAs diferentes podem entregar desempenho equivalente porque o contrato visível ao programador não determina por completo a microarquitetura interna: ARM, MIPS e RISC-V de gerações comparáveis competem em mesmo patamar. E a discrepância entre setenta e cinco e mil e quinhentas instruções não traduz superioridade: o PIC18 prioriza simplicidade de silício e regularidade do pipeline para o domínio embarcado, enquanto o Intel Core acumulou extensões SIMD, criptográficas e de virtualização ao longo de décadas para servidores e desktops. São pontos diferentes no espaço de trade-offs, cada um adequado a um domínio. No próximo módulo abriremos o processador e examinaremos o caminho de dados que executa exatamente as instruções estudadas aqui — e a regularidade RISC do PIC18 será, de novo, protagonista.