Computadora completa Turing de 8 bits en Factorio



Quiero compartir mi proyecto creado en Factorio basado en la lógica que ofrece este juego. Este proyecto se inspiró en una gran mente, que escribió una guía paso a paso para crear casi el mismo automóvil, pero en el mundo real. Te recomiendo verlo, te ayudará a entender y recrear este proyecto: Computadora de 8 bits



Inclino la cabeza ante Ben Eater, que me enseñó tanto a través de su canal, y quiero dedicarle este pequeño proyecto. ¡Buen trabajo Ben!



Aquí hay una computadora que calcula el número de Fibonacci, después de exceder el límite de 8 bits (255), realiza una bifurcación condicional y comienza de nuevo:



imagen


Veamos cómo funciona esta computadora. Y no temas, ¡estoy seguro de que con lo básico tú también puedes hacerlo! Comencemos con el diseño general de la computadora. Aquí he destacado las áreas importantes. A continuación explicaré cómo los creé.



CLK es un temporizador que proporciona sincronización de máquinas. Las CPU modernas pueden funcionar a 4-5 GHz (4-5.000.000.000 Hz). Por el momento, mi máquina puede funcionar a 2 Hz debido a las limitaciones de las puertas lógicas de Factorio; cada entrada debe calcularse para cada combinador (puerta), por lo que si tenemos 10 seguidas, entonces tenemos que esperar 10 juegos (fps ) para iniciar el siguiente ciclo del sistema. De lo contrario, las señales se confundirán y no se realizará el cálculo.



PC (Contador de programa, contador de programa): el contador indica en qué parte del programa estamos. Los programas se leen desde una memoria de 16 bytes (un byte contiene 8 bits). El contador cuenta hasta 16 (4 bits) en formato binario (0000, 0001, 0010, 0011, 0100, 0101 ... 1111), por lo que cada uno de estos cálculos nos da una dirección de registro, que luego podemos recuperar de la memoria y realizar con sus acciones. También contiene un salto que pone a cero el contador. Podemos ingresar un valor diferente para ir a una ubicación específica en nuestra memoria / código.



BUS es el punto de conexión principal para todos los componentes de la computadora. Podemos transferir datos hacia / desde él. Para hacer esto, usamos señales de control que abren / cierran las puertas de cada componente, de modo que más de dos puertas nunca estén abiertas (por lo tanto, los datos no se mezclan).



ALU es nuestra "calculadora" que realiza operaciones de suma y resta (¡puede hacer mucho más en CPU más complejas!). Recibe instantáneamente lo que está en los registros A y B, y luego realiza operaciones lógicas (seleccionamos la operación mediante el decodificador de instrucciones). Luego, estos datos se envían al bus (bus). ALU también almacena indicadores que se pueden usar en funciones de rama condicional.



Registros A y B: pueden almacenar números de 8 bits, que luego se concatenan en la ALU. Ambos registros pueden enviar y recibir datos del bus.



Dirección de registro / decodificador (RAD): lee una dirección de 4 bits del bus y decodifica cuánta RAM debemos leer. Por ejemplo, la dirección 1110 contiene el valor 0000 0011 (ver imagen).



RAM: las PC modernas suelen tener aproximadamente 16 GB de memoria (16 000 000 000 de bytes). Solo tenemos 16 bytes ... Esto nos deja espacio para 16 instrucciones o menos para que podamos almacenar datos / variables en otras partes de la memoria. Básicamente, la RAM aquí tiene 16 registros diferentes, como los que usamos en otros lugares, solo se accede a ellos a través de un decodificador de registros.



Registro de instrucciones (IR) / decodificador (DC): podemos poner datos en el registro de instrucciones desde el BUS y luego decodificarlos para saber cómo debe comportarse el programa. Solo se utilizan 4 bits (resaltados en turquesa), lo que nos da 16 tipos de comandos que se pueden programar. Digamos que tenemos un comando OUT que imprime lo que está almacenado en el registro A. Está codificado como 1110, por lo que cuando dicho comando llega al registro, podemos decodificarlo y decirle a la computadora cómo proceder.



Contador de microcódigo (MC): similar al contador de programa, pero ubicado dentro del decodificador de instrucciones. Nos da la capacidad de seguir cada comando.



LCD / Screen es en realidad un registro, pero más complicado, ya que imprime su contenido en una pantalla LCD (Lamp-Combinator-Display, "una pantalla de linternas y combinadores").



Tablero de interruptores (SB): este panel nos muestra qué funciones de interruptor enviamos para controlar cada uno de los componentes de la computadora. Por el momento, hay 17 interruptores diferentes que controlan diferentes cosas. Por ejemplo, si queremos leer del BUS al registro A, o escribir en la memoria / registro de comando, etc. Los interruptores que se describen a continuación se pueden utilizar para controlar manualmente la máquina.



Banderas (F) - un registro para almacenar banderas (acarreo [T] - cuando superamos los valores de 8 bits al sumar, poniendo a cero [O] - cuando la suma / diferencia es 0). Nos ayudarán con los comandos de salto condicional.



imagen


Permítanme primero entrar en más detalles sobre cada componente, y al final veremos cómo programar una computadora, porque el proceso se volverá más claro. Si solo está interesado en la programación, pase a la última parte del artículo.



CLK es nuestro generador de temporización, lo más importante en cualquier cálculo. Quería crear un oscilador que tuviera una señal alta [C = 1] y baja [C = 0] al mismo tiempo.



(1) Este es un combinador constante básico que alimenta señales a un generador. Salta a (2) donde la entrada y la salida se fusionan. Gracias a esta configuración, con cada reloj de juego (UPS), el valor de [C] se incrementa en 1. Cuando llega a [Z], se reinicia a 0. Es decir, Z nos dice cuántos relojes de juego se necesitan para restablecer el generador. También hay un divisor simple por 2 debajo, que mantiene el generador alto durante la mitad del tiempo y bajo durante la mitad del tiempo. Cuando C es menor que [Y] (que es la mitad de [Z]), el generador es alto; de lo contrario, es bajo.



El insertador (4) se utiliza como generador de sincronización secundario en caso de que necesitemos más control sobre los ticks. Si pones algo en el primer cofre, se producirá un latido. Si necesitamos 5 barras, necesitamos colocar cinco objetos en él.



(5) es la primera señal de control. [H] es la abreviatura del comando HALT {HLT}. Cuando tiene un valor bajo [H = 0], entonces el generador opera normalmente, y si es alto, entra en modo manual. Esto es facilitado por las puertas de control, ellas (5a) se usan para operación normal, y cuando la señal [H] no es 0, entonces se activa el modo manual y se emite [C] (nuestro CLK).



También creé una señal invertida usando la puerta (6) - cuando la salida es baja, la señal invertida es alta. No lo uso en un automóvil, pero es una buena idea recordarlo para referencia futura.



La señal [C] viaja a través del sistema a través del cable verde. Quería aislarlo en un cable completamente separado (por ejemplo, nuestro BUS está en el cable rojo) para poder rastrearlo fácilmente y no confundirlo con otras señales.



imagen


Registros: no se deje intimidar por ellos. Esta es probablemente la parte más compleja de todo el sistema, pero es importante comprender cómo funciona toda la máquina.



Los registros contienen valores. En la vida normal, tendríamos que crear un registro para cada uno de los 8 bits y otras señales. Afortunadamente, Factorio te permite enviar múltiples señales a través de una sola línea. Básicamente, estos son desencadenantes de JK.



Brevemente sobre cómo funcionan. En cada pulso de sincronización, eliminan lo que hay dentro y mantienen el valor entrante. Si no hay valores entrantes (todos ceros), se borran en el ciclo de sincronización. Por supuesto, no queremos que estén siempre vacíos, después de todo, necesitamos almacenar valores en ellos. Por lo tanto, usamos la lógica de control, de la que ahora hablaré, y luego trataremos la magia negra de crear un disparador.



Los valores almacenados (1) se muestran con linternas. Cuando la luz está encendida, significa 1, y apagada significa 0. Como puede ver, actualmente estamos almacenando el valor 1110 1001.



Para enviar el valor al bus, usamos la lógica de control de puerta (2). Cuando la señal [K] es baja, esta puerta envía lo que esté dentro del registro al bus principal.



¿Por qué lo usamos cuando la señal es baja y no alta? Debido a que las puertas lógicas emiten todo lo que se les suministra (rojo *), y como resultado, el bus tendrá una señal [K], y no la necesitamos, solo necesitamos [7, 6, 5, 4, 3 , 2, 10]. Por la misma razón, necesitamos filtrar las señales de control con la puerta (3) para que recibamos [K] solo cuando lo necesitemos.



La puerta (4) está conectada tanto al bus (cable rojo) como a las señales de control (cable verde). Como en el caso anterior, el registro recibe una entrada cuando la señal [A] es baja. Para filtrar todas las demás señales, usamos una puerta lógica (4a). De hecho, toma todas las entradas del bus y las señales de control no deseadas, y luego las agrega al combinador (4b), cuyas entradas son siempre señales [7, 6, ... 0] = 1. Entonces, si alguna de las señales es 0, entonces genera cada una de estas señales = 1. Es simple, ¿verdad? En este caso, solo aquellos valores del bus que son importantes para nosotros entran en los registros (los valores 0 seguirán siendo 1, parpadean durante un ciclo de reloj y luego permanecen deshabilitados durante todo el ciclo de CLK alto).



En tal situación, cuando [C] sube, el obturador (6) emite la señal [NEGRO] y el obturador (6a) anula [C]. Pero como se necesita 1 UPS más para reducir a cero, la puerta (6) emite una señal en tan poco tiempo.



Esta señal se transfiere luego a la puerta (7), que también se abre durante un breve período de tiempo. La puerta (7b) anula la señal [NEGRA] para que no se almacene en la puerta (8), que se utiliza como custodio de nuestra señal. Esto es similar a la red CLK, porque tanto la entrada como la salida están conectadas entre sí. Si no hay entrada, permanece sin cambios. Si cambiamos el reloj una vez más sin ingresar nuevos datos, la puerta (7a) ingresará una señal invertida con respecto al valor almacenado en el registro para borrarlo.



Ahora que sabemos cómo funcionan el reconocimiento de cambios y los registros, lo sabemos casi todo.



imagen


ALU: suma / resta constantemente lo que está en los registros (A) y (B). Solo controlamos si enviarlo al bus [Z] o cambiar el modo para restar [S].



¿Cómo funciona? Para obtener una imagen completa, recomiendo ver algunos de los videos de Ben Iter, porque la explicación será tres veces más larga que mi artículo.



Solo explicaré cómo crear un sumador de este tipo en Factorio.



Para hacer esto, necesitamos tres tipos de puertas: XOR (1), AND (2) y OR (3). Afortunadamente, son bastante fáciles de crear. Dado que podemos usar múltiples señales en la misma línea, nuestra primera puerta XOR y AND se puede simplificar a solo dos, y no es necesario que las hagamos para los 8 bits. Esto nos permite hacer (4) parte de la cadena y duplicarla para cada bit.



La resta se realiza con la señal [S], que invierte las señales provenientes del registro (B).



ALU también lleva las salidas (cuando la suma supera los 8 bits), la pone a cero y la almacena en el registro de la derecha (F en la imagen con la computadora principal).



imagen


LCD / Pantalla: parece intimidante, pero honestamente, fue el más fácil de hacer. Solo se necesita tiempo para conectar todo correctamente.



Primero, creamos un registro cuya entrada está controlada por la señal [P]. Luego multiplicamos cada bit por su valor, convirtiéndolo en un valor decimal para obtener la misma señal con un valor decimal (esto es una especie de trampa en Factorio, pero la falta de EEPROM programables no nos permite dar la vuelta demasiado). Para convertir, solo necesitamos tomar el primer bit [0] y multiplicarlo por * 1, luego tomar el segundo bit [1] y multiplicar por * 2, el tercero [2] por * 4, y así sucesivamente. En el proceso, generamos un valor arbitrario para determinar el número resultante (en este caso, es [una gota de agua]).



La pantalla LCD se enciende en 9 pasos para los números (3). Solo necesitamos configurar esas luces correspondientes a los pasos (1), y luego usar las puertas (2) para generar el valor exactamente donde lo necesitamos. Solo necesita recordar agregar un combinador constante separado (3) y conectarlo a una sola puerta especial (2). Luego, simplemente conectamos todas las luces entre sí y les damos instrucciones sobre en qué paso se encuentran (1).



imagen


Registro de RAM / memoria (RAD): aquí explicaré cómo funciona la RAM de forma aproximada.



Ya conocemos registros que usan pulsos de sincronización para almacenar valores. La RAM es solo una cuadrícula de 16 (en nuestro caso) registros diferentes (2). Sus entradas están controladas por otro registro (1) que almacena 4 bits [0, 1, 2, 3], que nos dice a qué ubicación de memoria estamos apuntando. Esto se implementa mediante un decodificador de direcciones (3), que funciona de manera similar a LED / Pantalla. Cada puerta recibe un valor del combinador constante (en nuestro caso 1100 bin = 10 dec), y luego emite el nombre de la señal del registro correspondiente (en nuestro caso [M]) para que se pueda acceder al valor (en nuestro caso 00110 0011).



También vale la pena mencionar aquí la programación de memoria manual. Esto se puede hacer usando la señal [W], habilitada / deshabilitada usando el combinador constante (4). Otro combinador (5) nos permite cambiar la dirección y usamos otro combinador (6) para ingresar el valor. Al final, simplemente ponemos todo en el cofre (7), para que al sincronizar, transfiera manualmente los valores a la RAM, sin tocar el CLK principal del ordenador.



imagen


Contador de programa (PC): su tarea es calcular en qué paso del programa estamos (1). Al inicio, tiene un valor de 0000, esta dirección se lee de la RAM y se transfiere al registro de comandos para su interpretación. Después de que se completa el comando, podemos incrementar el contador con la señal [X], luego se vuelve igual a 0001, y en la siguiente iteración esta dirección se toma de la memoria y el ciclo continúa.



Por supuesto, a veces necesitamos realizar una ramificación incondicional o condicional a otras partes del programa. Podemos hacer esto con la señal [J]. Si es bajo (en nuestro caso, bajo significa activo), entonces se resetea, lee del bus la dirección a la que debe saltar y la almacena en el registro (2). Cuando [J] sube de nuevo, envía una señal al detector de cambio (ubicado directamente debajo de 2) a la PC.



El contador en sí funciona de manera similar a CLK, pero en lugar de contar constantemente los ciclos de reloj, cuenta los ciclos de reloj cuando se detectan cambios en CLK (de hecho, solo cuando X y CLK están activos). Esto se puede ver en la imagen (1).



A continuación, la señal se puede aplicar al bus mediante la señal de control [C].



imagen


Tablero de interruptores (SB): este es el momento adecuado para explicar cada señal de control utilizada en el programa.



Las señales se dividen en dos colores, las verdes van a la izquierda, las rojas van a la derecha. Cada señal de combinadores constantes se pasa realmente como valores [-1]. Es decir, cuando los combinadores están configurados en *! = 0, pueden emitir la señal 1. Debido a esto, cuando la lógica de control envía la señal [1], se cancelan y obtenemos [0], y en todos los casos solo necesito esto (puedes leer en la parte donde explico los registros).



[H] - detiene el generador de reloj (cambia al modo manual), una señal alta significa que CLK no está activado.



[Q] - la dirección de la RAM, en la que se encuentra el registro, con una señal alta, el registro de dirección de RAM guardará el valor del bus en el siguiente ciclo CLK.



[Y] - Entrada de memoria RAM, cuando la señal RAM es alta, guardará el valor del bus en el siguiente ciclo CLK (en la dirección almacenada en el registro de direcciones).



[R] - Salida RAM, cuando la señal RAM es alta, envía el valor al bus en el siguiente ciclo CLK (desde la dirección almacenada en el registro de direcciones).



[V] - entrada del registro de comando, cuando la señal es alta, el registro de comando guarda el valor del bus en el siguiente ciclo CLK.



[U] - salida del registro de comando, cuando la señal es alta, el registro de comando envía el valor al bus en el siguiente ciclo CLK (solo los últimos 4 bits [3, 2, 1, 0]).



[C] - salida del contador del programa, cuando la señal es alta, el contador del programa envía el valor al bus en el siguiente ciclo CLK (solo los primeros 4 bits [7, 6, 5, 4]).



[J] - entrada de la dirección de transición, cuando la señal es alta, el contador del programa establecerá el valor del bus en el siguiente ciclo CLK (solo los últimos 4 bits [3, 2, 1, 0]).



[X] - aumentando el valor del contador de comando, cuando la señal es alta, el contador del programa realiza un incremento en el siguiente ciclo CLK.



[A] - entrada del registro A, con una señal alta en el registro A, el valor del bus se guarda en el siguiente ciclo de reloj CLK.



[K] - salida del registro A, con una señal alta del registro A, el valor se envía al bus en el siguiente ciclo de reloj CLK.



[Z] - Pin ALU, cuando la señal ALU es alta, envía el valor al bus en el siguiente ciclo CLK.



[S] - resta (ALU), cuando la señal es alta, la ALU cambia su modo de suma a resta.



[B] - entrada del registro B, con una señal alta en el registro B, el valor del bus se guarda en el siguiente ciclo de reloj CLK.



[L] - salida del registro B, con una señal alta del registro B, el valor se envía al bus en el siguiente ciclo de reloj CLK.



[P]: al ingresar al registro de pantalla / LCD, cuando la señal es alta, el valor del bus se guarda en el registro de pantalla / LCD en el siguiente ciclo CLK y se muestra este valor.



[W] - entrada del registro de banderas, cuando la señal es alta, el registro de banderas guarda el acarreo de ALU (cuando se superan los 8 bits), cero (cuando la operación de ALU = 0000 0000).



[señal rosa] - bandera de transporte levantada [T]



[señal turquesa] - bandera cero levantada [O]



Ahora digamos que necesitamos realizar una acción OUT: tome lo que está en el registro A e imprímalo en la pantalla LCD / (registro). .. Para hacer esto manualmente, solo necesitamos encender (apagando el combinador constante para una determinada letra) la señal [K] (salida del registro A -> bus) y la señal [P] (bus -> entrada de registro lcd / pantalla), luego ejecute el reloj CLK.



imagen


Registro de comandos / decodificador / contador de microcódigo: aquí es donde comienza la magia. Ahora que sabemos cómo controlar manualmente una computadora, esto nos ayudará a comprender. lo que hay que hacer para que pueda manejarse a sí mismo.



(1) el contador del microcódigo contará hasta 8 (el número se puede reducir si no necesitamos tanto), es decir, podemos ejecutar 8 comandos on / off diferentes para realizar una acción en un comando.



(2) los comandos se leen en un registro desde el bus, para esto necesitamos encender las señales [C] (salida del contador de comandos -> bus) y [Q] (bus -> dirección de memoria de entrada), y luego leer RAM [R] (RAM de salida -> bus) al registro de comando [V] (bus -> registro de comando), y también para incrementar el contador [X].



Dado que todas las acciones anteriores deben realizarse cada vez, conecté todo esto (4) directamente al contador de microcódigo para que esto suceda cada vez que el contador pase por los pasos 0 y 1.



Cuando hay algo en el registro, podemos usar tablas de verdad similares a las que creamos para el registro de direcciones de RAM y la salida a la pantalla / LCD.



Los valores [D] del registro de comando (siempre es mayor que 8) y el contador de microcódigo (siempre igual o menor que 8) se pueden sumar, y usando el número resultante, podemos crear puertas lógicas. Esto se hace mediante puertas (3).



El ejemplo muestra el comando 0110 XXXX (48 + X en dec, para el cual programé el comando JMP), que luego se agrega al paso 2 del contador de microcódigo, que da 50.



Otro ejemplo: comando ADD (0010 XXXX - 16 + X en dec); después de los pasos 0 y 1 el microcódigo será 2, es decir, los registros 18-24 se pueden usar para otra parte del código (en este caso solo necesitamos 18-20, ya que ADD es un proceso de 3 pasos).



(5) Los indicadores de acarreo se procesan mediante puertas lógicas simples, la entrada se deshabilita en ellos solo si no se aplica ningún acarreo [T] o cero [O] a las puertas lógicas.



A continuación se muestra mi lista completa de comandos implementados (¡puede cambiarlos o agregar los suyos propios!):



0 NOP - 0000 XXXX - no hace nada.



1 LDA X - 0001 XXXX: carga el valor de la dirección X RAM en el registro A.



2 ADD X - 0010 XXXX - carga el valor de la dirección X RAM en el registro B, y luego genera la suma y la coloca en el registro A.



3 ADD X - 0011 XXXX - carga el valor de la dirección X RAM en el registro B, y luego genera la resta y la pone en el registro A.



4 STA X - 0100 XXXX - carga el valor del registro A y lo almacena en la RAM en la dirección



X.5 LDI X - 0101 XXXX - carga rápidamente el valor del registro de comando ( solo valor de 4 bits) en el registro A.



6 JMP X - 0110 XXXX - incondicional (siempre ocurre), la transición al valor X (PC asigna el valor de X).



7 JC X - 0111 XXXX - cuando el valor de acarreo [T] es verdadero, salta al valor X (asigna PC a X).



8 JO X - 1000 XXXX - cuando la transferencia de cero [O] es verdadera, entonces pasa al valor X (asigna el valor de PC a X).



9 OUR X - 1001 XXXX - muestra el valor de la dirección X RAM.

...

...

...

14 OUT - 1110 XXXX - Realiza la visualización desde el registro A (X no hace nada).



15 HLT - 1111 XXXX - detiene el generador de sincronización (X no hace nada).



¡Escribamos un programa simple y veamos cómo funciona!



0 LDA 3 - carga el valor en el registro A desde la dirección de memoria 3



1 OUT - muestra el valor del registro A.



2 HLT - detiene CLK, es decir, toda la máquina.



3 42 - valor almacenado



Es decir, de hecho, este programa genera el valor almacenado en la dirección 3 RAM (0011 en binario).



Vamos a convertirlo a binario:



0 Dirección: 0000, valor: 0001 0011



1 Dirección: 0001, valor: 1110 0000



2 Dirección: 0010, valor: 1111 0000



3 Dirección: 0011, valor: 0010 1010



Es decir, para escribir un programa, necesitamos escribir en la memoria (W en el panel de memoria; ver la parte con la imagen de RAM), comenzando en la dirección 0000, e ingresar el valor 0001 0011 adentro (0001 significa el comando LDA, donde 0011 es X, es decir, dirección 3 en la memoria) ...



Luego hacemos lo mismo con los otros equipos.



No olvide volver a poner [W] en verde y guardar la parada en el mostrador.



También puede reiniciar la PC saltando con J (no es necesario cambiar el ritmo CLK).



All Articles