@

Windows系统:Windows10 x64

vmware:VMware Workstation 15 Pro

Linux系统:Ubuntu16.04 x64

BootLoader:u-boot-2010.03

Linux内核:Linux2.6

编译链:gcc-3.4.5-glibc-2.3.6

板子介绍:ARM9的CPU,自定义的硬件资源。基于SMDK2416来进行后续移植。

注:使用高版本的Linux内核(3.x、4.x)不一定要升级uboot、busybox的版本;编译器版本倒是必须匹配

一、uboot跳转到Linux

uboot就是裸机代码,可以根据现有裸机代码移植,能进入命令行则uboot的初期移植就完成了。

要启动Linux、传递参数,还需进行如下工作:

1、设置好Linux内核的机器码bi_arch_number。

必须与Linux内核支持的机器码相等才能正常启动Linux;

假设我在Linux内核make menuconfig中选择了SMDK2416型号的开发板,相当于在内核中“支持”了该型号。那么uboot想要启动Linux,则需在board/xxx/mini2440.c的int board_init (void)函数中修改

gd->bd->bi_arch_number = MACH_TYPE_SMDK2416;  //机器码定义在mach-types.h。
gd->bd->bi_boot_params = xxx; //bi_boot_params是启动参数的地址

2、设置好传递给Linux的启动参数bootargs。

bootargs解析:

root:

  根据文件系统存储的位置(flash、网络)不同,结合实际存储情况进行设置。例如:

  root=/dev/mtdblockx rw(x=0,1,2..)

  root=/dev/nfs rw nfsroot=10.103.4.216:/nfsroot/rootfs ip=10.103.4.211

  root=/dev/ram0 rw

  

console:

  console=ttySAC0,115200 控制台使用串口0,波特率115200.

串行端口终端(/dev/ttySn )

控制终端(/dev/tty )

控制台终端(/dev/ttyn, /dev/console )

init:

init 指定的是内核启起来后,进入系统中运行的第一个脚本,一般为init=/linuxrc, /linuxrc指的是/目录下面的linuxrc脚本,一般是一个连接。

initrd, noinitrd:

  当使用ramdisk启动系统的时候,需要指定initrd=r_addr,size。 r_addr表示initrd在内存中的位置,size表示initrd的大小。否则使用noinitrd。

根据实际情况在include/configs/mini2440.h中设置默认的bootargs宏。

也可以在uboot每次启动后setenv bootargs设置参数,再跳转到Linux。

3、移植好网络驱动,使开发板和电脑可以进行tftp文件传输。

如果你不想每次烧写Linux镜像到flash,就需要移植好网络驱动方便在线调试;

二、 Linux内核启动之解压阶段

Linux内核启动uImage需要先进行自解压,再跳到加载地址启动。

内核加载地址设置修改arch\arm\mach-s3c2410\Makefile.boot (虽然用的是基于SMDK2416的内核代码,但是加载地址修改都是在mach-s3c2410\Makefile.boot)

zreladdr-xxx	    := 0xX0008000	//linux内核加载地址
params_phys-xxx := 0xX0000100 //uboot传输过来的参数地址

为什么要偏移0x8000呢?因为0xX0000000到0xX0008000被用来存放uboot传递的参数和内核MMU Table。

自解压阶段,Linux首先运行arch\arm\boot\compressed\head.S文件,然后跳转到arm\boot\compressed\misc.c中运行decompress_kernel()函数,进行内核校验和解压:

makecrc();
putstr("Uncompressing Linux...");
gunzip();
putstr(" done, booting the kernel.\n");

putstr打印函数,会调用include\asm-arm\arch-s3c2410\uncompress.h中的static void putc(int ch)函数,如果uboot阶段已经移植好了串口,那这里我们可以直接将putc函数改写为直接往TX FIFO填充数据,即可实现最早期的打印。

我每次移植不同板子都会卡在这里,可以直接把decompress_kernel()中的打印函数屏蔽。因为打印函数使用了寄存器,和我们的板子不匹配。

static void putc(int ch)
{
//FILL TX FIFO
}

注意:由于还没有MMU映射,该putc函数实现使用寄存器的物理地址。

三、 Linux内核启动之汇编阶段

arch\arm\boot\compressed\head.S解压操作执行成功后跳转到 arch\arm\kernel\head.S

在arch\arm\kernel\head.S中,会判断cpu和机器码是否支持。

3.1 __lookup_processor_type,核对CPU:

每个型号的CPU有个参数结构体存在于汇编文件例如arch/arm/mm/proc-arm926.S中,包括CPU ID和掩码等众多信息。该汇编函数读取CPU ID,进行掩码比对,如果内核中有CPU ID与之相等则判断为内核支持该CPU,否则停止运行。

3.2 __lookup_machine_type,,核对机器码;

我们在内核中make menuconfig选中一个具体型号的开发板,那么就会使能对应板子的.C文件,例如mach-smdk2416.c。该文件中会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义开发板相关的一些属性及函数,包括机器类型ID。

我们可以在arch\arm\tools\mach-types.h中仿照smdk2416建立一个自己的开发板型号XXX:

注意该函数只是比对数字,而不是名字。所以我们可以把自己的板子的number改为SMDK2416的。

//machine_is_xxx	CONFIG_xxxx			MACH_TYPE_xxx		number
smdk2416 MACH_SMDK2416 SMDK2416 1685

插曲:关于Kconfig和Makefile

如果想使能一个需要在menuconfig中选择的宏定义选项,CONFIG_XXX,则需要修改对应的Kconfig文件;

如果想使能一个文件夹或者.c、.s文件,则需要修改对应的Makefile;

如何修改?照着别人的改就行了,很容易看会。

插曲结束

新建(copy)一个自己的板子文件arch\arm\mach-myboard\mach-myboard.c文件,我们进行如下开发板信息定义:

MACHINE_START(XXX, "XXXX")
//.phys_io = S3C2410_PA_UART, //未用,注释掉
//.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, //未用,注释掉
.boot_params = 0xX0000100, // uboot传来的参数的RAM地址
.init_irq = xxx_init_irq, //中断控制器初始化
.map_io = xxx_map_io, //内存重映射,以及部分硬件参数初始化
//.fixup = xxx_fixup, //内存映射会处理该地址范围,这里将它注释掉
.init_machine = xxx_machine_init,//部分初始化函数
.timer = &s3c24xx_timer, //timer初始化,给系统提供心跳时钟
MACHINE_END

做好以上工作后__lookup_machine_type汇编函数就能将uboot传来的机器码与内核支持的机器码比对,一致才能正常运行,否则停止运行。

3.3__create_page_tables:

因为汇编阶段会执行__turn_mmu_on打开MMU,所以必须进行部分地址映射。建立一个临时的page table(将来这个table会被清除,重新建立)。

内存的映射是根据我们设置的PHYS_OFFSET由内核完成的,如果想寄存器映射,得自己加汇编代码。

我在该函数后加入串口寄存器映射,这样开启MMU后用虚拟地址也能进行串口打印信息:

add	r0, r4, #0xXX000000 >> 18
orr r3, r7, #0xXX000000
str r3, [r0]

3.4 arch\arm\kernel\head-common.S

b start_kernel

从这里跳转到C语言启动函数start_kernel();

四、 Linux内核启动之C语言阶段

汇编语言跳到init\main.c中的asmlinkage void __init start_kernel(void)函数,asmlinkage是一个宏定义,主要是声明这个函数是给汇编代码调用的。

该阶段执行众多初始化函数,根据CPU架构和板级型号不同,又会调用众多板级相关文件,直至完成文件系统启动前的所有工作。

start_kernel-->rest_init-->kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND)-->init-->

init_post-->run_init_process(execute_command)
//execute_command=/linuxrc,由uboot指定。

还需屏蔽include/asm-arm/arch-s3c2410/system.h的s3c24xx_default_idle()

因为板子寄存器地址不同,这里需要把该函数注释掉,不然内核运行崩溃。

内核会依次调用我们在.c文件中:

MACHINE_START(XXX, "XXXX")
.xx
.xx
.xx

中建立的初始化函数,马上讲解。

五、板级重要函数修改

arch\arm\mach-xxx\mach-xxx.c

板级相关.c文件,把裸机代码中通用的c文件尽量都移植到此处。

include/asm-arm/plat-s3c24xx/xxx.h

板级相关.h文件,其它文件需要引用板级资源时,需要包含该头文件,注意和实际的绝对路径名不同(Linux特殊性):

#include <asm/plat-s3c24xx/xxx.h>

mach-xxx.c文件包含以下重要的板级相关宏定义:

MACHINE_START(XXX, "XXX")
...
MACHINE_END

MACHINE_START、MACHINE_END都是定义的宏,代码如下

#define MACHINE_START(_type,_name)			\
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name, #define MACHINE_END \
};

简化展开一下,就是定义了一个如下结构体:

	static const struct machine_desc = {
.nr = MACH_TYPE_ xXX,
.name = xXX,
.boot_params = 0xx0000100,
.init_irq = xxx_init_irq, //中断控制器初始化
.map_io = xxx_map_io,
.init_machine = xxx_machine_init,
.timer = &s3c24xx_timer,
}

5.1 MMU映射函数.map_io

这是该结构体中最先执行的成员函数。作用主要是MMU映射;部分硬件初始化,例如串口,也可以放在该成员函数中。

static void __init xxx_map_io(void)
{
xxx_init_io();//MMU映射
}

arch\arm\plat-s3c24xx\cpu.c:

void xxx_init_io(void)
{
iotable_init(xxx_iodesc, ARRAY_SIZE(xxx_iodesc));
} static struct map_desc xxx_iodesc[] __initdata = {
{
.virtual = (0xF0100000),
.pfn = __phys_to_pfn(0xXX000000),
.length = 0x00100000,
.type = MT_DEVICE,
},
{
.virtual = (0xF0200000),
.pfn = __phys_to_pfn(0xXX000000),
.length = 0x00100000,
.type = MT_DEVICE,
},
};

asm-arm\arch-s3c2410\memory.h

#define PHYS_OFFSET	UL(0xX0000000)  //很重要,RAM的基地址,内核根据该值映射RAM地址

Linux内核汇编结束后就已经开启了MMU功能,后面使用的所有地址都必须是映射过的。内核会自动完成两部分的映射,一是RAM地址PHYS_OFFSET,即将0xX0000000映射到0xC0000000,大小我们可以在bootargs的mem=XX M指定。第二个是中断向量表0xX0001000映射到0xFFFF0000,大小为0x00001000。因为ARM中断向量表可以存放于两个地址:0和0xFFFF0000。可通过设置ARM寄存器设置用哪个地址的中断向量表。这两部分映射是内核隐式完成,我们只需指定RAM起始地址和要使用的大小。

内核中映射有两种方式。

静态映射:将指定虚拟地址映射到物理地址,虚拟地址是已知的、固定的;

动态映射:ioremap等函数实现,映射的虚拟地址是随机的。因为我们操作寄存器时有一些宏定义操作,地址已知更方便编程,所以采用静态映射。

而struct map_desc xxx_iodesc[]里就是我们指定的静态映射数组,主要映射了串口寄存器等。

.virtual是要映射的虚拟地址,地址范围涉及到内核空间分布知识,IO寄存器映射一般映射到0xF0000000到0xFF000000

.pfn是要映射的物理地址。

.length是要映射的地址长度。

.type是要映射的地址类型,不同类型的权限是不同的,需要按照地址的属性正确设置,否则会带来意料之外的bug

arch\arm\mm\mmu.c :该文件不用改,但调试的时候可能需要用到如下两个函数。

static inline void prepare_page_table(struct meminfo *mi)
{
/* Clear out all the mappings below the kernel image.*/
for (addr = 0; addr < MODULE_START; addr += PGDIR_SIZE)
pmd_clear(pmd_off_k(addr));
}
#define PAGE_OFFSET UL(0xc0000000)
#define MODULE_END (PAGE_OFFSET)
#define MODULE_START (MODULE_END - 16*1048576)

该函数可能会清除我们在汇编语言建立好的映射,所以在该函数之后、xxx_map_io之前,注意虚拟地址的使用问题。

void __init create_mapping(struct map_desc *md)
{
printk(KERN_INFO "map PHYS:0x%08llx ,VIRT:0x%08lx ,length:0x%08lx\n",
__pfn_to_phys((u64)md->pfn), md->virtual, md->length);
}

所有的映射都会走该程序,所以我们加个如上打印,显示映射的物理地址、虚拟地址、映射长度,防止映射有冲突。

5.2、中断控制器移植.init_irq

我的中断控制器不一样,也得自己移植:中断控制器寄存器初始化;对中断描述符进行设置;对s3c_irq_ack、s3c_irq_mask、s3c_irq_unmask进行初始化。

void __init xx_init_irq(void)
{
int irq;
intr_init();//中断控制器初始化
for (irq = 0; irq < NR_IRQS; irq++) {
set_irq_chip(irq, &s3c_irq_level_chip);
set_irq_handler(irq, handle_level_irq);
set_irq_flags(irq, IRQF_VALID);
}
}

intr_init()函数对中断控制器进行初始化,主要是进行寄存器设置。和裸机基本一致,但是中断函数注册和中断处理流程需要用内核专用函数完成。

set_irq_chip(irq, &s3c_irq_level_chip)函数中,s3c_irq_level_chip就包含了s3c_irq_ack、s3c_irq_mask、s3c_irq_unmask三个处理函数。s3c_irq_ack作用为第一时间清除具体中断;s3c_irq_mask作用为向中断控制器mask具体中断,使对该中断的处理过程中,该中断不会再次发生;s3c_irq_unmask作用为向中断控制器unmask具体中断,使该中断可以再次触发。

5.3、 timer初始化.timer

很多实时操作系统都需要我们设置一个timer定时器,定时处理一个系统函数,给系统提供心跳时钟(嘀嗒时钟)。Linux也是如此,我们需要设置个timer定时器,每进一次timer中断处理一次timer_tick()函数,该函数一个最重要的工作就是给全局变量jiffies加一。Linux内核则根据jiffies的值可以知道系统相对运行时间。

struct sys_timer xx_timer = {
.init = xx_timer_init,
.offset = xx_gettimeoffset,
}; void xx_timer_init(void)
{
xx_timer_setup();
setup_irq(IRQ_TIMER_NUM, &xx_timer_irq);
}
static struct irqaction xx_timer_irq = {
.name = "Timer Tick",
.flags = IRQF_DISABLED | IRQF_TIMER,
.handler = xx_timer_interrupt,
}; static irqreturn_t xx_timer_interrupt(int irq, void *dev_id)
{
清中断;
timer_tick();//每次执行会让jiffies加一,内核依赖jiffies运行!!!
}

xx_timer_init函数的作用是进行timer定时器初始化、中断注册。一般可设为100ms或50ms触发一次timer中断,根据实际情况调整测试。

xx_gettimeoffset函数的作用是返回距离上次timer中断过了多少微秒。

include/asm-arm/param.h文件中有个HZ的宏定义,需要与我们timer定时中断使用的HZ匹配。因为内核需要根据HZ进行一些时间上的计时处理,不匹配会导致时间处理函数(例如判断超时时间)结果不准。

Linux移植到自己的开发板(二)UBOOT和Linux的更多相关文章

  1. Linux移植到自己的开发板(三)根文件系统

    @ 目录 1 Linux内核配置 2 ramdisk制作 3 busybox配置 4 genext2fs生成镜像 为了快速调试,采用ramdisk进行根文件系统测试.要使内核能挂载ramdisk根文件 ...

  2. 小白自制Linux开发板 二. u-boot移植

    上一篇:小白自制Linux开发板 一. 瞎抄原理图与乱画PCB  中我们做了一个小型而没用的开发板,用的是Licheepi Nano的镜像,那从本篇开始我们开始自己构建它的灵魂吧. 我们都知道,PC在 ...

  3. Linux移植到自己的开发板(一)环境搭建

    环境搭建 vmware:VMware Workstation 15 Pro Linux系统:Ubuntu16.04 x64 1. 在Windows系统安装VMware15软件: 2. 网上下载并解压u ...

  4. Linux移植到自己的开发板(四)问题汇总

    @ 目录 1 使ubuntu支持两个版本的编译链: 2 版本问题: 3 ubuntu版本的vscode下载网速太慢: 4 ubuntu占用空间过大 5 执行make zImage 出错 lzop: n ...

  5. 物联网操作系统HelloX已成功移植到MinnowBoard MAX开发板上

    在HelloX开发团队的努力下,以及Winzent Tech公司(总部在瑞典斯德哥尔摩)的支持下,HelloX最新版本V1.78已成功移植到MinnowBoard MAX开发板上.相关源代码已经发布到 ...

  6. live555 交叉编译移植到海思开发板

    本文章参考了.http://blog.csdn.net/lawishere/article/details/8182952,写了hi3518的配置说明.特此感谢 https://blog.csdn.n ...

  7. 移植Mplayer到OK6410开发板

    移植Mplayer到OK6410开发板 作者:vasage 项目需要,需要将Mplayer移植到开发板上,所以今天花了一下下午成功移植,其中参考很多文档,后发现许多文档陈旧,些许文档有少量错误,所以这 ...

  8. qemu 模拟-arm-mini2440开发板-启动u-boot,kernel和nfs文件系统

    qemu 本文介绍了如何编译u-boot.linux kernel,然后用qemu启动u-boot和linux kernel,达到与开发板上一样的学习效果! 虽然已经买了2440开发板,但是在实际学习 ...

  9. qemu 模拟-arm-mini2440开发板-启动u-boot,kernel和nfs文件系统【转】

    转自:http://www.cnblogs.com/riskyer/p/3366001.html qemu 本文介绍了如何编译u-boot.linux kernel,然后用qemu启动u-boot和l ...

随机推荐

  1. Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/zeppelin/server/ZeppelinServer : Unsupported major.minor version 52.0

    在启动Zeppelin时遇到了该问题: [root@quickstart bin]# ./zeppelin-daemon.sh restart Please specify HADOOP_CONF_D ...

  2. 流程控制( if while )

    目录 流程控制 必知必会 分支结构 if 1.单 if 分支结构 2. if与else连用 3. if, else和 elif if 判断之嵌套 if 练习题 while 循环 while+break ...

  3. MXNet学习:预测结果-识别单张图片

    用到了model里的FeedForward.load和predict import os import mxnet as mx import numpy as np import Image from ...

  4. Solution -「LOCAL」过河

    \(\mathcal{Description}\)   一段坐标轴 \([0,L]\),从 \(0\) 出发,每次可以 \(+a\) 或 \(-b\),但不能越出 \([0,L]\).求可达的整点数. ...

  5. linux下gdb如何处理coredump错误

    linux下gdb如何处理coredump错误 在编写C++程序中,我们经常会遇到一种错误,segment fault, 这种coredump错误 会导致程序运行时异常退出或者终止,这种错误没有明显错 ...

  6. suse 12 二进制部署 Kubernetets 1.19.7 - 第05章 - 部署kube-nginx

    文章目录 1.5.部署kube-nginx 1.5.0.下载nginx二进制文件 1.5.1.编译部署nginx 1.5.2.配置nginx.conf 1.5.3.配置nginx为systemctl管 ...

  7. Java基于ClassLoder/ InputStream 配合读取配置文件

    阅读java开源框架源码或者自己开发系统时配置文件是一个不能忽略的,在阅读开源代码的过程中尝尝困惑配置文件是如何被读取到内存中的.配置文件本身只是为系统运行提供参数的支持,个人阅读源码时重点不大可能放 ...

  8. 为什么使用Mybatis对JDBC进行包装探究

    一.原生JDBC在实际生产中使用存在的影响性能的问题 首先分析使用JDBC的代码: Connection connection = null; PreparedStatement preparedSt ...

  9. Nginx服务器SSL证书安装

    操作场景 本文档指导您如何在 Nginx 服务器中安装 SSL 证书. 说明: 本文档以证书名称 www.domain.com 为例. Nginx 版本以 nginx/1.16.0 为例. 当前服务器 ...

  10. 微服务从代码到k8s部署应有尽有系列(十四、部署环境搭建)

    我们用一个系列来讲解从需求到上线.从代码到k8s部署.从日志到监控等各个方面的微服务完整实践. 整个项目使用了go-zero开发的微服务,基本包含了go-zero以及相关go-zero作者开发的一些中 ...