Processador programável FPGA nativo com compilador de idioma de alto nível ou Song of the Mouse

Próprio processador de software FPGA com compilador de idioma de alto nível ou Song of the Mouse - experiência na adaptação de um compilador de idioma de alto nível ao núcleo do processador de pilha.

Um problema comum para processadores de software é a falta de ferramentas de desenvolvimento para eles, especialmente se o sistema de instruções não for um subconjunto das instruções de um de seus núcleos populares. Os desenvolvedores nesse caso terão que resolver esse problema. Sua solução direta é criar um compilador de linguagem assembler. No entanto, nas realidades modernas nem sempre é conveniente trabalhar no Assembler, pois no processo de desenvolvimento do projeto o sistema de comando pode mudar devido a, por exemplo, alterações nos requisitos. Portanto, a tarefa de fácil implementação de um compilador de linguagem de alto nível (JAV) para um processador de software é relevante.

Compilador Python - Uzh parece ser um kit de ferramentas fácil e conveniente para o desenvolvimento de software para processadores de software. O kit de ferramentas para definir primitivas e macros como funções da linguagem de destino permite que locais críticos sejam implementados na linguagem de montagem do processador. Este artigo discute os principais pontos de adaptação do compilador para processadores de arquitetura de pilha.

Em vez de uma epígrafe:

se você pegar um rato adulto
e, segurando-o com cuidado,
enfiar as agulhas nele
, obterá um porco-espinho.

Se este ouriço,
Nariz entupido, para não respirar,
Onde mais fundo, jogue no rio
Você vai ter uma preguiça.

Se este ruff, segurando sua
cabeça em um vício,
puxe com mais força pela cauda
Você receberá uma cobra.

Se isso já,
Tendo preparado duas facas ...
No entanto, ele provavelmente vai morrer,
mas a idéia é boa!


Introdução


Em muitos casos, ao implementar instrumentos de medição e equipamentos de pesquisa, é preferível usar soluções FPGA / FPGA reconfiguráveis ​​como o núcleo principal do sistema. Essa abordagem tem muitas vantagens, devido à capacidade de fazer alterações com facilidade e rapidez na lógica do trabalho, bem como devido à aceleração por hardware do processamento de dados e dos sinais de controle.

Para uma ampla gama de tarefas, como processamento de sinal digital, sistemas de controle embarcados, aquisição de dados e sistemas de análise, a abordagem foi comprovada, consistindo na combinação de blocos de uma solução implementados pela lógica do FPGA para processos críticos e elementos de controle de programa baseados em um ou vários processadores de software para gerenciamento e coordenação geral, bem como para implementar a interação com o usuário ou dispositivos / nós externos. O uso de processadores de software nesse caso nos permite reduzir um pouco o tempo gasto na depuração e verificação de algoritmos de controle do sistema ou algoritmos de interação de nós individuais.

Lista de desejos típica


Muitas vezes, neste caso, os processadores flexíveis não exigem desempenho ultra-alto (já que é mais fácil de obter, eu uso os recursos lógicos e de hardware do FPGA). Eles podem ser bastante simples (e do ponto de vista dos microcontroladores modernos - quase primitivos), porque eles podem funcionar sem um sistema de interrupção complexo, trabalhar apenas com determinados nós ou interfaces; não há necessidade de oferecer suporte a um sistema de comando específico. Pode haver muitos deles, enquanto cada um deles pode executar apenas um determinado conjunto de algoritmos ou subprogramas. A capacidade dos processadores flexíveis também pode ser qualquer, incluindo não um múltiplo de byte - dependendo dos requisitos da tarefa atual.

Os alvos típicos para processadores flexíveis são:

  • funcionalidade suficiente do sistema de comando, possivelmente otimizada para a tarefa;
  • , .. ;
  • – , .

Obviamente, um problema para os processadores de software é a falta de ferramentas de desenvolvimento para eles, especialmente se o sistema de instruções não for um subconjunto das instruções de um de seus núcleos populares. Os desenvolvedores nesse caso terão que resolver esse problema. Sua solução direta é criar um compilador de linguagem assembler para o processador de software. No entanto, nas realidades modernas nem sempre é conveniente trabalhar no Assembler, principalmente se o sistema da equipe mudar durante o desenvolvimento do projeto devido a, por exemplo, alterações nos requisitos. Portanto, é lógico adicionar aos requisitos acima o requisito de fácil implementação de um compilador de linguagem de alto nível (HLV) para o processador programável.

Componentes de origem


Os processadores de pilha atendem a esses requisitos com uma alta porcentagem de conformidade, como não há necessidade de endereçar registros, a profundidade de bits do comando pode ser pequena.
A profundidade de bits dos dados para eles pode variar e não está vinculada à profundidade de bits do sistema de comando. Sendo uma implementação de hardware (embora com algumas ressalvas) de hardware da representação intermediária do código do programa durante a compilação (uma máquina empilhada virtual ou em termos de gramáticas sem contexto - um autômato de loja), é possível com baixos custos de mão de obra traduzir a gramática de qualquer idioma em código executável. Além disso, para processadores de pilha, o idioma Fort é praticamente o idioma "nativo". Os custos de mão-de-obra da implementação de um compilador Fort para um processador de pilha são comparáveis ​​aos do Assembler, com muito mais flexibilidade e eficiência na implementação de programas no futuro.

Tendo a tarefa de construir um sistema para coletar dados de sensores inteligentes em um modo próximo ao tempo real, o processador Fort foi selecionado como a solução de referência (o chamado Design de Referência) do processador flexível, descrito em [ 1 ] (daqui em diante será às vezes chamado de processador whiteTiger pelo apelido de seu autor).

Suas principais características:

  • Dados separados e pilhas de retorno
  • Arquitetura de organização de memória de Harvard (programa separado e memória de dados, incluindo espaço de endereço);
  • expansão com periféricos usando um barramento paralelo simples.
  • O processador não usa um pipeline, a execução dos comandos é push-pull:

    1. buscar comandos e operandos;
    2. execução do comando e salvando o resultado.

O processador é complementado por um UART-loader do código do programa, que permite alterar o programa executável sem recompilar o projeto para FPGAs.

No que diz respeito à configuração da memória do bloco no FPGA, a capacidade das instruções é definida em 9 bits. A profundidade de bits dos dados é definida em 32 bits, mas pode ser basicamente qualquer.

O código do processador é escrito em VHDL sem o uso de bibliotecas específicas, o que permite que você trabalhe com este projeto em FPGAs de qualquer fabricante.

Para um uso relativamente amplo, diminuindo o "limite de entrada", bem como para reutilizar código e aplicar desenvolvimentos de código, é mais aconselhável alternar para um mecanismo Java que não seja Fort (em parte devido às superstições e equívocos dos programadores de fluxo de minas em relação às complexidades dessa linguagem e à legibilidade de seu código. (a propósito, um dos autores deste trabalho tem uma opinião semelhante sobre linguagens do tipo C).

Com base em vários fatores, a linguagem Python (Python) foi escolhida para o experimento "vincular" o processador de software e o Java Language Engine. Essa é uma linguagem de programação de alto nível para uso geral, focada em melhorar a produtividade do desenvolvedor e a legibilidade do código, suportando vários paradigmas de programação, incluindo estrutural, orientada a objetos, funcional, imperativa e orientada a aspectos [ 2]

Para desenvolvedores iniciantes, sua extensão MyHDL [ 3 , 4 ] é interessante , o que permite descrever elementos e estruturas de hardware em Python e traduzi-los em código VHDL ou Verilog.

Algum tempo atrás, o compilador Uzh [ 5 ] foi anunciado - um pequeno compilador para o processador de software Zmey FPGA (arquitetura de pilha de 32 bits com suporte a multithreading - se você rastrear a cadeia de versões / modificações / verificação - Zmey é um descendente distante do processador whiteTiger).
Uzh também é um subconjunto estaticamente compilado do Python, baseado no promissor kit de ferramentas raddsl (um conjunto de ferramentas para criar rapidamente protótipos de compiladores DSL) [ 6 , 7 ].

Assim, os fatores que influenciaram a escolha da direção do trabalho podem ser formulados aproximadamente assim:

  • interesse em ferramentas que diminuam o "limite de entrada" para desenvolvedores iniciantes de dispositivos e sistemas em FPGAs (sintaticamente o Python não é tão "assustador" para um iniciante quanto o VHDL);
  • buscando harmonia e um estilo único no projeto (é teoricamente possível descrever os blocos de hardware e o software necessários do processador de software em Python);
  • coincidência aleatória.

Pequenas, "quase" nuances sem sentido


O código fonte do processador Zmey não está aberto, mas está disponível uma descrição dos princípios de sua operação e alguns recursos de arquitetura. Embora também seja empilhável, há várias diferenças importantes em relação ao processador whiteTiger:

  • pilhas são software - ou seja, representado por ponteiros e colocado na memória de dados em diferentes endereços;
  • , - ;
  • ;
  • , .

Consequentemente, o compilador Uzh leva esses recursos em consideração. O compilador aceita o código Python e gera um fluxo de inicialização para a saída da memória do programa e da memória de dados do processador na saída, o ponto principal é que toda a funcionalidade da linguagem está disponível no estágio de compilação.

Para instalar o compilador Uzh, basta baixar o arquivo morto e descompactá-lo em qualquer pasta conveniente (é melhor seguir as recomendações gerais para software especializado - evite caminhos que contenham cirílico e espaços). Você também precisa baixar e descompactar o kit de ferramentas raddsl na pasta principal do compilador.

A pasta de teste do compilador contém exemplos de programas para o processador virtual, a pasta src contém os textos de origem dos elementos do compilador. Por conveniência, é melhor criar um pequeno arquivo em lotes (extensão .cmd) com o conteúdo :, em c.py C:\D\My_Docs\Documents\uzh-master\tests\abc.py que abc.py é o nome do arquivo com o programa para o processador virtual.

Uma cobra mordendo a cauda ou lambendo ferro e software


Para adaptar o Uzh ao processador whiteTiger, serão necessárias algumas alterações, assim como o próprio processador precisará ser ligeiramente corrigido.

Felizmente, não há muitos lugares para serem ajustados no compilador. Os principais arquivos "dependentes de hardware":

  • asm.py - assembler e a formação de números (literais);
  • gen.py - regras de geração de código de baixo nível (funções, variáveis, transições e condições);
  • stream.py - formando um fluxo de inicialização;
  • macro.py - definições de macro, de fato - extensões da linguagem base com funções específicas de hardware.

No design original do processador whiteTiger, o carregador UART inicializa apenas a memória do programa. O algoritmo do carregador de inicialização é simples, mas bem estabelecido e confiável:

  • ao receber um determinado byte de controle, o carregador define o nível ativo na linha interna da redefinição do processador;
  • o comando do segundo byte redefine o contador do endereço de memória;
  • a seguir, uma sequência de cadernos da palavra transmitida, começando pelo mais novo, combinada com um número de caderno;
  • após cada byte com um bloco de notas compactado, segue um par de bytes de controle, o primeiro define o nível ativo na linha de permissão de gravação na memória e o segundo o redefine;
  • após a conclusão da sequência de cadernos embalados, o nível ativo na linha de redefinição é removido pelo byte de controle.

Como o compilador também usa memória de dados, é necessário modificar o carregador para que também possa inicializar a memória de dados.

Como a memória de dados está envolvida na lógica do núcleo do processador, é necessário multiplexar seus dados e linhas de controle. Para isso, são introduzidos os sinais adicionais DataDinBtemp, LoaderAddrB, DataWeBtemp - dados, endereço e resolução de gravação para a porta In memory.

O código do carregador de inicialização agora fica assim:

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


Com um nível de redefinição ativo, os sinais DataDinBtemp, LoaderAddrB, DataWeBtemp são conectados às portas de memória de dados correspondentes.

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 acordo com o algoritmo do carregador de inicialização, é necessário modificar o módulo stream.py. Agora ele tem duas funções. A primeira função - get_val () - divide a palavra de entrada no número desejado de tetrads. Portanto, para obter instruções de 9 bits do processador whiteTiger, elas serão transformadas em grupos de três tetrads e dados de 32 bits em uma sequência de oito tetrads. A segunda função make () forma o bootstrap diretamente.
A forma final do módulo de fluxo:

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)


As seguintes alterações no compilador afetarão o módulo asm.py, que descreve o sistema de comandos do processador (os comandos mnemônicos e opcodes de comandos são gravados) e a maneira de representar / compilar valores numéricos - literais.

Os comandos são compactados em um dicionário e a função lite () é responsável por literais. Se tudo é simples com o sistema de comando - a lista de mnemônicos e os códigos opcionais correspondentes muda, a situação com literais é um pouco diferente. O processador Zmey possui instruções de 8 bits e há várias instruções especializadas para trabalhar com literais. No whiteTiger, o nono bit indica se o código de operação é um comando ou parte de um número.

Se o bit mais alto (nono) de uma palavra for 1, o código de operação será interpretado como um número - por exemplo, quatro códigos de operação consecutivos com um sinal de número formam um número de 32 bits como resultado. Um sinal do final de um número é a presença do comando opcode - para garantir a uniformidade e a uniformidade, o fim da determinação do número é o opcode do comando NOP (“sem operações”).

Como resultado, a função lit () modificada se parece com isso:


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)


As principais e mais importantes alterações / definições estão no módulo gen.py. Este módulo define a lógica básica do trabalho / execução do código de alto nível no nível do assembler:

  • saltos condicionais e incondicionais;
  • chamar funções e passar argumentos para elas;
  • retorno de funções e retorno de resultados;
  • ajustes nos tamanhos de memória de programa, memória de dados e pilhas;
  • sequência de ações na inicialização do processador.

Para suportar Java, o processador deve poder trabalhar arbitrariamente com memória e ponteiros e ter uma área de memória para armazenar funções variáveis ​​locais.

No processador Zmey, uma pilha de retorno é usada para trabalhar com variáveis ​​locais e argumentos de função - argumentos de função são transferidos para ela e, durante trabalhos posteriores, são acessados ​​através do registro de ponteiro da pilha de retorno (leia, modifique para cima / para baixo, leia no endereço do ponteiro). Como a pilha está fisicamente localizada na memória de dados, essas operações simplesmente se resumem às operações de memória, e as variáveis ​​globais estão localizadas na mesma memória.

No whiteTiger, as pilhas de retorno e de dados são pilhas de hardware dedicadas com seu espaço de endereço e não possuem instruções de ponteiro de pilha. Consequentemente, operações com passagem de argumentos para funções e trabalho com variáveis ​​locais precisarão ser organizadas através da memória de dados. Não faz muito sentido aumentar o volume de pilhas e retornos de dados para possível armazenamento de matrizes de dados relativamente grandes nelas; é mais lógico ter uma memória de dados um pouco grande.

Para trabalhar com variáveis ​​locais, foi adicionado um registro LocalReg dedicado, cuja tarefa é armazenar um ponteiro para a área de memória alocada para variáveis ​​locais (um tipo de heap). Também foram adicionadas operações para trabalhar com ele (arquivo cpu.vhd - área de definição 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 - retorna para a pilha de dados o valor atual do ponteiro LocalReg;
SETLOCAL - define o novo valor do ponteiro recebido da pilha de dados;
LOCALadd - deixa o valor atual do ponteiro na pilha de dados e o incrementa em 1;
LOCALsubb - deixa o valor atual do ponteiro na pilha de dados e o diminui em 1.
LOCALadd e LOCALsubb são adicionados para reduzir o número de ticks durante operações de passagem de parâmetros de função e vice-versa.

Diferentemente do whiteTiger original, as conexões da memória de dados foram ligeiramente alteradas - agora a porta In memory é constantemente endereçada pela saída da primeira célula da pilha de dados, a saída da segunda célula da pilha de dados é alimentada à sua entrada:

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

A lógica para executar os comandos STORE e FETCH também foi levemente corrigida - FETCH leva o valor de saída da porta In memory para a parte superior da pilha de dados e STORE simplesmente controla o sinal de habilitação de gravação para a porta B:

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

Como parte do treinamento, assim como para algum suporte de hardware para loops em um nível baixo (e no nível do compilador da linguagem Fort), uma pilha de contadores de loop foi adicionada ao núcleo whiteTiger (as ações são semelhantes às da declaração de dados e pilhas de retorno):

--  
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;


Comandos de contador de ciclo foram adicionados.

DO - move o número de iterações do ciclo da pilha de dados para a pilha do contador e coloca o valor incrementado do contador de instruções na pilha de retorno.

LOOP - verifica o zeramento do contador, se não for alcançado, o elemento superior da pilha de contadores é diminuído, a transição para o endereço na parte superior da pilha de retorno é executada. Se a parte superior da pilha do contador for zero, o elemento superior é redefinido, o endereço de retorno do início da ciclo, a partir da parte superior da pilha de retorno, também é redefinido.


	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;
			 

Agora você pode começar a modificar o código do módulo gen.py.

* As variáveis ​​_SIZE não precisam de comentários e requerem apenas a substituição dos valores especificados no projeto principal do processador.

A lista STUB é um stub temporário para criar um local para endereços de transição e preenchê-los com o compilador (os valores atuais correspondem ao espaço de endereço de 24 bits da memória de código).

Lista STARTUP - define a sequência de ações executadas pelo kernel após uma redefinição - nesse caso, o endereço inicial da memória de variáveis ​​locais é definido como 900 e a transição para o ponto inicial (se você não alterar nada, o ponto inicial / de entrada no aplicativo é gravado no compilador no endereço da memória de dados 2):

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

A definição de func () prescreve as ações que são executadas quando a função é chamada, a saber, a transferência de argumentos da função para a região de variáveis ​​locais, alocação de memória para suas próprias variáveis ​​locais da função.

@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 ações ao retornar de uma função - liberando a memória de variáveis ​​temporárias, retornando ao ponto de chamada.

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


O trabalho com variáveis ​​é feito através de seus endereços, a definição de chave para isso é push_local (), que deixa o endereço da variável "alto nível" na pilha de dados.

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

Os seguintes pontos-chave são transições condicionais e incondicionais. O salto condicional no processador whiteTiger verifica o segundo elemento da pilha de dados em busca de 0 e pula para o endereço na parte superior da pilha, se a condição for atendida. Um salto incondicional simplesmente define o valor do contador de instruções para o valor no topo da pilha.

@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


As duas definições a seguir especificam operações de deslocamento de bits - apenas em um nível baixo, são aplicados loops (isso dará algum ganho no tamanho do código) - no original, o compilador simplesmente coloca o número necessário de operações de deslocamento elementar em uma linha.

@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

E a principal definição do compilador em um nível baixo é um conjunto de regras para operações de linguagem e trabalho com memória:

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)))
))

O módulo macro.py permite "expandir" o dicionário do idioma de destino usando definições de macro no montador do processador de destino. Para o Java Compiler, as definições em macro.py não diferem dos operadores e funções "nativos" da linguagem. Assim, por exemplo, no compilador original, foram definidas funções de E / S do valor na porta externa. Seqüências de teste de operações com memória e variáveis ​​locais e uma operação com atraso de tempo foram adicionadas.

@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")]


Teste


Um pequeno programa de alto nível de teste para nosso processador contém a definição de uma função para calcular fatorial e a principal função que implementa a saída serial de valores fatoriais de 1 a 7 para a porta em um loop 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


Ele pode ser iniciado para compilação, por exemplo, por um script simples ou pela linha de comando pela sequência: Como resultado, será gerado um arquivo de inicialização stream.bin, que pode ser transferido para o núcleo do processador no FPGA pela porta serial (nas realidades modernas, por qualquer porta serial virtual que os conversores fornecem Interfaces USB-UART). Como resultado, o programa ocupa 146 palavras (9 bits) da memória do programa e 3 na memória de dados.
c.py C:\D\My_Docs\Documents\uzh-master\tests\fact2.py




Conclusão


Em geral, o compilador Uzh parece ser um kit de ferramentas fácil e conveniente para o desenvolvimento de software para processadores de software. É uma ótima alternativa para o assembler, pelo menos em termos de usabilidade do programador. O kit de ferramentas para definir primitivas e macros como funções da linguagem de destino permite que locais críticos sejam implementados na linguagem de montagem do processador. Para processadores de arquitetura de pilha, o procedimento de adaptação do compilador não é muito complicado e demorado. Podemos dizer que esse é apenas o caso quando a disponibilidade do código-fonte do compilador ajuda - as seções principais do compilador estão mudando.

Os resultados da síntese do processador (capacidade de 32 bits, palavras em 4K de memória de programa e 1K RAM) para a série FPGA Altera Cyclone V fornecem o seguinte:

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. Quarto processador em VHDL // m.habr.com/en/post/149686
  2. Python - Wikipedia // en.wikipedia.org/wiki/Python
  3. Começamos o FPGA em 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_ Ferramentas para prototipagem rápida de compiladores DSL // github.com/true-grue/raddsl
  7. sovietov.com/txt/dsl_python_conf.pdf

O autor agradece aos desenvolvedores do processador de software Zmey e do compilador Uzh pelas consultas e paciência.

All Articles