flowchart TD
OBJ["Pergunta inicial:<br/>como ADDWF cabe em<br/>4 pulsos de clock?"] --> ALU["ALU<br/>motor combinacional"]
OBJ --> REG["Banco de<br/>registradores<br/>WREG + File Register"]
OBJ --> BUS["Barramentos e<br/>multiplexadores"]
ALU --> CIC["Ciclo de instrução<br/>fases + prefetch"]
REG --> CIC
BUS --> CIC
CIC --> PIC["Caminho de dados<br/>do PIC18F4550<br/>estados Q1 a Q4"]
PIC --> QUANT["Análise quantitativa<br/>CPI e Amdahl"]
QUANT --> PI["Projeto Integrador:<br/>WREG, tabela de ciclos,<br/>fases observáveis"]
Módulo 04: Unidade Central de Processamento — Caminho de Dados
Bem-vindo ao quarto módulo. Até aqui tratamos o processador como uma caixa preta. Agora abrimos a tampa do gabinete e percorremos, em prosa direta, o subsistema chamado caminho de dados. Você sairá daqui sabendo como uma instrução ADDWF do seu PIC18F4550 transforma bits em resultado e em flags — e por quê isso cabe, no kit ACEPIC PRO V8.2, em quatro pulsos do oscilador.
O Problema Que Abre o Módulo
Tome a instrução ADDWF SOMA, F, A, que soma WREG ao registrador SOMA e grava o resultado no próprio SOMA. O datasheet afirma que essa instrução consome um ciclo de máquina, equivalente a quatro pulsos do oscilador. Em quatro tiquetaques, é preciso buscar a instrução na Flash, decodificá-la, ler SOMA, somá-lo a WREG, atualizar STATUS e escrever o resultado de volta em SOMA. Como cinco operações conceitualmente distintas cabem em quatro pulsos de relógio? A resposta, que você terá ao final do módulo, envolve uma ALU puramente combinacional, um banco de registradores assimétrico, uma rede de multiplexadores e uma sobreposição entre busca e execução herdada da arquitetura Harvard. Mantenha a pergunta na cabeça; vou amarrá-la na seção final.
Os módulos anteriores trataram o processador como contrato — a arquitetura. Aqui cruzamos a fronteira para a organização. Tudo o que veremos é organizacional: dois processadores com a mesma ISA podem ter caminhos de dados radicalmente diferentes, e o programador, em condições normais, só os distingue pelo desempenho.
A Unidade Lógica e Aritmética
A ULA, daqui em diante referida pela sigla ALU consagrada na literatura, é o componente combinacional que realiza todas as operações aritméticas e lógicas do processador. Quando uma instrução manda somar dois operandos, é a ALU quem soma; quando uma comparação precisa decidir se um operando é maior que outro, a ALU realiza uma subtração e os flags resultantes traduzem a comparação. Ela é o operário do processador: não decide o que fazer, apenas executa as transformações que o resto do caminho de dados lhe encaminha.
Por ser puramente combinacional, a ALU não tem memória interna. A cada ciclo recebe dois operandos A e B e um vetor de sinais de controle que determina a operação a aplicar, devolvendo um resultado R e um vetor de flags. Formalmente, é uma função \text{ALU}: \{0,1\}^n \times \{0,1\}^n \times \{0,1\}^k \to \{0,1\}^n \times \{0,1\}^f. No PIC18F4550 temos n=8, e o repertório suportado é da ordem de uma dúzia de operações: soma e subtração com e sem carry, AND, OR, XOR, NOT, deslocamentos com e sem rotação pelo carry, incremento e decremento.
flowchart LR
A["Operando A<br/>8 bits"] --> SOM["Somador<br/>8 bits"]
B["Operando B<br/>8 bits"] --> SOM
A --> LOG["Bloco lógico<br/>AND OR XOR NOT"]
B --> LOG
A --> SHF["Shifter<br/>esquerda/direita"]
A --> INC["Incremento<br/>Decremento"]
SOM --> MUX["MUX de saída"]
LOG --> MUX
SHF --> MUX
INC --> MUX
CTRL["Sinais de controle<br/>seletor s"] --> MUX
MUX --> R["Resultado R"]
SOM --> F["Flags<br/>Z C DC OV N"]
O tijolo elementar é o somador completo de um bit, o full adder, que recebe dois bits e um carry de entrada e devolve um bit de soma e um carry de saída. Encadeando oito desses tijolos em série obtém-se um somador ripple-carry de oito bits, no qual o carry precisa propagar-se serialmente do bit menos significativo ao mais significativo. Esse atraso cabe folgadamente em um ciclo de máquina do PIC18; em processadores de 32 ou 64 bits, exige circuitos sofisticados como o carry-lookahead. Sobre essa base aritmética monta-se a parte lógica com arranjos paralelos de portas elementares, e os deslocamentos saem de uma rede de multiplexadores em forma de barrel shifter. Todos esses subcircuitos operam em paralelo a partir dos mesmos operandos, e um grande multiplexador na saída escolhe o resultado válido. O atraso do caminho crítico é o do subcircuito mais lento, em geral o somador.
Antes de seguir, pergunte-se: se uma operação AND simplesmente não envolve o somador, qual deve ser a regra de atualização do carry em uma instrução ANDWF? Sua resposta diz muito sobre o que vem a seguir.
A ALU produz, além do resultado, um conjunto de flags armazenados no registrador especial STATUS. No PIC18F4550 são cinco: Z (zero), C (carry), DC (digit carry, útil em BCD), OV (overflow em complemento de dois) e N (negativo). Esses flags cumprem papel duplo: são consultados por instruções de desvio condicional como BZ ou BNC, base da lógica de controle de fluxo em assembly; e registram informação que se perderia — o carry produzido por uma soma de bytes é o único modo de implementar somas de múltipla precisão encadeando instruções ADDWFC.
Há uma sutileza importante: uma soma mexe em Z, C, DC, OV e N; um AND lógico mexe apenas em Z e N, deixando os demais intactos. O AND não produz carry, não produz digit carry e não tem sentido em complemento de dois. Manter os valores antigos preserva informação útil. Esse comportamento decorre diretamente da estrutura física — o MUX de saída de flags simplesmente não roteia C, DC e OV quando a operação não envolveu o somador.
O programa abaixo executa três operações que estressam classes distintas da ALU — soma com overflow, AND lógico e deslocamento à esquerda — capturando STATUS após cada uma. Ao rodá-lo no MPLAB X com Watch em binário sobre o STATUS, você verá o padrão de atualização de flags exatamente como descrito.
04_alu_demo.c
/*
* 04_alu_demo.c
* Exercita as principais classes de operacoes da ALU do
* PIC18F4550 (logicas, aritmeticas e de deslocamento) e
* imprime o estado dos flags Z, C, DC e OV em uma variavel
* 'snapshot' apos cada operacao.
*/
#include <xc.h>
unsigned char snapshot_status;
unsigned char r_aritm, r_logico, r_shift;
static inline void captura_status(void)
{
snapshot_status = STATUS; /* Z em bit 2, C em bit 0, OV em bit 3, N em bit 4 */
}
void main(void)
{
unsigned char x = 0x7F;
unsigned char y = 0x01;
/* Soma: deve ativar OV ao cruzar a fronteira de sinal */
r_aritm = x + y;
captura_status();
/* AND logico: nao mexe em C, DC, OV */
r_logico = x & y;
captura_status();
/* Deslocamento a esquerda: o bit 7 vai para C */
r_shift = x << 1;
captura_status();
while (1) { /* permanece para inspecao via debugger */ }
}04_alu_demo.asm
; 04_alu_demo.asm
; Versao em assembly que executa, em sequencia, uma soma com
; estouro de sinal, um AND logico e um deslocamento a esquerda,
; capturando o registrador STATUS apos cada operacao.
#include <xc.inc>
PSECT udata_acs
x_var: ds 1
y_var: ds 1
r_aritm: ds 1
r_logico: ds 1
r_shift: ds 1
snap_add: ds 1
snap_and: ds 1
snap_shf: ds 1
PSECT code
inicio:
movlw 0x7F
movwf x_var, A
movlw 0x01
movwf y_var, A
; Soma: ativa OV (0x7F + 0x01 cruza fronteira de sinal)
movf x_var, W, A
addwf y_var, W, A
movwf r_aritm, A
movff STATUS, snap_add
; AND logico: nao mexe em C, DC, OV (apenas Z, N)
movf x_var, W, A
andwf y_var, W, A
movwf r_logico, A
movff STATUS, snap_and
; Deslocamento a esquerda: bit 7 sai para C
rlncf x_var, W, A ; rotate left no carry
movwf r_shift, A
movff STATUS, snap_shf
loop_inf:
bra loop_inf
end inicioA versão em C deixa o compilador escolher como traduzir; a versão em assembly é deliberadamente literal, com captura imediata do STATUS após cada operação para evitar contaminação dos flags. Adquira desde já o hábito de inspecionar o assembly gerado pelo compilador como parte do seu raciocínio sobre desempenho.
O Banco de Registradores
Por que processadores têm registradores em vez de operarem diretamente sobre a RAM? Memórias rápidas são caras por bit e ocupam área expressiva de silício; memórias baratas são lentas. Se o processador buscasse cada operando diretamente da RAM, o ciclo de máquina seria dominado pelo tempo de acesso à memória. A solução foi introduzir, dentro do processador, um conjunto pequeno de células de armazenamento de altíssima velocidade fabricadas com a mesma tecnologia das portas lógicas da ALU. Essas células são os registradores, e a sua coleção organizada chama-se banco de registradores.
Um banco com r registradores de n bits possui p_r portas de leitura e p_w portas de escrita. Uma instrução RISC do tipo add r1, r2, r3 precisa ler dois operandos simultaneamente; com apenas uma porta de leitura, seria necessário serializar a leitura, dobrando o tempo. Por isso a convenção RISC oferece duas portas de leitura e uma de escrita, casando com a estrutura das instruções aritméticas mais comuns. O custo de cada porta adicional é concreto: mais MUXes internos, mais fios, mais área, mais consumo e maior atraso.
O PIC18F4550 não segue o modelo RISC simétrico. Ele segue um modelo herdado da família PIC original, com um único registrador acumulador chamado WREG e um amplo espaço de RAM rápida chamado File Register, que abriga tanto os registradores de função especial (SFR) quanto a área de uso geral. A maior parte das instruções aritméticas e lógicas tem a forma OP f, d, a: f é o endereço de um registrador no File Register, d indica se o resultado vai para WREG (0) ou para o próprio f (1), e a seleciona entre o Access Bank e o banco apontado pelo BSR. Um operando é implícito — WREG — e o outro é explícito — f. Como WREG é sempre um dos operandos, não é preciso codificar seu endereço, e isso economiza bits no formato da instrução.
flowchart LR
WREG["WREG<br/>acumulador 8 bits"] --> ALU
FR["File Register<br/>SRAM endereçável<br/>até 4096 posições"] --> ALU["ALU 8 bits"]
ALU -->|"d=0"| WREG
ALU -->|"d=1"| FR
AB["Access Bank<br/>96 posições rápidas<br/>sem ajustar BSR"] -.fatia rápida.-> FR
BSR["BSR<br/>seleciona banco"] --> FR
O banco do PIC18F4550 é, portanto, fisicamente assimétrico. WREG é independente, ligado a uma das portas da ALU. O File Register é uma SRAM dual-port que serve tanto de operando quanto de destino. Uma instrução ADDWF f, 1, A lê f da SRAM, soma com WREG e escreve o resultado de volta em f no mesmo ciclo de máquina, exigindo porta de leitura e porta de escrita operando concorrentemente sobre o File Register.
O Access Bank, selecionado quando o bit a vale 0, merece atenção. Ele inclui as primeiras posições da memória de dados, endereçáveis sem configurar o BSR. O XC8 aloca nele, por padrão, as variáveis de uso frequente. Compreender essa estrutura é decisivo para escrever código eficiente: cada variável fora do Access Bank exige um MOVLB adicional antes do acesso, e o custo se acumula em laços apertados.
A primeira tarefa do Projeto Integrador pede que você reescreva uma seção de código para minimizar acessos à RAM, mantendo dados frequentes em WREG. O trecho a seguir aplica uma constante a oito posições de um vetor; a versão didática relê a constante da RAM a cada iteração, e a otimizada mantém-na em WREG. Mais importante que o ganho numérico é o raciocínio: você ajusta o código à organização interna do processador — habilidade que se transfere para qualquer arquitetura.
04_uso_registradores.c
/*
* 04_uso_registradores.c
* Demonstra uso eficiente do registrador W (WREG) do PIC18F4550
* mantendo um valor frequentemente lido em W em vez de relê-lo
* da RAM a cada acesso. Compilar com XC8.
*/
#include <xc.h>
#define _XTAL_FREQ 8000000UL
unsigned char buffer[8];
/* Versão didática: cada soma relê a constante 0x10 da RAM por
* meio da variável 'k'. Cada leitura de 'k' custa um ciclo
* extra de MOVF f,W em relação ao caso em que k já mora em W. */
void soma_didatica(void)
{
unsigned char k = 0x10;
unsigned char i;
for (i = 0; i < 8; i++) {
buffer[i] = buffer[i] + k; /* MOVF k,W ; ADDWF buffer+i */
}
}
/* Versão otimizada para registrador: k é mantida em W por meio
* de uma variável local marcada com a dica 'register'. O compilador
* XC8 prioriza esse identificador para WREG quando o ciclo de
* vida permite. */
void soma_otimizada(void)
{
register unsigned char k = 0x10;
register unsigned char i;
for (i = 0; i < 8; i++) {
buffer[i] += k;
}
}
void main(void)
{
TRISB = 0x00;
LATB = 0x00;
while (1) {
soma_didatica();
soma_otimizada();
LATB ^= 0x01;
__delay_ms(100);
}
}04_uso_registradores.asm
; 04_uso_registradores.asm
; Demonstra, em assembly do PIC18F4550, a diferença de ciclos
; entre uma versão que relê uma constante da RAM a cada iteração
; e uma versão que mantém a constante em WREG durante todo o loop.
;
; Montar com MPASM/pic-as. Acoplar a um arquivo de configuração
; (CONFIG) e a um vetor de reset standard.
#include <xc.inc>
PSECT udata_acs
buffer: ds 8
k_var: ds 1
i_var: ds 1
PSECT code
; --- Versão didática: WREG é constantemente sobrescrita ---
soma_didatica:
movlw 0x10
movwf k_var, A ; k_var = 0x10
clrf i_var, A
loop_did:
movf k_var, W, A ; recarrega k da RAM em W (1 ciclo)
lfsr 0, buffer
movf i_var, W, A
addwf FSR0L, F, A ; FSR0 aponta para buffer[i]
movf POSTINC0, W, A ; W = buffer[i]
addwf k_var, W, A ; W = buffer[i] + k
movwf POSTDEC0, A
incf i_var, F, A
movlw 0x08
cpfslt i_var, A
return
bra loop_did
; --- Versão otimizada: k permanece em WREG durante o loop ---
soma_otimizada:
movlw 0x10 ; W = k (uma única vez)
clrf i_var, A
loop_otm:
lfsr 0, buffer
addwf FSR0L, F, A ; nao perturba W
movf INDF0, F, A ; le buffer[i] em W via INDF0
addwf INDF0, F, A ; buffer[i] = buffer[i] + W
incf i_var, F, A
movlw 0x08 ; (este movlw destroi W; em codigo
; real preservariamos k em outro reg)
cpfslt i_var, A
return
bra loop_otm
endObservação honesta: o XC8 já realiza parte dessa otimização automaticamente, e o ganho pode ser modesto. O objetivo pedagógico não é vencer o compilador; é compreender no detalhe o que ele tenta fazer. Sem essa compreensão, você fica à mercê dele quando ele não consegue otimizar — código com chamadas indiretas, com efeitos colaterais de hardware, com restrições rígidas de timing.
Barramentos e Multiplexadores
Os barramentos são as estradas pelas quais os dados trafegam entre os componentes do caminho de dados. Em qualquer instante, um barramento de n bits transporta uma única palavra de n bits. No PIC18F4550, a via interna de dados tem oito bits e a via de instruções tem dezesseis, refletindo o tamanho fixo da maior parte das instruções da família. Essa assimetria é típica das arquiteturas Harvard e contrasta com a Von Neumann pura, onde dados e instruções compartilham um único barramento.
A regra de que apenas uma fonte pode dirigir um barramento a cada ciclo é absoluta — duas fontes ativas simultaneamente produzem um curto interno potencialmente destrutivo. Tradicionalmente, evitava-se isso com buffers tri-state; em projetos internos a um chip moderno, prefere-se substituir tri-state por multiplexadores, mais robustos e melhor caracterizados pelos sintetizadores.
Um multiplexador, ou MUX, é um circuito combinacional que escolhe entre m entradas qual será conectada à saída, segundo um sinal de seleção. Eles são onipresentes: aparecem antes de cada porta da ALU, antes de cada porta de escrita do banco, antes do contador de programa para decidir se a próxima instrução vem da posição seguinte ou de um endereço de desvio.
flowchart LR
F1["Fonte 1"] --> MUX(("MUX"))
F2["Fonte 2"] --> MUX
F3["Fonte 3"] --> MUX
SEL["Seletor<br/>da unidade<br/>de controle"] --> MUX
MUX --> DEST["Destino<br/>porta da ALU<br/>ou registrador"]
A latência de um MUX é muito pequena, e mesmo um caminho com cinco ou seis MUXes em série tem latência inferior à de uma única passagem pelo somador. Por isso, MUXes raramente são gargalo, e os projetistas usam-nos com generosidade. Toda vez que um componente pode receber dados de mais de uma fonte possível, há um MUX antes da sua entrada e um sinal de controle decidindo qual fonte está ativa. Essa onipresença explica por que a unidade de controle, no Módulo 05, precisa produzir tantos sinais distintos: cada MUX exige seu próprio seletor.
Quando você desenha o caminho de dados completo, está, no fundo, desenhando um grafo dirigido em que os nós são componentes funcionais e as arestas são barramentos; em cada nó com mais de uma origem possível, há um MUX. Essa visão separa o que pode acontecer — o caminho de dados — do que de fato acontece a cada ciclo — o vetor de controle. O caminho de dados é o mapa rodoviário; o vetor de controle é o roteiro de viagem.
O Ciclo de Instrução
Toda instrução, em qualquer processador convencional, atravessa um ciclo composto por fases distintas. A nomenclatura clássica, de Hennessy e Patterson, decompõe esse ciclo em cinco fases. Apresento cada uma e em seguida mostro como o PIC18F4550 mapeia essa estrutura.
A primeira é a busca, IF (instruction fetch): o processador usa o PC como endereço da memória de programa, lê a próxima instrução e a armazena no IR; em paralelo, o PC é incrementado. A segunda é a decodificação ID, em que circuitos combinacionais extraem o opcode e identificam os campos de operando. A terceira é a execução EX, em que a ALU aplica a operação aos operandos lidos. A quarta é o acesso à memória MEM, ativada substantivamente apenas por loads e stores. A quinta é a escrita do resultado WB (write-back), em que o valor produzido pela ALU ou lido da memória é gravado no banco ou na RAM, materializando o efeito da instrução.
flowchart LR
IF["IF<br/>Busca da instrução"] --> ID["ID<br/>Decodificação"]
ID --> EX["EX<br/>Execução na ALU"]
EX --> MEM["MEM<br/>Acesso à memória<br/>de dados"]
MEM --> WB["WB<br/>Escrita do<br/>resultado"]
WB -.próxima instrução.-> IF
O PIC18F4550 não implementa exatamente essas cinco fases. O modelo de cinco estágios é mais útil para RISC com pipeline pleno, que veremos no Módulo 06. O PIC18 adota uma estrutura simplificada de duas fases sobrepostas, o prefetch. Em qualquer ciclo de máquina, ele executa a instrução n e busca a instrução n+1 ao mesmo tempo. Essa sobreposição é viável porque a memória de programa é fisicamente separada da memória de dados — a arquitetura Harvard —, e os dois acessos usam caminhos independentes que não competem por recursos.
flowchart LR
subgraph C1["Ciclo n"]
F1["Fetch I1"]
end
subgraph C2["Ciclo n+1"]
F2["Fetch I2"]
E1["Execute I1"]
end
subgraph C3["Ciclo n+2"]
F3["Fetch I3"]
E2["Execute I2"]
end
subgraph C4["Ciclo n+3<br/>desvio tomado"]
FX["Fetch descartado"]
E3["Execute I3"]
end
Em consequência, a maior parte das instruções do PIC18F4550 consome exatamente um ciclo de máquina, igual a quatro pulsos do oscilador. As cinco operações conceituais não cabem em quatro pulsos se executadas estritamente em sequência. O segredo é que a busca da próxima está sendo feita em paralelo com a execução da atual, e a escrita do resultado aproveita a borda final. Decodificação, leitura de operando e computação são combinacionais e fluem em uma única passagem entre bordas de clock.
Há, porém, uma classe que rompe com essa regra: os desvios efetivamente tomados. Quando o desvio é tomado, a instrução pré-buscada torna-se inútil porque o fluxo salta para outro endereço. O processador descarta a busca já feita e inicia outra a partir do destino, gastando um ciclo extra. Por isso, no PIC18, desvios condicionais tomados consomem dois ciclos; não tomados, apenas um. É a manifestação concreta do hazard de controle que generalizaremos no Módulo 07.
Cada ciclo de máquina subdivide-se em quatro estados Q. Q1 amostra o PC e ativa a leitura da memória de programa; Q2 amostra o IR e dispara a leitura do operando no File Register; Q3 é o pico da execução, com a ALU produzindo resultado e flags; Q4 escreve o destino e captura os flags em STATUS. Os estados Q se sobrepõem entre instruções consecutivas: enquanto a instrução n está em Q3 e Q4, a instrução n+1 está em Q1 e Q2.
flowchart LR
Q1["Q1<br/>amostra PC<br/>lê programa"] --> Q2["Q2<br/>carrega IR<br/>lê operando"]
Q2 --> Q3["Q3<br/>ALU produz<br/>resultado e flags"]
Q3 --> Q4["Q4<br/>escreve destino<br/>captura STATUS"]
Q4 -.próximo ciclo.-> Q1
A terceira tarefa do Projeto Integrador pede que você evidencie experimentalmente as fases do ciclo usando pontos de observação externos. Você não espia diretamente o interior do chip, mas pode acionar pinos do PORTB antes e depois de classes específicas de instruções e medir, com osciloscópio ou analisador lógico, as larguras de pulso. A diferença entre uma instrução ALU monociclo e um desvio tomado é perfeitamente distinguível.
04_ciclo_observavel.c
/*
* 04_ciclo_observavel.c
* Torna observavel externamente as fases do ciclo de instrucao
* pulsando pinos do PORTB antes e depois de cada classe de
* instrucao alvo. O osciloscopio capturara a diferenca de
* largura de pulso, que e proporcional ao numero de ciclos de
* maquina gastos por cada instrucao.
*/
#include <xc.h>
#define _XTAL_FREQ 8000000UL
#define MARCO_FETCH LATBbits.LATB0
#define MARCO_EXEC LATBbits.LATB1
void main(void)
{
TRISB = 0x00;
LATB = 0x00;
unsigned char a = 0x55;
unsigned char b = 0xAA;
unsigned char r;
while (1) {
/* Instrucao ALU registrador-registrador: 1 ciclo */
MARCO_FETCH = 1;
r = a + b;
MARCO_FETCH = 0;
/* Instrucao de leitura da RAM: 1 ciclo */
MARCO_EXEC = 1;
r = a;
MARCO_EXEC = 0;
/* Instrucao de desvio efetivamente tomado: 2 ciclos */
MARCO_FETCH = 1;
if (r) { __nop(); }
MARCO_FETCH = 0;
__delay_ms(10);
}
}04_ciclo_observavel.asm
; 04_ciclo_observavel.asm
; Versao em assembly: marca pinos do PORTB antes e depois de
; classes de instrucao distintas para que o osciloscopio
; evidencie a duracao em ciclos de maquina.
#include <xc.inc>
#define MARCO0 LATB,0,A
#define MARCO1 LATB,1,A
PSECT udata_acs
a_var: ds 1
b_var: ds 1
r_var: ds 1
PSECT code
inicio:
clrf TRISB, A
clrf LATB, A
movlw 0x55
movwf a_var, A
movlw 0xAA
movwf b_var, A
main_loop:
; ALU registrador-registrador (1 ciclo)
bsf MARCO0
movf a_var, W, A
addwf b_var, W, A
movwf r_var, A
bcf MARCO0
; Leitura da RAM em W (1 ciclo)
bsf MARCO1
movf a_var, W, A
bcf MARCO1
; Desvio condicional tomado (2 ciclos quando tomado, 1 quando nao)
bsf MARCO0
tstfsz r_var, A
nop
bcf MARCO0
bra main_loop
end inicioAtenção: as próprias instruções de marcação bsf e bcf sobre LATB consomem um ciclo cada e entram na largura medida. Em medições rigorosas, é necessário descontar esse custo, como descontamos a tara do recipiente antes de pesar a substância em laboratório. Raciocinar sobre as limitações da observação experimental é, talvez, o aprendizado mais valioso da tarefa.
O Caminho de Dados Monociclo
Apresento o caminho de dados monociclo como construção pedagógica. No modelo monociclo, cada instrução completa todas as suas fases em um único ciclo de clock. O período precisa ser longo o suficiente para acomodar o caminho mais lento — tipicamente o de uma instrução de carga, que envolve leitura da memória de dados como passo terminal. Formalmente, T_{clk} = \max_\iota \mathcal{L}(\iota), em que \mathcal{L}(\iota) é a latência combinacional da instrução \iota.
A desvantagem é óbvia: instruções simples gastam o mesmo tempo das mais complexas. Em programas reais, essa ineficiência é proibitiva. Por isso, processadores reais adotam multiciclo ou pipeline. O monociclo aparece aqui apenas pela clareza didática: as ideias que ele expõe — separação entre caminho de dados e controle, papel dos MUXes, organização do PC, distinção entre instruções tipo R, tipo I e de desvio — reaparecem, com refinamentos, em todos os modelos mais sofisticados.
flowchart LR
PC["PC"] --> MP["Memória<br/>de programa"]
MP --> IR["IR"]
IR --> DEC["Decodificador"]
DEC --> BR["Banco de<br/>registradores"]
BR -->|"porta A"| ALU["ALU"]
BR -->|"porta B"| MUX1(("MUX"))
IMM["Imediato<br/>sinal-estendido"] --> MUX1
MUX1 --> ALU
ALU --> MUX2(("MUX"))
ALU --> MD["Memória<br/>de dados"]
MD --> MUX2
MUX2 --> BR
ALU -.flags.-> PCNXT["Próximo PC"]
PCNXT --> PC
Visualize a topologia da esquerda para a direita. O PC alimenta a entrada de endereço da memória de instruções. A saída segue para o decodificador, que produz o opcode e os campos de operando. O banco é lido em duas portas: a primeira vai para a entrada A da ALU; a segunda passa por um MUX que escolhe entre o valor lido e um imediato sinal-estendido, alimentando a entrada B. A ALU produz o resultado, que segue por dois caminhos: um leva a um MUX antes da porta de escrita do banco; o outro vai à entrada de endereço da memória de dados, porque em loads e stores o que a ALU calcula é o endereço efetivo. A memória de dados, quando lida, alimenta a outra entrada do MUX de pré-escrita. Em paralelo, um subcircuito calcula o próximo PC, escolhendo entre PC incrementado, alvo de desvio incondicional e alvo de desvio condicional segundo os flags.
O PIC18 não implementa exatamente o monociclo no sentido estrito; implementa as duas fases sobrepostas com prefetch, passo intermediário entre monociclo e pipeline pleno. Os componentes lógicos são, em essência, os mesmos. A diferença está em como o ciclo de máquina é organizado, decomposto nos quatro estados Q já descritos.
Análise Quantitativa do Caminho de Dados
A teoria materializa-se, para o programador, em uma fórmula simples para o tempo total. Sejam \iota_1, \ldots, \iota_m as instruções executadas e T(\iota_i) o número de ciclos de cada uma. Em segundos, o tempo é o produto da soma de ciclos pelo tempo de um ciclo de máquina, T_{cm} = 4/f_{osc}. A 8 MHz, T_{cm} = 0{,}5\,\mu s. Em arquiteturas onde nem todas as instruções consomem o mesmo número de ciclos — como o PIC18, em razão dos desvios tomados, chamadas e skips — define-se o CPI médio como média ponderada \overline{\text{CPI}} = \sum_k f_k \cdot \text{CPI}_k.
T_{exec} = T_{cm} \cdot \sum_{i=1}^{m} T(\iota_i) \qquad \text{IPS} = \frac{f_{osc}}{4 \cdot \overline{\text{CPI}}}
O CPI captura, em um único número, quão eficientemente o programa explora a arquitetura. No PIC18, programas que evitam desvios e chamadas em laços apertados têm CPI próximo de 1,0; dominados por desvios condicionais tomados, em torno de 1,3 a 1,5; com muitas chamadas a subrotinas curtas, ainda mais alto, em razão do par chamada-retorno custar quatro ciclos. A 8 MHz e com \overline{\text{CPI}} = 1{,}2, o throughput é aproximadamente 1,67 milhões de instruções por segundo — o teto do microcontrolador nessa configuração.
Aplico ao concreto. Considere um laço com cem iterações, cada uma com cinco instruções aritméticas monociclo e um desvio condicional ao topo. Gastam-se cinco ciclos das aritméticas mais dois do desvio tomado, exceto na última iteração, em que o desvio não é tomado e gasta um. O total é 100 \cdot 5 + 99 \cdot 2 + 1 = 699 ciclos, que a 8 MHz correspondem a aproximadamente 350 microssegundos. Em uma aplicação de tempo real com restrição de resposta em meio milissegundo, esse laço consome setenta por cento do orçamento. Esse tipo de análise é exatamente o exercício da segunda tarefa do Projeto Integrador.
Ciclos típicos por classe de instrução no PIC18F4550
| Classe de instrução | Ciclos |
|---|---|
| Aritmética/lógica registrador-registrador | 1 |
| Acesso ao Access Bank | 1 |
Acesso a banco com MOVLB prévio |
1 + 1 prévio |
| Desvio condicional não tomado | 1 |
| Desvio condicional tomado | 2 |
GOTO, BRA |
2 |
CALL, RCALL, RETURN, RETLW |
2 |
Skip não disparado (BTFSS, BTFSC) |
1 |
| Skip disparado | 2 ou 3 |
TBLRD* |
2 |
Encerro com a Lei de Amdahl aplicada ao caminho de dados. Suponha que aritméticas monociclo respondam por oitenta por cento do tempo total e desvios condicionais pelos vinte restantes. Um novo projeto dobra a velocidade das aritméticas sem mexer nos desvios. O ganho global é S = 1/(0{,}2 + 0{,}8/2) \approx 1{,}67, não 2,0. A fração não acelerada atua como teto inviolável. Dobrar a velocidade de uma classe que responde por cinco por cento do tempo total raramente compensa, e a resposta numérica substitui o palpite.
Síntese e Conexões
Recapitulo o percurso. Estudamos a ALU como motor combinacional construído a partir de somadores completos encadeados e enriquecido com circuitos lógicos e de deslocamento em paralelo, com flags que viabilizam tanto desvios condicionais quanto aritmética de múltipla precisão. Estudamos o banco de registradores como memória interna de altíssima velocidade, e vimos como o PIC18F4550 adota uma organização assimétrica com WREG e File Register, complementada pelo Access Bank. Estudamos barramentos e multiplexadores como infraestrutura de transporte e seleção, e a ubiquidade dos MUXes justifica a unidade de controle. Decompomos o ciclo em fases canônicas e vimos o PIC18 sobrepor execução e prefetch para atingir um ciclo por instrução na maior parte dos casos, com exceção dos desvios tomados. Construímos o monociclo como modelo pedagógico e contrastamos com a realidade do PIC18 nos estados Q. Fechamos com a análise quantitativa via CPI, IPS e Amdahl.
Volto à pergunta da abertura. Cinco operações conceituais cabem em quatro pulsos porque a busca da próxima instrução está sobreposta à execução da atual, e porque decodificação, leitura de operando e computação são combinacionais. Os estados Q estruturam a coreografia: Q1 amostra o PC, Q2 carrega o IR e lê o operando, Q3 é o pico da ALU, Q4 escreve o destino e captura os flags.
As três tarefas do Projeto Integrador amarram tudo. A primeira, otimização de uso de WREG, materializa a discussão sobre o banco assimétrico e o Access Bank. A segunda, construção da tabela de ciclos, exige metodologia experimental cuidadosa: configure o Timer0 com prescaler 1:1, zere antes da janela, leia depois, repita N vezes, desabilite interrupções durante a janela ou use a mediana em vez da média, e documente os detalhes — uma tabela sem metodologia documentada é cientificamente inútil. A terceira, evidenciar fases via I/O externo, treina a inferência de comportamento interno por observação externa, habilidade central em sistemas embarcados.
No Módulo 05, vamos atrás de quem produz os sinais de controle que aqui apareceram apenas como entradas anônimas: a unidade de controle. Veremos como ela pode ser hardwired ou microprogramada, modelada em ambos os casos como máquina de estados finitos.
Antes da próxima aula, abra o MPLAB X com o programa 04_alu_demo carregado, configure a janela Watch para exibir STATUS em binário e execute passo a passo. Anote, para cada uma das três operações, quais bits do STATUS mudaram. Traga essas anotações para a sessão de tutoria; elas serão o ponto de partida para o início da primeira tarefa do Projeto Integrador.