x210:uboot和系统移植

嵌入式linux 专栏收录该内容
77 篇文章 0 订阅

注:本文是对朱老师uboot和系统移植课程的备忘引导性笔记,主要是为了能够在学完后快速回忆起相关内容。本文主要记录了一些关键易忘性知识点并包含少量理解性内容,遵循尽量精简的原则,以尽量少的篇幅概括整个课程的知识点,便于后期能够快速定位知识点。故本文不包含具体的命令及函数使用方法等,同时不要太纠结于字面表达,这些只是为了能够快速回忆起相关知识点,具体的准确描述以及用法等需参考具体的文章。

(未完待续)

uboot学习前传

PC机的启动过程
(1)典型的PC机的部署:BIOS程序部署在PC机主板上(随主板出厂时已经预制了),操作系统部署在硬盘上,内存在掉电时无作用,CPU在掉电时不工作。
(2)启动过程:PC上电后先执行BIOS程序(实际上PC的BIOS就是NorFlash),BIOS程序负责初始化DDR内存,负责初始化硬盘,然后从硬盘上将OS镜像读取到DDR中,然后跳转到DDR中去执行OS直到启动(OS启动后BIOS就无用了)

典型嵌入式linux系统启动过程
(1)嵌入式系统的部署和启动都是参考PC机的。只是设备上有一些差别
(2)典型嵌入式系统的部署:uboot程序部署在Flash(能作为启动设备的Flash)上、OS部署在FLash(嵌入式系统中用Flash代替了硬盘)上、内存在掉电时无作用,CPU在掉电时不工作。
(3)启动过程:嵌入式系统上电后先执行uboot、然后uboot负责初始化DDR,初始化Flash,然后将OS从Flash中读取到DDR中,然后启动OS(OS启动后uboot就无用了) 

android系统启动过程
(1)android系统的启动和linux系统(前面讲的典型的嵌入式系统启动)几乎一样。几乎一样意思就是前面完全一样,只是在内核启动后加载根文件系统后不同了。
(1)可以认为启动分为2个阶段:第一个阶段是uboot到OS启动;第二个阶段是OS启动后到rootfs加载到命令行执行;现在我们主要研究第一个阶段,android的启动和linux的差别在第二阶段。 

uboot到底是干嘛的
(1)uboot主要作用是用来启动操作系统内核。
(2)uboot还要负责部署整个计算机系统。
(3)uboot中还有操作Flash等板子上硬盘的驱动。
(4)uboot还得提供一个命令行界面供人来操作。 

uboot即universal bootloader(通用的启动代码)是最常用的一种bootloader,已经成为事实上的业内bootloader标准。现在大部分的嵌入式设备都会默认使用uboot来做为bootloader。 

早期的uboot的版本号类似于这样:uboot1.3.4。后来版本号便成了类似于uboot-2010.06。uboot的核心部分几乎没怎么变化,越新的版本支持的开发板越多而已,对于一个老版本的芯片来说,新旧版本的uboot并没有差异 

uboot必须解决哪些问题(待完善)

(1)自身可开机直接启动 

  • 一般的SoC都支持多种启动方式,譬如SD卡启动、NorFlash启动、NandFlash启动等·····uboot要能够开机启动,必须根据具体的SoC的启动设计来设计uboot。
  • uboot必须进行和硬件相对应的代码级别的更改和移植,才能够保证可以从相应的启动介质启动。uboot中第一阶段的start.S文件中具体处理了这一块。

(2)能够引导操作系统内核启动并给内核传参

  • uboot的终极目标就是启动内核。
  • linux内核在设计的时候,设计为可以被传参。也就是说我们可以在uboot中事先给linux内核准备一些启动参数放在内存中特定位置然后传给内核,内核启动后会到这个特定位置去取uboot传给他的参数,然后在内核中解析这些参数,这些参数将被用来指导linux内核的启动过程。

(3)能提供系统部署功能

  • uboot必须能够被人借助而完成整个系统(包括uboot、kernel、rootfs等的镜像)在Flash上的烧录下载工作。
  • 裸机教程中刷机(ARM裸机第三部分)就是利用uboot中的fastboot功能将各种镜像烧录到iNand中,然后从iNand启动。

(4)能进行soc级和板级硬件管理

  • uboot中实现了一部分硬件的控制能力(uboot中初始化了一部分硬件),因为uboot为了完成一些任务必须让这些硬件工作。譬如uboot要实现刷机必须能驱动iNand,譬如uboot要在刷机时LCD上显示进度条就必须能驱动LCD,譬如uboot能够通过串口提供操作界面就必须驱动串口。譬如uboot要实现网络功能就必须驱动网卡芯片。
  • SoC级(譬如串口)就是SoC内部外设,板级就是SoC外面开发板上面的硬件(譬如网卡、iNand)

uboot的“生命周期”
(1)uboot本质上是一个裸机程序
(2)uboot的入口就是开机自动启动,uboot的唯一出口就是启动内核。uboot还可以执行很多别的任务(譬如烧录系统),但是其他任务执行完后都可以回到uboot的命令行继续执行uboot命令,而启动内核命令一旦执行就回不来了。 

x210:uboot和系统移植扩展--uboot的常用命令和环境变量

环境变量如何参与程序运行
(1)环境变量有2份,一份在Flash中,另一份在DDR中。uboot开机时一次性从Flash中读取全部环境变量到DDR中作为环境变量的初始化值,然后使用过程中都是用DDR中这一份,用户可以用saveenv指令将DDR中的环境变量重新写入Flash中去更新Flash中环境变量。下次开机时又会从Flash中再读一次。
(2)环境变量在uboot中是用字符串表示的,也就是说uboot是按照字符匹配的方式来区分各个环境变量的。

uboot阶段Flash的分区

在一个移植中必须事先将分区方法设计好定死 ,定的标准是:
(1)uboot:uboot必须从Flash起始地址开始存放(也许是扇区0,也许是扇区1,也许是其他,取决于SoC的启动设计),uboot分区的大小必须保证uboot肯定能放下,一般设计为512KB或者1MB(因为一般uboot肯定不足512KB,给再大其实也可以工作,但是浪费);
(2)环境变量:环境变量分区一般紧贴着uboot来存放,大小为32KB或者更多一点。
(3)kernel:kernel可以紧贴环境变量存放,大小一般为3MB或5MB或其他。
(4)rootfs:······
(5)剩下的就是自由分区,一般kernel启动后将自由分区挂载到rootfs下使用

各分区彼此相连,前面一个分区的结尾就是后一个分区的开头。uboot必须在Flash开头,其他分区相对位置是可变的。分区在系统移植前确定好,在uboot中和kernel中使用同一个分区表。将来在系统部署时和系统代码中的分区方法也必须一样。

 补基础之shell和Makefile

shell部分 

shell是一类编程语言
(1)编写shell脚本时使用的语言就是shell语言,又叫脚本语言。
(2)shell脚本其实是一类语言而不是一个语言。

常用shell语言:sh、bash、csh、ksh、perl、python等
(1)在linux下常用的脚本语言其实就是bash、sh;
(2)perl、python这样的高级shell脚本语言,常用在网络管理配置等领域,系统运维人员一般要学习这些。

shell脚本的运行机制:解释运行 

shell程序运行的三种方法:
(1)./xx.sh,这样运行shell要求shell程序必须具有可执行权限。chmod a+x xx.sh来添加可执行权限。
(2)source xx.sh,source是linux的一个命令,这个命令就是用来执行脚本程序的。这样运行不需要脚本具有可执行权限。
(3)bash xx.sh,bash是一个脚本程序解释器,本质上是一个可执行程序。这样执行相当于我们执行了bash程序,然后把xx.sh作为argv[1]传给他运行。 

shell程序模板

#!/bin/sh        
#指定shell程序执行时被/bin/sh解释器解释执行
#在ubuntu下还可以写成[ #!/bin/bash ] 或 [ #!/bin/dash ]
#下面是代码

 

shell编程

Makefile部分 

 目标、依赖、命令
(1)目标就是我们要去make xxx的那个xxx,就是我们最终要生成的东西。
(2)依赖是用来生成目录的原材料
(3)命令就是加工方法,所以make xxx的过程其实就是使用命令将依赖加工成目标的过程。

 通配符%和Makefile自动推导(规则)
(1)%是Makefile中的通配符,代表一个或几个字母。也就是说%.o就代表所有以.o为结尾的文件。
(2)所谓自动推导其实就是Makefile的规则。当Makefile需要某一个目标时,他会把这个目标去套规则说明,一旦套上了某个规则说明,则Makefile会试图寻找这个规则中的依赖,如果能找到则会执行这个规则用依赖生成目标。

Makefile中定义和使用变量
(1)Makefile中定义和使用变量,和shell脚本中非常相似。相似是说:都没有变量类型,直接定义使用,引用变量时用$var 

伪目标(.PHONY)
(1)伪目标意思是这个目标本身不代表一个文件,执行这个目标不是为了得到某个文件或东西,而是单纯为了执行这个目标下面的命令。
(2)伪目标一般都没有依赖,因为执行伪目标就是为了执行目标下面的命令。既然一定要执行命令了那就不必加依赖,因为不加依赖意思就是无条件执行。
(3)伪目标可以直接写,不影响使用;但是有时候为了明确声明这个目标是伪目标会在伪目标的前面用.PHONY来明确声明它是伪目标。 

Makefile的文件名
(1)Makefile的文件名合法的一般有2个:Makefile或者makefile 

Makfile中引用其他Makefile(include指令)
(1)有时候Makefile总体比较复杂,因此分成好几个Makefile来写。然后在主Makefile中引用其他的,用include指令来引用。引用的效果也是原地展开,和C语言中的头文件包含非常相似。  

Makefile中的注释用#
(1)Makefile中注释使用#,和shell一样 

静默执行
(1)在makefile的命令行中前面的@表示静默执行。
(2)Makefile中默认情况下在执行一行命令前会先把这行命令给打印出来,然后再执行这行命令。
(3)如果你不想看到命令本身,只想看到命令执行就静默执行即可。 

Makefile中几种变量赋值运算符
(1)=        最简单的赋值
(2):=        一般也是赋值
以上这两个大部分情况下效果是一样的,但是有时候不一样。
用=赋值的变量,在被解析时他的值取决于最后一次赋值时的值,所以你看变量引用的值时不能只往前面看,还要往后面看。
用:=来赋值的,则是就地直接解析,只用往前看即可。

(3)?=    如果变量前面并没有赋值过则执行这条赋值,如果前面已经赋值过了则本行被忽略。(实验可以看出:所谓的没有赋值过其实就是这个变量没有被定义过)
(4)+=      用来给一个已经赋值的变量接续赋值,意思就是把这次的值加到原来的值的后面,有点类似于strcat。(在shell makefile等文件中,可以认为所有变量都是字符串,+=就相当于给字符串stcat接续内容)(注意一个细节,+=续接的内容和原来的内容之间会自动加一个空格隔开)

注意:Makefile中并不要求赋值运算符两边一定要有空格或者无空格,这一点比shell的格式要求要松一些。

Makefile的环境变量
(1)makefile中用export导出的就是环境变量。一般情况下要求环境变量名用大写,普通变量名用小写。
(2)环境变量和普通变量不同,可以这样理解:环境变量类似于整个工程中所有Makefile之间可以共享的全局变量,而普通变量只是当前本Makefile中使用的局部变量。所以要注意:定义了一个环境变量会影响到工程中别的Makefile文件,因此要小心。
(3)Makefile中可能有一些环境变量可能是makefile本身自己定义的内部的环境变量或者是当前的执行环境提供的环境变量(譬如我们在make执行时给makefile传参。make CC=arm-linux-gcc,其实就是给当前Makefile传了一个环境变量CC,值是arm-linux-gcc。我们在make时给makefile传的环境变量值优先级最高的,可以覆盖makefile中的赋值)。这就好像C语言中编译器预定义的宏__LINE__ __FUNCTION__等一样。 

Makefile中使用通配符
(1)*        若干个任意字符
(2)?        1个任意字符
(3)[]        将[]中的字符依次去和外面的结合匹配

还有个%,也是通配符,表示任意多个字符,和*很相似,但是%一般只用于规则描述中,又叫做规则通配符。

 Makefile的自动变量
(1)为什么使用自动变量。在有些情况下文件集合中文件非常多,描述的时候很麻烦,所以我们Makefile就用一些特殊的符号来替代符合某种条件的文件集,这就形成了自动变量。
(2)自动变量的含义:预定义的特殊意义的符号。就类似于C语言编译器中预制的那些宏__FILE__一样。
(3)常见自动变量:
$@        规则的目标文件名
$<        规则的依赖文件名
$^        依赖的文件集合

 零距离初体验uboot

uboot可以有3种获取途径:uboot官方、SoC官方、具体开发板的官方。

配置uboot

在uboot源码的根目录下执行:make x210_sd_config。如果出现:Configuring for x210_sd board... 则说明配置成功。

uboot的配置阶段主要解决的问题就是在可移植性方面能够帮助我们确定具体的开发板的文件夹路径等

编译uboot

编译前需确保uboot根目录下的Makefile文件中编译器的设置为正确的交叉编译工具链的路径和名字,此后make进行编译。可使用例如make -j4来进行4线程同时编译

主要文件介绍

  • arm_config.mk。后缀是.mk,是一个Makefile文件,将来在某个Makefile中会去调用它。
  • config.mk。和arm_config.mk差不多性质。
  • COPYING。版权声明,uboot本身是GPL许可证的。
  • image_split。一个脚本,看说明是用来分割uboot.bin到BL1的,暂时用不到,先不管。
  • MAKEALL。一个脚本,应该是帮助编译uboot的。
  • Makefile。这个很重要,是uboot源代码的主Makefile,将来整个uboot被编译时就是用这个Makefile管理编译的,所以我们在下个课程中研究uboot配置编译过程时就要分析这个Makefile。
  • mk。快速编译的脚本,其实就是先清理然后配置然后编译而已。
  • mkconfig。这个很重要,是uboot配置阶段的主要配置脚本。uboot的可移植性很大程度就是靠这个配置脚本在维护的。我们在下个课程中研究uboot配置编译过程时就要分析这个配置脚本。
  • mkmovi。暂时不去管他,一个脚本,和iNand/SD卡启动有关
  • rules.mk。这个文件是我们uboot的Makefile使用的规则,本身非常重要,但是我们不去分析他,不去看他。

目录分析 

  • api。        硬件无关的功能函数的API。uboot移植时基本不用管,这些函数是uboot本身使用的。
  • api_examples。 API相关的测试事例代码。
  • board。   board是板的意思,板就是开发板。board文件夹下每一个文件都代表一个开发板,这个文件夹下面放的文件就是用来描述这一个开发板的信息的。board目录下有多少个文件夹,就表示当前这个uboot已经被移植到多少个开发板上了(当前的uboot支持多少个开发板)。
  • common。common是普遍的普通的,这个文件夹下放的是一些与具体硬件无关的普遍适用的一些代码。譬如控制台实现、crc校验的。但是更多的主要是两类:一类是cmd开头的,是用来实现uboot的命令系统的;另一类是env开头的,是用来实现环境变量的。
  • cpu。这个目录是SoC相关的,里面存放的代码都是SoC相关初始化和控制代码(譬如CPU的、中断的、串口等SoC内部外设的,包括起始代码start.S也在这里)。里面很多子文件夹,每一个子文件夹就是一个SoC系列。
    注意:这个问价是严格和硬件相关的,因此移植时也是要注意的。但是因为这个文件夹内都是SoC有关的,我们自己的开发板和三星的开发板虽然板子设计不同但是SoC都是同一个,因此实际移植时这个目录几乎不用动。
  • disk。磁盘有关的,没研究过,没用过。
  • doc。文档目录,里面存放了很多uboot相关文档,这些文档可以帮助我们理解uboot代码。但是因为是纯英文的,而且很杂乱,所以几乎没用。
  • drivers。顾名思义,驱动。这里面放的就是从linux源代码中扣出来的原封不动的linux设备驱动,主要是开发板上必须用到的一些驱动,如网卡驱动、Inand/SD卡、NandFlash等的驱动。要知道:uboot中的驱动其实就是linux中的驱动,uboot在一定程度上移植了linux的驱动给自己用。但是linux是操作系统而uboot只是个裸机程序,因此这种移植会有不同,让我说:uboot中的驱动其实是linux中的驱动的一部分。
  • examples。示例代码,没用过。
  • fs。filesystem,文件系统。这个也是从linux源代码中移植过来的,用来管理Flash等资源。
  • include。头文件目录。uboot和linux kernel在管理头文件时都采用了同一个思路,就是把所有的头文件全部集中存放在include目录下,而不是头文件跟着自己对应的c文件。所以在uboot中头文件包含时路径结构要在这里去找。
  • lib_开头的一坨。(典型的lib_arm和lib_generic)架构相关的库文件。譬如lib_arm里面就是arm架构使用的一些库文件。lib_generic里是所有架构通用的库文件。这类文件夹中的内容移植时基本不用管。
  • libfdt。设备树有关的。linux内核在3.4左右的版本的时候更改了启动传参的机制,改用设备树来进行启动传参,进行硬件信息的描述了。
  • nand_spl。nand相关的,不讲。
  • net。网络相关的代码,譬如uboot中的tftp nfs ping命令 都是在这里实现的。
  • onenand开头的,是onenand相关的代码,是三星加的,标准uboot中应该是没有的。
  • sd_fusing。这里面代码实现了烧录uboot镜像到SD卡的代码。后面要仔细研究的。
  • tools。里面是一些工具类的代码。譬如mkimage。

 uboot配置和编译过程详解

Makefile中重要的变量

  •  U_BOOT_VERSION。uboot版本号
  • VERSION_FILE。存储uboot版本号的头文件的pathname
  • HOSTARCH。主机的CPU架构
  • HOSTOS。主机的操作系统
  • BUILD_DIR。编译生成文件的目录
  • OBJTREE。编译出的.o文件存放的目录的根目录
  • SRCTREE。源代码的根目录,即uboot源码根目录
  • TOPDIR。uboot源码根目录
  • MKCONFIG。uboot配置阶段的配置脚本的pathname
  • ARCH。arm
  • CPU。s5pc11x
  • BOARD。x210
  • VENDOR。samsung
  • SOC。s5pc110
  • CROSS_COMPILE。交叉编译工具链的前缀
  • TEXT_BASE。整个uboot链接时指定的链接地址

$(obj)include/version_autogenerated.h(make时生成)

#define U_BOOT_VERSION "U-Boot 1.3.4"

 

$(obj)include/config.mk(配置时生成)

ARCH   = arm
CPU    = s5pc11x
BOARD  = x210
VENDOR = samsung
SOC    = s5pc110

 

include/config.h(配置时生成)

#include <configs/x210_sd.h>

 

开发板配置项目文件:$(OBJTREE)/include/autoconf.mk(make时生成)

CONFIG_CMD_FAT=y
CONFIG_USB_OHCI=y
CONFIG_SYS_CLK_FREQ=24000000
CONFIG_CMD_ITEST=y
CONFIG_S3C_HSMMC=y
CONFIG_DISPLAY_BOARDINFO=y
...
...
...

 

$(obj)board/samsung/x210/config.mk(配置时生成)

TEXT_BASE = 0xc3e00000

 

uboot配置编译过程及其文件
配置阶段
    执行make x210_sd_config
    主Makefile中:
        x210_sd_config是主Makefile中的一个目标,会调用$(SRCTREE)/mkconfig配置脚本并传递这6个参数:$1:x210_sd、$2:arm、$3:s5pc11x、$4:x210、$5:samsumg、$6:s5pc110。
        生成$(obj)board/samsung/x210/config.mk文件并写入TEXT_BASE = 0xc3e00000
    $(SRCTREE)/mkconfig配置脚中:
        根据传入的参数创建对应的符号链接,具体到一个开发板的编译时用符号连接的方式提供一个具体的名字的文件夹供编译时使用。
        创建include/config.mk文件,其内容为:
            ARCH   = arm
            CPU    = s5pc11x
            BOARD  = x210
            VENDOR = samsung
            SOC    = s5pc110                
        创建include/config.h文件,其内容为:#include <configs/x210_sd.h>
make阶段
    创建文件$(obj)include/version_autogenerated.h,其内容为:#define U_BOOT_VERSION "U-Boot 1.3.4"
    创建文件$(OBJTREE)/include/autoconf.mk,其内部是一些CONFIG_开头的宏(可以理解为变量),原材料在include/config.h头文件,这些宏/变量会影响我们是否对某些文件进行编译,用来指导整个uboot的编译过程。
    ... 

Makefile主要做了哪些事情

  • uboot版本号的确定,存储在变量U_BOOT_VERSION中
  • 存储uboot版本号的头文件的确定,存储在变量VERSION_FILE中
  • 主机的CPU架构的确定,存储在变量HOSTARCH中
  • 主机的操作系统的确定,存储在变量HOSTOS中
  • 根据参数,是否静默编译
  • 原地编译还是指定目录编译并导出TOPDIR、SRCTREE、OBJTREE、MKCONFIG(存储了配置uboot时所使用的脚本的pathname)
  • 包含配置生成的$(obj)include/config.mk文件,文件中包含变量ARCH、CPU、BOARD、VENDOR、SOC。并导出这几个变量。
  • 根据ARCH确定交叉编译工具链的前缀,存储在变量CROSS_COMPILE中
  • 编译工具定义
  • 包含开发板配置项目文件autoconf.mk(配置时上生成)
  • 包含各种板级配置文件
  • 确定连接脚本
  • 自动推到规则
  • 主目标all
  • 配置目标x210_sd_config

主Makefile分析 

配置脚本mkconfig分析

uboot的链接脚本
(1)ENTRY(_start)用来指定整个程序的入口地址。所谓入口地址就是整个程序的开头地址,可以认为就是整个程序的第一句指令。有点像C语言中的main。
(2)之前在裸机中告诉大家,指定程序的链接地址有2种方法:一种是在Makefile中ld的flags用-Ttext 0x20000000来指定;第二种是在链接脚本的SECTIONS开头用.=0x20000000来指定。两种都可以实现相同效果。其实,这两种技巧是可以共同配合使用的,也就是说既在链接脚本中指定也在ld flags中用-Ttext来指定。两个都指定以后以-Ttext指定的为准。
(3)uboot的最终链接起始地址就是在Makefile中用-Ttext 来指定的,具体参见2.4.5.2节,注意TEXT_BASE变量。最终来源是Makefile中配置对应的命令中,在make xxx_config时得到的。
(4)在代码段中注意文件排列的顺序。指定必须放在前面部分的那些文件就是那些必须安排在前16KB内的文件,这些文件中的函数在前16KB会被调用。在后面第二部分(16KB之后)中调用的程序,前后顺序就无所谓了。
(5)链接脚本中除了.text  .data .rodata .bss段等编译工具自带的段之外,编译工具还允许我们自定义段。譬如uboot总的.u_boot_cmd段就是自定义段。

 uboot源码分析1-启动第一阶段

x210:uboot和系统移植扩展--uboot启动第一阶段

 uboot源码分析2-启动第二阶段

由宏观分析来讲,uboot的第二阶段就是要初始化剩下的还没被初始化的硬件。主要是SoC外部硬件(譬如iNand、网卡芯片····)、uboot本身的一些东西(uboot的命令、环境变量等····)。然后最终初始化完必要的东西后进入uboot的命令行准备接受命令。

x210:uboot和系统移植扩展--uboot启动第二阶段

 uboot源码分析3-uboot如何启动内核

操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。

运行时必须先加载到DDR中链接地址处
(1)uboot在第一阶段中进行重定位时将第二阶段(整个uboot镜像)加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。
(2)内核也有类似要求,uboot启动内核时将内存从SD卡读取放到DDR中(其实就是个重定位的过程),不能随意放置,必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000。 

内核启动需要必要的启动参数
(1)uboot是无条件启动的,从零开始启动的。
(2)内核是不能开机自动完全从零开始启动的,内核启动要别人帮忙。uboot要帮助内核实现重定位(从SD卡到DDR),uboot还要给内核提供启动参数。 

uboot要启动内核,分为2个步骤:第一步是将内核镜像从启动介质中加载到DDR中,第二步是去DDR中启动内核镜像。(内核代码根本就没考虑重定位,因为内核知道会有uboot之类的把自己加载到DDR中链接地址处的,所以内核直接就是从链接地址处开始运行的) 

静态内核镜像在哪里?
(1)SD卡/iNand/Nand/NorFlash等:raw分区(原始分区)
常规启动时各种镜像都在SD卡中,因此uboot只需要从SD卡的kernel分区去读取内核镜像到DDR中即可。读取要使用uboot的命令来读取(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)
这种启动方式来加载ddr,使用命令:movi read kernel 30008000。其中kernel指的是uboot中的kernel分区(就是uboot中规定的SD卡中的一个区域范围,这个区域范围被设计来存放kernel镜像,就是所谓的kernel分区) 

tftp、nfs等网络下载方式从远端服务器获取镜像
uboot还支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中,然后需要启动时uboot通过网络从服务器中下载镜像到开发板的DDR中。

镜像要放在DDR的什么地址?
(1)内核一定要放在链接地址处,链接地址去内核源代码的链接脚本或者Makefile中去查找。X210中是0x30008000。 

vmlinuz和zImage和uImage

(1)uboot经过编译直接生成的elf格式的可执行程序是u-boot,这个程序类似于windows下的exe格式,在操作系统下是可以直接执行的。但是这种格式不能用来烧录下载。我们用来烧录下载的是u-boot.bin,这个东西是由u-boot使用arm-linux-objcopy工具进行加工(主要目的是去掉一些无用的)得到的。这个u-boot.bin就叫镜像(image),镜像就是用来烧录到iNand中执行的。

(2)linux内核经过编译后也会生成一个elf格式的可执行程序,叫vmlinux或vmlinuz,这个就是原始的未经任何处理加工的原版内核elf文件;嵌入式系统部署时烧录的一般不是这个vmlinuz/vmlinux,而是要用objcopy工具去制作成烧录镜像格式(就是u-boot.bin这种,但是内核没有.bin后缀),经过制作加工成烧录镜像的文件就叫Image(制作把78M大的精简成了7.5M,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘)。

(3)原则上Image就可以直接被烧录到Flash上进行启动执行(类似于u-boot.bin),但是实际上并不是这么简单。实际上linux的作者们觉得Image还是太大了所以对Image进行了压缩,并且在image压缩后的文件的前端附加了一部分解压缩代码。构成了一个压缩格式的镜像就叫zImage。(因为当年Image大小刚好比一张软盘(软盘有2种,1.2M的和1.44MB两种)大,为了节省1张软盘的钱于是乎设计了这种压缩Image成zImage的技术)。

(4)uboot为了启动linux内核,还发明了一种内核格式叫uImage。uImage是由zImage加工得到的,uboot中有一个工具,可以将zImage加工生成uImage。注意:uImage不关linux内核的事,linux内核只管生成zImage即可,然后uboot中的mkimage工具再去由zImage加工生成uImage来给uboot启动。这个加工过程其实就是在zImage前面加上64字节的uImage的头信息即可。(此处描述不一定准确)

(5)原则上uboot启动时应该给他uImage格式的内核镜像,但是实际上uboot中也可以支持zImage,是否支持就看x210_sd.h中是否定义了LINUX_ZIMAGE_MAGIC这个宏并且有对应的代码支持。所以大家可以看出:有些uboot是支持zImage启动的,有些则不支持。但是所有的uboot肯定都支持uImage启动。 

uboot启动内核时要调用bootm命令。bootm命令对应do_bootm函数。

命令名前加do_即可构成这个命令对应的函数,因此当我们bootm命令执行时,uboot实际执行的函数叫do_bootm函数,在cmd_bootm.c。

do_bootm刚开始定义了一些变量,然后用宏来条件编译执行了secureboot的一些代码(主要进行签名认证),先不管他;然后进行了一些一些细节部分操作,也不管他。然后到了CONFIG_ZIMAGE_BOOT,用这个宏来控制进行条件编译一段代码,这段代码是用来支持zImage格式的内核启动的。

do_bootm函数中一直到397行的after_header_check这个符号处,都是在进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验。所以do_bootm函数的核心就是去分辨传进来的image到底是什么类型,然后按照这种类型的头信息格式去校验。校验通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。 

编译内核得到uImage去启动

(1)如果直接在kernel底下去make uImage会提供mkimage command not found。解决方案是去uboot/tools下cp mkimage /usr/local/bin/,复制mkimage工具到系统目录下。再去make uImage即可。 

zImage启动细节

  • LINUX_ZIMAGE_MAGIC

(1)这个是一个定义的魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。

(2)命令 bootm 0x30008000,所以do_boom的argc=2,argv[0]=bootm  argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。

(3)zImage头部开始的第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。

  • image_header_t

(1)这个数据结构是我们uboot启动内核使用的一个标准启动数据结构(大小是64个字节,默认是给uImage用的,这里相当于将zImage的头信息改成uImage的形式,后面会看到最终都是以一种方式启动的,可以认为zImage启动时现将zImage改造成uImage后再以uImage方式启动的)

hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);

(2)images全局变量是do_bootm函数中使用,用来完成启动过程的。zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。

uImage启动

(1)IMAGE_FORMAT_LEGACY。LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。

(2)uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm方式中叫FIT)

(3)uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。 

总结1:uboot本身设计时只支持uImage启动,原来uboot的代码也是这样写的。后来有了fdt方式之后,就把uImage方式命令为LEGACY方式,fdt方式命令为FIT方式,于是乎多了写#if #endif添加的代码。后来移植的人又为了省事添加了zImage启动的方式,又为了省事把zImage启动方式直接写在了uImage和fdt启动方式之前,于是乎又有了一对#if  #endif。于是乎整个的代码看起来很恶心。
总结2:第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。

do_bootm_linux函数

(1)ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。

(2)一般执行一个镜像都是:第一步先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步对镜像进行校验;第三步再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步就去entrypoint处开始执行镜像。 

(3)参数传递采用tag方式传参

(4)theKernel = (void (*)(int, int, uint))ep;将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。

tag方式传参

uboot启动内核的总结
(1)启动4步骤

第一步:将内核搬移到DDR中

第二步:校验内核格式、CRC等

第三步:准备传参

第四步:跳转执行内核

(2)涉及到的主要函数是:do_boom和do_bootm_linux

(3)uboot能启动的内核格式:zImage uImage fdt方式

(4)跳转与函数指针的方式运行内核 

 uboot源码分析4-uboot的命令体系

uboot命令体系的实现代码在uboot/common/cmd_xxx.c中。有若干个.c文件和命令体系有关。(还有command.c  main.c也是和命令有关的)

每个命令对应一个函数

(1)每一个uboot的命令背后都对应一个函数。这就是uboot实现命令体系的一种思路和方法。

(2)我们要找到每一个命令背后所对应的那个函数,而且要分析这个函数和这个命令是怎样对应起来的。 

(3)xxx命令对应的函数的函数名为do_xxx

命令参数以argc&argv传给函数
(1)有些uboot的命令还支持传递参数。也就是说命令背后对应的函数接收的参数列表中有argc和argv,然后命令体系会把我们执行命令时的命令+参数(md 30000000 10)以argc(3)和argv(argv[0]=md, argv[1]=30000000 argv[2]=10)的方式传递给执行命令的函数。 

uboot命令解析和执行过程分析

  • main_loop函数 

(1)uboot启动的第二阶段,在初始化了所有该初始化的东西后,进入了一个死循环,死循环的循环体就是main_loop

(2)main_loop函数执行一遍,就是一个获取命令、解析命令、执行命令的过程。

(3)CONFIG_AUTO_COMPLETE宏用来配置是否执行install_auto_complete函数完成自动补全

(4)run_command函数就是用来执行命令的函数

(5)解析

s = getenv ("bootcmd");

debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");	

if (bootdelay >= 0 && s && !abortboot (bootdelay)) {
#ifdef CONFIG_AUTOBOOT_KEYED
		int prev = disable_ctrlc(1);	/* disable Control C checking */
#endif

#ifndef CFG_HUSH_PARSER
		run_command (s, 0);
#else
		parse_string_outer(s, FLAG_PARSE_SEMICOLON |
				    FLAG_EXIT_FROM_LOOP);
#endif

#ifdef CONFIG_AUTOBOOT_KEYED
		disable_ctrlc(prev);	/* restore Control C checking */
#endif
	}

当没有按键按下打断自动启动并且bootcmd环境变量里面有值时,就会进入到该if里面去执行,abortboot里面会进行计时并检测按键,当有按键按下打断自动启动时就会返回1。

CFG_HUSH_PARSER宏在命令查找的查找方式上做了优化,这样可以提高查找速度。

这里如果没有定义CFG_HUSH_PARSER宏,就调用run_command函数来执行命令,否则调用parse_string_outer来进行命令的解析和执行。

	/*
	 * Main Loop for Monitor Command Processing
	 */
#ifdef CFG_HUSH_PARSER
	parse_file_outer();
	/* This point is never reached */
	for (;;);
#else
	for (;;) {
	#ifdef CONFIG_BOOT_RETRY_TIME
		if (rc >= 0) {
			/* Saw enough of a valid command to
			 * restart the timeout.
			 */
			reset_cmd_timeout();
		}
	#endif
		len = readline (CFG_PROMPT);

		flag = 0;	/* assume no special flags for now */
		if (len > 0)
			strcpy (lastcommand, console_buffer);
		else if (len == 0)
			flag |= CMD_FLAG_REPEAT;
#ifdef CONFIG_BOOT_RETRY_TIME
		else if (len == -2) {
			/* -2 means timed out, retry autoboot
			 */
			puts ("\nTimed out waiting for command\n");

	#ifdef CONFIG_RESET_TO_RETRY
			/* Reinit board to run initialization code again */
			do_reset (NULL, 0, 0, NULL);
	#else
			return;		/* retry autoboot */
	#endif
		}
#endif

		if (len == -1)
			puts ("<INTERRUPT>\n");
		else
			rc = run_command (lastcommand, flag);

		if (rc <= 0) {
			/* invalid command or not repeatable, forget it */
			lastcommand[0] = 0;
		}
	}
#endif /*CFG_HUSH_PARSER*/

上面这部分就是在实际的解析执行我们命令行中输入的命令。

使用了CFG_HUSH_PARSER的方式比较复杂,下面值解析没有定义CFG_HUSH_PARSER时的 命令执行函数run_command

 run_command

clear_ctrlc();

清除之前的的Ctrl+C

if (!cmd || !*cmd) {
		return -1;	/* empty command */
	}

判断cmd是否指向NULL或里面的值无效

if (!cmd || !*cmd) {
		return -1;	/* empty command */
	}

判断命令长度是否超限

if ((argc = parse_line (finaltoken, argv)) == 0) {
			rc = -1;	/* no command at all */
			continue;
		}

命令解析。parse_line函数把"md 30000000 10"解析成argv[0]=md, argv[1]=30000000 argv[2]=10;

/* Look up command in command table */
		if ((cmdtp = find_cmd(argv[0])) == NULL) {
			printf ("Unknown command '%s' - try 'help'\n", argv[0]);
			rc = -1;	/* give up after bad command */
			continue;
		}

命令集中查找命令。find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,命令的相关数据作为返回值输出

/* found - check max args */
		if (argc > cmdtp->maxargs) {
			printf ("Usage:\n%s\n", cmdtp->usage);
			rc = -1;
			continue;
		}

如果命令参数长度超过该命令的最大参数限制,则打印使用说明

/* OK - call function to do the command */
		if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
			rc = -1;
		}

最后用函数指针的方式调用执行了对应函数。

命令结构体cmd_tbl_t

struct cmd_tbl_s {
    char        *name;        /* Command Name            */
    int        maxargs;    /* maximum number of arguments    */
    int        repeatable;    /* autorepeat allowed?        */
                    /* Implementation function    */
    int        (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
    char        *usage;        /* Usage message    (short)    */
#ifdef    CFG_LONGHELP
    char        *help;        /* Help  message    (long)    */
#endif
#ifdef CONFIG_AUTO_COMPLETE
    /* do auto completion on the arguments */
    int        (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};

typedef struct cmd_tbl_s    cmd_tbl_t;

(1)name:命令名称,字符串格式。
(2)maxargs:命令最多可以接收多少个参数
(3)repeatable:指示这个命令是否可重复执行。重复执行是uboot命令行的一种工作机制,就是直接按回车则执行上一条执行的命令。
(4)cmd:函数指针,命令对应的函数的函数指针,将来执行这个命令的函数时使用这个函数指针来调用。
(5)usage:命令的短帮助信息。对命令的简单描述。
(6)help:命令的长帮助信息。细节的帮助信息。
(7)complete:函数指针,指向这个命令的自动补全的函数。
总结:uboot的命令体系在工作时,一个命令对应一个cmd_tbl_t结构体的一个实例,然后uboot支持多少个命令,就需要多少个结构体实例。uboot的命令体系把这些结构体实例管理起来,当用户输入了一个命令时,uboot会去这些结构体实例中查找(查找方法和存储管理的方法有关)。如果找到则执行命令,如果未找到则提示命令未知。

uboot实现命令管理的思路

(1)填充1个结构体实例构成一个命令

(2)给命令结构体实例附加特定段属性(用户自定义段),链接时将带有该段属性的内容链接在一起排列(挨着的,不会夹杂其他东西,也不会丢掉一个带有这种段属性的,但是顺序是乱序的)

(3)uboot重定位时将该段整体加载到DDR中。加载到DDR中的uboot镜像中带有特定段属性的这一段其实就是命令结构体的集合,有点像一个命令结构体数组。

(4)段起始地址和结束地址(链接地址、定义在u-boot.lds中)决定了这些命令集的开始和结束地址。 

uboot命令定义

(1)U_BOOT_CMD宏基本分析
这个宏定义在uboot/common/command.h中。

U_BOOT_CMD(
    version,    1,        1,    do_version,
    "version - print monitor version\n",
    NULL
);

这个宏替换后变成:

cmd_tbl_t __u_boot_cmd_version __attribute__ ((unused,section (".u_boot_cmd"))) = {#name, maxargs, rep, cmd, usage, help} 

这个U_BOOT_CMD宏的理解,关键在于结构体变量的名字和段属性。#用于将参数转换为字符串。##是连字符。并附加了用户自定义段属性,以保证链接时将这些数据结构链接在一起排布。

find_cmd函数

  • find_cmd函数的任务是从当前uboot的命令集中查找是否有某个命令。如果找到则返回这个命令结构体的指针,如果未找到返回NULL。
  • 解析

(1)

cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start;	/*Init value */

定义指针变量cmdtp_temp初始化为__u_boot_cmd_start变量的地址,__u_boot_cmd_start变量定义在连接脚本u-boot.lds中是.u_boot_cmd段的起始地址。同样的还有__u_boot_cmd_end表示.u_boot_cmd段的结束地址。

len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);

判断命令中是否包含“.”符号,从而判断命令长度

for (cmdtp = &__u_boot_cmd_start;
	     cmdtp != &__u_boot_cmd_end;
	     cmdtp++) {
		if (strncmp (cmd, cmdtp->name, len) == 0) {
			if (len == strlen (cmdtp->name))
				return cmdtp;	/* full match */

			cmdtp_temp = cmdtp;	/* abbreviated command ? */
			n_found++;
		}
	}
	if (n_found == 1) {			/* exactly one match */
		return cmdtp_temp;
	}

循环遍历.u_boot_cmd段中的每一个cmd_tbl_t类型的命令参数进行匹配。

 uboot源码分析5-uboot的环境变量

环境变量的优先级
(1)uboot代码当中有一个值,环境变量中也有一个值。uboot程序实际运行时规则是:如果环境变量为空则使用代码中的值;如果环境变量不为空则优先使用环境变量对应的值。
(2)譬如machid(机器码)。uboot中在x210_sd.h中定义了一个机器码2456,写死在程序中的不能更改。如果要修改uboot中配置的机器码,可以修改x210_sd.h中的机器码,但是修改源代码后需要重新编译烧录,很麻烦;比较简单的方法就是使用环境变量machid。set machid 0x998类似这样,有了machid环境变量后,系统启动时会优先使用machid对应的环境变量,这就是优先级问题。

环境变量在uboot中工作方式

(1)默认环境变量,在uboot/common/env_common.c中default_environment,这东西本质是一个字符数组,大小为CFG_ENV_SIZE(16kb),里面内容就是很多个环境变量连续分布组成的,每个环境变量最末端以'\0'结束。 

(2)SD卡中环境变量分区,在uboot的raw分区中。SD卡中其实就是给了个分区,专门用来存储而已。存储时其实是把DDR中的环境变量整体的写入SD卡中分区里。所以当我们saveenv时其实整个所有的环境变量都被保存了一遍,而不是只保存更改了的。

(3)DDR中环境变量,在default_environment中,实质是字符数组。在uboot中其实是一个全局变量,链接时在数据段,重定位时default_environment就被重定位到DDR中一个内存地址处了。这个地址处这个全局字符数组就是我们uboot运行时的DDR中的环境变量了。

总结:刚烧录的系统中环境变量分区是空白的,uboot第一次运行时加载的是uboot代码中自带的一份环境变量default_environment,叫默认环境变量。我们在saveenv时DDR中的环境变量会被更新到SD卡中的环境变量中,就可以被保存下来,下次开机会在环境变量relocate时,SD卡中的环境变量会被加载到DDR中去。default_environment中的内容虽然被uboot源代码初始化为一定的值(这个值就是我们的默认环境变量),但是在uboot启动的第二阶段,env_relocate时代码会去判断SD卡中的env分区的crc是否通过。如果crc校验通过说明SD卡中有正确的环境变量存储,则relocate函数会从SD卡中读取环境变量来覆盖default_environment字符数组,从而每次开机可以保持上一次更改过的环境变量。

环境变量相关命令

printenv、 setenv、saveenv

uboot内部获取环境变量

  • getenv

(1)不可重入的

(2)实现方式就是去遍历default_environment数组,挨个拿出所有的环境变量比对name,找到相等的直接返回这个环境变量的首地址即可。

  • getenv_r

(1)可重入版本

(2)getenv函数是直接返回这个找到的环境变量在DDR中环境变量处的地址,而getenv_r函数的做法是找到了DDR中环境变量地址后,将这个环境变量复制一份到提供的buf中,而不动原来DDR中环境变量。 

有关于环境变量的所有操作,主要理解了环境变量在DDR中的存储方法,理解了环境变量和gd全局变量的关联和优先级,理解了环境变量在存储介质中的存储方式(专用raw分区),整个环境变量相关的都清楚了。 

 uboot源码分析6-uboot的硬件驱动部分

MMC系统的初始化应该包含这么几部分:SoC里的MMC控制器初始化(MMC系统时钟的初始化、SFR初始化)、SoC里MMC相关的GPIO的初始化、SD卡/iNand芯片的初始化。

mmc_initialize
    cpu_mmc_init
        setup_hsmmc_clock    初始化SoC中MMC控制器中的时钟部分
        setup_hsmmc_cfg_gpio    配置SoC中MMC控制器相关的GPIO
        smdk_s3c_hsmmc_init
            s3c_hsmmc_initialize
                mmc_register    进行mmc设备的注册
    find_mmc_device    通过mmc设备编号来在系统中查找对应的mmc设备。
    mmc_init    进行mmc卡的初始化
        mmc_go_idle
            mmc_send_cmd
        mmc_send_if_cond
            mmc_send_cmd
        ······

smdk_s3c_hsmmc_init。函数内部通过宏定义USE_MMCx来决定是否调用s3c_hsmmc_initialize来进行具体的某个通道的初始化操作。
s3c_hsmmc_initialize。定义并且实例化一个struct mmc类型的对象,然后填充它的各种成员,调用mmc_register函数。
mmc_register。将当前这个struct mmc使用链表连接到mmc_devices这个全局变量中去。
find_mmc_device。工作原理就是通过遍历mmc_devices链表,去依次寻找系统中注册的mmc设备,然后对比其设备编号和我们当前要查找的设备编号,如果相同则就找到了要找的设备。
mmc_init。函数内部就是依次通过向mmc卡发送命令码(CMD0、CMD2那些)来初始化SD卡/iNand内部的控制器,以达到初始化SD卡的目的。

总结

  • 整个MMC系统初始化分为2大部分:SoC这一端的MMC控制器的初始化,SD卡这一端卡本身的初始化。前一步主要是在cpu_mmc_init函数中完成,后一部分主要是在mmc_init函数中完成。
  • 整个初始化完成后去使用sd卡/iNand时,操作方法和mmc_init函数中初始化SD卡的操作一样的方式。读写sd卡时也是通过总线向SD卡发送命令、读取/写入数据来完成的。
  • 顺着操作追下去,到了mmc_send_cmd函数处就断了,真正的向SD卡发送命令的硬件操作的函数找不到。这就是学习驱动的麻烦之处。
  • struct mmc结构体是关键。两部分初始化之间用mmc结构体来链接的,初始化完了后对mmc卡的常规读写操作也是通过mmc结构体来链接的。 

 struct mmc
(1)驱动的设计中有一个关键数据结构。譬如MMC驱动的结构体就是struct mmc这些结构体中包含一些变量和一些函数指针,变量用来记录驱动相关的一些属性,函数指针用来记录驱动相关的操作方法。这些变量和函数指针加起来就构成了驱动。驱动就被抽象为这个结构体。
(2)一个驱动工作时主要就分几部分:驱动构建(构建一个struct mmc然后填充它)、驱动运行时(调用这些函数指针指针的函数和变量)

分离思想
(1)分离思想就是说在驱动中将操作方法和数据分开。
(2)操作方法就是函数,数据就是变量。所谓操作方法和数据分离的意思就是:在不同的地方来存储和管理驱动的操作方法和变量,这样的优势就是驱动便于移植。 

分层思想

(1)分层思想是指一个整个的驱动分为好多个层次。简单理解就是驱动分为很多个源文件,放在很多个文件夹中。譬如本课程讲的mmc的驱动涉及到drivers/mmc下面的2个文件和cpu/s5pc11x下的好几个文件。

(2)以mmc驱动为例来分析各个文件的作用:

uboot/drivers/mmc/mmc.c:本文件的主要内容是和MMC卡操作有关的方法,譬如MMC卡设置空闲状态的、卡读写数据等。但是本文件中并没有具体的硬件操作函数,操作最终指向的是struct mmc结构体中的函数指针,这些函数指针是在驱动构建的时候和真正硬件操作的函数挂接的(真正的硬件操作的函数在别的文件中)。 

uboot/drivers/mmc/s3c_hsmmc.c:本文件中是SoC内部MMC控制器的硬件操作的方法,譬如向SD卡发送命令的函数(s3c_hsmmc_send_command),譬如和SD卡读写数据的函数(s3c_hsmmc_set_ios),这些函数就是具体操作硬件的函数,也就是mmc.c中需要的那些硬件操作函数。这些函数在mmc驱动初始化构建时(s3c_hsmmc_initialize函数中)和struct mmc挂接起来备用。

mmc.c和s3c_hsmmc.c构成了一个分层,mmc.c中调用了s3c_hsmmc.中的函数,所以mmc.c在上层,s3c_hsmmc.c在下层。这两个分层后我们发现mmc.c中不涉及具体硬件的操作,s3c_hsmmc.c中不涉及驱动工程时的时序操作。

(3)cpu/s5pc11x/下面还有一个setup_hsmmc.c,也和MMC驱动有关。这里面的2个函数(setup_hsmmc_clock和setup_hsmmc_cfg_gpio)都是和SoC有关的初始化函数。

 uboot的移植1-从三星官方uboot开始移植

电源管理IC

在lowlevel_init函数中有一句

bl PMIC_InitIp

用来初始化电源管理IC,但x210开发板上并没有这个电源管理IC,因此这个函数内部向PMIC发送I2C接口的命令时会卡死,所以需将这句屏蔽掉。

修改banner信息

(1)更改smdkv210single.h中的CONFIG_IDENT_STRING宏的值

时钟部分的配置

(1)时钟部分的运行结果本来就是对的,时钟部分的代码在lowlevel_init.S中的bl system_clock_init调用的这个函数中。函数的代码部分是没任何问题的,根本不需要改动,要改动的是寄存器写入的值,这些值都在配置头文件(smdkv210single.h)中用宏定义定义出来了。如果时钟部分要更改,关键是去更改头文件中的宏定义。

(2)三星移植时已经把210常用的各种时钟配置全都计算好用宏开关来控制了。只要打开相应的宏开关就能将系统配置为各种不同的频率。

DDR配置信息

(1)从运行信息以及bdinfo命令看到的结果,显示DRAM bank0和1的size值都设置错了。

(2)修改配置头文件smdkv210single.h中的DDR相关的配置宏

#define CONFIG_NR_DRAM_BANKS    2          /* we have 2 bank of DRAM */
//#define SDRAM_BANK_SIZE         0x20000000    /* 512 MB */
#define SDRAM_BANK_SIZE         0x10000000    /* 256 MB */

#define PHYS_SDRAM_1            MEMORY_BASE_ADDRESS /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE       SDRAM_BANK_SIZE
//#define PHYS_SDRAM_2            (MEMORY_BASE_ADDRESS + SDRAM_BANK_SIZE) /* SDRAM Bank #2 */
#define PHYS_SDRAM_2  			0x40000000
#define PHYS_SDRAM_2_SIZE       SDRAM_BANK_SIZE

(3)配置DDR端口0地址为30000000开头

  • 修改配置头文件smdkv210single.h中的宏MEMORY_BASE_ADDRESS
#define MEMORY_BASE_ADDRESS	0x30000000
  • DDR的初始化代码部分是在lowlevel_init.S中写的,是不动的。代码部分就是对相应寄存器做相应值的初始化;要动的是值,而uboot为了具有可移植性把值都宏定义在include/configs/xxx.h中了。因此我们只需要去这个配置头文件中更改配置值即可。
//#define DMC0_MEMCONFIG_0	0x20E01323
#define DMC0_MEMCONFIG_0	0x30F01323

(4)虚拟地址映射表中相应修改,将之前内存映射把20000000开始的256MB映射到C0000000开头的256MB改为30000000开始的256MB映射到C0000000开头的256MB。

(5)修改虚拟地址到物理地址的映射函数。修改uboot/board/samsung/smdkc110/smdkc110.c中的virt_to_phy_smdkc110,将其中的20000000改为30000000即可。

inand驱动问题的解决

我们使用的iNand卡的版本号大于5,而uboot代码本身不处理版本号大于5的卡,因此出错了。修改uboot中的代码,把判断的5改成更大的数字。譬如8,然后跳过这个错误。 

控制台串口更换为串口0

(1)三星公司推荐使用串口2来作为调试串口,所以在三星移植的uboot和内核版本中都是以串口2默认为控制台串口的。

(2)s5pv210中一共有4个串口(串口0、1、2、3),开发板X210上用DB9接口引出了2个串口,分别是串口2和串口0(靠边的是串口2,靠里那个是串口0)。

(3)uboot中真正去硬件初始化串口控制器的代码在lowlevel_init.S中的uart_asm_init中,其中初始化串口的寄存器用ELFIN_UART_CONSOLE_BASE宏作为串口n的寄存器的基地址,结合偏移量对寄存器进行寻址初始化。所以uart_asm_init中到底初始化的是串口几(从0到3)?取决于ELFIN_UART_CONSOLE_BASE宏(s5pc110.h)。这个宏的值又由CONFIG_SERIALn(n是从1到4)来决定

/*
 * UART
 */
#define ELFIN_UART_BASE			0XE2900000

#define ELFIN_UART0_OFFSET		0x0000
#define ELFIN_UART1_OFFSET		0x0400
#define ELFIN_UART2_OFFSET		0x0800
#define ELFIN_UART3_OFFSET		0x0c00

#if defined(CONFIG_SERIAL1)
#define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART0_OFFSET)
#elif defined(CONFIG_SERIAL2)
#define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART1_OFFSET)
#elif defined(CONFIG_SERIAL3)
#define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART2_OFFSET)
#elif defined(CONFIG_SERIAL4)
#define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART3_OFFSET)
#else
#define ELFIN_UART_CONSOLE_BASE (ELFIN_UART_BASE + ELFIN_UART0_OFFSET)
#endif

(4)修改smdkv210single.h中定义的CONFIG_SERIAL3为CONFIG_SERIAL1

/*
 * select serial console configuration
 */

//#define CONFIG_SERIAL3          1	/* we use UART1 on SMDKC110 */
#define CONFIG_SERIAL1          1	/* we use UART0 on SMDKC110 */

 

修改默认网络地址

(1)修改配置头文件smdkv210single.h中的CONFIG_IPADDR等宏,则可以修改uboot的默认环境变量中网络相关的部分

(2)更改完成后烧录并运行,如果环境变量不是刚才修改的值说明iNand中的环境变量依然有效,所以当前仍然使用的是iNand中的那一份环境变量。我们在uboot源代码中修改的只是默认的环境变量。解决方案是擦除掉iNand中的那一份环境变量,然后迫使uboot启动时使用uboot代码中自带的默认的这一份环境变量,就可以看到了。 

(3)可以使用mmc write 0 30000000 11# 32(表示将DDR的0x30000000开头的一段内存中的内容写入iNand中的17号(0x11)扇区开始的32个扇区内,写入长度是32个扇区长度(16KB))。环境变量分区在iNand的17号扇区,大小32个扇区大小。

修改行提示符

(1) 修改配置头文件smdkv210single.h中的CFG_PROMPT宏可以可以修改行提示符信息。

网卡驱动移植 

我们之前做实验时将串口改为了串口0,而内核zImage-qt的串口输出在串口2 

 uboot的移植2-从uboot官方标准uboot开始移植

 

uboot杂记-logo显示和fastboot原理等 

新旧版本开发板的LCD模组差异
(1)2015.11月初之前购买的X210开发板都属于老版本,型号是X210V3;之后购买的开发板都是新版本的,型号是X210V3S。
(2)两个开发板主要电路是完全一样的,不同主要有3点:一个是把拨码开关换成了短路帽;另一个是LCD分辨率从800*480升级成1024*600;第三个是触摸屏芯片型号换了。

本人使用的是X210V3,配套的LCD为LCD070HD

背光部分原理图分析

X210BV3S为我当前使用的开发板的底板

其中与背光有关的引脚有2个:PWMTOUT0和DISP,下面是触摸屏的PWM调节背光部分的原理图,使用MP3202芯片来做调光IC

分析原理图和MP3202的数据手册,可以得出结论:
第一:PWMTOUT0(GPD0_0)接在了IC的FB引脚上,SoC应该通过该引脚输出一个频率合适的PWM波形给调光IC,这个波形的占空比就会控制MP3202输出的电流大小,从而控制屏幕亮度。
第二:L_DISP(DISP、SYS_OE、GPF3_5)接在了MP3202的EN引脚上,SoC应该给该引脚一个高电平来让背光工作,或者给一个低电平来让背光不工作。
(3)综合分析:背光要点亮,要同时满足以上两个条件。GPD0_0要输出低电平或者PWM信号,同时GPF3_5要输出一个高电平。一般来说我们在uboot中都把GPD0_0设置成输出模式然后输出低电平来点亮背光。

X210的uboot中LCD代码分析

(1)在uboot-jiuding/board.c中init_sequence中的display_banner中的open_backlight函数中给GPF3_5输出高电平。但是这个其实是可以省略的,注释掉这一句uboot的LCD显示照样正常的,主要原因是后面LCD操作的部分还会再做一遍的。

(2)真正的初始化LCD系统并且显示logo是在start_armboot函数的后段的x210_preboot_init中。这个函数纯粹是九鼎在移植时添加的。如果我们自己移植uboot可以考虑自己去添加。 

x210_preboot_init

  • 该函数定义在x210.c中,它里面调用了uboot/drivers/video/mpadfb.c中的mpadfb_init函数来完成LCD的初始化以及显示,这是一种分层的思想
  • 流程分析

(1)fb_init。根据LCD的属性信息填充info结构体成员

(2)lcd_port_init。初始化LCD相关端口

(3)lcd_reg_init。初始化LCD相关寄存器

(4)display_logo。显示logo

(5)backlight_brigness_init。开背光

什么是fastboot

(1)fastboot是android使用的一种刷机方法.android系统设计了2种刷机方式:fastboot和recovery。 

(2)fastboot使用usb作为物理传输。刷机其实就是镜像传输+烧录,fastboot刷机时就是通过usb线来传输镜像的。

(3)fastboot是uboot中的一个命令。uboot进入命令行中后,如果需要刷机,则可以在命令行执行fastboot命令就可以让uboot进入fastboot模式,刷机就是在fastboot模式下进行的。

(4)fastboot需要主机端的fastboot软件配合。要实现fastboot刷机,只有开发板端uboot是不行的,还需要在主机上有fastboot.exe的软件配合。

(5)fastboot在开发板和主机间定义了一套协议。这套协议以usb为底层传输物理层,协议规定了主机fastboot软件和开发板fastboot软件之间的信息传输规则。消息传递可以实现功能有:主机可以向开发板发送命令、开发板可以向主机发送回复、主机可以向开发板发送文件(download)

fastboot工作原理

(1)uboot的fastboot命令将开发板伪装成一个usb从设备。开发板本身并不是一个usb设备,所以开发板直接插到电脑上电脑是没有反应,没有提示发现设备需要装驱动的。伪装之后开发板就被主机windows识别成一个安卓手机了。

(2)主机的fastboot软件和开发板的fastboot程序通信来工作。平时工作时,开发板端只要执行了fastboot命令进入fastboot模式即可,剩下的就不用管了。主机端通过运行fastboot命令,传递不同的参数来实现主机端和开发板端的通信。
譬如主机端执行fastboot devices,则这个命令通过USB线被传递到开发板中被开发板的fastboot程序接收,接收后去处理然后向主机端发送反馈信息,主机端接收到反馈信息后显示出来。

(3)我们这里的主要分析点就是uboot如何进入fastboot模式,fastboot模式下如何响应主机发送的各种命令。

do_fastboot函数

do_fastboot函数本身涉及到很多SD/Nand等磁盘的操作,主要目的是为了刷机。要完整的分析fastboot的函数细节很复杂很麻烦,我们只分析fastboot模式如何响应主机发送的各种命令。 

do_fastboot
    fastboot_init初始化fastboot_interface结构体变量
    fastboot_poll
        fboot_usb_int_hndlr
            fboot_usb_pkt_receive
                fboot_usb_int_bulkout
                    fastboot_interface->rx_handler(函数指针,分析fastboot_init可知它指向cmd_fastboot.c/rx_handler)

此过程涉及到USB物理层通信的一些概念和理解,相对比较复杂。最终uboot这边的fastboot是通过rx_handler函数来处理主机端fastboot软件发送过来的信息的。fastboot协议的命令实现都在这个函数中提现。所以这个函数的分析就是重点。

rx_handler函数

  • rx_handler函数中通过if和else分成了两部分,if部分负责处理download,else部分负责处理命令。usb传输单次传输最大只能有限个字节(64、256),因此当我们发送比较小的东西(譬如命令)时可以单次传输完毕;当我们发送比较大的东西(譬如文件)时就必须要分包发送。
  • download

(1)开发板端通过fastboot_tx_status函数向主机发送响应,主机显示这个响应。

  • 处理命令

(1)uboot中fastboot有关的一些宏定义设置值

CFG_FASTBOOT_TRANSFER_BUFFER    配置fastboot工作时的缓冲区地址,fastboot在执行某些功能时需要大块内存做缓冲区(譬如download时),这里就是在给他配置缓冲区。

CFG_FASTBOOT_TRANSFER_BUFFER_SIZE  fastboot缓冲区的大小。

(2)fastboot命令响应

fastboot reboot。重启开发板

fastboot getvar。得到一些fastboot中定义的变量名的值,譬如version、product等

fastboot oem。oem命令是用户自定义的。其他命令全都是fastboot协议定义的,但是有时候自带的命令不足以使用,oem厂商可能希望定义一些自己专有的命令,则可以使用oem命令。

话说linux内核 

操作系统核心功能:
(1)内存管理。如果没有操作系统,内存是需要程序自己来管理的。譬如在uboot中要使用内存的哪里是自己随便用的,没有注册也没有限制。这时候如果程序自己不小心把同一块内存重复用了就会出现程序逻辑错误。系统大了之后(内存多了)内存管理非常麻烦;有了操作系统之后,操作系统负责管控所有的内存,所有的应用程序需要使用内存时都要向操作系统去申请和注册,由操作系统的内存管理模块来分配内存给你使用,这样好处是可以保证内存使用不会冲突。
(2)进程调度。操作系统下支持多个应用程序同时运行(所以可以一边聊QQ一边看电影···),这是宏观上的并行。实际上在单核心CPU上微观上是不能并行的,宏观上的并行就是操作系统提供的分时复用机制。操作系统的进程调度模块负责在各个进程之间进行切换。
(3)硬件设备管理。没有操作系统时要控制任何硬件都要自己写代码,有了操作系统后操作系统本身会去控制各个硬件,应用程序就不用考虑硬件的具体细节了。操作系统的硬件设备管理模块就是驱动模块。
(4)文件系统。文件系统是管理存储设备的一种方式。存储设备是由很多个扇区组成的,每个扇区有512/1024/2048/4096字节,存储设备要以扇区为单位进行读写。如果没有文件系统,程序要自己去读写扇区,就得记得哪个文件在哪个扇区。有了文件系统之后我们人不用再关注扇区,人只用关注文件系统中的目录和文件名,而不用管这个文件在物理磁盘的哪个扇区。

内核和发行版的区别
区别:内核是操作系统内核的简称,内核负责实现操作系统的核心功能(资源管理模块,譬如内存管理、调度系统······),内核不包括应用程序。所以说只有内核人是没法用的,因为人做任何事情都是通过相应的应用程序来完成的。所以卖操作系统的人把内核和一些常用的应用程序打包在一起提供给普通用户,这就是操作系统的发行版(也就是普通意义上的操作系统)。
(1)内核只有一个。www.kernel.org
(2)发行版有很多。譬如ubuntu、redhat、suse、centos······ 

驱动属于内核的一部分
(1)驱动就是内核中的硬件设备管理模块
(2)驱动工作在内核态。
(3)驱动程序故障可能导致整个内核崩溃
(4)驱动程序漏洞会使内核不安全 

应用和内核的关系
(1)应用程序不属于内核,而是在内核之上的
(2)应用程序工作在用户态,是受限制的。
(3)应用程序故障不会导致内核崩溃
(4)应用程序通过内核定义的API接口来调用内核工作
(5)总结1:应用程序是最终目标
(6)总结2:内核就是为应用程序提供底层资源管理的服务员 

内核和根文件系统
(1)根文件系统提供根目录。
(2)进程1存放在根文件系统中
(3)内核启动最后会去装载根文件系统。
(4)总结:根文件系统为操作系统启动提供了很多必备的资源:根目录、进程1 

linux内核版本变迁简史
(1)linux0.01。初版
(2)linux0.11。很多讲linux内核源代码解析的书都是以这个版本为原本来讲。《图解linux内核设计的艺术》
(3)linux2.4。比较接近现代的版本,很多经典的书都是以2.4版本内核为参照的,譬如《LDD3》。linux2.4的晚期内核在前几年还会经常碰到有用的。
(4)linux2.6早期。2.6的早期和2.4晚期内核挺像的。
(5)linux2.6晚期。2.6的晚期内核较早期内核有一些改变,尤其是驱动相关的部分和一些头文件的位置。2.6的晚期内核目前还算是比较主流。
(6)linux3.x 4.x 

如何选择合适的内核版本
(1)并不是越新版本的内核越好
(2)选择SoC厂家移植版本会减少工作量 

S5PV210适用的内核版本
(1)2.6.35.7+android2.3/QT4.8.3
(2)3.0.8+android4.0 

这里使用2.6.35.7版本内核讲解 

 内核的配置和编译原理

这里分析的是九鼎为x210移植的linux版本,是2.6.35.7版本的内核,在X210V3S_B/linux/QT4.8/bsp/qt_x210v3s_160307.tar.bz2里的kernel文件夹下

源码目录下的单个文件

  • Kbuild,Kbuild是kernel build的意思,就是内核编译的意思。这个文件就是linux内核特有的内核编译体系需要用到的文件。
  • Makefile,这个是linux内核的总makefile,整个内核工程用这个Makefile来管理的。
  • mk,是九鼎在移植时自己添加的,不是linux内核本身的东西。九鼎添加这个文件的作用是用这个文件来整个管理kernel目录的配置和编译

Kbuild、Kconfig等文件,都是和内核的配置体系有关的。

源码目录下的文件夹

  • arch。arch是architecture的缩写,意思是架构。arch目录下是好多个不同架构的CPU的子目录,譬如arm这种cpu的所有文件都在arch/arm目录下,X86的CPU的所有文件都在arch/x86目录下。
  • block。英文是块的意思,在linux中block表示块设备(以块(多个字节组成的整体,类似于扇区)为单位来整体访问),譬如说SD卡、iNand、Nand、硬盘等都是块设备。你几乎可以认为块设备就是存储设备。block目录下放的是一些linux存储体系中关于块设备管理的代码。
  • crypto。英文意思是加密。这个目录下放了一些各种常见的加密算法的C语言代码实现。譬如crc32、md5、sha1等。 
  • Documentation。里面放了一些文档。
  • drivers。驱动目录,里面分门别类的列出了linux内核支持的所有硬件设备的驱动源代码。
  • firmware。固件。
  • fs。fs就是file system,文件系统,里面列出了linux支持的各种文件系统的实现。
  • include。头文件目录,公共的(各种CPU架构共用的)头文件都在这里。每种CPU架构特有的一些头文件在arch/arm/include目录及其子目录下。
  • init。init是初始化的意思,这个目录下的代码就是linux内核启动时初始化内核的代码。
  • ipc。ipc就是inter process commuication,进程间通信,里面都是linux支持的IPC的代码实现。
  • kernel。kernel就是内核,就是linux内核,所以这个文件夹下放的就是内核本身需要的一些代码文件。
  • lib。lib是库的意思,这里面都是一些公用的有用的库函数,注意这里的库函数和C语言的库函数不一样的。在内核编程中是不能用C语言标准库函数,这里的lib目录下的库函数就是用来替代那些标准库函数的。譬如在内核中要把字符串转成数字用atoi,但是内核编程中只能用lib目录下的atoi函数,不能用标准C语言库中的atoi。譬如在内核中要打印信息时不能用printf,而要用printk,这个printk就是我们这个lib目录下的。
  • mm。mm是memory management,内存管理,linux的内存管理代码都在这里。
  • net。该目录下是网络相关的代码,譬如TCP/IP协议栈等都在这里。
  • scripts。脚本,这个目录下全部是脚本文件,这些脚本文件不是linux内核工作时使用的,而是用来辅助对linux内核进行配置编译生产的。我们并不会详细进入分析这个目录下的脚本,而是通过外围来重点学会配置和编译linux内核即可。
  • security。安全相关的代码。
  • sound。音频处理相关的。
  • tools。linux中用到的一些有用工具
  • usr。目录下是initramfs相关的,和linux内核的启动有关
  • virt。内核虚拟机相关的

内核代码基本分为3块
(1)arch。        本目录下全是cpu架构有关的代码
(2)drivers        本目录下全是硬件的驱动
(3)其他            相同点是这些代码都和硬件无关,因此系统移植和驱动开发的时候这些代码几乎都是不用关注的。

架构相关的常用目录名及含义
(1)mach。(mach就是machine architecture)。arch/arm目录下的一个mach-xx目录就表示一类machine的定义,这类machine的共同点是都用xx这个cpu来做主芯片。(譬如mach-s5pv210这个文件夹里面都是s5pv210这个主芯片的开发板machine);mach-xx目录里面的一个mach-yy.c文件中定义了一个开发板(一个开发板对应一个机器码),这个是可以被扩展的。
(2)plat(plat是platform的缩写,含义是平台)plat在这里可以理解为SoC,也就是说这个plat目录下都是SoC里面的一些硬件(内部外设)相关的一些代码。
在内核中把SoC内部外设相关的硬件操作代码就叫做平台设备驱动。
(3)include。这个include目录中的所有代码都是架构相关的头文件。(linux内核通用的头文件在内核源码树根目录下的include目录里)

补充
(1)内核中的文件结构很庞大、很凌乱(不同版本的内核可能一个文件存放的位置是不同的),会给我们初学者带来一定的困扰。
(2)头文件目录include有好几个,譬如:
    kernel/include        内核通用头文件
    kernel/arch/arm/include        架构相关的头文件
        kernel/arch/arm/include/asm
            kernel\arch\arm\include\asm\mach
    kernel\arch\arm\mach-s5pv210\include\mach
    kernel\arch\arm\plat-s5p\include\plat
(3)内核中包含头文件时有一些格式

#include <linux/kernel.h>        kernel/include/linux/kernel.h
#include <asm/mach/arch.h>        kernel/arch/arm/include/asm/mach/arch.h
#include <asm/setup.h>            kernel\arch\arm\include\asm/setup.h
#include <plat/s5pv210.h>        kernel\arch\arm\plat-s5p\include\plat/s5pv210.h

(4)有些同名的头文件是有包含关系的,有时候我们需要包含某个头文件时可能并不是直接包含他,而是包含一个包含了他的头文件。

内核配置和编译

(1)确认Makefile

  • 检查交叉编译工具链有没有设置对。CROSS_COMPILE   ?= /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
  • 确认ARCH = arm。主要目的是为了编译时能找到arch/arm目录。

(2)make x210ii_qt_defconfig

  • 最后只要出现:configuration written to .config这句话,就证明我们的操作是正确的。
  • 成功之后会在源码根目录下生成.config文件。只有在得到该文件后,执行下一步才是有效的。

(3)make menuconfig

  • 执行之前确保安装了ncurses库。通过执行apt-get install libncurses5-dev进行安装
  • 确保命令行窗口够大,否则会出现错误
  • 根据需要进行配置

(4)make

  • 编译完成后得到的内核镜像在arch/arm/boot这个目录下。得到的镜像名是zImage

内核的配置原理

  • 配置的关键是得到.config文件,是一个隐藏文件,因此平时是看不到的,需要ls -a来查看
  •  make x210ii_qt_defconfig和make menuconfig就是为了得到我们需要的合适的.config文件
  • .config文件其作用类似与uboot中的/include/autoconf.mk文件,其中内容为类似于:CONFIG_ARM=y的一个一个的配置项。这些配置项就类似于脚本文件中定义的一个一个变量,所以这一行可以被理解为定义了一个变量CONFIG_ARM,这个变量的值为y。内核在编译过程中会读取.config中的配置项,并且用这些配置项去指导整个编译链接过程。
  • 第一步:make x210ii_qt_defconfig解决的问题是大部分的配置项(这一步结束后99%的配置项就已经正确了),下来就是对个别不同的针对我们的开发板进行细节调整,细节调整就通过make menuconfig来完成。
  • make x210ii_qt_defconfig其实相当于:cp arch/arm/configs/x210ii_qt_defconfig .config
  • make x210ii_qt_defconfig这一步其实是参考别人已经做好的,这样做有很多好处:减少很多工作量,避开了很多自己不懂的配置项(譬如对内存管理的、调度系统的等模块的配置项),我们只用管自己需要管的。
  • make menuconfig其实就是读取第一步得到的.config,然后给我们一个图形化的界面,让我们可以更加容易的找到自己想要修改的配置项,然后更改配置他。

menuconfig的使用

箭头按键导航整个菜单,回车按键选择子菜单(注意选项后面有 --->的选项才是有子菜单的,没有这个标识的没有子菜单),高亮的字母是热键(快捷键),键盘按键Y、N、M三个按键的作用分别是将选中模块编入、去除、模块化。双击ESC表示退出,按下?按键可以显示帮助信息,按下/按键可以输入搜索内容来全局搜索信息(类似于vi中的搜索),[]不可以模块化,<>的才可以模块化。 

linux内核中一个功能模块有三种编译方法:一种是编入、一种去去除、一种是模块化。所谓编入就是将这个模块的代码直接编译连接到zImage中去,去除就是将这个模块不编译链接到zImage中,模块化是将这个模块仍然编译,但是不会将其链接到zImage中,会将这个模块单独链接成一个内核模块.ko文件,将来linux系统内核启动起来后可以动态的加载或卸载这个模块。
在menuconfig中选项前面的括号里,*表示编入,空白表示去除,M表示模块化

menuconfig的工作原理

  • menuconfig本身由一套软件支持

(1)linux为了实现图形化界面的配置,专门提供了一套配置工具menuconfig。

(2)ncurses库是linux中用来实现文字式的图形界面,linux内核中使用了ncurses库来提供menuconfig

(3)scripts\kconfig\lxdialog目录下的一些c文件就是用来提供menuconfig的那些程序源代码。

  • menuconfig读取Kconfig文件

(1)menuconfig本身的软件只负责提供menuconfig工作的这一套逻辑(譬如在menuconfig中通过上下左右箭头按键来调整光标,Enter ESC键等按键按下的响应),而并不负责提供内容(菜单里的项目)。

(2)menuconfig显示的菜单内容(一方面是菜单的目录结构,另一方面是每一个菜单项目的细节)是由内核源码树各个目录下的Kconfig文件来支持的。Kconfig文件中按照一定的格式包含了一个又一个的配置项,每一个配置项在make menuconfig中都会成为一个菜单项目。而且menuconfig中显示的菜单目录结构和源码目录中的Kconfig的目录结构是一样的。

  • menuconfig读取/写入.config文件

(1)menuconfig工作时在我们make menuconfig打开时,他会读取.config文件,并且用.config文件中的配置选择结果来初始化menuconfig中各个菜单项的选择值。

(2)当我们每次退出make menuconfig时,menuconfig机制会首先检查我们有没有更改某些配置项的值,如果我们本次没有更改过任意一个配置项目的值那直接退出;如果我们有改动配置项的值则会提示我们是否保存。此时如果点保存,则会将我们更改过的配置重新写入.config文件中记录,下一次再次打开make menuconfig时会再次加载.config,最终去编译内核时编译连接程序会考虑.config中的配置值指导整个编译连接过程。

Kconfig文件

  • Kconfig按照一定的格式来书写,menuconfig程序可以识别这种格式,然后从中提取出有效信息组成menuconfig中的菜单项
  • menuconfig表示菜单(本身属于一个菜单中的项目,但是他又有子菜单项目)、config表示菜单中的一个配置项(本身并没有子菜单下的项目)。
  • menuconfig或者config后面空格隔开的大写字母表示的类似于 NETDEVICES 的就是这个配置项的配置项名字,这个字符串前面添加CONFIG_后就构成了.config中的配置项名字。
  • 一个menuconfig后面跟着的所有config项就是这个menuconfig的子菜单。这就是Kconfig中表示的目录关系。
  • 内核源码目录树中每一个Kconfig都会source引入其所有子目录下的Kconfig,从而保证了所有的Kconfig项目都被包含进menuconfig中。这个也告诉我们:如果你自己在linux内核中添加了一个文件夹,一定要在这个文件夹下创建一个Kconfig文件,然后在这个文件夹的上一层目录的Kconfig中source引入这个文件夹下的Kconfig文件。
  • tristate意思是三态(3种状态,对应Y、N、M三种选择方式),bool是要么真要么假(对应Y和N)。所以tristate的意思就是这个配置项可以被三种选择,bool的意思是这个配置项只能被2种选择。
  • depends表示本配置项依赖于另一个配置项。如果那个依赖的配置项为Y或者M,则本配置项才有意义;如果依赖的哪个配置项本身被设置为N,则本配置项根本没有意义。depends项目会导致make menuconfig的时候找不到一些配置项。所以你在menuconfig中如果找不到一个选项,但是这个选项在Kconfig中却是有的,则可能的原因就是这个配置项依赖的一个配置项是不成立的。depends并不要求依赖的配置项一定是一个,可以是多个,而且还可以有逻辑运算。这种时候只要依赖项目运算式子的逻辑结果为真则依赖就成立。 
  • select表示选中当前配置项,则select后的项目也会被选中
  • default表示当前配置项的默认值
  • help帮助信息,告诉我们这个配置项的含义,以及如何去配置他。

Kconfig和.config文件和Makefile三者的关联

  • 配置项被配置成Y、N、M会影响.config文件中的CONFIG_XXX变量的配置值。
  • 这个.config中的配置值(=y、=m、没有)会影响最终的编译链接过程。如果=y则会被编入(built-in),如果=m会被单独连接成一个ko模块,如果没有则对应的代码不会被编译。这些都是通过makefile实现的。 
  • obj-$(CONFIG_DM9000) += dm9000.o
    如果CONFIG_DM9000变量值为y,则obj-y += dm9000.o,因此dm9000.c会被编译;如果CONFIG_DM9000变量未定义,则dm9000.c不会被编译。如果CONFIG_DM9000变量的值为m则会被连接成ko模块(这个是在linux内核的Makefile中定义的规则)

内核的启动过程分析 

x210:uboot和系统移植扩展--内核启动之解压缩阶段

x210:uboot和系统移植扩展--内核启动之汇编初始化阶段

x210:uboot和系统移植扩展--内核启动之C语言阶段

内核启动过程图(该图不完全匹配当前版本内核,仅供参考)

 内核的移植1-从三星官方内核开始移植

修改解压后内核代码(image)的放置地址&tag传参的首地址

  • 尝试运行三星官方版本的内核发现没有打印出 Uncompressing Linux... done, booting the kernel. 
  • 解压后内核代码的放置地址应该等于内核代码(不包括前面的自解压代码)的链接地址,否则解压之后内核无法运行
  • 内核的连接地址是一个虚拟地址,而自解压代码解压内核时需要物理地址,因此上面说的等于,其实是链接地址对应的物理地址等于自解压地址
  • 链接地址和他对应的物理地址在arch/arm/kernel/head.S中可以查到,分别是0xC0008000(从make时生成的arch/arm/kernel/vmlinux.lds也可以得到其链接地址)和0x30008000。那么自解压代码配置的解压地址应该是30008000
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR	(PHYS_OFFSET + TEXT_OFFSET)
  • 解压后内核代码的放置地址在arch/arm/mach-s5pv210/Makefile.boot文件中,由zreladdr-y来控制,这里将其设置为0x30008000,即
  • zreladdr-$(CONFIG_MACH_SMDKV210)	:= 0x30008000

    在arch/arm/mach-s5pv210/Makefile.boot文件中,params_phys-y用来定义linux内核接收的tag传参的首地址。之前在uboot中tag传参的值放在了以0x30000100开始的地址处,这里也需要将其修改为该地址,即

    params_phys-$(CONFIG_MACH_SMDKV210)	:= 0x30000100

修改内核(image)链接地址的物理地址

修改arch/arm/mach-s5pv210/include/mach/memory.h文件中的PHYS_OFFSET宏的值为0x30000000

内核中机器码的确定

  • MACHINE_START宏

(1)这个宏用来定义一个机器相关的数据结构。这个宏的使用其实是用来定义一个结构体类型为machine_desc类型的结构体变量,名为例如__mach_desc_SMDKV210。这个结构体变量会被定义到一个特定段.arch.info.init,因此这个结构体变量将来会被链接器链接到这个.arch.info.init段中。

(2)经过分析,发现一个mach-xxx.c文件中定义了一个机器码的开发板的machine_desc结构体变量,这个结构体变量放到.arch.info.init段中后,那么就表示当前内核可以支持这个机器码的开发板。

(3)对于x210开发板,我们基于三星的SMDKV210开发板来移植。但是定义该机器的machine_desc结构体变量在arch/arm/mach-s5pv210/mach-smdkc110.c文件中,可通过查看该目录下的Makefile文件得知该情况

#ifdef CONFIG_MACH_SMDKC110
MACHINE_START(SMDKC110, "SMDKC110")
#elif CONFIG_MACH_SMDKV210
MACHINE_START(SMDKV210, "SMDKV210")
#endif
        /* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
        .phys_io        = S3C_PA_UART & 0xfff00000,
        .io_pg_offst    = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
        .boot_params    = S5P_PA_SDRAM + 0x100,
        //.fixup                = smdkv210_fixup,
        .init_irq       = s5pv210_init_irq,
        .map_io         = smdkc110_map_io,
        .init_machine   = smdkc110_machine_init,
        .timer          = &s5p_systimer,
MACHINE_END

解析开来就是

static const struct machine_desc __mach_desc_SMDKV210	\
 __used							\
 __attribute__((__section__(".arch.info.init"))) = {	\
	.nr		= MACH_TYPE_SMDKV210,		\
	.name		= "SMDKV210",
	.phys_io	= S3C_PA_UART & 0xfff00000,
	.io_pg_offst	= (((u32)S3C_VA_UART) >> 18) & 0xfffc,
	.boot_params	= S5P_PA_SDRAM + 0x100,
	.init_irq	= s5pv210_init_irq,
	.map_io		= smdkv210_map_io,
	.init_machine	= smdkv210_machine_init,
	.timer		= &s5p_systimer,
};

这里的nr就是MACH_TYPE_SMDKV210,它的值为2456,也就是机器码

init_machine是硬件驱动的加载和初始化函数,这个函数中绑定了我们这个开发板linux内核启动过程中会初始化的各种硬件的信息。在linux初始化时会间接调用该函数

去除max8698电源管理IC相关代码

  • 逐行查看Linux启动过程中的打印信息发现如下错误

Unable to handle kernel NULL pointer dereference at virtual address 00000060
Internal error: Oops: 5 [#1] PREEMPT
PC is at dev_driver_string+0xc/0x44
LR is at max8698_pmic_probe+0x150/0x32c

  • 从以上错误信息中的PC和LR的值可以看出,程序是执行到dev_driver_string或者max8698_pmic_probe(这两个是函数或者汇编中的标号)符号部分的时候出错了。我们就从这两个符号出发去寻找、思考可能出错的地方然后试图去解决。
  • max8698_pmic_probe看名字是max8698这个电源管理IC的驱动安装函数部分出错了,应该是我们的开发板系统中配置了支持这个电源管理IC,于是乎启动时去加载他的驱动,结果驱动在加载执行的过程中出错了。
  • 我们X210开发板上根本就没有max8698这个电源管理IC,既然硬件都没有驱动执行了肯定会出错。
  • 三星的SMDKV210开发板中是用了max8698这个电源管理IC的,所以三星的uboot和kernel中都有默认支持这个。但是X210中是没用的,因此都需要去掉。
  • 解决方法:make menuconfig,然后搜索"MAX8698"这几个关键字,然后看到这个配置项的路径,然后到路径下去按N键去掉这个模块的支持,保存,重新编译即可
  • CONFIG_MFD_MAX8698这个配置宏决定了max8698相关的代码是否会被运行
  • max8698是基于iic总线的,需要i2c_driver和i2c_client的匹配。i2c_client在arch/arm/mach-s5pv210/mach-smdkc110.c中有
i2c_register_board_info(2, i2c_devs2, ARRAY_SIZE(i2c_devs2));
/* I2C2 */
static struct i2c_board_info i2c_devs2[] __initdata = {
	{
#if defined(CONFIG_SMDKC110_REV03) || defined(CONFIG_SMDKV210_REV02)
		/* The address is 0xCC used since SRAD = 0 */
		I2C_BOARD_INFO("max8998", (0xCC >> 1)),
		.platform_data = &max8998_pdata,
#else
		/* The address is 0xCC used since SRAD = 0 */
		I2C_BOARD_INFO("max8698", (0xCC >> 1)),
		.platform_data = &max8698_pdata,
#endif
	},
};

这里的max8698就是用来和i2c_driver里边的name进行匹配的。

i2c_driver在drivers/mfd/max8698.c中,只要修改CONFIG_MFD_MAX8698宏为n就不会编译该文件,也就不会使的i2c_client和i2c_driver匹配上,也就不会执行max8698相关的代码

iNand问题(未解决)

 

网卡驱动的移植和添加

  • 网卡驱动移植ok时,启动信息为:

[    1.452008] dm9000 Ethernet Driver, V1.31
[    1.455870] eth0: dm9000c at e08f4300,e08f8304 IRQ 42 MAC: 00:09:c0:ff:ec:48 (platform data)

  • 当前内核中网卡驱动尚未移植,因此内核启动时有错误的打印信息:

[    1.130308] dm9000 Ethernet Driver, V1.31
[    1.133113] ERROR : resetting 
[    1.135700] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.140915] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.145941] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.150963] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.155992] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.161018] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.166041] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.171070] dm9000 dm9000.0: read wrong id 0x2b2a2928
[    1.176092] dm9000 dm9000.0: wrong id: 0x2b2a2928
[    1.180774] dm9000 dm9000.0: not found (-19).

(1)make menuconfig中添加DM9000支持,内核中已经默认添加了所以不用执行

(2)在arch/arm/mach-s5pv210/mach-smdkv210.c中smdkc110_machine_init函数中有

#ifdef CONFIG_DM9000
	smdkc110_dm9000_set();
#endif

smdkc110_dm9000_set这个函数就是DM9000相关的SROM bank的寄存器设置,相当于uboot中dm9000移植时的dm9000_pre_init函数。只是读写寄存器的函数名称不同了。

(3)修改相应的配置参数

DM9000相关的数据配置在arch/arm/plat-s5p/devs.c中更改

在arch/arm/mach-s5pv210/include/mach/map.h中定义了DM9000的IO基地址,和DM9000接在哪个bank

还有+2改成+4,IRQ_EINT9改成10即可

根文件系统的原理 

为什么需要根文件系统
(1)init进程的应用程序在根文件系统上
(2)根文件系统提供了根目录/
(3)内核启动后的应用层配置(etc目录)在根文件系统上。几乎可以认为:发行版=内核+rootfs
(4)shell命令程序在根文件系统上。譬如ls、cd等命令
总结:一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin  /sbin等目录下的shell命令,还有/lib目录下的库文件等···)相配合才能工作。

根文件系统的实质是什么
(1)根文件系统是特殊用途的文件系统。
(2)根文件系统也必须属于某种文件系统格式。rootfstype=

根文件系统的形式

  • 镜像文件形式

(1)使用专用工具软件制作的可供烧录的镜像文件
(2)镜像中包含了根文件系统中的所有文件
(3)烧录此镜像类似于对相应分区格式化。
(4)镜像文件系统具有一定的格式,格式是内化的,跟文件名后缀是无关的。

  • 文件夹形式

(1)根文件系统其实就是一个包含特定内容的文件夹而已
(2)根文件系统可由任何一个空文件夹添加必要文件构成而成
(3)根文件系统的雏形就是在开发主机中构造的文件夹形式的

  • 总结

(1)镜像文件形式的根文件系统主要目的是用来烧录到块设备上,设备上的内核启动后去挂载它。镜像文件形式的根文件系统是由文件夹形式的根文件系统使用专用的镜像制作工具制作而成的。
(2)最初在开发主机中随便mkdir创建了一个空文件夹,然后向其中添加一些必要的文件(包括etc目录下的运行时配置文件、/bin等目录下的可执行程序、/lib目录下的库文件等···)后就形成了一个文件夹形式的rootfs。然后这个文件夹形式的rootfs可以被kernel通过nfs方式来远程挂载使用,但是不能用来烧录块设备。我们为了将这个rootfs烧录到块设备中于是用一些专用的软件工具将其制作成可供烧录的一定格式的根文件系统镜像。
(3)文件夹形式的rootfs是没有格式的,制作成镜像后就有了一定的rootfs格式了,格式是由我们的镜像制作过程和制作工具来决定的。每一种格式的镜像制作工具的用法都不同。

镜像制作工具mke2fs介绍
(1)mke2fs是一个应用程序,在ubuntu中默认是安装了的。这个应用程序就是用来制作ext2、ext3、ext4等格式的根文件系统的。
(2)一般用来制作各种不同格式的rootfs的应用程序的名字都很相似,类似于mkfs.xxx(譬如用来制作ext2格式的rootfs的工具叫mkfs.ext2、用来制作jffs2格式的rootfs的工具就叫mkfs.jffs2)
(3)ubuntu14.04中的mkfs.ext2等都是mke2fs的符号链接而已。

制作ext2格式的根文件系统

  • 创建rootfs.ext2文件并且将之挂载到一个目录下方便访问它。bs表示块大小,count表示块个数

dd if=/dev/zero of=rootfs.ext2 bs=1024 count=2048
losetup  /dev/loop1 rootfs.ext2
mke2fs -m 0 /dev/loop1 2048
mount -t ext2 /dev/loop1 ./rootfs/

  •  

nfs方式启动文件夹形式的rootfs

  • 什么是nfs

(1)nfs是一种网络通讯协议,由服务器和客户端构成。

(2)利用nfs协议可以做出很多直接性应用,我们这里使用nfs主要是做rootfs挂载。开发板中运行kernel做nfs客户端,主机ubuntu中搭建nfs服务器。在主机ubuntu的nfs服务器中导出我们制作的文件夹形式的rootfs目录,则在客户端中就可以去挂载这个文件夹形式的rootfs进而去启动系统。

  • 搭建nfs服务器
  • 配置内核以支持nfs作为rootfs

(1)设置nfs启动方式的bootargs

(2)在menuconfig中配置支持nfs启动方式

  • 总结

(1)nfs方式启动相当于开发板上的内核远程挂载到主机上的rootfs

(2)nfs方式启动不用制作rootfs镜像

(3)nfs方式不适合真正的产品,一般作为产品开发阶段调试使用

linuxrc

(1)/linuxrc是一个可执行的应用程序

  • /linuxrc是应用层的,和内核源码一点关系都没有
  • /linuxrc在开发板当前内核系统下是可执行的。因此在ARM SoC的linux系统下,这个应用程序就是用arm-linux-gcc编译链接的;如果是在PC机linux系统下,那么这个程序就是用gcc编译连接的。
  • /linuxrc如果是静态编译连接的那么直接可以运行;如果是动态编译连接的那么我们还必须给他提供必要的库文件才能运行。但是因为我们/linuxrc这个程序是由内核直接调用执行的,因此用户没有机会去导出库文件的路径,因此实际上这个/linuxrc没法动态连接,一般都是静态连接的。

(2)/linuxrc负责系统启动后的配置

  • 操作系统启动起来后也不能直接用,要配置下。
  • 操作系统启动后的应用层的配置(一般叫运行时配置,英文简写etc)是为了让我们的操作系统用起来更方便,更适合我个人的爱好或者实用性。

(3)/linuxrc执行时引出用户界面

  • 操作系统启动后在一系列的自己运行配置之后,最终会给用户一个操作界面(也许是cmdline,也许是GUI),这个用户操作界面就是由/linuxrc带出来的。
  • 用户界面等很多事并不是在/linuxrc程序中负责的,用户界面有自己专门的应用程序,但是用户界面的应用程序是直接或者间接的被/linuxrc调用执行的。

(4)/linuxrc在嵌入式linux中一般就是busybox

  • busybox是一个C语言写出来的项目,里面包含了很多.c文件和.h文件。这个项目可以被配置编译成各个平台下面可以运行的应用程序。我们如果用arm-linux-gcc来编译busybox就会得到一个可以在我们开发板linux内核上运行的应用程序。
  • busybox这个程序开发出来就是为了在嵌入式环境下构建rootfs使用的,也就是说他就是专门开发的init进程应用程序。
  • busybox为当前系统提供了一整套的shell命令程序集。譬如vi、cd、mkdir、ls等。在桌面版的linux发行版(譬如ubuntu、redhat、centOS等)中vi、cd、ls等都是一个一个的单独的应用程序。但是在嵌入式linux中,为了省事我们把vi、cd等所有常用的shell命令集合到一起构成了一个shell命令包,起名叫busybox。

rootfs中应该有什么

  • /linuxrc
  • dev目录下的设备文件。在linux中一切皆是文件,因此一个硬件设备也被虚拟化成一个设备文件来访问,在linux系统中/dev/xxx就表示一个硬件设备,我们要操作这个硬件时就是open打开这个设备文件,然后read/write/ioctl操作这个设备,最后close关闭这个设备。在最小rootfs中/dev目录也是不可少的,这里面有一两个设备文件是rootfs必须的。
  • sys和proc目录。在最小rootfs中也是不可省略的,但是这两个只要创建了空文件夹即可,里面是没东西的,也不用有东西。这两个目录也是和驱动有关的。属于linux中的虚拟文件系统。
  • usr是系统的用户所有的一些文件的存放地,这个东西将来busybox安装时会自动生成。
  • etc目录是很关键很重要的一个,目录中的所有文件全部都是运行时配置文件。/etc目录下的所有配置文件会直接或者间接的被/linuxrc所调用执行,完成操作系统的运行时配置。etc目录是制作rootfs的关键
  • lib目录也是rootfs中很关键的一个,不能省略的一个。lib目录下放的是当前操作系统中的动态和静态链接库文件。我们主要是为了其中的动态链接库。

根文件系统构建实验及过程详解 

busybox的移植实战

(1)busybox源码下载

  • busybox是一个开源项目,源代码可以直接从网上下载
  • busybox的版本差异不大,版本新旧无所谓
  • 下载busybox可以去linuxidc等镜像网站,也可以去www.busybox.net官方网站下载

(2)修改Makefile

  • ARCH = arm
  • CROSS_COMPILE = /usr/local/arm/arm-2009q3/bin//arm-none-linux-gnueabi-

(3)make menuconfig进行配置

Busybox Settings--->
	Build Options--->
		[*]Build BusyBox as a static binary(no shared libs)

		
Busybox Library Tuning--->
	[*]vi-style line editing commands
	[*]Fancy shell prompts
	
	
Linux Module Utilities--->
	[ ]Simplified modutils
	[*]insmod
	[*]rmmod
	[*]lsmod
	[*]modprobe
	[*]depmod

	
Linux System Utilities--->[*]mdev
	[*]Support /etc/mdev.conf
	[*]Support subdirs/symlinks
	[*]Support regular expressions substitutions when renaming dev
	[*]Support command execution at device addition/removal
	[*]Support loading of firmwares

(4)设置busybox的安装目标路径

Settings  --->

(5)make 然后 make install

  • make编译(如果遇到错误,可尝试解决后者通过make menuconfig的方式跳过错误项的编译)
  • make install执行的时候其实是在执行busybox顶层目录下的一个目标install
  • make install在所有的linux下的软件中作用都是安装软件。在传统的linux系统中安装软件时都是选择源代码方式安装的。我们下载要安装的软件源代码,然后配置、编译、安装。make install的目的就是将编译生成的可执行程序及其依赖的库文件、配置文件、头文件安装到当前系统中指定(一般都可以自己指定安装到哪个目录下,如果不指定一般都有个默认目录)的目录下

(6)成功之后再目标路径下生成了如下文件

inittab

(1)inittab文件一般在/etc/目录下

(2)典型的inittab文件实例

#first:run the system script file
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::ctrlaltdel:-/sbin/reboot
#umount all filesystem
::shutdown:/bin/umount -a -r
#restart init process
::restart:/sbin/init

(3)inittab格式解析

  • inittab的工作原理就是被/linuxrc(也就是busybox)执行时所调用起作用。
  • inittab在/etc目录下,所以属于一个运行时配置文件,是文本格式的(内容是由一系列的遵照一个格式组织的字符组成的),实际工作的时候busybox会(按照一定的格式)解析这个inittab文本文件,然后根据解析的内容来决定要怎么工作。
  • inittab的格式在busybox中定义的,网上可以搜索到详细的格式说明

#开始的行是注释

冒号在里面是分隔符,分隔开各个部分

inittab内容是以行为单位的,行与行之间没有关联,每行都是一个独立的配置项,每一个配置项表示一个具体的含义。

每一行的配置项都是由3个冒号分隔开的4个配置值共同确定的。这四个配置值就是id:runlevels:action:process。值得注意得是有些配置值可以空缺,空缺后冒号不能空缺,所以有时候会看到连续2个冒号。

每一行的配置项中4个配置值中最重要的是action和process,action是一个条件/状态,process是一个可被执行的程序的pathname。合起来的意思就是:当满足action的条件时就会执行process这个程序。

busybox最终会进入一个死循环,在这个死循环中去反复检查是否满足各个action的条件,如果某个action的条件满足就会去执行对应的process。

busybox源码分析

(1)函数入口main

  • busybox是linux启动起来后工作的一个应用程序,因此其中必然有main函数,而且main就是入口
  • busybox入口就是main函数,其中有很多个main但是只有一个起作用了,其他的是没起作用的。真正的busybox工作时的入口是libbb/appletlib.c中的main函数
  • busubox中有很多xxx_main函数,这些main函数每一个都是busybox支持的一个命令的真正入口。譬如ls_main函数就是busybox当作ls函数使用时的入口程序。
  • ls或者cd等命令其实都是busybox一个程序,但是实际执行时的效果却是各自的效果。busybox是如何实现一个程序化身万千还能各自工作的?答案就是main转xxx_main。也就是说busybox每次执行时都是先执行其main,在main函数中识别(靠main函数的传参argv[0]来识别)我们真正要执行的函数(譬如ls)然后去调用相应的xxx_main(譬如ls_main)来具体实现这个命令。

(2)inittab解析与执行

  • inittab的解析是在busybox/init/init.c/init_main函数中
  • 执行逻辑是:先通过parse_inittab函数解析/etc/inittab(解析的重点是将inittab中的各个action和process解析出来),然后后面先直接执行sysinit和wait和once(注意这里只执行一遍),然后在while(1)死循环中去执行respwan和askfirst。

busybox的体积优势

(1)busybox实际上就是把ls、cd、mkdir等很多个linux中常用的shell命令集成在一起了。集成在一起后有一个体积优势:就是busybox程序的大小比busybox中实现的那些命令的大小加起来要小很多。
(2)busybox体系变小的原因主要有2个:第一个是busybox本身提供的shell命令是阉割版的(busybox中的命令支持的参数选项比发行版中要少,譬如ls在发行版中可以有几十个-x,但是在busybox中只保留了几个常用的选项,不常用的都删除掉了);第二个是busybox中因为所有的命令的实现代码都在一个程序中实现,而各个命令中有很多代码函数都是通用的(譬如ls和cd、mkdir等命令都会需要去操作目录,因此在busybox中实现目录操作的函数就可以被这些命令共用),共用会降低重复代码出现的次数,从而减少总的代码量和体积。
(3)经过分析,busybox的体积优势是嵌入式系统本身的要求和特点造成的。

rcS文件

(1)/etc/init.d/rcS文件是linux的运行时配置文件中最重要的一个,其他的一些配置都是由这个文件引出来的。这个文件可以很复杂也可以很简单,里面可以有很多的配置项。

(2)典型的rcS文件实例

#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin

runlevel=S
prevlevel=N

umask 022

export PATH runlevel prevlevel

mount -a

echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

/bin/hostname -F /etc/sysconfig/HOSTNAME

ifconfig eth0 192.168.1.10

(3)PATH=xxx

  • 首先从shell脚本的语法角度分析,这一行定义了一个变量PATH,值等于后面的字符串
  • 后面用export导出了这个PATH,那么PATH就变成了一个环境变量。
  • PATH这个环境变量是linux系统内部定义的一个环境变量,含义是操作系统去执行程序时会默认到PATH指定的各个目录下去寻找。如果找不到就认定这个程序不存在,如果找到了就去执行它。将一个可执行程序的目录导出到PATH,可以让我们不带路径来执行这个程序。
  • rcS中为什么要先导出PATH?就是因为我们希望一旦进入命令行下时,PATH环境变量中就有默认的/bin /sbin /usr/bin /usr/sbin 这几个常见的可执行程序的路径,这样我们进入命令行后就可以ls、cd等直接使用了。
  • 为什么我们的rcS文件还没添加,系统启动就有了PATH中的值?原因在于busybox自己用代码硬编码为我们导出了一些环境变量,其中就有PATH。

(4)runlevel=

  • runlevel也是一个shell变量,并且被导出为环境变量。
  • runlevel=S表示将系统设置为单用户模式
  • 在当前版本的busybox中没有做runlevel相关的实现

(5)umask=

  • umask是linux的一个命令,作用是设置linux系统的umask值
  • umask值决定当前用户在创建文件时的默认权限
  • umask值和默认创建文件的权限值加起来是666.

(6)mount -a

  • mount命令是用来挂载文件系统的
  • mount -a是挂载所有的应该被挂载的文件系统,在busybox中mount -a时busybox会去查找一个文件/etc/fstab文件,这个文件按照一定的格式列出来所有应该被挂载的文件系统(包括了虚拟文件系统)
  • /etc/fstab实例
# /etc/fstab: static file system information.
#
# Use 'vol_id --uuid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# 	<file system> 	<mount point> 	<type> 	<options> 	<dump> 	<pass>
	proc 			/proc 			proc 	defaults 	0 		0
	sysfs 			/sys 			sysfs 	defaults 	0 		0
	tmpfs 			/var 			tmpfs 	defaults 	0 		0
	tmpfs 			/tmp 			tmpfs 	defaults 	0 		0
	tmpfs 			/dev 			tmpfs 	defaults 	0 		0
  • mount point表示挂载点,对应一个文件夹
  • type表示文件系统类型
  • 如果挂在时出现如下错误

mount: mounting proc on /proc failed: No such file or directory
mount: mounting sysfs on /sys failed: No such file or directory
mount: mounting tmpfs on /var failed: No such file or directory
mount: mounting tmpfs on /tmp failed: No such file or directory
mount: mounting tmpfs on /dev failed: No such file or directory

意思是挂载点所对应的文件夹不存在,需要我们自己创建。

  • 挂在成功后在板子的命令行下打开挂载点对应文件夹可以看到里面已经存在一些内容。注意如果是采用nfs方式挂载的跟文件系统在主机命令行下看不到挂载点对应文件夹里面的内容。

(7)mdev

  • mdev是udev的嵌入式简化版本,udev/mdev是用来配合linux驱动工作的一个应用层的软件,udev/mdev的工作就是配合linux驱动生成相应的/dev目录下的设备文件。
  • 在rcS文件中没有启动mdev的时候,/dev目录下启动后是空的;在rcS文件中添加上mdev有关的2行配置项后,再次启动系统后发现/dev目录下生成了很多的设备驱动文件。
  • /dev目录下的设备驱动文件就是mdev生成的,这就是mdev的效果和意义。

(8)hostname

  • hostname是linux中的一个shell命令。命令(hostname xxx)执行后可以用来设置当前系统的主机名为xxx,直接hostname不加参数可以显示当前系统的主机名。
  • /bin/hostname -F /etc/sysconfig/HOSTNAME。通过-F来指定了一个主机名配置文件(这个文件一般文件名叫hostname或者HOSTNAME),HOSTNAME文件中直接定义主机名即可。

(9)ifconfig

  • 有时候我们希望开机后进入命令行时ip地址就是一个指定的ip地址(譬如192.168.1.30),这时候就可以在rcS文件中ifconfig eth0 192.168.1.30

profile文件

  • 之前在rcS文件中添加了/bin/hostname -F /etc/sysconfig/HOSTNAME,并在/etc/sysconfig/HOSTNAME文件中定义了一个hostname(aston210),实际效果是:命令行下hostname命令查到的host名字确实是aston210。但是问题就是命令行的提示符是没有显示的。
  • 这个问题的解决就要靠profile文件。将提供的profile文件放入/etc/目录下即可,profile文件内容如下
# Ash profile
# vim: syntax=sh

# No core files by default
ulimit -S -c 0 > /dev/null 2>&1

USER="`id -un`"
LOGNAME=$USER
PS1='[\u@\h \W]\# '
PATH=$PATH

HOSTNAME=`/bin/hostname`

export USER LOGNAME PS1 PATH
  • 添加了之后的实验现象:命令行提示符前面显示:[@aston210 ]#。这里登录用户名没显示出来。原因就是我们直接进入了命令行而没有做登录。等我们添加了用户登录功能,并且成功登陆后这个问题就能解决。
  • profile文件工作原理是:profile文件也是被busybox(init进程)自动调用的,所以是认名字的。

显示用户登录界面

  • 我们之前intttab中有一个配置项 ::askfirst:-/bin/sh,这个配置项作用就是当系统启动后就去执行/bin/sh,执行这个就会出现命令行。因此我们这样的安排就会直接进入命令行而不会出现登录界面。
  • 要出现登录界面,就不能直接执行/bin/sh,而应该执行一个负责出现登录界面并且负责管理用户名和密码的一个程序,busybox中也集成了这个程序(就是/bin/login和/sbin/gettty),因此我们要在inittab中用/bin/login或者/sbin/getty去替代/bin/sh。
  • 用户名和密码的设置是和登录程序有关联的,但是/bin/login和/sbin/getty在用户名和密码的管理上是一样的。其实常见的所有的linux系统的用户名和密码的管理几乎都是一样的。
  • 密码一般都是用加密文字的,而不是用明文。意思就是系统中的密码肯定是在系统中的一个专门用来存密码的文件中存储的,用明文存密码有风险,因此linux系统都是用密文来存储密码的。

添加passwd和shadow文件

  • linux系统中用来描述用户名和密码的文件是passwd和shadow文件,这两个文件都在etc目录下。passwd文件中存储的是用户的密码设置,shadow文件中存储的是加密后的密码。
  • ubuntu的/etc/passwd文件
root:x:0:0:root:/root:/bin/bash
......

:是分隔符。第一个root表示用户名;x表示密码是密文并保存在/etc/shadow中;/root表示该用户的主目录为/root;/bin/bash表示默认的Shell

  • ubuntu的/etc/shadow文件
root:$6$ZzzBs8PF$/6n8CIwDfQO6oZJpJ6JafB48.qc12EXSFBS1hv1b6r3rZtxIZNf24Eup7G02dG5thTq9c0McjJaiDeZOvw5Jw1:17836:0:99999:7:::
......

 其中第一个:和第二个:之间的就是加密后的密码

  • 我们直接复制ubuntu系统中的/etc/passwd和/etc/shadow文件到当前制作的rootfs目录下并修改成如下的内容
root:x:0:0:root:/root:/bin/sh

这里有2点需要注意:

1、保证跟文件系统中存在/root目录

2、busybox不支持/bin/bash所以要修改成/bin/sh 

root:$6$ZzzBs8PF$/6n8CIwDfQO6oZJpJ6JafB48.qc12EXSFBS1hv1b6r3rZtxIZNf24Eup7G02dG5thTq9c0McjJaiDeZOvw5Jw1:17836:0:99999:7:::

ubuntu的shadow中默认有一个root加密的密码口令,所以移植过去之后的密码和Ubuntu中的密码一样

重置密码

  • ubuntu刚装好的时候默认登录是用普通用户登录的,默认root用户是关闭的。普通用户的密码是在装系统的时候设置的,普通用户登陆后可以使用su passwd root给root用户设置密码,设置了密码后root用户才可以登录
  • 其实这个原因就是root用户在/etc/shadow文件中加密口令是空白的。所以是不能登录的。
  • busybox中因为没有普通用户,所以做法是:默认root用户如果加密口令是空的则默认无密码直接登录。等我们登陆了之后还是可以用passwd root给root用户设置密码。

-sh: can`t access tty; job control turned off问题

  • 出现这个错误的原因是因为登录时没有指定tty即串口
  • 在/dev目录下有s3c2410_serial0、s3c2410_serial1、s3c2410_serial2、s3c2410_serial3
  • 我的开发板使用的是串口2,所以将s3c2410_serial2添加到/etc/inittab中的/bin/login的id处即,s3c2410_serial2::sysinit:/bin/login

getty

  • 做项目会发现,inittab中最常见的用于登录的程序不是/bin/login,反而是/sbin/getty
  • 这两个的差别不详,但是在busybox中这两个是一样的。这两个其实都是busybox的符号链接而已。因此不用严格区分这两个
  • 我们可以在inittab中用getty替换login程序来实现同样的效果,即将/etc/inittab中的/bin/login那一行修改为:s3c2410_serial2::respawn:/sbin/getty -L s3c2410_serial2 115200 vt100

动态链接库的拷贝

  • C程序如果使用gcc来编译则可以在主机ubuntu中运行,但是不能在开发板运行;要在开发板运行需要用arm-linux-gcc来交叉编译,但是这时候就不能在主机ubuntu中运行了。我们可以用file xx命令来查看一个elf可执行程序是哪个架构的。
  • 静态链接:arm-linux-gcc hello.c -o hello_satic -static
  • 动态链接:arm-linux-gcc hello.c -o hello_dynamic
  • 在拷贝动态链接库之前,静态编译连接后生成的hello_satic是可以正确执行的;而动态链接后生成的hello_dynamic是不能执行的,会提示错误:-sh: ./hello_dynamic: not found
  • 动态连接的hello程序中调用到了printf函数,而printf函数在动态连接时要在运行时环境(开发板的rootfs)中去寻找对应的库文件(开发板rootfs中部署的动态链接库中包含了printf函数的那个库文件)。如果找到了则printf函数就会被成功解析,然后hello_dynamic程序就会被执行;如果找不到则程序就不能被执行,命令行会提示错误信息-sh: ./hello_dynamic: not found
  • 解决方案:将arm-linux-gcc的动态链接库文件复制到开发板rootfs的/lib目录下即可解决
  • 我们用的arm-2009q3这个交叉编译工具链的动态链接库在/usr/local/arm/arm-2009q3/arm-none-linux-gnueabi/libc/lib目录下。其他的一些交叉编译工具链中动态链接库的目录不一定在这里,要去找一下。找的方法就是find命令
  • 复制动态链接库到roots/lib目录下。复制时要注意参数用-rdf,主要目的就是符号链接复制过来还是符号链接。
    复制命令:cp lib/*so* /root/porting_x210/rootfs/rootfs/lib/ -rdf
  • 使用strip工具去掉库中符号信息。动态链接库so文件中包含了调试符号信息,这些符号信息在运行时是没用的(调试时用的),这些符号会占用一定空间。在传统的嵌入式系统中flash空间是有限的,为了节省空间常常把这些符号信息去掉。这样节省空间并且不影响运行。
  • 去掉符号命令:arm-linux-strip *so*。实际操作后发现库文件由3.8M变成了3.0M,节省了0.8M的空间。

开机自启动

(1)开机自启动指的是让一些应用程序能够开机后自动执行
(2)开机自启动的实现原理就是在开机会自动执行的脚本rcS中添加上执行某个程序的语句代码即可

前台运行与后台运行

  • 程序运行时占用了当前的控制台,因此这个程序不结束我们都无法使用控制台,这就叫前台运行。默认执行程序就是前台运行的
  • 后台运行就是让这个程序运行,并且同时让出控制台。这时候运行的程序还能照常运行而且还能够不影响当前控制台的使用
  • 让一个程序后台运行的方法就是 ./xxx &

开机装载驱动等其他开机自动执行

实际开发中rootfs的rcS

  • 我们以X210开发板九鼎科技做的rootfs中rcS部分来分析
  • 分析inittab发现:sysinit执行rcS,shutdown时执行rcK
  • 分析/etc/init.d/rcS和rcK文件发现,rcS和rcK都是去遍历执行/etc/init.d/目录下的S开头的脚本文件,区别是rcS传参是start,rcK传参是stop
  • 由此可以分析出来,正式产品中的rcS和rcK都是一个引入,而不是真正干活的。真正干活的配置脚本是/etc/init.d/S??*。这些文件中肯定有一个判断参数是start还是stop,然后start时去做一些初始化,stop时做一些清理工作

制作ext2格式的镜像并烧录启动

  • 确定文件夹格式的rootfs可用

(1)设置bootargs为nfs启动方式,然后从主机ubuntu中做好的文件夹格式的rootfs去启动,然后看启动效果,作为将来的参照物。

  • 动手制作ext2格式的镜像

(1)
dd if=/dev/zero of=rootfs.ext2 bs=1024 count=10240
losetup  /dev/loop1 rootfs.ext2
mke2fs -m 0 /dev/loop1 10240
mount -t ext2 /dev/loop1 ./ext2_rootfs/
(2)向./rootfs中复制内容,用cp ../rootfs/* ./ -rf
(3)umount /dev/loop1
losetup -d /dev/loop1
(4)完成后得到的rootfs.ext2就是我们做好的rootfs镜像。拿去烧录即可。

  • 烧录镜像并设置合适的bootargs

(1)使用fastboot烧录制作好的rootfs.ext2到开发板inand中
fastboot flash system rootfs.ext2

(2)设置bootargs为:set bootargs console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext2
(3)启动后发现现象和之前nfs方式启动挂载rootfs后一样的,至此rootfs制作实验圆满完成。

 buildroot的引入和介绍

X210的bsp

大部分的ARM架构的linux平台的bsp的内容和结构都是相似的

bsp一般是芯片厂家/板卡厂家提供的。我所使用的x210的bsp由九鼎科技提供。其中linux对应的bsp在ubuntu下解压后如下图

  • tslib_x210_qtopia.tgz是用来支持QT的触摸屏操作的应用层库
  • xboot和uboot是X210支持的2个bootloader源代码
  • kernel文件夹中是内核源代码
  • buildroot文件夹是用来构建根文件系统的文件夹
  • tools里是一些有用工具
  • mk脚本是用来管理和编译整个bsp的

mk的帮助信息

通过执行./mk -h或./mk --help可以显示出mk的帮助信息

mk脚本的主要作用是编译bsp中的所有的源代码(包括bootloader、kernel、rootfs等),但是我们可以完整编译也可以部分编译,我们通过执行mk 后面加不同的参数来指定mk脚本去编译相应的部分。
譬如:
mk -a    即可编译所有的bsp源代码
mk -x    即可只编译xboot
mk -ui    即可只编译uboot针对inand版本开发板的源代码
mk -r      即可只编译buildroot,-r只是得到了文件夹形式的rootfs,并没有将其制作成镜像文件。
mk -re    即可编译buildroot并且制作得到ext3格式的rootfs镜像
mk -rj    即可编译buildroot并且制作得到jffs2格式的rootfs镜像

mk文件分析

x210:九鼎官方linux+QT版bsp的mk脚本分析

buildroot

(1)之前自己从零开始构建根文件系统,一路下来事情还挺多,步骤比较麻烦。
(2)交叉编译工具链arm-linux-gcc,我们目前都是从soc官方直接拿来使用的,官方的工具链从何而来?实际上交叉编译工具链都是由gcc配置编译生成的,这个配置编译过程比较复杂,一般人自己去配置编译得到自己的交叉编译工具链是比较麻烦的,所以经常都是用别人最好的。
(3)buildroot就是一个集成包,这个包里集成了交叉编译工具链的制作,以及整个rootfs的配置编译过程。也就是说,使用buildroot可以很简便的得到一个做好的文件夹形式的根文件系统。
(4)buildroot将很多东西集成进来后,移植了linux kernel的make xxx_defconfig+make menuconfig的2步配置法,我们可以在buildroot的配置界面下完成集成在里边的所有东西的配置,然后直接make就可以最终得到文件夹形式的rootfs。

make xxx_defconfig
make menuconfig
make

make及其错误解决
(1)直接make会遇到很多错误,这些错误原因都是因为ubuntu中缺乏一些必要软件包造成的。解决方案是先安装这些必要的软件包。
(2)编译过程会需要从网上下载一些软件包,因此整个编译过程需要在联网状态下进行。

编译buildroot的错误解决方案(环境为ubuntu14.04)

(1)配置
make x210ii_defconfig


(2)安装需要的软件包
sudo apt-get install g++ bison flex texinfo git hgsubversion whois

(3)编译
make


-----------------------------------------------------------------
以下是我直接编译时遇到的错误及其解决过程,希望对大家有所帮助。

错误1、You may have to install 'g++' on your build machine


You must install 'bison' on your build machine


make: *** [core-dependencies] Error 1

解决:
安装g++:			sudo apt-get install g++		
安装bison:			sudo apt-get install bison

(3)再次make
错误2、You must install 'flex' on your build machine

解决:
安装bison:			sudo apt-get install flex

(4)再次make
错误3、You must install 'makeinfo' on your build machine
makeinfo is usually part of the texinfo package in your distribution

解决:
安装makeinfo:		sudo apt-get install makeinfo

提示:Reading package lists... Done
Building dependency tree       
Reading state information... Done
E: Unable to locate package makeinfo

解决:看错误3的提示信息中,有提示makeinfo是texinfo的一部分,因此尝试安装texinfo替代:	sudo apt-get install texinfo

(5)再次make
错误4、You must install 'git' on your build machine

解决:
安装git:			sudo apt-get install git

(6)再次make
错误5、You must install 'hg' on your build machine

解决:
安装git:			sudo apt-get install hg

提示:Reading package lists... Done
Building dependency tree       
Reading state information... Done
E: Unable to locate package hg

解决:
首先使用:apt-cache search hg查找和hg有关的安装包,在查找到的列表中发现有hgsubversion,于是安装这个替代,命令为:sudo apt-get install hgsubversion

(7)再次make
错误6、You need the 'mkpasswd' utility to set the root password

(in Debian/ubuntu, 'mkpasswd' provided by the whois package)

解决:
根据提示信息,安装whois:	sudo apt-get install whois

编译后结果查看与分析
(1)编译后生成的文件夹格式的rootfs在buildroot/output/images/rootfs.tar。我们将其复制到了根目录下的release目录下去,这个文件就是一个完整的可以工作的文件夹形式的rootfs。

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

毛裤先生_2

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值