修订历史
版本号 | 修订日期 | 修订的章节 | 修订的内容 |
---|---|---|---|
1.0 | 2019/11/21 | N/A | 初始版本 |
2.0 | 2021/09/01 | 1、3 | 补充Nuclei N200及以上系列配套SDK的说明 修改Demo_irqc相关说明 |
1. n100-sdk下载地址
为了让用户能够快速的熟悉使用N100系列处理器内核开发软件,N100系列内核结合配套的SoC原型平台,开发了一套与之配套的软件开发平台(Software Development Kit),称之为n100-sdk平台。
为了便于用户随时跟踪状态和方便使用,n100-sdk的所有源代码均开源托管于GitHub网站上,网址为https://github.com/riscv-mcu/n100-sdk,本文将以“n100-sdk项目”代指其GitHub上的具体网址。
注意:
-
本SDK是基于配套的样例SoC的一个参考SDK,用户在集成N100处理器内核至其自有的SoC中之后,可能需要对SDK进行适当的修改以适配其SoC。
-
本文档涉及到Linux操作和Makefile的基本知识,本文档将不做过多介绍。
-
Nuclei的N200及以上型号(如N200、N300、N600、N900等)配套的SDK为Nuclei SDK,可通过芯来科技官方网站“文档与工具”进行了解。
2. 基于n100-sdk的软件开发与运行
2.1. n100-sdk简介
n100-sdk并不是一个软件,它本质上是由一些Makefile、板级支持包(Board Support Package,BSP)、脚本和软件示例组成的一套开发环境。n100-sdk基于Linux平台,使用标准的RISC-V GNU工具链对程序进行编译,使用OpenOCD+GDB将程序下载到硬件平台中并进行调试。n100-sdk主要包含如下两个方面的内容。
-
板级支持包(Board Support Package,BSP)。
-
若干软件示例。
2.2. n100-sdk代码结构
n100-sdk平台的代码结构如下:
n100-sdk // 存放n100-sdk的目录
|----bsp // 存放板级支持包(Board Support Package)的目录
|----nuclei-n100 // 存放Nuclei N100处理器Core相关的BSP文件
|----env //存放一些基本的支持性文件
|----drivers //存放处理器Core相关的函数和驱动文件
|----func.c //处理器Core的常用函数
|----func.h //处理器Core的头文件
|----timer.h //TIMER单元相关的头文件
|----irqc.h //IRQC单元相关的头文件
|----riscv_encoding.h //RISC-V架构相关的编码信息
|----stubs //存放移植newlib的底层桩函数
|----soc //存放配套SoC相关的BSP文件,用户可以替代成自己SoC的驱动
|----drivers //存放配套SoC相关的函数和驱动文件
|----soc_func.c //SoC的常用函数
|----soc_func.h //SoC的头文件
|----software //存放示例程序的源代码
|----hello_world //hello_world示例程序,见第2.4.2节
|----demo_iasm //内嵌汇编示例程序,见第3.3节
|----dhrystone //Dhrystone跑分程序,见第3.1节
|----coremark //CoreMark跑分程序,见第3.2节
|----demo_irqc //Demo_irqc示例程序,见第3.4节
|----work //存放工具链的目录
|----Makefile //主Makefile文件
各个主要的目录简述如下:
-
software目录主要用于存放软件示例,包括基本的hello_world示例程序、demo_iasm示例程序、Dhrystone跑分程序和CoreMark跑分程序程序。每个示例均有单独的文件夹,包含了各自的源代码、Makefile和编译选项(在Makefile中指定)等。
-
bsp/core/drivers目录主要用于存放N100系列处理器内核的驱动程序代码,譬如IRQC单元的底层驱动函数和代码。
-
bsp/soc/drivers目录主要用于存放配套SoC的驱动程序代码,譬如系统外设UART的底层驱动函数和代码。该文件夹可以被替代成为用户SoC自己的驱动。
-
bsp/core/stubs目录主要用于存放一些移植Newlib所需的底层桩函数的具体实现。见第2.3.1节,了解Newlib移植桩函数的更多信息。
-
bsp/core/env目录主要用于存放一些基本的支持性文件,简述如下。
-
common.mk:调用GCC进行编译的Makefile脚本,也会指定编译相关的选项。
-
*.lds:程序编译的链接脚本,见第2.3.3节了解其详情。
-
start.S:Core的上电启动引导程序,见2.3.4节了解其详情。
-
init.c:Core的上电初始化函数,见2.3.4节了解其详情。
-
entry.S:Core的异常和中断入口函数,见2.3.5节了解其详情。
-
handlers.c:Core的中断、异常处理函数,见2.3.5节了解其详情。
-
*.cfg:OpenOCD的配置文件。
2.3. n100-sdk板级支持包解析
嵌入式平台通常会提供板级支持包(Board Support Package,BSP),使得应用开发人员无需关注底层的细节。n100-sdk平台的板级支持包均存在于BSP目录下,下文将介绍该BSP如何解决嵌入式开发的几个基本问题。
注意:对于不想关注底层细节的应用开发人员可以略过此节,请直接参见第2.4节了解如何使用n100-sdk进行程序的开发。
2.3.1. 移植Newlib桩函数
Newlib是嵌入式系统常用的C运行库。Newlib的所有库函数都建立在20个桩函数的基础上,这20个桩函数完成具体操作系统和底层硬件相关的功能。
注意:不同的桩函数可能会被不同的C库函数所调用,所以如果嵌入式程序中使用到的C库函数不多的时候,便并不需要实现所有的20个桩函数。
n100-sdk平台在板级支持包中完成了Newlib桩函数的实现。具体体现在bsp/core/stubs目录下实现了如下几个桩函数:
-
close.c:实现了_close函数。
-
_exit.c:实现了_exit函数。
-
fstat.c:实现了_fstat函数。
-
lseek.c:实现了_lseek函数。
-
read.c:实现了_read函数。
-
sbrk.c:实现了_sbrk函数。
-
write.c:实现了_write函数。
注意:上述有的函数的函数体实现为空,因为在嵌入式程序中这些函数所支持的功能基本使用不到(譬如文件操作)。
上述的函数名称都是以下划线开始(譬如_write),与原始的Newlib定义的桩函数名称(譬如write)不一致。这是因为,在Newlib的底层桩函数中存在着多层嵌套,wirte函数会调用名为write_r的可重入函数,然后write_r函数调用了最终的_write函数。
上述实现的桩函数将会在bsp/core/env/common.mk脚本中作为普通源文件加入被编译的文件列表,common.mk代码片段如下:
// bsp/core/env/common.mk脚本片段
//将桩函数加入源文件列表
C_SRCS += $(STUB_DIR)/_exit.c
C_SRCS += $(STUB_DIR)/write_hex.c
C_SRCS += $(STUB_DIR)/fstat.c
C_SRCS += $(STUB_DIR)/isatty.c
C_SRCS += $(STUB_DIR)/lseek.c
C_SRCS += $(STUB_DIR)/read.c
C_SRCS += $(STUB_DIR)/sbrk.c
C_SRCS += $(STUB_DIR)/write.c
……
C_OBJS := $(C_SRCS:.c=.o)
……
//调用GCC对源文件进行编译
$(C_OBJS): %.o: %.c $(HEADERS)
$(CC) $(CFLAGS) $(INCLUDES) -include sys/cdefs.h -c -o $@ $<
综上,n100-sdk平台通过实现桩函数的函数体并且将其与其他普通源文件进行一并编译,便实现了Newlib的移植和支持。由于这些桩函数作为源文件一起进行了编译,所以在链接阶段,链接器在链接Newlib的C库函数的时候能够找到这些桩函数一并进行链接(否则便会报错称找不到桩函数的实现)。
2.3.2. 支持printf函数
printf函数在嵌入式早期开发阶段对于分析程序行为非常有帮助,因此对printf的支持必不可少。嵌入式平台中通常需要将printf的输出重定位到UART接口传输至主机PC的显示器上。
由于printf函数属于典型的C标准函数,因此会调用Newlib C运行库中的库函数,而在Newlib的printf库函数中,最终将字符逐个的输出是依靠的底层桩函数write函数。因此,对于printf函数的移植归根结底在于对于Newlib桩函数write函数的实现。
n100-sdk平台在BSP中完成了write桩函数的实现。如本章第2.3.1节中所述,write函数最终调用_write函数,而该函数被实现在bsp/core/stubs/write.c文件中,其代码片段如下:
……
//write.c函数片段
ssize_t _write(int fd, const void* ptr, size_t len)
{
const uint8_t * current = (const char *)ptr;
if (isatty(fd)) {
for (size_t jj = 0; jj < len; jj++) {
while (UART0_REG(UART_REG_TXFIFO) & 0x80000000) ;//等待UART的TXFIFO有空
//向UART的TXFIFO寄存器写入字符,从而使得字符通过UART输出
UART0_REG(UART_REG_TXFIFO) = current[jj];
if (current[jj] == '\n') {
while (UART0_REG(UART_REG_TXFIFO) & 0x80000000) ;
UART0_REG(UART_REG_TXFIFO) = '\r';
}
}
return len;
}
return _stub(EBADF);
}
从_write函数体中可以看出,该函数通过向SoC的UART0的TXFIFO写入字符,最终将输出字符重定向至UART将其输出,最终能够显示在主机PC的显示屏幕上(借助主机PC的串口调试助手软件)。
综上,n100-sdk平台通过实现桩函数_write便实现了printf的移植。
2.3.3. 提供系统链接脚本
嵌入式系统中需要关注“链接脚本”为程序分配合适的存储器空间,譬如程序段放在什么区间、数据段放在什么区间等等。有关GCC的“链接脚本(Link Scripts)”的语法和说明请用户自行查阅其他资料学习。
n100-sdk平台提供二个不同的“链接脚本”,在编译时,可以通过Makefile的命令行指定不同的“链接脚本”作为GCC的链接脚本,从而实现不同的运行方式。二个不同的“链接脚本”分别在后文介绍。
- 程序存放在ILM中且从ILM中直接执行
使用链接脚本bsp/core/env/link_ilm.lds,可以将程序存放在ILM中并且直接从ILM中进行执行。该链接脚本代码片段及解释如下:
//bsp/core/env/link_ilm.lds代码片段
ENTRY( _start ) //指明程序入口为_start标签
MEMORY
{
//定义了两块地址区间,分别名为ilm和ram,对应ILM和DLM的地址区间
ilm (rxai!w) : ORIGIN = 0x80000, LENGTH = 64K
ram (wxa!ri) : ORIGIN = 0x90000, LENGTH = 64K
}
SECTIONS
{
__stack_size = DEFINED(__stack_size) ? __stack_size : 2K;
.init :
{
*(.vtable) //中断向量表
KEEP (*(SORT_NONE(.init)))
} >ilm AT>ilm
.ilalign :
{
. = ALIGN(4);
PROVIDE( _ilm_lma = . );//创建一个标签名为_ilm_lma,地址为ilm地址区间的起始地址
} >ilm AT>ilm
.ialign :
{
PROVIDE( _ilm = . ); //创建一个标签名为_ilm,地址也为ilm地址区间的起始地址
} >ilm AT>ilm
.text :
{
*(.text.unlikely .text.unlikely.*)
*(.text.startup .text.startup.*)
*(.text .text.*)
*(.gnu.linkonce.t.*)
} >ilm AT>ilm
//注意:由于此“链接脚本”意图是让程序存储在ILM之中,且直接从ILM中进行运行,所以其物理
//地址和运行地址相同,所以,上述.text代码段的物理地址是ilm区间,而运行地址也为ilm区间
.data :
{
*(.rdata)
*(.rodata .rodata.*)
*(.gnu.linkonce.r.*)
*(.data .data.*)
*(.gnu.linkonce.d.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 );//创建一个标签名为__global_pointer$
*(.sdata .sdata.* .sdata*)
*(.gnu.linkonce.s.*)
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
} >ram AT>ilm
//注意:由于此“链接脚本”意图是让数据存储在ILM之中,而将数据段上载至DLM中进行运行,所以数据段物理地址和运行地址不同,所以,上述.data数据段的物理地址是ilm区间,而运行地址为ram区间
- 程序存放在Flash中但是上电后上载至ILM中进行执行
使用链接脚本bsp/core/env/link_flash.lds,可以将程序存放在Flash中但是上电后上载至ILM中进行执行。该链接脚本代码片段及解释如下:
//bsp/core/env/link_flash.lds代码片段
ENTRY( _start ) //指明程序入口为_start标签
MEMORY
{
//定义了三块地址区间,分别名为flash,ilm和ram,对应Flash,ILM和DLM的地址区间
flash (rxai!w) : ORIGIN = 0x00020000, LENGTH = 4M
ilm (rxai!w) : ORIGIN = 0x00080000, LENGTH = 64K
ram (wxa!ri) : ORIGIN = 0x00090000, LENGTH = 64K
}
SECTIONS
{
__stack_size = DEFINED(__stack_size) ? __stack_size : 2K;
.init :
{
KEEP (*(SORT_NONE(.init)))
} >flash AT>flash
//注意:上述语法中AT前的一个flash表示该段的运行地址,AT后的flash表示该段的物理地址。有关此语法的详细细节请用户自行搜索学习GCC Link脚本语法。
//物理地址是该程序要被存储在的存储器地址(调试器下载程序之时会遵从此物理地址进行下载),运行地址却是指程序真正运行起来后所处于的地址,所以程序中的相对寻址都会遵从此运行地址。
//注意:上述.init段为上电引导程序所处的段,所以它直接在Flash里面执行,所以其运行地址和物理地址相同,都是flash区间。
.ilalign :
{
. = ALIGN(4);
PROVIDE( _ilm_lma = . ); //创建一个标签名为_ilm_lma,地址为flash地址区间的起始地址
} >flash AT>flash
.ialign :
{
PROVIDE( _ilm = . ); //创建一个标签名为_ilm,地址为ilm地址区间的起始地址
} >ilm AT>flash
.text :
{
*(.vtable_ilm) //中断向量表(ILM)
*(.text.unlikely .text.unlikely.*)
*(.text.startup .text.startup.*)
*(.text .text.*)
*(.gnu.linkonce.t.*)
} >ilm AT>flash
//注意:由于此“链接脚本”意图是让程序存储在Flash之中,而上载至ILM中进行运行,所以其物理
//地址和运行地址不同,所以,上述.text代码段的物理地址是flash区间,而运行地址为ilm区间
.data :
{
*(.rdata)
*(.rodata .rodata.*)
*(.gnu.linkonce.r.*)
*(.data .data.*)
*(.gnu.linkonce.d.*)
. = ALIGN(8);
PROVIDE( __global_pointer$ = . + 0x800 ); //创建一个标签名为__global_pointer$
*(.sdata .sdata.* .sdata*)
*(.gnu.linkonce.s.* )
. = ALIGN(8);
*(.srodata.cst16)
*(.srodata.cst8)
*(.srodata.cst4)
*(.srodata.cst2)
*(.srodata .srodata.*)
} >ram AT>flash
//注意:由于此“链接脚本”意图是让数据存储在Flash之中,而将数据段上载至DLM中进行运行,所以数据段物理地址和运行地址不同,所以,上述.data数据段的物理地址是flash区间,而运行地址为ram区间
2.3.4. 系统启动引导程序
嵌入式系统上电后执行的第一段软件代码是引导程序,该程序往往由用汇编语言编写。n100-sdk平台的引导程序为bsp/core/env/start.S,该程序由汇编语言编写。
start.S代码中主要完成一些基本配置,如果有需要还会将代码从Flash上载至ILM中(即将Flash中的代码搬运到ILM中)。
- start.S代码解读
start.S代码片段和功能解释如下:
// start.S文件代码片段
.section .init //声明此处的section名为.init
.globl _start //指明标签_start的属性为全局性的
.type _start,@function
_start: //标签名_start处于此处
//下列代码通过将CSR寄存器MSTATUS的FS域设置为非零值,从而将FPU打开使能。
.option push
.option norelax
//设置全局指针
la gp, __global_pointer$ //将标签__global_pointer$所处的地址赋值给gp寄存器
//注意:标签__global_pointer在链接脚本中定义参见链接脚本的__global_pointer$标签
.option pop
//设置堆栈指针
la sp, _sp //将标签_sp所处的地址赋值给sp寄存器
//注意:标签_sp在链接脚本中定义,参见链接脚本的_sp标签
//下列代码判断_ilm_lma与_ilm标签的地址值是否相同:
// 如果相同则意味着代码直接从Flash中进行执行(link_flashxip.lds中定义的_ilm_lma与_ilm标签地址相等),那么直接跳转到后面数字标签2所在的代码继续执行;
//如果不相同则意味着代码需要从Flash中上载至ILM中进行执行(link_flash.lds中定义的_ilm_lma与_ilm标签地址不相等),因此使用lw指令逐条地将指令从Flash中读取出来,然后使用sw指令逐条地写入ILM中,通过此方式完成指令的上载至ILM中。
la a0, _ilm_lma//将标签_ilm_lma所处的地址赋值给a0寄存器
//注意:标签_ilm_lma在链接脚本中定义,参见链接脚本的_ilm_lma标签
la a1, _ilm//将标签_ilm所处的地址赋值给a1寄存器
//注意:标签a1在链接脚本中定义,参见链接脚本的a1标签
beq a0, a1, 2f //a0和a1的值分别为标签_ilm_lma和_ilm标签的地址,判断其
//是否相等,如果相等则直接跳到后面的数字“2”标签所在的地方,
//如果不等则继续向下执行
la a2, _eilm //将标签_eilm所处的地址赋值给a2寄存器
//注意:标签_eilm在链接脚本中定义,参见链接脚本的_eilm标签
//通过一个循环,将指令从Flash中搬到ILM中
bgeu a1, a2, 2f //如果_ilm标签地址比_eilm标签地址还大,则属于不正常的配置,
//放弃搬运,直接跳转到后面数字标签2所在的位置
1:
lw t0, (a0) //从地址指针a0所在的位置(Flash中)读取32位数
sw t0, (a1) //将读取的32位数写入地址指针a1所在的位置(ILM中)
addi a0, a0, 4 //将地址指针a0寄存器加4(即32位)
addi a1, a1, 4 //将地址指针a0寄存器加4(即32位)
bltu a1, a2, 1b //跳转回之前数字标签1所在的位置
2:
/* 使用与上述相同的原理,通过一个循环,将 数据从Flash中搬运到DLM中*/
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:
//BSS段是链接器预留的未初始化变量所处的地址段,引导程序必须对其初始化为0
//此处通过一个循环来初始化BSS段
la a0, __bss_start
la a1, _end
bgeu a0, a1, 2f
1:
sw zero, (a0)
addi a0, a0, 4
bltu a0, a1, 1b
2:
/* 以下调用Newlib全局的构造函数(Global constructors) */
//
la a0, __libc_fini_array //将标签__libc_fini_array的值赋给a0作为函数参数
call atexit //调用atexit函数(Newlib的函数)
call __libc_init_array //调用__libc_init_array(Newlib的函数)
//
//注意:上述的__libc_fini_array,atexit和__libc_init_array函数都是Newlib C
// 运行库的特殊库函数,用于处理一些C/C++程序中的全局性的构造和析构函数。本文档在
// 此对其不做详细介绍,请用户自行查阅相关资料学习。
//
//值得注意的是:__libc_init_array函数中会调用一个名为_init的函数,n100-sdk
//环境中的_init函数定义在bsp/core/env/init.c中,因此此处会执行该函数,后
//文对此_init.c文件将进行进一步介绍。
//调用main函数
//根据ABI调用原则,函数调用时由a0和a1寄存器传递参数,因此此处赋参数值给a0和a1
/* argc = argv = 0 */
li a0, 0
li a1, 0
call main //调用main函数,开始执行main函数
tail exit //如果完成了main函数后,调用exit函数(Newlib桩函数之一,参见第2.3.1
//节了解Newlib桩函数的更多信息)
1:
j 1b //最后的死循环,程序理论上不可能执行到此处
.global disable_mcycle_minstret
disable_mcycle_minstret: //用于控制计数器的关闭(低功耗考虑)
csrsi CSR_MCOUNTINHIBIT, 0x5
ret
.global enable_mcycle_minstret
enable_mcycle_minstret: //用于控制计数器的开启
csrci CSR_MCOUNTINHIBIT, 0x5
ret
.global core_wfe
core_wfe: //休眠时,Wait for Event
csrc CSR_MSTATUS, MSTATUS_MIE
csrs CSR_WFE, 0x1
wfi //WFI的休眠模式
csrc CSR_WFE, 0x1
csrs CSR_MSTATUS, MSTATUS_MIE
ret
- init.c代码解读
如start.S代码中所述,在执行__libc_init_array函数时会调用一个名为_init的函数,而n100-sdk平台中的_init函数定义在bsp/core/env/init.c中。
init.c文件中的_init函数定义和功能解释如下:
//bsp/core/env/init.c代码片段
//_init函数声明
void _init()
{
#ifndef NO_INIT
soc_init(); //调用soc_init函数进行配套SoC的初始化。soc_init函数主要调用uart_init
// 函数,对UART模块进行设置。如第2.3.2节中所述,
// 由于UART是支持printf函数输出的物理接口,所以必须对UART
// 进行正确的设置。
//打印当前core的运行频率,此处调用了get_cpu_freq()函数来计算当前运行频率。参见后文
//对此函数的详解。
printf("**************************************\n");
printf("**************************************\n");
printf("* *\n");
printf("Core freq at %d Hz\n", get_cpu_freq());
printf("* *\n");
printf("**************************************\n");
printf("**************************************\n");
//IRQC 初始化并
irqc_init(irqc_NUM_INTERRUPTS);
disable_mcycle_minstret(); //在进入到main函数之前,将计数器关闭。(低功耗考虑)
#endif
}
_init函数中调用到的若干功能函数解释如下:
//uart_init函数实现(来自于bsp/nuclei-N100/soc/drivers/soc_func.c代码片段)
static void uart_init(size_t baud_rate)//参数为波特率
{
//设置UART0相关的寄存器和GPIO相关寄存器
GPIO_REG(GPIO_IOF_SEL) &= ~IOF0_UART0_MASK;
GPIO_REG(GPIO_IOF_EN) |= IOF0_UART0_MASK;
UART0_REG(UART_REG_DIV) = get_cpu_freq() / baud_rate - 1;
UART0_REG(UART_REG_TXCTRL) |= UART_TXEN;
UART0_REG(UART_REG_RXCTRL) |= UART_RXEN;
}
//get_cpu_freq函数实现 (来自于bsp/core/drivers/N100_func.c代码片段)
unsigned long get_cpu_freq()
{
uint32_t cpu_freq;
// warm up
measure_cpu_freq(1);
// measure for real
cpu_freq = measure_cpu_freq(100);//调用measure_cpu_freq函数
return cpu_freq;
}
//measure_cpu_freq函数实现(来自于bsp/core/drivers/N100_func.c代码片段)
static unsigned long __attribute__((noinline)) measure_cpu_freq(size_t n)
{
unsigned long start_mtime, delta_mtime;
unsigned long mtime_freq = get_timer_freq();
// Don't start measuruing until we see an mtime tick
unsigned long tmp = mtime_lo();
do {
start_mtime = mtime_lo();
} while (start_mtime == tmp); //不断观察MTIME计数器并将其值作为初始时间值
//通过读取CSR寄存器MCYCLE得到当前时钟周期,并作为 初始计数值
unsigned long start_mcycle = read_csr(mcycle);
do {
delta_mtime = mtime_lo() - start_mtime;
} while (delta_mtime < n); //不断观察MTIME计数器直到其值等于函数参数设定的目标值
//通过读取CSR寄存器MCYCLE得到当前时钟周期,并与初始计数值相减得到这段时间消耗的时钟周期
unsigned long delta_mcycle = read_csr(mcycle) - start_mcycle;
//由于MTIME计数器的频率是Always-On Domain的参考频率(譬如32.768KHz),而Core的运行频率与CSR寄存器MCYCLE的值一致。有关N100系列配套SoC的时钟域划分,请参见单独文档《Nuclei_N100系列配套SoC介绍》。
//因此可以通过MCYCLE和MTIME的相对关系计算出当前Core的时钟频率。
return (delta_mcycle / delta_mtime) * mtime_freq
+ ((delta_mcycle % delta_mtime) * mtime_freq) / delta_mtime;
}
//irqc_init函数实现(来自于bsp/nuclei-N100/soc/drivers/soc_func.c代码片段)
void irqc_init ( uint32_t num_irq )
{
//清除所有中断源的 IP/IE/lvl/edge 位域
write_csr(0xBD0,0x0);
write_csr(0xBD1,0x0);
write_csr(0xBD2,0x0);
write_csr(0xBD3,0x0);
}
2.3.5. 异常和中断
本节需要了解N100处理器内核的中断、异常和NMI相关知识,请参见《Nuclei_N100系列指令架构手册》了解详情。
n100-sdk平台的BSP中已经将中断和异常处理的基础框架实现,使得普通应用开发人员无需关心底层这些细节。
板级支持包中对于中断和异常处理基础框架实现的相关源代码介绍如下。
-
mtvec、mtvt寄存器的值。如《Nuclei_N100系列指令架构手册》中所述:
-
中断向量表的起始地址由CSR寄存器mtvt指定,mtvt寄存器实现为只读寄存器,为了适用本SDK,mtvt寄存器的硬件设置为0x80000。
-
异常的入口地址由mtvec寄存器指定,mtvec寄存器实现为只读寄存器,为了适用本SDK,mtvt寄存器的由硬件设置为0x80080。
-
异常入口程序trap_entry。如《Nuclei_N100系列指令架构手册》中所述:
-
由于N100系列内核进入异常和退出异常机制中没有硬件自动保存和恢复上下文的 操作,因此需要软件明确地使用(汇编语言编写的)指令进行上下文的保存和恢复。
trap_entry函数即为使用汇编语言编写的异常入口程序,该函数位于bsp/core/env/entry.S中,主要用于上下文的保存和恢复,其代码如下:
// bsp/core/env/entry.S代码片段
//该宏用于保存ABI定义的“调用者应保持的寄存器(Caller saved register)”进入堆栈
.macro SAVE_CONTEXT
//更改堆栈指针,分配20个单字(32位)的空间用于保存寄存器
addi sp, sp, -20*REGBYTES
STORE x1, 0*REGBYTES(sp)
STORE x4, 1*REGBYTES(sp)
STORE x5, 2*REGBYTES(sp)
STORE x6, 3*REGBYTES(sp)
STORE x7, 4*REGBYTES(sp)
STORE x10, 5*REGBYTES(sp)
STORE x11, 6*REGBYTES(sp)
STORE x12, 7*REGBYTES(sp)
STORE x13, 8*REGBYTES(sp)
STORE x14, 9*REGBYTES(sp)
STORE x15, 10*REGBYTES(sp)
.endm
//该宏用于从堆栈中恢复ABI定义的“调用者应保存的寄存器(Caller saved register)”
.macro RESTORE_CONTEXT
LOAD x1, 0*REGBYTES(sp)
LOAD x4, 1*REGBYTES(sp)
LOAD x5, 2*REGBYTES(sp)
LOAD x6, 3*REGBYTES(sp)
LOAD x7, 4*REGBYTES(sp)
LOAD x10, 5*REGBYTES(sp)
LOAD x11, 6*REGBYTES(sp)
LOAD x12, 7*REGBYTES(sp)
LOAD x13, 8*REGBYTES(sp)
LOAD x14, 9*REGBYTES(sp)
LOAD x15, 10*REGBYTES(sp)
addi sp, sp, 20*REGBYTES
//恢复寄存器之后,更改堆栈指针,回收19个单字(32位)的空间
.endm
//该宏用于保存MEPC和MSTATUS寄存器进入堆栈
.macro SAVE_EPC_STATUS
csrr x5, CSR_MEPC
STORE x5, 16*REGBYTES(sp)
csrr x5, CSR_MSTATUS
STORE x5, 17*REGBYTES(sp)
csrr x5, CSR_MCAUSE
STORE x5, 18*REGBYTES(sp)
.endm
//该宏用于从堆栈中恢复MEPC和MSTATUS寄存器
.macro RESTORE_EPC_STATUS
LOAD x5, 16*REGBYTES(sp)
csrw CSR_MEPC, x5
LOAD x5, 17*REGBYTES(sp)
csrw CSR_MSTATUS, x5
LOAD x5, 18*REGBYTES(sp)
csrw CSR_MCAUSE, x5
.endm
.section .text.trap
.align 6 // The trap entry must be 64bytes aligned
.global trap_entry
.weak trap_entry //指定该标签为weak类型,标签为“弱(weak)”属性。“弱(weak)”属性是
// C/C++语法中定义的一种属性,一旦有具体的“非弱”性质同名函数存在,将
//会覆盖此函数。
trap_entry: //定义标签名trap_entry,该标签名作为函数入口
//进入异常处理函数之前必须先保存处理器的上下文
SAVE_CONTEXT
//此处调用SAVE_CONTEXT保存ABI定义的“调用者应存储的寄存器(Caller saved
//register)”进入堆栈
SAVE_EPC_MSTATUS
//此处调用SAVE_EPC_MSTATUS保存MEPC和MSTATUS寄存器进入堆栈
//调用handle_trap函数
//根据ABI调用原则,函数调用时由a0和a1寄存器传递参数,因此此处赋参数值给a0和a1
csrr a0, mcause //参数1
mv a1, sp //参数2
call handle_trap //调用handle_trap函数
//在退出异常处理函数之后需要恢复之前保存的处理器上下文
RESTORE_EPC_MSTATUS
//此处调用RESTORE_EPC_MSTATUS从堆栈中恢复MEPC和MSTATUS寄存器
RESTORE_CONTEXT
//调用RESTORE_CONTEXT从堆栈中恢复ABI定义的“调用者应存储的寄存器(Caller saved
//register)”
mret //使用mret指令从异常模式返回
- 异常处理函数handle_trap
handle_trap函数是使用C/C++语言编写的中断和异常处理函数,该函数位于bsp/core/env/handlers.c中,其代码如下:
//bsp/core/env/handlers.c代码片段
//该函数理论上需要由用户自行填写,此处的函数内容仅仅作为一个示例以打印进入异常后的信息。所以此处
//指定该标签为weak类型,标签为“弱(weak)”属性。“弱(weak)”属性是C/C++语法中定义的一种属性,一旦用户定义有具体的“非弱”性质同名函数存在,将会覆盖此函数。__attribute__((weak)) uintptr_t uintptr_t
__attribute__((weak)) uintptr_t handle_trap(uintptr_t mcause, uintptr_t sp)
{
write(1, "trap\n", 5);
//printf("In trap handler, the mcause is %d\n", mcause);
//printf("In trap handler, the mepc is 0x%x\n", read_csr(mepc));
//printf("In trap handler, the mtval is 0x%x\n", read_csr(mbadaddr));
_exit(mcause);
return 0;
}
2.3.6. 使用newlib-nano
newlib-nano是一个特殊的newlib版本,它提供了更加精简版本的malloc和printf函数的实现,并且对所有库函数使用GCC的-Os(对于代码体积“Code Size”的优化)选项进行编译优化。
因此,在嵌入式系统中,推荐使用newlib-nano版本作为C运行库。如果需要使用newlib-nano版本,需要如下步骤:
-
在GCC的链接步骤时使用选项(--specs=nano.specs)来指定使用newlib-nano作为链接库。
-
如果不需要使用系统调用,还可以在链接时添加选项(--specs=nosys.specs)来指定使用空的桩函数来进行链接。
-
默认的newlib-nano的精简版printf是不支持浮点数的,如果需要输出浮点数,那么需要额外再加上一个选项(-u _printf_float)来指定支持浮点数的格式输出。注意:添加此选项后会造成代码体积一定的膨胀,因为它需要链接更多的浮点相关的函数库。
在n100-sdk平台中使用的是newlib-nano版本,可以控制是否支持浮点数的格式输出。相关脚本的代码片段和解释如下:
//n100-sdk/目录下的Makefile片段
PFLOAT := 0 //在此Makefile中有一个变量控制是否需要newlib-nano版本的
//printf支持浮点数,默认为0
2.4. 使用n100-sdk开发和编译程序
2.4.1. 在n100-sdk环境中安装工具链
因为编译程序需要使用到RISC-V GCC交叉编译工具链,所以本节先介绍如何在n100-sdk环境中安装预先编译好的GCC工具链,步骤如下:
// 步骤一:准备好自己的电脑环境,可以在公司的服务器环境中运行,如果是个人用户,推荐如下配置:
(1)使用VMware虚拟机在个人电脑上安装虚拟的Linux操作系统。
(2)由于Linux操作系统的版本众多,推荐使用Ubuntu 16.04版本的Linux操作系统
有关如何安装VMware以及Ubuntu操作系统本文档不做介绍,有关Linux的基本使用本文档
也不做介绍,请用户自行查阅资料学习。
// 步骤二:将n100-sdk项目下载到本机Linux环境中,使用如下命令:
git clone https://github.com/riscv-mcu/n100-sdk.git
// 经过此步骤将项目克隆下来,本机上即可具有如第2.2节中所述完整的
// n100-sdk目录文件夹,假设该目录为<your_sdk_dir>,后文将使用该缩写指代。
// 步骤三:由于编译软件程序需要使用到GNU工具链,假设使用完整的riscv-tools来自己编译GNU工具链则费时费力,因此本文档推荐使用芯来科技开发好的GCC工具链。用户可以在芯来科技公司网站的下载中心,下载到最新的RISC-V GCC工具链,下载中心地址记载地址为https://www.nucleisys.com/download.php。根据自己的运行平台选择对应工具链的版本,比如下载Ubuntu 64位版本工具链和Linux64的openocd,下载到本机后压缩包的名称分别为rv_linux_bare_9.2_ubuntu64.tar.bz2和nuclei-openocd-0.10.0-13-linux-x64。
cp rv_linux_bare_9.2_ubuntu64.tar.bz2 ~/
cp nuclei-openocd-0.10.0-13-linux-x64.tgz ~/
//将两个压缩包均拷贝到用户的根目录下
cd ~/
tar -xvf rv_linux_bare_9.2_ubuntu64.tar.bz2
tar -xvf nuclei-openocd-0.10.0-13-linux-x64.tgz
// 进入根目录并解压上述两个压缩包,解压后可以看到生成的rv_linux_bare_19-10-17-11-10和Nuclei两个文件夹
// 进入到 n100-sdk目录文件夹
cd <your_sdk_dir>/prebuilt_tools
// 在n100-sdk/prebuilt_tools目录下创建上述这个riscv-nuclei-elf-gcc目录
mkdir riscv-nuclei-elf-gcc
cd riscv-nuclei-elf-gcc
ln -s ~/rv_linux_bare_19-10-17-11-10/bin bin
// 再次进入到 nuclei-n-sdk 目录文件夹
cd <your_sdk_dir>/prebuilt_tools
// 在 n100-sdk/prebuilt_tools目录下创建上述这个 openocd 目录
mkdir openocd
cd openocd
ln -s ~/Nuclei/openocd/0.10.0-13/bin bin
2.4.2. 在n100-sdk环境中开发程序
本节以一个简单的Hello World程序为例,介绍如何在n100-sdk环境中开发一个应用程序。其步骤如下:
-
步骤一:在n100-sdk/software目录下创建一个hello_world的文件夹
-
步骤二:在n100-sdk/software/hello_world目录下创建一个文件hello_world.c,其内容如下:
#include <stdio.h>
int main(void)
{
//简单的Printf输出Hello World字符串
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
printf("Hello World!" "\n");
return 0;
}
- 步骤三:在n100-sdk/software/hello_world目录下创建一个文件Makefile,其内容如下:
TARGET = hello_world //指明生成的elf文件名
CFLAGS += -O2 //指明程序所需要的特别的GCC编译选项,此处指明使用GCC的O2优化级别
BSP_BASE = ../../bsp
C_SRCS += hello_world.c //指明程序所需要的C源文件
//调用板级支持包(bsp)目录下的common.mk
include $(BSP_BASE)/core/env/common.mk
经过上述步骤之后,Hello World程序在n100-sdk的相关代码结构如下所示。
n100-sdk // 存放n100-sdk的目录
|----software // 存放示例程序的源代码
|----hello_world // Hello World示例程序目录
|----hello_world.c //Hello World源代码
|----Makefile //Makefile脚本
2.4.3. 编译使得程序从ILM中运行
如第2.3.3节中所述,通过系统链接脚本link_ilm.lds可以控制将程序段存放在ILM中,并且使得代码段的物理地址和运行地址完全一致,那么在上电系统引导程序(参见第2.3.4节所述start.S)中便不会将程序进行重复上载,而是直接在ILM中运行。
以2.4.2节中开发的Hello World程序为例,在n100-sdk平台中进行编译时,使用如下命令选项将会使用链接脚本link_ilm.lds:
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1了解其详情。
cd n100-sdk
// 确保目前处于n100-sdk目录下。
make dasm PROGRAM=hello_world CORE=n101 DOWNLOAD=ilm PFLOAT=0
// DOWNLOAD=ilm:指明采用“将程序从ILM中运行的方式”进行编译,即选择使用链接脚本link_ilm.lds。
编译成功后在终端的显示信息如下,从其中同样可以看出代码尺寸信息,并且可以看到反汇编文件生成在software/hello_world/hello_world.dump中。
如果使用此种方式进行编译,按照第2.5节中所述的步骤下载程序至开发板,然后按照第2.6.1节中所述的步骤在开发板上运行程序,通过其打印到PC中断上的字符串显示速度可以看出其运行速度非常之快,这是因为程序直接从ILM中运行时每次都从ILM中取指令,能够做到每一个周期取一条指令,所以执行速度很快。
2.4.4. 编译使得程序从Flash上载至ILM中运行
如第2.3.3节中所述,通过系统链接脚本(link_flash.lds)可以控制将程序段存放在Flash中,但是使得代码段的物理地址和运行地址不一致,那么在上电系统引导程序(参见第2.3.4节所述start.S)中便会将程序上载至ILM中运行。
以2.4.2节中开发的Hello World程序为例,在n100-sdk平台中进行编译时,使用如下命令选项将会使用链接脚本link_flash.lds:
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1了解其详情。
cd n100-sdk
// 确保目前处于n100-sdk目录下。
make dasm PROGRAM=hello_world CORE=n101 DOWNLOAD=flash PFLOAT=0
// DOWNLOAD=flash:指明采用“将程序从Flash上载至ILM中运行的方式”进行编译,即选择使用链接脚本link_flash.lds。
// 注意:DOWNLAD选项的默认值在Makefile中被设定成了flash。所以,如果不指明DOWNLOAD选项,则默认采用“将程序从Flash上载至ILM进行执行的方式”进行编译。
编译成功后在终端的显示信息与图 2-1 中所示几乎一致,从其中同样可以看出代码尺寸信息,并且可以看到反汇编文件生成在software/hello_world/hello_world.dump中。
如果使用此种方式进行编译,按照第2.5节中所述的步骤下载程序至开发板,然后按照第2.6.1节中所述的步骤在开发板上运行程序,通过其打印到PC中断上的字符串显示速度可以看出其运行速度非常之快,这是因为程序从Flash上载至ILM中运行后,运行时每次都从ILM中取指令,能够做到每一个周期取一条指令,所以执行速度很快。
2.5. 使用n100-sdk下载程序
2.5.1. JTAG调试器与开发板的连接
Nuclei N100定制了专用的JTAG调试器,该调试器具有如下特性:
-
调试器的一端为普通U盘接口,便于直接将其插入主机PC的USB接口;另一端为标准的4线JTAG接口 和2线UART接口。
-
调试器具备USB转JTAG功能,通过标准的4线JTAG接口可与配套SoC原型开发板连接。
-
N100处理器内核支持标准的JTAG接口,通过此接口可以进行程序的下载与交互式调试。
-
调试器具备UART转USB功能,通过标准的2线UART接口可与配套SoC原型开发板连接。
-
由于嵌入式系统往往没有配备显示屏,因此常用UART口连接主机PC的COM口(或者将UART转换为USB后连接主机PC的USB口)进行调试,这样便可以将嵌入式系统中的printf函数重定向打印至主机的显示屏。
Nuclei N100定制了专用的FPGA评估板,作为评估的原型平台。
有关Nuclei N100定制的专用JTAG调试器和FPGA评估板的详细介绍请参见单独文档《Nuclei_N100系列配套FPGA实现》。
2.5.2. 设置JTAG调试器在Linux系统中的USB权限
如果使用Linux操作系统,需要按照如下步骤保证正确的设置JTAG调试器的USB权限。
// 步骤一:准备好自己的电脑环境,可以在公司的服务器环境中运行,如果是个人用户,推荐如下配置:
(1)使用VMware虚拟机在个人电脑上安装虚拟的Linux操作系统。
(2)由于Linux操作系统的版本众多,推荐使用Ubuntu 16.04版本的Linux操作系统。
有关如何安装VMware和Ubuntu操作系统本文档不做介绍,有关Linux的基本使用本文档也不做介绍,请用户自行查阅资料学习。
// 步骤二:将“专用JTAG调试器”插入电脑PC的USB接口。
注意:
务必使该USB接口被虚拟机的Linux系统识别(而非被Windows识别),如图 2-2中圆圈所示,若USB图标在虚拟机中显示为高亮,则表明USB被虚拟机中Linux系统正确识别(而非被Windows识别)。
若USB图标在虚拟机中显示为灰色,则表明USB没有被虚拟机中的Linux系统正确识别,如图 2-3中所示,可以使用鼠标点中USB图标,选择将其“连接(与主机的连接)”,将其连接至Linux系统(而非外部Windows)。
// 步骤三:使用如下命令查看USB设备的状态:
lsusb // 运行该命令后会显示如下信息。
...
Bus 001 Device 003: ID 0403:6010 Future Technology Devices International, Ltd FT2232C Dual USB-UART/FIFO IC
// 步骤四:使用如下命令设置udev rules使得该USB设备能够被plugdev group所访问:
sudo vi /etc/udev/rules.d/99-openocd.rules
// 用vi打开该文件,然后添加以下内容至该文件中,然后保存退出。
SUBSYSTEM=="usb", ATTR{idVendor}=="0403",
ATTR{idProduct}=="6010", MODE="664", GROUP="plugdev"
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403",
ATTRS{idProduct}=="6010", MODE="664", GROUP="plugdev"
// 步骤五:使用如下命令查看该USB设备是否属于plugdev group:
ls /dev/ttyUSB* // 运行该命令后会显示类似如下信息。
/dev/ttyUSB0 /dev/ttyUSB1
ls -l /dev/ttyUSB1 // 运行该命令后会显示类似如下信息。
crw-rw-r-- 1 root plugdev 188, 1 Nov 28 12:53 /dev/ttyUSB1
// 步骤六:将你自己的用户添加到plugdev group中:
whoami
// 运行该命令能显示自己的用户名,假设你的用户名显示为 your_user_name。
// 运行如下命令将your_user_name添加到plugdev group中。
sudo usermod -a -G plugdev your_user_name
// 步骤七:确认自己的用户是否属于plugdev group:
groups // 运行该命令后会显示类似如下信息。
... plugdev ...
// 只要从显示的groups中看到plugdev则意味着自己的用户属于该组,表示设置成功。
2.5.3. 将程序下载至FPGA原型开发板
以2.4.2节中开发的Hello World程序为例,在n100-sdk平台中,使用如下命令将编译好的hello_world程序下载至FPGA原型开发板中。
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1了解其详情。
// 注意:确保FPGA评估板与JTAG调试器正确的进行了连接,请参见本文档第2.5.1了解其详情。
// 注意:确保在Linux系统中正确设置了JTAG调试器的USB权限,请参见本文档第2.5.2了解其详情。
//将编译好的hello_world程序下载至FPGA原型开发板中,使用如下命令:
make upload PROGRAM=hello_world CORE=n101
// 上述命令使用到了如下几个Makefile参数,分别解释如下:
// upload:该选项表示对程序进行下载。
// PROGRAM=hello_world:指定需要下载software/hello_world目录下的示例程序。
// CORE=n101:指明使用的N100系列的具体处理器内核型号,此处的示例是指明n101,但是用户需要按照具体型号进行指明,譬如如果使用N101的用户此处需要指明CORE=n101。
2.6. 在FPGA评估板上运行程序
由于程序将通过UART(转换为USB口)连接至主机PC,成为一个串口,打印一个Log符号到主机PC的显示屏上。因此需要先将串口显示终端准备好,在Ubuntu的命令行终端中使用如下命令。
sudo screen /dev/ttyUSB1 115200
// 该命令将设备ttyUSB1设置为串口显示的来源,波特率为115200
// 若该命令执行成功的话,Ubuntu的该命令行终端将被锁定,用于显示串口发送的字符。
// 注意:
// 若该命令无法执行成功,请检查如下几项。
// (1) 确保已按照第2.5.2节中所述方法将USB的权限设置正确。
// (2) 确保已按照第2.5.2节中所述方法将USB被Linux虚拟机识别(右下角
// 显示为高亮)。
// (3) 按照2.5.2节中所述使用命令“ls /dev/ttyUSB*”查看USB被识别成为
// ttyUSB1还是ttyUSB2,若被识别成为ttyUSB2,则应使用命令sudo screen
// /dev/ttyUSB2 115200
将主机PC的串口显示终端准备好之后,按照后续小节所介绍的方法运行程序。
2.6.1. 程序从ILM中运行
以2.4.2节中开发的Hello World程序为例,将程序按照第2.4.3节中所介绍的编译方式(使得程序从ILM中直接运行)进行编译之后,然后按照第2.5.3节中所介绍的方法将程序下载至开发板后,便可以在开发板上运行程序。
Hello World程序正确执行后将字符串打印至主机PC的串口显示终端上。通过其打印到PC中断上的字符串显示速度可以看出其运行速度非常之快,这是因为程序直接从ILM中运行时每次都从ILM中取指令,能够做到每一个周期取一条指令,所以执行速度很快。
注意:由于程序被烧写进入了ILM中,而ILM在FPGA评估板上是由SRAM组成,因此程序会掉电丢失。
2.6.2. 程序从Flash上载至ILM中运行
以2.4.2节中开发的Hello World程序为例,将程序按照第2.4.4节中所介绍的编译方式(使得程序从Flash上载至ILM中运行)进行编译之后,然后按照第2.5.3节中所介绍的方法将程序下载至开发板后,便可以在开发板上运行程序。
Hello World程序正确执行后将字符串打印至主机PC的串口显示终端上。通过其打印到PC中断上的字符串显示速度可以看出其运行速度非常之快,这是因为程序从Flash上载至ILM中运行后,运行时每次都从ILM中取指令,能够做到每一个周期取一条指令,所以执行速度很快。
由于程序被烧写进入了Flash中,因此程序不会掉电丢失。
2.7. 使用GDB远程调试程序
GDB是常用的远程调试工具,本节将介绍如何使用GDB在n100-sdk平台中进行远程调试。
2.7.1. 调试器工作原理
不同于普通的固定电路芯片,处理器运行的是软件程序。因此,处理器对于运行于其上的软件程序提供调试能力是至关重要的。
对于处理器的调试功能而言,常用的两种是“交互式调试”和“追踪调试”。本节将对此两种调试的功能及原理加以简述。感兴趣的用户可以参见中文书籍《手把手教你设计CPU——RISC-V处理器篇》的第14章了解更多调试机制的详细介绍。
- 交互调试概述
交互调试(Interactive Debug)功能是处理器提供的最常见的一种调试功能,从最低端的处理器到最高端的处理器,交互调试几乎是必备的功能。交互调试是指调试器软件(譬如常见的调试软件GDB)能够直接对处理器取得控制权,进而对其以一种交互的方式进行调试,譬如通过调试软件对处理器。
-
下载或者启动程序。
-
通过设定各种特定条件来停止程序。
-
查看处理器的运行状态。包括通用寄存器的值、存储器地址的值等。
-
查看程序的状态。包括变量的值、函数的状态等。
-
改变处理器的运行状态。包括通用寄存器的值、存储器地址的值等。
-
改变程序的状态。包括变量的值、函数的状态等。
对于嵌入式平台而言,调试器软件一般是运行于主机PC端的一款软件,而被调试的处理器往往是在嵌入式开发板之上,这是交叉编译和远程调试的一种典型情形。调试器软件为何能够取得处理器的控制权,从而对其进行调试呢?可想而知,需要硬件的支持才能做到。在处理器核的硬件中,往往需要一个硬件调试模块。该调试模块通过物理介质(譬如JTAG接口)与主机端的调试软件进行通信接受其控制,然后调试模块对处理器核进行控制。
为了帮助用户进一步理解,以交互式调试中常见的一种调试情形为例来阐述此过程。假设调试软件GDB试图对程序中的某个PC地址设置一个断点,然后希望程序运行到此处之后停下来,之后GDB能够读取处理器当时的某个寄存器的值。调试软件和调试模块便会进行如下协同操作:
-
开发人员通过运行于主机端的GDB软件在其软件界面上设置某行程序的断点,GDB 软件通过底层驱动JTAG接口访问远程处理器的调试模块,对其下达命令,告诉其希望于某 PC设置一个断点。
-
调试模块得令即开始对处理器核进行控制,首先它会请求处理器核停止;然后修改存 储器中那个PC地址的指令,将其替换成一个Breakpoint指令;最后将处理器核放行,让其 恢复执行。
-
当处理器恢复执行后,执行到那个PC地址时,由于碰到了Breakpoint指令,会产 生异常进入调试模式的异常服务程序。调试模块探测到处理器核进入了调试模式的异 常服 务程序,并将此信息显示出来。主机端的GDB软件一直在监测调试模块的状态从而得知此信 息,便得知处理器核已经运行到断点处停止了下来,并显示在GDB软件界面上。
-
开发人员通过运行于主机端的GDB软件在其软件界面上设置读取某个寄存器的值, GDB软件通过底层驱动JTAG接口访问远程处理器的调试模块,对其下达命令,告诉其希望 读取某个寄存器的值。
-
调试模块得令即开始对处理器核进行控制,从处理器核中将那个寄存器的值读取出来, 并将此信息显示出来。主机端的GDB软件一直在监测调试模块的状态,从而得知此信息,便 通过JTAG接口将读取的值返回到主机PC端,并显示在GDB软件界面上。
注意:以上采用极为通俗的语言来描述此过程,以帮助用户理解,但难免失之严谨,请以具体的调试机制文档为准。
从上述过程中可以看出,调试机制是一套复杂的软硬件协同工作机制,需要调试软件和硬件调试模块的精密配合。
同时,也可以看出交互式调试对于处理器的运行往往是具有打扰性(Intrusive)的。调试单元会在后台偷偷地控制住处理器核,时而让其停止,时而让其运行。由于交互式调试对处理器运行的程序具有影响,甚至会改变其行为,尤其是对时间先后性有依赖的程序,有时候交互式调试并不能完整地重现某些程序的Bug。最常见的情形便是处理器在全速运行某个程序时会出现Bug,当开发人员使用调试软件对其进行交互式调试时,Bug又不见了。其主要原因往往就是交互式调试过程的打扰性(Intrusive),使得程序在调试模式和全速运行下的结果出现了差异。
- 跟踪调试概述
上一节中论述了交互式调试的一个缺点是对处理器的运行具有打扰性,为了克服此种缺陷,便引入了跟踪调试(Trace Debug)机制。
跟踪调试,即调试器只跟踪记录处理器核执行过的所有程序指令,而不会打断干扰处理器的执行过程。跟踪调试同样需要硬件的支持才能做到,相比交互式调试的实现难度更大。由于处理器的运行速度非常快,每秒钟能执行上百万条指令,如果长时间运行某个程序,其产生的信息量十分巨大。跟踪调试器的硬件单元需要跟踪记录下所有的指令,对于处理速度的要求,数据的压缩、传输和存储等都是极大挑战。跟踪调试器的硬件实现会涉及相比交互调试而言更加复杂的技术,同时硬件开销也更大,因此跟踪调试器往往只在比较高端的处理器中使用。
注意:N100系列内核不支持跟踪调试。
2.7.2. GDB常用操作示例
GDB能够用于调试C、C++、Ada等等各种语言编写的程序,它提供如下功能。
-
下载或者启动程序。
-
通过设定各种特定条件来停止程序。
-
查看处理器的运行状态,包括通用寄存器的值、存储器地址的值等。
-
查看程序的状态,包括变量的值、函数的状态等。
-
改变处理器的运行状态,包括通用寄存器的值、存储器地址的值等。
-
改变程序的状态,包括变量的值、函数的状态等。
GDB可以用于在主机PC的Linux系统中调试运行的程序,同时也能用于调试嵌入式硬件。在嵌入式硬件的环境中,由于资源有限,一般的嵌入式目标硬件上无法直接构建GDB的调试环境(譬如显示屏和Linux系统等),这时可以通过GDB+GdbServer的方式进行远程(Remote)调试,通常GdbServer在目标硬件上运行,而GDB则在主机PC上运行。
为了能够支持GDB对其进行调试,Nuclei N100系列配套SoC使用OpenOCD作为其GdbServer,与GDB进行配合。OpenOCD(Open On-Chip Debugger )是一款开源的免费调试软件,由社区共同维护。由于其开放开源的特点,众多的公司和个人使用其作为调试软件,支持大多数主流的MCU和硬件开发板。通过编写OpenOCD的底层驱动文件能够使其通过JTAG接口连接Nuclei N100系列配套SoC,并利用其硬件调试特性对Nuclei N100系列配套SoC进行调试。
为了能够完全支持GDB的功能,在使用GCC对源代码进行编译时,需要使用-g选项,例如:‘gcc -g -o hello hello.c’。-g选项会将调试所需信息加入编译所得的可执行程序中,因此该选项会增大可执行程序的大小,在正式发布的版本中通常不使用该选项。
GDB虽然可以使用一些前端工具实现图形化界面,但是更常见的是使用命令行直接对其进行操作。常用的GDB命令介绍如下表所示。
命令 | 介绍 |
---|---|
load file | 动态链入file文件,并读取它的符号表 |
jump | 使当前执行的程序跳转到某一行,或者跳转到某个地址 |
info br | 使用该指令可查看断点信息,br是断点break的缩写,GDB具有自动补齐功能,此命令等效于info break |
info source | 使用该指令可查看当前程序的信息 |
info stack | 使用该指令可查看程序的调用层次关系 |
list function-name | 使用该指令可列出某个函数 |
list line-number | 列出某行附近的代码 |
break function break line-number | 在指定的函数,或者行号处设置断点 |
break *address | 在指定的地址处设置断点,一般在没有源代码时使用 |
continue | 恢复程序运行,直到遇到下一个断点 |
step | 进入下一行代码的执行,会进入函数内部 |
step number | 等效于连续执行number次step命令 |
next | 执行下一行代码,但不会进入函数内部 |
next number | 等效于连续执行number次next命令 |
until until line-number | 继续运行直到到达指定行号,或者函数、地址等 |
stepi nexti | stepi/nexti命令与step/next的区别在于其执行下一条汇编指令,而不是下一行代码(譬如C/C++中的一行代码) |
x address | 打印指定存储器地址中的值 |
p variable | 打印指定变量的值 |
表 21 GDB常用命令
2.7.3. 使用GDB调试Hello World示例
以2.4.3节中开发的Hello World程序为例(从ILM中直接执行),在n100-sdk平台中,按照如下步骤使用GDB和OpenOCD对基于Nuclei N100系列配套SoC原型开发板进行调试。
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1节了解其详情。
// 注意:确保FPGA评估板与JTAG调试器正确的进行了连接,请参见本文档第2.5.1了解其详情。
// 注意:确保在Linux系统中正确设置了JTAG调试器的USB权限,请参见本文档第2.5.2节了解其详情。
// 确保位于n100-sdk目录。
cd n100-sdk
// 步骤一:从第一个Terminal中打开OpenOCD
// 首先使用如下命令打开OpenOCD
make run_openocd PROGRAM=hello_world CORE=n101 DOWNLOAD=ilm
// 运行该命令会来打开OpenOCD,并与开发板相连。
// 如果该步骤执行成功,则如图 2-4所示。
// 步骤二:新开一个Terminal,打开GDB
// 由于命令行界面已经被OpenOCD挂住,因此需要重新开启一个新的Terminal终端,
// 注意:再次强调,此处是重新开启一个新的Terminal终端。
// 在新的Terminal终端下,同样确保位于n100-sdk目录。
cd n100-sdk
// 然后使用如下命令打开GDB
make run_gdb PROGRAM=hello_world CORE=n101 DOWNLOAD=ilm
// 运行该命令会自动打开GDB来调试hello_world示例程序。
// 如果该步骤执行成功,则进入了GDB的调试命令行界面,如图 2-5所示。
// 步骤三:演示使用GDB命令:
// 接下来便可使用GDB的常用命令进行调试。
b main
// 在main函数的入口处设置断点。
info b
// 查看目前程序设置的断点,显示如图 2-6所示。
x 0x80084
x 0x80088
x 0x80092
// 查看存储器地址0x80084/0x80088/0x80092中的数值,显示如图 2-7
// 所示。
info reg
info reg mstatus
// 查看当前处理器的通用寄存器的值和CSR寄存器mstatus的值,显示如图 2-8所示。
info reg csr768
// 查看当前处理器的地址768的CSR寄存器的值。
// 注意:编号768为十进制数,对应十六进制为0x300,对应于mstatus寄存器的CSR
// 地址。参见《Nuclei_N100系列指令架构手册》了解RSIC-V架构的CSR寄存器列表和地址。
info reg mcause
info reg mepc
info reg mtval
// 查看当前处理器的CSR寄存器mcause,mepc和mtval的值。
// 注意:当程序出现了异常(程序运行结果显示结果为Trap)时,可以通过GDB查看此
// 三个寄存器的值有效的定位异常的原因和发生位置。有关mcause,mepc和mtval
// 寄存器的详情,请参见《Nuclei_N100系列指令架构手册》。
jump main
// 从程序的main入口开始执行,将停于设置的第一个断点处,显示如图 2-9所示。
ni
// 单步执行,显示如图 2-10所示。
continue
// 继续执行,将停于下一个断点处,若无断点则一直执行至程序结束处。
3. 使用n100-sdk运行更多示例程序
在本文档上一章中介绍了基于n100-sdk平台开发一个简单的Hello World程序,并且下载、运行调试的方法。本章将进一步解析几个功能更加丰富的示例程序,以便于用户巩固和加深理解。
3.1. Dhrystone示例程序
3.1.1. Dhrystone示例程序功能简介
Dhrystone是一个综合的处理器Benchmark Program(跑分程序),由Reinhold P. Weicker 于1984年开发,用于衡量处理器的整数运算处理性能。
有关Dhrystone跑分程序详细的背景知识和计算方法,用户可以参阅中文书籍《手把手教你设计CPU——RISC-V处理器篇》的第20章。本文档在此不做赘述,仅讲解如何使用n100-sdk运行Dhrystone。
3.1.2. Dhrystone示例程序代码结构
Dhrystone示例程序的相关代码结构如下所示。
n100-sdk // 存放n100-sdk的目录
|----software // 存放示例程序的源代码
|----dhrystone // Dhrystone程序目录
|----dhry_1.c //源代码
|----dhry_2.c //源代码
|----dhry_stubs.c //源代码
|----Makefile //Makefile脚本
Makefile为主控制脚本,其代码片段如下:
//指明生成的elf文件名
TARGET = dhrystone
//指明Dhrystone程序所需要的特别的GCC编译选项
CFLAGS := -O2 -fno-inline -fno-common -falign-labels=4 -falign-functions=4 -falign-jumps=4 -falign-loops=4
BSP_BASE = ../../bsp
//指明Dhrystone程序所需要的C源文件
C_SRCS := dhry_stubs.c dhry_1.c dhry_2.c
HEADERS := dhry.h
//调用板级支持包(bsp)目录下的common.mk
include $(BSP_BASE)/core/env/common.mk
3.1.3. 运行Dhrystone
Dhrystone跑分程序示例可运行于Nuclei N100系列配套SoC平台中,使用本文档第2.4节中介绍的方法按照如下步骤运行:
// 步骤一:参照本文档第2.4节中描述的方法,编译Dhrystone示例程序,使用如下命令:
make dasm PROGRAM=dhrystone CORE=n101 PFLOAT=1 DOWNLOAD=flash
//注意:由于Dhrystone程序的printf函数需要输出浮点数,上述PFLOAT=1指明newlib-nano的printf函数需要支持浮点数,请参见本文档第2.3.6节了解相关信息。
//注意:此处指定DOWNLOAD=flash选项,则采用“将程序从Flash上载至ILM进行执行的方式”进行编译,请参见本文档第2.4.4节了解更多详情。
// 步骤二:参照本文档第2.5节中描述的方法,将编译好的Dhrystone程序下载至FPGA原型开发板中,使用如下命令:
make upload PROGRAM=dhrystone CORE=n101 PFLOAT=1 DOWNLOAD=flash
// 步骤三:参照本文档第2.6节中描述的方法,在FPGA原型开发板上运行Dhrystone程序:
// 由于示例程序将需要通过UART打印结果到主机PC的显示屏上。参考第2.6节中
// 所述方法将串口显示电脑屏幕设置好,使得程序的打印信息能够显示在电脑屏幕上。
//
// 由于步骤二已经将程序烧写进FPGA评估板的Flash之中,因此每次按FPGA评估板的
// RESET按键,则处理器复位开始执行Dhrystone程序,并将字符串打印至主机PC
// 的串口显示终端上,从其打印的结果我们可以看出处理器内核运行Dhrystone程
// 序的结果性能指标。
3.2. CoreMark示例程序
3.2.1. CoreMark示例程序功能简介
CoreMark也是一个综合的处理器Benchmark程序,由非盈利组织EEMBC(Embedded Microprocessor Benchmark Consortium)的Shay Gal-On于2009年开发。
有关CoreMark跑分程序详细的背景知识和计算方法,用户可以参阅中文书籍《手把手教你设计CPU——RISC-V处理器篇》的第20章。本文档在此不做赘述,仅讲解如何使用n100-sdk运行CoreMark。
3.2.2. CoreMark示例程序代码结构
CoreMark示例程序的相关代码结构如下所示。
n100-sdk // 存放n100-sdk的目录
|----software // 存放示例程序的源代码
|----coremark // CoreMark示例程序目录
|----core_list_join.c //Coremark的源代码
|----core_main.c
|----core_matrix.c
|----core_state.c
|----core_util.c
|----core_portme.c
|----Makefile //Makefile脚本
Makefile为主控制脚本,其代码片段如下:
//指明生成的elf文件名
TARGET := coremark
//指明CoreMark程序所需要的C源文件
C_SRCS := \
core_list_join.c \
core_main.c \
core_matrix.c \
core_state.c \
core_util.c \
core_portme.c \
HEADERS := \
coremark.h \
core_portme.h \
//指明CoreMark程序所需要的特别的GCC编译选项
CFLAGS := -O3 -funroll-all-loops -finline-limit=600 -ftree-dominator-opts -fno-if-conversion2 -fselective-scheduling -fno-code-hoisting -fno-common -funroll-loops -finline-functions -falign-functions=4 -falign-jumps=4 -falign-loops=4
CFLAGS += -DFLAGS_STR=\""$(CFLAGS)"\"
CFLAGS += -DITERATIONS=10000 -DPERFORMANCE_RUN=1
BSP_BASE = ../../bsp
//调用板级支持包(bsp)目录下的common.mk
include $(BSP_BASE)/core/env/common.mk
3.2.3. 运行CoreMark
CoreMark跑分程序示例可运行于Nuclei N100系列配套SoC平台中,使用本文档第2.4节中介绍的方法按照如下步骤运行:
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1了解其详情。
// 步骤一:参照本文档第2.4节中描述的方法,编译CoreMark示例程序,使用如下命令:
make dasm PROGRAM=coremark CORE=n101 PFLOAT=1 DOWNLOAD=flash
//注意:由于CoreMark程序的printf函数需要输出浮点数,上述选项PFLOAT=1指明newlib-nano的printf函数需要支持浮点数,请参见本文档第2.3.6节了解相关信息。
//注意:此处指定DOWNLOAD=flash选项,则采用“将程序从Flash上载至ILM进行执行的方式”进行编译,请参见本文档第2.4.4节了解更多详情。
// 步骤二:参照本文档第2.5节中描述的方法,将编译好的CoreMark程序下载至FPGA原型开发板中,使用如下命令:
make upload PROGRAM=coremark CORE=n101 PFLOAT=1 DOWNLOAD=flash
// 步骤三:参照本文档第2.6节中描述的方法,在FPGA原型开发板上运行CoreMark程序:
// 由于示例程序将需要通过UART打印结果到主机PC的显示屏上。参考第2.6节中
// 所述方法将串口显示电脑屏幕设置好,使得程序的打印信息能够显示在电脑屏幕上。
//
// 由于步骤二已经将程序烧写进FPGA评估板的Flash之中,因此每次按FPGA评估板的
// RESET按键,则处理器复位开始执行CoreMark程序,并将字符串打印至主机PC
// 的串口显示终端上,从其打印的结果我们可以看出处理器内核运行CoreMark程
// 序的结果性能指标。
3.3. Demo_iasm示例程序
3.3.1. Demo_iasm示例程序功能简介
Demo_iasm程序是一个完整的示例程序,用于演示在C/C++程序中直接嵌入汇编程序的执行结果。
3.3.2. Demo_iasm示例程序代码结构
Demo_iasm示例程序的相关代码结构如下所示。
n100-sdk // 存放n100-sdk的目录
|----software // 存放示例程序的源代码
|----demo_iasm // demo_iasm示例程序目录
|----demo_iasm.c //demo_iasm源代码
|----Makefile //Makefile脚本
Makefile为主控制脚本,其代码片段如下:
//指明生成的elf文件名
TARGET = demo_iasm
//指明Demo_iasm程序所需要的特别的GCC编译选项
CFLAGS += -O2
BSP_BASE = ../../bsp
//指明Demo_iasm程序所需要的C源文件
C_SRCS += demo_iasm.c
//调用板级支持包(bsp)目录下的common.mk
include $(BSP_BASE)/core/env/common.mk
其中demo_iasm.c为源代码,下节对其源码和功能进行详述。
3.3.3. 运行Demo_iasm
Demo_iasm示例可运行于Nuclei N100系列配套SoC平台中,使用本文档第2.4节中介绍的方法按照如下步骤运行:
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1解其详情。
// 步骤一:参照本文档第2.4节中描述的方法,编译Demo_iasm示例程序,使用如下命令:
make dasm PROGRAM=demo_iasm CORE=n101 PFLOAT=0 DOWNLOAD=flash
//注意:由于Demo_iasm程序的printf函数不需要输出浮点数,上述选项PFLOAT=0指明newlib-nano的printf函数无需支持浮点数,请参见本文档第2.3.6节了解相关信息。
//注意:此处指定DOWNLOAD=flash选项,则采用“将程序从Flash上载至ILM进行执行的方式”进行编译,请参见本文档第2.4.4节了解更多详情。
// 步骤二:参照本文档第2.5节中描述的方法,将编译好的Demo_iasm程序下载至FPGA原型开发板中,使用如下命令:
make upload PROGRAM=demo_iasm CORE=n101 DOWNLOAD=flash
// 步骤三:参照本文档第2.6节中描述的方法,在FPGA原型开发板上运行Demo_iasm程序:
// 由于示例程序将需要通过UART打印结果到主机PC的显示屏上。参考第2.6节中
// 所述方法将串口显示电脑屏幕设置好,使得程序的打印信息能够显示在电脑屏幕上。
//
// 由于步骤二已经将程序烧写进FPGA评估板的Flash之中,因此每次按开发板的
// RESET按键,则处理器复位开始执行Demo_iasm程序,并将字符串打印至主机PC
// 的串口显示终端上,如图 3-1所示,程序运行的结果为PASS,意味着达到了预期。
3.4. Demo_irqc示例程序
3.4.1. Demo_irqc示例程序功能简介
Demo_irqc程序是一个完整的示例程序,相比Dhrystone和Coremark这样纯粹的跑分程序,Demo_irqc更加接近一个常见的嵌入式应用程序,它使用到了SoC系统中的外设,调用了中断处理函数等,其功能简述如下:
-
通过printf函数输出一串RISC-V的字符,printf输出将会通过UART串口重定向至主机PC的屏幕上,如图 3-2所示。
-
等待通过getc函数输入一个字符,然后将得到的字符通过printf输出值主机PC的屏幕上,如所示。
-
进入死循环不断地对SoC的GPIO 13的输出管脚进行翻转,如果使用示波器观测此GPIO输出管脚,可以看到其产生规律的输出方波。
-
评估板上的两个用户按键连接到了SoC的GPIO管脚,这两个GPIO管脚各自作为一个IRQC的外部中断,BTN_L和BTN_R分别与GPIO0和GPIO1连接,在其中断处理函数中会将对GPIO的输出管脚(对应评估板的绿灯和蓝灯)进行设置,从而造成评估板上绿灯、蓝灯的颜色发生变化。
3.4.2. Demo_irqc示例程序代码结构
Demo_irqc示例程序的相关代码结构如下所示。
n100-sdk // 存放n100-sdk的目录
|----software // 存放示例程序的源代码
|----demo_irqc // demo_irqc示例程序目录
|----demo_irqc.c //demo_irqc源代码
|----Makefile //Makefile脚本
Makefile为主控制脚本,其代码片段如下:
//指明生成的elf文件名
TARGET = demo_irqc
//指明Demo_irqc程序所需要的特别的GCC编译选项
CFLAGS += -O2
BSP_BASE = ../../bsp
//指明Demo_irqc程序所需要的C源文件
C_SRCS += demo_irqc.c
//调用板级支持包(bsp)目录下的common.mk
include $(BSP_BASE)/core/env/common.mk
其中demo_irqc.c为源代码,下节对其源码和功能进行详述。
3.4.3. Demo_irqc示例程序源码分析
- 主函数
Demo_irqc的主程序位于software/demo_irqc/demo_irqc.c中,其源代码片段如下:
//software/demo_irqc/demo_irqc.c代码片段
//主函数的入口
int main(int argc, char **argv)
{
//设置开发板上按键相关的GPIO寄存器
//通过“与”操作将GPIO_OUTPUT_EN寄存器某些位清0,即将开发板按键对应的GPIO输出使能关闭
GPIO_REG(GPIO_OUTPUT_EN) &=
~(
(0x1 << BUTTON_1_GPIO_OFFSET) |
(0x1 << BUTTON_2_GPIO_OFFSET)
);
//通过“与”操作将GPIO_PULLUP_EN寄存器某些位清0,即将开发板按键对应的GPIO输入上拉关闭
GPIO_REG(GPIO_PULLUP_EN) &=
~(
(0x1 << BUTTON_1_GPIO_OFFSET) |
(0x1 << BUTTON_2_GPIO_OFFSET)
);
//通过“或”操作将GPIO_INPUT_EN寄存器某些位设置为1,即将开发板按键对应的GPIO输入使能关闭
GPIO_REG(GPIO_INPUT_EN) |=
(
(0x1 << BUTTON_1_GPIO_OFFSET) |
(0x1 << BUTTON_2_GPIO_OFFSET)
);
//通过“或”操作将GPIO_RISE_IE寄存器某些位设置为1,即将开发板按键对应的GPIO管脚设置为上升沿触发的中断来源
GPIO_REG(GPIO_RISE_IE) |= (1 << BUTTON_1_GPIO_OFFSET);
GPIO_REG(GPIO_RISE_IE) |= (1 << BUTTON_2_GPIO_OFFSET);
//设置开发板上三色灯相关的GPIO寄存器
//通过“与”操作将GPIO_INPUT_EN寄存器某些位清0,即将开发板三色灯对应的GPIO输入使能关闭
GPIO_REG(GPIO_INPUT_EN) &=
~(
(0x1<< RED_LED_GPIO_OFFSET) |
(0x1<< GREEN_LED_GPIO_OFFSET) |
(0x1 << BLUE_LED_GPIO_OFFSET)
) ;
//通过“或”操作将GPIO_INPUT_EN寄存器对应的位置1,即将开发板三色灯对应的GPIO输出使能打开
GPIO_REG(GPIO_OUTPUT_EN) |=
(
(0x1<< RED_LED_GPIO_OFFSET) |
(0x1<< GREEN_LED_GPIO_OFFSET) |
(0x1 << BLUE_LED_GPIO_OFFSET)
) ;
//通过“或”操作将GPIO_OUTPUT_EN寄存器对应的位置1,即将开发板三色灯中的红色灯对应的GPIO输出值设置为1,这意味着将三色灯的红色灯打开。
GPIO_REG(GPIO_OUTPUT_VAL) |= (0x1 << RED_LED_GPIO_OFFSET) ;
//通过“与”操作将GPIO_OUTPUT_EN寄存器某些清0,即将开发板三色灯中的蓝色和绿色灯对应的GPIO输出值设置为0,这意味着将三色灯的蓝色和绿色灯关闭,所以只会显示红色。
GPIO_REG(GPIO_OUTPUT_VAL) &= ~((0x1<< BLUE_LED_GPIO_OFFSET) | (0x1<< GREEN_LED_GPIO_OFFSET)) ;
// 输出特殊字符串至屏幕
printf ("%s",printf_instructions_msg);
// 提示输入任意字符
printf ("%s","\nPlease enter any letter from keyboard to continue!\n");
// 进入循环等待用户从键盘输入任意字符(使用_getc函数),得到输入后跳出循环。
// 注意:_getc函数的函数体定义在demo_irqc.c文件中,通过UART0的输入通道抓取字符。
char c;
// Check for user input
while(1){
if (_getc(&c) != 0){
printf ("%s","I got an input, it is\n\r");
break;
}
}
_putc(c);
printf ("\n\r");
printf ("%s","\nThank you for supporting RISC-V, you will see the blink soon on the board!\n");
// 配置irqc,使能GPIO中断,参见后文对此函数的介绍
config_irqc_irqs ();
//使能全局中断
set_csr(mstatus, MSTATUS_MIE);
/*************************************************************************/
//接下来进入死循环,每个循环中都使用原子操作对bitbang_mask对应的GPIO管脚输出值进行反转。
// 设置位操作(bitbang)的指示位
uint32_t bitbang_mask = 0;
bitbang_mask = (1 << 13); //bitbang_mask对应GPIO 13管脚。
//将位操作(bitbang)对应的GPIO管脚设置为输出
GPIO_REG(GPIO_OUTPUT_EN) |= bitbang_mask;
while (1){
// 进入死循环不断地对某个GPIO的输出管脚进行翻转(使用原子操作库函数),如果使用示波器观测此GPIO输出管脚,可以看到其产生规律的输出方波。
GPIO_REG(GPIO_OUTPUT_VAL) ^= bitbang_mask;
}
return 0;
}
- IRQC配置
如第2.3.5节中所述,从上述代码可以看出,config_irqc_irqs负责中断的使能,其代码片段如下:
void config_irqc_irqs (){
//通过配置IRQC的寄存器使能“开发板两个按键的GPIO中断”。注意:irqc_enable_interrupt的函数原型定义在/bsp/core/drivers/fun.c中
irqc_enable_interrupt (IRQC_INT_DEVICE_BUTTON_1);
irqc_enable_interrupt (IRQC_INT_DEVICE_BUTTON_2);
- 外部中断处理函数
如第3.4.1节所述,开发板上的两个按键连接到了GPIO的管脚,这两个GPIO管脚各自作为一个IRQC的外部中断。Demo_irqc使用这两个中断,所以定义了BUTTON_1_HANDLER和BUTTON_2_HANDLER分别作为它们的中断处理函数,其代码如下:
void __attribute__ ((interrupt)) BUTTON_1_HANDLER(void) {
printf ("%s","----Begin button1 handler\n");
// 点亮绿灯
GPIO_REG(GPIO_OUTPUT_VAL) |= (1 << GREEN_LED_GPIO_OFFSET);
GPIO_REG(GPIO_RISE_IP) = (0x1 << BUTTON_1_GPIO_OFFSET);
// 等待2S
wait_seconds(2);
printf ("%s","----End button1 handler\n");
};
void BUTTON_2_HANDLER(void) {
printf ("%s","--------Begin button2 handler\n");
// 点亮蓝灯
GPIO_REG(GPIO_OUTPUT_VAL) |= (1 << BLUE_LED_GPIO_OFFSET);
GPIO_REG(GPIO_RISE_IP) = (0x1 << BUTTON_2_GPIO_OFFSET);
// 等待2S
wait_seconds(2);
printf ("%s","--------End button2 handler\n");
};
3.4.4. 运行Demo_irqc
Demo_irqc示例可运行于Nuclei N100系列配套SoC平台中,使用本文档第2.4节中介绍的方法按照如下步骤运行:
// 注意:确保在n100-sdk中正确的安装了RISC-V GCC工具链,请参见本文档第2.4.1解其详情。
// 步骤一:参照本文档第2.4.4节中描述的方法,编译Demo_irqc示例程序,使用如下命令:
make dasm PROGRAM=demo_irqc CORE=n101 DOWNLOAD=flash
//注意:此处指定DOWNLOAD=flash选项,则采用“将程序从Flash上载至ILM进行执行的方式”进行编译,请参见本文档第2.4.4节了解更多详情。
// 步骤二:参照本文档第2.5.3节中描述的方法,将编译好的Demo_irqc程序下载至FPGA原型开发板中,使用如下命令:
make upload PROGRAM=demo_irqc CORE=n101 DOWNLOAD=flash
// 步骤三:参照本文档第2.6节中描述的方法,在FPGA原型开发板上运行Demo_irqc程序:
// 由于示例程序将需要通过UART打印结果到主机PC的显示屏上。参考第2.6节中
// 所述方法将串口显示电脑屏幕设置好,使得程序的打印信息能够显示在电脑屏幕上。
//
// 由于步骤二已经将程序烧写进FPGA评估板的Flash之中,因此每次按评估板的
// MCU_RESET按键,则处理器复位开始执行Demo_irqc程序,并将RISC-V字符串打印至主
// 机PC的串口显示终端上,如图3-3所示,然后用户可以输入任意字符(譬如字母y),
// 程序继续运行,通过按评估板上的BTN_L、BTN_R按键可分别点亮绿灯、蓝灯。
图33 运行Demo_irqc示例后于主机串口终端上显示信息