RISC-V용 소프트웨어와 컴파일러

누군가 처음부터 알려 주었다면 도움이 되었을 내용입니다.

RISC-V gcc 컴파일러

RISC-V에는 여러 '변형(flavor)'이 있으며, 가장 먼저 기본 정수 명령어 집합(RV32I)이 있습니다. 여기에 선택적 확장으로 곱셈과 나눗셈(M), 단정밀도 및 배정밀도 부동소수점(F,D), 원자적 명령어(A)가 있습니다.

사용할 컴파일러는 당연히 프로세서가 어떤 명령어를 지원하는지에 따라 달라집니다(참고: 예외 핸들러에서 잘못된 명령어를 잡아 그 명령어가 할 일을 소프트웨어로 처리하는 방식으로 명령어를 에뮬레이트할 수도 있지만, FemtoRV32는 아직 예외를 지원하지 않습니다). 여기서는 RTL/femtosoc_config.v에 설정된 내용에 따라 RV32I 또는 RV32IM을 대상으로 하는 컴파일러가 필요합니다(IceStick에서는 RV32I 외에는 선택지가 없습니다). 앞으로 F를 지원하고, 어쩌면 D도 지원할 계획입니다.

RISC-V gcc 컴파일러에 관해 알아 두어야 할 것이 하나 더 있습니다(저도 이것을 이해하는 데 시간이 좀 걸렸습니다). gcc는 대상 명령어 집합과 별개로, Linux 시스템을 대상으로 하는지(이 경우 GCC의 이름은 riscv64-linux-gnu-gcc입니다) 마이크로컨트롤러를 대상으로 하는지(riscv64-unknown-elf-gcc)에 따라 두 가지 변형으로도 나뉩니다. 툴체인의 모든 실행 파일은 같은 접두사로 시작합니다. 접두사가 riscv64로 시작하지만, 이 툴체인은 32비트 riscv 타깃(여기서 사용하는 것)과 64비트 타깃을 모두 지원한다는 점에 유의하십시오.

여기서는 마이크로컨트롤러용 컴파일러가 필요합니다(FemtoRV32는 아직 Linux를 실행할 수 없습니다). Linux 배포판에서 일반적으로 제공하는 패키지는 Linux 시스템을 대상으로 하므로 그대로 사용할 수 없습니다(베어메탈 펌웨어에는 여전히 사용할 수 있지만, FemtoRV32와 호환되는 elf를 생성하는 데에는 사용할 수 없습니다. 자세한 내용은 아래에서 설명합니다). 여러 코어 구성을 실험해 보거나 Verilog 또는 예외 핸들러로 FPU를 구현하려 한다면, 가능한 모든 RV32I(M)(A)(F)(D) 조합을 설치해 두는 것도 좋습니다.

SIFIVE 웹사이트에서 미리 컴파일된 툴체인을 받을 수 있습니다. 여기를 참고하십시오. 이 툴체인은 Linux나 Windows 10/WSL에서 바로 사용할 수 있습니다. FemtoRV32의 Makefile(FIRMWARE/makefile.inc)이 이를 자동으로 내려받습니다. 용량이 큰 패키지(300Mb 정도)이지만, 가능한 모든 명령어 집합 조합과 더 중요하게는 관련 라이브러리까지 갖추게 됩니다. 이것이 중요한 이유는, 예를 들어 RV32I를 사용하면 하드웨어 곱셈이 없기 때문입니다. 이 경우 곱셈을 수행하는 함수가 필요하고(올바른 gcc 라이브러리와 링크해야 합니다), 컴파일러가 MUL 명령어 대신 그 함수를 호출하는 코드를 생성하도록 지정해야 합니다. 이는 모든 부동소수점 연산에도 해당하며, 부동소수점 연산은 정수 곱셈을 사용해 구현하거나 소프트웨어 버전을 호출하는 방식으로 처리할 수 있습니다. 조합은 매우 다양합니다. 이것은 FIRMWARE/makefile.inc의 앞부분에서 ARCH를 설정하여 선택합니다(현재는 rv32i 또는 rv32im이지만, c,a,f... 등 다른 모든 확장에 대한 준비도 되어 있습니다).

더 자세히 알고 싶거나 한층 더 베어메탈에 가까운 방식을 원한다면 다음을 참고하십시오.

gcc로 오브젝트 파일 .o만 생성한 다음, (표준 C 런타임을 사용하는 대신) 필요한 함수를 구현한 라이브러리와 ld로 링크하는 방법도 있습니다. 이렇게 하면 컴파일러가 Linux 런타임을 대상으로 하더라도 동작합니다(C 라이브러리를 사용하지 않기 때문입니다). 처음에는 저도 이렇게 했지만, 모든 것이 고통스러워지므로 권장하지 않습니다(바퀴를 다시 발명해야 하고, memcpy() 등을 다시 구현해야 하며, 부동소수점도 없습니다). 정말로 이렇게 하고 싶다면 FIRMWARE/LIBFEMTOC/Makefile을 살펴보고 MISSING_OBJECTS와 MISSING_OBJECTS_WITH_DIR의 주석을 해제하십시오. 이것이 어떻게 동작하는지, 그리고 이러한 함수를 어떻게 찾는지 더 알고 싶다면 툴체인과 라이브러리의 소스가 여기에 있습니다. 곱셈 함수의 소스는 여기에서 찾았습니다: riscv-gcc/libgcc/config/riscv/muldi.S. 64비트(muldi) 곱셈과 32비트(mulsi) 곱셈에 같은 함수를 사용하며, 매크로로 알맞은 쪽을 선택한다는 점에 유의하십시오. 나눗셈과 나머지 연산도 같은 디렉터리에 있습니다. 그곳에서 찾을 수 있는 또 다른 흥미로운 함수는 C 런타임 시작 함수인 riscv-newlib/libgloss/riscv/crt0.S입니다. 나중에 elf 실행 파일을 실행할 때 C 런타임이 우리에게 무엇을 기대하는지 알 수 있으므로, 이 함수를 살펴보는 것이 좋습니다.

RISC-V 실행 파일 만들기: 베어 메탈 ELF와 표준 ELF

기본적으로 gcc는 ELF 형식(Executable and Linkable Format)의 실행 파일을 생성합니다. 이제 이것을 FPGA 위의 risc-v 프로세서에 로드할 수 있는 형태로 변환하려고 합니다. 이를 위한 방법은 두 가지가 있습니다(그리고 둘 다 해 볼 것입니다):

  1. ELF 실행 파일을 16진수로 된 ascii 파일인 FIRMWARE/firmware.hex로 변환합니다. 이 파일은 RTL/femtosoc.v에서 하는 것처럼 Verilog의 readmemh() 함수로 직접 로드하여 RAM을 초기화할 수 있습니다. 이 방식은 SD 카드 리더가 없거나 RAM이 충분하지 않은 소형 장치(IceStick)에서 사용합니다.

  2. ELF 실행 파일을 그대로 SD 카드에 복사하고, 최소한의/조잡한 OS(FemtOS)로 프로그램을 시작합니다. 이 방법을 사용하려면 OS가 RAM에 미리 로드되어 있어야 한다는 점에 유의하십시오(두 방법이 모두 필요한 이유가 이것입니다).

  3. 실행 파일을 장치의 SPI 플래시에 복사합니다. FPGA 보드에는 FPGA의 구성을 저장하는 데 쓰이는 다리 8개짜리 작은 칩이 달려 있는 경우가 많습니다. 이 칩은 수 메가바이트 용량의 플래시 메모리이며, 직렬 프로토콜로 접근합니다. FPGA의 구성은 수십 킬로바이트밖에 차지하지 않습니다. 따라서 그곳에는 코드를 저장할 공간이 많이 남으며, FemtoRV32가 그 코드를 직접 실행하게 할 수도 있습니다! 이를 위해서는 특별한 링커 스크립트와 C 시작 함수가 필요합니다.

베어 메탈 ELF

먼저 방법 1)을 더 자세히 살펴봅시다. 사실 우리에게 필요한 중요한 것이 하나 더 있습니다. gcc는 기본적으로 특정한 메모리 맵을 가진 실행 파일을 생성하는데, 이 메모리 맵이 우리의 필요에 꼭 맞지는 않습니다. 예를 들어 텍스트 세그먼트는 주소 0x10000(=64kb)에서 시작합니다. IceStick에는 그만한 양의 RAM조차 없습니다. femtosoc.v의 주소 디코더를 배선하여 RAM이 인위적으로 이 주소에서 시작하도록 만들 수도 있지만, gcc에게 다른 메모리 맵을 사용하도록 지시하는 방법도 있다는 점에 유의하십시오. 이를 위해 저는 FIRMWARE/CRT_BAREMETAL/femtorv32.ld에 있는 링커 스크립트를 사용하며, 그 내용은 다음과 같습니다:

MEMORY
{
   BRAM (RWX) : ORIGIN = 0x0000, LENGTH = 0x40000
}
SECTIONS
{
    .text :
    {
        crt0.o (.text) 
        *(.text)
    }
}

면책 조항: 저도 여기서 하는 일을 완전히 이해하지는 못합니다. 링커 스크립트는 그 자체로 하나의 학문 분야인 것 같지만, 적어도 여기서 한 작업은 제 필요에 맞는 것 같습니다!

같은 디렉터리에는 gcc의 기본 C 런타임 초기화 함수를 대체하는 제 함수 crt0.S도 있습니다:

.include "femtorv32.inc"

.text
.global _start
.type _start, @function

_start:
.option push
.option norelax
     li gp,IO_BASE       #   Base address of memory-mapped IO
.option pop

     lw sp,IO_RAM(gp)      # Read RAM size in hw config register and
        #   initialize SP one position past end of RAM
    
     # Should find a way of clearing BSS here...
    
     call main
     tail exit

이 함수는 여러 가지 일을 합니다:

우리의 베어 메탈 환경에서는 기본 crt0.S를 반드시 교체해야 합니다. 기본 crt0.S는 스택 포인터를 초기화하지 않기 때문입니다. 제 말을 믿지 못하시겠다면, riscv 툴체인의 소스를 설치한 경우 riscv-newlib/libgloss/riscv/crt0.S를 직접 살펴보십시오. 스택 포인터 초기화는 OS가 할 일이기 때문입니다(그리고 우리는 일종의 OS를 작성하고 있으므로 이것은 우리가 할 일입니다!).

그다음 FIRMWARE/EXAMPLES/Makefile에서는 다음과 같이 실행 파일을 생성합니다:

%.bm_elf: %.o $(RV_BINARIES)
    $(RVLD) $(RVLDFLAGS) -T$(FIRMWARE_DIR)/CRT_BAREMETAL/femtorv32.ld $< -o $@ \
    -L$(FIRMWARE_DIR)/CRT_BAREMETAL -L$(FIRMWARE_DIR)/LIBFEMTORV32 -L$(FIRMWARE_DIR)/LIBFEMTOC \
    -lfemtorv32 -lfemtoc $(RVGCC_LIB)

사용된 매크로는 모두 FIRMWARE/makefile.inc에 정의되어 있습니다. 자세한 설명은 다음과 같습니다:

자, 이제 C 프로그램으로부터 주소 0x00000000에 배치될 ELF를 만들 수 있습니다. 이제 이 ELF로부터 Verilog의 readmemh() 함수가 읽을 수 있는 ASCII hex 파일을 생성해야 합니다. 이 작업에는 objcopy 명령을 쓸 수 있습니다:

$ riscv64-unknown-elf-objcopy -O verilog firmware.bm_elf firmware.hex    

... 안타깝게도 아직 끝난 것이 아닙니다. Verilog가 기대하는 형식과 정확히 같은 형식으로 만들어 주지 않기 때문입니다(적어도 저는 그렇게 하는 방법을 찾지 못했습니다). 이를 해결하는 방법은 여러 가지가 있습니다. 예를 들어 Claire Wolf(picorv32 작성자)는 Python 스크립트를 사용합니다. 저는 이 일을 해 주는 firmware_words라는 작은 C++ 프로그램을 작성하기로 했습니다. 또한 이 프로그램은 모든 것이 RTL/femtosoc_config.v에 선언된 메모리에 들어가는지도 확인합니다. 그다음 생성된 파일 FIRMWARE/firmware.hex는 readmemh() Verilog 명령을 통해 FIRMWARE/femtosoc.v의 RAM을 초기화하는 데 사용됩니다.

FIRMWARE 하위 디렉터리에 있는 Makefiles가 이 모든 단계를 수행합니다. 사용 방법은 다음과 같습니다:

$ cd FIRMWARE/EXAMPLES
$ make xxx.hex
$ cd ..

(여기서 xxxx는 컴파일하려는 프로그램의 이름입니다.) 이 명령은 FIRMWARE/firmware.hex를 갱신합니다. 그러면 make ULX3S나 make ICESTICK 등으로 FPGA를 프로그래밍할 준비가 된 것입니다.

자, 문제가 해결되어 기쁩니다. 하지만 어차피 추가 프로그램이 필요했으니, 그 프로그램이 ELF를 직접 읽어서 Verilog hex를 생성하게 만들 수는 없을까요? (그리고 더 나아가, ELF 형식을 읽는 방법을 안다면 그것을 FemtOS에 포함시켜서 SD 카드에서 프로그램을 직접 선택하고 실행하게 할 수는 없을까요?) 두 질문에 대한 답은 모두 YES입니다!

ELF 실행 파일 읽기

Olof Kindgren은 표준 libelf를 사용하는 FuseSoc용 Verilog 플러그인을 여기에 작성했습니다. 하지만 Windows에서 컴파일하는 것은 악몽과 같습니다(애초에 가능한지조차 모르겠습니다). 그래서 저는 다르게 생각했습니다. 우리 필요에 맞는 최소한의 코드만 작성할 수는 없을까요? ELF(Executable and Linking Format, 실행 파일 및 링킹 형식)는 이름이 말해 주는 그대로의 일을 하기 때문에 복잡합니다. 즉, 동적으로 링크될 수 있는 프로그램을 로드하는 데 필요한 것들을 담고 있습니다. 우리의 경우에는 정적으로 링크된 실행 파일만 사용하므로, 물론 코드 섹션을 제외하면 ELF 파일에 있는 정보 대부분을 무시할 수 있습니다! ELF 파일의 내용을 살펴봅시다:

$readelf -a firmware.bm_elf

와, 안에 정말 많은 것이 들어 있습니다. 한번 살펴보면서 추측해 봅시다:

다음 단계는 코드를 읽는 것입니다. Linux 시스템의 /usr/include/elf.h를 살펴봅시다. 이 파일은 매우 범용적이어서 64비트와 32비트 시스템 모두를 위한 정의가 들어 있고, 모든 아키텍처를 다룹니다. 그런데 riscv 툴체인의 readelf 대신 시스템의 readelf 명령을 사용했는데도 잘 동작했습니다! 메타 정보는 아키텍처와 시스템에 전혀 의존하지 않습니다. 훌륭합니다. 와, elf.h는 정말 귀중한 정보원입니다. 모든 필드와 상수가 문서화되어 있어서 무척 마음에 듭니다! 여기서 우리가 해야 할 일은 파일 앞부분의 Elf32_Ehdr 구조체를 fread로 읽은 다음, 파일에서 e-shoff 필드가 가리키는 위치로 fseek하는 것임을 알 수 있습니다. 그런 다음 섹션 헤더를 e_shnum개 읽습니다. 좋습니다. 이어서 elf.h의 뒷부분에서 Elf32_Shdr 구조체를 찾을 수 있는데, 여기에는 readelf가 출력한 표에서 본 모든 열에 대한 설명이 함께 있습니다:

필드 설명
sh_type 우리가 관심을 두는 것은 PROGBITS(섹션을 로드함)와 NOBITS(메모리를 0으로 비움)입니다
sh_flags SHF_ALLOC은 어떤 섹션이 실제로 메모리에 할당되어야 하는지 알려 줍니다
sh_addr 섹션이 메모리에 매핑될 위치
sh_offset 파일 안에서 섹션 데이터가 있는 위치
sh_size 섹션의 바이트 수

훌륭합니다! 이제 무엇을 해야 하는지 정확히 알게 되었습니다. PROGBITS 타입의 각 섹션에 대해 플래그에 SHF_ALLOC이 설정되어 있으면 sh_offset으로 fseek한 다음, sh_size바이트를 fread로 읽어 sh_addr에 저장해야 합니다. NOBITS 타입의 각 섹션에 대해서는 플래그에 SHF_ALLOC이 설정되어 있으면 sh_addr에서 sh_size만큼을 0으로 비워야 합니다. 이는 FIRMWARE/LIBFEMTORV32/femto_elf.h/.c에 구현되어 있습니다. 몇 가지 정보를 추적하기 위해 작은 구조체를 선언했습니다. 특히 기준 주소를 바꿀 수 있는데, 같은 코드를 ELF를 버퍼에 로드할 때(이때 base_address는 버퍼를 가리킵니다)와 ELF를 FemtOS에 로드할 때(이때 base_adress는 NULL입니다) 모두 사용하기 때문입니다. 또한 텍스트 세그먼트의 시작 위치(파일을 실행할 때 FemtOS가 그곳으로 점프하기 때문입니다)와 최대 주소도 추적하여, 로드하기 전에 모든 것이 메모리에 들어가는지 확인합니다:

typedef uint32_t elf32_addr; 
typedef struct {
  void*      base_address; /* Base memory address (NULL on normal operation). */
  elf32_addr text_address; /* The address of the text segment.                */
  elf32_addr max_address;  /* The maximum address of a segment.               */
} Elf32Info;

이제 femto_elf.c에서 ELF를 로드하는 함수는 다음과 같습니다(실제 파일에는 몇 가지 유효성 검사가 들어 있는데, 가독성을 위해 여기서는 제거했습니다):

int elf32_parse(const char* filename, Elf32Info* info) {
  Elf32_Ehdr elf_header;
  Elf32_Shdr sec_header;
  FILE* f = fopen(filename,"r");
  uint8_t* base_mem = (uint8_t*)(info->base_address);
  info->text_address = 0;
  
  /* read elf header */
  fread(&elf_header, 1, sizeof(elf_header), f);
  
  /* read all section headers */  
  for(int i=0; i<elf_header.e_shnum; ++i) {
    
    fseek(f,elf_header.e_shoff + i*sizeof(sec_header), SEEK_SET);
    fread(&sec_header, 1, sizeof(sec_header), f);    
    
    /* The sections we are interested in are the ALLOC sections. Skip the other ones. */
    if(!(sec_header.sh_flags & SHF_ALLOC)) continue;

    /* I assume that the first PROGBITS section is the text segment */
    if(sec_header.sh_type == SHT_PROGBITS && info->text_address == 0) {
      info->text_address = sec_header.sh_addr;
    }

    /* Update max address */ 
    info->max_address = MAX(
      info->max_address, 
      sec_header.sh_addr + sec_header.sh_size
    );
     
    /* PROGBIT, INI_ARRAY and FINI_ARRAY need to be loaded. */
    if(
       sec_header.sh_type == SHT_PROGBITS ||
       sec_header.sh_type == SHT_INIT_ARRAY ||
       sec_header.sh_type == SHT_FINI_ARRAY
    ) {
    if(info->base_address != NO_ADDRESS) {
      fseek(f,sec_header.sh_offset, SEEK_SET);
      fread(
               base_mem + sec_header.sh_addr, 1,
           sec_header.sh_size, f
      );
    }
    }

    /* NOBITS need to be cleared. */    
    if(sec_header.sh_type == SHT_NOBITS && info->base_address != NO_ADDRESS) {  
      memset(base_mem + sec_header.sh_addr, 0, sec_header.sh_size);
    }
  }  
  fclose(f);
  
  return ELF32_OK;
}

(femto_elf.c를 살펴보면 elf.h의 구조체 정의와 상수를 복사해 두었다는 것을 알 수 있습니다. 어디서든, 심지어 미개한 Windows 나라에서도 컴파일되도록 하기 위해서입니다.)

자, 이렇게 해서 이제 RAM을 초기화하는 Verilog ASCII .hex 파일을 출력하는 firmware_words 유틸리티에서 femto_elf.h/femto_elf.c를 사용하고 있습니다.

FemtOS와 표준 ELF 실행 파일

이제 아주 기초적이고 조잡한 운영체제를 작성하는 데 필요한 것을 모두 갖추었습니다. 저는 이 작업을 ULX3S에서 하고 있습니다. 이것은 베어 메탈 실행 파일로, OLED 디스플레이에 파일 목록을 표시하고, 사용자가 버튼으로 파일을 선택하게 한 뒤, 선택한 파일을 실행합니다. 소스는 FIRMWARE/EXAMPLES/commander.c에 있으므로 다음과 같이 빌드할 수 있습니다:

$ cd FIRMWARE
$ ./make_firmware.sh EXAMPLES/commander.c
$ cd ..

(그런 다음 $make ULX3S를 실행합니다).

이제 몇몇 프로그램을 SD 카드에 복사하고, 그것을 ULX3S에 꽂은 다음 실행할 수 있습니다(up과 down 버튼으로 선택하고, right로 실행합니다). reset 버튼은 SD 카드 근처에 있는 버튼입니다.

실행 파일은 다음 명령으로 생성합니다:

$ cd FIRMWARE/EXAMPLES
$ make xxx.elf

(make everything을 실행하여 모든 실행 파일을 SD 카드에 복사할 수도 있습니다.) 이제 FIRMWARE/EXAMPLES/Makefile의 규칙을 보면 매우 간단합니다:

%.elf: %.o $(RV_BINARIES)
    $(RVGCC) $(RVCFLAGS) $< -o $@ -Wl,-gc-sections \
    -L$(FIRMWARE_DIR)/LIBFEMTORV32 -L$(FIRMWARE_DIR)/LIBFEMTOC -lfemtorv32 -lfemtoc -lm

여기서는 사용자 코드를 '0x10000' 주소(즉, 64Kb)에 배치하는 기본 메모리 맵을 그대로 사용할 수 있습니다. FemtOS commander는 64Kb 안에 들어가므로 우리에게 딱 맞습니다!

하지만 어리석은 점이 하나 있습니다. 많은 코드가 중복된다는 것입니다. 예를 들어 SD 카드의 파일에 접근하는 ST_NICCC를 실행하면, (@ultraembedded가 만든) FAT32 라이브러리 전체가 FemtOS에 한 번, 프로그램 이미지에 한 번, 이렇게 두 번 로드됩니다. 물론 몇십 Kb에 불과하지만, 저는 이것이 마음에 들지 않으며 좋은 방식도 아닙니다.

우리가 할 수 있는 일은 두 가지입니다:

첫 번째 방법을 택하려면 ELF 형식에 대해 훨씬 더 많이 배워야 합니다. 두 번째 방법을 택하려면 특권 명령어와 예외를 구현해야 합니다. 아마 이것이 제가 다음에 할 일일 것입니다.

SPI 플래시에서 프로그램 실행하기

IceStick은 BRAM이 많지 않아서(전체 8K, 사용 가능 6K) 복잡한 프로그램은 들어가지 않으며, 한계에 금방 도달합니다. 하지만 SPI 플래시에서 코드를 직접 실행할 수 있습니다. RTL/femtosoc_config.v에서 RTL/CONFIGS/icestick_spi_flash_config.v를 선택한 다음, 이 파일을 편집하여 장착된 장치를 선택하십시오. 이 구성은 "mini-femtorv32" 프로세서를 사용하는데, 이 프로세서는 LUT 사용량이 더 적고 SPI 플래시에서 코드를 직접 실행할 수 있습니다. 그리고 FIRMWARE/SPI_FLASH에 몇 가지 예제가 있습니다. 프로그램 중 하나(예: mandelbrot.c)를 컴파일하고 설치하려면 다음과 같이 하십시오:

$ make mandelbrot.prog

BRAM에서 코드를 직접 실행하는 것보다 훨씬 느으으으리다는 점에 유의하십시오. SPI 플래시는 직렬 프로토콜로 접근하는데, 32비트 워드 하나를 읽는 데 52사이클이 필요하기 때문입니다.

동작 방식은 다음과 같습니다. SPI 플래시는 주소 0x800000에 매핑됩니다. FemtoRV32는 이 주소로 바로 점프하도록 설정되어 있습니다. 그리고 picorv32에서 가져온 링커 스크립트 FIRMWARE/CRT_BAREMETAL/spi_flash.ld가 있는데, 이 스크립트는 코드의 여러 세그먼트를 SPI 플래시 또는 BRAM으로 보냅니다. 읽기 전용 데이터(.rodata)와 작은 읽기 전용 데이터(.srodata) 세그먼트는 SPI 플래시로 보내지고, 초기화되지 않은 데이터 세그먼트(.bss와 .sbss)는 BRAM으로 보내집니다. 초기화된 데이터 세그먼트(.data, .sdata)는 BRAM으로 보내지며, 그 초기화 데이터는 SPI 플래시에 저장됩니다. 그런 다음, 역시 picorv32에서 영감을 받은 C 런타임 시작 코드가 초기화 데이터를 SPI 플래시에서 BRAM으로 복사합니다. 복사할 메모리 영역의 시작 주소와 끝 주소는 링커 스크립트가 내보냅니다. 링커 스크립트에 대해 더 알고 싶다면 이 페이지 끝에 있는 링크를 참고하십시오.

SPI 플래시와 함수 속성

SPI 플래시는 BRAM보다 훨씬 느립니다. SPI Flash에서 명령어 하나를 가져오는 데 44사이클이 걸립니다! (BRAM은 1사이클입니다). 어떤 함수는 자주 사용되므로, 이러한 함수를 SPI 플래시 대신 빠른 메모리에 두는 것이 유용할 수 있습니다. CRT_BAREMETAL에 있는 링커 스크립트 spi_flash.ld는 RV32I 프로세서에서 정수 곱셈과 나눗셈에 쓰이는 __mulsi3,__divsi3, __udivsi3 함수의 사본을 하나씩 BRAM에 두며, 그럴 만한 가치가 있습니다(이 함수들은 매우 짧아서 BRAM을 많이 차지하지 않습니다). 이제 직접 작성한 함수 중 일부를 BRAM에 두고 싶을 수도 있습니다. 다음은 FIRMWARE/SPI_FLASH/riscv_logo_OLED.v에 있는, 그렇게 하는 방법의 예시입니다:

void draw_frame(int frame) __attribute((section(".fastcode"))); 

void draw_frame(int frame) {
....
}

첫 번째 줄은 함수가 어느 섹션에 있어야 하는지를 나타내는 GCC 속성을 선언합니다. 그러면 링커 스크립트 FIRMWARE/CRT_BAREMETAL/spi_flash.ld는 이 함수를 .data_and_fastcode 세그먼트에 넣어야 한다는 것을 알게 됩니다. 이 세그먼트는 BRAM에 있으며, 프로그램이 시작될 때 C 런타임 FIRMWARE/CRT_BAREMETAL/crt0_spiflash.S가 이 세그먼트를 BRAM으로 복사합니다.

C++

FemtoRV용 C++ 프로그램을 컴파일할 수도 있습니다. FIRMWARE/CPP_EXAMPLES 하위 디렉터리에 몇 가지 예제가 들어 있습니다. 가장 간단한 예제(cpp_test)는 IceStick에서 동작합니다. 하지만 작은 레이트레이서는 동작하지 않습니다. C++ 런타임이 사용 가능한 6kB BRAM 중 너무 많은 부분을 차지해서 프로그램에 필요한 스택 공간이 충분히 남지 않기 때문입니다(BRAM이 256kB인 ULX3S에서는 동작합니다). EXAMPLES/tinyraytracer.c에 있는 C 버전은 IceStick에서 동작한다는 점에 유의하십시오(6 KB 안에 들어갑니다).

동적 할당도 동작하며, 내부적으로 sbrk() 구현을 사용합니다. Claire Wolf의 picorv32에 예제가 여기 있습니다. 구현은 꽤 간단합니다. 유일하게 까다로운 부분은 brk를 코드 세그먼트의 끝으로 초기화하는 방법인데, 링커 스크립트 어딘가에서 링커가 정의하는 _end 심볼이 이 일을 해 줍니다. 그런데 C++ 표준 라이브러리는 매우 크고(1메가바이트 이상), 우리에게는 (현재로서는) 256킬로바이트밖에 없습니다. 하지만 iostream을 사용하지 않는다면 대신 libsupc++와 링크할 수 있습니다. 이 라이브러리에는 C++ 프로그램을 실행하는 데 필요한 최소한의 기능이 들어 있습니다. 링크할 때 -nostdlib 플래그도 넘겨야 합니다. 몇 가지 예제와 Makefile은 여기 있습니다. ULX3S에서는 256킬로바이트 한계에 금방 도달한다는 점에 유의하십시오. 더 많은 공간을 확보하려면 SDRAM 컨트롤러가 필요할 것입니다!