1.1、start_armboot函数简介
这个函数整个构成了uboot启动的第二阶段。
1.2、uboot第二阶段做的事情
uboot第一阶段主要就是初始化了SoC内部的一些部件(譬如看门狗、时钟、串口…),然后初始化DDR并且完成重定位。那么,uboot的第二阶段就是要初始化剩下的还没被初始化的硬件。主要是SoC外部硬件(譬如iNand、网卡芯片…)、以及uboot本身的一些东西(uboot的命令集、环境变量…),最终初始化完成后进入uboot的命令行准备接受命令、然后解析命令、执行命令。
1.3、uboot第二阶段完结于何处?
uboot启动后会自动打印出很多信息,这些信息是uboot第一阶段和第二阶段初始化时打印出来的调试信息。然后uboot倒数bootdelay秒然后执行bootcmd对应的启动命令,启动内核后,uboot就死掉了,在倒数bootdelay秒之前用户可以按下回车键打断uboot的自动启动进入uboot的命令行下,然后uboot就一直工作在命令行下。uboot的命令行是一个死循环,在start_armboot函数的最后可以看到,
for(;;)
mainloop();
循环体内不断重复:接收命令、解析命令、执行命令。这就是uboot最终的归宿。
2.1、init_fnc_t
(1)typedef int (init_fnc_t) (void); 这是一个函数类型
(2)init_fnc_ptr是一个二重函数指针,二重指针的作用有2个,第一个是用来指向一重指针,第二个是用来指向指针数组。后续得知init_fuc_ptr指向了一个函数指针数组,数组中的每个元素都是函数名。
2.2、DECLARE_GLOBAL_DATA_PTR
(1)#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm (“r8”)
定义了一个指针类型的全局变量名字叫gd,占4字节。用volatile修饰表示防止编译器优化,用register修饰表示这个变量要尽量放到寄存器中,后面的asm(“r8”)是gcc支持的一种语法,意思就是要把gd放到寄存器r8中。
这个全局变量gd(global data的简称)是一个结构体指针,里面有成员变量,每一个成员变量都可以看成是一个全局变量,在uboot程序中经常被访问,因此放在register中提升运行效率
(2)gd_t定义在include/asm-arm/global_data.h中。
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? *//*是布尔型,环境变量是否可以使用,环境变量从SDcard送到DDR中建立,需要一段过程*/
unsigned long fb_base; /* base address of frame buffer */ /*缓存地址*/
void **jt; /* jump table */ /*跳转表*/
} gd_t;
typedef struct bd_info {
int bi_baudrate; /* serial console baudrate *//*串口控制台波特率,和外面那个一致*/
unsigned long bi_ip_addr; /* IP Address */
/*开发板IP地址*/
unsigned char bi_enetaddr[6]; /* Ethernet adress *//*MAC地址*/
struct environment_s *bi_env; /*环境变量指针*/
ulong bi_arch_number; /* unique id for this board *//*开发板的机器码*/
ulong bi_boot_params; /* where this board expects params *//**启动参数的地址/
struct /* RAM configuration */
{
ulong start;
ulong size;
} bi_dram[CONFIG_NR_DRAM_BANKS]; /*DDR内存分布的信息*/
} bd_t;
gd_t中定义了很多全局变量,都是整个uboot使用的;其中有一个bd_t类型的指针,指向一个bd_t类型的变量,这个bd是承载开发板的板级信息的结构体,里面有不少硬件相关的参数,譬如波特率、IP地址、机器码、DDR内存分布信息。
2.2.1、为gd、bd分配内存
(1)DECLARE_GLOBAL_DATA_PTR只是定义了一个指针gd,并没有被分配内存,是一个野指针。gd和bd需要分配内存,内存当前没有人管理(因为没有操作系统统一管理内存),大片的DDR内存散放着可以随意使用(只要使用内存地址直接去访问内存即可),因此要分配内存,只需赋予其一个内存地址即可。但是因为uboot中后续很多操作还需要大片的连着内存块,出于紧凑排布的原则。我们在uboot中需要有一个整体规划。
2.2.2、内存排布
(1)uboot区 CFG_UBOOT_BASE-xx(长度为uboot的实际长度)
(2)堆区 长度为CFG_MALLOC_LEN,实际为912KB
(3)栈区 长度为CFG_STACK_SIZE,实际为512KB
(4)gd 长度为sizeof(gd_t),实际36字节
(5)bd 长度为sizeof(bd_t),实际为44字节左右
(6)内存间隔 为了防止高版本的gcc的优化造成错误。
ulong gd_base; /*gd在内存中分配的起始地址*/
gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);
/*
* gd_base = 33e00000 + 2MB - 912KB -512KB - 36B
*/
gd = (gd_t*)gd_base;//强制类型转换,给gd分配内存
/* compiler optimization barrier needed for GCC >= 3.4 */
__asm__ __volatile__("": : :"memory");//为了防止高版本的gcc的优化造成错误。
memset ((void*)gd, 0, sizeof (gd_t));
gd->bd = (bd_t*)((char*)gd - sizeof(bd_t));//给bd分配内存,位置在(gd_base - 44B)处
memset (gd->bd, 0, sizeof (bd_t));
3.1、for循环遍历init_sequence
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
(1)init_fnc_ptr是一个二重函数指针,指向init_sequence这个函数指针数组。init_sequenc的每个元素都是函数,这些函数的特征是int (init_fnc_t) (void); 这次遍历的目的是依次执行这个数组中的所有函数,完成uboot第二阶段相关的硬件初始化。
typedef int (init_fnc_t) (void);
int print_cpuinfo (void); /* test-only */
init_fnc_t *init_sequence[] = {
cpu_init, /* basic cpu dependent setup */
#if defined(CONFIG_SKIP_RELOCATE_UBOOT)
reloc_init, /* Set the relocation done flag, must
do this AFTER cpu_init(), but as soon
as possible */
#endif
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
#if defined(CONFIG_HARD_I2C) || defined(CONFIG_SOFT_I2C)
init_func_i2c,
#endif
dram_init, /* configure available RAM banks */
display_dram_config,
NULL,
};
(2)如何遍历一个函数指针数组?
有2种方法:
第一种是用数组下标去遍历,用数组元素个数来截至。
第二种是在数组的末尾放一个标志NULL,直到遍历到NULL才结束循环。(类似字符串末尾的’\0’标志)这种方法的优势是不用事先统计数组有多少个元素。
(3)循环体
if ((*init_fnc_ptr)() != 0) {
hang ();
}
init_sequence数组里的函数的返回值定义方式一样的,都是:函数执行正确时返回0,不正确时返回-1。所以当我们在遍历过程中有一个函数返回值不等于0,则说明我们的板级硬件有问题,因此必须要终止uboot的启动,调用hang()挂起。
void hang (void)
{
puts ("### ERROR ### Please RESET the board ###\n");
for (;;);
}
3.2、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;
#endif
return 0;
}
(1)看这个函数名应该是要进行cpu内部的初始化,cpu内相关的初始化在start.S第一阶段就完成了,CONFIG_USE_IRQ并未定义,所以这里是空的。
3.3、board_init
int board_init(void)
{
DECLARE_GLOBAL_DATA_PTR; //DECLARE_GLOBAL_DATA_PTR在这里声明是为了后面使用gd方便。。
#ifdef CONFIG_DRIVER_DM9000
dm9000_pre_init();
#endif
gd->bd->bi_arch_number = MACH_TYPE;
gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100);
return 0;
}
(1)board_init在uboot/board/samsung/x210/x210.c中,这个函数做的事情从名字就知道是x210开发板相关的初始化。
(2)DECLARE_GLOBAL_DATA_PTR在这里声明是为了后面使用gd方便。可以看出把gd的声明定义成一个宏的后我们就可以到处去使用gd了,而不需要把这个变量放到.h头文件中去包含,使用更方便。
(3)网卡初始化。CONFIG_DRIVER_DM9000这个宏是x210_sd.h中定义的,这个宏用来配置开发板的网卡。dm9000_pre_init函数就是DM9000网卡的初始化函数。开发板移植uboot时,如果要移植网卡,主要的工作就在这里。
(4)dm9000_pre_init这个函数的工作主要是DM9000这个网卡的GPIO和端口的配置,而不是驱动。因为网卡的驱动都是现成的正确的,移植的时候驱动是不需要改动的,关键是这里的基本初始化不能出错。因为这些基本初始化是硬件相关的。
3.3.1、gd->bd->bi_arch_number
(1)board_info中的一个重要变量,含义是:开发板的机器码。所谓机器码就是uboot官方给这个开发板定义的一个唯一编号,在linux内核中也有个机器码,所以机器码是用于开发板和linux内核之间的适配,开发板的机器码通常维护在这个开发板移植的uboot当中,也就是我们这里定义的bi_arch_number,只有当两者机器码相等才能启动内核,否则就不启动;嵌入式设备中每个设备的硬件都是高度定制化的,不像PC机那样通用,这样就导致硬件和软件不能适配使用。
(2)MACH_TYPE在x210_sd.h中定义,说明当前开发板对应的编号是2456。将来这个开发板上面移植的linux内核中的机器码也必须是2456,否则就启动不起来。
(3)uboot中配置的这个机器码,会作为uboot给linux内核的传参的一部分传给linux内核,内核启动过程中会比对这个接收到的机器码,和自己本身的机器码相对比,如果相等就启动,如果不等就不启动。这个机器码是在linux内核中比对的,uboot中并不比对,uboot只是收好,在将来传参给linux内核。
(4)理论上来说,一个开发板的机器码不能自己随便定,只有uboot官方能发放,但是国内出于英文水平低的原因并没有向uboot官方申请,都是自己随便编号的,只要保证uboot和kernel中的编号是一致的,就不影响自己的开发板启动。
3.3.2、gd->bd->bi_boot_params
(1)board_info中的一个重要变量,bi_boot_params表示uboot给linux kernel传参的参数的内存地址。也就是说uboot会事先将准备好的参数(字符串bootargs)放到bi_boot_params地址处,然后uboot启动内核时通过r0、r1、r2寄存器来传递bi_boot_params这个内存地址给内核,内核通过这些寄存器得到参数存放的地址,进而找到uboot传递的参数。
(2)经过计算得知:X210中bi_boot_params的值为0x30000100,这个内存地址被分配用来做内核传参。所以在uboot的其他地方使用内存时需要注意,千万不敢把这里给淹没了。
3.3.3、PHYS_SDRAM_1涉及DDR的配置信息:
(1)注意:这里的DDR配置信息和汇编阶段lowlevel_init中初始化DDR是不同的。当时是硬件的初始化,目的是让DDR可以开始工作。现在是软件结构中一些DDR相关的属性的配置、地址的设置,是纯软件层面的,目的是为了告诉uboot本身,这块开发板接了一块什么样的内存。
(2)软件层次配置DDR的原因:对于uboot来说,他并不知道开发板上接了几片DDR,每一片的长度大小、起始地址等信息;因此必须由程序员告知,程序员在移植uboot到开发板时,在x210_sd.h中使用宏定义去配置出来板子上DDR内存的信息,然后uboot只需读取这些信息即可。(实际上还有另外一条思路:就是uboot通过代码读取硬件信息来知道DDR配置,但是uboot没有这样。实际上PC的BIOS采用的是这种)
(3)x210_sd.h的496行到501行中使用了标准的宏定义来配置DDR相关的参数。主要配置了这么几个信息:有几片DDR内存、每一片DDR的起始地址、长度。这里的配置信息我们在uboot代码的其它地方中使用。
#define CONFIG_NR_DRAM_BANKS 2 /* we have 2 bank of DRAM */
#define SDRAM_BANK_SIZE 0x10000000 /* 512 MB lqm*/
//#define SDRAM_BANK_SIZE 0x20000000 /* 1GB lqm*/
#define PHYS_SDRAM_1 MEMORY_BASE_ADDRESS /* SDRAM Bank #1 */
#define PHYS_SDRAM_1_SIZE SDRAM_BANK_SIZE
#define PHYS_SDRAM_2 MEMORY_BASE_ADDRESS2 /* SDRAM Bank #2 */
#define PHYS_SDRAM_2_SIZE SDRAM_BANK_SIZE
3.4、interrupt_init
int interrupt_init(void)
{
S5PC11X_TIMERS *const timers = S5PC11X_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 @ 66 MHz
*/
timer_load_val = get_PCLK() / (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 & ~0x00700000) | TCON_4_AUTO | TCON_4_UPDATE;
/* auto load, start Timer 4 */
timers->TCON = (timers->TCON & ~0x00700000) | TCON_4_AUTO | COUNT_4_ON;
timestamp = 0;
return (0);
}
(1)看函数名是和中断初始化有关的,但是实际上不是,实际上这个函数是用来初始化定时器Timer4.
(2)210中共有5个PWM定时器。其中Timer0-timer3都有PWM输出引脚。而Timer4没有引脚,也没有TCMPB输出比较寄存器,因此无法输出PWM波形,只能用来做计时。
(3)Timer4用作计时主要使用到2个寄存器:TCNTB4、TCNTO4。TCNTB中存放的数用作定时次数(每一次时间是由时钟决定的,而时钟是由2级时钟分频器决定的)。我们定时时只需要把定时时间/基准时间=数,将这个数放入TCNTB中即可;我们通过TCNTO寄存器即可知道当前定时时间走到哪里了。
(4)定时器Timer4是没有中断支持的,所以CPU只能使用轮询方式来不断查看TCNTO寄存器才能知道定时时间到了没。uboot中的bootdelay倒计时就是通过Timer4来实现的,然后通过检测按键是否要进入uboot命令行。和轮询方式处理按键是一个道理。
(5)interrupt_init函数将Timer4设置为定时10ms。get_PCLK函数获取系统设置的PCLK_PSYS时钟频率66MHz,然后设置TCFG0和TCFG1寄存器进行16分频和1分频,然后计算出要将定时设置为10ms时需要向TCNTB中写入的值是66MHz/(16x100),将其写入TCNTB,然后设置为auto reload模式并打开定时器开始计时。
总结:在这个函数中采用结构体的方式来访问寄存器。
3.5、env_init
(1)env_init,看函数名就知道是和uboot自身环境变量的初始化有关。
(2)这个函数仅仅只是对环境变量相关的数据结构gd->env_addr和gd->env_valid进行初始化了而已,并没有将环境变量从SD卡复制到DDR中,所以当前还不能使用环境变量。真正的初始化(即重定位)环境变量的地方 在start_armboot函数中776行处调用了env_relocate才将环境变量从SD卡中复制到DDR中,必须要DDR中有环境变量才能使用。
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);
}
3.6、init_baudrate
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);
}
int getenv_r (char *name, char *buf, unsigned len)
{
int i, nxt;
for (i=0; env_get_char(i) != '\0'; i=nxt+1) {
int val, n;
for (nxt=i; env_get_char(nxt) != '\0'; ++nxt) {
if (nxt >= CFG_ENV_SIZE) {
return (-1);
}
}
if ((val=envmatch((uchar *)name, i)) < 0)
continue;
/* found; copy out */
n = 0;
while ((len > n++) && (*buf++ = env_get_char(val++)) != '\0')
;
if (len == n)
*buf = '\0';
return (n);
}
return (-1);
}
(1)init_baudrate是配置串口波特率的函数,并非初始化串口的函数,串口初始化早在第一阶段完成,这里只是纯软件结构,告诉uboot我使用的串口波特率是多少。
(2)getenv_r函数用来读取环境变量“baudrate”的值(注意读取到的不是int型而是字符串类型),然后用simple_strtoul函数将字符串转成数字格式的波特率。
(3)这个函数的工作是,先通过getenv_r函数读取环境变量"baudrate"的值,如果读取成功,则记录在gd->baudrate和gd->bd->bi_baudrate中;如果读取不成功,则将x210_sd.h中的CONFIG_BAUDRATE宏(值为115200)作为记录。从这可以看出:环境变量的优先级是高于配置值的。
3.7、serial_init
void serial_setbrg(void)
{
DECLARE_GLOBAL_DATA_PTR;
int i;
for (i = 0; i < 100; i++);
}
/*
* Initialise the serial port with the given baudrate. The settings
* are always 8 data bits, no parity, 1 stop bit, no start bits.
*
*/
int serial_init(void)
{
serial_setbrg();
return (0);
}
(1)serial_init看函数名是初始化串口的函数,但汇编阶段便已经初始化过串口,这个函数的路径在uboot/cpu/s5pc11x/serial.c中,函数体中什么也没做,是空的。
3.8、console_init_f
(1)console_init_f是console(控制台)的第一阶段初始化。_f表示是第一阶段初始化,_r表示第二阶段初始化。有时候初始化函数不能一次一起完成,中间必须要夹杂一些代码,因此将完整的一个模块的初始化分成了2个阶段。(我们的uboot中start_armboot的826行进行了console_init_r的初始化)
int console_init_f (void)
{
gd->have_console = 1;
return (0);
}
(2)console_init_f在uboot/common/console.c中,仅仅是对gd->have_console设置为1而已,其他事情都没做,因此现在控制台还是不能用的,
3.9、display_banner
(1)display_banner用来串口输出显示uboot的logo
//board.c 59行
#undef DEBUG
//由于未定义DEBUG宏;因此display_banner()函数中的debug全部无效
//board.c 304行
static int display_banner (void)
{
printf ("\n\n%s\n\n", version_string);
debug ("U-Boot code: %08lX -> %08lX BSS: -> %08lX\n",
_armboot_start, _bss_start, _bss_end);
#ifdef CONFIG_MEMORY_UPPER_CODE /* by scsuh */
debug("\t\bMalloc and Stack is above the U-Boot Code.\n");
#else
debug("\t\bMalloc and Stack is below the U-Boot Code.\n");
#endif
#ifdef CONFIG_MODEM_SUPPORT
debug ("Modem Support enabled\n");
#endif
#ifdef CONFIG_USE_IRQ
debug ("IRQ Stack: %08lx\n", IRQ_STACK_START);
debug ("FIQ Stack: %08lx\n", FIQ_STACK_START);
#endif
open_backlight();//lqm.打开LCD背光
//open_gprs();
return (0);
}
(2)display_banner中使用printf函数向串口输出了version_string这个字符串。但是此时控制台并没有初始化好,怎么就能使用了呢?
//console.c 217行
//可以看出printf函数最后调用puts函数
void printf (const char *fmt, ...)
{
va_list args;
uint i;
char printbuffer[CFG_PBSIZE];
va_start (args, fmt);
/* For this to work, printbuffer must be larger than
* anything we ever want to print.
*/
i = vsprintf (printbuffer, fmt, args);
va_end (args);
/* Print the string */
puts (printbuffer);
}
//console.c 201行
//puts函数中判断当前console控制台是否初始化好
void puts (const char *s)
{
#ifdef CONFIG_SILENT_CONSOLE
if (gd->flags & GD_FLG_SILENT)
return;
#endif
if (gd->flags & GD_FLG_DEVINIT) {//当前console控制台是否初始化好
//GD_FLG_DEVINIT在global_data.h 61行定义
/* Send to the standard output */
fputs (stdout, s);
} else {
//serial_puts ()在serial.c 146行定义
/* Send directly to the handler */
serial_puts (s);
}
}
//================================================================
//global_data.h 61行定义
#define GD_FLG_DEVINIT 0x00002 /* Devices have been initialized */
//serial.c 146行
void serial_puts(const char *s)
{
while (*s) {
serial_putc(*s++);
}
}
//serial.c 112行
void serial_putc(const char c)
{
S5PC11X_UART *const uart = S5PC11X_GetBase_UART(UART_NR);
#ifdef CONFIG_MODEM_SUPPORT
if (be_quiet)
return;
#endif
/* wait for room in the tx FIFO */
while (!(uart->UTRSTAT & 0x2));
#ifdef CONFIG_HWFLOW
/* Wait for CTS up */
while (hwflow && !(uart->UMSTAT & 0x1));
#endif
uart->UTXH = c; //直接操作的是串口的寄存器
/* If \n, also do \r */
if (c == '\n')
serial_putc('\r');
}
(3)通过追踪printf的实现,发现printf->puts->serial_puts->serial_putc->uart->UTXH = c; ,而puts函数中会判断当前uboot中console有没有被初始化好。如果console初始化好了则调用fputs完成串口发送(这条线才是控制台);如果console尚未初始化好则会调用serial_puts(再调用serial_putc直接操作串口寄存器进行内容发送)。
(4)控制台也是通过串口输出,非控制台也是通过串口输出。其实控制台只是软件虚拟出来的设备,这个设备有一套专用的通信函数(在uboot中,发送fputs、接收fgets),fputs封装了serial_puts,所以在uboot中两者最终都是使用硬件串口的通信函数,并无本质区别。但是在别的体系中,控制台的通信函数可以做中间优化,譬如缓冲机制,增设一个缓冲区来保存要输出的数据,这样CPU就不用盯着串口输出一个一个的字符,只需将要输出的数据放到缓冲区中,让串口自己打印,而CPU可以去做别的事情。(操作系统中的控制台都使用了缓冲机制,所以有时候我们printf了内容但是屏幕上并没有看到输出信息,就是因为被缓冲了。我们输出的信息只是到了console的buffer中,buffer还没有被刷新到硬件输出设备上,尤其是在输出设备是LCD屏幕时,这种优势极其明显,我们并不是每次输出一个字符然后就要刷新整个显存,而是将要输出的字符串都放到缓存buffer中,然后一起刷新到显存中去。)
(5)U_BOOT_VERSION在uboot源代码中找不到定义,实际上它的定义在include/version_autogenerated.h中用一个宏定义U_BOOT_VERSION来实现,version_autogenerated.h是Makefile编译的时候自动生成的,由Makefile创建,这个宏的值来自Makefile的变量。
3.10、print_cpuinfo
(1)uboot启动过程中控制台显示的:
CPU: S5PV210@1000MHz(OK)
APLL = 1000MHz, HclkMsys = 200MHz, PclkMsys = 100MHz
MPLL = 667MHz, EPLL = 96MHz
HclkDsys = 166MHz, PclkDsys = 83MHz
HclkPsys = 133MHz, PclkPsys = 66MHz
SCLKA2M = 200MHz
Serial = CLKUART
这些信息都是print_cpuinfo打印出来的。
//speed.c 228行
int print_cpuinfo(void)
{
uint set_speed;
uint tmp;
uchar result_set;
#if defined(CONFIG_CLK_533_133_100_100)
set_speed = 53300;
#elif defined(CONFIG_CLK_667_166_166_133)
set_speed = 66700;
#elif defined(CONFIG_CLK_800_200_166_133)
set_speed = 80000;
#elif defined(CONFIG_CLK_1000_200_166_133) //x210_sd.h中配置的是这个
set_speed = 100000;
#elif defined(CONFIG_CLK_1200_200_166_133)
set_speed = 120000;
#else
set_speed = 100000;
printf("Any CONFIG_CLK_XXX is not enabled\n");
#endif
tmp = (set_speed / (get_ARMCLK()/1000000));
/* get_ARMCLK()根据我们时钟配置的寄存器,软件计算出我们设置的时钟频率
* get_ARMCLK() = 10^9
* tmp = 100000/ (10^9/10^6) = 100000/1000 = 100
*/
if((tmp < 105) && (tmp > 95)){ //tmp = 100
result_set = 1; //result_set = 1
} else {
result_set = 0;
}
#ifdef CONFIG_MCP_SINGLE
printf("\nCPU: S5PV210@%ldMHz(%s)\n", get_ARMCLK()/1000000, ((result_set == 1) ? "OK" : "FAIL"));
#else
printf("\nCPU: S5PC110@%ldMHz(%s)\n", get_ARMCLK()/1000000, ((result_set == 1) ? "OK" : "FAIL"));
#endif
printf(" APLL = %ldMHz, HclkMsys = %ldMHz, PclkMsys = %ldMHz\n",
get_FCLK()/1000000, get_HCLK()/1000000, get_PCLK()/1000000);
#if 1
printf(" MPLL = %ldMHz, EPLL = %ldMHz\n",
get_MPLL_CLK()/1000000, get_PLLCLK(EPLL)/1000000);
printf(" HclkDsys = %ldMHz, PclkDsys = %ldMHz\n",
get_HCLKD()/1000000, get_PCLKD()/1000000);
printf(" HclkPsys = %ldMHz, PclkPsys = %ldMHz\n",
get_HCLKP()/1000000, get_PCLKP()/1000000);
printf(" SCLKA2M = %ldMHz\n", get_SCLKA2M()/1000000);
#endif
puts("Serial = CLKUART ");
return 0;
}
3.11、checkboard
(1)checkboard这个函数的作用就是检查当前开发板是哪个开发板并且打印出开发板的名字。
3.12、init_func_i2c
(1)这个函数实际没有被执行,X210的uboot中并没有使用I2C。如果将来我们的开发板要扩展I2C来接外接硬件,则在x210_sd.h中配置相应的宏即可开启。
3.13、uboot学习实践
(1)对uboot源代码进行完修改(修改内容根据自己的理解和分析来修改)
这里只是修改了版本号和开发板信息
(2)make distclean然后make x210_sd_config然后make
(3)编译完成得到u-boot.bin,然后去烧录。在linux下使用dd命令来烧录。
(4)烧写过程:
第一步:进入sd_fusing目录下
第二步:make clean
第三步:make(必须保证当前虚拟机是32位的)
第四步:插入sd卡,ls /dev/sd*得到SD卡在ubuntu中的设备号(一般是/dev/sdb,注意SD卡要连接到虚拟机ubuntu中,不要接到windows中)
第五步:./sd_fusing.sh /dev/sdb完成烧录(注意不是sd_fusing2.sh)
(5)总结:uboot就是个庞大点复杂点的裸机程序而已,我们完全可以对他进行调试。调试的方法就是按照上面步骤,根据自己对代码的分析和理解对代码进行更改,然后重新编译烧录运行,根据运行结果来学习。
3.14、dram_init、
(1)在汇编阶段已经初始化过DDR否则也无法进行relocate,这里只是纯软件的配置,告知uboot自己用的是什么内存,有几片、起始地址、长度大小。以便uboot之后使用内存,譬如说给mm命令使用。
(2)dram_init都是在给gd->bd里面关于DDR配置部分的全局变量赋值,让gd->bd数据记录下当前开发板的DDR的配置信息。
(3)从代码来看,其实就是初始化gd->bd->bi_dram这个结构体数组。
3.15、display_dram_config
(1)这个函数的作用是打印显示dram的配置信息。
(2)启动信息中的:(DRAM: 512 MB)就是在这个函数中打印出来的。
(3)思考:如何在uboot命令行中得知uboot的DDR配置信息?uboot中有一个命令叫bdinfo,这个命令可以打印出gd->bd中记录的所有硬件相关的全局变量的值,因此可以得知DDR的配置信息。
DRAM bank = 0x00000000
-> start = 0x30000000
-> size = 0x10000000
DRAM bank = 0x00000001
-> start = 0x40000000
-> size = 0x10000000
3.16、init_sequence总结
(1)都是板级硬件的初始化以及gd、gd->bd中的数据结构的初始化。譬如:
cpu_init:空
board_init:网卡初始化、机器码(gd->bd->bi_arch_number)、内核传参DDR地址(gd->bd->bi_boot_params)
interrupt_init:Timer4初始化为10ms一次
env_init:检查可用的环境变量,没有什么实质性的东西,此时并未将环境变量从SD卡复制到内存
init_baudrate:波特率设置(gd->bd->bi_baudrate和gd->baudrate)
serial_init:空
console_init_f:console第一阶段初始化(gd->have_console设置为1)
display_banner:打印uboot的版本信息
print_cpuinfo:打印时钟相关设置信息
checkboard:检查并打印当前开发板名字
init_func_i2c:空
dram_init:DDR配置信息初始化(gd->bd->bi_dram)
display_dram_config:打印DDR总容量。
4.1、CFG_NO_FLASH
(1)flash_init执行的是开发板中对应的NorFlash的初始化、display_flash_config打印的也是NorFlash的配置信息(Flash: 8 MB就是这里打印出来的)。但是实际上X210中是没有Norflash的。所以这两行代码是可以去掉的,加上CONFIG_NOFLASH宏之后就可去掉,发现编译出错,说明代码移植的不好,那个文件的包含没有被这个宏控制。于是乎移植的人就直接放这没管。
(2)CONFIG_VFD和CONFIG_LCD是显示相关的,这个是uboot中自带的LCD显示的软件架构。但是实际上我们用LCD而没有使用uboot中设置的这套软件架构,我们自己在后面自己添加了一个LCD显示的部分。
4.2、mem_malloc_init
(1)mem_malloc_init函数用来初始化uboot的堆管理器。
(2)uboot中自己维护了一段堆内存,肯定自己就有一套代码来管理这个堆内存。有了这些东西就可以在uboot中使用malloc、free这套机制来申请内存和释放内存。我们在DDR内存中给堆预留了912KB的内存。
4.3、开发板独有初始化:mmc初始化
(1)从536到768行为开发板独有的初始化。意思是三星用一套uboot同时满足了好多个系列型号的开发板,然后在这里把不同开发板自己独有的一些初始化写到了这里。用#if条件编译配合CONFIG_xxx宏来选定特定的开发板。
(2)X210相关的配置在599行到632行。
#if defined(CONFIG_X210)
#if defined(CONFIG_GENERIC_MMC)
puts ("SD/MMC: ");
mmc_exist = mmc_initialize(gd->bd);
if (mmc_exist != 0)
{
puts ("0 MB\n");
#ifdef CONFIG_CHECK_X210CV3
check_flash_flag=0;//check inand error!
#endif
}
#ifdef CONFIG_CHECK_X210CV3
else
{
check_flash_flag=1;//check inand ok!
}
#endif
#endif
#if defined(CONFIG_MTD_ONENAND)
puts("OneNAND: ");
onenand_init();
/*setenv("bootcmd", "onenand read c0008000 80000 380000;bootm c0008000");*/
#else
//puts("OneNAND: (FSR layer enabled)\n");
#endif
#if defined(CONFIG_CMD_NAND)
puts("NAND: ");
nand_init();
#endif
#endif /* CONFIG_X210 */
(3)mmc_initialize看名字就应该是MMC相关的一些基础的初始化,其实就是用来初始化SoC内部的SD/MMC控制器的。函数在uboot/drivers/mmc/mmc.c 1177行。
int mmc_initialize(bd_t *bis)
{
struct mmc *mmc;
int err;
INIT_LIST_HEAD(&mmc_devices);//初始化内核链表,用于记录当前系统所有MMC设备
cur_dev_num = 0;//当前设备的编号:0,可选0~3,对应SDcare四个通道
if (board_mmc_init(bis) < 0)
cpu_mmc_init(bis);
#if defined(DEBUG_S3C_HSMMC)
print_mmc_devices(',');
#endif
#ifdef CONFIG_CHECK_X210CV3
mmc = find_mmc_device(1);//lqm
#else
mmc = find_mmc_device(0);
#endif
if (mmc) {
err = mmc_init(mmc);
if (err)
err = mmc_init(mmc);
if (err) {
printf("Card init fail!\n");
return err;
}
}
printf("%ldMB\n", (mmc->capacity/(1024*1024/(1<<9))));
return 0;
}
(4)uboot中对硬件的操作(譬如网卡、SD卡···)都是借用的linux内核中的驱动来实现的,uboot根目录底下有个drivers文件夹,这里面放的全都是从linux内核中移植过来的各种驱动源文件。
(5)mmc_initialize是具体硬件架构无关的一个MMC初始化函数,所有的使用了这套架构的代码都调用这个函数来完成MMC的初始化。mmc_initialize中再调用board_mmc_init或者cpu_mmc_init来完成具体的硬件的MMC控制器初始化工作。
调用board_mmc_init时实际调用的是_def_c_init,board_mmc_init是函数_def_mmc_init起的一个别名,在这里board_mmc_init函数返回值被定为-1,严格小于0,原因是SD控制器是集成在SOC里面的,和CPU待在一起,属于CPU级的,而不是board板级。
(6)cpu_mmc_init在uboot/cpu/s5pc11x/cpu.c中,这里面又间接的调用了drivers/mmc/s3c_mmcxxx.c中的驱动代码来初始化硬件MMC控制器。这里面分层很多,
cpu_mmc_init->smdk_s3c_hsmmc_init->s3c_hsmmc_initialize->drivers/mmc/s3c_mmcxxx.c中的驱动代码
/*
* Initializes on-chip MMC controllers.
* to override, implement board_mmc_init()
*/
int cpu_mmc_init(bd_t *bis)
{
#ifdef CONFIG_S3C_HSMMC
setup_hsmmc_clock(); //SD控制器时钟相关初始化
setup_hsmmc_cfg_gpio(); //SD控制器GPIO相关初始化
return smdk_s3c_hsmmc_init();
#else
return 0;
#endif
}
4.4、env_relocate
void env_relocate (void)
{
/*
* We must allocate a buffer for the environment
*/
//1.给环境变量分配内存区域,大小为CFG_ENV_SIZE(4G)
env_ptr = (env_t *)malloc (CFG_ENV_SIZE);
//2.判断当前环境变量是否可用,第一次由于环境变量尚未重定位到DDR中,所以是不能用的。
if (gd->env_valid == 0) {
puts ("*** Warning - bad CRC, using default environment\n\n");
//打印一串调试信息
show_boot_progress (-60);
//也是调试信息,说明当前uboot进行到了百分之多少
set_default_env();
/* uboot代码中默认封装了一套环境变量,将其memcpy到第一步申请的内存中
* 计算CRC校验保证下次使用不会错误
* 设置gd->env_valid = 1;表示当前内存中有环境变量,可以使用了
*/
}
else {
// 由于第一次gd->env_valid = 1,第二次进来这里进行重定位。
env_relocate_spec ();
}
//3.设置环境变量所在的env分区的内存地址
gd->env_addr = (ulong)&(env_ptr->data);
}
(1)env_relocate是环境变量的重定位,完成从SD卡中将环境变量读取到DDR中的任务。
(2)环境变量到底从哪里来?SD卡中有一些(8个)独立的扇区作为环境变量存储区域的。但是我们烧录/部署系统时,我们只是烧录了uboot分区、kernel分区和rootfs分区,根本不曾烧录env分区。所以当我们烧录完系统第一次启动时ENV分区是空的,本次启动uboot尝试去SD卡的ENV分区读取环境变量时失败(读取回来后进行CRC校验时失败),我们uboot选择从uboot内部代码中设置的一套默认的环境变量出发来使用(这就是默认环境变量);这套默认的环境变量在本次运行时会被读取到DDR中的环境变量中,然后被写入(也可能是你saveenv时写入,也可能是uboot设计了第一次读取默认环境变量后就写入)SD卡的ENV分区。然后下次再次开机时uboot就会从SD卡的ENV分区读取环境变量到DDR中,这次读取就不会失败了。
(3)真正的从SD卡到DDR中重定位ENV的代码是在env_relocate_spec内部的movi_read_env完成的。
//位置是uboot/common/env_movi.c 93行
void env_relocate_spec (void)
{
#if !defined(ENV_IS_EMBEDDED)
uint *magic = (uint*)(PHYS_SDRAM_1);
if ((0x24564236 != magic[0]) || (0x20764316 != magic[1]))
movi_read_env(virt_to_phys((ulong)env_ptr));
if (crc32(0, env_ptr->data, ENV_SIZE) != env_ptr->crc)
return use_default();
#endif /* ! ENV_IS_EMBEDDED */
}
//位置是uboot/cpu/s5pc11x/movi.c 112行
void movi_read_env(ulong addr)
{
movi_read(raw_area_control.image[2].start_blk,
raw_area_control.image[2].used_blk, addr);
}
4.5、IP地址、MAC地址的确定
x210_sd.h第231行开始,是uboot代码封装的关于IP、MAC地址等的默认环境变量
#define CONFIG_BOOTARGS “console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3”
//#define CONFIG_BOOTARGS “console=ttySAC0,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3”
#define CONFIG_ETHADDR 00:40:5c:26:0a:5b
#define CONFIG_NETMASK 255.255.0.0
#define CONFIG_IPADDR 192.168.1.88
#define CONFIG_SERVERIP 192.168.1.102
#define CONFIG_GATEWAYIP 192.168.0.1
(1)开发板的IP地址数据结构是在gd->bd中维护的,来源于环境变量ipaddr。getenv函数用来获取字符串格式的IP地址,然后用string_to_ip将字符串格式的IP地址转成字符串格式的点分十进制格式。
(2)IP地址由4个0-255之间的数字组成,因此一个IP地址在程序中最简单的存储方法就是一个unsigend int。但是人类容易看懂的并不是这种类型,而是点分十进制类型(192.168.1.2)。这两种类型可以互相转换。
/* IP Address */
gd->bd->bi_ip_addr = getenv_IPaddr ("ipaddr");
IPaddr_t getenv_IPaddr (char *var)
{
return (string_to_ip(getenv(var)));
}
/* 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;
}
4.6、devices_init
(1)devices_init看名字是设备的初始化。这里的设备指的就是开发板上的硬件设备。和前面的各种硬件设备初始化不同的是,放在这里初始化的设备都是驱动设备,这个函数本来就是从驱动框架中衍生出来的。uboot中很多设备的驱动是直接移植linux内核的(譬如网卡、SD卡),linux内核中的驱动都有相应的设备初始化函数。linux内核在启动过程中就有一个devices_init(名字不一定完全对,但是差不多),作用就是集中执行各种硬件驱动的init函数。
(2)uboot的这个函数其实就是从linux内核中移植过来的,它的作用也是去执行所有的从linux内核中继承来的那些硬件驱动的初始化函数。
4.7、jumptable_init
void jumptable_init (void)
{
int i;
gd->jt = (void **) malloc (XF_MAX * sizeof (void *));
for (i = 0; i < XF_MAX; i++)
gd->jt[i] = (void *) dummy;
gd->jt[XF_get_version] = (void *) get_version;
gd->jt[XF_malloc] = (void *) malloc;
gd->jt[XF_free] = (void *) free;
gd->jt[XF_getenv] = (void *) getenv;
gd->jt[XF_setenv] = (void *) setenv;
gd->jt[XF_get_timer] = (void *) get_timer;
gd->jt[XF_simple_strtoul] = (void *) simple_strtoul;
gd->jt[XF_udelay] = (void *) udelay;
gd->jt[XF_simple_strtol] = (void *) simple_strtol;
gd->jt[XF_strcmp] = (void *) strcmp;
#if defined(CONFIG_I386) || defined(CONFIG_PPC)
gd->jt[XF_install_hdlr] = (void *) irq_install_handler;
gd->jt[XF_free_hdlr] = (void *) irq_free_handler;
#endif /* I386 || PPC */
#if defined(CONFIG_CMD_I2C)
gd->jt[XF_i2c_write] = (void *) i2c_write;
gd->jt[XF_i2c_read] = (void *) i2c_read;
#endif
}
(1)jumptable跳转表,jt本身是一个函数指针数组,里面记录了很多工具函数的函数名,譬如堆的申请和释放的函数、字符串转换函数、环境变量相关的函数、I2c读写相关的函数等。看这阵势是要实现一个函数指针到具体函数的映射关系,将来通过跳转表中的函数指针就可以执行具体的函数。这个其实就是在用C语言实现面向对象编程。在linux内核中有很多这种技巧。但是这个跳转表在uboot中只有赋值,而从未被使用,所以根本无用,这里可以忽略。
4.8、console_init_r
(1)console_init_f是控制台的第一阶段初始化,console_init_r是第二阶段初始化。实际上第一阶段初始化并没有实质性工作,第二阶段初始化才进行了实质性工作。
(2)uboot中有很多同名函数,使用SI工具去索引时经常索引到不对的函数处(回忆下当时start.S中找lowlevel_init.S时,自动索引找到的是错误的,真正的反而根本没找到。)
(3)console_init_r就是console的纯软件架构方面的初始化(说白了就是去给console相关的数据结构中填充相应的值),所以属于纯软件配置类型的初始化。
(4)uboot的console实际上并没有干有意义的转化,它就是直接调用的串口通信的函数。所以用不用console实际并没有什么分别。(在linux内console就可以提供缓冲机制等不用console不能实现的东西)。
4.9、enable_interrupts
(1)看名字应该是中断初始化代码。但因为我们uboot中没有使用中断,因此没有定义CONFIG_USE_IRQ宏,因此我们这里这个函数是个空壳子。
(2)uboot中经常出现一种情况就是根据一个宏是否定义了来条件编译决定是否调用一个函数内部的代码。uboot中有2种解决方案来处理这种情况:方案一:在调用函数处使用条件编译,然后函数体实际完全提供代码。方案二:在调用函数处直接调用,然后在函数体处提供2个函数体,一个是有实体的一个是空壳子,用宏定义条件编译来决定实际编译时编译哪个函数进去。
4.10、loadaddr、bootfile两个环境变量
(1)定义在uboot/common/cmd_bootm.c,所以这两个环境变量都是内核启动有关的,在启动linux内核时会参考这两个环境变量的值。
4.11、board_late_init
(1)这个函数的作用是开发板后续硬件的初始化,前面该初始化的都初始化过了。侧面说明了开发板级别的硬件软件初始化即将告一段落了。
(2)对于X210来说,这个函数是空的。
4.12、eth_initialize
(1)看名字应该是网卡相关的一些初始化。这里不是SoC与网卡芯片连接时SoC这边的初始化,而是网卡芯片本身的一些初始化。
(2)对于X210(DM9000)来说,这个函数是空的。X210的网卡IO端口的初始化在board_init函数中实现,网卡芯片的初始化在驱动中。
0
4.13、x210_preboot_init(LCD和logo显示)
(1)uboot命令行在启动起来之前的LCD初始化,以及LCD屏幕上的logo显示。
4.14、check menukey to update from sd
/* check menukey to update from sd */
extern void update_all(void);
if(check_menu_update_from_sd()==0)//update mode
{
puts ("[LEFT DOWN] update mode\n");
run_command("fdisk -c 0",0);
update_all();
}
else
puts ("[LEFT UP] boot mode\n");
(1)uboot启动的最后阶段设计了一个自动更新的功能。就是:我们可以将要升级的镜像放到SD卡的固定目录/x210目录中,然后开机时在uboot启动的最后阶段检查升级标志(是一个按键。按键中标志为"LEFT"的那个按键,这个按键如果按下则表示update mode,如果启动时未按下则表示boot mode)。如果进入update mode则uboot会自动从SD卡中读取镜像文件然后烧录到iNand中;如果进入boot mode则uboot不执行update,直接启动正常运行。
(2)这种机制能够帮助我们快速烧录系统,常用于量产时用SD卡进行系统烧录部署。
4.15、死循环mainloop函数功能
(1)解析器
(2)开机倒数自动执行
(3)命令补全
4.16、uboot启动2阶段总结
1)第二阶段主要是对开发板级别的硬件、软件数据结构进行初始化。
2)
init_sequence 遍历以下函数
cpu_init 空的
board_init 网卡、机器码、内存传参地址
dm9000_pre_init 网卡
gd->bd->bi_arch_number 机器码
gd->bd->bi_boot_params 内存传参地址
interrupt_init 定时器4的初始化
env_init 没什么实际的事情,gd数据结构的赋值
init_baudrate gd数据结构中波特率
serial_init 空的,第一阶段初始化过
console_init_f 基本是空的,一条gd数据结构赋值
display_banner 打印uboot版本信息
print_cpuinfo 打印CPU时钟设置信息
checkboard 检验并打印开发板名字
dram_init gd数据结构中DDR信息
display_dram_config 打印DDR配置信息
mem_malloc_init 初始化uboot的自己维护的堆内存
mmc_initialize SD/emmc控制器和SD卡的初始化
env_relocate 环境变量重定位
gd->bd->bi_ip_addr ip地址,gd数据结构赋值
gd->bd->bi_enetaddr mac地址,gd数据结构赋值
devices_init 基本空的没有使用,从linux内核中搬过来的,里面包含各种硬件驱动的初始化函数,但我们前面的硬件基本都初始化差不多了
jumptable_init 跳转表,uboot定义了却不用,不用特别关注的
console_init_r 真正的控制台初始化,有没有控制台没什么区别,不用特别关注
enable_interrupts 空的,uboot中不使用中断
loadaddr、bootfile 两个要传给内核的参数,环境变量读出初始化成全局变量
board_late_init 空的,后续要添加硬件,可以往这里添加
eth_initialize 空的,一些网卡比较深层次的东西,看不懂
x210_preboot_init LCD初始化和显示logo
check_menu_update_from_sd 检查自动更新,实现量产功能
main_loop 主循环
3)启动过程特征总结
(1)第一阶段为汇编阶段、第二阶段为C阶段
(2)第一阶段在SRAM中、第二阶段在DRAM中
(3)第一阶段注重SoC内部硬件、第二阶段注重SoC外部Board上面
4)移植时的注意点
(1)x210_sd.h头文件中的宏定义
(2)特定硬件的初始化函数位置(譬如网卡)