Table of Contents

作者简介

书名页

版权页

目录

推荐序

博客真情推荐

作者序

导读

新版特色

本书定位

本书人物及背景

前情提要

章节简介

延伸学习

Chapter 1　系统·嵌入·硬件

01-01　Welcome on board!

01-02　嵌入式系统开发团队

01-03　老调重弹：何谓嵌入式系统？

01-04　限制！限制！限制！

01-05　基本职能：老鸟也曾是菜鸟

01-06　工作内容：做个工程师，而非程序工人

Chapter 2　嵌入式项目管理

02-01　菜鸟啊！要立大志！

02-02　项目管理基本概念

02-03　项目生命周期五大阶段

02-04　项目范围（Scope）管理

02-05　项目进度（Time/Schedule）管理

02-06　项目成本（Cost）管理

02-07　项目质量（Quality）管理

02-08　项目人力资源（Human Resource）管理

02-09　项目沟通（Communication）管理

02-10　项目风险（Risk）管理

02-11　项目采购/合约管理

02-12　项目配置（Configuration）管理

02-13　企事业与组织对项目的影响

Chapter 3　嵌入式系统开发项目生命周期：项目启动与规划

03-01　嵌入式系统项目简介

03-02　初期规划：是否该接这个项目？

03-03　进度规则：ASAP？

03-04　规格规划：请接受这个不完美的世界

03-05　人力规划：营级组织、连级人力

03-06　成本规划：一切都是为了Cost Down

03-07　质量策略规划

Chapter 4　嵌入式系统开发项目生命周期：设计、执行与结项

04-01　动手之前：确定项目的执行原则

04-02　设计阶段：拟定作战计划

04-03　产品规格设计

04-04　硬件设计

04-05　系统设计

04-06　测试计划设计

04-07　风险评估

04-08　设计文件的重要性

04-09　开始实作之前：设计审查

04-10　实作阶段：执行所有设计

04-11　冲突不断：协同作战有多难？

04-12　产品化

04-13　无间道：项目可有close的一天？

Chapter 5　实作你的第一个嵌入式系统

05-01　嵌入式系统的开发环境

05-02　无痛起步：善用Sample Code

05-03　如何确定程序有执行?

05-04　标准C库函数可以用吗？

05-05　总结

Chapter 6　实作嵌入式系统平台

06-01　系统与平台

06-02　系统架构设计

06-03　API与程序风格设计

06-04　嵌入式操作系统：OS在哪里？

06-05　模拟器

06-06　Source Tree设计与程序风格典范

06-07　SDK

06-08　系统架构设计实例分析

Chapter 7　构建良好的嵌入式系统开发环境

07-01　开发工具

07-02　makefile &批处理文件（.BAT）

07-03　Link Script

07-04　ROM Maker

07-05　Offline Tools 概论

07-06　下载与执行

07-07　版本控制Server

07-08　说故事时间

Chapter 8　上电之后：Boot Loader

08-01　第一行程序

08-02　基本硬件测试

08-03　载入程序段与数据初始化

08-04　案例研究

Chapter 9　驱动程序

09-01　莫恐惧！

09-02　准备工作

09-03　控制CPU

09-04　存储器

09-05　控制其他芯片

09-06　ISR写作注意事项

09-07　驱动程序调试

09-08　结论

Chapter 10　设计硬件抽象层

10-01　由eCos & Android的系统架构谈起

10-02　HAL vs. BSP

10-03　为什么会需要HAL?

10-04　HAL是否会增加开发的难度？

10-05　HAL实例

Chapter 11　菜鸟当自强：软件工程师硬起来

11-01　硬件开发流程

11-02　卷起袖子用烙铁

11-03　善用测量仪器

Chapter 12　做好存储器管理

12-01　动态存储器空间配置

12-02　Stack

12-03　Heap：动态存储器配置

12-04　烧录器

12-05　突破物理存储器大小的限制

Chapter 13　存储器管理（Ⅱ）: NAND Flash概论

13-01　NAND简介

13-02　控制NAND Flash

13-03　Bad Block 管理

13-04　ECC（Error Correcting Code）

13-05　平均读写机制

13-06　NAAD Flash烧录器：特殊烧录格式

Chpater 14　模拟器

14-01　模拟器概论

14-02　Emulator vs Simulator

14-03　模拟器对项目开发的贡献

14-04　实战篇

Chapter 15　项目进度追踪实务

15-01　进度追踪与变更控制流程

15-02　进度追踪工具

15-03　总结：“便宜”可行的项目追踪工具组合

Chapter 16　SoC设计公司中嵌入式系统团队的管理

16-01　SA（System Application）团队简介

16-02　SoC设计公司到底葫芦里是卖什么药？

16-03　正确的SoC设计流程

16-04　SA的严酷考验

16-05　SA团队的管理

Chapter 17　系统整台

17-01　第一次整合

17-02　全功能整舍

17-03　发行第一个版本

Chapter 18　Testing、Debugging与Tuning

18-01　测试

18-02　Bug Sheet管理

18-03　Debug基本技法

18-04　Tuning

Chapter 19　结项前的煎熬

19-01　版本发行：兵荒马乱的日子

19-02　自动测试程序

19-03　决定量产版本

19-04　出货≠结项

19-05　项目结项

19-06　期许下一个项目

19-07　总结

Appendix A　未执行项目管理的项目

A-1　前言

A-2　说故事时闻（I）

A-3　说故事时间（II）

A-4　结论

Appendix B　Callback Function

Appendix C　用C来实现面向对象的概念

C-1　属性（Attribute）

C-2　方法（Method）

C-3　对象实例（Instance）

C-4　消息传递（Message Passing）

C-5　基类定义

C-6　基类的公用方法

C-7　对象类型判断范例

C-8　继承（Inheritance）

C-9　多态性（Polymorphism）

Appendix D　电子产品设计的最终依据：用户体验

D-1　界面设计vs用户体验

D-2　用户体验&可用性（Usability）

D-3　客户性格（Persona）

D-4　结论

封底

博客真情推荐

作者简介

邱毅凌，Ealin Chiu

台湾成功大学工程科学研究所计算机专业硕士，因毕业论文为“分布式面向对象操作系统的实作”，从此便与系统底层结下不解之缘。毕业后进入“资策会”，开发了生平第二个操作系统，并任项目经理，开始电子产品开发项目管理的工作。

之后赴深圳网京科技有限公司带领研发团队，技术方向为信息家电（IA），2001年任职于台北市的Canon研发中心，期间除了负责系统开发与研发人员管理外，还主持大陆、台湾地区、美国、日本、韩国等研发单位的共同开发项目。

2007年，加入某上市IC设计公司，担任SA团队的部门经理，顺利引入项目管理与软件工程的研发流程，并改善了质量系统，提高了开发效率。

专长：操作系统实践、嵌入式系统开发与项目管理

著作：《现代嵌入式系统开发专案实务》

图书在版编目（C I P）数据

嵌入式系统开发之道：菜鸟成长日志与项目经理的

私房菜 / 邱毅凌著．-- 北京：人民邮电出版社，

2011.12

ISBN 978-7-115-26603-3

I．①嵌…　II．①邱…　III．①微型计算机—系统开发

IV．①TP360.21

中国版本图书馆CIP数据核字（2011）第208713号

版　权　声　明

本书为台湾精诚资讯股份有限公司悦知文化授权人民邮电出版社出版。此中文简体字版本仅限在中华人民共和国境内（不包括香港、澳门特别行政区及台湾地区）印刷、发行及经销。

本书原版权属精诚资讯股份有限公司悦知文化。

版权所有，侵权必究。

内 容 提 要

本书用平易朴实的语言，以一个完整的嵌入式系统的开发流程为架构，通过一位“菜鸟”工程师与项目经理的诙谐对话，故事性地带出嵌入式系统概念及开发要素，并点出要成为一名称职的嵌入式系统工程师，在实际工作中所必须具备的各项知识及技能。

全书可以分为三大部分：第1、3、4、17、18、19章和附录D为嵌入式系统概论与开发流程；第2、15、16章和附录A介绍了嵌入式系统项目管理与软件工程方面的知识；第5〜14章，以及附录B、附录C介绍了嵌入式系统的开发技术。

本书不仅可以作为致力于嵌入式系统开发初学者的入门教程，也可以作为从事嵌入式系统开发的项目经理、技术团队主管等不可不读的参考书。

嵌入式系统开发之道——菜鸟成长日志与项目经理的私房菜

◆　著　　　　邱毅凌

改　　编　谢　亮　谢　晖

责任编辑　俞　彬

◆　人民邮电出版社出版发行　　北京市崇文区夕照寺街14号

邮编　100061　　电子邮件　315@ptpress.com.cn

网址　http://www.ptpress.com.cn

北京铭成印刷有限公司印刷

◆　开本：787×1092　1/16

印张：36.25

字数：992千字　　　　　　　　 2011年12月第1版

印数：1-3 500册　　　 　　　 　2011年12月北京第1次印刷

著作权合同登记号　图字：01-2011-4975

ISBN 978-7-115-26603-3

定价：69.00元

读者服务热线：（010）67132692　印装质量热线：（010）67129223

反盗版热线：（010）67171154

广告经营许可证：京崇工商广字第0021号

内　容　提　要

本书用平易朴实的语言，以一个完整的嵌入式系统的开发流程为架构，通过一位“菜鸟”工程师与项目经理的诙谐对话，故事性地带出嵌入式系统概念及开发要素，并点出要成为一名称职的嵌入式系统工程师，在实际工作中所必须具备的各项知识及技能。

全书可以分为三大部分：第1、3、4、17、18、19章和附录D为嵌入式系统概论与开发流程；第2、15、16章和附录A介绍了嵌入式系统项目管理与软件工程方面的知识；第5〜14章，以及附录B、附录C介绍了嵌入式系统的开发技术。

本书不仅可以作为致力于嵌入式系统开发初学者的入门教程，也可以作为从事嵌入式系统开发的项目经理、技术团队主管等不可不读的参考书。

目 录

推荐序

博客真情推荐

作者序

导读

新版特色

本书定位

本书人物及背景

前情提要

章节简介

延伸学习

Chapter 1　系统·嵌入·硬件

01-01　Welcome on board!

01-02　嵌入式系统开发团队

01-03　老调重弹：何谓嵌入式系统？

01-04　限制！限制！限制！

01-05　基本职能：老鸟也曾是菜鸟

01-06　工作内容：做个工程师，而非程序工人

Chapter 2　嵌入式项目管理

02-01　菜鸟啊！要立大志！

02-02　项目管理基本概念

02-03　项目生命周期五大阶段

02-04　项目范围（Scope）管理

02-05　项目进度（Time/Schedule）管理

02-06　项目成本（Cost）管理

02-07　项目质量（Quality）管理

02-08　项目人力资源（Human Resource）管理

02-09　项目沟通（Communication）管理

02-10　项目风险（Risk）管理

02-11　项目采购/合约管理

02-12　项目配置（Configuration）管理

02-13　企事业与组织对项目的影响

Chapter 3　嵌入式系统开发项目生命周期：项目启动与规划

03-01　嵌入式系统项目简介

03-02　初期规划：是否该接这个项目？

03-03　进度规则：ASAP？

03-04　规格规划：请接受这个不完美的世界

03-05　人力规划：营级组织、连级人力

03-06　成本规划：一切都是为了Cost Down

03-07　质量策略规划

Chapter 4　嵌入式系统开发项目生命周期：设计、执行与结项

04-01　动手之前：确定项目的执行原则

04-02　设计阶段：拟定作战计划

04-03　产品规格设计

04-04　硬件设计

04-05　系统设计

04-06　测试计划设计

04-07　风险评估

04-08　设计文件的重要性

04-09　开始实作之前：设计审查

04-10　实作阶段：执行所有设计

04-11　冲突不断：协同作战有多难？

04-12　产品化

04-13　无间道：项目可有close的一天？

Chapter 5　实作你的第一个嵌入式系统

05-01　嵌入式系统的开发环境

05-02　无痛起步：善用Sample Code

05-03　如何确定程序有执行?

05-04　标准C库函数可以用吗？

05-05　总结

Chapter 6　实作嵌入式系统平台

06-01　系统与平台

06-02　系统架构设计

06-03　API与程序风格设计

06-04　嵌入式操作系统：OS在哪里？

06-05　模拟器

06-06　Source Tree设计与程序风格典范

06-07　SDK

06-08　系统架构设计实例分析

Chapter 7　构建良好的嵌入式系统开发环境

07-01　开发工具

07-02　makefile &批处理文件（.BAT）

07-03　Link Script

07-04　ROM Maker

07-05　Offline Tools 概论

07-06　下载与执行

07-07　版本控制Server

07-08　说故事时间

Chapter 8　上电之后：Boot Loader

08-01　第一行程序

08-02　基本硬件测试

08-03　载入程序段与数据初始化

08-04　案例研究

Chapter 9　驱动程序

09-01　莫恐惧！

09-02　准备工作

09-03　控制CPU

09-04　存储器

09-05　控制其他芯片

09-06　ISR写作注意事项

09-07　驱动程序调试

09-08　结论

Chapter 10　设计硬件抽象层

10-01　由eCos & Android的系统架构谈起

10-02　HAL vs. BSP

10-03　为什么会需要HAL?

10-04　HAL是否会增加开发的难度？

10-05　HAL实例

Chapter 11　菜鸟当自强：软件工程师硬起来

11-01　硬件开发流程

11-02　卷起袖子用烙铁

11-03　善用测量仪器

Chapter 12　做好存储器管理

12-01　动态存储器空间配置

12-02　Stack

12-03　Heap：动态存储器配置

12-04　烧录器

12-05　突破物理存储器大小的限制

Chapter 13　存储器管理（Ⅱ）: NAND Flash概论

13-01　NAND简介

13-02　控制NAND Flash

13-03　Bad Block 管理

13-04　ECC（Error Correcting Code）

13-05　平均读写机制

13-06　NAAD Flash烧录器：特殊烧录格式

Chpater 14　模拟器

14-01　模拟器概论

14-02　Emulator vs Simulator

14-03　模拟器对项目开发的贡献

14-04　实战篇

Chapter 15　项目进度追踪实务

15-01　进度追踪与变更控制流程

15-02　进度追踪工具

15-03　总结：“便宜”可行的项目追踪工具组合

Chapter 16　SoC设计公司中嵌入式系统团队的管理

16-01　SA（System Application）团队简介

16-02　SoC设计公司到底葫芦里是卖什么药？

16-03　正确的SoC设计流程

16-04　SA的严酷考验

16-05　SA团队的管理

Chapter 17　系统整台

17-01　第一次整合

17-02　全功能整舍

17-03　发行第一个版本

Chapter 18　Testing、Debugging与Tuning

18-01　测试

18-02　Bug Sheet管理

18-03　Debug基本技法

18-04　Tuning

Chapter 19　结项前的煎熬

19-01　版本发行：兵荒马乱的日子

19-02　自动测试程序

19-03　决定量产版本

19-04　出货≠结项

19-05　项目结项

19-06　期许下一个项目

19-07　总结

Appendix A　未执行项目管理的项目

A-1　前言

A-2　说故事时闻（I）

A-3　说故事时间（II）

A-4　结论

Appendix B　Callback Function

Appendix C　用C来实现面向对象的概念

C-1　属性（Attribute）

C-2　方法（Method）

C-3　对象实例（Instance）

C-4　消息传递（Message Passing）

C-5　基类定义

C-6　基类的公用方法

C-7　对象类型判断范例

C-8　继承（Inheritance）

C-9　多态性（Polymorphism）

Appendix D　电子产品设计的最终依据：用户体验

D-1　界面设计vs用户体验

D-2　用户体验&可用性（Usability）

D-3　客户性格（Persona）

D-4　结论

封底

博客真情推荐

推荐序

两年前我为此书第一版作序时，即被作者无私地经验分享精神所感动，并期许其可在写作上多花时间。本书第一版销售成绩极佳，相信大部分的读者应该会从书中得到启发，更加深了我对作者其他新著作的期待。结果两年多来杳无音信，后来得知作者到某IC设计公司上班，他原本只答应去帮忙一阵子，为其系统应用（SA）团队导入正确的嵌入式系统开发思想与流程，但以其负责任的本性，对所有任务总是全力以赴，最后这家公司占用了他两年又两个月的时间。就我看来，相当可惜，作者把他可能的影响力只局限在一家公司、单一领域了！

2010年初，作者终于重拾写作，其广泛的出版计划令我相当吃惊，可见其分享的热情仍在，且依然不停地对产业界进行观察、思考与学习，在此我给予其深深的祝福！

自从苹果计算机推出的iPod、iPhone、iPad蔚为风潮，意味着MP3播放器、智能手机、游戏机等消费性电子产品已逐渐取代PC，而成为主流数字产业，对于台湾地区信息产业的升级，扮演了关键性的角色，而其幕后的核心技术正是多媒体嵌入式系统。好的嵌入式系统，可以让硬梆梆的硬件更有价值。为摆脱代工的低毛利率，台湾地区业界对相关人才需求紧迫，遗憾的是，目前学校信息系的教育显然与现状有所落差。

本书以电子产品开发项目的生命周期为经，嵌入式系统技术与思维为纬，是一本行文轻松流畅却不失严谨的技术书籍。嵌入式系统技术的开发瓶颈，往往在于如何把嵌入式系统设计的重要思想与电子产品的硬件平台做创意性的结合。通过书中项目经理与菜鸟工程师的问答，嵌入式系统开发与项目管理的精神跃然于字里行间，传达重要思维与技巧于无形之中。这种风格在市面众多信息参考书籍中极为少见，对初学者当可趋缓其学习曲线，更快抓住重要的思想与学习的重点；对想迈入项目管理领域的技术人员，可更了解电子产品开发的生命周期与项目管理的技巧；对已有丰富经验的项目经理，他山之石，可以攻玉，本书绝对会带给你许多改进项目运行的灵感。

本书较前版增加了许多近年来开发消费性电子产品一定会碰到的主题（例如NAND Flash），并对项目管理理论、实际执行项目时必备的思维与技巧，以及fabless公司中的嵌入式系统开发团队等做了不少着墨。相信这都是作者亲身体验所凝聚而成的智慧，即便你已经读过本书第一版，还是可以从本书中得到不少的收获。

读完此书，你会发现作者欲将多年执行嵌入式系统开发项目的经验一吐为快，我觉得这是身为工程师、项目经理、技术团队主管等不可不读的好书，在此推荐给你！

吴锦城

思科（CISCO）前副总裁

美国Acopia Networks董事长

博客真情推荐

仔细想想，我靠嵌入式系统（Embedded System）吃饭有五六年了。念书时醉心于C++、图形学、影像处理等技术，立志成为游戏制作人的我，大概怎么也想不到今天得在2KB的内存限制内，写8位处理器的程序吧？

虽然很少有信息系的学生会立志当个嵌入式系统的程序员，但事实上，在台湾地区从事这方面工作的程序员不在少数。因为我们在生活中会遇到太多嵌入式系统：空调、洗衣机、电冰箱、电磁炉等，只要冠上“微电脑”的几乎都有嵌入式系统。电视机有一个嵌入式系统，电视机遥控器里也有一个嵌入式系统，DVD播放器、游戏机也算是嵌入式系统，电子表、手机、PDA、电子字典等，所有你想得到的电子产品几乎都属于嵌入式系统的范畴。

正因为人才需求高，市面上和嵌入式系统程序设计相关的书籍、补习班课程等亦多如牛毛。这些书大多是学院派的，从理论出发。偶有书名冠上“实作”的，也仅限于列出程序的范例。很少有书籍从现实出发，告诉你在职场上会遇到什么样的问题，该用什么样的方法解决。很幸运的是，本书就是一本这样的书，而且更幸运的是，本书是台湾地区土生土长的中文原创书。

本书最大的特点，就是从目前台湾地区业界的现实面出发，点出项目团队人力缺乏、内部各小组互有摩擦、老板客户常有各种要求等真相。这要归功于作者拥有的数十年项目开发经验（而非数十篇学术论文）。书中提到的要点及案例，字字珠玑，只要曾在业界有些经验的人，读这本书应会不时露出会心的微笑（或是苦笑）。

也许是想要突显出“实务”的特性，也许是希望你能更轻松地消化书中的思想，本书采用技术书籍少见的“剧情对话式”文体，描述一位刚从学校毕业的信息系硕士从无到有学习嵌入式系统开发流程的经过。全文的主要内容，就是这位“菜鸟”和“项目经理”及各小组“老鸟”之间的对话。菜鸟所代表的是学院派、主流计算机系统的思维（动辄上GHz的32/64-bit CPU、数百MB甚至上GB的内存、还有“用不完”的硬盘空间）；而老鸟们则代表了业界和嵌入式系统的现实（微处理只有数十MHz、数据存储器只有几KB、读写NAND ROM还要考虑效率问题）。通过菜鸟和老鸟之间的对话，我们可以清楚地看到“学院VS.实际”以及“PC VS.嵌入式系统”的不同之处。再加上诙谐的文笔，让读者可以快速而专注地了解嵌入式系统的特性。

书中这位“菜鸟”以成为全才为目标，因此，随着书中项目的进行，被派到各个不同的部门了解学习。作者通过这个方法带领你了解项目团队中每个小组的工作内容，也大略地介绍了项目开发的流程。书中提到一个重要的思路：学硬件的人不要怕软件，学软件的人也不要怕硬件。在项目人数普遍不足的情况下，软件人员遇到简单的硬件问题需要解决时，最快的方法就是靠自己做简单的实验，不要全指着别人来帮忙。相同的，其他小组人员的工作，我们也不是永远不会碰到。所以了解每个项目人员负责扮演的角色，不仅是项目经理的工作，了解这些知识对每个项目成员都有好处。

而本书最重要的一个思想，就是作者不断强调的：一法通，万法通。嵌入式系统使用的处理器种类繁多，再加上不同的系统级芯片（SoC）实作，是永远学不完的。因此，一位嵌入式系统工程师的学习目标，并不是把手中的芯片研究透彻（那是卖你芯片厂商的工作），而是要了解嵌入式系统有什么特性。等到真的把芯片和data sheet拿到手之后，再把思想套上去。作者在书中举出案例时，也不忘提醒你什么是重要的，什么是旁枝末节，大概看懂就好。

本书着重概念思想，细节方面则提供不少经典书籍供参考。这本书可以当成进入嵌入式系统开发的敲门砖，看完之后再看书目上的书，应该更有感觉。

总而言之，这本书是中文计算机领域难得一见的原创好书！强力推荐给正在这个业界中努力求生存，以及将要进入这个业界的勇士们。此外，我也要推荐给信息系的学弟学妹们，书中描述的情景可能就是你们未来工作上会遇到的问题。即使将来不走这条路，书中的内容也可以和课本上的微电脑组织架构互相比较验证。而且这本书写得那么轻松，就算是当故事书看也不错！

——Closer丨酒精浓度百分之五

这本书可以说是项目经理笔记，通过对白式的小说形式，呈现一个菜鸟软/固件工程师成长的经历，文笔不脱工程师风格；又或者说与一般技术性书籍相同，到处都有流程图、表格，还有程序代码。每一段故事后都有作者的经验之谈，并以黑体字表现，在轻松地演出后，留下重点。

本书前1/4的内容着重于项目管理，主要探讨嵌入式系统项目的特性、复杂性与先天限制，也包含了公司人事管理、跨部门沟通的思维。这部分是全面性的考虑，你可以一窥整个项目所牵涉到的各种问题、各部门的特性与考虑。其中与我现在公司的所见有相当多可以借鉴之处，对眼界的开阔极有帮助。

而书后约3/4的内容则是属于研发的部分，描述菜鸟工程师如何由一个新人，在资深同事的带领下，应用学校所学，通过比较与现实工作的异同，打破单纯软件归软件、硬件归硬件的刻板印象。从CPU的启动程序到基本的驱动程序的编写，以及最后调试与测试工作，再回归到项目的本质，完整的一个成长过程，予我有莫大裨益。

整体而言，本书可看得出笔者经过相当多的工程设计实践，文笔颇为流畅，所分享的经验也相当恳切，非常适合作为相关单位新人培训使用，资深人员则是看着自己的故事类似地上演，是值得一读的好书。

——小麦丨小麦的心得笔记

本书用“老鸟”与“菜鸟”之间的互动，将嵌入式系统开发及项目管理所需注意到的事项都详细地描述出来。如果我在毕业专题及论文实作时先看这本书，那可能就不用走这么多冤枉路了。

这本书不仅适用于嵌入式系统，也适用于FPGA、DSP开发。我在开发FPGA、DSP的时候，很多时候都是去翻spec.；要不然就是耗费在解决移植到板子上的问题，像是内存配置、处理速度不够快诸如此类的问题，有时候还用猜测的方式来解决，不过，这些在本书中都解释得非常清楚。

这本书对于刚入门嵌入式系统的菜鸟来说，真的很值得一看！

——kewang丨杂七杂八的kewang博客

自研究所毕业后，我就一直在嵌入式系统的设计领域，本书以一位职场“菜鸟”为串场主角，述说“菜鸟”参与开发项目的经历，通过与共事的项目经理和一些“老鸟”的对话，带入相关主题，让我回想起以前参与的许多项目经历。要培养这个领域的专家并不容易，因为现代科技与时俱进，必须一直保持学习，不过，踏入这个圈子后，就没有失业的问题，因为永远缺人。本书非常值得想进入或已经在这个领域的朋友阅读。

——Bridan丨研发培训机构（Bridan's Blog - 4rdp, For R&D Person）

作者序

本书上一版自2007年6月上市至今已有三年多的时间了，期间收到很多反馈、鼓励、建议与指教。最令人开心的，则是能够对想进入嵌入式系统开发领域的朋友提供咨询，他们通过这本书找到了笔者，让笔者有机会分享自己在这个领域的经历与体验，让迷惘的朋友得到指引。

短短几年间，消费性电子产品市场发生了不小的质变：2008年金融海啸对电子业的冲击、iPhone及APP Store红透半边天、E-Book卷土重来、平板电脑死而复生（iPad带动电子阅读器的新风潮）、Google亲自下海搞了个Embedded OS (Android)、Intel收购WindRiver、ARM几乎囊括了高端处理器的市场、MP3产业盛极而衰、全世界各国陆续切换为数字电视（引发STB爆炸式需求）。以消费性电子产品类别来说，目前最热门的是：智能手机、电子书、微投影机、高画质播放器、3D电视等。当今市场气氛和两年前已有极大的改变。

虽说本书强调的是嵌入式系统开发与项目管理的思想，不会因产品不同而有太大的差异，但笔者仍认为本书必须与时俱进，有许多初版未放入的主题现在变得相对重要，而且部分既有的主题值得更深入探讨，更重要的是，必须修正初版中疏漏与错误的部分。

本书除了内容更丰富、完整之外，还加入了笔者这些年来对产业观察与实际执行电子产品开发项目的心得，并延请更专业的出版团队进行设计，使这本书的质量更加精良。

邱毅凌

导读

本书主要目的是希望你不论身处于哪个职位，都能了解嵌入式系统开发是怎么一回事。项目管理者需要知道一个电子产品从无到有，包含哪些开发工作及困难；工程人员必须知道项目管理的精神，才能与其他领域同仁顺利合作；老板则必须了解项目成员们的职责，必要时才能适时给予协助及资源等。但每家公司的组织结构与企业文化不尽相同，很难在一本书中说完所有组织架构可能碰到的问题，因此，本书采用最常见的组织架构，如下图所示。其实一般公司就算职称不同，运作一个电子产品开发项目时，也不外乎就是这些角色，书中提到的思想与工具自然仍可一体适用。

组织架构范例

项目的执行时间有长有短，与项目困难度或组织所投入的资源有关，但无论成功或失败，项目总有结束的一天。项目成员从项目启动后陆续加入，并在项目任务完成后逐一离开，回到其原有组织。也就是说，项目组织是临时性的，但公司的组织则必须持续发展。举例来说，软件团队已经具有RTOS的技术，所以，公司在接了相关的项目后，随即会指派一名项目经理，并从软件团队中挑选成员加入此项目。在此同时，软件团队可能正在导入Embedded Linux技术，这是该团队的长期发展计划，和目前正在执行的所有项目没有直接的关系。因为这种组织架构（称为强矩阵组织），可兼顾组织的日常运作及临时性项目的执行，目前已被广为采用。

在强矩阵组织中最可能发生冲突的来源有两个：第一，负责某项目成败的项目经理，与负责组织中人力调配、新技术研发的部门经理（为与Project Manager区别，通常称之为Functional Manager）两者的职责、所必须具备的能力和对组织发展的看法并不相同；第二，当工程人员加入某项目后，在项目执行期间会有两位直属老板，如果这两个老板之间沟通不好，则项目成员可能无所适从。

为避免过多角色导致笔者想表达的主题失焦，书中会淡化“Functional Manager”的角色。例如，工程师的招聘通常由Functional Manager面试并决定是否录用，而Project Manager会在项目启动时，在目前组织中挑选成员加入项目[1]，本书会将此关系简化为直接由Project Manager来招聘项目成员。本书两位主角就是Project Manager以及一名新项目招聘的菜鸟工程师，但Project Manager与Functional Manager间沟通渠道是否畅通、权责能否区分清楚等，也是项目成功与否的重要因素之一，所以书中新增一个专门讨论Functional Manager在嵌入式系统开发项目中角色界限的章节[2]。

[1]　除非组织中无合适人选，才会由Project Manager负责招聘，但此新进人员还是会被挂在某位Functional Manager的团队之下。

[2]　第4-1节“动手之前：确定项目的执行原则”。

新版特色

本书相较于上一版，改动极大，绝非修正错别字、增加一些无关痛痒的章节而已。虽然主要架构不变，但笔者对全书进行了全面的检查，力求去芜存菁，新增笔者认为读者必须知道的主题多达50种以上。此外，根据笔者这两年讲授嵌入式系统项目管理课程，以及实际运作电子产品开发项目的经验，归纳出许多业界普遍存在的问题，并在本书中提出实际可行的解决方案。

业界有太多一直抓不到嵌入式系统开发本质、把时间用错地方的热血工程师；有太多仍以“土法炼钢”，不知如何使用所掌握的技能，只好无效率地不断加班，靠工程师的生命与运气执行项目的疲劳团队。更不幸的是，许多团队都会有个好打官腔、不知民间疾苦、要求项目按照教科书规范执行，却又不愿提供足够资源的老板！本书即便无法对整个产业起到震聋发聩之效，也希望能让所有读者，无论是工程师、团队主管，还是高层老板，都能用有效的方式执行嵌入式系统项目，并进行电子产品开发。

需知唯有软件才能赋予硬件灵魂！只有通过培养更多的嵌入式系统开发人才，建立具有国际竞争力的电子产品开发团队，经产业政策鼓励可在全球竞争的信息/电子公司，才是产业升级的不二法门。

至此笔者仍一秉初衷，若本书出版能对此产业有所帮助，能让你本人以及所属团队和公司用正确的方法，发挥最大的创造力，则甚幸！

本书定位

市面上电子产品琳琅满目，业界正在使用的CPU、IC与嵌入式操作系统多如繁星，嵌入式系统开发的技巧数不胜数，书店里嵌入式系统的图书繁杂多如过江之鲫，更不要提在Google搜寻“Embedded System”会出现多少相关网页！由于嵌入式系统是计算机产业中非常特殊的一个领域，几乎没有一项电子产品或嵌入式系统开发项目是完全相同的，身为这个产业链中的一员，面对排山倒海的信息，你该如何制定自己的学习目标？

所谓“一法通，万法通”，嵌入式系统开发也是如此。如果只专注于研究某颗CPU的功能或某个产品的特性，实质意义并不大，重点应放在如何从繁杂的信息中萃取出放诸四海而皆准的思想，因为下个产品开发项目可能是完全不同领域的应用。例如，前一个项目开发的是多功能打印机，下一个项目可能是DVD播放器；前一个产品使用Android，下个产品则根本不需要OS。再加上不同项目可能使用完全不同的CPU或主控IC，这都增加了嵌入式系统开发人员对自身学习经历是否足以应付未来任务的质疑。笔者希望能将自身的经验与见闻整理成真正有用的思想，通过轻松的行文风格，传达给想加入嵌入式系统这一行业的你，希望本书可以让对嵌入式系统开发有兴趣的你少走一些冤枉路。

本书人物及背景

菜鸟：一位刚毕业的信息系本科硕士，这是他第一份被录取的工作，刚成为职场新人。

项目经理：拥有十年以上消费性电子产品丰富开发经验，职位为PM，目前带领该公司的研发团队负责某日本国际大厂委托的电子辞典开发项目，同时也是“菜鸟”职业生涯中遇到的第一位启蒙老师。

软件部门经理：比项目经理还要有经验的软件人，是目前公司中软件开发团队的主管，对软件开发流程与质量要求，始终保持着几近固执的坚持。

固件老鸟：项目团队中固件小组的组长，虽然工作时间不长（约三年多），但控制过的IC不少，对驱动程序开发相当有心得。

系统老鸟：项目团队中系统小组的组长，在团队中的地位与影响力如同项目的副主管。技术能力相当全面且扎实，“年轻”时曾独力开发过RTOS，深知系统架构与开发流程对项目成败的影响。

硬件老鸟：负责过许多国际大厂的产品设计与生产，对硬件设计的性能与成本总是斤斤计较。

测试老鸟：曾是优秀的软件工程师，因配合度高，授命组建测试团队。对软件开发流程相当熟练，能使用最有效的方式进行测试，与RD团队关系良好，但测试团队人员流动率高是其最烦恼的事。

前情提要

让我们从一个面试的场景开始。

项目经理：“我觉得你的学习经历及专业技能都符合这个缺失职位的需求，工作内容是消费性电子产品开发工程师。简单地说，就是现在最热门的‘嵌入式系统’，我们团队目前正承接某日本大厂的电子辞典开发项目。”

菜鸟：“其实我有点担心我的能力是否真能符合贵公司的职务需求。虽然我在学校修过微处理器实习，但对所谓的‘嵌入式系统’仅有粗略的概念，不仅汇编语言没写过几行，驱动程序也没碰过，电子学还被挂过一次，更不要说什么实际的经验了。还有，嵌入式系统的程序要怎么写？是现在热门的Google Android还是Windows Mobile平台？API手册可不可以先让我回家恶补啊？”

项目经理：“放轻松，别告诉我现在学校不教C语言了！操作系统、数据结构、算法及计算机组织都是必修的吧？希望你上这些课时不是在鬼混，其他实战必备的技能，等你进来后我们会慢慢教你。此外，我们有自己的软硬件架构，所有的API都是自己写的，要用到Windows或Linux那套的机会恐怕不多。”

菜鸟：“是喔！其实我对Java和网页程序比较熟，公司的开发项目会用到Java吗？”

项目经理：“恐怕不行，我们用的CPU执行时序只有24MHz，而且客户对产品性能要求相当严格，我想都没想过要跑Java。之前我们还用8051做产品，你要8 bit MCU跑Java VM是有点过分了！”

菜鸟：“什么？24 MHz?我的计算机都飙到3.2 GHz了，还双核心耶！这样的CPU能做什么？”

项目经理：“放轻松嘛，除了跑Windows 7外，可做的事情多了，我还没跟你说我们系统只有8 MB的SDRAM，至于那个8051项目用到多少内存，以后有空我再慢慢让你知道，现在不宜给你太大的惊吓。”

菜鸟：“冒昧问一句，像电子字典这样的产品有什么技术可言？好像蛮low-end的，我刚刚毕业，希望能在第一份工作中多学点东西。”

项目经理：“在PC上开发执行的电子辞典系统也许很简单，但在24MHz的CPU、8 MB的SDRAM上，需要的技术可远超出你的想象！此外，日本厂商对高质量的严格要求，我想你也略有耳闻，不但要机器稳定、耗电量低，死机绝对是不可饶恕的罪过，在这样的前提下，需要的技术恐怕真的比你想象的还多。

在开发电子产品的过程中，你可以学到嵌入式系统的思想、产品化的流程以及项目管理的技巧。此外，我们还会教你使用示波器、逻辑分析仪等信号测量仪器，当然还必须要会看原理图，会测量硬件的信号。”

菜鸟：“哇！我是学软件的耶……”

项目经理：“就如同大部分学硬件的人也同样会害怕写程序一样，真的不需要有这种莫名奇妙的排斥与担心，只要逻辑思维清楚就可以了。试想，当你要把程序放上某个硬件板子上执行，机器通电后，执行的第一行指令就是你的程序，而你是写程序的人，怎么可以不清楚这个CPU的运行方式与这块硬件板子的电路配置？”

菜鸟：“好像有点懂了，似乎还蛮好玩的。请教一个私人的问题，现在智能手机好像很红，我对智能手机的开发还蛮有兴趣的，在贵公司有机会开发手机吗？”

项目经理：“我刚和你说的工作内容是‘消费性电子产品’或‘嵌入式系统’的开发，在较高的层面上，我觉得开发电子辞典或手机所需要的基本知识是一样的。等做了几个产品之后，有天你就会突然顿悟的。”

菜鸟：“好像有点玄……”

项目经理：“如果你愿意加入我们，我会让你在一件件任务中学习。”

这些年来，“嵌入式系统”一直是个被喊得震天价响的名词，但真正了解其内涵的人并不多。因为离开学校已久，笔者曾试着从面试或培训新人时做些简单的调查，却惊讶地发现，几乎所有信息本科毕业生（包含研究生）或有两三年软件开发工作经验者，他们可以把“嵌入式系统”的定义说得头头是道，但却只有少部分人具备如何实现的概念，更甭提一些产品化的细节与注意事项了。

许多知识学校不会教、也无法教，因为那需要实际经验。就如同操作系统的教科书不会教你Boot-Loader、Context-Switch等实作一样，或软件工程的老师绝对不会讲授在人力缺乏的状况之下，如何去执行教科书中的那一套质量系统！

笔者在业界多年，深知台湾地区硬件厂商被客户压榨或同行削价的竞争之苦，而台湾地区商用软件开发公司的规模也远不及欧美，甚至印度，唯有通过软件及创意来增加硬件的价值才是信息业的出路。因此，培训信息系本科学生具备嵌入式系统开发的相关职能确实刻不容缓。

现在的新人都需要一到两年的培训，通常是必须参与某个产品的开发工作之后，才能真正了解嵌入式系统开发的本质，但笔者认为这个时间应该缩至就业前。市面上有关嵌入式系统的书籍很多，但通常只能讲述一些基本思想，对实际操作则点到为止。其他则在某个特定平台上深入钻研，这些对嵌入式系统开发的帮助微乎其微。从这些书中，你可以学到如何在ARM的评估板上写程序、如何架构Embedded Linux系统、如何移植Java VM到嵌入式系统等，如果工程师遇到的项目是：使用日商SEIKO EPSON特制化的CPU开发手持式电子装置，或者使用8 bit 8051这颗CPU加上凌阳的DSP，开发类似电子狗的玩具宠物，这些书能帮上什么忙？

其实，这行的高手可说是卧虎藏龙，也许大家都忙于工作，愿意（或有空）分享者少之又少。笔者愿将多年嵌入式系统开发的经验聚集成册，基于电子产品的多样性，阐述真正对你有益的思想，再加上实际发生的案例及其解决方案，读者可从中学到与其他教科书不同的“嵌入式系统”。

希望此书可起到抛砖引玉之效，让嵌入式系统培养教育更为扎实，并且让信息业更上一层楼。总之，这不是一本全都在讲述何谓嵌入式系统的书，更不是只教你在某个特定平台写程序的Programming Guide，笔者希望通过实例让你从本书中学到嵌入式系统项目管理的思维及真正实作的技巧。

笔者才疏学浅，虽竭尽心血并经多次校正以成本书，其中难免错误之处，希望高手前辈不吝指教，万分感谢。

章节简介

本书风格稍异于一般技术书籍，以一个完整的嵌入式系统开发项目为支架，通过菜鸟与项目经理及资深技术人员的对话，推导出实际上正确的思想。通过这种方式，你会对这些思想与知识留下深刻的印象，而且是透彻的了解，学习效果当然比教条式的宣读要好。

对第一次阅读本书的你，建议遵循以下提示，当可收到事半功倍之效。

■　项目经理的回答与菜鸟对讨论的总结往往是重要的思想所在，为避免你忽略重点，书中会以粗体字表示（请注意，本书中使用PM缩写时都是代表“Project Manager，项目经理”的意思）。

■　较重要的概念，在章节的结尾处会加以深入探讨。

■　对于篇幅较长的章节，在结尾处条列出该章节的重点回顾。

■　本书旨在传达嵌入式系统项目的思想，并不局限于特定平台。至于书中程序部分，你应该先看粗体字的注释，了解程序目的之后，再看程序的细节。

■　如果对C语言不是那么熟悉，可以先略过书中的程序部份，你还是可以学到嵌入式系统开发项目的重要思想。

书中各个章节尽可能保持独立，基本上是以嵌入式系统项目生命周期的各个阶段作为区分。全书主轴为：一个在电子产品开发领域中经验丰富的项目经理，与其团队中的研发主管们，带着刚毕业的菜鸟进行嵌入式系统开发的成长历程。嵌入式系统开发的思想与经验都融入在项目经理与菜鸟的问答中，再加上在许多实际工作上发生的案例，可对嵌入式系统开发有更透彻的认识。

本书共分19章，基本上，是按照嵌入式系统开发项目的生命周期排序。除了第3、4章之外，其他章基本上保持独立，你可选择有兴趣的章节先阅读，在必须参考到其他章节的地方都会详加注明。但笔者仍建议第一次还是从头读起为佳，就如同嵌入式系统开发项目必须按部就班一样，总不能设计阶段还没做完，就开始忙着实作吧！

全书分为3个领域，分别如下。

嵌入式系统概论与开发流程

■　第1章：系统·嵌入·硬件

■　第3章：嵌入式系统开发项目生命周期：项目启动与规划

■　第4章：嵌入式系统开发项目生命周期：设计、执行与结项

■　第17章：系统整合

■　第18章：Testing、Debugging与Tuning

■　第19章：结项前的煎熬

■　附录D：电子产品设计的最终依据：用户体验

项目管理与软件工程

■　第2章：嵌入式项目管理

■　第15章：项目进度追踪实务

■　第16章：SoC设计公司中嵌入式系统团队的管理

■　附录A：未执行项目管理的项目

对项目管理较没兴趣者可先跳过这些章节，或先快速读过，待全书读完之后，再回头来看一次，应当会有更多的体会与收获。笔者在此要强调的是，千万不要认为自己只是个工程师就不必学习项目管理的思维，以台湾地区业界的环境来说，很难有人可以做一辈子的工程师，如果想在职业生涯更上一层楼，除了弄清楚产品的开发流程，具备项目经理的视野与职能也是相当重要的一件事。

嵌入式系统开发技术

■　第5章：实作你的第一个嵌入式系统

■　第6章：实作嵌入式系统平台

■　第7章：构建良好的嵌入式系统开发环境

■　第8章：上电之后：Boot Loader

■　第9章：驱动程序

■　第10章：设计硬件抽象层

■　第11章：菜鸟当自强：软件工程师硬起来

■　第12章：做好存储器管理

■　第13章：存储器管理（II）：NAND Flash概论

■　第14章：模拟器

■　附录B：Callback Function

■　附录C：用C来实作面向对象的概念

以下是各章节的简介。

■　第1章：系统·嵌入·硬件

主题为嵌入式系统概论，内容包含嵌入式系统的定义与特性，还介绍了几本值得一读的参考书籍，最后说明一位优秀的嵌入式系统从业人员应具备的基本职能是什么。

■　第2章：嵌入式项目管理

本章是一个完整的项目管理概论课程，利用讲解投影片教材的方式，阐述项目管理的重要思想，并逐一说明项目管理知识体系（PMP）中的流程与工具。

■　第3章：嵌入式系统开发项目生命周期：项目启动与规划

第1、2章介绍嵌入式系统开发项目的生命周期，你将发现业界的实际情况与软件工程、、项目管理教科书中确实存在着差异。每项产品开发项目都应该视作一个项目，并使用项目管理的思维与工具来管理。偏偏大部分项目经理都是由工程师升上来的，并未受过正式的项目管理培训，本章会花一些篇幅说明项目管理的重要思维。此外，本章内容还包含嵌入式系统开发项目简介、项目开始之前的规划评估以及项目初期的规划工作，其中包括进度、规格、人力及成本规划等。

■　第4章：嵌入式系统开发项目生命周期：设计、执行与结项

描述嵌入式系统开发项目中的设计阶段、执行阶段与最后的结项阶段，其中对设计阶段有较深入的描述，包括产品规格、硬件设计、系统设计、测试计划设计及风险评估等。

■　第5章：实作你的第一个嵌入式系统

实际描述一个嵌入式系统的开发流程。虽然目的只是让硬件板子上的一个LED闪烁，但却已是一个具体入微的嵌入式系统。本章包含开发嵌入式系统程序的步骤与注意事项，除此之外，还对计算机系统的运行原理有深入的探讨。

■　第6章：实作嵌入式系统平台

所谓的嵌入式系统平台就是让电子产品的应用程序得以顺利开发的环境，它不仅是软件或硬件的概念，还是一个稳定的环境。在这个环境下，负责应用程序开发的工程师，可以将心力放在产品功能的实现，以及改善其质量与性能上。本章内容包含嵌入式系统平台的定义、系统架构设计、API与程序风格设计、嵌入式操作系统简介、Source Tree设计以及程序风格典范等。此外，为了让你对系统架构有更深层的认识，本章还加入了SDK与Turn-Key Solution概论，以及许多系统架构的案例分析。

■　第7章：构建良好的嵌入式系统开发环境

描述何谓嵌入式系统开发环境，以及为什么要构建嵌入式系统开发环境，内容包含嵌入式系统开发工具（Cross-Tools）简介、Makefile与Link Script的思维与写法、何谓ROM Maker、如何将程序下载到机器上并执行，以及版本控制server的重要性。最后，举一个实际发生过的案例，说明构建所谓嵌入式系统开发环境的重要性。

■　第8章：上电之后：Boot Loader

描述机器上电后，如何让CPU可以正确执行我们的程序，以及开机后系统该做的初始化动作。本章内容包括第一行程序如何被执行、基本硬件测试，以及如何加载程序段与初始化数据段等细节。最后介绍目前最热门的NAND Flash，描述NAND Flash Booting的工作原理以及注意事项。

■　第9章：驱动程序

有些人会认为嵌入式系统开发几乎就是在做驱动程序开发，这当然是错误的观念。驱动程序仅是嵌入式系统中的一个环节而已，但一般软件工程师总会对驱动程序开发怀着莫名的恐惧。本章首先陈述驱动程序开发并没有想象中困难，接着描述驱动程序开发之前的准备工作，其他内容包括如何控制CPU、如何控制内存与其他IC、编写中断处理程序的注意事项，以及如何为驱动程序调试。

■　第10章：设计硬件抽象层

业界通常认为硬件抽象层是个复杂且不容易实现的概念，其实这是一个谬误的说法。因为唯有系统架构中包含硬件抽象层，才可能满足嵌入式系统对可移植性的需求。本章将说明硬件抽象层的重要性、设计原则，以及实作时的注意事项。

■　第11章：菜鸟当自强：软件工程师硬起来

负责嵌入式系统的软件工程师，必须对硬件的知识与技能有相当程度的了解。本章从软件工程师的需求出发，说明硬件设计流程、烙铁与测量仪器的使用原则。

■　第12章：做好存储器管理

存储器管理是嵌入式系统开发中相当重要的一环，本章内容包括存储器空间配置，以及Stack与Heap在嵌入式系统中的应用，最后提到可将程序或数据写入EEPROM或Flash的烧录器。

■　第13章：存储器管理（II）：NAND Flash概论

目前NAND Flash应用越来越普及，但NAND Flash却有着让系统设计者头大的特性一Bad Block。本章从系统开发的角度说明NAND Flash特性、NAND Flash控制Bad Block、ECC与平均写入机制（Wear-Leveling）。

■　第14章：模拟器

嵌入式系统的开发环境通常十分昂贵，不可能给每位工程师都配上一套，再加上项目初期可能没有足够的硬件平台可供应用程序开发之用，由此可知，在嵌入式系统中模拟器的功能是很强大的。本章首先介绍何谓模拟器，接着说明Emulator与Simulator的差别，以及模拟器对嵌入式系统开发项目的贡献。最后列举许多如何在PC上模拟实际机器装置的实例。

■　第15章：项目进度追踪实务

系统是否能如期完成，除了技术问题之外，最重要的，当然是项目能否被“妥善且正确”地管理。本章说明如何执行项目进度的追踪，以及介绍一组容易使用的项目管理，以及进度追踪的工具套件。

■　第16章：SoC设计公司中嵌入式系统团队的管理

SoC设计公司中的SA (System Application)团队也是一个嵌入式系统开发团队，但却有着更为“艰苦”的处境。本章除了介绍SA团队的工作流程与管理方式之外，也花了不少篇幅说明Turnkey Solution的特性。

■　第17章：系统整合

大部分的应用程序会先在仿真器上开发，而驱动程序与部分系统功能则必须在真实机器上才能验证。当底层平台已趋稳定，就可逐一将应用程序移植到实际机器上执行。本章内容包含进行第一次整合的最佳时间点、导致整合失败的原因、开发进度重新检讨，以及程序移植时的注意事项。整合完毕后就可以发行正式版本，本章最后则会提到bug管理系统。

■　第18章：Testing、Debugging与Tuning

当程序交付测试后，项目就进入了测试、调试与调整时期。本章以测试、调试与调整3大主题，详细内容包括嵌入式系统的测试概论、静态测试原理与工具、bug的管理原则、debug基本技巧，以及如何进行系统性能、power与footprint的最佳化。

■　第19章：结项前的煎熬

介绍结项之前的工作项目，包含版本发行、介绍生产线专用的自动测试程序、如何决定量产版本、出货不等于结项的怪现象，以及结项相关事宜等。最后对本书作一个简单的结语。

附录则共分为4大主题。

■　附录A：没有执行项目管理程序的开发项目会有什么状况。

■　附录B：解释何谓Callback Function。

■　附录C：如何用C语言实现面向对象的思想。

■　附录D：电子产品设计导论。说明电子产品设计不只是软硬件技术，唯有以用户体验为出发点，才可能设计出可被使用者接受的产品。

延伸学习

笔者专为本书设立了一个专属的Blog，可作为与读者之间的沟通平台。内容除了本书中既有主题的延伸与讨论之外，还会陆陆续续加上未收录在本书中的实例，以及嵌入式系统开发的相关新知识等。网址为：

http://tw.myblog.yahoo.com/embedded_system_book

若你有任何问题或指教，可以直接寄到以下Email Address，或在笔者的微博中留言，我当竭尽所能为你服务。

Email address: ealin.chiu@gmail.com

Facebook: ealin.chiu@gmail.com

Plurk: Big_Blackdog

Twitter: Big_Blackdog

致谢

笔者工作至今十多年，帮助和教导过我的人不知凡几，否则怎能累积经验以成本书？所有在我生活与工作中出现过的朋友，谢谢你们，是你们督促我学习与成长！还有目前KME工作室的伙伴们，谢谢你们，别忘了我们要共同实现的美好目标，美好的旅程才刚刚开始！

我还要感谢的是我亲爱的老婆——俞娜。在我埋首于工作与写作时，她无怨无悔地照顾好我的一家老小，与一只狗、一只猫，让我毫无后顾之忧地写完这本书。当然，还要谢谢出版团队后期制作的细心与努力，你们的专业让这本书除了专业，更添质感！

最后感谢翻阅或购买这本书的你，希望本书的内容真的对你有所帮助。

Ealin Chiu于台北

2010年8月20日

Chapter 1

系统·嵌入·硬件

本书主角及背景

这是一个信息本科毕业生，从对嵌入式系统一知半解，蜕变为电子产品开发领域高手的故事，文中简称为菜鸟。

刚从学校毕业的他，幸运地碰到一个不藏私且爱说教的项目经理，在一个个项目的磨练下，除了写程序和调试外，他做了许多之前认为软件工程师不该做的事情，包括焊接电路板、看原理图、使用仪器测信号、测试和客户或厂商用E-mail沟通等，甚至还帮忙编写产品规格书，也会三更半夜进工厂去处理一些并不像是软件bug的问题。

他自诩写程序的功力不差，逻辑能力强，专业课成绩也都很好；但在项目的进行中，才发现嵌入式系统或消费性电子产品的开发有太多未知的学问与技巧，更发现学校里教的东西居然大部分都派不上用场。

故事从他被一家约一百多人的信息电子公司以“研发工程师”的职务录取开始，在此之前，他只知道他的工作内容是“消费性电子产品”与“嵌入式系统”的开发[1]。

01-01　Welcome on board!

这天是菜鸟工程师报到的第一天，他怀着忐忑不安的心情踏入公司。

时间：01月10日　 地点：大会议室　 登场人物：菜鸟、项目经理（PM）

PM：“Welcome on Board!欢迎加入我们团队，我希望你在正式报到之前，能先了解一下工作性质。如果可以的话，我想先帮你做岗前培训。”

菜鸟：“嗯，我会努力学习的。”

PM：“你不用紧张，我先带你参观一下工作环境，介绍一下各个部门的成员。对了，本项目团队目前负责一个日本大客户交付的电子产品开发，我是这个项目的PM，也就是项目经理，我需要一个助手，以后我就是你的主管，我会让你尽快上轨道的。”

我们姑且称这个主管为项目经理（Project Manager，以下简称PM[2]），手下约有30名工程师，他总是在喊缺人，而老板认为他已经是公司中最大项目团队的主管。公司提供的资源已经够多了，但工程师每月加班工时居高不下却也是不争的事实。

他总想起第二次世界大战时德军的某个连队，在莫斯科前线，掩护其他部队撤退的故事。这个连长只剩下15个人，在零下二三十度的冬天，缺乏弹药的情况下，负责防守一公里的战线，如图1-1所示。他们面对前苏军营级部队一次又一次的冲锋，他们因为部署得宜，节制弹药的使用，再加上士兵训练精良，居然守了3天，让友军得以成功撤退。

PM：“你知道这个连队最后怎样了吗？”他老是喜欢讲这个故事给新来员工听。

菜鸟：“我很想知道!”看来菜鸟工程师对军事也很有兴趣。

PM：“全军覆没！能守住3天简直是奇迹了。但是你知道我为什么说这个故事给你听吗？”

菜鸟：“不知道，这和我以后的工作有关吗？”

PM：“当然有关！和国际大厂比起来，像我们这样中小型的开发公司，电子产品或嵌入式系统开发就是这样严酷的战场，研发团队没有足够的资源，却要承接越来越复杂的开发项目。我就是那个连长，而你是其中一个小兵，每个人都要负责过长的战线。如果有必要，我也得拿起步枪和你们一起冲锋。”

▲ 图1-1　残酷的战场

菜鸟：“有这么可怕吗？”

PM苦笑：“别担心，那位连长只有15个人，至少我还有30人……”

成功的以寡击众只是偶然的奇迹，绝对不会是常事，然而电子业这一行，在第一线人员严重熬夜操劳以及用聪明才智的努力下，却不乏奇迹的故事。

01-02　嵌入式系统开发团队

菜鸟：“你的意思是说，我们的团队也是编制不全的啰？如果是这样，我们怎么还敢承接那个日本国际大厂的开发项目？”

PM：“刚刚谈的是人力与资源[3]的匮乏，我可没说我们团队编制不全。一般研发单位都有很严谨漂亮的组织架构，但这不代表每个小组都会有合适且足够的人力配置。先不管团队中现有人力的多寡与素质，就以功能面来看，一个开发嵌入式系统的研发团队，至少应该有以下的小组。

■　Project Management：项目管理组

■　Hardware：硬件组

■　ID design & Mechanisms：产品外观与结构设计组

■　Embedded Software：软件组

□　Firmware：固件组

□　System：系统组

□　Application：应用程序组

■　Testing & QC/QA：测试与质量小组

■　Supporting：支持组（生产联系、美工、法务、行政事务等）”

菜鸟：“除了硬件与结构小组之外，基本上，好像和课本写的软件开发团队也相差无几吧？”

PM：“但课本恐怕没跟你说有些组员的工作是跨组的吧！应该这样说，我们团队的状况是，几乎没有人只会或只需要做某个小组的工作。举例来说，项目刚开始时，项目管理组会比较忙，此时，几乎所有人都要帮忙评估规格、进度以及技术可行性，至于设计阶段则几乎是全员参与。当硬件工程师开始找零件、规划硬件架构，固件工程师必须和硬件工程师密切合作。同理，在项目的不同阶段，各有不同的人力瓶颈。前期阶段，上层的人要帮底层的人；中期阶段，固件的人要帮系统的人，系统的人要帮AP的人；后期阶段则所有的人都要帮忙测试与debug等。”

菜鸟：“一个人负责多件事情不是很容易造成混乱吗？这样工作效率会高吗？”

PM：“这也是没有办法的事，我们只能尽量做好时间与人力的配置。在团队的组织表中，每个重要的工作项目都有其对应的小组及负责人，这些小主管必须对项目状态、风险管理及人力调配付出相当多的精力。我就只有这些人力，为了调配他们去追赶几乎无法达成的出货日期，只好把每个人都当成全能在用，这并非我们公司与项目独有的现象，我想多才多艺[4]是业界对‘优秀嵌入式系统工程师’的基本要求。

但请注意，我可不是说时间与人力吃紧时就允许草率地执行设计工作，人力短缺就可以不写文件！人少有人少的办法，无视设计与文件工作，不但不能节省时间，反而导致项目失控或失败，但教科书上往往无视这个普遍存在的现实。”

菜鸟：“你是说，软件工程那套对嵌入式系统开发不适用啰？”

PM：“你还是没搞懂。第一，我从来没有贬低软件工程或对象导向开发模式的意思，它们也都是经验和天才的累积。我的重点是软件工程教科书中无法完全考虑所有的现实状况，偏偏这个状况又是业界普遍的现象。第二，上述的现象并不只发生于嵌入式系统或电子产品开发，我想，一般的软件公司或游戏开发公司也有这种看似漂亮的组织表，但人力永远捉襟见肘的状况吧！”

菜鸟：“我大概懂了，但我还是不知道嵌入式系统开发和一般软件开发有何不同，我的工作内容到底是什么？还有，您会把我分配到哪一组啊？”

01-03　老调重弹：何谓嵌入式系统？

PM：“别急！工作内容我会慢慢告诉你。在此之前，你先说说对嵌入式系统的了解，何谓嵌入式系统？”

菜鸟：“按照书上的定义，嵌入式系统是一种用于特定用途的计算机系统。例如，汽车里的ABS刹车系统就是一个小型的计算机系统，有一个CPU执行着内存里的程序，不断地接收外界的输入，计算后输出结果。在ABS这个例子里，输入是轮子的状况与驾驶员踩下刹车的急促程度，输出则是对刹车器的控制。”

PM：“不错嘛！你的概念还蛮清楚的！有一本教科书是这么说的：

嵌入式系统是计算机软件与硬件的综合体，也包括机器或其他的附属装置，而这个综合体设计的目的，在于满足某种特殊功能。

还有另一个定义也颇传神：

以应用为中心、以计算机技术为基础，软硬件可裁剪，系统对功能、可靠性、成本、体积、耗电量和应用环境，有特殊要求的专用计算机系统。是将应用程序、操作系统和计算机硬件集成在一起的系统。

IEEE定义为：嵌入式系统是'用于控制、监视或者辅助操作的机器、设备或装置，（原文为devices used to control、monitor or assist the operation of equipment, machinery or plants）,其特性为：

■　通常执行特定功能

■　内含嵌入式微处理器

■　严格的时序和稳定性要求

■　全自动操作循环

而维基百科的定义则是：嵌入式系统是一种完全嵌入受控器件内部，为特定应用设计的专用计算机系统。与个人计算机这样的通用计算机系统不同，嵌入式系统通常执行的是带有特定要求预先定义的任务。由于嵌入式系统只针对一项特殊的任务，设计人员能够对它进行优化，减小尺寸降低成本。由于嵌入式系统通常进行大量生产，所以单一个的成本节约，能够随着产能进行成千上万的放大[5]。

我觉得这些定义都太绕舌。简单地说，就是那句我们常听到的口号：软件加值硬件（通过软件，为硬件提高价值）。图1-2所示为一个CPU，加上不同的设备，再配合不同的系统与应用程序，就可以开发出完全不同用途的电子产品——这就是嵌入式系统。”

▲ 图1-2　软件加值硬件

笔者比较喜欢的定义如下，嵌入式系统的特性都表现在粗体字上。

Embedded System use general or specialized[6] purpose CPUs running custom software along with specialized hardware to perform applica-tion-specific functions.

基本上，这些定义大同小异，主要是范围的差别，最大的范围可以包含硬件设计，甚至时下最流行的SoC (System On Chip)都可以纳入嵌入式系统的范畴。但万变不离其宗，说穿了，嵌入式系统的本质就是以下两点而已。

■　计算机系统

■　特定应用

1-3-1　嵌入式系统本质（I）：计算机系统

首先，嵌入式系统必然是一个计算机系统，计算机系统的定义如图1-3所示，由4个主要部分组成。

■　CPU

■　内存

■　输入设备

■　输出设备

▲ 图1-3　典型的计算机系统

无论个人计算机、超级计算机或者任一嵌入式系统，至少有一个或一个以上的CPU；而CPU的计算能力根据其应用领域相差何止千万里远，在一般PC的CPU运行速度已达3 GHz大关的今日，诸如8051、Z80、80186等历史悠久的CPU仍隐身在许多你我身边的产品内，如图1-47所示；更别提一些在特定的领域中大放异彩的CPU，恐怕你连听都没听过。

▲ 图1-4　8 bit CPU-8051

除了CPU之外，计算机系统的程序必须存储于内存之中。同样的，根据产品的特性，所使用内存大小与种类都大不相同，而嵌入式系统内的内存使用更是一门学问。说穿了，无非就是成本与性能的抉择。在许多应用中，成本的重要性出乎意外的占了上风[7]，本书会于专门的章节讨论内存。

计算机系统必定是为了解决某种特殊的问题而设计，复杂的PC是如此，嵌入式系统更是如此。解决问题的过程包含数据的输入与结果的输出，在嵌入式系统的领域中，除了处理数据的算法随产品应用领域而不同之外，输入与输出的种类更是无奇不有，其中大部分直接牵涉了硬件的操作。举例来说，冷气恒温控制器的输入是来自检测温度的芯片，输出则是对冷气强弱的控制；Wii的无线游戏杆，输入是来自使用者操作导致的方位变化或被按下的按钮，输出则是通过红外线将这些处理信息打包后送回Wii主机。这样的例子不胜枚举，有些产品可能没有明显的输入，但肯定要有某种形式的输出，否则就失去制作这个产品的意义了。

附带一提，随着IC代工业的兴起，IC设计公司也如雨后春笋般一家家成立，上述所谓的计算机系统几乎已可做到一个IC里面，如图1-5虚线框起来的部分所示（没被完全框起来的方块表示内存、输入/输出设备可以整合到CPU内部，也可外挂在CPU外部）。对电子产品开发者而言，硬件设计的复杂度与出问题的机会当然都会大幅度降低，但对软件系统来说，所开发系统的基本原理与可能要面对的挑战则是完全一致的。

▲ 图1-5　包含在IC中的嵌入式系统

举例来说，图1-6所示的IC功能是将输入的字符串转为音频流并输出，这样的功能称为TTS ( Text To Speech )，可以看到这个IC的内部有CPU与内存（用以存储与执行TTS转换程序），并且有明确的输入（字符串）与输出（声音），根本就是一个不折不扣的嵌入式系统。

▲ 图1-6　包含在IC里的嵌入式系统（TTS Engine）

1-3-2　嵌入式系统本质（II）：特殊应用

再来谈到嵌入式系统的第二个本质——特殊应用，这一点可引申出嵌入式系统更多的特质。嵌入式系统应用范围何其广大，从家里的微波炉、男厕的自动冲水系统、现在流行的游戏机、学生用的电子辞典、汽车里的行车控制系统（如图1-7和图1-8所示），到宇宙飞船的燃料控制、核电厂的监控系统等。在一个大系统中，往往包含很多小型的嵌入式系统，以个人计算机为例，键盘、光驱和显示器里都包含着一个计算机系统，CPU执行内存里的程序，处理着不同性质的输入，并输出不同性质的结果。

▲ 图1-7　一台汽车内包含许多嵌入式系统且自成网络(1)

▲ 图1-8　一台汽车内包含许多嵌入式系统且自成网络(2)

因为电子产品性质的各有不同，嵌入式系统的开发很难有一套统一的标准，没有一个国际标准组织或学术单位，规定嵌入式系统一定要用什么CPU、一定要使用什么程序语言开发、一定要用什么操作系统（有时候要不要用“操作系统”都还是个问题)、一定要用哪套开发工具，实际上，开发者并不需要因此而感觉无所适从，因为无论开发什么电子产品，基本的开发技巧与思想是一致的。

用一个比较传神的说法，嵌入式系统的开发很像太极拳，需要的是思想、基本功及实战经验，拘泥于招式与规定只会做出一个四不像的产品。简单的说，在开始设计某个系统时，弄清楚产品的应用范围与详细规格是最重要的事情，因为产品规格必然会影响以下项目的设计。

■　开发进度规划

■　预算与成本规划

■　资源调配

■　CPU的选择

■　硬件的设计

■　软件的架构

■　测试计划

■　生产流程

举例来说，汽车的ABS刹车控制器，可能使用运算能力一般的CPU就可以了，但因为牵涉到人身安全，系统必须稳定且要求有严谨的测试计划，如太阳能电子计算机，除了计算结果的正确性之外，成本及省电却是更重要的考虑。每个产品都可以说出它和其他产品的不同处。在此不妨做个简单的练习，找一个你随手可得的电子产品，如MP3随身听或智能型洗衣机，如果是你来设计这个产品，最重要的考虑是什么？

实际上，许多项目都是在规格尚未确定的状态下就开工，这就如同只知道客户要你做一辆车，但却不知要做牛车、马车还是汽车；或者即使知道要做汽车，却又不知道客户要你做双B跑车、国产房车、还是火柴盒小汽车等。听起来很荒谬，但这种闹剧却在业界不时上演。

PM：“我希望你能对嵌入式系统开发有正确的概念，试着训练自己对产品设计的敏锐度，我要你选出10个身边的电子产品，分别列出它们的输入与输出是什么？并且以设计者的角度，列出设计这个产品时的重要考虑。以下是一些在产品设计阶段必须考虑的事情，你也可以试着思考还有哪些不在清单内的影响因素。”

■　成本

■　外观

■　预计销售市场与消费群体

■　CPU计算能力

■　内存大小

■　省电需求

■　稳定度

■　反应实时性（Real-Time）

■　软件复杂度

■　测试复杂度

PM：“等正式报到当天交给我，这是你第一个任务，也是很好的岗前培训。”

菜鸟：“是。”

1-3-3　何谓嵌入式系统？

PM：“谈了那么多嵌入式系统的概念，考你一个问题，个人计算机算不算是嵌入式系统？”

菜鸟：“PC是个开放式系统，以Windows或Linux操作系统来说，它并不是专用于某种特定用途，使用者可以在上面执行任何程序。在某些时候，它可能是一台多媒体影音播放器；在另一个时候，它又像是一台上网机、打字机、游戏机，甚至是电子计算机。因为个人计算机的通用特性，所以它应该不是嵌入式系统。”

PM：“其实我这个问题是有陷阱的。刚刚说过，PC里的键盘、显示卡等就是一个个嵌入式系统，如果我们只看主机板，当主机板上电后，CPU执行的第一行程序是什么？存储在哪里？”

菜鸟：“这我知道，在操作系统启动之前，BIOS会先被执行，BIOS会去做self-test的动作，检测硬件有没有问题，如果使用者有按下特殊键（如F2键）的话，就会进入BIOS的设定功能，否则就会到硬盘、光驱或USB设备的特定地址加载操作系统。至于BIOS存储在哪里我就不清楚了，但应该不在硬盘里才对。”

PM：“不错！很有概念，以前的BIOS是存在Mask ROM里面，后来为了让使用者可以自己更新BIOS，现在的BIOS都是存在可擦写的Flash里面。看一下如图1-9所示的主机板，除了零件多一点外，它和一般电子产品的电路板有什么不同？主机板加上CPU以及内存后，即使没有硬盘，BIOS也会被执行，而BIOS是有特定用途的程序，它负责检测硬件以及加载硬盘里的操作系统。所以我说PC的主机板就是一个嵌入式系统。”

▲ 图1-9　没有硬盘的主机板也可以算是一个嵌入式系统

PM：“举个简单的例子你会更了解。假设我能改写BIOS程序，我可以更改它的用途为当使用者按下某个键，就会从PC喇叭播放生日快乐歌，那么，同样这块主机板就变成了特定用途的嵌入式系统。当然实际上不会有人设计那么贵、计算能力那么强的硬件平台，只为了播放一首歌！”

菜鸟点头：“的确是蛮无聊的应用。”

PM：“如果我稍微改变一下应用你可能就不会觉得无聊了，你知道大部分的测量仪器，如示波器，就是一片PC主机板加上精确的测量设备，以及分析信号的程序所组成的吗？你可以看一下这台逻辑分析仪，开机时你甚至可以看到Windows的执行画面如图1-10所示；因为它具有特定用途，我们也可以说它是嵌入式系统。此外，你不妨研究一下工业用PC，这类的产品除了特定用途，必须能在恶劣环境运行、对稳定性要求甚高之外，就是一台PC。当然，它是一种嵌入式系统，而且是很贵的嵌入式系统。再看如图1-11所示的这台公用电话，虽然里面执行的是Windows XP，但它也是一种嵌入式系统。”

▲ 图1-10　逻辑分析仪——执行Windows的嵌入式系统

▲ 图1-11　执行Windows XP的公用电话

菜鸟：“没错！我曾看过银行自动提款机死机后重启的画面，就是我们熟悉的Windows画面。我懂了，一个产品是否为嵌入式系统，和价格、大小、CPU计算能力与使用地点都无关，必须视其是否为了特定用途量身定做。”

PM：“没错！你要记住其实某项产品是否符合嵌入式系统的定义并不重要，重要的是开发者必须完全了解嵌入式系统的本质，以避免在设计开发阶段做出错误的判断与决定。”

1-3-4　电子产品的核心：CPU、MCU、DSP、SoC、ASIC

菜鸟：“计算机系统里一定会有个主控IC，刚才的图里都称其为CPU，但我们常看到一些不同的名字，最常见的是MCU或SoC，此外，我记得还有专长于运算与信号处理的DSP，以前报章杂志也经常看到ASIC产业相关新闻，这些名词我常常搞混，但它们在计算机系统里的地位有差异吗？”

PM：“其实这也没什么大不了，只是在说明嵌入式系统的多样性罢了！举例来说，如果要选个Power IC，你会考虑什么？”

菜鸟：“这个问题对我来说似乎太难了 ！细节我无法讲清楚,但我觉得市面上可选择的Power IC一定很多，因为每个产品需要的电源状况都不相同，可能有的只需要一组3V的电源，有的需要两组1.8/3.3V的电源，有的产品对电源的误差要求很高，有的则要求转换效率等，再加上成本因素，看来要考虑的事情还真不少。”

PM：“既然Power IC都必须根据产品特性而有不同的选择，更何况对电子产品最重要的主控IC？所以市面上有general purpose的CPU、有计算能力没那么强的MCU、有整合了许多装置的SoC、有强于计算的DSP、有专为客户设计的ASIC，这么说你应该可以理解了吧？至于各个名词的定义你去问Google或查维基百科就会知道了。”

我们经常会混用这些名词，笔者认为这不伤大雅，只要在产品设计时，能正确地选择真正适合此产品的主控IC即可，在此仍节录这些名词的定义如下。

■　CPU：中央处理器（Central Processing Unit, CPU）是计算机系统的主要设备之一。其功能主要是解释内存中的指令，并处理内存中的数据，所谓CPU就是遵循冯·诺依曼架构（Von Neumann Architecture）设计的装置。CPU的运行原理可分为4个阶段：取指（Fetch)、译码（Decode)、执行（Execute）和写回（Write-Back）。简单地说，CPU是一个概念性的名词，但用CPU称呼任何种类的主控IC都算妥当。

■　MCU：微控制器（目前在中国仍多沿用“单片机”的称呼），是把中央处理器、内存、定时/计数器、输入/输出接口等都整合在一块集成电路芯片上的微型计算机。与应用在个人计算机中的通用型微处理器相比，它更强调自供应（不用外接硬件）和节约成本。其最大优点是体积小、整合性高，但存储量小，输入/输出接口简单，功能较低。图1-6所示就是MCU的典型架构，IC内整合了一个CPU core，以及其他的装置或I/O接口。

■　SoC：是将一个完整的计算机系统，加上部分的电路，放入一个芯片内。这个芯片可能会包含数字电路、模拟电路、混合信号及射频电路等在内。SoC通常也会整合一块非挥发性内存（如ROM或One Time Programming Memory, OTP）在内，包含在其中的程序也会被称为嵌入式系统。

■　ASIC：特殊应用集成电路（Application-Specific Integrated Circuit, ASIC），是指根据特定用途而设计的特殊规格逻辑IC。品种多、批量少，要求设计和生产周期短，作为集成电路技术与特定用户的整机或系统技术紧密结合的产物。与通用集成电路相比，具有体积更小、重量更轻、功耗更低、可靠性提高、性能提高、保密性增强、成本降低等优点。

■　FPGA ：主控IC的另一个选择是直接在产品上使用“具有可程序化（Programmable）特性的FPGA”出货，相较于专门客制化的ASIC, FPGA在性价比上越来越具竞争力，且已致力于最为人所非议的耗电量的改进，对ASIC产业是不小的打击[8]。

■　DSP：数字信号处理器（Digital Signal Processor），简单的说，DSP就是一种专用于数字 信号处理的微处理器。相较于一般微处理器，DSP具有特殊硬件及指令设计，以及分开的程序和数据存储器（Harvard Architecture，哈佛结构），可使其更有效率地存取数据。一般我们说CPU擅于控制，而DSP则擅于运算。DSP的应用领域如表1-1所示。

表1-1　DSP的应用领域

项目

应用领域

实例

1

数字AV音响

DTS、Dolby AC-3、Pro Logic、MPEG2

2

数字通信

MODEN、行动电话

3

伺服控制

机械人、光驱、硬盘

4

电力电子

马达系统、变频器、UPS

5

信号处理

噪音消除、数据撷取系统

6

数据处理

数据压缩、数据加密解密

7

图像处理

车牌辨识、指纹辨识、脸型辨识

8

绘画处理

计算机绘画动画、CAD/CAM

9

语音处理

语音辨识、语音压缩

10

汽车工业

引擎点火控制、ABS、主动悬吊系统、噪音消灭

此外，MCU和DSP在应用上不一定是互斥的。图1-12所示是一个MP3 IC, IC里包含了一个8 bit MCU以及一个DSP。MCU负责NAND管理与流程控制，而DSP则负责处理MP3数据，MCU与DSP都只执行其最擅长的事情，所以，当然可以用最低的成本以最有效率的方式完成工作。

▲ 图1-12　MCU与DSP通力合作

1-3-5　案例研究（I）：使用与PC同等级CPU的嵌入式系统

菜鸟：“既然谈到CPU，就不能不让人想起PC上的X86系列CPU。X86应该是个十分稳定的平台，相关的研发资源也相当丰富，想到可以执行Windows、Linux以及无数的应用程序，相信就让无数开发商垂涎三尺了。虽然X86 CPU比较贵、耗电也较高，难道就真的只能用来做工业计算机或测量仪器之类的产品吗？”

PM：“X86背负着CISC架构的原罪，而一般研发人员早就被灌输CISC不适合用来当作电子产品CPU的观念，然而，这个观念并不一定在每个情况都适用。对了，你应该知道CISC和RISC的分别吧？”

菜鸟：“以前在学校学过，CISC与RISC主要是指令集长度上的差异。CISC的指令较具多样性，而为了支持更多特殊功能的指令，指令长度就无法一致。所以CISC需大量晶体管支持各种指令，并且需要高频率才能拥有不错的性能，但相对大量晶体管与高频率，却会带来高耗电。”

PM：“没错！耗电就是CISC最大的致命伤，但厂商仍不断的设法改善架构（例如，为提高性能，会把x86架构下的CISC指令先译码成类似RISC指令，再利用各种方式同步处理、加速执行），使CISC与RISC理论上的差距不再那么遥不可及。此外，通过对X86进行SoC化（即将x86所需的所有外部功能，与处理器运算核心（Core）封装在同一IC内），如图1-13所示，使得用这些SoC进行产品设计不像做PC主机板那么复杂，目前已有相当多SoC化x86处理器，应用在诸如健保IC卡片阅读机、停车管理系统、电子式水电表、超市查价机、心电图设备、CNC控制、PLC、网通设备、终端机、交通号志控制系统等领域。

而且换个角度想，在部分应用领域中，主控IC的单价与耗电量不见得是其最重要的考虑因素，而且如你所说，Windows与Linux上开发环境的便利性与现成的资源，对某些领域的产品开发商有着极大的诱惑力，所以采用X86核心的SoC目前也在某些领域被广为使用。”

▲ 图1-13　采用x86 core的SoC架构

菜鸟：“这又符合了您刚刚说的思想，进行嵌入式系统或电子产品开发，一定要先彻底分析最终产品的特性，在主控IC或零件的选择上，千万不可固步自封。”

1-3-6　案例研究（II）：“藏”在IC内部的嵌入式系统

菜鸟：“讲到这里，嵌入式系统真的是无处不在，除了我们日常会碰到的3C商品、家电、玩具、手持式装置等小玩意外，还有逻辑分析仪等测量仪器这种价值不斐的大东西。此外，我们刚刚说到那个可以做TTS (Text To Speech）的IC也是一个具体而微的嵌入式系统，如图1-6所示。我比较好奇的是，这种被‘封印’在IC里的嵌入式系统是怎么开发出来的？它不像一般嵌入式系统有个开发板，可以外接一些debug装置，而且在开发阶段，程序怎么‘烧’到IC里的ROM? ROM应该是只读的啊！”

PM：“其实开发的流程与原理是一样的，但在这个开发流程中，软硬件工程师必须和IC designer一起co-work。嵌入式系统开发团队的开发环境当然不会是一个IC，而是一个以FPGA为主控的板子，你可以从图1-14所示看到，FPGA的板子和一般硬件板子没什么太大的不同，但FPGA中电路是可程序化的。”

▲ 图1-14　采用FPGA的主控的板子

PM：“IC designer以硬件描述语言（Verilog或VHDL）所完成的电路设计，可以经过简单的综合与布局，快速地烧录至FPGA上进行测试，就好像一个电路试验板被放在了一个芯片里，对软件工程师而言，这个FPGA的功能应该与最终要开发的IC完全相同，只是在IC设计阶段，FPGA的行为难免会有些bug，而软/硬件工程师也必须负责从系统应用的观点，帮这个开发中的IC找出潜在的问题[9]。

言归正传，以图1-6所示这个TTS IC为例，利用FPGA平台进行IC内嵌入式系统开发的流程如下。

Step01：如同一般电子产品项目，硬件工程师进行硬件板子设计，主控IC为FPGA，除了这个IC在应用时需要的外部装置之外（如喇叭、ICE接口、UART传输接口等），还必须包含用以更新FPGA内部电路（程序）的接口。

Step02：IC team必须先提供一个功能与实际IC几乎完全相同，但ROM（in FPGA）中没有数据的FPGA版本，并协同硬件工程师确认FPGA与板子共同运行无误。

Step03：软件工程师将编译好程序，使用ICE下载程序到SRAM （in FPGA）执行与调试。开发中碰到任何问题时，必须考虑可能是FPGA内电路设计有误,并与IC designer 密集讨论。

Step04：当软/硬件工程师在FPGA板子上完成系统开发后，将程序复位后交付IC designers（在开发阶段，程序是在SRAM上执行，但实际IC在运行时，程序必须在ROM里执行），IC designer会再提供一版FPGA，待软/硬件工程师验证无误后，才可进入IC制造流程。

Step05：IC里的ROM不是用烧的，而是在IC设计阶段，将程序或数据写入ROM的电路。”

01-04　限制！限制！限制！

PM：“这个组就是我们的固件组。你可以看到每个人桌上除了个人计算机外，有着各式各样的板子、仪器、文件和一堆不知名的线。前阵子大老板要我要求所有工程师保持桌子的整齐，我马上向他报告此事窒碍难行，不同领域的员工自有其工作模式，身为管理者必须为其创造最能发挥工作力的环境，不应该妄加限制。”

菜鸟：“好像负责固件的人数不是很多，面试时您曾提过除了正在开发的产品外，我们已经开始设计下一代的硬件平台。我以为开发阶段应该会有许多问题等待解决，而新的硬件平台设计应该需要更多的人力，但我们就这四五个人，够吗？”

PM：“确实不够，但这是我目前仅能挤出的人力，况且也不是每个人都适合做固件开发的。这样说吧，若非不得已，不会有任何一个军队指挥官，会把一条一公里的战线交给只剩15人的连队防守，基本上，只有15人的连队根本就是不合理的存在，然而，这是不得不的现实。”

菜鸟：“老板知道这样的事吗？”

PM：“当然，其实老板正是始作俑者之一！哪一位老板不希望用最少的资源创造最大的价值？公司存在的主要目的是什么？不就是营利吗？这就是我这个专业项目经理存在的原因之一。嵌入式系统开发本来就是限制重重的，人力缺乏只是其中之一而已，而我的责任就是妥善统筹资源、人力与时间分配。”

在1-3节中说明了嵌入式系统的多样性，接下来，我们从以下5个方面来说明嵌入式系统的另一个特性——限制。

■　产品规格设计

■　人力分配

■　进度管理

■　硬件设计

■　系统设计

1-4-1　产品规格的限制

嵌入式系统和一般软件项目有个显著的不同点，嵌入式系统完全应用于电子产品，而电子产品的开发必然牵涉硬件、结构、外观设计及其制造成本，也就是说，软件并不是某件电子产品是否热销的唯一原因[10]。

任何电子产品的开发计划都始于需求或创意，而创意的实现却往往被制造成本所限制，也就是说，制定电子产品规格时必须有所节制，不能天马行空的无限想象。举例来说，要用国产车的造价，设计出奔驰同等级的车款，无异是缘木求鱼。成本会影响规格，而规格则会影响所有与开发有关的工作。生产电子产品必须在成本与规格上取得微妙的平衡，如果规格及制造成本都不愿让步，那就只能考验开发团队的能力了[11]。

偏偏现实世界是大部分的客户或制定产品规格的人员都高估工程师的能力了。确切地说，应该是高估了目前计算机与管理科学的水平。在PC上某些很简单的事情，要在8位CPU，或少得可怜的内存（可以想象整个系统的内存容量居然是以KB或byte为单位来计算的吗？）的平台上实作时，需要的算法有时都可用来写论文了。

结论是：因为制造成本的考虑，电子产品在制定规格时便限制重重，而产品规格更是直接影响了运行其中的嵌入式系统的开发。

1-4-2　人力分配的限制

嵌入式系统开发并不一定都是复杂的项目，例如“电子宠物机”就是一个技术简单、成本低廉的产品，但其所牵涉的人力或技术种类却肯定比一般软件项目要多。以下简单列出嵌入式系统开发流程不可或缺，但一般软件项目可能不需要的人力资源。

■　硬件设计工程师

■　PCB Layout 工程师

■　PCB制作协助厂商

■　结构工程师

■　模具制造协助厂商

■　提供硬件零件的厂商（如CPU、设备IC、内存、LCD模块、电阻电容等）

■　固件工程师

■　硬件测试协助厂商

■　硬件品管及验货人员

■　工厂

即便只负责软件开发，也无可避免地要和其他单位协调与合作，如果软件工程师能稍具其他领域的知识与技能，对嵌入式系统的开发绝对大有裨益。但现实情况是要找到具备与其他领域人员沟通的热诚、管理协助厂商的能力，并兼具程序设计之外技能的软件工程师确实不多，只能让工程师从工作中学习，等工程师好不容易渐入佳境，却又要担心被其他公司挖角，这是每一位管理嵌入式系统项目的项目经理心中无法抹灭的痛！

嵌入式系统开发在人力资源上的限制不仅仅是项目人数的问题，主要是牵涉单位太多，而各单位各有自己的文化及语言，管理的复杂度自然大增，若想把所有相关单位都纳于同一公司或团队之内，或要求每位员工都必须多才多艺且可同时执行不同领域的工作，却也显得不切实际且成本过高。

嵌入式系统不一定比一般软件项目复杂（其实有些电子产品的软件简单的出奇），也不一定需要更多或更高素质的人力，但“人”的管理，绝对是嵌入式系统项目能否顺利结项最重要的因素之一。

1-4-3　进度管理的限制：测不准原理

嵌入式系统的进度计划（Schedule）管理与人力管理面对同样的问题与限制，即参与开发的单位通常很多。项目主管一方面要花时间精力协调各单位，以便精准控制进度；另一方面，某项任务的延误，往往会影响其他单位的开发，尤其当这个单位隶属于别家公司，甚至这家公司不在国内时，不理性的争执、责任的推诿以及语言与文化的隔阂，必然让进度延误的状况雪上加霜。

图1-15所示是个典型的嵌入式系统的进度计划表，你可以发现很多任务是有前后关系的，而这些任务之间又往往属于不同领域、牵涉到不同公司。实际上，因为不确定性因素太多，进度计划表几乎都是边做边修正的。

项目中的任务之间必然会有一些相依性。举例来说，硬件的延误会影响软件的开发，而驱动程序的开发延误也可能影响硬件设计的验证，产品规格迟迟无法确定，则会打乱其他开发单位的步调，零件供货商供货出问题，则会直接影响生产，若到开发中后期，某零件无法供应，则直接受冲击的将是硬件设计以及固件工程师，整体项目进度势必推迟。

以图1-15所示为例，最常发生的悲剧是所有时间表都是由预计量产时间点往前推导得来。举例来说，营销部门希望12月底产品可以上架贩卖，所以往前推12月5日必须开始量产，再往前推一个月必须做小量试产，也就是说，在11月5日前硬件所有问题都要解决并完成备料。再继续推演。假设公司规定试产后软硬件设计就不该再更动，那么，既然11月5日要小量试产，软件应该要在半个月前就绪，而根据经验，软件要经过3轮QC测试才能将所有问题解决完，所以推导出软件整合必须在10月初就完成，于是管理人员拍拍脑袋就把进度计划画出来了。

▲ 图1-15　各单位协同作战的进度规划

注意到了吗？上述推演是以销售时间推导量产时间，用量产时间推导硬件开发时间，再用硬件开发时间推导软件开发时间，其中完全没考虑到人力与规格复杂度，主事者的说法必然是：“如果软件没办法照我的计划完成，这个产品就无法如期上架销售，则项目视同失败，所以请大家尽量配合加班……”其中逻辑看似合理，其实相当荒谬，一个摆明无法执行的进度规划行同废纸一张。

而且嵌入式系统是软硬件整合，调试较为困难，测试负载量也较大。在产品应用日渐复杂的情况下，偏偏电子产品的生命周期越来越短，为求尽快将产品推到市场，开发时间往往被迫压缩。然而产品质量和上市时机（Time to Market）孰重孰轻没有一定的答案，要视情况而定，所以嵌入式系统开发的进度管理需要考虑的事情非常多，要订出合理且可满足质量与上市时间的开发进度，几乎是件不可能的任务。

身为项目经理，制定进度计划时只能秉持经验、知识与职业道德勉力为之。原则上，项目进入执行阶段就不该大幅度修改进度计划，但实际上，频繁的更改进度计划却是业界的常事。其实项目进度并不是测不准，主要是嵌入式系统项目牵涉的变量太多，偏偏预定上市时间又太赶，往往没做好完整的风险评估就贸然进入执行阶段，任何一件小事若发生推迟的状况，就可能对整个项目进度造成出乎意料的影响。

1-4-4　硬件设计的限制

嵌入式系统的硬件设计也是限制重重，成本考虑是造成这些限制的主因，其他会影响硬件设计的原因还包括：

■　产品规格与客户要求：产品规格当然会直接影响硬件设计，而负责制定产品规格的客户 常常有些特殊的要求。例如，指定使用该集团子公司生产的零件，或要求硬件设计必须符合其旧有产品的结构。尤其是某些传统的国际大厂自有一堆内部标准，从键盘的反应时间、系统的耗电流、音频输出的质量、必须通过环境测试的种类，一直到零件采购公司的限制等。举例来说，通常这些大公司内部都有一份可采购零件的厂商清单，如果硬件设计者忽略了这个限制，到后期才发现可就麻烦了。若一开始不弄清楚客户的要求，等到产品试产之后才发现问题一堆，除了影响进度，势必影响工程师的士气以及公司之间的合作气氛。

■　CPU特性：CPU是电子产品的心脏，当CPU选定后，硬件设计才可随之展开，嵌入式系统使用的CPU往往不为人所熟知。举例来说，EPSON可不是只作打印机而已，他们的半导体部门根据特定的应用领域设计了一系列的嵌入式CPU。该CPU内部集成了许多外围芯片，如SDRAM、LCD Controller等，广泛地应用在诸如电子辞典或电子书等产品上。除此之外，许多硬件产商为特定应用自行设计的ASIC或SoC更是不胜枚举。

CPU的特性与功能直接影响其他部分的硬件设计，举例来说，若CPU的GPI/O（General Purpose I/O；CPU中可用于一般用途的输出或输入脚位）不够，则势必要外加扩展I/O电路或芯片，假若CPU有提供诸如I2C或I2S等通信接口，硬件设计者就可以选用支持此接口的外围芯片，因为这些接口可以把多个芯片串在同一个bus上，不但控制简单，还可节省I/O脚位。此外，嵌入式系统领域里的CPU通常集成了许多其他外围IC，如此可减轻硬件设计的复杂度与工作量。

CPU的选择对产品开发有着决定性影响，选择CPU时必须在性能、单价以及可用资源上取得平衡。基本上，工程师不必奢望会有性能远超过产品应用需求的CPU可用，这是嵌入式系统工程师的宿命！

■　耗电流：开发PC或Web应用程序时几乎不需考虑系统耗电的问题，但在许多产品应用上，尽量延长使用时间却是重要的规格或功能之一，手机是最容易明了的例子。要延长产品的使用时间，软硬件都要配合，硬件设计上除了使用更长效的充电电池之外，还必须花时间选用较省电的零件。为了在待机时可以尽可能的把没在动作的零件电源关闭，硬件设计时必须考虑到软件的需求，如选用CPU某根I/O脚来当作某个零件的电源开关。

■　产品Size大小及外观：结构设计也可能影响硬件设计。同样功能的硬件零件要“摆”在开发初期用的大板子（Target Board）与最终产品的小板子（Real Size Board），在硬件设计的困难度并不是在同一个等级上的！其中牵涉到Layout走线以及为了抗干扰所增加的硬件设计。

图1-16所示有两张比较图，其机器具有完全相同的电路设计，但因为最终产品外观结构不同，其中一台机器必须分为两块板子，就硬件设计而言，其复杂度自然较高。

▲ 图1-16　产品外观对设计的影响

■　销售国家或地区：每个国家或区域对电子产品上市前要通过的检查标准都不同，简单地 说，同样类型的产品，销售某些非洲国家和销售美国、欧洲、日本等已开发国家就可能采用不同的硬件设计（这样说并没有任何轻视非洲国家的意思）。往往硬件设计为了提升一点性能必须付出极大的代价，例如，CA认证标准要求产品的抗静电能力必须达到某个等级，但有些廉价的芯片抗静电能力就很差，要解决这个问题，要不就得加抗静电电路、修改结构或加铜箔保护，否则就只能更换主控芯片，除了成本增加外，也可能影响其他部分的硬件设计。

■　工厂制造与备料能力：这是一个常被项目经理或工程师忽略的因素，硬件工程师设计出来的东西，最终当然希望顺利大量生产，但选中的工厂却不见得有生产这些产品的能力。工厂并不是只有组装而已，同样以手机的例子来说，要验证生产在线手机半成品的通信功能需要昂贵的仪器，并非每间工厂都负担得起。此外，有时选择零件还必须考虑工厂的库存与备料的能力，所以硬件设计初期，最好就能和工厂人员确认设计是否可行。例如，这个产品要用到某个型号的Flash Memory（闪存），在设计定案之前，应该先了解工厂是否有烧录该Flash的能力（如支持该Flash的 烧录器）。

1-4-5　软件系统设计的限制

嵌入式系统开发在软件上的限制显而易见，许多工作项目在一般软件开发项目上都是没有的，而本书大半的篇幅便是试图说明面对这个现实的因应之道，在此我们先列举如下。

■　不熟悉的CPU与硬件平台

■　不熟悉的开发环境

■　不易调试

■　CPU计算能力限制

■　内存容量限制

■　在不稳定的硬件平台上进行驱动程序开发

■　电源管理程序

■　工厂验证专用软件开发

■　嵌入式操作系统开发

■　仿真器开发

■　软硬件整合测试

■　稳定度与性能调整

■　Code Size调整

细节会在之后的章节陆续提到，本节仅用一个例子来说明嵌入式系统软件开发的“艰苦卓绝”。

在个人计算机CPU已迈入64位的时代，工作频率超过3 GHz，还可以多核心，内存容量从GB等级起跳，请想想，你可以用8 bit、2MHz的CPU来做什么？

答案是任天堂电视游戏机（红白机），虽然这已是老古董了，且效果当然比不上PS2[12]、PS3、Wii、Xbox 360等，但当你知道它的CPU是8 bit、1.77 MHz时，是否也对开发这个系统以及许多爱不释手游戏的软件工程师怀有钦佩之意？这是个20多年前的产品，当时许多软件技术都不如今日发达，他们是怎么做到的？即便其下一代主机“超级任天堂”的CPU也仅仅是16 Bit、3.68 MHz！

千万不要以为8 bit CPU的时代已经过去了，据统计，微控制器厂商（MCU）在产品布局是以8位微控制器为主力，其次为16位，主要锁定消费性应用市场。目前仍以8位微控制器的市场最大，就最典型的8位8051架构微控制器而言，全球一年的出货量就高达33亿个。这个数量已经明显大过个人计算机CPU的需求！

2001年嵌入式系统国际会议年会Jim Turley的报告中提到：根据统计报告，PC的数量只占CPU总销售量的0.1%,而且直到可预见的未来，这个悬殊的比例都不会有太大的改变。这就是嵌入式系统开发的业界生态——绝大部分的电子产品制造商，正使用着不为人所熟知的CPU，开发围绕你我生活的各种电子产品。

这也是嵌入式系统软件工程师的宿命之一，他们的价值不在于用过许多CPU，写过很多驱动程序，或开发过很多产品，而在于处处限制的情况下把产品开发出来，这意味着清晰的计算机系统概念、高效能的算法、良好的程序写作习惯以及有效率的资源管理。

01-05　基本职能：老鸟也曾是菜鸟

PM：“请进！这是我们的实验室，该有的设备一应俱全，以后你应该会常常待在这里，这是示波器，如图1-17所示，在学校用过吗？那边拉了一堆线到板子上的是逻辑分析仪。”

菜鸟：“上实习课时曾经看过，但好像和你们的不一样，学校的仪器都很旧。哇！你们的还是彩色的！”

▲ 图1-17　各种示波器

PM：“就是没用过啰！无妨，这很简单，以后我们会教你使用的。还有，你看一下，这是我们目前产品的电路图[13]，如图1-18所示。”

菜鸟：“好像很复杂，我一看这个东西就头大，密密麻麻的谁看得懂？我除了要用这些测量仪器外，也需要会看原理图？”

PM：“是啊！不然你怎么写驱动程序？”

菜鸟：“不懂！我之前用过微软的DDK写过一个USB设备的驱动程序，也没用过这些东西啊。”

PM：“应该这么说，驱动程序也是分种类与层次的。我来谈谈你在Microsoft DDK上做的驱动程序，首先已经确认硬件是OK的，再来基于微软已经成熟的驱动程序平台来开发。而嵌入式系统工程师在开发驱动程序的状况通常是从无到有的，更不好的消息是，开发初期硬件设计还没确定，我们的固件工程师通常还要协同硬件工程师一起帮硬件板子调试。在这样的状况下，你不测量信号，怎么知道LCD的Framerate（如图1-19所示）是否符合日本市场的70Hz要求？怎么知道键盘的反应Timing是对的？怎么知道某个芯片的电源开关已经打开,并已开始运行？除此之外，有时还必须同时测量多个信号间的关系，特别是在调试的时候，如图1-20所示。

▲ 图1-18　原理图实例

测量信号可以让固件工程师知道目前硬件的状况，写一堆驱动程序的目的不就是为了驱动并控制硬件吗？不会有人这么优秀或运气那么好，程序写完，一执行就OK了，如果你要调试，怎么可能自己不去测量信号？总不可能要我派个硬件工程师给你当助手吧！”

▲ 图1-19　信号测量实例(1)——LCD Framerate

▲ 图1-20　信号测量实例(2)——同时测量多个信号

菜鸟：“嗯，我可以理解用示波器测量信号确实是固件工程师必备的技能之一，那原理图呢？”

PM：“你不看原理图，怎么知道要测哪里，而且我是说固件工程师必须会‘看’原理图，我可不放心让你来‘设计’原理图，电路设计是硬件工程师的工作。嵌入式系统开发工程师必须“多才多艺”的另一个原因，是为了提升自己的工作效率，项目中其他人都很忙，我们不希望软件工程师遇到了简单的硬件问题就停下来等待硬件工程师的帮忙。而且你看，那位在改驱动程序的是位硬件工程师，而那位软件工程师居然也学会了拆换芯片。

年轻人多学一点有好无坏，久而久之，你会更了解这个业界的生态，而不是只懂软件或写程序的事情，懂吗？”

菜鸟：“我很有兴趣学习新的知识，也愿意接受挑战，但还是有点怕……”

PM：“我已经说过很多次了，别担心，我们会培训你的。以下是我认为要成为一个优秀的嵌入式系统软件开发者所必须具备的基本职业技能。”

■　要会写程序，尤其是C语言，而且程序必须精简有效率

■　数据结构与算法

■　完全了解计算机系统的运行方式

■　操作系统的概念

■　懂得系统的整个架构，包含电源管理等

■　必须了解硬件设计的精神与架构，要能看得懂原理图

■　基本测量仪器（电表、示波器等）

笔者遇到过许多博学多闻，看起来什么都懂的高手，其中有位高手令人印象深刻。他主要的工作是设计CPU，当该CPU准备推广上市前，他会帮忙设计评估板（或实验板）的原理图[14]，如果有空的话，他也会自己布局评估板线路，当评估板洗回来后，他会亲自验证，焊零件也都自己来，接下来还要写程序验证。他们公司提供给客户的评估板和示例代码都出自他一人之手，而他居然还是CPU的设计者！等到实际深交之后，才讶然发现他并非信息电机本科毕业生，本身是数学系毕业，在当了一段时间的中学老师后，才“转战”到信息界。

笔者提这个例子并非鼓励你要做一个通才，其实就在某个领域内出类拔萃也很好，毕竟这是一个精细分工的世界。笔者要点出的是，知识工作者必须放开心胸，只要是对工作或者对达成任务有帮助的，软件工程师不要排斥去学硬件的知识或技能，硬件设计者亦然。笔者常常听到这样的对话。

硬件工程师：“我觉得你们会写程序好厉害，想要什么功能改一下就好，那么复杂的程序是怎么写出来的？”

软件工程师：“我觉得你们可以做出一台实体的机器才厉害，什么电阻、电容的，我一个都搞不懂，那个原理图密密麻麻比我们程序复杂多了！”

笔者会给这两位嵌入式系统开发工程师一人一句建议：

软件工程师硬起来！

硬件工程师软下去！

01-06　工作内容：做个工程师，而非程序工人

菜鸟：“谢谢您今天的介绍，让我对嵌入式系统有了更深一层的了解，并对这份工作可习得的技能以及将面对的挑战有了更深的期待，但我还是不清楚自己的工作内容是什么，可以请您说得更具体一点吗？”

PM：“有两份工作让你选，一个是AP（应用程序）组那里缺人，另一个是我需要一个助手。前者的工作性质是根据制定好的设计规格，在仿真器上开发应用程序，如果没出大问题的话，基本上都是在PC上写程序与调试。后者以军事术语来说的话就是预备队与传令兵，哪里需要帮忙就补上，此外，可能要常常听我啰嗦几句。

你要选哪个？”

对一个初出社会的本科毕业生而言，职业生涯规划不外乎3种选项：

■　技术深耕

■　纵观全局

■　缺乏规划

除了第三个选项外，笔者以为要往“技术超人”或“项目管理”方向努力迈进并没有绝对的对错，但最好先审视个人的专长和个性。重点是项目管理者最好是经历过一定程度的技术训练，而工程师也不可以永远只停留在自己熟悉的领域里闭门造车，对产品的开发流程必须要有概念。

特别是嵌入式系统的开发工作，专业领域之广、开发限制之多、牵涉单位之复杂是其他软件项目比不上的，而且要求上至项目经理，下至基层工程师都要对全局有所了解。想要加入这行的你，务必先做好心理建设，敞开心胸接受各式各样的任务，例如，以固件开发人员的身份参与硬件设计审查会议，拿起烙铁、焊锡、电表以及示波器探棒检修板子，参与测试项目（Testing Case）的制定，解决仿真器正常但机器不正常的bug，甚至进工厂分析生产线出现的错误等。

当然，不见得任何嵌入式系统研发单位都可以让每位工程师触碰到所有的技术，特别是在人力充足、建置完整的单位，每个人的定位都是固定的。笔者曾经面试一位在手机制造大厂工作的应征者，他已是一位小主管，想要换工作的原因是：三年来他的工作内容只有一个——“手机窗口应用程序开发”，做了不知多少机种，随着时间流逝，他已然是这个小领域里的专家，但他对自己的能力越来越感到心虚，他觉得他和外界的发展脱节了，甚至他经手的机种从何而来、客户是谁、什么时候量产，以及最后销售成绩如何似乎都不关他的事。

笔者对新加入者的建议是：切记要做一位工程师（Engineer or Program Designer），而不仅是一位程序工人（Code Typist）。工程师除了具有创新、设计与整合的能力外，还必须具备纵观全局的视野，以及具有独立解决问题的能力与担当，而所谓的程序工人则仅需听命行事，照着设计规格写程序，虽然工作性质包装着“高科技”的外衣，但实质工作内容与一般付出劳力的工人没有两样。

而且工人的工作较不具发展性且容易被取代，工程师则不然！

[1]　请注意，文中提到的是“信息电子公司”而非“软件公司”，至于职务则是“研发工程师”而非“软件工程师”，如果可以的话，称之为“电子产品设计公司”的“嵌入式系统开发工程师”应该更能表达其工作性质。

[2]　一般企业往往弄不清项目经理与部门经理（例如软件研发团队经理）的职责界限（本书在项目管理相关章节会对此详细说明）

[3]　除了人力之外，所谓研发资源泛指时间、能力、预算与老板的支持等。

[4]　“多才多艺”其实是来自于以下两种人格特质：吸收新知识的求知欲与速度，以及解决问题的能力与态度。

[5]　可上中文维基百科（http://zh.wikipedia.org/）搜寻“嵌入式系统”，笔者认为其中有许多有用的信息。

[6]　原始定义中并没有“Specialized”这个形容词，但随着IC设计与半导体制造的进步，市面上已有无数的特定用途CPU可供选择。若有必要，你也很容易找到帮您量身定做主控IC的厂商。

[7]　笔者这里说的是一般工程师的意料之外的。对老板而言，世界上并没有完美的产品，就算有，也不是我们所处的团队可以完成的，更何况，能赚到钱的产品才是好产品，所以产品设计及开发人员都应该为了降低产品的成本而努力。

[8]　FPGA一般来说比ASIC的速度要慢，而且消耗更多的电能。但它们也有很多的优点，比如可以快速成品、可以编辑以改正程序中的错误，更便宜的造价。在一些技术更新比较快的行业，FPGA几乎是电子系统中的必要部件，因为在大批量供货前，必须迅速抢占市场，这时，FPGA方便灵活的优势就显得很有竞争力。

[9]　可参考《第16章：SoC设计公司中嵌入式系统团队的管理》。

[10]　除了技术问题外，更重要的是产品设计的出发点是否真的是以满足客户需求为主？《附录D：电子产品设计的最终依据：用户体验》对此有深入的阐述。

[11]　制定规格的人应重视制造成本给嵌入式系统开发带来的限制，否则，不但会为该项目带来灾难，而且对第一线的开发人员也是不道德的。

[12]　PS2与任天堂64这两台游戏主机都是使用MIPS RISC架构，运算能力当然比任天堂红白机好得多，但性能与PC上的CPU还是有一大段差距。

[13]　这里只是show一下原理图，你无须花时间了解这张图的实际内容与应用。

[14]　硬件电路设计和CPU设计是完全不同领域的两回事。

Chapter 2

嵌入式项目管理

时间：02月01日　 地点：培训教室　 登场人物：菜鸟、系统老鸟、项目经理（PM)

菜鸟：“这将是我参加的第一个嵌入式系统开发项目，‘项目’这名词常听到，但‘项目’到底是什么？我现在有两个老板，一位是研发部的软件部门经理，另一位就是您——项目经理了，同样是经理，你们的职责有什么不同？说白一点，研发部经理和项目经理哪位比较大啊？如果你们同时下命令给我，我到底应该听谁的？”

PM：“对工作资历不深的工程师，几乎没有人觉得他应该要了解项目管理的知识，更不要说你这刚进社会的菜鸟了。一来是他们认为学习‘专业’知识都来不及了，哪有空再去学项目管理？再则就是一般工程人员普遍认为项目管理没有什么难的，再难也不会比TCP/IP协议复杂吧！等需要时再学就是了。

个人认为这种观念影响企业界很‘嵌入式项目管理入门’的课，不管你是主管还是菜鸟，只要参与我的项目，我就会要求你们必须符合项目管理的精神来做事。看来这个项目还是不能偷懒，我就专门为你这菜鸟再讲授一次这个课程吧！”

02-01　菜鸟啊！要立大志！

菜鸟：“不是说这是专为‘本菜鸟’开设的项目管理课程吗？怎么其他项目人员也都来了？特别是学长，你都贵为老鸟了，怎么还来上这个课程？”

系统老鸟：“公司的教育培训就是这样，明明是专为某些人开设的课，真正有需求的人反而不来上。项目管理是很有意义的课啊！以我们PM老大的丰富经历，经常会信手拈来许多实例，我每次听这堂课都会有不同的收获。

公司的技术主管还算支持PM老大的想法，知道他要开课，就要求其他项目人员也来了。不管他们现在是抱着什么样的心态来听这堂课，当有一天他们真正碰到问题时，能够想起好像曾经听过有个概念或工具可以解决当前困境，那么，这堂课就值得了。”

菜鸟：“我的确没想那么多，老板叫我来上我就来了。我，小小工程师一个，真的需要现在就了解项目管理的思想吗？管理耶～等我爬到主管位置都不知道是猴年马月的事了！”

系统老鸟：“你的想法其实是错的！首先，项目管理的思想可以应用到所有公司或私人的大小工作上，项目管理教导我们：任何工作都应该先评估可行性，接着作计划，然后有效率地利用时间、成本与资源，并在可接受的范围内管控成果的质量。没有人不希望高效率的完成各项工作吧？

而且项目管理不仅仅只是项目经理的工作，一个电子产品开发项目牵涉到那么多单位，相关技术或执行细节如此繁杂，项目经理怎么有办法全盘管控？所以管理与开发工作必须层层把关，就算你只是个工程师，在此项目中只负责某一个模块的开发，当你接到这个任务时，也要把它当作一个小型项目来执行，同样要有计划、要做管控。而你就是这个小型项目的项目经理，必须要用有效的办法来完成这个模块开发。

因此，整个项目在执行时将会是一个‘递归’的概念，一个项目其实是由许多中/小型的项目所组成，所有项目成员必须以共同的理念做事，使用相同的语言沟通，自然项目的成功率就会高很多。

此外，身为一位工程师要随时为提高自己的管理能力作准备。信息电子业人员的流动率高，千万不要当你突然晋升为管理数人的小组长时才手忙脚乱。事实上，大部分的技术团队主管都还是由技术人员提上来的，出来工作打拼千万不要妄自菲薄。年轻人！要立大志！”

菜鸟：“你说的我会好好思考，上完这堂课，知道项目管理到底是什么之后，或许会对你的话有更深一层的理解吧！老板来了，准备上课啰！”

课程名称：嵌入式项目管理入门——所有参与项目成员都必须知道的项目管理知识

课程时数：3小时

02-02　项目管理基本概念

项目管理理论基础

——所有参与项目成员都必须知道的项目管理知识

PM：“首先感谢大家抽空来上这堂课。项目管理目的在于建立一套知识体系，提供高效完成项目的流程指导。任何性质的工作都可以是一个项目，所以，学习项目管理最重要的还是思想！这门课当然无法在短时间内解说所有项目管理的细节，但今天的教材是基于我多年的项目管理经验，体现个人认为所有项目成员都必须知道的项目管理思想、流程与工具，并适时提示嵌入式系统开发项目应该注意的地方。

这门课的副标题为‘所有参与项目成员都必须知道的项目管理知识’，也就是说，我所设定的目标对象并不仅仅只是项目管理者。如果你被指派加入某个项目，我希望你能认真地从这个简报里学到 一些正确的思想，以后就能用更有效率的方式工作，相信对你日后的职业生涯也会有很大的帮助。

首先，我们来看关于项目一些‘不能说的秘密’。

2-2-1　何谓项目

关于“项目”

■ 70%的项目失败是因为管理失败引起的，而非技术原因

■ 大约只有10%的项目能够在预定的费用和原定计划下交付

■ 大部分项目经理会认为项目管理是一种“意外的工作”

当企业想要培养具备某种技术专长，且可为公司所用的人才时，必须经过长时间、计划性的培训。例如，当公司给你一个之前从未做过的开发任务时，一定会给你时间自我学习，或采用师徒制（找个师父带着你在工作中学习），甚至是直接送你去外部的教育培训中心上相关课程（培训）等。

然而，电子产业的项目经理通常都由技术人员直接晋升，大部分的项目经理在就任之前，多半是位兢兢业业、优秀且精干的工程师！通常直到被升任为负责某项目的项目经理之前，都未受过完整的‘项目管理’培训。

照理说，不胜任的项目经理对项目的‘危害’，远远超过一位工程师的影响，而企业花了这么多的时间与金钱，将工程师培养为技术专家之后，居然是为了指派他去做不熟悉的‘管理’工作？这真的是十分奇怪的现象！正因为大部分的项目经理都是这么产生的，因此，管理不善往往成为大部分项目延误、超支，甚至失败的主要原因。

领导与管理是截然不同的概念，领导或许需要天份，也需要实际的战绩来服人，但管理的技巧与思想绝对是可以学到的，而项目管理更是一门成熟且已有无数实践实例的知识体系，项目的思想也已广为电子产品开发行业所接受，无论你将来是要开发手机、网络设备、玩具、家电等，你都曾是某项嵌入式系统开发项目中的成员。

现在，你还会觉得项目管理的思想与工具对你来说不重要吗？

大部分的人身处于项目之中，却不见得真的了解‘项目’到底是什么。以下我们就来了解项目的定义。

Definition：项目、单项、Project

■ 项目是为完成某一独特的产品或服务所做的一次性努力

■ 目标导向，每个项目都是唯一的

■ 非批次或日常活动

■ 软件项目：

■ 软件开发规范严谨度远不及其他领域；“经验”在软件项目中仍起着极大的影响力

■ 软件仍未摆脱手工开发模式，而人的管理是最麻烦的

■ 软件开发仍是最复杂的领域之一

■ 软件是纯知识产品，其开发进度和质量很难估计和度量

项目的特性如同幻灯片所述，有明确的开始与结束时间，且每个项目都是独一无二的。例如：工厂生产线上完成每项成品组装的过程并不算是项目，但设计组装流程，并尽可能精简步骤的过程，则可以使用项目的思想来执行。

在嵌入式系统项目中，软件开发所占比重很大，相对于其他种类的项目，软件项目有其特殊性。举例来说，建筑业有很明确的工序与材料选择规范，只要按照规范来做，项目质量就得以确保，则失败的可能较小。但软件开发只有软件工程这套开发‘原则’，且软件质量水平很难加以量化，使得软件项目管理难度大增。

嵌入式系统开发还牵涉到硬件及诸多限制，就算同所有软件项目的种类相比，说嵌入式系统是最复杂的都不为过。稍后我们会提到软件估价与复杂度评估，在所有评估方法中，都把嵌入式系统的复杂度设为最高。简而言之，嵌入式系统里一行程序的‘价格’，往往高于其他种类的软件项目。

一般项目vs.嵌入式项目系统

■ 嵌入式项目包括所有软件项目的特性，除了：

■ 对硬件性能的依赖

■ 对硬件成本相当敏感

■ 设计生产相关事宜

■ 产品质量要求视最终市场定位而定

在座各位都是从事嵌入式系统开发的，对嵌入式系统项目的特性，以及开发时可能会遇到的限制等，应该都有深刻的体会，在此我就不再赘述了[1]。

项目管理+软件工程

■ 项目管理与软件工程都关注达成目标前的过程，意图归纳出最佳实践

项目管理试图规范‘任何符合项目定义工作，的执行流程，以增加项目成功的机率，而软件开发也是一种项目，自然可以运用项目管理的知识加以管理，项目管理与软件工程是两套独立的知识体系，我们在开发软件时也必须按照软件工程的精神来执行，但实际上，两者的规范是否有所冲突呢？

答案是否定的。两者的基本思想其实是完全相同，都是将项目的执行流程定义为'规划→执行→控制’的循环，只是软件工程会针对软件开发的特性，而发展出特有的思想与工具[2]。

项目管理的困惑

■ PMI：PM最重要的工作是沟通，PM应该要有90％的时间花在沟通

■ 事实上，台湾软件开发团队PM的工作性质却是？

■ PMI：项目的质量是规划出来的

■ 实际上几乎所有项目都是用“检査”来保证质量

美国PMI（Project Management Institute，项目管理协会）收集来自世界许多项目运行的实例，并分析与归纳出一套目前广为接受的项目管理知识体系。我们现在的课程内容基本上都是以这套知识体系为出发点。但刚刚说过，每件项目都有其独特性，不可能有完美的流程可完全套用在所有的项目上，所以在讲解项目管理的知识体系时，首重思想与应用，而非其中的条文。

一般人对嵌入式系统项目有两个最大的误解：

■　项目经理必须是位软硬件全才，只会出张嘴的项目经理无法带领项目成功。嵌入式系统项目牵涉的领域非常广泛，项目经理居中协调、review都已经没时间了，怎么还会有空去写code？如果你项目中的PM每天埋头写code，他其实是想要掩饰自己在管理与沟通上的无力感。项目中的每位成员都有其重要使命，有的人是写程序、有的人是硬件设计，而项目经理的任务之一就是按照计划进行沟通协调，每个人都应该先把自己分内的工作做好，才能去帮助其他人，这个道理我想大家应该都知道吧！话说回来，身为嵌入式系统的项目经理，想要做好沟通协调，自然不能什么技术都不懂，否则，很容易就会被项目成员所蒙蔽。

■　只要经过不断的测试，就能够保证产品的质量。这已经是上世纪中叶的旧观念了，认为只要先把东西做出来，然后不断测试与debug，只要测试人员够精干，迟早可以把所有问题都找出来并解决。但现代产品的复杂度远比当时高出不知多少，且软件复杂度更是指数级的倍增，只靠事后测试是无法保证产品质量的。

现代的思想是：质量是规划出来的。特别是软件系统，若项目前期的设计工作做得扎实，执行时期不断监控，自然测试时期的bug就会较少，且修改bug的投入也较小！

2-2-2　何谓项目经理

项目经理

■ PM最主要的一个特点是：责任大于权力

■ PM责任：

■ 开发项目计划

■ 组织项目执行

■ 项目控制

■ PM应具备的能力：

■ 对项目目标有透彻地了解

■ 了解项目需要什么专长的成员完成项目、注重实际、对项目有足够强烈的责任心

■ 具备谈判技巧

■ 具有成本意识

■ 能应付挫折与失望、能容忍模糊不清的煎熬

在了解项目的定义之后，接下来，我将和各位介绍项目经理的职责。

目前的企业普遍采用矩阵型组织，在座的工程师们，你们在组织中会有一位直属老板（Functional Manager）。例如：软件部门课长、硬件部门经理等，当工程师被指派参与某个项目时，在项目运行期间，项目经理则是你另一位老板。在组织中，项目经理的角色其实有点尴尬，他并非项目成员的直属上司，但却要带领所有项目成员完成项目，而这正是幻灯片中‘责任大于权力’的意思。

因此，项目经理必须具备极佳的沟通技巧，让工程师的直属上司愿意支持此项目，使得项目成员可以更专注在此项目上。

项目和一般研发团队的运行有着本质上的差异，前者是要在限期内完成指定目标，而后者则是根据既定的长期发展计划前进。因此，主事者需要的管理风格与人格特质也不尽相同。一般而言，项目运行的节奏较快，项目经理必须强迫自己，从老板到客户的诸多信息中去挖掘并定义出项目的目标，在项目执行阶段，必须能心平气和应付所有突发事件的袭击[3]。

项目经理的角色变换

项目在不同阶段会有不同的特性，因此，项目经理在项目的不同阶段，必须适时做角色变换。

■　项目初期：目标尚未确定，项目团队也刚组成，成员彼此间默契不足。此时，项目经理必须主动明快的制订决策，并积极地指派工作。越是混沌不明的状况，领导者越是需要花更多的精力带领所有人找出正确的方向。

■　项目目标已确认：紧接着就是拟定计划。项目计划为项目的蓝图，对项目成败影响甚大，由于项目经理是最了解项目目标的人，因此，项目经理一定要全程参与计划拟定，必要时指引方向并协调纷争。

■　项目计划已定案：项目经理的主要工作转变为监督项目执行，随时检验查看实际的项目进度是否与计划有落差，执行风险管控与变更管理，并主动提供工程人员需要的协助与资源。

■　项目后期：项目团队的技术能力已成长到能够完成计划，且团队成员间的默契也已形成，此阶段项目经理只需管控进度，放手让项目成员发挥。

2-2-3　项目管理的基本概念

项目管理基本概念（1）

■ 项目一开始面对的挑战是—项目目标含糊，充满冲突

■ 第二个挑战是—项目关系人缺乏沟通技巧与工具

■ 执行过程面临最大的挑战是—计划的准确性低

某项目会被启动的背景并不一定，有的是经过长期的评估，有的只是两家公司老板在酒酣耳热下随口敲定。经常项目经理在被指派新项目时，连具体目标是什么都不清楚。例如，客户可能说：‘开发一台像iPod Nano的MP3随身听，最好可以支持所有的音乐格式，希望圣诞节或农历春节前可以上市’。此时项目经理的老板会说：‘去告诉每个研发团队的经理，全力支持这个项目，请他们提供所有你需要的资源——好好干！这个项目别再delay了。’

客户的规格说得不清不楚，老板的指令下得模模糊糊，项目经理在设法理清项目规格的同时，还要协调各单位，以取得项目运行所需的资源（人与钱）。除此之外，项目经理还必须将所有与此项目有关的人连接起来，否则，这些关系人是不会主动投注心力在这个新项目上的。

在执行项目时，最常听到的一句话是：‘计划赶不上变化，变化赶不上老板的一句话’。所谓计划是对未来事物的规划，确实没有人能够完美地预知未来，因此，执行计划本来就是一个不断对计划进行微调，使之与实际状况吻合的过程，而且在拟定计划的过程中，能让项目成员对项目的目标与可能发生的问题有更深入的思考与分析，也就是说，即使无法制定出完美的计划，我们仍必须认真地将项目计划做出来。

在项目管理知识体系中，有许多工具与方法可以在执行阶段缩小计划与实际状况误差，稍后我们会陆续谈到。

项目管理基本概念（2）

■ PDCA

相信大家对PDCA（Plan→Do→Check→Act）应该都不陌生吧！老板们总是将PDCA挂在嘴边，但请大家不要因此而认为PDCA只是个打官腔、不实用的论调。相反地，我希望各位在执行项目、开发软件，甚至进行任何工作时，都要切记这4个英文字母。

即便是天纵英明的神人，面对没做过或不熟练的工作时，也很难一次就把事情做对，都是一边做、一边检查、一边改善，直到做对为止。项目管理与软件工程的基本思想都是PDCA，整个项目要PDCA，某个模块的开发要PDCA，系统的测试工作也要PDCA。放大来看，项目从大到小的所有事情，都需要PDCA！

PDCA是一种做事的方法与态度，我希望在座各位都能掌握其精髓，这对各位在职场与私人生活中都会有很大的帮助。

@@@@

项目管理基本概念（3）

■ 项目管理的铁三角—质能守恒定律

幻灯片里的图称为项目管理的铁三角，但我比较喜欢‘质能守恒’这个说法——意即人力、时间及其他资源不可能被无限制地压榨出来。

举例来说，某件项目计划表明需要5个人、花费10万元、限期两个月、通过日系大厂的质量检验，当项目运行到一半时，在其他条件都不变更的前提下，客户希望项目能够提前一个月完成。

根据项目铁三角的原理，当你要缩短三角形的某一边，又要保持三角形的面积不变，则势必有另一个边或另两个边的边长必须加长。也就是说，根据拟定好的计划，如果必须缩短执行时间，要不增加成本或人力，要不减少或简化产品功能，不然，就只能降低质量的要求。否则只有一种可能一之前所拟定的计划肯定大有问题，要不是灌水灌得太严重，就是太过低估工程人员的能力。

在项目管理理论中有谈到为项目计划‘灌水’的原则[4]，以降低项目失败的风险，但等到执行时期才来大幅修改某些项目属性，却要不更动其他属性是完全不可能的。此时必须启动变更管制系统，否则计划表只会变成废纸一张，之后该项目的执行将完全失去准则、进度完全无法追踪。

因此，做好变更管理将是PM非常重要的工作，变更是一定会发生的，如何有效地管理变更是很重要的一门学问。

根据统计，85%的项目是在超过时间、未达质量标准、耗用过多资源的状况下完成，这也说明要让一个项目顺利结项并非易事，而要成功完成一个项目更显困难。根据我个人的经验，若三角形的三边（质量/规格、成本、进度）在一开始时没有构想清楚，过程中也没有做好变更管理的话，通常最后都会让项目成员之间弄得很不愉快，为了赶付交期，而牺牲质量，可能要求员工加班，进而垫高成本。

你的项目有这些“东西”吗？（1）

■ SOWT分析

■ 项目授权书(Project Charter)

■ 规模管理(Scope Management)

■ 变更控制系统(Change Control system)

■ WBS (Work Breakdown Structure)

■ 作业排序(Activity Sequencing)

■ 关键路径(Critical Path)

在进入项目管理知识的细节之前，我必须先强调一项重点——项目管理知识体系并未要求所有项目必须全盘导入它所提供的工具，重点还是放在思想的传达上。

但以我管理项目多年的经验，仍然认为有许多工具应该是必备的，例如：

■　一件项目是否能为公司赚到钱？是否会让团队陷入无法结项的泥沼？该不该启动一个项目必须经过严谨的评估，有个方法叫做SWOT[5]。

■　要解决项目经理‘责多于权’所导致的问题，老板在项目启动时千万别忘了颁发‘项目的圣旨’——项目授权书。

■　弄清楚项目的规格后，接着要细分工作，并为细部工作进行排序与设定进度……这一系列工作是拟定项目计划时的重要流程。项目管理知识体系提供了许多实用的工具与方法，其中的WBS可说是项目计划的基石，关键路径则是控制项目进度时最重要的信息。

■　我们刚刚在‘无意间’已经讲了不少关于变更管理的重要性，但它偏偏是常被项目经理忽略的一项工具。

在座的各位应该都曾经参加过一项或一项以上的项目，请问你们的项目里有这些东西吗？如果没有，我可以直接地说，这些项目并非用最佳的方式在执行，一定还有改善的空间！接下来的课程内容都会提到这些重要的工具及思想，麻烦你们带回各自的项目中。

你的项目有这些“东西”吗？（2）

■ 挣值管理(Earned Value Management; EVM)

■ QA&QC

■ 激励理论

■ 冲突管理

■ 风险管理-定性分析、定量分析、风险控制

■ 结项管理

项目管理知识体系统合了不少在管理学里已证明有效的工具，例如：幻灯片中提到的‘挣值管理’或激励理论，并非所有项目都必须导入这些工具才能成功，但他山之石，可以攻玉，既然已经有一套完整的项目管理知识体系摆在那里，我们这些靠执行电子产品开发项目过日子的人，就算无法应用它所有的工具，至少也要学得正确的思想。

项目管理不像软件工程有那么多不同的学派，不同体系间的差距并不大。我们接下来讲的内容是基于美国项目管理协会（PMI）制定的知识体系，是目前全世界最多人学习的项目管理知识，权威性与可执行性毋庸置疑。

02-03　项目生命周期五大阶段

项目生命周期介绍项目管理知识体系

我们前面讲过项目的定义，特别谈到每件项目都是独一无二的，都有各自的目标、可应用的资源、必须面对的限制与风险等。但所谓的知识体系就是要设法异中求同，通过分析与比较足够数量且不同种类的案例，试图归纳出适用于所有项目的思想与方法。

这么做并不牵强，因为不同项目间确实具有共同的特性，可以使用相同的思想与方法论来执行，就如同我们的本行——嵌入式系统与电子产品开发，如果不能在不同的项目间秉持共通的概念，工程人员免不了要多走很多冤枉路，其职业生涯将会非常辛苦！

2-3-1　项目管理知识体系

PMP简介

■ PMP-Project Management professional

■ PMI-Project Management Institute

■ PMBOK-Project Management Body Of Knowledge

■ PMP知识体系并非闭门造车，而是世界成功项目经理的经验积累。

PMI制定的项目管理体系称为 PMBOK（Project Management Body of Knowledge），其举办的全球性项目管理知识认证考试则称为PMP。市面上有许多相关参考书籍，建议各位可以花时间找一本来研读。在此，我并非建议各位一定要取得PMP认证，但PMP考试以实际题居多，在做模拟试题的过程中，可以让你进入一项又一项不同项目的实境里。如果你是一位项目成员，如何在各个阶段协助项目成功？如果你是一位项目经理，碰到各式各样的状况时该如何处理？

建议只要买本PMP模拟试题与讲解的书籍即可，我个人认为这真的是很难得的管理培训，自我研读也未尝不可，因为管理的事本来就没有绝对的对错，只要能引发思考都是有益的。在座的各位总不会希望当自己到四五十岁时还在做工程师吧？就算你们想当一辈子的工程师，也要考虑现实环境，毕竟在中国，很少有企业愿意花高薪聘请一个资深的工程师。

所以，我亲爱的同事们，务必要抽空研读一些管理书籍，相信对各位的职业生涯会有很大的帮助！

项目管理的五大步骤

任何一件项目都必须经过以下5个阶段。

■　启动阶段

■　规划阶段

■　执行阶段

■　监控阶段

■　结项阶段

一般人都会直觉地将项目切分为这5项步骤，各位会发现，其实软件开发也是按照这样的流程，重点在于每一个阶段要做好哪些工作，才能保证下一个阶段可顺利的运行？幻灯片中的这张流程图乃归纳自 PMBOK 的各个章节，各位可以从这张图看出项目生命周期间。除了Implementation 外，到底还有哪些必要的管理工作？而这些工作存在目的只有一个，即为了增加项目成功的几率。

请各位先对幻灯片中的每项Item稍具概念即可，我们稍后会陆续提到。

2-3-2　项目生命周期

项目生命周期

■ 项目启动阶段

■ 项目规划阶段

■ Scope/Time/Cost/Quality Plan

■ Resource/Communication Plan

■ Risk Plan

■ Configuration Plan

■ 项目执行/控制阶段

■ 项目结项阶段

同样重要的，还有项目生命周期的5个步骤。

■　启动阶段必须保证执行这个项目确实是有意义的，并赋予项目经理足够的资源与权力。

■　规划阶段的成果是‘项目执行计划’，过程中必须对项目的诸多特性（包含规格、进度、成本、质量需求、资源调配、沟通管理、风险管控及配置配置）做深入的分析。

■　执行阶段必须保持严谨，工程人员根据项目执行计划按章行事。当计划与实际状况出现落差，或因不可抗拒的因素必须修改项目规格时，则必须启动变更管控流程，更新项目执行计划——使得工程人员始终按照最新的计划来执行即可。

■　监控阶段则是在执行阶段，随时检查项目执行状况与既定计划是否即将或已出现落差，最好能够提前发出警示，以实时启动变更或风险管控流程。

■　结项的目的是为了累积企业资产，以供以后项目参考，照理来说，这是相当有意义的工作，但实际上却往往最常被忽略（到项目后期，许多项目成员已功成身退，剩下的势必也正在另觅新项目。因此，必须在项目执行阶段即通过许多自动化工具，不断地收集与整理项目资料，使结项时期的投入越小越好）。

若要我说项目生命周期中5个步骤最重要的是哪一个？工程师可能就想着执行阶段，但我会认为是规划阶段。整个 PMP 知识体系里，大部分的篇幅都在说明如何造出合理的计划，而我今天也会花最多时间在规划阶段上。这个道理非常简单，事前想得越清楚，执行的变量也越少，成功的机会自然而然便可提高。

等哪一天我有机会当上企业的老板，也许我会转而认为结项才是最重要的阶段。

项目生命周期的特性

■ 项目初期与末期成本与人力需要较低，中段最高

■ 项目初期不确定大（即风险大）

■ 项目初期，项目关系人较能影响项目列出与成本

■ 项目初期改变规格或修正错误的成本较低

■ 项目各阶段结束前为重要核查点

项目在不同阶段会有不同的特性，因此，必须视项目所属阶段，采取不同的态度，并在不同之处投入不同的人力及关注，这点相当重要！虽然项目的5个阶段并没有明显的界线，但项目经理有责任让所有项目成员随时都知道目前项目正处于什么阶段。千万不要在规划阶段忙着写code，在执行阶段又不按照计划来执行；又或者监督阶段太晚启动，review活动不够积极，等到问题爆发时已无可挽回！

2-3-3　项目启动阶段

接下来我将遵循着项目生命周期的5个阶段，逐一说明各阶段的思想以及可使用的工具。一天的课程当然无法将细节全都说清楚，希望各位在今天的课程能够尽量将PMP的思想纳为己用，知道有工具可实际应用，等日后有需要时再去查询即可。

项目启动（1）

■ 项目可行性分析

■ 项目授权书

■ 明确说明项目目标与管理方向

■ 明确对授权PM

■ 任何与项目有关的信息

企业以营利为目的，除非有其他 Business 上的考虑（例如，为了与重要伙伴建立关系，即使赔钱都要做），否则，不赚钱的项目不应该接。此外，小虾米团队也不该妄想吃下大鲸鱼的项目。如此看来，某个项目到底该不该做确实是跟企业的利益息息相关，所以项目不应该说启动就可以启动的，必须经过严密的可行性分析。

可惜的是，大部分项目都过于忽视这个步骤，通常会直接跳过量化的分析，由高层或资深工程人员使用直觉与经验来决定是否启动。但我们说过，每件项目的特性都不同，即便是规格完全相同的产品，但面对不同的客户，可能会有截然不同的执行结果。

就算是老板和客户已决定要做的项目，当我被 assign 来执行这个项目，还是会找必要的工程人员执行可行性分析。分析工作的深入程度，根据项目复杂度与规格是否明确而定，规格越不明确的项目，越是需要做更深入的分析，在分析过程中顺势尽可能逼客户提供明确的规格。一旦分析结果风险极大，必须及早警示老板，即便老板还是执意要做，也能尽可能在项目启动前取得更多的资源，或设法让客户在规格上让步。

在项目启动阶段还有两件重要的事：

■　理清项目关系人。要分清楚只是纯粹感兴趣的人，以及实际具有影响力的人。

■　老板颁布项目授权书，并正式 kick-off。

通常项目经理在没执行项目的空档时，麾下是没有团队可供调度的，所以当公司决定启动某项目时，老板必须正式授权给项目经理，让他可以申请经费与调动人力，最佳的方式就是老板‘颁布圣旨’——即项目授权书，白纸黑字说明项目目标，以及项目经理可以调动哪些公司资源。

通常在公司的组织章程中，并不会根据不同项目特性，明确各单位对项目的支持程度。因此，项目授权书就如同公司内部的行政命令，职位与法律相同，使得项目经理能够在授权书的范围内，‘合法’地取得资源。

项目启动（2）

■ 嵌入式系统项目启动

■ 确认产品规格（成本7性能/质量…需求）

■ 确认产品限制

■ 初步确认将参与项目的公司与单位

■ 确认开发模式（S/W Development Life Cycle）

■ Waterfall Model

■ Prototype Model（初期需求不明确）

■ Spiral Model（Waterfall+ Prototype的多次迭代）

■ …

我们公司就是一直碍于没有正式的项目授权书，搞得所有项目经理常常为了抢资源而搞得不愉快，部门主管也很难判断各个项目的优先级，使得本来可以‘依法行事’的，却因为缺少了老板这个举手之劳，项目经理最终还是得拉下自己的老脸到处要资源。

项目启动前后必然是混沌不清，充满了各种不确定。不确定的事情拖得越久，对项目影响越大，所以项目经理必须尽可能在项目前期就设法理清大部分的事情。以嵌入式系统项目来说，产品规格与成本限制是最重要的。同样是手机，智能型手机和一般手机所需的研发资源就大相迳庭！如果只知道‘大概’要做什么，前面谈到可行性分析的可信度，自然就会大打折扣。

再强调一次，越前期的决策错误，对项目成败的影响越大！项目启动阶段参与人员不需太多，最好是较有经验的人员，此时，项目经理要带头用谨慎的态度尽可能挖掘出该项目可能的变量与风险。

一旦确定要执行这个项目之后，接下来就是规划、执行及监控阶段，稍后我们会详细解说这3个阶段所需要的流程与思想。事实上，PMP 大部分的重心都着重在这3个阶段。即便如此，我在此仍然要再次强调项目的启动与结项两者缺一不可，前者是确认此项目是否有执行的价值，并正式赋予项目经理应有的权力去执行此项目，而结项则是对此项目进行检讨，项目执行中的所有成果都应该转换成有用的组织资产。

在此我们先跳到项目结项阶段。

2-3-4　项目结项阶段

项目的结项阶段

■ 合约结项

■ 项目结项：

■ 项目资料归档

■ 技术数据归档

■ 记录经验，累计企业的项目资产

■ 人员解散

结项有两种，一种是对外部的合约结项，另一种是内部的项目结项。

一般项目都只知道要做合约结项，不是公司急着跟客户收尾款，就是我们的厂商要跟公司讨钱，因为这会牵涉到营利或是否违约的法律问题，所以到项目后期，无论团队再怎么疲惫，都一定会设法尽快将相关合约 close掉。

我们前面已经谈过项目结项的重要性，但大部分的项目通常都没有正式的项目结项过程。状况好的，可能所有项目成员一起吃个饭就算结项；状况不好的，项目执行时团队间弄得不愉快，项目后期，大部分人（或明或暗）早就投入另一个新项目的怀抱了，更不可能有余力进行项目结项。

项目后期的普遍状况是：人员就算尚未作鸟兽散，但对此即将结束的项目也渐无心力，项目经理的影响力也急速缩减。这是业界的常事，甚至老板可能就是将人力提早抽到新项目的‘凶手’。在这个状况下，项目经理只能从两方面完成项目结项的工作：

■　在项目执行期间，制定流程，并使用自动化工具，将项目开发的轨迹（包含程序、文件、bug 管理、issue 管理、变更管理等）记录下来，并定期备份。

■　明确规定执行项目结项流程的起讫时间，最好不超过一周，并与项目成员的部门主管以及现任的项目经理沟通与协调，请这些同事们在某段时间内帮这个项目最后的忙，只要项目结项流程的时间明确，大部分主管与员工应该都会乐意帮忙。

项目结项除了整理项目执行期间的所有资料之外，最好还能聚集重要的项目相关人员，在轻松的气氛下，对此项目从头至尾进行检查，并留下宝贵记录。

2-3-5　项目管理九大知识体系

项目管理九大知识体系

■ 整合(Integration)管理

■ 范围(Scope)管理

■ 时间/进度(Time)管理

■ 成本(Cost)管理

■ 质量(Quality)管理

■ 人力资源(Human Resource)管理

■ 沟通(Communication)管理

■ 风险(Risk)管理

■ 采购(Procurement)管理

我们已经谈了项目生命周期的启动与结项阶段，接下来，将正式进入与工程人员有直接关系的规划、执行及监督阶段。这些阶段的工作可以画分为9大类，称为 PMP 的9大知识体系，其中与工程人员较有关系的则有7项：范围（Scope）、时间/进度（Time/Schedule）、成本（Cost）、质量（Quality）、人力资源（Human Resource）、沟通（Communication）和风险（Risk），这也是以下课程的重点。

02-04　项目范围（Scope）管理

项目范围管理（1）

■ 产品完成后发现一个需求上的缺陷，修改这个缺陷要比在项目初期发现这个缺陷，要多付出68倍的成本（另有研究结果为200倍）

刚刚我们已经强调了，必须尽量在项目初期排除不确定性，否则，越在项目前期犯的错误，对项目成败的影响就越大。在此我们看到了一个惊人的数据——68倍！前期的分析与规划工作做得越仔细，后期出现‘惊喜’的机会就越低。虽然所有人都了解这个道理，但往往都过份低估了项目独特性可能带来的影响。

有相关经验的工程人员，常会认为同质性高的项目没什么了不起，没必要做太多规划工作，做就对了！等到项目后期才发现，新项目和以前做过的项目还是有些差异，此时已完成的系统架构已经无法容纳这个差异，最后只能投入更多的时间或人力，进行架构性的改变。

这就是68倍的由来。过度轻视项目的困难度、走一步算一步的工作方式，最严重的往往是：根本不知道项目的真正目标（通常是自以为知道，但其实对细节完全不清楚），就直接往错误方向硬干，做越久，偏差越大，最后想要拉回来更是难上加难！

所以PMP知识体系告诉我们，当项目启动后，首先就是要花时间做好项目的范围管理，唯有定义出正确的范围（Scope），之后做的进度、成本和人力计划才是有意义的。

所谓项目范围除了包含该做的项目，也要弄清楚不该做的工作，两者同样重要。项目团队的人力、时间与预算都是有限的，没有理由对项目范围不加限制，任由工程师天马行空的按照自己的想法去做。在项目管理的思想中，这绝对是缺乏纪律的行为，称为镀金（Gold Plating）或范围蔓延（Scope Creep），这都是应该要尽力防止的事情。

从幻灯片的流程图中可以看出，需求分析也是个循序渐进、不断修正的工作，甚至经常发生已经到了项目执行阶段，客户才要求更动规格的事。此时，项目计划既已完成，就应该执行变更管理流程，在新计划中适时反应项目范围变化所带来的影响。

范围管理有两项重要的工具，一项是我们现在要谈的工作分解结构（Work Breakdown Structure, WBS），另一项则是变更管理。

项目范围管理（2）

■ WBS (Work Breakdown Structure)

■ 工作分解，其他项目计划（成本、时间、人力…）最重要的依据

■ 分解的标准：

■ 按“功能组成”分解

■ 按“项目生命周期”（如“规划”→“设计”→“coding”→“测试”）

■ WBS最底层的工作（Work Package）要非常具体，建议至少要拆分到约1周或40HRs的工作量

在评估工作量时，有个人人皆知的简单概念：事情越大越复杂，越不容易估计准确。因为这样很容易就会忽略一些重要的细节，所以人们早就知道由繁化简的道理，并将其应用到项目范围管理中。简单来说，就是将一件复杂的工作，切割成许多较容易执行的小工作，假使这些小工作还是太复杂，就继续切割，并反复递归地执行这项分割工作，直到我们有把握评估每件小工作的特性为止。

举例来说，最初的项目范围可能只是一句话：开发一台多媒体播放器。谁也没办法由此精确估计出Schedule、Cost等项目特性，所以必须继续将开发工作切分为软件、硬件、结构、生产，然后再为每个项目继续分割，可能会得到：选择MP3译码IC、系统架构设计、实现电源管理模块、准备备料计划等较可掌握的小型工作。

工作分割最终会长成一个树状结构，根（Root）为项目目标，树叶（Leaf）则为许多小工作，这个树状结构即称之为WBS。至于工作要切割到多细才合理呢？如果切割太细，则项目范围分析时难免会触碰到太多技术细节，一旦工作项目太多，会使得项目计划过于复杂，而且容易扼杀工程人员的创意空间；如果切割太粗，则项目计划就会有评估不准的风险。

对此PMP给出了建议：WBS最底层的工作（Leaf）要非常具体，不容模棱两可，而且至少必须切割到约一周或40个工作小时的工作量——这是一项经验值，这种工作量的工作，用于评估不至于产生太大的误差，且工程人员仍保有足够的发挥空间。

制定WBS是项目规划阶段最重要的工作，除了将项目目标切割为项目计划中的基本单位外，更重要的是，在制定 WBS 的过程中，我们可以过滤出所有规格、管理与技术上的盲点，并在项目初期尽快理清所有不明确之处。

就算从没听过PMP或WBS，一般人在解决问题时也都会自然地将其切割为许多小问题，然后逐一寻求解决方案。WBS的思想很靠直觉，大部分项目都知道要按此思想做事，但并未真正把WBS画出来。所谓没图没真相，我们就无法对WBS的合理性做严谨的review，而且在拟定计划时，很容易就会忽略某些工作项目。

记录WBS的方法有很多种，我会建议使用Microsoft Project来做（要用Excel来记录也无妨，但在制定项目计划时，还是得复制到项目管理工具上）。

WBS有两种描述方式，一个是图表法（如幻灯片的左半部），优点是一目了然，但当项目工作较为复杂时，这样的图难免显得杂乱，且较难以工具进行处理。所以通常我们会用另外一描述方法——在纸上画出树状结构草稿，然后使用如幻灯片右半部的列表来做管理。

WBS实务

在进行任务分解时，由上至下的标准必须一致，否则，当项目较复杂时，很容易因为任务分割标准不一，导致任务重复。一般使用的任务分解方法如下。

■　按照子功能分割

■　按照系统架构

■　按照项目生命周期

■　参考以往类似性质的项目

初版 WBS 完成后，一定要经过相当严谨的检查与公开 review。再强调一次，如果 WBS有问题，之后进行的 Schedule、成本（Cost）、人力资源（Human Resource）等评估都会跟着出问题。

WBS注意事项

■ 每项工作可再细分为许多子工作，直到可明确分配给某人或某小组

■ 是WBS里的每项工作进行Job Description

■ WBS最底层的每项工作都必须要有明确的Schedule

■ 根据WBS，找出工作间的相依性，即找出可能的瓶颈所在

WBS其实就是项目的 Scope Baseline（基线或基准），它除了描述本项目中所有必须执行的工作之外，同时也说明不在WBS中的工作，都是没必要执行的！有时候，后者反而是容易被忽略的思想，项目主管放任工程人员执行了半天似乎与本项目有关却不属于项目目标范围内的工作，这样的行为对项目来说都是非常不利的！

项目有一个重要的特性——必须先做‘对’，行有余力才做‘好’。嵌入式系统产品开发项目更是如此，客户要你做低价位的 MP3 播放器，你却做了一个可以播MP3的智能型手机给它，你说客户该哭还是该笑？

总之，WBS绝对是项目计划的基础，如果连要做什么都不知道，项目运行宛如瞎子摸象，要顺利结项只能靠老天保佑了。

项目范围管理（3）

■ 项目执行控制过程

接下来，我们将谈谈项目范围管理的第二项重点——变更管理[6]。

变更管理是在项目谈行阶段用来处理项目计划与实际状况有落差的情况。如幻灯片中的流程图所示，项目在执行时，追踪与监控工作必须同步运行，一旦发现计划与实际状况不符，或客户提出规格更改要求时，就必须提出变更需求。

在变更处理流程中，相关人会评估接受此变更的影响。还记得我们前面说的项目铁三角吗？某一边的更动，势必要影响其他的两个边。如果这个变更真的轻微到不会对项目其他的面造成影响，那我们也没必要为此担心。一旦某项变更会严重影响进度或需要增加成本，则必须所有关系人（例如：PM、公司高层、客户代表等）讨论同意后才能接受此变更，并将所造成的影响全部照实 Update 到计划书中。假使项目关系人无法接受变更带来的负面效果，就只能放弃此变更，或请工程人员另觅他法。

无论如何，我们不能放任一个已知的问题在项目中，而不去处理！

对项目来说，变更宛如万恶根源，是很负面的字眼，也是项目经理的梦魇。变更可能会对项目的正常进展带来无尽的麻烦，但奇怪的是，几乎没有项目不会碰到变更。特别是电子产品开发项目，电子产品的生命周期短、开发复杂度高，有时真的就是计划赶不上变化，当客户说出‘若不改规格就不用卖了’时，身为伙伴的我们不配合也不行。

面对这种状况，流着眼泪、带着微笑地接受绝对不是最好的处理之道，这只是让冲突点延迟爆发而已。最好的方法应该是如上所述，召集所有关系人，大家一起审视原先拟妥的计划书，共同面对问题，以寻求解决的方法，让 RD 加班只是方法之一，缩减规格、增加预算或时间，甚至放弃变更都可以拿出来讨论。

项目范围管理（4）

■ 需求变更管理

■ 需求变更并不可怕，可怕的是对变更束手无策

■ PM必须坚持“绝不轻易答应需求的变更”，应该要循变更管理控制流程决定是否接受变更

面对变更需求绝对不能慌乱，务必使变更在受控制的前提下对项目产生影响，千万不可任其随意变化。项目经理一定要把持一个铁的原则——因为变更会影响项目进行，因此，所有的变更一定要经过CCB（Change Control Board，变更管理委员会，即与此变更有关的关系人参与决策的会谈）的同意，并造出新的计划书，而工程人员只需随时按照最新版本的计划执行即可，严格禁止接受客户私下变更规格的请托。

实际上，我们会把变更处理流程制定的越简单越好，并鼓励员工一旦发现任何计划与实际状况有落差时，马上向主管报告，若连主管也无法处理时，则提出变更需求。项目经理会视状况召集需要参加CCB的项目关系人，变更审查时必须谨慎，必要时，一定要深入进行技术的review。

项目团队存在的目的只有一个，就是让该项目顺利结项，假使发现某项变更请求可能会使项目出现大问题，也应将此突发事件视为当前最高优先级的工作，项目经理必须尽力协调，务必要将其处理妥当，直到造出新计划为止。否则，放任项目继续执行下去并无任何意义！

02-05　项目进度（Time/Schedule）管理

项目进度管理（1）

■ 时间为单向性、不可重复性、不可替代性、与其他资源特性不同

■ 规划“进度计划”

■ 由WBS取得项目中所有的任务（或活动Activity）

■ 确认各任务之间的关系（需时长短，先后顺序…）

■ 进度管理图表（Gantt/甘特图+网络图）

接下来我们要讲项目管理9大知识体系中的时间管理。衡量一个项目是否成功的最简单方法就是是否在约定时间内，按照规格完成所有工作。而工程人员也常被问到：‘某某模块能不能on time完成？’、‘你预估要 delay 多久？’等与时间有关的问题。

时间是一种特殊的资源，也是项目计划中灵活度最小的因子。当项目缺人、缺钱、缺器材、缺技术时，都还有办法可想，但时间一旦过了就要不回来了！因此，实际上都是采用‘用金钱换取时间’、‘用 RD 肝功能换取时间’等策略，但若为企业长远发展着想，这都不该视为常事，不如在计划阶段就把时间规划好。

PMI 强调项目成功的三要素为：计划、计划、计划。项目计划是通往项目成功的路线图，而进度计划则是项目计划中最重要的部分，是项目计划的核心。进度计划是基于WBS分解出来的所有Task（任务，即WBS树状结构的最末端——Leaf），确认Task之间的关系，然后估算出每项Task需要的资源与时间，进而编制出项目的进度计划。

同一件事由不同人执行，可能需要不同的时间，所以最好先确定资源，再估算时间，但实际上常有资源不足的状况，有时顺序必须颠倒过来——先预估每件工作的时间，等进度计划完成后再来分配资源。

拟定进度计划之前，必须先理清工作之间的关系。一般来说，工作间的关系有4种（请参考幻灯片中的图）。

■　FS（Finish to Start）：任务A结束后，才可启动任务B。

■　FF（Finish to Finish）：任务A结束后，任务B才可结束。

■　SF（Start to Finish）：任务A启动后，任务B才可结束。

■　SS（Start to Start）：任务A启动后，任务B才可以启动。

实际上用到最多的是 FS，例如：设计完才能开始Implement, Implement完才能开始测试等，这都是FS的概念。工作间的关系相当重要，会直接影响整个项目的执行时间与关键路径的计算。举例来说，完成任务A要5天，完成任务B要10天，若它们的关系是FS，则完成任务A、B最短也需要15天；若它们的关系是 SS,则可能需要10天就可以完成任务A与B（因为任务A、B可以同时启动）。

当WBS里任务较多时，人工排序并非是件容易的事，幸好有工具可以帮我们做这件事，我们只要输入每个任务所需的工时，以及任务间的关系即可。

项目进度管理（2）

■ 甘特图

■ 网络网

在此我们看到 Microsoft Project 这个专门用于项目管理的工具，只要输入任务信息，就会自动产生甘特图（Gantt）。在甘特图里，每项任务会有一根时间 bar,代表其起讫时间。除了时间外，甘特图也能包含里程碑（执行时间为0的任务）与人力资源的信息。更重要的，我们还可以拿甘特图来执行进度追踪。因为甘特图具有简单易用与容易理解的特性，所以被广泛地应用在项目管理中。

一般来说，所谓的项目计划至少要包含以下信息，使用 Microsoft Project 这种轻量级的项目管理软件已经绰绰有余。

■　工作内容描述

■　工作起讫时间

■　工作间的关系

■　工作所需的资源（或负责执行工作的员工）

■　工作所需的费用或人力成本

当进度计划制定出来后，我们必须找出关键路径（下一张投影片会讲到），此时仅使用甘特图是不够的，还必须导入‘网络图’的概念。网络图描述了项目中所有工作的特性，以及工作之间的关系。投影片中就是一个简单的网络图范例。

以嵌入式系统项目的复杂程度而言，用人工去画网络图是不切实际的，而项目管理工具可以协助我们画出网络图，并找出关键路径。

项目进度管理（3）

■ 关键路径（CPM）

■ 网络图中最长的路径

■ 唯有缩短关键路径上各个工作的时间，对整个项目的结束时间才会有影响

■ 项目管理软件会自动从甘特图中推出关键路径

网络图有好几种形式，幻灯片中的网络图是最简单普遍的一种，节点代表工作，连接节点间的箭头指示线则代表完成工作所需的时间。所谓的关键路径，就是网络图里从‘开始（S）’到‘结束（E）’之间最长的路径，表示想要完成此项目，最快也需要这些时间。

以投影片中的网络图为例，关键路径是：S→Task3→Task4→E，路径长度为10。从这个项目计划来看，就算缩短 Task1 的执行时间，甚至让Task2 不需执行，都无法缩短完成此项目的时间。

许多项目经理完全不知道项目的关键路径与关键工作是什么，导致项目团队可能花太多精力在整体 Schedule 缺乏帮助的工作上，却任由关键的工作延迟，导致整个项目一同延迟。若能从项目的进度计划中找出关键路径，可使项目主管更有效率地调配资源，让所有项目相关人员都能知道不同工作项目间的重要性等级。更重要的，所有人都会特别关注关键路径上的工作，因为不管任何关键工作 delay一天，项目的Schedule 就必须跟着延迟一天！

知道了关键路径，项目经理会倾向尽可能提早每项关键工作的启动时间，或设法缩短其执行时间。必须注意的是，当我们加派资源在关键路径上，可能会导致项目路径改变，此时，项目经理就必须检验查看并设法缩短新的关键路径。项目经理通过不断改善关键路径上工作的执行计划，从而完成项目计划的最优化。

项目进度管理（4）

■ Parkinson's Law-不论工作困难度与紧急程度，员工都倾向用完所有分配给他的时间。

■ 管理预留

■ 为整个项目预留时间，而不是为各个工作预留时间

■ 一般会预留Critical Path的10%~15%

■ 另有一种说法是预留到50%

我们说过，没有人能够一次就做出完美、零误差的计划，要估计准确每一项任务的工作时间绝非易事，且任务的执行时间显然与负责执行的工程人员有绝对的关系。当项目里的任务为数众多时，每项任务预估时间的累积误差将非常大。

PMI应该观察到一个普遍性的状况。无论何种领域的项目，估计工时的误差总是很难做到准确。所以PMI建议进度计划拟定好后，项目经理最好再多灌一点水，把整个项目的预定执行时间拉长一些。至于要拉长多久，应视项目计划中还有多少未知的事情，以及风险评估的结果。如果要拉长50%,则表示项目计划准确度相当低。个人认为，与其拉长项目执行时间，不如利用这个时间把规划与分析做得更扎实些。

要注意的是，我们是为整个项目预留时间，这个时间是放在项目经理的口袋里，非到必要时不会轻易拿出来用。这意味着项目经理仍必须强烈要求各项目成员把预计工作时间估计准确，如果员工反应有太多未知，实在不知如何估起，这通常表示 WBS 切割得还不够细，必须把规划工作退回项目范围分析，或者采用滚动式规划（Rolling Wave Planning）这项方法。

举例来说，面对一个团队完全没做过的项目，同事们普遍反应未知的东西太多，无法在初期就 identify 出明确的工作项目与时间，但我们至少知道项目的目的与预定的deadline。假设6个月内要完成这个项目，那么，我们至少可以精确做出前一、两个月的计划，其中的工作项目可能是评估、分析、研究、买入 solution 等，而后面几个月的计划则保持模糊。例如：工作项目：implementation；执行时间：3个月。待分析工作完成后，对后期的工作性质应该就比较有把握了，此时，再来更新并细化计划的后半部。

至于幻灯片中的 Parkinson's Law（帕金森定律）就不用提了，我本身也由工程师起家，深知大家是如何利用时间的。但我现在是项目经理，得把项目里时间的buffer 收回我的口袋。

项目进度管理（5）

■ 进度管理

■ Schedule vs. Cost-几乎呈现反比关系（加速Schedule 往往必须增加成本，反之亦然）

■ WBS分解成果的好坏，直接影响项目进度计划

■ 执行时期必须不断检査项目计划与实际进度，是否存在偏差

■ 当发生第一次偏差（甚至趋势发生反转），PM就应该开始追踪与处理

这张幻灯片将对项目进度管理做个总结。

虽然我们会为项目进度留些 buffer，但对于项目进度的安排应该要有适度的压力，让项目成员有适度的紧迫感。但这不表示项目主管必须不断地在进度上压迫工程人员，只会问‘还要多久可以搞定？’、‘会不会 delay?’这些话，严格来说，都不能算是合格的项目主管，过度强调进度，很容易会伤害项目人员的士气。

当进度计划已经公布，同事们也知道关键路径与关键工作是什么，工程人员只要按章行事即可。此时，项目主管应该专注在任务的完成质量上。也就是说，基于任务的 schedule，通过不定时对任务成果的 review，自然能够得知该任务的进度，也能实时挽救即将 delay的任务。

02-06　项目成本（Cost）管理

项目三本管理（1）

■ 软件系统的项目成本预估，永远不会是一门精确的科学；只能依靠以前的项目经验或业界验证过的共识来提高精度。

■ 基本嵌入式系统成本来源：

■ 管理成本

■ 硬件/结构设计成本

■ 生产&材料成本

■ 软件开发成本-Lines of Code, Function Point, 人月…

■ e.g. COCOMO模型的参数（评估软件开发项目的成本）

企业组成项目团队来执行项目绝对是为了公司利益，所以在规划阶段必须仔细估算项目成本，除非有更大的利益，否则，执行没利润的项目是毫无意义的。

嵌入式系统开发有很大部分是软件工作，而软件开发大部分是脑力工作，所以很难预估软件系统的开发成本及其真正价值。举例来说，当我们对客户报价时可能会说：‘我们会指派5位工程师，3个月 full time 做这个项目，根据业界软件开发项目的行情，台湾地区工程师一个人月是4000～5000美金，所以我们的软件报价是……’。

这个算法看似有道理，但5位资深工程师与5位菜鸟工程师创造的价值完全不会在同一个水平，即便是同样薪资水平或工作年资的工程师，其单位时间产出的差异可能达数倍之多。就算只看个人表现，对这个领域的熟悉程度也会影响其产出，甚至工程师当时的情绪或生活状况，都可能会使其产出不一致。

所以业界用人月来计算软件的成本或‘售价’，其实是无计可施之下的办法。也许你会问，那我们不要用工时来计算软件开发成本，直接计算最后交付给客户的‘软件价值’，然后乘上一个该赚的比例不就好了？

然而软件价值估算是一件更麻烦的事情！一个方法是用程序的行数来计价，同一个模块，工程师 A 用100行完成，但工程师B只用50行完成，且性能更好。请问，这个模块到底该如何计价？嵌入式系统里的一行程序和网络程序里的一行程序，价格又该如何区分？

软件价值估算是一个专门的学科，其中有许多复杂的数学模型。幻灯片中有个表格，说明了在某项评估软件开发成本的模型中，它包含一项公式，你会发现嵌入式系统的乘数与指数因子都高过其他种类的软件开发项目。这只说明嵌入式系统里的某个模块，应该比其他软件系统中功能类似的模块要来得贵，但软件系统种类这么多，怎么可能就这么简单区分为3种价格？

这个世界上没有多少人搞得懂教科书里的软件价值模型，所以软件报价与软件开发成本估算，一直是项目经理心中的痛。在嵌入式系统中，硬件与生产部分的成本较容易估算，而软件的成本估算还是只能基于 WBS 与资深软件人员的经验，毕竟分别对多个小任务进行估价，总是比直接评估整个项目要容易。

看吧！我们又碰到 WBS 了，WBS真的很重要！

项目成本管理（2）

■ 各阶段成本估算的误差

项目阶段

准确度

合约前（量级估算）

-25%~75%

合约期间（预算级估算）

-10%~25%

WBS完成后（确定级估算）

-5%~10%

■ 误差的来源

■ 基础数据不足，或项目仍存在许多不确定因素

■ 该项目成不对“需求”相当敏感

■ 经验不足、低劣的三本估算技术

■ 签约前后不连贯（如Sales.用高规格低价格杀回来的项目）

从幻灯片中的表格可以看出，工作被切割的越细致，所估算出来的成本就越准确。但实际上，根本等不到项目团队把项目需求弄清楚、把 WBS 做出来后才对客户报价，通常就是用这张幻灯片所提到的人月估价法（实际运行时，根本就是心里已经预算了一个总价，换算成人月后，再设法做个表格说明各个工作项目、需要多少人做多少个月，因此，这个项目总共需要多少人月，基本上就是一个凑答案的做法）。表格里显示出这种报价法的误差可达25%～75%,现在你们应该知道这个75%是怎么来的了。有一本很有名的项目管理书籍叫做《人月神话：软件项目管理之道》，极力推荐所有从事软件开发的同事们细读此书。

也就是说，嵌入式系统开发项目的合约价格通常和实际价格有段差距，所以这个价格拿来参考就可以了，千万不可以当作项目运行期间成本管控的基础。项目经理还是应该根据 WBS重新评估开发成本，并在项目执行期间，监督预算的使用状况。

项目成本管理（3）

■ 软件项目中人力成本是总成本最主要的部分；但嵌入式系统项目因为有硬件、结构与生产等成本，往往会忽略了人力成本所占的比例。

■ COQ (Cost of Quality)-质量管理也是要成本的。

台湾地区信息界其实是明显的硬大软小，许多系统厂家还是有‘买硬件送软件’的 business model——意即客户只要最后将产品交由该公司生产，则该公司只会收取一些软件开发费，甚至真的是软件免费，只要能在大批量生产时赚回来就可以了。

就算对客户的报价中不包含软件的金额，真正项目在运行时，软件开发不可能毫无花费，因此，项目经理还是要把‘实际’的预估成本算清楚，以确保项目不会超支。

项目成本管理（4）

■ 你知道你的项目目前的完成度吗？

■ 你知道项目进度与预算花费的趋势吗？

■ 挣值管理(EVM)

■ 成本管理

■ 进度管理

成本管理的主要目的就是避免超支，通常一般工程人员不需操心预算的问题，但如同进度落后会导致项目失败一样，预算控制不当，也可能导致项目断炊，一样会使得项目失败。而项目经理存在的目的就是尽所有的努力，使项目往成功之路迈进。因此，项目经理除了要监控进度外，也要控制项目‘烧钱’的速度。

换句话说，只用进度来描述项目目前的状况是不够的，还必须同时考虑成本的使用状况。项目管理知识体系提供一个已广为采用的方法——挣值管理。挣值管理主要用于项目成本和进度的监控，它将目前为止所完成的工作，与项目计划里的估计值进行比较，这会提供一个关于‘项目距完成还有多远’的估量。通过从已经完成的工作量推算，项目经理可以得到距离项目完成还需要花费多少资源的估计。

挣值管理是一个很有用的工具，除了可以告诉你目前项目的进度、成本花费以及与项目计划间的差异之外，更重要的是从曲线图中可以看出项目的趋势。虽然趋势表明项目可能会delay或超支，但不代表项目就真的会fail，但趋势向下无疑是个警告，此时项目经理必须提早do something，以阻止状况恶化。

挣值管理并不复杂，实际上我们也不需要自己按计算器，项目管理软件会帮忙计算出所有的进度与成本指标。对挣值管理有兴趣的朋友可以翻阅PMP的书，直接找个案例来看，今天我就不详述了。

02-07　项目质量（Quality）管理

项目质量管理（1）

■ 基本思想

■ 质儆是规划出来的，而不是检查出来的

■ 项目的质量是所有评发人员的工作质量之积

■ 例如：项目成员有10名，毎个人都只达到90%的质量要求

■ 则整体项目的质量将只有0.9∧10=3%

■ 质量管理并非一次性活动，而是贯穿整个项目

■ 质量等级是相对的，以客户需求和价格而定

■ 质最管理是一种精神，并非通过IS09000或CMM就能做出有质量的产品

把项目在预算内准时完成，并不代表就可以顺利结项。如果质量不符合要求，在改善之前，客户是不会愿意当冤大头验收的。

嵌入式系统包含软件与硬件，硬件的质量比较容易理解，例如：结构接缝是否密合、各零件的功能是否正确运行。但软件是逻辑实体，不容易描述其质量等级，软件问题基本上是由人为差错所引起，且问题隐藏在千变万化的逻辑组合中，若等到开发告一段落后才想要去一一找出所有问题，困难度会非常高。软件质量管理的关键是预防重于治疗，尽可能在规划阶段就要做好‘质量计划’，不能全靠事后检查。

一般以研发为主的公司，虽然都知道质量对产品的重要性，但在实际运行时，往往又无视质量单位应发挥的作用。PMI 注意到了这件事情，因而在项目管理知识体系中特别提到：在项目运行的过程中，质量单位应该全程参与，不只具有发言权，面对攸关项目的决策，甚至应该具有否决权。

在软件开发项目里更是如此。道理很简单，研发单位根据流程执行产品开发，但最终产品的质量（甚至可上推到项目的成败）却应该是质量单位的责任[7]。唯有如此，才能避免研发单位陷入‘球员兼裁判’的盲点。

投影片中列举了不少项目质量的思想，无论你是项目经理、工程师或测试人员，只要身为项目的一员，都请务必了解质量的精髓。质量管理必须贯穿整个项目，没有质量观念的团队，将会在项目后期付出沉重的代价。

质量系统是门大学问，市面上有不少体系复杂的质量系统，但我一向认为只需要弄清质量系统的基本概念即可，实际上则必须根据项目的实际状况来制定该项目的质量策略[8]。

项目质量管理（2）

■QA-Quality Assurance

■Is it done right?

■是一种“管理”职能

■在项目执行期间，根据质量计划对项目各方面进行质量稽核(Audit)

■规划、设计、实现、外包、测试…等阶段都应该被稽核，以确定是否有破坏产品质量的可能性

■QA应为独立单位，且拥有越级上报的权利

一般公司或项目团队总是弄不清楚 QA、QC、Testing-Team 这些名词的定义，所以老是会把团队冠上名不副实的名字。最常见的就是称呼只负责测试的团队为 QA，但实际上，公司内部根本没有任何单位负责执行真正 QA 该做的事。

质量保证（QA）主要目的就是确保产品在既定进度与预算下，能圆满达成预期质量水平与可靠度目标。QA 单位的工具就是稽核（Audit），任何项目流程中的工作，从计划拟定、模块设计、Code Review、测试计划、测试涵盖率、生产流程、备料计划等，QA 单位都可以进行稽核。特别是项目团队完成了某个里程碑、提出了某项变更请求，或是项目即将进入下一个阶段时，QA单位都应该主动介入稽核，避免问题被拖延到项目后期，将会变得更棘手。

我们知道工程师都是很爱面子的，所以 QA 单位在执行稽核时一定要注意态度，不能自以为是RD的上级单位，造成团队彼此的冲突。PM 更必须注意 RD 与 QA 间的关系，并尽可能参加所有Audit Meeting。

很可惜，本公司中并没有‘真正’的QA，但项目里不能没有QA，所以我的项目就只能由PM及其他主管包办代替。因为由主管来执行Audit（请注意，Audit和Review是不同的概念，前者主要关注的是流程与结果，而Review则较为技术性，关注的是 detail），工程人员也较容易配合。本公司因人力资源不足，项目执行者也只能从权。

PMP 提到了许多 QA 的工具，基本上和软件工程提到的 SQA 概念一致，各位有兴趣可以找来看一看。

项目质量管理（3）

■QC-Quality Control

■Is it done right?

■是一种“检查”职能

■执行QC的办法

■TR-Technical Review

■Code Review

■测试

■趋势分析

■缺陷跟踪

QC和QA的职责完全不同，QA是专注在流程的正确性，而QC则必须确定项目结果与质量标准是否相符，同时确定消除不符的原因和方法。以软件开发来说，QC就是根据质量标准，设法找出某软件版本的所有bug，并持续追踪每个bug被处理的状态。

软件测试有其方法论，QC必须有系统、有方法、有计划地执行测试工作，绝对不是乱枪打鸟。当找到bug后，必须利用bug管理工具妥善管理，每个bug的处理流程都必须记录下来。此外，bug管理软件也能自动产生报表，让项目经理可以判断各个版本间质量水平的趋势。

最后再强调一次，项目团队不能过度依赖QC的测试，如同RD无法写出没有bug的程序，测试团队也无法找出所有的bug。质量工作一定要在项目前面执行，如果等到后期才来做，势必得付出相当可观的成本。

并非所有公司都能言行一致，虽然口头上说质量第一，但组织中却无法落实质量系统。我们在业界工作，还是要让自己的项目尽量可以站在质量的保护伞下，以下是我的经验和建议。

■　质量活动必须经过规划，同时符合项目需求，并明文公布

■　推广‘提高质量，就是尊重客户’的理念

■　质量活动必须尽早启动

■　质量小组尽量可以独立存在

■　质量人员必须经过培训

■　工程人员必须能够理解，质量人员也是为了项目成功而努力着，并非存心挑剔或找麻烦

02-08　项目人力资源（Human Resource）管理

项目人力资源管理（1）

■ “人”足软件项目中最重要的因素，而“人”这种资源具备主动性与情感，且每个人都有自己的特性

■ 组织结构

■ 职能型组织（组织与项目利益可能冲突）

■ 项目型组织（资源重复，无法贯彻公司策略）

■ 矩阵型组织（职能经理与项目经理的冲突）

■ 人员的组织管理是否得当，是影响软件项目的决定性因素

■ MS有一个明确的规则一项目组的人员不要超过10人

项目团队是由于项目而成立的临时性组织，项目经理可能对成员并不熟悉，且成员之间的默契也不足，所以反而更加重了项目中团队管理的困难。

项目依附于企业而存在，因此，企业的组织架构当然会影响项目团队的管理。对项目来说，职能型组织与项目型组织是两种极端组合，前者并没有‘明显’的项目经理的职位，项目决策由参与该项目的所有部门主管共同决定；后者则是以项目来区分部门，部门主管就是项目经理，其最大缺点就是资源可能重复。

嵌入式系统项目并不适合在这两种组织架构中运行。以职能型组织来说，嵌入式系统牵涉的领域较广，若缺乏一位可掌控全局的项目经理，重要的事情需要由软件、硬件、结构、测试、生产等部门经理共同决定，不但效率差，而且每项决定可能都得经过妥协，对项目来说极为不利。对嵌入式系统来说，项目型组织的缺点更为明显，每个部门（项目团队）都必须有软件、硬件等各项专长的工程师。公司的技术能力分散，不但无法累积，对未来的技术发展也难有长远规划。

因此，对嵌入式系统开发项目来说，若能在职能型组织中增加一个平行单位——项目经理办公室（PMO），这种矩阵型组织相比较之下较为有利。

项目人力资源管理（2）

■ 强矩阵组织

许多企业只知道追赶潮流，不明就里地成立了 PMO，却仍是以职能型组织的方式做事，使得PMO变成企业中的闲人办公室或冷宫，这实在是辱没了 PMO 的设计原意。

我们公司就是采取矩阵型组织，项目成员来自各个团队。在项目运行期间，每个项目成员会有两位老板。至于项目成员究竟应该分配多少时间与心力在项目上，则视公司对该项目的重视程度，以及项目经理与部门经理的沟通而定。在对项目的支持上，部门经理与项目经理的认知往往是对立的，部门经理除了要在不同的项目配置人力之外，还要负责执行该部门的未来计划；但项目经理的目标只有一个，就是让当前的项目成功！

项目成员难免会被卷入两个经理之间的纷争，而这也是项目经理必须尽全力避免的事！身为项目经理，就是要尽力与部门经理沟通，设法让项目成员可以 full time 支持项目，若有必要，可以延请高层介入协调。

项目经理对于处理‘人’的问题务必谨慎，目的就是让来自各部门的成员可以专注于自己的项目，尽量不要与部门经理或高层正面冲突，毕竟，这对目前的项目与往后的项目是没有帮助的。

项目团队管理除了人力取得之外，最重要的就是团队激励，让团队成员始终保持旺盛进取的士气。团队管理是否得当，往往是影响软件项目质量的关键性因素，必须特别注意下列重点。

项目人力资源管理（3）

■ 团队管理

■ 软件开发时高密度的智力工作，太多的SOP会限制了人的主动性→宏观上采用制度管理，微观上采用自我管理

■ 激励理论-PM应利用任务分配权尽可能满足成员的需求层次

■　创建有实际存在感的项目团队

■　建立奖励机制

■　确立良好的人际关系

■　设定工作授权系统

嵌入式系统开发项目经常涵盖许多不同领域、不同文化的成员，所以管理方式也要相对有弹性。例如：软件工程师需要较轻松自由的工作气氛，不能像工厂采取军事化管理。面对不同的项目成员，也要观察其真正的需求，设法让其安心做事。

PMP 里提到了许多激励理论，幻灯片中的金字塔——马斯洛的需求层次理论，就是其中之一。各位若有兴趣可以去询问 Google 大神，简单来说就是8字箴言——因材适所、投其所好。项目里的工作不胜枚举，有苦工型、技术型、规划型、管理型等工作，项目经理必须让适合的人执行适当的工作。举例来说，若让自负型的成员去做苦工型的工作，其难免心生不满；若把技术型的员工硬塞去做管理性的工作，对项目及个人都不好。这个道理简单易懂，重点是项目经理必须把识别每位员工的特性与需求，当成最重要的事情之一。

嵌入式系统需要的软件工程人员数量不少，且软件开发是智力的活动，工程师一旦不愉快就不会有创造力！项目经理要尽可能与工程师‘博感情’，富有弹性的工作环境加上适度的压力，并按照计划与流程按部就班执行。

02-09　项目沟通（Communication）管理

项目沟通管理

■ 管理中70%的错误是因为的不善于沟通造成的

■ PM的80%的时间是用在沟通（或90%）

■ Communication Channel

■ N(N-1)/2

■ 为保持沟通的良好效果. 必须保持沟通管道的畅通与单一

据研究指出，IT项目成功的4项因素分别为：

■　管理层的大力支持

■　终端使用者的积极参与

■　有经验的项目管理者

■　明确的需求表达

而这四项要素全都依赖良好的沟通技巧。奇怪的是，IT产业里的技术人员通常只受过严格的专业培训，往往不知如何与外界沟通、协调及谈判等。试想，如果项目经理无法由客户了解真正需求、项目经理无法传递项目目标给项目成员、项目成员在技术讨论上各持己见，主管却无法协调等，则此项目成功的机会将微乎其微。

PMP 要求项目中必须具备项目沟通计划，内容应包含以下重点。

■　沟通需求：哪些人、哪些时候、需要哪些需求？

■　沟通内容：沟通的格式、内容、详细程度等。

■　沟通方法：明定沟通方式与沟通管道。例如：会议记录的管理方式、bug 管理软件就是RD与测试人员间的沟通方法、工作报告就是工程师与主管之间的沟通方法等。

■　沟通职责：谁发送信息，谁接受信息？

■　沟通时间安排：在项目计划中必须包含重要沟通事件的 Schedule，例如：每周的 review meeting、与客户的定时会议等。

实际上，过度正式化的沟通会议会让工程人员产生莫大的压力，项目主管必须尽量做到沟通于无形之中，而且在制定沟通计划时，必须尽量地简化，人多不会好办事，正如幻灯片中所说的，某件事参与的人越多，沟通管道会呈倍数增加，这只会让事情更加复杂。

02-10　项目风险（Risk）管理

项目风险管理（1）

■ Risk三要素：

■ 是一个事件

■ 有发生的概率

■ 会对项目造成一定的影响

■ 风险管理是项目管理中最容易被（故意）忽略，而且是最难以管理的环节

一般项目时常会无视风险管理，但以嵌入式系统开发项目来看，重要工程人员离职、存储器 size 过小、预算超支、规格变更、零件涨价或缺料等状况都可能发生。如果项目计划中完全没有考虑到防治风险发生的可能性，当执行时期‘恶梦成真’时，难免令人措手不及。

风险管理的思想是：并非每个可能的风险都需要处理（毕竟风险不一定真的会发生），但一定要在项目初期就将其辨识出来，并评估其发生的几率，以及可能产生的影响力。

举例来说，某个产品开发项目会采用Embedded Linux作为系统核心，但项目成员里只有一位工程师熟悉此领域，且最近有倦勤的倾向。当项目经理识别出这个风险后，他判断该工程师离职的几率很高，若一旦离职，对项目将是严重的打击。于是他决定先试着沟通，再度确认该工程师执行完此项目的几率，同时，他也延请另一位工程师进入项目团队，从项目一开始就熟悉Embedded Linux。

再举另一个例子。NAND Flash的价格变动很大，而且随时都有可能缺货，当项目团队识别到这个风险，就应该直接建议生管单位提前备料。

从这些例子看来，大部分的风险处理其实并不困难，而且可以在项目初期就排除或减缓。但如果未将风险识别出来，想要事后处理将难如登天，使得这些风险宛如项目中的不定时炸弹，让项目经理不得安眠。

项目风险管理（2）

■ 风险识别→列出风险列表

■ 风险评估

■ 定性分析

■ 列出几率/严重性矩阵

■ 对项目的薄弱环节有一定的了解

■ 定量分析-基于定性分析成果的数学处理过程

■ 列出“项目可能风险排行榜”（几率×严重性）

PMP 提供了许多风险定性与定量分析的实用工具，首要任务就是找出项目运行时可能发生的风险。风险识别需要具备一定经验的人来执行，通常就是根据项目目标与WBS，把项目在脑袋里‘跑’ 一遍，然后列出可能发生的意外事件，或是请所有项目成员，在轻松的气氛下举行‘头脑风暴法’，并记录下所有与会者对本项目风险的想法。

风险是未来才会发生的事，所以没有人可以列出完全正确无误的风险列表，但无论如何，有做风险识别绝对比没做的好，至少我们已经预先排除了项目的某些不确定性。

有了风险列表后，我们要为每个风险打两项分数——发生几率及严重性。同样的，这在项目初期是估不准的，但至少可以比较出高低，例如：客户临时修改规格的严重性比某零件缺料的严重性要高，而工厂发生火灾的机率比某重要工程师离职的机率要低。风险列表里的每项 Item 都会有两个分数，将其相乘并排序，名列前茅的项目就是我们非得do something的风险。

风险列表范例

项目风险管理（3）

■ 项目管理不可能消除所有的风险，但仍必须制定风险计划，以期消除或减弱特定的风险事件

■ 风险应对计划

■ 回避风险—及时改变项目计划

■ 转移风险—外包或保险

■ 损失控制

■ 承担风险

■ 持续维护“风险跟踪控制表格”

项目因为有着独特性与前期不确定性，因此，项目执行本身就是一件高风险的活动。项目管理不可能消除所有的风险，但通过一定的风险规划，并采取必要的风险控制策略，通常可以消除或减少某些风险的影响程度。

当‘风险排行榜’完成后，我们就该针对这些风险，拟定项目风险应对计划，以确定某个风险该不该处理？何时处理？怎样的处理方式？以下有4项规划降低风险的主要策略。

■　回避风险：主动放弃或拒绝使用导致风险的方案，试着寻找替代方案。

■　转移风险：将风险转移给外包公司。

■　损失控制：减缓风险发生时的危害程度。

■　承担风险：对影响力小或发生机率低的风险，可选择先不予理会。

再次强调，风险识别往往只能借助资深员工与顾问的宝贵经验，或许并非科学，但绝不能因此就不做风险分析。项目中可能发生各种意外事件，事先不可能全部料到，但能事先料想到的风险，却又不加处理，都只是平白错失提高项目成功的机会罢了！

项目风险管理（4）

■ 常见的风险：

■ 项目缺乏可见度

■ 新技术的吸引力

■ 技术兼容性风险

■ 性能问题

■ 仓促全面上线

■ 可用性问题(For End User)

■ 缺料、零件涨价

幻灯片列出了嵌入式系统可能发生的风险种类，以下再补充一些软件开发项目常见的风险与预防措施。

■　合约风险：主要就是合约条款有漏洞，项目经理必须会同法务人员详细检验查看合约内容。

■　需求变更风险：提前以书面方式，和客户约定好变更处理流程。

■　沟通不良风险：不可忽略‘项目沟通计划’的制定与公布。

■　缺乏高层支持风险：只能不断的沟通再沟通，或对高层诱之以利，说明此项目可以为公司带来的利益。

■　进度风险：有些项目的进度要求相当‘苛刻’，项目 delay 可能意味着市场机会变小。假使Schedule 无法调整，只能尽量多取得资源与预算，此时，分阶段交付项目成果也是方法之一。

■　质量风险：谨慎制定项目质量策略，成立质量小组等。

■　系统性能风险：增加执行设计阶段的力度、制作原型（Prototype）等。

■　技术风险：可考虑外包，或变更执行人员等。

■　团队成员能力风险：提早开始教育培训或变更团队成员。

■　团队合作风险：目标要明确，绩效制度要公开、公正、公平。

■　人员流动风险：尽可能将核心工作分派给多个人执行。

■　协助厂商风险：合作前即制定审核稽核（Audit）与验收流程。

02-11　项目采购/合约管理

项目合约管理

■ 软件外包，比本体的净投资多15%~20%

■ 外包项目的管理比企业开发项目更复杂，风险更大：

■ 沟通问题

■ 做好详细计划

■ 避免延误

■ 明确验收标准（软件规格与API…）

在嵌入式系统开发项目中，可能的外包项目：买入新技术、委托开发项目、硬件制作、代工生产等。一般人总以为将某些开发工作外包，会让项目团队更轻松一些，并增加项目成功的机会，其实这是个很大的困惑！我们在前面沟通管理中谈过，需要沟通的点越多，沟通管道就越复杂。公司内部成员沟通有时都难免会出现误会，更何况还要管理其他公司的开发进度。

所以在决定是否外包之前，一定要做 Make-or-Buy Decision，考虑的因素必须包含：

■　技术难度

■　沟通复杂度

■　外包金额

PMP中谈到许多不同形式合约的性质，但通常公司都会有专门的法务部门帮忙处理这些事情（即使我身为老板，也不会放心让工程师或技术出身的项目经理来处理合约），所以在此就不多着墨了。

02-12　项目配置（Configuration）管理

项目配置管理

■ 贯彻开发的全过程，目的是用于建立和维护产品的完整性和可追溯性

■ 对应软件开发项目中成熟的版本控制流程

■ 软件配置管理—对开发过程中的产品进行标识、追踪、控制的过程

■ 程序代码版本

■ 技术文件

■ 硬件配置（BOM）

项目配置（Project Configuration）这个名词听起来有点玄，简单的说，就是项目资产的管理。以嵌入式系统开发项目来说，当一件项目结束后，有哪些资料可以变成有用的组织资产呢？所谓组织资产就是有利于以后项目的资料，最直觉联想到的就是‘程序代码与技术文件’，除此之外，更重要的应该是‘开发轨迹’。例如：

■　项目的全貌，包含产品规格与设计规格。

■　项目原始的计划（第一版计划书），以及项目运行期间所有曾经做过的变更。

■　设计阶段的任何重大转折点、与项目运行期间技术上的重大突破。

■　软件开发期间曾经遇到哪些问题（如 bug），解决的方式是什么？对应的程序代码是哪些？

■　重要软件版本或项目里程碑（项目进度表时间检查点）所代表的意义，以及该时间点项目状况的快照（Snapshot：以软件系统来说，所谓的 Snapshot 就是当时的版本）。

■　硬件设计与生产阶段的问题履历及解决方式。

在嵌入式系统项目中，所谓的‘开发轨迹’其实就已经包含了计划书、规格、设计文件、程序代码和问题清单（可能是 bug 管理 server 的 database 备份），为了将这些宝贵的资料保留下来，必须在项目运行期间做好配置管理。其实配置管理并没有想象中复杂，各位在项目中都有使用版本控制 server、bug 管理 server，以及定时的统一备份文件等，这就是在做配置管理，只是在我管理的项目中，会更明确地指派相关工作，通常我会请系统团队的小组长帮忙，而且我个人也会随时监督。

关于配置管理最容易被忽略的就是配置管理可以在项目运行期间发挥极大的功效，可让项目成员了解自己在做什么，也能随时追溯到任意一个时间的状态，但配置管理更大的目的却是让项目技术与管理资产可以不断累积，特别是在项目结项阶段人员鸟兽散时，就有待项目主管们能够定下心来将这些信息整理与归档。

各位是否经常在从事开发工作时碰到以下问题？‘这个模块以前好像做过，但程序代码不知道在哪里’、‘这是一个 REOPEN 的 bug，之前在某个版本是 OK 的，但现在怎么也调不出那个版本’、‘实在搞不懂当初为什么要这样设计，看 code 也看不懂，但目前仅存的设计文件居然是旧的’。如果这是你们项目运行的常事，应该不难想象这样的‘常事’到底浪费了多少企业宝贵的研发资源。

02-13　企事业与组织对项目的影响

最后不能不提一点，就算我们拥有很棒的项目团队，技术能力与管理流程也都没问题，也不见得能够保证项目成功。因为电子产品开发项目依附企业或组织而存在，每家公司都有自己特殊的企业文化及财务状况，而且组织有更高层的老板与技术资产，都可能影响到项目执行，这是项目经理必须面对的层层考验。

所以在 PMP 中，除了今天说明的5大流程与9大知识领域之外，还有两项在项目运行中不可忽略的因子。

■　Enterprise Environmental Factors（企业环境因子，简称 EEF）：大部分PMP 的流程都必须参考 EEF, EEF 泛指会影响到项目成败的企业内、外部环境因素。例如，组织文化、组织架构、企业运行流程、政府或产业标准、市场状况、项目关系人的风险忍受度、政治气候和研发团队专长等。

■　Organization Process Assets（组织流程资产，简称 OPA）：基本上，OPA应该也是一种EEF，包含企业中所有与项目相关的流程规范，主要区分为两大类。

□　流程与程序：例如，工作授权程序、组织标准流程、各类文件模板、绩效衡量准则、issue 与 bug 管理、变更控制程序、财务控制程序等。

□　公司知识库：例如项目文件、以前项目的程序代码、issue 与 bug 数据库、历史信息与经验学习知识库等。

为何要在最后才来谈EEF和OPA呢？因为我想再次强调 PMP 绝对没有强制规定项目一定要如何执行，它仅是一组思想，加上许多流程及工具而已。每件项目都是独一无二的，死背 PMP的规范及工具一点意义也没有！因此，项目团队还是必须因地制宜，根据项目的特性，以及企业环境的状况，选择合适的方法与工具来执行项目。

希望今天的课程可以让各位对项目管理有正确的理解，也希望今后本公司的所有项目都能采用更有效率的方法来执行。谢谢大家的参与，下课啰！”

[1]　关于这部分，可参见《第1章：浅谈系统·嵌入·硬件》。

[2]　在《第4章：嵌入式系统开发项目生命周期：设计、执行与结项》，将会对此流程图有更深入的阐述。

[3]　意外或风险事件这些计划外的事情，对进度吃紧的项目来说，真的可以说是袭击。

[4]　当项目所有工作的Schedule订定完毕后，将完成项目所需的时间（关键路径的总工时）乘上30%~50%，当作此项目工时的缓冲时间。因为项目很难有完美的计划，所以必须合理的“灌水”，以免执行项目时完全无法承受任何风险。

[5]　通过评估新项目的优势（Strengths）、劣势（Weaknesses）、竞争市场上的机会（Opportunities）和威胁（Threats），以决定启动这个新项目是否具有意义。

[6]　关于变更管理，请参考《第15-1节：进度追踪与变更控制流程》，其中有更深入的说明。

[7]　项目最终的成败，当然是项目经理的责任。这里只是说明一般老板通常认为质量问题完全是RD的责任，其实这是错误的观念。

[8]　请参阅《第3-7节：质量策略规划》

Chapter 3

嵌入式系统开发项目生命周期：项目启动与规划

时间：03月01日　 地点：办公桌前　 登场人物：菜鸟、项目经理（PM）

PM：“这是你第一天上班，你决定到AP组还是待在我旁边当‘预备队’？”

菜鸟：“我思前想后，觉得个人的学习能力与热情都还可以，如果可以跟着项目经理做事，应该可以对产品的开发流程有比较全面的认识。”

PM：“嗯，孺子可教也。那你准备好了面对各种不同性质工作的挑战吗？例如，这个月我可能派你去写驱动程序，下个月可能请你去帮忙应用程序解 bug。”

菜鸟：“没问题，就怕我的能力无法应付这些任务，万一耽误了进度就不好了。目前我只对自己程序设计能力有信心，也大概知道嵌入式系统是什么，但对于诸如驱动程序、嵌入式操作系统或和硬件相关的软件调试都没有概念，更不要提看线路图或者和硬件工作人员合作了。此外，我对联络其他领域的厂商也有点不知所措。”

PM：“别担心！因为我们团队的人不多，而且大家一直处于非常忙碌的状态，不会有多余的时间和人力专为新进工程师做教育培训。我的做法都是在任务中培训工程师，当然会从小任务开始，然后我会观察你的能力是否能持续成长。此外，你跟在我身边做事，也可以随时观察我、其他同事或协助厂商解决问题的方法和态度，我想，这是再好不过的教育培训了。

首先第一堂课的内容是：启动嵌入式系统开发项目。因为某日本大厂要委托我们公司开发下一代的电子辞典，现在客户代表已经到了，你就跟我去参加这个会议吧！记住一件事，这种会议总是高来高去的，可以的话你旁听就好，尽量不要发言，不了解的事，会后再问我，OK？”

除了学校的实验课程或期末作业外，进行某嵌入式系统的开发当然是为了让该产品及时上市，所以开发工作必然有时间、质量及资源的限制。为了在规定的时间内，善用有限的资源，开发出品质可被客户接受的产品，嵌入式系统的开发工作必须被当作一个项目（Project）严格控管。然而，项目管理并非嵌入式系统开发特有的概念，事实上，项目管理的思想，适用于小至学生读书计划的管理与实现，大至火星探测计划等领域。但单就软件开发而言，因为嵌入式系统开发会涉及到硬件以及第一章提到的诸多限制，所以嵌入式系统项目管理相对较为复杂。

一个项目可层层细分为较小的子项目，一直细分到每位工程师的工作规划都可以算是一个项目。因此，无论工程师或项目经理，都必须具备项目管理的思想与知识。良好的嵌入式系统项目管理者通常技术扎实、经验丰富，能综观主因（Root Cause），要求正确的单位或人去解决。而一位好的工程师除了技术能力之外，也会把自己的事情根据重要性与轻重缓急做适当的排程、不会排斥编写技术文件，和其他单位或其他公司员工有良好的互动，并且对任何异常状况也会主动回报。

以笔者的观察，在台湾地区地区与大陆业界中，大部分的软件系统开发项目虽然都号称项目，但真正用项目管理方法的执行者却少之又少，实际的状况可参见《附录A：未执行项目管理的项目》。人力不足、时间太赶都是不执行项目管理方法的借口，重要的是项目主管是否对产品质量有所坚持。本章会详述在资源缺乏的状况下，如何用项目管理的思想来开发嵌入式系统。

有鉴于项目管理的思想对知识工作者极为重要，在开始深入探讨嵌入式系统开发技术之前，第3、4章会先从项目管理的角度，完整描述嵌入式系统开发的全貌。你也可先略过，等其他与技术较相关的章节读完后，再回头详读嵌入式系统项目管理，必然会对嵌入式系统开发这个领域有更深入地了解。

03-01　嵌入式系统项目简介

菜鸟：“刚刚那个所谓‘启动新产品开发项目的会议’和我预想的很不太一样。例如，那个日本客户从头到尾就只提了产品的概念及上市的大概时间，其他时间都和我们老板在嘻嘻哈哈，最后大老板二话不说就答应要承接这个项目，并且命令我们团队在一周内做好技术评估与人力和进度的规划，我感觉不确定的事情也太多了吧！”

PM：“幸好你这些话没在会议上提出。那我这样问你吧！你能点出你认为会议结论里最奇怪的地方吗？”

菜鸟：“先不要考虑开发费的问题，我一直以为客户会先把规格订好，然后等我们仔细评估之后，公司才会决定接不接这个项目。”

PM：“嗯～你的想法是对的，但现实却往往不是如此。教科书当然会说为了控制质量与开发进度，需求评估与产品规格制定必须按部就班，但书上谈的是管理行为，而刚刚那个会议明显是商业行为。教科书总不会说老板昨天带着客户上酒店，就把这个开发项目敲下来了吧！虽然身为项目经理，在这样的会议中，我也只是进去当技术顾问或咨询人员。基本上不会有人问我该不该做这个项目，更甭提你这个菜鸟了。”

菜鸟：“这不合理！如果都不知道要做什么，怎么能决定该不该做？”

PM：“呵呵～的确是不合理。”

菜鸟：“接了这个项目或许是灾难的开始，你还笑的出来……”

PM：“呵呵～明天我会召集干部评估这个项目，你再来旁听吧！目前这个阶段，项目还没正式kick off（启动），我的专业是根据目前团队的技术能量，以评估这个项目所需的软硬件技术、资源、人力及外包厂商，而我的责任就是提供正确的信息给决策者。至于要不要接一个项目，我可以报告我的意见或建议，但老板自有他的考虑，我无法做这项决定，而且也不是我该做的决定！”

电子产品开发周期，应该从以下3个方面来观察（请注意粗体部分的动词）。

■　设计产品：商业行为

■　管理项目：管理行为

■　开发系统：技术行为

要开发一项成功的产品，这3个方面必须互相配合，且面面俱到，缺一不可。举例来说，即便项目管理的十分完善，产品也如期开发完成上市，若一开始产品定位设计是错的，产品卖不出去无异做义工。反之亦然，若技术人员的技术无法配合，产品无法根据原先设计开发出来，就算产品设计再有创意、市场调查结果再令人绚丽也是惘然。

而通常每个方面有不同的决策者，各自都有着不同的考虑。例如，常常客户的某些决定看在工程师眼中无异是慢性自杀，而负责做决策的老板，在职衔前总是会被冠上“猪头”这个形容词。然而，客户通常不会拿他们公司的钱开玩笑，而也不见得每一位老板都是那么猪头。这个情况就像瞎子摸象，除了项目经理要建立好高层与工程师之间沟通的桥梁之外，工程师也要培养自己开阔对全局的视野，这样的团队运作起来才会更为顺畅。

以下分别对这3个方面做较深入的探讨。

3-1-1　“设计”电子产品

很多项目之所以会失败或陷入似乎难以脱身的泥沼，不全是管理或技术的问题。有时事后分析才发现，某些产品根本就不该做，或某些项目根本就不该接。电子产品因为具有高时效性、生命周期短、竞争强度高的特性，很容易会让决策者一时昏了头，而做出错误的决定。市场上永远不乏“自杀”的产品，就像旅鼠迁徙一样，一只一只前仆后继地跌入水里。

产品设计立项之前，一定要做好市场调查和评估，营销（Marketing）是另一门高深的学问，而这并非本书的重点。在此笔者要点出一个业界真实的状况：即便做过详尽的市场研究，许多产品依旧销售不出去，但却有些成功的产品仅基于一瞬间的简单创意而来。现今世界电子产品的市场变动日新月异，要定义一个保证可以符合未来需求的产品相当困难，特别是消费性电子产品，“未来”二字指的可能只是短短3个月或半年。这也是为什么嵌入式系统开发团队接到项目的进度总是如此紧迫，而不合理的进度肯定会对往后的管理与开发工作造成极大的影响。

所以产品设计不能仅考虑时效性及市场需求，也要就技术可行性做基本的分析评估。一个根本无法如期达成的目标仅是空中楼阁而已，而且强迫开发团队执行一个不可能完成的任务，除了耽误商机之外，还会造成资源的浪费与人员士气的低落。

产品设计结束后，要写下详细的产品规格，这个规格可以用来估计技术层次、需要的配合单位与人员、开发费用以及产品造价等，因为接下来的人力配置及系统设计的工作，都会根据这个规格来展开。照理说，要更动任何产品规格都必须经过严格的审查流程，越到开发后期，更改规格对项目的影响程度越大，如图3-17所示。例如：某手机产品已经接近量产阶段，客户才说应该要加上插卡的功能，这影响不单单只是软件的系统架构，CPU能否支持插卡也还要仔细评估（当然可采取外加芯片来解决这件事情，但是因为最初没有这项需求，CPU的所有PIN脚可能已全部用完），更甭提结构设计了，重新开模绝对势在必行。造成“兵慌马乱”不说，整个进度无可避免的必须往后延迟。

而现实情况往往是：客户永远是对的。只要一句：没有某项功能，这个产品我们恐怕没办法卖。假使开发团队不想让执行一半的项目腰斩的话，也只能摸摸鼻子去想对策，但也加深了研发单位和客户或市场营销部门的对立关系。

▲ 图3-1　越到项目后期，更改规格所要付出的代价越大

更改产品规格在所难免，但应该尽量避免或减少。要做到这点，产品设计这个阶段绝对马虎不得。 一般总把产品的失败原因归咎于开发单位的管理或技术能力不足，但往往忽略其实没有完备规划的产品设计才是导致灾难的主因，而开发单位的delay不过是必然的结果及代罪羔羊罢了！

笔者虽然并非专职的Marketing People，但在业界打拼多年，见过失败的产品设计不知多少，再加上自己开公司负责消费性电子产品的设计与产销，因而对电子产品设计小有心得，相关建议可参考《附录D：电子产品设计的最终依据：用户体验》一节。

一般公司都把产品设计交付给工程师这样的“科技专家”负责，但其实他们是最不适合执行这件事的人，因为工程师总是会用工程学的方法去思考与解决问题，用想当然的态度去‘配合计算机’，而非真正站在使用者的角度去思考。当然，有些规模较大的公司会有较专业的产品设计部门，即便如此，也不该将工程师的培养教育都集中在技术层次，殊不知满足客户需求与重视客户体验，才是产品设计成功与否的最关键因素。笔者认为，必须把“以用户为本”的思想传达给所有项目成员，任何工作指派都必须以“满足终端用户需求”作为唯一目的。

现今市场上充斥着太多“过度设计”与“设计不良”的电子产品，把end user搞得跟计算机白痴一样！为了这些会让终端用户产生“恐惧感”的产品，开发团队不知得投入多少额外的人力与资源，却只是为了在产品规格上多加几行没几个人知道干什么用的功能名称，且这些功能恐怕一般用户一辈子都用不到！与其乱枪打鸟，不如在产品开发项目一开始时，就把产品设计做好，认真思考这类产品要卖给哪一类消费者，彻底弄清楚他们的需求到底是什么。

3-1-2　“管理”项目

常常有老板级的人物问笔者，既然软件工程规范了这么多的准则，为什么绝大部分的软件项目还是会delay?为什么没有人敢说他的系统或软件是绝对没有bug的？为什么解过的问题还会再出现？为什么工程师拼命加班，事情还是做不完？你们读信息系的不是都上过“软件工程”课程吗？

其实不单是软件工程有这个问题，大部分的教科书都有类似的状况。书中的道理基本上都是对的，但未必能在每一个领域或在不同状况下成为标榜。如果教科书是圣经，那么，这个业界中不合理的事情简直是数不清了！我们应该要从教科书中学习到软件工程的精神或技巧，而并非是不经思考就全盘应用在目前的工作上。嵌入式系统的项目管理更是如此！嵌入式系统项目的复杂度也许不及一些大型软件项目，但因牵涉领域的单位较多、开发进度较紧急，再加上诸多限制，所以需要更多技巧来进行项目的管理。

举例来说，因为产品规格中允许的开发时间通常是不合理的，如此势必压缩许多工作项目（如硬件设计、系统设计、测试等）的时间。面对这样的困境，许多项目经理索性将设计和实现工作的执行时间重叠，或默许工程师不写设计文件，甚至减少测试量，更甚者会草草跳过设计阶段。但根据经验，这样的做法通常不但不能加速开发的进度，反而会因设计不良，而在开发的中后期发生不可弥补的错误，到时项目就会处于完全失控的状况，产品上市将遥遥无期。

项目经理就像战地指挥官，他必须清楚地了解其任务是要在期限内完成质量可接受的产品开发，在这项前提下，必须衡量人力及相关资源，只做该做的事，能够因时制宜才是好的管理。至于何谓“该做的事”，则是本章接下来的重点。

3-1-3　“开发”嵌入式系统

系统开发应该是工程师最熟悉的领域了，但绝大部分工程师往往会忽略一件事，即任何开发工作都必须在项目管理的约束下进行。举例来说，工程师绝对不能完全不理会进度和人力的限制，怀抱着浪漫的想法，一心只想设计出世界最优秀的系统。所以开发单位的小主管，必须在尽量不打击士气的前提下，抑制工程师的浪漫天性。

所有参与开发的员工都必须被灌输一个思想：我们开发的是一个嵌入式系统，是一个电子产品，且必须在有限的时间、人力、存储器空间、CPU计算能力内完成开发，这意味着我们只需达到“足够好”的水平，任何要求十全十美的设计都可能带来灾难。例如：手机市场，使用者永远找不到一款功能、价格及外型都满意的机型。除了牵涉到市场营销的策略外，这更告诉所有工程师一个资本主义世界永恒不变的真理——只要能卖出去且赚得到钱的产品就是好产品！

也许没有教科书或老师愿意正式做出这样的论述，然而，这是普遍存在的经验法则与事实！一个做得再好的产品，无法赶上商机，都只是白费工夫而已。

开发工作源于对产品规格的了解与讨论，接着开始设计。在设计结果经过严格审查并文件化后，才能进入实作阶段（Implementation），最后则是测试与调试，经过品保单位同意后，才可进入生产阶段。因此，所谓的开发工作不只是写程序而已，甚至可以说编写程序并非最重要的事，只要设计工作做的好，不需要天才工程师也可以将其实现出来。

但业界中本末倒置的状况屡见不鲜。设计工作还未完成就兴冲冲地开始动手，最后所有的“设计”都在某人的脑中，偏偏团队中这样的超级巨星都不相信其他人的能力，很怕其他凡夫俗子把他的设计搞烂。最后他累的要死，其他人却帮不上忙。简单地说，这位超级巨星反而变成项目开发过程中最大的瓶颈。

即便嵌入式系统有种种限制，无论如何，设计工作一定要确实执行，绝对要文件化，这样才能让开发团队的力量发挥到最大。我们将在《第4-8节：设计文件的重要性》中做更深入的讨论。

03-02　初期规划：是否该接这个项目？

PM：“第一次评估会议开完了，各小组主管的意见你都听懂了吗？”

菜鸟：“大部分听懂了，但其中有些专有技术名词我不太清楚，而且我隐约觉得会议的结论和我昨天说的一样，大家都觉得如果接了这个项目将会是一场大灾难。虽然没有人敢直接说出来。”

PM：“我认为会议结论和你想的并不一样，各部门主管都已经把困难点点出来了，我也都听到了。和人力或其他资源有关的，我会再去和老板交涉；和技术有关的，主管会让他底下的工程师再去研究。我不是说3天后再开一次评估会议吗？工程师通常有个坏习惯，热爱接受挑战，常把困难事情想得太简单；而研发小主管刚好相反，因为要扛责任，反而会把事情讲得比实际困难。项目经理必须保持头脑清醒，不能仅听片面之言，不经思考就妄下决定。”

菜鸟：“果然是老江湖，想得比较长远。”

PM：“我也是从工程师→小主管→项目经理助手一路爬上来，在不同位置应该要有不同的想法。我认为坐不同位子，就得换不同的脑袋。因为肩上担负的责任不同，视野也会因此有所不同；要照顾的人员数目不同，自然做出的判断也会不同。项目经理必须担负项目开发的成败，不能有太任性的言语或行为，否则会造成管理上的困难。既然上层决定接下这个项目，我的责任就是在此时规划出可行的实施计划。

还记得我在‘项目管理’课程中一再强调的：项目经理必须在沟通上花80%以上的时间。所以我一定会尽快找这些小主管了解他们真正的困难所在，并一起找出解决的方法。在项目启动阶段，项目经理一定要主动出击，与干部们进行充分的讨论，并对项目的目标与执行方式达成共识。

有些老板喜欢说他办公室的门永远敞开着，欢迎各级员工找他聊天。其实这只是偷懒的推托之词，这会使得老板永远只能听到偏见，看到表面，离组织真正的状态渐行渐远。为了让项目可以顺利结项，项目经理只能主动去发掘问题，并不断地与项目成员进行沟通。”

该不该接一个项目需要考虑的层面非常广泛。例如：有时老板为了接下大厂的项目，就算赔钱、倾整个公司的能量都得做，就为以后可以作为推销公司研发能力的最佳证明。但这些层面是老板该考虑的，本章是以项目经理和技术人员的观点为出发点，讨论在项目开始之前必须做哪些规划，以确保项目的可行性。

做完进度、规格、技术、人力、成本等项目的分析与规划后，除了可以得知项目是否可行之外，对所需的资源以及可能遇到的风险，心里也会有个谱。不仅是项目经理，就连工程师都有义务提供其负责领域的分析结果给他的主管。如果让老板在信息错误或不足的状况下做出决策，通常只会给研发团队带来无可挽救的灾难，到头来倒霉的还是自己。

若项目开始前的分析与规划做得够确实，在这个基础上进行项目设计与开发，绝对会给项目进行中的各个阶段带来良性影响。因此，团队上下绝对不能忽视初期规划这个动作的重要性，更不能草率行之。

3-2-1　没规划，一定挂！

菜鸟：“既然大老板都决定接这个日本大厂的项目了，加上进度这么赶，我们何不立即招集人手开工，偷跑半个月总是有帮助的吧！”

PM：“我以前做工程师时也是这样的想法，当接到老板要求我评估某项技术问题的可行性时，我总是兴冲冲的马上动手做出一个简单的prototype（雏型）[1]，并骄傲的展示给我的老板看，并告诉他，这件事包在我身上！”

菜鸟：“这有什么不对吗？”

PM：“不对的地方有两点。第一，我误解了老板的命令，他要的是评估报告，而不是证明我是否可以做得出来。第二，展示或实验结果，和最终的产品功能往往有很大的出入。我在这种个性上吃过很大的亏，还有一次差点害惨了整个项目。”

菜鸟：“不懂，做完Demo不就应该可以证明技术上是可行的吗？产品化不就是把Demo程序加以修改或调整就好了？”

PM：“在一般状况下，你说的是对的，但嵌入式系统项目常常会巅复所谓正统的想法。就我的经验来说，我们以前曾经做过一个电子辞典的项目，同样也是日本厂商，他们要求在有限的存储器内放下大容量的数据，这等于要求系统必须要有极高压缩率的算法。当时我的老板要我评估是否可行，于是我先从客户那里取得要存入电子辞典的原始数据，然后花了一个星期的时间到网络及相关的学术论文中搜寻，研究出一个算法，对客户提供的数据格式有极佳的压缩率，于是我马上在PC上写了简单的工具，并向老板报告我的发现，这是我们公司当时决定接下这个项目的主要原因之一。

项目进行到一半，才发现我这个算法需要大量的存储器以及CPU计算能力，在实际机器上解压缩一个字词解释居然要10秒以上！连我都无法接受这样的性能，更甭提客户了。虽然后来换了个效能较佳的算法，但架构整个大改。于是进度delay了约两个月。更严重的是，我们根本设计不出能运行在低运算性能CPU上，同时又兼具如此高压缩率的算法，于是只能要求增大存储器的容量，产品制造成本因此提升。客户对此相当不高兴，认为我们不但打乱了他们的计划，还压缩了获利空间，除了对大老板发出严重的客诉外，似乎还打算根据合约要求我们公司赔偿。

你知道在这件事情中，我犯的最严重错误是什么吗？我把项目前规划的意义搞错了！否则我会先去弄清楚平台的规格才开始进行分析，然后我就会发现客户要求的压缩率是不可能达成的，应该先和客户讨论是否接受较差的压缩率，或是改换性能较好的CPU。若能在先期发现这个问题就可避免上述悲剧的发生。

在这个例子中，因为我好大喜功的偏差观念，提供给了老板错误的信息。事后我检讨自己，完全是根据自己的看法去执行老板交付的任务，当时也许只为了证明自己的技术能力好，却完全没有考虑到全局！换个角度想，当时我的老板也是经验不足，否则他应该实时提醒我，他要看的是分析报告，而非程序Demo。如果当时他要求我以书面或口头报告分析过程，应该就能发现我的分析破绽百出。我常常用这个例子提醒自己一定要确实做好项目启动之前的分析与规划。”

嵌入式系统开发团队几乎不会接到性质完全相同的产品开发项目，所以即便是经验丰富的团队，在确定接项目之前，一定要做过极为仔细的分析。若贸然略过这个步骤，那么，随后展开的项目设计与开发，无可避免将是一场恶梦的开始。

3-2-2　完成不可能的任务

菜鸟：“可是刚刚评估会议中固件部门的小主管明确的表示，如果客户坚持要用指定的CPU，并要在规定的时间内完成，恐怕是另一件‘不可能的任务’，而硬件部门主管也同意他的说法。当时他说的慷慨激昂，我听着也是频频点头，我觉得他说的有道理啊！要换一个还在ES[2]的新CPU，不确定的因素很多，这颗CPU尚未被真正应用在已上市的产品中，有没有大问题？开发是否很困难？这些都不清楚。您不是一直说要在接项目之前，尽量扫除所有不确定因素吗？”

PM：“你知道《不可能的任务》电影已经拍到第几集了吗？”

菜鸟：“好像是第3集了，这和我们要接的这个项目有什么关系呢？”

PM：“当然有关系，老板花钱请我工作就是要帮公司把看似不可能的事情变成可能，如果每一个项目都要看起来很简单才能做，那我们团队恐怕也没什么竞争力可言吧！你看片中主角Hunter都已经演到第3集了，有哪一个Impossible Mission最后是失败的？执行任务期间虽然脱离不了惊涛骇浪，但这也是那一行的乐趣所在吧！不幸的是，偏偏我们嵌入式系统项目开发和情报员的工作性质还真的有点类似！”

菜鸟：“会不会夸张了一点？”

PM：“不会！你有听到固件部门主管说这将是另一个不可能的任务吗？‘另一个’这3个字表示什么你知道吗？就是我们公司或团队至今接了不计其数的‘不可能的任务’。如果我们从接项目前的分析规划、设计、开发、测试等，按部就班地做好每一个步骤，我不敢说我参与的每一个项目都不会delay，但到目前为止，真正完全失败的项目还真的不多。”

菜鸟：“是喔！那你觉得这次我们到底有没有办法完全满足客户的要求？”

PM：“其实，当我听完客户对规格及进度的需求后，自己也密集地做了一些功课，我发现假使人力支持充足的话，这个产品开发项目未必这么困难。就以换新CPU这个例子来说好了，虽说是新CPU，但无非是我们目前平台CPU的下一代罢了。CPU核心是一样的，只是为了符合我们产品的新需求而抽换了CPU内部的一些周边模块。也就是说，大部分旧的程序都可以直接在新CPU上执行，只要修改一些驱动程序就可以了。

此外，这家CPU的厂商可是日系大厂，我们已经采用了他们几代的CPU，质量一直都不错。而且他们对于技术支持一直都很积极，我前天和他们联络，并签了保密协议之后，我已经拿到新CPU的Sample Code，目前也已交给固件人员去研究了。当然换CPU绝对是一个风险，但深入评估后，其实需要的人力和时间还是可以控制的。

我明天就会和各部门主管沟通，毕竟他们都有各自的专业背景，项目经理必须确定每个人都清楚明白他们要做的事情。如果有小主管对项目的可行性心存怀疑，这种人在团队碰到困难时往往都会讲一些打击士气的风凉话。”

菜鸟：“所以项目开始之前，管理者要拿捏好分寸，不能不做任何分析就把事情想得太简单，更不能只听从某些人的片面之词，就认为既定目标无法达成。所以项目经理基本上要懂很多啰？”

PM：“慢慢你就会知道，一法通，万法通，许多判断真的只需正确的思想即可。当然项目经理也不能不具备技术知识，否则很容易被项目成员蒙骗。而我刚好是那种懂很多的项目经理，嘿嘿，想蒙我，门都没有！”

客户交付的开发项目肯定不会是太简单的（假使是个简单的项目，客户就会用更少的钱委托其他团队开发了），再加上客户一定会提出特殊需求，在未深入分析之前，确实会觉得荆棘遍地、窒碍难行。许多事情深入分析后会发现其实没有那么困难，而且工程师通常都无法抗拒新技术的挑战。

综合本节的讨论，我们知道接项目前的规划与分析绝对要异常小心，虽然难免有所疏忽，但一定要尽量避免把事情想得太简单或太困难。笔者必须苦口婆心的一再提醒：早期的疏忽或偷懒累积到后期，往往就会变成难以挽救的大灾难。这是经验法则，绝非危言耸听！

03-03　进度规则：ASAP？

菜鸟：“所以，其实你的心里已经很有把握这个项目是可以做的啰？”

PM：“还没，目前为止我只深入地考虑了技术的问题，但除了技术层面外，进度的可行性也是重要的评估项目。就算先不考虑软件开发的工作量，客户要求的上市时间距离现在还有约8个月，期间必须做3次试产，每次试产后要做500小时以上的环境测试。你知道500小时是多久吗？是一个月喔！再加上Layout、洗板子所需的时间，这些无法压缩时间的事项，就已经几乎把Schedule最后的5、6个月排满。如果中间出了什么纰漏，后面的事情无可避免的都要往后延期！”

菜鸟：“哇！我从来没想过这些事情。”

PM：“制定进度这件事牵涉极广，特别是嵌入式系统开发这个领域，牵涉的单位极为广泛，而且各单位负责的事项还彼此有关联。举例来说，如果软件单位无法如期发行版本，或者结构设计延误出图的时间，导致模具来不及修改，工厂试产都必须被迫延期。如果仅以软件开发来说，各个模块难免会有前后关系（模块A测试无误后，才能继续开发模块B）[3]，再加上人力吃紧，往往一个工程师要负责多个模块，而固件工程师的工作又会与硬件开发进度息息相关，如此更提高了制定Schedule的复杂度。”

菜鸟：“既然复杂度与不确定性那么高，那么在项目启动前所制定的进度准确度会高吗？”

PM：“现今的项目复杂度都非常高，一开始订的Schedule肯定不可能准确，至今我还没看过不用修改Schedule的项目。我只能这么说，虽然明知无法在一开始就订出可以适用到项目结束的Schedule，但任何参与项目的单位与个人都必须把Schedule的制定视为一件很严肃的事情。

我见过许多很爱画Schedule的项目经理，每过了一个Milestone，一旦他发现许多工作的进度已然失去控制，就会大费周章地把所有相关单位的主管通通找来，花个半天到一天的时间来重订进度计划表，然后信誓旦旦地告诉已经疲惫不堪的与会者：‘接下来我一定会做好协调工作，也请各单位谨守这个最新的进度计划表’。通常这个时候不会有人有意见，因为他们知道下个Milestone势必失守，届时再来画一次Schedule就好了。”

菜鸟：“我有点不懂，既然某个时间点已经确定无法准时达成，那么重新制定一个比较符合现实的Schedule有什么不对？”

PM：“前面提过，嵌入式系统项目的进度制定有一定的难度，所以在业界中，修改Schedule的确是件很平常的事情。但重点是：第一，项目既定的完成时间是不能轻易更动，换句比较传神的说法，就是要‘死守’；第二，所有人都必须具备一个思想，Schedule是用来遵守的，不是用来修改的。

所以应该用下述精神来对待制定Schedule这件事。Schedule应该是由粗到细的，而且任何工作项目的进度除了要先咨询有相关经验的同事之外，还要询问负责该项工作的工程师或其主管的意见，这样才可能制定出切实可行的Schedule。而在项目的执行过程中，项目经理必须随时掌握各个工作项目的进度，定期地审视Schedule与现实的差距，当某个项目出现偏离Schedule的征兆时，就要主动关切并提供协助。也就是说，当delay发生时，项目经理或小主管，甚至工程师本人都应该积极地设法解决问题并赶上进度，而不是消极地想要修改Schedule，否则项目真的不会有完成的一天。但前提必须是：制定Schedule时，务必用心协调并整合各单位的意见，否则自己闭门造车，真的会创造出一个不可能的任务，工程师就算做到死，也无法如期完工。”

▲ 图3-2　使用Microsoft Project来管理嵌入式系统开发项目的进度

客户在委托开发项目时当然会说明期望的结项时间，或者产品上市的时间，项目经理就必须根据这个时间限制，规划出合理的执行计划，如图3-2所示，项目经理可以使用软件辅助管理项目进度。因为“软件开发总是会delay”这个印象已经深植在客户的脑袋里，所以客户通常都会自己暗中保留一段缓冲时间[4]，规定进度也会说的比较模糊。例如：年底圣诞节前要上市（其实农历年前才要开始卖）、学生放完暑假，开学之前一定要结项（这就可能真的要抢圣诞节的商机）等。笔者也当过客户的角色，深知压缩厂商开发时间的各种技巧，因此绝对不能轻易让对方知道我们设定的时间底线，每件事情都只能说ASAP（As soon as possible）。

但是负责开发的项目经理在管理时间时就不能有模糊，所有关于时间的事项都必须精确。客户可以说ASAP，但是负责开发的人员绝对不能用“我尽快就是了！”这样的态度来评估任何一件事情所需的时间。一个大任务的进度估不出来，就必须把它切分为许多小任务；一件事情牵涉到多个单位，就必须把相关人员找来面对面讨论。如果制定出一个自己看了都会心虚的进度计划，想要让项目顺利结项，恐怕只能祈求老天保佑了。

项目管理并非赌博游戏，不可能凭借运气就可过关。所以制定Schedule绝对是一个良心的工作，一个不合理的Schedule对公司及客户而言都是种欺瞒，对参与的工程师更是一种压榨。再次强调，嵌入式系统开发项目是一种商业行为，严格要求效率与获利，关于制定合理的Schedule这件事，工程师要提供正确的评估结果，项目经理要用心协调与整理各方的信息，绝对不能等闲视之。

03-04　规格规划：请接受这个不完美的世界

PM：“客户提供的产品规格书你读过了吗？有没有什么看法？”

菜鸟:“那个两三页的文件就是规格书吗？我以为像这样的国际大厂至少应该会把规格书弄得像样一点，这样不清不楚的规格叫我们怎么评估啊？”

PM：“你必须习惯现实和教科书里往往是不同的。就如同Schedule一样，客户总是说ASAP，而关于规格，客户也通常倾向As good as possible，希望开发团队尽可能地做出造价最便宜，且功能领先所有市面竞争者的产品。因此，客户在规格方面也会自己留下缓冲空间，在刚开始谈规格时偶尔会模糊其词，如果我们可以做到客户的高标准当然最好，假使不行的话，只要没有低于客户藏在心里的底限，其实都还是会被接受的。表面上是客户做了退让，实际上还是客户掌握了大部分的谈判筹码，谁叫我们公司要赚人家的钱呢！”

菜鸟：“那反过来想，我们也可以把我们已经具备的技术，故意和客户说这个困难度很高，需要更长的时间和更多的人力以及开发费啊！这样就不会被客户吃得死死了。嘿嘿，我很聪明吧!”

PM：“嗯，有天分，的确有很多项目的规格是在各方妥协的状况下谈出来的。但更多的状况是客户会先拟定一个产品规格，先把大方向定下来，项目开发人员据此进行规划。在进入开发阶段后，才用市场营销部门的要求当借口（如果不改就卖不出去！），并陆陆续续要求小部分改动规格。以项目管理的术语来说，就叫做‘范围蔓延’（Scope Creep），一旦规格改变，相关的成本、Schedule与质量计划都会受到影响。”

菜鸟：“但随意更动规格不是开发的大忌吗？且市场调查应该在项目开发前就完成的啊！”

PM：“改动规格在所难免，通常开发单位都会尽力配合，但制定规格的人通常并不具备深厚的技术背景，有时他认为只是小小的改动，但对工程师来说，可能必须大费周章更改架构才做得到。而此时工程师的解释又会被认为是推诿之词，于是规格制定者与技术人员两个本该合作无间的单位，难免会为了规格更动这类事情搞得不对盘。

例如，我们之前做过的某项产品具备MP3播放的功能，为了成本考虑，设计时选了没有录音功能的芯片（因为有录音功能的MP3芯片必须额外付版权给MP3组织），音质也在可以接受的程度上。当做完一次试产，也就是说硬件、驱动程序和系统架构都已经完成大部分功能，此时客户才说没有录音功能的产品卖不出去，要求我们把录音功能加上去，而且进度不能延后。这句话说起来很简单，或者在设计阶段之前说也OK，但他们就是不能理解为什么仅仅更换芯片、改架构，对于开发单位会像是天要塌下来一样。”

菜鸟：“结果呢？这个项目还做得下去吗？”

PM：“这是个大项目，咬紧牙根都要做下去。虽然‘顾客是上帝’，但我总相信客户是可以教育的，此时项目经理负责协调的角色就相当重要，他不能像工程师一样情绪化的反弹，他必须耐心的向客户解释规格更动所带来的影响，并积极争取更多的时间与资源。且前面提过，因为我们不要不合理的Schedule，当规格更动后，没有理由可以在相同人力与资源的情况下，还可以按照原来的时间完成。所以邀请客户列席，重新制定合理的Schedule也是重大危机处理的方法之一。”

笔者一再强调：只要卖得出去的产品就是好产品。嵌入式系统开发必须秉持一种叫做“刚刚好就好”的理念，成本刚刚好、资源刚刚好、进度刚刚好、技术刚刚好、规格也只要刚刚好就好。一味追求高深而不实用的技术层次，不见得会受到消费者的青睐，如果为了完成一个华而不实的功能，反而延误了最佳的上市时机，岂非得不偿失！

不仅市场营销与制定规格的人，项目管理者甚至工程师都必须体会这样的思想。开发者当然可以在系统上加上任何可行的功能，一旦客户或消费者不接受，也只是浪费人力，无谓的延长开发时间[5]而已。消费性电子产品或嵌入式系统开发绝对是一种商业行为，只要能如期完成市场接受的产品就算胜利，即代表该项目成功了，一般人根本不会、也不想了解技术的细节。

一般工程师都抗拒不了新技术的诱惑，但只要想想以8051为核心的8 bit CPU仍是目前使用量最大、应用范围最广的，应该就可以有所领悟。所以无论是制定产品规格，或实现某个软件模块的功能时，请尝试接受这个不完美的世界，最新的东西不见得是最好的，而标榜使用高技术产品却惨遭市场淘汰的例子，也是屡见不鲜。

此外，笔者要在此说句题外话。业界评估人才等级的标准是以学习的速度与解决问题的能力为主。当然，技术能力可以帮助提升解决问题的能力，但并非绝对，特别是技术更新如此快速的现今世界。而且业界和学界大不相同，一切都是以任务为导向，只有完成任务才算是成功，只有具备解决问题能力的人才能往上爬，所以千万不要看不起什么技术都不懂的老板，想想为什么我们现在只能当他的员工，想想为什么大老板愿意给他比我们多更多的年终分红！

3-4-1　妥协的艺术：进度vs规格

菜鸟：“万一客户坚持不妥协怎么办？假设市场营销的人认为这项产品不具备某个规格，就会被竞争厂商压着打；又或者如果在今年的圣诞节无法上市，到了明年就会失去竞争力……”

PM：“你知道质能守恒定律吗？简单地说，这样多的质量就只能转换产生那样多的能量，这是亘古不变的真理。如果真的不可能如期达成，我们有义务及早通知大老板以及客户。”

菜鸟：“我知道您的意思，但万一老板的回答就是您常说的那句话——死守！那怎么办？”

PM：“呵呵，你的语气学得真像，但我每次说‘死守’某个时间点其实都是有根据的，基本上都是合理的，且做过人力调派的，需要的只是执行者多一点的用心与努力。话说回来，针对客户与老板提出的不合理要求，首先我们必须会同各单位，再重新仔细审视Schedule一次。若仍然没有调整的空间，此时无可选择只能据理力争，既然我们已经用严谨的态度制定了合理的Schedule与资源调配，那么就不该有很多可供压缩的时间，否则只是证明当初的规划有问题。”

菜鸟：“请老板多调派点人力加入这个项目可以吗？”

PM：“像我们人力这么缺乏的研发团队，老板愿意加派人手我简直感激涕零，对新规格所造成的影响肯定有某种程度的帮助。但‘人多好办事’这句话不一定适用于每个状况，你该去读读《人月神话》这本书，书中提到一个重点：当参与项目的人力超过某个临界点后，项目的复杂度会随着人员的加入而增加，人员越后期加入，复杂度增加的幅度越高。软件项目开发和盖金字塔大不相同，并非人多就可以解决一切，而且公司内每个团队都喊缺人，老板哪来多余的人力可以加派到我这里来？”

菜鸟：“加派人手也行不通，那，到底该如何解决这个难题？”

PM：“还是那句老话——质能守恒。诚恳的和客户沟通，一定要清楚表达即便我方愿意多付出一些努力，并已经再次压缩进度，也不可能在既定进度达成的原因。之前不是和你提过吗？不论是进度或规格，客户一定会保留一点缓冲，弄到两败俱伤，产品还是出不来，对客户自己的利益一点好处也没有。通常经过互相妥协，客户都会让步的。如果客户坚持不合理的要求，而且老板也坚持要承接一个注定要失败的项目，那我们只能相信老板的判断，勉力为之，不然就只能离开这个工作岗位，但无论如何都尽量不要影响到团队的士气，这是基本的职业道德。”

03-05　人力规划：营级组织、连级人力

菜鸟：“怎么你今天灰头土脸的，好像心情不太好？”

PM：“去年拼死拼活把一个大项目如期完成，现在被质量系统单位质疑说我身为项目经理，却放纵各部门主管，带头不认真执行质量系统中的各项流程。这些人讲得头头是道，但完全不晓得执行者的辛苦！说到软件工程、质量系统，我懂得比他们还要多，居然还轮到他们来教训我！”

菜鸟：“长官请息怒，我还是没听懂这是怎么一回事，什么是质量系统啊？”

PM：“国际间有些组织制定一些用来规范软件开发的标准，如IS09001，以及最近很红的CMMI（Capability Maturity Model Integration，信息软件质量成熟度验证）都是。一般稍具规模的软件公司都会想要取得这些认证，以证明该公司是采取正确的方法在开发软件，且其质量都在控制中。

当然这些规定都是立意良善的，认真照着做必定可以提升软件质量，但前提是研发团队必须打从心里愿意执行。可是在我看过的公司中，通常都仅是雷声大、雨点小，刚开始弄得沸沸扬扬，等到事情开始忙碌的时候，最常看到的状况就是团队中被分派去做质量系统的人员，拿着‘编’好的文件或会议记录请主管或工程师签名。等到要稽核的前几天，所有人才开始临时抱佛脚地补齐文件，背诵条文。最令人百思不解的是，稽核人员明知大部分文件都是造假而来，在稽核时也列举了一些缺失，但最后都可以‘低空飞过，并拿到证照’。”

菜鸟：“这样好像不好吧！为什么不干脆按部就班做呢？”

PM：“我想没有任何主管愿意造假吧！上行下效，主管带头造假应付，会为管理工作带来很多麻烦。你知道我有多希望每一件项目在执行时，都能留下所有开发过程的文件吗？除了质量管理的目的之外，这对团队经验传承，项目结项后的检讨都有莫大的帮助。我问你，明知是有帮助的事情，明知是公司的政策，却无法配合执行的原因是什么？”

菜鸟：“嗯～是因为进度和人力吗？”

PM：“是啊，在进度和人力的双重限制下，有时真的无法完全按照质量系统的要求来做，但为了控制项目质量，此时也只能按照质量系统的精神，选择性地做。例如，因为设计文件后补就失去意义，所以必不可缺。此外，重要决策的会议记录一定要留存等。我一直对老板说这是现实，并非借口，虽然老板也可体谅，但拿到证照又是公司的既定政策，所以老板也只能默许每到稽核时期整个公司就兵荒马乱的现象。

每件项目在开始前，我都会提醒自己：这次一定要把质量系统run起来。可是当我把助理、勤杂工，甚至我自己都排入测试人员时，我就知道在这家公司恐怕是无法执行质量系统了。但当我换了几家公司，做了更多的项目，却发现每家公司状况都差不多。主因是每位老板都是极为精打细算，他绝对不会给你太多的人力，却期望你做更多的事，虽然对老板来说，每件事情都很重要，但执行者却不得不把某些事情的优先级降低。”

上一节已经提到“人多好办事”并不适用于所有状况，但人力不足也万万不行。特别是嵌入式系统的开发，牵涉到的单位非常广泛，许多人力并不会一直挂在这个项目上，也有可能发生某个人力会同时负责多个项目的工作。举例来说，经过几次试产过后，硬件和结构已经逐渐稳定，负责硬件和结构的员工可能就会被指派到其他的工作，一旦前一个项目发生问题，他们的工作就难免会重叠，抢人大战在同时开发多件项目的公司里更是屡见不鲜。

要求工程师在几个工作间切换是很没效率的事。对于项目的人力配置，最好的状况当然是每一件工作都有位固定的负责人，从项目开始到结束，只专心在这个项目上。此外，最好还有预备人力可以调配。但实际上，老板一般并不会这么慷慨，编制不全反而才是一般开发团队的状况，几乎每一位项目经理都在喊缺人。面对排山倒海的工作项目，把某些优先级较低的工作排出Schedule之外，也是不难想象的事情。

有时人力资源管理比技术开发还要麻烦，除了人力多寡的问题外，每一位项目成员都有各自的个性与脾气，不同公司的人也会有不同的文化，要整合来自不同领域的人共同完成一项电子产品开发，确实不是件容易的事情。但这不是本书的主题，在此笔者还是推荐可以参考《人月神话：软件项目管理之道》一书。

03-06　成本规划：一切都是为了Cost Down

菜鸟：“您交代我去研究前一件项目的文件，有个地方我一直百思不得其解。同样的硬件架构要做不同外型、一大一小的两项产品（如图3-3所示），小台的因为空间太小，放不下所有零件，所以必须用两块板子叠起来，它是使用一般封装的CPU；而大台的空间明明还很充足，为什么反而使用Die（裸片，未经封装的芯片）？结果弄得两台机种的信号都不好测量[6]，这在许多会议记录中都看得到工程师的抱怨。而这些和我原先的想法完全背道而驰，制造使用裸片的产品时，工序不是应该比较麻烦吗？而且不良率也比较高，应该都是空间不足时才会考虑使用吧！怎么我们小台不采用，反而是大台采用？”

▲ 图3-3　同一型号CPU的不同封装（裸片与一般包装）

PM：“这是一个好问题，你看过这两项产品的BOM表[7]（如图3-47所示）及我们公司给客户的报价单吗？”

菜鸟：“要看这些文件喔？我以为和我这个小小工程师扯不上关系。”

PM：“再给你一个作业，你自己去找到你问题的答案，可以找其他同事讨论，有头绪后再向我报告。”

▲ 图3-4　BOM表实例

菜鸟：“报告长官，我知道答案了！”

PM：“不错嘛，才花半天的时间就找到答案了，你说说看，为什么前一件项目的两项产品要如此设计硬件？”

菜鸟：“答案其实是一个学长告诉我的，但我也去翻阅了更多的资料与记录。原来最初的硬件设计中，大小两台机型的CPU都是使用一般封装类型，后来因为大台机器的功能和结构都比较复杂，所以我们从BOM表计算出的造价和客户的Target Price相差太多，于是客户要求在外观和功能都不能打折扣的前提下设法Cost Down。于是，把CPU换成Die可以省下一块多美金，SDRAM换成二线厂商、LCD改用台制、使用容量较小的存储器等，为了执行Cost Down，到最后就变成这副德行了。”

PM：“说的很好，但你要确实了解其中道理。和一般软件开发项目不一样，嵌入式系统开发的目的通常是为了完成某项电子产品，而规划产品时必然会制定出一项市场价格，再根据这个市场价格，减去各单位合理的利润，就可以算出可接受的造价。也就是说，如果做不到这个目标，那这项产品就是不会赚钱的东西。在这个例子中，目标的造价会直接影响到软硬件的架构。在嵌入式系统的领域中，我们会为了要降低产品制造成本去做任何必要的事情。

以上述例子来说，你以为只有硬件架构受影响吗？事实上，换了SDRAM后，系统时序设定大受影响，整个系统变得极不稳定，我们还额外花了许多时间去调整。因为台制LCD的质量没有日系产品的清晰，我们固件工程师翻遍了LCD Controller的data sheet，甚至在某段时间内还得天天到LCD供应商那里上班。更惨的是存储器容量变小，同样大小的数据要如何才能塞进较小的空间？为了要找到压缩率和速度都可以接受的算法，简直把我们最优秀的工程师给操翻了。”

菜鸟：“是喔！我倒没想到这么多。我们这么辛苦，就为了每台机器省下五块美金，这样值得吗？”

PM：“我一再提到项目开发是一个商业行为，特别是嵌入式系统。因为消费性电子产品最贴近消费者，而且比较没有盗版的问题，如果是件优秀产品的话，销售全世界的数量会是非常可观的。你思考一下，一台机器省下五块美金，一万台机器省下多少？一百万台机器又省下多少？如果像Sony PlayStation 2或iPhone这种在全球热销几千万甚至上亿台的产品，又可以省下多少？

身为嵌入式系统开发者，万万不能不具成本的观念。开发出一项赚不到钱的产品或一个没人要的产品，对公司的利益同样没有帮助，我们又不是在经营慈善事业！请务必把这个观念铭记在心，了解吗？”

菜鸟：“报告长官，知道了！”

03-07　质量策略规划[8]

PM：“嗨，菜鸟，测试主管对你报告的产品规格颇有好评，你帮他们把重点都highlight出来了，干得不错！接下来你再和QA[9]部门约一下时间，不用额外准备什么，就报告产品规格即可。”

菜鸟：“啥？QA部门现在就要参与了？”

PM：“当然啊！并非只有程序才要求质量，我们现在做的设计工作都应该受到质量系统的规范。记住！只有高质量的设计，才可能做出高质量的产品，这是我在项目管理课程中一再强调的。产品质量是设计出来的，这个思想你应该已经了解了吧？”

菜鸟：“嗯，可是你不是说过公司之前已经引进一套质量系统了，我们去执行不就好了，为什么要在这个大家都忙着设计的时间点去和质量部门扯上关系？而且你不是一直说自有一套符合项目现实状况的质量系统吗？”

PM：“我总是希望负责我们这件项目的QA与QC人员在一开始就参与，他们应该要尽早了解项目的范围、日程及相关限制等，然后再根据他们的专业建议，QA要尽快把本项目的质量规划书制定出来，而QC则必须根据质量策略与产品规格制定测试计划。最糟糕的状况是质量单位拖延到项目后期才加入，然后不明就里地就把国际质量标准拿出来要求研发团队照做，这并不符合现实，只会为项目带来灾难，毕竟每一件项目都有自己的特性。”

菜鸟：“质量规划书是什么？按照你的说法，每一件项目都会有自己的‘质量政策’啰？”

PM：“虽然我不是搞质量系统出身的，但我身为这个项目的负责人，当然要有质量优先的思想。但质量计划必须是可执行的，否则就如同空中楼阁，流于空谈，到最后一定是不了了之，徒然浪费了人力，比没有执行质量计划更糟糕，简单的说，质量规划就是要定义出哪些质量标准适合本项目，并确定如何满足这些标准的要求。质量规划是设计阶段的关键过程之一，因此，应与其他设计规划项目彼此结合进行。

例如，为了达到QA位要求的质量标准，可能必须要求对进度或成本进行调整，也可能需要针对某项待确认的问题，再做更详细的风险评估。简单的说，不能要求马儿好、又要马儿不吃草，一旦经评估，在目前的状况下无法达到规定的质量水平，要不就是设法增加资源、要不是修改进度或改变规格，不然，就只能改变质量规范。项目管理有一个关键重点，就是要尽量去除不确定性，如果一开始就知道质量规划书的规范不可行，那它就只是废纸一张，并使得这个项目形同不具质量计划，天知道会做出什么样的东西。”

菜鸟:“我觉得你说得很有道理，那怎么还是会发生稽核质量系统时还要被修理的状况？（请参见《第3-5节：人力规划：营级组织、连级人力》）”

PM：“现实情况是QA的人好像总是很忙，他们往往觉得既然已经有CMMI这样的国际标准，所以针对各个项目进行质量规划是多此一举且毫无意义的！然而这是一个似是而非的讲法，现代质量管理的一个基本准则：质量是规划、设计出来的，不是检查出来的。我觉得很多QA的人，对质量系统的认识往往还没有我来得正确。所以假使QA部门不打算现在开始做质量规划，我也只能自己把质量规划书做出来，绝对不能允许我的项目在没有质量规划下进行！”

菜鸟：“那你明知他们不会现在参与，还叫我去跟他们报告？”

PM：“勉力为之，无愧己心！放心啦，项目质量一定要放第一位，搞不好他们突然‘有空’了！反正最后是我面对质量稽核，到时难过几天就是了。”

菜鸟：“这件事情表面看似是PM不遵守质量系统，但其实PM才是真正推动‘可执行质量系统，的人，真是矛盾！老大，您真的是用心良苦啊！但说了那么多，我还是不知道这个项目的质量政策是怎样的一个东西，那是一份文件吗？”

PM：“项目的质量政策不必是一本厚厚的文件，这只会让项目成员望之生惧。你会发现执行项目有一个可遵循的基本逻辑——不论是范围、进度、成本到管理，我们都会先制定基准（Baseline），简单地说就是底线，也就是无论如何都必须达成的标准[10]。项目经理必须将这些基准推广到所有项目成员，并成为此项目的管理准则。这些Baseline必须简单明了，也可暂时忽略项目初期的某些不确定性，等所有项目成员对此达成共识后，才可将其逐渐细化。”

举例来说，再有经验的人，都无法在项目初期就分析出优化的WBS（Work Breakdown Structure，请参考《第2-4节：项目范围（Scope）管理》），一定要先确认项目Scope等信息，才有办法进行工作细分。所以WBS会是树状结构，越靠近root的部分就是基准，越不会有争议。

质量政策也是一样，在项目初期我们只会根据项目特性（范围大小、进度紧迫性、可调用的人力、客户对质量的要求等）制定出品质管理基准，让所有项目成员都知道我们必须将这个产品做到什么样的水平，以及为了达成这个目标，我们必须应该具备何种工作模式。

以下是一个已经结项的项目的Quality Baseline文件，提供给你参考。”

XXX Project Quality Baseline (ver.0.9 20100315)

质量需求描述：

本项目的客户为国际大厂XXX委托的电子产品开发项目，客户对质量水平要求相当严格，认为严谨的设计阶段至为重要，客户甚至可能会参与本公司内部的技术审查会议。此外，Design & Implementation的一致性也是客户关心的重点。

此项目虽然与本公司之前某项目性质类似，但前项目执行人员已离职，而且新项目扩展了不少功能，原系统架构是否具此扩展性为先期设计重点，对此，质量人员会积极介入review。

本项目执行期间为4个月，详细进度可详见“XXX project plan.mpp”。期间本公司同时有多个项目在执行，各Team Leader应及早规划人力安排，避免非专长派任而导致质量问题。

……

固定会议：

•　每周技术小组进度会议（each team member report to team leader）

•　每周项目进度review会议（each team leader report to PM & QA）

•　每双周对客户报告进度（PM report to customer）

•　Bug讨论会议

•　版本发行会议

•　……

实作前需审查通过的文件：

•　产品功能规格书

•　硬件规格书

•　开发计划表（by Microsoft Project）

•　测试计划书& Testing Cases

•　系统架构

•　……

应造出的文件种类：

•　硬件设计文件

•　各模块设计文件

•　各送测版本的测试报告

变更控制：

•　任何项目的规格与时间变更，必须经过以下人员组成之变更管理委员会通过才可施行。当变更通过后，必须先通告所有项目成员，并告知通过变更的原因，以及对项目会造成的影响。

•　客户代表

•　项目经理

•　QA

•　相关技术人员、Team Leader及其主管

测试等级：

•　进测标准：除非RD特别强调某功能仍有缺失，否则，当新版本测试时，若测试人员发现有两个以上的主功能不Work，则可直接退货，停止测试。

•　测试方式：RD指定版本控制Server上的label，由测试人员负责自Server上获取程序代码并编译、下载与测试。

•　V0.1或20100615后，造出“每周构建报告”

•　版本pass标准

•　连续3个版本呈现bug收敛状态

•　不能有Critical & Serious等级的bug

QA介入标准：

•　Code Review——新建模块、bug较多的模块、在设计审查阶段决定必须执行Code Review的模块

•　bug收敛状况不正常

•　bug reopen次数太高

•　既定milestone delay

Coding Style：

•　请遵循公司内部文件“B-Company coding style标准.doc”

•　Software Team Leader必须随时review员工的Code，尤其是全新开发与较为核心的模块。

•　QA不定时抽检Coding Style，发现异状仅通知Software Team Leader，并追踪改善状况。

项目配置控管（Configuration Management）工具：

•　进度管理：Microsoft Project

•　文件管理：Wiki Server，会议记录集中到指定的Gmail account

•　软件版本控制：SVN

•　Bug & Issue tracking: bugzilla

项目质量相关联络人：

•　QA：John

•　QC/testing team： Tom

•　客户品保窗口：Mary@A-compamy.com

PM：“这种一页式的质量基准是简单明了的，工程人员实在没有理由再推托没时间去了解质量系统了。先定Baseline，再制定细则，确实是我们这种研发团队推行质量系统的不二法门。我相信工程人员这样执行几个项目后，质量观念会深植在他们的做事方法中。”

一般软件开发团队会采用的质量管理系统为IS09000与CMMI这两种国际标准，这些标准立意良善且有理论依据，但业界的实际情形却是：（1）并非每个项目的Schedule与资源配置都能够让项目成员按照这些标准来执行；（2）绝大部分公司认为只要能够通过稽核，取得证照就好，以后接项目会比较容易。所以往往在项目团队对外时，还会有一组成员专门负责用以应付稽核所需要的文件。

本书对质量系统的中心思想为：若硬着头皮让团队成员去执行这些国际标准，到最后还是会因为资源与时间的关系而窒碍难行，而必须阳奉阴违，这样对项目的质量一点帮助也没有！与其如此，倒不如要求开发团队必须能掌握这些标准的精神，并根据项目特性，在规划阶段就制定好该项目的质量策略。一旦质量策略确定，就必须在整个项目中贯彻执行。

ISO9000在业界风行了一阵子，现在通过ISO9000已经不具任何意义了（只要企业肯出人、出钱，顾问公司一定可以把相关员工通过‘培训’通过稽核）。所以本章节只对CMMI作个简单的介绍。

CMMI（信息软件质量成熟度验证）是卡内基美隆大学软件工程学院（SEI）发展成的标准。CMMI以“个别领域为主的能力等级（Capability Level）”与“相关流程群组综合的成熟度等级（Maturity Level）”来判定软件厂商（或团队）的等级。CMMI提供了包含5个软件开发过程、能力成熟度的框架，定义一个用来测量软件开发过程成熟度，以及评价软件开发过程能力的尺度，如图3-57所示。CMMI的5个成熟度等级分别为：

■　初始（Initial）/无法预测且监控不佳：软件开发过程是无秩序的，偶尔甚至是混乱的。软件开发有时是成功的，有时是不成功的。几乎没有什么过程是经过定义的，成功通常是依赖于个人的努力。

■　管理（Managed）/可重复先前的成功经验：已建立基本的项目管理程序去追踪成本、效益、进度和功能性；必要的开发过程已稍具纪律，此后有类似的项目可重复使用以前的成功经验。

■　定义（Defined）/描述流程特性，并用于管理：项目管理活动和软件开发活动的过程均已文件化及标准化。

■　量化管理（Quantitatively Managed）/流程可被度量、控制：已收集软件开发过程与产品质量的相关信息，并加以量化、记录与分析。

■　优化（Optimizing）/专注在流程改善：利用组织中的历史数据，辅以新的技术与观念，使持续的改善成为可能。

▲ 图3-5　CMM的5个成熟度等级

质量规划是一门专门的学问，但并非本书的重点，在此仅列出执行质量规划时必要的信息、可使用的工具、会产生的质量计划文件（质量管理计划、流程改善计划、质量标准、质量核对清单），最终形成本项目的质量基准，并纳入项目管理计划中。请参考图3-6，可以具备较清晰的概念。

▲ 图3-6　质量规划流程图

[1]　软件工程中有所谓“雏型迭代”的开发模式，意指当项目初期无法明确定义产品规格，只能通过不断改善prototype，以逐步确认最终产品规格的开发方式。这和本例中通过先行开发prototype以确认规格可行性，是两种截然不同的概念。

[2]　ES（Engineer Sample）：一般芯片在还没正式量产之前，会先提供一个算是稳定的版本给打算使用的客户做先期开发。

[3]　即便有各种技巧可以减少模块间的相关程度，但现代的软件应用系统复杂度真的很高，往往工程师为了尽快解决某些问题，难免会破坏开始制定系统的架构。管理者很难杜绝这样的事情。

[4]　在项目管理教科书中，也建议项目管理者在制定Schedule时应该要为整个项目预留Buffer（请注意，并非单为某些单一工作项目多留一些时间）。

[5]　请注意，系统设计时必须考虑到将来的扩展性，这绝对是有意义的；这和笔者在本章想强调“刚刚好就好”的规格是不同的两件事情。

[6]　两块板子叠起来时，连接面很难测量；而使用裸片的芯片，如果某引脚没有设计测点，那几乎会无法测量该引脚的信号。

[7]　BOM表是物料清单（Bill Of Material）的缩写，也就是产品的物料构成清单，反映了产品由原材料到半成品、再到成品的加工装配过程，是公司计划、采购、生产、商务、成本核算及技术管理的重要基础数据。物料清单是否准确、是否实时齐备，都会直接影响到市场、商务、计划、生产等相关业务的运作。

[8]　本章节在第一版中是放在第二章（设计阶段中），实际上，质量策略越早确定越好，其它的工作才可有所依循；故第二版将“质量策略规划”提前到规划阶段说明。

[9]　QA（质量保证部门）——按照定义，QA部门必须从项目初始就介入，确认项目的执行过程是否根据既定的质量政策执行。例如，确认团队是否取得完整规格后才开始设计工作、设计工作是否接留下文件、Implementation是否都根据设计文件执行、测试计划的完整度等。所以QA和QC（主要工作之一为执行测试工作）的工作性质完全不同，但时下很多公司对QA、QC完全混淆，最常见的就是完全忽略QA该做的工作，或把测试工作称作QA。

[10]　很少有项目是能在一开始就能看清楚整个全貌，也没有人规定说一定要全都看清楚才能开始进行项目，但在项目开始前，必须要先将基准（Baseline）建立出来，Baseline一旦制定出来，在项目执行时，除非通过项目的关键人员（Key Stakeholder）等同意，才能做变更基准的动作。

Chapter 4

嵌入式系统开发项目生命周期：设计、执行与结项

时间：04月01日　 登场人物：菜鸟、项目经理（PM）

前一章我们谈到项目前期规划该考虑的事情，当进度、规格、人力及成本都确定没问题后，才可以决定是否接下这个开发项目。接下来，老板或sales会和客户商谈业务或合约的问题，通常不会等到签约完毕，客户就会正式kick off项目开发工作，此时，项目经理就会开始召集项目成员，并开始设计工作。通常项目成员不会一次全部加入，刚开始加入的，一定是执行设计工作的核心成员，在设计工作完成前后，负责实作的工程师与支持人员会陆续加入，当开发工作逐渐完成时，测试人员就会加入。

本章重点将放在嵌入式系统项目生命周期的后半阶段——设计、执行与结项。

04-01　动手之前：确定项目的执行原则

当项目经理与部分干部完成了规划阶段的工作后，在真正开始动手进入设计阶段前，因为已经需要技术人员专心投入此项目，我们就会发现组织架构上的矛盾。项目是临时性、一次性的工作，在编制上，项目经理是没有兵的将军（常见的公司组织架构图，请参阅《导读》中的图）。他必须由各部门召集合适的员工参加此项目，而这些员工在公司的编制上却是隶属于软件部门、硬件部门、结构设计、生产管理部门、QC、QA、supporting team（美术设计人员等）等常设部门，而这些部门自然有其既定目标，这件项目需要的工程师不一定能如PM所愿地加入此项目。

所以嵌入式系统开发项目在开始进入设计阶段之前，项目经理必须先敲定人的事情，这件事情的关键取决于公司对此项目的重视程度（或说为了这个项目，组织可以赋予项目经理的权力范围），以及各部门经理的态度。此时项目经理又得发挥在项目管理中，必须具备的最重要的能力——沟通。

菜鸟：“感谢先前花费了大量时间教导我们项目管理的概念，让我确实获益良多，但这个项目即将进入设计阶段，看来软件相关的工作也不少。以软件工程师的角度来看，我们是否也应该要遵循软件工程的规范？我记得以前在学校修软件工程时，有一堆瀑布式开发、模块化、耦合度、软件质量保证（SQA）等名词，这些软件工程的指导原则在嵌入式系统项目中该如何执行？与项目管理的知识体系会有冲突吗？”

PM：“以前公司高层对此也很困扰，他们知道要以项目来管理每个开发项目（无论内部或外部项目），也‘听说’软件开发必须遵循软件工程。刚好有一天总管开发的大老板——技术主管突然灵光乍现，想是不是应该要找个项目来试run看看，但他一时也不知道该找负责项目的PM，还是负责软件团队发展的软件部门经理来主导这件事情，于是就找了一堆人来开个大锅炒的会。当时我还是菜鸟项目经理，你的老板（软件部门资深经理，以下称软件经理）给我们扎扎实实地上了一课。这一切都要由两年前的一个会议说起……”

4-1-1　说故事时间：从Internal Project Kick-Off Meeting谈起

时间：2008年07月11日　 地点：大会议室

会议名称：

Internal Project—“Porting Android to MK1234 platform” kick-off meeting

会议背景说明：

本公司使用MK1234为主控IC，为智能手机市场设计了不少产品，并已于2008年初陆续上市。系统软件是基于一个稳定的RTOS（如Nucleus），配合本公司自行开发的driver、middleware与手机应用程序。

会议目的：

由于本公司智能手机产品线销售遇到瓶颈，客户纷纷要求提供更高端的智能手机功能，例如：多点触控、开放式平台、软件市场（如Apple的AppStore）等。根据客户访问的结果统计，客户普遍希望本公司的手机产品可支持Android Solution，经Marketing部门深入调查分析，高层决定启动Android的移植计划。此会议邀请技术主管与各研发团队主管参加，希望可以对执行此计划时的指导原则达成共识。

与会人员：

技术主管、产品研发处处长（软件与测试经理的直属老板）、软件团队资深经理、测试团队主管、内部计划的项目经理。

会议进行状况：

虽已明确会议目的是讨论项目的执行原则，技术主管却喜爱针对某些技术细节深入追问，似乎想显示其并未与技术脱钩，但却不断让会议内容失去重点。于是此会议在技术主管‘苦苦追问’，底下经理们东扶西倒、查阅文件、请员工支持，研发处长帮忙缓颊的循环中进行。所有人都卡在技术细节里，原本应该设法确定方向的高级主管们虚耗了一个下午，最后终于技术主管也累了，总算提出他对此项目的期望……

技术主管：“我们已经讨论了许多技术与人力的issue，结论是我们的硬件平台已通过市场的洗礼，绝对是稳定的，所以应该只有软件的effort。按照你们的说法，这个内部项目的主要工作为：

1．就是把Linux Kernel及Android Framework移植到我们的硬件平台。

2．写好相关硬件装置的Linux driver。

3．将本公司在手机应用上，既有的middleware与AP移植到Android上。

4．最后，做必要的benchmarking与性能调试。

我尊重你们在软件开发上的专业，虽然我本身是硬件专业出身，但对这个项目仍抱有以下的期望，希望你们能够配合……”

软件经理：“折腾了一个下午，终于可以进入主题了……”

技术主管：“我对这个内部项目的3大期望是：

1．希望每个module都要具备设计文件，包含Linux与Android Framework中的所有模块。

2．希望这件项目要严格执行软件工程。

3．要在这件项目里推行QA。

你们谁可以take这些action items?”

顿时一阵静默，一来大家都累了，再来本公司虽执行嵌入式系统开发项目许久，但已经很久没听到这样’伟大’且‘形而上’的名词了，一时所有人不知该如何反应，也不知道技术主管是从工商时报还是商业周刊看来的这些term？

技术主管：“大家都没意见吗？我看先这样吧。

■　软件经理在执行项目时，应严格遵守先有设计文件，通过review后才能implement的原则。此后这个项目每次的技术文件review meeting都要CC给我，我会尽量参加。

■　研发处长应该尽快把执行软件工程的SOP订定出来，下星期同一时间召开会议，我希望可以看到第一个版本。

■　至于QA，就请测试团队主管拟定策略，同样下周报告。

以上有问题吗？”

软件经理（心中OS：不愧是技术主管，果然条理清晰，讲什么都是1234的，不如我也来个abc）：报告长官，我们拟定好的计划是希望以下两个项目能够同步进行。

a．移植Linux Kernel & Android Framework到本公司的MK1234硬件平台。

b．移植手机应用必要的driver、middleware & AP到市售的Android平台。

其中第二项我们当然会做好架构与每个模块设计工作，至于第一项我们决定暂时不去细究Linux Kernel与Android中每个模块的工作原理，毕竟Linux与Android都已算是稳定的系统。我们计划先让Android在我们的硬件平台跑起来，再根据benchmark得到的数据与实际执行的性能去拟定改善计划。

也就是说，我们可能无法完全按照您上面所指示的去执行，在项目初期并不会造出Linux与Android每个模块的‘设计文件’，因为这些对‘移植工作’并没有帮助。

此外，我们也无法如您所说，等到这些无意义的‘设计文件’出来后，才开始展开‘移植工作’。事实上，我们员工已经开始做Android的移植工作了。”

研发处长细声叹息：“这家伙还有体力去讲这些，有些事只能做不能说，怎么教都教不会，等着被老板‘枪毙’吧！”

技术主管：“我完全不同意您的说法，我无法想象软件开发可以没有设计文件就开始执行（Implementation）。我强烈怀疑你的专业判断，这个项目交给你我很担心！”

软件经理心里暗干：“公司不是正在推广对上级长官的强力对话吗？怎么动不动就把问题扯到主管的胜任性？他真的不知道本公司之前的软件是如何开发出来的吗？他真的不知道我的团队就那么几只小猫吗？

而且软件工程并非一堆硬梆梆的教条集合，porting和create something的确是不同的软件开发行为，不是把软件工程教科书里的东西拿出来硬干就可以的！该怎么‘不那么强烈’的跟老板说明这个道理呢？”

研发处长：“请老板不用担心，我们内部会设法取得共识，也会如期达成您指示的目标。此外，我们也会拟订出在本项目推行软件工程与QA的计划，对吧，测试经理？”

测试经理：“当然！当然！”（心中OS: QA的Q应该是Quality，A是指啥东东啊？回去赶快Google一下。）

技术主管：“即便我们有很好的硬件平台，但如果软件开发总是跟不上也不是办法，大家好自为之，散会！”

会议终于结束了。技术主管离席，其他人瘫软在椅子上。

软件经理无力呢喃：“唉～为什么老板就不愿好好听听软件人员的专业建议呢？”

研发处长：“老板要设计文件你给他就是了，Linux有那么多Manual与Howto，Android的技术文件也眼花缭乱，其中OpenCore的文件就多达400多页，用不着跟他争嘛。”

测试主管：“我们Team一直都只做测试工作，最近还是由软件经理导入QC的概念，现在已经会在开发初期就开始做测试计划与测试案例，但我对QA要做什么一窍不通，原本我们是Testing Team，现在你们硬要叫我们是QC Team。现在连QA也丢到我头上了，该如何是好？”

研发处长：“软件经理之前在系统厂呆过，帮日本厂商做过产品，听说日本人对质量最注重了，你们好好聊聊吧，有结果告诉我，别忘了下礼拜要给老板报告！”

软件经理继续无力呢喃：“怎么在这家公司我总是有适应不良的感觉呢？”

故事在此先告一段落，你是否觉得其中有一些不合理的地方？

假使企业经营与产品开发只要根据教科书按部就班来执行就能成功的话，那么，企业就不必给高级主管或专业人士如此优厚的待遇了，只根据教科书来做尚且不足，更可怕的是对教科书里的知识还一知半解时，就急着把其中的名词在企业组织与流程中东拼西凑。

你可观察在周围的工作环境中是否总是能从高层拟定的公司策略中找到一些诸如3P、5P、RACI、SMART等企管名词？HR单位是否每隔一段时间就会推出一些新的激励、管理、自我开发等工具（结果到底有人知道360度评鉴是什么吗？只做270度会怎样呢？）。而在每位老板莅临指导的会议中，是否总是充斥着各种最新的技术名词术语缩写？

让我们继续把故事说下去。

4-1-2　项目经理vs.资深软件经理

PM：“根据惯例，技术主管迟早会直接找你要‘如何在Android Porting项目施行软件工程？’的报告。而测试团队的主管似乎已经大概知道了QA（Quality Assurance；质量保证）的定义，他摆明了测试团队的能量目前最多只能是QC（Quality Control）; 而QA则应该从整个项目的高度来看，所以他认为QA是PM负责的事。

看来这个烫手山芋现在已经落到我们两个人的手上，我们得好好想想怎么处理。对了！你的team member中有没有对这些领域较为熟悉的？先请他们帮忙弄份报告初稿出来再说。”

软件经理：“最近我也渐渐抓到技术主管的行为模式了，他总喜欢找些‘新的’名词来教育我们，而且爱过度追问技术与执行细节，所以这件事情我打算先不要麻烦工程师。但老板的命令还是要去执行，免得他哪天又问起，既然目前本项目——Android Porting已按照我们讨论好的方式在进行，我不打算临时拉人来作这报告，我想，就我们两个人设法搞定老板吧！”

PM：“嗯，其实这个内部项目根本就有点练兵的性质，目前也还没对应到明确的客户或市场目标。如果我们就把教科书或维基（Wikipedia）百科拿来抄一抄交差，我担心即便最后如期完成研发目标，老板也不会满意我们在执行流程上说一套做一套，导致最后团队的心血成果可能会被这些杂音盖过，这对辛苦的团队成员来说并不公平，你得考虑到这一点。”

软件经理：“没必要做假啊！我的团队一直依循软件工程的精神运作，而软件工程中本来就包含SQA（Software Quality Assurance）这个项目。一直以来，我们的开发流程规范中就定义了基本的质量策略，此外，我们还会根据不同项目的需求，分别制定不同的质量标准与工作流程。只要把我们施行以久的内部流程，加上一些专有名词润饰一下，应该就可以把报告生出来了。

根据我的职权范围，我只能管到软件工程中的SQA，但一个嵌入式系统开发项目不只包含软件而已。测试经理说的没错，你是项目经理，必须以整件项目的高度来看待质量保证（QA）这件事情。”

PM：“不好意思，我加入本公司也好几年了，哪次公司高层对软件开发流程的改革不是雷声大雨点小？事后想想，真能期待由硬件背景的人所主持的软件开发流程改革可以搞出什么名堂来吗？我从不知道我们公司曾真心投资预算或时间在导入CMMI或XP（eXtreme Programming）等软件开发流程，而软件团队就不断的在验证硬件、写driver、移植SDK、做客户的项目等工作中轮回，你怎么可能有空闲时间进行软件开发流程改善？更何况你觉得有老板会在乎项目成本与进度外的事情吗？

我们测试团队经过你导入QC的思想后，确实已经进步很多了，但他们也知道目前还没能力执行QA。可你刚刚又说你的团队内部有自己的质量策略，并已导入了SQA，这不是有所矛盾吗？

此外，我很熟悉项目管理理论中的质量系统，它提供了一体适用的思想与工具，所以并未特别提到如何应用在软件开发上，我一时还是想不通到底项目管理与软件工程这两类知识体系如何结合在一起？项目管理里的QA与你刚提到的SQA到底有何不同？”

软件经理：“如你所说，项目管理和软件工程都是一系列思想与工具的集合，许多理论与实际的专家依循这些思想，发展出各种更细致的指导原则。例如，你说的CMMI与XP都是在软件开发上的应用。以我们公司的文化与软件团队的规模，实际去导入或执行这些软件工程方法是不切实际的！但我们可以因此就不执行项目管理与软件工程吗？

正确的思想与工具摆在那里不去应用岂不可惜？正因为这些知识体系要传达的是思想，所以我们可以根据自身的状况与需求，拟定适合自己且真正可执行的软件开发与项目管理流程。如果执着于非得要选用某项方法才能去执行软件工程与项目管理，这就完全误解制定这些知识体系的初衷。所以重点是思想、是弹性、是因地制宜，绝对不是一条条的SOP！我想起《金刚经》里的一句：‘所言善法者，如来说即非善法，是名善法’，善知识是用来应用的，如果执着于名和相，那就什么都做不了了。

我大致归纳一下你的问题，共有3个。

■　软件团队如何在经费、人力、时间有限的条件下执行软件工程？

■　SQA是什么？该如何执行？SQA与项目管理中的QA有何不同？

■　项目管理与软件工程该如何配合执行？

在说明细节之前，我们应该先确认一项原则：既然软件只是电子产品开发项目的其中一环，那么，软件开发的所有活动必定不能违反其所依附项目的运作原则。以QA来说，软件团队的质量策略要具备弹性，不能违背各项项目的质量需求，也就是SQA的等级虽不能低于项目的质量要求，但也无需高出太多。

举个极端的例子，要求用日本大厂的质量方法来执行某白牌小客户的项目是绝对不make sense的！ 一般人都搞不清楚一件事，质量系统要求的并非完美无缺，而是在可控制的范围内，质量必须是可以被保证的。

详细的相关概念可以参考本书的《第3-5节：人力规划：营级组织、连级人力》与《第3-7节：质量策略规划》。这两节讲得很清楚，根据项目管理的精神，我们要的是一件确实可执行的方案，其他雷声大雨点小的也许适用于Microsoft或NASA等机构，但在我们这样的公司，只可能搞砸一个个项目。”

PM：“我了解了，一直以来你和公司高层无法达成共识就在于这里。老板们认为公司内应该只有一套软件工程与质量系统的SOP，什么项目照着做就可以了。而你始终无法说服他们，软件开发流程与质量系统是与项目特性相关的。老板要的是一体适用的SOP，殊不知可以一体适用的是‘思想’，而非执行方法！”

软件经理：“我已经跟老板们解释过不只一遍了，我们有基本的软件开发流程与质量系统，无论开发什么样的项目，诸如：设计审查机制、coding style、软件版本控制、bug管理等基本运作流程都是大同小异的，这些基本工作我们都有制定执行规范，并不会因不同项目而有太大的相异，但他们就是无法接受不同项目，可能难免会有不同的执行原则。

以Linux Kernel Porting为例，Porting和Implementation是完全不同的软件开发行为。对于新开发的Linux driver，我们仍会根据‘Design’ → ‘Review’ → ‘Coding’ → ‘Test&Debug’的流程来进行。但在Porting Linux Kernel时，若只因软件工程教科书认为软件项目必须具备明确的系统架构图，且模块间的关系与模块内的细节都要有设计文件，就要求项目成员必须完成Linux Kernel里所有module的设计文件。我们不做这件事，绝非如老板所说‘不依软件工程规范做事’，而是‘根据项目特性与软件工程规范，定义最合适的工作方法，并确保项目可按规格如期完成’。

抱怨的话就不多说了，让我先从你第3个问题回答起吧！就是关于项目管理与软件工程该如何配合执行？”

4-1-3　软件工程+项目管理

软件经理：“从项目管理的角度，你对‘项目管理与软件工程该如何配合执行？’这个问题有什么看法？”

PM：“基本上，项目管理知识体系重视的是达成目标以前的过程，也就是在明确定义项目规格后，通过反复执行‘规划’ → ‘实施’ → ‘监控’ 这3个动作来达成项目目标。因为没有两个项目是完全一样的，所以无法在项目初期拟定出绝对完美的计划，甚至项目目标也可能随着时间而有所变化，因此，项目执行可说是不断优化‘规划、实施、监控’这3个动作的循环，如图4-1所示。”

▲ 图4-1　项目管理流程

软件经理：“先抛开软件工程的细项，软件工程关注软件开发的过程，力图对软件项目的各个方面做出指导，包含从软件的可行性分析，一直到软件完成以后的维护工作。软件工程的主要目的是使开发成果的正确性与质量合乎规格要求，并通过某些方法与工具，让软件系统具有可维护性、可重用性和可移植性等重要特性。

简单来说，软件工程要做的就是不断优化‘开发过程定义’→‘软件开发’→‘质量监控’这3个动作的循环。而这3个动作正好可以跟项目管理的3个步骤彼此结合，如图4-2所示。

▲ 图4-2　项目管理结合软件工程流程

所以在嵌入式系统开发项目的例子中，软件工程乃依附于项目管理的流程内，两者基本的执行逻辑相同，所以可配合执行，两者并不冲突。”

PM：“也就是说，当我这个项目经理在执行项目的每个步骤时，软件团队的成员也必须有相应的行为啰？”

软件经理：“没错！从以下各项可以看出软件开发的确是亦步亦趋地随着项目执行流程。”

Step01：当PM在做整个项目的范围（Scope）定义时，关心的是整个产品的规格，而我们同时会执行软件系统的细部规格定义，包含UI规格、性能规范等。

Step02：当PM在根据项目Scope切割整个项目的WBS（Work Breakdown Structure）时，软件团队同时会去定义系统架构与各个模块间的关系。在软件开发方面，PM的WBS可能只会切割到‘软件开发与整合’，接下来，就会由我们根据软件工程中模块化的思想继续细分工作。

Step03：当PM利用WBS开始规划项目的schedule & cost时，软件团队必须提供软件开发所需的时间、人力和成本等信息给PM，而这些信息的取得乃根据软件工程的流程。

Step04：当PM在规划项目质量策略时，软件团队会同步取得客户对质量的要求，并制定软件开发的质量工作指导原则，包含design review、code review、测试计划、变更控管等流程。

Step05：PM会有一份整个项目的执行计划，例如：用Microsoft Project编出的.mpp文件，这份表中应该只有软件开发的重要项目与milestone，而软件团队则会有另一份包含所有软件开发细项工作的执行计划（例如.mpp文件），当然软件开发计划必须满足整个项目的需求。

Step06：当项目进入执行阶段，质量活动（如design review、testing plan review、code review、s/w version control、change control、testing等）会同步展开。软件团队会根据既定的质量计划执行，而PM除了关心软件质量之外，还必须随时追踪诸如硬件设计、结构设计、工厂备料安排等流程是否依循该项目的质量政策。

Step07：同样的，PM会定期召开项目进度会议，确认项目是否根据计划执行；而软件主管在参加此项目会议之前，当然会先开软件开发进度会议，检验查看目前状况，以确认软件开发进度与项目预计进度始终保持同步。

PM：“这道理说起来简单，但做起来并不容易。要能像我们这样运作，需要具备两项必要条件。

■　项目经理必须适当授权，并确保每位团队成员及其直属技术经理，都能知道项目运作的流程与原则。

■　软件团队主管必须在其团队中深植项目管理、软件工程、质量管理等概念。

电子产品开发牵涉领域较广，若项目经理想亲自管好每个领域的工作细节，结果就是什么都管不好！关于这一点我有深刻体会，当初我开始做PM的时候，自认是软件专长，因而过度参与软件的计划与review，结果不但疏忽了硬件与结构的成本与进度管理，还与软件开发团队的主管争执不断。这样的结果对整个项目一点好处都没有，而这些确实也不是项目经理应该要做的事情。

项目管理知识告诉我们：项目经理80%的时间应该都用在沟通，所以PM该做的重点是不断地去沟通项目运作原则与需求，专业的事交给专业，即由来自各部门的项目成员一起参与计划拟定、项目执行与质量保证等工作。

此外，我们公司虽然是以项目为导向，但软件团队仍必须建立软件团队该有的基本工作流程，并积极推广正确的软件工程与质量观念到每一位工程人员身上。举例来说，PM要管的是一个手机的开发项目，若与他配合的软件团队完全没有设计审查、版本控制等工作流程与基础建设（如bug management server、source code version control server、coding style definition等），PM硬要在项目期间改善其软件开发流程绝对是缓不济急的，而且会冒着顾此失彼的高风险。

项目经理（Project Manager）与技术团队经理（Functional Manager）若不各自做好自己的本分，则每件项目必定都是在经理们吵吵闹闹、同事们无效率的无限加班下勉强完成，而公司内的软件团队永远不会进步，而且总是会背负每件项目都支持度不佳的恶名！”

软件经理：“没错！在本公司这样的组织架构（强矩阵组织）中，最怕的就是老板搞不清楚‘项目团队’与‘专业工程团队’的差异，所以要求不对的人去处理或承担原本不该他管的事情。例如，要PM在某个项目中执行软件工程、要软件团队主管执行某个项目的QA都是不make sense的事情。

项目团队是因项目需求临时组成的，有其生命周期；项目成员可能来自不同领域的专业团队，项目结束后自当解散归建。项目团队的目标只有一个，就是设法达成项目目标！

而所谓专业团队，如软件团队是公司根据其营业目的所成立的常设单位，工作目标有二：持续改善执行流程与开发新技术；根据其专业，提供人员支持项目。

回到上面那两个make no sense例子。根据软件工程的思想做事，本来就是软件团队应该发展的重要项目，这是基本的思想，并不会因为某件项目才使用或不使用，但细节可根据项目性质调整之，而质量需求则可能根据不同项目而异，项目经理必须明确拟定项目质量策略与指导原则，再交由项目成员根据其专业分别执行之。”

PM：“对我来说，既然工作分配下来，鼻子摸摸去做就是了，也不知道老板是不是故意混淆，长期以来老是这样，弄得下面职责不清，工作效率难免大打折扣。你要不要跟老板‘强力对话，一下？”

软件经理：“Orz……好吧！无所谓了，反正已经够黑了，不在乎再多这一笔，为了公司好，真的希望老板们能听得进去。”

故事结束，时间回到现在

PM：“这个故事对嵌入式系统开发项目中的项目管理、软件工程、质量保证等项目，进行明确的定位，并说明了项目经理（Project Manager）与技术团队经理（Functional Manager）的职责权限。

为了尽可能提升所有项目的成功率，组织必须让正确的人去执行他们专长的工作。虽然公司内部一定会有个Job Description Table，其中明确说明组织中各个职务的职责，但是否公司内的每个人（包含老板）真的都清楚自己的职责范围呢？是否所有人都知道当初设计此种组织架构的目的与精神呢？如果答案是否定的话，那么可能从工作指派开始就会产生问题，工作效率自然大打折扣。

当组织中常发生因职责不清而导致的纷争时，自然无法正确且完整地导入良好的制度、工具、思想、制度与流程等，公司存在的目的就是为了营利，而内部组织是公司营运的基础，大老板自己一定要很清楚自己公司的组织，什么单位要负责什么性质的工作，这绝对是最基本的事情！但很遗憾的是许多老板天天把竞争策略挂在嘴上，无视公司组织正无效率的运作着。”

菜鸟：“这就是所谓的上有政策，下有对策吧！无论老板用什么样的角度想，如果组织交给你的团队没带好、交给你的项目不能顺利结项，最后还不是得处分主管。所以大家还是得兢兢业业，按照专业精神做事，用项目管理来管理项目，用软件工程来执行软件项目。”

在结束本节之前，笔者条列出软件工程知识体系（SWEBOK）的定义，所谓软件工程应包含以下领域。

■　软件需求（Software Requirements）

■　软件设计（Software Design）

■　软件构建（Software Construction）

■　软件测试（Software Test）

■　软件维护与更新（Software Maintenance）

■　软件构型管理（Software Configuration Management; SCM）

■　软件工程管理（Software Engineering Management）

■　软件开发过程（Software Development Process）

■　软件工程工具与方法（Computer-Aided Software Engineering; CASE）

■　软件质量（Software Quality）

上文提及软件工程中“开发过程定义”→“软件开发”→“质量监控”这3个步骤，笔者则可分别对应到SWEBOK中的各项主题。

■　定义开发过程：软件需求、软件开发过程、软件构型管理。

■　软件开发：软件设计、软件构建、软件测试、软件维护与更新。

■　软件质量保证：软件质量。

可以在网络上搜寻相关资料作为延伸阅读的材料。

04-02　设计阶段：拟定作战计划

时间：04月21日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

PM：“经过这阵子的规划，确定我们应该可以取得足够的资源，加上技术问题也有信心突破，所以老板已经批准我的计划，这个项目确定要go。我已经协调好了，下午准备搬家。”

菜鸟：“搬家？为什么要搬家？”

PM：“我要把和这个项目相关的所有人员集中在一起，当然也包括你。为了这个项目，我从各个团队中征调了 10位工程师，5位测试人员，以及硬件工程师、结构工程师、美工设计等人员，当然他们并非所有人都是从现在一开始就加入项目，但我已经帮他们把位子安排好了。”

菜鸟：“大家都在同一公司里，联系应该很方便，为什么还要大费周折呢？”

PM：“这绝对是有必要的，每个人的事情都很多，也许前一个项目还在收尾，如果不把所有人集中在一起，万一某个人被其他长官叫去做什么事，我只能哑巴吃黄莲。当然，书上都会说通过定期会议以及进度回报可以解决这样的问题。但经验告诉我，这么做有很大的好处。我甚至把客户专用的会议室，以及厂商、协助厂商（Third Party）工程师的位子都准备好了，必要时，把所有相关人员集中起来解决问题，这比E-mail往返要有效率的多。

别啰嗦了！其他较资深的工程师更不愿意搬家，他们现有的位置已经被文件淹没了，你的东西算少的了。对了，你的东西搬完后，过来帮我一下。”

菜鸟：“是！”

实际上，设计工作并不能在项目kick off后立即展开，虽然此时规格、进度以及所需人力都已大致确定，但公司有哪些人员可以适时加入此项目却还未知。一方面确实有那种可以以一当十的工程师，但职场却也不乏那种领钱鬼混的人，所以要尽量确认核心人员何时到齐。例如，某位工程师按表应该在项目开始的两周后full time参与本项目开发，但他却可能被其他项目牵绊而脱不了身。更麻烦的状况是，他虽然表面上已经加入项目，但却还经常被借调去其他项目解决问题。

一般公司的做法是让所有参与项目的员工尽量坐在一起，如果公司规模够大的话，可能会将公司的某一层楼或某个明确分隔的区域划分给项目团队。例如，某美工人员虽然仍隶属于美工部门，但在参与项目的期间，就会和项目的其他员工坐在一起，统一管理；绩效考核虽然还是由原部门主管[1]处理，但是项目经理可以拥有较高比重的建议权。人力资源管理一直是项目管理中的重要项目之一，项目经理必须尽早确认一个萝卜一个坑，把所有人员找齐。图4-3与图4-4所示是一般项目组织的人员架构图。

▲ 图4-3　矩阵式组织图

▲ 图4-4　复合式矩阵式组织图

换句话说，在设计工作开始之前，要尽可能排除所有不确定的因素。如果在很多事情都尚未确定的状况下就贸然开始开发工作，要完成项目就只能靠运气或上帝保佑。另一方面，即便项目开发初期已经排除了所有不确定因素，身为项目经理也要做好风险管理，也就是突发状况的应变计划，例如：某位工程师离职了、客户更改规格等。

做完事前规划及人员配置后，才可以进入设计阶段。而这个阶段该做的设计工作不仅仅是简单的软件系统设计，还有诸如硬件设计、测试计划、质量系统及风险管理等，都需要经过有条理的设计，这些在本章中都会详细说明。在设计工作进行期间，所有参与人员都应该积极地贡献他的脑力与创意，而不是急着写程序或画原理图。简单地说，设计未完成，都还不知道系统会长成什么样子，在这个状况下所做的implementation意义何在[2]？

以下就分别详述设计阶段该做的工作项目。

04-03　产品规格设计

PM：“在设计工作展开的同时，我需要一位专人负责和客户沟通，以及编写产品规格书。”

菜鸟：“产品规格书？是不是就是你们常提起的Product SPEC或Functional SEPC？这不是客户该负责的吗？”

PM：“名称不重要，在不同公司可能有不同的称呼。总之，我现在需要的是完整描述产品功能的规格。客户一开始提供的规格通常过于粗略，有时候甚至仅止于描述产品的思想而已，而且客户总是倾向刻意保持产品规格的模糊地带，以后才有扩展或修改的理由。虽然此时系统架构的设计工作已经展开，但是我们没有办法用这样粗略的规格来进行细部的设计工作。现在我们需要的产品规格是举凡产品理念、使用者的操作流程、UI（User Interface）、美工图、性能要求、注意事项等，都必须根据产品的各项功能原原本本的写清楚。更重要的是要和客户广泛讨论，以确定我们所设计的功能确实是客户所需要的。

当产品规格写完后，最后再让客户确认一次并签名。注意！签名这个动作非常重要，可以让以后客户要改动规格时有所节制。以后，所有设计工作都必须以此规格为模板，因为它规范了此次开发产品的所有功能，任何人的命令，包括客户，与此抵触都视为改动规格。”

菜鸟：“如果客户就是执意要更动规格，怎么办？”

PM：“把产品规格文件化的好处有很多，它对此次项目开发的范围作了清楚的规范，也可以说这是研发团队承诺能在进度内做到的事情，任何超出这个范围的要求就叫做‘Request’或‘Inquiry’。一旦要将新的需求‘变成’规格，必须通过改动已通过审查的正式文件，而根据质量系统，变更正式文件必须要经过一定流程。把产品规格用白纸黑字写下来，并经客户审查，这样对研发团队的权利才有保障。”

菜鸟：“居然此时可以把质量系统搬出来当替身符！”

PM：“本着服务客户的精神，通常规格改动不大的话，研发团队都会接受并修改。但一般非技术人员无法深刻感受规格更改会对项目有多大的影响，因此，我们有义务节制客户更改规格的欲望，最有效的方法就是尽早把产品规格立项，并制定出修改规格的审定流程。”

在开发初期完成产品规格好处极多，整个开发团队，包含测试人员与客户都可根据它作为工作的依据。一般来讲，产品规格应该包含以下项目。

▲ 图4-5　产品规格书中的流程图

■　硬件规格简介

■　产品设计理念、限制与应用范围

■　使用者功能说明

■　操作流程图（如图4-5所示）

■　使用者界面（User Interface）与美工图案（如图4-6所示）

■　性能规定（如产品耗电流）

■　特殊注意事项

▲ 图4-6　产品规格书中的美工图

04-04　硬件设计

PM：“目前产品规格书的进度是什么？客户有没有特别的要求？”

菜鸟：“有些UI的部分他们目前尚未决定。此外，有个新的要求是希望产品电池用量可以超过100个小时，且抗ESD（静电）能力可以符合他们公司的标准。这些要求我都会写在产品规格内，而且在写入之前，我已经先通知相关单位，并确认应该可行。”

PM：“不错，蛮进入状况的。其中抗静电能力的要求你去通知了哪个单位？”

菜鸟：“我其实不太清楚抗静电是怎样的一个概念，所以我在和固件工程师讨论时就顺便问了之前的产品是否可以达成这个标准，他们认为可以。”

PM：“大部分的软件工程师都还蛮害怕和硬件工程师接触的，反之亦然。以ESD防护能力为例，固件工程师可以将某些PIN脚设为较不易被ESD影响的状态[3]，但产品抗ESD的能力主要由硬件和结构设计主导。关于这个问题，你必须事先通知硬件和结构工程师。”

菜鸟：“我进公司也一阵子了，但是我只和软件及固件部门的员工比较熟，实在没概念什么问题该找哪一个部门。”

PM：“OK，那你明天跟着我，和固件员工一起去参加target board的设计审查会议。”

菜鸟：“我知道嵌入式系统开发项目的软硬件工程师要密切沟通，但毕竟术业有专攻，为什么硬件的设计审查需要那么多单位参加，我们能提供什么意见？毕竟我们的专业并非硬件设计啊!”

PM：“固件工程师可以提供的意见可多啰！应该这么说，我会要求硬件部门的主管，在硬件设计阶段必须时常征询软件工程师的想法，否则，设计出的硬件若无法达到软件设计的要求，麻烦就大了。举例来说[4]，产品规格里定义了播放.PWM声音格式的功能，若硬件工程师随便选了一支一般用途的输出脚连接CPU与放音电路，当然软件工程师也可据此硬件设计来做，软件开发的困难度就比直接用CPU提供的TIMER脚输出频率信号高的多。也就是说，硬件工程师在设计时通常没有能力主动考虑到软件的需求，如果可以在设计阶段就做好沟通，除了可以避免事后不必要的争执，对开发工作绝对有正面的帮助。”

硬件设计阶段的工作项目如下，除了外观与结构设计之外，固件工程师应该尽量参与每件事情的决策。

■　CPU选择

■　主要芯片选择

■　电路设计

■　Layout

■　零件、物料管理（避免选用了某个零件，结果交期太久，赶不上进度）

■　产品外型（ID）设计

■　结构设计

■　开模之前的模型制作

在此阶段应该要做出的文件为：

■　硬件设计规格书

■　BOM表

■　3D外观图（如图4-7所示）

■　结构图（如图4-8所示）

■　原理图（如图4-9所示）

■　Layout图（如图4-10所示）

在笔者过往的经验中，常常发生固件工程师拿到第一版的硬件板子后，才发现很多不合理且窒碍难行的地方。如此一来，难免要重新修改硬件电路，并重新制作一版硬件板子，不但旷日费时，也延误进度。此外，固件工程师因此会有很长一段时间[5]没有正确的板子可供验证，而且还要浪费开发费。与其如此，不如在一开始，软硬件人员就协同做好硬件设计的审查工作。

▲ 图4-7　3D外观图实例

▲ 图4-8　结构图（产品组合图）实例

▲ 图4-9　原理图实例

▲ 图4-10　两张Layout图实例

04-05　系统设计

PM：“和硬件工程师沟通的如何？产品规格有问题吗？”

菜鸟：“有关抗静电能力的部分，硬件工程师已经针对一些比较敏感的PIN脚增加一些零件保护，但是结构人员觉得客户提供的外观图完全没考虑到抗静电，据此做出来的结构抗静电能力一定很弱，恐怕要增加很多保护措施才行。如此一来，不但增加成本，制造时的工序也会比较复杂，所以结构人员还在和客户讨论中。对这些事情，像ESD、结构、还有什么制造的工序，我都是第一次接触，有点似懂非懂的，怎么一项产品光在设计阶段就这么复杂？”

PM：“别担心，慢慢你就会懂的。既然产品规格已经编写完成，那么下星期开始你就跟着软件部门的员工进行系统设计的工作吧！目前系统架构已经设计的差不多了（如图4-11所示），接下来要做的是各模块的细部设计，包含API、数据处理流程图和控制流程图等。我和主管讨论过后，他会和你说明细节，并分一个模块给你。在此之前，你必须先弄清楚这个项目的系统架构。”

▲ 图4-11　系统架构图实例

菜鸟：“报告长官，这可是我的专长，我在学校学过OO设计，尤其是UML[6]，那些工具我超熟的。您放心，交给我就搞定了！”

PM：“很抱歉通知你，本项目不会用UML来进行系统设计。在嵌入式系统领域中，如果可以的话，我几乎不会选择使用面向对象语言，因为C++的性能比C语言差，更不用提Java了。当然我们还是会用到OO的思想来设计系统。从技术上来说，也可以用C语言来实现大部分的面向对象思想[7]，但若强迫工程师用C语言来实现UML规划出来的系统，恐怕还是有叠床架屋之感，更不用提对嵌入式系统性能的影响了。C++编译器产生了太多程序员无法掌握的机器码，这在嵌入式系统开发的许多状况下是无法忍受的。”

菜鸟：“所以您的意思是说，我们还是用传统的结构化方法在设计系统？”

PM：“也不能完全这么说。如果是设计Smart Phone或平板计算机，使用的CPU动不动就是4、5百MHz，此时使用UML、C++、甚至Java应该都没问题（Android的AP就是使用Java语言开发的）。但本项目的CPU只有24MHz，所以我们无法那样操作。话说回来，在嵌入式系统这个领域，许多技术已经在业界延用了二三十年，你只要想想8051这个CPU问世多久了，目前还是用量最大的CPU。不过话说回来，就如同我对待质量系统一样，我也希望引进许多最新的规范与工具，但现实中人力和时间的诸多限制，再加上一个项目结束，紧接着另一个项目就开始，我实在无法去和老板说我需要更多的时间和人力来建立这些制度。

我们可以找到很多软件开发的方法学，一般台湾地区项目开发团队都算是‘轻量级’的，硬要使用重量级的方法可是会受伤的喔！”

重量级方法呈现的是一种“防御型”的姿态。在应用“重量级方法”的软件组织中，由于软件项目经理不参与或者很少参与程序设计，无法从细节上把握项目进度，因而会对项目产生恐惧感，不得不要求程序员不断编写许多“软件开发文件”。

而轻量级方法则呈现“进攻型”的姿态，这一点从极限编程（eXtreme Programming；简称XP）方法特别强调的4个准则——沟通、简单、反馈和勇气上有所体现。目前有一些人认为，“重量级方法”适用于大型的软件团队（数十人以上）使用，而“轻量级方法”适用于小型的软件团队（几人、十几人）使用。当然，关于重量级方法和轻量级方法的优劣存在很多争论，而各种方法也在不断进化中。

PM：“在现实环境中，我们无法采用最新的方法来执行设计和开发工作，身为项目经理的我并不是刚才所说的那样恐惧，而是面对现实不得不然的应变！懂吗？不论用什么方法进行系统设计，最重要的是设计结果必须经过严格的审查程序，从大架构到小模块都必须一一确认无误。此外，一定要留下经审查的设计文件，而且必须要求负责实作的工程师确实按照这份设计规格书来执行。”

菜鸟：“所以，负责设计的和写程序的可以是不同的工程师啰？”

PM：“理论上是的，但我就只有这些人，所以通常都会让设计者自行实现与测试，这样可以有比较好的效率。团队中有些人就是比较有sense、执行力强，如果可以让这样的员工帮我多管理一些事情，我怎么舍得安排他们就照着设计文件去写程序？可是天总是不从人愿的，不是吗？”

系统设计是本书的主题之一，在《第5章：实作你的第一个嵌入式系统》会有更深入的探讨，在此就不再赘述。这里只想再强调一点，系统设计工作的重点不在于使用哪些方法或工具，重点是必须能控制设计规格书能够符合需求并可执行，通常刚开始参与设计的人数不多，先把系统架构设计出来，文件化后再加入更多基层工程师，把大模块划分成小模块，逐一设计完毕，最后再确认模块间的关系与沟通方式是否无误。

最后，项目经理必须严格控管，一定要完成设计工作才可进入实作阶段。宁愿多花一点时间设计，也不要项目进行到一半才发现设计有误，这样之前写的程序和花掉的时间都付诸流水，对项目伤害之大莫此为甚。

04-06　测试计划设计

时间：04月06日　 登场人物：菜鸟、项目经理（PM）、测试老鸟

PM：“产品规格书客户同意签名了没有？”

菜鸟：“口头上同意了，但仍不愿意签名，也就是保留规格修改权利。”

PM：“我早知道会这样，就算签了名也不表示以后就不会改了。没关系，你把最新的产品规格寄给测试部门的负责人，然后和他约个时间，你去和测试小组的员工讲解一下规格及特殊的注意事项。我们的时间所剩不多，很多事情必须重叠进行，现在就应该展开测试计划的设计工作。”

菜鸟：“没问题！报告时有什么是我该注意的？或者我该这么问，测试人员想得到的信息是什么？”

PM：“没错！很有概念，做报告最重要的就是要先了解听众的需求，你先和测试小组的主管讨论一下吧！”

菜鸟：“PM老大要我向测试team报告一下这个开发项目的产品规格，因为我是新人，实在不清楚在项目刚开始的阶段，测试团队应该需要得到哪些信息，可以麻烦您指教一下吗？我很害怕我的报告会浪费你们的时间。”

测试老鸟：“现阶段的任务是测试计划的设计，你知道测试计划书是用来做什么的吗？”

菜鸟：“应该是计划要怎么测试吧（废话！），但内容包含什么以及在嵌入式系统项目中角色是什么？我实在不是很清楚。”

测试老鸟：“这也难怪，一般信息、电机本科的毕业生对测试工作通常都怀有鄙视的概念，所以对测试工作的内容都不太了解，一般公司也都把最顶尖的人才安排到开发部门，但测试工作质量的优劣，对项目的影响程度绝对不亚于开发工作。不好意思，发了一下牢骚。言归正传，测试计划是软件质量流程的前期作业，测试计划的设计与执行会直接影响软件项目的质量。一般研发团队因为人力的限制，常在项目中后期才让测试人员正式加入，届时没组织、没计划的乱测，常常让研发工程师在小问题上疲于奔命，却完全找不出真正严重的问题！这样不但扰乱质量管理工作的步调，还让测试人员与工程师一直处于对立的状态，对项目一点好处也没有。”

菜鸟：“其实PM老大也一直灌输我‘测试工作的重要性不亚于开发工作’的思想，我只是不知道测试计划这个工作该如何展开，或测试计划书应该包含什么数据？”

测试老鸟：“编写测试计划书的主要目的有3个。第一，让软件测试进行的更加顺利；第二，让测试人员之间的沟通更加的畅通；第三，让软件测试采取系统化的方式来进行，同时也易于管理。软件测试也有国际标准，IEEE 829详细描述了测试计划书的规定与注意事项。当然以我们投到每个项目的测试人力来说，是无法完全按照IEEE 829标准来做，但我们一直都将它的思想和精神秉记于心。待测试计划书设计完毕后，我们会花一段比较长的时间，仔细研究规格，然后据此来制定测试个案。所以你在讲解产品规格时，应着重在较复杂的功能、新功能、客户特殊的要求或规格书中的模糊地带。”

菜鸟：“这样我大概知道该怎么报告了。另外有一个问题是：规格书中有一些condition table用来描述各种操作间的关系，这和您刚刚说的测试个案有何不同？”

测试老鸟：“很多人都会把condition table和testing case搞混。举例来说，我们的电子辞典产品有磁簧开关[8]，condition table会定义盒盖打开时应该开机，关闭时必须关机，我相信RD将版本送测试时，一定会先测试这个功能，测试人员只要稍做验证即可。针对这个功能，我们根据之前的经验得知频繁的开关机一直是最容易出问题的地方，所以我们会制定一个测试个案。”

表4-1　测试案例实例

编　号

案　例

应产生行为

结　果

MR-1

打开电源，并频繁的开关盒盖一分钟，最后开启盒盖

系统可继续正常运作

OK or NG

测试老鸟：“在设计嵌入式系统的测试计划时不能仅考虑软件，像上述磁簧开关就是一个很好的例子。有时测试人员为了模拟电池电量陡降的现象，还得操作电源供应器。为了测试系统耗电流，就必须学会使用电表；为了确认LCD显示频率，就必须使用示波器等。而且我们还必须管理bug，还要设计自动测试与压力测试的个案，必要时测试人员也要写程序。你知道吗？测试人员负责产品质量的把关动作，压力很大的！”

菜鸟：“我知道了。”（牢骚一堆，测试人员压力果然蛮大的……）

软件测试是一门相当专业的学问，包含很多理论、方法论与国际标准，尤其是嵌入式系统的测试更是复杂，你不妨思考一下如何进行刹车系统的测试。但每一件嵌入式系统项目的成员都应该有一个概念：软件工程师不可能写出没有bug的程序，测试工程师也不可能找出系统中所有的问题，所以测试工作一定要经过仔细的规划，则测试工作才可以被控制，测试结果才可以被量化，产品质量才可以被保证。

笔者在业界看过太多不重视测试的团队，时至今日，大多公司仍然把优秀的人才放在程序写作上，当然也有更多公司让优秀的人才进行设计，可是很少公司让优秀的人才执行测试工作。但实际的软件工程实践证明，让对软件思想有深刻理解的工程师进行软件测试，可以大幅度地提高软件质量。质量对产品开发而言不是最重要的吗？

有关测试工作的实践，请参考《第18章：Testing、Debugging 与 Tuning》。

04-07　风险评估

PM：“设计工作已经顺利上轨道了，身为项目经理的我也该开始认真地做风险评估了。上次我给你看的《与熊共舞》那本书你看完了吗？有没有什么心得？”

菜鸟：“你要我用看故事书的心态去看这本书，可是很多地方我真的看不太懂。风险是还没发生的事情，要如何预测？如何预防？书里写了很多策略，难道用这些策略就可以减轻风险带来的伤害吗？”

PM：“风险评估与管理确实不是一件容易的事情，但其重要性应该无庸置疑吧！你应该听过莫非定律（Murphy's Law）吧！只要能出错的地方，就一定会出错！既然如此，我们就要尽可能、尽早地把可能会出错的地方找出来，并管理它，这就是风险评估的基本概念。”

菜鸟：“如果能把会发生的事情都找出来，不就可以先解决它，那就不叫风险了吧？就是可能会发生，但又不知道会不会发生的才叫风险，不是吗？”

PM：“没错，但你没听清楚我的定义，我可没说有办法把可能发生的问题通通找出来，我也没说要解决所有可能发生的问题，我用的动词是管理。举个简单的例子，到项目后期最害怕的就是重要人员离职造成的问题，身为管理者在初期就该意识到这个风险，所以该做的对策是严谨的制作设计文件、重要的工作找另一位员工相助、维持团队合谐的气氛（离职者会愿意认真交接才离开），则当风险成为实际的问题时，管理者才不会措手不及。

简单地说，所谓风险管理就是在风险演变成致命的问题之前，就先试着舒缓风险。风险就在那里，而我们当然有办法管理它们。《与熊共舞》一书中提到软件项目中最普遍的风险有下列5项。

■　进度拖延

■　需求膨胀

■　人员流失

■　规格崩溃

■　绩效低落

但我认为嵌入式系统开发的风险还包含：

■　技术落差

■　不同领域人员或公司间的文化差异

■　软硬件整合的相关问题

■　生产、制造的相关问题（零件缺货或涨价、工厂产能不足等）”

菜鸟：“我了解风险评估与管理的重要性了。可是像我这样的菜鸟，怎么有办法列出项目可能出现的风险？所以经验很重要啰？”

PM：“是的，原则上风险评估就是把之前项目的经验，加上这个项目特有的风险，所以整个项目的风险评估当然是项目经理的工作，但我还是会要求各位小主管提供他们所负责领域的风险列表（小范围的风险评估）给我，而且我希望风险管理的思想能深植在每一位员工或工程师的工作态度中，这样你了解吗？”

图4-12　风险识别流程图

《与熊共舞》是一本从软件从业人员到项目经理都值得一读的好书，讲述从项目管理到系统开发都不能缺乏风险管理的思想。此外，书中还提到了一些量化、评估与验证风险的方法，都可实际应用在项目开发之中。面对风险时，有4种策略可以选择。

■　回避：不去碰触会产生风险的部分

■　抑制：准备足够的时间与金钱，在风险成形时砸下去

■　舒缓：在风险成形前，就先采取某些措施

■　逃避：什么都不做，祈求老天保佑

如同书名所示，假设风险是一只在我们身边出没的大熊，与其回避它、和它硬碰硬或者祈求老天保佑，倒不如一开始就和它交好，与熊共舞。

04-08　设计文件的重要性

菜鸟：“开了那么多会，系统架构和各个模块之间的关系总算都已经讨论清楚了。据我所知，已经有很多工程师开始摩拳擦掌准备开始写程序了。有些模块真的很简单，就算是我也只要两三天就可以做完，但是你要求一定要做完文件并经过审查才能开始动手，工程师间已经有些抱怨的声音了；他们觉得规划出来必须执行的事情那么多，为什么还要浪费那么多时间去写文件？”

PM：“这个情况我也知道，已经请各部门主管去疏通，但做文件这件事情绝对不是浪费时间，即便项目可用的人很少，时间也很短，还是坚持要把设计文件写下来，就算要我说出：只要不配合编写设计文件的工程师就离开这个项目，也在所不惜！”

菜鸟：“有这么严重吗？”

PM：“当然严重！我太清楚工程师的心态，每个人都只想要写程序，没有人想测试，每个人都觉得写文件是没用且浪费时间的。所以当你允许某位工程师不写文件，这样的气氛是会传染的。在我管理的项目内，是绝对不允许设计只留存在某些人的脑袋中！你也知道我们项目的软件架构相当复杂，我希望当所有工程师碰到疑问时，都能直接到文件服务器中找到‘正确’的文件，而不是去问其他也在忙的同事，或默默地去看别人的程序。不仅浪费时间，而且你问到的或看到的也不一定正确。

如果任由这种可能不遵循设计规格的程序像病毒一样散布在系统里，会发生什么事情完全无法预测。到时离结项时间越来越近，要把这些问题一一找出来已然不可能，于是只能用patch的方式看到一个洞就去补一个洞，殊不知patch这个动作，又是另一个违背设计规格之举！于是系统的bug层出不穷，此时已经不是何时可以解完所有bug的问题了，而是该系统的质量完全失控，就算暂时找不到问题，客户也无法相信真的没有问题！

这一切灾难都是起于文件管控不严格。现在会对团队中工程师做出如此严格的要求，其实都是血和泪的教训。而且我们并没有规定文件一定要写得非常正式啊！总之，人少有人少的办法，但就是不能不留下设计文件。”

菜鸟：“报告长官，我知道了，我认真去写文件就是了。”

从项目一开始，就应该准备一个服务器存放并管理项目的所有文件，并让所有项目成员都可以很容易取得所需的文件或记录。现今有许多免费的项目文件或记录的管理系统，如Trac Project（http://trac.edgewall.org/），使用起来相当简单，也可以做文件版本的控制。

应该被记录的文件包含：

■　产品规格

■　Schedule

■　硬件设计相关文件（CPU PIN脚配置图、原理图、Layout图等）

■　技术文件-芯片的data sheet、3rd party软件函式库的API等

■　系统架构图

■　系统（软件）设计规格书（包含各模块API）

■　测试计划书

■　测试报告及bug sheets

■　质量文件

■　重要会议记录

■　重要邮件备份

■　其他

文件除了必须执行版本控制之外，有时也会有多人同时编辑同一份文件的需求。实际上，笔者会建议使用以下方法。

■　架设WIKI Server（如维基百科使用的MediaWiki Serevr，架设上非常简单，团队中所有人可以共同编辑文件，你可在http://www.mediawiki.org/wiki/Download下载）。

■　使用Google DOCs。

04-09　开始实作之前：设计审查

菜鸟：“我昨天仔细地把我们这个项目的计划文件（Microsoft Project/.mpp file）看了一下，发现我负责设计的模块出现在许多个地方，分别是在design、implementation、integration & debug这3大项工作之中（如图4-13所示）。

▲ 图4-13　项目计划中必须明确地将设计与实现的工作项目分开

我认为这个计划文件看起来颇为复杂，工作项目的数目几乎暴增了3倍。既然你已经将这个模块交给我，我自己会安排时间，在期限内把这个模块的design、implement、debug工作全部做完，可以不需要切分为这么多的工作项目嘛。”

PM：“万一你在期限到时才跟我说有困难，无法如期完成，请问我的项目是不是就得准备delay？你认为我这个老江湖会把项目成败的担子放在你这个菜鸟肩上？如果每个人都能自动自发、且自我管理得很好，那么，项目经理岂不就没饭吃了？

如果你真的能确实根据‘design’ → ‘implement’ → ‘unit-test’ → ‘debug’ 的流程来开发这个模块，何必担心中间的查核点？而且从这个计划表中，一眼就可以看出我们执行这个项目的精神——每个模块都必须经过design review才能进入implementation阶段。而且，计划中为每个模块明确安排了执行单元测试的进度，所以工程师就不至于把一个完成度不高的模块送入整合阶段。

项目的进行是一关卡一关，绝对不能为了省下阶段间review的工作，就让可能的风险累积到项目后期。我们都知道，越到项目后期发生的问题，需要加倍的时间与人力资源去解决。在嵌入式系统开发项目，设计阶段的工作真的非常重要，只要设计审查执行的够扎实，真的可以由不同工程人员分别负责design与implement，这可使得项目人力调派更具弹性。

当然计划的制定不会那么死板，项目团队中总有较值得信任的工程人员，我们在分配工作给他时，就不需要切分的这么细，但目前身为菜鸟的你还没有获得这样的credit。总之，你就根据项目计划里的schedule，在设计审查前把设计文件写出来吧！”

菜鸟：“我了解了，相信总有一天我会脱离菜鸟的行列。至少我现在还是你心中的菜鸟，可以麻烦您提示一下设计审查的执行方式，以及我送交审查的设计文件应该注意哪些事情吗？”

4-9-1　设计文件存在的意义：服务内部客户

PM：“许多公司会在设计文件的格式上作文章，要求工程师必须按照规定的格式来编写设计文件。我认为这样的规范并非不好，但是很容易失焦。最后组织累积了许多文件，但文件内容和系统的现况完全搭不起来。我们在许多想要通过ISO9000质量标准的单位最容易看到这个畸形的状况（因为ISO9000的基本思想就是凡走过必留下痕迹，除了技术文件之外，组织流程、设计审查过程、会议记录等都必须以特定格式留下记录。企业的应变方式为一将团队分为负责赶项目进度的实作组，以及专门造出文件与编造会议记录的‘应付’审查组）。

我们的工作态度一向是实事求是，要求技术人员编写设计文件的目的非常简单，就是希望无论任何时候（项目执行中或项目结束后），即便原设计者不在，其他人都可以从此文件中得知此设计的技术内含、使用方式和注意事项等。基于这样的需求，你认为设计文件应该包含哪些内容？”

菜鸟：“以我负责的模块为例，若要让其他员工知道，我想该模块的设计文件至少要包含：

■　模块功能描述

■　与其他模块间的关系

■　功能限制

■　数据结构

■　API（包含函数功能、使用时机与限制、参数及返回值的意义）

■　实现方式：当别人用这个模块时，不见得需要知道技术细节，对外的文件可隐藏这一部分”

PM：“基本上没错，但有些设计文件描述的不只是一个模块，例如，系统架构的设计文件需要包含的项目可能就有所差异。在此我们还是从设计文件的用途来思考这件事，想要参考你设计文件的人有两种：一种是想要使用你这个设计的人；另一种是想要修改或扩展你这个设计的人。

新技术开发出来就是要被使用（若能不断被重复使用更好），所以技术文件必须满足第一种人，至少得说清楚该设计与外界的关系。正确的思想必须是

■　谁会用到你的设计，他就是你的客户。你必须弄清楚客户的需求，然后才是说明你的设计能提供什么东西给客户。

■　你的设计是否必须依赖其他模块才能正确运作，彼此间存在的接口是什么？

■　你的设计的使用时机，以及使用上的限制。

设计文件满足了新设计的使用者后，接下来才是对技术内涵、实现方式、参考资料等细节的描述。基于这些原则，你除了可以了解设计文件要写些什么，还会知道设计审查会议要如何召开。”

菜鸟：“思想我听懂了，但要把公司内部其他员工当做客户，似乎有点怪怪的。”

PM：“一点都不奇怪！公司内部总是会有一些部门之间争功诿过的纠纷，这就是没贯彻‘永远把上一层当作你的客户’的精神。你可以想象我们的分层式系统架构，上层会调用底层的功能，所以底层开发单位就应该把上层开发单位当成客户，认真倾听他们的需求。唯有如此，组织间才真的可以做到层层负责。

最后我再花点时间说明一下设计文件审查的原则。

■　设计审查必须是渐进式的。简单的说，不能等到工程人员认为设计完毕才进行review，届时才发现方向偏差就麻烦了。通常一个重要的topic，我们一开始就会排定三次以上的review meeting。每次的review meeting可能会发现某些没注意到的盲点，或与会者决议应该更深入探讨的议题，都会成为下一轮审查会议的议题。

■　设计审查时，一定要邀请该设计的‘客户’参加。

■　越是大范围的设计工作，越是要召集所有项目人员参加。例如，所有开发人员都必须了解系统架构的精神，所以系统架构的设计审查最好每个单位都要派人参加，到设计即将立项时，应该尽量请所有开发人员都参与审查会。

■　设计审查会议无特定议事规则，但会议气氛应尽量保持轻松，设计审查时不分职等，任何与会人员都可以提供意见，尤其是该设计的‘客户’。”

04-10　实作阶段：执行所有设计

菜鸟：“从项目开始的一两个月里不是在开会，就是paper work，许多工程师都和我有种很闷的感觉。现在所有的设计文件都陆陆续续完成审查，终于可以大展身手，开始帮忙写程序了吧？”

PM：“实际上，我们不会等所有设计文件都审查完毕才开始实作阶段，目前硬件设计、结构设计和固件部分的程序写作都已经开始了。你放心，我们绝对有写不完的程序，但恐怕你‘大展身手’的时候还没到。”

菜鸟：“长官，你不要小看我写程序的功力喔！我在学校时可是参加过程序设计比赛，还得过奖呢！”

PM：“放心，你是我面试进来的，你的‘丰功伟业’我铭记于心。可是如果我们的设计文件都做得很好，每一个模块该有哪些API，每一个API的prototype、参数与传回值的意义，甚至参考的算法或pseudo code都准备好了，那我真的觉得要你写这个程序可是大材小用了。我们花那么多时间在设计阶段，不就是为了任何人都可以根据设计文件进行系统开发吗？唯有如此，实作阶段的工作才是受控制的！

在重视项目管理流程的团队中，工程师的创意只能发挥在设计、调试、调整与优化等阶段，就是不能用在程序实作阶段。在实作阶段滥用创意，如同随意改变设计成果，则项目开发必然失控。”

菜鸟：“这个道理我是懂啦！但难道软件工程师就这么不重要？”

PM：“你还是没搞懂工程师和工人的区别。你只要想想为什么美国的软件公司可以把大部分的开发工作交付给印度软件公司去执行，而同文同种的台湾地区软件公司和大陆软件公司合作起来总是一团乱。软件工程师的价值在于架构设计、系统整合，以及解决问题的能力，而软件工人的价值则是根据规格，快速地完成高质量的程序。前者用的是创意，后者用的是技巧，这样你懂吗？我年纪比你大这么多，即便写程序的技巧还没荒废，但写得会比你快吗？而我却可以领你两倍的薪水，除了管理能力之外，你觉得这是为什么？”

菜鸟：“嗯～解决问题的能力吗？”

PM：“应该说是找出问题或是定位问题的能力。身边总是有人可以很快地点出问题所在，不懂的人认为那是直觉或运气，实际上，那是经验与专业的累积，而这才是工程人员最需要的能力特质吧！不过话说回来，我们的人力实在不多，也只有委屈你这位程序设计比赛得奖的高材生，帮忙写一下程序啰！”

菜鸟：“本来是兴高采烈的，怎么现在有点沮丧的感觉？”

PM：“放轻松点，到调试、整合还有性能调试时，你就可以大展功力了，到时候有得你忙的！”

当所有的设计文件都审查通过后，才可以真正进入实作阶段。从项目一开始，工程师们被主管压抑了许久，现在终于可以开始进行程序写作了。请注意！笔者这里用的词汇不是一般说的“程序设计”，而是“程序写作”，因为项目生命周期中的设计阶段已经结束，所有的设计工作应该都已完成，而实作阶段要做的就是根据设计文件按章行事罢了！

设计工作需要创意，解决问题也需要创意，但实作则不然。一般工程师应该很难接受这样的论点，但偏偏许多研究都证实，程序代码开发阶段是最不适合创意与灵机创作的地方。总之，工程师不应该按照自己的直觉做事，否则无论设计出任何天才之作，假使不是客户所需求的，那就只是浪费时间，并且让项目进度和人力的管理出现缺口，对项目的进行有弊无利。因为终究我们谈的是商业项目开发，而不是学术研究。

所谓的实作不只是程序开发而已，笔者根据美国项目管理协会（PMI）的精神[9]，将所有执行设计成果的工作都包含在实作阶段之中。因此，一个嵌入式系统开发项目的实作阶段应该包含：

■　程序开发与调试

■　硬件（电子、结构）开发

■　软件与硬件测试

■　质量系统执行

■　工厂试产

没人可以保证设计结果绝对不会有错。在实作阶段才发现设计的疏失在所难免，此时项目主管不能放任工程师自行修改，因为该工程师不一定知道其他相关模块的状况，也无法掌握他修改的方法是否有副作用（Side Effect）。此时若不严谨的执行“设计变更”流程，则如同埋了一个不定时炸弹在项目中，无人知晓何时会爆炸。简单地说，项目的每一个阶段都应该根据PDCA（Plan→Do→Check→Act）循环，如图4-14所示。当发现有设计问题时，就不能不暂时局部性地停下实作的脚步，找出对策，确认影响范围，通过相关单位的设计审查后，才可以再继续实作。

▲ 图4-14　PDCA循环示意图

遗憾的是，笔者接触过的嵌入式系统开发团队通常都有个很大的毛病，无论硬件、软件、甚至结构工程师，几乎都在设计未完成的状况下就开始进行实作。当发生问题时，就交由工程师们自行协商处理，整合失败时，就互相推诿责任。一旦项目经理放任这样的工作模式在团队发生，后果将不堪设想！本来是为了赶进度，而压缩设计工作的时间和质量，最后的结果就是必须付出更多的时间来收尾。就算运气好、顺利结了案，但究竟还有多少问题存在，谁也没有把握！要让这样的产品上市，我想项目经理每天都得过着提心吊胆的日子吧。毕竟客诉对研发团队的商誉与士气是有很大杀伤力的。

04-11　冲突不断：协同作战有多难？

菜鸟：“长官，今天怎么又垂头丧气的，又被质量系统部门的人XX了吗？”

PM：“看你一副幸灾乐祸的样子，我被XX你很开心吗？质量系统部门的人也没那么闲，我们公司那么多项目，我管理的项目至少还有我自己的质量规范，其他鬼混的PM一堆，我算是公司里的模范生了。”

菜鸟：“报告长官，如果你被XX我绝对是感同身受。我是想知道目前这个项目不是已经渐上轨道了吗？还有什么事情值得您烦心呢？”

PM：“你也知道像我们这种嵌入式系统项目牵涉的单位组织那么广，常常会面临文化上的差异，当事情忙起来或出问题的时候，各单位的争执与冲突在所难免。而项目经理还有两件重要的任务——人力资源管理与沟通管理。有时候，我单单处理这些mail就要花掉一整天，当冲突点还没扩大之前就要积极处理，否则，到最后沦为意气之争就麻烦了。刚刚我们的RD和客户为了争论一个功能的困难度，差点吵起来，工程师可以放话：‘如果你们真的不相信这个功能在短时间无法实现，那就把我fire算了！’。对客户无理的要求我也很不愉快，但我可不能动不动就发火，我必须同时顾虑到双方的心情，处理这种事情确实蛮让人心烦的。

更麻烦的是发生多个单位乱斗的状况，此时项目经理必须很快找到问题的重点，一一突破。其实，管理一个嵌入式系统项目最困难的部分不是技术，而是人。我大概说一下可能会发生的冲突点。”

■　Project Manager vs Functional Manager：在本章的《第4-1节：动手之前：确定项目的执行原则》我们讨论过，在一般研发组织中，参与项目的工程师会有两位直属老板，即项目经理与技术部门经理，两者的任务性质并不相同。项目经理的责任是让项目顺利且如期结项，而技术部门经理则必须考虑到人员管理以及该单位长远的发展方向，两者难免会对某项目的人力配置与研发方向有完全不同的看法。对工程师来说，最令人不知该如何是好的状况不外乎两位老板分别对他下了不同的指令，或两位经理在会议中公然争执起来，要求工程师选边站。一般企业的解决方案，是让项目经理也可以对工程师绩效考核，占的比例可能与部门经理不同（如项目经理：部门经理=40%:60%）。但这样并没有解决两位经理间的矛盾，工程师依旧可能陷入老板们的冲突之中。根本的解决方案是企业必须严肃对待项目启动阶段的‘项目授权书’，这份文件会明确规范组织对这个项目的支持程度，承诺授予项目经理哪些权限、可调用哪些资源等等。唯有一开始就说清楚游戏规则，才能减少项目经理滥权、部门经理阻扰项目进行等现象发生。即便发生冲突，大家也有个评断是非的依据。

■　客户vs厂商：虽然说客户至上，但研发团队接项目的目的无非为了赚客户的钱，若对客户服务到累死自己的团队，等项目结束后，团队重要成员陆续离职，然后公司又赚不到钱，这就不是信息业，而是服务业或公益事业了。研发团队和客户难免会发生冲突，最常发生的就是客户随意更动规格，却又无力评估规格更动对既有架构带来的冲击，此举最易引起研发团队的反感。另一方面，客户也常对研发团队进度落后以及层出不穷的bug感到不耐烦，假如一开始规划与设计的工作做得不扎实，到后期项目管理已然失控，开给客户的支票一再跳票，双方的关系也会越来越剑拔弩张。

■　软件vs硬件：就笔者的观察，软件与硬件工程师几乎是两个不同世界的人，文化、语言、思考问题的逻辑都不相同，唯一相似的特性就是对另一个领域怀有恐惧，软件工程师害怕看原理图、用示波器，硬件工程师害怕任何和程序有关的东西。当然，并非所有的工程师都有这种心理障碍，但这的确是目前台湾地区信息电子人才普遍存在的问题。

因为文化与思考方式的差异，对同一个问题可能有不同的解释，硬件认为是软件修改造成的副作用，软件则认为此次的修改根本简单的不得了，怎么可能会是软件的问题？双方都无法理解对方的想法，再加上可能隶属于不同单位或公司，于是花了大多的时间和精力去发mail，证明问题不在己方，却不愿花时间去沟通，去协同解决问题。此时，就需要一个的德高望重、软硬兼修的人将相关人等聚集在一起，逐一解决争端。

对PM来说，谁对、谁错没那么重要，找出问题并解决它才是第一要务，而且万万不能轻忽维持软硬件工程师合谐工作气氛的重要性。

■　研发人员vs规格制定者：前面章节已经深入探讨规格更动对项目的影响，在此不再赘述。通常制定规格的人不见得有技术背景，所以无法想象为什么“小小的规格更动”会对工程师带来这么大的困扰，这就是为什么研发人员和规格制定订者总是不断争执的原因。产品规格的好坏应该取决于使用者的接受程度，某些规格制定者有一个很要不得的毛病，认为不断的改动规格是他们正在进行“设计工作”的证明，万一这个设计规格的人就是客户，那研发单位简直麻烦大了！另一方面，工程师的毛病则是懒得用“一般人”的语言来和非技术人员沟通，再加上一直处于高度工作压力之下，所以总是给人火气很大的印象。

■　国内单位vs海外单位：无论规模的大小，台湾地区厂商普遍都会在大陆设立研发团队，而且生产基地几乎都在大陆，即便语言相通，文化差异往往会造成许多误会、冲突与推诿责任的现象。此外，跨国合作的状况也不少，可能客户是外国厂商，或使用国外software house所开发的功能，或交付海外工厂生产等，许多项目成员因为恐惧用英文或日文沟通，就索性不沟通。项目主管绝对不能任由这种事情发生而不处理。

■　系统整合者vs外包厂商：产品开发的进度都极为仓促，开发团队通常无可避免的必须把某些业务外包出去。一般来说，需要外包的业务有两种，一是人力密集的工作，二是特殊领域的技术，如字型、手写辨识或语音压缩技术。不同的嵌入式系统开发项目各有其独特的硬件配置及系统设定，必须在外包项目启动时就告知厂商所有注意事项，否则整合时肯定会出问题。举例来说，若厂商不知道我们系统的CPU性能以及存储器容量，那么，它们提供的library就无法针对我们的CPU做优化，或者可能使用了过多的存储器当作buffer。

再加上这些厂商都把自己的关键技术当作压箱宝，所以只提供library。在无法取得source code的状况下，负责系统整合的人调试相当困难。而某些不负责任的厂商也会以‘某某公司用我们的library都顺利出货了，所以一定是你们系统的问题！’来推卸责任。遇到这种状况，项目经理必须在冲突爆发之前，应用其身为客户的power，要求厂商的工程师系统与整合者坐在一起调试，直到找出问题为止。

■　工程师vs测试人员：不管在哪个研发单位，测试人员和工程师几乎总是对立的，为了某个bug该不该改，而争得面红耳赤的情景屡见不鲜。无论如何，这并不是好现象，因为工程师和测试人员对产品开发而言一样重要（有人认为如果设计工作做得好的话，测试人员甚至比工程师还重要）。

当工程师写的程序被找出一堆问题，看到如雪片般飞来的bug sheets，感觉程序功力受到严重的质疑，心情当然好不到哪里去。但工程师必须要有‘测试人员是在帮助他们找出程序的问题’的思想，某个bug要不要改可以讨论，但实在无须为了被找出bug而迁怒测试人员，而测试人员则必须有系统、有方法地根据测试计划书执行测试工作，费尽心思找出user永远碰不到的bug并不会提升产品的质量，只会严重浪费工程师的时间，打击工程师的士气。项目主管绝对要避免工程师和测试人员的意气之争，毕竟大家都是为了产品好。

■　开发端vs工厂端：嵌入式系统研发团队和一般软件研发团队有个很大的差异点，就是前者很有机会要和工厂打交道[10]，而工厂和研发单位简直就是两个世界，不论文化、管理方式或处理问题的逻辑都大不相同。最常发生的就是某个版本在实验室测试没有问题，小量试产也没有问题，偏偏量产时不良率就突然飙高了！工程师会认为显然是生产线或工厂的外包厂商（如模具射出厂、ROM代烧公司）出了问题，而工厂则认为绝对是设计不良所导致。

这种问题是最难查的，而偏偏总是发生在量产时。责任归属事小，延迟出货则耽误商机，甚至会有违约的可能。笔者看过很多实际的案例，因为事情草率了，产线停下来之后，相关人等不是积极合作解决问题，而是各自写报告证明此事与本单位无关，甚至用mail对骂，最后各单位关系搞得很差、气氛弄得很僵，而问题依然存在。正确的处理方法应该是由项目主管立即协调各单位（必要时寻求高级主管的授权），明确的做好危机处理。除了要找出问题外，更要说明该问题的影响范围，并证明解决方式不会有副作用[11]。

■　QA/QC vs其他人：按照软件工程的精神，QC是利用各种工具与手段来控制质量；而QA则有一套方法来衡量产品的质量，并评估产品的质量是否已达到可出货的标准。所以理论上，产品质量的优劣应该要由质量单位负责才对，对于必须负责的单位自然应该赋予最高的权利。然而，实际上很多QA、QC一方面不了解质量人员的职责，都把自己当作测试人员，做着和测试人员相同的事情；另一方面，却恣意运用质量单位本身的权力，甚至只凭自己喜好，要求研发人员改这改那[12]。

这样的后果让质量无法受到控制与保证，除了因为没有贯彻执行质量系统的方法，修改程序可能造成的副作用也没有经过评估。偏偏最后产品质量出了问题，老板通常不会怪QA、QC，反而工程师成了代罪羔羊，虽然程序或系统有bug是不争的事实，但往往不专业的QA、QC才是项目质量低落的主因。不然，公司要QA、QC做什么？

这样的文化普遍存在业界，几个项目run下来后，研发单位和QA、QC的关系自然形同水火，每件项目都背黑锅，工程师不离职已是万幸，怎么可能对工作还存有热情？身为公司主管，绝对不能让有权无责的文化在公司或项目中蔓延，管理人员、研发人员与测试人员都需要专业，当然，负责为质量把关的QA、QC也不能不具该领域的专业素养。

项目管理知识体系认为项目冲突的7大来源如下，很多人认为最后一个“人性”才是造成项目冲突的最大原因。

■　Schedule（进度）

■　Project priority（项目或工作事项的优先级）

■　Resource（资源分配）

■　Technical opinions（技术相关因素）

■　Administrative procedure（管理流程）

■　Cost（成本）

■　Personality（人性或个性）

要避免冲突几乎是不可能的，每个人都应该自省‘我选择如何面对冲突？’这个问题。在项目的执行中，项目经理需担任协调者或调停者的角色，真诚且创意地面对各单位甚至每个人的差异，完整了解差异的复杂性及范围，且让涉事者都能朝新的解决方案推进。项目经理必须运用个人及专业技巧，把严重的冲突转化为合作的契机。此事说来简单，实际上却是困难重重，但无论如何，项目经理必须要有更宏观的视野，不能单凭个人好恶或组织赋予的权力做决定，如此只会加深项目中的矛盾和冲突。

最后列出项目管理知识体系中解决冲突的5个技巧，何时采用何种技巧，就视项目管理者的经验了。

■　面对问题（最好的方法）

■　各退一步

■　暂时搁置

■　折衷一异中求同

■　强迫接受（最不好的方法，但是情况紧急时可能最适用）

04-12　产品化

一般软件的开发成果通常是压成光盘或上传到网络让使用者下载，情况较为简单，而嵌入式系统开发的成果最终就是要产品化，也就是交付工厂生产，因此，嵌入式系统研发团队（特别是硬件、固件和结构工程师）无可避免要和工厂打交道。本节会列举各单位和工厂的互动关系，并对工厂制造产品的流程做一简单的介绍。

软件工程师必须交付给工厂的项目有：

■　用来烧录的Binary File

■　生产线专用自动测试程序与说明书

■　环境测试（如高温高湿环境运作100小时）专用的测试程序

硬件、电子工程师必须交付给工厂的项目为：

■　电子电路

■　电气工程规格

■　特殊规格测试说明（简单地说，就是工厂之前没生产过的功能，而工程师必须特别说明测试的注意事项）

结构工程师必须交付给工厂的项目为：

■　组合图（或称爆炸图，把所有零件一一展开）

■　特殊部品外型图（如非标准品的电池）

■　特殊加工组合图（之前该工厂没有作过类似的工序，工程师必须特别说明组装时的注意事项）

工厂工艺工程师与采购人员收到以上信息后，就会进行

■　加工线排定（有些零件进厂后，还必须经过加工才能进行组装）

■　组装线、测试线排定

■　制规（标准生产制作规范）制定

■　测试架设计

■　估价并计算工时

■　计算产能

■　教育培训（培训所有参与该产品制造的员工，包含操作员）

在软件final release之前，一般要经过几次的小量试产。试产除了可以验证软硬件与结构整合后产品的成熟度外，还可以提供予研发单位当实验平台或销售单位做市场调查，但试产的主要目的还是在测试制造流程设计有无疏失，大量制造时会不会出问题。

一般电子产品制造的流程为：

■　软件烧录

■　PCB[13]SMT[14]

■　PCB测试

■　PCB加工

■　PCB半成品测试（使用测试架）

■　组装

■　成品测试

■　包装

■　验货

一旦试产没有问题，工厂的QA人员会发出“量产同意书”，而客户也会发出正式的“允许量产通知单”，此时才能进入正式量产阶段。值得注意的是，开发或试产阶段出问题都还有机会和时间解决，而大量的制造意味着资本的投入，量产时出的问题才是真的恶梦，最常发生的状况是：

■　软件到量产前，还在持续不断地发布版本，新版本的测试时间根本不够，即便是范围很小的修正，都可能会引起副作用。

■　系统重新编译时，因为工程师的疏忽，可能使用到旧版本的程序，导致已经解决的bug再度发生。

■　有些软件（通常是驱动程序）的问题，真的要大批样品才会出现。产品制造和压光盘完全不一样，每台机器里有那么多零件，很可能某一批零件特性较差，而驱动程序没注意到这种状况。

举例来说，某个产品中有SDRAM，而SDRAM对timing是很敏感的。驱动程序在设定SDRAM工作timing时，通常都会设定的比较慢，先求稳定、能运作就好。但后来可能为了调整性能，就把timing调快，在试产的机器上一直运作的很顺畅，所有人就认为应该没问题了。偏偏到了量产时，买进来多批SDRAM里的某几批特性较差（相同型号，但不同批次），生产线上一直出现死机的现象。

这种问题真的很难预防，而且也无法向SDRAM厂商求偿（读者可以把这个例子想成超频使用SDRAM，基本上属于不正常操作）。所以在调整像是CPU、memory这些重要部件的效能时真的要很小心。

■　工厂为了降低成本，可能私下更换较便宜的零件或原料。莫非定律真是无所不在，自以为不会出问题，结果就是会出问题！笔者看过太多次工厂发生这种事情了。

04-13　无间道：项目可有close的一天？

菜鸟：“隔壁部门的产品不是今天正式出货了吗？苦难终于结束了，怎么他们每个人还是愁眉苦脸的？”

PM：“据我在主管会议中听到的，今天开始出货的是第一批10K，好像客户的质量单位对某些功能还是有点意见，为了赶在圣诞节前上市，所以先签了量产需求书，这叫做‘条件式允收’，也就是暂时接受的意思。但持续要求研发团队更改许多已知的bug，一周后再出版本，然后再测一个星期。如果没有问题的话，两周后第二批的量产就用新的版本。”

菜鸟：“如果一周内所有的bug还是解不完呢？或一个星期后的版本，又被找到新bug呢？难道继续做下去吗？这个产品看来可以卖很久吧？那么，项目何时可以结案？负责的项目团队什么时候才能解散归建？”

PM：“这就是无间道啊！我看他们有得忙了。客户通常都不会爽快的结项，总认为改的越多，功能就会越强，质量就应该越好，这绝对是一个错误的思想！软件中的bug是找不完的，当系统质量已经趋于稳定时，任何不恰当的修正要求，都可能造成意想不到的副作用。届时我们收到的就不是bug sheet，而是客诉单。”

菜鸟：“我们的项目也会这样吗？”

PM：“嗯～在这个生态环境，每个人都高喊着质量至上，但真正懂质量的恐怕没几个人，恐怕我们也逃不出这个宿命吧！我手边就还有几个项目还没结，公司也不允许我把过多的人力配置在该结未结的项目上。所以在量产后，我就会开始内部的结案流程，把所有文件、开会记录、程序及硬件电路图等汇总归档，然后召开close meeting，总结这个项目的优缺点，以及我们从中学得了什么技术或教训。当客户再有新需求出现时，要让我召集相关同事密集工作个几天，并经过完整测试几乎是不可能的任务，因为每个人都有其他的事情在忙了，这样改出来的东西会有什么质量可言？”

菜鸟：“我知道实际上是这样，似乎身为项目经理的你也无法解决这个难题，难道全世界的人都是这样run项目的吗？应该还是有什么可以用来解决这种困境的方法论吧！”

PM：“我认为如果开发单位已按照规格完成开发，且质量经专业评估已在某个水平之上，就应该做正式结案的动作。此后若有修改需求，就发起另一个项目，这个新项目也应该照规定来做，规划、设计、开发、执行、测试、品管等步骤缺一不可！然而，一般客户都宁愿让项目苟延残喘，漠视项目人员已经解散的事实。”

客户或品保人员总是不肯相信“不可能开发出没有bug的软件”这件事，然而，质量系统本来就不是在追求完美无暇的产品，而是要求产品开发的过程必须根据一定的程序，好让产品的质量是可以被控制的。

最害怕的就是在项目后期或量产后才要求修改程序或设计，因为通常已经没有足够的时间再做一次详细的测试，只能根据工程师评估的影响范围，做局部的测试。现今的软件系统这么复杂，工程师对程序进行修改，造成副作用其实在所难免。在这个阶段修改程序简直是拿质量开玩笑！如果还用这个没有经过完整品管的版本出货，那几乎就是在冒险。而偏偏最该有这个思想、负责决策的人（如：客户、品管人员）往往陷入软件应该会越改越好的沉思。实务上，具有一定成熟度的软件，通常总是越改越烂的！

此外，结案是项目管理程序的5大步骤之一，还是有许多工作要执行，最重要的就是经验累积。如果公司能把每件项目的合约、运作流程、技术文件、会议记录、bug sheet、质量文件等都有计划、有条理地整理归档，这对将来项目的运行绝对会有无可估量的帮助，新任的项目经理也可以减少学习摸索的时间。可惜的是，笔者看到台湾地区大部分的研发单位都不是如此，甚至也不是所有的日本或美国公司都把项目结案视为关键的事项，经验若无法累积并文件化管理，则人员的流失将很可能对公司或团队造成毁灭性的影响。

[1]　功能型部门经理（Functional Manager）：如固件组、硬件组、美工组、测试组的主管。当某个人员调派给某个项目时，在项目运行期间，就应该接受项目经理（Project Manager）指挥。

[2]　当然此时可以写些程序或做些实验来验证设计的可行性，但真正的开发工作绝对应该在设计完毕后才能展开。否则到后期才要更动系统架构，将会对质量、士气，以及进度管理造成莫大的伤害。

[3]　为了降低成本以及简化设计复杂度，用于嵌入式系统的CPU通常某些PIN脚会有一个以上的用途，这种设计方式普遍存在现代的SoC中。在这样的平台上设计系统，固件设计人员必须更密切的与硬件人员沟通，以便弄清楚每一根PIN脚的确切用途，驱动程序才能做正确的设定。

[4]　你可不理会这个例子的技术细节，只要知道同样的CPU与外部设备，如果硬件设计人员选错用以控制外部设备的PIN脚，可能会对固件工程师造成极大的困扰。

[5]　不包含修改电路与Layout，重新送洗一次板子，最快也要一周才能收到裸板。硬件工程师还要手工焊接，硬件板子调试又得花掉几天的时间，不可不慎。

[6]　UML（Unified Modeling Language）是非专利的第三代建模和规约语言。UML是一种开放的方法，用于构建和编写一个正在开发的、面向对象的、软件密集系统的产品，UML已广泛的应用于许多大型软件开发项目。在对大规模复杂系统进行建模方面，特别是在软件架构层次，UML已经被验证为相当有效的方法。

[7]　请参考《附录C：用C来实作面向对象的概念》。

[8]　磁簧开关（MR Sensor）：可以检测磁场靠近或离开的芯片，通常用来实现产品盒盖开关时自动开机和关机的功能。

[9]　PMI将项目管理分为五个阶段：发起→规划→执行→监督与控制→结束。

[10]　软件人员至少必须准备自动测试程序，让产线的操作员在每一个阶段可以挑出瑕疵品，而用于产线的测试程序操作方式要简单、测试结果要明确。此外，产品要经过多次试产才可以正式量产，试产阶段可能发生许多问题，所以试产不单是硬件人员的事，结构和软件（固件）工程师也常常要进工厂，直接分析并解决在线发生的问题。

[11]　在《第18-3-3节：莫非定律：量产才出现的bug》有一个实例，你可以先行参考。

[12]　项目管理理论中特别指出，必须赋予QA人员有越级上报的权利与机制；但实际执行时，QA活动仍然应该由项目经理做第一线的管控。其实，项目中的大小事情都可引申为与质量有关，若QA动辄越级上报，则会对PM的管理造成困扰，甚者大大影响团队的士气。

[13]　PCB（Printed Circuit Board）是电子组件的支撑体，也是电子组件电力连接的提供者。由于它是采用电子印刷术制作的，故被称为“印刷”电路板。简单地说，就是没上零件的电路板。

[14]　SMT（Surface Mount Technology）是表面粘着技术的简称，简单地说，SMT在组装厂是一台上微小零件或是精密零件的机器，这种零件因为很精密或很小颗（有时只和芝麻一样小），无法用人工方式来插件，所以需要用自动化SMT机器将零件粘着至PCB（印刷电路板）上。

Chapter 5

实作你的第一个嵌入式系统

时间：05月01日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

菜鸟：“我到公司报到差不多3个月了，天天都跟着您开会，虽然项目管理的思想学了不少，但心里总觉得不踏实，对如何着手进行嵌入式系统开发完全没有概念。简单地说，面对这块CPU厂商提供的评估板（Evaluation Boards），如图5-1和图5-2所示，我真不知道该如何开始编程。”

▲ 图5-1　评估板实例（1）

▲ 图5-2　评估板实例（2）

PM：“写程序？设计规划都还没做完要写什么程序!”

菜鸟：“总要让我练习一下嘛！免得需要时我帮不上忙，我真的很想知道如何写程控这块板子。”

PM：“也有道理，既然目前所有工程师都还在编写设计文件，而你负责的小模块文件也完成了，距离所有设计文件完成还要一段时间，这段时间就先让你接触一些简单的开发工作吧！我出个题目给你，一周后，你要证明给我看，你的程序确实可以在这块板子上执行。”

菜鸟：“我就是不知道如何在这样的平台上写程序啊！我以为您会找个同事教我。”

PM：“同学～我们是私人商业公司，并非教育单位，现在每个人都在忙，谁有空教你这个新人？我会找个固件工程师让你问，但记得一件事，在问问题之前，请自己先思考一下。你知道，工程师在忙的时候对蠢问题是很没耐性的。”

在详述系统设计与开发技巧之前，本章将描述一个简单的嵌入式系统开发的实例，可以想象在非PC的硬件板子上写一个最简单的“Hello World!”程序，希望你借此可以先对嵌入式系统的开发工作具备初步概念。即便是一个简单的“Hello World!”，软件工程师该做的工作还真是不少呢！

此外，这个实例和真实状况也有点差距，在此我们假设硬件是正确且稳定，但实际状况往往是固件工程师一边写程序，一边帮忙硬件调试。当程序执行结果错误时，有时候不一定是软件的错误，这种状况在嵌入式系统项目初期更是常见。

05-01　嵌入式系统的开发环境

时间：05月02日　 地点：办公室一隅　 登场人物：菜鸟、固件老鸟

菜鸟：“学长好，PM老大让我有问题的时候来请教您，不知道您现在方便吗？”

固件老鸟：“OK！目前我们组员正在study CPU厂商提供的sample code，而硬件工作人员也正忙着设计Target Board[1]，所以现在还没那么忙。PM老大前几天已经知道我要好好‘照顾’你，刚好EV Board[2]（Evaluation Board）我还蛮熟的，有什么问题你就问吧！”

▲ 图5-3　Target Board及Real-Size-Board

菜鸟：“是这样的，我手上有块CPU厂商提供的评估板，插电后板上的LCD会显示正在执行DEMO程序，这表示硬件应该是OK的。另外还有一台叫做ICE[3]的设备，其操作架构如图5-4所示，好像要利用这台机器才能下载自己写的程序到板子上。此外，我读了许多CPU厂商提供的文件和sample code，但还是不知该如何下手，一行程序都写不下去，这种感觉蛮令人沮丧的。”

▲ 图5-4　ICE操作架构图

固件老鸟：“难怪！因为你开发环境都还没架设好，就想着如何写程序，难怪会无法下手。想想你要在PC上写Java程序，需要准备什么？”

菜鸟：“这我就熟了，要写Java程序需要Java Virtual Machine与Java compiler，只要到Sun下载并安装Java SDK即可，通常我还会安装Eclipse这个IDE（Integrated Development Environment；集成开发环境），这样管理程序项目和调试都比较方便。假使我是要写网络程序，如JSP或Servlet，我还会在自己的机器上安装Apache Tomcat，当成测试平台。”

固件老鸟：“思路很清楚嘛！计算机系统不就是这么一回事吗？套用刚刚Java的思想，试着推想在这块板子上写程序需要架设什么样的环境。”

菜鸟：“我大概有点头绪了，把这个对比画成一张表，请您帮我看看想得对不对。”

固件老鸟：“所以除了 ‘下载’这项动作外，嵌入式系统的开发似乎也没有特别复杂的地方。就利用这张表格，你可以试着将如何让程序在评估板子上执行的所有步骤列出来吗？”

菜鸟：“谢谢学长循循善诱的指导，我先以不是那么专业的用语试试看。”

Step01：在PC上安装开发环境（包含cross-compiler、debugging tool、editor、ICE的驱动程序等）。

Step02：程序编写。

Step03：编写makefile（指示要做出可执行文件之所有步骤的细节，通常包含编译、连接及可执行文件格式转换，如图5-5所示）。

▲ 图5-5　制作（build）嵌入式系统软件的程序

Step04：build程序（执行makefile中的所有指令）。

Step05：利用ICE或其他工具将程序下载到机器上，并测试程序的执行结果。

Step06：如果程序无误，用烧录器将程序烧录到ROM中，则以后机器上电，就会执行新程序。

固件老鸟：“差不多就是这样，接下来先别急着写程序，直接用CPU厂商提供的sample code和makefile，把你刚刚列出来的步骤从头到尾做一次看看。”

这一节只需要了解嵌入式系统开发环境的思想即可（如图5-6所示），相关细节可参考《第7章：构建良好的嵌入式系统开发环境》。

笔者在这一节要强调的重点是：嵌入式系统也如同一个计算机系统，虽然其开发环境的功能不如PC程序的开发环境（如Visual Studio）方便强大，但基本思想是相同的，都需要程序项目管理工具、compiler、editor、debugging tools以及测试的硬件平台。此外，程序编写的基本技巧和思想也没有特殊之处。初接触嵌入式系统开发的工程师只要弄清楚这个思想，再请CPU或评估板厂商详细示范一次开发环境的使用，应该都能很快进入状况。

▲ 图5-6　嵌入式系统开发环境简图

另外一项重点是：不同的产品开发项目可能会使用不同的CPU，所以就会有不同的开发环境。一位优秀的嵌入式系统开发工程师不会花太多时间去熟悉一个新的开发环境，基本上只要会用就行了，需要什么功能再去查data sheet或直接问厂商即可。最快的方法就是：利用厂商提供的sample code，试着自己编译、下载、执行并使用debugging tool的各个功能，如单步执行、设断点、观察CPU寄存器等。接着以厂商的sample code为基础，开始修改并新增自己的程序，再逐步扩展为自己的程序project[7]。

05-02　无痛起步：善用Sample Code

请注意！你可以先不理会以下提到某些文件的内容，在后续章节会陆续说明。在这一节，笔者想要传达以下两个信息。

■　一个完整的嵌入式系统至少应包含多少种类的文件，哪些文件是程序员必须编写的，哪些是由工具（Cross Tools）产生的。

■　一个最简单的嵌入式系统，应该包含哪些程序模块。

菜鸟：“我从CPU厂商提供的sample code中找到一个比较简单的project，程序功能是利用CPU内部的定时器机制（Timer），每隔一段时间就去开关LED一次，这个project中包含以下文件。”

■　程序部分（由程序员编写）：

□　vector.c：中断矢量表

□　boot.c：启动程序

□　main.c：应用程序的主程序

□　drv_timer0.c：Timer的驱动程序

□　drv_LED.c：LED的驱动程序

■　其它文件（由程序员编写）：

□　test.mak：makefile

□　test.lds：Link Script（提供给Linker的信息，描述程序要被寻址[8]到哪个地址去）

□　build.bat：执行makefile

■　执行build.bat后产生：

□　test.elf：build后产生之ELF格式的可执行文件

□　test.bin：由ELF格式转换而来的binary文件

□　test.err：错误信息文件（存储编译及连接时期的输出信息）

□　test.map：连接后产生的文件，记录程序中每个函数、变量的地址

□　vector.o、boot.o、main.o、drv_timer0.o、drv_LED.o：.c文件编译后产生的object file

菜鸟：“我把build产生的ELF文件通过ICE下载到板子上，并让它执行，发现LED确实会闪烁，这应该代表程序已经可以正常执行了吧！我决定先不管makefile、link script，还有可执行文件格式的细节，就算暂时看不懂这些文件的内容，只要照着去做，就可以把我自己写的程序加到project中，所以我想试着先把几个程序文件（.c文件）看懂。虽然行数不多，但我看了半天，还是有些懵懵懂懂，许多程序代码不知道是干什么的，而它居然是可以运行的！”

固件老鸟：“初学者通常都搞不太清楚makefile这些东西，尤其是link script，因为写PC程序时根本不用理会重寻址这件事。所以你的学习方向是对的，先把可以新增、修改、测试程序的环境架设起来，接着弄懂程序的架构，然后再以这个project为基础去实现你想要做的功能。至于程序部分，你应该要先弄清楚这个CPU规定的启动流程。我这样问好了，CPU重新启动（Reset）后，会到哪个地址执行第一个指令？”

菜鸟：“CPU使用手册中似乎写得不是很清楚，我只知道它规定我们程序的开头必须被寻址到0xC0000，但根本不知道0xC0000这个地址的意义何在，而且我们程序的开头[9]是vector.c（如程序代码5-1所示），其中除了一个叫做dummy的函数之外，就只有一个数组。我实在是迷糊了，如果0xC0000是CPU取得第一行指令的地址，为什么sample code会把一个数组寻址到这个地方？”

固件老鸟：“恐怕你没把CPU的data sheet看清楚吧！在告诉你答案之前，我们先来看看一般系统的启动流程。”

5-2-1　计算机系统启动流程

固件老鸟：“一般嵌入式系统的启动流程为

Step01：CPU会到特定的地址获取第一行指令来执行，实际的细节有两种。

□　CPU重新启动后，将其PC[10]（Program Counter）寄存器设为特定地址，只要user的程序确实存储在这个地址，就可以正确被执行。

□　CPU重新启动后，会将CPU中存储中断矢量表[11]地址的寄存器设为某特定地址，接着引发‘RESET中断’，所以程序员只要把中断矢量表存储在这个地址，并指定‘Reset中断的处理程序’为自己写的某个函数（如：boot()），则该函数就可以在开机后被CPU执行（详细流程如图5-7所示）。

Step02：User程序开始运行后，会先对CPU做初始化的动作。

Step03：将程序的数据段从只读存储器（ROM或Flash）载入到RAM中。

Step04：CPU初始化完毕后，紧接着会初始化应用程序用到的硬设备。

Step05：初始化各个子系统，如嵌入式操作系统（RTOS）、动态存储器管理、图形界面系统等。

Step06：执行应用程序的主程序。

固件老鸟：“如果先不管Step03、04、05，你对这样的流程有疑问吗？”

菜鸟：“可以再说明一下Step01吗？虽然这些名词以前在学校都学过，但实在无法把这些名词和实际的板子联想在一起。”

固件老鸟：“不管是哪一种CPU，它总是会规定一种启动的方式，这样要在它上面写程序的人才会知道要把程序放（寻址）在哪里。写应用程序的工程师可以不清楚这些细节，但是我们做固件的人就不能不弄清楚CPU启动程序的来龙去脉。以我们现在用的这个CPU为例，它是采用上述步骤一的第二种方式。意即CPU启动后会把0xC0000当作存放中断矢量表的地址，并引发Reset中断，所以只要把我们启动程序所在的地址（如图5-7的boot()所示），存在中断矢量表中代表RESET中断的entry即可（详情如图5-7所示）。”

▲ 图5-7　CPU通过RESET中断的启动流程

不同的CPU可能会有不同的启动方式，基本上，不外乎本节提到的两种。

■　指定特定地址为程序启动地址。

■　通过中断矢量表存储程序启动地址。

前者比较简单，所以本章会以后者当成解说的实例，无须拘泥于以下将列出的程序代码，只要知道一个最简单的嵌入式系统所应包含的程序模块各代表什么意义即可。实际上，从CPU的data sheet以及厂商提供的sample code，应该就可以得知该CPU的启动细节。

5-2-2　中断矢量表

菜鸟：“现在我终于知道，为什么link时要把vector.c中的那个数组寻址到0xC0000了，原来我的主程序是历经这些步骤才被执行到的。中断矢量表和“中断处理程序（Interrupt Service Routine；以下简称为ISR）”的概念，我在学校修汇编语言与计算机组织时曾经学过，只是从没想过如何用程序来实现它。”

固件老鸟：“没错！有时是很难想象如何实作教科书中所提到的道理。没关系，我们先来看看计算机系统的程序是如何执行的，如图5-8所示，看看你能否说出一般函数和ISR有什么不同？”

▲ 图5-8　计算机系统程序执行流程

菜鸟：“这个我懂呀！ 一般程序都是循序执行的。如图5-8所示，CPU一定要执行到main()的第20行，才有可能去调用sub_fun()，而中断则可能在任何的时间点产生，例如，使用者可以在任何时候按下键盘的某个键。此时不论CPU下个要执行的指令是什么，CPU都会先记录目前的状态，然后参考中断矢量表，接着去执行键盘中断的处理程序（Keyboard ISR）。等ISR执行完后，CPU会取回中断之前的状态，然后返回被中断的地址继续循序执行。”

固件老鸟：“不错不错，思路很清楚。接下来，我和你一起来看看中断矢量表是怎么实作的，如程序代码5-1所示。”

程序代码5－1：中断矢量表范例

固件老鸟：“一般的，中断矢量表的实作就是一个C的数组，当然也可以用汇编语言写，但意义是一样的。实际上，CPU只有存储器地址的思想，当然不知道C的数组是什么。但说穿了，所谓的中断矢量表就是从某个地址开始，每4个Byte[12]为一个单位（entry），每一个entry记录一个函数的地址，就是这么简单。根据这个定义你再回头看vector.c，是不是就比较清楚了？”

菜鸟：“这样我懂了，再请问一下，中断矢量表（vector[]）中填了一堆dummy，又是代表什么意义？”

固件老鸟：“CPU（或中断控制器）会为每一个中断编一个序列号，而中断可以分为两个种类，一种是CPU自行产生的内部中断（又称例外；exception），以及由外围设备产生的外部中断。以我们的程序为例，第0号中断就是Reset中断（请看程序的注解），第1号中断是‘除以0’的中断[13]，这两个都属于内部中断。而第6、7号中断是port中断，表示CPU的某一根PIN脚接到某个装置，若这根PIN脚发生了电位变化（如高电位（High）→低电位（Low））就会产生这个中断，这就是所谓的外部中断。

根据系统要实现的应用不同，通常我们不会用到所有CPU的中断，也就是说，我们不需要实现每一个中断处理程序。举例来说，Timer1 timeout时会引发第11号中断（请回头看一下vector.c），而我们的应用只会用到Timer0，所以根本不可能产生第11号中断，就算去实现第11号中断的ISR也是没有意义的。这就是dummy这个中断处理程序存在的原因，你可以看到它根本没做任何事情。

假使再深入讨论这个话题，没有用到的中断不但不应该产生，而且当其产生时，还应该视为一种错误，除了上述提过除以0的错误之外，例如，静电所产生的硬件噪声也可能引发不应该产生的外部中断，开发电子产品时这些状况都要处理。真正系统中的dummy()就不是那么简单了。经过我的说明，你搞清楚vector.c了没有？”

菜鸟：“我现在好像已经能了解这个sample code里每一个程序的意义了。在上述的中断矢量表中有两个不是dummy()的中断矢量，第一个指到boot()（第0号中断），CPU启动后会产生RESET中断，而boot()就是Reset中断的ISR，所以CPU启动后第一个被执行的程序就是boot()。另一个不是dummy()的中断矢量则是指向drv_isr_timer0()（第10号中断），当Timer0每隔一段时间timeout时，CPU会引发第10号中断，所以drv_isr_timer0()会被调用。这个ISR要做的就是开关LED，以制造闪烁的效果。”

固件老鸟：“没错！只要你弄清楚计算机系统的启动流程，这个程序就是这么简单。没关系，我今天没那么忙，我们把其它的程序也一起看过，看看还有没有其他问题。”

5-2-3　Boot程序（启动程序）

固件老鸟：“接着我们来看启动程序Boot.c，如程序代码5-2所示。由刚刚的推导，你应该已经了解为什么它是CPU启动后第一个被执行的函数了。初学者在看这个程序的时候，先不要理会被调用的各个函数的详细内容，只要知道这些函数的用途即可，这个程序的注解写得还蛮清楚的，应该理解不会有困难才对，你要学习的重点是boot程序在这个阶段到底要负责做什么，了解吗？”

菜鸟：“其实看注解就知道了，CPU启动后，第一个被执行的程序至少要负责以下的动作。

Step01：设定某些重要的CPU寄存器，特别是堆栈指针寄存器[14]（Stack Pointer；简称SP）与状态寄存器[15]（Status Register），如图5-9所示。

▲ 图5-9　ARM的状态寄存器（PSR）

Step02：CPU各部分功能初始化。

Step03：系统初始化。

Step04：调用应用程序的主程序。

Step05：结束（通常应用程序的主程序不会返回boot程序）。

程序代码5－2：Boot程序范例

固件老鸟：“基本上没错，关于这个程序有什么问题要问的吗？”

菜鸟：“问题可多呢。

Q1：函数定义前面怎么有‘__interrupt’这个东西，它应该不是标准C语言的关键字吧？

Q2：那个asm（“….”）好像叫做Inline Assembly吧，为什么要在C程序里面夹杂汇编语言？

Q3：boot()的最后一行是调用exit()，我想不通的是，一般PC程序结束就是视窗被关掉，然后回到Windows，像这样的系统，调用exit()是回到哪里去？”

固件老鸟：“OK！我一一来回答你的问题。”

A1：‘__interrupt’并非C语言的标准，它是这个Cross-Compiler自订的关键字，写在代表中断处理程序的函数定义前面。这个关键字并不是只用来区别一般函数和ISR那么简单，实际上，ISR的开头和结尾要做的动作和一般C语言函数不一样，如表5-2[16]所示，所以compiler会对有加上‘__interrupt’的函数进行特殊的处理，即产生不同的汇编语言指令。对程序员而言，只需要加上这个关键字，就无须理会细节，可以同写一般函数的方式写ISR。总之，你现在只要先记得，写ISR时要加上这个关键字[17]，之后我会再教你其中的道理。

表5-2　一般函数与ISR的不同点

一般函数

中断处理程序（ISR）

开头

只需将返回地址存在堆栈（Stack）中

1．_必须将返回地址（发生中断的地址）以及CPU状态寄存器存在堆栈中

2．存储所有CPU寄存器的值到堆栈中

结尾

使用CPU的ret指令（return）

1．从堆栈中回存所有CPU寄存器的值

2．使用CPU的iret指令（interrupt return）

A2：通常我会用汇编语言来写这个启动程序，因为必须去设定CPU内部的寄存器，而这些动作是很难用C语言去写的[18]。在你这个sample code中是用C来编写启动程序，所以只能用内嵌式汇编语言（Inline Assembly）来设定堆栈指针与状态寄存器。其实汇编语言并不是那么难懂，程序中这两行的意义是：

目前看不懂这些汇编语言程序没关系，通常我们不会花太多时间去学某个CPU的汇编语言，因为每个CPU的汇编语言写法都可能不同，我们做固件的哪有那么多时间通通弄熟？将来我会教你，我们都是怎么写汇编语言程序的。

A3：嵌入式系统基本上是不结束的，会持续运行直到电源被切掉为止，在此期间都得让CPU有指令可以执行。所以在嵌入式系统的主程序中一定会看到一个无穷循环（有些书本称之为super loop），循环内一直执行着某件工作。例如，水族箱的恒温系统，一直测量温度，并持续改变加热器的温度，将水温维持在一定温度。要不就是让系统进入省电状态，直到有外部输入产生中断（如按下键盘或触控式屏幕），CPU才会恢复全速模式，等处理完外部事件后，就又进入待机模式。一般的手持式装置为了延长电池寿命，应该都是这样的做法。”

菜鸟：“您说的没错，主程序的最后面的确是一个无限循环。所以当启动程序（boot()）将控制权交给主程序（main()）后，就永远不会返回boot()。”

固件老鸟：“没错！boot()最后调用了main()，main()可说是这个嵌入式系统的应用程序的主程序，接下来我们来看看main()，如程序代码5-3所示。”

5-2-4　主程序

菜鸟：“我终于渐渐搞懂boot程序和应用程序的概念了。当CPU顺利启动，且硬件已初始化完毕，接下来就是将控制权交给应用程序。这个范例程序的目的是让LED持续固定每100ms闪烁一次，main()里的所有程序就是为了达到这个目的。”

固件老鸟：“这种说法基本上没错，但这是因为现在讨论的这个例子还算简单，否则在boot程序和应用程序间还有一层系统程序，你可以把这一层想象成操作系统加上驱动程序。main()这个程序比较简单，应该没有问题吧！”

程序代码5－3：主程序main()范例

菜鸟：“程序中只对Timer和LED做了初始化动作，接下来启动Timer，然后就是一个无限循环，循环内什么事都没做。原本我觉得很奇怪，为什么无限循环内没有开关LED的动作，也没有跳离的机制，经过刚刚的讲解，我现在应该已经弄懂了。LED开关的动作会在定时引发的Timer中断处理程序中执行，而嵌入式系统是不会结束的，所以main()的结尾就是一个while（1）这样的无限循环。

这样程序的写法对我这种习惯写PC程序的人的确是一种震撼，一般我们都要避免写出跳不出来的无限循环，可是在嵌入式系统中的无限循环却是必须的。”

固件老鸟：“无限循环是很好用的技巧，在调试时也会用到，请务必牢记。”

5-2-5　驱动程序（1）：Timer

菜鸟：“我再归纳一下这个范例程序的运行流程，如图5-10所示”

Step01：程序的中断矢量表是一个数组，它会被寻址在CPU指定的启动地址。中断矢量表Reset ISR的中断矢量指到我们的boot程序中。

Step02：CPU启动后，会引发RESET中断，所以boot程序会被执行。

Step03：boot程序负责初始化CPU，并将控制权交给应用程序的主程序。

Step04：主程序仅初始化它会用到的硬设备，在本例中则是设定Timer与LED，最后则是一个无限循环。

Step05：在本例中，每100ms会产生一次Timer中断，即Timer ISR每100ms会被执行一次，由Timer ISR负责LED的开关，以制造闪烁的效果。

▲ 图5-10　本章范例程序的运行流程

固件老鸟：“最后让我们来看看驱动程序，如程序代码5-4所示。这个范例包含了两个驱动程序一drv_timer0.c及drv_LED.c。有时候，我们会把驱动程序分成两个种类，一种是会产生中断的，如本例中的Timer；另一种则是不会产生中断的，如本例中的LED就是其一。首先来看drv_timer0.c，先不管控制Timer的细节，只要看这个程序中各个函数的意义就好。看起来有什么问题吗？”

菜鸟：“这个程序是个比较简单的模块，它包含了三个API以及一个ISR，分别是：设定Timer、启动Timer、停止Timer及Timer中断处理程序。我想这个程序要注意的事项只有一个，就是在定义代表ISR的函数时，前头必须加上‘__interrupt’关键字，其它细节我去查CPU data sheet中有关Timer的相关章节就可以了。”

程序代码5－4：Timer驱动程序范例

5-2-6　驱动程序（2）：LED

菜鸟：“LED这个装置不会产生中断，驱动程序中包含的API更为简单，只有初始化LED、点亮LED及熄灭LED 3个函数而已，如程序代码5-5所示。我对各个函数代表的意义没有疑问，但函数内的编写方法我有几个地方看不懂。”

固件老鸟：“这个程序全部都是用标准C编写的，怎么会看不懂呢？”

程序代码5-5：LED驱动程序范例

菜鸟：“‘volatile’也是标准C的关键字吗？我以前从来没看过这样的写法。”

固件老鸟：“‘volatile’当然是标准C的关键字，通常只有在写驱动程序时才会用到，这也难怪你之前没看过这样的写法。你知道C compiler都会对程序做优化吧！举个例子来说，如表5-2所示。”

表5-3　volatile变量与优化

原始程序

volatile变量与优化

固件老鸟：“在一般的状况，C compiler会把无效的程序剔除，除了可以缩小程序的size，也能加快执行速度，可是如果某些状况，连续做3次A=1是有意义的，C compiler此举就可说是帮倒忙了！‘volatile’这个关键字的作用就是用来告诉C compiler，凡是定义成‘volatile’类型的变量，就不需对其做出上述的优化。”

菜鸟：“恕我资质愚钝，我实在想不出什么状况连续做3次A=1是有意义的？就算做了一百次，A的值还不就是1！”

固件老鸟：“一般的状况是这样的没错，但写驱动程序时可能就不是。假设A与B代表某个设备的寄存器，当它们的值是1、2、3时分别代表执行3种不同的功能，你再回头看一下这个例子。A=1做3次，表示我要这个设备执行功能1 3次；让B分别等于1、2、3，则表示我要这个设备分别执行功能1、2、3。如果你的变量不定义成‘volatile’类型，则A的例子只会执行功能1一次，B的例子则只会执行功能3，这就违背程序员的本意了，很多驱动程序的初学者都会犯这样的错误。”

菜鸟：“听起来是蛮神奇的，我从来没想过这样的事情，以后写驱动程序时会小心的。此外，我对dev_LED.c还有一个问题，我翻了CPU的datasheet，程序中那些数字0×300022、0×300023是代表CPU内的寄存器[19]，设定其中的某个bit就能控制CPU中的某根输出PIN，可是我实在看不懂sample code中的写法。”

？*(volatile unsigned char*)0×300022|=0×2

固件老鸟：“这样的写法确实对初学者有点复杂，首先看datasheet里对寄存器0x300022的定义，如图5-17所示，程序的目的就是要将P51设为High。如果把这行程序改写成下面的形式（如程序代码5-6所示），是不是就可以看懂了呢？”

▲ 图5-11　memory mapping寄存器定义实例

程序代码5－6：改写drv_LED_on()

菜鸟：“这样写我就看懂了，原来利用C语言的指针就可以操作CPU寄存器啊!”

固件老鸟：“这行命令用汇编语言写反而更简单明了，你有兴趣知道吗？”

菜鸟：“今天受的惊吓已经够多了，我想我只要会一种写法就好了。”

固件老鸟：“你说的没错，真的只需要知道思想就可以了，当你忘记怎么写的时候，再从sample code复制出想要的code来改即可。但是写驱动程序的人还是要稍微能看懂汇编语言比较好，有时候厂商提供的sample code就是用汇编语言写的。话说回来，无论如何你就给个面子，让我改写成汇编语言，参考一下嘛，只要两行程序喔！”

菜鸟：“……（心中OS：真是爱显摆！）”

05-03　如何确定程序有执行?

菜鸟：“终于把这个范例程序搞懂了，当彻底了解思想后，其实也不觉得有那么困难了。我整理一下程序和今天的笔记，应该就可以向老板交差了。”

固件老鸟：“架设开发环境是第一步，看懂程序是第二步，接下来还有许多技巧要学习。我问你，除了肉眼可以看到LED在闪烁之外，还能用什么方式证明你的程序正在正确的执行？如果没有LED，你如何证明Timer中断一直被触发？”

菜鸟：“我想到一个方法，用ICE调试时可以设定断点。只要在Timer ISR的程序中设断点，如果程序有停下来，就表示Timer中断有被触发；如果Timer ISR一直被调用，那么，就可以证明我的程序在执行。”

固件老鸟：“这的确是个好方法。但因为设定断点，程序就会停下，所以你无法证明你的时间中断确实是100ms产生一次。再想想，还有什么方法？”

菜鸟：“我想到了，但可能要改一下Timer ISR的程序。”

菜鸟：“如果我在以上函数的最后一行设定断点，程序开始执行即用秒表计时，经过一分钟后，如果会在断点停下来，就表示我程序的Timer确实是每100ms产生一次中断。”

固件老鸟：“这个方法也很有创意，利用增加重复执行的次数，来等比例计算每一次执行的时间，这个方法在我们做速度调整时常用到，你的确很有想法。但这个方法，时间还是会有点误差，而且非得挂上ICE才能设定断点，难道单机时就没有办法证明你的程序确实在执行吗？”

菜鸟：“我实在想不出来了。”

固件老鸟：“其实很简单，Timer ISR每100ms会变换P10这根CPU PIN脚的电位一次，所以应该会产生波长为200ms的方波（频率为1000ms/200ms = 5Hz），即可以预期在P10这根脚上测量到频率为5Hz的连续方波。走！我们到实验室去，我教你怎么用示波器测量这个信号。”

菜鸟：“真的有频率为5Hz的连续方波耶～这台机器真神奇（如图5-12所示）。”

固件老鸟：“使用示波器并非硬件工程师的专利，它更是固件工程师验证驱动程序正确性的最佳工具，以后你会常常用到它的。”

▲ 图5-12　示波器测量结果

05-04　标准C库函数可以用吗？

菜鸟：“我们的硬件平台有LCD，我想要打印输出一些字在LCD上，如果LCD上有个数字一直在累加，这样应该也可以证明我的程序正在执行。不知道我们的平台有没有printf()可以用？”

固件老鸟：“我换个方向来回答你这个问题。compiler的厂商为了吸引更多开发单位使用他们的产品，当然希望能提供标准C的库函数给程序员使用。如果你是compiler的厂商，你想要怎么实现printf这个函数？printf()有不定个数参数的问题，比较复杂，你如何帮助你的客户实现print_character()这个函数？只要打印输出一个字符即可。”

菜鸟：“打印输出一个字符还不简单，先为每一个字符建立一个bitmap，然后只要根据这个bitmap一点一点画在屏幕，就可以show出一个字符了。”

固件老鸟：“你的答案有两个问题。第一，要在‘屏幕’上画点，需要‘屏幕’的驱动程序吧？你怎么知道客户用的是哪一种‘屏幕’？第二，你有没有想过，客户的硬件平台可能根本没有你所谓的‘屏幕’？”

菜鸟：“这……怎么会这样，我完全没想过这样的事情。”

固件老鸟：“所以你还是没抓到嵌入式系统多样性的精髓所在。我们可以使用同样的CPU、同样的compiler来开发完全不同性质的产品，例如，PDA和Router这两项不同性质的产品，就可能使用同一种CPU，用同样的开发环境开发出来的。对Router这样的网络设备而言，你认为打印输出字要输出到哪里去？”

菜鸟：“输出到文件里吗？”

固件老鸟：“输出到文件是一个选择，通过网络传给某一台装有特定接收程序的PC，也是一种print out的方式。但问题是，compiler厂商如何知道你的硬件平台或应用是什么？还有一个很容易明白的例子，compiler厂商要如何实作动态存储器配置函数——malloc()、free()？他们根本不知道客户的硬件版子中的存储器状况，包含起始地址、大小等。

所以针对这些和硬件相关的标准C函数，compiler厂商就各出奇招。以malloc/free为例，通常会提供几个非标准的函数，让程序员设定存储器区块的信息后，才可正常使用malloc/free，但每家compiler厂商的做法可能都不一样。为了避免不确定性，通常我们都会自己实现print out以及存储器配置等功能。一来容易维护，二来比较容易根据我们产品的需求做调整，特别在做性能调整时，这些底层的函数常常是瓶颈所在。

此外，标准C中还包含了一系列文件操作的函数（fopen、fread、fwrite、fclose等），文件系统的复杂度比打印或存储器管理要难得多。所以不论compiler厂商支持到何种程度，我们还是倾向用自己的文件系统比较有把握。”

菜鸟：“所以像字符串操作（strcpy、strcat等）这些和硬件无关的标准C函数，就可以直接用啰！”

固件老鸟：“是的，只要你思路清晰，什么可以用、什么不能用应该就很容易推测得出来。不过你不用担心，compiler使用手册里都会清楚标明使用库函数时的注意事项。这和PC程序开发有很大的不同，嵌入式系统初学者一定要把这个思想弄清楚。”

菜鸟：“我想我搞清楚状况了，如果要实现像printf()这样的函数，至少要有我们平台的LCD驱动程序，也许团队中或CPU厂商已经有人做好了，我可以拿来直接用。假使没有人做过，那我就只能自己来，compiler提供的库函数是帮不上忙的。”

本节既然提到了标准C库函数，笔者想花点篇幅继续深入探讨。以下以memset()或memcpy()为例，说明嵌入式系统标准库函数的性能考虑。

现代程序开发工具的便利性与上个世纪相比，简直不可同日而语，在嵌入式系统开发领域也是如此。尤其在compiler与library的领域，因为GNU C/C++即其附属库函数质量日趋成熟，加上其开放源码与容易移植的特性（移植compiler到新的CPU还是有相当程度的困难与复杂度，但GNU C/C++compiler已成功移植到许多平台，相关技术文件与可参考的实例相当丰富），所以现在几乎所有新的CPU都会以GNU C/C++套件（tool-chain）当作其开发工具的基础，辅以GDB当作远程调试工具，再以免费或相当便宜的价格提供给开发商。

这对工程师而言算是一个好消息。一来使用不同CPU开发产品时，会减缓开发工具的学习曲线，而且GNU C/C++tool-chain附属的标准库函数，使得程序更容易移植到不同的平台上（其实这点带来的好处远比想象的多，试想网络上有多少open source的project是基于这些标准库函数的？这意味着这些共同技术资产都可能无痛移植到你的项目上）。再加上这些库函数都是open source，开发者可以根据产品需求以及平台的特性自行修改。

但使用如ANSI C、POSIX等库函数还是得小心。举例来说，在PC上printf()自然是输出到屏幕上，但在嵌入式系统上可就不一定了，有的平台连‘较明显’的输出设备（如LCD）都没有。再以malloc()、free()为例，库函数的开发者怎么可能会知道每个产品的存储器起始位置与大小？也就是说，不是所有标准库函数都可以直接用在每个平台上。

另一个应该注意的事项是性能问题。每个平台或CPU都有其特性，在许多应用上，要开发出在所有平台都具有优良性能的算法是不切实际的，再加上这些库函数最开始还是以PC当作平台，正确性最重要，算法的性能并非最重要考虑，PC上的CPU自然可以将其执行得飞快。

以下举一个实际上经常碰到的问题——memcpy（memory copy）及memset（memory setting）。

相信你对这两个函数都不陌生，有的应用需要大量的存储器传输，此时，memcpy()的性能就很重要了。举例来说，一般要在LCD上显示字符或图画都是采用frame buffer，即在存储器中配置一块Video RAM，LCD上每一点都会对应到特定的存储器空间，且连续的点（X轴）在Video RAM中的存储器位置也是连续的，每一个点所占据的存储器空间则根据目前设定之最大色阶而定。如果是黑白系统，则一个bit代表一个点，16色灰度，则4个bit代表一个点，以此类推。

因此，只要设定Video RAM中某一点所代表的存储器位置，LCD Controller在下次refresh时，就会将这个改变反应到LCD之上，从而达到在LCD‘画图’的效果。在这种架构中，要完成一个图形必须一点一点逐一修改Video RAM内容，如果要画的图比较复杂，或者必须边解压缩边画，则会让user感觉到图画好像一点一点或一行一行慢慢show出来，这种感觉相当不好。user宁愿等待一小段时间，然后画面一瞬间全部刷新。

所以实际上我们都会采用double-buffer的做法，顾名思义就是两块Video RAM， 一个代表前台（LCD controller在refresh时会将代表前台之Video RAM的内容反应到LCD panel上），另一个则是后台。绘图库函数会将图画在后台（改变后台Video RAM的内容），等到所有的绘图工作告一段落后，再一股脑的将整个后台Video RAM（或者只包含此次改变的区域）的内容复制到前台的Video RAM中。等下次refresh时，user就会发现LCD panel的内容改变了（有的LCD Controller提供随时改变Video RAM地址的功能，在上述应用中，就无需做存储器传输）。

现在问题来了，在传输整个Video RAM的过程中，如果性能不佳，对user而言也会感觉整个屏幕是由上至下，一行一行的改变。这样一来，使用后台Video RAM等于没有效果。我们可以试算一下，一个 320×240×16 bit、70Hz（每秒刷新70次）的panel，其Video RAM的size为320×240×2 = 153,600 Byte，约150 KB的数据量要在小于1/70秒内复制完毕。这使得开发者必须采用该平台上性能较佳的存储器传输算法。

最简单的存储器传输方式是调用标准C函数——memcpy()，但这个函数的性能如何呢？每个平台都有其最佳的access存储器的方式。例如，从偶数地址一次传输两个Byte或4个Byte，但当初写memcpy()的工程师并不知道你的平台特性是什么，也不能保证你传入的一定是偶数地址，所以其实现方式相当简单明了。

可想而知，这是性能最差的做法，在大部分16 bit或32 bit的CPU上，在奇数地址上，access一个Byte是最浪费CPU运算资源的事情。以上述应用为例，假设你使用的平台在当地址为4的倍数，同时access 4个Byte的性能最佳，是否可以把前、后景Video RAM的起始地址都设在4的倍数的地方，然后调用自己的memory copy function?

实际上，使用上面的function速度比原来的memcpy()快上不只4倍，LCD显示性能不彰的问题也因此解决。如果不能保证传入的地址都是4的倍数，则可将my_memcpy()做如下改善。

实际上，一般状况还是调用标准C函数里的memcpy()，只有在对性能相当敏感的应用时，才会去调用自己的memory copy函数。读者可以再对上面的memcpy()函数做进一步的改进，例如：这个函数可先做判断，在起始与结束地址不全为4 Byte倍数的状况下，可先单独复制前后没有与4 Byte对齐的Byte，然后中间大量的数据都是以4 Byte为单位做复制。当然，这种复制的数据量要够大，则多做上述判断对性能提升才是有意义的。

这章牵扯了不少东西，但重点就在使用my_memcpy()的时机与动机。Performance调整是每个嵌入式系统工程师的必经历程，当碰上性能问题时，你可能会后悔当初为什么没有认真修数据结构、算法等课程，在电子产品开发领域有个很严肃的规范，那就是：不能接受的性能和结果错误是等价的。PC上的user可以忍受漏斗跑个半天，但绝对无法忍受电子鸡变成慢动作。

05-05　总结

现在嵌入式系统的书籍对实作技术都是点到即止或避而不谈，致使阅读时有见山不见林之憾，读完全书还是对开发工作没有半点头绪。本章试着描述要写作一个最简单的嵌入式系统程序必须具备的基本知识，这应该是一般人看过最麻烦的“Hello World”程序了。

嵌入式系统最令人头痛的就是多样性，虽然没有标准的CPU和开发工具，但观念和开发流程绝对是一致的。希望读者可以反复阅读本章的范例，不要拘泥在技术细节上，应该先把嵌入式系统程序的开发与运作流程完完全全的弄清楚。真正的电子产品开发比本章的范例程序还要复杂百倍，以下章节将逐一为你补上“Hello World”之外的技术环节。

[1]　如果不是很熟悉的架构，硬件工程师不会直接设计和产品一样大小的板子（Real-Size-Board），通常会先设计一块比较大的板子，称为Target Board。一来Layout复杂性低：二来易于调试，而且要在大板子上加工也比较简单。当电路设计确认无误后，才会去制作Real-Size-Board，如图5-3所示。

[2]　EV Board（Evaluation Board）：CPU或芯片厂商提供予客户评估其产品的板子，主要目的是减缓客户使用其产品的学习曲线。一方面可以作为很好的reference design：另一方面可以让客户在自己的板子还没完成或稳定前，就可以开始开发及测试工作。

[3]　ICE（In Circuit Emulator）：这台机器连接PC与Target Board，读者可以想象成ICE会取代Target Board上的CPU，提供目标程序一个完整的模拟环境，让开发者能够非常清楚地了解到程序在Target Board上的工作状态，以便于监视与测试程序。

[7]　请注意，文中提到的是“信息电子公司”而非“软件公司”，至于职务则是“研发工程师”而非“软件工程师”，如果可以的话，称之为“电子产品设计公司”的“嵌入式系统开发工程师”应该更能表达其工作性质。

[8]　嵌入式系统的程序和PC程序不同，因为每个产品的存储器配置可能都不相同，所以程序员要自己指定程序要在哪个地址执行，这个动作称为“重寻址（Relocation）”。重寻址这个动作是由Linker来做，至于什么程序区段要寻址到哪个地址，则必须由程序员自己来指定（只有自己才知道硬件的存储器配置是什么吧）。

[9]　Linking时除了可以指定可执行文件的起始位置外，还可以指定哪一个程序是可执行文件的开头。

[10]　PC（Program Counter）：CPU内一定会有的寄存器，不同CPU可能会用不同的名字，如IP（Instruction Pointer）；是用来记录下一个指令的地址。当CPU执行完一个指令后，PC值会自动累加，此外，某些指令（如JUMP）就是通过改变PC的值来改变CPU的执行顺序。

[11]　中断矢量表（Interrupt Vector Table）：计算机系统通过中断（Interrupt）来处理突发事件，例如，键盘被按下或计时器到时等。通常CPU（或CPU+中断处理器）可以处理许多中断来源，所以程序员必须指定当某个中断发生时，要让CPU去执行哪一个函数，而记录这些设定的表格就叫做中断矢量表。

[12]　也有一些CPU的中断矢量表是以8 byte为单位的。

[13]　除以0是不合法的运算，当然程序员是不会故意写这样的表达式，但有时候因为程序的bug或算法有问题，导致除数是0时，CPU就会引发一个中断，程序员可以在这个ISR处理这种不正常的状况。

[14]　Stack Pointer：“调用函数时的参数与返回地址”及“函数内的局部变量”，都是存储在称为堆栈（Stack）的存储器区域中。CPU内一定有一个寄存器用来记录目前堆栈的底部，用C语言编写的程序几乎不可避免地要用到函数调用及局部变量，所以在CPU启动后，要尽快设定堆栈指针寄存器，使其指到一块足够大的存储器。在嵌入式系统中，这个动作必须由程序员自己做，且堆栈的使用要很小心，否则很容易发生不可测的错误，《第12-2节：Stack》将对堆栈做更详细的介绍。

[15]　状态寄存器：在大部分的初始化动作还未完成之前，此时，若产生中断是很麻烦的事情。所以在boot阶段最重要的事就是命令CPU禁止中断产生，直到初始化动作都做完后，才把中断功能打开。CPU的状态寄存器由很多状态标志组成，通常都会包含用来设定CPU是否接受中断的标志。

[16]　你目前只需知道函数和ISR的进入和结束必须做不同的事情即可，《第9-6节：ISR编写注意事项》将会对中断处理程序做更详细的解释。

[17]　一般compiler都会有类似的关键字，名字或语法可能不一样，但意义及思想与本节举的例子都是相同的。

[18]　有些compiler会提供某些非标准的关键字，思想类似__interrupt，让程序员可以用C语言的语法去操作CPU的寄存器。基本上，笔者不建议在程序中使用太多非标准的关键字，这样会让程序看起来很不直觉，而且程序员往往不知道compiler到底会为此产生什么样的汇编语言代码，这种不确定性有时会带来麻烦。笔者认为，用Inline Assembly在C程序中写汇编语言是比较一目了然的方法。

[19]　这样的寄存器称为“Memory Mapping Register”。你在这里只需要知道，这种寄存器就像一般的存储器一样，每一个寄存器都有自己的地址，在这个地址上填值，就相当于设定这个寄存器；从这个地址取值，就等同于读取这个寄存器目前的状态。在实作上，通常用一个指针指到寄存器所在的地址，就可以用C的语法操作寄存器了。

Chapter 6

实作嵌入式系统平台

时间：06月01日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

PM：“写了一个能在非PC的硬件平台上执行的程序，是不是很有成就感？”

菜鸟：“是的，当我看到LED亮起来的那一刻，确实是很激动。如今我也慢慢可以体会到您之前一直和我灌输的甘苦谈。开发嵌入式系统的确比PC程序开发要多注意一些事情，而且要求对计算机系统的运行原理要有更清楚的认识，这些日子我把之前计算机组织的教科书拿出来复习了一下，的确很有帮助。”

PM：“很好，谈到嵌入式系统中软件的组成，从底层到上层无外乎是：‘Boot-Loader’→‘系统初始化’→‘驱动程序’→‘系统’→‘API’→‘应用程序’，你写的那个小程序虽然只能点亮LED，但其实已经是个具体而微的嵌入式系统了。当然实际的项目会复杂的多，开发时必须更细心，并且需要更多的技巧，以后我会慢慢让你从工作中学习的。在实作阶段的初期，因为软件是整个项目中复杂度最高的部分，我会花比较多时间与精神在软件开发部门上，尤其是系统平台的实现更是马虎不得，越底层的模块，稳定性越重要，否则，整合时真的很难理清究竟是底层还是上层的问题。”

菜鸟：“我一直都搞不懂所谓的‘系统平台’到底包含了什么？它是一个什么样的概念，是软件还是硬件？有哪些工作要做？由开发团队中的哪一个组织负责？身为菜鸟的我能帮上什么忙？”

PM：“哪来的这么多问题？我这个星期要把系统平台的相关设计再仔细review一次，你趁这个机会把设计文件K一下，将系统平台的思想建立起来。”

之后的章节将会正式进入开发阶段，本书内容将以实际上会发生的实例为主。而本章旨在介绍嵌入式系统平台的特性，以及实作时的注意事项。

所谓的平台，简单地说，就是一个可以让应用程序顺利开发与运行的环境，例如：Windows XP + Win32 SDK或MySQL+PHP就可说是视窗程序或网络数据库程序的平台。但嵌入式系统的平台还包含了硬件，如果有必要的话，通常模拟器（Simulator）也是不可或缺的，所以会比一般软件项目要来得复杂一些。

06-01　系统与平台

PM：“不同的团体对某个名词可能会有不同的定义，而平台（Platform）这个词已经被滥用了，任何东西加上‘平台’这个词好像就变得比较专业。例如，就业信息平台、电子支付平台、网上互动学习平台等。我对嵌入式系统平台的定义其实很简单，首先把嵌入式系统的软件分成4个层次，从底层到上层分别是：

■　Boot-Loader[1]与驱动程序

■　OS与API

■　子系统与库函数

■　应用程序

▲ 图6-1　两种不同风格的系统架构图

▲ 图6-2　另一种风格的系统架构图

所谓的系统应该就是Boot-Loader+驱动程序+OS+API，当然也有人把子系统与库函数也算到系统内。此外，也有不少人认为他开发电子产品不需要系统，直接就把驱动程序和应用程序混在一起写，反正合乎产品需求就好。但我认为这绝对不是良好的设计方法，除了不易维护、难以调试之外，程序要重复使用的可能性几乎是零，在此我们就不讨论‘不需要系统的嵌入式系统’ 了。所以嵌入式系统平台应该包含下列项目。

▲ 图6-3　使用DSP当主控IC的系统架构图

■　系统软件+驱动程序

■　硬件平台

■　开发环境（compiler、调试与下载工具）

■　模拟器

■　程序编写规范

简单地说，嵌入式系统平台就是让电子产品的‘应用程序’得以顺利开发的环境，所以系统平台不只是软件或硬件的概念而已，它是一个稳定的环境。在这个环境下，负责应用程序开发的工程师可以将心力放在产品功能的开发，以及改善其质量与性能即可。现在电子产品的功能都很复杂，往往应用程序才是最耗费人力与时间的部分，如果可以的话，我会希望在我们系统平台上写应用程序就像写Windows程序一样的方便。”

菜鸟：“这样定义好像蛮有道理的，但好像没有看过哪本书是这样定义的。”

PM：“一般的书哪里会做这样的名词定义，但我们是多个单位共同开发一个项目，许多名词绝对不能混淆，否则，我下的命令势必会被打折扣。假设我的命令是：‘请进行本项目的系统平台规划’或‘请协助设计某个模块的设计规格（Design SPEC.）’，如果负责人对所谓‘平台’或‘设计规格’这两个名词的认知和我有所差异的话，那么，他怎能做出我满意的结果？因此，我的原则是不管某个名词是否有国际标准的解释，只要团队中所有的人和我都有相同的认识，不要造成沟通的困扰即可，而且我也相信我的解释不会和正确解释有太离谱的差异。”

菜鸟：“所以在我们的软件团队中，就有一个‘系统平台组’啰！”

PM：“是的，所谓系统平台包含的工作项目很多，但基于系统平台的重要性，我觉得应该要有一组人负责整合的工作，这个小组可说是嵌入式系统软件部分的核心成员。这个小组中的工作划分如下。”

■　系统架构设计与实现

■　嵌入式操作系统设计与实现

■　API设计与实现

■　存储器使用配置（Memory Layout；规范某个模块或程序区域可使用某个范围内的存储器）

■　开发环境设计（如何build整个系统，并让其在机器上执行？）

■　模拟器设计与实现

■　系统整合（整合底层的驱动程序、系统程序，以及上层的子系统、库函数与应用程序）

■　版本制作

PM：“同样的，我设计的组织不一定是最好的，而且我也不认为驱动程序小组或应用程序小组不重要，但在我们人力资源如此匮乏的状况下，维持一个稳定、积极、具有系统思想与协调管理能力的系统平台小组，的确是最有效率的做法，这也是我总是称他们为Kernel Team的原因。”

上述工作划分并非笔者首创，而是业界普遍认为比较有效率的做法。实际上，如果团队人够多的话，为了管理方便，也许会将上述工作分配给几个小组；如果人力相当匮乏，也有可能把驱动程序开发纳入这个系统平台小组。当你读到后面几章就会慢慢发现，上述之工作项目在产品开发上都是相当重要且息息相关的，为了避免团队间争功诿过的陋习，并畅通沟通管道，把这些工作相关人员配置到同一个team应该是有其道理的。

举例来说，负责开发应用程序的某位工程师，需要将某段程序寻址到速度较快的存储器（如CPU内部的SRAM），以加快某个算法的执行速度。为了满足他的需求，Kernel Team要做的事情是：

■　变更存储器配置

■　修改Link-Script（重寻址）

■　新增或修改API

■　模拟器对应

■　系统整合，确认对其它模块的影响

■　应用程序功能测试

一旦其中几件事情属于不同的小组负责，那么，原本可能一天内可以完成的事情，就要召开会议，再经过mail往返[2]，恐怕一周都做不出来。而嵌入式系统项目中，系统平台是最容易影响其他模块进度的部分，如果反应如此迟钝，那么项目要如期完成恐怕岌岌可危。

06-02　系统架构设计

菜鸟：“我本来是尽量不来烦您，但现阶段大家都忙着开发没空理我，系统平台相关的设计文件这么多，我实在不知道该从哪看起，还是您指派个模块让我开发好了，不然，大家会以为我很闲。”

PM：“可怜的孩子，我知道你没在鬼混就可以了。现在是开发阶段初期，很多架构都还在建立，所以不是人多就可以加快进度的。我对你在组织中的定位是预备队，当某部分出现缺口时，你必须能迅速的帮上忙。所以对本项目开发工作的细节，你应该要和我一样熟悉，我不是让你先K一下系统平台的设计文件吗？”

菜鸟：“你说过系统平台涵盖的工作项目很多，我到底该从API、OS、boot-loader，还是开发环境的文件开始看起？我大概都翻了一下，但还是没办法有个整体的概念。”

PM：“嗯～我应该交代清楚，毕竟你还没有嵌入式系统软件开发的相关经验。这样吧！我花半天的时间带你看一下文件。首先看一下这张图（如图6-4所示），你认为系统平台包含哪几个部分？”

▲ 图6-4　系统平台架构图

菜鸟：“按照你的定义，应该除了 Sub-System（子系统）与AP（应用程序）之外，都算是系统平台。”

PM：“没错！现在我们把范围缩小，先看看系统架构和API的思想就好。”

系统平台的其他部分请参考其他章节。

■　第七章：构建良好的嵌入式系统开发环境

■　第八章：上电之后：Boot-Loader

■　第九章：驱动程序

■　第十四章：模拟器

6-2-1　产品规格Review

PM：“在开始系统平台的设计之前，我会要求所有参与设计的人员，都必须清楚我们所开发产品的性质、可调用的资源，以及硬件规格所造成的限制。过度设计或思绪不周都不是件好事。基本上，系统设计[3]的工作必须根据以下流程的思想来进行：

PM：“不管系统复杂度如何，都应该要照着这个流程的精神执行，并且在各个阶段之间都应该要做好控制审查。如图6-5所示，设计工作的前两个步骤，是确认核心需求与分析系统行为模式，根据你对嵌入式系统的了解，说说看，在设计系统架构前要注意的事情有哪些？”

▲ 图6-5　系统设计流程

菜鸟：“经过这几个月的学习，以我现在对嵌入式系统的了解，我认为该注意的事项有：

■　硬件规格

□　CPU速度：如果CPU速度不够快，应该避免用太复杂的算法（或者应该研究是否有更具效率的算法）。

□　存储器容量：因为物理存储器size有限，设计系统架构时必须注意到程序大小、静态空间（全局变量）、动态空间（Buffer）的size同样也是有限制的。

□　外围设备的性能：外围设备速度有快有慢（例如，RAM速度会比ROM快的多），必须事先注意整个系统的瓶颈可能在哪里。

□　产品规格：系统需要实现哪些功能？

■　产品特性：产品使用环境、销售地区、目标使用者族群及应用范围等都可能影响系统架构的设计。例如，刹车系统对正确性及快速反应时间的要求就比洗衣机高；智能型手机的功能很多，而且可以让使用者随意操作。所以在设计使用者界面时，就要比只允许受过培训的专人所使用的电子设备要严谨的多（例如，挂在候鸟或鲸鱼身上的信号发射器）。

■　驱动程序、系统、子系统与应用程序各层之间的沟通接口。

■　是否使用面向对象观念设计系统。”

PM：“如果从项目管理的角度来看，系统设计还必须注意

■　人力与进度：设计结果必须合理，没有足够的时间或人力来执行的设计，就是失败的设计。

■　质量要求：系统架构的设计确实有严谨与否之分，并可能直接影响最终产品的质量等级。

如果再从系统的角度来看，要考虑的事情还有：

■　实时（Real Time）的需求

■　多任务（Multi-Tasking或Multi-Threading）的需求

■　电源管理的重要性

■　可扩展性的重要性

■　可移植性的重要性

说穿了，就是要弄清楚产品规格才能开始进行系统设计，告诉你一个业界曾经发生的实例。某软件公司前后接了两位不同客户的产品开发项目，因为应用程序的需求几乎一模一样，所以老板和sales二话不说就接了下来。研发团队也以为这个项目是个大便宜，认为只要用之前的系统改一改即可交差。结果，因为第二家客户要求的产品造价较低，只能选用性能较差的CPU与较少的存储器，之前的系统架构与算法几乎完全不能用，最后下场就是必须花费更多的时间和人力来做完这个项目。不但进度delay、开发费超出预算，而且还赔上了公司商誉。”

菜鸟：“有这么严重啊？”

PM：“这就是我们项目管理的范围管理（Scope Management）。在你还没有弄清楚项目的范围之前，怎么可能知道要如何设计系统架构？用茅屋的架构盖大厦，或用钢骨结构盖平房都不对吧！上面那个例子就是想把F1跑车的引擎装到mini里，跑起来不散掉才怪。电子产品的系统架构就如同房子的结构、车子的引擎，设计者不可不慎!”

6-2-2　系统架构：从粗略到逐渐精细的方块图

PM：“因为你之前帮忙编写过产品规格，假设你对我们要做什么样的产品已经很清楚了，现在让我们来看看系统架构到底长成什么样子。我相信软件工程或面向对象设计有很多方法论可以来描述一个系统，但实际上，我个人觉得用方块图来描述系统架构是最好的选择，图6-6所示是我们最粗略的系统架构图。”

▲ 图6-6　系统架构图范例

PM：“有了这样的图，是不是对我们的系统架构更加一目了然？我很喜欢这样的图示，因为一个方块就对应了一个工作包（Work Package），不但便于管理，每一个方块都可以再细分为较小的方块图，如此一直细分下去，就可以产生工作列表。当然，你要用UML或一些CASE[4]工具来描述这样的架构也可以，但合用的UML或CASE工具都很贵，而且学习曲线通常也都非常陡峭。以UML为例，它用来描述一个系统的图就有如表6-1所示这么多种，所以工程师要不就是看不懂，不然就是阳奉阴违，总之就是不怎么爱用。所以实际上我喜欢那种一目了然的图（如图6-6所示）。”

表6-1　UML描述系统所需要的图

观　点

图　形

使用者模型观点

使用案例图（User Case Diagram）

结构模型观点

类别图（Class Diagram）

行为模型观点

顺序图（Sequence Diagram）与合作图（Collaboration Diagram）

状态图（State Diagram）

活动图（Activity Diagram）

实作模型观点

组件图（Component Diagram）

环境模型观点

部署图（Deployment Diagram）

菜鸟：“是啊！我以前在学校修面向对象软件设计的时候，其实思想都已经了解了，但考试时这些UML的图总是会搞混，老师总是说，实际上只要使用好的商业软件就没问题了。没想到真正的‘实际’是这个样子，难道其他软件公司也都不用UML或CASE工具的吗？”

PM：“当然还是有许多研发团队在使用这些方法论与工具，但究竟有多少团队能真正抓到这些方法论的精随，并真正在研发工作中获得帮助就值得研究了。就我看到的软件开发团队领导人都和我一样，心有余而力不足，这就和质量系统一样，我很想照着专家学者研究出来的方法论去做，但确实是有实际上的困难。与其最后变成四不像，不如就遵照这些方法论的精神，因地制宜，制定可施行的方法。我一直梦想着有一天能接到一个人力、成本或进度没有限制的项目，我一定好好地按部就班地施行质量系统和软件工程方法论，但梦想终究是梦想……

不好意思，离题了。接下来我分别对这些方块做一个简单的介绍，你可以对系统架构有进一步的了解。我们看到系统架构分为多个层次，由底层往上依次是

■　硬件层：这一层有两个方块，一个是Real Target（就是真正的硬件）；另一个是运行于PC上的模拟器（Simulator）。前者比较容易理解，在此不再赘述。简单地说，所谓的模拟器就是要实现一个PC程序，而这个程序必须和我们产品的硬件具有相同行为模式。

举例来说，我们的硬件有8×8的键盘以及160×160的LCD，这个PC程序就应该会用64个button的模拟键盘，以及长160点，宽160点的视窗来模拟LCD（关于模拟器相关细节请参考《第6-5节：模拟器》与《第14章：模拟器》）。

把模拟器和真实硬件划在同一层的意义是希望运行于真实硬件与模拟器上的程序（包含系统与应用程序）就是同一套，即系统工程师或应用程序工程师可以先在PC上用模拟器开发，同一套程序经过Cross Compiler编译后，就可以在真实硬件上执行，而且执行结果必须一致。

■　驱动程序层：这一层和硬件完全相关，除了要实现驱动周边设备的功能外，还必须设计可让其他程序模块调用的API。请注意，模拟器必须实现具有相同行为模式的同一组API，则应用程序才能同时在真实硬件与模拟器上开发与执行。

在此有必要解释一下HAL（Hardware abstraction Level；硬件抽象层）这个名词。驱动程序有一个重要的任务，就是必须设法隐藏所有的硬件特性，使系统中其它的程序只要调用驱动程序层所提供的API就可以操作硬件，所以有时我们会把驱动程序称为HAL。设计HAL最大的好处是即便更换硬件，只要驱动程序包装出相同的接口，之前项目的程序几乎都可重复使用在新的项目中。

■　操作系统层（I）——和硬件相关的部分：稍具复杂度的嵌入式系统还是需要一个操作系统（OS）。用于嵌入式系统的OS一般称为RTOS（Real Time OS，请参考《第6-4节：嵌入式操作系统：OS在哪里？》），基本上功能都相当简单，虽然无法和Windows或Linux等相提并论，但用于开发一般电子产品却已绰绰有余。市面上可选用的RTOS很多，通常CPU厂商为了加速客户开发产品的速度，都会先行把某个操作系统移植到该CPU上。例如，几乎现在所有销售ARM系列评估板的厂商，都会用Linux当评估板的展示程序。而本章范例图中的RTOS——ITron，实际应该称为μITron（MicroITron），是日本制定的嵌入式操作系统标准，基本上，日系CPU销售时都会提供一个符合μITron标准的RTOS。

我们把操作系统根据是否与硬件相关分为两个部分，这样区分的目的，是因为模拟器必须另外模拟和硬件相关的功能，而与硬件无关的功能则只需一套程序即可。与硬件相关的功能包含：多任务功能（Task或Thread，切换时必须保留目前所有CPU的状态）、中断控制、RTC（Real-Tim Clock）以及Timer功能等。

以上描述的驱动程序层与操作系统层（I）是硬件相关的系统部分。

■　操作系统层（II）——与硬件无关的部分：与硬件无关的功能包含task或thread间通信或同步的机制（如message box、message queue、event、semaphore、Mutex等），以及动态存储器管理功能等[5]。

■　图形库函数（Graphics Library）及图形使用者界面（GUI）：假使产品有任何形式的屏幕时，系统必须包含基本的图形库函数（如画点、线、矩形、圆形等），及简单的图型使用者界面（Graphics User Interface； GUI）子系统，你可将其想象成一个简单的视窗系统。后者是基于前者来实现，但较为复杂。GUI子系统除了提供基本图形界面操作组件之外（称为Control或Widget，如Button、List-Box、Edit-Box、Pop-Up Message Box、Menu及Combo and Ratio box等），还必须处理字型显示，并规定图形化应用程序的写作风格等。

要注意的是，这里提到的Graphics Library及GUI仅包含较通用的功能，所以在设计上必须考虑到扩展的可能性。这有点类似面向对象中继承的概念，如果某款产品有比较特殊的UI风格要求，应用程序工程师可以自行进行扩展，则不用更改系统层次的设计。

■　其他子系统：如网络通信协议、解压缩库函数、文件系统等独立的功能模块。

以上3层都算是系统功能，除了与硬件无关外，其所提供的功能都是属于比较通用的。我们希望同样的系统模块可以用来开发几个不同的产品，也就是产品特有的功能应该在应用程序层之中实现才合理。

■　基于产品特色的专属功能库函数：一个电子产品可能包含不只一项应用程序（想想电子书、智能型手机或电子辞典的例子，即便是洗衣机也有多种洗衣模式，以及脱水、烘干的功能），将许多会共享的模块抽取出来成为库函数会是比较好的选择。一个设计得好的库函数就如同一个对象，必须有自己的属性、私有方法（Private Method）与公用API（Public Method），原本归类于应用程序的模块，若可独立于其它应用程序运行，经严格测试后，亦可将其视为系统模块一员。

■　应用程序层：应用程序就是利用上述各层提供的API，实现产品功能的程序。

以上两层只与某一产品的特有功能有关，都算是应用程序，不能算是系统程序。

应用程序是电子产品的主角，其功能需求通常比较复杂，而底层的模块往往要求更高的稳定度或具有特殊的性能需求（如省电），所以在设计系统架构时，必须注意以下几点。

■　API必须简单明了

■　程序模块间的相关性越低愈好

■　设计范围应该包含各模块的单元测试与压力测试

■　利用callback的思想，让应用程序工程师可以嵌入程序到系统中[6]”

菜鸟：“我知道系统程序通常会比应用程序早完成，所以在开始整合之前，可以有比较充裕的时间做测试，并确保系统稳定。”

PM：“没错！但这并不代表应用程序的稳定度就不重要，主要是因为系统程序稳定度的影响范围通常是全局的。例如，存储器的timing设定有误，则系统在任何时间点都可能会死机。如果在系统还不稳定的状况下进行应用程序测试，当测试人员做了很多操作后死机，那么，实在很难理清到底是系统的问题还是应用程序的问题。而且，同一组系统程序可能会用在不同的产品开发项目上，所以其稳定度更显重要。”

6-2-3　数据流

菜鸟：“这样的系统架构图的确简单明了，但只能显示各功能区块间的上下关系，并无法描述信息与事件是如何传递的。”

PM：“没错！这种系统架构图的用途类似UML的组件图。对于数据流，我们同样有张简单明了的数据流架构图，如图6-7所示。”

▲ 图6-7　数据流架构图

PM：“计算机系统的原理不外乎处理输入事件，产生结果并输出，嵌入式系统也不例外，让我们从外围设备状态发生变化开始讲起。”

■　通常输入的来源有两个。一种是由监督程序（Monitor）持续主动地监督某个外围设备的状态是否改变（我们称这种方法为Polling或Busy Waiting）；另一种则是由外围设备主动产生中断。不管是Polling或中断，基本上都是因为外围设备的状态产生变化，例如，使用者点触了触控式屏幕、或温度检测器发现异常温度等等。

■　当某设备状态发生变化，相应的驱动程序会被执行。如果是Polling方式，则驱动程序是由监督程序所引发；若是中断的方式，则相应的中断处理程序（ISR）会被执行。

■　ISR（以下就用中断方式来说明）只会马上判断硬件状态发生了什么变化[7]，并且把新的状态往上层送（如哪个key被按下）。所谓的传送方式有很多，最简单的方式是设定某个全局变量，上层程序只要读取这个变量，就知道某个硬件事件发生了。如果硬件事件发生的很频繁，例如：网络的封包，只用一个全局变量传递信息，势必会遗失很多的硬件事件，此时就要用其它较复杂的方式，如message queue。

■　系统层会有一个无限循环，循环内的工作就是持续检查有没有新的硬件事件到达，若有则处理之；若无则系统可暂时进入idle mode。范例程序如程序代码6-1所示。

程序代码6－1：系统中的message loop

■　当硬件事件被系统处理完之后，系统会决定是否将其送给应用程序。同样的，系统程序与应用程序间有许多不同的沟通方式，例如，应用程序可以对其欲处理的硬件事件注册callback function，则当该事件发生时，系统自然会执行应用程序的事件处理函数。

菜鸟：“在设计阶段，数据流架构图是不是也要逐次细分下去？”

PM：“当然！根据研究，图形表示是最容易让人的大脑理解并留下存储的方式，实际上也是如此。好的图示胜过千言万语，尤其做软件设计的人，一定要学会用图形表达系统架构与数据流。当架构图画完后，负责文件写作的人员只要按图说故事就可以了。”

6-2-4　硬件相关与硬件无关：可重用性及可移植性

PM：“在设计用于电子产品的系统时，还有一点必须注意，要把与硬件相关和与硬件无关的模块分开。因为虽然每一个嵌入式系统或电子产品各有其特性，但可移植到其他平台并重复使用的程序模块，对软件团队而言是最重要的资产。一般软件方法论都只提到软件模块必须可以重复使用（Reusable），但在嵌入式系统的领域中，硬件平台会随开发项目不同而改变，所以可移植性（Portability）也是不可忽略的设计重点。

和硬件相关的程序模块可移植性与可重用性都很低，除非硬件平台完全相同，否则，该程序模块几乎不具可移植性。举例来说，如果某个程序模块中包含ARM的汇编语言，要不经修改就将其移植到另一个CPU（如MIPS）上是不可能的。所以我们在设计驱动程序时，通常还会将其分成两层，如图6-8所示，其中只有hw_key_api_1()所在的模块是与硬件相关的，其上层的两个模块都具可移植性与可重用性。”

▲ 图6-8　驱动程序的两个层次——硬件相关与硬件无关

菜鸟：“这个道理我懂，但我们的系统不是几乎都是用C编写的吗？既然是用C写的，移植应该很容易啊！把硬件相关的部分改一改不就好了？为什么还需要分那么多层？似乎有点叠床架屋的感觉！”

PM：“我以前也是这么认为，把程序看懂，然后再改一改，不就可以重新使用了吗？我就不相信有程序是我看不懂的。但当我要管理整个项目，负责管控软件系统的质量时，我就不是这么想了。”

菜鸟：“为什么？这样的想法有错吗？”

PM：“对工程师来说没有错，因为工程师总是乐于接受挑战。但我的经验是，第一，世界上真的有看不懂也改不动的程序，要花很长时间才能看得懂、改得动，若超出了项目允许的时间，则等同于看不懂、改不动；第二，以我身为管理者的角度来看，所谓可重复使用的软件模块应该是经过测试与质量控制的，只要经过任何形式的修改，即便是由天才工程师经手，这个模块的质量就无法被信任，也即仍必须经过测试与质量管理流程。当然，通过修改既有的程序模块，达到重复使用的目的，通常也能够缩短新项目的开发时间，但这样的模块会带来变量，不能算是软件研发团队的资产。

在嵌入式系统的领域里，最简单的原则是在设计阶段就把硬件相关与硬件无关的模块明确地区分开，而且各模块间只能使用公开的API来沟通，你可以从我们的系统架构图中清楚地感受到这种思想的落实。管理阶层必须严格要求所有工程师，在和硬件无关的模块中，绝对不能写入和硬件相关的程序。

但是当项目进行到后期时，工程师在时间紧迫的状况下，改bug时，若为了图方便就会忘了（或无视）所有的原则，例如：全局变量乱用、应用程序中插入汇编语言等等。而管理阶层在那个时候也没有时间做code review，只能默许这种不对的状况发生。当项目结束后再回头看程序代码，只能用千疮百孔来形容。虽然所有的程序都已经过验证，但可重复使用的部分却少之又少。等到下个项目，即便产品性质类似，但可能已经换了另一批工程师，因为改不动既有的某些程序模块，结果只有重做一遍，这种浪费对软件团队或公司来说都是很大的伤害。”

菜鸟：“如果照你所说的，即便设计阶段把各个模块做好分类，也无法避免工程师不按照规定写程序啰？”

PM：“不是没有办法，只是难免有漏网之鱼。当设计工作完成，工程师开始写程序后，我们会安排密集的code review，可以在开发中期保证工程师都会按照设计结果来写程序。在开发后期，工程师忙着调试时，小主管们此时可能没那么多紧急的事情要忙，我会要求他们有空就去server看看修改过的程序有没有问题。我有空时也会去看，若发现问题会先记下来，然后在相关工程师不那么忙的时候要求其改善。这么做或多或少可以维护程序的写作质量、可重用性与可移植性。”

6-2-5　System Configuration：可扩展性及可调整性

菜鸟：“有件事情我觉得很奇怪，在设计文件中居然包含了一个程序文件，叫做sys_config.h，里面定义了一堆宏常数（MACRO）。其他设计文件则解释这个.h文件定义了系统里所有可调整的配置，在编译系统之前，可以通过修改这个.h文件来设定系统的功能与特性。

我的问题是，嵌入式系统不都是专用系统吗？为什么还要做到可以调整配置？而且一个.h文件如何能实现系统配置设定？”

PM：“在设计嵌入式系统的系统架构时，必须考虑到两项特性，一项是可扩展性（Scalability），另一项是可调整性（Reconfiguration）。基本上，这两种特性表示差不多的概念，即系统必须高度的模块化，在不同的应用时可选用不同的模块来组成系统。

举例来说，假设系统中实现了3种不同算法的动态存储器管理机制，产品A的应用较简单，根本用不到动态存储器管理功能；产品B的功能也不复杂，只要用到较简单的算法；而产品C因为要实现Java虚拟机，必须要有可靠性较高的动态存储器管理机制。假设这3种产品其他功能完全相同，我们当然希望不要设计3套系统，如果能通过某种方式，就可以很容易地抽换系统的某个功能，那么只要用一套系统程序，就可以做出这3种产品的系统程序，并分别满足各个产品特性的需求。因为功能是可抽换的，所以实际运行于机器中的系统并没有多余的功能，自然就不会浪费程序空间[8]，当然也不违背嵌入式系统或电子产品必须采用专用系统的特色。

这就有点像编译Linux核心时要先执行‘make config’一样，你可以选择要把哪些功能加到新的核心程序里，例如：某某网络卡、显示卡的驱动程序，甚至是否支持TCP/IP网络功能等等。Linux核心的选项之多，往往让初学者不知所措，但换个角度看，这表示Linux核心的可扩展性和可调整性很高。图6-9所示是嵌入式Linux-μClinux编译核心可选择的配置。

▲ 图6-9　设定μClinux核心的配置

菜鸟：“我还是不懂，为什么这样勾选之后，就会影响到有些模块会被加入新核心？”

PM：“我们系统可供调整的配置（Configuration）当然没有Linux那么多，也没有如图6-9所示那么好用的工具，但用到的程序原理却是一样的。首先回头来看我们系统中的sys_config.h，如程序代码6-2所示。

程序代码6－2：System Configuration（sys_config.h）

PM：“Linux虽然提供了一个图形化的界面，可以让使用者方便设定核心的配置，但最终还是会产生一堆header file[9]，其原理和我们系统中的sys_config.h一样，只是Linux的选项比较多，而我们没时间去做一个图形界面的配置工具。用到的程序技巧只有一个，即C语言的条件式编译，在sys_config.h中定义了许多宏常数，当赋予这些常数不同的值时，C Preprocessor（C前处理器）[10]就会根据程序中定义的宏常数，以含括或排除部分的程序，其产生的中间文件才会交给C编译器去产生真正的code。好像说的有点复杂，我们来看一个范例会比较清楚，如程序代码6-3所示。

程序代码6－3：条件式编译的范例

PM：“就以我们刚刚看过的sys_config.h的内容为准，上面这个程序经C Preprocessor后会产生什么code？”

菜鸟：“这个简单，因为PRODUCT_NAME被定义成KME_MODEL_B，而HW_MUSIC_ MP3_SUPPORT则被定义成HW_CONFIG_FALSE，所以第一个判断式为真（true），第二个判断式为伪（false），那么真正被编译的程序应该是：

这样我懂了，如果我们产品B的下一代要新增播放MP3的功能，其他的功能都不变。那么，我只要到sys_config.h中把HW_MUSIC_MP3_SUPPORT定义为HW_ CONFIG_TRUE，其他的应用程序（如my_function()）都不需修改。而这次真正被编译的程序如下。”

PM：“是的！原理就是这样简单。重点是这些可调整的系统配置应该尽量在设计阶段的初期就要制定好，而且必须统一管理，并明确定义各个配置的意义，如此一来，负责系统架构设计的人员才有遵循的依据。基本上，每个配置都应该是一个绝对独立的模块，所以当其不加入系统时，也不会对其他程序模块造成影响，这也是为什么sys_config.h会出现在系统设计文件里的原因。”

06-03　API与程序风格设计

菜鸟：“我们看了系统架构图、数据流架构图及模块规划，接下来再去读各个模块的API设计文件，应该就可以比较轻松了。”

PM：“系统架构与模块规划的设计工作会花上比较长的时间，在此期间，相关人员要反复的讨论，确定系统架构、模块化程度、扩展性与可移植性都没有问题。接下来会将各个模块交给负责人员进行细部设计，当然包含你所说的API，但同时还得有人去制定系统程序，与应用程序的写作风格。”

菜鸟：“工程师不就是根据设计结果去把程序写出来就好了吗？连写作风格都要规定，这样限制会不会太多了？”

PM：“这是软件项目管理的必要之处，我可不希望我们设计工作做得很扎实，但最后写出一些能运行，但没人看得懂、改得动的程序。万一工程师离职了，这种程序你就只能提心吊胆期待它不要出错，否则几乎只有重写一遍，而且我们制定的程序风格几乎都是原则性的，不会给工程师带来太多麻烦。”

菜鸟：“您说的该不会是匈牙利命名法（Huangrian Naming Convention）那种东西吧？”

PM：“设计阶段要制定的程序风格有两种。一种是你说的命名规范，这个规范我们已经run了几个项目，基本上就是沿用，你有空去看一下文件，很容易懂的（请参阅《第6-6节：Source Tree设计与程序风格典范》）。

另一种与其说是风格，不如说是在这个系统上写作应用程序的方法，我举个例子你就懂了。同样是用C语言，但是写Windows的程序、DOS的程序、PDA的程序都有不同的风格。再想象一下Java语言，如果你要写Java Applet，就必须遵守它的规定，以下程序中的粗体字可说是Java Applet特有的程序编写方法，或者说是风格。”

菜鸟：“这样我就了解了，应用程序要根据系统平台的特色来写作，但系统程序也有类似的规定吗？”

6-3-1　系统程序风格

PM：“系统程序也有许多程序风格的限制，但强制性没有那么强。举例来说，我们的设计文件中会有一段sample code或pseudo code当范例，用以说明如何实现‘数据流架构图’（如图6-7所示）中的message-dispatcher。从图中可知，这是一个和产品相关的系统模块，不同产品可能会有不同的需求，但基本的程序架构应该是一样的，如程序代码6-4所示。

程序代码6－4：系统程序的风格定义

菜鸟：“所以当使用这个系统来开发某个产品时，可以直接拿这个范例来修改啰？”

PM：“是的！使用同一个系统开发不同的产品时，基本上，系统架构不至于有太大的异动。程序员当然可以选用不同的程序风格来实作，但直接使用设计文件中建议的范例肯定是较好的选择，因为系统架构设计者最清楚该系统的特质，除非有特殊的用途，否则实在没有道理不去遵守设计文件中范例的规定。

像这样已提供范例的系统模块比较没有问题，系统工程师应该都乐于配合，系统设计文件中还可能规范一种系统程序编写风格，这是基于系统架构与设计理念而来。举例来说，假设系统是采用‘面向结构’的思想，即每一个模块都应该表现出‘以处理信息为主’的特性，这样的规范会表现在程序风格上。”

菜鸟：“我知道所谓‘面向结构’的概念是：系统中所有的事件都会被转换成信息，而信息会送给特定模块或在所有active的模块间流动，由各个模块决定是否处理或如何处理这个信息，同时，每一个模块也可以送信息给整个系统或其他模组。在这样的系统架构下，每一个模块可以比较独立，程序就是判断接收到的信息，及判断当时系统的状况，并接着做出反应而已。但这和程序风格有什么关系呢？”

PM：“还是有关系，如果你不规定的话，就会有人写出这样的程序，如程序代码6-5所示。”

程序代码6－5：面向结构系统模块之程序风格（1）

PM：“其实这也没有什么不对，但整个项目中的程序可能会相当多，各有各的性质。例如，driver的程序风格和从厂商或网络取得的程序代码就不会一样，以上的程序风格就象是一个一般的程序文件，一点都不象是‘面向结构’系统中的一个基本单位。如果所有负责系统模块程序编写的工程师都根据以下的规定来做（如程序代码6-6所示），会不会比较好呢？

程序代码6-6：面向结构系统模块之程序风格（2）

菜鸟：“这样的写法确实比较清楚。我只要一看到这个程序，就知道它是一个处理信息的基本模块，而且这样的规定并不会为工程师带来太多的困扰。”

6-3-2　应用程序风格

PM：“应用程序编写风格规范的思想比较简单，你只要想一想Windows应用程序或Java Applet的程序写法就清楚了。在此要注意两件事，第一，如果我们的系统可支持多任务（Multi-tasking 或 Multi-threading），那么，应用程序在建立 task 或 thread 时不能像在 Windows上写程序那样随意，其生命周期以及要使用哪些资源都应该要接受规范。举例来说，假如某个task用了过多的存储器，很可能会影响其它task的运行，一旦某个task将自己设为最高执行等级，则这个task很可能一直占据CPU，除了影响其他task，也可能影响系统待机功能的运行。所以在系统设计阶段，除了制订应用程序编写方法外，还必须制订应用程序编写时应注意的限制事项，并且严格要求应用程序工程师遵守。

第二，假如应用程序采用面向对象方式设计，但只无能用C来开发时（例如：开发环境并未提供C++的编译器，或者产品相当注重性能时），为了贯彻设计的精神，我们会有一些规范或建议（请参阅《附录C：用C来实作面向对象的概念》，让工程师知道如何用C语言来实现大部分面向对象的观念。虽然C语言并不是面向对象的语言，而且许多面向对象的观念并不是那么容易可以用C语言来实现，但是面向对象观念在设计较复杂的应用时的确是较佳的方法，所以很可能出现使用非面向对象语言（C语言）来实作面向对象设计的状况。

在PC上写程序有很多面向对象程序语言可以用，实在无需用C语言来包办代替。但在嵌入式系统要考虑的事情就较多了，而且性能是一个主要的因素。此外，确实不是每一个平台都有C++的编译器，这不就意味着用C++写的程序无法移植到其它平台的风险性较高吗？假使一个‘对象’不具可重用性，那么，使用面向对象设计方法的好处恐怕少了一大半吧！

所以，如果不好好规范应用程序的写作风格，我实在无法想象工程师会天马行空地写出什么样的程序！事后就很难去维护这些‘对象’的程序了。”

菜鸟：“这个我有切身之痛。之前在学校，我带着学弟在PC上实作一个以面向对象设计的系统时，即便是用C#这种纯面向对象语言来写，还是有人会以直述的方式来写程序，就像用C++来写C 一样，最后要整合时简直是一团乱。所以我知道，就算设计的再好，执行的人理解不对，还是会有问题。”

项目经理：“是的！学校的期末作业可以允许失败，但商场上的项目则必须一切事情都受控制，执行的人不能配合设计精神对项目质量是一个很大的风险，所以应该在一开始就把相关事宜用白纸黑字写下，并发布给相关项目人员。”

6-3-3　API

PM：“除了程序编写风格外，和基层工程师最有关的就是API设计文件了。我想一般程序员应该都看过各式各样的API文件，例如：标准C库函数（POSIX）、Win32 API、Java class library, MySQL API等，只要是提供功能给其它程序用的模块，必然要提供一份详细的API文件，否则使用者如何知道那么多的程序代码到底要用哪些函数?调用顺序如何才是对的？以及有哪些限制或注意事项？更何况我们常会用到没有源码的库函数。

一份好的API文件应该包含：

■　该模块的功能说明与使用范围

■　数据结构与常数说明

■　各个函数的功能、参数与返回值的意义说明

■　足够多的范例

■　注意事项及限制

■　相关函数

菜鸟：“我知道我们team的API文件和微软的很像，为了说明一个Function，必须包含以下内容：

■　Parameters（所有参数的意义）

■　Return Value（返回值的意义）

■　Remarks（注意事项）

■　Requirements（基本需求）

■　Example（使用范例）

■　See Also（相关函数）

PM：“其实API文件都差不了多少，有时候为了容易说明，我会要求设计者要画张图，通常一张图可以胜过千言万语，毕竟不是每个写作文件的人员，都能用文字把一件事情描述清楚。以下是一个很好的范例，如图6-10所示。

▲ 图6-10　API文件范例

PM：“系统设计文件我们都浏览过一次了，我相信你应该已经有能力自己把它们读完吧！然后你就去和系统组的主管报告一下，他应该会把你之前设计的那个模块开发工作交给你做，写程序前注意一下，先读过我们的程序风格典范，如果你用自己的方法写程序，code review时被系统组的主管修理可别怪我没提醒你。”

菜鸟：“我知道了，你在项目会议上已经公布这是本团队程序编写的基本规范，我会注意的。”

06-04　嵌入式操作系统：OS在哪里？

在开始介绍嵌入式操作系统之前，如图6-11所示是PDA、电子字典或手机的系统启动流程图，你可以看到，嵌入式操作系统最重要的任务应该是协调系统中多个task（或thread）的执行。

▲ 图6-11　系统启动流程图

6-4-1　操作系统的定义

菜鸟：“刚刚说了那么多有关‘系统’的事，这个系统和平常我们说的‘操作系统’有何不同？在我们的系统设计文件中，有一份叫做‘RTOS（Real Time-OS）设计规格说明书’，这个所谓的实时操作系统又是什么东西？我对‘系统’这个词已经有点混淆了。”

PM：“在嵌入式系统的世界里，每项产品都有自己的特性，可能都是用不同架构来实现的，如果你始终执着于名词定义的话，很可能会看不清很多事情。在回答你的问题之前，我先问你一个问题。当我还是个工程师时，开发了一个简单的操作系统，专用于电子产品。在某次正式的会议上，有位高级研发经理问了我一个问题‘你的操作系统中有没有WORD的功能？’。以你对操作系统的认识，你会如何回答这个问题？”

菜鸟：“这问题有点无厘头。WORD是Windows操作系统上的一个应用程序，它不能算是操作系统的功能。我可能会回答说：‘Windows本身也没有WORD的功能，它是要额外安装的，所以我的操作系统也不会有WORD的功能。’这样回答还可以吧？”

PM：“当初我的答案比你还充满不屑，接着这位长官反问我说：‘那么IE、Outlook Express、小画家，甚至踩地雷呢？安装好Windows之后就有这些功能，那么它们算不算是Windows这个操作系统的功能呢？’”

菜鸟：“这……这是强辩!”

PM：“先不管这位长官的专业素养如何，至少我们知道操作系统的定义是很模糊的。我们之前谈到嵌入式系统的可调整与可重组性，同一套系统用在不同的产品可能会含括不同的功能，那么，到底该如何界定操作系统的范围？在定义何谓嵌入式操作系统之前，我们先来看看一般对操作系统的定义。

操作系统（Operating System；简称OS）是一管理计算机硬件与软件资源的程序，同时也是计算机系统的核心与基石。OS身负诸如管理与配置存储空间（Memory Space）、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务。OS也提供一个让使用者与系统互动的操作界面。

OS的类型非常多样，不同机器安装的OS可从简单到复杂，可从手机的嵌入式系统到超级计算机的大型操作系统。许多OS制造者对OS的定义也不大一致，例如：有些OS集成了图形用户界面，而有些OS仅使用文本界面，而将图形界面视为一种非必要的应用程序。[11]

6-4-2　嵌入式操作系统

PM：“许多嵌入式系统的应用非常简单，这样的产品几乎不会用到操作系统的概念，软件架构就像你之前写的LED闪烁程序（请复习一下《第5章：实作你的第一个嵌入式系统》），充其量就是一个boot程序加上许多库函数，以及应用程序而已。

但现今的电子产品的应用日趋复杂，所以系统中确实需要一个‘核心’，负责工作调度（Scheduling）、存储器管理、文件系统等基本的系统服务。不管产品的应用是什么，都需要这个核心程序，我们称这个‘系统里的核心’为嵌入式操作系统。目前市面上可选用的嵌入式操作系统很多（请参考表6-2，你可自行到各个嵌入式操作系统的官方网站浏览），各有各的优点，不同的应用领域偏好使用不同的嵌入式操作系统。例如：网络产品大多使用Embedded Linux或VxWorks，因为它们网络功能支持较好，而目前手机产业则偏好使用Nucleus、Symbian OS、Windows Mobile 或μC/OS II。

此外，网络上Open Source的嵌入式操作系统多如繁星，其中不乏已广泛应用在商业电子产品之上者，例如：μC/OS II、Embedded Linux及ECos等。值得一提的是电子产品制造大国日本，自行制定了嵌入式系统的国家标准—μITRON，其定义了 Embedded OS的行为以及API。几乎所有日系的CPU都会搭配一套与μITRON兼容的Embedded OS，这代表的意义是：之前在其他CPU平台开发的程序，可以很轻易地移植到其他平台。”

表6-2　市面上嵌入式操作系统列表

Open Source的Embedded OS

需付费的Embedded OS

μC/OS

μC/OS II

Embedded Linux

Vx Works

ECos

Nucleus

μCLinux

pSOS

RTEMS

QNX

FreeRTOS

Windows XP Embedded

μITRON

Windows CE（Windows Mobile）

Android

Symbian OS

…

OS-9

BeOS

菜鸟：“那RTOS又是什么？和嵌入式操作系统又有何不同？”

PM：“我之前不是让你看了一本书吗？《μC/OS II——The Real-Time Kernel》可是嵌入式操作系统的入门书籍，你最好还是抽空详读，内容不算太难。一般电子产品实在用不到太复杂的系统功能，有的甚至不需要多任务的功能，再加上程序代码size的限制，所以嵌入式操作系统通常不会太复杂，无论是商用型或免费的Embedded OS，其架构、功能与特性都和μC/OS II大同小异，这就是我要你好好读这本书的原因。书中详细描述了μC/OS II有哪些功能，以及各个功能的原理与程序代码，在嵌入式系统的领域中，这样架构的OS我们称为RTOS。”

使用RTOS的优点有

■　使应用程序的设计和扩展变得容易，大大提高了开发效率。

■　多任务系统。

■　可满足实时（Realtime）的应用。

■　更容易满足系统的Reliability & Robustness需求。

使用RTOS的缺点是：嵌入式操作系统增加ROM/RAM等额外开销，以及5%〜10%的CPU额外负荷。

6-4-3　实时系统

菜鸟：“我在学校学过实时系统的概念，Real-Time分为Hard Real-Time与Soft Real-Time。前者最有名的例子就是核电厂控制系统，若使用Windows XP来控制核电厂，则当发生紧急状况时，什么时候可以得到正确的反应完全无法预测。所以Windows XP必然不是一个Hard Real-Time系统，因为其反应时间不可预测。而Soft Real-Time系统则是用来处理延迟反应的后果不那么严重的情况。我现在不懂的是，为什么嵌入式操作系统要称为RTOS，和Real-Time有什么必然的关系吗？”

PM：“这是一个好问题，我们在PC可以忍受Windows把明明很快的硬设备弄得慢吞吞，但是你可以忍受刹车系统两秒钟后才反应吗？与人身安全有关的系统当然要求实时反应，但就算是电视机、微波炉、电子辞典、手机、甚至电子鸡，按了键后有时反应很快，有时要两秒钟后才会反应，你觉得这种电子产品卖得出去吗？更不要说反应延迟在GPS、医疗设备、航空电子等领域会带来什么后果。

不论是什么应用，使用者就是无法忍受电子产品的反应迟钝或不可预测。因此，嵌入式操作系统有以下几个基本的要求。

■　可中断性（Preemptive）：不管系统目前的状况是什么，当紧急事件发生时，OS必须保证相应的程序会马上被执行，这是实时系统的第一个要求。

■　可预测性：OS中所有函数与服务的执行时间必须是确定的，这是实时系统的第二个要求。

■　稳定性与可靠性：这是实时系统的第3个要求。

这就是我们统称嵌入式操作系统为RTOS的原因。但自从微软加入嵌入式操作系统的战局，这个定义渐渐有点失真了，我之前买了一台Smart Phone，有时候用起来的感觉居然和XP差不多。”

菜鸟：“除了实时性之外，这样的OS还有什么特性吗？”

PM：“就把这个问题当作你的作业吧！你把μC/OS II那本书读完后，再来告诉我嵌入式操作系统的特色是什么。”

6-4-4　嵌入式操作系统的特性

菜鸟：“除了μC/OS II那本书外，我还到其他RTOS的官方网站看了一下，归纳出RTOS的特性与基本功能如下。

■　可移植性高（Portable）。

■　ROMable：通常嵌入式系统没有磁盘设备以及文件系统的思想，所以系统必须可在ROM里直接执行（Execution in place）。

■　可调整性（Scalable）与可重组性（Configuration）。

■　多任务（Multi-Tasking）与任务管理。

■　可调整的任务调度算法（Scheduling Algorithm）。

■　Task（或称 Thread）的同步机制：如 semaphore、Mutex 等。

■　Task 间通信机制（Inter-Process-Communication； IPC）：如 mail box、message queue 等。

■　中断管理。

■　存储器管理。

■　资源管理。

操作系统当然会提供各式各样的服务（或API），但其最重要的任务应该是负责协调系统中所有任务（task或thread）的执行，OS必须决定在某个时间点，哪一个task可以使用CPU，而决定的根据就是调度算法。系统是否可以达成实时响应是由调度算法决定，所以这些任务管理的功能就是嵌入式操作系统的精髓了。”

有些嵌入式操作系统，还包含调试子系统，在执行时期可以和PC的程序沟通，执行调试命令或送出调试信息，如图6-12所示。

▲ 图6-12　调试子系统架构图（范例）

6-4-5　嵌入式系统Task架构实例

PM：“那你知道我们系统中有哪些task，彼此间的交互关系如何呢？”

菜鸟：“这次开发的产品虽然功能很复杂，但并没有需要并行的功能，所以基本还用不到多任务。也就是说，应用程序只需要一个main task处理即可，但因为产品有省电的需求，所以当main task没有事情做的时候会自行sleep，则RTOS会将控制权交给优先级较低的idle task，由idle task负责让CPU进入睡眠模式。

当有硬件事件发生时，其ISR会将硬件事件传入main task的message queue中，此时CPU会从睡眠模式醒来，idle task继续执行，接着唤醒（Wakeup）main task。因为main task的优先级较高，所以RTOS自然会将CPU执行权交给main task，则main task可以处理新来的硬件事件，处理完后又会去sleep，RTOS再将控制权交给idle task以进入待机模式。执行时期系统就是根据这个循环不停地运行。”

我们的设计文件中有简单明了的Sample Code，如程序代码6-7所示，可以看出main task、idle task与ISR间的互动关系，如图6-13所示。

程序代码6-7：系统中task与ISR的交互关系

▲ 图6-13　系统中task与ISR交互关系的示意图

读了《μC/OS II》那本书后，你会发现嵌入式操作系统的原理其实还算简单，但实际上很少会有机会去重新写一个嵌入式操作系统，最多就是将现有的RTOS移植到其他平台而已。通常CPU厂商都会提供或推荐合适的RTOS，系统设计者除了弄清楚它有什么功能之外，设计系统该有哪些task及其交互关系才是重点。多任务的程序在执行时比较容易发生不可预测的问题，图6-14所示是一个很容易理解的例子，其中较低优先级的task被中断后，全局变量Temp的值就变了，执行结果当然不会正确。在应用程序不是太复杂的状况下，笔者建议可以采用上述程序的架构。

▲ 图6-14　多任务的应用很容易造成资源冲突或破坏

嵌入式操作系统并非本书的主题，若有兴趣可以详读《μC/OS II The Real-Time Kernel》一书，以下章节将不再赘述。

6-4-6　Multiple Tasking Programming注意事项

菜鸟：“我之前在Windows上学过Multiple-Threading Programming（多执行绪或多线程），基本原理与相关API都和我们刚刚讨论的RTOS的多任务（Multiple-Tasking）差不多。只是Windows上的‘thread’是执行于某个程序中，而RTOS中的task则是嵌入式系统中的基本执行单位。我很好奇，thread和task究竟有什么不同？”

PM：“这里谈的thread和task其实是一样的东西。Windows是一个Multiple-Process的系统，所谓process你可以把它想成一个个Windows的可执行文件。在Windows上，每个process都‘以为’自己拥有整个系统的控制权，实际上，就是每个process都拥有自己的地址空间，不会互相干扰。在这样的系统中，如果某个程序（Process）想要其中的多个模块并行（Concurrent）执行，则可create多个thread，而系统提供的调度器（Scheduler）则根据其算法在适当时机切换thread的执行权。简单地说，process是Windows的基本执行单位，而thread则是process内的执行单位。

回到我们的RTOS，基本执行单位就是task，而这些task共享地址空间，原理和多个thread共享一个process的地址空间是一样的。因此，在Windows上写作Multiple-Threading程序的注意事项，诸如 critical section protection、预防 deadlock 等，在 RTOS 上都适用；但 Windows 上若有个thread，最多就是把一个process搞死。在RTOS里的某个task作乱，则整个系统都会被影响！

既然我们做的是嵌入式系统开发，以后都还是用Multiple-Tasking这个名词。看你那么有兴趣，我们还是对RTOS的Multiple-Tasking做个严谨的定义。首先看一下图6-15，应该不陌生吧！”

▲ 图6-15　Multiple-Tasking-System 的执行流程

菜鸟：“这就是我们系统的执行流程，boot（）→main（）→create-tasks→sys_start（），然后user task会开始执行，完成了系统的启动，这个流程我都已经很清楚了。我认为这张图的重点应该是rtos_scheduler（）方块，当系统有多个task处于执行状态时，调度器必须负责选出哪一个task可获得CPU执行权。从图6-15中我们可以看出，调度器被调用任务的时机有3种。”

■　boot阶段结束，系统要选第一个task起来执行。

■　task调用系统与调度有关的系统功能（如sleep、delay、wakeup_task、wait_event等）。

■　硬件中断产生，相应ISR执行完毕前（硬件事件发生，意味着系统可能有更重要的事情必须执行，task间的优先权可能因为这个硬件事件而改变）。

PM：“没错！从图6-16中我们可以归纳出RTOS多任务系统的特性为：

■　Multi-Tasking System = Many User tasks + Scheduler。

■　Task的执行点（Entry Point）：就是一个C语言的函数。

■　Active Task = Functions + Context：Context可想象为task的执行状态，系统会为每个被启动的task保留一段空间，用以存储其执行状态。

■　系统会根据task目前的状态，将其信息（称为Task-Control-Block）存储在相应的List中，典型的task list架构如图6-16所示，调度器就是按照这些队列中各个task的信息来做调度。

说明了多任务系统运行流程后，最重要的还是得让所有工程师了解在多任务系统下，进行应用程序开发时必须注意的事项。你不是说在Windows上学过Multiple-Threading Programming吗？除了让task起床、睡觉外，还学了什么？”

▲ 图6-16　Task-Control-Block List

菜鸟：“当时在练习时确实是先create 了不少task，让它们一一执行，并观察其执行顺序，然后让某些task去睡觉，某些去等待硬件事件，再让其他task把这些‘休息中’的task叫起来。当初好像也就做了这些练习，但我记得还有一些IPC与同步机制也很重要，不过，那些名词如 message-queue、mutex、semaphore 等我都还给老师了。”

PM：“那就是没真的写过一个多任务应用嘛！我没时间一一教你这些功能的意义，自己抓紧时间查书去，过几天我会来抽问。

在多任务系统上写程序，许多规则还是得在实作经验中累积，一般程序员很难想象同样的程序代码，可能在不同时间会执行出不同的结果来。几乎所有状况都因为多个task共享有限的系统资源，这些资源包括CPU执行权、存储器（或全局变量）、硬件设备等。而会碰到的问题如：死锁（Deadlock）、饥饿（Starvation）、Critical Section等，这些名词都相当传神。

虽然Multiple-Tasking Programming并不如一般人想象的那么容易，我们还是可以归纳出一些规则与注意事项。先看以下这段有两个task的程序，你能看出这两个task执行的结果吗？”

菜鸟：“这两个程序实现了模块化，让decoder模块可以不必处理UI工作，或者当要更换另一个译码模块时，UI程序可以不需要更动。这个应用的重点应该就是影音播放时不能有中断的现象。因此，程序中对两个task的执行流程做了简单的同步：UI-Task在将影音数据放到buffer后，马上调用了 wakeup（）把decoder-task叫起来工作；而decoder-task做完译码后，则马上调用sleep（）让自己睡觉，使得UI-task可以立即再塞数据进buffer。

从程序看来，虽然两个task感觉像是各做各的，但通过一些同步机制，即可呈现接力工作的效果。”

PM：“世界真的有那么完美吗？你少考虑了两点：第一，调度算法；第二，既然你都说到模块化了，模块化的目的就是希望重复使用，也就是我们以后可能会把这两个task拿到更复杂的状况执行，届时系统的task可能就不只有两个了。你再想想看，在不同的环境下，这个程序还有其他的执行结果吗？

在开发多任务系统的应用程序时，我们最常听到的就是：‘这个模块在上个项目已经测过了，应该没问题啊？’、‘怎么这个bug今天又试不出来了？是谁偷改好了它？’、‘为什么加了一些无关大局的debug code之后，问题就消失了……”

菜鸟：“关于调度算法，在create task时需要指定一个priority（优先级）的参数，我知道错在哪里了！上例中，decoder-task的优先级一定要比UI-task高，否则，即便UI-task已经唤醒decoder-task，decoder-task还是只能在ready-queue中等待，永远无法被执行，因为尚有优先级更高的task（UI-task）正掌握着执行权（在UI-task的程序中，其并未主动放弃执行权）。”

PM：“你的答案还是只答对一半。你已经发现，当两个task的程序完全不改，调度算法确实会影响task的执行顺序，当调度算法是priority-base（保证最高优先级的task可优先获得执行权），这两个task确实会按照你说的流程去执行。但当调度算法是Round-Robin（每个task可以公平地获得一特定时间的执行权），即使UI-task并未主动放弃执行权，decode-task仍有机会被执行。

此外，若系统中除了 UI-task、decoder-task之外还有其他task的话，无论系统采用的调度算法是什么，都可能会影响执行结果。所以在写Multiple-Tasking程序时必须考虑很多事情，以这个例子来说，programmer可能必须在模块的文件或程序的注解中，详细说明：为了避免影音中断，此task必须执行于最高优先级，译码完buffer的数据后，本task会立即释放执行权”

菜鸟：“这样看来系统采用的调度算法真的是影响重大。可是，当我负责应用程序开发时，我怎么会知道系统采用的调度算法是什么？”

PM：“从这一点就可以看出研发团队是否够专业。首先，RTOS的调度算法一定要被设计成可设定的（Configurable），因为世界上没有一种调度算法可以完美地满足所有的应用。跟人类社会一样，某时候可以讲公平，但考虑效率时就得讲特权。第二，绝对是由应用来决定系统的调度算法，系统与应用程序工程人员必须充分讨论，绝对不能由系统团队闭门造车。

我看过许多团队在还不清楚系统调度算法时，应用程序开发就开工了。刚开始时，因为只有少数task在测试，可能还看不出问题，等到所有task都整合上来了，小则影响效率，大则完全run不起来。项目后期碰到这种架构性的问题是最令人头痛的了，所以有经验的团队都知道，既然要采用Multiple-Tasking系统，就必须在设计阶段就把系统调度算法确认下来，并且要让所有参与项目的软件工程师都确实了解这件事情的严重性。

既然都讲到这里了，我们就顺便把RTOS调度算法说一说吧！基本上，RTOS并不倾向使用太复杂的调度算法，以下是常用的算法。实际上，我们常常会基于以下某基本算法，根据应用的特性进行微调。

■　FIFO：先被排进ready-queue的task可一直执行到自己放弃执行权（exit、sleep、wait event 等）。

■　Round-Robin：所有在ready-queue中的task均分CPU执行权，task在执行了指定时间后会被切换出去。每个task每次可执行的时间称为time-slice，系统工程师必须对time-slice进行微调，以使系统可以最有效率的运行。time-slice如果太长，就等于变成FIFO；如果太短，则系统会忙于task-switch，而拖垮整体性能。

■　Priority-Base：系统保证ready-queue中priority最高的task一定可以优先获得执行权。

■　Priority with Round-Robin：Priority base 和 Round-Robin 两种算法的混合体。基本上，priority较高的task仍然会优先被执行，但其执行一段较长的时间后，系统会让priority较低的task获得较短时间的执行权，以避免后者永远都执行不到。这种算法看似‘贴心’，其实是让task间的运行顺序更不可预测。

■　EDF（Earliest Deadline First Scheduling）：执行期限越靠近，则相对的优先权也会调高。

■　RMS（Rate Monotonic Scheduling）：执行周期越短，优先权较高。

■　PM：“至于你的系统要选用哪种算法，则必须考虑到应用程序是否具有以下两个特性。

■　preemptive：是否任意一个task都可能因为有更重要的事情发生，而被中断其执行状态。

■　determinative：是否所有task的执行顺序，以及task switch的时间都必须是可预测的。”

菜鸟：“这个以前学校教过，记得当初老师老爱用核电厂发生意外当例子，我心里想，这种人命关天的项目还是别落在我的手上比较好，所以……”

PM：“所以又统统还给老师了，对吧？这不能怪学校没把学生培训好，在业界实际执行项目，通过亲身血汗体验所学到的东西，印象才会比较深刻。”

菜鸟：“我们刚讲了调度算法必须满足实际的系统应用的特性，除此之外，写作多任务系统还有哪些需要特别注意的事情呢？”

PM：“除了尽量消除task间执行顺序的不确定性外，Multiple-Tasking Programming最重要的应该就是critical section的保护了。要注意的是，多任务系统并不是要无限细化的切割task，相反的，系统中同一时间内运行的task越多，复杂度越高；这和我们在项目管理、沟通管理讲的道理一样，沟通的channel数=N（N-1）/2），人多真的不一定好办事。

不像Linux或Windows等系统，应用程序与系统成分属user mode与kernel mode，且每个应用程序有自己的地址空间，不会互相干扰。RTOS系统中的所有执行单位（系统、应用程序、ISR）共享地址空间，一不小心就可能互相影响（例如：应用程序可以access得到驱动程序或另一个task的全局变量），再加上多个task与不定时发生之硬件事件所触发的ISR，这些程序的执行顺序无法正确预测，更使得被共享的全局变量几无安全性可言[12]！为此系统必须提供保护机制。

这个issue可分为两个状况来讨论：（1）ISR与task之间；（2）task之间的critical section保护。首先我们来看以下这个例子，如图6-17所示。多个task可能会‘同时’调用函数fun（），与某硬件设备的中断处理程序ISR（）都会用到全局变量‘B’，碰到这种与ISR共享变量的状况，最好的保护方式就是fun（）进入critical section前先disable中断，则在critical section执行期间，不但保证ISR（）不会被执行，同时系统的调度器也不会被启动（还记得本节前面曾谈到调度器被唤起工作的时机吗？），变量‘B’就不会有机会被其它程序所破坏。”

▲ 图6-17　task与ISR间的critical section保护

菜鸟：“这个机制很容易了解，但编写上层程序的工程师怎么知道哪些程序段需要当作critical section 保护？”

PM：“既然我们都知道系统所有的程序会共享地址空间，就应该更严格控管全局变量的共享，全局变量未善加保护是造成多任务系统不稳定的主因之一。此外，模块间尽量通过接口调用，以减少通过全局变量来做沟通，也可减少critical section的数量，被破坏的机会自然减低，这是模块化带来的另一个好处。

禁止中断是最强而有效的critical section保护机制，但过度频繁禁止中断，则有丢失重要硬件事件的可能。操作系统教科书告诉我们几种task间保护critical section的方法，如Mutex（只有一个task可以进入critical section）与semaphore（可控制进入 critical section之task的数量），这些方法也被普遍地实现在所有的RTOS之上。

来看看以下这个例子，如图6-18所示。task2虽然priority较低，但因其先进入critical section，并将其lock了（调用了enter_critical_section()），所以，即使task2在critical section中被切换出去，由priority较高的task1执行，但当taskl调用enter_critical_section()时，发现critical section已经被lock起来了，系统会迫使task1放弃执行权并等待，使得task2有机会继续把critical section执行完。

当task2调用了exit_critical_section()，表示对critical section‘解锁’，此时，系统会判断有个priority较高的task1正在等待，就会立刻将执行权交给task1，由task1来‘锁住’critical section 。”

菜鸟：“这些东西以前都学过，可惜都还给老师了，回去得赶快拿课本出来复习。”

PM：“最后还要提醒一件事。虽然系统提供了保护critical section的机制，但工程师仍必须谨慎地使用全局变量与绝对地址，明确地界定各个task的分工，千万不要滥用mutex、semaphore等机制，否则容易造成deadlock或starvation，而这两者也是多任务系统debug时的梦靥。通过模块化，尽量降低系统的复杂度，减少变量共享的机会（即critical section的数量），才是解决此问题的王道啊！”

▲ 图6-18　task之间的critical section保护

6-4-7　移植RTOS上的应用程序至Embedded Linux

菜鸟：“我能了解一般嵌入式系统的应用，并不需要用到太强大功能的OS，所以在这个行业使用量最大的嵌入式操作系统，应该就是我们前面说的RTOS的架构。但现在Embedded Linux这么红，连Google的Andriod都使用Linux当核心。如果有一天客户要求我们扩展既有产品的功能，例如：将简单播放音乐的MP3播放器，‘升级’到具有大触控屏幕、可浏览网页的多媒体播放器，并要求保留既有的应用程序的操作流程与UI，但必须额外增加上网的功能。到时候，我们就不得不考虑使用Embedded Linux当新产品的OS吧？只是不知道既有RTOS上的研发成果要如何移植到新平台上？”

PM：“其实很多厂商都会碰到类似的问题，一方面计划扩展公司在该领域的产品线，希望从低端到高端通包；另一方面又不希望因为换了平台，核心应用必须重新开发，使得维护困难，并导致开发费用增加。

在技术面会碰到的问题就如你说的：如何用最简单的方式，移植RTOS上的应用程序到Embedded Linux，且程序代码修改越少越好。这个问题并不复杂，既然希望同一套程序可以执行在不同的平台上，我们要做的第一件工作就是理清RTOS与Embedded Linux本质上最大的差异是什么，之前给了你那么多时间去读我开的书目清单，你现在应该能够回答这个问题了吧？”

菜鸟：“Linux的架构在各方面都比RTOS复杂多了。基本上，Linux与RTOS都是多任务系统，但Linux的基本执行单位为process，每个process都有自己的地址空间，且process间不会互相干扰。而RTOS的基本执行单位为task，task的本质就是一个function，所有task共同share地址空间。”

PM：“理解没错，这件事的重点就是：Linux虽然是以process作为多任务的执行单位，但在每一个process中，其实还可以执行多个thread的（thread与RTOS上task的思想相同）。所以要让Linux可以共享既有RTOS的应用程序，基本的解法就是让RTOS上的多个task，执行于Linux的一个process内（如图6-19所示）。使得该产品的核心功能可在Linux上正常运行，然后系统设计者可以在Linux新增process来执行新增的高端功能。

有些原先以设计RTOS起家的厂商，如WindRiver，现在都有提供Embedded Linux的技术支持，既然这些OS厂商的产品线同时包含RTOS与Embedded Linux，他们当然必须提供足够的技术建议，让客户可以很快地将既有应用从RTOS移植到Embedded Linux上。

▲ 图6-19　移植RTOS上的应用程序到Embedded Linux上（I）

图6-20所示是两种Solution的示意图，基本思想是一样的。”

▲ 图6-20　移植RTOS上的应用程序到Embedded Linux上（Ⅱ）

06-05　模拟器

菜鸟：“我负责的模块和硬件无关，系统软件部门主管要我先在模拟器上开发即可，但我有点不太清楚嵌入式系统的模拟器是个什么样的概念？大家都没空理我，书上也没有相关的知识，所以我只好来问您。

我常常在PC上玩电视游乐器的模拟器，在网上找了一堆游戏的ROM，像是任天堂、超级任天堂、Game Boy Advance、PlayStation，甚至大型电玩机台都有模拟器。模拟器似乎会模拟电视游乐器的硬件，包含CPU、影音输出等，操作ROM里面的程序和数据就像真正插入游戏卡匣或光盘一样。我们的模拟器也是这样的概念吗？”

PM：“一般我们说的模拟器有两种，一种叫Emulator（如图6-21所示），另一种是Simulator[13]。你说的那种电视游乐器模拟器是前者，主要模拟的层次是CPU或外围设备，它会从ROM file中找出一行一行电视游乐器CPU的机器码，辨识后再模拟该CPU的行为。简单地说，就是在PC上模拟执行其他CPU的机器码程序，我们通常称Emulator为Binary Level模拟，因为它是直接执行机器码程序。

而所谓的Simulator主要是模拟系统与应用程序的层次，如图6-22所示。例如有些模拟飞行的PC Game，就是Simulator的最佳实例。因为它并非直接模拟飞机控制系统的CPU，而是在PC上通过程序模拟硬件与环境（用电脑绘图模拟仪表板、天空与地面环境等），再把控制飞机的算法和上述模拟硬件的程序连接在一起。虽然说Simulator是在模拟某个系统，但它却是个货真价实的PC程序。所以我们通常称Simulator为source level模拟，它不像Emulator要模拟执行其他CPU的机器码，也就是我们可以在PC上检验系统以及应用程序的算法，如果有问题，我们可以直接修改程序，而这是Emulator所做不到的。”

▲ 图6-21　Emulator——在PC上执行Game Boy Advance的游戏

菜鸟：“归纳一下，Emulator和Simulator都是PC的程序，但前者是模拟执行其他CPU的机器码，而Simulator则是要设法让系统与应用程序中与硬件无关的程序代码（Source Code），使其能够在PC上执行。这样理解没错吧？”

PM：“没错！所以你觉得嵌入式系统开发项目所说的模拟器是指Emulator还是Simulator?”

菜鸟：“现在我知道我们做的是Simulator 了，因为我们用的这颗CPU不像ARM或8051那么被广泛使用，所以要去开发一个模拟该CPU的程序，简直是不切实际（几乎可以开另一个项目来做这件事了）。而且我们的硬件平台上有那么多不同厂商的芯片，要让我们的程序代码（机器码）可以在模拟器（Emulator）上执行，怎么想都是一个大工程，更不用提有的产品项目乃采用客制化的CPU 了。而且当下一个产品换了 CPU，整个流程还要再来一次，这样的话，我们公司干脆改行做‘专业CPU模拟器开发’算了。

▲ 图6-22　在Visual Studio上对Simulator调试

但我回想起以前在学校修单片机实习课时，是采用8051这个CPU，当时老师教我们的流程是先把程序写好，并编译成机器码，然后先在8051模拟器（Emulator）上执行。当时我们用的那个开发环境就有包含8051 CPU Emulator，等确认无误后才放上板子执行。居然没有老师曾经提过开发电子产品要搞一个Simulator?”

PM：“这可能是产界和学界的另一个断层，以前业界使用Emulator的状况很多。我觉得有几个原因：第一，以前可选用的CPU比较少，CPU复杂度低，所以有的CPU或编译器厂商会提供CPU模拟器；第二，以前硬件板子制作比较麻烦且昂贵；第三，以前电子产品的复杂度较低。记得我跟你提过吗？有的电子产品开发是没有嵌入式操作系统的，对一些老一代的工程师来说，只要有Emulator他们就可以搞定软件开发，不需要Simulator的协助。

但是时代渐渐在改变了，现在电子产品的应用程序越来越复杂，产业分工越来越细，已经不再是以前那种由一、两位超级工程师，包山包海就可以搞定的。再加上进度很赶，系统程序与应用程序开发人员不可能等待硬件完成才开始开发或测试，这样肯定来不及。因此，现在嵌入式系统项目在运行时，模拟器（Simulator）、系统、驱动程序和硬件开发几乎都会同时进行，当模拟器完成基本功能之后，系统就可以开始在模拟器上测试。等系统基本测试通过后，接着应用程序开发可以启动。等硬件与应用程序稳定后，几乎就可以做第一次的产品系统整合（硬件+驱动程序+系统+应用程序）。

此外，开发Simulator的好处很多，在硬件还没稳定之前，客户或规格制定人员就可以先看看应用程序的开发有无偏差或是否有需要改善之处。而且可以把模拟器想象成另一个硬件平台，如果我们的系统与应用程序都是先在模拟器上开发的，自然就可验证系统的可移植性设计是否有问题。”

菜鸟：“的确很有意思，可以抽空跟我说一下我们模拟器的原理吗？”

PM：“现在没空啦！你先把思想弄懂即可（有关嵌入式系统模拟器的实作请参阅《第14章：模拟器》）。”

06-06　Source Tree设计与程序风格典范

菜鸟：“我负责的模块已经写完，并经过基本的单元测试，我该如何把它整合到我们的系统上呢？”

PM：“我们不是有一份source tree设计文件吗？哪些模块要放在哪一个目录下，里面规划的很清楚，当你加入新的程序后，记得写个mail通知负责系统整合的人员，他会更改mekefile，然后你最好再花时间检查一下，不要让系统build不过，也不要因为你的程序让系统跑不起来，忙碌中的工程师脾气可是很差的。”

菜鸟：“我的程序怎么可能会让系统跑不起来？我都测试过了耶！”

PM：“这种例子常常发生，你以为OK，但只要整合到系统来就会有问题。原因很多，这里就不赘述了（请参考《第10章：设计硬件抽象层》）。我举个简单的例子给你看，如程序代码6-8所示。”

6-6-1　整合时的重要思想

程序代码6-8：新模块整合到系统时可能会出问题

PM：“如果模块设计得好，它的独立性应该很强，在其他系统模块还没稳定或完成之前，通常都可以先在Windows的开发环境（如Visual Studio）上执行，除了可以在稳定的环境上开发外，程序调试也比较方便。相信你也是这么做的吧！

就上面这个例子来说，模块独立开发时会执行Point A，基本上，向Windows要求配置动态存储器很少会失败的，所以这个程序在Windows上可以通过测试。但整合到开发中的系统可就不同了，系统会执行Point B向系统要求配置存储器，可能在sys_init_rookie()执行前，其他的程序就把存储器要光了，也很可能os_malloc()根本就有问题，甚至可能存储器驱动程序还没完成，所以系统执行到sys_init_rookie()就会停下来。此时，你就会听到办公室中骂声四起，因为所有人的系统都执行不了。”

菜鸟：“但这是别的模块的问题，不是吗？”

PM：“是没错，但却是因为加入你的程序才导致系统不能运行，而这个动作浪费了团队大部分人的时间也是不争的事实。这是思想问题。团队中每一个工程师在整合某个新的程序到系统之前，都必须先从Server[14]上下载目前最新版本的系统，在自己的计算机上测试后，确认不会影响其他人，才可以将这个程序加入Server。”

菜鸟：“如果发生问题时应该通知谁？难道没问题的程序，就因为其他先加入的模块有问题而不能加入吗？”

PM：“假设原来的系统是可以动的，因为加入你的程序而不能动了，那么谁最适合来查这个问题？应该就是你吧，即便最后查出不是你程序的问题，那你也应该通知部门主管，越早找到隐藏的问题越容易解决，这对研发团队而言是大功一件啊！

此外，所有的程序都应该接受版本控制，所以我的建议是工程师还是要把程序加入Server，并且先自行记录这个问题，稍微修改一下程序让系统可以继续执行。例如：先mark掉sys_init_all_system_modules()中的sys_init_rookie()，等系统稳定后，再回来做该模块的整合测试。”

6-6-2　Source Tree 设计原则

菜鸟：“话说回来，我们系统的目录结构是谁负责设计？用什么原则设计的？”

PM：“基本上应该由能纵观全局的人来设计，目前的目录架构几乎是沿用以前的项目，最早是本人在当资深工程师时设计的。有什么意见吗？”

菜鸟：“意见是不敢啦！我只是想知道设计目录架构的原则是什么？整个项目那么多的程序文件，要如何井然有序地安排？目录结构太复杂似乎也不好，一堆文件挤在同一个目录下，就如同没整理一样也不好。”

PM：“你只要想想嵌入式系统的特性，就知道该怎么摆了。”

菜鸟：“这个之前您说过，嵌入式系统软件首要重视可移植性、硬件相关与硬件无关的code要分开、产品相关与产品无关的code要分开等。

这样我有点概念了，您的意思是说，好的嵌入式系统程序的目录架构，应该可以很容易在其他的项目中重复使用。假如下个产品的硬件平台不变，只有应用程序改变，则系统与driver的程序目录就可以直接拿来用。如果下个产品的硬件被换掉，但系统与应用程序只需小幅调整，则只需把驱动程序的目录替换掉即可。”

PM：“没错！思路就是这么简单。但在实际上往往没有办法达到这么理想，总会有人把程序放到不合适的目录，不然，就是在硬件无关的模块里写入和硬件相关的程序代码，研发主管只有不断用心地code review，才能避免这种不受规范的程序产生。偏偏到项目后期每个人都忙得很，而且人都是短视的，总想着无论如何先让目前的项目可以结项再说。所以在设计阶段就该把source tree构建好，相关规定越早发布越容易施行，因为此时完成的程序还不多，工程师也比较愿意配合。

而且我们source tree的结构并不复杂，应该看了就知道每一个目录是做什么的。基本上，一个目录可以对应到系统架构图里的一个方块，以下稍微解释一下目前项目的目录架构[15]。”

■　Driver：驱动程序目录

□　Boot-Loader：Boot-Loader 程序目录

□　Hardware：各个设备的驱动程序

□　Include：驱动程序的 header file

□　API：将所有驱动程序包装成API

□　Boot：Boot 程序

□　LCD：LCD程序目录

□　SDRAM：SDRAM的驱动程序

■　System：系统程序目录

□　Include：系统程序的 header file

□　API：将所有系统程序包装成API

□　Common：系统程序中的通用功能

□　RTOS：嵌入式操作系统目录

□　Include：RTOS 模块的 header File

□　API：将RTOS的功能包装成API

□　Memory：存储器管理

□　Sync：task间同步机制

□　IPC：task间通信机制Task

□　Glib：图形库函数

□　Sub-System：各个子系统目录

□　GUI：图形用户界面子系统

□　TCP/IP：TCP/IP Stack 子系统

□　FileSystem：文件系统

■　AP：应用程序目录

□　Include：应用程序的 header file

□　Source：应用程序的程序代码

□　Common：应用程序中的通用功能

□　AP1：应用程序1的目录

□　AP2：应用程序2的目录

□　Resource：统一管理应用程序中的图形、字符串等资源文件

■　Third_Party_Library：由协助厂商或网络取得的库函数

□　HandWriting：手写辨识库函数

□　Font：字形

□　声音压缩库函数

■　Include：项目的header file，sys_config.h（系统配置）就放在这个目录

■　Build：所有用以制作系统可执行文件的文件，如makefile及link script

■　Simulator：模拟器目录

□　Build：模拟器的build目录，可以产生模拟器可执行文件

□　Simulated Hardware：在PC上模拟硬设备

□　LCD：模拟LCD的程序

□　Keyboard：模拟 keyboard 的程序

■　Tools：开发过程所需要的工具

□　Cross Compiler：CPU厂商建议的编译器

□　Offline Tools：开发过程会用到的工具，可能由团队自行开发

■　Document：和项目有关的文件

□　Project Management：和项目管理有关的文件图表，如schedule等

□　Design Spec：所有设计文件

□　Hardware：硬件相关的文件，如线路图、芯片的data sheet等

□　Others：其它技术文件或sample code

上述source tree的结构只是一个范例而已，你应该根据项目特性与需求自行制定之。重点是制定项目目录结构时的思想，尽量能表达系统架构的精神，不要让不同性质的程序混杂在一起，否则不但难以管理，以后要移植到其他产品项目或平台也会比较困难。

6-6-3　程序风格典范

PM：“按照本团队的惯例，我们会对新进软件工程师所写的第一个程序做code review，以确认他的程序风格没有大问题。每位工程师写程序的习惯和风格都不同，原本也没有什么对错，但我们是研发团队，绝对不能任由乱七八糟的程序代码充斥在系统中。正确性、可读性和可维护性一样重要，否则，当某个艺术派软件工程师离职后，恐怕没有人具有一样的慧根可以改动他的程序。在我的团队中，所有软件工程师都必须遵守一份叫做‘程序风格典范（Programming Style Convention）’的文件。所谓的程序风格，不仅是命名规则，还规范了程序代码中至少应该具有哪些信息，以及程序编写的注意事项。”

菜鸟：“我是有尽量按照程序风格典范的规定来写作程序，但有个不懂的地方。我们规范的命名规则很简单，为什么不直接用匈牙利命名法？”

PM：“确定你真的了解什么叫‘匈牙利命名法’吗？它到底规定了什么？恐怕知道的人也不多吧！而且我的经验告诉我，越复杂的要求，工程师越会阳奉阴违。其实一般研发单位用的都是简化过的匈牙利命名法，重点是它要表达的思想：让一个变量名称除了代表一个变量本身之外，更重要的是让检阅程序的人能够对该变量的意义一目了然。我们当然可以根据我们的实际状况来更改命名规则，例如，我认为在嵌入式系统中，全局变量（Global Variable）和局部变量（Local Variable）的重要性不同[16]，所以我就会规定者两种变量的命名方法应该有所区别。除了命名规则之外，工程师写作程序必须注意的事项为：

■　该程序文件的用途描述。

■　编写者与日期。

■　修改履历。

■　用特殊显眼的符号区分程序段。

■　每个function必须详述用途、各个参数的意义、返回值的意义。

■　每个全局变量必须详述用途。

■　定义数据结构时必须详述每一个栏位的意义。

■　尽可能多写注解。如果合作单位有外国人，则尽量写英文；但如果英文表达真的有问题，则宁愿用中文写。否则，即便写了一堆注解，如果没人看得懂也是枉然。

■　程序中的缩排必须整齐，最好用tab键，或直接规定缩排应该要空几个空格。

■　如果某一个循环（for and while loop）或条件式判断（if）的内容太长，则在结尾大括号（}）的后面要加注解，说明这个循环或条件式判断的内容。

■　不要吝啬空白行。

■　一个程序文件的行数不应该太多（笔者认为一个程序文件不应该超过一、两千行）。你不是说你负责的模块已经做好了吗？那么，我们今天就来review一下你的程序吧。”

菜鸟：“OK〜我负责的是OS模块中的Message Queue功能，程序名称也是按照规定取的，叫做os_message_queue.c。程序代码如程序代码6-9所示[17]。”

程序代码6-9：程序风格规范

软件开发团队经常发生这样的悲剧。接手的工程师完全看不懂离职者留下的程序，更甚者，自己写的程序，过了两个月后自己也看不懂了。像下面的程序段，工程师可能觉得自己真的是天纵英才，但以项目管理的角度来看，这样的程序只能称为系统的‘祸根’。

为了修改程序中的bug或想为其新增功能，与其花很长时间把它看懂，工程师会倾向自己重写。对项目的伤害是必须重复的投入人力与时间在同一件事情上，而且“返工”做出来的模块还必须再测试一次，以避免影响其他模块。

即便市面有许多功能很强的Editor与程序代码管理工具（如Source Insight、Ultra-Editor、Ultra-Compare、WinMerge），可以做Syntax Highlight或把程序之间的架构与交互关系找出来。但为了程序代码的可读性与可维护性，工程师的程序风格绝对必须被限制，这种限制应该清楚地写在内部质量文件中，而且项目经理必须命令各部门主管严格执行code review，以确定所有工程师都有根据规定执行。

另一方面，这个限制必须合理、清楚且简单，不能为工程师带来太多困扰。本节的范例也许不是最好，但却是笔者实际运行于嵌入式系统开发项目的实例。你可参考，为自己或团队量身制定程序风格典范。

6-6-4　Header File

PM：“不错！年轻人的可塑性还是比较高。很多资深工程师把遵从这些规定视为痛苦不堪的事情，他们在之前的工作岗位没能养成良好的程序编写习惯，有的甚至以写作别人看不懂的程序为荣。对项目管理者而言，这些员工都是team work的漏洞，必须花时间要求小主管去疏通他们的情绪，无论如何，都要把这个制度run起来。对了！让我看看你这个模块的.h文件”

菜鸟：“OK!”

.h文件要遵循的规范如下。

■　所有程序文件的规定。

■　避免重复include造成的重复定义。基本上，header file的开头和结尾应该是

■　常数和宏（MACRO）定义必须描述清楚。

■　定义数据结构（struct、union、enum）和数据类型（typedef）时，必须详述其意义

■　不要在.h文件中实现函数或定义变量，也就是在header file中的是函数或变量的声明（declaration），而非定义（definition）。

菜鸟：“根据本团队‘程序风格典范’中的规定，每个模块的header file可以视为该模块简单的技术文件。也就是说，要使用这个模块的人员，只要看过header file，就应该可以在他的程序中使用这个模块。根据这个要求，我从设计规格书中贴了很多说明到os_message queue.h中当注解。刚开始我也觉得这种做法麻烦又费时，后来才发现有其他员工把我的这个.h文件给打印输出来当作文件参考。心中有一点莫名的成就感，我觉得这样才是专业的程序编写风格。”

PM：“随着项目的进行，工程师会慢慢发现，这些规定虽然对他们在开始程序编写时造成了一些短暂的不便，但到了后期调试与维护时，这些写在程序里的信息，可为他们节省可观的时间。”

6-6-5　嵌入式系统C语言程序编写指南：MISRA C

菜鸟：“我们系统做起来之后，程序少说也有个十几二十万行，任凭主管或工程师再怎么精干，也不可能review完所有的code。尤其到了项目后期，我能想象大家忙着解bug，客户拿着鞭子在后面盯着，恐怕什么程序规范都已抛到九霄云外了。到时PM老大您恐怕也不会有空吧！我就曾在以前项目的程序中看到很多没注解、变量名称乱取的状况。

我认为制定coding style的立意真的很好，也确实可以为团队的发展带来莫大的好处，但执行起来似乎困难重重啊！”

PM：“你说得没错，大部分的项目主管都会因此而睁一只眼、闭一只眼，而工程师也已习惯每个项目对codeing style的要求都是雷声大、雨点小。要由人工解决这个问题非常困难，除非把这件事情变成开发流程上不可或缺的步骤，就像当程序编译失败，若不找出语法错误的地方，就无法再继续下一步一样。因此，我们在构建开发环境时，必须强力导入‘静态测试（Static Test）’工具。”

菜鸟：“我们不是在谈coding style吗，怎么突然说到测试了？”

PM：“你在学校不是修过Compiler吗？能不能说说Compiler的工作原理。”

菜鸟：“（心中OS：才刚说你离题，现在又跳到编译器去了……）编译器最重要的工作就是分析输入程序代码的语法，是否合乎该语言的要求，至于其中语法分析的算法，我早就忘光了。”

PM：“我们以C语言的编译器为例，编译完后会告诉你有多少error与warnning，所以Compier不只是检查程序是否合乎C语言的标准语法，它一定还有一些自订的内部规则，否则Compier应该只会输出error才对。由此可知，Compier已经帮programmer做了部分的‘静态测试’，这些warnning其实正在警告programmer：虽然你的程序合乎C语言的语法（没有error），但某些写法可能会有潜在的错误，你最好还是检查看看这些有warnning的程序段吧！

这也告诉我们一件事，只有标准C语言（ANSI-C）的规范是不够的，显然许多厂商都因工程师天马行空的coding style而吃过亏。”

菜鸟：“这道理我知道啊！你不是一直在推动‘0 warnning运动’吗？要求工程师要重视每一个warnning，测试团队甚至可以退回未处理warnning的版本，这很符合你‘在流程中推广思想’的基本项目运行思想，同事们也都能配合。

我已经隐约知道你要表达的意思了。既然Compiler可以多一些规则来警示programmer可能有潜在错误的程序写法，如果Compiler也可以根据我们公司的coding style对程序进行检查，而且最好把违反规则的程序段都显示为error，那么，工程师自然就无法违反coding style 了。你说这种Compiler帮我们做的事情叫做‘静态测试’？但据我所知，我们的Compiler只可以设定warnning的强度，甚至可以把某个等级以上的warnning当作error，但并没有让我们自己设定新规则的功能啊！”

PM：“Compiler已经帮我们做太多事了，静态测试的工作就交给其它工具帮忙吧！如PC-Lint、LDRA Testbed、LogiScope/Rule-Check……。以我们使用的 PC-Lint 为例，它可以与市面常用的程序编辑器（如 Sourcelnsight、UltraEditor、Virtual Studio .Net 等）集成，programmer使用这些工具就像使用Compiler一样方便，这个工具会扫描你的程序，并像Compiler列出error/warnning一样，列出所有违反规则的程序段，如图6-23所示。

工程师使用起来相当方便，甚至可以请测试人员在开发阶段，就帮忙统计各个程序违反coding rule的状况。因为这些违反coding rule的程序代码，都可能是之后run-time问题的根源，所以把它当作某种形式的bug也未尝不可，因为这种找bug的动作不需要系统实际run起来就可以进行，所以称之为‘静态测试’[18]。”

▲ 图6-23　urcelnsight整合PC-Lint执行静态测试的结果

菜鸟：“它内置的rule合乎嵌入式系统软件开发的需求吗？我并没有看到您安排人员根据我们公司的coding style，去开发这些静态测试工具的rule啊！”

PM：“开发这些rule确实也是工程一件，但开发完成后即成为可贵的组织资产。可惜我们的老板始终不愿意投注人力做这件事情，但上有政策，下有对策，既然我不想任由工程师以自己的方式写code，一定要通过静态测试工具来管控，所以我在制定coding style时就会参考这些静态测试工具已经有的rule，项目进行时直接拿来用就好了。

说到这些 rule-checking，就不能不提 MISRA C（由 the Motor Industry Software Reliability Association这个组织所制定，最新版本为MISRA C 2004 & MISRA C++ 2008）。这是一套专为嵌入式系统所制定的程序编写规范，而大部分的静态测试工具都会支持MISRA C的rules，至少我们使用的PC-Lint就已支持MISRA C。

软件工程方法学并无法完全保证工程师都可写出具可靠性的程序，但遵循正确的coding rules的确可以显着减少程序中的错误。MISRA C便是这样一个很有价值的工具。它是一个C语言的子集，并定义了 141个规则（包含在21个类别）来约束限制C语言的使用。这些规则设计的很好，指出了很多可能的错误和会影响可移植性的结构和用法，更重要的，这些规则乃基于嵌入式系统的特性所制定的。著名的嵌入式操作系统μC/OS II，就特别宣称其99%的程序代码都遵守MISRA C的规则，可见MISRA C在嵌入式系统的重要性。”

菜鸟：“如果您认为MISRA C那么重要，怎么我完全没听过？此外，μC/OS II为什么不加把劲，直接做到100%，要留个1%的code不符合标准？”

PM：“MISRA C是一个C语言扩展的典范，但它不像ANSI-C是国际标准，嵌入式系统开发几乎都是使用C语言，而嵌入式系统又有着必须注意可读性、可靠性、可移植性、可维护性等特性，所以我们必须适当约束程序员，而MISRA C是目前最完整的嵌入式C语言的编码典范，已有许多国际公司与中国公司都已导入。

嵌入式系统的应用领域既多且杂，因此，MISRA C的规则分为两种等级：强制规则（Required）及推荐规则（Advisory），还是保留了弹性给使用者，这也是μC/OS II不做到100%的原因吧。”

菜鸟：“有没有MISRA C的文件可以让我参考？”

PM：“其规范文件必须付费才可取得，本公司一如往常不愿意花这个钱，你可自MISRA官方网站（http://www.misra-c2.com）进一步了解MISRAC 2004的细节，或者问问Google也很容易找到完整的信息。虽然MISRA C不是国际标准，而且需要付费才能取得规范文件，但如前面提过的，大部分静态测试软件都已集成了 MISRA C，所我们在使用这些软件时，就可以得知其规范的条文与解释了。图6-24所示就是使用LDRA这套工具配合MIRSA C++2008的状况。”

▲ 图6-24　使用LDRA搭配MIRSA C++ 2008对程序进行检查

06-07　SDK

菜鸟：“有个问题一直困扰着我。所谓SDK（Software Development Kit）应该是以开发系统为主的大公司，如Microsoft、Apple、Google，提供给产品开发商，让他们可专注于诸如Windows、iPhone，Android应用程序开发的套件。而本公司主要是接电子产品开发项目，面对的客户通常就是出钱与出嘴巴，比较积极的客户会介入我们的质量检测流程，我的意思是说，客户并不直接参与开发工作，我们也不需要提供开发套件给客户。

既然如此，为什么我们在做系统架构设计与工作分组时，还要特别组成SDK team，负责所有与嵌入式系统平台相关的工作？”

PM：“记不记得我对所有项目成员都有一个要求：永远要把上一层的团队当作客户。从系统架构图来看，system team 是 driver team 的客户、middleware team 是 system team 的客户，而AP team则是所有软件研发团队的客户，若再往后延伸，工厂是硬件、软件团队的客户等。唯有用这样的思想做事，团队间才能权责分明，而系统中各部分的规格才有办法真正符合外部客户的需求。

但据我观察，一般研发团队的文化恰恰相反，他们认为越底层才是越有技术的，既然底层团队以技术挂帅，一不小心就会变成闭门造车，完全不认真考虑上层的真正需求。而上层团队往往也只能逆来顺受，用不合适的系统接口，勉强地按照客户的规格开发应用程序，工作效率与产品质量难免因此而大打折扣。无论底层或上层团队，对公司来说一样重要，我要表达的是‘越接近真正客户的团队，他们的意见越应该被重视’。

我一直设法跟公司高层与项目成员灌输这样的思想。消费性电子产品的开发一定是top-down的，一切都必须由终端市场或使用者需求启动，也就是所有的研发目标都必须对准end user，所以硬件上最重要的是结构外观与制造成本，软件上则是应用程序，然而很多公司都本末倒置了！”

菜鸟：“每次我问A，您都不正面回答，往往答案都直接跳到X、Y、Z去。恕弟子驽钝，我的问题是：为什么嵌入式系统开发团队必须维护自己的SDK？您却回答要把上层当作客户、开发产品应该是top-down。是不是有点太离题了？”

6-7-1　为什么嵌入式系统开发团队必须维护自己的SDK？

PM：“我不至于那么跳tone吧！你仔细想想，既然微软必须提供SDK给开发Windows应用程序的客户，为什么我们的系统团队不必为应用程序团队准备SDK？

在讨论下去之前，你先说说我们的SDK里面包含什么套件，有什么用途？”

菜鸟：“SDK的目的其实就是建立我们（即本章）一直讨论的嵌入式系统平台，提供应用程序开发人员‘舒适’的开发环境与足够的系统功能。根据我们的定义，SDK（或说嵌入式系统平台）应该包含：

■　开发环境：tool-chain + IDE + debug tool。

■　系统功能：为所有系统与硬件功能定义明确的API，并将其implementation包装为库函数（Library）。若有必要，可提供少数系统模块的源码，供上层工程人员做客制化之用。

■　API文件与SDK使用手册。

■　模拟器环境（Simulator或Emulator）：图6-25所示就是iPhone OS SDK提供的模拟器环境，当没有硬件环境时，也可以进行应用程序的开发。

■　硬件环境：EV-board（评估板）、EV-board使用手册、线路图。

■　其他工具：系统更新工具、系统性能统计工具（Profile Tool、Benchmark Tools）、Resource（text & image）转换工具等。

SDK包含了必要的开发工具与范例程序，让工程师能够马上进行应用程序的开发，不必理会程序编译、image build、下载、调试等细节。此外，系统与硬件功能应已经过严谨的抽象化，工程师只要关注自己要开发的部分，系统功能都可以当成是黑盒子。”

PM：“根据SDK的定义，你再思考一下，包装SDK会需要多少额外的effort以及SDK可以为我们这样的嵌入式系统开发团队带来什么优势？”

菜鸟：“按照以上的列表来看，全都是本来就必须要做的事，只是找个专职单位负责将其包装成SDK Package，让其他工程师可以专注于应用程序的开发，以满足客户的真正需求。有些单位会认为包装SDK Package需要更多的时间人力资源，我想是因为制定不出API的原因吧！若在设计阶段，系统架构中并未严格规范driver、system、middleware、AP之间只能通过API沟通，则工程师在实作时，上下层间难免交互参考，造成系统与应用程序模块间的耦合度过高，此时才想要包装出所谓的SDK，当然是困难重重。

▲ 图6-25　iPhone OS SDK提供的iPad模拟器（Emualtor）

至于SDK能为嵌入式系统开发团队带来什么优势，我就想不清楚了。刚刚才说过，SDK并不会对我们这种本来就具备严谨系统架构设计的单位造成额外的effort。换句话说，SDK team除了集成嵌入式系统平台为SDK package，并持续maintain外，其实并没有做什么新的或额外的功能。我实在看不出来SDK能为当前项目带来什么好处，难道SDK能带来的优势是在下一个项目吗？ ”

PM：“Bingo！答对了。工程人员的青春有限，既然我们得面对一个又一个的开发项目，且前后项目之间难免会有相似性与延续性[19]，此时，SDK能为团队带来的优势就显而易见。你刚刚说SDK team并不需要开发新的额外的功能，但他们必须负责maintain SDK,‘maintain’这个词就是关键所在。

维护SDK需要做的事情太多了，其中较重要的有两项。”

■　与上层“客户”讨论SDK需要release的API：必须兼顾此项目的应用，以及以后项目的扩展性。

■　从source tree与makefile，保证SDK与“隶属于某项目之应用程序”之间的独立性。

菜鸟：“第一项我懂，SDK必须提供真正上层需要用的API，这当然必须与application team或middleware team密集讨论，而application team则必须负责确实了解客户产品的规格需求。第二项从我们团队与其他团队在运行了多个项目之后，source tree的管理状况就可以看得出来了，如图6-26所示。”

▲ 图6-26　source tree管理方式之优劣

PM：“因为在当前项目中已配置了技术团队专门负责维护SDK，使得下个项目可以沿用前一个项目已通过量产标准的系统与工具，从而达成组织研发资产的累积。此外，其好处还包含：

■　在项目初期就能够完成新项目的prototype，以供客户评估。

■　当项目中某些模块需要外包时，可使厂商有个标准平台进行开发与测试。

■　若组织中所有项目都采用具有一贯性的SDK架构，则新人较容易进入状况，经验传承不致中断。

■　通过SDK，可明确地将系统与应用程序区分开来，从而使系统易于维护，也容易根据不同项目特性进行客制化。

我们公司是电子产品开发商，对外界来说，我们卖的是电子产品方案，使用SDK是为了研发资产的积累。但现在的SoC设计公司都标榜提供Turn-Key Solution，卖的虽然是IC，但必须提供客户使用此SoC进行设计产品时所需的完整软硬件方案，且这些方案必须已达到量产水平，并易于完成局部客制化。简单的说，SoC设计公司必须提供客户一套SDK，而且这个SDK需要经历更严谨的设计过程，才有办法满足为数众多的客户。”

6-7-2　SDK for Turn-Key Solution

菜鸟：“我有个同学，就在您说的那种SoC设计公司的SA（System Application）团队上班，最近因为新IC准备上市，他们几乎天天为了SDK的bug无法收敛而加班赶工。我这才知道，原来除了像我们这种产品设计公司需要‘内部’的SDK之外，连我一直认为最‘硬’的IC设计公司，也必须提供更完整的SDK给客户。

请问，这两种厂商开发的SDK在本质上有什么不同？还有你刚刚提到的Turnkey Solution在新闻里常听到，但真正的定义是什么？ ”

PM：“我们先从Turnkey Solution讲起吧！Turnkey Solution是统包解决方案的意思。从字面来看，Turnkey为转钥，也就是负责承包项目的厂商，从头负责到尾，最后交给客户一把钥匙，客户只要把钥匙一转，系统便可开始运行的意思。用在IC设计领域，Turnkey表示必须提供给客户完整的方案，即转手即可生产的SDK。

要了解何谓符合Turnkey Solution精神的SDK，首先你必须知道MTK（联发科）成功的故事。

业界往往把联发科（MTK）在手机IC方面的成功归结为两个因素。第一，‘一站式’的Turnkey Solution模式，在将IC销售给客户的同时搭配以软件，客户在此基础上，可以快速地推出其手机产品；第二，因为中国黑手机（不经电检通过即营销售的手机）市场的存在，它帮助MTK解决方案迈出最初的销售步伐，并给予其技术验证机会。随后，MTK方案才得以登堂入室，成为中国内地大多数正牌手机企业的主流平台。

MTK是从CD与DVD芯片起家的，从一开始，MTK的经营理念和企业文化就是这两点。（1）服务：直接把钥匙交给你，你只要转动钥匙就可以把车开走了，客户夫复何求？（2）低成本：别人的方案要两、三颗主IC，用我的方案只要一颗就搞定了，客户产品的成本可降低，结构设计更有弹性。

当时无论是欧美企业做手机IC，还是日本企业做光存储业务，这些IC设计大厂只单纯卖IC，其它所有东西都必须由下游企业自己做，因此，下游企业本身需要维持一个很大的研发团队，开发成本高，且开发周期较长。

MTK蔡明介董事长曾说过：‘IC设计公司最大的使命是把产品做出来，而不是以拥有什么特别的技术为荣，只有顺利地把技术商品化，做出可以被市场接受的产品，并且卖到市场，整个产品开发的程序才算完成。’因此，如何不惜代价将产品卖出去，是MTK领导人产品管理能力的重要考虑指标。为了抢食技术已成熟、即将放量的市场，由IC原厂提供Turnkey是必然的方向。

业内极端的形容应用MTK的方案只需要3个人：一个接洽MTK，一个找代工厂，另一个负责销售与收款。当然这是比较夸张的说法，但这表示MTK提供方案的稳定度与功能的完整度，都足以媲美像我们这种系统厂专为某客户项目所设计的方案。”

菜鸟：“我觉得更厉害的是，试想有多少大小厂商已使用过MTK的方案？IC的稳定度先不用说，为了满足每个客户想要独特化自家产品的需求，MTK所提供之SDK必然具有极严谨的架构，使不同的客户易于执行客制化，并可轻易在同一系列IC间无缝切换。”

PM：“这是一个很特殊的现象，原本IC设计被认为是硬件产业，但现在发现只有IC是远远不够的，完整的软件系统方案才是最重要的致胜因子，但目前IC设计公司的领导人一方面垂涎于MTK的成功模式，但‘硬’梆梆的脑袋一时还软不下来，所以并未在建立正确的软件开发流程上下太多工夫（或者在不明所以的状况下，花了大钱引进不甚可行的软件工程工具），导致许多IC设计公司里的软件人，仍都是用睡眠时间和肝指数在拼业绩！

我们讲了不少项目管理与软件工程的概念，一再证明只要思想正确，可以采用便宜或免费的工具，通过流程将这些思想导入工程师的习惯与大脑里，并且不会增加太多的effort。有时候真的隔行如隔山，尤其当很多软件人都说不清如何有效率地进行软件系统开发时，如何让掌权的硬件人提升软件团队的战力，提升公司的竞争价值？”

菜鸟：“（心中OS：听说PM老大在前一个工作受了点气，看来牢骚不少，转移个话题好了。）既然卖这种SoCIC就是卖一个电子产品或Total Solution，那么，要如何知道哪种规格的产品才是客户想要的？开一颗IC应该要不少钱吧？”

PM：“对产品制造商而言，主控IC只是板子上的零件之一，重点是系统啊！一切的根源应该是来自‘市场研究’，进而决定‘应用（Application)’，再检验查看自己公司具备的能力，最后才是IC与系统规格的制定。在许多IC设计公司中，根本把决策的顺序搞错了，我认为这种贩卖SoCIC的公司，不应该只是间IC设计公司，本质上也该是间消费性电子产品开发的公司，所以我们之前谈电子产品开发的点点滴滴都适用于SoC设计公司。如果主事者还是只会用‘传统’IC设计公司的思维思考，难免与真正的市场需求渐行渐远。

举例来说，图6-27所示就是我认为‘错误流程’的例子[20]。

许多公司都忽略了市场研究分析（Marketing）的重要程度，他们应该负责定义产品规格，研发人员只要根据此规格做出来即可，而产品在市场的接受程度则必须是Marketing部门的绩效标准。在某些公司，Marketing负责收集并分析市场信息，却不必为最终产品规格的成败负责，如此一来，Marketing自然乐得轻松，大可把产品规格需求讲得模模糊糊、莫测高深，最后产品大卖是因为市场定位正确；若不幸产品接受度差，则是因为RD无能，未能实时推出产品，错失成功的契机。

▲ 图6-27　IC设计公司的产品开发流程一（错误范例）

若公司的Marketing部门不负责地定义‘产品规格’，那么就顺理成章由IC设计团队来定义‘IC的规格’，期间当然偶尔会咨询系统应用团队的意见，但通常只是善尽告知的义务，反正外面的电子产品开发团队不就是这样吗？IC里有什么就用什么，谁会让你参与IC规格的定义？唯一不同的是，IC设计公司里的系统应用团队并没有选（换）IC的权利！

于是，一颗‘不知民间疾苦’或‘不知今夕何夕’的IC诞生了，系统应用团队只能根据以下3点自行定义产品规格（由负责开发产品的团队定义产品规格，基本上就犯了类似球员兼裁判的大忌）。

■　IC的规格

■　Marketing与老板的意见

■　客户的需求

接着由产品规格，定义软硬件规格（硬件板子+软件SDK）并进行开发，此外，系统应用团队还有个重要任务，就是必须负责把IC的bug隐藏起来（一般称为Workaround），并设法挤出IC的所有能力。

如果这个Solution侥幸卖出去（再强调一次，这里说的是卖Solution，而非卖IC），而且居然卖得还不错，此时，SDK会根据不同客户的新需求而不断增修，并逐渐累积此颗IC在应用上的限制或注意事项。当IC出货量到达一定程度时，IC设计团队就会开始计划做此IC的cost down版，同时把已知的IC bug尽量收掉。通常此时会被想到的就是：拿掉IC中没有用的IP、重新检讨IC内部layout（以期再减小IC的size)、缩短IC测试的时间[21]、使用更精密的工艺种种，考虑了所有的硬件与制造手段，但居然就‘忘了’要咨询系统应用团队的意见！

例如：是否因为IC的局限性（Limitation）造成客户的某些功能无法实现？某些SDK的新增功能是否可以改用硬件加速？毕竟系统应用团队才是在第一线接触客户的单位，肯定最熟悉客户的状况与需求，一般客户根本不管IC里面藏着什么高科技，他们只在乎产品造价（System BOM）是否具竞争力，以及SDK功能是否足够且稳定。也就是说，新IC卖出去后，除了设法精简IC生产成本之外，更应该积极地思考下一代‘产品’。

再强调一下我的中心思想。IC设计公司卖的东西是‘电子产品’或‘完整解决方案’，只是用IC交货罢了！接下来，再来看看我认为较正确的流程（如图6-28所示），与前一个流程不同处如下。”

■　Marketing负责分析市场与客户需求，进而定义Application，确定产品规格并为此负责。

■　以Marketing主导的Application SPEC.（或product SPEC.)，做为此SoC设计项目的基准（Baseline)。

■　IC与SA团队根据Application SPEC.，分别定义IC与软硬件系统规格，并交互Review。

■　下一代IC的规格，来自新的市场信息，以及IC与SA团队的反馈。

▲ 图6-28　IC设计公司的产品开发流程二（正确范例）

菜鸟：“其实道理真的很简单，客户与其关心IC的规格，不如多花心思了解SDK能提供的功能，对其产品定义与设计更有帮助。IC cost down当然很重要，但SDK的架构、API的完整性、系统的可扩展性、内置的应用程序是否具代表性等相对来说更为重要，而这些都是软件工作。对企业来说，在重要的工作上投注关心、改善流程、引进专业人才等，绝对是正确的方向。”

06-08　系统架构设计实例分析

菜鸟：“您花了好多时间在说明系统架构的重要性，大部分我都能理解，但心里还是有点不踏实。因为实际上每个项目的特性与要求都不一样，我没有把握当自己碰到新的应用或硬件配置时，也能设计出正确的系统架构！”

PM：“我们做产品的并没有所谓‘正确的’或‘最好的’系统架构。业界并没有评估系统架构优劣的标准，而且执行电子产品开发追求的应该是一种‘足够好’的水平，所以实作上系统架构的设计总是循序渐进的，资深如我也没把握一次就把系统架构考虑周全，毕竟CPU的运算能力、存储器的size、应用的复杂程度、可投入项目的资源、公司之前是否有相关技术累积等，都可能影响到系统架构。

好吧！今天我花点时间讲讲我们之前做过的两个项目，这两个项目相当具有代表性，在设计系统架构时都经历了一番讨论与取舍。”

6-8-1　案例研究（I）：8 bit MCU+NAND的系统架构

PM：“首先来说一个Computing Power与Memory Size严重受限的例子，其硬件架构如下。

■　CPU： 8 bit MCU（如8051兼容），寻址空间最大为64 KB。

■　DSP：因为CPU计算能力实在太弱，所以需要一颗DSP负责影音数据的译码。

■　Memory： 40 KB（程序执行与runtime数据所需的空间都在这40 KB内，甚至比CPU地址空间的极限64 KB还要小）。

■　NAND Flash： GB等级。

■　Boot ROM inside CPU：Boot-ROM会自NAND Flash的特定位置载入一小段程序到SRAM中执行。

因为Memory Size实在太小，最后真的是寸土必争，而且都是以Byte为单位，再加上每新增一组的API就会多出一些函数，势必要付出code size增加的代价。所有工程师都认为在这个状况下还要去做模块化是不现实的，所以当时的PM就决定以土法炼钢的方式硬干下去了。后来在同事们的努力下，这个项目确实也拼出来了。

但真正的困难出现在接续的项目上，没考虑模块化的code本来就较难以maintain，而且几乎每项功能都曾专门为code size的问题做过调整，导致新项目想要重复使用前项目的研发成果并不容易。经过几个同性质的项目做下来，面对新功能导致的code size问题只能挖东墙补西墙，最后整个系统简直是到处贴满了狗皮膏药，新进工程师的学习曲线相当陡峭，这种项目变成PM间的烫手山芋。

当我还是菜鸟PM时，这个烫手山芋丢到了我的手上。我研究了一阵子，决定断尾求生，开始组织人员重新进行系统架构设计。如果是由你负责这个项目，你想怎么设计系统架构？试着在纸上把方块图画出来。”

菜鸟：“以前听您说过这个项目，似乎曾把大家搞得灰头土脸，幸好我们现在的项目用的是32 bit CPU，存储器是MB等级的，不用跟一个个Byte奋战。

以硬件配置来看，我认为首先要解决的问题是程序的footprint（包含code与data size）远大于SRAM的size（40 KB)。既然NAND Flash的空间那么大，我们可以把所有的产品功能切分为一个个独立的程序，并存储在NAND Flash内，而这些程序的footprint必须控制在40 KB内，需要时可自NAND Flash载入SRAM执行。通过不断的切换，应该就可以完成最终产品所需的功能。我的系统架构如图6-29所示，并将其称为Program-Switch Arch。

首先，SRAM中必须有一块永远不会被置换出去的程序，我称之为Basic System，负责做Program之间的切换，而当我们把所有程序烧入NAND Flash之后，必须将这些program的size与在NAND Flash内的位置记录在一张表格（称为Program Index Table)，并写入NAND Flash中，则basic system在runtime时才知道去哪里载入program。

如何，这个架构应该可行吧？ ”

PM：“不错！这个架构不但可行，还实际用在不少项目呢！”

菜鸟：“难道您刚刚说造成最后很难maintain的架构也是长这样？ ”

PM：“是的！当初的分析结果也是认为code size是最大的问题，最直觉的做法的确就是如你所设计的架构。用这个架构来完成第一个项目确实也没什么大问题，但后续项目要沿用此系统就会碰到一个个麻烦。

举例来说，当碰到code size问题时，只要把当前 program切成两个program即可。而这两个program可能都需要用到同一个模块，但可用memory size实在太少，无法在每个program中link完整的模块，即模块提供的功能多少必须依使用者（Caller)而异。

我们在做系统时总是秉持着一个基本原则：底层的系统必须尽量可以支持各种不同应用，所以系统的性质是比较一般化的，它不该与使用者有太深的关系，否则，一旦换了使用者，系统的运行就可能会出问题。以上做法导致系统与应用程序的耦合度太高，破坏了模块化的基本精神，使得之后的maintain与扩展都会非常麻烦！”

▲ 图6-29　Limited-Memory系统架构设计：使用Program-Switch机制突破存储器的限制

菜鸟：“有些没有听懂，可以举个例子吗？ ”

PM：“我们有个功能是MP3播放，播放时有两个主要的功能，一个是显示歌词或频谱；另一个则是显示文件列表，让使用者可以浏览文件或换曲。显然40KB做这些功能是远远不足的，所以我们使用两个program来实现这个应用。会用到的系统模块有：（1)Drivers；（2)Play Engine（负责播放MP3)；（3)File System（负责文件的存取）；（4)UI Common（提供简单的视窗系统功能）。

如图6-30所示，两个program都无法把这些完整的模块塞入40KB中。因为第一个program 播MP3时还要做歌词同步，但同一时间只需要access一个文件，所以它需要较多的Play Engine，与较少的File System；第二个program恰则恰相反，它需要较少的Play Engine，与较多的File System。

为了满足这样的需求，于是所有系统模块的程序就会变成像以下的程序一样，如图6-31所示，就一个字——乱！原本应该功能单一的模块，为了满足不同program的code size需求，里面加了一堆#ifdef……#else..#endif。这还是一个项目的状况，假使用同一个系统run了多个项目，你能想象系统程序会乱成什么样子吗？

▲ 图6-30　Limited-Memory系统架构设计：根据应用调整Program的Module种类与大小

程序毫无可读性可言，且难以维护，但这还不是最糟糕的状况！”

菜鸟：“这个程序真的很难一眼看出某个程序段到底会不会被执行，对不熟的工程师来说，确实很容易因此犯错，但您说这还不是最糟的？”

▲ 图6-31　Limited-Memory系统架构设计：程序中的乱象

PM：“我还是习惯从管理的角度看事情。建立严谨系统架构的目的之一，就是可以依此进行工作分组，并层层负责。但以上这种系统架构，负责系统开发的工程师并无法满足每个program的需求，最后总会逼着做应用程序的工程师非得自己下来调整系统模块的程序不可。这种状况破坏了分层负责的优势，对我来说，简直是模块化的大忌！

系统模块必须能够服务所有的应用程序，而应用程序工程师只会知道自己应用的需求。这本来应该是一对多的关系，一旦系统模块中加入越来越多某应用程序专用的code，该系统模块如同有多个版本，则变成多对多的复杂关系。

这种架构最终导致工作分工是根据program分组，每个program的工程师必须负责从driver到application的调整工作。在programA改好了某个系统的bug，很可能在其他program却仍然存在。这种混乱的状况，让项目主管根本无法掌握真正的质量水平。

研发团队面对新领域的产品开发时，许多系统架构的问题都是始料未及，必须等到实际做下去、到了项目中后期才知道。碰到这种状况，大部分团队会选择见招拆招，继续凹下去，通常也多能凹到结项，但最后完成的系统并不能成为组织的研发资产。若在项目设计阶段，因为经验局限，无法设计出正确的系统架构。在执行阶段，碍于进度与预算已无法打破重来，但至少结项时要有检讨，当下一个同质性项目开始时要下定决心进行系统架构的改善，绝对不能自我催眠，以为用既有的系统可以缩短开发时间与人力，通常结果却是恰恰相反！”

菜鸟：“既然我想的系统架构有这么多缺点，那你们后来使用的改善架构长成什么样子？ ”

PM：“当初我接这个项目时，因为系统架构与未执行模块化的问题都已爆发，眼前最大的问题变成：如何在这种存储器极端限制的平台上，设计可自然导入软件模块化的系统架构？两个重点。

■　设法通过系统架构，‘硬把’系统模块与应用程序分开。

■　必须解决SRAM Size Limitation的问题。

显然动态载入（NAND→SRAM）仍是突破存储器限制的唯一方法，但我们做了更多的考虑。当初基本的想法如图6-32所示。”

▲ 图6-32　Limited-Memory系统架构设计：改善方案

PM：“改善方案的基本思想非常简单。在以前的方案中，可切换的基本单位是program，而每个program都是一个完整的程序，其中包含driver、系统、middleware与应用程序，同一时间SRAM中只有一个program，因此，一个program会尽可能用掉所有的SRAM空间。

在新架构中，基本的执行单位仍是program，但同一时间SRAM中最多会有3个program，分别是AP program、System program与Driver program，各个program之间只能通过API沟通。为此，我们必须开发program间沟通的机制（Inter-Program-Call;IPC)[22]。这个架构很清楚地告知所有开发人员：这是一个分层架构，而我们的开发团队也会依此分组。组织自然可以回到我一直要求的基本运行之道——‘永远把上层当作客户’。通过不断沟通，底层必须满足上层的需求，底层、上层开发人员各自处理自己的技术问题，当然包含code size调整。

图6-33的左半部所示就是我们的系统架构图，每个方块都必须设计出明确的interface，是不是和一般嵌入式系统架构差不多？这些模块会被link到不同的program，图6-33的右半部所示则是执行时期每一种program里所包含的模块，以及program之间的关系。”

▲ 图6-33　Limited-Memory系统架构设计：Multiple-Program系统架构

菜鸟：“这个架构确实较容易实现模块化，但各个program还是可能碰到存储器空间不够的问题。在原先的架构，负责整合的program工程师还可以设法挖东墙补西墙，但在新的架构中，各个program的可用空间显然更小了，新架构执行起来不会有问题吗？”

PM：“没错！即便实现了模块化，code size还是个大问题。我们总不能因为要做模块化而导致项目做不下去。就像我们为了让program间可以沟通，设计了IPC。为了解决code size的问题，我们设计了比program更小的执行单位——bank，如图6-34所示。

简单地说，我们还是利用NAND Falsh当buffer。在设计与开发阶段，工程师可以将不会同时执行的功能做成可切换的bank，则在执行时期，program便可以做bank的切换，用较小的SRAM空间执行实际较大的程序，从而解决了code size的问题。

若模块间的关系未经仔细设计，这种机制便可能造成执行时期频繁的bank-switch，并严重影响执行性能。所以这个新增功能无形中会通过系统架构，push工程师对各个program内部进行更细部的模块化设计。

举例来说，play-engine这个program可以播放多种音乐格式，但无论如何，不可能发生同时播放MP3与WMA的状况，所以负责play-engine的工程师在设计时，就可以把播放MP3与播放WMA的程序做成bank，视执行时期的需求载入对应的播放模块（bank)。”

▲ 图6-34　Limited-Memory系统架构设计：从系统架构实现模块化

PM：“当然这个架构并非完美，甚至比旧架构需要付出更多的effort去实现。但我们很清楚模块化与稳健的系统架构，可以为团队带来哪些利益。技术细节在此就不继续深入讨论了，这样讨论下来，你对系统架构的重要性应该有更深刻的体会吧！”

菜鸟：“从这个案例看下来，我有些心得。第一，毕竟每个项目都有其独特性，所以系统架构的设计很难一次就到位，势必要经过不断改善，必要时还要增加新的系统机制以突破硬件或应用上的限制。第二，系统架构并没有绝对的好坏与对错，但必须同时考虑技术面与管理面。像我们这样不断接项目的公司，技术资产累积真的很重要，如果每件新项目都得从头做起，就算工程人员再厉害，公司与团队的发展还是有限！”

6-8-2　案例研究（II）：RTOS/Embedded Linux通用的系统架构

菜鸟：“可不可以再提一个比较高端的例子，毕竟我们现在用的是32 bit的CPU，而且似乎也没可怜到需要为了几个Byte斤斤计较。”

PM：“OK！我来讲一个跟Embedded Linux有关的项目。这个内部的项目我之前应该有跟你提过[23]，希望将原本在RTOS上已开发好的应用移植到Embedded Linux上。在讨论之前，这种项目有几个重要的背景需要先让你知道。

■　Embedded Linux会用在比较高端的产品线，但采用Embedded Linux的产品仍必须具有原RTOS系统上的所有应用（例如：可上网的多媒体播放器，虽然新增网络应用，但绝对不能忽略了多媒体播放功能）。

■　使用RTOS的低端产品仍有市场需求，即应用程序仍可能继续发展与改善，而所有应用程序的改变都必须能反应到使用Embedded Linux的高端产品线。简单地说，我们希望使用RTOS与Embedded Linux的系统，可以共享同一套应用程序的source code。

■　高端产品（采用Embedded Linux）新增功能，不要影响到既有RTOS上的应用程序。

■　无论项目采用RTOS或Embedded Linux，系统必须能够执行在同样的硬件架构之上；但RTOS与Linux驱动程序的写法是截然不同的。

因为要考虑的事情真的很多，大家往往很容易一不小心就钻入技术细节的黑洞。而这也就是设计系统架构的大忌——骨架没出来，任何对骨、肉的讨论都没有意义！而且我们稍早也说过，系统架构设计本来就是一种循序渐进的过程。

当我参与这个项目的讨论之后，实在受不了这种冗长又无法产生结论的会议，虽然不是由我主导这个项目，但还是画了几张架构图。虽然不会是最后的方案，但总算把大家拉回系统架构设计的正轨上。

一旦我们认真检验查看这个项目真正的目的，你会发现一开始就把重心放在Embedded Linux是不对的。”

菜鸟：“没错！工程师个性就是这样，总是倾向于花时间与注意力在自己没做过的事情上。Porting Linux到既有硬件平台上只是这个项目的主要工作之一，这个项目主要的目的其实是要找出一个可以让Embedded Linux能共享RTOS上既有应用程序的方式罢了。”

PM：“没错！很多事情都是当局者迷，旁观者清。既然目标是共享RTOS上的应用程序，我们当然得先来看看既有的系统架构，这是个很严谨的分层架构。”

PM：“这是一个典型的、基于RTOS的Multi-Tasking System，让我们来检验查看一下，这个系统中哪些模块是可以与Linux共享的？”

菜鸟：“Linux本身就是一个完整的系统，有自己的驱动程序、存储器管理、标准C函数支持等。此外，还有许多专为嵌入式系统所设计的GUI系统可选择，如QT/Embedded、Micro-Win等。

从图6-35所示看来，我认为OS-API以下属于系统部分，RTOS与Embedded Linux无法共享这些系统模块。而设计OS-API的目的，就是希望OS-API以上的程序模块是OS-independent，亦即上层程序只能通过调用OS-API来使用系统与硬件功能，所以理论上只要在Embedded Linux上包装出完全相同（接口相同+行为相同）的OS-API，并使用Embedded Linux上的thread模拟原先RTOS上的task，则原先RTOS上的middleware与application应该就能在Linux上跑了吧！”

▲ 图6-35　既有产品线（使用RTOS当核心的系统架构）

PM：“不错！分析得很好，越来越有sense了。我们画出的系统架构图如图6-36所示。”

▲ 图6-36　RTOS与Linux共享Middleware与Application的系统架构

PM：“图6-36所示以OS-API作为区隔，往上是与OS无关的部分，也就是我们希望RTOS与Embedded Linux能共享的模块。往下则分为两部分，左边是RTOS，右边是Embedded Linux，后者为了要包装出既定的OS-API，势必要额外补上一些功能。

图6-36中值得一提的是HAL（Hardware Abstraction Layer；硬件抽象层[24])。驱动程序的稳定度是整个产品质量的根基，所以我们希望两个系统能最大限度地共享驱动程序。为此，我们定义了硬件抽象层API。也就是说，无论硬件配置是否相同，驱动程序都必须包装出通同的接口（HAL)，系统（RTOS或Embedded Linux）则将HAL当成是抽象的硬件，再根据该系统的规范实作驱动程序。

因此，即便RTOS与Embedded Linux上对驱动程序的写法有不同的规范，在我们的系统架构中，真正‘驱动’硬件的程序则只有一套，这可以为日后的测试与质量工作避开很多麻烦。

言归正传。当我们有了两个系统共存的架构图之后，可以依此画出Embedded Linux执行原RTOS上application的架构图，其实就是把图6-35中RTOS的部分去掉，再加上Embedded Linux上为了要包装出OS-API所需额外补上的模块，如图6-37所示。”

PM：“图6-37所示应该很容易理解，在此我就不解释了。看图说故事是最好的沟通方式，到目前这3张图应该可以把故事说清楚了。当然不是系统架构图出来之后就天下太平了，这只是初稿而已，但至少所有人有了可以讨论的平台，而且在接下来的讨论中，我们还会再扩展或改进系统架构图，让所有人都可以从这些图了解，我们到底要把这个项目做成什么样子？

举例来说，老板看了以上3张图后似懂非懂地问道：‘这些图说明了两个系统如何共享application，那如果Embedded Linux想要扩展功能，如何保证不会影响既有的application? ’，我们会发现少了一张图说明Embedded Linux上既有application与扩展功能如何共存。当碰到这种情况时，立即补上就是了，如图6-38所示。我们不是一再强调系统架构设计是一种循序渐进的过程吗？没有人有把握一次就将新项目的系统架构设计的完整无误。”

▲ 图6-37　以Linux当核心，执行原RTOS系统上的所有功能

▲ 图6-38　以Linux当核心，同时执行原RTOS系统上的既有功能，以及其他扩展功能

[1]　Boot-Loader就是我们在《第5章：实作你的第一个嵌入式系统》所提到的启动程序，实际上的启动程序更为复杂，而且完全与硬件相关。简单地说，Boot-Loader就是在系统开始运行之前的一段小程序；通过这段小程序，我们可以初始化硬设备，从而将系统的软硬件带到一个合适的状态，以便为随后的系统执行准备好正确的环境。Boot-Loader是嵌入式系统开发相当重要的部分，我们将在《第8章：上电之后—Boot-Loader》详细说明。

[2]　职场上的mail往返，往往不是为了沟通，而是为了CC给相关人等，明确且留下证明，好让老板知道自己又接了一项工作。这样的状况在跨部门或跨公司间合作更为明显，对项目最大的影响就是浪费时间。

[3]　这个流程称为“Booch面向对象开发程序”，不管是否使用面向对象的观念来进行系统架构设计，基本思想都是一样的。

[4]　CASE：计算机辅助软件工程，市面上的CASE软件通常都是天价，一般软件研发团队实在是用不起。

[5]　这些功能不是本书的重点，在操作系统的相关教科书中都有提到这些功能，请可自行参考相关资料。

[6]　Callback Function是在系统软件中广泛使用的小技巧，嵌入式系统工程师请务必学会，请参阅《附录B：Callback Function》，通过其中的实例，应该很容易了解何谓Callback Function。

[7]　因为中断可能会发生在任何的时刻，当时CPU可能正在执行任何的程序，所以ISR会有很多API不能使用，否则会有重复进入（Reentry）的麻烦。详细情形可参阅《第9-6节：ISR写作注意事项》。

[8]　在嵌入式系统中，存储器空间是必须严格控制的，所以产品用不到的程序就不该被连接到最终产品的程序中。

[9]　实际上是诸如名为config.in的设定文件，这个设定文件会被make程序使用；你可以不理会Linux的细节，就想象其配置设定工具会产生一个全局的header file即可。

[10]　又称宏处理器，C compiler在开始编译之前会自动令其执行，它的功能有：表头文件的含括、宏的扩展、条件式编译等。

[11]　有关操作系统的定义节录自中文维基百科。

[12]　同样状况也会发生在Windows或Linux上Multiple-Threading Programming。

[13]　有时候我们称Emulator为模拟器，称Simulator为仿真器。

[14]　软件开发的版本控制相当重要，所以团队中一定会架设一个版本管理的Server，有权限的团队成员都可以登入这个Server，取得最新版本或某个较旧版本的程序，并且可以将修改过后的某个程序，提交（称为Check in或Commit）到Server，这是多人协同开发必备的工具。

[15]　Source Tree的结构没有一定的标准，甚至可说是很主观的。本节的例子仅供你参考，实际上，应该要根据产品特性与系统架构自行制定之。

[16]　全局变量又分为有初值和没有初值的两种，存放的空间是在指定的存储器区域，其地址在linking阶段即已决定。而局部变量是存放在stack中，所以每次某function被执行时，其局部变量的地址可能会不同。而且如果局部变量占据的空间太大，很可能会造成stack overflow，压到其他用途的存储器区域。在执行时期，这样的问题通常不会马上爆发，所以很容易误判bug真正的发生原因。嵌入式系统中有关存储器的注意事项，请参考《第8章：上电之后：Boot Loader》。

[17]　这个范例的重点在程序编写风格，你不必看懂其内容，只要注意程序中粗体的部分即可。

[18]　静态测试并非只是程序语法的rule check而已，我们在《第18-1节：测试》，还将更详细的说明。

[19]　研发团队自有其专长的领域，被指派的项目应不致离该领域太远，所以即便前后项目采用的硬件不同，系统架构与许多模块应该都可以重复使用。

[20]　对、错没有绝对，也许有些前辈认为这个流程在某些公司run得很OK，笔者深表祝福……

[21]　每颗IC在出厂前都必须经过几道关卡的测试（CP→FT→系统测试），以刷掉功能不正常的IC，因此，测试时间是IC制造的重要成本之一。IC设计公司中会有专业的技术人员，在不影响最终IC的良率的前提下，不断地尝试减少IC的测试步骤与时间。

[22]　一般我们说的IPC是Inter-Process-Communication。

[23]　请先回头复习《第6-4节：嵌入式操作系统——OS在哪里？》。

[24]　《第10章：设计硬件抽象层》会专门讨论HAL，在此你可先将其视为驱动程序的标准API。

Chapter 7

构建良好的嵌入式系统开发环境

时间：07月01日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

PM：“我们系统第一版的开发环境已经构建好了，麻烦你到Server上把开发环境及其文件与目前版本的程序下载到你的计算机，试试看能不能做出可执行文件，然后下载到机器上看能否执行。下午系统组的主管，他在这个项目负责系统整合（以下称其为：系统老鸟），要向所有工程师报告如何使用这个开发环境以及相关注意事项。在此之前，我们应该先使用一下，看看有没有需要改善的地方。”

菜鸟：“前几天已经把整个开发环境下载来了，设计文件中有一份‘开发环境使用者手册’，我按照里面的步骤试了一下，但是当时版本的程序好像有点问题，有些程序文件会编译不过，当时我向系统组的主管报告了这个状况，后来我就没有再试了。我想现在可能改好了，等下我就会再去试试看。

可是我还是有些事情不了解，我知道开发环境的最终目的就是要产生可执行文件，但里面除了厂商提供的编译器等工具外，似乎还包含许多我们自己写的，像程序又不是程序的文件（makefile & Link Script)，而这些文件的内容似乎不是那么直觉，此外还有许多我们自己写的工具，我想知道所谓的开发环境到底包含什么？”

PM：“就如同我们构建系统平台时的思想一样，每个电子产品开发项目用的CPU或产品特性可能都不一样，所以编译工具、制作出可下载到机器的可执行文件的流程可能都大不相同，我们希望大部分的工程师只要专注在自己负责的开发工作，不要再花时间去适应各个不同的开发工具或可执行文件制作流程，我们会有专门的人员把这些繁杂的动作包装成简单的步骤。

具体来说，应用程序工程师只要在集成开发环境（IDE）上选择‘build project’，或在DOS视窗上执行build.bat这个批处理文件，如果程序无误的话，应该就能制作出可以下载到机器上运行的可执行文件，最好一切都是自动的。所有做出可执行文件的动作与细节都包装在build.bat内，这些流程由负责系统整合的人员统一管控，应用程序工程师其实不用理解太多，只要会用即可。”

菜鸟：“之前我们在Visual Studio上开发PC程序时，不论是一般可执行文件、DLL、ASP等，只要使用Visual Studio的功能就可以把软件项目管得好好的，从来也没想过还要去构建开发环境。我也在Linux上写过程序，以前要自己写makefile，但是目前已经有诸如glade、designer、kdeveloper等开发环境，写Linux的应用程序似乎也不是什么困难的事情，其实我还想知道嵌入式系统的开发环境是怎么做出来的、包含什么、有什么事要特别注意的？”

PM：“多一个人知道开发环境怎么架设也好，也许下个项目你也可以帮忙建立开发环境，我会请系统组组长花点时间向你讲解开发环境的各个环节。这里我先列出开发环境该有的项目。”

■　集成开发工具

■　Cross compiler

■　制作可执行文件的批处理文件（build.bat)

■　makefile

■　Link Script

■　调试工具

■　下载工具

■　其他工具（Offline Tools)

■　模拟器

■　版本控制工具

07-01　开发工具

时间：07月02日　 地点：办公室一隅　 登场人物：菜鸟、系统老鸟

系统老鸟：“经过刚刚的报告，你对开发环境的使用还有问题吗？”

菜鸟：“报告长官！我前几天就已经先用过了，基本上没什么问题，因为步骤真的相当简单。只要从Server上把所有的程序与工具下载到自己的计算机，确定路径与程序都没问题后，在DOS视窗下执行build.bat，它会自动去做很多事情，过一阵子就会自动把可执行文件产生出来了！最后再利用工具把可执行文件下载到机器就可以了。对写系统程序或应用程序的人员来说相当简单，完全不需理会细节。但是我对build. bat其中做的事情相当有兴趣，希望我以后也能够自己建立嵌入式系统的开发环境。”

系统老鸟：“PM老大有交代过，要我把其中的技术教你一下。首先我先问你，以你对嵌入式系统开发的了解，从大家把程序写好之后，到要生成可执行文件，其中必须包含哪些步骤？”

菜鸟：“之前PM老大曾让我试着在板子上，写了一个简单且独立的小程序，以下的动作在每次改完程序测试之前都要做一次。”

Step01：编译各个程序。

Step02：编辑Link Script，指定程序要被定位到哪个地址。

Step03：连接Step01产生的所有目标文件（Object File），产生可执行文件。

Step04：用ICE下载并调试。

Step05：如果程序无误，则将其烧到ROM[1]中。

系统老鸟：“这些是制作可执行文件最基本的步骤。但你要知道，软件项目并不是只有程序而已，往往还包含一些源文件，例如，图像文件、字型、音频文件等，更复杂的项目可能还需要一个文件系统，要如何把这所有的东西一一塞到ROM中就是一件麻烦的事情。此外项目内的程序文件可能相当多，不可避免地要写一个或多个makefile，总不能要工程师一个一个程序自己打指令去编译吧！再加上不同程序可能要在不同的地址执行，而且你要每位应用程序工程师都要会用ICE调试、用烧录器烧ROM简直有点为难人了，为了节省大部分研发人员时间，有许多大大小小的工作要做，虽然每项电子产品项目所需的开发环境都大不相同，但基本上，你刚列出来的项目还得再补上下列步骤。”

Step06： makefile写作。

Step07： Offline Tool——将不同的源文件做成可加入ROM的二进制文件（Binary File)。

Step08： ROM-Maker——将可执行文件以及其他源文件制作成可烧录到ROM的二进制文件。

Step09： Build Simulator。

Step10： Update Tool——是一个PC程序，可通过某种方式（如：USB、网络或RS232）与机器上的程序（称为update-program）连接，让工程师可以不用通过烧录器就可以更新机器上的程序。

菜鸟：“我觉得这是个很具挑战性的工作，因为一定会对整个软件项目的细节最为了解，所以通常都会在项目的核心之内。我能感觉大PM非常信任您，才会把这个工作交给您。”

系统老鸟：“还好啦！开发环境是嵌入式系统开发不可或缺的项目，如图7-1和图7-2所示；然而因为工作内容很杂，而且与个别产品相关，所以一般书籍都不会提到。我们就以目前这个开发项目为例，尽量把基本思想传达给你。”

▲ 图7-1　嵌入式系统开发环境示意图

▲ 图7-2　嵌入式系统开发环境架构图——范例

7-1-1　IDE（集成开发环境）

菜鸟：“难道嵌入式系统就没有像Visual Studio或Eclipse那样的集成开发环境（Integrated Developing Environment；IDE）吗？终究自己写makefile与Link Script还是蛮麻烦的。”

系统老鸟：“其实许多比较通用的CPU所搭配的开发环境还是有IDE可以用，例如，ARM、8051等。图7-3所示就是一个商用的8051集成开发环境的画面，功能相当齐全，有基本的软件项目管理、程序编辑区、对应的汇编语言、显示目前CPU寄存器与存储器的状态，以及debug功能等。工程师只要用这个环境就可以完成开发、调试、模拟，以及制作二元可执行文件等所有动作，在比较简单的开发项目，这种环境其实还蛮方便的。”

菜鸟：“既然这样，我们为何不使用CPU厂商建议的IDE就好了，为什么还要自己去弄一套开发环境呢？”

系统老鸟：“以我的经验来说，即便是功能齐备的商业级IDE，也很难完全符合每一个项目的应用需求，越复杂的项目更是如此，就以刚刚我们所谈到的开发环境该具备的功能来说，商业级IDE就很难集成我们自己开发ROM-Maker及Offline Tools。再以软件项目管理来说，IDE最后也是产生makefile和Link Script，并据此制作可执行文件，如果我们有比较特殊的应用，最终也是要手动去更改这些文件。与其如此，不如一开始就根据项目的需求自行编辑相关文件。

而且，刚刚看的图7-3所示是8051的例子。如果我们选用的不是那么通用的CPU，恐怕就没有功能那么强的IDE可以用了。假如使用这些CPU的厂商并不是那么多，根本不会有专业的开发环境厂商（如：Metrowerks的CodeWarrior Development Tools）为其开发IDE，所以通常都是由CPU厂商自行开发，自然功能会比较阳春、使用者界面比较不friendly（如图7-4所示）等。尤其是写程序时一定要用到的文本编辑器（editor))，对用惯UltraEdit、Source Insight或Visual Studio的工程师而言，上述IDE所提供的editor简直是难用的不得了。

▲ 图7-3　功能较完整的集成开发环境

▲ 图7-4　较不friendly的集成开发环境

实际上，通常我们会舍弃IDE不用，开发程序的工程师可以使用任何自己习惯的editor（如Sourcelnsight），然后使用项目团队自行设计的开发环境，只要一个指令就可以做完所有的事情，最后产生可直接下载到ROM的可执行文件。”

菜鸟：“难道CPU厂商提供的IDE真的不会用到吗？”

系统老鸟：“还不至于完全用不到，通常我会把它定义为远程调试（Remote Debugger[2]）工具，我们可以自己开发ROM-Maker及下载工具，但是Remote Debugger就如同Emulator一样，公司是不可能为了某件项目配置大量资源，并自行开发，而系统程序及应用程序的调试主要还是在Simulator之上，只有特殊情况才真的需要做远程调试（例如，Simulator上OK，但机器上NG的bug），所以除了驱动程序开发者之外，其他工程师要用到IDE的机会还真的不多。”

7-1-2　Cross-Tools

系统老鸟：“你认为在为新平台设计开发环境时，首先要考虑的是什么？”

菜鸟：“是makefile和Link Script吗？因为我觉得这两样东西最麻烦。”

系统老鸟：“你会觉得麻烦，一来是因为你不熟悉，二来因为makefile和Link Script的语法比较不直觉，而且许多人喜欢把它们写得高深莫测，其实它们的思想是很简单的。

话说回来，开始设计开发环境的时间点应该会在项目的早期，此时很多事情都还没确定，很难掌握需要哪些步骤才可以做出可执行文件，再加上不同CPU的平台通常会采用不同的开发工具，所以我认为在刚开始时最好花点时间去研究一下这些工具，看看到底提供了哪些功能，使用时有没有特殊的注意事项，这在项目后期调试或优化时都可能帮得上忙，我们最好心里有个底。等系统设计渐趋完成时，再来开始写makefile也不迟，在嵌入式系统开发中，我们把编译器等工具称为Cross-Tools，我不知道你是否清楚所谓‘cross’的思想？”

菜鸟：“这个之前固件组的学长曾经教过我，所谓的‘cross’的意义就是要在PC上产生其他机器的可执行文件。回想PC程序的产生流程为：

■　编译（Compile）

■　汇编（Assemble，有的编译器只会产生汇编语言代码，此时必须经过汇编才会产生目地码——Object File）

■　寻找标准库函数

■　连接所有需要的Object File与库函数

■　产生可执行文件（.EXE）

因此，Cross-Tools应该包含

■　Cross-Assembler

■　Cross-Compiler

■　Cross-Library

■　Cross-Linker”

系统老鸟：“基本上理解没错，但我们会用到的Cross-Tools应该还包含以下几项。

■　可执行文件格式转换工具：业界常用的可执行文件格式很多，例如，Windows上的EXE以及DLL，或业界中嵌入式系统开发环境行之有效的HEX与S-Record格式，以及最近常用的ELF[3]。

■　可执行文件dump工具：将可执行文件dump出（或‘转换为’）汇编语言代码与相关信息，这个工具在开发后期的性能分析与优化时相当有用。

■　Debug Library与工具：例如，知名的GNU gdb，只要系统中连接了Debug Library，就可以通过串行端口、并行端口、USB线或网络线，与PC端的debug程序连接，工程师就可以在PC上进行远程调试。

可使用的工具可能会随平台不同而有差别，所以在设计开发环境之前，负责的人员一定要详读User Manual，弄清楚这套工具的极限在哪里。”

菜鸟：“如果要让每一位应用程序工程师都搞懂这些事情，的确是令人头痛的一件事，所以还是让专人去处理会比较有效率。我还有一些问题，您刚刚提到要先注意Cross-Tools是否有提供某些特殊功能，我想知道所谓特殊的功能有哪些？”

系统老鸟：“主要的‘特殊’功能都在编译器上，其实也不是多特殊啦！我用已经被移植到多个平台的开发环境一GNU Tool-Chain中的C编译器为例，以下是一些编译时期的选项。

■-Werror：将所有警告信息变成错误信息，即一旦有警告信息产生，就不会产生object file。

■-S：编译时期顺便输出汇编语言代码。

■-E：只执行C preprocessor，不产生object file。

■-D：编译时期才定义宏常数，效果如同在程序中使用#defineXXX。

■-O、-02、-03：优化等级，等级越高，产生的程序越有效率，相对的可能会比较不稳定。

■-g：编译时加入debugging information，使GDB可以进行远程调试。GNU Tool-Chain中的Linker，则有以下参数可使用。

■-T：指定Link Script，其中会指定要连接哪些文件，并分别寻址到哪个地址。

■-Map：连接时产生map文件，map文件中包含了程序中所有symbol的地址信息，在调试时可以帮上很大的忙[4]。

此外，有些Linker还可以产生不同格式的可执行文件。举例来说，GNU tool chain可以支持许多不同的CPU，而且这套工具和公用库函数都是开放源码，使用者可以根据其需求设定其配置（Configuration）自行制作GNU Tools，可以从其工具的名称得知其基本属性，例如，arm-elf-gcc就是会以ELF格式产生ARM机器码的C编译器，而68K-coff-ld就是会以COFF格式产生68000机器码的linker。”

菜鸟：“既然每一个项目使用的cross compiler都可能不一样，我怎么知道用什么参数，产生什么格式的可执行文件才是对的？”

系统老鸟：“具体来说，你这个问题并没有标准答案，随项目需求而定。以一般状况为例，编译时期是不需要产生汇编语言代码的，只有在调试或程序加速时，才有检查程序汇编语言代码的需求。但有个大原则是，刚开始时只要按照使用手册或厂商sample code中的建议来设定参数，先让系统可以运行无误，以后再根据需求修改参数即可。”

7-1-3　Make

系统老鸟：“除了compiler、linker等工具外，开发环境中还有一个重要的东西——make，我知道一般工程师都很抗拒使用这个工具的，你以前有自己写过makefile吗？”

菜鸟：“我在Linux上写程序时曾写过makefile，不过都是拿别人写好的范例来改的，而且只会用很简单的技巧而已。此外，我也在Linux上看过一些很复杂的makefile，几乎完全看不懂它在写什么！”

系统老鸟：“有些Cross-Tools会提供图形化的makefile与Link Script的编辑工具（如图7-5所示），假使系统中的source tree与复位的需求不那么复杂，使用这些工具并无不可。实际上makefile与Link Scrip的功能颇为强大，而上述工具往往缺乏弹性，碰到一些比较复杂的状况只能束手无策。举例来说，为了加速，系统中有几个模块必须载入到CPU内部的RAM（Internal RAM）中执行，但Internal RAM的size很小，无法同时容纳这些模块，所以这些大小不一的模块必须寻址到同一个地址（即Internal RAM的起始地址），执行时期有需要再分别将其载入。这种状况在嵌入式系统开发中并不算复杂，但一般makefile与Link Script的自动化编辑工具（如图7-6所示）并无法处理这种状况。

我认为身为嵌入式系统的开发人员，必须正视不同的开发项目会有不同的开发环境、不同的硬件与存储器配置、不同的Source Tree、不同的库函数等状况，会碰到什么复杂的应用根本无法想象，而且不见得每次都有这种自动化工具可用，所以建议你还是多学习一下makefile与Link Script的基本思想和语法比较好，其实并不会太难。”

▲ 图7-5　较复杂的复位状况，自动化Link Script编辑工具通常无法处理

▲ 图7-6　有些Cross-Tools会包含可自动产生makefile与Link-Script的工具

系统老鸟：“makefile勉强可以算是一种程序语言，它的语法不像C语言那么直觉。其实什么语言都一样，有人可以把程序写得人人都看得懂，也有人可以把程序写得高深莫测，最后连自己都看不懂。对makefile我个人的看法是：它不像系统程序有性能、code size等考虑，如果可以的话，我们只要用到makefile最基本的技巧就行了。此外，makefile也可以写注解，注解可以消除语法的不直觉。你跟在PM老大身边也有一段时间了，应该知道他最讨厌那种不容易看得懂的程序。”

菜鸟：“嗯，我是看过他为了这种事情飙某个工程师，当初我是觉得哪有这么严重，后来他分析给我听，在他的管理立场，控制程序的可读性确实是很重要的事情。不过，我还有一点搞不懂，make不是UNIX的东西吗？嵌入式系统的Cross-Tools也会提供这个工具吗？”

系统老鸟：“你可能对make的理解有点偏差，第一，make这个工具基本上和哪一套开发工具无关，你可以用同一个make工具来构建不同平台的可执行文件，简单的说，make是用来进行自动编译的程序工具，你只要在makefile中详细叙述要用什么工具（例如，产生ARM机器码的cross-compiler)，对哪一个程序（. c）或文件（. obj……）做何种的处理（例如，产生不要优化的object file), make就会检查这些文件是否已‘过期’[5]，如果有较新的版本，make就会根据makefile中的规定去处理这个文件。

第二，make这个工具并非是UNIX的专利，PC上的开发环境也都会用到make，只是一般人在PC上都是使用集成开发环境来写程序，所以不知道可执行文件产生的细节。例如，微软开发环境的make工具叫nmake. exe，而Borland C++ builder的make工具就叫做make. exe。”

菜鸟：“你的意思是说，make工具除了可以根据makefile的内容去执行compiler、linker等程序外，最主要的功能是可以判断某个程序是否有新版本，而且只会处理这些有新版本的程序，它到底是怎么办到的？”

07-02　makefile &批处理文件（.BAT）

系统老鸟：“我先问你一个问题，Windows的批处理文件（. bat）或UNIX环境中的shell script file都可以达到批量执行命令的需求，你觉得它们和makefile有什么不一样？”

菜鸟：“批处理文件和DOS活跃的时代我没赶上，但是Linux下的shell script倒是写了不少，我想他们是差不多的东西，只是shell script的语法和功能似乎比批处理文件复杂且强大一点，基本上它们也是一种程序语言，只是它们大部分的功能都是通过执行其他可执行文件来达成。以开发环境为例，假设我们有两个文件要编译并连接，我可以通过Windows的批处理文件（sample. bat）来做。”

程序代码7-1：批处理文件范例——sample.bat

菜鸟：“以后我只要在Windows上开个DOS视窗（命令列）执行sample. bat，就会依序执行批处理文件中的三个命令，一切无误的话，就可以把可执行文件做出来。”

系统老鸟：“假设你的系统总共有两百个程序文件，如果使用批处理文件来做，岂不是每次都要从头到尾把所有程序文件都编译一次，才能产生可执行文件，即便我只修改了一个文件？”

菜鸟：“这样的确很不方便，以前我还搞不太懂makefile和批处理文件的分别，它们都可以批次执行命令，现在我已经隐约知道它们的分别：make是专门用于管理庞大软件项目的工具，可以让制作项目‘最终结果’（可能是可执行文件或其他数据、文件）的所有作业自动化，降低错误编译的可能性。”

系统老鸟：“没错！make就是专用于软件项目管理的工具，除了语法以及许多功能细节不同外，makefile和批处理文件最大的不同，就是make会比较文件之间的依存关系与日期，以决定某个文件是否为新版。举例来说，make可以比较程序文件（test.c）和目标文件（test. obj）的日期，如果test.c的日期比test.obj的日期新，那就表示有较新版本的程序。当有新版本的程序，则这次执行make时，test.c就应该被重新编译。

再进一步说下去，重新编译后产生新的test.obj，此时，make又可以比较此新编译出来的test.obj与可执行文件（test. exe）的日期，发现test. obj比test. exe新，即test. obj是新版，所以此次执行make就应该要重新连接，以产生新的可执行文件。假使一开始test.c就没修改过，也就是说，test.obj比test.c的日期新，则上述动作都不会被执行。

Make就是根据这种简单的逻辑在运作，虽然还有些其他的小功能，但我们只要知道这个逻辑，几乎就可以做到需要的功能。

有些比较复杂的软件项目，程序数量可能多达数百个，如果每次都要把所有程序文件编译一次，那么，每次编译时工程师只好喝杯咖啡、散散步再回来看结果，万一编译到后期才发现错误，那真的会把人气死。较复杂的软件开发必定会采用团队合作的方式，make能够在众多的程序文件以及项目相关的数据中，持续追踪各个组成组件间的相依关系，所以在构建开发环境时一定是以makefile为主，而非批处理文件！”

笔者在此推荐两本书给对make较高深技巧有兴趣的读者，都有中文版。

■　《Programming with GNU Software/GNU程序设计》O'REILLY出版。

■　《Managing Project with make/make项目开发工具》O'REILLY出版。

7-2-1　makefile

菜鸟：“道理我大概懂了，如果要把上述的批处理文件改成makefile形式，应该如何做呢？”

系统老鸟：“即便Windows上可以用的make工具很多，如：Visual Studio（nmake.exe)、C++ builder（make. exe）或GNU tool-chain的Windows版本一Cygwin（也叫make. exe)，只要不用到比较艰涩的语法，makefile的写法都差不多。在看makefile的实例之前，我先说明makefile里两个重要的基本名词。

■　Target（目标）——顾名思义，目标就是你希望make帮你完成的事情，make会到makefile中寻找这个目标的定义。通常目标就是工程师想要产生的文件名称；例如，‘目标test.obj’必须通过处理test.c而来，‘目标test. exe’，则必须通过处理‘test. obj’而来。要注意的是，目标可以是，也可以不是文件名称，稍后我们会举一个常见的例子来加以说明。

■　Dependency（依存关系）——makefile中可以定义两个目标之间是否有依存关系。还是用上面的例子来说明：我们必须定义test. c与test. obj有依存关系，所以当make发现test. c变动了，它才会去执行目标test. obj。同理，test. obj必然也与test. exe有依存关系。此外有人把这种依存关系的定义简化为

■　Prerequisite（必备文件，如test. c)：一些能建立target（如test. obj）的程序文件，一个target通常由数个文件建立。

■　新版（Up To Date）的定义——假使某个文件（如test. c）比它所依存的文件（如test.obj）还要新，则表示这个文件有了新版本。同样地，因为要有test.c才可能产生test.obj，如果test.c的时间比test.obj还要新，表示test.c被编辑过，即test.c在上次编译后有新的版本。

这些定义可能有点绕口，但你只要想想test. c、test. obj与test. exe的关系，应该就可以明白了。一旦没有问题的话，让我们来把你刚刚的批处理文件改写为makefile，如程序代码7-2所示。”

程序代码7-2： makefile范例——sample.mak

系统老鸟：“要执行这个makefile的命令是：‘make -f sample. mak’，如果没有用-f指定makefile的文件名的话，make会在目前目录寻找名为‘makefile’或‘makefile’的文件。此外，如果没有特别指定target的话，make就会以makefile中的第一个target当作目标，在这个例子中就是‘main. exe’。假使你只要编译main. c的话，命令应该是：‘make -f sample. mak main. obj’。”

菜鸟：“这个例子相当简单，也颇为实用，但是我以前看到的makefile远比这个例子复杂的多，还有些像‘%’、‘$*’或‘$ @’等怪符号[6]，这些东西真的让makefile看起来很不直觉，不那么容易看得懂，所以我想知道，实际上我们到底会用到makefile的哪些语法？”

系统老鸟：“这个问题见仁见智，如果你对makefile的高深语法有兴趣，可以到网络找到很多相关资料，恐怕我现在也无法教你，因为我也要翻资料才看得懂那些怪符号。一直以来我就觉得make最主要的功能是：定义程序间的相关性，让只有新版的程序会被编译，并做出可执行文件。如果只是为了达到这个目的，我不觉得需要用到多么复杂的语法，基本上，就是前面的那个例子再加上一些功能，对大部分的项目就应该游刃有余。我们再来看一组比较复杂的makefile，如程序代码7-3和程序代码7-4所示。”

程序代码7-3：较复杂的makefile范例——define.mak

程序代码7-4：较复杂的makefile范例——main.mak

菜鸟：“写了注解的makefile确实较容易了解，原来只要熟悉几种简单的语法，就可以写出很实用的makefile。当项目中新增了程序文件、想要更改编译或连接的选项，只要对makefile做局部的更改即可，只要有make工具的正确思想，我相信大部分工程师都可以很容易看懂并编辑这些makefile。”

系统老鸟：“makefile还有其他较复杂的语法，例如，‘预设编译规则’这个功能也蛮好用的。在上面的例子中，项目中有几个。c文件，就要写几个。obj的target，虽然只要copy-paste就可以了，但整个makefile难免显得冗长，‘预设编译规则’就可以规定用同样的规则来处理扩展名为。obj的target。举例来说，下列语法就表示规定一个编译规则，以作为在make需要建立。obj文件时之预设规则。

以后所有.obj的目标只要写成以下形式就可以了，也就是说只要写target和dependency，make碰到扩展名为。obj的target时，会自动参考上述的预设编译规则。

菜鸟：“我就是一直看不懂.‘c.obj’还有‘$<’这些东西，天书似的！”

系统老鸟：“是啊！我自己也是，有时候翻书把它看懂了，过了不久再来看又看不懂了，终究makefile不像程序要常常更改，我个人的做法是尽量不要用太复杂的语法，而且我们通常会用某些工具自动产生makefile，所以我觉得工程师只要知道make能达到的功能即可。至于刚刚那个例子的语法我也记不得，你去查一查再来告诉我。”

菜鸟：“怎么这样！你不知道的东西居然还拿来问我。”

无论如何还是必须帮你解释上述命令的语法，在此之前，希望你都能了解，编写makefile的重点不在于使用了多晦涩难懂的语法，而是make工具处理target间依存关系的能力，这才是用来管理软件编译、制作可执行文件的主要功能。

7-2-2　非文件名称的Target

菜鸟：“我已经试用过我们的开发环境，最终目的是产生binary文件，现在我知道它是怎么从一个个C的程序文件，先编译成object文件，再连接所有的object文件与库函数，最终生成可执行文件，然后再把它转成可以下载到机器的binary文件，每次make时只会处理有新版本的文件，不会把所有的动作都做过一次，这就是make工具带来的好处。

我看了你写的使用说明，我们除了make最终的binary文件之外，还可以make一些不是文件名称的target，我也记得以前在编译Linux核心时，也要先执行‘make config’、‘make dep’之后，最后才能‘make kernel’。最特别的是‘make config’时简直不像在编译程序，它会跑出很多选项，感觉就像是在执行一个程序。还有另一个方式叫‘make xconfig’，如图7-7所示，它会跑出一个视窗，让你勾选Linux Kernel选项。因此make工具似乎可以做到编译、制作可执行文件之外的功能。”

▲ 图7-7　make xconfig的画面

系统老鸟：“没错！make工具的功能其实还蛮强大的，所以我们在设计开发环境的makefile时不只用它来执行编译、连接等动作而已，还会有一些不是文件名称的target，最常见的例子就是‘clean’这个target，几乎每个makefile都会有类似的target。你能根据你对make运行逻辑的了解，说明以下这个target的意义吗？”

菜鸟：“执行‘make clean’时，因为clean这个target没有dependency，所以一定会去执行命令，也就是说强迫把所有objects文件删除。”

系统老鸟：“没错！那你说说‘make clean’的意义是什么？也就是删除所有object文件对整个制作可执行文件的过程有什么影响？”

菜鸟：“制作可执行文件时，因为所有object文件都已经被删除，和这些以obj为扩展名的target有依存关系的。C文件会被视为新版，所以所有的。C文件会被重新编译以产生object文件，既然object文件也产生了新版，所以可执行文件、binary文件都会被重新制作。makefile中‘clean’这个target恰如其名，等下次要制作可执行文件时，不管程序文件是什么版本，可以保证所有的程序文件都会被重新编译。”

系统老鸟：“make工具是以文件的最后修改时间来判断其新旧，项目运行时难免会有人的计算机日期比较不准，或者与外国厂商合作开发时，常常会发生因为文件日期的原因，导致新版的程序不被编译，此时‘make clean’就可以解决这个问题。

虽然‘clean’这个target不是文件名称，但当它被make时可以执行一系列的命令，它的效果有点像batch文件或script。像这种非文件名的target也可以被当作其他target的dependency，所以可以做到当要make某个target时，先去执行一系列指令的效果，举例来说。”

7-2-3　人工编写vs. Automake Tools

菜鸟：“当弄懂了make的基本原则和语法后，会发现其实makefile并不是那么难懂难写。我还有一个问题，从项目开始，随着时间推移，程序才会慢慢地加入项目，如果每加入一个程序文件就要麻烦负责维护开发环境的人员修改makefile，那你岂不是有段时间会很烦？”

系统老鸟：“当makefile的基本架构写好之后，新增文件不会是件多麻烦的事情，不过就是copy-paste罢了，不会花费太多时间，而且我们管理层希望文件加入项目时能被统一管控，例如，是否放到正确的目录，加入后是否会影响目前可正常运作的模块，如果更积极一点，可能还可以顺便看一下程序风格，我已获得老板的授权，如果有不根据规范写作的程序，可以要求工程师修改后才可加入。我个人觉得手动修改makefile并没有什么太大的缺点或麻烦。

如果你觉得这样不够专业的话，有一种称为‘automake’的工具[7]（如图7-8所示）可以扫描指定的目录，根据你提供的makefile模板（Template)，把指定目录中所有的程序文件加入makefile中。这个工具的好处是负责开发环境的人只要把makefile的模板写好，以后任何人都可以自行产生新的makefile，工作目录中的所有程序文件会无一遗漏地被加入makefile。采用automake工具当然可以减少人为的失误，但很难避免没必要的测试程序被加入makefile以及最后的可执行文件，而且又要学另一种工具的规定，一般来说makefile的模板比makefile还复杂一点。”

▲ 图7-8　automake的步骤与流程相当复杂（*表示该文件为可执行文件）

菜鸟：“那我们目前的项目是用手工还是自动的？”

系统老鸟：“你觉得哪一种方式比较好？思考一下。”

菜鸟：“以我工程师的想法，当然觉得用自动产生makefile的会比较好，手动修改好像蛮low-end的。”

系统老鸟：“从你开始参与嵌入式系统开发至今，难道不觉得很多事情都蛮low-end的吗？CPU很慢、很难直接使用面向对象设计工具、产品也没什么令人眼睛为之一亮的功能等，这就是电子产品市场的生态，能卖出去的产品就是好产品。所以很多工程师都有这样的盲点。其实low-end并不代表不好，high-technology也不代表就能做出好产品。

本项目的做法很简单，任何文件要加入项目都要让我知道，由我管控程序质量的第一关，并统一发布新版的makefile。至于我的makefile怎么做出来的，嘿嘿！你应该猜得到吧！”

菜鸟：“原来如此（心中OS：果然就是用automake)。”

7-2-4　Debug版与Release版

系统老鸟：“有关makefile设计还有一件重要的事情，就是可以让工程师选择制作调试版或正式版。譬如说，我可能提供两个批处理文件，‘make-release. bat’表示要制作正式版，而‘make_debug.bat’则表示要做debug版[8]。同一套程序和开发环境可以做出多个版本并非嵌入式系统的专利，开发期间难免会有许多用于调试或测试的程序代码，在正式发行的版本中，这些程序应该不能被编译，特别是存储器大小受限制的系统，对最终程序的大小更是锱铢必较。此外，你能想象你买到一台手机或PDA，用到一半时，屏幕突然出现一行调试信息‘[Debug message]：file xxx. xxx access error, please call Kevin! ’吗？”

菜鸟：“应该是用C语言的条件式编译来达到此目的吧，之前PM老大在教我系统设计时曾讨论过这个问题，用于debug版的程序段写法如下。如果要做debug版，工程师只要在定义系统配置的sys_config.h文件[9]中定义‘_DEBUG_VERSION’，反之，若取消‘_DEBUG_VERSION’的定义，则被包在‘#ifdef__DEBUG_VERSION’与‘#endif’之间的程序就不会被编译，也就是说，工程师只要去修改系统配置，然后重新build就可以制作出不同的版本，相当地方便。”

系统老鸟：“你的做法当然是可行的，但是我们一向不希望任何一位工程师都可以随意修改系统配置，因为不是每位工程师都了解每一个配置的意义，如果改错了，绝对会让整个办公室一整天鸡飞狗跳。你可能没听清楚我的需求，我希望只要去执行不同的批处理文件，就能够达到改变系统配置的目的，还记得C compile的-D参数吗？它可以在编译时期才定义宏变量。你先看一下以下的3个文件，如程序代码7-5，程序代码7-6和程序代码7-7所示，执行make_debug.bat会有什么样的效果？”

程序代码 7-5：make_debug.bat

程序代码 7-6：make_release.bat

程序代码7-7：makefile

菜鸟：“没想到makefile中居然可以使用Windows/DOS的系统环境变量，执行make_debug.bat会将Windows/DOS环境变量‘BUILD_MODE’设定为‘Debug’，然后就去make我们的系统。当然make会去参考我们的makefile， 一开始有个判断式，若‘BUILD_MODE’的值为‘Debug’，则会利用C compiler的-D参数设定宏变量‘__DEBUG_VERSION’，所以程序中所有的调试与测试程序都会被编译，做出来的可执行文件就是所谓的调试版。

同理，执行make_release.bat时，makefile中的判断式为false，所以给C compiler的参数为空，也就是‘__DEBUG_VERSION’不会被定义，所以程序中调试与测试程序都不会被编译，就可以做出Release版了。”

系统老鸟：“有许多方法可以达到使用同一套程序和开发环境，在不更改任何程序的前提下可以做出不同的版本，上面这个例子是最简单的方法。虽然makefile和批处理文件有许多很好用的语法，但我始终认为复杂不一定好，只要能达到目的，采用最简单的方法和每个人都可以看懂得语法才是最好的。就如同爱因斯坦说的：Simple is good！”

07-03　Link Script

菜鸟：“制作可执行文件的流程中，要先把所有程序文件都编译成目标文件，接下来就是连接（link）的动作，通过linker可以将所有的目标文件与库函数连接为可执行文件。无论是否开发嵌入式系统的程序，只要是C或C++写的程序，可执行文件的制作都必须经过这些步骤。我不了解的是，为什么在我们的开发环境中，执行linker时要传入一个扩展名为.ld文本文件[10]这个文件比makefile更像天书，内容包含了程序的名称与一些数字，不就是把所有会用到的目标文件连接在一起就可以了吗？为什么还要写如此复杂的描述文件？”

系统老鸟：“这个文本文件叫做Link Script，其中的内容是用来描述项目中的各个程序文件，分别要被定位到哪个地址，Link Script不像makefile，不同的编译工具会有不同的语法，想看懂Link Script恐怕只能详读编译工具的使用者手册了（虽然不同Cross-Tools间Link Script的格式差异极大，但原理大同小异。实际上我们不会花太多时间研究每套工具的Link Script语法，通常都只是先拿sample code来修改，碰到特殊需求时再去翻书就好了。

现在先不要管Link Script的语法，你可否先说说看Windows上的应用程序（XXX.EXE）是如何执行起来的？”

菜鸟：“细节我是不清楚啦！不过在学校修汇编语言时老师好像讲过，X86系列的CPU有虚拟存储器的功能，所以每一个Windows的应用程序有自己的地址空间[11]，程序在做linking时应该都会被定位到某个预设的地址，我们称这个地址为程序的“Entry Point（进入点）”，一般程序员根本不用管这个细节。这些细节都被诸如Visual Studio的集成开发环境包装起来，程序员只要告诉IDE项目中有哪些程序文件，IDE就可以把可执行文件完成，至于linking时下了什么参数，知道的人恐怕不多吧！

当使用者要执行某个.EXE文件时，系统（此指Windows）会去检查该可执行文件的文件头，以判断此文件是否为合法的可执行文件，并取得其相关信息，然后系统会建立一个新的地址空间，从硬盘中将该可执行文件的程序部分载到存储器中，然后把CPU的执行权跳到该应用程序的Entry Point，这个程序就可以执行起来了。”

系统老鸟：“这个年头能把这个流程说得头头是道的年轻人已经不多了，连接（linking）与执行的思想就是如此。我把嵌入式系统和Windows应用程序的不同处列举如下，你再思考一下，我们该如何来处理嵌入式系统linking时的工作。

■　系统和所有应用程序通常在同一个地址空间。

■　嵌入式系统通常没有硬盘装置。

■　程序通常直接在ROM或Flash中执行，而ROM是只读的。

■　程序中的数据必须存储在ROM中，因为ROM是只读的，所以执行时期必须被载入RAM 中。

■　有时为了执行效率，某些程序段必须在性能较好的存储器中执行，即不是将所有程序都连接在一起就好了。

■　最重要的一点，PC上有Windows帮程序员处理载入的动作，但在嵌入式系统中，从Reset起的每一行程序都必须自己写，当然还包含让应用程序可以顺利执行的所有动作。

菜鸟：“我大概有点概念了，最大的不同点是PC程序完全都在RAM中执行，再加上每个应用程序都有自己的地址空间，而且没有只读的问题，因此，所有的程序只要从同一个起始地址连接在一起即可。而嵌入式系统就不同了，程序段可以在只读存储器中执行，但数据则只能被寻址在RAM所在的地址，所以在连接时势必要告诉linker，程序段要被寻址到哪里（或说ROM的起始地址在哪里），数据要被寻址到哪里（RAM的起始地址在哪里）。

但老实说，即便我已经知道系统中ROM与RAM的地址，还是完全不知道怎么描述‘程序该如何连接’这件事情。”

系统老鸟：“你已经抓到一个重点，就是在执行时期，程序中的某些段会被从ROM中搬到RAM，而这些动作不能奢望有个Windows帮你做，只能由我们的系统自行负责。要弄清楚这些概念对初学者的确不容易，因为你必须先知道所谓‘程序’的架构。”

7-3-1　程序段与数据段

系统老鸟：“细节我就不多说了，信息本科应该要修‘系统程序’吧！ 一般人只知道程序会分为程序段和数据段，实际上应该再细分为：

■　text段：就是所谓的程序部分，基本上，执行时期text段的内容不会改变，除非有特别的需求（例如，为了加速），否则text段可以直接在ROM里执行，无须载入到RAM。

■　Read-only-data（rodata）段：程序中定义为 ‘const’的变量，以及诸如‘Hello World’这样的字符串，都会被归类到rodata段（请见以下例子）。程序段还有载到RAM中执行的可能，但rodata段没有加速或改变内容的需求，所以应该‘待’在ROM里即可。

■　Data段：有初值的global变量都属于这个段，请见下列的程序。在连接时期，这些初值必须加入可执行文件中，在执行时期，因为这些变量的值是可以被改变的，所以它们不能被寻址在ROM的地址，即系统在开始使用这些变量之前，必须将其初值载入RAM。所以在连接时，数据的处理是比较特殊的，一方面它必须被加入可执行文件，这样才可以与可执行文件一起被烧入ROM中，但这些变量不能被寻址到ROM，它们真正被操作的地方是RAM。总体来说，必须告知linker。data段会被加入ROM，但却要寻址到RAM的地址。

■　bss（Block Started by Symbol）段：没有初值的全局变量，请见下列程序。因为没有初值，所以无需加入程序之中，只要在连接时将其寻址到RAM即可。执行时期也没有载入的问题，但机器RESET后，不能保证RAM的内容都会是0，所以通常要由系统主动将整个bss段清为0。有些工程师在写程序时，会自己预设bss变量的初始值应该是0，在未设值的状况下就直接拿来用，所以每次开机后，该变量的初始值可能都不一样。这个问题呈现出来的状态就是某功能有时正确，有时错误，而错误的情形也都不相同，虽然这只是工程师的小小疏忽，但会让其他人觉得系统很不稳定。

在嵌入式系统中，上述程序四种区段的处理方式都不相同，连接时期要由Link Script告知linker如何处理这些区段。在执行时期的系统初始化阶段，就要针对各个区段做不同的处置[12]。根据以上的解释，我们接下来探讨Link Script到底包含了什么。在此之前，我先考你一个问题：‘可执行文件’内包含了哪些段的内容？”

菜鸟：“这个考不倒我，根据你刚刚的定义，因为bss段是没有初值的全局变量，所以只要知道它的地址与大小即可，放一堆0在可执行文件里是没有意义的，而其他的text、data、rodata段都对应到工程师写下的程序代码，当然必须加到可执行文件中。”

7-3-2　Link Script 的内容

系统老鸟：“在写Links Script之前，我们要先设计执行时期的存储器使用状况，图7-9所示是嵌入式系统最基本的存储器配置：

▲ 图7-9　执行时期的存储器使用状况

首先我们来看一个简单的例子，如程序代码7-8所示，你先不要管它的语法，先看注解里的说明，只要知道这个Link Script内包含什么即可。”

程序代码7-8：Link-Script实例

系统老鸟：“在解释这个例子之前，你必须先了解LMA与VMA的意义：

■　LMA (Load Memory Address)：某程序区段被‘放置’的地址。

■　VMA (Virtual Memory Address)：程序区段在执行时期的地址。

例如：数据会被放置在ROM，但执行时必须载入到RAM，则在ROM中的地址就称为LMA，而在RAM中的地址就是VMA。

一个程序区段要指定两种地址（LMA与VMA），确实是不太容易懂，但我们之前已经讲解过嵌入式系统连接时期的概念，你就试着描述一下以上那段Link Script到底想表达些什么。”

菜鸟：“我再仔细看了一下这个文件，配合刚刚说的思想，似乎也没有那么复杂，这个Link Script文件要传达的概念为：

■　我们的系统中有一块ROM，它的起始地址是0xC00000，另外有一块RAM，起始地址为0。

■　可执行文件中必须包含text、rodata与data段，其中text段和rodata当然在ROM里执行即可，所以被寻址到0xC0000，而rodata段跟在text段后面。

■　bss段因为没有初值，所以不会占据可执行文件或ROM的空间，它会被寻址到RAM的起始地址，在本例中RAM的起始地址为0。

■　data段比较复杂，它的内容也必须包含在可执行文件内，但执行时期它必须被载入到RAM里。所以data段的VMA在RAM中，跟在bss段之后，而LMA则跟在rodata段后，当然是在ROM中。”

系统老鸟：“我补充一下有关data段的细节，data段会和程序段一起被烧到ROM里，它的地址（LMA）应该是0xC0000多，但程序中操作这些有初值的全局变量时却是用另一个地址，也就是所谓的VMA，应该是地址0之上。初学者很容易忽略这个事情，如果在启动阶段没有将data段载入到VMA的地址，执行时期就会发现这些全局变量的初值都不对，当然会影响执行结果。你再看看图7-10所示应该就更清楚了。”

系统老鸟：“我再考你一个问题：假设我们有某一个模块（姑且称之speed_up模块）的性能需要改善，一个可行的方法是让其在速度较快的RAM中执行，问题来了，请问Link Script要如何来描述这件事情？”

菜鸟：“基本的做法应该和data段一样，这个模块的程序LMA在ROM里，VMA则寻址到RAM中，执行时期在使用这个模块之前，必须先将其从ROM载入到RAM里，但我不知道详细的语法怎么写。”

▲ 图7-10　执行时期的memory mapping

系统老鸟：“写法如下（以下程序段接在程序代码7-8的最后面），你要知道其实语法不重要，到时翻使用手册即可，重要的是思想。在嵌入式系统中这样的应用很常见，通常都是为了改善性能，程序在不同的存储器中执行有不同的速度，有兴趣的话，可以请教固件开发人员。”

7-3-3　定义在Link Script中的常数

菜鸟：“我还有一个实作上的问题，刚刚我们说要把data段从ROM搬到RAM，说起来很简单，但是我们程序中怎么知道data段在ROM的地址和size？如何知道要搬到哪个地址？难道是要用写死的吗？而且在以上的那个例子中，data段在RAM中的地址接在bss段后，所以每次bss段大小更动，data段的起始地址（VMA）也会变动，而且data段的大小也可能变化，那岂不是每次都要去改动程序？会不会太麻烦了？”

系统老鸟：“以前的编译工具功能比较弱，确实有可能如你所说把地址写死在程序中，是很麻烦没错。但现代linker的功能比较强，除了提供某些预设的变量名称（如‘__start_text’表示程序段的起始地址（VMA）等），工程师可以在程序中直接使用外，一般linker都可以让工程师在Link Script中定义代表各区段起始地址（LMA或VMA）及size的变量，所以我们可以在程序中利用这些变量，达到传输区段的功能。”

菜鸟：“你说得很清楚，我听得很模糊，可否再具体一点？”

系统老鸟：“在刚刚的例子中，我们可以在程序中使用的变量有：

■　__START_text_speed_up: speed_up 模块程序段 VMA 的起始地址。

■　__END_text_speed_up: speed_up 模块程序段 VMA 的结束地址。

■　__START_text_speed_up_lma: speed_up 模块程序段 LMA 的起始地址。

■　__SIZE_TEXT_SPEED_UP: speed_up 模块程序段的 size。

因此，你在程序中该做的事情就是：从地址‘__START_text_speed_up_lma’开始（ROM），复制size为‘ __SIZE_TEXT_SPEED_UP’Bytes的存储器内容，到起始地址为‘__START_text_speed_up’的存储器（RAM）中。至于程序的写法如程序代码7-9所示。”

程序代码7-9：将某个程序模块传输到速度较快的存储器上执行

菜鸟：“真神奇，我想都没想过居然可以在程序之外定义变量。”

系统老鸟：“给你一个作业，想想看如何在程序中把整个bss段的内容设为0。”

清除bss段比传输程序区段简单的多，请参考以下程序。

7-3-4　Map File 或 Symbol Table

系统老鸟：“连接后除了产生可执行文件外，通常还可要求linker产生map文件（例如，GNU Linker ‘ld’的-m参数），虽然每套编译工具产生的map文件格式可能都不一样，但它的基本用途就是记录项目中每一个Symbol[13]之LMA与VMA的对应关系。这个map文件很有用，我们可以从map文件中得知下列信息：

■　程序各区段的寻址是否正确

■　程序各区段的size，即目前存储器（ROM与RAM）的使用量

■　程序中各个symbol的地址

■　各个symbol在存储器中的顺序关系（这在调试时很有用）

■　各个程序文件的存储器用量

节录一段map文件的内容来看看，你就会明白了。”

因为map文件不是程序，所以实际上不会有注解在里面。以下范例用/**/包起来的文字，是笔者自行加入的解释。

菜鸟：“这个文件里面全是数字，看得人眼睛都花了，我怎么有办法从其中找到我要的信息？”

系统老鸟：“做出可执行文件后的下个步骤就是设法将其下载到机器上，做出可执行文件是一回事，你如何知道你的程序段或数据会不会太大，会不会超过ROM或RAM的size？你如何知道你的Link Script有没有写错，每个程序区段都确实寻址到符合机器的存储器设定？当然你可以说在机器执行一下不就知道了吗？但我认为负责整合的工程师一定要检查一下map文件，有些问题只会造成系统的不稳定，而不会马上死机，这种问题最麻烦了。

例如，数据超过RAM的size，在测试时可能不会马上出问题，只有在用到高地址的数据时才可能会压到其他变量，即便破坏了其他变量，只要系统不会马上用到这个被破坏变量，那么，当下也不会感觉程序有问题，当发现系统出问题时，极有可能已经在不相关的功能上了，这对调试会造成很大的困扰，一定要尽量预防。

至于检查的方法很简单，只要搜寻各个区段的起始和结束地址，以及你自己在Link Script定义的变量，确认其LMA、VMA和size都没有问题就可以了。”

07-04　ROM Maker

系统老鸟：“开发环境中编译工具的原理，已经都讲解过了，还有什么疑问吗？”

菜鸟：“我对编译工具中最麻烦的makefile和Link Script的写法还是有点迷惘，但基本的思想都弄懂了。”

系统老鸟：“在嵌入式系统的领域你一定要把持一个信念，花时间把这些工具的语法弄得滚瓜烂熟是没有用的，等下个项目换了新工具之后，语法可能完全不一样，你只要把思想搞清楚，其他的看data sheet或拿sample code来改就可以了。”

菜鸟：“这个道理我知道，只是第一次还是希望能把这些文件完全看懂。在整个开发环境的步骤中，编译工具已经帮我们把可执行文件做出来，可执行文件不就已经是二进制文件了吗，为什么还要转换？”

系统老鸟：“可执行文件格式很多，如ELF、COFF、HEX、S-Record及Windows用的EXE等，谁跟你说可执行文件一定就是二进制文件？以下是一个S-Record可执行文件的片段，一般来说它是一个文本文件，这样的文件怎么可以烧到ROM里？当然要将其转为纯二进制文件。即便可执行文件不是文本文件，如ELF、COFF以及EXE，但可执行文件内还是包含了文件头等信息，所以这种可执行文件也不能直接烧到ROM里，还是必须经过转换。总之，ROM里程序的内容应该就是一个个机器指令，CPU可以直接找指令出来执行。”

菜鸟：“接下来，是不是将可执行文件转换后的二进制文件（Binary File）烧到ROM或通过下载工具，就可以在机器上测试了？”

系统老鸟：“一般是这样没错，但实际上可没这么简单，可执行文件可能只是整个项目中多个二进制文件其中的一个而已。”

菜鸟：“啥？！”

7-4-1　Making ROM

菜鸟：“不是可执行文件转成二进制文件就可以了吗？那其他的二进制文件是什么？”

系统老鸟：“这有什么好惊讶的？你在安装PC的商业套装软件时，难道其中就只有一个可执行文件而已吗？现在的电子产品功能越来越复杂，以电子字典为例，我们该如何处理其诸多数据库文件？因此，我说可执行文件只是整个项目中诸多二进制文件其中的一个而已。

可执行文件要下载到ROM（通常我们说‘烧’到ROM执行），并直接在ROM中执行，这应该没有问题吧！但其他的文件呢？因为嵌入式系统通常没有硬盘这样的装置[14]，因此，必须把项目中所有会用到的文件（包含可执行文件的二进制文件），结合成一个下载工具可接受的单一二进制文件，我们通常称这个动作为make ROM，这样的文件我们称为Image File，研发团队在发布版本时也应该是Image File，而不只是单纯的可执行文件或其二进制文件。”

菜鸟：“先是可执行文件，接着是二进制文件，最后变成Image File，我已经有点混淆了！这些名词是哪一本书规定的？”

系统老鸟：“没人规定这样的名词，总之在我们团队中就是这样用，你看一下图7-11应该就明了了。举个简单的例子来说明，假使你的机器在开机时要显示一个JPG文件当开机画面，你要如何处理这个JPG文件？”

▲ 图7-11　制作最终Image File的流程

菜鸟：“如果是写PC的程序，则只要把它放在某个目录，然后用标准C库函数中的fopen（）、fread()等函数就可以将其内容读出，然后设法解析JPG文件并show在屏幕上即可，不管是Windows或Linux都有完整而稳定的文件系统可用，应用程序处理文件相当简单。我们的系统有文件系统的功能吗？”

系统老鸟：“在嵌入式系统中这个问题应该这么问：‘我们的系统需要文件系统吗？ ’，假设我们系统只有一个JPG文件要处理，你会如何设计你的系统？”

7-4-2　Constant C Array

菜鸟：“假使只有几个文件要处理，那当然就不需要文件系统了。假设是我来做的话，我应该会把这个文件的内容转成一个C的常数数组（程序如下），加到某个程序文件中，则该JPG文件的内容自然就会被连接到可执行文件，程序中我只要操作这个数组就可以show出这张JPG图像文件了。也就是说，做出来的可执行文件中就包含这个JPG文件的内容，具体来说，它应该是包含在rodata段里，所以只要将可执行文件转为二进制文件就可以下载到机器执行了，应该可以省略将项目中所有binary文件结合成image文件的步骤。

理论上不管有几个文件要加到系统中，只要将其一一转为C array，并给予不同的名称，在程序中就可以很容易地使用这些文件的内容，而且因为是直接操作存储器，没有File System的overhead，所以性能肯定会比File System好。”

系统老鸟：“没错！将文件转为C array的方法不但易于使用，而且做出来的可执行文件就已经包含其他所需的数据，无须再做其他的额外处理，而且操作数据时的性能较好，所以在嵌入式系统开发时被广泛使用。

这个方法的缺点是当数据文件很多时管理比较困难，必须有人负责严格管理数据文件与数组间的对应关系，否则很容易出问题。实际上常常发生数据文件更新后，但系统中相应的数组没更新，或者程序用错了数组名称，执行结果当然不会正确！这种问题并不难解决，但是当数据文件很多的时候，实在是烦不胜烦。此外负责产生数据文件的人员（如美工人员，或数据处理工程师）在修改了数据文件之后，要不自己更新相应的C array，要不就实时通知负责人员，中间任何一个环节的疏忽，都会造成新版本数据文件的丢失。”

菜鸟：“除了要求相关人员小心处理所有数据文件转为C array的流程之外，应该还有其他比较有效率的方法吧！”

系统老鸟：“嗯，实际上我们不会要求负责处理数据文件的人员产生C array，试想，要求美工人员做这样的工作似乎不太对，所以我们会要求所有数据文件必须进行版本控制，负责制作C array的人员定期从Server下载最新的版本，并产生array名称、文件名称与用途的对应表[15]。工程师只要有这个表格，再下载最新的C array程序，犯错的机会就会大幅减少。

另一种方式就是在系统中加入文件系统的思想。”

菜鸟：“嵌入式系统中使用文件系统会不会太复杂了？我看过FAT32的格式，似乎复杂的不得了，如果还要加上cache的机制，恐怕会有找不完的bug吧！而且我们又没有硬盘，文件系统要放哪里？”

系统老鸟：“我说加入文件系统的功能，谁跟你说要用FAT的？而且为什么文件系统不能在ROM里，一定要在硬盘上？”

菜鸟：“啊？”

程序代码7-10所示是一个简单的PC程序，可以将PC上的某个文件，转成C array。

程序代码 7-10：Binary File to C array

7-4-3　File System Image

系统老鸟：“首先我们来看一下维基百科中对文件系统的定义。”

计算机的文件系统是一种存储和组织计算机文件和数据的方法，它使得对文件的访问和查找变得容易。文件系统通常使用硬盘和光盘这样的存储设备，并维护文件在设备中的物理位置。但实际上文件系统也可能仅仅是一种访问数据的接口而已，实际的数据可以是通过网络协议提供的或者在存储器上，甚至可能根本不存在对应的文件（如proc文件系统）。

严格来说，文件系统是一套实现了数据的存储、分级组织、访问和获取等操作的“抽象数据类型。”

菜鸟：“简单地说，文件系统就是将文件存储、操作等细节抽象化的程序，它会提供一套简单的接口（API）让其他程序使用。至于文件存储在什么地方、用什么格式存储都有可能，重点是使用文件系统的工程师根本不需要知道这些细节，所以即便嵌入式系统没有硬盘或光盘，还是有办法实现文件系统。但我还是想不出来如何在ROM上面实现文件系统，ROM不是只读的吗？”

系统老鸟：“你还是执着于硬盘的概念，CD与DVD不也是只读的吗？Windows是如何实现文件系统的？ 一般嵌入式系统需要的文件系统其实很简单，因为执行时期通常没有写入文件的需求，所以只要在其他程序要求读取某个文件时，能够知道该文件所在的位置即可。不像用于硬盘上的文件系统（如FAT32、NTFS，或Linux上的ext2/ext3）， 一个文件可能会被切成多个部分来存储，我们的文件系统中每一个文件都是连续的，所以嵌入式系统的文件系统表格相当简单，只要记录每一个文件的文件名、起始地址和size即可。图7-12所示是一个用于嵌入式系统文件系统的概念，非常简单。”

菜鸟：“所谓的文件系统充其量只是一个索引表格罢了，设计系统时要先把文件系统表格的格式定义好，然后负责开发环境的人要设计一个工具，在制作Image File时要去扫描所有要加入系统的数据文件，这些文件要如上图般一一接在可执行文件的Binary File之后，所以可以算出各个文件在Image File的位置（offset），根据这些信息可产生File System Table（即图中的File Index Table），然后把可执行文件的Binary File、所有数据文件以及File System Table逐一加到Image File中。这个过程应该就是之前说过的making ROM吧！”

▲ 图7-12　用于嵌入式系统的文件系统

系统老鸟：“基本上没错，但你似乎疏忽了一件事。在做Image File时，所有的程序已经编译完毕了，如果某个数据文件的大小变动了，那么File System Table的起始地址应该也会跟着往上移吧！那么在执行时期，程序怎么有办法知道File System Table的真正位置呢？”

菜鸟：“这的确是个问题，我可不可以在File System Table前面加一段特别的字符串或数字数组（称为Magic String或Magic Number），程序只要去扫描ROM就可以查到File System的位置了？”

系统老鸟：“这个问题没有标准答案，你说的也是方法之一，但显然比较没有效率。别忘了，在嵌入式系统的世界里，开发者可以掌控所有的事情。既然ROM Maker是我们自己写的，ROM中哪些位置要放什么东西，也是我们可以控制的，不是吗？既然如此，为什么不把File System Table的起始地址写在ROM的最高地址呢？执行时期系统只要到这个地址把值读出即可，是不是比你的方法有效率呢？”

菜鸟：“果然是老江湖，的确要有正确理解才会有正确的方法。”

因为电子产品的功能越来越复杂，越来越多的嵌入式系统必须使用文件系统。实际上可以采用本节描述的方法，此外几乎所有目前业界在使用的嵌入式操作系统，都会额外提供File System的功能（称为中介软件；middleware），如μ COS II就可搭配μ CFS，开发团队可以直接使用，无需自己重新开发。

但这些File System middleware支持的文件系统格式通常都是业界标准的格式，如FAT16。在这种情况下，研发团队必须评估是否真的需要用到FAT，以及对系统性能的冲击等议题。笔者的经验是：除非应用程序有频繁写入文件的需求，产品必须可以接受使用者插入自己的存储卡（使用者自己的存储卡通常都是标准FAT的格式）或产品可以用USB与PC连接变成U-Disk等，否则使用FAT的意义不大，而且对系统的overhead很高。

07-05　Offline Tools 概论

菜鸟：“刚才谈到，如果系统需要一个简单的文件系统，则我们需要一个PC上的工具，用来制作File System image，既然我们的文件系统是采用自订的格式，我想应该没有现成的工具可以直接拿来用，所以势必要自己开发，也就是说，嵌入式系统开发并非单纯的写机器端的程序就好了，还必须有人要去写PC端的程序？”

系统老鸟：“没错，而且需要写的工具可能比你想象的还要多！通常我们称这种用于开发阶段必须自行开发，并且执行于PC上的工具叫做Offline-Tools。我把Offline-Tools归纳为六大类：

■　Program Generator（程序产生器）

□　系统配置（Configuration）设定工具——设计一个PC上的工具程序，可让负责系统整合的工程人员选择系统配置（如某些功能的开关、LCD的分辨率等），并自动产生.h文件或 make-file。

□　Resource Manager：我们称要加到系统中的字符串、图形与数据文件为resource（资源）。举例来说，某些使用者操作界面较丰富的产品会有很多小图像文件，这些小图像文件必须转成C array并加到程序中，其他程序则通过resource ID对应到该array。图像文件转为C array的动作当然可以用人工来做，但当图像文件数目很多，或碰到常常要更新图像文件的状况，用人工来做就显得相当没有效率。

因此通常我们会设计一个PC工具，可以让工程师编辑字符串、输入图像文件或数据文件文件名，即可自动转出包含所有resource ID定义的.h文件，以及包含这些resource内容之C array的.c文件。当碰到resource需要增修时，也只要重新跑一次这个程序，就可以马上产生所需的程序文件，除了效率高之外，也避免了人工疏失所造成的错误。

■　Data Maker：数据处理工具类似我们上面说的File System Image Maker，就是希望系统在执行时期能够以比较有效率的方式来存取多批数据。在某些应用中，数据量可能非常大，此时就不能再使用上面所提的简单的索引表格，而必须将这些数据文件整合为数据库。当然，一般嵌入式系统无法使用诸如mySQL这种数据库系统，必须根据应用的特性自行设计。以下列举一些较具代表性的DatabaseMaker应用。

□　File System：严格来说，File System 也可以算是一种 Database，只是 File System存取的单位为文件，而并非如一般数据库系统般是以一个一个记录（Record）为基本单位。

□　Database：以电子辞典为例，开发者需要从没有严谨格式的原始数据，建立可快速查询、索引的数据库。所谓的原始数据通常是纯文本文件，或字典出版社提供的特殊格式，其中可能包含上百万条长短不一的辞条，其中比较麻烦的是这些格式并不具有一致的标准。

此时开发者必须自行设计Database格式，所谓的Database基本上会包含数据文件与多个层级的index table。在CPU运算能力与存储器空间并非如此余裕的系统，部分的数据库文件还必须被压缩，而为了让系统在runtime操作数据库时能有较好的性能，并不是直接拿mySQL或zlib（ZIP文件的压缩算法）来用即可，数据库格式与压缩算法的设计，会是这种产品开发项目中最重要且不可忽略的工作之一。

□　随机出货的文件：在一些类似MP3播放器的产品中，会要求出货前就先存入指定的音乐，而这些音乐是无法被使用者删除的，一般的做法就是将这些音乐文件存储在另一个区域，不与使用者可以操作的区域混用。在类似的应用中，File System Image Maker的功能必须扩展，而系统则可以同时支持多个File System。其他类似的应用还包括电子书内置许多版权书籍，以此提高终端产品售价，以及随身碟内置许多工具程序等。

□　产品信息或预设出厂设定：每编辑一次程序或重新build一次系统就会有风险，所以对一些比较容易变动的System Configuration（如厂商信息、日期等），我们会倾向不要写死在程序里，而是将这些信息通过Offline Tool存储在file或Database中，让系统在run-time时取得其中信息即可。

■　Image Maker： Image Maker的主要功能就是要制作最后要烧入存储器中的image。通常这个image中不只包含程序，可能还包含了我们上述所说的产品资信、File System image、Database等。如图7-13所示，清楚地描述了 Offline Tool在嵌入式系统开发项目中所扮演的角色，图中的工具分别是：

▲ 图7-13　Offline Tools在嵌入式系统开发的应用

①　程序产生器（Resource Manager、System Configuration 管理器等）

②　Database Maker 或 File System Image Maker

③　Compiler/Linker 等

④　Image Maker——因为每个产品项目的属性不一，Image Maker并没有一致的规格，有的项目甚至不需要Image Maker（除了可执行文件外，不需整入其他的data），所以Image Maker必须根据系统设计时对存储器（ROM或Flash）的规划，将其他Offline Tool的输出，整合为最终可烧入存储器中的Image File。

■　下载工具：当Image Maker产生image后，除了可以用烧录器写入存储器外，系统团队还必须提供所有工程人员另一个较方便的下载工具，通常这个工具必须具有局部下载的功能，因为有时工程师只编辑了部分程序，或只更新了文件系统里的某些文件，此时，若仍需要全部更新整个image，势必相当费时，影响开发效率[16]。

■　量产工具：举例来说，厂商希望每批机器出厂后都有不同的序号（可能包含厂商、日期、保密用途等信息），但这些信息在量产阶段，进行存储器前烧录才可决定，此时研发单位必须提供一个PC工具给工厂，先将工厂人员输入的出厂信息写入image的特定位置中，然后才进行存储器烧录。诸如此类的应用不少，只要研发单位把能最终用以烧录之image的格式定义清楚，就不会有问题了。

■　模拟器（详情请参阅《第14章：模拟器》）

■　其他

不同产品或应用可能会衍生出不同Offline Tool的需求，其用途无非就是希望把一些较复杂、且具有一次性性质的工作（例如，将为整理的辞典数据，转换为可快速索引的数据库，或是将JPG文件转为点阵数据），能利用PC较人性化的操作界面与计算能力，在开发时期先处理完毕。”

07-06　下载与执行

菜鸟：“之前我曾经用过ICE，把程序可执行文件下载到机器的RAM中执行和调试（请参考第5章：实作你的第一个嵌入式系统），但那毕竟只是调试的手段，只要机器不接上ICE就无法执行刚下载的新程序，且可执行文件以外的数据文件完全没办法用ICE处理，所以开发环境中应该要包含可确实将最终Image File烧录或下载到ROM里的方法，让机器可以独立运行新版的程序。”

系统老鸟：“严格来说，所谓的ROM是无法烧录的。在大量生产前，要委托专业的Mask-ROM制造商根据我们提供的Image File开光罩（Mask），交期应该在两三个月左右，所以我们在开发时期一定要选用其他可重复读写的替代品，通常是NOR-Flash、EPROM或EEPROM，要将数据写进去有以下几个做法。

■　先利用ICE下载到RAM执行并测试。一个最简单的方式就是先将可执行文件寻址到RAM，并通过ICE将可执行文件下载到RAM里执行，暂时避开烧录的问题。使用这种方式首先要有足够大的RAM，可以麻烦硬件人员在开发时期先帮忙换上较大的RAM。此外，RAM比ROM与Flash的速度快得多，开发时期先在RAM上测试只是权宜之计，在正式版本推出前一定要将程序烧录到EEPROM或Flash中，并执行完整的测试。

■　烧录器：市面上有许多专业的存储器烧录器厂商，所谓的烧录器是一个可以与PC连接的机器（基本上烧录器就是一个很典型的嵌入式系统，如图7-13与图7-14所示），使用者要先把存储器的Chip放在烧录器的socket上，然后在PC上操作烧录器厂商提供的应用程序，选择Image File并执行烧录即可[17]。

▲ 图7-14　各式各样的存储器芯片烧录器

接着的问题是，烧好的存储器芯片该如何mount到板子上？对硬件工程师而言，IC的拆焊是基本技能，可是要软件工程师拿烙铁已是勉强，还要拆拔、焊接芯片几乎是不可能的任务。而且时常拆焊对存储器和板子来说都不是好事，最好的方法就是板子上存储器芯片不要直接焊在板子上，而是通过socket与板子连接（如图7-15所示），软件工程师只要把烧录好的IC放在socket上夹好即可。

■　ROM-Emulator：如果觉得每次要测试新程序时都要用烧录器烧录很麻烦的话，市面上有称之为ROM模拟器的开发工具可以使用，简单地说，这个设备就是用来模拟EPROM/EEPROM，它的一端接到板子EPROM/EEPROM的socket上，另一端则接到PC（如图7-16所示）。在PC上，工程师通过厂商提供的应用程序，可以将Image File下载到ROM模拟器的存储器内，机器连接着ROM模拟器，就如同接着一颗真正的ROM一样。

▲ 图7-15　板子上使用Socket，方便软件工程师抽换存储器

▲ 图7-16　ROM模拟器实例

■　Update程序：假使机器可以通过某种方式与PC连接并传输数据的话（如USB、RS232或网络线等），那么我们可以写一个称为updater（系统更新程序）的程序模块，如图7-17所示。在正常状况下，系统不会执行它，只有当某个特殊事件发生时才会被执行（例如，PC端送来连接需求，或某个hot-key被按下）。当updater被调用来运行的时候，可以从PC接收新的Image File的内容，并将其写入NOR Flash中，如此一来，就完成了更新机器上程序版本的功能。

菜鸟：“我觉得一般工程师应该会比较喜欢用updater吧！操作起来有点类似PDA的同步程序，编译到下载一系列的动作在PC上就可以完成，而且我们团队中好像只有几台烧录器和ROM模拟器而已，紧急时，要工程师排队才用得到这些设备。”

▲ 图7-17　Update程序的架构

系统老鸟：“没错！下载是整个开发环境构建的最后一关，帮工程师建立一个容易下载的程序是我的工作职责。除非是复杂度、参与人数较少的项目，否则，我们会倾向调派人力开发updater（系统更新程序），在项目测试调试阶段，可以帮团队节省很多的时间。实际上，updater没办法这么快完成，所幸在开发初期，应用程序也不急着上机器测试，但负责驱动程序、系统模块与updater的人员就比较辛苦了，他们刚开始只能用烧录器、ICE、ROM模拟器等设备来开发程序。等到驱动程序稳定后，updater的开发会和系统程序与模拟器开发同步展开。值得注意的是，updater除了执行于机器上的模块之外，还包含一个PC上的程序。”

菜鸟：“PC程序我很拿手，我可以帮忙写吗？”

系统老鸟：“呵呵，这个就不劳烦你了，你想想看，我们团队做了那么多的产品开发，虽然每次的硬件平台都不一样，但我们总可以制定PC端程序与机器间的通信协议（Protocol）吧！所以每次开发新产品时，我们应该只要在机器上实作符合这个protocol的updater, PC端的程序只要小修正，应该就可以work 了。你知道PM老大对保存团队研发资产很重视的，再加上人力不足，所以在设计时都会考虑到重复使用的可能性，虽然施行起来比较困难，但朝着这个方向走总是对的。

既然我们团队已经将updater的架构做出来，你再看看以上这个操作流程图，是不是还能衍生出其他应用？”

菜鸟：“updater其实就是在执行时期，隐藏于系统中的一个monitor，更重要的它是系统的一部分，且具有与PC程序沟通的能力，所以我认为这个monitor可以负责收集系统执行时期的状态，定时或当发生特殊事件时，将信息送到PC，例如，系统存储器使用量、存储器某个区域的内容、task的执行顺序、各个硬件中断的发生频率等。

此外，这个monitor也能够提供API供上层程序调用，我想到最简单的应用就是上层程序可以把debug information往PC丢，工程师可事后分析程序的执行结果与流程。总之，这个monitor就像是系统工程师派遣到系统内部的间谍，可以源源不断地将系统运作的信息往PC送，更进一步想象，PC程序可以自动分析monitor传出来的数据，整理成有用的报表，甚至主动警示系统曾经出现过异常的执行状况。”

系统老鸟：“没错！但你只说到monitor可往PC端传递信息，别忘了 PC端也可以往机器端传命令。举例来说，PC程序可以命令monitor将存储器某个位置的值记录下来，并插入特定的值，例如，CPU instruction——trap或break（CPU执行到这些命令就会触发软件中断），等软件中断ISR执行完后，再将该存储器位置原来的值填回去，并返回该位置继续执行。”

菜鸟：“这不就是远程调试的‘断点’功能吗？”

系统老鸟：“是的！ 一般人都以为debugger很复杂，其实基本原理还颇为简单，这种monitor/PC-program架构在嵌入式系统开发时可以发挥很大的作用，所以在此基础建设上投注资源绝对是值得的！”

最后顺带一提，本节提到Mask ROM的交货时间太长，而且烧录后就无法再更动内容，这对电子产品第一批生产是很大的风险。例如，已经交付厂商开光罩后才发现程序有问题，则不但开好的光罩与制作好的ROM都形同废物，时间的浪费所带来的影响更是难以估计，除非对程序的正确性很有把握，否则一般的做法都是：在第一批生产时，选用可重复写入的EEPROM或NOR Flash取代Mask ROM，生产只要使用烧录器烧录即可，虽然EEPROM或NOR Flash的单价比较贵，但好处是无需等待开光罩的时间，而且发现程序有问题时还有补救的机会。等到第一批产品上市并确定程序完全没问题，以后就可以更改工艺，使用Mask-ROM来生产。

07-07　版本控制Server

系统老鸟：“终于把整个开发环境的流程都讲完了，还有什么地方不懂的？”

菜鸟：“有些东西是第一次听到，我需要时间消化一下。对了！我读了你写的开发环境使用手册，里面有一节提到使用版本控制Server的规定，可否麻烦您和我讲解一下版本控制的概念，在此先说一下我以前写程序的经验

■　刚开始写程序时，总是会发生程序改烂了，之前的版本却忘了备份的悲剧。

■　后来我学会天天备份程序，随着版本越来越多，我根本不知道哪一个日期的备份代表什么样的状态。

■　于是我准备了一张清单，详列版本间的差异，但随着程序越来越多，一天可能改了很多程序，我后来就懒得去填那张清单了。

■　后来有个机会带着几个学弟一起做期末项目，虽然各有负责的部分，最后就是五个人做出五个版本，结果整合工作花了最多的时间。

其实我以前在玩Linux时就知道版本控制软件，如CVS、SVN（Subversion）等，即便吃了许多苦头，总认为大型项目才用得到专业的版本控制软件。直到进到公司后，我发现就算是一人项目，这些软件也可以帮我们管理程序开发的历史记录，以降低程序开发的风险，好处很多。更重要的是，目前这些工具非常容易使用，比起我以前在Linux上使用CVS要简单多了，并不会对工程师造成太多的负担。”

系统老鸟：“市面上有许多版本控制软件可以选择，基本功能都差不多，实在没有必要选用太贵的软件，一般我们会考虑使用的有微软的Visual Source Safe或open source的Subversion，而后者是不用钱的。详细的操作方法我就不介绍了，如你说的，它们都是图形操作界面，不需要怎么学习就可以上手，如果有兴趣不妨到它们的官网逛逛。”

系统老鸟：“无论是嵌入式系统或一般软件项目，只要涉及多人协同开发，就一定要做版本控制。每一个程序编辑后并上传到server就是一个版本，server中会记录每一个版本的修改者与日期，如图7-18所示，而且可以很容易取回任何一个版本，并比较任两个版本间的差异。如图7-19所示，这是利用版本控制工具，比较某个程序文件的第5个版本与目前工作目录下副本的差异点。

当软件开发达到某个milestone或有重大突破时，管理者可以为目前的版本取一个版本名，我们称为‘Label’（有些软件称之为tag），以后任何人都可以从版本控制server下载（称为Check Out）某label时间点的所有程序。这个功能相当实用，我们总会听到测试人员的抱怨：‘某某功能在某某版本是OK的，怎么现在又不行了？ ’，有了这个信息，负责调试的工程师就可以比较这两个时间点的程序究竟改了什么，很容易就可以把问题找出来。”

▲ 图7-18　某个程序文件的版本记录

▲ 图7-19　某个版本和计算机中工作副本的差异比较

系统老鸟：“值得一提的是SVN提供了一个很实用的功能——Branch（分支）。简单地说，当系统的稳定度达到一定水平，此时若要新增一个尚未经过测试的功能，或者对某个已经稳定的模块进行调速，在这些工作进行期间，为避免影响其他开发人员，最好可以将系统暂时分为稳定版本（Trunk）与非稳定版本（Branch）。如图7-20所示，开发中或调整中的功能可以在分支上开发，待其确认稳定无误后，就可以将其放回主轴上。”

菜鸟：“如果有多人同时编辑一个程序文件（如，XXX.C），编辑的地方可能相同，也可能不同，而且彼此也不知道别人改了什么，当他们分别把自己改过的xxx.c上传时，server会不会混淆啊？”

▲ 图7-20　主轴与分支平行开发

系统老鸟：“这是一个好问题，多人同时改一个程序确实很容易出问题。如果是在人比较少而且在同一个办公室的团队，因为彼此沟通很容易，通常我会采用禁止同时编辑同一个文件的策略。也就是要编辑之前，先下载目前最新的版本到自己的计算机，然后用版本控制程序的lock功能把这个程序文件lock起来。则下一位要编辑的人员，将会从server上得知这个程序文件被某人lock 了，此时，他可以先通知lock文件的人员并选择等待，如果很紧急的话，可以和lock文件的人员沟通，请先unlock，或请示主管仲裁。这样的好处是几乎不会发生版本冲突（Conflict或Collision）的状况，但不适用在人数较多，或开发团队分散多地的项目。

在参与软件工程师较多的项目，只有某些人具有lock版本控制server中文件的权力，否则某个工程师无意间lock 了某个文件，然后放大假去了，真正要编辑的人反而动不了。基本的原则是个人计算机里的程序文件都只能是副本，只有上传（称为Commit或Check-in）到server后才可被视为正式的版本。如果同一个程序文件，别人已经编辑过并Commit到server上了，则你在上传的过程中就会发生冲突，此时可以让版本控制程序自动合并（Merge），或协同上一个编辑的人员一起讨论如何解决程序代码的冲突。”

菜鸟：“像Linux这种开发者分散在世界各地的开发项目都可以被管理得很好，我们就这些工程师，应该不会出大问题吧！而且我觉得工程师应该会把自己的程序管理得好好的，既然如此，那为什么您还要在文件中叮咛（其实就是‘要求’）大家要遵循一些规范？”

系统老鸟：“如果你以为工程师会好好管理自己的程序，那我们就不用规定‘程序风格典范’，也不用定期做code review 了。尤其是当项目后期忙起来时，工程师会把所有规范抛在脑后，注解也不写了、全局变量乱用、什么模块化全忘了，等到大家都在忙着调试bug的时候，你和工程师说要code review，他们不摆个臭脸给你看才怪。身为管理者只能好说歹说，并时常找最新的版本回来看，如果看到太夸张的地方就顺手帮忙改一下，等工程师发现主管帮他改程序，下次他就会比较注意了。

有规范已经是这样了，如果没有规范岂不是更惨？所以我在程序版本控制中定义了一些规范，其实都是举手之劳，希望所有工程师都能遵守。”

■　每天到办公室的第一件事，就是去server找最新版的程序。若发现build不过，应该马上通知部门主管，并立即找出原因（所有开发人员随时保持使用最新版进行开发工作，会减少个人新编辑的code加入server后，导致最新版系统build不过的机会）。

■　当天编辑好的程序，在下班前就Commit到server上。

■　Commit程序时一定要注明这个版本做了什么修改。在Commit程序时，以不影响整个系统build通过为前提，特别在新增/删除文件时，务必记得makefile与Link Script要同时对应。

■　工程师应尽量每日Commit自己的工作成果，以留下开发轨迹。如果当日结果尚未完成，且可能影响系统build通过时，应该用#ifdef …… #endif把相关的code包起来，确保其他人员使用server上最新版本时，这些code不会被编译。

■　当系统正在进行架构或大范围的修改，若不另建branch，则应同样使用#ifdef……#endf将修改中的code包起来再Commit，使其他人员使用server上最新版本时，可确保尚未完成之新架构的code不会被编译。

■　编辑.h时，必须注意到对其他.c文件的影响（例如，编辑了某个structure的内容……）。

■　如果新版本是为了解决某个bug， Commit时一定要注明bug编号。

■　当工程师要lock某个文件时，请发E-mail通知相关人员。

■　发生程序版本冲突时，一定要实时找前一个修改的人员讨论，除非很简单，否则不可在不明白前一个修改的目的时，就径自解决冲突。

■　如果可以的话，尽量不要用版本控制程序的merge功能，只要有程序版本冲突发生就应该找相关人员一起解决。

■　当任何员工在工作上有重大突破，或完成某个重要功能，都可以在Commit程序后加上label（或tag），定义label时要详细注明该版本的特性。

笔者在此想花一些篇幅，来更深入地谈谈版本控制软件这个主题。以下内容都是多年来的实际经验，管理本来就不该是个死板的工作，所以软件工程教科书上的那一套很难照办。

首先我们来谈谈何谓“版本”。

一般来说，在项目启动与计划阶段，项目主管们会根据目标与管理需求设定几个查核点，例如，“基本功能展示”、“第一次试产”、“量产”等总都需要一个软件版本吧！甚至“客户将于某日到访”、“客户要求对某个问题限期改善”等都可能成为版本发行的原因，而每个版本都该有个“名字”，像是0.10.23、1.0A、2.5 SP2、1.0-20080916等，每个研发团队都应该对版本号的设定有严谨的规范，在此就不赘述。

对软件开发团队而言，版本发行是件大事，最终目的不只是产生可执行文件（或可下载到机器的Binary File）而已，过程中必须经过整合、测试、QC，发行的套件中应该还包含release note、技术文件，以及最重要的程序与数据文件备份等。在笔者的理解里，正式的版本（或说要“出门”或给客户的版本）应该要完整地包含这些东西，更确切的说，版本就是开发过程中，所有程序、文件、技术报告等在某个时间点冻结后的Snapshot。

在管理上，不可能要求每个版本都是完美无缺（否则，哪里还需要出新版本？），但要求每个版本的状态都必须在掌控之中，最基本的就是哪些功能OK，哪些功能NG。换句话说，一个版本应该很容易对应到一个状态（可能记录在release notes中），在未来的任何一个时间点，系统中的任一个程序或功能都可以拿来和这个Snapshot做比较。

要知道软件开发是多人协助进行的复杂工作，必须设法在创意与纪律间保持一个平衡。原本OK的东西被改烂，是项目经理一定要承受的风险，而“版本”（或Snapshot）可以在这种风险发生时，让团队快速反应，让破坏范围最小。

OK，以上都是简单的道理，就当作是前言吧！接着我们谈谈本节的主题——版本控制软件里的“版本观”。

如上所述，对软件开发团队来说，每个版本都是重要的查核点，这样的版本代表着系统某个时间点的状况。但对版本控制软件来说，每次Commit的动作都算一个版本（有的软件是用submit、check-in等这样的term，指的都是把编辑过的一个或多个文件提交到server），各位可以试想一个项目运行下来后会有多少版本！

这种每次Commit后产生的版本号可以算是这个（些）文件的版本号。以SVN来说，这个版本号就是一串号码（累加的序列号），事后也可以根据这个版号取得Commit时的时间点、系统的所有程序与文件。笔者要强调的是这种版本号是更细致时间段的Snapshot，但其代表的意义就不那么全面了，充其量只是某个文件或某个模块在某个时间点的状态而已，在开发人数与系统复杂程度达到一定程度以上时，要求版本控制软件上的每个版本都要运作正常的意义并不大。

讨论至此，其实项目经理碰到的困境是：项目计划中预计发行的正式版本通常只有几个，但版本控制软件上的版本又太多太杂，项目经理要如何随时掌握“当下”系统的状况？

（这里说的是整个系统的状况，因为还处于开发阶段，系统的状态应该是随时在改变的，如果想知道某个功能或模块的状态，只要叫相关研发主管来问就知道了，而整个系统的状态要整合并测试后才能知道，很可能某个功能在开发者的platform上OK，但只要与整个系统整合被其他程序影响就NG 了）。

笔者相信在不同组织，不同先进主管自有其掌控项目状况的方法。实际上，笔者喜欢用一种叫做“每周构建”的方法（这并非笔者发明的，最早是在微软出版的某本书里看到的）。简单地说，就是每周固定时间，对版本控制server上最新的code做一次整合，以整个系统通过编译、build成功为前提，若碰到问题，相关人员必须优先协助（注意！是要build pass，不是调试bug）。

并非build OK就没事了，整件事的流程如下，其实包含了不少准备工作。

Step01： RD根据规格，准备查核表（查核表里的项目会随着开发工作的进行慢慢越来越细）。

Step02： RD根据查核表，准备测试文件与测试案例（例如，产品具备播放MP3的功能，则必须准备各种bit rate的MP3文件，以及根据过去经验比较容易出问题的文件）。

Step03：开发工作刚开始时，每周系统build完毕后，由RD单位根据查核表，将相关功能检查一遍，查核表即为每周构建的报告（刚开始只要检查功能有或没有，随着系统逐渐成熟，就要开始检查功能正确性，同时，查核表的项目也会变多变细，需要的pattern也会增加）。

Step04：在版本控制Server上为此版本制定一个标记（不同软件有不同名称，如Label或Tag），则在接下来的一个星期内，这个版本是一个“基本上”状态受控制、一定可build通过且执行的版本，任何用途都可以使用此版本进行（例如，客户或其他单位需要一个暂时的版本做实验等）。

Step05：经过几周后，RD单位已完成查核表并收集好足够的pattern， build完毕后的测试工作可以变成不需要花掉太多时间的标准流程（SOP），既然是SOP，就不需要特定的技术人员来执行，即可将此routine的工作交由测试或QC单位执行，并写出报告。

到开发工作后期，或重要版本发行日的前一段时间，项目主管往往需要更新更快的信息，这样才能及时知道哪里有缺口，并实时调动资源，避免miss重要的查核点。此时可以把每周构建改为每周两次，甚至每日构建，如图7-21所示。因为SOP已经建立，系统状态报告产出只是routine的工作，重点是项目主管对报告的解读与反应。

通常使用Label（或tag）的时机都是在重要版本发行，或开发工作有重大突破时，如果再加上每周构建的label，则整个项目的开发轨迹就能够在被控制的状况下被记录下来。

以上说明了许多关于版本控制软件的想法，就笔者的观察，并非所有软件开发从业人员都真正的明白其重要性，可能有些学术或研发单位靠着定期备份，就可以安然无事完成论文或项目。笔者甚至也看过完全不需要回头看，就是拼命往前冲，靠老天保佑在拼事业的团队，对他们来说，软件开发只有一个版本，就是“卖出去赚大钱的那个”，对“开发轨迹”的重要性视若无物。

▲ 图7-21　每周构建报告的表格范例

要不就是公司高层规定所有开发工作都要用版本控制软件，但其实公司高层也不知道这个东西的重要性与可带来的利益，更谈不上执行时的重点何在！上梁不正、下梁歪，自然工程师也是阳奉阴违，只把版本控制Server当作另一种网络闺蜜或FTP server使用。

笔者从业以来，看过太多因版本管理不善造成研发成果丢失、重复开发（以前某个版本OK，不知道为什么突然NG，偏偏之前负责开发的员工离职了）、研发资产无法累积等，浪费时间与人力的事情。所以笔者已习惯，就算是自己一个人开发的小程序或文件，也一定会用SVN来管理，每次Commit时会把当时的进度与状态写清楚，并定期备份SVN的Database。

在此，笔者节录了一位网友的文章，个人觉得他把版本控制软件描述得非常简单扼要：

在版本管理系统的概念中，会有一个称为“文件库（Repository）”的区域，就像图书馆一样，收纳了许许多多的文件。当你需要编辑文件时，就要先去图书馆柜台登记取出，编辑完后再放回图书馆。勤奋的图书馆管理员会记录你取出跟送回的时间，以及同一批送回的文件有哪些，甚至为每一次的变更加上注解。这个图书馆并不单单只是管理文件出入流通而已，它还会保存每一次文件送回去的样子，也能够帮你比对任两个不同版本间的差异。

当然，如果你不慎损毁了取出的文件，只需要向图书馆再登记一下，就又可以取回旧有的副本了，丝毫没有风险。如果有两个人同时对同一个文件做了变更，怎么办呢？这时图书馆也可以在所谓的“合并（Merge）程序”协助下，解决这样的“版本冲突”，产生合并双方的修改内容。事实上随着“合并程序”的发展，现在甚至能够做到“三方合并”！

另外一段话更是精髓：俗话说“工欲善其事，必先利其器”，做任何事成功的秘诀，就在于随时掌握工作中各个部分的进度与变革，并且随时做好回溯旧有版本的准备。因为许多美妙灵巧的想法，往往一个不小心就被掩埋了。

用来做版本控制的软件很多，free到天价都有，如Microsoft SourceSave、SVN、CVS、Git、Perforce等，但基本功能都差不多，笔者无缘用过传说中天价、神奇的软件工程工具，只知道使用像Git或SVN这样free的版本控制软件，就足以管控工程人员散布世界各处，像Linux与Android这样复杂的软件开发项目。如果你在办公室里都没办法搞定开发团队的版本控制乱象，那么，花再多钱也无法解决你的麻烦！因为问题肯定出在管理方式与思想。就笔者的观察，一般软件从业人员都对版本控制软件抱持着两极化态度：莫名的担心与过多的期望。

首先说说较悲观的想法，当开发团队内的所有成员同时在编辑系统里众多程序时，难免有人担心谁的code会导致目前的版本编译不过、导致某个功能变成NG、谁不小心把重要的code或文件删掉了等等，在此，笔者举一个例子：任何人（即便不会写程序的人）都可以申请一个Linux source server的账号，并参与Linux的开发工作（听起来相当伟大），然后不明就里地乱改一通，当然也不用管编译通不通过，就把这些程序更新回server。

笔者想问的是，全世界的Linuxer会因此受到什么影响？（如果这样就可以阻碍Linux的开发，Microsoft真的可以躺着赚钱了）。

答案是几乎没有影响！或者说这种破坏利用版本管理软件的基本机制，很容易就可以回复（Recover）。假使你的团队里没有不受控制的狂人，再加上些许的规定，根本就不用担心有人会破坏你的系统（唯一要担心的是server的硬盘坏掉、Database被破坏，这就要通过磁盘数组或定期备份来防范了）。

此外，维基百科也是一个很好的例子。每个人都可以任意增删编修文章，不同文化对同一个主题可能会有南辕北辙的见解，那么，维基百科是怎么运作的？可以认为这是一种动态的平衡，在此平衡机制下，整个系统仍会往进步的方向持续前进。

再来说说乐观者对版本管理软件的看法。再好的工具还是要人来操作，并非把server架设好，软件开发团队就可以达到使用版本管理软件的目的，当然还必须搭配思想灌输与一些管理手段（如上一篇提及的每周构建）。不过话说回来，你确定团队里的所有人都知道“软件开发团队使用版本管理软件的目的”吗？

很多政策推不下去都是因为执行者本身对目标也是一知半解，假使所有人都知道某个政策可为自己带来的“好处”，自然就会心甘情愿地去执行，而组织也会得到该政策被执行所带来的利益。软件工程那套在此就不提了，笔者认为使用版本控制软件最主要的目的就是记录项目中软件开发过程的所有轨迹，包含

■　记录每个程序或文件的每个版本、何时被谁修改过、修改的目的等等。

■　Label（或tag）表示系统开发途中的checkpoint或milestone。

■　当系统开发时，有时会不希望某个模块的开发工作影响到其他人，会建一个分支（Branch）出来，这个branch上的开发轨迹也应该被记录。

■　等该Branch的模块验证无误后，再merge回主轴（Trunk），这些动作都会被记录在server 中 。

对个人来说，所有软件从业人员都碰过自己把code改烂，但最近OK的那份code没有备份的状况吧！对团队而言，整个系统整合之后可能会出现的问题更为复杂，当碰到某个一直查不出主要原因的顽劣bug时，比较之前某个OK的版本往往是找出头绪的唯一方法。从项目管理的角度来看，所谓“项目团队资产”指的就是项目进行过程的点滴，并非项目最后完成的成果。

举例来说，开发初期使用algorithm-1去实现某个模块，当开发完成后发生规格修改，该模块必须重新使用algorithm-2来实现，到了结项时提交的成果就是用algorithm-2。以组织的角度来看这个案例，algorithm-2是这个项目的成果之一，但开发团队执行这个项目应该累积下来的资产，则还要包含algorithm-1才对，也许在其他项目中，algorithm-1就可以派上用场。

笔者执行电子产品开发项目多年，看过太多重复开发这样没效率的事情，其实只要善用版本控制软件，工程人员并不需要增加太多effort，就可以避免研发资产流失的情形。在这样的论述下，错误的implementation也是一种轨迹，因为事后我可以从后面的版本找到正确的解法。因此，对笔者而言，某位工程师因疏忽而上传某个不work或无法编译通过的程序到server上，并非什么了不起的错误，隔天，另一位同事更新程序时即可发现此错误，并通过版本控制软件的log找出始作俑者，然后其他人只要使用上一个版本就可以正常运行，而管理者不废吹灰之力就知道系统中有这个可能发生错误的地方，甚至可以知道该工程师的理解有某种程度的错误，并依此做出必要的处置。

所以笔者认为，一位好的技术主管的价值不是自己很认真加班写程序，自认可作为员工表率，而应该多看每位员工每天Commit到server的code，持续每天花时间做，技术主管可以掌握目前的状态与每位员工的进度，不会因为不写程序就远离技术，更大的好处是可以随时纠正错误的程序写法，并随时避免有工程师为图一时方便破坏系统架构，而且不需要太久的时间，技术主管应该就可以更明了每位工程师的特性与能力。

主管该做的就是管理，否则继续做工程师就好了，假使你刚刚莫名奇妙地顶上了一个主管缺位，不知道如何管理下属的时候，笔者建议先从程序的版本管起，看了每个人的程序，你自然知道要对你的member下什么命令，而且这些命令一定很实际且一针见血。

07-08　说故事时间

系统老鸟：“如果工程师或研发团队主管对嵌入式系统没有清晰正确的理解，很容易就会做出错误的判断，讲个真人实事让你参考一下。有一位产品开发项目的项目经理请我帮忙看一个问题，他们有个驱动程序做了两三个月完全没有进展，据他们所说，做了多次的code review，应该是什么方法都试过了，但机器就是没有反应。如果是你遇到这个问题，你会先帮忙检查哪里？”

菜鸟：“如果是我，我应该会先把那个驱动程序的data sheet看一下，然后检查一下他的程序，假使还是看不出问题的话，我就会请他们提供一套开发环境给我，我帮忙debug看看。”

系统老鸟：“果然是年轻的热血工程师，什么都想试一下。但他们是请你帮忙找出问题所在，并非要你帮他们debug，你还有自己的事要做，他们需要的只是你用你的经验去判断，并给出合理可行的建议即可。

你想想，程序错误的可能性当然有，但是他们已经做过多次code review，如果一堆人都看不出问题，我觉得以我对那个项目与平台的熟悉度，一个人恐怕不是那么容易从程序中找出问题所在。因此，我的第一个建议是：

你们有把程序请该CHIP与CPU厂商的FAE看过吗？他们的意见是什么？必要时请他们的人过来看一下。

许多工程师主管都忘了一件事情，我们是商业机构，不是学术机构，进度、质量与利润才是最重要的，发生问题时要尽快解决问题，而解决问题最快的方法之一就是寻求专家的协助，厂商要卖IC给我们，他们就有义务提供技术支持。”

菜鸟：“您说的也有道理，真正厉害的人只要动口就可以解决问题。他们一个问题弄了那么久，真的没有请厂商帮忙吗？”

系统老鸟：“的确没有，这可能和团队主管经验不足有关。接下来又说了我的第二个建议：

确定硬件是OK的吗？有没有请硬件工程师帮忙看过？

得到的答案是：硬件是厂商提供的评估版，所以应该是OK的，也请硬件员工帮忙测试过信号，似乎就是完全没有反应。如果硬件确定是OK的，那么是软件问题的可能性比较大，所以我的第3个建议是：

通过trace或断点等方法，你们可以把问题局限在某段程序代码吗？

负责该任务的工程师开始支吾其词，却又信誓旦旦地表示他已经做过很多测试，硬件就是没反应。我察觉到该工程师完全没提到trace的结果，也无法说出程序发生错误的地方，于是我请他帮个忙，我随便找了一段设定某寄存器的程序代码。

可否麻烦证明一下这行程序是有作用的？

如果是你，你会怎么做？

菜鸟：“这简单啊！前面说了那么多。首先要‘make clean’，确认所有程序都会被重新编译到，可执行文件做出来后，让你看一下可执行文件的日期，确定是刚刚做出来的。要证明某行程序有被执行有两个方法。假使可以输出字符串到LCD或RS232的话，我会在该行指令之前和之后各印一个字符串。如果可以的话，我会顺便把该寄存器的值读出并show出来，这样就可以知道该行程序是否已被执行。这个方法可以用烧录器或ROM模拟器来做。

第二个方法就是使用诸如ICE来trace程序了，我可以在那一行指令设断点，先确定程序是否会如预期的在这行停下来。停下来后观察所有寄存器的值，可以看一下PC（Program Counter）寄存器，确认停下来的地址是否和这行程序所在函数的地址很靠近（查一下map文件可以得到任一函数的起始地址，函数内的程序代码应该比这个地址高一些）。重点是执行这行程序之前，先看与设定之寄存器的值，然后单步执行这行程序，再查一下该寄存器的值是否有如预期改变。如果以上都无误的话，应该就可以证明程序确实有在机器上执行。”

系统老鸟：“你的想法很正确，但实际上，嵌入式系统开发团队常常会浪费很多时间在与程序无关的事情上。这个故事的结局是，该工程师改程序改了两三个月，结果他从来不曾正确的把程序下载到机器上执行过。”

菜鸟：“我觉得问题出在管理者身上，他应该清楚每一位组员的能力，并授予不同型态的工作或使用不同的管理方法，怎么可能让一件任务拖了那么久才发现工程师在鬼混。”

系统老鸟：“说工程师完全在鬼混也不尽然，主要是他碰到问题时解决问题的能力太差，他的主管也不遑多让，要不是太忙、经验太嫩，要不就是一样思想有问题。没有团队是完美的，团队成员各有各的专长和能力，尤其是电子产品开发要牵涉到那么多的领域，就是会发生诸如程序搞了半天没被编译到、使用到旧版本的程序或数据文件、下载失败等问题。

因此，开发环境的构建确实是很重要的一件事，让软件工程师可以专心地写程序、解决bug，不要花时间在弄懂这些工具，只要会用就可以了。身为软件工程师，假使写程序都会出状况，那恐怕就是胜任与否的问题了。”

[1]　嵌入式系统有各式各样的硬件架构，但通常不会是PC的架构，基本上程序是直接放在如Mark-ROM或Flash等存储器中执行；其中Mask-ROM是只读的，开发时期通常会用可多次写入的EEPROM或NOR-Flash代替。本章中统一称其为ROM，总之就是存储并执行程序的存储器。

[2]　远程调试——通常嵌入式CPU的IDE都会提供调试的功能，实际上它是在PC上通过某种方式与机器的CPU连接，在PC上对远程机器上执行的程序作调试。

[3]　可执行文件格式：业界常用的可执行文件格式很多，这些可执行文件不是单纯的二进制文件，其中还包含了地址信息以及调试信息，所以一般ICE、烧录器或调试程序接受的执行文件格式通常都不是单纯的二进制文件，而编译工具产生的执行档格式可能和上述工具产生的格式不同，所以需要许多转换文件程序。

[4]　map文件：有关map文件的应用，请参考《第7-3节：Link Script》。

[5]　举个例子，如果某程序文件的最后更改日期比可执行文件还要新，这表示上次产生可执行文件后，该程序文件已做了修改，则此时应该重新编译、连接，以产生符合目前版本程序的可执行文件。

[6]　在makefile中，“$ @”表示target的全名，“$*”表示target全名去掉扩展名，百分比符号“%”则代表万用字符，makefile语法的规则还蛮繁琐的，此非本书重点，你如果有兴趣可以参考《第7-2节：Makefile & 批处理文件。（.BAT）》提供的参考书或自行上网找资料。

[7]　automake工具并非本书的重点，你可自行上网查询相关资料，或参考：http：//sources.redhat.com/automake/。

[8]　善用批处理文件与makefile的技巧，有很多方法可以达成同一套程序制作多个版本的目的。本节的方法应该是最简单的方式，你可根据不同需求自行更改。”

[9]　请参考《第6-2节：系统架构设计》。

[10]　不同Cross-Tools里的linker可接受的Link-Script的格式可能不同，本书是以GNU的Linker - ld为例，其他linker的Link Script格式基本上与此大同小异。

[11]　地址空间（Address Space） —较高端的CPU都会提供虚拟存储器的功能，有了这个功能，系统可以实现让各个应用程序宛如有自己的存储器空间（称为地址空间），就算在自己的存储器空间中乱搞，最多就是自己死机，不会影响到系统以及其他正在运行中的程序。虚拟存储器的功能可用来实现多任务、多使用者系统，但复杂度相对来说比较高。而一般电子产品并未有此需求，所以大部分用于功能较简单的嵌入式系统开发的CPU都不会有此功能，即系统与所有应用程序会共享一个地址空间。

[12]　本节旨在说明连接时期的概念，有关执行时期对各个区段的特别处理，请参考《第8-3节：加载程序段与数据段初始化》。

[13]　Symbol表示程序中所有出现过的函数、库函数、Global变量名称，以及linker自动产生之代表各个区段（text、rodata、data、bss）起始结束地址的变量。

[14]　随着NAND Flash的普及与降价，现在很多嵌入式系统都会把NAND Flash当作类似硬盘的存储装置，即便是使用NAND Flash，在大量生产时，RD也要提供二进制文件（Binary File或Image File）供烧录器大量烧录。

[15]　实际上会自行开发一个工具，把某个目录下的所有文件一一转为C array的程序文件，并产生一个.h文件，其中包含所有代表数据文件的C array声明与每一个array的size，工程师只要include这个.h文件就可以使用这些array。

[16]　请参考《第7-6节：下载与执行》。

[17]　烧录EPROM、EEPROM与NOR Flash的步骤各不相同，请于操作时参阅烧录器使用手册即可，本书于此不再赘述。

Chapter 8

上电之后：Boot Loader

时间：08月01日

登场人物：菜鸟、项目经理（PM）、固件老鸟、硬件老鸟、CPU厂商FAE

PM：“办公室怎么那么热闹，你们一堆人挤在实验室做什么？”

菜鸟：“您不知道吗？前几天我们第一版的real size裸板（PCB）寄来了，硬件部门的员工已经加紧赶工把主要组件一一焊上去了，初步测试后，电源与时序都正确。固件部门的工程师正设法把Boot程序（或说Start Up Code）弄上去执行，目前似乎没什么进展，好像第一行程序都没运行起来。固件人员和硬件人员虽然偶有争执，但仍然一起测量信号、讨论程序，其他较资浅的软件工程师对这种合作关系觉得非常新奇，就纷纷跑到实验室，但大部分人都听不懂他们在讲什么，插不上嘴，所幸您在之前曾让我在板子上写过程序，所以我大概知道目前碰到了什么问题。”

PM：“我应该尽量安排机会和时间给员工教育培训的，大部分的应用程序工程师虽然身在电子产品的开发团队中工作，但普遍对嵌入式系统的概念都很薄弱，这个缺失迟早会反应在应用程序的质量上，用写PC程序的概念来实作嵌入式系统应用程序是很危险的。可是现在资深人员都忙着做事，谁愿意抽空来做教育培训啊！唉！真是麻烦。

先不管了，之前硬件电路设计与PCB Layout已经delay 一个多礼拜，如果Boot程序不能赶快上去的话，硬件设计正确性的验证与驱动程序的开发都会被影响。我很担心这边的进度，你跟我去实验室看看目前的状况。”

菜鸟：“哇！老板要御驾亲征了。”

从本章起，将偏重嵌入式系统实作的细节。在开始阅读本章之前，笔者希望你可以先回头翻阅《第5章：实作你的第一个嵌入式系统》，所谓的Boot-Loader是机器上电后被执行的第一个程序，其主要任务为负责布置其他程序（系统程序、驱动程序及应用程序）的执行环境，包含CPU初始化、必要的设备初始化，以及将各个程序段传输到正确的存储器等。因为第5章已经把boot程序的原理详细解释一遍，因此，本章内容将着重在细节与实际应用，对基本原理就不再赘述。

08-01　第一行程序

PM：“好久没到实验室走走了，还是一如往常的杂乱，你们真的能随时找到需要的东西吗？”

硬件老鸟：“这就不烦劳PM老大您操心了，您要找什么我随时可以找给您。您看起来很乱，我反倒觉得我们的实验室乱中有序，这样才有做事的感觉嘛。”

PM：“不跟你抬杠了，目前状况如何？听说boot程序还上不去新板子。”

硬件老鸟：“我已经验证过电源电路，系统上电后，供给CPU与各重要组件的电压都是对的，而且没有出现太离谱的耗电流，此外，还量了供给CPU Clock的振荡器（Crystal或Oscill ators），也是输出该CPU要求的24MHz。硬件看来没什么问题。”

PM：“那，外部存储器呢？量过板子上的线路是否有问题？”

硬件老鸟：“报告长官，您应该知道我做事是很严谨的。PCB layout图出去之前我已经check了好几遍，板子拿回来后的第一件事不是急着把组件焊上去，我会先用电表检查主要的线路是否有空焊、断接或短路的现象，这是我们专业该做的事情。”

PM：“我只是re-check一下，没有不信任你的意思。固件部分呢？有什么头绪吗？”

固件老鸟：“其实现在谈到外部存储器都还太早，我现在只想用ICE把程序下载到CPU内部的RAM中执行，要程序放上第一版的板子就可以动的几率简直和中大乐透一样，至少我从来没这么好运过。在板子制作的过程中，我已经准备好许多测试程序，这件事急不得，必须一关一关打通。开始若一股脑儿放了一堆程序上去，执行结果错误，甚至根本不能执行时，到底是硬件、软件，还是工具的问题都不知道，变量一堆要如何分析问题何在？

现在的状况是第一关就碰到麻烦了，ICE和CPU连不上，工具就出问题了，还谈不上程序的正确性。我们请了CPU厂商的FAE[1]协助，他正在看我们的原理图。”

CPU厂商FAE: “经理您好！我检查过了，基本上CPU部分相关的电路设计没有问题。我刚刚量了连接ICE与CPU各个PIN脚的信号，发现某一根PIN, ICE有送出信号，但是CPU端量到的始终是低电位（LOW）,于是我检查从CPU的该PIN脚到与ICE连接的connector间的电路，发现其中有一颗电阻没上，参考如图8-1所示就会了解了。等我把它焊上后，就可以通过ICE下载程序到CPU内部的RAM 了。”

▲ 图8-1　组件没焊上，是硬件调试阶段经常发生的疏忽

CPU厂商FAE: “其实这种疏忽在所难免，毕竟一块板子上有那么多大大小小的组件，所以一般我们在做第一次的板子验证时，应该不会只做一块，至少会做两块以上，这样才可以交叉比较。如果硬件人力充足的话，我可能会请每位分别焊一块，总不会那么刚好每个人都忽略同一件事吧！”

PM：“问题解决就好。硬件老鸟，去请你们部门的员工[2]帮忙再焊3片出来，就说我交代的，记得提醒他们该注意的事项。固件老鸟，现在就试一下你的第一个测试程序是否OK。”

菜鸟：“结果到目前为止，也不是硬件设计的问题，只是某个组件疏忽没有上而已，难怪之前一直有人提醒我嵌入式系统开发有一个特色：发生问题时必须多方考虑，不一定就是程序的问题，因为通常硬件与软件一样，也还处于开发中的阶段。”

硬件老鸟：“其实我也已经开始在对组件了，虽然我们迟早也会找到这个问题，但还是请厂商来看比较快，这颗CPU应该没人比他们熟吧！”

固件老鸟：“没错！这位FAE之前也帮我解决了许多和CPU有关的程序问题，有些事情是‘规定’，不是‘学问’、‘知识’，或‘思想’，遇到这类的问题还是尽快寻求协助比较好，不然我们的第一行程序不知道何时才能在这个板子上成功运行。

我刚刚把我的程序用ICE下载后，并做了以下的检查，判定程序确实有执行起来。

■　通过ICE在PC上的远程调试工具，在程序的第一行设定断点，确定程序有停下来。

■　检查CPU的PC（Program Counter）寄存器是否正确。

■　检查CPU内部RAM的内容是否和我们下载的可执行文件（Binary File）相同。

■　程序的第一行是设定CPU状态寄存器，执行这一行命令后停下来，观察CPU的状态寄存器是否如预期改变。

■　继续单步执行，确认PC寄存器是否会跟着改变（通常都是累加，除非执行到Function Call、goto或中断产生），且每行程序的执行结果都是正确的。

到目前为止，我只能说，板子上的电源电路以及CPU是正常的，接下来我会继续验证CPU与外围设备，确认板子的正确性与稳定性后，我们才会把所有验证过的板子交付给其他有需要的单位或员工。”

菜鸟：“可否容我插嘴，你们刚刚提到在板子制作时，固件工程师也不能闲着，必须准备一系列的测试程序，我想知道需要准备哪些程序？当确定CPU可以动之后，接下来的测试流程会是什么？”

固件老鸟：“你很久没来找我问问题了，依然是个求知欲旺盛的热血青年啊！PM老大，您可以把这名菜鸟借我一个礼拜吗？我想请他帮我debug几个测试程序，这样进度应该可以快一点。”

PM：“没问题！希望你们可以尽快把进度赶上，赶快确认板子是OK的，后面很多人等着要上板子测试。”

菜鸟：“我会加油的！”

PM：“学习很重要，但如期达成任务更重要，知道吗？”

08-02　基本硬件测试

时间：08月02日　 地点：办公室一隅　 登场人物：菜鸟、固件老鸟

固件老鸟：“你不是问我在板子制作的时候，固件工程师该准备什么测试程序吗？我们现在已经确认ICE和CPU可正确连接，也就是程序可以下载到板子上执行并调试。以你的认知，我们接下来该做什么测试？”

菜鸟：“您前阵子教过我啊！接下来不就是初始化CPU、传输数据、初始化必要的周边装置，然后将控制权交给主程序吗？”

固件老鸟：“你说的这个是boot程序的执行流程，我的问题是：既然Boot-Loader的责任是帮其他程序布置可运行的执行环境，那么我们该为这个第一版的real size板子做哪些测试，才能保证这个包含硬件板子与Boot-Loader执行环境是正确的。以下是我接着要验证的项目。

■　CPU寄存器操作测试。

■　Stack Pointer的设定是否正确？Function Call是否可正确运行？

■　中断矢量表设定是否正确？中断矢量程序是否可正确运行？

■　存储器初始化及其操作测试，并保证所有的存储器都可正常读或写（如果可以写入的话）。

■　将data段载入RAM,对BSS段设定初值。如果有程序段必须在ROM以外的存储器执行的话，Boot-Loader也要负责将其载入。在此阶段，我们必须保证当主程序执行起来后，程序中全局变量的初始值都是正确的。假使有需要被载入的程序段，也必须确认其可正确执行。

只有做好这些验证，才能将板子分配给其他人员继续开发工作。把状况不明或不稳定的板子交给其他人员，只会制造团队的纠纷、浪费大家的时间。”

菜鸟：“有道理！之前PM老大说过，越底层的程序越要保证其稳定性，如果整个系统的基础不稳的话，当应用程序出问题时，那真的会花很多时间在责任归属的争执上。”

固件老鸟：“对我来说，是谁的错并不是最重要的事情，再厉害的人也不可能保证自己写的程序绝对没有bug。重要的是，当系统内所有的程序模块都整合在一起时，不定时死机的问题真的很难找出真凶，与其到那个时候再来头痛，不如先确认较底层的模块都稳定无误后，才开始上一层的整合工作。”

菜鸟：“了解！我可以先帮忙验证您先前说的第一点，应该就是写程序去操作CPU的寄存器，然后逐步执行并观察寄存器是否如预期的更动。”

固件老鸟：“你应该知道CPU的寄存器有两大类吧？一种是状态寄存器与通用寄存器（General Purpose Register）,另一种是Memory Mapping Register。用程序存取这两种寄存器的方法是不同的，我记得我应该教过你。”

菜鸟：“您放心！之前您教我的思想和技巧我都弄懂了，因为每一个CPU内部寄存器都有自己特殊的名字，如 SP（Stack Pointer）、PSR（Processor Status Register）、R0（General Purpose Register #0）等，除非compiler有支持，否则很难用C语言的语法来操作这些寄存器，我想我会用inline assembly来编写测试程序。至于Memory Mapping Register,这种寄存器就如同存储器一样，所以只要用指针来操作即可，重点是指针类型必须加上volatile,我想我会用之前那个点亮LED的例子（请参阅《第5章：实作你的第一个嵌入式系统》）来验证Memory Mapping Register是否可以正常运行，如程序代码8-1所示。”

程序代码8-1：CPU寄存器操作测试

8-2-1　确认Function Call可否正常运行

菜鸟：“Function Call不是C语言的基本功能吗？而且编译工具会帮忙完成寻址的工作，程序员只要写对function的名称，填对参数，该函数就应该可以被正确调用执行。我还没听谁说过要测试这项功能的。”

固件老鸟：“Stack存储器是函数调用的基础，在嵌入式系统开发时，系统程序要自行管理Stack,如果管理不当，可能会发生一执行函数调用或函数调用多几层之后就死机的状况，而且有许多系统不稳定的现象或无厘头的bug，都是因为Stack使用不当所导致。

首先你要知道，C语言是利用Stack存储器完成下列事项。

■　Function Call时的返回地址记录。

■　Function Call时的参数传递（有些CPU的编译器在参数个数较少且类型简单时，会用CPU寄存器来传递参数，但碰到参数较多或类型复杂的状况时，一定是利用Stack来传递参数）。

■　函数内部的local变量也是使用Stack存储器。

■　ISR被执行时的CPU状态与返回地址。

Stack Point（目前Stack堆积的顶端）的设定与Stack存储器的配置是一件很重要的事情，但许多人却极为容易疏忽，而Stack会被忽略的主要原因是：在Windows或Linux上编程时，我们完全不需要设置Stack的大小与位置，也不太可能发生堆栈溢出（Stack Overflow）去挤压（或破坏）到其他存储器区的状况，这是因为在产生Windows或Linux可执行文件时，Linker会自动帮程序加上一段Startup Code,其中就包含了 Stack存储器的配置，再加上PC上有虚拟存储器，所以Stack的地址可以与其他程序段保持一定的距离。

但在嵌入式系统中就没有那么幸运了，linker如何知道你的存储器配置？如何知道要将你的Stack设在哪里？更不用说，大部分用在嵌入式系统的CPU都不会有虚拟存储器的功能。试着思考一下，在调用第一个函数之前Boot-Loader必须完成哪些事情？”

菜鸟：“我记得Boot-Loader的第一行程序就是设定CPU的堆栈指针（Stack Pointer,简称SP）寄存器，但是我有点搞不清楚C语言的Stack与CPU的Stack Pointer有何关系？”

固件老鸟：“一般工程师的确会弄不清楚这两者之间的关系，我简单说明一下，当你用C语言调用了一个函数，并传入参数，例如fun(a,b)，编译后的机器码应该包含以下动作。

Step01：执行CPU指令push,将参数a、b存入Stack（Stack是一种先入后出的数据结构，存入的动作我们称为push），接着CPU执行‘push’，指令，根据CPU SP寄存器的值，把值存入SP指到的地址，并把SP减1（通常Stack是从高地址往低地址堆）。

Step02：执行CPU指令call, CPU会把目前PC寄存器（Program Counter）的值push到Stack中，这个动作即意味着存储返回地址。

Step03：接着CPU把PC寄存器的值设为函数fun的地址，则下个被执行的指令就是函数fun的第一行命令。

Step04：当函数fun在执行时，可以利用目前SP寄存器的值，计算出参数a与b的地址。

Step05：如果函数fun有定义局部变量，这些变量的地址会从目前Stack的顶端往下摆，就是为什么我们在嵌入式系统开发时，老是会要求应用程序工程师不要定义size太大的局部变量，否则很容易发生Stack overflow的原因，如图8-2所示。

Step06：当函数fun执行完毕后，CPU会执行ret指令（也就是return的意思），这个命令会从Stack顶层取出（称为Pop）返回地址，然后更改PC寄存器的值为这个返回地址；则下个指令就会返回执行函数fun的下一行指令，从而完成了函数调用。

以上每一个动作几乎都与Stack有关，如果SP寄存器没有设定到正确的地址，或是没有配置足够大的存储器当作Stack，那么，在程序中调用函数时就很可能会出问题，如图8-2所示。”

菜鸟：“那么Stack存储器要放在哪里比较好呢？而且Stack的大小要规划多少才够呢？”

▲ 图8-2　局部变量太大导致Stack Overflow

固件老鸟：“其实没有一定的标准，因为一般CPU的Stack都是由高地址往低地址堆，所以在Boot-Loader阶段，我会选择某块RAM的顶端当作SP寄存器的初始值，之后驱动程序或系统要重新设定SP就不关我的事了。顺带一提，一般的嵌入式操作系统（RTOS）都会提供多任务的功能，每一个执行单位（称为Task或Thread）都会有自己的Stack,此时就会声明几个Array当作各个Task的Stack,如程序代码8-2所示。

程序代码8-2：用Array当作Task的Stack Area

至于Stack的size就更难定义了。在存储器空间很小的系统中，Stack size定得太大或太小都不对。我们刚开始会定大一点，并规范所有工程师尽量不要定义太大的局部变量，使用递归（Recursive）时也要小心一点，通常2K〜4K Byte应该就绰绰有余了，等到后期空间不够时再来调整。至于要调整为多少才合理，最好用工具来测量比较好。简单地说，就是在每一个function结束前读取SP寄存器的值，接着让测试人员操作所有的功能，最后会得出一个Stack曾被使用的最大值，把这个值再加一些buffer,应该就是最合适的Stack size[3]了。”

菜鸟：“Stack设定好之后，只要trace一下就知道函数调用有没有问题了。”

8-2-2　确认中断系统可否正常运行

固件老鸟：“接着要确认中断矢量有没有问题。负责写驱动程序的工程师只要将他的中断服务程序（ISR）的地址填入中断矢量表即可；Boot-Loader必须保证当驱动程序被执行时，中断系统是OK的，所以无可避免的，我们必须找一个中断源来测试。”

菜鸟：“中断系统不就是告诉CPU中断矢量表在哪里就好了吗？怎么会有问题。”

固件老鸟：“你把这件事情看得太简单了，有很多因素可能导致中断系统不work。换个方向讲，Boot-Loader阶段要做好以下工作。

■　中断矢量表数组（请参考程序代码8-3）,详细注解每个entry代表的中断源。

■　有的平台是CPU外接中断控制器，那就必须先完成中断控制器的驱动程序，才可能开始测试中断系统是否正常。

■　设定CPU的‘中断矢量表地址寄存器’（有些CPU的中断矢量表只能放在规定的地址，通常这种CPU就不会有中断矢量表寄存器），即告知CPU中断矢量表数组所在的地址。

■　设定CPU的中断控制寄存器，一般而言，每个中断都可以设定优先级，而CPU可以设定是否允许中断产生，或仅允许哪个优先级以上的中断才可被触发。

■　确定中断被触发之后，相应的ISR就会被执行。

■　提供ISR的写作范例（如程序代码8-3所示），让写ISR的工程师不用知道中断系统的细节，基本上就是编写一个C的函数即可。

程序代码8-3：ISR写作范例

固件老鸟：“以上任意一个环节出错，都可能造成中断不产生或ISR不被执行。举例来说，常犯的错误有

■　中断优先级寄存器没设正确，可能导致所有中断的优先级变得比较低，因而所有中断都无法产生。

■　中断矢量表中各个entry与中断源的对应关系错误，导致中断产生，却执行到错误的ISR。

■　将中断矢量表设定在错误的地址，许多CPU会规定放置中断矢量表的地址必须是某个值的倍数。例如，一定要放在偶数地址，或是4的倍数，甚至是128 K Byte的倍数都有可能。一旦程序把中断矢量表放置在不合法的地址，则CPU会认为没有中断矢量表。一旦中断产生，就会发生问题。

至于如何测试中断是否已产生，相应的ISR是否被执行，你应该很清楚吧？”

菜鸟：“在Boot-Loader阶段，应该没什么可用的信息输出函数，所以要证明ISR确实会被执行，应该只有使用远程调试工具（如ICE）来trace 一途吧！我们只要在ISR的程序中设定断点就可以了。程序停止后就开始单步执行，先确定ISR确实按照程序的内容执行，再确认是否可以正确返回中断发生的地方。如果接下来的程序还能够正常的运行，那应该就没什么问题了。只是我们该用哪个中断源来进行测试呢？用CPU内部的timer来测试可以吗？就如同之前那个LED的例子一样（请参阅《第5章：实作你的第一个嵌入式系统》）。”

固件老鸟：“当然可以啊！但是timer程序的写作毕竟比较麻烦，写timer驱动程序不是现阶段该做的事情，我们只是要测试中断系统的正常性而已不是吗？所以通常我会采用比较简单的中断源。例如，CPU执行到‘除以0’的指令时，会引发一个除以0中断（Exception）,所以我只要在程序中写‘int a = 100/0’这一行指令[4]就可以触发中断，进行测试。”

菜鸟：“居然可以这样，又学了一招。”

8-2-3　存储器测试

菜鸟：“接着，存储器测试是要测试什么啊？不是只要可以access到就行了吗？”

固件老鸟：“存储器会出问题的机会远比你想象的多。从硬件方面来说，如果数据线或地址线连接错误，就会导致读写一直出现错误。从软件方面来说，有些存储器（如SRAM、NOR Flash及ROM）不需要额外设定，只要给对地址就可以直接使用，但是其他的存储器种类，以SDRAM为例，则必须通过额外的控制IC（SDRAM Controller）才能操作，程序必须先设定好SDRAM Controller的配置，如SDRAM的size、速度等，才能正确的access到SDRAM。假使某些参数设定错误，如size设定的比较小，则SDRAM或许还是可以动作，只是会access不到高地址的存储器。

此外，系统中的存储器特性各异，所以当CPU在access不同的存储器时，一定要使用不同的时序（Timing）,至于外部存储器的时序该如何设定，则每种CPU都大不相同。你现在只要知道Boot-Loader必须负责设定各个外部存储器的时序。时序若设太快，系统会较不稳定；设太慢则系统整体性能变差。CPU的data sheet中通常会提示CPU在什么速度下使用什么存储器，Timing应该如何设定。图8-3所示为某CPU的Timing设定表[5]。

假设某存储器的Timing设定有问题，虽然程序或许还是能够access到该存储器，但经大量的测试后肯定会出问题。因此，在把板子与Boot-Loader交付给其他人员之前，一定要把各个存储器的每一个Byte都测过，确定读写（如果能写的话）都没问题。”

菜鸟：“把每一个Byte都测试过是什么意思？”

▲ 图8-3　实例——某CPU存储器Timing的表格

固件老鸟：“问题这么简单，年轻人要多动脑筋，你觉得是什么意思？”

菜鸟：“喔！我应该会写个测试程序，假设系统中有块RAM,起始地址是0x2000000,大小是2MB,程序如下。”

固件老鸟：“基本上正确，但是你这样无法测试到每一个bit,通常我们会对每一个Byte依序写入 0x00（二进制表示为：00000000）、0xFF（11111111）、0x55 （01010101）、0xAA（10101010），这样一来，每一个bit都会被写入0与1,这样的测试相对会比较严谨。

另外一个问题，假使存储器不能在执行时被写入（如ROM）,或者即便有方法可以写入，一旦存储器中的内容是有意义且不容破坏的（如用来存储程序的NOR Flash）,在执行时，Boot-Loader该如何验证ROM或NOR Flash中的数据是正确的呢？”

菜鸟：“我想到一个方法，可以用远程调试工具中检验查看存储器内容的功能，和当初烧入ROM或NAND flash的image file做比较。应该只要头尾选几个段，各找几个Byte来比较就可以了吧!”

固件老鸟：“想到和原始image file比对是正确的，但你的比较方法比较不专业，而且我们确实曾发生过一个Byte、甚至一个bit错误的状况。一般来说，有许多方法可以采用，最简单的就是计算checksum,我们会分别在PC为image file算checksum,另外在机器上为ROM或NOR Flash计算checksum,只要这两者值是相同的，我们就可以说机器上的ROM或NOR Flash读取功能正常，而且也可以顺便验证存储器的内容是否正确。”

菜鸟：“计算checksum是一种算法吗？以前在学校修算法时好像没听老师讲过。”

固件老鸟：“算checksum是一种简单且实用的小技巧，望文生义，就是把要验证的文件或存储器区域，一个Byte、一个Byte累加起来，最后得到的总和就叫做checksum。内容不同的文件或存储器区域，要算出相同checksum值的几率是非常低的。如程序代码8-4所示，一个是在PC上计算文件checksum的小程序，另一个则是在机器上计算存储器checksum的函数，程序代码非常简单，如程序代码8-5所示”

程序代码8-4：PC上的小工具——用来计算image file的checksum

程序代码8-5：运行于机器上的函数——用来计算存储器的checksum

8-2-4　CPU初始化

固件老鸟：“接下来要做一些CPU初始化的动作，总括刚刚提到的，在Boot-Loader阶段应该要做好以下与CPU相关的设定。

■　设定Stack Point寄存器。

■　设定状态寄存器，至少在此阶段要禁止（Disable）中断产生。

■　设定中断矢量表指针。

■　设定CPU执行状态（用于嵌入式系统的CPU通常都具有省电功能，可以让CPU以不同的速度执行。例如，图8-4所示就是某CPU的clock系统，看起来很复杂，可符合不同应用的需求，但就算不做额外的设定，只用CPU内部的预设值也没有问题）。

▲ 图8-4　实例——CPU中可设定的时序

■　设定存储器控制器（如果有用到像SDRAM的存储器的话）。

■　设定CPU操作各个存储器的时序。

■　有些CPU的PIN脚可以有多个功能，可以通过设定寄存器来设定。图8-5所示是某CPU的寄存器菜单，请看0x402DC的D1,某根PIN脚可以有两种功能——#BUSGET与P31（先不理会其功能与意义是什么），设定0x402DC的bit 1就可以选择CPU这根PIN脚的功能。在Boot-Loader阶段，尽可能把这种多功能的PIN脚设定为我们系统定义的用途。

▲ 图8-5　CPU之PIN脚设定实例

■　通常我们会选用已经整合许多外围设备的CPU,如：LCD Controller、USB Controller或SD卡接口等，虽然这已经算是驱动程序的范畴了，但如果有必要的话，也可以在Boot-Loader阶段就先初始化这些外围。

一般研发团队总是人手奇缺，所以做Boot-Loader的人，通常也要负责某些驱动程序的开发，在boot程序中顺便初始化硬设备也是极为稀松平常的事情。严格来说，其实Boot-Loader也可以算是一种驱动程序，没必要划分得那么清楚。”

菜鸟：“好像本项目就是您及您的其中一位徒弟，统包所有的驱动程序开发吧！我对驱动程序开发真的非常有兴趣，没参与到驱动程序与系统的开发，感觉好像不是在做嵌入式系统似的。偏偏PM老大最近忙得要命，根本没空理我，等Boot-Loader完成后，有什么工作就请尽量分配给我吧！”

固件老鸟：“呵呵，说是帮忙，其实还是想学东西吧！这也无妨，多个人帮忙总是好的。其实你现在已经懂不少东西了，碰到问题时不妨先思考一下，如果这是你的工作，你会怎么做？嵌入式系统开发其实没那么困难。”

08-03　载入程序段与数据初始化

固件老鸟：“确认存储器操作无误后，最后还要做数据的传输等动作，一般工程师绝对不会想知道这些事情，还记得Boot-Loader的任务吗？就是要帮之后的程序开发布置好执行环境，如果其他程序运行时才发现所有全局变量的初值都不对，试想，这会让研发团队浪费多少时间？因为牵涉到linker的设定等，所以这件事情还蛮棘手的，有空我再说给你听吧！”

菜鸟：“嘿嘿，这个我刚好懂！有关程序段的传输，前几天系统组的主管在教我开发环境相关知识时有提到（请参考《第7-3节：Link Script》），当时的确听得迷迷糊糊，后来回家从头到尾想了一遍，除了细节外，整个流程大致上应该都了解了。”

固件老鸟：“既然不用从头讲起，那我们就花点时间，把这个步骤要做的事一起复习一遍吧！你有什么问题就直接问，不必客气。”

8-3-1　载入data段

菜鸟：“我个人认为原理很简单，就是‘有设定初值的全局变量’必须被存储在可执行文件里、被烧录到ROM中，但执行时因为这些全局变量的值可能会被程序改变，当然不能在ROM里运行，连接时期必须重寻址到RAM中。就是这种‘存储在ROM，在RAM执行’，的特性，所以才有传输data段的需求，而且必须在所有程序使用全局变量之前，完成这件事。

我觉得比较麻烦的是LMA与VMA的定义[6]，我老是会把两者搞混。此外，在Link Script中定义可以在程序里使用的变量名称，对我个人来说，是非常令人印象深刻的新概念。”

固件老鸟：“惯于在PC上写程序的工程师应该会认为‘全局变量可以被设定初值’是天经地义的事情吧！殊不知这是因为Windows或Linux上的编译工具，自动帮每个程序加了称为startup code的程序代码，startup code就如同Boot-Loader 一样，负责替使用者的程序布置合理的执行环境，其中当然包含配置Stack、heap的空间，以及设定全局变量的初始值等。PC程序之所以能这样做，是因为PC的CPU可以实现虚拟存储器，每个程序拥有自己的地址空间，所以每一个程序中各个段（程序段、数据、bss段等）的起始地址可以是相同，这就是为什么编译工具可以提供所有使用者程序通用的startup code。

嵌入式系统的开发者可就没那么好命了，因为每一项产品的存储器配置可能都相同，所以编译器厂商无法提供通用的startup code,开发人员只能自行编写。”

菜鸟：“我知道这个做法，首先必须在Link Script中定义data段的寻址信息时，一起定义几个变量。以下面的Link Script片段为例[7]，这些变量的含义为

■　__START_data_LMA：data段在ROM中的地址（可把LMA想象为存储地址）。

■　__SIZE_DATA：data段的 size,即要传输的Byte数。

■　__START_data：data段在RAM中的地址（可把VMA想象成执行地址）。

有了这3个变量，我想传输data段的程序就很简单了，如程序代码8-6所示。不过在看程序之前，我们还是先复习一下执行时期的存储器使用状况，如图8-6所示。”

▲ 图8-6　执行时期的存储器使用状况

程序代码8-6：Link Scrip范例——data段描述

固件老鸟：“嗯，思想和程序都很简单，对初学者而言可能会觉得写Link Script稍嫌麻烦了点，等到把语法弄懂之后，就会发现其实蛮简单的，再就是LMA和VMA的观念不要搞混，总之记得前者是存储地址，后者是执行地址就不会错了。”

菜鸟：“之前我看到的范例是一个Byte、一个Byte传输，后来我翻过CPU的data sheet发现这样的性能会变得很差。我们目前使用的是32 bit的CPU,外部的Data Bus和CPU内部的一般寄存器都是32 bit,所以对CPU而言，性能最佳的方式是一次操作4个Byte,这个传输程序应该如程序代码8-7所示。”

程序代码8-7：copy_data_section()快速版

8-3-2　设定bss段

固件老鸟：“bss段的设定相对而言比较简单，因为它的成员是没有初始值的全局变量，所以根本不需要存储空间，在执行时只要把bss段的执行空间（VMA）都设为0即可。你能写出初始化bss段的程序吗？”

菜鸟：“Of Course! bss段的处理方式相对简单。程序代码8-8和程序代码8-9所示是Link Script中有关bss段的叙述，以及Boot-Loader中设定bss段初值的函数。”

程序代码8-8：Link Scrip范例——bss段描述

程序代码 8-9：copy_data_section()快速版

固件老鸟：“在boot阶段，data段和bss段一定要设定，否则执行时期全局变量的值就不会正确。换句话说，在设定完data段与bss段之前，Boot-Loader程序是不能使用全局变量的，如果一定要使用的话，在变量定义中给定初值是没有用的，一定要在程序内明确的设定初值才行。如程序代码8-10所示。”

程序代码8-10：Boot-Loader程序使用全局变量时必须谨慎

8-3-3　载入程序段

菜鸟：“除了data与bss段之外，需要加速的程序模块也要在Boot-Loader阶段传输吗？还是让使用这个程序模块的程序自行执行就可以了[8]”

固件老鸟:“这是一个好问题。你也知道要传输程序段时需要用到Link Script中定义的变量，让应用程序工程师负责这件事情可能较不适宜，应用程序工程师可能对相关知识比较不熟悉，而且万一搬到错误的地址就麻烦了。

通常我们会希望Link Script由负责架构开发环境的人员统一负责管理，他必须是团队中最了解目前系统存储器配置的人，最清楚要把什么段输送到哪里，一直以来，我们团队的做法都是让他处理程序段传输的事情，至于是否要在boot阶段做则视情况而定。假使要传输的是重要的系统程序模块，尽早就定位会比较好；如果要传输的是应用程序模块，我们会写一个函数让应用程序在需要的时候调用。有时候实际状况会比较复杂，例如，不同程序模块要共享同一块存储器区，在这种状况下，当然是有需要时才做载入的动作。”

菜鸟：“居然会有多个模块共享一块存储器的状况！那，Link Script不就更复杂了吗？”

固件老鸟：“你要知道一个观念，越快的存储器肯定越昂贵，越贵的资源在系统里自然就越缺乏，如图8-7所示。我们在学校修计算机组织时都学过，每单位存储器的价格从最贵到最便宜的依序排行为：CPU寄存器>CPU Cache>CPU内部存储器>外部RAM>硬盘，这个思想在嵌入式系统中依然通用。我们可以把上述关系再细分为：

CPU 寄存器>CPU Cache>CPU 内部存储器（Internal）>外部 SRAM>NOR Flash[9]>SDRAM>Mask ROM>NAND Flash[10]

你可以想象在系统当中，工程师可以使用的ROM一定比速度较快且昂贵的SRAM大上许多，这也是为什么CPU的Internal RAM总是小的可怜。像我们目前这颗CPU的Internal RAM就只有8KB，所以系统需要加速模块时，无法同时载入到Internal RAM中执行，只好将各个模块都寻址到同一个地址，视需求再载入执行。”

▲ 图8-7　存储器速度/价格金字塔

菜鸟：“在PC上随便编写一个Windows的应用程序容量就要1M Byte多，8K Byte能做什么？”

固件老鸟：“那是因为编译工具帮你连接了许多库函数，所以PC程序才会如此庞大，而嵌入式系统的空间反而要锱铢必较，8KB可以做的事情足够多了，估计相当于几百行C语言程序，尤其到项目后期发现空间不够时，有可能是RAM不够，也可能是ROM不够，你要到各程序中挤出空间来，到时你就知道。在嵌入式系统开发时，养成良好的程序编写习惯有多么重要。”

08-04　案例研究

8-4-1　案例研究（I）：由NAND Flash载入

菜鸟：“到目前为止，我们看了 Boot-Loader的所有程序，它做的都是和硬件相关的设定，如CPU寄存器、存储器以及简单的周边驱动程序，除了传输data段之外，它并没有载入任何程序段啊，这样的程序称为‘Loader’，似乎有点怪怪的，应该称为boot程序或startup程序比较合理吧！”

固件老鸟：“其实这只是名词的定义罢了，就像我们习惯把嵌入式操作系统称为‘RTOS’，难道每一个嵌入式系统都有Real-Time的需求和特性吗？在PC的概念中，boot程序要负责将系统程序由硬盘载入RAM里执行，这种boot程序就确实有执行‘载入’的动作了，因此，我们习惯把开机后的第一个程序叫做Boot-Loader，这个流程称为Boot-Strap。

我之前做过一个项目，选用的CPU很特殊，系统中的存储器为相对低廉的NAND Flash与SDRAM（可把NAND Flash想象成是类似硬盘的存储设备，只不过程序无法在上面直接执行，如图8-8所示），它的启动流程为：

Step01：CPU内部有一段小程序，供电后会从NAND Flash的第一个block,读出16KB的程序到CPU的内部存储器中。

Step02：CPU将控制权交给内部存储器的程序，这个程序就是所谓的Boot-Loader。

Step03：Boot-Loader会初始化SDRAM，再从NAND Flash将主程序载入到SDRAM。

Step04：Boot-Loader将控制权交给SDRAM内的主程序。

这个项目的Boot-Loader和主程序其实是不同的两个可执行文件，而整个系统中没有可执行程序的只读存储器，程序都是在RAM里执行，这样的架构和PC其实蛮类似的，但在嵌入式系统的领域中，这样的架构反而算是特殊案例。”

▲ 图8-8　从NAND Flash启动的架构

菜鸟：“为什么这颗CPU和我们之前谈到的ROM+RAM的概念不同呢？”

固件老鸟：“市场上各式各样、奇奇怪怪的CPU多的很，你有兴趣的话不妨研究一下玩具市场，玩具中采用的CPU几乎都是客制的，各式各样的功能都有，而且根据某市场研究单位的调查，设计这种CPU的市场相当可观。话说回来，这颗CPU是专用于较低端的手持式装置上，这种产品的成本控制相当重要，在此你要先知道一件事，NOR Flash和SRAM相对来说都是很贵的组件，而Mask ROM除了有制造时间长、风险大的特性外，更麻烦的是Mask ROM最大的size有技术上的限制，假使你的产品数据量是128MB （现在的电子字典容量超过128MB的比比皆是），那就可能需要4颗的（或更多的）Mask ROM，不但制造上会非常麻烦，而且还可能影响PCB Layout或产品外型。

假设要节省产品的成本，可以选用较便宜的NAND Flash与SDRAM，同样的价格可以买到较大的容量，但天下没有白吃的午餐，你可以把NAND Flash想象成硬盘，程序无法像Mask ROM或NOR Flash一样，利用指针、给个地址就能读取，而是必须通过命令才能读写，程序当然也不能在NAND Flash上执行，而SDRAM则是有耗电流高等缺点[11]。就是因为这些产品需求、组件特性与成本考虑，这家半导体厂商会设计出这样的CPU是其来有自。”

菜鸟：“我来推演一下设计这个CPU时的‘心路历程’。

■　由于产品内的数据量庞大，基于成本考虑，不可能使用NOR-Flash或Mask ROM。

■　市面上大容量且价格合理的存储器就仅有NAND Flash，但NAND Flash无法直接寻址操作，所以要把程序般到RAM去执行，而SRAM相对昂贵，所以最好选用SDRAM。

■　既然程序和数据都存储在无法寻址的NAND Flash，必然要有一段小程序将NAND Flash里的程序载入到RAM中执行，刚刚已经提过产品设计者不希望增加成本，多使用一颗Mask ROM或NOR Flash，所以这段小程序只能放在CPU里。

■　CPU的存储器价格也不便宜，所以这段程序size不能太大，自然无法拥有太多的弹性；它只能从NAND Flash的第一个block（block 0）读出程序到RAM，而这个放在NAND Flash的程序size也是有限制的。

■　CPU在设计时，根本无法知道客户要采用何种SDRAM，所以将程序从NAND Flash载入SDRAM是不实际的。刚好这系列的CPU内部都有一块RAM，原本是当作Video RAM用的，正好可以把NAND Flash内的Boot-Loader程序，载入到这块内部RAM。

■　被CPU载入的程序有大小的限制，所以它就只能是名符其实的Boot-Loader，布置执行环境，初始化SDRAM，然后负责将主程序从NAND Flash中载入SDRAM，并将控制权交给主程序，从而完成启动程序。”

固件老鸟：“不错！分析的很有条理。总之，Boot-Loader该做什么事情和CPU与硬件架构有关，至于有没有loading的动作并不是重点。”

8-4-2　案例研究（II）：开发Boot ROM

菜鸟：“您刚刚提到‘CPU内部有一段小程序’，这才是机器上电后被执行的第一段程序，它的作用是辨别NAND Flash的种类，然后到NAND Flash的特定地址载入我们写的Boot-Loader，这个程序显然也是一个简单的Embedded System，我还蛮想知道这个程序是如何开发，以及这段程序是何时被烧入CPU的？”

固件老鸟：“虽然我们公司是做产品开发的，但我之前在IC设计公司待过，也做过boot-ROM的开发，其实并没有如你想象中的神奇，实际上，开发的思想和一般的嵌入式系统开发是相同的。”

菜鸟：“可是IC明明是‘硬件’，怎么会和我们做嵌入式系统的软件开发理解一样呢？”

固件老鸟：“哈哈，果然是求知欲旺盛的热血少年，听我慢慢道来。你应该知道FPGA吧？细节不清楚也没关系，你可以把FPGA当作一个可程序化的SoC就可以了，FPGA除了能够模拟CPU core外，还能模拟其他数字符件与存储器，包含SRAM、ROM、OTP （One Time Programming memory；又称为单次烧录）。IC内置boot-ROM的开发流程如下。

Step01：IC team提供FPGA平台给软件团队（在SoC设计公司内称为SA—系统应用团队），而第一版FPGA的ROM当然不会有程序或任何有意义的数据在其中。

Step02：FPGA平台就如同一般的硬件板子，只是其中的主控IC是一颗FPGA，以及板子上会有用于更新FPGA的接口与相关线路。SA团队拿到FPGA平台后，只能利用ICE将程序下载到RAM里执行与测试。

Step03：当boot程序在RAM里执行且验证无误后，软件工程师会将程序重寻址到ROM的位置，并将产生的Binary Image提交IC team。接着，IC设计工程师会将boot程序的Binary Image做进FPGA的ROM里。

Step04：SA对必须对ROM里已有boot程序的FPGA平台再做验证。若有问题，则再重复 Step02～Step04。

上述步骤中，如果你忽略FPGA的部分，就当成有某个单位可以帮你把程序烧到ROM中，那么，这样的开发流程是不是与一般嵌入式系统的boot程序几乎相同？”

菜鸟：“但在FPGA平台上开发程序，应该还是有特别要注意的地方吧？”

固件老鸟：“的确，FPGA平台还是和真正IC平台不同，FPGA主要是模拟IC中Digital（数字）的部分，而Analog （模拟）的部分就无法完全模拟出来。例如，FPGA不能随意改变clock,而真实IC通常都可以调整clock。Boot程序必须做硬件初始化，其中当然包含Analog部分的初始化，这些程序段还是要写，但在FPGA平台不见得可以测试到，所以工程师对Analog相关的code必须相当小心，必须review再review。

此外SA除了利用FPGA平台开发boot程序之外，同时还肩负验证FPGA的责任。换句话说，一般硬件板子的主控IC至少不会产生太大问题，但FPGA平台中的主控IC却有较高的风险出现bug，这种状况难免会造成程序开发时的困扰，但对SA而言，程序运行不正常的地方，往往是找出FPGA中bug的线索所在。”

[1]　FAE（Field Application Engineer）通常中译为现场应用工程师，一般都直称为FAE, FAE要非常了解自家产品（如CPU或其他IC），最好有相关领域的工作经验，最主要的工作是协助客户解决使用产品上的问题。跟RD比较起来，RD做的事情比较封闭，只是待在公司里做研发，要当FAE必须懂得沟通，才能了解客户的需求并协助解决。

[2]　在开发初期用SMT自动打件制作板子不太实际，通常都是硬件工程师边焊边测试，有必要的话，可能要rework板子。但是当板子的状况稍微稳定之后，许多单位都要板子进行开发工作，此时让硬件工程师一片片焊未免过于浪费人力，所以硬件设计团队中通常会有较资深的支持人力，他们不负责设计与调试工作，其特点是手巧与细心，焊板子通常又快又好。

[3]　这种做法称为profiling,除了统计Stack size之外，还可统计函数的执行时间、被执行次数、存储器使用量等信息；通过这些数据可以推论出系统的瓶颈所在，在做系统性能调整时是很有用的信息。

[4]　编译时期应该会有警告信息，不予理会即可。

[5]　每颗CPU的设定项目可能都不同，你无需看懂表格的内容，只要知道设定系统中存储器的Timing时，一定要先了解CPU的相关规定。

[6]　如果你忘记了LMA（Load Memory Address）与 VMA（Virtual Memory Address）的定义，请回头温习《第 7-3 节:Link Script》。

[7]　完整的Link Script范例请回头参阅《程序代码7-8》。

[8]　传输程序段的范例，请参考《第8-3节：载入程序段与数据初始化》的《程序代码8-9》。

[9]　可以把NOR Flash想成可以重复写入的ROM。

[10]　可以先不用理会NAND Flash的原理与特性，只要知道现在的MP3随身听、随身碟中的存储装置就是NAND Flash，它是目前单位容量最便宜的存储器。

[11]　耗电流高是相对于SRAM而言，CPU必须持续提供clock给SDRAM，使其不断refresh，才能保证数据不丢失，而且当CPU进入待机模式时，可能就无法供给clock予SDRAM，此时SDRAM虽然可进入Self-Refresh模式，但仍需较大的耗电流，否则就只能任由其数据丢失。

Chapter 9

驱动程序

时间：09月01日　 地点：办公室一隅　 登场人物：菜鸟、固件老鸟

菜鸟：“因为最近比较重要的工作是板子验证与驱动程序开发，而且您这里好像也蛮缺人的，我已经和PM老大报告过了，这一两个礼拜我会在您这边帮忙，有什么工作就尽量分配给我吧！”

固件老鸟：“少了你在旁边问东问西，我想他也乐得清闲吧！其实驱动程序开发并不是人多就好办事，因为驱动程序位于系统的最底层，小小的错误，若在系统整合时才被发现，就会变成被形容为‘系统不稳定’的大事件，所以写驱动程序的工程师在开发时要很细心，测试时期心脏要很强。总结来说，在嵌入式系统的开发周期中，驱动工程师在初期会最忙碌，中期则要花时间在压力测试及稳定度测试上，到后期其他人开始忙的时候，驱动程序工程师则会被感觉似乎无事可做，在这个时期若被找出与驱动程序相关的问题，通常都会非常棘手。

对管理阶层而言，同样是程序的bug，但若是驱动程序有问题则代表系统不稳定，系统不稳定则代表质量不好，所以项目后期与驱动程序有关的问题通常都会要求相关人员写报告，解释问题发生的原因，以及修改后会不会有Side Effect（副作用）等，要求工程师用‘一般人’，的语言去解释程序逻辑的问题、硬件设定的问题，甚至是程序笔误的问题，简直会要了他们的命。

更惨的是，如果系统一直都很稳定，但那种偶尔发生过一次死机，或者测试人员用user根本不会这么操作的手法进行无厘头的乱测，导致系统变得不正常。像这样无法重现的问题，工程人员很难找出原因，更甭提修改了。但QC人员时不时就会把这个问题拿出来提一下，表示一切都在他们的控制之中，不明就里的老板就会问是哪里出的问题，于是负责应用程序的工程师说不是他们的问题、负责系统程序的工程师也说不是他们的问题，硬件更是推得一干二净，此时驱动程序工程师只能勇敢地把这个‘系统不稳定’的无头公案扛起来。

每到项目后期，总是看到驱动程序和系统程序工程师为稳定度在伤脑筋、熬夜加班；结项会议时，驱动程序和系统的质量更是千夫所指，所以我们团队中愿意学习写driver的人不多，你算是不怕死的。”

菜鸟：“记得PM老大说过：‘房子会倒塌不一定是地基的问题，但地基不稳的房子一定会倒塌’，他鼓励我要多参与驱动程序和系统的实作，甚至硬件的知识也要多学习，越到底层越能了解嵌入式系统的本质，至于项目中质量的责任归属以及相关政治运作，自然有你们这些老鸟扛着，我小小工程师人微言轻，暂时就做好分内工作就是了。”

固件老鸟：“当然应用程序开发也有其专业知识与技巧，但我始终相信，驱动程序与系统开发的学习门槛较高，工程师对计算机系统的原理要有更清晰的概念，会写驱动程序或系统的人再去写应用程序应该都没问题啦！ ”

一般人总会认为嵌入式系统和PC程序最大的不同，就是前者有一堆驱动程序要自己编写，这种想法当然不完全正确，经过前面几个章节的探讨，我们知道嵌入式系统的开发不仅仅只是开发非PC平台的程序而已，它有着诸多的限制，如：CPU计算能力、存储器大小、成本及进度等。每一位工程师，无论是开发驱动程序还是应用程序，都应该要了解目前正在开发的产品的本质与特性，如果用写PC程序的思想来开发嵌入式系统，无疑就像在系统中埋下一堆不定时炸弹。

举一个经常发生的案例：在PC程序的某个函数中，定义一个有1024个长整数的数组（4096 Byte）不会有任何问题，但在嵌入式系统中就可能造成Stack Overflow[1]（假设系统的Stack size为2048 Bytes ）。更麻烦的是，假使测试时没有把数组中所有的元素都设定数值，可能还不会出问题，一旦等到发生较极端的状况，当高地址的元素被写入值了，stack之外的存储器就会被破坏，而且假使被破坏的存储器不会马上被用到，而该工程师还浑然不觉函数已出现问题，要等到被破坏的存储器被使用时，系统才会出状况。此时正在执行的功能（function B）可能和罪魁祸首所在的函数（function A）完全无关，工程师会误判问题的来源，就算把function B从头到尾看一次，也看不出个所以然，因为问题是出在和function B完全不相关的function A身上。

一旦其他许多函数都会用到这个已被破坏的存储器，测试人员就误以为系统到处都会死机，这种随机性的不稳定就会被归类到系统或是驱动程序。

驱动程序是系统的基础，基础不稳定如同房子底层发生小小的摇动，传导到上层就会感觉是大地震，所以驱动程序稳定度的重要性众所周知。但这并不表示系统程序、应用程序的工程师就不需要了解嵌入式系统的思想，把系统不稳定的责任都推给底层的程序是极不专业的。像上面提到Stack Overflow的例子是应用程序很常犯的错误，有丰富经验的工程师可以很快找出问题所在[2]，但没概念的工程师就会在这个问题上耗很久，甚至到出货时都不知道问题在哪里。这种系统不稳定的现象是驱动程序造成的吗？显然不是吧！

在《第5章：实作你的第一个嵌入式系统》中，我们曾提及许多嵌入式系统驱动程序开发的基本思想，本章将更深入相关细节，在此之前，建议你可以回头复习一下第5章的内容。

09-01　莫恐惧！

固件老鸟：“一般工程师对驱动程序编写都有莫名的恐惧，其实嵌入式系统的驱动程序开发要比在Windows或Linux上简单很多。”

菜鸟：“怎么可能？Windows和Linux上的驱动程序有很严谨的架构、完整的文件与参考书籍以及方便的开发工具，就以Windows来说，安装了DDK（Driver Development Kit；设备驱动程序）套件后，再参考MSDN中的范例程序，根据微软的规定来写就OK了，不是吗？”

固件老鸟：“你曾经在PC上写过产品的驱动程序吗？恐怕没有吧！在Windows和Linux上编写驱动程序，除了必须了解新设备的硬件特性，把控制硬件的程序尽快完成之外，大部分时间都在设法让驱动程序符合Windows或Linux的规定。简单地说，就算硬件的控制程序是正确无误的，如果不符合操作系统的规定，这个驱动程序还是完全运行不起来。

Windows或Linux的开发环境都已经有稳定的操作系统，要增加对新硬件设备的支持，驱动程序自然要遵循操作系统的规定。可是偏偏Windows和Linux是通用型的操作系统，其驱动程序的架构又大又复杂，若不照着它们的规定去作就是不行，尤其是Windows DDK中定义的一些常数与架构的规范，都让Windows驱动程序开发人员吃了不少苦头，可以说他们花了大部分的时间让程序满足一种规定，我觉得这并非驱动程序开发的技术精神所在。”

菜鸟：“嵌入式系统的驱动程序开发有何不同吗？”

固件老鸟：“嵌入式系统的开发流程几乎完全倒反过来，你回头想想目前我们这个项目的开发流程就知道了。”

菜鸟：“没错，当产品的功能确定后，硬件规格的定义会比系统设计早完成，而且为了让硬件板子出来时就有测试程序可供验证，在硬件设计阶段，你们就已经在评估板上开始开发驱动程序了。在这个阶段，系统还在设计呢！难怪你说嵌入式系统的驱动程序开发流程与Windows或Linux相较是反过来的，在我们这个项目中，是先有驱动程序才有系统，所以你们只需要致力于驱动各个外围设备，不用花时间去配合一堆规定。但所有的嵌入式系统项目都是如此吗？”

固件老鸟：“嵌入式系统开发的状况千奇百怪，在现有的系统上增加硬件设备当然也有可能。但有一个理解却是不会变的：已上市的消费性电子产品需要扩展硬设备的机会几乎是零，所以设计一个可扩展的驱动程序架构意义不大，而且嵌入式系统中的资源是很宝贵的，把架构设计的很复杂完全不合乎逻辑!。不管先有系统还是先有驱动程序，嵌入式系统的驱动程序架构绝对要清晰简单，一般来说最多就是两层。

■　Driver层：真正驱动硬件设备的程序，上层的程序不应该直接调用到这层的函数。

■　API层：根据系统或应用程序的需求，将driver层包装成较简单的接口，这应该是上层程序与硬件沟通的唯一管道。

简单地说，驱动程序就是要设法隐藏硬件的特性，如程序代码9-1所示。例如，设定CPU状态寄存器（PSR）中的某个bit可以禁止中断的产生，但只有用汇编语言才能操作PSR，驱动程序工程师不但要负责确认该程序真正有禁止中断的效果，而且还要将其包装成API，就如同一般的C函数一样，让上层程序可以直接调用，这样做还有另一个好处，可以把机器相关与机器无关的程序代码分开。将来如果要更换平台，只要驱动程序的API相同，系统程序与应用程序应该就可以重复使用。”

程序代码9-1：驱动程序API范例——隐藏硬件特性

菜鸟：“驱动程序API是由固件组（实现的人）还是系统组（使用的人）负责定义的呢？”

固件老鸟：“理论上应该由使用这些API的人负责定义，只有他们才知道系统或应用程序需要用到哪些硬件的功能，可是他们显然比较不清楚硬件特性的细节，也不知道哪些功能可以实现，哪些功能可能做不到。所以实际上都会由负责固件开发的工程师，先根据硬件的特性与对最终产品的了解，先行定义驱动程序API的初版，然后再提交给将来会使用的单位参考，并根据其反馈做修正。”

09-02　准备工作

菜鸟：“我迫不及待要开始写驱动程序了，该从哪里开始？我们系统中有LCD、有keyboard、有touch panel，还有一堆不知道干什么的IC，我想都应该要写程序去驱动它们吧！您就分一个较简单的部分让我试试看好了。”

固件老鸟：“你什么准备工作都没做，该如何开始？驱动程序开发的工作模式和天马行空想一个算法来解决某个问题完全不同，即便没有上述操作系统的限制，硬件设计和外围设备都已经先定了下来，你总需要对要写驱动程序的硬件设计有所了解吧！否则，怎么会知道CPU和外围设备是如何连接的？假使不知道如何连接的，那么驱动程序要怎么写？你总要知道我们选用的外围设备或IC要如何驱动吧？如果你不知道要下什么命令才能让某一颗IC初始化，你驱动程序要怎么写？

驱动程序在整个系统开发中是属于没机会发挥个人创意的工作，若CPU或外围IC规定就是要照某些步骤执行它才能正常运行，你就不可能用其他方法让它动。这是很显而易见的道理，你要用它的功能，就得按照它的规定来走。”

菜鸟：“这我理解,那么在开始驱动程序开发之前要先准备些什么？”

9-2-1　检验查看可用的资源

固件老鸟：“首先，你必须思考写驱动程序需要哪些人或物的支持，在开始之前必须先弄清楚，在开发期间你是否能够取得这些资源与协助。以下是嵌入式系统驱动程序开发，应该设法取得并妥善应用的资源。”

■　IC原厂的技术人员或代理商的FAE:芯片的代理商会为其代理的每样产品提供一个技术资源的窗口，我们称为FAE。因为这个技术支持的人力是由代理商指派，所以应该都是本国人，不会有语言上的障碍。照理说，FAE应该是最熟悉其负责之芯片的技术人员（实际上，鬼混的FAE也不在少数），一旦我们选择了某颗芯片用在我们的产品上，无论硬件或固件工程师，一定要设法熟识原厂或代理商的技术人员，特别是CPU。在开发期间，无论是设计、实现或调试阶段，我们都可以从厂商的技术人员身上得到适时的协助。

■　评估板：不只CPU有评估板，外围IC的厂商也会提供评估板，这些可以运行的板子，对硬件工程师而言是设计阶段最好的参考；对固件工程师来说，则是自己的硬件平台尚未完成前的实验平台。此外，评估板也可以在硬件调试阶段当成对照组之用。

■　范例程序代码（Sample Code）:固件工程师应该善用厂商提供的sample code,如果可以的话，直接用在系统中也没有关系，因为这些sample code应该是由最懂这些CPU或IC的技术人员所编写，我们自己慢慢研究也不会比他们更清楚这些IC，写出来的驱动程序也不见得会比厂商提供的更完整。

■　产品规格书（Data Sheet）: data sheet是硬件人员设计线路与固件工程师编写驱动程序最主要的依据，有些CPU的data sheet厚达好几百页，在产品开发的进度，是不允许工程师慢慢地、仔细地研究其来龙去脉。基本上，data sheet的读法应该是先快速的全部浏览过一次，然后挑重点详读（有些部分是写给硬件工程师看的，固件工程师就可以先略过），最好能搭配sample code一起看更好。

■　硬件板子设计规格书：前面提过，最好固件工程师可以全面参与硬件的设计，如果做不到的话，至少要参加重要的硬件设计审查会议，除了原理图、layout图之外，硬件设计单位应该要提供设计规格书、CPU各个PIN脚的使用规划表格等书面资料，固件工程师必须根据这些资料规划驱动程序设计工作，假使有不可行的硬件设计，一定要尽早与硬件工程师沟通。

■　原理图与Layout图：固件工程师至少要能看懂原理图中CPU是用哪些PIN脚与各个外部IC连接的，这直接牵涉到驱动程序该如何控制这些IC。至于Layout图，固件工程师则通常只要能找到原理图的组件在板子的哪个位置即可，在量信号时才不会量错位置。

菜鸟：“我们公司也不是什么大公司，那些IC厂商的技术人员会理我们吗？”

固件老鸟：“这就要靠我们的口才了，这些卖芯片的厂商的确是‘唯量是问’，如果是你也会选择优先服务‘量’比较大的客户吧！所以身为小厂商的我们，有时候是得吹牛一下，谁知道你最后的产品可以卖几台？在不离谱的前提下，把这次产品的量说多一点，不然就狐假虎威，把我们的大客户抬出来，暗示IC厂商若不好好服务，我们的客户以后可能就会把该厂商列为观察名单或拒绝往来户。经过如此威迫利诱，IC厂商的技术人员通常都会乖乖来我们实验室报到。如果这几招都行不通，干脆就我们一群人，带着NoteBook和板子直接杀去他们公司找他或他的主管。”

菜鸟：“想不到信息界是如此黑暗……”

固件老鸟：“IC的代理商设置FAE这个职务，就是为了服务使用他们组件的客户们，通常FAE一个人要负责许多公司代理的组件，他要服务很多的公司与单位，你总得给他一个理由让他优先服务你吧！”

9-2-2　用C语言写驱动程序

固件老鸟：“另外一个对驱动程序的误解是：驱动程序通常都要用汇编语言写，这也是让一般工程师对驱动程序开发抱有恐惧感的原因之一。但实际状况是无论Windows、Linux或嵌入式系统的驱动程序开发，几乎都是用C语言为主，除非必要，我们尽量不会去写汇编语言程序，而且现在的C编译器通常都支持Inline Assembly的语法，所以原则上我们不会去写一个全部都是汇编语言的程序文件（.s或.asm）,需要汇编语言时在.c文件中插入Inline Assembly 即可。”

菜鸟：“是啊！ 一开始我也是对用C语言写驱动程序完全没概念，上次在评估板写作LED闪烁的程序后，我才知道在C语言中可以使用以下技巧来开发驱动程序。

■　volatile变量：操作CPU内Memory Mapping Register的方法就是使用C语言的指针，为避免相关程序被优化（例如，连续3行程序‘*a=1;’，就会被C compiler的优化功能变成一行），必须将指针形态设为‘volatile’，如程序代码9-2所示。

程序代码9-2

■　Inline Assembly：现代的嵌入式系统开发，为了可移植性、可维护性等原因，除了要操作CPU内部寄存器及对某个模块做性能调整外，用汇编语言写程序的机会不多，通常我们也会用Inline Assembly，免去汇编语言函数与C语言函数间互相调用时的麻烦，如程序代码9-3所示。

程序代码9-3

■　中断处理程序（ISR）的写法：虽说ISR其实就是一个C的函数，但ISR的起始和结尾与一般的函数还是有点不同。简单地说，在ISR开始时要将所有寄存器都存储在Stack Memory中，结束前要恢复这些寄存器的值，而且一般函数是用‘return’，的指令返回调用的函数，而ISR必须用‘interrupt return’的指令返回被中断的程序[3]。原本这些细节都要写ISR的工程师自己写，有的编译器会提供额外的语法，如在函数声明时多加一个描述字符串‘__interrupt__’，则编译器碰到这样的函数时，就会自动加上存储/恢复寄存器的指令，并用‘interrup treturn’指令来返回中断发生点，如程序代码9-4所示。

程序代码9-4

■　编译器定义的寄存器变量：有的编译器会提供非标准C语言的语法，让程序员可以操作CPU的内部寄存器，且并非所有C编译器都会提供这种额外的语法，而且我们也不知道使用这种语法时，C编译器到底会产生什么样的code，所以建议还是用Inline Assembly来操作CPU内部寄存器比较好。”

固件老鸟：“既然以前已经教过你了，我就不再花时间赘述了，用C语言开发驱动程序除了程序好写易读、容易调试与维护之外，工程师不用每换一颗CPU就要学习另一种汇编语言指令与语法。例如，同样是对寄存器赋值的汇编语言指令，X86家族CPU[4]汇编语言的写法就与大部分其他CPU的写法不同，如程序代码9-5所示。此外，驱动程序需要做很严谨的单元测试与压力测试，用C语言写还是比汇编语言要来得方便一些。”

程序代码9-5：不同CPU可能会有完全不同的汇编语言语法

9-2-3　新世纪汇编语言实务

菜鸟：“如果真的碰到非写汇编语言不可的状况，例如，发现某个模块是系统性能不佳的瓶颈，到时才去学习这颗CPU汇编语言的写法，会不会来不及啊？”

固件老鸟：“针对这个问题，我要纠正你两个观念。第一，汇编语言并不是改善性能的万灵丹，根据研究，大部分程序效率不佳的原因来自算法本身的问题；其次是程序编写习惯，例如，用了过多不必要的循环或判断式，和CPU特性有关的主要是数据存取的性能问题，我们前面提到过，假设CPU操作寄存器或外部存储器是以32 bit为单位，那么在程序中每次操作1个Byte（char）的性能当然比一次读出4 Bytes（long）到寄存器中的性能要来得差。通过改善程序的算法和多加注意数据存取的效率才是解决性能问题的主要方法，然后调整编译器优化等级，或将该程序模块传输到性能比较好的RAM里执行，用汇编语言改写绝对是最后的办法。以上步骤千万不能本末倒置，当用汇编语言改写后，再找出逻辑与算法的问题时，就不像改C语言程序那么容易了。

第二，非要用到汇编语言去改写某程序模块时，通常是对性能要求相当严格的应用。例如，播放压缩过后的音频文件，解压缩的性能不佳会造成播放时断断续续，这样的性能问题不只是简单的结果比较慢出来而已，而是代表这个应用根本不堪用，这种状况就必须用上任何的优化手段，包含用汇编语言改写。在我的经验中，真的不曾从头到尾写一个汇编语言程序文件，我都是用修改的，有时候要弄清楚某CPU的汇编语言函数和C语言函数是如何沟通就很令人伤脑筋，就算弄懂了第一个参数是用第几号寄存器存放，超过几个参数就要用Stack传递等细节，稍后换个编译器或CPU又有截然不同的规定。工程师的脑力和时间实在不应该浪费在这种事情上。”

菜鸟：“不懂，我们要改写的是我们自己的程序啊！谁会先帮你写好再让你改？”

固件老鸟：“讲个真实案例给你听。在某件项目中，我请硬件厂商的FAE帮忙检查某段汇编语言程序的硬件设定部分是否有问题，他后来问我：‘你常写汇编语言吗？这颗CPU我玩了这么久，也用汇编语言写了不少sample code给其他客户，但我实在写不出来像你用了那么多高级技巧的汇编语言程序。’刚刚不是谈过吗，我根本不曾从头至尾写过一个汇编语言程序，那上述的高级技巧到底从何而来？

答案很简单：我自己不写汇编语言程序，但我‘请’编译器帮我做，然后我设法看懂它，并根据我的需求去改写它。”

菜鸟：“说得那么神，编译器会帮您写程序？那它以前怎么不帮我写论文？”

固件老鸟：“呵呵，别着急嘛，你之前不是研究过开发环境吗？大部分的编译器的选项中都有产生汇编语言代码的选项（例如：GNU C Compiler相应的选项是-S），我都是把要用汇编语言编写的程序先用C语言来写，让编译器产生对应的汇编语言代码，然后我再拿这个一定正确的汇编语言代码来改就好了。程序代码9-6所示就是一个C语言函数与自动产生汇编语言的对照。”

程序代码9-6：编译器自动产生的汇编语言代码

固件老鸟：“对一个新的CPU来说，要从头到尾写一个汇编语言程序出来，大部分的时间都要花在遵循许多编译器或汇编器的规定，还记得我们用汇编语言改写程序的目的是什么吗？不就是为了改善某个模块的效率吗？如果我们把时间花在学习某CPU汇编语言的写作，岂非本末倒置且浪费时间？”

菜鸟：“我懂了，无论是驱动程序或其他应用程序模块，非得使用汇编语言的状况只有两种，一个是CPU寄存器名称无法用C语言表示的时候，另一个则是局部性能调整的状况。而后者我们仅需要知道到底我们写的C语言程序对应到哪些汇编语言代码，即CPU到底执行了哪些指令。只要深入的检查这些汇编语言代码，也许就可以找出瓶颈所在。”

固件老鸟：“因为现今编译器优化的技术已达一定水平，等你做过几次这个动作，就会发现编译器转出来的汇编语言绝对比我们人工写的优秀多了。人生苦短，工程师要学的东西太多了，要多把时间花在有用、有趣的事物上啊！”

9-2-4　Driver API 设计

固件老鸟：“之前我们谈过驱动程序最主要的目的，就是把硬件特性隐藏或封装起来，我们Team里严格规定，除了 RTOS之外，其他系统程序和应用程序只能通过Driver层提供的API接口间接地操作硬件。除此之外，任何直接操作硬件的动作都会破坏程序的可移植性，工程师必须严格遵守这个原则，所以在嵌入式系统项目开发初期，驱动程序设计者就要根据硬件架构及产品规格，开始设计Driver API。

Driver API的设计必须经过审查，以确定符合其他上层程序的需求，随着驱动程序的内部功能逐一完成，我们会把已完成的功能包装为制定好的API，并正式发布给其他人员使用。”

菜鸟：“好像在硬件电路设计完成之前，Driver API就已经通过审查了，难道硬件设计的细节对API都没有影响吗？”

固件老鸟：“在回答这个问题之前，我们先看一下Driver API设计文件中有关LCD的API,如程序代码9-7所示。”

程序代码9-7：驱动程序API设计范例——LCD driver

固件老鸟:“你能从API中看出我们用的是哪一颗LCD Controller吗？其中有开关背光的函数，能看出硬件电路是用哪一根I/O脚来控制背光电路吗？接着，再让我们看播放语音以及音乐的API，如程序代码9-8所示。”

程序代码9-8：驱动程序API设计范例——Music driver

固件老鸟：“用于播放音乐的硬件设计则有更多的变化。以播放MP3为例，即便是用软件解码[5]，CPU输出的信号还是要经过DAC IC（Digital to Analog Converter）、OP （放大器）以及静音电路等。如果用硬件解码的话，则CPU需要通过某种接口与MP3译码IC沟通，目前业界常用的接口是I2C或I2S[6]，此时驱动程序中就要去实现这些协议（Protocol）等，你在我们设计的Music API中看得到这些细节吗？

假设我们用的MP3 IC的型号是KME-3333，将Music API中播放音乐的函数定义为KME3333_play()将是错误的。第一，这样的接口势必会引起应用程序工程师的恐慌。第二，以后我们如果更换了其他IC，甚至可能采用MP3以外的格式，硬件电路设计可能完全不一样，此时KME3333_play()这个接口显然就不合适了，那就意味着应用程序必须跟着修改，所以我们把播放音乐的API定义为drvMusicPlay()。对应程序工程师而言，其代表意义非常清楚，基本上，他们只需知道call了这个function后，硬件就会开始播放音乐，至于硬件真正的运行方式，则是驱动程序工程师应该关心的事情。”

菜鸟：“所以，当产品规格定义完成以及硬件架构大致底定后，Driver API就可以开始进行设计了。我想在设计这套API时，应该要将重心放在系统式与应用程序的需求上；对于硬件设计，我们只需确认其确实能够达成这些API的功能，至于硬件电路的细节应该不会影响Driver API的设计。”

固件老鸟：“没错！实际上我们会把.h文件和包含空函数的.c文件先写好，这样的好处是在驱动程序还没稳定之前，不会影响系统的编译，其他的程序都可以同步开发。如此一来，我们会接收到其他人员对Driver API的意见，有必要的话，也可以尽早修改设计。

此外，我们一般称Driver API为硬件抽象层（Hardware Abstract Level; HAL），它对嵌入式系统开发还有一项重要的意义。因为Driver API上层的程序都是和硬件无关的，所以负责模拟器的人员只要在PC上模拟出这些API的行为，其他上层的程序应该完全不需要任何更动，就可以在机器与模拟器上运行，这样的架构自然会让系统具备较佳的可移植性。”

9-2-5　软硬之争：这板子真的可以动吗？

菜鸟：“记得您说过，在我们自己的板子完成之前，驱动程序工程师必须先在CPU厂商提供的评估板上搭配sample code进行开发。我的问题是，评估板除了CPU外，搭配的组件和我们的硬件设计可能会不一样，在这种情况下，驱动程序工程师在板子完成之前要如何做准备工作？”

固件老鸟：“我们先把状况稍微简化一点，假设CPU厂商的评估板是采用组件A，而我们的硬件设计将采用组件B。我们可以试着从硬件工程师的角度来看这件事情，当然可以根据组件B的特性，参考评估板上CPU与组件A的电路设计，再依循专业知识与经验来规划我们板子的电路设计，因为PCB板子制作是一个非常耗费时间与金钱的步骤，如果再请PCB厂商开始制作板子之前都没做过实际的实验，似乎太冒险了点。这就好像软件工程师对已经稳定的系统做了一些修改，即便这个修改很简单，在未经测试的情况下就发行正式版本，是一个风险极高且不负责任的做法。

因此，硬件工程师在重新制作板子之前，还是会设法评估或验证设计的正确性。通常都是手工在空的电路板或面包板上把组件根据电路设计连接，如图9-1所示，再将这块子板（Dauther Board）与CPU评估板连接，必要时会把评估板上的某些组件拆掉，有些IC的厂商便提供这样的评估板，硬件工程师可以设法修改，再把它和CPU的评估板连接。这个手工打造的平台就可用来验证硬件设计有没有大问题，但硬件工程师也仅只能验证其电气特性，还是需要固件工程师帮忙写程序验证。”

▲ 图9-1　面包板实例

菜鸟：“您的意思是在板子完成之前，驱动程序工程师必须先在这个手工的环境下测试？”

固件老鸟：“没错，除非CPU与其他组件都是我们以前产品留用的，否则，这种早期的验证手段绝不可少，与其花钱、花时间把板子做出来，才发现硬件设计是错的，不如早一点弄一个环境，供硬件和固件工程师进行硬件设计验证的工作，而且固件工程师也可尽早开始驱动程序的实现。

我就看过硬体工程师对自己太自信，硬件设计完成后也不先做实验就送Layout，Layout完成就直接请厂商制作板子了，结果板子回来后没有一片是可以动的。最后发现硬件设计有个致命的错误——SDRAM的地址线与CPU连接的地方位移了一位，因为SDRAM是对Timing很敏感的存储器，即便硬件工程师大费周章地把板子上错误的地方切掉，并重新用皮线（即电线）连接也于事无补，这次制作PCB的时间和费用等于是白白浪费了。少了事前的实验就去制作板子，如同赌博一样，真正有经验的硬件工程师都知道这个环节的重要性，就算犯错也是小失误，不会发生这种板子白做、浪费时间金钱的事[7]。”

▲ 图9-2　CPU评估板+组件IC子板

菜鸟：“您说的很有道理。在板子还没完成之前，无论硬件或固件工程师都应该设法先弄出一个可供实验的平台，如图9-2所示，但是这种用手工作的东西，我如何知道它的硬件电路有没有问题？在这样平台上写驱动程序会不会有点浪费时间啊？”

固件老鸟：“你实在太不了解硬件工程师的能耐了，硬件工程师也会觉得你写了一堆程序，怎么知道你的程序是没有问题的？用手工制作难免失误，但硬件工程师自然有办法把问题找出来，你不用觉得这有多神奇，就像你在一堆程序中把bug找出来一样，术业有专攻，既然合作开发一个产品，就要互相信任、互相帮忙。

我谈过嵌入式系统的驱动程序开发有一项很重要的特色，就是你在开始写驱动程序时，硬件可能还在设计，当有实验平台可以用时，其实不只硬件，驱动程序也是处于不稳定的状态。当发生问题，硬件工程师和固件工程师千万不要忙着推卸责任，一定要一起合作把问题症结找出来。当硬件工程师把实验平台交给固件工程师时，自然做过了基本的验证，固件工程师在此阶段的任务，则是用程序验证硬件设计是正确无误的，验证完毕后，该设备的驱动程序也差不多写好了。”

菜鸟：“要如何软硬合作？可以举个例子吗？”

固件老鸟：“我举个最简单的例子好了。假设硬件工程师焊了一个LED子板到评估板上，固件工程师的任务是验证LED相关硬件电路的正确性，当程序下载到评估板上执行后发现LED不会闪烁，可能的原因有

■　LED电源供给有问题（硬件问题）。

■　连接LED子板后，导致评估板无法运行（比较象是硬件问题，通常是电源问题、接错PIN或焊接技术不佳）。

■　LED子板和CPU连接有问题，例如，软件是用Port I/O#1来控制LED开关，但是LED子板是接到CPU的Port I/O #2（软件或硬件问题）。

■　LED坏掉了（硬件的问题）。

■　程序在还没执行到点亮LED的程序段就死机了（Boot-Loader的问题？）。

■　Timer的Timeout中断没产生（这就是驱动程序的问题了）。

许多原因可能导致同一现象，可能是软件问题，也可能是硬件问题，当然这个例子是比较简单啦！如果是硬件电路或控制方式较为复杂的设备，如LCD或红外线设备等，此时，软、硬件工程师应该坐下来开会，一起把所有可能的问题列出清单，然后逐一检查。假使可以的话，最好双方一起工作一段时间，直到解决问题为止。有些问题不是硬件工程师一个人可以解决的，举例来说，硬件工程师可能比较不熟悉驱动程序或者测试程序做了什么事，你要他量信号也不知从何量起，此时就要有固件工程师的协助，甚至边Trace Code边量信号才有办法找出问题所在。”

09-03　控制CPU

菜鸟：“我已经知道在开始写驱动程序前的准备工作了，现在您可以分个外围设备让我来试看看了吧！”

固件老鸟：“一般人对驱动程序的印象都是要驱动或控制外围设备，却往往忽略了整个系统中最重要的一颗IC-CPU。在PC上，BIOS和操作系统已经把CPU控制得很好，所以写驱动程序时几乎不用理会它，如果说Intel Pentium 4是目前最复杂的CPU其实并不为过，但它可不是给电后就会自己动了，我想BIOS及Windows、Linux还是默默做了很多事情才能让它执行的。

用于嵌入式系统中的CPU通常没有Pentium 4那么复杂,但CPU内整合许多外围设备的设计已蔚为风尚。采用这种CPU最大的优点，是硬件架构相对简单，甚至有可能系统中只要有CPU这颗主要IC,就足以实现产品的所有功能。若再深入地探讨下去，这种CPU的价格一定比使用多颗不同IC便宜，而且可以使产品体积变小，产品外观设计可以有更高的弹性。此外，研发团队不用面对太多的IC厂商，取得支持相对容易，甚至有些IC设计的厂商提供专用CPU订制的服务（称为ASIC）,可根据产品设计者的需求，整合不同的设备（我们称为Hardware IP）到CPU中。

图9-3所示是某CPU内部的架构，你可以看到里面有很多不同的设备，像LCD、NAND Flash、SDRAM、USB Controller等。你不妨可以思考一下，使用这颗CPU,在不额外加外部IC的前提下，是不是就足以设计诸如随身碟的产品了？如果再加一颗MP3译码IC,是不是就可以设计MP3随身听了？”

▲ 图9-3　用于嵌入式系统之CPU的内部架构

菜鸟：“虽然我对硬件设计一窍不通，但我可以想象这样的CPU,应该可以大幅度减少硬件设计的复杂度。话说回来，所以在嵌入式系统中最重要的驱动程序就是控制CPU啰？”

固件老鸟：“没错！CPU就是驱动程序工程师首要处理、也是最重要的设备。以下我们就来一一检验查看一般嵌入式系统CPU所需要设定的功能。”

9-3-1　内部寄存器

固件老鸟：“首先是内部寄存器，我们先前已经看过很多案例了，虽说CPU内整合了很多外围，但其中还是有一个称为CPU core（核心）的区块。举例来说，同一系列的两颗CPU都采用8051为核心，虽然整合了不同的外围，但是他们CPU的内部寄存器和所接受的机器码是相同的。”

菜鸟：“这个我了解，如同我们目前正在开发的产品所使用的CPU，就和前一代产品的CPU是相同的核心，只是整合进CPU的外围不同。所以虽然是新的CPU，但汇编语言写法是一样的，编译工具也是一样的，所以团队中的老鸟很快就能上手，这也是当初我们在评估这个项目能不能接的因素之一。”

固件老鸟：“是的，很多之前项目留下来的程序，包含Boot-Loader、驱动程序都可以重复使用，因为写汇编语言的机会不多，实际上，我们并不会去弄懂每一个寄存器的用途，而且现在流行的是RISC（精简指令集CPU），这种CPU的特定用途寄存器（如Stack Point、Processor Status Register、Program Counte）不多，大部分都是一般用途寄存器，通常就直接命名为R0、R1、R2等，图9-4所示是某CPU的寄存器列表。

▲ 图9-4　用于嵌入式系统的CPU寄存器列表

菜鸟：“数一数总共也不超过20个吧！我还以为CPU内会有很多寄存器呢！只要这些寄存器，CPU就能完成如此复杂的功能吗？”

固件老鸟：“我相信你了解现代计算机架构的原型是图灵机（Turing Machine，又称杜林机）吧！它的架构很简单，但已被证明可以完成任何复杂的任务。CPU只是一直到存储器中取出下一个指令出来执行的设备，而寄存器是用来记录目前CPU的状态及操作存储器时的暂存区，有兴趣的话，可以把用C语言写的复杂程序转成汇编语言，你会发现里面就只是用到这些一般寄存器而已。

刚刚谈到我们写汇编语言的机会不多，通常只有在设定CPU的状态寄存器时才会用到，一般我们又把这种状态寄存器分为两类。

■　可以直接设定值的状态寄存器，如程序代码9-9所示。例如，我们在系统启动后，要马上设定STACK存储器的地址，否则函数调用以及中断处理程序都会运行不正常。此时，我们会写如下的汇编语言去设定CPU的Stack Point（SP）寄存器。此外，我们可以从之前提过的CPU状态寄存器（PSR），从中知道CPU执行了某个指令之后的状态，也可以设定其中某个bit的值来控制CPU的行为，最常用的就是设定IE（Interrupt Enable）这个bit，可允许或禁止中断发生。

程序代码9-9

■　不允许赋值的寄存器。如PC（Program Counter）寄存器，存储CPU接下来要执行指令的地址，当CPU自存储器获取了一个指令来执行时，PC寄存器会自动累加，程序不允许直接设定PC寄存器的值，只能通过jump、call等指令来改变执行顺序时，才会使得PC值跟着改变。

菜鸟：“再加上我们可以从CPU厂商取得sample code，里面一定会有操作这些状态寄存器的程序代码，直接copy过来修改即可。这样说来，即便更换了新的CPU，也不需花太多时间搞懂这些内部寄存器啰？”

固件老鸟：“是的，基本上只要把data sheet的相关部分浏览一下，知道这颗CPU有哪些寄存器，清楚个别功能就够了。”

9-3-2　Memory Mapping 寄存器

固件老鸟：“前面提到过，用于嵌入式系统的CPU通常会整合许多外围设备到CPU内部，你知不知道内部设备和外部设备有何不同？”

菜鸟：“有什么不同？外部设备就是一个独立的IC，和CPU以特定的方式连接，中间可能还有一些组件与线路什么的，内部设备不就是把这些东西一股脑儿‘塞到’CPU中吗？”

固件老鸟：“你的用词还真口语化。先不管硬件原理的细节，就写驱动程序的人来说，要控制接在CPU外部的芯片势必是通过CPU的PIN脚，可能只是简单的将GPIO脚（General Purpose I/O）设为High或Low来控制外部的chip。同理，外部的设备也可以通过GPIO脚将信号传回给CPU，或者驱动程序可以通过某种通信协议来控制I/C，如SPI或I2C或等[8]。但无论是什么样的通信协议，CPU和外部chip间数据与命令的传递，还是要通过CPU的PIN脚。

假使这个chip是整合到CPU内部，姑且不管它与CPU的core是怎么连接的，驱动程序肯定不是通过操作CPU的PIN脚来控制CPU内部的chip。CPU设计者为了让整合进来的chip确实像是CPU的一部分，会把该chip的特性包装起来，所以驱动程序工程师看到的就是一系列的寄存器，通过设定这些寄存器的值，等于通知CPU Core去操作整合进来的chip，而该chip要传递信息给驱动程序时，也是通过这些寄存器。

这种用来控制CPU内部chip的寄存器，与刚刚提到的CPU内部寄存器是完全不同的性质，CPU内部寄存器有自己的名字，而内部外围设备的寄存器只有一个地址，所以通常我们称之为Memory Mapping Register，而这种控制内置chip的方式称为memory mapping I/O。”

菜鸟：“这个我知道，要操作memory mapping I/O寄存器的程序写法如下。

寄存器编号其实是一个地址，使用指针就可以对其赋值或取值，要注意的是一定要用volatile关键字，否则编译器的优化功能，很可能会把某些程序当成是没意义的，我翻过CPU的data sheet，类似这样的寄存器有上百个耶！”

固件老鸟：“因为这颗CPU整合了许多硬件设备（Hardware IP），其中某些复杂的硬件设备功能较多，自然需要较多的寄存器来做设定与操作。图9-5所示的表格就是某颗CPU内置的LCD controller的寄存器列表，即驱动程序工程师要让LCD亮起来，可能要把相关寄存器都设定过。这个设备还不是最复杂的，你想想，如果某颗CPU整合了 Ethernet IC、MP3译码器等设备，用来控制这些设备的寄存器会有多少？”

▲ 图9-5　内置之LCD Controller的寄存器列表

固件老鸟：“CPU内部的Memory Mapping Register除了用来控制内置chip（严格来说，应该称为‘内部Hardware IP’）之外，也可用以操作CPU Core的基本功能与控制CPU的PIN脚。举例来说，要让某根GPIO脚变成High或Low电位，工程师必须控制如图9-6所示的寄存器。”

▲ 图9-6　可控制CPU PIN脚的寄存器列表

菜鸟：“虽然寄存器的数目很多，但您也谈过，这些寄存器都只是‘规定’，并非‘学问’或‘知识’，要用时先读一下data sheet，然后拿sample code来修改就可以了。”

固件老鸟：“没错！有些设备的初始化动作相当繁杂，疏忽了某个寄存器可能就动不起来，而这种‘疏忽’是因为不了解CPU设计者的规定，你只能一直翻data sheet,否则想破头也想不出来，偏偏有些data sheet总是语言不详，此时工程师就应该立即求援，参考确定可以动的sample code或要求CPU厂商的FAE来协助。”

菜鸟：“在那本业界奉为圣经的书——《嵌入式系统一使用C/C++（Programming Embedded System in C and C++）》中也提到了这种Memory Mapping Register，书中提到要将所有会用到的寄存器编号（地址）定义为较有意义的名字，写法如程序代码9-10所示。但是我们CPU的这种寄存器这么多，难道都要一一为其定名吗？”

程序代码9-10

固件老鸟：“基本上，教科书都是这样教的，好像在程序中出现‘数字’就是没有弹性的写法，这适用于大部分的状况。举例来说：假设我们的系统有LCD，用于产品A的分辨率是160×160,用于产品B的分辨率是160×240,那么我们在程序中就直接用160、240这些值来计算坐标。假设产品C的分辨率是80×80,如此一来，工程师就要到整个系统中找出与LCD宽和高有关的程序段，逐一修改并测试，不但浪费时间，而且很容易发生疏忽或错误。如果把LCD的宽与高分别定义在.h文件中（如程序代码9-11所示），则产品A、B或C的程序部分都是一样的。”

程序代码9-11

固件老鸟：“此外，在驱动程序的设计中也可以用到。例如，操作NAND Flash时，程序要按照NAND Flash chip的需求，依次控制CPU某些与NAND Flash连接的PIN脚，因为这些动作相当频繁，程序中一堆‘*（volatile unsigned char*）0×402d9|=0×20；’这样的东西，即便写了注解，还是感觉程序有点杂乱，此时在.h文件中加入如“程序代码9-12所示”的定义，程序中就可以直接用‘SET_ALE_H；’这样的叙述，程序可读性会比较好。”

程序代码9-12

菜鸟：“您谈到了‘定义常数’这种方法在应用程序及驱动程序上的应用，但就是不提CPU Memory Mapping Register编号的设定，是不是暗示寄存器的编号不适合这种做法命名呢？”

固件老鸟：“基本上，我是不会公开违背教科书的‘教诲’，但你想想看，我们CPU的寄存器有N百个，当我们在调整程序，要查某段程序代码使用到之寄存器的意义时，该怎么做比较快？应该就是翻data sheet吧！data sheet中一定会有按照寄存器编号顺序排列的表格，所以知道编号的话，翻纸本的data sheet往往会比搜寻电子文件还快。

如果你的程序中充满了诸如REG_LCD_ENABLE、REG_GPIO_P02_SEL、REG_USB_DMA_SETTING等，我才不相信有人可以记得这些名字所代表的寄存器编号，所以就要先到.h文件去搜寻，才能去查data sheet。如果程序是几行就算了，有时操作设备的一个动作要用到好几个寄存器，工程师好不容易看懂这个程序，结果火气也上来了。如果有人把上面那个例子定义如下，那简直是气死人了，好不容易找到SET_CLE_L定义的.h文件，结果居然看不到寄存器的编号，还要去查另一个常数的定义，我个人觉得这是在浪费时间。”

菜鸟：“我虽然没有实际经验，但也翻过CPU data sheet中那一堆寄存器的列表，的确没有人可以记得哪些编号的寄存器是做什么的。实作时，一定是一本data sheet放在旁边翻。但我们直接在程序中写寄存器编号是否会破坏程序的可读性？”

固件老鸟：“我觉得把寄存器编号定义成其他名字，乍看好像很有意义，殊不知这样才是破坏的程序的可读性与可维护性[9]。在程序中将常数定义为有意义的名字，是因为这些常数可能会改变。例如，不同的产品LCD的分辨率可能改变，或者我们可以用CPU的另一根PIN脚当作CLE来控制NAND Flash，但是寄存器的值在我们选定CPU后就定下来了，基本上，不会有改变的可能。即便教科书是这么教育我们的，但实际上我严格禁止我的固件工程师把CPU Memory Mapping Register重新命名，看到一个我改一个，看到一堆我就会把相关工程师叫来念一顿。

举例来说，我们看看LCD Controller相关寄存器的列表（如图9-5所示），你真的能从FRMRPT这样的寄存器名称联想到它的意义是‘Frame repeat for EL panel’吗？与其在程序中使用这些看似有意义的命名，不如直接在程序中直接用寄存器的地址（本例是0x0380200:D7），然后在程序后加上关于此寄存器意义的注解，这样一来就不用对每个寄存器做名称定义（例如#define FRMRPT 0x0380200），而且也不会降低驱动程序的可读性。”

菜鸟：“这就是理论与实战经验的鸿沟吧！”

绝大部分用于嵌入式系统的CPU，是采用Memory Mapping I/O的设计方式来控制CPU的所有功能，包含CPU内置的IC。但Memory Mapping I/O并非唯一的设计方式，另一种称为Port I/O,最著名的就是X86系列。实际上很少有嵌入式系统开发项目会选用Pentinum以上等级的X86 CPU，除了价格昂贵之外，CPU的体积、耗电等都是其致命伤，而且大部分的产品都用不着如此强大的计算能力，但X86家族中历史悠久的80186系列，如图9-7所示，目前还广泛的应用在电子产品中。

▲ 图9-7　著名的CPU厂商所提供的80186 CPU

简单地说，所谓的Port I/O也是一段连续的地址空间（但Memory的地址空间和Port I/O的地址空间是分开、独立且互不相干的），但CPU不能以地址或指针的方式对其进行存取，必须通过特殊的CPU指令，以X86系列CPU为例，提供了“in”与“out”指令，可从某个特定port进行输出或读值的动作。例如，PC上的RS232就是利用Port 0x3F8去读取数据。

对固件工程师而言，Port I/O比Memory Mapping I/O麻烦一些，必须用到汇编语言指令才可以操作CPU内部的Port I/O,但基本原理并不太复杂，只要遵循CPU手册的规定即可。目前Memory Mapping I/O的设计方式是嵌入式系统CPU的主流，本书会以讲述Memory Mapping I/O为主。

9-3-3　中断处理器

固件老鸟：“我们之前讲过中断矢量的概念，但你知道最早的CPU可能是没有中断控制器的，它必须外加一个chip[10]才能接受外部设备的中断要求。”

菜鸟：“记得在学校学汇编语言时，好像有谈到PC中有一颗中断处理器，叫做8259，教科书中还特别花了一个章节讲解它的运行方式，只是学期结束就忘光了。”

固件老鸟：“这年头还知道8259这东西的信息系本科生实在不多了，所幸目前用于嵌入式系统的CPU几乎都把中断控制器内含在CPU，驱动程序需要做的设定并不多。基本上，就是通过Memory Mapping Register以及之前提过的PSR寄存器（Processor Status Register）。驱动程序在控制中断系统时要注意的事项有

■　Boot之后应立刻禁止中断产生（应该都是设定PSR），因为除了中断系统还没设定好之外，可能产生中断的硬设备也还没初始化完毕。

■　内置中断处理器的CPU会规定哪些PIN脚是可以产生中断的，而哪些PIN脚会产生什么中断通常也是定义好的。例如，内置USB Controller的CPU，可能就会规定某根PIN脚用来接收USB设备的中断信号，所以在硬件设计时，一定要注意CPU与中断有关的相关规范。举例来说，如果键盘输入接到CPU不会产生中断的PIN脚，则程序只能用polling（查询）的方式，一直去察看有没有key被按下（该PIN脚的电位是否改变），这是很缺乏效率的设计。

■　固件工程师必须知道什么设备接到CPU的哪一根PIN脚，是否会引发中断，如果会的话，又是第几号中断，一定要弄清楚相关的硬件设计，否则怎么有办法写该设备的驱动程序？

■　设定中断矢量表的起始地址（通常都是通过寄存器来设定），中断矢量表可以写成一个C的数组（请参考《第5-2节：无痛起步：善用Sample Code》），数组内的元素就是各个ISR的地址。

■　我们的产品不见得会用到所有CPU支持的中断来源，也就是说，并非每一个中断矢量表的entry都有对应的ISR，但中断矢量表的每一个entry都必须有值，此时就必须为这些没有用到的中断来源写一个空的ISR（请参考《第5-2节：无痛起步：善用Sample Code》的dummy()）。当这个空的ISR被执行时，就表示有不该产生的中断产生了，这可能是硬件设计错误或静电产生的误动作，必须通知硬件人员处理。

■　驱动程序可以通过设定寄存器为每一个中断来源设定优先级（一般CPU都是8个等级），PSR寄存器中除了可以设定中断是否可以产生之外，也可以设定哪一个优先级以下的中断不会被CPU处理。

■　一般情况下，除非产品有hard real time的应用，否则我们不允许巢状中断（ISR在执行时允许发生中断，于是ISR可能被中断，CPU去执行另一个ISR），即ISR执行时期禁止中断产生，这是因为巢状中断会出现很复杂的状况。此时，可以通过设定CPU允许产生中断的优先级。例如，一般中断的优先级都是6,而不允许任何等待或丢失的中断事件则设为7，平常ISR执行时设定CPU仅允许优先级7以上的中断产生，则所有优先级6的中断不会产生巢状中断，但优先级7的中断在任何时候都保证会被处理。

■　有一个比较特殊的中断叫做NMI（Non-Maskable Interrupt；不可屏蔽式中断），顾名思义，就是无论如何都不会被禁止的中断，硬件设计时可以把hard real time的中断来源接到NMI上。

■　ISR也是一个函数，但有很多注意事项要遵守，尤其是critical section的保护，稍后我们会再提到。

此外，ISR不像一般程序是循序执行的，它会在任何程序在执行时被调用，可能发生的状况很多，所以我会要求对ISR进行更严格的测试。”

菜鸟：“像键盘、touch panel、Timer等需要中断通知系统有外部事件发生，这很容易想象，但我还是没什么概念，到底什么样的设备才需要产生中断？”

固件老鸟：“一言以蔽之，由程序主动控制的就不需要中断，例如，点亮LED、设定LCD、SDRAM Controller、关闭放大器的电源等；反之，可能任何时间点发生的硬件事件就必须用中断通知系统，假如系统一直在监视着外部设备的状态，不但效率不佳（在等待事件发生时无法做其他的事情）、耗电（系统无法进入最省电的模式），假使要监视的设备很多就更麻烦了。举几个你可能没听过的例子。”

■　DMA完成（直接存储器存取；Direct Memory Access; DMA），允许某些计算机内部的硬件子系统可以独立地存取系统存储器的读取或写入，而不需牵涉到CPU。

■　LCD Controller完成一次整个屏幕的更新。

■　AD convert转换完毕（Analog to Digital Convert）。

■　MP3译码IC—buffer内的MP3内容已经解完了，通知系统尽快把下一段数据送到buffer中。

■　序列传输或网络设备传输—收到的数据错误、传送buffer已经空了、接收buffer已经满了，都可能会产生中断。

9-3-4　Clock

菜鸟：“之前您提过，硬件工程师在把板子交给固件工程师之前，至少必须验证两件事情。首先是CPU与主要组件的电源是否正确，例如，CPU规定的工作电压是2.8V～3.3V，而另一个chip的工作电压可能只要1.5V，工作电压和可接受最大电流是让电子组件正常运行最基本的条件，硬件工程师自然会搞定它。

第二个一定要确认的就是输入CPU的Clock是否正确。电源比较容易了解，电子的东西总是要有电才能运行，但是Clock就比较难以想象，所谓的Clock到底是什么样的概念啊？”

固件老鸟：“所谓的Clock是维持CPU正常运行所需的基本时间单位，我试着简化这些电子组件运行时的复杂度。举例来说，当CPU要到ROM或其他存储器中读取一个Byte时，假设这个动作CPU要在两个时间单位内做完，第一个时间单位从Address Bus（地址总线）送出地址，第二个时间单位自Data Bus（数据总线）取回数据的值[11]，问题来了，如果CPU和ROM之间的时间单位长度不同，或者不同步会发生什么事情？”

菜鸟：“就好比我和您约好下午5点20分见面，逾时不候。但我的手表和您有1小时的时差，或者我的表每10分钟会快2分钟，所以我们可顺利碰面的机会微乎其微。”

固件老鸟：“你这个比喻还不赖，大概就是这个概念。系统中的CPU和存储器应该要有一个统一的时间基准，我们称之为时序。例如，我们说某颗Pentium 4 CPU是2GHz的，则表示这颗CPU运行的基本时间单位为1/（2×1024×1024）秒，这里所说的基本时间单位可以简称为一个Clock。如果CPU一个Clock可以执行一个指令[12]，那么一个Clock越短的CPU，在每1秒内就可以执行更多的指令，对使用者而言，这颗CPU的性能也就越好。”

菜鸟：“这就是所谓CPU‘超频’的原理吧！同一颗CPU使用不同的时序就会有运算速度不同的效果。”

固件老鸟：“没错！用于嵌入式系统之CPU的概念也是一样的，只是它们的时序通常没那么快，现在用于PDA或Smart Phone的ARM系列CPU时序已达500多MHz，这在一般电子产品的设计上是无法想象的。Clock的思想应该很容易了解，就像CPU的心跳，问题是这个稳定的‘心跳’从何而来？CPU与记体体之间的‘心跳’要如何同步？在继续往下说明之前，你先看看如图9-8所示的时序图，说明CPU如何从SRAM中读取一个Byte。”

▲ 图9-8　CPU与外部SRAM互动的时序图

菜鸟：“哇！我从以前到现在最怕的就是看这种图，实在不知道它要表达什么？”

固件老鸟：“这就和硬件工程师看到程序就莫名奇妙的害怕一样，如果你不觉得写程序有什么难的话，那么真的没有理由看不懂这张图。所谓的时序图就是在某个时间点chip上各个PIN脚上信号的关系图，这种图一定要有基本的时间参考。这样，工程师才知道哪根PIN脚在什么时间点应该是什么状态才是对的。以图9-8所示来说，你先不要管图里的细节，只要看每根PIN脚高高低低的变化关系即可。当CPU要到ROM里面读一个Byte出来时，CPU各个PIN脚随着一个一个Clock的推移（即图中的BCLK），必须符合图中的状态才可能运行正确。如果程序到ROM中始终读不到正确的数据。那么，我们就可以当程序在运行时，量一下图中各个PIN脚的信号是否和时序图中的一样，应该很容易就可以找出问题所在。

顺带一提，固件工程师必须要能看懂data sheet中的时序图，不是为了抢硬件工程师的饭碗,而是在对驱动程序调试时一定会用到，以后我们再来看实例。”

菜鸟：“如果只专注在每一个PIN脚的高低变化，这张时序图确实没那么复杂。话说回来,您还没解答Clock的来源与设备之间的时序如何保持同步这两个问题。”

固件老鸟：“上次我们在为Boot-Loader无法执行而伤脑筋时，CPU厂商的FAE一来就现用电表量输入CPU的电压与耗电流，接着他量了离CPU不远的振荡器IC，示波器屏幕出现了频率为48MHz的漂亮方波（如图9-9所示）。”

▲ 图9-9　振荡器所产生之固定频率的Clock

固件老鸟：“你还记得吗？他确认了这两件事后才开始找程序不能执行的问题，因为电源与Clock是CPU要运行的基本条件。如果电源有问题或者没有稳定的Clock输入CPU,那么，CPU肯定是不会动的，这些基本的检验虽然简单，但我就常看到有工程师量了半天信号，却完全没想到可能是输入CPU的电压有问题，或者Clock的频率不对。

那个离CPU最近的振荡器IC就是CPU时序的来源（如图9-10所示），这种chip（Crystal或Oscill ators）可以稳定的产生固定频率的方波，而CPU会有PIN脚接受外部的时序输入。

时序进入CPU内部后，CPU可将其升频或降频，并分出几个频率不同的分支，分别供给CPU Core与其他的内置设备。例如，输入CPU的频率是48MHz, CPU可将其放大成两倍，也就是96MHz，供给CPU核心的执行时序；或降频为12MHz，供给时序需求比较慢的LCD控制器等。

▲ 图9-10　CPU与外部振荡器（1）

接下来，说到外部设备的时序同步。为了保持振荡器的精准度，在PCB layout时，不会把振荡器与需要时序输入的芯片离太远，而且由同一个振荡器供应一个以上芯片所需的时序也不是个好主意，因为每个芯片需要的工作频率可能大不相同。如果把所有需要Clock的芯片都配置一个振荡器更不可行，因为振荡器和各个组件的距离并不相同，一定会有时序不同步的问题，最好的方法就是由CPU统一供给，振荡器输入CPU的时序频率可以升频或降频为各个芯片工作所需要的频率，再通过CPU既定的输出PIN脚连接到需要时序输入的芯片。如此一来，不但不需要额外不同频率的振荡器，而且由CPU产生的Clock肯定不会有不同步的问题。

然而，有些外部芯片的时序是不需要与CPU同步的。例如，系统可能同时存在两个CPU，或拥有另一个功能独立的DSP，这种状况下没有一定的硬件设计，视其规格而定。一种做法是将另一个CPU或DSP与其外围电路当作另一台机器，主、副CPU间通过诸如序列、并行传输或I2C、I2S等通信协议沟通，此时，副CPU就会需要自己的时序并搭配另一个振荡器，如图9-11所示。”

▲ 图9-11　CPU与外部芯片各有自己的振荡器（2）

菜鸟：“要帮CPU以及其他外围提供时序的方法很容易，就是使用一个振荡器IC产生固定频率的方波给CPU即可，但系统中所有设备协同运行的原理似乎还蛮复杂和多样的，我需要回去再看一下刚刚的笔记，好好思考一下。

此外，我们的系统中似乎有两个速度差很多的振荡器，一个是48 MHz，另一个是32768 Hz，分别输入CPU不同的PIN脚,为什么CPU会需要两个时序输入？而且这样的设计似乎还是CPU的data sheet中所建议的。”

固件老鸟：“data sheet中有提到另一个慢速的振荡器，是什么情况要用的啊！你去把相关章节看一下就懂了。”

菜鸟：“我有很认真地‘翻过’CPU的data sheet，我想，一般工程师都有和我一样的困扰，那么厚一大本书，又全是英文，总不可能全部看完吧！CPU内有许多Memory Mapping Register是用来设定CPU Clock的。例如，工程师可以设定CPU内部各个设备，包含CPU Core，真正的工作时序，基本上就是由外部输入的时序作升频或降频。此外，工程师也可以设定CPU要用哪一个振荡器的输入，当作真正运行时的时序来源等。”

固件老鸟：“所以，你已了解需要两个频率输入的原因是什么了吗？”

菜鸟：“CPU若使用较慢的振荡器当作运行时的时序来源，基本上它就是处于一个慢速的状态，而且这个Clock输入的频率（32768 Hz）慢得有点离谱，估计系统不会希望用这样的速度执行什么重要的任务。所以，当系统程序切换CPU的时序输入为慢速振荡器时，其目的应该是当系统待机时，可以让CPU进入较省电的状态吗？”

固件老鸟：“答对了！”

9-3-5　Bus & Chip Select

固件老鸟：“Clock与Bus是一般人比较不清楚的东西，但却是CPU运行里相当重要的思想。前面我们谈了Clock，你知道所谓的Bus[13]是什么吗？，”

菜鸟：“我参加过一次硬件电路review会议，大概知道Bus的用途。所谓的Bus应该包含Address Bus与Data Bus两种，CPU以及可用地址操作的芯片（以存储器居多）都会有类似称为A0、A1……A15……或D0、D1……D15……的PIN脚，前者就是Address Bus，后者则是Data Bus。CPU与所有的外部存储器主要就是用这些PIN脚串接起来，图9-12所示的原理图可能比较清楚。”

▲ 图9-12　CPU与外部存储器的串接方式

菜鸟：“刚刚我们看的那张时序图里就包含Data Bus与Address Bus的状态变化。首先，CPU通过Address Bus送出要操作的地址，如果是写入存储器的动作，CPU接着会送出要写入的数据到Data Bus上，存储器可从Bus上分别取得地址与要写入的值，并执行CPU要求的动作。反之，存储器从Address Bus上取得地址后，会把地址上的值放到Data Bus上，CPU则会自Data Bus上把值读入，从而完成从存储器上取值的操作。简单地说，CPU必须通过Data Bus、Address Bus与存储器连接，才能用指定地址的方式来存取存储器。”

固件老鸟：“基本上没错。我问你一个问题。假设系统中有好几颗外部存储器，都串在Data与Address Bus上，当CPU要到某个地址取值时，它会先把Address Bus的所有PIN脚设为该地址，此时这些外部的存储器怎么知道是谁该把数据放上Data Bus，如果bus上的所有存储器都把数据放上Data Bus，岂不天下大乱？”

菜鸟：“存储器芯片根据Address Bus上的地址，判断CPU要操作的不是自己吗？”

固件老鸟：“你想想，存储器芯片在设计时怎么会知道要和哪种CPU配合？即便CPU已选定，不同应用的硬件设计也可能将这个存储器芯片定义在不同的地址上，所以存储器芯片本身怎么可能事先知道自己的地址范围是什么呢？

所以CPU要使用某个芯片之前，通常要明确的指定或‘告知’。所谓‘告知’的方法就是改变存储器芯片上CS PIN（Chip Select）的状态，一般都是由High拉到Low，当芯片的CS PIN状态被改变时，即表示通知其准备开始工作。当CPU不再使用该芯片时，就把该存储器芯片的CS PIN状态设为High。这就是为什么即使Bus上串了许多存储器芯片，但同时只有一个芯片会动作的原因。”

菜鸟：“所以CPU和存储器芯片除了用Address Bus与Data Bus连接之外，还需要额外一根CS PIN啰？每次操作存储器的前后都要控制CS PIN，这应该不是程序要做的吧，程序中使用指针时，并不需要额外的处理啊？”

固件老鸟：“是啊！在系统有多个存储器芯片的状况下，CPU与所有存储器芯片当然是通过同一组Address与Data Bus串接，但是CPU会用不同的PIN脚去控制不同存储器芯片的CS PIN。例如，用名为CS#1的PIN脚控制SRAM,用CS #2控制ROM。

此外，当然是由CPU负责控制存储器芯片的CS PIN，否则，编译器怎么可能预先知道产品的硬件设计会选用哪一根PIN脚来控制某个存储器，怎么有办法为诸如‘操作变量’的C语言程序代码产生汇编语言代码？你可以想象CPU内有一个对应关系的表格（图9-13所示的表格是某CPU的实例），CPU会根据要操作的地址，换算接在这个范围内的存储器芯片，是用哪个PIN脚控制其CS PIN。在送出地址之前或同时，CPU就会把该PIN脚拉Low（设为低电位）。图9-13中，一个area对应一段地址空间，你可以想象每个Area对应一个CPU的chip select PIN，一个chip select PIN可以接一颗存储器IC，所以，当你的程序要存取某个地址时，CPU会根据地址判断是哪个Area，再把相应的chip select PIN拉Low，就可以存取到正确的存储器IC了。

换句话说，因为CPU内部有这样的对应规则，所以只要参考硬件设计，就可以由CPU使用哪根PIN脚连接某颗存储器芯片的CS PIN，得知该存储器芯片在系统中的地址范围。有了这个地址范围，固件工程师就知道在程序或Link Script中该用什么地址来操作这个存储器芯片。也就是说，系统中某个存储器芯片会对应到哪个地址范围，视CPU是用哪根PIN脚连接其Chip-Select PIN，这些信息都会定义在CPU的data sheet里面。

通常我们把这个地址范围称为一个Area，每一个Area对应一个地址范围以及一根CPU PIN脚（一般也称其为CS PIN）。因为系统中的每一个存储器芯片会有不同的特性，例如，存取速度、基本的数据宽度（8bit、16bit或32bit）等，所以CPU在操作不同存储器芯片时应该会有不同的行为模式。如ROM和CPU比起来算是较为慢速的设备，假设CPU要到ROM读一笔数据，当CPU将地址通过Address Bus送出后，可能要多等待几个Clock，该存储器才来得及将数据放上Data Bus。其中等待的Clock数就称为waiting cycle，这也就是上面所说得行为模式之一。CPU会提供许多寄存器让程序先设定各个Area的特性，通常可以设定的特性如图9-14所示，而这些特性当然要在实际操作存储器前设定完毕。

▲ 图9-13　地址范围与Chip Select PIN的对应关系

▲ 图9-14　用以设定“CPU操作外部存储器的行为模式”的寄存器实例

理论上，CPU操作某存储器要插入几个waiting cycle是可以计算的，CPU的data sheet内都会有此信息，通常是一个公式，输入CPU的时序（如24 MHz）以及存储器芯片的执行速度（如90 ns），就可以得出理论的waiting cycle数。实际上，我们开始会把这个值设大一点，即CPU存取该存储器的速度会慢一点，等系统可以稳定的执行之后，再来慢慢调整系统各部门的timing。

最后，值得注意的是，并非串在Bus上的存储器芯片才有Chip-Select PIN，许多非存储器的芯片在控制之前都必须改变其Chip-Select PIN的状态。”

菜鸟：“第一次接触这个CPU是通过CPU厂商提供的评估板，它的使用手册上就明确指定了各个存储器的地址范围。现在才知道，原来系统中各个存储器的地址范围是这样推演来的。这个东西不太容易了解，我得回去再复习一下刚刚的内容，但话说回来，一般工程师应该可以不知道这些原理吧？”

固件老鸟：“是的，一般软件工程师甚至系统中哪一个存储器是哪个地址范围都不需要知道，因为link script会描述所有在连接时所需要的信息，但是负责驱动CPU的固件工程师就不能不清楚这个系统架构了，每一个存储器芯片都有不同的特性，必须对CPU做不同的设定，存储器操作不稳定，系统绝对也不会稳定。”

9-3-6　GPIO Port

固件老鸟：“CPU中比较难懂的原理已经讲完了，其他的就是很简单的内部设备控制了。CPU是整个计算机系统的大脑，但它总是要控制外部设备，最简单的像是点亮LED、发出一段beep（哔～～）的声音，或是将计算结果输出到LCD上。除了输出，CPU也可能要接受并处理各种形式的输入，如按键、温度变化、网络来的封包等。当然有的CPU就是为了处理特殊的输出或输入而设计，但基本上大部分的CPU在设计时，不可能知道客户会拿它来设计什么样的产品，所以一定会设计许多用来做通用输出/输入的PIN脚，我们称为GPIO Port（Gen eral Purpose I/0）。”

菜鸟：“这个我知道，我之前就是通过控制某根Output PIN的High或Low电位去控制CPU外部的LED。我想输入也是一样的道理，外部设备与CPU某根Input PIN连接，外部设备通过控制该脚位的High/Low状态，CPU内的程序就可以自这根Input PIN中得知外部设备的状态变化。”

固件老鸟：“假使CPU是大脑，那么，这些通用GPIO Port就如同神经线，用以与外部的设备沟通。如你所说，沟通的方式都是数字，就是把连接CPU与设备的PIN脚电位设为High或Low。由CPU输出到设备比较简单，只要在合适的时间设定Output PIN的电位即可。例如，有些非存储器的IC在操作前也需要先控制其CS（Chip Select）PIN,硬件设计时，就要用到一根CPU的Output PIN连接CPU与该IC的CS PIN。

CPU的Input PIN可分为会引发中断与不会引发中断两种。硬件中断的基本原理之前已陆陆续续提到，视外部设备的特性来选择要使用哪种Input PIN，因为中断会打断目前正在执行的程序，也可以说ISR是优先级最高的程序，这样的输入很适合不定时发生的事件，例如，使用者压下按键、网络送来一个封包、DMA完成、负责传输数据的buffer空了（通知CPU尽快送数据来）或者满了（通知CPU尽快把数据取走）等。当外部设备通过CPU的Input PIN产生中断，CPU得知有‘紧急’事件发生后，就可以通过其他的I/0 PIN与该设备沟通，如自外部设备中接收数据，此时使用的Input PIN就无需具备中断的功能。”

菜鸟：“不会产生中断的Input PIN比较简单，程序只要通过CPU的寄存器读出该PIN脚目前的状态是高电位还是低电位即可。如果是可以产生中断的Input PIN，就要查出该PIN脚对应的中断矢量表里的第几个中断，然后要为其编写一个ISR。”

固件老鸟：“除此之外，CPU也会提供寄存器，让固件工程师可以设定每一个中断的属性，中断的属性分为两大类。

■　高电位变为低电位时产生中断，或者低电位变成高电位时产生中断。

■　Edge trigger或Level trigger。

这些属性精确地决定中断会产生的时间点，只要看如图9-15所示就可以理解。”

▲ 图9-15　触发中断的时间点

固件老鸟：“一根GPIO PIN就只有High、Low两种状态，只能做开关式的控制。多根PIN脚再搭配时间，就可以组合出各种复杂的通信协议。例如，之前提过的NAND Flash控制，我们的驱动程序就是通过控制CPU的多根Output PIN（图9-16所示名为CE[14]、ALE、CLE、I/0x等）来对NAND Flash下命令。请注意，这个命令是由一段时间内多根PIN脚的电位变化所组成的，当NAND Flash收到命令后，它可以把数据通过改变多根CPU Input PIN的电位来传回给CPU。如果知道这个原理，你知道NAND Flash的驱动程序要怎么写了吗？”

▲ 图9-16　多根PIN脚就可以组合出复杂的控制方式

菜鸟：“简单来说，就是按照NAND Flash data sheet里的时序图（如图9-16所示），先依序改变各个Output PIN的电位并对NAND Flash下指令，然后在时序图规定的时间内逐一读取Input PIN的电位，就可以取得NAND Flash的返回值。”

固件老鸟：“是的！如果驱动程序只是要让设备正常的动作，其实并没有想象中的复杂吧！我们再举一个例子，如果CPU的GPIO PIN不够的话，可以选用象是I2C这种序列式通信协议接口[15]的芯片，只要两根I/0脚（如图9-17所示的SDA与SDL），一根产生固定频率的方波当作Clock，另一根负责序列式数据交换，如图9-18所示。系统中每一个I2C接口的设备都有自己独有的ID编号，通过这个机制，系统中所有I2C接口的设备可以串在一起，CPU送出包含ID的封包才可以控制到正确的设备。表面上这种方式看起来很复杂，但说到底也不过就是I/0 PIN的控制罢了。”

▲ 图9-17　序列式通信协议－I2C架构图

▲ 图9-18　序列式通信协议－I2C时序图

菜鸟：“您说得很简单，但实际上没那么容易吧！否则你们就不用每天都忙得焦头烂额了。”

固件老鸟：“总得先知道原理嘛，如果原理都不知道要如何开始实作？其实最麻烦的地方应该是要先弄清楚要控制的设备规定，假使是特殊通信协议接口，就必须先搞清楚这个通信协议的细节。这些都是人家的规定，你不照着做，它就‘坐如钟’给你看。另一个麻烦就是Timing的控制，刚刚说过，CPU与设备间的命令或数据的传递是通过I/0 PIN电位变化与时间的组合，如果Timing不对，设备肯定也纹丝不动。”

菜鸟：“所以写驱动程序的人肯定离不开示波器啰？”

固件老鸟：“没错！一般我们不会用到太复杂的功能，示波器可以测得某些PIN脚在某段时间内的状态变化，图9-19所示是一个例子，和我们刚刚看的时序图是不是很像？如果datasheet内说某PIN脚的控制要按照某个时序图，该设备才能正常运行，但我的驱动程序就是不work怎么办？没有示波器的话，我只能对着data sheet和程序发愁；反之，我只要量一下信号，大概就可以知道问题出在哪里。”

▲ 图9-19　示波器可以量出在某个时间点，系统中多个PIN脚间的电位关系

固件老鸟：“最后，还有一个使用GPI0该注意的事项。以前应该稍微提到了，用于嵌入式系统的CPU为了节省PIN脚，通常某些PIN脚会具有一个以上的功能（当然，同时只能有一个功能），例如，某根PIN脚可以有GPI0与AD Port（Analog to Digital）的功能，假设系统电压是3.3V，当它被设定为AD Port时，输入的信号可以是0V～3.3V中间任意一个等级的电压。当它被当成GPI0时，输入的信号必须是数字的，不是High（3.3V）就是Low (0)。

所以，在使用这种PIN脚之前，一定要记得把它设定成正确的用途（通过CPU的Memory Mapping Register），否则结果一定不会正确。例如某根GPIO PIN，硬件设计的原意是要从设备输入信号，如果驱动程序把它设定为Output PIN，那么，CPU就无法保证可以读到正确的值。”

菜鸟：“我懂了，这就是您为什么要求硬件设计人员一定要做出‘CPU所有PIN脚的配置与用途’表格的原因了。”

固件老鸟：“从原理图可以看出PIN脚间的连接，但不容易看出用途。软硬件工程人员思维模式还是有点不同，如果固件工程师误解了硬件设计的原意，就算把程序看到烂掉也不会知道问题所在。话说回来，固件工程师和硬件工程师协同作战真的很重要！”

9-3-7　NOP与实作Busy Waiting的时间区段

菜鸟：“我有个问题。刚刚我们看了一些时序图，控制信号彼此之间都有时间的关系，我想知道我们的程序是如何精准地控制时间？我知道有些操作要求的Timing非常严格，举例来说，假设CPU通过两根PIN脚与某个IC连接，一根是CS（Chip Select），另一根是输入PIN—P#1，data sheet里说明如何从IC取得数据的时序图如图9-20所示。其中规定，当CPU把CS由High被拉到Low时，该IC会开始动作，经过T1的时间后，IC会把数据放在P#1上（这里所谓的‘数据’不是High就是Low，或者说不是0就是1），维持的时间为T2。在这种状况下，程序如何在正确的时间里到P#1上获取正确的数据？”

▲ 图9-20　时序图范例

固件老鸟：“刚刚已经讲解过，要把某根输出PIN做High/Low变化，以及要从某根输入PIN读出状态都可以通过CPU的寄存器，基本的控制应该没问题吧！这个例子还算简单，从时序图中，我们可以看出程序该做的事情有

Step01：把CS PIN设定为Low。

Step02：等待T1的时间。

Step03：立即从P#1读出状态。

通常执行Step03的时间不会大于T2，所以上述的步骤中省略了T2。假设T1 = 10ms，先不要管操作CPU PIN脚的细节，我们可以将以上的步骤写成如程序代码9-13所示的程序。

程序代码9-13

菜鸟：“这个程序很简单，但我的问题是drv_wait_1ms（）要如何implement？难道是用Timer吗？”

固件老鸟：“用Timer也是一个方法，但设定Timer、执行Timer, ISR都需要时间执行吧！假设我们要等待的这个T1很短，使用Timer会造成一定的误差。而且要驱动Timer也需要一个driver，在开发时期尽量不需要把一件工作加入太多的变量。我们在开发驱动程序时，最常用，也是最简单的方法，就是写一个什么事情都不做的循环。例如程序代码9-14所示。

程序代码9-14

菜鸟：“这个程序就是让CPU执行指定次数的无意义指令，而执行这些指令所需要的时间刚好就是我们想要等待的时间。这个程序很简单，但我有两个问题，第一个，我们如何得到‘COUNTER_PER_IMS’的值？第二个，‘nop’这个CPU的指令代表什么意思？”

固件老鸟：“先回答你第二个问题。‘nop’是no operation的意思，CPU执行这行指令时什么都不做，就是很单纯的耗掉一个或多个Clock的时间。应该是所有的CPU都会提供这样的指令，程序可以用这样的指令让CPU空转一段时间，且不会影响任何存储器或一般寄存器的值[16]。在回答你第一个问题之前，先看一下图9-21所示，每一个CPU都会提供这样的表格，它说明了CPU执行不同的指令需要多少个Clock。你会算每一个Clock的时间是多长吗？”

▲ 图9-21　CPU各种指令需要不同Clock的执行时间

菜鸟：“您之前教过我啊！如果CPU运行的频率是8 Mhz（这个频率和振荡器输入的频率不一定相同，CPU内部可以升频或降频），也就是说，一秒钟有8 M的Clock，所以一个Clock的时间就是1秒除以（8 × 1024 × 1024），等于7.6 micro-second（7.6 × 10-6秒）。”

固件老鸟：“理论上CPU执行指令的时间是很精确的，如果你把上面那个循环的程序转为汇编语言，就可以知道这个循环执行了哪些指令，每一个指令需要多少个Clock，也可以算出这个循环要转几次才会用掉你想要的时间。”

菜鸟：“大概知道原理了，您可不可以举个实例给我听听？”

固件老鸟：“OK！假设那个循环对应的CPU指令如程序代码9-15所示。”

程序代码9-15

菜鸟：“一次loop会用掉6个Clock的时间，所以我们可以很容易算出R0的初值等于多少时，执行这个循环的时间刚好是lms。”

固件老鸟：“没错，就是这么简单。这个function的误差不大（可使用示波器测量进出此function的确切时间，若有必要，可微调程序中的counter），以后在开发其他驱动程序，甚至系统程序与应用程序时都可以使用。”

9-3-8　Power Management & Low Power

菜鸟：“在驱动程序的工作列表中，除了Power Manager，其他的项目名称都很明确，就是某个设备的驱动程序，例如Boot-Loader、SDRAM Driver、LCD Driver、USB Driver等。所谓Power Manager应该就是所谓的电源管理吧！系统中有这么多大大小小的电子组件，每一个电子组件都要用电，电源管理该从何做起？我感觉这应该是硬件设计人员负责的事情，不是吗？为什么你要在电源管理这个项目上预先配置了如此长的开发时间？”

固件老鸟：“有些电子产品的确是不需要太严谨的电源管理功能，如家电或车用电子，你会真的去测量PC、微波炉或洗衣机每个月吃掉多少电吗？电源管理程序能省下的电和整体耗电来说简直微不足道。但如果换成产品吃的是电池的电源，如手机、电子字典、电子书、玩具等，要使用者忍受每天充电、没几天就要换电池，或者在外面用到一半突然没电等状况，这样的产品就算功能再强大，也不会是成功或热销的产品吧！

当然，硬件设计和系统耗电有绝对的关系，但当系统闲置时，是否需要关闭外部设备的电源、调整PIN脚的状态、让CPU降频或切换CPU的模式等则是软件的工作。我们在此先不讨论硬件如何设计会比较省电，这是硬件工程师的工作。我们的重点是：系统在不同的执行状态应该要对应不同的耗电状态。简单地说，在不同状态下，系统要关掉用不到的设备，不能关掉的设备就要让其尽量处于最省电的状态。以手机为例，大部分的时间都是处于开机但没被使用的状态，假使系统不做电源管理，CPU、LCD模块、信号（音源）放大器等都是很耗电的设备，你认为你手机的电池能撑多久？”

菜鸟：“所以在设计电源管理程序时，必须先区分出系统应具有哪些执行状态，还是以手机为例，我认为至少应该包含以下几种用电量的等级（由最耗电的等级往下排）。

■　通话或上网

■　拍照、玩游戏、播放MP3或动画

■　使用手机其他的功能

■　待机（背光关闭、屏幕亮着）

■　待机（屏幕关闭）

■　关机

耗电的状况，几乎系统中所有的设备都全速动作，而在关机的状况下，应该是把整个系统的电源都切掉了吧！”

固件老鸟：“根据产品特性不同可以区分出不同的耗电等级，一般来说，电源管理程序会把执行状态至少分为以下3种等级。

■　Full Run Mode——系统中所有设备全速执行。

■　Idle Mode——待机状态，系统在等待下一个命令，目前没事可做。以手机的例子来说，就是屏幕还亮着的待机状态。

■　Sleep Mode——睡眠状态，使用者看起来会觉得机器象是关机，但仍可通过某种输入设备立刻将其唤醒。

要做到省电，可以从以下几个方向着手。”

■　CPU：这是整个系统的心脏，不能轻易停掉，但‘跳慢一点’当然会比较省电。一般用于嵌入是系统的CPU，都会提供以下3种方式来控制CPU的用电。

□　Clock调整。例如，输入CPU的Clock是48 Mhz，CPU内部可以设定将其升频（如x2，变成96 MHz），或降频（如除以2、除以4、除以8等）后的频率当作CPU的工作频率。

□　有的CPU设计为可执行于不同工作电压下，当然，比较低的电压会比较省电，但执行效率通常较差。

□　执行状态调整。CPU通常会提供诸如‘halt’或‘sleep’的指令，执行后可以让CPU停止一些CPU内部的功能，并暂时停止执行指令，如图9-22所示。顾名思义，halt是暂停的意思，sleep则是睡眠的意思，两者同样会让CPU暂停执行指令，但后者显然会关掉比较多的CPU功能，当然比较省电。当产生特定的中断（一般会使用Timer、key、touch panel等中断来源来唤醒CPU）时，就可以把CPU从halt或sleep mode中唤醒，继续获取指令执行（CPU醒来后执行的第一行指令一定是中断处理程序）。

▲ 图9-22　某CPU提供的几种待机模式

■　关闭暂时不使用的设备（包含外部设备以及整合进CPU内部的设备）的电源：这个当然必须硬件设计配合，首先，该设备及相关组件的电源供给必须独立，接着硬件设计必须提供可以控制该设备电源切换的开关，这样驱动程序才可以通过Port I/O控制开关，达到真正切断该设备电源的目的。基本上，设备的电源切断后就不应该再耗电。

■　切换内/外部设备的执行模式或工作电压：和CPU一样，有些设备不能任意切断电源，所以就会提供不同的执行模式，但基本上不会像CPU那么复杂，通常就是Full Run Mode与Standby Mode两种模式。驱动程序必须根据data sheet内的规定，在暂时用不到该设备的时候，让其进入较省电的Standby Mode。

■　将CPU的各个PIN脚逐一切换为较省电的模式：用于嵌入式系统的CPU为了节省PIN脚、降低成本等因素，它的某些PIN脚可能是多用途的，当这些PIN脚被设定成不同的用途，就可能有不同的耗电状态，所以当某根PIN脚暂时不使用时，必须将其设定为最省电的用途。这是项挺麻烦的工作，每个CPU的设计逻辑都不一样，有时候甚至要用trial-and-error，实际测量在不同设定下的耗电流。虽然如此，我们还是有几个简单的基本原则。

□　通常GPIO会比其他的设定（如AD Port、Data Bus等）省电。

□　通常Output PIN会比Input PIN省电。

□　通常将Output PIN保持在低电位，会比保持在高电位省电。

□　如果GPIO有被电路拉高（Pull High）或拉低（Pull Low），逆势而为通常会比较耗电。

□　以上原则不见得适用于所有CPU, IC从设计到制造有太多的状况会发生，有可能是IC设计上的特殊考虑或bug，也有可能是工艺上的瑕疵，造成某些IC的耗电特性与一般原则不同。这种状况并不罕见，通常原厂提供的data sheet或sample code中内会特别标明。”

菜鸟：“我们的硬件设计都有考虑到这些吗？如果硬件设计没对应好，会使得原有的固件也很难做吧！”

固件老鸟：“这就是为什么固件和系统人员必须参与硬件设计的原因了。就算再没空，至少都要参加电路图的review会议，并对硬件设计人员讲解电源管理程序的基本架构，例如，系统有几种耗电状态、idle mode和sleep mode有哪些设备必须关掉、哪些不用等。固件人员必须确认电源管理的相关硬件设计是什么，包含：哪些设备具有独立电源、该设备的耗电流是否可以单独测量、用哪根PIN脚切换设备的电源开关、CPU各个PIN脚在各个执行状态应如何设定较省电等。如果事前不做这些沟通，等板子回来后，你就会发现有些设备的电源无法控制，或者使用了不合适的PIN脚导致耗电升高。

举个常会犯错的例子。硬件工程师经常忽略有些CPU PIN脚的状态（High或Low）会影响耗电流，假设硬件工程师选了一根CPU的GPIO Port当做切换某个设备的电源开关，并定义低电位是切掉电源，高电位是开启电源，这样的设计乍看没有问题，一旦用来控制电源开关的这根PIN脚的特性是维持低电位时较耗电（例如，这根PIN在CPU内部有pull-high电阻，要把被拉高的电位维持在低电位，当然需要更多的电）。为了关掉这个外部设备的电源，结果却使CPU耗了较多的电。”

菜鸟：“遇到这种状况，硬件工程师就只能试着rework板子，这就是为什么我们现在的板子拉了好几条红色、蓝色电线的原因吗？”

固件老鸟：“不全是电源管理的原因啦！新的硬件设计要做到第一次就完全OK几乎是不可能，很多CPU或设备芯片的规范一不小心就会被忽略了。如同刚刚的例子，CPU data sheet那么厚，谁会注意到这根PIN脚在CPU内部会被拉成高电位？同样一本data sheet，硬件工程师和固件工程师看的重点一定不一样，所以，硬件设计绝对不能让硬件工程师闭门造车，否则一定会浪费很多很多时间。

最后对电源管理做个结论。一般人以为电源管理就是把机器的总体用电量尽量的调低，但这只说对了一半。基本上，只要关闭当下不需使用的‘设备’（例如，不播音乐时，就可以把译码器与放大器关掉），并让每个设备运行在可正常运行的最低频率或最低电压，就可以做到低耗电。但整个系统中包含了许多设备，尤其是大部分设备都整合到一颗SOC里的状况，这些设备往往会互相影响。例如，设备A与设备B共享Clock的来源，在某个电源配置不会用到设备B，却需要设备A全速运转，程序代码9-16所示的程序就会出问题。

程序代码9-16

除了硬件先天的限制外，软件系统运行在不同的应用下，也会有不同的省电需求。所以我们应该以整个产品的观点来看电源管理，不能任由各个驱动程序，或应用程序工程师直接进行省电设定，否则即便在开发阶段，系统局部运行都能满足需求，等到整个系统整合起来后，轻则各个模式耗电状况不如预期，重则因为省电设定互相冲突，导致系统频频死机。

正确的做法是：根据软件的需求与硬件的限制，系统设计者可以归纳出这个产品应该包含的所有电源模式，并在系统中设计一个模块——Power Manager，由其统一管控。

■　系统在什么状况，应该要切换到哪个Power Mode。

■　在某个Power Mode下，系统中的CPU与各个设备的耗电设定。

图9-23所示是我们系统的电源管理架构图，你能解释它的意义吗？”

▲ 图9-23　Power Manager系统架构图范例

菜鸟：“我现在能从这张图看出一些端倪来了，既然系统的电源管理是由Power Manager来主导，驱动程序则应该提供标准格式的电源管理API供Power Manager使用。另一方面，系统与应用程序必须根据其需求设定系统的耗电状态,但又不需要知道所有设备的细节，因此，Power Manager必须提供已经抽象化的Power Mode及API，供其他系统模块或AP调用。”

固件老鸟：“这样理解没错，实际在implement电源管理系统时，还必须注意以下事项。

■　同一个系统常会应用在不同的硬件设置上（或说电子产品可能因成本的需求，取消某硬件功能，或替换功能相近的硬件设备），所以必须要求所有驱动程序中跟电源管理有关的行为，都保持一致的style与interface，并且符合上层Power Manager的规范。通常我们会将所有驱动程序应该遵守的规范定义在HAL (Hardware Abstraction Layer)API中[17]。

■　驱动程序只提供电源管理的机制（Mechanism），由Power Manager决定电源管理的策略（Policy）。也就是说，Power Manager需明确定义各Power Mode的配置，并根据该配置去调用HAL中相关functions。

例如，假设有一个Power Mode ‘IDLE’，它的配置定义为‘1.CPU Clock降至最低；2.除了LCD Controller之外，关闭所有的设备’。当Power Manager切换到此Power Mode时，要做的事情就是根据Power Mode配置定义，调用驱动程序公开的电源管理API。

■　若说驱动程序提供mechanism, Power Manager制定policy，则启动或切换Power Mode的时机则由系统模块或应用程序来决定。因此，Power Manager必须提供足够的Power Mode与API让其他程序调用。

■　无论是否允许应用程序主动改变Power Mode，系统都必须根据系统架构主动切换Power Mode。例如，当系统中的idle task被唤醒执行时，这表示系统暂时无事可做，所以idle task必须主动将Power Mode切换到低耗电量的等级，从而满足系统架构中对电源管理的设计要求。

■　除了 Power Mode切换外，电源管理还有一个重要的topic——低电压与突然断电的处理。系统必须在这些‘紧急’状况发生时，抓紧时间做一些必要的处理，以避免重要系统信息的丢失，造成下次上电开机时发生不正常的现象。”

9-3-9　断电前处理

菜鸟：“我对上面说的两个‘紧急’状况有点无法理解。当‘低电压’状况发生时，系统不就是show一个信息出来，应该还有充足的时间慢慢存储系统信息吧！相反的，当‘突然断电’发生时，不就已经断电了吗？既然没电了，程序就没办法运行了，哪里有办法再去做什么必要的处理？”

固件老鸟：“很多初学者都用你一样的观点在看这些事情，而认为这两个状况都不需要特别处理；如果你试着用‘电’的观点来看就不是这么一回事了。首先我们来看看一般电池放电的特性，如图9-24所示。”

▲ 图9-24　电池特性曲线图

菜鸟：“我了解了！从这张图来看，电池电压在降到约3.6V时就会快速降低，当降到约3.5 V以下时，电压就像溜滑梯一样，马上降到CPU可容忍的最低电压以下，因为电池放电的这种特性，当低电压事件发生时，其实已经没有多少时间可以让CPU继续执行了，当然就必须当成紧急事件处理。”

固件老鸟：“这个事情麻烦的地方在于‘要设定哪个电压值当作低电压的临界点？’。若设太高，虽然争取到充裕的时间，但难免浪费了电池可用电量，也减少了机器实际可操作的时间；若设太低，才检测到低电压，还来不及处理任何事情，CPU或其他设备就已经没电罢工了。所以实际上我们会至少设两个低电压等级，一个是wamning level，另一个是fatal level。前者系统仍有充裕时间备份数据与警告使用者，而后者通常就必须强制关机了。

另一个是突然断电的状况，可能是使用者不小心拔掉插头，或机器经撞击将电池弹出。一般人会认为马上就应该断电了，但对CPU的运算速度来说，能争取到几个mini-second就能执行不少指令了，而且电路板上或多或少都会有些电容，这些电容平常的主要作用可能是用来稳压。但在这种状况下，电容里的电可以使系统再撑一小段时间。所以这种突然断电的状况是有办法处理的，但重点是时间有限，系统必须把握时间做真正必须做的事情。

无论是低电压或突然断电，系统能判断到的硬件事件都是电压已经下降到某个临界值。从如图9-24所示的架构图中，我们可以看到当电池电量逐渐降低到一定程度时，电池的驱动程序会发送一个低电压事件给系统（系统可定时检测电池电压，或利用CPU内、外部的硬件功能进行低电压检测[18]），你认为此时系统应该做什么事情？”

菜鸟：“实在没概念这样的时间大概能做多少事情？但我认为应该从产品的应用来看，有的应用根本没有系统状态的概念，每次开机都是重头开始，此时就没有必要做什么处理。若是像MP3播放器这种产品，产品规格可能会规定下次开机要从上次播放的歌曲继续播放，则当系统碰到低电压时，就必须尽快将相关信息存回NAND Flash里。

以必须存储系统信息的产品为例。从系统来看，我认为至少应该结束目前正在做的事情（如写入某个文件），并存储系统状态，如果还有电的话，应该要试着正常关机。”

固件老鸟：“有人称这种电量快速降低到系统无法运行的状态为‘Brown-Out’[19]，这段时间内能做多少事，会跟CPU、电路设计与电池规格有关，所以必须先经过实际测量信号，以得出系统还可执行较保险的时间长度，接着是测量或计算要做的事情分别会花掉多少时间，判断还有没有缩短的可能。

上面你说的都是系统该做的事情，值得注意的是，像这种不正常的突然断电可能会使CPU运行不正常，尤其是瞬间断电又上电的状况，这会使CPU没有正常的reset。换句话说，通常低电压处理的最后一件事情,就是一定要保留时间让CPU来得及reset（主控IC通常都会有RESET PIN，控制这根脚就能让CPU RESET,或者CPU会提供reset的instruction，只要用Inline Assembly执行这个指令即可）。”

9-3-10　充电（Charger）控制

固件老鸟：“有的产品会使用充电电池（如手机等手持式设备），既然已经说到了电源管理，我们就顺便来谈谈充电（charger）控制。

因为锂离子电池有高的能量密度、高的工作电压、无记忆效应、寿命长、无污染、重量轻、自放电小等优势，所以目前的电子产品多采用锂离子电池。但对锂离子电池充电仍必须考虑一些事情，否则轻则充不满或减损电池寿命，重则造成电池变形、漏液，甚至发烫爆炸等现象。偶尔还是会看到手机电池自燃或爆炸的新闻，主要原因可能是电池质量不好或充电控制出了问题。”

菜鸟：“Charger还需要控制吗？不是接上电源就会自己充电了吗？”

固件老鸟：“你老是改不了用一般人的观点去想象电子组件行为的坏习惯，所有你在机器或板子上看到的东西都是要控制才会动的，只是到底是由工程师通过编写驱动程序进行控制，还是利用其他硬件或IC来帮忙控制罢了。

充电电池可以外加一个Charger IC来做充电控制，也能通过系统中的驱动程序来控制，当然也可以如你所说的，完全不加控制。”

菜鸟：“如果真的不控制，是不是会如您所说的，有可能造成自燃或爆炸的危险啊？”

固件老鸟：“这种我们称为‘直充’的充电方式，必须依赖电池里的过充保护线路。当电池充电充到一定程度时，电池会自动停止充电，从而防止‘过充’所可能造成的危险。一般对制造成本较敏感的产品会采用这种方式，但电池的质量良莠不齐，若过充保护线路检测所得的电压值不准确，还是有机会造成过充。甚至有比较没良心的厂商，量产时故意夹杂不含保护线路的电池。”

菜鸟：“听起来蛮可怕的！可是我们在新闻上看到电池出纰漏的不乏国际大厂，我想国际大厂总不可能甘冒风险而偷工减料吧！这么说来，Charger的控制真的很困难啰？”

固件老鸟：“电池会出问题的原因不少,但还是由电池或电源供应器质量不良所引起的居多，锂离子电池充电的算法其实并没有那么复杂，主要目的就是要尽量避免过充与过放的状况发生。我们先来谈谈何谓过充与过放。

■　过充：过充电是指电池经一定充电过程充满电后，再继续充电的行为。锂离子电池在设计时，负极容量比正极容量要高，因此，正极产生的气体通过隔膜纸与负极产生的镉复合；故一般充电情况下，电池的内压不会有明显升高。但如果充电电流过大，或充电时间过长，产生的氧气来不及被负极消耗，就可能造成内压升高，电池变形、漏液等不良现象。同时，其电性能也会显著降低。

我们可将锂离子电池的充放电原理，用生活中常见的泡沫现象来比喻。锂离子电池如同一堆肥皂泡沫，泡泡内存储的就是电能。当充电时，汽泡会随着充电时间加长而不断增大，当超过其极限值时，汽泡就会破裂，此时即损坏了锂电晶型，造成永久性损坏。当过度放电则会造成汽泡塌陷、消失，这样下次充电时汽泡也充不起来，而造成锂电失效。如何控制汽泡不充爆和汽泡不过度塌陷？就必须要用保护电路加以严格控制。当然，优质的电池芯和精确的控制算法可大大地延长电池的使用寿命。

■　过放：电池放完内部存储的电量，电压达到一定值后，再继续放电就会造成过放电。过放电会使电池内压升高，正负极活性物质可逆性受到破坏，即使充电也只能部分恢复，容量也会有明显衰减。

锂离子电池过放之后，电压会降低到2.5 V以下，如果过放不是很严重，通过小电流涓流充电，还能够挽回一些容量，使电池不至于废弃。但是如果对已经过放的.电池直接充太大电流，则可能会更彻底的摧毁它。”

菜鸟：“所以Charger控制的重点是：为了避免过充，当快充满的时候，必须放慢充电的速度，当电池电压充到某个临界值时就必须停止充电。而要避免过放就如同我们前面说的低电压控制，在电池快没电时，电池电压会突然陡降，此时必须停止系统运行或自动关机，以避免电池电量被过度消耗，造成永久性的破坏。”

固件老鸟：“你的解释的确很口语化，让我们先来看看充电算法是如何运行的。如图9-25所示，纵轴是用来充电的电流大小，横轴是时间，我们可以看到电池充电分为3个阶段。

■　小电流启动：charger会先用小电流来充电，为了就是避免当时电池电压过低。若是马上使用大电流可能会对电池有影响。

▲ 图9-25　充电曲线图（1）

■　定电流充电（CC）：使用固定电流，将电池快速充到一个特定电压（接近充饱的电压）后，此时就应该放慢充电的速度，以避免过充。

■　定电压充电（CV）：当电池使用定电流充到某个电压值后，必须要使用小电流来慢慢充电，否则会因为电池的内阻效应，在大电流时会造成一个电压差，进而影响电池的饱和度。通常需要控制充电电压为电池的额定电压±50mV以内，以4.2V锂电来说，就是4.15V〜4.25V。

一般我们就是采用这种先定电流充电后，再定电压充电的充电方式，它是在充电开始时先用定电流充电，当充电到某电压之后，再改用定电压的充电方式，直至充满为止。CC/CV充电方式的优点是它可以缩短电池的充电时间，又能防止电池过充电。图9-26所示是将充电周期内电压与电流的变化画在同一张图内，应该可以看得更清楚。实作时当然还有一些细节必须注意，但充电算法的基本原理就是如此。”

▲ 图9-26　充电曲线图（2）

固件老鸟：“有些系统开发商会假设电池内有极优的保护电路，假设end user不会频繁充放，所以对Charger Algorithm不甚注意，即不仔细防止过充状况发生。原本这样也无可厚非，但实际状况却是工厂可能为了省成本，暗中选用较便宜的电池，结果当产品交到end user手上，电池不是用没几次就充不饱（感觉电量越来越少），不然就是容易发烫、膨胀甚至爆炸。

为了避免造成使用者人身危险的意外，我们做系统的必须尽量在软硬件设计上做好把关的工作。除了基本的CC、CV mode切换之外，还必须能够检测有问题的充电器（输出电压过高、过低或不稳定），必要时，停止充电并警告使用者。”

菜鸟：“这样听下来真的有点可怕，我以前一直以为电池会发烫或膨胀是正常的。”

9-3-11　ADC & DAC

菜鸟：“在实现charger算法时，必须随时可以判断电池的电压值，才知道什么时候该停止充电。但电压值是连续的，或是模拟的值，而CPU只能处理0、1组成的数字数据，我们的驱动程序到底该如何才能取得电压值呢？”

固件老鸟：“CPU确实只能处理数字的数据，然而真实世界却是以模拟的信号存在。除了电压之外，诸如温度、速度、音频等都是连续的模拟数据，计算机系统最终仍必须与真实世界联系，为了能取得这些模拟信号加以处理，就必须藉由模拟/数字转换器——ADC（Analog To Digital Converter），做模拟/数字信号的转换。

既然计算机系统可以利用ADC收集外界的模拟信号，自然也必须通过DAC，将计算的数字结果转换为模拟信号，并输出到外界去。例如，MP3文件是数字的数据，系统译码完后还是数字的数据，这些数据必须经过一颗DAC转换为模拟信号，才能通过放大器输出。图9-27所示是一个典型的ADC与DAC应用的例子。”

▲ 图9-27　ADC & DAC

固件老鸟：“首先说明ADC。ADC的运行原理相当简单，就以测量电池电压为例，输入AD Port的电压范围是0V～3V，如果该AD Port的精度是10bit，即模拟转为数字后的值为0〜1023之间（210=1024）。举例来说，当电压值为0时，转换后的AD值为0，当电压值为最大值3V时，则转换后的AD值是1023。至于在0V〜3V之间的输入，则会以线性的方式作转换。例如，输入电压为2.2V，则取得的AD值为：

（2.2/3.0）×1024 =751

对驱动程序而言，自AD port取得的值（通常是通过Memory Mapping Register取得），反算可得真正的电压值。同上例的状况（电压值范围0V〜3V、AD port精度10bit），假设得到的AD值是620，则反算可得输入的电压值为：

（620/1024）×3V=1.82V

外部芯片只要能将模拟的输入（如温度、音量等级、震动程度等）转换为不同的电压输出，再连接到CPU的AD Port，则程序就可以在控制的精度之下算得外界模拟的输入。这种模拟转换数字的输入方式可以应用在许多领域，因此，CPU内的程序可以收集到各式各样外部信息，并依此作出不同的控制与处理。常见的应用有：温度感应器、光电感应器（检测亮度）、速度计、压力感测器、磁场传感器等。”

菜鸟：“其实ADC是用离散的电压值模拟外界的连续信号，所以ADC的精度就很重要。在刚刚的例子中，10 bit ADC只能将信号分为1024个等级，如果是12bit就将信号分为4096个等级，当然比1024个等级精细，系统可以察觉出信号间的细微变化，而精度越高则表示转出的数字信号越接近原始的模拟信号，但要选用多少精度的ADC，应该还是跟应用与成本有关吧！”

固件老鸟：“最后我们还是稍微提一下DAC。其实一般DAC IC的功能比你想象得高端，根据其定义的应用领域，CPU只要将数字数据传给DAC IC，它就会帮忙做完所有的事。举例来说，有一颗称为“Ultralow Noise20-Bit Audio DAC”的IC，系统只要把PCM音频数据传给它，它会执行译码，并将结果转换为模拟数据输出，有的Audio DACIC甚至可以直接处理MP3的数据。再例如，一颗名为“Voltage Output 10-Bit DAC”的IC，系统甚至只要传给它诸如1.8、3.3等数字，就会转出1.8V或3.3V的电压。“

所以系统在使用DAC IC时，只需要注意DAC IC与CPU之间的接口（例如Audio DAC都是用I2C或I2S与CPU沟通），以及数据传递的protocol即可。但有些比较简单的应用，例如，控制马达转速、调整LED亮度、输出较低质量的声音（如语音或特效音），若还要通过另一颗DAC IC来做，难免有点杀鸡用牛刀之嫌，而且还增加成本与系统复杂度。所以我们还是需要让CPU模拟信号的方法，最常用的就是PWM（Pulse Width Modulation），原理如图9-28所示，上面是欲输出的模拟信号，下面则是PWM的数字信号。

▲ 图9-28　PWM示意图（用不同的

Duty Cycle 来模拟 analog 信号）

PWM的原理很简单，固定时间段内高电压所占的比例称为Duty Cycle，而不同的Duty Cycle代表不同模拟输出的值。简单地说，就是对欲输出的模拟信号进行编码。许多SoC本身就提供PWM输出的功能，就算主控IC没提供PWM输出，驱动程序也能够通过一个Timer计时，控制某根IO脚的Duty Cycle。”

9-3-12　Watch-Dog

菜鸟：“刚刚在讲低电压处理时，您说一定要保留时间让CPU执行reset，让我突然想到一件事情。大部分电子产品都会留一个RESET孔，当系统死机不会动时，用回形针去戳这个小孔，可以强迫系统reset，至少使用者可以继续使用这台机器。但有些机器并没有这个reset孔，如果发生死机怎么办？难道一定要拆电池、拔电源才可以重启机器吗？或者真的有产品的系统可以保证绝对不会死机？”

固件老鸟：“我想，就算连NASA的宇宙飞船都不敢保证自己的系统是bug-free吧！若产品为了结构外型而不想留reset孔，那么，系统中一定要养只看门狗，随时监看系统是不是还活着，若系统已经卡在某个地方了，这只看门狗就会汪汪一声，并启动一个中断，这个ISR会主动reset机器。”

菜鸟：“大哥，我知道我问题很多，但您也不要这样随便敷衍我，系统里居然还能养只狗？”

固件老鸟：“CPU的block diagram你没认真看吧，里面有个Watchdog Timer你不知道吗？”

菜鸟：“好像有点印象，我以为那不就是个Timer吗？不知道它还真的能帮忙‘看家’。”

固件老鸟：“好了，不跟你开玩笑了，我来说说Watchdog Timer在嵌入式系统中的应用。电子产品在运行时常常会受到来自外界电磁场的干扰，造成程序‘乱跑’或陷入死循环，使得系统无法继续工作，并造成整个系统的陷入停滞状态。Watchdog Timer的主要功能就是当程序发生不可预期的错误（程序本身的漏洞、程序跑进无穷循环，或外部信号干扰，导致产生错误动作）时，会自动重置系统，以确保系统稳定。

顾名思义，Watchdog的意思是看门狗，而Watchdog Timer当然也是一种Timer。当系统enable了Watchdog Timer后，它就会从某个已设定好的值开始倒数，数到0时，就会做产生一个最高优先级的中断或直接让CPU reset。”

菜鸟：“按照您的定义，Watchdog相较于一般的Timer，不过就是当timeout时会去reset CPU而已，为什么它会有监督系统是否已死机的功能？”

固件老鸟：“没错，Watchdog Timer的确就是一种Timer，若想让看门狗认真帮你看门，系统就不能不定时喂狗。”

菜鸟：“又来了，那我们系统岂不是要多准备一些西莎（狗食）？”

固件老鸟：“呵呵，工程师生涯苦闷，要培养点幽默感嘛！当Watchdog Timer开始倒数时，为避免timeout后Watchdog去reset CPU，系统必须及时重新设定Watchdog的counter，我们称之为‘喂狗’，以此表示系统还活着。若某个程序（任何程序，可能是驱动程序、系统程序或应用成程序）在循环内运行了太久的时间，又没有及时重新设定Watchdog的counter，则Watchdog会以为系统死机了，自然会去reset CPU。

所以，Watchdog倒数计时的时间，与系统重设Wtachdog Counter的时机都必须好好规划，除非事先已知某个程序段的执行时间会超过Watchdog的倒数的时间，否则，应该尽量统一由系统来‘喂狗’，main-task中每次super-loop结束前就是很好的时机。

在多任务系统中，某个程序段在runtime的执行时间不一定能够准确预测，也就是说Watchdog还是有可能在系统并未死机的状况下误动作。因此Watchdog Timer的使用并不如想象中简单，设计阶段与测试阶段都必须投注一定的心力。”

菜鸟：“若硬件未提供Watchdog Timer的功能，是否可以用软件模拟？”

固件老鸟：“当然可以，但你必须保证相关的code绝对是没问题的，否则，死在‘检测系统是否死机的程序’中，就算大罗神仙也救不了你。此外我们也可以使用Watchdog的思想，来定时执行系统运行状况的检查，即随时检查系统里的重要表格是否有异状。若有异常，可进行修复或reset系统，不要等到系统真的死机后再来处理。

举例来说，Linux里内置了一个Watchdog的功能，用于监视系统的运行。Kernel里的Watchdog module通过/dev/Watchdog这个‘设备’，与user space的程序沟通。应用程序一旦open了/dev/Watchdog，等同命令Kernel启动一个1分钟的定时器，以后，该应用程序必须保证在1分钟之内向这个设备写入数据，每次写入数据就会导致重新设定counter。若1分钟之内没有写入数据，timeout时会导致一次系统reboot操作。应用程序只要close掉/dev/Watchdog就能停止kernel中的定时器。

此外，Linux还提供一个称为Watchdog的应用程序，它可以定期对系统进行检测，检测项目包括

■　Process table是否满了（系统里同时执行的程序太多了？）

■　Memory Poo里剩余可用空间？

■　文件系统是否有异状？同时开启的文件数量是否超出限制？

■　CPU或系统的Loading是否太重？

■　网络通信是否曾发生问题？Loading是否太重？

如果某项检测失败，这个Watchdog的应用程序则可能引发一次Soft Reboot（如同执行shutdown命令），它也可以通过/dev/Watchdog来触发Kernel Watchdog的运行。”

菜鸟：“就算我们这些开发者竭尽全力提升系统的稳定度，一旦电子产品实际交到使用者手上，会发生什么事情实在无法全部事先预测，而且外界环境确实也可能影响硬件的运行，所以系统开发者才会想方设法，在硬件平台、系统或应用程序里养了看门狗，让他们在产品出货后还可以帮忙看家。”

固件老鸟：“没错，但必须让系统好好照顾好它的狗，一旦饿着它，它可是会咬人的！”

9-3-13　CPU初始化

固件老鸟：“我们陆陆续续说明了CPU内部的运行原理，你现在知道‘CPU的驱动程序’该如何写了吗？”

菜鸟：“综合以上的讨论，CPU也是系统的设备之一，当然也需要驱动程序，它必须在其他程序开始执行前就做好CPU的初始化工作，包含：

■　设定CPU内部寄存器，包含PSR与SP等。

■　设定中断矢量表。

■　设定CPU内部各单元（CPU core以及内置设备）的Clock。

■　BUS设定——设定各个外部存储器的特性，包含要插入几个waiting cycle、操作该存储器的基本单位宽度（是16bit或32bit）等。

■　设定CPU各个PIN脚的用途使其符合系统或应用程序的需求。

■　设定CPU的执行模式，刚开始应该是在full run mode。

■　其他内部设备的初始化。

固件老鸟：“差不多就是这些了，这些动作要尽早做，有些甚至在Boot-Loader阶段的前期就必须搞定。此外，虽然CPU就是一颗IC，但是它内含了许多设备与功能，驱动程序还是必须将其区分为一个个独立的模块，例如Initialization（初始化）、Clock（时序设定）、Power Manager（电源管理）、Memory and Bus（存储器设定）、Timer（计时器）、SDRAM Controller、LCD Controlle等，对上层工程师而言，它们应该是一个个独立的模块，只要调用这些模块提供的API即可。”

菜鸟：“看来单单一颗CPU的驱动程序就有得写了，幸好CPU厂商有提供sample code。”

9-3-14　CPU内部还有什么？

固件老鸟：“随着半导体技术的进步与IC设计产业的蓬勃发展，越来越多的CPU整合了各式各样的功能。有些CPU就是一个具体而微的系统，CPU里面甚至有ROM以及RAM，几乎只要供给电源就可以独立运行。这种高度整合的CPU简化了硬件设计的复杂度，同样让固件工程师较少有机会碰到在完全不稳定的硬件平台上开发的困境。”

菜鸟：“以前的时代是如何我不知道，虽然现在的CPU整合了这么多功能，但我们的板子上还不是密密麻麻的组件。CPU功能增加了，成本降低了，但同时也开启了产品设计者的视野，就像PM老大说的，简单的事情就不会交给我们做了。”

固件老鸟：“你说的也有道理，但是别忘了每年用量最大的CPU是像8051这种8 bit的CPU，对应的产品也是以简单廉价的居多。当然，8 bit CPU也常常变型成那种整合了一堆设备的无敌铁金刚，对成本越敏感的产品，越适合这种整合性CPU。讲了这么多，你对我们的CPU还有什么疑问吗？”

菜鸟：“可不可以再稍微说明一下CPU内部设备的功能？”

固件老鸟：“不同的CPU有不同的内部设备，要从何说起呢？有些设备因为太常用了，如ADC（Analog to Digital Converter）、Timer或DMA控制器等，从前都需要外接控制IC才有的功能现在几乎变成各家CPU的基本功能了，我们就从这些功能说起吧。”

■　Timer：前面说过，CPU必须有稳定的Clock输入才可运行，CPU将输入的Clock升频或降频后，提供给CPU内部的各单元，或者输出到其他的外部设备，使其时序可以与CPU同步。应用这个Clock，CPU可以轻易地提供计时或闹铃的功能，我们称之为Timer。CPU内可以提供多个可独立运行的Timer，每一个Timer都会有其相应的中断号码，通过许多Memory Mapping Register来控制Timer。

驱动程序要先设定Timer的属性，例如，该Timer启动后多久到时；Timer的基本时间单位为Clock，假如Timer的Clock来源是24Mhz，表示（24× 1024 × 1024）个Clock就是1秒钟，或者可换算基本的时间单位为：

1秒/（24×1024×1024）=40×10-9秒=40ns（纳秒）

接着写作相应的timeout中断处理程序，则该ISR会从Timer启动起的指定时间后被执行。电子产品的应用或多或少都有计时的需求，以驱动程序来说，要驱动某些设备或实现某些protocol要有很精准的Timing。以系统程序来说，Timer几乎是必备的功能，并且基于驱动程序提供的基本计时功能，扩展出更复杂、以手表时间（秒、分、时）为主的Timer功能。例如，每N个mini-second（千分之一秒）产生一次timeout，连续产生M次等。

应用程序使用系统提供的Timer功能时，只要指定callback function即可，当然不用自己写ISR。

■　RTC（Real-Time-Clock）：上述的Timer是用来做较精准的计时，如mini-second（10-3秒）或micro-second（10-6秒）等级，如果是应用在计算以秒或分为单位的计时反而不适用。以16 bit Timer为例，它最多仅能计时到216（65536）个时间单位，通常还远少于1秒钟，要累积很多次timeout才能计时到1秒钟，则为了达成简单的时钟功能，系统将一直处于中断产生的状态。

RTC也是一种Timer，但是它的精度比较粗，通常是百分之一秒的等级，程序可以将其调整到每隔1秒甚至1分钟才产生中断，如此一来，可以轻易达成时钟或日历的功能。因为这种每分钟才产生一次的中断并不会耗掉太多的电，系统被中断唤醒后，只需要做简单的累加计算即可，即便系统处于sleeping mode，仍然可以维持长时间计时。

■　DMA（Direct Memory Access；直接存储器存取）：DMA允许某些外部设备可以独立地对系统存储器进行读取或写入，而不需牵涉到CPU。在PC中，许多硬件的系统会使用DMA，包含硬盘控制器、绘图显示卡、网络卡，以及声卡等。当CPU要从外部设备（如硬盘、NAND Flash或网络封包）读取大量的数据到主存储器时，如果不使用DMA的话，CPU必须自设备逐一读出数据，通常是将数据先读到一般寄存器，然后才写入主存储器。

在做这种外部设备到主存储器的数据传输时，CPU除了I/O之外什么事也不能做。如果传输的数据量很大，或者传输的一方是像硬盘般的慢速设备（不管硬盘技术再怎么进步，性能还是不会比存储器快），系统在一段长时间内只能执行数据传输的动作，无法做计算的工作，这对嵌入式系统中最宝贵的资源——computing power是一种严重的浪费，简单地说，就是杀鸡用牛刀。应该要把简单的I/O工作交给DMA控制器去做即可，同时间CPU可以执行更重要的程序。

用于嵌入式系统的CPU一般都会内置DMA控制器，所以都可通过寄存器去控制它。在进行数据传输之前，驱动程序要先设定好来源地址、目标地址以及要传输的数据量，上述的来源与目标地址可能是存储器的地址，也可能是Memory Mapping Register。例如，写入某寄存器即代表输出到某设备，或者某设备会将数据逐一存入某寄存器之中。当驱动程序设定好传输数据的信息后，就可以下指令启动（Trigger）DMA控制器。接着，系统可以去做其他的事情或切换到其他的task，当数据传输完毕后，DMA控制器会产生中断通知CPU。

■　其他内置IP：目前业界CPU内部功能整合度之高已超乎想象，有些几乎是为特定产品量身定做，例如，手持式设备、手机、MP3随身听等都有整合型的CPU可选用。图9-29所示是一个用于MP3 Player整合型CPU的实例，图中大方框内的方块就是整合在CPU内部的芯片，你能想到MP3 Player该有的功能都包含在里面了，包含MP3译码器、LCD Controller、NAND Flash Controller、USB Controller等。产品开发商用了这种CPU，软、硬件都不需要太大的effort，就能整合出完整的产品。如此一来，不但缩短研发时间与经费，还可加速产品上市的进度。

既然这些功能已经被整合到CPU内，当然就不能用CPU的Port I/O来控制。CPU的data sheet内会定义许多Memory Mapping Register，按照该CPU的规定，就可以控制这些整合在CPU内部的IC。”

▲ 图9-29　整合型CPU架构图

固件老鸟：“关于CPU驱动程序的基本架构我们谈得差不多了，当然，应用于不同领域的CPU各有特色，整合在CPU内部的功能也大不相同，但基本原理就是这些了。有的CPU还提供MMU（Memory Management Unit）的功能，系统可用来实现虚拟存储器、虚拟地址空间、page on demand等大型操作系统的存储器管理功能，相信你在学校修操作系统时，应该也被这东西搞得迷迷糊糊吧？幸好一般电子产品实在用不到这么‘高级’的功能，而且这样的CPU通常也是设计给WinCE或Embedded Linux专用的，MMU的功能实现不难，但要调到稳定就要花时间了。”

菜鸟：“我渐渐能找到那种感觉了，像我们这样的研发单位，有着产品上市的进度压力，实在不可能什么技术都重头做起。如果可以的话，尽量找到现成且稳定的技术来源，单单做技术整合以及产品化的工作量就很大了。”

固件老鸟：“有些人认为拿厂商的sample code来改好像没什么技术，事实上，这才是最聪明的做法，我们不可能花太多的时间在一颗CPU或IC上，谁知道以后还会不会用到它？重要的是理解，理解不对，就算有sample code，你也无法将其移植到自己的板子。理解对的话，管它是什么CPU或IC都难不倒你。这就是所谓的：一法通，万法通，阿弥陀佛。”

菜鸟：“善哉，原来驱动程序的思想和佛法是有关联的。”

09-04　存储器

菜鸟：“您在讲开发驱动程序的准备工作时，曾经提到您以前碰到的一个案例，因为硬件设计错误导致SDRAM运行不正常，所有板子都必须重新制作的‘惨剧’。我想知道这种事情是否可以事先预防？”

固件老鸟：“我记得我也讲过，存储器出问题的机会比一般人想象的高，而且程序是依赖存储器执行的，不管是硬件设计还是驱动程序，存储器一旦出问题就是大问题。在嵌入式系统中，我们会用到的存储器不是像PC中那种一条一条的DDR或DDRII，而是一颗一颗的IC，我们可以把存储器分成以下几类。

■　可用地址读取的只读存储器：如EEPROM与Mask ROM等，通常用来存放程序与执行时期不会更动的数据，程序可以直接在ROM里面执行，除非有性能的需求，否则没有载入到RAM的必要。CPU或驱动程序可以通过地址或指针直接读取ROM里面的数据，但执行时期无法将数据写入ROM里。CPU是通过Address Bus与Chip select寻址，利用Data Bus取得ROM里的数据。

■　可用地址读取写入的RAM：和ROM一样，CPU通过Address Bus与Chip Select寻址，利用Data Bus存取（读写）RAM里的数据，但是RAM的内容是可以在执行时被改变的。

□　Static RAM：顾名思义，只要持续供应电源，SRAM里的数据就得以保存；SRAM耗电流比DRAM低，价格比DRAM贵。对固件工程师而言，SRAM的控制很简单（只要设定CPU与SRAM之间的timing即可，几乎不需要其他的控制就可以存取SRAM了）。对硬件工程师来说，SRAM的线路设计非常简单。虽然SRAM的好处多多，但为了成本考虑，通常在系统中，不会使用size太大的SRAM。

□　Dynamic RAM： DRAM有个要命的特性，是必须持续refresh才能维持数据的正确性，所以需要一个额外的DRAM控制器。驱动程序必须通过设定DRAM控制器，需要设定的数据包含DRAM的size、存取的时序、refresh的特性等，DRAM才可以正常的运行。在嵌入式系统中，我们通常使用的是SDRAM（Synchronous DRAM），详细原理在此就不详述了，最大的不同点是CPU必须提供持续而稳定的Clock给SDRAM。SDRAM的好处是较SRAM便宜很多，如果系统需要较大的缓冲区，通常都会选择使用SDRAM。

■　可用地址读取，但必须下命令写入的Flash（快闪存储器）：这就是我们之前提到过的NOR Flash，驱动程序可以把它当作ROM使用，CPU通过Address Bus与Chip select寻址，利用Data Bus取得ROM里的数据。NOR Flash的内容是可以更改的，但不像写入RAM那么容易，即不是通过address与Data Bus，必须对NOR Flash IC下命令才行。此外，NOR Flash有个特性，写入时必须以block为单位（一个block可能是16K或8K Byte），而且写入前必须先做擦除（Erase）的动作。这个特性使得NOR Flash的写入很麻烦，且性能很差，所以一般NOR Flash的用途为：一个是当作ROM的代替品，如果程序有问题可以重复烧录，免除开Mask以及ROM的内容无法修改的风险；因为NOR Flash有断电后内容不会丢失的特性，所以NOR Flash的第二个用途就是用来存储或备份执行时期的数据，例如，使用者私人的设定或录音数据等（除了NAND Flash外，使用SPI或I2C控制的NOR Flash、EEPROM也可算入此种存储器分类，但其单位价格比NAND Flash贵上许多）。

■　只能用命令读取和写入的Flash：MP3 Player与随身碟就是用这种NAND Flash当作大量存储介质。对固件工程师而言，它不能直接用地址存取，不论读写都要下命令，读取是以page为单位（512 Byte或2048 Byte），写入和擦除则是以block为单位（block是多个page的集合）。此外，NAND Flash有个比较麻烦的特性，它和硬盘一样会产生坏道（Bad Block），所以通常我们可以把NAND Flash当做比较小的硬盘使用，例如，MP3 Player或随身碟就会把NAND Flash格式化成FAT的格式。

就像SRAM与SDRAM的对比一样，相对于NAND Flash，NOR Flash有较快的速度，以及比较贵的价格。如果用NOR Flash设计2GB的MP3 Player，恐怕没几个人买得起。所以NOR Flash通常用来执行程序，而NAND Flash则可以用来存放大量数据。

其实存储器的种类很多，用于不同领域有其不同的需求。例如，有的CPU会包含一小块SRAM，最早以前的8051 CPU内部就有一个EEPROM（甚至它的接脚都和一般的EEPROM兼容，CPU可以直接用烧录器烧录）、有的是同一颗IC内包含了NOR Flash与SRAM等；不管半导体厂商如何变化，对固件工程师而言，若根据操作方式来分的话，应该就是上述的这四种分类。”

菜鸟：“我渐渐对这些存储器的异同有概念了，除了功能特性外，有时成本也是重要的考虑。举例来说，用NAND Flash来存储数据显然比NOR Flash或Mark ROM麻烦很多，但市面上的随身碟都是用NAND Flash，而且我看电视财金台，NAND Flash似乎逐年在降价中。此外操作SDRAM显然比SRAM麻烦，还要额外控制SDRAM Controller，电源管理似乎也比较麻烦，但我们的产品还是选择使用SDRAM，主要也是成本的考虑吧！”

固件老鸟：“除了成本考虑之外，硬件组件的选择必须配合产品的功能。假设我们的应用不需要很大的缓冲区，那么选用一颗小size的SRAM并不会比选用SDRAM贵多少。打个极端的比喻，假设单位SRAM比SDRAM贵两倍，我们第一个产品的应用不需要大量的RAM，选用SDRAM只要0.2美金，比选用SRAM只可以省下0.2美金；如果我们的第二个产品的应用需要大量的RAM，需要用到4块美金的SDRAM，相同容量的SRAM要8块，此时，不选用SRAM就可以省下4块。要记住，选用便宜的组件势必要付出更高的研发费用，在估计成本时，千万不能忘了人力也是要钱的，譬如SDRAM的系统当然比SRAM的系统复杂，也容易出bug。”

菜鸟：“了解。您还没说那个SDRAM的实例，SDRAM还有什么特性容易导致硬件设计错误？”

固件老鸟：“其实那只是硬件设计审查不严谨导致的问题。即使只是拉错一根线，板子上别的芯片也许rework一下（先切断连接线路，再拉皮（电）线即可），还可勉强使用，但像SDRAM这种依赖精准timing的芯片可就是大灾难了。图9-30所示是CPU data sheet建议与SDRAM的连接方式，有没有注意到特殊的地方？地址线必须移动一位，即SDRAM的A0要接到CPU的A1，以下以此类推。你先不要管其中的原理（即便是硬件工程师也可以不理会原理的细节），如果硬件设计时忽略了地址线要移动一位，即CPU的A0接SDRAM的A0，程序执行时会发生什么事？”

▲ 图9-30　CPU与SDRAM连接的范例

菜鸟：“当程序要到SDRAM获取一个Byte时，CPU会把正确的地址放在地址在线，根据规定也就是CPU的A1、A2、……，原本CPU的A1应该要连接到SDRAM的A0，但因为硬件设计错误，CPU的A1接到SDRAM的A1，所以SDRAM会收到错误的地址，自然就会将错误的数据放在Data Bus上。这样说好像有点复杂，我来画一张示意图，我们只看地址线的前四位就好，如果CPU的（A4，A3，A2、A1）分别是（1、1、0、1），如果硬件没错的话，如图9-31所示的CASE 1，应该分别接到SDRAM的（A3，A2，A1，A0），SDRAM可以得到正确的地址1101。

如果地址线接错，SDRAM的（A3，A2，A1，A0）是接到CPU的（A3，A2，A1，A0），因为对CPU而言，操作SDRAM时A0没有意义，所以此时CPU的A0可能是任何值，则SDRAM得到的地址将是101X（X表示任意值，也就是1010或1011）。这个错误不但使地址shift了一位，还让SRAM的A0可能为1或0。”

▲ 图9-31　SDRAM地址线连接错误造成的影响

固件老鸟：“其实硬件的存储器设计错误都还蛮容易找的，我们会试着把特殊值写入SDRAM。例如，每一个Byte写入它的地址除以10的余数，接着将其读出检查。只要能把某块存储器的值dump出来，不论是Data Bus还是Address Bus接错，应该都可以发现一些规律。当存储器读写发生错误时，有个基本的原则：如果数据dump出来有某种规律，那应该是接线出问题了，有时设计没错，是Layout或PCB制作时出错，通知硬件人员检查一下硬件，应该很容易可以找出来。例如，不管什么地址读出来的数据的某个bit总是1，应该要怀疑Data Bus相应的脚位可能因为短路一直被拉到High。如果存储器的内容没有规律，那可能是不稳定的问题，此时应该检查一下驱动程序的设定，以及输入到存储器IC的电源与Clock等是否合乎IC的规定。”

菜鸟：“在这个例子里，先把SDRAM与CPU相连接的地址线切掉，然后将SDRAM的每根地址线重新拉皮（电）线到CPU正确的PIN脚，也不行吗？”

固件老鸟：“理论上不是不行，事实上我们也这么做了。结果是如果使用ICE单步执行时OK，证明确实是这个问题。但让板子单独执行时就非常不稳定，显然是SDRAM的问题。这样的板子几乎完全不能使用，只能将修改过后的电路紧急送Layout，再重新洗板子，这样来来回回就用掉了大约两个礼拜的时间。不但延误进度，也因为这个错误太简单了，还被老板骂得满头包，只要审查电路设计与layout图时多用点心就可以避免了。”

09-05　控制其他芯片

固件老鸟：“说了这么多，你能指出驱动程序在控制CPU内置芯片与外部设备时有何不同吗？”

菜鸟：“我认为有两个最主要的不同点。

■　CPU内部的设备性能通常比较好，而且绝对不会有硬件线路设计错误的问题，所以固件工程师直接写驱动程序即可。至于外部设备则可能发生大大小小的设计疏失，驱动程序编写时往往还肩负帮忙硬件调试的任务。

■　CPU内部的设备是用寄存器（Memory Mapping Register）控制，而外部设备则只能利用CPU的PIN脚与其连接，所以必须通过控制这些PIN脚来控制外部设备。

我仔细想了一下，虽然外部设备是通过CPU的PIN脚来控制，但程序要设定CPU PIN脚的状态、从CPU PIN脚中取得设备的状态，或者外部设备可以通过CPU PIN脚产生中断，还是得通过CPU的寄存器。”

固件老鸟：“没错，你要记住，我们写的程序是让CPU执行的，所有的控制命令都是以CPU为起点，所有外部的状况也都是集中到CPU。如果说CPU是大脑，它的PIN脚就像是神经线，连接到各个受控制的器官。所以驱动程序要做的事情就是通过CPU规定的方法来控制各个PIN脚，所谓CPU规定的方法通常就是操作寄存器，至于要用哪种类型的PIN脚（GPIO、AD port……）与外部设备连接，要用什么方式控制（从最简单的设定PIN脚的High/Low状态、使用中断或者以较复杂的通信协议，如I2C等），则每个设备都不一样，好好详读该设备的data sheet与sample code是必要的。”

菜鸟：“我有个问题，您刚刚也说过，我们写的程序是要在CPU上执行的，所以CPU厂商提供的sample code应该都可以直接使用，但是外部设备或IC则不一定专为某个CPU设计，从IC厂商取得的sample code恐怕无法直接使用吧？”

固件老鸟：“没错，但这些sample code还是很有参考价值。你要知道，IC的data sheet不是单单写给固件工程师看的。实际上，一般IC的data sheet内大部分的篇幅都在描述PIN脚定义、时序图、电器特性等，这些都是硬件工程师在设计电路时的必要信息。也不是说这些信息对固件工程师完全没用，但通常是debug时才会用到，编写驱动程序最需要的信息是控制方式，而这些信息在data sheet内通常不会占据太多篇幅。如果对相关类型的IC不是很熟悉的话，常常会看不懂data sheet想表达什么，此时sample code就很有用了，虽然sample code中不是我们用的这颗CPU的控制方式，但是至少能看出控制流程，这样就很有用了。”

菜鸟：“我还以为IC厂商的技术人员会专为我们的CPU写驱动程序范例呢！”

固件老鸟：“如果我们公司是国际大厂，或者我们的产品出货量很大，IC厂商会为了和大公司建立关系，或者为了让他们的IC可以随着我们的产品出货，也许会考虑帮我们的平台写驱动程序，否则，有sample code给你就偷笑了。但IC厂商还是会提供咨询的服务，通常我们的硬件设计完毕之前，都会把局部的原理图请相关厂商的技术人员帮忙review一下。驱动程序不会动时，通常我们会做两件事。

■　把我们的程序和CPU的data sheet一起寄给IC厂商的FAE。

■　把我们的程序和IC的data sheet一起寄给CPU厂商的FAE。

一般而言，都会得到有用的信息或线索，再不行的话就得请厂商的技术人员过来看看，或者我们带着板子、开发环境和notebook去拜访人家。”

菜鸟：“了解，难怪我老是看到您打着客户公司的名义对IC厂商威迫利诱。”

固件老鸟：“厂商的技术人员通常不只负责一家厂商或一颗IC，他们都很忙，就算不忙也会装忙，只能吓唬一下我们的产品有多大的量，或者威胁说我们的客户对他们公司的技术支持很不满意，考虑日后都不再采用他们的产品了，不然每次要他们帮忙时总是拖拖拉拉的。”

9-5-1　CPU与外围IC的连接与控制方式

菜鸟：“为了控制外部芯片，CPU总是要用某种方式与外部芯片连接，简单的可能一根GPIO就够了，复杂的则必须接Address Bus与Data Base[20]。我想知道除了我们之前讲过的GPIO、BUS、I2C之外，还有其他的连接与控制方式吗？”

固件老鸟：“业界根据不同的应用类型，逐渐发展出许多控制外部IC的标准，我们稍做整理如下。

■　GPIO（请参考《第9-3-6节：GPIO Port》）

■　BUS（请参考《第9-3节：Bus& Chip Select》）

■　I2C（请参考《第9-3节：控制CPU》）

■　SPI（Serial Peripheral Interface Bus）：SPI是一种高速串行传输总线的全双工通信协议，主要应用在CPU与转换器（ADC、DAC）、存储器（EEPROM、FLASH）、实时时钟（RTC）、传感器（温度、压力）、存储卡（SD card）等IC的串接。SPI是一种四线制串行总线接口，如图9-32所示。其为主/从结构（Master/Slave），主控IC只要4根PIN就可以串接多个slave组件，4条导线分别为

□　SCLK—Serial Clock（自master输出）

□　MOSI/SIMO—Master Output；Slave Input（自master输出）

□　MISO/SOMI—Master Input；Slave Output（自slave输出）

□　SS——Slave Selectactive low（自master输出）

▲ 图9-32　使用SPI接口串接多个外部设备及SPI时序图

Master为频率提供者（通常就是CPU或主控IC），可发起读取或写入slave组件的作业。和I2C协议不同（I2C的每个组件都有独自的ID），SPI的master必须拉低目前欲控制之slave组件的SS PIN，被指定的slave收到命令后才会开始动作（SS的作用如同之前说过的Chip Select PIN）。

■　I2S（Inter-IC Sound或Integrated Interchip Sound）：是IC间传输数字音频数据的一种接口标准，采用序列的方式传输2组（左右声道）数据。I2S常被使用在传送音频数据到DAC中。由于I2S将数据信号和频率信号分开传送，其失真率非常小。

■　CAN（Controller Area Network）：CAN为一序列总线，它提供高安全等级及有效率的实时控制，更具备了侦错和优先权判别的机制。在这样的机制下，网络信息的传输变得更为可靠而有效率；CAN在网络上支持多主机，在整个分布式系统中，每个主机负责各自局部的监听与控制，如图9-34所示。CAN被广泛的应用在各种车辆、医疗仪器与设备、航空/航海电子仪器、工厂自动化、工业机械控制等领域上。

▲ 图9-33　I2S规格图（循序传送左右声道的音频数据）

▲ 图9-34　每个节点都是一个小型嵌入式系统，通过CAN Bus互相通信

以下再详细列出CAN的诸多优点。

□　采用独特的非破坏性Bus仲裁技术，优先级高的节点优先传送数据，能满足实时性要求。

□　具有点对点、一点对多点及全局广播传送数据的功能。

□　对传输数据进行CRC及其他校验措施，数据出错率极低，万一某节点出现严重错误，可自动脱离Bus,Bus上的其他操作不受影响

□　Bus只有两根导线，系统扩展时，可直接将新节点挂在bus上即可，因此，走线较少，系统扩展越容易，改型也相对灵活。

□　传输速度快，在传输距离小于40 m时，最大传输速率可达1 Mbit/s。

总之CAN具有实时性强、可靠性高、通信速率快、结构简单、互操作性好、Bus协议具有完善的错误处理机制、灵活性高和价格低廉等特点。

此外还有个和CAN类似的网络接口，称为LIN（Local Interconnect Network），它是个低价位的初阶多任务通信网路，也是针对汽车网络所建立的局部控制标准。虽说CAN具有构建高频宽的高端网络功能，但对于简单设备的控制如座椅、车窗及后视镜控制等，LIN已足够达成高效益的通信作业。

■　其他标准：UART（RS232、RS422）、Irda（红外线）、RS485、Ethernet、USB等。菜鸟：“先暂停一下，世界上有这么多国际标准，那我们在设计硬件平台时，该如何选择主控IC与其他IC的正确连接方式呢？”

固件老鸟：“这其实不需要我们系统厂操心，CPU或SoC在设计计时就会考虑其主要的应用领域，例如，应用于多媒体播放的主控IC多半都会内置I2S或I2C的支持，主攻汽车电子的主控IC不能不配备CAN，而UART与适当数量的GPIO几乎算是所有CPU或SoC的基本配备了。”

09-06　ISR写作注意事项

菜鸟：“听您说了这么多，我觉得只要理解对的话，驱动程序的写作好像并不是那么困难耶！”

固件老鸟：“不是我老王卖瓜，驱动程序开发还是有较高的技术门槛，市面上电子设备与IC何止万千，对固件工程师而言，某个产品里用到的CPU或IC，以后可能就不会再碰到了，所以除了8051、ARM等，这些在业界已有广泛应用的CPU外，其他CPU真的不容易找到参考书，更不用说非CPU的IC了，工程师可依赖的就只有原厂的data sheet与sample code而已。虽说思想是一样的，但还是有一些靠经验累积来的小技巧。驱动程序开发有两个地方和一般程序比较不一样的地方，一个是中断处理程序（ISR），另一个是测试手法。”

菜鸟：“ISR的写作是比较麻烦，但除了要准备中断矢量表之外，所谓的ISR不就也是个函数吗？还有什么该注意的？”

固件老鸟：“写作不严谨的ISR很容易造成系统不稳定，因为硬件事件可能在任何时间点发生，即任何程序段在执行时都可能被ISR中断。不见得可以重现这个问题，所以很难调试。

此外还有一个重要的概念。CPU执行程序的基本单位是汇编语言代码，而不是C语言程序段，例如，一行‘A=B+5；’的C语言指令可能对应到好几行汇编语言指令，在这些汇编语言指令之间都可能产生中断。我举个极端的例子。

假设第二行C语言程序对应到以下几行CPU执行的基本指令。

Step01：将地址‘B’内的值置于寄存器R0。

Step02：将R0的内容加5。

Step03：将R0的值存到地址‘A’中。

在Step01之前、Step01与Step02之间、Step02与Step03之间以及Step03之后都可能会产生中断，你想想如果在这些点上产生的中断，而且ISR动到了全局变量A或B，会发生什么事情？”

菜鸟：“如果在Step01之前产生中断，而且ISR把B设为12的话，上述程序执行结果会变成12+5等于17,而不是预料中的10+5等于15。系统表现出来的现象就是所谓的‘不稳定’，大部分时间执行结果都是正确的，偶尔会出现几次错误；我可以理解你说这种问题很难调试的原因，如果我们一行一行的trace程序，只要中断不‘适时’，产生，则错误的状况根本不可能重现，也无从trace起。”

固件老鸟：“没错，所以ISR写作注意事项第一条：尽量不要在ISR内使用全局变量，如果一定要使用的话一定要做好保护，我们称这种应该受保护的程序区段为Critical Section。保护的方法很多，最简单的是在进入critical section之前，先禁止中断的产生，所以在critical section中全局变量就不会被ISR破坏，从而达到保护的效果，如程序代码9-17所示。”

程序代码9-17

菜鸟：“C语言程序会对应到多行的汇编语言指令，而这些汇编语言指令会广泛的使用内部寄存器，就像上个例子的R0一样。既然全局变量要保护，这些寄存器也应该要保护吧，否则ISR执行完毕后，所有寄存器的值都改变了，返回被中断的程序后也一定会有问题吧？”

固件老鸟：“没错，因为寄存器会一直被使用，所以不能用禁止中断产生的方法。ISR写作要注意的第二个事项为：ISR的第一个动作一定是将所有的CPU内部寄存器存储起来，一般都是存在Stack Memory内，而ISR的最后一个动作就是回复所有寄存器的值。因此，当程序自中断返回继续执行时，所有寄存器的值会和发生中断前完全一样，不会因为中断发生而影响程序执行的正确性。通常CPU会有类似pushall与popall的指令，意指将所有内部寄存器暂存于stack memory中，如果没有push-all与pop-all的指令，我们就要逐一将所有寄存器push到stack memory中，并在ISR的最后以相反的顺序将所有寄存器值pop出来，如程序代码9-18所示。”

程序代码9-18

菜鸟：“所以这样推演下去，在ISR中调用其他函数也要很小心啰，如果在ISR中调用的函数也会操作到全局变量，也可能造成全局变量被破坏的状况。”

固件老鸟：“没错，而且这种状况更难查，因为有的函数是来自其他厂商或单位的库函数，一来我们可能没source code，就算有，也不熟悉其运行原理，发生问题时根本无从查起。所以ISR写作的第3个注意事项是：调用外部函数时要确认其是否‘可重进入’（Reentry）[21]，如果一定要在ISR内调用非reentry的函数，则一般程序在使用同一个模块的函数时，一定要加以保护。

ISR内调用外部函数的问题比较复杂，特别是在我们不熟这些函数的设计原理时。前面谈的只是一两个全局变量所造成的影响，当我们在ISR内调用一个函数，是否真的可以确定这个函数中用了几个全局变量，以及这些全局变量又被其他哪些函数使用？如果无法确认，则一般函数中使用到‘可疑’的函数时都要保护。”

菜鸟：“其实最保险的方法就是尽量不要在ISR内使用未保证为reentry的函数。但这样一路讨论下来，我们得到尽量不要用全局变量与函数的结论，如此一来，ISR内可以做的事情岂不是很少？”

固件老鸟：“ISR内的程序是越简单越好，ISR写作的第4个注意事项是：ISR的程序或执行时间尽量不要太长，一来不易掌握全局变量以及外部函数的使用，二来ISR执行时通常不允许其他中断产生。如果ISR执行时间太长，也就是上层程序被暂停的时间过长，使用者会觉得系统的性能很不稳定。

ISR写作的第5个注意事项是：ISR中调用系统功能时必须谨慎，尤其是与Task/Thread/Process切换有关的功能。有些应用需要在ISR内唤醒某个Task，使其立即去执行某件重要工作，此时，必须在ISR内调用诸如wakeup-task、send-message等这类的系统功能。这些功能无可避免的必须修改系统表格，如果发生中断的当下也正在操作这些重要的表格，则势必引起系统死机。要避免这种状况，一是做好critical section的保护，二是调用系统提供给ISR专用的系统功能。

最后，我会建议ISR与其他程序的系统架构为：ISR尽量不要牵涉到复杂的算法，它的任务只是负责弄清楚发生了什么硬件事件（举例来说，哪个键被按下、转换完的AD值是什么、哪个Timer已经到时等），然后将硬件事件送给上层的程序，交由系统或应用程序决定要如何处理这个硬件事件。具体的做法是ISR会将硬件事件的细节存储在诸如message-queue的数据结构中，而上层程序会从message-queue中取得硬件事件的信息，并做出合适的处理。”

菜鸟：“在这种系统架构下，message-queue是ISR与其他程序的唯一桥梁，显然所有操作message-queue的程序段都是必须保护的Critical Section，但整个系统也只有message-queue相关的函数需要保护而已。这样的架构使得ISR的内容只是简单的硬件操作，至于该如何处理硬件事件则是上层系统或应用程序的事情（例如，使用者按了PLAY键，系统会开始播放MP3，驱动程序只要负责告知上层程序PLAY键被按下了即可）。”

名词定义——Reentry：可重入的程序代码指的是一段可以被多个任务（task或ISR）同时调用，而不必担心数据会被破坏的程序代码（如一个函数）。即可重入型函数在任何时候都可以被中断执行，过一段时间以后又可以继续运行，而不会因为在函数中断时被其他的任务重新调用，而影响函数中的数据。

09-07　驱动程序调试

固件老鸟：“最后说到驱动程序的debug。一般程序的debug手法，像是设断点、单步执行、观察变量及存储器内容，检查程序的执行流程等，应该不用我再来教你了吧？虽然程序是执行于机器上，我们还是可以通过ICE连接来调试，也许开发环境不像Visual Studio那么功能齐全，但基本的调试功能应该都能使用。但驱动程序有时候必须遵守严格的Timing规定，此时，设定断点或一行一行地trace程序都帮不上忙。

还是用我们之前提过的例子来解释（请参考《第9-3-7节：NOP与实作Busy Waiting的时间区段》），程序的逻辑相当简单，先将CS PIN设为Low，然后等待T1的时间，从P#1 PIN取得值之后，再把CS PIN设回High，如图9-35所示。如果这么简单的程序在机器上却无法正确运行，你会用什么方法来debug?”

▲ 图9-35　时序图范例

菜鸟：“如果程序逻辑无误的话，应该就是Timing的问题吧，我现在似乎可以理解您刚刚说的道理，设定断点让程序停下来后，整个Timing都不对了，这个状况确实无法用trace code的方式来检查。面对这个问题，我想我应该会先把计时的fimction再仔细检查一下。”

固件老鸟：“如果程序是你自己写的，就算有错也经常会看不出来，因为你认为对的做法可能是错的，所以请其他员工帮忙，或者请主管召开code review会议都是比较好的做法。如果一堆人还是看不出来怎么办？别忘记了，固件工程师面对的可能是不稳定的硬件，搞不好你的程序没问题，是硬件接线错误，程控了半天，结果命令没传到IC去，当然得不到正确的回应。”

菜鸟：“所以您的意思是说等程序都查不出来时，就要请硬件人员帮忙检查一下硬件电路设计吗？”

固件老鸟：“不对！我认为固件工程师应该更积极地主动出击。在这个例子中，你要如何证明你的驱动程序运行是正确的？还记得这个驱动程序的依据是什么吗？应该是IC data sheet内的时序图和我们的硬件原理图吧！要证明驱动程序正常运行最直觉的方法就是能在正确的PIN脚上量到和时序图一样的信号。在这个例子，量信号可以得到很多有用信息，假设程序执行过后，IC端却没有量到信号，那表示要不是程控错脚位，就是硬件的接线有问题。此外，用示波器可以测量到很精准的时间，如图9-36所示。在这个例子中，使用示波器可以清楚地测量到程序是否正确地依循IC对时间的规定，这比一行一行trace程序，或找一堆软件工程师来看程序快且有效的多了。所以对固件工程师而言，示波器是不可或缺的工具，基础的示波器操作更是和写程序一样重要的职业技能。”

▲ 图9-36　使用示波器测量某PIN脚的信号

菜鸟：“职业技能？会不会有点夸张？我想，现今信息本科毕业生会使用示波器的比例恐怕不超过10%吧？”

固件老鸟：“10%?你可能太乐观了！虽然驱动程序是每一项电子产品不可或缺的基础建设，在电子业挂帅的台湾地区业界，为了加大硬件价值，对优秀的驱动程序开发人才更是求才若渴，但固件工程师从来就不是信息本科毕业生工作的首选。根据我面试这么多应届毕业生的经验，每个应征者听到示波器、原理图等无不眼神闪烁、面露惊慌神色，还记得你来应征时不也是这样吗？我早已接受这样的状况，其实固件工程师必须具备的硬件知识与技能也不太复杂，等新进人员进来后再花些时间培训就是了。你进来都那么久了，还会觉得示波器学习起来很困难？原理图怎么也看不懂吗？”

菜鸟：“我之前请硬件工程师教了我一些示波器的基本功能，确实也没那么难，因为在开发驱动程序时也不会用到太复杂的功能。我觉得人对不同领域的事物心生恐惧是很自然的，然而，固件工程师是软件领域最接近硬件的人，如您所说，固件工程师除了开发驱动程序之外，还肩负帮忙硬件调试的责任，所以整个开发团队中最不该推拖什么是软件问题、什么是硬件问题的人就是固件工程师，他必须协调软/硬件间的矛盾，让整个系统能够有个稳定的平台。”

固件老鸟：“没错！开发应用程序的人员可以抱怨系统不稳定，开发系统程序的人员可以抱怨平台不稳定，但固件工程师不能只是抱怨硬件怎么那么烂，反而要尽力协助硬件人员找出问题所在。

话说回来，有关驱动程序的debug，如果操作得当，示波器应该可以帮忙找出大部分的问题，除了时序与信号的正确性之外，例如，电源是否正确、是否有噪声、信号的频率等都可以量得出来，好一点的示波器有多个channel，可以同时测量多个PIN脚的信号。”

菜鸟：“我看我们实验室里的示波器好像只有4个channel而已，如果要测量超过4个以上PIN脚的信号时该怎么办？例如，我想测量Data Bus或Address Bus这种同时8条、16条或32条线一起动作的信号，总不可能分开量吧！”

固件老鸟：“我们之前就碰过一个例子，系统运行起来就是不稳定，我们怀疑是SRAM不稳定所导致，我们想要观察某一段时间内SRAM的Data Bus以及Address Bus的信号有没有问题，示波器当然无法达成这个任务。此时就只好请出最后神兵——逻辑分析仪（简称LA），如图9-37和图9-38所示。这是一台远比示波器昂贵的仪器，自然功能强大，它能把一段时间中多个PIN脚的信号都记录下来，研发人员可以事后慢慢分析。”

▲ 图9-37　逻辑分析仪

固件老鸟：“LA虽然功能强大，但一般的状况确实没有示波器方便，要用到LA调试的问题都是大问题，所以我们并不常把LA搬出来用，对固件工程师更是如此，基本上会用示波器就可以了。”

逻辑分析仪的操作并不在本书的范畴之内，在此只需要有一个概念。这些仪器的操作并不困难，学习相当容易。只要可以掌握驱动程控某IC的流程，应该就可以知道该测量哪些PIN脚，以得到什么样的信号才是正确的。一个好的固件工程师，绝对不要害怕向硬件工程师请教，并且要提醒自己时刻充实硬件知识。此外，研发团队的高层也应该清楚硬件与固件工程师必须紧密的合作，在平台开发的这件任务上，不应该有软硬相争的现象，把平台弄稳定是硬件工程师与软件工程师共同的责任。

▲ 图9-38　逻辑分析仪的测量结果

09-08　结论

其实驱动程序的写作并不困难，重要的是必须对计算机系统的运行原理要有完整而清晰的概念，特别是CPU与外围设备的沟通方式，如Bus、Chip Select、Interrupt、DMA等。固件工程师在职业生涯中要碰到的CPU与IC不知多少，这些IC当然各有特色，控制方式自然也大不相同，但无论这个IC的功能如何复杂，重点是它内部运行方式都不是我们该知道的，只要仔细分析过，一定可以用基本的计算机系统运行原理来说明如何控制这颗IC。

举例来说，即便复杂如无线网络控制器，驱动程序还不就是通过控制I/O Port、中断、DMA这些基本的操作与它沟通，至于沟通方式则必然清清楚楚地写在它的data sheet里，这是规定，不是学问，工程师照做就是了。无线网络系统真正复杂的是驱动程序以上的通信协议与应用层，如果非要说驱动程序有什么比较麻烦的地方，应该就是驱动程序稳定度的要求较高、调试与测试比较困难等，驱动程序本身并不复杂。

此外，驱动程序是最靠近硬件的软件，所以固件工程师应该是软件团队中‘最硬’的人。驱动程序与硬件设计的调试有时可以看作是同一件事，和硬件工程师共同解决问题是固件工程师最大的乐趣与挑战之一。

[1]　Local变量使用Stack存储器，且在资源受限的系统中，Stack size不可能是无限制的，如果真的有这种需求，最好先去问问系统设计者，系统中可用的Stack size是什么。

[2]　在《第12-2节：Stack》会再提到Stack，其中包含这种问题的解决方法。

[3]　一般函数调用时，CPU只会将返回地址存在Stack中，所以指令“return”只会从Stack中取得返回地址，但中断发生时，CPU除了会将返回地址存储在Stack中，还会将PSR寄存器也存储到Stack里，所以当ISR结束时就必须用另外一个指令（Interrupt Return）返回中断发生点，不能用“return”指令返回，否则Stak中会残留一个值，以后运行将会出问题。

[4]　所谓X86系列CPU泛指用于PC上的80386、80486、Pentium、P2、P3、P4等的CPU，其实X86 CPU家族还有一个16 bit的CPU——80186，指令集与基本的X86 CPU兼容，广泛使用于较低端的嵌入式系统。

[5]　你可以想象PC上的WinAmp这类的软件播放器，它们是用纯软件去解码MP3，这需要极大量的数学运算，在PC上当然不成问题，但在嵌入式系统的CPU上，恐怕就心有余而力不足了。市场上有许多解码MP3的IC, CPU只要将MP3文件的内容设法传给这颗IC，它会负责译码的动作；这种架构我们称为硬件解码。

[6]　可先不理会这里提到的技术名词，重点是Driver API必须将这些硬件特性都包装起来。

[7]　这是一个很有警惕性的案例，本节后面还会再详细说明这个案例。

[8]　SPI是一种速度较快的序列传输通信协议。而I2C通信协议可以将多个chip串在一起，CPU不需要和每个chip直接连接，当要控制某个chip时，只要在I2C bus上将设备编号以及命令传出去即可，这对CPU PIN脚不够，但要控制多个chip的系统相当有用。

[9]　这是笔者实际上的经验谈，可能有些争议性；若你不认同这样的做法，可选择忽略此段文字。

[10]　最有名的中断控制器就是Intel的8259 PIC （Programmable Interrupt Controllers）系列芯片，从最早期XT/AT 一直到现在的PC，都看得到这颗中断控制器的踪影。

[11]　存取存储器实际运行牵涉到Data Bus、Address Bus，以及一般人比较不清楚的chip select，我们将在本节的后面陆续说明。

[12]　实际的情形当然并非如此简单，不同的指令需要不同的执行时间，而这些时间都是clock的整数倍，即clock是CPU最小的时间单位。

[13]　BUS——台湾翻译成“汇流排”，大陆则翻译为“总线”，笔者认为都不足以表达其含义，文中还是直接用英文名称来称呼。

[14]　CE （Chip Enable），和我们说的CS（Chip Select）是相同的东西。

[15]　I2C接口是由Philps所定义，CPU只要通过两根I/0 Port模拟的序列bus，就可以操作bus上的IC，最高速率100 Kbit/s，25英尺，最多可支持40个设备。

[16]　“nop”也是CPU的指令，CPU执行指令时，PC（Program Counter）寄存器一定会跟着改变，CPU才知道要到哪里获取下一个命令来执行。

[17]　实际上，HAL规范驱动程序应包装给上层调用的API，上层要使用硬件功能也只能调用HAL中的API。HAL对嵌入式系统的可移植性有绝对性的帮助，将在《第10章：设计硬件抽象层》专门说明HAL。

[18]　市面有IC专门用来作低电压检测，当电压低于某临界值时会立即对CPU发出中断，这种IC称为Brown-Out-Protection IC，当然也可将此功能作进SoC内。

[19]　一般把低电压事件称为“Brown-Out”，电源被拔掉称为“Black-Out”。

[20]　并非只有存储器才会用Address+Data Bus与CPU连接，有些LCD Controller因为内置Video RAM，所以也必须用Bus与CPU连接。

[21]　所谓的Reentry的函数，就是当其执行到一半时发生中断，或所在的task被暂停并切换到其他的task，即使此函数在ISR或其他task中又被调用，当返回继续执行时，也不会影响结果的正确性。根据本节的讨论，基本上，尽量不要用到全局变量或调用其他会用到全局变量的函数，即可保证该函数是可重进入的。

Chapter 10

设计硬件抽象层

时间：10月01日　 登场人物：菜鸟、项目经理（PM）、固件老鸟、系统老鸟

PM：“经过driver与hardware team这阵子的努力，我们的硬件板子已经debug的差不多了，在做第一次系统整合之前，我希望与两位（系统与driver team）主管再共同确认一遍驱动程序与系统间的接口是否完整，并安排驱动程序code review meeting的时间，以确保驱动程序都有按照HAL design document去做。

如各位所知，这个项目才进行到一半，公司的市场与销售部门已经急着扩张战果，拿着板子与简单的demo program到处吹牛去了。我们在系统架构设计上所做的努力即将看到成效，我希望之后类似的项目都可以leverage这个项目的成果，如果大部分新的客制化功能都能通过模块切换来完成，则新项目的资源需求与成本可大幅降低。硬件设置是新项目最可能变动的部分，所以HAL（硬件抽象层）的设计在Implementation阶段一定要彻底贯彻！”

菜鸟：“Excuse Me，各位长官，容我打断一下。所谓HAL就是driver的API吧！我们的固件工程师当然会遵循设计文件，将驱动程序包装出这些API来，但HAL真的如此重要吗？公司其他部门的项目连所谓driver API都没有，还不是做得好好的？”

固件老鸟：“driver是系统平台的基础，而HAL能使系统架构更为稳定。叫你念书不念书，问这种问题！”

菜鸟：“如果市面有书可以参考就好了，我也是对着我们的系统架构图端详了好一阵子，始终无法参透你们这些主管如此重视HAL的原因，而且您刚对HAL的描述也未免太形而上了吧！系统架构已经有点虚无飘渺了，什么又叫做‘使系统架构更为稳定’？请恕小徒资质愚钝。”

PM：“系统架构怎么会虚无飘渺呢？之前花了那么多时间，跟你说明系统架构能为团队带来的优势，都忘光了吗？”

菜鸟：“我当然知道系统架构的重要性，我时不时想着如果由我来做，如何才能设计出一个好的系统架构？我认为只要有时间，程序技巧与某某技术规范都能够很快地熟悉并上手，而且程序与技术文件都是有形的，可以参考很多资料与范例，但系统架构的好坏会严重影响整个系统，却无绝对的评量标准。例如您刚说的‘更稳定’，也是一个相对的标准，HAL能让系统架构更好，但到底能好到什么程度，应该也很难用言语表述吧！”

PM：“呵呵，我误会你所说‘虚无飘渺’的意思了，系统架构的精神的确不容易说清楚。但所谓有图有真相，一张好的系统架构图胜过千言万语，之前跟你提了一个RTOS/Embedded-Linux共享系统架构的例子（请参考《第6-8节：案例研究（Ⅱ）一RTOS/Embedded Linux通用的系统架构》），那个系统架构颇具代表性，我们再把它拿出来看看吧！

从图10-1所示的系统架构图中，你能看出HAL的功能吗？”

菜鸟：“这个架构同时要支持两种系统（RTOS与Embedded Linux），但我们又希望driver可以共享，不要有两套，否则maintain将会是个大麻烦，所以在driver上再加了一层HAL，所谓HAL就是将硬件抽象化，Linux可以基于HAL再往上开发自己格式的driver。

HAL的另一个好处是就算硬件配置做了修改，也只需要修改driver，两方系统都不需要大更动。”

系统老鸟：“基本上你对HAL的思路还算清楚，但我并不同意你刚刚说系统架构是‘虚无飘渺’的说法，我认为系统架构事实上也能够让所有人很具体地了解它到底是什么！

我们之前不是谈过吗？系统架构图里的每一个方块对应到设计文件里的一个模块，而每一个模块必须对应到source tree中的一个目录，一切都是清清楚楚的，如图10-1所示。所谓的更稳定也相当直觉，如果你将图中HAL那个方块抽掉，把各个驱动程序方块改成有大有小，如图10-2所示，是不是感觉上层的方块堆栈随时会倒塌的感觉？”

▲ 图10-1　系统架构图范例

菜鸟：“您说的没错，但反而让我越听越糊涂了，图10-2所示看似很直觉，但我们的系统实际上是一堆程序啊！如果都是一堆程序，怎么会‘倒塌’呢？”

▲ 图10-2　根基不稳的系统架构图范例

PM：“对我来说，各个driver没有一致的规范就是最大的风险，若有的driver只实现了该设备的少部分功能，而有的driver则连系统的工作（与硬件无关的工作）都做完了，则上层的系统与应用程序必然无所适从。不要说下个项目不敢用这组driver来做客制化的修改，我都可以想象这个项目的系统与应用程序必然写得乱七八糟，而且项目运行期间，driver team和其他单位必然争执不断（出了问题很难判断是哪一层的问题），导致最后项目失败的机会变高，我想这就是因为系统架构根基不稳所导致的‘倒塌’吧！”

菜鸟：“感谢3位老大的讲解，我似乎有点懂了。但如果HAL这么重要，为什么我们在研究其他嵌入式操作系统的架构时，并非所有嵌入式操作系统都包含HAL呢？”

10-01　由eCos & amp;Android的系统架构谈起

系统老鸟：“一般通用性的嵌入式操作系统，如VxWorks、μCOS II等，可能应用在截然不同的产品上，硬件配置南辕北辙，所以很难去定义通用的HAL，但我们的产品线定义颇为明确，不会要你拿手机的系统去做飞机。若希望我们的系统能够具有可扩展性，可以用较少的effort来执行未来的项目，那么，HAL就不可或缺。”

固件老鸟：“我说得没错吧！就是叫你平时要多念点书，Open Source的嵌入式操作系统——eCos，它的系统架构就明确定义了HAL这一层（如图10-3所示），μμCOS名字里的C表示Controller，那你知道eCos的‘C’代表什么意思吗？”

▲ 图10-3　eCos系统架构

菜鸟：“是Controller或Communication之类的吗？”

固件老鸟：“错！是Configurable的意思。表示这个系统具有相当高的‘可配置性’，除了system或middleware需要可配置或抽换外，希望硬件（包含CPU）及驱动程序也是可容易配置的，要做到这点，自然就会需要一个硬件抽象层。

图10-3所示可以看到，因为eCos还算是个general purpose的嵌入式操作系统，所以能被抽象化的硬件特性并不多（从图10-3中看到eCos的HAL只包含与中断有关的部分）。”

PM：“我们来看另一个例子。最近火红的手机系统Android，其系统架构如图10-4所示，因为Android的应用锁定在手机上，且智能手机的硬件架构差异有限，所以你可以看到Android的HAL中就包含了许多硬件设备。

举例来说，不同的手机制造商可以选用不同的GPS硬件模块，只要能按照Android HAL中对GPS设备所定义的行为模式（如送出位置信息的格式），并包装出指定的API，则其他程序都不需要修改，内置的导航应用程序应该就可以正常运行。”

▲ 图10-4　Android系统架构图

菜鸟：“用Android的例子，的确比较容易理解HAL所能带来的好处，如果希望某个系统可以重复在不同的项目中使用，而又不失可修改硬件配置的弹性，则定义标准的硬件抽象层的确是不二法门。”

系统老鸟：“其实Android前几个版本对HAL的定义并不完整，但瑕不掩瑜，至少Google努力通过完善Android的系统架构，以减少厂商的开发手机产品的effort，并增加厂商使用Android的诱因。我们也应该这样思考，若系统的架构可以缩短客户产品Time To Market的时间，无形中也增加了自己团队的竞争力。”

菜鸟：“对了！之前在学校因为要做计划，选购过ARM的实验版，除了板子外，这个套件还附送了porting Linux所需要的软件，其中有一个套件叫做BSP（Board Support Package）， BSP按照Linux driver的规范实现了这个板子上所有设备的驱动程序。这个BSP是不是和HAL的概念很像？”

10-02　HAL vs. BSP

固件老鸟：“这个问题由我来回答吧，以下是BSP的定义。

■　BSP（Board Support Package）：BSP介于主板硬件和操作系统间，其功能与PC主机板上的BIOS相类似，主要功能为完成硬件初始化，并切换到相应的操作系统。

■　BSP是相对于操作系统而言的，不同的操作系统会对应于不同定义形式的BSP。例如，某一CPU上的VxWorks BSP和Linux BSP，尽管实现的功能一样，但写法和接口定义是完全不同的。

■　BSP单纯只支持某个硬件板子的配置，无法用于其他板子、CPU或操作系统。举例来说：微软定义BSP是由Boot-loader、OAL（OEM Adaptation Layer）、Device Driver及Run-Time Image Configuration File这几个组件所构成。

所以BSP和HAL是不一样的东西，应用时机也不同。BSP是针对某个板子的硬件配置，进行驱动程序套件开发，并包装出某种OS的接口，让使用者拿到板子后，可以很快地将OS与应用程序移植到这个板子。Linux、VxWorks、Microsoft Windows Mobile都有BSP这样的说法。

相较于HAL，BSP是一专用于某个板子、某种系统的一组驱动程序套件，但HAL是一种概念、一组作为系统标准的API。嵌入式系统基本上是个分层架构，上层通过下层提供的API与下层沟通，而分层的好处就在于只要API不变，上层不需要知道下层的实现方式与细节，就算下层模块整个切换或全部改写，上层程序都不需要修改。

举例来说，我们知道电子产品对生产成本相当敏感，所以更换电子零件的机会很大。此时，driver势必要重写；如果能将影响范围仅局限于该driver，只要新的driver可以按照HAL规范包装出API来，则上层系统与AP完全不需要修改。”

菜鸟：“了解！所以对我们来说，最重要的是一定要制定出HAL的标准。至于最后按照HAL API所实现的所有driver，是不是要称作BSP就无关紧要了。”

10-03　为什么会需要HAL?

菜鸟：“对那种具有未来延续性的项目而言，HAL确实有很大的帮助，但如果我们接的是一个特殊的产品开发项目，几乎不可能有其他的厂商会有类似的需求，或是那种一次性或末代产品的项目，这样的case是否还需要设计HAL? ”

系统老鸟：“这是一个好问题！你想想，像这样的项目是否仍要执行项目管理、质量管控？再把问题缩小一点，你在为这样的项目做系统设计时，是否仍需要考虑到模块化？”

菜鸟：“我当然知道无论什么项目都需要做项目管理、质量管控与模块化，也完全理解HAL的重要性，但要设计与实现HAL，毕竟还是会增加项目执行的effort。记得上次我们的某位工程师，对客户不小心说溜了嘴，说我们正集中人力在定义HAL API，客户听了大吃一惊：

‘我们的项目这么赶，你们不是一直喊没时间、没人力，为什么还要做这个叫做HAL的东西？我们其他的开发商总说像我们这样规模的项目根本不需要HAL，不然就是说HAL会用掉太多时间人力，总体来说并不实际！’

业界似乎仍普遍认为当系统开发时，定义HAL是困难且耗费人力的。如果我们对HAL的坚持是对的，为什么业界其他公司会有这种误解呢？”

系统老鸟：“首先，我不认为目前的电子产品市场会有那种在开发阶段就认定没有下一代的产品。若电子产品无法进行第二代开发，都是因为产品定义错误、推出时间不对、质量瑕疵、销售不力等原因，不会有厂商做生意不希望第一代产品大卖，然后乘胜追击，推出功能较强或毛利较高的第二代。若第一代真的大卖，其他竞争者见有利可图，一定会马上投入研发资源；此时，反而是投入第二代开发，拉大领先优势的最佳时机。

对系统开发商而言，第一代系统花的effort最大，当然希望可以将研发成果充分利用到第二代，乃至第N代，让开发成本逐次降低。另一方面，当研发团队完成了一个项目，表示公司累积了新的技术资产，既然如此，岂有不敲锣打鼓地扩大战果，设法把‘新产品’推销给其他客户的理由？如果一个项目没有开发新客户的潜力，那么公司高层必然会思考这个项目的投资报酬率是否合理，企业绝对不会去开发无利可图的项目。

而且将来的产业趋势谁能完全看得清楚？所以我们研发单位还是应该按部就班，维持嵌入式软件系统的reusability与portability等基本特性。

HAL反对者的主要说法为：担心自己无法制定出‘正确的’HAL接口，或者若制定出‘错误的’接口，将会导致资源浪费与进度延后。其实这些担心都是多余的，接口设计本来就是由简至繁的工作，没有任何人可以保证自己一次就可以订出完美的API，重点是HAL必须随新项目的需求不断演进，而且建立标准（可以把HAL当作驱动程序的标准）绝对比没有标准好！

此外，实际上HAL并不会为研发团队带来太多的overhead，我们本来就应该要为每一个驱动程序模块制定API，不是吗？而HAL只要求固件工程师多思考一些可移植性的需求而已。

许多工程师总是抱怨写文件浪费时间，而有些工程主管也主观认为code review会打扰工程开发，这些讲法都似是而非。‘把规划、设计、review工作尽量做好，就能在项目后期省下可观的时间’这个结论经产/学界严谨的统计与研究得来，不是我们自己编的，最害怕连主管的理解也不正确，或者临战时慌了手脚，被工程师‘绑架’，任由系统逐渐混乱，最终毁灭！”

PM：“在目前业界中，混乱不一定会导致毁灭，因为许多成功的产品，也是由‘混乱’的团队，用‘混乱’的系统完成，表面上风光，实际上，这些成果都是由工程人员的血汗拼出来的！

原本可以在设计阶段找出的问题，要拖到测试阶段变成难解的bug才去处理。原本换掉driver就可以完成新平台的移植，却因为没有HAL，导致工程师需要熬夜赶工修改架构，还使得同一系列的产品必须开发多套系统，不知以后的maintain工作又要耗掉工程师多少血汗。

我随时都会提醒自己：千万不要让优秀的工程师以无效率的方法工作，那会证明自己是个无能的主管。

好了，言归正传，HAL的重要性毋庸置疑，不需要再讨论下去了。接下来我们来说说实际执行HAL设计时，到底有哪些工作要执行？”

10-04　HAL是否会增加开发的难度？

固件老鸟：“与制定其他API—样，HAL的设计需要实现者与使用者共同参与。系统架构中与HAL相接——下面是驱动程序与硬件，上面是系统。也就是说，对下我们必须检验查看目前硬件配置的能力与限制，以及将来可能的扩展性，对上我们得倾听系统软件的需求。”

系统老鸟：“简单地说，HAL就是我们系统的‘硬件’，而‘硬件’的功能就是它所提供的API，即所有上层的程序完全不需要知道硬件与驱动程序的细节，只能通过HAL来操控硬件。按照这样的逻辑，通常我们实现HAL的流程如下。

Step01：定义HAL的规模（Scope）。根据产品特性，分析上层的系统与应用程序需要哪些硬件功能，这些需求就是HAL必须要包含的基本模块，然后再根据硬件配置，分析是否仍有目前系统没用到的硬件功能，这些功能可能在下一代产品就会被用到，我们可为其设计相应HAL模块的API，但在本项目内可先不implement。

Step02：定义HAL API。如果可以的话，最好从硬件到应用程序的开发小组，都要派人员参与HAL设计工作，否则至少要有资深的系统与固件工程师参与，不能仅由一个部门闭门造车。一般定义HAL API步骤为：

□　系统工程师说明系统需求，包含系统对硬件事件的处理方式。

□　固件工程师根据硬件功能，提供第一版的HAL API定义文件。

□　系统工程师协同固件工程师，逐一review HAL API。

□　在HAL API立项前，必须针对项目中的所有软件工程师做详细报告，收集建议，并做必要的修正。

Step03：由固件工程师负责实现所有HAL的功能，并执行每个模块的单元测试。

Step04：固件工程师release第一版HAL library，由系统工程师负责整合测试，若有需要。各个部门随时可以提出对HAL API增修的建议。”

菜鸟：“当HAL的设计立项后，固件工程师就是照着文件去implement，而系统工程师就被规范只能用HAL API来控制硬件，并不会增加工程师们在实作阶段的工作量。就算系统架构中没有HAL，一般driver的开发流程和上述步骤也差异不大吧，所以我觉得那些反对HAL的杂音，实质上只是为了逃避将所有驱动程序模块化的借口罢了！

若说有什么困难的地方，应该就是上面的Step01吧！HAL的scope必须考虑到未来的扩展性与可移植性。本菜鸟见不多、识不广，对未来实在无法想得周全。”

PM：“没有人可以正确预知未来，但不能因此而放弃追求好的改变的勇气。我觉得你还是把HAL设计想得太难了，HAL不过是驱动程序提供给上层程序的‘API标准’罢了，只要HAL API的架构具有可扩展性，API不够再补就是了。微软在上个世纪末设计WIN32 API时，怎么可能想得到Windows 7要应付多少各式各样的应用程序？所以WIN32 API当然是不断扩展的，但必须坚持一点：尽可能的往下兼容，让‘旧程序’依然能够在‘新系统’上执行。

在嵌入式系统上可能会有code size的issue，不可能无限制地扩展HAL API，但嵌入式系统开发的弹性也较大，如同刚刚Step01里谈到的，我们可能定义了某些API，但却没有implem entation。总之，HAL的目的就是希望使用同一套系统，就能够进行同一系列的产品开发，只要原则不变，实际上你很容易就可以想到一些技巧处理碰到的问题。”

10-05　HAL实例

PM：“一早光忙着开示你这个菜鸟，差点耽误了正事。这个会议的目的是请大家来review HAL设计的，先请driver team leader根据HAL设计文件里的章节，每个driver模块、每个API逐一走一遍，而system team leader则以HAL使用者的角色，确认API是否足够、各个driver的行为是否合乎系统的需求，必要时，我们可以把目前driver的code调出来review。”

菜鸟：“那，我要做什么？”

PM：“帮忙记录会议中提到任何需要修改的项目，以及所有衍生出来的工作项目（Action Items）。除此之外，好好学习，看看HAL到底是怎么一回事，是否真的如其他人想象的那样困难？”

10-5-1　HAL基本设计原则

固件老鸟：“基本上，HAL设计文件是以driver来区分章节，不同的driver则由不同专长的工程师负责设计与编写。因此，设计文件的第一章是‘HAL基本设计原则’，第二章是‘共享数据结构与常数定义’，这两章在设计之初均由本人负责完成。

以下是所有负责设计HAL API的人员应共同遵守的原则。

■　往前兼容（新版HAL的API可以变多，但不能变少）。

■　HAL旨在对硬件做抽象化，不应与任何系统有直接关系。若OS对driver有特殊的需求（如Linux有制式的driver写法），应由系统团队根据需求自行在HAL之上，再往上包一层high level driver。

■　Driver模块化，尽量降低driver之间的耦合程度。这个原则相当重要，因为我们设计HAL的主要目的之一，就是未来项目的硬件配置可能改变，而我们只要切换驱动程序即可。若driver间耦合度过高，则系统的可移植性与HAL的优势将大打折扣。

■　每个driver分别设计，但章节内的结构必须保持一致，应该包含以下信息。

□　数据结构

□　API

□　控制流程或状态机

□　会产生的硬件事件，以及事件传递流程

■　每个driver模块应该尽量包装出以下的API，以保持所有driver模块的API风格一致，使得系统在使用HAL API时，比较不会发生误用的状况。

□　Open（）：执行该设备的初始化工作（包含硬件初始化、取得driver所需之buffer、数据结构或配置的设定）。

□　Enable（）：driver开始运行。

□　Disable（）：driver暂停运行。

□　Close（）：关掉硬件设备，还回已配置的buffer，重设（Reset）driver的数据结构或配置。

□　Set_Power_mode（）：设定该设备的power mode（如full run、idle、sleep mode），即便有些设备无法实现这么多种的power mode，甚至有些设备根本不需做电源管理，HAL设计者还是可以考虑为此设备包出空的Set_Power_mode（）函数，以保持所有driver API的一致性。

■　driver与power manager的配合：driver仅提供电源管理的mechanism，而电源管理的policy由系统的power manager统一管控（请参考《第9-3-8节：Power Management & Low Power》），即HAL中的每个driver模块都必须配合power manager的需求，例如都必须实现Set_Power_mode（）这样的API。

■　HAL的每个driver模块都必须遵守共同命名规则与API style。

■　统一定义HAL的系统配置（Configuration）：这些配置必须开放给系统使用，使得本项目不需要的driver模块或功能不会被link进来，请参考以下程序代码。

10-5-2　HAL的系统架构

固件老鸟：“刚刚提到driver之间的耦合程度越低越好，但driver间难免也是会有阶层关系，例如I2C driver与FM Module Driver（可播放FM广播的芯片，与其他必备零件整合在一个比拇指小的板子里），主控IC通过I2C对其FM Module下命令，FM Module就会根据设定开始接收与播放FM频道。以HAL设计的角度来看，虽然I2C在这个项目只用来控制FM Module，但其他项目可能会用I2C来控制其他芯片，所以I2C必须是独立的driver，而FM Module Driver则通过调用I2C的API来运行。

就如同系统设计会做出各个模块之间的关系图，包含模块间的调用关系、参数传递的规范、共享全局变量的规则、模块间运行的流程与顺序等，我们可以把HAL当作一个独立的子系统，所以在进入个别driver的设计阶段前，HAL设计文件必须比照系统设计文件，事先定义出所有driver间的关系，作为设计个别driver时的规范。”

PM：“在启动单个driver的设计工作之前，我们已经将上述HAL的基本设计原则都review过了，等下开始review各个模块API时，应该不至于看到意料之外的东西吧！”

10-5-3　驱动程序与系统的沟通机制

系统老鸟：“系统与HAL的沟通方式有3种，如图10-5所示。

▲ 图10-5　HAL与系统沟通的机制

■　HAL API：系统可以直接调用HAL开放的API。

■　ISR & Hardware Event：当硬件产生中断时，相应的ISR会被执行；当ISR处理完毕后，可将硬件事件抽象化，包装成硬件事件，往上层系统传递。以低电压事件为例，驱动程序自硬件读取的是AD值，必须将其换算为电压值（此举即为抽象化），并对上层传递此硬件事件。

传递硬件事件的方法很多，例如

□　驱动程序可以把一个全局变量当作flag，当上层程序发现此flag的值改变了，即表示发生了某硬件事件。这种做法的时效性较差，且必须注意Critical Section的保护。

□　在ISR内调用系统的功能，如send_message（）、wakeup_task（）等，这种做法是比较普遍的做法，但此举破坏了HAL必须与上层系统无关的规范。

□　较好的做法是HAL不决定传递硬件事件的机制，改为提供API，让系统可‘注册’用以传递硬件事件的函数（称为callback function），ISR会在适当时机调用这些callback function，至于系统如何处理硬件事件，ISR并不会知道。

■　Callback：HAL提供注册callback function的API，并明确说明调用此callback function的时机。系统只要传入function pointer，HAL即会在上述时机调用这些callback function。例如上面所说的实时处理硬件事件，或是当HAL准备关机时，会去调用系统预先注册的callback fimction，让系统有机会做一些处理（如存储重要信息或目前的执行状态）。

10-5-4　Driver API

固件老鸟：“接下来，我们要开始review各个driver模块的API。刚刚说过，每个模块的设计文件都会保持相同的style，这会让大家看这份文件时能够很快就进入状况。

首先我们要看的是HAL里一个重要的driver——Audio。按照设计文件style的规范，必须先描述这个模块中所有API的关系，如图10-6所示。

▲ 图10-6　HAL设计文件中的流程图范例

（以上只是范例，你不需了解这张图里各个API的作用。这里只是要强调：每个模块的设计文件不能只有API描述而已，最好还要有API间的流程图、状态描述等。）

■　固件老鸟：“接着，我们来逐一 review这个模块会开放给上层的API。”

■　（以下只是范例，你应该已经很清楚如何描述一个function，函数的名称、用途、参数、返回值等都不可缺，如果有sample code就更好了。）

■　HalAudEn

程序代码10-1

Parameters

Parameter

In/Out

Comment

E_HAL_AUD_MOD eHalAudMode

In

Audio mode

Hal Audio Mode

Return value

Value

Comment

None

■　HalAudDis

程序代码10-2

Parameters

Parameter

In/Out

Comment

None

Return value

Value

Comment

None

■　HalAudSetSampleRate

程序代码10-3

Parameters

Parameter

In/Out

Comment

E_HAL_SAMPLE_RATE eSample

In

The audio samplerate.

Return value

Value

Comment

None

Hal Audio Mode

10-5-5　难以抽象化的设备

PM：“HAL API的review相当重要，希望大家能集中精神，任何疑虑都应该主动提出讨论。我看大家都有点累了，今天的review就到这里，明天再继续。”

菜鸟：“老板！问完最后一个问题再去吃饭可以吗？ HAL就是要将硬件抽象化，抽象化说得很简单，但真的所有硬件特性都能够被抽象化吗？例如某颗CPU的clock可以调整为3.768 K、12 MHz、24 MHz、48MHz4 个选项，那我们的 HAL API——hal_set_CPU—clock()的参数要如何设计？假设简单的设计如程序代码10-4所示。

程序代码10-4

系统程序写成：

若下个项目换了较快的CPU，它的clock的设定值根本不一样，假设是3.768 K、16 MHz、32 MHz、64 MHz，这时候HAL API不就非改不可了吗？否则在新CPU的系统里，仍然看到hal_set_CPU_clock（_24MHZ）。这样的程序，不是很奇怪吗？”

系统老鸟：“这种问题用不着老大来回答，看来你还是没完全了解抽象化的意义。像这种不是标准的参数选项，如果你还是直接用它的值来当作参数，则等于没做抽象化。当你碰到这种状况时，记得提醒自己再往高端抽象化一层，还是借用你的例子，参数的选项可以设定为程序代码10-5所示。”

程序代码10-5

PM：“抽象化的思想很容易理解，但实际的执行方法却只能意会，难以言传，其实只要按照你刚才问题的方式去思考，想想当下一代的产品变成如何时，目前制定的API是否还适用？如果答案是否定的，这表示你还得再做多一次抽象化的工作。”

菜鸟：“看来抽象化还真的是很‘抽象’的东西。经过整个下午的讨论，我已经了解HAL的精神、思想与重要性，但真要我动手去做时，还真怕做出不对的东西来。”

PM： “不用担心这么多，难道Google、Microsoft、Apple他们订出来的接口就完美无缺吗？我们的工作是电子产品开发，不是卖系统，我们的客户是目前项目的伙伴，以及之后使用此系统的项目团队，你只要随时提醒自己：‘这个产品线之后的延续项目都会用到这组HAL API，我该如何让HAL真的发挥它的效用？’即可。

好了，吃饭去吧！长夜漫漫，吃饱还要回来赶报告呢！”

Chapter 11

菜鸟当自强：软件工程师硬起来

时间：11月01日　 登场人物：菜鸟、固件老鸟、硬件老鸟

菜鸟：“我一直对一件事情很迷惑，PM老大老是说嵌入式系统开发团队里的软件工程师一定要具备基本的硬件知识与技能，但我认为隔行如隔山，硬件专业岂是一、两个月就可以速成的？软件工程师到底需要具备哪些硬件技能，才能符合PM老大对本团队软件工程师的要求？如果我们把硬件的技术都学起来了，那公司岂不就不需要硬件工程师了？”

固件老鸟：“身为固件工程师，我偶尔也会希望如果硬件工程师也会写点简单的driver，能修改程序配合硬件调试就好了，这样就不用大家都在忙时，还得为了编辑一两行程序打断固件工程师的工作。同理，当人家硬件工程师在忙的时候，你就为了要焊根线或量个信号，打断硬件工程师的工作，是不是也不太好？

因为嵌入式系统包含了软件与硬件开发，除了软、硬件的人要不断沟通外，我们会希望硬件工程师多少要能了解软件系统架构，并能看懂driver程序，这样才不会闭门造车，而忽略软件开发的需求。举例来说，有些硬件工程师就是无法了解为什么硬件板子必须留出Debug Port或UART供调试之用，他们认为这样不但浪费板子空间，还可能影响系统抗ESD（静电）的等级。

对软件或固件工程师来说，我们希望在硬件设计阶段，软件工程师能提供建议与需求给硬件工程师。在板子调试阶段，可以与硬件工程师共同合作、理清问题；在驱动程序开发与测试阶段，软件工程师必须执行第一关的分析工作，确定是程序还是硬件的问题。我们希望软件工程师都会做基本的硬件焊接、信号测量与分析，不要什么跟硬件有关的事情都等硬件工程师的帮忙，他们也是RD，并非软件工程师的助手。

更重要的，我们公司是做产品开发的，不希望当这些工程师升起来当小主管或项目经理时，就只是个软件或硬件的专家，唯有在工程师阶段不断地从不同领域的人员身上学习，才能对产品开发有正确且完整的观念。

你现在搞懂PM老大所谓‘软件工程师硬起来’的意义是什么了没？若要达到这样的目的，你认为软件工程师需要具备什么能力？”

菜鸟：“我认为你们老鸟是希望我们这些菜鸟，不应轻易放弃任何可以参与硬件设计的机会。只要踏入嵌入式系统开发这一行开始，就注定我们不再是‘纯粹的’软件工程师，必须了解硬件设计的流程，并积极使用‘软件’技能支持硬件开发，这样才可能做出好的产品。

既然都说到了硬件设计的思想了，一般嵌入式系统的软件工程师是否需要熟悉电子学呢？我在大学修了两学期电子学，成绩相当惨烈，除了 V = IR外，其他的应该都还给老师了。”

固件老鸟：“既然原本就不属于你的东西，我想电子学就还是继续留在老师那儿吧！你所记得的东西应该已经足够让你应付日常的开发工作了。”

11-01　硬件开发流程

固件老鸟：“这是我们硬件团队的高手，现在由他接手跟你说明软件工程师必须知道的硬件知识。”

菜鸟：“学长好！不好意思又来打扰您了。”

硬件老鸟：“之前我在别家公司服务的时候，软件与硬件工程师几乎每逢开会必吵架，大家对同一个问题看法南辕北辙，而各单位的研发主管们只想要理清责任，都不愿意好好坐下来听听对方的意见，所以项目开发相当没效率，而项目经理对此争功诿过的文化也束手无策。

我们的PM老大就不同了。他希望所有人都以‘设法让项目成功’的角度来看事情，也积极推动软、硬件工程师共同设计的概念，我认为这才是正确的产品开发原则。首先，我们先从‘硬件板子设计不应该只是硬件工程师的事情’这个思想说起吧!”

菜鸟：“这个思想我知道啊！每次关于硬件板子电路设计的review meeting我都有参加，但可能我的硬件知识不足，常常感觉是鸭子听雷。”

硬件老鸟：“一般人都容易误会hardware review meeting的目的，以为这是让硬件工程师对软件工程师‘报告’目前硬件设计的进度与细节。这是完全错误的，hardware review meeting应该是以沟通为目的，当项目一开始，软硬件工程师就应该针对硬件板子的设计不断进行沟通，否则一个做登山车的项目，等实体板子拿回来才发现是一台小折叠车，到那时才来讨论已缓不济急。

所以在hardware review meeting中，你听不懂hardware的细节没关系，但总知道你们软件系统的需求吧？登山车需要的是宽轮胎以及坚固的车身，当硬件工程师说他打算设计一台短小精干，可以塞入一般汽车的小轮径单车，并想新增一些机身结构，让这台车可以折叠时，你总不能不提出意见吧？”

11-1-1　固件工程师，你不能没意见！

菜鸟：“我常听到其他项目的固件工程师，等板子回来时才抱怨：

‘怎么会这样？这个IO Port无法设定为双向。搞什么~要我怎么控制啊？’

‘这只脚位无法输出时序，我要如何产生Clock?唉！硬件的，又搞错了。’

以上都是事前没有沟通的后果吧？我不懂的是，软、硬件人员应该都会被事先告知项目的产品规格，硬件工程师总不可能故意做出不符合最终产品规格的板子吧？”

硬件老鸟：“硬件工程师自有其专业，当然会尽量做出符合规格的‘硬件’，但硬件工程师往往会将重心放在电源控制、组件选择、电路走线、制造成本等，而固件工程师最关心的却是能否顺利地控制CPU与外部设备，软件开发时是否可以方便调试等。

最近发生一个状况。软件工程师认为目前系统尚未稳定，坚持在real-size board上一定要把Debug Port（如JTAG）留出来，否则，开发进度势必会delay。但硬件工程师认为此举会严重影响产品的抗静电（ESD）能力，最终也是无法取得客户QA的量产许可。双方讲得都有道理，彼此沟通后也都能谅解对方的考虑，所以最后决定不留出Debug Port，但必须做出UART的测点，必要时，软件工程师还可以接线连接PC的串行端口，打出Debug Information来进行分析。

试想，如果双方不事先沟通，硬件工程师就按照自己的想法硬做下去，等板子回来会发生什么事？重新制作板子或进度延误已在所难免，若影响团队间的和谐就不好了。

固件工程师参与硬件设计真的是好处多多。

■　可了解电路设计的主体，在设计阶段就可发现固件编写时必须特别注意的项目。

■　若对某外部设备的控制方式不是很有把握，可以在电路设计时，请硬件人员预留其他设计方案（举例来说，驱动程序可以用SPI接口控制SD卡，也可以外接一颗SD card Controller。前者电路简单、成本低、速度慢；后者则刚好相反。在设计阶段，系统工程师不见得有把握使用SPI接口就可以符合产品应用的需求，此时可以请硬件工程师将两个方案都做到target board上）。

■　不仅仅只有硬件工程师才可与硬件原厂联系，固件工程师也应该尽可能寻求原厂的协助，并多与硬件工程师交换意见。

■　事前的讨论，胜过事后的争执。

11-1-2　电路设计流程

菜鸟：“看来固件工程师参与硬件设计是绝对必要的，但我实在不知道硬件设计到底要做什么事情？”

硬件老鸟：“在板子回来之前，最重要的就是做出硬件的蓝图——线路图（或称Schematic、Circuit diagram、原理图等），设计线路图的过程称为电路设计，我们就从电路设计的流程开始说明吧！

和软件系统设计一样，硬件工程师必须先取得产品详细规格、开发成本、制造成本、进度、质量要求等信息。此外，还要评估自家软硬件团队的能力（可用不同的硬件架构来设计同类型的产品，若能沿用软件团队之前的经验绝对可事半功倍），但这个因素常常造成决定硬件架构时的困扰。举例来说，客户委托设计手机类型的产品，自家的软件团队比较熟悉MIPS，较不熟悉ARM，但市面上手机的reference design都是以ARM的系统为主。决定主控IC绝对是整个开发项目的大事，软硬件的设计人员都必须参与决策。

当明确知道要做什么之后，硬件工程师必须maintain一份‘硬件规格工程表’，尽可能详细的列出硬件系统各部分的规格（如图11-1所示的范例图）。这个表格相当重要，除了供所有工程人员与备料、生产单位参考之外，也是以后项目中所有关于硬件讨论的基础。

▲ 图11-1　硬件规格工程表范例

在设计阶段，硬件工程师的工作流程如下。

Step01：详列产品功能规格。

Step02：硬件设计也讲究模块化与图形化设计（就如同软件系统的模块一样），硬件工程师会先将整个硬件系统根据功能区分，画成一个个功能方块，例如：电源、显示、存储器、音源输出功能模块等，并将各个方块根据模块间的关系互相连接。

Step03：确认CPU与每个功能模块的连接方式（请参考《第9-5节：控制其他芯片》）

Step04：组件挑选（考虑因素：客户规定、价格、熟悉厂商、工厂库存等）

Step05：逐一确认每个功能模块方块的内部电路，若有需要，可以事先做些简单的实验，确认局部线路设计的可行性。

Step06：电路设计绘制

□　Chip Select 规划：有些 SoC 内部就内置 SRAM、Mask-ROM、LCD Controller 等，使用单颗几乎就可以完成整个硬件架构。有些CPU则需要连接较多的外部设备与存储器，而通常外部设备都会需要Chip Select PIN，某些size较大的NAND Flash甚至要用到4根Chip Select PIN。硬件系统里需要的Chip Select PIN必须统一事先配置。

□　存储器规划：根据产品规格，规划系统需要的RAM、ROM或Flash等，包含种类与size，这部分一定要与软件工程人员讨论。

□　CPU的PIN脚规划：固件工程师只能通过这些PIN脚来控制外部设备，软硬件人员必须对所有PIN脚的设定取得共识。举例来说，假设某个设备必须用PWM来控制，那么选用的PIN脚就必须有timer输出功能。

□　电源系统设计：硬件设计上，除了硬件各区块必须能分别测量耗电流之外，若必要，还得加上可将某外部IC电源切掉的电路，这牵涉到产品对于耗电量的需求，软、硬件人员彼此必须充分讨论。

□　Debug接口规划。

□　因为Target board的尺寸没有限制，硬件工程师对同一个功能模块，可以设计不同的解决方案，同时加在板子的线路上（但不会同时运行，通常是利用jumper切换），等target board回来，经过软、硬件工程师的实验与评估后，才决定在最终硬件设计上要使用哪个方案。

Step07：电路功能测试与实验。根据产品规格的需求，可以有好几种的电路设计方式，但是会被设计到产品上的电路只会有一种，通常硬件工程师会尽量参考原厂的reference design，硬件工程师对自己不熟的组件与线路，在电路设计立项时，应该尽量多做些实验。

Step08： PCB Layout设计。Layout审查确认后，硬件工程师会正式出PCB Layout图，提供给板厂进行板子的制作，制作PCB所需的时间与PCB是否为多层板和走线密度有关，通常1~2周内‘热腾腾’的板子就可以到手。

板子回来后，硬件工程师会立即安排PCB板的组件焊接。焊接的方式分为很多种，有利用SMT机台Mount组件，也有利用手工焊接将组件一颗颗粘到板子上去。硬件工程师必须把制作板子的schedule告知固件工程师，让固件工程师可以提前准备测试程序，至少在打件完的板子回来时，测试程序就要准备好。这样硬件工程师对板子做完基本的检测之后，就可以试着执行测试程序，此时，软、硬件工程师应该要协同作业。”

菜鸟：“几乎每个步骤都需要软件人员的意见，软、硬件工程师不好好合作还真的会出问题。”

11-1-3　硬件设计文件：不只是写给硬件人员们看的

硬件老鸟：“在上述硬件设计流程中，硬件工程师必须编制以下文件。”

■　硬件设计规格书

□　记载产品电气规格标准，例如：产品耗电流标准、喇叭输出功率、RF发射接收功率等。

□　记载产品规格与组件变更日程与原因。

□　描述CPU、主要组件与替代组件的相关电气规格。

□　电路方块图。

■　电子电路图。

■　硬件设计日程控制表。

□　开发时间/人力控制：设计人员根据此控制表可以了解设计时间进度，避免人力重叠与浪费。

□　电子组件备料的进度控制。

■　产品测试验证规格书。

□　设计验证。

□　安规验证。

菜鸟：“我以为只有软件工程才需要做一堆文件，原来硬件工程师也逃不了啊，呵呵。”

硬件老鸟：“这些文件不是用来被检查的，更不只是做给硬件工程师自己看的，软件、结构工程师，以及生产单位都会需要参考这些技术文件的。”

11-1-4　硬件工程师专用测试程序

硬件老鸟：“除了板子验证阶段的测试程序外，软件工程师还必须额外提供一些程序给硬件工程师，通常这些程序可称为系统的工程模式（Engineering Mode）或测试模式（Self-Test Mode），主要用途就是快速地检测产品规格，并检查硬件是否可正常运行。”

会使用这些程序的状况包括

■　工程人员使用：通常软件开发的进度会比硬件缓慢，软件工程师可以在开发阶段初期先行完成工程模式（基本上就是驱动程序的Unit Test程序，加上简单的操作界面），好让硬件工程师在还没有完整系统软件的状况下，也能检验硬件与驱动程序之间的搭配性，并筛选出有问题的硬件板子加以分析。

■　工厂生产制造使用：电子产品的生产流程其实非常繁琐，且电子产品代工的毛利并不高，所以工厂会非常注重生产过程的流畅性与产品良率，这表示工程人员在设计之初，就应该要思索生产时可能出现的问题。工厂的操作员会利用工程模式或测试模式去检验生产过程的半成品或成品，通过简单明显的警示，操作员可以将不良品马上移开生产线，使良品继续往下一个阶段进行。生产线上，这一站的投入就是上一站所完成的良品，所以越到后面的流程，不良品就会越少，生产线所制造出来的产品良率就会越高。

■　安规和环境测试认证使用：电子产品都需要做安规认证与环境实验，否则，可能拿不到生产许可。通常我们会在工程模式下增加测试选项，供安规与环境测试验证使用。举例来说，在做EMI认证时，需要让测试产品在Full Run Mode下进行测试。所谓的Full Run Mode，就是要设法让机器上的所有组件都能在Loading最大的状态下同时运行，实际的应用可能不容易碰到这样的状况。不过，为了方便硬件工程师执行测试，最好的方式，就是由固件工程师写一个专用的测试程序去‘硬操作’硬件。

■　产品维修人员使用：当产品在市面上销售时，难免会发生使用机器故障的问题，客服的维修人员可以先利用工程模式内的硬件测试选项进行测试，并确认问题点发生原因，快速进行维修，避免每次修护都需将机器送回原厂。若是软件版本别的差异所造成，也可以利用工程模式进行系统软件更新。

菜鸟：“原来这些程序都要软件团队写喔！工厂没有人会写吗？”

硬件老鸟：“废话！这些测试程序都要用到driver，当然是软件团队的工作，总不可能要工厂或硬件工程师重新开发driver吧！”

菜鸟：“了解了解！”（心中OS:写就写嘛，有啥了不起！）

11-1-5　案例研究（I）见鬼了！硬件线路没问题，程序也没问题，但就是不会动！

硬件老鸟：“我来举一个以前处理过的案例，通过软、硬件工程师，与原厂的通力合作才得以解决。

控制GPIO对于固件工程师来说如同家常便饭，但那次发生的灵异事件——CPU总是有固定的几只GPIO不听使唤，而且不论如何设定相关寄存器，这些‘中邪’的GPIO的输出状态都是HIGH。硬件工程师针对CPU和周边组件，所有的电路检查了好几遍，完全找不出任何蛛丝马迹，固件工程师则把驱动程序的Code看了又看，也是一筹莫展。

当时想到一些可能，有些CPU内部会把某些PIN硬拉为HIGH，必须设定其他寄存器才能解除，或者这些PIN除了 GPIO之外，还有其他的功能，例如AD port，这也能导致程控无效。所以我们请了 CPU代理商的FAE看了程序，并请其代为询问原厂，CPU是否对这些出问题的PIN有任何特殊的设计，最后得到的答案也是否定的，这些PIN就只是单纯的GPIO，驱动程序也没有任何问题，甚至这些程序在原厂EV-board上的运行结果都是正确的。

最后只能想到，可能我们拿到的这一批CPU是有问题的，于是我们把CPU送回原厂分析，原厂也相当关注这个现象，所以分析报告很快就出来了，拿到报告后，所有人的眼神都投到我身上。”

菜鸟：“结果是硬件的问题吗？你不是说大家都认真review过硬体设计了吗？而且GPIO不就是PIN与PIN直接对接吗？怎么可能会出问题？”

硬件老鸟：“报告中说明，这些送分析的CPU里许多PIN已被烧毁，显然CPU曾受到大电流或大电压的冲击。原因再清楚不过了，我们板子的电源系统可能有问题，最后发现在开机阶段确实会有大电流产生，并对CPU内部造成了永久性的破坏，所以导致控制这些GPIO无效。

PM老大并未因此怪罪任何人，至少大家都很认真地解决问题，而软件工程师也为终于找到root-cause而松了一口气，毕竟大家都不愿意看到项目失败。”

菜鸟：“在其他公司或团队，应该很容易就会引发‘软、硬大战’，所有人忙着写报告证明不是自己的错，就算最后问题解决了，软、硬件团队之间也结下了难解的梁子。要避免这种状况，必须从设计阶段就把软、硬件团队绑在一起，把构建稳定的系统平台当成彼此的共同目标。”

11-02　卷起袖子用烙铁

硬件老鸟：“你老板应该有交待你要找个时间练练烙铁吧？”

菜鸟：“是说过几次，他还要我找实验室没人的时候来练习，但我就无法理解，这个东西有需要练吗？不就是把烙铁加热，然后沾一点锡膏就好了吗？”

硬件老鸟：“身为新人千万不要眼高手低。学学你们老板，他烙铁用得几乎不比我差，听说他也是被逼到，才下定决心决定把这门‘手艺’学好。因为某个系统不稳定的问题始终搞不定，追到最后怀疑可能是NAND Flash坏掉的问题，于是他想把板子上的NAND Flash换另一颗新的试试看，但当时已是深夜，大家以为这种系统不稳定的现象应该是纯软件的问题，所以硬件工程师也没留下支持。你应该知道差一步就可接近问题核心的那种感觉，但只为了换一颗IC，似乎也不好意思硬把硬件工程师从床上拉起来，于是他彻夜难眠，在办公室等到第二天硬件工程师来上班。

工程师或多或少都有DIY的精神，大部分人应该都会简单地焊接，但碰到要从CPU的PIN脚接电线出来测量，如图11-2所示，或者更换IC这种以较精密的工作，通常就束手无策了。并不是说要固件工程师取代硬件工程师的工作，但驱动程序调试时难免要动动硬件（如：调整电阻的值等），自己来也会比较快一点。但以我身为硬件工程师的角度来说，还是希望把比较精细的焊接工作交给我们。”

菜鸟：“你是怕我们把板子弄得乱七八糟吧？”

硬件老鸟：“呵呵~你这么说也没错，一般是不会有什么大问题，但如果操作烙铁技术不佳，有时会破坏板子上的焊点（PAD），若是重要的点，则整块板子可能就报销了！

既然你已进入电子产品开发这行，想要永远‘软趴趴’是不可能的，你要认清除了键盘之外，烙铁也是你的重要工具。总之，我还是把使用烙铁的原则说一说，记得找时间来练功，我们这里什么都没有，就是没用的板子一大堆，足够你练习了。”

▲ 图11-2　精细手工艺——从IC的PIN脚焊接电线，以连接逻辑分析仪的探针

烙铁使用技巧

■　温度要够热：烙铁的温度掌控，是焊接组件最重要的关键。温度不够，焊锡无法有效的融化，会造成焊接点的冷焊；温度过高，锡线内的松香太早被挥发，组件与PCB之间不易吃锡，组件的粘着容易失败。

焊锡一般融化温度约在350°C左右（无铅的焊锡需要的融化温度更高），若无法得知烙铁温度的话，可以使用目视的方法。将锡线直接点处烙铁头，锡可以在1秒钟左右将锡丝融化成为液体状，此时烙铁的温度已经适合进行焊接。

■　若发现烙铁头融化锡丝的速度过快，烙铁温度已经过高了，请尽快将烙铁头轻轻点触清洁海绵进行降温。反之，融化锡丝的速度很慢或是无法融化，就是代表温度不够，请再等候一下烙铁的温度上升。

■　烙铁头要适度清理：长时间的使用烙铁头，容易残留部分黑黑的杂质在烙铁头上方，这些杂质可能影响焊接的质量。处理方式就是将烙铁头在清洁海绵上画几笔，杂质会直接吸附在海绵上，等烙铁头清洁之后，一定要尽快让锡丝在烙铁头上溶化，以确保烙铁头上吃锡，这是延长烙铁头寿命一个很重要的方法。

■　焊接时，要胆大心细，不要怕组件会烧坏，通常封装类型的电子组件可以承受温度都很高，像SMT回焊炉融锡温度约在250°C~300°C之间，在组件停留时间约5秒钟左右。也就是说，只要烙铁的温度设定适当，将不太容易将组件烧坏。

■　使用助焊剂（松香）：当组件焊了很多次就是粘不上板子时，其主因与解决方法为

□　电子组件的金属焊脚氧化：利用刀片，刮除金属焊脚氧化的部分。

□　PCB焊点氧化：将PCB焊点（Pad）的部分擦上松香，再用烙铁将锡丝融化粘在PCB焊点，PCB焊点会恢复成像新的板子一样容易吃锡。

松香还有另一个好用的小技巧。封装IC之PIN脚间的距离比较小，拉焊时容易造成短路。此时，滴上一些松香，PIN脚之间的锡会因为松香的特性而增加锡与金属的黏着度，多余的锡会散开到PIN脚上，拉焊的成功率就会大增。

■　一般焊接技巧——点焊：焊接前，先分别在组件与PCB的焊点上吃一些锡丝，这样可以增加焊接物与被焊接物之间的黏着性，两者间的黏着度增加，焊接成功的机率就会大大增加。

■　高级焊接技巧——拉焊：PIN脚较少的组件用点焊即可顺利焊上，但较大型的IC动辄几十根脚，不可能一根一根脚去点，这么做也可能造成邻近PIN脚的短路。拉焊就是针对这种IC的焊接的技巧，利用金属液体流动的特性，以烙铁去拉动多余的锡前进，以进行焊接。

拉焊的步骤

Step01：先将组件平贴于PCB上，并将组件四周的PIN脚与PCB相应的焊点对齐。

Step02：将该组件的每根PIN脚都均匀且确实吃上锡，让组件黏附于PCB上，此步骤难免会在PIN脚之间造成短路，可以暂时不理。必须注意的是，上锡的多少会直接影响板子和组件间的黏着度。

Step03：此时欲焊接的组件已固定在PCB上，将烙铁顺着一个方向，慢慢扫过IC某一边的所有PIN脚，多余的液状锡会被烙铁带离每根PIN脚，并粘附在烙铁上，使得PIN脚之间只会剩下牢牢黏着PCB与组件的锡，然后，只要在板子外轻轻敲一下烙铁，多余的锡就会顺势流下来，使烙铁头恢复干净。重复上述的步骤，直到所有PIN脚间的锡被清理干净为止。

■　善用镊子：随着半导体制作工艺不断进步，某些电子组件的体积与重量真的很小（如电阻），有时候不小心吐气，都可能造成组件四处飞散。焊接时，不可能用手去固定组件位置，此时，使用尖头的小镊子可以增加焊接的稳定度。

11-03　善用测量仪器

硬件老鸟：“这么多的硬件相关技能，你认为对固件工程师来说，最重要的是那一项？”

菜鸟：“应该不是焊接或电子学吧！因为我看driver team的学长对这些好像也不是很精通。此外，电路设计与Layout都有专门的know-how，固件工程师不应包办代替，所以若单就写driver的需求来看，我认为信号测量与分析最重要，不会量信号，有时还真的不知道driver的运行是否正确，而且这些测量仪器对driver的调试也是不可或缺的，可以说是固件工程师观察driver运行的眼睛。”

硬件老鸟：“你说的没错，能够善用测量仪器确实是固件工程师必备的技能。测量仪器的种类很多，有些只应用在特定领域，例如，音频或视频分析仪、USB Bus分析仪、光学（背光、辉度、色度、色温、对比、视角）测量系统、网络分析仪、信号发生器、频谱分析仪等，这些仪器不需要每个都会用，等需要时再学就可以了。

我们在此要讲的是基本电子特性的测量，说穿了，不外乎就是时间、电压、电流、电阻、频率等特性的组合，而一定要会用的仪器也只有电源供应器、万用电表及示波器这3种而已。”

11-3-1　按图索骥

硬件老鸟：“在测量信号之前，当然得先找到测量点在板子上的位置，所以，软、硬件工程师都必须能看懂电路图。软件工程师至少要能看懂主要组件间的连接关系，若能知道其他被动组件的功能就更好了。

电路图是一种语言，它沟通的方式是利用简单的线段和符号去叙述组件之间的关系，电路图里包含各个组件及其PIN脚的名称，及所有组件之间的连接关系。我们可以从电路图中找到要测量的点，例如，CPU的GPIO#P01 PIN、电阻R08、LCD插槽上的FRAME PIN、测点T03等测试点名称。”

菜鸟：“在电路图中找到测量点的名称后，有时得找半天才能在板子上找到相应位置，尤其是real size板子面积比较小，组件往往布置得密密麻麻，或者多片PCB的状况时，要找到测量点真的很费眼力！”

硬件老鸟：“组件的摆放位置还是有迹可循的。在设计板子的时候，PCB Layout工程师会因为组件Layout走线方便和组件的电气特性等因素，将每个电路的相关电子组件排放在一起，但有些板子的面积实在太小，或形状较为怪异，Layout工程师能把所有组件摆入且不影响电器特性已是勉强，不得已只能将某些组件硬塞到有限的空间里，如果再加上多层板的走线是在实体板子上看不到的，要用肉眼去板子上找组件确实不容易。当工程师急着要同时测量多个点，光把这些点全找出来就真的够让人火大了！

幸好不是只有你们软件人才用电脑，在硬件设计流程中，从电路设计、PCB Layout、结构设计到生产规格都已整合并计算机化，PCB Layout工具程序可以直接使用电路设计工具所产生的结果（电路图），并产生如图11-3所示的背纹图，实体板子上就会有相同的组件配置与印刷，而且我们可以在计算机上对这个图做搜寻，马上就能找到某个测量点的位置。

▲ 图11-3　板子背纹图（由此图可找到电路图中的组件在板子上的位置）

此外，根据我的观察，你们通常都是把电路图打印来看，当碰到比较复杂的线路时，看纸本的图并不是很有效率。建议还是用工具来看，除了可搜寻外，工程人员很容易就能看出组件之间的连接关系。”

菜鸟：“我们项目的Server上好像确实有PCB Layout的相关文件，我以为那是给PCB制作看的，不知道可以拿来做搜寻，所以从来都没开过。我的计算机可以开吗？我想这种编辑电路图或Layout的工具应该都很大吧？”

硬件老鸟：“我想你没开过的硬件设计文件应该还不少吧？《Target Board使用手册》有认真K过吗？里面对这些文件的用途写得清清楚楚，甚至这些图的viewer程序也都放到server上了。一般来说，我们不希望软件工程师直接去修改这些硬件设计的图，你只要安装这些图像文件格式的viewer即可，不然问问Google也可以找到免费的工具。

开发驱动程序的目标就是要控制好硬件系统，固件工程师绝对不能对目前板子的硬件设计仅是一知半解，切记切记！”

11-3-2　从上电开始：电源供应器

硬件老鸟：“当你知道板子上所有的组件配置后，才知道要把这些仪器接到哪里去，接下来我们逐一介绍开发电子产品一定要用到的仪器。首先是电源供应器，因为没有电，什么电子产品都运行不起来。你认为电源供应器应该具备哪些功能？”

菜鸟：“首先一定要能供电（废话），再来就是要调整输出电压与电流的值。”

硬件老鸟：“没错！图11-4所示的这台机器是我们家的电源供应器，它可同时输出两组自设电源，以及一组固定电源（本例中是5V/3A）。电源供应器的操作非常简单，唯一要注意的事是：把电源线接到板子之前，一定要确定电源供应器的电压、电流值设定是对的。最常发生的悲剧是，上个使用的人把电压值调高了，下个使用者没注意，直接就接到另一块板子上，使得板子被高电压烧坏。”

▲ 图11-4　2 channel 电源供应器面板示意图

菜鸟：“除了测试低电压时系统的反应是否正确，我们一般不太会去调整电压值。我唯一不了解的是，电源供应器上的C.C.、C.V.指示灯，这两个指示灯的作用是什么？”

硬件老鸟：“当C.V.指示灯亮起时，表示目前在Constant Voltage下工作，此时电源供应器的输出为一理想的电压源；反之，当C.C.指示灯亮起时，表示目前在Constant Current下工作。但在一般的实验中，我们所需的往往是理想的电压源，所以当C.C.指示灯亮起时代表一个警示，即电源供应器所供应的电流不足，使得我们的板子无法在定压上工作。这个现象可能表示两种状况。一是电路接错导致短路，非常危险，应该立即断掉电源，找出短路的原因；二是电路所需的电流较高，试着将电源供应器的电流值调大看看。”

11-3-3　三用电表：确认电压、电流及是否短路

硬件老鸟：“当新板子上电后，我们必须先观察整个系统的工作电压与耗电流是否正常（新板子通常不会一上电就有很好的耗电状况，毕竟电源管理程序可能还没做好调整，但如果超出理想值太多，则表示板子哪里出了大问题，应该要优先找出原因），接着才是检查各个硬件模块的耗电状况，确定都没‘大’问题后，才能开始验证驱动程序。如果硬件工程师没有先做这些基本检查，就直接测试驱动程序，则当发生问题时，还得去理清到底是软件还是硬件的问题。

要测量电压、电流，以及是否有短路状况时，就要请出工程师的好朋友——万用电表，如图11-5所示。”

▲ 图11-5　三用电表

菜鸟：“以前我们都说三用电表，现在已经升级到万用电表了喔？”

硬件老鸟：“三用电表基本上必须能测量：电压、电流、电阻3大要素，现今的三用电表除了指针变成数字显示之外，还能测量晶体管的特性脚位以及干电池的电力足够等，所以三用电表渐渐也称为万用电表了。

图11-6所示是三用电表的面板示意图，主要分为4类档位：DCV（直流电压）、ACV（交流电压）、DCA（电流测量）与Ω欧姆（电阻测量）。利用这些档位，我们可以执行以下的测量工作。

■　电阻测量：欧姆档

□　红色探针插入V/Ω黑色探针插入COM。

□　开关转到OHM（Ω）区域，由大往小调整至最佳值。

□　把测试棒接到待测电阻或组件上。

□　等待液晶屏幕显示该组件的电阻值。

■　短路蜂鸣：设置如同测量电阻值，档位转到有蜂鸣图案的档位，若测量的两点之间为短路（电阻值=0），则电表会发出蜂鸣声。

■　直流电压的测量：DCV

□　红色探针插入V/Ω黑色探针插入COM。

□　旋转开关转至DCV，若不知道欲测电压大小，可先转到最大再慢慢往下调整。

□　将测试棒与待测电路并联。

□　等待液晶屏幕上显示数据。

■　交流电压的测量：ACV

□　红色探针插入V/Ω，黑色探针插入COM。

□　旋转档位至ACV，若不知道欲测电压大小，可先转到最大再慢慢往下调整。

□　将测试棒与待测电路并联。

□　等待液晶屏幕上显示数据。

■　直流电流测试：DCA

□　红色探针插入mA（大于200mA则插入10A），黑色探针插入COM。

▲ 图11-6　三用电表功能示意图

□　旋转开关转至DCA，不知道大小则一样由最大值慢慢往下调整。

□　将测试棒与待测电路串联。

□　等待液晶屏幕上显示数据。

■　电池剩余电量（电压）测试

□　红色探针插入V/Ω，黑色探针插入COM。

□　档位开关转至1.5V或9V附近的档位。

□　将测试棒两端接至待测电池，红色接正，黑色接负。

□　等待液晶屏幕上显示数据。

■　其他：电表还能测量晶体管与二极管的特性，固件工程师比较不会用到这些功能，有兴趣可自行参考电表的使用手册。

菜鸟：“使用电表就可以量出电的3大基本特性——电阻（R）、电流（I）、电压（V），但我看到大部分的固件工程师只在找短路、量耗电流时，才把您所谓工程师的好朋友请出来，他们最常用的应该是示波器吧？”

硬件老鸟：“没错！电表是工程师的好朋友，但身为固件工程师，你必须把示波器当兄弟或死党！它们最主要的差别为：电表观察到的是瞬时的单一数值，而示波器观察的是一段时间的一个变化趋势。也就是说，电表没有时间的概念，所以电表便无法测量电的另一个基本特性——频率。”

11-3-4　示波器的基本功能

菜鸟：“我们实验室中有许多不同品牌的示波器，机器大小不同、面板的按钮密密麻麻，好像也长得都不一样，采样速率不同，操作界面也是中英文都有（中文的反而看不懂那些名词的意义），这么多‘好兄弟’，要如何才能跟它们好好相处啊？”

硬件老鸟：“虽说每家示波器的功能大同小异，基本操作方式与测量原理也都差不多，但为了尽快上手，还是应该读读该示波器的使用手册。

在开始使用示波器之前，务必花个一分钟为探针做自动校正，如图11-7所示。有些工程师比较性急，结果量出来的信号乱七八糟，还以为是硬件问题，查了半天反而浪费更多时间。

▲ 图11-7　示波器使用前记得做探针校正

然后就是要记得接地，将示波器测试棒的接地端连接到地（示波器有两个接地端可以接地，一个在示波器上，另一个在测试棒上。两个点接地的效果是相同的，使用任何一个都可以）。再利用测试棒的尖端去点触想要测量的位置就可以了（如图11-8所示）。一开始量可能波形太密，或电压值太高，此时可以调整水平（时间）和垂直（电压）精度，或按下AUTO按钮，示波器会自动调整到合适的精度，并计算出我们需要的信息。

一般示波器会有多个Channel，让使用者可以同时测量多个点，并观察其信号在同一时间的相应关系（如图11-9所示）。

▲ 图11-8　测量某点的电压峰值、频率与周期

▲ 图11-9　同时测量多个测点的信号

示波器屏幕上看到的就是测量点的电压波形，示波器得到的电压波形误差比电表小，而且可以观察到长时间下电压的波形。”

菜鸟：“我已经知道示波器可以显示某个测量点的电压值及变化状况，也就是说，示波器能把肉眼看不见的电信号变换成看得见的图像。我想知道除了电压之外，还能用示波器测量什么？”

硬件老鸟：“示波器主要的信息来源就是电压与时间，根据这个机制，工程师可以进行以下电的基本特性测量。

■　电压的测量：利用示波器所做的任何测量，都是归类为对电压的测量，示波器可以做交流电压与直流电压的测量，如图11-10所示。

▲ 图11-10　纵轴代表电压，工程师可根据测点的电压变动范围，调整示波器电压敏感度

■　时间的测量：对驱动程序而言，控制硬件有时需要非常精准的timing，示波器的横轴代表时间，操作示波器，我们很容易可以得知事件之间的时间间隔，如图11-11所示。

■　频率的测量：频率是时间的倒数（F= 1/T），示波器能帮我们算出信号的频率，这在确认clock是否正确时相当方便，如图11-12所示。

▲ 图11-11　示波器的纵轴代表时间，使用者可根据需求，调整示波器的时间精度

▲ 图11-12　示波器测量结果-频率

■　相位的测量：相位（Phase）是描述信号波形变化的度量，通常以度（角度）作为单位，也称作相角。当信号波形以周期的方式变化，波形循环一周即为360°。通信系统的信号都是由各种余弦和正弦函数所组成的，其中的相位和幅度都不同，示波器的相位测量功能主要就是应用在通信系统上。

当然还有其他较复杂的测量技巧，但基本上，都是应用上述基本测量所组成。”

11-3-5　示波器画面提供的信息

硬件老鸟：“接着我们来看看示波器上的屏幕可提供哪些信息。图11-13所示是一个范例，基本上，其他品牌的示波器画面也大同小异。

▲ 图11-13　示波器画面上的信息

示波器能提供的信息如下。

■　撷取信号的方式（即采样方式）：可能的方式为峰值或平均值（后者可能过滤掉noise，但有时就是要找noise，此时就要用峰值采样）。

■　触发（Trigger）状态：稍后我们会专门谈Trigger。

■　发生触发的时间点。

■　触发时间点与屏幕中央的时间偏差值。

■　发生触发时的电压。

■　发生触发时的电压值。

■　触发类型：稍后谈Trigger时会解释。

■　这张图显示使用者同时使用两个通道（Channel）进行测量，所以可以看到两条信号。

使用者可以沿着时间轴移动垂直标尺，并从画面读出各个Channel在该时间点的电压值。”

菜鸟：“总之横轴代表时间，纵轴代表电压值，两者的精度都可调整，也能精密度量（利用示波器上屏幕横轴与纵轴的刻度），而我们想知道的不外乎就是波形的起伏与持续时间是否合乎规范，示波器这种视觉性的输出已可满足大部分的需求。

而且这些测量结果的波形可以输出到文件或打印机，这样大家在讨论问题时比较有所根据。我看到许多我们的问题分析报告，都会贴上波形当作佐证。”

硬件老鸟：“除了示波器屏幕显示的信息之外，其实我们还能够从图中‘算’出一些更有用的信息。图11-14所示的这张示波器的测量结果，测量的状况是系统上电后，电压从0爬升，抖动一下后才逐渐变为稳定。在这个流程中，我们会想知道以下信息。

■　电压爬升到稳定电压的时间

■　电压的最高值

■　电压不稳定的时间

这些信息至关重要，电压从0到稳定值的时间太长可能导致开机失败，而瞬间电压飙得太高，可能导致Reset IC或其他组件烧毁。在这个阶段，所有程序都还没开始运行（CPU也还没正常工作），无法用任何软件手段获得这些信息。使用示波器可以轻易解决这些问题，使用示波器的两条纵轴指标可夹出两个信号之间的精确时间值，使用横轴指标则可以算出电压的差值，如图11-14所示。”

▲ 图11-14　示波器画面上的信息

11-3-6　Trigger（触发）

菜鸟：“从示波器显示的波形图中，确实很容易就能得到信号的电压与时间信息，但总得把画面停在我们‘感兴趣’的画面上，才能开始测量工作啊！

例如，我的驱动程序在‘硬件事件A’发生后才会开始运行，这个驱动程序会去控制某些PIN脚，我想要测量这些PIN脚的信号是否正确，但示波器的信号跳动得那么快，我只好一直盯着示波器，等看到‘硬件事件A’发生时，用最快的速度按下示波器的‘STOP’按钮。但就算我是亚洲第一快手，再快也比不上电的速度啊!”

硬件老鸟：“没有人教你要用Trigger（触发）吗？

你举的例子还有机会手动实现，但有些系统不稳定的现象源自不可预期的噪声，根本不知道什么时候在哪根脚上发生，怎么可能盯着示波器屏幕看？既然示波器的工作就是不断的收集测试点上的电压信息，当然也有办法根据我们的需求预做判断。以图11-15所示为例，示波器可以帮我们找到‘信号突然拉高到一定电压等级’的硬件事件，这样的功能就叫做Trigger。”

菜鸟：“好像听过这个词，固件老鸟常常对他的工程师说：‘你在某某根PIN脚设Trigger试试看，等找到了我们再来讨论！’。当时我还不知道Trigger是什么，也不知道他们到底要去‘找’什么。”

硬件老鸟：“呵呵~听不懂就要问啊！

Trigger（触发）功能是使用示波器一定要会用的技巧，示波器预设一直从探针中收集信号，即便探针没有点在有意义的测点也一样，一旦启动了示波器的Trigger功能，其设定决定了当信号发生某种变化时，示波器才会再开始显示波形，并记录一段时间内的波形变化。

示波器的获取到的信号会暂存在存储器中，但没有一台示波器会拥有无限大的存储器，因此，所有的示波器都必须使用Trigger[1]，有限的存储器迫使示波器必须使用触发。举例来说，某某型号的示波器提供可容纳20亿个取样的存储器深度，但即便如此，示波器仍需要靠一些事件来分辨该将哪20亿个取样显示给使用者。20亿个取样看似庞大，但却不足以保证示波器的存储器确实撷取到了使用者想要的事件。

示波器的存储器可以想成是一条输送带，每当撷取到新的取样，便会将它放入存储器中。当存储器满了之后，最早撷取到的取样就会被舍弃，因此，存储器只会包含最新的取样。当触发事件发生时，示波器会撷取足够的额外取样，以便将触发放在要求的存储器位置（通常在中间），然后将数据显示给使用者。

简单来说，触发是指使用者所关心，而示波器必须发现的一个事件，换句话说，它是使用者在波形中想要找到的某种情况。一般来说，我们会关心的触发点有两大类：信号反转的瞬间（High→Low、Low→High）以及信号变化的幅度超过某个范围，如图11-16所示。

▲ 图11-15　使用Trigger可以实时找到如图中这种硬件事件

▲ 图11-16　触发条件范例

一般示波器都会提供多种Trigger的方式，表11-1所示是某款示波器所提供的功能，你可以参考一下。”

表11-1　示波器提供多种Trigger方式的说明

Trigger类型

描述

信号边沿（Edge）

信号边沿触发是所有触发中最常见的一种（如图11-16所示），它会这么常被使用的原因是所

有的波形都包含信号边沿，因此，如果触发位准设定正确，这项触发就能发生作用。当然，

这项优点也是最大的缺点，因为在大多数的波形上，它触发的次数非常地频繁，就跟使用自

动触发差不多。

毛刺/脉冲宽度/超时

虽然使用信号边沿触发就能发现许多问题，但有时候工程人员还是得使用较复杂的触发。其

中最简单的一种触发就是脉冲（Pulse），它是以在临界位准之上（正脉冲）或之下（负脉冲）

的一个时间范围来界定。毛刺（Glitch）是最常见的脉冲触发，可在脉冲比最小宽度来得短

时进行触发。

在某些情况下，使用者可能希望只要一超过设定之时间值，示波器就进行触发，此称为

“超时”（Time Out）触发，因为示波器不需等到出现完整的脉冲才触发。

“脉冲”触发则是在Pulse即将结束时触发（例如，信号原来为Low，发生了一个往上

的Pulse，当信号维持在High一段时间后，可能会发生Time-Out触发；当信号由High

往Low变化时，则会发生Pulse触发。）

Trigger类型

描述

上升时间与下降时间

上升时间与下降时间触发会找寻上升或下降速度太快或太慢的信号边沿。这类触发是由

高/低两种触发位准来界定，信号在这两个位准之间还有最小和最大时间值的限制。

建立与保持时间

（Setup & Hold）

这是基本Trigger功能的组合，使用者可以事先设定信号种类与时间限制，当实际波形

违反这些设定时，就会产生Trigger。这个功能操作上比较复杂，但可以用来找出长期运

行时，随机产生之不正常的信号。

时窗

时窗（Window）是一种进阶触发，它使用两个电压临界值及最多两个时间值当成Trigger设

定参数。时窗触发可以是每当信号进入或离开电压范围就执行这么简单，或者也可以使用时

间范围来指定在电压范围之内（或之外）所花的最短和最长时间，这提供了执行各种不同触

发的弹性。

菜鸟：“一般我们不会用到太复杂的Trigger方式吧！示波器上的操作界面也没那么直觉，要设定上面说的‘设定与保持时间’或‘时窗’，Trigger应该不会太容易吧！”

硬件老鸟：“老实说，有的Trigger方式我还真的没用过。有的高级示波器还会提供更复杂的Trigger，有兴趣不妨参考使用手册，但上面列举的基本Trigger机制，应付大部分状况已是绰绰有余。”

11-3-7　案例研究（II）:同时测量多个信号

菜鸟：“听你这样说，其实我们只要会用示波器的基本测量功能，再加上Trigger的应用，应该就能解决大部分的状况了。”

硬件老鸟：“没错，其实比你想象的简单吧！示波器的使用绝对是固件工程师必备的基本技能，要尽可能的熟练。最后我们来谈谈实际的应用实例。

之前，应该有人跟你提过NAND Flash的控制方式（可参考《第13章：存储器管理（II）:NAND Flash概论》），基本上就是分别控制连接CPU与NAND Flash的所有GPIO脚，使其信号与data sheet内的时序图一致。若有需要，可使用示波器来检验查看各个PIN脚间的信号变化是否正确。

其实NAND Flash这个应用对PIN脚之间的timing要求并没有这么严格，基本上，就算没有示波器也可能够硬搞出来。以下讲一个对timing要求相当严格的应用，这里没有示波器的帮忙，恐怕连驱动程序错在哪里都不知道。

在这个例子中，要控制的IC的功能为利用数字方式控制输出电压，主要用途为LCD对比度和显示灰度的控制，data sheet写得很简单，只要我们把连接这颗IC的3根PIN按照图11-17所示的方式控制，这颗IC即可输出我们想要的电压值。”

▲ 图11-17　案例研究——根据data sheet，驱动程序必须“制造”出来的波形

硬件老鸟：“看看我们驱动程序做出的信号是不是和data sheet中的几乎一样？其实在开发期间，driver 一直搞不定这颗IC，下命令给它都没反应，但驱动程序和硬件看来都没什么问题。最后发现是因为这颗IC的售价比较低，所以在控制上对timing要求相当严格，信号之间的时间若偏差一点就会使IC不接受命令，而偏偏data sheet上也没写清楚，所以我们只能trial and error。

最后，是固件工程师用示波器测量这3根脚的组合信号，如图11-18所示，不断地修改程序以调整信号间的timing，并把每个程序设定的波形图存储下来，反复比较后，才终于让这颗IC动了起来。”

▲ 图11-18　案例研究——使用示波器测量所有相关PIN脚的实际时序

菜鸟：“了解，如果没有示波器，驱动程序工程师的工作还真的有如瞎子摸象！正所谓有图有真相，从程序中只能看出设定流程是否正确，至于细微的timing变化，或是否有其他不可知的干扰发生，真的就只能靠示波器的帮忙。难怪您要说示波器是固件工程师的好兄弟了，看来我得找时间和我的兄弟好好相处相处。”

[1]　当碰到毫无头绪的问题时，系统内有那么多的PIN脚，根本不知从何量起，更不要说如何设定Trigger条件了。而示波器通常最多只有4个Channel，同时只能量4个测点，且其用以存储信号的存储器有限，面对这种问题时，也只能束手无策。此时就要请出逻辑分析仪，它可同时测量的Channel较多，而且能将信号存在硬盘里，工程师可将所有可疑PIN脚在某段时间内的信号先存储起来，然后再慢慢的分析。

Chapter 12

做好存储器管理

时间：12月01日　 登场人物：菜鸟、项目经理（PM）、系统老鸟

PM： “你在固件组那里帮忙了一个多月，有没有什么心得？”

菜鸟：“嗯，学到很多以前不知道的东西，现在才明白我对计算机系统运行原理的理解居然如此薄弱，这是我要加强的地方。目前我们系统上所有设备（包含CPU及其内部设备）的驱动程序都已完成并通过基本的单元测试，target board需要一些rework才可正常运行，real-size board的电路正在根据固件工程师从target board上找到的问题进行修改，等review过后就可以送layout，看来硬件设计目前并没有什么大问题。现在固件组的工程师们正在逐一做进一步的压力测试，我看再过一两周就可以把板子和整套驱动程序交付系统组做整合了。”

PM： “通知固件组一下，晚一点等他们有空我要和他们开个会，确认一下目前的状况与进度。对了！ target board你现在应该很熟了吧？”

菜鸟：“虽然最后固件老鸟只分配了扬声器（Buzzer）和系统电压检测这两个最简单的驱动程序给我写，其他都由他自己亲自操刀，但我还是帮忙写了很多测试程序，也解了许多小问题，我想现在我应该是除了固件组之外对target board最熟悉的人吧！”

PM： “很好。你在实验室鬼混也够久了，从明天起去系统组帮忙准备系统整合相关事宜。”

菜鸟：“系统整合耶！我行吗？”

PM： “别啰嗦！打仗没人的时候，就算是炊事兵也得拿着步枪硬上，更何况你都说了，你是我能调动的人之中对target board最熟的人，不是你上谁上？我说你可以上，你就要做给我看！”

菜鸟：“知道了。”

CPU + Memory的架构是现代计算机系统的理论基础（还记得图灵机（Turing Machine，又称杜林机）理论吗？），存储器是嵌入式系统中除了 CPU以外最重要的设备，嵌入式系统中不可能没有存储器，否则CPU要到哪里获取程序指令来执行？程序中的变量要存储在哪里？让存储器可以被其他程序正确存取，是驱动程序的责任，但配置系统中所有存储器的用途则是系统工程师的工作。本章的重点即为：嵌入式系统开发流程中所有和存储器有关的细节。

12-01　动态存储器空间配置

菜鸟：“报告学长！又是我，今天起我奉命到系统组‘服务’。”

系统老鸟：“这几个月看你在各组间游走，想必学了不少东西吧！你的运气算不错了，很多刚毕业的菜鸟，进到大公司内就被分到某个组内，做着千篇一律的工作，等到好不容易有机会升到管理职位，才发现自己虽然已是某个领域的专家，但对项目管理以及产品开发的视野却是如此狭隘。工程人员可以培训，基层管理人才的断层才是现代企业的最大的隐忧，有时候把资深工程师拔擢为项目经理反而是灾难的开端，因为工程人员和项目经理必须具备的人格特质与处事态度是截然不同的。”

菜鸟：“您应该不是在说我们PM老大吧？我觉得他还好啊！不但具备工程师的专业技能，对整个产业与项目进度控管的sense也很到位，至少到目前为止，我还蛮服他的。”

系统老鸟：“我当然不是在说他，台湾地区和美国的职场结构不同，在我碰到的美国客户公司中，四五十岁还在当工程师的有很多，而且其薪资或地位并不亚于管理职位。台湾地区信息界的职场生态则是所有资深工程师必须设法争取管理职，所以我是希望你要自我期许有朝一日可以成为称职的管理人员。

好啦，言归正传，在开始做系统整合之前，我们必须先弄清楚哪些程序要在哪里执行？程序可以用哪些区域当缓冲区？程序之外的数据要如何存储等。简单地说，就是配置我们系统中所有存储器的用途，称之为存储器用途配置（Memory Layout），以目前项目为例，你能列出系统中的所有存储器以及它们的用途吗？”

菜鸟：“据我所知，我们的系统包含以下存储器区块。

■　CPU Internal SRAM——CPU 内部有两块 RAM，分别是 40K Bytes 与 16K Bytes。作为LCD控制器的Video RAM，或用来加速程序模块的执行性能。

■　外部NOR Flash——1M Bytes。用来放置程序与data，且程序可以在NOR Flash中直接执行，这块存储器也可以用Mask ROM取代。

■　外部SRAM——512K Bytes。执行时期放置程序的所有变量（data段与bss段）、程序执行时所需的Stack Memory、以及动态配置存储器（Dynamic Allocation Memory）或缓冲区（Buffer）。

■　SD Card。我们的系统可支持SD Card，额外的大批数据可以存储在SD Card中。

这样的架构其实还蛮简单的，我想所有的工程师都知道什么东西该放在什么位置吧。例如，程序应该要放在ROM或Flash，全局变量及buffer应该放在RAM，这些事情怎么可能会有人搞混？”

系统老鸟：“什么东西要放在哪个存储器是一回事，要怎么放进去又是另外一回事。例如，NOR的内容要通过烧录器写入数据或者利用我们自行开发的‘Update程序’更新，SRAM则只需要知道地址就可以把数据写入，至于SD Card则必须通过Card File System来存取其中的文件，不同存储器操作的方式可能都有所差异，一般工程师可能会搞不清楚。而且话说回来，就算是最简单的SRAM，你如何知道要用什么地址去存取它？”

菜鸟：“这个我在写驱动程序时曾经学过，先从线路图或硬件设计文件得知CPU是用哪根Chip Select脚与SRAM连接，然后再查CPU的data sheet，就可以知道这根Chip Select脚连接的存储器的起始地址。”

系统老鸟：“是啊！你知道这些细节，但你觉得应用程序工程师会知道吗？或者说，他们需要知道吗？应用程序可以直接定义全局变量或数组，至于这些变量会被寻址到哪里去则会由系统工程师负责的Link Script决定，但有时系统或应用程序可能暂时需要一块size比较大的buffer，工程师必须要知道系统中哪个地址空间的存储器是可以使用的。此时，他们也只需要知道地址范围即可，所以系统人员必须提供一张简单易懂的图或表格，让所有参与项目的工程人员都知道系统存储器配置的状况。图12-1、图12-2和图12-3所示是一个例子[1]。

▲ 图12-1　Memory Layout 图范例——第一块 Internal RAM

▲ 图12-2　Memory Layout 图范例——第二块 Internal RAM

▲ 图12-3　Memory Layout图范例——外部存储器

系统老鸟：“有了这张图，所有的工程师就不至于写出太离谱的程序。例如，我们的外部SRAM虽然有512K Bytes，但规划为可以当作buffer的空间只有128K Bytes，所以应用程序工程师就知道不能用到150K Bytes的buffer，此时要不改变算法，不然就是和系统工程师讨论重新配置存储器用途的可能性。再举另外一个例子，如果用以加速程序的Internal SRAM在规划中只剩3K Bytes，应用程序工程师就该把要加速的模块控制在这个size内。

基于上述的Memory Layout，我们来讨论一个很常见的状况。如果你有一个约100K Bytes的数据要加入程序中，你的程序要如何编写？”

菜鸟：“我们的项目最后会build出一个Binary File，烧录器可将这个Binary File烧入NOR Flash（或EEPROM），所以我们只要把这个100K Bytes的数据加入Binary File就OK 了，程序执行时，它就会在NOR Flash的某个地址上（图12-3所示的Data File区），利用这个地址，程序就可以使用这个100K Bytes的数据。”

系统老鸟：“基本上大批数据放在你说的‘Data File’区是比较合理的，如果Data File区已经满了，我们还是可以参酌其他存储器的使用状况，把这个大家伙摆到其他的存储器区块域。

■　放置在rodata区（Read-Only Data Section），如程序代码12-1所示。程序中只要用数组的名称即可操作这批数据。

程序代码 12-1

■　如果上述数组不定义为const，则这批数据会被连接到data区（有初值的全局变量）。data区的数据也会占据可执行文件的空间，在执行时期会被复制到RAM中，所以程序可以改变这个数组的值。这个方法会加快操作这批数据的性能（RAM速度一定比Flash快），缺点是执行时期时同一批数据分别在RAM与Flash占据一份空间。如果没有改变内容与加速的需求，这么做实在是浪费空间。

■　当NOR Flash或Mask ROM空间所剩不多，但RAM的空间还充裕时，有时候我们会考虑将这批数据压缩。

也许你还可以想出不同的状况、不同的方法。总之，我要表达的就是嵌入式系统开发往往要到硬件与成本都已立项后才发现存储器不足，只是有时缺RAM，有时缺ROM，此时，系统工程师就要重新审视目前系统的状况，设法将程序或数据全部‘塞’到我们平台的存储器去。你要说这是挖东墙补西墙也可以，我们的任务就是在可接受的成本条件下，尽力完成指定的功能。”

菜鸟：“我问个外行的问题，为什么不在硬件设计初期就把系统的存储器用量估计准确？”

系统老鸟：“你都知道这个问题外行了，如果真的可以估计准确，我们为什么要把自己弄到这个困境中？这个问题的答案可能有3种。

■　真的估不准:嵌入式系统开发项目管理有许多事情在项目前期就要进行估计，包含进度、风险、可达成的性能、需要的资源等，但这些估计只能凭经验，很难有更科学的方法。如果项目复杂度低，或者团队之前做过相似度很高的项目，那么，估计准确的可能性会高一点，否则，碰到复杂度高的项目，在项目初期只有初略的产品规格时，要准确估计存储器用量简直是不可能的任务。

■　硬件设计跑在前面，软件规格却持续变动：硬件规格牵涉到产品造价，所以通常会尽快定下来，但老板或客户总是有错误的理解，认为软件修改没有成本，项目期间就不断恣意变更软件规格。即便项目初期把存储器用量等估计准确了，到后期已经不是同一回事。

■　因为成本考虑，客户或老板帮你估好存储器用量：这是最无奈的状况，老板或客户当然希望用最少的成本做到最多、最高质量的功能，所以有时候客户会事先订下一个离谱的成本底线，或者极为苛刻的硬件规格。另外就是天才的老板、业务人员未与技术人员讨论，就对客户包山包海，答应了一个根本做不到的规格，这样的项目我碰过不知道多少次了，打从项目一开始就知道存储器的用量不够，计算机系统的存储器容量和性能往往有着密切的关系，当存储器不够时，从系统程序到应用程序都要为此付出代价，要不接受较差的性能，要不然就要去研究性能更好的算法。

我想这就是嵌入式系统具有挑战性的地方，在PC上写程序，CPU速度那么快，存储器那么大，程序员不需要对算法有什么研究，就可以写出性能还可以接受的程序，这样的程序拿到嵌入式系统上执行恐怕会慢得像乌龟在爬吧！我记得以前在学校老师说过，算法是计算机科学的精髓之一，而高效率的解题技巧才是一个优秀的软件工程师该追求的，我想这是嵌入式系统工程师必须经过的磨练，难怪PM老大总是说：‘我始终相信完整经历过一个电子产品开发周期的工程师，之后再去参与其他的软件开发项目绝对驾轻就熟，就像在弹尽援绝的状况下打过仗的士兵，对不同的环境会有较好的适应能力，以后什么战场也难不了他。’

菜鸟：“ 了解！这话我也听PM老大讲过几次，现在慢慢可以了解个中深意了。”

12-02　Stack

系统老鸟：“不同的项目会有不同的存储器配置。例如，我用过一颗CPU，它在CPU内部有一小段boot程序，可以从NAND Flash载入程序到RAM中，所以这种CPU就可以不需要由ROM或NOR Flash来启动程序，系统可以只有NAND Flash与SDRAM这两种‘便宜又大碗’的存储器，这种配置就和上一节例子大不相同，但无论是什么样的应用，有些存储区段都是必要的。

■　程序段（text段与rodata段）：可直接在ROM或Flash中执行，也可将某个模块传输到速度较快的RAM里执行。

■　有初始值的全局变量（data段）：会占据可执行文件空间，执行时期必须将其从ROM或Flash传输到RAM。

■　没有初始值的全局变量（bss段）：不会占据可执行文件空间，执行时期必须将该区段的内容全部设为0。

■　Stack（堆栈）。

前3项内的程序或变量，其地址都由Linker产生（当然，必须由系统工程师在Link Script中描述），我们在谈开发环境时都已经说过了，就不再赘述。在此我们来谈谈Stack，你知道Stack有哪些用途吗？”

12-2-1　Stack 的用途

菜鸟：“由CPU内部都有一个SP（Stack Pointer；堆栈指针）寄存器看来，可以得知Stack是CPU运行不可或缺的机制。从CPU的角度来看，Stack的用途如下。

■　在执行“call”指令时，会将返回地址存储（Push）到目前SP寄存器指到的位置（SP始终指到Stack的顶端）。

■　当执行“ret”（Return）指令时，会自目前Stack的顶端取回（Pop）返回地址。

■　当中断产生时，中断控制器会将发生中断的地址（返回地址）以及PSR（状态寄存器）存储（Push）到Stack里。

■　当ISR执行“iret”（Interrupt Return）指令时，CPU会从目前Stack顶端Pop出PSR与被中断的地址。

■　我们都知道Stack这个数据结构特性就是先进后出，CPU提供“push”与“pop”指令供程序员操作Stack，最常见的用途就是暂时将某些存储寄存器的值存在Stack中，在某些动作后，再‘依次’取回这些寄存器的值，如程序代码12-2所示。此外，“push”与“pop”的动作必须是对称的，push多少东西到Stack，就必须记得依次pop多少东西出来，否则下次存取Stack时就数据就会乱掉。

程序代码12-2

如果由C语言程序的角度来看，Stack Memory的用途就更广了。

■　Local变量会存储在Stack中，如果应用程序工程师不知道可以用的Stack有多大，一不小心就会把Stack用爆了。此外还要注意的是，用于嵌入式系统的编译器通常不会产生‘将局部变量的值设为0’的code，也就是说，一旦程序员没有明确指定局部变量初值的话，局部变量的初值可能是任何的值。如果程序员疏忽将没给初值的局部变量拿来使用，则结果自然是无法预测的，例如程序代码12-3所示。

程序代码12-3

■　函数调用时，返回地址当然会根据目前SP寄存器，存储在Stack中，这是CPU的机制，C语言的函数自然也是如此运行。

■　函数调用时参数的传递可以通过寄存器，也可以通过Stack，端视编译器的策略而定。通常如果参数个数不多的话，编译器会倾向用寄存器来传递参数，反之，则把参数存在Stack 中。

如果函数调用太多层，每一层函数的返回地址、参数、局部变量就会一直吃掉Stack。例如，递归的结束条件没控制好，就可能发生Stack Overflow的现象，结果当然是其他的存储器被压到与破坏，虽然我没有实际碰过这样的bug，但你之前说过，Stack Overflow的问题很棘手，因为现象不会在发生的哪一瞬间反映出来，而会一直等到被破坏的存储器被使用时才会知道出了问题，而被存储器被破坏的程序，可能和造成Stack Overflow的程序一点关系也没有。

程序代码12-4所示是个递归的例子。函数内定义了一个100 Bytes的数组，100 Bytes其实也不算太大，但当递归开始返回时，Stack用量已经累加了至少20000 Bytes， 一般嵌入式系统不太可能配置这么大的Stack，即这个函数执行完后，必然已经破坏了其他的存储器。”

程序代码12-4

12-2-2　Stack Overflow

系统老鸟：“原本一般的工程师也不需要知道这些细节，因为在PC有虚拟存储器，操作系统与编译器可以分配一大段的地址空间当作Stack，所以程序员确实不太需要担心Stack Overflow这种事情。但开发嵌入式系统的工程师就不能不知道Stack运行的概念，否则程序编写时完全不顾Stack的大小，局部变量乱用，最后发生的问题将会被归类为‘系统不稳定’。你也知道，这种bug是最难找的。

测试人员只知道某个功能（以下称之为功能A）单独执行时OK，但有时候做了一系列操作后，再去执行功能A就会出问题。这种时好时坏的问题最容易造成管理阶层与QA/QC员工的紧张，偏偏出问题的程序（功能A）怎么看都看不出问题，只知道它会用到的某个变量或某块存储器的值被破坏了。有经验的工程师会推测应该是Stack Overflow或指针误用的问题。但系统里的函数那么多，我们在debug时如何知道到底是哪一个函数在何时造成Stack Overflow，并且破坏了功能A要用的存储器呢？碰到这种问题时，测试人员要尽量归纳出一定会NG的操作手法，工程师才能循着这个手法的路径上找出有问题的函数[2]。”

菜鸟：“总之，不要使用太大的局部变量就不会有问题了吧？”

系统老鸟：“局部变量的大小当然要有所节制，假设系统的Stack只有2K Bytes，工程师在某函数内定义了一个局部变量‘char local_var[2050]’，假使不去操作可能还没事，一旦做了诸如‘local_var[2049] = 0 ；’的赋值操作，当场就超过了原先分配给Stack Memory的存储器配置。至于被压到的存储器是否有意义，属于什么用途，则视系统整体的存储器配置而定。根据我的经验，嵌入式系统开发项目的存储器通常都不会剩下，也就是说，RAM中总是塞得满满的，甚至当存储器不够时，系统工程师马上就会想到可以从Stack Memory中‘偷’，一点来作为其他用途。

除了局部变量的大小必须节制之外，调用递归函数的层数也要小心。每一层会用去的Stack空间包含这个函数的返回地址、参数与所有局部变量的size总和，假使不稍做计算，多调用几层后，同样会造成Stack Overflow。

避免用爆Stack是每一个工程师的责任，除此之外，局部变量的操作也要谨慎。先不管程序的用途，你仔细看看如程序代码12-5所示的程序，是否能察觉这个程序有什么问题吗？”

程序代码12-5

菜鸟：“这个函数只是要把数组a的每个元素都设为100，如果说有什么问题的话，就是用了太复杂的方法来处理这么简单的事。”

系统老鸟：“乍看之下是没什么问题，但这个函数只要一执行，很可能就会死机。它有个简单的逻辑错误，当b一路递减到a时，因为while循环的条件是b>=a，所以最后一次进入循环时，b会等于（a-1），你认为把数组a开头的上一个元素设为100会发生什么事？”

菜鸟：“这叫做Stack Underflow吧！和Overflow不一样吗？前者是破坏Stack内的存储器，后者是破坏Stack外的存储器，总之，就是存储器被破坏了。这是程序的bug，应该很容易可以查出来吧！”

系统老鸟：“说穿了当然很容易，刚刚你不就觉得这个程序没什么问题吗？这种存储器边界的问题在使用某些复杂的算法时还真的很容易发生，尤其是用一个索引变量来存取数组时，不谨慎很容易就会变成Overflow或Underflow，尤其是读取的动作。发生的错误若是立即性的，通常比较容易查，如果是写入的话，问题会拖到被破坏的存储器被用到时才会爆发出来，这种问题就比较难查了。

我们来分析刚刚那个例子。数组a会被寻址在Stack内，而a是这个函数的第一个局部变量，这个函数刚执行时Stack的状态如图12-4所示，你现在知道刚刚那个程序的小疏忽会破坏什么了没？”

▲ 图12-4　Stack Memory范例

菜鸟：“破坏了 Return Address!所以这个函数只要一 return就会不知道跑到哪里执行，可能是data区，也可能是程序区，也可能是根本没有存储器的地址。总之，最终就是死机吧！”

系统老鸟：“通常CPU会有称为“Address Error”的exception（可以把exception想象成CPU引发的内部中断），当程序要CPU去存取明显不合法的地址时，CPU就会引发这个exception。我们可以写一个相应的ISR去拦截这个状况，再设法往前回溯，找出发生问题的点。”

12-2-3　Stack & RTOS

系统老鸟：“最后，我们从RTOS的角度来看Stack Memory的用途，一般用于嵌入式系统的CPU都不会具备虚拟存储器的功能，也就是说系统无法像Windows或Linux 一样为每个程序配置独立的虚拟地址空间，在这样的硬件限制下，你知道RTOS是如何实现多任务的功能吗？”

菜鸟：“我读过许多Windows与Linux的技术文件，以及《MicroC/OS-II: the real-time kernel》这本书，对操作系统的多任务，或多或少还有点了解。在Windows或Linux上的多任务可分为两种，一种是Multiple-Process，另一种是Multiple-Thread。每一个Process有自己独立的地址空间，而且每一个Process内可以建立多个Thread，所以process内的所有Thread则位于同一个地址空间。

举个简单的例子来说明地址空间的概念。可以把process想象成不同的可执行文件（.exe），因为不同的process有独立的地址空间，假设不同的process都去存取同样的地址（如0×100000），实际上，操作系统会将其mapping到不同的物理地址，所以绝对不会互相冲突。至于同一个process内的不同Thread，因为共享地址空间，所以不同Thread存取同样的地址时就可能会引发冲突[3]。因为Thread没有自己的地址空间，所以Thread之间切换的复杂度远比process间的切换低很多，而且由于多个Thread共享地址空间，所以Thread之间的通信相对简单。总之，只要做好critical section保护，Thread的性能较好、程序编写较简单，却同样可以达到多任务的效果，这也是目前Multiple-Threading程序设计方法广泛流行的主要原因。

用于嵌入式系统的RTOS的多任务功能就是上述的Multiple-Thread，通常嵌入式操作系统的书或RTOS的网站会把这个功能称为Multiple-Task，其实是相同的东西，只是名词差异而已。总之，就是所有的执行单位不论称之为Thread或task，都共享同一个地址空间。RTOS多任务功能的实现方法我不是很清楚，只知道每一个执行单位都要有自己的Stack， 一般嵌入式系统多任务程序的风格如程序代码12-6所示。”

程序代码12-6　多任务（Multiple-Threading）程序范例

系统老鸟：“不错，你的理解很正确，很难得有菜鸟可以把这些原理讲得头头是道。从上面这个范例可以看出来，所谓的Thread或Task不过就是一个函数罢了，编译器根本就没有Thread的思想。举个极端的例子，就算在task2中调用taskl()，即使逻辑大有问题，但因为语法完全符合C语言的规范，所以编译器绝对不会报错，即Thread之间完全没有隔阂，当Thread交互执行时，如果系统还是只有一个Stack肯定会天下大乱，所以RTOS的多任务功能会让每一个Thread有自己的Stack， Thread的Stack和之前提到的系统Stack功能相同，包含函数调用时的参数、返回地址及局部变量等，只是每一个Thread可以根据需求配置不同大小的Stack。

你不妨思考一下，刚刚我们提到许多程序员的注意事项，用以避免破坏系统的Stack。如今每一个Thread有自己的Stack，而且还是用Global Array的方式配置存储器，写程序时是不是应该更小心一点？”

菜鸟：“在没有多任务需求的应用时，系统工程师会根据存储器的使用状况，分配一块存储器区块域当作系统的Stack， Stack的起始地址与结束地址是由系统工程师指定，而非连接时期由Linker算出来的。如果我身为系统工程师，我想我在指定Stack的区域时会在前后保留一点缓冲的空间，如果有不可预测的状态导致Stack Overflow 了一点，也不至于让系统死机。

假使是用Global Array的方式配置每一个Thread的Stack时，对Linker而言，这个array和一般的全局变量并没什么不同，所以，当然会和其他全局变量寻址在一起（bss段），其前后各是什么变量是由Linker决定的，也就是说，当Stack Overflow或Underflow时，一定会破坏到别的变量，至于如何查出哪些变量会被破坏，我就不知道了。”

系统老鸟：“你忘了 Linker会产生Symbol Table或MAP文件吗？在里面你可以很容易查出变量的前后关系，想知道Stack操作可能破坏的变量，在MAP文件中找出代表Stack空间的全局变量，再看看它的前后各是哪些变量就知道了。”

12-2-4　Context Switch

菜鸟：“从前在修操作系统这门课时曾学过Context Switch，记得当时老师也只是随便带过，反正就是操作系统执行程序间的切换，至于实际是怎么实现的，实在一点概念都没有。”

系统老鸟：“基本上可以想象成多个Thread轮流在CPU上执行，但无论如何，CPU只有一个，一段时间中只能有一个Thread获准取得CPU的使用权，同时RTOS必须妥善保存其他被搁置的Thread的执行状态，当换成另一个Thread执行时，它才可以顺利地从上次被Task-Switch的位置继续执行。RTOS用一个数据结构记录各个Thread的执行状态，我们称这个状态为Context（task之间的切换也称为Context-Switch）。每个Thread的Context中包含了该Thread上次被切换出去时所有寄存器的值以及一些系统信息，而Thread的Context会存储在自己的Stack中，Context Switch也是通过操作Stack完成。”

菜鸟：“不太了解Context Cwitch和Stack有什么关系，您可以说明一下Context Switch是如何完成的吗？”

系统老鸟：Context Switch的原理很简单，但程序并不是很好懂，你要先清楚一件事。在一般状态下，CPU是循序地在执行程序，程序可通过某些手段改变PC（Program Counter）的值，就可以迫使CPU跳到另一个地址去执行，一般的方法是function call与goto。另外有个较具技巧性的方法是：修改在Stack中函数的返回地址，则程序执行return时，就不会返回原来的caller function，而是跳到另一个地址去执行，如图12-5及其中的pseudo code所示。”

▲ 图12-5　通过修改Stack的技巧改变执行顺序

菜鸟：“因为return命令会到Stack中获取返回地址，原本B函数执行完毕后应该要返回A函数，但经过B函数对Stack的‘恶搞’，结果B函数返回时居然跑到C函数去了。这个技巧确实很神奇，但和Context Switch有什么关系？”

系统老鸟：“如果你能接受这个技巧，接下来我们才能继续说Context Switch的原理。你想想，刚刚那个例子的流程像不像task A执行了 Context Switch的函数（B()），然后就跳到task C去执行了？如果我们有办法知道A函数中“a = 0;”这行指令的地址（被Task Switch的地址），是不是也有办法从task C跳回task A?如果函数B有一个表格记录所有task的地址，那么，它是不是有办法切换到任意的task?”

菜鸟：“对耶，真神奇！ 一般的function call每次都要从函数的开头执行，利用操作Stack的技巧就可以做到类似task切换的效果。”

系统老鸟：“真正Context Switch的做法比上述的例子更具技巧性，它不但操作Stack的内容，而且因为每一个task都有自己的Stack，所以还必须改变SP （Stack Pointer）寄存器的值，这已经是我们系统中操作Stack技巧的极致了。Context Switch通常是通过一个可重入（Reentry）的函数完成，每一个task被切换出去的地址正是这个函数的最后一行（return），听起来有点玄对不对？看了程序代码12-7中的pseudo code，你可能会清楚一点。”

程序代码12-7　Context Switch程序范例

系统老鸟：“第一次看不懂没关系，多看几次就会有感觉了。一般来说，我们很容易取得或购得某个CPU平台的RTOS，所以除非必要，否则我们不会自己写RTOS。所谓必要的状况其实也常常发生，例如，我们团队在μCOS Ⅱ上累积了很多程序，但目前项目指定的CPU厂商提供的RTOS却是另外一套，此时，我们就要评估是否要将μ COS II移植到这颗CPU上。其实移植RTOS的困难度并不高，Context Switch的功能算是最复杂的了。”

12-2-5　没有足够RAM供Stack使用的平台

菜鸟：“我们谈了很多关于Stack的应用，Stack空间可说是实现C语言与RTOS的基础，我突然想到之前在学校修微处理器这门课时，记得8051只有256 Byte的Internal RAM，是Byte耶！当初还不清楚C语言程序运行的细节，现在知道的越多，越搞不懂当初我在8051平台上，用C语言做的项目是如何执行起来的？总不可能把256 Bytes都拿来做Stack啊！”

系统老鸟：“8051系列的8 bit MCU，还是目前世界上使用量最大的主控IC，即使在可见的未来，这个状况仍不会有所改变，所以当我们在谈嵌入式系统开发时，绝对不能忽略它。8051最麻烦的地方除了寻址空间最大只到64 KB之外，Stack也是个大麻烦！

无论几bit的CPU，采用高级语言（C语言）已是现代的嵌入式系统的开发趋势，但8051的 SP （Stack Pointer）却只能指到 256 Bytes[4]的Internal RAM，这会导致 8051 的 C Compiler 无法根据我们刚刚说的原则，产生C语言程序所对应的汇编语言代码，否则在runtime动不动就会 Stack Overflow。

8051的C Compiler势必得另辟蹊径，其中最著名且广为开发商使用的就是Keil C，以下我们就以Keil C的做法来说明。如果你是8051 C Compiler开发商，想想必须解决的问题有哪些？”

菜鸟：“在一般C语言的实现方法中，函数中的局部变量会从Stack中配置空间，而函数调用时的参数传递与返回值也都是通过Stack，如果只能使用这256 Bytes当作Stack，那么写C语言程序的工程师更得小心翼翼了。工程师在写某个函数时，并不知道runtime，当这个函数被调用时，前面已经调用了几层，各层的函数又分别使用了多少Stack，所以也无法知道这个函数的局部变量、参数、返回值等该使用多少空间才是安全的。

如果我是8051 C Compiler开发商，我认为首要任务是设法让工程人员可以专注在程序编写，不要时时担心Stack是否可能会Overflow。”

系统老鸟：“Keil C会从两方面来解决Stack size不足的问题。

首先，Keil C Compiler会分析所有函数之间的调用关系，以及每个函数会使用的Stack空间大小，编译完成后会生成*.M51文件，就如同我们前面说过的map file，里面会列出程序的存储器空间的配置，其中包含Stack所剩的空间。对8051来说，memory size有限是已知的硬件限制，所以工程师在写作8051的程序时，还是必须养成节省存储器的好习惯，尽量去除没必要的变量。

第二， 8051虽然有一般用途的Stack和Stack Pointer，但Keil C可以不用像标准C Compiler那样，将函数里的局部变量都放在Stack中，反而是将局部变量都配置到RAM中固定位置（除了内部的256 Byte RAM外，8051也可以外接RAM，但CPU还是‘认为’ Stack只有内部的256 Bytes。）。

Keil C提供了几个等级的优化选项，当设定的优化等级大于2以上，Keil C Compiler会建立Function Dependency Tree，以确定哪些函数不会有调用的关系，则这些函数里的局部变量可以共享空间，称为Data Overlaying。这种处理方式除了能够避过使用内部仅有的256 Bytes来存储局部变量外，同时还能节省所需外部RAM的空间，因此，Keil C的优化等级2以上，对节省RAM空间有很大的帮助。

另外一个影响RAM用量的优化等级4， Keil C会判断当前函数所产生的code使用寄存器的状况，在容许的时候，会直接配置寄存器作为局部变量，此来也能节省RAM的使用。

当然Keil C所做的不仅于此。例如，高级的Keil C版本在连接时期，可找出完全没用到的函数，并且不会将其连接到最终的可执行文件，以免浪费空间，所以工程师使用C语言进行开发时已是相当方便，通常只要知道Keil C帮忙做了哪些事情，加上程序编写习惯不要太差，应该都不会碰上太大的问题。”

菜鸟：“听您这样说来，其实是Compiler帮忙解决了 C语言程序执行时，实际Stack空间太小的问题，所以一般C语言程序编写还不算大问题。但是RTOS呢？ RTOS的基本原理就是每个task都要有自己的Stack空间，在8051上有办法实现RTOS吗？”

系统老鸟：“始终会有人问像是‘在8051这样的CPU上使用RTOS是否有意义？’这样的问题，而一般的答案都是负面的，但Keil C内置一套8051的RTOS-RTX51 Tiny，如图12-6所示，该有的功能一应俱全，我想Keil C已经帮忙回答了这个问题。我们一再强调模块化的好处是值得付出其他的effort去追求的，若在某些应用上，使用了 RTOS的多任务功能可以达到更佳的模块化，那我们就应该使用RTOS，并设法处理存储器的问题（RTX51 Tiny的code size只有 900 Bytes）。

言归正传，市面上也有不少书籍说明如何在8051上实现RTOS，例如μ COS就有8051版本，也容易取得原始码。它的实作方式是在切换Task时，将目前task的整个Stack都搬到外部存储器去，然后又从外部存储器搬到另一个task的Stack回来。”

▲ 图12-6　RTX Tiny的功能列表

12-03　Heap：动态存储器配置

系统老鸟：“除了 Stack外，系统里还需要保留一块或多块供程序员动态使用的存储器，一般书中都称之为Heap，我个人则比较喜欢‘动态配置存储器’这样的说法。”

菜鸟：“我们常在程序语言的教科书中看到Stack和Heap的关系图，如图12-7所示。我以前就很担心，如果Stack或Heap无限制地长下去的话，不是就会压到对方吗？在嵌入式系统中，也是这种架构吗？”

系统老鸟：“在PC上，因为每个程序都有自己独立的地址空间，编译器与操作系统可以分配很大的区域来做程序的Stack与Heap （用到时才需要分配物理存储器），所以，基本上你的担心完全是多余的。如果真的有PC程序让Heap与Stack的顶端交会了，那么，这个程序要不是复杂的不得了，不然就是错的很离谱。

在嵌入式系统就没这么好运了。除非使用了有MMU （Memory Management Unit；存储器管理单元）的CPU，否则就得由系统工程师计算所有程序可能的最大的动态存储器用量，并根据系统存储器的状态，配置一块足够大的区域当作Heap。在嵌入式系统中，我们可以根据需求与应用自行配置存储器的用途，例如，某个主要的应用在计算时就一定要用掉100K Byte的存储器，此时，系统工程师就可以分配一块适合大小的存储器让这个功能专用。这样的存储器区块不是我们即将讨论的Heap，所谓的Heap （或说动态存储器）是工程师可以通过诸如malloc()与free()这样的函数，或是C++的new/delete operator来操作的存储器区块。

通常这个Heap我们也可称为Memory Pool，这个名字是不是比较传神一些？相信你以前在学校修数据结构时应该学过存储器管理吧，像是先适法（First-Fit）、最不适法（Worst-Fit）与最适法（Best Fit Method）都还记得吗?”

菜鸟：“早就忘光了，我以为这些算法就像线性代数或工程数学一样，应该一辈子都用不到。我只依稀记得动态存储器管理就是一大块存储器，应该就是你说的Memory Pool，根据程序allocate时的size切割成一块块串起来的存储器区块，每块存储器的开头是一个数据结构，称为Memory Control Block （MCB），记录有该存储器区块的size、下一个/上一个存储器区块的地址以及其他信息等。说很难讲清楚，看如图12-8所示的内容吧。”

▲ 图12-7　传统Stack与Heap的关系图

▲ 图12-8　动态存储器管理架构图（1）

12-3-1　Memory Allocate & Free

系统老鸟：“当然还有许多动态存储器管理的算法，像是边界标示法（Boundary Tag Method）及伙伴系统（Buddy System），但在嵌入式系统中，我们通常用的就是这种。你画的这张图就是Heap管理的基本架构，而所谓的First-Fit、Worst-Fit及Best Fit Method等算法，就是当其他程序调用malloc()时，如何在这些串起来的存储器区块中找到合适的区块，今天就姑且帮你复习一下吧！

假设所需求的存储器size为N。

■　First-Fit：自Memory Pool中存储器队列的开头往下找寻，直到寻得第一块空间大于或等于N的可用存储器区块。

■　Best-Fit：寻遍整个存储器队列，选择一块空间最接近N的可用存储器区块。

■　Worst-Fit：选择一块空间最大的可用存储器区块。有时采用此法的原因是size最大片的存储器区块段使用了一部分后，仍可供其他程序使用，减少小片段存储器区块的产生。

因为Worst-Fit与Bets-Fit Method都要寻遍整个存储器队列才能选出合适的区块，当队列成员很多时，性能自然会变慢，所以我们通常都采用最简单的First-Fit Method。”

菜鸟：“我想起来了，在学校修算法时的确有学过这些东西，我还记得这3个方法都各有缺点，First-Fit固然简单，但容易制造许多size小到为无法供程序使用的存储器区块（碎片），而且每次都从头开始寻找，假使程序复杂一点，势必会在存储器队列的开头集合了很多碎片，则搜寻可用区块的速度会越来越慢。”

系统老鸟：“这的确是First-Fit的缺点。我们很难完全避免碎片的产生，但是可以通过以下3个方法减少碎片的产生或减缓碎片的影响。

■　取一适当常数n，若配置后剩下的存储器size小于等于n，则整块存储器都分配给要求配置存储器的程序，这样可以减少小块存储器区块的产生。

■　将存储器队列构建成环状队列，每次搜寻可用存储器区块都自队列的不同点开始搜寻，使得小块存储器可以尽量均匀分布在整个队列中。

■　程序归还存储器区块时，系统必须检查该存储器区块的邻边是否也是空闲区块。如果是的话，必须将其合并为较大的一块存储器区块。

这些方法都不会太复杂，基本上，我们系统用的动态存储器管理算法就是First-fit-Method加上这3个策略。”

12-3-2　API

菜鸟：“这个动态存储器管理原理虽然很简单，但实际运行起来应该状况很多，谁知道其他程序会怎么用它？我想，应该要花很多时间来测试相关的程序吧？”

系统老鸟：“大量测试是需要的，但我们不会重新写动态存储器管理的函数，你只需要知道动态存储器管理的与原理与思想即可。因为我们太容易可以取得现成且已应用在嵌入式系统的API及其source code。这个API很简单，只有3个函数。

基本上，系统工程师只要把Heap存储器区块段的起始地址与size传入，其他程序就可以使用malloc与free这两个函数（当然函数名字可能不一样）向系统要求配置动态存储器。我们系统中的动态存储器管理程序也是从GNU ANSI-C函数库中‘抄来的’，有空试着把程序看懂，不会很难的。”

程序代码12-8所示的程序取自GNU ANSI C函数库中的动态存储器管理模块（如图12-9所示），它使用另一种算法，存储器区块的信息不是分散记录在MCB中，而是统一记录在一个表格里，但基本原理是一样的。这是个效率还不错的算法，实际上也通过产品化的验证。系统工程师只要先配置出一个存储器区块域，将起始地址以及size传入ansi_InitMalloc()函数，以后应用程序就可以用malloc()与free()来使用动态存储器配置的功能。虽然程序基本上不是太复杂，你实在也无需完全看懂，只要能将其移植到自己的系统中运行即可。

▲ 图12-9　动态存储器管理架构图（2）

程序代码12-8　动态存储器管理算法范例

12-3-3　碎片处理？

系统老鸟：“随着程序每次执行的顺序与状态不同，会调用allocate/free的次数、顺序、大小也不同，当然，Memory Pool内存储器区块的状态也会大不相同。久而久之，整个存储器队列中会出现许多分散在队列各处，size比较小，明显用不到的存储器区块，这就是所谓的碎片（Fragment）。最差的状况是明明整个队列中可以用的存储器区块size总和还很大，但程序要求配置存储器时却找不到一块足够大的存储器区块，如果产品出货后才发生这个问题就麻烦了。对使用者来说，会感觉某个功能本来可以用，怎么突然不能用了？这个状况多发生几次，使用者的结论就是系统不稳定。

这是个很棘手的难题，整个系统一定是经过长时间的运行才可能产生过多的碎片，至于中间到底做了什么操作实在很难一一记录，也就是说，不容易找到一个手法可以让问题重现，所以工程师要追踪问题并不容易。而且这种问题通常是长久累积下来的，并不一定是哪一支程序的bug，要修改就比较困难。”

菜鸟:“既然碎片的问题那么麻烦，我们难道没办法像Java 一样实现Garbage Collection吗？”

系统老鸟：“你要先知道两件事。第一，在C语言中，系统配置一块存储器给程序后，程序得到的是一个指针，以后，程序就是通过这个指针来操作这块存储器。第二，所谓的Garbage Collection就是设法将所有不用的存储器或者碎片集合成一块，假使系统提供虚拟存储器的功能，那么，只要改变虚拟地址和物理地址的对应关系，程序就可以使用同样的指针继续正确运行。如果我们的系统无法提供虚拟存储器，而存储器管理架构就如图12-7所示一样，你觉得我们要实现Garbage Collection会碰到什么问题？”

菜鸟：“要收集Memory Pool里所有的碎片，最简单的方式就是将所有已配置的区块全部往上移动；其他available的区块，无论大小，就可以结合成一块，则Memory Pool内就再无碎片可言，如图12-10所示。可是移动已配置的区块自然就会改变其地址，也就是说，当碎片整理后，各个已配置区块的地址已经和程序调用malloc()时取得的地址不一样了，应用程序是用一个指针变量来记录动态存储器区块的地址，存储器管理程序如何有办法去改变应用程序里指针的值？所以，在这种存储器管理架构下是无法实现 Garbage Collection 的啰？”

▲ 图12-10　碎片收集

系统老鸟：“要做还是有办法的，只是当应用程序调用malloc()时不能直接传回指针，而是传回一个handle。存储器管理系统必须维护每一个已配置区块的handle与地址的对应表，而应用程序每次要使用已配置的存储器区块之前，都要先通过API将handle转为实际的地址。使用这样的做法，就可以在做完碎片收集后，即使各个存储器区块的地址已经改变，应用程序仍然可以正常运行，如程序代码12-9所示。”

程序代码12-9

菜鸟：“这样的做法不但程序编写麻烦，而且性能恐怕也会受影响吧？”

系统老鸟：“没错！更严重的是工程师往往容易疏忽甚至阳奉阴违，取得动态存储器的地址后就一直用，当系统做完一次Garbage Collection后，Memory Pool内的存储器区块都被移动了，使用到动态存储器的程序如果没有重新由handle取得正确的地址，当然无法正确运行。原本验证OK的程序，加入Garbage Collection的机制后都变得‘不稳定’ 了，当时的情况只能以哀鸿遍野来形容。当然我们可以要求工程师审慎检验查看他们的程序，可是有些屡见不鲜的函数库，要将所有的指针操作修改为handle操作恐怕是一个大工程，而且只要一个地方没有改到就会出问题，一定要经过严谨的测试才行。如果是没有source code的函数库，而且其中使用了malloc/free等函数，那更是不知该从何改起。

基本上，嵌入式系统和Windows或Linux等开放性系统还是有本质上的不同，除了 Netbook或Smart Phone这种可以让使用者额外安装程序的系统之外（但通常这种系统会使用具有虚拟存储器功能的CPU），电子产品可能的操作方式是被限制的。也就是说，即便是再复杂的应用，程序可能的执行流程‘大部分’应该是可以被掌握的。所以我们可以要求工程师改善使用动态存储器时的程序编写习惯，以减少碎片产生的机会。基本的原则就是：‘用完即还’、‘有借有还，再借不难’。

碎片往往都是因工程师的疏忽，或不好的程序编写习惯所造成的，例如：

■　allocate 了一块存储器，当不使用后却没有归还，这样就会产生一个垃圾碎片。如果这个有问题的模块一再被执行，系统累积的垃圾碎片就会越来越多，最后终于导致程序要不到任何存储器可以用。

■　负责allocate动态存储器的程序与负责free的程序属于不同的模块，这也是碎片产生的原因之一。主要是因为不同模块的设计逻辑、设计理念与负责工程师可能都不相同。实际上，这种设计方式确实经常造成有些存储器区块没有被free，应该尽量避免这样的设计。

■　连size很小的存储器区块（如一个short int）都要向系统allocate。这样的程序风格未免有点矫枉过正了，无谓增加Memory Pool中的区个数，对动态存储器管理系统的性能自然会有所影响。

■　因为动态存储器管理程序会把邻接的未使用的存储器区块merge成一块，所以工程师在配置与free存储器区块时要稍微考虑一下顺序，有时候稍微改变一下顺序就可以避免碎片的产生如程序代码12-10所示。

程序代码12-10

换成如程序代码12-11所示的顺序会不会比较好呢？虽然不能保证一起配置的存储器区块在Memory Pool中一定会是连续的区块，但几率总是高一点，在这种状况下，当Ptr_C与Ptr_A归还后就可以被merge成一块。若按照原来程序的顺序，Ptr_C与Ptr_A归还后变成碎片的可能性很高。

程序代码12-11

■　通常整个系统包含许多应用程序或独立功能的小模块，当某个应用程序执行完毕后，应该要归还所有配置的存储器。在开发期间，当系统在处理应用程序切换时，可以检查Memory Pool中是否还有未归还的存储器区块，如果有的话，可以视为错误，要求负责的工程师修正。”

菜鸟：“您的意思是说，我们的系统中通常不会在执行时去处理碎片的问题，而是要求工程师改善写程序的习惯，以控制碎片的产生？”

系统老鸟：“嵌入式系统的应用范围通常很固定，所以碎片问题其实不是那么难以控制。而且我们也试过许多方法来改善碎片问题，例如使用两个（或多个）Memory Pool，一个专用来配置小块的存储器（如64 Bytes以下），另一个则配置大块的存储器。这些方法虽然都可以某种程度地改善碎片的问题，但都仅是治标不治本，效果都不如改善配置存储器的程序风格。而且这些程序风格的要求在项目一开始时，就该明确传达。否则当程序越来越复杂，且程序中的alocate/free都未按照规范写作时，要通过检查程序的行为来改善碎片的状况已是缘木求鱼，为时晚矣。”

12-3-4　保护Heap?

菜鸟：“我们讨论过Stack Overflow与Underflow的问题，如果程序不谨慎使用从Heap配置的存储器，很容易就会破坏其他用途的存储器。而且根据Memory Pool中存储器区块的数据结构来看，一旦程序超用某个动态存储器区块时，第一个被破坏的，就是下一个存储器区块（MCB）的header，此来整个Memory Pool中的存储器区块队列就会乱掉。有方法防止这种状况发生吗？”

系统老鸟：“除了要求工程师改善程序编写习惯之外，几乎没有预防Stack被误操作的方法，Heap被破坏的问题也是如此，而且更容易发生。再加上破坏者和被破坏的存储器区块可能是由完全不相关的程序所配置，当发现某个存储器区块被破坏时，破坏者的存储器区块可能早已归还，或已经再配置给其他程序，从存储器队列的现状根本无从查起。

当某个程序会破坏Heap时，被影响的程序可能每次的执行结果都不一样，这种问题在系统整合阶段（或系统还不稳定的时候）出现简直是要人命，感觉到处都有问题，工程师疲于奔命地检查曾经出过错的程序，但真正的元凶反而暂时不会被怀疑。”

菜鸟：“如果最后查出许多bug都是因为Heap被破坏所导致，那我们该如何找到始作俑者呢？”

系统老鸟：“说穿了方法也不复杂。首先，系统要先实现一个检查目前存储器队列是否正常的函数，如sys_checkMemoryList()，这个函数也很简单，只要遍寻整个Memory Pool队列，把所有区块的size累加，看是否等于原先Memory Pool的size。或者存储器区块的header中可以包含特殊的信息，例如owner ID或magic number （header某个栏位的值一定是特殊数字，如果不是的话，表示已被破坏），若有异状表示Heap也已被破坏。

有了这个函数之后，接下来就是找出到底是哪段程序执行后，Heap才出现问题的。如果是你，会怎么做？”

菜鸟：“先找出一个最简单就会出问题的操作手法，然后利用上述检查存储器队列的函数（sys_checkMemoryList()），确认在问题发生之前，存储器队列已经不正常了。然后让在出问题之前所有执行过的函数，都去调用sys_checkMemoryList()，就可以查出在哪一个函数内存储器队列的状态由OK变成NG，仔细检查一下这个函数就八九不离十了。”

系统老鸟：“基本上没错，虽然感觉上像是暴力法，但反而是最有效的。值得注意的是，当存储器队列状况较复杂时，sys_checkMemoryList()可能要花掉不少时间，所以要小心这些用来调试的程序，千万不能被编译进正式版本中。”

12-04　烧录器

菜鸟：“我们讨论了许多工程师程序编写时关于存储器的注意事项，大部分都是RAM中存储器区块的保护，包含Stack与Heap。在使用其他存储器时是否也有要注意的地方？”

系统老鸟：“一般工程师只需要了解RAM的应用即可，例如data段、bss段、Stack及Heap等，但系统工程师则必须对系统中所有的存储器配置了然于胸，包含用以存放与执行程序的ROM或Flash。即便产品在最终生产时是采用只读的Mask ROM，因为开Mask （光罩）旷日费时，在开发期间，为了便于频繁的测试，势必要使用可重复读写的存储器设备代替，如EEPROM、NOR Flash或ROM模拟器。

之前我们已经提过ROM模拟器，顾名思义，它是一个用来模拟ROM的设备，工程师可以把这个设备一边连接到机器上ROM的插槽，一边连接到PC （串行端口或USB），然后从PC将欲烧录到ROM的内容（Binary File）传送到这个设备中，则机器就可以执行或存取ROM模拟器中的程序或数据，就如同有一颗真的ROM焊在机器上一样。至于要将Binary File写入实际的EEPROM或NOR Flash中，则必须通过烧录器。烧录器的种类、品牌繁多，好的烧录器有容易使用的PC端控制程序、可以支持多种存储器种类，或者可以同时烧录多颗存储器芯片。

如果产品量产时是采用EEPROM、NOR Flash或其他可重复写入、断电后数据不会漏失的存储器，量产时就必须先对大量的存储器芯片执行烧录的动作，之后PCB板才能进SMT以及接下来的生产流程。为了提升产能，工厂使用的一定是同时可烧录多颗存储器芯片的烧录器（如图12-11所示），而且为了减少操作员操作失误的风险，操作方法一定要尽量简单，通常是由技术人员设定好之后，操作员只要按一个键就可以开始烧录一批存储器芯片，烧录好后由机器上代表每一个socket的灯号来判断OK或NG（如绿灯表示OK，红灯表示烧录失败），操作员很容易就可以挑掉NG的芯片。”

▲ 图12-11　用于量产的大型烧录机台

▲ 图12-12　烧录器PC端程序

菜鸟：“实验室中的烧录器我曾用过几次，PC上的控制程序还蛮容易操作的，如图12-12所示，烧录的流程如下。

Step01 ：将Binary File转换成烧录器控制程序可接受的格式，如HEX或S-Record，这些都是标准的格式，通常烧录器或编译器套件中都会包含相关的转换工具。

Step02：载入欲烧录的文件。

Step03：擦除（Erase）存储器芯片的所有内容。有些存储器芯片不能用控制的方式，必须先经过紫外线照射，才能擦除存储器芯片内的所有内容，如图12-13所示的设备。

Step04：将欲烧录的内容写入Program存储器芯片。

Step05：自烧录完毕的存储器芯片中读出内容与原始数据比较（Verify），确认烧录的正确性。

▲ 图12-13　紫外线Eraser

我觉得这种烧录器除了操作简单之外，更大的优点是可以用同样的速度，同时烧录多颗存储器芯片，这样的功能应该是量产时，为了增加产能所量身定做的吧！”

系统老鸟：“通常代工厂内都会有一组专门负责烧录的单位，当产品量很大时，如1KK（一百万台），烧录这个动作就是一个大工程，一颗一颗要烧到什么时候？几乎每一个电子产品都有烧录的需求，因而有了提供代客烧录服务的公司出现（广义来说，Mask-ROM的制作也是一种烧录的动作），通常就是设计、制作烧录器的公司。通过这些公司进行存储器芯片烧录，虽然成本增加，但可省下购买烧录器的预算，而且烧录质量及进度的风险，都可转嫁给代客烧录公司。”

菜鸟：“工厂里最多的不就是人力吗？而且烧录器一台会贵到哪里去，这种完全没有技术性工作还需要外包吗？”

系统老鸟：“你相信我们买过一台70多万台币的烧录器吗？这么贵必然是因为其支持特殊格式或特殊用途，加上工厂买了两台，3台总共就是200多万，这都算是项目的成本。如果产品的量不大，毛利不高，或者以后很可能就不再使用了，在这种状况下，烧录工作外包似乎是比较划算的抉择。”

菜鸟：“哗！ 70万一台烧录器，它会飞吗？”

系统老鸟：“它不但不会飞，目前还安安静静地躺在实验室的角落，恐怕已经长蜘蛛网了，但它还不是最贵的，基于成本考虑，有时我们还是会选择代烧服务。以下是代烧服务的工作流程，你可以参考一下。

Step01 ：确认订单。

Step02：排程：根据要求安排设备以及人力，并将烧录容量优化，以缩短交货进度。

Step03：进料：确定来料之种类、数量正确。

Step04：烧录：大量烧录前，必须再次确认机器设定及烧录内容正确性，烧录时要严格监控烧录质量及良率，并严格保密与控管烧录码。

Step05：烧录QA: QA人员检验烧录码正确性及有否弯脚、混料。

Step06：打印：照客户要求的文字、颜色、位置自动打印。

Step07：包装：按客户要求的材料、方法包装成品。

Step08：外观QA:就打印、混料、弯脚，以及包装材料、包装方法做外观检查。

Step09：出货：确保将正确的产品与数量，实时运交给正确的客户。

一般烧录EEPROM或NOR Flash的烧录器相当普遍，原则上就是按照ROM image的内容，一个个Byte写入memory即可，但NAND Flash比较特殊，因为每颗NAND Flash的Bad Block分布都不同，如果按ROM Image循序写入，难免会试图把有效数据写入Bad Block中，所以烧录器在烧录NAND flash时必须做更多事情，而且往往必须客制化烧录器firmware。相关细节等日后更深入讨论NAND Flash时，再详细探讨[5]。”

12-05　突破物理存储器大小的限制

菜鸟：“电子产品生产总能省一毛就拼命去省，导致产品的规格与物理存储器大小总是不对称，前面我们谈了那么多关于存储器的注意事项，看来嵌入式系统工程师永远逃不了与存储器问题纠缠的宿命。”

系统老鸟：“这也是没办法的事，当电子产品的差异化越来越困难，成本因素当然变得更为重要。对做生意的老板们来说，减少产品的物理存储器size只是简单的数字考虑，但对嵌入式系统软件工程师而言，则可能造成结构性的影响。

基于这样的业界生态，除了系统工程师必须不断精进之外，SoC设计与Compiler厂商也提供了不少解决方案，让产品设计可以以小博大，用较少的物理存储器来实现较多的功能。我们来介绍3个比较常用的方法。

■　虚拟存储器（Virtial Memory）:要让程序认为自己有足够大的虚拟存储器空间可使用，通过CPU所提供之虚拟地址与物理地址的转换机制，暂时没用到的存储器内容可以存储在Secondary Storage （如硬盘、NAND Flash）中，当程序access到相应的虚拟地址时，CPU会产生硬件事件（Page Fault），通知系统从Secondary Storage置换存储器内容。而这一切对应用程序来说都是透明的。

■　动态载入：并非所有的CPU都会提供虚拟存储器的功能，但系统仍可使用Secondary Storage当主存储器的缓存区，只有在需要某个程序或数据区段时，才将其载入主存储器的特定位置。因为这个方案没有CPU介入，系统或应用程序必须做较多的处理。

■　Banking： 8 bit MCU的寻址空间只有64 KB，对较复杂的应用远远不足，就算使用较大的物理存储器也无法突破MCU既有的硬件限制。一般的解决方案为系统可利用GPIO模拟地址线，扩充寻址空间，使得系统可以使用大于64 KB的存储器。大部分Compiler都会提供banking的支持，即系统只要提供操作模拟地址线之GPIO的程序，Compiler会处理相关寻址的工作。对应用程序而言，寻址空间扩充也完全是透明的。

12-5-1　虚拟存储器

菜鸟：“以前在学校修操作系统课程时曾学过虚拟存储器的原理，修课时大家都认为自己哪有机会开发操作系统，所以都不怎么用心，后来准备考研时才又把相关知识复习了一下，但还是有些似懂非懂的。”

系统老鸟：“虚拟存储器的原理如同其名，确实有点虚无飘渺，不是那么容易明白。幸好要自己去实现相关功能的机会并不多，当系统要用到虚拟存储器功能时，表示程序的footprint必然不小，即系统相对较为复杂，此时，我们会选用诸如Embedded Linux、Windows Mobile这种大型系统，而它们已经对虚拟存储器做了最佳处理。

因此，你能够了解虚拟存储器的基本原理也就足够了。考考你，还记得虚拟地址与物理地址的分别吗？”

菜鸟：“这个基本的知识我还知道。

■　虚拟地址：以32 bit的CPU来说，应用程序会认为其有完整的4 GB空间可用，但物理存储器当然不会那么大，所以应用程序可使用的空间称之为虚拟存储器空间，而应用程序指令送出的地址都是虚拟地址。

■　物理地址：顾名思义，物理地址就是实体RAM的地址。CPU里的MMU （存储器管理单元）和操作系统要做的事情，就是将应用程序传来的虚拟存储器转换为物理地址，并将正确的内容放到这个物理地址。”

系统老鸟：“你说到重点了，一般人最容易搞混的就是‘谁’，用‘哪一种地址’。图12-14所示可以看得比较清楚。从硬件来看，CPU接收到的都是虚拟地址，而MMU负责将虚拟地址转为物理地址，并据此操作物理存储器。”

系统老鸟：“从软件来看，应用程序与操作系统用的都是虚拟地址，但操作系统必须维护虚拟与物理地址的对应表，因为地址对应的基本单位为Page （Page Size乃基于CPU的设定），所以称此表格为Page Table。

Page Table还必须并记录目前在虚拟存储器空间里的内容（包含程序和数据），哪些Page在实体RAM内，哪些Page被置换到Secondary Storage中，以及这些Page在RAM与Secondary Storage的位置。图12-15所示说明Page Table必须记录的对应关系。”

▲ 图12-14　虚拟存储器硬件运行原理图

▲ 图12-15　以小博大——page mapping

系统老鸟：“图12-15所示的左边表示虚拟地址，可以看到虚拟存储器比物理存储器大很多。无论如何，物理存储器的size是有限的，所以虚拟存储器空间的每一个page，不是已经对应到物理存储器的某个page，就是被存储在Secondary Storage中，而Page Table正是用来记录这些信息，另外需要注意的是，MMU也是利用操作系统所维护的Page Table进行虚拟/物理地址转换。

以图12-16所示为例，目前的程序总共会用到8个page （page 0～8，这些page可以是程序或数据），从Page Table的记录可知，page 0、3、4、6已被载入主存储器（RAM），其他目前没用到的page 1、2、5、7则被暂存在磁盘中。

假设程序要操作在page 7中的程序或数据，会出示page 7所代表的虚拟地址，而MMU在Page Table中发现page 7目前并未被载入RAM，所以会引发分页错误（Page Fault）。Page Fault基本上也是一种硬件中断，会唤起系统里相关的ISR （Page Fault Handler；分页错误处理程序）执行。Page Fault Handler负责找出目前主存储器中的哪个位置可以给page 7用，必要时可将目前已经在主存储器的page置换出去（称为swap），然后将page 7载入主存储器，并更新Page Table，当ISR返回时，MMU再去查Page Table时，就可在物理存储器里access到page 7了。

▲ 图12-16　将暂时没用到的区域swap出去，存储在Secondary Storage里

图12-17所示是说明Page Fault的处理流程。这个流程把处理Page Fault的policy与mechanism分开，操作系统只负责page swap，而哪些page该留在主存储器，或被置换到磁盘，则由上层的算法决定（如图12-17所示步骤2的External pager），且这个算法可以根据需求而进行切换。

▲ 图12-17　Page Fault处理流程

在实作时还有很多细节要注意。

■　为了性能考虑，Page Table通常是分为两个阶层。

■　MMU或操作系统还会保留空间作为page table的cache （称为Translation Lookaside Buffers； TLBs）。

■　大部分系统会利用虚拟存储器功能进行文件系统性能的优化。

■　选择哪些page要被置换出去的算法也是个大学问。

■　POD （Page On Demand）。

■　如果再把多个process同时执行也考虑进去，状况就更复杂了。

如果有兴趣，不妨去找Linux Kernel的书籍来阅读。”

菜鸟：“这种mapping关系还真的讲不清楚，还是看图比较清楚。利用Secondary Storage来缓存主存储器放不下的空间，虽然是以执行效率来换取存储器空间，但的确是物理存储器不足时最好的解法。操作系统与CPU/MMU会负责搞定mapping的事情，应用程序并不需要担心存储器不够的问题。”

系统老鸟：“性能的问题其实不需要太担心，因为整个流程中，CPU/MMU等‘硬件’会介入帮忙，而且系统可选择合适的Page Swap算法（这已是相当成熟的领域），再加上嵌入式系统的应用较为独立，应该不致于像PC/Windows不时就听到硬盘嗡嗡作响才对。”

12-5-2　动态载入

菜鸟：“利用Secondary Storage真的是解决物理存储器不足一个很有效的方法，但如果CPU没有MMU，无法从硬件实现虚拟存储器的支持，是否系统就不能使用这个方法了？例如，我们现在用的CPU虽然也是32 bit，但并未支持MMU，而且跑的是RTOS，也不支持Page Table等功能”

系统老鸟：“像Linux、Windows都是Multiple-Process的系统，要让每一个process都有自己的地址空间，还得通过虚拟存储器的功能来实现，而process没被执行时也只是一个可执行文件，被存储在文件系统中。

而RTOS是Single Address Space/Multiple-Tasking的系统，通常应用于没有MMU的平台，所以要应用Secondary Storage来存储程序或数据段，只能由系统自己来实作类似Windows DLL（动态载入函数库）的功能。简单地说，程序里用到的某些函数并未真正被连接到可执行文件里，直到runtime该函数被调用时，系统才会将其载入存储器并执行之。”

菜鸟：“如果这些要被动态载入的模块，每次载入的地址可能不同，那么，系统岂不是在runtime时就必须查询该模块的Symbol Table （符号表，表示该模块里所有函数、变量的相对地址），并做‘动态连接’，这好像有点麻烦。”

系统老鸟：“在PC上硬盘很大，系统也有足够的computing power做动态连接，但在嵌入式系统不能这样做，Symbol Table是单纯的文本文件，有时比可执行文件还要大！而且Symbol Table的格式是与Compiler相关的，如果真的要做动态连接，会使得系统的这个动态载入功能不具有移植性。通常我们会采用比较简单的方法，例如，在存储器的特定地址中保留空间给动态载入模块使用，则这些模块在编译/连接阶段都可以连接到这个已知地址，从而避开了动态连接的麻烦。

解决了动态载入模块的寻址问题后，另一个麻烦是因为要被动态载入的程序段并未与系统程序连接，所以无法直接调用系统功能。有几种做法可以解决这个问题，一种是用Software Interrupt指令，利用寄存器或Share Memory传送系统功能编号与参数（例如，规定系统功能os_draw_pixel（x, y）为第5号功能），而系统可以从Software Interrupt ISR启动动态载入模块所需的系统服务。举例来说，嵌入式系统操作系统free-DOS就是利用硬件中断（Software Interrupt）来提供系统服务。

并不是所有的CPU都有提供Software Interrupt指令，此时，系统必须设法让动态载入模块可以取得每个open API的实际地址，最简单的方式就是建一个表格，并放置在存储器的特定地址。图12-18所示是一种实现方法的流程。

这个范例的系统配置为：系统与内部AP烧在NOR Flash内，动态载入的模块则存储在SD card内，这些程序段都被寻址到SRAM中固定的地址。当动态载入的模块调用了system_call1()这个系统函数，其实是执行了图12-18所示的步骤3、4，而这些动作对该模块而言都是透明的。

通过这样的流程，要在RTOS上做到类似动态载入的功能，其实并不困难！”

▲ 图12-18　动态载入的执行流程

12-5-3　Banking

系统老鸟：“最后，我们来说说如何突破8 bit MCU地址空间太小的限制。诸如8051这种MCU的Data Bus只有8条，所以称为8 bit MCU。但其地址线有16条，其可寻址的最大空间为216，也就是64 KB。64 KB在微处理机刚被广泛应用时，因为应用简单且存储器还很贵，没有人认为64 KB会不够。但随着存储器工艺进步，价格降低，且应用越来越复杂，64 KB对习惯在Windows上写程序的人来说，简直连塞牙缝都不够。

这已不是增加成本、加大存储器就能解决的问题，问题症结在于MCU的地址线只有16根。最简单的想法就是设法增加地址线，如果能多一根，寻址空间就可以变成217——128 KB，依此类推。”

菜鸟：“这个方法我可以想象，但实际上，MCU本身还是只知道自己有16根地址线啊！也就是说，CPU提供的指令只能处理16 bit长的地址，例如，0×ABCD对8 bit MCU来说是合法的地址，而增加了一位的地址，如0×01ABCD，对MCU来说是不会处理那个bit的，还是会当作0×ABCD来处理。”

系统老鸟：“你说得没错，MCU提供的instruction格式是无法改变的，所以程序必须自己多做一些事情。举个简化过的例子，如果我们的硬件设计具有以下功能。

■　有两块64 KB的ROM。

■　当GPIO #1为High时，CPU的地址线与chip select都接到第一块ROM。

■　当GPIO #1为Low时，CPU的地址线与chip select都接到第二块ROM。

则当程序同样要求MCU去access 0×ABCD这个地址，随着GPIO #1的状态不同，CPU其实access的存储器位置会不同。利用这个简单的原理，我们就可以使用比64 KB还大的物理存储器。图12-19所示是利用8051的4根GPIO （P13~P16）来模拟address bus的线路，你可以参考一下。”

菜鸟：“我还有个问题。如果按照您说的这种设计，应用程序在调用某个函数时，都必须先知道它到底被寻址到哪一块物理存储器里，然后据此调整GPIO，但这明显违背了一般程序设计的经验，如果不是特殊的状况，程序员根本不需要知道某个函数被寻址到哪里！

此外，除非特别指定，Linker的基本原则是不会制造overlap的状况，简单地说，不会把两个函数寻址到同一个地址。回到刚刚您说的那个例子，整个系统里的function与变量那么多，工程师必须自己定义哪些要放到第一颗ROM，哪些要放到第二颗。这表示工程师必须知道每一个function的footprint，这一切会让程序编写变得困难重重。你们以前开发8051的项目都是这么做的吗？”

▲ 图12-19　Bank-Switch的硬件接线图

系统老鸟：“自己做Bank-Switch确实比较麻烦，以前许多老工程师都是使用汇编语言硬写，自己决定所有函数与数据的摆放地址。但那种年代已经过去了，现在的Compiler可以帮工程师处理你说的那些问题。

■　函数与变量位置配置优化。

■　应用程序只管直接做函数调用，不需要处理Bank-Switch相关工作。

我们看看如图12-20所示的例子会比较清楚。

▲ 图12-20　Bank Download运行流程范例

系统老鸟：“图12-20所示的左半部是物理存储器的配置，其中Common bank是永远不会被置换的区域。通过硬件设计，bank#l、#2、#3的起始地址都是B，可以利用GPIO来做bank切换；对MCU而言，同一时间可以操作的就是Common bank与bank #1、#2、#3三个其中之一。系统中有3个函数fl()、f2()、f3()，Compiler分别将其配置到bank #1、#2、#3。

工程师并不会知道f1()、f2()、 f3()分别被配置到哪一个bank，他只知道如图12-20所示的右半部程序里的main()，直接调用函数名称。举例来说，当main()调用了 f2()时，Compiler会产生一些额外的code，如图12-20所示程序中的f2()。只有Compiler才知道这个函数被配置到了哪个bank，所以f2()会先执行Bank-Switch，然后才真正去调用这个函数的程序（如图12-20所示程序的real_f2()）。

要注意的是，Compiler并不知道我们会用哪些GPIO脚来模拟Address Bus，所以上述bank_switch()是由我们系统设计者根据Compiler的规范来编写，并指定这个函数让Compiler知道即可。”

菜鸟：“其实Bank-Switch的原理并不复杂，但如果Compiler没帮忙产生这些code，工程师写起程序还是颇为麻烦的。”

系统老鸟：“8051最广为采用的C Compiler为Keil C，它就提供了至少4种Bank-Switch的实现方式。只要系统工程师提供必要的function （如上面例子的bank_switch()）， Compiler就会帮工程师搞定所有的事情。若想知道更详细的步骤，你可以到网络上找找Keil C的使用手册来参考。”

[1]　你无需理会图中的细节，重点是Memory Layout图的必须简单明了，让所有工程师一看就懂，哪块存储器的哪个区域是用作什么用途。

[2]　通常我们会用profile的技巧找出Stack最大的用量，利用此技巧，当然也可以找出造成Stack Overflow的位置。

[3]　就像一般程序与ISR一样，多个thread可能同时存取的存储器、变量或函数都称为Critical Section，除了可以用“禁止中断”来保护外，通常系统还会提供Mutex、Semaphore、Even等机制供程序员用于Critical Section的保护。

[4]　其实8051的Internal data只有128 Bytes，额外的128 Bytes只有8052系列MCU才有。

[5]　请参考《第13-6节：NAND Flash烧录器：特殊烧录格式》

Chapter 13

存储器管理（Ⅱ）: NAND Flash概论

时间：01月01日　 地点：办公室一隅　 登场人物：菜鸟、系统老鸟

菜鸟：“最近新闻常听到NAND Flash的新闻，除了想取代硬盘的SSD （固态硬盘）之外，很多电子产品也都以NAND作为大容量存储媒介，之前也听学长们说设计NAND Flash的系统是很大的挑战，但NAND不就是一种像硬盘的存储器吗？实在无法理解设计带NAND之系统的困难度。”

系统老鸟：“简单地说，开发NAND Flash系统时麻烦的地方为：

■　Memory中某些区域的特性是不稳定的（也就是说有些空间是无法使用的）。

■　更麻烦的是每颗IC不稳定区域的数量与位置并不相同。

■　最麻烦的是存储器某些区块在使用后可能会无法写入，甚至数据会丢失。

你能接受今天在随身碟中存了文件，明天再开启时数据就损毁了？或者你的MP3随身听，听音乐时突然发生大暴音吗？”

菜鸟：“用一用就会坏掉的存储器居然可以拿出来卖？还卖到缺货，这世界到底怎么了？。”

系统老鸟：“因为NAND Flash的单位存储器价格相较于ROM、NOR Flash、RAM而言，简直是便宜的不得了！所以业界普遍认为用系统复杂度去取得这个成本优势是值得的。既然你那么感兴趣，我们就来谈谈NAND Flash系统需要注意的事项吧！”

13-01　NAND简介

系统老鸟：“NAND Flash ‘天生’就有可靠性（Reliability）的问题，这与NAND Flash内部的半导体特性及运行原理息息相关，但我们是做产品与系统开发的，以下我们只会简单说明硬件架构，并将重点摆在如何设计带NAND Flash的系统，如果你对NAND Flash的半导体特性有兴趣，请自己花时间去请教Google大师。

首先让我们来看看NAND Flash的硬件架构，以图13-1所示的2Gb （bit） NAND为例。

■　NAND的基本单位是block，这个设备共有2048个block。

■　每个block里有64个page。

■　每个 page 的 size 是（2048+64 ）Byte，其中 2048 Byte 是 Data Area，而 64 Byte 是 Redundant Area。

市面上NAND Flash有多种不同的配置，一颗IC里block的数目，一个block里的page数目，每个page的数据区与redundant区的实际大小，都可能随不同厂牌、不同工艺、不同size而不一样，系统必须设计得较具弹性，才有办法支持所有的NAND Flash型号[1]。

此外，不要以为每一个page里的Redundant Area是附送的，Redundant Area是提高NAND Flash Reliability所不可或缺的额外存储区域，后面我会再深入说明。”

菜鸟：“除了您说得高深莫测的Redundant Area外，其他好像也跟一般存储器没什么不同。”

系统老鸟：“图13-2所示是NAND Flash原厂提供的一张有趣的图，其中说明了使用NAND Flash必须面对的问题。”

▲ 图13-1　NAND Flash硬件架构图

■　Reliability：可靠性。

■　Stronger ECC（Error Correction Code）：ECC是种纠错算法，后面会有专门章节说明。

■　Bad Block管理机制。

■　Optimized Wear-Leveling：平均读写机制，同样在稍后会说明。

■　逻辑与虚拟地址的Mapping：因为每颗NAND Flash的Bad Block分布都不同，所以系统必须将不连续的NAND Flash物理地址（必须跳过Bad Block），包装成连续的虚拟地址。

■　提高性能。

■　新命令：NAND Flash的工艺不断进步，新的NAND Flash可能会提供某厂商或某工艺独有的新命令，而且通常必须使用此命令才能发挥其最好的性能，故系统设计时必须考虑到这一点，系统一定要具备可扩展性。

▲ 图13-2　控制NAND Flash必须面对的问题

菜鸟：“这就好比说我卖给您一台很便宜的汽车，然后我告诉您，我早知道这种车子的引擎有时会过热、安全气囊可能会失效、汽车电子不知何时会死机等一堆已知的问题，您自己必须设法让它安全上路，出了问题可不关我的事，然后您居然也欣然接受这种‘烂’产品，真的很诡异耶！”

系统老鸟：“你的比喻非常传神，但对大批量生产的电子产品，成本因素大于一切！如果没有NAND Flash, Apple的iPod就只能使用硬盘，超薄的iPod Nano、iPod Shuffle、iPod Touch、iPhone、iPad根本就不可能出现。换个方向想，如果不采用NAND Flash，改用稳定度高但也贵得多的NOR Flash，你认为做出来的产品会具有价格竞争力吗？

不管你觉得合不合理，业界大量使用NAND Flash进行产品设计已是趋势。我们还是回到现实，我们说过NAND Flash的种类很多，以下我们将其稍做分类，种类之多绝对超乎你的想象。”

■　Raw NAND：虽然是目前市场的主流，但规格相当多样且混乱，因为一般中小型厂商不容易取得特定厂商、特定型号的NAND Flash，所以会要求系统必须能够自动判别并支持各种不同属性的NAND Flash。NAND Flash除了Reliability的问题外，其种类的繁多也是系统设计时必须面对的问题，有些厂商会在市场上寻找便宜的NAND Flash，所以同一种产品，在不同批次的量产中可能选用完全不同的NAND Flash，系统设计难度不可谓不高。系统设计时可根据以下属性分类。

□　存储单位的size: block number、page size、page number per block、Redundant Area size。

□　控制这个NAND需要几根chip select：会影响硬件与driver的设计。

□　根据NAND架构区分：目前市面上的NAND Flash架构有3种：SLC、MLC、TLC。简单地说，MLC是在同一个IC中塞入两个SLC，密度较高、容量较大，单位存储容量较便宜，同时Reliability也较差，而TLC则是塞入3个SLC，单一芯片容量可以做到最大，单位存储容量最便宜、Reliability也最差。

因SLC、MLC、TLC在不同属性上各有优缺点，所以会被应用在不同的领域。

•　SLC：对Reliability要求较高的应用，例如用来取代NOR Flash、用来存储程序代码的应用。

•　MLC/TLC：对制造成本较敏感的应用，例如MP3播放器（Apple的iPod、iPhone、iPad就是采用MLC）、SSD。

■　根据工艺区分：当采用较精密的工艺，一个晶圆里可以生成更多的IC，单位存储容量成本降低，但数据可靠性降低，目前最先进的工艺已经到3x nm，并正往2x nm工艺迈进。

■　根据需要什么等级的ECC来区分：Reliability越低的NAND Flash，需要越高等级的纠错算法，目前被采用的ECC算法可分为：1 bit、4 bit、8 bit、12 bit、24 bit（在后面章节会做说明）。

■　带Controller的NAND： NAND Flash有着许多已知的问题，对系统开发而言每个都是大麻烦，但采用先进的工艺只能降低单位存储空间的制造成本，并无法解决既有问题（事实上，工艺越进步，NAND Flash的Reliability反而越来越差，本章后面会对此奇怪现象进行说明）。

并非所有开发团队都能很好的驾驭NAND Flash这匹野马，所以导致客诉问题层出不穷，造成NAND Flash厂商只能疲于奔命。为此NAND Flash厂商想出在NAND Flash中加入Controller的方法，用内置硬件，处理掉NAND Flash全部或部分的问题，使系统工程师不需再为此烦恼。几乎所有NAND Flash厂商都推出了这种内置Controller的产品，但系统厂商似乎不愿买单，因为要在NAND Flash内加一颗Controller，显然又会使成本增加。这种NAND Flash分为两类。

□　内置ECC Controller的NAND Flash：例如Hynix的Emulated NAND、Samsung的EF（Error-Free） NAND。

□　内置ECC & Wear-Leveling Controller的NAND Flash：例如Toshiba LBA。

目前NAND Flash市场由几家大厂分食，分别是Samsung、Toshiba、Micron、Intel、Hynix。raw NAND的规格还勉强维持着一致性，至少基本操作的命令格式都是兼容的，但在内置Controller的NAND这个领域则可说是百家争鸣，甚至同一间厂商都可能都有多种规格，图13-3、图13-4和图13-5所示的是几个例子，原理都差不多，你可以参考一下。”

▲ 图13-3　内含Controller的NAND Flash（厂商A）

▲ 图13-4　内含Controller的NAND Flash（厂商B）

▲ 图13-5　内含Controller的NAND Flash（厂商C）

系统老鸟：“因为NAND Flash有太多‘卡’的特性，而且工艺越进步，情况越严重（近期的3x nm～2x nm工艺的NAND Flash，其每个block的可写入次数甚至只能达成500次～1000次[2]！）。所以厂商Vendor一直想要推内含Controller的NAND，但加了一个Controller，势必就增加了成本，厂商的接受度不高，所以effort还是存在系统开发团队身上。”

菜鸟：“工艺越进步，不是应该更能解决存在的问题吗？怎么反而越严重？”

系统老鸟：“这还是市场因素的考虑，NAND厂商目前竞争标的是将工艺尽可能缩小，使同一个size的晶圆可生成更多颗的IC，使存储器的单位成本继续降低，以期能拉大与其他竞争者的差距。也就是说，怕麻烦、不考虑成本的厂商，可以使用内置Controller的NAND，对成本敏感的制造商，则请在自家系统多下点工夫了！”

菜鸟：“看来大部分的厂商都属于后者吧！”

13-1-1　NAND Flash的特性

系统老鸟：“知道NAND Flash是什么后，我们接着更深入地说明NAND Flash的特性，其基本特性如下。”

■　NAND Flash读出（Read）与写入（Program）的操作是以page为单位，而擦除（Erase）操作则是以block为单位。

■　要先Erase后，才能进行Program，否则数据可能会发生错误（NAND Flash的每个cell在写的时候只能从1→0，不能从0→1；而Erase后，该block中所有的Cell值都是1。所以若要对一个已经使用过的page进行写入的时候，必须先Erase包含这个page的整个block。）

■　NOP （Number Of Program）

■　Erase后，每个page能被写入（program）的次数。

■　通常MLC的NOP为1，SLC NOP则不一定。

■　系统为了支持各种NAND Flash，通常会限制每个page只能被program一次。

■　Page Program的顺序

■　SLC可以random page write （page 0，3，1，2 ……）。

■　MLC只能依次往后写（page 0，1，2，3……）。

■　系统为了支持各种NAND Flash，通常会限制page program只能依次往后写。

■　NAND Flash中存在Bad Block，且随着对NAND Flash的操作，可能会有新的Bad Block产生，因为对这些Bad Block进行读写的时候，并无法保证数据的完整性，所以系统必须对Bad Block进行相应的处理。

■　相较于其他存储器，NAND Flash的存取速度算是非常慢的，如表13-1所示。

表13-1　各种存储器速度比较表

存储设备

存取时间

读取（Read）

写入（Write）

擦除（Erase）

DRAM

60ns （2 B） 2.56us（512 B）

60ns（2 B） 2.56us（12 B）

N/A

NOR Flash

150ns（1 B） 14.4us（512 B）

211us（1 B） 3.52ms（512 B）

1.2s（16 KB）

NAND Flash

10.2us（1B） 35.9us（512 B）

201us（1 B） 226us（512 B）

2ms（16 KB）

硬盘

12.4ms（512 B）（平均）

12.4ms（512 B）（平均）

N/A

菜鸟：“block不是比page大吗（因为一个block中有64个page）？您刚又说想要修改一个page里的值，必须要先Erase一整个block，那岂不是会影响这个block里的其他page吗？假设我们用在NAND Flash里的空间当File System，有办法实现随机写入的功能吗？”

系统老鸟：“还是有办法的，就是有点麻烦，我们后面会再提random-write的功能如何implement，才说到一半，这里你就又发现了一个NAND Flash令系统工程师伤脑筋的地方，但NAND Flash的特性还没说完。

■　常被写的block容易坏，这是由于MLC的特性，在频繁读取数据的时候，里面存储单元的电荷有可能产生变化，从而导致数据错误和遗失。

■　NAND Flash里的任意一个block或page里，随时都可能发生某些bit的值变为错误——如果一个bit坏了就会导致整个page不能使用，那NAND Flash真的可以说是不堪用了！为此，在NAND Flash厂商的‘指导’下，我们导入了ECC的思想。

ECC是个数学算法——若我们说这个是N bit的ECC算法，则代表它最多能修正N个bit的错误[3]。我们后面会再说明ECC算法在NAND Flash上的应用，在此我们先看如图13-6所示的上半部分——随着NAND Flash操作次数的增加（C→A→B），我们可以看到Error Bit逐渐增加，在B点之前，使用ECC都可以把数据救回来，一旦超过B点，数据就永远lose了。由图13-6所示还可以看到NAND Flash的另一个特性：由C点到A点，Error Bit是缓步增加的，一旦突破A点，Error Bit的数目会很快增加到无可挽救的B点。根据上述的特性，请接着看图13-6所示的下半部，系统应该在适当时机（A点）把数据传输到另一个block（C点），再把原来的block擦除（Erase），则其特性可部分恢复正常。

▲ 图13-6　NAND Flash的特性（Error Bit逐渐增加）

■　常被存取（Read/Program）的page，其附近的page容易坏（称为Read/Write Disturbance）。

■　MLC的部分block具有SLC的特性（Higher Reliability），系统设计可善加应用（但每家每款flash的分布状况并不一定）。图13-7所示的流程图建议系统设计者可将‘hot data’写入Reliability较佳的block，至于何谓‘hot data’则视各个系统而定了。”

▲ 图13-7　MLC的部分block具有SLC的特性

菜鸟：“NAND Flash奇奇怪怪的特性还真不少，系统设计者必须好好利用，才能同时提升NAND Flash应用的Performance与Reliability，我认为这个挑战并不小。”

13-1-2　NAND Flash的发展趋势

系统老鸟：“没错！再告诉你更不幸的消息，根据现在NAND Flash发展的趋势，NAND Flash厂商不但不打算处理这些让系统设计者恶梦不断的特性，反而为了让价格更具竞争力，专心致力于缩小工艺，结果使得新一代NAND Flash的Reliability越来越差，系统工程师的担子也越来越重。

以下列举NAND Flash发展的趋势。

■　更精密的工艺、更大的容量。

■　Block可读写次数减少（从SLC的万次等级，降到今日TLC的小于1000次的等级）。

■　单位售价越来越便宜。

■　必须使用更复杂的error correction algorithm，目前TLC已经需要用到24 bit ECC，其他诸如44 bit ECC、Turbo code、LDPC等算法都已ready，就等着更‘烂’的NAND Flash问世。

■　因为系统与NAND Controller支持新NAND Flash的速度，根本跟不上新NAND Flash对Reliability需求增加的速度，所以NAND Flash厂商仍不会放弃推出带Controller的NAND Flash。”

菜鸟：“真的越听越不理解！居然有一种产品，其新一代的产品只在价格竞争力上求进步，对Reliability却一筹莫展。”

13-1-3　如何自动辨认NAND Flash种类？

菜鸟：“刚刚谈到因为中小型厂商在NAND Flash市场备货不易，系统必须能判断板子上NAND Flash的属性。这些NAND Flash厂商是否制定一个标准，让系统可以在run-time时较容易地取得这些信息？”

系统老鸟：“你真的想太多了，既然NAND Flash这个产品已经帮我们制造一堆麻烦了，哪还怕多这项？市面上NAND Flash的种类实在是太繁杂了，若要说真的有标准，就是这些厂商在封装IC时，至少会尽量做到PIN脚配置往前兼容，而基本控制命令也都会相同。

也就是说，即便已经量产的产品，要换品牌或不同size的NAND Flash通常无须修改硬件（有些NAND Flash需要较多的chip select，我们可以在硬件板子设计时预留弹性，使之可支持需要不同chip select数目的NAND Flash），既然硬件可以不用改，软件是否也能够做到自动检测NAND属性，并正确控制它？

早期NAND Flash种类还没这么混乱的时候，各厂商制定NAND Flash ID时还会遵循某些规则，所以系统可以利用NAND Flash都有的‘Read-ID’命令，先读出ID，再根据ID判断其属性。但随着NAND种类越来越多，最后连同一家厂商在制定自家不同NAND Flash产品的ID时，都不一定会再遵守原先的规则。

简单地说，系统并无法在run-time辨识出板子上NAND Flash的所有属性，在软件不能修改的前提下，只能通过量产工具，在量产期间把目前用以生产之NAND Flash的属性，写入NAND Flash的特定位置，系统才有办法在run-time正确地操作所有的NAND Flash。

13-02　控制NAND Flash

系统老鸟：“以上就是对NAND Flash的‘简介’，接下来我们谈点有深度的东西。”

菜鸟：“上面是简介啊？我听得头昏脑胀的，要注意的事情好像真的不少。”

系统老鸟：“没错！市面上对NAND Flash的技术书籍真的不多，许多‘un-document’的特性，连NAND Flash代理商的FAE也不一定全盘清楚，往往要出大问题，送回原厂分析后，才发现原来是系统做得不够solid。最常发生的是：对NAND Flash不熟悉的工程师，疏忽了‘NAND Flash每一颗都不同’（注意：是每一颗都不同，即便型号相同，每颗NAND Flash IC都有其Bad Block分布状态。）的这个事实，往往在自家实验室测试无误后就认为没问题了，直到量产后才爆发‘不稳定’、‘数据丢失’等问题。

相关技术细节后面再说，我们先要知道基本的NAND Flash控制方式。”

13-2-1　NAND Flash的基本操作指令（E/P/R）

系统老鸟：“NAND Flash基本操作就是3种：Erase、Program、Read。驱动程序必须利用‘下命令’的方式，令NAND Flash运作。E/P/R这些基本命令可以算是一种约定成俗的标准了，几家NAND Flash大厂都有默契不去更动这些基本命令，所以一组驱动程序，要同时支持Samsung、Toshiba或Micron等任意一款NAND Flash的基本读写功能并不困难。

但每间厂商在工艺研发上各自努力，难免会开发出自有的独特功能，通常都可以在性能或Reliability上有所改善。例如，某款TLC就提供一种叫做retry-reading的读取命令，简单地说，当读出来的数据发生无法恢复的ECC error，通过这个命令序列，会有机会再读出正确的数据，让系统有机会对此weak-block做处理。再例如某家的NAND Flash提供称为‘Multi-Command’的命令，号称可‘Parallel Process’，无论读写都可以有两倍的性能等。”

菜鸟：“市面上NAND Flash的种类多如繁星，假使每间厂家都有自己独特的命令，那我们系统岂非没有完整支持的一天？”

系统老鸟：“这种命令对系统工程师确实具有致命的吸引力，加入这些命令可使客户在选用某些NAND Flash时有较佳的性能与Reliability，但客户却不一定能顺利取得这些型号的NAND Flash来量产，与其到量产时才发现使用一般NAND Flash的性能与Reliability不如预期，倒不如在系统开发时就只以基本指令为主，调整出最佳的性能与稳定度。确定客户会使用某款NAND Flash量产时，才使用其特殊指令来做优化。

有些NAND Flash型号在市场上只是昙花一现，或早被大厂搜刮一空，一般厂商根本拿不到货，所以我们的系统设计倾向不主动支持这些特殊命令，也可以避免系统中一堆patch。

言归正传，你还是先要弄懂NAND Flash基本的E/P/R操作。首先，我们以如图13-8所示的‘Read-Command的时序图’来说明如何对NAND Flash下命令，基本上主控IC就是通过以下几根PIN与NAND连接，只要将这些PIN按照data sheet里的时序图加以控制，NAND Flash就会执行相应的命令。

■　CLE （Command Latch Enable）：下命令时应把此PIN拉为High。

■　CE （Chip Select）：开始操作此NANFD flash时必须把此PIN拉低，有的NAND Flash有多根Chip Select PIN，此时驱动程序要先判断目前要操作的地址属于哪个区块，并控制相应的Chip Select PIN。

■　WE （Write Enable）：在写入数据、命令、地址时，应将此PIN拉Low。

■　ALE （Address Latch Enable）：传输地址时应把此PIN拉为High。

■　RE（Read Enable）：自NAND Flash读取数据时，应把此PIN拉Low。

■　IOx或Data：这8或16根PIN如同Data Bus或Address Bus，NAND驱动程序通过这些PIN传送命令、地址与数据，而NAND Flash则是通过这些PIN传回数据。

■　R/B （Ready/Busy）：当NAND驱动程序把命令下给NAND Flash后，NAND Flash需要时间处理这个命令，它会将R/B PIN拉Low，驱动程序必须等其变回High时才可接续下面的动作（以READ-Command而言，必须等R/B变为High后才能开始读取数据。）。

控制方式相当简单，就是把这些PIN当GPIO控制，根据时序图拉High拉Low，或从GPIO读回NAND传回的数据或状态。”

▲ 图13-8　NAND Flash READ-Command时序图

系统老鸟：“明白如何对NAND Flash下命令与读写数据后，data sheet内都会有指令的流程图，驱动程序只要照着做即可。以图13-9、图13-10、图13-11所示的节录自某NAND Flash的data sheet为例，基本上，所有NAND Flash都会支持Erase、Program、Read这3个指令，控制方式也完全相同。你先看过这些流程图，再来看看有没有什么问题？”

▲ 图13-9　Block-ERASE Command Flow

▲ 图13-10　Page-PROGRAM Command Flow

菜鸟：“我有两个问题，第一个是我刚刚问过的，某个block在Erase后，只能一个page、一个page循序写入，但很多应用可能只修改文件的局部，假设就只需要改一个Byte好了，系统要如何处理这种需求？

第二个问题是有关READ Command的READ Command的流程图中，在读出数据后接着要做两个动作：‘ECC generation’与‘Verify ECC’。对ECC的处理，我还是一点概念都没有！而且流程图中指出，当‘Verify ECC’为错误时，必须传回error。当读取某个page发生错误时，应该表示这个block‘坏掉了’吧！此时系统又应该做何处理？”

▲ 图13-11　Page-READ Command Flow

系统老鸟：“别着急！Bad-Block处理和ECC都是个不小的课题，我们随后就会说明，我先来解决你对Random-Write功能的疑惑。”

13-2-2　Random Page-Write

系统老鸟：“系统必须提供单独写入某个page的功能，但NAND Flash的Erase Operation 却是以block为单位，即为了写入某个page，必须Erase一整个block，使得这个block里的其 他page也被‘误杀’。

这是NAND Flash先天的限制，目前解法称为Copy-Back，步骤如下，显然并不聪明。以下步骤是假设要将数据X写入block #N的page #m。

Step01：系统必须管理整颗NAND Flash的使用状况，哪些block有写入数据、哪些是 Bad-Block，以及哪些block目前没被使用，可以拿来当缓存或置换区（这些block在烧录或系统初始化时就已被Erase过了）。

Step02：系统选出一个没被使用的block，将block #N的page #1～page #（m-1）写入这个新block。

Step03：将数据X写入新block的page #m。

Step04：将block #N的page #（m+1）之后的所有page写入新block。

Step05：Erase block #N，系统使用上述新block取代原来的block #N（例如：更改File System的对应表）

菜鸟：“我还以为有什么了不起的做法呢，看来写入一个page和写入一整个block所花的时间差不了多少，我们可接受这种做法的performance吗？”

系统老鸟：“这也是没办法下的办法啊！确实对performance造成影响，所以无论AP或系统层次，我们会希望尽量‘集满一个block’，再真正去做写入NAND Flash的动作，例如，File System都会有cache的功能，能使系统不至于太频繁去做copy-back的动作。”

13-03　Bad Block 管理

系统老鸟：“了解了NAND Flash奇怪的特性，以及基本操作方式后，你应该就可以写NAND Flash的驱动程序了，接下来我们要聚焦在系统处理NAND Flash的Reliability时，最重要且不可或缺的3个的机制。

■　Bad Block处理

■　ECC（Error Correction Code；错误更正码）

■　Wear-Leveling

菜鸟：“终于要进入重点了……”

系统老鸟：“NAND Flash的‘祸根’来自于有些block是‘坏的’，原来好的block也可能渐渐‘变坏’，而且每一颗NAND Flash的状态都不一样，所以系统一定要有个机制来记录NAND Flash中的Bad Block，并且让上层程序不需担心如何处理每颗NAND Flash中Bad-Block状态都不相同的问题。

NAND厂商对Bad-Block处理只做了原则性的描述（如图13-12所示），但在系统中其实非常麻烦！”

▲ 图13-12　NAND Flash Bad Block处理原则

菜鸟：“感觉NAND厂商总是避重就轻，谁不知道Bad Block是不能用的？重点是系统该如何有效的管理NAND Flash里的Bad Block吧！”

系统老鸟：“不要抱怨了，首先你必须知道除了斯斯之外，Bad Block也有两种，一种是先天的，一种是开始使用才产生的，两者都不可忽略，重点来了，系统开发者最容易忽略的地方是：

■　擦除了Initial Bad Block Information

■　无法适时辨认出已经或将要变成Bad Block的block”

13-3-1　Initial Bad Block

系统老鸟：“首先介绍NAND Flash中‘先天不良’的Initial Bad Block，其具有以下特性。

■　NAND Flash出厂时会在Bad Block里的特定位置上标示记号（mark），用来辨别Initial Bad Block。

■　用以标示Initial Bad Block的mark，只要经过Erase后就无法复原。

■　NAND原厂自有其判断某个block是否不堪使用的方式，被标示为initial invalid block的block并非完全无法使用，有的只是在某些状况（例如低电压）比较‘weak’，这也是为什么原厂有时会将其保守的称为‘invalid’block，而不是‘bad’block。但无论叫什么名字，对系统设计者来说这两者都是一样的——就是‘不能用！’

■　接续上一点，系统不能拿Initial Bad Block来用，否则绝对会变成系统里的未爆弹。

■　每种型号的NAND Flash，其Initial Bad Block标示法可能不同，必须参考data sheet。

■　因为Initial Bad Block Information太重要了，而且每颗NAND Flash的Initial Bad Block都分布与数量多寡都不相同，所以系统通常会设法将其备份在NAND Flash的特殊区域里，必要时可取出重建正确的Bad Block Table。

■　接续上一点，在开发阶段，工程师难免会将NAND Flash写烂，此时若Initial Bad Block Information也不见了，则表示这颗NAND Flash已不能用，因为以后系统若发生不稳定的状况，我们很难理清到底是系统的问题，还是Initial Bad Block正在作怪。

所以我们通常会开发一个小工具，在开发阶段，一旦有新的NAND Flash IC入手，首先为每颗NAND编号，然后利用此工具先将其Initial Bad Block Information存起来（存在PC），当NAND Flash被写烂时，同样利用此工具，将其Initial Bad Block Information存回NAND Flash。

图13-13所示是某颗NAND Flash取得Initial Bad Block Information的流程图，基本上，NAND原厂都是在Initial Bad Block里的特殊位置写入一个非0xFF的值。”

▲ 图13-13　取得Initial Invalid Block Information的流程

13-3-2　虚拟/实体位置转换

系统老鸟：“系统搜集了Initial Bad Block Information后，必须将其记录在一个表格里——Bad Block Table（以下称之为BB table）。因为每颗NAND Flash的Bad Block的分布状况与数量都不同，使得系统无法像烧录ROM一样，将image file循序地一个一个Byte写入NAND Flash。所以系统必须基于Bad Block Table，建立一个逻辑位置与实体位置的对应表。

由图13-14所示可知逻辑位置是连续的，而实体位置则因为Bad Block而变成是不连续的。”

▲ 图13-14　Logical→Physical Address Mapping

菜鸟：“从系统的观点来看，这样的mapping是为了让上层程序‘以为’NAND Flash的空间为连续且完全都是‘good’的，否则当所有应用程序都需要自己处理，‘碰到Bad Block要跳过’这样的麻烦时，实在难以想象会造成多大的混乱！

所以让系统来统一管理是比较好的方法，上层程序只需要出示虚拟地址，系统自然会将其mapping到一个Good Block。”

系统老鸟：“我还是第一次听到‘Good Block’这种说法，但倒也颇为贴切。系统主要的工作本来就是‘抽象化’，尤其是NAND Flash具有那么多奇奇怪怪的特性，系统一定要设法加以封装，多加一层mapping是最直觉的做法。

这种对应关系的implementation并未有一定的标准，我们团队称这个对应表为Look-Up-Table （LUT；查找表），其实就是记录Logical Block与Physical Block间，一对一的对应关系，数据结构并不复杂。举例来说（如图13-15所示），系统中的File-System并不知道其物理存储设备（NAND Flash）有着Bad Block的特性，File-System看到的就是连续的空间，它要access的位置会被NAND Flash Firmware根据LUT转换为NAND Flash的物理地址。”

▲ 图 13-15　Logical→Physical Address Mapping（2）

系统老鸟：“在较复杂的系统中，甚至还包含Garbage Collection（回收处理）的机制，但我们的系统相对简单，所以图13-16所示用来参考即可。”

▲ 图13-16　较复杂的NAND Flash系统架构

13-3-3　New Bad Block

菜鸟：“刚刚说了‘先天不良’的Initial Bad Block，那么系统如何发现‘后天失调’、在run-time才新产生的Bad Block?”

系统老鸟：“系统只能在access某block时，才被动的知道该block已经‘完蛋’了，例如：

■　对NAND Flash下命令时，NAND Flash传回Error。

■　从NAND Flash读回的数据，发生ECC Error。

当发生第一个现象时，则此block通常已经没救了，必须被标示为Bad Block，同时里面的数据也宣告丢失，如果此时丢失的是程序代码那就麻烦大了！所以我们在设计系统时，对存储于NAND Flash内的程序代码与重要的数据通常会多存储几份。当第一份坏掉时，还可以使用第二份（实作上必须指定另一个free的block，立即将第二份的数据复制过去，即NAND Flash中要时时维护原来数量的备份，这么做比较安全，没有人可以保证N份备份，经过长时间后不会全部坏掉！）。

为了避免block完全坏掉后才来处理，系统的策略应该是，当发现某block已经有‘快坏掉’的征兆时，就将数据复制到另一个free block，同时修改Look-Up-Table （Logical/Physical Address Mapping Table），并对原来的block执行Erase操作，则此block的特性可恢复，以后还有继续使用的机会。”

菜鸟：“所谓‘快坏掉’的block，指的就是其Error Bit逐渐增加到ECC算法可以修复的极限吧？”

系统老鸟：“Yes! You got it!接下来我们可以开始讲解何谓ECC了。”

13-04　ECC（Error Correcting Code）

系统老鸟：“假使确定操作时序和电路稳定性都没有问题的话，NAND Flash一般不会发生整个block或page无法读取的状况，而是整个page（例如512 Bytes）中只有一个或几个bit出错。因此我们可以使用ECC算法，检测在某个page中，当初写入与现在读出的数据是否有所差异？如果有差异，是几个bit的差异？并在算法的极限内，可以计算并取回当初写入的数据。”

菜鸟：“这个算法真的很神奇，能跟我讲解一下它的计算原理吗？”

系统老鸟：“我又不是数学系毕业的，而且我从来也没想要把它搞懂，有兴趣自己去问Google大神，你现在只要知道在将数据写入NAND Flash之前，先使用ECC Algorithm对正确的数据做运算并得到一组值，然后将这组值存储在某处（就是Redundant Area），之后利用这组值，通过与自NAND Flash读出来的数据做运算，即可得知是否有错误。若错误在限定的bit以内，则可进行修正，得回原来正确的值。

例如，某款NAND Flash里，一个page有（512+16） Byte，系统采用8 bit ECC算法，则当这个page发生错误的bit在8个以下（包含8个）时，这个算法都可以进行修复。简单地说，1个page里的数据，只要错不超过7个，系统都可以当成正确数据。”

菜鸟：“虽然我不知道这个ECC算法的原理，但显然计算量不会太少，而且越多bit的ECC应该越复杂；如果系统每次从NAND Flash中读出数据时都要做这样的运算，会不会导致系统性能变慢啊？”

系统老鸟：“你问到重点了，通常ECC不能由软件做，loading真的太大，必须要由硬件算。一般号称支持NAND Flash的SoC或NAND Controller里一定会包含ECC Engine，只是算法的强度等级可能不同。

当写入数据到NANDFlash的某个page时，ECC的值会被记录在该page的冗余区（Redundant Area），之后要从这个读回数据的时候，ECC Engine会开始计算，并根据运算结果判断发生错误的bit数是否在可修复的范围内？若是，则ECC engine会自动修复，并传回正确的数据，上述ECC Engine的动作对NAND Flash Driver来说都是透明的。

另外有一个重点是：ECC只是这类型算法的统称，有的算法可以纠错较多的bit，当然运算量也会较大，如表13-2所示。”

表13-2　NAND Flash控制IC主要ECC编码方式演进

编码方式

Hamming Code

RS Code

BCH

LDPC

特色

编码最简单，但一次仅

能更正1个字节错误

运算速度快，一次可处

理数个字节错误

效率高、调试能力强，

为目前ECC技术主流

效率最高、调试能力最

强，为未来发展趋势

调试能力

（bits/512 Bytes）

1

4

≥8

≥12

适用产品

小容量CE产品、SLC

SSD

普通存储卡与随身碟

高速存储卡与随身碟、

大容量CE产品

MLC SSD

菜鸟：“刚刚我们看过NAND Flash Error Bit的增加曲线（图13-6：NAND Flash的特性），当Error Bit增加到一定程度后，其增加速度会突然变快，系统当然希望在突破ECC算法的限制之前，就将这些‘快坏掉’的page揪出来，但如果坏掉的bit比ECC算法能检测或纠正的bit还多的话，会发生什么事？”

系统老鸟：“ECC算法毕竟不是万能的，当发生这个状况时，ECC算法根本无法发现有错，所以只好传回ECC engine认为没错，但实际却是‘多错了’几个bit的数据，这算是最悲惨的状况了，无法修正，更无法被检测，直到这些错误的数据或程序代码被使用时才会爆发，届时才去查问题根源，难免旷日费时。

所以系统设计的重点应该是不能任由block烂到极限，应该在快到极限，甚至Error Bit开始陡增之前（例如系统使用8 bit ECC，在发现5～6个Error Bit时），就该将其交换掉，将数据复制到另一个free block，并修改LUT。”

菜鸟：“我还有一个问题，Redundant Area专门只用来放ECC的值吗？”

系统老鸟：“一般ECC算法还是以512 Bytes为单位，所以2 KB、4 KB、8 KB的page则会切成多个512 Byte单位，分别计算其ECC值，所以Redundant Area也会以等比例切分（例如2 KB的page，则是分为4个512+16的单位）。

越高bit ECC的algorithm就需要越多Byte的空间存储ECC值，例如4 bit ECC需要存储52 bit，而8 bit ECC需要存储104 bit的ECC data。剩下的Redundant Area还可能被用来存储其他系统信息（例如File System Information），空间的利用必须仔细规划，否则当ECC算法对Redundant Area需求空间不断增加时，可能会影响系统其他部分对Redundant Area的使用状况。

新类型的NAND Flash层出不穷，从要求1 bit ECC、4 bit ECC，跳到要求8 bit ECC、12 bit ECC，新架构的TLC甚至要求24bit ECC，而每个page的size也从2KB、4KB一路增加到8KB，这些变化也不过是两年间的事情。换句话说，NAND Flash的这种市场生态增加了系统设计的困难度，导致系统设计往往跟不上NAND Flash的步调，所以系统设计在一开始就必须考虑到弹性与可扩展性。”

菜鸟：“Bad-Block管理与ECC都算是比较消极的手段，只能被动地等待Error Bit或Bad Block出现，难道没有比较积极的方法，能够主动出击，让Error Bit出现的几率变小吗？”

系统老鸟：“当然有，那就是我们接下来要说的：维护NAND Flash系统Reliability的第3根脚——Wear-Leveling（耗损平均技术）。”

13-05　平均读写机制

菜鸟：“可是话说回来，我觉得系统内加入Bad-Block管理、Logical/Physical Address Mapping与ECC等机制已经够复杂了，真的还需要加入这种主动减少Error Bit产生几率的方法吗？而且block被写坏之前，就已经被系统换走了，半导体这种东西‘感觉’似乎没那么容易被写坏！”

系统老鸟：“很遗憾你的感觉是错的，有些block真的很容易被写到连ECC都无法修复；此外，还记得我们前面说的Read/Write Disturbance吗？就算没被直接读写也可能被‘连累’。而且目前NAND Flash的可写入次数还越来越少，所以平均读写机制绝对不可少！

图13-7所示的表是由某间NAND Flash厂商所提供，请注意其中两个箭头的部分，说明使用4xnm工艺的TLC，原厂保证的写入次数只有1500次，当工艺‘进步’到3xnm，保证写入次数更降低到只有1000次，相较SLC有50000次的保证写入次数，简直就是天差地远，所以系统绝对要如你所说——主动出击、积极保护。而最有效的做法就是‘平均写入’机制，设法让系统运行期间的所有写入动作，平均分散到NAND Flash里的所有block，从而减少每个block坏掉的几率，并延长整个NAND Flash与产品的寿命。”

▲ 图13-17　工艺越精细，NAND Flash可写入次数反而骤降

13-5-1　Wear-Leveling（耗损平均技术）

菜鸟：“我大概可以体会这个方法的精神，举个比较极端的例子，假设系统要对上述P/E cycle只有1000次的NAND Flash，做1000次写入缓存数据的动作，假使系统就使用同一个block当作写入的缓存区，则做完一次这个动作后，该block可能已遭致永久性的破坏，而系统也可能就无法正常运行了。如果系统使用20个block轮流当作写入的缓存区，则这样的动作至少可以做20次。

一般产品应不至于有如此频繁写入NAND Flash的应用（就算是随身碟，通常也不会天天更新文件），再加上Firmware提供了这种平均写入的机制。我想这也是为什么这种只有1000次P/E cycle的NAND Flash，却也敢拿出来卖的原因吧！”

系统老鸟：“随着NAND Flash工艺的不断进步，NAND Flash的容量越来越大，但每个可容忍的P/E cycle却越来越少，系统设计者也只好想到将写入次数平均分散到每个block中，因为block数目够多，就能延长NAND Flash的使用时限，业界称这种方法为Wear-leveling，它的理解也很简单，以下是这个方法的特性。

■　 Wear-Leveling主要的功能，是让每个block的Program/Erase的次数尽量达到平均。

■　NAND Flash Spec上都会注明每个block可Erase的次数，为了增加Flash的使用寿命，使用Wear-Leveling是完全有必要的。

■　WLC（Wear-Leveling Counter）：系统维护一个表格，称为WLC table，记录每个block到目前为止已经被Erase的次数。

■　系统将整个NAND Flash中没用到的所有block归类到spare区中，如图13-18所示。

在典型的应用中，NAND Flash里的所有block可以分为4类：Data Block、Block for Private Information、Bad Block和Spare Block，原本是散布在NAND Flash的各个位置（如图13-18所示的左半部），但系统会maintain一个Look-Up-Table （LUT或Logical/Physical Address Mapping Table），使得上层程序看到的是连续的存储器空间（如图13-18所示的右半部）

▲ 图13-18　Logical NAND Flash Layout

■　Wear-Leveling简单地说，就是系统要maintain一个WLC table，记录每个block被Erase/Program过的次数（称为WLC），若某个block的counter很大，就要去Spare区找一个次数相对少的block来替换，并修改LUT。

■　LUT在Wear-Leveling中扮演着不可或缺的角色，唯有通过修改LUT的mapping，才能做到把WLC较高的block‘置换’为较低的block。”

菜鸟：“基本的原理确实不复杂，我想知道系统执行block置换的最佳时机是什么？应该不需要每次写入时都要置换吧！”

系统老鸟：“上面讲的是mechanism，至于置换时机的选择则是policy，必须可以根据不同的应用或NAND Flash的种类而调整，并没有绝对标准的做法。

基本上，系统会根据NAND特性定一个临界值（对MLC来说，WLC=100可能还算小case；但像上述的TLC，若某WLC大到超过100，可能就要稍微注意一下了），当即将被写入数据的block的WLC到达这个临界值，系统就会执行置换。另一个做法是：目前block与spare block的WLC相差到达某个值，系统才会执行置换。”

菜鸟：“除了基本操作命令外，NAND Flash这个领域似乎没什么标准，大家都知道要做Bad Block管理、要算ECC、要尽量平均写入，但是要如何implement就得要自己想办法了。我能想象如果系统做得不好，又碰到工厂使用烂NAND Flash，那么真的可能会有接不完的客诉了！”

系统老鸟：“同样使用NAND Flash的系统，很容易就可以从量产后呈现出来的Reliability，看出测试团队是否够精干，及研发团队的技术层次。

这样讲下来，你应该会发现Bad-Block Management、ECC、Wear-Leveling三者其实息息相关，缺一不可，但这都只是不可或缺的基础建设而已，带NAND Flash的系统开发要注意的事情还很多，例如：

■　随时追踪NAND Flash厂商的roadmap，因为新款的NAND Flash通常会具有较好的成本优势，系统设计者必须事先知道需要准备的技术（例如：4 bit ECC跳到8 bit ECC），否则当客户需求来的时候，难免会措手不及。

■　Redundant Area的规划，必须考虑到将来存储ECC data所需要的空间会增加。

■　TLC的一个block有192个page，不是2的幂次方（一般NAND Flash的一个block有64、128、256个page），系统在计算地址时比较麻烦。

■　写入NAND Flash的性能调整。

■　文件系统（如FAT）如何与NAND Flash firmware衔接？

■　我们必须制定严格的NAND Flash的测试流程，否则系统如何保证在实验室内测试一颗pass，到量产时也会OK?

■　量产时NAND Flash的烧录工具。

你现在可能很庆幸自己不需要从头开发这种带NAND Flash的系统吧？”

菜鸟：“这个嘛……现在确实有点这种感觉，但我还是很有心学习的，希望以后可以帮忙maintain这部分系统，因为NAND Flash总是会推陈出新嘛！”

13-06　NAAD Flash烧录器：特殊烧录格式

系统老鸟：“既然NAND Flash是一种非挥发性存储器，量产时就会有烧录的需求，最后我们再来说说在量产阶段，如何将数据与程序烧录到NAND Flash中。”

菜鸟：“烧录器不就是负责把Binary File的内容逐一写入存储器芯片吗？说穿了不就是一个Byte、一个Byte写入，难道烧录NAND Flash需要什么特殊的Binary File格式吗？”

系统老鸟：“以EEPROM或NOR Flash而言是这样没错，Binary File和存储器芯片中的内容是一一对应的，即便是Binary File内实际的格式再复杂（例如：其中包含一个RAM-Disk），对烧录器而言就是如你所说：一个Byte一个Byte写入即可，完全不需要知道Binary File内的某个Byte代表什么意义。但要烧录目前广泛用于存储大量数据的NAND Flash可就不是那么简单了，因为NAND Flash工艺与材料的特性，NAND Flash和硬盘一样会有Bad Block，根据原厂规定，刚出厂的NAND Flash芯片的Bad Block数目只要在某个数目以下就不算瑕疵品，更麻烦的是每颗芯片的Bad Block数目与位置都不一样，而且随着操作次数增加，NAND Flash还可能产生新的Bad Block。”

菜鸟：“这样说来，烧录器在将Binary File写入NAND Flash时就不像EEPROM或NOR Flash那么简单了，当烧录器一个block一个block写入数据时，一旦碰到Bad Block时，势必要找另一个‘Good Block’来代替，而且因为每颗芯片的状况都不一样，最后烧录器一定要把Bad Block的状况记录在NAND Flash之内，那么在执行时期程序才知道该如何跳过Bad Block取得正确的数据。”

系统老鸟：“这就是我们刚刚提过的Bad Block Table（故障区块表；简称BBT），你觉得烧录器该把BBT记录在NAND Flash的哪一个位置比较恰当呢？要注意的是每一颗NAND Flash都可能会有不同的Bad Block分布状况，而烧录器与我们系统里的程序却不可能随着每一颗NAND Flash而改变。”

菜鸟：“如果指定特定block（如block #0）的特定位置用以存储每颗NAND Flash芯片的BBT，当这个block也是Bad Block时该怎么处理？深入想想的确是蛮复杂的。”

系统老鸟：“你的想法基本上没错，我们的确会选择block 0来记录每颗NAND Flash的状况，包含BBT。在此我们复习一下NAND Flash的data sheet中对Bad Block的规定。

■　block 0一定不会是Bad Block。

■　Bad Block区分为两种，一种是芯片出厂时就已存在的Bad Block，称为Initial Bad Block。Initial Bad Block的信息会记录在每一个Bad Block中的特殊位置，我们的系统或烧录器可以根据data sheet中的相关规定，取得该芯片出厂时有哪些Initial Bad Block。值得注意的是，一旦对整颗NAND Flash做Erase的动作后，Initial Bad Block的信息就会丢失，再也无法取回，所以对全新的芯片做Erase时一定要谨慎，最好先收集Initial Bad Block的信息，并将其记录在BBT中，总之这些block以后绝对不会被使用，所以也没有Erase的必要。另一种Bad Block是NAND Flash芯片经过操作后新产生出来的，最简单的检测方法就是判断写入某block的值与读出来的值是否相同。

所以由以上第一点可知，烧录器将每颗NAND Flash的BBT等重要信息存在block 0是OK的，程序执行时只要先从block 0将BBT读出，就可以由Bad Block的分布状况得知如何跳过Bad Block取得正确的数据。”

菜鸟：“我想到一个问题，就算程序知道烧录器将BBT存储在block 0中，可是一个block可能是16K Byte，也可能是128KByte，程序怎么知道烧录器将BBT存在block 0的哪里，又怎么知道BBT的格式是什么，这种东西有国际标准吗？如果没有，那么BBT的格式是由烧录器厂商定义并让我们遵守吗？”

系统老鸟：“如果真要说国际标准，应该就只有FAT吧！也就是用于随身碟或MP3随身听中的文件系统，通常烧录器厂商都会支持这样的格式。FAT已普遍应用在PC+Windows平台上的硬盘，硬盘和NAND Flash有许多相同的特性，如操作单位都可以是512 Byte（硬盘称为一个sector,NAND Flash称为page[4]4），都会产生新的Bad Block，把FAT应用在NAND Flash上简直就是天作之合。

如果决定将所有数据以FAT的格式存储在NAND Flash中的话，那么就没有制作Binary File的必要了，可以把NAND Flash当成一个磁盘，只要告诉烧录器哪些文件要存到这个‘磁盘’中，烧录器内的程序自然会先对NAND Flash做格式化的动作，然后就像把文件存到随身碟一样，将文件按FAT格式逐一写到NAND Flash中。一但NAND Flash被烧录成FAT格式，就如同机器中多了一颗硬盘一样，所以系统必须支持FAT文件系统才能存取NAND Flash中的数据。”

菜鸟：“我知道有些产品就是把NAND Flash当硬盘使用，而且NAND Flash有体积小、耗电少的优点。如果产品需要大量的存储器存储数据，NAND Flash是目前最便宜的选择，我觉得使用FAT格式很直觉啊！有什么明显的缺点吗？”

系统老鸟：“只要把系统中的数据整理成一个个的文件，用FAT的确很直觉；可是如果系统中其实只有几个文件，用FAT去存取NAND Flash的代价就有点大了，基本上FAT文件系统的效率并不好，所以必须再加上一层cache，而cache的程序一旦出错就很难检查。所以当数据量大但文件数目不多的状况下，我们通常不倾向把事情复杂化，我们会定义自己的文件系统与Bad Block管理机制。

有些烧录器厂商会有建议的格式与现成的烧录程序，要遵循烧录器厂商现成的规格也并无不可，但如果有特殊应用时，烧录器厂商通常也乐意帮助我们开发新的烧录程序，不过要花点钱就是了。”

13-6-1　NAND Flash量产工具

菜鸟：“用NAND烧录器生产对工厂是很方便，但对研发单位就有点麻烦了，毕竟我们并非专门做烧录器的公司，一旦其他项目对NAND Flash的使用规格与目前不同，即便只是小修改，我们都得再启动另一个外包项目，整个开发流程中有个重要的步骤卡在别人手上，感觉总是怪怪的。”

系统老鸟：“一般采用EEPROM、NOR的电子产品在准备量产时，软件的工程单位只需要提供ROM Image，工厂会设法寻找烧录器，并以最快的方式将数据与程序写入Memory，基本上工程单位不需介入。原本存储器烧录这是很简单的事情，没什么技术含量（只有提升产能的issue）。

但NAND Flash就不同了，不同开发团队的系统有自己处理Bad-Block的方式，且通常不愿公开。所以导致工厂在生产阶段，因为不知道该产品NAND Flash内的格式，自然无法独力完成将程序与数据写入NAND Flash的工作。所以研发团队必须主动介入，帮忙解决量产阶段NAND Flash的烧录问题。

第一个方法就是我们刚刚讲的烧录器，在PCB打件（SMT）之前，程序与数据就已经利用烧录器写入NAND Flash里了。烧录器的Firmware可以委外订制，以支持我们的特定格式，也可以自己开发。通常就是用现有产品的硬件板子，使用可插拔NAND Flash的socket，将程序修改为专门烧录之用。因为NAND Flash driver或文件系统的程序也都是‘现成的’（系统开发时即已完成），要做到烧录一颗并非难事，但要完成多颗烧录就没那么容易了，硬件和驱动程序都得修改，此外烧录器还有性能与操作界面的issue，因为对工厂来说，生产线上每位操作员的每一分钟都是金钱，如果决定量产时使用烧录器，建议还是交给专业的烧录器公司来负责吧！

第二种方法则反其道而行，量产时直接做SMT，将‘空的’NAND Flash上到PCB，然后根据以下步骤运行。

Step01：将带NAND Flash的半成品机器通过USB连接到PC。

Step02：在PC上执行我们自己开发的量产工具（以下称为MP-Tool）。

Step03：此时机器上主控IC里的Boot ROM，会与PC上的MP-Tool连接，MP-Tool会从PC下载一段程序到机器的RAM中（这段程序我们称为Updater）。

Step04： Updater对机器里的NAND Flash进行分析，存储Initial Bad Block Information，并对NAND Flash做必要的格式化。

Step05： Updater与PC上的MP-Tool沟通，开始下载要烧入NAND Flash的image file，并开始写入NAND Flash。

Step06：若Updater在烧录期间发生任何问题，应主动回报给MP-Tool, MP-Tool会在PC屏幕上显示烧录NG。

使用USB当传输介质的好处是通过USB Hub，一台PC同时可以连接多台半成品机器，使得MP-Tool一次可以烧录多台机器的NAND Flash，从而加速量产的时间，如图13-19所示。”

▲ 图13-19　MP-Tool运行架构与流程

菜鸟：“这个MP-tool的工作原理跟我们之前谈的，工程人员在开发阶段是用的系统更新程序非常类似（请参考《第7-6节：下载与执行》），有了这个流程，当NAND Flash里的格式需要改变时，我们也只需要修改Updater程序，不必再找烧录器厂商帮忙了。而且这样的架构能够在不同项目间重复使用，工厂操作员也不需要重新适应不同的烧录器，只要会操作PC上的MP-Tool即可，确实是用以量产带NAND Flash产品的最佳方式之一”。

[1]　这也是NAND Flash应用比较特殊的地方。首先，因为NAND Flash的种类太多，制造厂商不一定可以取得特定型号的NAND Flash，而且NAND的价格变动很快，厂商会尽量选便宜的NAND Flash来量产，因此系统会被要求能够尽量支持“所有”型别的 NAND Flash。

[2]　500次这个数字与SLC的十万次等级简直少得不可思议！但经研究与精算，如果可以有效的执行平均读写，则诸如MP3 Player或手机的应用，500次其实是绰绰有余的。

[3]　ECC的修正能力会因为使用算法不同而有些许不同，例如BCH mode的8 bit ECC，就代表最多可以修正到8 bit，以及最多可以发现到16 bit的错误。

[4]　NAND Flash的一个page可能为512 Bytes、2048 Bytes或更大，即便是一个page大于512 Bytes的NAND Flash也可以使用FAT文件系统，举例来说，2048 Bytes/page的NAND,就是一个page当成4个FAT的sector使用。

Chpater 14

模拟器

时间：02月02日　 地点：办公室一隅　 登场人物：菜鸟、系统老鸟

菜鸟：“按照计划，目前项目的时间已经run了约2/3，我们的平台（硬件+驱动程序+系统）经过这阵子的调试与调整已经越来越稳定了，老板也决定从下周起开始做系统整合。很难想象到目前为止所有的应用程序都在模拟器（Simulator或称仿真器）上测试，开发环境也不是什么Cross-Compiler、ICE、ROM模拟器、烧录器、示波器这些东西，甚至开发应用程序的工程师就如同在写一般的Windows程序一样，我看他们都是使用微软的Visual Studio来开发。

目前项目的产品是一个在日本销售的电子字典，应用程序在模拟器（Simulator）上执行起来就是一个视窗，如图14-1所示。视窗分为两部分，上半部分模拟LCD与Touch Panel，下半部分则是一堆button，以模拟电子辞典的键盘。一般人看到这个视窗，应该会以为这活生生就是个Windows上的字典程序，殊不知它完全地模拟了一台即将上市的机器。而其中更深刻的含义是：这是一个开发平台，无论执行于真实机器或Windows的模拟器上，应用程序的code只会有一套，所以模拟器上的所有画面、操作方式与执行结果，当然会与实际机器上一模一样。”

▲ 图14-1　电子字典产品的模拟器

14-01　模拟器概论

系统老鸟：“对于大部分决定购买某项电子产品的使用者而言，他们并不想知道产品内使用了什么高超的软硬件技术[1]，真正吸引他们购买的主要原因，应该是应用程序的规格，或是说这个产品究竟可以帮他们解决什么问题。基于这样的观察，业界逐渐了解到应用程序才是产品成功与否的关键，系统平台只是为了支持某项创意或应用的实现。所以近年来电子产品的应用程序有越来越复杂的趋势，开发产品的厂商也深知硬件需要软件加值，同样的硬件平台，理论上造价成本相同，但执行不同的应用程序，可能就可带来不同的市场价值。

嵌入式系统底层平台的开发需要比较多的硬件知识，所谓的底层包含驱动程序、RTOS与部分系统程序，这样的人才确实难寻，就算是通过内部培训，一来需要时间，项目进度往往又不允许我们这么做，再来，也不一定所有工程师都适合开发底层的程序。刚刚谈到电子产品最重要、最复杂、最耗费人力的部分通常是应用程序的开发，所以我们无法要求所有的应用程序工程师都要在硬件板子或实际机器上开发、测试与调整程序。实际上，在项目的初期到中期，我们也弄不到那么多的机器或板子给所有的应用程序工程师。基于这样的现实状况，即便开发模拟器要耗掉系统工程师一些人力与时间，但对具有复杂应用程序的项目来说却有极大的好处。”

■　节省培训众多应用程序工程师使用Cross-Compiler、Download Tools、烧录器等的人力与时间。

■　解决实际机器与开发工具数量不足的窘境。

■　在硬件平台还没设计完成，或驱动程序与系统程序尚未稳定前，应用程序就可以先行开发。

■　应用程序工程师可只专注于应用程序逻辑与算法的设计。

■　Windows上的开发工具调试功能较强，而且工程师通常较为熟悉。

■　在项目初、中期，产品规格设计者或客户就可以先在模拟器上检查应用程序的逻辑是否合乎需求。

菜鸟：“有时候，我真的觉得应用程序工程师比固件工程师幸福多了。对驱动程序工程师而言，就算程序逻辑没问题，程序仍然可能因为硬件的问题而不能动，往往花了很多时间做tria和error，而应用程序工程师似乎就只要专心致力于找出正确的算法即可。”

系统老鸟：“这么说也不完全正确，其实各有各的难处！驱动程序和系统注重的是如何与硬件整合、稳定度和性能，相对来说复杂度并不高。如果系统清楚地把硬件相关与无关的模块分开，而且提供完整的API供上层程序操作硬件，应用程序工程师几乎不用和硬件扯上关系，他们要伤脑筋的是其他的事。举例来说，GPS接收器的驱动程序要注意稳定度、性能、耗电等问题，但基本的逻辑并不复杂，就是和硬件工程师合作，按照data sheet设法让硬件正确运行。对应用程序而言，驱动程序自然会定时把正确的经纬度坐标送给应用程序，至于该如何从数据库中快速找出正确的地图并显示，技术上绝对不是件简单的工作。对电子产品的开发，无论硬件、固件、系统、子系统或应用程序，每个部分都很重要。

还记得我们的系统架构图吗？让我们稍微再复习一下，从图14-2中可以看出模拟器并非一个独立的可执行文件，说穿了，所谓的模拟器就只是Windows上的函数库或DLL罢了，它提供了与驱动程序API完全相同的接口，而这些界面具有与驱动程序相同的行为模式。我记得以前教过你文件系统的模拟，现在考考你是否能举一反三？”

菜鸟：“最简单的就是LCD的模拟吧！假设驱动程序提供一个在LCD上画点的API，输入的参数是X、Y坐标以及点的颜色，在机器上，驱动程序会计算（X，Y）这个点在Video RAM中的地址，然后把Color的值写入这个地址，则LCD Controller下次做刷新（Refresh）时，就会反应这个点颜色的改变。

▲ 图14-2　模拟器与系统架构

要在Windows上模拟LCD，如图14-4所示，最直觉的方法就是用一个Canvas（画布）对象，其长宽与机器上LCD的规格一致，要在模拟器上实现drv_draw_pixel（）这个函数很简单，就是把Canvas对象上（X，Y）这个点设定为指定的颜色。如果觉得PC屏幕上的点太小（举例来说，160×160的Canvas在PC屏幕上恐怕只是一个小方块吧！），我们可以用PC上的4个点来模拟机器上LCD的一个点，坐标的计算很简单，如图14-3所示。”

▲ 图14-3　LCD的模拟

▲ 图14-4　LCD的模拟

系统老鸟：“没错！总之，当应用程序调用驱动程序API时，无论执行于机器或模拟器上，都该有相同的行为模式。虽然应用程序加上模拟器是一个完整的PC程序，但其目的只是为了支持与硬件无关的程序开发，并非预计上市的产品，所以在设计模拟器时不会受到产品规格的限制，系统工程师可以用任何想到的方法来实现驱动程序API，你刚刚那个用Canvas上的4个点，以模拟实际机器上LCD的一个点，就是很好的例子。”

菜鸟：“您的意思是说客户定义的产品规格仅针对实际机器，而模拟器是给开发工程师用的，在设计时无需考虑类似安装/卸载、界面是否美观、操作是否简单易学等PC程序设计时的细节。”

系统老鸟：“实际上，并没有必要一定要让客户知道有模拟器这回事，否则，有时候还得费唇舌解释为什么需要额外的人力与时间来开发模拟器，但话说回来，在硬件与系统尚未稳定时，模拟器是研发人员和客户、规格制定者之间最简单的沟通桥梁，双方可以根据模拟器表现出来的效果，以确认研发方向是否正确。

此外，关于模拟器的应用还有几个注意事项。

■　模拟器还是有些特性无法模拟，例如，程序的性能优劣必须实际上机器才知道。

■　提供模拟器是让应用程序工程师可以使用最方便的方法及工具来进行开发，但不能把方便当随便，最终程序还是要执行于机器上。之前我们提过嵌入式系统开发的注意事项还是要严格遵守，尤其是存储器的使用更是要谨慎。

■　并非只有应用程序才可以在模拟器上开发，实际上只要是与硬件无关的程序，工程师都可以设法先在模拟器上开发与调试，等到确认逻辑无误后，才会到机器上确认效果。Windows上的开发环境绝对比每次改了一点程序就要下载到机器上来的简单有效率，所以不只是应用程序，绝大部分的系统程序也都可以在模拟器上开发。

■　模拟器存在的意义是因为Windows上开发环境功能强大，以及对工程师的学习曲线较为平缓，并非单纯为了图形化界面，所以不是要有LCD或其他显示设备的产品才可以从使用模拟器开发中受益。试想，要在机器上对某个有着复杂算法，或需要大量trial and error的程序调试，每修改一次，程序就要重新下载，再加上本身不好用且限制重重的开发环境[2]，那需要浪费多少时间啊！

菜鸟：“如果模拟器那么重要，为什么几乎没有任何嵌入式系统的教科书提到模拟器的应用呢？”

系统老鸟：“也许这些作者觉得这是common sense，或者他们的功力与耐心超人一等，所以开发产品不需要模拟器。但我觉得主要的原因是嵌入式系统的知识存在二三十年了，以前的电子产品确实没那么复杂，一些老工程师还活在用汇编语言统包一个产品的年代，但现在的电子产品的复杂度岂可同日而语，再加上进度急迫，用模拟器绝对可以加快开发速度，硬件相关的问题就交给硬件与固件工程师慢慢解决。在此同时，其他程序的开发也可以同步进行。”

14-02　Emulator vs Simulator

菜鸟：“虽然以前您教过我Emulator（模拟器）与Simulator（也称作模拟器，有时为了与Emulator分别，而将Simulator称作仿真器）的分别，我知道我们刚刚谈的架构是Simulator，但有时候，我还是无法完全区分这两者的差别。”

系统老鸟：“有个很简单的原则：Emulator主要是模拟CPU,Simulator则是模拟系统中硬件的行为模式，而且Emulator通常只能看执行结果，较难在PC上进行程序的trace与调试。以下是更为详细的叙述。

■　Emulator：完全模拟CPU的运行方式——‘根据PC（Program Counter）寄存器的值，自存储器中取得下一个指令，进行译码并执行’。输入程序的格式是编译、连接完毕的Binary Code（二进制码），如果用在产品开发，工程师可以不用把程序下载到真实机器，只要通过Emulator就可以得知程序执行的结果。因为Emulator和真实机器一样，接收的是程序的Binary File，所以即便Emulator也是一支Windows的程序，除非开发工具支持[3]，否则，工程师依然无法对源码进行调试。

在嵌入式系统的开发上，Emulator对开发的帮助仅只于硬件尚未完成之前，可以先让系统尝试执行。如果我们的CPU有现成的Emulator，那么用用无妨；如果必须自行开发，就真的大可不必了！首先，要完整模拟一颗CPU是一件相当困难的事情，恐怕这件事情的复杂度比我们的项目还高出许多（而且现在产品多以内含许多装置的SoC当作主控IC，要开发这种主控IC的模拟器真的不是件容易的事）。再来就是Emulator对开发的帮助相当有限，等Emulator开发测试完毕，硬件板子早就完成了，此时，Emulator将完全失去作用。

■　Simulator：模拟系统中硬件的行为模式，例如，模拟飞行的PC game就是一种Simulator，它只模拟驾驶舱中的操作按钮与仪器，并没有模拟这些仪器中的CPU吧！应用在嵌入式系统的开发中，我们所谓的模拟器（Simulator）模拟的是实际机器里驱动程序的API，这些API必须与其他的程序连接成可执行文件后才可用运行。

你看过电影《阿波罗13号（Apollo 13）》吗？在休斯敦太空中心（Houston Space Center；NASA）里有一套和真正宇宙飞船控制舱完全一模一样的设备，操作这个设备里的每一个按钮或控制器的反应也都会和运行中的宇宙飞船相同。它们具备相同的行为模式，但一个是造价昂贵的宇宙飞船，另一个只是摆在地球上某个房间中的设备而已。也是因为有这样的设备，NASA不用为了培训航天员而制造N台真正的宇宙飞船，而且当宇宙飞船在外层空间遇难时，在地球上居然还可以利用这套设备找出解决之道。

根据我们上面的定义，你觉得这样的设备是Emulator还是Simulator?”

菜鸟：“当然是Simulator!现在的飞行员也都得先在飞行模拟器（Simulator）上练习过才能真正的操控实机，而且飞行模拟器还可以模拟各式各样的状况，例如敌机攻击、乱流等。随着科技的进步，这种异常状况的模拟相当真实，所以这种培训相当有效，而且相对的成本非常低廉。”

系统老鸟：“你知道阿波罗13升空是多少年前的事情吗？1970年！直到现在，‘仿真’这个概念在产品开发上依旧发挥着无比的影响力。阿波罗13是用相对便宜的设备模拟昂贵的宇宙飞船，飞行模拟器则是用高超的计算机绘图与虚拟实境设备来模拟飞机操控，而我们用Windows上的功能来模拟实际机器的行为模式，其实概念是一样的，一方面节省了成本，另一方面则加速了开发或训练进度。”

菜鸟：“这样看来，Emulator的应用领域似乎比较狭隘，应该就只能应用在：想于A机器执行B机器的可执行文件，由于没有source code，所以只能在A机器上模拟B机器的CPU与外围设备。目前最常见的例子就是游戏机的模拟器，比如任天堂系列（超任、任天堂64）、PC Engine、PS、Game Boy, NDS系列，甚至我们小时候要投钱的大型电玩（街机）等，都有可以在PC上执行的模拟器，只要设法在网络上取得游戏的ROM（有心人士从游戏卡匣中将ROM的内容吸取出来，成为一个个的文件），就可以在PC上重温儿时的旧梦。甚至还有人将这些模拟器移植到PDA、Smart Phone上，重点是被模拟CPU的性能一定要比执行模拟器的CPU差很多，这是我唯一可以想到Emulator的应用了。”

系统老鸟：“在嵌入式系统开发的领域中，Emulator的应用确实越来越少，一方面也是因为现代CPU越来越复杂，要用程序模拟CPU执行指令的行为确实不容易。但我曾经碰过一个比较特殊的例子：某国际大厂委托开发新型的科学型电子计算机，为了要增加某些新功能，所以选用了更powerful的CPU，它们希望可以沿用旧机型中大部分的算法。此时问题来了，他们负责这些算法的工程师已离职许久，所有的程序都是用‘旧’CPU的汇编语言所编写的，据说已经有十多年历史了，现在根本没人看得懂，他们只能提供这些程序的Binary Code，这十多年来他们就是用这套没人看得懂的汇编语言程序及其组译出来的Binary Code撑着，根本没人敢说要换CPU，这样居然还出了好几款产品。

现在这家国际大厂把这难题丢给我们，如何在只有‘旧’CPU Binary Code的状况下，转换成在‘新’CPU上使用客户的算法？”

菜鸟：“难不成真的是在‘新’CPU上写一个‘旧’CPU的Emulator吧？”

系统老鸟：“对啦！确实也只能这么做了，幸好所谓的‘旧’CPU还算普遍，网络上可以找到其Emulator的source code，我们只要设法将其移植到‘新’CPU，并把算法的执行结果传出来即可。你可能会觉得很奇怪，为什么那么重要的程序居然在十多年间没有设法找人看懂？不要以为国际大厂一切就会照规矩来，或者大公司内部就会有多严谨的规范，信息界的确是无奇不有，特别是电子产品开发，啥怪事都有。”

菜鸟：“的确如此，看来那个所谓的国际大厂里的确养了许多得过且过的员工，这也难怪PM老大那么重视文件编写与组织技术资产的管理了。”

14-03　模拟器对项目开发的贡献

14-3-1　开发环境

菜鸟：“我记得就在target board开始debug不久，应用程序工程师已经开始写程序了，几乎和驱动程序开发同时。这样看来，模拟器应该是整个项目中最先完成的程序。既然模拟器对电子产品的开发如此重要，那我们团队中是由哪一组负责的？”

系统老鸟：“模拟器对嵌入式系统开发的意义，不仅是单纯的把驱动程序API在Windows上实现出来而已，更重要的是为系统与应用程序工程师构建一个在Windows上的开发环境。还记得我们之前讨论过如何构建开发环境吗[4]？当时讨论的重点为如何build出可下载到实际机器上执行的Binary File（或称Image File）。实际上，整个电子产品的开发周期中，使用最频繁的开发工具通常就是模拟器，在项目的初、中期，除了开发驱动程序与RTOS的部分功能，或者为了测试性能，相信绝大部分的工程师应该都不会想要在还不稳定的硬件平台上测试与调整自己的程序吧！

特别是在应用程序开发初期，需要密集测试调整的阶段，一个方便的调试环境绝对可以让开发的进度加快，如果只是要为了验证一个与硬件无关之逻辑的执行结果，都要花个几分钟下载才能开始Trace（跟踪程序），工程师恐怕不久就会发疯吧！因为工程师通常会急于知道程序修改后的执行结果，根据此结果才能做继续的修改，而下载的时间就只能耐心等待。除了浪费时间之外，工程师做事感觉老是被中断，无法一气呵成，久而久之就会不耐烦，失去耐性，长期处于不稳定情绪的工程师，其开发质量自然大打折扣。

如果在设计阶段就把驱动程序的API定义的很完善，模拟器的开发工作其实是很独立的事情，重要的是要尽快将开发环境构建起来，提供一个方法让应用程序工程师可以轻易加入自己的程序，能够在程序中调用驱动程序的API，而且可以在Windows上看到执行结果，并进行调试与跟踪。”

菜鸟：“我懂了！让硬件平台稳定原本就是固件与硬件工程师的工作，但应用程序工程师总不能没有平台可以执行与调整程序。在这样的情况下，模拟器就变成整个项目中的第一个目标。就如同要攻击河对岸的敌军一样，目标是由步兵歼灭敌军，当然步兵可以由河流上游渡河，不但效率低，而且经过长途跋涉，等到接近敌军时已是兵疲马累，如果有工兵先行负责架桥，攻击就可以尽快顺利展开。在这个例子中，架桥并不是最终的目标之一，但这个工作项目可以有效且大幅度的提高整体效率，所以就成为整个计划中最急迫的内部任务。”

系统老鸟：“你这个比喻还蛮传神的。在我们组织中，开发环境是由系统组负责的，所以模拟器也是由我们负责。在我前一家公司里，人力缺乏、组织也不健全，你觉得在这样的组织中最适合架设开发环境的人是谁？硬件、固件、系统、应用程序还是测试工程师？”

菜鸟：“您刚说过只要设计阶段就把硬件相关与硬件无关的接口定义好，模拟器的开发应该是一件很简单的工作，我觉得谁比较闲就让谁去做，开发初期最闲的应该是应用程序或测试工程师吧！”

系统老鸟：“在大公司待久了，你可能无法想象规模很小的team是如何用最有效率的方法压缩出时间与人力的，而这个现象在业界中并不是特例，反而还蛮普遍的。关于这个问题，你基本的假设可能就错了，根本不会有任何人是闲置的，而且设计文件可能缺东缺西。不妨再想想看，你觉得在这个状况下，谁最适合开发模拟器以及整体开发环境的架构？”

菜鸟：“不会是固件工程师吧，他真是劳苦功高！实际上，他一定要会使用所有的开发工具，包含Cross-Compiler、ICE、烧录器等，同时也是最熟悉硬件架构的‘软件人员’，而且驱动程序的开发本来就是他的责任，当然对驱动程序API最为了解。这样想起来，还真的是舍他其谁！”

系统老鸟：“如果可以的话，组织内能专人专用当然是最好的，但你也知道老板都是精打细算的，对年轻工程师而言，这样的工作性质其实不差，虽然工作量大、比较累，但总比月复一月地做着同样的事情要好吧！而且能对整个系统的开发流程有通盘的了解，看来我们老板就是想把你培训成这样的人。”

菜鸟：“是吗？我觉得他应该是把我当成他照顾的流浪狗，今天叫我去这里，明天叫我去那里，听说最近还打算叫我试产时去工厂待命，随时帮忙解决或回报驱动程序与测试程序的问题，我在这家公司好像没有自己的位置似的。”

系统老鸟：“别身在福中不知福，过个一两年你就会发现你的视野已远远超过你的同学。”

菜鸟：“最好是这样，我只希望有一天我可以像您一样，好像什么事都知道就可以了。”

系统老鸟：“在台湾地区，技术能力是重要，但是你很难在整个职业生涯中就只写程序。相信我，从现在开始就试着抓住任何可以扩展视野的机会，不要排斥你所不熟悉的工作，而且还要时时提醒自己加强管理、沟通及吸收新知识的能力。渐渐的你将发现，职业生涯时间过得很快，一转眼你已经30几岁了，如果你还是只会写程序，更惨的是居然只会写某一领域的程序，我想许多机会自然会渐渐离你远去。”

14-3-2　测试（Cross-Test）

系统老鸟：“模拟器除了可以加速开发之外，在某些具有大量数据或复杂使用者界面的产品开发项目上，模拟器还可以有其他的贡献。

■　在开发初期就可以让客户或规格制定人员确认研发方向是否正确。有时候非得要看到实际的东西才能发现规格定义有缺陷，或者工程人员对规格有误解。如果等到硬件OK，以及系统平台与应用程序整合完毕，才对规格做初步的确认，此时应用程序已经开发得差不多了，若发生结构性的问题就麻烦了。

■　在模拟器上可以实现一些额外的调试功能。例如：在模拟器上，程序可以输出一些调试信息（Log）或统计数据（Profile）到某个视窗或文件中，这在没有LCD或文件系统的实际机器上是比较难做到的。

■　有些测试工作可以提前开始，特别是像电子字典这种数据量很大的产品，必须尽早检查所有数据的正确性。又或者像是手机或PDA，这种功能多、操作方式复杂的产品，也应该尽早开始检查各种可能的操作流程。这些测试通常需要较多的测试人力与时间，而且并非一定得在机器上做，如果估算出来需要的测试时间与软硬件整合完毕的时间点无法配合，或可供测试的实际机器数量不敷测试人员使用的话，通常我们会决定让测试人员先行在模拟器上做较完整的测试（称之为Cross-Test），等实际机器入手后，再做规模较小的抽样检查。

菜鸟：“这样做QA人员不会有意见吗？虽然我们都知道运行在模拟器与实际机器上的程序是同一套，但RD单位如何向QA证明模拟器的行为确实与实际机器是一致的？”

系统老鸟：“这的确是一大难题，而且如果很不幸被找出几次模拟器OK，但机器上NG的bug后，即便这些bug明显是与机器相关的问题，产品质量与在模拟器上执行测试的有效性一定会被拿出来讨论，甚至大做文章。如果可以的话，谁不希望所有的测试都在实际机器上做？必然是有不得已的原因才会决定在模拟器上测试，例如测试量太大或所需时间太长，使得测试工作不能等到机器入手才开始。

因为模拟器并非万能，一定会有些硬件特性无法模拟，所以要清楚地界定模拟器的能力，即整理有哪些功能无论在模拟器或实际机器上一定是相同的。通常只要好好解释，QA人员还是必须要接受。如果QA部门还是无法想象为什么有的功能在模拟器上OK，在机器上反而不OK?那只好让负责测试的主管和质量部门‘瞧瞧看’，这种情况下恐怕只能修改测试计划，原本可以全检的功能可能会变成只能抽检了。”

菜鸟：“为什么我一路听你们说下来，好像质量部门跟谁都不对盘，动不动就要RD部门针对某个bug或现象进行解释，解释后要么听不懂、要么要求进行影响性评估，然后又不信任评估的结果。像刚刚那个例子，RD们都可以理解，某些功能用模拟器测试和实际机器是一样的，原本可以做完整的测试，却因为质量主管部门的偏执，导致只能抽样检查。应该控管质量的人，反而是造成质量降低的主凶，而大部分的老板只会将质量不良归咎到RD不够专业或测试人员不够认真。”

系统老鸟：“相信PM老大也常常抱怨这样的事情，一个产品要成功，一般老板以为只要研发团队的技术专业度够就可以了。近年来，渐渐有人知道测试工作也是一种专业，殊不知QA才是更需要专业的工作，因为产品质量的好坏必须被量化，不能凡事只凭感觉。以现代产品的复杂度而言，即便是IBM或美国国防部这些对软件开发流程相当严谨的单位，也没有人敢说他们的软件是完全没问题的（Zero Defect或Bug Free），所以软件质量的好坏不在于没有bug，而是软件的质量状况是否被控制。

一般人总以为程序只会越改越好，或者既然被找出bug就应该要修改，殊不知越到项目后期，程序通常越复杂，修改程序常常会造成副作用（Side Effect），导致原来OK的功能‘突然’NG了。可能为了修改一个不重要的bug（如一些user根本不可能做的操作）导致真正重要的基本功能反而出问题了，于是之前做的测试工作等于白做。以专业的术语来说，就是应该要控制质量的人反而造成了质量失控。”

菜鸟：“的确很讽刺，难道没办法改善吗？”

系统老鸟：“你去问PM老大吧！我也还在这个洪流之内浮沉，记得日后如果自己做老板，千万不要任由这种对立在自己的团队内蔓延，好的人才会待不久的！”

14-04　实战篇

菜鸟：“我已经了解模拟器对电子产品开发的好处了，您可不可以顺便教我一下‘模拟’的细节？”

系统老鸟：“OK!最近我要负责系统整合，模拟器有些功能还在扩展中，正需要人手帮忙，我试着讲解几个模拟的例子，然后将一些还未实现的驱动程序API分配给你做。”

菜鸟：“刚刚还说模拟器是整个项目中最先完成的程序，怎么现在已经要开始系统整合了，居然还有API尚未实现？”

系统老鸟：“这有什么好吃惊的，系统组的事情这么多，要构建开发环境，还要实现大部分的系统功能，我哪有人力做这么多事情？所以当然是按照事情的轻重缓急来调整工作顺序，模拟器的在初期最重要，所以我们当然会按照设计文件中驱动程序的API来实现模拟器的功能，但是随着驱动程序与应用程序开始实作，难免会陆续有新的功能需求出现，有可能是驱动程序工程师发现当初设计的某些功能难以实现，或者其实硬件功能比设计阶段了解的还要强大，所以对API有所增删。此外，应用程序工程师也可能发现API的缺陷，要求底层支持新的功能。凡此种种，一旦通过设计规格修正后，模拟器自然也要跟着对应，其他人员正等着用这些新增功能，偏偏我们又没时间做，看来这个拯救地球的重大责任就只能交给你了。”

菜鸟：“PM老大把我当作预备队，您把我当成救火队，可不可以哪天让我当啦啦队？”

系统老鸟：“别啰嗦了，这年头有工作做还能学习新东西，应该偷笑了！”

菜鸟心中OS：每次都这样霸王硬上弓……

14-4-1　模拟器实作的基本思想：模拟LCD

系统老鸟：“LCD的模拟非常直觉，我们就从LCD讲起，对此你应该有些概念了吧？”

菜鸟：“这个我曾偷偷研究了一下，总之，就是用Windows的Canvas对象（或其他继承自Canvas的对象）来模拟就是了，虽然细节我还没深入想过，要在Canvas上实现这些功能应该不难吧？”

系统老鸟：“的确是不难，我们开发系统时有个重点，就是能在Windows上做的就尽量先在Windows上做。例如等一下会提到的基本绘图功能，虽然Canvas本身就有画点、画线等功能，但我们实际在模拟LCD功能时，会把在Canvas上绘图的方法规划为类似LCD Driver对Video RAM的操作，则绘图功能将变成和实际机器一样，只要计算后再去改变存储器（称为Video RAM或Frame Buffer）内容即可，所以模拟LCD相关的程序就可以先在Windows上调试，即实际机器与模拟器可以使用同一套程序，相关细节稍后就会谈到。”

我们系统中与LCD有关的API如下。

■　LCD初始化：在机器上必须初始化LCD Controller并设置相关电源，LCD才可以顺利运行。在模拟器上就比较简单了，只要把Canvas对象的长、宽及颜色等级设定完成就可以了。

□　drv_LcdInit——初始化LCD。

■　LCD属性API：从配置文件中可以得到LCD的基本属性。

□　drv_LcdWidth——传回LCD的宽度（单位：pixel）。

□　drvLcdHeight——传回LCD的高度（单位：pixel）。

□　drv_LcdPixelBits——获得LCD的显示模式，每pixel所需bit数，如4色灰度就是每2个bit代表一个pixel的颜色。

■　LCD背光API：有的LCD模块包含背光功能，在机器上通常必须由某根Output PIN来控制背光功能的开关，在模拟器上可以实现背光功能模拟的方法很多，例如，模拟LCD的Canvas，其基本色调是黑白，当开启背光后可以把Canvas的基本色调调为绿色。在Windows上实作相当简单，应该只要调整Canvas的调色盘就可以了。

□　drv_LcdBackLightOn——打开/关闭LCD背光。

□　drv_LcdGetBackLightStatus——传回目前LCD开关的状态。

■　LCD明暗度API：有种IC在接收到特殊命令时可以输出指定的电压值，通常LCD的明暗度就是通过这种IC来控制，不同的输入电压可以让LCD产生不同的明暗度效果。在Windows上要模拟这种功能方法有很多，最简单的应该就是在Canvas的调色盘上做文章。

□　drv_LcdSetContrast——设定LCD明暗度等级。

□　drv_LcdGetContrast——取得目前LCD明暗度的等级。

■　LCD Frame Buffer API: LCD Controller要在LCD上显示什么内容，是由Video RAM的内容来决定，而LCD Controller的工作就是根据Video RAM的内容定时刷新。换个方向来看，LCD驱动程序只要改变Video RAM的内容，在下次LCD刷新时，就可以看到LCD的画面改变了。有时候我们也把Video RAM称为Frame Buffer。举例来说，如果LCD Controller设定为控制4色灰度的LCD，则一个点由两个bit表示，或说一个Byte可以表示4个点，如图14-5所示。

▲ 图14-5　四色灰度Frame Buffer图示

通常我们会准备两块Frame Buffer，一块是前台（Foreground)，一块是后台（Background)。在做图形计算时，会先保持前台与后台同步，将结果画在后台（实际上是将计算所得的结果写入后台Frame Buffer中），等所有图形画完后，再一次把后台的内容覆盖到前台。这种做法的好处是使用者不会看到一张图慢慢地画出来，而是稍等一会儿，整张图才全部显现。两者相较，后者的使用者感觉较佳。

通常我们在模拟LCD时都会保留Frame Buffer的操作画面方式，所以机器与模拟器上之基本绘图函数的程序代码几乎是一样的。总之，就是对前/后台Frame Buffer进行操作。这么做的好处是可以先在PC上调整与测试Frame Buffer的算法，届时LCD驱动程序就可以使用这些算法。

话说回来，通常模拟器会先配置两块和实际机器Video RAM相同大小的存储器，并以此当作前、后台的Video RAM。虽然Windows的Canvas对象也会有类似Frame Buffer的属性，但格式可能和实际机器的Video RAM不一样。所以较好的做法是模拟LCD Controller的工作，Graphics Library每做完Frame Buffer的操作后，就把Frame Buffer的内容反映到Canvas对象的画面上，使Canvas可以按照前台Frame Buffer的内容来显示。

□　drv_SetLcdDrawType——设定Graphics Library会画到前台还是后台，基本上就是一个全局变量而已。

□　drv_GetLcdDrawType——取得目前Graphics Library会画到前台还是后台的系统属性。

□　drv_LcdSetForegroundVRamBase——设定前台Frame Buffer的起始地址。在机器上，LCD Controller可能会规定Video RAM必须在特定的地址，或者Video RAM的地址必须是某个值的倍数（如16 K或128 K)。在Windows上这个特定地址可能是无效的，例如程序代码14-1所示的程序在实际机器上是OK的，但在Windows上就会有问题。

程序代码14-1

比较好的方式如程序代码14-2所示。

程序代码14-2

□　drv_LcdGetForegroundVRamBase——取得目前前台Frame Buffer的起始地址。

□　drv_LcdSetBackgroudVRamBase——设定后台Frame Buffer的起始地址。通常我们会把后台的Frame Buffer定义为一个array，模拟器与实际机器上的程序应该可以完全相同，如程序代码14-3所示。

程序代码14-3

□　drv_LcdGetBackgroudVRamBase——取得目前后台Frame Buffer的起始地址。

□　drv_LcdUpdate——使前台与后台同步，屏幕应该会立即显示后台Frame Buffer的内容。最简单的方法，就是将后台Frame Buffer的存储器内容以Byte为单位逐一复制到前台的Frame Buffer中，在机器与模拟器上都可以这么做。通常在实际机器上的LCD Controller会提供快速切换Video RAM地址的硬件功能，所以在机器上，只要把原本后台Frame Buffer的地址设定为LCD Controller新的Video RAM地址，则下次LCD Controller做屏幕刷新时，就会将原本后台的内容显示在屏幕上，这个方法只要对LCD Controller下一个简单的命令，性能当然比memory copy好得多。结论是，模拟器上实现drv_LcdUpdate()的方法可能会与实际机器上大不相同。

□　drv_LcdUpdateArea——好的graphics library会记录上次前/后台同步之后后台被修改的范围，则下次要做前/后台同步时，就不用整个Frame Buffer复制，只要同步有被修该的部分即可。这个函数在模拟器与实际机器上的实现方式应该是一致的，简单地说，就是Memory Copy。

■　LCD Graphics Library API：所谓的Graphic Library就是诸如画点、画线、画矩形、show字等画图函数。虽然Windows已然提供API可以在Canvas对象上做这些动作，但在实际上，无论模拟器或实际机器，我们会用相同的算法，即运算后直接写入Frame Buffer，如此一来，我们可以先在Windows上验证算法的正确性。

■　drv_DrawPixel——画点。根据点的坐标，算出其在Frame Buffer的地址，将代表颜色的值写入这个地址即可。要注意的是，Graphic Library会广泛用到bit operation，例如，根据坐标算出的地址是地址0x1234的第0与第1个bit（假设系统是4色灰度的LCD，两个bit代表一个点）。

□　drv_DrawVLine——画直线没有比较快的做法，就是通过调用多次drv_DrawPixel()来完成（x坐标相同，y坐标累加）。

□　drv_DrawHLine——横线每一个点的存储器地址都是连续的，所以画横线时可以不用一个点一个点画，可以先计算出起始点与结束点是第几个Byte的第几个bit，其中如果包含完整的Byte，就可以直接对整个Byte给值。假设系统是两个bit代表一个点，可以想象成原本要分别对4个点着色，现在只需要一个动作就可以对4点着色，速度自然快很多。

□　drv_DrawLine——基本的做法是由起始点与结束点算出斜率，然后利用斜率逐一算出轨迹上的各个点，这种做法简单直觉，但会用到大量的乘法与除法，性能相当差。我们很容易找到效率极佳的斜线算法，这些算法已经被证明是有效的，做产品的人只要将其设法移植到自己的系统即可，不需要花时间去研究算法的原理。

□　drv_DrawRect——矩形分为实心与空心两种，前者就画多条横线（y坐标累加）即可，后者则是由两条直线与两条横线构成。两者在模拟器与实际机器上都是一样的。

□　drv_GetImage——取得某个矩形区域对应的Frame Buffer的存储器内容，并将其存储为特定的格式（例如存储器内容之前加上长、宽、颜色数等信息。）

□　drv_PutImage——将drv_GetImage()取得之屏幕矩形区域，贴到屏幕的另一个位置，这组函数在做动画或show字的功能时很有用，但基本上就是Frame Buffer的操作而已。

□　drv_ClearScreen——清除屏幕，将整个Frame Buffer的内容都设为特定颜色（白色或黑色等）。

□　drv_DrawChar——基本显示英文字母的功能，底层提供的show字功能通常只是为了调试用，有时显示一些信息在屏幕上是最简单的调试方法，可以知道程序的运行结果等。其实show字功能就是通过drv_PutImage()来实现的，工程师必须事先把字型的bitmap转换为drv_PutImage()可接受的格式，通常是存储成array的格式，如图14-6所示。

▲ 图14-6　Font Bitmap图示

为了简单说明，假设LCD只有黑白两色，即一个bit代表一个点。首先我们把字型画在如图14-6的方格中，每一个方格代表一个bit，填满代表1，反之为0。可以看出这个字的宽度是24点，也就是3个Byte，高度也是24点，所以可以用3×24=72个Byte的数组来表示这个字型。图14-6所示范例的字可以定义为：

程序代码14-4

□　drv_DrawString——show字符串就是由调用多次drv_DrawChar()来完成。如果要做到像printf()这样可以格式化输出的功能，可通过与机器无关[5]的ANSI C函数——sprintf()，如程序代码14-5所示。

程序代码14-5

14-4-2　模拟器与系统配置

菜鸟：“我突然想到我们系统中有个sys_config.h，其中定义了许多系统配置，包含了LCD的宽度、高度、色阶等，我们在设计模拟器时应该也会用到这些配置吧？”

系统老鸟：“虽然不同电子产品的开发项目可能会采用不同的硬件，但研发团队的发展策略还是希望持续累积研发资产，即尽可能地重复使用已经验证过的程序代码，这就是系统配置存在的原因——使用同一套程序代码就可以build出可执行于不同平台的可执行文件。执行于不同硬件平台的系统，其程序代码都能够重复使用，模拟器程序代码可重复使用程度当然就更高了，因为不论产品硬件规格是什么，其模拟器势必执行于Windows或Linux上[6]，开发环境与‘硬件’都不变，所以只要善用系统配置的思想与技巧，模拟器的程序绝对可以重复使用在其他的产品开发项目上。”

菜鸟：“您的意思是说，模拟器和实际机器的程序共享一个sys_config.h吗？”

系统老鸟：“我个人是比较喜欢这样的架构啦，但我看过其他人有不同的做法，如图14-7所示。”

▲ 图14-7　独立的模拟器架构

系统老鸟：“在这种架构中，模拟器是一个独立的可执行文件（假设称为simulator.exe)，不管是什么类型的产品都可以支持，启动时会先去读一个配置文件（假设是sys_config.ini)，里面描述了目前模拟产品的特性，例如，存储器大小配置、LCD的分辨率、颜色阶度、是否支持Touch Panel、有多少key及其位置关系等。这个sys_config.ini必须由系统工程师编写，有了这个配置文件，模拟器就可以表现出相应的行为。

至于我们的程序会和另一套函数库或DLL连接，称为Simulator-Library。Simulator-Library会负责和simulator.exe沟通，一旦连接建立，当我们的程序调用驱动程序API时，最后会call到Simulator-Library中的相关函数，它会对simulator.exe下命令，当simulator.exe接到命令后，就会执行相应的动作，例如：应用程序调用了drv_DrawPixel()，真正的动作是Simulator-Library送出命令给simulator.exe，而simulator.exe会在视窗上画出一个点，此外，simulator.exe会将返回值送给Simulator-Library，Simulator-Library再往上传回应用程序。”

菜鸟：“您说这是输出的API，输入事件的模拟是不是由simulator.exe主动送给Simulator-Library?Simulator-Library在处理、过滤后，再把事件往上层送就可以了？”

系统老鸟：“没错！图14-7所示可以很清楚地看出来。其实这个架构只是把模拟器区分成simulator.exe与Simulator-Library两个部分而已，和我们之前讨论的运行原理都是一样的。而这个方法的好处是更具弹性、更加的模块化，模拟器本身可以在最小幅度的修改下，让下个产品开发项目使用。”

14-4-3　其他功能的模拟

系统老鸟：“接下来我们很快地把其他功能的模拟讲过一遍，如果你会使用如Visual Studio或Borland C++ builder这类开发工具写作Windows图形化界面程序的话，将会发现这些所谓‘模拟’的功能，真的相当简单。”

■　键（盘）：用一个button模拟一个按键，如图14-8所示。当button被按下（Key-Down)或放开（Key-Release），模拟key ISR被触发时该做的事情，我相信应该有很多人提醒过你，千万不要在ISR中做太多事情，所以key的模拟基本上是判断哪一个key是被按下或放开，然后将这个事件往上传递即可，可能是通过event的机制或者是message queue，视系统架构而定。要注意的是，真正处理key事件的程序是与硬件无关的，即可以直接在PC上开发。

▲ 图14-8　模拟器上用button模拟键盘

■　触控式屏幕：Touch Panel的操作也是通过中断触发，当使用者触压Touch Panel时会产生中断。Touch Panel ISR要判断两件事情，第一，目前被压下的坐标[7]7；第二，事件的类型是pen-down、pen-move或pen-up，和key ISR一样，将坐标与事件类型往上送。我们用Canvas或image物件模拟LCD，当鼠标游标在这些物件上动作时，它们会收到如OnMouseDown、OnMouseMove、OnMouseUp这些事件及坐标，正好可以用来模拟pen-down、pen-move与pen-up事件。

■　存储器缓冲区：如果实际机器中保留一块特定起始地址与size的存储器以做为程序的buffer，在实际机器上可以直接使用地址来操作，但在Windows显然行不通，比较直觉的方法是用一个相同大小的全域数组来模拟这个存储器缓冲区，再通过指针间接来存取，如程序代码14-6所示。

程序代码14-6

■　文件系统：在实际机器上，文件系统是存储在ROM或Flash中，可能有自己的文件系统表格，而系统也会提供诸如open、read、write、close等函数让其他程序可以操作文件。Windows具备很完善的文件系统，要在模拟器上模拟文件系统不是大问题，问题是要模拟到哪一个层次？第一个层次是用一个文件模拟ROM，该文件的内容就和实际机器上的ROM一模一样，所以模拟器和实际机器上文件系统的程序除了‘取得ROM里的数据，方式不同外，其余算法应该完全相同。这个方法的好处是可以先在PC上验证ROM文件系统算法的正确性。如程序代码14-7所示的pseudo code，这个函数的用途是从ROM里的某个文件读出一个Byte。

程序代码14-7

■　另一个模拟文件系统的方法比较简单，将所有会加入ROM文件系统的文件统一存放在PC硬盘的某个目录下，当应用程序调用文件系统API时，在机器上当然还是要进行读取ROM里文件系统表格等动作，如果在模拟器上就转换为Windows的API，直接存取上述特殊目录中的文件即可。这个方法在PC上实现起来很简单，在ROM文件系统还来完成以前，可以很快让模拟器先有文件系统的功能，才不致于影响到其他应用程序的开发。

■　电压检测模拟：如果实际机器是使用电池的产品，系统可能必须定时去检测电池的剩余电量（测量电压值），一旦电量不足应该要警示使用者，并且禁止其执行一些比较耗电的操作，否则，如果瞬间耗电太大，将使系统瞬间电压太低，如果超过RESET IC的临界值，会导致机器RESET。在机器上电池电量自然会随着时间改变，当系统发现电池电量不足时，就像key或Touch Panel一样会送出一个硬件事件，上层程序就是根据这个硬件事件做反应。

虽然在Windows上似乎没有直接对应的功能，但要模拟这个功能也相当简单，方法之一是用一个edit box。工程师在测试时可以输入一个电压值，模拟器会判断这个值，如果低于某个值便送出低电压事件，工程师就可以在PC上验证处理低电压事件的程序逻辑有没有错误。如果电压就是用一个值来模拟的话，那么，在PC上可以做出很多的变化。例如，一般干电池有个特性就是到快没电时，如果突然用电量变大，电压会被快速拉低，然后再慢慢恢复，这是低电压处理程序很容易出错的地方。我们可以在PC上写一个小程序来模拟这个状况，根据要测试的电池特性，定时改变代表模拟器电压的值，工程师就可以验证处理低电压的程序是否运行正常。

菜鸟：“只要对Windows图形化界面程序开发有一点了解，再发挥一点想象力，几乎所有的硬件功能或特性都有办法模拟。”

14-4-4　模拟器其他功能：Logging与Profiling

系统老鸟：“除了模拟硬件之外，模拟器基本上就是一个Windows程序，它可以做一些在实际机器上做不到的事情，这对我们调试或统计都有很大的帮助。举例来说，有些bug很难马上推测出错误的程序所在，要trace也不知从何trace起，这种状况下，show字是很常用的调试方式！假设我们的产品是没有屏幕的，该如何是好？”

菜鸟：“我知道您的意思了，虽然机器上没有屏幕，但是要在模拟器上加一个‘屏幕’却是一件简单又不花费成本的事情，而且机器上LCD通常是图形模式，show字其实就是画图。如果我们要在模拟器上做一个只能show字的‘屏幕’，只要用Windows上‘MEMO’、‘RichEdit’或其他可以显示字符的对象即可，它本身就提供显示字符串的功能。系统可以提供类似sys_Log_string()的API，但只有运行于模拟器上时才有作用，会把使用者输入的字符串显示到MEMO或RichEdit对象上，如程序代码14-8所示。”

程序代码14-8

菜鸟：“如此一来，不论实际机器有无显示设备，在模拟器上都可以很容易的输出调试信息（字符串）。”

系统老鸟：“输出字符串的功用很多，除了调试之外，还可以用来观察程序的执行顺序。例如在重要的函数开头处，都印个不同的字符串出来。尤其是在多任务（Multiple Tasking）与中断频繁发生的系统更是需要，因为程序可能在task间、或一般程序与ISR间跳来跳去，如果连程序是怎么执行的都没把握，有些较棘手的问题可能真的会找不到头绪。此外，字符串输出不一定要输出到所谓的‘屏幕’或‘视窗’，难道不能输出到文件吗？有时候，我们会先把实际的执行流程或计算结果输出到文件中，然后再慢慢分析该文件中的信息，这样的文件称为Log（记录）文件，可以让我们跟踪程序执行时的行为。

此外，有时候在做性能调试时，我们需要了解系统的瓶颈在哪里，才可以针对影响性能最大的函数或模块进行调整。举例来说，我们会想知道在真正执行某个功能时，哪一个函数占用了最多的CPU时间？哪一个函数被执行了最多次？或哪一个函数使用了较多的动态存储器等，收集执行时期系统信息的功能称为‘Profiling’，这些数据量可大可小，有些最好在机器上做，有些则在模拟器上做即可。

假设要统计性能的信息，如果机器的输出设备不方便（例如没有屏幕或屏幕很小，无法用诸如serial、parallel或网络线将信息传到PC，或可存储Profiling结果的存储器不够大），其实在PC上做也可以得到相对的比较值，通常也能找到问题所在。先在PC上调整完之后，再到机器上做较小范围的测试即可。”

菜鸟：“我试着highlight一下，所谓的Profiling就是在每一个函数的开头或结尾收集我们想要的信息，然后设法将其输出，因为数据量通常较大，所以输出到屏幕并不是很好的选择，最好能将其输出到文件中，之后再慢慢分析。要在Windows上实现这样的功能非常容易，我想到一个不错的应用：你之前提到动态存储器被破坏是很难检查的，如果我在所有或每一个值得怀疑的函数最后都去检查Memory Pool是否正常，然后将结果Log起来，只要分析这个Log文件应该就可以轻易找出始作俑者吧！然后再仔细trace这个破坏存储器的函数，应该就可以把问题解决。”

系统老鸟：“原理很简单，但是可以应用的领域很多，任何可以想象到的信息都可以被记录与统计。例如，我们在函数的最前面和最后面都去取得系统时间，或是在函数开头开始计时，结尾时结束计时，就可以得到这个函数的执行时间，也许这个做法不是非常精确，即便是在PC上取得的信息，只要比较各个函数执行时间的相对值，应该可以得到一些性能调整的灵感。

另一个应用比较简单，我们可以为每一个函数定义一个global变量，初始值为0，以后这个函数每被调用一次，这个变量就加一。我们可以在某个流程的开头将这些变量都设为0，然后在流程结束后（应该已经有许多函数已经被调用一次或一次以上）输出所有这些变量的值。分析这个结果便可得知这个流程之内哪些函数被频繁的调用，很可能是瓶颈所在。

此外，我们不是说过Stack的用量很难准确估计吗？通常项目一开始我们会给个比较大（比较安全）的值，等系统整合完毕后，再利用Profiling的方法来计算Stack的最大用量。做法非常直接，就是在每一个函数被调用时取得SP寄存器（Stack Pointer）的值，借此得知目前的Stack用量。如果把所有可能的操作都做了一遍，就可以得到Stack用量的最大值，通常我们再把这个值加大些，应该就是安全且不浪费存储器空间的值了。”

[1]　当然有些科技迷喜欢购买一些很炫却不实用的电子产品，但这些产品在广大的消费性电子产品、家电、汽车电子等市场中，毕竟只占极少数。

[2]　有些CPU的开发环境限制很多，例如，因为CPU的限制，最多只能设定两个中断点，这样的环境对复杂的程序而言是相当难以调试的。

[3]　例如iPhone APP的开发工具Xcode，与Android的开发工具Eclipse，都集成了各自的Emulator，并能对其“远程调试”（就像在PC上，用USB线连接实际机器进行调试一样）。

[4]　请参考《第7章：构建良好的嵌入式系统开发环境》。

[5]　虽然通常Cross-Compiler套件都会提供标准C的函数库，但printf()是要输出到显示设备的函数，编译器厂商怎么可能事先知道我们的硬设备是什么，必须经过一番修正或手续才能使用printf()。sprintf()则没有这个问题，它是一个完全与机器无关的函数，使用方式与printf()相同，但它会将结果输出到字符串，而不是任何显示设备。

[6]　至少笔者还没听过把Mac当成嵌入式系统开发环境的案例（iPhone与iPad应用程序开发则不在此例）。

[7]　有许多硬件架构可以实现触控式屏幕的功能，电阻式触控式屏幕的架构是当panel的某一点被压着时，驱动程序可以从两个AD Port分别得到代表x轴与y轴的电压值，然后利用线性的方法计算目前被压着的点所在坐标。例如，电压0时代表坐标0，电压3.3V时代表坐标319，如果电压是1.65V，则表示坐标是160左右。实际上的状况比这个复杂许多，主要是整片Touch Panel的电压分布并不是完全线性的，所以会需要校正的功能。本书限于篇幅就不详述Touch Panel的详细原理，如果工作上需要开发触控式屏幕的驱动程序，可以直接询问panel制造商的技术人员即可。

Chapter 15

项目进度追踪实务

时间：03月02日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

菜鸟：“漫长的规划与设计阶段终于结束了，至此我们已经有了一个计划与一堆设计文件，接下来，工程师应该可以暂时脱离不断被review设计文件的日子，专注在写code上面了吧！”

PM：“如同‘设计’是用来‘实现’的，‘计划’也是用来‘执行’的。既然项目已经进入执行阶段，除了工程师们按章行事外，我们管理者也不能闲着，必须更密集且确实地执行项目进度追踪，时时检验查看计划与实际状况间的差异。

所谓‘计划’与‘设计’，都是对未来状况的规划，我希望谁可以在什么时间点前用多少经费完成某件工作、某个模块做出来时会包含这些API、最终系统的性能怎样等，但人毕竟不是神，不可能正确无误地预知未来，所以‘执行计划’是一种不断检验查看现状、不断微调计划的过程。

有些项目管理者认为项目计划书必须像圣旨一样不容侵犯，其实这已是冒犯了‘神，的工作，我们身为平凡人，必须接受不可能有完美计划的事实。另外有一些懒得做计划的项目管理人，喜欢用‘计划赶不上变化’当推托之词，认为混乱终究会归于平静、凡事只要见招拆招即可。这种用运气来执行项目的方式或许偶可成功，但绝对不能当作组织运行的常事。

有一句话最可以表达项目经理这种‘做计划，并控制变化’的两难处境，就是美国五星上将艾森豪威尔将军说的：‘Planning is everything, Plans are nothing.’（实际情况与计划必定会有出入，但拟定计划的过程才是最重要的），做计划的过程可以让所有人事先想清楚项目中的种种细节，想得越清楚，失败的机会自然会减小，而计划执行过程难免会碰到之前预想不到的状况。若仍硬要根据最早制定的计划执行，则最后难免窒碍难行！”

15-01　进度追踪与变更控制流程

菜鸟：“这道理就是你老生常谈的‘在项目变更流程的控制下，计划是制定来遵守并死命达成的’，所以在项目执行阶段，项目经理要做的事情就是进度追踪与变更控制吧！”

PM：“是的！如图15-1所示的流程，项目进度追踪与项目计划执行是并行的两个工作，而且所有项目成员都必须参与其中。当任何工作项目发生延误，或者发现设计文件有重大疏失时，任何人都可以启动‘变更请求’，主管会召集相关人员讨论是否有补救的方法（例如请其他专家帮忙、加班赶工等）。若计划与实际状况出入太大，则势必得要修改计划。若放任不理，则实际状况与计划的落差会越来越大！使得之后项目的执行状况再也无法被追踪与掌控。”

▲ 图15-1　项目进度追踪

15-1-1　变更控制与进度追踪的原则

菜鸟：“若要让所有项目人员都能提高警觉，对任何可能无法按照计划达成的工作提出警示，我认为变更管理流程应该要尽量简单明了，而且要奖励在问题发生初期就提出变更请求的人员。”

PM：“项目管理教科书认为每个项目都必须成立一个‘变更控制委员会’，就是这样的规范使得某些人觉得变更管理是一件难以执行的工作！其实变更管理就是召集该变更影响所及的关系人（如果只是内部的技术或进度问题，计划调整后并不会影响最终产品的规格与进度，这种变更根本不需要惊动老板与客户）。讨论如何处理此变更请求，重点是对其他工作项目是否会有影响，然后将更新后的计划，发布给所有被此变更影响的项目成员，并责请各主管追踪‘新’计划的执行状况。

其实变更控制并不麻烦，和一般处理问题的流程差不多，只是变更管理的成果是一份新的计划。如果能从项目执行阶段一开始，就鼓励项目成员提出警示，则需要处理的变更需求应该都是局部的小问题，所谓的‘新计划’，也只会有局部的小影响。

当然只靠员工们主动提醒是不够的，项目主管还是必须根据计划里的查核点或milestone，时时进行查核，如果查核点够细，就不会等到问题大到无法收拾时‘突然’爆发。”

菜鸟：“您说的应该就是我们的进度review meeting吧！除非有特别要追踪的工作，否则我们是一个星期开一次，有些工作项目需要几个星期才能完成，我觉得这个review meeting的频率好像太频繁了。”

PM：“我的感觉跟你完全相反。我们并不愿工程师花太多时间在准备review meeting的报告上，所以才一个星期开一次会议。其实在两次review meeting中间，我会要求主管主动找版本下来build，随时掌握进度，对进度已有落后迹象的模块，必要时也请主管找程序代码下来看，这就是我要求你们每天都要把程序check-in到version control server的主要原因之一。

简单地说，进度追踪的原则是：

■　所有项目人员必须定时参加review meeting。

■　鼓励员工及早highlight可能发生的风险。

■　主管主动检查系统状态。

■　特别关注进度与计划有落差的工作项目（不是等到delay才关注，假设计划工时是10天，过了5天若进度未达50%，则应将其列入特别追踪清单）。

■　为了执行进度追踪，我们会引进一些工具（本章后半部分会说明），并将工具整合入流程中，例如，规定进度审查会议必须使用MS Project所产生的报表。”

15-02　进度追踪工具

PM：“我们公司和其他我所熟知的公司一样，买了不少工具保心安，引进后就束之高阁，谁也没认真学过，而MIS也尽量能不碰就不碰，老板偶尔想起来会问一下，但看到工程师已经熬了几天的黑眼圈，也就不好意思追问下去了。”

菜鸟：“想想，不好好利用这些工具其实是造成大家黑眼圈的原因之一吧！但有些工具功能太多，若组织没有制定详细使用流程，我想大家不会有使用的意愿的。

根据我的观察，需要熬夜的状况都是因为工作怎么做都做不完，工作做不完大多都是因为计划做不好所导致的，如果大家能使用更有效率的方法来执行项目，也许对大家的肝都比较好。”

PM：“是的！原本导入这些工具的立意很好，但就像IS09000或CMMI一样，若不认真执行，最终不免流为劳民伤财之举，但使用合适的项目管理工具，确实能够明显地增加工作效率。像我们这样的战斗单位，倾向选择较’轻量级’，的工具，除了比较容易嵌入既有企业或团队的管理流程外，工程人员也能在较无压力的状态下使用这些工具，而开始实践项目管理的精神。

另一种极端则是完全不采用项目管理工具，使用Microsoft Office自行设计了许多表格与文件格式，例如不使用bug管理软件，用Excel表格或Word写Bug Sheet，或者不使用软件版本管理软件，利用频繁的备份取代。其实这些做法不是不可行，但文件与文件之间、文件与人之间的连接很容易就会丢失或被忽视。例如，某个文件被误删、工程人员的个人计算机硬盘crash，或工程人员不查收mail等。

举个我曾听过例子：你可曾想像某张Bug Sheet（真的是一张纸），被工程师一气之下揉掉了，而测试单位的Excel表格也没管理好，最后工程师与测试人员都忘了这件事，而项目主管则完全不知道有这个bug，于是这个bug就此人间蒸发，等下次这个bug再出现时，已是在终端客户手上的机器。这个事件最后导致客诉，引起不小的风波。”

菜鸟：“了解！正如您常说的，穷人有穷人的做法，所以我们的做法是尽量采用便宜或free的工具来执行项目进度管理吧！”

PM：“公司确实是蛮抠的，但还不至于买不起那些昂贵的软件工程工具，虽然这些商业工具的整合程度确实较高，但通常也无法一套搞定所有嵌入式系统项目的管理工作，同时也因为其整合度高，导致功能较为复杂，不是那么容易上手。更麻烦的是，决定工具采购的人不见得是最后使用的人，常常花了钱买进与公司研发流程不那么match的工具。

对工程人员与项目管理者而言，每间公司或团队所采用的工具并不一致，所以他们学习使用这些工具的意愿并不高。因此，我习惯采用那些较易取得、容易安装与使用的工具组合，同样可以达到管理的效果，重点不是便宜，而是这些工具很容易可以嵌入不同公司里的既有流程。”

15-2-1　个人进度追踪

PM：“我们先从你每周都要写的进度报告说起吧！”

菜鸟：“原本我以为每周都要写工作报告是一件很麻烦的事情，不是小学生才要写周记吗？为什么我们做到了工程师还要写这种报告？于是我去找某个学长要‘周记簿’，没想到是一个简单的Excel表格，如图15-2所示。”

PM：“呵呵〜原来你们称这个表格为周记簿啊！我在团队中使用这个表格已经很久了，因为它很简单，一目了然，且不会让工程师有写报告的压力。

工程师对自己这一周内的工作一定记忆犹新，只要以条列式写下工作项目名称、状态（例如：已完成、在本周内完成、执行中等）与简单的描述即可，并以不同颜色或字体来表示状态，例如：黑色表示已经完成、蓝色表示本周完成、红色表示执行中等。写这样的表格花费不到10分钟，应该不会有人不知道自己本周做了什么吧？而且这种每周一次的会议，工程师若碰到困难也可以实时反应。

小组的review meeting时，由工程师向小组长报告自己的进度，就是讲述自己本周内的工作状况，基本上不需要特别准备，只有碰到问题或寻求帮助时，才需要额外准备资料。如果一切照计划执行的话，这种会议不需要太长的时间，研发团队主管与项目经理应尽量抽空参加，必要时可以马上帮忙协调或解决困难。

此外，这种表格可以当作考核时的根据。有谁会记得自己半年来做过的所有工作？更甭提自己的主管了！尤其是那种被临时assign，或做到一半被腰斩的工作。每周花10分钟做一次工作记录，也是工程师对自己的付出与绩效的证明。”

▲ 图15-2　项目成员进度报告表格

15-2-2　研发小组进度追踪

PM：“不论企业或项目团队都是需要层层上报的阶层式组织，通常一个小主管同时管5个人左右最有效率，尽量不要超过10个人，如果项目规模不是太大的话，项目组织尽量维持在只有3层：项目经理一技术组长一工程师。组织阶层太多，底层的声音容易被忽略。

技术组长必须对项目经理报告该团队的进度，同样的，我们不希望为了报告花费太多时间。所以技术组长的报告同样是每周一次，内容由其组员的每周报告内容汇整而来，如图15-3所示。表格中的主要item为该小组在这个项目中所承接的工作，重点是：基于目前的项目计划，是否有任何工作项目有延迟迹象？是否需要其他单位协助？是否应变更需求等。

项目经理与技术组长不需要特别安排每周会议，项目经理与技术组长们随时可直接沟通，部门间需要协调的时候，项目经理也可随时召开技术组长们的会议。

工程师与技术组长所有的每周报告都必须存储在server上公开的地方，所有项目成员都有权限可以看。通过工程师每周的review meeting及技术组长的weekly report，项目经理可以了解项目进度与团队运行情况，时间误差不会超过一周。这样的流程相当直观，且不会增加工程团队太多的负担，实际上，推行此制度的困难度很小。”

▲ 图15-3　研发小组进度表格

15-2-3　项目进度追踪

菜鸟：“我心里一直有个疑问：我们的每周进度报告表格和action item list都是用Excel来记录与管理，为什么项目记录一定要用Microsoft Project？感觉Project也是试算表啊，只是每个工作的后面会自动生成甘特（Gantt）图，其他功能好像Excel也做得到啊？”

PM：“你真的认真用过Microsoft Project吗？建议你去看看微软的在线使用手册，或干脆买本书来看，现在Microsoft Project的书都会顺便讲一些项目管理的思想与如何利用这个工具来管理项目。

项目有几个基本要素——工作（Work/Activity)、时间、成本、资源。这些要素彼此之间都是息息相关的。以下举几个用Excel不是那么容易做到的功能。

■　工作之间有前后的顺序关系，例如，工作A必须在工作B完成后才能开始，当你在规划工作A与工作B的起讫时间时，Microsoft Project会自动帮你检查时间的前后关系。

■　每件工作必须分配资源（或人力）才能完成，当你为每件工作都分配好了资源，如何知道这些资源的loading是否合理？有没有可能你为某工程师在同一天内安排了两天的工作？用Excel表格不容易查出这样的错误，但Microsoft Project可以切换到Resource View，马上就可以知道某工程师在什么时间点要作什么事，并且可以筛选出over loading的状况。此外，Microsoft Project还可以帮你做‘资源抚平’，试着消除计划中不合理的资源安排。

■　在项目管理的思想中，当计划完成后，它就是一个基准（Baseline)，所有在执行阶段的工作都不能低于这个Baseline。Microsoft Project可以将某个时间点的计划版本设为Baseline，以后即进入追踪模式，项目管理者可以随时输入各个工作项目的进度（例如工作A，在4月20号已经完成60%），我们可以很容易筛选出进度落后的工作，如图15-4所示。

▲ 图15-4　使用Microsoft Project筛检各个item的进度状况

■　还记得项目的关键路径吗？关键路径上的工作项目必须‘严加看管’，只要这些工作项目delay一天，整个项目的进度也会跟着delay一天。Microsoft Project可以在诸多工作项目中找出关键路径，Excel则很难做到这点。

■　Microsoft Project还包含许多项目管理教科书所推荐的工具，例如PERT、挣值管理、网络图、项目进度报告输出等。

Microsoft Project是专为项目管理所设计工具，功能应付一般的项目已绰绰有余，而且比起一些昂贵的项目管理工具，Microsoft Project算是相当容易上手。此外，你也可以选择其他的项目管理工具，如图15-5所示的GanttProject，它虽然是一个freeware，但不失为一个实用的工具。”

菜鸟：“所以我们就是用Microsoft Project来做项目进度的追踪吧！”

PM：“如同我上面谈到的，我们会把在某个时间点，将目前的计划设定为Baseline，并公告周知‘这就是我们要执行的计划，里面有工作项目、起讫时间以及负责执行的人员，请大家按章行事，以后，我们会按照这个计划上的时间点进行查核，任何员工发现任务无法如期达成者，请务必尽快通知你的主管！’

▲ 图15-5　使用freeware——GanttProject进行项目追踪

当计划的Baseline公布后，项目中各个研发团队组长以及重点工程人员要定期开进度追踪会议，会前项目经理已经从各研发小组的weekly report得知项目大概的状态，这个会议的主要目的是review并更新计划文件上各个工作项目的进度，检验查看是否有落后进度的项目，并讨论任何已知的进度或技术问题。”

15-2-4　变更管理

菜鸟：“当Baseline定下后，若发生变更该如何处理？Microsoft Project可以更改计划的Baseline吗？”

PM：“记不记得我们刚刚说过，处理完变更需求后会产生一个‘新的计划’，改变的部分当然要更新到我们的Project文件中，并将其设定为新的Baseline，Microsoft Project会重新规划关键路径，并找出目前已经落后的工作项目。

变更的需求有以下几个来源。

■　客户更动产品规格。

■　技术限制，导致必须修改规格。

■　项目计划与实际状况误差太大，导致进度或费用变更。

变更越到项目后期影响越大，项目经理必须随时观察可能发生变更的人与事，更要让所有项目成员都知道目前的Baseline在哪里，工程人员才有办法适时提出警示。此外，当变更发生时，要尽量沟通并寻求其他单位的协助，不能只是想到压缩工程时间。”

15-2-5　Event与Open Issues追踪

菜鸟：“照理来说，Baseline应该是不能轻易更动的，否则，很容易让工程师不把Baseline当作一回事。”

PM：“有些临时发生的小事或规划时没想到的事情，不一定都要启动变更控制流程，通常我们会使用Issue Tracking System，或简单的Action Item List追踪即可。

首先，我们来讲Issue Tracking System，它有点类似bug管理系统。市面的相关工具很多，收费的如：CodeBeamer、TeamTrack、Jira等（请自行搜寻相关资料），free的Trac （http://trac.edgewall.org/）也广为使用。这种工具可以让使用者制定处理某类事件的处理流程（所谓的流程就是规范。当某件工作，处于某个状态时，应该由哪个单位或个人来处理；处理完后，应该进入另一个状态，并通知另一个单位接续处理），使得该事件可以根据规划，一站一站的被处理，如图15-6所示。

▲ 图15-6　issue处理的细节信息都可以被记录下来

Issue Tracking System的运行是通过mail提醒目前该处理的人，当某个事件或issue已经进入处理流程，管理人员在任何时候都能够马上知道目前的处理状态，目前处理到‘哪一站’、事情正由谁在处理、之前经过了哪些处理等。使用Issue Tracking System很容易可以达成这个效果，每一站的处理人，处理时间、下一站接手的人等一目了然。当状态转移时，这个工具会自动发mail通知所有需要知道的人，此时，负责人与承接人都可以写下comment，这些信息都是很有用的经验累积与企业资产。

目前这些工具都已做成web-base，使用上非常方便，搜寻与筛选功能也很强，例如，能够马上产生诸如‘所有处理时间超过5天的issue’的列表，如图15-7所示，因为所有issue处理的信息细节都可以保留下来，是很有用的管理工具。”

▲ 图15-7　Issue Tracking Tool自动产生的几种报表

15-2-6　Action Items追踪

PM：“理论上，所有不在计划内的突发事件都可以用Issue Tracking System来管理，但有些事情比较简单，例如：工程师A于一周内采购XXX样品机，工程师B于下周二报告XXX.c的工作原理、项目经理帮忙找出前项目的某某数据等。使用Issue Tracking System来管理似乎有杀鸡用牛刀的感觉，但这些事情又十分重要，一定要被追踪。

实际上，我们用一个称为Action Item List的Excel表格，记录这些‘重要的小事’，如图15-8所示。除了记录时间信息外，若有必要也可指定priority，让PIC（Person In Charge；即负责完成此事的员工）感受压力。”

▲ 图15-8　Action Items追踪表格

15-2-7　Bugs追踪[1]

PM：“Bug管理也是一种流程，所以许多Issue Tracking Tool也可以用来管理bug，不过，我们选用专门用来管理bug的管理系统——Bugzilla，既然散布全世界的所有Linux Kernel开发者都可以共享一个Bugzilla Servere来管理bug，相信我们的项目复杂度不会比Linux Kernel高，而且我们的软件开发人员就这么小猫两三只，只要使用得当，我相信Bugzilla一定可以把我们的bug管理得好好的。

此外，bug管理系统也都会与一般常用的版本控制软件做整合。试想，我们有一个server可以查询所有曾发生的bug, bug的页面中会有测试工程师对bug的描述及工程师对解决方法的说明，然后还能连接到版本控制server，找出与此bug相关的程序代码。是不是感觉我们其实是个不错的软件团队？而这一切只要一台不用太强的计算机，其他工具都不用钱！”

15-2-8　Source Code与文件管理[2]

菜鸟：“这些不用钱的server会不会很难安装？很难相信便宜也有好货。”

PM：“在商业软件的世界，便宜可能确实没有好货，但Open Source软件中的好东西还真的不少，这些Open Source server的质量已经通过全世界不知多少研发团队的验证，质量你就不用怀疑了。

设计文件是设计与规划工作的成果，而程序代码则是软件开发工作的实际付出，两者同样重要。开发团队一定要设法将开发的轨迹保留下来，绝对不能等工程人员宣称完成后才去review，届时才发现问题就已经来不及了。因此，我们一开始就架构了版本控制server，并明确使用规范，不仅程序，所有设计文件也都必须做版本控制。

我们前面说过，研发小主管有一个重要的任务，就是在不干扰工程师的前提下，主动review编写中的文件与开发中的程序，以随时掌握并追踪进度，这也必须善用版本控制server才能做到。我在项目启动时，会很严肃的请所有工程人员天天commit他们的程序与文件，并要求工程主管频繁地观察文件与程序更新的部分，以随时掌握状况与进度，并可实时处理coding style不遵循规范、implementation不按照设计执行等状况。”

15-03　总结：“便宜”可行的项目追踪工具组合

PM：“导入大型项目管理工具在某些公司或研发单位并不实际，反而会成为执行人员推托的理由。与其执行半套，浪费金钱与时间，倒不如都不要执行。

Open Source界已陆陆续续开发出许多项目管理工具，试想，如果分散在各地的开发者可以同步开发Linux或其他大型的open source project，并把Linux的质量做到有目共睹，相信这些工具也能够帮公司内的project做好项目管理。

图15-9所示是我们团队在项目不同阶段所使用的工具。

■　规划/设计阶段

□　计划拟定

·　Microsoft Project（不算贵啦！)。

·　GanttProject （Free）。

·　Google日历（Free)：可将输入项目行事历与查核点，时间到之前会主动发mail通知。

□　设计文件

·　Microsoft Office（公司的计算机总不可能没有Office吧！）。

·　Wiki Server （Free）：架设与维基百科相同的服务器相当简单，这是一个可以让所有项目成员共同编辑文件的平台。

·　Google Document （Free）。

■　执行阶段

口　Issue Tracking Server

·　公司如果有引进Issue Tracking System，就直接使用那套，除了公司的内部流程已经建立，与其他单位的沟通也比较没问题。如果没有的话，建议就使用Free的Trac。

·　Bug管理——Bugzilla。

□　Version Control Server：使用CVS、SVN或后起之秀Git皆可，都是Free的。

□　Regular report和Action Item List-Excel。

▲ 图15-9　“便宜”可行的项目追踪工具组合

菜鸟：“果然就是如我所说的‘穷人版’吧！”

PM：“胡说！这是‘实用版’，贵的并不一定好，重点是容不容易利用流程来顺利导入这些工具。例如：

■　测试人员必须使用Bugzilla自动产生的报告，对高层简报系统bug的分布状况。

■　测试人员必须从SVN上找版本，并自己build用以测试的Binary Image。

■　项目经理必须根据Microsoft Project产生的进度报告，对高层与客户报告项目进度。

■　项目经理在谈论项目进度时，一律使用Project的计划文件（这会逼着其他人员不得不常常去检验查看Project计划文件中与自己相关的工作项目）。

■　在项目经理与研发主管的会议中，同时对Issue Tracking Server里尚未close的issue，以及Action Item List里的项目进行review。

市面上有许多成熟、稳定、免费或较便宜的项目管理软件，像是Version Control、Issue-Tracking、Bug-Tracking、文件管理等免费软件，都已经历经无数个大型项目的试炼，唯一的缺点就是这些工具彼此间欠缺整合，但这其实瑕不掩瑜，小型工具推行容易，而且在不同项目间切换更具弹性。只要项目主管有心，绝对可以找到合适的项目管理工具，工具太贵、太复杂、会影响项目进度等绝对都不是理由！甚至在iPhone上都可以找到不少管理项目的软件，如图15-10和图15-11所示。”

▲ 图15-10　iPhone上的项目管理软件——Task&Sponsor Tracking

▲ 图15-11　iPhonea上的项目管理软件——iGantt Chart

[1]　请参考《第17-3节：发行第一个版本》与《第18-2节：Bug Sheet管理》。

[2]　请参考《第7-7节：版本控制Server》，这里对版本控制的重要性有很详细的说明。

Chapter 16

SoC设计公司中嵌入式系统团队的管理

时间：04月02日　 地点：办公室一隅　 登场人物：菜鸟、系统老鸟

本书至今已经将嵌入式系统开发的规划、设计与执行阶段做了说明，在进入开发后期阶段（整合、测试、调试、调整、结项）之前，笔者认为有必要新增一个章节来介绍一种特殊的嵌入式系统开发团队——SA（System Application）。

一般电子产品开发要历经选择主控IC（CPU或MCU）的阶段，一旦选择后，即可取得该IC厂商的Reference Design（包含硬件参考电路与Sample Code)，但SoC设计公司中的嵌入式系统开发团队并没有选择的权力，因为他们就是该公司欲销售IC之Reference Design的设计者。

SA的处境比一般电子产品开发团队更为艰辛，但市面却少有书籍或文章说明SA的教战守则。相反的，在业界有许多SA团队还在使用土法炼钢的方式来执行开发工作。

16-01　SA（System Application）团队简介

菜鸟：“听说您之前在IC设计公司当软件团队的头头，这些公司的薪水和股票不是都很敢给吗？为什么会想要离开？是因为太操劳吗？”

系统老鸟：“难道我们这边就不操劳了？而且你认为我看起来像是怕苦的人吗？每个人都想在公司有长远的发展，但我之前待的IC设计公司显然无法给我这个远景。年轻人！人生还是有比钱和股票更重要的东西！”

菜鸟：“不会吧！我资工系的同学们挤破头都想要进这些公司耶！”

系统老鸟：“当然，我之前待的公司的经营文化可能不是最好的，但在IC设计领域普遍对软件人不重视却是常事。这个事情说来话长，且听我娓娓道来。

市面上的IC有千百种，CPU就一种，但随着IC工艺的进步与IC设计技术的成熟，现在业界比较常用SoC（System On Chip）作为主控IC来设计产品，以前必须放在CPU外的一些IC与组件，现在都可以很容易地整合到一颗IC里去。如此一来，不但节省了成本，也让电路板可以越来越小，结构设计有更大的发挥空间。

IC设计公司为了减轻客户使用自家IC的学习曲线，使客户的产品尽快量产上市，IC设计公司在销售其IC产品时，一定要提供Reference Design（硬件板子与参考程序代码）给客户。简单来说，这个Reference Design就是客户应用此IC的一个产品范例，而IC设计公司内负责提供这个产品范例的团队就称之为SA（System Application）。

SA团队的工作除了demo board的电路设计与板子制作之外，若是‘设备IC’的SA，必须提供demo board上的主控IC与操作系统的driver source code给客户。举例来说，Ethernet IC必须由CPU控制才可运作，SA会先制作此IC的子板，再根据这颗IC的产品定位，选择市面上比较popular的demo board，将子板与demo board连接，接着开始编写demo board上CPU与操作系统的driver，并将其调整到具有最佳性能，即这颗新IC的Reference Design。若有必要，SA可能必须多开发几个demo board或操作系统上的driver。

若是CPU或SoC的SA，则必须提供一个简单而完整的应用系统，包含以下项目。

■　已达量产水平的硬件平台。

■　必要的驱动程序。

■　嵌入式操作系统。

■　Library。

■　可以展示IC能力的Demo Application。

除了开发demo board系统之外，通常SA还要负责IC开发时期的FPGA验证，与IC试产阶段的IC验证，有时SA还必须负责分析、解决客户在使用此IC时碰到的疑难杂症（一般嵌入式系统团队在分析问题时，到最后才会怀疑是否为IC的问题，而SA的责任之一，就是要找出IC的潜在问题）。

原本SA仅要提供客户一个可以demo的软硬件平台，当成客户使用该IC时软硬件设计的参考即可，并非用以量产。而我们都知道，即便规格相同，做demo平台与产品开发（真正可卖到end user手上），实际上是完全不同的两码事！

因此，‘以前’的SA团队与真正开发电子产品的团队，在基本技能与开发思想上有着本质的不同，请见以下说明。

■　Scope与SPEC.的复杂度必然不同

■　质量的严谨度不同

■　Demo程序通常可土法炼钢：也就是硬干，以下重点都可不考虑。

□　系统架构。

□　软件工程。

□　扩展性、可移植性。

□　客户API。

如果只是做demo platform与客户支持，那么，其实并不太需要一个组织与流程严谨的嵌入式系统开发团队，再加上IC设计公司通常还是由IC设计专业起家，管理与技术主管都是IC设计专家，对IC生产流程的细节了如指掌，但这些IC设计的业界精英不见得真的了解电子产品开发的细节，更不可能知道如何run一个嵌入式系统开发团队，所以SA团队往往只是‘高科技IC设计公司’中的一个附属团队。

在某些IC设计公司，他们对IC设计流程有很严谨的规范，流程之间定义了严格的查核点，在IC Tape Out（将IC制造所需的数据提交给晶圆厂）前，也必须经过一次又一次的模拟与review！令人无法理解的是，在这样的公司中，SA居然不知如何做source code版本控制与bug管理、软件工程与质量概念异常薄弱、系统架构与模块化只是空中楼阁、设计文件杂乱无章等，更谈不上利用项目管理的思想来管理开发项目，SA的工作项目与进度规划完全取决于IC设计团队。

IC设计出了问题，制造好的IC可能变为垃圾，所以必须在开发流程中严格管控，若提供给客户的参考平台与sample code极不稳定，code乱七八糟且没有扩展性，客户对本公司的IC也会失去信心，即便再好的IC同样也会变为垃圾。也许这些公司的高层并非不注重SA团队，只是因为他们对嵌入式软件开发这个领域真的不熟悉，却又要用IC设计的思想来管理SA。

如果IC设计公司主要是‘卖’IC，那么，将IC设计团队比喻是强干，SA是弱枝也并无不妥，但让我们好好审视目前的市场状况，SoC设计公司真的是在‘卖’IC吗？”

16-02　SoC设计公司到底葫芦里是卖什么药？

菜鸟：“IC设计公司不是卖IC，难道是卖泡面吗？”

系统老鸟：“还记得我之前那间家公司的新任董事长，在誓师大会中聚集了所有员工，大声疾呼：‘我们是一间电子产品开发公司！我们必须直接面对最终端客户的需求！’，而且最近IC设计股王的老董也公开表明他们要开发云技术，这些大老板们可没说他们公司在卖IC吧！只可惜不是所有IC设计公司的经营者都能有此深谋远虑。”

菜鸟：“您的意思应该是说‘SoC设计公司只靠IC是赚不到钱的’吗？”

系统老鸟：“差不多！但只对了一半，具体的说法应该是‘若SoC设计公司只专注在提供高性价比的IC，是不足以让客户为这个IC买单的！‘还记得我们之前讨论过的’Turnkey Solution’吗？（请参考《第6-7节：SDK》）”

16-2-1　TurnKey Solution（统包解决方案）

系统老鸟：“电子产品的生命周期越来越短，要设计出全新产品（或所谓Killer Application）的机会也越来越低，制造商只能在既有产品种类中设法加一些独特性，既然独特性越来越难找到，制造商只好退而求其次，尽量压缩制造成本与Time To Market（上市时机）。在这样的情境下，电子产品设计团队倾向寻找一个完整的软硬件平台，最好这个平台上的应用程序已经整合完毕。

以山寨手机厂商为例，规模较小的手机制造商不愿再花时间与研发资源去做以下事情。

■　主要IC与组件选择、硬件设计与调整（‘这个主控IC与哪个通信模块配合，会有最好的性能与最佳的成本？’诸如此类的问题以前一直困扰着开发团队，因为规模较小的团队根本不可能有时间与资源去慢慢做不同组件之间排列组合的评估。）。

■　驱动程序开发。

■　系统开发。

■　通信模块开发。

■　基本手机应用程序开发。

■　系统整合与性能调整。

■　电源管理优化。

这些厂商认为以自己公司的规模，不可能在手机的基本功能上做出任何差异化，要在市场胜出，只能尽量专注在外型结构、特殊的应用程序（如游戏）与附加功能（如倍率较高的镜头或大功率喇叭）上创造独特性，如图16-1所示。因此，希望IC公司可以直接提供这类产品所需之基本功能的参考设计，而且质量必须可以达到成本要求与量产标准。

▲ 图16-1　山寨手机厂商在结构与新增特殊功能上创意无限

如果所有IC设计公司都不理会这个需求与趋势便罢，但优秀的IC设计公司深知服务至上的时代已然来临，率先对客户提交Turnkey Solution，客户只要稍加客制化便可出货。虽然该IC设计公司因此必须付出更多的资源，但也创造了惊人的业绩，蚕食鲸吞了可观的市场。导致其他IC设计公司被迫不得不迎头赶上，于是Turnkey Solution演变成SoC行业的基本经营模式。

这个变化只是短短几年间的事情，市场状况以及营利模式都已改变，但SoC设计公司内的组织文化与架构却没能来得及跟上（其实大部分原因还是因为公司内掌握权力的‘IC派’阻挡了变革，既然不愿深入了解嵌入式系统开发的精神，怎么可能重用或留得住真正的产品开发人才？）。使得公司内的SA团队无法尽快蜕变成一个完整的电子产品开发团队，直接影响该公司的竞争力。

简单地说，设计并制造一颗高性价比的IC并不容易，有其需要的专业知识与经验累积，虽然统称为电子业，但‘电子产品开发’与‘IC设计’几乎属于不同领域。你只要思考一个问题就好，同样都在手机市场，宏达电与联发科IC设计部门需要的研发与管理人才会一样吗？许多SoC设计公司的经营者还是没办法看透这个简单的道理。”

菜鸟：“我想倒不是这些大老板没想通，要在既有组织中改变已根深蒂固的思想与文化，并破坏组织间错综复杂的权力纠结，确实需要决心与魄力吧！”

系统老鸟：“的确如此，商业界也是适者生存的战场，唯有能快速适应环境的公司才有胜出的机会！蔡明介先生在《竞争力的探求：IC设计、高科技产业实战策略与观察》一书中谈到他对IC设计公司的看法，其中几段话都表示了系统软件的重要性。

■　IC设计业与晶圆代工业有很大的不同，IC设计是以设计应用产品为主。

■　Low Cost Solution是IC设计公司最重要的核心竞争力，而Low Cost Solution需要更创新的系统架构、更先进的数字类比电路设计，以及更复杂的系统软件来达成。

蔡董都已经说得这么明白了，SoC设计是个很特殊的产业，IC设计和嵌入式系统（软、硬件设计）开发各具专业，不可偏废。IC设计是台湾地区可以站上世界舞台的产业之一，只可惜行业里的某些老前辈们自己阻碍了更上一层楼的机会！

言归正传，我认为SoC公司真正在卖的东西是Turnkey与Service，只不过是通过IC的样貌出货罢了！”

16-2-2　卖Turnkey与Service，但以IC交货

菜鸟：“按照您这样的说法，IC设计概念股当场变成信息服务与软件概念股，我想，IC设计公司的老板与投资人恐怕都不会同意您的看法吧！”

系统老鸟：“如果按照股市分析师的方法来对产业分类，岂不天下大乱！电子资信业是由很庞大的产业链组成，我们在这个业界里浮浮沉沉，多少会知道上下游厂商间的供需关系。以手机业为例，就算现在有间天才公司开发出一个用于手机的无敌SoC，但如果就只是单卖这一颗IC，或者再‘附送’demo board和driver的sample code，我并不认为它在目前的市场状况能够大卖！你能告诉我，为什么明明是颗好的IC，却不见得卖得出去吗？”

菜鸟：“是因为客户的胃口已经被宠坏了吧？”

系统老鸟：“没错！这就是重点。产品开发商已渐渐习惯IC设计公司如此‘贴心’的服务了，现在IC设计公司如雨后春笋般的冒出，主打的产品线重复性质很高[1]，就算A厂商没有Turnkey、B厂商技术服务不好、C厂商的价格太贵等，产品开发商通常不愁找不到真正能提供‘一站式，服务的IC厂商。

这就是目前的业界状况，而且这是个不可逆的改变，SoC设计厂商要回到以前那种只卖IC就可以爽爽过日子的时代已经不可能了（除非该公司掌握了关键技术，或发现了真正的Killer Application）。你要卖IC给我，前期请给我Turnkey、中期请帮忙客制化、后期请协助解决生产与客诉问题，如果做不到，那真的就只能谢谢再联络了。”

菜鸟：“我了解了，实际上，客户只在乎最终产品的应用规格、系统稳定度与生产成本等，至于SoC里有什么高深技术，应该不是他们关心的重点。这样看来，SA的工作的确很重要啊！”

16-2-3　SA的工作内容

系统老鸟：“虽然市场状况已经至为明显，仍有些SoC设计公司的领导人，认为SA的工作确实是‘必要’的，但并不是那么‘重要’的，必要性和重要性还是有本质上的差异。以下我简单说明SoC设计公司内SA的工作内容，你可自行判断。

■　IC设计阶段

□　市场调查。

□　收集既有客户与潜在客户的需求。

□　Demo Board设计。

□　System BOM规划（客户直接拿Turnkey Solution去量产时，所需要组件以及预估成本总和）。

□　SDK开发。

□　FPGA＆IC验证。

■　Alpha-Site阶段：新IC上市前，为了确保IC与SDK的稳定性，必须实际模拟客户采用此IC进行设计，直到量产的所有流程，当新IC的设计告一阶段时，SA要开始设法找愿意当‘小白鼠’的客户与产品，通常我们将这个小白鼠客户美其名曰‘alpha site’。在alpha site产品的开发期间，SA必须全程介入软硬件设计与量产流程，并随时留意是否有因IC瑕疵所导致的问题。

■　Design-In阶段（客户已开始采用本公司的SoC进行客制化与新产品设计）。

■　此时，客户只有采用本公司IC的意愿，为促使其最后能够量产，SA在此阶段必须尽可能的提供协助，甚至直接帮其执行客制化（这很像一般系统开发商，接项目子回来做的工作方式）。

■　根据客户需求，为SDK新增功能。

■　协助客户导入量产，以及排解相关问题。

■　量产后阶段：很多IC的问题往往要在已具有足够量时才会显现出来（IC在生产时，除了每片晶圆的特性都不同之外，同一片晶圆里的不同位置，切出来的IC也可能有不同的特性。IC设计者必须提供几个阶段的测试程序，让工厂可以剔除不良的IC，若这些测试程序考虑不周或有bug，将会导致有问题的IC流到客户工厂），即客户量产后仍可能有‘灾情’传回——包含产在线发现的问题，以及终端客户退货或返修的case。此时，SA要设法帮忙分析与排除。”

菜鸟：“我觉得IC设计与SA应该是这种SoC设计公司的两大支柱，缺一不可，且重要性不分高下！”

16-2-4　SIP（Silicon Intellectual Property)

系统老鸟：“除了卖Turnkey与Service的SoC设计厂商之外，某些IC设计厂商也不直接卖IC，而是销售SIP （Silicon Intellectual Property；硅智慧财产权）的盈利。

所谓IP是一种事先定义，经验证可以重复使用的功能模块。随着IC内部的晶体管密集度变大，线路复杂，使得IC设计越来越困难，因而有IP的出现，可将IP想象为组成IC的一个个造型各异的积木。IC设计业者可以运用功能组件数据库中的IP，做适当的组合后，成为一颗IC。使用IP可以加快IC设计的速度，目前IP使用率大增，因此，导致许多设计能力较为完整的IC设计服务厂商，也开始经营IP业务。

IP可粗分为两大类，一为基本型，由工业标准所形成的功能组件，如PCI、USB、IEEE 1394及MPEG等；另一种属于特殊应用型IP，其复杂度高，不易开发，而且具有专利权的保护，属于明星级，无论是在产品生命周期、附加价值均较基本型IP高。

如果SoC里必然有一个CPU core，通常SoC厂商不会自己去开发一个新的CPU core，而是通过IP授权的方式，采用广为使用的CPU core，例如8051、ARM、MIPS等。如此一来，可以解决compiler的问题，也使得采用此SoC的开发商，有较多的研发资源可使用。

卖IP与卖SoC Total Solution的厂商还是有着本质上的不同。IP的用途通常较广，例如客户A将mp3-decoder IP整合进用于手机的SoC里、客户B拿去做多媒体播放器、客户C拿去做Set Top Box等。虽然卖IP也需要实现一个完整的demo平台，但原厂不可能为每种应用都提供一把Turnkey。所以这种公司的SA团队比较不需要接触产品开发商，通常要面对的是SoC设计公司，为该IP做的demo平台与SDK也鲜少需要再执行客制化。

我就曾碰过之前在卖知名CPU Core IP的老板级人物，始终质疑SoC设计公司里用来做客户服务的SA人数太多，质疑是不是SDK有问题，而要求一直改。这实在是本质上认知的错误！

16-03　正确的SoC设计流程

菜鸟：“我还有一事不明白。业界明明就有很多嵌入式系统开发的人才，对电子产品的开发与管理流程相当熟练，SoC设计公司里的SA可以leverage他们的经验，让开发流程流程更有效率，做出来的系统也更符合产品开发商的需求，因为这就是他们之前在业界一直在做的事情啊！

嵌入式软件开发的know how明显对这种SoC设计公司大有裨益，为什么您会觉得自己在IC设计公司中没有发展的空间呢？”

系统老鸟：“像我们做产品开发的，很清楚产品设计绝对是Top-Down的，必须先考虑到终端客户的需求与市场状况，才可决定产品或应用程序的规格，最后才是设定系统与硬件的规格。也就是说，当开发阶段碰到什么难以决定的问题时，应该要以终端客户的市场需求作为依托。

另一种设计逻辑Bottom-Up的思考方向则是完全反过来的。基本上是由最底层的IC能够完成的功能，来决定系统与应用，这在卖IC的时代应该是OK的，因为IC的选择性不多，产品制造商会自行开发系统与应用程序，并自己设法解决客制化的问题。但现在已经进入卖Turnkey的时代了，若SoC设计公司仍旧是以Bottom-Up的思维在想事情，是不是有可能与实际的市场状况渐行渐远？”

菜鸟：“我可以想象那种明知不可为而为之的感觉，所有工程人员都知道目前正在开发的solution不会有市场，但既然IC已经开了，进度与人力资源也都已经安排了，大多数主管也只能选择掩耳盗铃，持续为这颗注定失败的IC开发系统。长期下来，工程人员当然会有无力感，以及类似神风特攻队般悲壮的心理压力。

产品设计的流程错误真的会影响全局，我想您也是因此离开前公司的，但我好奇的是，您心中认为正确的流程是什么？”

系统老鸟：“要讲正确的SoC设计流程，最好还是从成功的案例说起。”

Turnkey Solution最为人津津乐道的成功例子，应该就是使用MTKIC的白牌或山寨手机了。由于MTK的崛起，不管其他SoC设计公司的主事者都能够学得几分精髓，在任何场合言必称“垂直整合”与“Turn-Key Solution”。

从硬件面来看，希望客户只要买你一颗IC，加上你免费提供的硬件设计范例，就能用最便宜的价格搞定全部硬件功能。从软件面来看，IC设计公司会提供所谓的SDK，基本上，客户只要修改User Interface就能够整合出一个完整的系统。因为Driver（驱动程序）、System（系统）、Middleware（中介软件），甚至大部分的Application（应用程序）都是由IC原厂提供，自然能够最稳定地发挥此颗IC的最大能力。

基于这个SDK，有理想有抱负的客户，会想要新增自己的特殊应用。有点品牌的客户，至少会要求修改UI，不会想要与其他公司的产品太过雷同。至于所谓的白牌厂商，根本改都懒得改（其实大部分这种公司不会养任何软件工程师），直接用原厂的SDK出货就是了。

有了IC公司的SDK，加上所谓的公板（硬件板子与组件）与公模（外型结构），不是IT本科的人，凑点钱、找几位操作员做组装与测试（测试多久就看老板的良心了），就能够做起手机制造商的生意了。深圳这么多的山寨手机就是由此而生。

在这样的生态下，一颗IC卖得好不好不再只是“传统”的价格与功能因素了，通常客户还会做以下比较。

■　系统的BOM Cost（搭配这颗IC的其他组件）够不够低。

■　SDK是否稳定，是否易于修改。

■　软硬件整合后的整体性能（例如耗电流、Audio Performance、ESD/EMI等）。

■　SDK提供的Applications是否可满足“客户的客户”的需求。

也就是说，卖SoC不再只是卖IC而已，卖这种IC几乎就是在卖一个可以马上拿去量产（Ready For MP）的电子产品。因此，这种IC设计公司不能老是想着用最低成本把IC做出来而已，他们还要养一个电子产品设计团队（SA)，里面包含软件、硬件、质量人员、客服技术人员等。这个团队和一般电子产品开发团队要做的事情基本上差不多，除了他们拿来设计产品的主IC，通常还是有bug的（IC设计很少一版OK的，SA团队也要帮忙IC找问题）。

如果在这样的SoC设计公司中，有任何一条以下的情形，那么在目前的商业环境下，即便IC设计人员都是天纵英才，就算作出了全世界最了不起的IC，恐怕都不见得是客户想要的。

■　还是把SA当作IC设计的黑手。

■　企业文化中本来就把SA当成帮IC擦脂抹粉的单位。

■　潜意识认为IC设计才是有技术的，即所谓的“RD”。

■　只把硬件人员当作“支持”人力。

■　永远不正视软件质量的重要性（我认为大部分搞IC出身的老板，还是有“软件质量完全是工程师的责任”的错误思想）。

既然卖SOC IC就是卖一个电子产品或Solution，那么，要如何知道哪种规格的产品才是客户想要的？IC只是电子产品里的组件之一，显然一切的根源应该是来自“市场研究”，进而决定“应用”（Application），再看看自己公司具备的能力，然后才是系统与IC规格的定义。许多SoC设计公司根本把决策的顺序搞错了，我认为这种贩卖SOC IC的公司，不只是间IC设计公司而已，本质上也该是间消费性电子产品开发的公司。

举例来说，图16-2所示就是我认为错误流程的例子[2]（其中MKT是Marketing；市场营销部门的缩写）。

▲ 图16-2　SoC的设计流程

许多公司都忽略了Marketing的重要程度，或者只是表面上认为Marketing很重要，实际上，并未用科学的方法与严谨的态度来做市场分析。Marketing负责定义产品规格，研发人员只要根据规格做出来即可，而产品在市场的接受程度则是Marketing的绩效标准。在大部分公司中，Marketing负责收集并分析市场信息，却不必为最终产品规格负责，所以Marketing当然乐得轻松，大可把产品规格需求讲得含糊不清、高深莫测。产品大卖是因为市场定位正确，若产品不幸失败，则是因为RD无能，把好好的产品规划做烂了。

既然Marketing不会明确定义“产品规格”，那么，就顺理成章改由IC设计团队来定义“SoC的规格”，期间当然偶尔会咨询SA单位的意见，但通常只是善尽告知的义务，反正系统厂里的电子产品开发团队不也是这样吗？IC有什么就用什么吧，难道Intel或Broadcom会让你参与IC规格的定义吗？

唯一不同的是，即便这颗IC的规格根本不符合市场的需求，IC设计公司里的SA团队始终没有选择或更换其他家IC的权利！

于是一颗可能“不知民间疾苦”或“不知今夕何夕”的IC诞生了，SA团队只能从IC的规格、Marketing的意见、与客户的需求自行定义产品规格，由负责开发产品的团队定义产品规格（球员兼裁判），岂不可笑？

再由产品规格，定义软/硬件规格（硬件板子+软件SDK) ，接下来就和一般电子产品开发流程一样，但SA团队还有个重要任务就是必须负责把IC的bug隐藏起来（一般称为Workaround，即用其他方法“避过”这个bug），并设法挤出IC的所有能力。

如果这个Solution侥幸卖出去（这里指的是卖Solution，而非卖IC) ，而且居然还卖得不错，此时，SDK会根据不同客户的新需求而不断扩展，当IC预期出货量到达一定程度时，IC设计团队就会开始计划做cost down版，并把已知的IC bug尽量收掉，通常此时会被想到的就是拿掉IC中没有用的IP、重新检讨IC内部layout、缩短IC测试的时间、使用更精密的工艺等，而居然就“忘了”要参考SA的feedback!

例如：是否因为IC的limitation造成客人的某些新功能无法实现？或某些SDK新增功能是否可改用硬件加速等？毕竟SA是在第一线接触客户的单位，应该最熟悉客户的需求与抱怨，而一般客户根本不管你IC里面藏着什么高科技，他只在乎产品造价（System BOM）是否具竞争力，以及SDK功能是否足够且稳定。

也就是说，新IC卖出去后，除了cost down版IC外，应该更积极思考下一代的“产品”（这里说的是产品，不是IC）。以下才是我心中的正确流程，如图16-3所示。

▲ 图16-3　SoC的设计流程之二

与前一个流程不同之处为：

■　Marketing负责分析市场与客户需求，进而定义Application，完成产品规格并为此负责。

■　Marketing所主导的Application SPEC。或Product SPEC.，为此SOC设计项目的基准（Baseline)。

■　IC与SA团队根据Application SPEC。分别定义IC与软/硬件系统规格，并交互review。

■　新版IC的规格，来自新的市场信息，以及IC与SA团队的反馈。

16-04　SA的严酷考验

菜鸟：“您待过两家公司，应该可以比较我们系统厂的嵌入式系统团队，与SoC公司里的SA团队，在必须承接之任务与所需技能上有何不同吗？”

系统老鸟：“你想要跳槽吗？我跟你说，虽然SA的地位普遍不受重视，但他们要做的工作性质可是比我们还要艰辛呢！”

■　比一般电子产品开发团队需要更全面的硬件知识。

□　必须执行FPGA与IC验证。

□　可能拿到“烂”IC。必须能分析到底是IC、硬件或软件的问题。

□　随时要能掌握IC的性能信息。

□　Workaround。尽量帮IC梳妆抹粉（通过软件或硬件手段）。

■　比一般电子产品开发团队需要更严谨的设计与评估流程。

□　系统在设计时，可能IC连影子都还没有，评估工作要尽量精确。

□　系统要能完全发挥IC的能力，隐藏IC的缺陷。

□　IC验证与系统的code必须可以重复使用。为了更严谨地执行系统设计，必须从流程规范中落实design review。

■　比一般电子产品开发团队需要更严谨的系统架构。

□　移植性（HAL)。同一系列的SoC必须使用同一个SDK, HAL在SoC的SDK中必不可缺。

□　可扩展性。客户可能会要求我们在SDK设计阶段根本想不到的新功能，系统必须高度模块化，并具有可扩展性。

□　客制化。通常客户只需要修改SDK里的Application，所以SDK一定要设计标准的Customer's API，让Application的开发更为容易。

■　比一般电子产品开发团队需要更强的硬件设计团队。

□　具弹性的硬件平台。必须同时支持不同客户的硬件客制化需求。

□　BOM分析。demo board不是可以动就好，必须到达量产水平。除了性能外，硬件工程师必须让硬件的生产成本具有优势，所选择的组件也必须易于备料。

■　比一般电子产品开发团队需要同时服务更多的客户。这对SDK的稳定度、硬件设计的弹性、客服技术人员的人力资源配置等都是很大的考验。

菜鸟：“从上述工作性质看来，SA的工作从新IC计划开始，潜在客户需求收集、IC规格需求分析、FGPA验证、SDK开发、IC验证等，一直到客户服务，从IC设计到量产与客服，几乎通包了，SA的工作量大，要求技术全面、软件架构，照理说，SA团队应该需要更多的资源，以及更严谨的工作流程规范才对。若部分公司反其道而行还能‘暂时’成功，但只能说，这些公司里的工程师真的太辛苦了，他们其实可以有更大的贡献！

如同PM老大常说的：让工程师得以有效率的方法做事，绝对是管理者的责任，若工程团队必须无止尽的加班才能完成多余的工作，其实正突显了管理者的无能！”

16-05　SA团队的管理

系统老鸟：“许多管理者都有种错误的思想，由团队的平均工时来评断其努力程度，这对用正确方式做事的团队是相当不公平的，而且所有工程师都知道如何来累积自己的工时（白天上网，晚上工作），但这偏偏是业界的常事。

这个迷失会如此泛滥，首先是因为现在的老板之前也是这样被评鉴上来的，只好继续恶性循环；第二个原因是管理者未受过真正的管理培训，除了工时外，实在不知道用其他的方式来评估团队的成果。你必须庆幸本团队的主管凡事会做计划，知道用实际的产出来评估工程人员的贡献。

我们刚刚谈过SA团队与其他嵌入式系统团队的差异，如果SA没有导入系统架构、项目管理与软件工程的思想，并制定正确的开发流程，反而还是用土法炼钢式的硬干，如同你说的，SA里的工程师会更加辛苦，无止尽的加班只会磨损士气，无效率的工作方式会扼杀创意与生产力，此绝非公司与团队的长久之计。

所以我认为SoC公司内的SA团队反而需要更严谨的管理，除了我们团队现有制度的精神与思想外，还要加强以下项目。”

■　流程的重要性：一个团队要执行这么多种类的工作，一定要制定流程，避免重复工作的资源浪费，举例来说。

□　FPGA与IC验证的code必须是同一套。

□　FPGA与IC验证程序里用到的driver，必须就是系统里用的那一套driver[3]。

■　HAL的重要性：SoC设计先期的投资金额不少，所以一定会计画推出同一产品线的一系列IC，为减少将来维护工作的effort，同一系列产品最好就只有一套SDK，此时就可看出HAL的重要性。

■　系统模块化（Configurable）：SoC的SDK和一般嵌入式系统的SDK不同，同一套系统要support的客户数量非常多，而不同客户有不同的系统配置与功能需求，未经模块化设计的系统，绝对无法快速反应客户的需求（否则，就只能用工程师的肝功能来换取客户的满意度了）。

■　SA面对的SoC可能是有bug的，系统必须设法避过（Workaround) ，这些bug可能会在下个版本的IC里被修掉，而新版的IC也可能会出新的bug，所以系统里会有不少针对不同版本IC的patch。除了SA要maintain好各代IC的Workaround List外，还要求系统必须要有很好的Configuration管控（简单地说，就是规范各个版本IC的系统，要加入哪些patch）。

■　客制化的effort越小越好：SDK一定要有标准的Customer's API，一来客户开发AP的学习曲线较平缓，而且既有的AP，可以很容易地移植到新版的IC与SDK上。

■　SDK与demo board的质量必须达到量产标准：SA里的QC或测试工程师，其测试强度必须达到产品化的水平，而测试强度则牵涉到测试流程的规范是否严谨。此外，SA里的测试工程师除了必须执行SDK（或产品功能）的测试外，有时也要帮忙与IC特性有关的测试工作。

■　硬件平台必须弹性：和软件系统一样，客户会以SA提供的demo board为基础，并做功能的增删，所以demo board的设计也必须考虑模块化。

[1]　因为新开一颗IC的前期投资金额较大，必须找出潜在销售量够大的产品线才可能营利，但目前这样的产品类型并不多，例如：手机、机顶盒、多媒体播放器、数字电视、网络设备等，却有几百家厂商要来同分一杯羹。

[2]　对错没有绝对，也许有些人认为这个流程在某些公司run得很OK，笔者也深表祝福。

[3]　笔者真的看过一颗IC，三套driver的团队。

Chapter 17

系统整台

时间：05月02日　 地点：办公室一隅　 登场人物：菜鸟、项目经理（PM）

PM：“刚刚系统组闹哄哄的，发生什么事了？”

菜鸟：“终于看到第一个应用程序在机器上执行，虽然速度卡卡的，但大家都很兴奋。”

PM：“这个项目到目前为止也执行了几个月了，之前硬件、固件、系统与应用程序各组虽然偶有交流，但几乎都算是独立工作，现在第一次系统整合可以顺利完成，除了感谢大家的努力外，其实更应该归功于设计阶段的成果。就像是分进合击的数支部队，要分别走不同的路径，面对不同的敌人，当他们如期在约定地点、约定时间顺利会师时，那种感动可想而知，要达到这种效果，必然要先制定完整且细密的先期计划，加上优秀军官的指挥，与训练有素的士兵负责执行。

嵌入式系统也是一样，如果中间某个环节疏忽了，例如硬件进度延误、应用程序调用了驱动程序不支持的API、开发环境设定有问题等，要准时完成系统整合简直是缘木求鱼。”

菜鸟：“我觉得许多军事管理的思想都可以用到业界的项目管理上，您这个比喻真的很传神，确实点出了设计工作在整个项目中不可轻忽的重要性。不过，要管理这么多不同领域的人共同完成这个可以动的小东西，仔细想想还真的不简单。”

PM：“管理嵌入式系统开发项目的重点，就是要搞清楚每一个时期的‘瓶颈’所在，并将其当作该阶段最重要的事情。例如硬件设计与板子制作如果出问题，势必影响驱动程序的验证，也会影响工厂试产的进度。如果开发环境与模拟器一直无法完成，其他的系统程序与应用程序如何开始实作？凡此种种都是项目管理者真正该关心的事情。”

菜鸟：“那么，目前整合阶段最重要的事情是什么？”

PM：“我现在最想要得到的信息是：

■　第一个应用程序是否可顺利在机器上执行？这是第一关，至少我们知道有一个应用程序已经顺利Portingl[1]上去，平台应该没有什么致命的问题。

■　底层驱动程序与系统程序是否稳定？应用程序执行起来会不会频繁地、莫名奇妙地死机？如果真是如此，我应该会下令整合工作暂停，先找出稳定性的问题所在，否则整合了越多的程序，状况只会更复杂而已。平台不稳定我是不会让整合工作继续的。

■　哪些功能OK，哪些NG？确定平台稳定之后，我会要求整合者制作一个表格，横轴是时间，纵轴是系统中所有的功能项目，我随时都要知道哪些功能还不能在机器上执行，这样我就知道该先去盯谁了。

■　性能如何？系统中各个功能难免会互相影响，所以整合工作最重要的目标就是要做出一个虽然还有bug，但基本上算稳定，而且产品的所有功能都可运行的版本，有了这个版本，我们才能制作‘性能缺陷表’，作为下一个阶段‘系统调整（Tuning）’工作计划的根据。

我等一下要去客户那讨论试产的schedule，你先帮我去系统组那里问一下状况，看有没有需要帮忙的，告诉系统老鸟，晚上我回来后要找他谈一下接下来的工作重点。”

菜鸟：“报告！是！反正我是传令兵嘛。”

17-01　第一次整合

菜鸟：“PM老大听到第一个AP已经可以顺利执行，心情好像还不错，他说晚上要找您谈谈。”

系统老鸟：“看来又是你这个传令兵多嘴，目前情况恐怕没你们想的那么乐观，我之前就和他说过目前还不是最合适的时间，新版的硬件板子还要一个星期才会好，所以驱动程序新的修改还没确认OK，再加上应用程序完成度都还不及八成。即便现在将一个应用程序整合到机器上执行又能代表什么？硬件板子和驱动程序都不是最新的，还没半个应用程序已完成所有功能，现在整合的状况仅能参考而已，其实再等半个月才是最好的时间点。”

菜鸟：“既然如此，为什么PM老大这么急着要做整合？他刚刚还说，如果驱动程序或平台不稳定，他是不会允许整合工作继续下去的。”

系统老鸟：“他要考虑的事情比较多，可能是客户的要求，或者试产的进度紧迫，他只能凭借经验，根据目前的状况，试图压缩一些时间以赶上已delay的进度，当然这么做还是有点风险，谁知道新的硬件板子是不是就完全没错误了？驱动程序真的可以如预期般顺利在新板子运行？应用程序未如期完成，搞不好是因为碰到了什么无法解决的瓶颈等。

以我的职责来说，在整合之前，我应该要帮忙了解各个单位的状况，或者我必须知道从底层到上层所有功能的完成度。在开始整合之后，我就要盯着所有的功能一个一个移植到机器上执行，然后再时时检查各个功能的完整程度与性能。我已经把我所知道的状况都报告给PM老大了，他决定现在就要开始做整合，应该有他的根据和考虑吧！”

菜鸟：“如果是这样，您觉得最佳的整合时间是什么时候？”

17-1-1　最佳时间点

系统老鸟：“如果不考虑到整个项目的进度的话，我觉得最好要达到以下条件再开始做整合会比较好。

■　硬件设计不会再有大变化：基本组件与PIN脚配置都已确定，剩下的只是微调的工作而已，即以后硬件的调整‘应该’[2]都不会影响到驱动程序。

■　驱动程序已经稳定：‘稳定’的定义应该是通过长时间且大量的单元测试、整合测试与压力测试，其中压力测试对驱动程序的稳定度更显重要，单单验证驱动程序的正确性是不够的，例如keyboard driver测试不能一个一个key慢慢按，至少必须测试快速且连续的按法，或者多个key同时被压下、某些key长时间被按下等。除了使用‘较死板’测试手法之外，固件工程师还必须写作一些压力测试程序，例如长时间连续对存储器读写、利用信号发生器制作特殊信号输入到系统中，测试驱动程序是否仍可正常运行。固件工程师必须自觉地做完这些测试，我们已经说过很多次了，底层的不稳定会随着被整合的程序增加而放大，稳定度是驱动程序最重要的基本需求之一。

■　重要的系统功能都已完成：至少要整合的应用程序会用到的系统功能都必须完成。

■　至少要有两个应用程序已完成所有功能，且已经请客户或规格制定者在模拟器上确认过其完成度。

但实际上不会这么美好，对吧？如果等一切条件都齐备才开始做第一次整合，PM老大恐怕要被客户抓去砍头了。”

菜鸟：“是啊！除了您说过最新的硬件板子还没完成之外，我知道许多系统的功能也还没完成，现在应用程序能够执行是因为我们先把这些系统功能用空函数取代，所谓的空函数就是先把函数的框架写出来，让系统连接可以通过，但函数的内容可能只是一行return而已。据我所知，这种尚未实现的函数好像还不少。”

系统老鸟：“这也是没办法的事，我现在负责整合，出了问题要到处去找相关的工程师来帮忙解决，哪有空把这些函数补上啊？现在的状况算好的了，至少所有程序的编译与连接都通过了，而且应用程序也在机器上运行起来了，我以前曾经碰过几次搞了很久还是整合不起来的状况。”

菜鸟：“如果大家都按照设计文件做的话，怎么会有整合不起来的状况？”

17-1-2　导致整合失败的原因

系统老鸟：“因为PM老大的坚持，我们team的设计审查工作算是做得相当扎实了，但不一定所有的开发团队都是如此，而且工程师在实作设计成果时，是否有误解设计的原意或者谎报进度，这都会导致整合工作失败。一般来说，可能的原因有以下几点。”

■　设计更改：设计阶段结束后还是有可能进行设计修改，可能因为来自客户的要求，或是在实作阶段才发现设计的瑕疵等原因，偏偏这个时间点正是兵荒马乱的时候，包含系统工程师，所有人都忙着写自己的程序，所以往往设计变更的结果无法顺利传达给每位工程师。而工程师忙起来时通常会‘不小心’忽略其他人的mail，当然也没那种闲功夫与精神主动到server下载新的设计文件。最后结果就是：有人始终活在旧设计的世界中、模拟器与实际机器上API的接口或行为不一致、应用程序使用了不存在的API、固件工程师居然不知道硬件的新修正，导致系统不稳定或某个装置在新的板子上‘突然’不work了，这些状况在整合时才一一浮现，而且通常一时三刻还看不出到底是谁出了问题。

■　这应该算是管理上的问题，要避免这种状况并不困难，只要设法贯彻设计更新的流程即可。首先要求所有工程师都必须注意设计修改的通知，并要求外部厂商（Third Party），如硬件或软件模块设计者，不得片面修改设计。系统工程师对新设计最好能实时对应，如果是API改变的话，只要模拟器的相关函数一修改，其他应用程序应该马上就会发现编译或连接error，一来可以提醒工程师注意设计更新的通知，二来所有受影响的程序可以马上跟着对应。

■　实作失误：有些自诩天纵英才的工程师根本不把设计文件当一回事，他们会用自己的方式实现其所负责的模块，一种是函数定义（参数及返回值）不同，另一种则是函数行为的差异，两者都会造成整合失败。定期且确实的code review可以及早阻止这种行为的发生，如果工程师对其负责的模块有较好的设计或想法，应该要循管道进行设计变更流程，研发团队主管必须以整体的角度来讨论是否需要更改之前的设计成果，并评估这个修改的影响范围与程度。

■　进度控制失误：这很容易理解，到了预计开始整合的时间点，但却有个重要的程序模块未完成，那么，整合工作当然无法如预期时间开始。就像在攻击发起日，所有的部队都已到达攻击发起线，但有个主力部队迟到，导致战线的中央有个明显的缺口，此时发起攻击必败无疑，但延后攻击时间却也未免耽误军机。所以项目经理要时时注意各个工作阶段的“瓶颈”所在，宁愿集中人力与资源去完成可能造成瓶颈的工作项目，让较不重要的功能delay。

■　进度管理失误：实际上，有些功能的进度（除了检查程序之外）是‘看不到，的，通常是没有画面显示的模块，例如文件系统或其他算法的实现。如果主管没空做code review的话，有时工程师会刻意隐瞒进度落后的事实或目前碰到问题的困难度，谎称一切都on schedule，往往要等到整合前才穿帮。为了掌握所有工程师的进度，各个小主管必须定时从server下载最新版本的程序，抽空检查其组员的程序，如果有问题应该立即召开code review会议，并协助该组员找出解决问题或追上schedule的方法。

■　驱动程序压力测试不足：驱动程序在独立运行时可能完全没有问题，系统负担加重后，可能问题就会逐一出笼，这样的状况是不适合开始整合工作的，所以驱动程序开发不能以一般状况没问题为满足，固件工程师必须根据装置的特性，主动设计各个驱动程序的压力测试方法，可能的压力测试手法如下，只有做完所有装置的压力测试，这个平台才合适开始做系统整合。

■　特殊的操作方式，例如：用笔或手指在touch panel上频繁的乱画。

□　长时间地操作某装置，例如：持续不停的压下某个key。

□　利用信号发生器产生特殊的信号，如图17 - 1所示，并连上CPU的特定PIN脚，模拟与此PIN脚连接的装置持续送出信号。

□　写测试程序主动频繁的操作待测装置，例如：持续地对存储器进行读写操作。

▲ 图17-1　信号发生器（右方则是示波器）

系统老鸟：“你可以看到，除了某些技术的瓶颈外，其实大部分都是管理上的问题，一般研发团队最大的问题就是小主管自己也要负责开发，而且通常是开发的主力，有些小主管会刻意排斥管理的工作，把自己埋首在程序堆中，这真的不是很好的现象。如果基层研发小组的code review能够确实落实的话，很多整合失败的问题就都不会出现了。”

菜鸟：“但偏偏大部分的研发团队都缺乏人手，所以code review或设计变更流程常常都只是流于形式，甚至干脆就免了。”

系统老鸟：“我觉得这不只是单纯人力或schedule的问题，你听过20/80法则吧！20%的人力提供了80%的贡献，那其他80%的人力都在干什么？难道不能通过管理手段，将他们的产出至少提升到40%或50%以上？在实际上，有经验的研发主管不见得就是优秀的管理者，开发和管理是两回事，无论是项目经理或部门经理都必须设法把对的人摆在对的位置上。

有时候硬要年资较高的工程师兼任管理职务反而适得其反，结果往往不但他的生产力降低了，他负责的研发小组也被弄得乌烟瘴气，整体生产力当然大降。所以啊！不要以为你在台湾地区能做一辈子的工程师，趁年轻时多看看管理的书，碰到状况时，看看老板是如何处理事情的，思考如果你是老板的话会怎么做。”

17-02　全功能整舍

菜鸟：“总之，第一次整合应该算是成功了，接下来的整合计划是什么？”

系统老鸟：“我想应该会请PM老大召开整合会议，所有的研发大小主管都应该参加，这个会议的主要目的是：

■　了解系统各个模块的工作进度与状况，包含硬件、驱动程序、系统与应用程序。

■　根据上述信息，制定合理的整合计划，包含整合模块的顺序与schedule。

■　上述schedule应该包含整合完成后，性能调整等相关工作的安排。”

菜鸟：“有必要在这个时候重新定义schedule吗？项目运行至今，大家都很清楚自己该负责的工作范围了吧，现在的情势不就是ASAP吗？现在工程师们的情绪和时间都绷得很紧，这个时候找大家重新定义进度，会不会让士气更低落？”

17-2-1　Schedule检讨：该是面对现实的时候

系统老鸟：“你说的不无道理，但我应该灌输过你一个思想，ASAP并非负责任的进度管理方法，再加上系统的诸多模块间难免会有依存关系，所以整合工作当然必须谨慎地安排先后顺序，并用心管理。工程师或其主管有责任要向管理阶层报告目前状况以及预计完成日期，而项目经理则必须汇总所有信息，找出可能的瓶颈所在。

我们在项目初期曾经制定了一个schedule，运行至今，其实已经有点与现实脱节了，有些工作可能已提前完成，有些工作可能比当初想象的还要棘手，目前是重新检验查看软件开发schedule最合适的时间点。如果用项目初期制定的schedule来看，整个项目应该已经delay约一个多月了，现在是面对现实的时候了。重新检讨schedule的好处很多。”

■　利于进度管理。工程师对schedule都有一定程度的依赖，一旦某项工作delay之后，排在后面的工作自然也得跟着delay，此时，管理者最好实时介入帮忙重排schedule，否则许多工程师会就此失去时间的方向，认为反正都delay了，先把手头的工作完成，其他的慢慢再说吧！

■　使追踪往后日子的进度成为可能，如果整个项目已经开始在delay了，则项目经理很难只根据旧的schedule追踪进度。系统中软件模块众多且实作状况都不相同，不能只是简单地把各单项的进度加上整体delay的时间而当作新的进度，必须趁着要开始整合的时间点，逐一了解每个模块开发的最新状况，制定合理、可施行、可管理的新schedule。

■　通过对软件开发现况的“普查”，可以试着分析造成delay的症结所在，管理者可据此设法调派人力与资源，或许可以找到好的方法把delay的时间赶回来。

菜鸟：“所以这个schedule会比以前那个要细啰？”

系统老鸟：“不只是工作项目较细，时间轴的精度也可以估计到以天甚至小时为单位，这样才可以找出瓶颈所在，并找出在鬼混的人。所以，接下来我的工作就是按章行事，按照新schedule上的进度整合并简单测试各个模块或应用程序，在预计时间前的一两天我都会先提醒相关的工程师，如果可能来不及完成，我必须将这个情况上报，老板自然会找人处理。如果即将delay的功能会影响其他功能，必要时，老板会实时做人力的调配。”

菜鸟：“随着整合进来的模块越来越多，系统中程序间的交互关系自然也越趋复杂，除了您刚刚说第一次整合会碰到的问题之外，全功能整合还可能会碰到什么问题？”

17-2-2　全功能整合会碰到的问题

系统老鸟：“当整合进系统的程序越来越多，可以想到的是，程序越来越多、code size越来越大、存储器越用越多、平台的负担越来越重、程序间的纠缠也越来越复杂等。实际上，常常发生一些始料未及的问题，而这些问题通常并不易解决。

■　Memory空间不足：这是全功能整合最常见的问题，因为存储器的使用量在项目早期并不容易估得精确，而硬件规格却已早早固定下来，而且应用程序在PC上使用模拟器开发几乎没有存储器用量的限制，所以‘用爆’存储器的问题往往在整合阶段才会爆发出来。存储器管理可能发生的问题，我们在之前已经提过，在此简单的复习一下。

□　Stack：虽然我们已经三令五申地要求所有工程师谨慎使用Stack，包含不要使用size太大的局部变量与注意递归（Recursive）调用的阶层，但总是有人会出状况。还记得Stack被破坏时显现的‘病兆’是什么吗？包含Stack内或Stack区域前后的存储器内容莫名奇妙被破坏，以及function call无法正确return等[3]。

除了程序的疏忽导致Stack被破坏之外，也有可能是我们把Stack的最大用量估计得太小了，只要先试着把Stack区域放大，如果问题都解决了，几乎可以确定就是Stack区域太小问题。解决这个问题的方法虽然多少有点劳师动众，但Stack Overflow呈现出来的现象就是系统很不稳定，其影响的范围几乎是整个系统，绝对必须优先解决。

·　检查所有程序，确认Stack使用量最大的程序，并检讨是否真的需要这么大的Stack。

·　使用Profiling的方法，得出系统可能Stack的最大使用量[4]。

□　Heap：程序中使用动态存储器可能发生的问题如下。

·　程序超用其配置来的存储器，导致Memory Pool中的队列被破坏。

·　碎片。程序明明没有用到这么多存储器，但有时候就是要不到，这会导致某些功能有时候运行正常，有时候无法执行。外行人统一用‘不稳定’这个词来形容这种系统状态。

·　目前配置之Heap空间的大小真的不符应用程序的需求。

Heap的问题越早解决越好，最好每整合一个模块，就观察一下Heap有没有异常状况，否则，等到所有程序都整合了，Heap的使用状况将变得更复杂，出了问题会更难找出到底是哪些程序造成的。

□　程序size超出预期、ROM空间不足。在系统设计时会先预估各个用途的存储器使用量，并据此设计Memory Layout[5]，但这毕竟都是估计值，等到整合工作开始后才会渐渐明朗。如果当初存储器配置的设计就是连接的很紧密，若某段存储器的size超出预期，势必会压到另一个存储器区段。举例来说，假设Memory Layout的设计如图17-2所示，我们先来看ROM的状况，data1原本设计配置在ROM的位置A，如果程序的size超过A，势必会压到data1的空间。这个状况在制作ROM的Binary File（或Image File）时就可以被发现，解决方法是程序减肥（Downsizing），或重新配置ROM里各个区块的位置。

▲ 图17-2　Memory Layout设计简图

接下来看RAM的状况。可以把data段和bss段的size理解成程序中所有全局变量的size总和，这个size在程序连接完毕后就已确定，工程师可以从连接阶段产生的Map File中得到相关信息。如果超出之前的设计，势必会破坏Heap的内容。问题是不会有工程师每次build完就去看一下Map File，所以这个问题通常会延迟到执行时期才被发现。同样的，要解决这个问题不难，只要重新检查程序中的全局变量使用状况，或重新配置RAM里各个区块的位置即可。

这种问题最严重的状况为：程序或数据已经无法减肥，存储器用量也已确定是最精简的。简单地说，就是估计和实际用量相差太大，目前硬件配置的存储器size不足以让系统全功能顺利运行。如果真的是这样，麻烦就大了，要不就是更改硬件规格，把存储器size加大，不然就只能缩减软件功能，即变更产品功能规格。

■　Task不协调：如果应用程序必须用到多任务，则系统中会有多个Task（或Thread），而这些Task可能由不同的工程师负责实现；如果彼此没有协调好，或者有某位工程师对多任务系统的理解不扎实，在整合时可能会发生各个Task单独执行都很正常，但‘同时’[6]执行时就出问题了。这些问题不外乎Task间通信、同步、优先级设定、共享资源的保护等，只要理解对都不难解决，你可以去参考一些Multi-Threading Programming（多线程操作）的数据。重点是在已经整合的Task还少时就要尽量测试，有问题便实时解决，千万不要等到所有Task都已整合进来，到时状况的复杂度可能要以倍数计。

■　Critical Section未保护：这里说的Critical Section有两种，第一种是Task间可能会‘同时’存取的全局变量，或同时执行的程序段；第二种是一般程序与ISR（中断处理程序）可能会同时存取的全局变量，或同时执行的程序段。中断与工作切换（Task Switch)会被引发的时间点可能每次执行都不一样，程序必须设法避免操作Critical Section尚未完成前就被切换到别的程序段，否则等恢复执行时，Critical Section的状态已经被改变。反映出来的状况是每次执行的结果可能都不一样，请参考程序代码17-1，Taskl的执行结果可能每次都不一样。

保护Critical Section的方法很多，driver层会提供中断enable/disable的功能，在进入Critical Section前禁止中断产生，离开Critical Section后再恢复，即在Critical Section内什么中断也不能产生[7]，保护功能为最佳。此外，RTOS都会提供诸如Mutex、Semaphore等机制，一般RTOS的书都会提到如何利用这些机制来保护Critical Section。

程序代码17-1：未保护Critical Section的实例

■　许多在模拟器上OK，但在机器上NG的bug会纷纷出笼，除了上述存储器或Critical Section的问题之外，这类bug多半与驱动程序、Cross-Compiler、甚至与CPU的特性有关。举例来说，以下程序在PC上一定不会有问题，看起来也没什么可疑之处，但在某些CPU上就会造成死机。这样的问题在整合时才会出现。

这种问题难处理的地方在于它不能在Windows上Trace，而应用程序工程师通常都不熟悉在机器上调试的流程，所以往往需要固件工程师的协助。”

系统老鸟：“你知道上述这些问题，我最怕的是什么吗？”

菜鸟：“我觉得是多任务吧！之前我在PC上写Multi-Threading的程序被搞的七荤八素的，Thread的执行顺序可能每次都不一样，导致调试困难，而且Thread间的同步与通信也是一大难题。”

系统老鸟：“如果工程师对‘多任务’的理解不扎实的话，确实可能写出一个‘唯我独尊’的Task，这种Task一旦整合到系统中，其他原本运行无误的Task可能都动不了了。但这毕竟还是软件的问题，可以通过教育以及code review的方式解决，只要理解正确，其实Multi-Threa ding的程序没那么困难，更何况一般嵌入式系统通常是特殊用途，不需要用到太复杂的多任务功能。

软件的问题我们都可以关起门来解决，最麻烦的还是存储器使用量的问题，如果到这个时候才发现存储器用量大于硬件配置的存储器size，你说会发生什么事？”

菜鸟：“减肥？把用不到的程序mark掉、检查存储器的使用是否浪费、压缩数据等，都可以减少存储器的用量。”

系统老鸟：“当然我们要尽力去做减肥的动作，毕竟现在硬件配置的存储器是我们评估OK的，如果当初我们估计的系统存储器用量和现在做出来的系统误差太大，怎么办？这种悲剧我碰过不只一次，不能只怪执行者考虑不周，有时候项目初期的信息真的不够，只能根据经验来评估。审查的工作固然重要，但我现在要谈的是如何面对这个困局？你知道这个问题会带来什么影响吗？”

菜鸟：“如果软件真的无法解决，只好由硬件配合加大存储器啰！”

系统老鸟：“这可不是‘就把存储器换大啰’这么简单一句话，就可以轻描淡写带过的，此举会让制造成本增加、工厂备料可能出问题、硬件电路必须修改、是否重新制作板子、之前做的硬件测试可能要重做等。其中最严重的问题应该是成本的增加，增加的制造成本由谁吸收？如果你是客户，你愿意认账吗？这种问题的责任归属很难扯得清楚，如果是客户更改规格导致存储器用量增加，那自然是客户的责任，否则没有人会听研发单位的解释，客户会认为估不准就是不专业，于是RD又多背了一个黑锅。如果公司最终得赔偿客户损失，通常老板会把它直接转嫁到RD们的年终奖金或分红上。”

菜鸟：“难怪我觉得您在做整合时的态度比之前开发时更谨慎，原来是为了大家的红包着想。”

17-2-3　Porting的原则

系统老鸟：“还有一种和系统整合有关的工作项目，叫做‘Porting’，顾名思义，就是将在别的平台运行正常的程序或函数库移植到我们的机器上。简单地说，就是利用现有的程序资源，避免浪费重复开发的人力与时间。一般而言，需要Porting的程序都是属于较专业领域的技术（如手写辨识引擎、压缩算法、FAT32文件系统等），并非我们团队的专长，与其再雇用专长较接近的工程师重新开发，不如设法取得别人已经做好的程序模块，我们只需要稍做微调，就可以让这些模块在我们的硬件平台上执行。这些程序模块的可能来源为：

■　其他项目的程序：即曾经开发过的技术或程序模块，也可以说是研发团队或公司的技术资产。虽然原本这些程序可能运行于不同的CPU，但我们可以取得它的source code与相关技术文件，只要这个技术并非与硬件直接相关，稍作修改（或者根本不需修改）就可以应用在新项目中。这种程序模块已经通过其他项目测试人员的考验，所以质量应该不会有什么大问题。实际上我们倾向沿用既有系统的架构，可能大规模地重复使用其他项目的成果，如图形界面子系统、ROM文件系统、网络通信协议等。

■　Open Source的函数库：近年来开放源码的思想大为流行，几乎什么样的应用都可以找到开放源码的程序，尤其是Linux，它简直就是技术人员的宝库。网络找到的函数库虽然不用花钱，但和公司内部技术资产比起来会有下列缺点。

□　未经专业测试，稳定度与质量不明。

□　Open Source函数库的运行平台通常都是PC，所以程序编写时比较不会考虑到嵌入式系统的限制，因此，使用在嵌入式系统开发项目上时要稍微谨慎一点。

□　程序编写风格良莠不齐，有些程序高手爱用晦涩难懂的程序技巧，偏偏又不写注解，这种程序较难以维护。

■　Third Party厂商开发的函数库：我们是负责产品开发的团队，有些特殊应用的技术网络上找不到，也不是我们做得来的，如手写辨识、语音压缩等，这些技术通常可以当成一家软件公司的主要业务了。如果我们的产品有这种功能需求的话，这部分的技术开发就只能外包，而这些技术几乎可以说是外包公司的命脉，他们不可能提供source code给其他公司。通常与这种公司的合作方式为：

□　我方提供技术规格以及我们平台的Cross-Compiler。

□　外包公司（Third Party)编制技术文件或使用手册，以及经Cross-Compiler编译过的函数库文件或Object Files。

□　由我方将该函数库与我们的系统连接，进行测试并反馈结果，这个步骤可能要经过数个来回。

□　因为我方没有该函数库的source code，如果测试始终有问题，可能必须在Third Party。工程师的计算机上安装我们系统的开发环境，并指导他们如何在我们的系统上调试。”

菜鸟：“我们的系统中有一个压缩/解压缩的函数库——Zlib[8]，应该是从Open Source的函数库移植过来的，我们有它的源码，经过调整后，在我们的系统上似乎也可正常运行。因为我对压缩的东西蛮有兴趣的，曾经试着要看懂它的程序，但困难重重，我一直以为是我资质驽钝，于是尝试去请教负责的员工，他的回答更令我错愕，他说：‘我们的项目不是在开发某某电子产品吗？为什么要花时间看懂压缩算法？’。到底工程师在做Porting前，需不需要先把这些程序看懂呢？如果不看懂，不是很没安全感吗？”

系统老鸟：“通常我们不会选用‘来路不明’的程序，Zlib鼎鼎有名，正确性与性能应该无庸置疑，在移植Zlib期间，我只关心存储器的使用量以及存取文件的接口，从来没有想过要把它的算法看懂。移植完成后，再经过严格的测试，没有问题就应该OK了吧！

从另一个角度来看，很多工程师会把时间卡在看懂每一行程序，其实这是个错误的迷思，请记得工程师的首要任务是什么？看懂这个程序对自己及团队的好处是什么？如果没有好处，倒不如把时间节省下来做些有意义的事情。就像之前我们提过画斜线的算法，如果你在做算法的研究，那确实应该看懂它；否则直接使用它就是了。工程师生涯中要经手的程序何其多，没有那么多时间看懂每一行的。”

菜鸟：“我觉得那是您这种程序高手的讲法，像我们这种菜鸟，多看一些程序应该可以增强写程序的功力吧！”

系统老鸟：“如果你觉得程序功力不够，应该在业余时间，或者在不耽误正事的状况下多多加强，我们公司是开发单位、营利性机构，不是补习班，如果老板的命令是尽快帮我们的系统找一个好用的压缩算法，结果你花了一个多月试图看懂ZLib，对老板来说，他只看到交付的任务没被执行，才不会觉得你是个用功的好孩子。但如果老板的任务是要你改善现有的压缩算法，那又另当别论，懂吗？目标导向（或任务导向）一直都是最实际的管理方式，任务没完成，就算你每天加班熬夜，老板只会觉得你不是失职，就是无能。

再提醒你一件事情，要搞清楚什么是真正的学问或技术。在嵌入式系统或电子产品开发这一行，如果你能针对问题尽快找到解决方案（Solution），将其移植到系统上，并验证其运行无误，使我们产品可以如期上市，那我会觉得你是有技术、有能力的人。如果你以为Porting不需要思想、不需要技术、或者是人人都可以做的，那就大错特错了。想想看，要将原本执行于PC或其他平台的程序移植到另一个平台去，需要检查的事情不多吗？这才是你该去看的程序吧！”

菜鸟：“我懂了！在这个例子中，压缩算法只是因为这个产品开发项目的应用程序有这个需求，它不该是我专业的主轴，我该学的东西应该是嵌入式系统开发，而‘如何找寻现成的解决方案，并Porting到我们的平台上’，才是我该focus或学习的工作内容。”

系统老鸟：“是的，你终于抓到重点了！”

17-03　发行第一个版本

菜鸟：“我们已经开始整合工作了，要到什么程度才可以发行第一个版本？我听说测试人员已经在模拟器上‘偷偷’测试一些基本功能，但我觉得应该还是要在机器上测试比较准吧！”

系统老鸟：“刚刚也谈过，随着整合的程序越来越多，各种奇奇怪怪的问题才会陆续出现，所以原则上应该所有功能都整合上来后，才发行第一个版本并送测会比较好，否则，若功能A整合完毕就送测，测试人员兴高采烈地测试、写Bug Sheet，负责功能A的工程师也兴高采烈地修改，结果当功能B整合上来后，影响了功能A，导致其某些功能不正常，那之前的测试工作岂不是如同白做工？而且这会让其他不懂的人认为RD的质量管控有问题，因为之前OK的换个版本就NG了，如果有这种耳语传出来，对RD士气打击是很大的。”

17-3-1　第一个正式版本之前的“版本”

菜鸟：“所以我们要坚持所有功能都整合完毕，而且由工程师们先大概测试一下，即在功能完整且没有太明显问题的前提下，才能出第一个版本。”

系统老鸟：“你工作也一段时间了，难道还不知道理论和实际总是有差距的？很多事情是没有办法照理论按部就班来的，而且测试人力也不足，如果测试主管是负责任、将产品质量视为己任的人，他确实会设法尽早开始测试工作，即使整合还没完成，他也会希望先测试一些已完成的功能。但此举确实会打乱RD原先的步调，难免会造成一些纷争，如果不好好的处理，RD与测试人员间的摩擦会越来越严重。

处理这种问题的方式需要良好的沟通，我已经和测试主管订好以下协议，PM老大也支持我们这么做，在整合完成、第一个正式版本发行之前，RD和测试人员都可以专注在自己的职责上。

■　在整合工作完成之前，所发行的版本都是内部版本。

■　系统整合人员会尽量配合测试人员的需求发布内部版本，并详细告知系统中各个功能的完成度，也可建议测试项目的优先级。

■　内部版本没有正式编号，如A01-build 05（当然还是需要个识别编号，用日期是个很好的选择）。

■　如果没有必要，为避免增加复杂度，在此阶段只会发行模拟器版本，不会发行实际机器的版本。

■　因为正式版本尚未发行，所以此时测试人员找出的问题点，尽量不要以‘bug’称之，也不需填Bug Sheet（工程师通常都有自尊心强的特性）。

■　测试人员将所有找到的问题点汇集给RD主管，不直接接触工程师，由RD主管分配处理之。

■　在第一个正式版本发行之前，bug管理系统暂不启动。”

菜鸟：“所以，此时RD人员正确的心态应该是：测试人员是在正式版本发行前，先帮忙RD找出各个模块已知的问题。研发工程师应该将测试人员的工作视为协助，而不是找麻烦。通过这样的合作关系，有些问题可以在第一个正式版本发行前就被解决，也可以减少以后被‘正式列管’的bug数，对RD人员有百利而无一害。”

系统老鸟：“是的，如果不这样处理事情，研发工程师总是会情绪化的说‘我又没说这个功能已经做好了，谁叫你们开始测的？这种bug单我不承认！’，此时你就会发现RD主管为难了，跟不跟着工程师一起发脾气都不对，要凝聚团队的向心力不容易，但要让士气瞬间瓦解却是很容易的事。”

菜鸟：“在发行第一个版本之前，除了整合工作以及解决测试人员陆续发现的问题之外，还有什么需要注意的？”

系统老鸟：“版本发行有许多注意事项，以后我们会再提到。从第一个正式版本到最终用于量产的版本，视产品特性与版本发行策略不同，中间经过十几、二十个正式版本是稀松平常的事，第一个正式版本就被用来量产几乎是不可能的，但这并非代表第一个版本就不重要。基本上，第一个正式版本应该要包含最终产品的所有功能[9]，而且是执行于实际机器上，这对嵌入式系统项目来说是一个重要的里程碑。所以我会要求发布版本时务必谨慎，在公布release note前，研发工程师应该把所有基本功能都测过一次，确定符合该版本的既定目标才可以正式发行。”

菜鸟：“让研发工程师测试基本功能，会不会引起工程师们的反感？”

系统老鸟：“这个时候不会，在正式送测之前，如果工程师对即将发行的版本中，自己负责的功能是否可以正常运行都不知道，就将这种版本交给测试人员测试会不会太随便了？所以RD端最好要确认所有功能都能运行才可以发行正式版本，即‘RD端测试’。

所以发行第一个正式版本这件事情还有个重要的意义，就是测试人员可以开始启动Bug控制系统，以后，所有被找出的bug都可以被管控。”

17-3-2　送测：启动BUG控制系统

菜鸟：“Bug控制系统的用途是什么？bug不是交给负责该模块的工程师修改就行了吗？”

系统老鸟：“你这是学生的想法，商业软件开发应该要更严谨地处理所有被找到的bug，简单来说，就是必须‘追踪（Trace）’每一个bug的状态，进而记录每一个bug的历史。当系统被找出的bug多达上百个，甚至上千个时，如果还是用一张张的Bug Sheet来管理绝对会出问题，有时候Bug Sheet就凭空不见了、有时候工程师会片面决定不解决（吃案）、有时候Bug Sheet就直接被情绪化的工程师送进碎纸机或丢进垃圾桶，而且纸张的篇幅有限，根本没有足够的空间记录这个bug的历史。你可以想象一下，在每一个版本发行之前，测试人员必须汇总目前所有bug的状况，包含已解的、未解的、不用改的、致命的（Critical）、解过又发生的（Reopen）等，如果其中丢了几张Bug Sheet，或者记录不清，这个报告恐怕会难产。

有时QA/QC人员会要求一些更深入的数据，例如：这一个月来，测试人员每天找到bug之数量的增长、Reopen bug的频率、近期内Critical Bug出现的频率等，要从一堆Bug Sheet中统计出这些信息几乎是不可能的任务。反之，如果使用Bug控制系统，就可以很容易得到这些信息或数据。”

菜鸟：“我懂了！这就是为什么研发团队需要一个Bug控制系统。它应该是一个类似网站的server，其中记录了每一个bug的属性与细节以及在每一个时间点的状态，并可自动生成您所谓的那些统计表格。所有的项目关系人，包含研发工程师、测试人员、QA/QC、项目主管、客户等，都可以上去看。如果可以的话，当测试人员找到新的bug并登录到server上，server应该可以发出信息给研发主管，当研发主管知道有这个bug后，也可以在server上指定某位工程师check这个bug，而该工程师应该也能收到这个信息。简单地说，这个Bug控制系统除了可以追踪bug外，也是项目团队所有成员（包含客户）对bug处理方式的沟通平台。”

系统老鸟：“以前这种bug追踪软件相当昂贵，这也导致规模较小的software house望而却步，宁愿承受bug管理混乱所造成的风险。现在有个功能完整，但免费且Open Source的bug追踪系统——Bugzilla (http://www.bugzilla.org/)，在2.18版之前只能执行在Linux上[10]，除了广泛使用于Open Source的软件开发项目外（如Linux Kernel、Open Office等大型软件），许多商业软件开发公司也使用Bugzilla来管理bug。

Bugzilla的功能和你说的差不多，使用浏览器即可操作，你可以到Linux Kernel的Bugzilla Server (http://bugzilla.kernel.org/)看看，图17-3所示是到2007年3月19日之前还没解的bug清单。每一个bug都有个负责人（Assigner），点入后可以看到该bug的详细数据与处理的历史。若是已经修改过的bug，也可以看到解决方案。如果你有兴趣的话，也可以注册一个账号并参与讨论，甚至帮忙修改某些bug。”

菜鸟：“Linux Kernel是个开发参与者不知多少，且散布全球各地的软件项目，如果Bugzilla可以胜任Linux Kernel的bug管理任务，像我们这种项目简直就是小菜一碟。”

系统老鸟：“Bugzilla是很有弹性的工具，可以让管理者自订bug种类或bug所属模块，在工程师忙着整合的时候，测试主管或其他熟悉系统规格的人员就该开始设定Bugzilla Server，设计确实符合本项目的bug分类（第一级分类称为Category，第二级分类称为Component），以及每一个bug类别预设的负责人。我们还是以Linux Kernel的Bugzilla Server为例，从图17-4中可以看到bug被分类为多种Category，其中一个叫做Driver的类别中，又可以分为许多子类别(Component)。”

▲ 图17-3　Linux Kernel Bugzilla Server——未解bug清单

▲ 图17-4　Linux Kernel Bugzilla Server——自订系统模块与bug分类

系统老鸟：“虽然Bugzilla是个很容易使用的工具，但无论是测试人员或开发工程师，在进入正式测试流程之前，项目主管都该请有经验的员工帮大家上课，提醒使用Bugzilla的注意事项。测试人员与负责解bug的工程师是以不同的角度去使用Bug控制系统，我只能提醒你RD处理bug的原则，如果你对测试人员该有的注意事项有兴趣，不妨去向测试主管请教。

开发工程师及其主管使用bug管理系统的注意事项如下。”

■　测试人员在填写bug时，会根据其认知将其分类，而每种类别的bug都会有预设的处理者，通常是RD部门的小主管，当某bug的状态有变化时，如新增、已修改（FIXED）、REOPEN、CLOSE等，Bugzilla会主动寄发mail给预设处理者（Assigner）。相关人员必须主动检查Bugzilla寄出来的mail，并登入Bugzilla Server处理之。

■　测试人员会将可重现bug的操作手法详细写在Bugzilla上，预设的处理者应该根据bug的内容判断该bug的分类对不对，如果分类不恰当，应该立即更改之。接着，根据研发团队各成员的工作职责，决定应该由哪位工程师负责，并将这个bug指定给他，被指定的工程师称为Assigner。

■　某bug的Assigner会收到Bugzilla发出的通知信，应该即刻登入确定可否处理该bug，如果这个bug不该是自己负责，则应该立即协调指定另一位工程师负责，而不是任其放着。

■　如果Assigner对该bug有疑问、无法重复发生、认为是规格的模糊地带、或不需要修改等，应该在Bugzilla上陈述理由，测试人员及其他可仲裁的人员可对此bug在在线进行讨论，所有讨论的内容都会被记录在Bugzilla Server中。如果最后确定这不是个bug，或者裁定不需要修改，则bug的状态会被改为INVALID或WONTFIX，再经多次确认后，这种bug就可以被CLOSE。

■　当Assigner“接受”并解决了某个bug之后，应该到Bugzilla Server描述造成该bug的原因、解决方式、Side Effect评估、预计在哪个版本可以对应等信息，留下这些信息最大的受益人应该就是Assigner本人。程序代码那么多，谁能记得什么bug改了什么地方？如果以后有类似的bug，或该bug的修改方式确实造成了Side Effect，只要参考Bugzilla内之前记录的资料，就能够以最快的速度找到程序的问题点。所以Assigner在填写解决方案时要尽可能详细，如果可以的话，最好把改了哪个程序文件的第几行都不分轻重的记录在server上。

■　当某bug被解决后，其状态由ASSIGN或NEW变为FIXED，要经过测试人员的重复验证后才可以被设为CLOSE（结项）。如果测试人员发现该bug未完全被修复，或发生其他的Side Effect，则此bug状态会被设为REOPEN，负责的工程师应该再行检查。

■　研发主管应该时时利用Bugzilla自动产生报告的功能，检验查看还有多少bug未被解决？哪位工程师“囤积”了最多的bug?有哪些bug从被找到至今尚未被解决的时间最长。此举可以找出人力与困难度的瓶颈，研发主管应该即刻处理，如约谈相关工程师并了解其工作方式，如果只是单纯loading太重，可以先调派人力减轻负担，如果是碰到无法解决的关键问题，研发主管也应当在团队内寻求解决方案。

菜鸟：“当RD部门正式发行第一个版本之后，产品开发将进入另一个阶段。之前是RD照着规格逐一实现功能并整合，测试人员则根据同一份规格制定测试计划与Testing Case，当Bug控制系统开始运行之后，所有开发的重点将围绕着一个一个bug展开，测试人员负责issue找到的bug，并详细说明如何操作可使bug重复发生，QC人员评估bug是否需要修正，工程师则逐一修改被发现的bug，并在server上记录bug发生的原因、修改的方法，以及可能的Side Effect；而项目主管则必须时时注意所有bug的状况，以掌握目前系统的状态。”

系统老鸟：“没错，项目后期所有人在谈论的都是bug还剩多少、还有多少Critical的bug未解、bug的消长比率、某某种类的bug还有多少、哪位工程师还剩下最多未CLOSE的bug、第几号bug如何如何、这种bug可不可以不要解、若解了会造成副作用的可能性。基本上，软件的bug是找不完的，一个礼拜或更长时间找不到bug并不代表软件就完全没问题了，QC人员必须有能力判断系统的质量是否已达到出货标准，而不是凭感觉永无止境的测试下去，无论测试人员或工程师都会弹性疲乏的。”

[1]　“Porting”就是移植的意思，在这里表示把原本执行于模拟器平台的程序“移植”到机器上。在产品开发时我们常常会用到这个词，例如把从网络上找到的开放源码、或从厂商（Third Party）处取得的函数库移植到我们的平台上，也可以把这个工作项目称为Porting。

[2]　产品开发牵涉的事务很多，谁都希望可以尽早消除项目中的所有不确定性，但实际上难免会有意外发生，例如某个组件大幅涨价或缺货，或者系统的存储器使用量估计错误，都会导致在项目中后期还要改变硬件规格，越底层的改变对项目整体的影响越大。

[3]　如果忘记答案了，请回到《第12-2节：Stack》复习一下。

[4]　假使也忘了Profiling怎么做，请回到《第14-4节：实战篇》温习。

[5]　假使忘了Memory Layout是什么，请回到《第12-1节：动态存储器空间配置》加以复习。

[6]　一般系统中只有一个CPU，不会有两个程序段可以同时执行。这里说的‘同时’意指concurrent，简单地说，当程序段A在存取资源X时，执行到一半，系统可能会将执行权切换到程序段B，如果程序段B也会使用到资源X，那么当执行权又回到程序段A时，资源X可能已经被改变了，所以造成不一致的状况。在一般嵌入式系统中，上述的程序段B可能是另一个task或临时产生的中断处理程序。

[7]　有些多任务的排程算法是以Timer中断来触发Task Switch(如Round-Robin排程算法)，所以使用禁止中断的方法，通常也能保护task间的Critical Section。

[8]　ZLIB的官方网站为：http://www.zlib.net/

[9]　视项目状况，可能有些特殊的功能迟迟无法完成，或者Third-Party负责开发的功能，当初约定的进度就比较晚，如果此时客户或QA/QC单位急着要评估开发状况，项目主管可能会同意在还未完全整合所有功能的状况下，先发行第一个正式版本。

[10]　有关Windows版的Bugzilla Server,请参考以下网站：http://www.bugzilla.org/docs/win32install.html

Chapter 18

Testing、Debugging与Tuning

时间：06月02日　 登场人物：菜鸟、项目经理（PM）、系统老鸟

PM：“目前开发工作已经慢慢完成，初次的系统整合虽然兵荒马乱，但看来状况还好。接下来的重头戏就是测试工作，因为人手不够，所以要请你帮忙测试某个功能区块，你愿意配合吗？”

菜鸟：“配合是没问题啦！可是我的专长是软件设计，要我做测试好像怪怪的。”

PM：“你会这样想很正常，一般人都会以为是技术不行的人才会被派去做测试，但这是错误的理解！我们可以由两方面来探讨技术这件事情，第一，测试真的是没技术的人在做的吗？第二，测试工作做的好坏对项目的影响程度是什么？”

菜鸟：“我知道测试很重要，就如同微软的开发人员和测试人员的人数比例是一比一，但对您刚问的那两个问题，我的确没什么概念。”

PM：“那你恐怕还不知道，老美对测试工程师的待遇可是不输开发工程师的。让我们先回到两个问题上来。第一，测试工作绝对不是没有技术的人在做的。在软件工程中测试工作有详细的分类，其中白箱测试、压力测试与自动测试等项目都是要写程序的，而且测试工程师必须比开发工程师更努力的详读产品规格书，并根据其经验与直觉，找出任何使用者可能发现的Bug，你能说这是没技术的工作吗？而且根据软件工程实践证明，让对软件思想有深刻理解的工程师进行软件测试，可以大幅提高软件质量。

第二，我觉得测试工作做得好，对项目的贡献度绝对不输开发工作。因为现代软件项目的复杂度太高，特别是嵌入式系统还牵涉到硬件的稳定度等，要开发出没有bug的软件是不可能的事。而且，根据研究机关的研究结果显示，就算是质量要求最严格的军事项目[1]，平均每千行的C语言程序中还是会出现零点几个bug，所以需要测试人员利用他们的项目素养，把这些bug尽量找出来，有些问题不是程序员可以找得到的，你能说测试工程师的任务不重要吗？”

菜鸟：“长官可以举个例子吗？如果让程序员们交互并反复地review程序，应该还是有可能找到您说的那种问题吧！”

PM：“你说的理论上是对的，bug是人创造的，原则上可以从程序中被发现。但现在的软件系统复杂度很高，而且你忘了嵌入式系统里不只是软件而已！举两个例子，都是我们前一个电子字典项目里真实发生的案例。

第一个是有关硬件的，测试人员发现机器偶尔在自动关机时会死机，发生的机会不多，但一天内总会发生个一两次。对我们来说，会死机的bug都算是critical，不可能置之不理，否则，上市后会对客户的商誉及本团队所开发的产品质量造成极大影响。因为进度已经到项目后期，客户限令马上解决这个问题，工程师们熬夜看了几天几夜也找不出头绪来，最后发现是我们用的这颗CPU搭配某厂牌较廉价的SDRAM时Timing出了问题，在关机切断SDRAM的电源之前，必须先改变SDRAM的状态，在某种很特殊的Timing下，就会让SDRAM控制器出问题。”

菜鸟：“听不太懂……”

PM：“没关系！细节不重要，重点是我们的程序已经按照CPU和SDRAM的规范去写了，基本上程序应该不会有问题，所以工程师看了几天也看不出头绪来。换句话说，CPU没有问题，SDRAM也没问题，但实际上就是出了状况，如果不是那位测试人员的耐心，这样的产品肯定让我们有接不完的客诉电话！

另一个例子比较简单。我们公司曾经承包过电子字典的开发项目，它的数据是来自纸版字典出版社所提供的电子版，其中一本字典的电子版数据，就比原本纸版的字典缺了连续的两三百个字，你认为这样的问题可以从程序中找出来吗？虽然这是字典出版社的错误，但如果测试人员没有发现这个错误，那就表示这个测试人员的素养不够，测试不是乱枪打鸟，而是必须要有系统有方向，除非逐字地看，否则，要找到几十万个字中漏掉的一两个字确实很困难，我们的人力并不允许。但连续几百个字漏掉还找不出来，我只能说这位测试人员是在鬼混了。”

菜鸟：“的确很厉害！他们怎么会知道该测哪里啊？”

PM：“这就是测试人员的专业素养，硬件人员觉得写程序的人很厉害，软件工程师觉得硬件工程师很神奇，但我觉得顶尖测试人员的经验和直觉才是我最佩服的！没有好的测试，就不可能会有好的产品。我自己都要偶尔充当测试人员了，你还有什么好啰嗦的！先到Server上把测试计划书download下来研究一下，然后明天找测试主管报到去，他会安排你工作的。”

软件测试其中一个定义是：为了评估而质疑产品的过程。身为项目管理或开发人员，绝对不可轻忽测试的重要性。测试的方法论很多，但一般研发团队都有人力的限制，很可能无法按照这些规范来做，实际上，人少有人少的做法，但无论如何，测试工作都必须是有系统、有效率地根据既定的测试计划，按部就班地执行。绝不是把测试人员找来，每人发一台机器，任其随便操作就叫做测试。

18-01　测试

菜鸟：“PM老大让我来帮忙测试工作，但我从来没做过类似的工作，可能要麻烦您指导一下，首先我该从哪里开始？”

测试老鸟：“在开始之前我先问你一个问题，你的技术背景应该是程序设计吧！你可以解释一下何谓‘测试’吗？”

菜鸟：“以前读软件工程时有学过，但老师上课时都把重心放在设计与开发的流程上，对测试好像讲解不多。我大概知道测试工作就是尽量找出软件中的问题，但对测试工作的理论与实际上的注意事项，我几乎一点概念都没有。”

测试老鸟：“我想信息本科生在学校时，应该不会有人立志要当个测试工程师吧！这也难怪老师大多轻描淡写带过测试这一块；另一个原因是老师通常也没有测试工作的实际经验，要他讲测试是有点难为他了。既然你不知道测试的定义是什么，怎么能肯定你完全没做过类似的工作呢？当你实现了某个软件模块，在提供给其他人使用前，你应该会先写一些额外的测试程序，验证模块内各个功能运行无误，这也是测试工作的范围之一，我们称为单元测试及白箱测试。或者当应用程序工程师完成某个应用程序之后，难道他自己不会先操作一下确认没有大问题才正式发布吗？因此，负责实作的程序员其实无形中都在做一些测试工作。”

菜鸟：“那么软件测试的定义与工作范围到底是什么？”

18-1-1　测试vs.质量系统

测试老鸟：“我节录几段维基百科中对软件测试的定义，虽然有点绕口，但我觉得这些定义确实有把软件测试的精神表达出来。”

■　软件测试的经典定义是：在规定的条件下对程序进行操作，以发现错误，对软件质量进行评估的过程。

■　软件测试描述一种用来促进鉴定软件的正确性、完整性、安全性，和质量的过程。据此，你可能会想，软件测试永远不可能完整确立任意计算机软件的正确性。然而，在可计算理论（计算机科学的一个支派）一个简单的数学证明推断出下列结果：不可能完全解决所谓“死机”（指任意计算机程序是否会进入无限循环、或者罢工，并产生输出）问题。换句话说，软件测试是一种实际输出与预期输出间的稽核或者比较过程。

■　测试的其中一个定义：为了评估而质疑产品的过程。这里的“质疑”是测试员试着对产品做的事，而产品应以testing case或产品规格上的定义作为回应。虽然大部分测试的过程不外乎回顾、检查，然而，“测试”这个词意味着产品动态分析——让产品流畅运行。程序的质量标准通常会随系统不同而有差异，不过，某些公认特性是共通的：可靠性、稳定性、轻便性、易于维护，以及实用性。

■　软件测试一度被认为是编程能力偏低的员工的工作，直到今天，仍然有许多公司把优秀的人才放在程序编写上，也有更多公司让优秀的人才进行设计，却很少有公司让优秀的人才进行测试工作。实际的软件工程实践证明，让对软件思想有深刻理解的工程师进行软件测试，可大幅度的提高软件质量。

■　软件测试和其他工程中的测试有很大的不同。人们可以用牙齿咬黄金来测试其硬度、“真金不怕火炼”测试它的熔点，用化学方法测试它的纯度，可是我们缺乏完善的指标来描述软件产品，比如通过率等。在一千个软件拷贝中，如果一个发现了错误，实际上就是这些拷贝都有错误。更重要的是，其他工业产品都有“通过检测”这个概念，可是软件产品测试的目的现在仍然是：尽可能多的找到软件中的错误，而并非证明软件的正确。

菜鸟：“所以，测试工作和质量系统并不是同一件事啰？我之前好像都把这两件事情混在一起。”

测试老鸟：“在制造业挂帅的时代，质量的思想是‘质量是测试出来的’。简单地说，就是在生产线上尽可能地挑出品质不良的货品，并据此改善产能。在那个领域里，测试和质量的确可以等同视之，但随着产品功能越来越复杂，最新的理解是‘质量是设计出来的’，即在设计阶段就必须考虑到质量，有很多方法论是用来保证设计的质量，无非就是严谨的设计审查，所有的设计活动都必须留下记录等。

我并非质量的专家，只知道测试工作是质量系统中的一环。软件评估的国际基准ISO 9126提供了传统的质量模型，它对质量的以下几个方面进行了规定。

■　功能性：软件应用的功能性需满足所设定的需求，软件的实现必须符合相关基准。

■　可用性：使用者能轻松学会并使用该应用，应用程序允许使用者能以协调高效的方式完成自己的任务。优质可用的应用也是简易的，对使用者隐藏了其复杂科技和实现。

■　效率和性能：应用能在目标环境中高效运行，使用合理数量的系统资源，并在适当的时间内完成这些任务。

■　可移植性：应用可以从一个环境或目标终端，以有效的方式被移植到另一个环境或终端。

■　可维护性：如有必要，应能对应用作修改，如软件功能更新和修正patch等。

上述对质量的基本要求中，完全没看到‘测试’工作的踪影。当然测试工作做得好，确实能提升产品的质量，但如果产品的硬件设计、系统设计或者程序员的能力出了问题，造成产品天生体质不良，此时就算投入再多的测试人力也只是头痛医头、脚痛医脚，根本就于事无补，还可能因为程序的修改，而造成更大的Side Effect。”

菜鸟：“我以前听PM老大说过设计质量的重要性，他说他最害怕的就是到项目后期才发现系统架构有问题，此时因为怕系统大修会延误进度，只能让测试人员尽量测量，RD则看到一个bug改一个bug。而bug修改也是蛮奇怪的事，因为软件bug当然是由程序造成的，修改bug势必要增修程序，当然也就会有制造新bug的风险！最后，系统就像病人全身贴满了膏药，但真正的病灶却在体内，简单地说就是质量失控，即便侥幸能够出货，主其事的项目经理恐怕也难以睡得安稳。”

18-1-2　“嵌入式系统”测试：Cross-Test

菜鸟：“我已经知道软件测试的定义与精神了，嵌入式系统的测试工作有什么特殊之处吗？”

测试老鸟：“由于嵌入式系统的特性，如CPU种类繁多、开发工具（如ICE）昂贵、存储器size小及其他硬件的限制等，嵌入式软件的测试当然与一般商用软件的测试策略有很大的不同，可以说嵌入式系统是最难测试的一种软件，所以在拟定测试计划时，一定要慎重考虑测试的策略，即测试工作应该从哪一个开发阶段开始、哪些测试工作应该在PC上执行、哪些测试工作一定要在机器上执行等。”

菜鸟：“为什么不将所有的测试工作都在机器上执行呢？”

测试老鸟：“如果可以的话，我当然会规划所有人都在真实的机器上测试，但在执行上通常会碰到资源短缺的问题。因为在开发期间不一定真的有完整的‘机器’可以投入开发或测试工作，我们有的可能只是一块板子（Target Board），要接电源供应器，用ICE、烧录器或ROM模拟器下载程序。你应该知道这些设备都花费很大，而且使用上有些难度，一般公司怎么可能为了一个项目花大钱并为每一位测试人员都购买一套设备？更不用提要培训所有测试人员完全会使用这些设备所要花的时间和精神了。

测试人员不必都是技术人员，当他们看到要测的东西不是一台插电或装电池就可以使用的电子产品，而是如图18-1所示的庞然大物和一堆接线时，不引起恐慌才怪。更令人担心的是，因为对ICE或其他调试、下载设备的操作方法错误而导致系统死机的状况，测试人员极可能误判为系统的问题。当这种bug被发布出来，势必会在RD与测试人员之间造成纷争。”

▲ 图18-1　测试环境（InTarget Board）

▲ 图18-2　测试环境（In Real Machine）

菜鸟：“所以嵌入式系统测试最大的特点就是，测试人员可能会在不是最终产品的平台上测试，这些平台包含：

■　模拟器。

■　Target Board。

■　机器半成品（可能是某次试产的机器，其硬件电路不是最终的设计，也可能会有不稳定的问题）。

■　机器成品（如图18-2所示）。

为了应付这样的现实，在项目初期就应该要根据规格以及其他部门的开发进度，拟定你刚刚说的测试‘策略’，对吧？”

测试老鸟：“是的！有人把这样的测试策略称为Cross-Test，在模拟器上执行多数的测试，只有最后的系统测试与整合测试是在实际机器上做的，这样可避免发生实际机器不足的瓶颈，也可以减少购买昂贵资源，如ICE的费用。此外，若实际机器的进度始终在delay，那么最后的确认测试可以推迟直到实际机器可用，这为嵌入式软件的开发测试提供了弹性。换句话说，软件架构的可移植性是成功进行Cross-Test的先决条件，这也说明了模拟器对嵌式入系统开发的重要性。

除此之外，嵌入式系统的测试工作还包含一些硬件相关的测试，例如，电气特性测试、环境测试、稳定度测试、强度测试等，这些测试项目通常不是由软件测试team负责，有兴趣的话，以后我再向你介绍。”

18-1-3　测试计划

菜鸟：“我们在设计阶段会编制一份测试计划书，它的内容是否就是刚刚说的‘测试策略’？”

测试老鸟：“没错，但测试计划书内，除了描述测试策略外，还包含了测试工作的人力分配与执行细节。总之，其内容就是在什么时间、用多少人、在什么平台上、花多少时间、测试哪些功能。说起来很简单，但在项目初期未知的事情这么多，而且谁能保证所有的开发工作都会按照预定进度进行？谁能保证项目的范围（Scope）与产品规格不会改变？所以测试计划书必须是与时俱进的，要根据项目现状随时update。

制定测试工作计划有一个重点：启动某项测试工作的条件，不应该是个绝对日期，应该是达成项目的某个里程碑。例如：确定系统功能全部开发完成，才能开始在模拟器上测试应用程序；确定硬件电路已经不会有影响性的修改后，才开始在实际机器上测试性能等。原理相当简单，就是把可能的变量降到最低，否则测了也是白测。”

菜鸟：“这个道理我了解，如果系统功能还没全部完成就开始测试应用程序是没有意义的，应用程序与系统功能的整合程度，以及系统功能的正确性都是很大的变数，此时测了没有问题不代表以后就不会有问题，等系统功能全部整合上来后，还是得再测一次。

对测试计划书的内容我还有个问题，在拟定测试计划时，有哪些事项是该注意的？”

测试老鸟：“要考虑的事情真的很多。首先，必须研究产品规格与相关设文件，否则如何知道系统分为哪些模块？有哪些功能？如果这些都不知道，那怎么可能知道要测什么？更不要说怎么测了。接着，要详读整个项目的schedule，除了安排投入测试人力的时间点外，还要注意开发工作完成的前后关系，例如，取得足够数量实际机器的时间点是什么？如果硬件设计的困难度低，可能第一次或第二次试产机器的稳定度，就足以分配给测试人员做测试，也有可能硬件设计与制作的进度拖很长，导致要到项目后期才可以让测试人员在实际机器上测试，那么，之前我们只能安排测试人员在模拟器上测试。

一般来说，要考虑的事项汇总如下。

■　有多少开发人员可以帮忙执行要写程序的测试工作（如单元测试、白箱测试与压力测试等）。

■　产品的规格范围，需要多少测试人力参与？

■　预估测试工作开始的时间点，以及在多长时间内必须尽可能做完所有的测试工作。

■　硬件设计预计何时稳定？预计何时可取得足够数量的机器。

■　模拟器功能的完成日期是何时？实际机器与模拟器共享程序的范围是多少？（用以评估哪些功能在模拟器上测试才是有意义的）。

■　团队中诸如电源供应器、ICE等设备的数量，研发人员的需求量是多少？（这些设备总是会优先配置给研发人员）。

■　如果要用ICE或ROM模拟器等工具将程序下载到机器才能测试的话，每次下载的时间是何时？相关软件的操作复杂度高不高？

■　组织中可以帮忙设计testing case的人力有多少？

RD在开发程序时，测试team也没闲着，在完成测试计划书后，就要开始根据产品与设计规格开始定义测试范围，并开始设计testing case。”

菜鸟：“总之，测试工作小能乱枪打鸟，一定要有计划、有范围、按部就班地照着测试计划执行。这个道理我已经听你们说过好多次，我都会背了。”

18-1-4　测试的种类

菜鸟：“刚刚您不经意就说了好几种测试的名称，像单元测试、白箱测试、整合测试、压力测试等，这些测试我大概都知道它们的意义，但实际应用在电子产品开发项目时，它们的工作内容到底是什么呢？”

测试老鸟：“测试的名词一大堆，每一个都有其理论上的意义与适用的状况。很多测试方法论教我们该先做什么测试、再做什么测试、最后再做什么测试，就可以保证产品的质量。这些理论都是对的，但实际上我们必须视项目的复杂度、进度以及人力做调整。举例来说，我们都知道白箱测试可以增进程序模块的可靠度，但实际上工程师总是哀嚎着：‘没空啦！Implement的时间都不够了，哪里有空做白箱测试？请别人照着这个模块的.h文件来做就好了啦！’。所以常常就不了了之，或工程师干脆阳奉阴违，随便做做。当然我不是说不了了之是对的，但这的确是项目管理的一个死角。

无论如何，我还是稍微解释一下这些不同的测试工作吧！

■　白箱测试或单元测试：白箱测试通常也称为单元测试、结构测试、逻辑驱动测试或基于程序本身的测试。就是将一个软件系统分成不同的单元（或称模块）逐一测试，因为这种测试有时需要利用debug工具一步一步的运行来进行调试，需要了解每一个模块的内部程序代码的构造、算法等信息，故称为白箱测试，这是从程序设计者的角度对程序进行的测试。

在嵌入式系统中，要通过ICE或其他装置来trace机器上的程序总是比较不方便，如果可以的话，除了驱动程序或少数系统模块之外，尽量安排所有模块的单元测试能够在模拟器上进行。在模拟器平台上执行白箱测试的速度当然会比在实际机器上快得多，当在模拟器完成测试，最好可以在实际机器或板子上重复做一次简单的‘确认测试’（以下会解释何谓确认测试），验证所有在模拟器上所做过白箱测试的模块在实际机器上也都是OK的。在实际机器上进行确认测试，可以找出一些实际机器与模拟器平台上所未预料到的不同点，例如，Cross-Compiler可能有bug，导致某些功能在模拟器上OK，但到了机器上变成NG。

■　黑箱测试：黑箱测试是一种概念，测试者不需要了解程序的内部情况，通过比较系统的预期输出与实际输出来测试系统功能是否正常，以下的各种测试方法（基本功能测试、整合测试、系统测试等）都算是黑箱测试。这种测试不需要了解软件的内部构造，是从使用者的角度对程序进行的测试，只要知道程序的输入、输出和系统的功能即可。

■　基本功能测试：按照测试软件的各个功能划分，进行有条理的测试，必须保证测试范围能覆盖所有功能和各种功能条件组合。

■　整合测试：所谓的整合测试乃相对于基本功能测试而言，主要在确认当多个基本功能整合在一起并交互操作后有没有问题。此外，整合测试也必须检查出一些环境上的问题，比如存储器配置上的一些错误。值得注意的是，小范围的整合测试可以在模拟器环境上完成，此举可以加速测试的进度，然而，最后还是必须在实际机器上重复测试过。

■　系统测试和确认测试：这两种测试必须要在实际机器上做，毕竟嵌入式系统项目的产出应该是一台应用于特定领域的机器，而不是PC程序，当然要在实际机器上测试所有预计的功能是否都可正确运行。系统测试旨在检查整个系统的功能是否完全合乎产品规格的需求，此处系统应该采用更广义的定义，即包含外观、结构、硬件、软件等，而这些测试当然必须在实际机器上执行。

在发行重要版本之前，则必须在机器上做‘确认测试’，检查这个版本各部分的功能是否都能根据规格正确运行。所谓的规格除了功能的正确性外，系统性能也是检查的重点项目之一。在最终的确认测试中，性能不良和功能错误的严重性相同，同样会导致版本发行失败，甚至影响最终的量产进度。

■　压力测试或极限值测试：压力测试是确认系统在负载极大时，是否还可以正常运行，执行压力测试有两个层次，一个是以使用者的角度，例如对键盘频繁且不规律的操作、同时执行许多应用程序，或使用仪器设备制造无用的网络封包或拥塞的网络环境等。另一个层次则是以系统架构或程序模块的角度来做压力测试，即由工程师写程序来测试系统模块，例如，反复不断的读写存储器，以测试其稳定度；模拟驱动程序持续不断的送出硬件事件，以测试上层模块是否可以应付此种状况、反复的allocate/free动态存储器、反复的读写文件等。压力测试比较简便，如果系统在高负载的状况下也可以正常运行，那么，应付一般使用者的‘正常’操作就应该不会有问题。

极限值测试的方式和压力测试差不多，基本的方法是：系统设计时会明定许多限制，例如电梯的最大载重量、LCD的分辨率、电池低电压警示的电压等级、系统中可同时开启的task数等。一般来说，对边界状况的处理是程序最容易出错的部分。同样的，我们可以从使用者的角度或工程师的角度来执行极限值测试，一旦通过极限值测试，可以证明就算遇到了超出极限值的状况，系统会略过或发出警示，至少不会执行错误的操作。举例来说，假设使用者在‘智能型’洗衣机内放置了超重的衣物，如果系统无法发出警示，并任由使用者继续操作，最差的状况就是造成机器或马达的损害，这样的产品绝对逃不过客诉的命运。

■　自动测试：在每次推出新版本或程序经过大幅修改时，测试人员一定要再三确认基本功能正常无误，否则会发生为了修改某个使用者不那么容易碰到的问题，结果造成基本功能不work的现象。如果系统功能复杂，或者要检查的项目繁多，在版本发行频繁的时期，由人工执行这些测试会用掉过多时间及宝贵的测试人力，而且基本功能检查是个颇为烦闷的工作，难免发生人为疏失。较有效率的方法是开发一个自动测试的程序，可以在人工不介入的状况下，把所有的基本测试执行过一次，并编制简单的测试报告。当然不是所有的功能都可以用自动测试来完成，自动测试的结果只能当作参考，但绝对可以减轻测试人员的负担，并尽量避免人为疏失而遗漏了重要的bug。

此外，电子产品最终还是要由工厂生产，生产线需要一个简单的测试程序，以验证某台机器硬件特性是否正确。这个测试程序的使用者并非技术人员或专业的测试人员，而是生产线上的操作员，他们（通常是‘她’们）需要一个工具判断所负责的部分是否正确，例如，存储器的内容是否正确、键盘操作是否正常等。如果OK就继续‘流’到生产线的下一站，假使NG就先将其剔除，分析问题不是他们的工作。所以用于生产线之测试程序的重点是：只测试硬件功能、全自动测试、避免任何错误操作的可能性、测试结果简单明了（就只要显示简单的OK或NG，或红色LED亮表示NG，黄色LED亮表示OK等）[2]。

■　硬件相关测试：嵌入式系统和一般软件项目最大的不同就是要搭配硬件，所以测试工作当然要包含硬件测试。硬件测试自然有其一套流程，软件工程师无须了解太多细节。当软件工程师忙着写程序时，硬件工程师除了修改电路外，会花很多时间做一些实验，以确保硬件设计可以通过客户以及最终销售所在国家对电子产品之电气特性的规定。这类规定很多，尤其是涉及无线射频的产品[3]，以下仅列出一般电子产品都必须通过的硬件测试。

□　ESD防护：即静电防护。静电可能经由任何路径打入机器，如果不加防护，重则破坏硬件组件，轻则导致硬件电路的电位变化，如果这个电路是连接到某输入PIN，可能会导致程序误动作。基本上，程序无力防堵静电的错误信号，只能在收到硬件输入信号时，多做一些判断，‘尽量’避免误动作（例如：静电打入机器，导致键盘中断产生，如果key ISR稍微延迟些时间后，再去判断一次该输入脚的电位，应该就可以避免大部分噪声所引发的误动作）。实际上，硬件工程师会使用静电枪（如图18-3所示）模拟静电的产生，固件工程师可以让每一个ISR都显示不同的字符串，以协助硬件工程判断静电的路径，并据此修改结构或增加保护电路。

□　电磁兼容性(Electro Magnetic Compatibility；EMC）测试：每一个国家或地区都会规范EMC的标准，电子产品必须达到这个标准才可获准在该地区上市。而EMI（Electro Magnetic Interference；电磁干扰）是指除了所需的信号以外，而出现在电路内的任何电气信号，基本上，所有的电子电路都会发射EMI，同时也受到EMI的干扰，因此，电子装置的设计应该既不受外在EMI干扰源的影响，本身也不应成为EMI的干扰源。所有电子系统或多或少都有些噪声，但只有当噪声影响到系统的正常执行时才会发生问题。基本上，噪声是不可能完全被去除的，但是经适当的接地（Grounding）、屏蔽（Shielding）与滤波（Filtering），则可将其干扰尽量降低。对于一个良好的电路设计，预防胜于发生问题后的电路修改。在电路板的布局开始时做好噪声防治的工作，是构建高可靠度低噪声电子系统的首要工作。

▲ 图18-3　静电枪

EMC测试必须要在特殊的场地实施，较大规模的信息厂通常会自行筹建EMI调试场地，加速产品的EMC设计，以达到外销各国相关的EMC规定。规模较小的厂商，则必须承租EMC测试场地的使用时段。EMC的测试场地除了有EMI的测试仪器外[4]，还必须是个隔离室，一方面隔离室是对外来电子电机干扰加以屏蔽，从而保证室内电磁噪声环境满足要求，另一方面则是对内部如天线等发射源进行屏蔽，而不对外界造成干扰，所以EMC测试场地通常都是在人车较少的郊区。

□　极端环境测试：通常开发工作都是在一般环境下执行，元件会有工作范围，如图18-4和图18-5所示，但最终产品可能是在较严峻的环境下使用，如极低温、高温、高湿、极干燥等。不要以为在亚热带的我们就不会碰到极端的环境，如果远程飞行时将电子产品与行李一起托运，就会有很长一段时间处于低温的环境。产品设计者必须弄清楚目标市场是什么，可能碰到何种极端环境？并保证产品在这些环境下仍然可以正常运行。试产后的成品必须经过长时间的环境测试，测试项目随目标市场与客户规定为准，如5°C、湿度95%、10小时，或者0°C、湿度0%、500小时，有些实验（如前述第二个例子）一做就是20天或1个月，如果测试结果NG的话，真的很令人沮丧，找出问题并修改可能又要一个月了[5]。所幸这些问题应该都与软件无关，通常是湿度过高，造成电路短路，或恶劣环境造成组件被破坏或特性改变。但这些经过长时间测试才NG的硬件人员难免病急乱投医，当软件人员受命check这种问题时，只能说是哭笑不得。

▲ 图18-4　IC的data sheet会注明使用范围

▲ 图18-5　组件的data sheet也会注明使用范围（本例为LCD Panel）

■　强度测试：强度测试通常和结构设计有关，例如，机器从一公尺高摔下不造成任何破坏、75公分摔下来电池盖不能弹开、遭受多少重量的打击后LCD不会破裂、按钮可以承受多少次的操作等。基本上，并没有电子产品强度相关的国家标准，结构工程师会根据其经验、产品特性与客户要求来执行强度测试。”

菜鸟：“有这么多种测试要做，我们的测试人力好像不够吧？”

测试老鸟：“和研发人力一样，测试人力从来没有满员过，所以我们就只能尽量提前开始测试工作，甚至在整合工作还没开始之前，就会要求RD先让我们测试某些已经完成的功能。此举难免引来研发工程师的反弹，甚至反感，但我们也是为产品的质量好啊！再来就是根据产品特性、开发进度与客户需求，调配测试工作的重心，例如有些进度比较赶的项目，我们会略过单元测试或基本功能测试，直接就进入系统测试或整合测试。此外，有时客户会暗示产品销往第三世界国家，许多压力测试与环境测试就免了吧！然而，测试主管必须尽早搞清楚状况，并将其反应在测试计划书中。”

18-1-5　TDD（Test Driven Development）

测试老鸟：“讲到测试，就不能不提一下最近很红的开发方式——TTD(Test Driven Development) ，顾名思义就是用测试来驱动开发，而不是开发后才来进行测试。”

菜鸟：“好像听过这个东西，它也是要求模块化，然后每个模块都必须可以独立测试，但这跟我们一般说的Unit-Test有何不同呢？”

测试老鸟：“Unit-Test还是传统的开发思维，也就是先写好了该模块的code，再去写测试程序。一般研发单位会要求写该模块的工程师，必须自己负责该模块的Unit-Test程序，此举难免有球员兼裁判之嫌，就算由测试工程师来编写测试程序，等到发现问题或某些功能未Implement时，因为该模块的架构已经定型，要做修改可能必须付出极大的代价！

TTD则是反其道而行，要求工程师在测试程序写好之前，不准写该模块的任何一行code。当测试程序根据设计与规格完成后，之后的所有开发工作都是为了让这个测试程序可以pass。换句话说，测试程序就是软件规格在程序开发时的‘化身’。

TTD的中心思想是通过倒转开发流程（先有模块的测试程序，然后才有该模块的Implementation) , ‘逼迫’开发团队必须认真的做需求分析、明确定义模块的规格与接口，否则会连测试程序都无法开工。

除了各个单一模块外，局部的子系统或整个系统也可以采用TTD的开发流程。但前提仍是系统设计必须清晰完整，负责系统整合的工程人员才可以在各个模块未完成前，先行编写子系统与整个系统的测试程序。

此外，虽然TTD刚开始是为诸如Java这种面向对象语言所设计，但只要系统设计已足够模块化，就算是普遍以C当成开发语言的嵌入式系统，也能导入TTD的思想。”

菜鸟：“这个方法听起来确实很酷，但我们团队似乎没打算这么干。而且我也很少听到其他嵌入式系统开发团队，如果是好的思想与流程，为什么大家不用？或者是根本用不起来？”

测试老鸟：“相信我们团队里的其他老鸟们已经跟你灌输了许多开发上的思想，他们有逼着你一定要按照某本书上的流程，步骤1、2、3……这样去做吗？我们都知道，一旦好的思想配上了死的流程，再好的思想也推不动。

TTD最基本的规定之一，就是要先写好测试程序后，才能进行模块的实现。想要做到这一点，研发团队需要极佳的纪律，不然就得采购不便宜的工具。因为电子产品开发周期有越来越短的趋势，要做到模块化设计已经不简单，还要说服客户或老板有耐性，他们将无法在开发阶段前期就看到Prototype。再加上工程师的思维无法马上转过来，这些都造成TTD无法在节奏明快的嵌入式系统开发领域中推展的原因。

但TTD的思想确实值得深思，对产品与系统的质量也会大有裨益。首先，TDD是一种设计与规格确认的活动，它并不像Unit-Test，只是单纯的在做Verification。TDD希望提供一个做法，确保在整个开发过程中，我们是开发真正对的软件，而不单纯只是把软件做对而已。

虽然我们没办法做到TTD提倡的‘Test Before’（测试在开发之前），但我们的开发团队总可以做到以下几点吧。

■　在开始写code之前，更严肃地检验查看规格与所有设计。

■　开发写code后，应不定时code review，确认各模块正在确实按照设计文件进行开发。

■　以测试团队主管的观点来说，我希望开发团队至少要做到‘Test After’（开发后执行Unit-Test) 。开发者有义务做第一关的质量把关，不能期待由测试人员去找出藏在某个模块的错误。”

菜鸟：“其实说穿了，TDD不过就是种软件开发的习惯罢了，但偏偏人的惯性才是最难克服的障碍。即便我们团队现在做不到TDD，但确实也吸收了它的思想——写code前必须严格的管控规格与设计，而各个模块coding完成后，则必须严格执行Unit-Test。”

18-1-6　静态测试概论

菜鸟：“今天系统团队里气氛不太好，好像昨天出的版本，才送测不到一天就被测试团队枪毙了，你知道工程师都爱面子，这样测不到一天就退货，会不会不太好啊？”

PM：“我最不喜欢看到的状况就是工程师老是把测试工作都当成是测试人员的责任。程序写好后，Unit-Test做得不扎实，系统整合完毕后，工程端也不先做些基本测试，就直接丢给测试人员。所以在我的项目中，都会先把游戏规则定好，也就是说清楚退货标准。

昨天送测的这个版本确实太离谱，主要功能有几个根本不会动，这已经不是找不找bug的问题了，显然工程师在整合或版本控制上出了问题。所以昨天测试team主管先来会过我，是我决定退货的，刚刚我还把负责整合的主管叫来骂了一顿！

我并不是要求工程人员去做测试人员的工作，自己写的code本来就要自己做白箱测试，送测前至少静态测试要pass，且每个模块的Unit-Test都要run过无误。”

菜鸟：“前阵子您跟我说过‘静态测试’（请参考《第6-6-5节：嵌入式系统C语言程序编写指南——MISRAC》），这不是用来检查程序是否符合我们Coding Style规范的工具吗？”

PM：“这只是静态测试的其中一个功能而已，在测试人员通过真正机器操作来执行测试工作之前，开发端可以利用静态测试工具对程序代码进行分析，试着在runtime之前就找出并修改潜在的bug。

通过对实际工程的统计，我们知道在不同的阶段发现和更改一个错误的费用是不同的，如表18-1所示。若我们将潜在bug留到测试或维护阶段才来处理，则必须花费更多人力、时间，并承担更大的风险。相反地，若我们能在coding阶段，工程师还记忆犹新时就把bug处理掉，对项目绝对有正面的影响。

表18-1　在不同阶段，发现和修改一个bug所需花费的cost可以相差达16倍

软件开发阶段

Coding阶段

Test/Debug阶段

维护阶段

发现和更改一个bug的cost

1倍

4倍

16倍

所谓静态分析，就是我们不需要真正去执行编译、连接好的可执行文件，而只是通过扫瞄程序对程序的数据流和控制流等信息进行分析，找出系统的缺陷，得出测试报告。为什么要执行静态分析呢？一个软件系统可能实现了所要求的功能，但如果它的内部结构组织的很复杂、很混乱，程序代码的编写也没有规范的话，这时软件系统中往往会隐藏一些不易被察觉的错误。即使这个软件基本满足了客户目前的要求，但到了日后对该产品进行维护或升级工作时，就会发现维护工作相当困难。

对软件进行科学、细致的静态分析，使系统的设计符合模块化、结构化，面向对象的要求，使开发人员编写的程序代码符合规定的Coding Style，就能够避免软件系统中大部分的错误（如不匹配的参数、不适当的循环嵌套和分支嵌套、不允许的递归、未使用过的变量、空指针的引用和可疑的计算等），同时为日后的维护工作节约大量的人力与时间。

其实我一直强调的code review也是一种静态测试的手段，但管理者没办法在项目执行期间review完所有的程序。统计表明一个软件项目中，80%的错误往往是由20%的程序引起的，因此，如何有效的确认和标识出这20%的部分是十分重要的！而某些静态工具可扫描整个系统里的所有程序，并分析所有函数以及变量之间的关系，最后编制系统的质量报告，而管理者可优先关注报告中质量分数较低的模块。

此外，这些静态工具（如PC-Lint、LogiScope等）使用起来必须如同使用compiler般简易，则推广起来阻力会比较小。例如图18-6所示就是LogiScope的执行画面，看起来就如同一般的集成开发环境。”

▲ 图18-6　使用静态测试工具应该和在IDE上编译程序一样容易

菜鸟：“所以，执行静态测试的目的并不是要取代真正的测试流程。对工程师来说，静态测试工具试图找出所有可能发生问题的程序区段，就像compiler除了找出error的程序段外，还会找出‘值得警告’(Warning）的地方。对管理者来说，静态测试工具试图找出系统里的弱点，则我们可以及早对相关程序执行code review。

这个静态测试工具被你说得很神奇，它除了可以检查Coding Style（如MISRA）外，我一时还想象不出来它到底可以找出哪些程序里的bug?”

PM: “我们来看下面两个简单的例子，如图18-7所示，你能看出哪里有错吗？”

菜鸟：“第一个应该是switch里的case没有break吧，第二个例子很怪，第一层的for循环只会执行一次就被break出来，不知道原始程序的原意是什么。”

▲ 图18-7　静态测试工具可以检测出来的bug

PM: “工程师难免会写出像这种有明显错误，但compiler却无法发现问题的程序。当compile通过后，就要等到执行时期才有机会发现这个错误，如果运气不佳，也许这些bug会等出货到使用者手上才会被发现。

第一个例子违反的规则是：‘Each case in a switch statement shall be terminated by a break statement’；第二个例子违反的规则是：‘Each logical block shall be delimited by braces, even if it contains no or one statement.’。所以第二个例子只是因为工程师偷懒或太过于自信，并未在每个循环头尾都加大括号，所以正确的程序应该如程序代码18-1所示。

程序代码18-1

实际上，这种非逻辑的错误占系统所有bug很大的比例（另外，非逻辑bug的大部分是‘误解规格’) ，有时通过人眼并不容易看出来，但静态测试工具可以很轻易的帮我们找出来。在实际使用中，这种Rule Checking Tool能快速找到30%〜70%的程序缺陷，更重要的是，Rule Checking看到的是问题程序本身，而非执行时期的征兆，后者还需要工程师花时间去找出问题程序所在，当系统整合了所有的程序，要从‘征兆’找到问题的root-cause确实是件花时费力的工作。”

菜鸟：“关于静态测试中RuleChecking的功能，我已经了解了。但您不是说静态测试工具还可以为系统的质量打分数，并找出系统中可能的弱点吗？”

PM：“这个功能我们称为‘静态结构分析’、‘程序代码质量度量’，或直接称之为‘Audit’。Audit Tool的主要用途是质量监控与程序代码分析，将被评价的软件系统与指定的‘质量模型’进行比较，并用图形或报表的方式显示软件质量的级别。

所以工程人员可以把精力集中到需要修改的程序代码部分，QA人员则可对度量元素和质量模型不一致的地方作出解释，并提出纠正的方法。”

菜鸟：“质量模型是什么东西？好像颇为复杂啊……”

PM: “质量模型是一个阶层式的概念，它的组成与度量标准如图18-8所示。基本上工程师并不需要懂这些，我们只要相信工具所产生的报告，得到诸如‘模块A与模块B的耦合度太高’、‘模块C的可维护性不好’、‘模块D的稳定度值得担忧’等结论即可。当然这些结论不代表最终的结果，但至少给质量人员一个追踪的方向。”

▲ 图18-8　质量模型的组成与度量标准

18-02　Bug Sheet管理

测试老鸟：“当RD出了第一个可在实际机器上运行的正式版本，测试工作即正式展开。通常一开始会是bug的爆炸期，Bug Sheet如雪片般飞来（这是比喻的说法，实际上的Bug Sheet是由bug server通过mail寄到相关人等的信箱）。我们必须有个机制与准则来管理这些bug单，如果就是一张张的纸本或一个个的Word文件，不但RD工程师会不胜其扰，我们也很难管理所有的bug。”

菜鸟：“这个我知道，之前系统老鸟曾经和我提了一下bug管理系统[6]，我们用的是free的Bugzilla吧！最近我在其他项目的Bugzilla server看过，我觉得如果所有项目成员都能知道bug管理的重要性，并打从心里愿意配合的话，对项目的质量一定大有裨益。对管理者而言，很容易就能够知道目前调试工作的瓶颈所在，也能追踪系统bug收敛的趋势。对工程师来说，Bugzilla则是记录bug和程序修改方式之间对应关系的好工具。此外，Bugzilla还应该是项目成员以bug为中心的在线讨论平台，某个bug当初为什么决定这样处理的来龙去脉应该都清清楚楚地被记录下来，这样可以减少许多事后的争议。”

测试老鸟：“你知道的事情还真不少，虽然Bugzilla的功能强大，就怕上至管理者，下至测试人员与工程师都在虚应故事，只把Bugzilla当作存储Bug Sheet的server。测试人员把bug单上传到server之后就不管了、工程师懒得写解决方案、项目成员对如何处理bug没有互动也没留下记录。那么，Bugzilla所带来的好处就没这么大了。所以在项目开始不久，PM老大已经找了我们几个主管协调，勒令各部门主管应教育项目成员正确使用Bugzilla的重要性，并从测试工作正式开始之后，要天天去看bug的状况，如果有工程师或测试人员敷衍了事，应该即刻加以纠正。此外，Bugzilla可以产生多种统计报表，图18-9、图18-10和图18-11所示是几个例子。如果你是管理者，从图18-9中你会得到什么信息？”

▲ 图18-9　Bugzilla产生的图形报表（LINE)

▲ 图18-10　Bugzilla产生的图形报表（PIE)

菜鸟：“在版本Beta 23. 04之前，显然bug已经逐渐收敛，被找到的bug数都在5个以下。但到了Beta 24. 02时，bug数却突然暴增到将近20个，这个现象很不寻常，我想，如果我是老板应该会很紧张，原本以为系统已经渐渐稳定了，没想到突然冒出这么多问题来。应该要找相关人员来开会，弄清楚到底这些bug是什么种类、严重度如何、是一直存在没被找到，还是因为程序修改后的副作用……”

测试老鸟：“没错！这就是Bugzilla好用的地方，只要定时做出报表，就算老板再忙，看到图也知道要去问一下状况，如果出了什么问题也可以及早解决。

话说回来，你之前在项目中都是做开发工作，在RD那里有人告诉过你，研发工程师‘对待，bug的正确心态吗？”

菜鸟：“既然被发现bug，改了不就行了吗？”

测试老鸟：“如果是这样就天下太平了。当工程师忙到心浮气躁时，有时看到Bug Sheet会不理性地认为是测试人员在找他麻烦，甚至认为有人在质疑或指责他写程序的质量或能力。如果测试人员的语气或态度稍微强势一点，或者测试人员没弄懂规格，把系统正确的行为当作bug，则RD与测试人员的争执与心结由此而起，如果不善加疏导，最后难免成为意气之争。

所以RD‘对待’bug的正确心态并非所有bug照单全收，研发工程师可以等心平气和再来处理。既然有Bugzilla这个讨论平台，RD与测试人员不用正面交锋，双方可各自陈述对此bug的看法，主管再适时介入仲裁即可。要知道只靠RD并无法把产品做到完美，测试人员在项目后期越显重要，两种工作不可偏废。如果团队中RD与测试人员水火不容，将会对项目质量造成致命性的伤害。

你接下来的工作是支持测试，在开始测试工作之前，我必须先提醒你测试人员发现bug时的处理流程。”

▲ 图18-11　Bugzilla产生的文字报表

18-2-1　填写Bug Sheet的注意事项

测试老鸟：“当测试人员找到bug时，传统的做法就是填写一张如图18-12所示的bug表单。这个bug表单是经过设计的，可以记录相当多的信息，上半部分由测试人员填写，包含基本描述、软件版本、测试人员、bug等级、发生频率，以及各个版本的检查结果。表单的下半部分则由负责解bug的工程师填写，包含bug发生的原因与解决方案等。这种表单虽然包含了足够的栏位，但缺点是篇幅太小，如果bug的操作方式或解决方案太复杂的话，根本就写不下，此时，测试人员或工程师可能写得不清不楚或干脆乱写。此外，无论将其打印成纸本或规定直接用电子版传递，都无法成为测试人员与工程师沟通的良好工具。”

▲ 图18-12　bug表单的填写范例

测试老鸟：“使用Bugzilla就没有这些顾虑了。图18-13所示是Linux Kernel Bugzilla Server中的某个bug记录，这个页面包含了这个bug的来龙去脉，如果有用文字较难描述的地方，任何关系人都可以插入图像文件、Word文件、文本文件等附件。你不要管这个bug的技术内容，先看一下这个页面包含了哪些栏位与信息。”

菜鸟：“当测试人员找到bug后，第一个动作就是到Bugzilla中新增一个bug，尽可能详细地描述如何操作以重现这个bug。如果用言语不足以描述这个bug的‘精妙之处’，则可以用图、照片或录音等附加文件，只要可以帮助工程师尽快找到问题所在的信息，都可以加入Bugzilla Server中。”

▲ 图18-13　Bugzilla内每一个bug可以记录的信息

测试老鸟：“没错！测试人员的责任就是协助工程师尽快解决问题，有经验的测试人员在找bug时，都会注意以下原则。

■　每个bug都应该属于某一个分类，而每一个分类的bug则有预设的处理者。为了避免bug送错人而延误处理时间，测试人员如果无法区分bug的类别，在issue这个bug之前，最好找研发主管或相关工程师讨论一下比较好。

■　尽量找出最简单就可以使bug重现的步骤。我见过那种要30多个步骤才能按出来的Bug Sheet[7]，这种Bug Sheet势必会在工程师间引起骚动，你要他们怎么解决这种问题？最后发现只要其中的两个步骤就会导致这个bug发生，我们不能说该测试人员不认真，但这样的Bug Sheet就是不专业的表现，好的测试人员会考虑到RD的需求，多花一点时间找出更精简的步骤。

■　好的测试人员应该可以辨别哪些现象应该属于同一个bug。类似的bug写一张Bug Sheet即可，不能心怀‘做业绩’的心态。举个极端的例子，系统中显示英文字母‘A’的功能出了问题，假设应用程序的功能是显示一篇英文文章，如果把每一个‘A’秀不出来的地方都当作一个bug，那这个版本的bug数势必大增，老板看到报表恐怕会发疯。

■　当发现了某个bug，测试工程师有义务根据规格去check其他可能发生同样问题的地方，提醒工程师一次就把所有类似的问题通通解掉。

■　测试人员不应单凭个人感觉判断某个现象是否合理，一切应以产品规格为依托，即测试人员必须详读产品规格。工程师最反感的事情，就是特意根据规格做出来的功能居然被说成是bug。

■　千万不要在Bug Sheet中妄加技术性的评论或情绪化的语言，例如：‘应该是某某人某某区块的问题’、‘请尽快解决！’、‘为什么同样问题一再发生？’等，都是写Bug Sheet时的大忌。

■　某功能不符合产品规格当然是bug，但测试人员最好能写明参考哪本规格书的什么章节。

■　性能不佳也可列为bug，但有时性能好不好是主观的感觉，这种问题最好可以事先找客户、制定规格的人员以及工程师讨论。

■　并不是所有bug的处理者都是工程师。例如，工程师根据规格中的定义实现了某个功能，但测试人员或end user以使用者的角度都觉得这个功能不易使用。此时测试人员可以把这个问题加入Bugzilla，并将其指定给客户或规格制定者，而非工程师。”

菜鸟：“听起来似乎都是为了迁就工程师易怒的个性，我觉得我们团队中的工程师人都很nice啊！不像是会有不理性的行为。”

测试老鸟：“等哪一天你手上有N张‘属于’你个人的Bug Sheet时，就知道那种压力了。一种压力是来自实际的工作量，谁也不想被当作所谓的‘瓶颈’，另一种压力则是来自心理，bug通常代表着程序的错误，自己写的程序被找出许多bug，技术人员的心里总是不好过。这种压力会持续累积，越到项目后期冲突爆发的可能性就越大。倒不是所有人都应该迁就工程师，而是工程师的工作性质确实很容易造成情绪的积累，同团队内的人员应该要互相体谅，大家的目标不都是让产品顺利完成、项目顺利结项吗？

我最讨厌有些测试工程师或QA、QC人员总自认是RD的‘上级单位’，凡事指指点点。而偏偏RD通常自尊心极强，再加上工作量大，往往情绪一发不可收拾，最后还不是要我们这些主管去收尾，这样对项目一点好处都没有。”

18-2-2　非RD人员使用bug管理系统的注意事项

菜鸟：“当测试人员把Bug Sheet填上Bugzilla后，接下来还需要做哪些追踪？”

测试老鸟：“你已经知道工程师接到新bug时该做的事情了吧，并不是所有的bug都要解，工程师对该bug有任何问题都可以在通过Bugzilla提出，相关人员最好即刻予以回应[8]，可能发生讨论的事项为：

■　规格的争议：测试人员与研发工程师对同一功能的规格认知不同，制定规格的人员或客户应立即介入仲裁。

■　无须修改的bug：有时测试人员会找出几近吹毛求疵的bug，一般使用者根本不可能做这样的操作。它是个bug，但却是产品卖到市面后几乎不可能发生的bug。如果很容易就可以改好，工程师可以顺手将其解掉。但通常这种bug都不会太容易，此时工程师应该陈述修改这个bug可能的风险与副作用，由QC人员评估是否要修改。

■　举个keyboard驱动程序的实例。一般人工操作不会有反应迟钝或太灵敏的问题，但有个测试人员使用信号发生器，模拟每隔1ms压一次按键，发现驱动程序会有漏掉key的状况，并写了一张Bug Sheet。工程师收到这种Bug Sheet简直啼笑皆非，即便是亚洲第一快手也按不出这样的速度，就算程序在这样的状况下会有问题又如何？既然产品的使用者是一般的地球人，这样的问题能算是bug吗？

■　只出现一次，且无法重现的bug：测试人员做了一系列的操作后发现系统出现异常现象，机器重启后却再也找不到重现异常现象的操作手法，而且以后再也没有人报告发现这个问题。这种现象的争议很大，我们只能说系统中确实存在着某个问题，但若经过足够时间与广度的测试仍然无法重现，RD根本无从找起，它就不该算是一个bug。测试人员最好口头或书面告知研发主管有这个问题就好，因为没有重现这个问题的操作手法，RD仅能通过code review试图找出程序的缺陷，就算找到怀疑的地方，也无法验明确实已经修正了这个问题，因为本来就无法重现，不是吗？若测试人员把这个问题当作bug给issue出来，将徒增所有人的困扰，RD无法进行debug、测试人员无法验证新版本是否已经修改、QC人员只能苦苦追问RD问题何在[9]，无论如何也不敢直接将其CLOSE掉。

■　REOPEN（某个版本已经改好的bug，在之后的版本又被发现）: REOPEN频率是外界对程序质量的重要评量指标之一，如果某类别的bug被REOPEN的次数过多，研发主管应主动调查，是否该功能最近有大幅度的修改，或者该功能程序的版本控制出了问题。

■　规格修正：客户可能随时有新的需求，或想要小幅度修改规格，最简单的方式就是在Bugzilla上新增一个形态为REQUEST的‘bug’。当然这不能算是bug，但在项目后期，如果只是小规模的修改，REQUEST可以采用bug一样的追踪方式，同样有个assigner，相关问题都可以在Bugzilla上讨论并留下记录。唯一要注意的是，制作统计报表时不应该把这个记录算在bug数内。

■　RD认为修改某个bug可能会造成严重的副作用：修改程序可能会带来副作用，而且越到后期风险越大。如果RD对某个bug有此疑虑，只要他不是主要功能，RD可能倾向于不修改。但其他非技术人员没有这种sense，他们觉得只要工程师修改程序时稍微谨慎一点，怎么可能造成副作用？殊不知软件的复杂度远超过其想象。所以RD应该详述修改这个bug可能的风险，再由项目经理会同QC人员与客户，一起决定该如何处置这个bug。”

菜鸟：“我知道上述那些不用修改的bug，其状态会被设为INVALID或WONTFIX(Won’t Fix) 。而已经改好的bug，其状态则会从NEW或ASSIGNED，被更改为FIXED。我的问题是：这些bug何时才可以结项（CLOSE) ?”

测试老鸟：“工程师修改程序后，当然会在自己的计算机上做一个版本，测试无误后，才可以上Bugzilla Server宣称他已经修正了某个bug，并将其状态改为FIXED。但测试人员只能测试正式版本，所以必须等到下一个正式版本才可能反应工程师的修改。而且并不是工程师说‘我改好了！’就真的没问题了，有时候工程师可能误解了Bug Sheet的意义，或者并未完全修正Bug Sheet内陈述的问题，而最常发生的状况，就是工程师忘了把改过的程序上传到版本控制server，即制作版本时并未用到‘最新’的程序，所以测试人员用新版本再测一次，结果无疑仍然是NG。

测试人员在新版本上重复测试Bug Sheet上的问题，如果确定已经OK，就可以将该bug结项（CLOSE），否则应该将测试结果写上Bugzilla，并将其状态设为重开（REOPEN）。此外，在重要版本发行之前，我通常会安排测试人员把之前累积已CLOSE的Bug Sheet全部重复检查一次，这样至少可以保证系统的基本功能都是正确的。如果发现有些已CLOSE的bug在新版本又重现了，应将其状态改为REOPEN。”

18-2-3　Bug的等级

测试老鸟：“Bug除了要根据所属功能或模块分类之外，应该还要制定严重程度与优先修改等级。这是bug统计报告里的重要信息，质量系统也可用较严重bug的数目增长，当作系统是否渐渐趋于稳定的指标。一般我们把严重等级分为以下7级，也可以根据系统的特性增删等级。

■　第1级（Blocker) ：引起OS、驱动程序或其他系统等级的程序‘死机’(Hang-up）或‘崩溃’（Crash）的错误。

■　第2级（Critical) ：引起应用程序本身‘死机’或‘崩溃’的错误。

■　第3级（Major）：不能完成软件规格书中所定义功能的错误。

■　第4级（Normal）：程序所完成的功能与软件规格书中定义不符的错误。

■　第5级（Minor）：显示方面的错误。

■　第6级（Trivial) ：其他‘轻微’的错误。

■　第7级（Enhancement）：增强或者改进。

实际上，当客户试用了半成品后，主动修改规格的机会很大，所以我们会再增加一个‘Request’等级。至于优先修改等级则分为以下5级。

■　P1：尽快（或立刻）修正。例如造成系统死机、系统无法测试、系统无法release。

■　P2：每一个里程碑（或测试周期）结束前必须修正。例如：与规格不合、功能不正常、无法达成某项作业功能、造成数据错误、画面出现错误文字或图案、执行性能越来越差。

■　P3：如果时间允许就修正。如偶尔造成系统死机、有使用上的问题、不清楚或会误导使用、反应时间很慢。

■　P4：低优先级。例如不影响功能的错误显示。

■　P5：在将来的某个版本修正也可以。例如会造成困扰或疑虑，属设计改善建议。

优先修改等级不一定完全等价于严重等级，当有些功能可能会成为系统的瓶颈，或客户要求尽快完成的功能，相关属性的bug都可能成为优先修改等级较高的bug。”

菜鸟：“这个等级是由测试人员决定的吗？”

测试老鸟：“就如同bug分类一样，刚开始是由测试人员决定。如果可以的话，当然定义优先级之前最好找相关人员或主管讨论一下，但即便订错了，事后再更正也无妨。总之，不要影响到bug统计报表的正确性就好了。此外，你必须要有一个观念，并非所有的bug都需要修改的，尤其到项目后期，如果为了修改一个正常使用不会发生的bug，却带来了引发常用功能不正常的风险，此时，品管人员就应该做出真正对项目有利的判断，为此bug设定合适的等级，如图18-14所示。”

▲ 图18-14　在项目各阶段修改bug的代价

18-2-4　Bug的生命周期

测试老鸟：“刚刚提了那么多bug处理的原则，最后我们来讨论一下bug的生命周期。所谓的生命周期就是从bug被新增到Bugzilla server开始到最后认可结项，这段期间bug的状态变化。我们可以参考Bugzilla的官方文件，图18-15所示就可以完全说明一个bug的生命周期。”

▲ 图18-15　bug的生命周期

以下列举出各个bug状态的定义。

■　新增（NEW) : Bug创建后的初始状态，尚未确认是否为‘合法’的bug，已寄发给该类别bug的预设处理者与其他关系人，但还没正式指定处理者。

■　已分配（ASSIGNED) ：也可称为ACCEPTED，表示这个bug已确认为合法的bug，已经分配给适当的开发人员处理，而且被分配的工程师也接受此Bug Sheet。

■　已修正，待验证（RESOLVED) ：开发部门对bug进行处理或修改后的状态。RESOLVED的子状态为：

□　FIXED：已找出问题所在，且已经修改。

□　INVALID：开发人员认为这不应该是个bug。

□　WONFIX：这是个不用修改的bug。

□　LATER：开发人员认为Bug在当前版本可以暂不修改，考虑在后续版本中再做修正。

□　REMIND：一般地，开发人员不会去修改这个bug，但当有空时或许会顺手进行修改。

□　DUPLICATE：表示这个bug和某个已经被找到的某个bug是同一个问题。

□　WORKSFORME：是‘Works For Me’的简写，意思是说‘在我的机器上是OK’，表示开发人员无法重复这个bug，所以无法进行修改。

■　重新开启（REOPENED) ：当Assigner号称某个bug已经解决了，若经过测试人员验证却仍然存在，则必须将其状态改为REOPENED。对于LATER状态的bug，如果时机成熟，也可将其状态改为REOPENED状态。

■　已验证（VERIFIED) ：测试部门对修改后的bug进行验证，若确认结果OK，则将其状态设为VERIFIED。

■　未经证实状态（UNCONFIRMED) ：由开发人员自己提交的Bug，是一种初始状态，待测试人员确定后变为New。

■　关闭（CLOSED) : Bug生命周期的结束。对于状态为VERIFIED的Bug，应由Bug的开启者，即测试人员主动关闭，开发人员无权关闭Bug。

18-2-5　后记：白天不懂夜的黑

菜鸟：“我刚刚收到Bugzilla寄给我的mail，通知我被assign了几个bug，最近忙着在Bugzilla上写Bug Sheet，都忘了系统中有个模块是我写的，还有两个driver的测试程序是我负责的。其中还有critical的bug，也有优先级很高的bug。老实说，第一次收到bug单其实还蛮兴奋的，感觉我们是以很专业的方法在开发产品，而且让我这个菜鸟很有参与感。所以我要向您请示一下，是否可以暂停测试几天，让我先把手头的bug解完。”

测试老鸟：“你在这个项目中做过的事情还真不少，居然连driver都碰过，就差没被叫去设计硬件了！测试工作刚开始，现在正是bug的大爆发期，我甚至要求测试人员先放下一些不重要的bug，好让工程师先把目前的诸多bug消化一下再说。OK！你先去帮忙把bug解一解吧！”

菜鸟：“下次碰到PM老大，我一定要建议他让工程师与测试人员多多交流，如果能够多了解对方的工作类型，相信可以减少许多纷争。因为我做过测试工作，以后应该就不会对着Bug Sheet发脾气了。毕竟测试人员不见得一定有软件技术背景，就算填了一张不算bug的Bug Sheet，RD可以陈述理由，并将其设为WONFIX或INVAILD不就好了？一旦某位测试人员的Bug Sheet老是被‘枪毙’，他应该也会自己寻求改进吧！”

测试老鸟：“你说的很有道理。有些组织会定期‘下放’研发工程师到测试team一阵子，或让有技术背景的测试人员参与开发，在不影响项目进度的前提下，这种做法对组织确实会有帮助。可是你看我们的项目，不但人手缺乏，而且schedule还被压缩，哪里有空闲让双方人员做交流？大家在同一间办公室里工作，虽然隶属于不同团队，却老是为了意气之争吵来吵去。明明知道症结在哪里却无法有效改善，所以每一件项目总是要为同样的问题头痛一次。”

菜鸟：“如果哪天我做到项目经理，我不但不会坐视RD与测试人员的争执，还会设法加强沟通。人力和时间都已经不足了，怎么能放任这种类似空转的事情发生！”

测试老鸟：“加油！有理想有抱负的青年，希望你早日成功！”

18-03　Debug基本技法

时间：06月15日　 登场人物：菜鸟、系统老鸟

菜鸟：“我手头有几张Bug Sheet，查了半天还是找不到哪里出错，结果您看了一下就告诉我可能是哪里的问题，我本来还想您应该是用猜的，程序是我写的，我自己都找不出来，怎么可能你一下就可以看出来？结果还真的如你所说，我看了一整天找不到问题，为什么您看一下Bug Sheet，再看看程序就知道错误在哪里？”

系统老鸟：“老实说，我还真的是猜的，但我的猜测都是有根据的。你这个bug是只要执行了你这个模块后，系统就可能不定时、在不同地方死机。这个状况表面上很复杂，但对嵌入式系统开发有经验的人，一定马上就会猜到存储器被破坏了。会破坏存储器的状况我直觉想就有三种：第一个是stack操作；第二个是heap操作；第三个是指针的值乱掉了。如果是我来解的话，也是根据我猜测的可能性逐一检查程序，结果我看到你定义了一个很大的局部变量，所以让你把局部变量暂时设为全局变量，如果这样就不会死机的话，表示你的程序造成Stack Overflow，所以才会导致其他的程序死机。如果不是这个问题，我就会接着看你动态存储器操作的程序。如果还是看不出来的话，我会检查这个模块中所有关于指针的操作有没有问题。如果这样还是找不出问题所在，相信在刚刚三个步骤的检查下，应该会顺便看到一些可疑的程序段，再根据bug发生的现象去推敲，绝对会找出一些蛛丝马迹。”

菜鸟：“虽然表面上看起来是用猜的，原来您的心里闪过了这么多的推理过程。我觉得我在debug时都是乱枪打鸟，毫无头绪的trace程序。简单的问题也许还解得掉，像刚刚那种比较无厘头的bug，要在浩瀚的程序代码中刚好让我蒙到问题所在，几率应该和大海捞针差不多吧！原本我对自己programming的能力还颇具信心，没想到和您比起来强弱立现。”

系统老鸟：“调试（Debug）应该算是程序设计领域中最重要却也最难从书中学到的部分，通常只能从经验中累积，如果从整个产品的角度来看，一个还有许多bug的模块，和功能未完成其实是一样的。在嵌入式系统中，debug除了要弄清楚程序的结构外，因为驱动程序、系统、函数库和应用程序全部都连接在一起，非常容易互相影响，所以即便是应用程序工程师，也应该对系统架构与限制有所了解。”

菜鸟：“我终于了解程序不是写完就没事了，就算程序写得再快再好都不可能没有bug，既然如此，如果像我一样解一个bug就要搞上半天，对人手吃紧的研发团队来说并不是好事情。能否提示一下，当我们对嵌入式系统的程序进行调试时，是否有什么特殊的注意事项？像我这样的菜鸟，要加强哪些能力才可能变得和您一样厉害啊？”

18-3-1　直觉与经验

系统老鸟：“有些bug只是因为简单地误解规格、写程序时笔误、逻辑错误或使用了错误的API等，这些bug的NG现象很明显，通常很容易就可以trace到程序中错误的点，无论是开发PC程序、网络程序或电子产品里的程序，解决这种问题应该是程序员的基本技能吧！在嵌入式系统中，这些bug通常与平台无关，即在模拟器上应该也可呈现同样的问题。所以工程师可以使用PC上开发工具的强大功能去debug，就像对PC程序进行调试一样。

我们应该把重点放在嵌入式系统专有的bug类型，其实你只要把我们这些老鸟以前教你的所有关于嵌入式系统的思想融会贯通，碰到问题时，你的脑袋就会闪过系统架构以及运行的流程，在哪一个环节出错可能会造成类似的问题，于是你就有了debug的方向。”

菜鸟：“因为你对系统运行原理相当了解，再加上经验，您看过的Bug Sheet恐怕比我写过的技术文件还多，所以碰到问题就会有很准确的直觉。例如刚刚的那个问题，您的思考逻辑应该是：

因为我们的CPU并没有虚拟存储器的功能，所以系统的stack是有限的。

■　局部变量是使用stack做存储空间。

■　如果使用超过stack size，就可能压缩到其他的存储器。

■　被破坏的存储器可能不会马上被用到，所以不会立即死机。

■　当其他的程序用到被破坏的存储器空间，就可能导致死机。

这个问题是应用程序造成的，但其影响层面却是系统性的，因为您对系统的运行原理很了解，所以您可以当场想出几种可能性，并即刻从最可能的地方开始查起。您教过我许多系统的知识，所以我能马上理解事情的来龙去脉，但对一般应用程序工程师而言，即便看到他自己写的那个超大size的局部变量，应该也不会觉得有任何奇怪之处吧！因为一直以来，他在PC上就是这样写的，而且一切运行正常，怎么会到了这台机器上就NG了呢？”

系统老鸟：“所以我一直强调职内培训的重要性。要组织一个所有工程师都具备嵌入式系统思想的研发团队并不容易，有时候能找齐够资格的programmer已属万幸。就像刚刚那个bug，起因是应用程序的失误，但它的影响却是全面性。如果为了节省一些教育培训的时间，将来这些对系统架构不了解的工程师，可能会制造出许多要花更多时间去解的bug，真的是得不偿失。

其实我们可以将这些看似无厘头的bug稍做分类，以下列出当bug表现出什么样的行为，可能是何种问题所导致的。虽然不一定百分百准确，但按照这个方向去查，应该是八九不离十。

■　如果驱动程序已经通过严格测试，当发生PC OK但机器上NG的bug时，通常是存储器使用不当的问题。

■　函数返回时死机，应该就是Stack Overflow的问题。

■　当某块memory被破坏时，可以检查一下map文件，看看这块存储器的前后各有什么用途，应该就是邻近的存储器使用不当。

■　当CPU发生Address Error时，在某些有Address Alignment限制的CPU上，可能是在奇数地址操作了偶数个Byte，此时，应该要检查程序中指针的值。

■　如果heap被破坏了，可以在任何可疑的地方调用‘检查Memory Pool完整性’的函数，应该就可以找出程序中破坏heap的地方。

■　在嵌入式系统中，局部变量是没有初始值的，必须由程序员自己设定。如果没设定局部变量的初始值就直接使用的话，将可能发生每次执行结果都不同的现象。

■　在程序中要用sizeof()取得变量或数据结构真正占据存储器的size，不要自己想当然的直接填写数字，否则，很容易造成存储器操作的错误，如程序代码18-2所示。

程序代码18-2

系统老鸟：“现在电子产品的功能那么多，其中的程序也越来越复杂。开发时期的bug简直无奇不有，有时候我也会估计错误，检查了许多可疑的地方，却都不是造成bug的主因。在这个状况下，有些工程师会一直卡在某个模块或程序文件，看了又看，trace了N次还是看不出端倪。重点是解bug就像办案一样，你不能期待犯人会来自首，也不能找一个看不顺眼的嫌疑人，硬逼着他要承认，这样只是做义工，能破案只能说是运气好。Debug时应该要收集‘案发现场’的所有蛛丝马迹，推理所有可能的原因，按部就班地把bug找出来。”

菜鸟：“听您这样讲好像很简单，还办案喔！能不能多举一些实例？”

系统老鸟：“在这行做了那么久，经手过的产品开发项目也不在少数，各式各样的bug碰多了，甚至有的bug出货后还是无头公案。如果你以为我碰到什么bug都有把握推论出原因，那就错了！这是嵌入式系统开发的特性，每一个项目要开发的产品都不一样，有可能是家电、网络设备、手持式装置、玩具等天差地远的东西，几乎每一个项目都会碰到让我头痛不已的bug，但这也是嵌入式系统迷人之处。既然你想听，我就举几个我曾碰到的bug，你可以想想，如果是你的话，会怎么着手解这些bug呢？”

18-3-2　实际案例：无厘头死机事件

系统老鸟：“前一个开发项目的后期碰到一个bug，让所有项目成员人仰马翻了快一个月。Bug Sheet上只有简单的一句话：正常操作状况下死机，操作时间在一分钟到两个小时之间，即持续操作两个小时之内一定会死机，如果是断断续续操作则死机频率较低。此外，PC上的模拟器版本，从未曾发现死机现象。”

菜鸟：“这不是和我刚刚那个bug很类似，应该是存储器的问题吧！”

系统老鸟：“你的问题比较简单，因为一定要执行过你的模块，系统才会变得不稳定，所以直觉上会怀疑是这个模块破坏了存储器，所以我才会把这个bug指定给你。但现在说的这个bug没有规律性，任何的操作都可能导致死机，而且测试人员非常尽职，把某一次从开机到死机的所有步骤全都记录下来，但再照着做几次状况都不一样，有时步骤还没做完就死机了，有时步骤全做完了，系统却安然无恙。

根据这些征兆，我们判断应该是系统或是更底层的问题。但这个产品的规格还算简单，系统没有用到多任务的功能，即程序为单线执行，所以如果是程序逻辑问题的话，同样的操作应该会造成同样的结果，但死机出现的时机却相当随机，经过这样的推论，我们暂时认为应该是驱动程序或硬件不稳定所导致。”

菜鸟：“但我们团队不是都会要求一定要做完驱动程序的压力测试才可以开始整合工作吗？照理说，驱动程序搭配硬件应该是OK的才对啊？”

系统老鸟：“理论上这么说没错。但压力测试项目难免不足，或者根本压力不够大，也有可能压力测试之后，驱动程序还做了修改等。当时状况相当紧张，出货在即才被发现这个问题。”

菜鸟：“我知道了！如果是近期才出现的新问题，那么，可以查一下以前的版本有没有同样的状况，只要能找到一个版本是OK的，把所有可以的程序比较一下就能找到答案了。”

系统老鸟：“没错！我们当初确实也做了版本的检查。后来才知道，其实这个问题始终都存在，只是较早的版本本来就不稳定，死机的情况很多，所以当测试人员碰到这种问题，又没办法用相同步骤重现的话，多半都会先放过。”

菜鸟：“我还以为我猜对了呢，那么，最后这个问题有解吗？”

系统老鸟：“这种随时可能死机的问题不解谁敢出货？最后，我们同时做了一些处置，要求各单位帮忙。

■　请CPU与各重要组件厂商的FAE帮忙检查我们driver的程序代码，并针对这个问题提供建议。

■　将CPU操作存储器的速度变慢再测试看看。

■　请硬件人员测量死机后的各主要组件的信号，必要时使用逻辑分析仪，请测试人员协助，记录从开机到死机的所有信号。

■　请测试人员尽量找出最容易死机的操作手法。

■　请驱动程序工程师，详细比对程序的设定与data sheet内的规定，是否有任何可疑之处。

因为这个问题可能导致无法出货，工程师拼命找可疑的地方，修改后再给测试人员测试，相关人员几乎天天熬夜。虽然我说debug不应该乱枪打鸟，可是在想不到可疑之处后，除了寻求协助外，也只能继续做实验以寻找更多的线索。”

菜鸟：“结果呢？结果呢？”

系统老鸟：“我们自己忙了半天，情况完全没有好转。最后是CPU厂商的FAE传来好消息，他‘偷偷’比较了我们和其他厂商的驱动程序程序代码，发现LCD Controller初始化的程序有一点不一样，我们的程序少了一行看起来不是那么重要的设定，他建议我们改改看。更绝的是居然这样一改，死机现象就完全没有了。负责LCD driver的工程师完全没有印象为什么要把这行删掉，它只记得从CPU厂商那边取得sample code后，就根据需求一个个段落copy到我们的程序中。”

菜鸟：“所以应该是copy、paste时疏忽了吗？”

系统老鸟：“看来是的。这个例子麻烦的地方在于即使用了错误的设定值，LCD在大部分的状况下都可以正常显示，否则，在很早期就可以把这个问题找出来了。就是因为要在CPU正在操作Video RAM，同时LCD Controller也要refresh屏幕时才会引起CPU状态混乱，这是一个‘很巧’的时间点，所以呈现出来的征兆就是随机地死机。好了，故事说完了，你在这个故事中学到了什么？”

菜鸟：“copy、paste时不要打瞌睡吗？我觉得这是个很倒霉的case，如果不是CPU厂商，以我们对这颗CPU的了解程度，恐怕一辈子都找不出来吧！”

系统老鸟：“的确有人研究过，copy、paste是制造程序bug的主要原因之一，但这不是这个例子的重点。我要说的是，嵌入式系统开发多的是这种完全找不着边际的bug，虽然我们这些小主管的心里也是会慌，但我们也能按部就班的找出问题来。在这个例子中，即便CPU厂商没帮上忙，我们也已要求驱动程序工程师开始做code review，硬件工程师迟早也会量到LCD信号怪怪的等，只要有条理、有计划的去做，解决问题只是时间的问题而已。”

18-3-3　莫非定律：量产才出现的bug

菜鸟：“您讲的这个实例很有趣，真庆幸自己没遇上。如果那个LCD driver是我负责的，一定会难过好几天。快下班了，再讲几个故事来听嘛！”

系统老鸟：“刚刚那是项目后期出现的bug，已经足够让整个team人仰马翻。我还碰过正式量产才出现的bug。在生产线上有一半以上组装完成的机器按键盘都没有反应，数量约有1000台。因为迟迟找不到问题的原因，上市进度眼看就要delay，那个时候客户简直急到快发疯了。”

菜鸟：“量产前不是都会做小量试产吗？而且我们在发行正式版本以前应该都会好好测过吧！怎么可能到生产线上还出现50%的不良率？会不会是结构设计或模具射出的质量不良，导致某些key卡住啊？”

系统老鸟：“模具出问题是很合理的推测，因为键盘操作是该产品最基本的功能，软件已经通过了长时间的测试，实在没理由会是软件的问题。当时我们所有人也都是这样想的，因为在试产阶段时，软件与机器搭配并没有这样的问题，接着工厂做了许多试验。

■　拆掉量产机器的外壳，即只用PCB板进行测试→NG。

■　在量产的机器上运行试产阶段的软件→NG。

■　在试产的机器上运行现有的软件→OK。

从这几个结果，你会做怎样的推测？”

菜鸟：“第一点应该可以证明和模具无关，至于从第二点和第三点似乎可以看出，NG的机器（量产阶段的机器）不管执行什么版本的软件依然是NG；OK的机器（试产阶段的机器）不管执行什么版本的软件仍旧是OK的。根据我的推测，应该就是硬件的问题，答案是量产阶段的硬件做了一些修改，导致键盘运行不稳定，不是吗？”

系统老鸟：“基本上这样推理没错，所以当时硬件工程师也只能鼻子摸摸找问题去了，而固件工程师也认为这个问题应该和driver无关。硬件工程师接着做了一些实验。

■　硬件工程师仔细地测量了NG机器的信号，发现当key被按下时的信号相当正常地传入CPU。

■　将NG与OK机器的CPU互换，发现NG的机器变OK了，OK的机器变NG了。”

菜鸟：“所以和硬件设计无关，是和CPU有关啰？但CPU不是同一颗吗？怎么会有不同的特性？”

系统老鸟：“不同批次的IC虽然功能是一样的，但有时还是会有点特性上的小差异（通常只是Timing上的差异），但绝对会在Data Sheet规定的范围内。这个问题分析至今，客户已经强力介入，不但要求CPU厂商限期内给出分析报告，并要求所有相关人员统统到工厂集合，包含PM、硬件工程师、固件工程师，连我这个系统工程师也被点名，客户代表每天盯着大家的讨论，天天都要报告。固件工程师虽然认为和driver绝对无关，但在这样的集体劳动的气氛下，也只得认真的把keyboard driver看了一遍又一遍。与此同时，CPU厂商把我们认为‘有问题’的CPU换到其他厂商的产品之上，别人产品的键盘却运行的相当正常。于是，硬件设计也没问题，CPU也没问题，现在球又被踢回软件部门，问题直指keyboard驱动程序。”

菜鸟：“搞了半天，问题在每一个部门绕了一圈，随着线索越来越多，越来愈多疑点被排除掉；但如果是驱动程序有问题，为什么这么多的测试人员、这么多的测试机器、这么长的测试时间，居然会没有一个人报告这种状况？要等到大量生产才出现50%这么高的NG比率，又回到原来的问题，我的头开始痛了。”

系统老鸟：“你的抗压性有待加强，当时的状况是一堆人已经连续熬夜了N天，可怜的固件工程师，你真的无法想象所有人都在看着你的那种压力。最后果然还是驱动程序的问题……

那颗CPU有个特性，有些寄存器的没有初始值，如果驱动程序没有主动设定的话，那个寄存器可能为任何值。造成这个问题的原因是：固件工程师忘了设定键盘中断的优先级，而系统设定优先级7以下的中断不准产生，试产使用的那批CPU之寄存器的初值刚好是7，所以键盘中断可以正常产生，而量产使用的那批CPU，有一半以上该寄存器的初值却小于7，所以在这些机器上，键盘中断被禁止，反应出来的症状就是按下key没有作用。这就是为什么在量产之前，测试人员或工程师根本不曾碰到这个问题，要等到量产才爆发出来。事后看来，毫无疑问是固件的问题。”

菜鸟：“这种问题不是只要在NG的机器上trace一下就知道了吗？实在不该弄到这样沸沸扬扬的。”

系统老鸟：“你这是事后诸葛，就是因为问题的表面非常不像软件问题，所以固件工程师太过自信，完全不愿意主动去帮忙检查一下NG的机器，否则就不用绕这么大一圈，应该很容易就可以找出问题所在了。好了，故事说完了，你在这个故事中学到了什么？”

菜鸟：“嵌入式系统整合了软件、硬件、结构设计、生产管理等领域，当发生一个问题时，不应该先入为主的认为一定是谁的问题，或一定不是谁的问题。固件工程师帮忙看一下似乎是硬件的问题又有何妨！”

系统老鸟：“其实工程师并非不愿意帮忙分析这个bug，但有时组织内的文化确实会让帮忙分析的工程师变成众矢之的。经过这次教训后，PM老大开始在团队中推广一种思想：软件是嵌入式系统开发最重要，也是最复杂的部分。写程序难免会有bug，管理阶层不会因为某个bug而责怪工程师。换句话说，请工程师帮忙解决一个问题并不代表责怪其制造了这个bug。如果时间允许的话，工程师真的无需排斥帮忙分析‘表面上’不相关的问题，反而应该将其视为老板交付的艰巨任务。

有些主管总是会把软件的bug‘罪恶化’，工程师能实时帮组织解决问题，应该获得嘉赏，而非责备，即便这个bug是由该工程师自己制造的。许多人就是无法理解‘软件总是会有bug’这个道理，老是用高压的方式责备制造bug的工程师，长久下来，只会让组织中争功诿过的文化越来越严重。”

18-3-4　Offline Tools开发与sizeof()的陷阱

菜鸟：“有一个问题我查了很久，始终看不出个所以然来。我们有个PC的程序，姑且称作Offline DB Maker，会在PC上将几千笔数据做成像数据库（Database）一样的文件，这些文件（包含数据文件与索引文件）都会被烧到机器上的ROM或Flash里。我查过PC上的程序及其产生的数据文件，应该都没什么问题。此外，在模拟器上测试，系统读取该数据库的数据也都正确无误。可是同样的程序、同样的数据库文件，一放到机器上执行就什么都不对了，读出来的数据全是错的！

这个问题牵涉到Offline DB maker这个PC程序、模拟器与真正运行于机器上的系统，查起来真的很麻烦。我一一比对这3个程序对数据库文件的操作，比的眼都花了。”

系统老鸟：“别装可怜了！这个问题一听就知道是不同平台对数据结构进行sizeof()运算的结果不一致所导致的问题，你只要找出这3个程序所有用到sizeof(）运算的地方，追踪其值，就会知道怎么解了。”

菜鸟：“跟sizeof()有关？我刚刚才想把数据库的课本翻出来看看。”

系统老鸟：“这个问题很基本，只要解过一次，保证你一辈子忘不了。”

在嵌入式系统中常常会有所谓Offline Tools的需求，最典型的例子就是Database的应用。举例来说，电子字典或语言学习机内会内置很多数据，有时甚至会上百万笔，为了搜寻的性能考虑，除了必须将其以数据库的格式存储外，有时还需要高性能的压缩算法。上述数据封装、压缩等动作当然不会在实际机器上做，而是通过PC上的程序先行将原始数据处理过，再将处理好的文件烧到实际机器的存储器（ROM或flash）里。这种不会运行在实际机器，却又是产品开发不可或缺的工具程序，就称为Offline Tools。

还有一个更容易了解的例子。某机器需要发音的功能，我们当然不能直接把WAV文件摆上机器，甚至有时MP3格式都嫌太大，需知存储器size很小。业界有许多公司提供压缩率极高，非国际标准之声音压缩算法，解压缩的算法有的是通过软件模块，通常都是通过chip。这些算法有个共通的特点，就是压缩复杂，而解压缩相对简单。所以声音压缩的动作势必要在PC上做，实际机器上的程序只负责处理压缩过后的音频文件。

一个嵌入式系统开发项目可能会需要许多Offline Tools，有些Offline Tools是现成的，例如上述声音PC上的压缩程序通常会由厂商提供，但大部分的Offline Tools都必须自行开发，这也是嵌入式系统开发项目的特性之一。工程师必须开发两种不同平台的程序，而PC与实际机器上的CPU、memory size、compiler等往往是天差地远！

本节的主题是Offline Tools开发时最容易犯的错误，以下是一个简化过的例子。PC程序要把一组格式如程序代码18-3所示的数据，依序存储到文件，程序相当简单。

程序代码18-3

上述的PC程序乍看下没什么问题，接下来我们来看看实际机器上是如何使用这个文件的，如程序代码18-4所示。

程序代码18-4

Target上的程序看来也没不妥，移动指针（一次跳sizeof(struct data_entry) ）来操作各笔数据。实际上在某些环境下（环境=CPU+compiler) ，这样的程序却出问题了，target上除了第一笔外，所有的数据找出来都是错的。

问题出在PC程序和target程序对sizeof(struct data_entry）的值认知不同，在X86 32 bit PC +Windows 7 + Visual Studio C++. Net环境下编译出来的可执行文件中，sizeof （struct data_entry）的值为8，前4个Byte为栏位a，第5、6个Byte为栏位b，最后两个Byte对齐用，使整个数据结构的大小为4 Byte的倍数，这样会使得在该平台上，存取这种类型数据的性能最佳。但在target（机器）上（如16 bit CPU (e. g. 80186) + XXX compiler) ，很可能sizeof(struct data_entry)的值为6，因为只要数据是2 Bytes的倍数，即可达到最佳存取效率。

在这个前提下再回头看看上面的程序，你就会知道问题出在哪里。把sizeof(struct data_entry）的部分都改为6即可正确运行，但这样的程序编写方法的确违背了C语言教课书里的谆谆教诲，但却是解决在不同平台共享程序与数据结构的必要途径！

这样的问题没有万无一失的预防对策，特别是碰到一个新平台的时候。Data Bus的宽度也许是一个线索，但最后还是要看各个compiler如何分配空间给Data Structure。所以结论是：如果某数据结构会被Offline Tools与target程序共享，使用sizeof()运算子时务必小心，不要去“猜”或“算”某数据结构到底占据多少空间，再有经验的工程师也可能算错，毕竟不同的CPU、compiler有不同的特性与考虑。最好实际用debug的手段（trace、印字等）得知sizeof(struct xxx）到底是多少，以确保程序不会因此出问题。

此外，定义数据结构时可以注意顺序与各个栏位的size，尤其是奇数Byte的状况，如由程序代码18-5所示的数据结构中，实在很难得知compiler会将空间如何分配。

程序代码18-5

18-04　Tuning

菜鸟：“测试人员已经开出了几张系统性能不佳的Bug Sheet，我们的CPU速度这么慢，存储器size又小，性能不好是可预期的，我想，同样的程序在PC上一定运行的吓吓叫！这种性能的问题要如何解啊？”

系统老鸟：“性能问题可说是嵌入式系统开发者心中的痛，除了硬件性能本来就不能与PC相提并论外，使用者对电子产品性能的要求几近严苛。使用者可以忍耐Windows上的漏斗出现，却不能接受洗衣机按下洗衣钮后很久后才反应，更不要提其他有Real-Time需求的应用了，晚来的答案等于错误的答案！如果ABS刹车系统要一两秒才能算出正确的控制方式，这种车子谁敢开？

有关嵌入式系统性能优化的学问恐怕可以写一本书了。不同的产品有不同的硬设备、不同的性能需求，以及不同的复杂度，所以性能调整的困难度也是天差地远。我会讲一些原则性的思想，如果你计算机概论、算法、数据结构、操作系统的课都有认真上，老师讲的话也都有听的话，碰上性能问题时，应该能想出对应的方法来做试验，但能不能真的把性能调整到符合需求，则视硬件与系统架构的极限。”

菜鸟：“您讲得很轻松，我还是一头雾水，可不可以讲得实际一点，碰到性能问题时该如何着手？有没有可遵循的步骤？”

系统老鸟：“虽说优化是case by case的事情，但还是有几项重点。

■　在进行程序优化之前，先要有一个稳定、可靠的版本。而且在确定优化未完成前，不要将程序汇入正式的版本之内。

■　调整编译器优化的等级。但务必记得一件事，编译器无法帮程序员完成所有优化的任务，尤其是风格不佳，或写得零零落落的程序。

■　优先使用改良算法的手段来做性能调整。举例来说，无论再怎么精简程序，也无法弥补气泡排序法（Bubble Sort）与快速排序法（Quick Sort）间的性能差距。

■　以空间换取时间。如果存储器还有剩的话，可以试着思考利用多余的存储器空间来改良算法，进而改善性能。

■　不要把程序优化与‘用汇编语言修改’混为一谈。首先。应该找到程序的热区（Hot Spot) ，也就是程序中性能的‘瓶颈’所在，再利用所有你在学校学到与优化有关的技巧来试着修改它，例如：

□　尽量改写程序中用到乘法、除法与余数计算的部分，如乘以2N（如a = b*8；）可以改成左移N个bit(a = (b<<3) ；）；除以2N（例如a = b/8；）可以改成右移N个bit(a=(b>>3)；)。

▲ 图18-16　尽量循序处理数据

□　最大限度地减少分支（if……else if……else或switch指令）的数目与复杂度。

□　尽量循序处理数据。例如数组a[100][200]，要将数组内每一个元素都设为0×0A，方法一的性能会比方法二的性能好，如程序代码18-6所示。

程序代码18-6

□　检查并尽量减少for循环或while循环内的迭代次数。

□　在迭代次数较多的for循环或while循环，尽量精简循环内的程序。在程序代码18-7所示的例子中，循环里计算字符串长度的程序是多余的，只要在进入循环前做一次即可。

程序代码18-7

□　牢记不同的存储器有不同的速度，尽量把热区（Hot Spot）的程序或数据传输到速度较快的存储器上执行或操作。

■　对大部分CPU而言，一次操作偶数或4的倍数Bytes的数据会有比较好的性能，检查程序中是否可能用到奇数的地址，或一次操作奇数个Bytes的程序代码。如果有的话，应参考CPU Data Sheet并试着修改它。以下的程序要将阵列‘buffer’内的每一个Byte都设为0×0A，方法二的性能会比方法一的性能快上数倍。

程序代码18-8

■　不要试图从头到尾改写某个模块为汇编语言，先检查编译器产生的汇编语言代码是否有明显的性能问题，再根据这份汇编语言代码来修改即可。在修改时，应仔细研究CPU各个指令的性能，尽量使用性能较高的指令。

■　去除不必要程序代码。许多用以调试的程序代码会忘了去除，导致在正式版本中，系统执行了许多没有必要的程序段。

优化的思想与技巧差不多就是这些了，还是那句老话，如果你程序设计与计算机系统的思想够扎实的话，等你碰到问题时，应该就会知道有哪些方法与技巧可以使用，不用太担心。”

18-4-1　执行时间测量

菜鸟：“刚刚提到在着手调整系统性能之前，应该要先找出程序性能的热区（Hot Spot），我想知道所谓的热区是怎么找出来的？”

系统老鸟：“我们说某个功能或模块的性能不佳，其实就是指相关程序完成该功能所需要的时间太长或无法合乎需求。为了找出程序中性能的瓶颈所在，我们需要能够‘测量CPU执行某段code所需要时间’的工具或方法。”

菜鸟：“应该用一个Timer来计时就可以了，在测量执行时间的程序段开头启动Timer，结束后即刻停止Timer; Timer ISR内有一个Counter, Timer ISR每被执行一次，Counter就累加1。最后由Counter的值乘以Timeout的周期，即可算出某个程序段的执行时间。”

系统老鸟：“用Timer是个不错的方法，但要注意可能的误差。除非待测程序段的执行时间较长，例如1秒或1ms以上，否则我并不建议用这个方式。首先Timer有其精度，我们不能把精度设太细，否则待测程序执行时，Timer中断会发生的很频繁，我们测量出来的时间段中，有一部分是在执行Timer ISR。此外，我们也不能把Timer精度设太粗，否则，测量出来的数据无法表达细微的改变，这对性能调整的工作毫无帮助。

有个方法很实用也很简单，如果你把待测程序段执行个十万或一百万次[10]，就可以用码表测量，量得的时间再除以十万或一百万，即可得到执行一次的时间。”

菜鸟：“用码表，这样误差不是很大吗？”

系统老鸟：“教科书当然不会教你这种听起来不是很专业方法，但这的确是相当实用简单的手段，而且你说的误差在除以100万之后几乎可以忽略了。实际上，我们就是用这种方式来测量程序区段的执行时间。当然你要用Timer或示波器（在该程序区段头尾分别去拉动某GPIO，示波器可以很容易量到这两个事件间的时间）来取得该程序区段的执行时间也都OK。”

18-4-2　杀牛还得用牛刀：逻辑分析仪

系统老鸟：“有一些比较复杂的状况。当多个应用程序、系统、驱动程序同时（Concurrently）在执行时，真的很难判断程序间的执行顺序，以及到底是哪个程序用了较多的时间。此时，示波器的channel可能会不够用，这时候就可以考虑请出逻辑分析仪来。”

菜鸟：“可以举例说明吗？大概是什么等级的问题会用到LA?”

系统老鸟：“越是平凡无奇的问题越是麻烦！我们之前在使用8 bit CPU做项目时曾碰到一个状况，某个画面的LCD显示速度特别慢，然而这个画面相比其他的画面并不会特别复杂，做完code review后，发现这个画面也没用到特殊的LCD功能，实在看不出来为什么只有这个画面有问题。”

菜鸟：“应该是系统或驱动程序在后台做一些事情，而拖累显示速度的吧？”

系统老鸟：“是的，当初我们也是朝着这个方向去追。因为8 bit CPU寻址空间有限，当应用程序在执行时，部分系统与device drivers的程序代码并未被载入存储器中，而是存放在NAND flash里，等相关函数被调用时，系统才会将其载入。当某程序区块被载入，原本在存储器里的程序区块当然就会被覆盖，我们称这样的流程为Bank Download。

试想一个极端的状况，原本系统的File-System功能已被载入存储器，此时，应用程序A调用了LCD的显示功能，引起了一次Bank-Download。若这个时候系统已没有多余的bank（可能是因为系统有更重要的工作正在执行，相关程序占用的bank都被lock住了），而应用程序B接着又调用了File-System功能，则会导致LCD程序模块被覆盖。当切换回应用程序A时，若要接着使用LCD程序模块，则系统又得做一次Bank Download。

最差的状况可能会造成系统不断做Bank Download, NAND Flash的存取速度是相对较慢的，自然整体的系统性能会被拉下来。而这必须是系统、应用程序A、应用程序B处于某一特定状况才会发生的。分别去看这3者的程序是看不出任何端倪的，因为它们在其他状况都运行的很好。实际上，我们碰到的例子更为复杂，可能同时执行的程序有N个，我们一定要知道发生错误时各个程序的执行顺序与执行时间才能进行分析。”

菜鸟：“要收集这些信息，就让每个程序分别打印出信息出来不就好了吗？”

系统老鸟：“一般状况是这样没错，但Print-Message是个相对较慢的动作，而多任务系统所碰到的棘手问题总是和Timing有关的。在调试阶段，常常会听到工程师哀号：‘为什么我在不相关的地方加一行打印debug message的code, NG状况就不见了？’其实，这反而是个有用的线索，代表这个问题显然与程序执行顺序或Timing有关。

到处打印debug message有时反而会制造出不同的NG现象来，若系统因此就正常了反而更令人不安（当时间紧急时，有些工程人员会倾向先把这个bug放着，等于是在系统中埋下不定时的大炸弹），这都只会让状况更复杂。因为控制GPIO应不致影响Timing，上一节介绍的信号测量法才是此时最合适的对策。

这种问题通常与系统、驱动程序的状态有关。当GPIO不够时，可以检查是否有些程序在执行时本身就会造成某些PIN脚的变动。例如：LCD driver被调用时当然会使LCD-data PIN跟着动作；而NAND Flash driver在动作时，至少chip-select PIN会被拉动，这些都可以用来当作相关程序段正在执行的参考。所以一般来说，并不需要担心GPIO PIN不够的状况。

图18-17所示是个简单的例子（你可不理会细节），通过LA，我们可以明确知道程序的执行顺序，以及某个function的执行时间。”

▲ 图18-17　使用逻辑分析仪分析程序区段的执行顺序与时间

18-4-3　找出热区

系统老鸟：“有了测量执行时间的工具，接下来我们要试着找出程序中对性能影响最大的热区。说说看你所能想到的方法？

如果程序是自己写的，应该很清楚效率不佳的程序区段在哪里吧？不然，你刚刚提到了几个重点，应该都是判断热区所在的线索。

■　迭代次数很多的循环。

■　乘除法运算频繁的程序区段。

■　频繁被调用的函数。

■　递归的程序段。

■　频繁task switch的程序段。

■　容易造成heap碎片的程序。

因为在PC上执行什么都很快，所以我们只能在模拟器上trace程序的逻辑，得知哪些程序段频繁地被执行，推测可能造成性能瓶颈的程序区段。至于要观察修改过的程序性能有没有改善，还是得到实际机器上测试。有必要的话，就应该实际测量修改前和修改后，‘热区’程序段执行时间的改变，或者直接在实际机器上trace程序，当执行到性能不彰的功能时，整个系统好像没在动且不知道正在做什么运算时，利用debug环境的暂停功能让程序暂时停止执行，再继续逐行trace，应该就能够知道到底程序在哪一个区段卡了那么长的时间。

如果负责执行性能调整的工程师，对你上面说的这些道理有概念，自然可找出性能的瓶颈。就怕这个工程师本来就不认为自己写的程序有效率上的问题，让他再看n次，还是觉得这已经是最佳的写法了，没有改善的空间。为避免这种类似裁判兼球员的问题，研发主管会强迫执行下列两个动作。

■　Profiling：统计可疑函数或程序区段执行时期的信息。所谓的‘信息’是指当使用者执行了某个功能时，期间相关的函数各被执行了多少次、被执行前后Memory Pool或Stack的状况、每次被执行各用了多少时间等。

如果实际上某个模块的性能确实不佳，但负责的工程师却始终找不出性能低落的原因时，我们会要求工程师使用Profiling的方法产生执行时期信息的书面报告，再由相关人员会诊，一齐找出所有值得怀疑的地方。然后根据此结果缩小范围，这次以函数内的程序区段为单位，再做一次Profiling。如此不但可以让其他人员参与性能问题的分析与讨论，而且根据这些信息，应该可以轻易地找出‘最值得’花心力去修改的程序段。

■　Code Review：工程师各有专长，对某些领域的思想难免会有偏颇之处。对已造成性能瓶颈的程序模块，我们不能只让原先负责的工程师去检查，对自己写的程序难免会陷入困惑，总是会有所偏执。最好的方法是召开code review会议，由负责的工程师主持，讲解该模块的架构以及程序细节，并尽量要求有经验的工程师参加。

这种会议与之前检查工程师的程序是否根据设计规格、是否符合规定的程序风格等code review不同。这样的会议应该是一种头脑风暴，所有参与人员都应该试着找出程序中性能或架构的缺陷，或者提出任何可改善该程序性能的方案。”

菜鸟：“可以想见这种code review会议一定是高手过招，有机会可不可以让我也参与一次。”

系统老鸟：“其实也没你想的那么神奇，除了改用性能较好的算法外，并用优化的技巧其实都是很基本的思路。刚刚不是说工程师常对自己写的程序有所偏执，容易陷入困惑。有时候问题真的很简单，但自己就是看不出来，这就是所谓的当局者迷吧！”

18-4-4　在较快的存储器中执行程序或操作数据

系统老鸟：“除了上述优化的技巧之外，在嵌入式系统中有个很有效的方法，就是将造成系统性能瓶颈的程序区段，传输到速度较快的存储器中。例如，程序在ROM里执行肯定比在RAM里执行慢，在外部RAM里执行一定比在CPU内部的RAM里执行慢，这是物理性的限制。有时候算法调了半天没有效果，结果只要把热区的程序段传输到速度较快的存储器执行，性能就可以提升约50%以上。

我遇过一个最极端的例子，是有关语音压缩算法的模块，其中要用到许多复杂的数学运算。首先，我们先从学术论文中找到已被证明为最佳的算法，发现性能在我们的CPU上还是不行；接着，我们把程序从ROM移到RAM里执行；然后，再把程序从外部RAM移到CPU内部的RAM里执行。做了这些步骤后，声音质量只能说是差强人意，还需要进一步的调整。一直到了山穷水尽的地步，我们才决定用汇编语言改写相关程序，最后性能刚好可以接受。对所有参与这次调整的人员，能在这样慢速的CPU进行调速都是相当难得的经验，几乎什么技巧都用上了。”

菜鸟：“除了程序之外，是否‘数据’也可以对照处理？有时候可能程序的算法没有大问题，改善的空间不大，但其所操作的数据是存储于较慢的存储器中，导致整体性能就是快不起来。就像同样的算法，直接操作硬盘中的数据，与‘先将数据载入RAM再操作’的性能一定会有所差异。”

系统老鸟：“没错！其实道理是相同的，如果某个功能的瓶颈在于操作数据的程序，优化这个程序的方法之一，就是在较快的记忆统中操作这些数据。广义来说，各种数据存储媒介的速度由慢到快依序是：

■　必须经文件系统存取的NAND Flash（最慢）。

■　直接存取NAND Flash。

■　必须经文件系统存取的NOR Flash或Mask-ROM。

■　直接存取NOR Flash或Mask-ROM。

■　CPU外部的RAM。

■　CPU内部的RAM。

■　CPU的一般寄存器（最快）。

需要注意的是：速度越快的存储设备通常越贵，而越贵的存储器，在系统中可用的size自然也比较少。所以在改善性能时，势必要把造成性能瓶颈的热区局限在更小的范围内，才有办法将其塞进速度较快的存储器中。”

菜鸟：“从前在PC上写程序几乎不用考虑到性能的问题，就算完全不考虑算法，或者存储器无限制的乱用，程序还是可以执行的很顺畅。我想我以前那些程序如果可以拿到我们现在的机器上执行，一定慢的和乌龟爬一样。现在我写我们项目的程序时，就算只是和硬件无关的应用程序，也都会小心翼翼地考虑到我们之前说的注意事项，例如存储器要谨慎使用、使用有效率的算法等。这是我进入嵌入式系统这个领域以来最大的改变与收获。”

系统老鸟：“一旦你写程序的心态与习惯改变后，就算以后不是开发嵌入式系统，都能写出高性能、高质量的程序了。”

18-4-5　Foot-Print调整

菜鸟：“我真的很佩服你们在分析bug时的‘直觉’，虽然知道这都是经验的累积，但我真的很想知道你们有比较害怕碰到的状况吗？”

系统老鸟：“无论大bug、小bug统统是bug，碰到了就要解决，基本上没什么好怕的。但找到bug的Root-Cause，和能不能顺利解掉其实是两件事，最怕的就是追到最后找到目前系统的‘死穴’，不动大手术没办法痊愈，其中最麻烦的就是程序或数据Footprint Size的问题。”

菜鸟：“我们之前谈了很多嵌入式系统开发时内存管理的问题，我想大家都会很小心的评估内存用量，应该不会到项目后期才爆发问题吧？”

系统老鸟：“所谓Foot-Print就是足迹的意思，在系统开发中包含code size、run-time RAM usage、data size、SDRAM Bandwidth[11]等。在项目初期要估计正确并不容易，其实应该说是很难才对！所以有经验的项目经理都倾向一方面在硬件规格上多要一点，另一方面压缩工程人员的空间。

你应该很容易可以想象，系统开发到后期才发现内存不够用了，这会是多麻烦的事情！如果把各种瘦身的方法都用上后还是不够，那恐怕就是系统架构或算法的问题了，当项目走到这一步，我想项目经理一定食不下咽了。Footprint的问题越到后期爆发越麻烦，所以必须在整个项目周期，不断地计算、精算、统计。任何工程师发现有问题，应立即提出警示。对生产成本较敏感的项目，系统应提供‘以时间（效率）换取空间’的机制，例如：动态加载（参考《第12-5节：突破物理内存大小的限制》)。

执行Foot-Print调整必须要有明确的目标，以及目前状况之精准的数据，通常可以由MapFile、Image Generator（自己的tool）得来，先找出瓶颈，确认是code size还是Runtime Memory的问题，再由code review进行调整，对症下药。

当发生内存不足时，可能的调整的方法为：找出冗余for debugging的code、修改algorithm、规格缩减。最差的状况是客户不愿接受规格修改（减少功能或增加实际Memory Size) ，逼得研发团队必须修改系统架构，这在项目后期是无法想象的事情，所以一定要尽量在项目早期预防此风险的发生。”

菜鸟：“听起来是蛮麻烦的，可不可以麻烦举个您碰过比较‘惨烈’的项目？”

系统老鸟：“之前做过一个电子辞典的项目，因为项目初期的分析工作做得不够确实，除了低估字典数据库的数据量，还高估了压缩算法的压缩率与解压时的性能，造成NAND Flash需求量大增（如原本预计8 GB，现在需要10 GB），当问题爆发出来时已经进入试产阶段。

当时我们提出的解决方案为：

■　使用较大的NAND，但NAND Flash size得从8 GB直接跳到16 GB；再加上NAND Flash价格宛如期货般不断变动，造成备料程序大乱。

■　使用8 GB+2 GB，更麻烦的是软硬件架构并未设计可支持两颗NAND Flash。

■　使用更复杂压缩算法，虽然可将数据压得更小，但除了执行速度变慢，还造成runtime需要更多的RAM，此时变成RAM size不够了！

最后不是增加NAND、就是增加RAM、要不就是不要卖！最后只能用商业方法解决，基本上，此project算是fail的，而且还赔上了研发团队的credit。”

18-4-6　耗电流调整

系统老鸟：“除了Foot-Print外，碰到对电池使用时间较为敏感的手持式装置，开发中后期对号电流的调整有时也会搞死人。”

菜鸟：“记得以前我们讨论过不少系统电源管理的注意事项（请参考《第9-3-8节：Power Management & Low Power》），我认为我们的Power Manager架构还算OK，而且各个driver也已根据HAL定义出power API进行实作，我不知道在这个阶段还需要什么调整？”

系统老鸟：“在设计阶段，我们可以根据所有硬件组件的规格来计算系统耗电流的理想值，但硬件与软件都可能影响最后所量出来的值。我问你，系统目前的整体耗电流是否已达目标？”

菜鸟：“目标是什么？没听说过耗电流有业界标准啊？”

系统老鸟：“系统耗电流的目标乃根据产品的特性与规格来定义，例如：音乐播放可以播几小时，充电一次可以待机多久等。系统有各种不同的执行模式，耗电状态都会不同（至少全功能执行与关机时的耗电流一定不会相同），通常我们只会关心下列几项。

■　使用者使用此机器时最常处于的状态，通常会是idle或sleep mode。

■　主要功能：如MP3 Player/播放音乐、ebook/画面显示或翻页、网络设备/封包传输）手机/通话。

■　最耗电的功能。

在项目的规格设计阶段就必须为此项目制定耗电标准，并明确定义在product SPEC。或Testing Plan中。当Power Manager与driver等软件都已按照系统架构运行，系统耗电流仍未符合上述规格，此时软硬件工程师就必须共同进行耗电流调整。基本步骤为：

■　精确测量当前耗电状况，必须测量：系统整体耗电量、系统局部耗电量（Target Board硬件设计时就必须考虑到此需求）、各个外部装置耗电量。

■　先确认是否硬件问题（上错组件、电路设计或Layout错误等），并尝试更换较省电的组件。

■　Review Power Manager在各个执行模式的Power Configuration，必要新增或微调powermode。调整的方式包含：

□　测量并记录CPU与外部IC的各个PIN脚的电位与状态，逐一检查是否有异状。

□　Review每个driver对相应装置的设定。

□　调整Clock。

□　调整工作电压！”

菜鸟：“我认为在制定耗电量规格时一定也会多找一点空间，如果就按照硬件理论值去制定肯定会出事。整个硬件板子上那么多组件，系统中那么多driver，只要每个都多用一点电，很容易就超出规格，导致软硬件工程师必须逐一去微调，必然也是耗时费力的工作吧！”

[1]　军事项目通常也是一种嵌入式系统，因为发生错误时可能造成伤亡以及财产的损失，所以开发时会绝对会严格遵守软件工程与质量系统的规范。

[2]　在《第19-2节：自动测试程序》会再详细说明用于工厂生产线上的自动测试程序。

[3]　如任天堂Wii电视游乐器还未获准在台上市之前，日本水货是否可以在台销售即造成争议，每一个国家对电子产品都有自己的规定，这和产品质量优劣并没有绝对的关联，可在日本贩卖的产品，不见得可以通过台湾地区的电气特性检查。

[4]　EMI测试机器包含：频谱分析仪、各式天线、电流注入感应器（Current Probe）、电压感应器、隔离变压器、存储式示波器、各型滤波器、定向耦合器等，你无需知道这些仪器的用途，只要知道EMC\EMI的意义即可。

[5]　不见得所有的产品都需要这么长时间的环境测试，也不是所有的厂商或工厂都这么注重产品的质量。如果要按部就班的做完这些实验，一个产品的开发势必要做2〜3次试产，而试产的时间可能要拉长到一个月以上，再加上前期硬件设计，与后期软件测试与量产的时间，一个电子产品开发所需的基本时间恐怕要在半年以上。

[6]　请参考《第17-3节：发行第一个版本》。

[7]　如果真的有一个bug要30多个特定顺序的步骤才会发生，那么，这种bug被使用者按到的机率并不高，所以相对的’这个bug的重要性也不高。工程师应该集中心力解决较容易发生或critical的bug上。

[8]　Bugzilla有个很好的功能，当任何人为某个bug提出意见时，可以设定关系人，以后，当此bug有任何状态的改变，Bugzilla会主动寄mail给所有关系人。

[9]　RD很反感这种问题，如果知道问题何在，不就可以把它改掉了吗？偏偏实际上喜欢问这种问题的人还真不少。

[10]　执行的次数根据待测程序段的大约执行时间而定。实际上，总共执行时间约为几十秒或分钟等级即可。

[11]　SDRAM每秒能处理的数据量，举例来说，若系统要求LCD每秒要显示30个frame, LCD分辨率是640′480，色阶是65535色，则我们可以算出LCD每秒要从SDRAM提取的数据量，此时就可检查SDRAM和SDRAM Controller的Data Sheet，以确定目前SDRAM的硬件规格是否可支持LCD显示的需求。

Chapter 19

结项前的煎熬

时间：07月02日

登场人物：登场人物：菜鸟、项目经理（PM）、系统老鸟、测试主管

菜鸟：“这一两周以来，测试人员已经渐渐找不到什么critical或serious的bug了，现在找到的bug简直有点吹毛求疵，几乎可说是一般使用者根本不可能如此操作。您也说过‘使用者按不出来的bug可以不算bug’。昨天才看到一个应用程序工程师很生气地揉掉bug单，大喊：‘现在都什么时候了，还要求改这种不可能按到的bug，要是影响到基本功能谁负责？’。看来人心有点浮动，RD与QA、测试人员的冲突事件层出不穷，大家都在问：‘目前版本已经很稳定了，到底什么时候才可以推出最终版本？何时才可以结项？’”

PM：“如果这是我可以决定的就好了，我又何尝不知道目前团队的状况，现在我每天的时间都用来应付客户改东改西的需求，虽然已经尽量推掉一些不合理的要求，但在项目开发的后期，似乎所有人都害怕RD team快解散似的，各式各样奇怪的需求都出现了。有时候，无论我们工程师再怎么解释修改可能造成的风险，对方只要举起‘客户服务至上’或‘质量第一’的大旗，我们接人家项目、赚人家钱，还不是得乖乖的照改不误，才不会有人关心研发团队的士气如何。殊不知这么做，表面上是改了几个bug，但实际上却让质量暴露在不确定性的风险下。”

菜鸟：“我以前也认为软件一定要做到零缺陷，现在已经知道这仅是个努力的目标而已。在项目开发的质量理论中，最重要的不是零缺陷，而是产品与项目的质量是受控制且可以保证的。简单地说，只要我们可以确定在某个范围内的功能都是OK的，就可以说质量是受到控制的。如果在项目后期硬要修改一个不在上述范围内的bug，程序的任何更动都可能造成副作用（Side Effect），除非把范围内的功能都再测试一次，否则，范围内的功能就存在着质量的风险。这种现象如您说的质量失控，如果QA/QC人员只会要求把所有被找到的bug修改完，那公司花钱请他们来上班干嘛？”

PM：“你说的没错，但我看你也有点情绪化了，放轻松，结项之前的这段日子是最难熬的，你必须习惯这样的日子。何况最难过的不是工程师，而是夹在RD与QA/QC、研发团队与客户之间的项目经理，简直里外不是人。”

菜鸟：“但项目总是要结项的。我们只是开发端，不应该负责产品定义以及销售的成败，只要我们如期完成合约中的规定，不管客户要不要生产，就应该可以顺利结项不是吗？”

PM：“说实在的，有时候我也没把握项目是否可以顺利结项。原因很多，第一，客户关系很重要，通常是以拖待变，但非到必要关头决不轻易说NO，除非是太不合理的要求，我们都会含着眼泪，带着微笑默默地接受。第二，合约中很难把产品规格、质量要求描述的清清楚楚，有时候老板为了接下项目，会默许客户在合约中留下极大的模糊空间。举个极端的例子，如果合约中对产品的描述只有‘智能手机的开发’，那么，就算客户临时要你把操作系统从Windiws Mibile换成Android，不增加开发费且不能delay，这么做似乎也不算违约吧！就是有这么不厚道的客户，而且通常越是国际大厂越敢这么做！

第三，你知道合约中最容易表达清楚的是什么吗？就是完工期限！产品规格模糊，但完工期限却是清清楚楚的白纸黑字，这样的合作关系很容易让客户通过要求新增、修改功能，或迟迟不验收等手段让开发进度delay。还记得我说过客户口袋中总是会有schedule的buffer吧？有时候我觉得在可容忍的范围内，客户会故意让开发厂商delay，如此一来，无论有什么要求都很好谈。

再来，最差的状况就是没有合约，没有合约代表什么你知道吗？没有合约代表没有规格约束、没有验收条件，这种状况下，结项判定的模糊空间大的不得了。也许你会问为什么不签合约，这样对客户与厂商双方不是都没有保障吗？答案我也不清楚，不是执行者偷懒无能，就是两间公司高层有什么我们不知道的默契，我知道有个日本国际大厂的海外分公司就是这样做事的，上次帮他们开发一款产品，到项目后期无止境地出版本，简直被搞死了。”

菜鸟：“我也注意到这些状况了，到目前为止，我们的项目已经delay了一个多月，其实最近这一两个月几乎都是在增补客户的新需求，以及修改一些不是那么重要的bug，如果一切按照当初客户已经同意并签名的设计规格来做，我想我们这次是有可能准时结束的。”

PM： “所有的老板、产品设计、市场营销、QA/QC以及所有不负责开发的人都有一个疑问：‘为何没有一个软件项目是不会delay的？’，你知道答案了吧！”

菜鸟：“我知道您的意思，软件设计只是产品开发项目中一个较大的工作项目而已，所有事情其实是环环相扣的。当然软件的复杂度较高，管理较困难，是比较容易出问题，但也并非不能管控，一个项目或产品delay的原因很多，而软件delay只是最明显的表现，并非就是delay的主凶。”

PM：“嗯〜每次项目结项会议中，软件经理总是众矢之的，哪一位有经验的软件主管肩膀上不是扛了许多黑锅？等到下个项目要开始时，所有人包含老板，就会来拜托你‘主导’这个项目，当设计工作完成的差不多，其他单位的手脚才会慢慢伸进来，最后整个项目被客户、老板或QA/QC人员弄得‘乌烟瘴气’后，再把责任推到软、硬件工程师身上，这种事情在像我们这样开发产品的公司中经常上演。这是生态，你要习惯。”

菜鸟：“（心中OS：一直要我习惯，我看您自己也还没习惯吧？）所以项目的结项可能会遥遥无期吗？”

PM：“无论如何，产品开发项目总会有个‘了结’，所谓的了结不一定是结项，可能是顺利生产、可能是客户放弃产品的上市计划、可能是双方没完没了地争执责任归属，但无论如何，我们公司不可能把宝贵的人力一直放在某件产品开发项目上，即便team没有解散，也会逐渐把最有生产力的人员调离，最后可能只剩一两个人，美其名为‘维护’，但真正的工作内容其实是在应付客户，我知道这样说不好，但实情就是如此。

所以不管客户是否愿意结项，当项目经理发现人力逐渐被调走时，应该及时召开一个长达一周以上的结项会议，严格要求各部门主管以及所有参与项目的人员把技术文件、会议记录、程序、线路图等整理好并备份，我们总不能等到所有人都投到其他项目后，才来做内部结项的动作，到时没有人会理PM的。”

菜鸟：“趁他们想‘逃离’这个项目时要求他们帮最后一个忙，我想他们都会尽力把这些技术资料整理好的，对吧！”

PM：“努力了几个月，所有项目成员到这个时期都已经有点弹性疲乏了，项目开始时的热情已渐渐熄灭，无论是对人、事都很容易情绪化，此时的管理真的很需要技巧与耐性，几乎很难再用长官的高姿态要求他们去做有些显然不合理的事情。这段时间是最难熬的，有时候要请团队吃饭搏感情、有时候要连哄带骗，再加上适时的身先士卒，但无论如何，做PM的人要很清楚谁会是帮你做事的人，要设身处地帮他们着想，毕竟我们公司以后还是要进行其他的产品项目开发。

不多说了，客户刚刚打电话来问我某个bug改好了没？他们急着要一个新版本测试，这完全打乱我们的版本的发行计划，系统老鸟和测试主管对这样的事情相当反感，你和我一起去找他沟通一下吧！”

19-01　版本发行：兵荒马乱的日子

PM：“客户大人要求我们今天出一个版本，我知道这不在我们原先的计划内，但请大家勉为其难地帮忙一下吧！”

测试主管：“这样疯狂的出版本有意义吗？即便RD认为此次的修改不会有副作用，但谁敢保证真的不会有失误？我就碰过N次在重新build的过程中，因为某些地方疏忽了，使用了旧的或有问题的版本，所以之前已经改好的问题重复出现（称为REOPEN），反而造成客户对质量管控的恐慌，并导致客户认为我们团队的质量有问题，老板和质量部门主管已经来关切好几次了，殊不知这些不专业又爱管事的人，才是质量失控的始作俑者。

总之，就我的专业与经验，出版本之前一定要经过严格的测试才能发行，以目前产品的复杂度来说，我们现有的人力需要4〜5天才能把所有基本功能以及曾经出现的问题重复测试一次，这就是为什么我们预估至少要一周才可以出一个正式版本。上一个送测的版本目前还没测完，就要再出新版本，而且还马上要，客户连这种common sense都没有吗？”

PM：“他们对产品开发的细节的确没我们清楚，毕竟他们不是技术人员，而我们也很难用平常人的语言向他们解释某个REOPEN的bug是如何产生的？为什么修改某个bug的风险是很大的？而偏偏他们老是爱问bug发生的原因。但实际上并不是每一位客户都是这样，既然碰上了还得好好应付，我们当主管的要扮演工程师及外界的缓冲区，我们可以和下属一起抱怨，但心中还是要有解决方案，毕竟你的责任是组织测试工作，尽可能找出产品可能的问题，而我的责任是让项目顺利进行，担任客户与研发团队间的桥梁。

我已经和客户语带威胁的说明可能的后果了，但他还是坚持今天一定要有个版本，我现在想知道的是我们该如何解决这个需求？系统老鸟，今天出版本可以吗？还有没有Critical Bug没改完的？”

系统老鸟：“今天做版本我可以配合，而且其实很久前就没有Critical Bug了，今天客户关注的这个bug也不是那么严重啊！我已经向他们解释过那是程序的疏忽，也不知道为什么客户急着今天就要看到，再过两天我们就会出正式版本了！越到后期客户越没有安全感，殊不知越急越容易出问题。总之，新旧版本间的差异我可以掌握，今天出版本前会写一个版本差异与风险评估报告。”

测试主管：“我现在就去停掉目前版本的测试工作，并把目前找到的问题整理一下寄给相关人员，因为测试计划尚未执行完成，正式的测试报告我就不写了。等新版本出来，我会请测试工程师先看客户关心的bug，然后从这次还没测完的部分优先测试。”

PM：“那就这么决定了！测试主管，你要设法安抚你们的弟兄，找一天我请他们聚餐。菜鸟同学，你今天就留在系统组帮忙出版本吧！”

菜鸟：“没问题！我正想学学版本发行的流程。”

19-1-1　版本发行流程

菜鸟：“我发现你们team要负责的事情还真不少，开发环境、模拟器、RTOS与系统功能、系统整合都是，现在才知道版本发行也由你们负责，果真是能者多劳。”

系统老鸟：“研发单位的组织与工作分配如何画分，可能每一个团队都不尽相同，但版本发行兹事体大，最好还是由对整个系统最熟悉的人来做比较好。一来较不会疏忽了哪一个步骤，而且当出了什么问题时也可以做出正确判断，以便立即请相关的人员处理。”

菜鸟：“到项目后期不是一切都逐渐稳定了吗？现在我们解的都是一些小问题，出版本不就是把整个项目重新build一下就可以了吗？难道还会出什么大问题？”

系统老鸟：“越到后期，被找到的bug数目应该会逐渐收敛，每一个版本间的修改幅度也应该要越来越小，此时会发生的问题通常都是人为疏忽造成的，而且造成的后果会反映在其他的功能上。例如某位工程师为了修改某个bug，开了一个很大的数组，在测试单一功能时可能没问题，但执行较复杂的操作时，可能会把原本刚好的存储器用爆了。这种为了修改功能A的bug，却使功能B变成NG的现象，就是所谓的Side Effect（副作用）。

对不懂程序的人来说，他们无法理解为什么以前OK的功能会突然NG了，他们会片面解释为：系统不稳定、工程师程度差、产品中应该还埋藏着许多不定时炸弹等。这些评论对研发团队士气会有致命性的打击，管理阶层要尽量避免这种事情发生，所以正式版本的发行并不仅仅只是一个人的事情，所有研发团队的成员必要时都必须配合。

版本发行是一件必须细心为之的事，除了工程师修改程序必须更加小心之外，做版本的人一定要知道新版本和目前版本的差异，修正了哪些bug?新增或调整了哪些功能？修改的程序有哪些？等整个项目中的程序文件这么多，而且是由许多工程师同时维护或修改，只要其中一个疏忽，就可能没把某些最新的程序整合进来。而且负责做版本的人必须对系统架构及所有的程序文件有基本的概念，即哪些功能应该与哪些程序文件有关。最常发生的事情是：某位工程师宣称其修改了某个bug，却没有将其加入版本控制的server，做版本的人必须对这种事情很敏感，要积极主动询问负责解bug的工程师。否则新版的程序没被编到新版本，这个版本等于白做，整个版本发行的流程还要再来一次。”

菜鸟：“我知道版本发行必须小心翼翼了，您可以带着我做这次的版本发行吗？我想知道版本发行要做哪些事。”

系统老鸟：“发行版本基本的流程是：

■　通知所有可能修改程序的工程师即将制作新版本，令其在某期限内将修改的程序check in到程序版本控制server上。

■　令工程师填写新旧版本间的差异点，包含修改过什么程序、要解决什么问题、Side Effect评估等，并由负责发行版本的人列出新版本的重大修正项目。

■　暂时关闭版本控制server，并从server取得最新版本的程序。

■　重新build整个项目，如果其间发生任何错误，可要求任何相关人员立即提供协助，让版本可顺利发行是这个时间点最重要的事，所有项目人员都要全力配合。

■　将build完成的Binary File烧录或下载到实体机器，并于短时间内测试完所有基本功能。

□　硬件相关特性测试，如系统耗电流、LCD Frame Rate（画面更新频率）[1]、使用键盘时的反应速度与反馈力道等。

□　应用程序的基本功能应该快速地全部操作一遍。

□　基本的系统性能测试，这也是使用者层次的测试，事前先定义一些较具代表性的操作方式，每次发行版本之前都应该测试一下，观察其性能是否有变化。

□　执行工厂专用的自动测试程序[2]。

■　编写Release Note，其中包含：

□　项目名称。

□　版本名称。

□　日期。

□　发行文件明细：包含文件名称、用途、文件size与checksum等（如果版本是通过网络上传/下载，难免会出现错误。例如，下载错文件或者利用FTP下载时发生错误。实际上不管版本文件是通过网络、光盘或其他任何方法传递，发行者都应该为每一个文件计算checksum，并详细记录在Release Notes中，如图19-1所示。当使用者要使用这些文件之前，一定要先算一下checksum，确定是否与Release Note内记载的相同）。

▲ 图19-1　使用WinMD5计算文件的checksum

□　新旧版本间的重大修改列表。

□　ROM的checksum值。自动测试程序中会包含计算ROM checksum的工具，这是用以验证目前这台机器中，ROM里的程序与数据是否确实属于某个版本的唯一工具。无论工厂生产，或者测试人员安装新版本，第一个动作一定是验证checksum与Release Note中记录的checksum是否相同，否则，在没有确定是否安装或烧录的正确版本前[3]，所有的测试动作可能都是没有意义的。

□　如果客户有要求的话，可能还要附上测试报告或bug分析报表等数据。

■　正式发行——应尽可能通知所有的关系人。

■　版本备份——现在大容量烧录设备相当便宜，应该尽可能的把目前状态备份起来，包含：所有程序与数据、版本控制server数据库、技术文件等。

■　Bug管理server上增加新版本名。

■　重新开启版本控制server。”

菜鸟：“发行一个版本怎么这么麻烦，整个步骤做下来恐怕要一整天吧？”

系统老鸟：“你这种心态来做这件事情是不对的，如果只是把系统重新build一下，Release Note拿以前的来随便改一改，那么，出一个版本并不会用掉太多的时间，但这样做很可能会为以后带来更大的麻烦。单单客户或老板要你报告为什么这个版本突然变得极不稳定？工程师宣称已经改好的bug为什么还是会出现？这些问题就够你头大了，如果测试人员把你敷衍所做出来的版本进行正式测试，测试报表肯定会叮得你满头包，所以还是应该把版本发行当作一件十分严肃的事情，团队中由上到下都该在版本发行的期间尽力配合。”

19-1-2　正式版与内部测试版

菜鸟：“但您也说过，有时候客户会有诸如‘马上出个版本让我看看效果！’、‘下午三点前我要开始下载新版本’、‘早上的版本有个bug没改，中午前马上再出个版本’等要求，我们先不管合不合理，为了要应付这样的要求，岂能容许您慢慢测试才发行版本？”

系统老鸟：“我们倾向不把这样的版本当作正式版本，否则测试人员的步调会被严重地打乱，如果客户可以接受的话，我们会称这个专为某人需求所制作的版本为‘评估板’、‘内部测试版’等。它只供需要的人评估使用，所以Release note也无须通知太多不相关的人，以免制造混乱。既然不是正式版本，当发现bug时可以口头、mail或以Bug Sheet告知工程师，切记不该填入bug管理系统，否则真的会后患无穷。

其实这种状况很多，不仅仅只是来自客户的要求，在做性能调整时，当有了一定的进展后，我们会先做一个内部版本，要求测试人员在这个版本上，全面性地测试这样的调整对整个系统有没有Side Effect。这样的版本当然不会是正式的版本，甚至程序还可能会show出一些调试信息，如果把这种版本的所有bug都登上bug管理系统，那么，接下来肯定会天下大乱！”

菜鸟：“也就是说，只有正式版本会经完整的测试流程，会有完整的Release Note与备份，也只有正式版本才有机会成为试产或量产的版本，对吧？其他的版本就如同工程师自己做的版本一样，只是单纯的用来评估、测试与调试。”

系统老鸟：“我有个经验是这样的。我们出了一个内部测试版给‘猴急’的客户，结果客户用了两天觉得这个版本‘很不错’，竟然指示‘就用这个版本量产吧！’，我们收到这个mail时简直当场傻眼，这种未经严谨测试的版本怎么可以当作量产的版本？根本没人知道‘很不错，是一个什么样的质量等级？”

菜鸟：“既然客户说要出就让他出嘛！反正是他做的决定，有问题由他负责！”

系统老鸟：“同学！社会是很险恶的，你说的未免太天真了！如果就用那个版本量产上市，等到有客诉时，所有人只会说RD team做出来的东西质量太差，测试工作不力等，到时那个决定量产版本的客户代表绝对就是骂得最大声的那个人，所以当时我要求PM老大和客户据理力争，就算要用这个版本，也应该经过完整测试后才能投入量产。”

菜鸟：“结果呢？客户应该不会这么不讲理吧？”

系统老鸟：“客户同意测试完才能量产，但必须压缩测试的时间，原本一个星期的测试要在两个工作日内做完。我觉得很奇怪，最关心质量的应该就是客户本身吧！怎么反而他们会常常做出一些破坏质量控制的决定呢？我知道他们有进度的压力，但让质量未受管控的产品上市，只会让公司的形象承受更大风险而已。当然我不能一竿子打翻一船人，有些客户代表还是很有sense的。”

19-1-3　版本号名称规范

系统老鸟：“所谓‘版本’是指软件系统所有程序代码在某个时间点的snapshot，利用版本控制软件，我们可以把开发期间，工程人员对程序代码的每个改变都记录下来，而每个改变都可算是一个版本。

能够‘发行’的版本必然具有代表性（如完成了某个重要功能或里程碑），或可用于特定用途。例如：对客户展示prototype、试产、量产等。对这些特殊的版本，我们当然要给它们取个‘名字’，称之为版本号（Version Number，但实际上版本号并非只有‘Number’），方便大家识别多个版本间的差异。”

菜鸟：“在为版本取名字时，有什么国际标准或行业间习惯用的规范可参考吗？”

系统老鸟：“版本号必须能让项目成员见名知义，所以对不同性质的项目，可能必须采用不同的版本号命名规范。例如A公司使用B系统，分别帮C、D、E公司开发产品，则其版本号可能会是B-D-0.1或B-E-2.0等。所以版本号的命名规范与项目特性相关，并没有什么标准，但所有项目都必须明确定义自己的版本规范，让所有项目成员都知道其代表的意义。

在设计版本号命名规则时，必须考虑到3种人。

■　客户或end user：版本号必须能告诉客户，和上一个版本相比，这个版本究竟有了多大的变化，是修复了几个无伤大雅的bug、新增了一个小功能，还是新增了一个大功能，或是对系统架构做了比较大的修改？客户可据此自行判断使用哪个版本风险较小。

以下是一个版本号定义的范例，用3个字母a、b、c代表一个版本，各字母的意义分别为：

a：产品世代

b：有新增功能或完成一个里程碑

c：小修改（bug修改）

我们还可在数字上做文章。举例来说，使用Linux的人都知道，kernel版本号若是偶数，表示这是稳定版；反之，则表示这是测试版本，可能有较大改变的功能正在开发中。所以想体验最新功能的工程师，自然会使用奇数的版本，而一般使用者则会使用比较稳定的偶数版本。

此外，我们可以使用诸如Alpha-1.0.2、Beta-0.5或RC-0.5（RC是Release Candidate的意思），这样的命名可用来表示开发进度、开发状态或质量属性。

■　测试人员：同一个版本可能会不只一次送测，所以测试人员必须能从版本号来区别这些版本。最简单的做法就是在版本号上再增加一位数字编号，表示这个版本的第几次送测，或者直接在版本号后直接增加送测日期。

版本控制软件会为程序每个时间点的snapshot指定一个编号，任何时间只要指定这个编号，就能取出该时间的所有程序。有时工程端送测时会把这个编号加在版本号后面，如0.9.8-build1234，其中1234就是表示版本控制软件中的编号。测试人员可根据编号自行取出所有程序代码，自己build并下载，然后才开始测试。这样能避免系统工程师用来出版本的程序与版本控制软件里的版本不一致，使得测试结果不具意义，而且很可能最终会导致某程序的版本遗失。

■　开发团队内部成员：开发团队常需要制定许多不对外发行的内部版本，代表某个里程碑达成，或表示从这个时间点将开始对系统进行大修改，必要时仍可‘退回’这个版本。这些版本通常都有技术上的意义，所以很难用数字表示，一般的做法是利用版本控制软件的功能，为某个时间打上诸如‘系统主功能完成’、‘内部测试版本for final release testing’、‘动态存储器配置算法大改开始’的标签（Label）。”

菜鸟：“原以为版本号只是为了可以很快地从版本控制软件中，找回某个时间的点的所有程序，没想到要注意的事情还真不少！”

19-1-4　标准版与客制化版

菜鸟：“这个项目还没结项，听说老板就已经想要扩大战果，开始跟其他潜在客户谈相似性质的产品开发项目，而且近期成功的机会很大，看来两个项目的时间与人力难免会有重叠。

因为我们在系统架构设计时下了不少功夫，要leverage目前项目的成果当然没问题，但新客户势必会要求一些与目前项目制造差异化的新功能，若两个项目同时开发，又要共享一个source tree，在这种状况下还要作版本管理，似乎颇为复杂。”

系统老鸟：“在此我们又可体会到完善的系统架构设计所带来的好处。现在这个项目，虽然一开始只是某个厂商的产品开发项目，但我们仍坚持在做系统设计时必须导入SDK的概念（请参考《第6-7-1节：为什么嵌入式系统开发团队必须维护自己的SDK?》），目前本项目虽然尚未结项，但SDK基本上已经稳定。而SDK存在的目的就是可以支持多个项目在上面开发，所以要用目前系统同时支持两个以上的同质性项目开发，问题并不大。

若我们把第一个项目的成果当作‘标准版’，那么，新项目中新增的功能就是‘客制化功能’。SDK在开发时，当然希望尽量能满足某个既定规格范围内的所有客户需求，最好是SDK开发完成后完全可以不需修改，只要开发并整合新增的应用程序与driver，就能够用来支持多个客制化项目。

简单地说，我们希望在执行客制化项目时，SDK的所有程序能被冻结（Freeze），假设同时有10个客制化项目在执行，我们不希望SDK必须为了每件客制化项目去改code，使得SDK的程序内充满了如下的程序代码，一来SDK的测试动作必须全部重来（只要有人改过任一行code，系统质量就会逐渐失去控制），二来程序的可读性对也荡然无存！

程序代码19-1

这就是为什么系统设计时必须分层，把将来可能需要客制化的功能放到Driver、Middleware或Application Layer中，使得System Layer在完成后保持不动，系统能否重复使用，是否可以不断的用来执行客制化项目，成败的关键绝对是系统架构。

有了对的系统架构后，更重要的是必须将系统架构设计，确实反应到source tree的目录结构里，如图19-2所示。”

▲ 图19-2　标准版/客制版的系统架构与source tree结构

菜鸟：“我听PM老大说过，公司以前执行过一系列以8051为主控IC的项目（请参考《第6-8-1节：案例研究（I）——8 bit MCU+NAND的系统架构》），因为第一个项目（或称标准版）的系统架构没有考虑清楚，导致后续客制化的项目并不容易执行，整个系统的程序也因此被改得乱七八糟，随着项目越作越多，客制化的困难度反而越来越高[4]，项目成员士气低落，情况眼见不可收拾，最后终于不得不决定重新设计系统架构。

我从书中看到一句话可以用来描述这个状况，第一次成功可能或多或少有点运气成分，能够第二次成功才是真正的令人佩服！我们做系统开发的，要让第一个产品顺利量产，只要项目成员够认真拼命就可以做到。但要使用同一个系统，让第二个、甚至第n个项目都可以顺利量产，并且维护人力越来越少，那才真能显现系统架构师的功力。”

19-02　自动测试程序

系统老鸟：“我已经把刚build好的Binary File下载到机器中，要请一些工程师帮忙测试一下基本功能，你帮我执行一下自动测试程序，等它运行完后，让我知道有没有NG的项目。”

菜鸟：“请问……啊……那个……自动测试程序……该怎么进去啊？使用者手册里好像没提到耶。”

系统老鸟：“原来你不知道我们产品的‘密技’怎么按啊！我来教你。这就像网络上常在讨论如何进入某支手机的‘工程模式’一样，例如，进入某大厂牌手机工程模式的方法为：

在待机时，按下以下按键组合：右*左左*左*（左右是方向键的左右，*是最左下角的按键），输入完毕后，就会出现工程模式的隐藏功能选项。

因为电子产品最终还是要进入量产阶段，在生产线上除了组装之外，还必须逐一测试硬件特性与产品基本功能是否正常，通常一台机器必须通过许多‘关卡’的‘考验’后，才会被允许出货。因此，软件工程师必须准备这些生产线上会用到的测试工具，但这些测试功能并不属于产品的规格，绝对不能让使用者用到这些功能，所以必须设计要用‘密技’才能进入的‘工程模式’。”

菜鸟：“我本以为所谓的工程模式都是工程师为自己开的后门，除了测试的用途外，将来在求职时还可以证明这台机器自己经手过，没想到原来还有这样的用途。”

系统老鸟：“有的工程师的确有这样的习惯，喜欢在机器中留个后门，进入后可以show出他的名字或照片，客户当然不会允许这种事情，但我们身为主管的在客户没发现的前提下，通常都会睁一只眼、闭一只眼。

但用于工厂的自动测试程序可不是随便写写就好，也是有需求规格的，通常这个程序的规格会由主管生产事务的工程师，与软、硬件开发工程师讨论后制定而成，这个程序的使用者是工厂生产线上的操作员，而非一般的消费者，所以操作模式与使用者界面势必与产品的应用程序本身大相径庭。”

菜鸟：“我可以想象这个自动测试程序的重要性，但我不懂的是，为什么您要强调生产线上的操作员和一般使用者是不同的？”

19-2-1　一般使用者vs生产线操作员

系统老鸟：“首先，现在电子产品的生产基地多半在中国大陆，通常工厂内操作员的学历都不高（这么说并没有任何诋毁或看不起操作员的意思），如果我们的自动测试程序上写着一堆英文，要操作员按照程序去做，你想会发生什么事情？第二，操作员与一般使用者或测试人员不一样，他们只负责组装验证，也许他们根本就不知道产品的用途是什么，有时他们今天负责打印机组装，明天就被调到手机测试，我们怎能要求他们必须对产品具备基本的了解？所以用于生产线的自动测试程序，操作方式应该尽可能的直觉，即便工程师觉得是common sense的东西，都不应该假设操作员应该会知道，基本上最好根本不要有任何的操作，程序就会自动的逐项测试，操作员只需要记录是否有NG或第几项NG即可。

你应该还没到工厂生产线上看过吧？操作员的工作相当枯燥乏味，几个小时内重复地做同一个动作。根据经验，如果我们的测试结果是一堆的数据，然后要让操作员自行判断测试结果OK或NG，他们疏忽或犯错的机会很大。通常就是让NG的机器‘流’到下一个测试点，你知道这样的小疏忽会造成怎样的大麻烦吗？通常工厂的流程是：硬件板子（PCB）经过SMT与基本加工后[5]，在尚未组装前会先测一次，等机壳组装完成后，会再测一次，在出货之前，客户代表会来验货，并抽一定的比率再测一次。如果因为操作员的疏忽，让为数不少的NG机器流到下一站，因为无法证明哪些是因为操作员的疏忽所导致，如果碰到比较没品的工厂主管，他们会说原来在第一站OK的机器，经过组装者上测试架之后就NG了，所以一定是结构、电子甚至软件不稳定所导致。为了避免产线的良率太难看，在找出问题之前，相关产线必须暂时停工，以后各单位就有得吵了！”

菜鸟：“所以您的意思是测试结果应该简单明了，即便操作员精神不佳也不至于放过NG的机器，最好的方法就是在LCD上显示又大又明显的OK或NG字样。如果没有显示设备，也可以用不同颜色的LED显示结果，如红色NG，绿色OK。我觉得让NG机器不小心出货到使用者手里才是最严重的事情，各单位都应该协助防止这种事情发生。”

系统老鸟：“你说的没错。举例来说，自动测试程序内一定会有个算ROM checksum的功能，如果它只是把待测机器的checksum值显示在屏幕上，要操作员自己去检查的话，那出错的机会一定很高，因为checksum的值只是一串没有意义的16进位数字，只要稍一走神就可能把NG的机器放过了。操作员不需要这些信息，他们只需要关心OK或NG就可以了，所以测试结果应该就只有OK或NG两种可能，如果这样操作员还会犯错，那就是工厂的管理与纪律的问题了。

此外，在自动测试程序中，使用诸如‘步骤一’、‘步骤二’，或‘测试一’、‘测试二’这样的功能描述，显然会比‘Keyboard Testing’、‘SRAM Testing’等要好，过多的英文专有名词显然只会造成操作员的混淆。常有不了解生产线状况的软件工程师，在实作这个自动测试程序时，很贴心地花了时间把操作界面设计成如Windows程序般‘友善’的风格，殊不知越具弹性的操作界面，越不适合给工厂的操作员使用，最后还是会被要求修改回‘简约’风格。”

19-2-2　自动测试程序的功能

菜鸟：“没想到功能日趋复杂的电子产品里，居然会藏着一个‘反璞归真’的测试程序，刚刚说的是操作界面的注意事项，但它的实际功能是什么？”

系统老鸟：“自动测试程序又称为自我测试程序（Self-Test Program)，主要目的是测试各个硬件功能的正确性，理想的操作流程应该是当机器上电后，操作员使用某种特殊的手法（即上述的‘密技’）进入自我测试程序后，系统自动开始执行每一项硬件测试，等全部执行完后，通知操作员OK或NG即可。总体来说，操作员没有必要了解他究竟测了什么功能，但负责实作自动测试程序的软件工程师，必须清楚了解各个测试的细节，通常自动测试程序应该包含以下功能。

■　各个硬件功能测试，硬件设计中的重要组件IC与周边设备都应该要测试，确定其可正常运行。

■　确认RAM读写功能OK。

■　计算ROM的checksum。

■　基本的CPU与存储器性能测试。

许多硬件的测试还是必须让操作员介入，如键盘测试，总要每一个key都按过一次才能确定OK；LCD总要有人看着，才知道显示的内容是否合乎预期；音乐或语音播放的功能，必须要有人听着才知道播放结果是否正确……”

菜鸟：“所以完全自动只是理想，不需要操作员介入几乎是不可能的啰？”

系统老鸟：“没错！但自动测试程序是生产阶段为硬件质量把关的重要工具，这个工具的设计理念应该站在操作员的角度，‘协助’操作员从成千上万流过他手上的成品或半成品里，把瑕疵品挑出来。”

19-2-3　自动测试程序会影响产能与组装成本？

系统老鸟：“最后，在实作自动测试程序时还有一个注意事项，那就是性能（Performance）。”

菜鸟：“应用程序注重性能表现，是因为考虑到购买这个产品使用者的感觉，但自动测试程序的使用者是生产线上的操作员，程序本身的效率有这么重要吗？就算慢一点，我想操作员也不会介意吧？”

系统老鸟：“操作员是不会介意，但老板或客户恐怕就不同了。自动测试程序的性能直接影响到测试一台机器所要花的时间，要记得，产品的量产目标往往是几千台或几万台，假设量产目标是五万台，而一台的测试时间比预期长了一分钟，则做完五万台机器要慢上五万分钟，你知道五万分钟是多久吗？是34天，约一个多月！

当然我们可以多开几条生产线，以增加产能。但你知道多开生产线代表什么意思吗？代表生产成品增加，这个成本应该转嫁到谁身上，恐怕还有得吵的。所以自动测试程序测完整个系统的时间要尽量缩短，最常见的问题发生在大容量存储器checksum的计算，一次算一个Byte的效率显然会比一次算4个Byte要差。此外，所有硬设备的测试方法应该要经过设计，最好是简单又具代表性，只要执行过这个测试程序，就可以确保该硬设备是OK的。”

菜鸟：“自动测试程序没写好，居然还会降低产能或增加生产成本，这倒是我始料未及的，看来制造生产管理的学问还真不少，不是我这个软件工程师所想得那么简单。”

系统老鸟：“开发嵌入式系统的软件工程师有许多接触到其他领域的人和事物的机会，千万不要固步自封，不要放弃对产品开发的流程有更深入了解的机会，如果将来你想更上一层楼，例如，像我们PM老大一样管理整个电子产品的开发项目，到时电子产品开发流程？有哪些单位？负责哪些事？工作文化是什么？假如这些事情你都不知道，那么工作该如何展开？资源如何分配？发生冲突如何协调？也许你仍然是个很优秀的软件工程师，但有哪一个老板敢把管理的事情交到你手上？如果有机会与硬件工程师合作、拜访客户、Third Party，或者到工厂解决问题，记得千万不要排斥，多去看看其他公司、其他领域的工程人员、其他国家的人是怎么做事的，开放视野，绝对有利无弊。”

19-03　决定量产版本

菜鸟：“据表制定的schedule，正式量产（Mass Production；简称为MP）之前的小量生产（Mass Testing；简称MT，或Pilot Production；简称PP）就要开始了。虽然最近陆陆续续还找到一些bug，但似乎都不是什么严重的问题，我觉得我们目前的版本很OK了，为什么还不决定最终量产版本？而且我记得看过我们公司质量系统的准则，其中提到除非碰到Critical Bug，否则量产前的几周就不该再更改程序，以避免无意间制造的Side Effect。”

PM：“那你觉得最终量产用哪一个版本应该是由谁决定？”

菜鸟：“不是应该由您这个项目经理，咨询QA、RD与测试人员的意见，再跟客户报告后就可以决定的吗？”

PM：“答案只对一半。理论上，应该由客户端的QA决定什么版本适合量产，因为‘理论上’只有他们最了解自家公司产品的质量要求。至于开发端的项目经理，也就是我本人，的确该收集相关信息，并做出报告与建议，但最后决定权并不在我手上。我看着我们RD兄弟们近来被这些鸟bug搞得灰头土脸，天天做版本，而测试人员则天天熬夜验证新版本，所有人天天做着对产品质量并无实际益处的事情。如你所说，最近发现的bug几近吹毛求疵了，就算不改也不会影响使用者的感觉，如果是我，早就决定量产版本了。

但话说回来，我碰过各式各样的客户，各有不同的考虑，有的重点是质量，有的重点是上市时间（Time To Market）。我举另一个极端的例子：我们团队中有自己的质量系统，如果时间充足的话，一定要经过严格的测试后才能当作试产或量产的正式版本，绝对不允许存在任何未知的质量问题。简单地说，这个版本有bug没关系，只要我们能掌握是哪些bug还未修改（或没必要修改）即可。我做过一个case，当系统还有几个Critical Bug，而且测试工作尚未告一段落之前，客户已经急着要我们开始量产出货了。对这个产品来说，销售地是第三世界国家，客户认为商机比质量重要，我们也不便多嘴。但为了保护自己，我们会在量产通知书附上目前版本的已知问题点，并要求客户签名，否则将来出问题，研发团队名誉是小，客户很可能因此对公司求偿。”

菜鸟：“我懂了！质量是一个相对的概念，并不是绝对的，我们的标准觉得OK，但对旁人来说可能还觉得不够，反之亦然。有许多关于软件或产品质量的国际标准阐述了如何实现高质量的思想与方法，这是开发团队努力追求的目标，也应该是客户品管人员最重视的事情，但市场状况瞬息万变，再加上品管人员专业素养的优劣，都是影响最终产品质量的因素。无论如何，使用者的感觉应该才是最重要的，对吧？”

PM：“以产品的角度来看，使用者的观感最重要，使用者可能为了一个功能而忍受其他的小瑕疵，但这些分析工作应该是客户产品设计或市场营销人员的责任，对我们开发单位来说，就是照着规格做事，有时候我们会提出一些建议，但是否采纳则由客户端决定，因为产品的销售区域可能与开发团队所在地不同，当然会有文化上的差异。有些开发团队觉得很不实用的功能，也许是客户市场营销人员针对销售地区所想出来的创意。”

菜鸟：“说了半天，使用哪一个版本当作量产的版本还是得等客户决定啰！”

PM：“谁叫我们要赚人家的钱呢？如果我们的产品是使用Mask ROM，也许就没有这些问题，因为Mask ROM开光罩要时间，而且除非花钱重做，否则光罩开了就不能改，这种情况下，最终版本当然在量产开始前几周就必须决定。但为了平衡开光罩所带来的进度与质量风险，第一批量产机器采用可重复读写的flash已蔚为风尚，原本是为了降低风险，结果却好像为客户的品保人员开了一扇后门，有些专业素养不够的品保人员心里是这么想的：

‘反正不用开光罩的时间，那么，在原定烧录日期开始之前，RD应该尽量把所有的bug都解决，bug越少，质量越趋完美！’

‘Bug越少，质量越完美，这句话基本上没错，但在系统质量已趋于稳定时，解了一个bug，却可能制造其他隐藏的bug，而且这些Side Effect可能隐藏在之前已经测试无误的功能之中，此时，已没有人可以保证系统的质量，除非重头再测一遍。我看过最离谱的品保人员一定要RD和测试人员熬到flash烧录的前一天，也就是说，flash开始烧录前一天的版本就是最终量产的版本。如果产品有更新程序的功能，最终版本将是产品出厂前一天的版本（成品机器一台一台更新），你说这样有何质量管理可言？原本该控制质量的人，反倒成了质量失控的元凶。”

菜鸟：“这种故事我已经听你们这些老鸟说了N百遍了，看来大家都是身受其害。RD与测试人员在这些项目该结未结的日子里等待量产版本的决定，现在大家的士气非常低落，难道这个时候管理阶层就没有办法做些什么吗？”

PM：“此时也只能和客户的品保人员好好沟通了。此外，你没看前几天我在会议桌上，当着项目大小主管和客户代表大小声？虽说‘顾客是上帝’，但偶尔也要据理力争一下。除此之 外，我也只能‘勤走基层’、多听听工程师们吐苦水、尽量多办些活动、承诺出货的那一天要盛大庆祝、做好基层主管的心理建设等。

但毕竟决定量产版本是客户的权利，合约规范的结项条件都很粗略，在此时并无法用以约制客户的要求，在项目后期，除了尽量沟通，我们也只能尽量满足客户的需求，谁叫出钱的是老大呢！”

菜鸟：“您不会想告诉我选择RD这条路是错的吧？”

PM：“我年轻时也想过这个问题，后来我的答案是：我的工作是产品研发，而非仅仅是一位只会写程序的软件工程师。如果你能这样想，你就会用产品的角度来观察这些看似不合理的事情，如果哪天换了工作，你要帮公司管理一个电子产品开发的外包项目，此时你变成客户代表了，必须顾及公司的利益，你会如何处理量产版本的问题？”

19-04　出货≠结项

菜鸟：“好不容易客户签了量产同意书，虽然现在第一批产品已经上市了，但客户还在追着许多未解的bug，执意要我们尽快全部修改完，并发行一个正式的‘Running Change’版本，看来客户还没有想要结项的意思吧！”

PM：“是啊！我早有心理准备了，我从来就不认为出货等于结项，有时候产品上市后才是恶梦的开始，再加上原来的项目成员可能已将心力投入另一个项目，此时你其他回来帮你解bug、出版本、测试等，几乎是不可能的事情，既然工程师们的心已经不在这个项目，此时修改程序的风险相对更大。除非使用者发现重大的瑕疵或bug，否则既然使用者对第一批产品的质量都没意见了，我们何必再去做无谓的修改，承担自找的风险？所以我会建议客户尽量不要做什么‘Running Change’版本。’

▲ 图19-3　项目不同阶段人力与成本投入示意图

19-4-1　Running Change版本

PM：“如果使用者对现行产品规格有意见或建议的话，我们应当严肃地告诉客户，这些新功能的规格已经超出原本项目的范围了，如果确定要修改的话，应该要重新发起一个项目，即便这个项目的范围相对较小，所需时间与资源较少。如此一来，项目经理才可（才有理由）在组织内正式召集项目成员进行修改。这才是正确的方法，我们不该漠视产品出货后组织成员解散的事实。”

菜鸟：“客户会那么配合和公司再开一个开发项目吗？我觉得我们老板一定又一口答应人家：‘没问题，我请我们工程师帮忙改！你直接去找某某某，就说我交代的’，到时您去哪里找人帮您做这个Running Change版本啊？就算做好了，谁要帮您测呢？”

PM：“唉〜项目范围应该是一件很严肃的事情，但有时就如同你说的，项目范围就在人情压力下莫名的被扩张或修改。有的客户会按照规矩来，多做事就该多给钱，再开启一个开发项目是应该的，但大部分的客户心态都是‘顺便改一下就好了，何必还要弄个什么新项目？’，老板也会要我们秉持着服务客户的精神，帮忙改一下，可是整个项目几乎只剩我一个人在收尾，最后还不是要卖我的老脸，去拜托现在负责工程师与测试人员的项目经理，借调这些人力几天，最怕他们离开这个项目已久，还要花时间熟悉，再加上心不在此，这样修改出来的程序实在令人提心吊胆。

如果碰到老是喜欢搞‘Running Change’这套的客户，在项目开始之前，我会建议最好可以顺便签下维护合约，将预估的人力与工时一并计费，如此一来，项目经理才有理由在产品出货后，还能把重要的项目成员留下，这样对产品的质量也比较有保障。”

菜鸟：“站在公司的角度，公司怎么可能任由重要的项目经理人才卡在一个不知何时才能结项的项目中？现在老板不是已经要您开始study另一个开发项目了吗？”

PM：“照理说，项目经理应该撑到项目结项，然而偏偏实际上就是有些怎么也结不了的项目，一般的做法是项目经理带着这个项目的班底开始进行下一个项目，如果‘前一个’项目有Running Change的需求，因为新项目仍属于初始阶段，而原来项目的key man都还在自己的麾下，此时，调派部分人力‘应付’前项目的需求阻力会比较小。我觉得这是兼顾公司实际状况与客户关系的做法，就算有点对不起客户也没办法了。”

19-4-2　CostDown版

PM：“除了Running Change版之外，量产之后，还有一种叫做Cost Down版的东西。和之前讨论的一样，如果合约没谈好的话，这种额外的开发需求都应该开启新项目。”

菜鸟：“所谓的Cost Down版是不是就像PS3的机器，第一个版本是体积很大的机器，几年后，Sony才出了体积轻薄短小且售价比第一版上市时还便宜的第二代。”

PM：“有点那种感觉，但第二代的PS3机器几乎是一个全新的产品，和我们说的Cost Down版不太一样。所谓的Cost Down版，是在产品功能和外型完全不变的前提下精简硬件设计，如flash换成ROM，选用较便宜的替代组件，以达成降低造价与成本的目的。通常硬件设计人员会设法找到完全兼容的组件，此时，软件通常仅需微调（不同组件的Timing可能不一样）与测试。如果找不到完全兼容的组件，势必得选用不兼容的组件，这代表着某些驱动程序必须重写。

无论如何，既然大幅度更改了硬件设计，所有量产前的程序应该都要再来一次，例如试产、硬件测试与环境测试、软件测试、发行版本等。所以我说应该重新启动一个项目来做这个Cost Down版本并不为过吧！试想，如果一台机器造价可以省下一块钱美金，如果出货量是百万台等级的产品，此举可以帮客户省下100万美金，从中分一些出来当作Cost Down版本的开发费用还是很值得的。”

19-4-3　出货之后：你以为就结束了吗？

菜鸟：“除了继续Running Change、Cost Down等，这些版本开发之外，嵌入式系统开发的周期在出货之后算是结束了吗？”

PM：“产品出货之后应该进入维护期，理论上，开发工作算是结束了，但实际上并没这么好运，总是会有一些料想不到的事情发生。

■　客诉[6]：所谓的客诉是使用者购买了某个产品后，使用上发现了重大的瑕疵或致命的问题，使用者可能投书、上公司的官方网站，或在网络的讨论区陈述自己的发现与不满。通常使用者不会为了小问题而大费周章，且客诉影响公司品牌形象甚巨，如果不好好处理，可能会严重影响产品在市场上的接受度与销售量。当客户的产品接到客诉时，势必会要求研发团队进行分析与解释，这件事情如果不好好处理的话，就会变成客户对我们研发团队的‘客诉’。所以客诉事件必须优先处理的工作，此时，项目经理一定要把相关工程师借调出来，找出问题所在，并视问题的严重性与客户的需求，决定是否要发行新版本。

■　组件涨价、停产或买不到货：有的存储器IC或电子组件就如同期货一样，价格时时在变化，当价格涨到不敷成本时，客户就会要求硬件工程师寻找替代方案（Second Source），如此一来，无可避免的也会冲击到软件。我也曾碰过某个IC因为单价比较贵，工厂备料不足，结果量产后不久市场即缺货，导致我们产品生产线可能面临停摆的窘境，此时，客户也会要求尽快寻找该IC的替代品，同时软件也必须对应并发行版本。严格来说，这些都是预期之外的状况，通常研发部门都会尽量帮忙。

■　量产时不良率偏高：我已经遇过好几次这种状况，有大批成品已从工厂出货了，生产线的不良率都维持在可接受的范围内，突然在某一时段内的不良率突然飙升，生产线因而停摆，客户当然要求调查。如果一切条件都没变的话，最简单的推论当然就是软件不稳定，这句话几乎是工厂拖延时间或推卸责任的万灵丹。生产线停下来是极为严重的事情，于是相关工程师被召集到工厂分析问题，虽然我们都知道，同样的软件不太可能在生产几千台之后才爆发出问题来。

最后果然都不是软件问题，有时候是工厂换新的操作员，不熟悉某个步骤的操作[7]。更常发生的是‘不诚实’的工厂，自行决定更换兼容但较便宜的组件，因为客户不知情，软件工程师也不知道，当然没做过严谨的测试，会发生问题自然也是意料之内。”

菜鸟：“但您刚刚说过，基于公司的立场，当产品出货之后，所有项目成员会逐渐离开去加入其他的项目。在这个情况下，我们哪里有人力应付这些突发状况？”

PM：“因为这些都是紧急状况，即便最后证明不是软件或固件的问题，在取得老板与其他项目经理的谅解下，我们都会帮客户解决这些问题。更何况我不是说过了吗，新项目的班底都是老面孔，而且解决这些问题并不需要太多的时间，在我可以cover的状况下，我会调动相关工程师尽力尽快完成的。”

19-05　项目结项

菜鸟：“如果出货不算结项，那何时算才结项呢？我们团队的原班人马都已经开始另一个项目的开发工作了，前一个项目的客户好像还没有打算和我们结项的意思耶！”

PM：“是啊！真是麻烦，我们已经‘免费赠送’一个Running Change版了，现在这个客户还想要我们帮忙做Cost Down版，不要说我们没空，硬件工程师也早就分配到另一个项目去了。我现在请公司的业务人员和客户协调，虽然delay了一个多月，但我们已经确实完成合约中项目范围内的所有事情，就算客户暂时不想结项，我想业务人员也会尽快把钱收回来。

因为大部分客户总是想尽办法榨干我们的最后一点残余价值，就我的经验，真正结项都要在开发完成以及产品出货的n个月之后。当然也有很爽快的客户，但毕竟还算少数。项目经理总不能等项目成员都已经去做其他项目的事情后，才来收集项目中所有的技术文件、程序、会议记录与相关数据吧！到时会有人理你才怪。为了因应这样的状况，我会遵循项目管理教科书，把结项分为两个独立的步骤。

■　行政结项：对研发团队来说，项目的经验累积相当重要，所以项目经理一定要趁key man都还在时进行行政结项，所谓的行政结项最重要的工作就是收集并整理所有的文件、资料与检讨报告。我的做法是当产品出货后的一个礼拜，所有大、小主管什么事都不用做，就和我关在会议室中收集资料和检讨此次项目的得失，至于其他基层人员则‘在营休假’，只要配合他的主管收集资料即可。当所有资料收集完成、制作清单并备份之后，才可以分批解散项目成员，归还项目资源，如计算机、测量仪器等，直至所有文件归档之后，才算完成行政结项作业。

以嵌入式系统来说，行政结项时至少收集到以下资料。

□　技术文件之外的所有的项目基本文件，包含合约复本、项目计划书、预算书、成本分析、Schedule、WBS（Work Breakdown Structure）、人员组织图等。

□　产品规格书及其修改记录。

□　硬件相关图面，如电路图、Layout图等，最好从Target Board到最终量产的版本都有。

□　硬件设计规格书以及所有组件的data sheet。

□　自组件厂商处取得之所有技术文件与sample code。

□　所有与ID以及结构设计有关的图面与文件。

□　每一个版本的备份光盘。

□　量产版本的完整程序代码。

□　版本管理系统之数据库备份。

□　bug管理系统之数据库备份与各时期的bug状态报表。

□　所有与项目有关的mail与会议记录。

□　项目检讨报告。

■　合约结项：如果项目发起人（通常是客户）愿意进行合约结项的话，我们会安排正式的close meeting，并把行政结项阶段收集的所有资料准备好，然后‘恭候’客户代表前来验收。在这个会议中，双方可以讨论项目进行期间的缺失，以及客户对最终交付成果的看法，有些客户会要求对尚未解决的open issue或bug逐一解释，再次确认对产品没有严重影响后，就会将其close掉。根据客户类型的不同，close meeting有时很快就可结束，有时要耗费n天；有时只要项目经理或老板在就可以；有时则需要工程师列席备询。此时我们通常会逆来顺受，只求客户赶快在结项同意书上签名，并确认取得尾款的日期，最后还要请客户填写满意度调查表。合约结项完成后，这个项目到此正式结束。

项目管理的教科书告诉我们，应该要先完成合约结项，才可以进行行政结项。但对嵌入式系统开发项目而言，实际上真的很难等到合约结项，才开始做项目资料的收集与人员解散，到时所有人员早被分配到其他的工作上，再把他们集合起来做行政结项，也只是虚应故事罢了。

当然我也碰过半路腰斩的项目，通常是客户主动放弃委托我们开发，原因很多，可能是因为开发单位技术能力不行或进度严重delay，或客户发现产品定义错误或已过时，甚至是文化不和或合作不愉快等。此时就只剩下合约、开发费，甚至赔偿的问题，双方的法务人员自然会去协调，研发团队的所有人员当然会相当沮丧，但这也是没有办法的事情，提前结束总比‘不会结束’要好。”

19-06　期许下一个项目

PM：“你跟着这个项目，从头到尾做完了一个嵌入式系统的开发项目，有没有什么感想？”

菜鸟：“以软件工程师的角度来说，写嵌入式系统的程序并没有特别复杂，甚至我们现在几乎不写汇编语言了，我觉得最重要的是理解要正确，只要以下理解正确，一般软件工程师要跨入嵌入式系统开发领域并不困难。

■　彻底搞清楚计算机系统的运行原理。千万不要拘泥于某颗CPU、某个IC或某种类电子产品的操作细节或开发工具，要认清楚什么只是厂商的‘规定’，什么才是放诸四海皆准的思想、技巧和知识。

■　清楚嵌入式系统的特性。

■　系统架构的重要性。

■　模拟器的重要性与用途。

■　驱动程序其实并不如想象的困难麻烦、硬件的理解也不如想象中可怕。

■　即使是写应用程序，都该考虑资源的局限性。

■　不要排斥与不同领域技术人员的沟通与合作。

除了参与开发工作外，我更了解了一个电子产品从无到有的开发流程，以及嵌入式系统项目是如何运行的。相信这些对我以后的职业生涯肯定有很大帮助。”

PM：“是的！这是一个知识爆炸的时代，无论嵌入式系统开发、项目管理、质量管控或其他事情，重要的都是理解是否正确，这会直接影响你处理事情的方法与态度，至于实作的细节，用到的时候再去查就好了。这么说你该知道以后学习的方向与重点了吧？

无论如何，恭喜你从头至尾经历了一个嵌入式系统开发项目的生命周期，也许你还觉得自己很嫩，但这一年中所学到的嵌入式系统开发项目的相关知识，已足够让你应付新电子产品开发项目的挑战。我想以后不该喊你菜鸟了，也不该再让你当预备队，好好回忆项目进行中你曾问过的问题，复习我们这些老鸟给你的东西，以后我需要你帮我带新进的菜鸟，他们就如同你刚进来时一样，除了写程序之外什么都不会。”

菜鸟：“在嵌入式系统开发这条路上，虽然我已经拥有基本理解，但我觉得我还是需要多一点的实际经验，听别人描述问题的解答似乎都很简单，我很羡慕我们team里那些老鸟，随口就可以说出系统架构、驱动程序或项目管理上的道理，一眼就能看出到底是硬件、固件、系统还是应用程序的问题，我希望有一天能拥有这样的经验和解决问题的能力！”

PM：“一定可以的，加油！”

19-07　总结

本书已接近尾声，笔者试图用轻松的行文风格取代硬梆梆的技术书籍文体，希望用一个个实际经验与实例说明市面上嵌入式系统教科书没有提及的概念。行文至此，自诩不负当初对自己的期待。唯一遗憾的是，因篇幅限制，本书中有些实际上的细节只能稍作解释，例如嵌入式操作系统原理、信号测量的方法、基本的硬件设计概念、更深入的优化技巧等。此外，笔者还有相当多的案例与技巧无法加入书中，若有兴趣的话，可将笔者的Blog: http://tw.myblog.yahoo.com/embedded_system_book当成本书的延伸阅读。

嵌入式系统开发是个相当具有挑战性且绝对不会枯燥的领域，嵌入式系统项目管理肯定是将来最热门的职务，希望本书对您真的有帮助。

“加油！”正是本书的结语。

[1]　LCD Frame Rate表示LCD每秒Refresh的次数，产品的LCD Frame Rate设定视要销往的国家或地区而有所不同，因为每一个国家或地区电源的频率不同（例如日本和大陆是50Hz，而台湾地区是60Hz），所以导致电灯闪烁的频率也不同。如果电子产品中LCD Frame Rate与当地电灯的频率太过接近，则使用者看起来会感觉有严重的闪烁现象。如果产品要销往许多不同的国家，系统中势必有个代表Frame Rate的配置，因为开发者和使用者可能位于不同地区，如果做版本时没去调整这个配置，则做出的版本也许在开发地看起来没有问题，直到出货到其他国家才发现在室内完全不能使用。这是一个很容易出错的地方，所以在每次发行正式版本之前，都应该要测一下Frame Rate。

[2]　工厂专用的自动测试程序在《第19-2节：自动测试程序》有较深入的探讨。

[3]　有时候在客户要求或其他原因之下，研发团队会频繁的发行版本，这种状况下，其他人的确很容易混淆，安装了错误的版本，导致做出错误的分析。例如，在旧的版本或测试版本上找到的Bug只会制造混乱而已。

[4]　同一批人使用同一个系统，照理说应该越做越熟练才对，但本例因为系统架构不完善，导致系统程序越改越乱，使得新项目反而需要更多的effort。

[5]　硬件板子（PCB）经过SMT与基本加工后所有的组件已经焊上，只要通电就可以运行。

[6]　嵌入式系统开发团队在项目结束后会碰到的客诉来源有两种：一是委托该团队进行开发的客户（通常是工厂良率问题）；另一种就是在市面上购买该电子产品的终端使用者。

[7]　最常发生的就是烧录器操作出错。在工厂中，有时同一台烧录器要用来烧录不同产品的程序或数据，如果交替时选错了chip或Binary File，将这样的chip焊到PCB板上，以后的测试当然会出问题。

Appendix A

未执行项目管理的项目

A-1　前言

网上流传着许多嘲讽电子产品开发项目的寓言式小品，唯有亲身经历，才能引喻的如此丝丝入扣。显然这些戏谑之文讲得是业界普遍的状况，即许多开发项目空有“项目”之名，却没有用正确的方法与工具加以管理，没有有效的管理与使用资源，导致项目延迟、浪费成本，甚至失败。企业商机、商誉因此损失。

贵公司是否也正发生着以下故事描述的情形？改善方式非常简单。

■　教育项目经理知道如何管理项目。

■　教育所有项目成员，令其了解嵌入式系统开发的本质。

■　以上两项，由公司高层的教育开始做起。

A-2　说故事时闻（I）

在现实生活中，做项目哪来什么项目评估，都是大老板一声令下就开工，然后结局通常是delay、加班、又delay、再加班，可怜的工程师及测试人员被累得精疲力尽下终于把成品产出了。如果顾客要的是一台BMW跑车，最后能做出一台TOYOTA的车就很不错了，但最后把成品做成三轮车也是常有的事。

做成三轮车也还好，最怕的是做出了一只酷斯拉，还会反咬你一口。这时比较有担当的大老板就会扮演成咸蛋超人的角色，把这只怪兽给收拾掉；比较没担当的就会装死，但至少他会跟顾客吓嘘一番，把酷斯拉说成是外表凶狠没人敢开的新式跑车。更没品的老板则把项目做不好的责任，全推到项目经理或工程师身上，认为这个团队能力不足。在这样的文化下，老板怪项目经理、项目经理怪品保与测试主管鬼混，测试人员则说工程师写这什么烂程序，最后最精实的工程师只能哑巴吃黄莲，忿忿的把黑锅背起来，离开了这家公司。

不管大老板有没有担当，通常这组team的人下场都蛮惨的，不是被收拾掉，就是自己收拾掉自己（逐一离职）。然后大头目再招兵买马，产生新的team，生产新的跑车……喔！不是，是第二代的酷斯拉。之后的结局，当然是同样的事不断的repeat。

A-3　说故事时间（II）

船长：“各位船员，我们准备建造一艘超级船舰，前往金银岛，把岛上的金银财宝全部搬回来。我们预计在6月底以前抵达金银岛，12月底以前要把所有的物资搬回这里。”

船员：“报告船长，金银岛在哪里，有地图吗？”

船长：“地图我会想办法画出来，我们就先来造船吧！”

船员：“报告船长！这计划很赶耶！还要造船，还要找金银岛，那我们船的建造计划是什么？”

船长：“这个我们还在计划中，等计划出来就会跟大家报告。进度很赶，还需要靠大家的努力。”

船员：“报告船长！那我们准备做出什么样子的船呢？”

船长：“这个我们也还在计划中，等计划出来就会跟大家报告。肯定的是，这艘船的性能一定会很好，功能一定齐全，请各位放心。”

船员：“报告船长！根据工作的分配，有的人被分配擦窗户、有的人被分配扫地，就是没有看到有人被分配造船。那我们的船是准备自行建造呢？还是由公司的其他单位帮我们造？还是准备买现成的船来改建呢？”

船长：“这个问题很好，你提的非常中肯。”

船员：“报告船长！因为进度很紧，有没有考虑买现成的呢？L厂牌跟M厂牌的都很好啊！”

船长：“虽然L厂牌的所有原料跟设计图都是免费的，但是没有组装与后期维护的服务，’有点麻烦；M厂牌的又要花钱，公司可能会不答应。我们应该会自己建造吧！”

船员：“报告船长！我们自己建会有风险，因为现在还没有设计图，我们这群人又没有造船的经验，6月底就要抵达金银岛可能会有问题。之前看别人造船，有经验的要花一两年才有个雏形，我们没经验的要一年内不但要造出来，还要去把东西搬回来卖，应该有问题吧！”

船长：“这个问题我会找船东讨论讨论，你们就根据分配到的工作开始造零件吧！”

船员：“报告船长！因为没有船的实体，那造出来的东西要先安装在哪里测试？”

船长：“就先安装在你的汽车上测吧！”

船员：“那不就得在汽车上装个转换器，否则设计出来的东西就会变成符合汽车使用，而不是船使用的啊？”

船长：“我们的目标就是可以同时在汽车以及船上使用。”

船员：“那我们是不是应该要把这两边的规范说好，应该要怎样做才会可以同时两边共享？”

船长：“这个问题我没有想过，应该放上去就可以直接使用吧！你不要想那么多！”

船员：“问了半天，我们还是不知道到底要做什么东西，有没有基本的蓝图？”

船长：“不好意思，暂时没有！总之，我会想办法在一月底以前把蓝图弄出来。还有，虽然我们不是一言堂，但我们还是得讲究纪律，要你做你就做，否则当心我把你丢到海里喂鲨鱼！”

大副出来打圆场：“各位放心，我们船长研究过许多年的船了，有非常丰富的经验。跟着我们船长的脚步走，一定可以顺利地把金银财宝搬回来的！”

船员：“（听说船长研究的是遥控船……）我没有问题了！”

此时突然有一只乌鸦飞过……啊啊啊啊……

船长突然回头说：“大家不要紧张，我本身呢！是一个船长。这个黑色的鸟呢！其实是只海鸥。正如我刚才所说，我身为船长，常有黑色的海鸥从身边飞过，也很符合逻辑。”

A-4　结论

看到这里，你可以回头读读《第2章：嵌入式项目管理》，这是一个很普遍的谬误，更是许多项目经理的借口。既然title是项目经理，就表示其任务就是管理一个项目，既然身为管理者，就不可以任由许多不确定、不受控制的事情一直存在。软件工程师往往成为失败的软件开发项目的代罪羔羊，实际上，大老板的好大喜功、项目经理的管理无方才是罪魁祸首。

Appendix B

Callback Function

如果程序A中使用了某个function B，我们会说程序A调用（Call）了function B。假使某个系统模块的API要求你给传入一个函数指针（Function Pointer），而这个函数指针指到一个实际的函数（这个函数多半是你自己写的），然后系统模块会在适当的时间调用此函数，则此函数就是所谓的Callback Function。因为这个函数是被“callback”了（你可以理解为“回call”）。

这样的技巧广泛的被使用在系统与驱动程序中。以系统处理键盘事件为例，当某个key被按下时，keyboard driver会辨认哪一个key被按下了，并且把这个事件送给OS处理，通常OS在经过某种转换后，也会把这个key的信息存在某个queue中，最后才交予应用程序处理。

我们假设一种状况，user连续按下了A、B、C、D与ENTER共5个key，应用程序处理key A需要的时间较长，则其他4个key事件会被存在queue中，暂时无法被处理。若此时使用者等得不耐烦了，按了ESC想要跳出这个功能，则应用程序该如何对应？

在一般的设计架构中，整个硬件事件处理流程中，应用程序完全无法插手，而OS也无法定义ESCkey的意义是什么。所以在上述状况中，ESC key也会被存在queue中，要等到前5个key都处理完后，应用程序才知道原来ESC key已经被按下了。Callback function可以解决这个问题，让应用程序设计更有弹性。简单地说，应用程序可以先对keyboard driver注册处理ESC key的Function Pointer，则每当有key事件发生时，keyboard driver除了执行原来的流程外，在某个阶段就会去调用应用程序注册的function，所以ESC key就可以提前被处理。

请参考程序代码B-1、程序代码B-2和程序代码B-3所示的程序范例。

程序代码B-1

程序代码B-2

程序代码B-3

Appendix C

用C来实作面向对象的概念

近年来许多专家学者的研究指出，在嵌入式系统领域中采用面向对象的思想进行设计有很多优点。不过实际上，大部分嵌入式系统开发仍然以采用C语言为主，主要原因是有的平台没有C++编译器可用，有的产品则CPU运算性能比较差，无法承受C++所带来性能变慢的风险，而且C++编译器产生的code较无法掌握，始终是其应用在嵌入式系统领域的致命伤。

可是使用面向对象思想进行系统设计的好处，却总令人难以割舍。所以实际上通常还是会使用面向对象方法论进行系统设计，实作时则使用C语言来仿真面向对象中的各种思想。其实只要不是用到面向对象中太复杂的概念，使用C语言还是可以很优雅的实现面向对象系统。

这样的做法有一个基本的思想支持。基本上，嵌入式系统是一种封闭式系统，也就是说，所有可能在这个系统上进行开发工作的工程师应该都是“自己人”，即所有的开发动作都应该以设计文件为依归，例如，当设计文件规定某个数据结构里的某个属性属于私有属性（Private Attribute），则除了该模块内的函数可以操作它之外，其他的函数在使用该数据结构时，只能通过该模块公开的API来间接操作这些私有属性。若某工程师违反了此一原则，虽然该程序还是可以编译通过并正常运行，但其违反的不是程序语言的规范，而是该项目团队的规定，照理说，当然应该要严格管制。

也就是说，C语法虽然无法完美仿真面向对象的所有思想，但是嵌入式系统团队却可藉由系统设计文件来规范工程师，从而使面向对象的系统可以用C语言来实现。

以下我们用一个实例来说明如何用C实现面向对象的各种概念，你只要详读程序中的批注（黑体字部分），应该就可以了解这个方法的精神。

C-1　属性（Attribute）

类定义是用数据结构的方式来仿真，但有两个缺点：第一个是无法区分私有属性（Private）或公有属性（Public）；第二个则是类属性与方法（method）无法强制关联。

在如程序代码C-1所示的my_object.h中，除了定义对象的公用与私有属性外，公用方法（Public Method）与类属性（Class Attribute）也必须在此声明。则其他程序只要include这个.h文件，即可操作这个对象。

程序代码C-1

C-2　方法（Method）

程序代码C-2

C-3　对象实例（Instance）

C语言无法自动调用对象的Constructor与Deconstructor，必须由程序设计师视需要自行调用之。在其他的程序模块中，可以如程序代码C-3所示的实例使用这个对象。

程序代码C-3

C-4　消息传递（Meessage Passing）

要实现消息传递机制很简单，请参考《程序代码6-6：信息导向系统模块之程序风格（二）》，将其中的信息处理主程序当作对象的公用方法之一即可。

C-5　基类定义

所有“类（Class）数据结构”定义时的第一个属性，一定要是一个基类，程序才能判断传入的对象指针究竟是指向哪一种对象，这样才有办法用C语言的转型功能来实现面向对象思想中的继承以及多态性（Polymorphism）。

基类的定义如程序代码C-4所示。

程序代码C-4

以下属性都是私有属性，其他程序必须通过公用方法才能操作。

C-6　基类的公用方法

基类的公用方法如程序代码C-5所示，就是用来操作基类的私有属性。

程序代码C-5

C-7　对象类型判断范例

参考程序代码C-6所示的实例就可以了解基类的意义。

程序代码C-6

C-8　继承（Inheritance）

用C语言实现继承的技巧与基础类的思想差不多，即用到C语言的转型。只要子类（派生类）的第一个属性是父类（基类），则子对象转型为父对象后，可以使用父对象的所有属性与方法，如程序代码C-7所示。

程序代码C-7

C-9　多态性（Polymorphism）

面向对象思想中的Polymorphism，简单来说，就是同一个function可以根据传入的对象类型不同而做不同的事，在面向对象语言中是用overloading语法达成的，但在C语言中不允许相同名称的function。我们基于继承的范例来说明如何用C语言来仿真Polymorphism的思想，如程序代码C-8所示。

程序代码C-8

Appendix D

电子产品设计的最终依据：用户体验

D-1　界面设计vs用户体验

“传统”的电子产品设计讲究人机界面设计，从机器外观、结构、软硬件都必须经过设计。人机界面的教科书里教导设计者必须遵循以下原则：

■　象征（Metaphors）。例如：►代表播放、代表暂停、■ 代表停止等。

■　反映使用者个人内心状态（User's Mental Model）。

■　回应（Responsiveness）。机器对使用者的任何操作都应该有所响应。

■　隐藏科技（Invisible technology）。隐藏科技说起来很简单，但执行产品设计的是科技人，对科技人来说，很多科技都是想当然的东西，但一般使用者可不这么认为。

■　所见即所得（WYSIWYG）。

■　一致性（Consistency）。

■　视觉上整合（Aesthetic Integrity）。

■　宽恕性（Forgiveness）。不能因为使用者无心或错误的操作，就导致机器死机或有任何不正常的反应。

注意到了吗？在整个设计过程中虽然号称处处为使用者“着想”，但终端使用者并未在此扮演任何主动的角色。知道吗？录放机早已推出20几年了，居然大部分使用者还有40%的机会无法成功录下自己想看的节目，难道是功能不足的原因吗？这牵涉到两个问题，第一，设计者真的了解使用者的需求与感受吗？第二，即便设计者确实对使用者进行了调查与研究，使用者真的能了解目前的技术能作到什么程度，或者使用者真的能明确说出他要的是什么吗？

第一个问题无庸置疑，以电子学习机为例，负责电子产品设计的科技天才，怎么可能了解国小学生需要什么。至于第二个问题，经产学界的许多研究与实验，证明了一个事实，在这个信息泛滥的扁平世界，电子产品开发从业人员所面对的终端使用者已经体验过太多优秀的设计，但这些使用者通常无法描述这些经验所产生良好的心理状态。因为他们不知道目前有哪些选择，未来可能有哪些方案可行，自然无法明确说明对产品的期待，更无法解释不选择某个产品的原因。这个道理充分的反应在下面这首流行歌歌词里。

“你看过了许多美景　你看过了许多美女

你迷失在地图上　每一道短暂的光阴

你品尝了夜的巴黎　你踏过下雪的北京

你熟记书本里　每一句你最爱的真理

却说不出你爱我的原因　却说不出你欣赏我哪一种表情

却说不出在什么场合我曾让你动心　说不出离开的原因

你累计了许多飞行　你用心挑选纪念品

你搜集了地图上　每一次的风和日丽

你拥抱热情的岛屿　你埋葬记忆的土耳其

你流连电影里美丽的不真实的场景

却说不出你爱我的原因　却说不出你欣赏我哪一种表情

却说不出在什么场合我曾让你分心　说不出旅行的意义”

终端使用者根本说不出他们真正想要的功能，而设计者不过是想得比较深入的“模拟”使用者罢了！即便使用者明确指定他对该产品的期待，也不一定与真正可触动其心灵的美好经验（Experience）相契合。也就是说，产品设计就是要设法挠到使用者在相关体验的“爽筋”，无意义的新增功能也许能让使用者暂时惊艳一下，但绝对不会让该使用者因此帮这个产品无偿宣传、制造口碑。这就是为什么我们常买到一些电子产品或软件，功能丰富无比，但总是觉得哪里不是很好用，却又无法明确说出个所以然来。

所以，现代电子产品设计者应该丢弃以功能导向为主的思维，举例来说，想买MP3随声听的准消费者就算没用过也该听过iPod吧！设计者还想针对MP3随声听这类产品做什么样的界面优化？难道把使用者界面与功能做得跟iPod一模一样就可以创造与Apple相同的销售成绩吗？

一个“成功”的电子产品应该考虑到3个方面（成功的定义只有一个，就是“畅销”并“获利”）：

■　设计面：此阶段该研究使用者到底渴望什么样的产品？重视什么样的功能？为什么市面上的产品都无法满足其需求？思考如何设计出“令人向往”的产品？

■　工程（技术）面：此阶段才开始考虑到功能的可行性，基于设计阶段的成果，弄清楚什么是可行的？使所有人员聚焦在客户渴望功能的技术开发上，并产出质量优良的产品。

■　业务面：要能将产品销售出去，更重要的是要能获利！

所以正确的经营模式应该是先判断哪些功能是可以触动用户体验、是客户渴望拥有的，然后才是安排工程人员设计、开发并制造出这样的产品，接着是驱使营销人员将该产品让需要的人看到并掏腰包购买。换句话说，花3个月作出让使用者气馁的产品，并不比花6个月推出让使用者满意的产品高明。推出一个不知是否可满足终端客户需求的产品无异于赌博，“TimeToMarket”不该是产品设计的唯一考虑。

许多企业都意识到“思考如何触动用户体验”已经成为设计畅销产品的不二法门，但大多数的产品开发者依然关着门埋头设计使用者界面，这涉及信息业一个不能说的秘密。用户体验需要花费资源与时间进行研究调查，但界面设计却是可以加班赶出来的。偏偏大多数的项目经理人宁可准时推出极可能失败的产品，也不愿冒险延期。

D-2　用户体验&可用性（Usability）

通常人机界面被当成仅仅是一层包裹于功能核心之外的“包装”，因而没有得到足够的重视，结果就是人机界面与核心功能（系统）分别独立开发，而且往往是在整个开发过程的尾声部分才开始，这种方式大大地限制了“人机关系”的设计，其结果当然带有很大的风险性。在产品开发最后阶段，修改核心需要付出极大代价，所以牺牲人机界面成为唯一的选择，这种带有猜测性与赌博性的开发“态度”，当然难以获得令人满意的用户体验。

当一般使用者操作某电子产品遇到挫折时，总会不好意思地摸摸头，责怪自己说：“唉～我这个计算机白痴，连这个都不会”。这也是个很有趣的现象，为什么要因花钱买来的产品不好用而感到自责呢？反而是设计者应该感到羞愧才对，无法让使用者有满意的体验，设计工作基本上算是彻底失败！其实使用者期待的并不是某某功能，而是想知道新产品可以帮他解决什么问题。

在电子产品充斥市面的今日，设计者与使用者都该改变自己的思想，身为使用者，不应以身为“计算机白痴”为耻。身为设计者，则应以消除计算机白痴为目标。更明确地说，只要让使用者感觉到界面的存在，就不是好的界面，即必须将原本“使人适事”的市场现状，转换为“使事适人”的设计原则。所有电子产品设计者都要深切的了解Nokia“以人为本”的真正意义，像Nokia这样的商业公司存在的目的就是为了营利，这表示Nokia认为“以人为本”的企业文化可创造利润。所谓的“以人为本”就是使用者导向，即应该注重系统之“可用性”（Usability）而非功能。造成电子产品不人性化的罪魁祸首是科技人，而非科技本身，或者说是科技公司、科技人采用的流程，造成市面上充斥着不人性化的电子产品。

所谓用户体验不仅仅是使用者能“说得出来”的需要，用户体验必须可以触动过去的生活经验，一般业界以“可用性”来度量产品、系统、服务之用户体验的优劣。可用性是和人类行为相关的一种概念，它清楚认识到人类是懒惰的、情绪化的，而且人类通常喜欢做些容易做的事情，而不是难做的事情。也就是说，“把使用者当作计算机专家”的设计其实是一种违反人性的行为！

Usability是产品的一种质量度量标准，度量产品对使用者来说是否具有以下几点。

■　行之有效的（Effective）。

■　有效率的（Efficient）。

■　令人满意的（Satisfying）。

Usability已被广泛重视，国际标准ISO 9241（Usability）与ISO13407（Design Process）已明确规范如何在设计阶段即确保最终产品的Usability。

企业要提供感性用户体验并确保产品的Usability，必须先改变思想，丢掉以功能导向为主的思维。企业必须采取一整套的方法与活动，势必要改变组织、职责、工作目标、人力资源、流程与营利模式，以确保以人为本的产品设计模式。

以上道理简单易懂，但我们不得不承认，几乎所有企业设计产品仍然是“先准备功能，再找人来买”。如果修正成以用户体验导向的设计方式，应该是“先为某些人设计，再去完成功能”；较专业的说法则是：先设计Persona（客户性格），再研究产生MRS (Market Request Specification），然后才能展开开发工作。

其实就是基本思想的差异，如果执行者能翻转思想，进而改变企业文化与组织流程，自然能设计出感动使用者的产品。

D-3　客户性格（Persona）

为了设计出可“触动用户体验”的产品，比较有效的方法就是先设计出“客户性格”（以下称Persona），所谓的Persona的基本理论是：“Design for one is design for all.”。在业界有一个屡试不爽的奇怪现象，就是如果想要设计出一个符合“所有人”需求的产品，最终只能做出一个四不像的失败产品。假设汽车市场包含了以下种类的客户：中年妇女（接送孩子、宠物）、送货工人（马力大、货架）、年轻经理（敞篷、双人座），如果试图用一种车型来满足上述需求的话，就算可以实现，肯定也没有人会买。

但是如果设计者只针对其中一个（种）人来进行设计，就能做出令某部分人完全满意的产品，Persona就是善用了这个奇怪的力量，实际上，所有成功的成品都会很明确地定义其市场区隔（Market Segment），不会天真的试图要满足所有的人。

一般人会认为：如果某个产品可以包含所有使用者需要的功能，自然可以容纳最多的用户，吸引最多的消费者掏腰包购卖。然而这种逻辑是错误的，实际上，只为一个（种）人所设计的产品，在市场上成功的机会要大得多！换句话说，就是要避免“弹性使用者”，“使用者”这个词会随着设计者的主观意愿改变其精确含意，而Persona就是对“使用者”精确定义的结果。

Persona是基于人们真实的行为与产生动机的模式，在整个设计过程中代表着“真实的使用者”，通过Persona可以让设计者清楚地了解使用者的目标是什么，什么样的“特定功能”可以满足客户的需求。生活中为少数人设计，而获得广泛成功的例子俯拾皆是，例如：“拉杆箱”原本只为航班机组人员设计，“便利贴”原本只是为了教会唱诗班的书签所设计等。

Persona不是真实的人物，但“它们”在设计过程中代表着真实人物，它们是真实使用者的假象模型。在设计工作开始之前就必须先有一个首要的Persona，它是设计工作打算为之服务的中心人物，设计的目标就是让“这个人”得到满足。Persona与市场划分不同，后者基于统计数据和销售渠道，而前者基于使用者行为和目标。

当设计方式改为以Persona为中心的设计模式，用户参与程度会大幅增加，基本上终端使用者在设计之初就该参与进来，创造完成Persona后，任何一个与系统功能和界面有关的决定，都是基于此用户角色。设计工作的流程会变成：

■　创造Persona。

■　了解使用者需求。

■　设计系统。

■　使用Persona对系统进行评估。

■　最后对系统进行完善和改进。

通常Persona会包含以下项目。

■　基本信息：年纪、性别、职业、婚姻状况、爱好等。

■　动机：动机不仅指出了特定的使用模式，也提供了这些行为存在的理由。理解使用者作某件事情的动机，能够改善甚至取消某些不必要的功能，同时仍能满足使用者的需求。

■　场景：是Persona典型行为的发生过程，包含使用者、背景信息、环境假设、操作顺序，以及可能发生的突发事件。

■　目标：目标是行为背后的驱动力，产品的功能和行为必须通过“任务”来解决目标，任务只是达到结果的手段，“目标”自身才是最终的结果。以MP3播放器为例，完整的播放音乐只是完成“任务”而已，让使用者在听音乐时能毫无压力、感觉愉悦则是设计的“目标”。

举例来说，微软在设计Windows时只用了6个Persona，换句话说，Windows居然是只为了6种人设计！Windows好不好用暂且不提，6种人当然不能代表全部人类，但如果让这6种人感到满意，基本上已能满足大部分人的需求。设计期间参与Persona研究之设计团队包含：PM、Usability engineer、Product Designer、图像Designer、市场研究员、Programmer，其中有一个大部分研发公司都没有的职衔——Usability Engineer，由此可见微软对用户体验、Usability与Persona的重视程度。

整体来说，使用Persona为中心之设计方式的优点为：

■　目标明确。设计者遇到任何问题时，都可用Persona的目标和行为得到解答。

■　突出重点。主要Persona的需求必须被满足，非Persona的需求可以不予考虑。缺点则是在设计之初，需要额外的人力物力创造Persona，这也是一般企业或设计者望之却步的主要原因。

如果产品上市后能创造更多利润，设计开发阶段投入资源进行Persona设计绝对是值得的。最后笔者扩充了传统“设计→开发→调试→修改”的产品设计流程，图20-1所示的粗体部分是新增且不可或缺的工作。

▲ 图D-1　正确的产品开发流程

D-4　结论

这是一个平坦的世界（Plain World），企业很难再利用“信息不平衡”来取得绝对的优势。一般企业或设计者宁愿花资源与人力，不断为产品新增自认使用者会感激涕零的功能，殊不知“过度设计”只会让使用者混淆，绝对无法“触动User内心的感动”，就算乱枪打鸟让某个产品成功了一次，若设计者依然无法跳离这个窠臼，不断地浪费设计单位与RD的资源，期望公司能长久运营无异缘木求鱼！因为永远有竞争者拥有比你更多的信息、人力与资本，永远有其他的设计者比你作过更深入的市场调查，进而执行更多的设计并开发更多的功能。

同时这是一个信息爆炸的时代，产品信息随手可得，使用者已不会只根据型录里功能的多寡来决定是否购买某产品，他们重视的是所谓的“口碑”与其他使用者对个别产品的评价。

经过本文的论述，我们知道意图“想要满足所有人”，往往结果是无法满足任何一个人，想要在某个产品包山包海，往往因为没有特色成为市场的弃儿！市场调查仍然很重要，因其关系到企业的战略设定。但若以产品设计而言，使用者特性调查更关乎该产品的成败。世间人千百种，如果不先设定好目标客户群的特性，并以此为产品开发的基准，则设计者在执行阶段势必顾此失彼，无所适从。此外，你必须认清“利基市场”（Niche Market）与Persona并不是同一件事情，试想Microsoft Windows的Persona只有6个，但Microsoft Windows显然并非只为6个niche market所设计的产品！

企业与所有电子产品相关从业人员必须认清世界在改变，使用者也在改变，唯有以面向“使用者经验”的设计逻辑，才能做出让使用者真正满意的人机界面，且企业文化的改变必须由上而下，必须由高层制订企业文化，并认真的推动，不要说一套做一套，否则，就算单位项目管理者或工程人员有心使用正确的流程设计产品，在资源与进度的压力下也只能举手投降。

其道理显而易见，希望以下简单的推导可以唤醒所有从事电子产品设计的人们，让我们设计出更多符合人性化的产品，也许这个世界会因此更加美好！

■　已知条件：

□　企业存在的目的→盈利。

□　盈利的方式→利用科技设计产品或服务，并销售这些产品或服务。

□　科技来自于人性化。

■　可推导出结论为企业必须关注人性化才有办法盈利！

例说51单片机（C语言版）（第3版）

畅销不衰的单片机经典入门教程，基本开发环境、8×51结构与应用、系统应用等根据需要安排于各章节，适于教学编排。

例说8051（第3版）

持续畅销的经典入门教程最新版，近50个实例，全面分析8051单片机的各种功能。内部结构、指令等根据需要安排于各章节，适于教学编排。

博客真情推荐

献给正在这个业界努力求生存，以及将要踏入这个领域的勇士们！

老鸟流泪！新手必看！最平易近人的嵌入式项目开发书籍！

这是一本专为系统开发人员撰写，能够快速理解何谓“嵌入式系统”的书，作者以自身丰富的经验与见闻，无私地分享在嵌入式领域真正有用的知识。

本书最大的特色是以一个完整的嵌入式系统开发流程为架构，通过一名菜鸟工程师与项目经理的诙谐对话，故事性地带出系统概念及开发要素，并点出要成为一名称职的嵌入式系统工程师，在实际工作中所必须具备的各项知识及技能。这是一本能让你少走冤枉路，学习如何轻松驾驭并有效完成开发项目的实用指南。

新版特色：

• 重新改写项目架构，更贴近开发人员的实践核心。

• 基于嵌入式系统开发项目的特性，深入说明项目管理的概念与工具。

• 完整介绍项目进度追踪实务，介绍如何通过工具更有效率地执行项目，避免项目进入“死循环”。

• 新增多个实际案例，全面阐述系统架构设计的方法与重要性。

• 收录了目前嵌入式系统开发领域中，从业人员不可不知的所有知识，主要包含以下内容。

• 阐述硬件抽象层（HAL）对嵌入式系统团队的重要性，并详细说明其开发实务。

• 完整介绍实现NAND Flash系统的流程与不能忽视的重点技术。

• 剖析开发Turnkey Solution与一般嵌入式系统项目的异同及注意事项。

• 深入探讨IC设计公司中嵌入式系统团队（System Application; SA）的管理。