来源:liangkangnan的博客
更新于 2021-01-31
tinyriscv 一个从零开始写的极简易懂的RISC-V处理器核
从零开始写RISC-V处理器之一 前言
从零开始写RISC-V处理器之二 绪论
从零开始写RISC-V处理器之三 硬件篇
从零开始写RISC-V处理器之四 软件篇
从零开始写RISC-V处理器之五 实践篇
从零开始写RISC-V处理器之六 写在最后
实践篇
移植tinyriscv到FPGA
这里只介绍xilinx vivado平台的移植,详见tinyriscv项目的fpga/README.md文件。
编写和运行C语言程序
C语言的例程都在tests/example目录里,其中include、lib为公共目录,所有例程都依赖这两个目录。
当所需编写一个新的例程(程序)时,可以通过以下步骤:
1.拷贝simple这个例程,然后改成自己想要的名字。
2.接着修改Makefile文件:
...
TARGET = simple
CFLAGS += -DSIMULATION
C_SRCS := \
main.c \
...
修改第2行,这个TARGET就是程序编译后生成的bin文件名字,这里是simple.bin。
修改第4行,CFLAGS是编译选项,这里的simple例程默认作为仿真使用,所以需要定义SIMULATION宏。
修改第6行,将需要编译的汇编文件全部添加到这里。
修改第8行,将涉及的头文件路径全部添加到这里。
修改第10行,将需要编译的c文件全部添加到这里。
3.打开终端,进入到例程的根目录,先输入make clean命令再输入make命令即可编译程序。
由于tinyriscv是支持通过jtag下载程序的,因此在fpga综合、实现时不需要预先读入程序(bin)文件,只需要在下载完bitstream文件后连上jtag和openocd,即可通过openocd的load_image命令下载程序。这样的好处是,当需要更新程序时不需要重新综合、实现,可以节省很多时间。
移植FreeRTOS
freertos是一款轻量级的实时嵌入式操作系统,支持多个平台(X86、ARM、RISC-V等)和多种编译器、编译环境(GCC、KEIL、IAR等),具有可配置、任务管理、信号量、消息队列、内存管理和软件定时器等功能。freertos遵循MIT开源协议。
鉴于freertos的国内外的知名度和代码的精简度,这里选择它作为tinyriscv支持的首个RTOS。
目前freertos已经支持好几款RISC-V开发板(处理器)了,比如SiFive的HiFive1_RevB,NXP的Vega。这样的话,在移植freertos到其他RISC-V处理器或者开发板时就不需要从零开始了,直接在其中一个的基础上修改就可以了。事实上,tinyriscv就是这么做的。
RISC-V的中断(异常)架构与ARM相比,其中有一点是做得非常好的,那就是RISC-V的中断(异常)入口地址是可以通过软件修改的(修改mtvec寄存器的值),而不像ARM那样硬件设计好了就不能变了。这样的好处是,在移植freertos时就可以共用原有(其他例程)的启动代码,只需要在系统初始化时设置mtvec为freertos的中断入口地址就可以了。
下面开始分析freertos的代码,先看初始化过程。
tinyriscv的启动代码前面已经分析过了,这里就直接从main()函数开始看了。先给出主要的函数调用层次关系。
examples/FreeRTOS/Demo/tinyriscv_GCC/main.c(main()-->main_blinky())
--examples/FreeRTOS/Demo/tinyriscv_GCC/blinky_demo/main_blinky.c(main_blinky()-->vTaskStartScheduler())
----examples/FreeRTOS/Source/tasks.c(vTaskStartScheduler()-->xPortStartScheduler())
------examples/FreeRTOS/Source/portable/RISC-V/port.c(xPortStartScheduler()-->xPortStartFirstTask())
--------examples/FreeRTOS/Source/portable/RISC-V/portASM.S(xPortStartFirstTask())
main()函数的定义:
int main( void )
{
prvSetupHardware();
#if( mainCREATE_SIMPLE_BLINKY_DEMO_ONLY == 1 )
{
main_blinky();
}
#else
{
main_full();
}
#endif
}
第3行,调用prvSetupHardware()函数,这个函数里只做了一件事情,就是将LED对应的GPIO设置为输出模式。
第7行,条件成立。这个demo实现(移植)的就是简单的LED闪灯功能,只不过是通过两个任务来实现。一个任务作为发送者,另一个任务作为接收者,发送者每隔一段时间向接收者发送一个消息,接收者收到这个消息后,判断是否是想要的消息,如果是则将LED所在的GPIO电平取反。
第9行,调用main_blinky()函数:
void main_blinky( void )
{
xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof( uint32_t ) );
if( xQueue != NULL )
{
xTaskCreate( prvQueueReceiveTask,
"Rx",
configMINIMAL_STACK_SIZE,
NULL,
mainQUEUE_RECEIVE_TASK_PRIORITY,
NULL );
xTaskCreate( prvQueueSendTask, "TX", configMINIMAL_STACK_SIZE, NULL, mainQUEUE_SEND_TASK_PRIORITY, NULL );
vTaskStartScheduler();
}
for( ;; );
}
这里并不会具体分析freertos代码的实现,只有涉及到移植相关的才会详细说明。
第4行,创建队列,发送任务和接收任务会利用这个队列会来收发数据。
第10行,创建接收任务。
第17行,创建发送任务。
第20行,开始任务调度,vTaskStartScheduler()这个函数代码比较长,这里就不贴代码了。这里面有一个很重要的操作,就是调用xPortStartScheduler()函数:
BaseType_t xPortStartScheduler( void )
{
extern void xPortStartFirstTask( void );
...
vPortSetupTimerInterrupt();
...
xPortStartFirstTask();
return pdFAIL;
}
第9行,调用vPortSetupTimerInterrupt()函数,在这个函数里需要初始化操作系统的systick定时器并启动。
第12行,xPortStartFirstTask()函数是用汇编语言实现的,这个函数跟移植密切相关,这里分析一下:
.func
xPortStartFirstTask:
#if( portasmHAS_SIFIVE_CLINT != 0 )
la t0, freertos_risc_v_trap_handler
csrw mtvec, t0
#endif
load_x sp, pxCurrentTCB
load_x sp, 0( sp )
load_x x1, 0( sp )
portasmRESTORE_ADDITIONAL_REGISTERS
load_x t0, 29 * portWORD_SIZE( sp )
addi t0, t0, 0x08
csrrw x0, mstatus, t0
load_x x5, 2 * portWORD_SIZE( sp )
load_x x6, 3 * portWORD_SIZE( sp )
load_x x7, 4 * portWORD_SIZE( sp )
load_x x8, 5 * portWORD_SIZE( sp )
load_x x9, 6 * portWORD_SIZE( sp )
load_x x10, 7 * portWORD_SIZE( sp )
load_x x11, 8 * portWORD_SIZE( sp )
load_x x12, 9 * portWORD_SIZE( sp )
load_x x13, 10 * portWORD_SIZE( sp )
load_x x14, 11 * portWORD_SIZE( sp )
load_x x15, 12 * portWORD_SIZE( sp )
load_x x16, 13 * portWORD_SIZE( sp )
load_x x17, 14 * portWORD_SIZE( sp )
load_x x18, 15 * portWORD_SIZE( sp )
load_x x19, 16 * portWORD_SIZE( sp )
load_x x20, 17 * portWORD_SIZE( sp )
load_x x21, 18 * portWORD_SIZE( sp )
load_x x22, 19 * portWORD_SIZE( sp )
load_x x23, 20 * portWORD_SIZE( sp )
load_x x24, 21 * portWORD_SIZE( sp )
load_x x25, 22 * portWORD_SIZE( sp )
load_x x26, 23 * portWORD_SIZE( sp )
load_x x27, 24 * portWORD_SIZE( sp )
load_x x28, 25 * portWORD_SIZE( sp )
load_x x29, 26 * portWORD_SIZE( sp )
load_x x30, 27 * portWORD_SIZE( sp )
load_x x31, 28 * portWORD_SIZE( sp )
addi sp, sp, portCONTEXT_SIZE
ret
.endfunc
第4行,条件成立。
第8~9行,将mtvec的值设置为freertos_risc_v_trap_handler()函数的地址,即中断(异常)入口函数为freertos_risc_v_trap_handler()。
后面的代码会将当前TCB里保存的寄存器值恢复到对应的寄存器,当xPortStartFirstTask()函数返回后就会执行当前(pxCurrentTCB所指的)任务。
到这里,我们就可以知道接下来的重点就是freertos_risc_v_trap_handler()函数,这个函数的代码也是比较长,这里只列出tinyriscv用到的部分:
.func
freertos_risc_v_trap_handler:
addi sp, sp, -portCONTEXT_SIZE
store_x x1, 1 * portWORD_SIZE( sp )
store_x x5, 2 * portWORD_SIZE( sp )
store_x x6, 3 * portWORD_SIZE( sp )
store_x x7, 4 * portWORD_SIZE( sp )
store_x x8, 5 * portWORD_SIZE( sp )
store_x x9, 6 * portWORD_SIZE( sp )
store_x x10, 7 * portWORD_SIZE( sp )
store_x x11, 8 * portWORD_SIZE( sp )
store_x x12, 9 * portWORD_SIZE( sp )
store_x x13, 10 * portWORD_SIZE( sp )
store_x x14, 11 * portWORD_SIZE( sp )
store_x x15, 12 * portWORD_SIZE( sp )
store_x x16, 13 * portWORD_SIZE( sp )
store_x x17, 14 * portWORD_SIZE( sp )
store_x x18, 15 * portWORD_SIZE( sp )
store_x x19, 16 * portWORD_SIZE( sp )
store_x x20, 17 * portWORD_SIZE( sp )
store_x x21, 18 * portWORD_SIZE( sp )
store_x x22, 19 * portWORD_SIZE( sp )
store_x x23, 20 * portWORD_SIZE( sp )
store_x x24, 21 * portWORD_SIZE( sp )
store_x x25, 22 * portWORD_SIZE( sp )
store_x x26, 23 * portWORD_SIZE( sp )
store_x x27, 24 * portWORD_SIZE( sp )
store_x x28, 25 * portWORD_SIZE( sp )
store_x x29, 26 * portWORD_SIZE( sp )
store_x x30, 27 * portWORD_SIZE( sp )
store_x x31, 28 * portWORD_SIZE( sp )
csrr t0, mstatus
store_x t0, 29 * portWORD_SIZE( sp )
portasmSAVE_ADDITIONAL_REGISTERS
load_x t0, pxCurrentTCB
store_x sp, 0( t0 )
csrr a0, mcause
csrr a1, mepc
test_if_asynchronous:
srli a2, a0, __riscv_xlen - 1
beq a2, x0, handle_synchronous
store_x a1, 0( sp )
handle_asynchronous:
load_x sp, xISRStackTop
call xPortClearTimerIntPending
jal xTaskIncrementTick
beqz a0, processed_source
jal vTaskSwitchContext
#jal portasmHANDLE_INTERRUPT
j processed_source
handle_synchronous:
addi a1, a1, 4
store_x a1, 0( sp )
test_if_environment_call:
li t0, 11
bne a0, t0, is_exception
load_x sp, xISRStackTop
jal vTaskSwitchContext
j processed_source
is_exception:
csrr t0, mcause
csrr t1, mepc
csrr t2, mstatus
j is_exception
as_yet_unhandled:
csrr t0, mcause
j as_yet_unhandled
processed_source:
load_x t1, pxCurrentTCB
load_x sp, 0( t1 )
load_x t0, 0( sp )
csrw mepc, t0
portasmRESTORE_ADDITIONAL_REGISTERS
load_x t0, 29 * portWORD_SIZE( sp )
csrw mstatus, t0
load_x x1, 1 * portWORD_SIZE( sp )
load_x x5, 2 * portWORD_SIZE( sp )
load_x x6, 3 * portWORD_SIZE( sp )
load_x x7, 4 * portWORD_SIZE( sp )
load_x x8, 5 * portWORD_SIZE( sp )
load_x x9, 6 * portWORD_SIZE( sp )
load_x x10, 7 * portWORD_SIZE( sp )
load_x x11, 8 * portWORD_SIZE( sp )
load_x x12, 9 * portWORD_SIZE( sp )
load_x x13, 10 * portWORD_SIZE( sp )
load_x x14, 11 * portWORD_SIZE( sp )
load_x x15, 12 * portWORD_SIZE( sp )
load_x x16, 13 * portWORD_SIZE( sp )
load_x x17, 14 * portWORD_SIZE( sp )
load_x x18, 15 * portWORD_SIZE( sp )
load_x x19, 16 * portWORD_SIZE( sp )
load_x x20, 17 * portWORD_SIZE( sp )
load_x x21, 18 * portWORD_SIZE( sp )
load_x x22, 19 * portWORD_SIZE( sp )
load_x x23, 20 * portWORD_SIZE( sp )
load_x x24, 21 * portWORD_SIZE( sp )
load_x x25, 22 * portWORD_SIZE( sp )
load_x x26, 23 * portWORD_SIZE( sp )
load_x x27, 24 * portWORD_SIZE( sp )
load_x x28, 25 * portWORD_SIZE( sp )
load_x x29, 26 * portWORD_SIZE( sp )
load_x x30, 27 * portWORD_SIZE( sp )
load_x x31, 28 * portWORD_SIZE( sp )
addi sp, sp, portCONTEXT_SIZE
mret
.endfunc
第3~34行,保护现场,即将寄存器压栈。
第36行,保存额外的寄存器,这里什么都不做。
第38~39行,将sp的值保存在当前TCB的起始地址处。
第41行,读取mcause的值到a0寄存器。
第42行,读取mepc的值到a1寄存器。
第45行,将a0寄存器的值右移31位,将移位后的值写入a2寄存器。
第46行,判断a2寄存器的值是否等于0,即判断是中断(异步中断)还是异常(同步中断),如果等于0则跳转到第59行。这里假设a2的值不等于0,因此继续往下看。
第47行,将中断返回地址保存在栈顶。
第51行,使用中断栈,即在中断里有专门的栈空间来进行函数调用。
第52行,调用xPortClearTimerIntPending()函数,在这里该函数的作用是清定时器中断pending。目前在freertos这个demo里只有定时器中断,因此就没有判断是否是其他外部中断了。
第53行,调用xTaskIncrementTick()函数,这个函数的返回值决定了是否需要切换到其他任务。如果返回值为0,表示不需要切换,否则需要任务切换。
第54行,判断xTaskIncrementTick()函数的返回值是否等于0,如果是则跳转到第80行,这里假设返回值不等于0。
第55行,调用vTaskSwitchContext()函数,这个函数会切换当前TCB到将要执行的任务上。
第57行,跳转到第80行。
第81~82行,使用当前TCB的sp。
第85~86行,将中断返回地址写入mepc寄存器。
第88行,恢复额外的寄存器,这里什么都没做。
第91~122行,从栈里恢复寄存器的值,这和前面进入中断时的保存现场操作是成对的,即恢复现场。
第124行,中断返回,从mepc的值所指的地址处开始执行代码。
接下来,看回第46行,如果a2寄存器的值为0,则跳转到第第59行。
第60行,将a1的值加4,即将中断返回地址的值加4,后面会用到。
第61行,将a1的值写入sp寄存器。
第64~65行,判断a0的值是否等于11,即mcause的值是否等于11,即是否是ecall指令异常。如果不是则跳转到第70行。
第70~74行是一段死循环代码。
第66~68行,前面已经分析过了。
总结一下,移植freertos需要修改以下几个地方:
1.修改prvSetupHardware()函数,在里面做一些硬件初始化的操作。
2.修改vPortSetupTimerInterrupt()函数,在里面初始化并使能系统滴答定时器。
3.修改freertos_risc_v_trap_handler()函数,根据具体的硬件实现处理好中断返回地址和中断(异常)的判断。