在上一篇帖子中,我使用 GCC 编译了一段操作 E203 SOC 的 GPIO 的代码,然后用 openocd 调试工具将二进制直接写入 E203 的 ITCM 内存里面去,再修改 PC 寄存器使 CPU 从指定的函数入口地址开始运行。这样只是调试模式下运行了一个程序片段,还没有搭建一个完整的程序,让 E203 从启动后自己找到程序入口地址,开始运行。
已经很熟悉的 ARM Cortex-M? 系列 CPU, 复位后是从中断向量表的第一项和第二项获得SP、PC的初值,也就是自动装入 Reset 向量地址,开始执行。但 E203 不是这样的,它是从硬件配置的一个固定的地址开始运行。在我综合过的代码中,地址有两个选项:一个是片内ROM(然后它会跳转到 ITCM 的开头地址),另一个是外部 QSPI flash 映射的地址。
那么我可以把 ITCM 的首地址 0x80000000 作为整个程序二进制代码的入口地址了。下面要搭建一个完整的程序,按照 MCU 开发的思路,还需要一个启动文件(提供初始化代码,跳转到main函数),和一个给链接器的脚本。现在从零开始,这两个文件怎么获得?
蜂鸟E203的git上面有一个 sirv-e-sdk 目录,其中的 bsp 子目录下有一些文件,可以参考:
bsp
├─drivers
│ ├─fe300prci
│ │ fe300prci_driver.c
│ │ fe300prci_driver.h
│ │
│ └─plic
│ plic_driver.c
│ plic_driver.h
│
├─env
│ │ common.mk
│ │ coreplexip-arty.h
│ │ encoding.h
│ │ entry.S
│ │ hifive1.h
│ │ sirv_printf.c
│ │ start.S
│ │
│ ├─sirv-e201-arty
│ │ init.c
│ │ link.lds
│ │ openocd.cfg
│ │ platform.h
│ │ settings.mk
│ ...
│
├─include
│ └─sifive
│ │ bits.h
│ │ const.h
│ │ sections.h
│ │ smp.h
│ │
│ └─devices
│ aon.h
│ clint.h
│ gpio.h
│ i2c.h
│ otp.h
│ plic.h
│ prci.h
│ pwm.h
│ spi.h
│ uart.h
│
├─libwrap
│ │ libwrap.mk
│ │
│ ├─misc
│ │ write_hex.c
│ │
│ ├─stdlib
│ │ malloc.c
│ │
│ └─sys
│ _exit.c
│ close.c
│ execve.c
│ fork.c
│ fstat.c
│ getpid.c
│ isatty.c
│ kill.c
│ link.c
│ lseek.c
│ open.c
│ openat.c
│ read.c
│ sbrk.c
│ stat.c
│ stub.h
│ times.c
│ unlink.c
│ wait.c
│ write.c
│
└─tools
openocd_upload.sh
注意到 env 目录下有两个汇编文件:start.S 和 entry.S, 其中 start.S 看内容像启动文件了:
.section .init
.globl _start
.type _start,[url=home.php?mod=space&uid=665173]@FUNCTION[/url] _start:
.option push
.option norelax
la gp, __global_pointer$
.option pop
la sp, _sp
/* Bob: Load code section from flash to ITCM */
la a0, _itcm_lma
la a1, _itcm
beq a0, a1, 2f /*If the ITCM phy-address same as the logic-address, then quit*/
la a2, _eitcm
bgeu a1, a2, 2f
1:
lw t0, (a0)
sw t0, (a1)
addi a0, a0, 4
addi a1, a1, 4
bltu a1, a2, 1b
2:
/* Load data section */
la a0, _data_lma
la a1, _data
la a2, _edata
bgeu a1, a2, 2f
1:
lw t0, (a0)
sw t0, (a1)
addi a0, a0, 4
addi a1, a1, 4
bltu a1, a2, 1b
2:
/* Clear bss section */
la a0, __bss_start
la a1, _end
bgeu a0, a1, 2f
1:
sw zero, (a0)
addi a0, a0, 4
bltu a0, a1, 1b
2:
/* Call global constructors */
la a0, __libc_fini_array
call atexit
call __libc_init_array
/* Enable FPU */
li t0, MSTATUS_FS
csrs mstatus, t0
csrr t1, mstatus
and t1, t1, t0
beqz t1, 1f
/*
fssr x0
*/
1:
/* argc = argv = 0 */
li a0, 0
li a1, 0
call main
tail exit
1:
j 1b
这里面引用的 _itcm_lma, _data_lma, __bss_start 等地址是在别处定义,应该是配合链接脚本使用。再留意看,env 目录的子目录里还能找到 link.lds 文件,多半就是链接脚本文件了。打开 sirv-e201-arty/link.lds 得到确认。其中对内存的描述是这样:
MEMORY
{
flash (rxai!w) : ORIGIN = 0x20000000, LENGTH = 8M
itcm (rxai!w) : ORIGIN = 0x80000000, LENGTH = 64K
ram (wxa!ri) : ORIGIN = 0x90000000, LENGTH = 64K
}
现在试写一个空的 main() 函数,编译链接一下试试。事先将 start.S 里面对 atexit, exit 等C标准库函数的调用去掉。
riscv-nuclei-elf-gcc -c -Wall -march=rv32imc -mabi=ilp32 -Os main.c
riscv-nuclei-elf-gcc -c -Wall -march=rv32imc -mabi=ilp32 ../bsp/env/start.S
riscv-nuclei-elf-gcc -nostdlib -march=rv32imc -mabi=ilp32 --specs=nano.specs main.o start.o -o test.elf -T ../bsp/env/sirv-e201-arty/link.lds
这样就生成了一个完整的程序。用 nm 看一下里面使用的地址:
>riscv-nuclei-elf-nm test.elf | sort
00000800 A __stack_size
20000000 T _start
200000ac B _itcm_lma
200000b0 ? _data_lma
80000000 B _itcm
80000000 T main
80000004 T _eitcm
90000000 ? _data
90000000 B __bss_start
90000000 B _edata
90000000 B _end
90000800 D __global_pointer$
90010000 B _sp
0x80000000 是 ITCM, 0x90000000 是 DTCM, 这两处都是 RAM 地址。然而出现了 0x20000000 地址,_start 放在那里。看来这个程序是从 flash 地址启动的。反汇编看一下:
Disassembly of section .init:
20000000 <_start>:
20000000: 70001197 auipc gp,0x70001
20000004: 80018193 addi gp,gp,-2048 # 90000800 <__global_pointer$>
20000008: 70010117 auipc sp,0x70010
2000000c: ff810113 addi sp,sp,-8 # 90010000 <_sp>
20000010: 00000517 auipc a0,0x0
20000014: 09c50513 addi a0,a0,156 # 200000ac <_itcm_lma>
20000018: 60000597 auipc a1,0x60000
2000001c: fe858593 addi a1,a1,-24 # 80000000 <_itcm>
20000020: 02b50063 beq a0,a1,20000040 <_start+0x40>
20000024: 60000617 auipc a2,0x60000
20000028: fe060613 addi a2,a2,-32 # 80000004 <_eitcm>
2000002c: 00c5fa63 bgeu a1,a2,20000040 <_start+0x40>
20000030: 00052283 lw t0,0(a0)
20000034: 0055a023 sw t0,0(a1)
20000038: 0511 addi a0,a0,4
2000003a: 0591 addi a1,a1,4
2000003c: fec5eae3 bltu a1,a2,20000030 <_start+0x30>
20000040: 00000517 auipc a0,0x0
20000044: 07050513 addi a0,a0,112 # 200000b0 <_data_lma>
20000048: 70000597 auipc a1,0x70000
2000004c: fb858593 addi a1,a1,-72 # 90000000 <_data>
20000050: 70000617 auipc a2,0x70000
20000054: fb060613 addi a2,a2,-80 # 90000000 <_data>
20000058: 00c5fa63 bgeu a1,a2,2000006c <_start+0x6c>
2000005c: 00052283 lw t0,0(a0)
20000060: 0055a023 sw t0,0(a1)
20000064: 0511 addi a0,a0,4
20000066: 0591 addi a1,a1,4
20000068: fec5eae3 bltu a1,a2,2000005c <_start+0x5c>
2000006c: 70000517 auipc a0,0x70000
20000070: f9450513 addi a0,a0,-108 # 90000000 <_data>
20000074: 70000597 auipc a1,0x70000
20000078: f8c58593 addi a1,a1,-116 # 90000000 <_data>
2000007c: 00b57763 bgeu a0,a1,2000008a <_start+0x8a>
20000080: 00052023 sw zero,0(a0)
20000084: 0511 addi a0,a0,4
20000086: feb56de3 bltu a0,a1,20000080 <_start+0x80>
2000008a: 6299 lui t0,0x6
2000008c: 3002a073 csrs mstatus,t0
20000090: 30002373 csrr t1,mstatus
20000094: 00537333 and t1,t1,t0
20000098: 00030263 beqz t1,2000009c <_start+0x9c>
2000009c: 4501 li a0,0
2000009e: 4581 li a1,0
200000a0: 60000097 auipc ra,0x60000
200000a4: f60080e7 jalr -160(ra) # 80000000 <_itcm>
200000a8: a001 j 200000a8 <_start+0xa8>
Disassembly of section .text:
80000000 <main>:
80000000: 4501 li a0,0
80000002: 8082 ret
这段代码涉及的 RISC-V 指令需要查阅资料熟悉一下。有的指令像运算用的,含义可以猜出来。
RISC-V RV32 架构有32个通用寄存器:x0~x31, 但是上面反汇编代码中用的是别名 (a0, a1, t0, gp ...)。
auipc 指令在 ARM 指令集里面没有对应,它的效果是把当前 PC 寄存器值和一个立即数向加,结果存入指定寄存器。立即数的高20位从指令中译码出来,低12位全0. 在 _start 入口的代码开头,有几处是 auipc 指令后跟一条 addi 指令对结果进行调整,综合效果是给一个寄存器赋值。看 start.S 汇编文件里面实际写的是一条 la 指令——这是一条伪指令。
gp, sp 寄存器先被用常量初始化了。
a0, a1, a2 寄存器分别初始化为 _itcm_lma , _itcm 及 _eitcm 三个地址,然后用了一个循环将 _itcm_lma 开始的内存复制到 _itcm 开始的地方去(通过 lw, sw 指令,即 load/store word 之意)。当到达 _eitcm 地址时结束。
bgeu, bltu 都是条件转移指令,分别表示 "greater than or equal to", "less than",后面的 u 表示无符号数比较。与 ARM 指令集有区别的是,RISC-V 的条件转移将比较运算和分支转移合并在一条指令中。
后面对 _data 数据的初始化也是同样的操作,增加了一个对 _bss 数据段清零的过程。
csrs, csrr 这两种指令是 RISC-V 的特点。CSR 意思是 "Control and Status Register", 即控制和状态寄存器。RISC-V的指令编码空间留出了4096个CSR的位置——太多了。
jalr 指令,是"jump and link register", 相当于 ARM 的 BLX 指令的增强方式:可带偏移量,还可以指定link的寄存器。
在 start.S 源文件中,只写了一条 "call main",翻译成了一条 auipc 指令跟一条 jalr 指令。如果直接写 "jal main" 如何呢?我试了,因为转移地址范围太大,链接失败。因为 _start 是 flash 代码,main 在 ITCM 中(已经复制过去了),这个跳转得用间接方式。
程序要有功能,需要使用片上设备跟外部环境打交道。E203 SOC 配置了一些设备比如 UART, SPI, PWM 等,可以从文档中找到寄存器的描述。在 sirv-e-sdk/bsp/include/sifive/devices 目录下有一些头文件,对操作寄存器提供了一点帮助。在别处还能找到 platform.h . 但是这些文件定义的东西只是聊胜于无,和常规MCU SDK里面的设备寄存器定义的完善程度差远了。
操作 UART 的寄存器,是类似这样的写法:
while (UART0_REG(UART_REG_TXFIFO) & 0x80000000) ;
UART0_REG(UART_REG_TXFIFO) = current[jj];
因为缺少寄存器位的宏,只能直接写数值,要看懂含义就得要注释。
为了使用 UART 输出字符,除了配置 UART (波特率等)外,还需要在I/O口配置中将 UART 的功能复用开启。否则,I/O口默认功能是GPIO.
在 e203_subsys_perips.v 中有如下代码:
assign uart_pins_0_io_pins_rxd_i_ival = gpio_iof_0_16_i_ival;
assign gpio_iof_0_16_o_oval = uart_pins_0_io_pins_rxd_o_oval;
assign gpio_iof_0_16_o_oe = uart_pins_0_io_pins_rxd_o_oe;
assign gpio_iof_0_16_o_ie = uart_pins_0_io_pins_rxd_o_ie;
assign gpio_iof_0_16_o_valid = 1'h1;
assign uart_pins_0_io_pins_txd_i_ival = gpio_iof_0_17_i_ival;
assign gpio_iof_0_17_o_oval = uart_pins_0_io_pins_txd_o_oval;
assign gpio_iof_0_17_o_oe = uart_pins_0_io_pins_txd_o_oe;
assign gpio_iof_0_17_o_ie = uart_pins_0_io_pins_txd_o_ie;
assign gpio_iof_0_17_o_valid = 1'h1;
这说明了 GPIO16,17 的 I/O function 是 UART0 的 RXD/TXD. 于是,需要将 GPIO IOF_EN 寄存器的16和17位设置成1. 至于 IOF_SEL 寄存器,是选择两个复用功能中的哪一个,暂且不管,猜 UART0 功能是默认的。
于是 HelloWorld 程序就可以写出来了:
#include<platform.h>
int main()
{
const char str[]="RISC-V E203 SOC Running\r\n";
UART0_REG(UART_REG_TXCTRL)=1; // txen
UART0_REG(UART_REG_DIV)=138; // 16000000/115200 = 139
GPIO_REG(GPIO_IOF_EN) = 1<<16|1<<17;
for(;;)
{
int i;
for(i=0;i<sizeof(str);i++)
{
while (UART0_REG(UART_REG_TXFIFO) & 0x80000000)
{}
UART0_REG(UART_REG_TXFIFO) = str[i];
}
}
}
编译之后将二进制代码写到 ITCM 里面,然后进行硬件复位。E203自动从ROM开始执行,跳转到ITCM我的代码执行。虽然 OpenOCD 可以调试,但不能像 STM32 那样用 reset init 从最前面开始调试。若要跟踪完整执行过程,对这小程序可以使用 RTL 仿真的办法,然后看仿真生成的波形文件。
UART TXD (GPIO17)连到一个作为串口的FT232R上,从电脑上收到了来自板子的字符。
移植 CoreMark 除了要补充输出函数(比如使用UART输出)外,还需要一个定时器,以获取纯计算部分消耗的时钟周期数。
我查阅了 SiFive-E300 手册,没有找到像STM32中那样类似的通用寄存器。有WDT,但是不合适此处用。但是 SIRV-E200-SOC 介绍中说有 CLINT: 主要实现 RISC-V 架构手册中规定的标准计时器(Timer)和软件中断功能。
在 SiFive-E3-Coreplex-v1.2.pdf 中稍有介绍:
在头文件 clint.h 中定义如下:
#define CLINT_MSIP 0x0000
#define CLINT_MSIP_size 0x4
#define CLINT_MTIMECMP 0x4000
#define CLINT_MTIMECMP_size 0x8
#define CLINT_MTIME 0xBFF8
#define CLINT_MTIME_size 0x8
CLINT 的 MTIME 寄存器就是64-bit real-time couonter 的值。在 OpenOCD 调试 halt 状态下访问此处地址,可以看到内容在变化。
> mdw 0x0200BFF8 2
0x0200bff8: 072ae195 00000000
> mdw 0x0200BFF8 2
0x0200bff8: 072bac11 00000000
但是经过掐表一算,这个计时器的频率应是源于 32768Hz 时钟。所以不太适合 CoreMark 的用途。
在 platform.h 末尾发现 get_timer_value, get_instret_value, get_cycle_value 三个函数声明。原来 RISC-V 已经包含了 performance counter, 对应的 CSR 如下:
sdk 中 get_cycle_value() 是用 read_csr(mcycle) 和 read_csr(mcycleh) 实现的,可获取CPU时钟周期数。对于 CoreMark, 32位计时已经够用,所以在 core_portme.c 里面写获取时钟的函数为:
CORETIMETYPE barebones_clock() {
return read_csr(mcycle);
}
至于为什么 CSR 用 mcycle, 而不是 cycle 我不解。写 read_csr(cycle) 结果计数一直是0.
CoreMark 运行结果:
CoreMark run on Perf-V (E203 SOC), ported by cruelfox.
2K performance run parameters for coremark.
CoreMark Size : 666
Total ticks : 509895664
Total time (secs): 31.868479
Iterations/Sec : 31.378969
Iterations : 1000
Compiler version : GCC9.2.0
Compiler flags : -O3
Memory location : STACK
seedcrc : 0xe9f5
[0]crclist : 0xe714
[0]crcmatrix : 0x1fd7
[0]crcstate : 0x8e3a
[0]crcfinal : 0xd340
Correct operation validated. See readme.txt for run and reporting rules.
CoreMark 1.0 : 31.378969 / GCC9.2.0 -O3 / STACK
附上编译用的 Makefile:
default: test.hex
CC =riscv-nuclei-elf-gcc -c
LD =riscv-nuclei-elf-gcc -nostdlib
CFLAGS =-Wall -O2 -march=rv32imc -mabi=ilp32
INC =-I../bsp/env -I../bsp/include -I../bsp/include/sifive/devices
LDFLAGS=-march=rv32imc -mabi=ilp32 --specs=nano.specs
COREMARK = core_main.o core_matrix.o core_state.o core_list_join.o \
core_util.o core_portme.o ee_printf.o cvt.o
test.elf : start_itcm.o $(COREMARK) uart_char.o
$(LD) $(LDFLAGS) $^ -o $@ -lc -T ../itcmload.ld -lgcc
%.o : ../bsp/env/%.S
$(CC) $(CFLAGS) $<
%.o : %.c core_portme.h
$(CC) $(CFLAGS) -O2 $(INC) $<
core_matrix.o : core_matrix.c core_portme.h
$(CC) $(CFLAGS) -O3 $(INC) -finline-functions $<
core_list_join.o : core_list_join.c core_portme.h
$(CC) $(CFLAGS) -O3 $(INC) $<
core_util.o : core_util.c core_portme.h
$(CC) $(CFLAGS) -O3 $(INC) $<
core_state.o : core_state.c core_portme.h
$(CC) $(CFLAGS) -O3 $(INC) $<
core_main.o : core_main.c core_portme.h
$(CC) $(CFLAGS) -O2 $(INC) $< -DFLAGS_STR='"-O3"'
%.hex : %.elf
riscv-nuclei-elf-objcopy -Oihex $< $@
本帖最后由 cruelfox 于 2021-7-8 09:33 编辑