Up

GD32VF103 시작하기

2023년 3월 5일

GD32VF103 소개

GD32VF103는 STM32 호환 MCU를 만드는 것으로 유명한 GigaDevice에서 출시한 RISC-V 기반 MCU입니다. 이름에서 알 수 있듯이 이 역시 STM32F103 시리즈와 아키텍처를 제외하면 레지스터 등이 거의 호환됩니다. 설명에 따르면 핵심 코어부는 Nuclei에서 개발한 N205와 동일한 설계의 Bumblebee 코어를 사용합니다.

GD32VF103 개발 보드는 GigaDevice에서 제공하는 평가 보드(GD32VF103V-EVAL) 외에도 여럿 출시되어 있는데, 현재 제일 구하기 쉬운 것은 SiPEED의 Longan Nano입니다. 알리익스프레스 등지에서 저렴한 가격에 구할 수 있습니다. 이 보드는 정확히 GD32VF103CBT6을 사용하며, 본 글에서는 이를 기준으로 설명합니다.

툴체인 설치

GD32VF103 펌웨어를 개발하려면 RISC-V 툴체인을 설치해야 합니다. GD32VF103에 맞게 GNU 툴체인을 설치해봅시다. 참고로 빌드에 10 GiB 이상 필요하니 충분한 용량을 먼저 마련하셔야 합니다.

$ git clone --recursive https://github.com/riscv-collab/riscv-gnu-toolchain.git
$ cd riscv-gnu-toolchain
$ mkdir build
$ cd build
$ ../configure --with-arch=rv32imac --with-abi=ilp32 --enable-multilib --prefix=<install path>
$ make

PATHLD_LIBRARY_PATH 환경 변수는 설치 경로에 맞게 잘 설정해줍시다. 라이브러리들은 설치 경로의 /lib/gcc에 있습니다. 설치가 잘 되었다면 riscv32-unknown-elf-gcc 등의 명령어를 실행할 수 있을 겁니다.

추가로 펌웨어 업로드 및 디버깅을 위해 OpenOCD를 설치합니다. 기존 OpenOCD는 RISC-V를 지원하지 않기 때문에, RISC-V를 지원하는 OpenOCD를 별도로 설치해야 한다는 번거로움이 있습니다.

$ git clone https://github.com/riscv/riscv-openocd.git
$ cd riscv-openocd
$ ./bootstrap
$ ./configure --prefix=<install path> --program-prefix=<executable name prefix>
$ make
$ make install

--program-prefix는 기존 OpenOCD와 겹치지 않게 실행 파일 이름의 접두어를 설정하는데, 보통 riscv-로 둡니다. 이 경우 riscv-openocd라는 이름으로 실행 파일이 설치됩니다.

부트 코드

컴파일 가능한 환경을 구축했으므로 이제 아주 기초적인 RISC-V 부트 코드를 만들어보겠습니다. 먼저 코어가 리셋된 후 처음으로 실행하는 명령의 위치를 알아야겠죠. RISC-V의 PC(program counter) 초기값은 표준에서 따로 정의하지 않지만 GD32VF103의 경우 0입니다. 따라서 부트 코드의 진입점(entry point)은 0x0000_0000이 됩니다.

GD32VF103의 메모리 맵을 보면 기본적으로(BOOT0 핀을 건드리지 않았을 때) 0x0000_0000으로 시작하는 메모리 공간은 0x0800_0000에서 시작하는 플래시 메모리에 매핑되어 있습니다. 즉, 부트 코드를 0x0800_0000에 업로드하고 리셋하면 정상적으로 부트 코드를 실행시킬 수 있습니다. 다만 한 가지 문제가 있다면 링커는 PC가 0x0800_0000 이상인 것으로 판단하고 링크하는데, 실제 실행 중에 PC는 그보다 0x0800_0000만큼 낮은 주소를 가리키고 있습니다. 플래시 메모리 내에서야 메모리가 매핑되어 있어 문제가 생기지 않지만 그 외 영역에 접근하는 데 컴파일러가 PC에 대한 상대 주소로 접근하는 코드를 생성해버리면 잘못된 주소로 접근해버립니다.

결론적으로 부트 코드에서는 위와 같은 일이 일어나기 전에 PC를 실제 플래시 메모리 주소로 옮겨야 합니다. 다음 코드를 보시죠.

gd32vf103cbt6.S
.section .start, "ax", %progbits

_start:
    /* Jump to reset_handler. */
    lui a0, %hi(reset_handler)              // a0 = (&reset_handler) & 0xFFFFF000
    jalr zero, %lo(reset_handler)(a0)       // pc = a0 + (&reset_handler) & 0x00000FFF

.globl reset_handler
.type reset_handler, %function
reset_handler:
    li a0, 0xdeadbeef                       // a0 = 0xdeadbeef

    li a1, 0                                // a1 = 0
    inf_loop:                               // while (1) {
        addi a1, a1, 1                      //     a1 = a1 + 1
        j inf_loop                          // }

위 코드에서 함수 reset_handler는 코어가 리셋된 후 처음 실행되는 함수인데, 이를 주소 0x0800_0000에 두지 않고 대신 그 위치에는 reset_handler의 주소로 점프하는 코드를 넣었습니다. 이 코드는 정확히 주소를 지정해야 하므로 별도의 .start 섹션을 만들었습니다.

이제 링커 스크립트를 작성할 시간입니다.

gd32vf103cbt6.ld
OUTPUT_ARCH("riscv")

/* Entry point. */
start = 0x00000000;

MEMORY
{
    FLASH(rxai!w) : ORIGIN = 0x08000000, LENGTH = 128K
    RAM(wxa!ri) : ORIGIN = 0x20000000, LENGTH = 32K
}

SECTIONS
{
    /* Flash sections. */

    .start :
    {
        KEEP(*(.start))
    } >FLASH
}

먼저 아키텍처를 RISC-V, 진입점을 주소 0x0000_0000으로 설정하고 메모리를 정의합니다. GD32VF103CBT6은 플래시 메모리가 128 KiB, SRAM이 32 KiB입니다. 섹션은 플래시 메모리에 들어가야 하는 .start 하나 뿐이고, 따라서 .start0x0800_0000에 들어가게 될 겁니다.

컴파일을 해봅시다.

$ riscv32-unknown-elf-gcc -O0 -g3 -march=rv32imac -mabi=ilp32 -mcmodel=medlow -c gd32vf103cbt6.S -o gd32vf103cbt6.o
$ riscv32-unknown-elf-gcc -O0 -g3 -march=rv32imac -mabi=ilp32 -mcmodel=medlow -nostartfiles -T gd32vf103cbt6.ld gd32vf103cbt6.o -o hello_gd32vf103.elf

objdump로 확인하면 0x0800_0000_start가 배치되어있음을 확인할 수 있습니다.

$ riscv32-unknown-elf-objdump -d hello_riscv.elf

hello_riscv.elf:     file format elf32-littleriscv


Disassembly of section .start:

08000000 <_start>:
 8000000:       08000537                lui     a0,0x8000
 8000004:       00850067                jr      8(a0) # 8000008 <reset_handler>

08000008 <reset_handler>:
 8000008:       deadc537                lui     a0,0xdeadc
 800000c:       eef50513                add     a0,a0,-273 # deadbeef <inf_loop+0xd6adbedd>
 8000010:       4581                    li      a1,0

08000012 <inf_loop>:
 8000012:       0585                    add     a1,a1,1
 8000014:       bffd                    j       8000012 <inf_loop>

업로드 및 디버그

컴파일이 끝났으니 업로드를 할 시간입니다. GD32VF103CBT6은 JTAG 인터페이스를 제공하므로 JTAG을 지원하는 디버거가 필요합니다. 가장 많이 사용하는 JTAG 디버거는 FT2232 기반의 디버거들로, 역시 알리익스프레스 등에서 쉽게 구할 수 있습니다. 저는 RV-Debugger-Plus를 사용합니다. JTAG 선 4개와 전원선 2개를 잘 연결합시다.

보드와 디버거 연결

OpenOCD로 GDB 서버를 시작하기 위해선 대상 칩과 디버거 정보를 담은 OpenOCD 설정 파일이 필요합니다. 공식 펌웨어 라이브러리에서 제공하는 설정 파일이 있긴 하지만 구 버전 형식을 따르다보니 OpenOCD에서 수많은 경고를 띄우는 관계로 다음과 같이 재작성했습니다.

ft2232.cfg
adapter speed 1000

adapter driver ftdi
ftdi vid_pid 0x0403 0x6010

transport select jtag
ftdi layout_init 0x0008 0x001b
ftdi layout_signal nSRST -oe 0x0020 -data 0x0020

set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 5

set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME riscv -chain-position $_TARGETNAME
$_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size 10000 -work-area-backup 1

set _FLASHNAME $_CHIPNAME.flash
flash bank $_FLASHNAME stm32f1x 0x08000000 0 0 0 $_TARGETNAME

init

halt

이 파일로 OpenOCD를 실행합니다.

$ openocd -f ft2232.cfg
...
Info : starting gdb server for riscv.cpu on 3333
Info : Listening on port 3333 for gdb connections
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections

기본적으로 GDB 서버는 포트 3333으로 열립니다. GDB를 실행해 해당 포트로 접속합시다. GDB에서 원격 포트 접속은 target remote 명령으로 할 수 있습니다.

$ riscv32-unknown-elf-gdb -q hello_gd32vf103.elf
Reading symbols from hello_gd32vf103.elf...
(gdb) target remote :3333
Remote debugging using :3333

GDB가 GD32VF103CBT6에 연결되었습니다. load 명령으로 ELF 파일을 업로드합니다.

(gdb) load
Loading section .start, size 0x16 lma 0x8000000
Start address 0x00000000, load size 22
Transfer rate: 49 bytes/sec, 22 bytes/write.

이제 GDB로 디버깅을 시작하면 됩니다. 여기서 continue 명령으로 프로그램을 실행하면 reset_handler로 진입해 무한 루프에 빠지겠죠.

(gdb) continue
Continuing.
^C
Program received signal SIGINT, Interrupt.
inf_loop () at gd32vf103cbt6.S:15
15              addi a1, a1, 1                      //     a1 = a1 + 1

레지스터 중 a0a1의 값을 확인하여 펌웨어가 정상 작동하는지 살펴봅시다.

(gdb) info register
ra             0x0      0x0
sp             0x0      0x0
gp             0x0      0x0
tp             0x0      0x0
t0             0x0      0
t1             0x0      0
t2             0x0      0
fp             0x40022014       0x40022014
s1             0x80     128
a0             0xdeadbeef       -559038737
a1             0x23a534 2336052
a2             0x20000046       536870982
a3             0x8000016        134217750
a4             0x0      0
a5             0x0      0
a6             0x0      0
a7             0x0      0
s2             0x0      0
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x0      0
t4             0x0      0
t5             0x0      0
t6             0x0      0
pc             0x8000012        0x8000012 <inf_loop>

예상대로 a00xdeadbeef이고 a1은 증가하고 있습니다.

위 코드들은 모두 GitHub에서 확인하실 수 있습니다.

참고 문헌