[分享] 【Perf-V评测】E203 SOC上程序的构建与CoreMark移植

cruelfox   2021-7-8 09:29 楼主

  在上一篇帖子中,我使用 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 ...)。
abiname.PNG   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上,从电脑上收到了来自板子的字符。
testuart.PNG

回复评论 (2)

  移植 CoreMark 除了要补充输出函数(比如使用UART输出)外,还需要一个定时器,以获取纯计算部分消耗的时钟周期数。
  我查阅了 SiFive-E300 手册,没有找到像STM32中那样类似的通用寄存器。有WDT,但是不合适此处用。但是 SIRV-E200-SOC 介绍中说有 CLINT: 主要实现 RISC-V 架构手册中规定的标准计时器(Timer)和软件中断功能。
  在 SiFive-E3-Coreplex-v1.2.pdf 中稍有介绍:

timer.PNG clint.PNG

  在头文件 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 如下:
counter.PNG   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 编辑
点赞  2021-7-8 09:29

看楼主的介绍,的确是不能像 STM32 那样用 reset init 从最前面开始调试

点赞  2021-7-8 11:31
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复