C/C++嵌入式系统编程

内存测试  时间:2021-01-16  阅读:()

MicbaelBarr著于志宏译作者简介MichaelBarr是Netrino公司(一个嵌入式系统共享软件和软件工程服务提供商)的创始人兼总裁.
Netrino公司鼓励所有职员通过为杂志撰稿和在业界会议演讲来分享自己的专业知识.
这些资料可以在公司的网站http://www.
netrino.
com找到.
Michael拥有马里兰大学的电机工程学士和硕士学位.
他的大部分时间都用在嵌入式软件、设备驱动和实时操作系统的开发上了.
他还喜欢写作、教书,并期待着开始下一部著作的创作.
目前他有好几个计划,其中包括一部小说.

前言首先需要弄清楚,你为什么希望你的学生学习某个主题,以及你希望他们学到什么,那么一般来说,你授课的方法或多或少就有了.
-RichardFeynman今天,几乎所有电子设备里面部包含了嵌人式的软件系统.
这些软件隐藏在我们的手表里、录像机里、蜂窝电话里,甚至可能在烤面包机里面.
军事上会使用嵌入式软件来引导导弹.
侦测敌方的飞行物.
外太空探测器和许多医疗仪器离开嵌人式软件几乎不可能工作.
设计人员不得不写所有的代码,实际上,成千上万的电子工程师计算机科学家和其他专业人员正在这样做.
我也是其中的一员,从我的个人经验来说,我很清楚掌握这门技术是多么的困难.
学校军从未开设有关嵌入式系统的课程.

而我也没能从哪个图书馆里找到一本有关这个题目的像样的书.
每一个嵌入式系统都是独特的,其硬件部分对它的应用目标来说是高度专用的.
这就导致了嵌入式系统编程的涉及面很广,而且可能会需要很多年才能掌握它.
不过,几乎所有的嵌入式软件开发都使用了C语言.
这本书就是要教你怎样在嵌入式系统中使用C和C的派生语言,C++.
即使你已经知道如问编写嵌入式软件,你还是可以从这本书里学到很多东西.
除了了解如何更有效地使用C和C++你还将会从本书中对常见的嵌人式软件问题的详细解释,并从本书所提供的源代码中得到益处.
本书中包含的高级主题有存储器检测和验证、设备驱动程序的设计和实现.
实时操作系统的内部机理,还有代码优化技术.
我为什么写这本书我曾经听到一个统计数字,在美国,平均下来大概每个人拥有八个微处理器.
我当时很惊讶,怎么可能呢难道我们周围真的有这么多计算机吗后来.
当我有更多时间来想这个问题的时候,我开始把我用过的并且可能含有一个微处理器的东西逐一列出来.
短短三分钟内,我的清单已经包含了十样物品了它们是:电视机、录音机、咖啡机、报时闹钟、录像机、微波炉、洗碗机、遥控器、烤面包机、还有数字式手表.
这还只是我的个人物品——我很快就可以拿出我工作中用到的另外十样东西.
进一步的发现是很自然的.
那些产品里的每一个都不仅仅包含一个处理器.

还有软件在里面.
最终,我知道在我一生里我想做些什么了.
我希望能用我的编程技能来开发这种嵌入式的什算机系统.
但是我如问能得到必要的知识呢当时我正在该大学的最后一年,而学校里迄今为止没有关干嵌入式系统编程的课程.
幸运的是、虽然我那时还处在学习的过程中但当我毕业的时候我还是找到了一家公司,从事编写嵌人式软件的工作.
不过在这里我必须要靠自己的努力,因为为数不多的了解嵌人式软件的几个人通常都非常的忙,以至于很少有时间来解答我的问题,所以找到处找能给我教益的书、最后.
才发现我必须自学所有的东西因为我从没有找到这么一本书,并且我很奇怪为什么会没有人来写这么一本书.
现在我决定自己来写这样一本书了.
在此过程中我也发现了为什么以前没有人做这件事.
关于这个题目最困难的是,决定什么时候可以收笔封稿了.
每一个嵌八式系统都是独一无二的,并且就我所知,每一条法则同时都会存在例外倩况.
不过,我已经尝试着提取出这个主题的本质的东西,并且仅仅讲述嵌人式系统程序员们必须要了解的那些部分.
面向的读者这是一本关于使用C和C++来进行嵌人式系统编程的书.
同样,这里假定读者已经有了一些编程经验,并且至少熟悉这两种语言的语法.
如果你比较熟悉基本的数据结构例如链表等,也会有些帮助.
这本书并不要求你在计算机硬件方面了解很多,但是希望你愿意由这本书而学一点有关硬件的知识.
这毕竟是一个嵌入式程序员工作的一部分.
写这本书的时候,在我的脑海里有两类读者.
第一类是初学者——正像我刚从大学毕业的时候那样.
她会何一些计算机科学或工程的背景,并有几年编程经验.
初学者感兴趣的是如何为一个既有的设备写嵌人式程序,却不能肯定该如问着手去做.
看完前五章后,她就能够用她的编程技术来开发简单的嵌入式程序了.
本书的其他部分可以作为她在以后的职业生涯里遇到更高级的主题时的参考.
第二类读者已经是嵌入式系统程序员了.
她熟悉嵌入式硬件,并目知道怎样来为此编写软件.
但是她正在寻找一本参考书来解释一些关键问题.
出许这位嵌入式系统用序员一直在用汇编语言编程,并且刚接触C和C++不久.
这样的话,这本书会教给她如问在嵌入式系统里使用这些语言.
后面的章节还会提供她所需要的更高级的材料.
不论你是否属丁上述两种读者之一,我还是希望这本书能够以一种友好和方便的形式给你一些帮助.
本书的组织本书包括十章、一个俘虏、一个词汇表,还有一个带注释的参考书目列表.

这十章恰好可以分为两个部分.
第一部分包含第一到第五章,主要面向嵌人式系统的初学者.
这些章节应该按照它们出现的次序完整地读一下,这将快速地带给你有关嵌入式软件开发的基础知识.
结束了第五章之后你就可以独立开发一些小的嵌入式软件了.
第二部分包括第六到第十章,讨论了不论有没有经验的嵌入式程序员都很感兴趣的一些高级主题.
这些章节基本上各自独立,可以按照随意的次序来读.

另外,第六到第九章包含的示例程序可能会对你将来的嵌人式系统项目有所帮助.
z第一章"引言".
介绍嵌入式系统.
其中定义了若干术语,给出了一些例子并且说明了为什么选择C和C++来作为本书的编程语言.
z第二章"你的第一个嵌人式程序".
引导你尝试用C语言编写一个简单的嵌入式程序的全过程.
这比较类似于其他很多编程书籍里的"Hello,World"的例子.
z第三章"编译、链接和定址".
介绍了一些软件工具.
你将用它们来为一个嵌人式处理器生成可执行文件.
z第四章"下载和调试".
介绍将可执行程序调人一个嵌入式系统的各种技术手段,同时也描述了你可以使用的调试工具和技术.
z第五章"接触硬件".
描述了学习一个不熟悉的硬件平台的简单过程.
结束本章后,你已经能够书写和调试简单的嵌人式程序了.
z第六章"存储器".
讲解了关于嵌人式系统内的存储器作所需要知道的全部知识.
这一章还包括了存储器测试和闪速存储器驱动程序的源代码实现.

z第七章"外围设备".
说明了设备驱动程序的设计和实现技术,同时包含了一个通用外围设备(定时器)的示范驱动程序.
z第八章"操作系统".
包含了一个可以用在任何嵌入式系统中的很简单的操作系统.
这有助于你决定你是否需要这么一个操作系统,如果需要的话,是买一个还是干脆自己写一个.
z第九章"合成一个整体'.
进一步拓展前面章节学到的关于设备驱动程序和操作系统的知识.
本章讲解了如何控制更复杂的外设,同时引入了一个完整的示范应用来把你学过的东西综合到一起.
z第十章"优化你的代码".
描述了如何在增加代码运行速度的同时,减少你的嵌入式软件对存储器的需求.
这包括使用一些技巧来刊用最有效的C++特性,而不导致显著的性能损失.
在整本书里,我一直在努力在特定的例子和通用的知识之间保持平衡,也就是尽可能地消除微小的细节,使这本书更加易读.
像我一样,通过阅读示例你会从这本书里得到最大的收获,但是应该只把它们作为理解重要概念的工具.

记住不要的在任问一个电路板或芯片的细节里面.
在理解了全面的概念以后,你将能够把它应用在你所碰到的任何嵌人式系统中.
在排版和其他方面的约定本书使用了如下的一些印刷约定:斜体{italic}当文件、函数、程序、方法、例程和选项出现在段落中的时候,用来表示它们的名宇.
斜体也用来强调或引人新的术语.
等宽(constantwidth)用来显示文件的内容和命令的输出.
在段落体中.
这种字体用来表示关键字、变量名、类、对象、参数和其他代码片断.
等宽祖体(constantwidthbold)用来在示例里表示你输人的命令和选项.
其他约定是和性别与角色有关的.
关于性别,我有意在全书区分使用了"他"和"她".
"他"代表奇数章节而"她"代表偶数章节.
关于角色我偶尔会在我的讨论中区对一下硬件工程师、嵌人式软件工程师和应用程序员的不同任务.
但是这些称谓只是工程师个体的角色,需要注意到的是一个人充当多个角色是常有的事.
在线取得示例这本书包含很多示例源计码,除了最小的单行代码以外都可以在线获得.
这些示例按照章节来组织、并包含了build指令(makefile)来帮助你重建每个可执行文件、完整的文件可以通过FTP得到,在ftp://ftp.
oreilly.
com/examples/nutshell/embedded_c/.
建议与评论我们已尽全力保证本书内容的正确性,但你仍可能发现有些内容不对(甚至可能是我们出了错误!
).
你的建议将帮助我们使下一版更加完美,请告诉我们你找到的错误以及你的建议,写信到:美国:O'Reilly&Associates,Inc.
101MorrisStreetSebastopol.
CA95472中国:100080北京市海淀区知春路49号希格玛公寓B座809室奥莱理软件(北京)有限公司询问技术问题或对本书的评论,请发电子邮件到:info@mail.
oreilly.
com.
cn最后,你可以在WWW找到我们:http://www.
oreilly.
comhttp://www.
oreilly.
com.
cn个人说明和致谢我曾经很想写一两本书,但是现在我这么做了以后,我必须承认我开始的时候非常的天真.
我甚至不知道要做多少工作、另外还会有多少人被牵扯进来.

不过令我吃惊的是,找一个愿意出版我的书的出版社是如此容易,我原以为这会是很困难的一件事.
从提议写书到出版,这个计划花了两年才完成.
这是因为我一直在全时工作并且希望尽可能保持我的社会生活所致.
要是我早知道我会熬夜到很晚来为最后的底稿而苦恼的话,我也许会放弃我的工作来早点交付这本书.
但过继续工作对这本书很有好处(同时对我的银行账户也有好处).
这使我有机会对以和很多嵌入式硬件和软件的专家进行广泛的讨论他们巾的很多人通过审阅部分成全部的书稿为这本书做出了直接的贡献.
我非常感或以下诸位,与我分享了他们的知识并且一直在帮助我的工作:TonyBennet、PaulCabler(和其他来自Arcom的很棒的人们),MikeCorish、KevinD'Souza、DonDavis、SteveEdwards、MikeFicco、BarbaraFlanngan、JackGanssle、StephenHarpster(他在看完早期书稿后管我叫"断句王")、JonathanHarris、JimJesen、MarkKohler、AndyKollegger、JeffMallory、IanMiller、HenryNeugauss、ChrisSchanck、BrianSilverman、JohnSnyder、JasonSteinhorn(正是他的流畅的语法素养和技术批评的眼光使这个书值得一读)、IanTaylar、LindseyWereen、JeffWhipple和GregYoung.
我还要感谢我的编辑AndyOram.
要是没有对我最初建议的巨大热情、超乎寻常的耐心和持续的鼓励,这本书将永远不会完成.
最后,我要感谢AplaDharla,感谢她在这个漫长的过程中给予我的支持和鼓励.
MichaelBarrmbarr@netrino.
com第一章引言我想全世界计算机市场也许会有五台.
——ThomasWatson(托马斯·沃森),IBM公司主席,1943没有人会想在家里放一台计算机.
——KenOlson(肯·奥尔森),DEC公司总裁,1977最近几十年里最令人惊讶的事,莫过于计算机逐渐占据了人类生活的主要地位.
今天在我们的家里和办公室里,计算机的数量要比使用它们来生活和工作的人还要多,只是这些计算机里有很大一部分我们没有意识到它们的存在罢了.

在这一章里,我将说明什么是嵌人式系统,以及可以在哪里找到它们.
同附会介绍一下嵌人式编程的主题,说明一下为什么本书采用C和C++语言讲述,另外简单介绍一下示例中所用到的硬件环境.
什么是嵌入式系统一个嵌入式系统(embeddedsystem)就是一个计算机硬件和软件的集合体,也许还包括其他一些机械部件,它是为完成某种特定的功能而设计的.
一个很好的例子就是微波炉.
几乎每个家庭都有一台,并且每天都有上千万台微波炉在被人们使用着,但是很少有人意识到有处理器和软件在帮助他们做饭.

这和家里的个人计算机形成了鲜明的对比.
同样是由计算机硬件和软件,还有机械部件(比如硬盘)组成的,个人计算机却不是用来完成某个特定功能的.

相反它可以做各种不同的事情.
很多人用通用计算机(general-purposecomputer)来区分这一点.
在发货的时候,通用计算机就像一块没有字的黑板,制造商并不知道用户要拿它来做什么.
一个用户可能会用它来做文件服务器.
另一个只用来玩游戏,还有一位可能会用它来写下一部伟大的美国小说.
本章内容:z什么是嵌入式系统z各种实现间的差异zC:最基本的必需品z关于硬件的一些说明而嵌入式系统常常是一些更大的系统中的一个组成部分.
比如,现代的轿车或卡车里就包含了很多嵌人式系统.
一个嵌人式系统会被用来控制防刹车锁死,另一个监控车辆的气体排放情况,还有一个用来在仪表板上显示信息.
虽然不是必需的,但在某些情况下,这些嵌人式系统会通过某种通信网络互相连起来.

为了不至于混淆你的思路,有必要指出,通用计算机本身就是由很多嵌入式系统组成的.
比如,我的电脑包含了键盘、鼠标、显示卡、调制解调器、硬盘、软盘和声卡,它们中的每一样都是一个嵌入式系统.
每个设备都包含处理器和相应的软件来完成特定的功能.
比如凋制解调器就是用来在模拟电话线上收发数字信号用的.
正是如此,所有其他的设备也都能归纳出这么一句话来.
如果一个嵌入式系统设计得很完善,那么它的使用者完全可以忽略它内部的处理器和软件的存在.
微波炉、录像机和报时闹钟就是很好的例子.
在某些情况下,用同样的功能的定制集成电路硬件来代替上面所说的处理器和软件,也能做出具有同样功能的设备来.
不过,如果真是这样用纯粹的硬件来设计的话,在灵活性上就会丧失不少了,改几行软件怎么说也要比重新设计一块硬件电路来得方便和便宜.
过去和将来本章开头定义的嵌人式系统的第一个产品直到1971年以后才出现.
这一年,Intel发布了世界上第一块微处理器,4004,主要被日本的Busicom公司用来生产商用计算器.
1969年,Busicom请Intel为他们的每一种新式计算器分别设计一种定制的集成电路,Intel则拿出了4004.
Intel没有为每一种计算器分别进行设计,而是设计了一种可以用在所有型号上的通用电路.
这个通用处理器被设计来读取存在外部存储芯片里的一系列指令(软件).
Intel的想法是通过软件的设计可以为每一种计算器提供各自的特性.
这种微处理器在一夜之间就成功了,并且在以后的十年中获得了广泛的应用.
早期的嵌入式应用包括无人空间探测器、计算机控制的交通信号灯以及航空灯光控制系统.
在整个80年代,嵌人式系统静悄悄地统治着微处理器时代,并把微处理器带人了我们个人和职业生活的每一个角落.
装有嵌人式系统的电子设备已经充斥了我们的厨房(烤面包机、食物处理机、微波炉)、卧室(电视、音响、遥控器)和工作场所(传真机、寻呼机、激光打印机、点钞机和信用卡读卡机).
嵌入式系统的数量看起来肯定会继续迅速增长.
已经有很多具有巨大市场潜力的新的嵌入式设备了:可以被中央计算机控制的调光器和恒温器、当小孩子或矮个子的人在的时候不会充气的智能气囊、掌上电子记事簿和个人数字助理(PDA)、数码照相机和仪表导航系统.
很明显,掌握一定技能并且愿意从事下一代嵌入式系统设计的人将会获得很多的机会.
实时系统现在很有必要介绍一下嵌入式系统的一个子集.
按照通常的定义,实时系统(real-timesystem)就是有一定时间约束的计算机系统.
换句话说,实时系统可以部分地从及时完成计算或判断的能力来辨别.
这些重要的计算有完成的明确期限,并且,对实际应用来说,一个延期的反应就像一个错误的结果一样糟糕.

如果一旦延期会产生什么结果,是至关重要的问题.
例如,如果一个实时系统是飞机飞行控制系统的一部分,那么一个延期的计算就可能会使乘客和机组人员的生命受到威胁.
而把这个系统用在卫星通信环境下,危害也许可以限制在仅仅一个损坏的数据包.
在更严格的情况下,很可能这个时间期限是"硬性"需求的,也就是说,这个系统是个"硬"实时系统,和它对应的就有"软"实时系统了.
本书中所有的主题和示例都可以应用到实时系统中.
不过一个实时系统的设计者必须更加细心,他必须保证软件和硬件在所有可能的情况下都能可靠工作.
同时,根据人们生活对该系统可靠执行的依赖程度,这种保证一定要有工程计算和描述性的论文加以支持.
各种实现间的差异与为通用计算机设计的软件不同,嵌人式软件通常无法在不做显著修改的情况下在其他嵌入式系统中运行.
这主要是由底层硬件之间的明显不同所致.
每个嵌人式系统的硬件都是为特定的应用专门调整过的,这样才能使系统的成本保持很低.
所以,不必要的电路就被省去了,硬件资源也尽可能地共享使用.

在这一节里你会学到哪些硬件特性是所有嵌人式系统共有的以及其他方面为什么又会有如此多的不同之处.
通过定义我们知道所有的嵌入式系统都包含处理器和软件,那么还有哪些特性是它们共有的呢当然,要想执行软件,就一定要有存储执行代码的地方和管理运行时数据的临时存储区,这就分别要用到ROM和RAM;任何嵌入式系统都会有一些存储区.
如果只要求很少的存储量,也许就使用与处理器在同一芯片里的存储器,否则就需要使用外部存储芯片来实现.
所有嵌人式系统都包含其种输入和输出.
例如,一个微波炉的输人就是前面板上的按钮和温度探测器,输出就是人可阅读的显示信息和微波射线.
嵌入式系统的输出几乎总是它的输入和其他一些因素的函数、包括花费的时间、当前的温度等等.
输入常见的形式有传感器和探测器.
通信信号或物理世界的某些变化.
图1-1给出了嵌入式系统的一个常见的例子.
图1-1一个基本的嵌入式系统除了上述几个共同点,嵌入式系统的其他部分通常是互不相同的.
实现之间的差异是由不同的设计侧重导致的.
每个系统都是面向完全不同的一整套需求,这些需求的折中考虑直接影响了产品的开发过程.
例如,如果一个系统要求成本低于10美元,那么就有可能要牺牲一些处理性能或可靠性才能达到要求.
当然,生产成本只是嵌入式硬件开发人员需要考虑的一个可能的限制而已.

其他要考虑的设计需要还包括:处理能力要完成目标所需的运算能力.
一个常用来衡量运算能力的指标是MIPS(以百万计算的每秒可执行的指令数量).
如果两个处理器的指标分别是25MIPS和40MIPS,那么就说后者的运算能力更强一些.
但是,还需要考虑处理器的其他一些重要特性.
其中之一是寄存器字长,一般会是8到64位.
现在的通用计算机一般使用32位或64位的处理器,但是嵌入式系统通常仍使用更老、更便宜的8位和16位处理器.
存储器用来保存执行代码和操作数据的存储器的容量.
硬件设计人员必须事先做出估计并且在软件开发完成之后增加或减少实际的容量.
存储容量也会影响处理器的选择,通常寄存器的字长构成了处理器可存取的存储容量的限制,例如,一个8位的寻址寄存器可以确定256个存储位置之一(注1).
开发费用硬件软件开发过程所需的费用.
这是一个确定的、一次性的花费,所以这也许无关紧要(通常对于大批量产品),也许需要仔细衡量(在只生产少量产品的情况下).
批量生产费用和开发费用的折中考虑主要由期望的生产批量和销量所决定.
例如,通常不会选择为一个小批量产品开发自己的专用硬件模块.

预计的生命周期系统必须延续多久(平均估算)一个月、一年、或者十年这影响到从硬件的选择到开发和生产费用方面的各种设计决策.
可靠性最终产品应具有什么程度的可靠性如果只是一个儿童玩具,那么不需要总是工作正常,但是如果是航天飞机或小轿车的一部分,那就最好在任何时间都要工作正常.
除了这些常见的要求之外,系统还有自己详细的功能要求.
正是这些要求赋予了嵌人式系统不同的特性,比如微波炉、起搏器或寻呼机.
注1:当然,寄存器的字长越小,处理器就更可能需要采取一些策略,如多个地址空间,以支持更大的内存.
几百字节是不足以做太多事情的.
即使对8位处理器而言,几千个字节也可能只是最低要求.
表1-1说明了前面谈到的设计要求的可能的取值范围.
这些只是估计数字,并不需要严格采用.
在某些情况下,几个标准是联系在一起的.
比如,处理能力的增加也会导致产品成本的增加.
同时,我们也可以设想同样是增加处理能力也会通过减少硬件和软件设计的复杂性来降低开发成本.
所以每一列的数值并不是一定要同时满足.
表1-1嵌入式系统常见的设计需求分类低中高处理器4或8位16位32或64位存储器1MB开发费用$1000000生产成本$1000批量10000预计的生命周期几日、几周或几月几年十年可靠性可以偶尔故障必须可靠工作必须无故障运行为了同时说明两个嵌人式系统之间的差异,以及这些设计需求对开发过程的影响,我会比较详细地介绍三个嵌入式系统.
我的想法是在具体讨论嵌人式软件开发之前,先从系统设计人员的角度考虑一下问题.
数字手表计时工具从日、滴漏、沙漏一路发展而来就是数字化手表.
它的特性包括显示日期和时间(通常精确到秒),以百分秒计时,还有在每个整点发出烦人的响声.
正如它所表现的那样,这些都是非常简单的功能,并不需要很多的处理能力或存储器.
实际上,采用处理器的唯一原因,只是为了使硬件设计可以支持一系列的型号.
典型的数字表包含一片简单、便宜的8位处理器.
因为这种处理器不能寻址较多的存储器,所以这类处理器一般都自带了片上ROM.
如果有足够的寄存器的话,那这个产品连RAM也用不着了.
实际上,所有电子部件——处理器、存储器、计数器和实时时钟,几乎都做在同一个芯片上,这块表还剩下的硬件就包括输人(按钮)和输出(LCD或扬声器)了.
数字手表的设计者的目标是用超低的生产成本来提供一个相对可靠的产品.

如果在生产后发现部分手表比其他大多数要更精确些,那么这些手表就会被冠以某个品牌以更高的价格出售.
或者也可以通过折扣分销渠道来获得利润.
对于低价品种,则可以把停止按钮和杨声器去掉.
这虽然会失去一些功能却几乎不需要改动软件.
当然,所有这些开发的花费可能会相当高,但随着成千上万只表卖出去,收入会源源不断地增加.
视频游戏机当你从娱乐中心取出任天堂Nintendo-64或者SONYPlayStation(PS)的时候,你就将要使用一个嵌人式的系统.
有时候这些机器比同级别的个人计算机的性能还要好,不过面向家用市场的视频游戏机同个人计算机比起来还是要便宜一些.
正是高的处理能力和低的生产成本这两个相抵触的要求,使得视频游戏机的设计师们经常熬夜工作(当然他们的孩子可就过得不错喽).
只要最终产品的生产成本能比较低——一般在100美元左右.
生产视频游戏机的公司一般不去关心系统的开发费用.
他们甚至鼓励他们的工程师们设计专用的处理器,因而每一次开发费用都比较高昂.
所以,尽管在你的视频游戏机里会有一个64位的处理器,它和一个64位个人计算机里的处理器可不一定是一样的.
一般来说,这个处理器是专门用来满足它要运行的视频游戏的要求的.

因为在家用视频游戏市场上生产成本是如此重要,设计人员也会用一些手段来分摊成本.
比如,一个常用的技巧是尽可能把存储器和其他外围电路从主电路板上挪到游戏上.
这样就会在降低游戏机的成本同时增加了每一个游戏的价格.
这样,一个系统也许会配备一个强劲的64位处理器但主板却只带了几兆内存.
这些内存只够启动机器让它可以存取游戏卡上的存储器.
火星探测器1976年,两个无人飞船抵达火星.
它们的任务是采集火星表面的岩石样本,并在分析其化学成分后把结果传回给地球上的科学家们.
那个"海盗船"的任务使我感到颇为吃惊.
因为我现在被一些几乎每天都要重新启动的个人计算机包围着,所以我发现对多年前的这些科学家和工程师真是很伟大,他们成功地设计了两台计算机井且使它们在五年里经过了3400万英里的旅程依然工作正常.
很明显,在这些系统中,可靠性是最重要的要求.
如果存储芯片损坏或者软件存在缺陷以至于导致运行崩溃,或者一个电连接在碰撞之下断开,结果会如何呢根本没有办法防止这些问题的发生.
所以必须通过增加冗余电路或额外的功能来消除这些隐患:使用额外的处理器、特殊的存储器检验、当软件死锁后用一个硬件定时器来复位系统等等,各种手段,不一而足.
最近,美国宇航局启动了"探路者"计划,主要的目标就是论证一下以有限的预算到达火星的可行性.
当然,随着70年代中期以来技术的极大发展,设计者并不需要为这个目标费太多脑筋了.
他们可以在给予"探路者"比"海盗船"更强大的处理能力和更多的存储量的同时,减少相当一部分冗余设计.
"火星探路者"实际包含两个嵌入式系统:着陆艇和漫游车.
着陆艇有一个32位处理器和128MB的RAM;漫游车只有一个8位处理器和512KB的存储量.
这种选择也许反映出了两个系统不同的功能需求的考虑,不过我可以保证生产成本不是问题.
C:最基本的必需品这些系统下多的几个共同点之一是都使用了C语言.
和其他语言相比,C已经成为嵌人式程序员的语言了,情况当然不全总是这样,事情总会变的.
不过,起码现在C是嵌入式世界里最接近标准的东西.
这一节里,我会说明为什么C会变得如此普遍,我又为什么选择C和C++作为这本书的主要语言.
因为对于一个给定的项目来说,选择一种语言对成功的开发是如此的重要,所以当一种语言被证明同时适合于8位和64位处理器,适用于字节、千字节甚至兆字节的系统,适用于从一个人到很多人的开发团队.
是很令人吃惊的.
而C语言做到了.

当然,C是有很多优势的.
它小而易学,今天每一种处理器都有C的编译器,同时有相当多的有经验的C程序员.
另外,C是和处理器无关的,这就让程序员可以着眼于算法和应用而不用考虑特定处理器结构的细节.
可是,很多其他的高级语言也具备这些优点,为什么只有C语言取得了成功呢也许C语言最具威力的地方——也正是把它和其他语言比如Pascal和FORTRAN区别开的地方——是,它是一个非常"低级"的高级语言.
正如我们将在整本书里看到的,C给予嵌入式程序员很大程度的直接控制硬件的能力,却不会失去高级语言带来的好处.
"低级"的内在本质是这个语言的创建者的明显目的.
实际上.
Kernighan和Ritchie在他们的书《CProgrammingLanguage》的开头有这么一段话:C是一种相对"低级"的语言.
这个特征并没有什么不好的含义;它只是说明C语言可以处理大多数计算机可以处理的事情.
这些事情通常和实际机器实现的技学和逻辑运算结合在一起.
很少有其他高级语言可以像C一样,为几乎所有处理器生成紧凑的、高效的代码.
同时,只有C允许程序员方便地和底层硬件打交道.
其他嵌入式语言当然C井不是嵌人式程序员使用的唯一语言.
至少还有其他三种值得详细说一下.
即汇编语言、C++语言和Ada语言.
在早期的时候,嵌人式软件只用目标处理器的汇编语言来书写.
这样做使程序员可以完全控制处理器和其他硬件,当然也是有代价的.
除了更高的软件开发费用和缺乏可移植性,汇编语言还有很多缺点,同时,最近几年找一个有经验的汇编语言程序员也变得越来越难.
汇编语言现在只用作高级语言的,通常只用在那些必须要求极高效率或非常紧凑,或其他方式无法编写的小段代码里面.
C++是C语言的面向对象的超集,正在嵌入式程序员中变得越来越流行.
它的核心语言特性和C完全一样,但是C++提供了更好的数据抽象和面向对象形式的编程功能.
这些新的特性对软件开发人员非常有帮助,但是部分特性会降低可执行程序的性能,所以C++在大的开发队伍里用的最为普遍,在那里只程序员的帮助要比程序效率的损失更为重要.
Ada也是一种面向对象的语言.
不过和C++完全不同.
Ada开始是美国国防部为了开发面向任务的军用软件而设计的.
尽管它曾两次被接纳为国际标准(Ada83和Ada95),但Ada从没有在防务和航空工业领域之外获得足够的应用.
即使是这些领地这几年也在逐渐丧失,这是很不幸的事,因为与C++比起来Ada有很多特性可以简化嵌人式软件的开发工作.
为这本书选择一种语言类似本书的同类书的作者面临的主要问题是采用哪一种语言来开展讨论.

同时使用太多的语言只会使读者犯晕或者偏离更重要的问题.
另一方面,着眼点太窄又会使讨论变得不必要的学术化,或者(对作者和出版商都很糟糕)限制了这本书的潜在市场.
很明显,C是所有关于嵌入式编程的书的核心,这本书也不例外.
超过一半的例子是用C编写的,同时讨论也主要集中在和C有关的编程问题上.
当然,所有关于C编程的问题同样适用于C++.
另外,我会在后面的例子里使用那些对嵌人式软件开发最有用的C++特性.
汇编语言在特定的环境下会加以讨论,但是会尽量避免.
换句话说,我只在用别的方法无法完成一个特定的编程任务时,才会考虑用汇编语言.
我觉得这种混合使用C、C++和汇编语言的安排方式,更能反映现在的嵌入式软件开发过程,并且在不久的将来还会是这样.
我希望这种选择会使讨论能比较清晰,可以提供给开发实际系统的人有用的信息,并尽可能地适合更多的潜在的读者.
关于硬件的一些说明关于编程的书籍必须要给出实际的例子.
通常,这些例子要能很容易地被感兴趣的读者试验.
这就是说读者必须可以接触和作者完全一样的软件开发了具和硬件平台.
很不幸,在嵌入式编程的情况下,这是不现实的.
在大多数读者的平台上,比如PC、Mac和Unix工作站上来运行任何示范程序都是没意义的.
即使要选择一个标准的嵌入式平台地是很困难的.
正如你已经知道的,沿有"典型的"嵌人式系统这么一种东西.
不管选了哪种硬件,大多数读者都没办法接触到.
但是尽管有这个相当重要的问题.
我还是觉得选择一个参考平台来使用示例是很重要的.
通过这样做,我希望可以使所有的例子保持一致性,以此来使整个讨论更加清楚.
为了只使用一个硬件来说明尽可能多的问题,我发现有必要选择一个中档的平台.
这个硬件包含一个16位处理器(Intel的80188EB,注2)、适量的存储器(128KB的RAM和256KB的ROM),还有一些常见的输入、输出和外设部件.
我选用的电路板是Arcom控制系统公司制造的Target188EB.
关于这块电路板和如何获取的信息可以参看附录"Arcom的Target188EB".
如果你可以接触到这个参考硬件的话.
你将能原封不动地使用本书里的例子.
否则,你需要把示例代码移植到你能用到的嵌人式平台上面.
为了这个目的,我尽可能地使示例程序易于移植.
可是读者必须要知道,每一种嵌入式系统的硬件都是不一样的,可能一些例子对地的硬件来说一点意义也没有,比如,把第六章"存储器"里提到的快闪存储器驱动程序,移植到一个不带闪存的板子上就很没意义.
不管怎样,在第五章"接触硬件"里面我还会讲很多东西.
但是首先我们还有很多软件问题需要讨论,这就开始吧.
注2:Intel的80188EB处理器是专门为嵌入式系统修改了设计的80186的特殊版本,原来的80I86是IBM的第一台个人计算机(PC/XT)使用的8086处理器的一个继承者.
它从来没有被实际使用.
因为当IBM设计下一个型号(PC/AT)的时候选择的是80286.
尽管早期是失败的,近几年来自Intel和AMD的80186却在嵌入式系统里面取得了巨大的成功.
第二章你的第一个嵌入式程序注意!
此机器不能摸也不能拿.
它的内部在飞速地转动,而且不断发出火花.
它不是傻瓜摆弄的玩意儿.
请把手放在口袋里,站得远远地,放松些,看那闪烁的火花.
——KenOlson(肯·奥尔森),DECC公司总裁,1977在这一章里我们将通过一个例子直接进入嵌入式编程.
这个例子看起来和其他大多数编程书籍开头的"Hello,world!
"例子差不多.
在讨论代码的时候,我会说明选择特定代码段的理由,并会指出依赖目标硬件的部分.
本章只包含这第一个程序的源码,在接下来的两章里我们会讨论如何创建可执行代码并运行它.
HelloWorld!
好像所有讲述编程的书都用同一个例子来开始,就是在用户的屏幕上显示出"Hello,World!
".
总是使用这个例子可能有一点叫人厌烦,可是它确实可以帮助读者迅速地接触到在编程环境中书写简单程序时的简便方法和可能的困难.

就这个意义来说,"Hello,World!
"可以作为检验编程语言和计算机平台的一个基准.
不幸的是,如果按照这个标准来说,嵌入式系统可能是程序员工作中碰到的最难的计算机平台了.
甚至在某些嵌入式系统中,根本无法实现"Hello,World!
"程序.
即使在那些可以实现这个程序的嵌入式系统里面,文本字符串的输出也更像是目标的一部分而不是开始的一部分.
本章内容:zHelloWorld!
z闪烁程序z无限循环的作用你看,"Hello,World!
"示例隐含的假设,就是有一个可以打印字符串的输出设备.
通常使用的是用户显示器上的一个窗口来完成这个功能.
但是大多数的嵌入式系统并没有一个显示器或者类似的输出设备.
即使是对那些有显示器的系统,通常也需要用一小段嵌入式程序,通过调用显示驱动程序来实现这个功能.
这对一个嵌入式编程者来说绝对是一个相当具有挑战性的开端.
看起来我们还是最好以一个小的,容易实现并且高度可移植的联人式程序来开始,这样的程序也不太会有编程错误.
归根到底,我这本书继续选用"Hello,World!
".
这个例子的原因是,实现这个程序实在太简单了.
这起码在读者的程序第一次就运行不起来的时候,会去掉一个可能的原因,即:错误不是因为代码里的缺陷:相反,问题出在开发工具或者创建可执行程序的过程里面.
嵌人式程序员在很大程度上必须要依靠自己的力量来工作.
在开始一个新项目的时候,除了他所熟悉的编程语言的语法,他必须首先假定什么东西都没有运转起来,甚至连标准库都没有,就是类似printf()和scanf()的那些程序员常常依赖的辅助函数.
实际上,库例程常常作为编程语言的基本语法出现.
可是这部分标准很难支持所有可能的计算平台,并且常常被嵌入式系统编译器的制造商们所忽略.
所以在这一章里你实际上将找不到一个真正的"Hello,World!
"程序,相反,我们假定在第一个例子中只可以使用最基本的C语言语法.
随着本书的进一步深人,我们会逐步向我们的指令系统里添加C++的语法、标准库例程和一个等效的字符输出设备.
然后,在第九章"综合所学的知识"里面.
我们才最终实现一个"Hello,World!
"程序.
到那时候你将顺利地走上成为一个嵌入式系统编程专家的道路.
闪烁程序(译注1)在我的职业生涯中所进到的嵌入式系统都至少有一个可以被软件控制的注1:当然,闪烁的频率的选择完全是任意的、我选择1Hz的原因是这可以根容易地用一个秒表来核对.
简单地启动秒表,计几次闪烁,燃后停下秒表看嵌闪烁的次数是不是和经过的秒数相同,如果需要更精确的话,简单地多计几次闪烁就行了.
译注1:原文为德语.
LED(发光二极管).
所以我用一个以1Hz(注1)频率闪烁LED(发光二极管)的程序来替代"Hello,World!
".
1Hz就是每秒完整地开关一次.
典型的情况是,用来开关一个LED的代码通常只有几行C或汇编代码,所以发生错误的机会也就很少.
同时因为几平所有的嵌入式系统都有LED,所以潜在的概念是非常容易移植的.
LED闪烁程序的高层部分如下所示.
这部分程序是与硬件无关的.
不过,它还要依赖分别使用和硬件有关的toggleLed()和delay()来改变LED的状态和控制计时.
*Functionmain()*Description:BlinkthegreenLEDonceasecond*Notes:Thisouterloopishardware-independent.
However.
*itdependsontwohardware-dependentfunctions.
*Returns:Thisroutinecontainsaninfiniteloop.
voidmain(void){while(1){toggleLed(LED_GREEN);/*ChangethestateoftheLED.
*/delay(500);/*Pausefor500millisenconds.
*/}}/*main()*/toggleLed在Arcom的电路板上,有两个LED:一红一绿.
每个LED的状态都被一个叫做端口2I/O锁存寄存器(缩写是P2LTCH)的一个位来控制.
这个寄存器和CPU在同一个芯片里,它实际上包含了芯片外围的8个I/O引脚的锁存状态.
这8个引脚合在一起叫做端口2.
P2LTCH寄存器里的每一位都和相应的I/O引脚的电压联系到一起.
比如,第6位控制送到绿色LED的电压:#defineLED_GREEN0X40/*ThegreenLEDiscontrolledbybit6.
*/通过修改这一位,就可以改变相应外部引脚的电压从而改变了绿色LED的状态.
如图2-1所示,当P2LTCH的第6位是1的时候LED关,第6位是0则LED打开.
图2-1Arcom电路板上的LEDP2LTCH寄存器位于I/O空间的一块特定内存里,偏移为OxFF5E.
不幸的是,80x86处理器的I/O空间里的寄存器只能使用汇编语言指令in和out来操作.
C语言没有内嵌的类似操作.
最接近的替换函数是定义在面向PC平台的头文件dos.
h里的inport()和outport().
理想情况下,我们可以包含这个头文件并从我们的嵌人式程序里调用这两个库函数.
不过,因为它们是DOS编程库的一部分,我们必须要考虑到最坏的情况:它们在我们的系统上不工作.
最起码的是,我们在第一个程序里不应该依赖它们.
下面列出了面向Arcom电路板并且不依赖库例程的toggleLed例程的实现.
实际的算法是很简单的:读P2LTCH寄存器的内容,切换要控制的LED的相应位,再把新的值写回寄存器.
你会注意到尽管这个例程是用C书写的,而实际的控制部分是用汇编语言实现的.
这种简便的方法叫内嵌汇编语言(inlineassembly).
它一方面使程序员避开了复杂的C函数凋用和参数的传递和转换过程,同时使她可以随意地使用汇编语言来工作(注2).
#defineP2LTCH0xFE5E/*TheoffsettheP2LTCHregiser.
*/注2:不幸的是,各种编译器的内嵌汇编语法是不一样的.
我在示例中使用的是BorlandC++编译器的格式.
Borland的内嵌汇编格式非常好,它主持在汇编行里引用用C代码定义的变量和常数.
*FunctiontoggleLed()*Description:TogglethestateofoneorbothLEDs.
*Notes:ThisfunctionisspecifictoArcom'sTarget188EBboard.
*Returns:Nonedefined.
voidtoggleLed(unsignedcharledMask){asm{movdx,P2LTCH/*Loadtheaddressoftheregister.
*/inal,dx/*Readthecontentoftheregister.
*/movah,ledMask/*MovetheledMaskintoaregister.
*/xora1,ah/*Toggletherequestedbits.
*/outdx,al/*Writethenewregistercontents.
*/}}/*toggleLed()*/delay()我们也需要在切换LED的动作之间实现一个半秒(500ms)的延时.
这是通过在如下所示的delay例程里使用忙等待技术实现的.
这个例程接受以毫秒计的参数作为请求的延迟时间,然后用这个参数和常数CYCLES_PRE_MS相乘来得到为了延迟制定时间需要的while循环重复次数.
*Functiondelay()*Description:Busy-waitfortherequestednumberofmilliseconds.
*Notes:Thenumberofdecrement-and-testcyclespermillisecond*wasdeterminedthroughtrialanderror.
Thisvalueis*dependentupontheprocessortypeandspeed.
*Returns:Nonedefined.
voiddelay(unsignedintnMilliseconds){#defineCYCLES_PER_MS260/*Numberofdecrement-and-testcycles.
*/unsignedlongnCycles=nMilliseconds*CYCLES_PER_MS;while(--nCycles);}}/*delay()*/与硬件相关的常数CYCLES_PER_MS,代表了处理器在1毫秒里可以执行的"减测试"(nCycles--!
=0)周期的次数.
为了确定这个值我使用了尝试和排错的方法.
我做了一个大概的估算(我想可能在200左右),然后写程序的其余部分,编译并运行.
LED确实闪了,不过频率要比1Hz快,然后我用一个精确的秒表来对CYCLES_PER_MS作了一系列小的调整直到闪烁的频率很接近1Hz为止.
就是这样,这就是闪烁LED程序的所有内容了.
有三个函数来完成整个工作:main()、toggleLed()和delay().
如果你想把这个程序移植到别的嵌人式系统的话,你应仔细阅读你的硬件的文档,必要时重写toggleLed(),并修改CYCLES_PER_MS的值.
当然,我们还得创建和执行这个程序,我会在下两章里讲这些.
但是首先,我得花一点时间说说无限循环和它在嵌入式系统里的作用.
无限循环的作用在为嵌入式系统和其他计算机平台写程序时有一个最基本的区别,就是嵌人式程序总是以一个无限循环作为结束.
典型地,这个无限循坏包含了程序功能的一个重要组成部分,就像在闪烁LED程序里那样.
无限循环是必要的,因为嵌入式软件的工作永不结束.
它一般要运行到世界末日到来或者电路板复位.

另外,大多数嵌入式系统只运行一块程序.
并且尽管硬件是重要的,可是没有了嵌入式软件它怎么也成不了一个数字手表、蜂窝电话或者微波炉.
如果软件停止运行了,那硬件也就没用了.
所以一个嵌入式程序的功能体总是被一个无限循环来包含着以使它们可以永远运行下去.
这种情形是如此普遍以至于不值一提.
但是我不会这样,因为我曾经看到相当多的嵌入式程序员新手们对这个微妙的区别感到很困惑.
所以如果你的第一个程序看起来运行了,可是LED没有闪烁而是就改变了一次状态,那也许就是你忘记了把对toggleLed()和delay()的调用包在一个无限循环里面.
第三章编译、链接和定址如果我喜欢一个程序那么我就应该和同样喜欢它的人分享它,我认为这是一条黄金法则.
软件销售商们意图分化和征服用户,使每一个用户同意不和别人共享软件.
我拒绝用这种方式打破其他用户的团结.

我不认为签署一份软件授权许可协议或者不公开协议是有良知的事.
因此我能够继续光荣地使用电脑.
我决定要汇集足够的自由软件,这样我就不会使用任何非由的软件了.
——RichardStallman(理查德·斯多曼),GNU工程创始人.
《GNU宣言》本章中,我们逐步学习使你的程序可以在嵌入式系统上运行的每个步骤,我们也会讨论相关的开发工具,并了解如问创建在第二章"你的第一个嵌人式程序"里讲述的闪烁LED程序.
在我们开始之前,我将讲清楚一件事,嵌入式系统编程和你以前从事的编程工作实质上并无区别.
唯一改变的是每一个硬件平台都是独特的.
不幸的是,一个不同点就会导致许多附加的软件复杂性.
这也是你必须要比以前格外注意软件创建过程的原因.
创建过程当目标平台(targetplatform,注1)选定之后软件开发工具可以自动做很多的事情.
这个自动过程是可能的,因为这些工具可以发掘程序运行的硬件和操作系统平台的特性.
例如,如果你的所有程序将执行在运行DOS的IBM兼容PC上,那么你的编译器就可以自动处理(因此也使你无法得知)软件创建过注1:在这种方式下,术语"目标平台"最好理解为构成运行你的软件的基本运行环境的硬件和操作系统的统一体.
在某些情况下,嵌入式系统并没有操作系统,那么你的目标平台就只是运行你的程序的处理器.
本章内容:zHelloWorld!
z闪烁程序z无限循环的作用程的某些方面.
而在另一方面,嵌入式软件开发工具很少时目标平台做出假定.

相反,用户必须给出更清晰的指令来告知这些工具有关系统的具体知识.

把你的嵌入式软件的源代码表述转换为可执行的二进制映像的过程,包括三个截然不同的步骤.
首先,每一个源文件都必须被编译或汇编到一个目标文件(objectfile).
然后.
第一步产生的所有目标文件要被链接成一个目标文件,它叫做可重定位程序(relocatableprogram).
最后,在一个称为重定址(relocation)的过程中,要把物理存储器地址指定给可重定位程序里的每个相对偏移处.
第三步的结果就是一个可以运行在嵌入式系统上的包含可执行二进制映像的文件.
图3-1说明了上述的嵌人式软件开发过程.
在图中,三个步骤是由上至下表示的,在圆角矩形框里说明了执行该步骤所用到的工具.
每一个开发工具都以一个或多个文件作为输人共产生一个输出文件.
本章接下来的部分会说明关干这些工具和文件的更详细的内容.
图3-1嵌入式软件开发过程嵌入式软件开发过程的每一个步骤都是在一个通用计算机上执行的软件的转换过程.
为了区别这台开发计算机(通常会是一台PC或Unix工作站)和目标嵌入式系统,我们称它作主机.
换句话说,编译器、汇编器,链接器和定址器都是运行在主机上的软件.
而不是在嵌入式系统上运行.
可是,尽管它们事实上在不同的计算机平台上运行,这些工具综合作用的结果是产生了可以在目标嵌人式系统上正确运行的可执行二进制映像.
图3-2显示了这种功能的划分.
图3-2主机和目标机的划分在本章和下一章里我将使用GNU工具(编译器、汇编器、链接器和定址器)作为示范.
这些工具在嵌入式软件开发人员中使用极为普遍,因为可以免费得到它们(甚至源代码也是免费的),而且它们支持大多数最流行的嵌人式处理器.
我会用这些特定工具的特性来说明一些讨论到的基本概念.
一旦你领悟以后,同样的概念就可以应用到任何相类似的开发工具上.
编译编译器的工作主要是把用人可读的语言所书写的程序,翻译为特定的处理器上等效的一系列操作码.
在这种意义上,一个汇编器也是编译器(你可以称之为"汇编语言编译器"),但是它只执行了一个简单地逐行把人可阅读的助记符翻译到对应操作码的过程.
这一节里的所有内容都同样适用于编译器和汇编器.

这些工具综合在一起形成了嵌入式软件开发过程的第一个步骤.
当然,每一种处理器都有它独特的机器语言,所以你需要选择一个可以为你的目标处理器产生程序的编译器.
在嵌人式系统的情况下,编译器几乎总是在主机上运行,在嵌入式系统本身运行编译器也没什么意义.
一个像这样运行在一个计算机下台上并为另一个平台产生代码的编译器叫做交叉编译器(cross-compiler).
使用交叉编译器是嵌人式软件开发的固定特征.
GNUC/C++编译器(gcc)和汇编器(as)可以被配置为本地编译器或交叉编译器.
用作交叉编译器的时候这些工具支持非常多的主机·目标机组合.
表3-1列出了最常见的一些得到支持的主权和目标机.
当然,主机和目标机的选择是相互独立的,这些工具可以被配置成住意的组合.
表3-1GNU编译器所支持的主机和目标机不管输入文件是C/C++,汇编还是什么别的,交叉编译器的输出总是一个目标文件.
这是语言翻译过程产生的包含指令集和数据的特定格式的二进制文件.

尽管目标文件包含了一部分可执行代码,它却是不能直接运行的.
实际上,目标文件的内部结构正强调了更大的程序的不完备性.
目标文件的内容可以想象成一个很大的、灵活的数据结构.
这个文件的结构通常是按照标准格式定义的,比如"通用对象文件格式"(COFF)和"扩展的链接器格式"(ELF).
如果你计划使用不止一个编译器(就是说你用不同的源代码语言写你的程序的各个部分),那么你应该确定它们产生相同的目标文件格式.
尽管很多编译器(特别是那些运行在Unix平台上的)支持类似COFF和ELF的标难格式(gcc两者都支持),还是有一些编译器只产生专有格式的目标文件.
如果你使用后一类编译器,你也许会发现你将不得不从同一个供货商处购买所有其他的开发工具.
大多数目标文件以一个描述后续段的头部开始每一段包含一块或几块源于源文主机平台目标机DECAlphaDigitalUnixHP9000/700HP-UXIBMPowerPCAIXIBMRS6000AIXSGIIrisIRIXSunSPARCSolarisSunSPARCSunOSX86Windows95/NTX86RedHatLinuxAMD/Intelx86(仅为32位)FujitsuSPARCliteHitachiH8/300,H8/300H,H8/SHitachiSHIBM/MotorolaPowerPCInteli960MIPSR3xxx,R4xx0MitsubishiD10V,M32R/DMotorola68kSunSPARC,MicroSPARCToshibaTX39件的代码或数据,不过,这些块被编译器重新组合到相关的段中.
比如,所有代码块都被收集到叫做text的段中,已初始化的全局数据(包括它们的初始值)被收集到叫做data的段中,未初始化的全局变量被收集到叫做bss的段里面.
通常在目标文件里还有一个符号表,记录了源文件引用的所有变量和函数的名字和位置.
这个表的部分内容可能不完整,因为不是所有的变量和函数都总在同一个文件里定义.
这些符号就是对别的源文件定义的变量和函数的引用,要一直到链接器的时候才会解决这些不完整的引用.
链接在程序能被执行前,所有第一步产生的目标文件都要以一种特殊的方式组合起来.
目标文件分开来看还是不完整的,特别是那些有未解决的内部变量和函数引用的目标文件.
链接器的工作就是把这些目标文件组合到一起,同时解决所有未解决的符号问题.
链接器的输出是同样格式的一个目标文件,其中包含了来自输人目标文件的所有代码和数据.
它通过合对输人文件里的text、data和bss段来完成这一点.
这样,当链接器运行结束以后,所有输入目标文件里的机器语言代码将出现在新文件的text段里,所有初始化变量和未初始化变量分别在data和bss段里面.
在链接器台并各段内容的过程中,它也监视没解决的符号.
例如,如果一个目标文件包含一个对变量foo的未解决的引用同时一个叫foo的变量在另外的一个目标文件里被声明,那么链接器将匹配它们.
那个没解决的引用就会被一个到实际变量的引用所取代.
换句话说,如果foo位于输出的数据段的偏移14的位置,它在符号表中的人口将包含这个地址.
GNU链接器(ld)运行在和GNU编译器一样的所有主机平台上.
它本质上是一个命令行工具,用来把参数中列出来的所有目标文件链接到一起.
对于嵌入式开发来说,一个特殊的包含编译过的启动代码的目标文件也必须包括在参数列表里面.
(参看本章后面的选读部分"启动代码".
)GNU链接器也有一种脚本语言,可以用来对输出的目标文件施加更严格的控制.
启动代码传统软件开发工具自动做的一件事是插入启动代码(startupcode).
启动代码是用来位高级语言写的软件做好运行前准备的一小段汇编语言.
每一种高级语言都有其希望的运行环境.
比如C和C++都使用了一个固定的堆栈.
在任何用此两种语言写的软件可以正确运行之前,必须为堆栈分配空间并进行初始化.
这还只是C/C++程序启动代码的一个职责而已.
大多数供嵌入式系统使用的交叉编译器包括一个叫startup.
asm,crt0.
s("C运行时"的所写)或者类似的一个汇编语言文件.
随编译器提供的文档通常会说明该文件的位置和内容.
C/C++程序的启动代码通常包含以下行为,并且按照所列的次序执行:1、禁止所有中断.
2、从ROM里复制所有初始化数据到RAM里.
3、把未初始化数据区清零.
4、未堆栈分配空间并初始化.
5、初始化处理器堆栈指针.
6、创建并初始化堆.
7、(只对C++有效)对所有全局变量执行构造函数和初始化函数.
8、允许中断.
9、调用main.
典型地,启动代码在调用main之后也包含一些指令.
这些指令只在高级语言程序退出地情况下运行(即:从对main的调用返回).
根据嵌入式系统的种类,你也许会希望利用这些指令来暂停处理器,复位整个系统或者把控制传到一个调试工具.
因为启动代码不是自动插入的,程序员通常必须亲自汇编这段代码并把得到的目标文件包括在链接器的输入文件列表里.
他甚至需要给链接器一个特殊的命令行选项以阻止它插入通常的启动代码.
适用于多种目标处理器的启动代码可以在GNU软件包libgloss中找到.
如果在超过一个目标文件里都声明了同一个标号,链结器就无法继续了.
它可能会通过显示一条错误信息来通知编程人员并退出.
然而,如果所有目标文件都合并之后还有符号没有解决,链结器会尝试自己来解决引用问题.
这个引用也许会指向标准库的一个函数,那么链接器按照命令行指示的顺序打开每一个库并检查它们的符号表.
如果它找到具有引用名字的函数,就会把有关的代码和数据包含进输出目标文件以解决引用(注2).
不幸的是,标准库例程在可以用到嵌入式系统之前经常需要做一些改动.
这里的问题是随大多数软件开发工具仅以目标文件格式提供标准库,所以你很少能自己来修改库的源代码.
令人感激的是,一个叫Cygnus的公司提供了一个可用在嵌入式系统中的自由软件版的标准C库.
这个软件包叫newlib.
你只需要从Cygnus的Web站点下载这个库的源代码,实现一些面向目标系统的功能,然后编译即可.
然后这个库就可以和你的嵌人式软件链接到一起来解决任何以前没有解决的标准库调用.
在合并了所有代码和数据段并且解决了所有符号引用之后,链接器产生程序的一个特殊的"可重定位的拷贝.
换句话说,程序要说完整还差一件事:还没有给其内部的代码和数据指定存储区地址.
如果你不是在为了一个嵌入式系统而工作,那么你现在就可以结束软件创建过程了.
但是嵌入式程序员一般在这个时候还没有结束整个创建过程.
即使你的嵌入式系统包括一个操作系统,你可能仍然需要一个绝对定址的二进制映像.
实际上,如果有一个操作系统,它包含的代码和数据很可能也在可重定位的程序里.

整个嵌入式应用——包括操作系统——几乎总是静态地链接在一起并作为一个二进制映像来运行.
定址把可重定址程序转换到可执行二进制映像的工具叫定址器.
它负责三个步骤中最容易的部分.
实际上,这一步你将不得不自己做大部分工作来为定址器提供关于目标电路板上的存储器的信息.
定址器将用这个信息来为可重定址程序注2:需要注意我这里谈的只是静态链接.
在非嵌入式环境里,动态链接库是很普遍的.
在那种情况下,和库例程关联的代码和数据并不直接插入到程序里.

里的每一个代码和数据段指定物理内存地址.
然后它将产生一个包含二进制内存映像的输出文件.
这个文件就可以被调人目标ROM中执行.
在很多情况下,定址器是一个独立的开发工具.
但是在GNU工具的情况下,这个功能建立在链接器里.
试着不要被这个个别的实现所迷惑.
不管你是为一个通用计算机还是一个嵌人式系统编写软件,你的可重定址程序里的段中的某些地方必须要被赋予实际地址.
在第一种情况下,操作系统在调用程序的时候为你做这些事,在后一种情况下,你必须用一个独立的工具执行这个步骤.
在定址器是链接器的一部分的情况下也是这样,就像在ld的情况下.
GNU链接器需要的存储器信息可以通过一个链接器脚本来传递.
这种脚本有时用来控制可重定址程序里代码和数据区的精确顺序.
但是在这里,我们希望做出控制次序更多的事:我们希望建立每一般在存储器里的位置.
下面是为一个假设的有512KRAM和512KROM的嵌入式目标板提供的链接器脚本的例子:MEMORY{ram:ORGIN=0x00000,LENGTH=512Krom:ORGIN=0x80000,LENGTH=512K}SECTIONS{dataram:/*Initializeddata.
*/{_DataStart=.
;*(.
data)_DataEnd=.
;}>rombss:{_BssStart=.
;*(.
bss)_BssEnd=.
;}_BottomOfHeapTheheapstartshere.
*/_TopOfStack=0x80000;/*Thestackendshere.
*/textrom:/*Theactualinstructions.
*/{*(.
text)}}这段脚本告知GNU链接器的内置定址器有关目标板上存储器的信息,并指导它把data和bss段定位在RAM中(从地址0X00000开始),把text段放在ROM中(从0x80000开始).
不过,通过在data段的定义后面添加>rom可以把data段中的变量的初始值作为ROM映像的一部分.
所有以下划线开始的名字(比如_TopOfStack)是可以从你的源代码内部引用的变量.
链接器将用这些符号来解决输入的目标文件里的引用.
这样,比方说,可能会有嵌人式软件的某一部分(通常在启动代码里面)把ROM可初始化变量的初始值拷贝到RAM的数据段中.
这个操作的开始和停止地址可以通过引用整型变量_DataStart和_DataEnd符号化地建立.
创建过程的最后一步的结果是一个绝对定址的二进制映像,它可以被下载到嵌入式系统内或写人到只读式存储设备中.
在前面的例子里,内存映像正好是1MB大小.
无比如何,因为初始化数据段的初始值存放在ROM里,这个映像的低512K字节将只包含0,所以只有映像较高的一半是有意义的.
你将在下一章里看到如何下载并执行这个内存映像.
创建闪烁程序不幸的是,因为我们使用Arcom的板子作为我们的参考平台,我们不能使用GNU工具来创建示例代码.
作为替换我们使用Borland的C++编译器和TurboAssembler汇编器.
这些工具可以在任何基于DOS和Windows的PC上运行(注3).
如果你有一个Arcom的板子来做实验,现在就可以把它安装起来并在你的主机上安装Borland的开发工具.
(参看附录"Arcom的Targetl99EB"来取得订货信息.
)我用的是3.
1版的编译器,运行在一台基于Windows95的PC上.
不过,任问可以为80186处理器生成代码的Borland工具都可以使用.
正如我所实现的那样,闪烁LED示例包含三个源文件模块:led.
c和blink.
c和startup,asm.
创建过程的第一步是编译这两个文件.
我们需要使用的命令行选项有:-c说明"编译,但是不要链接",-v说明"在输出文件里包含符号调试信息"-ml说明"使用大内存模式".
还有-l说明"目标是80186处理器".
这里是实际命令:bcc-c-v-ml-lled.
cbcc-c-v-ml-lblink.
c当然,要执行这些命令,bcc.
exe必须在你的PATH路径里并且这两个源文件在当前目录下.
换句话说,你应该在Chapter2子目录下.
每个命令的结果都创建了一个和.
c文件有同样前缀而后缀是.
obj的文件.
所以如果一切顺利的话,在工作目录里会出现两个文件——led.
obj和blink.
obj.
尽管看起来在我们的例子里只有两个目标文件需要链接,实际上是有三个.

这是因为我们必须给C程序链接某个启动代码.
(参看本章前面的选读"启动代码".
)Arcom电路板使用的示例启动代码在Chapter3子目录的文件startup.
asm里.
为了把这段代码编成目标文件,进入这个目录并发出如下命令:tasm/nxstartup.
asm结果是这个目录里多了一个startup.
obj文件.
实际把三个目标文件链接在一起的命令如下.
需要注意在这个例子中命令行里目标文件出现的次序是有讲究的:启动代码必须放在第一个才能正确链接.
注3:应该注意Borland的C++编译器不是专门为嵌入式软件开发人员设计的.
相反它是设计来为使用80x86处理器的PC生成基于DOS和Windows的程序的.
不过,通过使用特定的命令行选项可以允许我们指定特定的80x86处理器——比如80186,这样就可以用这个工具作为类似Arcom的电路板的嵌入式系统的交叉编译器.
tlink/m/v/s.
.
\Chapter3\startup.
objled.
objblink.
objblink.
exe,blink.
map作为执行tlink命令的结果,Borland的TurboLinker链接器产生两个新文件:blink.
exe和blink.
map.
第一个文件包含了可重定址的程序,第二个文件包含了人可阅读的程序映像.
如果你以前从来没有看过一个映像文件,记着在往下读之前看一看这个文件.
它提供了类似前面讲的链接器脚本所包含的信息.

只不过这里是结果,所以包含了每一段的长度和在可重定址程序里的公共符号的名字和位置.
还有一个工具要用来使闪烁LED程序可以运行,它就是定址器.
我们要用的定址器是Arcom随板子附带的SourceView开发和调试程序包的一部分提供的.
因为这个工具是为这个特定的嵌入式平台设计的,所以它没有更通用的定址器带的很多选项(注4).
实际上,只有三个参数:可重定址二进制映像的名字、ROM的起始地址(以十六进制形式提供)和目标RAM的总长度(以千字节单位提供):tlink/m/v/s.
.
\Chapter3\startup.
objled.
objblink.
objblink.
exe,blink.
map作为执行tlink命令的结果,Borland的TurboLinker链接器产生两个新文件:blink.
exe和blink.
map.
第一个文件包含了可重定址的程序,第二个文件包含了人可阅读的程序映像.
如果你以前从来没有看过一个映像文件,记着在往下读之前看一看这个文件.
它提供了类似前面讲的链接器脚本所包含的信息.

只不过这里是结果,所以包含了每一段的长度和在可重定址程序里的公共符号的名字和位置.
还有一个工具要用来使闪烁LED程序可以运行,它就是定址器.
我们要用的定址器是Arcom随板子附带的SourceView开发和调试程序包的一部分提供的.
因为这个工具是为这个特定的嵌入式平台设计的,所以它没有更通用的定址器带的很多选项(注4).
实际上,只有三个参数:可重定址二进制映像的名字、ROM的起始地址(以十六进制形式提供)和目标RAM的总长度(以千字节单位提供):tcromblink.
exeC000128SourceVIEWBorlandCROMRelocatorv1.
06Copyright(C)ArcomControlSystemsLtd1994RelocatingcodetoROMsegmentC000H,datatoRAMsegment100HChangingtargetRAMsizeto1I8KbytesOpening'blink.
exe'.
.
.
Startupstackat0102:0402PSPProgramsize550Hbytes(2K)TargetRAMsize20000Hbytes(128K)Targetdatasize20Hbytes(1K)Creating'blink.
rom'.
.
.
ROMimagesize55HHbytes(2K)tcrom定址器给出了给每个段指定了基地址的可重定址输人文件的内容,并产生文件blink.
rom.
这个文件包含了一个已经绝对定址的二进制映像,它可以直接调入到ROM里.
不过我们不是用一个设备编程器把它写人到ROM里,相反我们将生成这个二进制映像的一个ASCII版本并通过一个串行口把它下我到ROM型.
要做到这一点我们还是使用Arcom提供的一个工具,叫做bin2hex.
下面是此命令的语法:bin2hexblink.
rom/A=1000这个额外的步骤生成一个新的文件blink.
hex,它包合和blink.
rom一样的内容,不过是以一种叫做Intel十六进制格式(IntelHexFormat)的ASCII格式表示的.
注4:不管怎样,它是免费的,这可比更通用的定址器便宜多了.
第四章下载和调试我现在还清楚地记得那一刹那,我明白了从此以后我生活的很大部分将用来找我自己程序的错误,——MauriceWilkes,剑桥大学计算机实验室主任,1949当你已经在主机上有了一个可执行二进制映像文件的时候,你就需要有一种途径来把这个映像文件下载到嵌入式系统里来运行了.
可执行二进制映像一般是要下载到目标板上的存储设备里并在那里执行.
并且如果你配备了适当的工具的话,还可以在程序到设置断点或以一种不干扰它的方式来观察运行情况.

本章介绍了可用于下载、运行和调试嵌入式软件的各种技术.
在ROM中的时候……下载嵌入式软件的最明显的方式,是把二进制映像载人一片存储芯片并把它插在目标板上.
虽然一个真正的只读存储芯片是不能再覆盖写人的,不过你会在第六章"存储器"里看到,嵌人式系统通常使用了一种特殊的只读存储器,这种存储器可以用特殊的编程器来编程(或重新写人编程).
编程器是一种计算机系统,它上面有各种形状和大小的芯片插座,可以用来为各种存储芯片编程.

在一个理想的开发条件下,设备编程器应该和主机接在同一个网络上.
这样,可执行二进制映像文件就很容易传给它来对ROM芯片编程.
首先把映像文件传到编程器然后把存储芯片插入大小形状合适的插座里并从编程器屏幕上的菜单里选择芯片的型号.
实际的编程过程可能需要几秒钟到几分钟,这要看二进制映像文件的大小和你所用的芯片型号来定.
编程结束以后,你就可以把ROM插进板上的插座了.
当然,不能在嵌入式本章内容:z在ROM中的时候…z远程调试器z仿真器z模拟器和其他工具系统还加电的时候做这件事.
应该在插入芯片之前关掉电源,插入之后再打开.

一旦加电,处理器就开始从ROM里取出代码并执行.
不过,要注意到每一种处理器对第一条指令的位置都有自己的要求.
例如,当Intel80188EB处理器复位以后,它就会取位于物理地址FFFF0h的指令来执行.
这个地址叫复位地址,位于那里的指令就叫复位代码.
如果你的程序看起来像是没有正确运行,那可能是你的复位代码出了点问题.
你必须保证ROM里你的二进制映像格式要遵从目标处理器的复位要求.
在开发过程中,我发现在复位代码执行完后打开板子上的一个LED非常有用,这样我一眼就知道我的新ROM程序是不是满足了处理器的基本要求.
注意:调试技巧#1:一个最简单的调试技巧就是利用LED来相示成功或者失败.
基本的思路是慢慢地从LED驱动代码过渡到更大的程序.
换句话说,你先从启动地址处的LED驱动代码开始.
如果LED亮了,你就可以编辑程序,把LED驱动代码挪到下一个运行标记的地方.
这个方式最适合像启动代码那样的简单以线性执行的程序.
如果你没有本章后面提到的远程调试器或者任何其他调试工具的话,这也许是你唯一的调试办法了.
Arcom电路板包含一个特殊的在线可编程存储器,叫做快闪存储器(简称闪存),它可以在不从板上移走的情况下编程.
实际上,板上的另外一块存储器中已经包含了可以对这个快闪存储器编程的功能.
你知道吗,Arcom电路板上实际带了两个只读存储器,一个是真正的ROM,其中包含了可以让用户对另外一片(即快闪存储器)在线编程的简单程序.
主机只需通过一个串行通信口和一个终端程序就可以和这个监控程序沟通了.
随板提供的"Target188EBMonitorUser'sManual"包含了把一个Intel十六进制格式文件,比如blink.
hex,载入到闪存里的指令.
这种下载技术的最大缺点是没有一种简单的方法来调试运行在ROM外面的软件.
处理器以一种很高的速度提取指令并执行,并没有提供任何使你观察程序内部状态的手段.
这在你已经知道你的软件工作正常并且你正计划分发这个系统的时候看起来是不错的,不过对于正在开发的软件是一点用都没有.
当然,你还是可以检查LED的状态和其他外部可视的硬件指示,但这永远不会比一个调试器提供更多的信息和反馈.
远程调试器如果可能的话,一个远程凋试器(remotedebugger)可以通过主机和目标机之间的串行网络连接来下载、执行和调试嵌入式软件.
一个远程调试器的前端和你可能用过的其他调试器都一样,通常有一个基于文本或GUI(图形用户界面)的主窗口和几个小一点的窗口来显示正在运行的程序的源代码、寄存器内容和其他相关信息.
所不同的是,在嵌人式系统的情况下,调试器和被调试的软件分别运行在两个不同的计算机系统上.
一个远程调试器实际上包含两部分软件.
前端运行在主机上并提供前述的人机界面.
但还有一个运行在目标处理器上的隐藏的后端软件来负责通过某种通信链路和前端通信.
后端一般被称作调试监控器(debugmonitor),它提供了对目标处理器的低层控制.
图4-1显示了这两个部分是如何一起工作的.
图4-1一个远程调试会话调试监控器通常是由你或生产厂以前面讲过的方式放置在ROM的,它在目标处理器复位的时候会自动启动.
它监控和主机的通信链路并对远程调试器的请求做出回应.
当然,这些请求和监控器的响应必须符合某种预先定义好的通信协议,而且这些协议通常是很底层的.
远程调试器的请求的一些示例就如"读寄存器x"、"修改寄存器y"、"读从address开始的内存的n字节"还有"修改位于address的数据"等等.
远程调试器通过组合利用这些低层命令来完成诸如下载程序、单步执行和设置断点等高级调试任务.
GNU调试器(gdb)就是这样的一个调试器.
像其他GNU具一样,它一开始是被设计用来完成本机调试,后来才具有了跨平台调试的能力.
所以你可以创建一个运行在任问被支持的主机上的GDB前端,它就会理解任何被支持的目标机上的操作码和寄存器名称.
一个兼容的调试监控器的源代码包含在GDB软件包里面,并需要被移植到目标平台上.
不过,要知道这个移植可能需要一些技巧,特别是如果你的配置里只能通过LED来调试的话(参见调试技巧#1).
GDB前端和调试监控器之间的通信专门被设计来通过串行连接进行字节传输.
表4-1显示了命令格式和一些主要的命令.
这些命令示范了发生在典型的远程调试器前端和调试监控器之间的交互类型.
表4-1GDB调试监控器命令远程调试器是嵌入式软件开发里最常用到的下载和测试工具.
这主要是因为它们一般比较便宜.
嵌入式软件开发人员已经有了所需的主机了,何况一个远程调试器的价格并不会在全套跨平台开发工具(编译器、链接器、定址器等等)的价格上增加多少.
还有,远程调试器的供应商们通常会提供他们的调试监控器的源代码,以增加他们的用户群.
Arcom电路板在交付的时候在快闪存储器里包含了一个免费的调试监控器.
和Arcom提供的主机软件一起使用,这个调试监控器就可以把程序直接下载到目标权的RAM里并运行.
你可以用tload工具来完成这一工作.
按照"SourceVIEWforTarget188EBUser'sManual"的指示简单地把SourceVIEW用行通信适配器接到目标位和主机上,然后在主机PC上执行下述命令:命令请求格式响应格式读寄存器写寄存器读某地址数据写某地址数据启动/重启执行从某地址开始执行单步执行从某地址开始单步执行重置/中止程序GGdatamaddress,lengthMaddress,lenth:dataccaddressssaddresskdataOKdataOKSsignalSsignalSsignalSsignalnoresponsetload-gblink.
exeSourceViewTargetLoaderv1.
4Copyright(c)ArcomControlSystemsLtd1994Opening'blink.
exe'.
.
.
downloadsize750Hbytes(2K)CheckingCOM1(pressESCkeytoexit).
.
.
Remoteident:TDR188EBversion1.
02DownloadsuccessfulSending'GO'commandtotargetsystem-g选项告诉调试监控器程序下载一结束就马上开始运行.
这样一来,运行的就是和ROM里的程序完全对应的RAM里的程序了.
在这种情况下,我们也许会以可重定址程序来开始,那么tload工具也会自动地在RAM里第一个可利用的地址处为我们的程序重新定址.
对于远程调试的目的,Arcom的调试监控器可以用Borland的TurhoDebugger做前端.
然后TurboDebugger就可以单步执行你的C/C++和汇编语言程序、在程序里设置断点,并可以在程序运行时监控变量、寄存器和堆栈(注1).
下面是你可能用来启动一个对闪烁LED程序的调试会话的命令:tdrblink.
exetver-3.
1TargetDebuggerVersionChangerv1.
2Copyright(c)ArcomControlSystemsLtd1994CheckingCOM1(pressESCkeytoexit).
.
.
Remoteident:TDR188EBversion1.
02TDR88setforTDversion3.
1td-rp1-rs3blink.
exeTurboDebuggerVersion3.
1Copyright(c)1988,92BorlandInternationalWaitingforhandshakefromremotedriver(Ctrl-Breaktoquit)tdr命令实际是调用另外两个命令的一个批处理文件.
第一个命令告诉板上的调试监控器你用的是哪个版本的TurboDebugger,第二个才实际调用了TurboDebugger.
每一次用Arcom板启动一个调试会话的时候都要发出这两条命令,注1:实际的交互过程和作用TurboDebugger调试一个DOS或Windows程序没有什么不同.
tdr.
bat批处理文件只是用来把它们组合成一个单一的命令.
这里我们再一次使用了程序的可重定址版本,因为我们要把程序下载到RAM里井在那里执行它.
调试器启动选项-rpl和-rp3设置了到调试监控器的通信链路的参数.
-rpl代表"remote-port(远程端口)=1"(COM1).
-rp3代表"remote-speed(远程速率)=3"(38,400波特率),这些是同Arcom调试监控器通信所要求的参数,在建立了和调试监控器的联系之后,TurboDebugger就可以开始运行了.
如果没成功的话可能是串行连接出了问题.
把你的安装过程和SourceView用户手册中的描述对照一下吧.
一旦进人TurboDebugger,你就会看到一个对话框显示"Programoutofdateonremote,sendoverlink(远程的程序已过期,是否通过链路发送)",选择"Yes"后,blink.
exe的内容就会被下载到目标RAM里.
然后调试器会在main处设置第一个断点并指示调试监控器运行程序到此处.
所以你现在看到的就是main的C源代码,一个光标指示着嵌入式处理器的指令指针正指向这个例程的人口点.
使用标准的TurboDebugger命令,你可以单步执行程序、设置断点、监视变量和寄存器的值、做凋试器允许的任何事情、或者可以按下F9立即运行程序的剩下部分.
这样做了以后.
你就能看见板上的绿色LED开始闪烁了.
确认程序和调试器都工作正常之后,按下Arcom板上的复位开关来复位嵌人或处理器,然后LED会停止闪烁.
TurboDebugger又可以响应你的命令了.
仿真器远程调试器用来监视和控制嵌人式软件的状态是很有用,不过只有用在线仿真器(In-CircuitEmulator,ICE)才能检查运行程序的处理器的状态.
实际上,ICE取代了(或者仿真了)目标板上的处理器.
它自己就是一个嵌入式系统,有它自己的目标处理器、RAM、ROM和自己的嵌入式软件.
结果在线仿真器一段非常贵,往往要比目标硬件还贵.
但是这是一种强有力的工具,在某些严格的调试环境下可以帮你很大的忙.
同调试监控器一样,仿真器也有一个远程调试器作为用户界面.
某些情况下,甚至能使用相同的前端调试器.
但是因为仿真器有自己的目标处理器,所以就有可能实时地监视和控制处理器的状态.
这就允许仿真器在调试监控器提供的功能外支持一些高级的调试特性,如:硬件断点和实时跟踪.
使用调试监控器,你可以在你的程序里设置断点.
不过,这些软断点只能到指令提取级别,也就是相当于"在提取该指令前停止运行".
相比之下,仿真器同时支持硬件断点.
硬件断点允许响应多种事件来停止运行.
这些事件不仅包括指令提取,还有内存和I/O读写以及中断.
例如,你可以对事件"当变量foo等下15同时AX寄存器等于0"设置一个硬件断点.
在线仿真器的另一个有用的特性是实时跟踪.
典型地,仿真器包含了大块的专用RAM,专门用来存储执行过的每个指令周期的信息.
这个功能使你可以得知事件发生的精确次序,这就能帮助你回答诸如计时器中断是发生在变量bar变成94之前还是之后这类问题.
另外,通常可以限制存储的信息或者在察看之前预处理数据以精简要检查的数据的数量.
ROM仿真器另外一种仿真器也值得在这里提一下.
ROM仿真器被用来仿真一个只读存储芯片.
和ICE一样,它是一个独立的嵌入式系统并和主机与目标板相连.
不过,这次是通过ROM芯片插座来和目标板连接的.
对于嵌人式处理器,它就像一个只读存储芯片,而对于远程调试器,它又像一个调试监控器.
ROM仿真器相比调试监控器有如下几个优点.
首先,任问人都不需要为你的专有目标硬件移植调试监控器代码.
其次,ROM仿真器通常自带了连接主机的串行或网络连接,所以不必占用主机自己的通常很有限的资源.
最后,ROM仿真器完全替代了原来的ROM,所以不会占用目标板的存储空间来容纳调试监控器代码.
模拟器和其他工具当然,还可以使用另外很多种调试工具,比如模拟器(simulator)、逻辑分析仪和示波器.
模拟器是一个完全基于主机的程序,它模拟了目标处理器的功能和指令集,它的用户用面通常和远程调试器的一样或比较类似.
实际上,可以为后端模拟器使用一个调试器来做前端,就像图4-2显示的那样.
尽管模拟器有很多不足,它在项目的早期特别是还没有任何实际的硬件可以用来试验程序的时候是相当有用的.
图4-2理想的环境:通用的前端调试器注意:调试技巧#2:如果你曾经碰到目标处理器的行为和你阅读数据手册后所想的不一样的话,可以试着用模拟器试验一下程序.
如果你的程序在这里运行良好,那你就知道发生了某种硬件问题.
但是如果模拟器产生了和实际芯片同样的问题,那么你一定自始至终错误地理解了处理器的文档了.
到目前为止,模拟器最大的缺点是它仅能模拟处理器,而嵌人式系统经常包含一个或更多重要的外围设备.
和这些设备的交互有时会限制模拟器的脚本或其他工作内容,而这些用模拟器很难产生的工作内容又会是很重要的.
所以一旦你有了实际的嵌入式硬件以后就很可能用不着模拟器了.
一旦你开始接触你的目标硬件,特别是在硬件调试的时候,逻辑分析仪和示波器是绝对必要的调试工具.
它们是调试处理器和电路板上其他芯片的交互过程的最有用的工具.
不过,因为它们只能观察处理器外部的信号,所以它们不能像调试器或仿真器那样控制作的软件的执行过程.
但是和一个软件调试工具比如远程调试器或仿真器结合起来,它们就是非常有用的.
逻辑分析仪是专门用来调试数字电路硬件的一种实验室设备.
它会有几十个甚至上百个输入.
它们分别只用来做一件事:它所连接的电信号的逻辑电子是1还是0.
你选择的任问输人子集都可以以时间坐标显示出来,如图4-3所示.
大多数逻辑分析仪也允许你以特定的模式捕捉数据或"触发器".
例如,你可能发出如下请求:"显示输人信号1到10的值,但是直到输入2和5同时变为0时才开始记录".
图4-3一个典型的逻辑分析仪的显示结果注意:调试技巧#3:有时可能需要同时观察运行着嵌入式软件的目标板上电信号的一个子集.
例如:你可能想观察处理器和它所连的一个外设的总线交互信号.
一个技巧是在你感兴趣的交互的前面加上一个输出语句.
这个输出语句会在处理器的一个或多个引脚上产生特定的逻辑电平.
例如,你可以使一个空闲的I/O引脚从0变到1,然后逻辑分析仪就可以设置成响应这个事件的触发器并开始捕捉后续的所有情况.
示波器是用于硬件调试的另一种实验室设备,不过它可以在任何硬件上检查任何电信号,不管是模拟的还是数字的.
在手头没有逻辑为析仪的情况下,示波器可以迅速观察特定引脚上的电压,也可以做一些更复杂的事情.
不过,它的输入很少(通常有四个)而且通常没有高级的触发逻辑.
结果,只有在没有软件调试工具的情况下它才会对你有用.
本章讲述的大多数调试工具都会在每个嵌入式项目或多或少地用到.
示波器和逻辑分析仪常用来调试硬件问题,模拟器用在软件开发的早期,调试监控器和仿真器用在实际的软件调试过程中.
为了最有效地利用它们,你应该明白每一个工具用在什么地方,以及什么时候和什么地方使用它才会有最好的效果.

第五章接触硬件硬件[名]计算机系统里可以被你踢上一脚的部分.
作为一个嵌入式软件工程师,你在以后的职业生涯重将会遇到很多不同的硬件.
本章里我会介绍一些我用过的使自己熟悉一种新的电路板的简单过程.
我将引导你创建一个秒数电路板最重要特性的头文件和把硬件初始化到某一已知状态的一部分软件代码.
理解全貌在为一个嵌入式系统写软件之前,你必须先熟悉将要使用的硬件环境.
首先,你需要了解系统的一般操作.
你并不需要了解很小的细节,这些只是现在还用不到,慢慢就会碰到了.
无论何时你拿到一块新的电路板,都应该阅读一下附带的所有文档.
如果这块班子使货价上拿来的标准产品,那么很可能会附带着面向软件开发人员的"用户手册"或"程序员手册",如果板子是你为你的项目专门开发的,文档就可能写得更不清楚或主要是为硬件涉及人员做参考用的,不管怎样,这都是你唯一的最好起点.
再看文档的时候先把板子放在一边.
这会有助你着眼于全局.
等看完资料以后有得是时间来仔细检查电路板.
在拿起这块板子之前,你应该能回答如下两个基本问题:本章内容:z理解全貌z检查一下环境z了解通信过程z接触处理器z研究扩展的外围设备z初始化硬件z这块板子主要目标是什么z数据是如何在里面流动的例如,假设你是一个调制解调器涉及队伍的软件开发人员,并且刚从硬件设计人员那里拿到一块早期的原型电路板.
因为你已经对调制解调器比较熟悉,所以这块板子的主要目标和其中的数据流行你可能相当熟悉.
这块板子的目的是通过模拟电话线发送和接收数据.
硬件从一组电连接上读取数字信号然后转换程模拟信号传到相连的电话线上.
当从电话线上读到模拟数据并输出数字信号的时候数据也会反方向流动.
尽管大多数系统的目的会很明显,可是数据流就可能不会是这样.
我发现一份数据流图可以帮助你快速理解.
如果你幸运的话,硬件所带的文档重会有你所需要的一整套方框图,而且你会发现它会帮助你创建你自己的数据流图.
这样,你就可以不去理会那些和系统数据流无关的硬件元器件了.

对于Arcom电路板,硬件不是面向某一特殊应用设计的,所以为了本章后面的讨论,我们必须想象它有一个设计目的.
我们可以假定这个板子是为了一个打印共享器而设计的.
打印共享器可以允许两台计算机共用一台打印机.
这个设备通过串口来连接每台计算机,通过并行口连接打印机,然后这两台计算机就可以向打印机发送文件了,不过同一时刻只能有一台计算机使用.
为了说明打印共享器的数据流向,我画了图5-1.
(只画出了Arcom板上和这个应用相关的部分.
)通过看这张图,你可以很快地想象以下这个系统地数据流.
从任一串行口接收要打印地数据并保存在RAM里,直到打印机可以接受更多地数据,然后通过并行口把数据送给打印机.
ROM里放了控制这一切地软件.
既然画好了方框图,就别急着把它揉一揉扔掉了,相反在整个项目进行中应已知留着以备参考.
我建议使用一个项目记录本或装订夹,并把这张数据流图作为第一页.
随着你使用这块硬件的工作的进展,把你了解到的所有东西都记到记录本上,你也许还会希望保持有关软件设计和实现的记录.
一个项目记录本不仅在你开发软件时有用,而且在项目结束后也一样.
当你需要对你的软件做一些改动,或者在几个月或几年后做类似工作的时候,你就会很感谢自己当年为保持一份记录而做的额外努力.
图5-1打印共享器的数据流向如果你在读完硬件文档后,对整体情况还有什么疑问的话,你可以去询问一位硬件工程师来取得帮助.
如果你还不认识这个硬件的设计人员的话,先花几分钟把你自己介绍一下,要是有时间的话,可以请他吃午饭或在工作以后送给他一只玩具熊(你可以不必讲这个项目!
).
我发现很多软件工程师和硬件工程师沟通起来有困难,反过来也一样.
在嵌入式系统开发中,软件人员和硬件人员的交流是特别重要的.
检查一下环境经常把自己设想成处理器往往是很有帮助的,因为处理器是最终需要你指示来运行你的软件的部件.
想象一下处理器可能是什么样子:处理器看起来会像是什么如果你从这个角度想的话,你很快就会发现处理器有很多伙伴,在电路板上还有很多硬件部件,而处理器直接和它们进行通信.
这一节里你将学习认识并找到它们.
首先要知道有两种基本类型:存储器和外设.
很明显,存储器是用来存取数据和代码的.
但是你也要考虑外设是什么.
外设是和外部进行交互(I/O)或者完成某一特定硬件功能的特殊硬件设备.
例如,嵌入式系统里最常见的两种外设是串行口和计时器.
前者是一个I/O设备,后者基本上是一个计数器.
Intel80x86和其他一些处理器家族通过两个独立的地址空间来和这些存储器和外设打交道.
第一个地址空间叫存储空间,是用来在取存储器设备的;第二个只为外设保留,叫做I/O空间.
不过,硬件工程师也可以把外设放在存储空间里,这时,我们把这些外设称作存储器映像的外设.
从处理器的角度看,存储器映像的外设和存储设备非常相像.
不过,一个外设和一个存储器的功能有明显的不同.
外设下是简单地存储传给它的数据,而是把它翻译成一条命令或者要以某种方式处理的数据.
如果外设位于存储空间的话,我们就说这个系统有存储器映像I/O.
嵌入式硬件的设计人员往往喜欢只采用存储器映像I/O,因为这样做对硬件和软件开发人员都很方便.
它对硬件开发人员有吸引力是因为他可以因此而省去I/O空间,同时省去了相关的连线.
这也许不会显著地降低电路板的生产成本,但是会降低硬件设计的复杂性.
存储器映像外设对程序员也很有用,他可以更方便、更有效地使用指针、数据结构和联合来和外设进行交互(注1).
存储器映射所有的处理器都在存储器里存放它们的程序和数据.
有时存储器和处理器在同一个芯片里,不过更常见的是存储器会位于外部的存储芯片中.
这些芯片位于处理器的存储空间里,处理器通过叫做地址总线和数据总线的两组电子线路来和它们通信.
要读或写存储器取的某个位置,处理器先把希望的地址写到地址总线上,然后数据就会被传送到数据总线.
在你了解一块新的电路板的时候,可以创建一张表来显示在储空间里每个存储设备和外设的名字和地址范围.
组织一下这张表,让最低的地址位于底部,最高的地址位于顶端.
每次往存储器映射里添加一个设备的时候,按照它在内存里的大概位置来放进表内并用十六进制标出起始和结束地址.
在往存储器映射图里插入所有的设备以后,记着用同样的方式把没利用的存储区域也标记出来.
注1:如果P2LTCH寄存器是存储器映像的话,toggleLed函数就连一行汇编代码都用不着.
回过头来再看一下图5-1所示的Arcom电路板的方框图,你会发现有三个设备连在地址和数据总线上.
它们分别是RAM、ROM和一个标着"Zilog85230串行控制器"的神秘设备.
Arcom提供的文档说RAM位于存储器映射的底端并向上占据了128KB的存储空间.
ROM则位于存储器映射的顶部,井向下拓展了256KB的存储空间.
但是这块区域实际上包含两片ROM:一个EPROM和一个闪速存储器,并且分别具有128KR的容量、第三个设备,Zilog85230串行控制器,是一个存储器映射的外设,它的寄存器的寻址范围在70000h和72000h之间.
图5-2所示的存储器映射显示了对处理器而言这些设备的寻址范围.
在某种意义上,这就是处理器的"通讯录".
就像你在生活中要维护一个名字和地址的列表一样,你也要为处理器维护一张类似的表.
存储器映射图里包含了可以从处理器的存储空间访问的每个存储芯片和外设的人口.
可以证明这张表是关于系统的信息里最重要的部分,并且应该随时更新并作为项目的永久记录的一部分.
图5-2Arcom电路板的存储器映射对于每一块新的电路板来说,你应该创建一个头文件来描述它的特性,这个文件提供了硬件的一个抽象接口.
在实际中,它使你可以通过名字而不是地址来引用板子上的各种设备.
这样做带来的一个额外的好处,是使你的应用软件更加容易移植.
如果存储器映射发生了变化——例如128KB的RAM被移走了——你只需要改变面向电路板头文件中相关的几行,然而重新编译你的程序.

随着本章的进行,我会告诉你如何来为Arcom电路板创建一个头文件.
这个文件的第一部分就列在下面,这部分内容描述了存储器映射.
它和图5-2中所表示的最大的区别是地址的格式.
选读部分"指针和地址"解释了原因.
*MemoryMap*BaseAddressSizeDescription*0000:0000h128KSRAM*2000:0000hUnused*7000:0000hZilogSCCRegisters*7000:1000hZilogSCCInterruptAcknowledge*7000:2000hUnused*C000:0000h128KFlash*E000:0000h128KEPROM#defineSRAM_BASE(void*)0x00000000#defineSCC_BASE(void*)0x70000000#defineSCC_INTACK(void*)0x70000000#defineFLASH_BASE(void*)0xC0000000#defineEPROM_BASE(void*)0xE0000000I/O映射如果存在独立的I/O空间的话,那就需要像完成存储器映射一样也要为电路板创建一个I/O映射.
这个过程完全一样.
简单地创建一张包含外设名字和地址范围的表,并组织一下把低地址放在底端.
典型地,I/O空间里的大部分是未利用的因为大多动外设只有几个寄存器.
图5-3显示了Arcom电路板的I/O映射.
它包含三个设备:外设控制快(PCB)、井行口和调试口.
PCB是80188EB里的一组寄存器,用来控制片上的外设.
控制并行口和调试口的芯片在处理器外面.
这些端口分别是用来和打印机与基于主机的调试器通信用的.
指针和地址在C和C++里,指针的值就是地址.
所以当我们说我们有一个指向数据的指针的时候,实际就是说我们有这个数据的地址.
但是程序员通常不去直接设置或检查这些地址.
这条规则的例外是操作系统、设备驱动程序和嵌入式软件的开发人员,他们有时需要在代码里明确地设置一个指针的值.
不幸的是,对地址的确切表示会因处理器而不同,甚至还会依赖编译器的实现.
这就是说一个像12345h那样的地址可能不会精确地以那个格式被保存,甚至会因编译器不同而保存在不同的地方(注).
这就导致了一个问题,程序员该怎样明确地设置一个指针的值以使它指向存储器映射中希望的位置.
大多数80/86处理器的C/C++编译器使用32位的指针.
不过,比较老的处理器没有一个完全线性的32位地址空间.
例知,Intel80188EB就只有20位的地址空间.
另外,它没有一个内部处理器可以存放超过16位的数据.
所以在这个处理器上,是通过两个16位处理器(段寄存器和偏移寄存器)来组合形成20位物理地址.
(物理地址的计算是把段寄存器左移4位再加上偏移动寄存器,任何溢出到第21位的数据均被忽略.
)为了声明并初始化一个指向位于物理地址12345h处的寄存器的指针,我们如下书写:int*pRegister=(int*)0x10002345;左边16位是段地址,右边16位是偏移地址.
为了方便,80x86的程序员们经常以段地址:偏移地址对来书写地址,使用这种记法,物理地址12345h可以被写做0x1000:2345.
这就是上面我们用来初始化指针的不带冒号的值.
不过,对任一可能的物理地址会有4096个不同的段地址:编移地址对.
例如,地址0x1200:0345和0x1234:0005(还有另外4093个)都引用了物理地址12345h.
注:这种情况在你考虑了某些处理器提供的不同内存模式时变得更为复杂.

本书所有的例子都假设使用80188的大内存模式.
在这种内存模式下,我告诉你的所有细节都包含了整个指针的值,而在别的内存模式下,存储在指针里的地址的格式会因其指向的代码和数据的类型而不同.
图5-3Arcom电路板的I/O映射在为你的板子创建头文件的时候I/O映射也很有用.
I/O空间的每个区域直接映射到一个叫基地址的常数.
上面的I/O映射到常数的翻译如下表所示:**I/OMap**BaseAddressDescription*0000hUnused*FC00hSourceVIEWDebuggerPort(SVIEW)*FD00hParallelI/OPort(PIO)*FE00hUnused*FF00hPeripheralControlBlock(PCB)*#defineSVIEW_BASE0xFC00#definePIO_BASE0xFD00#definePCB_BASE0xFF00了解通信过程既然现在已经知道了与处理器相连的存储器和外设的名称和地址,那就接着学习如问与它们通信.
有两种基本的通信技术:轮询(Polling)和中断(interrupt).
在每一种情况下,处理器都会通过存储或I/O空间向设备发出一些命令.
然后等待设备完成指定的任务.
例如,处理器通知一个定时器从1000倒计数到0,一旦倒计数开始,处理器就只关心一件事:定时器记完数了吗如果使用轮询技术的话,处理器就反复地检查看任务完成没有.
这就像在一个漫长的旅途中一个小孩子不停地问"我们到那儿了吗".
就像那个小孩子一样,处理器花费了很多宝贵的时间来问这个问题而不停地得到否定的回答.
要用软件实现轮询,只需要写一个循环语句来读有疑问的设备的状态寄存器即可.

下面是一个例子:do{//Playgames,read,listentomusic,etc.
//Polltoseeifwe'rethereyet.
status=areWeThereYet();}while(status==NO);第二种通信技术用到了中断.
中断是外设发给处理器的一个异步的电信号.

在使用中断的情况下,处理器和与前面完全一样的方式向外设发命令,不过在处理器等待中断到达的时候,它可以继续做其他的事倩,当中断信号最终到来的时候,处理器把正在做的工作临时搁到一边,执行被称作中断服务例程(ISR)的一小段程序.
ISR执行完后,处理器就继续做刚才的工作.
当然,这不是完全自动的.
程序员必须自己书写ISR然后"安装"并启动它,然后它才会在相关的中断到来的时候被执行.
刚开始做这些的时候,可能对你是一个显著的挑战.
不过,尽管这样,使用中断通常会从整体上减少代码的复杂性井导致一个更好的结构.
与在一段不相干的程序里嵌人设备轮询不一样,这两部分代码保持了适当的独立性.
总的来说,中断与轮询相比是一种更有效的利用处理器的方式.
处理器可以用更多的空余时间去做有用的工作.
不过,使用中断也会带来一些开销.
相对于中断执行一个指令的时间来说需要很多时间来把处理器当前的工作保存到一旁并把控制权传给中断服务程序,需要把处理器的许多寄存器保存到存储器里,低优先级的中断也要被禁止.
所以在实践中这两种方法使用都很频繁.
中断用在效率非常重要或者需要同时监控多个设备的情况,轮询用在处理器需要比使用中断更迅速响应某些事件的情况下.
中断映射大多数嵌入式系统只有很少的几个中断.
每个中断都有一个中断引脚(在处理器芯片外部)和一个ISR.
为了使处理器执行正确的ISR,必须在中断引脚和ISR之间存在一个映射.
这个映射通常是以中断向量表实现的.
向量表通常是位于某些已知内存地址处的指向函数入口的指针,处理器用中断类型(是和每一个引脚相关的一个唯一的数值)来作为这个数组的索引.
存储在向量表那个地方的值通常就是要执行的ISR的地址(注2).
正确地初始化中断向量表非常重要.
(如果初始化不正确的话,ISR也许会响应错误的中断,或者根本不会执行.
)这个过程的第一步是创建一个组织了相关信息的中断映射.
一个中断映射就是一个包含了中断类型和它们所引用的设备的列表.
这些信息会包含在电路板带的文档里.
表5-1显示了Arcom电路板的中断映射.
表5-1Arcom电路板的中断映射中断类型引用设备81718192021Time/Counter#0Zilog85230SCCTime/Counter#1Time/Counter#2串行口接收串行口发送我们的目标依然是把这张表里的信息翻译成对程序员有用的形式.
在创建了注2:一些处理器在这些地方实际上只存放了ISR的头几条指令,而不是指向例程的指针.
像上面那样的一个中断映射以后,你可以在面向电路板的头文件里加人第三个部分.
中断映射里的每一行在头文件里是一个#define语句,如下所示:**InterruptMap*/**Zilog85230SCC*/#defineSCC_INT17/**On-ChipTimer/Counters*/#defineTIMER0_INT8#defineTIMER1_INT18#defineTIMER2_INT19/**On-ChipSerialPorts*/#defineRX_INT20#defineTX_INT21接触处理器如果你以前没用过你的电路板上的处理器的话,现在就应该花一些时间来熟悉一下.
如果你一直用C或C++编程的话,这花不了很长时间.
对高级语言的使用者来说,大多数处理器看起来和用起来都差不多.
不过,要是你做汇编语言编程的话,你就需要熟悉一下处理器的结构和指令集.
关于处理器你想了解的每一样东西都可以在制造商提供的数据手册里找到.

如果你还没有用于你的处理器的数据手册或程序员指南的话,那就马上弄一本来.
如果你想成为一个成功的嵌入式系统程序员的话,你必须能读数据手册并从中得到些什么.
处理器数据手册通常写得很好(就像数据手册应该的那样),所以它们会是一个理想的起点.
一开始先翻翻数据手册,把和手边的工作最有关系的章节记录下来,然后回头阅读处理器总述这一节.
处理器概述许多最常见的处理器都是彼此相关的芯片家族的成员.
在某些情况下,这样一个处理器的成员代表了发展途径上的几个点.
最明显的例子是Intel的80x86家族,它从最初的8086一直横跨到奔腾III,还有更新的.
实际上,80x86家族是如此成功,以至于光仿造它都成了一个工业.
本书中,术语处理器用来指微处理器、微控制器和数字信号处理器三种类型的芯片.
微处理器这个名字通常保留来代表包含了一个功能强大的CPU同时不是为任何已有的特定计算目的而设计的芯片.
这些芯片往往是个人计算机和高端工作站的基础.
最常见的微处理器是Motorola的68K家族(在老式的Macintosh计算机里可以找到)和到处都有的80X86家族.
除了是专门设计采用在嵌入式系统里面这一点,微控制器很像微处理器.
微控制器的特色是把CPU、存储器(少量的RAM、ROM、或者两者都有〕和其他外设包含在同一片集成电路里.
如果你购买包含这一切的一片芯片的话,就有可能充分地减少嵌入式系统的成本.
最流行的微控制器包括8051和它的许多仿造产品,还有Motorola的68HCxx系列.
也能经常发现流行的微处理器的微控制器版本.
比如,Intel的386EX就是很成功的80386微处理器的微控制器版本.
最后一种处理器是数字信号处理器,或者叫DSP.
DSP里的CPU是专门设计来极快地进行离散时间信号处理计算的——比方那些需要进行音频和视频通信的场合.
因为DSP可以比其地处理器更快地进行这类运算,它就为调制解调器和其他通信和多媒体设备的设计提供了一个功能强大、价格低廉的微处理器的替代品.
两个最常见的DSP家族分别是来自TI和Motorola的TMS320Cxx和5600x系列.
Intel的80188EB处理器Arcom板上使用的处理器是Intel80188EB,一个80186的微控制器版本.
除了CPU以外,80188EB还包含一个中断控制单元、两个可编程I/O口、三个定时器/计数器、两个串行口、一个DRAM控制器和一个片选单元.
这些额外的硬件设备位于同一个芯片里并被当作片内外设来使用.
CPU通过内部总线可以和片内外设通信并直接控制它们.
尽管这些片内外设是截然不同的硬件设备,它们都用作80186CPU的很小的扩展.
软件可以通过读写被叫做外设控制块(PCB)的一个256字节的寄存器区来控制它们.
你也许还记得我们在第一次讨论存储器和I/O映射的时候碰到过这个块.
PCB缺省地位于I/O空间里,从地址FF00h开始.
不过要是愿意的话,PCB也可以被重定位到I/O或存储器空间里的任何方便的地址处.
每一个片内外设的控制和状态寄存器都位于相对于PCB基地址的固定偏移处.
在80188EB微处理器用户手册里可以查到每一个寄存器的确切偏移地址.
为了把你的应用软件里和这些细节隔离开,把你会用到的寄存器的偏移地址包合到你的板子的头文件里会是一个很好的做法.
我已经为Arcom电路板做了这个工作,不过了面只显示了会在后面章节里讨论到的寄存器:**On-ChipPeripherals*/**InterruptControlUnit*/#defineEQI(PCB_BASE+0x02)#definePOLL(PCB_BASE+0x04)#definePOLLSTS(PCB_BASE+0x06)#defineIMASK(PCB_BASE+0x08)#definePRIMSK(PCB_BASE+0x0A)#defineINSERV(PCB_BASE+0x0C)#defineREQST(PCB_BASE+0x0E)#defineINSTS(PCB_BASE+0x10)/**Timer/Counters*/#defineTCUCON(PCB_BASE+0x12)#defineT0CNT(PCB_BASE+0x30)#defineT0CMPA(PCB_BASE+0x32)#defineT0CMPB(PCB_BASE+0x34)#defineT0CON(PCB_BASE+0x36)#defineT1CNT(PCB_BASE+0x38)#defineT1CMPA(PCB_BASE+0x3A)#defineT1CMPB(PCB_BASE+0x3C)#defineT1CON(PCB_BASE+0x3E)#defineT2CNT(PCB_BASE+0x40)#defineT2CMPA(PCB_BASE+0x42)#defineT2CON(PCB_BASE+0x46)/**ProgrammableI/OPorts*/#defineP1DIR(PCB_BASE+0x50)#defineP1PIN(PCB_BASE+0x52)#defineP1CON(PCB_BASE+0x54)#defineP1LTCH(PCB_BASE+0x56)#defineP2DIR(PCB_BASE+0x58)#defineP2PIN(PCB_BASE+0x5A)#defineP2CON(PCB_BASE+0x5C)#defineP2LTCH(PCB_BASE+0x5E)其他你会想从处理器手册里了解的事情还包括:z中断向量表应该放在哪里它是否必须位于内存中一个特定的地址如果不是,处理器如问知道在哪里找到它z中断向量表的格式是什么它只是一个指向ISR函数的指针的表吗z处理器自己是否产生一些特殊的叫做陷阱的中断也要为这些中断写ISR吗z怎样开和禁止中断(全部或个别)z怎样得知知或清除中断研究扩展的外围设备现在,你已经研究了除了扩展的外围设备之外的每个部件.
这些扩展的外设是位于处理器外部并通过中断和I/O或存储映射寄存器来和处理器通信的硬件设备.
首先来生成一张扩展设备的列表.
根据你的应用的不同,这张表可能会包含LCD或键盘控制器、A/D变换器、网络接口芯片或者一些定制的ASIC(专用集成电路).
对于Arcom电路板,这张表只包含三个部件:Zilog85230串行控制器、并行口和调试口.
你应该拿到列表上每个设备的用户于用或数据手册.
在项目的前期,你阅读这些文档的目的是要了解这些设备的基本功能.
这些设备做些什么哪些寄存器被用来发命令和取得结果这些寄存器里下同的位和字段的意义是什么如果这个设备会产生中断的话,什么时候产生如何得知或清除一个设备的中断当你设计嵌入式软件的时候,你应该是着按设备来分割程序.
通常为扩展外设结合一个叫做设备驱动程序的软件模块是个不错的主意.
这个工作只是构建一个控制外设执行的软件例程的集会从而把应用软件和具体的硬件设备隔离开来.
我会在第七章"外设"里详细介绍设备驱动程序.
初始化硬件接触你的新硬件的最后一步是写一些初始化程序.
这是你和硬件发展一种紧密的工作关系的最好机会,特别是如果你希望用高级语言编写剩下的软件的活.

在硬件初始化的过程中不可避免地要用到汇编语言.
不过,完成了这一步以后,你就可以用C或C++编写小程序了(注3).
注3:为了使第二章"你的第一个嵌入式程序"里的例子更交易懂一些,我在那里不说明任何初始化代码.
不过,在你写像闪烁LED那样简单的程序之前也要使硬件初始化代码工作起来.
注意:如果你是最早使用一个新的硬件(特别是一个原型产品)的软件工程师之一的话,这个硬件也许不会像所宣称的那样工作.
所有基于处理器的电路板都需要进行一些软件测试来确认硬件设计和各种外设的功能的正确性.
当一些功能发生错误的时候会把你置于尴尬的境地.
你怎么知道指责备硬件还是软件如果你碰巧对硬件比较熟悉或者可以使用模拟器的话,你也许能设计一些实验来回答这个问题.
否则,你可能得请一位硬件工程师来和你一起进行一个调试过程.
硬件初始化应该在第三章"编译、链接和定址"里所讲的启动代码之前执行,那里描述的代码假定硬件已被初始化从而只用来为高级语言程序创建一个合适的运行时环境.
图5-4提供了关于整个初始化过程的一般的描述,从处理器复位到硬件初始化和C/C++启动代码一直到main.
初始化过程的第一步是复位代码.
这是处理器上电或复位时立刻执行的一小段汇编语言(通常只有两到三十指令).
这段代码的唯一目的是把控制传给硬件初始比例程.
复位代码的第一个指令必须放在处理器数据手册里指定的在内存里的特定位置,通常叫做复位地址.
80188EB的复位地址是FFFF0h.
图5-4硬件和软件初始化过程大多数实际的硬件初始出发生在第二个阶段.
在这个地方,我们需要告诉处理器它自己所处的环境.
这也是初始化中断控制器和其他重要外设的好地方.

不太重要的外设可以在启动相应设备驱动程序的时候再初始化,而这些工作经常是在main里面完成的.
在用Inlel80188FB处理器做任何工作之前,有几个内部寄存器必须被编程.
这些寄存器作为处理器内部的片选单元的一部分负责设置存储器和I/O映射.
通过对片选寄存器编程,你实质上唤醒了连在处理器上的每一个存储和I/O设备.
每一个片选寄存器都和一条连结处理器和其他芯片的"芯片开关"相联系.
特定的片选寄存器和硬件设备之间的这种联系是由硬件工程师建立的.
你所要做的只是从他那里要一份片选寄存器的设置的清单并把这些设置调到寄存器里.

在刚启动的时候,80188EB假设了一个最坏环境.
它假定只有1024字节的ROM(位于地址范围FFC00h到FFFFFh)同时没有其他存储或I/O设备.
这是处理器的"胎儿阶段",它假定了例程hw_init必须位于地址FFC00h(或更高处),而且这个例程必须不要求使用RAM.
硬件初始化例程一开始就设置片选寄存器来告诉处理器有关安装在电路板上的存储和I/O设备的信息.
这个任务完成以后,整个ROM和RAM地址范围就会都有效,然后你的剩下的程序就可以位于ROM或RAM任何方便的地方.
第三个初始性阶段包含了启动代码,就是我们在第三章里看过的汇编语言代码.
提醒一下,它的任务是为用高级语言书写的代码做准备工作.
重要的是只有启动代码会调用main.
从这以后,你所有的软件都可以用C或C++来写了.
你已开始理解嵌人式软件是如何从处理器复位过渡到你的主程序了.
必须承认,第一次把所有这些组件(复位代码、硬件初始化、C/C++启动代码和应用程序)综合在一起放到一个新板子上可能有些问题,所以要准备花一些时间来分别进行调试.
坦率地说,这是这个项目里最难的部分.
你很快就会看到,如果你已经有了一个可以工作的闪烁LED程序作基础的话,工作将越来越容易,或者至少是更像普通的计算机编程.
一直到现在我们都在为嵌入式编程建造基础结构,但是我们接下来在后续章节里要讨论的主题是关于高级结构的:存储器测试、设备驱动程序、操作系统和真正有用的程序.
这可能会是你在其他计算机系统项目里见过的软件,不过在嵌入式编程环境下有一些新的做法.
第六章存储器Tyrell:如果我们给他们一个过去,我们就为他们的情感创造了一个缓冲,因此我们可以更好地控制他们.
Deckard:回忆,你是在说会议.
——电影《BladeRunner》在这一章里,你将学会嵌入式系统中关于存储器所需要知道的知识.
特别是,你会学习几种可能碰到的存储器,怎样测试存储设备以知道它们是否正常工作.

以及怎样使用快闪存储器.
存储器的类型很多类型的存储设备在现代计算机系统中都是可得的.
作为一个嵌入式软件工程师,你必须明白它们之间的差别以及理解怎么有效地使用每一种类型的存储器.
我的讨论中,我们要从一个软件的角度来接触这些设备.
你在读书的时候,要记住这些设备的发展经历了几十年,因而在底层的硬件有着明显的差别.

存储器类型的名字常常反映了历史发展的自然过程,经常是令人混淆的而没有更深的含义.
大部分的软件开发者把存储器想成是随机存取的(RAM)或者是只读的(ROM).
但是,实际上,每一种都有亚型,甚至有混合型的第三类存储器.
在一个RAM设备中,存储在存储器中每一个位置的数据都可以在需要的时候读或者写.
在一个ROM设备中,存储在存储器中每一个位置的数据可以随意的读取,但是不能够写入.
有些时候,在一个类ROM设备中改写其中的数据是可能的.
这种设备叫作混合存储器,因为它们同时表现了RAM和ROM的一些特征.
图6-1为常用于嵌入式系统中的存储设备提供了一个分类.
本章内容:z存储器的类型z存储器的测试z验证存储器内容z使用快闪存储器图6-1嵌入式系统中常用的存储类型RAM的类型在RAM家族中有两种重要的存储设备:SRAM和DRAM.
它们之间的主要差别是存储于其中的数据的寿命.
SRAM(静态RAM)只要是芯片有电就会保留其中的内容.
然而,如果电源切断了或者是暂时断电了,其中的内容就会永远的丢失.
另一方面,DRAM(动态RAM)只有极短的数据寿命——通常不超过0.
25秒.
即使是在连续供电的情况下也是如此.
简言之,当你听到RAM这个词的时候,SRAM具有你想像中的存储器的所有属性.
与此相比,DRAM听起来没有用.
一个只保留所存数据零点几秒的存储设备有什么好处呢就其本身来说这样一个易失数据的存储器确实是无用的.
然而,一个叫作DRAM控制器的简单硬件可以使DRAM的行为更像SRAM.
(请参看后面的选读部分"DRAM控制器".
)DRAM控制器的任务是周期性地刷新DRAM中存储的数据.
通过一秒钟之内几次刷新数据,DRAM控制器就可以在需要的时间内保持DRAM中数据有效.
因此DRAM归根结底和SRAM是同样有用的.
在决定选用哪一种类型的存储器的时候,系统设什者要考虑存取时间和成本.
SRAM设备提供了极快的存取时间(大约比DRAM快四倍),但是制造起来十分的昂贵.
通常SRAM只是用于那此存取速度极端重要的场合.
在大量的RAM需要的时候,每字节的更低价格使得DRAM很吸引人.
很多嵌入式系统两种类型都包括:关键数据通道上的一小块SRAM(几百个千字节)和其他所有地方的一大块DRAM(以兆计).
ROM的类型ROM家族中的存储器是按照向其中写入新数据的方法(通常叫作编程)及其可以重写的次数来区分的.
这个划分反映了ROM设备从硬连线,到一次性可编程,到可擦写可编程的演化过程.
这些设备的一个共同的特性就是它们都能够永久地保存数格和程序,甚至是断电之后.
真正第一个ROM是硬连线设备,它包含一组预先编排的数据或者指令.
ROM中的内容下得不在芯片生产出来之前指定.
因此实际的数据被用来安排芯片内部的晶体管.
硬连线内存仍旧在使用,但是它们现在叫作"掩膜ROM"以和其他类型的ROM区分.
掩膜ROM主要的优点是低的产品成本.
不幸的是,只有在需要成百上千相同ROM的拷贝时,成本才是低廉的.
比掩膜ROM更进一步的是PROM(可编程ROM).
它买来的时候处于未被编程的状态.
如果你要看一个未经编程的PROM地内容,你会看到数据的每一位完全由1组成.
把你的数据写人PROM的过程涉及到一个特殊的设备、叫作设备编程器.
设备编程器通过向芯片的管脚加电,每一次向设备中写入一个DRAM控制器如果你的嵌入式系统包括了DRAM,板子上(或者是芯片内)可能也会有一个DRAM控制器,DRAM控制器是位于处理器和存储器芯片之间额外的一个硬件.
它的主要用途是执行刷新操作使得DRAM中数据有效.
然而,没有你的帮助它不能够准确地做到这一点.
你的软件首先要做的一件事就是初始化DRAM控制器.
如果你的系统中没有其他任何的RAM,你必须在创建堆或者栈之前这样做.
结果,这个初始化代码通常用汇编语言编写,放置在硬件初始化模块里.
几乎所有的DRAM控制器都需要一个短的初始化序列,其中包括一条或者多条初始化命令.
初始化命令告诉控制器硬件对于DRAM的接口以及其中的数据必须以怎样的频率刷新.
要为作具体的系统确定初始化序列,就要咨询板子的设计者或者是读描述DRAM和DRAM控制器的数据手册.
如果你的系统中DRAM的工作好像不正常,那么可能是DRAM控制器没有初始化或者没有准确地初始化.
字节.
一旦一个PROM通过这种方法被编程了,其中的内容就再也不能改变了.
如果存储在PROM中的代码或者数据必须改变,目前这个设备就必须废弃.
PROM也称一次性可编程设备.
EPROM(可擦写可编程ROM)编程的方式和PROM完全一样.
然而,EPROM是可以被擦除并且反复被编程的.
为了擦除一个EPROM,你只要把设备暴露在强紫外线光源下.
(在设备的顶端有一个让紫外线照射到硅的窗口.
)这样做,你基本上可以把整个芯片重置到其初始状态——未编程状态.
尽管比PROM要贵,但是它们可以被再编程的能力使得EPROM成为软件开发及测试过程必需的一部分.
混合类型由于存储器技术在最近几年已经成熟,在RAM和ROM设备之间的界线已经变得模糊.
现在有几种类型的存储器结合了两者的优点.
这些存储器不属于任何一类,总体上可以看作是混合存储设备.
混合存储器随意地读写,像RAM一样,但是保持其内容而不需要供电,就像ROM一样.
有两种混合设备,EEPROM和快闪存储器,是ROM设备的子代;第三种,NVRAM,是SRAM的改版.
EEPROM是电可擦除可编程的.
在内部,它们和EPROM类似,但是擦除操作是完全依靠电力的,而不是通过在紫外线暴晒.
EEPROM中的任何一个字节都可以擦除和重写.
一旦写人,新的数据就永远的保留在设备中了——至少直到它被擦除.
对于这个改进了的功能的权衡主要是更高的价格.
其写入周期也明显比写人一个RAM的要长,因此你不要指望利用EEPROM作为你的主要系统内存.
快门存储器是存储器技术最新的发展.
它结合了目前为止所有存储设备的优点.
快闪存储设备具有高密度、低价格、非易失性、快速(读取,而不是写入)以及电气可重编程等特点.
这些优点是庄倒一切的,作为一个直接的结果,快闪存储器在嵌入式系统中的使用迅速增长.
从软件的观点来说快速存储和EEPROM技术十分的类似.
主要的差别是快速存储设备一次只能擦除一个扇区,而不是一个字节一个字节的擦除.
典型的质区的大小是在256字节到16千字节的范围.
尽管如此,快速存储设备比EEPROM要流行的多,并且还迅速地取代了很多ROM设备.
混合存储器的第三个成员是NVRAM(nonvolatileRAM,非易失RAM).
非易失性是ROM及混合存储器前面讨论过的一个特征.
然而,NVRAM物理上与那些设备非常不同.
NVRAM通常只是一个带有后备电池的SRAM.
当电源接通的时候,NVRAM就像任何一个其他的SRAM一样.
但是当电源切断的时候,NVRAM从电池中获取足够的电力以保持其中现存的内容.
NVRAM在嵌入式系统中是十分普遍的.
然而,它是十分的昂贵——甚至比SRAM还要昂贵——因此,它的应用被限制于存储仅仅几百字节的系统关键信息,这些信息不可能有更好的存储办法了.
表6-1概括了不同存储器类型的特征.
表6-1存储器设备特征存储器种类易失性可写擦除大小擦除周期相对价格相对速度SRAMDRAM掩膜ROMPROMEPROMEEPROM快闪存储器NVRAM是是否否否否否否是是否用编程器可写一次是,利用编程器是是是字节字节无无整个芯片字节扇区字节无限期无限期无无有限制(见说明书)有限制(见说明书)有限制(见说明书)无昂贵适中不贵适中适中昂贵适中昂贵快适中快快快快,读取快写入慢读取快写入慢快存储器的测试一个严格的嵌入式软件作首先可能要写的是存储器的测试.
一旦原型硬件就绪,设计者要做一些确认,确认她已经正确地连接了地址和数据线,确认内存芯片正常工作.
开始,这可能看上去是一个很简单的任务,但是当你更加仔细地查看你的问题的时候.
你会意识到利用一个简单的测试来发现细微的内存问题是很困难的.
事实上,作为程序员天真的结果.
很多嵌入式系统包含的内存测试只能发现最具灾难性的内存故障.
其中一些甚至检测不出内存从板子上移走了.
存储器测试的目的是确认在存储设备中的每一个存储位置都在工作.
换一句话说,如果你把数50存储在一个具体的地址,你希望可以找到存储在那里的那个数,直到另一个数写入.
任何存储器测试的基本思想是写一些数值到每一个内存设备的地址,校验读回的数据.
如果所有读回的数据和那些写入的数据是一样的,那么就可以说存储设备通过了测试.
正如你将要看到的,只有通过认直接存储器存取直接存储器存取(DMA)是一种直接在两个硬件设备之间传输数据决的技术.
如果没有DMA,处理器必须从一个设备中读取数据并且向另一个设备写入,一次一个字节或者一个字.
如果要传输的数据量很大,传输的频率很高,软件的其他部分可能再也没有机会运行了.
然而,如果有一个DMA控制器,就有可能让它执行整个传输,而几乎不借助于处理器.
DMA是这样工作的.
当一块数据要被传输,处理器向DMA控制器提供源和目标地址以及字节的总数.
DMA就自动地把数据从原传输到目标.
在每一个字节被拷贝之后,每一个地址加1并且剩余字节的数目减1.
当剩余字节数目为零的时候,快传递结束,DMA控制器向处理器发送一个中断.
在一个典型的DMA场景里,数据块被直接传输到内存或者从内存传出.
比如,一个网络控制器要把一个网络包在它到达的时候传入内存,只要在整个包接收后通知处理器即可.
利用DMA,处理器可以把更多的时间花费在数据到达后对其的处理,而花费更少的处理数据在设备之间的传输.
处理器和DMA控制器此时必须共享地址和数据总线,但是这是由硬件自动处理的,另一方面,处理器也不涉及实际的传输过程.
真选择的一组数据你才可以确信通过的结果是有意义的.
当然,像刚才描述的有储器的测试不可避免地具有破坏性.
在内存测试过程中,你必须覆盖它原先的内容.
因为重写非易失性存储器内容通常来说是不可行的,这一部分描述的测试通常只适用于RAM的测试.
然而,如果混合存储器的内容不重要——当它们处于产品的开发阶段——这些同样的算法也可以用于这些设备的测试.
验证一个非易失性存储器内容的问题在本章的后面部分介绍.

普通的存储器问题在学习具体的测试算法之前,你应该了解可能遇到的各种存储器问题.
在软件工程师中一个普遍的误解是,大部分的存储器问题发生在芯片的内部.
尽管这类问题一度是一个主要的问题,但是它们在日益减少.
存储设备的制造商们对于每一个批量的芯片都进行了各种产品后期测试.
因此,即使某一个批量有问题,其中某个坏芯片进人到你的系统的可能性是微乎极微的.
你可能遇到的一种类型的存储芯片问题是灾难性的失效.
这通常是在加工好之后芯片受到物理或者是电子损伤造成的.
灾难性大效是少见的,通常影响芯片中的大部分.
因为一大片区域受到影响,所以灾难性的失效当然可以被合适的侧试算法检测到.
在沃的经历中,存储器出问题比较普遍的原因是电路板故障.
典型的电路板故障有:z在处理器与存储设备之间的连线问题z无存储器芯片z存储器芯片的不正确插人这些问题是一个好的存储器测试算法可以检测到的.
这种测试不用特别地查找灾难性存储器故障,也应该能够检测到这类问题.
所以我们更详细地讨论电路板的故障.
电于线路问题电子线路问题可能是由电路板设计或者制造中的错误造成的,也可能是在加工好以后损坏的.
连接存储器和处理器的每一根线都是三种中的一种:地址线、数据线、控制线.
地址和数据线分别用来选择内存地址以及传输数据.
控制线告诉存储设备处理器是要读还是写,以及数据将被传输的精确时间.
不幸的是,这些线路中的一条或者多条有可能被不正确的布置或者以短路(也就是说,和板子上的其他线路连接)或者开路(不和任何电路连接)的方式受到损坏.
这些问题经常是由一点焊接飞溅或者是由断路造成的.
两种情况都在图6-2中做了说明.
图6-2可能出现的错误连接到处理器的电子线路问题会引起存储设备不正确的行为.
数据可能存储的不正确,或存储在错误的地址,或者根本就没有保存.
这样的情况可以分别地解释为数据线、地址线以及控制线上的线路问题.
如果问题出在数据线路上,几个数据位可能看上去像是被"粘"在了一起(也就是说,无论传输的数据如何,两个或者两个以上的位总是包含相同的值.
类似地,一个数据位可能或者"粘高(总是1),或者"粘低(总是0).
这些问题可以通过写入一个设计好的数据序列来检测.
每一个数据管脚可以被设置成0和1而坏受其他管脚的影响.
如果地址线出了问题,那么两个存储器位置中的内容看上去可能像是重叠的.
换言之,写到某一个地址的数据会覆盖其他地址的内容.
这是因为被短路或者开路的地址线会使得存储设备看到的地址不同于处理器选择的地址.

另一个可能性就是控制线短路或者开路.
尽管理论上可能对控制线路问题进行专门的测试,但是不可能描述一个对这类问题的普遍的测试.
很多控制信号的操作都是处理器或者存储器结构专用的.
庆幸的是,如果控制线路有问题,存储设备根本就不工作,而这是可以通过其他的有储器的测试检测到的.
如果你怀疑控制线路有问题,那么在构造一个专门的测试之前最好征求一下板子设计者的意见.
无存储器芯片无存储器芯片无疑是一个应该被检测出的问题.
糟糕的是,由于无连接电子线路的电容特性,一些存储器测试不能检测到这个问题.
比如,假设你决定使用下面的测试算法:把值l写入到存储器的第一个位置,然后把它读回,验证它的值,把值2写人到存储器的第二个位置,读回它并验证,把值3写入第三个位置,验证它,依次类推.
因为每一次读的操作都是紧跟在相应的写操作之后.

所以有可能读回来的数据不代表任何东西,只不过是上次写操作保留在数据总线上的电压罢了.
如果数据被过快的读回来,那么表面上看来数据好像已经被正确地保存入了存储器——即使在总线的另一端没有存储器芯片存在.

为了检测出无存储器芯片,测试必须改动.
不是在相应的写操作之后立即读人验证,而是执行几个连续的写操作后再进行同样数量的读操作.
比如,把值1写入到第一个位置,值2写入到第二个操作,值3写入到第三个位置,然后验证第一个位置的数据,验证第二个位置的数据等等.
如果数据的值是唯一的(像刚描述的那个测试那样),那么无存储器芯片就可以被测试出了:读回来的第一个值会对应于最后写人的值(3),而不是第一个写入的值(1).
芯片的不正确插入如果有存储器芯片,但是插入到插槽时不正确,系统通常会表现出好像是一个连线问题或者是找不到存储器芯片.
也就是说,存储器芯片的一些管脚根本没有和插槽相连,或者插入了错误的地方.
这些管脚会是数据线、地址线或者是控制线的一部分.
因此,只要你能检测连线问题及无芯片,任何芯片的不正确插入也就可以自动检测出来了.
在继续讨论之前,让我们快速回顾一下我们必须检测的存储器问题的类型.

存储器芯片很少有内部错误,但是,如果它们存在这样的错误,那么它们本质上可能是致命的,会被任何的测试检测到.
问题的较普通的原因是电路板故障,其中会遇到连线问题,或者是无存储器芯片,或者是芯片的不正确插入.
其他的错误也会遇到,但是,这里描述的问题是最普通的,也是用通用方法测试起来最简单的.
制定测试策略通过仔细地选择你的测试数据以及被测试地址的顺序,是有可能检测出所有前面描述的存储器错误的.
通常最好把你的存储器测试分成小的,思路简单的块.
这有助于提高总体测试的效率以及代码的可读性.
较专业的测试还会在检测到问题时提供更为详细的关于问题来源的信息.
我发现最好有三个独立的测试:数据总线的测试、地址总线的测试以及设备的测试.
前面两个测试针对电子连线的问题以及芯片的不正确插入;第三个倾向于检测芯片的有无以及灾难性失效.
作为一个意外的结果,设备的测试也可以发现控制总线的问题,尽管它不能提供关于问题来源的有用信息.
执行这三个测试的顺序是重要的.
正确的顺序是:首先进行数据总线测试,接着是地址总线测试,最后是设备测试.
那是因为地址总线测试假设数据总线在正常工作,除非数据总线和地址总线已知是正常的,否则设备测试便毫无意义.
如果任何测试失败,你都应该和一个硬件工程师一起确定问题的来源.
通过查看测试失败处的数据值或者地址,她应该能够迅速地找出电路板上的问题.

数据总线测试我们首先要测试的就是数据总线.
我们需要确定任何由处理器放置在数据总线上的值都被另一端的存储设备正确接收.
最明显的测试方法就是写人所有可能的数据值并且验证存储设备成功地存储了每一个.
然而,那并不是最有效率的测试方法.
一个更快的测试方法是一次测试总线上的一位.
如果每一个数据上可被设置成为0和1,而不受其他数据位的影响,那么数据总线就通过了测试.
独立测试每一个数据位的好办法是执行所谓的"走l测试".
表6-2说明了这个测试的8位版本中使用的数据模式.
这个名字"走1测试',来自这样一个事实:一个数据位被设置成1,并且走过整个数据字.
用于测试的数据值的数目和数据总线的宽度一样.
这就使数据测试的次数从2n减少到n,其中n是数据总线的宽度.
表6-2"走1测试法"中的连续数据值0000000100000010000001000000100000010000001000000100000010000000因为我们只是测试这一点的数据总线,所以所有的数据值都可以写人相同的地址.
在存储设备中的任何地址都可以.
然而,如果数据总线为了连接不见一个存储器芯片而分裂开来.
那么你需要在多个地址进行数据总线的测试,每一个芯片对应一个地址.
为了执行"走1操作",只要把表中的第一个数据写入,通过读操作来验证它,写入第二个数据,验证它等待.
当你到达表的结尾的时候,测试就结束了.

可以在写操作之后立即进行读操作,因为我们不是在测试芯片的有无.
事实上,即使存储器芯片没有安装,这个测试也可以提供有意义的结果.
函数memTestDataBus()说明了如何用C来实现"走1测试"的.
它假设调用者会选择测试地址,并且在这个地址测试整个一组数据.
如果数据总线正确工作,函数会返回0.
否则它会返回测试失败的那个数值.
如果有故障的话,那个返回的数值对应于第一个有故障的数据行.
typedefunsignedchardatum;/*Setthedatabuswidthto8bits.
*/*Function:memTestDataBus()**Description:Testthedatabuswiringinamemoryregionby*performingawalking1'stestatafixedaddress*withinthatregion.
Theaddress(andhencethe*memoryregion)isselectedbythecaller.
*Notes:**Returns:0ifthetestsucceeds.
*Anonzeroresultisthefirstpatternthatfailed.
*datummemTestDataBus(volatiledatum*address){datumpattern;/**Performawalking1'stestatthegivenaddress.
*/for(pattern=1;pattern!
=0;pattern>(WIDTH-8))^message[offset];remainder=crcTable[byte]^(remainderintControl.
timerControl&=~(TIMER_MASK|TIMER_PRIORITY);////Initializethehardwaredevice(useTimer#2).
//gProcessor.
pPCB->timer[2].
count=0;gProcessor.
pPCB->timer[2].
maxCountA=CYCLES_PER_TICK;gProcessor.
pPCB->timer[2].
control=TIMER_ENABLE|TIMER_INTERRUPT|TIMER_PERIODIC;////Markthetimerhardwareinitialized.
//bInitialized=1;}}/*Timer()*/全局对象gProcessor在头文件对i8018EB.
h中声明.
它表示Intel80188EB处理器.
i8018xEB类是我写过的东西,它包含了简化与处理器以及芯片级外设的交互的方法.
其中一个方法就是installHandler,它的工作是把中断服务例程插人到中断向量表中,这个类还包括一个叫作PCB的全局数据结构,它能够覆盖外设控制块的存储映像寄存器(注2).
与时钟/计数器单元2相关的三个寄存器正好是这个256字节的数据结构的一小部分.
(为了美观,我把PCB数据结构实现成为一组嵌套式结构.
因此,时钟/计数器单元2的控制寄自器可以像pPCB->timer[2].
control这样来访问.
)时钟/计数器单元的初始化由这几个部分组成:count寄存器重置为零,把倒计时长度调入maxCountA寄存器以及在control寄存器里设置几个位.
上面我们做的是启动一个1ms的周期性时钟,它在每一个周期结束的时候产生一个中断.
(这个周期性时钟将作为一个时钟节拍,我们需要用它来创建任意长度的软件时钟.
)调人maxCountA的值可以通过数学的办法来决定,因为它代表了1ms内输人到时钟/计数器中的时钟周期数.
按照80188EB的数据手册,这会是1ms内处理器周期数的四分之一.
因此,对于一个25MHz的处理器,就像我们现在正在使用的那个(也就是说,一秒钟25000000个周期,或者你喜欢的话,一毫秒25000个周期),maxCountA该被设置成25000/4——正如前面的常数CYCLES_PER_TICK一样.
注2:聪明的读者可能会想起在第五章"开始认识硬件"中,我说过PCB位于80188EB处理器的I/O空间.
然而,因为在设备驱动的情况下存储映像寄存器更为可能,所以我就把整个的PCB定位到物理地址72000h,即存储空间里.
书中剩余的部分将假设这个新的位置.
要知道这个重定位是如何可实现的,看看i8018xEB类的构造函数就可以了.
一旦硬件被初始化,时钟被建立,就可能启动一个任何长度的软件时钟了,只要那个长度可以表示成为时钟节拍的整数倍.
因为我们的时钟节拍是1ms长,因此应用程序员能够创建在1到65535ms之间(65.
536秒)的任意长度的时钟.
他可以通过调用start方法实现这一点.
**Method:start()**Description:Startasoftwaretimer,basedonthetickfromtheunderlyinghardwaretimer.
**Notes:**Returns:0onsuccess,-1ifthetimerisalreadyinuse.
*intTimer::start(unsignedintnMilliseconds,TimerTypetimerType){if(state!
=Idle){return(-1);}////Initializethesoftwaretimer.
//state=Active;type=timerType;length=nMilliseconds/MS_PER_TICK;////Addthistimertotheactivetimerlist.
//tiemrList.
insert(this);return(0);}/*start()*/当一个软件时钟被启动时,数据成员state、type和length被初始化,时钟被插入到一个叫作时钟列表的活动时钟键表.
在时钟列表中的时钟是经过排序的以使第一个到期的时钟在表的顶端.
此外,每一个时钟有一个与其相关的count变量.
这个值代表了所有列表前面的时钟到期时该软件时钟剩余的时钟节拍数.
总之,这些设计的选择有利于快速更新时钟列表,而付出的代价是减慢了插入和删除的速度.
刷新的速度是重要的,因为时钟列表在每次硬件产生时钟节拍中断的时候——每毫秒一次——都要刷新.
图7-1说明了在运转的时钟列表.
记住每一个软件时钟都有它自己的唯一的长度和开始时间,但是一旦它们被插入列表,就只有count字段和排序有关.
在说明的例子中,第一个和第二个时钟同时启动(第一个可能实际上是被重启的,因为它是周期性的).
由于第二个比第一个5ms,因此它晚5个时钟周期到期.
列表中第二个和第三个碰巧是同时结束,虽然第三十时钟可能要多运行10倍的时间.
图7-1运转的时钟列表中断服务例程的代码如下所示.
这个例程被声明为voidinterrupt类型.
关键字interrupt是只有针对80x86处理器的编译器才能理解的C/C++语言的一个延伸.
通过这样的声明,我们要求编译器在进出的时候保存和恢复所有处理器的寄存器,而不仅仅是普通函数凋用过程中保存的那些寄存器.
**Method:Interrupt()**Description:Aninterrupthandlerforthetimerhardware.
**Notes:Thismethodisdeclaredstatic,sothatwecannot*inadvertentlymodifyanyofthesoftwretimers.
**Returns:Nonedefined.
*voidinterruptTimer::Interrupt(){////Decrementtheactivetimer'scount.
//timerList.
tick();////Acknowledgethetimerinterrupt.
//gProcessor.
pPCB->intControl.
eoi=EOI_NONSPECIFIC;////CleartheMaximumCountbit(tostartthenextcycle).
//gProcessor.
pPCB->timer[2].
control&=~TIMER_MAXCOUNT;}/*Interrupt*/当然,TimerList类中的tick方法做了大部分的工作.
这个方法是和链表操作最相关的,但是看上去不是十分的令人兴奋.
简单地说,tick方法开始就把列表最顶端时钟的节拍计数减1.
如果那个时钟的count已经为零了,它就把软件时钟的状态改变到然后从时钟列表中删除它.
对于任何在那个时刻要到期的时钟,它会执行相同的操作.
这些就是位于列表新的头部并且count值为零的时钟.
创建和启动一个软件时钟之后,应用程序员可以做一些其他的工作,然后检查一下是否软件时钟已经到期.
waitfor方法就是为了这个目的而提供的.
这个例程会堵塞,直到TimeList.
tick把软件时钟的状态改变成Done.
这个方法的实现如下:**Method:waitfor()**Description:Waitforthesoftwaretimertofinish.
**Notes:**Returns:0onsuccess,-1ifthetimerisnotrunning.
*intTimer:waitfor(){if(state!
=Active){return(-1);}////Waitforthetimertoexpire.
//while(state!
=Done);////Restartoridlethetimer,dependingonitstype.
//if(type==Periodic){state=Active;timerList.
insert(this);}else{state=Idle;}return(0);}/*waitfor()*/关于这段代码要注意到的最重要的事情是while(state!
=Done)不是无限循环.
那是因为,正如我们几个段落之前学过的那样,时钟的状态是被timerList.
tick()修改的,而它是被中断服务例程所调用的.
实际上,如果我们是细心的嵌入式程序员,我们会把state声明成volatile型的变量.
这样做会防止编译器不正确地认为时神状态是完成或者是未完成,从而把while循环优化掉(注3).
Timer类的最后一个方法用来取消一个运行的时钟.
这容易实现,因为只要把时钟从时钟列表中删除即可,并且把它的状态改变为Idle状态即可.
实际的代码如下:**Method:cancel()**Description:Stoparunningtimer.
**Notes:**Returns:Nonedefined.
*注3:使用waitfor要小心,这个实现在等待软件时钟改变到完成状态的时候不停地运转.
这就是叫作忙等待,并且它是一种既不优雅又没有效率的使用处理器的方法.
在第八章"操作系统"中,我们会看到引入操作系统可以提高这个实现.
voidTimer::cancel(void){////Removethetimerfromthetimerlist.
//if(state==Active){timerList.
remove(this);}////Resetthetimer'sstate.
//state=Idle;}/*cancel()*/当然,Timer类还有一个析构函数,我没有在这里列出其代码.
不过说明下面这一点就已经足够了.
它只是查看软件时钟是否活动,如果是,就从时钟到表中删除它.
这样可以防止超出范围的周期性时钟无限地留在时钟到表里,可以防止任问指向"死"时钟的指针留在系统里.
为了保持完整性,最好加入一些公用的方法,也许叫作poll,它使得使用Timer类的用户可以测试软件时钟的状态而不堵塞.
限于篇幅,我就不把它加入到我的实现中来,但是加入这样一个例程很简单,它只要返回表达式state==Done的当前值即可.
然而,要这么做,需要设计某种技术来重启周期时钟而不再调用waitfor函数.
Timer类的另一个潜在的特性是异步回叫(callback).
也就是说,为什么不让软件时钟的创建者向它绑定一个函数.
这个函数可以被自动地调用——通过timerList.
tick——每一次时钟到期的时候.
当你阅读下面一部分的时候,一定要思考如果使用异步同叫,那么LED闪烁程序看上去会是什么样子.
这是异步回叫特别适合的一种应用.
修改后的闪烁程序现在Timer类已经在我们的掌握之中,因此有可能重写这本书的第一个例子以使得它的定时更加准确.
记得在我们的原始实现中,我们基于这样一个事实,对于一个给定的处理器和速度"减1和比较"操作的长度是固定的.
我们只是猜想那可能会多长,然后基于经验的测试修改我们的估计.
通过使用Timer类,我们能够在增加程序可靠性的同时去除这个猜测的工作.
在下面修改后LED闪烁程序中,你会看到我们现在能够简单地启动一个周期为500ms的软件时钟,切换指示灯,然后在再次切换指示灯之前等待时钟到期.
同时,我们可以执行应用程序其他处理任务.
#include"timer.
h"#include"led.
h"**Function:main()**Description:BlinkthegreenLEDonceasecond.
**Notes:Thisouterloopishardware-independent.
However,itcalls*thehardware-dependentfunctiontoggleLed().
**Returns:Thisroutinecontainsaninfiniteloop.
*voidmain(void){Timertimer;timer.
start(500,Periodic);//Startaperiodic500mstimer.
while(1){toggleLed(LED_GREEN);//TogglethegreenLED.
//Dootherusefulworkhere.
timer.
waitfor();//Waitforthetimertoexpire.
}}/*main()*/监视定时器(watchdog)你可能听说过另一种经常被提到的关于嵌入式系统的时钟是监视定时器.
这是一种特殊的硬件,它保护系统免受软件挂起之苦.
监视定时器总是从一个大数倒计收到零.
这个典型的过程要花费几秒钟来完成.
在此期间,嵌入式软件可能"踢出"监视定器,重设它的计数器为原先的大数.
如果计数器曾经到达零,那么监视定时器会认为软件被挂起.
它重新启动嵌入式芯片,从而重启软件.
在系统运行时软件也许会被意外地挂起,监视定时器是一个恢复系统的普遍方法.
比如,假设你的公司的新产品将进入太空.
无论你在使用之前做了多少测试,未发现的错误仍然有可能潜伏在软件里,并且其中的一个或者多个可能会使整个系统被挂起.
如果软件被挂起,你根本就不能够与它通信,你就不能远程地发出重启的命令.
因此你必须把一个自动恢复机制植入系统.
这就是监视定时器时钟出现的原因.
监视定时器"踢出"的实现看上去很像这一章里的LED闪烁程序.
不同的是其中不是切换指示灯而是监视定时器被重启.
第八章操作系统操作系统恐惧症(osophobia)[名]嵌入式系统开发人员普遍存在的恐惧症嵌入式编程的大多数问题都可以因为操作系统的引人而获益,这里的操作系统可以是你自己写的微内核的操作系统,或是功能完整的商用操作系统.
对于这些操作系统,你需要了解其最关键的性能,以及这些性能将来对应用程序的影响.
至少,你需要知道嵌入式操作系统从外面看起来应该是什么样的.
也许除了深入到一个小的操作系统里,没有其他办法可以让你对各种接口更加理解.

这就是我们在本章要做的事情.
历史和目的在早期的计算机中,没有操作系统一说,应用程序开发人员都要对处理器(CPU)和硬件进行彻头彻尾的控制.
实际上,第一个操作系统的诞生,就是为了提供一个虚拟的硬件平台,以方便程序员开发.
为了实现这个目标,操作系统只需要提供一些较为松散的函数、例程——就好像现在的软件库一样——以便于对硬件设备进行重置、读取状态、写入指令之类的操作.
现代的操作系统则在单处理器上加入了多任务机制,每个任务都是一个软件模块,可以是相互独立的.
嵌入式的软件经常是可以划分成小的互相独立的模块.
例如,第五章"接触硬件"讲到的打印共享设备就包含三个不同的软件任务:任务1:从计算机的串行口A接收数据任务2:从计算机的串行口B接收数据任务3:格式化数据并输送到计算机的并行口(打印机就连接在并行口)本章内容:z历史和目的zADEOSz实时特征z选择过程这些任务的划分提供了一个很关键的软件抽象概念,这使得嵌入式操作系统的设计和实现更加容易,源程序也更易于理解和维护.
通过把大的程序进行模块化划分,程序员可以集中精力克服系统开发过程中的关键问题.
坦言之,一个操作系统并不是嵌入式或其它计算机系统的必需的组件,它所能做的,也是像时用程序要实现的功能一样.
本书中的所有例子都说明了这一点.
应用程序执行起来,都是从main开始,然后进入系统调用、运行、结束.
这与系统中只有一个任务是一样的.
对于应用程序来说,仅仅是实现使LED进行闪烁,这就是操作系统的主要功用(屏蔽了很多复杂的操作).
如果你以前没作过对操作系统的研究,那么,在这里得提醒一下,操作系统是非常复杂的.
操作系统的厂商肯定是想使你相信,他们是唯一能生产出功能强大又易用的操作系统的科学家.
但是,我也要告诉你:这并不是根困难的.

实际上嵌入式操作系统要比桌面操作系统更容易编写,所需的模块和功能更为小巧、更易于实现.
一旦明确了要实现了功能,并有一定的实现技能,你将会发现,开发一个操作系统并不比开发嵌入式软件艰难多少.
嵌入式操作系统很小,因为它可以缺少很多桌面操作系统的功能.
例如,嵌入式操什系统很少有硬盘或图形界面,因此,嵌入式操作系统可以下需要文件系统和图形用户接口.
而且,一般来说,是单用户系统,所以多用户操作系统的安全特性也可以省去了.
上面所说的各种性能,都可以作为嵌入式操作系统的一部分,但不是必须的.
AOEOS下面要讲的是我自己开发的嵌入式操作系统,我称之为ADEOS,意即"得体的嵌入式操作系统(ADecentEmbededOperationSystem)".
ADEOS是一个操作系统,不算太好也不算太差.
实际上,一共不超过1000行程序、3/4的代码是用C++编写的,是与硬件平台无关的.
其余的代码是与硬件平台有关的,是用汇编语言开发的.
在后面的讨论中,我将详细讲解这些C++代码,并贯穿一些原理性的概念.
那些汇编代码,就不作详细讲解了,有兴趣的读者可以下载这些代码并自己进行深入研究.
如果你想用ADEOS(或是对它进行修改)作为嵌入式操作系统,那请便.
实际上我很高兴能有人使用它.
我花了很大的力气改善ADEOS的性能.
但是,我不能保证本章的代码对任问想了解嵌入式操作系统的人都很有用.
如果你决定自己使用它,那就应该自己努力进一步提高ADEOS的性能.
任务我们已经谈论过多任务的概念和操作系统如何在"同一时间执行多个程序"的想法.
那么,操作系统究竟是如何同时执行多个任务的呢实际上,任务并不是同时执行的,相反,是准并行的,它们只是轮流使用CPU.
就像很多人一起来读一本书,而每个人只有拿到书的时候才能读,但是他们可以传递着读.

操作系统的职责就是决定某一时刻执行哪一个任务,同时,它还要维护每个任务的状态信息,这个信息被称作任务的场景(context),就像书签的作用一样.
对于每个读者来说,都有必要拥有自己的书签.
书签的用户必须有办法识别书签(也就是说,书签必须有用户的名字),它必须知道上回他读到哪里了.
这就是读者的上下文环境信息.
在另外一个任务控制处理器之前,任务场景必须记录处理器的状态,通常包括指向下一条要执行的指令的指针、当前堆栈指针的位置、处理器的标志寄存器和通用寄存器的值.
在16位的80x86处理器上,这些寄存器是CS、IP、SS和SP、Flags、DS、ES、SI、DI、AX、BX、CX和DX等.
为了维护任务及其场景,操作系统必须对每个任务进行单独的维护.
用C语言编写的操作系统一段把这些信息保存在数据结构中,称作"任务控制块".
ADEOS是用C++编写的,能以对象的形式更好地维护这些数据信息,相关的C++对象为Task,它包含了操作系统所需的信息,如下所示:classTask{public:Task(void(*function)(),Priorityp,intstackSize);TaskIdid;Contextcontext;TaskStatestate;Prioritypriority;int*pStack;Task*pNext;void(*entryPoint)();private:staticTaskIdnextId;};这个类的许多数据成员变量的含义,要到我们详细讨论过操作系统之后才会显现.
id变量是一个单字节的整数(从0到255),用它来作为任务的标识.
换言之,id是书签的标识.
context是与处理器相关的数据结构,包含了上一个控制处理器的任务的处理器状态.
任务状态还记得我刚才说过,在一个时刻只能有一个任务使用处理器(CPU)吗那个正在使用处理器的任务被称作"运行(running)"的任务,没有别的什么任务能在同一时刻处于这一状态.
准备好运行的任务(目前还没有占用处理器)处于"就绪(ready)状态,而正在等待外部通知信号一来就运行的任务处于"等待(waiting)"状态.
图8-1显示了这三种状态之间的关系.
"就绪"和"运行"状态的转换是当操作系统选择了一个新任务来运行的时候发生的.
在运行之前,任务要处于"就绪"状态,然后处于就绪任务缓冲队列中的新任务被选择并且运行.
一旦任务运行起来,就会脱离"就绪"状态,除非操作系充强制它继续保持"就绪"或"等待"状态,或者,它还需要别的什么外部信息才能继续运行.
对于后一种情况,我们称作任务被阻塞了.
这时,该任务转入"等待"状态,操作系统会再挑选一个处于"就绪"状态的任务运行.
所以说,无论当前有多少任务处于"等待"或"就绪"状态,有且只有一个任务是处于"运行"状态的.
图8-1一个任务可能的状态下面就是ADEOS中如何定定义任务状态:enumTaskstate(Ready,Running,Waiting)值得注意的是,只有操作系统的"任务调度器"可以决定任务的状态.
最新创建的任务以及因等待外部信息而停止的任务.
被"任务调度器"放入"就绪"队列.
任务调度机制作为ADEOS的开发者(或是其他操作系统的开发者),你需要知道如何创建和使用任务.
就像别的抽象数据结构,Task类有自己的成员函数.
ADEOS的的任务接口比别的大多数操作系统要简单一些,因为它只是创建一个新的Task对象.
一旦创建,ADEOS任务继续在系统中存在,直到相关的函数返回.
当然,这也许永远不会发生(意即ADEOS任务也许永远不会结束),但是,如果一旦发生了,那么该任务就会被操作系统删除掉.
Task的构造函数如下所示.
调用者通过构造函数的参数分配一个函数,一个权限值,和一个可选择的新任务的堆栈大小.
第一个参数,fUnCtion,是一个指向C/C++语言或汇编语言的函数指针,该函数是要在新任务的上下文环境中运行的.
该函数不需要任何输人参数,也不返回任何结果.
第二个参数P,是一个单字节的整数(从1到255),代表了任务的权限级别,这个权限级别是与别的任务相对而言的,在任务调度器选择新的任务运行的时候会用到(p的值越大,表示权限越高).
TaskIdTask::nextId=0**Method:Task()**Description:Createanewtaskandinitializeitsstate.
**Notes:**Returns:*Task:Task(void(*function)(),Priorityp,intstackSize){stackSize/=sizeof(int);//Convertbytestowords.
enterCS();//CriticalSectionBegin////Initializethetask-specificdata.
//if=Task::nextId++;state=Ready;priority=p;entryPoint=function;pStack=newint[stackSize];pNext=NULL;////Initializetheprocessorcontext.
//contextInit(&context,run,this,pStack+stackSize);////Insertthetaskintothereadylist.
//os.
readyList.
insert(this);os.
schedule();//SchedulingPointexitCS();//CriticalSectionEnd}/*Task()*/注意这个例程的功能块被两个函数enterCS()和exitCS()的调用包围.
在这些调用之间的代码块叫作临界区(criticalsection).
临界区是一个程序必须完整执行的一部分.
也就是说,组成这一个部分的指令必须没有中断地按照顺序执行.

因为中断可能随时发生,保证不受到中断的唯一办法就是在执行关键区期间禁止中断.
因此在关键区的开始调用enterCS以保存中断的允许状态以及禁止进一步的中断.
在关键区尾部调用exitCS以恢复前面保存的中断调用.
我们会看到在下面每一个例程中都应用了同样的技巧.
在前面代码中,有几个在构造函数里调用的其他例程,但是在这里我没有空间列出.
它们是contextInit()和os.
readyList.
insert()例程.
例程contextInit()为任务建立了初始的设备场景.
这个例程必定是处理器专用的,因此是用汇编语言写的.
contextInit()有四个参数.
第一个是一个指向待初始比的设备场景数据结构指针.
第二个是一个指向启动函数的指针.
这是一个特殊的ADEOS函数,叫作run(),它被用来启动一个任务,并且如果以后相关的函数退出了,它被用来做其后的清理工作.
第三个参数是一个指向新任务对象的指针.
这个参数被传递给run(),因此相关的任务就能够被启动.
第四个和最后一个参数是指向新任务栈的指针.
另一个函数调用是os.
readyList.
insert().
这个函数把新任务加入到操作系统内部的就绪任务列表中.
readyList是一个TaskList类型的对象.
这个类是那些具有insert()和remove()两个方法的任务(按照优先级排序)的链表.
感兴趣的读者如果想知道这些函数是如何实现的就应该下载和研究其ADEOS的源代码.
你将在下面的讨论中了解到更多有关就绪列表的问题.
调度程序任何操作系统的核心和灵魂是它的调度程序(scheduler).
操作系统的这部分决定了在给定的时间用哪一个就绪任务有权使用处理器.
如果你曾经写过用于主流操作系统的软件,那么你可能会熟悉一些普通的调度算法:先进先出、短任务优先以及循环法(roundrobin).
这些是用于非嵌套式系统的简单调度算法.
先进先出(FIFO)调度描述了一个像DOS一样的操作系统,它不是一个多任务的操作系统.
相反地,每一个任务一直运行到它结束为止,并且直到那时下一个任务才被启动.
然而,在DOS中一个任务可以把自己挂起,因此为下一个任务释放出处理器.
那恰恰也是旧版本的Windows操作系统如何允许用户从应用程序接口(API)关于嵌入式系统最烦人的事情之一就是它们缺乏一个公共的API.
对于那些希望在基于不同操作系统的产品之间共享应用程序代码的公司来说,这是一个特别问题.
我曾经工作过的一个公司甚至在操作系统之上创建它们自己的层,只是为了把它们的应用程序从这些操作系统之间的差别里分用出来.

但是可以肯定,这不过是把创建另一个API的问题加入到总的问题中来.
每一个嵌入式的操作系统的基本功能大致一样.
每一个函数或者方法代表了操作系统可以向应用程序提供的一种服务.
但是没有那么多不同的可能的服务.
经常是这种情况:两个实现之间真正的不同只是在于函数和方法的名称.
这个问题已经持续了几十年了,并且在可见的日子里也不会解决.
然而,与此同时的Win32和POSIXAPI已经分别占据了PC机和Unix工作站.
因此,为什么没有出现一个嵌入式操作系统的类似标准呢不是因为缺乏尝试.
实际上,原始POSIX标准(IEEE1003.
1)的作者们也为实时操作系统创建了一个标准(IEEE1003.
4h).
一些很像Unix的嵌入式操作系统(让人想起了VxWorks和LynxOS)是符合这个标准API的.
然而,对于绝大部分的应用程序员来说,必须为每一个使用的操作系统学习一个新的API.
幸运的是,有了一线曙光,Java编程语言已经支持嵌入式的多任务和任务同步.
那意味着不管Java程序运行在什么样的操作系统中,创建和处理任务以及使它们同步的机理是一样的.
由于这个以及其他的一些原因,对于嵌入式程序员来说Java会是一个很好的语言.
我希望有一天大家需要一本关于嵌入式系统用Java语言编程的书,并且因此不再需要这样介绍了.
一个任务切换到其他任务的工作机理.
WindowsNT之前任何微软的操作系统都不包含真正的多任务.
短任务优先描述了一个近似的调度算法.
唯一的不同是,每一次运行的任务在完成或者挂起自己的时候,下一个被选择的任务是那个需要最小处理器完成时间的任务.
短任务优先在早期的主流系统中是普遍的,因为它能使最大多数用户满意.
(只有那些有最长任务的用户才会警告和抱怨.
)循环法是这三个调度算法中唯一的一个调度算法,它可以使运行中的任务被占先,也就是说,使它在运行的过程中遭到中断.
在这种情况下,每一个任务都运行一个预先决定的时间.
那个时间间隔过后,运行的程序被操作系统占先,然后下一个任务有机会运行.
被占先的任务直到其他所有的任务都有机会运行一轮之后才再次开始运行.
不幸的是,嵌入式操作系统不能利用任何这样简单的调度算法.
嵌入式操作系统(特别是实时操作系统)几乎总是需要一种方式来共享处理器,这种方式使得最重要的任务只要在它们需要的时候就可以获得处理器的控制.
因此,大部分的嵌入式操作系统利用一个支持占先的基于优先级的调度算法.
这是一个流行的说法,也就是说,在任何一个给定的时刻,正在使用处理器的任务保证是就绪任务中优先级最高的任务.
低优先级的任务必须等待直到高优先级任务使用它处理器之后才可以继续它们的工作.
"占先"这个词的意思就是如果一个高优先级的任务就绪之后任何任务都能被操作系统中断.
调度程序在有限的一组时间点检测这种情况,这些时间点叫作调度点.
当使用一个基于优先级的调度算法时,有一个备份的策略也是必要的.
这是在几个就绪任务具有相同优先级时使用的调度算法.
最普通的备份调度算法是循环调度法.
然而,为了简单起见,对于我的备份策略我只用了一个FIFO调度实现.
因此,ADEOS的使用者实尽可能的注意给每一个任务分配一个唯一的优先级.
这不应该成为一个问题,因为ADEOS支持的优先级与任务数(一个ADEOS最多支持255个)一样多.
在ADEOS中地调度程序是在一个叫作Sched地类中实现的.
classSched{public:Sched();voidstart();voidschedule();voidenterIsr();voidexitIsr();staticTask*pRunningTask;staticTaskListreadyList;enumSchedState(Uninitialized,Initialized,Started);private:staticSchedStatestate;staticTaskidleTask;staticintinterruptLevel;staticintbSchedule;};定义了这个类之后,在操作系统的一个模块中这种类型的对象就被初始化了.
那样,ADEOS的使用者只要连接sched.
obj以包含一个调度的实例.
这个例子叫作OS,它声明如下:externSchedos;对于这个全局变量可以在这个应用程序的任何一个部分引用.
但是,你很快就会明白在每个应用中只有一个这样的引用是必要的.
调度点简单地说,调度点是一组导致调用调度程序的操作系统事件.
我们已经遇到了两个这样的事件:任务创建和任务的删除.
每一个事件期间,方法os.
schedule被调用以选择下一个要运行的任务.
如果目前运行的任务仍旧在就绪的任务中具有最高的优先级,那么它将被允许继续使用处理器.
否则,接下来,最高优先级的任务将被执行.
当然,在任务删除的情况下,选择的总是一个新任务:根据它不再存在的事实,所以目前运行的任务不再处于就绪的状态.
一个第三方的调度点叫作时钟节拍.
时钟节拍是一个由时钟中断处触发的周期性事件.
时钟节拍提供了一个机会以唤醒那些等待软件时钟到期的任务.
这和我们在前而章节中看到的时钟节拍几乎是一样的.
实际上,对于软件时钟的支持是嵌入式系统的一个普遍特性.
在一个时钟节拍期间,操作系统使每一个活动的软件时钟减值并且检测它们.
当一个时钟到期了,所有等待它结束的任务从等待状态变为就绪状态.
然后,调度程序被调用以确定新唤醒的任务和此时钟中断之前运行的任务相比是否具有更高的优先级.
在ADEOS中时钟节拍例程几乎与第七章"外围设备"中的那个例程是一样的.
实际上,我们仍旧使用相同的Timer类.
只有这个类的实现被轻微地改变了.
这些变化是为了考虑到多个任务可能等待同一个软件时钟的事实.
此外,所有对于disable()和enable()的调用都被enterCS()和exitCS()替换了,并且时钟节拍的长度从1ms增加到10ms.
就绪列表调度程序使用一个叫作就绪列表的数据结构来追踪处于就绪状态的任务.

在ADFOS中,就绪列表被实现成为一个普通的按照优先级排序的链表.
因此,这个列表的头总是具有最高优先级的就绪任务.
跟踪一个调度程序的调用,这和追踪当前运行的任务一样.
实际上,唯一不是这种情况的时候是在重调度过程中.
图8-2为操作系统运行时的就绪列表.
图8-2就绪列表像这样排过序的链表的主要好处在于调度程序容易通过它选择下一个要运行的任务.
(它总是在最顶端.
)不幸的是,在查询时间和插入时间之间需要权衡.
由于数据成员readyList总是直接指向就绪的任务,查询时间被最小化了.
然而,每次一个新的任务改变到就绪状态时,在insert()方法中的代码必须一直搜寻就绪列表,直到它发现一个任务,这个任务比正在插入的任务的优先级更低.
新就绪的任务被插在那个任务的前面.
结果,插入时间正比于就绪列表中任务的平均数量.
空闲任务如果没有任务处于就绪状态,空闲任务将被执行.
空闲任务看起来与其他操作系统中的一样.
它只是一个不作任何事情的循环.
在ADEOS中,空闲任务对于应用开发者来说是完全隐藏的.
然而,它确实有一个任务的ID和优先级(两者都是零).
空闲任务总是被认为处于就绪的状态(当它不运行的时候),并且由于它的优先级最低,所以他总是出现在就绪列表的末尾.
那样,调度程序在没有任何其他任务处于就绪状态时会自动的找到它.
其他的任务则被认为是用户任务,以区分空闲任务.
调度程序因为我利用一个排序的链表来保存就绪列表,所以调度程序容易实现.
它只是检测正在运行的任务与最高优先级的就绪任务是否是一个任务和同样的优先级.
如果是,调度任务完成.
否则,它会启动一个从前者到后者的设备场景的切换.
这里是用C++实现的代码.
**Method:schedule()**Description:Selectanewtasktoberun.
**Notes:IfthisroutineiscalledfromwithinanISR,the*schedulewillbepostponeduntilthenestinglevel*returnstozero.
**Thecallerisresponsiblefordisablinginterrupts.
**Returns:Nonedefined.
*voidSched::schedule(void){Task*pOldTask;Task*pNewTask;if(state!
=Started)return;////Postponereschedulinguntilallinterruptsarecompleted.
//if(interruptLevel!
=0){bSchedule=1;return;}////Ifthereisahigher-priorityreadytask,switchtoit.
//if(pRunningTask!
=readyList.
pTop){pOldTask=pRunningTask;pNewTask=readyList.
pTop;pNewTask->state=Running;pRunningTask=pNewTask;if(pOldTask==NULL){contextSwitch(NULL,&pNewTask->context);}else{pOldTask->state=Ready;contextSwitch(&pOldTask->context,&pNewTask->context);}}}/*schedule()*/正如你从代码中看到的一样,有两种情况调度程序不启动设备场景的切换.

第一种情况是如果多任务没有被启用的时候.
这是必要的,因为有时应用程序员想要在调度程序真正启动之前创建其任务的一些或者全部.
在那种情况下,应用程序的main例程会类似于下面这段程序.
每次一个Task对象被创建的时候,调度程序就被调用(注1).
然而,因为调度程序为了检查多任务是否已被启动而检测变量state的值,所以直到start()被调用以后才会发生设备场景的转换.
#include"adeos.
h"voidtaskAfunction(void);voidtaskBfunction(void);/**Createtwotasks,eachwithitsownuniquefunctionandpriority.
*/TasktaskA(taskAfunction,150,256);TasktaskB(taskBfunction,200,256);**Method:main()注1:记住,任务的创建是我们调度点其中的一个.
如果调度程序已经启动了,新任务仍然可能是最高优先级的就绪任务.
*Description:Thisiswhatanapplicationprogrammightlooklike*ifADEOSwereusedastheoperatingsystem.
This*functionisresponsibleforstartingtheoperatingsystemonly.
**Notes:Anycodeplacedafterthecalltoos.
start()willnever*beexecuted.
Thisisbecausemain()isnotatask,*soitdoesnotgetachancetorunoncetheschedulerisstarted.
**Returns:Thisfunctionwillneverreturn!
*voidmain(void){os.
start();//Thispointwillneverbereached.
}/*main()*/因为这是一段重要的代码,所以让我重新讲解你正在看的东西.
这是一个你可能作为ADEOS用户写出的应用代码的例子.
你在开始加入头文件adeos.
h和声明你的任务.
在你声明了你的任务和调用os.
start之后,任务函数taskAfunction和taskBfunction开始执行(以伪并行的方式).
当然,taskB在这两个任务中具有最高的优先级(200),因此它将先运行.
然而,只要它由于任何原因放弃对处理器的控制,其他的任务就有机会运行.
另一种ADEOS调度程序不进行设备场景切换的情况是在中断进行期间.
操作系统跟踪目前运行的中断服务例程的嵌套级别,井且只有在嵌套级是零的时候才允许设备场景切换.
如果调度程序是从一个ISR调用的(就像它在时钟节拍期间一样),bSchedule标志被置位,表明只要最外层的中断处理程序退出,调度程序就应该再次被调用.
这个被延迟的调度加快了整个系统中断响应的时间.
设备场景切换从一个任务转变到另一个任务的实际过程叫作设备场景切换.
因为设备场景是处理器专用的,实现设备场景切换的实现也是这样.
那意味着它总是要用汇编来写.
与其向你展示我在ADEOS中使用的80x86专用的汇编代码,不如我用一种类C的伪代码来展示设备场景切换例程.
voidcontextSwitch(PContextpOldContext,PContextpNewContext){if(saveContext(pOldContext)){////RestorenewcontextonlyonanonzeroexitfromsaveContext().
//restoreContext(pNewContext);//Thislineisneverexecuted!
}//Instead,therestoredtaskcontinuestoexecuteatthispoint.
}例程contextSwitch()实际上是被调度程序凋用,而调度程序又在那此终止中断的系统调用中被调用,因此它不一定在这里终止中断.
此外,由于调用调度程序的操作系统调用是用高级语言写的,所以大部分运行任务的寄存器已经被保存到它自己当地的栈中了.
这减少了例程saveContext()和restoreContext()需要做的工作.
它们只需要关心指令指针,栈指针以及标志位的保存.
例程contextSwitch()的实际行为是很难仅仅通过看前面的代码来理解的.
大部分的软件开发者以连续的方式思考问题,认为每一行代码会紧接着上一条代码破执行.
然而,这个代码实际为并行地执行了两次.
当一个任务(新任务)转变到运行状态,另一个(旧任务)必须同时返回到就绪状态.
想一下新任务当它在restoreContext()代码中被恢复的时候就会明白.
无论新任务以前做什么,它在saveContext代码里总是醒着的——因为这就是它的指令存放的地方.
新任务如何知道它是否是第一次(也就是,在准备休眠的过程)或者是第二次(醒来的过程)从saveContext()中出来的呢它确实需要知道这个差别,因此我不得不用一种有点隐蔽的方法来实现saveContext().
例程saveContext()不是保存了准确的目前的指令指针.
实际上是保存了一些指令前面的地址.
那样,当保存的设备场景恢复的时候,程序从saveContext中另一个下同的点继续.
这也使得saveContext可能返回不同的值:当任务要休眠的时候为非零,当任务唤起的时候为零.
例程contextSwitch()利用这个返回的值来决定是否调用restoreContext().
如果不进行这个检测,那么与这个新任务相关的代码永远不会执行.
我知道这可能是一个复杂的事件序列,因此我在图8-3中说明了整个的过程.
任务的同步尽管我们常常把多任务操作系统中的任务看作是完全独立的实体来谈论,但是这个刻画不完全准确.
所有的任务是在一起工作以解决一个大问题,必须偶尔和另外一个住务通信使得它们的活动同步.
比如,在一个打印机共享的设备中.
打印机任务直到其中一个计算机任务向它提供了新的数据之前是没有任何工作可见的.
因此,打印机和计算机任务必须互相通信以协调它们获得共享数据.
做到这一点的一种办法是使用一种叫作互斥体(mutex)的数据结构.
图8-3设备场景切换很多的操作系统提供互斥体来协助实现任务的同步.
然而,它井不是唯一可以实现任务间同步的机制.
其他的机制有:信号灯、消息队列以及监视器.
可是如果你有一种这样的数据结构,就可以实现其他的任何一种.
实际上,互斥体本身就是一种特殊的信号灯,称为二元信号灯,或者是互斥信号灯.
你可以把一个互斥体想成是一个能感知多任务的二元标志.
因此,与一个具体互斥体相关的含义是由软件设计者来选择,被每一个使用它的任务来理解的.

比如,由打印机和计算机共享的数据缓存也许需要一个和它们相关的互斥体.

当这个二元标志位被设置的时候,共享数据缓存被假定是其中一个任务在使用.

所以在读写缓存中的数据之前,其他的任务必须等待,直到标志位被清除(然后又被它们自己设置).
我们说互斥体是感知多任务的,这是因为设置和清除二元标志位都是最基本的操作.
也就是说,这些操作不可能被中断.
一个任务可以安全地改变互斥体的状态而不会在改变之中冒设备场景切换的危险.
如果设备场景切换发生,二元标志位会处于一种不可预知的状态并且会导致任务之间的死锁.
互斥体设置和清除操作的不可分割性得到了操作系统的加强.
在读取和修改二元标志使之前操作系统禁止中断.
ADEOS包含了一个Mutex类.
利用这个类,应用软件可以创建和销毁互斥体,等待一个互斥体被清除,然后再设置它,或者清除一个前面设置的互斥体.

后面的两个操作被分别称为获取和释放一个互斤体:这里是Mutex类的定义:classMutex{public:Mutex();voidtake(void);voidrelease(void);private:TaskListwaitingList;enum{Available,Held}state;}创建一个新的Mutex类的过程简单.
每一次一个新的互斥体对象被启动的时候下面的构造函数会自动执行.
**Method:Mutex()**Description:Createanewmutex.
**Notes:**Returns:*Mutex::Mutex(){enterCS(CriticalSectionBeginstate=Available;waitingList.
pTop=NULL;exitCS(CriticalSectionEnd}/*Mutex()*/所有的互斥体是在Available状态下创建的,并且和一个开始为空的等待任务链表相关.
当然,一旦你创建了一个互斥体,就有必要通过某种方法改变它的状态,因此我们要讨论的下一个方法是take().
一般来说,这个例程会被一个任务在它读写一个共享资源之前调用.
当对take()的调用返回时,调用它的任务时于那个资源独占式的存取将受到操作系统的保证.
这个例程的代码如下:**Method:take()**Description:Waitforamutextobecomeavailable,thentakeit.
**Notes:**Returns:Nonedefined.
*voidMutex::take(void){Task*pCallingTask;enterCS(CriticalSectionBeginif(state==Available){////Themutexisavailable,Simplytakeitandreturn.
//state=Held;waitingList.
pTop=NULL;}else{////Themutexistaken.
Addthecallingtasktothewaitinglist.
//pCallingTask=os.
pRunningTask;pCallingTask->state=Waiting;os.
readyList.
remove(pCallingTask);waitingList.
insert(pCallingTask);os.
schedule();//SchedulingPoint//Whenthemutexisreleased,thecallerbeginsexecutinghere.
}exitCS(CriticalSectionEnd}/*take()*/关于take方法最巧妙的是:如果互斥体被另一个任务占据(也就是二元标志位已经被设置),那么直到互斥体被那个任务释放之前,调用take()的任务会被挂起.
这就好像是告诉你的配偶你要打个盹,在晚餐准备好了之后让他或者她叫醒你.
甚至多个任务等待一个互斥体也是可能的.
实际上,与每一个互斥体相关的等待列表是按照优先级排序的.
因此最高优先级的等待任务总是第一个被唤醒.
接下来的方法是用来释放一个排斥体.
尽管这个方法可能被任问其他的任务调用,但是人们希望只有先前调用过take方法的任务可以请求它.
不像take方法,这个例程不会阻塞.
然而释放互斥体的一个可能结果是唤起一个优先级更高的任务.
那种情况下,释放排斥体的任务会立刻被迫(被调度程序)放弃对处理器的控制,支持高优先级的任务.
**Method:release()**Description:Releaseamutexthatisheldbythecallingtask.
**Notes:**Returns:Nonedefined.
*voidMutex::release(void){Task*pWaitingTask;enterCS(CriticalSectionBeginsif(state==Held){pWaitingTask==waitingList.
pTop;if(pWaitingTask!
=NULL){////Wakethefirsttaskonthewaitinglist.
//waitingList.
pTop=pWaitingTask->pNext;pWaitingTask->state=Ready;os.
readyList.
insert(pWaitingTask);os.
schedule();//SchedulingPoint}else{state=Available;}}exitCS(CriticalSectionEnd}/*release()*/临界区互斥体的使用主要是为了保护共享资源.
共享资源包括全局变量,存储缓存或者是被多任务存取的设备寄存器.
对于那些在某一时刻只有一个任务可以访问的资源,互斥体可以用来限制它们的存取.
它就好像控制十字路口交通的指示灯.
记住在多任务环境中,你通常不知道运行时任务会按照什么顺序执行.

当一个任务突然被一个高优先级的任务中断的时候,它可能在向一个存储缓存中写入一些数据.
如果高优先级的任务也要修改同一块存储区域那么糟糕的事情就会发生了.
至少,一些低优先级任务的数据会被覆盖.
访问共享资源的代码段包含临界区.
我们已经在操作系统里面看到了类似的东西.
在那里,我们只是在临界区内禁止中断.
但是任务不能够(这是明智的)禁止中断.
如果它们被允许这样做,其他的任务——甚至是那些不共享相同资源的高优先级任务——不能够在此期间执行.
因此我们希望并且也需要一个在任务内部不禁止中断又能够保护临界区的机制.
互斥体提供了这种机制.

死锁和优先级倒置互斥体是一个实现各任务访问共享资源时同步的有力工具.
但是,它们也不是没有问题.
需要当心的两个最重要的问题是死锁和优先级倒置.
一旦任务与资源存在循环依赖的时候,死锁就会发生.
最简单的例子是有两个任务,它们都需要两个互斥体:A和B.
如果一个任务获得了A在等待B,同时另一个任务获得了B并且在等待A,那么两个任务都在等待一个永远不可能发生的事件.
当一个高优先级的任务被阻塞,等待一个被低优先级任务保留的互斥体的时候,优先级倒置就发生了.
这可能不像一个大问题——毕竟,互斥体正在仲裁对于共享资源的访问——因为高优先级任务知道有时低优先级任务会利用它共享的资源.
然而,考虑一下,如果出现一个优先级介于两者之间的第三方任务会发生什么事情.
这种情况在图8-4中做了说明.
这里有三个任务:高级、中级以及低级.
低级的任务首先就绪(由凸沿表示),那以后很快就获得互斥体.
现在当高级任务就绪,它必须被阻塞(用阴影区表示)直到低优先级的任务处理定它们共享的资源.
问题在于中级任务,由于它不需要对那个资源访问,所以抢先于低优先级的任务执行,于是它就会耽误高优先级任务对于处理器的使用.

对于这个问题人们已经提出了很多解决方案.
其中最普遍的方案叫作"优先级的继承".
这个解决方案在高优先级任务等待互斥体的时候,就把低优先级的任务的优先级升到那个高优先级任务的优先级.
一些操作系统在它们的互斥体实现中加入了这个修正,但是大部分都没有.
图8-4优先级倒置的例子你现在已经学会了一个简单嵌入式操作系统要学习的每一样东西.
它的基本元素有:调度程序和调度点、设备场景切换例程、任务的定义以及任务间通信的机制.
每一个有用的嵌入式操作系统都会具有这些相同的元素.
然而,你不必总是搞清楚它们是如问实现的.
你通常只要把操作系统看待成一个应用程序员可以依赖的黑盒子.
你只是为每一个任务编写代码,在必要的时候对操作系统做调用.
操作系统会保证这些任务相对于其他任务来说在合适的时间运行.

实时特征工程师经常使用"实时"这个术语来描述这样一个计算问题:延迟的响应和错误的响应是同样糟糕的.
这些问题被说成是具有最终期限,而嵌入式系统常常在这种限制之下操作.
比如,如果控制作解锁闸的嵌入式软件漏掉了一个最终期限,那么你可能会发现自己出了事故(你甚至会被杀死!
).
因此对于实时嵌入式系统的设计者来说,了解每一件他们所能了解的有关硬件和软件的行为及性能的事情,是极其重要的.
这一部分里我们要讨论实时使作系统特征,这些特征是实时系统的共同成分.
实时系统的设计者花费大量的时间关心最糟糕情况下的系统性能.
他们必须不断地回答自己如下的问题:最坏情况下,在人压迫闸板的操作和一个中断信号到达处理器之间要多少时间最坏情况的中断潜伏期即:在中断到达与启动相关中断服务例程之间的时间,是多少在最坏的情况下软件对于触发刹车机构的响应时间是多少平均或者预期情况的简单分析在这种系统中是不够的.

大部分商用嵌入式操作系统被设计为可以运行在实时系统之中.
在理想的情况下,这意味着它们在最坏情况下的性能都被很好地了解并且记录在案.
要得到与众不同的"实时操作系统"(RTOS)的头衔,一个操作系统应该是确定性的,并且保证最坏情况的中断等待时间以及设备场景切换的时间.
如果给出这些特征以及你的系统中任务和中断的相对优先级,就可能分析软件在最坏情况下的表现了.
如果每一个系统调用在最坏情况下的执行时间都是可以计算的,那么这个操作系统就被说成是确定性的(deterministic).
一个对待其RTOS实时行为采取慎重态度的操作系统供应商通常会发布一个数据表单,该表单提供了每一个系统调用要求的最小、平均及最大数目的时钟周期.
这些数字对于不同的处理器可能不同.
但是如果算法在一个处理器上是确定性的,那么在其他任何的处理器都是如此(实际的时间可能会有差别),这样期望是合理的.
中断等待时间是从一个中断信号到达处理器开始到启动相关的中断服务例程为止所经历的时间长度.
当一个中断发生的时候,处理器在执行ISR时必须采取几个步骤.
首先,处理器必须完成现在正在执行的指令.
这大概花费不到一个时钟周期,但是一些复杂的指令需要花费比这更多的时间;下一步,必须识别中断类型.
这是由处理器硬件来做的,不会减慢或者挂起正在运行的任务;最后,只要中断是允许的,那么和中断相关的ISR就会被启动.
当然,如果中断在操作系统中被禁用.
那么最差情况的中断等待时间会增加一个它们被关闭的时间.
但是正如我们刚才看到的那样,有很多地方中断是被禁止的.
这些就是我们前面讨论的关键区,并且没有其它的方法来保护它们.

每一个操作系统都会在一段不同的时间内禁止中断,因此时于你来说了解你的系统的要求是很重要的.
某一个实时过程可能要求保证中断响应时间为1μs,而另一个可能只需要100μs.
操作系统的第三个实时特征是执行一个设备场景切换所需的时间.
这是重要的,囚为它代表了整个系统的开销.
比如,想一下任间任务在它阻塞之前的平均执行时间为100us,但是设备场景切换的时间也是100us.
这种情况下,处理器的一半的时间完全花费在设备场景切换例程上了.
此外,没有不可思议的数字,并且实际的时间通常是机处理器而定的,因为它们依赖于必须保留的寄存器的数目以及它们的位置.
所以一定要从你想要使用的操作系统的供应商那里得到这些数据.
那样,就不会有任何最后关头的惊奇了.
选择过程尽管我前面说过写自己的操作系统是如何的容易,但是如果你可以负担得起的话,我还是强烈地建议你买一个商业的操作系统.
我再说一遍:我强烈建议购买一个商业的操作系统.
而不是写一个自己的操作系统.
我知道几个不错的操作系统,只要几千美金就可以得到.
考虑到写一个操作系统所花的时间,几乎从任何角度来说那都是一个不错的买卖.
实际上,很多种操作系统都很经济井且可以满足大部分工程的需要.
在这一部分,我们要讨论选择一个最适合你工程需要的商业操作系统的过程.
商业操作系统形成了一个功能、性能以及价格的统一体.
那些在低端范围内的操作系统只是提供一个基本的调度程序以及一些其他的系统调用.
这些操作系统通常并不昂贵,提供你可以改动的源代码,并且不需要支付版税.
AcceleratedTechnology公司的Nucleus和柯达公司的AMX都属于这一类型(注2),和任何的DOS的嵌入式版本一样.
范围另一端的操作系统一般会包括很多有用的功能而不仅仅是调度程序.

对于实时性能它们可能会有更强(好的)的保证.
然而,这些操作系统可能会很贵,启动成本在$10000和$50000之间,装载人ROM内的每一个拷贝都要支付版税.
然而,这个价格经常包括免费的技术支持和培训以及一系列配套的开发工具.
风河(WindRiver)系统公司的VxWorks、Integrated系统公司的pSOS以及Microtec公司的VRTX都属于这一类.
这些是市场上最流行的三个实时操作系统.
在这两个极端之间的那些操作系统除了基本的调度程序之外还有一些功能,对于实时性能它们提供了一些合理的保证.
前期成本和版税也是合理的,这些操作系统通常不包括源代码,并且技术支持可能需要另外收费.
在前面没有提到的大部分商业操作系统就是属于这一类.
有如此众多的操作系统和特性要选择,很难决定哪一个最适合作的工程.
试着把你的处理器,实时性能以及预算要求放在首位.
这些都是你不可改变的标准,因此你可以用它们把范围缩小到十几个或者更少的产品.
然后和所有这些注2;请不要抱怨.
我不是贬低这些操作系统.
事实上,从我的经验,、我高度推荐它们作为优质、低成本的商业解决方案.
操作系统的厂商联系以获得更加详细的技术信息.
在这一点上,很多人基于对现有的交叉编译器、调试工具以及其他开发工具的兼容性作出他们的选择.
但是决定哪些附加的特性对于你的工程最重要实际上是你的责任.
无论你决定买哪一个,其基本的内核与本章描述的这个操作系统大致是一样的.
差别可能是在于支持的处理器、最大及最小的存储要求、外接式附加软件模炔(网络协议栈、设备驱动以及Flash文件系统是常见的例子)以及对于第三方开发工具的兼容性.
记住选择一个商业操作系统最好的理由是,使用在分测试过的东西是有益的,因此商业操作系统比你内部开发的(或者从一本书上免费得到的)内核要更可靠.
因此你要从操作系统销售商那里找的最重要的就是经验.
如果你的系统要求实时性能,你就一定要采用一个曾经在很多实时系统中成功应用的操作系统.
比如,查出NASA为它最近的任务使用了哪个操作系统.
我很乐意打赌它是一个不错的选择.
第九章合成一个整体一个高水平的技术无异于一个奇迹.
——ArthurC.
Clarke在这一章里,我试图把到目前为止所有我们讨论过的单元合在一起,使之成为一个完整的嵌入式的应用程序.
在这里我没有把很多新的素材加入到讨论中,因此本章主要是描述其中给出的代码.
我的目的是描述这个应用程序的结构和它的源代码,通过这种方式使你对它不再感到神奇.
完成这一章以后,你应该对于示例程序有一个完整的理解,并且有能力开发自己的嵌入式应用程序.

应用程序的概述我们将要讨论的这个应用程序不比其他大部分编程书籍中找到的"Hello,World"例子更复杂.
它是对于嵌入式软件开发的一个实证,因此这个例子是出现在书的结尾而不是开始.
我们不得不逐渐地建立我们的道路通向大部分书籍甚至是高级语言编译器认为是理所当然的计算平台.
一旦你能写"Hello,World"程序,你的嵌入式平台就开始着上去很像任何其他编程环境.
但是,嵌入式软件开发过程中最困难的部分——使自己熟悉硬件,为它建立一个软件开发的过程,连接到具体的硬件设备——还在后面呢.

最后,你能够把你的力量集中于算法和用户界面,这是由你要开发的产品来确定的.
很多情况下,这些程序的高级方面可以在其他的计算机平台上开发,和我们一直在讨论的低级的嵌入式软件开发同时进行,并且只要把高级部分导入本章内容:z应用程序的概述z闪烁指示灯z打印"Hello,World!
"z利用串行端口zZilog85230串行端口控制器嵌入式系统一次,两者就都完成了.
图9-1包含了一个"Hello,World!
"应用程序的高级的示意图.
这个应用程序包括三个设备驱动程序,ADEOS操作系统和两个ADEOS任务.
第一个任务以每秒10Hz的速度切换Arcom板上的红色指示灯.
第二个每隔10秒钟向主机或是连接到位子串口上的哑终端发送字符串"Hello,WOrld!
".
图9-1"HelloWorld!
"应用程序这两个任务之外,图中还有三个设备的驱动程序.
这些驱动程序分别控制着Arcom板子的指示灯、时钟以及串行端口.
虽然通常把设备驱动画在操作系统的下面,但是我把它们三个和操作系统放在同一个级别,是为了着重说明它们事实上依赖于ADEOS比ADEOS依赖于它们更多.
实际上,ADEOS嵌入式操作系统甚至不知道(或者说下关心)这些设备驱动是否存在于系统之中.
这是嵌入式操作系统中设备驱动程序和其他硬件专用软件的共性.
程序main()的实现如下所示.
这段代码简单地创造厂两个任务,启动了操作系统的日程表.
在这样一个高的级别上,代码的含义是不言而喻的.
事实上、我们已经在上一章中讨论了类似的代码.
#include"adeos.
h"voidflashRed(void);voidhelloWorld(void);/**Createthetwotasks.
*/TasktaskA(flashRed,150,512);TasktaskB(helloWorld,200,512);**Function:main()**Description:ThisfunctionisresponsibleforstartingtheADEOSscheduleronly.
**Notes:**Returns:Thisfunctionwillneverreturn!
*voidmain(void){os.
start();//Thispointwillneverbereached.
}/*main()*/闪烁指示灯正如我早先说的.
这个应用程序所做的两件事之一是使红色指示灯闪烁.
这可以通过下面的代码来做到.
这里函数flashRed()作为一个任务来执行.
然而,如果忽略这一点以及新的函数名,那么这段代码和第七章"外围设备"中我们研究过的LED闪烁函数几乎是一样的.
在这一级上的唯一差别就是新的频率(10HZ)和指示灯的颜色(红色).
#include"led.
h"#include"timer.
h"**Function:flashRed()**Description:BlinktheredLEDtentimesasecond.
**Notes:Thisouterloopishardware-independent,However,it*callsthehardware-dependentfunctiontoggleLed().
**Returns:Thisroutinecontainsaninfiniteloop.
*voidflashRed(void){Timertimer;timer.
start(50,Periodic);//Startaperiodic50mstimer.
while(1){toggleLed(LED_RED);//ToggletheredLED.
timer.
waitfor();//Waitforthetimertoexpire.
}}/*flashRed()*/LED闪烁程序主要的改变在这段代码中是看不见的.
这些主要的变化是对toggleLed()函数和Timer类进行了修改以兼容多任务的环境.
toggleLed()函数就是我现在称之为指示灯驱动的东西.
一旦你开始这样考虑这个问题,也许会考虑把驱动重写成一个C++类,并已加入新的方法以明确地设置和清除指示灯,这个方法也是讲得通的.
但是,采用和第七章一样的实现方法,并且仅使用一个互斥体来保护P2LTCH寄存器不被一个以上的任务同时访问,就已经足够了(注1).
这里是修改后的代码:#include"i8018xEB.
h"#include"adeos.
h"staticMutexgLedMutex;**Function:toggleLed()**Description:TogglethestateofoneorbothLEDs.
**Notes:Thisversionisreadyformultitasking.
**Returns:Nonedefined.
*voidtoggleLed(unsignedcharledMask){gLedMutex.
take();//ReadP2LTCH,modifyitsvalue,andwritetheresult.
//gProcessor.
pPCB->ioPort[1].
latch^=ledMask;gLedMutex.
release();}/*toggleLed()*/注1:在早先的那个toggleLed()函数中存在竞争的情况.
为了理解这一点,返回去查看这段代码,并且假想两个任务在共享指示灯,第一个任务恰好调用了那个切换红色指示灯的函数,突然之间,第一个任务被第二个任务占先,此时,在toggleLed()函数中,指示灯的状态都被读入并存入一个处理器的寄存器.
现在第二个任务导致两个指示灯的状态都被再次读入并且存入另一个处理器的寄存器,然后两个指示灯的状态都被修改以改变绿色指示灯的状态,并且导致结果写出到P2LTCH寄存器.
当中断的任务重新启动时,它已经有一个指示灯状态的拷贝.
但是这个拷贝不再准确了.
当发生这个变化后,指示灯变成红灯,并且比新的指示灯状态写到P2LTCH寄存器.
这样,第二个任务的改变将会被撤销.
加入一个互斥体可以消除这个潜在的危险.
第七章中的时钟驱动程序在应用于一个多任务的环境之前,必须对它做类似的改动.
然而这时不存在竞争的情况(注2).
我们需要利用一个互斥体来消除waitfor方法中的轮询.
通过把一个互斥体和每一个软件时钟关联,我们可以使任何一个在等待时钟的任务休眠,因此,释放了处理器以执行就绪的低优先级的任务.
当等待的时钟到期了,休眠的任务会被操作系统唤起.
为此,一个指向互斥体的指针,叫做pMutex,被加入到类的定义中:classTimer{public:Timer();~Timer();intstart(unsignedintnMilliseconds,TimerType=OneShot);intwaitfor();voidcancel();TimerStatestate;TimerTypetype;unsignedintlength;Mutex*pMutex;unsignedintcount;Timer*pNext;private:staticvoidinterruptInterrupt();};每次构造函数创建软件时钟的时候,这个指针被初始化.
此后,无论何时一个时钟对象被启动,它的互斥体可以像下面这样获得:注2:记得时钟硬件只初始化一次(在第一次构造函数请求的时候),并且此后时钟专用寄存器只能由一个函数(即中断服务例程)来读写.
**Function:start()**Description:Startasoftwaretimer,basedonthetickfromthe*underlyinghardwaretimer.
**Notes:Thisversionisreadyformultitasking.
**Returns:0onsuccess,-1ifthetimerisalreadyinuse.
*intTimer::start(unsignedintnMilliseconds,TimerTypetimerType){if(state!
=Idle){return(-1);}////Takethemutex.
Itwillbereleasedwhenthetimerexpires.
//pMutex->take();////Initializethesoftwaretimer.
//state=Active;type=timerType;length=nMilliseconds/MS_PER_TICK;////Addthistimertotheactivetimerlist.
//timerList.
insert(this);return(0);}/*start()*/通过在时钟启动的时候获得互斥体,我们可以保证在同一个互斥体释放之前没有其他任务(甚至是启动时钟的任务)能够再获得它.
并且这在时钟自然到期(中断服务程序)或是人为取消(通过取消的办法)之前是不会发生的.
所以在waitfor()中的轮询可以由pMutex->take()来替换如下:**Function:waitfor()**Description:Waitforthesoftwaretimertofinish.
**Notes:Thisversionisreadyformultitasking.
**Returns:0onsuccess,-1ifthetimerisnotrunning.
*intTimer::waitfor(){if(state!
=Active){return(-1);}////Waitforthetimertoexpire.
//pMutex->take();////Restartoridlethetimer,dependingonitstype.
//if(type==Periodic){state==Active;timerList.
insert(this);}else{pMutex->release();state=Idle;}return(0);}/*waitfor()*/当时钟最后到期时,中断服务程序将会释放互斥体,调用的任务会在waitfor()中被唤起.
唤起的过程中,互斥体已经为下一个运行的时钟获得.
互斥体只有当时钟是OneShot类型时才会被释放,因此,是不会自动重启的.
打印"Hello,World!
"应用程序的其他部分是一个定时向串行端口打印文本字符串"Hello,World"的任务.
此外,用时钟的驱动程序来创建这个周期.
然而,这个任务还依赖于串行端口的驱动程序,这是我们以前没有见过的.
串行端口驱动内容将在本章的最后两部分进行描述.
但是在这里先说明这个用到它的任务.
为了理解这个任务,你只需要知道串行端口是一个C++类,并且利用puts()方法可以从那个端口打印字符串.
#include"timer.
h"#include"serial.
h"**Function:helloWorld()**Description:Sendatextmessagetotheserialportperiodically.
**Notes:Thisouterloopishardware-independent.
**Returns:Thisroutinecontainsaninfiniteloop.
*voidhelloWorld(void){Timertimer;SerialPortserial(PORTA,19200L);timer.
start(10000,Periodic);//Startaperiodic10stimer.
while(1){serial.
puts("Hello,World!
");//Outputasimpletextmessage.
timer.
waitfor();//Waitforthetimertoexpire.
}}/*helloWorld()*/尽管周期具有不同的长度,但是这个任务的总体结构和flashRed()函数的结构是一样的.
因此,剩下来我们要讨论的唯一的事情就是串行端口的组成.
我们从描述通用串行端口的接口开始,到Arcom板子上可以找到的专用串行控制器结束.
利用串行端口在应用程序级,一个串行端口只是一个双向的数据通道.
这个通道的每一端通常端接一个叫做串行通信控制器(SCC)的硬件设备.
在SCC中的每一个串行端口——每一个控制器至少有两个串行端口——一边连接到的嵌入式处理器,另一边连接到电缆(或者是某一设备的连接器).
电缆的另一端通常是一个具有自己内部的串行通信控制器的主机(或者是一些其他的嵌入式系统).
当然,串口的实际作用是依赖于应用的.
但是总的概念是这样的:在两个智能系统之间或者是这样一种设备和人类操作之间交换数据流.
一般来说,通过串行端口可以传送和接收的最小的数据单元是8位的字符.
因此,在传输之前,二进制数据流需要被识别成字节.
这个限制类似于C的stdio库的限制,因此,从其接口借用一些编程的约定是有意义的.
为了支持串口通信和仿效一个stdio风格的接口,我定义了SerialPort类,如下所示.
这个类把应用程序对串口的使用抽象成双向数据通道,使接口尽可能的和我们曾见过的类似.
除了构造函数和析构函数之外,这个类还包括四个方法——putchar()(注3)、puts()、getchar()和gets()——以发送和接收字符及字符串.
这些程序定义得就与在任何ANSIC兼容版本的头文件stdio.
h里的一样.
#include"circbuf.
h"#definePORTA0#definePORTB1classSerialPort{public:SerialPort(intport,unsignedlongbaudRate=19200L,unsignedinttxQueueSize=64,//TransmitBufferSizeunsignedintrxQueueSize=64);//ReceiveBufferSeize~SerialPort();intputchar(intc);intputs(constchar*s);intgetchar();char*gets(char*s);注3:你可能会奇怪为什么这个方法接受一个整数参数而不是一个字符.
毕竟我们正通过串口发送8位字符,对不嗯,别问见.
我只是试图遵守ANSIC库标准,也在问自己这个问题呢.
private:intchannel;CircBuf*pTxQueue;//TransmitBufferCircBuf*pRxQueue;//ReceiveBuffer};注意私有数据成员channel、pTxqueue和pRxqueue.
这些是在构造函数中初始化的,用作串行端口驱动的硬件专用部分的接口,这会在下一节介绍.
关于这个接口我一会儿会详细的说,但是现在只要知道SerialPort类没有包含任何对应于具体串口控制器的特殊代码.
这一切都隐藏在它引用的SCC类里面.
让我们看一下SerialPort的构造函数.
这个程序负责初始化三个私有的数据成员和设置SCC中请求的数据通道.
#include"scc.
h"staticSCCscc;**Function:SerialPort()**Description:Defaultconstructorfortheserialportclass.
**Notes:**Returns:Nonedefined.
*SerialPort::SerialPort(intport,unsignedlongbaudRate;unsignedinttxQueueSize,unsignedintrxQueueSize){////Initializethelogicaldevice.
//switch(port){casePORTA:channel=0;break;casePORTB:channel=1;break;default:channel=-1;break;}////CreateinputandoutputFIFOs.
//pTxQueue=newCircBuf(txQueueSize);pRxQueue=newCircBuf(rxQueueSize);////Initializethehardwaredevice.
//scc.
reset(channel);scc.
init(channel,baudRate,pTxQueue,pRxQueue);}/*SerialPort()*/一旦SerialPort对象被创建了,就可以使用前述的方法发送和接收数据.
比如,在早先的helloWorld函数中,puts("Hello,World!
")是把文本字符串送到端口A(也叫作:SCC通道0).
数据按照SeriaIPort构造函数中由baudRate,参数选择的波特率——19200bps,被送到串行端口.
发送和接收方法依赖于分别由pTxQueue和pRxQueue指向的循环缓存器.
PTxQueue是一个传输缓存器,它在应用程序发送字符的速率大于通道的波特率时提供了溢出存贮.
这通常发生在一个很短的时间里,因此,可以认为传输缓存通常自始至终都没有充满过.
类似地,接收缓行,PRxQueue,为那些已经被串行端口接收但是还没有被应用程序读取的字节提供溢出存储.
默认的情况下,上面的构造函数为它们分别创建64字节的缓存.
然而,缓存的大小可以设置成更小或是更大的值,这要视你应用程序的需要而定.
设置时只要简单的替换构造函数中的默认参数就可以了.
发送方法putchar()和puts()的实现说明如下.
在putchar()中我们首先检验是否传输缓存已经满了.
如果已经满了,我们就向调用者返回一个出错信息,因此它会知道字符没有被发送.
否则,我们把新的字符加入到传输缓存,接着确保SCC传输引擎在运行,然后成功返回.
puts()方法做了一系列putchar的调用,对字符串中的每一个字符做一次调用,然后在末尾加入一个换行字符.

**Function:putchar()**Description:Writeonecharactertotheserialport.
**Notes:**Returns:Thetransmittedcharacterisreturnedonsuccess.
*-1isreturnedinthecaseofanerror.
*intSerialPort::putchar(intc){if(pTxQueue->isFull()){return(-1);}////AddthecharactertothetransmitFIFO.
//pTxQueue->add((char)c);////Startthetransmitengine(ifit'sstalled).
//scc.
txStart(channel);return(c);}/*putchar()*/**Function:puts()**Description:Copiesthenull-terminatedstringstotheserial*portandappendsanewlinecharacter.
**Notes:Inrarecases,thisfunctionmayreturnsuccessthough*thenewlinewasnotactuallysent.
**Returns:Thenumberofcharacterstransmittedsuccessfully.
*Otherwise,-1isreturnedtoindicateerror.
*intSerialPort::puts(constchar*s){constchar*p;////Sendeachcharacterofthestring.
//for(p=s;*p!
='\0';p++){if(putchar(*p)isEmpty()){return(-1);//Thereisnoinputdataavailable.
}intrxStalled=pRxQueue->isFull();////ReadthenextbyteoutofthereceiveFIFO.
//c=pRxQueue->remove();////Ifthereceiveengineisstalled,restartit.
//if(rxStalled){scc.
rxStart(channel);}return(c);}/*getchar()*/**Method:gets()**Description:Collectsastringofcharactersterminatedbyanewline*characterfromtheserialportandplacesitins.
*Thenewlinecharacterisreplacedbyanullcharacter.
**Notes:Thecallerisresponsibleforallocatingadequatespaceforthestring.
**Warnings:Thisfunctiondoesnotblockwaitingforanewline.
*Ifacompletestringisnotfound,itwillreturn*whateverisavailableinthereceivequeue.
**Returns:Apointertothestring.
*Otherwise,NULLisreturnedtoindicateanerror.
*charSerialPort::gets(void){char*p;intc;////Readcharactersuntilanewlineisfoundornomoredata.
//for(p=s;(c=getchar(n'&&c>=0;p++){*p=c;}////Terminatethestring.
//*p='\0';return(s);}/*gets()*/Zilog89230串行端口控制器在Arcom板上的两个串行端口是同一个Zilog85230串行通信控制器的一部分.
不幸的是,这个特殊的芯片设置和使用起来比较复杂.
因此,我决定把串行端口驱动程序划分成两个部分,而不是用早先的特殊设备代码来填充SerialPort类.
上层是我们刚讨论过的类.
这个上层的类要通过任何两通道的SCC来工作,这个SCC提供面向字节的传输和接收接口以及可以设置的波特速率.
这一切所必需的是实现一个设备专用的SCC类(下面要描述的低屋),这个类具有和SerialPort类相同的reset()、init()、txStart()和rxStart()接口.
实际上,Zilog85230SCC设备如此难以设置和使用的一个原因是它有很多的选项,超过了这个简单程序真正的需要.
这个芯片不仅能够发送字节而且可以发送不超过八位的字符.
除了可以选择波特速率外,它还可以设置一个或者两个通道的很多其他特性,可以支持很多其他的通信的协议.
#include"circbuf.
h"classSCC{public:SCC();voidreset(intchannel);voidinit(intchannel,unsignedlongbaudRate,CircBuf*pTxQueue,CircBuf*pRxQueue);voidtxStart(intchannel);voidrxStart(intchannel);private:staticvoidinterruptInterrupt(void);};注意这个类也依赖于CircBuf类.
init()方法中pTxQueue和pRxQueue参数用来为通道建立输入和输出缓存.
这使得有可能在SCC设备内部把其中一个物理通道和逻辑串行端口连接起来.
init()方法是独立于构造函数的,这样定义的原因是大部分的SCC芯片控制着两个或者更多的串行端口.
构造函数在第一次被调用的时候对它们都进行了重置.
然后,调用init以设置具体某个通道的波特速率及其他的参数.
关于SCC类,还要说的就是专门针对Zilog85230设备的一个内部功能部分.
由于这个原因,我决定不在这本书里列出或是解释这段长而复杂的模块.
只要说明代码由读写设备寄存器的宏处理接收和发送中断的中断服务程序以及一些排错方法组成,如果接收和发送过程在等待更多数据时被停止,那么这些排错方法可以重新启动它们.
有兴趣的读者可以在文件scc.
cpp中找到实际的代码.
第十章优化你的代码事情应该尽可能简化,而不只是简单一点点,一爱因斯坦虽然使软件正确的工作好像应该是一个工程合乎逻辑的最后一个步骤,但是在嵌入式的系统的开发中,情况并不总是这样的.
出于对低价系列产品的需要,硬件的设计者需要提供刚好足够的存储器和完成工作的处理能力.
当然,在工程的软件开发阶段,使程序正确的工作是很重要的.
为此,通常需要一个或者更多的开发电路板,有的有附加的存贮器,有的有更快的处理器,有的两者都有.
这些电路板就是用来使软件正确工作的.
而工程的最后阶段则变成了对代码进行优化.
最后一步的目标是使得工作程序在一个廉价的硬件平台上运行.

提高代码的效率所有现代的C和C++编译器都提供了一定程度上的代码优化.
然而,大部分由编译器执行的优化技术仅涉及执行速度和代码大小的一个平衡.
你的程序能够变得更快或者更小,但是不可能又变快又变小.
事实上,在其中一个方面的提高就会对另一方面产生负面的影响.
哪一方面的提高对于程序更加的重要是由程序员来决定.
知道这一点后,无论什么时候遇到速度与大小的矛盾,编译器的优化阶段就会作出合适的选择.
因为你不可能让编译器为你同时做两种类型的优化,我建议你让它尽其所能的减少程序的大小.
执行的速度通常只对于某些有时间限制或者是频繁执行的代码段是重要的.
而且你可以通过手工的办法做很多事以提高这些代码段的效率.
然而,手工改变代码大小是一件很难的事情,而且编译器处于一个更有利的位置,使得它可以在你所有的软件模块之间进行这种改变.
本章内容:z提高代码的效率z减小代码的大小z降低内存的使用z限制C++的影响直到你的程序工作起来,你可能已经知道或者是非常的清楚,哪一个子程序或者模块对于整体代码效率是最关键的.
中断服务例程、高优先级的任务、有实时限制的计算、计算密集型或者频繁调用的函数都是候选对象.
有一个叫作profiler的工具,它包括在一些软件开发工具组中,这个工具可以用来把你的视线集中到那些程序花费大部分时间(或者很多时间)的例程上去.
一旦你确定了需要更高代码效率的例程,可以运用下面的一种或者多种技术来减少它们的执行时间.
inline函数在c++中,关键字inline可以被加入到任何函数的声明.
这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用.
这样做删去了和实际函数调用相关的时间开销,这种做法在inline函数频繁调用并且只包含几行代码的时候是最有效的.
inline函数提供了一个很好的例子,它说明了有时执行的速度和代码的太小是如何反向关联的.
重复的加入内联代码会增加你的程序的大小,增加的大小和函数调用的次数成正比.
而且,很明显,如果函数越大,程序大小增加得越明显.
优化后的程序运行的更快了,但是现在需要更多的ROM.
查询表switch语句是一个普通的编程技术,使用时需要注意.
每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么工作,就把宝贵的处理器时间耗尽了.
为了提高速度,设法把具体的情况按照它们发生的相对频率排序.
换句话说,把最可能发生的情况放在第一,最不可能的情况放在最后.
这样会减少平均的执行时间,但是在最差情况下根本没有改善.
如果每一个情况下都有许多的工作要做,那么也许把整个switch语句用一个指向函数指针的表替换含更加有效.
比如,下面的程序段是一个待改善的候选对象:enumNodeType{NodeA,NodeB,NodeC}switch(getNodeType()){caseNodeA:.
.
.
caseNodeB:.
.
.
caseNodeC:.
.
.
}为了提高速度,我们要用下面的代码替换这个switch语句.
这段代码的第一部分是准备工作:一个函数指针数组的创建.
第二部分是用更有效的一行语句替换switch语句.
intprocessNodeA(void);intprocessNodeB(void);intprocessNodeC(void);/**Establishmentofatableofpointerstofunctions.
*/int(*nodeFunctions[processNodeA,processNodeB,processNodeC};.
.
.
/**Theentireswitchstatementisreplacedbythenextline.
*/status=nodeFunctions[getNodeType()]();手工编写汇编一些软件模块最好是用汇编语言来写.
这使得程序员有机会把程序尽可能变得有效率.
尽管大部分的C/C++编译器产生的机器代码比一个一般水平的程序员编写的机器代码要好的多,但是对于一个给定的函数,一个好的程序员仍然可能做得比一般水平的编译器要好.
比如,在我职业生涯的早期,我用C实现了一个数字滤波器,把它作为TITMS320C30数字信号处理器的输出目标.
当时我们有的编译器也许是不知道,也许是不能利用一个特殊的指令,该指令准确地执行了我需要的那个数学操作.
我用功能相同的内联汇编指令手工地替换了一段C语言的循环,这样我就能够把整个计算时间降低了十分之一以上.
寄存器变量在声明局部变量的时候可以使用register关键字.
这就使得编译器把变量放入一个多用选的寄存器,而不是堆栈里.
合适地使用这种方珐,它会为编译器提供关于最经常访问变量的提示,会稍微提高函数的执行速度.
函数调用得越是频繁,这样的改变就越是可能提高代码的速度.
全局变量使用全局变量比向函数传递参数更加有效率.
这样做去除了函数调用前参数入栈和函数完成后参数出栈的需要.
实际上,任何子程序最有效率的实现是根本没有参数.
然而,决定使用全局变量对程序也可能有一些负作用.
软件工程人士通常不鼓励使用全局变量,努力促进模块化和重入目标,这些也是重要的考虑.
轮询中断服务例程经常用来提高程序的效率.
然而,也有少数例子由于过度和中断关联而造成实际上效率低下.
在这些情况中,中断间的平均时间和中断的等待时间具有相同量级.
这种情况下,利用轮询与硬件设备通信可能会更好.
当然,这也会使软件的模块更少.

定点运算除非你的目标平台包含一个浮点运算的协处理器,否则你会费很大的劲去操纵你程序中的浮点数据.
编译器提供的浮点库包含了一组模仿浮点运算协处理器指令组的子程序.
很多这种函数要花费比它们的整数运算函数更长的执行时间,并且也可能是不可重入的.
如果你只是利用浮点数进行少量的运算,那么可能只利用定点运算来实现它更好.
虽然只是明白如何做到这一点就够困难的了,但是理论上用定点运算实现任何浮点计算都是可能的.
(那就是所谓的浮点软件库.
)你最大的有利条件是,你可能不必只是为了实现一个或者两个计算而实现整个IEEE754标准.
如果真的需要那种类型的完整功能,别离开编译器的浮点库,去寻找其他加速你程序的方法吧.
减小代码的大小正如我早先说的那样,当问题归结于减小代码的大小的时候,你最好让编译器为你做这件事.
然而,如果处理后的程序代码对于你可得的只读存贮器仍然太大了,还有几种技术你可以用来进一步减少体程序的大小.
在本节中,自动的和人工的代码优化我们都要讨论.
当然,墨菲法则指出,第一次你启用编译器的优化特性后,你先前的工作程序会突然失效,也许自动优化最臭名昭著的是"死码删除".
这种优化会删除那些编译器相信是多余的或者是不相关的代码,比如,把零和一个变量相加不需要任何的计算时间.
但是你可能还是希望如果程序代码执行了编译器不了解的函数,编译器能够产生那些"不相关"的指示.
比如,下面这段给出的代码,大部分优化编译器会去除第一条语句,因为*pControl在重写(第三行)之前没有使用过:*pControl=DISABLE;*pData='a';*pCotrol=ENABLE;但是如果pControl和pData实际上是指向内存映像设备寄存器的指针怎么办这种情况下,外设在这个字节的数据写入之前将接收不到DISABLE的命令.
这可能会潜在地毁坏处理器和这个外设之间的所有未来的交互作用.
为了使你避免这种问题,你必须用关键字"volatile"声明所有指向内存映像设备寄存器的指针和线程之间(或者是一个线程和一个中断服务程序之间)共享的全局变量.

你只要漏掉了它们中的一个,墨菲法则就会在你的工程的最后几天里回来,搅得你心神不宁.
我保证.
警告:千万不要误以为程序优化后的行为会和未优化时的一样.
你必须在每一次新的优化后完全重新测试你的软件,以确保它的行为没有发生改变.
更糟糕的是,或者退一步说,调试一个优化过的程序是富有挑战性的.
启用了编译器的优化后,在源代码中的一行和实现这行代码的那组处理器指令之间的关联关系变得更加微弱了.
那些特定的指令可能被移动或者拆分开来,或者两个类似的代码可能现在共用一个共同的实现.
实际上,高级语言程序的有些行可能完全从程序中去除了(正如在前面例子里那样).
结果,你可能无法在程序特定的一行上设置一个断点或者无法研究一个感兴趣变量的值.
一旦你使用了自动优化,这里有一些关于用手工的办法进一步减少代码大小的技巧.
避免使用标准库例程为了减少你的程序的大小,你所能做的最好的一件事情就是避免使用大的标准库例程.
很多最大的库例程代价昂贵,只是因为它们设法处理所有可能的情况.
你自己有可能用更少的代码实现一个子功能.
比如,标准C的库中的spintf例程是出了名的大.
这个庞大代码中有相当一部分是位于它所依赖的浮点数处理例程.
但是如果你不需要格式化显示浮点数值(%f或者%d),那么你可以写你自己的sprintf的整数专用版本,并且可以节省几千字节的代码空间.
实际上,一些标准C的库(这让我想起Cygnus的newlib)里恰好包含了这样一个函数,叫作sprintf.
本地字长每一个处理器都有一个本地字长,并且ANSIC和C++标准规定数据类型int必须总是对应到那个字长.
处理更小或者更大的数据类型有时需要使用附加的机器语言指令.
在你的程序中通过尽可能的一致使用int类型,你也许能够从你的程序中削减宝贵的几百字节.
goto语句就像对待全局变量一样,好的软件工程实践规定反对使用这项技术.
但是危急的时候,goto语句可以用来去除复杂的控制结构或者共享一块经常重复的代码.
除了这些技术以外,在前一部分介绍的几种方法可能也会有帮助,特别是查询表、手工编写汇编、寄存器变最以及全局变量.
在这些技术之中,利用手工编写汇编通常可以得到代码最大幅度的减少量.
降低内存的使用在有些情况下,限制你的应用程序的因素是RAM而不是ROM.
在这些情况下,你想要降低对于全局变量、堆和栈的依赖.
这些优化由程序员来做比用编译器来做会更好.
由于ROM通常比RAM更加便宜(以每字节为基准),所以一个可接受的降低全局数据量的策略是把常数移到ROM中去.
如果你用关键字const声明所有的常数,那么这可以由编译器自动完成.
大部分的C/C++编译器把所有它们遇到的常全局数据放入一个特殊的数据段里,这个数据段可以被定位器识别为可分配ROM的数据段.
如果有很多的字符串和导向表数据在运行的时候不发生变化,那么这项技术是最有价值的.
如果有些数据一旦程序运行起来就固定了,但不一定是不变的,那么常数数据段可以改放在一个混合存储设备中.
然后,这个存贮设备可以通过网络来更新,或者由一个指派的技术员来完成这个改变.
在你的产品要部署的每一个地区的税率就是这种数据的一个例子.
如果税率发生了改变,那么存储设备可以更新,但是同时也节省了附加的RAM.
减小栈的大小也可以降低你的程序对于RAM的需要.
有一种方法可以准确地计算出你需要多大的栈.
做法是用一个特殊的数据类型填满整个为栈保留的存储区域.
然后,在软件运行一段时间之后——最好在正常和紧张两种情况下都运行一下——用调试工具研究被修改过的栈.
有一部分仍然包含有你的特殊类型数据的栈存储区,因此可以安全地从栈的大小中减去那部分存储区的大小(注1).
如果你在使用一个实时的操作系统,就要特别当心栈的大小.
大部分操作系注1:当然,你可能想在栈中留一点额外的空间——万一你的测试没有持续足够长的时间,或者没有准确地反映所有可能的运行场景.
千万不要忘记栈的溢出对于你的软件来说是一个潜在的致命事件,要不惜一切代价避免.
统为每一个任务创建一个分离的栈.
这些栈用于函数的调用以及在一个任务的设备场景中遇到的中断服务倒程.
你可以通过前面介绍的方式为每一个任务的栈决定其数量.
你可以设法减少任务的数量或者切换到一个操作系统,这个操作系统具有分离的为执行所有中断服务例程而建立的"中断栈".
后一种方法可以显著地降低每个任务对栈大小的要求.
堆的大小受限于RAM在所有的全局数据和栈空间都分配以后剩余的数量,如果堆太小,你的程序就不能够在需要的时候分配内存,因此在废弃它之前一定要把malloc和new的结果和NULL比较.
如果你试过了所有这些建议,而且你的程序仍然需要太多的存储空间,那么你除了完全删除所有的堆之外没有别的选择.
限制C++的影响在决定写这本书的时候我面临的一个最大的问题是:是否把C++加入到讨论中去.
尽管我熟悉C++,但是我不得不用C和汇编来写几乎所有我的嵌入式软件.
而且在嵌入式软件界对于C++是否值得所产生的性能损失的问题存有很大的争议.
一般认为C++程序会产生更大的代码,这些代码执行起来比完全用C写的程序要慢.
然而,C++给于程序员很多好处,并且我想在这本书中讨论一些这样的好处.
因此,我最终决定把C++加入到讨论中来,但是在我的例子中只是使用那些性能损失最小的特性.
我相信很多的读者在他们自己的嵌入式系统编程的时候会面对相同的问题.

在结束这本书之前.
我想简单地评判一下每一种我使用过的C++特性.
并且提醒你一些我没有使用过的比较昂贵的特性.
当然,并不是每一件C++引入的事情都是昂贵的.
很多老的C++编译器并入了一个叫作C.
front的技术,这项技术把C++的程序变成C,并且把结果供给标准的C编译器.
这个事实暗示这两种语言之间的句法差别很小,或与运行代价无关(注2).
只有最新的C++特性,如模板,不能够用这种方式处理.
比如,类的定义是完全有益的.
公有和私有成员数据及函数的列表与一个注2:而且,要澄清的是,用C++编译器编译一个普通的C程序不会有损失.
struct及函数原型的列表没有大的差别.
然而,C++编译器能够用public和private关键字决定,哪一个方法调用和数据访问是允许的或者是不允许的.
因为这个决定在编译的时候完成,所以运行时不会付出代价.
单纯的加入类既不会影响代码的大小,又不会影响你的程序的效率.
默认参数值也是没有损失的.
编译器只是加入代码使得在每次函数被无参数调用的时候传递一个默认的值.
类似地,函数名的重载也是编译时的修改.
具有相同名字但是不同参数的函数在编译过程中分别分配了一个唯一的名字.
每次函数名出现在程序中的时候编译器就替换它,然后连接器正确的把它们匹配起来.
我没有在我的例子中使用C++的这一特性,但是我这幺做过而没有影响性能.
嵌入式的C++标准你可能想知道为什么C++语言的创造者加入了如此多的昂贵的——就执行时间和代码大小来说——特性.
你并不是少数,全世界的人都在对同样的一件事情困惑——特别是用C++做嵌入式编程的用户们.
很多这些昂贵的特性是最近添加的,它们既不是绝对的必要也不是原来C++规范的一部分.
这些特性一个接着一个的被添加到正在进行着的"标准化"进程中来.
在1996年,一群日本的芯片厂商联台起来定义了一个C++语言和库的子集,它更加适合嵌入式软件开发.
他们把他们新的工业标准叫作嵌入式C++.
令人惊奇的是,在它的初期,它就在C++用户群中产生了很大的影响.
作为一个C++标准草案的合适子集,嵌入式C++省略了很多不限制下层语言可表达性的任何可以省略的东西.
这些被省略的特性不仅包括像多重继承性、虚拟基类、运行时类型识别和异常处理等昂贵的特性,而且还包括了一些最新的添加特性,比如:模板、命名空问、新的类型转换等.
所剩下的是一个C++的简单版本,它仍然是面向对象的并且是C的一个超集,但是它具有明显更少的运行开销和更小的运行库.
很多商业的C++编译器已经专门地支持嵌入式C++标准.
个别其他的编译器允许手工的禁用具体的语言特性,这样就使你能够模仿嵌入式C++或者创建你的很个性化的C++语言.
操作符的重载是另一个我使用过但是没有包括在例子中的特性.
无论何时编译器见到这样一个操作符,它只是用合适的函数调用来替换它.
因此,在下面列出的代码,最后两行是等价的,性能的损失很容易明白:Complexa,b,c;c=operator+(a,b)//Thetraditionalway:FunctionCall//TheC++way:OperatorOverloading构造函数和析构函数也有一点与它们相关的损失.
这些特殊的方法去分别保证每次这种类型的对象在创建或者超出了范围时被调用.
然而,这个小量的开销是为减少错误而支付的一个合理代价.
构造函数完整地删除了一个C语言编程中与未初始化数据结构编程错误有关的类.
这个特性也被证明是有用的,因为她隐藏了那些与像Timer和Task这样复杂的类相关的笨拙初始化顺序.
虚拟函数也具有一个合理的代价收益比.
不要深究太多的关于什么是虚拟函数的细节,让我们只是说一下没有它们多态性就是不可能的.
而没有多态性,C++就不可能是一个真正的面向对象的语言.
虚拟函数唯一一个明显的代价是在调用虚拟函数之前附加了一个存储查询.
普通的函数和方法调用是不受影响的.

就我的体验来说太昂贵的C++特性有模板、异常事件及运行类型识别.
这三个特性都对代码的大小有负面的影响,而且异常事件和运行时类型识别还会增加执行时间.
在决定是否使用这些特性之前,你可能要做一些实验来看看它们会怎么样影响你自己的应用程序的大小及速度.
附录Arcom的Target188EB本书所有的例子都是为Target188EB这个平台编写、测试的.
这块板子是低成本、高速度的嵌入式控制器,由Arcom控制系统公司设计、生产.
本章下面的部分就是关于订购该产品时应该了解的相关信息.
Target188EB硬件包括如下的组件:·处理器:Intel80188EB(25MHz)·RAM:128KSRAM(可增至256K),电源备份可选·ROM128KEPROM和128K快闪存储器(可增至512K)·两个RS232兼容的串口(外置DB9连接器)·24通道的并口·三个计时器·4个中断输入·8位PC/104扩展总线接口·一个可选的8位STEBus扩展接口·一个远程调试适配器,包含两个RS232兼容的串口这块板子的开发就像PC机编程一样,很容易.
使用板子自带的免费开发工具(BorlandC++和TurboAssembler编译器),你可以开发C/C++或汇编语言的应用程序.
同时,在板子的闪存中预先装好了调试监视器,以便于用BorlandTurboDebugger很容易地发现、修正应用程序的bug.
还有一个硬件接口函数库,使得开发人员对板子上的硬件进行编程就像用C语言的stdio库一样容易.
本书中的所有例子都是用BorlandC++3.
1编译、连接、调试的.
但是,用Borland的任何兼容80186的产品都可以作同样的事情.
这包括BorlandC++3.
1、4.
5、4.
52.
如果你有这几个版本中的某一个,就可以直接应用了.
如果没有,那你得询问一下Arcom公司,Borland公司的最新产品是否可以直接用.
如果订货量少的话,Targetl88EB板子(具体的编号是Targetl88EB-SBC)的零售价是195美元(注1).
一般说来是不包括开发工具和完善的技术支持的.
但是,Arcom会免费提供自己的开发工具(原价l00美元)给本书(注2)的读者.
如果你需要订货的话.
请按如下的联系方式:ArcomControlSystem13510SouthOakStreetKansasCity,MO64145Phone:888-941-2224Fax:816-941-7807Email:sales@arcomcontrols.
comWeb:http://www.
arcomcontrols.
com/注1:有关这个板的供货与价格的最新信息请与Arcom公司联系.
注2.
在我或O'Reilly与Arcom公司之间没有任何经济合约关系.
我在此推荐这块板子,只是为了感谢Arcom公司生产了这么好的产品,并为我写作此书给予了大力支持.
参考书目开始嵌人式系统的一大困难是参考书籍太少.
大多数的此类书籍都是不令人满意的.
下面就是本书的参考书目、杂志和其他有用的资源.
我并不是想罗列所有的相关资料,其实,很多的书籍部被我省略掉了,因为它们给我留下的印象并不深刻.
下面的书籍是值得收藏的、杂志是值得订阅的、网站是值得放入收藏里的.
书籍Ball,StuartR.
EmbeddedMicroprocessorSystems:RealWorldDesignNewton,Mass.
:Butterworth-Heinemann,1996这本小书收集了很多关于嵌入式系统开发的硬件知识,这是每个嵌入式系统开发工程师都应该好好阅读一下的.
Brown,JohnForrestEmbeddedSystemsProgramminginCandAssembly.
NewYork:VanNostrandReinhold,1994我几年前还不知道有这本书,但是这是好事,否则我就不可能自己钻研了.

当我试图竭力从汇编语言中解脱出来的时候,这本书帮了我很大的忙.
Ganssle,JackG.
TheArtofProgrammingEmbeddedSystems.
SanDiego:AcademicPress,1992这本书中有很多很有实际应用价值的好的建议.
这本书的作者还是《EmbeddedSystemsProgramming》(在后面介绍)杂志的撰稿人.
Ganssle先生在这本书中收集了很多很有帮助的建议.
本书的内容尤其易于查找、检索.

Kernighan,BrianW.
,andDennisM.
Ritchie.
TheCProgrammingLanguageEnglewoodCliffs.
N.
J.
:Prentice-Hall,1988来自创造者的对C语言语法和语义的简洁阐释.
编程人员必备藏书.
Labrosse,JeanJ.
uC/OS:TheReal-TimeKernel.
Lawrence,Kans.
:R&DPublications,1992一个实时操作系统,有源码.
有注释——本书的价格是物有所值的.
对于那些想开发自己的操作系统,或者正在寻找免费源程序的人来说,这本书是很值得购买的.
uC/OS(发音为micro-COS)已经被移植到了很多处理器上,有相当多的用户群体(译注1).
Rosenberg,JonathanB.
HowDebuggersWork:Algorithms,DataStructures,andArchitecture.
NewYork:JohnP.
Wiley&Sons,1996如果你对调试器的内部机理感到好奇,本书正适合你.
它将帮助你更好的理解调试器和调试监视器的区别,以及调试器与你的程序发生冲突的潜在威胁.

Satir,Gregory,andDougBrown.
C++:TheCoreLanguage.
Cambridge,Mass:O'Reilly&Associates,1995这本书是对C语言开发者进行C++进阶的.
如果你还没有收藏一本C++的书,那这本书很适合你(译注2).
VanderLinden,Peter.
ExpertCProgramming:DeepCSecrets.
EnglewoodCliffs,N.
J:Prentice-Hall.
1994本书由Sun公司编译器开发组成员撰写,可以使你从一名普通的C程序员成长为一位高手.
这些高级主题虽然并非全部都是必要的,但只有理解了它们,你才能成为更好的嵌入式系统程序员.
本书是一本绝佳的手册,而且写得非常有趣.
译注1:此书新版本为《MicroC/OS-II:TheReal-TimeKernel》.
中文版即将由中国电力出版社出版.
译注2:中文版《C++语言核心》已由中国电力出版社出版.
VanSickle,Ted.
ProgrammingMicrocontrollersinCSolanaBeach,CalifHighTextPublications.
1994正如我见过的大多数讲嵌入式系统的书一样,这本书也是针对某个特定处理器系列的.
但是,由于这本书写得很不错,而且Motorola微控制器被广泛应用,有些读者还是感到这本书很有用的.
杂志和会议EmbeddedSystemProgramming一个月刊,其内容主要是关于嵌入式系统软件开发人员在工作中遇到的问题.
每篇文章都有评语和建议,而且使用特殊字体,这样更易于阅读.
我强烈建议每个人都读一下这个杂志,甚至是放下我这本书几分钟,马上到http://www.
embedded.
com/mag.
shtml去订购一份.
通常需要几个月的时间才能拿得到杂志,但是这是很值得一等的.
另外,你可能想购买一份放在CD-ROM中的杂志.
这张CD-ROM中存有几百篇文章.
详情可以参考http://www.
embedded.
com/cd.
html.
EmbeddedSystemConference这项技术会议是由《EmbeddedSystemProgramming》杂志承办的,每年有几次,已经有10年的历史了,每年的参加者都在增加.
在会上学习知识的速度要远远快于在书海中苦读.
我会想尽办法去参加这个会的.
WorldWideWebChipDirectory(http://www.
hitex.
com/chipdir/)一个令人难以置信的关于处理器和外设的信息汇集地.
这个站点不是唯一的,但却是同类站点中最棒的,而且它还有许多有价值的链接.
CPUInfoCenter(http://infopad.
eecs.
berkeley.
edu/ClC/)这个网站上有大量关于处理器的信息,有一个专栏是关于嵌入式处理器的.

CRCPitstop(http://www.
ross.
net/crc)这个网站是专门讨论CRC实现的,包括RossWilliam的"PainlessGuidetoCRCErrorDetectionAlgorithms.
"后者是我所见过的最好的关于讲述CRC计算的书.
ElectronicEngineers'Toolbox(http://www.
eetoolbox.
com/ebox.
htm)这个网站专门讨论嵌入式系统、实时软件开发问题和其他Internet催生的技术,旨在使你的工作更轻松.
网站作者鉴别、索引和总结了成千上万个相关的Inlernet资源,你可以在此将它们一网打尽.
EmbeddedIntelArchitecture(http://www.
intel.
com/design/intarch/)Intel主页里关于嵌入式处理器的内容,包括80188EB的信息.
这个网站上不但有硬件信息,还有很多免费的软件开发、调试工具和很多源码.
news:comp.
arch.
embedded一个新闻组,讨论的内容在本书中都有所涉及,主要包括软件开发工具和开发过程、商用实时操作系统的比较,对于关键代码的处理等.
news:comp.
realtime另外一个讨论嵌入式系统的新闻组,更多的讨论内容集中在实时操作系统的任务调度上.
在这个新闻组上可以找到一系列FAQ:http://www.
faqs.
org/faqs/by-newsgroup/comp/comp.
realtime.
html.
词汇表ASIC专用集成电路与应用相关的集成电路.
集成在一个芯片中的用户设计的硬件.
addressbus地址总线与处理器及外设相连的电路线.
地址总线被处理器用来选择内存地址或指定外设的寄存器.
如果地址总线包括n条电路线,处理器就可以寻址2n个地址.
applicationsoftware应用软件与某个特定嵌入式项目相关的软件模块,这种软件模块一般是不可重复利用的,因为每个嵌入式的系统都不大一样.
assembler汇编程序一种软件开发工具,可以把人能读懂的汇编语言转换成处理器可以识别的机器码.
assemblerlanguage汇编语言一种人能读得懂的处理器指争集.
大多数与处理器相关的代码都出须用汇编语言编写.
binarysemaphore二元信号灯一种信号灯,只有开和关两种状态.
也叫互斥体(mutex).
boardsupportpackage板级支持软件包与处理器或硬件平台相关的软件包.
一般来说是一些示例源程序.
这些源程序必须与别的一些软件包一起编译、链接.
breakpoint断点程序中的某个位置,程序执行到这里要被中断,然后控制权要由处理器交到调试器那里.
生成、删除断点的方法一般是由调试工具提供的.
CISC(ComplexInstructionSetComputer)复杂指令集计算机处理器家族的一员.
CISC处理器可以产生长度可变的指争和多地址格式,而且只包含很少的寄存器.
Intel80x86处理器都是CISC的.
与CISC相对的是RISC.
CPU(CentralProcessingUnit)中央处理器处理器中自责执行指争的部件.
compiler编译器一种软件开发工具,能把高级语言转换成相应处理器能识别、执行的机器码.

context场景当前与处理器的寄存器和标志相关的状态.
contextswitch场景切换在多任务操作系统中,从一个任务切换到另一个任务的过程.
一个场景切换的过程包括保存当前正在运行的任务的场景,并把以前保存起来的某个任务的场景加载到处理器中.
这一过程的代码一定是与相应处理器有关的.
countingsemaphore计数型信号灯一种信号灯,用来跟踪多个同类的资源.
对于这种信号灯的操作只有在所有要跟踪的资源都在被使用时情况下才不能进行.
与这种信号灯相对的是Binarysemaphorec.
criticalsection临界区一段不允许被中断的代码,如果被中断则代码运行无法得到正确的结果.
参见Racecondition.
cross-compiler交叉编译在一种处理器的机器上运行,为另一种处理器的机器产生目标代码.
DMA(DirectMemoryAccess)直接存储器存取一种在外设之间(通常是内存和I/O设备)传递数据的技术,基本上不需要处理器参与.
DMA传输是由DMA控制器管理的.
DRAM(DynamicRandom-AccessMemory)动态随机存取存储器一种随机存取存储器,可以暂时的保留所存内容,直至设备中存储的数据按正常间隔刷新.
刷新周期通常是由一种称为DRAM控制器的外围设备负责的.
databus数据总线与处理器及同其通信的所有外围设备相连的一组电气线路.
当处理器要读/写某一特殊外围设备的存储器单元或寄存器中的内容时,它将适当的设置地址总线引脚,并接收/发送数据总线的内客.
deadline时间限制系统必须完成某个特定运算的时间.
参见real-timesystem.
deadlock死锁一种不希望出现的软件状态,其中整个任务集合都被阻塞.
等待着只有同一集合中的某一任务才能引发的事件.
如果死锁发生,唯一的解决办法是重启系统.

但是,如果遵循一定的软件设计实践,避免死锁通常是可能的.
debugmonitor调试监视器专门设计来用作调试工具的一种嵌入式软件.
通常驻留在ROM中,通过串行端口或网络连接与调试器通信.
调试监视器提供一套原语命令:察看和修改存储器单元和寄存器,创建和删除断点,井执行程序.
调试器结合这三组原语以实现程序下载和单步调试等高级请求.
debugger调试器用于测试和调试嵌入式软件的一种软件开发工具.
调试器运行在主机上,通过串行端口或网络连接与目标机相连.
使用调试器,你可以将软件下载到目标机上立即执行,还可以设置断点,并检查某个存储器单元和寄存器的内容.
devicedriver设备驱动一种隐藏外围设备细节并提供高级编程接口的软件模块.
deviceprogrammer设备编程器非易失存储芯片和其他电气可编程设备的编程工具.
通常情况下,将可编程设备插入设备编程器的插座中,然后将存储缓冲的内容传入.
digitalsignalprocessor(DSP)数字信号处理器与微处理器类似的一种设备,不同之处在于,其内部CPU是为离散时间信号处理的应用专门优化的.
除了标准微处理器指令外,数字信号处理器通常还支持一套用来快速执行通用信号处理运算的复杂指令.
常见的数字信号处理器产品是TI公司的320Cxx和Motorola公司的5600x系列.
EEPROM电可擦除可编程只读存储器英文读音为"double-EPROM".
一种可以通过电气方式擦除的可编程只读存储器(PROM).
EPROM可擦除可编程只读存储器一种可以通过紫外线暴晒来擦除的可编程只读存储器(PROM).
一旦擦除,它可以借助设备编程器重新编程.
embeddedsystem嵌入式系统一种软件、硬件的组合,目的是为了完成某项特定的功能.
与之相对的是"generalpurposecomputer(通用计算机)".
emulator仿真器在线仿真器(In-CircuitEmulator,ICE)的简称.
一种代替(模仿)目标板子上处理器的调试工具.
仿真器经常被并入目标处理器的特殊打包版本中,使用户可收在程序执行时观察和记录处理器的内部状态.
executable可执行(文件)包含了目标代码的文件,可以被读取并执行.
firmware固件存放在ROM中的嵌入式的软件代码.
在DSP编程中,这个术语是很常见的.
flashmemory快闪存储器RAM-ROM的一种混合.
可以在软件控制下擦除和重写.
这种设备分为可分别擦除的多个块(称为扇区).
快速存储器在需要廉价的非易失数据存储的系统中非常普遍.
在某些场合,甚至有用太快闪存储器代替磁盘驱动器的.
general-purposecomputer通用计算机用作通用计算平台的计算机软硬件的组合.
例如,一台个人计算机.
与之相对的是embeddedsystem(嵌入式系统).
heap堆用于动态存储分配的存储区域.
调用C的malloc和free函数,使用C++的new和delete运算,可以在运行时对堆进行操作.
high-levellanguage(HIL)高级语言独立于处理器的语言,如C或C++.
使用高级语言编程,可以不必考虑特定处理器的细节,将精力集中在算法和程序上.
host主机通过串行端口或网络连接与目标机通信的通用计算机.
此术语通常用来区分调试器运行的平台计算机和用来开发嵌入式系统的计算机.
ICE(In-CircuitEmulator)在线仿真器参见Emulator(仿真器).
I/O(Input/Output)输入,输出处理器和外设的接口.
最简单的例子是"开关"(输入)和LED(输出).
I/0mapI/0映射包含I/O空间中每个处理器可访问的外围设备的名字和地址范围的表或图.
I/O映射对于了解硬件很有帮助.
I/OspaceI/0(地址)空间某些处理器提供的专用存储空间,通常是为I/O设备的连接保留的.
I/0空间中的存储单元和寄存器只能通过特珠指令存取.
例如,80x86系列有称为in和out的特殊I/O空间指令.
与之相对的是memoryspace(存储器空间).
instructionpointer指令指针处理器中的寄存器,含有下一条要执行的指令.
也称作Programcounter(程序计数器).
interrupt中断一种从外围设备到处理器的异步电信号.
当外围设备发出此信号时,我们称发生了一个中断.
一旦中断发生,处理器保存当前状态,并执行一个中断服务例程.
当中断服务例程退出,处理器的控制将返回到中断前正在运行的软件位置.

interruptlatency中断等待时间从中断发生开始,到中断服务程序开始运行之间的时间间隔.
interrupserviceroutine(ISR)中断服务例程与特定中断相关的软件代码.
interrupttype中断类型与每个中断相关的唯一数字.
interruptvector中断向量中断服务例程的地址.
interruptvectortable中断向量表一个包含中断向量的表,以中断类型为索引.
这张表包含了处理器关于中断到中断向量的映射,必须由程序员初始化.
intertaskcommunication任务间通信任务和中断服务例程用以共享信息,使对共享资源的存取同步的一种机制.
最常见的任务间通信的构件是信号灯和互斥体.
linker链接器一个工具软件,以一个或几个OBJ文件为输入参数,输出是可重定位的程序.
链接器走在所有的源程序都被编译之后才运行的.
locator定址器为链接器生成的可重定位程序分配物理地址的一种软件开发工具.
这是嵌入式系统执行之前的最后一个软件准备步骤.
所生成的文件称为可执行文件.

某些情况下,定址器的功能隐藏在链接器中.
logicanalyzer逻辑分析仪用于实时捕捉几十乃至成百上千个电气信号的逻辑电平(O或1)的硬件调试工具.
逻辑分析仪在调试硬件问题和复杂的处理器-外围设备相互作用时非常有用.
memorymap存储器映射一张包含了外设的名称和地址空间的表,可以被处理器寻址.
对于鉴定硬件的类型来说,存储器映射是很好的工具.
memory-mappedI/O存储器映射I/O(方法)一种常见的硬件设计方法,把I/O地址放到内存中,而不走放到I/O地址空间里.
从处理器的角度上看,存储器映射I/O设备与内存设备是一样的.
memoryspace存储器空间一个处理器的标堆地址空间.
与之相对的是I/Ospace(I/O空间).
microcontroller微控制器微控制器与微处理器是很相似的.
主要的不同是,微控制器更适用于嵌入式系统.
微控制器包括CPU、内存(少量的RAM、ROM)和同一芯片上的外设.
例如,805l、Intel80196和Mototola68HCxx系列.
microprocessor微处理器含有通用的CPU的芯片.
最常见的例于是Intel80x86和Motorola680xO系列.
monitor监枧器本书中就是指调试监视器.
但是,还有另外一种与任务间通信相关的意思.

在那里监视器是一种语言级的同步化部件.
multiprocessing多处理器(技术)在一个计算机系统中使用一个以上的处理器(的技术、方法).
所谓"多处理器系统"通常有多个处理器可以通信和共享数据的公共存储器空间.
而且,有些多处理器系统还支特并行处理.
multitasking多任务多个软件任务游辈(12)兄葱杏的一种情形.
每个任务都是相对独立的线程.
操作系统通过分割处理器时间片来实现这种幼疾病(12)杏.
mutex互斥体一种相互排斥的数据结构,也称二无信号灯.
互斥体本质上是一种多任务二元标志可用于保护关键区免于中断.
mutualexclusion互斥对共享资源的独占性存取的一种保证措施.
在嵌入式系统中,共享资源通常是存储器的一块区域,一个全局变量,或一组寄存器.
至斥可以通过使用信号灯或互斥体来实现.
NVRAM(NonvolatileRandom-AccessMemory)非易失随机存取存储器一种在系统失电的情况下仍然能保留数据的随机存取存储器(RAM).
非易失随机存取存储器经常由一个静态RAM和一个长寿电池组成.
OTP参见One-timeprogrammable(一次可编程).
objectcode目标代码一组处理器能读得懂的代码和数据,编译器、汇编程序、链接器和定址器的输出文件都包括目标代码.
objectfile目标代码文件含有目标代码的文件就是目标代码文件,是编译器或汇编程序的输出结果one-timeprogrammable一次可编程任何终端用户只能编程一次的可编程设备,如可编程ROM.
但此术语几乎只用于指代片上可编程ROM的微控制器.
opcode操作码被处理器作为其指令集中指令的一组二进制代码序列operatingsystem操作系统一组使多任务成为可能的软件.
操作系统一般是由一系列函数或软件中断构成的.
操作系统负责决定在某小时刻应该运行某个任务,并控制共享资源的存取.

oscilloscope示波器可用来观察一个或多个电气线路的电压的一种硬件调试设奋.
例如,你可以使用示波器确定目前是否出现了某个中断请求.
PROM(ProgrammableRead-OnlyMemory)可编程只读存储器一种可以用设备编程器进行编程的只读存储器(ROM).
PROM只能被写入一次,所以有时也称作"一次性写入存储器".
parallelprocessing并行处理同时使用两个或多个处理器进行计算的能力.
peripheral外围设备除处理器以外的硬件,通常是存储器或I/O设备.
外围设备可能与处理器在同一个芯片上,这时称为内部外围设备.
physicaladdress物理地址在对存储器或寄存器进行寻址时,放在地址总线上的真实地址.
preamptive占先当有更高优先级的任务就绪时,如果允许正在运行的任务暂停,就称此调度程序是占先的.
非占先调度程序更易实现,但不适用于嵌入式系统.
priority优先级别任务重要程度的标志.
priorityinversion优先级倒置一种不希望出现的软件状态,其中高优先级的任务被延迟,等待存取无法使用的共享资源.
实践中,延迟期间此任务的优先级将被降低.
process进程进程的概念很容易跟线程、任务搞混.
它们之间最重要的区别是:任务是共享内存空间的.
而进程,却有各自独立的内存空间.
进程在多用户操作系统中很常见,但是在嵌入式操作系统中却很少见.
processor处理器微处理器、微控制器和数字信号处理器的通称.
本书中使用此术语的原因是,处理器的实际类型对所描述的嵌入式系统开发影响很小.
processorfamily处理器系列一组相关的处理器,通常是同一厂商的连续几代产品.
例如,Intel的80x86系列始于8086,目前有80186,286,386,486,Pentium等.
一个系列中,后出的通常会与前面的产品保持向后兼容.
processor-independent处理器无关用来描述与运行的处理器平台无关的软件的一个术语.
用高级语言编写的大多数程序是干处理器无关的.
与之相对的是processor-specific.
processor-specific处理器有关用来描述高度依赖所运竹的处理器平台的软件的一个术语.
这些软件的代码通常是用汇编语言编写的.
与之相对的是processor-independent.
profiler一种收集和报告程序执行统计数据的软件开发工具.
这些数据包括每个例程调用的次数和花费的总时间,可用来获知那个例程最为关键,从而需要最好的代码效率.
programcounter程序计数器参见instructionpointer.
RAM(Random-AccessMemory)随机访问内存一种被广泛使用的内存.
其中的内存位置可以按照需要进行读写访问.
RISC(ReducedInstructionSetComputer)精减指令集计算机一种处理器的系列.
RISC处理器通常只能产生固定长度的指争,并需要大量的寄存器.
MIPS处理器就是优异的RISC处理器.
与之相对的是CISC.
ROM(Read-OnlyMemory)只读内存一种被广泛使用的内存,其中的内存位置可以接照需要进行只读访问.
ROMemulatorROM仿真器一种代替(模仿)目标板子上处理器的调试工具.
ROM仿真器很像调试监视器,但它含有自己的与主机的串行或网络连接.
ROMmonitorROM监视器参见debugmonitor.
RTOS(Real-TimeOperatingSystem)实时操作系统一种专门用于实时任务环境的操作系统.
RTOS(Real-TimeOperatingSystem)实时操作系统一种专门用于实时任务环境的操作系统.
racecondition竞争条件程序的结果会受到指令执行顺序影响的一种条件.
竞争条件仅当中断和/或占先可能且存在关键区的情况下,才会产生.
real-timesystem实时系统有时间限制的任何计算机系统、嵌入式系统或其他系统.
以下问题可用于辨别一个系统是否是实时的:响应延迟是否和错误响应一样糟,甚至更糟或者说,如果运算没有按时完成,会发生什么事如果没有什么不好的结果,该系统就不是实时的.
如果因此会使任务失败或造成严重事故,我们通常称之为"硬"实时系统,意思是系统的时间限制非常严格.
介于此两种情况之间的,我们称之为"软"实时系统.
recursion递归指软件的自我调用.
递归在嵌入式系统中通常是应该避免使用的,因为它经常需要大堆栈.
reentrant可重入软件指软件可以同时执行多次.
可重入函数可安全的递归调用,或被多个任务所调用.
使代码可重入的关键在于,确保任何时候存取全局变量或共享寄存器都是互斥的.
register寄存器处理器或外设的一种内存地址.
换句话说,它不是普通的内存.
一般来说,寄存器的每一位都对控制更大的外设有作用.
relocatable可重定位(文件)包含目标代码的文件.
该目标代码已为在目标机上执行基本准备就绪.
剩下的步骤是使用定址器修改代码中剩下的可重定位地址.
处理生成的结果是可执行文件.
resetaddress复位地址处理器刚加电或重新启动的时候运行的第一条指令的地址.
rosetcode复位代码放在启动地址处的一小段代码.
通常是用汇编语言写的,可能就是简单的相当于说一句"跳到启对程序".
resetvector启动向量见Resetaddress.
SRAM(StaticRandom-AccessMemory)静态随机访问内存RAM的一种,SRAM中的数据直到系统关闭电源才丢失.
scheduler调度程序操作系统中一个部分,由它决定下一步被运行哪个任务.
这种决策的做出,是以每个任务是否就绪,它们的相对优先级,以及具体的调度算法为基础的.

semaphore信号灯用于任务间通信的一种数据结构.
信号灯通常是由操作系统提供的.
simulator模拟器一种运行在主机上,模拟目标处理器的调试工具.
模拟器可以在还没有嵌入式硬件的叶候,用来测试软件片断.
不幸的是,试图模拟复杂外围设备的交互作用,往往得不偿失.
softwareinterrupt软件中断一种由软件指令产生的中断.
软件中断通常用于实现断点和操作系统入口点.

与之相对的是Trap(陷阱).
stack堆栈包含后进先出队列的存储器区域,用于存储参数,自动变量、返回地址和其他在函数调用中必须保存的信息.
在多任务环境中,每个任务都生成自己的堆栈.

Gcore(gcorelabs)俄罗斯海参崴VPS简单测试

有一段时间没有分享Gcore(gcorelabs)的信息了,这是一家成立于2011年的国外主机商,总部位于卢森堡,主要提供VPS主机和独立服务器租用等,数据中心包括俄罗斯、美国、日本、韩国、新加坡、荷兰、中国(香港)等多个国家和地区的十几个机房,商家针对不同系列的产品分为不同管理系统,比如VPS(Hosting)、Cloud等都是独立的用户中心体系,部落分享的主要是商家的Hosting(Virtu...

妮妮云80元/月,香港站群云服务器 1核1G

妮妮云的来历妮妮云是 789 陈总 张总 三方共同投资建立的网站 本着“良心 便宜 稳定”的初衷 为小白用户避免被坑妮妮云的市场定位妮妮云主要代理市场稳定速度的云服务器产品,避免新手购买云服务器的时候众多商家不知道如何选择,妮妮云就帮你选择好了产品,无需承担购买风险,不用担心出现被跑路 被诈骗的情况。妮妮云的售后保证妮妮云退款 通过于合作商的友好协商,云服务器提供2天内全额退款,超过2天不退款 物...

CUBECLOUD:香港服务器、洛杉矶服务器、全场88折,69元/月

CUBECLOUD(魔方云)成立于2016年,亚太互联网络信息中心(APNIC)会员,全线产品均为完全自营,专业数据灾备冗余,全部产品均为SSD阵列,精品网络CN2(GIA) CU(10099VIP)接入,与当今主流云计算解决方案保持同步,为企业以及开发者用户实现灵活弹性自动化的基础设施。【夏日特促】全场产品88折优惠码:Summer_2021时间:2021年8月1日 — 2021年8月8日香港C...

内存测试为你推荐
info域名注册淘宝上有的注册info域名十元左右,是不是真的免费网站域名申请那里 可以申请免费的 网站域名啊??域名购买如何申请购买 永久域名网站空间申请网站空间申请万网虚拟主机万网虚拟、专享、独享主机有什么区别?天津虚拟主机天津APP开发的比较专业的公司有哪些?美国虚拟主机购买美国虚拟主机如何购买虚拟主机测评哪一种虚拟主机比较好用?域名邮箱哪个免费域名邮箱最好花生壳域名花生壳域名是什么
国外主机空间 免费国际域名 万网域名证书查询 asp.net主机 主机点评 diahosting 日志分析软件 云图标 web服务器架设软件 一元域名 空间出租 怎样建立邮箱 免费申请个人网站 web服务器安全 卡巴斯基是免费的吗 网络速度 wordpress空间 cdn加速 游戏服务器 dbank 更多