Elpis Light processor

License UPRJ_CI Caravel Build

Authors

  • Rodrigo Huerta (@rodhuega) (UPC¹)
  • Aurora Tomás (@theatomb) (UPC¹)
    ¹ Universitat Politècnica de Catalunya (UPC)



Project description

This chip is a reduced version of the one developed by the authors in the following subjects of the MIRI-HPC master of the UPC:

  • Processor Architecture
  • Multiprocessors Architecture
  • Processor Design

Chip layout

Chip description

Elpis is a 5-stage pipelined and multi-cycle processor implemented from scratch based on RISC-V architecture, mixed with some MIPS ideas. However, due to the limitations of the tools this Elpis is lighter than our initial Elpis core. Anyway, the version that we present has the following characteristics:

  • 32 integer registers
  • Special register file
  • Full bypasses
  • Powerful instruction set, mainly based on RISC-V instructions, but with some customed ones
  • L1 instruction and data caches (iCache and dCache)
  • Main memory simulates 5 cycles delay per access, to be more realistic
  • Store buffer
  • Support to exceptions
  • I/O support
  • Simulated virtual memory support (NO TLBs!)

Pipeline diagram

ISA

The supported instructions by Elpis are: ADD, SUB, MUL, LDB, LDW, STB, STW, BEQ, JUMP, IRET, MOVE, TLBWRITE, ADDI, OR, ORI, AND, ANDI, XOR, XORI, SLL, SRL, SRA, SLLI, SRLI, SRAI, BGE, BLT, BNE, MOVR, ECALL, READ, PRINT. The instruction set encoding is mainly based on RISCV32, but there are some differences respecting the pipeline management where we present new instructions not existing in RISC-V:

  • IRET: It is always codified as 0x0000007F, sets the PSW to 0 (user mode) and jumps to the PC that RM0 holds. This instruction is only permitted to execute if PSW=1 (privileged mode).
  • JUMP: Jumps to the address that the given register points to. For example, the instruction semantics could be jump x9, where its decoding will result in an OPCODE [6:0] = 1101111 and destination register = [19:15]. As for the rest, the other bits are ignored.
  • MOV: Used to move register data from special registers to regular ones. For example, the instruction semantics could be mov x7, rm2, where its decoding will result in an OPCODE [6:0] = 0101111, funct7 [31:25]= 0000000, source register (special register) = [21:20] and destination register (regular register) = [11:7]. As for the rest, the other bits are ignored.
  • MOVR: Used to move register data from regular registers to special ones. For example, the instruction semantics could be movr rm4 x15, where its decoding will result in an OPCODE [6:0] = 0101111, funct7 [31:25] = 0000001, source register (regular register) = [21:20] and destination register (special register) = [11:7]. As for the rest, the other bits are ignored.
  • ECALL: It is an instruction to call the operating system, so user-mode has to be enabled. However, at the moment, there are only supported two different calls. If it is performed ecall 6 is codified as 0x00600073, otherwise if is an ecall 7 is codified as 0x00700073. The code 6 is used in case we want to send data to PicoRV, and the code 7 is used in case we want to receive data from PicoRV.
  • PRINT: This instruction sends into the PicoRV whatever is inside the special register rm4, and it is always codified as 0x0200007D. However, this instruction is only legal if privileged mode is enabled. On the other hand, if user mode is enabled it should be executed through an ecall instruction (ecall 6), which would result in an equivalent functionality.
  • READ: This instruction receives a value from PicoRV, and it is always codified as 0x0400007D. However, this instruction is only legal if privileged mode is enabled. On the other hand, if user mode is enabled it should be executed through an ecall instruction (ecall 7), which would result in an equivalent functionality.

How to use the chip

First of all, we need the following function in order to load the instructions and data into Elpis:

void elpis_load_memory(uint32_t*  program_data, uint32_t*  program_addr)
{
	int i, continue_reading;
	continue_reading = 1;
	i = 0;
	reg_la3_data = 0x00000004;
	reg_la3_data = 0x00000005;
	reg_la3_data = 0x00000004;
	while (continue_reading)
	{
		if (program_data[i] == ((uint32_t) 0xFFFFFFFF))
		{
			continue_reading = 0;
		}else {
			reg_la0_data = program_addr[i];
			reg_la1_data = program_data[i];
		}
		reg_la3_data = 0x00000005;
		reg_la3_data = 0x00000004;
		i++;
	}
	reg_la3_data = 0x00000001;
	reg_la3_data = 0x00000000;
}

Inside the main() function we perform the following steps:

void main(){
  ...
    // Configuring LA probes
	// outputs from the cpu are inputs for my project denoted for been 0
	// inputs to the cpu are outpus for my project denoted for been 1
	reg_la0_oenb = reg_la0_iena = 0x00000000; // [31:0]
	reg_la1_oenb = reg_la1_iena = 0x00000000; // [63:32]
	reg_la2_oenb = reg_la2_iena = 0x00000000; // [95:64]
	reg_la3_oenb = reg_la3_iena = 0x00000000; // [127:96]
  
    // Flag start of the test
	reg_mprj_datal = 0xAB400000;

	// Elpis OS information
	uint32_t OS_DATA[30];
	OS_DATA[0] = 0x00502023;
	OS_DATA[1] = 0x00602223;
	OS_DATA[2] = 0x00702423;
	OS_DATA[3] = 0x00802623;
	OS_DATA[4] = 0x00902823;
	OS_DATA[5] = 0x00400413;
	OS_DATA[6] = 0x00500493;
	OS_DATA[7] = 0x00600293;
	OS_DATA[8] = 0x00700313;
	OS_DATA[9] = 0x002003af;
	OS_DATA[10] = 0x00838c63;
	OS_DATA[11] = 0x00938a63;
	OS_DATA[12] = 0x00538c63;
	OS_DATA[13] = 0x00638e63;
	OS_DATA[14] = 0x00038e63;
	OS_DATA[15] = 0x02000863;
	OS_DATA[16] = 0x0000002e;
	OS_DATA[17] = 0x00000863;
	OS_DATA[18] = 0x0200007D;
	OS_DATA[19] = 0x00000463;
	OS_DATA[20] = 0x0400007D;
	OS_DATA[21] = 0x00002283;
	OS_DATA[22] = 0x00402303;
	OS_DATA[23] = 0x00802383;
	OS_DATA[24] = 0x00c02403;
	OS_DATA[25] = 0x01002483;
	OS_DATA[26] = 0x0000007F;
	OS_DATA[27] = 0x00000033;
	OS_DATA[28] = 0x00002050;
	OS_DATA[29] = 0xFFFFFFFF;

	uint32_t OS_ADDR[30];
	OS_ADDR[0] =  0x00000010;
	OS_ADDR[1] =  0x00000011;
	OS_ADDR[2] =  0x00000012;
	OS_ADDR[3] =  0x00000013;
	OS_ADDR[4] =  0x00000014;
	OS_ADDR[5] =  0x00000015;
	OS_ADDR[6] =  0x00000016;
	OS_ADDR[7] =  0x00000017;
	OS_ADDR[8] =  0x00000018;
	OS_ADDR[9] =  0x00000019;
	OS_ADDR[10] = 0x0000001a;
	OS_ADDR[11] = 0x0000001b;
	OS_ADDR[12] = 0x0000001c;
	OS_ADDR[13] = 0x0000001d;
	OS_ADDR[14] = 0x0000001e;
	OS_ADDR[15] = 0x0000001f;
	OS_ADDR[16] = 0x00000020;
	OS_ADDR[17] = 0x00000021;
	OS_ADDR[18] = 0x00000022;
	OS_ADDR[19] = 0x00000023;
	OS_ADDR[20] = 0x00000024;
	OS_ADDR[21] = 0x00000025;
	OS_ADDR[22] = 0x00000026;
	OS_ADDR[23] = 0x00000027;
	OS_ADDR[24] = 0x00000028;
	OS_ADDR[25] = 0x00000029;
	OS_ADDR[26] = 0x0000002a;
	OS_ADDR[27] = 0x0000002b;
	OS_ADDR[28] = 0x00000005;
	OS_ADDR[29] = 0xFFFFFFFF;

	// Elpis user program
    // Sending data to Elpis example
	uint32_t USER_DATA[4];
	USER_DATA[0] = 0x00200093;
	USER_DATA[1] = 0x0210022F;
	USER_DATA[2] = 0x00600073;
	USER_DATA[3] = 0xFFFFFFFF;

	uint32_t USER_ADDR[4];
	USER_ADDR[0] = 0x00000040;
	USER_ADDR[1] = 0x00000041;
	USER_ADDR[2] = 0x00000042;
	USER_ADDR[3] = 0xFFFFFFFF;

	// Loading Elpis memory
	elpis_load_memory(OS_DATA, OS_ADDR);
	elpis_load_memory(USER_DATA, USER_ADDR);
	
	reg_la3_oenb = reg_la3_iena = 0x00000001; // Recovering fast clock not controlled by the user

	// Reset of Elpis and start of computation at Elpis
	reg_la3_data = 0x00000002;
	reg_la3_data = 0x00000000;

    // Start of sending data to PicoRV section (if needed)
	// Check bit 100 to be active
	while (reg_la3_data != 0x00000010);

	// Check bit 100 has the right data
	if (reg_wb_reads == 0x00000002){
        print("OK\n\n");
    }
    else {
        print("ERROR\n\n");
    }
    // End of sending data to PicoRV section (if needed)
  
    // Start of receiving data from PicoRV section (if needed)
	reg_la2_data = 0x00000002; // Setting the value that will be read at Elpis
	reg_la3_data = 0x00000008; // Notice Elpis that the values has been sent
	reg_la3_data = 0x00000000; // Stop notifiying Elpis that the value has been sent
    // End of receiving data from PicoRV section (if needed)
  
}

Available tests

We provide a set of tests that verifies all the described functionality.

RTL

The tests that we are providing in RTL are the following:

  • testBasicOps: This test checks the basic behaviour of the CPU.

  • testRead: This test checks that PicoRV is able to send data to Elpis.

  • testPrint: This test checks that Elpis is able to send data to PicoRV.

  • testBytes: This test checks that Elpis performs correctly loads and stores and in byte-level.

  • testMul3: This test checks that Elpis is able to do multiplications properly.

GL

The tests that we are providing in GL are the following:

  • testPrint: This test checks that Elpis is able to send data to PicoRV.