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

news2024/12/26 22:41:53

一、start_armboot 函数简介

在这里插入图片描述

1、一个很长的函数

(1) 这个函数在 uboot/lib_arm/board.c 的第 444 行开始到 908 行结束。

(2) 450 行还不是全部,因为里面还调用了别的函数。

(3)为什么这么长的函数,怎么不分成两三个函数?主要因为这个函数整个构成了 uboot 启动的第二阶段


2、一个函数组成 uboot 第二阶段


3、宏观分析:uboot 第二阶段应该做什么

(1) 概括来讲,uboot 第一阶段主要就是初始化了 SoC 内部的一些部件(譬如看门狗、时钟),然后初始化 DDR 并且完成重定位

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


4、思考:uboot 第二阶段完结于何处?

(1) uboot 启动后自动运行打印出很多信息(这些信息就是 uboot 在第一和第二阶段不断进行初始化时,打印出来的信息)。然后 uboot 进入了倒数 bootdelay 秒,然后执行 bootcmd 对应的启动命令。

(2) 如果用户没有干涉,则会执行 bootcmd 进入自动启动内核流程(uboot 就死掉了);此时用户可以按下回车键打断 uboot 的自动启动,进入到 uboot 的命令行下。然后 uboot 就一直工作在命令行下。

(3) uboot 的命令行就是一个死循环,循环体内不断重复:接收命令、解析命令、执行命令。这就是 uboot 最终的归宿。

在这里插入图片描述


二、start_armboot 解析1

1、init_fnc_t

在这里插入图片描述
在这里插入图片描述

(1) typedef int (init_fnc_t) (void); 这是一个函数类型。

(2) init_fnc_ptr 是一个二重函数指针,回顾高级 C 语言中讲过:二重指针的作用有 2 个(其中一个是用来指向一重指针),一个是用来指向指针数组。因此这里的 init_fuc_ptr可以用来指向一个函数指针数组。


2、DECLARE_GLOBAL_DATA_PTR

在这里插入图片描述

(1) #define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r8")
定义了一个全局变量名字叫 gd,这个全局变量是一个指针类型,占 4 字节。用 volatile 修饰表示可变的,用 register 修饰表示这个变量要尽量放到寄存器中,后面的 asm(“r8”)是 gcc 支持的一种语法,意思就是要把 gd 放到寄存器 r8 中。

(2) 综合分析,DECLARE_GLOBAL_DATA_PTR 就是定义了一个要放在寄存器 r8 中的全局变量,名字叫 gd,类型是一个指向 gd_t 类型变量的指针。

(3) 为什么要定义为 register?因为这个全局变量 gd(global data 的简称)是 uboot 中很重要的一个全局变量(准确的说这个全局变量是一个结构体,里面有很多内容,这些内容加起来构成的结构体就是 uboot 中常用的所有的全局变量),这个 gd 在程序中经常被访问,因此放在 register 中提升效率。因此纯粹是运行效率方面考虑,和功能要求无关。并不是必须的。


在这里插入图片描述

(4) gd_t 定义在 include/asm-arm/global_data.h 中。
gd_t 中定义了很多全局变量,都是整个 uboot 使用的;其中有一个 bd_t 类型的指针,指向一个 bd_t 类型的变量,这个 bd 是开发板的板级信息的结构体,里面有不少硬件相关的参数,譬如波特率、IP 地址、机器码、DDR 内存分布。

在这里插入图片描述


三、内存使用排布

在这里插入图片描述
在这里插入图片描述


1、为什么要分配内存

(1) DECLARE_GLOBAL_DATA_PTR 只能定义了一个指针,也就是说 gd 里的这些全局变量并没有被分配内存,我们在使用 gd 之前要给他分配内存,否则 gd 也只是一个野指针而已。

(2) gd 和 bd 需要内存,内存当前没有被人管理(因为没有操作系统统一管理内存),大片的 DDR 内存散放着可以随意使用(只要使用内存地址直接去访问内存即可)。但是因为 uboot 中后续很多操作还需要大片的连着内存块,因此这里使用内存要本着够用就好,紧凑排布的原则。所以我们在 uboot 中需要有一个整体规划。


2、内存排布

在这里插入图片描述

(1) uboot 区: CFG_UBOOT_BASE ~ xx(长度为 uboot 的实际长度);
(2) 堆区: 长度为 CFG_MALLOC_LEN,实际为 912 KB;
(3) 栈区: 长度为 CFG_STACK_SIZE,实际为 512 KB;
(4) gd: 长度为 sizeof(gd_t),实际 36 字节;
(5) bd: 长度为 sizeof(bd_t),实际为 44 字节左右;
(6) 内存间隔: 为了防止高版本的 gcc 的优化造成错误。

在这里插入图片描述


四、start_armboot 解析2

1、for 循环执行 init_sequence

在这里插入图片描述

在这里插入图片描述

(1) init_sequence 是一个函数指针数组,数组中存储了很多个函数指针,这些指向的函数都是 init_fnc_t 类型(特征是接收参数是 void 类型,返回值是 int)。

(2) init_sequence 在定义时就同时给了初始化,初始化的函数指针都是一些函数名。(C 语言高级专题中讲过:函数名的实质)

(3) init_fnc_ptr 是一个二重函数指针,可以指向 init_sequence 这个函数指针数组。

(4) 用 for 循环肯定是想要去遍历这个函数指针数组(遍历的目的也是去依次执行这个函数指针数组中的所有函数)。思考:如何遍历一个函数指针数组?有 2 种方法:第一种也是最常用的一种,用下标去遍历,用数组元素个数来截至。第二种不常用,但是也可以。就是在数组的有效元素末尾放一个标志,依次遍历到标准处即可截至(有点类似字符串的思路)。

我们这里使用了第二种思路。因为数组中存的全是函数指针,因此我们选用了 NULL 来作为标志。我们遍历时从开头依次进行,直到看到 NULL 标志截至。这种方法的优势是不用事先统计数组有多少个元素。

(5) init_fnc_t 的这些函数的返回值定义方式一样的,都是:函数执行正确时返回 0,不正确时返回 -1。所以我们在遍历时去检查函数返回值,如果遍历中有一个函数返回值不等于0,则 hang() 挂起。从分析 hang 函数可知:uboot 启动过程中初始化板级硬件时不能出任何错误,只要有一个错误整个启动就终止,除了重启开发板没有任何办法。

在这里插入图片描述

(6) init_sequence 中的这些函数,都是 board 级别的各种硬件初始化


2、cpu_init

在这里插入图片描述

(1) 看名字这个函数应该是 cpu 内部的初始化,所以这里是空的。


3、board_init

在这里插入图片描述

(1) board_init 在 uboot/board/samsung/x210/x210.c 中,这个看名字就知道是 x210 开发板相关的初始化。

(2) DECLARE_GLOBAL_DATA_PTR 在这里声明是为了后面使用 gd 方便。可以看出,把 gd 的声明定义成一个宏的原因就是,我们要到处去使用 gd,因此就要到处声明,定义成宏比较方便。


在这里插入图片描述

(3) 网卡初始化。CONFIG_DRIVER_DM9000 这个宏是 x210_sd.h 中定义的,这个宏用来配置开发板的网卡的。dm9000_pre_init 函数就是对应的 DM9000 网卡的初始化函数。开发板移植 uboot 时,如果要移植网卡,主要的工作就在这里。


在这里插入图片描述

(4) 这个函数中主要是网卡的 GPIO 和端口的配置,而不是驱动。因为网卡的驱动都是现成的正确的,移植的时候驱动是不需要改动的,关键是这里的基本初始化。因为这些基本初始化是硬件相关的。


五、start_armboot 解析3

在这里插入图片描述


1、gd->bd->bi_arch_number

(1) bi_arch_number 是 board_info 中的一个元素,含义是:开发板的机器码。所谓机器码就是 uboot 给这个开发板定义的一个唯一编号。

(2) 机器码的主要作用就是,在 uboot 和 linux 内核之间进行比对和适配。

(3) 嵌入式设备中,每一个设备的硬件都是定制化的,不能通用。嵌入式设备的高度定制化,导致硬件和软件不能随便适配使用。这就告诉我们:这个开发板移植的内核镜像绝对不能下载到另一个开发板去,否则也不能启动,就算启动也不能正常工作,有很多隐患。因此 linux 做了个设置:给每个开发板做个唯一编号(机器码),然后在 uboot、linux 内核中都有一个软件维护的机器码编号。然后开发板、uboot、linux 三者去比对机器码,如果机器码对上了就启动,否则就不启动(因为软件认为我和这个硬件不适配)。

(4) MACH_TYPE 在 x210_sd.h 中定义,值是 2456,并没有特殊含义,只是当前开发板对应的编号。这个编号就代表了 x210 这个开发板的机器码,将来这个开发板上面移植的 linux 内核中的机器码也必须是 2456,否则就启动不起来。

(5) uboot 中配置的这个机器码,会作为 uboot 给 linux 内核的传参的一部分,传给 linux 内核,内核启动过程中会比对这个接收到的机器码,和自己本身的机器码相对比,如果相等就启动,如果不相等就不启动。

(6) 理论上来说,一个开发板的机器码不能自己随便定。理论来说有权利去发放这个机器码的只有 uboot 官方,所以我们做好一个开发板并且移植了 uboot 之后,理论上应该提交给 uboot 官方审核并发放机器码(好像是免费的)。但是国内的开发板基本都没有申请(主要是因为国内开发者英文都不行,和国外开源社区接触比较少),都是自己随便编号的。随便编号的问题就是有可能和别人的编号冲突,但是只要保证 uboot 和 kernel 中的编号是一致的,就不影响自己的开发板启动。


2、gd->bd->bi_boot_params

(1) bd_info 中另一个主要元素,bi_boot_params 表示 uboot 给 linux kernel 启动时的传参的内存地址。也就是说,uboot 给 linux 内核传参的时候是这么传的:uboot 事先将准备好的传参(字符串,就是 bootargs)放在内存的一个地址处(就是 bi_boot_params ),然后 uboot 就启动了内核(uboot 在启动内核时,真正是通过寄存器 r0 r1 r2 来直接传递参数的,其中有一个寄存器中就是 bi_boot_params)。内核启动后从寄存器中读取bi_boot_params 就知道了 uboot 给我传递的参数到底在内存的哪里。然后自己去内存的那个地方去找 bootargs。

(2) 经过计算得知:X210 中 bi_boot_params 的值为 0x3000_0100,这个内存地址就被分配用来做内核传参了。所以在 uboot 的其他地方使用内存时要注意,千万不敢把这里给淹没了


六、背景:关于 DDR 的配置

(1) 注意:这里的初始化 DDR 和汇编阶段 lowlevel_init 中初始化 DDR 是不同的。当时 lowlevel_init 是硬件的初始化,目的是让 DDR 可以开始工作。现在是软件结构中一些 DDR 相关的属性配置、地址设置的初始化,是纯软件层面的

(2) 软件层次初始化 DDR 的原因:对于 uboot 来说,他怎么知道开发板上到底有几片 DDR 内存,每一片的起始地址、长度这些信息呢?

在 uboot 的设计中,采用了一种简单直接有效的方式:程序员在移植 uboot 到一个开发板时,程序员自己在 x210_sd.h 中使用宏定义去配置出来板子上 DDR 内存的信息,然后 uboot 只要读取这些信息即可。(实际上还有另外一条思路:就是 uboot 通过代码读取硬件信息来知道 DDR 配置,但是 uboot 没有这样。实际上 PC 的 BIOS 采用的是这种)

(3) x210_sd.h 的 496 行到 501 行中使用了标准的宏定义来配置 DDR 相关的参数。主要配置了这么几个信息:有几片 DDR 内存、每一片 DDR 的起始地址、长度。这里的配置信息我们在 uboot 代码中使用到内存时就可以从这里提取使用(想象 uboot 中使用到内存的地方都不是直接用地址数字的,都是用宏定义的)

在这里插入图片描述


七、start_armboot 解析4

1、interrupt_init

在这里插入图片描述

(1) 看名字,函数是和中断初始化有关的,但是实际上不是,实际上这个函数是用来初始化定时器的(实际使用的是Timer4)。

(2) 裸机中讲过:210 共有 5 个 PWM 定时器。其中 Timer0 - Timer3 都有一个对应的 PWM 信号输出的引脚。而 Timer4 没有引脚,无法输出 PWM 波形。Timer4 在设计的时候就不是用来输出 PWM 波形的(没有引脚,没有 TCMPB 寄存器),这个定时器被设计用来做计时。

(3) Timer4 用来做计时时,要使用到 2 个寄存器:TCNTB4、TCNTO4。TCNTB 中存了一个数,这个数就是定时次数(每一次时间是由时钟决定的,其实就是由 2 级时钟分频器决定的)。我们定时时,只需要把定时时间 / 基准时间 = 数,将这个数放入 TCNTB 中即可;我们通过 TCNTO 寄存器即可读取时间有没有减到 0,读取到 0 后就知道定的时间已经到了。

(4) 使用 Timer4 来定时,因为没有中断支持,所以 CPU 不能做其他事情同时定时,CPU 只能使用轮询方式来不断查看 TCNTO 寄存器才能知道定时时间到了没。因为 Timer4 的定时是不能实现微观上的并行。uboot 中定时就是通过 Timer4 来实现定时的。所以 uboot 中定时时,不能做其他事(考虑下,典型的就是 bootdelay,bootdelay 中实现定时并且检查用户输入是用轮询方式实现的,原理参考裸机中按键章节中的轮询方式处理按键)

(5) interrupt_init 函数将 timer4 设置为定时 10ms。关键部位就是 get_PCLK 函数获取系统设置的 PCLK_PSYS 时钟频率,然后设置 TCFG0 和 TCFG1 进行分频,然后计算出设置为 10ms 时需要向 TCNTB 中写入的值,将其写入 TCNTB,然后设置为 auto reload 模式,然后开定时器开始计时就没了。

总结:在学习这个函数时,注意标准代码和之前裸机代码中的区别,重点学会:通过定义结构体的方式来访问寄存器,通过函数来自动计算设置值以设置定时器。


2、env_init

在这里插入图片描述

(1) env_init,看名字就知道是和环境变量有关的初始化。

(2) 为什么有很多 env_init 函数,主要原因是 uboot 支持各种不同的启动介质(譬如 norflash、nandflash、inand、sd卡·····),我们一般从哪里启动就会把环境变量 env 放到哪里。而各种介质存取操作 env 的方法都是不一样的。因此 uboot 支持了各种不同介质中 env 的操作方法。所以有好多个 env_xx 开头的 c 文件。

实际使用的是哪一个要根据自己开发板使用的存储介质来定(这些 env_xx.c 同时只有1个会起作用,其他是不能进去的,通过 x210_sd.h 中配置的宏来决定谁被包含的),对于 x210 来说,我们应该看 env_movi.c 中的函数。

(3) 经过基本分析,这个函数只是对内存里维护的那一份 uboot 的 env 做了基本的初始化,或者说是判定(判定里面有没有能用的环境变量)。当前因为我们还没进行环境变量从 SD 卡到 DDR 中的 relocate,因此当前环境变量是不能用的。


在这里插入图片描述

(4) 在 start_armboot 函数中(776行)调用 env_relocate 才进行环境变量从 SD 卡中到 DDR 中的重定位。重定位之后需要环境变量时,才可以从 DDR 中去取,重定位之前如果要使用环境变量只能从 SD 卡中去读取


八、start_armboot 解析5

1、init_baudrate

在这里插入图片描述

(1) init_baudrate 看名字就是初始化串口通信的波特率的。


在这里插入图片描述

(2) getenv_r 函数用来读取环境变量的值。用 getenv 函数读取环境变量中 “baudrate” 的值(注意读取到的不是 int 型,而是字符串类型),然后用 simple_strtoul 函数将字符串转成数字格式的波特率。

(3) baudrate 初始化时的规则是:先去环境变量中读取 “baudrate” 这个环境变量的值。如果读取成功,则使用这个值作为环境变量,记录在 gd->baudrate 和 gd->bd->bi_baudrate 中;如果读取不成功,则使用 x210_sd.h 中的 CONFIG_BAUDRATE 的值作为波特率。从这可以看出:环境变量的优先级是很高的。


2、serial_init

在这里插入图片描述

(1) serial_init 看名字是初始化串口的。(疑问:start.S 中调用的 lowlevel_init.S 中已经使用汇编初始化过串口了,这里怎么又初始化?这两个初始化是重复的还是各自有不同?)


在这里插入图片描述

(2) 搜索发现, uboot 中有很多个 serial_init 函数,我们使用的是 uboot/cpu/s5pc11x/serial.c 中的 serial_init 函数。

(3) 进来后发现,serial_init 函数其实什么都没做。因为在汇编阶段串口已经被初始化过了,因此这里就不再进行硬件寄存器的初始化了。


源自朱有鹏老师.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/344053.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

100种思维模型之非sr思维模型-012

什么是sr? sr是stimulus-response的缩写,意思是刺激反应。 那么非sr思维模型就是非刺激反应思维模型的意思。 今天我们来聊聊非sr思维模型——一个提醒我们思考,提醒我们任何时刻都有选择权的思维模型。 本文依然从三个方面进行介绍,何谓…

你是真的“C”——详解结构体知识点

你是真的“C”——详解结构体知识点😎前言🙌什么是结构体?🙌1. 结构体的声明🙌1.1 结构的基础知识1.2 结构的声明1.3 结构成员的类型1.4 结构体变量的定义和初始化2. 结构体成员的访问🙌3结构体传参&#x…

推荐领域新人必看书籍:《推荐系统实践》

这本书非常适合推荐领域的新手,因为这本书的主要目的更接近于科普,而不是描述具体的推荐算法。什么是推荐系统?如果有一位你喜欢的女士约你一起外出,肯定不需要别人推荐你是否赴约吧!(信息量太小则不需要被…

VS Code中的GIT操作

一、前言 我们在进行项目开发时都免不了与GIT打交道,但是面对各种的难记的GIT命令总是手足无措;还好编译器中内置了GIT的仓库的一系列操作,掌握了可视化的操作就不用担心记不住GIT命令符了。下面主要介绍VS Code中具体的操作: 二…

【安全】Nginx实现反向代理负载均衡

基础概念 什么是负载均衡? 负载均衡用于从“upstream”模块定义的后端服务器列表中选取一台服务器接受用户的请求;即把请求均匀的分摊给上游的应用服务器。最基本的配置方式便是轮询: 负载均衡策略 策略 轮询 根据请求顺序分配 weight …

【软件工程】COMP5241 SE课程笔记

Software EngineeringCourse1 IntroductionCloud Native AppsScheduleSoftware InstallProject Chaos ReportWhat is Software EngineeringHow to define a good AppsSteps of SoftwareCourse4本笔记记录始于2023年2月13日,为在读研究生期间COMP5241 SE课程笔记整合…

检测脸部情绪有多难?10行代码就可以搞定!

引言面部表情展示人类内心的情感。它们帮助我们识别一个人是愤怒、悲伤、快乐还是正常。医学研究人员也使用面部情绪来检测和了解一个人的心理健康。人工智能在识别一个人的情绪方面可以发挥很大的作用。在卷积神经网络的帮助下,我们可以根据一个人的图像或实时视频…

封装、继承、多态、上下转型、静态绑定、动态绑定、PO/Bean/Vo/Do/Dto,dljd reyco郭

封装 “封装”这个概念,由两部分构成:一部分是封,一部分是装。“封装”这个动作,顺序应该是先装后封。 装:原本name、age、score是3个不同的、离散的数据,它们之间是有关系的是,都是用来描述一个…

东芝TLP5772光耦与SLM346兼容光耦的单通道隔离驱动器比较

东芝TLP5772光耦与SLM346兼容光耦的单通道隔离驱动器比较一般描述:SLM346是一款光兼容单通道,隔离栅驱动器,用于IGBT、MOSFET和2.5A源和2.5A汇峰值输出电流和5kVRMS加强隔离等级。SLM346可以驱动低侧和高侧功率场效应晶体管。可靠性升级超过标…

PDFPrinting.Net操作进行细粒度控制

PDFPrinting.Net操作进行细粒度控制 PDFPrinting.Net能够容易且灵活地预测完美的打印结果以及用户文件的示例性显示。可以快速浏览.NET PDF打印中最关键的元素。如果用户需要获得更详细的概述,那么他可以查看快速入门手册,甚至是现有文档的详细概述参考。…

如何发布一个 TypeScript 编写的 npm 包

本文正在参加「金石计划 . 瓜分6万现金大奖」 原文链接:www.strictmode.io/articles/bu… 作者:strictmode.io 前言 在这篇文章中,我们将使用TypeScript和Jest从头开始构建和发布一个NPM包。 我们将初始化一个项目,设置TypeS…

关于在Interceptor拦截器中使用autowired注入,但是却注入为null。引出在自动装配时,只有在ioc容器中的bean可以互相自动装配。

问题描述 在拦截器拦截登录请求,想通过从redis中取出token,判断token是否为null,进而判断是否登录。 Component public class LoginInterceptor implements HandlerInterceptor {AutowiredStringRedisTemplate redisTemplate;Overridepubli…

基于matlab的SAR图像中自动目标识别

一、前言此示例演示如何使用深度学习工具箱和并行计算工具箱™™训练基于区域的卷积神经网络 (R-CNN) 以识别大场景合成孔径雷达 (SAR) 图像中的目标。深度学习工具箱提供了一个框架,用于设计和实现具有算法、预训练模…

MyBatis_自定义映射resultMap

自定义映射resultMap 文章目录自定义映射resultMap创建数据表实体类字段名和属性名不一致(三种方式)取别名设置全局配置设置resultMap处理多对一的映射关系(三种方式)级联方式处理association分步查询处理一对多的映射关系(两种方式)collection分步查询创建数据表 复制进MySQL…

强化学习笔记-03有限马尔可夫决策过程MDP

本文是博主对《Reinforcement Learning- An introduction》的阅读笔记,不涉及内容的翻译,主要为个人的理解和思考。 前文我们了解了强化学习主要是为了实现最大化某个特定目标(收益),而学习如何通过环境进行交互。 而学…

PCI Express体系结构导读_3PCI总线的数据交换--读书笔记

前言本文为读书笔记,如有误可以指正,一块学习交流本章节主要介绍两种类型的数据传输:a- host读写pci设备的bar寄存器。b- pci设备通过DMA方式读写内存。对于PCI设备读写其他PCI设备的bar寄存器只了解3.1- pci设备bar空间的初始化3.1.1 内存域…

33复杂美:一文看懂加密算法为何物

加密算法 ,区块链底层技术的心脏究竟为何物?加密,简而言之,加密就是借助一种或多种算法将明文信息转换成密文信息,信息的接收方通过密钥对密文信息进行解密获得明文信息的过程。根据加解密的密钥是否相同,加…

C++:类和对象(下)

文章目录1 再谈构造函数1.1 构造函数体赋值1.2 初始化列表1.3 explicit关键字2 static成员2.1 概念2.2 特性3 友元3.1 友元函数&#xff08;流插入&#xff08;<<&#xff09;及流提取&#xff08;>>&#xff09;运算符重载&#xff09;3.2 友元类4 内部类5 匿名对…

使用脚本以可读的 JSON 格式显示 curl 命令输出

在我们经常调试微服务或者使用 Elasticsearch API 时&#xff0c;经常会使用curl 来进行调试。但是有时我们的输出不尽如意。显示的不是一 pretty 格式进行输出的。我们有时还必须借助于其他的一些网站工具&#xff0c;比如 Best JSON Formatter and JSON Validator: Online JS…

叮!一大波来自客户的感谢信

春风渐暖&#xff0c;美好如期&#xff0c;祝福的话语在日子的酝酿里更值得期待。神策数据走过 7 载春秋&#xff0c;描绘的大数据分析和营销科技图景在时间的打磨下清晰可见。时光沉淀经验&#xff0c;匠心兑换卓越&#xff0c;这条终点叫做「帮助中国三千万企业重构数据根基&…