Somadores e Subtratores
4. Somadores e Subtratores
4.1 Somadores de 1 bit a 16 bits
Para iniciar a exploração prática em Chisel, vamos construir um somador a partir do bloco mais básico: o Full Adder (somador completo de 1 bit). Em seguida, mostraremos como reutilizar esse bloco para formar um somador de 16 bits.
O código em Chisel se assemelha a uma mistura entre Scala (linguagem de alto nível) e operadores de hardware. Cada parte do código tem um papel específico na descrição do circuito.
class FullAddercria uma nova classe que representa o circuito.extends Moduleindica que essa classe descreve um módulo de hardware em Chisel.val io = IO(new Bundle {...})define as portas de entrada e saída do módulo.Input(Bool())eOutput(Bool())representam fios de 1 bit.- Os operadores
^,&e|descrevem lógica combinacional (XOR, AND, OR). - O símbolo
:=é usado para conectar sinais em Chisel (atribuição de hardware).
A partir desse bloco básico, podemos montar um somador de 16 bits, encadeando 16 instâncias do FullAdder.
UInt(16.W)define um fio com largura de 16 bits.Wire(Vec(...))cria vetores de sinais intermediários, aqui usados para armazenar somas e a propagação do carry.for (i <- 0 until 16)mostra como Chisel permite usar estruturas de controle da linguagem Scala para gerar hardware repetitivo, evitando escrever manualmente 16 instâncias deFullAdder.- A função
asUIntconverte o vetor de bits individuais em um único sinal de 16 bits.
4.2 Somadores completos parametrizados
No somador de 16 bits, a largura era fixa no código. Para torná-lo mais flexível, podemos parametrizar o número de bits utilizando um argumento na definição da classe. Assim, o mesmo código pode gerar somadores de qualquer largura.
Nesta linha a largura do somador passa a ser definida pelo parâmetro nBits, informado na instanciação do módulo, em vez de estar fixada em 16 bits.
Aqui os vetores auxiliares sums e carry assumem tamanhos variáveis de acordo com nBits. O vetor carry precisa de uma posição extra, já que a propagação do carry gera uma saída adicional.
O laço de repetição percorre exatamente nBits posições, instanciando automaticamente o número correto de FullAdders conforme a largura especificada.
Para instanciar um somador de 32 bits a partir da classe parametrizada, basta escrever:
4.3 Operadores Aritméticos de Alto Nível
Um ponto importante a destacar é o uso preferencial dos operadores de alto nível, como + e -, para implementar soma e subtração. Em FPGAs, esses operadores são automaticamente mapeados pelas ferramentas de síntese em blocos internos altamente otimizados, como carry chains e unidades aritméticas dedicadas, que fazem parte da lógica intrínseca do dispositivo. Isso garante implementações eficientes em termos de área e desempenho, sem exigir que o projetista especifique manualmente a arquitetura do somador. Já no caso de ASICs, a situação é diferente: não existem blocos aritméticos pré-definidos, sendo necessário escolher explicitamente a arquitetura a ser utilizada (ripple-carry, carry-lookahead, Kogge-Stone, entre outras). Essa escolha envolve um trade-off complexo entre área, latência e consumo de energia, e abre espaço para uma discussão muito mais profunda — que foge ao escopo deste tutorial.
A partir deste ponto, utilizaremos preferencialmente os operadores aritméticos de alto nível (+ e -) para implementar somadores e subtratores. Por exemplo, a classe CombParamSInt pode ser vista como uma versão avançada do uso direto de + e -, pois além de realizar soma e subtração, também verifica automaticamente se houve overflow durante as operações.
-
pad(nBits + 1)
Extende o operandoSIntem 1 bit adicional, preservando o sinal (sign-extend). Isso cria “folga” para capturar corretamente situações de overflow sem perder o bit de sinal. -
Wire(SInt((nBits + 1).W))
Cria um fio temporário (sumWide) com largura expandida em nBits + 1, onde será armazenado o resultado da operação antes de ser truncado. -
+&(soma com expansão)
Diferente do operador+, o+&preserva o bit extra gerado pela operação, garantindo que o resultado não perca a informação de overflow. -
sumWide(nBits - 1, 0).asSInt
Seleciona apenas os nBits menos significativos do resultado expandido, convertendo de volta para o tamanho original do módulo. Esse é o valor real da soma que será exposto emio.OutSum. -
(sumWide(nBits) ^ sumWide(nBits - 1)).asBool
Faz o XOR entre os dois bits mais significativos do resultado (MSB extraeMSB real), técnica padrão para detecção de overflow em operações com sinal. O resultado lógico (Bool) é disponibilizado emio.addOverflow.