Implementação de cache Verilog

Este artigo discute a implementação mais simples da RAM no Verilog.

Antes de prosseguir com a análise de código, é recomendável que você aprenda a sintaxe básica do Verilog.

Aqui você pode encontrar materiais de treinamento .

RAM


Etapa 1: declarar o módulo com os sinais de entrada / saída correspondentes


module ram ( input [word_size - 1:0] data, input [word_size - 1:0] addr, input wr, input clk, output response, output [word_size - 1:0] out ); parameter word_size = 32; 

  • data - dados para escrever.
  • endereço - endereço para a memória na RAM.
  • wr - status (leitura / gravação).
  • clk - sistema de ciclo de relógio.
  • resposta - prontidão da RAM (1 - se a RAM processou a solicitação de leitura / gravação, 0 - caso contrário).
  • dados de saída lidos da RAM.

Essa implementação foi integrada ao Altera Max 10 FPGA, que possui uma arquitetura de 32 bits e, portanto, o tamanho dos dados e do endereço (tamanho_da_ palavra) é de 32 bits.

Etapa 2: declarando os registradores dentro do módulo


Uma declaração de matriz para armazenar dados:

 parameter size = 1<<32; reg [word_size-1:0] ram [size-1:0]; 

Também precisamos armazenar os parâmetros de entrada anteriores para rastrear suas alterações no bloco always:

 reg [word_size-1:0] data_reg; reg [word_size-1:0] addr_reg; reg wr_reg; 

E os dois últimos registradores para atualizar os sinais de saída após os cálculos no bloco always:

 reg [word_size-1:0] out_reg; reg response_reg; 

Inicializamos os registros:

 initial begin response_reg = 1; data_reg = 0; addr_reg = 0; wr_reg = 0; end 

Etapa 3: implementando a lógica sempre do bloco


 always @(negedge clk) begin if ((data != data_reg) || (addr%size != addr_reg)|| (wr != wr_reg)) begin response_reg = 0; data_reg = data; addr_reg = addr%size; wr_reg = wr; end else begin if (response_reg == 0) begin if (wr) ram[addr] = data; else out_reg = ram[addr]; response_reg = 1; end end end 

Sempre o bloqueio é acionado por negedje, ou seja, no momento, o relógio se move de 1 para 0. Isso é feito para sincronizar corretamente a RAM com o cache. Caso contrário, poderá haver casos em que a RAM não tenha tempo para redefinir o status de pronto de 1 para 0 e, no próximo relógio, o cache decidirá que a RAM processou com êxito sua solicitação, o que é fundamentalmente errado.

A lógica do algoritmo sempre do bloco é a seguinte: se os dados forem atualizados, redefina o status de prontidão para 0 e escreva / leia os dados, se a gravação / leitura for concluída, atualizamos o status de prontidão para 1.

No final, adicione a seguinte seção de código:

 assign out = out_reg; assign response = response_reg; 

O tipo de sinais de saída do nosso módulo é fio. A única maneira de alterar sinais desse tipo é a atribuição a longo prazo, que é proibida dentro do bloco always. Por esse motivo, o bloco always usa registradores, que são posteriormente atribuídos aos sinais de saída.

Cache de mapeamento direto


O cache de mapeamento direto é um dos tipos mais simples de cache. Nesta implementação, o cache consiste em n elementos e a RAM é dividida condicionalmente em blocos por n; o i-ésimo elemento no cache corresponde a todos esses k-ésimos elementos da RAM que satisfazem a condição i = k% n.

A imagem abaixo mostra um cache de tamanho 4 e RAM de tamanho 16.



Cada elemento do cache contém as seguintes informações:

  • bit de validade - se as informações no cache são relevantes.
  • tag é o número do bloco na RAM em que esse elemento está localizado.
  • dados - informações que escrevemos / lemos.

Quando solicitado a ler, o cache divide o endereço de entrada em duas partes - uma tag e um índice. O tamanho do índice é log (n), em que n é o tamanho do cache.

Etapa 1: declarar o módulo com os sinais de entrada / saída correspondentes


 module direct_mapping_cache ( input [word_size-1:0] data, input [word_size-1:0] addr, input wr, input clk, output response, output is_missrate, output [word_size-1:0] out ); parameter word_size = 32; 

A declaração do módulo de cache é idêntica à RAM, com exceção do novo sinal de saída is_missrate. Esta saída armazena informações sobre se a última solicitação de leitura foi incorreta.

Etapa 2: declarando os registradores e RAM


Antes de declarar os registradores, determinamos o tamanho do cache e do índice:

 parameter size = 64; parameter index_size = 6; 

Em seguida, declaramos uma matriz na qual os dados que escrevemos e lemos serão armazenados:

 reg [word_size-1:0] data_array [size-1:0]; 

Também precisamos armazenar bits e tags de validade para cada item no cache:

 reg validity_array [size-1:0]; reg [word_size-index_size-1:0] tag_array [size-1:0]; reg [index_size-1:0] index_array [size-1:0]; 

Registros nos quais o endereço de entrada será dividido:

 reg [word_size-index_size-1:0] tag; reg [index_size-1:0] index; 

Registros que armazenam os valores de entrada no relógio anterior (para rastrear alterações nos dados de entrada):

 reg [word_size-1:0] data_reg; reg [word_size-1:0] addr_reg; reg wr_reg; 

Registra a atualização dos sinais de saída após os cálculos no bloco always:

 reg response_reg; reg is_missrate_reg; reg [word_size-1:0] out_reg; 

Valores de entrada para RAM:

 reg [word_size-1:0] ram_data; reg [word_size-1:0] ram_addr; reg ram_wr; 

Valores de saída para RAM:

 wire ram_response; wire [word_size-1:0] ram_out; 

Declarando um módulo de RAM e conectando sinais de entrada e saída:

 ram ram( .data(ram_data), .addr(ram_addr), .wr(ram_wr), .clk(clk), .response(ram_response), .out(ram_out)); 

Registrar inicialização:

 initial integer i initial begin data_reg = 0; addr_reg = 0; wr_reg = 0; for (i = 0; i < size; i=i+1) begin data_array[i] = 0; tag_array[i] = 0; validity_array[i] = 0; end end 

Etapa 3: implementando a lógica sempre do bloco


Para começar, para cada relógio, temos dois estados - os dados de entrada são alterados ou não. Com base nisso, temos a seguinte condição:

 always @(posedge clk) begin if (data_reg != data || addr_reg != addr || wr_reg != wr) begin end // 1:    else begin // 2:     end end 

Bloco 1. Caso os dados de entrada sejam alterados, a primeira coisa que fazemos é redefinir o status de prontidão para 0:

 response_reg = 0; 

Em seguida, atualizamos os registradores que armazenavam os valores de entrada do relógio anterior:

 data_reg = data; addr_reg = addr; wr_reg = wr; 

Dividimos o endereço de entrada em uma tag e um índice:

 tag = addr >> index_size; index = addr; 

Para calcular a tag, é usado um deslocamento bit a bit para a direita; para o índice, basta atribuí-la simplesmente, porque Bits extras do endereço não são levados em consideração.

O próximo passo é escolher entre escrever e ler:

 if (wr) begin //  data_array[index] = data; tag_array[index] = tag; validity_array[index] = 1; ram_data = data; ram_addr = addr; ram_wr = wr; end else begin //  if ((validity_array[index]) && (tag == tag_array[index])) begin //    is_missrate_reg = 0; out_reg = data_array[index]; response_reg = 1; end else begin //     is_missrate_reg = 1; ram_data = data; ram_addr = addr; ram_wr = wr; end end 

No caso de gravação, inicialmente modificamos os dados no cache e atualizamos os dados de entrada para a RAM. No caso da leitura, verificamos a presença desse elemento no cache e, se ele existe, escrevemos para out_reg, caso contrário, passamos para a RAM.

Bloco 2. Se os dados não foram alterados desde a execução do relógio anterior, temos o seguinte código:

 if ((ram_response) && (!response_reg)) begin if (wr == 0) begin validity_array [index] = 1; data_array [index] = ram_out; tag_array[index] = tag; out_reg = ram_out; end response_reg = 1; end 

Aqui, aguardamos a conclusão do acesso à RAM (se não houver acesso, ram_response é 1), atualizamos os dados se houver um comando de leitura e configuramos a disponibilidade do cache como 1.

E por último, atualize os valores de saída:

 assign out = out_reg; assign is_missrate = is_missrate_reg; assign response = response_reg; 

Source: https://habr.com/ru/post/pt461611/


All Articles