flowchart LR
A["Você pensa:<br/>'acender o LED 3'"] --> B["Em C:<br/>LATDbits.LATD3 = 1;"]
B --> C["O compilador XC8<br/>traduz para assembly"]
C --> D["Em assembly:<br/>BSF LATD, 3, ACCESS"]
D --> E["O montador traduz<br/>para binário (1 palavra)"]
E --> F["O processador executa<br/>em 1 ciclo de relógio"]
F --> G["O LED acende"]
Manual Didático de Assembly para o PIC18F4550
Este manual ensina linguagem de montagem (assembly) para o PIC18F4550 do zero, no ritmo de quem nunca escreveu uma linha sequer. Diferente do manual de referência, aqui eu não me preocupo em listar todas as instruções de uma vez; eu construo o conhecimento aos poucos, sempre partindo de um problema concreto no kit ACEPIC PRO V8.2 e mostrando como o processador o resolve por dentro. Leia em ordem, sem pular seções, executando mentalmente cada exemplo antes de prosseguir. Ao final, você terá compreendido não apenas como escrever assembly, mas por que cada decisão da arquitetura existe.
Antes de Começar: O Que Você Está Prestes a Entender
Quando você escreve LATD = 0xFF; em C e o LED do kit acende, há uma camada inteira de tradução que acontece em silêncio entre seu código e o circuito. O compilador XC8 olha essa linha e produz, no final, uma sequência de números binários que o processador interpreta como ordens elementares. Cada ordem dessas se chama instrução de máquina. Cada instrução de máquina tem um nome curto e mnemônico — uma sigla — que humanos usam para escrevê-la sem precisar lidar diretamente com binário. Esse conjunto de mnemônicos, mais as regras de como combiná-los, é o que chamamos de linguagem assembly.
A primeira coisa que preciso que você internalize é a seguinte: quando você programa em assembly, você não está mais conversando com o compilador; está conversando diretamente com o processador. Não existe if, não existe for, não existe atribuição. Existe apenas a sequência exata de instruções que o processador vai executar, uma por vez, sem reordenar e sem otimizar nada por você. Essa franqueza é o que torna assembly tão didático: ao escrevê-lo, você é forçado a entender exatamente como o processador funciona.
A segunda coisa é menos óbvia mas igualmente importante: aprender assembly do PIC18 não é desperdício de tempo mesmo que você programe sempre em C. Quando seu programa em C apresenta um comportamento estranho, você vai abrir a janela de desmontagem do MPLAB X e olhar o assembly gerado. Se você não souber lê-lo, é como tentar diagnosticar uma falha mecânica num carro sem entender o que cada peça faz. Saber assembly transforma o compilador, de caixa-preta misteriosa, em parceiro transparente.
A Anatomia de uma Instrução: Aprendendo a Ler Antes de Escrever
Quero que você olhe esta linha com calma:
Essa linha tem quatro partes, e cada uma carrega significado preciso. A primeira é o mnemônico BSF, que vem de Bit Set in File register — em português, “ativar um bit em um registrador de arquivo”. A segunda é LATD, o nome do registrador sobre o qual a instrução vai agir; nesse caso é o registrador que controla as saídas do PORTD, onde estão conectados os LEDs do kit. A terceira é 3, o número do bit a ser ativado dentro desse registrador; o bit 3 corresponde ao pino RD3, que acende o quarto LED da fileira (contando a partir de zero). A quarta é ACCESS, uma palavra-chave que diz ao processador para encontrar LATD em uma região especial da memória chamada Access Bank — sobre isso falo mais adiante.
A executar essa linha, o processador faz exatamente uma coisa: pega o byte armazenado em LATD, força o bit 3 a se tornar 1 sem mexer nos outros sete bits, e devolve o byte para LATD. Imediatamente o pino RD3 sai de 0 V para 5 V, e o LED acende. Tudo isso em um único ciclo de máquina, que a 8 MHz dura 500 nanossegundos.
Para ver o efeito completo no kit, esta única linha precisa estar dentro de um programa mínimo que configure RD3 como saída e mantenha o processador rodando:
; Acende o LED em RD3 e fica parado em laço infinito
#include <p18f4550.inc>
CONFIG FOSC=HS, WDT=OFF, LVP=OFF, MCLRE=ON, PBADEN=OFF, CPUDIV=OSC1_PLL2
ORG 0x1000 ; Vetor de reset (kit usa bootloader)
GOTO main
ORG 0x1008
RETFIE FAST
ORG 0x1018
RETFIE
ORG 0x1020
main:
BCF TRISD, 3, ACCESS ; RD3 configurado como saída
BSF LATD, 3, ACCESS ; Acende o LED em RD3
loop:
BRA loop ; Trava aqui para sempre
ENDRepare que a versão otimizada não muda a lógica; apenas comprime espaços em branco, comentários e abreviações. O comportamento no chip é idêntico: uma palavra de Flash por instrução, um ciclo de máquina por execução. Ao longo deste manual sempre apresento as duas formas para que você se habitue tanto a ler código bem espaçado e comentado quanto a interpretar código compacto, que é o que aparece em listagens reais e em trechos publicados na literatura.
O Modelo Mental do Processador: Quatro Caixas e Algumas Ruas
Para programar em assembly com confiança, você precisa de um modelo mental claro do que existe dentro do PIC18F4550. Esquecer transistores e portas lógicas por um momento; pensar apenas em quatro elementos. O primeiro elemento é o registrador de trabalho W, uma caixa de 8 bits que serve como rascunho universal: quase toda operação aritmética ou lógica passa por ele. O segundo elemento é a memória de programa, que guarda as instruções que você escreveu, gravadas na Flash do chip; ela tem 32 KB. O terceiro elemento é a memória de dados (RAM), com 2 KB, onde ficam as variáveis temporárias do seu programa. O quarto elemento é o conjunto de registradores especiais (SFRs), que são posições da RAM com nomes especiais como TRISD, LATD, PORTB, e que controlam os periféricos do chip — portas de entrada e saída, conversor analógico, temporizadores, USART.
flowchart TB
subgraph CPU["Dentro do processador"]
W["W (Working Register)<br/>8 bits — rascunho universal"]
PC["PC (Program Counter)<br/>21 bits — endereço da próxima instrução"]
STATUS["STATUS<br/>bits Z, C, N, OV, DC"]
end
subgraph FLASH["Memória de Programa (Flash, 32 KB)"]
INST["Suas instruções:<br/>BSF LATD, 3, ACCESS<br/>MOVLW 0x55<br/>..."]
end
subgraph RAM["Memória de Dados (RAM, 2 KB)"]
GPR["Variáveis (GPRs)<br/>minha_var, contador, ..."]
SFR["Periféricos (SFRs)<br/>TRISD, LATD, PORTB, ..."]
end
PC --> INST
INST --> W
W <--> GPR
W <--> SFR
Note bem a separação: instruções moram na Flash, dados moram na RAM. Essa arquitetura, conhecida como Harvard modificada, é uma das características que torna o PIC18 rápido — o processador pode buscar a próxima instrução enquanto executa a atual, sem disputar barramento com leituras de dados.
O Program Counter (PC) é uma caixa especial que guarda o endereço da próxima instrução a ser buscada na Flash. A cada instrução executada, o PC avança automaticamente. Quando você escreve um desvio como BRA loop, está modificando o PC manualmente, fazendo-o saltar para outro ponto da Flash. Esse é o mecanismo único por trás de toda lógica condicional, laços e chamadas de função em assembly.
Sua Primeira Linha: Acendendo um LED
Pare aqui e abra mentalmente o MPLAB X. Vamos escrever, juntos, o menor programa assembly possível que faça algo visível no kit: acender todos os LEDs do PORTD. Em C, esse programa tem três linhas; em assembly tem cerca de quinze, contando estrutura obrigatória. Mas cada uma dessas quinze linhas vai ensinar algo. Acompanhe:
; ----------------------------------------------------------------
; Primeiro programa: acende todos os LEDs do PORTD
; Processador: PIC18F4550 a 8 MHz com bootloader do kit
; ----------------------------------------------------------------
#include <p18f4550.inc> ; Traz os nomes TRISD, LATD, etc.
CONFIG FOSC=HS, WDT=OFF, LVP=OFF, MCLRE=ON, PBADEN=OFF, CPUDIV=OSC1_PLL2
; Como o kit usa bootloader, nosso código mora a partir de 0x1000.
; Aqui só temos o vetor de reset; nada mais.
ORG 0x1000 ; Bootloader pula para este endereço
GOTO main ; e nós imediatamente desviamos para 'main'
ORG 0x1008 ; Vetor de interrupção de alta prioridade
RETFIE FAST ; ainda não usamos interrupção; retorna logo
ORG 0x1018 ; Vetor de interrupção de baixa prioridade
RETFIE ; idem
; ----------------------------------------------------------------
; Código principal
; ----------------------------------------------------------------
ORG 0x1020 ; Programa começa após os vetores
main:
CLRF TRISD, ACCESS ; TRISD = 0 → PORTD inteiro como saída
SETF LATD, ACCESS ; LATD = 0xFF → todos os 8 LEDs acesos
loop:
BRA loop ; Para sempre aqui
END ; Diretiva: fim do arquivo fonteVou explicar cada bloco com paciência, porque entendê-lo bem é a base para tudo. A linha #include <p18f4550.inc> instrui o montador a ler um arquivo de definições fornecido pela Microchip que contém todos os nomes simbólicos — TRISD, LATD, PORTB, e centenas de outros — junto com os endereços onde cada SFR mora na RAM. Sem essa inclusão, você teria que escrever 0x95 em vez de TRISD, e seu código ficaria ilegível.
A diretiva CONFIG grava bytes especiais nas chamadas palavras de configuração da Flash, que o processador lê na partida para decidir como vai se comportar. FOSC=HS diz que o cristal de 8 MHz do kit é de alta velocidade; WDT=OFF desliga o watchdog timer (que reiniciaria o chip se ficasse ocioso); LVP=OFF desativa a programação em baixa tensão, exigência absoluta quando se usa o bootloader; MCLRE=ON ativa o pino de reset externo. Essas configurações são feitas uma única vez na vida do programa, antes de qualquer instrução rodar.
O bloco com ORG 0x1000, ORG 0x1008 e ORG 0x1018 define os três vetores fixos do PIC18F4550. O processador, quando é ligado ou recebe reset, executa automaticamente o que estiver no endereço de reset. Como o kit tem bootloader gravado de 0x0000 a 0x0FFF, o bootloader pula para 0x1000 ao terminar sua tarefa de receber código via USB; é por isso que nosso GOTO main precisa morar exatamente nesse endereço. Os outros dois vetores são para interrupções; como nosso programa não usa interrupções, eles apenas retornam imediatamente com RETFIE.
Finalmente chegamos a main. Apenas duas instruções compõem o coração do programa. CLRF TRISD, ACCESS zera o registrador TRISD (de Tri-State Register D), o que configura todos os oito pinos de PORTD como saídas — em PIC18, bit 0 em TRIS significa “este pino é saída”, bit 1 significa “este pino é entrada”. SETF LATD, ACCESS coloca 0xFF (todos os bits em 1) em LATD (de Latch D), o registrador de escrita do PORTD. No instante seguinte os oito LEDs acendem. Em seguida loop: BRA loop é um laço infinito; sem ele, o PC continuaria avançando para endereços não inicializados, com comportamento imprevisível.
A diretiva END no fim avisa ao montador que o arquivo terminou. Sem ela, o montador erra. Esqueça-se de colocá-la uma vez e você aprende para sempre.
Por que ACCESS Aparece em Toda Linha: O Quebra-Cabeça dos Bancos
Você deve ter notado que toda instrução até agora termina em ACCESS. Para entender o porquê, preciso mostrar como o PIC18 organiza sua RAM. A memória de dados do chip tem 4 KB de espaço de endereçamento (endereços de 0x000 a 0xFFF), mas o campo de endereço dentro de cada instrução tem apenas 8 bits — capaz de representar somente 256 endereços. A solução adotada pela arquitetura é dividir a RAM em dezesseis bancos de 256 bytes cada e usar um registrador especial chamado BSR (Bank Select Register) para indicar qual banco está ativo.
block-beta
columns 4
BSR0["Banco 0<br/>0x000-0x0FF"]:1
BSR1["Banco 1<br/>0x100-0x1FF"]:1
BSR2["Banco 2<br/>0x200-0x2FF"]:1
BSR3["..."]:1
BSR15a["..."]:1
BSR15b["..."]:1
BSR15c["..."]:1
BSRF["Banco 15<br/>0xF00-0xFFF<br/>(SFRs)"]:1
Sem mais nada, antes de acessar qualquer variável você teria que escrever MOVLB 5 (move literal to BSR) para selecionar o banco 5, fazer o acesso, e talvez precisar selecionar outro banco em seguida. Isso seria lento, propenso a erros, e particularmente irritante quando suas variáveis estão num banco e os SFRs (que ficam no banco 15) estão em outro. Os engenheiros da Microchip pensaram nisso e criaram uma janela especial chamada Access Bank: os primeiros 96 bytes da RAM física (0x000 a 0x05F, normalmente do banco 0) somados aos 160 bytes mais altos (0xF60 a 0xFFF, onde moram os SFRs) formam, juntos, uma região de 256 bytes acessível instantaneamente sem precisar setar o BSR. Quando você escreve ACCESS na sua instrução, está dizendo ao processador “use a janela Access Bank em vez do banco selecionado pelo BSR”.
A maioria absoluta do seu código vai usar ACCESS. As variáveis frequentes, você aloca no Access Bank com a diretiva UDATA_ACS (veja a próxima seção). Os SFRs como LATD e TRISD já estão sempre no Access Bank por construção do chip. Só quando precisar de mais de 96 bytes de RAM é que você usará bancos normais e, aí sim, terá que se preocupar com MOVLB.
Declarando Variáveis: A Diretiva UDATA_ACS
Em C você escreve unsigned char contador; e o compilador acha um cantinho na RAM para a variável. Em assembly você faz a mesma coisa, só que mais explícito:
A diretiva UDATA_ACS (de Uninitialized DATA in ACCess bank) abre uma seção de declaração de variáveis no Access Bank. Cada RES n reserva n bytes consecutivos e associa o rótulo à esquerda ao endereço do primeiro byte. O montador decide os endereços; você nunca precisa escolhê-los manualmente. Depois disso, você pode usar contador em qualquer instrução como se fosse um SFR:
Se um dia precisar de mais que os 96 bytes do Access Bank, troque UDATA_ACS por UDATA, e nas instruções use BANKED em vez de ACCESS, lembrando-se de chamar MOVLB k antes (onde k é o número do banco). Para começar, mantenha-se sempre em UDATA_ACS — fica mais simples e mais rápido.
Movendo Dados: A Trindade MOVLW, MOVWF, MOVF
Antes de fazer qualquer coisa interessante, você precisa saber mover dados entre registradores, RAM e literais. Três instruções fazem essa coreografia: MOVLW, MOVWF e MOVF. Pense nelas como três setas, cada uma apontando em uma direção diferente.
A instrução MOVLW k (MOVe Literal to W) carrega o valor constante k no registrador W. É a única forma de pôr um valor literal no processador. Exemplo:
A notação dos números no MPASM merece atenção. Por padrão, todo número é interpretado como hexadecimal — MOVLW 200 carregaria 0x200, que truncado a 8 bits vira 0x00, certamente não o que você queria. Para forçar decimal, prefixe com ponto: .200 significa 200 decimal. Para binário, use B'...'. Para hexadecimal explícito, use 0x ou H'...'. Adote como hábito sempre marcar a base; isso evita bugs sutis e horas perdidas de depuração.
A instrução MOVWF f (MOVe W to File register) faz o caminho inverso: copia o conteúdo de W para o registrador de RAM cujo endereço é f. Exemplo:
Note que para carregar um valor literal em um SFR é preciso sempre o intermédio de W. Não existe instrução que mova um literal diretamente para um endereço de RAM; o W age como zona de transferência obrigatória.
A instrução MOVF f, d, a (MOVe File register) lê o conteúdo do registrador f e o deposita em W (se d = W) ou de volta no próprio f (se d = F). O segundo caso parece inútil — copiar um valor de volta para si mesmo? — mas ele afeta o flag Z do STATUS, ficando útil quando você precisa testar se uma variável é zero. Exemplo do uso comum:
Essas três instruções juntas cobrem cerca de 60% do código que você escreverá. Domine-as antes de prosseguir.
Manipulando Bits: Quando Você Quer Apenas um LED
O PIC18 brilha em manipulação de bits individuais — uma característica fundamental para um microcontrolador, cujo trabalho cotidiano é mexer em pinos de entrada/saída. Quatro instruções fazem isso, e cada uma vale o tempo gasto em aprendê-la com calma.
A instrução BSF f, b, a (Bit Set in File register) força o bit b do registrador f a se tornar 1, sem mexer nos outros sete bits. Exemplo: BSF LATD, 3, ACCESS acende o LED conectado a RD3 (o quarto LED, contando a partir de zero). A instrução BCF f, b, a (Bit Clear in File register) faz o oposto: força o bit b a se tornar 0. Exemplo: BCF LATD, 3, ACCESS apaga aquele mesmo LED. A instrução BTG f, b, a (Bit Toggle) inverte o bit b: se estava em 0 vira 1; se estava em 1 vira 0. Exemplo: BTG LATD, 3, ACCESS faz o LED piscar uma vez se chamado em loop.
A quarta instrução é diferente em natureza — ela não altera nenhum bit; ela testa um bit e decide se a próxima instrução deve ser executada ou pulada. Aqui está o coração de toda lógica condicional em PIC18:
Leia com atenção. BTFSC significa “Bit Test, Skip if Clear” — teste o bit, e pule a próxima instrução se o bit estiver em zero (clear). Logo, se o botão em RB0 estiver pressionado (suponhamos que pressionado significa 0 no kit), PORTB, 0 é zero, a instrução BRA botao_solto é pulada, e a execução cai direto em BRA botao_apertado. Se o botão estiver solto (1 lógico), BTFSC não pula, e BRA botao_solto é executada.
A irmã gêmea BTFSS (Bit Test, Skip if Set) faz o oposto: pula se o bit estiver em 1. As duas juntas dão a você toda a flexibilidade para implementar if, else, while, for. Considere o problema: se o botão em RB0 estiver pressionado (nível lógico 0 no kit), acenda todos os LEDs do PORTD; se estiver solto (nível 1), apague-os. A solução em assembly é uma tradução direta de BTFSS mais dois ramos:
; Espelha o estado de RB0 nos 8 LEDs de PORTD, invertido.
; RB0 = 0 (apertado) → LATD = 0xFF (todos acesos)
; RB0 = 1 (solto) → LATD = 0x00 (todos apagados)
SETF TRISB, ACCESS ; PORTB inteiro como entrada
CLRF TRISD, ACCESS ; PORTD inteiro como saída
loop_principal:
BTFSS PORTB, 0, ACCESS ; Testa RB0; pula próx. se RB0 = 1
BRA apertado ; Não pulou: RB0 = 0 (botão apertado)
CLRF LATD, ACCESS ; RB0 = 1: apaga todos os LEDs
BRA loop_principal ; Volta a testar
apertado:
SETF LATD, ACCESS ; RB0 = 0: acende todos os LEDs
BRA loop_principal ; Volta a testarAcompanhe a execução mentalmente. Se RB0 = 1, BTFSS pula a BRA apertado, então CLRF LATD apaga os LEDs e BRA loop_principal volta ao início. Se RB0 = 0, BTFSS não pula, BRA apertado é executada, SETF LATD acende todos os LEDs, e BRA loop_principal retoma. É só isso. Aritmética de bits + desvios condicionais = qualquer estrutura de controle.
Aritmética e Lógica: Operações Sobre Bytes
Depois de mover dados e mexer em bits, o próximo passo natural é fazer contas. As instruções aritméticas e lógicas do PIC18 operam tipicamente entre o registrador W e um registrador de arquivo, depositando o resultado em um dos dois conforme você escolher. A sintaxe genérica é INSTR f, d, a, onde d é W para mandar o resultado para W ou F para mandar de volta para o registrador f.
Note a sutileza de SUBWF f, d, a: a operação é f - W, não W - f. Se você precisar de W - f, use SUBFWB ou inverta os operandos antes. Essa convenção parece confusa no início e é uma fonte recorrente de bugs; lembre-se sempre de que a sigla quer dizer “SUBtract W from F”.
As instruções lógicas seguem o mesmo padrão: ANDWF, IORWF (OR inclusivo), XORWF, COMF (complemento, isto é, NOT). Cada uma trabalha bit a bit entre W e o operando. Use-as para criar máscaras. Por exemplo, para isolar o nibble baixo de um byte, basta um AND com 0x0F:
A instrução ANDLW k é a versão “literal” de AND: faz AND entre W e a constante k. Existem versões literais para todas as operações principais: IORLW, XORLW, ADDLW, SUBLW. Elas dispensam usar a RAM e são uma palavra mais compactas.
Laços: A Coreografia do DECFSZ
Toda linguagem de programação tem alguma forma de repetir blocos de código. Em assembly do PIC, o padrão idiomático para laços contados é uma única instrução chamada DECFSZ f, d, a — DECrement, Skip if Zero. Ela decrementa o registrador f e, se o resultado for zero, pula a instrução seguinte. Combinada com um BRA que volta ao início, forma um laço contado:
MOVLW .10 ; Queremos repetir 10 vezes
MOVWF contador, ACCESS ; contador ← 10
inicio_laco:
; ... aqui vai o corpo do laço ...
BTG LATD, 0, ACCESS ; Por exemplo: pisca o LED 0
; ... mais do corpo ...
DECFSZ contador, F, ACCESS ; contador--; pula próx. se contador = 0
BRA inicio_laco ; Não chegou a zero: repete
; Aqui está fora do laço; contador = 0Acompanhe o que acontece. Na primeira passada, contador vai de 10 para 9; como 9 não é zero, o DECFSZ não pula, e o BRA inicio_laco é executado, voltando ao início. Na décima passada, contador vai de 1 para 0; como o resultado é zero, o DECFSZ pula a próxima instrução (BRA inicio_laco) e cai na primeira instrução após o laço. Resultado: o corpo do laço foi executado exatamente 10 vezes.
Existe ainda INCFSZ, que é igual mas incrementa e pula se chegou a zero (útil para contadores que sobem até estourar). Para a maioria absoluta dos casos, DECFSZ é a escolha.
flowchart TD
INIT["MOVLW .10<br/>MOVWF contador"] --> INICIO["inicio_laco:"]
INICIO --> CORPO["Corpo do laço<br/>(BTG LATD,0,ACCESS, etc.)"]
CORPO --> DEC["DECFSZ contador, F"]
DEC -->|"contador != 0"| BRA["BRA inicio_laco"]
BRA --> INICIO
DEC -->|"contador == 0"| SAIDA["Próxima instrução<br/>(saiu do laço)"]
Subrotinas: CALL e RETURN
Em C, você define funções; em assembly, você define subrotinas. O mecanismo é simples: você marca um trecho de código com um rótulo e termina-o com RETURN. Para invocá-lo, usa CALL nome_da_subrotina. Internamente, o processador empilha o endereço da próxima instrução numa pilha de hardware com 31 níveis de profundidade, e RETURN desempilha esse endereço de volta no PC.
main:
CLRF TRISD, ACCESS
laco_principal:
SETF LATD, ACCESS ; Liga LEDs
CALL delay_visivel ; Chama subrotina (espera ~500 ms)
CLRF LATD, ACCESS ; Apaga LEDs
CALL delay_visivel ; Chama de novo
BRA laco_principal
; -----------------------------------------------------------
; Subrotina: delay_visivel
; Aguarda aproximadamente 500 ms em F_OSC = 8 MHz.
; Usa três contadores aninhados.
; -----------------------------------------------------------
delay_visivel:
MOVLW .10
MOVWF c_externo, ACCESS
laco_e:
MOVLW .167
MOVWF c_medio, ACCESS
laco_m:
MOVLW .200
MOVWF c_interno, ACCESS
laco_i:
DECFSZ c_interno, F, ACCESS
BRA laco_i
DECFSZ c_medio, F, ACCESS
BRA laco_m
DECFSZ c_externo, F, ACCESS
BRA laco_e
RETURNQuando CALL delay_visivel executa, o PC (que aponta para a instrução imediatamente após o CALL) é empilhado, e o PC recebe o endereço de delay_visivel. A subrotina executa seus laços aninhados e ao final encontra RETURN, que desempilha o PC original, fazendo a execução retomar exatamente onde havia parado. Tudo isso é mecânica pura do processador; você não precisa gerenciar pilha manualmente.
Há uma armadilha aqui que não é óbvia: a pilha tem só 31 níveis. Se uma subrotina chama outra que chama outra que chama outra, em algum momento a pilha estoura, e o PIC18 simplesmente sobrescreve o nível mais antigo silenciosamente (por padrão). Recursão profunda, portanto, é proibida na prática. Para o tipo de código que se escreve em microcontroladores, esse limite raramente importa, mas é bom saber.
Lendo Entradas: Botões e Comparações
Para fazer algo mais interessante que piscar LEDs, você precisa ler entradas. Os pinos de PORTB do kit têm botões ligados a eles. Configurar PORTB como entrada e ler o estado de um botão é tão simples quanto:
Após essa leitura, W contém um byte cuja bit 0 reflete o estado de RB0, bit 1 reflete RB1, e assim por diante. Para tomar decisões baseadas em W, você usa as instruções de comparação. A mais comum é CPFSEQ f, a (ComPare File register, Skip if EQual), que compara f com W e pula a próxima instrução se forem iguais:
Existem irmãs CPFSLT (skip if less than) e CPFSGT (skip if greater than) para comparações de magnitude.
Frequentemente o que você quer é mais simples: testar um único bit. Aí volta BTFSC / BTFSS. Se você só precisa saber se RB0 está pressionado, não compare PORTB inteiro com um padrão; apenas teste o bit:
Como regra geral, prefira BTFSS/BTFSC para um único bit, e CPFSEQ para comparações com padrões inteiros.
Tudo Junto: Um Contador Binário nos LEDs
Para amarrar tudo, vamos construir um programa um pouco maior: um contador binário que conta de 0 a 255 nos oito LEDs, avançando uma unidade a cada meio segundo. Os bits mais altos de contador ficarão nos LEDs mais à esquerda, e os mais baixos à direita.
; ----------------------------------------------------------------
; Contador binário de 0 a 255 nos LEDs de PORTD
; Cada incremento separa-se por aproximadamente 500 ms
; ----------------------------------------------------------------
#include <p18f4550.inc>
CONFIG FOSC=HS, WDT=OFF, LVP=OFF, MCLRE=ON, PBADEN=OFF, CPUDIV=OSC1_PLL2
UDATA_ACS
contador RES 1
c_ext RES 1
c_med RES 1
c_int RES 1
ORG 0x1000
GOTO main
ORG 0x1008
RETFIE FAST
ORG 0x1018
RETFIE
ORG 0x1020
main:
; --- Inicializações ---
CLRF TRISD, ACCESS ; PORTD como saída
CLRF contador, ACCESS ; contador = 0
laco_principal:
MOVF contador, W, ACCESS ; W ← contador
MOVWF LATD, ACCESS ; LATD ← W (mostra contador nos LEDs)
CALL delay_500ms ; Aguarda 500 ms
INCF contador, F, ACCESS ; contador++
BRA laco_principal ; Repete (quando passar de 255, volta a 0)
; ----------------------------------------------------------------
; Subrotina de atraso de aproximadamente 500 ms a 8 MHz
; ----------------------------------------------------------------
delay_500ms:
MOVLW .10
MOVWF c_ext, ACCESS
d_ext:
MOVLW .167
MOVWF c_med, ACCESS
d_med:
MOVLW .200
MOVWF c_int, ACCESS
d_int:
DECFSZ c_int, F, ACCESS
BRA d_int
DECFSZ c_med, F, ACCESS
BRA d_med
DECFSZ c_ext, F, ACCESS
BRA d_ext
RETURN
ENDPause aqui e leia esse programa do início ao fim duas vezes. Identifique cada instrução, o que ela faz, e por que está naquela posição. Trace mentalmente a execução das primeiras iterações: contador começa em 0, é copiado para LATD (LEDs apagados), espera 500 ms, incrementa para 1 (LED 0 acende), espera, incrementa para 2 (LED 0 apaga, LED 1 acende), e assim por diante. Quando passar de 255, o byte transborda para 0 naturalmente, e a contagem recomeça. Esse é o seu primeiro programa assembly verdadeiramente útil.
STATUS: O Boletim de Notícias do Processador
Sempre que uma instrução aritmética ou lógica termina, o processador atualiza um registrador especial chamado STATUS, depositando ali um conjunto de flags que descrevem o resultado. Os mais importantes são Z (zero, ligado se o resultado foi zero), C (carry, ligado se houve transporte além do bit 7), N (negative, ligado se o bit 7 do resultado é 1), OV (overflow, ligado se houve estouro com sinal) e DC (digit carry, transporte do nibble baixo para o alto).
Você raramente lê STATUS diretamente. O mais comum é testar um flag específico imediatamente após a instrução que o gerou, usando BTFSC STATUS, Z, ACCESS ou similar. Por exemplo, para testar se uma subtração deu zero (isto é, se dois valores são iguais):
A ordem aqui é vital. O flag Z só reflete o resultado da última instrução aritmética ou lógica. Se entre SUBWF e o BTFSC você executar uma instrução que mexe em STATUS (como outro MOVF, ADDWF, etc.), o teste vai testar o flag errado. Mantenha o teste sempre imediatamente após a operação que produz a condição.
Bug Comum: Lendo PORTD em Vez de LATD
Esta seção poderia salvar você de horas de depuração. Em PIC18, todo pino de saída tem dois registradores associados: PORTx e LATx. Você lê o estado real dos pinos com PORTx e escreve nos pinos com LATx. Por que dois registradores? Porque se você ler PORTD imediatamente após escrever nele, pode ler o valor antigo dos pinos — o circuito interno do chip pode estar ainda atualizando o estado físico. Esse fenômeno se chama “read-modify-write hazard”. A regra prática é simples: sempre escreva em LATx; nunca escreva em PORTx. Para leitura de entradas, PORTx é o correto.
Internamente, BSF é uma operação de leitura-modificação-escrita; ele lê o byte atual, força o bit em 1, e reescreve. Se a leitura vier de PORTD, pode pegar o estado físico, que difere do que você “achou” que tinha escrito por último. Lendo de LATD, você sempre lê o que você escreveu por último. Adote esse hábito desde o primeiro dia.
Diretivas e Mnemônicos: A Diferença que Confunde Iniciantes
Você já viu várias palavras escritas em maiúsculas no código: MOVF, BSF, ORG, UDATA_ACS, CONFIG, RES, END. Vale a pena entender que essas palavras dividem-se em duas categorias completamente diferentes.
A primeira categoria é a dos mnemônicos de instruções de máquina. MOVF, BSF, BRA, RETURN e companhia são nomes de instruções reais que o processador vai executar; cada um corresponde a uma palavra binária na Flash. Quando o montador encontra esses mnemônicos, ele gera o código de máquina correspondente.
A segunda categoria é a das diretivas do montador. ORG, UDATA_ACS, CONFIG, RES, END, #include não geram instruções de máquina; são ordens para o próprio montador. ORG 0x1000 diz “a partir daqui, o endereço de origem das próximas instruções é 0x1000”. UDATA_ACS diz “abra uma seção de declaração de variáveis no Access Bank”. RES 1 diz “reserve 1 byte e dê este endereço ao rótulo”. O processador nunca vê essas diretivas; elas só existem durante a montagem.
Confundir as duas categorias é um sinal típico de quem está começando. Se você se pega pensando “o que essa instrução ORG faz quando o programa roda?”, lembre que ORG não roda; é uma instrução para o montador, não para o processador.
Arquivo de Listagem: Sua Janela para Dentro do Programa
Sempre que o MPLAB X monta seu projeto assembly, ele gera junto um arquivo de listagem com extensão .lst. Esse arquivo é uma das ferramentas mais úteis para aprender — e para depurar. Cada linha do arquivo mostra o endereço final na Flash, os bytes binários gerados, e a linha de código fonte original. Algo assim:
Address Binary Source
000020 6E95 CLRF TRISD, ACCESS
000022 6E8C SETF LATD, ACCESS
000024 D7FF BRA loop
Olhando essa listagem, você confirma exatamente o que o montador produziu, em que endereço cada instrução foi colocada, e quantos bytes ocupa cada uma. Quando algo der errado — por exemplo, quando você esquecer um END e o montador reclamar, ou quando o desvio relativo BRA não couber em ±1 KB — o arquivo de listagem é onde você descobre o porquê.
Erros Mais Comuns dos Iniciantes (e Como Evitá-los)
Quem começa em assembly do PIC18 cai em um conjunto pequeno e previsível de armadilhas. Vou listar os erros que vejo com mais frequência, junto com sintomas e soluções, em forma dissertativa para você memorizar como história, não como lista.
A primeira armadilha é esquecer o END no final do arquivo. O sintoma é o montador reclamar de algo críptico como “unexpected end of file”. A solução é simples: a última linha de todo arquivo assembly deve ser END, sem mais nada depois.
A segunda é colocar o vetor de reset em ORG 0x0000 quando o kit usa bootloader. O sintoma é que o programa parece “sumir” — ao gravar via bootloader, o programa não roda, ou roda comportamento estranho, e às vezes o bootloader em si fica corrompido. A solução é usar sempre ORG 0x1000 para o vetor de reset, ORG 0x1008 para alta prioridade e ORG 0x1018 para baixa prioridade quando o bootloader está envolvido.
A terceira é escrever em PORTx em vez de LATx. Já abordamos isso: o sintoma é que LEDs adjacentes acendem ou apagam quando você só queria mexer em um, ou que BSF/BCF em pinos diferentes interferem entre si. Use sempre LATx para escrita.
A quarta é misturar bases numéricas. Quando você escreve MOVLW 200 pensando em decimal mas o MPASM interpreta como hexadecimal, você carrega 512 truncado para 8 bits, ou seja, 0x00. O LED não acende, o atraso é zero, e você passa horas procurando um bug que está num único caractere ausente. Marque sempre a base: .200 para decimal, 0x para hexadecimal, B'...' para binário.
A quinta é a confusão entre d=W e d=F em instruções como ADDWF, INCF, DECF. Se você escreve INCF contador, W, ACCESS, o resultado vai para W, e contador não muda — o que provavelmente não é o que você queria. Para incrementar a variável de fato, é INCF contador, F, ACCESS. Esse erro é silencioso (não há mensagem de erro), e seu programa simplesmente faz algo diferente do esperado.
A sexta é interpor instruções entre uma operação aritmética e o teste do flag que ela produziu. Se você faz SUBWF a, W, ACCESS, depois MOVWF resultado, ACCESS, e só então BTFSC STATUS, Z, ACCESS, o flag Z testado é o resultado do MOVWF (que não altera Z, então preserva o anterior — funciona neste caso) ou de outra operação que possa estar no meio. Para evitar ambiguidade, teste imediatamente após a operação que gerou a condição.
A sétima e última que vou mencionar agora é tentar usar RETURN para sair de uma subrotina que foi chamada com GOTO em vez de CALL. O sintoma é que o programa pula para um endereço aparentemente aleatório (na verdade, o que estava no topo da pilha de hardware, que pode ser lixo da inicialização). Subrotinas só fazem sentido com CALL+RETURN; usar GOTO para entrar em código que termina com RETURN é receita para crash.
Próximos Passos: O Que Aprender Depois Deste Manual
Você dominou: estrutura mínima do programa, declaração de variáveis no Access Bank, instruções de movimento (MOVLW, MOVWF, MOVF), manipulação de bits (BSF, BCF, BTG, BTFSC, BTFSS), aritmética básica (ADDWF, SUBWF, INCF, DECF), lógica bit a bit (ANDWF, IORWF, XORWF), laços com DECFSZ, subrotinas com CALL/RETURN, comparações com CPFSEQ/CPFSLT/CPFSGT, e leitura de flags do STATUS. Isso já é o suficiente para escrever qualquer programa razoável envolvendo LEDs, botões e atrasos.
O próximo nível, que você encontrará no manual de referência (manual_asm.qmd), inclui endereçamento indireto via FSR/INDF (necessário para arrays e ponteiros), leitura de tabelas na Flash via TBLRD, técnicas de tabela com ADDWF PCL e RETLW, interrupções com salvaguarda de contexto, comunicação serial via USART, e detalhes sobre os bancos de RAM além do Access Bank. Cada um desses tópicos depende do que você consolidou aqui; sem esta base, eles são incompreensíveis. Com esta base, são apenas extensões naturais.
flowchart LR
AQUI["Você está aqui:<br/>fundamentos sólidos"] --> A["Endereçamento indireto<br/>(FSR/INDF, arrays)"]
AQUI --> B["Tabelas na Flash<br/>(TBLRD, dados const)"]
AQUI --> C["Interrupções<br/>(ISRs e salvaguarda)"]
AQUI --> D["USART<br/>(comunicação serial)"]
AQUI --> E["Bancos de RAM<br/>(MOVLB, BANKED)"]
A --> REF["manual_asm.qmd<br/>(referência completa)"]
B --> REF
C --> REF
D --> REF
E --> REF
A recomendação prática que faço é a seguinte. Antes de abrir o manual de referência, escreva você mesmo, com este manual fechado, três programas pequenos no kit: um que pisque os LEDs em sequência (efeito “knight rider”), um que conte de 0 a 9 nos LEDs respondendo a um botão a cada incremento, e uma calculadora simples que leia dois nibbles em PORTB e mostre a soma em PORTD. Não consulte ninguém; apenas o conjunto de instruções que você aprendeu aqui. Quando esses três programas estiverem rodando, você está pronto para o material de referência. Quando não estiverem, releia as seções correspondentes deste manual — provavelmente algum conceito não fixou tão bem quanto parecia.
Fechamento
Assembly não é uma linguagem difícil; é uma linguagem honesta. Cada linha faz exatamente uma coisa, sem mágica, sem abstração escondida. O que parece complicado no começo — bancos, flags, d=W ou d=F, ACCESS versus BANKED — é apenas o reflexo do processador real, sem o filtro do compilador entre você e ele. Uma vez que você aceita essa franqueza e para de procurar por abstrações que não estão lá, o assembly vira uma das ferramentas mais clarificadoras da sua formação. Você passa a ver o assembly gerado pelo XC8 como velho amigo, entende por que certas construções em C são rápidas e outras lentas, e ganha confiança para resolver problemas que antes pareceriam mistérios. Esse é o ganho permanente que justifica o esforço deste manual.