Módulo 05: Unidade Central de Processamento — A Unidade de Controle

Neste módulo, encerro com você o estudo do processador. No módulo anterior abrimos o caminho de dados — ULA, registradores, multiplexadores, barramentos — e ficou aberta uma pergunta: quem comanda esses componentes ciclo a ciclo? Quem decide, ao ver os dezesseis bits de uma instrução, que a ULA deve somar em vez de operar AND, que o resultado vai para W e não para a RAM, que os flags Z e C devem ser atualizados mas OV deve ficar intocado? A resposta tem nome próprio: unidade de controle.

O Problema Concreto Que Abre o Módulo

Imagine que você acaba de gravar no PIC18F4550 do KIT ACEPIC PRO V8.2 o firmware do seu Projeto Integrador. O sistema de monitoramento está rodando: o ADC lê a temperatura, o LCD Sunstar 2004A mostra o valor, os botões permitem alternar entre operação, configuração e diagnóstico. Para o usuário, é um único produto. Para o silício, é uma coreografia de centenas de milhares de instruções por segundo, cada uma exigindo uma combinação diferente de fios ativados no instante exato. Quem rege essa coreografia? Por que algumas instruções gastam um ciclo e outras dois? Como o opcode binário que sai da Flash vira soma, escrita ou desvio? E como você usa esse conhecimento para escrever firmware mais rápido?

Anote esta observação: o PIC18F4550 tem setenta e cinco instruções distintas e um único subsistema interno decide, instrução após instrução, qual fio ativa em qual nanossegundo. Esse subsistema é descrito por um modelo matemático que você já viu em outra disciplina: a máquina de estados finitos. Reconhecer que um conceito de teoria da computação se materializa em silício é uma das experiências mais bonitas desta disciplina.

1. O Papel da Unidade de Controle

A analogia que melhor captura a essência desse componente é a do regente de uma orquestra. Os músicos são profissionais habilíssimos, mas sem o regente indicando entradas, pausas e dinâmicas eles produzem ruído, não música. O caminho de dados que você estudou é a orquestra: a ULA sabe somar e deslocar, o banco de registradores sabe guardar valores, os barramentos sabem transportar bits. Cada componente cumpre sua função com precisão eletrônica, mas precisa receber, a cada ciclo, sinais que digam quando agir, como se configurar e para onde mandar o resultado. Esses sinais constituem a saída fundamental da unidade de controle.

Há um aspecto que costuma escapar à percepção inicial: a unidade de controle não move bits de dados. Ela não acrescenta nada à computação que o caminho de dados não pudesse executar. O que ela fornece é organização temporal e seleção — decide quais subsistemas operam em quais momentos e com quais configurações.

flowchart LR
    IR["Instruction<br/>Register (IR)"] --> DEC["Decodificador<br/>de opcode"]
    Q["Estado interno<br/>Q (flip-flops)"] --> DEC
    STA["Flags do<br/>STATUS (Z,C,N,OV,DC)"] --> DEC
    DEC --> SIG["Vetor de sinais<br/>de controle c(t)"]
    DEC --> NXT["Próximo estado<br/>q' em Q"]
    SIG -->|"AluOp"| ALU["ULA"]
    SIG -->|"MuxSel"| MUX["Multiplexadores"]
    SIG -->|"RegWE"| REG["Banco de<br/>registradores"]
    SIG -->|"MemRD/WR"| RAM["Memória RAM"]
    SIG -->|"PCsrc"| PC["Program Counter"]
    SIG -->|"FlagWE"| STA
Figura 1: Unidade de controle como geradora de sinais a partir do IR, do estado interno e dos flags.

Formalmente, podemos descrever a unidade de controle como uma função

\mathcal{K} : IR \times Q \longrightarrow \{0,1\}^n \times Q,

em que IR é o conjunto dos opcodes possíveis, Q é o conjunto finito de estados internos e n é a quantidade total de sinais de controle gerados. A função recebe o opcode corrente e o estado interno, e devolve dois resultados acoplados: o vetor de sinais a aplicar agora e o próximo estado interno a assumir.

Para tornar isso palpável, acompanhe o que acontece em ADDWF f, d, a. O opcode tem dezesseis bits no padrão 0010\,01\,d\,a\,fffffffff, em que o bit d escolhe entre destino W (d=0) ou o File Register (d=1), o bit a escolhe entre Access Bank e o banco apontado pelo BSR, e os oito bits restantes formam o endereço f. Ao decodificar esse padrão, a unidade de controle ativa simultaneamente sinais que selecionam soma na ULA, conectam \text{Mem}[f] e W às portas de entrada, encaminham o resultado ao destino especificado por d e atualizam Z, C, DC, OV e N. Compare com ANDWF f, d, a, opcode 0001\,01\,d\,a\,fffffffff. O único sinal que muda é o código da operação, que passa de soma para AND. Os demais permanecem idênticos, com uma exceção sutil: a máscara dos flags exclui C, DC e OV — porque o AND lógico não produz carry, e os fios que alimentam esses flip-flops não recebem sinal válido. O detalhe aparentemente arbitrário do datasheet — “ANDWF afeta apenas Z e N” — é consequência direta da topologia da unidade de controle.

Pergunta para você refletir antes de continuar: a decisão entre soma e AND depende apenas do opcode. Mas, e a decisão tomada por DECFSZ f, d, a, que decrementa f e pula a próxima instrução se o resultado for zero? Essa segunda decisão depende de algo que ainda nem aconteceu quando o opcode chegou. Conclusão: a unidade de controle precisa, no caso geral, ter memória interna. Ela é sequencial, não combinacional pura.

2. Máquinas de Estados Finitos Como Modelo

Toda essa lógica sequencial é descrita matematicamente por uma máquina de estados finitos determinística, formalizada pela sêxtupla

\mathcal{M} = (Q,\, \Sigma,\, \Gamma,\, \delta,\, \lambda,\, q_0),

em que Q é o conjunto finito de estados, \Sigma é o alfabeto de entrada, \Gamma é o alfabeto de saída, \delta : Q \times \Sigma \to Q é a função de transição, \lambda é a função de saída e q_0 é o estado inicial. Há dois modelos canônicos. Na máquina de Moore, \lambda : Q \to \Gamma depende apenas do estado atual; na máquina de Mealy, \lambda : Q \times \Sigma \to \Gamma depende também da entrada corrente.

A escolha entre os dois modelos não é mera elegância acadêmica. Em Moore, como as saídas estão associadas aos estados, elas permanecem firmes durante todo o intervalo em que a máquina ocupa um dado estado. Em circuitos síncronos rápidos, essa estabilidade vale ouro: os sinais de controle ficam firmes durante o ciclo inteiro, sem glitches transitórios que flip-flops sensíveis à borda poderiam interpretar como pulsos válidos. Em Mealy, a saída pode mudar imediatamente com a entrada, abrindo a porta para glitches; o benefício compensatório é exigir menos estados. Para processadores, a escolha quase universal é Moore — e o PIC18F4550 segue exatamente esse modelo, o que se reflete na previsibilidade absoluta do timing, característica essencial em sistemas embarcados de tempo real.

stateDiagram-v2
    [*] --> FETCH
    FETCH --> DECODE: MemRead, IRWrite,<br/>PCWrite
    DECODE --> EXEC_R: tipo R
    DECODE --> EXEC_M: tipo M
    DECODE --> EXEC_B: tipo B
    EXEC_R --> WB: AluOp
    EXEC_M --> MEM_R: LOAD
    EXEC_M --> MEM_W: STORE
    EXEC_B --> BRANCH: cond verdadeira
    EXEC_B --> FETCH: cond falsa
    MEM_R --> WB_MEM
    MEM_W --> FETCH
    WB --> FETCH
    WB_MEM --> FETCH
    BRANCH --> FETCH
Figura 2: Ciclo de instrução de um processador hipotético modelado como máquina de Moore.

No módulo anterior, você estudou o ciclo de instrução como sequência de fases. Cada fase é um estado da FSM que governa a unidade de controle. Considere um processador hipotético com três classes de instruções — tipo R (entre registradores), tipo M (acessos à memória) e tipo B (desvios). A máquina parte de FETCH, transita para DECODE e dali se ramifica: R vai a EXEC_R e depois WB; M vai a EXEC_M e dali MEM_R ou MEM_W; B vai a EXEC_B e, conforme a condição, atualiza ou não o PC. Em qualquer ramo, ao fim retorna a FETCH. A tabela de transições — cada linha indicando estado atual, condição, próximo estado e sinais ativos — é o documento de projeto definitivo. No hardwired, cada linha vira termo de uma expressão booleana; no microprogramado, vira uma microinstrução em memória.

3. Controle Hardwired

O controle hardwired implementa a máquina de estados diretamente como circuito digital síncrono. Um conjunto de flip-flops armazena o estado atual codificado em binário; uma rede de portas lógicas combinacionais, alimentada pelos bits do estado atual e pelos bits relevantes do IR, calcula simultaneamente os bits do próximo estado e o vetor de sinais de controle. A cada borda ativa do clock, os flip-flops capturam o próximo estado e o ciclo se repete. O adjetivo “hardwired” vem do fato de essa lógica estar fisicamente fixada em silício: alterá-la requer refabricar o chip.

flowchart LR
    IR["IR (opcode<br/>16 bits)"] --> COMB["Lógica<br/>combinacional<br/>de próximo estado"]
    FF["Registrador<br/>de estado<br/>(m flip-flops)"] --> COMB
    COMB --> FF
    FF --> OUT["Lógica<br/>combinacional<br/>de saída (Moore)"]
    OUT --> CTRL["Sinais de controle<br/>para caminho de dados"]
    CLK["Clock"] -.-> FF
Figura 3: Estrutura geral do controle hardwired: registrador de estado, lógica combinacional de próximo estado e lógica de saída.

A síntese segue cinco etapas clássicas. Primeira: codificação dos estados. Para k estados são necessários m = \lceil \log_2 k \rceil flip-flops. A codificação binária sequencial minimiza flip-flops mas pode causar muitas comutações simultâneas. A one-hot usa um flip-flop por estado, simplificando a lógica combinacional — preferida em FPGAs. A Gray garante que estados adjacentes nas transições mais frequentes difiram em apenas um bit, minimizando glitches. Segunda: tabela de próximo estado em termos binários. Terceira: derivar, para cada bit do próximo estado e cada saída, uma expressão booleana minimizada via Karnaugh, Quine-McCluskey ou ESPRESSO. Quarta: implementar em portas lógicas. Quinta: verificar que o caminho crítico cabe no período de clock pretendido.

A vantagem fundamental do hardwired é a velocidade. Os sinais emergem de lógica combinacional dedicada com latência de poucos nanossegundos, permitindo operar próximo dos limites tecnológicos. Não há acesso à memória nem decodificação intermediária. Por isso processadores RISC modernos — ARM Cortex-A, RISC-V, o caminho rápido dos x86 — usam essa abordagem. A limitação é a imutabilidade: descoberto um bug, a única solução é refabricar o chip. O caso mais famoso é o defeito de divisão em ponto flutuante do Intel Pentium em 1994, que custou à Intel cerca de 475 milhões de dólares.

O PIC18F4550 emprega controle essencialmente hardwired. Suas setenta e cinco instruções, ortogonais e regulares, cabem confortavelmente nessa abordagem, e a previsibilidade do timing — um ciclo para a maioria, dois para desvios tomados — é garantida pela natureza determinística dos circuitos combinacionais.

4. Controle Microprogramado

Em 1951, Maurice Wilkes, em Cambridge, propôs uma solução elegante: em vez de circuitos lógicos fixos, armazenar as sequências de sinais de controle em uma memória interna e executar cada instrução da arquitetura como se fosse um pequeno programa de nível baixíssimo, escrito em uma linguagem cujas palavras são padrões de bits que comandam diretamente os fios do caminho de dados. Essa linguagem ficou conhecida como microcódigo, e a memória que a armazena, como memória de controle ou ROM de microcódigo. A ideia revolucionou o projeto de processadores entre 1950 e 1980, viabilizando o IBM System/360, o DEC VAX, o Motorola 68000 e os primeiros x86 da Intel. Sem microcódigo, seria impraticável construir hardware para suportar centenas de instruções heterogêneas.

flowchart LR
    OP["Opcode<br/>no IR"] --> MAP["Memória de<br/>mapeamento"]
    MAP --> MUAR["µAR<br/>(endereço da<br/>microinstrução)"]
    MUAR --> ROM["Memória de<br/>controle<br/>(ROM de microcódigo)"]
    ROM --> MUIR["µIR<br/>(microinstrução)"]
    MUIR --> CDP["Campos de controle<br/>para o caminho<br/>de dados"]
    MUIR --> SEQ["Microsequenciador"]
    FLAGS["Flags<br/>STATUS"] --> SEQ
    SEQ --> MUAR
Figura 4: Estrutura do controle microprogramado: opcode mapeia para µAR, que indexa a memória de controle, cuja microinstrução governa o caminho de dados e o microsequenciador.

Cinco componentes operam em harmonia. O registrador \mu AR contém o endereço da microinstrução atual. A memória de controle armazena todas as micro-rotinas, normalmente em ROM ou SRAM carregável. O \mu IR mantém a palavra recém-lida, cujos campos são roteados diretamente aos fios do caminho de dados. O microsequenciador decide a próxima microinstrução, podendo realizar incremento sequencial, desvio incondicional ou desvio condicional baseado em flags.

O formato da microinstrução fica entre dois extremos. Na microprogramação horizontal, cada bit controla diretamente um único sinal: decodificação trivial, paralelismo máximo, memória grande — o VAX usava microinstruções de até noventa e seis bits. Na vertical, os sinais são agrupados em campos compactos decodificados por pequenos decodificadores; microinstruções de dezesseis a vinte e quatro bits, ao custo de latência extra. Na prática, processadores reais usam formatos intermediários.

A flexibilidade pós-fabricação é a vantagem mais notável: atualizações de microcódigo corrigem bugs e até acrescentam instruções sem alterar uma única porta. A Intel distribui regularmente essas atualizações; grande parte das mitigações para Spectre e Meltdown, em 2018, veio por essa via, tornando seguros computadores já vendidos. A facilidade de emulação é outra capacidade: por microcódigo é possível fazer um processador implementar uma arquitetura diferente — recurso usado historicamente pela IBM para preservar compatibilidade entre mainframes ao longo de décadas. A limitação clara é o overhead: cada microciclo exige um acesso à ROM.

Os processadores modernos adotam abordagem híbrida. Núcleos Intel Core e AMD Zen traduzem x86 em \mu\text{ops} RISC internas via decodificador hardwired no caminho rápido; instruções complexas ou raras vão a um motor de microcódigo dedicado. É a regra geral da engenharia de sistemas: otimize o caminho rápido com lógica especializada e trate exceções com mecanismos flexíveis.

5. O Pipeline de Dois Estágios do PIC18F4550

O PIC18F4550 implementa o pipeline mais simples possível: dois estágios. O estágio IF (Instruction Fetch) acessa a Flash no endereço do PC, lê a palavra de dezesseis bits, deposita-a em um registrador de pipeline e incrementa o PC em dois bytes — cada instrução do PIC18 ocupa essa quantidade. O estágio EX recebe a instrução pré-buscada, decodifica-a pela unidade de controle, lê operandos, comanda a ULA, escreve o destino e atualiza flags. Os dois estágios funcionam simultaneamente: enquanto a instrução i está em EX, a i+1 já está sendo buscada em IF.

flowchart LR
    subgraph C1["Ciclo N"]
        IF1["IF: busca I_n"]
        EX0["EX: executa I_(n-1)"]
    end
    subgraph C2["Ciclo N+1"]
        IF2["IF: busca I_(n+1)"]
        EX1["EX: executa I_n"]
    end
    subgraph C3["Ciclo N+2 (desvio tomado)"]
        IFx["IF: descarta I_(n+1)<br/>busca destino"]
        EXx["EX: ocioso<br/>(bolha)"]
    end
    C1 --> C2 --> C3
Figura 5: Pipeline de dois estágios em condições ideais e o efeito de um desvio tomado, que descarta a instrução pré-buscada.

Em condições ideais, o throughput é 1/T_{cm} instruções por unidade de tempo e a latência por instrução é k \cdot T_{cm}, com k = 2. Após o preenchimento inicial, o throughput efetivo é de uma instrução por ciclo de máquina, ainda que cada instrução atravesse dois ciclos. Sem pipeline, o throughput cairia pela metade. É um ganho líquido de cerca de cem por cento, ao custo de um pequeno registrador de pipeline.

Cada ciclo de máquina subdivide-se em quatro sub-ciclos — Q1, Q2, Q3 e Q4 — com função específica. Em Q1, decodifica-se a instrução em EX e inicia-se o fetch da próxima. Em Q2, leem-se W e o File Register. Em Q3, a ULA opera e calcula flags. Em Q4, o resultado é escrito e STATUS é atualizado.

flowchart LR
    Q1["Q1<br/>Decodifica opcode<br/>Inicia fetch da<br/>próxima instrução"] --> Q2["Q2<br/>Lê operandos<br/>(W e File Register)"]
    Q2 --> Q3["Q3<br/>ULA opera<br/>Calcula flags"]
    Q3 --> Q4["Q4<br/>Escreve destino<br/>Atualiza STATUS"]
    Q4 -.->|"próximo<br/>ciclo de máquina"| Q1
Figura 6: Os quatro sub-ciclos Q1 a Q4 que compõem um ciclo de máquina do PIC18F4550.

A frequência interna máxima do PIC18F4550 com o PLL habilitado é de 48 MHz, portanto

T_{Qx} = \frac{1}{48 \times 10^6} \approx 20{,}83~\text{ns},

T_{cm} = 4 \cdot T_{Qx} \approx 83{,}33~\text{ns}.

No KIT ACEPIC PRO V8.2, o cristal externo de 20 MHz é multiplicado pelo PLL por fator efetivo de 2,4 para gerar o clock interno de 48 MHz. Esse ciclo de máquina de cerca de oitenta e três nanossegundos é o número que você usará em cálculos de tempo ao longo do Projeto Integrador.

O pipeline flui enquanto as instruções são executadas em sequência linear. O problema surge com desvios: GOTO, BRA, BZ, BNZ, CALL, RETURN, RETFIE, RCALL e os skips DECFSZ, INCFSZ, CPFSGT, BTFSC, BTFSS. Quando GOTO destino está em EX, o IF já buscou a instrução seguinte. Ao executar o GOTO, o PC vira destino e a instrução pré-buscada torna-se inválida. O PIC18 a descarta sem efeito observável e o IF inicia novo fetch. A correção custa um ciclo adicional, durante o qual EX fica ocioso. O CPI das instruções de desvio tomadas sobe de um para dois. Sendo f_b \in [0,1] a fração de desvios tomados,

\overline{\text{CPI}} = (1 - f_b) \cdot 1 + f_b \cdot 2 = 1 + f_b,

e o tempo total fica T_{\text{exec}} = N_{\text{inst}} \cdot (1 + f_b) \cdot 83{,}33~\text{ns}. Desvios não tomados não incorrem em penalidade — a instrução pré-buscada é justamente a próxima a executar. Essa assimetria é a base da discussão sobre reorganização de código que faremos adiante. Em um laço com corpo de dez instruções, f_b \approx 9{,}1\% e o CPI sobe a 1{,}091 — impacto modesto. Em um laço com corpo de duas, f_b = 50\%, CPI vira 1{,}5 e o throughput cai trinta e três por cento. Reconhecer essas situações é parte do trabalho da terceira tarefa do Projeto Integrador.

6. Timing e a Física do Clock

A operação do processador é governada pelo clock. Os flip-flops capturam novos valores apenas nas bordas ativas, tipicamente as de subida. Entre duas bordas consecutivas transcorre T_{clk}, intervalo em que toda a lógica combinacional entre dois flip-flops deve estabilizar.

Três parâmetros caracterizam cada flip-flop D: t_{su} (setup time, estabilidade mínima antes da borda), t_h (hold time, estabilidade mínima após a borda) e t_{c\to q} (clock-to-Q, atraso entre a borda e a propagação à saída). Para operação correta,

T_{clk} \geq t_{c\to q} + t_{\text{logic}} + t_{\text{routing}} + t_{su},

e a frequência máxima resulta diretamente:

f_{\max} = \frac{1}{t_{c\to q} + t_{\text{logic, crit}} + t_{\text{routing, crit}} + t_{su}}.

flowchart LR
    FF1["Flip-flop<br/>origem"] -->|"t_c→q"| LOGIC["Lógica<br/>combinacional<br/>(t_logic + t_routing)"]
    LOGIC -->|"deve estabilizar<br/>antes de t_su"| FF2["Flip-flop<br/>destino"]
    CLK["Clock<br/>período T_clk"] -.-> FF1
    CLK -.-> FF2
Figura 7: O caminho combinacional entre dois flip-flops e seu orçamento temporal.

O caminho crítico — o combinacional com maior atraso total — determina o limite global. Em unidades de controle hardwired, ele costuma passar pela lógica de próximo estado. A Microchip dimensionou os caminhos críticos do PIC18F4550 para estabilizar com folga dentro de 20{,}83 ns nas piores condições de temperatura e tensão. Para o engenheiro de firmware, a consequência é simples: dentro da especificação, o chip funciona; fora dela, surgem erros esporádicos e travamentos aleatórios.

7. ISA versus Microarquitetura

Um dos princípios mais poderosos da engenharia de computadores é a separação entre interface e implementação, formalizada pela distinção entre ISA (arquitetura do conjunto de instruções) e microarquitetura. A ISA é o contrato entre software e hardware: especifica quais instruções existem, suas semânticas, formato binário, registradores visíveis e comportamento em situações excepcionais. A microarquitetura é a implementação: largura de barramentos, profundidade de pipeline, organização de caches, forma da unidade de controle, número de unidades funcionais.

flowchart TB
    SW["Software (firmware,<br/>compilador, assembler)"] --> ISA["ISA<br/>Contrato imutável:<br/>75 instruções do PIC18,<br/>opcodes, semântica,<br/>registradores visíveis"]
    ISA --> UA1["Microarquitetura A<br/>PIC18F4550 atual:<br/>hardwired, pipeline 2 estágios"]
    ISA --> UA2["Microarquitetura B<br/>hipotética: pipeline 5 estágios,<br/>microprogramada, 100 MHz"]
    UA1 --> HW1["Silício produzido<br/>em 2024"]
    UA2 --> HW2["Silício hipotético<br/>do futuro"]
Figura 8: A ISA como contrato estável; microarquiteturas diferentes podem implementar a mesma ISA.

A microarquitetura pode ser completamente redesenhada sem invalidar software, desde que continue implementando fielmente a mesma ISA. É talvez o mecanismo mais importante pelo qual a indústria evoluiu continuamente o desempenho dos processadores. O exemplo mais espetacular é o x86. A ISA foi definida em 1978 com o 8086; software compilado para o 8086 ainda executa em um Intel Core i9 de 2024, mais de quarenta e cinco anos depois. Internamente, o Core i9 nada tem do 8086: traduz x86 em \mu\text{ops} RISC, executa várias por ciclo em ordem alterada, tem três níveis de cache e predição de desvios sofisticadíssima. O contrato ISA, porém, é preservado meticulosamente.

Para o PIC18F4550 a separação é igualmente real. O .hex gerado com XC8 ou MPASM é uma sequência de palavras da ISA do PIC18; o pipeline de dois estágios, a ULA de oito bits, o controle hardwired são detalhes de implementação. Se a Microchip lançasse amanhã uma versão com pipeline de cinco estágios e cem megahertz preservando a ISA, seu firmware funcionaria sem recompilação.

Internamente, mesmo no PIC18 hardwired, cada instrução é decomposta em microoperações que mapeiam diretamente nos sub-ciclos Q1 a Q4. Para ADDWF f, F, em Q1 ocorre a decodificação e o cálculo do endereço efetivo; em Q2 leem-se \text{Mem}[f] e W; em Q3 a ULA soma e calcula flags; em Q4 o resultado vai ao destino. O programador percebe uma única instrução; a unidade de controle orquestra internamente quatro microoperações. Essa decomposição transparente é o que concilia simplicidade no modelo de programação com eficiência do hardware.

8. Projetando FSMs Para Firmware

Tudo o que vimos sobre máquinas de estados tem aplicação direta no firmware do Projeto Integrador. Implementar uma FSM em C para controlar modos de operação é, do ponto de vista lógico, o mesmo problema que o engenheiro de hardware enfrenta ao projetar uma unidade de controle. Mudam apenas as ferramentas — software no lugar de silício — e as restrições — RAM e ciclos no lugar de área e atraso combinacional.

A forma canônica em C combina dois elementos. Uma variável guarda o estado atual, idealmente como tipo enumerado. Um switch determina, a partir do estado e da entrada, qual ação executar (função \lambda de Moore) e qual será o próximo estado (função \delta). Alternativa mais escalável substitui o switch por uma tabela bidimensional indexada por estado e entrada, O(1) por chamada. A correspondência com hardware é direta: ambas realizam \delta : Q \times \Sigma \to Q — em hardware por portas, em software por acesso a array.

stateDiagram-v2
    [*] --> ESPERA
    ESPERA --> OPERANDO: BTN_OK
    ESPERA --> DIAGNOSTICO: BTN_MENU
    OPERANDO --> CONFIG: BTN_MENU
    OPERANDO --> ESPERA: BTN_LONG
    CONFIG --> OPERANDO: BTN_OK
    CONFIG --> ESPERA: BTN_CANCEL
    DIAGNOSTICO --> ESPERA: BTN_CANCEL
    DIAGNOSTICO --> OPERANDO: BTN_OK
Figura 9: FSM de modos de operação do Projeto Integrador implementada sobre o KIT ACEPIC PRO V8.2.

A seguir, as duas versões do código que implementam, em C e em assembly do PIC18F4550, a FSM de modos de operação do sistema de monitoramento sobre o KIT ACEPIC PRO V8.2.

05_fsm_modos.c
/*
 * Modulo 05 - Projeto Integrador
 * FSM (Maquina de Estados Finitos) de Moore para gerenciar
 * modos de operacao do sistema de monitoramento construido
 * sobre o KIT ACEPIC PRO V8.2 com PIC18F4550 e LCD Sunstar 2004A.
 *
 * Estados:
 *   ST_ESPERA       -> aguarda interacao; LCD exibe boas-vindas
 *   ST_OPERANDO     -> ciclo normal: leitura ADC + atualizacao LCD
 *   ST_CONFIG       -> ajuste de parametros pelo usuario
 *   ST_DIAGNOSTICO  -> mostra contadores internos e versao do firmware
 *
 * Entradas (filtradas por debounce em ISR de Timer0):
 *   IN_BTN_OK       -> confirmar / avancar
 *   IN_BTN_MENU     -> abrir menu / alternar
 *   IN_BTN_CANCEL   -> cancelar / voltar para ESPERA
 *   IN_BTN_LONG     -> OK pressionado por mais de 1 s
 *
 * Esta implementacao didatica privilegia a leitura passo a passo
 * sobre a otimizacao de codigo. Tres fases sao executadas a cada
 * passo: acao de saida do estado atual (modelo Moore), calculo do
 * proximo estado pela funcao de transicao delta, e atualizacao
 * atomica da variavel de estado.
 */
#include <xc.h>
#include <stdint.h>

typedef enum {
    ST_ESPERA       = 0,
    ST_OPERANDO     = 1,
    ST_CONFIG       = 2,
    ST_DIAGNOSTICO  = 3,
    NUM_ESTADOS     = 4
} Estado_t;

typedef enum {
    IN_NENHUMA      = 0,
    IN_BTN_OK       = 1,
    IN_BTN_MENU     = 2,
    IN_BTN_CANCEL   = 3,
    IN_BTN_LONG     = 4,
    NUM_ENTRADAS    = 5
} Entrada_t;

static Estado_t estado_atual = ST_ESPERA;

/* Acoes de saida (funcoes Moore: dependem apenas do estado) */
static void acao_espera(void);
static void acao_operando(void);
static void acao_config(void);
static void acao_diagnostico(void);

/*
 * fsm_passo:
 * Deve ser chamada periodicamente pelo laco principal, tipicamente
 * a cada 20 ms. O argumento 'entrada' precisa ser uma copia atomica
 * do evento mais recente, capturado antes desta chamada para evitar
 * que o registrador de botoes mude no meio do processamento.
 */
void fsm_passo(Entrada_t entrada) {
    Estado_t proximo = estado_atual;

    /* Fase 1: executar acao do estado atual (saida Moore) */
    switch (estado_atual) {
        case ST_ESPERA:       acao_espera();       break;
        case ST_OPERANDO:     acao_operando();     break;
        case ST_CONFIG:       acao_config();       break;
        case ST_DIAGNOSTICO:  acao_diagnostico();  break;
        default:
            proximo = ST_ESPERA;  /* recuperacao segura */
            break;
    }

    /* Fase 2: calcular proximo estado pela funcao de transicao */
    switch (estado_atual) {
        case ST_ESPERA:
            if (entrada == IN_BTN_OK)     proximo = ST_OPERANDO;
            if (entrada == IN_BTN_MENU)   proximo = ST_DIAGNOSTICO;
            break;
        case ST_OPERANDO:
            if (entrada == IN_BTN_MENU)   proximo = ST_CONFIG;
            if (entrada == IN_BTN_LONG)   proximo = ST_ESPERA;
            break;
        case ST_CONFIG:
            if (entrada == IN_BTN_OK)     proximo = ST_OPERANDO;
            if (entrada == IN_BTN_CANCEL) proximo = ST_ESPERA;
            break;
        case ST_DIAGNOSTICO:
            if (entrada == IN_BTN_CANCEL) proximo = ST_ESPERA;
            if (entrada == IN_BTN_OK)     proximo = ST_OPERANDO;
            break;
        default:
            break;
    }

    /* Fase 3: atualizar estado */
    estado_atual = proximo;
}

/* Implementacao das acoes de saida */
static void acao_espera(void) {
    /* LCD: "Sistema Pronto" / "Pressione OK" */
    LATB &= 0xF0;            /* todos os LEDs inferiores apagados */
}

static void acao_operando(void) {
    /* Leitura ADC + atualizacao LCD ocorrem fora desta acao didatica */
    LATBbits.LATB0 = 1;      /* LED de status: aceso */
    LATBbits.LATB1 = 0;
    LATBbits.LATB2 = 0;
    LATBbits.LATB3 = 0;
}

static void acao_config(void) {
    /* Pisca os 4 LEDs inferiores para sinalizar modo de ajuste */
    LATB ^= 0x0F;
}

static void acao_diagnostico(void) {
    /* Todos os LEDs inferiores acesos */
    LATB |= 0x0F;
}
05_fsm_modos.asm
;------------------------------------------------------------
; Modulo 05 - Projeto Integrador
; FSM didatica em assembly PIC18F4550.
;
; A variavel ESTADO ocupa o endereco 0x20 (Access Bank).
; Codificacao dos estados (2 bits suficientes, mas usamos 1 byte
; para clareza):
;   0 = ST_ESPERA
;   1 = ST_OPERANDO
;   2 = ST_CONFIG
;   3 = ST_DIAGNOSTICO
;
; A variavel ENTRADA (0x21) contem o evento atual:
;   0 = IN_NENHUMA
;   1 = IN_BTN_OK
;   2 = IN_BTN_MENU
;   3 = IN_BTN_CANCEL
;   4 = IN_BTN_LONG
;
; A rotina fsm_passo nao executa as acoes Moore (delegadas a
; sub-rotinas externas acao_espera, acao_operando, ...);
; concentra-se na funcao de transicao delta(estado, entrada).
;------------------------------------------------------------

#include <p18f4550.inc>

    GLOBAL  fsm_passo
    EXTERN  acao_espera, acao_operando, acao_config, acao_diagnostico

    UDATA_ACS
ESTADO   res 1
ENTRADA  res 1

    CODE

;------------------------------------------------------------
; fsm_passo:
;   Le ESTADO, executa acao Moore correspondente, calcula
;   proximo estado em funcao de ENTRADA, atualiza ESTADO.
;------------------------------------------------------------
fsm_passo:
    ; Fase 1: despachar acao do estado atual
    MOVF    ESTADO, W, 0          ; W <- ESTADO
    BZ      acao_st0              ; estado 0 -> ST_ESPERA
    DECF    WREG, W, 0
    BZ      acao_st1              ; estado 1 -> ST_OPERANDO
    DECF    WREG, W, 0
    BZ      acao_st2              ; estado 2 -> ST_CONFIG
    BRA     acao_st3              ; estado 3 -> ST_DIAGNOSTICO

acao_st0:
    RCALL   acao_espera
    BRA     calc_proximo
acao_st1:
    RCALL   acao_operando
    BRA     calc_proximo
acao_st2:
    RCALL   acao_config
    BRA     calc_proximo
acao_st3:
    RCALL   acao_diagnostico

calc_proximo:
    ; Funcao de transicao: switch (ESTADO) { case x: switch (ENTRADA) ... }
    MOVF    ESTADO, W, 0
    BZ      trans_st0
    DECF    WREG, W, 0
    BZ      trans_st1
    DECF    WREG, W, 0
    BZ      trans_st2
    BRA     trans_st3

trans_st0:                        ; ST_ESPERA
    MOVLW   1                     ; IN_BTN_OK ?
    CPFSEQ  ENTRADA, 0
    BRA     chk_st0_menu
    MOVLW   1                     ; proximo = ST_OPERANDO
    MOVWF   ESTADO, 0
    RETURN
chk_st0_menu:
    MOVLW   2                     ; IN_BTN_MENU ?
    CPFSEQ  ENTRADA, 0
    RETURN
    MOVLW   3                     ; proximo = ST_DIAGNOSTICO
    MOVWF   ESTADO, 0
    RETURN

trans_st1:                        ; ST_OPERANDO
    MOVLW   2                     ; IN_BTN_MENU ?
    CPFSEQ  ENTRADA, 0
    BRA     chk_st1_long
    MOVLW   2                     ; proximo = ST_CONFIG
    MOVWF   ESTADO, 0
    RETURN
chk_st1_long:
    MOVLW   4                     ; IN_BTN_LONG ?
    CPFSEQ  ENTRADA, 0
    RETURN
    CLRF    ESTADO, 0             ; proximo = ST_ESPERA
    RETURN

trans_st2:                        ; ST_CONFIG
    MOVLW   1                     ; IN_BTN_OK ?
    CPFSEQ  ENTRADA, 0
    BRA     chk_st2_cancel
    MOVLW   1                     ; proximo = ST_OPERANDO
    MOVWF   ESTADO, 0
    RETURN
chk_st2_cancel:
    MOVLW   3                     ; IN_BTN_CANCEL ?
    CPFSEQ  ENTRADA, 0
    RETURN
    CLRF    ESTADO, 0             ; proximo = ST_ESPERA
    RETURN

trans_st3:                        ; ST_DIAGNOSTICO
    MOVLW   3                     ; IN_BTN_CANCEL ?
    CPFSEQ  ENTRADA, 0
    BRA     chk_st3_ok
    CLRF    ESTADO, 0             ; proximo = ST_ESPERA
    RETURN
chk_st3_ok:
    MOVLW   1                     ; IN_BTN_OK ?
    CPFSEQ  ENTRADA, 0
    RETURN
    MOVLW   1                     ; proximo = ST_OPERANDO
    MOVWF   ESTADO, 0
    RETURN

    END

Quatro armadilhas recorrentes consomem horas de depuração. Primeira: estado sem transição definida para todas as entradas — defina sempre, como padrão, a permanência no estado atual ou transição a um estado seguro. Segunda: inconsistência entre diagrama e código — transcreva o diagrama em tabela antes de escrever uma linha. Terceira: leitura inconsistente de entradas — leia todas no início e trabalhe sobre um snapshot. Quarta: misturar debouncing com lógica de transição — mantenha o debounce em ISR de Timer0 e entregue à FSM apenas eventos filtrados.

Três técnicas de otimização para o pipeline merecem destaque. A primeira é a programação sem desvios: substituir um if-else por uma expressão aritmética. O exemplo canônico é o valor absoluto de um inteiro de oito bits com sinal — a versão ingênua custa dois ciclos em entradas negativas; a versão sem desvio usa (x \oplus \text{máscara}) - \text{máscara}, em que a máscara vem de deslocamento aritmético, com custo fixo. A segunda é a expansão de laços: para laços com poucas iterações e corpo simples, expandir manualmente elimina os desvios de retorno, ao custo de aumento no tamanho do código. A terceira é a reorganização do ramo de teste para que o caso frequente caia no caminho não tomado: invertendo if (condicao) { raro; } else { frequente; } para if (!condicao) { frequente; } else { raro; }, o caso frequente passa a não pagar penalidade.

A seguir, as duas versões de código que ilustram essas técnicas aplicadas ao processamento de leituras do ADC no Projeto Integrador.

05_otimiza_pipeline.c
/*
 * Modulo 05 - Projeto Integrador
 * Tres tecnicas de otimizacao para minimizar penalidades do
 * pipeline de dois estagios do PIC18F4550:
 *
 *   1. Branchless programming  - elimina desvio condicional via
 *                                expressao aritmetica equivalente
 *   2. Loop unrolling          - elimina o desvio de retorno do
 *                                laco para iteracoes pequenas
 *   3. Reorganizacao do ramo   - coloca o caso frequente no ramo
 *                                "nao tomado", evitando o ciclo
 *                                extra de descarte da instrucao
 *                                pre-buscada
 *
 * Cada bloco apresenta a versao ingenua (com desvios) e a versao
 * otimizada (sem desvios ou com menos desvios tomados).
 */
#include <xc.h>
#include <stdint.h>

#define TAM_BUFFER 8
int8_t buffer_temp[TAM_BUFFER] = { 22, -5, 18, 31, -2, 15, 28, -8 };

/* ------------------------------------------------------------- */
/* Tecnica 1: valor absoluto sem desvio                          */
/* ------------------------------------------------------------- */
int8_t abs_com_desvio(int8_t x) {
    if (x < 0) return (int8_t)(-x);
    return x;
}

/*
 * Para int8_t em complemento de 2:
 *   mascara = x >> 7   -> 0x00 se x >= 0, 0xFF se x < 0
 *   (x XOR mascara) - mascara devolve |x| sem nenhum desvio.
 */
int8_t abs_sem_desvio(int8_t x) {
    int8_t mascara = (int8_t)(x >> 7);
    return (int8_t)((x ^ mascara) - mascara);
}

/* ------------------------------------------------------------- */
/* Tecnica 2: soma dos 4 primeiros elementos do buffer           */
/* ------------------------------------------------------------- */
int16_t soma_4_com_loop(void) {
    int16_t soma = 0;
    uint8_t i;
    for (i = 0; i < 4; i++) {
        soma = (int16_t)(soma + buffer_temp[i]);
    }
    return soma;
}

int16_t soma_4_unrolled(void) {
    return (int16_t)((int16_t)buffer_temp[0] + buffer_temp[1]
                   + buffer_temp[2]          + buffer_temp[3]);
}

/* ------------------------------------------------------------- */
/* Tecnica 3: alerta de temperatura fora dos limites             */
/* ------------------------------------------------------------- */
#define TEMP_LIMITE_MIN  (-10)
#define TEMP_LIMITE_MAX   (40)

void processa_temperatura(int8_t temp) {
    /*
     * Em operacao normal, ~90% das amostras estao dentro do
     * limite. A condicao composta (temp < MIN || temp > MAX)
     * sera, portanto, falsa na maior parte do tempo, deixando
     * o ramo IF como "nao tomado" - exatamente o caminho sem
     * penalidade de pipeline no PIC18F4550.
     */
    if (temp < TEMP_LIMITE_MIN || temp > TEMP_LIMITE_MAX) {
        LATBbits.LATB3 = 1;   /* caso raro: aciona LED de alerta */
    } else {
        LATBbits.LATB3 = 0;   /* caso frequente: LED apagado */
    }
}
05_otimiza_pipeline.asm
;------------------------------------------------------------
; Modulo 05 - Otimizacao manual em assembly PIC18F4550.
;
; abs8: valor absoluto de int8_t sem nenhum desvio condicional.
;   Entrada:  W contem o valor x (8 bits, complemento de 2)
;   Saida:    W contem |x|
;
; A ideia eh a mesma do codigo C: propagar o bit de sinal para
; gerar uma mascara (0x00 ou 0xFF) e calcular (x XOR m) - m.
;------------------------------------------------------------

#include <p18f4550.inc>

    GLOBAL  abs8, soma4

    UDATA_ACS
TMP_M    res 1
TMP_X    res 1
BUF      res 4    ; buffer_temp[0..3] (somente para soma4)

    CODE

;------------------------------------------------------------
; abs8: branchless, custo fixo, sem penalidade de pipeline.
;------------------------------------------------------------
abs8:
    MOVWF   TMP_X, 0           ; salva x
    BTFSS   TMP_X, 7, 0        ; bit 7 = bit de sinal
    GOTO    abs8_pos           ; x >= 0 -> mascara = 0
    SETF    TMP_M, 0           ; x <  0 -> mascara = 0xFF
    GOTO    abs8_aplica
abs8_pos:
    CLRF    TMP_M, 0
abs8_aplica:
    MOVF    TMP_X, W, 0        ; W <- x
    XORWF   TMP_M, W, 0        ; W <- x XOR mascara
    SUBFWB  TMP_M, W, 0        ; W <- (x XOR mascara) - mascara - !C
    RETURN

;------------------------------------------------------------
; soma4: unrolled - soma os 4 primeiros bytes de BUF em
; complemento de 2 estendido para 16 bits.
;   Saida: PRODH:PRODL = soma assinada de 16 bits
;
; Sem laco -> nenhum desvio de retorno, nenhuma penalidade.
;------------------------------------------------------------
soma4:
    CLRF    PRODH, 0
    MOVF    BUF+0, W, 0
    MOVWF   PRODL, 0
    BTFSC   WREG, 7
    SETF    PRODH, 0           ; extensao de sinal manual

    MOVF    BUF+1, W, 0
    ADDWF   PRODL, F, 0
    MOVLW   0x00
    BTFSC   BUF+1, 7
    MOVLW   0xFF
    ADDWFC  PRODH, F, 0

    MOVF    BUF+2, W, 0
    ADDWF   PRODL, F, 0
    MOVLW   0x00
    BTFSC   BUF+2, 7
    MOVLW   0xFF
    ADDWFC  PRODH, F, 0

    MOVF    BUF+3, W, 0
    ADDWF   PRODL, F, 0
    MOVLW   0x00
    BTFSC   BUF+3, 7
    MOVLW   0xFF
    ADDWFC  PRODH, F, 0

    RETURN

    END

9. Lendo o Datasheet Com Olhos de Arquiteto

A tabela de instruções do PIC18F4550 no datasheet é, na verdade, uma especificação direta do comportamento da unidade de controle. Quando você lê que ADDWF realiza W + f \to \text{destino}, gasta um ciclo e atualiza Z, C, DC, OV e N, está lendo quatro fatos: o código enviado à ULA seleciona soma; os multiplexadores conectam W e f às entradas; o sinal de habilitação de escrita vai para W ou f conforme d; a máscara dos flags inclui os cinco. Quando lê que ANDWF afeta apenas Z e N, descobre que a máscara, naquele caso, exclui C, DC e OV — não por capricho, mas porque o AND não produz carry.

O registrador STATUS como interface ULA-controle

O registrador STATUS é o ponto físico onde a saída de flags da ULA encontra a lógica que avalia condições para desvios. C marca carry/borrow sem sinal; DC marca carry do nibble inferior, útil em BCD; Z indica resultado zero; OV indica overflow com sinal; N é o bit sete do resultado.

Em rotinas de interrupção, qualquer instrução da ISR que modifique flags corrompe o contexto interrompido. A boa prática é salvar STATUS e W no início de toda ISR e restaurá-los antes do retorno. O PIC18 oferece registradores sombra WS, STATUSS e BSRS que automatizam isso para a interrupção de alta prioridade; para a de baixa, o salvamento é responsabilidade explícita do programador.

10. Síntese Comparativa e Volta ao Problema Inicial

Ao chegarmos ao fim, você tem condições de apreciar com profundidade a comparação entre as duas abordagens.

Critério Hardwired Microprogramado
Velocidade Máxima (lógica combinacional pura) Menor (acesso à memória de controle)
Flexibilidade após fabricação Nenhuma Alta (atualização de microcódigo)
ISAs simples e RISC Excelente Overhead desnecessário
ISAs complexas e CISC Impraticável Excelente
Correção de bugs Refabricar o chip Atualizar microcódigo
Área para ISA simples Menor Maior (ROM de controle)
Latência por instrução simples Mínima Maior (microciclos extras)
Exemplos ARM, RISC-V, MIPS, PIC x86 original, VAX, mainframes IBM
Processadores modernos Fast path RISC interno Engine para instruções complexas
mindmap
    root((Unidade de Controle))
        Modelo Formal
            FSM Moore
            FSM Mealy
            Sêxtupla Q Σ Γ δ λ q0
        Implementação
            Hardwired
                Rápido
                Imutável
                RISC PIC18
            Microprogramado
                Flexível
                Microcódigo em ROM
                CISC x86 VAX
        Manifestação no PIC18
            Pipeline 2 estágios
            Sub-ciclos Q1-Q4
            48 MHz interno
        Separações conceituais
            ISA contrato
            Microarquitetura implementação
        Aplicação em firmware
            FSM em C com switch
            Otimização branchless
            Desenrolar laços
Figura 10: Síntese dos conceitos do módulo organizados em torno da unidade de controle.

A lição mais importante é que a pergunta “qual abordagem é melhor?” não tem sentido em abstrato. Para um microcontrolador de oito bits com ISA regular como o PIC18F4550, o hardwired é a escolha natural — velocidade máxima, área mínima, previsibilidade absoluta. Para CISC complexo, o microprogramado foi historicamente indispensável. Para processadores de alto desempenho contemporâneos, a hibridização casa o melhor das duas filosofias.

Voltemos ao problema inicial. Tenho condições agora de responder com precisão: o opcode é decodificado pela unidade de controle hardwired do PIC18, modelada como FSM de Moore, que gera o vetor de sinais a cada ciclo de máquina de cerca de oitenta e três nanossegundos; algumas instruções gastam dois ciclos porque o pipeline de dois estágios introduz uma bolha quando um desvio é tomado e a instrução pré-buscada é descartada; você usa esse conhecimento aplicando programação sem desvios em laços críticos, expandindo laços curtos e colocando o ramo frequente no caminho não tomado.

As três tarefas do Projeto Integrador deste módulo aplicam diretamente esses conceitos. A primeira pede uma FSM de modos de operação — os códigos da seção 8 são referência canônica, ilustrando as três fases de toda FSM bem projetada: ação Moore, transição \delta, atualização atômica. A segunda pede análise do pipeline e identificação de situações em que o prefetch falha; a seção 5 deu o ferramental, com \overline{\text{CPI}} = 1 + f_b. A terceira pede otimização para minimizar penalidades, e as três técnicas apresentadas formam o repertório. Documente cada decisão no diário do projeto. A documentação é o instrumento por meio do qual você reconstrói, no laboratório, a cadeia de raciocínio que vai da matemática das máquinas de Moore à medição em osciloscópio dos ciclos gastos por um trecho de assembly.

No Módulo 06, ampliaremos o pipeline para o modelo clássico de cinco estágios — IF, ID, EX, MEM, WB. Você derivará o ganho teórico de throughput em função do número de estágios e estudará os registradores de pipeline que isolam um estágio do seguinte. Todo o conhecimento sobre o pipeline de dois estágios adquirido aqui é a fundação direta. Antes da próxima aula, esboce no papel como você imagina um pipeline de cinco estágios: por que cinco em vez de dois? O que cada estágio faria? Quais informações precisariam ser propagadas? Esse esboço, mesmo parcial, torna o próximo módulo muito mais acessível, porque você chegará a ele com perguntas formuladas e hipóteses a confirmar.