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

程序 0
Barr著于志宏译 作者简介 MichaelBarr是Netrino公司(一个嵌入式系统共享软件和软件工程服务提供商)的创始人兼总裁。
Netrino公司鼓励所有职员通过为杂志撰稿和在业界会议演讲来分享自己的专业知识。
这些资料可以在公司的网站找到。
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:///examples/nutshell/embedded_c/。
建议与评论 我们已尽全力保证本书内容的正确性,但你仍可能发现有些内容不对(甚至可能是我们出了错误!)。
你的建议将帮助我们使下一版更加完美,请告诉我们你找到的错误以及你的建议,写信到: 美国:O'Reilly&Associates,Inc.101MorrisStreetol.CA95472 中国:100080北京市海淀区知春路49号希格玛公寓B座809室奥莱理软件(北京)有限公司 询问技术问题或对本书的评论,请发电子邮件到:info@ 最后,你可以在WWW找到我们: 个人说明和致谢 我曾经很想写一两本书,但是现在我这么做了以后,我必须承认我开始的时候非常的天真。
我甚至不知道要做多少工作、另外还会有多少人被牵扯进来。
不过令我吃惊的是,找一个愿意出版我的书的出版社是如此容易,我原以为这会是很困难的一件事。
从提议写书到出版,这个计划花了两年才完成。
这是因为我一直在全时工作并且希望尽可能保持我的社会生活所致。
要是我早知道我会熬夜到很晚来为最后的底稿而苦恼的话,我也许会放弃我的工作来早点交付这本书。
但过继续工作对这本书很有好处(同时对我的银行账户也有好处)。
这使我有机会对以和很多嵌入式硬件和软件的专家进行广泛的讨论他们巾的很多人通过审阅部分成全部 的书稿为这本书做出了直接的贡献。
我非常感或以下诸位,与我分享了他们的知识并且一直在帮助我的工作:Tony、PaulCabler(和其他来自的很棒的人们),MikeCorish、KevinD'Souza、DonDavis、SteveEdwards、Mikeo、BarbaraFlanngan、JackGanssle、StephenHarpster(他在看完早期书稿后管我叫“断句王”)、JonathanHarris、JimJesen、MarkKohler、AndyKollegger、JeffMallory、IanMiller、HenryNeugauss、ChrisSchanck、BrianSilverman、JohnSnyder、JasonSteinhorn(正是他的流畅的语法素养和技术批评的眼光使这个书值得一读)、IanTaylar、LindseyWereen、JeffWhipple和GregYoung。
我还要感谢我的编辑AndyOram。
要是没有对我最初建议的巨大热情、超乎寻常的耐心和持续的鼓励,这本书将永远不会完成。
最后,我要感谢AplaDharla,感谢她在这个漫长的过程中给予我的支持和鼓励。
MichaelBarr 本章内容: z什么是嵌入式系统z各种实现间的差异zC:最基本的必需品z关于硬件的一些说明 第一章 引言 我想全世界计算机市场也许会有五台。
——ThomasWatson(托马斯·沃森),IBM公司主席,1943 没有人会想在家里放一台计算机。
——KenOlson(肯·奥尔森),DEC公司总裁,1977 最近几十年里最令人惊讶的事,莫过于计算机逐渐占据了人类生活的主要地位。
今天在我们的家里和办公室里,计算机的数量要比使用它们来生活和工作的人还要多,只是这些计算机里有很大一部分我们没有意识到它们的存在罢了。
在这一章里,我将说明什么是嵌人式系统,以及可以在哪里找到它们。
同附会介绍一下嵌人式编程的主题,说明一下为什么本书采用C和C++语言讲述,另外简单介绍一下示例中所用到的硬件环境。
什么是嵌入式系统 一个嵌入式系统(embeddedsystem)就是一个计算机硬件和软件的集合体,也许还包括其他一些机械部件,它是为完成某种特定的功能而设计的。
一个很好的例子就是微波炉。
几乎每个家庭都有一台,并且每天都有上千万台微波炉在被人们使用着,但是很少有人意识到有处理器和软件在帮助他们做饭。
这和家里的个人计算机形成了鲜明的对比。
同样是由计算机硬件和软件,还有机械部件(比如硬盘)组成的,个人计算机却不是用来完成某个特定功能的。
相反它可以做各种不同的事情。
很多人用通用计算机(puter)来区分这一点。
在发货的时候,通用计算机就像一块没有字的黑板,制造商并不知道用户要拿它来做什么。
一个用户可能会用它来做文件服务器。
另一个只用来玩游戏,还有一位可能会用它来写下一部伟大的美国小说。
而嵌入式系统常常是一些更大的系统中的一个组成部分。
比如,现代的轿车或卡车里就包含了很多嵌人式系统。
一个嵌人式系统会被用来控制防刹车锁死,另一个监控车辆的气体排放情况,还有一个用来在仪表板上显示信息。
虽然不是必需的,但在某些情况下,这些嵌人式系统会通过某种通信网络互相连起来。
为了不至于混淆你的思路,有必要指出,通用计算机本身就是由很多嵌入式系统组成的。
比如,我的电脑包含了键盘、鼠标、显示卡、调制解调器、硬盘、软盘和声卡,它们中的每一样都是一个嵌入式系统。
每个设备都包含处理器和相应的软件来完成特定的功能。
比如凋制解调器就是用来在模拟电话线上收发数字信号用的。
正是如此,所有其他的设备也都能归纳出这么一句话来。
如果一个嵌入式系统设计得很完善,那么它的使用者完全可以忽略它内部的处理器和软件的存在。
微波炉、录像机和报时闹钟就是很好的例子。
在某些情况下,用同样的功能的定制集成电路硬件来代替上面所说的处理器和软件,也能做出具有同样功能的设备来。
不过,如果真是这样用纯粹的硬件来设计的话,在灵活性上就会丧失不少了,改几行软件怎么说也要比重新设计一块硬件电路来得方便和便宜。
过去和将来 本章开头定义的嵌人式系统的第一个产品直到1971年以后才出现。
这一年,Intel发布了世界上第一块微处理器,4004,主要被日本的公司用来生产商用计算器。
1969年,请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位 <16KB<$100000 生产成本批量预计的生命周期可靠性 <$10 <100几日、几周或几月可以偶尔故障 中16位 64KB-1MB$100,000~$1,000,000 $10~$1000100~10000几年必须可靠工作 高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),还有一些常见的输入、输出和外设部件。
我选用的电路板是控制系统公司制造的Target188EB。
关于这块电路板和如何获取的信息可以参看附录“的Target188EB”。
如果你可以接触到这个参考硬件的话。
你将能原封不动地使用本书里的例子。
否则,你需要把示例代码移植到你能用到的嵌人式平台上面。
为了这个目的,我尽可能地使示例程序易于移植。
可是读者必须要知道,每一种嵌入式系统的硬件都是不一样的,可能一些例子对地的硬件来说一点意义也没有,比如,把第六章“存储器”里提到的快闪存储器驱动程序,移植到一个不带闪存的板子上就很没意义。
不管怎样,在第五章“接触硬件”里面我还会讲很多东西。
但是首先我们还有很多软件问题需要讨论,这就开始吧。
——————————————————————————————————注2:Intel的80188EB处理器是专门为嵌入式系统修改了设计的80186的 特殊版本,原来的80I86是IBM的第一台个人计算机(PC/XT)使用的8086处理器的一个继承者。
它从来没有被实际使用。
因为当IBM设计下一个型号(PC/AT)的时候选择的是80286。
尽管早期是失败的,近几年来自Intel和AMD的80186却在嵌入式系统里面取得了巨大的成功。
第二章 你的第一个嵌入式程序 本章内容: zHelloWorld!
z闪烁程序z无限循环的作用 注意!此机器不能摸也不能拿。
它的内部在飞速地转动,而且不断发出火花。
它不是傻瓜摆弄的玩意儿。
请把手放在口袋里,站得远远地,放松些,看那闪烁的火花。
——KenOlson(肯·奥尔森),DECC公司总裁,1977 在这一章里我们将通过一个例子直接进入嵌入式编程。
这个例子看起来和其他大多数编程书籍开头的“Hello,world!
”例子差不多。
在讨论代码的时候,我会说明选择特定代码段的理由,并会指出依赖目标硬件的部分。
本章只包含这第一个程序的源码,在接下来的两章里我们会讨论如何创建可执行代码并运行它。
HelloWorld!
好像所有讲述编程的书都用同一个例子来开始,就是在用户的屏幕上显示出“Hello,World!
”。
总是使用这个例子可能有一点叫人厌烦,可是它确实可以帮助读者迅速地接触到在编程环境中书写简单程序时的简便方法和可能的困难。
就这个意义来说,“Hello,World!
”可以作为检验编程语言和计算机平台的一个基准。
不幸的是,如果按照这个标准来说,嵌入式系统可能是程序员工作中碰到的最难的计算机平台了。
甚至在某些嵌入式系统中,根本无法实现“Hello,World!
”程序。
即使在那些可以实现这个程序的嵌入式系统里面,文本字符串的输出也更像是目标的一部分而不是开始的一部分。
你看,“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. /******************************************************************/ void main(void) { while
(1) { toggleLed(LED_GREEN);
/*ChangethestateoftheLED.*/ delay(500); /*Pausefor500millisenconds.*/ } }/*main()*/ toggleLed 在的电路板上,有两个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-1电路板上的LED P2LTCH寄存器位于I/O空间的一块特定内存里,偏移为OxFF5E。
不幸的是,80x86处理器的I/O空间里的寄存器只能使用汇编语言指令in和out来操作。
C语言没有内嵌的类似操作。
最接近的替换函数是定义在面向PC平台的头文件dos.h里的inport()和outport()。
理想情况下,我们可以包含这个头文件并从我们的嵌人式程序里调用这两个库函数。
不过,因为它们是DOS编程库的一部分,我们必须要考虑到最坏的情况:它们在我们的系统上不工作。
最起码的是,我们在第一个程序里不应该依赖它们。
下面列出了面向电路板并且不依赖库例程的toggleLed例程的实现。
实际的算法是很简单的:读P2LTCH寄存器的内容,切换要控制的LED的相应位,再把新的值写回寄存器。
你会注意到尽管这个例程是用C书写的,而实际的控制部分是用汇编语言实现的。
这种简便的方法叫内嵌汇编语言(inlineassembly)。
它一方面使程序员避开了复杂的C函数凋用和参数的传递和转换过程,同时使她可以随意地使用汇编语言来工作(注2)。
#defineP2LTCH0xFE5E/*TheoffsettheP2LTCHregiser.*/ ————————————————————————————————————————————— 注2:不幸的是,各种编译器的内嵌汇编语法是不一样的。
我在示例中使用的是BorlandC++编译器的格式。
Borland的内嵌汇编格式非常好,它主持在汇编行里引用用C代码定义的变量和常数。
/******************************************************************/ *FunctiontoggleLed() *Description:TogglethestateofoneorbothLEDs. *Notes:Thisfunctionisspecificto’sTarget188EBboard. *Returns:Nonedefined. /******************************************************************/ void toggleLed(unsigned
charledMask) { asm{ movdx,P2LTCH/*Loadtheaddressoftheregister.*/ inal,dx /*Readthecontentoftheregister.*/ movah,ledMask/*MovetheledMaskintoaregister.*/ xora1,ah /*Toggletherequestedbits.*/ outdx,al}}/*toggleLed()*/ /*Writethenewregistercontents.*/ 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()的调用包在一个无限循环里面。
本章内容: zHelloWorld!
z闪烁程序z无限循环的作用 第三章 编译、链接和定址 如果我喜欢一个程序那么我就应该和同样喜欢它的人分享它,我认为这是一条黄金法则。
软件销售商们意图分化和征服用户,使每一个用户同意不和别人共享软件。
我拒绝用这种方式打破其他用户的团结。
我不认为签署一份软件授权许可协议或者不公开协议是有良知的事。
因此我能够继续光荣地使用电脑。
我决定要汇集足够的自由软件, 这样我就不会使用任何非由的软件了。
——RichardStallman(理查德·斯多曼),GNU工程创始人。
《GNU宣言》 本章中,我们逐步学习使你的程序可以在嵌入式系统上运行的每个步骤,我们也会讨论相关的开发工具,并了解如问创建在第二章“你的第一个嵌人式程序”里讲述的闪烁LED程序。
在我们开始之前,我将讲清楚一件事,嵌入式系统编程和你以前从事的编程工作实质上并无区别。
唯一改变的是每一个硬件平台都是独特的。
不幸的是,一个不同点就会导致许多附加的软件复杂性。
这也是你必须要比以前格外注意软件创建过程的原因。
创建过程 当目标平台(targetplatform,注1)选定之后软件开发工具可以自动做很多的事情。
这个自动过程是可能的,因为这些工具可以发掘程序运行的硬件和操作系统平台的特性。
例如,如果你的所有程序将执行在运行DOS的IBM兼容PC上,那么你的编译器就可以自动处理(因此也使你无法得知)软件创建过——————————————————————————————————注1:在这种方式下,术语“目标平台”最好理解为构成运行你的软件的基本运行环境的硬件和操作系统的统一体。
在某些情况下,嵌入式系统并没有操作系统,那么你的目标平台就只是运行你的程序的处理器。
程的某些方面。
而在另一方面,嵌入式软件开发工具很少时目标平台做出假定。
相反,用户必须给出更清晰的指令来告知这些工具有关系统的具体知识。
把你的嵌入式软件的源代码表述转换为可执行的二进制映像的过程,包括三个截然不同的步骤。
首先,每一个源文件都必须被编译或汇编到一个目标文件(objectfile)。
然后.第一步产生的所有目标文件要被链接成一个目标文件,它叫做可重定位程序(relocatableprogram)。
最后,在一个称为重定址(relocation)的过程中,要把物理存储器地址指定给可重定位程序里的每个相对偏移处。
第三步的结果就是一个可以运行在嵌入式系统上的包含可执行二进制映像的文件。
图3-1说明了上述的嵌人式软件开发过程。
在图中,三个步骤是由上至下表示的,在圆角矩形框里说明了执行该步骤所用到的工具。
每一个开发工具都以一个或多个文件作为输人共产生一个输出文件。
本章接下来的部分会说明关干这些工具和文件的更详细的内容。
图3-1嵌入式软件开发过程嵌入式软件开发过程的每一个步骤都是在一个通用计算机上执行的软件的转换过程。
为了区别这台开发计算机(通常会是一台PC或Unix工作站)和目标嵌入式系统,我们称它作主机。
换句话说,编译器、汇编器,链接器和定址器都是运行在主机上的软件。
而不是在嵌入式系统上运行。
可是,尽管它们事 实上在不同的计算机平台上运行,这些工具综合作用的结果是产生了可以在目标嵌人式系统上正确运行的可执行二进制映像。
图3-2显示了这种功能的划分。
图3-2主机和目标机的划分在本章和下一章里我将使用GNU工具(编译器、汇编器、链接器和定址器)作为示范。
这些工具在嵌入式软件开发人员中使用极为普遍,因为可以免费得到它们(甚至源代码也是免费的),而且它们支持大多数最流行的嵌人式处理器。
我会用这些特定工具的特性来说明一些讨论到的基本概念。
一旦你领悟以后,同样的概念就可以应用到任何相类似的开发工具上。
编译 编译器的工作主要是把用人可读的语言所书写的程序,翻译为特定的处理器上等效的一系列操作码。
在这种意义上,一个汇编器也是编译器(你可以称之为“汇编语言编译器”),但是它只执行了一个简单地逐行把人可阅读的助记符翻译到对应操作码的过程。
这一节里的所有内容都同样适用于编译器和汇编器。
这些工具综合在一起形成了嵌入式软件开发过程的第一个步骤。
当然,每一种处理器都有它独特的机器语言,所以你需要选择一个可以为你的目标处理器产生程序的编译器。
在嵌人式系统的情况下,编译器几乎总是在主机上运行,在嵌入式系统本身运行编译器也没什么意义。
一个像这样运行在一个计算机下台上并为另一个平台产生代码的编译器叫做交叉编译器 (piler)。
使用交叉编译器是嵌人式软件开发的固定特征。
GNUC/C++编译器()和汇编器(as)可以被配置为本地编译器或交叉编译器。
用作交叉编译器的时候这些工具支持非常多的主机·目标机组合。
表3-1列出了最常见的一些得到支持的主权和目标机。
当然,主机和目标机的选择是相互独立的,这些工具可以被配置成住意的组合。
表3-1GNU编译器所支持的主机和目标机 主机平台 目标机 DECAlphaDigitalUnixHP9000/700HP-UXIBMPowerPCAIXIBMRS6000AIXSGIIrisIRIXSunSPARCSolarisSunSPARCSunOSX86Windows95/NTX86RedHatLinux AMD/Intelx86(仅为32位)FujitsuSPARCliteHitachiH8/300,H8/300H,H8/SHitachiSHIBM/MotorolaPowerPCInteli960MIPSR3xxx,R4xx0MitsubishiD10V,M32R/DMotorola68kSunSPARC,MicroSPARCToshibaTX39 不管输入文件是C/C++,汇编还是什么别的,交叉编译器的输出总是一个目标文件。
这是语言翻译过程产生的包含指令集和数据的特定格式的二进制文件。
尽管目标文件包含了一部分可执行代码,它却是不能直接运行的。
实际上,目标文件的内部结构正强调了更大的程序的不完备性。
目标文件的内容可以想象成一个很大的、灵活的数据结构。
这个文件的结构通常是按照标准格式定义的,比如“通用对象文件格式”(COFF)和“扩展的链接器格式”(ELF)。
如果你计划使用不止一个编译器(就是说你用不同的源代码语言写你的程序的各个部分),那么你应该确定它们产生相同的目标文件格式。
尽管很多编译器(特别是那些运行在Unix平台上的)支持类似COFF和ELF的标难格式(两者都支持),还是有一些编译器只产生专有格式的目标文件。
如果你使用后一类编译器,你也许会发现你将不得不从同一个供货商处购买所有其他的开发工具。
大多数目标文件以一个描述后续段的头部开始每一段包含一块或几块源于源文 件的代码或数据,不过,这些块被编译器重新组合到相关的段中。
比如,所有代码块都被收集到叫做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=.;}>rom bss:{ _BssStart=.;*(.bss) _BssEnd=.;} _BottomOfHeap=.;/*Theheapstartshere.*/_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,所以只有映像较高的一半是有意义的。
你将在下一章里看到如何下载并执行这个内存映像。
创建闪烁程序 不幸的是,因为我们使用的板子作为我们的参考平台,我们不能使用GNU工具来创建示例代码。
作为替换我们使用Borland的C++编译器和TurboAssembler汇编器。
这些工具可以在任何基于DOS和Windows的PC上运行(注 3)。
如果你有一个的板子来做实验,现在就可以把它安装起来并在你的主机上安装Borland的开发工具。
(参看附录“的Targetl99EB”来取得订货信息。
)我用的是3.1版的编译器,运行在一台基于Windows95的PC上。
不过,任问可以为80186处理器生成代码的Borland工具都可以使用。
正如我所实现的那样,闪烁LED示例包含三个源文件模块:led.c和blink.c和startup,asm。
创建过程的第一步是编译这两个文件。
我们需要使用的命令行选项有:-c说明“编译,但是不要链接”,-v说明“在输出文件里包含符号调试信息”-ml说明“使用大内存模式”。
还有-l说明“目标是80186处理器”。
这里是实际命令: -c-v-ml-lled.c-c-v-ml-lblink.c 当然,要执行这些命令,.exe必须在你的PATH路径里并且这两个源文件在当前目录下。
换句话说,你应该在Chapter2子目录下。
每个命令的结果都创建了一个和.c文件有同样前缀而后缀是.obj的文件。
所以如果一切顺利的话,在工作目录里会出现两个文件——led.obj和blink.obj。
尽管看起来在我们的例子里只有两个目标文件需要链接,实际上是有三个。
这是因为我们必须给C程序链接某个启动代码。
(参看本章前面的选读”启动代码”。
)电路板使用的示例启动代码在Chapter3子目录的文件startup.asm里。
为了把这段代码编成目标文件,进入这个目录并发出如下命令: tasm/nxstartup.asm 结果是这个目录里多了一个startup.obj文件。
实际把三个目标文件链接在一起的命令如下。
需要注意在这个例子中命令行里目标文件出现的次序是有讲究的:启动代码必须放在第一个才能正确链接。
——————————————————————————————————注3:应该注意Borland的C++编译器不是专门为嵌入式软件开发人员设计的。
相反它是设计来为使用80x86处理器的PC生成基于DOS和Windows的程序的。
不过,通过使用特定的命令行选项可以允许我们指定特定的80x86处理器——比如80186,这样就可以用这个工具作为类似的电路板的嵌入式系统的交叉编译器。
tlink/m/v/s..\Chapter3\startup.objled.objblink.objblink.exe,blink.map 作为执行tlink命令的结果,Borland的TurboLinker链接器产生两个新文件:blink.exe和blink.map。
第一个文件包含了可重定址的程序,第二个文件包含了人可阅读的程序映像。
如果你以前从来没有看过一个映像文件,记着在往下读之前看一看这个文件。
它提供了类似前面讲的链接器脚本所包含的信息。
只不过这里是结果,所以包含了每一段的长度和在可重定址程序里的公共符号的名字和位置。
还有一个工具要用来使闪烁LED程序可以运行,它就是定址器。
我们要用的定址器是随板子附带的SourceView开发和调试程序包的一部分提供的。
因为这个工具是为这个特定的嵌入式平台设计的,所以它没有更通用的定址器带的很多选项(注4)。
实际上,只有三个参数:可重定址二进制映像的名字、ROM的起始地址(以十六进制形式提供)和目标RAM的总长度(以千字节单位提供): tlink/m/v/s..\Chapter3\startup.objled.objblink.objblink.exe,blink.map 作为执行tlink命令的结果,Borland的TurboLinker链接器产生两个新文件:blink.exe和blink.map。
第一个文件包含了可重定址的程序,第二个文件包含了人可阅读的程序映像。
如果你以前从来没有看过一个映像文件,记着在往下读之前看一看这个文件。
它提供了类似前面讲的链接器脚本所包含的信息。
只不过这里是结果,所以包含了每一段的长度和在可重定址程序里的公共符号的名字和位置。
还有一个工具要用来使闪烁LED程序可以运行,它就是定址器。
我们要用的定址器是随板子附带的SourceView开发和调试程序包的一部分提供的。
因为这个工具是为这个特定的嵌入式平台设计的,所以它没有更通用的定址器带的很多选项(注4)。
实际上,只有三个参数:可重定址二进制映像的名字、ROM的起始地址(以十六进制形式提供)和目标RAM的总长度(以千字节单位提供): tcromblink.exeC000128SourceVIEWBorlandCROMRelocatorv1.06Copyright(C)ControlSystemsLtd1994RelocatingcodetoROMsegmentC000H,datatoRAMsegment100HChangingtargetRAMsizeto1I8KbytesOpening'blink.exe'...Startupstackat0102:0402PSPProgramsize550Hbytes(2K)TargetRAMsize20000Hbytes(128K)Targetdatasize20Hbytes(1K)Creating'blink.rom'...ROMimagesize55HHbytes(2K) tcrom定址器给出了给每个段指定了基地址的可重定址输人文件的内容,并产生文件blink.rom。
这个文件包含了一个已经绝对定址的二进制映像,它可以直接调入到ROM里。
不过我们不是用一个设备编程器把它写人到ROM里,相反我们将生成这个二进制映像的一个ASCII版本并通过一个串行口把它下我到ROM型。
要做到这一点我们还是使用提供的一个工具,叫做bin2hex。
下面是此命令的语法: bin2hexblink.rom/A=1000 这个额外的步骤生成一个新的文件blink.hex,它包合和blink.rom一样的内容,不过是以一种叫做Intel十六进制格式(IntelHexFormat)的ASCII格式表示的。
——————————————————————————————————注4:不管怎样,它是免费的,这可比更通用的定址器便宜多了。
本章内容: z在ROM中的时候…z远程调试器z仿真器z模拟器和其他工具 第四章 下载和调试 我现在还清楚地记得那一刹那,我明白了从此以后我生活的很大部分将用来找我自己程序的错误, ——MauriceWilkes,剑桥大学计算机实验室主任,1949 当你已经在主机上有了一个可执行二进制映像文件的时候,你就需要有一种途径来把这个映像文件下载到嵌入式系统里来运行了。
可执行二进制映像一般是要下载到目标板上的存储设备里并在那里执行。
并且如果你配备了适当的工具的话,还可以在程序到设置断点或以一种不干扰它的方式来观察运行情况。
本章介绍了可用于下载、运行和调试嵌入式软件的各种技术。
在ROM中的时候…… 下载嵌入式软件的最明显的方式,是把二进制映像载人一片存储芯片并把它插在目标板上。
虽然一个真正的只读存储芯片是不能再覆盖写人的,不过你会在第六章“存储器”里看到,嵌人式系统通常使用了一种特殊的只读存储器,这种存储器可以用特殊的编程器来编程(或重新写人编程)。
编程器是一种计算机系统,它上面有各种形状和大小的芯片插座,可以用来为各种存储芯片编程。
在一个理想的开发条件下,设备编程器应该和主机接在同一个网络上。
这样,可执行二进制映像文件就很容易传给它来对ROM芯片编程。
首先把映像文件传到编程器然后把存储芯片插入大小形状合适的插座里并从编程器屏幕上的菜单里选择芯片的型号。
实际的编程过程可能需要几秒钟到几分钟,这要看二进制映像文件的大小和你所用的芯片型号来定。
编程结束以后,你就可以把ROM插进板上的插座了。
当然,不能在嵌入式 系统还加电的时候做这件事。
应该在插入芯片之前关掉电源,插入之后再打开。
一旦加电,处理器就开始从ROM里取出代码并执行。
不过,要注意到每一种处理器对第一条指令的位置都有自己的要求。
例如,当Intel80188EB处理器复位以后,它就会取位于物理地址FFFF0h的指令来执行。
这个地址叫复位地址,位于那里的指令就叫复位代码。
如果你的程序看起来像是没有正确运行,那可能是你的复位代码出了点问题。
你必须保证ROM里你的二进制映像格式要遵从目标处理器的复位要求。
在开发过程中,我发现在复位代码执行完后打开板子上的一个LED非常有用,这样我一眼就知道我的新ROM程序是不是满足了处理器的基本要求。
——————————————————————————————————注意:调试技巧#1:一个最简单的调试技巧就是利用LED来相示成功或者失败。
基本的思路是慢慢地从LED驱动代码过渡到更大的程序。
换句话说,你先从启动地址处的LED驱动代码开始。
如果LED亮了,你就可以编辑程序,把LED驱动代码挪到下一个运行标记的地方。
这个方式最适合像启动代码那样的简单以线性执行的程序。
如果你没有本章后面提到的远程调试器或者任何其他调试工具的话,这也许是你唯一的调试办法了。
—————————————————————————————————— 电路板包含一个特殊的在线可编程存储器,叫做快闪存储器(简称闪存),它可以在不从板上移走的情况下编程。
实际上,板上的另外一块存储器中已经包含了可以对这个快闪存储器编程的功能。
你知道吗,电路板上实际带了两个只读存储器,一个是真正的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调试监控器命令 命令 请求格式 读寄存器写寄存器读某地址数据写某地址数据启动/重启执行从某地址开始执行单步执行从某地址开始单步执行重置/中止程序 GGdatamaddress,lengthMaddress,lenth:dataccaddressssaddressk 响应格式 dataOKdataOKSsignalSsignalSsignalSsignalnoresponse 远程调试器是嵌入式软件开发里最常用到的下载和测试工具。
这主要是因为它们一般比较便宜。
嵌入式软件开发人员已经有了所需的主机了,何况一个远程调试器的价格并不会在全套跨平台开发工具(编译器、链接器、定址器等等)的价格上增加多少。
还有,远程调试器的供应商们通常会提供他们的调试监控器的源代码,以增加他们的用户群。
电路板在交付的时候在快闪存储器里包含了一个免费的调试监控器。
和提供的主机软件一起使用,这个调试监控器就可以把程序直接下载到目标权的RAM里并运行。
你可以用tload工具来完成这一工作。
按照“SourceVIEWforTarget188EBUser'sManual”的指示简单地把SourceVIEW用行通信适配器接到目标位和主机上,然后在主机PC上执行下述命令: tload-gblink.exeSourceViewTargetLoaderv1.4Copyright(c)ControlSystemsLtd1994Opening'blink.exe'...downloadsize750Hbytes(2K)CheckingCOM1(pressESCkeytoexit)...Remoteident:TDR188EBversion1.02DownloadessfulSending'GO'mandtotargetsystem-g选项告诉调试监控器程序下载一结束就马上开始运行。
这样一来,运行的就是和ROM里的程序完全对应的RAM里的程序了。
在这种情况下,我们也许会以可重定址程序来开始,那么tload工具也会自动地在RAM里第一个可利用的地址处为我们的程序重新定址。
对于远程调试的目的,的调试监控器可以用Borland的TurhoDebugger做前端。
然后TurboDebugger就可以单步执行你的C/C++和汇编语言程序、在程序里设置断点,并可以在程序运行时监控变量、寄存器和堆栈(注1)。
下面是你可能用来启动一个对闪烁LED程序的调试会话的命令:tdrer-3.1TargetDebuggerVersionChangerv1.2Copyright(c)ControlSystemsLtd1994CheckingCOM1(pressESCkeytoexit)...Remoteident:TDR188EBversion1.02TDR88setforTDversion3.1 td-rp1-rs3blink.exeTurboDebuggerVersion3.1Copyright(c)1988,92BorlandInternationalWaitingforhandshakefromremotedriver(Ctrl-Breaktoquit) tdr命令实际是调用另外两个命令的一个批处理文件。
第一个命令告诉板上的调试监控器你用的是哪个版本的TurboDebugger,第二个才实际调用了TurboDebugger。
每一次用板启动一个调试会话的时候都要发出这两条命令,——————————————————————————————————注1:实际的交互过程和作用TurboDebugger调试一个DOS或Windows程序没有什么不同。
tdr.bat批处理文件只是用来把它们组合成一个单一的命令。
这里我们再一次使用了程序的可重定址版本,因为我们要把程序下载到RAM里井在那里执行它。
调试器启动选项-rpl和-rp3设置了到调试监控器的通信链路的参数。
-rpl代表“remote-port(远程端口)=1”(COM1).-rp3代表“remote-speed(远程速率)=3”(38,400波特率),这些是同调试监控器通信所要求的参数,在建立了和调试监控器的联系之后,TurboDebugger就可以开始运行了。
如果没成功的话可能是串行连接出了问题。
把你的安装过程和SourceView用户手册中的描述对照一下吧。
一旦进人TurboDebugger,你就会看到一个对话框显示“Programoutofdateonremote,sendoverlink?(远程的程序已过期,是否通过链路发送?)”,选择“Yes”后,blink.exe的内容就会被下载到目标RAM里。
然后调试器会在main处设置第一个断点并指示调试监控器运行程序到此处。
所以你现在看到的就是main的C源代码,一个光标指示着嵌入式处理器的指令指针正指向这个例程的人口点。
使用标准的TurboDebugger命令,你可以单步执行程序、设置断点、监视变量和寄存器的值、做凋试器允许的任何事情、或者可以按下F9立即运行程序的剩下部分。
这样做了以后。
你就能看见板上的绿色LED开始闪烁了。
确认程序和调试器都工作正常之后,按下板上的复位开关来复位嵌人或处理器,然后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数据是如何在里面流动的? 例如,假设你是一个调制解调器涉及队伍的软件开发人员,并且刚从硬件设计人员那里拿到一块早期的原型电路板。
因为你已经对调制解调器比较熟悉,所以这块板子的主要目标和其中的数据流行你可能相当熟悉。
这块板子的目的是通过模拟电话线发送和接收数据。
硬件从一组电连接上读取数字信号然后转换程模拟信号传到相连的电话线上。
当从电话线上读到模拟数据并输出数字信号的时候数据也会反方向流动。
尽管大多数系统的目的会很明显,可是数据流就可能不会是这样。
我发现一份数据流图可以帮助你快速理解。
如果你幸运的话,硬件所带的文档重会有你所需要的一整套方框图,而且你会发现它会帮助你创建你自己的数据流图。
这样,你就可以不去理会那些和系统数据流无关的硬件元器件了。
对于电路板,硬件不是面向某一特殊应用设计的,所以为了本章后面的讨论,我们必须想象它有一个设计目的。
我们可以假定这个板子是为了一个打印共享器而设计的。
打印共享器可以允许两台计算机共用一台打印机。
这个设备通过串口来连接每台计算机,通过并行口连接打印机,然后这两台计算机就可以向打印机发送文件了,不过同一时刻只能有一台计算机使用。
为了说明打印共享器的数据流向,我画了图5-
1。
(只画出了板上和这个应用相关的部分。
)通过看这张图,你可以很快地想象以下这个系统地数据流。
从任一串行口接收要打印地数据并保存在RAM里,直到打印机可以接受更多地数据,然后通过并行口把数据送给打印机。
ROM里放了控制这一切地软件。
既然画好了方框图,就别急着把它揉一揉扔掉了,相反在整个项目进行中应已知留着以备参考。
我建议使用一个项目记录本或装订夹,并把这张数据流图作为第一页。
随着你使用这块硬件的工作的进展,把你了解到的所有东西都记到记录本上,你也许还会希望保持有关软件设计和实现的记录。
一个项目记录本不仅在你开发软件时有用,而且在项目结束后也一样。
当你需要对你的软件做一些改动,或者在几个月或几年后做类似工作的时候,你就会很感谢自己当年为保持一份记录而做的额外努力。
图5-1打印共享器的数据流向 如果你在读完硬件文档后,对整体情况还有什么疑问的话,你可以去询问一位硬件工程师来取得帮助。
如果你还不认识这个硬件的设计人员的话,先花几分钟把你自己介绍一下,要是有时间的话,可以请他吃午饭或在工作以后送给他一只玩具熊(你可以不必讲这个项目!)。
我发现很多软件工程师和硬件工程师沟通起来有困难,反过来也一样。
在嵌入式系统开发中,软件人员和硬件人员的交流是特别重要的。
检查一下环境 经常把自己设想成处理器往往是很有帮助的,因为处理器是最终需要你指示来运行你的软件的部件。
想象一下处理器可能是什么样子:处理器看起来会像是什么?如果你从这个角度想的话,你很快就会发现处理器有很多伙伴,在电路板上还有很多硬件部件,而处理器直接和它们进行通信。
这一节里你将学习认识并找到它们。
首先要知道有两种基本类型:存储器和外设。
很明显,存储器是用来存取数据和代码的。
但是你也要考虑外设是什么。
外设是和外部进行交互(I/O)或者完成某一特定硬件功能的特殊硬件设备。
例如,嵌入式系统里最常见的两种外设是串行口和计时器。
前者是一个I/O设备,后者基本上是一个计数器。
Intel80x86和其他一些处理器家族通过两个独立的地址空间来和这些存储器和外设打交道。
第一个地址空间叫存储空间,是用来在取存储器设备的;第二个只为外设保留,叫做I/O空间。
不过,硬件工程师也可以把外设放在存储空间里,这时,我们把这些外设称作存储器映像的外设。
从处理器的角度看,存储器映像的外设和存储设备非常相像。
不过,一个外设和一个存储器的功能有明显的不同。
外设下是简单地存储传给它的数据,而是把它翻译成一条命令或者要以某种方式处理的数据。
如果外设位于存储空间的话,我们就说这个系统有存储器映像I/O。
嵌入式硬件的设计人员往往喜欢只采用存储器映像I/O,因为这样做对硬件和软件开发人员都很方便。
它对硬件开发人员有吸引力是因为他可以因此而省去I/O空间,同时省去了相关的连线。
这也许不会显著地降低电路板的生产成本,但是会降低硬件设计的复杂性。
存储器映像外设对程序员也很有用,他可以更方便、更有效地使用指针、数据结构和联合来和外设进行交互(注1)。
存储器映射 所有的处理器都在存储器里存放它们的程序和数据。
有时存储器和处理器在同一个芯片里,不过更常见的是存储器会位于外部的存储芯片中。
这些芯片位于处理器的存储空间里,处理器通过叫做地址总线和数据总线的两组电子线路来和它们通信。
要读或写存储器取的某个位置,处理器先把希望的地址写到地址总线上,然后数据就会被传送到数据总线。
在你了解一块新的电路板的时候,可以创建一张表来显示在储空间里每个存储设备和外设的名字和地址范围。
组织一下这张表,让最低的地址位于底部,最高的地址位于顶端。
每次往存储器映射里添加一个设备的时候,按照它在内存里的大概位置来放进表内并用十六进制标出起始和结束地址。
在往存储器映射图里插入所有的设备以后,记着用同样的方式把没利用的存储区域也标记出来。
——————————————————————————————————注1:如果P2LTCH寄存器是存储器映像的话,toggleLed函数就连一行汇编代码都用不着。
回过头来再看一下图5-1所示的电路板的方框图,你会发现有三个设备连在地址和数据总线上。
它们分别是RAM、ROM和一个标着“Zilog85230串行控制器”的神秘设备。
提供的文档说RAM位于存储器映射的底端并向上占据了128KB的存储空间。
ROM则位于存储器映射的顶部,井向下拓展了256KB的存储空间。
但是这块区域实际上包含两片ROM:一个EPROM和一个闪速存储器,并且分别具有128KR的容量、第三个设备,Zilog85230串行控制器,是一个存储器映射的外设,它的寄存器的寻址范围在70000h和72000h之间。
图5-2所示的存储器映射显示了对处理器而言这些设备的寻址范围。
在某种意义上,这就是处理器的”通讯录”。
就像你在生活中要维护一个名字和地址的列表一样,你也要为处理器维护一张类似的表。
存储器映射图里包含了可以从处理器的存储空间访问的每个存储芯片和外设的人口。
可以证明这张表是关于系统的信息里最重要的部分,并且应该随时更新并作为项目的永久记录的一部分。
图5-2电路板的存储器映射对于每一块新的电路板来说,你应该创建一个头文件来描述它的特性,这个文件提供了硬件的一个抽象接口。
在实际中,它使你可以通过名字而不是地址来引用板子上的各种设备。
这样做带来的一个额外的好处,是使你的应用软件更加容易移植。
如果存储器映射发生了变化——例如128KB的RAM被移走了——你只需要改变面向电路板头文件中相关的几行,然而重新编译你的程序。
随着本章的进行,我会告诉你如何来为电路板创建一个头文件。
这个文件的第一部分就列在下面,这部分内容描述了存储器映射。
它和图5-2中所表示的最大的区别是地址的格式。
选读部分“指针和地址”解释了原因。
/******************************************************** *
MemoryMap *BaseAddressSizeDescription *--------------------------- *0000:0000h 128KSRAM *2000:0000h Unused *7000:0000h ZilogSCCRegisters *7000:1000h ZilogSCCInterruptAcknowledge *7000:2000h Unused *C000:0000h 128KFlash *E000:0000h 128KEPROM ********************************************************/ #defineSRAM_BASE(void*)0x00000000#defineSCC_BASE(void*)0x70000000#defineSCC_INTACK(void*)0x70000000#defineFLASH_BASE(void*)0xC0000000#defineEPROM_BASE(void*)0xE0000000 I/O映射 如果存在独立的I/O空间的话,那就需要像完成存储器映射一样也要为电路板创建一个I/O映射。
这个过程完全一样。
简单地创建一张包含外设名字和地址范围的表,并组织一下把低地址放在底端。
典型地,I/O空间里的大部分是未利用的因为大多动外设只有几个寄存器。
图5-3显示了电路板的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-3电路板的I/O映射 在为你的板子创建头文件的时候I/O映射也很有用。
I/O空间的每个区域直接映射到一个叫基地址的常数。
上面的I/O映射到常数的翻译如下表所示: /********************************************** * *I/OMap * *BaseAddressDescription *--------------------------------------- *0000h Unused *FC00h SourceVIEWDebuggerPort(SVIEW) *FD00h ParallelI/OPort(PIO) *FE00h Unused *FF00h PeripheralControlBlock(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显示了电路板的中断映射。
表5-1电路板的中断映射 中断类型 引用设备
8 Time/Counter#
0 17 Zilog85230SCC 18 Time/Counter#
1 19 Time/Counter#
2 20 串行口接收 21 串行口发送 我们的目标依然是把这张表里的信息翻译成对程序员有用的形式。
在创建了——————————————————————————————————注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处理器 板上使用的处理器是Intel80188EB,一个80186的微控制器版本。
除了CPU以外,80188EB还包含一个中断控制单元、两个可编程I/O口、三个定时器/计数器、两个串行口、一个DRAM控制器和一个片选单元。
这些额外的硬件设备位于同一个芯片里并被当作片内外设来使用。
CPU通过内部总线可以和片内外设通信并直接控制它们。
尽管这些片内外设是截然不同的硬件设备,它们都用作80186CPU的很小的扩展。
软件可以通过读写被叫做外设控制块(PCB)的一个256字节的寄存器区来控制它们。
你也许还记得我们在第一次讨论存储器和I/O映射的时候碰到过这个块。
PCB缺省地位于I/O空间里,从地址FF00h开始。
不过要是愿意的话,PCB也可以被重定位到I/O或存储器空间里的任何方便的地址处。
每一个片内外设的控制和状态寄存器都位于相对于PCB基地址的固定偏移处。
在80188EB微处理器用户手册里可以查到每一个寄存器的确切偏移地址。
为了把你的应用软件里和这些细节隔离开,把你会用到的寄存器的偏移地址包合到你的板子的头文件里会是一个很好的做法。
我已经为电路板做了这个工作,不过了面只显示了会在后面章节里讨论到的寄存器: /***********************************************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(专用集成电路)。
对于电路板,这张表只包含三个部件:Zilog85230串行控制器、并行口和调试口。
你应该拿到列表上每个设备的用户于用或数据手册。
在项目的前期,你阅读这些文档的目的是要了解这些设备的基本功能。
这些设备做些什么?哪些寄存器被用来发命令和取得结果?这些寄存器里下同的位和字段的意义是什么?如果这个设备会产生中断的话,什么时候产生?如何得知或清除一个设备的中断? 当你设计嵌入式软件的时候,你应该是着按设备来分割程序。
通常为扩展外设结合一个叫做设备驱动程序的软件模块是个不错的主意。
这个工作只是构建一个控制外设执行的软件例程的集会从而把应用软件和具体的硬件设备隔离开来。
我会在第七章“外设”里详细介绍设备驱动程序。
初始化硬件 接触你的新硬件的最后一步是写一些初始化程序。
这是你和硬件发展一种紧 密的工作关系的最好机会,特别是如果你希望用高级语言编写剩下的软件的活。

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

标签: #传真机 #程序开发 #怎么做 #密码 #程序 #php #网页制作 #后台程序