Hardware e sistemas operacionais · parte 11 de 27
ULA e registradores: somar, comparar e guardar operandos
Como a ULA soma, subtrai, compara e faz lógica, o que são as flags Z, N, C e V e para que servem os registradores dentro da CPU.
intermediárioPublicada em · 35 min de leitura
ULA e registradores: somar, comparar e guardar operandos
Pense no caixa de um supermercado antigo. Ele tem uma calculadora na frente, que faz as contas, e uma gaveta ao lado, onde ficam os papéis com os números que ele está usando agora. O caixa não sai da loja para buscar o próximo número no depósito, porque a gaveta está a um movimento de distância. Na CPU, a calculadora é a ULA e a gaveta são os registradores. A analogia tem um limite claro: a calculadora do caixa não decide o que fazer com o resultado. Quem decide é a instrução, e essa parte entra só nas aulas seguintes da fase.
Esta aula responde a uma pergunta: como a ULA soma, compara e faz operações lógicas, e para que servem os registradores. Ela parte de dois pontos já resolvidos. O primeiro é o somador completo e o somador-subtrator de 1 bit, que estão em Circuitos combinacionais. O segundo são os flip-flops e os registradores de carga, que estão em Circuitos sequenciais. Aqui não vamos refazer esses circuitos. Vamos usá-los para montar uma ULA de n bits com flags, um banco de registradores e uma instrução executando de ponta a ponta. Como a instrução é codificada e como a CPU a busca e decodifica ficam para ISA, CISC e RISC e Ciclo de instrução e unidade de controle.
Mapa da aula
| Seção | Pergunta que responde |
|---|---|
| 1. O que é a ULA e o que é um registrador | Quem calcula e quem guarda? |
| 2. Para que serve | Por que a CPU precisa dos dois, e não só de um deles? |
| 3. Exemplos em contextos diferentes | Onde essas peças aparecem fora de um livro de arquitetura? |
| 4. Como funciona | Como a ULA soma, compara, faz lógica, desloca e como o banco de registradores é lido e escrito? |
| 5. Detalhes e bordas | Em que as arquiteturas divergem e onde a regra de overflow para? |
| 6. Erros comuns e boas práticas | Quais enganos aparecem quando alguém começa a ler flags? |
| 7. O programa que simula | Como a ULA e o banco se comportam quando são executados? |
| 8. Exercícios | Como praticar, com as respostas? |
| 9. Glossário | Quais termos a aula usa? |
1. O que é a ULA e o que é um registrador
A ULA, sigla de unidade lógica e aritmética, é um circuito combinacional. Recebe dois operandos, que são os números sobre os quais se opera, e um código que diz qual operação fazer. Devolve um resultado e um conjunto de bits de estado, chamados flags, que descrevem o resultado: se deu zero, se o bit de sinal ficou ligado, se houve vai-um ou empréstimo e se a conta estourou a faixa com sinal. A ULA não guarda nada. Se você trocar os operandos, a saída muda de imediato, sem depender do que foi calculado antes.
Um registrador é o oposto disso. É um conjunto de flip-flops com um clock comum, como visto em Circuitos sequenciais, e guarda uma palavra inteira, normalmente do tamanho que a CPU processa de uma vez. Ele só muda na borda do clock, quando a escrita está habilitada. O registrador é a memória mais próxima da ULA, e a diferença de papel entre as duas peças resume a aula: a ULA calcula, o registrador segura o que vai ser calculado e o que acabou de ser calculado.
As flags ficam no meio do caminho. Elas são calculadas pela ULA, mas guardadas num registrador próprio, para que a instrução seguinte possa ler o resultado da anterior. A seção 4.2 explica cada uma.
2. Para que serve
Uma CPU faz, na maior parte do tempo, três coisas com números: soma e subtrai, compara dois valores e combina bits com máscaras. A ULA existe para que essas três tarefas sejam um único bloco de hardware, com um código de operação escolhendo o caminho. Sem ela, cada operação seria um circuito separado, e um processador com dezenas de instruções aritméticas teria dezenas de circuitos ligados a um mesmo barramento. Com a ULA, o custo é um somador, um conjunto de portas lógicas e um seletor.
Os registradores existem por um motivo de tempo. Para que a ULA opere, os operandos precisam estar na entrada dela. Se cada operando viesse da memória principal, a ULA ficaria esperando a memória a cada conta. A fase 4 da trilha mede essa espera com números, mas a ideia já basta aqui: operar sobre valores que já estão ao alcance da ULA é o que deixa a CPU útil. Por isso a arquitetura separa as duas coisas. A ULA só enxerga registradores, e a memória só entra no caminho por instruções de leitura e escrita, como a seção 4.8 mostra.
3. Exemplos em contextos diferentes
O primeiro exemplo é o controle de estoque de uma loja. Cada venda subtrai uma unidade do saldo, e cada reposição soma. A conta é uma soma ou uma subtração com o mesmo circuito, e o que muda é só o código de operação. Dentro da CPU, o contador de um laço e o endereço de um vetor são somados e subtraídos do mesmo jeito, muitas vezes em cada programa.
O segundo exemplo é a regra “se o saldo for menor que o limite, bloqueie a compra”. A decisão depende de uma comparação, e a comparação dentro da CPU é uma subtração que não guarda o resultado, só as flags. A seção 4.3 mostra como essa leitura funciona, e por que o mesmo par de bits pode dar uma resposta diferente conforme os números sejam tratados como com sinal ou sem sinal.
O terceiro exemplo é um byte de permissões, em que cada bit liga ou desliga um direito. Para perguntar se um direito está ativo, a conta é um E com uma máscara que tem só o bit daquele direito ligado. Esse uso das portas lógicas, sem nenhuma soma, aparece em qualquer código que precise olhar um bit específico, como a leitura de um registrador de configuração. A seção 4.4 faz essa conta com 4 bits.
O quarto exemplo é a multiplicação por potências de dois. Deslocar um número uma casa para a esquerda equivale a multiplicá-lo por 2, desde que o bit que sai não se perca. O deslocamento é uma operação de ULA em muitos conjuntos de instruções, e a flag C guarda o bit que saiu, para que o programa saiba se o deslocamento perdeu informação.
4. Como funciona
A figura abaixo separa a ULA em blocos. Os operandos entram à esquerda e chegam ao mesmo tempo ao somador, às portas lógicas e ao deslocador, e cada um calcula a sua parte. O código de operação, em laranja, chega ao seletor, que escolhe qual saída vira o resultado. As flags saem do somador e do próprio resultado, e a seção 4.2 explica cada uma delas.
Leia a figura da esquerda para a direita. O caminho verde é o dos operandos, e o caminho laranja é o do código. Um detalhe de leitura: o somador e o deslocador aparecem como blocos separados, mas a ULA real pode compartilhar circuitos entre eles. A figura mostra a função de cada parte, e não o layout.
4.1 Somar e subtrair com o mesmo circuito
A soma com vai-um e o somador-subtrator já foram montados em Circuitos combinacionais, e não vamos repetir essa montagem. Vale lembrar a ideia que faz a subtração caber no mesmo circuito: A menos B é A somado ao complemento de B mais 1, e o complemento de B é cada bit de B invertido. Um XOR com uma linha de controle SUB inverte B quando SUB vale 1, e a mesma linha entra como vai-um inicial do somador. A ULA desta aula usa exatamente esse arranjo.
A figura abaixo mostra a conta 3 menos 5 em 4 bits. O resultado é 1110, que, lido sem sinal, é 14, e lido com sinal de complemento de dois, é −2. Repare que o vai-um final do somador é 0, porque não houve soma além da faixa de 4 bits, mas a flag C desta aula vale 1. A diferença vem de uma convenção, que a seção 4.2 explica com cuidado: na subtração, a flag C é o contrário do vai-um que sai do somador.
Siga a figura de cima para baixo e da esquerda para a direita. Os operandos A e B entram pelo lado esquerdo, e o controle SUB, em laranja, chega ao bloco que inverte B e ao vai-um de entrada. B sai invertido como 1010, e o somador recebe A, o B invertido e o vai-um 1. O resultado 1110 sai à direita, e as flags aparecem no bloco azul, com C igual a 1, N igual a 1 e V igual a 0. Essa figura mostra um caso só. A conta vale para qualquer par de números de 4 bits, e a seção 7 confere todos os pares.
4.2 As quatro flags
A ULA desta aula produz quatro flags. Cada uma responde a uma pergunta diferente sobre o resultado, e confundi-las é o erro mais comum de quem começa a ler arquitetura.
A flag Z, de zero, vale 1 quando todos os bits do resultado são 0. Ela é a mais simples e a mais usada, porque uma comparação de igualdade vira uma subtração cujo resultado é zero.
A flag N, de negativo, copia o bit de maior peso do resultado. Num número com sinal em complemento de dois, esse bit é o sinal, e por isso N = 1 quer dizer que o resultado, lido com sinal, é negativo. Ela não diz se a conta estourou, e isso é o papel da V.
A flag C tem dois sentidos, conforme a operação. Na soma, é o vai-um que sai do bit de maior peso, e diz que o resultado não coube na faixa sem sinal. Na subtração, a convenção desta aula segue o x86: C vale 1 quando houve empréstimo, ou seja, quando A é menor que B sem sinal. Esse valor é o contrário do vai-um final do somador, porque o somador de complemento soma o B invertido e um vai-um só sobra quando A é maior ou igual a B. A seção 5 mostra que a ARM usa a convenção oposta.
A flag V, de overflow, diz que o resultado com sinal saiu da faixa que cabe no registrador. Para uma soma de n bits, a faixa com sinal vai de −2 elevado a n−1 até 2 elevado a n−1 menos 1. A regra de hardware é curta: o overflow com sinal é o XOR entre o vai-um que entra no bit de sinal e o vai-um que sai dele. A página de Wikipedia sobre a flag de overflow descreve a mesma regra como um XOR entre os vai-uns interno que entra e sai do bit de sinal. A regra foi conferida contra a definição aritmética na seção 7, para todos os pares de 4 e 8 bits.
Um exemplo mostra por que C e V são coisas diferentes. Some 0111, que é 7, com 0001, que é 1, em 4 bits. O resultado é 1000. Sem sinal, 7 mais 1 é 8, que cabe em 4 bits, então C vale 0. Com sinal, 7 mais 1 é 8, e o maior número positivo de 4 bits é 7, então houve overflow e V vale 1. O bit de sinal do resultado também ficou ligado, e por isso N vale 1. A figura mostra a conta bit a bit.
Cada bloco de soma mostra a conta do bit com a soma e o vai-um que sai, e os fios rotulados com c1 a c4 mostram os vai-uns entre os blocos. O vai-um que entra no bit 3 é c3, e o que sai dele é c4. O XOR no canto recebe c3 e c4, que valem 1 e 0, e dá V igual a 1. Note que o vai-um final c4 é 0, então C também é 0, e só a flag V denuncia o problema. Quem lê só o vai-um final acha que a conta deu certo.
4.3 Comparar é subtrair sem guardar o resultado
Uma instrução de comparação faz a subtração A menos B, descarta o resultado e guarda só as flags. O programa seguinte, de um desvio condicional, lê essas flags para decidir o caminho. A figura abaixo mostra essa cadeia para 3 menos 5, em que a ULA não guarda nada, e o desvio lê N e V.
Para números com sinal, a regra é que A é menor que B quando N e V são diferentes. O motivo é que, sem overflow, o bit de sinal do resultado é o sinal verdadeiro da diferença, e com overflow o bit de sinal fica invertido. Com overflow, o bit de sinal aparece invertido, e V é justamente o bit que registra essa inversão, de modo que N diferente de V reconstrói o sinal verdadeiro. Para 3 menos 5 em 4 bits, o resultado é 1110, com N = 1 e V = 0. Os dois são diferentes, então 3 é menor que 5.
O fluxo tem quatro etapas, lidas de cima para baixo. A ULA calcula a subtração e atualiza as flags sem guardar o número. As flags, no bloco azul, chegam à decisão em losango, que pergunta se N é diferente de V. Se for, o desvio acontece, e o programa pula o bloco. Se for igual, o programa executa o bloco. A seta tracejada entre a ULA e as flags indica que o resultado numérico não segue adiante.
Para números sem sinal, a regra é mais simples. Na convenção do x86, que é a desta aula, A é menor que B quando C vale 1, porque C indica empréstimo. Com 7 e 15 em 4 bits, C vale 1 e 7 é de fato menor que 15 sem sinal. Com sinal, o mesmo par dá outra resposta, que a seção 6 usa como erro comum.
4.4 Operações lógicas e deslocamento
As portas E, OU e XOR operam bit a bit. O bit 3 de A se combina só com o bit 3 de B, e nunca há vai-um entre as posições. Essa é a diferença central em relação à soma, e é por isso que as operações lógicas são as mais simples da ULA: cada posição é independente das outras.
A operação mais útil dessas é a máscara. Uma máscara é um valor em que os bits que interessam estão em 1 e os demais em 0. Um E entre o valor e a máscara zera tudo que não interessa e preserva o que interessa. Para testar se o bit 3 de 1101 está ligado, use a máscara 1000: o resultado 1000 é diferente de zero, e isso diz que o bit está ligado. Para testar o bit 2, use 0100, e o resultado 0000 diz que ele está desligado. A figura mostra a conta do bit 3, com as quatro portas E recebendo os bits de A e da máscara.
Cada porta E recebe um bit de 1101 e um bit de 1000, e entrega o resultado da sua posição. A porta do bit 3 recebe 1 e 1, e sai 1. As outras três recebem um bit de A que pode valer 1, mas o bit da máscara vale 0, e saem 0. A caixa à direita junta os quatro resultados e mostra 1000. As portas não estão ligadas à caixa por fios porque a figura serve para ler a conta de cada posição, não para mostrar o caminho elétrico.
O deslocamento move todos os bits uma casa. No deslocamento para a esquerda, entra 0 pela direita e o bit de maior peso sai pela flag C. Assim, 1101 deslocado para a esquerda dá 1010, com C = 1. No deslocamento lógico para a direita, entra 0 pela esquerda e o bit de menor peso sai pela C. Assim, 1101 deslocado para a direita dá 0110, com C = 1. A flag V não tem uma regra única para deslocamentos, e a seção 5 explica por que esta aula a deixa em 0.
4.5 O banco de registradores
A ULA precisa de dois operandos na mesma etapa, então o banco de registradores tem duas portas de leitura. Cada porta é um seletor de 4 para 1 de palavras. Os campos rs1 e rs2 da instrução escolhem qual registrador entra em cada porta. Escrever um resultado precisa de outro caminho, e o banco tem uma porta de escrita, que é um decodificador de 2 para 4 ligado a um sinal de habilitação, chamado we. O decodificador ativa só a linha do registrador escolhido por rd, e só se we valer 1. Os demais registradores mantêm o valor que já tinham, porque o flip-flop com habilitação, visto em Circuitos sequenciais, segura o dado quando não há escrita.
A figura mostra o banco com quatro registradores de 4 bits. O estado representado é o de depois da instrução ADD R2, R0, R1, com R0 igual a 0101, R1 igual a 0011 e R2 recebendo 1000. O campo rd vale 10, que escolhe R2, e we vale 1, então só R2 é atualizado.
Leia a figura da esquerda para a direita. Os campos rs1 e rs2, em laranja, escolhem as leituras, e as duas saídas dos seletores entram na ULA como A e B. O resultado da ULA, 1000, volta a R2 pela seta grossa, e o decodificador, ligado a rd e we, habilita só essa linha. R0 e R1 aparecem como fontes, porque são lidos e não mudam. R3 aparece com 0000 e nenhuma seta, porque nenhuma instrução desta sequência o usa.
O número de registradores é uma escolha da arquitetura, e a figura usa quatro só para caber na tela. A fonte consultada informa os números de três famílias. A RISC-V tem 32 registradores inteiros na base do conjunto, com o primeiro, x0, fixo em zero, de modo que uma leitura dele devolve sempre 0 e uma escrita não tem efeito. A x86-64 tem 16 registradores de uso geral de 64 bits, ante 8 na x86 de 32 bits. A AArch64 tem 31 registradores de uso geral de 64 bits, além de um registrador que serve como zero ou como ponteiro de pilha, dependendo da instrução. A regra de hardware é a mesma nas três: duas leituras e uma escrita por instrução, com seletores e decodificadores do tamanho do número de registradores.
4.6 Uma instrução passo a passo
Juntando as peças, uma instrução ADD R2, R0, R1 passa pelo decodificador, pelo banco, pela ULA e volta ao banco. Com R0 igual a 0101 e R1 igual a 0011, o resultado é 1000, com V = 1 e N = 1. A figura mostra as mensagens na ordem em que acontecem.
Cada seta é uma etapa, lida de cima para baixo. Primeiro, o decodificador entrega rs1, rs2 e rd ao banco. Depois, o banco envia o valor de R0 e o valor de R1 à ULA, que soma e produz 1000. A nota à direita da ULA mostra o resultado e a flag V. Por fim, a ULA devolve o resultado ao banco, que o grava em R2. A figura não mostra o estado de R2 antes da escrita, porque o valor anterior não importa para a instrução.
O exemplo tem um detalhe que vale guardar. Sem sinal, 5 mais 3 é 8, que cabe em 4 bits, e o C seria 0. Com sinal, 5 mais 3 é 8, que é maior que o maior número de 4 bits com sinal, então V vale 1. Um programa que usa números com sinal e não lê V aceita 1000 como se fosse uma resposta correta, quando o valor já estourou. Essa é a razão de as flags existirem: o resultado sozinho não informa se a conta fez sentido.
4.7 Registradores que o programador não escolhe
Os registradores de uso geral são os que a instrução cita, como R0 a R3 acima. Dentro da CPU há também registradores especiais, que o programador não escolhe por nome, mas que a execução usa o tempo todo. A figura mostra quatro deles, ao lado dos de uso geral e da memória.
O contador de programa, chamado PC nesta aula, guarda o endereço da próxima instrução a buscar. Depois de cada instrução, ele avança, a menos que um desvio mande outro valor. O registrador de instrução, IR, guarda a instrução que está sendo executada, para que a unidade de controle possa decodificá-la. O ponteiro de pilha, SP, guarda o topo da pilha de chamadas de função, e o registrador de flags, chamado status ou CCR, guarda as flags da última operação.
A figura separa os dois grupos em caixas tracejadas. Os registradores de uso geral, à esquerda, recebem operandos da ULA. Os especiais, à direita, têm papéis fixos: o PC envia um endereço à memória pela seta laranja, e o IR recebe a instrução de volta pela seta cinza. A seta pontilhada da ULA para o registrador de flags mostra que as flags são o único resultado da ULA que vai para um registrador especial, enquanto o valor numérico segue para um registrador de uso geral. O desenho não mostra a unidade de controle, que é assunto da aula de ciclo de instrução.
4.8 Load-store: por que a ULA só vê registradores
Uma arquitetura load-store divide as instruções em duas classes. Uma classe acessa a memória, com instruções de load, que trazem um valor da memória para um registrador, e de store, que levam um registrador de volta à memória. A outra classe faz operações da ULA, e essas só trabalham entre registradores. A Wikipedia dá essa definição, e cita como exemplos as arquiteturas PowerPC, SPARC, RISC-V, ARM e MIPS.
O padrão tem uma consequência prática que a figura mostra. Para somar dois valores que estão na memória, o programa faz um load de cada um para um registrador, depois a ULA soma os dois registradores e o resultado vai para outro registrador, e por fim um store leva o resultado de volta à memória. Três instruções de transferência e uma de ULA para uma soma. Em troca, cada instrução faz uma coisa só, e a ULA pode ser um bloco simples, sem caminhos especiais para a memória.
Leia a figura de cima para baixo. Primeiro, um load traz 5 da RAM para R1. Depois, a ULA recebe R1 igual a 5 e R2 igual a 3, e devolve 8 para R3. Por fim, um store grava R3 no endereço 17. As setas cheias são chamadas, e as tracejadas são retornos de dados. A ULA não aparece em contato com a RAM em nenhum momento, e esse é o ponto da arquitetura.
Famílias que não seguem esse padrão, como a x86 em muitas de suas instruções, permitem que a ULA use um operando diretamente da memória. Esta aula não confere essa regra numa fonte primária, e ela fica registrada na seção de limites.
5. Detalhes e bordas
A primeira diferença é a convenção da flag C na subtração. A página de Wikipedia sobre a flag de carry afirma que processadores x86 ligam a flag de carry no empréstimo, enquanto o ARM a desliga no empréstimo. A mesma conta, 3 menos 5, dá resultado 1110 nas duas arquiteturas, mas a flag de nome parecido tem valor oposto. A figura compara os dois casos.
As duas colunas da figura têm a mesma conta. A da esquerda mostra a convenção x86, em que o empréstimo liga a flag CF. A da direita mostra a ARM, em que o carry invertido desliga a flag C no mesmo caso. Ao ler código de outra arquitetura, a primeira pergunta deve ser qual convenção a flag segue, porque um desvio escrito para uma convenção faz o oposto na outra.
A segunda borda é o overflow em deslocamentos. A mesma página de Wikipedia sobre a flag de overflow registra que, no x86, o conjunto de instruções define a flag de overflow só para multiplicações e para deslocamentos de 1 bit, e que deslocamentos de mais de um bit deixam a flag indefinida. Por isso a ULA desta aula zera V nos deslocamentos. Essa escolha é uma simplificação didática, e não uma regra de arquitetura.
A terceira borda é a largura. O programa desta aula usa 4 e 8 bits para caber na tela e na conferência exaustiva. Registradores reais têm 64 bits em famílias de 64 bits, e a regra de V muda de posição com a largura, mas a lógica é a mesma. A quarta borda é o que esta aula deixa de fora: multiplicação, divisão e operações de ponto flutuante. Elas usam outros circuitos, ou a mesma ULA em etapas, e não são tratadas aqui.
6. Erros comuns e boas práticas
O erro mais frequente é confundir C com V. Quem lê só o vai-um final de uma soma acredita que uma conta sem vai-um não pode ter errado. Isso é verdade para a soma sem sinal e falso para a soma com sinal. No exemplo 0111 mais 0001, o vai-um final é 0, e a conta com sinal estourou. Quem lê só C deixa passar esse erro, e a correção é olhar V sempre que o número representa algo com sinal.
O segundo erro é comparar com a flag errada. Para 0111 menos 1111, a leitura sem sinal diz que 7 é menor que 15, porque C vale 1. A leitura com sinal diz que 7 não é menor que −1, porque N e V valem 1, e são iguais. Ambas estão certas, cada uma para o seu tipo de número. O erro está em usar a leitura sem sinal para um valor que o programa trata como negativo. Antes de escolher o desvio, confirme se os operandos são com ou sem sinal.
O terceiro erro é achar que uma flag sempre significa a mesma coisa. A flag C é vai-um na soma e empréstimo na subtração, com a convenção do x86, e o ARM a inverte. A flag V é overflow com sinal, e não se aplica a operações lógicas. A boa prática é ler a especificação da arquitetura antes de escrever um desvio, e não supor a convenção pelo nome da flag.
O quarto erro é escrever num registrador que é fixo. Em RISC-V, escrever em x0 não tem efeito, e ler x0 devolve zero, segundo a documentação citada na seção 4.5. Quem gera código sem saber disso perde o resultado de uma conta sem nenhum aviso.
A boa prática geral segue o que foi feito neste texto: escreva a tabela de casos de limite antes de confiar no circuito. Teste o maior positivo, o menor negativo, zero, e um par que estoura com sinal mas não sem sinal. Esses casos mostram os erros de convenção que a leitura da figura deixa passar.
7. O programa que simula
O programa abaixo implementa a ULA de n bits com portas lógicas, e o banco de registradores com um decodificador e seletores. Ele confere a ULA contra a aritmética de Python para todas as operações e todos os pares de números de 4 e 8 bits, o que dá 1792 e 458752 casos. Depois confere as comparações com e sem sinal, e por fim executa a sequência de instruções da seção 4.6 para mostrar o estado do banco. O programa foi executado com Python 3.12.8, no modo isolado, com o comando python3 -I ula_registradores.py. A saída abaixo é a real, copiada sem edição.
# ULA de n bits e banco de registradores, construídos só com portas e um laço de leitura.
# Cada resultado é conferido contra a aritmética direta de Python.
import itertools
def NOT(a): return 1 - a
def AND(a, b): return a & b
def OR(a, b): return a | b
def XOR(a, b): return a ^ b
def to_bits(x, n):
return [(x >> i) & 1 for i in range(n)]
def from_bits(bits):
return sum(b << i for i, b in enumerate(bits))
def somador_completo(a, b, cin):
s1 = XOR(a, b); c1 = AND(a, b)
s2 = XOR(s1, cin); c2 = AND(s1, cin)
return s2, OR(c1, c2)
def somador(a_bits, b_bits, cin):
# devolve soma, vai-um de saída e o vai-um que entra no bit de sinal
s, c = [], cin
c_antes_do_sinal = cin
for i, (a, b) in enumerate(zip(a_bits, b_bits)):
if i == len(a_bits) - 1:
c_antes_do_sinal = c
si, c = somador_completo(a, b, c)
s.append(si)
return s, c, c_antes_do_sinal
def ula(n, a, b, op):
"""op: 'ADD', 'SUB', 'AND', 'OR', 'XOR', 'SHL', 'SHR'. Devolve (resultado, Z, N, C, V)."""
A = to_bits(a, n)
B = to_bits(b, n)
if op == 'ADD':
s, cout, cin_sinal = somador(A, B, 0)
C = cout
V = XOR(cin_sinal, cout)
elif op == 'SUB':
# A - B = A + NOT(B) + 1. O vai-um de saída vale 1 quando não há empréstimo.
# A flag C desta aula segue o x86: C = 1 quando há empréstimo, ou seja, NOT(vai-um).
s, cout, cin_sinal = somador(A, [NOT(b_) for b_ in B], 1)
C = NOT(cout)
V = XOR(cin_sinal, cout)
elif op == 'AND':
s = [AND(x, y) for x, y in zip(A, B)]; C = 0; V = 0
elif op == 'OR':
s = [OR(x, y) for x, y in zip(A, B)]; C = 0; V = 0
elif op == 'XOR':
s = [XOR(x, y) for x, y in zip(A, B)]; C = 0; V = 0
elif op == 'SHL':
# o bit de maior peso sai pela C; entra 0 pela direita
s = [0] + A[:-1]; C = A[-1]; V = 0
elif op == 'SHR':
# deslocamento lógico: o bit de menor peso sai pela C; entra 0 pela esquerda
s = A[1:] + [0]; C = A[0]; V = 0
res = from_bits(s)
Z = 1 if res == 0 else 0
N = s[-1]
return res, Z, N, C, V
def com_sinal(x, n):
return x - (1 << n) if x >= (1 << (n - 1)) else x
# 1. conferência exaustiva contra a aritmética, para n = 4 e n = 8
def referencia(n, a, b, op):
m = (1 << n) - 1
if op == 'ADD':
r = (a + b) & m
C = (a + b) >> n
V = int(not (-(1 << (n - 1)) <= com_sinal(a, n) + com_sinal(b, n) < (1 << (n - 1))))
elif op == 'SUB':
r = (a - b) & m
C = int(a < b) # empréstimo, convenção do x86
V = int(not (-(1 << (n - 1)) <= com_sinal(a, n) - com_sinal(b, n) < (1 << (n - 1))))
elif op == 'AND': r, C, V = a & b, 0, 0
elif op == 'OR': r, C, V = a | b, 0, 0
elif op == 'XOR': r, C, V = a ^ b, 0, 0
elif op == 'SHL': r, C, V = (a << 1) & m, a >> (n - 1), 0
elif op == 'SHR': r, C, V = a >> 1, a & 1, 0
Z = int(r == 0)
N = (r >> (n - 1)) & 1
return r, Z, N, C, V
for n in (4, 8):
ok = True
casos = 0
for a, b in itertools.product(range(1 << n), repeat=2):
for op in ('ADD', 'SUB', 'AND', 'OR', 'XOR', 'SHL', 'SHR'):
casos += 1
if ula(n, a, b, op) != referencia(n, a, b, op):
ok = False
print("ULA %d bits, %d casos, todas as operações e flags conferem: %s" % (n, casos, ok))
# 2. comparações: com sinal, a < b é N diferente de V; sem sinal, a < b é C = 1
for n in (4, 8):
ok = True
for a, b in itertools.product(range(1 << n), repeat=2):
_, Z, N, C, V = ula(n, a, b, 'SUB')
if (N ^ V) != int(com_sinal(a, n) < com_sinal(b, n)):
ok = False
if C != int(a < b):
ok = False
print("comparacao com e sem sinal, %d bits, todos os pares: %s" % (n, ok))
print()
# 3. os exemplos da aula, com as flags
exemplos = [
("0111 + 0001", 4, 0b0111, 0b0001, 'ADD'),
("0011 - 0101 (3 - 5)", 4, 0b0011, 0b0101, 'SUB'),
("0101 - 0101 (5 - 5)", 4, 0b0101, 0b0101, 'SUB'),
("1101 AND 1000", 4, 0b1101, 0b1000, 'AND'),
("1101 << 1 (SHL)", 4, 0b1101, 0, 'SHL'),
("1101 >> 1 (SHR)", 4, 0b1101, 0, 'SHR'),
]
for nome, n, a, b, op in exemplos:
r, Z, N, C, V = ula(n, a, b, op)
print("%-22s -> resultado %s Z=%d N=%d C=%d V=%d" % (nome, format(r, '0%db' % n), Z, N, C, V))
print()
# 4. banco de 4 registradores de 4 bits: duas leituras, uma escrita
def decodificador2x4(sel, we):
# uma linha de escrita por registrador; só a linha escolhida fica ativa, e só se we = 1
s1, s0 = (sel >> 1) & 1, sel & 1
linhas = [AND(NOT(s1), NOT(s0)), AND(NOT(s1), s0), AND(s1, NOT(s0)), AND(s1, s0)]
return [AND(we, l) for l in linhas]
def mux4_palavra(palavras, sel, n):
# seleciona uma palavra de n bits: cada bit da saída é um mux 4:1 sobre o mesmo bit das quatro palavras
s1, s0 = (sel >> 1) & 1, sel & 1
linhas = [AND(NOT(s1), NOT(s0)), AND(NOT(s1), s0), AND(s1, NOT(s0)), AND(s1, s0)]
bits = []
for i in range(n):
r = 0
for palavra, l in zip(palavras, linhas):
r = OR(r, AND(to_bits(palavra, n)[i], l))
bits.append(r)
return from_bits(bits)
banco = [0, 0, 0, 0]
def escreve(rd, valor, we):
global banco
habilita = decodificador2x4(rd, we)
banco = [valor if habilita[i] else banco[i] for i in range(4)]
def le(rs):
return mux4_palavra(banco, rs, 4)
def instrucao(nome, rd, rs1, rs2, op, we=1):
a, b = le(rs1), le(rs2)
r, Z, N, C, V = ula(4, a, b, op)
escreve(rd, r, we)
print("%-26s rs1=R%d (%s) rs2=R%d (%s) ULA=%s -> R%d = %s Z=%d N=%d C=%d V=%d" % (
nome, rs1, format(a, '04b'), rs2, format(b, '04b'), op,
rd, format(r, '04b'), Z, N, C, V))
escreve(0, 5, 1)
escreve(1, 3, 1)
print("estado inicial:", " ".join("R%d=%s" % (i, format(v, '04b')) for i, v in enumerate(banco)))
instrucao("ADD R2, R0, R1", 2, 0, 1, 'ADD')
instrucao("SUB R3, R0, R1", 3, 0, 1, 'SUB')
instrucao("ADD R2, R0, R0 (we=0)", 2, 0, 0, 'ADD', we=0)
print("estado final:", " ".join("R%d=%s" % (i, format(v, '04b')) for i, v in enumerate(banco)))
print("banco bate com a conta direta (5, 3, 8, 2):", banco == [5, 3, 8, 2])
Saída real do programa, com Python 3.12.8:
ULA 4 bits, 1792 casos, todas as operações e flags conferem: True
ULA 8 bits, 458752 casos, todas as operações e flags conferem: True
comparacao com e sem sinal, 4 bits, todos os pares: True
comparacao com e sem sinal, 8 bits, todos os pares: True
0111 + 0001 -> resultado 1000 Z=0 N=1 C=0 V=1
0011 - 0101 (3 - 5) -> resultado 1110 Z=0 N=1 C=1 V=0
0101 - 0101 (5 - 5) -> resultado 0000 Z=1 N=0 C=0 V=0
1101 AND 1000 -> resultado 1000 Z=0 N=1 C=0 V=0
1101 << 1 (SHL) -> resultado 1010 Z=0 N=1 C=1 V=0
1101 >> 1 (SHR) -> resultado 0110 Z=0 N=0 C=1 V=0
estado inicial: R0=0101 R1=0011 R2=0000 R3=0000
ADD R2, R0, R1 rs1=R0 (0101) rs2=R1 (0011) ULA=ADD -> R2 = 1000 Z=0 N=1 C=0 V=1
SUB R3, R0, R1 rs1=R0 (0101) rs2=R1 (0011) ULA=SUB -> R3 = 0010 Z=0 N=0 C=0 V=0
ADD R2, R0, R0 (we=0) rs1=R0 (0101) rs2=R0 (0101) ULA=ADD -> R2 = 1010 Z=0 N=1 C=0 V=1
estado final: R0=0101 R1=0011 R2=1000 R3=0010
banco bate com a conta direta (5, 3, 8, 2): True
Leia a saída em duas partes. As três primeiras linhas confirmam a ULA contra a aritmética direta, e a linha das comparações confirma as duas regras da seção 4.3. A seção seguinte mostra os seis exemplos com as flags, e a conta bate com as figuras e com o texto: 0111 mais 0001 dá 1000 com V = 1, e 0011 menos 0101 dá 1110 com C = 1. A última parte mostra o banco depois de três instruções. A instrução com we = 0 calcula 1010 e não grava, e R2 continua com 1000. O estado final, 5, 3, 8 e 2, bate com a conta feita à mão.
8. Exercícios
-
Some 1001 com 0111 em 4 bits e diga o resultado e as flags Z, N, C e V. Resposta: o resultado é 0000, com vai-um de saída 1. Z vale 1, N vale 0, C vale 1 e V vale 0. Lido sem sinal, 9 mais 7 é 16, que estoura a faixa sem sinal. Lido com sinal, −7 mais 7 é 0, que cabe, então V é 0.
-
Calcule 0110 menos 1001 em 4 bits, com as quatro flags. Resposta: o resultado é 1101, com N = 1, Z = 0, C = 1 e V = 1. Sem sinal, 6 é menor que 9, então há empréstimo e C vale 1. Com sinal, 6 menos −7 é 13, fora da faixa de −8 a 7, então V vale 1.
-
Para 0010 menos 1111 em 4 bits, diga se 2 é menor que 15 sem sinal e se 2 é menor que −1 com sinal, usando as flags. Resposta: C vale 1, então 2 é menor que 15 sem sinal. N e V valem 0 e 0, e são iguais, então 2 não é menor que −1 com sinal.
-
Use uma máscara para testar se o bit 2 de 1011 está ligado. Qual é a máscara e qual é o resultado? Resposta: a máscara é 0100, e o E de 1011 com 0100 dá 0000. Z vale 1, então o bit 2 está desligado, o que confere, porque o bit 2 de 1011 é 0.
-
No banco de quatro registradores, uma instrução tem we igual a 0. O que acontece com o registrador apontado por rd, e com o resultado da ULA? Resposta: o registrador mantém o valor anterior, porque nenhuma linha de escrita é habilitada. A ULA continua calculando e as flags continuam sendo atualizadas, mas o resultado não é gravado no banco.
-
Por que a sequência 0111 menos 1111 é um caso que pede atenção ao tipo do número? Resposta: C vale 1 e a leitura sem sinal diz que 7 é menor que 15. N e V valem 1 e 1, então a leitura com sinal diz que 7 não é menor que −1. O mesmo par de bits dá respostas diferentes conforme o programa trata os números, e o código precisa escolher a leitura certa.
9. Glossário
ULA: unidade lógica e aritmética, circuito combinacional que opera sobre dois operandos conforme um código de operação e devolve um resultado e flags.
Registrador: conjunto de flip-flops com clock comum que guarda uma palavra, e só muda quando a escrita está habilitada.
Flag: bit de estado calculado pela ULA e guardado num registrador de status, que descreve o resultado da última operação.
Flag Z: vale 1 quando o resultado é zero.
Flag N: copia o bit de maior peso do resultado, que é o sinal em complemento de dois.
Flag C: vai-um na soma sem sinal, e, na convenção do x86 usada nesta aula, empréstimo na subtração.
Flag V: indica overflow com sinal, calculado como o XOR entre o vai-um que entra no bit de sinal e o que sai dele.
Máscara: valor usado com E para isolar ou zerar bits específicos de outro valor.
Deslocamento: operação que move os bits uma casa para a esquerda ou para a direita, com o bit que sai indo para a flag C.
Banco de registradores: conjunto de registradores de uso geral, com portas de leitura e de escrita selecionadas pelos campos da instrução.
Arquitetura load-store: arquitetura em que a ULA opera só sobre registradores, e apenas instruções de load e store acessam a memória.
Contador de programa (PC): registrador que guarda o endereço da próxima instrução a buscar.
Registrador de instrução (IR): registrador que guarda a instrução em execução.
O que não foi confirmado ou medido
O programa desta aula foi executado e as saídas são reais. Ele verifica a lógica da ULA e do banco contra a aritmética de Python, mas não simula tensão, corrente nem tempo de propagação. Nenhum número de atraso, de frequência ou de latência aparece nesta aula, e a fase 4 trata desses valores.
As figuras de circuito, 03, 05 e 09, são representações em nível de portas e de blocos, não o caminho elétrico real. A figura 05 mostra quatro registradores de 4 bits por clareza, e a figura 09 não liga as portas à caixa de saída, porque mostra a conta bit a bit. As figuras 01, 02, 04, 06, 07, 08 e 10 são diagramas de blocos e de sequência, sem valores medidos.
A contagem de registradores de três famílias vem da Wikipedia em inglês, nas páginas citadas abaixo, e não de manuais de arquitetura. Os números são os que a Wikipedia informa na data da consulta, e uma consulta às especificações oficiais pode mostrar detalhes que a página não traz.
A convenção de C na subtração segue a página sobre a flag de carry, que descreve o x86 e o ARM. A aula não leu o manual oficial da Intel nem o da ARM para confirmar a convenção linha por linha. A regra de V em deslocamentos segue a mesma página sobre overflow, e a aula não conferiu essa regra no manual.
A regra de que a x86 aceita operando de memória em instruções aritméticas, citada na seção 4.8, não foi confirmada em fonte primária nesta aula. A aula também não descreve as flags de operações lógicas em cada arquitetura real, e a convenção de zerar C e V nas lógicas é uma escolha do modelo.
A seção 5 cita uma definição de arquitetura load-store da Wikipedia, sem consultar a literatura acadêmica original.
Fontes
Consultadas em 1 de novembro de 2026.
- Wikipedia em inglês, verbete “Carry flag”, sobre a convenção do carry na subtração no x86 e no ARM: https://en.wikipedia.org/wiki/Carry_flag
- Wikipedia em inglês, verbete “Arithmetic logic unit”, sobre as operações típicas de uma ULA e as flags de estado: https://en.wikipedia.org/wiki/Arithmetic_logic_unit
- Wikipedia em inglês, verbete “Overflow flag”, sobre a regra de overflow como XOR entre os vai-uns do bit de sinal e sobre as instruções x86 que definem essa flag: https://en.wikipedia.org/wiki/Overflow_flag
- Wikipedia em inglês, verbete “Status register”, sobre as flags Z, N, C e V e o uso em desvios condicionais: https://en.wikipedia.org/wiki/Status_register
- Wikipedia em inglês, verbete “Two’s complement”, sobre a negação por inversão e soma de 1 e a subtração como soma do negativo: https://en.wikipedia.org/wiki/Two%27s_complement
- Wikipedia em inglês, verbete “Load-store architecture”, sobre a divisão entre acesso à memória e operações da ULA: https://en.wikipedia.org/wiki/Load%E2%80%93store_architecture
- Wikipedia em inglês, verbete “X86-64”, sobre o número de registradores de uso geral: https://en.wikipedia.org/wiki/X86-64
- Wikipedia em inglês, verbete “RISC-V”, sobre os 32 registradores inteiros e o registrador x0 fixo em zero: https://en.wikipedia.org/wiki/RISC-V
- Wikipedia em inglês, verbete “AArch64”, sobre os 31 registradores de uso geral de 64 bits: https://en.wikipedia.org/wiki/AArch64
As fontes confirmam os conceitos e as convenções citados. As tabelas de casos, os exemplos e as respostas dos exercícios foram calculados e conferidos pelo programa da seção 7, e não copiados das fontes.
Relacionados
- Trilha de Hardware
- CPU
- Como a CPU executa instruções
- Circuitos combinacionais
- Circuitos sequenciais
- Pipeline, cache e paralelismo
- ISA, CISC e RISC
- Ciclo de instrução e unidade de controle