简介:
本文主要介绍在u-boot-1.1.6中代码的运行过程,以此来了解在u-boot中如何实现引导并启动内核。这里我们就要介绍u-boot第二阶段的代码了。而第二阶段的代码主要就是填充gd_t结构体,并做一些其他硬件的设置。而这里对gd_t结构体的填充以及对硬件的设置则是在为下面的第三阶段代码做铺垫。
声明:
本文主要是看了韦东山老师的视频后所写,希望对你有所帮助。
u-boot版本 : u-boot-1.1.6
开发板 : JZ2440
Linux内核 : Linux-2.6.22.6
设置硬件以及填充gd_t结构体 :
我们在这里做的一些硬件的设置,其实也包括对一些硬件的初始化,以及对一些硬件做进一步的设置来让他们为CPU提供更多的功能。而在程序中我们以一个函数指针加for循环的形式来对每个硬件进行初始化。而详细的代码为:
typedef int (init_fnc_t) (void);
init_fnc_t **init_fnc_ptr;
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
上面代码就是对init_sequence列表中的各个函数进行的初始化或者设置,在这里我要说明的有两点,第一是上面这种代码的使用。第二个是init_sequence列表。
我们先说代码的使用,这里用typedef定义了一种init_fnc_t的变量,只不过这种变量只能对函数使用。而我们比较常见的定义函数变量的形式是函数指针,他的形式为 :
typedef int (init_fnc_t*) (void);
而当函数指针指向某个函数并执行这个函数时,用下面的表达式 :
init_fnc_t ();
而定义init_fnc_t变量的形式其实与我们上面函数指针的形式相似。但由于init_fnc_t变量要实现在列表中函数地址的变化,即当我们执行完上一个操作函数后,通过init_fnc_t型指针的加一来实现指向下一个操作函数的功能,这里就不能简单的使用一个单一的指针了。因为在上面函数指针的例子中我们不能实现指针的移动(即在不同操作函数之间跳转)。而要实现指针的移动我们就要使用双重指针,即指向指针的指针。也就是我们在上文中所使用的 :
init_fnc_t **init_fnc_ptr;
而当我们像函数指针一样实现函数调用时,我们要做的其实就是为这个双重指针去一重指针。即文中的:
(*init_fnc_ptr)() ;
我想讲到这里大家对这种方式的代码使用了解了吧。而在文中又由于init_sequence被定义为:init_fnc_t *init_sequence[] 是指向数组的指针,其实就可以理解为指向指针的指针。所以我们可以用init_fnc_ptr直接指向init_sequence,而现在我们看init_sequence其实更容易理解。因为init_sequence可以理解为函数指针的数组,而*init_fnc_ptr是数组中的一项。而++init_fnc_ptr表示的是数组地址的增加。好了讲了这么多,确实也很绕,希望大家可以理解。
下面我们看一下init_sequence列表:
init_fnc_t *init_sequence[] = {
cpu_init, /* basic cpu dependent setup */
board_init, /* basic board dependent setup */
interrupt_init, /* set up exceptions */
env_init, /* initialize environment */
init_baudrate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /* stage 1 init of console */
display_banner, /* say that we are here */
#if defined(CONFIG_DISPLAY_CPUINFO)
print_cpuinfo, /* display cpu info (and speed) */
#endif
#if defined(CONFIG_DISPLAY_BOARDINFO)
checkboard, /* display board info */
#endif
dram_init, /* configure available RAM banks */
display_dram_config,
NULL,
};
前面我们已经说了这是个函数指针数组,所以可以知道数组中各个函数的基本形式为:没有输入参数,而输出整型参数,并且0表示下面还有函数,而非0表示下面没有函数。接下来我们介绍init_sequence列表中一些重要的设置函数。
cpu_init : 内核中各存储空间大小的设置
int cpu_init (void)
{
/*
* setup up stacks if necessary
*/
#ifdef CONFIG_USE_IRQ
IRQ_STACK_START = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4;
FIQ_STACK_START = IRQ_STACK_START - CONFIG_STACKSIZE_IRQ;
FREE_RAM_END = FIQ_STACK_START - CONFIG_STACKSIZE_FIQ - CONFIG_STACKSIZE;
FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
#else
FREE_RAM_END = _armboot_start - CFG_MALLOC_LEN - CFG_GBL_DATA_SIZE - 4 - CONFIG_STACKSIZE;
FREE_RAM_SIZE = FREE_RAM_END - PHYS_SDRAM_1;
#endif
return 0;
}
我们看代码会发现其实这里主要是定义了一些栈空间大小以及RAM的结束地址和大小。也可能是我没有看懂,我觉得这里的设置与CPU并没有直接的关系。不知道他为什么会将这个函数命名为cpu_init ??
board_init : 单板相关的设置
int board_init (void)
{
S3C24X0_CLOCK_POWER * const clk_power = S3C24X0_GetBase_CLOCK_POWER();
S3C24X0_GPIO * const gpio = S3C24X0_GetBase_GPIO();
/* set up the I/O ports */
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;
/* support both of S3C2410 and S3C2440, by www.100ask.net */
if (isS3C2410)
{
/* arch number of SMDK2410-Board */
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
}
else
{
/* arch number of SMDK2440-Board */
gd->bd->bi_arch_number = MACH_TYPE_S3C2440;
}
/* adress of boot parameters */
gd->bd->bi_boot_params = 0x30000100;
return 0;
}
从上面代码知道这里主要是对GPIO端口的设置,同时分别对2440和2410单板的机器ID进行设置。最后是对TAG参数的地址进行的设置。而关于单板机器ID和TAG参数的设置我在文章:嵌入式Linux——写jz2440BootLoader的第二阶段代码 中已经做了详细的说明,这里不再细说,只是在这里强调一下这两个参数在后面很重要。而我们从上面的代码就可以看出这里就有了对gd_t结构体的填充了。
interrupt_init : 中断相关的设置
int interrupt_init (void)
{
S3C24X0_TIMERS * const timers = S3C24X0_GetBase_TIMERS();
/* use PWM Timer 4 because it has no output */
/* prescaler for Timer 4 is 16 */
timers->TCFG0 = 0x0f00;
if (timer_load_val == 0)
{
/*
* for 10 ms clock period @ PCLK with 4 bit divider = 1/2
* (default) and prescaler = 16. Should be 10390
* @33.25MHz and 15625 @ 50 MHz
*/
timer_load_val = get_PCLK()/(2 * 16 * 100);
}
/* load value for 10 ms timeout */
lastdec = timers->TCNTB4 = timer_load_val;
/* auto load, manual update of Timer 4 */
timers->TCON = (timers->TCON & ~0x0700000) | 0x600000;
/* auto load, start Timer 4 */
timers->TCON = (timers->TCON & ~0x0700000) | 0x500000;
timestamp = 0;
return (0);
}
这段代码更应该说是对定时器的设置(定时器中断只是各个中断中的一种),而不应该说是对中断的设置。从上面的代码我们还可以看出这是设置了一个10ms自动重载的定时器4 。而具体的设置就是向定时器中断控制寄存器中写入预设值。其中函数:S3C24X0_GetBase_TIMERS() 就是获得定时器中断寄存器首地址的函数。而对于预设值的确定就要大家看2440 的芯片手册了。
env_init :环境变量相关的初始化
#ifdef ENV_IS_EMBEDDED
extern uchar environment[];
env_t *env_ptr = (env_t *)(&environment[0]);
#else /* ! ENV_IS_EMBEDDED */
env_t *env_ptr = 0;
#endif /* ENV_IS_EMBEDDED */
#define CONFIG_BOOTARGS "noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0"
static uchar default_environment[] = {
#if defined(CONFIG_BOOTARGS)
"bootargs=" CONFIG_BOOTARGS "\0"
#endif
int env_init(void)
{
#if defined(ENV_IS_EMBEDDED)
ulong total;
int crc1_ok = 0, crc2_ok = 0;
env_t *tmp_env1, *tmp_env2;
total = CFG_ENV_SIZE;
tmp_env1 = env_ptr;
tmp_env2 = (env_t *)((ulong)env_ptr + CFG_ENV_SIZE);
crc1_ok = (crc32(0, tmp_env1->data, ENV_SIZE) == tmp_env1->crc);
crc2_ok = (crc32(0, tmp_env2->data, ENV_SIZE) == tmp_env2->crc);
if (!crc1_ok && !crc2_ok)
gd->env_valid = 0;
else if(crc1_ok && !crc2_ok)
gd->env_valid = 1;
else if(!crc1_ok && crc2_ok)
gd->env_valid = 2;
else {
/* both ok - check serial */
if(tmp_env1->flags == 255 && tmp_env2->flags == 0)
gd->env_valid = 2;
else if(tmp_env2->flags == 255 && tmp_env1->flags == 0)
gd->env_valid = 1;
else if(tmp_env1->flags > tmp_env2->flags)
gd->env_valid = 1;
else if(tmp_env2->flags > tmp_env1->flags)
gd->env_valid = 2;
else /* flags are equal - almost impossible */
gd->env_valid = 1;
}
if (gd->env_valid == 1)
env_ptr = tmp_env1;
else if (gd->env_valid == 2)
env_ptr = tmp_env2;
#else /* ENV_IS_EMBEDDED */
gd->env_addr = (ulong)&default_environment[0];
gd->env_valid = 1;
#endif /* ENV_IS_EMBEDDED */
return (0);
}
从上面的代码可以看出,这里主要是设置了单板的环境变量(或者更确切的说是启动参数),而在上面程序中起主要控制作用的是宏:ENV_IS_EMBEDDED ,程序通过这个宏来控制我们的环境变量是使用默认设置的环境变量,还是使用我们自己通过u-boot设置的环境变量。而我们默认的环境变量为: noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0 。同样上面函数中将与环境变量相关的设置填入到了gd_t相关的项中。
init_baudrate :设置波特率
#define CONFIG_BAUDRATE 115200
static int init_baudrate (void)
{
char tmp[64]; /* long enough for environment variables */
int i = getenv_r ("baudrate", tmp, sizeof (tmp));
gd->bd->bi_baudrate = gd->baudrate = (i > 0)
? (int) simple_strtoul (tmp, NULL, 10)
: CONFIG_BAUDRATE;
return (0);
}
这里的代码的意思是设置串口的波特率,我们知道在嵌入式系统中串口的信息交互方式是我们主要的人机交互方式,而串口的波特率又是决定串口传输数据快慢的重要保障。因此这里波特率相关的设置很重要。而这个波特率的设置有两种方式,第一是通过环境变量的设置,如果环境变量中对此有设置,则使用环境变量中的波特率,否则则使用默认的波特率。在本单板上的默认波特率为115200 。而这里关于波特率的设置其实就是将波特率的值写入到gd_t结构体波特率对应的选项。
dram_init : 设置内存的起始地址和内存的大小
#define CONFIG_NR_DRAM_BANKS 1 /* we have 1 bank of DRAM */
#define PHYS_SDRAM_1 0x30000000 /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE 0x04000000 /* 64 MB */
int dram_init (void)
{
gd->bd->bi_dram[0].start = PHYS_SDRAM_1;
gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;
return 0;
}
从上面的代码我们看出来内核的起始地址为 :0x30000000 。而内存的大小为 : 0x04000000 /* 64 MB */ 。同样我们这里是将SDRAM中的相关信息写入到gd_t结构体中。
讲了这么多,其实仔细的你会从上面的代码中发现,不管是在初始化列表还是在下面的代码中,我们所做的操作其实就是填充gd_t这个结构体,其实我们在这阶段中主要做的工作就是根据不同的CPU来完成对其gd_t描述的填写。而填写完这个结构体后我们在后面的代码中会经常的使用在gd中放入的参数。所以在这里了解一下gd_t结构体中的内容还是很重要的。
typedef struct global_data {
bd_t *bd;
unsigned long flags;
unsigned long baudrate;
unsigned long have_console; /* serial_init() was called */
unsigned long reloc_off; /* Relocation Offset */
unsigned long env_addr; /* Address of Environment struct */
unsigned long env_valid; /* Checksum of Environment valid? */
unsigned long fb_base; /* base address of frame buffer */
void **jt; /* jump table */
} gd_t;
而在gd_t中,最重要的就是bd结构体了,而bd就是与单板相关的各种设置。
typedef struct bd_info {
int bi_baudrate; /* 串口控制台波特率 */
unsigned long bi_ip_addr; /* IP地址 */
unsigned char bi_enetaddr[6]; /* Ethernet地址 */
struct environment_s *bi_env; /* 环境变量 */
ulong bi_arch_number; /* 单板ID */
ulong bi_boot_params; /* where this board expects params */
struct /* RAM的参数 */
{
ulong start; /* 起始地址 */
ulong size; /* RAM大小 */
} bi_dram[CONFIG_NR_DRAM_BANKS];
#ifdef CONFIG_HAS_ETH1
/* second onboard ethernet port */
unsigned char bi_enet1addr[6];
#endif
} bd_t;
其实start_armboot函数主要就是对gd结构体的填充,例如下面的对于gd机构体中网络信息的填充:
/* IP Address */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");
/* MAC Address */
{
int i;
ulong reg;
char *s, *e;
char tmp[64];
i = getenv_r ("ethaddr", tmp, sizeof (tmp));
s = (i > 0) ? tmp : NULL;
for (reg = 0; reg < 6; ++reg) {
gd->bd->bi_enetaddr[reg] = s ? simple_strtoul (s, &e, 16) : 0;
if (s)
s = (*e) ? e + 1 : e;
}
}
讲完上面的关于gd_t结构体相关的设置,下面我们就要讲关于nor flash和nand flash的设置了。这两个的设置将直接影响u-boot的存放位置和内核的启动方式。
flash_init : 配置可用的flash
ulong flash_init (void)
{
int i, j;
ulong size = 0;
for (i = 0; i < CFG_MAX_FLASH_BANKS; i++) {
ulong flashbase = 0;
flash_info[i].flash_id =
(AMD_MANUFACT & FLASH_VENDMASK) |
(AMD_ID_LV400B & FLASH_TYPEMASK);
flash_info[i].size = FLASH_BANK_SIZE;
flash_info[i].sector_count = CFG_MAX_FLASH_SECT;
memset (flash_info[i].protect, 0, CFG_MAX_FLASH_SECT);
if (i == 0)
flashbase = PHYS_FLASH_1;
else
panic ("configured too many flash banks!\n");
for (j = 0; j < flash_info[i].sector_count; j++) {
if (j <= 3) {
/* 1st one is 16 KB */
if (j == 0) {
flash_info[i].start[j] =
flashbase + 0;
}
/* 2nd and 3rd are both 8 KB */
if ((j == 1) || (j == 2)) {
flash_info[i].start[j] =
flashbase + 0x4000 + (j -
1) *
0x2000;
}
/* 4th 32 KB */
if (j == 3) {
flash_info[i].start[j] =
flashbase + 0x8000;
}
} else {
flash_info[i].start[j] =
flashbase + (j - 3) * MAIN_SECT_SIZE;
}
}
size += flash_info[i].size;
}
flash_protect (FLAG_PROTECT_SET,
CFG_FLASH_BASE,
CFG_FLASH_BASE + monitor_flash_len - 1,
&flash_info[0]);
flash_protect (FLAG_PROTECT_SET,
CFG_ENV_ADDR,
CFG_ENV_ADDR + CFG_ENV_SIZE - 1, &flash_info[0]);
return size;
}
而我们观察flash_init函数中的代码,你会发现上面代码的主要内容是在填充flash_info这个结构体,而在这个结构体中包含了一个nor flash应该提供的内容,如一个nor flash的总大小,他的擦除块的大小,他的开始地址,以及他写保护的状态等。而这些正是你单板的nor flash区别于其他单板nor flash的重要特性。而关于nor flash的特性我们可以看结构体:flash_info_t来了解。
typedef struct {
ulong size; /* 区块的总大小(用字节表示) */
ushort sector_count; /* 擦除单元的个数 */
ulong flash_id; /* 厂家ID和设置ID */
ulong start[CFG_MAX_FLASH_SECT]; /* 物理扇区开始地址 */
uchar protect[CFG_MAX_FLASH_SECT]; /* 扇区的保护状态 */
#ifdef CFG_FLASH_CFI
uchar portwidth; /* 端口宽度 */
uchar chipwidth; /* the width of the chip */
ushort buffer_size; /* # of bytes in write buffer */
ulong erase_blk_tout; /* maximum block erase timeout */
ulong write_tout; /* maximum write timeout */
ulong buffer_write_tout; /* maximum buffer write timeout */
ushort vendor; /* the primary vendor id */
ushort cmd_reset; /* vendor specific reset command */
ushort interface; /* used for x8/x16 adjustments */
ushort legacy_unlock; /* support Intel legacy (un)locking */
uchar manufacturer_id; /* manufacturer id */
ushort device_id; /* device id */
ushort device_id2; /* extended device id */
ushort ext_addr; /* extended query table address */
ushort cfi_version; /* cfi version */
ushort cfi_offset; /* offset for cfi query */
ulong addr_unlock1; /* unlock address 1 for AMD flash roms */
ulong addr_unlock2; /* unlock address 2 for AMD flash roms */
const char *name; /* human-readable name */
#endif
} flash_info_t;
nand_init : nand flash的初始化
上面我们看了对nor 的设置,或者更确切的说是对flash_info_t结构体的填充。而现在我们来介绍对nand的初始化,我们知道nor在读取数值时与内存一样可以直接读取。所以我们在开发板上并没有集成nor的控制器,而对于nand就不一样了。我们要先初始化片内集成的nand控制器,而对于初始化nand的控制器其实就是初始化2440中相应的寄存器的值。
static inline void NF_Init(void)
{
#define TACLS 0
#define TWRPH0 4
#define TWRPH1 2
/* Set flash memory timing */
if (gd->bd->bi_arch_number == MACH_TYPE_SMDK2410)
{
NF_Conf((1<<15)|(0<<14)|(0<<13)|(1<<12)|(1<<11)|(TACLS<<8)|(TWRPH0<<4)|(TWRPH1<<0));
}
else
{
NF_Conf((3<<12)|(7<<8)|(7<<4));
}
NF_Reset();
}
void nand_init(void)
{
S3C2410_NAND * const nand = S3C2410_GetBase_NAND();
NF_Init();
printf ("%4lu MB\n", nand_probe((ulong)nand) >> 20);
}
我们对比于我前面文章:嵌入式Linux——写jz2440BootLoader的第一阶段代码 中对于初始化nand 的描述,你会发现他们的实质是一样的,都是向nand控制寄存器中写入特定的值,来实现对单板上nand 的初始化。而具体的寄存器中各个位表示的内容和具体的实现方法大家就要看一下2440的芯片手册了。
做完了上面的设置,就要进入main循环了,而进入之后我们就要进入另一个阶段了。我将这个阶段称为第三阶段。而在第三阶段我们就会进入u-boot的菜单界面来根据默认的boot参数或者是自己重新设置的boot参数来读取和启动内核了。