flowchart TD
M3["Módulo 3<br/>ISA e Modos de<br/>Endereçamento"]
M2["Módulo 2<br/>Representação<br/>de Dados"]
A["Visão Geral do<br/>Caminho de Dados"]
B["Unidade Lógica e<br/>Aritmética (ULA)"]
C["Banco de<br/>Registradores"]
D["Barramentos e<br/>Multiplexadores"]
E["Ciclo de Instrução<br/>(Fetch–Decode–Execute–WB)"]
F["Caminho de Dados<br/>Monociclo"]
G["Registrador STATUS<br/>e Flags"]
PI["Projeto Integrador<br/>Otimização de Registradores<br/>Medição de Ciclos"]
M5["Módulo 5<br/>Unidade de Controle"]
M6["Módulo 6<br/>Pipeline"]
M3 --> A
M2 --> B
A --> B
A --> C
A --> D
B --> G
C --> D
D --> E
E --> F
G --> E
F --> PI
F -.->|"próximo módulo"| M5
M5 -.->|"dois módulos"| M6
Módulo 4: Unidade Central de Processamento — O Caminho de Dados
Seja bem-vindo ao quarto módulo! Até aqui, você aprendeu o que são arquiteturas de computadores, como os dados são representados em bits, e quais instruções o PIC18F4550 consegue executar. Agora chegou o momento mais esperado: abrir a CPU por dentro. Neste módulo você vai descobrir exatamente como os componentes físicos do processador trabalham em conjunto para executar cada instrução que você escreveu nos módulos anteriores. Se você já se perguntou “mas o que acontece de verdade lá dentro quando o processador soma dois números?”, a resposta começa aqui.
Por Que Entender o Caminho de Dados Muda Tudo
Imagine que você está depurando um programa no MPLAB X e, ao executar passo a passo, percebe que uma variável assume um valor inesperado. Você sabe que o compilador gerou a instrução ADDWF e que ela deveria somar dois valores — mas de onde vêm esses valores? Para onde vai o resultado? Por que certos flags de status mudam e outros não? Sem entender o caminho de dados, você está essencialmente trabalhando no escuro.
O caminho de dados (datapath) é o conjunto de componentes físicos do processador responsável por movimentar e transformar informação durante a execução das instruções. Ele é composto pela Unidade Lógica e Aritmética, pelo banco de registradores, pelos barramentos de dados e pelos multiplexadores que selecionam as rotas corretas a cada ciclo de clock. Quando você entende esses componentes e como eles se interconectam, o comportamento do processador deixa de ser misterioso e passa a ser completamente previsível.
Além do aspecto prático, este módulo representa uma virada conceitual importante no seu aprendizado: você passará a enxergar o processador não como uma “caixa preta” mágica, mas como um sistema de engenharia com partes bem definidas, cada uma com função específica. Essa perspectiva é exatamente a que engenheiros e arquitetos de sistemas utilizam para projetar, otimizar e depurar processadores reais.
No Projeto Integrador, o conhecimento deste módulo será aplicado de forma imediata: você irá analisar o uso dos registradores no código que seu grupo já desenvolveu, medir o tempo de execução de diferentes classes de instruções e otimizar seções críticas do código para aproveitar melhor o caminho de dados do PIC18F4550. Ao final da sessão de tutoria, você terá uma compreensão profunda de como cada linha de código impacta os componentes físicos do seu microcontrolador.
O diagrama abaixo mostra como os temas deste módulo se relacionam entre si e com os módulos anteriores e posteriores:
O Que É o Caminho de Dados, Afinal?
Para entender o caminho de dados, parta de uma analogia simples. Imagine uma fábrica que produz peças metálicas. Nessa fábrica há máquinas (que realizam operações como cortar, dobrar e soldar), esteiras rolantes (que transportam as peças entre as máquinas) e depósitos temporários onde as peças aguardam enquanto a próxima etapa está ocupada. O caminho de dados de um processador funciona de forma análoga: a ULA é a máquina que transforma os dados, os registradores são os depósitos de alta velocidade onde os dados ficam enquanto são processados, e os barramentos são as esteiras que transportam os bits de um componente para outro.
A diferença crucial em relação a uma fábrica real é a velocidade: no PIC18F4550 rodando a 48 MHz com seu pipeline de dois estágios, a maioria das instruções conclui em apenas um ciclo de máquina — quatro ciclos de clock, ou seja, aproximadamente 83 nanossegundos. Em processadores modernos, essa velocidade é centenas de vezes maior. Mas a estrutura fundamental — operação, armazenamento e transporte — é a mesma.
Definição: Caminho de Dados (Datapath)
O caminho de dados (datapath) é o conjunto de componentes de hardware responsáveis pela movimentação e processamento de dados dentro do processador durante a execução de instruções. Ele inclui:
- A Unidade Lógica e Aritmética (ULA), que executa operações computacionais
- O banco de registradores, que fornece armazenamento temporário de alta velocidade
- Os barramentos internos, que transportam dados entre componentes
- Os multiplexadores, que selecionam qual fonte de dado é conectada a qual destino em cada ciclo
- Os registradores de controle como o Program Counter (PC) e o Stack Pointer (SP)
O caminho de dados é complementado pela unidade de controle, que você estudará no próximo módulo. Enquanto o caminho de dados “faz o trabalho”, a unidade de controle “diz o que fazer”.
Uma distinção importante que você já encontrou no Módulo 1 torna-se muito concreta agora: a diferença entre arquitetura e organização. A ISA (que você estudou no Módulo 3) define o que o processador faz na perspectiva do programador — quais registradores existem, quais instruções estão disponíveis, como os operandos são especificados. O caminho de dados é a organização — a implementação física concreta que realiza o que a ISA promete. Dois processadores com a mesma ISA podem ter caminhos de dados radicalmente diferentes, com desempenhos igualmente diferentes.
A Unidade Lógica e Aritmética: O Coração Computacional
A Unidade Lógica e Aritmética — mais conhecida pela sigla ULA, ou ALU em inglês (Arithmetic Logic Unit) — é o componente do processador responsável por executar todas as operações computacionais. Quando o PIC18F4550 soma dois bytes, aplica uma máscara AND a um registrador, ou verifica se um valor é zero, é a ULA que realiza o trabalho.
Para compreender como a ULA funciona, é preciso dar um passo atrás e entender que ela é construída a partir de componentes ainda mais elementares: as portas lógicas. Você provavelmente já encontrou portas AND, OR e NOT em disciplinas anteriores. A ULA nada mais é do que uma coleção muito bem organizada dessas portas, arranjadas para realizar operações úteis sobre palavras de bits.
Das Portas Lógicas às Operações
Comece pelo caso mais simples: a operação AND bit a bit entre dois registradores de 8 bits. Para realizá-la, a ULA precisa apenas de 8 portas AND independentes, cada uma recebendo um bit correspondente dos dois operandos e produzindo um bit do resultado. Isso é o que chamamos de circuito combinacional: a saída depende apenas dos valores atuais das entradas, sem nenhuma memória do passado.
flowchart LR
subgraph ENTRADA["Operandos (8 bits cada)"]
A["A = 10110101"]
B["B = 00001111"]
end
subgraph AND8["ULA: AND de 8 bits"]
G0["AND"]
G1["AND"]
G2["AND"]
G3["AND"]
G4["AND"]
G5["AND"]
G6["AND"]
G7["AND"]
end
subgraph SAIDA["Resultado"]
R["R = 00000101"]
end
A --> AND8
B --> AND8
AND8 --> SAIDA
A operação OR de 8 bits é igualmente simples: troca-se as 8 portas AND por 8 portas OR. A operação XOR substitui por portas XOR. A operação NOT (complemento) usa 8 portas inversoras. Todas essas operações lógicas têm em comum o fato de serem executadas em paralelo — todos os 8 bits são processados simultaneamente, não um por vez.
A operação aritmética de adição é mais interessante porque introduz o conceito de carry (transporte). Quando você soma dois bits que resultam em 1 + 1 = 10 em binário, o resultado tem dois bits: o 0 fica na posição atual e o 1 “transporta” para a posição seguinte. Isso é exatamente o que acontece quando você soma números de múltiplos dígitos à mão: se a soma de uma coluna ultrapassa 9, você anota o excesso e “vai um” para a próxima coluna.
Exemplo: O Somador Completo de 1 Bit
O somador completo (full adder) é o bloco fundamental da adição binária. Ele recebe três entradas — o bit A, o bit B e o carry de entrada (Cin) — e produz duas saídas: a soma S e o carry de saída (Cout).
A tabela verdade completa é:
| A | B | Cin | S | Cout |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 1 | 0 |
| 0 | 1 | 0 | 1 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 |
| 1 | 1 | 1 | 1 | 1 |
As equações lógicas derivadas dessa tabela são:
S = A \oplus B \oplus C_{in}
C_{out} = (A \cdot B) + (B \cdot C_{in}) + (A \cdot C_{in})
Onde \oplus representa a operação XOR, \cdot representa AND e + representa OR.
Para construir um somador de 8 bits, você encadeia 8 somadores completos em série: o carry de saída do bit 0 torna-se o carry de entrada do bit 1, e assim por diante. Essa configuração é chamada de somador ripple-carry (carry que se propaga em ondas). O nome é muito descritivo: o carry “ondula” da posição menos significativa até a mais significativa, e o processador precisa esperar que essa propagação se complete antes de ter o resultado correto.
flowchart LR
A0["A[0]"] --> FA0["Full Adder<br/>Bit 0"]
B0["B[0]"] --> FA0
C0["Cin=0"] --> FA0
FA0 -->|"S[0]"| R0["R[0]"]
FA0 -->|"Cout"| FA1["Full Adder<br/>Bit 1"]
A1["A[1]"] --> FA1
B1["B[1]"] --> FA1
FA1 -->|"S[1]"| R1["R[1]"]
FA1 -->|"Cout"| FA2["Full Adder<br/>Bit 2"]
A2["A[2]"] --> FA2
B2["B[2]"] --> FA2
FA2 -->|"S[2]"| R2["R[2]"]
FA2 -->|"Cout..."| FAN["...até bit 7"]
FAN -->|"Cout final"| CF["Carry Out<br/>(Flag C)"]
O carry final que sai do somador do bit 7 é exatamente o flag Carry (C) que você vê no registrador STATUS do PIC18F4550! Quando a soma de dois bytes ultrapassa 255 (ou seja, não cabe em 8 bits), esse carry final é 1, e o flag C é setado para avisar o programador que houve um transbordamento sem sinal.
A ULA Completa: Múltiplas Operações, Uma Saída
Uma ULA real não faz apenas adição — ela precisa realizar um menu de operações: adição, subtração, AND, OR, XOR, NOT, deslocamentos. O truque para implementar todas essas operações em um único circuito é simples mas elegante: calcule todas elas em paralelo e use um multiplexador para selecionar qual resultado aparece na saída.
flowchart TD
subgraph ENTRADAS["Entradas"]
A["Operando A<br/>(8 bits)"]
B["Operando B<br/>(8 bits)"]
SEL["Sinais de Controle<br/>da ULA (ALUControl)"]
end
subgraph CIRCUITOS["Circuitos Internos (operam em paralelo)"]
ADD["Somador<br/>A + B"]
SUB["Subtrator<br/>A - B"]
AND_C["AND<br/>A AND B"]
OR_C["OR<br/>A OR B"]
XOR_C["XOR<br/>A XOR B"]
NOT_C["NOT<br/>NOT A"]
SHL["Shift Left<br/>A << 1"]
SHR["Shift Right<br/>A >> 1"]
end
MUX["Multiplexador<br/>(selecionado por ALUControl)"]
subgraph SAIDAS["Saídas"]
RESULT["Resultado<br/>(8 bits)"]
FLAGS["Flags de Status<br/>(Z, C, DC, OV, N)"]
end
A --> ADD
A --> SUB
A --> AND_C
A --> OR_C
A --> XOR_C
A --> NOT_C
A --> SHL
A --> SHR
B --> ADD
B --> SUB
B --> AND_C
B --> OR_C
B --> XOR_C
SEL --> MUX
ADD --> MUX
SUB --> MUX
AND_C --> MUX
OR_C --> MUX
XOR_C --> MUX
NOT_C --> MUX
SHL --> MUX
SHR --> MUX
MUX --> RESULT
MUX --> FLAGS
Os sinais de controle da ULA (ALUControl) são uma palavra de bits que seleciona qual operação deve aparecer na saída do multiplexador. Esses sinais são gerados pela unidade de controle — que você estudará no próximo módulo — baseada no opcode da instrução que está sendo executada. Quando o opcode é ADDWF, os sinais de controle dizem ao multiplexador “selecione a saída do somador”. Quando é ANDWF, dizem “selecione a saída do circuito AND”.
A ULA do PIC18F4550
O PIC18F4550 possui uma ULA de 8 bits, o que significa que ela opera sobre palavras de um byte. Isso está diretamente alinhado com a natureza do microcontrolador: é um dispositivo de 8 bits projetado para aplicações embarcadas onde a eficiência energética e a simplicidade são mais importantes do que a capacidade de processar números grandes em uma única instrução.
Operações Suportadas pela ULA do PIC18F4550
As operações que a ULA do PIC18F4550 pode executar são dividas em quatro categorias:
Operações Aritméticas: adição (ADDWF, ADDLW), subtração (SUBWF, SUBLW), incremento (INCF) e decremento (DECF). A subtração é implementada usando complemento a dois: subtrair B de A é equivalente a adicionar A ao complemento de dois de B, ou seja, A + (\sim B + 1).
Operações Lógicas: AND bit a bit (ANDWF, ANDLW), OR bit a bit (IORWF, IORLW), XOR bit a bit (XORWF, XORLW) e complemento (COMF). Cada uma dessas operações afeta os flags Z e N do registrador STATUS.
Operações de Deslocamento: deslocamento à esquerda (RLCF, RLNCF) e à direita (RRCF, RRNCF), tanto com quanto sem o bit de carry. Os deslocamentos têm uso frequente em multiplicações e divisões por potências de 2, bem como em manipulação de bits individuais.
Operações de Transferência: MOVF, MOVWF, MOVLW — transferem dados sem modificá-los. Tecnicamente, mesmo uma transferência passa pela ULA (o dado é “somado com zero” ou simplesmente passado pelo circuito), o que permite atualizar os flags Z e N.
Os Flags de Status: A Memória de Curto Prazo da ULA
Toda vez que a ULA realiza uma operação, ela não produz apenas o resultado: ela também atualiza um conjunto de flags de status no registrador STATUS. Esses flags são como “post-its” que a ULA deixa para o processador, informando características do resultado. Sem os flags, você não poderia fazer desvios condicionais — e sem desvios condicionais, não haveria if, while ou for.
Registrador STATUS: Os Cinco Flags Essenciais
O registrador STATUS do PIC18F4550 mantém cinco flags atualizados pela ULA:
O flag Zero (Z) é setado (torna-se 1) quando o resultado de uma operação é zero. É o flag mais usado para implementar comparações: CPFSEQ compara dois valores subtraindo um do outro e verifica se Z é 1.
O flag Carry (C) é setado quando há carry fora do bit mais significativo em operações aritméticas, ou borrow em subtrações. Em deslocamentos, recebe o bit que “saiu pela borda”.
O flag Digit Carry (DC) é o carry do nibble inferior (bit 3) para o nibble superior (bit 4). É fundamental para aritmética BCD (Binary Coded Decimal), onde cada nibble representa um dígito decimal.
O flag Overflow (OV) é setado quando uma operação em complemento a dois produz resultado que não cabe no intervalo [-128, +127]. Diferente do Carry, que indica overflow sem sinal, OV indica overflow com sinal.
O flag Negative (N) espelha o bit mais significativo do resultado, indicando que o resultado é negativo em representação com sinal (complemento a dois).
flowchart LR
ULA["ULA<br/>(8 bits)"] --> Z["Flag Z<br/>(Zero)"]
ULA --> C["Flag C<br/>(Carry)"]
ULA --> DC["Flag DC<br/>(Digit Carry)"]
ULA --> OV["Flag OV<br/>(Overflow)"]
ULA --> N["Flag N<br/>(Negative)"]
Z --> STATUS["Registrador<br/>STATUS"]
C --> STATUS
DC --> STATUS
OV --> STATUS
N --> STATUS
STATUS --> DEC["Unidade de Controle<br/>(desvios condicionais)"]
A tabela a seguir resume quais flags cada categoria de instrução afeta:
| Instrução | Z | C | DC | OV | N |
|---|---|---|---|---|---|
| ADD, ADDLW | ✓ | ✓ | ✓ | ✓ | ✓ |
| SUB, SUBLW | ✓ | ✓ | ✓ | ✓ | ✓ |
| AND, OR, XOR, COM | ✓ | — | — | — | ✓ |
| INC, DEC | ✓ | — | — | ✓ | ✓ |
| MOV (com STATUS) | ✓ | — | — | — | ✓ |
| RLCF, RRCF | ✓ | ✓ | — | — | ✓ |
| RLNCF, RRNCF | ✓ | — | — | — | ✓ |
Observe a coluna do flag Carry para as instruções AND, OR e XOR: o traço (—) significa que essas instruções não afetam o flag C. Isso faz sentido conceitualmente — operações lógicas não produzem carry, pois operam bit a bit sem propagar transporte. Conhecer esses detalhes é fundamental para escrever código correto que usa flags para tomada de decisões.
O Banco de Registradores: Armazenamento de Alta Velocidade
Se a ULA é o coração computacional do processador, os registradores são o sangue que flui por ele. Um registrador é um elemento de armazenamento de alta velocidade construído diretamente no chip do processador, capaz de ser lido ou escrito em um único ciclo de clock. A diferença de velocidade entre um registrador e a memória RAM externa é de ordem de grandeza: enquanto um registrador responde em nanosegundos, uma leitura da RAM pode levar dezenas ou centenas de nanosegundos.
O motivo dessa diferença é físico: os registradores são construídos com células de SRAM estática diretamente adjacentes à ULA, interligados por trilhas metálicas curtíssimas dentro do chip. A memória RAM externa, ao contrário, precisa ser acessada através de barramentos que percorrem centímetros de trilha no circuito impresso — uma eternidade em termos de velocidade de sinal.
O Registrador de Trabalho W: O Acumulador Central
A arquitetura do PIC18F4550 é conhecida como arquitetura de acumulador, e o registrador W (Working Register, também chamado WREG) é esse acumulador. Na grande maioria das instruções aritméticas e lógicas, W participa como um dos operandos e frequentemente recebe o resultado. Isso simplifica o conjunto de instruções — você não precisa especificar dois registradores fonte, pois um deles é sempre W — mas exige planejamento cuidadoso do programador para evitar “perder” o conteúdo de W antes de utilizá-lo.
Exemplo: O Papel Central do Registrador W
Considere o cálculo simples resultado = a + b + c onde a, b e c são variáveis na RAM. Em assembly para o PIC18F4550, a sequência seria:
Note que W é o ponto de passagem obrigatório: todo valor que entra na ULA passa por W. Se você precisasse calcular d = a + b e e = c + d imediatamente depois, precisaria salvar W antes de recomeçar, pois a segunda conta sobrescreveria o primeiro resultado.
Os Registradores de Propósito Geral (GPRs)
Além do registrador W, o PIC18F4550 possui 4096 bytes de RAM de dados organizados em 16 bancos de 256 bytes cada, dos quais a maior parte são registradores de propósito geral (General Purpose Registers, GPRs). Esses registradores armazenam variáveis locais, parâmetros de funções, resultados intermediários e qualquer dado que o programa precisa manter enquanto trabalha.
A organização em bancos surge de uma limitação de endereçamento: como o campo de endereço em muitas instruções tem apenas 8 bits, ele pode endereçar diretamente apenas 256 posições (0x00 a 0xFF). Para acessar qualquer posição além dessas 256, é necessário usar o BSR (Bank Select Register), que indica qual dos 16 bancos está atualmente ativo.
flowchart TB
BSR["BSR<br/>(Bank Select Register)<br/>4 bits → 16 bancos"]
subgraph BANCOS["Espaço de Endereçamento RAM (4096 bytes)"]
B0["Banco 0<br/>0x000 - 0x0FF<br/>256 bytes (GPR)"]
B1["Banco 1<br/>0x100 - 0x1FF<br/>256 bytes (GPR)"]
B2["Banco 2<br/>0x200 - 0x2FF<br/>256 bytes (GPR)"]
BDOTS["... (Bancos 3-14)"]
B15["Banco 15<br/>0xF00 - 0xFFF<br/>SFRs e GPRs"]
end
ACCESS["Access Bank<br/>(acesso direto sem BSR):<br/>0x000-0x05F (GPR Banco 0)<br/>0xF60-0xFFF (SFRs Banco 15)"]
BSR --> BANCOS
BANCOS --> ACCESS
O Access Bank é um mecanismo muito conveniente: ele permite acessar os 96 bytes inferiores do Banco 0 (endereços 0x000–0x05F) e os 160 bytes superiores do Banco 15 (endereços 0xF60–0xFFF, onde ficam os SFRs mais importantes) sem precisar configurar o BSR. Muitas instruções têm um bit a que seleciona entre “usar BSR” e “usar Access Bank”. O compilador XC8 coloca automaticamente variáveis frequentemente usadas no Access Bank para evitar o overhead de chavear bancos.
Os Registradores de Função Especial (SFRs)
O Banco 15 contém os Registradores de Função Especial (Special Function Registers, SFRs), e estes são de importância central no seu Projeto Integrador. Cada SFR controla ou monitora um aspecto específico do hardware do PIC18F4550. Ao escrever um valor em um SFR, você está literalmente configurando transistores no chip — ligando ou desligando partes do hardware.
SFRs Mais Importantes para o Projeto Integrador
Os SFRs dividem-se em grupos funcionais que você encontrará repetidamente:
Controle de direção dos pinos I/O: Os registradores TRISA, TRISB, TRISC, TRISD e TRISE controlam se cada pino dos ports correspondentes é entrada (bit = 1) ou saída (bit = 0). O mnemônico TRIS vem de tri-state, o estado de alta impedância de uma entrada digital.
Leitura e escrita dos ports I/O: PORTA, PORTB, PORTC, PORTD e PORTE permitem ler o estado lógico dos pinos (quando configurados como entrada) ou escrever valores nos pinos (quando configurados como saída). LATA, LATB, LATC, LATD e LATE são os registradores de latch — eles representam o valor que o processador quer colocar nos pinos, independente do que está fisicamente no pino.
Controle do processador: STATUS guarda os flags da ULA. WREG é o registrador W. PCL, PCLATH e PCLATU compõem o Program Counter de 21 bits. STKPTR é o Stack Pointer (ponteiro da pilha de hardware do PIC18).
Controle de periféricos: ADCON0, ADCON1, ADCON2 controlam o conversor A/D. T0CON, T1CON, T2CON controlam os três timers. RCSTA, TXSTA controlam a UART serial. SSPCON1, SSPCON2 controlam o módulo SSP (SPI e I²C). Você explorará estes em profundidade nos Módulos 11–14.
flowchart LR
CPU["Núcleo CPU<br/>(caminho de dados)"]
CPU <-->|"escrita"| TRIS["TRISx<br/>(direção do pino)"]
CPU <-->|"leitura/escrita"| PORT["PORTx / LATx<br/>(valor do pino)"]
CPU <-->|"escrita"| ADCON["ADCONx<br/>(conversor A/D)"]
CPU <-->|"escrita/leitura"| TCON["TxCON<br/>(timers)"]
TRIS --> PIN["Pinos físicos<br/>do PIC18F4550"]
PORT --> PIN
ADCON --> ADC["Circuito<br/>A/D interno"]
TCON --> TIMER["Circuitos<br/>de timer"]
Registradores de Controle do Fluxo: PC, SP e FSRs
Além dos GPRs e SFRs, existem registradores fundamentais para o controle do fluxo de execução:
O Program Counter (PC) é um registrador de 21 bits que sempre aponta para o endereço da próxima instrução a ser executada na memória de programa Flash. A cada instrução buscada, o PC é automaticamente incrementado em 2 (pois cada instrução ocupa 2 bytes de endereço, já que a memória de programa é endereçada em palavras de 16 bits). Quando uma instrução de desvio (GOTO, BRA, CALL) é executada, a ULA de cálculo de endereço carrega um novo valor no PC, redirecionando o fluxo do programa.
O Stack Pointer (SP) gerencia a pilha de hardware do PIC18F4550 — uma estrutura de dados LIFO (Last In, First Out) usada para salvar e restaurar o PC durante chamadas e retornos de sub-rotinas. O PIC18F4550 tem uma pilha de hardware de 31 níveis, o que significa que você pode ter no máximo 31 chamadas aninhadas sem retornar. Esta limitação é um aspecto arquitetural importante que influencia como você escreve código para este microcontrolador.
Os FSRs (File Select Registers) — FSR0, FSR1 e FSR2, cada um de 12 bits — são ponteiros de memória usados para endereçamento indireto. Quando você usa as instruções INDF0, INDF1 ou INDF2, o processador lê ou escreve na posição de memória apontada pelo FSR correspondente. Esse mecanismo é equivalente ao uso de ponteiros em C e é fundamental para manipular arrays e estruturas de dados em assembly.
Barramentos: As Autoestradas dos Dados
Imagine tentar conectar todos os componentes do processador diretamente uns aos outros: a ULA precisaria de fios indo para cada registrador, cada registrador precisaria de fios indo para a ULA e para os outros registradores, e assim por diante. Com apenas 16 registradores de 8 bits, isso já produziria centenas de conexões. A solução elegante adotada pelos projetistas de processadores é o barramento.
Um barramento (bus) é um conjunto de fios compartilhados por múltiplos componentes. Em vez de cada componente ter sua própria conexão privada com todos os outros, todos se conectam ao barramento. A cada momento, apenas um componente “fala” no barramento (coloca dados nele) enquanto um ou mais componentes “ouvem” (leem os dados). O controle de quem fala e quem ouve é responsabilidade da unidade de controle.
Tipos de Barramento no Caminho de Dados
No contexto do caminho de dados interno, os barramentos mais importantes são o barramento de dados, o barramento de endereços e o barramento de controle.
O barramento de dados interno transporta os operandos entre registradores e a ULA, bem como o resultado da ULA de volta para os registradores. No PIC18F4550, este barramento tem 8 bits de largura — o que significa que exatamente 8 bits são transferidos em paralelo a cada ciclo. Se você precisar processar um número de 16 bits (um int em C para PIC), duas transferências de 8 bits serão necessárias.
O barramento de endereços é usado para especificar qual posição de memória (RAM ou Flash) está sendo acessada. Para a memória de dados RAM, o PIC18F4550 usa um barramento de endereços de 12 bits (pois 2¹² = 4096, o tamanho da RAM). Para a memória de programa Flash, o barramento de endereços tem 21 bits (pois o PIC18F4550 tem até 2 MB de espaço endereçável de programa, embora o chip tenha apenas 32KB de Flash instalados).
flowchart TB
subgraph CPU["Caminho de Dados Interno"]
direction TB
WREG["Registrador W"]
ALU["ULA (8 bits)"]
subgraph REG["Banco de Registradores"]
GPR["GPRs"]
SFR["SFRs"]
end
PC["Program Counter (21 bits)"]
FSR["FSR0/1/2 (12 bits)"]
WBUS["Barramento W (8 bits)"]
DBUS["Barramento de Dados (8 bits)"]
ABUS_D["Barramento de Endereços RAM (12 bits)"]
ABUS_P["Barramento de Endereços Flash (21 bits)"]
end
DRAM["RAM de Dados<br/>(4 KB)"]
PFLASH["Memória de Programa<br/>(Flash 32 KB)"]
WREG <--> WBUS
WBUS --> ALU
REG <--> DBUS
ALU --> DBUS
DBUS --> WREG
REG --> ABUS_D
FSR --> ABUS_D
ABUS_D --> DRAM
DRAM <--> DBUS
PC --> ABUS_P
ABUS_P --> PFLASH
PFLASH --> ALU
Multiplexadores: Os Controladores de Tráfego
Um barramento compartilhado introduz um problema: quando múltiplas fontes poderiam falar ao mesmo tempo, como o processador decide qual delas tem a vez? A resposta é o multiplexador (mux).
Um multiplexador é um circuito digital que recebe múltiplas entradas e uma entrada de seleção (select), e conecta exatamente uma das entradas à saída, com base no valor da seleção. É análogo a um interruptor de trilhos ferroviários: o trem pode vir de vários ramais, mas o interruptor determina por qual trilho ele seguirá.
Definição: Multiplexador (MUX)
Um multiplexador de 2^n entradas é um circuito combinacional com 2^n entradas de dados, n entradas de seleção (select) e uma saída. A saída reproduz o valor da entrada selecionada pelos bits de seleção.
Para um MUX 4:1 (4 entradas, 2 bits de seleção):
| S1 | S0 | Saída |
|---|---|---|
| 0 | 0 | D0 |
| 0 | 1 | D1 |
| 1 | 0 | D2 |
| 1 | 1 | D3 |
No caminho de dados, o sinal de seleção é gerado pela unidade de controle com base no opcode da instrução sendo executada. Isso é o que permite que a mesma ULA realize operações diferentes para instruções diferentes.
No caminho de dados do PIC18F4550, os multiplexadores aparecem em vários pontos críticos. O multiplexador de entrada da ULA seleciona se o segundo operando vem do registrador especificado pela instrução, de um literal embutido na instrução (endereçamento imediato), ou do FSR para operações de endereçamento indireto. O multiplexador de destino seleciona se o resultado vai para o registrador W ou para o registrador de arquivo especificado — exatamente o bit d das instruções como ADDWF f, d.
O Ciclo de Instrução: A Dança dos Componentes
Agora que você conhece cada componente do caminho de dados, está pronto para entender como eles trabalham juntos durante a execução de uma instrução. Este processo tem um nome clássico: o ciclo de instrução (instruction cycle).
O ciclo de instrução é dividido em fases, cada uma utilizando partes específicas do caminho de dados. As fases fundamentais são: busca (fetch), decodificação (decode), execução (execute) e escrita de resultado (write-back). Em alguns tipos de instrução, há também uma fase de acesso à memória (memory access) entre execução e write-back.
Fase 1: Busca (Fetch)
A fase de busca começa com o Program Counter apontando para o endereço da próxima instrução na memória Flash. O endereço contido no PC é colocado no barramento de endereços, a memória Flash é ativada e produz os 16 bits da instrução no barramento de dados. Esses 16 bits são carregados no Instruction Register (IR) — um registrador interno que mantém a instrução atual enquanto ela é processada.
Após a busca, o PC é incrementado automaticamente em 2 unidades (pois a Flash do PIC18 é endereçada em palavras de 16 bits e cada instrução ocupa uma palavra de 2 bytes no espaço de endereços), passando a apontar para a próxima instrução. Isso acontece em paralelo com as fases seguintes — este é o mecanismo do pipeline de dois estágios do PIC18F4550 que você estudou rapidamente no Módulo 1.
sequenceDiagram
participant PC as Program Counter
participant FLASH as Memória Flash
participant IR as Instruction Register
participant DEC as Decodificador
PC->>FLASH: Endereço da instrução atual
FLASH->>IR: Instrução de 16 bits
PC->>PC: PC ← PC + 2 (automático)
IR->>DEC: Envia instrução para decodificação
Note over PC: PC já aponta para a próxima instrução
Fase 2: Decodificação (Decode)
Com a instrução no Instruction Register, a unidade de decodificação — que faz parte da unidade de controle — analisa os bits do opcode e determina:
- Qual operação a ULA deve executar (gera os sinais
ALUControl) - Quais registradores são os operandos fonte (gera os sinais de seleção dos multiplexadores de entrada)
- Qual é o destino do resultado (gera os sinais de seleção do multiplexador de destino)
- Se há acesso à memória e em que direção (leitura ou escrita)
- Se o Program Counter precisa ser atualizado para um desvio
Em processadores simples, a decodificação é essencialmente uma grande tabela de verdade: o opcode entra como entrada e um conjunto de sinais de controle sai como saída. Para o PIC18F4550, com 75 instruções únicas, o decodificador precisa cobrir todos esses casos.
Fase 3: Execução (Execute)
Esta é a fase onde o “trabalho real” acontece. Com base nos sinais gerados na decodificação, os multiplexadores selecionam os operandos corretos, a ULA executa a operação especificada, e o resultado é disponibilizado na saída da ULA. Simultaneamente, os flags de status são atualizados.
Para instruções que não envolvem a ULA — como GOTO ou CALL — a “execução” consiste em calcular o novo valor do Program Counter. Para GOTO, o endereço de destino está codificado na instrução. Para CALL, o endereço de destino está na instrução e o endereço de retorno (PC+2) é salvo na pilha de hardware.
Fase 4: Acesso à Memória (Memory Access)
Esta fase é opcional e só ocorre para instruções que precisam ler ou escrever na RAM. Instruções como MOVF f, W precisam ler o conteúdo do endereço f na RAM. Instruções como MOVWF f precisam escrever o conteúdo de W no endereço f da RAM.
O acesso à memória RAM é mais lento que o acesso a registradores internos, mas ainda ocorre dentro de um único ciclo de máquina no PIC18F4550. Isso é possível porque a RAM interna do PIC18 é do tipo SRAM, construída no mesmo chip e acessível em um ciclo de clock.
Fase 5: Escrita de Resultado (Write-Back)
Na última fase, o resultado disponível na saída da ULA (ou lido da RAM) é escrito no registrador de destino. O multiplexador de destino seleciona, com base no bit d da instrução ou no tipo da instrução, se o resultado vai para o registrador W ou para o File Register especificado.
O diagrama completo do ciclo de instrução para a instrução ADDWF f, d ilustra como todas as fases se integram:
sequenceDiagram
participant PC as Program Counter
participant FLASH as Flash
participant IR as Instruction Register
participant DEC as Decodificador
participant REG as Banco de Registradores
participant MUX as Multiplexadores
participant ALU as ULA
participant STATUS as Registrador STATUS
participant WREG as Registrador W
Note over PC,WREG: Fase 1: Busca (Fetch)
PC->>FLASH: Endereço (ex: 0x0010)
FLASH->>IR: Instrução ADDWF (16 bits)
PC->>PC: PC ← 0x0012
Note over PC,WREG: Fase 2: Decodificação (Decode)
IR->>DEC: Opcode = ADDWF
DEC->>MUX: ALUControl = ADD
DEC->>MUX: Src1 = WREG, Src2 = f
DEC->>MUX: Dest = (d=0: WREG) ou (d=1: f)
Note over PC,WREG: Fase 3: Execução (Execute)
WREG->>MUX: Operando A
REG->>MUX: Operando B (endereço f)
MUX->>ALU: A e B selecionados
ALU->>ALU: Calcula A + B
Note over PC,WREG: Fase 4: Acesso à Memória
REG->>MUX: Lê registrador f da RAM
Note over PC,WREG: Fase 5: Write-Back
ALU->>STATUS: Atualiza Z, C, DC, OV, N
ALU->>MUX: Resultado disponível
MUX->>WREG: Se d=0: salva em W
MUX->>REG: Se d=1: salva em f
O Pipeline de Dois Estágios do PIC18F4550
O PIC18F4550 implementa um pipeline simples de dois estágios: enquanto uma instrução está na fase de execução (estágios 2-5), a instrução seguinte já está sendo buscada da Flash (fase 1). Isso significa que, em condições ideais (sem desvios), o processador completa uma instrução a cada ciclo de máquina (4 ciclos de clock), mesmo que cada instrução individualmente passe por múltiplas fases.
gantt
title Pipeline de 2 Estágios do PIC18F4550
dateFormat X
axisFormat %s
section Instrução 1
Busca (Fetch) :done, 0, 1
Execução :done, 1, 2
section Instrução 2
Busca (Fetch) :done, 1, 2
Execução :done, 2, 3
section Instrução 3
Busca (Fetch) :done, 2, 3
Execução :done, 3, 4
section Instrução 4 (desvio tomado)
Busca (Fetch) :done, 3, 4
Execução :done, 4, 5
section Instrução 5 (descartada!)
Busca (inválida) :crit, 4, 5
Execução (nova) :done, 5, 6
O problema aparece com desvios: quando uma instrução de desvio (GOTO, BRA, CALL) é executada, a instrução que já estava sendo buscada em paralelo pode ser do endereço errado. Quando o desvio é tomado, essa instrução é descartada e uma nova busca é iniciada no endereço correto de destino. Isso resulta em um ciclo de penalidade para desvios tomados — uma realidade que você perceberá ao medir tempos de execução no Projeto Integrador.
O Caminho de Dados Monociclo: Um Modelo Para Entender
Para consolidar todos os conceitos que você estudou neste módulo, é muito útil construir mentalmente (e no papel!) um caminho de dados monociclo — um modelo simplificado onde cada instrução completa em exatamente um ciclo de clock. Este modelo não é o que o PIC18F4550 usa internamente (o PIC18 tem pipeline e múltiplos ciclos de clock por instrução de máquina), mas é o ponto de partida pedagógico clássico para compreender como todos os componentes se encaixam.
A ideia central do caminho de dados monociclo é: todos os componentes necessários para executar qualquer instrução devem estar disponíveis simultaneamente, pois tudo acontece em um único ciclo de clock. Isso implica que alguns componentes precisam estar duplicados — por exemplo, memória de instrução e memória de dados devem ser separadas (não podemos ler uma instrução e ler um dado da mesma memória no mesmo ciclo).
Por que estudar o modelo monociclo se o PIC18 não usa esse modelo? Porque o caminho de dados monociclo revela a estrutura essencial que todo processador precisa ter: unidade de busca, decodificador, ULA, banco de registradores, interfaces de memória e multiplexadores de roteamento. Quando você entender o monociclo, entenderá por que o pipeline existe e quais problemas ele resolve — e isso será o tema central do Módulo 6.
Componentes do Caminho de Dados Monociclo
Vamos construir o caminho de dados monociclo para suportar três tipos representativos de instruções do PIC18F4550:
Tipo 1 — Instruções Registrador-Registrador (ex: ADDWF f, d): buscam dois operandos dos registradores, realizam operação na ULA, e escrevem o resultado de volta em um registrador.
Tipo 2 — Instruções de Desvio Incondicional (ex: GOTO): calculam um novo valor para o PC a partir de um campo de endereço na instrução.
Tipo 3 — Instruções de Carga com Imediato (ex: MOVLW k): carregam um valor literal (constante) da instrução diretamente no registrador W.
flowchart TB
subgraph FETCH["Estágio de Busca"]
PC2["Program Counter"]
IMEM["Memória de<br/>Instruções (Flash)"]
ADDER_PC["Somador<br/>PC + 2"]
PC2 -->|"endereço"| IMEM
PC2 --> ADDER_PC
ADDER_PC -->|"PC+2"| MUX_PC["MUX<br/>PC+2 / Desvio"]
end
subgraph DECODE["Estágio de Decodificação"]
IR2["Instruction Register"]
CTRL["Unidade de<br/>Controle"]
IMEM -->|"instrução 16 bits"| IR2
IR2 -->|"opcode"| CTRL
end
subgraph EXECUTE["Estágio de Execução"]
REGFILE["Banco de<br/>Registradores<br/>(W, GPRs)"]
MUX_A["MUX<br/>Operando A"]
MUX_B["MUX<br/>Operando B<br/>(reg ou imediato)"]
ALU2["ULA<br/>8 bits"]
SIGNEXT["Extensão de<br/>Sinal/Zero<br/>(para imediatos)"]
IR2 -->|"endereço f"| REGFILE
IR2 -->|"literal k"| SIGNEXT
REGFILE -->|"W"| MUX_A
REGFILE -->|"f"| MUX_B
SIGNEXT -->|"k estendido"| MUX_B
MUX_A --> ALU2
MUX_B --> ALU2
end
subgraph WRITEBACK["Write-Back"]
MUX_DEST["MUX<br/>Destino<br/>(W ou f)"]
ALU2 -->|"resultado"| MUX_DEST
MUX_DEST -->|"escreve W"| REGFILE
MUX_DEST -->|"escreve f"| REGFILE
end
CTRL -->|"ALUControl"| ALU2
CTRL -->|"SelA, SelB"| MUX_A
CTRL -->|"SelA, SelB"| MUX_B
CTRL -->|"DestSel"| MUX_DEST
CTRL -->|"PCSel"| MUX_PC
MUX_PC -->|"novo PC"| PC2
ALU2 -->|"endereço desvio"| MUX_PC
Como Cada Tipo de Instrução Atravessa o Caminho
Para uma instrução Tipo 1 como ADDWF f, W (d=0, resultado em W):
A unidade de controle gera: ALUControl = ADD, SelB = registrador (não imediato), DestSel = W. O banco de registradores fornece W para a entrada A da ULA e o conteúdo do endereço f para a entrada B. A ULA realiza a adição, o resultado é roteado pelo MUX de destino para escrever de volta em W, e os flags são atualizados no STATUS.
Para uma instrução Tipo 3 como MOVLW k (carrega literal em W):
A unidade de controle gera: ALUControl = PASS (passa o operando B direto para a saída, sem modificação), SelB = imediato (usa o literal k da instrução). O valor k de 8 bits extraído da instrução é conectado à entrada B da ULA. A ULA “executa” uma operação de passagem e o resultado (que é simplesmente k) é escrito em W. Note que esta operação não afeta os flags de status — verifique na tabela de flags que vimos anteriormente.
Para uma instrução Tipo 2 como GOTO n (desvio incondicional):
A unidade de controle gera: PCSel = desvio. O campo de 20 bits da instrução que contém o endereço de destino n é conectado ao MUX do PC. Em vez de PC+2, o novo valor do PC é o endereço n, e na próxima busca o processador começará a executar a partir desse novo endereço.
Colocando em Prática: Análise do Caminho de Dados no PIC18F4550
Agora que você tem o modelo conceitual claro, vamos conectá-lo com o hardware real que você usa no Projeto Integrador.
Analisando o Registrador STATUS em Código Real
Considere o seguinte trecho de código C típico de um sistema embarcado:
#include <xc.h>
#include <stdint.h>
/* Verifica se a soma de dois bytes resulta em overflow sem sinal */
void verifica_overflow(uint8_t a, uint8_t b) {
uint8_t soma = a + b;
if (soma < a) {
/* Houve overflow sem sinal! O Carry foi setado */
LATD = 0xFF; /* Acende todos os LEDs */
} else {
LATD = soma; /* Mostra o resultado nos LEDs */
}
}; Parâmetros em W (a) e registrador aux (b)
; Compilado com XC8 -O1
_verifica_overflow:
MOVWF _a, ACCESS ; salva 'a' na RAM
MOVWF _soma_temp ; copia 'a' para calcular
MOVF _b, W, ACCESS ; W ← b
ADDWF _a, W, ACCESS ; W ← a + b (CARRY atualizado!)
MOVWF _soma, ACCESS ; salva soma
; Verifica if (soma < a): equivale a CARRY setado após adição?
; O compilador compara soma e a usando subtração
SUBWF _a, W, ACCESS ; W ← a - soma; se soma > a, CARRY = 0
BNC _else_branch ; desvia se Carry = 0 (sem overflow)
; Ramo then: houve overflow
MOVLW 0xFF
MOVWF LATD, ACCESS ; LATD ← 0xFF
RETURN
_else_branch:
MOVF _soma, W, ACCESS
MOVWF LATD, ACCESS ; LATD ← soma
RETURNA coisa mais interessante neste exemplo é observar que a detecção de overflow (if (soma < a)) é implementada pelo compilador usando uma subtração seguida de verificação do flag Carry. Isso revela algo importante: os flags de status da ULA são a ponte entre a aritmética e o fluxo de controle do programa. Toda instrução condicional em C se traduz, em última instância, em uma operação que atualiza flags e uma instrução de desvio condicional que verifica esses flags.
Medindo o Custo Real das Operações
Um aspecto prático fundamental que você explorará no Projeto Integrador é medir quantos ciclos de máquina cada tipo de operação consome. A tabela a seguir resume o custo das instruções mais comuns do PIC18F4550:
| Categoria de Instrução | Exemplos | Ciclos de Máquina |
|---|---|---|
| Operações registrador ↔︎ ULA | ADDWF, ANDWF, MOVF | 1 |
| Operações com literal | ADDLW, ANDLW, MOVLW | 1 |
| Leitura de registrador para W | MOVF f, W | 1 |
| Escrita de W para registrador | MOVWF f | 1 |
| Desvio não tomado | BRA, BC, BZ (condição falsa) | 1 |
| Desvio tomado (relativo) | BRA (condição verdadeira) | 2 |
| Desvio incondicional longo | GOTO | 2 |
| Chamada de sub-rotina | CALL | 2 |
| Retorno de sub-rotina | RETURN, RETLW | 2 |
| Pulo condicional (skip) | CPFSEQ, DECFSZ (condição falsa) | 1 |
| Pulo condicional (skip) | CPFSEQ, DECFSZ (condição verdad) | 2 |
| Operação sobre tabela | TBLRD, TBLWT | 2 |
O fato de desvios tomados custarem 2 ciclos enquanto desvios não tomados custam apenas 1 tem implicações diretas para otimização de laços. Se você tem um laço for que itera 100 vezes, o desvio de retorno ao início do laço é tomado 99 vezes (custo = 99 × 2 = 198 ciclos) e não tomado 1 vez na saída (custo = 1 ciclo). Esse overhead do pipeline é parte do custo real de qualquer laço em assembly ou C.
Exemplo Prático: Calculando o Tempo de Execução de um Laço
Suponha que você tenha o seguinte laço em assembly, que limpa 32 bytes de RAM:
; FSR0 aponta para o início do buffer
; WREG = 0x00 (preparado antes do laço)
; Contador em registrador aux
MOVLW 0x20 ; 1 ciclo — carrega 32 no contador
MOVWF contador, ACCESS ; 1 ciclo — salva contador
loop:
CLRF INDF0, ACCESS ; 1 ciclo — limpa byte apontado por FSR0
MOVLW 1 ; 1 ciclo — prepara incremento
ADDWF FSR0L, F, ACCESS ; 1 ciclo — incrementa FSR0
DECFSZ contador, F, ACCESS ; 1 ciclo (não pulou) ou 2 ciclos (pulou)
BRA loop ; 2 ciclos (quando tomado) ou 1 (quando não)
; Após o laço: próxima instruçãoCalculando o tempo total:
Para as 32 iterações, o corpo do laço (excluindo o DECFSZ e BRA finais) consome 32 \times 3 = 96 ciclos para as instruções CLRF, MOVLW e ADDWF.
O DECFSZ custa 1 ciclo nas primeiras 31 iterações (não pula) e 2 ciclos na última (pula): 31 \times 1 + 1 \times 2 = 33 ciclos.
O BRA custa 2 ciclos nas primeiras 31 iterações (desvio tomado) e não é executado na última: 31 \times 2 = 62 ciclos.
Mais os 2 ciclos de inicialização antes do laço: 2 ciclos.
Total: 2 + 96 + 33 + 62 = 193 ciclos de máquina.
A 48 MHz com clock de instrução a 12 MHz (cada ciclo de máquina = 4 ciclos de clock), cada ciclo de máquina dura \frac{1}{12 \times 10^6} \approx 83,3 ns.
Tempo total: 193 \times 83,3 \text{ ns} \approx 16,1 \text{ μs}
Otimização Usando o Caminho de Dados
Compreender o caminho de dados revela oportunidades de otimização que não são óbvias olhando apenas para o código C. A regra de ouro é: dados em registradores são sempre mais rápidos que dados na RAM, porque acessar a RAM exige um ciclo de barramento de endereços e um ciclo de dados, enquanto um registrador já está fisicamente conectado à ULA.
No PIC18F4550, o “melhor registrador” é o próprio W: instruções como ADDLW e ANDLW operam diretamente sobre W com um literal, sem precisar nem mesmo acessar a RAM. A segunda melhor opção é o Access Bank — os primeiros 96 bytes do Banco 0 e os SFRs do Banco 15 são acessíveis sem precisar configurar o BSR.
Exemplo: Impacto da Alocação de Variáveis no Desempenho
Considere duas versões de um cálculo simples:
Versão 1 — Variáveis em banco diferente do Access Bank:
Versão 2 — Variáveis no Access Bank:
A versão 2 é 25% mais rápida apenas por posicionar as variáveis no Access Bank. O compilador XC8 faz isso automaticamente para variáveis static e globais declaradas antes de qualquer outra alocação. Para forçar uma variável no Access Bank em código C com XC8, use o atributo __near:
Registradores Especiais do Caminho de Dados: Uma Visão Integrada
Para consolidar sua compreensão, vamos traçar como todos os registradores especiais do PIC18F4550 se encaixam no caminho de dados que você estudou:
flowchart TB
subgraph FLUXO_PROGRAMA["Controle de Fluxo"]
PC_REG["PC (21 bits)<br/>Program Counter"]
SP_REG["STKPTR<br/>Stack Pointer (5 bits)"]
STACK_MEM["Pilha de Hardware<br/>31 níveis × 21 bits"]
PC_REG -->|"salvo em CALL"| STACK_MEM
STACK_MEM -->|"restaurado em RETURN"| PC_REG
SP_REG -->|"controla"| STACK_MEM
end
subgraph DADOS["Caminho de Dados"]
WREG2["WREG<br/>(acumulador)"]
BSR2["BSR<br/>(4 bits)"]
STATUS2["STATUS<br/>Z C DC OV N"]
FSR0_R["FSR0 (12 bits)<br/>ponteiro indireto"]
FSR1_R["FSR1 (12 bits)<br/>ponteiro indireto"]
FSR2_R["FSR2 (12 bits)<br/>ponteiro indireto"]
ALU3["ULA"]
WREG2 <-->|"operandos"| ALU3
ALU3 -->|"atualiza"| STATUS2
STATUS2 -->|"flags"| PC_REG
BSR2 -->|"seleciona banco"| RAM_BLOCK["RAM 4KB"]
FSR0_R -->|"endereço indireto"| RAM_BLOCK
FSR1_R -->|"endereço indireto"| RAM_BLOCK
FSR2_R -->|"endereço indireto"| RAM_BLOCK
RAM_BLOCK <-->|"dados"| ALU3
end
subgraph PERIFERICO2["Interface com Periféricos"]
TRIS_R["TRISx<br/>(direção I/O)"]
PORT_R["PORTx / LATx<br/>(valor I/O)"]
TRIS_R -->|"configura"| PINOS["Pinos físicos"]
PORT_R <-->|"lê/escreve"| PINOS
RAM_BLOCK <-->|"SFRs"| TRIS_R
RAM_BLOCK <-->|"SFRs"| PORT_R
end
Esse diagrama integrado mostra que o caminho de dados não é apenas a ULA e os registradores — ele inclui toda a infraestrutura que permite ao processador buscar instruções, gerenciar sub-rotinas através da pilha, acessar memória de forma direta e indireta, e interagir com os periféricos do chip.
Conexão com o Projeto Integrador
Neste módulo, o trabalho prático do Projeto Integrador tem dois focos principais que aplicam diretamente o que você acabou de estudar.
O primeiro foco é a otimização de registradores: você e seu grupo irão identificar uma seção do código do projeto que realiza operações repetidas (provavelmente algum cálculo de display ou processamento de sensor), analisar o assembly gerado pelo compilador MPLAB X, e refatorar o código C para reduzir o número de acessos à RAM, mantendo dados frequentemente usados no registrador W ou no Access Bank. A seção de Disassembly Listing do MPLAB X (menu Window → Debugging → Disassembly) mostrará exatamente quais instruções estão sendo geradas.
O segundo foco é a medição do ciclo de instrução: usando o simulador do MPLAB X e os timers do PIC18F4550, você criará uma tabela de referência com o número de ciclos que diferentes classes de instruções consomem. Para isso, você usará o Timer0 como cronômetro de alta resolução — configurado para contar ciclos de instrução — e medirá o tempo de execução de blocos de código cuidadosamente construídos.
Atenção para a sessão de tutoria: Antes de medir tempos, certifique-se de que o Timer0 está configurado com prescaler 1:1 e que você sabe como ler seus 16 bits (THigh e TLow). Consulte a seção de Timer0 no datasheet do PIC18F4550 (páginas 125-138) para entender os registradores T0CON, TMR0H e TMR0L.
Síntese: O Que Você Aprendeu Neste Módulo
Você atravessou um módulo denso e repleto de conceitos fundamentais. Vamos recapitular o que foi construído.
O caminho de dados é a infraestrutura física do processador que move e transforma informação. Ele é composto pela ULA, pelo banco de registradores, pelos barramentos e pelos multiplexadores — e esses componentes trabalham em coordenação a cada ciclo de clock para executar as instruções que você escreve.
A ULA é construída a partir de portas lógicas organizadas em circuitos combinacionais. Para operações lógicas, são circuitos simples e paralelos. Para adição, o somador ripple-carry propaga o carry da posição menos significativa para a mais significativa. Todos os circuitos operam em paralelo e um multiplexador seleciona qual resultado aparece na saída. Os flags de status (Z, C, DC, OV, N) no registrador STATUS são a ponte entre a ULA e as instruções de desvio condicional.
O banco de registradores do PIC18F4550 é organizado em 16 bancos de 256 bytes, com o BSR selecionando o banco ativo. O registrador W é o acumulador central — obrigatório em quase todas as instruções aritméticas e lógicas. O Access Bank oferece acesso rápido aos GPRs do Banco 0 e aos SFRs do Banco 15 sem precisar configurar o BSR. Os SFRs são a interface entre o núcleo do processador e os periféricos do chip.
O ciclo de instrução divide-se em cinco fases: busca, decodificação, execução, acesso à memória e escrita de resultado. O pipeline de dois estágios do PIC18F4550 sobrepõe a busca da próxima instrução com a execução da atual, alcançando throughput de uma instrução por ciclo de máquina — exceto em desvios tomados, que custam um ciclo extra de penalidade.
O caminho de dados monociclo é o modelo conceitual que revela a estrutura essencial de qualquer processador: busca, decodificação, ULA, registradores, memória e multiplexadores. Entendê-lo é o fundamento para compreender o pipeline (Módulo 6) e a unidade de controle (Módulo 5), que você estudará nas próximas semanas.
Antes da próxima aula (Módulo 5): Leia o manual do MPLAB X sobre a janela de Registers e o painel de Watch (variáveis monitoradas). Explore o registrador STATUS durante a execução passo a passo de um programa simples com operações aritméticas. Anote quando os flags Z, C e N mudam e por quê. Essa observação direta dos flags em ação será o ponto de partida perfeito para entender como a unidade de controle usa esses flags para tomar decisões de desvio.
A Subtração e o Complemento a Dois: Uma Elegância Arquitetural
Você pode ter estranhado que a tabela de operações da ULA lista “subtrator” como um circuito separado, mas na prática a grande maioria das ULAs reais — incluindo a do PIC18F4550 — não implementa subtração com um circuito independente. Em vez disso, utiliza uma propriedade matemática que torna a subtração equivalente a uma adição: o complemento a dois.
Lembre-se do Módulo 2: o complemento a dois de um número B de n bits é definido como \overline{B} + 1, onde \overline{B} é o complemento a um (inversão bit a bit). A propriedade fundamental é:
A - B = A + (-B) = A + \overline{B} + 1
Isso significa que para subtrair B de A, a ULA pode simplesmente: (1) inverter todos os bits de B (operação NOT, que ela já sabe fazer), (2) somar 1 ao resultado (que equivale a colocar o carry de entrada do somador como 1), e (3) somar com A usando o somador ripple-carry que ela já tem. Nenhum circuito adicional de subtração é necessário!
flowchart LR
subgraph ADD_BLOCK["Somador Ripple-Carry"]
S["Soma A + B'"]
end
A2["Operando A"] --> S
B2["Operando B"] --> INV["Inversores<br/>(NOT bit a bit)"]
INV -->|"B' = NOT(B)"| S
CTRL_SUB["Sinal de Controle<br/>(ADD=0 / SUB=1)"] -->|"Cin = 1"| S
S --> RESULT2["A - B"]
Note["Quando SUB=1:<br/>Cin recebe 1 (em vez de 0)<br/>B é invertido antes de somar<br/>Resultado = A + NOT(B) + 1 = A - B"]
CTRL_SUB --> Note
Essa unificação é mais do que elegância matemática — ela tem impacto direto no hardware: a ULA física precisa de apenas um somador, não dois circuitos separados. O custo extra é um conjunto de multiplexadores que decide se B deve ser invertido ou não (controlado pelo sinal de subtração), e se o carry de entrada deve ser 0 (adição) ou 1 (subtração). Isso reduz o número de transistores e, portanto, o consumo de energia e o custo do chip.
O flag Carry no registrador STATUS ganha um significado específico nesse contexto. Em adição, Carry indica que o resultado ultrapassou 255 (overflow sem sinal). Em subtração, o Carry é na verdade o borrow inverso: Carry = 1 indica que a subtração aconteceu sem empréstimo (ou seja, A \geq B), enquanto Carry = 0 indica que houve empréstimo (A < B). Essa inversão pode parecer confusa, mas é consistente com a implementação via complemento a dois — e é por isso que a instrução BNC (Branch if No Carry) é usada para detectar “menor que” em comparações sem sinal.
Deslocamentos e Rotações: Multiplicação e Divisão Gratuitas
Uma categoria de operações da ULA merece atenção especial por sua utilidade prática no Projeto Integrador: os deslocamentos (shifts) e rotações (rotates). Essas operações movem todos os bits de um registrador uma posição para a esquerda ou para a direita, e têm um efeito matemático muito conveniente.
Deslocar um número binário uma posição para a esquerda equivale a multiplicá-lo por 2. Deslocar uma posição para a direita equivale a dividi-lo por 2 (divisão inteira). Isso ocorre porque na representação binária, cada posição à esquerda representa o dobro do valor da posição anterior — exatamente como no sistema decimal, onde mover um dígito para a esquerda equivale a multiplicar por 10.
O PIC18F4550 oferece quatro variantes dessas operações, cujas diferenças estão em como o bit que “sai pela borda” é tratado e de onde vem o bit que “entra pela outra borda”:
A instrução RLCF (Rotate Left through Carry) desloca todos os bits um posição para a esquerda. O bit que estava na posição 7 (mais significativa) vai para o flag Carry. O bit que entra na posição 0 vem do flag Carry anterior. Isso cria uma rotação de 9 bits que passa pelo registrador de status — extremamente útil para deslocar números de 16 bits armazenados em dois registradores de 8 bits.
A instrução RLNCF (Rotate Left, No Carry) faz uma rotação de 8 bits pura: o bit que sai pela esquerda entra pela direita, sem envolver o flag Carry. O flag Carry é atualizado com o bit que saiu, mas não é usado como entrada.
Exemplo: Multiplicando por 4 Usando Deslocamentos
O cálculo resultado = valor * 4 pode ser implementado com dois deslocamentos à esquerda, sem usar multiplicação:
Em assembly, o compilador provavelmente gerará:
Dois deslocamentos à esquerda = multiplicação por 4, tudo em 3 ciclos de máquina.
Compare com o custo de uma multiplicação real de 8 bits no PIC18F4550: como o PIC18F4550 não tem instrução de multiplicação de 8×8 bits com resultado de 8 bits no caminho de dados principal (ele tem MULWF para multiplicação de 8×8 com resultado de 16 bits nos registradores PRODH:PRODL), deslocamentos são frequentemente a abordagem mais eficiente para potências de 2.
Atenção: para valores com sinal (inteiros com sinal em complemento a dois), deslocamentos à direita devem preservar o bit de sinal para manter o sinal do número. Isso é chamado de deslocamento aritmético — o bit que entra pela esquerda é uma cópia do bit de sinal, não zero. Para tipos unsigned, o deslocamento lógico (zero entra pela esquerda) é correto. O compilador XC8 usa automaticamente a versão correta baseada no tipo da variável.
A instrução MULWF merece menção especial: ela realiza multiplicação de 8×8 bits com resultado de 16 bits, armazenados no par de registradores PRODH:PRODL. Essa é uma capacidade relativamente avançada para um microcontrolador de 8 bits — muitos MCUs de 8 bits de gerações anteriores não tinham hardware de multiplicação e precisavam implementá-la via software com deslocamentos e somas. No PIC18F4550, MULWF executa em apenas 1 ciclo de máquina, tornando-o adequado até para aplicações que exigem aritmética de ponto fixo.
O Banco de Registradores em Perspectiva: Comparando com Outras Arquiteturas
Uma pergunta natural ao estudar o PIC18F4550 é: “por que tão poucos registradores de propósito geral explícitos?” — afinal, a instrução ADDWF tem apenas um registrador de propósito geral nomeado (o File Register f) além do acumulador W. Compare isso com um processador ARM Cortex-M, que tem 16 registradores de 32 bits todos intercambiáveis.
A resposta está na filosofia de projeto. O PIC18F4550 é um descendente de uma família de microcontroladores projetada nos anos 1980 para aplicações embarcadas simples, onde o custo do chip era a restrição principal. Um banco de registradores com 16 registradores de 32 bits como o ARM requer mais transistores, mais área de chip, mais energia — tudo isso em conflito com os requisitos de um microcontrolador barato para controlar um eletrodoméstico ou um automóvel.
A arquitetura de acumulador com banco de registradores na RAM foi uma solução engenhosa para esse compromisso: ao tratar toda a RAM interna como “banco de registradores endereçável”, o PIC oferece uma quantidade enorme de armazenamento local (centenas de bytes) ao custo de ter apenas um registro de trabalho rápido (W). O programador paga o preço de ter que gerenciar explicitamente o conteúdo de W, mas ganha um chip muito menor e mais barato.
Comparativo: Filosofias de Banco de Registradores
| Característica | PIC18F4550 | ARM Cortex-M0+ | x86-64 |
|---|---|---|---|
| Registradores GPR | 1 (W) + RAM | 16 × 32 bits | 16 × 64 bits |
| Largura de dados | 8 bits | 32 bits | 64 bits |
| Filosofia | Acumulador | Load-Store RISC | CISC acumulador |
| Acesso à RAM | 1 ciclo (on-chip) | 1+ ciclos (AHB) | Vários ciclos |
| Tamanho do chip | Muito pequeno | Pequeno | Grande |
| Aplicações típicas | MCU embarcado | MCU IoT | Computadores |
Note que o ARM Cortex-M0+ (presente no Arduino Zero, por exemplo) tem 16 registradores intercambiáveis — uma abordagem mais flexível que a do PIC, mas ainda conservadora comparada ao x86-64. O princípio é que registradores são a memória mais rápida disponível: quanto mais deles você tiver, menos vezes o processador precisará ir buscar dados na memória mais lenta.
Para o seu trabalho no Projeto Integrador, essa comparação tem uma implicação prática direta: ao escrever código C para o PIC18F4550, você deve ser muito consciente de quando está “derramando” variáveis da RAM para dentro e para fora da ULA. Funções com muitas variáveis locais podem gerar código assembly ineficiente porque o compilador precisa fazer muitos MOVF e MOVWF para carregar e salvar variáveis de ida e volta da RAM para W. A boa prática de manter funções curtas e com poucas variáveis locais não é apenas uma questão de estilo — é uma estratégia de desempenho real no PIC18F4550.
Endereçamento Indireto: FSRs Como Ponteiros em Hardware
No Módulo 3, você aprendeu sobre o modo de endereçamento indireto — quando o endereço efetivo do operando não está na instrução, mas sim em um registrador que funciona como ponteiro. No PIC18F4550, os FSRs (File Select Registers) implementam exatamente esse mecanismo, e eles são componentes do caminho de dados que merecem atenção especial porque você os usará frequentemente no Projeto Integrador ao trabalhar com arrays e buffers.
O PIC18F4550 tem três pares FSR: FSR0 (composto por FSR0H:FSR0L), FSR1 (FSR1H:FSR1L) e FSR2 (FSR2H:FSR2L). Cada FSR é um registrador de 12 bits que aponta para qualquer posição do espaço de dados RAM. Para ler o dado apontado por FSR0, você usa a instrução MOVF INDF0, W — onde INDF0 é um endereço especial que não corresponde a nenhuma localização real, mas instrui o processador a usar FSR0 como endereço. O caminho de dados detalha esse processo:
sequenceDiagram
participant IR3 as Instruction Register
participant DEC2 as Decodificador
participant FSR_R as FSR0 (12 bits)
participant MUXADDR as MUX Endereço RAM
participant RAM2 as RAM de Dados
participant ALU4 as ULA
participant WREG3 as Registrador W
IR3->>DEC2: MOVF INDF0, W (instrução)
DEC2->>MUXADDR: Seleciona "modo indireto via FSR0"
FSR0_R->>MUXADDR: Fornece endereço (12 bits)
MUXADDR->>RAM2: Apresenta endereço do FSR0
RAM2->>ALU4: Dado lido da RAM
ALU4->>WREG3: W ← dado (operação de passagem)
O PIC18F4550 tem variantes das instruções de endereçamento indireto que fazem operações de pós-incremento e pré-decremento automaticamente. MOVF POSTINC0, W lê o valor apontado por FSR0 e depois incrementa FSR0. MOVF PREINC0, W primeiro incrementa FSR0 e depois lê. MOVF POSTDEC0, W lê e depois decrementa. Essas variantes são o equivalente exato dos operadores *p++ e *--p em C, e permitem percorrer arrays de forma muito eficiente.
Exemplo: Somando um Array com Endereçamento Indireto
Suponha que você tem um array de 8 bytes na RAM e quer calcular a soma de todos os seus elementos:
O assembly equivalente usando FSR0:
; Inicializa FSR0 para apontar para o início do array
MOVLW LOW(dados) ; byte baixo do endereço
MOVWF FSR0L, ACCESS
MOVLW HIGH(dados) ; byte alto do endereço
MOVWF FSR0H, ACCESS
; Inicializa contador e acumulador
MOVLW 8 ; 8 iterações
MOVWF contador, ACCESS
CLRF soma_total, ACCESS ; soma_total = 0
loop_soma:
MOVF POSTINC0, W, ACCESS ; W ← *FSR0; FSR0++
ADDWF soma_total, F, ACCESS ; soma_total += W
DECFSZ contador, F, ACCESS ; contador--; pula se zero
BRA loop_soma
; soma_total agora contém a soma dos 8 elementosO uso de POSTINC0 elimina a instrução de incremento do ponteiro que seria necessária em endereçamento direto, tornando o laço mais compacto e rápido.
Entendendo o Timing: Clock, Ciclos e a Relação com o Caminho de Dados
Uma questão que frequentemente gera confusão nos estudantes é a relação entre a frequência do oscilador (clock externo ou interno), o clock de instrução e os ciclos de máquina. Compreender essa relação é essencial para calcular tempos de execução no Projeto Integrador.
O PIC18F4550 tem um oscilador que pode ser configurado para diferentes frequências. Quando conectado ao cristal de 20 MHz do kit ACEPIC PRO V8.2 e configurado com o PLL interno, o oscilador efetivo opera a 48 MHz. Cada oscilação do cristal é um ciclo de clock — e é a unidade mais básica de tempo no processador.
O clock de instrução, internamente chamado de Fosc/4 (frequência do oscilador dividida por 4), é a frequência com que a maioria das instruções é executada. No caso do kit com PLL a 48 MHz, o clock de instrução é 48 / 4 = 12 MHz. Isso significa que, a cada 4 ciclos de oscilador, ocorre um ciclo de instrução — cada ciclo de instrução dura 1/12 \times 10^6 \approx 83,3 ns.
Por que dividir por 4? Porque o PIC18F4550 usa um clock de quatro fases interno (Q1, Q2, Q3, Q4) para coordenar as diferentes atividades dentro do caminho de dados durante um ciclo de instrução. Na fase Q1, a instrução é decodificada. Na fase Q2, os registradores são lidos. Na fase Q3, a ULA executa a operação. Na fase Q4, o resultado é escrito de volta e a próxima busca começa. Essas quatro fases juntas constituem um ciclo de máquina completo.
gantt
title Ciclo de Máquina do PIC18F4550 (48 MHz → 12 MHz instrução)
dateFormat X
axisFormat Q%s
section Ciclos de Clock (Fosc = 48 MHz)
Q1 :done, 0, 1
Q2 :done, 1, 2
Q3 :done, 2, 3
Q4 :done, 3, 4
section Atividades do Caminho de Dados
Decodificação (Q1) :active, 0, 1
Leitura Registradores (Q2) :active, 1, 2
Execução ULA (Q3) :active, 2, 3
Write-Back + Fetch (Q4) :active, 3, 4
Essa organização em quatro fases é relevante para você porque determina como os periféricos do PIC18F4550 funcionam. Por exemplo, os timers e o módulo de captura/comparação/PWM (CCP) têm resolução de 1 ciclo de instrução (83,3 ns a 48 MHz), não de 1 ciclo de clock (20,8 ns). Portanto, ao configurar um timer para gerar uma frequência específica, o período mínimo configurável é de 83,3 ns, não de 20,8 ns.
Uma Visão Histórica: Como o Caminho de Dados Evoluiu
Para colocar o caminho de dados do PIC18F4550 em perspectiva, é instrutivo traçar brevemente como os caminhos de dados evoluíram ao longo da história dos processadores. Isso não é apenas história — é um relato de como as pressões de engenharia moldaram as decisões de projeto que você estuda hoje.
Os primeiros microprocessadores dos anos 1970, como o Intel 4004 e o 8080, tinham caminhos de dados extremamente simples: uma ULA de 4 ou 8 bits, um punhado de registradores (tipicamente A, B, C, D, E, H, L no 8080), e um barramento único pelo qual tanto dados quanto endereços eram multiplexados no tempo. A simplicidade era necessária porque a tecnologia de fabricação da época limitava o número de transistores por chip.
O MOS 6502 (usado no Apple II e no Atari), contemporâneo do 8080, fez escolhas ligeiramente diferentes: acumulador central (como o PIC), X e Y como registradores de índice, e endereçamento indireto via “página zero” (os primeiros 256 bytes da RAM). Essas escolhas influenciaram diretamente o projeto da família PIC — a organização em bancos de memória e o Access Bank do PIC18 são descendentes conceituais da página zero do 6502.
Os anos 1980 trouxeram os processadores de 16 e 32 bits (8086, 68000, SPARC, MIPS) e com eles o debate CISC versus RISC que você estudou no Módulo 3. Os processadores RISC simplificaram drasticamente o caminho de dados: instruções de comprimento fixo, banco de registradores grande e uniforme, e o princípio load-store (apenas instruções de load e store acessam a memória; todas as operações aritméticas são feita entre registradores). Essa simplicidade permitiu pipelines mais profundos e velocidades de clock mais altas.
Os processadores modernos chegaram a caminhos de dados extraordinariamente complexos: execução fora de ordem (onde o hardware decide executar instruções em ordem diferente da programada para maximizar o uso das unidades funcionais), renaming de registradores (onde o hardware mapeia os registradores visíveis da ISA para um pool muito maior de registradores físicos), e múltiplas unidades de execução em paralelo. Um Intel Core i9 moderno tem centenas de unidades funcionais e pode executar dezenas de operações por ciclo de clock.
O PIC18F4550, com seu caminho de dados modestos e pipeline de dois estágios, representa um ponto muito diferente nesse espectro — deliberadamente simples para atender seus requisitos de custo, consumo de energia e previsibilidade de timing. E é exatamente por essa simplicidade que ele é um veículo de aprendizado tão eficaz: todos os conceitos que você estudou aqui são visíveis e verificáveis, sem camadas de complexidade que obscurecem o funcionamento fundamental.
Referências e Leituras Complementares
Para aprofundar os conceitos deste módulo, os seguintes recursos são especialmente recomendados:
O Datasheet do PIC18F4550 (disponível no site da Microchip) é a referência primária para toda a arquitetura do processador que você está usando. O Capítulo 4 descreve os registradores de memória de dados (GPRs, SFRs, bancos). O Capítulo 3 descreve a memória de programa e o Program Counter. O Capítulo 5 apresenta o mapa completo dos SFRs.
Patterson e Hennessy — “Computer Organization and Design” (disponível em português como “Organização e Projeto de Computadores”): os Capítulos 4.1 a 4.4 descrevem o caminho de dados monociclo com profundidade e rigor, usando a ISA MIPS como exemplo. Os conceitos são diretamente aplicáveis ao que você estudou aqui com o PIC18F4550.
Stallings — “Arquitetura e Organização de Computadores”: os Capítulos 11 e 12 cobrem a organização do processador, o caminho de dados e a unidade de controle com abordagem mais tradicional e igualmente sólida.
O manual do compilador XC8 (disponível no site da Microchip) explica como o compilador aloca variáveis nos bancos de memória e como usar atributos para controlar a alocação. A seção sobre “Memory Models” é particularmente relevante para as otimizações do Projeto Integrador.