ARM嵌入式系统移植学习
2021-08-13 22:11:53 # 嵌入式 # GNU/Linux # Linux

Arm嵌入式系统移植学习记录

说明

此文章仅记录本人的学习ARM嵌入式系统移植学习.
参考书籍和文章
《ARM嵌入式系统移植实战开发》 韩少云,奚海蛟,谌利编著

RISC-V架构与嵌入式开发快速入门 胡振波

目录

  • 各类嵌入式系统介绍及其移植的概述
  • 构建嵌入式Linux开发环境(交叉编译环境,可用wsl或者虚拟机程序)
  • Bootloader移植(u-boot移植)
  • Kernel移植(ARM移植)
  • 文件系统移植(busybox)
  • 设备驱动移植
  • 应用程序移植
  • Android在Soc上的移植

各类嵌入式系统概述

嵌入式系统的需要

我们知道,由于嵌入式设备的各类要求,例如体积、温度、可靠性,成本以及功耗等,使得嵌入式设备从”出生”起,就不是像PC的命。

在上个世纪70年代,单片机的出现,这类就是最早的嵌入式设备。由于工业上,科研上的各种设备要求,使得嵌入式设备大量的生产并投入应用,并且为了解决通用的复杂问题,出现了运行在嵌入式设备上的操作系统。嵌入式设备到现在,数量远超于PC机。

由于嵌入式设备的性能往往远不如PC,但是也需要系统去实现复杂的业务,所以对应的系统不能像PC那样巨大。由此,嵌入设设备的系统通常而言会有一些这样的特点:

  • 专用性:有时候只是为了针对某一种特定的任务。
  • 架构的精简:由于低功耗,小体积,高性能等要求,这类设备的架构不再是CISC或者十分多流水线的RISC,而是很精简的架构设计。
  • 低成本:这类系统通常是针对性的,不会去开发很大体量的系统。
  • 实施约束:即类RTOS,实时操作系统。
  • 多任务
  • 低功耗
  • 容易移植开发

各类嵌入式系统

VxWorks 操作系统是美国WindRiver公司设计开发的,是一款实时操作系统(RTOS).
其优点如下:

  • 可靠性:由于其很高的可靠性,这个系统往往被应用在卫星,探测车、火箭、导弹、战斗机等军事用途。
  • 实时性:对任务控制采用优先级抢占和轮转调度机制(后面介绍)
  • 可裁剪性:其内核最小为8K,而且可以根据需要去添加必要的模块,总的来说,这个系统的占用存储空间很小。

μC/OS - Ⅱ 其前身是μC/OS,最早由一个美国嵌入式系统专家在一个行业杂志上刊登并且连载,并且源码也在其上。μC/OS - Ⅱ 是由ANSI C开发的,其源码里包含了一些和处理器类型相关的代码,使其可以在很多处理器上运行,这也是RTOS。并且得到了很广泛的应用。

Linux 这里的Linux指的是将GNU/Linux进行剪切修改,使其能在嵌入设备上运行的系统。由于Linux的知名度,我在此就简单介绍一下一些参数。

  • 生态很好
  • 开源免费
  • kernel小,效率高:系统内核最小约134KB,一个带中文和图形的用户界面核心也可以做到不到1MB.
  • 成熟稳定,但不是RTOS
  • 最TCP/IP有完备的支持

完整嵌入式设备组成

  • 微处理器/核心控制器(Soc/MCU)
  • 外围的硬件设备(存储设备、计算加速设备、显示设备、电源等)
  • 嵌入式操作系统
  • 特定的应用程序(在系统中开发)

要求:嵌入式操作系统可以控制外围硬件设备的,比如控制电源,显示等,所以需要对应的驱动程序。微处理器/核心控制器和外围的硬件设备通过总线相连。

注:嵌入式设备通常用板载的Flash或者外界SD卡,但Flash的容量通常不高。

嵌入式系统的移植

这篇文章的主要内容是:利用Linux内核剪裁出可以在特定的开发板上运行的系统,然后配置特定开发板的编译环境,将剪裁出的Linux进行编译,并且让裁剪后的Linux在特定的开发板上跑起来。

首先,我们要确定我们裁剪出的系统的基本有哪些结构:

  • Bootloader:u-boot 用于引导开发板运行系统的代码,并且初始化各种参数
  • Kernel:裁剪出的内核
  • 根文件系统(rootfs)
  • 应用程序(如WebServer)

Bootloader
当开发板上电以后,会通过Bootloader引导系统内核。开发板的核心型号不同,往往bootloader也不同,所以需要根据所用的开发板的手册去配置bootloader。
但是也有技巧,比如现在我有开发板A,bootloader没有自带这个开发板的配置,但是支持和开发板A相同系列的B,那么我们可以把B的配置进行修改,修改的根据就是A的手册。

当开发板A的bootloader配置好以后,将其程序烧录到开发板中。

内核kernel
内核有着自己的结构层次,其中有进程管理,内存管理,文件系统最基本的三个子系统。
而用户程序通过系统的API去访问内核资源,此处要注意,我们使用的内核是对标准的linux内核进行修剪得到的。

文件系统rootfs
嵌入式Linux不可缺少的一部分,其本质就是将linux的根目录进行修剪,更改得到(使用busybox进行修剪).

应用程序
根据需要,添加移植对应的应用程序,如SSH,WebServer、database,GCC等。

构建嵌入式Linux开发环境

交叉编译环境介绍

我们要在PC上编译特定开发板的系统,那么就不能使用PC平台上的X86架构的编译器,而是要得到可以编译出开发板能跑的程序。
所以,像这类 在一个平台上编译得到可以在另一个与之不同的平台上运行的程序称为交叉编译

如果你选择了一款开发板,这款开发板的厂商通常会提供这种编译工具,你只需要搭建好对应的环境就好。
比如我用arm架构的开发板,arm会提供对应的交叉编译的一系列工具;我用RISC-V的开发板,会有针对RISC-V的交叉编译一系列的工具。
这种整套的交叉编译工具称为工具链

通常工具链称为某某某 GCC工具链,GCC其实并不是只是针对C语言的编译工具,而是一种编译工具的集合。
比如在一个GCC工具链中有:

  • GCC: 用于将C/C++语言编写的程序转化为开发板能执行的二进制代码

  • binutils: 一组二进制处理工具,其中包括addr2line,ar、objcopy、as、ld、ldd,readelf和size等工具。

    • as: 主要用于汇编
    • ld:主要用于链接
    • ar:主要用于创建静态库(与动态库不同),在windows下静态库后缀为.lib动态库为.dll,而在Linux下静态库为.a,动态库(共享库)为.so 【关于静态库和动态库的区别的具体工具分析会另外讲】
    • ldd:查看可以执行程序所依赖的动态库.
    • objcopy: 将一种对象文件翻译成另一种,例如将.bin转换为.elf,或者反过来。
    • objump: 用于反汇编。
    • readelf:读取.elf文件的信息
    • size: 列出可执行文件的每个部分的信息,如总尺寸,代码段,数据段等。
  • C 运行库:如果你看过C标准库的文件,你会发现标准库都只是定义的函数,并没有具体的实现代码。这是因为C的标准库的实现代码并不是在C标准库里,而是在另一个库里。这个库称为C 运行时库或者称为C运行库。也就是说,当C编写的代码在进行编译时,编译器会把这个库引用以下,然后程序在这个平台运行的时候,会调用这些库。你会发现这似乎是动态库,因为是在运行的时候才调用,而不是编译的时候就写进程序里。没错,这里就是共享库(动态库)。如果你想让这些库都写进程序里,而不是通过共享库调用,你可以给编译器指定参数,将所有用到的库都写进了一个二进制程序里。这也会导致你编译出来的程序会很大。

    • C标准库其实本质只是调用了这些C 运行库。而这些C 运行库通常有以下这些:glibc(Linux下的)uclibklibcLinux libc其中glibc使用最广泛。
  • GDB:用于对程序的调试

简单介绍编译过程

为了后面更加容易理解交叉编译的整个过程,我们要对一个简单的程序的编译过程进行理解和分析。

首先说一下编译一段C代码的整个过程:
预处理 -> 编译 -> 汇编 -> 链接

实验所的准备代码

1
2
3
4
5
6
// file: hello.c
#include <stdio.h> // 引入"standard input and output"
int main(void){
printf("Hello World \n");
return 0;
}

预处理(Preprocessing)
代码写好以后,要通过编译器,而在编译器里第一个流程就是预处理。
其内容主要包括:

  • 将宏展开进行处理,比如将所有的#define删除,并展开。
  • 处理#include 预编译指令,将包含的文件插入到该编译指令的位置
  • 删除所有的注释
  • 添加行号和文件标识,以防报错来提醒Coder哪里报错,为什么报错
  • 保留所有的#pragma 编译器指令

使用gcc对代码进行预处理
gcc -E hello.c -o hello.i // 将源代码文件预处理
avater

编译后,得到的hello.i就是预处理后的文件。
avater

编译 (Compilation)
编译就是对预处理后的hello.i进行语法分析,词法分析,语义分析及优化等(具体可以学习编译原理),然后生成相应的汇编代码,此处的汇编代码是根据编译器而得到。比如,在arm和x86两个不同平台的编译器下,得到的汇编代码就不同,因为这两个最主要的区别就是指令集的区别(ISA),也可以说汇编代码及其实现的不同。

使用gcc对hello.i进行编译
gcc -S hello.i -o hello.s // 将预处理文件hello.i编译成汇编文件hello.s
avater

编译后,得到汇编文件hello.s
avater

汇编 (Assembly)
汇编过程调用汇编代码(hello.s)进行处理,生成处理器能识别的指令,保存为后缀为.o目标文件中。从这里以后,就不再是C语言的天下了,而是汇编的天下。
接下来,就是正常编译汇编代码的流程了。

这里针对汇编代码有两个方法:

  • 使用gcc进行汇编,将hello.s汇编为hello.o文件
  • 使用binutils的工具as,将hello.s汇编为hello.o文件
    具体使用如下:
    gcc版:gcc -c hello.s -o hello_gcc.o
    as版: as -c hello.s -o hello_as.o
    avater

链接(Linking)
首先要知道,经过汇编后的目标文件还不能直接执行,为了变成能够被加载的可执行文件,链接需要做下面的工作:

  • 加入系统给定的信息头
  • 与系统提供的启动代码链接起来
    由于链接的话,需要链接个各种依赖文件,引导程序和链接脚本,所以如果使用ld进行链接的话就会很麻烦。(其实是我懒得查了)
    所以,此处直接使用gcc原本的直接编译的方法,直接编译为elf格式的可执行文件。
    avater

此处要注意:由于elf格式的可执行文件是不能直接以普通文本查看的(打开会乱码)。

静态链接和动态链接(静态库和动态库)
前面我们说到,程序的库有静态库和动态库,与之相对应的就是静态链接与动态链接。
静态链接:编译阶段直接把静态库加入到可执行文件里。
动态链接:编译阶段只是把库链接到可执行文件里,程序执行的时候才会去调用动态库。

可以使用ldd来查看一个可执行文件依赖的动态库。
ldd hello
avater
使用size查看可执行文件的一些参数
size hello
avater

当然,如果你想要通过gcc编译一个不依赖动态库的可执行文件,可以通过gcc添加参数设置。
gcc -static hello.c hello // 通过 -static 参数设置静态编译
avater

分析可执行文件elf
首先要知道,在elf之前,还有一种常见的二进制文件格式.bin

区分一下:
(bin)binary:文件中只含有机器码
elf:文件中含机器码、段加载地址(段基地址)、运行入口地址、数据段等等

elf格式文件还要再细分:

  • 重定向文件:文件保存着代码和适当的数据,用来和其他目标文件(汇编后得到)一起创建一个可执行的文件或者一个共享目标文件。
  • 可执行文件:保存着一个用来执行的程序。
  • 共享目标文件:即后缀为.so的文件,共享库。

avater
其中:

  • .text:已编译好的指令代码文件。
  • .rodata: ro代表只读(only read),即只读数据(例如const)
  • .data:已初始化的全局变量和局部变量

具体感兴趣的可以去使用readelfobjdump实际去进行查看。

交叉编译环境安装

我们已经知道交叉编译所需要的基本工具:binutilsGCCglibc。由于我们是交叉编译arm平台下程序,所以所用的工具也是和PC上的GCC工具集名字是有点不同的。

下载源文件:

使用tar -zxvf 包名.tar.gz进行解压
avater

建立文件夹,以准备编译产生的文件进行放置。
avater

编译binutils
将工作目录转变为刚刚解压后的binutils文件夹
然后执行
./configure --target=arm-linux --prefix=/home/xeanyu/armenv/binutils/
make
make install

avater
可以看到,这些都是有前缀arm-linux-的binutils的工具。

添加bin环境环境变量
export PATH=$PATH:/home/xeanyu/armenv/binutils/bin (注意,这是一次性的环境变量,当你重启后,就会消失,所以可以去查一下如何永久性的添加环境变量,此处我就不再赘述了。)

avater

第一次编译gcc
首先,我们要知道,编译完整的gcc需要glibc的支持,但是编译glibc又需要gcc的支持。
所以,此处我们要先编译出有基本功能的gcc,而不是完整的。
使用基本的gcc去编译glibc,然后使用glibc再去帮助编译完整的gcc(第二遍编译gcc)。

编辑配置文件,禁止编译libc和gthr_posix.h文件编译。
vim /home/xeanyuGCC/gcc-3.3.2/gcc/config/arm/t-linux

【!这里出现了问题,还没解决,但是先往下写,后面回来解决】

编译glibc

【!这里出现了问题,还没解决,但是先往下写,后面回来解决】

第二次编译gcc
【!这里出现了问题,还没解决,但是先往下写,后面回来解决】

Bootloader移植

Bootloader非常广泛的用在各种设备中,如手机,智能家电等等。其作用是完成硬件的初始化,然后引导操作系统及配置。类似于PC上的BIOS。
基本功能:

  • 初始化部分硬件,包括时钟、内存等。
  • 加载内核到内存上
  • 加载文件系统,atags和dtb到内存上
  • 配置操作系统到硬件
  • 启动操作系统

monitor额外功能:

  • 进行调试
  • 读写内存
  • 烧写Flash
  • 配置环境变量
  • 命令引导操作系统

Bootloader有很多种,这些都是属于Bootloader.

bootloader monitor 描述 X86 ARM RISC-V
LILO Linux磁盘引导程序
GRUB GNU的LILO替代程序
Loadlin 从DOS引导Linux
ROLO 从ROM引导Linux而不需要BIOS
LinuxBIOS 完全代替BUIS的Linux引导程序
BLOB LART等硬件平台的引导程序
U-boot 通用引导程序
RedBoot 基于eCos的引导程序
vivi Mizi公司针对SAMSUNG的ARM CPU设计的引导程序

PC和嵌入式启动对比
PC机: 上电 -> BIOS(固化在ROM) -> MBR(还是Bootloader,从硬盘到RAM) -> OS
嵌入式: 上电 -> Bootloader(将0x00000000映射ROM或RAM) -> OS

Bootloader启动流程

层次

  1. 引导程序,即Bootloader和固化在ROM内的Boot代码.
  2. 系统内核,为特定开发板裁剪的系统内核和启动参数,其参数是默认的,或是Bootloader初始化后传来的。如果是Bootloader传来的,先用Bootloader的。
  3. 文件系统,即建立在Flash上的存储器的管理。通常是用ramdisk作为rootfs,里面存放着各种库,配置文件,系统运行所需的各种程序库等。
  4. 系统应用程序

avater

启动阶段
Bootloader启动有两个阶段: Stage1和Stage2

等我缓缓再写….