Manual de Referência: Assembly para o PIC18F4550

Este apêndice é o seu mapa do assembly do PIC18. Ele começa bem devagar — do “o que é uma instrução” — e termina com as tabelas de consulta rápida que você vai abrir toda vez que esquecer a sintaxe de um mnemônico. A ideia não é decorar tudo, e sim entender o suficiente para ler o assembly que o XC8 gera e escrever os trechos curtos onde assembly compensa. Tudo para o PIC18F4550 no kit ACEPIC PRO V8.2, com o montador pic-as do compilador MPLAB XC8.

Por que aprender assembly se o compilador já faz isso?

Pergunta justa. Eu respondo com três situações reais. Quando você abre o arquivo de listagem (.lst) e vê tblrd*+ acessando um array const, entende o que está acontecendo porque já escreveu tblrd na mão. Quando precisa de uma rotina muito enxuta — uma ISR mínima, um delay de precisão, um protocolo bit-bang — assembly dá o controle fino que C não dá. E quando um programa em C se comporta de um jeito estranho, você abre a janela de desmontagem do MPLAB X e lê o que o processador realmente está fazendo. Saber assembly transforma o compilador de caixa-preta em parceiro transparente.

O modelo mental: quatro caixas

Antes de escrever uma linha, fixe quatro elementos do PIC18F4550. O primeiro é o W (Working register), uma caixa de 8 bits que é o rascunho universal: quase toda conta passa por ele. O segundo é a memória de programa (Flash, 32 KB), onde moram as instruções. O terceiro é a memória de dados (RAM, 2 KB), onde ficam as variáveis. O quarto são os SFRs, que são posições de RAM com nomes especiais (TRISD, LATD, PORTB…) que controlam os periféricos.

flowchart TB
    subgraph CPU["dentro do processador"]
        W["W — 8 bits, rascunho"]
        PC["PC — endereço da próxima instrução"]
        STATUS["STATUS — Z, C, N, OV, DC"]
    end
    subgraph FLASH["Flash (32 KB)"]
        INST["suas instruções:<br/>bsf LATD, 3"]
    end
    subgraph RAM["RAM (2 KB)"]
        GPR["variáveis"]
        SFR["SFRs: TRISD, LATD..."]
    end
    PC --> INST
    INST --> W
    W <--> GPR
    W <--> SFR

Guarde a separação: instrução mora na Flash, dado mora na RAM (é a arquitetura Harvard modificada). O PC (Program Counter) guarda o endereço da próxima instrução e avança sozinho; quando você escreve bra Loop, está mexendo no PC à mão — e é só isso que existe por trás de laços, condicionais e chamadas.

Anatomia de uma instrução

Olhe esta linha com calma:

    bsf     LATD, 3

São três partes. bsf é o mnemônico (Bit Set in File register: ligue um bit). LATD é o registrador sobre o qual ela age. 3 é o bit (o bit 3 é o pino RD3). Repare que não há um operando de banco: o pic-as descobre sozinho que LATD fica no Access Bank, então você não precisa escrever o seletor. Ao executar, o processador pega o byte de LATD, força o bit 3 a 1 sem tocar nos outros sete, e o LED de RD3 acende — tudo em um ciclo, que a 8 MHz dura 500 ns.

A estrutura obrigatória de um programa

Todo arquivo de assembly para o kit segue este molde: inclui as definições, configura os fusíveis, declara variáveis, define os vetores e termina com end. No kit, o bootloader AN1310 mora no topo da Flash (0x7CFC0x7FFF) e preserva os vetores nativos — por isso seu código começa em 0x0000, como num PIC sem bootloader.

    processor 18F4550
    #include <xc.inc>           ; traz os nomes TRISD, LATD, etc.

    config  FOSC = HSPLL_HS
    config  WDT = OFF
    config  LVP = OFF
    config  MCLRE = ON
    config  PBADEN = OFF
    config  CPUDIV = OSC1_PLL2

    psect   udata_acs           ; variáveis no Access Bank
contador:   ds 1                ; reserva 1 byte; 'contador' vira o endereço

    psect   resetVec,abs
    org     0x0000              ; vetor de reset nativo
    goto    Main

    psect   ivHigh,abs
    org     0x0008              ; interrupção de alta prioridade
    retfie  1                   ; 1 = restaura do shadow (FAST)

    psect   ivLow,abs
    org     0x0018              ; interrupção de baixa prioridade
    retfie  0

    psect   code                ; o linker posiciona este código
Main:
    clrf    TRISD               ; PORTD inteiro como saída
    setf    LATD                ; todos os 8 LEDs acesos
Loop:
    bra     Loop                ; trava aqui para sempre
    end                         ; fim do arquivo (esquecer dá erro!)
    processor 18F4550
    #include <xc.inc>
    config FOSC = HSPLL_HS
    config WDT = OFF
    config LVP = OFF
    config MCLRE = ON
    config PBADEN = OFF
    config CPUDIV = OSC1_PLL2
    psect udata_acs
contador: ds 1
    psect resetVec,abs
    org 0x0000
    goto Main
    psect ivHigh,abs
    org 0x0008
    retfie 1
    psect ivLow,abs
    org 0x0018
    retfie 0
    psect code
Main:
    clrf TRISD
    setf LATD
Loop:
    bra Loop
    end

No pic-as, o linker posiciona sozinho os psects relocáveis (psect code); você só fixa endereços à mão, declarando o psect como absoluto (abs) e usando org, exatamente o que fazemos para os vetores de hardware. Se um dia usar o XC8 em projeto grande, reserve a área do bootloader em Project Properties com ROM ranges: default,-7CFC-7FFF, para o linker não escrever por cima do AN1310. Os vetores do seu programa continuam nos endereços nativos (0x0000, 0x0008, 0x0018).

Por que você quase nunca escreve o seletor de banco

A RAM do PIC18 tem 4 KB de espaço, mas o campo de endereço dentro da instrução só tem 8 bits (256 endereços). A solução foi dividir a RAM em 16 bancos de 256 bytes e usar o BSR (Bank Select Register) para escolher o banco ativo. Trocar de banco a toda hora com movlb seria lento e cheio de bugs. Por isso existe o Access Bank: uma janela de 256 bytes que junta os primeiros 96 bytes de RAM (0x0000x05F) com os SFRs no topo (0xF600xFFF), acessível na hora sem mexer no BSR. O pic-as resolve isso por você: como LATD, TRISD, PORTB e as variáveis que você declara em psect udata_acs vivem no Access Bank, o montador escolhe sozinho essa janela e você simplesmente não escreve nenhum seletor de banco — daí clrf TRISD, e não clrf TRISD, ACCESS. Só quando precisar de uma variável fora do Access Bank é que você acrescenta , b (banked) e seleciona o banco com movlb antes.

Declarando variáveis: psect udata_acs e ds

Em C você escreve unsigned char contador;. Em assembly é a mesma ideia, mais explícita:

    psect   udata_acs    ; daqui em diante, variáveis no Access Bank
contador:   ds 1         ; reserva 1 byte; o rótulo vira o endereço
buffer:     ds 8         ; 8 bytes consecutivos

ds n (Define Storage) reserva n bytes e associa o rótulo ao primeiro. O linker escolhe os endereços — você nunca os fixa à mão. Precisou de mais de 96 bytes? Declare um psect próprio de RAM (com space=1), use , b (banked) nas instruções e chame movlb k antes. Para começar, fique no Access Bank.

Movendo dados: movlw, movwf, movf

Três setas, cada uma numa direção. movlw k carrega o literal k em W (é a única forma de pôr uma constante no processador). movwf f copia W para o registrador f. movf f, df e manda para W (se d=w) ou de volta para f (se d=f, útil porque atualiza o flag Z).

    movlw   0x55             ; W <- 0x55
    movwf   LATD             ; LATD <- W
    movf    PORTB, w         ; W <- PORTB (lê os pinos)

Cuidado com a base dos números: no pic-as, um literal sem prefixo é decimal. Então movlw 200 carrega duzentos. Para hexadecimal, use o prefixo 0x (movlw 0x55); para binário, use o sufixo B (movlw 01010101B). Marque sempre a base que não for óbvia — escrever movlw 55 achando que carregava 0x55 põe cinquenta e cinco no W em silêncio, e esse engano já custou muitas horas de gente.

Manipulando bits e tomando decisões

Quatro instruções de bit, e elas são o coração do controle de I/O. bsf f, b liga o bit b; bcf f, b desliga; btg f, b inverte. A quarta é diferente: ela não muda nada, ela testa um bit e decide se pula a próxima instrução. btfss (skip if set) pula se o bit for 1; btfsc (skip if clear) pula se for 0. Esse “pula ou não pula” é toda a lógica condicional do PIC.

; if (RB0 apertado, nível 0) acende tudo; else apaga tudo
    setf    TRISB              ; PORTB entrada
    clrf    TRISD              ; PORTD saída
Loop:
    btfss   PORTB, 0           ; pula próx. se RB0 = 1 (solto)
    bra     Apertado           ; não pulou: RB0 = 0
    clrf    LATD               ; RB0=1: apaga
    bra     Loop
Apertado:
    setf    LATD               ; RB0=0: acende
    bra     Loop

Quando a condição de pular é verdadeira, btfss/btfsc gastam 2 ciclos (descartam a instrução seguinte); quando é falsa, gastam 1.

Aritmética, lógica e a sutileza do subwf

As instruções de byte operam entre W e um registrador, com o resultado indo para W ou para F conforme você escolher:

    movlw   10
    addwf   contador, f          ; contador <- contador + 10
    incf    contador, f          ; contador++
    decf    contador, f          ; contador--

Atenção a subwf f, d: a conta é f - W, não W - f (a sigla é “SUBtract W from F”). É fonte recorrente de bug. As lógicas andwf, iorwf, xorwf, comf seguem o mesmo molde e servem para máscaras; e há as versões com literal andlw, iorlw, xorlw, addlw, sublw, uma palavra mais compactas. Para isolar o nibble baixo, por exemplo: movf valor, w seguido de andlw 0x0F.

Laços: a coreografia do decfsz

O padrão idiomático de laço contado é decfsz f, d (Decrement, Skip if Zero): decrementa e pula a próxima se chegou a zero. Junto com um bra que volta, vira um laço:

    movlw   10                  ; 10 iterações
    movwf   contador
Inicio:
    btg     LATD, 0             ; corpo do laço
    decfsz  contador, f         ; contador--; pula se zerou
    bra     Inicio              ; não zerou: repete
    ; aqui está fora do laço

Subrotinas e a pilha de hardware

Você marca um trecho com rótulo, termina com return e chama com call. O call empilha o endereço de retorno numa pilha de hardware de 31 níveis; o return o desempilha. Não existe PUSH/POP genérico — a pilha só guarda endereços de retorno; variáveis locais vão para a RAM. Cuidado com aninhamento muito profundo: estourados os 31 níveis, o PIC sobrescreve o mais antigo silenciosamente. Recursão profunda, na prática, é proibida.

Delay500ms:                     ; ~500 ms a 8 MHz (3 laços aninhados)
    movlw   10
    movwf   c_ext
D_ext:
    movlw   167
    movwf   c_med
D_med:
    movlw   200
    movwf   c_int
D_int:
    decfsz  c_int, f
    bra     D_int
    decfsz  c_med, f
    bra     D_med
    decfsz  c_ext, f
    bra     D_ext
    return

A instrução movff fs, fd copia de qualquer endereço para qualquer endereço sem passar por W — ótima para copiar entre SFRs, como movff TABLAT, LATD.

STATUS: o boletim do processador

Toda operação aritmética ou lógica atualiza o registrador STATUS com flags: Z (resultado zero), C (carry/transporte), N (negativo, bit 7), OV (estouro com sinal) e DC (transporte do nibble baixo). Você raramente lê STATUS direto; o normal é testar um flag logo depois da operação que o gerou:

    movf    valor_a, w
    subwf   valor_b, w           ; faz valor_b - valor_a
    btfsc   STATUS, Z            ; pula se Z=0 (deu diferente de zero)
    bra     Iguais               ; Z=1: eram iguais
    bra     Diferentes

A ordem é vital: o flag reflete só a última operação. Se você interpuser outra instrução que mexe em STATUS entre a conta e o teste, testa o flag errado.

Os modos de endereçamento

São quatro maneiras de a instrução achar o operando. O imediato embute a constante na instrução (movlw 0x5A). O direto usa o campo f (clrf TRISD). O relativo ao PC é o dos desvios (bra n soma um deslocamento ao PC). E o indireto, via FSR/INDF, é o mais poderoso: o PIC tem três ponteiros (FSR0/1/2); ao acessar INDF0 você acessa o endereço que está em FSR0. Com os modificadores, você percorre arrays:

    lfsr    FSR0, array         ; FSR0 aponta para o array
    movf    indice, w           ; W <- indice
    movf    PLUSW0, w           ; W <- RAM[FSR0 + indice] = array[indice]

POSTINC0 acessa e incrementa FSR0; POSTDEC0 acessa e decrementa; PREINC0 incrementa e acessa; PLUSW0 faz acesso indexado. É exatamente assim que o C implementa array[indice] na RAM.

bra contra goto

bra n é um desvio relativo: deslocamento de 11 bits com sinal, alcance de ±1 KB, 1 palavra de instrução, 2 ciclos. goto k é absoluto: endereço completo, alcança todo o espaço de programa, mas ocupa 2 palavras. Regra prática: bra para desvios locais (que é a maioria) e goto só para saltos longos, como o goto Main no vetor de reset.

Lendo a Flash com tblrd

Dados const (tabelas) ficam na Flash e se leem com a família tblrd, carregando o endereço em TBLPTR e lendo o byte em TABLAT:

    movlw   upper(tabela)
    movwf   TBLPTRU
    movlw   high(tabela)
    movwf   TBLPTRH
    movlw   low(tabela)
    movwf   TBLPTRL
    tblrd*+                      ; lê o byte e incrementa TBLPTR
    movf    TABLAT, w            ; W <- byte lido

Os modificadores são * (não mexe no ponteiro), *+ (pós-incremento, o mais usado para percorrer arrays), *- (pós-decremento) e +* (pré-incremento).

Onde alocar variáveis: o psect udata_acs e os bancos

Três escolhas decidem onde suas variáveis nascem, todas expressas com a diretiva psect. O psect predefinido udata_acs aloca no Access Bank (acesso rápido, sem movlb e sem seletor de banco na instrução) e é a sua escolha padrão. Para mais que os 96 bytes do Access Bank, declare um psect próprio de RAM (com space=1) e deixe o linker posicioná-lo num banco normal — aí use , b (banked) nas instruções e chame movlb k antes, ou endereçamento indireto via FSR, que ignora bancos. Variáveis já inicializadas com um valor não cabem num psect de RAM pura: guarde os valores num psect de dados na Flash e copie-os para a RAM na partida, o que custa ciclos e espaço; em projetos C o runtime do XC8 faz isso por você, mas em assembly puro a cópia é sua — use com parcimônia.

Tabelas de despacho com retlw: um idioma clássico do PIC

Esta é uma das técnicas mais elegantes e características do assembly para PIC, e vale conhecer mesmo que você acabe preferindo arrays na RAM. A ideia é guardar constantes como operandos de instruções retlw (Return with Literal in W) e saltar para a retlw certa somando o índice ao PC. Para acessar o elemento de índice i, você soma 2i ao byte baixo do Program Counter (PCL) com addwf PCL, f, caindo exatamente sobre a retlw desejada, que retorna na hora com o valor em W.

; tabela de padrões de LED (acende i+1 LEDs)
    psect   tabela,abs
    org     0x1100          ; OBRIGATÓRIO: alinhada em 256 bytes
Get_padrao:
    addwf   PCL, f          ; PC <- PC + W (W = 2 x indice)
    retlw   0x01            ; indice 0
    retlw   0x03            ; indice 1
    retlw   0x07            ; indice 2
    retlw   0x0F            ; indice 3
    ; ... ate 0xFF

flowchart LR
    CALL["call Get_padrao<br/>(W = 2 x indice)"] --> ADD["addwf PCL, f<br/>soma 2i ao PC"]
    ADD --> R0["retlw 0x01 (i=0)"]
    ADD --> R1["retlw 0x03 (i=1)"]
    ADD --> RN["... (demais indices)"]
    R0 --> RET["retorna com o valor em W"]
    R1 --> RET
    RN --> RET

Duas regras inegociáveis aqui. A tabela precisa começar num endereço múltiplo de 256, senão a soma em PCL pode gerar um carry que não se propaga para PCLATH e o salto cai no lugar errado (no pior caso, na área do bootloader); por isso ela vive num psect absoluto com org alinhado. E a tabela pode ser acessada com call, nunca com bra ou goto — o retlw desempilha o endereço que o call empilhou; sem esse call, ele retorna para um endereço inválido.

Depurando em assembly

Mesmas três técnicas do C, com um detalhe: em assembly não há “variável com nome” para o depurador, então você precisa saber em qual endereço de RAM cada coisa foi alocada (a janela File Registers mostra a RAM toda).

A depuração por LEDs é imediata e barata — escreva direto em LATD em qualquer ponto: movlw 0x55 / movwf LATD / call Delay200ms. Espalhe esses checkpoints e veja até qual padrão o programa chegou. A depuração por USART segue a mesma lógica do C, com SPBRG = 51 para 9600 bps a 8 MHz. E o simulador do MPLAB X deixa executar instrução por instrução (F7 = Step Into) e observar W, STATUS, FSR e a RAM mudarem a cada passo; use Run to Cursor para pular o delay sem rodar milhares de iterações.

Armadilhas que pegam todo mundo

São poucas e previsíveis, então vou contá-las como história para você memorizar. Esquecer o end no fim do arquivo dá um erro críptico de “unexpected end of file” — a última linha é sempre end. Escrever em PORTx em vez de LATx causa o read-modify-write hazard: LEDs vizinhos acendem ou apagam sozinhos; escreva sempre em LATx e leia entradas em PORTx. Esquecer o prefixo 0x num valor hexadecimal: no pic-as movlw 55 carrega cinquenta e cinco (decimal), não 0x55; marque a base. Confundir d=w com d=f (incf contador, w não muda contador, manda o resultado para W) é outro erro mudo. Interpor instruções entre a conta e o teste de flag testa o flag errado. E usar return para sair de um trecho onde se entrou com goto em vez de call faz o programa saltar para um endereço aleatório — subrotina só com call+return.

Referência rápida das instruções

Instrução Operação Ciclos
movlw k / movwf f / movf f,d W←k / f←W / W←f (ou f←f) 1
clrf f / setf f f←0 / f←0xFF 1
incf f,d / decf f,d f±1 1
addwf f,d / subwf f,d W+f / f−W 1
andwf/iorwf/xorwf/comf f,d lógicas bit a bit 1
addlw/sublw/andlw/iorlw/xorlw k mesmas, com literal em W 1
bsf/bcf/btg f,b liga/desliga/inverte bit b 1
btfss/btfsc f,b pula próx. se bit =1 / =0 1 (2 se pular)
decfsz/incfsz f,d ±1 e pula se zerou 1 (2/3 se pular)
cpfseq/cpfsgt/cpfslt f compara com W; pula se =,>,< 1 (2/3 se pular)
bra n / goto k desvio relativo ±1 KB / absoluto 2
call k / rcall n / return chamada / chamada relativa / retorno 2
tblrd* (, +, -, +) lê Flash para TABLAT 2

Nas instruções de byte acima, o destino d é w (W) ou f (registrador). O seletor de banco normalmente é omitido, porque o pic-as escolhe sozinho o Access Bank; para um registrador fora dele, acrescente , b (banked) e selecione o banco com movlb antes.

Endereços dos SFRs mais usados

SFR Endereço Função
PORTA–PORTE 0x080–0x084 leitura dos pinos
LATA–LATE 0x089–0x08D escrita nos pinos (LATD=LEDs)
TRISA–TRISE 0x092–0x096 direção (1=entrada, 0=saída)
STATUS 0xFD8 flags C, Z, N, OV, DC
WREG 0xFE8 acumulador W
BSR 0xFE0 seleção de banco
FSR0L/FSR0H 0xFEA/0xFEB ponteiro indireto 0
TBLPTRL/H/U 0xFF6/7/8 ponteiro de tabela (tblrd)
TABLAT 0xFF5 byte lido da Flash
ADCON1 0xFC1 0x0F = pinos digitais
CMCON 0xFB4 0x07 = comparadores off
TXREG/SPBRG/TXSTA/RCSTA 0xFAD/0xFAF/0xFAC/0xFAB USART

Checklist antes de gravar

Passe por aqui antes de cada gravação. processor 18F4550 e #include <xc.inc> no topo e diretivas config corretas (sobretudo LVP = OFF). Vetores em psects absolutos (abs) nos endereços nativos (0x0000, 0x0008, 0x0018) e código principal num psect code. Toda variável declarada com ds antes do uso. Todo laço com saída garantida (um decfsz que zera) e toda subrotina terminando em return. Tabelas com retlw/addwf PCL em psect absoluto alinhado a 256 bytes. O arquivo termina com end. Compilou sem erros nem avisos. Só então grave pelo bootloader.

Para aprofundar

O arquivo xc.inc (na pasta do compilador MPLAB XC8) e os cabeçalhos de dispositivo que ele inclui listam os nomes dos SFRs e os nomes de campo do config. O comportamento binário, os flags e os ciclos de cada instrução estão no PIC18F/PIC18LF Instruction Set Reference; os periféricos, no PIC18F4550 Data Sheet (DS39632E); e todas as diretivas do montador (psect, org, ds, equ, macros), no MPLAB XC8 PIC Assembler User’s Guide.