1. 概述
1.1 缘由
最近在待业,由于之前的工作是在一个初创的芯片原厂做底层驱动开发,所以最近想着能不能以一个芯片原厂的角度移植最新的u-boot到jz2440开发板呢?于是便有了这篇文章。
整篇文章遵循着以问题为导向的讲解方式,重实践,基本没有原理性的知识,并且从零开始进行移植。我相信,如果你能认真的跟着文章实际操作一把,以后不管遇到什么类型的芯片、什么类型的开发板,同样都能完成从零移植u-boot的工作。
1.2 实现的功能
移植的u-boot 2020.07目前实现的功能如下:
- 如标题所述,全部驱动使用设备树与dm设备模型
- 实现的驱动:
- 串口驱动
- sdcard驱动
- usb udc驱动
- nand flash驱动
- gpio子系统驱动
- pinctrl子系统驱动
- clock子系统驱动
- 硬件无关的命令可以按需添加
- 硬件相关的命令:
-
一些基本的必须的命令,如nand,mmc等
-
ums命令(通过usb总线,支持在pc端查看sdcard中的文件系统,暂时由于sdcard驱动问题导致当分区文件内容比较大时会不稳定)
-
dfu命令(通过usb总线,支持在pc端下载文件到nand flash)
- 其他:
- 支持从nand flash启动u-boot以及内核
- 支持从sdcard启动u-boot以及内核,内核映像文件以及dtb文件放在sdcard的第一个vfat格式分区中,根文件系统放在第二个ext4格式分区中
1.3 后续文章写作思路
我打算把移植工作分为一系列的文章来进行讲解:
- 本篇文章,讲解到以最小的改动实现最初始的打印u-boot启动信息功能为止;
- 以驱动为分类标准,之后的每一个驱动单独用一篇文章来讲解。
另外,之后如果有时间会有相同思路的移植最新linux内核的系列文章,敬请期待!
2. spl闪亮登场
2.1 引入
在讨论u-boot之前,我们需要认识另一位兄弟——spl,这里我暂且称为spl,首先它并不是u-boot中的spl(second program loader,第二程序加载器,相对于soc中固化的bootcode代码而言),但是作用是相同的,那具体是什么呢?
我这里所谓的spl,主要是参考韦东山老师的自己写bootloader的课程中的内容,一个独立于u-boot的工程。
如果将u-boot直接放在nand flash的0地址处,我们知道如果设置开发板为nand flash启动,s3c2440上电后会自动从nand flash复制4KB代码到内部ram,然后运行。这样,u-boot的重定位代码可能会在4KB之后,那之后的u-boot的代码就不会被重定位到sdram,这样显然是不行的。
所以,spl便是我的一个解决方法:
- 首先初始化时钟、sdram以及外部存储设备;
- 然后将u-boot从外部存储设备(sdcard或者nand flash)直接复制到sdram中;
- 最后调整pc指针到u-boot所在的sdram地址处直接运行u-boot。
2.2 工程构成
整个spl工程构成如下图所示,
可以看到,基本和韦东山老师自己写bootloader的课程中的内容一致,唯一不同的一点是,之前由于学习sdcard裸机驱动,我在网上找了一个s3c2440的sdcard裸机驱动代码,现在正好,如果将u-boot.bin放到sdcard中相比于nandflash,对于调试来说那将是非常的方便,只要有一个读卡器,复制一个几百k的u-boot.bin不需要一秒就可以完成。
2.3 整体思路
整个spl的思路如下:
-
首先初始化栈、时钟、sdram、nand flash,
.text .global _start /*****************************************************************************/ @程序入口 _start: @ 0x00: 复位异常的向量地址 b Reset @ 0x04: 未定义指令中止模式的向量地址 HandleUndef: b HandleUndef @ 0x08: 管理模式的向量地址(在用户模式下,可通过SWI指令进入该模式) HandleSvc: b HandleSvc @ 0x0c: 指令预取中止异常的向量地址 HandlePrefetchAbort: b HandlePrefetchAbort @ 0x10: 数据访问中止异常的向量地址 HandleDataAbort: b HandleDataAbort @ 0x14: 保留 HandleNotUsed: b HandleNotUsed @ 0x18: 中断模式的向量地址 HandleIRQ: b HandleIRQ @ 0x1c: 快中断模式的向量地址 HandleFIQ: b HandleFIQ /*****************************************************************************/ @复位异常入口 Reset: ldr sp, =4096 @重启默认进入SVC模式,此处设置SVC模式栈指针 bl disable_watch_dog bl clock_init bl sdram_init bl nand_init @初始化 NAND Flash
-
由于编译后的spl.bin大概十几k,并且s3c2440上电后只会从nandflash复制前4KB代码到内部ram,所以我首先在spl的前4k代码中将spl自己从nandflash复制到sdram的起始位置0x30000000处,然后跳到sdram中接着执行spl;
@nand_read函数需要3个参数: ldr r1, =0x30000000 @1.目标地址=0x30000000,这是SDRAM的起始地址 mov r0, #0 @2.源地址 = 0 mov r2, #(1024*20) @3.复制长度 = 20KB bl nand_read @调用C函数nand_read ldr pc, =on_sdram @跳到SDRAM中继续执行 on_sdram: msr cpsr_c, #0xdf @进入系统模式 ldr sp, =0x34000000 @设置系统模式栈指针 @ ldr lr, =loop ldr lr, =0x33f00000 ldr pc, =main loop: b loop
-
在c代码main函数中,初始化串口用于调试,接着初始化sdcard,最后,从sdcard的指定位置处复制提前在pc上写好的u-boot.bin到u-boot.bin的链接地址0x33f00000处;
#include <uart.h> #include <sd.h> #define SD_BLOCK_SIZE (512) void delay(void) { /* 由于使用了-O2优化编译选项,volatile的作用在于 告诉编译器空循环不要优化,否则会被优化为空语句 */ volatile unsigned long i, j; for (i = 0; i < 300; ++i) for (j = 0; j < 300; ++j); } void main(void) { unsigned int *uboot_addr_on_sdram = (unsigned int *)0x33f00000; // 将u-boot读取到sdram的0x33f00000地址处 unsigned int uboot_addr_on_sdcard = 2 * 1024 / SD_BLOCK_SIZE; // u-boot存放在sdcard的2KB地址处 unsigned int block_num = (512 * 1024) / SD_BLOCK_SIZE; // u-boot存放在sdcard的大小为512KB // void (*theKernel)(void); uart0_init(); delay(); sd_init(); printk("\n\rBegin to copy uboot from sdcard(%dKB) to sdram(0x%x)...", uboot_addr_on_sdcard * 512 / 1024, uboot_addr_on_sdram); if (!sd_read_sector(uboot_addr_on_sdram, uboot_addr_on_sdcard, block_num)) { printk(" [done]\n\r"); } else { printk(" [failed]\n\r"); printk("Please check your sdcard!\n\r"); while (1); } printk("\n\rbooting uboot..."); // theKernel = (void (*)(void))uboot_addr_on_sdram; // theKernel(); }
-
main函数返回到汇编代码start.S中,提前在跳转main前设置为0x33f00000的lr寄存器将发挥作用,将0x33f00000赋值给pc,然后就开始u-boot.bin的天下了!
@ ldr lr, =loop ldr lr, =0x33f00000 ldr pc, =main loop: b loop
2.4 需要改进的地方
现在对于spl,我发现有两点需要改进:
- 整个spl能否小于4k?这样就不用再进行搬移了;
- sdcard驱动在我的4g的卡上运行没有任何问题,但是换成16g的卡就会初始化失败,现在还没去解决,当然后期调试好后也可以将u-boot.bin放到nandflash中;
3. 获取u-boot源码
好的,我们正式开始移植u-boot之旅!
正所谓巧妇难为无米之炊,u-boot的源码我是从其官方的github上克隆下来的,这样可以保证是最新的。具体我是这样做的:
-
将github上u-boot的工程fork到自己的github仓库:
u-boot的github网址:https://github.com/u-boot/u-boot
进入该网址,点击右上角的fork按钮,
这样,便将u-boot工程克隆到了自己的github仓库中,
2. 之后就是正常的git操作了:
点击自己github仓库中刚刚克隆下来的u-boot页面中的Clone or download这个绿色按钮复制git地址到系统剪贴板,然后在pc终端中执行如下命令:
根据网络的速度不同,需等待一段时间。完成后,便将u-boot的源码下载到了当前目录下,目录名称是u-boot。
4. 明确移植方向
源码已经有了,但是在动手之前,我们可以先大概思考一下:
我们知道,u-boot是一个裸机驱动集大成者,它能运行在各种不同架构的芯片为核心的各种不同的单板上面,那它是如何做到的呢?答案也很简单:
-
首先,很容易想到的是,它需要有针对各种不同芯片、单板的驱动程序文件;
-
然后,它还需要一个很好的中间框架层,以抽取各种不同架构芯片、单板的共性,向上提供相同的api给上层的各种命令,向下屏蔽这些芯片、单板不同的硬件特性,将这些硬件特性操作留给驱动程序。
很显然,这用到了分层思想,一旦A需要兼容各种不同的B、C、D等等时,分层思想总能让你达到目的。
接下来,我们看一下u-boot 2020.07的顶层目录,
各种目录的功能这里我就不做详细介绍了,这里我想重点说明的是:
-
针对上述的第一点,arch/、board/、drivers/ 主要描述了不同架构芯片、单板的特性;
-
针对第二点的共性,基本包括了剩下的所有除了编译相关的目录、文件。
所以,弄清楚了上述的问题之后,我们便很容易想到移植的工作大致方向在哪:没错,就是在arch/、board/、drivers/这三个目录!
4.1 arch/目录
- arch,描述了各种不同架构的cpu,比如我们熟悉的arm,还有诸如mips、powerpc等等,接下来我们以arm架构为例进行说明:
- arch/arm/cpu,描述了同一cpu架构下的不同代产品,比如s3c2440使用的arm920t,还有armv7、armv8等等:
- arch/arm,描述了同一cpu架构下的不同的soc厂家,下图中他们都使用了arm架构,但是会有许多不同的soc芯片产品,比如三星、nxp、瑞芯微、全志等等,接下来以瑞芯微为例进行说明:
- arch/arm/mach-rockchip,是对国产芯片公司瑞芯微旗下的soc芯片的描述,比如rk3128、rk3328等等:
4.2 board/目录
- board,描述了不同单板厂家的开发板产品,由于目录众多,下图只截取了部分:
- board/xxx,描述了使用同一soc芯片厂家产品的不同的开发板产品,比如都是使用了三星的s3c2440,除了三星的公版开发板smdk2440外,还有jz2440、mini2440、tq2440等等:
4.3 drivers/目录
该目录下便是各种soc芯片内部的各种控制器主机驱动,以及各种外设驱动的天下了。
这里顺便提一下,驱动的编写并不会关心你的cpu核是arm还是mips,是arm920t还是armv7,他们的差别只在于不同汇编启动代码以及编译器,我们都是使用c语言进行驱动程序的编写,驱动关心的是soc芯片内部的控制器寄存器以及各种不同外设的寄存器。
所以,驱动编程对应的硬件对象主要包括两点:
- soc内部的各种控制器,比如usb控制器、sd/mmc控制器、spi控制器、中断控制器、gpio控制器等等,在驱动程序框架中这部分一般被称为主机端驱动,基本是由soc厂家来完成驱动的编写工作,相对复杂些;
- 各种外设,比如led、按键、lcd、各种传感器等等,这部分一般由外设厂家或是具体的开发板厂家进行驱动的编写工作,相对简单些。