Quero compartilhar meu projeto criado em Factorio com base na lógica oferecida por este jogo. Este projeto foi inspirado por uma grande mente, que escreveu um guia passo a passo para criar quase o mesmo carro, mas no mundo real. Recomendo assistir, pois vai te ajudar a entender e recriar este projeto: computador de 8 bits
Eu inclino minha cabeça para Ben Eater, que me ensinou muito através de seu canal, e quero dedicar este pequeno projeto a ele. Ótimo trabalho Ben!
Aqui está um computador calculando o número de Fibonacci, após exceder o limite de 8 bits (255), ele realiza uma ramificação condicional e começa de novo:
Vamos ver como funciona esse computador. E não tenha medo - tenho certeza de que com o básico você também pode fazer isso! Vamos começar com o layout geral do computador. Aqui, destaquei as áreas importantes. Abaixo vou explicar como os criei.
CLK é um cronômetro que fornece sincronização de máquina. CPUs modernas são capazes de operar em 4-5 GHz (4-5.000.000.000 Hz). No momento minha máquina pode rodar a 2 Hz devido às limitações das portas lógicas Factorio - cada entrada deve ser calculada para cada combinador (porta), então se temos 10 em uma linha, então precisamos esperar 10 relógio de jogo (fps ) para iniciar o próximo ciclo do sistema. Caso contrário, os sinais ficarão confusos e o cálculo não será executado.
PC (contador de programa, contador de programa) - o contador informa em qual parte do programa estamos. Os programas são lidos da memória de 16 bytes (um byte contém 8 bits). O contador conta até 16 (4 bits) em formato binário (0000, 0001, 0010, 0011, 0100, 0101 ... 1111), então cada um desses cálculos nos dá um endereço de registro, que podemos posteriormente recuperar da memória e executar com ele ações. Ele também contém um salto que zera o contador. Podemos inserir um valor diferente para ir a um local específico em nossa memória / código.
BUS é o principal ponto de conexão para todos os componentes do computador. Podemos transferir dados de / para ele. Para fazer isso, usamos sinais de controle que abrem / fecham as portas de cada componente, de forma que mais de duas portas nunca sejam abertas (assim, os dados não são misturados).
ALU é a nossa "calculadora" que realiza operações de adição e subtração (pode fazer muito mais em CPUs mais complexas!). Ele recebe instantaneamente o que está nos registros A e B e, em seguida, realiza operações lógicas (selecionamos a operação pelo decodificador de instruções). Em seguida, esses dados são enviados para o barramento (barramento). ALU também armazena sinalizadores que podem ser usados em funções de desvio condicional.
Registros A e B - eles podem armazenar números de 8 bits, que são posteriormente concatenados na ALU. Ambos os registradores podem enviar e receber dados do barramento.
Endereço de registro / decodificador (RAD) - lê um endereço de 4 bits do barramento e decodifica a quantidade de RAM que devemos ler. Por exemplo, o endereço 1110 contém o valor 0000 0011 (veja a imagem).
RAM - PCs modernos normalmente têm aproximadamente 16 GB de memória (16.000.000.000 bytes). Temos apenas 16 bytes ... Isso nos deixa espaço para 16 instruções ou menos instruções para que possamos armazenar dados / variáveis em outras partes da memória. Basicamente, a RAM aqui tem 16 registros diferentes, como os que usamos em outros lugares, eles são acessados apenas através de um decodificador de registro.
Registrador de instrução (IR) / decodificador (DC) - Podemos colocar dados no registrador de instrução do BUS e então decodificá-lo para informar como o programa deve se comportar. São usados apenas 4 bits (destacados em turquesa), o que nos dá 16 tipos de comandos que podem ser programados. Digamos que temos um comando OUT que imprime o que está armazenado no registro A. Ele é codificado como 1110, portanto, quando tal comando atinge o registro, podemos decodificá-lo e dizer ao computador como proceder.
Contador de microcódigo (MC) - Semelhante ao contador de programa, mas localizado dentro do decodificador de instrução. Nos dá a capacidade de executar cada comando.
LCD / Tela é na verdade um registro, porém mais complicado, pois imprime seu conteúdo em uma tela LCD (Lamp-Combinator-Display, "um display de lanternas e combinadores").
Placa do interruptor (SB) - Este painel mostra quais funções do interruptor enviamos para controlar cada um dos componentes do computador. No momento, existem 17 interruptores diferentes que controlam coisas diferentes. Por exemplo, se quisermos ler do BUS para o registro A, ou escrever na memória / registro de comando, etc. As opções descritas abaixo podem ser usadas para controlar manualmente a máquina.
Flags (F) - um registro para armazenar sinalizadores (carry [T] - quando excedemos os valores de 8 bits ao adicionar, zerar [O] - quando a soma / diferença é 0). Eles nos ajudarão com os comandos de salto condicional.
Deixe-me primeiro entrar em mais detalhes sobre cada componente, e no final veremos como programar um computador, porque o processo ficará mais claro. Se você está interessado apenas em programação, pule para a última parte do artigo.
CLK é o nosso gerador de tempo, a coisa mais importante em qualquer cálculo. Eu queria criar um oscilador que tivesse um sinal alto [C = 1] e um sinal baixo [C = 0] ao mesmo tempo.
(1) Este é um combinador constante básico que fornece sinais para um gerador. Ele pula para (2), onde a entrada e a saída são mescladas. Graças a esta configuração, com cada relógio de jogo (UPS), o valor de [C] é aumentado em 1. Quando atinge [Z], ele é redefinido para 0. Ou seja, Z nos informa quanto tempo de jogo leva para reinicie o gerador. Há também um divisor simples por 2 abaixo, que mantém o gerador alto pela metade do tempo e baixo pela metade do tempo. Quando C é menor que [Y] (que é a metade [Z]), o gerador está alto, caso contrário, está baixo.
O insersor (4) é usado como um gerador de sincronismo secundário no caso de precisarmos de mais controle sobre os ticks. Se você colocar algo no primeiro baú, uma batida acontecerá. Se precisarmos de 5 barras, precisamos colocar cinco objetos nela.
(5) é o primeiro sinal de controle. [H] é a abreviação de comando HALT {HLT}. Quando estiver com valor baixo [H = 0], o gerador opera normalmente e, se estiver alto, passa para o modo manual. Isto é facilitado pelas portas de controle, elas (5a) são usadas para operação normal, e quando o sinal [H] não é 0, então o modo manual é ativado e [C] (nosso CLK) é enviado.
Eu também criei um sinal invertido usando a porta (6) - quando a saída é baixa, o sinal invertido é alto. Não o uso no carro, mas é uma boa ideia lembrar para referência futura.
O sinal [C] viaja pelo sistema através do fio verde. Eu queria isolá-lo em um fio completamente separado (por exemplo, nosso BUS está no fio vermelho) para que possa ser facilmente rastreado e não confundido com outros sinais.
Registros - não se deixe intimidar por eles. Esta é provavelmente a parte mais complexa de todo o sistema, mas é importante entender como a máquina inteira funciona.
Os registros contêm valores. Na vida normal, teríamos que criar um registro para cada um dos 8 bits e outros sinais. Felizmente, o Factorio permite enviar vários sinais em uma única linha. Essencialmente, esses são gatilhos JK.
Resumidamente sobre como eles funcionam. Em cada pulso de sincronização, eles liberam o que está dentro e mantêm o valor de entrada. Se não houver valores de entrada (todos zeros), eles serão apagados no ciclo de sincronização. Claro, não queremos que eles fiquem sempre vazios, afinal, precisamos armazenar valores neles. Portanto, usamos a lógica de controle, da qual falarei agora, e lidaremos com a magia negra de criar um gatilho mais tarde.
Os valores armazenados (1) são exibidos com lanternas. Quando a luz está acesa, significa 1 e apagada significa 0. Como você pode ver, estamos atualmente armazenando o valor 1110 1001.
Para enviar o valor para o barramento, usamos a lógica de controle da porta (2). Quando o sinal [K] está baixo, esta porta envia o que quer que esteja dentro do registrador para o bus principal.
Por que o usamos quando o sinal está baixo e não alto? Como as portas lógicas produzem tudo o que é fornecido a elas (vermelho *) e, como resultado, o barramento terá um sinal [K], e não precisamos disso, só precisamos de [7, 6, 5, 4, 3 , 2, 10]. Pelo mesmo motivo, precisamos filtrar os sinais de controle com a porta (3) para que recebamos [K] apenas quando precisarmos dele.
O gate (4) é conectado ao barramento (fio vermelho) e aos sinais de controle (fio verde). Como no caso anterior, o registrador recebe uma entrada quando o sinal [A] está baixo. Para filtrar todos os outros sinais, usamos uma porta lógica (4a). Na verdade, ele pega todas as entradas do barramento e sinais de controle indesejados e, em seguida, adiciona-os ao combinador (4b), cujas entradas são sempre sinais [7, 6, ... 0] = 1. Então, se algum dos sinais é 0, então ele produz cada um desses sinais = 1. É simples, certo? Nesse caso, apenas os valores do barramento que são importantes para nós entram nos registradores (os valores 0 ainda serão 1, eles piscam por um ciclo de clock e então permanecem desabilitados durante todo o ciclo de CLK alto).
Em tal situação, quando [C] fica alto, o obturador (6) emite o sinal [PRETO] e o obturador (6a) anula [C]. Mas como é necessário mais 1 UPS para reduzir a zero, o gate (6) emite um sinal em tão pouco tempo.
Este sinal é então transferido para o gate (7), que também abre por um curto período de tempo. A porta (7b) anula o sinal [PRETO] para que ele não seja armazenado na porta (8), que é usada como guardiã do nosso sinal. Isso é semelhante à rede CLK, porque a entrada e a saída estão conectadas juntas. Se não houver entrada, ele permanecerá inalterado. Se mudarmos o relógio mais uma vez sem inserir novos dados, a porta (7a) irá inserir um sinal invertido em relação ao valor armazenado no registrador para apagá-lo.
Agora que sabemos como funcionam os registros e o reconhecimento de alterações, sabemos quase tudo.
ALU - constantemente adiciona / subtrai o que está nos registros (A) e (B). Nós apenas controlamos se a saída será no bus [Z] ou alteramos o modo para subtrair [S].
Como funciona? Para ter uma ideia completa, recomendo assistir a alguns vídeos de Ben Iter, porque a explicação será três vezes mais longa do que meu artigo.
Vou apenas explicar como criar tal somador no Factorio.
Para fazer isso, precisamos de três tipos de portas: XOR (1), AND (2) e OR (3). Felizmente, eles são fáceis de criar. Como podemos usar vários sinais na mesma linha, nossa primeira porta XOR e AND pode ser simplificada para apenas duas, e não precisamos fazer isso para todos os 8 bits. Isso nos permite fazer (4) parte da cadeia e duplicá-la para cada bit.
A subtração é realizada com o sinal [S], que inverte os sinais vindos do registrador (B).
A ALU também dá saída ao carry (quando a soma ultrapassa 8 bits), zerando e armazenando no registro à direita (F na imagem com o computador principal).
LCD / tela - Parece intimidante, mas, honestamente, foi o mais fácil de fazer. Leva apenas tempo para conectar tudo corretamente.
Primeiro, criamos um registrador cuja entrada é controlada pelo sinal [P]. Em seguida, multiplicamos cada bit pelo seu valor, convertendo-o em um valor decimal para obter o mesmo sinal com um valor decimal (isso é uma espécie de trapaça em Factorio, mas a falta de EEPROMs programáveis não nos permite dar meia volta). Para converter, precisamos apenas pegar o primeiro bit [0] e multiplicá-lo por * 1, depois pegar o segundo bit [1] e multiplicar por * 2, o terceiro [2] por * 4 e assim por diante. No processo, geramos algum valor arbitrário para determinar o número resultante (neste caso, é [uma gota d'água]).
O LCD liga em 9 etapas para números (3). Precisamos apenas definir as luzes correspondentes às etapas (1) e, em seguida, usar as portas (2) para gerar o valor exatamente onde precisamos. Você só precisa se lembrar de adicionar um combinador constante separado (3) e conectá-lo a apenas uma porta especial (2). Em seguida, simplesmente conectamos todas as luzes umas às outras e lhes damos instruções sobre em que etapa estão (1).
RAM / Memory Register (RAD) - Explicarei aqui como funciona a RAM.
Já conhecemos registradores que usam pulsos de sincronização para armazenar valores. A RAM é apenas uma grade de 16 (no nosso caso) registros diferentes (2). Suas entradas são controladas por outro registrador (1) que armazena 4 bits [0, 1, 2, 3], que nos diz para qual local da memória estamos apontando. Isso é implementado usando um decodificador de endereço (3), que funciona de maneira semelhante ao LED / Tela. Cada porta recebe um valor do combinador constante (no nosso caso 1100 bin = 10 dec) e, em seguida, emite o nome do sinal do registro correspondente (no nosso caso [M]) para que o valor possa ser acessado (no nosso caso 00110 0011).
A programação manual da memória também vale a pena mencionar aqui. Isso pode ser feito usando o sinal [W], habilitado / desabilitado usando o combinador constante (4). Outro combinador (5) nos permite alterar o endereço e usamos outro combinador (6) para inserir o valor. No final, basta colocarmos tudo no baú (7), para que ao sincronizar, transfira manualmente os valores para a RAM, sem tocar no CLK principal do computador.
Contador de programa (PC) - sua tarefa é calcular em qual etapa do programa estamos (1). Na inicialização, ele tem o valor 0000, esse endereço é lido da RAM e transferido para o registrador de comando para interpretação. Após a conclusão do comando, podemos incrementar o contador com o sinal [X], então ele se torna igual a 0001 e na próxima iteração este endereço é retirado da memória e o loop continua.
Claro, às vezes precisamos realizar uma ramificação incondicional ou condicional para outras partes do programa. Podemos fazer isso com o sinal [J]. Se for baixo (no nosso caso, baixo significa ativo), então ele é zerado, lê do barramento o endereço para o qual deve pular e o armazena no registrador (2). Quando [J] fica alto novamente, ele sinaliza o detector de mudança (localizado diretamente abaixo de 2) para o PC.
O contador em si funciona de forma semelhante ao CLK, mas em vez de contar constantemente os ciclos do clock, ele conta os ciclos do clock quando mudanças no CLK são detectadas (na verdade, apenas quando X e CLK estão ativos). Isso pode ser visto na imagem (1).
O sinal pode então ser aplicado ao barramento usando o sinal de controle [C].
Quadro elétrico (SB) - Este é o momento certo para explicar cada sinal de controle utilizado no programa.
Os sinais são divididos em duas cores, os verdes vão para a esquerda e os vermelhos vão para a direita. Cada sinal de combinadores constantes é realmente passado como valores [-1]. Ou seja, quando os combinadores são configurados para *! = 0, eles podem emitir o sinal 1. Devido a isso, quando a lógica de controle envia o sinal [1], eles são cancelados e obtemos [0], e em todos os casos nós precisa apenas disso (você pode ler na parte onde eu explico os registros).
[H] - para o gerador de relógio (muda para o modo manual), um sinal alto significa que CLK não está comutado.
[Q] - o endereço da RAM, na qual o registrador está localizado, com um sinal alto, o registrador de endereço RAM salvará o valor do barramento no próximo ciclo de CLK.
[Y] - entrada de memória RAM, quando o sinal de RAM estiver alto, salvará o valor do barramento no próximo ciclo de CLK (no endereço armazenado no registrador de endereço).
[R] - Saída de RAM, quando o sinal de RAM está alto, ele envia o valor para o barramento no próximo ciclo CLK (a partir do endereço armazenado no registrador de endereço).
[V] - entrada do registrador de comando, quando o sinal estiver alto, o registrador de comando salva o valor do barramento no próximo ciclo de CLK.
[U] - saída do registrador de comando, quando o sinal está alto, o registrador de comando emite o valor para o barramento no próximo ciclo CLK (apenas os últimos 4 bits [3, 2, 1, 0]).
[C] - saída do contador de programa, quando o sinal está alto, o contador de programa emite o valor para o barramento no próximo ciclo CLK (apenas os primeiros 4 bits [7, 6, 5, 4]).
[J] - entrada do endereço de transição, quando o sinal estiver alto, o contador do programa irá definir o valor do barramento no próximo ciclo CLK (apenas os últimos 4 bits [3, 2, 1, 0]).
[X] - aumentando o valor do contador de comando, quando o sinal está alto, o contador de programa realiza um incremento no próximo ciclo CLK.
[A] - entrada do registro A, com sinal alto no registro A, o valor do barramento é salvo no próximo ciclo do clock CLK.
[K] - saída do registro A, com um sinal alto do registro A, o valor é enviado ao barramento no próximo ciclo de clock CLK.
[Z] - Pino ALU, quando o sinal ALU está alto, ele envia o valor para o barramento no próximo ciclo CLK.
[S] - subtração (ALU), quando o sinal está alto, a ALU muda seu modo de adição para subtração.
[B] - entrada do registro B, com sinal alto no registro B, o valor do barramento é salvo no próximo ciclo do clock CLK.
[L] - saída do registro B, com um sinal alto do registro B, o valor é enviado ao barramento no próximo ciclo de clock CLK.
[P] - entrando no registro LCD / tela, quando o sinal está alto, o valor do barramento é salvo no registro LCD / tela no próximo ciclo CLK, e este valor é mostrado.
[W] - entrada do registrador de flags, quando o sinal está alto, o registrador de flags salva o carry da ALU (quando 8 bits são excedidos), zero (quando ALU operação = 0000 0000).
[sinal rosa] - bandeira de transporte levantada [T]
[sinal turquesa] - bandeira zero levantada [O]
Agora digamos que precisamos realizar uma ação OUT: pegue o que está no registro A e imprima no LCD / tela (registro). .. Para fazer isso manualmente, só precisamos ligar (desligando o combinador constante para uma determinada letra) o sinal [K] (saída do registrador A -> bus) e o sinal [P] (bus -> entrada do registrador lcd / tela) e, em seguida, execute o relógio CLK.
Registro de comando / decodificador / contador de microcódigo - é aqui que a mágica começa. Agora que sabemos como controlar manualmente um computador, isso nos ajudará a entender. o que precisa ser feito para que ele possa se controlar.
(1) o contador do microcódigo contará até 8 (o número pode ser reduzido se não precisarmos tanto), ou seja, podemos executar 8 comandos liga / desliga diferentes para realizar uma ação em um comando.
(2) os comandos são lidos em um registro do barramento, para isso precisamos ligar os sinais [C] (saída do contador de comando -> barramento) e [Q] (barramento -> endereço de memória de entrada), e então leia RAM [R] (saída RAM -> barramento) para o registro de comando [V] (barramento -> registro de comando), e também para incrementar o contador [X].
Como todas as ações acima precisam ser feitas todas as vezes, conectei tudo isso (4) diretamente ao contador do microcódigo para que isso aconteça toda vez que o contador passar pelos passos 0 e 1.
Quando houver algo no registro, podemos usar tabelas verdade semelhantes às que criamos para o registro de endereço de RAM e saída para o LCD / tela.
Os valores [D] do registrador de comando (é sempre maior que 8) e do contador do microcódigo (sempre igual ou menor que 8) podem ser adicionados, e usando o número resultante, podemos criar portas lógicas. Isso é feito por portas (3).
O exemplo mostra o comando 0110 XXXX (48 + X em dec, para o qual programei o comando JMP), que é então adicionado ao passo 2 do contador de microcódigo, que dá 50.
Outro exemplo: comando ADD (0010 XXXX - 16 + X em dez); após as etapas 0 e 1 o microcódigo será 2, ou seja, os registros 18-24 podem ser usados para outra parte do código (neste caso, precisamos apenas de 18-20, já que ADD é um processo de 3 etapas).
(5) Os sinalizadores de transporte são processados por portas lógicas simples, a entrada é desabilitada neles somente se nenhum transporte [T] ou zero [O] for aplicado às portas lógicas.
Abaixo está minha lista completa de comandos implementados (você pode alterá-los ou adicionar seus próprios!):
0 NOP - 0000 XXXX - não faz nada.
1 LDA X - 0001 XXXX - carrega o valor do endereço X RAM no registro A.
2 ADD X - 0010 XXXX - carrega o valor do endereço X RAM no registro B e, em seguida, produz a adição e a coloca no registro A.
3 ADD X - 0011 XXXX - carrega o valor do endereço X RAM no registro B, e, em seguida, produz a subtração e a coloca no registro A.
4 STA X - 0100 XXXX - carrega o valor do registro A e o armazena na RAM no endereço
X.5 LDI X - 0101 XXXX - carrega rapidamente o valor do registro de comando ( apenas valor de 4 bits) no registro A.
6 JMP X - 0110 XXXX - incondicional (sempre ocorre), a transição para o valor X (o PC atribui o valor de X).
7 JC X - 0111 XXXX - quando o valor de transporte [T] for verdadeiro, salta para o valor X (atribui PC a X).
8 JO X - 1000 XXXX - quando a transferência de zero [O] for verdadeira, vai para o valor X (atribui o valor do PC a X).
9 NOSSO X - 1001 XXXX - exibe o valor do endereço X RAM.
...
...
...
14 OUT - 1110 XXXX - Executa a exibição do registro A (X não faz nada).
15 HLT - 1111 XXXX - para o gerador de sincronização (X não faz nada).
Vamos escrever um programa simples e ver como funciona!
0 LDA 3 - carrega o valor no registrador A do endereço de memória 3
1 OUT - exibe o valor do registrador A.
2 HLT - para o CLK, ou seja, a máquina inteira.
3 42 - valor armazenado
Isto é, de fato, este programa emite o valor armazenado no endereço 3 RAM (0011 em binário).
Vamos convertê-lo em binário:
0 Endereço: 0000, valor: 0001 0011
1 Endereço: 0001, valor: 1110 0000
2 Endereço: 0010, valor: 1111 0000
3 Endereço: 0011, valor: 0010 1010
Ou seja, para escrever um programa, precisamos escrever na memória (W no painel de memória; ver a parte com a imagem RAM), começando no endereço 0000, e inserir o valor 0001 0011 dentro (0001 significa o comando LDA, onde 0011 é X, ou seja, o endereço 3 na memória) ...
Então fazemos o mesmo para as outras equipes.
Não se esqueça de tornar [W] verde novamente e salvar a parada para o balcão.
Você também pode reiniciar o PC saltando com J (não há necessidade de alterar a batida CLK).