Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

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çãoPergunta que responde
1. O que é a ULA e o que é um registradorQuem calcula e quem guarda?
2. Para que servePor que a CPU precisa dos dois, e não só de um deles?
3. Exemplos em contextos diferentesOnde essas peças aparecem fora de um livro de arquitetura?
4. Como funcionaComo a ULA soma, compara, faz lógica, desloca e como o banco de registradores é lido e escrito?
5. Detalhes e bordasEm que as arquiteturas divergem e onde a regra de overflow para?
6. Erros comuns e boas práticasQuais enganos aparecem quando alguém começa a ler flags?
7. O programa que simulaComo a ULA e o banco se comportam quando são executados?
8. ExercíciosComo praticar, com as respostas?
9. GlossárioQuais 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.

Blocos internos de uma ULA de 4 bits: A e B entram no somador, nas portas lógicas e no deslocador, e o código escolhe a saída que vira resultado
figura 1 · Blocos internos de uma ULA de 4 bits: os operandos passam pelo somador, pela lóg

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.

Subtração 3 menos 5 com o mesmo somador: B é invertido por XOR, o SUB entra como vai-um, e o resultado 1110 sai com C igual a 1 por empréstimo
figura 2 · Subtração 3 menos 5 com o mesmo somador: B é invertido, o SUB entra como vai-um

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.

Overflow com sinal em 0111 mais 0001: os vai-uns do bit 3 de entrada e de saída passam por um XOR, que dá V igual a 1 mesmo com carry final zero
figura 3 · Overflow com sinal em 0111 + 0001: o vai-um que entra e o que sai do bit de sina

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.

Desvio com sinal depois de 3 menos 5: a ULA não guarda o resultado, o desvio lê N e V, e N diferente de V manda o programa pular o bloco
figura 4 · Desvio com sinal: a ULA calcula 3 menos 5 sem guardar o resultado, e o desvio lê

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.

Máscara 1000 com portas E bit a bit sobre 1101: só o bit 3 passa, e a saída 1000 mostra que o bit mais significativo estava ligado
figura 5 · Máscara 1000 com porta E bit a bit: só o bit 3 de 1101 passa, e o resultado 1000

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.

Banco de quatro registradores com duas leituras por seletores de 4 para 1 e uma escrita por decodificador, mostrando R2 recebendo 1000 depois do ADD
figura 6 · Banco de 4 registradores: duas leituras pelos seletores de 4 para 1 e uma escrit

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.

Execução de ADD R2, R0, R1 em forma de sequência: o decodificador passa os campos, o banco entrega R0 e R1 à ULA, e a soma 1000 volta para R2
figura 7 · Execução de ADD R2, R0, R1: a CPU lê dois registradores, soma na ULA e grava o r

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.

Registradores de uso geral e especiais da CPU: PC e IR ligam o núcleo à memória, e o registrador de flags recebe Z, N, C e V da ULA
figura 8 · Registradores de uso geral e especiais dentro da CPU: o contador de programa e 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.

Padrão load-store em sequência: a RAM entrega um valor a um registrador, a ULA soma dois registradores e o resultado volta à memória com store
figura 9 · Carregar, operar e guardar: a ULA só soma registradores, e a memória só entra po

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.

Mesma subtração 3 menos 5 nos padrões x86 e ARM: o x86 liga o carry no empréstimo e o ARM o desliga, por isso a mesma conta muda o nome da flag
figura 10 · Mesma subtração 3 menos 5 nas famílias x86 e ARM: o x86 liga o carry no emprésti

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

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

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?