Jaa


BootLoader开发经验谈

image 

何宗键,微软Windows Embedded MVP,同济大学软件学院的讲师。目前负责"微软——同济移动与嵌入式中心"的工作。主要兴趣为基于Windows

CE的嵌入式系统与Windows Mobile移动设备。熟悉Platform Builder和Windows CE内核以及BSP开发。

 

原文地址: https://www.blogcn.com/User5/omale/blog/43575050.html

 

嵌入式操作系统在嵌入式软件开发中占的重要性越来越大。而提到嵌入式操作系统,Boot Loader恐怕是不可或缺的话题,无论是Windows CE还是嵌入式Linux,抑或是其他的嵌入式操作系统。大多数都需要Boot Loader来加载操作系统。当我们拿到一块新的板子,希望在上面运行某个嵌入式操作系统,通常第一件需要我们来做的事情就是移植该操作系统相对应的Boot Loader。而据有关文章分析,在嵌入式开发中,通常会在Boot Loader上消耗大量的时间人力和物力。

为什么Bootloader的开发这么复杂?为什么作为嵌入式操作系统辅助而存在的Bootloader反而会占据这么多的开发时间?Bootloader复杂在什么地方?我们该如何克服这些困难,去更快更好的开发Bootloader?本文就以Windows CE这一广泛应用的嵌入式操作系统为例,向读者分析和解决上面提出来的问题。本文不会介绍Bootloader的结构功能和启动流程等信息,如果读者对Windows CE的Bootloader不甚了解,希望补充这方面的知识,可以参考其他书籍。

1. Bootloader开发难点分析

(1) 硬件初始化:
当硬件上电之后,运行的第一条指令就是Bootloader的代码(如果各式各样的BIOS忽略不计的话)。Bootloader需要完成硬件初始化的一系列工作。然后才可以进入正常的逻辑,例如加载OS Image等。对于软件工程师而言,硬件的初始化工作是很冗长乏味的。需要详细阅读各类硬件的资料规范。然后就是一系列的对寄存器的操作。虽然Bootloader中不需要初始化板子上的所有的硬件,而只需要初始化最基本的可以让Loader正常工作的硬件就可以,有一些外设可以放到OS启动的时候,甚至驱动加载的时候再进行初始化不迟。即使是这样,要初始化的硬件也不在少数,对于一个典型的ARM系统而言,有可能要做的事:初始化内存控制器,初始化MMU,配置GPIO口,配置调试串口,对RTC进行读写操作,如果要通过以太网下载,还需要驱动网络接口……这一系列的工作,没有一个不是体力活。需要细心的琢磨,很有可能对寄存器某一个bit的粗心设置,就会导致整个Bootloader无法工作。所以这一部分内容通常马虎不得,需要耐心完成。

(2) 代码编写和构建:
由于Bootloader是最底层的代码,汇编语言肯定是少不了的了。就连一些整天喊“汇编已死”的人也不能否认,系统启动的那一段代码,还是需要汇编语言的。而汇编语言通常也是软件工程师们不太希望去碰的“硬骨头”。这也增加了Bootloader编写的困难。
代码写好之后,当然要编译成机器码才可以在板子上运行。目前的编译器大多数只会把代码编译成某些流行的可执行文件格式,例如Windows上的PE和*Nix上的ELF等。这些带有格式的可执行文件,也没有办法再目标设备上直接运行。所以,通常OS都会提供一些工具,把这些可执行文件去头去尾,转成纯二进制格式,这样才可以在目标设备上运行。例如ADS提供的FromELF工具与Windows CE提供的ROMImage工具就是完成这类工作的。通常,我们需要为这类工具做一些配置,例如告诉这些工具代码段放在什么地方,起始地址是多少等等。如果这些参数没有配置正确,很有可能最终生成的Bootloader的映像是不可用的。那么烧写到目标设备上,自然也就无法运行。同样,对于这些参数的配置,也需要仔细检查核对,一步步进行。

(3) 开发的效率:
Bootloader的另外一个开发困难的原因是它的开发效率。通常当我们做了一些代码修改之后,都需要把修改后的二进制文件使用烧写工具烧写到目标设备的Flash中,无论是NAND还是NOR Flash,烧写的过程都不快。所以,即使是改了一行代码,也需要经过编译->烧写->运行这样一个完整的流程。一般而言再快也要10分钟左右。这样算算,一个钟头可以修改个5次代码,一天可以修改个50次代码就相当不错了。机械的重复这一过程,经常会使开发人员感到开发效率低下,从而产生反感和抵触情绪。这也是Bootloader开发的一大劣势。一个解决的方法是使用硬件调试工具把Bootloader的映像直接灌到RAM里面运行,往RAM里面灌通常比烧写Flash要快。但是这样需要调试工具来初始化RAM,又有很多的其他逻辑上的和细节上的事情要做。

(4) 调试
上面说的几大问题,其实还都是可以克服的问题。其实在我看来,开发Bootloader最大的问题还是调试问题。试想:无论汇编多么难,我还是写好了,无论烧写多么烦,我还是烧写下去了,但是当我怀着激动的心情按下Reset键的时候,整个硬件设备毫无反应。我怎么知道我的代码写的正确还是不正确呢?如果不正确,我又怎么能定位到我的错误呢?现在的软件开发中,无论是编译型语言还是script,一般都会提供相应的Debugger,让开发人员来定位代码错误。“摸黑”写代码是软件工程师们最害怕的事情。代码出了问题,如果没有行之有效的手段来做问题定位,十有八九会造成项目“卡壳”。如果定位准确,那么问题也就解决了一大半了。所以归根结底,还是调试的方法论问题。Bootloader中难以调试,是因为可以使用的手段非常少,也不常规。在OS下开发应用程序用到的那些调试手段手段,在Bootloader的开发中通常都用不上。需要有“非常”手段来调试。下面的专题,就向大家介绍一些Bootloader的常用调试方法。

2. 一些调试技巧

(1) 硬件调试器
相信很多从应用开发走过来的开发人员,都会对Set Breakpoint,Step into, Step over等这些调试手段相当怀念。但是那些东西都是调试器在搞鬼。到了裸板上,可没有Debugger来帮我们了。那怎么办?好在我们有硬件调试器。很多CPU体系结构都提供了相应的硬件调试器,例如ARM CPU的仿真器。借助硬件调试器,我们可以完成汇编级的Set Breakpoint,Step into, Step over这些操作。这对于调试Bootloader来说,实在是太重要了。至少我们可以看到我们烧写下去的代码是否正确,然后可以看到是否在运行。这对于调试Reset后的第一段代码来说,实在是雨后春笋一般的珍贵。
但是硬件调试器也有很多不足:首先,它们一般都价格不菲,不用国内自己的D的话,几W是少不了了。其次,大多数硬件仿真器只能实现汇编级的调试,当代码进入了C语言之后,硬件调试器就显得力不从心了。第三,一劣质的仿真器,通常还无法对MMU打开后的虚拟内存进行仿真。所以限制也是很多的。

(2) 一闪一闪亮晶晶——LED灯
如果我们买不起硬件仿真器,或者手头上根本就没有。那么只好通过硬件的一些输出端口来输出一些信息,让我们知道代码到底运行到哪里了。在诸多的端口中,最简单的恐怕就是LED灯了。一般而言,用个几句汇编,就可以让LED闪烁起来。这样,我们就可以在代码中安插让LED闪烁的语句。来看我们的代码到底在哪里跑飞或者挂掉的。可惜LED等只能闪烁,想通过LED来获得更多的消息,是很困难的。例如我们想知道某个变量的值,用LED的闪烁表示出来,恐怕就很伤脑筋。用LED来闪烁出摩尔斯代码是一种可行方案,但是除了开开玩笑之外,恐怕真的去实践的人是不存在的……

(3) 终于可以Printf了——调试串口
目前为止,在Bootloader甚至整个OS开发过程中,串口可能是使用最广泛的调试端口了。它的设备简约却不简单。用一根串口线,把PC跟目标设备连接起来,这样我们可以通过串口来输出一些字符,在PC一端接收。终于,在有OS的应用程序开发中最传统的,最原始的调试方法printf就可以用了。在Bootloader的开发中,如果可以通过串口输出调试信息,那调试的手段就前进了一大步了。至少我们可以通过printf来跟踪目前代码在哪个函数里,分支语句走了哪个流程,某个变量的值到底是不是我们想要的……世界真美好。串口的问题是它不像LED那样想用就用,还是需要初始化的。好多嵌入式CPU都把串口控制器集成到了里面,甚至有一些会在CPU上电的时候自动初始化串口,这使得串口初始化相对简单。但是如果有一些串口是外挂的控制器,那么初始化一个串口有可能也需要耗费你半天时间。

(4) 走进新时代——内核调试器
随着开发进一步复杂,串口输出调试信息恐怕又不能满足我们的要求了。首先,输出的信息一多,很容易乱套。其次,输出信息业是需要花费时间的。在中断处理函数等一些时间敏感的地方使用串口输出,有时候依然不明智。如果能把远程调试器接上,那对BootLoader的调试跟对应用程序的调试就没有什么二致了(源码级设置断点,一步步跟,随时看某些数据结构的值,都成了可能)。Linux提供了GDB,Windows CE提供了KD。但是这都需要实现相当多的工作,实现一些调试器Stub。GDB可能串口就可走。但CE的KD通常是要通过KITL,通过以太网的。虽然复杂,长远来看还是非常有必要的,所以,当感觉其它手段力不从心的时候,可以考虑启用更高级别的调试器了。

(5) 其它歪门邪道
如果我的板子上没有硬件调试器,没有LED,串口也不工作,更别提内核调试器了,那我怎么办?别着急,只要你的硬件有输入输出设备,我们都是可以想出办法来的。只要能想办法往输出设备中输出一点东西,并且可以通过一些手段得到这些输出信息,我们调试的目的就达到了。例如:往喇叭里面输出一段杂音,往块设备上写一个扇区,往屏幕上画一些乱七八糟的东西……代码运行起来之后,只要不是当“瞎子”,都可以实现调试的目的。再走投无路的时候,这些手段不妨想想。

3. 让Bootloader更完美

以前看过一篇文章说,开发一个好的Bootloader,其工作量可能跟开发一个简单的操作系统相提并论。到这里为止,我们才刚刚开始,后面还有很长的路要走。

Comments