移植UBoot

嵌入式Linux系统移植之移植U-Boot

《嵌入式Linux应用完全开发手册》第3篇第15章总结归纳

本章目标

  1. 了解BootLoader的作用及工作流程
  2. 了解U-Boot的代码结构、编译过程
  3. 移植U-Boot
  4. 掌握常用的U-Boot命令

BootLoader简介

BootLoader的概念

BootLoader的引入

从前面的篇章可以知道,系统上电之后,需要一段程序来进行初始化:关闭WATCHDOG、改变系统时钟、初始化存储控制器、将更多的代码复制到内存中等。如果它能将操作系统内核复制到内存中运行,无论是从本地(Flash)还是远端(网络),那么就称这段程序为BootLoader。
简单来说,BootLoader就是这么一段程序,它在系统上电时运行,初始化硬件设备,准备好软件环境,最后调用操作系统内核。
可以增强BootLoader的功能,比如增加网络功能、从PC上通过串口或网络下载文件、烧写文件、将Flash上压缩的文件解压后再运行等,这就是一个功能更为强大的BootLoader,也称为Monitor。实际上,最终产品并不需要这些功能,它们只是为了方便开发。
BootLoader的实现非常依赖于具体硬件,在嵌入式系统中硬件的配置千差万别,即使CPU相同,它们的外设也可能不同,所以不可能有一个BootLoader支持所有的CPU、所有的电路板卡。即使支持CPU架构较多的U-Boot,也不是拿来就可以使用的,需要进行一些移植。

BootLoader的启动方式

CPU上电后,会从某个地址开始执行。比如MIPS架构的CPU会从0xBFC00000取第一条指令,而ARM架构的CPU则会从地址0x00000000开始。嵌入式开发板中,需要把存储器件ROM或Flash等映射到这个地址,BootLoader就存放在这个地址开始处,这样一上电就可以执行。
在开发时,通常需要使用各种命令操作BootLoader,一般通过串口来连接PC和开发板,可以在串口上输入各种命令、观察运行结果等。这也只是对开发人员才有意义,用户使用产品时是不需要通过串口来控制BootLoader的。BootLoader可以分为以下两种操作模式。

  1. 启动加载模式(Boot loading)
    上电后,BootLoader从板子上的某个固态存储设备上将操作相同加载到RAM中运行,整个过程没有用户的介入。产品发布时,BootLoader工作在这种模式下。
  2. 下载模式(Downloading)
    在这种模式下,开发人员可以使用各种命令,通过串口连接或网络连接等通信手段从主机(Host)下载文件(内核映像、文件系统映像),将它们直接放在内存运行或是烧入Flash。
    板子与主机间传输文件时,可以使用串口的xmodem、ymodem、zmodem协议,它们使用简单,只是速度较慢;可以使用nfs、tftp协议传输,主机需要开启nfs、tftp服务;也可以使用USB。
    像Blob或U-Boot等这样功能强大的BootLoader通常支持者两种工作模式,而且运行用户在这两种模式之间切换。比如,U-Boot在启动时处于正常的启动加载模式,但是它会延时若干秒(可设置),等待终端用户按下任意键,而将U-Boot切换到下载模式。如果指定时间内没有用户按键,则U-Boot继续启动Linux内核。

BootLoader的结构和启动过程

概述

在移植之前先了解BootLoader的一些通用概念,对理解它的代码会有所帮助。
嵌入式Linux系统从软件的角度通常可以分为以下4个层次。

  1. 引导加载程序,包括固化在固件中的boot代码和BootLoader两大部分。
    有些CPU在运行BootLoader之前先运行一段固化的程序,比如x86结构的CPU就是先运行BIOS中的固件,然后才运行硬盘的第一个分区(MBR)中的BootLoader。在大多数嵌入式系统中并没有固件,BootLoader是上电后执行的第一个程序。
  2. Linux内核
    特定于嵌入式板子的定制内核以及内核的启动参数。内核的启动参数可以是内核默认的,或是由BootLoader传递给它的。
  3. 文件系统
    包括根文件系统和建立于Flash内存设备之上的文件系统。里面包含了Linux系统能够运行所必需的应用程序、库等,比如可以给用户提供操作Linux的控制界面Shell程序、动态连接的程序运行时需要的glibc或uClibc库等。
  4. 用户应用程序
    特定于用户的应用程序,它们也存储在文件系统中。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。

显然,在嵌入式系统的固态存储设备上有相应的分区来存储它们,下图是一个典型的分区结构:
img not found
“Boot parameter”分区中存放一些可设置的参数,比如IP地址,串口波特率、要传递给内核的命令行参数等。正常启动过程中,BootLoader首先运行,然后它将内核复制到内存中(也有些内核可以在固态存储设备上直接运行),并且在内存某个固定的地址设置好要传递给内核的参数,最后运行内核。内核启动之后,它会mount根文件系统(root filesystem),启动根文件系统中的应用程序。

BootLoader的两个阶段

BootLoader的启动过程可以分为单阶段(Single Stage)、多阶段(Multi-Stage)两种。通常多阶段的BootLoader能提供更为复杂的功能以及更好的移植性。从固态存储设备上启动的BootLoader大多都是两阶段的启动过程。第一阶段使用汇编来实现,它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码;第二阶段则通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。
一般而言,这两个阶段完成的功能可以如下分类。
BootLoader第一阶段的功能。

  1. 硬件设备初始化
  2. 为加载BootLoader的第二阶段代码准备到RAM空间
  3. 复制BootLoader的第二阶段代码到RAM空间中
  4. 设置好栈
  5. 跳转到第二阶段代码的C入口点

在第一阶段进行的硬件初始化一般包括:关闭WATCHDOG、关中断、设置CPU的速度和时钟频率、RAM初始化等。这些其实不是必须的,比如S3C2410/S3C2440的开发板所使用的U-Boot中,就将CPU的速度和时钟频率的设置放在第二阶段。
甚至,将第二阶段的代码复制到RAM空间中也不是必须的,对于NOR Flash等存储设备,完全可以在上面直接执行代码,只不过相比在RAM中执行效率大为降低。
BootLoader第二阶段的功能

  1. 初始化本阶段要用到的硬件设备
  2. 检测系统内存映射(memory map)
  3. 将内核映像和根文件系统映像从Flash上读到RAM空间中
  4. 为内核设置启动参数
  5. 调用内核

为了方便开发,至少要初始化一个串口以便程序员与BootLoader交互。
所谓检测内存映射,就是确定板上使用了多少内存、它们的地址空间是什么。由于嵌入式开发中BootLoader多是针对某类板子进行编写,所以可以根据板子的情况直接设置,不需要考虑可以适用于各类情况的复杂算法。
Flash上的内核映像有可能是经过压缩的,在读到RAM之后,还需要进行解压。当然,如果内核带有自解压功能,不需要BootLoader来进行解压。
将根文件系统映像映射到RAM中,这不是必须的。这取决于是什么类型的根文件系统,以及内核访问它的方法。
将内核存放在适当的位置之后,直接跳到它的入口点即可调用内核。调用内核之前,下列条件要满足:

  1. CPU寄存器的设置
    1. R0 = 0
    2. R1 = 机器类型ID;对于ARM架构的CPU,其机器类型ID可以参见linux/arch/arm/tools/mach-types
    3. R2 = 启动参数标记列表在RAM中的起始基地址
  2. CPU工作模式
    1. 必须禁止中断(IRQs和FIQs)
    2. CPU必须为SVC模式
  3. Cache和MMU的设置
    1. MMU必须关闭
    2. 指令Cache可以打开也可关闭
    3. 数据Cache必须关闭
      如果用C语言,可以像下列代码一样来调用内核:
      1
      2
      3
      void (*theKernel)(int zero,int arch,u32 params_addr) = (void (*)(int,int,u32))KERNEL_RAM_BASE;
      ...
      theKernel(0,ARCH_NUMBER,(u32)kernel_params_start);

BootLoader与内核的交互

BootLoader与内核的交互是单向的,BootLoader将各类参数传给内核。由于它们不能同时运行,传递办法只有一个:BootLoader将参数放在某个约定的地方,再启动内核,内核启动后从这个地方获取参数。
除了约定好参数存放的地址外,还要规定参数的结构。Linux 2.4.x之后的内核都期望以标记列表(tagged list)的形式来传递启动参数。标记就是一种数据结构;标记列表,就是挨着存放的多个标记。标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。
标记的数据结构为tag,它由一个tag_header结构和一个联合(union)组成。tag_header结构表示标记的类型和长度,比如是表示内存还是表示命令行参数等。对于不同类型的标记使用不同的联合(union),比如表示内存时使用tag_mem32,表示命令行时使用tag_cmdline。数据结构tag和tag_header定义在Linux内核源码的include/asm/setup.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
struct  tag_header  {
u32 size;
u32 tag;
};

struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;

/*
Acorn specific
*/
struct tag_acorn acorn;

/*
DC21285 specific
*/
struct tag_memclk memclk;
}u;
};

下面以设置内存标记、命令行标记为例说明参数的传递。

  1. 设置标记ATAG_CORE
    标记列表以标记ATAG_CORE开始,假设BootLoader与内核约定的参数存放地址为0x30000100,则可以以如下代码设置标记ATAG_CORE
    1
    2
    3
    4
    5
    6
    7
    8
    9
    params = (struct tag *)0x30000100;

    params->hdr.tag = ATAG_CORE;
    params->hdr.size = tag_size(tag_core);
    params->u.core.flags = 0;
    params->u.core.pagesize = 0;
    params->u.core.rootdev = 0;

    params = tag_next(params);
    其中,tag_next定义如下,它指向当前标记的末尾:
    1
    #define tag_next(t) ((struct tag *) ((u32 *)(t) + (t)->hdr.size))
  2. 设置内存标记
    假设开发板使用的内存起始地址为0x30000000,大小为0x4000000,则内存标记可以加如下设置:
    1
    2
    3
    4
    5
    6
    7
    params->hdr.tag = ATAG_MEM;
    params->hdr.size = tag_size(tag_mem32);

    params->u.mem.start = 0x30000000;
    params->u.size = 0x4000000;

    params = tag_next(params);
  3. 设置命令行标记
    命令行就是一个字符串,它被用来控制内核的一些行为。比如“root=/dev/mtdblock 2 init=/linuxrc console=ttySAC0”表示根文件系统在MTD2分区上,系统启动后执行的第一个程序为/linuxrc,控制台为ttySAC0(即第一个串口)。
    命令行可以在BootLoader中通过命令设置好,然后按如下构造标记传给内核。
    1
    2
    3
    4
    5
    char *p = "root=/dev/mtdblock 2 init=/linuxrc console=ttySAC0";
    params->hdr.tag = ATAG_CMDLINE;
    params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;
    strcpy(params->u.cmdline.cmdline,p);
    params = tag_next(params);
  4. 设置标记ATAG_NONE
    标记列表以标记ATAG_NONE结束,如下设置:
    1
    2
    params->hdr.tag = ATAG_NONE;
    params->hdr.size = 0;

常用的BootLoader介绍

现在的BootLoader种类繁多,比如x86上有LILO、GRUB等。对于ARM架构的CPU,有U-Boot、ViVi等。它们各有特点。

BootLoader Monitor 描述 X86 ARM PowerPC
LILO Linux磁盘引导程序
GRUB GNU的LILO替代程序
Loadlin 从DOS引导Linux
ROLO 从ROM引导Linux而不需要BIOS
Etherboot 通过以太网卡启动Linux系统的固件
LinuxBIOS 完全替代BUIS的Linux引导程序
BLOB LART等硬件平台的引导程序
U-Boot 通用引导程序
RedBoot 基于eCos的引导程序
Vivi Mizi公司针对SAMSUNG的ARM CPU设计的引导程序

对于S3C2410/S3C2440开发板,U-Boot和Vivi是两个好选择。
Vivi是Mizi针对SAMSUNG的ARM架构CPU专门设计的,基本上可以直接使用,命令简单方便。不过其初始版本只支持串口下载,速度较慢。在网上出现了各种改进版本:支持网络功能、USB功能、烧写YAFFS文件系统映像等。
U-Boot支持大多CPU,可以烧写EXT2、JFFS2文件系统映像,支持串口下载、网络下载,并提供大量的命令。相对于Vivi,它的使用更复杂,但是可以用来更方便的调试程序。

U-Boot分析与移植

U-Boot工程简介

U-Boot,全称Universal Boot Loader,即通用BootLoader,是遵循GPL条款的开源代码项目。其前身是由德国DENX软件工程中心的Wolfgang Denk基于8xxROM的源码创建的PPCBOOT工程。后来整理代码结构使得非常容易增加其他类型的开发板、其他架构的CPU(之前只支持PowerPC);增加更多的功能,比如启动Linux、下载S-Record格式的文件、通过网络启动、通过PCMCIA/CompactFLash/ATA disk/SCSI等方式启动。增加ARM架构CPU及其他更多CPU的支持后,改名U-Boot。
它的名字“通用”有两层含义:可以引导多种操作系统、支持多种架构的CPU。它支持如下操作系统:Linux、NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS等,支持如下架构的CPU:PowerPC、MIPS、x86、ARM、NIOS、XScale等。
U-Boot有如下特性:

  1. 开放源码
  2. 支持多种嵌入式操作系统内核,Linux、NetBSD、VxWorks、QNX、RTEMS、ARTOS、LynxOS等
  3. 支持多个处理器系列,PowerPC、MIPS、x86、ARM、NIOS、XScale等
  4. 较高的可靠性和稳定性
  5. 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布等。
  6. 丰富的设备驱动源码,如串口、以太网、SDRAM、Flash、LCD、NVRAM、EEPROM、RTC、键盘等
  7. 较为丰富的开发调试文档以及强大的网络技术支持
  8. 支持NFS挂载、RAMDISK(压缩或非压缩)形式的根文件系统
  9. 支持NFS挂载、从Flash中引导压缩或非压缩的操作系统内核
  10. 可灵活设置、传递多个关键参数给操作系统,适合系统在不同开发阶段的调试要求与产品发布,尤对Linux支持最为强劲
  11. 支持目标板环境变量多种存储方式,如Flash、NVRAM、EEPROM
  12. CRC32校验,可校验Flash中内核、RAMDISK镜像文件是否完好
  13. 上电自检功能:SDRAM、Flash大小自动检测、SDRAM故障检测、CPU型号
  14. 特殊功能:XIP内核引导

U-Boot源码结构

在U-Boot-1.1.6的基础上进行分析和移植,从sourceforge网站下载U-Boot-1.1.6.tar.bz2后解压得到全部源码。U-Boot-1.1.6源码根目录下共有26个子目录,可以分为4类:

  1. 平台相关的或开发板相关的
  2. 通用的函数
  3. 通用的设备驱动程序
  4. U-Boot工具、示例程序、文档
目录 特性 解释说明
board 开发板相关 对于不同配置的电路板(即使CPU相同),比如smdk2410、sbc2410x
cpu 平台相关 对应不同的CPU,比如arm920t、arm925t、i386等;在它们的子目录下仍可以进一步细分,比如arm920t下就有at91rm9200、s3c24x0
lib_i386等 平台相关 某一架构下的通用文件
include 通用的函数 头文件和开发板配置文件,开发板的配置文件都放在include/configs目录下,U-Boot没有make menuconfig类似的菜单来进行可视化设置,需要手动的修改配置文件中的宏定义
lib_generic 通用的函数 通用的库函数,例如printf等
common 通用的函数 通用的函数,多是对下一层驱动程序的进一步封装
disk 通用的设备驱动程序 硬盘接口程序
drivers 通用的设备驱动程序 各类具体设备的驱动程序,基本上可以通用,它们通过宏从外面引入平台/开发板相关的函数
dtt 通用的设备驱动程序 数字温度测量器或者传感器的驱动
fs 通用的设备驱动程序 文件系统
nand_spl 通用的设备驱动程序 U-Boot一般从ROM、NOR Flash等设备启动,现在开始支持从NAND Flash启动,但是支持的CPU种类还不多
net 通用的设备驱动程序 各种网络协议
post 通用的设备驱动程序 上电自检程序
rtc 通用的设备驱动程序 实时时钟的驱动
doc 文档 开发、使用文档
examples 示例程序 一些测试程序,可以使用U-Boot下载后运行
tools 工具 制作S-Record、U-Boot格式映像的工具,比如mkimage

U-Boot各目录间的层次结构如下图所示:
img not found

比如common/cmd_nand.c文件提供了操作NAND Flash的各种命令,这些命令通过调用drivers/nand/nand_base.c中的擦除、读写函数来实现。这些函数针对NAND Flash的共性作了一些封装,将平台/开发板相关的代码用宏或外部函数替代。而这些宏与外部函数,如果与平台相关,就要在下一层次的cpu/xxx(xxx表示某型号的CPU)中实现;如果与开发板相关,就要在下一层次的board/xxx目录(xxx表示某款开发板)中实现。本书移植的U-Boot,就是在cpu/arm920t/s3c24x0目录下增加一个nand_flash.c文件来实现这些函数。
以增加烧写yaffs文件系统映像的功能为例,即在common目录下的cmd_nand.c中增加命令。比如nand write.yaffs,这个命令要调用drivers/nand/nand_util.c中的相应函数,针对yaffs文件系统的特点依次调用擦除、烧写函数。而这些函数依赖于drivers/nand/nand_base.c、cpu/arm920t/s3c24x0/nand_flash.c文件中的相关函数。
目前U-Boot-1.1.6支持10种架构,根目录下有10个类似lib_i386的目录;31个型号(类型)的CPU,cpu目录下有31个子目录;214种开发板,board目录下有214个子目录,很容易从中找到与自己板子相似的配置,在上面稍作修改即可使用。

U-Boot的配置、编译、连接过程

U-Boot初体验

U-Boot-1.1.6中有几千个文件,要想了解对于某款开发板,使用哪些文件、哪个文件首先执行,可执行文件占用内存的情况,最好的方法就是阅读它的Makefile。
根据顶层Readme文件的说明,可以知道如果要使用开发板board/,就先执行“make _config”命令进行配置,然后执行“make all”,就可以生成如下3个文件:

  1. U-Boot.bin:二进制可执行文件,它就是可以直接烧入ROM、NOR Flash的文件。
  2. U-Boot:ELF格式的可执行文件。
  3. U-Boot.srec:Motorola S-Record格式的可执行文件。

对于S3C2410的开发板,执行“make smdk2410_config”、“make all”后生成的U-Boot.bin可以烧入NOR Flash中运行。启动后可以看到串口输出一些信息后进入控制界面,等待用户的输入。
对于S3C2440的开发板,烧入上面生成的U-Boot.bin,串口无输出,需要修改代码。
在修改代码之前,先看看上面两个命令“make smdk2410_config”、“make all”做了什么事情,以了解程序的流程,知道要修改哪些文件。
另外,编译U-Boot成功之后,还会在它的tools子目录下生成一些工具,比如mkimage等。将它们复制到/usr/local/bin目录下,以后就可以直接使用它们了,比如编译内核时,会使用mkimage来生成U-Boot格式的内核映像文件uImage。

U-Boot的配置过程

在顶层Makefile中可以看到如下代码:

1
2
3
4
5
6
SRCTREE     :=  $(CURDIR)
... ...
MKCONFIG := $(SRCTREE)/mkconfig
... ...
smdk2410_config : unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t smdk2410 NULL s3c24x0

假定在U-Boot-1.1.6的根目录下编译,则其中的MKCONFIG就是根目录下的mkconfig文件。$(@:_config=)的结果就是将“smdk2410_config”中的“_config”去掉,结果为“smdk2410”。所以“make smdk2410_config”实际上就是执行如下命令:

1
./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0

再来看看mkconfig的作用,在mkconfig文件开头第6行给出了它的用法:

1
#Paramters: Target Architecture CPU Board [VENDOR] [SOC]

这里解释一下概念,对于S3C2410、S3C2440,它们被称为SOC(System on Chip),上面除了CPU外,还集成了UART、USB控制器、NAND Flash控制器等设备(称为片内外设)。S3C2410/S3C2440中的CPU为ARM920T
下面分步骤分析mkconfig的作用。

  1. 确定开发板名称BOARD_NAME,相关代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    APPEND=no               #default: Create new config file
    BOARD_NAME="" #Name to print in make output

    while [ $# -gt 0 ] ; do
    case "$1" in
    --) shift ; break ;;
    -a) shift ; APPEND=yes ;;
    -n) shift ; BOARD_NAME="${1%%_config}" ; shift ;;
    *) break ;;
    esac
    done

    [ "${BOARD_NAME}" ] || BOARD_NAME="$1"
    对于“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令。其中没有“–”、“-a”、“-n”等符号,所以第4-11行没做任何事情。第1、2行两个变量仍旧维持原来的值。
    执行完第13行之后,BOARD_NAME的值等于第1个参数,即“smdk2410”。
  2. 创建到平台/开发板相关的头文件的链接。
    略过mkconfig文件中的一些没有起作用的行,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 
    # Create link to architecture specific headers
    #
    if [ "$SRCTREE" != "$OBJTREE" ] ; then
    ... ...
    else
    cd ./include
    rm -f asm
    ln -s asm-$2 asm
    fi
    第4行判断源代码目录和目标文件目录是否一样,可以选择在其他目录下编译U-Boot,这可以令源代码目录保持干净,可以同时使用不同的配置进行编译。
    第7-9行进入include目录,删除asm文件(这时上一次配置时建立的链接文件),然后再次建立asm文件,并令它链接向asm-$2目录,即asm-arm。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    rm -f asm-$2/arch

    if [ -z "$6" -o "$6" = "NULL" ] ; then
    ln -s ${LNPREFIX}arch-$3 asm-$2/arch
    else
    ln -s ${LNPREFIX}arch-$6 asm-$2/arch
    fi

    if [ "$2" = "arm" ] ; then
    rm -f asm-$2/proc
    ln -s ${LNPREFIX}proc-armv asm-$2/proc
    fi
    第1行删除asm-$2/arch目录,即asm-arm/arch。
    对于“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令,$6为“s3c24x0”,不为空,也不是“NULL”,所以第3行的条件不满足,将执行else分支。
    第6行中LNPREFIX为空,所以这个命令实际上就是“ln -s arch-$6 asm-$2/arch”,即“ln -s arch-$6 asm-arm/arch”。
    第10、11行重新建立asm-arm/arch文件,并让它链接向proc-armv目录。
  3. 创建顶层Makefile包含的文件include/config.mk,如下所示:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #
    # Create include file for Make
    #
    echo "ARCH = $2" > config.mk
    echo "CPU = $3" >> config.mk
    echo "BOARD = $4" >> config.mk

    [ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk

    [ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk
    对于“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令,上面几行代码创建的config.mk文件内容如下:
    1
    2
    3
    4
    ARCH = arm
    CPU = arm920t
    BOARD = smdk2410
    SOC = s3c24x0
  4. 创建开发板相关的头文件include/config.h
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 
    # Create board specific header file
    #
    if [ "$APPEND" = "yes" ] # Append to existing config file
    then
    echo >> config.h
    else
    > config.h # Create new config file
    fi
    echo "/* Automatically generated - do not edit */" >> config.h
    echo "#include <configs/$1.h>" >> config.h
    APPEND维持原值“no”,所以config.h被重新建立,它的内容如下:
    1
    2
    /* Automatically generated - do not edit */
    #include <configs/smdk2410.h>

现在总结一下,配置命令“make smdk2410_config”,实际的作用就是执行“./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0”命令。假设执行“./mkconfig $1 $2 $3 $4 $5 $6”命令,则将产生如下结果。

  1. 开发板名称BOARD_NAME等于$1。
  2. 创建到平台/开发板相关的头文件的链接,如下所示:
    1
    2
    3
    ln -s asm-$2 asm
    ln -s arch-$6 asm-$2/arch
    ln -s proc-armv asm-$2/porc # 如果$2不是asm的话,此行没有
  3. 创建顶层Makefile包含的文件include/config.mk,如下所示:
    1
    2
    3
    4
    5
    ARCH = $2
    CPU = $3
    BOARD = $4
    VENDOR = $5 # $5为空,或者是NULL的话,此行没有
    SOC = $6 # $6为空,或者是NULL的话,此行没有
  4. 创建开发板相关的头文件include/config.h,如下所示:
    1
    2
    /* Automatically generated - do not edit */
    #include <configs/$1.h>
    从这4个结果可以知道,如果要在board目录下新建一个开发板的目录,则在include/config目录下也要建立一个文件.h,里面存放的就是开发板的配置信息。
    U-Boot还没有类似Linux一样的可视化配置界面(make menuconfig),需要手动修改配置文件include/config/<board_name>.h来裁剪、设置U-Boot。

配置文件中有以下两类宏。

  1. 一类是选项(Options),前缀为“CONFIG_”,它们用于选择CPU、SOC、开发板类型,设置系统时钟、选择设备驱动等。比如:
    1
    2
    3
    4
    5
    #define CONFIG_ARM920T        1           /*This is an ARM920T Core*/
    #define CONFIG_S3C2410 1 /*in a SAMSUNG S3C2410 SoC*/
    #define CONFIG_SMDK2410 1 /*on a SAMSUNG SMDK2410 Board*/
    #define CONFIG_SYS_CLK_FREQ 12000000 /*the SMDK2410 has 12MHz input clock*/
    #define CONFIG_DRVER_CS8900 1 /*we have a CS8900 on-board*/
  2. 另一类参数(Setting),前缀为“CFG_”,它们用于设置malloc缓冲池的大小、U-Boot的提示符、U-Boot下载文件时的默认加载地址、Flash的起始地址等。比如:
    1
    2
    3
    4
    #define CFG_MALLOC_LEN  (CFG_ENV_SIZE + 128*1024)
    #define CFG_PROMPT "Nibil>" /*控制台命令行前缀*/
    #define CFG_LOAD_ADDR 0x33000000 /*default load address*/
    #define PHYS_FLASH_l 0x00000000 /*Flash Bank #1*/
    从下面的编译、链接过程可知,U-Boot中几乎每个文件都被编译和连接,但是这些文件是否包含有效代码,则由宏开关来控制。比如对于网卡驱动cs8900.c,它的格式为:
    1
    2
    3
    4
    5
    6
    #include <common.h>                     /*将包含配置文件include/config/<board_name>.h*/
    ...
    #ifdef CONFIG_DRIVER_CS8900
    /*实际的代码*/
    ...
    #endif /*CONFIG_DRIVER_CS8900*/
    如果定义了宏CONFIG_DRIVER_CS8900,则文件中包含有效的代码;否则,文件被注释为空。
    可以这样认为,“CONFIG_”除了设置一些参数外,主要用来设置U-Boot的功能、选择使用文件中的哪一部分;而“CFG_”用来设置更细节的参数。

U-Boot的编译、连接过程

配置完后,执行“make all”即可编译,从Makefile中可以了解U-Boot使用了哪些文件、哪个文件首先执行、可执行文件占用内存的情况。
先确定用到哪些文件,下面所示为Makefile中与ARM相关的部分。

1
2
3
4
5
6
7
8
9
10
11
include $(OBJTREE)/include/config.mk
export ARCH CPU BOARD VENDOR SOC

...
ifeq ($(ARCH),arm)
CROSS_COMPILE = arm-linux-
endif

...
# load other configuration
include $(TOPDIR)/config.mk

第1行、第11行用于包含其他的config.mk文件,第1行所要包含文件的就是在上面的配置过程中制作出来的include/config.mk,其中定义了ARCH、CPU、BOARD、SOC等4个变量的值为arm、arm920t、smdk2410、s3c24x0。
第11行包含顶层目录的config.mk文件,它根据上面的4个变量得值确定了编译器、编译选项等。其中对我们理解编译过程有帮助的是BOARDDIR、LDFLAGS的值,如下所示:

1
2
3
4
5
6
7
BOADRDIR = $(BOARD)
...
include $(TOPDIR)/board/$(BOARDDIR)/config.mk # include board specific rules
...
LDSCRIPT := $(TOPDI)/board/$(BOADRDIR)/U-Boot.lds
...
LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PALTFORM_LDFALGS)

在board/smdk2410/config.mk中,定义了“TEXT_BASE = 0x33F80000”。所以,最终结果如下:BOARDDIR为smdk2410;LDFLAGS中有“-T board/smdk2410/U-Boot.lds -Ttext 0x33F80000”字样。
继续往下看Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
OBJS = cpu/$(CPU)/start.o
...
LIBS = lib_generic/libgeneric.a
LIBS += board/$(BOARDDIR)/lib$(BOARD).a
LIBS += cpu/$(CPU)/lib$(CPU).a
...
LIBS += lib_$ARCH/lib$(ARCH).a
LIBS += fs/cramfs/libcramfs.a fs/fat/libfat.a fs/fdos/libfdos.a fs/jffs2/libjffs2.a \
fs/reiserfs/libreiserfs.a fs/ext2/libext2fs.a
LIBS += net/libnet.a
...
LIB += $(BOARDLIBS)
......

从第1行可知,OBJS的第一个值为“cpu/$(CPU)/start.o”,即“cpu/arm920t/start.o”。
第3-13行指定了LIBS变量就是平台/开发板相关的各个目录、通用目录下相应的库,比如:lib_generic/libgeneric.a、board/smdk2410/libsmdk2410.a、cpu/arm920t/libarm920t.a、lib_arm/libarm.a、fs/cramfs/libcramfs.a、fs/fat/libfat.a等。
OBJS、LIBS所代表的.a、.o文件就是U-Boot的构成,它们通过如下命令有相应的源文件编译得到。

1
2
3
4
5
6
7
8
$(OBJS):
$(MAKE) -C cpu/$(CPU)/ $(if $(REMOTE_BUILD),$@,$(notdir $@))

$(LIBS):
$(MAKE) -C $(dir $(subset $(obj),,$@))

$(SUBDIRS):
$(MAKE) -C $@ all

第1、2行的规则表示,对于OBJS中的每个成员,都将进入cpu/$(CPU)目录(即cpu/arm920t)编译它们。现在OBJS为cpu/arm920t/start.o,它将由cpu/arm920t/start.S编译得到。
第4、5行的规则表示,对于LIBS中的每个成员,都将进入相应的子目录执行“make”命令。这些子目录中的Makefile,结构相似,它们将Makefile中指定的文件编译、连接成一个库文件。
当所有的OBJS、LIBS表示的.o、.a文件都生成之后,就剩最后的连接了,这对应Makefile中如下几行:

1
2
3
4
5
6
7
8
9
10
11
12
$(obj)U-Boot.srec:        $(obj)U-Boot
$(OBJCOPY) $(OBJCFLAGS) -O srec $< $@

$(obj)U-Boot.bin: $(obj)U-Boot
$(OBJCOPY) $(OBJCFLAGS) -O binary $< $@

......
$(obj)U-Boot: depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)
UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) | sed -n -e 's/.*\(_u_boot_cmd_.*\)/-u\1/p'|sort | uniq`;\
cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
-Map U-Boot.map -o U-Boot

先使用第8-12行的规则连接得到ELF格式的U-Boot,最后转换为二进制格式U-Boot.bin、S-Record格式U-Boot.srec。LDFLAGS确定了连接方式,其中的“-T board/smdk2410/U-Boot.lds -Ttext 0x33F80000”这样指定了程序的布局、地址。board/smdk2410/U-Boot.lds文件如下:

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
SECTIONS {
. = 0x00000000;

. = ALIGN(4);
.text :
{
cpu/arm920t/start.o (.text)
*(.text)
}

. = ALIGN(4)
.rodata : { *(.rodata) }

. = ALIGN(4)
.data : { *(.data) }

. = ALIGN(4)
.got : { *(.got) }

. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;

. = ALIGN(4)
__bss_satrt = .;
.bss : { *(.bss) }
_end = .;
}

从第7行可知,cpu/arm920t/start.o被放在程序的最前面,所以U-Boot的入口点在cpu/arm920t/start.S中。
现在来总结一下U-Boot的编译流程。

  1. 首先编译cpu/$(CPU)/start.S,对于不同的CPU,还可能编译cpu/$(CPU)下其他的文件。
  2. 然后,对于平台/开发板相关的每个目录、每个通用目录都使用它们各自的Makefile生成相应的库。
  3. 将1、2步骤生成的.o、.a文件按照board/$(BOARDDIR)/config.mk文件中指定的代码段起始地址、board/$(BOARDDIR)/U-Boot.lds连接脚本进行连接。
  4. 第3步得到的ELF格式的U-Boot,后面Makefile还会将它转换为二进制格式、S-Record格式。

U-Boot的启动过程源码分析

本书使用的U-Boot从NOR Flash 启动,下面以开发板smdk2410的U-Boot为例、
U-Boot属于两阶段的Bootloader,第一阶段的文件为cpu/arm920t/start.S和board/smdk2410/lowlevel_init.S,前者是平台相关的,后者是开发板相关的。

U-Boot第一阶段代码分析

  1. 硬件设备初始化
    依次完成如下设置:将CPU的工作模式设为管理模式(SVC),关闭WATCHDOG,设置FCLK、HCLK、PCLK的比例(即设置CLKDIVN寄存器),关闭MMU、CACHE。
    代码都在cpu/arm920t/start.S中,注释也比较完善。

  2. 为加载Bootloader的第二阶段代码准备RAM空间。
    所谓准备RAM空间,就是初始化内存芯片,使它可用。对于S3C2410/S3C2440,通过在start.S中调用lowlevel_init函数来设置存储控制器,使得外接的SDRAM可用。代码在board/smdk2410/lowlevel_init.S中。

    1
    注:lowlevel_init.S文件是开发板相关的,这表示如果外接的设备不一样,可以修改lowlevel_init.S文件中的宏。

    lowlevel_init函数并不复杂,只要注意这时的代码、数据都只存放在NOR 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
    _TEXT_BASE:
    .word TEXT_BASE

    .global lowlevel_init
    lowlevel_init:
    /* memory control configuration */
    /* make r0 relative the current location so that it */
    /* reads SMRDATA out of FLASH rather than memory! */
    ldr r0,=SMRDATA
    ldr r1,_TEXT_BASE
    sub r0,r0,r1
    ldr r1,=BWSCON/*Bus Width Status Controller*/
    add r2,r0,#13*4

    0:
    ldr r3,[r0],#4
    str r3,[r1],#4
    cmp r2,r0
    bne 0b

    /* everything is fine now */
    mov pc,lr

    .ltorg
    /* the literal pools origin */

    SMRDATA:
    .word ... ...
    .word ... ...

    第9-11行进行地址变换,因为这时候内存中没有数据,不能使用连接程序时确定的地址来读取数据。
    第9行中SMRDATA表示这13个寄存器的值存放的开始地址(连接地址),值为0x33F8xxxx,处于内存中。
    第10行获得代码段的起始地址,它就是第1行中的“TEXT_BASE”,其值在board/smdk2410/config.mk中定义为“TEXT_BASE = 0x33F80000”。
    第11行将0x33F8xxxx与0x33F80000相减,这就是13个寄存器值在NOR Flash上存放的开始地址。

  3. 复制Bootloader的第二阶段代码到RAM空间中
    这里将整个U-Boot的代码(包括第一、第二阶段)都复制到SDARM中,这在cpu/arm920t/start.S中实现。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    relocate:                               /*将U-Boot复制到RAM中*/
    adr r0,_start /*r0:当前代码的开始地址*/
    ldr r1,_TEXT_BASE /*r1:代码段的连接地址*/
    cmp r0,r1 /*测试现在是在Flash中还是RAM中*/
    beq stack_setup /*如果已经在RAM中(通常是调试时直接下载到RAM中),则不需要复制*/

    ldr r2,_armboot_start /*_armboot_start在前面定义,是第一条指令的运行地址*/
    ldr r3,_bss_start /*在连接脚本U-Boot.lds中定义,是代码段的结束地址*/
    sub r2,r3,r2 /*r2 = 代码段长度*/
    add r2,r0,r2 /*r2 = NOR Flash 上代码段的结束地址*/

    copy_loop:
    ldmia r0!,{r3-r10} /*从地址[r0]处获得数据*/
    stmia r1!,{r3-r10} /*复制到地址[r1]处*/
    cmp r0,r2 /*判断是否复制完毕*/
    ble copy_loop /*没复制完,则继续*/
  4. 设置好栈
    栈的设置灵活性很大,只要让sp寄存器指向一段没有使用的内存即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* set up the stack */
    stack_setup:
    ldr r0,_TEXT_BASE /*_TEXT_BASE为代码段的开始地址,值为0x33F80000*/
    sub r0,r0,#CFG_MALLOC_LEN /*代码段下面,留出一段内存以实现malloc*/
    sub r0,r0,#CFG_GBL_DATA_SIZE /*再留出一段内存,存一些全局参数*/

    #ifdef CONFIG_USE_IRQ
    sub r0,r0,#(CONFIG_STACKSIZE_IRQ + CONFIG_STACKSIZE_FIQ) /*IRQ、FIQ模式的栈*/
    #endif

    sub sp,r0,#12 /*最后,留出12字节的内存给abort异常,往下的内存就是栈了*/

    到了这一步,读者就可以知道内存的使用情况了,如下图所示,图中与上面代码的划分稍有不同,这时因为在cpu/arm920t/cpu.c中的cpu_init函数中才真正为IRQ、FIQ模式划分了栈。
    img not found

  5. 跳转到第二阶段代码的C入口点
    在跳转之前,还要清除BSS段(初始值为0、无初始值的全局变量、静态变量放在BSS段),代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    clear_bss:
    ldr r0,_bss_start /*BSS段的开始地址,它的值在连接脚本U-Boot.lds中确定*/
    ldr r1,_bss_end /*BSS段的结束地址,它的值在连接脚本U-Boot.lds中确定*/
    mov r2,#0x00000000

    clbss_l:
    str r2,[r0] /*往BSS段中写入0值*/
    add r0,r0,#4
    cmp r0,r1
    ble clbss_l

    现在,C函数的环境已经完全准备好,通过如下命令直接跳转(这之后,程序才在内存中执行),它将调用lib_arm/board.c中的start_arm_boot函数,这是第二阶段的入口点。

    1
    2
    3
      ldr pc,_start_armboot

    _start_armboot: .word start_armboot

U-Boot第二阶段代码分析

U-Boot在启动内核之前可以让用户选择是否进入下载模式,即进入U-Boot的控制界面。
第二阶段从lib_arm/board.c中的start_armboot函数开始,程序流程图如下所示:
img not found
移植U-Boot的主要工作在于对硬件的初始化、驱动,所以下面讲解时将重点放在硬件的操作上。

  1. 初始化本阶段要使用到的硬件设备
    最主要的的是系统时钟、初始化串口,只要设置好这两个,就可以从串口看到打印信息。
    board_init函数设置MPLL、改变系统时钟,它是开发板相关的函数,在board/smdk2410/smdk2410.c中实现。值得注意的是,board_init函数中保存了机器类型ID,这将在调用内核时传给内核。代码如下:
    1
    2
    /*arch number of SMDK2410-Board*/
    gd->bd->bi_arch_number = MACH_TYPE_SMDK2410; //值为193
    串口的初始化函数主要是serial_init,它设置UART控制器,是CPU相关的函数,在cpu/arm920t/s3c24x0/serail.c中实现。
  2. 检测系统内存映射(memory map)
    对于特定的开发板,其内存的分布是明确的,所以可以直接设置。board/smdk2410/smdk2410.c中的dram_init函数指定了本开发板的内存起始地址为0x30000000,大小为0x4000000。代码如下:
    1
    2
    3
    4
    5
    6
    int dram_init(void)
    {
    gd->bd->bi_dram[0].start = PHYS_SDRAM_1; //即0x30000000;
    gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE; //即0x4000000;
    return 0
    }
    这些参数的设置,将在后面向内核传递参数时用到。
  3. U-Boot命令的格式
    即使是内核的启动,也是通过U-Boot命令来实现的。U-Boot中每个命令都通过U_BOOT_CMD宏来定义,格式如下:
    1
    U_BOOT_CMD(name,maxargs,repeatable,command,"usage","help")
    各项参数意义如下:
    ①name:命令的名字,它不是一个字符串,不要用双引号括起来。
    ②maxargs:最大的参数个数
    ③repeatable:命令是否可以重复,可重复是指运行一个命令后,下次再敲回车即可再次运行
    ④command:对应的函数指针,类型为(*cmd)(struct cmd_tbl_s *,int,int,char *[])
    ⑤usage:简短的使用说明,字符串。
    ⑥help:详细的使用说明,字符串。

宏U_BOOT_CMD在include/command.h中定义,如下所示:

1
2
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name,maxargs,rep,cmd,usage,help}

Struct_Section也是在include/command.h中定义,如下所示:

1
#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))

比如对与bootm命令,它如下定义:

1
2
3
4
5
U_BOOT_CMD(
bootm,CFG_MAXARGS,1,do_bootm,
"string1",
"string2"
);

宏U_BOOT_CMD扩展开后如下所示:

1
cmd_tbl_t __u_boot_cmd_bootm __attribute__ ((unused,section (".u_boot_cmd"))) = {"bootm",CFG_MAXARGS,1,do_bootm,"string1","string2"};

对于每个使用U_BOOT_CMD宏来定义的命令,其实都是在“.u_boot_cmd”段中定义一个cmd_tbl_t结构。连接脚本U-Boot.lds中有如下代码:

1
2
3
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;

程序中就是根据命令的名字在内存段__u_boot_cmd_start~__u_boot_cmd_end找到它的cmd_tbl_t结构,然后调用它的函数。
内核的复制与启动,可以通过如下命令来完成:bootm从内存、ROM、NOR Flash中启动内核,bootp则通过网络来启动,而nboot从NAND Flash启动内核。它们都是先将内核映像从各种媒介中读出,存放在指定的位置;然后设置标记列表以给内核传递参数;最后跳到内核的入口点去执行。
4. 为内核设置启动参数
U-Boot也是通过标记列表向内核传递参数。并且,在之前的小节中内存标记、命令行标记的示例代码就是取自U-Boot中的setup_memory_tags、setup_commandline_tag函数,它们都是在lib_arm/armlinux.c中定义。一般而言,设置这两个标记就可以了,在配置文件include/configs/smdk2410.h中增加如下两个配置项即可:

1
2
#define CONFIG_SETUP_MEMORY_TAGS  1
#define CONFIG_CMDLINE_TAG 1

对于ARM架构的CPU,都是通过lib_arm/armlinux.c中的do_bootm_linux函数来启动内核。这个函数中,设置标记列表,最后通过theKernel(0,bd->bi_arch_number,bd->bi_boot_params)调用内核。其中。theKernel指向内核存放的地址(对于ARM架构的CPU,通常是0x30008000),bd->bi_arch_number就是前面board_init函数设置的机器类型ID,而bd->bi_boot_params就是标记列表的开始地址。

U-Boot的移植

开发板smdk2410的配置适用于大多数S342410开发板,或者只需要极少的修改即可使用。但是目前的U-Boot中没有对S3C2410的支持,需要我们自己移植。
本书基于S342410、S342440两款开发板,它们的外接硬件相同。

  1. BANK0外界容量为1MB,位宽为8的NOR Flash芯片AM29LV800。
  2. BANK3外接10M网卡芯片CS8900,位宽为16。
  3. BANK6外接两片容量为32MB、位宽为16的SDRAM芯片K4S561632,组成容量为64MB、位宽为32的内存。
  4. 听过NAND Flash控制器外接容量为64MB,位宽为8的NAND Flash芯片K9S1208。

对于NOR Flash和NAND Flash,下图划分了它们的使用区域。
img not found
由于NAND Flash的“位反转”现象比较常见,为保证数据的正确,在读写数据时需要使用ECC校验。另外,NAND Flash在使用过程中、运输过程中还有可能出现坏块。所以下面使用NOR Flash保存U-Boot,NAND Flash保存内核和文件系统,并使用U-Boot烧写内核、文件系统时进行坏块检查、ECC校验。这样,即使NAND Flash出现坏块导致内核或文件系统不能使用,也可以通过NOR Flash中的U-Boot来重新烧写。
smdk2410开发板已经支持NOR Flash芯片AM29LV800,U-Boot本身也已经支持JFFS2文件系统映像的烧写。下面一步一步移植U-Boot,增加以下新功能。

  1. 同时支持本书使用的S3C2410和S3C2440开发板。
  2. 支持串口xmodem协议。
  3. 支持网卡芯片CS8900。
  4. 支持NAND Flash读写。
  5. 支持烧写yaffs文件系统映像。

同时支持S3C2410和S3C2440

我们将在开发板smdk2410的基础上进行移植。

新建一个开发板的相应目录和文件

为了不破坏原来的代码,在board目录下将smdk2410复制为100ask24x0目录,并将board/100ask24x0/smdk2410.c改名为100ask2410.c
根据前面描述的配置过程可知,还要在include/configs目录下建立一个配置文件100ask24x0.h,可以将include/configs/smdk2410.h直接复制为100ask24x0.h
还要修改两个Makefile,首先在顶层Makefile中增加两行:

1
2
100ask24x0_config : unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0

board/100ask24x0/Makefile中进行如下修改:

1
2
3
COBJS := smdk2410.o flash.o
改为
COBJS := 100ask24x0.o flash.o

修改SDRAM的设置

SDRAM的初始化在U-Boot的第一阶段完成,就是在board/100ask24x0/lowlevel_init.S文件中设置存储寄存器。
检查BANK6的设置:位宽为32,宏B6_BWSON刚好为DW32(32位),无需改变;另外还需要根据HCLK设置SDRAM的刷新参数,主要是REFCNT寄存器。
本书所用开发板的HCLK都设为100MHz,需要根据SDRAM芯片的具体参数重新计算REFCNT寄存器的值。代码修改如下:

1
2
3
#define REFCNT 1113         //period=15.6μs,HCLK=60MHz,(2048+1-15.6*60)
改为
#define REFCNT 0x4f4 //period=7.8125μs,HCLK=100MHz,(2048+1-7.8125*100)

对于其他BANK,比如网卡芯片CS8900所在的BANK2,原来的设置刚好匹配,无需更改;而对于BANK1、BANK2、BANK4、BANK5、BANK7,在U-Boot中并没有使用到它们外接的设备,也不需要理会。

增加对S3C2440的支持

S3C2440是S3C2410的改进版,它们的操作基本相似。不过在系统时间的设置、NAND Flash控制器的操作方面有一些小差别。它们的MPLL、UPLL计算公式不一样,FCLK、HCLK和PCLK的分频化设置也不一样。
我们的目标是令同一个二进制映像文件既能在S3C2410上运行,也可以在S3C2440上运行。首先需要在代码中自动识别芯片是S3C2440还是S3C2410,这可以通过读取GSTATUS1寄存器来分辨:0x32410000表示S3C2410,0x32410002表示S3C2410A,0x32440000表示S3C2440,0x32440001表示S3C2440A。
对于S3C2410开发板,将FCLK设为200MHz,分频比为FCLK:HCLK:PCLK=1:2:4;对于S3C2440,将FCLK设为400MHz,分频比为FCLK:HCLK:PCLK=1:4:8。还将UPLL设为48MHz,即UCLK为48MHz,以在内核中支持USB控制器。
首先修改board/100ask24x0/100ask24x0.c中的board_init函数,下面是修改后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* S3C2440:
MPLL = (2 * m * Fin) / (p * 2^s)
UPLL = (m * Fin) / (p * 2^s)
m = M(the value for divider M) + 8
p = P(the value for divider P) + 2
*/
#define S3C2440_MPLL_400MHz ((0x5c << 12) | (0x01 << 4) | 0x01)
#define S3C2440_UPLL_48MHz ((0x38 << 12) | (0x02 << 4) | 0x02)
#define S3C2440_CLKDIV (0x05) //FCLK:HCLK:PLCK = 1:4:8 UCLK = UPLL

/* S3C2410:
MPLL = (m * Fin) / (p * 2^s)
UPLL = (m * Fin) / (p * 2^s)
m = M(the value for divider M) + 8
p = P(the value for divider P) + 2
*/
#define S3C2410_MPLL_200MHz ((0x5c << 12) | (0x04 << 4) | 0x00)
#define S3C2410_UPLL_48MHz ((0x28 << 12) | (0x01 << 4) | 0x02)
#define S3C2410_CLKDIV (0x05) //FCLK:HCLK:PLCK = 1:2:8

针对S3C2410、S3C2440分别定义了MPLL、UPLL寄存器的值。开发板输入时钟为12MHz(include/configs/100ask24x0.h中的宏CONFIG_SYS_CLK_FREQ中定义)。
下面针对S3C2410、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
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
int board_init(void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();

/*设置GPIO*/
gpio->GPACON = 0x007FFFFF;
gpio->GPBCON = 0x00044555;
gpio->GPBUP = 0x000007FF;
gpio->GPCCON = 0xAAAAAAAA;
gpio->GPCUP = 0x0000FFFF;
gpio->GPDCON = 0xAAAAAAAA;
gpio->GPDUP = 0x0000FFFF;
gpio->GPECON = 0xAAAAAAAA;
gpio->GPEUP = 0x0000FFFF;
gpio->GPFCON = 0x000055AA;
gpio->GPFUP = 0x000000FF;
gpio->GPGCON = 0xFF95FFBA;
gpio->GPGUP = 0x0000FFFF;
gpio->GPHCON = 0x002AFAAA;
gpio->GPHUP = 0x000007FF;

/*同时支持S3C2410和S3C2440*/
if((gpio->GSTATUS1 == 0x32410000) || (gpio->GSTATUS1 == 0x32410002))
{
/*FCLK:HCLK:PCLK=1:2:4*/
clk_power->CLKDIVN = S3C2410_CLKDIV;

/*修改为异步总线模式*/
__asm__("mrc p15,0,r1,c1,c0,0 \n" /*read ctrl register*/
"orr r1,r1, #0xc0000000 \n" /*Asynchronous*/
"mcr p15,0,r1,c1,c0,0 \n" /*write ctrl register*/
:::"r1"
);

/*设置PLL锁定时间*/
clk_power->LOCKTIME = 0xFFFFFF;

/*配置MPLL*/
clk_power->MPLLCON = S3C2410_MPLL_200MHZ;

/*配置MPLL后,要延时一段时间再配置UPLL*/
delay(4000);

/*配置UPLL*/
clk_power_UPLLCON = S3C2410_UPLL_48MHZ;

/*再延时一会*/
delay(8000);

/*机器类型ID,这在调用Linux内核时用到*/
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
}
else
{
/*FCLK:HCLK:PCLK=1:4:8*/
clk_power->CLKDIVN = S3C2440_CLKDIV;

/*修改为异步总线模式*/
__asm__("mrc p15,0,r1,c1,c0,0 \n" /*read ctrl register*/
"orr r1,r1, #0xc0000000 \n" /*Asynchronous*/
"mcr p15,0,r1,c1,c0,0 \n" /*write ctrl register*/
:::"r1"
);

/*设置PLL锁定时间*/
clk_power->LOCKTIME = 0xFFFFFF;

/*配置MPLL*/
clk_power->MPLLCON = S3C2440_MPLL_400MHZ;

/*配置MPLL后,要延时一段时间再配置UPLL*/
delay(4000);

/*配置UPLL*/
clk_power_UPLLCON = S3C2440_UPLL_48MHZ;

/*再延时一会*/
delay(8000);

/*机器类型ID,这在调用Linux内核时用到*/
gd->bd->bi_arch_number = MACH_TYPE_SMDK2440;
}

/*启动内核时,参数存放位置。这个值在构造标记列表时用到*/
gd->bd->bi_boot_params = 0x30000100;

icache_enable();
dcache_enable();

return 0;
}

最后一步:获取系统时钟的函数需要针对S3C2410、S3C2440的不同进行修改。
在后面设置串口波特率时需要获得系统时钟,就是在U-Boot第二阶段,lib_arm/board.cstart_armboot函数调用serial_init函数初始化串口时,会调用get_PCLK函数。它在cpu/arm920t/s3c24x0/speed.c中定义,与它相关的还有get_HCLKget_PLLCLK等函数。
前面的board_init函数在识别出S3C2410或S3C2440后,设置了机器类型ID:gd->bd->bi_arch_number,后面的函数可以通过它分辨是S3C2410还是S3C2440。首先要在程序的开头增加如下代码,这样才可以使用gd变量。

1
DECLARE_GLOBAL_DATA_PTR;

S3C2410和S3C2440的MPLL、UPLL计算公式不一样,所以get_PLLCLK也需要修改,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static ulong get_PLLCLK(int pllreg)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
ulong r,m,p,s;

if(pllreg == MPLL)
r = clk_power->MPLLCON;
else if(pllreg == UPLL)
r = clk_power->UPLLCON;
else
hang();

m = ((r & 0xFF000) >> 12) + 8;
p = ((r & 0x003F0) >> 4 ) + 2;
s = r & 0x3;

/*同时支持S3C2410和S3C2440*/
if(gd->bd->bi_arch_number == MACH_TYPE_SMDK2410)
return ((CONFIG_SYS_CLK_FREQ * m) / (p << s));
else
return ((CONFIG_SYS_CLK_FREQ * m * 2) / (p << s));
}

由于分频系数的设置方法也不一样,get_HCLKgetPCLK也需要修改。对于S3C2410,沿用原来的计算方法,else分支中是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
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
/* for s3c2440 */
#define S3C2440_CLKDIVN_PDIVN (1 << 0)
#define S3C2440_CLKDIVN_HDIVN_MASK (3 << 1)
#define S3C2440_CLKDIVN_HDIVN_1 (0 << 1)
#define S3C2440_CLKDIVN_HDIVN_2 (1 << 1)
#define S3C2440_CLKDIVN_HDIVN_4_8 (2 << 1)
#define S3C2440_CLKDIVN_HDIVN_3_6 (3 << 1)
#define S3C2440_CLKDIVN_UCLK (1 << 3)

#define S3C2440_CAMDIVN_CAMCLK_MASK (0xf << 0)
#define S3C2440_CAMDIVN_CAMCLK_SEL (1 << 4)
#define S3C2440_CAMDIVN_HCLK3_HALF (1 << 8)
#define S3C2440_CAMDIVN_HCLK4_HALF (1 << 9)
#define S3C2440_CAMDIVN_DVSEN (1 << 12)

/*return HCLK frequency*/
ulong get_HCLK(void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
unsigned long clkdiv;
unsigned long camdiv;
int hdiv = 1;

/*同时支持S3C2410和S3C2440*/
if(gd->bd->bi_arch_number == MACH_TYPE_SMDK2410)
return ((clk_power->CLKDIVN & 0x2) ? get_FCLK() / 2 : getFCLK());
else
{
clkdiv = clk_power->CLKDIVN;
camdiv = clk_power->CAMDIVN;

/*计算分频比*/
switch(clkdiv & S3C2440_CLKDIVN_HDIVN_MASK)
{
case S3C2440_CLKDIVN_HDIVN_1:
hdiv = 1;
break;

case S3C2440_CLKDIVN_HDIVN_2:
hdiv = 2;
break;

case S3C2440_CLKDIVN_HDIVN_4_8:
hdiv = (camdiv & S3C2440_CAMDIVN_HCLK4_HALF) ? 8 : 4;
break;

case S3C2440_CLKDIVN_HDIVN_3_6:
hdiv = (camdiv & S3C2440_CAMDIVN_HCLK3_HALF) ? 6 : 3;
break;
}

return get_FCLK() / hdiv;
}
}

/*return PCLK frequence*/
ulong getPCLK(void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
unsigned long clkdiv;
unsigned long camdiv;
int hdiv = 1;

/*同时支持S3C2410和S3C2440*/
if(gd->bd->bi_arch_number == MACH_TYPE_SMDK2410)
return ((clk_power->CLKDIVN & 0x1) ? get_HCLK() / 2 : getHCLK());
else
{
clkdiv = clk_power->CLKDIVN;
camdiv = clk_power->CAMDIVN;

/*计算分频比*/
switch(clkdiv & S3C2440_CLKDIVN_HDIVN_MASK)
{
case S3C2440_CLKDIVN_HDIVN_1:
hdiv = 1;
break;

case S3C2440_CLKDIVN_HDIVN_2:
hdiv = 2;
break;

case S3C2440_CLKDIVN_HDIVN_4_8:
hdiv = (camdiv & S3C2440_CAMDIVN_HCLK4_HALF) ? 8 : 4;
break;

case S3C2440_CLKDIVN_HDIVN_3_6:
hdiv = (camdiv & S3C2440_CAMDIVN_HCLK3_HALF) ? 6 : 3;
break;
}

return get_FCLK() / hdiv / ((clkdiv & S3C2440_CLKDIVN_PDIVN)?2:1);
}
}

现在重新执行“make 100ask24x0_config”和“make all”生成的U-Boot.bin文件既可以运行于S3C2410开发板,也可以运行于S3C2440开发板。将它烧录进入NOR Flash后启动,就可以在串口工具中看到提示信息,可以输入各种命令操作U-Boot了。

选择NOR Flash的型号

但是,现在还无法通过U-Boot命令烧写NOR Flash。本书所用开发板中的NOR Flash型号为AM29LV800,而配置文件include/configs/a00ask24x0.h中的默认型号为AM29LV400。修改如下:

1
2
3
4
5
6
7
8
9
#define CONFIG_AMD_LV400 1  //uncomment this if you have a LV400 flash
#if 0
#define CONFIG_AMD_LV800 1 //uncomment this if you have a LV800 flash
#endif
改为
#if 0
#define CONFIG_AMD_LV400 1 //uncomment this if you have a LV400 flash
#endif
#define CONFIG_AMD_LV800 1 //uncomment this if you have a LV800 flash

本例子中NOR Flash的操作函数在board/100ask24x0/flash.c中实现,它支持AM29LV400y和AM29LV800。对于其他型号的NOR Flash,如果符合CFI接口标准,则可以在使用drivers/cfi_flash.c中的接口函数;否则,只好自己编写了。如果要使用cfi_flash.c,如下修改两个文件:
include/configs/100ask24x0.h中增加以下一行:

1
#define CFG_FLASH_CFI_DRIVER 1

board/100ask24x0/Makefile去掉flash.o:

1
2
3
COBJS :=  100ask24x0.o flash.o
改为
COBJS := 100ask24x0.o

修改好对NOR Flash的支持后,重新编译U-Boot:make clean、make all。运行后可以在串口中看到如下字样:

1
Flash: 1MB

现在可以使用loadb、loady等命令通过串口下载文件,然后使用erase、cp命令分别擦除、烧写NOR Flash了,它们的效率比JTAG高好几倍。

支持串口xmodem协议

上面的loadb命令需要配合Linux下的kermit工具来使用,loady命令通过串口ymodem协议来传输文件。Windows工具SecureCRT只支持xmodem和zmodem。现在修改代码对xmodem的支持,即增加一个命令loadx。

1
2
3
4
5
6
7
8
/* 支持xmodem */
U_BOOT_CMD(
loadx,3,0 do_load_serial_bin,
"loadx - load binary file over serial line (xmodem mode)\n",
"[ off ] [ baud ]\n"
" -load binary file over serial line"
"with offset 'off' and baudrate 'baud'\n"
)

其次,在do_load_serial_bin函数中增加对loadx命令的处理分支。也是依照loady来实现:

1
2
3
4
5
6
7
8
/*支持xmodem*/
if(strcmp(argv[0],"loadx") == 0){
printf("## Ready for binary (xmodem) download to 0x%081X at %d bps...\n",offset,load_baudrate);
addr = load_serial_xmodem(offset);
}else if(strcmp(argv[0],"loady") == 0){
printf("## Ready for binary (ymodem) download to 0x%081X at %d bps...\n",offset,load_baudrate);
addr = load_serial_ymodem(offset);
}

第2-5行就是为loadx命令增加的代码。
第4行调用load_serial_xmodem函数,它是依照load_serial_ymodem实现的一个新函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#if (CONFIG_COMMANDS & CFG_CMD_LOADB)
/*支持xmodem*/
static ulong load_serial_xmodem(ulong offset);
static ulong load_serial_ymodem(ulong offset);
#endif

/*支持xmodem*/
static ulong load_serial_xmodem(ulong offset)
{
char xmodemBuf[1024]; //原来是ymodemBuf,这只是为了与函数名称一致

info.mode = xyzmodem_xmodem; //原来是xyzmodem_ymodem,对应ymodem
}

首先在开头增加load_serail_xmodem函数的声明,然后复制load_serial_ymodem函数为load_serial_xmodem。稍作修改:

  1. 将局部数组ymodemBuf改名为xmodemBuf,并在后面使用到的地方统一修改。这只是为了与函数名称一致。
  2. info.mode的值从xyzModem_ymodem改为xyzModem_xmodem
    重新编译、烧写U-Boot.bin后,就可以使用laodx命令下载文件了。

支持网卡芯片CS8900

使用串口来传输文件的速率太低,现在增加对网卡芯片CS8900的支持。
本书使用开发板的网卡芯片CS8900的连接方式与smdk2410完全一样,所以现在的U-Boot中已经支持CS8900了,它的驱动程序为drivers/cs8900.c。只要在U-Boot控制界面中稍加配置就可以使用网络功能。使用网络之前,先设置开发板IP地址、MAC地址,服务器IP地址,比如可以在U-Boot中执行以下命令:

1
2
3
4
setenv ipdaar 192.168.1.17
setenv ethaddr 08:00:3e:26:0a:5b
setenv serverip 192.168.1.11
saveenv

然后就可以使用tftp或nfs命令下载文件了,注意:服务器上要开启tftp或nfs服务。比如可以使用如下命令将U-Boot.bin命令下载到内存0x30000000中。

1
2
3
tftp 0x30000000 U-Boot.bin

tftp 0x30000000 192.168.1.57:/work/nfs_root/U-Boot.bin

可以修改配置文件,让网卡的各个默认值就是上面设置的值。在此之前,先了解网卡的相关文件,这有助于移植代码以支持其他连接方式的CS8900。
首先,CS8900接在S3C2410、S3C2440的BANK3,位宽16,使用WAIT、nBE信号。在设置存储控制器时要设置好BANK3,代码在board/100ask24x0/lowlevel_init.S中,如下所示:

1
2
3
4
5
6
7
8
9
10
#define B3_BWSCON           (DW16 + WAIT + UBLB)
... ...
/* 时序参数 */
#define B3_Tacs (0x0) /*0clk*/
#define B3_Tcos (0x3) /*4clk*/
#define B3_Tacc (0x7) /*14clk*/
#define B3_Tcoh (0x1) /*1clk*/
#define B3_Tah (0x0) /*0clk*/
#define B3_Tacp (0x3) /*6clk*/
#define B3_PMC (0x0) /*normal*/

接下来,还要确定CS8900的基地址。这在配置文件include/configs/100ask24x0.h中定义,如下所示:

1
2
3
#define CONFIG_DRIVER_CS8900            1           /* 使用CS8900 */
#define CS8900_BASE 0x19000300 /* 基地址 */
#define CS8900_BUS16 1 /* 位宽为16 */

网卡CS8900的访问基址为0x1900000,之所以再偏移0x300是由它的特性决定的。
最后,还是在配置文件include/config/100ask24x0.h中定义CS8900的各个默认地址,如下所示:

1
2
3
4
#define CONFIG_ETHADDR        08:00:3e:26:0a:5b
#define CONFIG_NETMASK 255.255.255.0
#define CONFIG_IPADDR 192.168.1.17
#define CONFIG_SERVERIP 192.168.1.11

如果要增加ping命令,还可以在配置文件include/configs/100ask24x0.h的宏CONFIG_COMMANDS中增加CFG_CMD_PING,如下所示:

1
2
3
#define CONFIG_COMMANDS (CONFIG_CMD_DFL | CFG_CMD_CACHE | CFG_CMD_PING \
| ... ... \
)

支持NAND Flash

U-Boot 1.1.6中对NAND Flash的支持有新旧两套代码,新代码在drivers/nand目录下,旧代码在drivers/nand_legacy目录下。文档doc/README.md对这两套代码有所说明:使用旧代码需要定义更多的宏,而新代码移植自Linux内核2.6.12,它更加智能,可以自动识别更多型号的NAND Flash。目前之所以还保留旧的代码,是因为两个目标板NETTA、NETTA_ISDN使用JFFS文件系统,它们还依赖于旧代码。当相关的功能移植到新代码之后,旧代码将从U-Boot中移除。
要让U-Boot支持NAND Flash,首先在配置文件include/configs/100ask24x0.h的宏CONFIG_COMMANDS中增加CFG_CMD_NAND,如下所示:

1
2
3
4
5
6
#define CONFIG_COMMANDS                     \
(CONFIG_CMD_DEL | \
CFG_CMD_CACHE | \
CFG_CMD_PING | \
CFG_CMD_NAND | \
... ... )

然后选择使用哪套代码:在配置文件中定义宏CFG_NAND_LEGACY则使用旧代码,否则使用新代码。
使用旧代码时,需要实现drivers/nand_legacy/nand_legacy.c中使用到的各种宏,比如:

1
2
#define NAND_WAIT_READY(nand)       /* 等待Nand Flash的状态为“就绪”,代码依赖于具体的开发板 */
#define WRITE_NAND_COMMAN(d,adr) /* 写NAND Flash命令,代码依赖于具体的开发板 */

本书使用新代码,下面讲述移植过程。
代码的移植没有现成的文档,可以在配置文件include/configs/100ask24x0.h的宏CONFIG_COMMANDS中增加CFG_CMD_NAND后就编译代码,然后一个一个的解决出现的的错误。编译结果中出现的错误和警告如下:

1
2
3
4
5
6
7
8
nand.h: 412: error: 'NAND_MAX_CHIPS' undeclared here (not in a function)
nand.h: 35: error: 'CFG_MAX_NAND_DEVICE' undeclared here (not in a function)
nand.h: 38: error: 'CFG_NAND_BASE' undeclared here (not in a function)
nand.h: 35: error: storage size of 'nand_info' isn't known
nand.h: 37: error: storage size of 'nand_chip' isn't known
nand.h: 38: error: storage size of 'base_address' isn't known
nand.h: 37: warning: 'nand_chip' defined but not used
nand.h: 38: warning: 'base_address' defined but not used

在配置文件include/configs/100ask24x0.h中增加如下3个宏就可以解决上述错误。在Flash的驱动程序中,设备是逻辑上的概念,表示一组相同结构、访问函数相同的Flash芯片。在本书所用的开发板中,只有一个NAND Flash芯片,所以设备数为1,芯片数也为1。

1
2
3
#define CFG_NAND_BASE           0 /* 无实际意义:基地址,这在board_nand_init中重新指定 */
#define CFG_MAX_NAND_DEVICE 1 /* NAND Flash “设备”的数目为1 */
#define NAND_MAX_CHIPS 1 /* 每个NAND Flash “设备”由1个NAND Flash“芯片”组成 */

修改配置文件后再次编译,现在只有一个错误了,“board_nand_init函数未定义”,如下所示:

1
nand.c:50: undefined reference to 'board_nand_init'

调用board_nand_init函数的过程为:NAND Flash的初始化入口函数是nand_init,它在lib_arm/board.cstart_armboot函数中被调用;nand_init函数在drivers/nand/nand.c中实现,它调用相同文件中的nand_init_chip函数;nand_init_chip函数首先调用board_nand_init函数来初始化NAND Flash设备,最后才是统一的识别过程。
board_nand_init函数的名称就可以知道它是平台/开发板相关的函数,需要自己编写。本书在cpu/arm920t/s3c24x0目录下新建一个文件nand_flash.c,在里面针对S3C2410、S3C2440实现了统一的board_nand_init函数。
在编写board_nand_init函数之前,需要针对S3C2410、S3C2440 Nand Flash控制器的不同来定义一些数据结构和函数:

  1. include/s3c24x0.h文件中增加S3C2440_NAND数据结构。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    typdef struct {
    S3C24X0_REG32 NFCONF;
    S3C24X0_REG32 NFCONT;
    S3C24X0_REG32 NFCMD;
    S3C24X0_REG32 NFADDR;
    S3C24X0_REG32 NFDATA;
    S3C24X0_REG32 NFMECCD0;
    S3C24X0_REG32 NFMECCD1;
    S3C24X0_REG32 NFSECCD;
    S3C24X0_REG32 NFSTAT;
    S3C24X0_REG32 NFESTAT0;
    S3C24X0_REG32 NFESTAT1;
    S3C24X0_REG32 NFMECC0;
    S3C24X0_REG32 NFMECC1;
    S3C24X0_REG32 NFSECC;
    S3C24X0_REG32 NFSBLK;
    S3C24X0_REG32 NFEBLK;
    }/* __attribute__((__packed__)) */S3C2440_NAND;
  2. include/s3c2410.h文件中仿照S3C2410_GetBase_NAND函数定义S3C2440_GetBase_NAND函数。
    1
    2
    3
    4
    5
    /* for s3c2440 */
    static inline S3C2440_NAND * const S3C2440_GetBase_NAND(void)
    {
    return (S3C2440_NAND * const)S3C2410_NAND_BASE;
    }
    既然新的NAND Flash代码是从Linux 内核2.6.12中移植来的,那么cpu/arm920t/s3c24x0/nand_flash.c文件也可以仿照内核中对S3C2410、S3C2440的NAND Flash进行初始化的drivers/mtd/nand/s3c2410.c文件来编写。为了方便阅读,先把cpu/arm920t/s3c24x0/nand_flash.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
    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
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    /*
    s3c2410/s3c2440的NAND Flash控制器接口,
    修改自Linux内核2.6.13文件 drivers/mtd/nand/s3c2410.c
    */
    #include <common.h>

    #if (CONFIG_COMMANDS & CFG_CMD_NAND) && !defined(CFG_NAND_LEGACY)

    #include<s3c2410.h>
    #include<nand.h>

    DECLARE_GLOBAL_DATA_PTR;

    #define S3C2410_NFSTAT_READY (1<<0)
    #define S3C2410_NFCONF_nFCE (1<<11)

    #define S3C2440_NFSTAT_READY (1<<0)
    #define S3C2440_NFCONF_nFCE (1<<1)

    /* S3C2410: NAND Flash 的片选函数 */
    static void s3c2410_nand_select_chip(struct mtd_info *mtd,int chip)
    {
    S3C2410_NAND * const s3c2410nand = S3C2410_GetBase_NAND();

    if(chip == -1){
    s3c2410nand->NFCONF |= S3C2410_NFCONF_nFCE; //禁止片选信号
    }else{
    s3c2410nand->NFCONF |= ~S3C2410_NFCONF_nFCE; //使能片选信号
    }
    }

    /*
    S3C2410: 命令和控制函数
    注意,这个函数仅仅是根据各种命令来修改“写地址”IO_ADDR_W的值(这称为tglx方法),这种方法使得平台/开发板相关的代码很简单。
    真正发出命令是在上一层NAND Flash的统一的驱动中实现,它首先调用这个函数修改“写地址”,然后才分别发出控制、地址、数据序列。
    */
    static void s3c2410_nand_hwcontrol(struct mtd_info *mtd,int cmd)
    {
    S3C2410_NAND * const s3c2410nand = S3C2410_GetBase_NAND();
    struct nand_chip *chip = mtd->priv;

    switch(cmd)
    {
    case NAND_CTL_SETNCE:
    case NAND_CTL_CLRNCE:
    printf("%s: called for NCE\n",__FUNCTION__);
    break;

    case NAND_CTL_SETCLE:
    chip->IO_ADDR_W = (void *)&s3c2410nand->NFCMD;
    break;

    case NAND_CTL_SETALE:
    chip->IO_ADDR_W = (void *)&s3c2410nand->NFADDR;
    break;

    /* NAND_CTL_CLRCLE */
    /* NAND_CTL_CLRALE */
    default:
    chip->IO_ADDR_W = (void *)&s3c2410nand->NFDATA;
    break;
    }
    }

    /*
    S3C2410: 查询NAND Flash状态
    返回值:0表示忙,1表示就绪
    */
    static int s3c2410_nand_devread(struct mtd_info *mtd)
    {
    S3C2410_NAND * const s3c2410nand = S3C2410_GetBase_NAND();

    return (s3c2410nand->NFSTAT & S3C2410_NFSTAT_READY);
    }

    /* S3C2440: NAND Flash的片选函数 */
    static void s3c2440_nand_select_chip(struct mtd_info *mtd,int chip)
    {
    S3C2440_NAND * const s3c2440nand = S3C2440_GetBase_NAND();

    if(chip == -1){
    s3c2440nand->NFCONT |= S3C2440_NFCONT_nFCE; //禁止片选信号
    }else{
    s3c2440nand->NFCONT &= ~S3C2440_NFCONT_nFCE; //使能片选信号
    }
    }

    /* S3C2440: 命令和控制函数,与s3c2440_nand_hwcontrol函数类似 */
    static void s3c2440_nand_hwcontrol(struct mtd_info *mtd,int cmd)
    {
    S3C2440_NAND * const s3c2440nand = S3C2440_GetBase_NAND();
    struct nand_chip *chip = mtd->priv;

    switch(cmd)
    {
    case NAND_CTL_SETNCE:
    case NAND_CTL_CLRNCE:
    printf("%s: called for NCE\n",__FUNCTION__);
    break;

    case NAND_CTL_SETCLE:
    chip->IO_ADDR_W = (void *)&s3c2440nand->NFCMD;
    break;

    case NAND_CTL_SETALE:
    chip->IO_ADDR_W = (void *)&s3c2440nand->NFADDR;
    break;

    /* NAND_CTL_CLRCLE */
    /* NAND_CTL_CLRALE */
    default:
    chip->IO_ADDR_W = (void *)&s3c2440nand->NFDATA;
    break;
    }
    }

    /*
    S3C2440: 查询NAND Flash状态
    返回值:0表示忙,1表示就绪
    */
    static int s3c2440_nand_devready(struct mtd_info *mtd)
    {
    S3C2440_NAND * const s3c2440nand = S3C2440_GetBase_NAND();

    return (s3c2440nand->NFSTAT & S3C2440_NFSTAT_READY);
    }

    /*
    NAND Flash 硬件初始化:
    设置NAND Flash 的时序,使能NAND Flash控制器
    */
    static void s3c2440_nand_inithw(void)
    {
    S3C2410_NAND * const s3c2410nand = S3C2410_GetBase_NAND();
    S3C2440_NAND * const s3c2440nand = S3C2440_GetBase_NAND();

    #define TACLS 0
    #define TWRPH0 4
    #define TWRPH1 2

    if(gd->bd->bi_arch_number == MACH_TYPE_SMDK2410)
    {
    //使能NAND Flash控制器,初始化ECC,使能片选信号,设置时序
    s3c2410nand->NFCONF = (1 << 15) | (1 << 12) | (1 << 11) | (TACLS << 8) | (TWRPH0 << 4) | (TWRPH1 << 0);
    }
    else
    {
    //设置时序
    s3c2440nand->NFCONF = (TACLS << 12) | (TWRPH0 << 8) | (TWRPH1 << 4);
    //初始化ECC,使能NAND Flash控制器,使能片选信号
    s3c2440nand->NFCONT = (1 << 4) | (0 << 1) | (1 << 0);
    }
    }

    /*
    被drivers/nand/nand.c调用,初始化NAND Flash硬件,初始化访问接口函数
    */
    void board_nand_init(struct nand_chip *chip)
    {
    S3C2410_NAND * const s3c2410nand = S3C2410_GetBase_NAND();
    S3C2440_NAND * const s3c2440nand = S3C2440_GetBase_NAND();

    s3c24x0_nand_inithw(); //NAND Flash 硬件初始化

    if(gd->bd->bi_arch_number == MACH_TYPE_SMDK2410){
    chip->IO_ADDR_R = (void *)&s3c2410nand->NFDATA;
    chip->IO_ADDR_W = (void *)&s3c2410nand->NFDATA;
    chip->hwconrol = s3c2410_nand_hwcontrol;
    chip->dev_ready = s3c2410_nand_devready;
    chip->select_chip = s3c2410_nand_select_chip;
    chip->options = 0;//设置位宽等,位宽为8
    }else{
    chip->IO_ADDR_R = (void *)&s3c2440nand->NFDATA;
    chip->IO_ADDR_W = (void *)&s3c2440nand->NFDATA;
    chip->hwconrol = s3c2440_nand_hwcontrol;
    chip->dev_ready = s3c2440_nand_devready;
    chip->select_chip = s3c2440_nand_select_chip;
    chip->options = 0;//设置位宽等,位宽为8
    }

    chip->eccmode = NAND_ECC_SOFT; //ECC校验方式:软件ECC
    }
    #endif
    文件中分别针对S3C2410、S3C2440实现了NAND Flash最底层访问函数,并进行了一些硬件的设置(比如时序、使能NAND Flash控制器等)。新的代码对NAND Flash的封装做的很好,只要向上提供底层初始化函数board_nand_init来设置好平台/开发板的相关的初始化、提供底层接口即可。
    最后,只要将新建的nand_flash.c文件编入U-Boot中就可以擦除、读写NAND Flash了。如下修改cpu/arm920t/s3c24x0/Makefile文件即可。
    修改前:
    1
    2
    COBJS = i2c.o interrupts.o serial.o speed.o \
    usb_ohci.o
    修改后:
    1
    2
    COBJS = i2c.o interrupts.o serial.o speed.o \
    usb_ohci.o nand_flash.o
    现在,可以使用新编译的U-Boot.bin烧写内核映像到NAND Flash中去了。

支持烧写yaffs文件系统映像

在实际生产中,可以通过烧片器等手段将内核、文件系统映像烧入固态存储设备中,Bootloader不需要具备烧写功能。但为了方便开发,通常在Bootloader中增加烧写内核、文件系统映像文件的功能。
增加了NAND Flash功能的U-Boot 1.1.6已经可以通过“nand write …”、“nand write.jffs2…”等命令来烧写内核,烧写cramfs、jffs2文件系统映像文件。但是在NAND Flash上,yaffs文件系统的性能更佳,下面增加“nand write.yaffs…”命令以烧写yaffs文件系统映像文件。
“nand write.yaffs…”字样的命令中,“nand”是具体命令,“write.yaffs…”是参数。nand命令在common/cmd_nand.c中实现。

1
2
3
4
5
6
7
8
9
U_BOOT_CMD(nand,5,1,do_nand,
"nand - NAND sub-system\n",
"info -show available NAND devices\n"
"nand device [dev] -show or set current device\n"
"nand read[.jffs2] -addr off|partition size \n"
"nand write[.jffs2] -addr off|partition size - read/write 'size' bytes starting\n"
"at offset 'off' to/from memory address 'addr'\n"
...
)

先在其中增加“nand write.yaffs…”的使用说明,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
U_BOOT_CMD(nand,5,1,do_nand,
"nand - NAND sub-system\n",
"info -show available NAND devices\n"
"nand device [dev] -show or set current device\n"
"nand read[.jffs2] -addr off|partition size \n"
"nand write[.jffs2] -addr off|partition size - read/write 'size' bytes starting\n"
"at offset 'off' to/from memory address 'addr'\n"
"nand read.yaffs addr off size - read the 'size' byte yaffs image starting\n"
"at offset 'off' to memory address 'addr'\n"
"nand write.yaffs addr off size - write the 'size' byte yaffs image starting\n"
"at offset 'off' from memory address 'addr'\n"
...
)

然后,在nand命令的处理函数do_nand中增加对“write.yaffs…”的支持。do_nand函数仍在common/cmd_nand.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
else if(s != NULL && !strcmp(s,".yaffs")){
if(read){
/*read*/
nand_read_options_t opts;
memset(&opts,0,sizeof(opts));
opts.buffer = (u_char *)addr;
opts.length = size;
opts.offset = off;
opts.readoob = 1;
opts.quiet = quiet;
ret = nand_read_opts(nand,&opts);
}else{
/*write*/
nand_write_options_t opts;
memset(&opts,0,sizeof(opts));
opts.buffer = (u_char *)addr; //yaffs 文件系统映像存放的地址
opts.length = size; //长度
opts.offset = off; //要烧写到的NAND Flash的偏移地址
/* opts.foreceyaffs = 1 */ //计算ECC码的方法,没有使用
opts.noecc = 1; //不需要计算ECC,yaffs映像中有OOB数据
opts.writeoob = 1; //写OOB区
opts.blockalign = 1; //每个“逻辑块”大小为1个“物理块”
opts.quiet = quiet; //是否打印提示信息
opts.skipfirstblk = 1; //跳过第一个可用块
ret = nand_write_opts(nand,&opts);
}
}

下面开始分析这段代码,即“nand write.yaffs…”命令的实现。
NAND Flash 每一页大小为(512 + 16)字节(还有其他格式的NAND Flash,比如每页大小为(256 + 8)、(2048 + 64)等),其中的512字节就是一般存储数据的区域,16字节称为OOB(Out Of Band)区。通常在OOB区存放坏块标记、前面512字节的ECC校验码等。
cramfs、jffs2文件系统映像文件中没有OOB区的内容,如果将它们烧入NOR Flash中,则是简单的“平铺”关系;如果将它们烧入NAND Flash中,则NAND Flash的驱动程序首先根据OOB的标记略过坏块,然后将一页数据(512字节)写入后,还会计算这512字节的ECC校验码,最会将它写入OOB区,如此循环。cramfs、jffs2文件系统映像文件的大小通常是512的整数倍。
而yaffs文件系统映像文件的格式则跟它们不同,文件本身就包含了OOB区的数据(里面有坏块标记、ECC校验码、其他yaffs相关的信息)。所以烧写时,不需要再计算ECC值,首先检查是否坏块(是则跳过),然后写入512字节的数据,最后写入16字节的OOB数据,如此循环。yaffs文件系统映像文件的大小是(512 + 16)的整数倍。
第16-18行设置源地址、目的地址、长度。烧写yaffs文件系统映像之前,一般通过网络将它下载到内存的某个地址处,比如0x30000000,然后通过类似“nand write.yaffs 0x30000000 0x00A00000 $(filesize)”的命令烧到NAND Flash的偏移地址0x00A00000处。对于这个命令,第16行中的“opts.buffer = 0x30000000”,第17行中的“opts.length = $(filesize)”,就是前面下载文件的大小,第18行中的“opts.offset = 0x00A00000”。
这里列出不使用的第19行,是因为“opts.forceyaffs”这个名字,很有欺骗性,它其实是指计算ECC校验码的一种方法。烧写yaffsw文件系统映像时,不需要计算ECC校验码。
第20-21行指定烧写数据时不计算ECC校验码,而是烧入文件中的OOB数据。
第22行指定“逻辑块”的大小,“逻辑块”可以由多个“物理块”组成,在yaffs文件系统映像中,它们是1:1的关系。
第24行的“opts.skipfirstblk”是新加的项,nand_write_options_t结构中没有skipfirstblk成员。它表示烧写时跳过第一个可以的逻辑块,这是由yaffs系统的特性决定的。
既然skipfirstblk是在nand_write_options_t结构中新加的项,那么就要重新定义nand_write_options_t结构,并在下面调用的nand_write_opts函数中对它进行处理。
首先在include/nand.h中进行如下修改,增加skipfirstblk成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct nand_write_options {
u_char *buffer; //memory block containing image to write
ulong length; //number of bytes to write
ulong offset; //start address in NAND
int quiet; //dont display progress message
int autopalce; //if true use auto oob layout
int forcejffs2; //force jffs2 oob layout
int forceyaffs; //force yaffs oob layout
int noecc; //write without ecc
int writeoob; //image contain oob data
int pad; //pad to page size
int blockalign; //1 | 2 | 4 set multiple of eraseblocks to align to
int skipfirstblk; //新加,烧写时跳过第一个可用的逻辑块
};
typedef struct nand_write_options nand_write_options_t;

然后,修改nand_write_opts函数,增加对skipfirstblk成员的支持。它在drivers/nand/nand_utils.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int nand_write_opts(nand_info_t *meminfo,const nand_write_options_t *opts)
{
int result;
int skipfirstblk = opts->skipfirstblk;

while(blockstart != (mtdoffset & (~erasesize_blockalign + 1))){
... ...

if(baderaseblcok){
mtdoffset = blockstart + erasesize_blockalign;
}
... ...
}
}

进行上面的移植之后,U-Boot已经支持烧写yaffs文件系统映像了。由于前面设置“opts.noecc = 1”不使用ECC校验码,在烧写过程中会出现很多的提示信息。

1
Writing data without ECC to NAND-FLASH is not recommended

修改前:

1
2
case NAND_ECC_NONE:
printk(KERNEL_WARING"Writing data without ECC to NAND-FLASH is not recommended\n");

修改后:

1
2
case NAND_ECC_NONE:
// printk(KERNEL_WARING"Writing data without ECC to NAND-FLASH is not recommended\n");

修改默认配置参数以方便使用

前面移植网卡芯片CS8900时,已经设置过默认IP地址等。为了使用U-Boot时减少一些设置,现在修改配置文件include/configs/100ask24x0.h,增加默认配置参数,其中一些在移植过程中已经增加的选项这里也再次说明。

  1. Linux启动参数
    增加如下3个宏:
    1
    2
    3
    4
    #define CONFIG_SETUP_MEMORY_TAGS        1       //向内核传递内存分布信息
    #define CONFIG_CMDLINE_TAG 1 //向内核传递命令行参数
    //默认命令行参数
    #define CONFIG_BOOTTAGS "noinited root=/dev/mtdblack 2 init=/linuxrc console=ttySAC0"
  2. 自动启动命令
    增加如下2个宏:
    1
    2
    3
    4
    //自动启动前延时3s
    #define CONFIG_BOOTDELAY 3
    //自动启动的命令
    #define CONFIG_BOOTCOMMAND "nboot 0x32000000 0 0; bootm 0x32000000"
    自动启动时(开机3s内无输入),首先执行“nboot 0x32000000 0 0”命令将第0个NAND Flash的偏移地址0上的映像文件复制到内存0x32000000中;然后执行“bootm 0x32000000”命令启动内存中的映像。
  3. 默认网络设置
    根据具体网络环境增加、修改下面4个宏
    1
    2
    3
    4
    #define CONFIG_ETHADDR  08:00:3e:26:0a:5b
    #define CONFIG_NETMASK 255.255.255.0
    #define CONFIG_IPADDR 192.168.1.17
    #define CONFIG_SERVERIP 192.168.1.11

U-Boot的常用命令

U-Boot的常用命令的用法

进入U-Boot的控制界面之后,可以运行各种命令,比如下载文件到内存,擦除、读写Flash,运行内存、NOR Flash、NAND Flash中的程序,查看、修改、比较内存中的数据等。
使用各种命令时,可以使用其开头的若干字母代替它。比如tftpboot命令,可以使用t、tf、tft、tftp等代替,只要其他命令不以这些字母开头即可。
当运行一个命令之后,如果它是可重复执行的(代码中使用U_BOOT_CMD定义这个命令时,第3个参数是1),若想再次运行可以直接输入回车。
U-Boot接受的数据都是十六进制,输入时可以省略前缀0x、0X。
下面介绍常用的命令:

  1. 帮助命令help
    运行help命令可以看到U-Boot中所有命令的作用,如果要查看某个命令的使用方法,运行“help 命令名”,比如“help bootm”。
    可以使用“?”来代替help,比如直接输入“?”、“?bootm”。

  2. 下载命令
    U-Boot支持串口下载、网络下载,相关命令有:loadb、loads、loadx、loady和tftpboot、nfs。
    前几个串口下载命令使用方法相似,以loadx命令为例,它的用法为“loadx [off] [baud]”。“[]”表示里面的参数可以省略,off表示文件下载后存放的内存地址,baud表示使用的波特率。如果baud参数省略,则使用当前的波特率;如果off参数省略,存放的地址为配置文件中定义的宏CFG_LOAD_ADDR。
    tftpboot命令使用TFTP协议从服务器下载文件,服务器的IP地址为环境变量serverip。用法为“tftpboot [loadAddress] [bootfilename]”,loadAddress表示文件下载后存放的内存地址,bootfilename表示要下载的文件的名称。如果loadAddress省略,存放的地址为配置文件中定义的宏CFG_LOAD_ADDR;如果bootfilename省略,则使用开发板的IP地址构造一个文件名,比如开发板IP为192.168.1.17,则默认的文件名为C0A80711.img。
    nfs命令使用NFS协议下载文件,用法为“nfs [loadAddress] [host ip addr:bootfilename]”。“loadAddress、bootfilename”的意义与tftpboot命令一样,“host ip addr”表示服务器的IP地址,默认为环境变量serverip。
    下载文件成功后,U-Boot会自动创建或更新环境变量filesize,它表示下载的文件的长度,可以在后续命令中使用”$(filesize)“来引用它。

  3. 内存操作命令
    常用的命令有:查看内存命令md、修改内存命令mm、填充内存命令mw、复制命令cp。这些命令都可以带上后缀“.b”、“.w”或“.l”,表示以字节、字(2个字节)、双字(4个字节)为单位进行操作。比如“cp.l 30000000 31000000 2”将从开始地址0x3000000处,复制2个双字到开始地址为0x31000000的地方。
    md命令用法为“md[.b,.w,.l] address [count]”,表示以字节、字或双字(默认为双字)为单位,显示地址address开始的内存数据,显示的数据个数为count。
    mm命令用法为“mm[.b,.w,.l] address ”,表示以字节、字或双字(默认为双字)为单位,从地址address处开始修改内存数据。执行命令mm后,输入新数据后回车,地址会自动增加,按“Ctrl + C”键退出。
    mw命令用法为“mw[.b,.w,.l] address value [count]”,表示以字节、字或双字(默认为双字)为单位,往开始地址为address的内存中填充count个数据,数据值为value。
    cp命令用法为“cp[.b,.w,.l] source target count”,表示以字节、字或双字(默认为双字)为单位,往源地址source的内存复制count个数据到目的地址的内存。

  4. NOR Flash操作命令
    常用的命令有查看Flash信息的info命令、加/解写保护命令protect、擦除命令erase。由于NOR Flash的接口与一般内存相似,所以一些内存命令可以在NOR Flash上使用,比如读NOR Flash时可以使用md、cp命令,写NOR Flash时可以使用cp命令(cp根据地址分辨出是NOR Flash,从而调用NOR Flash驱动完成写操作)。
    直接运行“flinfo”即可看到NOR Flash的信息,有NOR Flash的型号、容量、各扇区的开始地址、是否只读等信息。比如对于本书基于的开发板,flinfo命令的结果如下:

    1
    2
    3
    4
    5
    6
    7
    Bank # 1: AMD: 1x Amd29LV800BB(8Mbit)
    Size: 1 MB in 19 Sectors
    Sector Start Address:
    00000000 (RO) 00004000 (RO) 00006000 (RO) 00008000 (RO) 00010000 (RO)
    00020000 (RO) 00030000 00040000 00050000 00060000
    00070000 00080000 00090000 000A0000 000B0000
    000C0000 000D0000 000E0000 000F0000 (RO)

    其中的RO表示该扇区处于写保护状态,只读。
    对于只读的扇区,在擦除、烧写它之前,要先接触写保护。最简单的命令为“protect off all”,解除所有的NOR Flash的写保护。
    erase命令常用的格式为“erase start end”,擦除的地址范围为startend;“erase start + len”,擦除的地址范围为start(start + len - 1);“erase all”,表示擦除所有NOR Flash。

  5. NAND Flash操作命令
    NAND Flash操作命令只有一个:nand,它根据不同的参数进行不同操作,比如擦除、读取、烧写等。
    “nand info”查看NAND Flash信息。
    “nand erase [clean] [off size]”擦除NAND Flash。加上“clean”时,表示在每个块的第一个扇区的OOB区写入清除标记;off、size表示要擦除的开始偏移地址的长度,如果省略off和size,表示要擦除整个NAND Flash。
    “nand read [.jffs2] addr off size”从NAND Flash偏移地址off处读出size个字节的数据存放到开始地址为addr的内存中。是否加后缀“.jffs”的差别只是读操作时的ECC校验方法不同。
    “nand write [.jffs2] addr off size”把开始地址为addr的内存中的size个字节数据写到NAND Flash的偏移地址off处。是否加后缀“.jffs”的差别只是写操作时的ECC校验方法不同。
    “nand read.yaffs addr off size”从NAND Flash偏移地址off处读出size个字节的数据(包括OOB区域),存放到开始地址为addr的内存中。
    “nand write.yaffs addr off size”把开始地址为addr的内存中的size个字节数据(其中有要写入OOB区域的数据)写到NAND Flash的偏移地址off处。
    “nand dump off”将NAND Flash偏移地址off的一个扇区的数据打印出来,包括OOB数据。

  6. 环境变量命令
    “printenv”命令打印全部环境变量,“printenv name1 name2……”打印名字为name1、name2、…的环境变量。
    “setenv name value”设置名字为name的环境变量的值为value。
    “setenv name”删除名字为name的环境变量。
    上面的设置、删除操作只是在内存中进行,“saveenv”将更改后的所有环境变量写入NOR Flash中。

  7. 启动命令
    不带参数的“boot”、“bootm”命令都是执行环境变量bootcmd所指定的命令。
    “bootm [addr [arg…]]”命令启动存放在地址addr处的U-Boot格式的映像文件(使用U-Boot目录tools下的mkimage制作工具得到),[arg…]表示参数。如果addr参数省略,映像文件所在地址为配置文件中定义的宏CFG_LOAD_ADDR。
    “go addr [arg…]”与bootm命令类似,启动存放在地址addr处的二进制文件,[arg…]表示参数。
    “nboot [[[loadAddr]dev]offset]”命令将NAND Flash设备dev上偏移地址off处的映像文件复制到内存loadAddr处,然后,如果环境变量autostart的值为“yes”,就启动这个映像。如果loadAddr参数省略,存放地址为配置文件中定义的宏CFG_LOAD_ADDR;如果dev参数省略,则它的取值为环境变量bootdevice的值;如果offset参数省略,则默认为0。

U-Boot命令使用实例

下面通过一个例子来演示如何使用各种命令烧写内核映像文件,yaffs映像文件,并启动系统。

  1. 制作内核映像文件
    对于本书使用的Linux2.6.22.6版本,编译内核时可以直接生成U-Boot格式的映像文件uImage。
    对于不能直接生成uImage的内核,制作方法在U-Boot根目录下的README文件中有说明,假设已经编译好的内核文件为vmlimux,它是ELF格式的。mkimage是U-Boot命令tools下的工具,它在编译U-Boot是自动生成。执行以下3个命令将内核文件vmlinux制作为U-Boot格式的映像文件uImage,它们首先将vmlinux转换为二进制格式,然后压缩,最后构造头部信息(里面包含文件名称、大小、类型、CRC校验码等),如下所示:

    1
    2
    3
    arm-linux-objcopy -O binary -R .note -R .comment -S vmlinux linux.bin
    gzip -9 linux.bin
    mkimage -A arm -O linux -T kernel -C gzip -a 0x30008000 -e 0x30008000 -n "Linux Kernel Image" -d linux.bin.gz uImage
  2. 烧写内核映像文件uImage
    首先将uImage放在主机上的tftp或nfs目录下,确保已经开启tftp或nfs服务。
    然后运行如下命令下载文件,擦除、烧写NAND Flash,如下所示:

    1
    2
    3
    tftp 0x30000000 uImage 或nfs 0x30000000 192.168.1.57:/work/nfs_root/uImage
    nand erase 0x0 0x00200000
    nand write.jffs2 0x30000000 0x0 $(filesize)

    第3条命令之所以使用“nand write.jffs2”而不是“nand write”,是因为前者不要求文件的长度是页对齐的(512字节对齐)。也可以使用“nand write”,但是需要将命令中的长度参数改为$(filesize)向上进行512取整(513向上取整得1024)。比如uImage的大小为1540883,向上进行取整为1541120(0x178400),可以使用命令“nand write 0x30000000 0x0 0x178400”进行烧写。

  3. 烧写yaffs文件系统映像
    假设yaffs文件系统映像的文件名为yaffs.img,首先将它放在主机上的tftp或nfs目录下,确保已经开启或nfs服务;然后执行如下命令下载、擦除、烧写,如下所示:

    1
    2
    3
    tftp 0x30000000 yaffs.img 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/yaffs.img
    nand erase 0xA00000 0x3600000
    nand write.yaffs 0x30000000 0xA00000 $(filesize)

    这时,重启系统,在U-Boot倒数3s之后,就会自动启动Linux系统。

  4. 烧写jffs2文件系统映像
    假设jffs2文件系统映像的文件名为jffs2.img,首先将它放在主机上的tftp或nfs目录下,确保已经开启tftp或nfs服务;然后执行如下命令下载、擦除、烧写,如下所示。

    1
    2
    3
    tftp 0x30000000 jffs2.img 或 nfs 0x30000000 192.168.1.57/work/nfs_root/jffs2.img
    nand erase 0x200000 0x800000
    nand write.jffs2 0x30000000 0x200000 $(filesize)

    系统启动后,就可以使用“mount -t jffs2 /dev/mtdblock1 /mnt”挂载jffs2文件系统。

使用U-Boot来执行程序

之前章节的实例使用JTAG烧写程序到NAND Flash,烧写过程十分缓慢。如果使用U-Boot来烧写NAND Flash,效率会高很多。烧写二进制文件到NAND Flash中所使用的命令与上面烧写内核映像文件uImage的过程类似,只是不需要将二进制文件制作成U-Boot格式。
另外,可以将程序下载到内存中,然后使用go命令执行它。假设有一个程序的二进制可执行文件test.bin,连接地址为0x30000000。首先将它放在主机上的tftp和nfs目录下,确保已经开启tftp或nfs服务;然后将它下载到内存0x30000000处,最后使用go命令执行它。

1
2
tftp 0x3000000 test.bin 或 nfs 0x30000000 192.168.1.57:/work/nfs_root/test.bin
go 0x30000000