Skip to content
Diogo Valadares Reis dos Santos edited this page Aug 26, 2025 · 1 revision

[English]

[← Página Anterior | Próxima Página →]

Unidade Aritmética e Lógica

A Unidade Aritmética e Lógica (ULA) é essencialmente a calculadora do processador. Ela é responsável por executar as diversas operações matemáticas definidas na ISA. Uma ULA pode operar de diferentes formas dependendo da arquitetura, mas no DRISC-V, podemos pensá-la como realizando uma operação matemática simples entre dois operandos:

Fonte1 Operação Fonte2 = Resultado com CNZV

Onde:

  • Fonte1 pode ser o valor do Barramento A ou do Contador de Programa Atual
  • Fonte2 pode ser o valor do Barramento B ou do Barramento Imediato
  • Operação representa um conjunto de operações matemáticas como +, -, *, >>, etc., codificadas em 5 bits
  • Resultado é direcionado ao Barramento C para ser escrito de volta no Banco de Registradores
  • CNZV são 4 bits usados para calcular comparações em instruções de desvio e são enviados ao Controlador de Operações
    • CNZV significa:
      • Carry (vai-um)
      • Negativo
      • Zero
      • Overflow (estouro)

A figura a seguir mostra o componente ULA na simulação Logisim. Note que as entradas para os barramentos A e C são rotuladas como Fonte 1 e Fonte 2, e que há dois sinais de controle extras para selecionar o Contador de Programa e o Valor Imediato:

Componente ULA no Logisim

A tabela a seguir mostra as operações disponíveis na ULA:

Codificação Símbolo da Operação Observações Exemplo
0 + Soma com carry armazenado em cnzv[0] 5 + 3 = 8
1 << Deslocamento lógico à esquerda, equivalente a multiplicar por 2 5 << 1 = 10
2 < Comparação menor que (com sinal) -2 < 3 → 1
3 <U Comparação menor que (sem sinal) 250 < 300 → 1
4 ^ XOR bit a bit 0101₂ ^ 0011₂ = 0110₂
5 >> Deslocamento lógico à direita 1000₂ >> 2 = 0010₂
6 ` ` OR bit a bit
7 & AND bit a bit 0101₂ & 0011₂ = 0001₂
8 * Multiplicação de 32 bits (parte inferior do resultado) 6 * 7 = 42
9 *H Multiplicação com sinal, retorna os 32 bits superiores 100000 * 100000 → bits altos
10 *HSU Multiplicação sinal × sem sinal, retorna os 32 bits superiores -1 * 2^31 → bits altos
11 *HU Multiplicação sem sinal, retorna os 32 bits superiores 2^31 * 2^31 → bits altos
16 - Subtração com carry armazenado em cnzv[0], usada em comparações 10 - 3 = 7
21 >>> Deslocamento aritmético à direita (com sinal), equivalente a dividir por 2 -8 >>> 1 = -4
default 0 Caso padrão, resultado é zero Operação inválida → 0

Nota: O DRISC-V, como a maioria das arquiteturas modernas, utiliza o sistema de números complemento de dois para representar números com sinal (ou seja, que podem ser negativos).

Quando uma operação é rotulada como "sem sinal", significa que o número é tratado como se o bit mais significativo não representasse o sinal.

Por exemplo, na operação <U, testar se -1 < 0 resulta em falso, pois -1 (0xffffffff) é tratado como 4.294.967.295, que é maior que 0.

De acordo com a especificação RISC-V, parte do valor imediato é usada para codificar a operação >>> (deslocamento aritmético à direita).

Isso ocorre porque deslocar mais de 32 bits em um valor de 32 bits resulta em 0 ou 0xFFFFFFFF, dependendo do sinal. Portanto, apenas 5 bits são necessários do valor imediato para especificar o deslocamento.

O formato de instrução OP-IMM fornece apenas 3 bits para selecionar a operação, que já são usados por outras instruções. Para acomodar a instrução SRAI (Shift Right Arithmetic Immediate), bits extras do campo imediato são reaproveitados.

No entanto, o seletor de operação limitado a 3 bits impede que multiplicação e subtração sejam suportadas diretamente na forma imediata. Há uma solução alternativa para subtração: usar um imediato negativo com a instrução ADDI para efetivamente realizar uma subtração.

Dentro da ULA

Dentro da ULA

Podemos dividir a operação da ULA em três partes:

1. Seleção das Fontes

É aqui que os operandos são escolhidos. Há também um pequeno circuito para seleção da operação, responsável por ignorar os 2 bits mais significativos em operações imediatas que não sejam deslocamentos aritméticos, evitando que valores imediatos interfiram na operação atual.

2. Cálculo do Resultado

Todas as operações recebem as mesmas entradas e são calculadas simultaneamente.
Note que o somador/subtrator e o multiplicador recebem bits de operação que permitem modificar seu comportamento. Isso possibilita a reutilização de circuitos, minimizando o número de componentes necessários.

Dentro do Somador e Multiplicador

3. Seleção da Operação e Cálculo do CNZV

A seleção é feita usando dois multiplexadores:

  • O primeiro é de 1 bit e seleciona entre operações padrão e o deslocamento aritmético à direita (>>>)
  • O segundo é de 4 bits e seleciona o resultado final entre todas as operações calculadas

O valor CNZV só é verdadeiramente válido para operações de subtração (e só é usado com elas), mas ainda pode retornar valores para outras operações, mesmo que não sejam significativos no contexto.

Código em System Verilog

O código a seguir é a versão System Verilog da ULA:

`timescale 1s/1s
// Entradas e Saídas
module alu(
    input use_pc,
    input [31:0] a,
    input [31:0] program_counter,
    input use_immediate,
    input [31:0] b,
    input [31:0] immediate,
    input [4:0] operation,
    output logic [31:0] result,
    output logic [3:0] cnzv
    );
// Primeira parte: seleção das fontes e máscara de bits para operações imediatas
    wire [4:0]op = (use_immediate & ~(operation == 1 | operation == 5 | operation == 21)) ?
                        {2'b0, operation[2:0]} :
                        operation;

    wire [31:0] source1 = use_pc ? program_counter : a;
    wire [31:0] source2 = use_immediate ? immediate : b;

    always@ * begin
        // Segunda e terceira partes (o case gera os multiplexadores)
        case(op)
            0: {cnzv[0], result} = source1 + source2;
            1: result = source1 << source2[4:0];
            2: result = ($signed(source1) < $signed(source2)) ? 1 : 0;
            3: result = ($unsigned(source1) < $unsigned(source2)) ? 1 : 0;
            4: result = source1 ^ source2;
            5: result = source1 >> source2[4:0];
            6: result = source1 | source2;
            7: result = source1 & source2;
            8: result = source1 * source2;
            9: begin
                logic [63:0] r64;
                r64 = $signed(source1) * $signed(source2);
                result = r64[63:32];
            end
            10: begin
                logic [63:0] r64;
                r64 = $signed({source1[31], source1}) * $signed({1'b0, source2});
                result = r64[63:32];
            end
            11: begin
                logic [63:0] r64;
                r64 = source1 * source2;
                result = r64[63:32];
            end
            16: {cnzv[0], result} = source1 - source2;
            21: result = $signed(source1) >>> source2[4:0];
            default: result = 32'b0;
        endcase

        // Cálculo final do CNZV
        cnzv[3:1] = {
            (operation == 16 ^ (source1[31] == source2[31]) && (result[31] != source1[31])), // Overflow
            result == 0,  // Zero
            result[31]    // Negativo
        };
    end
endmodule

Usos da ULA

A ULA é o componente principal utilizado tanto para instruções OP quanto OP-IMM. Mas além disso, no DRISC-V, ela também é utilizada para instruções de desvio e para a instrução AUIPC.

Nas instruções de desvio, a operação selecionada é sempre a subtração. Isso ocorre porque a subtração fornece todas as informações necessárias por meio dos sinais CNZV para avaliar comparações de forma eficaz.

Além das operações de desvio, a instrução AUIPC captura o valor do contador de programa e adiciona um valor imediato a ele.

Em versões mais antigas do DRISC, o somador da ULA era usado para calcular endereços de salto para o contador de programa. No entanto, para suportar o pipeline durante instruções de desvio, um circuito somador adicional foi introduzido dentro do próprio Contador de Programa.

Clone this wiki locally