본문 바로가기

Verilog HDL 설계

Single Cycle RISC-V 32I 프로세서 설계

이번에는 RISC-V 프로세서 중 가장 단순하고 느려터진 Single cycle cpu를 설계했다.

 

https://github.com/pyong-1459/RISC-V_32I

 

GitHub - pyong-1459/RISC-V_32I

Contribute to pyong-1459/RISC-V_32I development by creating an account on GitHub.

github.com

 

베릴로그 코드는 깃허브에 올려뒀다.

 

1. RISC란?

 

Reduced instruction set computer 의 줄임말이다.

 

RISC-V는 RISC의 5번째 버전을 의미하며, RISC의 최신 ISA(Instruction Set Architecture)이다.

 

https://en.wikipedia.org/wiki/RISC-V

 

RISC-V - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Open-source CPU hardware instruction set architecture RISC-VDesignerUniversity of California, BerkeleyBits32, 64, 128Introduced2010; 12 years ago (2010)Version unprivileged ISA 20191

en.wikipedia.org

 

다른 ISA에 비해 간단하며 널리 공개되어 있다.

 

x86같은 ISA는 명령어(instruction)의 종류가 RISC에 비해 많고 명령어의 길이 또한 다른 경우가 있다.

 

하지만 RISC는 복잡하지 않고, 명령어의 길이가 제각기 다르지도 않다.

 

또한 opcode 위치도 거의 고정되어 있어서 편하게 설계할 수 있다.

 

2. 32I? 32IM? 이건 대체 뭘 의미하는걸까?

 

수행 가능한 명령어의 종류를 나타내는 것이다.

 

위키피디아에서 알 수 있듯이(Design 항목의 표 참고) I는 정수(Integer) 연산, F는 부동소수점(Floating point) 연산, M은 곱셈/나눗셈 연산을 지원한다는 뜻이다.

 

즉 32IM은 정수 연산 및 곱셈/나눗셈을 지원하는 cpu를 의미한다.

 

3. Instruction의 종류 및 구조

 

위키피디아뿐만 아니라, 공식 문서에서도 확인할 수 있다.

 

 

R 타입의 명령어는 레지스터끼리의 연산을 register destination(rd)에 저장한다.

I 타입은 Immediate와 하나의 레지스터와의 연산을 나타낸다.

S 타입은 데이터 메모리에 Save를 할 때 쓰이는 명령어다.

B 타입은 Branch를 위해 쓰이며, PC(Program Counter)를 원하는 값으로 변경시키는 것을 의미한다.

U 타입은 Upper Immediate(MSB 20bit + zero extended LSM 12bit)를 의미한다. 큰 값을 저장하거나 할때 유용하게 쓰인다.

J 타입은 Jump를 위해 쓰이며, Branch와 비슷하게 PC를 원하는 값으로 변경시키는 것을 의미한다.

Jump와 Branch의 차이점은 Jump는 조건없이 PC를 변경시키지만, Branch는 조건에 부합하는 경우에만 PC를 변경시킨다.

 

 

32I의 명령어들은 위의 표와 같다. 각 명령어는 다음과 같은 의미를 가진다.

 

AUIPC : Add upper immediate to PC
LUI : Load Upper Immediate
JAL : Jump And Link - $ra 저장 후 점프
JALR : Jump And Link Register - $rd 저장 후 $rs로 점프

BEQ : Branch if Equal
BNE : Branch Not Equal
BLT : Branch Less Than
BGE : Branch Greater or Equal

LB : Load Byte (8bit)
LH : Load Halfword (16bit)
LW : Load Word (32bit)
LBU : Load Byte Upper
LHU: Load Halfword Upper (16bit upper)
SB : Save Byte(8bit)
SH : Save Halfword(16bit)
SW : Save Word(32bit)

SLT : Set Less Than
SLTU : Set Less Than Unsigned
shamt : shift amount
SLL : Shift Left Logical
SRL : Shift Right Logical
SRA : Shift Right Arithmetic (sign extension)
SRA : $rs1 >> $rs2(lsb 5bit is shamt)

 

ADD : 덧셈

SUB : 뺄셈

XOR, AND, OR : bitwise 논리 연산

 

ADDI와 같이 I가 붙은 명령어는 I 타입 명령어를 나타낸다.

 

표에서 알 수 있듯이, LSB 2비트는 무조건 11이며, opcode 7비트 중 실제로 5비트가 쓰인다.

 

4. 실제 하드웨어 구조

 

구글링을 하다가 발견한 강의자료를 참고했다. 아래 이미지는 해당 강의자료의 한 페이지다.

 

 

5. CPU가 작동하는 원리

 

앞의 이미지를 참고하면서 읽으면 이해하기가 조금 더 쉽다.

 

우선 PC값에 따라 IM(Instruction Memory)에 적재되어 있는 Instruction을 통해 ImmGen, Reg[](Register File)에 입력을 한다.

 

Reg[]에 들어가는 명령어는 15비트, ImmGen에는  25bit가 들어간다.

 

Register file에서 저장되어 있는 값을 읽어 Output으로 출력하고, ImmGen의 출력과 함께 명령어에 해당하는 값들을 ALU에 전달한다.

 

ALU의 입력은 pc/Reg[rs1], Reg[rs2]/Imm으로, MUX에 의해 입력이 나뉜다.

 

ALU에서 연산된 값은 Data Memory와 WB(Write Back) MUX, PC MUX에 입력되며, WB의 값은 Register File에 입력되어 Register File을 갱신시킨다.

 

Data Memory에 저장된 값들을 다시 레지스터에 쓰는 것도 가능하다.

 

위의 과정을 PC 값에 따라 다르게 수행하는게 CPU의 동작 원리다.

 

Register file은 32I의 경우 총 32개가 쓰이는데, 아래와 같이 분류된다.

 

위키피디아 자료

 

32개의 레지스터들을 사용하면서 원하는 연산을 수행하는 것이다.

 

흔히 기계어, 어셈블리라고 불리는 가장 낮은 레벨의 언어는 레지스터들의 값을 바꾸는 것이다.

 

6. Verilog 설계 과정

 

우선 제어 신호를 어떻게 출력해야할지가 첫번째 관문이었다.

 

각 명령어에 따라 제어 신호를 각기 달리 출력해줘야한다.

 

제어신호는 총 18비트로 설계했다(control.v 참고). 

 

ALUSel은 ALU의 연산을 제어하고, ASel, BSel은 ALU의 입력인 A, B를 제어하며, BrUn은 Branch 비교 중 unsigned 비교를 할지 말지를 제어한다.

ImmSel은 5가지 각기 다른 Immediate들 중 어느 것을 선택할지를 제어하며, PCSel은 PC+4를 다음 PC 값으로 할지 혹은 ALU의 출력을 다음 PC 값으로 할지를 제어한다.

RegWen은 레지스터 파일의 destination의 쓰기를 할지 말지를 제어하며, MemRW는 데이터 메모리를 Write할지 Read할지를 제어한다. 

WBSel은 레지스터 파일에 Write Back을 ALU, PC+4, 데이터 메모리 값 중 하나로 할지를 선택한다. PC+4의 경우 Jump를 할때 쓰이며, 데이터 메모리값은 Load 명령어를 수행할때 사용된다. 나머지 경우는 ALU의 출력을 선택한다.

 

설계에 앞서 엑셀파일(Document 폴더에 있다)에 각기 다른 명령어에 대한 제어 신호 값들을 정리했다.

 

실 사용되는 opcode 9비트([30], [14:12], [6:2])의 값들을 각 명령어마다 나타냈다.

 

잘 만들어진 ISA답게, opcode가 몇가지 종류로 그룹지어져있다.

 

Load 명령어들은 opcode LSB 5bit가 전부 0이고, SW는 01000, Immediate 연산은 00100(사칙연산)/ 00101(AUIPC)/ 01101(LUI), J 타입은 110x1, B 타입은 11000, R 타입 사칙연산은 01100이었다.

 

opcode 값에 따라 여러 경우의 수를 wire 변수로 나타내어 이를 or로 합침으로써 제어신호를 만들었다.

 

ALUSel의 경우에는 I 타입과 R 타입, 혹은 그 외일 경우에 따라 다른 연산을 하도록 조건문을 활용하여 구현했다.

 

Instruction 메모리는 내 능력의 한계로 인해 128개의 레지스터로 구성되었다.

 

데이터 메모리는 65536x4x8bit의 용량을 가졌다.

 

ALU는 원래 덧셈기를 따로 게이트 단위로 만들어야 했으나 그러지 않았다. 원래는 따로 만들어야 한다.

 

ImmGen은 ImmSel에 따라 각기 다른 Immediate를 출력하도록 설계했으며, Immediate의 값은 앞서 본 표를 참조하면 된다.

 

7. 동작 검증

 

제대로 동작하는지 아닌지를 점검하기 위해 기계어를 직접 Instruction Memory에 넣고 cpu를 동작시켜야 했다.

 

예전에도 기계어를 썼던 적이 있어서 큰 문제는 아니라고 생각했으나... 16진수를 손수 만들기에는 시간이 너무 많이 소요됐다.

 

그래서 기계어를 32비트 16진수 코드로 바꿔주는 사이트를 통해 hex 코드로 변환했다.

 

https://venus.cs61c.org/

 

venus

Save on Close Save on Close AlignedAddressing Force Aligned Addressing? Mutable Text Mutable Text? Only Ecall Exit Only Ecall Exit? Default Reg States Set Registers on Init? Allow Access Allow Access Between Stack and Heap? Max number of steps:(Negative me

venus.cs61c.org

 

또한 내부에서 레지스터 값을 확인하기 위해 시뮬레이터를 이용하여 검증했다.

 

ECALL, FENCE, EBREAK를 제외한 나머지 명령어들을 검증하기 위해 총 4개의 기계어 코드를 준비했다.

 

assembly test 폴더와 machine code 폴더에 있는 각 코드들은 번호로 정렬해뒀으며, 위 사이트의 Dump 기능을 활용했다.

 

test1.s(code1.txt)는 간단한 덧셈을 검증하기 위해 썼다.

test2.s는 shift 연산, slt, 논리연산, bge 및 load를 검증하기 위해 작성하였다.

test3.s는 루프를 수행하기 위한 beq, blt, bne, jal을 위해 작성하였다.

test4.s는 뺄셈 연산을 검증하기 위해 작성하였다.

 

8. 시뮬레이션

 

 

code2.txt를 실행한 결과다. 우선 첫번째 이미지는 PC가 branch하는 것을 나타내고 있다.

bge a3, a2, shift

a3의 값이 a2보다 크거나 같을 경우 test2.s의 9번째 줄 명령어로 돌아간다는 의미의 어셈블리다.

실제로는 shift로 되돌아가는 것은 PC-48(110000)이기 때문에 hex code는 FCC6D8E3가 된다.

 

두번째 이미지는 레지스터 t0, a0~9까지의 값이 어떻게 변화하는지를 나타내고 있다.

x29(t4)은 slt 값을 저장하는 레지스터로 1 또는 0이 되는 것을 확인할 수 있다.

x30(t5)은 마지막에 t0값을 저장하는 역할이다.