系统移植笔记

本文最后更新于:2022年6月6日 晚上

课程内容 :

  1. 怎么移植一个系统到开发板,怎么编译系统内核、怎么制作设备树文件、怎么制作文件系统
  2. 制作bootloader(u_boot为例)

必须掌握以上的这些工程方法

PC 和 嵌入式设备 系统启动比较

PC机启动过程

  1. BIOS (Basic Inpu Output System) ( 硬件初始化 : 系统时钟,内存(SDRAM/DDR2/DDR3/DDR4),启动方式:USB 硬盘 光驱 …)
  2. 引导程序 (grub,lilo,…) =>加载操作系统
  3. OS (windows,Linux,Mac os)
  4. 挂载文件系统 ( NTFS, FAT(16/32), EXT(2,3,4) )
  5. 应用程序

嵌入式设备(exynos4412)

  1. iROM固化代码

    • 基本硬件初始化

    • 判别启动方式—(SD/USB/EMMC)

    • 读取存储介质一部分数据到 iRAM

  2. iRAM代码运行bootloader第一阶段

    • 初始化系统时钟
    • 初始化内存
    • 自搬移bootloader到内存
  3. 内存中运行bootloader第二阶段

    • 初始化基本的硬件设备 (串口,EMMC,SD)

    • 加载OS到内存

  4. 内存中运行操作系统 (Linux,windows CE,Mac OS)

  5. 挂载文件系统

  6. 运行应用程序

bootloader = BIOS + 引导程序

Item Description
iROM internal Read-Only Memory — 掉电后数据保留
iRAM internal Random-Access Memory — 掉电后数据丢失
LPDDR Low-Power Double Data Rate (SDRAM —- Synchronous Dynamic Random-Access Memory)
eMMC Embedded Multi Media Card

开发环境

  • 硬件:fs4412开发板 — Samsung Exynos芯片

  • PC机环境:Ubuntu 16.04

  • 交叉开发概念:

    在PC机完成代码的编写和编译,在开发板运行编译完成的程序

Linux 环境配置

在 Linux 上添加交叉开发工具链

  • 方式一:在当前终端添加环境变量
1
2
3
export PATH=$PATH:TOOL_CHAIN 
# TOOL_CHAIN 是交叉开发工具链的绝对路径
# 这种方式仅对当前终端有效
  • 方式二:添加环境变量到配置文件 (选择这种

    • /home/linux (用户名)/.bashrc —– (针对当前登录用户)
    • /etc/bash.bashrc —- (所有用户有效) (选这个
1
2
3
4
5
sudo vi /etc/bash.bashrc
# 在文件末尾添加
TOOL_CHAIN=/home/linux/fs4412/toolchain/gcc-4.6.4/bin
export PATH=$PATH:$TOOL_CHAIN
# TOOL_CHAIN 根据自己工具链文件的绝对路径,每个人不一样

如果是64bit 的Ubuntu系统,需要安装32bit运行库:

1
2
sudo apt install lib32ncurses5
sudo apt install lib32z1

Linux 服务配置

配置tftp服务 (文件传输)

  1. 安装软件包:

    1
    2
    sudo apt-get install tftp-hpa  (客户端程序)
    sudo apt-get install tftpd-hpa (服务端程序)
  2. 修改默认配置文件 /etc/default/tftp-hpa

    1
    2
    sudo vi /etc/default/tftp-hpa
    # 修改 TFTP_DIRECTORY="自己的tftpboot路径"
  3. 重启tftp 服务

    1
    2
    3
    sudo /etc/init.d/tftpd-hpa restart
    # 或者
    sudo service tftpd-hpa restart
  4. 测试:

    1
    2
    3
    tftp 127.0.0.1 (server IP)
    tftp> get (下载文件)
    tftp> quit

配置nfs (network file system)服务

  1. 安装软件包:

    1
    sudo apt-get install nfs-kernel-server
  2. 修改默认配置文件 /etc/exports

    1
    2
    3
    4
    5
    6
    7
    sudo vi /etc/exports
    # 添加自己电脑上需要共享的目录路径
    /home/linux/fs4412/rootfs *(rw,sync,no_root_squash)
    # *前面有空格,*后面部分代表目录的权限设置
    # rw : 读写权限
    # sync : 文件同步
    # no_root_squash : 不对root用户进行权限压缩
  3. 重新启动nfs服务

    1
    2
    3
    sudo /etc/init.d/nfs-kernel-server restart
    # 或者
    sudo service nfs-kernel-server restart
  4. 测试:

    1
    2
    3
    4
    5
    6
    7
    sudo mount IP:共享路径  自己的挂载点目录
    # e.g.
    sudo mount 127.0.0.1:/home/linux/fs4412/rootfs ./nfs
    # ./nfs (自己电脑上当前目录下的nfs子目录)

    sudo umount ./nfs
    # 撤销对nfs目录的挂载

开发板环境配置

u_boot 常用命令

  1. 设置开发板ip地址

    1
    2
    3
    setenv ipaddr [ip]
    print 查看板子环境信息
    save 记得保存
  2. 设置tftp 服务器的ip地址

    1
    setenv serverip [server IP] 
  3. 开发板和Ubuntu之间进行网络通信 (板子 Ping Ubuntu)

    <1>板子IP地址和Ubuntu的IP地址的网络号一样
    <2>虚拟机桥接模式
    <3>无线网卡禁用
    <4>防火墙关闭

  4. 用tftp服务,下载Linux 系统、设备树(硬件信息)、文件系统 到开发板上

    1
    2
    3
    tftp 41000000 uImage  
    tftp 42000000 exynos4412-fs4412.dtb
    tftp 43000000 ramdisk.img
  5. 启动内存

    1
    2
    bootm 内核在内存地址  ramdisk在内存地址  设备树在内存地址
    bootm 41000000 43000000 42000000

挂载文件系统

网络方式加载Linux内核(开发阶段)

  1. 查看已经设置的参数:

    1
    print
  2. 设置tftp 服务器端 ip 地址:

    1
    setenv serverip 192.168.154.178
  3. 设置开发班开启后自动执行的命令:

    1
    setenv bootcmd tftp 41000000 uImage\;tftp 42000000 exynos4412-fs4412.dtb\;bootm 41000000 - 42000000

    bootm 41000000 - 42000000 : 分别代表 Linux系统地址、文件系统地址(“-”:表示不在本地内存地址加载)、设备树地址

  4. 告诉操作系统系统启动后,挂载文件系统的方式 (网络方式挂载)

    1
    setenv bootargs root=/dev/nfs nfsroot=192.168.154.178:/home/linux/03.fs4412/rootfs,proto=tcp,nfsvers=3,nolock  rw console=ttySAC2,115200  init=/linuxrc  ip=192.168.154.126
  5. 保存设置好的环境变量

    1
    save

从EMMC加载内核和文件系统(产品发布阶段)

  1. 烧写内核镜像到EMMC上

    1
    2
    3
    4
    #下载到内存
    tftp 41000000 uImage
    #将内存的数据写到EMMC
    movi write kernel 41000000
  2. 烧写设备树文件到EMMC上

    1
    2
    tftp  41000000  exynos4412-fs4412.dtb  
    movi write dtb 41000000
  3. 烧写文件系统镜像到EMMC上

    1
    2
    tftp  41000000  ramdisk.img
    movi write rootfs 41000000 300000(大小)
  4. 设置启动参数

    1
    2
    setenv bootcmd movi read kernel 41000000\;movi read dtb 42000000\;movi read rootfs 43000000 300000\;bootm 41000000 43000000 42000000
    saveenv

编译Linux Kernel

Linux Kernel 目录结构 (记忆)

  1. arch 目录 — 存放架构相关代码

    1
    2
    3
    4
    arch/arm/boot/compressed #存放的是内核的自解压代码
    arch/arm/kernel/head.s #Linux内核的开始代码
    arch/arm/boot/dts #存放开发板相关的设备树文件
    arch/arm/configs #存放SOC芯片相关的内核配置文件
  2. driver 目录 — 设备驱动代码

  3. net 目录 — 网络协议栈的实现代码

  4. Makefile 文件 — 指定平台信息和交叉开发工具链

针对自己的开发板配置Linux内核

生成 uImage 文件

  1. 修改 Makefile (不推荐)

    1
    2
    ARCH  		?= arm
    CROSS_COMPILE ?= arm-none-linux-guneabi-
  2. 使用默认配置文件配置内核 (推荐)

    1
    make ARCH=arm exynos_defconfig 	(cp arch/arm/configs/exynos_defconfig  .config)
  3. 通过配置菜单配置内核

    1. 安装 libcurses 库

      1
      sudo apt-get install libncurses5-dev
    2. ```bash
      make ARCH=arm menuconfig

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14

      y : 选中编译进行内核 n : 不编译进行内核 m : 编译成模块(会编译但不连接进入内核)

      确认信息:

      > 1. 确认编译的内核是否是自己需要的
      > 2. 输出信息的串口

      注意保存

      4. 编译 Linux 内核

      ```bash
      make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-

    编译完后有四个文件需要注意:

    1. vmlinux (60M) (elf linux内核镜像)(存放在Linux内核的顶层目录)

    2. Image (5.5M) (去掉elf头和调试信息之后的Linux内核镜像) arch/arm/boot

    3. vmlinux (3.0M) (elf Linux内核镜像 : 自解压代码 + gzip压缩后的内核镜像) (arch/arm/boot/compressed )

    4. zImage (去掉elf头之后的Linux内核镜像 : 自解压代码 + gzip压缩后的内核镜像) arch/arm/boot

    arch/arm/boot: 2,4

  4. 生成uImage 格式 Linux 内核镜像

    1
    make ARCH=arm  CROSS_COMPILE=arm-none-linux-gnueabi- uImage

    如果提示:mkimage找不到

    1
    sudo apt install u-boot-tools 

完成以上步骤,我们将制作好的 uImage 放到开发板,仍然不能正常启动,因为板子没有设置网卡驱动,没有办法通过 nfs 服务挂载文件系统

设置网卡驱动

  1. 通过配置菜单,配置内核

    1
    make ARCH=arm menuconfig
  2. 选中以下项目 (前面有 * 号表示选中,Enter 进入子菜单,y 选中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [*] Networking support --->
    Networking options --->
    <*> Packet socket
    <*> Unix domain sockets
    [*] TCP/IP networking
    [*] IP: kernel level autoconfiguration
    Device Drivers --->
    [*] Network device support --->
    [*] Ethernet driver support (NEW) --->
    <*> DM9000 support
    File systems --->
    [*] Network File Systems (NEW) --->
    <*> NFS client support
    [*] NFS client support for NFS version 3
    [*] NFS client support for the NFSv3 ACL protocol extension
    [*] Root file system on NFS
  3. 修改文件 driver/clk/clk.c — 关闭时钟,不关闭网卡起不来

    1
    2
    3
    static bool clk_ignore_unused; 
    /*改为true*/
    static bool clk_ignore_unused = true;
  4. 重新编译 uImage

uImage 加载地址分析

  1. 命令:

    1
    2
    3
    4
    5
    6
    mkimage -l uImage
    => >读取uImage头的信息(判断uImage应该加载到内存的那个地址运行)

    bootm 0x4100,0000(ram_addr)

    uImage => head + zImage
  2. 头中记录的信息:

    • Load Address: 40008000 (期望加载的地址)
    • Entry Point: 40008000 / 40008040 (最终Linux运行的地址【PC寄存器记录的地址】)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <1>从这个地址中读取了64byte头信息
    <2>发现ram_addr 和 load_addr 不相等,此时会将41000000偏移64字节之后的zImage搬移到load address,然后将pc指向enty_point

    bootm 0x40008000(ram_addr)
    uImage => head + zImage
    <===============================================================================================>
    <1>从这个地址中读取64byte信息
    <2>发现ram_addr和load_addr 相等,此时会将pc指针指向entry point

    此时完蛋,因为0x40008000开始部分是64byte头信息数据,应该将pc指向 0x40008000 + 64byte头信息
  3. 总结:

    • 如果uImage头信息中load address 和 enty point相等,此时uImage不能加载到load address地址运行
    • 如果 uImage 头信息中load address 和 enty point不相等(偏移64byte),此时只能将uImage加载到 load address 运行。

go 命令 和 bootm 命令

Kconfig 和 Makefile

几个思考题:

1
2
3
4
5
6
7
8
思考:Makefile里面指定的文件是否参与编译,收谁的影响呢?
回答:.config里面配置宏会产生影响

思考:.config是如何生成的呢?
回答:make ARCH=arm menuconfig进行配置的时候生成

思考:menuconfig时候,菜单的信息来源于哪里呢?
回答:由Kconfig文件提供菜单信息

Kconfig 和 Makefile

  1. Makefile : 完成对文件或目录编译

    基本语法:

    1. 写死

      1
      obj-y += dir/ 或 obj-y += file.o

      表示对应目录需要编译进内核或指定的文件需要编译进内核

    2. 根据宏指定

      1
      obj-$(CONFIG_XXX) += dir/ 或 obj-$(CONFIG_XXX) += file.o

      表示对应目录或文件是否需要编译进内核,取决于CONFIG_XXX宏的定义,也就是在 .config 中是否有这个

      宏的定义:
      CONFIG_XXX=y

    3. 问题:

      1
      2
      问题:.config 是什么文件呢?
      回答:内核的配置文件
  2. Kconfig : 提供内核的配置菜单选项

    1
    2
    3
    4
    5
    6
    问:Linux源码中的文件是如何编译进内核的?
    答:(1)make menuconfig
    Kconfig为它提供菜单信息配置完以后,会将配置信息写入.config (例如:CONFIG_XXXX=y)

    (2).config文件会被Makefile使用,Makefile将会根据.config
    文件中CONFIG_XXX来决定对应的文件是否需要编译进内核
  3. Makefile 和 Kconfig 都是通过分级管理代码编译

  4. Makefile 是相对路径,Kconfig 是绝对路径

Kconfig 语法

1
2
3
4
5
# 配置选项
config 选项名
属性1
属性2
...
  1. 选项名

    • config HELLO

    展现的形式:CONFIG_HELLO,在.config文件中定义

  2. 属性

    1. 选择类型

      1
      2
      3
      4
      5
      <1> tristate(三态)   y:编译进内核  m:编译成模块  n:不编译   < >
      <2> bool y:编译进内核 n:不编译 [ ]
      <3> string CONFIG_选项名="字符串" ()
      <4> int CONFIG_选项名=整数
      <5> hex CONFIG_选项名=十六进制数
    2. 提示字符串

      prompt “提示字符串” (配置菜单中显示)

    3. range 指定值的范围

    4. help 帮助信息

      1
      2
      help 
      "test help ..."
    5. default 默认设置

      1
      2
      3
      4
      config HELLO 
      tristate
      prompt "hello support"
      default y
    6. depends on 配置选项名

      1
      2
      3
      4
      5
      6
      7
      8
      9
      [1]depends on 配置选项名 
      [2]depends on 配置选项名1(m) || 配置选项名2(y)
      [3]depends on 配置选项名1(y) && 配置选项名3(m)

      y:2 m:1 n:0
      && -> 最小值
      || -> 最大值

      注意:如果依赖的结果为0:不可见, 2:三态 , 1:两态
    7. select 配置选项名

      当前配置选项被选中的时候,同时选择select 指定的配置型选项

    8. source 路径/Kconfig

      将这个路径下的Kconfig文件包含进来

    注意:1. 选择类型 2. 提示字符串 必须有,其他的是可选项

如何在Linux 内核中添加自己的代码

  1. 把自己的代码拷贝到内核源码树下

  2. 编写一个自己的Makefile和 Kconfig (如果目录下已经有Makefile和Kconfig文件,则直接修改他们)

    1
    2
    # Makefile:
    obj-$(CONFIG_XXX) += file.o
    1
    2
    3
    # Kconfig:
    config XXX
    tristate "....."
  3. 在它的上一层目录下,修改Makefile和Kconfig (可选)

    注意 : 如果是直接修改 Makefile 和 Kconfig,则不需要第三步

  4. make menuconfig

    选中我们的配置选项

  5. 重新编译内核

制作设备树文件

  1. 进入 arch/arm/boot/dts/ 目录

    1
    cp exynos4412-origen.dts exynos4412-fs4412.dts
  2. 修改 添加要编译的设备树文件 ,在Makefile中 (在arch/arm/boot/dts/ 下的Makefile)

    1
    2
    # 在exynos4412-origen.dtb \ 后添加
    exynos4412-fs4412.dtb \
  3. 修改 exynos4412-fs4412.dts 添加:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    srom-cs1@5000000 {
    compatible = "simple-bus";
    #address-cells = <1>;
    #size-cells = <1>;
    reg = <0x5000000 0x1000000>;
    ranges;
    ethernet@5000000 {
    compatible = "davicom,dm9000";
    reg = <0x5000000 0x2 0x5000004 0x2>;
    interrupt-parent = <&gpx0>;
    interrupts = <6 4>;
    davicom,no-eeprom;
    mac-address = [00 0a 2d a6 55 a2];
    };
    };
  4. 重新编译 设备树文件

    1
    2
    make ARCH=arm  CROSS_COMPILE=arm-none-linux-gnueabi- dtbs
    # 在Linux Kernel 顶级目录下执行

制作文件系统

Linux 解压后的启动流程

  1. 启动 bootloader
  2. 建立映射表,使能 MMU
  3. 清除 bss段,设置sp
  4. 跳转到C语言部分执行,启动内核

u_boot 的参数传递

u_boot 和 Linux 内核之间的参数传递

  1. r0 : 0
  2. r1 : 开发板ID
  3. r2 : 其他参数在内存中的开始地址

r1 的参数,让Linux内核自己去判断,它是否支持当前的开发板

r2 的参数,是其他参数在内存中的开始地址

r2所保存的地址中,存放哪些信息?

  1. 内存的信息
    内存的开始地址和内存的大小

  2. 命令行参数:bootargs的内容
    告诉Linux内核启动后,挂载文件系统的方式(例如:NFS方式)

  3. 其他信息

Linux内核怎么知道访问的参数在哪结束?

问:u_boot引导Linux内核启动的时候,需要传递一些参数告诉Linux内核当前环境, 但是在传递参数的时候,只是告诉了参数所在内存的开始地址,并没有告诉Linux内核,可以访问的内存的大小,Linux内核它是如何知道它访问的参数结束的地方在那里?

答:u_boot和Linux之间在传递参数的时候,是按照双方约定的格式传递的

目前Linux内核支持哪些格式的传递呢?

  1. 通过struct parm_struct结构体来传递 (Linux 2.6之前使用的方式,已经过时,不便于扩展)

  2. 通过tag列表来传递 (Linux 2.6开始使用的方式)

    一个tag节点 : tag头 + 大小 + 内容

  3. 通过设备树来传递(Linux 3.0开始使用的方式)

    1. u_boot 修改了设备树的节点(bootargs参数节点和内存信息节点),把需要传递的信息放在设备树中。
    2. 在r2寄存器中记录了设备树在内存中的地址。

注意:

  1. 在Linux 3.0之后的内核,这些方式都支持。
  2. 可以参看u-boot-2013-learn/arch/arm/lib/bootm.c文件了解参数传递过程

系统启动时如何自动执行自己的程序

  1. 在bootargs参数中指定自己的程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    init=/linuxrc 默认的写法
    init=我们自己程序的路径

    例如:
    我们需要执行的程序是hello,它的路径是在/test/hello

    init=/test/hello

    缺点:
    没有启动init进程
  2. 在/etc/inittab文件中,添加自己的程序

    1
    2
    3
    4
    5
    6
    默认启动的是init进程,init进程启动的过程中,读取/etc/inittab文件,并且将这个文件指定的程序启动起来。所以我们可以在这个文件中添加自己程序。

    例如:
    我们需要执行的程序是hello,它的路径是在/test/hello,并且我们这个程序不能被杀死

    ::respawn:/test/hello
  3. 在/etc/init.d/rcS的脚本 ,添加自己的程序

    1
    2
    3
    init进程启动的时候,会执行/etc/init.d/rcS的脚本,我们可以在这个脚本中添加自己的程序。
    例如:
    /test/hello 或 /test/hello & (后台运行)
  4. 在/etc/profile文件中,添加自己的程序

    1
    2
    3
    4
    init进程启动的时候,会执行/bin/sh,是一个shell程序,shell程序执行的时候,会执行/etc/profile文件,这是一个脚本文件,所以我们在脚本中添加自己的程序。

    例如:
    /test/hello 或 /test/hello & (后台运行)

注意:

很多时候我们会选择在 2 或 4 中添加

2 可以让自己的进程死掉后,重新启动

4 可以让自己的程序在执行的时候,执行的环境比较好

制作根文件系统 — 以 busybox 为例

步骤

  1. 编译 busybox

    1. 下载源码:busybox-1.22.1.tar.bz2

    2. 解压源码: tar -xvf busybox-1.22.1.tar.bz2

    3. 进入源码目录 cd busybox-1.22.1

    4. 配置源码:

      1
      2
      3
      4
      5
      6
      7
      8
      $ make menuconfig
      Busybox Settings --->
      Build Options --->
      [*] Build BusyBox as a static binary (no shared libs)
      [ ] Force NOMMU build
      [ ] Build with Large File Support (for accessing files > 2 GB)
      (arm-none-linux-gnueabi-) Cross Compiler prefix
      () Additional CFLAGS
  2. 安装 make install

  3. 创建需要的其他目录

    1. 安装完成后进入 _install 目录

    2. 创建其他需要的目录

      1
      mkdir dev etc mnt proc var tmp sys root
  4. 添加库文件

    1. 寻找编译器路径 :

      1
      2
      3
      which arm-none-linux-gnueabi-gcc

      # /home/linux/03.fs4412/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-gcc

      进入 该路径下的 bin 目录发现都是编译器的文件

      我们再退到上一级目录,即 /home/linux/03.fs4412/toolchain/gcc-4.6.4/ 下搜寻

    2. 搜寻 共享库文件

      1
      find -name *.so

      注意我们要的是系统的动态库文件, 最后的路径是:

      1
      /home/linux/03.fs4412/toolchain/gcc-4.6.4/arm-arm1176jzfssf-linux-gnueabi/sysroot/lib
    3. lib 目录 拷贝到 _install 目录下 注意:拷贝整个目录 结尾加 -a 参数

    4. 删除静态库 和 共享库文件中的符号表 — 确保 lib 目录大小不超过 8M

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      cd _install/lib
      # 删除静态库
      sudo rm -rf *.a
      # 删除所有的符号表
      arm-none-linux-guneabi-strip *
      # strip 命令 : 从特定文件中剥掉一些符号信息和调试信息,使文件变小。

      # 这里会出现权限不够,或者是找不到命令的错误,如果出现上述错误则 :
      which arm-none-linux-guneabi-strip
      # 复制路径 如:
      # /home/linux/03.fs4412/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-strip
      # sudo 刚刚复制的路径 *
      sudo /home/linux/03.fs4412/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi-strip *

用到的一些 Linux 命令

  • which xxx :定位文件的路径
  • find -name xxx :按名字搜索文件
  • nm libc-2.17.so | grep printf : 搜寻 c 库中,printf 的符号
  • du -mh xxx :显示xxx 文件的大小

添加配置文件

  1. 在 etc 下添加文件 inittab,文件内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #this is run first except when booting in single-user mode.
    ::sysinit:/etc/init.d/rcS
    # /bin/sh invocations on selected ttys
    # start an "askfirst" shell on the console (whatever that may be)
    ::askfirst:-/bin/sh
    # stuff to do when restarting the init process
    ::restart:/sbin/init
    # stuff to do before rebooting
    ::ctrlaltdel:/sbin/reboot
  2. 在 etc 下添加文件 fstab,文件内容如下:

    1
    2
    3
    4
    5
    #device mount-point type options dump fsck order
    proc /proc proc defaults 0 0
    tmpfs /tmp tmpfs defaults 0 0
    sysfs /sys sysfs defaults 0 0
    tmpfs /dev tmpfs defaults 0 0
  3. 在 etc 下创建 init.d 目录,并在 init.d 下创建 rcS 文件,rcS 文件内容为:

    1
    2
    3
    4
    5
    #!/bin/sh
    # This is the first script called by init process
    /bin/mount -a
    echo /sbin/mdev > /proc/sys/kernel/hotplug
    /sbin/mdev -s

    为 rcS 添加可执行权限:

    1
    2
    3
    chmod +x init.d/rcS
    # 或者
    chmod 777 init.d/rcS
  4. 在 etc 下添加 profile 文件,文件内容为:

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/sh
    export HOSTNAME=farsight
    export USER=root
    export HOME=root
    export PS1="[$USER@$HOSTNAME \W]\# "
    PATH=/bin:/sbin:/usr/bin:/usr/sbin
    LD_LIBRARY_PATH=/lib:/usr/lib:$LD_LIBRARY_PATH
    export PATH LD_LIBRARY_PATH

制作 ramdisk 文件系统

  1. 制作一个大小为 8M 的镜像文件

    1
    2
    $ cd ~
    $ dd if=/dev/zero of=ramdisk bs=1k count=8192 (ramdisk 为 8M)
  2. 格式化这个镜像文件为 ext2

    1
    $ mkfs.ext2 -F ramdisk
  3. 创建 initrd 目录作为挂载点

    1
    $ sudo mkdir initrd
  4. 将这个磁盘镜像文件挂载 initrd/ 下

    1
    $ sudo mount -t ext2 ramdisk ./initrd
  5. 将我们的文件系统复制到 ramdisk 中

    1
    sudo cp /source/rootfs/* ./initrd –a
  6. 卸载 initrd

    1
    $ sudo umount ./initrd
  7. 压缩 ramdisk 为 ramdisk.gz

    1
    $ gzip --best -c ramdisk > ramdisk.gz
  8. 格式化为 uboot 识别的格式 复制到 tftpboot

    1
    2
    3
    $ mkimage -n "ramdisk" -A arm -O linux -T ramdisk -C gzip -d ramdisk.gz ramdisk.img

    $ cp ramdisk.img /tftpboot
  9. 在 U-BOOT 命令行重新设置启动参数 bootcmd

  10. 配置内核支持RAMDISK (重要)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    make menuconfig
    File systems --->
    <*> Second extended fs support
    Device Drivers
    SCSI device support --->
    <*> SCSI disk support
    Block devices --->
    <*>RAM block device support
    (16)Default number of RAM disks
    (8192) Default RAM disk size (kbytes) (修改为 8M)
    General setup --->
    [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

    重新编译内核,复制到 /tftpboot

制作u_boot

前期准备

开发平台

  1. CPU核:

    ARM cortex-A9 armv7指令集

  2. SOC:

    Samsung exynos4412 4个cortex-A9核

  3. board:

    fs4412 —–> 参照三星的母板进行设计

u_boot 的目录结构

Directory Description
board/ 存放是特定开发板相关的代码,一般以芯片厂家的名字命名
arch/ 跟CPU架构相关的代码
include/ 存放的是uboot相关代码需要头文件
net/ uboot支持的一些网络协议
lib/ 多有平台通用的代码
common/ uboot支持的命令实现代码
driver/ uboot支持的一些硬件驱动代码
boards.cfg u-boot 支持的板子的配置信息

通过 tags 文件阅读源码

  1. tags 文件是通过命令: ctags -R 生成索引文件

    源代码目录下所有的结构体定义,函数的定义,宏定义都可以通过索引文件找到它所在的路径文件

  2. 使用方法:

    ctrl + ] 跳转到指定符号所定义的文件
    ctrl + t 返回

  3. 路径配置

    必须让vi编辑器能寻找到tags文件

    vi 默认在当前目录下寻找 tags 文件,需要添加路径方可在任意路径访问

    修改用户主目录下的.vimrc文件,添加自己tags文件路径,这样vi编辑器就可以找到我们的tags啦

    1
    set tags+=/home/linux/03.fs4412/bootloader/u-boot-2013-learn/tags
  4. 通过grep/find命令搜索我们的符号所定义的地方

    在源码的顶层目录执行如下命令:

    1
    2
    grep "符号名"  * -n -R
    find -name 文件名

u-boot 启动流程 (重点)

案例:fs4412开发板

1
2
3
4
硬件信息
SOC : 三星 exynos4412 (4个cortex-A9 ARM核) 主频 1.4GHZ
board : origen<->fs4412
u-boot版本 : u-boot-2013.01

1. fs4412启动

  1. exyxnos4412 内部固化的irom代码先运行

    1. 确定启动的设备
    2. 初始化启动的设备
    3. 将其懂设备一开始的一部分代码 (BL1)搬移到 exynos4412 内部的 iram(SRAM)运行

    注意:BL1 代码是三星提供的,irom的代码在读取BL1的时候,会对它做验证,如果验证通过则运行它。

  2. 在iram 运行BL1代码

    1. 确定启动设备
    2. 将启动设备中的 BL2 代码拷贝到 exynos4412 内部的 iram 运行

    注意:BL2指的的是u_boot的第一阶段代码

2. u-boot 代码开始运行

  1. u-boot 第一阶段 (BL2 代码):

    1. 设置ARM核为SVC模式
    2. 设置异常向量表,告诉ARM核异常向量表基地址
    3. 关闭Cache和MMU
    4. 初始化系统时钟
    5. 初始化内存
    6. 将u_boot(第二阶段)从存储介质中搬移到内存中(board_init_f)
  2. u-boot 第二阶段 (BL3 代码):

    1. 设置ARM核为SVC模式

    2. 设置异常向量表,告诉ARM核异常向量表基地址

    3. 关闭Cache和MMU

    4. ….(此时u_boot已经运行在内存,不需要初始化系统时钟和内存

    1. _main

      1. 设置 sp
      2. 调用了 board_init_f
    2. board_init_f (板子第一阶段初始化)

      1. 完成基本的初始化

        • SOC ID的获取
        • 定时器初始化
        • 串口的初始化
        • 从串口中输出了一些信息
      2. 为 u_boot 重定向做准备

        在内存最顶端划分空间

    3. 重定向 u_boot

      如果 u_boot 不是在顶端内存,则将 u_boot 从底端内存搬移到顶端内存

    4. 清除 BSS 段

      为运行C语言程序做准备

    5. board_init_r (板子第二阶段初始化)

      1. 初始化硬件设备

        • EMMC存储器
        • DM9000网卡设备
        • …….
      2. 从存储介质中读取了环境变量的值

    6. main_loop

      • 获取了bootdelay和bootcmd环境变量的值,如果没有设置bootcmd则进入u_boot交互界面。
      • 如果设置了bootcmd,判断在到计时时间内,如果用户没有打断,则执行bootcmd命令。
      • 如果倒计时时间内,被用户打断,则进入u_boot交互界面。

      注意:u_boot交互界面,循环读取用户输入的命令,然后执行。

u-boot 移植 (重点)

u-boot 移植的核心思想

  1. 厂家直接提供u-boot -> 烧写 或者 修改(增加新功能) 或 u-boot 版本升级
  2. 芯片公司,让u-boot支持公司的芯片 ,自己仿照别的厂家,添加自己开发板相关代码
  3. 芯片公司根据自己的芯片编写bootloader,仿照u-boot设计思想

需要掌握的核心知识点:

  1. 熟悉SOC芯片和u-boot启动流程

  2. 系统时钟,内存是否能正常初始化

  3. 搬移u-boot到内存过程 (从存储介质中读取数据,然后写到内存)

  4. 对存储介质(emmc)如何进行读写

u-boot 移植

在u-boot 源码中找到和自己开发板类似的板子,基于该开发板做移植

  1. 芯片厂家 && 芯片型号

    1
    2
    3
    4
    5
    samsung   exynos4412    ===> 三星母板 : smdk4412 (orgien)

    在u-boot中没有找到 母板(芯片厂家做出一款芯片后,设计的第一块开发板)
    <1>查阅一下芯片厂家给予这个芯片是否做了其他母板
    <2>是不是当前u-boot版本中,芯片厂家还没有将自己的母板代码添加进来 ----> 去新版本的u-boot中寻找
  2. boards.cfg 文件中选择和自己开发板相近的参考板进行编译

    1. 针对自己的开发板配置u-boot : make 开发板名_config

      1
      2
      # 例如(fs4412 <-----> origen):
      make origen_config ----------> include/config.mk
    2. 编译

      1
      2
      make CROSS_COMPILE=arm-none-linux-gnueabi-
      # 生成u-boot.bin适用于origen ,移植的思想:修改origen的开发板的uboot代码
  3. u-boot.bin 下载到内存运行

    下载的地址 : 编译u_boot的时候指定的代码运行地址(CONFIG_SYS_TEXT_BASE)

  4. 运行内存中的 u-boot.bin

    go u_boot所在内存的地址

  5. 如果通过 go 命令启动 u-boot 的时候,没有任何输出效果?

    1. u-boot在内存中运行的时候,重新初始化了系统时钟和内存?
    2. 没有初始化串口?
    3. 指定内存基地址不对?
    4. ? (点灯大法:通过在一些位置加亮灯代码,确定导致u-boot死掉的代码)
    5. TrustZone代码有问题?

    注意:

    1
    2
    3
    在u-boot-2013.01 for fs4412上面,是由于TruszZone的代码有问题,我们只需要将 
    board/samsung/origen/lowlevel_init.S
    文件中的bl tzpc_init代码注释掉就可以了
    1
    2
    3
    4
    5
    - "u-boot.bin"   is a raw binary image (bootloader第二阶段代码)
    - "u-boot" is an image in ELF binary format
    - "u-boot.srec" is in Motorola S-Record format
    - "u-boot.map" 记录了生成u-boot代码所需要文件的路径
    - "System.map" 记录了u-boot中标签名和函数名对应的地址

DM9000 网卡移植

通信方式 :

There are only two addressing ports through the access of the host interface. One port is the INDEX port and the other is the DATA port. The INDEX port is decoded by the pin CMD =0 and the DATA port by the pin CMD =1.

The contents of the INDEX port are the register address of the DATA port. Before the access of any register, the address of the register must be saved in the INDEX port.

主要通过别人已经实现的代码,去了解DM9000的使用方法,根据自己的需求进行修改

对于驱动一个硬件设备的时候,需要做的事情

  1. 了解硬件设备的背景知识 (网上查资料或datasheet)

    硬件功能,特性,参数,硬件工作原理

  2. 阅读硬件原理图 : 硬件接到了那些CPU管脚

  3. 从SOC芯片手册中,找到控制管脚的GPIO控制器(需要初始化GPIO管脚)

  4. 从SOC芯片手册中,找到控制设备的控制器(需要初始化控制器)

  5. 有些设备对他进行控制的时候,是通过操作它内部的寄存器完成控制

    此时,我们阅读datasheet,查看如何操作芯片内部的寄存器

一些工程小方法

  1. 编写脚本执行编译命令

    1
    2
    make CROSS_COMPILE=arm-none-linux-guneabi-
    # 编译u-boot
  2. 屏蔽非报错信息

    1
    2
    3
    make CROSS_COMPILE=arm-none-linux-guneabi- 1>/dev/null

    # 将标准输出信息重定向到 /dev/null 目录下,就是丢弃标准输出信息,只输出错误信息
  3. 学会 “抄”别人的实现代码


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!