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

Esta é a versão ultra-enxuta do Módulo 05, para revisão rápida antes da aula, da prova ou de uma tutoria do Projeto Integrador. O aprofundamento, com demonstrações, contraexemplos e referências históricas, está em 05_material.qmd e em 05_livro.qmd.

No módulo anterior abrimos o caminho de dados: ULA, registradores, multiplexadores, barramentos. Sobrou uma pergunta de regência: quem decide, ao ver o opcode de dezesseis bits, que a ULA deve somar em vez de operar AND, que o resultado vai para W e não para a RAM, que Z e C devem ser atualizados mas OV deve ficar intocado? A resposta tem nome próprio: unidade de controle. Volto a ela com você agora, em versão de revisão.

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 1: Mapa do módulo, da unidade de controle ao firmware do Projeto Integrador.

O Papel da Unidade de Controle

Penso na unidade de controle como o regente de uma orquestra. Os músicos — ULA, banco de registradores, barramentos — são profissionais habilíssimos, mas sem alguém indicando entradas, pausas e dinâmicas produzem ruído. A unidade de controle não move bits de dados; ela fornece organização temporal e seleção, decidindo quais subsistemas operam em quais momentos e com quais configurações. Formalmente, posso descrevê-la como uma função \mathcal{K} : IR \times Q \longrightarrow \{0,1\}^n \times Q, em que IR é o opcode corrente, Q é o estado interno finito e n é a quantidade de sinais de controle. A função devolve dois resultados acoplados: o vetor de sinais a aplicar agora e o próximo estado a assumir.

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 2: Unidade de controle como geradora de sinais a partir do IR, do estado interno e dos flags.

Para tornar isso palpável, acompanhe ADDWF f, d, a no PIC18F4550. O opcode tem padrão 0010\,01\,d\,a\,fffffffff; o bit d escolhe entre W e o File Register, o bit a escolhe entre Access Bank e o banco apontado pelo BSR. Ao decodificar, a unidade de controle ativa simultaneamente os sinais que selecionam soma na ULA, conectam \text{Mem}[f] e W às entradas, encaminham o resultado ao destino e atualizam Z, C, DC, OV e N. Em ANDWF, opcode 0001\,01\,d\,a\,fffffffff, muda apenas o código da operação; a máscara dos flags, porém, exclui C, DC e OV — não por capricho, mas porque o AND lógico não produz carry, e os fios que alimentariam esses flip-flops não recebem sinal válido. O detalhe aparentemente arbitrário do datasheet é consequência direta da topologia da unidade de controle.

A decisão entre soma e AND depende apenas do opcode. Mas, e a decisão de 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 sequer aconteceu quando o opcode chegou. Conclusão obrigatória: a unidade de controle precisa, no caso geral, ter memória interna. Ela é sequencial, não combinacional pura.

Máquinas de Estados Finitos como Modelo

A lógica sequencial da unidade de controle é descrita por uma máquina de estados finitos determinística, \mathcal{M} = (Q, \Sigma, \Gamma, \delta, \lambda, q_0), com \delta : Q \times \Sigma \to Q e duas alternativas para a função de saída \lambda. Em Moore, \lambda depende apenas do estado atual; em Mealy, depende também da entrada corrente. A escolha não é mera elegância: em Moore as saídas permanecem firmes durante todo o intervalo em que a máquina ocupa um estado, sem glitches transitórios que flip-flops sensíveis à borda interpretariam como pulsos válidos; em Mealy a saída pode mudar imediatamente com a entrada, abrindo a porta para glitches, em troca de menos estados. Para processadores, a escolha quase universal é Moore — e o PIC18F4550 segue exatamente esse modelo, o que se reflete na previsibilidade absoluta de seu timing.

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 3: Ciclo de instrução de um processador hipotético modelado como máquina de Moore.

Cada fase do ciclo de instrução vira um estado da FSM. Em um processador hipotético com três classes de instruções — tipo R entre registradores, tipo M com memória, tipo B de desvios — a máquina parte de FETCH, transita para DECODE e ramifica: R vai a EXEC_R e 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. A tabela de transições, com colunas estado atual, condição, próximo estado e sinais ativos, é o documento de projeto definitivo: no hardwired ela vira expressões booleanas; no microprogramado, vira microinstruções em memória.

Controle Hardwired versus Microprogramado

O controle hardwired implementa a FSM diretamente em silício: flip-flops armazenam o estado em binário, uma rede de portas lógicas combinacionais alimentada por estado atual e bits do IR calcula simultaneamente o próximo estado e o vetor de sinais, e a cada borda ativa do clock os flip-flops capturam o novo estado. Sua vantagem é a velocidade — sinais emergem com latência de poucos nanossegundos, sem acesso à memória. Sua limitação é a imutabilidade: descoberto um bug, a única solução é refabricar o chip. O caso emblemático é o defeito de divisão em ponto flutuante do Intel Pentium em 1994, que custou cerca de quatrocentos e setenta e cinco milhões de dólares.

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 4: Estrutura do controle hardwired e elementos do controle microprogramado.

Em 1951, Maurice Wilkes propôs o caminho alternativo: armazenar as sequências de sinais em uma memória interna e executar cada instrução da arquitetura como um pequeno programa de nível baixíssimo. Essa linguagem ficou conhecida como microcódigo. Cinco componentes operam em harmonia: o \mu AR contém o endereço da microinstrução atual; a memória de controle armazena as micro-rotinas; o \mu IR mantém a palavra recém-lida e roteia seus campos aos fios do caminho de dados; o microsequenciador decide a próxima microinstrução, com incremento sequencial, desvio incondicional ou desvio condicional baseado em flags. O formato fica entre dois extremos: na horizontal, cada bit controla um sinal — decodificação trivial, paralelismo máximo, memória grande; na vertical, sinais são agrupados em campos compactos, ao custo de latência extra. A flexibilidade pós-fabricação é o ganho mais notável: atualizações de microcódigo corrigem bugs e acrescentam instruções. Boa parte das mitigações para Spectre e Meltdown em 2018 chegou por essa via.

O PIC18F4550 emprega controle essencialmente hardwired. Suas setenta e cinco instruções, ortogonais e regulares, cabem confortavelmente nessa abordagem. Já processadores modernos adotam híbrido: o Intel Core e o AMD Zen traduzem x86 em \mu\text{ops} RISC no caminho rápido por decodificador hardwired e mandam instruções complexas ou raras a um motor de microcódigo — a regra geral da engenharia de sistemas, otimize o caminho frequente e trate exceções com flexibilidade.

O Pipeline de Dois Estágios do PIC18F4550

O PIC18F4550 implementa o pipeline mínimo: dois estágios. IF (Instruction Fetch) acessa a Flash no endereço do PC, lê dezesseis bits, deposita-os em um registrador de pipeline e incrementa o PC em dois bytes. EX recebe a instrução pré-buscada, decodifica-a, lê operandos, comanda a ULA, escreve o destino e atualiza flags. Enquanto a instrução i está em EX, a i+1 já está sendo buscada em IF. O throughput efetivo, após o preenchimento inicial, é de uma instrução por ciclo de máquina, ainda que cada instrução atravesse dois ciclos — ganho líquido próximo de cem por cento, ao custo de um pequeno registrador.

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.

Cada ciclo de máquina subdivide-se em quatro sub-ciclos Q1, Q2, Q3, Q4. 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. Com o PLL habilitado, o PIC18F4550 atinge frequência interna de 48 MHz, donde

T_{Qx} = \frac{1}{48 \times 10^6} \approx 20{,}83~\text{ns}, \qquad 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 esse clock interno. Esse ciclo de máquina de cerca de oitenta e três nanossegundos é o número que você usará em todos os cálculos de tempo do Projeto Integrador.

O pipeline flui enquanto as instruções são executadas em sequência linear. O problema aparece com desvios tomados — GOTO, BRA, BZ, CALL, RETURN, e os skips DECFSZ, BTFSC e companhia. Quando GOTO destino está em EX, o IF já buscou a próxima instrução; ao executar o GOTO o PC vira destino e a instrução pré-buscada é descartada. O CPI das instruções de desvio tomadas sobe de um para dois. Sendo f_b a fração de desvios tomados,

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

Desvios não tomados não incorrem em penalidade, pois a instrução pré-buscada é justamente a próxima a executar. Em um laço com corpo de dez instruções, f_b \approx 9{,}1\% e o CPI sobe modestamente a 1{,}091; em um laço com corpo de duas, f_b = 50\% e o CPI vira 1{,}5, derrubando o throughput em um terço. Reconhecer essas situações é parte da terceira tarefa do Projeto Integrador.

Timing, Caminho Crítico e a Distinção ISA versus Microarquitetura

A operação síncrona impõe restrições temporais inelutáveis. Cada flip-flop D tem t_{su} (setup), t_h (hold) e t_{c\to q} (clock-to-Q). Para operação correta entre dois flip-flops,

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

e o caminho crítico — combinacional com maior atraso — determina f_{\max}. Em unidades de controle hardwired, o caminho crítico costuma passar pela lógica de próximo estado. A Microchip dimensionou o 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 é direta: dentro da especificação o chip funciona; fora dela, surgem travamentos aleatórios.

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 6: Caminho combinacional entre dois flip-flops e seu orçamento temporal.

Um princípio mais profundo organiza toda essa discussão: a separação entre ISA e microarquitetura. A ISA é o contrato entre software e hardware — instruções, formatos binários, registradores visíveis, comportamento em exceções. A microarquitetura é a implementação — largura de barramentos, profundidade do pipeline, organização de caches, forma da unidade de controle. A microarquitetura pode ser inteiramente redesenhada sem invalidar software, contanto que continue implementando fielmente a mesma ISA. O exemplo espetacular é o x86: software compilado para o 8086 de 1978 ainda executa em um Core i9 de hoje, e internamente o Core nada tem do 8086 — traduz x86 em \mu\text{ops} RISC, executa várias por ciclo fora de ordem, tem três níveis de cache e predição de desvios sofisticadíssima.

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 7: A ISA como contrato estável; microarquiteturas diferentes podem implementar a mesma ISA.

Para o PIC18F4550 a separação é igualmente real: o .hex gerado por XC8 ou MPASM é uma sequência de palavras da ISA do PIC18; o pipeline de dois estágios, a ULA de oito bits e o controle hardwired são detalhes de microarquitetura. Internamente, ADDWF f, F é decomposto em microoperações que mapeiam diretamente em Q1 a Q4. O programador percebe uma única instrução; a unidade de controle orquestra quatro microoperações.

Projetando FSMs para o Firmware do Projeto Integrador

Tudo o que vimos sobre máquinas de estados tem aplicação direta no firmware. Implementar uma FSM em C para gerenciar modos de operação — operação normal, configuração, diagnóstico — é, 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 uma variável de estado, idealmente como tipo enumerado, e um switch que determina ação (\lambda) e próximo estado (\delta). Alternativa mais escalável substitui o switch por tabela bidimensional indexada por estado e entrada, com custo O(1) por chamada.

A seguir, a FSM de modos de operação do sistema de monitoramento sobre o KIT ACEPIC PRO V8.2, nas duas versões de código que mostro ao longo da disciplina.

typedef enum { OPERACAO, CONFIG, DIAG } estado_t;

estado_t passo(estado_t s, uint8_t btn) {
    switch (s) {
    case OPERACAO: ler_adc_e_mostrar();
        return (btn == BTN_MENU) ? CONFIG : OPERACAO;
    case CONFIG:   editar_setpoint();
        return (btn == BTN_OK)   ? OPERACAO : CONFIG;
    case DIAG:     mostrar_diagnostico();
        return (btn == BTN_OK)   ? OPERACAO : DIAG;
    }
    return OPERACAO;
}
; W = estado atual (0,1,2), botao em PORTB
    movf    estado, W
    bz      st_oper          ; 0 -> OPERACAO
    decf    WREG, W
    bz      st_conf          ; 1 -> CONFIG
    bra     st_diag          ; 2 -> DIAG
st_oper: call ler_adc_mostrar
    btfsc   PORTB, BTN_MENU
    movlw   .1               ; transita para CONFIG
    movwf   estado
    return

Quatro armadilhas recorrentes consomem horas de depuração. Defina sempre transição padrão para toda entrada possível, mantendo permanência no estado atual como fallback seguro. Transcreva o diagrama em tabela antes de escrever uma linha — inconsistência entre diagrama e código é a maior fonte de bugs. Leia todas as entradas no início e trabalhe sobre um snapshot, para evitar leitura inconsistente. Mantenha o debouncing em ISR de Timer0 e entregue à FSM apenas eventos filtrados, em vez de misturar debounce com lógica de transição.

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 8: FSM de modos de operação do Projeto Integrador sobre o KIT ACEPIC PRO V8.2.

Para minimizar penalidades de pipeline, três técnicas pesam mais no PIC18. A programação sem desvios substitui um if-else por uma expressão aritmética: o valor absoluto de um inteiro de oito bits com sinal pode ser obtido por (x \oplus \text{máscara}) - \text{máscara}, com a máscara vinda de deslocamento aritmético, custo fixo e nenhum salto. A expansão de laços, para corpos curtos e iterações poucas, elimina o desvio de retorno ao preço de aumentar o tamanho do código. A reorganização do teste coloca o caso frequente no caminho não tomado: inverta if (cond) raro; else frequente; para if (!cond) frequente; else raro; e o caso comum deixa de pagar a bolha. Essas três técnicas são a base da terceira tarefa do Projeto Integrador.

Síntese e Volta ao Problema

Posso responder agora com precisão à pergunta de abertura: 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. A pergunta “qual abordagem é melhor, hardwired ou microprogramada?” 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 as duas filosofias. As três tarefas do módulo no Projeto Integrador aplicam diretamente esses conceitos: FSM de modos, análise do pipeline com \overline{\text{CPI}} = 1 + f_b e otimização para minimizar penalidades. No Módulo 06, ampliaremos o pipeline para o modelo clássico de cinco estágios; o que aprendemos aqui sobre dois estágios é a fundação direta dessa próxima conversa.