Neste módulo estão presentes as instanciações do CPU, do barramento, dos periféricos e todas as variáveis de entrada e saída do SoC, como por exemplo, os pinos de entrada e de saída.
////////////////////////////////////////// module D_Blaze_SoC( //inputs input sys_clk, input rst, input [7:0] in_pins, input [4:0] buttons, input Rx, //outputs output [7:0] out_pins, output Tx ); //////////////////////////////////////////
Figura 40 – Entradas/Saídas do Módulo hierarquicamente superior
A placa de desenvolvimento utilizada é a FPGA da Xilinx XUPV5-LX110T. Nesta placa o sinal de reset por hardware tem lógica inversa. Para evitar que seja necessário mudar a variável de reset, foi criado um wire que toma o valor do reset, podendo ele estar negado para casos como a Virtex 5.
////////////////////////////////////////// // Development Board specific variables //////////////////////////////////////////
wire sys_rst; // sys_rst signal for CPU
assign sys_rst = !rst; // Virtex 5 sys_rst is always 1
//////////////////////////////////////////
55
CPU
Como o processador desenvolvido encontra-se pipelined em cinco estágios, foi necessário criar um módulo para cada estágio, vários módulos para os buffers e um módulo para a hazard
unit. As instanciações destes módulos são feitas no módulo CPU. Estágio de Leitura de instrução
Neste estágio é onde se encontra a memória de instruções e é feita a atualização do valor do
PC.A memória é um ipcore disponível na ferramenta da Xilinx. É uma memória ROM, uma vez que apenas é necessário ler os valores presentes na mesma, sendo estes as diferentes instruções que devem ser executadas.
A cada ciclo de relógio, o valor presente na posição de memória indicada pelo PC é passado para o wire IR, que por sua vez passa o seu valor, através de um assign para o wire instruction. O valor contido no wire instruction é a próxima instrução a ser executada, ou seja, tem que ser enviada para os estágios seguintes para ser descodificada e executada.
Na Figura 42 pode-se ver como o PC é atualizado a cada ciclo. Dependendo das instruções executadas, o valor do PC pode apenas passar a apontar para a próxima posição na memória, ou ter o seu valor alterado por uma instrução.
////////////////////////////////////////////////////////////// always@(posedge sys_clk)
if(sys_rst)
begin
PC <= 32'b0; End
else if(jump_signal == 2'b01 | jump_signal == 2'b11 )
begin PC <= ((PC + jump_value) - 2'b10); End else if(jump_signal == 2'b10) begin PC <= jump_value; End else if(pc_restore != 0) begin PC <= pc_restore; end else begin PC <= PC + 1'b1; end ////////////////////////////////////////////////////////////// Figura 42 – Atualização do PC
56
Existem três condições para a atualização do valor do PC, sendo elas o reset, instruções de salto e execução normal. O caso de reset serve para o PC passar a apontar para o início da memória de instruções caso seja feito um reset na placa. Execução normal é quando não existem instruções que alterem o valor do PC, por isso ele apenas é incrementado para passar a apontar para a posição seguinte. Nas instruções de salto pode-se ter saltos relativos ou saltos absolutos, onde os primeiros são obtidos através da adição de um valor ao PC, enquanto nos outros o PC toma um valor dado por uma instrução.
Estágio de Descodificação de Instrução
Neste estágio é feita a decodificação das várias instruções presentes na memória de instruções. Também se resolvem neste estágio os diferentes hazards que ocorrem no pipeline, usando as técnicas de stalls, bubbles ou data forwarding. Neste estágio também se encontra o
register file onde se realiza a leitura/escrita dos registos.
Como se pode ver na Figura 43, neste estágio é descodificado o opcode da instrução a executar, quais os registos a usar e valor imediado se necessário. Também se encontra a variavel
barrel_dir que serve para informar ao módulo barrel shifter qual a direção do shift.
////////////////////////////////////////////////////////////////////////////////// // Decoding //////////////////////////////////////////////////////////////////////
assign opcode_aux = (ignore) ? 6'b0:
instruction[31:26];
assign Rd_aux = (ignore) ? 5'b0:
instruction[25:21];
assign Ra = (opcode == `RETI) ? 5'd31:
(opcode == `PERACC) ? 5'b0: (opcode == `CALL) ? 5'd31: (ignore) ? 5'b0: (opcode == `RET) ? 5'd31: (opcode == `PUSH) ? 5'd31: (opcode == `POP) ? 5'd31: instruction[20:16]; assign Rb = (ignore) ? 5'b0: (opcode == `PERACC) ? 5'b0: instruction[15:11];
assign Imm = ((opcode == `ADDI | (…)) & !imm_signal) ? {16'b0,instruction[15:0]}:
((opcode == `LWI | (…)) & !imm_signal) ? {16'b0,instruction[15:0]}: ((opcode == `ADDI | (…)) & imm_signal) ? {imm_temp,instruction[15:0]}: ((opcode == `LWI | (…)) & imm_signal) ? {imm_temp,instruction[15:0]}:
32'b0;
assign barrel_dir = ((opcode == `BS) & (instruction[10] == 1'b1)) ? 1'b1 : 1'b0;
57 Na Figura 44 está apresentada a resolução dos saltos, tal como hazards envolvendo acessos à memória e mudanças do valor do apontador da pilha.
////////////////////////////////////////////////////////////////////// /// Branches
//////////////////////////////////////////////////////////////////////
assign jump_signal = ((opcode == `BR | opcode == `BRI) & Ra == 5'b00000) ? 2'b01:
((opcode == `BR | opcode == `BRI) & Ra == 5'b01000) ? 2'b10: ((opcode == `BEQ) & (Rd == 5'b00000) & (RF_out_a == 2'b00)) ? 2'b11: ((opcode == `BEQ) & (…) | (RF_out_a == 2'b01))) ? 2'b11: ((opcode == `BEQ) & (…) | (RF_out_a == 2'b00))) ? 2'b11: ((opcode == `BEQ) & (Rd == 5'b00100) & (RF_out_a == 2'b10)) ? 2'b11: ((opcode == `BEQ) & (…) | (RF_out_a == 2'b01))) ? 2'b11: ((opcode == `BEQ) & (Rd == 5'b00010) & (RF_out_a == 2'b01)) ? 2'b11: (Forward_ctrl[7:6] == 2'b01 | Forward_ctrl[7:6] == 2'b11) ? 2'b01: (opcode == `CALL) ? 2'b10:
(Forward_ctrl[9:8] == 2'b01 | Forward_ctrl[9:8] == 2'b10) ? 2'b01: 2'b00;
assign jump_value = (opcode == `BR | opcode == `BEQ) ? RF_out_b:
(opcode == `BRI & !jump_side) ? Imm: (opcode == `BRI & jump_side) ? (~Imm + 1'b1): (opcode == `CALL) ? {6'b0, instruction[25:0]}: (Forward_ctrl[9:8]) ? 32'b1: 32'b0;
assign jump_side = ((opcode == `BRI) & (Rd[4] == 1'b1)) ? 1'b1 : 1'b0;
//////////////////////////////////////////////////////////////////////
Figura 44 – Resolver saltos
Sempre que é feito um salto, existe a necessidade de ignorar as duas próximas instruções que estão no pipeline. Quando uma instrução avança para o próximo estágio, na transição ascendente do ciclo de relógio, ou seja, quando a instrução de salto está no estágio de decode já foi feito o fetch a uma outra instrução. Quando se verifica que o salto tem que ser executado, são enviados dois sinais, um de controlo e outro de dados, para o estágio de fetch para que o valor do PC seja atualizado, o que só acontece no ciclo de relógio seguinte, havendo assim uma outra instrução que foi anteriormente carregada para execução. Na Figura 45 pode-se ver que sempre que há um sinal de salto – jump_signal encontra-se a 1 – a variavel ignore passa a ter um valor diferente de zero para que quando estiver a ser feito o decoding da instrução, sejam introduzidas duas bubbles no pipeline.
58
////////////////////////////////////////////////////////////////////// // Branches auiliary
//////////////////////////////////////////////////////////////////////
always@(posedge sys_clk)
begin if(sys_rst) begin ignore <= 3'b000; end else if (jump_signal) begin ignore <= 3'b010; end
else if (opcode == `RET | opcode == `RETI)
begin ignore <= 3'b100; end else if(ignore != 0) begin ignore <= ignore - 1'b1; end end //////////////////////////////////////////////////////////////////////
Figura 45 – Auxilio nos saltos
Neste processador existe o suporte para o prefixo IMM. Esta instrução faz com que seja possivel usar valores imediatos de 32 bits em instruções do tipo B. Como estas tem um campo para um valor imediato de 16 bits apenas, se for necessário carregar um valor de 32 bits estas instruções terão de ser precedidas por um prefixo IMM. O campo de 16 bits do prefixo IMM irá formar a parte mais significativa da palavra de 32 bits e o campo de 16 bits da instrução do tipo B irá formar a parte menos significativa da palavra de 32 bits.
Register file
A Figura 46 mostra a implementação do register-file onde são feitas as leituras/escritas dos diferentes registos. Tem-se que o R0 terá sempre o valor zero e o R31 está hardwired como o apontador da pilha. Caso se verifique um sinal de reset todo o register file fica com o valor zero, à excepção do registo 31 que toma o valor do topo da pilha.
59
always@(posedge sys_clk) begin
if(sys_rst)
begin
for (i = 0; i < 31; i = i + 1)
register_file[i] <= 0;
register_file[31] <= 32'hFFF; // Top of stack in RAM end
if(wr_en)
begin
register_file[addr_write] <= data_ina; // Write end
if(stack_signal)
begin
register_file[31] <= data_in_stack; // Write stack end
end
assign data_outa = register_file[addra]; assign data_outb = register_file[addrb]; assign data_outd = register_file[addrd];
Figura 46 – Atualização do register file Estágio de Execução
Neste estágio encontram-se o barrel_shifter, o comparator, o multiplier e a ALU, onde são feitas todas as operações lógicas e aritméticas. Na Figura 47 encontra-se o código referente aos
inputs e que tipo de operação irá ser realizada na ALU.
/////////////////////////////////////////////////////////////////////////////// // ALU inputs
///////////////////////////////////////////////////////////////////////////////
assign ALU_input0 = (opcode == `BR | opcode == `BEQ | opcode == `BS |
opcode == `BRI) ? 32'b0: (opcode == `MOV | opcode == `MOVI) ? 32'b0: (opcode == `NOT) ? RF_out_d: (opcode == `CMP | opcode == `MUL) ? 32'b0: RF_out_a;
assign ALU_input1 = (opcode == `BR | (…)) ? 32'b0:
(opcode == `NOT) ? 32'b0: (opcode == `ADDI | (…)) ? Imm: (opcode == `SWI | (…)) ? Imm: (opcode == `MOV) ? RF_out_a: (opcode == `MOVI) ? Imm: (opcode == `CMP | opcode == `MUL) ? 32'b0: RF_out_b;
assign operation = (opcode == `ADD | opcode == `ADDI) ? `ALU_ADD:
(…)
(opcode == `NOT) ? `ALU_LOGIC_NOT:
ALU_NOP;
///////////////////////////////////////////////////////////////////////////////
60
A ALU contém dois inputs de dados e outro input com a operação que irá ser efectuada sobre os mesmos. Caso as instruções sejam acessos à memória, neste estágio é feita a adição do registo A com o registo B/Imm para calcular o endereço de memória a ser acedido.
As variáveis na Figura 48 servem para auxiliar nas instruções de leitura da memória. Não são enviadas pelo buffer que liga o estágio de execução ao estágio de acesso à memória, mas sim por uma ligação direta. Seguiu-se esta abordagem pois, para ler um posição da memória de dados é necessário fornecer o endereço, mas apenas no ciclo de relógio seguinte é que o valor é atualizado na variável de saída da memória.
////////////////////////////////////////////////////////////////////// // Load auxiliary
//////////////////////////////////////////////////////////////////////
assign load_signal = (opcode == `LW | opcode == `LWI | opcode == `LWBI) ? 1'b1:
1'b0;
assign ret_signal = (opcode == `RETI | opcode == `RET | opcode == `POP) ? 1'b1:
1'b0;
assign forward_execute_output = (opcode == `RETI) ? execute_output:
(opcode == `LW) ? execute_output: (opcode == `LWI) ? execute_output: (opcode == `LWBI) ? execute_output:
(opcode == `RET) ? execute_output:
(opcode == `POP) ? execute_output:
32'b0;
//////////////////////////////////////////////////////////////////////
Figura 48 – Auxilio em instruções de Load
Desta maneira evita-se perder dois ciclos para ler um valor da memória de dados. Existe um problema com esta implementação, que se verifica sempre que existe uma instrução de escrita para a memória de dados no ciclo anterior a uma instrução de leitura. Este problema é resolvido pela hazard unit.
Estágio de Acesso à Memória
Neste estágio são feitos todos os acessos à memória, sejam eles para escrita ou leitura. Na Figura 49 encontram-se as variáveis necessárias para aceder à memória de dados. A variável
wea_data_mem pode ter o valor um ou zero e serve para dar permissão para escrita na memória.
A variavel addr_data_mem diz qual o endereço se deve aceder, quer para leitura ou escrita. Por fim tem-se a variavel dina_data_in que possui o valor que irá ser escrito.
61 //////////////////////////////////////////////////////////////////////////////////
// Data memory variables /////////////////////////////////////////////////////////
assign wea_data_mem = (opcode == `SW | opcode == `SWI | opcode == `SWBI) ? 4'b1111:
(opcode == `CALL | opcode == `PUSH) ? 4'b1111: 4'b0;
assign addr_data_mem = (ret_signal) ? (forward_execute_output):
(opcode == `SW | opcode == `SWI | opcode == `SWBI) ? execute_output[`INST_ADDR-1:0]: (load_signal) ? forward_execute_output[11:0]: (opcode == `CALL | opcode == `PUSH) ?
execute_output[`INST_ADDR-1:0]: 5'b0;
assign dina_data_in = (opcode == `SW | opcode == `SWI | opcode == `SWBI) ? RF_out_d:
(opcode == `CALL | opcode == `PUSH) ? RF_out_d: 32'b0;
assign pc_restore = (opcode == `RET | opcode == `RETI) ? data_mem_out[`INST_ADDR-1:0]:
14'b0;
//////////////////////////////////////////////////////////////////////////////////
Figura 49 – Variáveis de acesso à memória Estágio de Atualização do Register File
Neste estágio é feita a atualização dos valores no register file. Não existe um módulo para este estágio, é apenas efectuada a ligação entre o buffer que conecta o estágio memory access ao estágio de decode para efectuar a actualização do register file.
///////////////////////////////////////////////////////////////////////////////// // Write Back values //////////////////////////////////////////////////////////// assign RF_addr = (opcode == `ADD | (…) ) ? Rd: (opcode == `ADDI | (…) ) ? Rd: (opcode == `LW | (…) ) ? Rd: (opcode == `BS) ? Rd: (opcode == `MOV) ? Rd: (opcode == `MOVI) ? Rd: (opcode == `POP) ? Rd: (opcode == `PERACC) ? Rd: 5'b0; assign RF_data = (opcode == `ADD | (…) ) ? execute_output:
(opcode == `ADDI | (…) ) ? execute_output:
(opcode == `MOV) ? execute_output: (opcode == `MOVI) ? execute_output:
(opcode == `LW | (…) ) ? data_mem_out:
(opcode == `BS) ? execute_output:
(opcode == `CMP) ? {30'b0, CMP_output}:
(opcode == `POP) ? data_mem_out:
(opcode == `PERACC) ? execute_output:
32'b0;
/////////////////////////////////////////////////////////////////////////////////
62
Buffers
São responsaveis por passar a informação de um estágio para o seguinte. Existem quatro
buffers, em que a instanciação dos mesmos é igual para todos, havendo apenas diferenças nos
valores que são passados para a variavel in e o tamanho das variaveis in e out.
Na Figura 51 tem-se o módulo do buffer. Este módulo comporta-se como um flip-flop normal, ou seja, a cada ciclo positivo de relógio o valor da entrada é passado para a saida do módulo. ////////////////////////////////// module D_Blaze_buf#( input sys_clk, input sys_rst, input [`BUF#_SIZE-1:0] in,
output reg [`BUF#_SIZE-1:0] out );
always@(posedge sys_clk)
begin if(sys_rst) out <= 0; else out <= in; end endmodule ////////////////////////////////// Figura 51 – Buffer #