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"]
Módulo 3: Conjunto de Instruções e Modos de Endereçamento
Esta é a versão enxuta do Módulo 3, pensada para você revisar antes da aula, durante deslocamentos ou na véspera de uma avaliação. Mantenho aqui os conceitos, diagramas e exemplos indispensáveis; o livro do módulo continua sendo a fonte completa para aprofundamento.
O Problema Que Abre o Módulo
Nos dois módulos anteriores usei repetidamente uma palavra que ficou propositalmente vaga: instrução. Mas o que é, com precisão, uma instrução? Como ela está representada na memória? Como o processador, ao buscá-la, sabe o que fazer? E, sobretudo, como ela diz ao processador onde estão os dados sobre os quais deve operar?
Deixo-lhe duas provocações. Primeira: dois processadores podem ter ISAs completamente diferentes e ainda assim serem indistinguíveis em desempenho num benchmark típico — como isso é possível? Segunda: o PIC18F4550 tem setenta e cinco instruções; um Intel Core moderno tem mais de mil e quinhentas. Como justificar a discrepância sem dizer que um é “mais poderoso” que o outro? Guarde as suas respostas: retorno a elas ao fim do material.
O caminho percorre a ISA como contrato entre software e hardware; a anatomia de uma instrução; os modos de endereçamento; a oposição CISC × RISC e a convergência moderna; o conjunto de instruções do PIC18F4550 organizado em famílias; o trajeto do C até o assembly; e amarra tudo nas três tarefas do Projeto Integrador deste módulo.
ISA: o Contrato entre Software e Hardware
Toda a engenharia de computação moderna separa cuidadosamente o que uma máquina faz de como ela faz. A descrição do que a máquina faz — o conjunto de operações executáveis, o significado de cada uma, os recursos visíveis ao programador — chama-se Instruction Set Architecture, ou ISA. Tudo o que está abaixo é organização ou microarquitetura, e o programador não tem acesso direto a essa camada: dialoga com a máquina exclusivamente pela ISA.
A palavra contrato é deliberada. O software se compromete a emitir apenas sequências de instruções definidas; o hardware se compromete a executá-las com a semântica documentada, independentemente de como a microarquitetura interna esteja organizada em uma geração específica. É esse contrato que permite que um programa compilado em 2003 para x86 ainda execute num chip de 2026, vinte e três anos durante os quais a microarquitetura foi profundamente reinventada sem que o contrato fosse rompido.
flowchart LR
SW["Software<br/>(programa, compilador)"]
ISA["ISA<br/>contrato durável"]
UARCH["Microarquitetura<br/>(pipeline, cache,<br/>execução fora de ordem)"]
IMPL["Implementação<br/>(silício, transistores,<br/>tensões, timings)"]
SW -->|"emite instruções"| ISA
ISA -->|"é realizada por"| UARCH
UARCH -->|"é fabricada como"| IMPL
IMPL -.->|"executa<br/>preservando o contrato"| ISA
Formalmente, defino uma ISA como uma tupla \mathcal{I} = (\Sigma, \mathcal{F}, \mathcal{R}, \mathcal{M}, \mathcal{A}, \mathcal{X}, \sigma), onde \Sigma é o conjunto finito de instruções, \mathcal{F} mapeia padrões binários em operações abstratas, \mathcal{R} é o conjunto de registradores visíveis, \mathcal{M} o modelo de memória, \mathcal{A} o conjunto de modos de endereçamento, \mathcal{X} os mecanismos de exceção e \sigma a função de transição de estado. A consequência prática é a compatibilidade binária: o mesmo arquivo binário executa em qualquer chip da família sem recompilação. Foi a IBM, em 1964, com a família System/360, que inaugurou esse princípio ao definir uma única ISA implementada por modelos com microarquiteturas radicalmente distintas.
Antes de seguir, pergunte-se: se a Microchip lançar amanhã um PIC18F4550 fabricado em tecnologia de 28 nm em vez dos 130 nm originais, com pipeline de quatro estágios em vez de dois, isso quebraria a compatibilidade com o seu código existente? Por quê? A resposta está no conceito de contrato arquitetural que acabei de apresentar.
Há um custo nessa permanência: decisões originais ficam cravadas no contrato. O x86 carrega ainda hoje modos de endereçamento dos tempos do 8086 e instruções aritméticas decimais quase nunca usadas. A indústria aprendeu a conviver com a herança porque quebrar a compatibilidade seria mais caro. Para o estudante de baixo nível há uma consequência prática direta: quando você compreende a ISA, consegue prever que sequência de instruções o compilador gerará para uma construção em C. Essa capacidade é o fundamento da otimização e, mais importante, da depuração rigorosa quando um problema aparece no nível do assembly — situação que ocorrerá em algum momento do seu Projeto Integrador.
Anatomia de uma Instrução de Máquina
Considere ADDWF f, d, a — add W and file — soma o conteúdo de W com o de um File Register e armazena o resultado em um destino especificado pela própria instrução. Três operandos textuais: f é o endereço do registrador, d é o destino (W ou o próprio f) e a é o seletor de banco. No nível binário, essa instrução é uma palavra única de dezesseis bits: seis bits de opcode (0010 01), um bit de destino, um bit de banco e oito bits de endereço do File Register.
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"]
Os seis bits mais significativos identificam univocamente que esta instrução é ADDWF; formam o opcode. Os dez bits seguintes especificam os operandos. Essa partição rígida — uma região para o opcode, outra para os operandos — é a estrutura tipológica de qualquer instrução de máquina em qualquer ISA. A questão central que todo arquiteto enfrenta é a divisão do orçamento de bits entre opcode e operandos.
Existe uma classificação clássica pelo número de operandos explícitos. Máquinas três-operandos escrevem ADD R3, R1, R2 (R_3 \leftarrow R_1 + R_2) e oferecem máxima flexibilidade ao custo de instruções maiores: é o padrão RISC clássico (MIPS, RISC-V, ARMv8). Máquinas dois-operandos fundem segundo fonte com destino (ADD R1, R2): é o x86. Máquinas um-operando ou de acumulador escrevem simplesmente ADD R2 (A \leftarrow A + R_2): foi o esquema do Intel 4004 e 8008. O PIC18F4550 é máquina de um operando e meio — tem um único acumulador W mas o bit d decide se o resultado volta para W ou é escrito no próprio File Register. Essa flexibilidade econômica é marca da Microchip.
Outra decisão fundamental é comprimento fixo ou variável. O PIC18F4550 é exemplo clássico de comprimento fixo: dezesseis bits por instrução. A unidade de busca sabe, antes de decodificar, que está lendo uma instrução completa. O decodificador opera com largura de entrada constante e o pipeline fica regular. O x86, em contraste, tem instruções de um a quinze bytes; ganha densidade e paga complexidade de decodificador. O PIC18 tem uma exceção: poucas instruções — CALL, GOTO longo e MOVFF — ocupam duas palavras, mas o pipeline preserva regularidade tratando a segunda palavra como NOP enquanto seus bits complementam o endereço da instrução anterior.
Há um aspecto da semântica que escapa na primeira passagem e é fonte de bugs sutis: muitas instruções alteram o registrador STATUS como efeito colateral documentado. No PIC18F4550 ele mantém cinco bits: C (carry/borrow), DC (digit carry), Z (zero), OV (overflow) e N (negativo). Cada instrução aritmética e lógica documenta quais flags atualiza. A regra mnemônica é simples: quando uma instrução parece não fazer nada visível, suspeite do registrador de status. O MOVF f, F aparentemente move f para si mesmo — mas atualiza Z conforme o conteúdo, e funciona como teste-de-zero implícito em um único ciclo.
Modos de Endereçamento
Como cada campo de operando especifica de onde vem o dado a ser somado? O dado está embutido literalmente na instrução? Está em um registrador? Numa posição da memória cujo endereço a instrução fornece? Numa posição cujo endereço deve ser calculado a partir de um endereço-base e de um deslocamento? Cada uma dessas formas é um modo de endereçamento.
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"]
O modo mais simples é o imediato: o valor do operando está embutido na instrução. MOVLW 0x2A carrega o literal 0x2A em W sem acesso à memória de dados. É rápido mas rígido — o valor é fixado em tempo de compilação. Toda atribuição x = 42 ou comparação if (n == 100) em C gera, com altíssima probabilidade, uma instrução com operando imediato.
O endereçamento por registrador especifica que o operando reside em um registrador identificado por um índice na instrução. No PIC18, W participa de quase todas as instruções aritméticas como operando implícito no opcode; quando uma instrução referencia um File Register, o campo de oito bits do operando f identifica diretamente a posição no banco. Note que o “registrador” do PIC18 é uma noção peculiar: diferentemente de ARM ou MIPS, que têm banco dedicado separado da RAM, o PIC18 trata os File Registers como posições de RAM endereçáveis. W é a única exceção — não tem endereço; vive dentro da CPU.
No endereçamento direto (ou absoluto), a instrução contém o endereço completo do operando. A limitação clássica é o espaço endereçável: oito bits cobrem 256 posições. O PIC18 resolve isso com bancos: o campo de oito bits é complementado pelo BSR (Bank Select Register), que indica em qual dos dezesseis bancos a posição está. O bit a de ADDWF f, d, a controla esse comportamento: a = 0 ignora o BSR e usa o Access Bank (primeiras 128 posições do banco 0 mais últimas 128 do banco 15, onde residem os SFRs); a = 1 consulta o BSR.
O modo que abre as portas para estruturas de dados dinâmicas e ponteiros é o endereçamento indireto: o operando não é o dado, é o endereço de uma posição que contém o endereço do dado. No PIC18 isso é implementado pelos três File Select Registers (FSR0, FSR1, FSR2), cada um formado por dois registradores de oito bits que formam um ponteiro de doze bits para qualquer posição da RAM. Para cada FSR existem cinco registradores virtuais — INDFn, POSTINCn, POSTDECn, PREINCn, PLUSWn — que ao serem referenciados causam leitura ou escrita na posição apontada, opcionalmente com pré- ou pós-incremento/decremento ou deslocamento somado a W. A correspondência com C é direta: INDF0 é *p, POSTINC0 é *p++, PREINC0 é *++p, PLUSW0 é *(p + w).
Por que o indireto é o modo mais importante
O endereçamento indireto é o mecanismo pelo qual todas as estruturas de dados dinâmicas, todas as travessias de arrays, todas as chamadas a funções com parâmetros por referência são implementadas em código de máquina. Quando você escreve *p em C, é ele que faz a coisa funcionar. Sem esse modo, linguagens com ponteiros simplesmente não seriam implementáveis com a eficiência que temos hoje.
O endereçamento indexado combina dois operandos no endereço efetivo, tipicamente endereço-base mais deslocamento. É o casamento natural com array em linguagens de alto nível. Em ARM, LDR R3, [R1, #8] carrega R_3 com o conteúdo de R_1 + 8. O PIC18 não tem indexado puro com soma livre, mas PLUSWn realiza, em uma instrução, o cálculo *(FSRn + W) — equivalente do base-deslocamento com base no FSR e deslocamento em W. Combinado com pré- e pós-incremento, é expressividade suficiente para qualquer iteração sobre arrays.
Por fim, a arquitetura Harvard modificada do PIC18 oferece o endereçamento por tabela em memória de programa. Como as instruções aritméticas operam apenas sobre memória de dados, valores em Flash precisam ser trazidos para a RAM antes do uso. Os registradores TBLPTRU:TBLPTRH:TBLPTRL formam um ponteiro de vinte e um bits para qualquer posição da Flash, e as instruções TBLRD*, TBLRD*+, TBLRD*-, TBLRD+* leem o byte apontado para o registrador TABLAT, opcionalmente com auto-incremento. Esse é o mecanismo que o compilador C usa quando você declara uma variável const em memória de programa.
CISC e RISC: Duas Filosofias e a Convergência Moderna
A sigla CISC, Complex Instruction Set Computer, é retroativa. O IBM System/360, o DEC VAX-11, o Motorola 68000, o Intel 8086 foram projetados sob restrições tecnológicas concretas dos anos 1960 e 1970. A primeira era o custo absurdo da memória: cada byte do binário era item de custo material, e reduzir tamanho era prioridade. A consequência foi natural: instruções de comprimento variável, modos de endereçamento sofisticados, instruções complexas capazes de copiar blocos de memória, calcular polinômios ou manipular caracteres em uma única operação. A segunda restrição era a lentidão da memória em relação ao processador. A terceira, talvez a mais determinante, era a ausência de compiladores otimizadores: software dos anos 1970 ainda era frequentemente escrito à mão em assembly, e instruções de alto nível semântico aproximavam a ISA das construções de Pascal e Algol — a famosa semantic gap closure.
flowchart LR
subgraph CISC["CISC (anos 1970)"]
C1["Memória cara<br/>= densidade<br/>de código alta"]
C2["Tamanho variável<br/>1 a 15 bytes"]
C3["Muitos modos<br/>de endereçamento"]
C4["Instruções<br/>complexas<br/>(POLY, MOVS...)"]
end
subgraph RISC["RISC (anos 1980)"]
R1["Memória barata<br/>= pipeline limpo<br/>é prioridade"]
R2["Tamanho fixo<br/>(16 ou 32 bits)"]
R3["Poucos modos<br/>de endereçamento"]
R4["Load-store<br/>+ banco grande<br/>de registradores"]
end
subgraph CONV["Convergência moderna"]
M1["x86: traduz<br/>internamente<br/>em micro-ops RISC"]
M2["ARM: ganhou<br/>extensões SIMD<br/>complexas"]
M3["PIC18: RISC<br/>com concessões<br/>pragmáticas"]
end
CISC --> CONV
RISC --> CONV
Por volta de 1980, pesquisadores da IBM (projeto 801, John Cocke), de Berkeley (RISC I, David Patterson) e de Stanford (MIPS, John Hennessy) questionaram essas premissas. As evidências eram cumulativas: apenas uma fração pequena das instruções CISC era de fato usada pelos compiladores; as instruções complexas ocupavam transistores que poderiam ser caches ou pipelines mais profundos; e pipelines eficientes — técnica que estudaremos nos Módulos 06 e 07 — eram praticamente impossíveis sobre ISAs com instruções de duração variável e acessos arbitrários à memória embutidos em operações aritméticas. A irregularidade CISC torna-se inimiga estrutural do paralelismo de pipeline.
A resposta foi o RISC, Reduced Instruction Set Computer: núcleo simples, regular e ortogonal, deixando que a complexidade seja construída pelo compilador. Os princípios canônicos: instruções de comprimento fixo, regularidade de codificação, arquitetura load-store, poucos modos de endereçamento, banco grande de registradores e ausência de efeitos colaterais não documentados.
A oposição não é equilibrada como costuma ser apresentada. CISC oferece densidade de código superior, decisiva em memória restrita. RISC oferece pipelines profundos eficientes, decodificação simples e suporte natural para superscalaridade. A surpresa histórica veio nos anos 1990: a Intel, pressionada por processadores RISC, não abandonou a ISA x86 — a base de software era valiosa demais — e, a partir do Pentium Pro de 1995, passou a traduzir internamente as instruções x86 em micro-operações RISC chamadas \muops, executá-las em núcleo internamente RISC com pipeline profundo e superscalar, e apresentar externamente o mesmo x86. Foi simultaneamente capitulação parcial ao mérito do RISC e vitória do princípio contratual da ISA.
O PIC18F4550 é inequivocamente RISC: setenta e cinco instruções em famílias regulares, comprimento fixo, um ou dois ciclos por instrução, princípio load-store. Há concessões CISC-like: a fusão semântica do bit d, as instruções compostas como DECFSZ que combinam aritmética com decisão de fluxo, e os modos de endereçamento indireto com auto-incremento embutido. São concessões típicas de microcontroladores, em que cada byte de Flash conta e a economia de uma instrução num laço executado milhares de vezes é mensurável.
O Conjunto de Instruções do PIC18F4550
As setenta e cinco instruções do PIC18 estão organizadas em seis famílias funcionais. Você não precisa decorá-las — basta reconhecer os mnemônicos, entender a estrutura de cada família e saber consultar o datasheet quando necessário. Aprender uma ISA é como aprender vocabulário de língua estrangeira: a fluência vem do uso.
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"]
A família de movimentação de dados é a mais usada. A instrução fundamental é MOVF f, d, a, que move f para W ou para o próprio f — e atualiza Z, o que a torna útil para testes de zero. MOVWF f, a faz o caminho inverso. MOVLW k carrega um imediato em W. MOVFF fs, fd move dados entre dois File Registers sem passar por W (dupla palavra). LFSR f, k carrega doze bits no FSR atomicamente, também em dupla palavra.
A família aritmética inclui ADDWF, ADDWFC (soma com carry, essencial para múltiplas palavras), SUBWF, SUBFWB, INCF, DECF, MULWF (multiplica W por f depositando o resultado de dezesseis bits em PRODH:PRODL) e a versão literal MULLW. O PIC18 implementa multiplicação em um ciclo, vantagem importante em processamento de sinais. Não há divisão em hardware — divisões são feitas em software pelo compilador, com custo de dezenas de ciclos para oito bits e centenas para dezesseis bits, motivo para rever algoritmos e preferir deslocamentos ou tabelas pré-calculadas.
A família lógica é análoga à aritmética: ANDWF, IORWF, XORWF com versões literais; COMF (complemento bit a bit) e SWAPF (troca nibbles em um ciclo, útil em conversões BCD/ASCII). As instruções de deslocamento são RLCF e RRCF (rotações através do carry) e RLNCF/RRNCF (sem carry).
A família de manipulação de bit é central em microcontroladores, em que ligar/desligar pinos, testar bits de status e construir máscaras é tarefa onipresente. As instruções são BCF (clear), BSF (set), BTG (toggle), BTFSC (test, skip if clear) e BTFSS (test, skip if set). Cada uma faz em um ciclo o que sem instrução dedicada exigiria três (carrega, mascara, armazena). BTFSC e BTFSS ainda fundem o teste com a decisão de pular a próxima instrução.
A família de controle de fluxo inclui desvios incondicionais BRA (relativo, onze bits) e GOTO (absoluto, dupla palavra); desvios condicionais BZ, BNZ, BC, BNC, BOV, BNOV, BN, BNN; chamadas CALL (dupla palavra, com push do PC na pilha de hardware) e RCALL (relativa); retornos RETURN e RETLW. As skip combinadas, das quais DECFSZ é a mais usada, decrementam f e pulam a próxima instrução se o resultado for zero — laços de N iterações tomam exatamente essa forma. A pilha de hardware tem trinta e um níveis e não é visível como memória endereçável.
A família de propósito específico agrupa SLEEP, RESET, CLRWDT (limpa o watchdog), NOP (útil em rotinas de timing fino do LCD Sunstar 2004A do seu projeto) e as quatro TBLRD para acesso à Flash, com suas duais TBLWT para escrita.
Exemplo prático: checksum de oito bits
Vou tornar tudo isso concreto com o exemplo que considero canônico deste módulo: o cálculo de um checksum de oito bits sobre um vetor de trinta e dois bytes em RAM. Em C escrevemos a versão didática deixando ao compilador todas as decisões; a versão otimizada aplica 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.
flowchart LR
INIT["LFSR 0, buffer<br/>(carrega ponteiro)"]
INIT --> CLR["CLRF W<br/>(zera acumulador)"]
CLR --> SET["MOVLW N<br/>MOVWF CONT"]
SET --> LOOP["Laço:<br/>ADDWF POSTINC0, W<br/>DECFSZ CONT<br/>BRA laço"]
LOOP -->|"DECFSZ<br/>chega a 0"| FIM["W contém<br/>checksum"]
LOOP -.->|"3 ciclos<br/>por byte"| LOOP
03_checksum_didatico.c
/*
* 03_checksum_didatico.c
*
* Calculo de checksum de 8 bits sobre um vetor armazenado em RAM.
* Versao didatica: usa indice inteiro convencional, deixando todas as
* decisoes de alocacao de registradores e modos de enderecamento a
* cargo do compilador XC8 com nivel de otimizacao padrao.
*
* Plataforma: PIC18F4550 @ 48 MHz (kit ACEPIC PRO V8.2).
*/
#include <xc.h>
#include <stdint.h>
/* Tamanho fixo do buffer usado pelo Projeto Integrador. */
#define TAM_BUFFER 32
/* Buffer global em RAM. Alocado pelo linker em um banco fixo,
* o que produz acesso direto (enderecamento absoluto) nas
* instrucoes geradas pelo compilador. */
volatile uint8_t buffer[TAM_BUFFER];
/* Calcula a soma modulo 256 dos bytes do buffer.
* Esta funcao serve como referencia para a versao otimizada. */
uint8_t checksum_didatico(void) {
uint8_t soma = 0;
uint8_t i;
for (i = 0; i < TAM_BUFFER; i++) {
soma += buffer[i];
}
return soma;
}03_checksum_otimizado.c
/*
* 03_checksum_otimizado.c
*
* Versao extremamente otimizada do checksum de 8 bits.
* Estrategias aplicadas:
* - ponteiro avanca diretamente em vez de indice indexado;
* - laco do-while permite ao compilador eliminar o teste
* redundante na primeira iteracao;
* - contagem decrescente ate zero permite que o teste de
* fim se transforme em DECFSZ (uma unica instrucao);
* - 'register' sugere manutencao em W/temporarios.
*
* Plataforma: PIC18F4550 @ 48 MHz (kit ACEPIC PRO V8.2).
*/
#include <xc.h>
#include <stdint.h>
#define TAM_BUFFER 32
extern volatile uint8_t buffer[TAM_BUFFER];
uint8_t checksum_otimizado(void) {
register uint8_t soma = 0;
register uint8_t n = TAM_BUFFER;
register const volatile uint8_t *p = buffer;
do {
soma += *p++;
} while (--n);
return soma;
}03_checksum_didatico.asm
;
; 03_checksum_didatico.asm
;
; Versao manuscrita em assembly do checksum de 8 bits sobre o
; vetor 'buffer' (32 bytes em RAM). Estilo didatico: prioriza
; clareza e correspondencia direta com o pseudocodigo em C.
;
; Conjunto de instrucoes: PIC18F4550 (MPASM/pic-as).
; Convencoes: WREG armazena resultados intermediarios; FSR0
; aponta para o vetor; o contador esta em CONTADOR.
;
#include <xc.inc>
PSECT udata_acs
SOMA: DS 1 ; resultado do checksum
CONTADOR: DS 1 ; numero de bytes restantes
PSECT code
GLOBAL checksum_didatico
checksum_didatico:
; Inicializa FSR0 com o endereco do buffer (modo indireto).
LFSR 0, buffer
; SOMA <- 0
CLRF SOMA, ACCESS
; CONTADOR <- 32
MOVLW 32 ; W <- 32 (modo imediato)
MOVWF CONTADOR, ACCESS
laco:
; W <- *FSR0; FSR0 incrementa apos uso (POSTINC0).
MOVF POSTINC0, W, ACCESS
; SOMA <- SOMA + W
ADDWF SOMA, F, ACCESS
; CONTADOR <- CONTADOR - 1; pula a proxima se ficar zero.
DECFSZ CONTADOR, F, ACCESS
BRA laco
; Retorna em W o resultado do checksum.
MOVF SOMA, W, ACCESS
RETURN03_checksum_otimizado.asm
;
; 03_checksum_otimizado.asm
;
; Versao manuscrita extremamente otimizada do checksum de 8 bits.
; Diferencas em relacao a versao didatica:
; - elimina a variavel SOMA em RAM: usa apenas WREG para acumular;
; - usa CONTADOR diretamente em registrador de acesso rapido (ACCESS);
; - reduz o corpo do laco a 3 instrucoes apenas.
;
; CPI medio teorico (sem contar o desvio tomado): 3 ciclos por byte.
;
#include <xc.inc>
PSECT udata_acs
CONT: DS 1
PSECT code
GLOBAL checksum_otimizado
checksum_otimizado:
LFSR 0, buffer ; FSR0 -> &buffer[0]
MOVLW 32
MOVWF CONT, ACCESS
CLRF WREG, ACCESS ; W = 0 (acumulador)
laco:
ADDWF POSTINC0, W, ACCESS ; W <- W + *FSR0++ (uma unica instrucao!)
DECFSZ CONT, F, ACCESS
BRA laco
; W ja contem o checksum; basta retornar.
RETURNA versão otimizada em assembly exemplifica de forma cristalina os modos de endereçamento. LFSR 0, buffer carrega no FSR0 o endereço-base do vetor em uma única instrução de dupla palavra. O contador é mantido no Access Bank, com endereçamento direto. O acumulador é o próprio W. O corpo do laço é a estrela: ADDWF POSTINC0, W, ACCESS realiza, em um ciclo, três operações — lê o byte apontado por FSR0 (indireto), soma ao W, e auto-incrementa FSR0. Em RISC puro sem modos auto-incrementais isso demandaria duas ou três instruções. A versão otimizada reduz o corpo a três instruções (soma com pós-incremento, decremento-com-skip, desvio), CPI próximo de três ciclos por byte. A diferença típica para a didática é de quinze a vinte por cento — mensurável quando o laço executa milhares de vezes por segundo.
Pergunta para fixar: por que a versão didática em assembly paga uma penalidade de desempenho em relação à otimizada, se ambas realizam exatamente o mesmo cálculo? Qual recurso da ISA do PIC18 a versão otimizada usa que a didática descarta? Identificar essa diferença é o teste de que a Seção de modos foi compreendida.
Exemplo prático: busca indexada com tabela
O segundo exemplo combina endereçamento indexado via FSR com o conceito de tabela. Suponha que o seu Projeto Integrador precise traduzir um código numérico de um byte em uma medida correspondente armazenada como inteiro de dezesseis bits. A tabela é fixa em tempo de compilação e ocuparia espaço precioso em RAM. A solução natural é armazená-la em Flash via const e implementar a busca linear via FSR.
03_busca_indexada_didatico.c
/*
* 03_busca_indexada_didatico.c
*
* Busca linear em uma tabela armazenada na memoria de programa
* (Flash) do PIC18F4550. A tabela contem pares (codigo, valor)
* usados pelo Projeto Integrador para traduzir codigos numericos
* em mensagens curtas exibidas no LCD Sunstar 2004A.
*
* O exemplo serve para ilustrar enderecamento indexado (acesso a
* arrays) combinado com enderecamento por tabela em Flash via
* registradores TBLPTR e a instrucao TBLRD.
*
* Versao didatica: escreve a busca como em qualquer C ANSI,
* deixando o compilador gerar todo o codigo de acesso a Flash.
*/
#include <xc.h>
#include <stdint.h>
#define N_ENTRADAS 8
/* Tabela armazenada em memoria de programa (Flash).
* O atributo 'const' instrui o compilador a aloca-la em
* uma secao de Flash, acessada por enderecamento indireto
* via TBLPTR (no PIC18 isso usa instrucao TBLRD*+). */
const struct {
uint8_t codigo;
uint16_t valor;
} tabela[N_ENTRADAS] = {
{0x01, 1000}, {0x02, 1500}, {0x03, 2000}, {0x04, 2500},
{0x05, 3000}, {0x06, 3500}, {0x07, 4000}, {0x08, 4500}
};
/* Retorna o valor associado ao codigo solicitado;
* devolve 0 quando nao encontrado. */
uint16_t buscar_didatico(uint8_t codigo_alvo) {
uint8_t i;
for (i = 0; i < N_ENTRADAS; i++) {
if (tabela[i].codigo == codigo_alvo) {
return tabela[i].valor;
}
}
return 0;
}03_busca_indexada_didatico.asm
;
; 03_busca_indexada_didatico.asm
;
; Implementacao didatica da busca linear em uma tabela armazenada
; em RAM (por simplicidade, ja replicada em RAM pelo programa).
; Cada entrada ocupa 3 bytes: 1 byte de codigo + 2 bytes de valor
; (little-endian). FSR1 percorre a tabela com POSTINC1.
;
; Esta versao prioriza clareza: usa rotulos explicitos e nao funde
; operacoes em uma unica instrucao.
;
#include <xc.inc>
PSECT udata_acs
ALVO: DS 1
RESL: DS 1
RESH: DS 1
CONT: DS 1
PSECT code
GLOBAL buscar_didatico
buscar_didatico:
MOVWF ALVO, ACCESS ; argumento veio em W
LFSR 1, tabela
MOVLW 8 ; N_ENTRADAS
MOVWF CONT, ACCESS
prox:
; Compara *FSR1 com ALVO; se igual, copia os 2 bytes seguintes.
MOVF POSTINC1, W, ACCESS ; W <- codigo da entrada
CPFSEQ ALVO, ACCESS ; pula proxima se W == ALVO
BRA nao_eh
; Encontrou: le dois bytes consecutivos (LSB depois MSB).
MOVF POSTINC1, W, ACCESS
MOVWF RESL, ACCESS
MOVF POSTINC1, W, ACCESS
MOVWF RESH, ACCESS
RETURN
nao_eh:
; Avanca FSR1 dois bytes para alinhar na proxima entrada.
MOVF POSTINC1, W, ACCESS
MOVF POSTINC1, W, ACCESS
DECFSZ CONT, F, ACCESS
BRA prox
; Nao encontrou: zera o resultado.
CLRF RESL, ACCESS
CLRF RESH, ACCESS
RETURNA análise do assembly revela um padrão recorrente: a instrução CPFSEQ compara dois File Registers e pula a próxima instrução se forem iguais. É exemplo de como o PIC18, embora RISC, oferece instruções compostas que fundem aritmética com decisão de fluxo em um ciclo. A alternativa em RISC puro seria duas instruções. Acostume-se a procurar CPFSEQ, CPFSLT, CPFSGT e suas duais BTFSC/BTFSS no Disassembly Listing — reconhecer esses idiomas é o que separa o leitor fluente de assembly do leitor titubeante.
Da Linguagem C ao Assembly
O compilador XC8 da Microchip atravessa cinco fases: análise lexical e sintática; análise semântica (tipos e símbolos); geração de código intermediário; otimização (propagação de constantes, subexpressões comuns, inlining, eliminação de código morto, desenrolamento de laços); e geração de código de máquina. Você pode influenciar algumas escolhas — pelo nível de otimização, pelos qualificadores register, volatile, const, e pela forma como escreve o algoritmo. Outras dependem da heurística interna.
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"]
Alguns padrões típicos. Uma atribuição a = b; entre variáveis de oito bits traduz-se em MOVF b, W e MOVWF a — dois ciclos no Access Bank; em bancos diferentes, o compilador insere MOVLB, adicionando ciclos. Uma soma a = b + c; vira MOVF b, W, ADDWF c, W, MOVWF a. Um teste if (n == 0) vira MOVF n, F (que atualiza Z) seguido de BNZ rotulo_skip — exatamente o atalho do MOVF f, F. Um laço for (i = 0; i < 10; i++) é frequentemente transformado pelo compilador em contagem decrescente para usar DECFSZ em vez de comparação explícita, o que significa que escrever laços decrescentes na origem nem sempre traz ganho.
Quando vale a pena escrever assembly
Na maioria dos casos, otimização manual em assembly não vale a pena. O XC8 com otimização moderada produz código tão bom quanto o que um humano escreveria após muitas horas. As exceções são três: laços críticos que dominam o tempo total do programa (ganhos de vinte a cinquenta por cento plausíveis); rotinas com timing preciso em que cada ciclo conta (bit-banging serial, controle de displays, varredura de teclado); e acesso a recursos que o C não expõe. Fora dessas situações, a recomendação é simples: escreva em C, meça, e só desça para assembly com evidência empírica. Premature optimization is the root of all evil, dizia Knuth.
Para examinar o assembly, o MPLAB X oferece o Disassembly Listing em Window > Output, mostrando lado a lado C e assembly. A Symbol Table lista variáveis e funções com endereços e bancos — útil para estimar custos de acesso. O simulador integrado executa passo a passo. O Stopwatch mede o número exato de ciclos entre dois pontos, e é com ele que você cumprirá a segunda tarefa do Projeto Integrador.
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}, executada como transição de estado \sigma. Vimos que 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, do imediato ao indireto com auto-incremento. Confrontamos CISC e RISC e a convergência moderna. Estudamos as setenta e cinco instruções organizadas em famílias funcionais. E atravessamos a fronteira do C para o assembly entendendo como o compilador traduz e quando otimização manual se justifica.
O Plano de Aulas prevê três tarefas no kit ACEPIC PRO V8.2. A primeira é a análise sistemática do assembly gerado pelo XC8 para pelo menos três funções já implementadas no seu projeto — por exemplo, conversão de inteiro para string no LCD, validação de entrada por teclado e inicialização de periférico. Para cada uma você abrirá o Disassembly Listing, identificará as instruções geradas, classificará os modos de endereçamento e produzirá uma análise de uma a duas páginas. A segunda é a reimplementação otimizada em assembly de uma função crítica — checksum, cópia ou conversão de string, varredura de teclado matricial — medindo o desempenho com o Stopwatch e relatando quantitativamente o ganho. A terceira é a documentação dos modos de endereçamento usados no projeto, correlacionando padrões do seu C (“acesso a campo de struct”, “iteração sobre array”, “teste de bit”, “leitura de constante em Flash”) com modos e instruções geradas. Essa tabela serve como referência permanente para o restante do semestre.
Antecipo o que virá. O Módulo 4 abre o processador e examina o caminho de dados que executa as instruções que estudamos aqui. O Módulo 5 estuda a unidade de controle, vendo as setenta e cinco instruções como entradas de uma máquina de estados finitos. Os Módulos 6 e 7 introduzem o pipeline, e você se lembrará da regularidade RISC sendo amiga do pipeline — as penalidades de desvio de GOTO e BRA ganharão tratamento quantitativo.
Retorno às duas provocações. Primeira: dois processadores podem ser indistinguíveis em desempenho com ISAs completamente diferentes? Sim. ARM e MIPS de gerações comparáveis são indistinguíveis em muitos benchmarks; RISC-V moderno compete em mesmo patamar com ARM. ISAs distintas comprometem a compatibilidade binária, não o desempenho potencial. Segunda: como justificar 75 instruções no PIC18 contra mais de 1500 no Intel Core sem dizer que um é “mais poderoso”? O número de instruções reflete decisões sobre densidade, expressividade, complexidade do decodificador e domínio de aplicação. O PIC18 prioriza simplicidade de silício, decodificação rápida e regularidade do pipeline — coerente com uma ISA enxuta. O Intel Core acumulou ao longo de décadas extensões SIMD, criptográficas, vetoriais e de virtualização para cargas diversas. Pontos diferentes no espaço de trade-offs; nenhum é objetivamente mais poderoso, cada um é adequado a um domínio distinto.
Antes da próxima aula, abra o capítulo do datasheet do PIC18F4550 dedicado ao conjunto de instruções e folheie-o sem pressa. Não é para memorizar — é para descobrir onde encontrar cada informação. Esse capítulo será o seu companheiro de trabalho pelas próximas semanas, e quanto antes você criar intimidade com ele, menor o atrito que sentirá ao desenvolver o projeto.