Os módulos HDL do SAPHO por dentro

Este capítulo abre a caixa: o que exatamente é o processador que o YANC gera, e por que ele merece o rótulo de soft-core otimizado. É leitura para quem vai estudar ou estender a arquitetura; o uso normal da plataforma não exige nada daqui.

A biblioteca de módulos

O processador é montado a partir de uma biblioteca fixa de módulos Verilog, instalada com a AURORA em components/HDL:

Arquivo

Contém

processor.v

as memórias de instrução e de dados, e o módulo processor que amarra tudo

core.v

o contador de programa, o prefetch, as duas pilhas, o controle de memória e de I/O, e o núcleo

instr_dec.v

o decodificador de instruções

ula.v

a unidade lógica e aritmética, um submódulo por operação

addr_dec.v

o decodificador one-hot das portas de I/O

O arquivo gerado por projeto, Hardware/<proc>.v, é um invólucro fino: instancia processor com os parâmetros do seu programa e liga as portas externas.

A arquitetura

Um processador de acumulador único, Harvard, com três estágios de pipeline efetivos e uma instrução por ciclo:

  • Memórias separadas de programa e de dados, com larguras independentes. As duas são inicializadas pelos .mif via $readmemb.

  • Não há banco de registradores: toda operação passa pelo acumulador. Expressões aninhadas usam a pilha de dados (profundidade #NDSTAC); chamadas de função usam a pilha de retorno (profundidade #SDEPTH).

  • Os desvios resolvem no estágio de busca, sem penalidade. Não há forwarding nem stall porque a arquitetura não cria hazards: o caminho de dados e o de controle são casados por construção.

  • Reset síncrono em tudo, uma escolha amigável a FPGA.

  • O conjunto completo de instruções, com os 108 opcodes, está em Conjunto de instruções do processador.

A otimização que dá nome à plataforma

Aqui está o mecanismo central. Abra um <proc>.v gerado e olhe a instância do processor: além dos parâmetros numéricos, há uma lista de parâmetros com nomes de instruções, cada um ligado em 1:

processor #(
    .NUBITS(16), .NBMANT(10), .NBEXPO(5),
    // ...
    .ADD(1), .S_ADD(1), .SHR(1), .INN(1), .OUT(1)
    // as demais instrucoes ficam em 0
) u_proc ( /* ... */ );

Cada instrução que o seu programa usa vira um parâmetro em 1. Dentro dos módulos da biblioteca, cada bloco está embrulhado em um generate if condicionado a esse parâmetro: instrução não usada, bloco não sintetizado. O somador de ponto flutuante, o divisor, o deslocador, cada um só existe no circuito se alguma linha do seu C± o exigir.

É por isso que o TASM imprime o relatório de recursos instanciados e a estimativa de uso do conjunto de instruções: aquilo é a lista dos generate que ligaram. E é por isso que trocar / 4 por >> 2 no tutorial muda o diagrama do PRISM: o parâmetro DIV foi de 1 a 0, e o divisor evaporou.

O resultado prático: dois programas diferentes geram dois processadores de tamanhos diferentes sobre a mesma biblioteca. O processador é do tamanho do algoritmo, e o custo de área em FPGA acompanha o que o código pede, não o pior caso da arquitetura.

A visibilidade de simulação

Os módulos carregam um arnês de simulação, ativado apenas nos simuladores, que expõe as suas variáveis pelo nome, as trilhas de assembly e de linha C±, e os sinais delta_float e delta_int de erro de arredondamento. Nada disso existe na síntese física: o <proc>.v sintetizado é só o circuito.

Parâmetros derivados

Dois parâmetros são calculados pelo compilador, não escolhidos por você: a largura do campo de operando (que cresce com o tamanho do programa e dos dados) e os tamanhos das memórias (o número exato de instruções e de células de dados do programa). O processador não carrega memória sobrando.

Quem quiser ir além, o código-fonte da biblioteca e dos compiladores é aberto, no repositório do YANC na organização nipscernlab.