嵌入式之uboot原始碼分析-啟動第二階段學習筆記(上篇)

Donke_Dong發表於2020-12-02

注:以下內容來自朱老師物聯網大講堂

本部分主要是init_sequence函式中子函式的介紹

在啟動的第一階段我們最後呼叫start_armboot,進入uboot的第二階段,下面我們將來世第二階段的分析。

1. start_armboot函式簡介

1.1 一個很長的函式

(1)這個函式在uboot/lib_arm/board.c的第444行開始到908行結束。
(2)450行還不是全部,因為裡面還呼叫了別的函式。
(3)為什麼這麼長的函式,怎麼不分成兩三個函式?主要因為這個函式整個構成了uboot啟動的第二階段。

1.2 一個函式組成uboot第二階段

1.3 巨集觀分析:uboot第二階段應該做什麼

(1)概括來講uboot第一階段主要就是初始化了SoC內部的一些部件(譬如看門狗、時鐘),然後初始化DDR並且完成重定位。
(2)由巨集觀分析來講,uboot的第二階段就是要初始化剩下的還沒被初始化的硬體。主要是SoC外部硬體(譬如iNand、網路卡晶片····)、uboot本身的一些東西(uboot的命令、環境變數等····)。然後最終初始化完必要的東西后進入uboot的命令列準備接受命令。

1.4 思考:uboot第二階段完結於何處?

(1)uboot啟動後自動執行列印出很多資訊(這些資訊就是uboot在第一和第二階段不斷進行初始化時,列印出來的資訊)。然後uboot進入了倒數bootdelay秒然後執行bootcmd對應的啟動命令。
(2)如果使用者沒有干涉則會執行bootcmd進入自動啟動核心流程(uboot就死掉了);此時使用者可以按下Enter鍵打斷uboot的自動啟動進入uboot的命令列下。然後uboot就一直工作在命令列下。
(3)uboot的命令列就是一個死迴圈,迴圈體內不斷重複:接收命令、解析命令、執行命令。這就是uboot最終的歸宿。

2. start_armboot解析1

2.1 init_fnc_t

首先我們發現定義了一個二重指標,其次我們發現init_fnc_t是一個使用typedef重新定義的型別,綜合分析我們知道,init_fnc_ptr是一個二重函式指標

typedef int (init_fnc_t) (void);
init_fnc_t **init_fnc_ptr;

(1)typedef int (init_fnc_t) (void); 這是一個函式型別
(2)init_fnc_ptr是一個二重函式指標,回顧高階C語言中講過:二重指標的作用有2個(其中一個是用來指向一重指標),一個是用來指向指標陣列。因此這裡的init_fuc_ptr可以用來指向一個函式指標陣列。

2.2 DECLARE_GLOBAL_DATA_PTR

我們在467行開始發現有一個gd變數,這個時候我們發現在70行有個巨集定義,順著這個巨集定義我們看到了變數的原型。

gd = (gd_t*)gd_base;
DECLARE_GLOBAL_DATA_PTR
#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("r8")

(1)#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm (“r8”)
定義了一個全域性變數名字叫gd,這個全域性變數是一個指標型別,佔4位元組。用volatile修飾表示可變的,用register修飾表示這個變數要儘量放到暫存器中,後面的asm(“r8”)是gcc支援的一種語法,意思就是要把gd放到暫存器r8中。
(2)綜合分析,DECLARE_GLOBAL_DATA_PTR就是定義了一個要放在暫存器r8中的全域性變數,名字叫gd,型別是一個指向gd_t型別變數的指標。
(3)為什麼要定義為register?因為這個全域性變數gd(global data的簡稱)是uboot中很重要的一個全域性變數(準確的說這個全域性變數是一個結構體,裡面有很多內容,這些內容加起來構成的結構體就是uboot中常用的所有的全域性變數),這個gd在程式中經常被訪問,因此放在register中提升效率。因此純粹是執行效率方面考慮,和功能要求無關。並不是必須的。
(4)gd_t定義在include/asm-arm/global_data.h中。
gd_t中定義了很多全域性變數,都是整個uboot使用的;其中有一個bd_t型別的指標,指向一個bd_t型別的變數,這個bd是開發板的板級資訊的結構體,裡面有不少硬體相關的引數,譬如波特率、IP地址、機器碼、DDR記憶體分佈。

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 */
#ifdef CONFIG_VFD
	unsigned char	vfd_type;	/* display type */
#endif
#if 0
	unsigned long	cpu_clk;	/* CPU clock in Hz!		*/
	unsigned long	bus_clk;
	phys_size_t	ram_size;	/* RAM size */
	unsigned long	reset_status;	/* reset status register at boot */
#endif
	void		**jt;		/* jump table */
} gd_t;

bd_t

typedef struct bd_info {
    int			bi_baudrate;	/* serial console baudrate */
    unsigned long	bi_ip_addr;	/* IP Address */
    unsigned char	bi_enetaddr[6]; /* Ethernet adress */
    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];
#ifdef CONFIG_HAS_ETH1
    /* second onboard ethernet port */
    unsigned char   bi_enet1addr[6];
#endif
} bd_t;

2.3 記憶體使用排布

2.3.1 為什麼要分配記憶體

(1)DECLARE_GLOBAL_DATA_PTR只能定義了一個指標,也就是說gd裡的這些全域性變數並沒有被分配記憶體,我們在使用gd之前要給他分配記憶體,否則gd也只是一個野指標而已。
(2)gd和bd需要記憶體,記憶體當前沒有被人管理(因為沒有作業系統統一管理記憶體),大片的DDR記憶體散放著可以隨意使用(只要使用記憶體地址直接去訪問記憶體即可)。但是因為uboot中後續很多操作還需要大片的連著記憶體塊,因此這裡使用記憶體要本著夠用就好,緊湊排布的原則。所以我們在uboot中需要有一個整體規劃。

2.3.2 DDR中的記憶體排布

gd_base = CFG_UBOOT_BASE + CFG_UBOOT_SIZE - CFG_MALLOC_LEN - CFG_STACK_SIZE - sizeof(gd_t);

我們知道我們在第一階段進行了DDR的初始化和uboot的重定位,所以根據如上程式碼我們可以得到如下的DDR記憶體分配圖。
DDR中的記憶體分配
(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的優化造成錯誤。

3. start_armboot解析2

3.1 for迴圈執行init_sequence(487~491)

	for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
		if ((*init_fnc_ptr)() != 0) {
			hang ();
		}
	}

首先我們知道init_fnc_ptr是一個二重指標指向的是一個函式(返回值為int,引數是viod型別的函式),init_sequence是我們定義的一個函式指標陣列,函式名代表陣列的首元素地址,函式實現過程,就是依次訪問也就是遍歷執行init_sequence中的函式,如果函式執行異常則啟動失敗執行hang()函式。中止條件就是*init_fnc_ptr=NULL,即執行結束。

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,
};

總結:
(1)init_sequence是一個函式指標陣列,陣列中儲存了很多個函式指標,這些指標指向的函式都是init_fnc_t型別(特徵是接收引數是void型別,返回值是int)。
(2)init_sequence在定義時就同時給了初始化,初始化的函式指標都是一些函式名。(C語言高階專題中講過:函式名的實質)
(3)init_fnc_ptr是一個二重函式指標,可以指向init_sequence這個函式指標陣列。
(4)用for迴圈肯定是想要去遍歷這個函式指標陣列(遍歷的目的也是去依次執行這個函式指標陣列中的所有函式)。

思考: 如何遍歷一個函式指標陣列?
有2種方法:第一種也是最常用的一種,用下標去遍歷,用陣列元素個數來截至。第二種不常用,但是也可以。就是在陣列的有效元素末尾放一個標誌,依次遍歷到標準處即可截至(有點類似字串的思路)。
我們這裡使用了第二種思路。因為陣列中存的全是函式指標,因此我們選用了NULL來作為標誌。我們遍歷時從開頭依次進行,直到看到NULL標誌截至。這種方法的優勢是不用事先統計陣列有多少個元素。
(5)init_fnc_t的這些函式的返回值定義方式一樣的,都是:函式執行正確時返回0,不正確時返回-1.所以我們在遍歷時去檢查函式返回值,如果遍歷中有一個函式返回值不等於0則hang()掛起。從分析hang函式可知:uboot啟動過程中初始化板級硬體時不能出任何錯誤,只要有一個錯誤整個啟動就終止,除了重啟開發板沒有任何辦法。
(6)init_sequence中的這些函式,都是board級別的各種硬體初始化。

3.2 init_sequence中實現的函式

3.2.1 cpu_init

看名字這個函式應該是cpu內部的初始化,這個之前已經初始化過了,所以所以這裡是空的。

3.2.2 board_init

int board_init(void)
{
	DECLARE_GLOBAL_DATA_PTR;
#ifdef CONFIG_DRIVER_SMC911X
	smc9115_pre_init();
#endif

#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方便。可以參考2.2,可以看出把gd的宣告定義成一個巨集的原因就是我們要到處去使用gd,因此就要到處宣告,定義成巨集比較方便。
(3)網路卡初始化。CONFIG_DRIVER_DM9000這個巨集是x210_sd.h中定義的,這個巨集用來配置開發板的網路卡的。dm9000_pre_init函式就是對應的DM9000網路卡的初始化函式。開發板移植uboot時,如果要移植網路卡,主要的工作就在這裡。
(4)這個函式中主要是網路卡的GPIO和埠的配置,而不是驅動。因為網路卡的驅動都是現成的正確的,移植的時候驅動是不需要改動的,關鍵是這裡的基本初始化。因為這些基本初始化是硬體相關的。

(5)gd->bd->bi_arch_number

  • bi_arch_number是board_info中的一個元素,含義是:開發板的機器碼。所謂機器碼就是uboot給這個開發板定義的一個唯一編號。
  • 機器碼的主要作用就是在uboot和linux核心之間進行比對和適配。
  • 嵌入式裝置中每一個裝置的硬體都是定製化的,不能通用。嵌入式裝置的高度定製化導致硬體和軟體不能隨便適配使用。這就告訴我們:這個開發板移植的核心映象絕對不能下載到另一個開發板去,否則也不能啟動,就算啟動也不能正常工作,有很多隱患。因此linux做了個設定:給每個開發板做個唯一編號(機器碼),然後在uboot、linux核心中都有一個軟體維護的機器碼編號。然後開發板、uboot、linux三者去比對機器碼,如果機器碼對上了就啟動,否則就不啟動(因為軟體認為我和這個硬體不適配)。
  • MACH_TYPE在x210_sd.h中定義,值是2456,並沒有特殊含義,只是當前開發板對應的編號。這個編號就代表了x210這個開發板的機器碼,將來這個開發板上面移植的linux核心中的機器碼也必須是2456,否則就啟動不起來。
  • uboot中配置的這個機器碼,會作為uboot給linux核心的傳參的一部分傳給linux核心,核心啟動過程中會比對這個接收到的機器碼,和自己本身的機器碼相對比,如果相等就啟動,如果不相等就不啟動。
  • 理論上來說,一個開發板的機器碼不能自己隨便定。理論來說有權利去發放這個機器碼的只有uboot官方,所以我們做好一個開發板並且移植了uboot之後,理論上應該提交給uboot官方稽核併發放機器碼(好像是免費的)。但是國內的開發板基本都沒有申請(主要是因為國內開發者英文都不行,和國外開源社群接觸比較少),都是自己隨便編號的。隨便編號的問題就是有可能和別人的編號衝突,但是隻要保證uboot和kernel中的編號是一致的,就不影響自己的開發板啟動。

(6)gd->bd->bi_boot_params

  • bd_info中另一個主要元素,bi_boot_params表示uboot給linux kernel啟動時的傳參的記憶體地址。也就是說uboot給linux核心傳參的時候是這麼傳的:uboot事先將準備好的傳參(字串,就是bootargs)放在記憶體的一個地址處(就是bi_boot_params),然後uboot就啟動了核心(uboot在啟動核心時真正是通過暫存器r0 r1 r2來直接傳遞引數的,其中有一個暫存器中就是bi_boot_params)。核心啟動後從暫存器中讀取bi_boot_params就知道了uboot給我傳遞的引數到底在記憶體的哪裡。然後自己去記憶體的那個地方去找bootargs。
  • 經過計算得知:X210中bi_boot_params的值為0x30000100,這個記憶體地址就被分配用來做核心傳參了。所以在uboot的其他地方使用記憶體時要注意,千萬不敢把這裡給淹沒了。

3.2.3 interrupt_init

知識點1 :使用結構體來繫結暫存器

在這裡插入圖片描述

知識點2 :使用結構體來訪問暫存器

例:timers->TCFG0 = 0x0f00;
Timer Input Clock Frequency = PCLK / ( {prescaler value + 1} ) / {divider value}
所以prescaler+1 = 16
在這裡插入圖片描述
在這裡插入圖片描述

知識點3 :通過函式還獲取系統CLK
/* return PCLK frequency */
ulong get_PCLK(void)
{
	ulong hclk;                      
	uint div = CLK_DIV0_REG;       //將CLK_DIV0的地址賦值給div
	uint pclk_msys_ratio = ((div>>12) & 0x7);

	hclk = get_HCLK();	
	return hclk/(pclk_msys_ratio+1);
}

interrupt_init函式

int interrupt_init(void)
{
	//定義了一個結構體指標timers ,並繫結初始地址0xE2500000(該地址為暫存器TCFG0的首地址也是基地址)
	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);
}
/* return PCLK frequency */
ulong get_PCLK(void)
{
	ulong hclk;
	uint div = CLK_DIV0_REG;    //0xE0100000+0x20
	uint pclk_msys_ratio = ((div>>12) & 0x7);

	hclk = get_HCLK();	

	return hclk/(pclk_msys_ratio+1);
}

(1)看名字函式是和中斷初始化有關的,但是實際上不是,實際上這個函式是用來初始化定時器的(實際使用的是Timer4)。
(2)裸機中講過:210共有5個PWM定時器。其中Timer0-timer3都有一個對應的PWM訊號輸出的引腳。而Timer4沒有引腳,無法輸出PWM波形。Timer4在設計的時候就不是用來輸出PWM波形的(沒有引腳,沒有TCMPB暫存器),這個定時器被設計用來做計時。
(3)Timer4用來做計時時要使用到2個暫存器:TCNTB4、TCNTO4。TCNTB中存了一個數,這個數就是定時次數(每一次時間是由時鐘決定的,其實就是由2級時鐘分頻器決定的)。我們定時時只需要把定時時間/基準時間=數,將這個數放入TCNTB中即可;我們通過TCNTO暫存器即可讀取時間有沒有減到0,讀取到0後就知道定的時間已經到了。
(4)使用Timer4來定時,因為沒有中斷支援,所以CPU不能做其他事情同時定時,CPU只能使用輪詢方式來不斷檢視TCNTO暫存器才能知道定時時間到了沒。因為Timer4的定時是不能實現微觀上的並行。uboot中定時就是通過Timer4來實現定時的。所以uboot中定時時不能做其他事(考慮下,典型的就是bootdelay,bootdelay中實現定時並且檢查使用者輸入是用輪詢方式實現的,原理參考裸機中按鍵章節中的輪詢方式處理按鍵)
(5)interrupt_init函式將timer4設定為定時10ms。關鍵部位就是get_PCLK函式獲取系統設定的PCLK_PSYS時脈頻率,然後設定TCFG0和TCFG1進行分頻,然後計算出設定為10ms時需要向TCNTB中寫入的值,將其寫入TCNTB,然後設定為auto reload模式,然後開定時器開始計時就沒了。

總結:在學習這個函式時,注意標準程式碼和之前裸機程式碼中的區別,重點學會:通過定義結構體的方式來訪問暫存器,通過函式來自動計算設定值以設定定時器。

3.2.4 env_init

(1)env_init,看名字就知道是和環境變數有關的初始化。
(2)為什麼有很多env_init函式,主要原因是uboot支援各種不同的啟動介質(譬如norflash、nandflash、inand、sd卡·····),我們一般從哪裡啟動就會把環境變數env放到哪裡。而各種介質存取操作env的方法都是不一樣的。因此uboot支援了各種不同介質中env的操作方法。所以有好多個env_xx開頭的c檔案。實際使用的是哪一個要根據自己開發板使用的儲存介質來定(這些env_xx.c同時只有1個會起作用,其他是不能進去的,通過x210_sd.h中配置的巨集來決定誰被包含的),對於x210來說,我們應該看env_movi.c中的函式。
(3)經過基本分析,這個函式只是對記憶體裡維護的那一份uboot的env做了基本的初始化或者說是判定(判定裡面有沒有能用的環境變數)。當前因為我們還沒進行環境變數從SD卡到DDR中的relocate,因此當前環境變數是不能用的。
(4)在start_armboot函式中(776行)呼叫env_relocate才進行環境變數從SD卡中到DDR中的重定位。重定位之後需要環境變數時才可以從DDR中去取,重定位之前如果要使用環境變數只能從SD卡中去讀取。

  • 關於環境變數會在後續講解,所以暫時不深入探究

3.2.5 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);
}

如上函式解析:
(1)init_baudrate看名字就是初始化串列埠通訊的波特率的。
(2)getenv_r函式用來讀取環境變數的值。用getenv函式讀取環境變數中“baudrate”的值(注意讀取到的不是int型而是字串型別),然後用simple_strtoul函式將字串轉成數字格式的波特率。
(3)baudrate初始化時的規則是:先去環境變數中讀取"baudrate"這個環境變數的值。如果讀取成功則使用這個值作為環境變數,記錄在gd->baudrate和gd->bd->bi_baudrate中;如果讀取不成功則使用x210_sd.h中的的CONFIG_BAUDRATE的值作為波特率。從這可以看出:環境變數的優先順序是很高的。

3.2.6 serial_init

(1)serial_init看名字是初始化串列埠的。(疑問:start.S中呼叫的lowlevel_init.S中已經使用匯編初始化過串列埠了,這裡怎麼又初始化?這兩個初始化是重複的還是各自有不同?)
(2)SI中可以看出uboot中有很多個serial_init函式,我們使用的是uboot/cpu/s5pc11x/serial.c中的serial_init函式。
(3)進來後發現serial_init函式其實什麼都沒做。因為在彙編階段串列埠已經被初始化過了,因此這裡就不再進行硬體暫存器的初始化了。

3.2.7 console_init_f

(1)console_init_f是console(控制檯)的第一階段初始化。_f表示是第一階段初始化,_r表示第二階段初始化。有時候初始化函式不能一次一起完成,中間必須要夾雜一些程式碼,因此將完整的一個模組的初始化分成了2個階段。(我們的uboot中start_armboot的826行進行了console_init_r的初始化)
(2)console_init_f在uboot/common/console.c中,僅僅是對gd->have_console設定為1而已,其他事情都沒做。

3.2.8 display_banner

(1)display_banner用來串列埠輸出顯示uboot的標語
(2)display_banner中使用printf函式向串列埠輸出了version_string這個字串。

問題: 那麼上面的分析表示console_init_f並沒有初始化好console怎麼就可以printf了呢?
答:通過追蹤printf的實現,發現printf->puts,而puts函式中會判斷當前uboot中console有沒有被初始化好。如果console初始化好了則呼叫fputs完成串列埠傳送(這條線才是控制檯);如果console尚未初始化好則會呼叫serial_puts(再呼叫serial_putc直接操作串列埠暫存器進行內容傳送)。

(3)控制檯也是通過串列埠輸出,非控制檯也是通過串列埠輸出。究竟什麼是控制檯?和不用控制檯的區別?實際上分析程式碼會發現,控制檯就是一個用軟體虛擬出來的裝置,這個裝置有一套專用的通訊函式(傳送、接收···),控制檯的通訊函式最終會對映到硬體的通訊函式中來實現。uboot中實際上控制檯的通訊函式是直接對映到硬體串列埠的通訊函式中的,也就是說uboot中用沒用控制器其實並沒有本質差別。
(4)但是在別的體系中,控制檯的通訊函式對映到硬體通訊函式時可以用軟體來做一些中間優化,譬如說緩衝機制。(作業系統中的控制檯都使用了緩衝機制,所以有時候我們printf了內容但是螢幕上並沒有看到輸出資訊,就是因為被緩衝了。我們輸出的資訊只是到了console的buffer中,buffer還沒有被重新整理到硬體輸出裝置上,尤其是在輸出裝置是LCD螢幕時)
(5)U_BOOT_VERSION在uboot原始碼中找不到定義,這個變數實際上是在makefile中定義的,然後在編譯時生成的include/version_autogenerated.h中用一個巨集定義來實現的。

3.2.9 print_cpuinfo

int print_cpuinfo(void)
{
	uint set_speed;
	uint tmp;
	uchar result_set;

#if defined(CONFIG_CLK_533_133_100_100)
	set_speed = 53300;

//我們在x210_sd.h中有定義CONFIG_CLK_1000_200_166_133這個巨集
//並且通過這個巨集定義,我們確定了我們的倍頻和分頻等
#elif defined(CONFIG_CLK_1000_200_166_133)  
	set_speed = 100000;
#else
	set_speed = 100000;
	printf("Any CONFIG_CLK_XXX is not enabled\n");
#endif

//tmp=100000/(1000000000/1000000)=100
	tmp = (set_speed / (get_ARMCLK()/1000000));

	if((tmp < 105) && (tmp > 95)){
		result_set = 1;
	} else {
		result_set = 0;
	}

//列印各個時脈頻率
#ifdef CONFIG_MCP_SINGLE
	printf("\nS5PV210@%ldMHz(%s)\n", get_ARMCLK()/1000000, ((result_set == 1) ? "OK" : "FAIL"));
#else
	printf("\nS5PC110@%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;
}

uboot啟動過程中,這些資訊都是print_cpuinfo列印出來的。
在這裡插入圖片描述

知識點1 :格式說明符中的型別

(1)%ld表示資料按十進位制有符號長型整數輸入或輸出。

(2)%d表示資料按十進位制有符號整型數輸入或輸出。

(3)%u表示資料按十進位制無符號整型數輸入或輸出。

知識點2 :時脈頻率的計算
  • get_PLLCLK(int pllreg),計算nPLL的頻率函式,引數pllreg用來選擇A、M、E
  • ulong get_ARMCLK(void) ,計算匯流排上ARMCLK頻率的函式,通過時鐘框圖我們可知
    在這裡插入圖片描述
  • ulong get_HCLK(void),就是把APLL經過兩個分頻器得到HCLK的函式
  • ulong get_PCLK(void),就是在HCLK的基礎上,再經過一個分頻器所得到的PCLK

提醒: 通過分析程式我們可以看到倍頻/分頻都是直接使用對應暫存器中的對應位的資料,因為我們在x210_sd.h中進行了暫存器的配置,所以直接呼叫暫存器即可。

  • ?: 列印最開始的那個CPU是在那個地方列印的?

3.2.10 checkboard

checkboard看名字是檢查、確認開發板的意思。這個函式的作用就是檢查當前開發板是哪個開發板並且列印出開發板的名字。

int checkboard(void)
{
#ifdef CONFIG_MCP_SINGLE
#if defined(CONFIG_VOGUES)
	printf("\nBoard:   VOGUESV210\n");
#else
	printf("\nBoard:   X210\n");
#endif //CONFIG_VOGUES
#else
	printf("\nBoard:   X210\n");
#endif
	return (0);
}

3.2.11 init_func_i2c

這個函式實際沒有被執行,X210的uboot中並沒有使用I2C。如果將來我們的開發板要擴充套件I2C來接外接硬體,則在x210_sd.h中配置相應的巨集即可開啟。

3.2.12 dram_init

board_init中

	gd->bd->bi_arch_number = MACH_TYPE;
	gd->bd->bi_boot_params = (PHYS_SDRAM_1+0x100);
int dram_init(void)
{
	DECLARE_GLOBAL_DATA_PTR;

	gd->bd->bi_dram[0].start = PHYS_SDRAM_1;      //0x30000000
	gd->bd->bi_dram[0].size = PHYS_SDRAM_1_SIZE;  //512MB

#if defined(PHYS_SDRAM_2)
	gd->bd->bi_dram[1].start = PHYS_SDRAM_2;      //0x40000000
	gd->bd->bi_dram[1].size = PHYS_SDRAM_2_SIZE;  //512MB
#endif

//未使用
#if defined(PHYS_SDRAM_3)
	gd->bd->bi_dram[2].start = PHYS_SDRAM_3;
	gd->bd->bi_dram[2].size = PHYS_SDRAM_3_SIZE;
#endif

	return 0;
}

背景:關於DDR的配置(這部分是在dram_init中實現的):

(1)board_init中除了網路卡的初始化之外,剩下的2行用來初始化DDR。
(2)注意:這裡的初始化DDR和彙編階段lowlevel_init中初始化DDR是不同的。當時是硬體的初始化,目的是讓DDR可以開始工作。現在是軟體結構中一些DDR相關的屬性配置、地址設定的初始化,是純軟體層面的。
(3)軟體層次初始化DDR的原因:對於uboot來說,他怎麼知道開發板上到底有幾片DDR記憶體,每一片的起始地址、長度這些資訊呢?在uboot的設計中採用了一種簡單直接有效的方式:程式設計師在移植uboot到一個開發板時,程式設計師自己在x210_sd.h中使用巨集定義去配置出來板子上DDR記憶體的資訊,然後uboot只要讀取這些資訊即可。(實際上還有另外一條思路:就是uboot通過程式碼讀取硬體資訊來知道DDR配置,但是uboot沒有這樣。實際上PC的BIOS採用的是這種)
(4)x210_sd.h的496行到501行中使用了標準的巨集定義來配置DDR相關的引數。主要配置了這麼幾個資訊:有幾片DDR記憶體、每一片DDR的起始地址、長度。這裡的配置資訊我們在uboot程式碼中使用到記憶體時就可以從這裡提取使用(想象uboot中使用到記憶體的地方都不是直接用地址數字的,都是用巨集定義的)

uboot學習實踐

目標: 更改uboot名稱和版本
當前為:U-Boot 1.3.4 (Aug 6 2013 - 11:21:07) for x210
更改為:X-Boot 6.6.6 (Aug 6 2013 - 11:21:07) for x210

實現過程:
第一步:在主Makefile中更改版本和名稱,儲存退出
第二步: 在uboot資料夾下進行配置:make x210_sd_config
第三步:make 進行編譯連結
第四步:進入sd_fusing目錄下make clean —>make (為了產生和自己硬體匹配的mkbl1,我的是80386)
在這裡插入圖片描述
第五步:進入sd_fusing.sh檔案檢視你要燒寫的原始檔名稱和路徑是否正確,還有扇區。
在這裡插入圖片描述
第六步:接上SD卡,進行燒錄 使用./sd_fusing.sh /dev/sdb
燒寫完成後,使用開發板SD卡啟動進行驗證

實驗結果:
在這裡插入圖片描述
實驗總結:
1.程式燒錄的一般過程:
先配置(make x210_sd_config)後編譯(make),生成u-boot.bin
製作SD啟動卡:參考如上第4步到第6步
2.注意事項:
1)主Makefile中的執行地址是否正確(TEXT_BASE = 0xc3e00000)
2)在sd_fusing.sh檔案中注意檔案目錄和檔名是否和實際是對應的

3.2.13 display_dram_config

static int display_dram_config (void)
{
	int i;

#ifdef DEBUG
	puts ("RAM Configuration:\n");

	for(i=0; i<CONFIG_NR_DRAM_BANKS; i++) {
		printf ("Bank #%d: %08lx ", i, gd->bd->bi_dram[i].start);
		print_size (gd->bd->bi_dram[i].size, "\n");
	}
#else
	ulong size = 0;

	for (i=0; i<CONFIG_NR_DRAM_BANKS; i++) {
		size += gd->bd->bi_dram[i].size;
	}

	puts("DRAM:    ");
	print_size(size, "\n");
#endif

	return (0);
}

(1)看名字意思就是列印顯示dram的配置資訊。
(2)啟動資訊中的:(DRAM: 512 MB)就是在這個函式中列印出來的。
(3)思考:如何在uboot執行中得知uboot的DDR配置資訊?uboot中有一個命令叫bdinfo,這個命令可以列印出gd->bd中記錄的所有硬體相關的全域性變數的值,因此可以得知DDR的配置資訊。
在這裡插入圖片描述

init_sequence總結

  1. 巨集宣告:DECLARE_GLOBAL_DATA_PTR 網路卡的GPIO和埠的配置
  2. 機器碼設定gd->bd->bi_arch_number,啟動時的傳參的記憶體地址設定bi_boot_params表示uboot給linux kernel
  3. 設定timer4
  4. 設定環境變數
  5. 設定波特率
  6. 設定串列埠(之前已經設定過,此處函式為空)
  7. console(控制檯)的第一階段初始化
  8. 輸出如下這些資訊
    在這裡插入圖片描述

因為這個筆記已經太長了,不方便檢視,所以分為上下篇

相關文章