Exercícios do Módulo 03: Conjunto de Instruções e Modos de Endereçamento

Os três exercícios a seguir foram montados para você consolidar, em dificuldade crescente, o que estudamos sobre a ISA do PIC18F4550: a anatomia de uma instrução de dezesseis bits, os modos de endereçamento que dizem ao processador onde estão os dados, a oposição entre as filosofias CISC e RISC e a maneira como o compilador XC8 traduz o seu C em assembly. O primeiro pede que você decodifique uma instrução campo a campo; o segundo exige que você compare dois trechos equivalentes e raciocine sobre modos de endereçamento e custo; o terceiro liga toda essa teoria à primeira tarefa do Projeto Integrador deste módulo, em que você abre o Disassembly Listing do seu próprio código no MPLAB X. Resolva cada um sozinho antes de comparar com o grupo — é decodificando a instrução à mão, com o datasheet aberto, que você ganha a fluência de ler assembly do PIC18 sem titubear.

Exercício 1: Decodificando uma Instrução Campo a Campo

Nível Básico

Eu quero começar pela peça mais elementar da ISA: uma única palavra de dezesseis bits parada na memória de programa do PIC18F4550. Vimos no material que toda instrução do PIC18 tem comprimento fixo, repartida em uma região de opcode e uma região de operandos, e que é justamente essa partição rígida que mantém o decodificador simples e o pipeline regular. Agora você vai percorrer essa partição na direção inversa: em vez de partir do mnemônico, parte dos bits.

Considere a instrução addwf f, dadd W and file — que estudamos como exemplo canônico de anatomia. No montador pic-as você escreve apenas os operandos f e d, porque o Access Bank é assumido por padrão; ainda assim, a palavra de máquina carrega o seletor de banco. Lembre que ela ocupa uma palavra única de dezesseis bits, com seis bits de opcode (0010 01), um bit de destino d, um bit de seletor de banco a e oito bits de endereço do File Register f. Lembre também do significado de cada bit de controle: d = 0 manda o resultado para W e d = 1 manda o resultado de volta para o próprio f; a = 0 ignora o BSR e usa o Access Bank, enquanto a = 1 consulta o BSR.

Este exercício parece trivial até você tentar a primeira vez — eu mesmo, no começo, trocava a ordem dos campos e decodificava o destino errado. A ideia é você pegar lápis e papel, escrever os dezesseis bits em uma fileira, marcar embaixo de cada grupo qual campo ele representa e só então traduzir.

O que peço de você. Primeiro, considere o seguinte conteúdo de uma palavra de programa: opcode 0010 01, bit d = 1, bit a = 0 e o campo de endereço com o valor 0x20; escreva os dezesseis bits completos na ordem correta e diga qual instrução assembly, com seus três operandos textuais, essa palavra representa. Segundo, explique em uma ou duas frases para onde o resultado da soma vai e em qual região da memória de dados o operando f será procurado, justificando a partir dos valores de d e de a. Terceiro, suponha que o mesmo padrão de bits aparecesse, mas com d = 0: descreva o que muda na semântica da execução e por que essa diferença de um único bit é um exemplo da economia que chamei no material de “máquina de um operando e meio”. Quarto, explique por que o decodificador do PIC18 consegue saber que está lendo uma instrução completa antes mesmo de interpretar os operandos, ligando isso à decisão de comprimento fixo da ISA.

O diagrama abaixo mostra o esqueleto de campos que você vai preencher para a palavra dada — é a moldura sobre a qual o seu lápis trabalha:

flowchart LR
    OP["6 bits<br/>opcode<br/>0010 01"] --> D["1 bit<br/>destino d"]
    D --> A["1 bit<br/>banco a"]
    A --> F["8 bits<br/>endereco do<br/>File Register f"]

Você terá terminado quando tiver, escrito por extenso, o mnemônico completo com os três operandos, a direção do resultado, a região de memória consultada e uma frase ligando o comprimento fixo à simplicidade da decodificação.


Exercício 2: Comparando Modos de Endereçamento em Dois Caminhos Equivalentes

Nível Intermediário

Agora vamos sair de uma instrução isolada e raciocinar sobre escolhas de modo de endereçamento, que é onde se decide quanto código você gasta e quantos ciclos cada acesso custa. Vimos que o PIC18F4550 oferece o endereçamento imediato, o por registrador, o direto com o complemento do BSR, o indireto através dos File Select Registers e o quase-indexado do PLUSWn, além do acesso à Flash por tabela. Cada um responde de modo diferente à pergunta “de onde vem o dado”.

Imagine uma tarefa concreta que vai aparecer no seu projeto: somar todos os trinta e dois bytes de um vetor que está na RAM, acumulando o resultado em W, exatamente o núcleo do checksum que discutimos no material. Há dois caminhos para varrer esse vetor. No primeiro caminho, você mantém uma variável contadora-índice na RAM e, a cada iteração, recalcula o endereço do byte atual a partir do endereço-base somado ao índice, lê o byte por esse endereço, soma a W e incrementa o índice. No segundo caminho, você carrega o endereço-base no FSR0 com lfsr 0, buffer e, a cada iteração, usa addwf POSTINC0, w, que lê o byte apontado pelo FSR0, soma a W e auto-incrementa o ponteiro em uma única instrução de um ciclo.

O que peço de você. Primeiro, classifique qual modo de endereçamento domina cada caminho — diga, com as palavras do material, qual é o modo do primeiro caminho e qual é o do segundo, e explique a correspondência do segundo com a notação *p++ da linguagem C. Segundo, conte, para o corpo de uma iteração de cada caminho, quantas instruções e quantos acessos à memória de dados são necessários, e use essa contagem para estimar qual caminho gera código mais compacto e mais rápido. Terceiro, escreva a razão aproximada de ciclos por byte entre os dois caminhos usando a forma r = C_{\text{ind}} / C_{\text{ptr}}, onde C_{\text{ind}} é o custo por byte do caminho por índice e C_{\text{ptr}} o do caminho por ponteiro com pós-incremento, e comente por que essa diferença, que parece pequena em uma iteração, se torna mensurável num laço executado milhares de vezes. Quarto, relacione essa comparação à oposição CISC × RISC: o POSTINC0 funde acesso indireto com auto-incremento numa só instrução — explique por que considero isso uma concessão “CISC-like” dentro de um processador que classifiquei como inequivocamente RISC, e por que esse tipo de concessão é típico de microcontroladores onde cada byte de Flash conta.

O diagrama abaixo confronta os dois caminhos lado a lado para você organizar a contagem de instruções e de acessos:

flowchart TB
    BUF["Vetor de 32 bytes<br/>na RAM"] --> C1["Caminho 1: por indice<br/>recalcula endereco<br/>base + indice a cada byte"]
    BUF --> C2["Caminho 2: por ponteiro<br/>FSR0 + POSTINC0<br/>le e auto-incrementa"]
    C1 --> CMP["Conte instrucoes e acessos<br/>por iteracao em cada caminho"]
    C2 --> CMP
    CMP --> RAZ["Estime a razao de ciclos<br/>por byte entre os dois"]

Você terá terminado quando tiver, com unidades, a contagem de instruções e de acessos por byte de cada caminho, a razão aproximada entre eles, a classificação correta dos modos de endereçamento e um parágrafo justificando por que a fusão do POSTINC0 é uma concessão CISC-like racional num processador RISC.


Exercício 3: Lendo o Assembly do Seu Próprio Código no MPLAB X

Nível Desafiador

Chegou a hora de amarrar toda a teoria à primeira tarefa do Projeto Integrador deste módulo, em que você vai abrir, no MPLAB X, o Disassembly Listing do código C que o seu grupo já gravou no PIC18F4550 do kit ACEPIC PRO V8.2 e analisar sistematicamente o que o compilador XC8 produziu. O objetivo desta tarefa não é otimizar nada ainda — é aprender a ler, classificar e prever, que é o alicerce de qualquer otimização futura. Você vai escolher uma função simples e concreta do seu projeto, algo como escrever um byte no LCD Sunstar 2004A, percorrer um pequeno array de leituras ou testar um bit de um botão lido em PORTB.

Quero que você faça primeiro a previsão no papel e só depois confronte com o que o XC8 realmente gerou — essa ordem é o coração do exercício, porque prever antes de ver é o que treina a intuição. Lembre dos padrões típicos que estudamos: uma atribuição a = b; entre variáveis de oito bits costuma virar movf b, w seguido de movwf a; uma soma a = b + c; tende a movf b, w, addwf c, w, movwf a; um teste if (n == 0) aproveita que o movf n, f atualiza o flag Z e desvia com bnz; e um laço crescente em C é com frequência convertido pelo compilador em contagem decrescente para poder usar decfsz. Lembre também que acessos a variáveis fora do Access Bank fazem o compilador inserir um movlb para ajustar o BSR, adicionando ciclos.

O que peço de você. Primeiro, escolha uma função curta do seu projeto e, antes de abrir o Disassembly Listing, escreva à mão a sequência de instruções assembly que você espera que o XC8 gere para ela, justificando cada escolha de modo de endereçamento esperado. Segundo, descreva o procedimento que você seguirá no MPLAB X para confrontar a sua previsão com a realidade — em que janela está o Disassembly Listing, como a Symbol Table ajuda a descobrir em qual banco cada variável foi alocada, e como você usará isso para explicar eventuais movlb que aparecerem e que você não tinha previsto. Terceiro, para cada instrução do trecho real, classifique o modo de endereçamento de cada operando — imediato, por registrador, direto no Access Bank, direto com BSR, indireto via FSR ou por tabela na Flash — e monte a tabela de correspondência “padrão de código C → modo de endereçamento → instrução gerada” que a terceira tarefa do projeto pede como entregável permanente. Quarto, identifique pelo menos um ponto em que a sua previsão divergiu do código real e explique a causa da divergência, porque investigar onde você errou a previsão ensina mais sobre a ISA do que acertar de primeira.

O diagrama abaixo organiza o fluxo de prever, confrontar e documentar que você vai seguir:

flowchart TB
    SEL["Escolhe uma funcao<br/>curta do projeto"] --> PREV["Preve no papel o assembly<br/>e os modos esperados"]
    PREV --> DIS["Abre o Disassembly Listing<br/>no MPLAB X"]
    DIS --> SYM["Consulta a Symbol Table<br/>bancos e enderecos"]
    SYM --> CLASS["Classifica o modo de<br/>endereçamento de cada operando"]
    CLASS --> TAB["Monta a tabela C -> modo -> instrucao"]
    TAB --> DIV["Identifica e explica<br/>onde a previsao divergiu"]

Você terá terminado quando a sua tabela de correspondência cobrir todas as instruções do trecho escolhido com o modo de endereçamento corretamente classificado, e quando você conseguir explicar, em prosa, a causa de pelo menos uma divergência entre o que previu e o que o XC8 gerou. Se a sua previsão tiver batido inteira, escolha uma função um pouco maior — uma previsão perfeita logo na primeira tentativa quase sempre significa que o trecho era simples demais para revelar como o compilador toma decisões de banco e de fluxo.


Orientações para Resolução

O propósito destes exercícios não é decorar os setenta e cinco mnemônicos do PIC18, mas treinar três movimentos que você repetirá o semestre inteiro: decodificar uma instrução nos seus campos, escolher e reconhecer o modo de endereçamento certo, e prever o assembly que o compilador vai gerar. Antes de qualquer coisa, releia no material do módulo as seções sobre a anatomia da instrução, os modos de endereçamento e o trajeto do C ao assembly, e deixe o capítulo do conjunto de instruções do datasheet do PIC18F4550 aberto ao lado.

Para o exercício básico, escreva os dezesseis bits à mão e marque os campos embaixo, sem pular a etapa do desenho — é o traçado manual que fixa a ordem opcode-destino-banco-endereço. Para o intermediário, faça a contagem de instruções e de acessos por iteração com cuidado e confira se a razão que obteve faz sentido com a ideia de que o ponteiro com pós-incremento economiza trabalho repetido. Para o desafiador, trate a previsão e a leitura do Disassembly Listing como duas peças que precisam encaixar: registre a previsão antes de abrir o MPLAB X, porque comparar com honestidade só é possível se você não tiver olhado o gabarito do compilador antes.

Discuta suas soluções com os colegas do seu grupo e traga suas dúvidas para a aula teórica, onde vamos decodificar instruções juntos e comparar as previsões de assembly de cada grupo com o que o XC8 realmente gerou no kit.