上電之后(bootload階段)該做什么
1怠缸、第一行程序
拿到空PCB板之后睁蕾,硬件工程師首先會(huì)測(cè)試各主要線路是否通連秕狰,各焊點(diǎn)是否有空焊帘皿、斷接或短路的情況东跪,然后逐個(gè)模塊焊接上去。之后需要驗(yàn)證系統(tǒng)上電之后鹰溜,CPU與各組件的供電電壓是否正常虽填,供給CPU的震蕩電路能否能夠正常起振,外部存儲(chǔ)器能否正常讀寫曹动。當(dāng)把我們的程序用JTAG工具下載到板子上后斋日,在真正調(diào)試系統(tǒng)前需要做好以下檢查:
- 利用調(diào)試工具,在程序的第一行設(shè)定斷點(diǎn)墓陈,確定程序有停下來(lái)恶守;
- 檢查CPU的程序計(jì)數(shù)器PC是否正確;
- 檢查CPU內(nèi)部RAM的內(nèi)容和我們下載的可執(zhí)行文件是否相同贡必;
- 程序的第一行命令為設(shè)定CPU狀態(tài)寄存器兔港,并觀察CPU的狀態(tài)寄存器是否如預(yù)期改變;
- 繼續(xù)單步執(zhí)行仔拟,確認(rèn)PC寄存器是否會(huì)跟著改變衫樊,且每行命令的執(zhí)行結(jié)果都是正確的。
檢查完以上各項(xiàng)后利花,只能證明板子上的電源電路以及CPU是正常的橡伞,接下來(lái)要繼續(xù)驗(yàn)證CPU與外圍設(shè)備盒揉,確認(rèn)板子的正確性與穩(wěn)定性后,才能進(jìn)行下一步測(cè)試兑徘。
2刚盈、基本硬件測(cè)試
既然Boot-Loader的責(zé)任是幫其它程序布置可運(yùn)行的環(huán)境,那么就要做好以下驗(yàn)證:
- CPU寄存器(狀態(tài)寄存器挂脑、通用寄存器藕漱、內(nèi)存映射寄存器)操作測(cè)試;
// 設(shè)定SP(Stack Point)寄存器
//
asm("xld.w %r15, 0x2000");
asm("ld.w %sp, %r15");
// 設(shè)定CPU的狀態(tài)寄存器
//
asm("xld.w %r15, 0x200010");
asm("ld.w %psr, %r15");
// 將寄存器0x300023的bit 1設(shè)為1
*(volatile unsigned char *)0x300023 |= 0x2;
- Stack Pointer的設(shè)置是否正確崭闲?函數(shù)調(diào)用是否正確運(yùn)行肋联?
- 中斷是量表設(shè)置是否正確?中斷矢量程序是否正常運(yùn)行?
- 存儲(chǔ)器初始化及其操作測(cè)試刁俭,保證所有的存儲(chǔ)器都可以正常讀寫橄仍;
- 將數(shù)據(jù)段載入RAM,對(duì)bss段設(shè)定初值牍戚,并將需要在RAM中運(yùn)行的程序載入到RAM侮繁。保證當(dāng)主程序執(zhí)行起來(lái)后,全局變量的初始值都是正確的如孝。
只有確保以上測(cè)試通過(guò)后才能進(jìn)行下一步工作宪哩。
(1)確認(rèn)函數(shù)調(diào)用能否正常運(yùn)行
正確設(shè)置堆棧(Stack)是函數(shù)能否成功調(diào)用的前提,在嵌入式系統(tǒng)開(kāi)發(fā)時(shí)第晰,系統(tǒng)要自行管理堆棧锁孟,如果管理不當(dāng),可能會(huì)發(fā)生函數(shù)調(diào)用或調(diào)用幾層之后就死機(jī)的狀況茁瘦。因?yàn)镃語(yǔ)言利用堆棧完成以下事項(xiàng):
- 存儲(chǔ)函數(shù)返回地址品抽;
- 函數(shù)調(diào)用時(shí)的參數(shù)傳遞(參數(shù)較多時(shí));
- 存儲(chǔ)函數(shù)內(nèi)部的局部變量甜熔;
- 中斷服務(wù)程序執(zhí)行時(shí)(發(fā)生中斷時(shí))圆恤,存儲(chǔ)CPU當(dāng)前狀態(tài)及返回地址。
堆棧頂點(diǎn)地址(Stack Point)的配置是一件很重要的事纺非,但卻極易被人忽略哑了。主要是在Windows或Linux上編程時(shí),操作系統(tǒng)在產(chǎn)生可執(zhí)行文件時(shí)烧颖,linker會(huì)自動(dòng)幫程序加上一段Startup Code弱左,其中就包含了Stack存儲(chǔ)器的配置。但在無(wú)操作系統(tǒng)的嵌入式系統(tǒng)中炕淮,調(diào)用任何函數(shù)之前都要先為其設(shè)置好堆棽鸹穑空間(Stack Point)。
當(dāng)用C語(yǔ)言調(diào)用了一個(gè)函數(shù),例如fun(a,b)们镜,編譯后的機(jī)器碼應(yīng)該包含以下動(dòng)作:
- 執(zhí)行指令push彻坛,將參數(shù)a和b存入Stack兑燥,同時(shí)堆棧指針SP減一足陨;
- 將當(dāng)前程序計(jì)數(shù)寄存器PC的值(也即返回地址:函數(shù)調(diào)用指令的下一條指令地址)存到堆棧中叫乌;
- 執(zhí)行指令Call,把PC的值設(shè)為函數(shù)fun()的地址嚼鹉,下一個(gè)被執(zhí)行的指令就是函數(shù)的第一條命令贩汉。
- 當(dāng)函數(shù)fun執(zhí)行時(shí),可利用當(dāng)前SP的值計(jì)算出參數(shù)a和b的地址锚赤;
- 如果函數(shù)內(nèi)部有局部變量匹舞,則依次將這些變量存到堆棧中。所以在嵌入式開(kāi)發(fā)中盡量不要定義size太大的變量线脚,否則有棧溢出(Stack Overflow)的風(fēng)險(xiǎn)赐稽。
- 當(dāng)函數(shù)執(zhí)行完畢,CPU會(huì)執(zhí)行ret命令浑侥,該命令會(huì)從Stack頂層取出返回地址姊舵,然后賦值給PC寄存器,則下個(gè)指令就會(huì)執(zhí)行函數(shù)后面的下一行指令锭吨,從而完成函數(shù)的調(diào)用蠢莺。
如果SP寄存器沒(méi)有設(shè)定到正確的地址寒匙,或是沒(méi)有配置足夠大的存儲(chǔ)區(qū)域作為椓闳纾空間,那么在調(diào)用函數(shù)時(shí)很可能就會(huì)出錯(cuò)锄弱。下圖就是一個(gè)椏祭伲空間溢出,破壞程序數(shù)據(jù)段的例子:
為避免以上情況的發(fā)生会宪,一般會(huì)選擇某塊RAM 的頂端(最大地址)當(dāng)作SP寄存器的初值肖卧,但具體棧的大小定位多少合適要根據(jù)具體軟硬件環(huán)境和項(xiàng)目要求。一般采用的方法是掸鹅,剛開(kāi)始稍微定義大一點(diǎn)塞帐,例如2KB-4KB左右,然后讓測(cè)試人員運(yùn)行完系統(tǒng)所有功能(函數(shù))后巍沙,記錄下SP在每次函數(shù)調(diào)用后的最小值葵姥,它與棧頂?shù)刂返牟罹褪撬枳钚?臻g句携,一般會(huì)稍微再放一點(diǎn)榔幸。
(2)確認(rèn)中斷系統(tǒng)能否正常運(yùn)行
負(fù)責(zé)寫驅(qū)動(dòng)程序的工程師要將中斷服務(wù)程序的地址填入中斷矢量表,并必須保證當(dāng)驅(qū)動(dòng)程序被執(zhí)行時(shí),中斷系統(tǒng)是正常的削咆。一般來(lái)說(shuō)主要做好以下工作:
- 中斷矢量表數(shù)組牍疏,詳細(xì)注解每個(gè)entry代表的中斷源;
- 如果是外接中斷控制器拨齐,要先完成中斷控制器的驅(qū)動(dòng)程序鳞陨,才能開(kāi)始中斷系統(tǒng)的測(cè)試。
- 設(shè)定CPU的中斷矢量表地址寄存器(有些CPU無(wú)中斷矢量表地址寄存器瞻惋,但它會(huì)指定某個(gè)固定地址為中斷矢量表的地址)
- 設(shè)定CPU的中斷控制寄存器(優(yōu)先級(jí)炊邦、中斷允許位等)
- 確定中斷被觸發(fā)后,對(duì)應(yīng)的ISR會(huì)被執(zhí)行熟史。
- 提供ISR的范例馁害,讓ISR編寫者不用知道中斷系統(tǒng)的細(xì)節(jié)。
// ISR模板
//
void isr_template(void)
{
// 將所有通用目的寄存器存到堆棧
//
asm("pushn %r15"); /*將r0 - r15 都存到堆棧中 */
//將ALR與AHR寄存器通過(guò)r1存到堆棧
//你無(wú)需搞清ALR和AHR是什么寄存器蹂匹,不同的CPU有不同的寄存器需要存儲(chǔ)
//
asm("ld.w %r1, %alr");
asm("ld.w %r0, %ahr");
asm("pushn %r1");
//調(diào)用C語(yǔ)言函數(shù)your_ISR碘菜,即真正ISR要處理的事寫在該函數(shù)里就行
//
asm("xcall your_ISR");
//從堆棧中取回被調(diào)用時(shí)的ALR和AHR寄存器的值
//
asm("popn %r1");
asm("ld.w %alr, %r1");
asm("ld.w %ahr, %r0");
//從堆棧中取回r1 - r15的值
//
asm("popn %r15");
//執(zhí)行中斷返回指令,返回被中斷的程序
//
asm("reti");
}
在以上各環(huán)節(jié)中容易出錯(cuò)的地方有:
- 中斷優(yōu)先級(jí)寄存器沒(méi)設(shè)正確限寞;
- 中斷矢量表中各個(gè)entry與中斷源的對(duì)應(yīng)關(guān)系錯(cuò)誤忍啸;
- 中斷矢量表地址設(shè)置錯(cuò)誤,很多CPU會(huì)要求中斷矢量表的地址要設(shè)置在偶數(shù)地址或是4的倍數(shù)履植,甚至是128KB的倍數(shù)计雌。
那如何判斷ISR有沒(méi)有被正確執(zhí)行呢?一般的方法是選擇一個(gè)簡(jiǎn)單的中斷源(例如除0錯(cuò)誤中斷)玫霎,在其ISR中設(shè)定一個(gè)斷點(diǎn)凿滤,然后單步執(zhí)行,看能否順利執(zhí)行ISR程序及正確返回中斷發(fā)生的地方(除零指令的下一條語(yǔ)句)庶近。
(3)存儲(chǔ)器測(cè)試
存儲(chǔ)器出問(wèn)題的地方有:
硬件方面:數(shù)據(jù)線翁脆、地址線連接錯(cuò)誤;
軟件方面:SRAM鼻种、NOR Flash反番、ROM不需要額外電路,直接可以使用叉钥,但SDRAM則還需要額外的SDRAM Controler電路才能使用罢缸,程序必須先設(shè)定好SDRAM Controler的配置(SDRAM大小、速度等)投队;
外部存儲(chǔ)器的時(shí)序設(shè)置枫疆,若時(shí)序設(shè)定太快,系統(tǒng)會(huì)不穩(wěn)定蛾洛,太慢养铸,則系統(tǒng)性能變差雁芙。一般CPU的Timing設(shè)定表會(huì)說(shuō)明應(yīng)該如何設(shè)定。
在進(jìn)行下部工作前要先測(cè)試存儲(chǔ)器的每一個(gè)Byte钞螟,確保讀寫(如果可以寫入的話)正常兔甘。方法是對(duì)每一個(gè)字節(jié)依次寫入0x00、0xFF鳞滨、0x55洞焙、0xAA,確保每一位都會(huì)被寫入0與1拯啦。
int SRAM_testing(void) { int i,counter =0; //待測(cè)RAM起始地址為0x2000000,大小為2MB. unsigned char *pointer = (unsigned char *)0x2000000; unsigned char data[4]={0x00,0xFF,0x55,0xAA}; for(i=0; i<4; i++) { // 逐一對(duì)每個(gè)字節(jié)寫入某特殊值 for(j=0; j<(8*1024*1024); j++) pointer[i] = data[i] // 逐一讀出每個(gè)字節(jié)澡匪,判斷寫入的值是否正確 for(j=0; j<(8*1024*1024); j++) pointer[i]==data[i]?::counter++; } return counter; //返回出錯(cuò)字節(jié)的個(gè)數(shù) }
對(duì)于只讀ROM,如何驗(yàn)證燒錄到存儲(chǔ)器中的數(shù)據(jù)和原始映像文件一致呢褒链?一般會(huì)采用校驗(yàn)和檢驗(yàn)法唁情。即分別計(jì)算原始映像文件和燒錄到ROM中文件的校驗(yàn)和是否相等。
/*************************************************************** Function Name: calculate_ROM_checksum Function Purpuse:計(jì)算起始地址為0x2000000甫匹,size為8MB存儲(chǔ)器的校驗(yàn)和 ****************************************************************/ unsigned long calculate_ROM_checksum(void) { unsigned long checksum = 0; unsigned char *pointer = 0x2000000; for(i=0; i<(8*1024*1024); i++) checksum += pointer[i]; return checksum; }
(4)CPU初始化
在Boot-Loader階段因該做好以下CPU相關(guān)的設(shè)定:
- 設(shè)定堆棧指針寄存器SP甸鸟;
- 設(shè)定狀態(tài)寄存器,禁止中斷兵迅;
- 設(shè)定中斷矢量表指針抢韭;
- 設(shè)定CPU執(zhí)行狀態(tài)(時(shí)鐘時(shí)序);
- 設(shè)定存儲(chǔ)器控制器(如果用到了類似SDRAM的存儲(chǔ)器)恍箭;
- 設(shè)定CPU操作各存儲(chǔ)器的時(shí)序刻恭;
- 設(shè)定CPU的PIN腳功能;
- 初始化外圍設(shè)備(LCD Controler扯夭、USB Controler鳍贾、SD卡接口等)
3、載入程序段與數(shù)據(jù)初始化
(1)載入data段
有初值的全局變量必須被存儲(chǔ)在可執(zhí)行文件中勉抓、被燒錄到ROM里贾漏。但執(zhí)行時(shí)因?yàn)檫@些全局變量的值會(huì)被改變候学,所以當(dāng)然不能在ROM里運(yùn)行藕筋,連接時(shí)必須尋址到RAM中。正因?yàn)檫@種 “存儲(chǔ)在ROM梳码,運(yùn)行在RAM” 的特性隐圾,才有傳輸data段的需要,且必須在所有程序使用全局變量前完成這些事掰茶。
上圖中暇藏,data段的內(nèi)容原本在可執(zhí)行文件中的rodata段之后,但執(zhí)行時(shí)濒蒋,需要將data段復(fù)制到RAM中的bss段之后盐碱。連接腳本如下:
.data __END_bss : AT(__END_rodata)
{
__START_data = .;
*(.data);
__END_data = .;
// 定義可在程序中使用的變量“__START_data_LMA”,表示data段的存儲(chǔ)起始地址LMA
__START_data_LMA = LOADADDR(.data);
//定義可在程序中使用的變量“__SIZE_DATA”,表示data段的大小
__SIZE_DATA = __END_data - __START_data;
}
傳輸程序如下:
/**************************************************
Function Name: copy_data_section()
Function Purpuse:將可執(zhí)行文件中的數(shù)據(jù)段復(fù)制到內(nèi)存中
***************************************************/
extern unsigned long *__START_data;
extern unsigned long *__START_data_LMA;
extern int __SIZE_DATA;
void copy_data_section(void)
{
int i;
unsigned long *dest = __START_data;
unsigned long *src = __START_data_LMA;
//假設(shè)data段的大小是4的整數(shù)倍個(gè)字節(jié)
for(i=0; i<(__SIZE_DATA/4); i++)
dest[i] = src[i];
}
(2)設(shè)定bss段
bss段的設(shè)定較為簡(jiǎn)單把兔,因?yàn)閎ss段里的成員都是沒(méi)有初始值的全局變量,所有根本不需要存儲(chǔ)空間瓮顽,在執(zhí)行時(shí)只要把bss段的執(zhí)行空間(VMA)都設(shè)為0即可县好。
/*******************************************
定義bss段,起始地址(VMA)從0開(kāi)始
******************************************/
.bss 0x0 :
{
__START_bss = .;
*(.bss);
__END_bss = .;
//定義可在程序中使用的變量:__SIZE_BSS
__SIZE_BSS = __END_bss - __START_bss;
}
設(shè)定bss段為0的代碼如下:
/**************************************************
Function Name: clear_bss_section()
Function Purpuse:將bss段清零
***************************************************/
extern unsigned long * __START_bss;
extern int __START_BSS;
void clear_bss_section(void)
{
int i;
unsigned long * dest = __START_bss;
//假設(shè)bss段的大小為4的整數(shù)倍字節(jié)大小
for(i=0; i<(__SIZE_BSS/4); i++)
dest[i] = 0;
}
Attention:在boot階段暖混,data段和bss段一定要先設(shè)定缕贡,否則執(zhí)行期間全局變量的值就不正確。換句話說(shuō)拣播,在設(shè)定完data和bss段之前晾咪,boot-load程序是不能使用全局變量的,如果一定要使用贮配,那就避免在定義全局變量時(shí)賦值谍倦,一定要在程序內(nèi)明確賦值才行。例如:
<img src="https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20201125224022.png" alt="Boot-Loader程序使用全局變量時(shí)必須謹(jǐn)慎" style="zoom:80%;" />
(3)載入text段
當(dāng)某個(gè)系統(tǒng)程序或者應(yīng)用程序模塊需要較高的執(zhí)行速度時(shí)泪勒,往往可以將他們復(fù)制到系統(tǒng)內(nèi)存中執(zhí)行剂跟。但系統(tǒng)內(nèi)存往往空間有限,不可能同時(shí)全部加載進(jìn)去酣藻。所以我們一般會(huì)寫一個(gè)函數(shù)曹洽,并尋址到同一個(gè)地址,在需要時(shí)才做載入的動(dòng)作辽剧。
各種類型的存儲(chǔ)器性能由大至小分別為:CPU寄存器送淆、CPU cache、CPU內(nèi)部RAM怕轿、外部SRAM偷崩、NOR Flash、SDRAM撞羽、Mask ROM阐斜、NAND Flash。
NAND Flash:價(jià)格低诀紊,容量大谒出,可把其想象成類似硬盤的設(shè)備,只不過(guò)無(wú)法直接尋址操作邻奠,程序無(wú)法再上面直接執(zhí)行笤喳;
NOR Flash:價(jià)格高,容量小碌宴,但讀數(shù)據(jù)快杀狡,可把其想象成可重復(fù)寫的ROM,程序可在上面直接運(yùn)行贰镣。
Mask ROM:成本高呜象,容量有限膳凝,但程序可直接在上面運(yùn)行;
SDRAM:性價(jià)比高恭陡,一般作為系統(tǒng)的外置內(nèi)存鸠项,程序可直接在上面運(yùn)行;
SRAM:價(jià)格昂貴子姜,容量小祟绊,一般作為系統(tǒng)的內(nèi)置內(nèi)存,程序可在上面直接運(yùn)行哥捕。
(4)幾種系統(tǒng)存儲(chǔ)器架構(gòu)
從NAND Flash啟動(dòng)的架構(gòu):
- image-20201125230828425
-
啟動(dòng)流程為:
- 上電后牧抽,CPU內(nèi)置程序會(huì)從NAND Flash的特定地址(一般是第一個(gè)block塊地址)讀出Boot-Loader程序到CPU的內(nèi)部?jī)?nèi)存中。
- CPU將控制權(quán)交給內(nèi)部存儲(chǔ)器中的Boot-Loader遥赚;
- Boot-Loader初始化SDRAM扬舒,再?gòu)腘AND Flash中將主程序載入到SDRAM中;
- Boot-Loader將控制權(quán)交給主程序凫佛。
獲取更多知識(shí)讲坎,請(qǐng)點(diǎn)擊關(guān)注:
嵌入式Linux&ARM
CSDN博客
簡(jiǎn)書博客
知乎專欄