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,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44.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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@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处;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42#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的天下了!
1
2
3
4
5
6@ 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、各种传感器等等,这部分一般由外设厂家或是具体的开发板厂家进行驱动的编写工作,相对简单些。
5. 初步配置
5.1 引入
根据我们上述的分析,arm < arm920t < samsung < s3c2440 < smdk2440是依次后者包含前者的关系,也只有到了最后的开发板级别,我们才能在此基础上面做出具体的项目产品,想一想,我们只有一个arm核、一个soc芯片可以做什么?答案是什么也做不了,因为它不满足一个微型计算机系统构成所必需的各种部件。
首先,我们需要明确的是,我们移植的目标是开发板级别,所以我们第一步应该关心的就是board/samsung目录。但是该版本的u-boot已经不提供对smdk2440开发板的支持了,这点我们可以从board/samsung目录下得到验证,
所以正好,我们可以尝试着站在soc芯片原厂的角度来进行移植工作。
5.2 新建单板目录
我们在board/samsung目录下新建一个目录,命名为jz2440,
接下来,我们可以看一下同目录下的其他开发板目录都有什么文件,如下图
我们发现,除去特有的部分,它们基本都有Kconfig、Makefile、MAINTAINERS、目录同名的c文件,所以我们复制其中一个开发板目录的上述文件到我们刚刚新建的jz2440目录,这里我们以smdkc100为例进行复制,
接着,我们依次对上述四个文件进行介绍以及修改。
-
Kconfig文件
原文件内容如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if TARGET_SMDKC100
config SYS_BOARD
default "smdkc100"
config SYS_VENDOR
default "samsung"
config SYS_SOC
default "s5pc1xx"
config SYS_CONFIG_NAME
default "smdkc100"
endif
这是一个Kconfig文件,熟悉内核编译的应该都清楚,u-boot好像在2015版本就使用了与linux内核相同的编译系统。
- 开始的if/endif表明只有TARGET_SMDKC100配置为y,下述的几个config才有效,所以我们应该在某个地方将该宏设置为y,现在我们先不管这个问题,这里我们将其改为TARGET_JZ2440;
接下来的几个宏,default的意思是默认将对应的宏的值设为default后面的值:
-
SYS_BOARD 表明开发板的名称,这里我们改为jz2440
-
SYS_VENDOR 表明soc厂家的名称,这里还是使用samsung
-
SYS_SOC 表明具体的soc芯片名称,这里我们改为s3c2440
-
SYS_CONFIG_NAME 表明对应的配置文件的名称,这里我们改为jz2440,其对应一个.h文件,所以我们需要建立这么一个文件,具体内容下文会有介绍,该文件位置在include/configs目录,下图列出目录下部分.h文件
改好后如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if TARGET_JZ2440 config SYS_BOARD default "jz2440" config SYS_VENDOR default "samsung" config SYS_SOC default "s3c2440" config SYS_CONFIG_NAME default "jz2440" endif |
-
Makefile文件
原文件内容如下,
1
2
3
4
5
6
7
8
9
10
11# SPDX-License-Identifier: GPL-2.0+
#
# (C) Copyright 2000, 2001, 2002
# Wolfgang Denk, DENX Software Engineering, [email protected].
#
# (C) Copyright 2008
# Guennadi Liakhovetki, DENX Software Engineering, <[email protected]>
obj-y := smdkc100.o
obj-$(CONFIG_SAMSUNG_ONENAND) += onenand.o
obj-y += lowlevel_init.o这里我们暂时只保留一个.c文件,
1
2
3
4
5# SPDX-License-Identifier: GPL-2.0+
#
# (C) Copyright 2020 Asymptote
obj-y := jz2440.o -
MAINTAINERS文件
原文件内容如下,
1
2
3
4
5
6SMDKC100 BOARD
M: Minkyu Kang <[email protected]>
S: Maintained
F: board/samsung/smdkc100/
F: include/configs/smdkc100.h
F: configs/smdkc100_defconfig1这是一个描述文件,我们改为1
2
3
4
5
6JZ2440 BOARD
M: Asymptote
S: Maintained
F: board/samsung/jz2440/
F: include/configs/jz2440.h
F: configs/jz2440_defconfig -
目录同名的c文件
这个文件主要描述了我们jz2440单板的一些内容,我们将文件名改为jz2440.c,内容暂时为空。
5.3 新建.h配置文件
由4.2我们知道,我们还需要在include/configs目录新建一个jz2440.h的文件,在新建之前我们先看看对应的smdkc100.h的内容是什么,以作为参考,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2009 Samsung Electronics * Minkyu Kang <[email protected]> * HeungJun Kim <[email protected]> * Inki Dae <[email protected]> * * Configuation settings for the SAMSUNG SMDKC100 board. */ #ifndef __CONFIG_H #define __CONFIG_H /* * High Level Configuration Options * (easy to change) */ #define CONFIG_SAMSUNG 1 /* in a SAMSUNG core */ #define CONFIG_S5P 1 /* which is in a S5P Family */ #define CONFIG_S5PC100 1 /* which is in a S5PC100 */ #include <asm/arch/cpu.h> /* get chip and board defs */ /* input clock of PLL: SMDKC100 has 12MHz input clock */ #define CONFIG_SYS_CLK_FREQ 12000000 /* DRAM Base */ #define CONFIG_SYS_SDRAM_BASE 0x30000000 /* Text Base */ #define CONFIG_SETUP_MEMORY_TAGS #define CONFIG_CMDLINE_TAG #define CONFIG_INITRD_TAG /* * Size of malloc() pool * 1MB = 0x100000, 0x100000 = 1024 * 1024 */ #define CONFIG_SYS_MALLOC_LEN (CONFIG_ENV_SIZE + (1 << 20)) /* * select serial console configuration */ /* PWM */ #define CONFIG_PWM 1 /* allow to overwrite serial and ethaddr */ #define CONFIG_ENV_OVERWRITE #define CONFIG_BOOTCOMMAND "run ubifsboot" #define CONFIG_RAMDISK_BOOT "root=/dev/ram0 rw rootfstype=ext2" \ " console=ttySAC0,115200n8" \ " mem=128M" #define CONFIG_COMMON_BOOT "console=ttySAC0,115200n8" \ " mem=128M " \ " " CONFIG_MTDPARTS_DEFAULT #define CONFIG_UPDATEB "updateb=onenand erase 0x0 0x40000;" \ " onenand write 0x32008000 0x0 0x40000\0" #define CONFIG_ENV_OVERWRITE #define CONFIG_EXTRA_ENV_SETTINGS \ CONFIG_UPDATEB \ "updatek=" \ "onenand erase 0x60000 0x300000;" \ "onenand write 0x31008000 0x60000 0x300000\0" \ "updateu=" \ "onenand erase block 147-4095;" \ "onenand write 0x32000000 0x1260000 0x8C0000\0" \ "bootk=" \ "onenand read 0x30007FC0 0x60000 0x300000;" \ "bootm 0x30007FC0\0" \ "flashboot=" \ "set bootargs root=/dev/mtdblock${bootblock} " \ "rootfstype=${rootfstype} " \ "ubi.mtd=${ubiblock} ${opts} " CONFIG_COMMON_BOOT ";" \ "run bootk\0" \ "ubifsboot=" \ "set bootargs root=ubi0!rootfs rootfstype=ubifs " \ " ubi.mtd=${ubiblock} ${opts} " CONFIG_COMMON_BOOT "; " \ "run bootk\0" \ "boottrace=setenv opts initcall_debug; run bootcmd\0" \ "android=" \ "set bootargs root=ubi0!ramdisk ubi.mtd=${ubiblock} " \ "rootfstype=ubifs init=/init.sh " CONFIG_COMMON_BOOT "; " \ "run bootk\0" \ "nfsboot=" \ "set bootargs root=/dev/nfs ubi.mtd=${ubiblock} " \ "nfsroot=${nfsroot},nolock " \ "ip=${ipaddr}:${serverip}:${gatewayip}:" \ "${netmask}:nowplus:usb0:off " CONFIG_COMMON_BOOT "; " \ "run bootk\0" \ "ramboot=" \ "set bootargs " CONFIG_RAMDISK_BOOT \ " initrd=0x33000000,8M ramdisk=8192\0" \ "rootfstype=cramfs\0" \ "mtdparts=" CONFIG_MTDPARTS_DEFAULT "\0" \ "meminfo=mem=128M\0" \ "nfsroot=/nfsroot/arm\0" \ "bootblock=5\0" \ "ubiblock=4\0" \ "ubi=enabled" /* * Miscellaneous configurable options */ #define CONFIG_SYS_PBSIZE 384 /* Print Buffer Size */ /* memtest works on */ #define CONFIG_SYS_LOAD_ADDR CONFIG_SYS_SDRAM_BASE /* SMDKC100 has 1 banks of DRAM, we use only one in U-Boot */ #define PHYS_SDRAM_1 CONFIG_SYS_SDRAM_BASE /* SDRAM Bank #1 */ #define PHYS_SDRAM_1_SIZE (128 << 20) /* 0x8000000, 128 MB Bank #1 */ #define CONFIG_SYS_MONITOR_BASE 0x00000000 /*----------------------------------------------------------------------- * FLASH and environment organization */ #define CONFIG_SYS_MONITOR_LEN (256 << 10) /* 256 KiB */ #if !defined(CONFIG_NAND_SPL) && (CONFIG_SYS_TEXT_BASE >= 0xc0000000) #define CONFIG_ENABLE_MMU #endif #ifdef CONFIG_ENABLE_MMU #define CONFIG_SYS_MAPPED_RAM_BASE 0xc0000000 #else #define CONFIG_SYS_MAPPED_RAM_BASE CONFIG_SYS_SDRAM_BASE #endif /*----------------------------------------------------------------------- * Boot configuration */ #define CONFIG_USE_ONENAND_BOARD_INIT #define CONFIG_SAMSUNG_ONENAND 1 #define CONFIG_SYS_ONENAND_BASE 0xE7100000 #define CONFIG_SYS_INIT_SP_ADDR (CONFIG_SYS_LOAD_ADDR - 0x1000000) /* * Ethernet Contoller driver */ #ifdef CONFIG_CMD_NET #define CONFIG_ENV_SROM_BANK 3 /* Select SROM Bank-3 for Ethernet*/ #endif /* CONFIG_CMD_NET */ #endif /* __CONFIG_H */ |
显然,这是一个配置文件,包含有各种宏定义。
我们在include/configs目录新建一个jz2440.h的文件,内容暂时如下:
1 2 3 4 5 6 7 8 9 10 11 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote * * Configuation settings for the SAMSUNG JZ2440 board. */ #ifndef __CONFIG_JZ2440_H #define __CONFIG_JZ2440_H #endif /* __CONFIG_JZ2440_H */ |
没错,我们里面什么配置也不做,读者不必在此纠结,先看下去。
5.4 新建defconfig文件
编译过比较新版本u-boot的读者一定会知道,现在u-boot与linux内核的编译系统已经基本相同,都有Kconfig、Makefile,以及用于配置的make xxx_defconfig和make menuconfig的功能,所以此处我们需要新建一个defconfig文件,以用于控制各个源代码文件的编译,其位置位于顶层目录的configs目录下。
我们在configs目录新建一个jz2440_defconfig的文件,与jz2440.h类似,内容暂时为空。
5.5 defconfig与.h文件的内容
我们已经知道,这两个文件都是配置文件,但是内容究竟应该写什么?
此时,我们应该停下脚步,好好思考一下,请看下文。
5.6 初探u-boot的配置系统
首先,根据我们3明确移植方向中的内容,我们应该选择描述arm架构的宏;其次,我们应该选择描述arm920t架构的宏。我们知道,所谓选择某个宏,其作用就是控制是否编译某个(几个)c文件。
根据上述思路,我们进入到arch/arm/cpu/arm920t目录,
再看该目录下的Makefile,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # SPDX-License-Identifier: GPL-2.0+ # # (C) Copyright 2000-2006 # Wolfgang Denk, DENX Software Engineering, [email protected]. extra-y = start.o obj-y += cpu.o obj-$(CONFIG_EP93XX) += ep93xx/ obj-$(CONFIG_IMX) += imx/ # some files can only build in ARM mode ifdef CONFIG_$(SPL_)SYS_THUMB_BUILD CFLAGS_cpu.o := -marm endif |
可以看到,主要是start.S以及cpu.c两个文件,它们均已经默认被设置为了y,表示编译进u-boot,至于其他的均是其他soc相关的,我们可以不用管。这里顺便说一下,start.S是不是有点熟悉?没错,它就是arm920t架构cpu的汇编启动代码,里面的内容这里就不分析了,它不是此篇文章的重点。
接着,我们再看上一层目录arch/arm/cpu的文件以及Makefile,
1 2 3 | # SPDX-License-Identifier: GPL-2.0+ obj- += dummy.o |
这里的Makefile,我没有看懂,此处留一个疑问,欢迎大家解答O(∩_∩)O
这里再顺便提一句,大家看到u-boot.lds没?没错,它就是此目录下所有使用arm架构soc芯片的开发板的u-boot可执行文件的链接脚本,同样,不是重点,此处也不分析了。
我们再看上一层目录arch/arm的文件,
这个目录的文件我们在3.1有过分析。
重点关注此目录下的Kconfig,由于Kconfig内容众多,我们此处挑选比较关心的内容,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | # arch/arm/Kconfig menu "ARM architecture" depends on ARM ...... config CPU_ARM920T bool select SYS_CACHE_SHIFT_5 imply SYS_ARM_MMU ...... config CPU_V7A bool select HAS_THUMB2 select HAS_VBAR select SYS_CACHE_SHIFT_6 imply SYS_ARM_MMU ...... config ARCH_S5PC1XX bool "Samsung S5PC1XX" select CPU_V7A select DM select DM_GPIO select DM_I2C select DM_SERIAL imply CMD_DM ...... source "arch/arm/mach-s5pc1xx/Kconfig" ...... endmenu |
此处我们注意最上面的depends on ARM,也就是说,我们需要配置CONFIG_ARM=y之后,整个menu才会生效,并且会导入arch/arm/mach-s5pc1xx/Kconfig等众多子Kconfig,所以这应该是我们的配置文件中的第一个配置项。
接着,看到ARCH_S5PC1XX以及最后的source "arch/arm/mach-s5pc1xx/Kconfig"了吗?我们暂且先看看这个Kconfig,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | # arch/arm/mach-s5pc1xx/Kconfig if ARCH_S5PC1XX choice prompt "S5PC1XX board select" optional config TARGET_S5P_GONI bool "S5P Goni board" select OF_CONTROL select BLK select DM_MMC config TARGET_SMDKC100 bool "Support smdkc100 board" select OF_CONTROL endchoice config SYS_SOC default "s5pc1xx" source "board/samsung/goni/Kconfig" source "board/samsung/smdkc100/Kconfig" endif |
首先,看到ARCH_S5PC1XX了吗?它就是上文提到的arch/arm/Kconfig中的ARCH_S5PC1XX,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # arch/arm/Kconfig ...... config ARCH_S5PC1XX bool "Samsung S5PC1XX" select CPU_V7A select DM select DM_GPIO select DM_I2C select DM_SERIAL imply CMD_DM ...... |
以及source “board/samsung/smdkc100/Kconfig”,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # board/samsung/smdkc100/Kconfig if TARGET_SMDKC100 config SYS_BOARD default "smdkc100" config SYS_VENDOR default "samsung" config SYS_SOC default "s5pc1xx" config SYS_CONFIG_NAME default "smdkc100" endif |
也就是说,当我们配置CONFIG_ARCH_S5PC1XX=y之后:
- 根据arch/arm/Kconfig,它会自动配置CONFIG_CPU_V7A=y,CONFIG_DM=y,CONFIG_DM_GPIO=y,CONFIG_DM_I2C=y,CONFIG_DM_SERIAL=y,CONFIG_CMD_DM=y;
- 根据arch/arm/mach-s5pc1xx/Kconfig(根据上文,当我们配置CONFIG_ARM=y时便会导入),它会导入board/samsung/smdkc100/Kconfig;
- 根据board/samsung/smdkc100/Kconfig,我们需要配置CONFIG_TARGET_SMDKC100=y,之后u-boot便会执行board/samsung/smdkc100目录下的Makefile,以及知道你的.h配置文件为smdkc100.h;
综上,继CONFIG_ARM=y之后,我们需要配置CONFIG_ARCH_S5PC1XX=y以及CONFIG_TARGET_SMDKC100=y。
5.7 举一反三,新增Kconfig与defconfig中的内容
有了上述smdkc100单板的例子,凭大家聪明的头脑,举一反三的能力,应该知道怎么做了吧?
5.7.1 新增Kconfig中的内容
在arch/arm/Kconfig中增加关于jz2440的内容,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | menu "ARM architecture" depends on ARM ...... config CPU_ARM920T bool select SYS_CACHE_SHIFT_5 imply SYS_ARM_MMU ...... config CPU_V7A bool select HAS_THUMB2 select HAS_VBAR select SYS_CACHE_SHIFT_6 imply SYS_ARM_MMU ...... config ARCH_S5PC1XX bool "Samsung S5PC1XX" select CPU_V7A select DM select DM_GPIO select DM_I2C select DM_SERIAL imply CMD_DM ...... config TARGET_JZ2440 bool "Support jz2440" select CPU_ARM920T ...... source "arch/arm/mach-s5pc1xx/Kconfig" ...... source "board/samsung/jz2440/Kconfig" ...... endmenu |
注意,由于我们针对的soc只有s3c2440,所以这里我们就不在arch/arm目录下新建mach-xxx的目录了,建立这些mach-xxx目录的目的在于同一厂家或者同一厂家下的同一代产品可能会包含有好多soc产品,这里来进行统一描述。
所以,我们越过了ARCH_S3C24XX,直接配置TARGET_JZ2440,有兴趣的读者也可以新建诸如mach-s3c24xx目录,配置ARCH_S3C24XX,以支持s3c2410、s3c2440等等。
5.7.2 新增defconfig中的内容
在configs/jz2440_defconfig中进行增加后,内容如下,
1 2 | CONFIG_ARM=y CONFIG_TARGET_JZ2440=y |
这样,我们便初步串联起了jz2440的配置过程。
5.8 make xxx_defconfig与.config的关系
到这里,我们可以尝试进行配置并编译了,但是,这里我们再暂停一下,解决一个疑惑。
好多人,包括我自己,之前一直认为,make xxx_defconfig就是将xxx_defconfig复制为.config,但是,是这么简单吗?这里我们正好可以做个实验。
我们先在jz2440_defconfig中将之前的内容注释掉,
1 2 | # CONFIG_ARM=y # CONFIG_TARGET_JZ2440=y |
然后开始配置,执行如下命令,
接下来看一下.config的内容,由于太多,只截取前面的一小部分,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | # # Automatically generated file; DO NOT EDIT. # U-Boot 2020.07-rc2 Configuration # # CONFIG_ARC is not set # CONFIG_ARM is not set # CONFIG_M68K is not set # CONFIG_MICROBLAZE is not set # CONFIG_MIPS is not set # CONFIG_NDS32 is not set # CONFIG_NIOS2 is not set # CONFIG_PPC is not set # CONFIG_RISCV is not set CONFIG_SANDBOX=y # CONFIG_SH is not set # CONFIG_X86 is not set # CONFIG_XTENSA is not set CONFIG_SYS_ARCH="sandbox" CONFIG_SYS_CPU="sandbox" CONFIG_SYS_BOARD="sandbox" CONFIG_SYS_CONFIG_NAME="sandbox" CONFIG_SYS_TEXT_BASE= CONFIG_SYS_MALLOC_F_LEN=0x2800 CONFIG_ENV_SIZE=0x1f000 CONFIG_DM_GPIO=y CONFIG_ERR_PTR_OFFSET=0x0 CONFIG_NR_DRAM_BANKS=4 CONFIG_BOOTSTAGE_STASH_ADDR=0 CONFIG_IDENT_STRING="" CONFIG_BUILD_TARGET="" # # Sandbox architecture # # CONFIG_SANDBOX64 is not set # CONFIG_SANDBOX_SPL is not set # CONFIG_HOST_32BIT is not set CONFIG_HOST_64BIT=y CONFIG_SANDBOX_BITS_PER_LONG=64 # CONFIG_DEBUG_UART is not set # CONFIG_AHCI is not set # # General setup # CONFIG_LOCALVERSION="" CONFIG_LOCALVERSION_AUTO=y CONFIG_CC_OPTIMIZE_FOR_SIZE=y # CONFIG_CC_COVERAGE is not set # CONFIG_DISTRO_DEFAULTS is not set # CONFIG_ENV_VARS_UBOOT_CONFIG is not set # CONFIG_SYS_BOOT_GET_CMDLINE is not set # CONFIG_SYS_BOOT_GET_KBD is not set CONFIG_SYS_MALLOC_F=y CONFIG_EXPERT=y CONFIG_SYS_MALLOC_CLEAR_ON_INIT=y # CONFIG_SYS_MALLOC_DEFAULT_TO_INIT is not set # CONFIG_TOOLS_DEBUG is not set # CONFIG_PHYS_64BIT is not set # CONFIG_SYS_CUSTOM_LDSCRIPT is not set CONFIG_PLATFORM_ELFENTRY="_start" |
怎么样?是不是直接复制已经一目了然了吧?可以看到,由于我们在jz2440_defconfig中什么也没指定,u-boot在.config中给了我们一个cpu架构的默认值sandbox,此外还有好多架构无关的默认值。
接下来我们在jz2440_defconfig中打开CONFIG_ARM,
1 2 | CONFIG_ARM=y # CONFIG_TARGET_JZ2440=y |
然后清理工程后重新进行配置,
再次查看.config内容,同样只列出前面一小部分,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | # # Automatically generated file; DO NOT EDIT. # U-Boot 2020.07-rc2 Configuration # CONFIG_CREATE_ARCH_SYMLINK=y # CONFIG_ARC is not set CONFIG_ARM=y # CONFIG_M68K is not set # CONFIG_MICROBLAZE is not set # CONFIG_MIPS is not set # CONFIG_NDS32 is not set # CONFIG_NIOS2 is not set # CONFIG_PPC is not set # CONFIG_RISCV is not set # CONFIG_SANDBOX is not set # CONFIG_SH is not set # CONFIG_X86 is not set # CONFIG_XTENSA is not set CONFIG_SYS_ARCH="arm" CONFIG_SYS_CPU="armv8" CONFIG_SYS_SOC="hi6220" CONFIG_SYS_VENDOR="hisilicon" CONFIG_SYS_BOARD="hikey" CONFIG_SYS_CONFIG_NAME="hikey" # CONFIG_SYS_ICACHE_OFF is not set # CONFIG_SYS_DCACHE_OFF is not set # # ARM architecture # CONFIG_ARM64=y # CONFIG_POSITION_INDEPENDENT is not set # CONFIG_INIT_SP_RELATIVE is not set # CONFIG_GIC_V3_ITS is not set CONFIG_STATIC_RELA=y CONFIG_DMA_ADDR_T_64BIT=y CONFIG_ARM_ASM_UNIFIED=y # CONFIG_SYS_ARM_CACHE_CP15 is not set # CONFIG_SYS_ARM_MMU is not set # CONFIG_SYS_ARM_MPU is not set CONFIG_SYS_ARM_ARCH=8 CONFIG_SYS_CACHE_SHIFT_6=y CONFIG_SYS_CACHELINE_SIZE=64 CONFIG_SYS_ARM_CACHE_WRITEBACK=y # CONFIG_SYS_ARM_CACHE_WRITETHROUGH is not set # CONFIG_SYS_ARM_CACHE_WRITEALLOC is not set # CONFIG_ARCH_CPU_INIT is not set CONFIG_SYS_ARCH_TIMER=y CONFIG_ARM_SMCCC=y |
怎么样,变为arm了吧?只不过之后的cpu,soc等等都不对,我们再打开jz2440_defconfig中的CONFIG_TARGET_JZ2440,
1 2 | CONFIG_ARM=y CONFIG_TARGET_JZ2440=y |
然后清理工程后再次重新进行编译,
再次查看.config内容,同样只列出前面一小部分,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | # # Automatically generated file; DO NOT EDIT. # U-Boot 2020.07-rc2 Configuration # CONFIG_CREATE_ARCH_SYMLINK=y # CONFIG_ARC is not set CONFIG_ARM=y # CONFIG_M68K is not set # CONFIG_MICROBLAZE is not set # CONFIG_MIPS is not set # CONFIG_NDS32 is not set # CONFIG_NIOS2 is not set # CONFIG_PPC is not set # CONFIG_RISCV is not set # CONFIG_SANDBOX is not set # CONFIG_SH is not set # CONFIG_X86 is not set # CONFIG_XTENSA is not set CONFIG_SYS_ARCH="arm" CONFIG_SYS_CPU="arm920t" CONFIG_SYS_SOC="s3c2440" CONFIG_SYS_VENDOR="samsung" CONFIG_SYS_BOARD="jz2440" CONFIG_SYS_CONFIG_NAME="jz2440" # CONFIG_SYS_ICACHE_OFF is not set # CONFIG_SYS_DCACHE_OFF is not set # # ARM architecture # # CONFIG_GIC_V3_ITS is not set CONFIG_ARM_ASM_UNIFIED=y CONFIG_SYS_ARM_CACHE_CP15=y CONFIG_SYS_ARM_MMU=y # CONFIG_SYS_ARM_MPU is not set CONFIG_CPU_ARM920T=y CONFIG_SYS_ARM_ARCH=4 CONFIG_SYS_CACHE_SHIFT_5=y CONFIG_SYS_CACHELINE_SIZE=32 CONFIG_SYS_ARM_CACHE_WRITEBACK=y # CONFIG_SYS_ARM_CACHE_WRITETHROUGH is not set # CONFIG_SYS_ARM_CACHE_WRITEALLOC is not set # CONFIG_ARCH_CPU_INIT is not set # CONFIG_SEMIHOSTING is not set # CONFIG_SYS_THUMB_BUILD is not set # CONFIG_SYS_L2CACHE_OFF is not set # CONFIG_ENABLE_ARM_SOC_BOOT0_HOOK is not set CONFIG_USE_ARCH_MEMCPY=y CONFIG_USE_ARCH_MEMSET=y # CONFIG_SET_STACK_SIZE is not set # CONFIG_ARCH_AT91 is not set # CONFIG_TARGET_EDB93XX is not set # CONFIG_TARGET_ASPENITE is not set |
感觉如何?
可以看到,
1 2 3 4 5 6 | CONFIG_SYS_ARCH="arm" CONFIG_SYS_CPU="arm920t" CONFIG_SYS_SOC="s3c2440" CONFIG_SYS_VENDOR="samsung" CONFIG_SYS_BOARD="jz2440" CONFIG_SYS_CONFIG_NAME="jz2440" |
这些正是在arch/arm/Kconfig以及borad/samsung/jz2440/Kconfig中配置的选项值,忘记的读者可以回头去看看,只不过要注意Kconfig中并没有CONFIG_的前缀,这是u-boot默认给加上去的。
到这里,我们可以想到,执行make xxx_defconfig的背后,不仅会读取xxx_defconfig中的内容,还会读取各级Kconfig中的配置,最终生成.config。
6. 初步编译
终于可以尝试编译了,这里关于交叉编译器的设置就不做介绍了,相信大家应该都会的,只不过需要注意的是高版本的u-boot同样需要高版本的编译器。
好的,执行如下命令,指定编译器、cpu架构,开始进行编译,
但是,又有问题了,根据上述信息,我们还需要配置CONFIG_SYS_TEXT_BASE,这是u-boot的链接地址,我们编译好u-boot后要把可执行文件复制到内存的这个地址上才行,好,我们在jz2440_defconfig中加上这个配置,
1 2 3 | CONFIG_ARM=y CONFIG_TARGET_JZ2440=y CONFIG_SYS_TEXT_BASE=0x33f00000 |
然后重新配置编译,
嗯,已经可以进行正常编译了。
终于出现错误了,
上述编译信息表明,我们需要定义两个宏:
- CONFIG_SYS_MALLOC_LEN,表明u-boot使用malloc函数时的堆内存大小;
- CONFIG_SYS_LOAD_ADDR,表明u-boot的source命令运行脚本时脚本在sdram的位置,看来虽然我们没有选择这个命令,但是u-boot已经默认为我们加上了。
好的,我们在jz2440.h中加入以上两个宏定义,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote * * Configuation settings for the SAMSUNG JZ2440 board. */ #ifndef __CONFIG_JZ2440_H #define __CONFIG_JZ2440_H #define CONFIG_SYS_LOAD_ADDR 0x30800000 #define CONFIG_SYS_MALLOC_LEN (4 * 1024 * 1024) #endif /* __CONFIG_JZ2440_H */ |
这里为了方便,我将清理,配置,编译三个命令写成了一个build.sh脚本放到u-boot工程的根目录,以后调试会方便些,
1 2 3 | make distclean make jz2440_defconfig make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- -j8 |
然后,./build.sh,到最后链接的时候还是有错误,
我们首先解决第一个CONFIG_SYS_INIT_SP_ADDR宏定义的问题,这个宏的功能是指定c函数使用的栈的地址,我们在jz2440.h中加入这个宏定义,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote * * Configuation settings for the SAMSUNG JZ2440 board. */ #ifndef __CONFIG_JZ2440_H #define __CONFIG_JZ2440_H #define CONFIG_SYS_LOAD_ADDR 0x30800000 #define CONFIG_SYS_MALLOC_LEN (4 * 1024 * 1024) #define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */ #define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */ #define CONFIG_SYS_SDRAM_BASE PHYS_SDRAM_1 #define CONFIG_SYS_INIT_SP_ADDR (CONFIG_SYS_SDRAM_BASE + 0x1000 - GENERATED_GBL_DATA_SIZE) #endif /* __CONFIG_JZ2440_H */ |
./build.sh,
这里需要继续解决上面的函数未定义问题,这些未定义的函数都是硬性相关的,需要根据具体的硬件进行编写:
- 在arch/arm/lib/reset.c中的do_reset函数里面调用了未定义的reset_cpu函数
- 在arch/arm/cpu/arm920t/start.S中的cpu_init_crit函数中调用了未定义的lowlevel_init函数
- 在common/bootretry.c中的bootretry_reset_cmd_timeout函数调用了未定义的get_tbclk函数
- 在common/cli_readline.c中的cread_line函数调用了未定义的get_tbclk函数
- 在.rodata.init_sequence_f段中调用了未定义的print_cpuinfo函数、dram_init函数
- 在.data.init_sequence_r段中调用了未定义的board_init函数
- 在drivers/serial/serial.c中的get_current函数、serial_initialize函数均调用了未定义的default_serial_console函数
- 在lib/time.c中的tick_to_time函数、usec_to_tick函数均调用了未定义的get_tbclk函数
总结下来,我们需要实现以下函数:
- reset_cpu()
- lowlevel_init()
- get_tbclk()
- print_cpuinfo()
- dram_init()
- board_init()
- default_serial_console()
7. 完成未定义的函数
首先我们关注lowlevel_init函数。
lowlevel_init函数主要完成一些特定于硬件的早期初始化工作,一般是初始化时钟以及sdram,这些我们在spl中已经完成了,所以就不用再初始化了。那如何去掉未定义错误呢?我们到arch/arm/cpu/arm920t/start.S中去看看,其中有如下一段汇编代码,
1 2 3 4 5 6 7 8 9 10 11 | #ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY /* * before relocating, we have to setup RAM timing * because memory timing is board-dependend, you will * find a lowlevel_init.S in your board directory. */ mov ip, lr bl lowlevel_init mov lr, ip #endif |
如果我们不需要执行lowlevel_init函数,那么只需要定义CONFIG_SKIP_LOWLEVEL_INIT_ONLY这个宏即可,我们在jz2440.h中定义CONFIG_SKIP_LOWLEVEL_INIT_ONLY,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote * * Configuation settings for the SAMSUNG JZ2440 board. */ #ifndef __CONFIG_JZ2440_H #define __CONFIG_JZ2440_H #define CONFIG_SKIP_LOWLEVEL_INIT_ONLY #define CONFIG_SYS_LOAD_ADDR 0x30800000 #define CONFIG_SYS_MALLOC_LEN (4 * 1024 * 1024) #define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */ #define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */ #define CONFIG_SYS_SDRAM_BASE PHYS_SDRAM_1 #define CONFIG_SYS_INIT_SP_ADDR (CONFIG_SYS_SDRAM_BASE + 0x1000 - GENERATED_GBL_DATA_SIZE) #endif /* __CONFIG_JZ2440_H */ |
然后重新进行配置并编译,
我们看到,已经没有lowlevel_init函数未定义的错误了。
接下来,剩下的几个函数,我参考了u-boot 2016版本,在这个版本中还有对s3c2440的支持,将这些函数写在jz2440.c中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | // SPDX-License-Identifier: GPL-2.0+ /* * Copyright (C) 2008-2009 Samsung Electronics * Minkyu Kang <[email protected]> * Kyungmin Park <[email protected]> */ #include <common.h> #include <init.h> #include <asm/io.h> #include <asm/mach-types.h> DECLARE_GLOBAL_DATA_PTR; int board_init(void) { return 0; } int print_cpuinfo(void) { printf("hello, u-boot!\n"); return 0; } /* * This function is derived from PowerPC code (timebase clock frequency). * On ARM it returns the number of timer ticks per second. */ ulong get_tbclk(void) { return CONFIG_SYS_HZ; } /* * reset the cpu by setting up the watchdog timer and let him time out */ void reset_cpu(ulong ignored) { #define WTCON 0x53000000 #define WTCNT 0x53000008 /* Disable watchdog */ writel(0x0000, WTCON); /* Initialize watchdog timer count register */ writel(0x0001, WTCNT); /* Enable watchdog timer; assert reset at timer timeout */ writel(0x0021, WTCON); while (1) /* loop forever and wait for reset to happen */; /*NOTREACHED*/ } int dram_init(void) { gd->ram_size = get_ram_size((long *)PHYS_SDRAM_1, PHYS_SDRAM_1_SIZE); return 0; } int dram_init_banksize(void) { gd->bd->bi_dram[0].start = PHYS_SDRAM_1; gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; return 0; } |
-
对于board_init函数、print_cpuinfo函数,为了简单我都做了简化,直接返回
-
对于get_tbclk函数,我们还需要定义一个宏CONFIG_SYS_HZ,表明soc基本的时钟单位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote * * Configuation settings for the SAMSUNG JZ2440 board. */ #ifndef __CONFIG_JZ2440_H #define __CONFIG_JZ2440_H #define CONFIG_SKIP_LOWLEVEL_INIT_ONLY #define CONFIG_SYS_LOAD_ADDR 0x30800000 #define CONFIG_SYS_MALLOC_LEN (4 * 1024 * 1024) #define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */ #define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */ #define CONFIG_SYS_SDRAM_BASE PHYS_SDRAM_1 #define CONFIG_SYS_INIT_SP_ADDR (CONFIG_SYS_SDRAM_BASE + 0x1000 - GENERATED_GBL_DATA_SIZE) #define CONFIG_SYS_HZ 1000 #endif /* __CONFIG_JZ2440_H */ |
现在只剩下最后一个default_serial_console函数了,这个函数的功能是提供一个串口设备给u-boot,对啊,没有串口我们怎么打印启动信息呢?
8. 编写串口驱动
终于开始进行驱动的移植与编写了!
提到驱动,在最近几年的u-boot版本中,uboot引入了驱动模型(driver model),那具体是什么呢?各位别急,我们之后的每一个驱动都将使用这种驱动模型进行编写,那现在先看比较简单的串口驱动,从实际的驱动中一步步的了解驱动模型dm。
8.1 驱动代码
首先在drivers/serial目录下新建一个文件serial_s3c2440.c,全部内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | /* SPDX-License-Identifier: GPL-2.0+ */ /* * (C) Copyright 2020 Asymptote */ #include <common.h> #include <dm.h> #include <errno.h> #include <fdtdec.h> #include <linux/compiler.h> #include <asm/io.h> #include <serial.h> DECLARE_GLOBAL_DATA_PTR; struct s3c2440_serial { u32 ulcon; u32 ucon; u32 ufcon; u32 umcon; u32 utrstat; u32 uerstat; u32 ufstat; u32 umstat; u32 utxh; u32 urxh; u32 ubrdiv; }; struct s3c2440_serial_priv { struct s3c2440_serial *reg; }; int s3c2440_serial_setbrg(struct udevice *dev, int baudrate) { struct s3c2440_serial_priv *priv = dev_get_priv(dev); u32 val, uclk; /* 使用pclk时钟,大小为50MHz */ uclk = 50000000; val = uclk / baudrate; writel(val / 16 - 1, &priv->reg->ubrdiv); return 0; } static int s3c2440_serial_getc(struct udevice *dev) { struct s3c2440_serial_priv *priv = dev_get_priv(dev); if (!(readl(&priv->reg->utrstat) & (1 << 0))) return -EAGAIN; return (int)(readb(&priv->reg->urxh) & 0xff); } static int s3c2440_serial_putc(struct udevice *dev, const char ch) { struct s3c2440_serial_priv *priv = dev_get_priv(dev); if (!(readl(&priv->reg->utrstat) & (1 << 2))) return -EAGAIN; writeb(ch, &priv->reg->utxh); return 0; } static int s3c2440_serial_pending(struct udevice *dev, bool input) { struct s3c2440_serial_priv *priv = dev_get_priv(dev); uint32_t utrstat; utrstat = readl(&priv->reg->utrstat); if (input) return (utrstat & (1 << 0)); else return (utrstat & (1 << 2)); } static const struct dm_serial_ops s3c2440_serial_ops = { .putc = s3c2440_serial_putc, .pending = s3c2440_serial_pending, .getc = s3c2440_serial_getc, .setbrg = s3c2440_serial_setbrg, }; #define GPHCON (*(volatile unsigned long *)0x56000070) #define GPHUP (*(volatile unsigned long *)0x56000078) static int s3c2440_serial_probe(struct udevice *dev) { struct s3c2440_serial_priv *priv = dev_get_priv(dev); priv->reg = (void *)dev_read_addr(dev); /* GPH2,GPH3用作TXD0,RXD0 */ GPHCON |= 0xa0; /* GPH2,GPH3内部上拉 */ GPHUP = 0x0c; /* 8N1(8个数据位,无较验,1个停止位) */ writel(0x03, &priv->reg->ulcon); /* 查询方式,UART时钟源为PCLK */ writel(0x05, &priv->reg->ucon); /* 不使用FIFO */ writel(0x00, &priv->reg->ufcon); /* 不使用流控 */ writel(0x00, &priv->reg->umcon); return 0; } static const struct udevice_id s3c2440_serial_ids[] = { { .compatible = "samsung,s3c2440-uart" }, { } }; U_BOOT_DRIVER(serial_s3c2440) = { .name = "serial_s3c2440", .id = UCLASS_SERIAL, .of_match = s3c2440_serial_ids, .probe = s3c2440_serial_probe, .ops = &s3c2440_serial_ops, .priv_auto_alloc_size = sizeof(struct s3c2440_serial_priv), }; |
这个驱动非常简单,只有区区130行左右,但是简单归简单,麻雀虽小五脏俱全,要想学习u-boot的设备模型框架,这是一个非常好的例子。
首先说明一下,这个驱动是我借鉴韦东山老师的串口裸机驱动以及u-boot中的其他使用设备模型的串口驱动重新写的,算是做了一个组合者的作用。
另外,大家还记得之前编译的最后default_serial_console函数未定义的问题吗?default_serial_console函数属于u-boot旧的串口驱动框架,我们的驱动由于是遵循新的设备模型框架的,所以就可以不用理会这个问题了。
好,下面开始进行介绍。
与阅读linux驱动代码相同,我们从下往上看,首先是定义一个如下的结构体,
1 2 3 4 5 6 7 8 9 | /* 可以类比为linux设备驱动模型中的platform_driver */ U_BOOT_DRIVER(serial_s3c2440) = { .name = "serial_s3c2440", /* 该驱动的名称 */ .id = UCLASS_SERIAL, /* 该驱动属于UCLASS_SERIAL这一类 */ .of_match = s3c2440_serial_ids, /* 用于与设备树中的节点匹配 */ .probe = s3c2440_serial_probe, /* 匹配成功后执行的探测函数 */ .ops = &s3c2440_serial_ops, /* 串口这一类驱动的操作函数集合 */ .priv_auto_alloc_size = sizeof(struct s3c2440_serial_priv), /* u-boot为该驱动分配的私有数据空间的大小 */ }; |
对于上面的内容,许多读者可能会感到很陌生,这里我想顺便说一下我自己的学习经验供大家借鉴。
当我移植进行到这里的时候看到u-boot有了这样一个新的设备模型框架,此时,我的做法并不是去网上查找这方面的内容,我直接找了几个同目录下使用该设备模型的串口驱动,经过对比研究,发现了它们的大致结构都是相同的(其实这里就是所谓的遵从同一框架),我就模仿着他们的代码框架,把相同的部分保留下来,其实剩下的不同的部分一般就是硬件相关的操作各种寄存器了,这样,我可以先不必去详细了解设备模型的框架原理,只关注于硬件操作,等之后有了一定的实践经验了,再回过头去详细了解设备模型的具体内容。这也符合人的认知过程,从感性到理性,而不是一上来就去研究设备模型的框架原理,把自己搞得很是难受,以至于根本没有信心去做接下来的事情了。
言归正传,这里我们可以先只了解每个变量的作用是什么,至于何时被调用之类的可以之后再去了解,免得给自己增加入门难度,打击自信心。
可以看到,每个变量的作用我已经在后面写了注释说明,其中比较重要的有三个变量:
- s3c2440_serial_ids
说到这个变量,不得不提到另一个除了设备模型外u-boot的新改变,那就是开始使用设备树。至于语法与linux的设备树完全相同,可以说就是借鉴linux而来。那这样的话,我们在arch/arm/dts目录下新建一个jz2440.dts的文件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // SPDX-License-Identifier: GPL-2.0+ /* * Samsung's S3c2440-based JZ2440 board device tree source * * Copyright (c) 2020 Asymptote */ /dts-v1/; #include "skeleton.dtsi" / { model = "JZ2440"; compatible = "samsung,s3c2440"; aliases { console = &serial0; }; serial0: serial@50000000 { compatible = "samsung,s3c24x0-uart"; reg = <0x50000000 0x100>; }; }; |
熟悉linux设备树的读者应该会感到很熟悉,这里我们:
-
指定默认的串口设备为serial0
-
添加串口设备的节点serial0,compatible对应s3c2440_serial_ids变量中的compatible成员,只要二者内容相同,u-boot设备模型框架便会为我们执行下面的s3c2440_serial_probe函数。
同时还需要在arch/arm/dts/Makefile文件中加入编译信息,这样我们的dts文件才会被编译,
1 2 3 4 5 6 7 8 9 | ...... dtb-$(CONFIG_TARGET_GURNARD) += at91sam9g45-gurnard.dtb dtb-$(CONFIG_TARGET_JZ2440) += jz2440.dtb dtb-$(CONFIG_S5PC100) += s5pc1xx-smdkc100.dtb ...... |
最后,还需要在jz2440_defconfig中加入设备树相关的配置信息,
1 2 3 4 5 6 7 8 9 10 11 | # Architecture and machine CONFIG_ARM=y CONFIG_TARGET_JZ2440=y # Link address CONFIG_SYS_TEXT_BASE=0x33f00000 # Device tree CONFIG_OF_CONTROL=y CONFIG_OF_SEPARATE=y CONFIG_DEFAULT_DEVICE_TREE="jz2440" |
- CONFIG_OF_CONTROL表示使用设备树;
- CONFIG_OF_SEPARATE表示由dts编译成的dtb文件追加到u-boot.bin的最后面;
- CONFIG_DEFAULT_DEVICE_TREE表示使用的默认dts文件名称前缀,因为可能对于有的单板会有好几个dts文件同时被编译,那这里便是指定默认使用哪一个;
关于更具体的设备树信息,可以参考如下博客,我当时就是参考的这个博客,在此也十分感谢这位博主:
https://blog.csdn.net/ooonebook/article/details/53206623
- s3c2440_serial_probe
只要与设备树中的对应节点匹配成功,此函数便会被调用。一般在这个函数中都是完成从设备树中获取设备信息,初始化硬件等等。
这里我们完成的功能主要有两点:
- 从设备树中获取串口的寄存器首地址,并存到设备私有结构体中,需要说明的是:
- 该函数的参数struct udevice *dev:与linux的平台总线驱动模型中的xxx_probe函数的参数struct platform_device *pdev功能类似,同样是u-boot初始化的时候解析设备树,当匹配后就会将设备树中对应的设备信息转换成struct udevice结构体,并作为probe函数的参数最后再调用probe函数(这里就是我们赋值的s3c2440_serial_probe函数)。所以,我们可以从struct udevice *dev中获取到设备树中的关于该设备的所有信息,这里主要是寄存器首地址
- 初始化串口,需要说明的是:
- 设置串口的gpio引脚时,这里是直接操作的寄存器。其实我已经实现了基于设备模型的pinctrl子系统驱动,但是这里为了简单起见,就直接操作寄存器了,后面我会讲解pinctrl子系统的驱动,之后我们再在设备树中利用pinctrl设置gpio为串口功能以及内部上拉功能
- 设置串口的时钟时,也是直接操作的寄存器,我也已经实现了基于设备模型的clock子系统,同样为了简单起见,就直接操作寄存器了,后面讲解clock驱动后,再改回来
- s3c2440_serial_ops
这应该是该驱动的核心,主要实现了串口驱动常见的几个函数:
- s3c2440_serial_putc:向pc串口终端打印一个字符
- s3c2440_serial_getc:从pc串口终端获取一个字符
- s3c2440_serial_pending:返回pc串口终端的输入状态给soc芯片
- s3c2440_serial_setbrg:设置串口的波特率
这应该是再熟悉不过的几个串口驱动函数了,这里只说明一点,就是在设置波特率的时候,这里是直接指定给到串口的时钟频率为50MHz,如果要是正规来说,还是要使用clock子系统来进行设置,就像初始化时候一样,为了简化就直接指定了,后面讲解clock驱动后,同样会改回来。
另外,关于每个函数的参数struct udevice *dev与s3c2440_serial_probe函数的参数相同,我们在s3c2440_serial_probe函数中获取到寄存器的首地址,在ops的这几个函数中可以直接使用这个值。
8.2 修改Kconfig以及Makefile
最后,我们还需要修改Kconfig以及Makefile。
- 在drivers/serial目录下的Kconfig中添加关于s3c2440串口驱动的配置信息,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ...... config MTK_SERIAL bool "MediaTek High-speed UART support" depends on DM_SERIAL help Select this to enable UART support for MediaTek High-speed UART devices. This driver uses driver model and requires a device tree binding to operate. The High-speed UART is compatible with the ns16550a UART and have its own high-speed registers. config S3C2440_SERIAL bool "Samsung s3c2440 UART support" depends on DM_SERIAL help This driver supports the Samsung s3c2440 UART. If unsure say N. config MPC8XX_CONS bool "Console driver for MPC8XX" depends on MPC8xx default y ...... |
- 在drivers/serial目录下的Makefile中添加关于s3c2440串口驱动的编译信息,
1 2 3 4 5 6 7 | ...... obj-$(CONFIG_MTK_SERIAL) += serial_mtk.o obj-$(CONFIG_SIFIVE_SERIAL) += serial_sifive.o obj-$(CONFIG_S3C2440_SERIAL) += serial_s3c2440.o ...... |
8.3 修改defconfig
我们还需要修改jz2440_defconfig:
- 由于这是第一个设备模型驱动,需要配置相关宏定义
- 添加关于串口驱动的宏定义,这里把波特率设为115200
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # Architecture and machine CONFIG_ARM=y CONFIG_TARGET_JZ2440=y # Link address CONFIG_SYS_TEXT_BASE=0x33f00000 # Device tree CONFIG_OF_CONTROL=y CONFIG_OF_SEPARATE=y CONFIG_DEFAULT_DEVICE_TREE="jz2440" # Device Model CONFIG_DM=y # Serial driver CONFIG_BAUDRATE=115200 CONFIG_DM_SERIAL=y CONFIG_S3C2440_SERIAL=y |
8.4 编译
好了,可以试着编译一把。
说实话,这个错误当时可让我很是捉急,主要它没有明显的提示信息,我当时差点都想看这个编译器的源码了。。。
后来,我将这个错误贴到网上,最后发现了有个国外网友也遇到类似的问题,最后的解决方法是添加get_timer这个函数。
我结合之前有未定义的错误时也出现了这个断言失败的错误,感觉很大概率是由于get_timer这个函数未定义造成的,于是找到u-boot 2016版本中关于s3c2440的get_timer函数,最后放到了jz2440.c文件中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | // SPDX-License-Identifier: GPL-2.0+ /* * Copyright (C) 2008-2009 Samsung Electronics * Minkyu Kang <[email protected]> * Kyungmin Park <[email protected]> */ #include <common.h> #include <init.h> #include <asm/io.h> #include <asm/mach-types.h> DECLARE_GLOBAL_DATA_PTR; int board_init(void) { return 0; } int print_cpuinfo(void) { printf("hello, u-boot!\n"); return 0; } /* * This function is derived from PowerPC code (timebase clock frequency). * On ARM it returns the number of timer ticks per second. */ ulong get_tbclk(void) { return CONFIG_SYS_HZ; } /* * reset the cpu by setting up the watchdog timer and let him time out */ void reset_cpu(ulong ignored) { #define WTCON 0x53000000 #define WTCNT 0x53000008 /* Disable watchdog */ writel(0x0000, WTCON); /* Initialize watchdog timer count register */ writel(0x0001, WTCNT); /* Enable watchdog timer; assert reset at timer timeout */ writel(0x0021, WTCON); while (1) /* loop forever and wait for reset to happen */; /*NOTREACHED*/ } int dram_init(void) { gd->ram_size = get_ram_size((long *)PHYS_SDRAM_1, PHYS_SDRAM_1_SIZE); return 0; } int dram_init_banksize(void) { gd->bd->bi_dram[0].start = PHYS_SDRAM_1; gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; return 0; } /* * This function is derived from PowerPC code (read timebase as long long). * On ARM it just returns the timer value. */ unsigned long long get_ticks(void) { #define TCNTO4 0x51000040 ulong now = readl(TCNTO4) & 0xffff; if (gd->arch.lastinc >= now) { /* normal mode */ gd->arch.tbl += gd->arch.lastinc - now; } else { /* we have an overflow ... */ gd->arch.tbl += gd->arch.lastinc + gd->arch.tbu - now; } gd->arch.lastinc = now; return gd->arch.tbl; } ulong get_timer_masked(void) { ulong tmr = get_ticks(); return tmr / (gd->arch.timer_rate_hz / CONFIG_SYS_HZ); } /* * timer without interrupts */ ulong get_timer(ulong base) { return get_timer_masked() - base; } |
再次编译,
成功了!看来真是get_timer函数未定义的问题,但让人比较郁闷的是为何编译器它不提示呢?算了,这个问题已经超出我的能力范围了,既然已经解决了就不纠结了。
8.5 烧写
8.5.1 烧写spl.bin到nand flash
我们先利用openjtag将spl.bin烧写到nandflash的0地址处,这里相信大家都已经很熟练了,就不截图示范了。
8.5.2 烧写u-boot.bin到sdcard
这里需要用到一张sdcard(前面也说过,由于spl的sdcard裸机驱动有问题,现在只能支持4g及以下的卡)以及一个读卡器,将sdcard插到读卡器并将读卡器查到pc的usb端口后,在pc端的/dev目录下会出现一个sdx的设备节点,这里的x表示未知的意思,一般是最后一个,比如,我没有插读卡器的时候,/dev目录下sdx是这样的:
插上后是这样的:
也就是说多了一个sdb,至于后面的sdb1,sdb2是我自己为后面放linux的uImage以及根文件系统分的两个区,这个大家可以不用分区,只要有sdx(我这里是sdb)就可以。
接着,执行下面的命令烧写u-boot.bin到sdcard:
1 2 3 4 | sleep 2 ls /dev/sd* sudo dd if=/home/zhangxu/study/s3c2440/u-boot/u-boot.bin of=/dev/sdb bs=512 seek=4 sync |
前面两条命令主要是因为有时候烧写会出现错误,所以我先延时了2秒钟,然后又列出了/dev/sd*,以防写错设备。
第三条命令就是烧写命令了,不熟悉dd命令的读者可以自行去网上查阅资料,这里我就不详细介绍了,大概说一下,这条命令的功能是:
- 将/home/zhangxu/study/s3c2440/u-boot/u-boot.bin写到/dev/sdb(代表我的sdcard)
- 写入的块大小为512字节(与spl中sdcard驱动程序中读写的块大小相同)
- 空过前面4个块(也就是从sdcard绝对地址的第512 * 4 = 2048字节 = 2K字节处开始写入)
将以上命令写成一个脚本copy2sdcard.sh,方便之后的调试,然后执行之进行烧写,
一般速度在一点几M就是正常的,如果几百M的话就出现问题了,需要重新拔插读卡器再次进行烧写。
好了,烧写完毕,激动人心的时刻来临了。
8.6 运行
串口终端我使用的是Ubuntu下的picocom,波特率设为之前在jz2440_defconfig中设置的115200,
好了,现在将sdcard插到jz2440的sd插槽,上电——
yes!成功打印!
最上面的两行信息是我在spl中打印的调试信息,u-boot打印的信息从第三行开始。
此时我们也可以看到,在我们没有人为添加任何命令的情况下,u-boot默认为我们加入了很多的命令,下图只截取了一部分:
正所谓万事开头难,我们现在已经完成了移植的开头工作,现在我们移植的u-boot仅仅只有打印的功能,比如,有的读者可能会问怎么没有倒计时的功能?这是因为我们还没有把这个功能加上,u-boot默认是不支持的,还有许多常用的功能都需要我们后面按需要进行添加。
8.7 写在后面
经过这个串口驱动,我想大家应该对u-boot的设备模型有了感性的认识,接下来,如果想上升到理性认识,想深入了解设备模型,推荐下面这篇博客:
https://blog.csdn.net/ooonebook/article/details/53234020
-END-