详解嵌入式系统Boot-Loader

上电之后(bootload阶段)该做什么


1、第一行程序

拿到空PCB板之后,硬件工程师首先会测试各主要线路是否通连,各焊点是否有空焊、断接或短路的情况,然后逐个模块焊接上去。之后需要验证系统上电之后,CPU与各组件的供电电压是否正常,供给CPU的震荡电路能否能够正常起振,外部存储器能否正常读写。当把我们的程序用JTAG工具下载到板子上后,在真正调试系统前需要做好以下检查:

  • 利用调试工具,在程序的第一行设定断点,确定程序有停下来;
  • 检查CPU的程序计数器PC是否正确;
  • 检查CPU内部RAM的内容和我们下载的可执行文件是否相同;
  • 程序的第一行命令为设定CPU状态寄存器,并观察CPU的状态寄存器是否如预期改变;
  • 继续单步执行,确认PC寄存器是否会跟着改变,且每行命令的执行结果都是正确的。

检查完以上各项后,只能证明板子上的电源电路以及CPU是正常的,接下来要继续验证CPU与外围设备,确认板子的正确性与稳定性后,才能进行下一步测试。


2、基本硬件测试

既然Boot-Loader的责任是帮其它程序布置可运行的环境,那么就要做好以下验证:

  • CPU寄存器(状态寄存器、通用寄存器、内存映射寄存器)操作测试;
// 设定SP(Stack Point)寄存器
//
asm("xld.w  %r15, 0x2000");
asm("ld.w %sp, %r15");

// 设定CPU的状态寄存器
//
asm("xld.w  %r15, 0x200010");
asm("ld.w   %psr, %r15");

// 将寄存器0x300023的bit 1设为1
*(volatile unsigned char *)0x300023 |= 0x2;
  • Stack Pointer的设置是否正确?函数调用是否正确运行?
  • 中断是量表设置是否正确?中断矢量程序是否正常运行?
  • 存储器初始化及其操作测试,保证所有的存储器都可以正常读写;
  • 将数据段载入RAM,对bss段设定初值,并将需要在RAM中运行的程序载入到RAM。保证当主程序执行起来后,全局变量的初始值都是正确的。

只有确保以上测试通过后才能进行下一步工作。


(1)确认函数调用能否正常运行

正确设置堆栈(Stack)是函数能否成功调用的前提,在嵌入式系统开发时,系统要自行管理堆栈,如果管理不当,可能会发生函数调用或调用几层之后就死机的状况。因为C语言利用堆栈完成以下事项:

  • 存储函数返回地址;
  • 函数调用时的参数传递(参数较多时);
  • 存储函数内部的局部变量;
  • 中断服务程序执行时(发生中断时),存储CPU当前状态及返回地址。

堆栈顶点地址(Stack Point)的配置是一件很重要的事,但却极易被人忽略。主要是在Windows或Linux上编程时,操作系统在产生可执行文件时,linker会自动帮程序加上一段Startup Code,其中就包含了Stack存储器的配置。但在无操作系统的嵌入式系统中,调用任何函数之前都要先为其设置好堆栈空间(Stack Point)。

当用C语言调用了一个函数,例如fun(a,b),编译后的机器码应该包含以下动作:

  • 执行指令push,将参数a和b存入Stack,同时堆栈指针SP减一;
  • 将当前程序计数寄存器PC的值(也即返回地址:函数调用指令的下一条指令地址)存到堆栈中;
  • 执行指令Call,把PC的值设为函数fun()的地址,下一个被执行的指令就是函数的第一条命令。
  • 当函数fun执行时,可利用当前SP的值计算出参数a和b的地址;
  • 如果函数内部有局部变量,则依次将这些变量存到堆栈中。所以在嵌入式开发中尽量不要定义size太大的变量,否则有栈溢出(Stack Overflow)的风险。
  • 当函数执行完毕,CPU会执行ret命令,该命令会从Stack顶层取出返回地址,然后赋值给PC寄存器,则下个指令就会执行函数后面的下一行指令,从而完成函数的调用。

如果SP寄存器没有设定到正确的地址,或是没有配置足够大的存储区域作为栈空间,那么在调用函数时很可能就会出错。下图就是一个栈空间溢出,破坏程序数据段的例子:

局部变量太大导致Stack Overflow

为避免以上情况的发生,一般会选择某块RAM 的顶端(最大地址)当作SP寄存器的初值,但具体栈的大小定位多少合适要根据具体软硬件环境和项目要求。一般采用的方法是,刚开始稍微定义大一点,例如2KB-4KB左右,然后让测试人员运行完系统所有功能(函数)后,记录下SP在每次函数调用后的最小值,它与栈顶地址的差就是所需最小栈空间,一般会稍微再放一点。


(2)确认中断系统能否正常运行

负责写驱动程序的工程师要将中断服务程序的地址填入中断矢量表,并必须保证当驱动程序被执行时,中断系统是正常的。一般来说主要做好以下工作:

  • 中断矢量表数组,详细注解每个entry代表的中断源;
  • 如果是外接中断控制器,要先完成中断控制器的驱动程序,才能开始中断系统的测试。
  • 设定CPU的中断矢量表地址寄存器(有些CPU无中断矢量表地址寄存器,但它会指定某个固定地址为中断矢量表的地址)
  • 设定CPU的中断控制寄存器(优先级、中断允许位等)
  • 确定中断被触发后,对应的ISR会被执行。
  • 提供ISR的范例,让ISR编写者不用知道中断系统的细节。
// ISR模板
//
void isr_template(void)
{
    // 将所有通用目的寄存器存到堆栈
    //
    asm("pushn %r15"); /*将r0 - r15 都存到堆栈中 */
    
    //将ALR与AHR寄存器通过r1存到堆栈
    //你无需搞清ALR和AHR是什么寄存器,不同的CPU有不同的寄存器需要存储
    //
    asm("ld.w   %r1, %alr");
    asm("ld.w   %r0, %ahr");
    asm("pushn  %r1");
    
    //调用C语言函数your_ISR,即真正ISR要处理的事写在该函数里就行
    //
    asm("xcall your_ISR");
    
    //从堆栈中取回被调用时的ALR和AHR寄存器的值
    //
    asm("popn   %r1");
    asm("ld.w   %alr, %r1");
    asm("ld.w   %ahr, %r0");
    
    //从堆栈中取回r1 - r15的值
    //
    asm("popn   %r15");
    
    //执行中断返回指令,返回被中断的程序
    //
    asm("reti");
}

在以上各环节中容易出错的地方有:

  • 中断优先级寄存器没设正确;
  • 中断矢量表中各个entry与中断源的对应关系错误;
  • 中断矢量表地址设置错误,很多CPU会要求中断矢量表的地址要设置在偶数地址或是4的倍数,甚至是128KB的倍数。

那如何判断ISR有没有被正确执行呢?一般的方法是选择一个简单的中断源(例如除0错误中断),在其ISR中设定一个断点,然后单步执行,看能否顺利执行ISR程序及正确返回中断发生的地方(除零指令的下一条语句)。

(3)存储器测试

存储器出问题的地方有:

  • 硬件方面:数据线、地址线连接错误;

  • 软件方面:SRAM、NOR Flash、ROM不需要额外电路,直接可以使用,但SDRAM则还需要额外的SDRAM Controler电路才能使用,程序必须先设定好SDRAM Controler的配置(SDRAM大小、速度等);

  • 外部存储器的时序设置,若时序设定太快,系统会不稳定,太慢,则系统性能变差。一般CPU的Timing设定表会说明应该如何设定。

  • 在进行下部工作前要先测试存储器的每一个Byte,确保读写(如果可以写入的话)正常。方法是对每一个字节依次写入0x00、0xFF、0x55、0xAA,确保每一位都会被写入0与1。

  • int SRAM_testing(void)
    {
        int i,counter =0;
        //待测RAM起始地址为0x2000000,大小为2MB.
        unsigned char *pointer = (unsigned char *)0x2000000;
        unsigned char data[4]={0x00,0xFF,0x55,0xAA};
        
        for(i=0; i<4; i++)
        {    // 逐一对每个字节写入某特殊值
            for(j=0; j<(8*1024*1024); j++)
                pointer[i] = data[i] 
             // 逐一读出每个字节,判断写入的值是否正确      
            for(j=0; j<(8*1024*1024); j++)
                pointer[i]==data[i]?::counter++;
        }      
        return counter; //返回出错字节的个数  
    }
    
  • 对于只读ROM,如何验证烧录到存储器中的数据和原始映像文件一致呢?一般会采用校验和检验法。即分别计算原始映像文件和烧录到ROM中文件的校验和是否相等。

  • /***************************************************************
    Function Name: calculate_ROM_checksum
    Function Purpuse:计算起始地址为0x2000000,size为8MB存储器的校验和
    ****************************************************************/
    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相关的设定:

  • 设定堆栈指针寄存器SP;
  • 设定状态寄存器,禁止中断;
  • 设定中断矢量表指针;
  • 设定CPU执行状态(时钟时序);
  • 设定存储器控制器(如果用到了类似SDRAM的存储器);
  • 设定CPU操作各存储器的时序;
  • 设定CPU的PIN脚功能;
  • 初始化外围设备(LCD Controler、USB Controler、SD卡接口等)

3、载入程序段与数据初始化

(1)载入data段

有初值的全局变量必须被存储在可执行文件中、被烧录到ROM里。但执行时因为这些全局变量的值会被改变,所以当然不能在ROM里运行,连接时必须寻址到RAM中。正因为这种 “存储在ROM,运行在RAM” 的特性,才有传输data段的需要,且必须在所有程序使用全局变量前完成这些事。

执行时期的存储器使用状况

上图中,data段的内容原本在可执行文件中的rodata段之后,但执行时,需要将data段复制到RAM中的bss段之后。连接脚本如下:

.data __END_bss : AT(__END_rodata)
{
    __START_data = .;
    *(.data);
    __END_data = .;
    
    // 定义可在程序中使用的变量“__START_data_LMA”,表示data段的存储起始地址LMA
    __START_data_LMA = LOADADDR(.data);
    
    //定义可在程序中使用的变量“__SIZE_DATA”,表示data段的大小
    __SIZE_DATA = __END_data - __START_data;
}

传输程序如下:

/**************************************************
Function Name: copy_data_section()
Function Purpuse:将可执行文件中的数据段复制到内存中
***************************************************/
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;
   //假设data段的大小是4的整数倍个字节
   for(i=0; i<(__SIZE_DATA/4); i++)
       dest[i] = src[i];    
}

(2)设定bss段

bss段的设定较为简单,因为bss段里的成员都是没有初始值的全局变量,所有根本不需要存储空间,在执行时只要把bss段的执行空间(VMA)都设为0即可。

/*******************************************
定义bss段,起始地址(VMA)从0开始
******************************************/
.bss 0x0 : 
{
    __START_bss = .;
    *(.bss);
    __END_bss = .;
    
    //定义可在程序中使用的变量:__SIZE_BSS
    __SIZE_BSS = __END_bss - __START_bss;
}

设定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;
    //假设bss段的大小为4的整数倍字节大小
    for(i=0; i<(__SIZE_BSS/4); i++)
        dest[i] = 0;
}

Attention:在boot阶段,data段和bss段一定要先设定,否则执行期间全局变量的值就不正确。换句话说,在设定完data和bss段之前,boot-load程序是不能使用全局变量的,如果一定要使用,那就避免在定义全局变量时赋值,一定要在程序内明确赋值才行。例如:

<img src="https://cdn.jsdelivr.net/gh/Leon1023/leon_pics/img/20201125224022.png" alt="Boot-Loader程序使用全局变量时必须谨慎" style="zoom:80%;" />

(3)载入text段

当某个系统程序或者应用程序模块需要较高的执行速度时,往往可以将他们复制到系统内存中执行。但系统内存往往空间有限,不可能同时全部加载进去。所以我们一般会写一个函数,并寻址到同一个地址,在需要时才做载入的动作。

各种类型的存储器性能由大至小分别为:CPU寄存器、CPU cache、CPU内部RAM、外部SRAM、NOR Flash、SDRAM、Mask ROM、NAND Flash。

NAND Flash:价格低,容量大,可把其想象成类似硬盘的设备,只不过无法直接寻址操作,程序无法再上面直接执行;

NOR Flash:价格高,容量小,但读数据快,可把其想象成可重复写的ROM,程序可在上面直接运行。

Mask ROM:成本高,容量有限,但程序可直接在上面运行;

SDRAM:性价比高,一般作为系统的外置内存,程序可直接在上面运行;

SRAM:价格昂贵,容量小,一般作为系统的内置内存,程序可在上面直接运行。

(4)几种系统存储器架构

  • 从NAND Flash启动的架构:

  • image-20201125230828425
  • 启动流程为

    • 上电后,CPU内置程序会从NAND Flash的特定地址(一般是第一个block块地址)读出Boot-Loader程序到CPU的内部内存中。
    • CPU将控制权交给内部存储器中的Boot-Loader;
    • Boot-Loader初始化SDRAM,再从NAND Flash中将主程序载入到SDRAM中;
    • Boot-Loader将控制权交给主程序。

    获取更多知识,请点击关注:
    嵌入式Linux&ARM
    CSDN博客
    简书博客
    知乎专栏

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容