热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

RISCV处理器的C语言启动代码设计方法

关注、星标公众号,不错过精彩内容作者:唐思超来源:嵌入式资讯精选随着微处理器市场竞争加剧,RISC-V指令集越来越受到关注。

关注、星标公众,不错过精彩内容

作者:唐思超

来源:嵌入式资讯精选

随着微处理器市场竞争加剧,RISC-V指令集越来越受到关注。虽然RISC-V并非第一个开源的指令集(ISA),却是第一个可依据实际应用场景灵活选择指令集的指令集架构。RISC-V指令集架构可以满足从高性能服务器CPU直至超低功耗传感器内嵌CPU的全部应用场景。

通常情况下,一款处理器的启动代码基本采用汇编语言设计。其原因包括:

  • 在处理器启动阶段,C运行环境还未初始化;

  • 汇编语言实现的代码不受编译器影响;

  • 某些特殊寄存器操作无法通过C编译得到对应汇编代码;

  • 处理器的某些特殊设计不利于C语言的使用等。

本文将解决前述问题,展示一种使用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内联汇编,实例介绍如下:

asmvolatile (      ❶
"csrw0x307, %0"    ❷
:                   ❸
:"r"(vector_base) ❹
:                   ❺
);

其中:❶ asm为GCC内联汇编关键字,volatile为修饰符;❷ 双引号引用的汇编指令列表,如有多条指令,可以使用"\n"分割;其中%0代表输入操作数列表中的第一个值;❸ 可选的输出操作数列表;❹ 可选的输入操作数列表,此处"r"代表使用编译器自动分配的寄存器来存储变量vector_base;❺ 可选的受影响寄存器列表。

(2)p与初始化

简单来讲,将目标文件中的ps链接起来就是可执行文件。在默认情况下,编译器会创建标准ps。表1是标准p的简单介绍。

表1   标准p概要

通过表1可以看出,程序的可执行代码存放于.text p,已初始化的全局和静态变量存放于.data p。

一个典型的SoC系统通常包含两类存储器,即ROM和RAM。对于当今的处理器来说,这两部分通常是Flash和SRAM。系统掉电情况下,SRAM中是无法保存数据的,因此C语言中的变量初始值需要保存于Flash中。系统上电后,由初始化代码将初始化数据从Flash拷贝到SRAM的目标地址。如前所述,这是初始化代码的重要工作之一。

接下来将阐述如何从Flash中找到初始化数据的位置并在C语言中引用。

(3)链接器变量的C语言访问

从链接器的观点看,初始值在Flash中的存放地址称为LMA(加载存储地址),对应变量在SRAM的运行时地址称为VMA(虚拟存储地址)。链接器脚本是用来描述处理器存储器分布、各p 及标准p的包含关系、相应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__。

其中:

  • __data_load_start__代表LMA地址

  • __data_start__代表VMA地址

在C语言中访问这些变量有以下两种方法:

将链接器脚本变量声明为数据类型,例如在C语言文件中声明extern uint32_t __data_load_start__;通过&__data_load_start__获取变量的值;

将链接器脚本变量声明为数组,例如在C语言文件中声明externuint32_t __data_load_start__[];通过__data_load_start__获取变量的值。

(4)函数属性

在通常情况下,编译器会为每个函数自动产生序言和结尾序列,即在函数的头部进行一些入栈操作,在函数的末尾进行对应的出栈操作。一个明显的问题就是在C语言启动代码中,复位函数执行时可能栈指针或栈寄存器还没有进行初始化,这时的栈操作极有可能会导致处理器访问非法地址而使程序崩溃。此外,如前文所提到的RISC-V处理器的复位入口可能保存的是跳转指令而不是地址,短的跳转地址可以保证用一条指令完成跳转。

鉴于上述原因,需要使用相关的函数属性来通知编译器剔除默认的函数序列并指定p,如下形式的复位函数定义可满足该要求:

void __attribute__((p(".init"),naked)) reset_handler(){
--snip--
};

三、RISC-V内核的C语言启动代码实例

前面内容介绍了相关背景知识和技术手段,下面通过一个实际的框架程序来展示RISC-V处理器的C语言启动代码。其中,代码段3是C语言启动代码的实现,代码段4是向量表。代码中的所有关键点前文均有介绍,在此不在赘述。

【代码段-3】

#include"riscv_encoding.h"
#include
--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;
}dp[3] = {--snip--
};
conststruct {uint32_t* start;uint32_t* end;
}bp[3] = {--snip--
};
void __attribute__((p(".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__ !&#61;&__RAM_segment_start__){ for(uint8_t idx &#61; 0; idx <3; idx&#43;&#43;){src&#61;dp[idx].load;dst&#61;dp[idx].start;while(dst --snip--

【代码段-4】

.p .vectors, "ax"--snip--.globl vector_base
vector_base:jreset_handler.align 2.word 0--snip--

四、结  语

通常半导体厂商会在配套的软件开发包中提供处理器的启动代码&#xff0c;这导致多数嵌入式开发人员可能更关注应用部分的代码实现而忽视启动代码的存在。鉴于厂商提供的启动代码几乎都用汇编语言编写&#xff0c;这使得很多开发人员误以为启动代码必须使用汇编语言开发。

事实上&#xff0c;大多数处理器的启动代码都可以使用C语言进行开发且代码效率与汇编几乎没有差异。在工程实践中&#xff0c;很多深层次开发都需要对启动代码进行修改或重写&#xff0c;基于C语言的代码可以节省开发人员在学习汇编指令方面的时间和精力&#xff0c;同时在后续的升级维护中更加高效。

补充知识点&#xff1a;

[1]考虑到Cortex-M系列架构的开发多使用IAR、MDK等环境&#xff0c;此处以IAR环境为例。

[2]考虑到当前RISC-V的集成开发环境多基于Eclipse构建&#xff0c;SEGGER Embedded Studio环境基于自有构架且使用方便、功能强大&#xff0c;故此处以SES为例。另外&#xff0c;包括SES在内的RISC-V开发环境下的编译系统均基于GCC&#xff0c;故本文讨论的方法也适用于其他开发环境。

[3]如果需要在GCC内联汇编代码中使用宏定义&#xff0c;就需要使用一种称为“双重宏定义”的方法&#xff0c;示例如下&#xff1a;

#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));

作者简介&#xff1a;

唐思超,现任北京知存科技有限公司软件开发经理&#xff0c;负责人工智能芯片工具链及嵌入式开发,具有14年硬件电路设计及软件开发经验&#xff0c;擅长处理器、编译系统及操作系统的相关设计开发及底层机制的综合运用。

声明&#xff1a;本文内容仅代表原创作者观点&#xff0c;如有错误敬请理解。

‧  END  

推荐阅读&#xff1a;

精选汇总 | 目录 | 搜索

研发低功耗产品&#xff0c;你遇到过哪些“坑”&#xff1f;

十年来影响最大的C&#43;&#43; 20准备发布

关注微信公众号『strongerHuang』&#xff0c;后台回复“1024”&#xff0c;查看更多精彩内容。

长按前往图中包含的公众号关注


推荐阅读
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • c语言\n不换行,c语言printf不换行
    本文目录一览:1、C语言不换行输入2、c语言的 ... [详细]
  • OO第一单元自白:简单多项式导函数的设计与bug分析
    本文介绍了作者在学习OO的第一次作业中所遇到的问题及其解决方案。作者通过建立Multinomial和Monomial两个类来实现多项式和单项式,并通过append方法将单项式组合为多项式,并在此过程中合并同类项。作者还介绍了单项式和多项式的求导方法,并解释了如何利用正则表达式提取各个单项式并进行求导。同时,作者还对自己在输入合法性判断上的不足进行了bug分析,指出了自己在处理指数情况时出现的问题,并总结了被hack的原因。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • JDK源码学习之HashTable(附带面试题)的学习笔记
    本文介绍了JDK源码学习之HashTable(附带面试题)的学习笔记,包括HashTable的定义、数据类型、与HashMap的关系和区别。文章提供了干货,并附带了其他相关主题的学习笔记。 ... [详细]
  • 如何在跨函数中使用内存?
    本文介绍了在跨函数中使用内存的方法,包括使用指针变量、动态分配内存和静态分配内存的区别。通过示例代码说明了如何正确地在不同函数中使用内存,并提醒程序员在使用动态分配内存时要手动释放内存,以防止内存泄漏。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • 本文介绍了在go语言中利用(*interface{})(nil)传递参数类型的原理及应用。通过分析Martini框架中的injector类型的声明,解释了values映射表的作用以及parent Injector的含义。同时,讨论了该技术在实际开发中的应用场景。 ... [详细]
  • 本文介绍了在实现了System.Collections.Generic.IDictionary接口的泛型字典类中如何使用foreach循环来枚举字典中的键值对。同时还讨论了非泛型字典类和泛型字典类在foreach循环中使用的不同类型,以及使用KeyValuePair类型在foreach循环中枚举泛型字典类的优势。阅读本文可以帮助您更好地理解泛型字典类的使用和性能优化。 ... [详细]
author-avatar
清潇静凌
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有