嵌入式开发的特点进行简单的科普与回顾

本文为risc-v嵌入式开发准备篇2:嵌入式开发的特点介绍。
本文的目的是对嵌入式开发的特点进行简单的科普与回顾,为后续详细介绍“risc-v gcc工具链”和“risc-v汇编语言程序设计”打下基础。注:本文力求通俗易懂,主要面向初学者,对嵌入式开发有所了解的读者可以忽略此文。
在本号上次发表的文章《编译过程简介》中介绍过,嵌入式系统的程序编译过程和开发有其特殊性,譬如:
嵌入式系统需要使用交叉编译与远程调试的方法进行开发。
需要自己定义引导程序。
需要注意减少代码体积(code size)。
需要移植printf从而使得嵌入式系统也能够打印输入。
使用newlib作为c运行库。
每个特定的嵌入式系统都需要配套的板级支持包。
下文将分别予以介绍。
1 交叉编译和远程调试
在本号上次发表的文章《编译过程简介》中介绍了如何在linux系统的pc电脑上开发一个hello world程序,对其进行编译,然后运行在此电脑上。在这种方式下,我们使用pc电脑上的编译器编译出该pc电脑本身可执行的程序,这种编译方式称之为本地编译。
嵌入式平台上往往资源有限,嵌入式系统(譬如常见arm mcu或8051单片机)的存储器容量通常只在几kb到几mb之间,且只有闪存而没有硬盘这种大容量存储设备,在这种资源有限的环境中,不可能将编译器等开发工具安装在嵌入式设备中,所以无法直接在嵌入式设备中进行软件开发。因此,嵌入式平台的软件一般在主机pc上进行开发和编译,然后将编译好的二进制代码下载至目标嵌入式系统平台上运行,这种编译方式属于交叉编译。
交叉编译可以简单理解为,在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序,譬如,在x86平台的pc电脑上编写程序并编译成能运行在arm平台的程序,编译得到的程序在x86平台上不能运行,必须放到arm平台上才能运行。
与交叉编译同理,在嵌入式平台上往往也无法运行完整的调试器,因此当运行于嵌入式平台上的程序出现问题时,需要借助主机pc平台上的调试器来对嵌入式平台进行调试。这种调试方式属于远程调试。
常见的交叉编译和远程调试工具是gcc和gdb。在本号上次发表的文章《编译过程简介》中介绍了如何使用linux自带的gcc本地编译一个hello world程序并运行。但是,gcc不仅能作为本地编译器,还能作为交叉编译器;同理gdb不仅可以作为本地调试器,还可以作为远程调试器。
当作为交叉编译器之时,gcc通常有不同的命名,譬如:
arm-none-eabi-gcc和arm-none-eabi-gdb是面向裸机(bare-metal)arm平台的交叉编译器和远程调试器。
所谓裸机(bare-metal)是嵌入式领域的一个常见形态,表示不运行操作系统的系统
而riscv-none-embed-gcc和riscv-none-embed-gdb是面向裸机risc-v平台的交叉编译器和远程调试器。
本号后续发文《risc-v gcc工具链的介绍》将介绍risc-v gcc工具链的更多信息。
2 移植newlib或newlib-nano作为c运行库
newlib是一个面向嵌入式系统的c运行库。相对于本号上次发表的文章《编译过程简介》中介绍的glibc,newlib实现了大部分的功能函数,但体积却小很多。newlib独特的体系结构将功能实现与具体的操作系统分层,使之能够很好地进行配置以满足嵌入式系统的要求。由于专为嵌入式系统设计,newlib具有可移植性强、轻量级、速度快、功能完备等特点,已广泛应用于各种嵌入式系统中。
由于嵌入式操作系统和底层硬件的多样性,为了能够将c/c++语言所需要的库函数实现与具体的操作系统和底层硬件进行分层,newlib的所有库函数都建立在20个桩函数的基础上,这20个桩函数完成具体操作系统和底层硬件相关的功能:
i/o和文件系统访问(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);
扩大内存堆的需求(sbrk);
获得当前系统的日期和时间(gettimeofday、times);
各种类型的任务管理函数(execve、fork、getpid、kill、wait、_exit);
这20个桩函数在语义、语法上与posix(portable operating system interface of unix)标准下对应的20个同名系统调用完全兼容。
所以,如果需要移植newlib至某个目标嵌入式平台,成功移植的关键是在目标平台下找到能够与newlib桩函数衔接的功能函数或者实现这些桩函数。本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将介绍蜂鸟e200的hbird-e-sdk平台如何实现移植实现newlib的桩函数。
注意:newlib的一个特殊版本newlib-nano版本进一步为嵌入式平台减少了代码体积(code size),因为newlib-nano提供了更加精简版本的malloc和printf函数的实现,并且对库函数使用gcc的-os(侧重代码体积的优化)选项进行编译优化。
3 嵌入式引导程序和中断异常处理
在本号上次发表的文章《编译过程简介》中介绍了如何在linux系统的pc电脑上开发一个hello world程序,对其进行编译,然后运行在此电脑上。在这种方式下,程序员仅仅只需要关注hello world程序本身,程序的主体由main函数组织而成,程序员可以无需关注linux操作系统在运行该程序的main函数之前和之后需要做什么。事实上,在linux操作系统中运行应用程序(譬如简单的hello world)时,操作系统需要动态地创建一个进程、为其分配内存空间、创建并运行该进程的引导程序,然后才会开始执行该程序的main函数,待其运行结束之后,操作系统还要清除并释放其内存空间、注销该进程等。
从上述过程中可以看出,程序的引导和清除这些“脏活累活”都是由linux这样的操作系统来负责进行。但是在嵌入式系统中,程序员除了开发以main函数为主体的功能程序之外,还需要关注如下两个方面:
引导程序:
嵌入式系统上电后需要对系统硬件和软件运行环境进行初始化,这些工作往往由用汇编语言编写的引导程序完成。
引导程序是嵌入式系统上电后运行的第一段软件代码。引导程序对于嵌入式系统非常关键,引导程序所执行的操作依赖于所开发的嵌入式系统的软硬件特性,一般流程包括:初始化硬件、设置异常和中断向量表、把程序拷贝到片上sram中、完成代码的重映射等,最后跳转到main函数入口。
本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk平台的引导程序实例了解引导程序的更多细节。
中断异常处理
中断和异常是嵌入式系统非常重要的一个环节,因此,嵌入式系统软件还必须正确地配置中断和异常处理函数。有关risc-v架构的中断和异常的详细信息,请参见risc-v中文书籍《手把手教你设计cpu——risc-v处理器篇》 中第13章内容《不得不说的故事——中断和异常》。
本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk程序实例了解如何配置中断和异常处理函数。
4 嵌入式系统链接脚本
在本号上次发表的文章《编译过程简介》中介绍了如何在linux系统的pc电脑上开发一个hello world程序,对其进行编译,然后运行在此电脑上。在这种方式下,程序员也无需关心编译过程中的“链接”这一步骤所使用的链接脚本,无需为程序分配具体的内存空间。
但是在嵌入式系统中,程序员除了开发以main函数为主体的功能程序之外,还需要关注“链接脚本”为程序分配合适的存储器空间,譬如程序段放在什么区间、数据段放在什么区间等等。
本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk的“链接脚本”实例了解更多细节。
5 减少代码体积
嵌入式平台上往往存储器资源有限,嵌入式系统(譬如常见的arm mcu或8051单片机)的存储器容量通常只在几kb到几mb之间,且只有闪存而没有硬盘这种大容量存储设备,在这种资源有限的环境中,程序的代码体积(code size)显得尤其重要,因此,有效地降低降低代码体积(code size)是嵌入式软件开发必须要考虑的问题,常见的方法如:
使用newlib-nano作为c运行库以取得较小代码体积(code size)的c库函数。
尽量少使用c语言的大型库函数,譬如在正式发行版本的程序中避免使用printf和scanf等函数。
如果在开发的过程中一定需要使用printf函数,可以使用某些自己实现的阉割版printf函数(而不是c运行库中提供的printf函数)以生成较小的代码体积。
除此之外,在c/c++语言的语法和程序开发方面也有众多技巧以取得更小的代码体积(code size)。
本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk平台实例了解更多“减少代码体积”的实现细节。减小代码体积(code size)的方法很多,本文在此不做一一赘述,请初学的读者自行查阅相关资料进行学习。
6 支持printf函数
在本号上次发表的文章《编译过程简介 》中介绍了如何在linux系统的pc电脑上开发一个hello world程序,程序中使用c语言的标准库函数printf打印了一个“hello world”字符串。该程序在linux系统里面运行的时候字符串被成功的输出到了linux的终端界面上。在这个过程中,程序员无需关心linux系统到底是如何将printf函数的字符串输出到linux终端上的。事实上,如《编译过程简介》中所述,在linux本地编译的程序会链接使用linux系统的c运行库glibc,而glibc充当了应用程序和linux操作系统之间的接口,glibc提供的 printf 函数就会调用如sys_write等操作系统的底层系统调用函数,从而能够将“字符串”输出到linux终端上。
从上述过程中可以看出,由于有glibc的支持,所以printf函数能够在linux系统中正确的进行输出。但是在嵌入式系统中,printf的输出却不那么容易了,基于如下几个原因:
嵌入式系统使用newlib作为c运行库,而newlib的c运行库所提供的printf函数最终依赖于如本文中所介绍的newlib桩函数write,因此必须实现此write函数才能够正确的执行printf函数。
嵌入式系统往往没有“显示终端”存在,譬如常见的单片机其作为一个黑盒子一般的芯片,根本没有显示终端。因此,为了能够支持显示输出,通常需要借助单片机芯片的uart接口将printf函数的输出重新定向到主机pc的com口上,然后借助主机pc的串口调试助手显示出输出信息。同理,对于scanf输入函数,也需要通过主机pc的串口调试助手获取输入然后通过主机pc的com口发送给单片机芯片的uart接口。
从以上两点可以看出,嵌入式平台的uart接口非常重要,往往扮演了输出管道的角色,为了能够将printf函数的输出定向到uart接口,需要实现newlib的桩函数write,使其通过编程uart的相关寄存器将字符通过uart接口输出。本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk平台移植printf函数的实例了解更多细节。
7 提供板级支持包
对于特定的嵌入式硬件平台,为了方便用户在硬件平台上开发嵌入式程序,硬件平台一般会提供板级支持包(board support package,bsp)。板级支持包所包含的内容没有绝对的标准,通常说来,其必须包含如下内容:
底层硬件设备的地址分配信息
底层硬件设备的驱动函数
系统的引导程序
中断和异常处理服务程序
系统的链接脚本
如果使用newlib作为c运行库,一般还提供newlib桩函数的实现。
由于板级支持包往往会将很多底层的基础设施和移植工作搭建好,因此应用程序开发人员通常都无需关心本文第1.2节至第1.6节中描述的内容,能够从底层细节中被解放出来避免重复建设而出错。本号后续发文《基于hbird-e-sdk平台的软件开发与运行》将结合hbird-e-sdk平台的bsp实例了解更多细节。

WTV语音芯片概述及功能特点
中国人工智能发展面临怎样的障碍
D38N电流钳的性能特点及应用范围
世界粮食日 | 智能技术如何惠及更多种植者,将一杯橙汁更好地送到大洋彼岸
DRV10983-Q1主要特性_功能框图
嵌入式开发的特点进行简单的科普与回顾
中创新航蓄力万亿储能赛道攻势
陶大程:机器人商业化大势所趋但基础很重要
中国芯片巨头台积电市值突破4.2万亿
海信电视亮相阿那亚实验电影周,超级影像嘉年华即将开启
光电式液位传感器能抗腐蚀吗?
石墨烯的用途是什么,它可以用来取暖吗
进入机器学习时代,怎样才能抓住机遇?
小米MIX2什么时候上市?小米MIX2最新设计图来了,超过95%的屏占比,骁龙835+8GB大运存还有谁?
TL431稳压芯片的工作过程,TL431稳压芯片的好坏检测
控制器是芯片吗 微控制器和芯片的关系 微控制器和微处理器区别
中国最幸运的城市,拥有一家绝顶聪明的公司,或许成下一个杭州
贵州省委书记在航天电器调研军民融合发展
荣湃数字隔离器的特点及在72V额定电压BMS中的应用
三星推出980 Pro M.2 NVMe SSD 增加了对PCIe 4.0的支持