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
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.
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
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
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
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
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
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"]
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;
}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
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.