Procesador suave nativo FPGA con compilador de lenguaje de alto nivel o Canción del Ratón

Procesador de software propio FPGA con compilador de lenguaje de alto nivel o Canción del mouse: experiencia en la adaptación de un compilador de lenguaje de alto nivel al núcleo del procesador de pila.

Un problema común para los procesadores de software es la falta de herramientas de desarrollo para ellos, especialmente si su sistema de instrucción no es un subconjunto de las instrucciones de uno de sus núcleos de procesador populares. Los desarrolladores en este caso tendrán que resolver este problema. Su solución directa es crear un compilador de lenguaje ensamblador. Sin embargo, en las realidades modernas no siempre es conveniente trabajar en Assembler, ya que en el proceso de desarrollo del proyecto, el sistema de comando puede cambiar debido, por ejemplo, a requisitos cambiantes. Por lo tanto, la tarea de implementar fácilmente un compilador de lenguaje de alto nivel (JAV) para un procesador de software es relevante.

Compilador de Python: Uzh parece ser un juego de herramientas fácil y conveniente para desarrollar software para procesadores de software. El juego de herramientas para definir primitivas y macros como funciones del lenguaje de destino permite implementar lugares críticos en el lenguaje ensamblador del procesador. Este artículo discute los puntos principales de la adaptación del compilador para procesadores de arquitectura de pila.

En lugar de un epígrafe:

si toma un ratón adulto
y, sosteniéndolo con cuidado,
mete las agujas en él
, obtendrá un erizo.

Si este erizo,
nariz tapada, para no respirar,
donde más profundo, tirar al río
obtendrá un ruff.

Si esto está mal, sosteniendo tu
cabeza en un vicio,
tira más fuerte de la cola
y obtendrás una serpiente.

Si esto ya está,
habiendo preparado dos cuchillos ...
Sin embargo, probablemente morirá, ¡
Pero la idea es buena!


Introducción


En muchos casos, al implementar instrumentos de medición, equipos de investigación, es preferible utilizar soluciones reconfigurables FPGA / FPGA como núcleo principal del sistema. Este enfoque tiene muchas ventajas, debido a la capacidad de realizar cambios en la lógica de trabajo de manera fácil y rápida, así como a la aceleración por hardware del procesamiento de datos y las señales de control.

Para una amplia gama de tareas, como el procesamiento de señales digitales, sistemas de control integrados, adquisición de datos y sistemas de análisis, el enfoque ha demostrado su eficacia, que consiste en combinar en una solución bloques implementados por la lógica FPGA para procesos críticos y elementos de control de programas basados ​​en uno o más Varios procesadores de software para la gestión y coordinación general, así como para implementar la interacción con el usuario o dispositivos / nodos externos. El uso de procesadores de software en este caso nos permite reducir ligeramente el tiempo dedicado a la depuración y verificación de algoritmos de control del sistema o algoritmos de interacción de nodos individuales.

Lista de deseos típica


A menudo, los procesadores blandos en este caso no requieren un rendimiento ultra alto (dado que es más fácil de lograr, utilizo los recursos lógicos y de hardware FPGA). Pueden ser bastante simples (y desde el punto de vista de los microcontroladores modernos, casi primitivos), porque pueden prescindir de un sistema de interrupción complejo, trabajar solo con ciertos nodos o interfaces, no hay necesidad de soportar un sistema de comando en particular. Puede haber muchos de ellos, mientras que cada uno de ellos solo puede ejecutar un cierto conjunto de algoritmos o subprogramas. La capacidad de los procesadores blandos también puede ser cualquiera, incluido no un múltiplo de un byte, según los requisitos de la tarea actual.

Los objetivos típicos para procesadores blandos son:

  • suficiente funcionalidad del sistema de comando, posiblemente optimizado para la tarea;
  • , .. ;
  • – , .

Por supuesto, un problema para los procesadores de software es la falta de herramientas de desarrollo para ellos, especialmente si su sistema de instrucción no es un subconjunto de las instrucciones de uno de sus núcleos de procesador populares. Los desarrolladores en este caso tendrán que resolver este problema. Su solución directa es crear un compilador de lenguaje ensamblador para el procesador de software. Sin embargo, en las realidades modernas no siempre es conveniente trabajar en Assembler, especialmente si el sistema del equipo cambia durante el desarrollo del proyecto debido, por ejemplo, a cambios en los requisitos. Por lo tanto, es lógico agregar a los requisitos anteriores el requisito de una fácil implementación de un compilador de lenguaje de alto nivel (HLV) para el procesador de software.

Componentes fuente


Los procesadores de pila satisfacen estos requisitos con un alto porcentaje de cumplimiento, como no es necesario direccionar registros, la profundidad de bits del comando puede ser pequeña.
La profundidad de bits de los datos para ellos puede variar y no está vinculada a la profundidad de bits del sistema de comando. Al ser una implementación de hardware de facto (aunque con algunas advertencias) de la representación intermedia del código del programa durante la compilación (una máquina virtual apilada, o en términos de gramáticas libres de contexto, un autómata de la tienda), es posible con bajos costos de mano de obra traducir la gramática de cualquier idioma en código ejecutable. Además, para los procesadores de pila, el lenguaje Fort es prácticamente el idioma "nativo". Los costos laborales de implementar un compilador Fort para un procesador de pila son comparables a los de Assembler, con mucha más flexibilidad y eficiencia en la implementación de programas en el futuro.

Teniendo la tarea de construir un sistema para recolectar datos de sensores inteligentes en un modo cercano al tiempo real, el procesador Fort fue seleccionado como la solución de referencia (el denominado Diseño de referencia) del procesador blando, descrito en [ 1 ] (en adelante será a veces denominado procesador whiteTiger por el apodo de su autor).

Sus características principales:

  • Datos separados y pilas de devolución
  • Arquitectura de organización de memoria de Harvard (programa separado y memoria de datos, incluido el espacio de direcciones);
  • expansión con periféricos usando un simple bus paralelo.
  • El procesador no usa una tubería, la ejecución de comandos es push-pull:

    1. buscar comandos y operandos;
    2. ejecución del comando y guardar el resultado.

El procesador se complementa con un cargador de código de programa UART, que le permite cambiar el programa ejecutable sin volver a compilar el proyecto para FPGA.

Con respecto a la configuración de la memoria de bloque en el FPGA, la capacidad de las instrucciones se establece en 9 bits. La profundidad de bits de los datos se establece en 32 bits, pero puede ser básicamente cualquiera.

El código del procesador está escrito en VHDL sin el uso de ninguna biblioteca específica, lo que le permite trabajar con este proyecto en FPGA de cualquier fabricante.

Para un uso relativamente extendido, reduciendo el "umbral de entrada", así como para reutilizar código y aplicar desarrollos de código, es más conveniente cambiar a un motor Java que no sea Fort (esto se debe en parte a las supersticiones y conceptos erróneos de los programadores de minas sobre la complejidad de este lenguaje y la legibilidad de su código (por cierto, uno de los autores de este trabajo tiene una opinión similar sobre los lenguajes tipo C)).

Basado en una serie de factores, se eligió el lenguaje Python (Python) para que el experimento "vincule" el procesador de software y el Java Language Engine. Este es un lenguaje de programación de propósito general de alto nivel enfocado en mejorar la productividad del desarrollador y la legibilidad del código, que admite varios paradigmas de programación, incluidos los estructurales, orientados a objetos, funcionales, imperativos y orientados a aspectos [ 2]

Para los desarrolladores novatos, su extensión MyHDL [ 3 , 4 ] es interesante , lo que permite describir elementos y estructuras de hardware en Python y traducirlos a código VHDL o Verilog.

Hace algún tiempo, se anunció el compilador Uzh [ 5 ], un pequeño compilador para el procesador de software Zmey FPGA (arquitectura de pila de 32 bits con soporte de subprocesamiento múltiple), si sigue la cadena de versiones / modificaciones / verificación, Zmey es un descendiente lejano del procesador whiteTiger).
Uzh también es un subconjunto estático compilado de Python, basado en el prometedor kit de herramientas raddsl (un conjunto de herramientas para crear rápidamente prototipos de compiladores DSL) [ 6 , 7 ].

Por lo tanto, los factores que influyeron en la elección de la dirección del trabajo se pueden formular aproximadamente de esta manera:

  • interés en herramientas que reducen el "umbral de entrada" para desarrolladores novatos de dispositivos y sistemas en FPGA (sintácticamente Python no es tan "aterrador" para un principiante como VHDL);
  • luchando por la armonía y un estilo único en el proyecto (es teóricamente posible describir los bloques de hardware y software necesarios del procesador de software en Python);
  • coincidencia aleatoria

Pequeños matices "casi" sin sentido


El código fuente del procesador Zmey no está abierto, pero está disponible una descripción de los principios de su funcionamiento y algunas características de la arquitectura. Aunque también es apilable, existen varias diferencias clave con respecto al procesador whiteTiger:

  • las pilas son software, es decir representado por punteros y colocado en la memoria de datos en diferentes direcciones;
  • , - ;
  • ;
  • , .

En consecuencia, el compilador Uzh tiene en cuenta estas características. El compilador acepta el código Python y genera una secuencia de arranque en la salida para iniciar la memoria del programa y la memoria de datos del procesador, el punto clave es que toda la funcionalidad del lenguaje está disponible en la etapa de compilación.

Para instalar el compilador Uzh, simplemente descargue su archivo comprimido y descomprímalo en cualquier carpeta conveniente (es mejor cumplir con las recomendaciones generales para software especializado, para evitar rutas que contengan cirílico y espacios). También debe descargar y descomprimir el kit de herramientas raddsl en la carpeta principal del compilador.

La carpeta de prueba del compilador contiene ejemplos de programas para el procesador de software; la carpeta src contiene los textos fuente de los elementos del compilador. Por conveniencia, es mejor crear un pequeño archivo por lotes (extensión .cmd) con el contenido :, c.py C:\D\My_Docs\Documents\uzh-master\tests\abc.py donde abc.py es el nombre del archivo con el programa para el procesador de software.

Una serpiente mordiendo su cola o lapeando hierro y software


Para adaptar Uzh al procesador whiteTiger, se requerirán algunos cambios, así como el propio procesador deberá corregirse ligeramente.

Afortunadamente, no hay muchos lugares para ajustar en el compilador. Los principales archivos "dependientes del hardware":

  • asm.py: ensamblador y formación de números (literales);
  • gen.py: reglas de generación de código de bajo nivel (funciones, variables, transiciones y condiciones);
  • stream.py - formando una secuencia de arranque;
  • macro.py - definiciones de macro, de hecho - extensiones del lenguaje base con funciones específicas de hardware.

En el diseño original del procesador whiteTiger, el cargador UART solo inicializa la memoria del programa. El algoritmo del gestor de arranque es simple, pero bien establecido y confiable:

  • al recibir un cierto byte de control, el cargador establece el nivel activo en la línea interna de reinicio del procesador;
  • el segundo comando de byte restablece el contador de direcciones de memoria;
  • La siguiente es una secuencia de cuadernos de la palabra transmitida, comenzando por el más joven, combinado con un número de cuaderno;
  • después de cada byte con un cuaderno empaquetado, sigue un par de bytes de control, el primero de los cuales establece el nivel activo en la línea de permiso de escritura de memoria, el segundo lo restablece;
  • una vez completada la secuencia de cuadernos empaquetados, el byte de control elimina el nivel activo en la línea de reinicio.

Dado que el compilador también usa memoria de datos, es necesario modificar el cargador para que también pueda inicializar la memoria de datos.

Dado que la memoria de datos está involucrada en la lógica del núcleo del procesador, es necesario multiplexar sus datos y líneas de control. Para esto, se introducen señales adicionales DataDinBtemp, LoaderAddrB, DataWeBtemp: datos, dirección y resolución de grabación para el puerto En memoria.

El código del gestor de arranque ahora se ve así:

uart_unit: entity work.uart
--uart_unit: entity uart
  Generic map(
    ClkFreq => 50_000_000,
    Baudrate => 115200)
  port map(
    clk => clk,
    rxd => rx,
    txd => tx,
    dout => receivedByte,
    received => received,
    din => transmitByte,
    transmit => transmit);
    
process(clk)
begin
  if rising_edge(clk) then
    if received = '1' then
      case conv_integer(receivedByte) is
      -- 0-F   - 0-3 bits
        when 0 to 15 => CodeDinA(3 downto 0) <= receivedByte(3 downto 0);
		                  DataDinBtemp(3 downto 0) <= receivedByte(3 downto 0);
      -- 10-1F -4-7bits
        when 16 to 31 => CodeDinA(7 downto 4) <= receivedByte(3 downto 0);
		                   DataDinBtemp(7 downto 4) <= receivedByte(3 downto 0); 
      -- 20-2F -8bit 
        when 32 to 47 => CodeDinA(8) <= receivedByte(0);
	                   DataDinBtemp(11 downto 8) <= receivedByte(3 downto 0);
	  when 48 to 63 => DataDinBtemp(15 downto 12) <= receivedByte(3 downto 0);
	  when 64 to 79 => DataDinBtemp(19 downto 16) <= receivedByte(3 downto 0);
	  when 80 to 95 => DataDinBtemp(23 downto 20) <= receivedByte(3 downto 0);
	  when 96 to 111 => DataDinBtemp(27 downto 24) <= receivedByte(3 downto 0);
        when 112 to 127 => DataDinBtemp(31 downto 28) <= receivedByte(3 downto 0);

      -- F0 addr=0
        when 240 => CodeAddrA <= (others => '0');
      -- F1 - WE=1
        when 241 => CodeWeA <= '1';
      -- F2 WE=0 addr++
        when 242 => CodeWeA <= '0'; CodeAddrA <= CodeAddrA + 1;
      -- F3 RESET=1
        when 243 => int_reset <= '1';
      -- F4 RESET=0
        when 244 => int_reset <= '0';

      -- F5 addr=0
        when 245 => LoaderAddrB <= (others => '0');
      -- F6 - WE=1
        when 246 => DataWeBtemp <= '1';
      -- F7 WE=0 addr++
        when 247 => DataWeBtemp <= '0'; LoaderAddrB <= LoaderAddrB + 1;
		  
		  
        when others => null;
      end case;
    end if;
  end if;
end process;

---- end of loader


Con un nivel de reinicio activo, las señales DataDinBtemp, LoaderAddrB, DataWeBtemp se conectan a los puertos de memoria de datos correspondientes.

if reset = '1' or int_reset = '1' then
      DSAddrA <= (others => '0');      
      
      RSAddrA <= (others => '0');
      RSAddrB <= (others => '0');
      RSWeA <= '0';
      
      DataAddrB <= LoaderAddrB;
		DataDinB<=DataDinBtemp;
		DataWeB<=DataWeBtemp;
      DataWeA <= '0';

De acuerdo con el algoritmo del gestor de arranque, es necesario modificar el módulo stream.py. Ahora tiene dos funciones. La primera función, get_val () , divide la palabra de entrada en el número deseado de tétradas. Entonces, para las instrucciones de 9 bits del procesador whiteTiger, se transformarán en grupos de tres tétradas y datos de 32 bits en una secuencia de ocho tétradas. La segunda función make () forma el bootstrap directamente.
La forma final del módulo de transmisión:

def get_val(x, by_4):
  r = []
  for i in range(by_4):
    r.append((x & 0xf) | (i << 4))
    x >>= 4
  return r

def make(code, data, core=0):
  #        0  
  stream = [243,245] 
  for x in data:
    #    32- 
    #         
    stream += get_val(x, 8) + [246, 247]
  #       0
  stream += [240]
  for x in code:
    #    9-  
    #         
    stream += get_val(x, 3) + [241, 242]
  #  
  stream.append(244)

  return bytearray(stream)


Los siguientes cambios en el compilador afectarán el módulo asm.py, que describe el sistema de comando del procesador (se escriben los comandos y los códigos de operación del comando) y la forma de representar / compilar valores numéricos: literales.

Los comandos se empaquetan en un diccionario, y la función lite () es responsable de los literales. Si todo es simple con el sistema de comandos: la lista de mnemónicos y los códigos de operación correspondientes simplemente cambia, entonces la situación con literales es un poco diferente. El procesador Zmey tiene instrucciones de 8 bits y hay varias instrucciones especializadas para trabajar con literales. En whiteTiger, el noveno bit indica si el código de operación es un comando o parte de un número.

Si el bit más alto (noveno) de una palabra es 1, el código de operación se interpreta como un número; por ejemplo, cuatro códigos de operación consecutivos con un signo de un número forman un número de 32 bits como resultado. Un signo del final de un número es la presencia del código de operación del comando: para definir con precisión y garantizar la uniformidad, el final de la determinación del número es el código de operación del comando NOP ("sin operaciones").

Como resultado, la función iluminada () modificada se ve así:


def lit(x):
  x &= 0xffffffff
  r = [] 
  if (x>>24) & 255 :
    r.append(int((x>>24) & 255) | 256)
  if (x>>16) & 255:
    r.append(int((x>>16) & 255) | 256)
  if (x>>8) & 255:
    r.append(int((x>>8) & 255) | 256)
  r.append(int(x & 255) | 256)
  r += asm("NOP")
  return list(r)


Los cambios / definiciones principales y más importantes se encuentran en el módulo gen.py. Este módulo define la lógica básica del trabajo / ejecución de código de alto nivel a nivel de ensamblador:

  • saltos condicionales e incondicionales;
  • llamar funciones y pasarles argumentos;
  • retorno de funciones y resultados devueltos;
  • ajustes a los tamaños de memoria de programa, memoria de datos y pilas;
  • secuencia de acciones al inicio del procesador.

Para admitir Java, el procesador debe poder trabajar arbitrariamente con memoria y punteros y tener un área de memoria para almacenar funciones variables locales.

En el procesador Zmey, se utiliza una pila de retorno para trabajar con variables locales y argumentos de función: los argumentos de función se transfieren a ella y durante el trabajo posterior, se accede a ellos a través del registro de puntero de la pila de retorno (leer, modificar arriba / abajo, leer en la dirección del puntero). Dado que la pila se encuentra físicamente en la memoria de datos, tales operaciones esencialmente se reducen a operaciones de memoria, y las variables globales se encuentran dentro de la misma memoria.

En whiteTiger, las pilas de retorno y de datos son pilas de hardware dedicadas con su espacio de direcciones y no tienen instrucciones de puntero de pila. En consecuencia, las operaciones con pasar argumentos a funciones y trabajar con variables locales deberán organizarse a través de la memoria de datos. No tiene mucho sentido aumentar el volumen de pilas de datos y los retornos para el posible almacenamiento de matrices de datos relativamente grandes en ellos; es más lógico tener una memoria de datos ligeramente grande.

Para trabajar con variables locales, se agregó un registro LocalReg dedicado, cuya tarea es almacenar un puntero en el área de memoria asignada para las variables locales (una especie de montón). También se agregaron operaciones para trabajar con él (archivo cpu.vhd - área de definición de comando):


          -- group 1; pop 0; push 1;
          when cmdLOCAL => DSDinA <= LocalReg;
			 when cmdLOCALadd => DSDinA <= LocalReg; LocalReg <= LocalReg+1;
			 when cmdLOCALsubb => DSDinA <= LocalReg; LocalReg <= LocalReg-1;
          -- group 2; pop 1; push 0;
          when cmdSETLOCAL => LocalReg <= DSDinA;

LOCAL: devuelve a la pila de datos el valor actual del puntero LocalReg;
SETLOCAL: establece el nuevo valor de puntero recibido de la pila de datos;
LOCALadd: deja el valor actual del puntero en la pila de datos y lo incrementa en 1;
LOCALsubb: deja el valor actual del puntero en la pila de datos y lo disminuye en 1.
LOCALadd y LOCALsubb se agregan para reducir el número de tics durante las operaciones de pasar parámetros de función y viceversa.

A diferencia del whiteTiger original, las conexiones de la memoria de datos se modificaron ligeramente: ahora el puerto de entrada de memoria se dirige constantemente por la salida de la primera celda de la pila de datos, la salida de la segunda celda de la pila de datos se alimenta a su entrada:

-- ++
DataAddrB <= DSDoutA(DataAddrB'range);
DataDinB <= DSDoutB;

La lógica para ejecutar los comandos STORE y FETCH también se ha corregido ligeramente: FETCH recibe el valor de salida del puerto En memoria en la parte superior de la pila de datos, y STORE simplemente controla la señal de habilitación de escritura para el puerto B:

-- group 3; pop 1; push 1;
          when cmdFETCH => DSDinA <= DataDoutB;
          when cmdSTORE =>            
            DataWeB <= '1';

Como parte de la capacitación, así como para un poco de soporte de hardware para bucles en un nivel bajo (y en el nivel del compilador del lenguaje Fort), se agregó una pila de contadores de bucles al núcleo de WhiteTiger (las acciones son similares a las que se realizan al declarar datos y devolver pilas):

--  
type TCycleStack is array(0 to LocalSize-1) of DataSignal;
signal CycleStack: TCycleStack;
signal CSAddrA, CSAddrB: StackAddrSignal;
signal CSDoutA, CSDoutB: DataSignal;
signal CSDinA, CSDinB: DataSignal;
signal CSWeA, CSWeB: std_logic;
--  
process(clk)
begin
  if rising_edge(clk) then
    if CSWeA = '1' then
      CycleStack(conv_integer(CSAddrA)) <= CSDinA;
      CSDoutA <= CSDinA;
    else
      CSDoutA <= CycleStack(conv_integer(CSAddrA));
    end if;
  end if;
end process;


Se han agregado comandos de contador de ciclos.

DO: mueve el número de iteraciones del ciclo de la pila de datos a la pila del contador y coloca el valor incrementado del contador de instrucciones en la pila de retorno.

LOOP: comprueba la puesta a cero del contador; si no se alcanza, el elemento superior de la pila de contadores disminuye, se realiza la transición a la dirección en la parte superior de la pila de retorno. Si la parte superior de la pila de contador es cero, el elemento superior se restablece, la dirección de retorno al comienzo del ciclo desde la parte superior de la pila de retorno también se restablece.


	when cmdDO => -- DO - 
               RSAddrA <= RSAddrA + 1; -- 
               RSDinA <= ip + 1;
               RSWeA <= '1';
				
               CSAddrA <= CSAddrA + 1; --
         		CSDinA <= DSDoutA;
 		         CSWeA <= '1';
		         DSAddrA <= DSAddrA - 1; --
		         ip <= ip + 1;	-- 

      when cmdLOOP => --            
           if conv_integer(CSDoutA) = 0 then
	          ip <= ip + 1;	-- 
		         RSAddrA <= RSAddrA - 1; -- 
		         CSAddrA <= CSAddrA - 1; -- 
            else
		         CSDinA <= CSDoutA - 1;
		         CSWeA <= '1';
		         ip <= RSDoutA(ip'range);
            end if;
			 

Ahora puede comenzar a modificar el código para el módulo gen.py.

* Las variables _SIZE no necesitan comentarios y solo requieren la sustitución de los valores especificados en el proyecto principal del procesador.

La lista STUB es un código auxiliar temporal para crear un lugar para las direcciones de transición y luego llenarlas con el compilador (los valores actuales corresponden al espacio de direcciones de 24 bits de la memoria de código).

Lista de INICIO: establece la secuencia de acciones realizadas por el núcleo después de un reinicio; en este caso, la dirección de inicio de la memoria de variables locales se establece en 900 y la transición al punto de inicio (si no cambia nada, el punto de inicio / entrada en la aplicación se escribe en el compilador en la dirección de memoria de datos 2):

STARTUP = asm("""
900  SETLOCAL
2 NOP FETCH JMP
""")

La definición de func () prescribe las acciones que se realizan cuando se llama a la función, a saber, la transferencia de argumentos de función a la región de variables locales, asignación de memoria para sus propias variables locales de la función.

@act
def func(t, X):
  t.c.entry = t.c.globs[X]
  t.c.entry["offs"] = len(t.c.code) # - 1
  args = t.c.entry["args"]
  temps_size = len(t.c.entry["locs"]) - args
#      
  t.out = asm("LOCALadd STORE " * args)
  if temps_size:
#      
    t.out += asm("LOCAL %d PLUS SETLOCAL" % temps_size)
  return True

Epilog () define acciones al regresar de una función, liberando la memoria de variables temporales, volviendo al punto de llamada.

def epilog(t, X):
  locs_size = len(t.c.entry["locs"])
#    
  t.out = asm("RET")
  if locs_size:
#    ()  
    t.out = asm("LOCAL %d MINUS SETLOCAL" % locs_size) + t.out
  return True


El trabajo con variables se realiza a través de sus direcciones, la definición clave para esto es push_local (), que deja la dirección de la variable de "alto nivel" en la pila de datos.

def push_local(t, X):
#          
#  
  t.out = asm("LOCAL %d MINUS" % get_loc_offset(t, X))
  return True

Los siguientes puntos clave son transiciones condicionales e incondicionales. El salto condicional en el procesador whiteTiger comprueba el segundo elemento de la pila de datos para 0 y salta a la dirección en la parte superior de la pila si se cumple la condición. Un salto incondicional simplemente establece el valor del contador de instrucciones en el valor en la parte superior de la pila.

@act
def goto_if_0(t, X):
  push_label(t, X)
  t.out += asm("IF")
  return True

@act
def goto(t, X):
  push_label(t, X)
  t.out += asm("JMP")
  return True


Las siguientes dos definiciones especifican las operaciones de desplazamiento de bits: solo en un nivel bajo, se aplican bucles (dará cierta ganancia en el tamaño del código), en el original, el compilador simplemente coloca el número requerido de operaciones de desplazamiento elementales en una fila.

@act
def shl_const(t, X):
  t.out = asm("%d DO SHL LOOP" %(X-1))
  return True

@act
def shr_const(t, X):
  t.out = asm("%d DO SHR LOOP" %(X-1))
  return True

Y la definición principal del compilador en un nivel bajo es un conjunto de reglas para las operaciones de lenguaje y trabajar con memoria:

stmt = rule(alt(
  seq(Push(Int(X)), to(lambda v: asm("%d" % v.X))),
  seq(Push(Local(X)), push_local),
  seq(Push(Global(X)), push_global),
  seq(Load(), to(lambda v: asm("NOP FETCH"))),
  seq(Store(), to(lambda v: asm("STORE"))),
  seq(Call(), to(lambda v: asm("CALL"))),
  seq(BinOp("+"), to(lambda v: asm("PLUS"))),
  seq(BinOp("-"), to(lambda v: asm("MINUS"))),
  seq(BinOp("&"), to(lambda v: asm("AND"))),
  seq(BinOp("|"), to(lambda v: asm("OR"))),
  seq(BinOp("^"), to(lambda v: asm("XOR"))),
  seq(BinOp("*"), to(lambda v: asm("MUL"))),
  seq(BinOp("<"), to(lambda v: asm("LESS"))),
  seq(BinOp(">"), to(lambda v: asm("GREATER"))),
  seq(BinOp("=="), to(lambda v: asm("EQUAL"))),
  seq(BinOp("~"), to(lambda v: asm("NOT"))),
  seq(ShlConst(X), shl_const),
  seq(ShrConst(X), shr_const),
  seq(Func(X), func),
  seq(Label(X), label),
  seq(Return(X), epilog),
  seq(GotoIf0(X), goto_if_0),
  seq(Goto(X), goto),
  seq(Nop(), to(lambda v: asm("NOP"))),
  seq(Asm(X), to(lambda v: asm(v.X)))
))

El módulo macro.py le permite "expandir" el diccionario del idioma de destino utilizando definiciones de macro en el ensamblador del procesador de destino. Para el compilador de Java, las definiciones en macro.py no diferirán de los operadores y funciones "nativas" del lenguaje. Entonces, por ejemplo, en el compilador original, se definieron las funciones de E / S del valor en el puerto externo. Se agregaron secuencias de prueba de operaciones con memoria y variables locales y una operación de retardo de tiempo.

@macro(1,0)
def testasm(c,x):
  return Asm("1 1 OUTPORT 0 1 OUTPORT 11 10 STORE 10 FETCH 1 OUTPORT  15 100 STORE 100  FETCH 1 OUTPORT")

@macro(1,0)
def testlocal(c,x):
   return Asm("1 100 STORE 2 101 STORE 100 SETLOCAL LOCAL NOP FETCH 1 OUTPORT LOCAL 1 PLUS NOP FETCH 1 OUTPORT")

@prim(1, 0)
def delay(c, val):
  return [val, Asm("DO LOOP")]


Pruebas


Un pequeño programa de prueba de alto nivel para nuestro procesador contiene la definición de una función para calcular factorial y la función principal que implementa la salida en serie de valores factoriales de 1 a 7 al puerto en un bucle infinito.

def fact(n):
  r = 1
  while n > 1:
    r *= n
    n -= 1
  return r


def main():
  n=1
  while True:
     digital_write(1, fact(n))
     delay(10)
     n=(n+1)&0x7


Se puede iniciar para la compilación, por ejemplo, mediante un script simple o desde la línea de comandos mediante la secuencia: Como resultado, se generará un archivo de arranque stream.bin, que se puede transferir al núcleo del procesador en el FPGA a través del puerto serie (en realidades modernas, a través de cualquier puerto serie virtual que proporcionen los convertidores) Interfaces USB-UART). Como resultado, el programa ocupa 146 palabras (9 bits) de la memoria del programa y 3 en la memoria de datos.
c.py C:\D\My_Docs\Documents\uzh-master\tests\fact2.py




Conclusión


En general, el compilador Uzh parece ser un conjunto de herramientas fácil y conveniente para desarrollar software para procesadores de software. Es una gran alternativa al ensamblador, al menos en términos de usabilidad del programador. El conjunto de herramientas para definir primitivas y macros como funciones del lenguaje de destino permite implementar lugares críticos en el procesador ensamblador. Para los procesadores de arquitectura de pila, el procedimiento de adaptación del compilador no es demasiado complicado y largo. Podemos decir que este es el caso cuando la disponibilidad del código fuente del compilador ayuda: las secciones clave del compilador están cambiando.

Los resultados de la síntesis del procesador (capacidad de 32 bits, 4K palabras de memoria de programa y 1K RAM) para la serie FPGA Altera Cyclone V ofrecen lo siguiente:

Family	Cyclone V
Device	5CEBA4F23C7
Logic utilization (in ALMs)	694 / 18,480 ( 4 % )
Total registers	447
Total pins	83 / 224 ( 37 % )
Total virtual pins	0
Total block memory bits	72,192 / 3,153,920 ( 2 % )
Total DSP Blocks	2 / 66 ( 3 % )

Literatura

  1. Cuarto procesador en VHDL // m.habr.com/en/post/149686
  2. Python - Wikipedia // es.wikipedia.org/wiki/Python
  3. Comenzamos FPGA en Python _ Habr // m.habr.com/en/post/439638
  4. MyHDL // www.myhdl.org
  5. GitHub - compilador true-grue_uzh_ Uzh // github.com/true-grue/uzh
  6. GitHub - true-grue_raddsl_ Herramientas para la creación rápida de prototipos de compiladores DSL // github.com/true-grue/raddsl
  7. sovietov.com/txt/dsl_python_conf.pdf

El autor agradece a los desarrolladores del procesador de software Zmey y el compilador Uzh por consultas y paciencia.

All Articles