Nas últimas semanas tenho estudado a circ, uma linguagem textual para descrever e simular circuitos lógicos digitais. A proposta dela é simples: em vez de arrastar portas lógicas em um simulador gráfico, você escreve o circuito em texto e roda ele direto no navegador, pelo playground oficial.

Para praticar, criei o repositório aprendendo-circ-lang, onde fui construindo circuitos combinacionais e sequenciais do zero, de uma porta lógica isolada até um pequeno somador de 3 bits com registrador e saída em display de 7 segmentos. Neste post vou mostrar esses circuitos e explicar o que cada trecho de código faz.
Sintaxe básica da circ
Antes de ir para os circuitos, vale entender os elementos principais da linguagem (a referência completa tem todos os detalhes).
inputdeclara pinos de entrada. Também aceita barramentos:input[3] acria uma entrada de 3 bits;output nome(in = ...)declara uma saída, sempre vinculada a um sinal através da portain;- portas lógicas (
and,or,not,xor,nand,nor,xnor) são instanciadas como componentes, cada uma com um nome e as portas conectadas entre parênteses, por exemploand g(a = x, b = y); wireé um componente de passagem, usado para nomear um sinal intermediário. Ele tem uma portaine uma portaout;importtraz um circuito de outro arquivo para ser reutilizado como um componente, por exemploimport HA "HA.circ";{a, b}concatena sinais em um barramento maior, esinal[i]extrai um bit específico dele;- comentários seguem o estilo
// comentário até o fim da linha.
Com isso já dá para ler qualquer um dos circuitos abaixo.
Half Adder: somando dois bits
O primeiro circuito do repositório é o meio somador (HA.circ), que soma dois bits e gera a soma (s) e o carry (c):
// HA - Half Adder (meio somador)
// S = A xor B, C = A and B
input a, b
xor s_g(a = a, b = b)
and c_g(a = a, b = b)
output s(in = s_g.out)
output c(in = c_g.out)
input a, bdeclara as duas entradas de 1 bit;xor s_g(...)instancia uma porta XOR chamadas_g, que calcula a soma sem considerar o carry;and c_g(...)instancia uma porta AND chamadac_g, que calcula o carry (só é1quandoaebsão1);- as duas saídas (
sec) são conectadas aooutde cada porta.
Full Adder: somando com carry de entrada
O somador completo (FA.circ) soma dois bits e um carry de entrada (cin), reaproveitando o HA que acabamos de ver:
// FA - Full Adder (somador completo)
// dois half adders + OR para o carry de saída
import HA "HA.circ"
input a, b, cin
HA ha1(a = a, b = b)
HA ha2(a = ha1.s, b = cin)
or cout_g(a = ha1.c, b = ha2.c)
output s(in = ha2.s)
output cout(in = cout_g.out)
Aqui aparece o import: HA deixa de ser só um arquivo e passa a ser um componente que pode ser instanciado, igual a uma porta lógica. A ideia do full adder é:
ha1somaaeb, gerando uma soma parcial (ha1.s) e um carry parcial (ha1.c);ha2soma essa soma parcial comcin, gerando a soma final (ha2.s) e outro carry parcial (ha2.c);- a porta
or cout_gcombina os dois carries parciais no carry de saída (cout) — se qualquer um dos dois half adders gerou carry, o full adder também gera.
Somador de 3 bits
Encadeando três FA, dá para somar números de 3 bits (somador3bits.circ):
// somador3bits - somador de 3 bits com carry ripple
// a[0]/b[0] = LSB, a[2]/b[2] = MSB
import FA "FA.circ"
input[3] a, b
input cin
FA fa0(a = a[0], b = b[0], cin = cin)
FA fa1(a = a[1], b = b[1], cin = fa0.cout)
FA fa2(a = a[2], b = b[2], cin = fa1.cout)
output[3] s(in = {fa0.s, fa1.s, fa2.s})
output cout(in = fa2.cout)
input[3] a, b já mostra os barramentos em ação: a e b passam a ter 3 bits, acessados como a[0] (menos significativo) até a[2] (mais significativo). Essa é a técnica de carry ripple: o carry de saída de um FA vira o cin do próximo, e a soma final é montada com {fa0.s, fa1.s, fa2.s}, que concatena os três bits de soma em um barramento de 3 bits.
Registrador: guardando um bit
Para a parte sequencial, registrador.circ implementa um flip-flop tipo D mestre-escravo, que só atualiza a saída na borda de descida do clock:
input dado, clock
not nclk(in = clock)
// latch mestre: aberto quando clock = 1
and m1(a = dado, b = clock)
and m2(a = qm, b = nclk.out)
or m_o(a = m1.out, b = m2.out)
wire qm(in = m_o.out)
// latch escravo: aberto quando clock = 0
and s1(a = qm.out, b = nclk.out)
and s2(a = qs, b = clock)
or s_o(a = s1.out, b = s2.out)
wire qs(in = s_o.out)
output s(in = qs.out)
São dois latches em série: o mestre fica “aberto” (acompanha dado) enquanto clock = 1, e o escravo só abre quando clock = 0. Como nunca ficam abertos ao mesmo tempo, o valor só se propaga do mestre para o escravo no instante em que o clock desce de 1 para 0 — daí o circuito ser sensível à borda de descida. Repare também no uso do wire para nomear os sinais intermediários qm e qs, o que deixa as realimentações (m2 usando qm, s2 usando qs) mais legíveis.
Empilhando três desses registradores com um clock compartilhado (registrado3bit.circ), dá para guardar um valor de 3 bits inteiro:
import registrador "registrador.circ"
input[3] d
input clock
registrador r0(dado = d[0], clock = clock)
registrador r1(dado = d[1], clock = clock)
registrador r2(dado = d[2], clock = clock)
output[3] q(in = {r0.s, r1.s, r2.s})
Juntando tudo: main.circ
O circuito principal importa o somador, o registrador de 3 bits e um decodificador para display de 7 segmentos (driver7seg.circ, que traduz um número de 3 bits nos sinais dos sete segmentos do display), conectando os três:
import somador3bits "somador3bits.circ"
import registrado3bit "registrado3bit.circ"
import driver7seg "driver7seg.circ"
input[3] a, b
input clk
// circ não tem constante, então: x AND (NOT x) == 0
not zero_n(in = clk)
and zero(a = clk, b = zero_n.out)
somador3bits add(a = a, b = b, cin = zero.out)
registrado3bit regs(d = add.s, clock = clk)
driver7seg display(n = regs.q)
output[3] soma(in = add.s)
output[3] registrado(in = regs.q)
Um detalhe interessante: a circ não tem um valor constante 0 embutido, então o truque x AND (NOT x) é usado para gerar um 0 fixo, usado como carry de entrada do somador. Fora isso, o main.circ é basicamente fiação: a + b entra no somador3bits, o resultado é guardado no registrado3bit a cada pulso de clock, e o valor registrado é decodificado pelo driver7seg para acender os segmentos corretos.
Testando no playground
Não é preciso instalar nada para rodar esses circuitos. Basta abrir o playground da circ e colar o conteúdo de qualquer arquivo .circ do repositório. O main.circ já dá para ver o somador completo funcionando, com os leds do display reagindo em tempo real.
Considerações finais
Construir esses circuitos aos poucos, começando de uma porta XOR isolada até um somador com registrador e display, ajudou bastante a fixar conceitos de lógica digital que, no papel, às vezes parecem abstratos. E como a circ é um projeto open source, criado pelo Jefferson Mourak, também é uma boa desculpa para estudar a linguagem e, quem sabe, contribuir com ela no futuro.
Quem quiser ver o repositório completo ou testar os circuitos pode acessar aprendendo-circ-lang no GitHub.