从现在开始将正式进入到riscv从零开始写操作系统的实践。本次实践内容参考《循序渐进,学习开发一个RISC-V上的操作系统 - 汪辰》汪老师的课程,将本来在qemu上运行的rvos系统一步步移植到沁恒的CH32V307单片机上。
一、编写初始链接脚本
首先在编写链接脚本时,需要明确链接脚本的作用到底是什么?其主要作用就是用于描述输入文件的段如何被映射到输出文件中,并控制输出文件的内存分布。
并且链接器(ld)总是会使用链接脚本的。但是当我们使用gcc编译链接一个程序时,并没有指定链接脚本,这又是为什么呢?因为链接器中有一个默认链接脚本,当未指明链接脚本时,将采用默认链接脚本(默认链接脚本可通过ld --verbose命令查看)。同时我们可以在ld链接时,使用-T选项来指定我们的链接脚本文件。
所以由此可知,通过查看链接文件,我们基本就可以获取到代码的映射排列顺序以及板卡的内存分布情况。接下来将编写一个简单的链接脚本来进行说明(随着后续代码的完善,此链接脚本也将添加更多的内容):
/* Simple Linker Script */
/* 分配2kB内存作为栈空间使用 */
PROVIDE(_stack_size = 2048);
/* 设置入口点 */
ENTRY(_start)
/* Memory Layout */
MEMORY
{
/* Define the program memory layout */
FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 256K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K
}
/* Sections */
SECTIONS
{
/* Section for text (code) */
.text :
{
/* Entry point and code */
*(.text)
} >FLASH AT>FLASH
/* Section for data */
.data :
{
/* Initialized data */
*(.data)
} >RAM AT>FLASH
/* Section for uninitialized data */
.bss :
{
/* Uninitialized data */
*(.bss)
} >RAM AT>FLASH
/* Section for stack */
/*
输出段格式为:SECTION [ADDRESS] [(TYPE)] : [AT(LMA)]
其中SECTION为输出文件的段名,后面的都为可选项,其中ADDRESS为输出段的内存运行地址
*/
/*
输入段格式为,举代码段为例:*(.text)
其中*号表示所有的输入文件,即.o文件;.text表示输入文件的代码段。
*/
.stack ORIGIN(RAM) + LENGTH(RAM) - _stack_size :
{
/* stack */
PROVIDE(_heap_end = .);
PROVIDE(_stack_start = .);
. += _stack_size;
PROVIDE(_stack_end = .);
} >RAM
}
- 设置栈区大小为2KB空间;
- 设置代码的入口点为_start;
- 将输出文件分为4个段,分别为代码段、数据段、bss段和栈区段;
- 同时将栈区的起始地址划分到ram的62KB处。
以上就完成了一个简单的链接脚本的编写,并且通过此链接文件可以得到程序在板卡上的内存分布情况:
二、简单的boot code编写
编写完链接脚本后,开始进行简单的boot code代码的编写。此代码需要实现的功能就是从汇编代码跳转到c代码开始执行,并点亮开发板上的一个LED灯。
要想从汇编跳转到c代码,首先需要知道c语言执行环境的必要条件。C 代码中的函数调用、局部变量等都是通过栈来管理的,所以在执行c代码之前,首先需要设置cpu的sp寄存器,即设置栈指针。
boot代码如下所示:
.global _start
# 以下内容都将存放在代码段
.text
_start:
# 设置栈指针地址,_stack_end变量已经在链接文件中定义
la sp, _stack_end
# 跳转到c代码执行
j start_kernel
通过以上代码,可以直接跳转到c语言环境中执行程序。而这里需要注意的是设置栈指针时,是采用_stack_start还是_stack_end,这需要根据操作系统和CPU来决定;对于riscv来说,栈是向下增长(即向低地址增长),是自顶向下的,所以这里需要将栈的高地址设置给sp寄存器。(其实riscv中有控制状态寄存器可以用于设置栈的增长方向)
而c代码的内容如下所示:
//设置GPIO_PD3为输出模式,并根据传参来设置gpio的输出电平
void led2_ctrl(int on)
{
//使能GPIO模块的时钟
*(unsigned int *)0x40021018 |= 0x00000020;
//设置GPIO管脚为输出模式
*(unsigned int *)0x40011400 &= ~0x0000F000;
*(unsigned int *)0x40011400 |= 0x00003000;
//设置GPIO管脚的输出电平
if(on)
*(unsigned int *)0x4001140C |= 0x00000008;
else
*(unsigned int *)0x4001140C &= ~0x00000008;
}
void start_kernel(void)
{
//设置GPIO_PD3为高电平输出,点亮led灯
led2_ctrl(1);
while (1) {} // stop here!
}
以上代码并没有对MCU进行其他操作,比如时钟分频、中断配置、data数据段搬运以及bss段清零等。只进行了最简单的汇编到c的跳转以及gpio管脚的设置,并且经过实践运行是切实可行的。而在之后的专题中,将一步步的完善相关功能。