Ice-V : Silice의 간단하고 컴팩트 한 RISC-V RV32I 구현

Ice-V : Silice의 간단하고 컴팩트 한 RISC-V RV32I 구현

TL; DR 편리하게 사용할 수있는 소형 CPU 설계, 자세한 코드 연습, Silice 및 RISC-V 모두에 대해 배우기 시작하기에 좋은 곳입니다.

참고 : 텍스트 가능성 더 많은 광택이 필요하면 의견을 보내주십시오!

이게 뭐지?

Ice-V는 RISC-V RV32I 사양을 구현하는 프로세서입니다. 간단하고 콤팩트하며 (줄이면 최대 100 줄, 아래 이미지 참조) Silice의 많은 기능을 보여 주며 프로젝트에서 좋은 동반자가 될 수 있습니다. BRAM에서 코드를 실행하는 데 특화되어 있으며, 합성시 코드가 BRAM에 베이크됩니다 (나중에 다른 소스에서로드되는 부트 로더가 될 수 있음).

쉽게 해킹 할 수 있으며 SPI에서 부팅하고 RAM에서 코드를 실행하고 다양한 주변 장치에 연결할 수 있도록 확장 할 수 있습니다. 이 예제는 LED와 외부 SPI 화면을 구동합니다.

여기에있는 버전은 IceStick에서 바로 사용할 수 있습니다. ice40 1HK이며 최소한의 노력으로 다른 보드에 적용 할 수 있습니다.

풍모

  • 는 RV32I 사양
  • gcc RISC-V (빌드 스크립트 포함)로 컴파일 된 코드 실행
  • 는 3 주기로 명령을 실행하고 4 주기로로드 / 저장
  • 1K LUT 미만
  • 는 약 65에서 유효합니다. Mz on the IceStick
  • 사이클 시프터 당 1 비트
  • 32 비트 RDCYCLE
  • 는 DooM과 함께 제공됩니다. 화재 데모;)

전체 프로세서 코드

디자인 실행

빌드는 두 단계로 수행됩니다. , 먼저 프로세서가 실행할 코드를 컴파일하십시오.

에서 프로젝트 / ice-v (이 디렉토리) 실행 :

  ./ compile_c.sh tests / c / test_leds.c  

플러그 프로그래밍을 위해 컴퓨터에 보드를 연결하고 프로젝트 폴더에서 다음을 실행합니다.

IceStick에서 LED는 회전 패턴으로 중앙 주변에서 깜박입니다.

다음을 사용하여 설계를 시뮬레이션 할 수도 있습니다.

  ./ compile_c.sh tests / c / test_leds_simul.c make verilator  

콘솔은 y까지 LED 패턴을 출력합니다. ou CTRL + C를 눌러 시뮬레이션을 중단합니다.

  LED : 00001 LED : 00010 LED : 00100 LED : 01000 LED : 00001 ...  

선택적으로 작은 OLED 화면을 연결할 수 있습니다 (SSD1351 드라이버와 함께 128×128 RGB 사용).

IceStick의 핀아웃은 :

IceStick OLED
PMOD10 (핀 91 ) 소음
PMOD9 (핀 90) clk
PMOD8 (핀 88) cs
PMOD7 (핀 87) dc
PMOD1 (핀 78) rst

이 기능을 통해 DooM 화재 또는 스타 필드 데모를 테스트 할 수 있습니다.

DooM 화재의 경우 :

  ./compile_c.sh tests / c / fire.c make icestick -f Makefile.oled  

노트: 프로세서의 컴파일 코드에는 RISC-V 툴체인. Windows에서 이것은 내 fpga-binutils repo의 바이너리 패키지에 포함되어 있습니다. macOS 및 Linux에는 미리 컴파일 된 패키지가 있거나 소스에서 컴파일하는 것을 선호 할 수 있습니다. 자세한 지침은 시작하기를 참조하세요.

Ice-V 디자인 : 코드 연습

Ice-V를 테스트 했으니 이제 코드를 살펴 보겠습니다! 전체 프로세서는 300 줄 미만의 Silice 코드 (주석없이 ~ 130 개)에 적합합니다.

리스크 -V 프로세서는 놀랍도록 간단합니다! 또한 Silice 구문과 기능을 발견 할 수있는 좋은 기회입니다.

프로세서가 파일 아이스에 있습니다. 바이스. 데모의 경우 ice-v-soc.ice 파일의 미니멀 SOC에 포함되어 있습니다.

프로세서는 세 가지 알고리즘으로 구성됩니다.

  • 알고리즘 실행 는 메모리에서 방금 읽은 32 비트 명령어를 정보로 분할합니다. 나머지 프로세서 (디코더)에서 사용하고 모든 정수 산술 (ALU) 수행 : 더하기, 하위, 시프트, 비트 연산자 등
  • 알고리즘 rv32i_cpu 는 주 프로세서 루프입니다. 메모리에서 명령어를 가져오고, 레지스터를 읽고,이 데이터로 디코더 및 ALU를 설정하고, 필요에 따라 추가 메모리로드 / 저장을 수행하고, 결과를 레지스터에 저장합니다.

의 프로세서 루프 개요 알고리즘 rv32i_cpu .

프로세서 루프

처음에는 모든 것을 건너 뛰고 (필요할 때 다시 돌아올 것입니다) 명령을 실행하는 무한 루프에 집중합니다. 다음과 같은 구조가 있습니다 :

 동안 (1 ) {  //  1.-방금 사용 가능한 지침    // -설정 레지스터 읽기  ++ :   //  레지스터 읽기 대기 (1주기)    //  2.-등록 데이터 사용 가능    // -트리거 ALU  동안 (1) {  //  디코딩 + ALU 루프에 진입하는 동안 (1주기 )    //  디코더 및 ALU 사용 가능  만약 (exec. 하중 |  exec. 저장) {  //  4 .-설정로드 / 저장 RAM 주소    // -메모리 저장소를 사용 하시겠습니까?  ++ :   //  메모리 트랜잭션 대기 (1주기)    //  5.-로드 된 데이터를 regis에 기록  ter?    // -다음 명령어 주소 복원  단절;    //  완료    //  루프백 중 다음 명령어 읽기 (1주기) } 그밖에 {  //  6.-명령 결과를 레지스터에 저장    // -다음 명령어 주소 설정  만약 (exec. == 0 ) {   //  ALU 완료?  단절;    //  완료    //  루프백 중 다음 명령어 읽기 (1주기) }}}} 

루프 구조는 대부분의 명령어가 세 사이클을 거치도록 구성되며로드 / 저장에는 추가 사이클이 필요합니다. 또한 때때로 여러 사이클이 필요한 ALU를 기다릴 수 있습니다 (시프트는 사이클 당 1 비트 진행). Silice는 제어 흐름 (while / break / if / else)에서 사이클이 사용되는 방식에 대한 정확한 규칙을 가지고있어, 사이클이 낭비되지 않도록 루프를 작성할 수 있습니다.

새로운 지침이 들어옵니다

이 단계를 단계별로 살펴 보겠습니다. 첫번째 동안 (1) 는 주 프로세서 루프입니다. 반복 시작시 (마커 1.) 시작시 부팅 주소 또는 이전 반복 설정에서 메모리에서 명령어를 사용할 수 있습니다. 먼저 메모리에서 읽은 데이터를 로컬 instr 변수이므로 다른 메모리 트랜잭션을 자유롭게 수행 할 수 있습니다. 또한 라는 변수에 명령어가 들어온 메모리 주소를 복사합니다. pc for 프로그램 카운터.

   //  데이터를 사용할 수 있습니다  instr=mem.rdata;  pc=mem.addr; 

레지스터

에 지침 저장 instr 도 읽은 값을 업데이트합니다. 레지스터에서. 그것은 always_after 수행 할 작업을 지정하는 블록 모든주기 다른 모든 것 이후 . always_after 블록에는 에서 읽은 레지스터를 설정하는이 두 줄이 있습니다. instr :

   //  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv 지금은 무시  xregsA.addr=  /  xregsA.wenable?  exec.write_rd :  /   Rtype (instr) .rs1;  xregsB.addr=  /  xregsA.wenable?  exec.write_rd :  /   Rtype (instr) .rs2; 

레지스터 두 개의 BRAM에 저장됩니다 xregsA xregsB . 설정하여 addr 필드에 레지스터 값이 rdata 다음 클럭주기 . 그것이 우리가 ++ : 마커 뒤 1.

Rtype (instr) .rs1 구문은 다음에 선언 된 비트 필드를 사용합니다. 파일 상단 :

   / /  명령을 쉽게 디코딩 할 수있는 비트 필드  비트 필드 Rtype {uint1 unused1, uint1 sign, uint5 unused2, uint5 rs2, uint5 rs1, uint3 op, uint5 rd, uint7 opcode} 

쓰기 Rtype (instr) .rs1 와 같다 instr (비트 15에서 5 비트 너비), 읽기 / 수정하기 쉬운 형식 .

두 개의 BRAM을 사용하는 이유는 단일 사이클에서 두 개의 레지스터를 읽을 수 있기 때문입니다. 따라서이 두 BRAM은 항상 동일한 값을 포함하지만 독립적으로 읽을 수 있습니다.

BRAM은 프로세서 시작 :

 bram int32 xregsA [32]={인주 ( 0 )};  bram int32 xregsB [32]={인주 ( 0 )}; 

패드 (0) 배열을 z로 채 웁니다. 에로스.

xregsA xregsB 는 항상 함께 기록되므로 동일한 값을 보유합니다. 이것은 또한 always_after 블록:

   //  두 레지스터 BRAM에 데이터 쓰기  xregsA.wdata=write_back;  xregsB.wdata=쓰기 _ 백;    //  xregsA가  일 때 작성되는 xregsB  xregsB.wenable=xregsA.wenable;    //  write_rd에 쓰고, 그렇지 않으면 명령 레지스터를 추적합니다  xregsA.addr=xregsA. 웬만한?  exec.write_rd : Rtype (instr) .rs1;  xregsB.addr=xregsA.wenable?  exec.write_rd : Rtype (instr) .rs2; 

두 BRAM wdata 필드가 동일한 으로 설정되어 있습니다. 다시 쓰기 값 및 xregsB.wenable 트랙 xregsA.wenable . 마지막으로 자신의 분야 wenabled=1 둘 다 같은 파일에 씁니다 addr 주어진 exec.write_rd . 이렇게하면 두 BRAM에 항상 동일한 값이 포함됩니다.

디코더 및 ALU 트리거

이 설정 후 한주기를 기다립니다 ( ++ : ) BRAM 출력에서 ​​사용할 수있는 레지스터 값. 레지스터 값을 사용할 수있게되면 (마커 2. ), 디코더 및 ALU는 이러한 업데이트 된 값에서 새로 고침을 시작합니다. 디코더와 ALU는 둘 다 라는 두 번째 알고리즘으로 그룹화됩니다. 실행 . 의 출력 실행 는 ‘dot’구문으로 액세스됩니다. exec.name_of_output .

알고리즘 실행은 다음과 같이 프로세서 내부에서 인스턴스화됩니다.

// 디코더 + ALU, 명령을 실행하고 프로세서에 수행 할 작업 실행 exec (instr <:: instr pc xa xregsa.rdata xb xregsb .rdata>

알고리즘이 명령을 수신합니다 instr , 프로그램 카운터 pc , 레지스터 값 xregsA.rdata xregsB.rdata . 이것들은 경계를 배선 연산자 로 알고리즘의 입력에 <::> 및 <:>. 타이밍과 관련된 두 가지 사이에는 중요한 차이가 있습니다. 운영자 <::>는 인스턴스화 된 알고리즘 과 같이 변수가 연결되었음을 의미합니다. 실행 는 값 전에 호스트 알고리즘에 의해 변경됨 rv32i_cpu . 따라서 실행 둘 다 instr pc 는 마커에 할당됩니다 1., 그러나 다음주기 이후)에만 변경 사항이 표시됩니다. ++ : ). 이로 인해 1주기 지연이 발생하지만 회로가 짧아 져 설계의 최대 주파수가 증가합니다. 이것은 디자인에서 중요한 트레이드 오프입니다.

마커로 돌아 가기 2. , 레지스터 데이터를 사용할 수있는 즉시 정보가 흐릅니다 execute 우리는 특별히 할 일이 없습니다. 그러나 의 ALU 부분은 실행 는이 특정주기에서 계산을 트리거해야한다고 알려야합니다.

알고리즘 바인딩 및 타이밍의 (중요!) 주제에 대한 모든 세부 사항은 전용 페이지를 참조하십시오.

그만큼 작업 루프

그때 초를 입력합니다 동안 (1) 루프. 많은 경우에 우리는 단 한 사이클 후에이 두 번째 루프를 빠져 나갈 것이지만 때때로 ALU는 여러 사이클에 걸쳐 작동해야하므로 루프가 대기 할 수 있습니다. 루프에 들어가는 데는 한 사이클이 걸리므로 루프에 들어가는 동안 데이터는 실행 및 루프에있을 때 출력이 준비됩니다.

루프에서 두 가지 경우를 구분합니다.로드 / 저장이 수행되어야합니다 if (exec.load | exec.store) 또는 다른 명령이 실행 중입니다. 먼저 두 번째 경우 (마커 6. ). 비로드 / 저장 명령이 디코더와 ALU를 통해 실행되었습니다.

로드 / 스토어 외

먼저 레지스터에 명령어 결과를 쓰는 것을 고려합니다. 이 작업은 다음 코드로 수행됩니다.

   //  레지스터에 결과 저장  xregsA.wenable=~ exec.no_rd; 

리콜 xregsA 는 BRAM 보유 레지스터 값. 이것의 필드는 쓰기 (1) 또는 읽기 (0) 여부를 나타냅니다. 여기서 디코더 출력 exec.no_rd 가 낮습니다. 하지만 조금 짧아 보입니까? 예를 들어 우리는 어디에 말합니까 쓰기? 이것은 실제로 always_after 블록, 앞서 살펴본 것처럼 레지스터 섹션). 쓸 데이터는 다음과 같이 설정됩니다 :

 xregsA.wdata=write_back;  xregsB.wdata=쓰기 _ 백;  

결과를 작성할 때 다시 설정할 필요가없는 이유를 설명합니다. 레지스터에 대한 명령의.

그런데 왜 그렇게합니까? 이 코드를 6. 나머지와 함께? 이것은 회로 크기와 주파수 측면에서 효율성을위한 것입니다. 과제가 6. 이 특정 상태에서만 수행되도록 더 복잡한 회로가 생성됩니다. 더 복잡한 멀티플렉서 회로가 필요하므로 항상이 값을 에 맹목적으로 설정하는 것이 가장 좋습니다. always_after 블록. 설정하지 않는 한 xregsA.wenable=1 어쨌든 아무것도 기록되지 않습니다. 이것은 효율적인 하드웨어 설계의 매우 중요한 측면이며 불필요한 조건을 신중하게 피함으로써 설계를 훨씬 더 효율적으로 만들 수 있습니다. 기쁨 또한 Silice 설계 지침을 참조하십시오.

그래서 다시 쓰기? 다음 코드로 정의됩니다.

   //  레지스터에 무엇을 쓰나요?  (pc, alu 또는 val, 부하는 별도로 처리됩니다)    //  '또는 femtorv32  int32 write_back <:> 2 ) : 32b0) |  (exec.storeAddr? exec.n [0,$addrW+2$] : 32b0) |  (exec.storeVal? exec.val : 32b0) |  (exec.load?로드 됨 : 32b0) |  (exec.intop? exec.r : 32b0); 

write_back <: ...>는 식 추적기를 정의합니다 : 읽기 전용 변수 다시 쓰기는 정의에 지정된 표현식의 별칭입니다 ( ㅏ 철사 Verilog 용어). 다시 쓰기는 디코더 출력에 따라 다시 쓸 값을 제공합니다. exec.storeAddr 는 에서 ALU에 의해 계산 된 주소를 다시 쓰도록 나타냅니다. exec.n (AUIPC). exec.storeVal 값을 다시 쓰도록 나타냅니다 exec.val 디코더 (LUI 또는 RDCYCLE)에서 . exec.jump 는 다시 쓰기를 나타냅니다 next_pc (JAL, JALR, 조건부 분기 사용). 시프트는 32 비트 명령어 포인터를 바이트 주소로 변환합니다.

좋구나! 레지스터가 업데이트됩니다. 마커로 돌아 가기 6. 다음으로 가져 와서 실행할 다음 명령어의 주소를 설정합니다.

하중/ 저장

비로드 / 저장 지침을위한 것입니다. 이제 if (exec.load | exec.store) 로드 / 스토어 처리 방법을 확인하세요. Ice-V는 BRAM에 특화되어 있기 때문에 모든 메모리 트랜잭션이 단일주기를 사용한다는 것을 알고 있습니다. 이주기를 고려해야하지만 외부 메모리 컨트롤러에서 알 수없는주기를 기다려야하는 것에 비해 큰 사치입니다.

마커 도달시 . 4 먼저로드 / 스토어 주소를 설정합니다. 이 주소는 ALU에서 가져옵니다 :

   // ==Store (exec.store==1 인 경우 아래 활성화)    //  SB, SH, SW에 따라 쓰기 마스크 구축  mem.wenable=({ 4  {exec. 저장}} 및 {{ 2  {exec.  op  [0,2]==2b10}}, exec.  op  [0,1] |  exec.  op  [1,1], 1b1}) 0,2]; 

약간 애매하게 보일 수 있지만 이것이하는 일은 형식의 쓰기 마스크를 생성합니다. 4b0001, 4b0010, 4b0100, 4b1000 (SB) 또는 4b0011, 4b1100 (SH) 또는 4b1111 (SW) exec.op [0,2] (부하 유형) 및 exec.n [0,2] (최하위 비트 주소). 이것은 결국 상점이 아닐지도 모르기 때문에 마스크와 사이의 AND exec.store 은 적용되다. 구문 {4 {exec.store}} 는 비트 exec.store 를 4 번 복제하여 uint4 .

다음으로 한주기를 기다립니다. BRAM에서 발생할 메모리 트랜잭션 ++ : . 그것이 우리가 방금 작성한 상점이라면 마커에 도달하면 끝났습니다 5.

그것이로드라면 우리는 방금 메모리에서 읽어 들였고 이제 결과를 선택된 레지스터에 저장해야합니다. 이것은 다음 코드에 의해 수행됩니다 :

   // ==로드 (exec.load==1 인 경우 아래에서 활성화 됨)    //  커밋 결과  xregsA.wenable=~ exec.no_rd;  

xregsA.wdata xregsA.addr 는 나중에 [15,1]에서 올바르게 설정됩니다. always_after 블록 wdata 가 할당되었습니다 다시 쓰기. 다시 쓰기 선택 짐을 실은 언제 exec.load 가 높습니다. (의 정의 참조) 다시 쓰기). 짐을 실은는 다음과 같이 정의됩니다.

) 정렬 [ 7,1]}}, 정렬 [ 0,8]}; } case 2b01 : {로드 됨={{16 {(~ exec.op [2,1]) & aligned [15,1]} }, aligned [ 0,16]};} 케이스 2b10 : {로드 됨=정렬 됨; } 기본값 : {로드 됨={32 {1bx}}; } // 상관 없음 (발생하지 않음)} “>

   // 는 메모리에서로드 된 값을 디코딩합니다 (exec.load==1 일 때 사용됨)  uint32 정렬 <: mem.rdata>> {exec.  [0,2], 3b000  };  스위치 (exec.op [0,2]) {  //  LB / LBU, LH / LHU, LW  케이스 2b00 : {로드 됨={{ 24  {(~ exec.  op  [2,1]) 정렬 [ 7,1]}}, 정렬 [ 0,8]};  } 케이스 2b01 : {로드 됨={{ 16  {(~ exec.  op  [2,1]) 정렬 [15,1]}}, 정렬 됨 [ 0,16]};} 케이스 2b1  0 : {로드 됨=정렬 됨;  } 기본 : {로드 됨={ 32  {1bx}};  }   //  상관 없음 (발생하지 않음) } 

바이트 (LB / LBU), 16 비트 여부에 따라로드 된 값을 선택합니다. (LH / LHU) 또는 32 비트 (LW)에 액세스했습니다 (U는 서명되지 않음을 나타냄). mem.rdata 는 메모리에서 바로 나오는 값이며 정렬 이 주소 최하위 비트 exec.n [0,2] .

{exec.n [0,2], 3b000} 는 단순히 exec.n [0,2] (왼쪽으로 3 비트 이동은 3 개의 0 비트를 연결하는 것과 같습니다. 오른쪽으로).

로드 / 저장이 완료된 후 다음 명령을 복원합니다. 주소 next_pc , 프로세서가 중단 후 다음 반복을 진행할 준비가되도록합니다.

   //  프로그램 카운터에 주소 복원  wide_addr=next_pc;    //  작업 루프 종료  단절; 

그리고 그게 다야! 우리는 전체 프로세서 로직을 보았습니다. 이제 다른 구성 요소에 대해 살펴 보겠습니다.

디코더

디코더는 알고리즘 실행 . 비교적 간단한 일입니다. 가능한 모든 것을 디코딩하여 시작합니다 즉시 값-다음과 같은 다양한 유형의 명령어로 인코딩 된 상수입니다.

   //  즉시 디코딩  int32 imm_u <:>12,20], 12b0};  int32 imm_j <:> 12  {instr [31,1]}}, instr [12,8], instr [20,1], instr [21,10], 1b0};  int32 imm_i <:> 20  {instr [31,1]}}, instr [20,12]};  int32 imm_b <:> 20  {instr [31,1]}}, instr [7,1], instr [25,6], instr [8,4], 1b0};  int32 imm_s <:> 20  {instr [31,1]}}, instr [25,7], instr [7,5]}; 

이 값은 일치 명령이 실행될 때만 사용됩니다. 예를 들어 imm_i 는 레지스터 즉시 정수 연산에 사용됩니다.

다음 부분은 opcode를 확인하고 가능한 모든 명령어에 대해 부울을 설정합니다.

uint5 opcode    <: instr>2, 5];uint1 AUIPC     <: opcode="=5b00101;" uint1 lui jal jalr intimm intreg cycles branch>

These are of course mutually-exclusive, so only one of these is 1 at a givencycle.

Finally we set the decoder outputs, telling the processor what to do with the instruction.

//====set decoder outputs depending on incoming instructions// load/store?load         :=opcode==5b00000;   store        :=opcode==5b01000;   // operator for load/store           // register to write to?op           :=Rtype(instr).op;     write_rd     :=Rtype(instr).rd;    // do we have to write a result to a register?no_rd        :=branch  | store  | (Rtype(instr).rd==5b0);// integer operations                // store next address?intop        :=(IntImm | IntReg);   storeAddr    :=AUIPC;  // value to store directly           // store value?val          :=LUI ? imm_u : cycle; storeVal     :=LUI     | Cycles;   

The always assign operator := used on outputs means thatthe output is set to this value first thing every cycle (this is a shortcutequivalent to a normal assignment = in an always_before block).

For instance write_rd :=Rtype(instr).rd is the index of the destinationregister for the instruction, while no_rd :=branch | store | (Rtype(instr).rd==5b0)indicates whether the write to the register is enabled or not.

Note the condition Rtype(instr).rd==5b0 in no_rd. That is becauseregister zero, as per the RISC-V spec, should always remain zero.

The ALU

The ALU performs all integer computations. It consists of three parts. Theinteger operations such as ADD, SUB, SLLI, SRLI, AND, XOR (output r) ;the comparator for conditional branches (output jump) ; the next address adder (output n).

Due to the way the data flow is setup we can use a nice trick. The ALU as wellas the comparator select two integers for their operations. The setup of the Ice-Vis such that both can input the same integers, so they can share the same circuitsto perform similar operations. And what is common to ,,>,>=? Theycan all be done with a single subtraction! This trick is implemented as follows:

   // ====단일 가산기로 뺄셈과 모든 비교를 할 수 있습니다    //  femtorv32 / swapforth / J1의 트릭  int33 a_minus_b <: b xa uint1 a_lt_b>31,1] ^ b [31,1])?  xa [31,1] :   a_minus_b [32,1];  uint1 a_lt_b_u <: a_minus_b>32,1];  uint1 a_eq_b <: a_minus_b>0,32]== 0 ; 

xa 가 첫 번째 레지스터이고 는 디코더의 결과 :

//====select ALU second input int32 b         <: regorimm : imm_i>

The choice is made by this line in the decoder:

uint1 regOrImm  <: intreg branch>

Similarly, the next address adder selects its first input based on the decoderindications:

   // ====다음 주소 추가 기 첫 번째 입력 선택  int32 addr_a <: pcorreg __signed>0,$addrW-2$], 2b0}) : xa; 

예를 들어 , 지침 AUIPC, JAL 분기는 프로그램 카운터 [1,1]를 선택합니다. pc addr_a 디코더에서 볼 수 있듯이 :

uint1 pcOrReg   <: auipc jal branch>

The second value in the next address computation is an immediate selected basedon the running instruction:

//====select immediate for the next address computationint32 addr_imm  <: imm_u : imm_j imm_b imm_i imm_s>

The next address is then simply the sum of addr_a and the immediate: n=addr_a + addr_imm.

The comparator and most of the ALU are switch cases returning the selectedcomputation from op :=Rtype(instr).op.For the comparator:

   // =====================분기 비교기  스위치 (op [1,2]) {케이스 2b00 : {j=a_eq_b;  }   /  BEQ  /   케이스 2b10 : {j=a_lt_b;}   /  BLT  /   케이스 2b11 : {j=a_lt_b_u;}   /  BLTU  /   기본 : {j=1bx;  }} 점프=(JAL | JALR) |  (분기 & (j ^ op [0,1]));    //  ^^^^^^^ 비교기 결과를 부정합니다  

정수 연산의 경우 :

   //  모든 ALU 작업  스위치 (op) {케이스 3b000 : {r=sub?  a_minus_b : xa + b;  }   //  ADD / SUB  케이스 3b010 : {r=a_lt_b;  } 케이스 3b011 : {r=a_lt_b_u;  }   //  SLTI / SLTU  케이스 3b100 : {r=xa ^ b;  } 케이스 3b110 : {r=xa |  비;  }   //  XOR / OR  케이스 3b001 : {r=시프트;  } 케이스 3b101 : {r=시프트;  }   //  SLLI / SRLI / SRAI   케이스 3b111 : {r=xa & b;  }   //  AND  기본 : {r={ 32  {1bx}};  }   //  상관 없음 } 

그러나 교대조 (SLLI, SRLI, SRAI)를 위해 뭔가 진행되고 있습니다. 실제로 정수 시프트 >> 는 한 주기로 수행 할 수 있지만 큰 회로 ( 많은 LUT!). 대신 우리는 컴팩트 한 디자인을 원합니다. 따라서 ALU의 나머지 코드는 사이클 당 1 비트 이동하는 시프터를 설명합니다. 여기있어:

 int32 시프트 ( 0 );    //  시프트 (클럭 당 1 비트)  만약 (작동 중) {  //  시프트 크기 감소  shamt=shamt-1;    //  1 비트 시프트  shift=op [2,1]?  ( Rtype  (instr). 기호? {r [31,1],아르 자형[1,31]} : { __ signed  (1b0), r [1,31]}) : {r [0,31],  __ 서명 됨  (1b0)};  } 그밖에 {  //  변속을 시작 하시겠습니까?  shamt=((aluShift & trigger)?  __ unsigned (비[0,5]) :  0 );    //  이동할 값 저장  시프트=xa;  }   //  우리는 여전히 변하고 있습니까?  작업=(shamt!= 0 ); 

아이디어는 시프트는 이동의 결과입니다 아르 자형 각 사이클마다 1 비트 씩. 아르 자형가 로 업데이트됩니다. 시프트 ALU 스위치 케이스 : 케이스 3b001 : {r=shift; } case 3b101 : {r=shift; } . 처음에는 시프터가 아닙니다 시프트가 할당되었습니다 xa . 그 후, 시프트는 입니다. 아르 자형 가 적절한 서명으로 1 비트 이동 : shift=op [2,1]? ...

shamt 는 이동할 비트 수입니다. 디코더에서 읽은 양으로 시작합니다 ((aluShift & trigger)? __unsigned (b [0,5]) : 0) 후 각각 하나씩 감소 주기 .

노트 어떻게 방아쇠는 테스트 초기화 [20,12]에 사용됩니다. shamt 및 시프터 시작. 이렇게하면 일 때 올바른 사이클에서만 변속기가 트리거됩니다. exec.trigger 가 1 프로세서에 의해 .

그리고 Voilà, 우리의 ALU가 완료되었으며 프로세서의 모든 주요 구성 요소를 보았습니다. !

더 나아가

Ice-V는 멋진 놀이터 역할을 할 수 있습니다. 대기 인터페이스가있는 RAM에 연결하고 RV32I 로 바꾸는 것이 좋습니다. 미디엄, 디코더 및 ALU의 다른 설정으로 실험하십시오. 많은 가능성과 트레이드 오프가 있습니다!

연결

이 구현은 다른 프로젝트의 이점을 크게 얻었습니다 (소스의 주석 참조) :

다른 몇 가지 훌륭한 RISC-V 프로젝트 (많이 있습니다! 링크를 추가하게되어 기쁘게 생각합니다. 알려주세요)

  • 세계에서 가장 작은 프로세서 : SERV
  • vexriscv
  • neorv32

도구 체인 링크 :

더보기

Author: Joan Pepper

Leave a Reply