作者:唐思超 来源:嵌入式资讯精选 随着微处理器市场竞争加剧,RISC-V指令集越来越受到关注。虽然RISC-V并非第一个开源的指令集(ISA),却是第一个可依据实际应用场景灵活选择指令集的指令集架构。RISC-V指令集架构可以满足从高性能服务器CPU直至超低功耗传感器内嵌CPU的全部应用场景。 通常情况下,一款处理器的启动代码基本采用汇编语言设计。其原因包括:
本文将解决前述问题,展示一种使用C语言为RISC-V处理器设计启动代码的方法。 为了更清晰地讨论问题并最大程度的便于读者理解某些流程,本文以芯来科技基于RV32IMC指令集的N205系列内核作为目标处理器,从N205内核的对标架构——来自ARM的Cortex-M内核在IAR EmbeddedWorkbench for ARM[1](后文简称IAR)环境下的C语言启动代码切入,逐步引入并实现SEGGER Embedded Studio[2](后文简称SES)环境下N205系列内核的C语言启动代码。 一、Cortex-M内核在IAR环境下的C语言启动代码 Cortex系列内核是ARM公司迄今为止最成功的系列产品,包括A、R、M三类,其中M系列主要针对微控制器市场。 Cortex-M内核具有以下特点:
代码段1所示内容是Cortex-M内核在IAR环境下使用C语言开发的启动代码。 【代码段-1】 #pragma language=extended ❶
--snip-- voidResetISR(void); ❷ --snip-- externvoid __iar_program_start(void); ❸ staticunsigned long pulStack[64] @'.noinit'; ❹ typedefunion ❺ { void (*pfnHandler)(void); unsigned long ulPtr; } uVectorEntry; __rootconst uVectorEntry __vector_table[] @'.intvec' = ❻ { { .ulPtr = (unsigned long)pulStack +sizeof(pulStack) }, ❼ ResetISR, --snip-- }; --snip-- voidResetISR(void) { __iar_program_start(); } 此处对上述代码做简要分析: ❶是IAR的#pragma指导符。 ❷是复位函数声明,复位函数是处理器复位后首先执行的代码,有时也称为复位入口函数。 ❸是IAR系统函数声明,__iar_program_start是IAR的系统函数,主要作用是执行C运行环境初始化并调用系统主函数main。 ❹使用IAR @操作符定义系统栈区。 ❺声明向量表的联合类型。 ❻使用IAR对象属性声明__root及@操作符定义向量表,其中,第一个元素❼保存了栈底地址,后续元素均为函数地址。 从上述分析过程可以看出启动代码的必要工作包括定义栈区、定义并初始化向量表,定义并实现系统复位函数,初始化栈指针或栈寄存器等。依据处理器的架构不同,上述操作中某些过程需要由软件完成,有些则由硬件自动加载。 另外,有关IAR的指导符、对象属属性等内容不属于本文讨论范畴,有需要可自行查阅。这里给出两点提示:IAR环境的编译系统为IAR自行开发,故示例代码中的指导符号不适用于GCC;某些指导符会因IAR环境版本不同而有所差异。 二、在SES环境下实现RISC-V内核C语言启动代码的必要知识 前文提到,RISC-V是指令集而不是具体的设计实现,这与之前讨论的Cortex-M系列内核有很大不同。简单地说,不同厂商基于同种Cortex-M内核的处理器,仅从内核的层面来看可能没有太大差异,但不同厂商开发的具有相同指令集的RISC-V处理器则各有千秋:一方面是相同功能的具体实现可能不同;另一方面,不同厂商可以实现不同的指令扩展。 这里对比Cortex-M内核,列举RISC-V处理器的一些特点:不同厂商中断控制器的实现各有特色;中断响应时,处理器硬件不会保存上下文,需要软件完成该功能;向量表依据厂商不同而有明显差异,可能向量表的首地址保存的是指令而非地址。 在不同厂商的Cortex-M内核处理器间作切换时,由于处理器内核的一致性,启动代码几乎无需改动,因而使用汇编或者C语言来设计启动代码似乎差异不大,但要降低在不同厂商的RISC-V处理器间切换的复杂度,使用C语言开发启动代码是一种有效途径。 前文曾提到启动代码的必要工作包括定义栈区、定义并初始化向量表,定义并实现系统复位函数,初始化栈指针或栈寄存器等。在前述Cortex-M内核的C启动代码中,IAR提供了接口__iar_program_start,该接口隐藏了几乎所有细节。在SES环境下并没有这样的接口可供使用,为了实现RISC-V处理器的C语言启动代码,需要如下的编译器及链接器相关知识。 (1)GCC内联汇编 RISC-V处理器中的CSR寄存器需要特殊的指令才能进行访问,C编译器无法产生类似的指令,故C语言启动代码中仍然需要插入数条汇编指令。为了实现汇编指令与C语言的交互,需要使用GCC内联汇编,实例介绍如下:
其中:❶ asm为GCC内联汇编关键字,volatile为修饰符;❷ 双引号引用的汇编指令列表,如有多条指令,可以使用'\n'分割;其中%0代表输入操作数列表中的第一个值;❸ 可选的输出操作数列表;❹ 可选的输入操作数列表,此处'r'代表使用编译器自动分配的寄存器来存储变量vector_base;❺ 可选的受影响寄存器列表。 (2)section与初始化 简单来讲,将目标文件中的sections链接起来就是可执行文件。在默认情况下,编译器会创建标准sections。表1是标准section的简单介绍。 表1 标准section概要 通过表1可以看出,程序的可执行代码存放于.text section,已初始化的全局和静态变量存放于.data section。 一个典型的SoC系统通常包含两类存储器,即ROM和RAM。对于当今的处理器来说,这两部分通常是Flash和SRAM。系统掉电情况下,SRAM中是无法保存数据的,因此C语言中的变量初始值需要保存于Flash中。系统上电后,由初始化代码将初始化数据从Flash拷贝到SRAM的目标地址。如前所述,这是初始化代码的重要工作之一。 接下来将阐述如何从Flash中找到初始化数据的位置并在C语言中引用。 (3)链接器变量的C语言访问 从链接器的观点看,初始值在Flash中的存放地址称为LMA(加载存储地址),对应变量在SRAM的运行时地址称为VMA(虚拟存储地址)。链接器脚本是用来描述处理器存储器分布、各section 及标准section的包含关系、相应LMA及VMA地址或存放区域等的文件。 代码段2是一个标准链接器脚本的片段。这里通过这个片段来讲述链接器变量的C语言访问。 【代码段-2】 MEMORY { --snip-- } SECTIONS { --snip-- __data_load_start__ = ALIGN(__srodata_end__ ,4); .data ALIGN(__RAM_segment_start__ , 4) :AT(ALIGN(__srodata_end__ , 4)) { __data_start__ = .; *(.data .data.*) } __data_end__ = __data_start__ +SIZEOF(.data); __data_size__ = SIZEOF(.data); __data_load_end__ = __data_load_start__ +SIZEOF(.data); --snip-- } 在代码段2中,定义了链接器脚本变量__data_load_start__、__data_start__及__data_end__。 其中:
在C语言中访问这些变量有以下两种方法: 将链接器脚本变量声明为数据类型,例如在C语言文件中声明extern uint32_t __data_load_start__;通过&__data_load_start__获取变量的值; 将链接器脚本变量声明为数组,例如在C语言文件中声明externuint32_t __data_load_start__[];通过__data_load_start__获取变量的值。 (4)函数属性 在通常情况下,编译器会为每个函数自动产生序言和结尾序列,即在函数的头部进行一些入栈操作,在函数的末尾进行对应的出栈操作。一个明显的问题就是在C语言启动代码中,复位函数执行时可能栈指针或栈寄存器还没有进行初始化,这时的栈操作极有可能会导致处理器访问非法地址而使程序崩溃。此外,如前文所提到的RISC-V处理器的复位入口可能保存的是跳转指令而不是地址,短的跳转地址可以保证用一条指令完成跳转。 鉴于上述原因,需要使用相关的函数属性来通知编译器剔除默认的函数序列并指定section,如下形式的复位函数定义可满足该要求:
三、RISC-V内核的C语言启动代码实例 前面内容介绍了相关背景知识和技术手段,下面通过一个实际的框架程序来展示RISC-V处理器的C语言启动代码。其中,代码段3是C语言启动代码的实现,代码段4是向量表。代码中的所有关键点前文均有介绍,在此不在赘述。 【代码段-3】 #include'riscv_encoding.h' #include<stdint.h> --snip-- externuint32_t __data_load_start__; --snip-- externuint32_t __bss_start__; --snip-- externvoid (*const vector_base[])(void); externvoid main(void); --snip-- conststruct { uint32_t* load; uint32_t* start; uint32_t* end; }dsection[3] = { --snip-- }; conststruct { uint32_t* start; uint32_t* end; }bsection[3] = { --snip-- }; void __attribute__((section('.init'),naked)) reset_handler() { register uint32_t *src, *dst; --snip-- /* 嵌入汇编 */ asm volatile('csrw 0x307,%0'::'r'(vector_base)); --snip-- asm volatile('la gp, __sdata_start__+0x800'); asm volatile('la sp,__stack_end__'); --snip-- /* 进行系统时钟初始化等 */ init(); /* 将数据的初始化值拷贝至RAM */ if(&__vectors_load_start__ !=&__RAM_segment_start__){ for(uint8_t idx = 0; idx < 3; idx++){ src=dsection[idx].load; dst=dsection[idx].start; while(dst < dsection[idx].end){ *dst=*src; dst++; src++; } } } /* 将.bss区域清零 */ for(uint8_t idx=0;idx < 3;idx++){ dst=bsection[idx].start; while(dst<bsection[idx].end){ *dst=0U; dst++; } } /* 调用主函数 */ main(); } --snip-- 【代码段-4】
四、结 语 #defineCSR_MTVT 0x307 #defineSTR(R) #R #defineXSTR(R) STR(R) /*asm volatile('csrw 0x307, %0'::'r'(vector_base)); */ asmvolatile('csrw 'XSTR(CSR_MTVT)',%0'::'r'(vector_base)); 作者简介: 唐思超,现任北京知存科技有限公司软件开发经理,负责人工智能芯片工具链及嵌入式开发,具有14年硬件电路设计及软件开发经验,擅长处理器、编译系统及操作系统的相关设计开发及底层机制的综合运用。 |
|
来自: 西北望msm66g9f > 《编程》