数据库技术是信息资源管理最有效的手段。数据库设计是指对于一个给定的应用环境,构造最优的数据库模式,建立数据库及其应用系统,有效存储数据,满足用户信息要求和处理要求。
数
据库设计中需求分析阶段综合各个用户的应用需求(现实世界的需求),在概念设计阶段形成独立于机器特点、独立于各个DBMS产品的概念模式(信息世界模
型),用E-R图来描述。在逻辑设计阶段将E-R图转换成具体的数据库产品支持的数据模型如关系模型,形成数据库逻辑模式。然后根据用户处理的要求,安全
性的考虑,在基本表的基础上再建立必要的视图(VIEW)形成数据的外模式。在物理设计阶段根据DBMS特点和处理的需要,进行物理存储安排,设计索引,
形成数据库内模式。
1. 需求分析阶段
需求收集和分析,结果得到数据字典描述的数据需求(和数据流图描述的处理需求)。
需求分析的重点是调查、收集与分析用户在数据管理中的信息要求、处理要求、安全性与完整性要求。
需求分析的方法:调查组织机构情况、调查各部门的业务活动情况、协助用户明确对新系统的各种要求、确定新系统的边界。
常用的调查方法有: 跟班作业、开调查会、请专人介绍、询问、设计调查表请用户填写、查阅记录。
分析和表达用户需求的方法主要包括自顶向下和自底向上两类方法。自顶向下的结构化分析方法(Structured Analysis,简称SA方法)从最上层的系统组织机构入手,采用逐层分解的方式分析系统,并把每一层用数据流图和数据字典描述。
数据流图表达了数据和处理过程的关系。系统中的数据则借助数据字典(Data Dictionary,简称DD)来描述。
数据字典是各类数据描述的集合,它是关于数据库中数据的描述,即元数据,而不是数据本身。数据字典通常包括数据项、数据结构、数据流、数据存储和处理过程五个部分(至少应该包含每个字段的数据类型和在每个表内的主外键)。
数据项描述={数据项名,数据项含义说明,别名,数据类型,长度,
取值范围,取值含义,与其他数据项的逻辑关系}
数据结构描述={数据结构名,含义说明,组成:{数据项或数据结构}}
数据流描述={数据流名,说明,数据流来源,数据流去向,
组成:{数据结构},平均流量,高峰期流量}
数据存储描述={数据存储名,说明,编号,流入的数据流,流出的数据流,
组成:{数据结构},数据量,存取方式}
处理过程描述={处理过程名,说明,输入:{数据流},输出:{数据流},
处理:{简要说明}}
2. 概念结构设计阶段
通过对用户需求进行综合、归纳与抽象,形成一个独立于具体DBMS的概念模型,可以用E-R图表示。
概念模型用于信息世界的建模。概念模型不依赖于某一个DBMS支持的数据模型。概念模型可以转换为计算机上某一DBMS支持的特定数据模型。
概念模型特点:
(1) 具有较强的语义表达能力,能够方便、直接地表达应用中的各种语义知识。
(2) 应该简单、清晰、易于用户理解,是用户与数据库设计人员之间进行交流的语言。
概念模型设计的一种常用方法为IDEF1X方法,它就是把实体-联系方法应用到语义数据模型中的一种语义模型化技术,用于建立系统信息模型。
使用IDEF1X方法创建E-R模型的步骤如下所示:
2.1 第零步——初始化工程
这个阶段的任务是从目的描述和范围描述开始,确定建模目标,开发建模计划,组织建模队伍,收集源材料,制定约束和规范。收集源材料是这阶段的重点。通过调查和观察结果,业务流程,原有系统的输入输出,各种报表,收集原始数据,形成了基本数据资料表。
2.2 第一步——定义实体
实
体集成员都有一个共同的特征和属性集,可以从收集的源材料——基本数据资料表中直接或间接标识出大部分实体。根据源材料名字表中表示物的术语以及具有“代
码”结尾的术语,如客户代码、代理商代码、产品代码等将其名词部分代表的实体标识出来,从而初步找出潜在的实体,形成初步实体表。
2.3 第二步——定义联系
IDEF1X
模型中只允许二元联系,n元联系必须定义为n个二元联系。根据实际的业务需求和规则,使用实体联系矩阵来标识实体间的二元关系,然后根据实际情况确定出连
接关系的势、关系名和说明,确定关系类型,是标识关系、非标识关系(强制的或可选的)还是非确定关系、分类关系。如果子实体的每个实例都需要通过和父实体
的关系来标识,则为标识关系,否则为非标识关系。非标识关系中,如果每个子实体的实例都与而且只与一个父实体关联,则为强制的,否则为非强制的。如果父实
体与子实体代表的是同一现实对象,那么它们为分类关系。
2.4 第三步——定义码
通过引入交叉实体除去上一阶段产生的非确定关系,然后从
非交叉实体和独立实体开始标识侯选码属性,以便唯一识别每个实体的实例,再从侯选码中确定主码。为了确定主码和关系的有效性,通过非空规则和非多值规则来
保证,即一个实体实例的一个属性不能是空值,也不能在同一个时刻有一个以上的值。找出误认的确定关系,将实体进一步分解,最后构造出IDEF1X模型的键
基视图(KB图)。
2.5 第四步——定义属性
从源数据表中抽取说明性的名词开发出属性表,确定属性的所有者。定义非主码属性,检查属性
的非空及非多值规则。此外,还要检查完全依赖函数规则和非传递依赖规则,保证一个非主码属性必须依赖于主码、整个主码、仅仅是主码。以此得到了至少符合关
系理论第三范式的改进的IDEF1X模型的全属性视图。
2.6 第五步——定义其他对象和规则
定义属性的数据类型、长度、精度、非空、缺省值、约束规则等。定义触发器、存储过程、视图、角色、同义词、序列等对象信息。
3. 逻辑结构设计阶段
将概念结构转换为某个DBMS所支持的数据模型(例如关系模型),并对其进行优化。设计逻辑结构应该选择最适于描述与表达相应概念结构的数据模型,然后选择最合适的DBMS。
将E-R图转换为关系模型实际上就是要将实体、实体的属性和实体之间的联系转化为关系模式,这种转换一般遵循如下原则:
1)一个实体型转换为一个关系模式。实体的属性就是关系的属性。实体的码就是关系的码。
2)一个m:n联系转换为一个关系模式。与该联系相连的各实体的码以及联系本身的属性均转换为关系的属性。而关系的码为各实体码的组合。
3)一个1:n联系可以转换为一个独立的关系模式,也可以与n端对应的关系模式合并。如果转换为一个独立的关系模式,则与该联系相连的各实体的码以及联系本身的属性均转换为关系的属性,而关系的码为n端实体的码。
4)一个1:1联系可以转换为一个独立的关系模式,也可以与任意一端对应的关系模式合并。
5)三个或三个以上实体间的一个多元联系转换为一个关系模式。与该多元联系相连的各实体的码以及联系本身的属性均转换为关系的属性。而关系的码为各实体码的组合。
6)同一实体集的实体间的联系,即自联系,也可按上述1:1、1:n和m:n三种情况分别处理。
7)具有相同码的关系模式可合并。
为了进一步提高数据库应用系统的性能,通常以规范化理论为指导,还应该适当地修改、调整数据模型的结构,这就是数据模型的优化。确定数据依赖。消除冗余的联系。确定各关系模式分别属于第几范式。确定是否要对它们进行合并或分解。一般来说将关系分解为3NF的标准,即:
表内的每一个值都只能被表达一次。
•?表内的每一行都应该被唯一的标识(有唯一键)。
表内不应该存储依赖于其他键的非键信息。
4. 数据库物理设计阶段
为逻辑数据模型选取一个最适合应用环境的物理结构(包括存储结构和存取方法)。根据DBMS特点和处理的需要,进行物理存储安排,设计索引,形成数据库内模式。
5. 数据库实施阶段
运
用DBMS提供的数据语言(例如SQL)及其宿主语言(例如C),根据逻辑设计和物理设计的结果建立数据库,编制与调试应用程序,组织数据入库,并进行试
运行。 数据库实施主要包括以下工作:用DDL定义数据库结构、组织数据入库 、编制与调试应用程序、数据库试运行
6. 数据库运行和维护阶段
数据库应用系统经过试运行后即可投入正式运行。在数据库系统运行过程中必须不断地对其进行评价、调整与修改。包括:数据库的转储和恢复、数据库的安全性、完整性控制、数据库性能的监督、分析和改进、数据库的重组织和重构造。
建模工具的使用
为加快数据库设计速度,目前有很多数据库辅助工具(CASE工具),如Rational公司的Rational Rose,CA公司的Erwin和Bpwin,Sybase公司的PowerDesigner以及Oracle公司的Oracle Designer等。
ERwin
主要用来建立数据库的概念模型和物理模型。它能用图形化的方式,描述出实体、联系及实体的属性。ERwin支持IDEF1X方法。通过使用ERwin建模
工具自动生成、更改和分析IDEF1X模型,不仅能得到优秀的业务功能和数据需求模型,而且可以实现从IDEF1X模型到数据库物理设计的转变。
ERwin工具绘制的模型对应于逻辑模型和物理模型两种。在逻辑模型中,IDEF1X工具箱可以方便地用图形化的方式构建和绘制实体联系及实体的属性。在
物理模型中,ERwin可以定义对应的表、列,并可针对各种数据库管理系统自动转换为适当的类型。
设计人员可根据需要选用相应的数据库设计建模工具。例如需求分析完成之后,设计人员可以使用Erwin画ER图,将ER图转换为关系数据模型,生成数据库结构;画数据流图,生成应用程序。
二、数据库设计技巧
1. 设计数据库之前(需求分析阶段)
1) 理解客户需求,询问用户如何看待未来需求变化。让客户解释其需求,而且随着开发的继续,还要经常询问客户保证其需求仍然在开发的目的之中。
2) 了解企业业务可以在以后的开发阶段节约大量的时间。
3) 重视输入输出。
在定义数据库表和字段需求(输入)时,首先应检查现有的或者已经设计出的报表、查询和视图(输出)以决定为了支持这些输出哪些是必要的表和字段。
举例:假如客户需要一个报表按照邮政编码排序、分段和求和,你要保证其中包括了单独的邮政编码字段而不要把邮政编码糅进地址字段里。
4) 创建数据字典和ER 图表
ER 图表和数据字典可以让任何了解数据库的人都明确如何从数据库中获得数据。ER图对表明表之间关系很有用,而数据字典则说明了每个字段的用途以及任何可能存在的别名。对SQL 表达式的文档化来说这是完全必要的。
5) 定义标准的对象命名规范
数据库各种对象的命名必须规范。
2. 表和字段的设计(数据库逻辑设计)
表设计原则
1) 标准化和规范化
数
据的标准化有助于消除数据库中的数据冗余。标准化有好几种形式,但Third Normal Form(3NF)通常被认为在性能、扩展性和数据完整性方
面达到了最好平衡。简单来说,遵守3NF 标准的数据库的表设计原则是:“One Fact in One Place”即某个表只包括其本身基本的属
性,当不是它们本身所具有的属性时需进行分解。表之间的关系通过外键相连接。它具有以下特点:有一组表专门存放通过键连接起来的关联数据。
举例:某个存放客户及其有关定单的3NF 数据库就可能有两个表:Customer 和Order。Order 表不包含定单关联客户的任何信息,但表内会存放一个键值,该键指向Customer 表里包含该客户信息的那一行。
事实上,为了效率的缘故,对表不进行标准化有时也是必要的。
2) 数据驱动
采用数据驱动而非硬编码的方式,许多策略变更和维护都会方便得多,大大增强系统的灵活性和扩展性。
举
例,假如用户界面要访问外部数据源(文件、XML 文档、其他数据库等),不妨把相应的连接和路径信息存储在用户界面支持表里。还有,如果用户界面执行工
作流之类的任务(发送邮件、打印信笺、修改记录状态等),那么产生工作流的数据也可以存放在数据库里。角色权限管理也可以通过数据驱动来完成。事实上,如
果过程是数据驱动的,你就可以把相当大的责任推给用户,由用户来维护自己的工作流过程。
3) 考虑各种变化
在设计数据库的时候考虑到哪些数据字段将来可能会发生变更。
举例,姓氏就是如此(注意是西方人的姓氏,比如女性结婚后从夫姓等)。所以,在建立系统存储客户信息时,在单独的一个数据表里存储姓氏字段,而且还附加起始日和终止日等字段,这样就可以跟踪这一数据条目的变化。
字段设计原则
4) 每个表中都应该添加的3 个有用的字段
•?dRecordCreationDate,在VB 下默认是Now(),而在SQL Server 下默认为GETDATE()
•?sRecordCreator,在SQL Server 下默认为NOT NULL DEFAULT USER
•?nRecordVersion,记录的版本标记;有助于准确说明记录中出现null 数据或者丢失数据的原因
5) 对地址和电话采用多个字段
描述街道地址就短短一行记录是不够的。Address_Line1、Address_Line2 和Address_Line3 可以提供更大的灵活性。还有,电话号码和邮件地址最好拥有自己的数据表,其间具有自身的类型和标记类别。
6) 使用角色实体定义属于某类别的列
在需要对属于特定类别或者具有特定角色的事物做定义时,可以用角色实体来创建特定的时间关联关系,从而可以实现自我文档化。
举
例:用PERSON 实体和PERSON_TYPE 实体来描述人员。比方说,当John Smith, Engineer 提升为
John Smith, Director 乃至最后爬到John Smith, CIO 的高位,而所有你要做的不过是改变两个表PERSON 和
PERSON_TYPE 之间关系的键值,同时增加一个日期/时间字段来知道变化是何时发生的。这样,你的PERSON_TYPE 表就包含了所有
PERSON 的可能类型,比如Associate、Engineer、Director、CIO 或者CEO 等。还有个替代办法就是改变
PERSON 记录来反映新头衔的变化,不过这样一来在时间上无法跟踪个人所处位置的具体时间。
7) 选择数字类型和文本类型尽量充足
在SQL 中使用smallint 和tinyint 类型要特别小心。比如,假如想看看月销售总额,总额字段类型是smallint,那么,如果总额超过了$32,767 就不能进行计算操作了。
而ID 类型的文本字段,比如客户ID 或定单号等等都应该设置得比一般想象更大。假设客户ID 为10 位数长。那你应该把数据库表字段的长度设为12 或者13 个字符长。但这额外占据的空间却无需将来重构整个数据库就可以实现数据库规模的增长了。
8) 增加删除标记字段
在表中包含一个“删除标记”字段,这样就可以把行标记为删除。在关系数据库里不要单独删除某一行;最好采用清除数据程序而且要仔细维护索引整体性。
3. 选择键和索引(数据库逻辑设计)
键选择原则:
1) 键设计4 原则
•?为关联字段创建外键。
•?所有的键都必须唯一。
•?避免使用复合键。
•?外键总是关联唯一的键字段。
2) 使用系统生成的主键
设计数据库的时候采用系统生成的键作为主键,那么实际控制了数据库的索引完整性。这样,数据库和非人工机制就有效地控制了对存储数据中每一行的访问。采用系统生成键作为主键还有一个优点:当拥有一致的键结构时,找到逻辑缺陷很容易。
3) 不要用用户的键(不让主键具有可更新性)
在确定采用什么字段作为表的键的时候,可一定要小心用户将要编辑的字段。通常的情况下不要选择用户可编辑的字段作为键。
4) 可选键有时可做主键
把可选键进一步用做主键,可以拥有建立强大索引的能力。
索引使用原则:
索引是从数据库中获取数据的最高效方式之一。95%的数据库性能问题都可以采用索引技术得到解决。
1) 逻辑主键使用唯一的成组索引,对系统键(作为存储过程)采用唯一的非成组索引,对任何外键列采用非成组索引。考虑数据库的空间有多大,表如何进行访问,还有这些访问是否主要用作读写。
2) 大多数数据库都索引自动创建的主键字段,但是可别忘了索引外键,它们也是经常使用的键,比如运行查询显示主表和所有关联表的某条记录就用得上。
3) 不要索引memo/note 字段,不要索引大型字段(有很多字符),这样作会让索引占用太多的存储空间。
4) 不要索引常用的小型表
不要为小型数据表设置任何键,假如它们经常有插入和删除操作就更别这样作了。对这些插入和删除操作的索引维护可能比扫描表空间消耗更多的时间。
4. 数据完整性设计(数据库逻辑设计)
1) 完整性实现机制:
实体完整性:主键
参照完整性:
父表中删除数据:级联删除;受限删除;置空值
父表中插入数据:受限插入;递归插入
父表中更新数据:级联更新;受限更新;置空值
DBMS对参照完整性可以有两种方法实现:外键实现机制(约束规则)和触发器实现机制
用户定义完整性:
NOT NULL;CHECK;触发器
2) 用约束而非商务规则强制数据完整性
采用数据库系统实现数据的完整性。这不但包括通过标准化实现的完整性而且还包括数据的功能性。在写数据的时候还可以增加触发器来保证数据的正确性。不要依赖于商务层保证数据完整性;它不能保证表之间(外键)的完整性所以不能强加于其他完整性规则之上。
3) 强制指示完整性
在有害数据进入数据库之前将其剔除。激活数据库系统的指示完整性特性。这样可以保持数据的清洁而能迫使开发人员投入更多的时间处理错误条件。
4) 使用查找控制数据完整性
控制数据完整性的最佳方式就是限制用户的选择。只要有可能都应该提供给用户一个清晰的价值列表供其选择。这样将减少键入代码的错误和误解同时提供数据的一致性。某些公共数据特别适合查找:国家代码、状态代码等。
5) 采用视图
为了在数据库和应用程序代码之间提供另一层抽象,可以为应用程序建立专门的视图而不必非要应用程序直接访问数据表。这样做还等于在处理数据库变更时给你提供了更多的自由。
5. 其他设计技巧
1) 避免使用触发器
触发器的功能通常可以用其他方式实现。在调试程序时触发器可能成为干扰。假如你确实需要采用触发器,你最好集中对它文档化。
2) 使用常用英语(或者其他任何语言)而不要使用编码
在创建下拉菜单、列表、报表时最好按照英语名排序。假如需要编码,可以在编码旁附上用户知道的英语。
3) 保存常用信息
让
一个表专门存放一般数据库信息非常有用。在这个表里存放数据库当前版本、最近检查/修复(对Access)、关联设计文档的名称、客户等信息。这样可以实
现一种简单机制跟踪数据库,当客户抱怨他们的数据库没有达到希望的要求而与你联系时,这样做对非客户机/服务器环境特别有用。
4) 包含版本机制
在数据库中引入版本控制机制来确定使用中的数据库的版本。时间一长,用户的需求总是会改变的。最终可能会要求修改数据库结构。把版本信息直接存放到数据库中更为方便。
5) 编制文档
对所有的快捷方式、命名规范、限制和函数都要编制文档。
采用给表、列、触发器等加注释的数据库工具。对开发、支持和跟踪修改非常有用。
对数据库文档化,或者在数据库自身的内部或者单独建立文档。这样,当过了一年多时间后再回过头来做第2 个版本,犯错的机会将大大减少。
6) 测试、测试、反复测试
建立或者修订数据库之后,必须用用户新输入的数据测试数据字段。最重要的是,让用户进行测试并且同用户一道保证选择的数据类型满足商业要求。测试需要在把新数据库投入实际服务之前完成。
7) 检查设计
在开发期间检查数据库设计的常用技术是通过其所支持的应用程序原型检查数据库。换句话说,针对每一种最终表达数据的原型应用,保证你检查了数据模型并且查看如何取出数据。
三、数据库命名规范
1. 实体(表)的命名
1)
表以名词或名词短语命名,确定表名是采用复数还是单数形式,此外给表的别名定义简单规则(比方说,如果表名是一个单词,别名就取单词的前4 个字母;如
果表名是两个单词,就各取两个单词的前两个字母组成4 个字母长的别名;如果表的名字由3 个单词组成,从头两个单词中各取一个然后从最后一个单词中再取
出两个字母,结果还是组成4 字母长的别名,其余依次类推)
对工作用表来说,表名可以加上前缀WORK_ 后面附上采用该表的应用程序的名字。在命名过程当中,根据语义拼凑缩写即可。注意,由于ORCLE会将字段名称统一成大写或者小写中的一种,所以要求加上下划线。
举例:
定义的缩写 Sales: Sal 销售;
Order: Ord 订单;
Detail: Dtl 明细;
则销售订单明细表命名为:Sal_Ord_Dtl;
2) 如果表或者是字段的名称仅有一个单词,那么建议不使用缩写,而是用完整的单词。
举例:
定义的缩写 Material Ma 物品;
物品表名为:Material, 而不是 Ma.
但是字段物品编码则是:Ma_ID;而不是Material_ID
3) 所有的存储值列表的表前面加上前缀Z
目的是将这些值列表类排序在数据库最后。
4) 所有的冗余类的命名(主要是累计表)前面加上前缀X
冗余类是为了提高数据库效率,非规范化数据库的时候加入的字段或者表
5) 关联类通过用下划线连接两个基本类之后,再加前缀R的方式命名,后面按照字母顺序罗列两个表名或者表名的缩写。
关联表用于保存多对多关系。
如果被关联的表名大于10个字母,必须将原来的表名的进行缩写。如果没有其他原因,建议都使用缩写。
举例:表Object与自身存在多对多的关系,则保存多对多关系的表命名为:R_Object;
表 Depart和Employee;存在多对多的关系;则关联表命名为R_Dept_Emp
2. 属性(列)的命名
1)
采用有意义的列名,表内的列要针对键采用一整套设计规则。每一个表都将有一个自动ID作为主健,逻辑上的主健作为第一组候选主健来定义,如果是数据库自
动生成的编码,统一命名为:ID;如果是自定义的逻辑上的编码则用缩写加“ID”的方法命名。如果键是数字类型,你可以用_NO 作为后缀;如果是字符类
型则可以采用_CODE 后缀。对列名应该采用标准的前缀和后缀。
举例:销售订单的编号字段命名:Sal_Ord_ID;如果还存在一个数据库生成的自动编号,则命名为:ID。
2) 所有的属性加上有关类型的后缀,注意,如果还需要其它的后缀,都放在类型后缀之前。
注: 数据类型是文本的字段,类型后缀TX可以不写。有些类型比较明显的字段,可以不写类型后缀。
3) 采用前缀命名
给每个表的列名都采用统一的前缀,那么在编写SQL表达式的时候会得到大大的简化。这样做也确实有缺点,比如破坏了自动表连接工具的作用,后者把公共列名同某些数据库联系起来。
3. 视图的命名
1) 视图以V作为前缀,其他命名规则和表的命名类似;
2) 命名应尽量体现各视图的功能。
4. 触发器的命名
触发器以TR作为前缀,触发器名为相应的表名加上后缀,Insert触发器加 _I ,Delete触发器加 _D ,Update触发器加 _U ,如:TR_Customer_I,TR_Customer_D,TR_Customer_U。
5. 存储过程名
存储过程应以 UP_ 开头,和系统的存储过程区分,后续部分主要以动宾形式构成,并用下划线分割各个组成部分。如增加代理商的帐户的存储过程为 UP_Ins_Agent_Account 。
6. 变量名
变量名采用小写,若属于词组形式,用下划线分隔每个单词,如@my_err_no。
7. 命名中其他注意事项
1) 以上命名都不得超过30个字符的系统限制。变量名的长度限制为29(不包括标识字符@)。
2) 数据对象、变量的命名都采用英文字符,禁止使用中文命名。绝对不要在对象名的字符之间留空格。
3) 小心保留词,要保证你的字段名没有和保留词、数据库系统或者常用访问方法冲突
5) 保持字段名和类型的一致性,在命名字段并为其指定数据类型的时候一定要保证一致性。假如数据类型在一个表里是整数,那在另一个表里可就别变成字符型了。
如今在Internet上,传统基于字符界面的应用逐渐被能够浏览图象信息的WWW(World Wide Web)方式所取代。WWW尽管漂亮,但是也带来了一个问题:图象信息的数据量太大了,本来就已经非常紧张的网络带宽变得更加不堪重负,使得World Wide Web变成了World Wide Wait。
总之,大数据量的图象信息会给存储器的存储容量,通信干线信道的带宽,以及计算机的处理速度增加极大的压力。单纯靠增加存储器容量,提高信道带宽以及计算机的处理速度等方法来解决这个问题是不现实的,这时就要考虑压缩。压缩的理论基础是信息论。从信息论的角度来看,压缩就是去掉信息中的冗余,即保留不确定的信息,去掉确定的信息(可推知的),也就是用一种更接近信息本质的描述来代替原有冗余的描述。这个本质的东西就是信息量(即不确定因素)。
压缩可分为两大类,第一类压缩过程是可逆的,也就是说,从压缩后的图象能够完全恢复出原来的图象,信息没有任何丢失,称为无损压缩;第二类压缩过程是不可逆的,无法完全恢复出原图象,信息有一定的丢失,成为有损压缩。选择哪一类压缩,要折中考虑,尽管我们希望能够无损压缩,但是通常有损压缩的压缩比(即原图象占的字节数与压缩后图象占的字节数之比,压缩比越大,说明压缩效率越高)比无损压缩的高。
图象压缩一般是通过改变图象的表示方式来达到,因此压缩和编码是分不开的。图象压缩的主要应用是图象信息的传输和存储,可广泛地应用于广播电视,电视会议,计算机通讯,传真,多媒体系统,医学图象,卫星图象等领域。
压缩编码的方法有很多,主要分成以下4大类:1.像素编码;2.预测编码;3.变换编码;4.其它方法。
所谓像素编码是指,编码时对每个像素单独处理,不考虑像素之间的相关性。在像素编码中常用的几种方法有:1.脉冲编码调制(Pulse Code Modulation,PCM);2.熵编码(Entropy Coding);3.行程编码(Run Length Coding);4.位平面编码(Bit Plane Coding)。这里面,我们要介绍的是熵编码中的哈夫曼(Huffman)编码,行程编码(以读取.PCX文件为例)。
所谓预测编码是指,去掉相邻像素之间的相关性和冗余性,只对新的信息进行编码。举个简单的例子,因为像素的灰度是连续的,所以在一片区域中,相邻像素之间灰度值的差别可能很小。如果我们只记录第一个像素的灰度,其它像素的灰度都用它与前一个像素灰度之差来表示,就能起到压缩的目的。如248,2,1,0,1,3,实际上这6个像素的灰度是248,250,251,251,252,255。表示250需要8个比特,而表示2只需要两个比特,这样就实现了压缩。
常用的预测编码有Δ调制(Delta Modulation,简称DM);微分预测编码(Differential Pulse Code Modulation,DPCM),具体的细节,我们就不详述了。
所谓变换编码是指,将给定的图象变换到另一个数据域(如频域)上,使得大量的信息能用较少的数据来表示,从而达到压缩的目的。变换编码有很多,如1.离散傅立叶变换(Discrete Fourier Transform,DFT);2.离散余弦变换(Discrete Cosine Transform,DCT);3.离散哈达玛变换(Discrete Hadamard Transform,DHT)。
其它的编码方法也有很多,如混合编码(Hybird Coding),矢量量化(Vector Quantize,VQ),LZW算法。在这里,我们只介绍LZW算法的大体思想。
值得注意的是,近些年来出现了很多新的压缩编码方法,如使用人工神经元网络(Artificial Neural Network,ANN)的压缩编码算法;分形(Fractl);小波(Wavelet);基于对象(Object -Based)的压缩编码算法;基于模型(Model -Based)的压缩编码算法(应 用在MPEG4及未来的视频压缩编码标准中)。这些都超出了本讲座的范围。
本讲的最后,我们将以JPEG压缩编码标准为例,看看上面的几种编码方法在实际的压缩编码中是怎样应用的。
1. 哈夫曼(Huffman)编码
Huffman编码是一种常用的压缩编码方法,是Huffman于1952年为压缩文本文件建立的。它的基本原理是频繁使用的数据用较短的代码代替,较少使用的数据用较长的代码代替,每个数据的代码各不相同。这些代码都是二进制码,且码的长度是可变的。举个例子:假设一个文件中出现了8种符号S0,S1,S2,S3,S4,S5,S6,S7,那么每种符号要编码,至少需要3比特,假设编码成000,001,010,011,100,101,110,111(称做码字)。那么符号序列S0S1S7S0S1S6S2S2S3S4S5S0S0S1编码后变成000001111000001110010010011100101000000001,共用了42比特。我们发现S0,S1,S2这三个符号出现的频率比较大,其它符号出现的频率比较小,如果我们采用一种编码方案使得S0,S1,S2的码字短,其它符号的码字长,这样就能够减少占用的比特数。
例如,我们采用这样的编码方案:S0到S7的码字分别01,11,101,0000,0001,0010,0011,100,那么上述符号序列变成011110001110011101101000000010010010111,共用了39比特,尽管有些码字如S3,S4,S5,S6变长了(由3位变成4位),但使用频繁的几个码字如S0,S1变短了,所以实现了压缩。
上述的编码是如何得到的呢?随意乱写是不行的。编码必须保证不能出现一个码字和另一个的前几位相同的情况,比如说,如果S0的码字为01,S2的码字为011,那么当序列中出现011时,你不知道是S0的码字后面跟了个1,还是完整的一个S2的码字。我们给出的编码能够保证这一点。
下面给出具体的Huffman编码算法。
1.首先统计出每个符号出现的频率,上例S0到S7的出现频率分别为4/14,3/14,2/14,1/14,1/14,1/14,1/14,1/14。
2.从左到右把上述频率按从小到大的顺序排列。
3.每一次选出最小的两个值,作为二叉树的两个叶子节点,将和作为它们的根节点,这两个叶子节点不再参与比较,新的根节点参与比较。
4.重复3,直到最后得到和为1的根节点。
5.将形成的二叉树的左节点标0,右节点标1。把从最上面的根节点到最下面的叶子节点途中遇到的0,1序列串起来,就得到了各个符号的编码。
上面的例子用Huffman编码的过程如下图所示,其中圆圈中的数字是新节点产生的顺序。可见,我们上面给出的编码就是这么得到的。

图1. Huffman编码的示意图
产生霍夫曼编码需要对原始数据扫描两遍,第一遍扫描要精确地统计出原始数据中,每个值出现的频率,第二遍是建立霍夫曼树并进行编码,由于需要建立二叉树并遍历二叉树生成编码,因此数据压缩和还原速度都较慢,但简单有效,因而得到广泛的应用。
2.行程编码(Run Length Coding)
行程编码的原理也很简单:将一行中颜色值相同的相邻像素用一个计数值和该颜色值来代替。例如:aaabccccccddeee可以表示为3a1b6c2d3e。如果一幅图象是由很多块颜色相同的大面积区域组成,那么采用行程编码的压缩效率是惊人的。然而,该算法也导致了一个致命弱点,如果图象中每两个相邻点的颜色都不同,用这种算法不但不能压缩,反而数据量增加一倍。所以现在单纯采用行程编码的压缩算法用得并不多,PCX文件算是其中的一种.
PCX文件最早是PC Paintbrush软件所采用的一种文件格式,由于压缩比不高,现在用的并不是很多了。它也是由头信息,调色板,实际的图象数据三个部分组成。其中头信息的结构为:
typedef struct{
char manufacturer;
char version;
char encoding;
char bits_per_pixel;
WORD xmin,ymin;
WORD xmax,ymax;
WORD hres;
WORD vres;
char palette[48];
char reserved;
char colour_planes;
WORD bytes_per_line;
WORD palette_type;
char filler[58];
} PCXHEAD;
其中值得注意的是以下几个数据:manufacturer为PCX文件的标识,必须为0x0a;xmin为最小的x坐标,xmax最大的x坐标,所以图象的宽度为xmax-xmin+1,同样图象的高度为ymax-yin+1;bytes_per_line为每个编码行所占的字节数,过一会儿详细介绍。
PCX的调色板在文件的最后。以256色PCX文件为例,倒数第769个字节为颜色数的标识,256时该字节必须为12,剩下的768(256*3)为调色板的RGB值。
为了讲起来方便,下面我们针对256色PCX文件,介绍一下它的解码过程。编码是解码的逆过程,有兴趣的读者可以试着自己来完成。
解码是以行为单位的,该行所占的字节数由bytes_per_line给定。为此,我们开一个大小为bytes_per_line的解码缓冲区。一开始,将缓冲区的所有内容清零。从文件中读出一个字节C,若C>0xc0,说明是行程(Run Length)信息,即C的低6位表示后面连续的字节个数(所以最多63个连续颜色相同的像素,若还有颜色相同的像素,将在下一个行程处理),文件的下一个字节就是实际的图象数据(即该颜色在调色板中的索引值);若C<0xc0,则表示C是实际的图象数据。如此反复,直到这bytes_per_line个字节处理完,这一行的解码完成。PCX就是有若干个这样的解码行组成。
实现256色PCX文件解码的源程序
3. LZW算法的大体思想
LZW是一种比较复杂的压缩算法,其压缩效率也比较高。我们在这里只介绍一下它的基本原理:LZW把每一个第一次出现的字符串用一个数值来编码,在还原程序中再将这个数值还成原来的字符串。例如:用数值0x100代替字符串"abccddeee",每当出现该字符串时,都用0x100代替,这样就起到了压缩的作用。
至于0x100与字符串的对应关系则是在压缩过程中动态生成的,而且这种对应关系隐含在压缩数据中,随着解压缩的进行,这张编码表会从压缩数据中逐步得到恢复,后面的压缩数据再根据前面数据产生的对应关系产生更多的对应关系,直到压缩文件结束为止。LZW是无损的。GIF文件采用了这种压缩算法。
要注意的是,LZW算法由Unisys公司在美国申请了专利,要使用它首先要获得该公司的认可。
4.JPEG压缩编码标准
JPEG是联合图象专家组(Joint Picture Expert Group)的英文缩写,是国际标准化组织(ISO)和CCITT联合制定的静态图象的压缩编码标准。和相同图象质量的其它常用文件格式(如GIF,TIFF,PCX)相比,JPEG是目前静态图象中压缩比最高的。我们给出具体的数据来对比一下。例图采用Windows95目录下的Clouds.bmp,原图大小为640*480,256色。用工具SEA(version1.3)将其分别转成24位色BMP,24位色JPEG,GIF(只能转成256色)压缩格式,24位色TIFF压缩格式,24位色TGA压缩格式,得到的文件大小(以字节为单位)分别为:921,654;17,707;177,152;923,044;768,136。可见JPEG比其它几种压缩比要高得多,而图象质量都差不多(JPEG处理的颜色只有真彩和灰度图)。 正是由于其高压缩比,使得JPEG被广泛地应用于多媒体和网络程序中,例如HTML语法中选用的图象格式之一就是JPEG(另一种是GIF),这是显然的,因为网络的带宽是非常宝贵的,选用一种高压缩比的文件格式是十分必要的。
JPEG有几种模式,其中最常用的是基于DCT变换的顺序型模式,又称为基本系统(Baseline),以下都是针对这种格式进行讨论。
JPEG的压缩原理
JPEG的压缩原理其实上面介绍的那些原理的综合,博采众家之长,这也正是JPEG有高压缩比的原因。其编码器的流程为

图3. 编码器流程
解码器基本上为上述过程的逆过程:

图4. 解码器流程
8*8的图象经过DCT变换后,其低频分量都集中在左上角,高频分量分布在右下角(DCT变换实际上是空间域的低通滤波器)。由于该低频分量包含了图象的主要信息(如亮度),而高频与之相比,就不那么重要了,所以我们可以忽略高频分量,从而达到压缩的目的。如何将高频分量去掉,这就要用到量化,它是产生信息损失的根源。这里的量化操作,就是将某一个值除以量化表中对应的值。由于量化表左上角的值较小,右上角的值较大,这样就起到了保持低频分量,抑制高频分量的目的。
JPEG使用的颜色是YUV格式。我们提到过,Y分量代表了亮度信息,UV分量代表了色差信息。相比而言,Y分量更重要一些。我们可以对Y采用细量化,对UV采用粗量化,可进一步提高压缩比。所以上面所说的量化表通常有两张,一张是针对Y的;一张是针对UV的。
上面讲了,经过DCT变换后,低频分量集中在左上角,其中F(0,0)(即第一行第一列元素)代表了直流(DC)系数,即8*8子块的平均值,要对它单独编码。由于两个相邻的8*8子块的DC系数相差很小,所以对它们采用差分编码DPCM,可以提高压缩比,也就是说对相邻的子块DC系数的差值进行编码。8*8的其它63个元素是交流(AC)系数,采用行程编码。这里出现一个问题:这63个系数应该按照怎么样的顺序排列? 为了保证低频分量先出现,高频分量后出现,以增加行程中连续"0"的个数,这63个元素采用了"之"字型(Zig-Zag)的排列方法,如下图所示:

图5. Zig-Zag
这63个AC系数行程编码的码字用两个字节表示,如下图

图6. 行程编码
上面,我们得到了DC码字和 AC行程码字。为了进一步提高压缩比,需要对其再进行熵编码,这里选用Huffman编码,分成两步:
(1) 熵编码的中间格式表示:
对于AC系数,有两个符号。符号1:行程和尺寸,即上面的(RunLength,Size)。(0,0)和(15,0)是两个比较特殊的情况。(0,0)表示块结束标志(EOB),(15,0)表示ZRL,当行程长度超过15时,用增加ZRL的个数来解决,所以最多有三个ZRL(3*16+15=63)。符号2为幅度值(Amplitude)。
对于DC系数,也有两个符号。符号1:尺寸(Size);符号2为幅度值(Amplitude)。
(2) 熵编码:
对于AC系数,符号1和符号2分别进行编码。零行程长度超过15个时,有一个符(15,0),块结束时只有一个符号(0,0)。 对符号1进行Hufffman编码(亮度,色差的Huffman码表不同)。对符号2:进行变长整数VLI编码,举例来说:Size=6时,Amplitude的范围是-63~-32,以及32~63,对绝对值相同,符号相反的码字之间为反码关系。所以AC系数为32的码字为100000,33的码字为100001,-32的码字为011111,-33的码字为011110。符号2的码字紧接于符号1的码字之后。
对于DC系数,Y和UV的Huffman码表也不同。掉了这么半天的书包,你可能已经晕了,呵呵。举个例子来说明上述过程就容易明白了。
下面为8*8的亮度(Y)图象子块经过量化后的系数。
15 0 -1 0 0 0 0 0
-2 -1 0 0 0 0 0 0
-1 -1 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
可见量化后只有左上角的几个点(低频分量)不为零,这样采用行程编码就很有效。
第一步,熵编码的中间格式表示:先看DC系数。假设前一个8*8子块DC系数的量化值为12,则本块DC系数与它的差为3,根据下表
Size Amplitude
0 0
1 -1,1
2 -3,-2,2,3
3 -7~-4,4~7
4 -15~-8,8~15
5 -31~-16,16~31
6 -63~-32,32~63
7 -127~-64,64~127
8 -255~-128,128~255
9 -511~-256,256~511
10 -1023~512,512~1023
11 -2047~-1024,1024~2047
查表得Size=2,Amplitude=3,所以DC中间格式为(2)(3).AC系数接着被编码,经过Zig-Zag扫描后,遇到的第一个非零系数为-2,其中遇到零的个数为1(即RunLength),根据下面这张AC系数表
Size Amplitude
1 -1,1
2 -3,-2,2,3
3 -7~-4,4~7
4 -15~-8,8~15
5 -31~-16,16~31
6 -63~-32,32~63
7 -127~-64,64~127
8 -255~-128,128~255
9 -511~-256,256~511
10 -1023~512,512~1023
查表得Size=2。所以RunLength=1,Size=2,Amplitude=3,所以AC中间格式为(1,2)(-2)。
其余的点类似,可以求得这个8*8子块熵编码的中间格式为
(DC)(2)(3),(1,2)(-2),(0,1)(-1),(0,1)(-1),(0,1)(-1),(2,1)(-1),(EOB)(0,0)
第二步:熵编码
对于(2)(3):2查DC亮度Huffman表得到11,3经过VLI编码为011
对于(1,2)(-2):(1,2)查AC亮度Huffman表得到11011,-2是2的反码,为01
对于(0,1)(-1):(0,1)查AC亮度Huffman表得到00,-1是1的反码,为0
……
最后这一8*8子块亮度信息压缩后的数据流为11011,1101101,000,000,000,111000,1010。总共31比特,其压缩比是64*8/31=16.5,大约每个像素用半个比特。
可以想见,压缩比和图象质量是呈反比的,以下是压缩效率与图象质量之间的大致关系,可以根据你的需要,选择合适 的压缩比。
压缩效率(单位:bits/pixel) 图象质量
0.25~0.50中~好,可满足某些应用
0.50~0.75好~很好,满足多数应用
0.75~1.5 极好,满足大多数应用
1.5~2.0与原始图象几乎一样
以上我们介绍了JPEG压缩的原理,其中DC系数使用了预测编码DPCM,AC系数使用了变换编码DCT,二者都使用了熵编码Huffman,可见几乎所有传统的压缩方法在这里都用到了。这几种方法的结合正是产生JPEG高压缩比的原因。顺便说一下,该标准是JPEG小组从很多种不同中方案中比较测试得到的,并非空穴来风。
上面介绍了JPEG压缩的基本原理,下面介绍一下JPEG的文件格式。
JPEG的文件格式
JPEG文件大体上可以分成以下两个部分:标记码(Tag)加压缩数据。先介绍标记码部分。
标记码部分给出了JPEG图象的所有信息(有点类似于BMP中的头信息,但要复杂的多),如图象的宽,高,Huffman表,量化表等等。标记码有很多,但绝大多数的JPEG文件只包含几种。标记码的结构为:
SOI
DQT
DRI
SOF0
DHT
SOS
…
EOI
标记码由两个字节组成,高字节为0XFF,每个标记码之前可以填上个数不限的填充字节0XFF。
下面介绍一些常用的标记码的结构及其含义。
SOI(Start of Image)
标记结构 字节数
0XFF1
0XD81
可作为JPEG格式的判据(JFIF还需要APP0的配合)
APP0(Application)
标记结构 字节数 意义
0XFF1
0XE01
Lp2 APP0标记码长度,不包括前两个字节0XFF,0XE0
Identifier5JFIF识别码 0X4A,0X46,0X49,0X46,0X00
Version 2JFIF版本号 可为0X0101或者0X0102
Units 1单位,等于零时表示未指定,为1表示英寸,为2表示厘米
Xdensity2水平分辨率
Ydensity2竖直分辨率
Xthumbnail1水平点数
Ythumbnail1竖直点数
RGB03RGB的值
RGB13RGB的值
…
RGBn 3 RGB的值,n=Xthumbnail*Ythumbnail
APP0是JPEG保留给Application所使用的标记码,而JFIF将文件的相关信息定义在此标记中。
DQT(Define Quantization Table)
标记结构 字节数 意义
0XFF1
0XDB1
Lq2DQT标记码长度,不包括前两个字节0XFF,0XDB
(Pq,Tq)1 高四位Pq为量化表的数据精确度,Pq=0时,Q0~Qn的值为8位,Pq=1时,Qt的值为16位,Tq表示量化表的编号,为0~3。在基本系统中,Pq=0,Tq=0~1,也就是说最多有两个量化表。
Q01或2 量化表的值,Pq=0时,为一个字节,Pq=1时,为两个字节
Q11或2 量化表的值,Pq=0时,为一个字节,Pq=1时,为两个字节
…
Qn1或2 量化表的值,Pq=0时,为一个字节,Pq=1时,为两个字节n的值为0~63,表示量化表中64个值(之字形排列)
DRI(Define Restart Interval)
此标记需要用到最小编码单元(MCU,Minimum Coding Unit)的概念。前面提到,Y分量数据重要,UV分量的数据相对不重要,所以可以只取UV的一部分,以增加压缩比。目前支持JPEG格式的软件通常提供两种取样方式YUV411和YUV422,其含义是YUV三个分量的数据取样比例。举例来说,如果Y取四个数据单元,即水平取样因子Hy乘以垂直取样因子Vy的值为4,而U和V各取一个数据单元,即Hu*Vu=1,Hv*Vv=1。那么这种部分取样就称为YUV411。如下图所示:
图7. YUV411的示意图
易知YUV411有50%的压缩比(原来有12个数据单元,现在有6个数据单元),YUV422
有33%的压缩比(原来有12个数据单元,现在有8个数据单元)。 那么你可能会想,YUV911,YUV1611压缩比不是更高嘛?但是要考虑到图象质量的因素。所以JPEG标准规定了最小编码单元MCU,要求Hy*Vy+Hu*Vu+Hv*Vv≤10。
MCU中块的排列方式与H,V的值有密切关系。如以下几幅图所示:

图8. YUV111的排列顺序

图9. YUV211的排列顺序

图10. YUV411的排列顺序
标记结构 字节数 意义
0XFF1
0XDD1
Lr2DRI标记码长度,不包括前两个字节0XFF,0XDD
Ri2重入间隔的MCU个数,Ri必须是一MCU行中MCU个数的整数,最后一个零头不一定刚好是Ri个MCU。每个重入间隔各自独立编码。
SOF(Start of Frame) 在基本系统中,只处理SOF0
标记结构 字节数 意义
0XFF1
0XC01
Lf2SOF标记码长度,不包括前两个字节0XFF,0XC0
P1 基本系统中,为0X08
Y2 图象高度
X2 图象宽度
Nf1Frame中的成分个数,一般为1或3,1代表灰度图,3代表真彩图
C11成分编号1
(H1,V1)1 第一个水平和垂直采样因子
Tq11该量化表编号
C21成分编号2
(H2,V2)1第二个水平和垂直采样因子
Tq21该量化表编号
…
Cn1 成分编号n
(Hn,Vn)1第n个水平和垂直采样因子
Tqn1该量化表编号
DHT(Define Huffman Table)
标记结构 字节数 意义
0XFF1
0XC41
Lh2DHT标记码长度,不包括前两个字节0XFF,0XC4
(Tc,Th)1
L11
L21
…
L161
V11
V21
…
Vt1
Tc为高4位,Th为低4位。在基本系统中,Tc为0或1,为0时,指DC所用的Huffman表,为1时,指AC所用的Huffman表。Th表示Huffman表的编号,在基本系统中,其值为0或1。
所以,在基本系统中,最多有4个Huffman表,如下所示:
Tc Th Huffman表编号(2*Tc+Th)
0 0 0
0 1 1
1 0 2
1 1 3
Ln表示每个n比特的Huffman码字的个数,n=1~16
Vt表示每个Huffman码字所对应的值,也就是我们前面所讲的符号1,对DC来说该值为(Size),对AC来说该值为(RunLength,Size)。 t=L1+L2+…L16
SOS(Start of Scan)
标记结构 字节数 意义
0XFF1
0XDA1
Ls2 DHT标记码长度,不包括前两个字节0XFF,0XDA
Ns1
Cs11
(Td1,Ta1)1
Cs21
(Td2,Ta2)1
…
CsNs1
(TdNs,TaNs) 1
Ss1
Se1
(Ah,Al)1
Ns为Scan中成分的个数,在基本系统中,Ns=Nf(Frame中成分个数)。CSNs为在Scan中成分的编号。TdNs为高4位,TaNs为低4位,分别表示DC和AC编码表的编号。在基本系统中Ss=0,Se=63,Ah=0,Al=0。
EOI(End of Image) 结束标志
标记结构 字节数 意义
0XFF1
0XD91
JPEG基本系统解码器的程序流程图。

图11. JPEG基本系统解码器的程序流程图
由于没有用到什么优化算法,该解码器的速度并不高,在用VC的性能评测工具Profile评测该程序时我发现最耗时的地方是反离散余弦变换(IDCT)那里,其实这是显然的,浮点数的指令条数要比整数的多得多,因此采用一种快速的IDCT算法能很大的提高性能,我这里采用是目前被认为比较好的一种快速IDCT算法,其主要思想是把二维IDCT分解成行和列两个一维IDCT。
Fvwm is a window manager for X11. It is designed to minimize memory
consumption, provide a 3D look to window frames, and a virtual desktop.
--- The FVWM(F? Virtual Window Manager) manual
- FVWM 是什么样子?
这个问题是永远不会有答案的。问这个问题就像在问:“Xwindow
是什么样子的?”也许勉强可以接受的回答是:“你想让它是什么样子,它就会成为什么样子。”
FVWM 是一个完全可定制的窗口管理器。这是FVWM不同于很多WM的一个特点,它的一切行为方式都是由一个配置文件:
.fvwm2rc 决定的。没有了这个配置文件,FVWM 就成了一个废物:
鼠标和键盘几乎不起任何作用,没有菜单,没有窗口边框,没有按钮,甚至你根本不知道它其实正在运行!
但是一旦有了配置文件,FVWM就会变得威力无比,简单的配置文件可以实现基本的功能,复杂的配置文件甚至可以模拟很多其它WM甚至 Windows
XP。下面就是一个模拟 Windows XP 的例子,点击可以放大。很漂亮吧?什么时候 Windows XP 也能这么漂亮就好了 :)
下面是一个模拟 CDE 的 dtwm 的例子:
以后如果有人告诉你:“FVWM是那个样子。”你就可以对他说:“FVWM不一定是那个样子。” :)
你可以在http://www.fvwm.org/screenshots/看到很多漂亮的配置。
- 配置文件和它的位置
配置文件叫做 ~/.fvwm/.fvwm2rc, 在你的用户目录下。
配置文件有点像一个脚本语言。不要怕,这种脚本语言比起 Perl, awk 简单多了。你不需要学会编程。
写配置文件,一个很好的出发点就是随 FVWM 源码发行的 system.fvwm2rc 文件。它的位置现在在源码包的
sample.fvwmrc/system.fvwm2rc。你也可以在这里下载一份 2.5.4 的system.fvwm2rc.
把它拷贝到你的 ~/.fvwm/ 目录下面, 改名为 .fvwm2rc. 作为我们的起始点。
注意sample.fvwmrc/ 这个目录下还有system.fvwm2rc-sample-95这样的配置文件,它们可以模拟 Windows 95
的操作方式,但是这个配置文件太大了,不适合用来修改成为自己的配置文件,你有兴趣可以自己看看,然后把里面某些你觉得很cool的东西贴到你的配置文件里。
现在我们就来分析一下这个简单的配置文件里到底在说些什么。
- 什么是FVWM命令
一个配置文件里基本上是一些命令,与其它WM不同的是,FVWM并不区分样式命令和动作命令,你可以几乎在任何情况下使用任何命令。比如命令:
Mouse 1 A CSM Style gvim TitleAtBottom
让你在任何时候按住 Ctrl-Shift-Alt 再点击鼠标左键(编号1),名叫gvim的窗口的标题栏就会跑到下面去。是不是很好玩?呵呵。这里
"Style" 是一个样式命令。Mouse 2 A CSM All (rxvt) MoveToDesk 0
按住 Ctrl-Shift-Alt 再点击鼠标左键中键(编号2),所有的 rxvt 都会被移动到当前的桌面. 这里的 "All"
是一个可以附加条件和操作的条件动作命令。
- 怎样实验新的FVWM命令
实验一个命令的作用不需要重新启动FVWM,你可以先在 .fvwm2rc 里这样定义:
Key F3 A A Module FvwmConsole
然后启动fvwm, 这样你在任何时候按下 <F3> 键,就会启动一个叫做 FvwmConsole
的模块,你可以在里面输入Fvwm命令,回车它们就会执行,并且立即生效。这是直接与FVWM对话的方法。当然如果你想得到下面这么漂亮的 FvwmConsole,
还需要对 xterm/rxvt 的参数作一些设定。其实我的定义是:Key F3 A A Module FvwmConsole -terminal rxvt -geometry 45x5-0+0 \
-bg gold -fg midnightblue \
-fn "-adobe-courier-medium-r-*-*-14-*-*-*-*-*-*-*"
- DeskTopSize 2x2 以及其它常用的命令
FVWM
可以有很多个虚拟桌面(desk)(几乎无穷多!),每个虚拟桌面可以被分成很多个页("page")。这些page相当于把你的屏幕扩大了很多倍。这一行就是设定每个desk包含多少page.
DeskTopSize 这类语句指定了整个FVWM的某种行为方式,类似的语句还有很多。比如:
- Read file. 插入另外一个文件file的内容。如果你会C语言,你就知道这个语句相当于 #include
.
当你的配置文件在某一方面有很长内容时,比如我的配置文件里有大量stroke,你就可以把这些都写到另一个文件里,这样使主配置文件容易修改。
- Exec app. 启动一个shell,并且在里面执行"app"命令。这时用菜单和按钮来启动程序时经常用到的命令。
如果你要启动一个X程序最好同时使用 exec, 比如
Exec exec rxvt
这样shell会执行"exec rxvt", 用rxvt替代自己的正文段,这样才不会出现很多shell在那里等待X程序返回。
- Module FvwmXxx. 启动一个叫做 FvwmXxx 的模块。一个模块是一个程序,它直接通过管道与Fvwm通信,所以必须从Fvwm
fork() 出来,而不能从一个 xterm 独立启动。模块可以无限制的扩展Fvwm的功能,只要 Xlib 允许。
- ImagePath path. 指定一个路径,在配置文件里要用到的图标等文件就会到这个路径里去寻找。比如:
ImagePath +:/usr/share/icons:/usr/share/pixmaps:
加号是表示以前定义过的那个 ImagePath. FVWM 可以使用 .xbm, .xpm 和 .png 格式的图标。
$HOME/.fvwm/icons
- Move, Raise, Lower, Resize, ... 这些都是常用的操纵窗口的命令。
- Close, Delete, Destroy, Iconify, Maximize, WindowShade.
关闭窗口,图标化窗口,最大化窗口,shade 化窗口. 你会发现Xwindow关闭窗口的方式有很多种,其中 Close
是最文明的一种,这会发给窗口一个消息让它收拾干净然后退出。Destroy 是立即杀死这个窗口,跟 xkill
的功能一样。Delete介于两者之间,先礼后兵,如果窗口不知道怎么收拾干净,那么就强行杀死它。
- OpaqueMoveSize x. 如果一个窗口移动时显示内容,那么它必需占屏幕面积的 x%.
- MoveToDesk, MoveToPage. 可以把任何窗口移动到指定的桌面和页面。
- SnapAttraction. 设定在什么距离以内,满足什么条件的窗口就被吸附在一起。biaji~~~~~
- WarpToWindow x[p] y[p]. 让鼠标移动到窗口范围以内。x,y是在窗口内的坐标,用百分比表示。后面如果有后缀"p",
就用像素来表示。
- IgnoreModifiers. 你可以忽略某些键盘控制键。这将影响到你的鼠标和键盘热键定义。详细情况见鼠标和键盘一节。
- DesktopName desk name. 定义第desk号桌面的名字叫 name.
- Scroll. 移动你在桌面上的 viewport, 这样你可以把桌面当成一个整体来浏览。看到很大的范围。
- Nop. 不操作,在有些时候需要用它来占位,下面我们会遇到这样的例子。
- PipeRead. 从一个外部命令得到输入。这可以用来根据你的系统构造许多非常高级的控制方式。其中一种叫做“菜单式文件管理器”。我们在菜单一节会遇到这个用法。
- SetEnv. 设置FVWM的环境变量。
就举这些吧……我只是举出了我有时会用到的,其实还有很多很多,你看看 FVWM 的manpage就知道了。
- Read file. 插入另外一个文件file的内容。如果你会C语言,你就知道这个语句相当于 #include
- 窗口上下文
Move, Close, WarpToWindow
...这些命令如何知道作用于那个窗口呢?如果你因为点击了窗口上的按钮,边框,……而激发了这些命令,那么这些命令就会作用于这个窗口。或者你也可以用条件选择命令确定一个或者一批窗口进行操作,见条件命令。否则,这些命令不知道应该作用于哪个窗口,比如你在
FvwmConsole 里键入 "Close", Close
命令就没有窗口上下文,它缺省会出现一个“+”状的选择器让你选择一个窗口。如果你不希望命令在没有窗口上下文的时候自动让你选择一个窗口,那么你可以在命令前面加上
"Silent".
- 鼠标和键盘
FVWM几乎可以以无穷的方式组合,来进行鼠标和键盘的操作。你还可以加入窗口上下文来进行更方便的动作。
键盘操作的定义:
Key Keyname Context Modifiers Function
它表示:在名叫 Keyname 的键在 Context 上下文按下时,如果控制键 Modifiers 组合按下,那么执行 Function.
键盘操作后面的部分跟鼠标一样的含义,我们下面只用鼠标操作来一起说明这些命令的用途。
鼠标操作的定义:
Mouse Button Context Modifiers Function
它表示:在鼠标编号为 Button 的键在 Context 上下文按下时,如果键盘控制键 Modifiers 组合按下,那么执行 Function.
鼠标键编号的方法是:1 左键,2 中键,3 右键。如果你的鼠标有轮子,那么一般4表示往上滚动,5表示往下滚动。
Context是鼠标按下的位置,它可以是:
- R(Root Window) 根窗口
- n (n 是0...9 之间的一个数)。第 n 号窗口按钮。按钮是这样编号的:
1 3 5 7 9 0 8 6
4 2
左边是奇数右边是偶数, 外面的大中间的小。
- T(Title)标题栏
- S(Sidebar)也就是边框. 也可以用 '[', ']', '-' , '_' 分别表示左,右,上,下的边框。
- F(Frame)就是用来resize的那四个角落. 也可以用 '<', '^', '>' and 'v'
分别表示左上,右上,右下,左下的角落。
- W(Working Area) 应用程序窗口工作区域
- I(Icon window) 图标化的窗口。
这些上下文可以组合。比如 "FST" 表示在frame, sidebar, 或者 title.
Modifiers 是鼠标操作时同时的键盘控制键。M 表示 "Meta",在PC上就是Alt,S: shift, C: ctrl. 还有 A:
any, N: none. 也可以组合,比如"MS" 表示同时按下Alt-Shift.
Function 就是任意的FVWM操作了,可以是一个直接的命令,也可以是一个 FVWM 函数。
现在我们分析一下下面这个定义:
Mouse 3 W SC CloseOrNot
这个定义是说,在窗口上点击鼠标右键,并且先按下 Shift-Ctrl,那么调用 CloseOrNot
这个FVWM函数。这个函数会作用与当前鼠标所在的上下文,也就是一个窗口。函数是这样定义的:
- R(Root Window) 根窗口
- 函数
DestroyFunc CloseOrNot
AddToFunc CloseOrNot
+ C Silent Close
+ M Nop
你可以把一系列的操作有条件的加入到一个叫做“函数”的结构里,以后这个函数就可以像命令一样被使用了。
AddToFunc 把动作附加到函数,
如果函数不存在就先创建这个函数。除了第一行,后面的行都以一个"+"号开头,这说明以下是上一个命令(AddToFunc)的继续。DestroyFunc
是为了消除以前有可能定义过的函数体。这个函数 CloseOrNot 表示:
- 如果是一个鼠标点击(C), 那么关闭这个窗口(Close),
但是如果现在不是在窗口上下文,也就是说,函数调用的时候没有一个确定的目标,那么不进行操作,而不是出现一个"+"字瞄准器让用户选择窗口。 这就是
"Silent" 的含义。
- 如果鼠标点下去之后移动了,也就是“拖动”(M),那么不进行操作。
- 如果是一个鼠标点击(C), 那么关闭这个窗口(Close),
- 启动函数和退出函数
在 FVWM 启动和重新启动时都会调用 StartFunction, 而且在首次启动时会调用 InitFunction, 在重新启动时会调用
RestartFunction, InitFunction 和 RestartFunction 都是在 StartFunction
之后调用。每次重起和完全退出时都要执行 ExitFunction.
如果你有什么程序需要在FVWM启动时启动,那么就把它加到合适的函数里面去。比如,我的配置文件有这些内容:
DestroyFunc StartFunction
AddToFunc StartFunction
+ I Module FvwmButtons MainPanel
+ I Module FvwmAuto 500 Raise Nop
+ I Module FvwmAnimate
+ I Module FvwmTaskBar
+ I Exec exec xdaliclock
+ I Exec exec xloadimage -onroot -fullscreen ~/pic/cat_20.jpg
+ I Exec exec xsim
DestroyFunc InitFunction
AddToFunc InitFunction
+ I Exec exec xscreensaver -no-splash
DestroyFunc ExitFunction
AddToFunc ExitFunction
+ I All (xdaliclock) Close
+ I All (xscreensaver) Close
+ I All (xsim) Close
可见,我在第一次启动时会启动 xscreensaver 屏幕保护程序。-no-splash 是 xscreensaver
的参数。在每次重新启动和第一次启动时都要运行 FvwmButtons, FvwmAuto, FvwmAnimate, FvwmTaskBar 几个模块和
xdaliclock,一种 morph 数字的时钟,然后用 xloadimage 放一张漂亮的图片作为桌面背景,最后启动 xsim 中文输入法。
退出和重起时,我特意关闭了那几个启动时打开的程序,因为如果不关闭他们,像 xwin32, Exceed 这样的 Windows X server 不会
Reset.
每个命令前的 "I" 表示 Imediately, 立即执行,联想上面提到的 "C" 和 "M", 这个操作不等待任何鼠标动作。
- 窗口样式
Style 语句用于设定窗口的样式。你可以随心所欲的让不同的窗口有不同的样式。语法为:
Style stylename options
其中 stylename
是你的窗口的名字,窗口的class名字,或者窗口的resource名字。如果你不知道这些 X window
的术语,那现在就姑且当作窗口的名字好了,以后多看看 Xlib 的说明书你就会明白这些东西。窗口的名字有可能不同于程序的名字,你不知道它叫什么名字可以用
xwininfo 程序或者 FvwmIdent 模块来查询。再次说明,FvwmIdent 是模块,不能从 xterm 的命令行运行。
stylename 里可以有 "*" 作为通配符。比如你可以说
Style *term TitleAtLeft
让所有以 "term" 结尾的那些窗口的标题拦都在左边。比如
"xterm", "cxterm", "qterm", ... 都会采用这种样式。
options 是你想让满足条件的窗口以什么样的方式存在。options
的种类非常之多。比如:BorderWidth, HandleWidth, FocusFollowsMouse, TileCascadePlacement,
... 它们有的需要参数,比如 BorderWidth 7, 指定边框宽度为7个像素。有些不需要参数,比如 FocusFollowsMouse/
SloppyFocus/ NeverFocus/ ClickToFocus 指明了几种互相排斥的键盘聚焦方式。 先举几个例子,这些都是
system.fvwm2rc 里的内容:Style * FocusFollowsMouse
Style * TileCascadePlacement
Style "Fvwm*" NoTitle, Sticky, WindowListSkip
Style "Fvwm*" BorderWidth 2, CirculateSkipIcon, CirculateSkip
Style "FvwmPager" StaysOnTop
Style "FvwmBanner" StaysOnTop
Style "FvwmButtons" Icon toolbox.xpm, ClickToFocus
开头的两行说明所有窗口,都是鼠标移进去的时候得到键盘聚焦,鼠标移出来就失去聚焦(FocusFollowsMouse),窗口出现的时候,先试图找一个可以放下它而不挡住其它窗口的地方,如果不行再采用层叠放置的方式(TileCascadePlacement)。
下面是说明所有名字以 "Fvwm" 开头的窗口(在这里一般都是 FVWM
内部的模块),它们都没有标题栏(NoTitle),而且是sticky,也就是说即使桌面切换,它们也一直显示在屏幕上,边框宽度为 2,
CirculateSkip 说明当FVWM要求轮询窗口进行批量操作时,这些窗口不被计算在内。第3,4行说明 FvwmPager, FvwmBanner
这两个模块一直显示在最上面。最后一行说明 FvwmButtons 模块使用 toolbox.xpm
的图标,需要鼠标点击才能得到键盘聚焦(ClickToFocus)。
总的说来,options指出了控制窗口的基本样式和政策,而不包括窗口各个部件具体的样式,它包括以下几个方面内容:
- 窗口聚焦方式。是跟随鼠标(FocusFollowsMouse),还是需要点击才聚焦(ClickToFocus)...
- 窗口标题栏。是否给窗口加上标题拦?如果加上,是放在左边,上边,还是下边?注意这里也不是设定具体标题样式的地方,参看 TitleStyle.
- 窗口应该显示哪些按钮。FVWM可以为每个窗口设定最多10个按钮,但是你通常用不到10个,这样你可以设定对于某一个程序那些按钮应该出现。注意这里也不是设定按钮样式的地方,参看
ButtonStyle.
- 窗口边框样式。边框宽度,handle(就是边框角上那个用来resize的东东)的宽度,边框被鼠标按住的时候是否陷下去?...
- 窗口图标。用那个图标作为窗口iconify时候的图标?
- 窗口最大化,移动,改变大小操作时的样式。是显示窗口内容还是只显示一个“橡皮框”?还是让尺寸小于某个值的窗口才在拖动时显示内容?...
- 窗口放置策略。窗口出现的时候,是层叠放置,最小遮挡放置,还是……?
- 是否允许程序自己放置自己?这是一个政策问题,有些窗口程序启动时会自己选择一个位置出现,但是你可能会发现你不喜欢它那样做,你可以设定NoPPosition,
不允许那个程序自作聪明。
- 对瞬时窗口(transient window)的策略。transient window
是指类似弹出菜单,对话框之类的窗口。当它们出现的时候,你是否想给它们也加上标准的边框?
- 高级特性。还有很多很多选项比如是否允许窗口 backing store,这些如果你还不理解现在暂时不用管它。
以上每项都包含许许多多可以设定的东西。具体还是请参考 fvwm 的manpage。
- 窗口聚焦方式。是跟随鼠标(FocusFollowsMouse),还是需要点击才聚焦(ClickToFocus)...
- 菜单
一个窗口管理器怎么能没有菜单?FVWM的菜单是可以随意自己定义的,它在任何时候出现在你想让它出现的任何地方。一个菜单首先有一个定义,然后有一个激发这个菜单的条件,菜单的样式也可以随意定制。如果使用
PipeRead 命令和一些 shell 命令组合,你就可以用菜单的方式遍历你的文件目录树,成为一个“菜单式file manager”。
- 菜单的定义
菜单是由AddToMenu命令定义的,比如这样一个菜单
是这样定义的
DestroyMenu RootMenu
AddToMenu RootMenu "Root Menu" Title
+ "&xterm%mini.display.xpm%" Exec exec xterm
+ "&Rxvt%mini.monitor.xpm%" Exec exec rxvt
+ "&Big Rxvt" Exec exec rxvt -geometry 78x43
+ "" Nop
+ "&Programs" Popup ProgramsMenu
+ "&Utilities" Popup Utilities
+ "" Nop
+ "Re&fresh Screen" Refresh
+ "Re&capture Screen" Recapture
+ "" Nop
+ "&Lock" Exec exec xscreensaver-command -lock
+ "&Exit Fvwm%mini.exit.xpm%" Popup Quit-Verify
除了第一行,后面的行都以一个+号开头,这说明以下是上一个命令的继续。这样我们定义了一个菜单,它的名字叫 "RootMenu",
它有一个标题叫"Root Menu", 里面有一些程序。当我们选中 "XTerm" 时,会使用FVWM 的 Exec
命令启动一个shell,这个shell马上会执行"exec xterm", 也就是启动一个 xterm。空字串"" 表示在菜单里画一条分隔线。Popup
可以弹出子菜单,子菜单也是用同样的方法定义的。"&"后面的那个字母会变成键盘的热键而被加上下划线,"%"括起来的是图标文件的名字,你需要设置
ImagePath 指向图标文件所在的目录。你还可以在菜单里加入侧面图标,等等等等。
- 菜单的消灭
随后的AddToMenu命令会把内容附加到菜单的末尾。所以如果你想重新定义一个菜单,就需要先把它销毁掉。用
DestroyMenu RootMenu
就可以把刚才那个 "RootMenu" 菜单消灭掉。
- 菜单的激活
光是定义了一个菜单你是不能马上使用它的。这个菜单在什么情况下出现?这个问题是需要你自己来决定,这也是显示FVWM的完全可定制性的地方。比如,我们可以这样定义一个激活菜单的方式:
Mouse 1 R A Menu RootMenu Nop
这句话的意思是:“当鼠标(Mouse)左键(1)在根窗口(R)上点击,同时有任何控制键(A)按下,这个时候显示叫做 RootMenu 的菜单。”
但是有时候我们不容易在屏幕上找到一个可以看到根窗口的地方来点击鼠标。我们可以再加一个定义:
Mouse 3 A MC Menu RootMenu Nop
这样,当右键(3) 在任何地方(A)点击, 同时有Alt(M)和Ctrl(C)按下,那么弹出名叫 "RootMenu" 的菜单。
上面的 "Nop" 表示的是鼠标在菜单上进行双击时的操作。我定义为不操作。另外 Menu 还可以随意定义菜单出现的位置,详细请看 fvwm
manpage。
- 菜单的样式
菜单的样式是由 MenuStyle 定义的:
MenuStyle * MWM
MenuStyle * PopupDelayed, PopupDelay 160, Animation, TitleWarp
MenuStyle * Foreground gold, Background gray40
MenuStyle * ActiveFore White
MenuStyle * Font -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
MenuStyle * MenuFace VGradient 64 darkgray MidnightBlue
这样我规定:所有的菜单,他们使用 mwm 的行为方式,弹出子菜单延时 160
ms,子菜单弹出时如果靠近屏幕边沿放不下,那么菜单整体移动使得子菜单刚好能弹出,前景色gold,背景色 gray40,
活动的项目(就是鼠标正在它上方的时候)前景色变为白色,菜单使用字体 -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*,
背景是垂直的梯度颜色,一共64阶,从 darkgray 变化到 MidnightBlue.
- 动态菜单定义
前面我们说过了,可以用 PipeRead 来构造一个动态菜单。现在举一个简单的例子:
AddToMenu HomeDirMenu
PipeRead 'for i in $HOME/prog/*.c; \
do echo "+ $i Exec xterm -e vi $i"; done'
当你激发这个菜单 HomeDirMenu,
就会出现你主目录/prog下的所有C程序文件的列表,当你点击其中一个就会启动vi来编辑这个C程序。是不是很方便呢?你想一想,可以用怎样无穷无尽的方式来构造一个菜单呢?
- 菜单的定义
- 按钮
窗口的标题栏上都有一些按钮。那不是窗口程序自己的,而是WM给它们加上的。
FVWM 可以给窗口加上最多10个按钮,它们不光可以实现基本的最大化,最小化,关闭,等功能。FVWM的灵活性允许你赋予按钮几乎任意的功能!
- 按钮编号
按钮是这样编号的:
1 3 5 7 9
0 8 6
4 2左边是奇数右边是偶数,
外面的大中间的小。
- 按钮功能定义
下面看看按钮的功能是怎么定义的,在鼠标和键盘一节我们已经知道怎么定义鼠标了,按钮的功能只不过是把鼠标与按钮号码组合在一起。
比如我的窗口上一般有三个按钮, 都在右上角,注意它们的编号:
6 4 2
他们的功能是这样定义的:
Mouse 1 4 A Iconify
其中 Maximize-Func2 是用了 system.fvwm2rc 里一个函数:
Mouse 1 6 A Close
Mouse 3 2 A Maximize-Func2DestroyFunc Maximize-Func2
AddToFunc Maximize-Func2 "M" Maximize 100 0
+ "C" Maximize 80 0
+ "D" Maximize 100 100
如果鼠标右键在“最大化”按钮上点击(C)那么高度增长为屏幕的 80%, 宽度不变。如果按下鼠标右键后有拖动(M),
那么高度增长为屏幕高度(100%), 宽度不变。如果双击(D), 就是一般的最大化。
为什么是右键?因为我为左键在这个按钮上定义了更高级的 stroke 来改变窗口大小。我们稍后介绍。
- 按钮样式
按钮的样式是用ButtonStyle定义的。比如我的那三个按钮实际上是如下几句话定义的。
ButtonStyle All -- UseTitleStyle
ButtonStyle All ActiveDown VGradient 8 palevioletred black
ButtonStyle 6 16 20x20@1 30x20@1 50x40@1 70x20@1 80x20@1 80x30@0 \
60x50@0 80x80@0 70x80@0 50x60@0 30x80@0 20x80@0 \
20x70@0 40x50@1 20x30@0 20x20@1
那个X形状的关闭按钮实际上是用很简单的语句画出来的。ButtonStyle
之后的数字是按钮编号,后面一个数子表示一共有多少笔画。后面的XxY@C都是笔画的内容,XxY是坐标, 坐标都是用百分比表示的。@C 表示颜色,
C是一个数字,0 表示阴影色,1 是高亮色,2 是背景色,3 是前景色,4 是移动光标而不画线。
你可以画你自己的按钮,也可以去那别人设计好的来用 FVWM 的主页上有很多人提供这种按钮。
第二个语句 "ActiveDown VGradient 8 palevioletred black"
设定了所有按钮按下去还没有松开鼠标时候的样式,是一个颜色梯度。
- 按钮编号
- 其它样式
我们已经知道 Style 可以决定窗口的样式,MenuStyle 可以决定菜单的样式,ButtonStyle 按钮的样式。其实还有
CursorStyle, TitleStyle, BorderStyle. 他们决定了光标,标题栏,边框的样式。他们都有多样的语法,详细的就看
magpage 吧。这里就不照抄了。
- 条件命令
All, Any, Cond, Current, Direction, Next, None, Pick, WindowId, ...
这些命令是条件选择窗口的办法,它们让你可以用非常多样的方法,来确定你的操作需要对哪一个或者哪些窗口进行。比如:All (Iconic) MoveToPage -1 -1
把所有图标化的窗口都移动到桌面右下角的那一页。Key F5 A A Direction North Maximize True 0 growdown
以后按 F5 就可以让当前聚焦窗口上面(North)那个窗口往下长大,直到被当前窗口挡住去路。你有时候想在VIM里抄 Acrobat Reader
里的内容,安排窗口大小的时候就可以用这招。
- 手写操作 (Stroke)
你用过 EDA 软件吗?用过的话,你就可以知道鼠标动作(stroke)是多么的方便!你是否想在你的窗口管理器里也使用鼠标动作?
- 让 FVWM 支持 Stroke
如果你的FVWM窗口管理器编译进了 libstroke, 你就可以使用鼠标动作操纵程序。libstroke 是一个免费使用的 stroke
库,你可以在 http://www.etla.net/libstroke/得到 libstroke. 下载那个为 FVWM
准备的版本,编译后安装,然后再编译 FVWM,它一般就会找到 libstoke,从而加入 stroke 的功能。
比如我在屏幕上按住 ctrl, 用右键
- 画一个 "r" 字就可以启动 rxvt
- 画一个 "V" 就可以启动 vim
- 画出 "D" 右边的弧线就可以启动 IBM 智能辞典
- 画一个 "e" 启动 emacs ...
- 鼠标左右一晃,就可以启动 xkill,再往某个窗口一点,就可以强制杀死不听话的窗口
- 在窗口里右键往下一划,就可以最小化窗口
- 在窗口边框上用右键……
- 向上拖就可以使窗口往上一直长到被别的窗口挡住的地方
- 向左拖就可以使窗口往左一直长到被别的窗口挡住的地方
- 右……下……斜上…… 从边框开始画一个"L"形就可以回复窗口原来大小
当然这些控制方式都是你自己决定的, 这一切只需要在 .fvwm2rc 里加入一些Stroke语句. 因为太多了,写在主配置文件影响编辑,
这些语句被我写到了另一个文件里,然后在主文件用 Read 语句读入。你可以在这里下载我的fvwm.stroke文件作为参考。
你还可以定义非常高级的操作,你甚至可以这样:按住 ctrl, 用鼠标中键画出一条射线箭头指向的那个窗口,
不论它在那个桌面,就会被吸过来,并且随鼠标移动,你点击左键就可以放置它。
- 画一个 "r" 字就可以启动 rxvt
- 轨迹
stroke 的原理很简单,libstroke
可以识别出你在屏幕上画出的轨迹,把它报告给FVWM,这样FVWM根据轨迹的不同采取不同的操作。轨迹是由一个电话拨号盘的方式确定的。也就是说,把你画出的东西分成9个区域,看你的鼠标依次经过那几个区域。
1 2 3
轨迹也可以用你的小键盘上的数字键来确认。看看你的小键盘:
4 5 6
7 8 97 8 9
4 5 6
1 2 3
- Stroke 项目的定义
在你的配置文件里写入一些 Stroke 语句:
Stroke Sequence Button Context Modifiers Function
比如:Stroke N7414789 0 A C Exec exec rxvt
Stroke N7414759 0 A C Exec exec rxvt
Stroke N74147589 0 A C Exec exec rxvt
Stroke N7414756 0 A C Exec exec rxvt
Stroke N74156 0 A C Exec exec rxvt
Stroke N74159 0 A C Exec exec rxvt
Stroke 关键字之后跟上轨迹说明。轨迹是一系列数字,如果数字前面有一个"N",
就表示我们采用小键盘的布局,而不是电话拨号盘。你看我的那几个轨迹,实际上是我们在写 "r" 字母的时候有可能出现的几种情况。
比如,这个轨迹就是符合 "N7414589".
轨迹之后是鼠标按键号码。如果号码不是0,那么一旦识别到这个轨迹,就会马上执行操作。但是如果号码是0,那么说明这个定义不是在任何时候识别到就马上进行的。而是当
StrokeFunc 命令被调用的时候才进行。StrokeFunc 为你提供了更多的灵活性。
号码之后是 Context Modifiers Function. 他们跟 Mouse, Key 的那两个同名参数是一个意思,参看 鼠标和键盘.
- StrokeFunc
如果你的鼠标号码是0. 那么当 StrokeFunc 被调用的时候,这个轨迹如果被识别,就会执行相应的操作。比如:
#Drag mouse 1 on the maxmize button
现在看到了? 这就是我的最大化按钮上对鼠标左键的绑定。DrawMotion 是 StrokeFunc
Mouse 1 2 N StrokeFunc DrawMotion
的一个可选参数,它可以让轨迹在屏幕上被画出来,这样你可以清楚的看到你到底写了什么。
我有如下的一系列 stroke 定义:
#grow horizontal and vertically
Stroke N258 0 TSF2 N Maximize True 0 growup
Stroke N852 0 TSF2 N Maximize True 0 growdown
Stroke N456 0 TSF2 N Maximize True growright 0
Stroke N654 0 TSF2 N Maximize True growleft 0
#grow bidirectional
Stroke N25852 0 TSF2 N Maximize True 0 grow
Stroke N5852 0 TSF2 N Maximize True 0 grow
........
#reverse to unmaximized
Stroke N74123 0 TSF2 N Maximize False
我的鼠标左键按下“最大化”按钮之后可以进行绘画,然后窗口会随着轨迹的不同而采取各种各样的改变大小的行动!
我还有一个定义:
Mouse 3 TSF N StrokeFunc DrawMotion
这样鼠标右键在窗口标题栏,边框,frame 上绘画时也会触发 StrokeFunc
函数,达到跟左键在“最大化”按钮上绘画同样的效果。发现了吧?StrokeFunc
为我省去了重复的轨迹定义,否则我需要为“左键+最大化按钮”和“右键在边框”定义两套 stroke.
- 实例分析
我们来分析一种可能的执行情况:用鼠标左键按下“最大化”按钮(2),然后向右画。就像这个样子:
当鼠标左键在“最大化”按钮(2)上按下之后,如果没有键盘控制键按下(N),而那么根据"Mouse 1 2 N StrokeFunc
DrawMotion", FVWM就会发现应该调用 StrokeFunc.
StrokeFunc 会马上记录鼠标按下的时候有哪些控制键按下了,现在是没有控制键(N).
然后它发现鼠标随即向右画出了一条线,看看你的小键盘,这是N456。StrokeFunc 就会在已经定义的 Stroke
里去找,是否存在这样的一个定义,它的前面部分是Stroke N456 0 2 N ...
它发现有一个Stroke N456 0 TSF2 N Maximize True growright 0
它的 Context: TSF2 包含了标题栏按钮2。鼠标动作开始时没有控制键按下,而这个项目的Modifiers里也是N.
那么这是一个符合的项目。所以进行操作 "Maximize True growright 0":把窗口向右扩大,直到被另一个窗口或者屏幕边沿挡住。
注意控制键都是在动作开始时就已经记录下来了。如果你在绘画的途中放开了或者按下了控制键是不会改变识别的效果的。
- 怎样提高识别率
通常不要定义太复杂的轨迹,因为变化太多了就不容易识别。左右晃一晃,上下摇一摇,转个圈儿,……已经可以完成你很多任务了。
如果是复杂的 stroke, 比如写一个字母,你需要定义很多相似的 stroke,否则有时不能匹配。如果你不能确定会出现那些轨迹,你可以给
StrokeFunc 一个参数,比如:Mouse 1 2 N StrokeFunc EchoSequence
然后你在屏幕上多画几次你的那个字母,无论它是否匹配一个定义,FVWM
会在启动它的那个终端输出你画出的轨迹号码。那些就是你写这个字母时有可能出现的轨迹,你把这些序列都加到你的配置文件,这样就提高了识别率。
注意这个输出号码的终端很有可能是
tty1,在Linux下你需要Ctrl-Alt-F1切换到tty1才能看到输出。如果你不喜欢这么麻烦,你可以在启动X的时候只启动一个 xterm,
然后在这个 xterm 里面启动 fvwm.
- 键盘触发 Stroke
stroke 也可以由键盘来触发。比如:
Key F6 A C StrokeFunc DrawMotion NotStayPressed
按下 Ctrl-F6 之后,FVWM就会调用 StrokeFunc, 由于我们设定了 NotStayPressed
参数,绘画一直会延续到一个鼠标键按下的时候才结束。这时你就可以用鼠标移动画出一个轨迹,然后按一下鼠标。
- 让 FVWM 支持 Stroke
- 模块
模块是FVWM可以扩展的奥秘。模块是通过管道跟FVWM通信的程序,它们必须由FVWM启动(fork). 也就是说,你可以从 FvwmConsole
来启动这些模块,也可以用菜单,鼠标,热键……来启动。但是就是不能从 xterm 或者 rxvt 敲入命令来启动它们。
- 你有没有发现。当你的鼠标移动到窗口后,如果它被别的窗口挡住了,它并不会跑到上面来。如果你想让它自动上来,你可以使用 FvwmAuto
模块来实现一个简单的“自动提升”功能。我的 StartFunction 里有如下内容:AddToFunc StartFunction
其实你还可以用 FvwmAuto 实现非常复杂的自动提升功能。
+ I Module FvwmAuto 500 Raise Nop
- 你想让你的窗口图标化(Iconify) 和取消图标化(Deinconify) 的时候都有漂亮的动画吗?用以下设定来配置你的
FvwmAnimate 模块,然后启动它,就可以有眼花缭乱的效果了 :)*FvwmAnimate: Delay 25
这些行是对 FvwmAnimate 的配置,模块的配置命令都是 "*" 号开始的。
*FvwmAnimate: Effect Random
*FvwmAnimate: Width 3
- 你想要一个 Windows 那样的任务栏吗?启动 FvwmTaskBar 模块就行了。
- 想要一个 Pager? FvwmPager 可以提供你用不完的功能。参看FvwmPager.
- Drag & Drop? 启动 FvwmDragWell, 就可以让支持 XDND 的程序工作。
- 你想这样一种功能:每次当名叫 XXX 的程序出现时,就把它大小变为 400x300, 移动到屏幕右边,然后启动一个 rxvt 跟它作伴?用
FvwmEvent 可以轻松达到你的目的。
- 你想做一个简单的图形界面程序?用 FvwmScript 可以快速的达到你的目的。
- 写配置文件太冗长了?用 FvwmM4 可以让你用 M4 宏处理语言来预处理配置文件。
- 你想有更加超级的操纵方式?FvwmPerl 可以让你使用 Perl 脚本的方式来操纵 FVWM.
- 你有没有发现。当你的鼠标移动到窗口后,如果它被别的窗口挡住了,它并不会跑到上面来。如果你想让它自动上来,你可以使用 FvwmAuto
- FvwmPager
既然 fvwm
有很多工作区。能不能有一个东西可以方便的看到那些工作区上有哪些程序,而且可以方便的切换工作区呢?FvwmPager就是为这个目的设计的。
我的Pager是这个样子:
FvwmPager有很多可以设定的参数,现在你可以试试这个简单的配置,这就是上面这个 pager 的配置。
*FvwmPager: Rows 4
这些行是对 FvwmPager 的配置,模块的配置命令都是 "*" 号开始的。 在 FVWM 里启动它:
*FvwmPager: Columns 1
*FvwmPagerBack #908090
*FvwmPagerFore #484048
*FvwmPager:Font -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
*FvwmPagerHilight #cab3ca
*FvwmPagerLabel 0 Main
*FvwmPagerLabel 1 Internet
*FvwmPagerLabel 2 Program
*FvwmPagerLabel 3 Amusement
*FvwmPager:SmallFont -*-simsun-medium-r-*-*-12-*-*-*-*-*-*-*
*FvwmPagerBalloons All
*FvwmPagerBalloonBack Yellow
*FvwmPagerBalloonFore Black
*FvwmPager:BalloonFont -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
*FvwmPagerBalloonYOffset +2
*FvwmPagerBalloonBorderWidth 1
*FvwmPagerBalloonBorderColor BlackModule FvwmPager 0 3
- FvwmButtons
上面的Pager不错吧?不过它总是在屏幕上占那么一块位置,有没有办法让它可以在需要的时候才伸出来呢?你可以用 FvwmButtons
把FvwmPager包装起来实现这个功能。
这里是我的一个简单的配置:
*FvwmButtonsBack bisque3
这个FvwmButtons设置了一个 button 叫做 "MainPanel". 你可以用:
*MainPanel: Geometry 80x18+40+4
*MainPanel: Back SeaGreen
*MainPanel: (Panel(down, indicator, delay 0, steps 1) \
PagerPanel "Module FvwmButtons PagerPanel")
*MainPanel: Font -*-simsun-medium-r-*-*-16-*-*-*-*-*-*-*
*PagerPanel: Geometry 80x352
*PagerPanel: (Swallow FvwmPager "Module FvwmPager 0 3")
*PagerPanel: Font -*-simsun-medium-r-*-*-16-*-*-*-*-*-*-*Module FvwmButtons MainPanel
来启动它. 它启动时是这个样子:
挂在屏幕左上偏右一点的地方,既没有挡住左边的按钮,又不会挡住窗口的下拉菜单。点一下就会展开,展开以后就是这个样子:
再点就会缩回去。
FvwmButtons 可以提供的功能远远不止这些。FvwmButtons 是一个非常强大的模块。你有兴趣可以看看它的 manpage.
- FAQ
这一节来看看我遇到过的一些问题。
- 为什么 FVWM 不能用图片作为背景?
不熟悉Xwindow的人经常问这种问题。答案是FVWM确实不能设置复杂的高清晰图片作为背景,但是你却可以用图片作为背景。原因是:设置背景根本不是WM的职责,你需要用其它程序,比如
xloadimage, xv, ... 在根窗口上放置一幅图片,那就是所谓的“桌面背景”。你可以把它加入你的启动函数,一个 xloadimage
的例子可以在上面看到。
- FVWM怎么锁定屏幕呢?
你又问到一个容易混淆的问题。锁定屏幕也不是WM必须有的功能。几乎所有WM都是调用另外一个程序,比如 xscreensaver
来锁定屏幕和提供屏幕保护,然后在它们的菜单里加入对 xscreensaver 配置程序 xscreensaver-demo
的调用。看起来好像是WM提供了屏幕保护功能,让很多用户模糊了WM的职责。
你可以把 xsreensaver 加入到FVWM的启动函数里。参看启动函数和退出函数.
- 为什么 FVWM 的窗口标题不能显示汉字?
FVWM当然能显示汉字了,它是一个国际化的程序。原因在于你没有设置好汉字字体。你可以在配置文件里加入汉字字体的设定:
Style * Font -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
simsun
是我机器上一种同时可以支持汉字和英语编码的字体,如果你的字体只有汉字编码,那么你的英文全部都会“乱码”,这时你需要在后面再加一个英文字体。比如:Style * Font -cjacker-magicsong-medium-r-*-*-14-*-*-*-*-*-gb2312.1980-0,*-r-*
类似的,pager,windowlist 都有自己的字体设定,你需要把它们都设置为你喜欢的中文字体。
- FVWM 有工具条吗?
有。启动 FvwmTaskBar 模块就行了。你还可以把它配置的非常漂亮。这里给出一个我的简陋的配置方案。
Style FvwmTaskBar HandleWidth 0, BorderWidth 0
*FvwmTaskBar: UseSkipList
*FvwmTaskBar: AutoStick
*FvwmTaskBar: DeskOnly
*FvwmTaskBar: Action Click1 DeiconifyRaiseAndFocus
*FvwmTaskBar: Action Click2 Iconify On
*FvwmTaskBar: Action Click3 Lower
*FvwmTaskBar: MailCommand Exec exec rxvt -e mutt
*FvwmTaskBar: 3DFvwm
*FvwmTaskBar: StartName FVWM
*FvwmTaskBar: StartMenu RootMenu
*FvwmTaskBar: Font -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
*FvwmTaskBar: SelFont -*-simsun-medium-r-*-*-14-*-*-*-*-*-*-*
*FvwmTaskBar: ShowTips
*FvwmTaskBar: ClockFormat
*FvwmTaskBar: WindowButtonsRightMargin 20
*FvwmTaskBar: Back seagreen
*FvwmTaskBar: Fore gold2
*FvwmTaskBar: FocusFore cornsilk
*FvwmTaskBar: IconBack darkgreen
*FvwmTaskBar: IconFore white
另外,FvwmButtons
模块提供了更加复杂的功能。你可以把很多小程序(xclock,biff...)和模块(FvwmIconMan)嵌入到它里面。形成一个复杂的工具条。详情请
man FvwmButtons.
- FVWM 能不能像 Windows 那样用 Alt-Tab 切换窗口?
能。把这行加入 .fvwm2rc:
Key Tab A M WindowList Root c c NoDeskSort
这个绑定不知道什么时候好像成了 FVWM 缺省的。如果你不喜欢,那么加入:Key Tab A M -
取消这个定义。
- FVWM 能和KDE, Gnome 一起工作吗?
KDE 和 Gnome 都是完整的桌面系统,包括了WM和其它很多东西。FVWM 只是一个WM。FVWM可以替代 KDE 缺省的 kwin,或者
Gnome 缺省的 sawfish 成为它们的WM。
- 只用KDE和Gnome的工具条
很多时候 Gnome 和 KDE 的程序是跟他们的桌面系统可以分开使用的。其实你有可能只需要它们漂亮的panel。
Gnome 的工具条叫做 gnome-panel, 在 xterm 启动一个就行了。
KDE 的工具条叫做 kicker. 注意 KDE 有些程序需要 dcopserver, 你可以先启动 dcopserver。
KDE 和 gnome 的panel上的pager和fvwm的FvwmPager都是相通的,所以你可以用它们任何一个来切换桌面 :)
如果你在kde的任务条用右键选择“总在最前”可能不起作用,因为现在它们得完全听fvwm的话不过你可以给它们额外的权力,请参考fvwm
manpage 的有关EWMH 的部分
- 完全启动Gnome和KDE与FVWM一起工作
还有些kde程序不知道用了什么通信方式,启动后就dump了。你可以用 startkde 来启动整个 KDE
系统。一般来说它们都可以与fvwm一起很好的工作。
Gnome 的启动命令叫做 gnome-session. 它也可以完全与 fvwm 一起工作。
这样你就可以用 fvwm 的方式来控制所有桌面系统的窗口了。嘿嘿
在一起。嘿嘿。点击可以放大。
你甚至可以让 Gnome 和 KDE 同时出现。不过估计除了耍酷,没人会像这样做:
- 只用KDE和Gnome的工具条
- 为什么 FVWM 不能用图片作为背景?
在看这个文档之前你最好对 Xwindow 的工作机制有一定了解。知道 X server 跟 WM
有什么关系。我以后或许会增加这些内容,但是现在暂时还没有时间写这些。
1.概述
~~~~~~~~
GIF(Graphics Interchange Format,图形交换格式)文件是由 CompuServe公司开发的图形文件格式,版权所有,任何商业目的使用均须 CompuServe公司授权。
GIF图象是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多只支持8位(256色)。GIF文件内部分成许多存储块,用来存储多幅图象或者是决定图象表现行为的控制块,用以实现动画和交互式应用。GIF文件还通过LZW压缩算法压缩图象数据来减少图象尺寸(关于LZW算法和GIF数据压缩>>...)。
2.GIF文件存储结构
~~~~~~~~~~~~~~~~~~~
GIF文件内部是按块划分的,包括控制块( Control Block )和数据块(Data Sub-blocks)两种。控制块是控制数据块行为的,根据不同的控制块包含一些不同的控制参数;数据块只包含一些8-bit的字符流,由它前面的控制块来决定它的功能,每个数据块大小从0到255个字节,数据块的第一个字节指出这个数据块大小(字节数),计算数据块的大小时不包括这个字节,所以一个空的数据块有一个字节,那就是数据块的大小0x00。下表是一个数据块的结构:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
0 | 块大小 | Block Size - 块大小,不包括这个这个字节(不计算块大小自身) | |||||||
1 | Data Values - 块数据,8-bit的字符串 | ||||||||
2 | |||||||||
... | |||||||||
254 | |||||||||
255 |
一个GIF文件的结构可分为文件头(File Header)、GIF数据流(GIF Data Stream)和文件终结器(Trailer)三个部分。文件头包含GIF文件署名(Signature)和版本号(Version);GIF数据流由控制标识符、图象块(Image Block)和其他的一些扩展块组成;文件终结器只有一个值为0x3B的字符(';')表示文件结束。下表显示了一个GIF文件的组成结构:
GIF署名 | 文件头 | |||
版本号 | ||||
逻辑屏幕标识符 | GIF数据流 | |||
全局颜色列表 | ||||
... | ||||
图象标识符 | 图象块 | |||
图象局部颜色列表图 | ||||
基于颜色列表的图象数据 | ||||
... | ||||
GIF结尾 | 文件结尾 |
下面就具体介绍各个部分:
文件头部分(Header)
~~~~~~~~~~~~~~~~~
GIF署名(Signature)和版本号(Version)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
GIF署名用来确认一个文件是否是GIF格式的文件,这一部分由三个字符组成:"GIF";文件版本号也是由三个字节组成,可以为"87a"或"89a".具体描述见下表:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 'G' | GIF文件标识 | |||||||
2 | 'I' | ||||||||
3 | 'F' | ||||||||
4 | '8' | GIF文件版本号:87a - 1987年5月 89a - 1989年7月 | |||||||
5 | '7'或'9' | ||||||||
6 | 'a' |
GIF数据流部分(GIF Data Stream)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
逻辑屏幕标识符(Logical Screen Descriptor)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这一部分由7个字节组成,定义了GIF图象的大小(Logical Screen Width & Height)、颜色深度(Color Bits)、背景色(Blackground Color Index)以及有无全局颜色列表(Global Color Table)和颜色列表的索引数(Index Count),具体描述见下表:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT | |
1 | 逻辑屏幕宽度 | 像素数,定义GIF图象的宽度 | ||||||||
2 | ||||||||||
3 | 逻辑屏幕高度 | 像素数,定义GIF图象的高度 | ||||||||
4 | ||||||||||
5 | m | cr | s | pixel | 具体描述见下... | |||||
6 | 背景色 | 背景颜色(在全局颜色列表中的索引,如果没有全局颜色列表,该值没有意义) | ||||||||
7 | 像素宽高比 | 像素宽高比(Pixel Aspect Radio) |
m - 全局颜色列表标志(Global Color Table Flag),当置位时表示有全局颜色列表,pixel值有意义.
cr - 颜色深度(Color ResoluTion),cr+1确定图象的颜色深度.
s - 分类标志(Sort Flag),如果置位表示全局颜色列表分类排列.
pixel - 全局颜色列表大小,pixel+1确定颜色列表的索引数(2的pixel+1次方).
全局颜色列表(Global Color Table)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
全局颜色列表必须紧跟在逻辑屏幕标识符后面,每个颜色列表索引条目由三个字节组成,按R、G、B的顺序排列。
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 索引1的红色值 | ||||||||
2 | 索引1的绿色值 | ||||||||
3 | 索引1的蓝色值 | ||||||||
4 | 索引2的红色值 | ||||||||
5 | 索引2的绿色值 | ||||||||
6 | 索引2的蓝色值 | ||||||||
7 | ... |
图象标识符(Image Descriptor)
~~~~~~~~~~~~~~~~~~~~~~~~~
一个GIF文件内可以包含多幅图象,一幅图象结束之后紧接着下是一幅图象的标识符,图象标识符以0x2C(',')字符开始,定义紧接着它的图象的性质,包括图象相对于逻辑屏幕边界的偏移量、图象大小以及有无局部颜色列表和颜色列表大小,由10个字节组成:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT | |
1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 图象标识符开始,固定值为',' | |
2 | X方向偏移量 | 必须限定在逻辑屏幕尺寸范围内 | ||||||||
3 | ||||||||||
4 | Y方向偏移量 | |||||||||
5 | ||||||||||
6 | 图象宽度 | |||||||||
7 | ||||||||||
8 | 图象高度 | |||||||||
9 | ||||||||||
10 | m | i | s | r | pixel | m - 局部颜色列表标志(Local Color Table Flag) | ||||
置位时标识紧接在图象标识符之后有一个局部颜色列表,供紧跟在它之后的一幅图象使用;值否时使用全局颜色列表,忽略pixel值。 i - 交织标志(Interlace Flag),置位时图象数据使用交织方式排列(详细描述...),否则使用顺序排列。 s - 分类标志(Sort Flag),如果置位表示紧跟着的局部颜色列表分类排列. r - 保留,必须初始化为0. pixel - 局部颜色列表大小(Size of Local Color Table),pixel+1就为颜色列表的位数 |
局部颜色列表(Local Color Table)
~~~~~~~~~~~~~~~~~~~~~~~~~~
如果上面的局部颜色列表标志置位的话,则需要在这里(紧跟在图象标识符之后)定义一个局部颜色列表以供紧接着它的图象使用,注意使用前应线保存原来的颜色列表,使用结束之后回复原来保存的全局颜色列表。如果一个GIF文件即没有提供全局颜色列表,也没有提供局部颜色列表,可以自己创建一个颜色列表,或使用系统的颜色列表。局部颜色列表的排列方式和全局颜色列表一样:RGBRGB......
基于颜色列表的图象数据(Table-Based Image Data)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
由两部分组成:LZW编码长度(LZW Minimum Code Size)和图象数据(Image Data)。
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | LZW编码长度 | LZW编码初始码表大小的位数,详细描述见LZW编码... | |||||||
... | 图象数据,由一个或几个数据块(Data Sub-blocks)组成 | ||||||||
数据块 | |||||||||
... |
GIF图象数据使用了LZW压缩算法(详细介绍请看后面的『LZW算法和GIF数据压缩』),大大减小了图象数据的大小。图象数据在压缩前有两种排列格式:连续的和交织的(由图象标识符的交织标志控制)。连续方式按从左到右、从上到下的顺序排列图象的光栅数据;交织图象按下面的方法处理光栅数据:
创建四个通道(pass)保存数据,每个通道提取不同行的数据:
第一通道(Pass 1)提取从第0行开始每隔8行的数据;
第二通道(Pass 2)提取从第4行开始每隔8行的数据;
第三通道(Pass 3)提取从第2行开始每隔4行的数据;
第四通道(Pass 4)提取从第1行开始每隔2行的数据;
下面的例子演示了提取交织图象数据的顺序:
行 | 通道1 | 通道2 | 通道3 | 通道4 | |
0 -------------------------------------------------------- | 1 | ||||
1 -------------------------------------------------------- | 4 | ||||
2 -------------------------------------------------------- | 3 | ||||
3 -------------------------------------------------------- | 4 | ||||
4 -------------------------------------------------------- | 2 | ||||
5 -------------------------------------------------------- | 4 | ||||
6 -------------------------------------------------------- | 3 | ||||
7 -------------------------------------------------------- | 4 | ||||
8 -------------------------------------------------------- | 1 | ||||
9 -------------------------------------------------------- | 4 | ||||
10 -------------------------------------------------------- | 3 | ||||
11 -------------------------------------------------------- | 4 | ||||
12 -------------------------------------------------------- | 2 | ||||
13 -------------------------------------------------------- | 4 | ||||
14 -------------------------------------------------------- | 3 | ||||
15 -------------------------------------------------------- | 4 | ||||
16 -------------------------------------------------------- | 1 | ||||
17 -------------------------------------------------------- | 4 | ||||
18 -------------------------------------------------------- | 3 | ||||
19 -------------------------------------------------------- | 4 | ||||
20 -------------------------------------------------------- | 2 |
图形控制扩展(Graphic Control Extension)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这一部分是可选的(需要89a版本),可以放在一个图象块(图象标识符)或文本扩展块的前面,用来控制跟在它后面的第一个图象(或文本)的渲染(Render)形式,组成结构如下:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 扩展块标识 | Extension Introducer - 标识这是一个扩展块,固定值0x21 | |||||||
2 | 图形控制扩展标签 | Graphic Control Label - 标识这是一个图形控制扩展块,固定值0xF9 | |||||||
3 | 块大小 | Block Size - 不包括块终结器,固定值4 | |||||||
4 | 保留 | 处置方法 | i | t | i - 用户输入标志;t - 透明色标志。详细描述见下... | ||||
5 | 延迟时间 | Delay Time - 单位1/100秒,如果值不为1,表示暂停规定的时间后再继续往下处理数据流 | |||||||
6 | |||||||||
7 | 透明色索引 | Transparent Color Index - 透明色索引值 | |||||||
8 | 块终结器 | Block Terminator - 标识块终结,固定值0 |
处置方法(Disposal Method):指出处置图形的方法,当值为:
0 - 不使用处置方法
1 - 不处置图形,把图形从当前位置移去
2 - 回复到背景色
3 - 回复到先前状态
4-7 - 自定义
用户输入标志(Use Input Flag):指出是否期待用户有输入之后才继续进行下去,置位表示期待,值否表示不期待。用户输入可以是按回车键、鼠标点击等,可以和延迟时间一起使用,在设置的延迟时间内用户有输入则马上继续进行,或者没有输入直到延迟时间到达而继续
透明颜色标志(Transparent Color Flag):置位表示使用透明颜色
注释扩展(Comment Extension)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
这一部分是可选的(需要89a版本),可以用来记录图形、版权、描述等任何的非图形和控制的纯文本数据(7-bit ASCII字符),注释扩展并不影响对图象数据流的处理,解码器完全可以忽略它。存放位置可以是数据流的任何地方,最好不要妨碍控制和数据块,推荐放在数据流的开始或结尾。具体组成:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 扩展块标识 | Extension Introducer - 标识这是一个扩展块,固定值0x21 | |||||||
2 | 注释块标签 | Comment Label - 标识这是一个注释块,固定值0xFE | |||||||
... | Comment Data - 一个或多个数据块(Data Sub-Blocks)组成 | ||||||||
注释块 | |||||||||
... | |||||||||
块终结器 | Block Terminator - 标识注释块结束,固定值0 |
图形文本扩展(Plain Text Extension)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这一部分是可选的(需要89a版本),用来绘制一个简单的文本图象,这一部分由用来绘制的纯文本数据(7-bit ASCII字符)和控制绘制的参数等组成。绘制文本借助于一个文本框(Text Grid)来定义边界,在文本框中划分多个单元格,每个字符占用一个单元,绘制时按从左到右、从上到下的顺序依次进行,直到最后一个字符或者占满整个文本框(之后的字符将被忽略,因此定义文本框的大小时应该注意到是否可以容纳整个文本),绘制文本的颜色索引使用全局颜色列表,没有则可以使用一个已经保存的前一个颜色列表。另外,图形文本扩展块也属于图形块(Graphic Rendering Block),可以在它前面定义图形控制扩展对它的表现形式进一步修改。图形文本扩展的组成:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 扩展块标识 | Extension Introducer - 标识这是一个扩展块,固定值0x21 | |||||||
2 | 图形控制扩展标签 | Plain Text Label - 标识这是一个图形文本扩展块,固定值0x01 | |||||||
3 | 块大小 | Block Size - 块大小,固定值12 | |||||||
4 | 文本框左边界位置 | Text Glid Left Posotion - 像素值,文本框离逻辑屏幕的左边界距离 | |||||||
5 | |||||||||
6 | 文本框上边界位置 | Text Glid Top Posotion - 像素值,文本框离逻辑屏幕的上边界距离 | |||||||
7 | |||||||||
8 | 文本框高度 | Text Glid Width -像素值 | |||||||
9 | |||||||||
10 | 文本框高度 | Text Glid Height - 像素值 | |||||||
11 | |||||||||
12 | 字符单元格宽度 | Character Cell Width - 像素值,单个单元格宽度 | |||||||
13 | 字符单元格高度 | Character Cell Height- 像素值,单个单元格高度 | |||||||
14 | 文本前景色索引 | Text Foreground Color Index - 前景色在全局颜色列表中的索引 | |||||||
15 | 文本背景色索引 | Text Blackground Color Index - 背景色在全局颜色列表中的索引 | |||||||
N | ... | Plain Text Data - 一个或多个数据块(Data Sub-Blocks)组成,保存要在显示的字符串。 | |||||||
文本数据块 | |||||||||
... | |||||||||
N+1 | 块终结 | Block Terminator - 标识注释块结束,固定值0 |
推荐:1.由于文本的字体(Font)和尺寸(Size)没有定义,解码器应该根据情况选择最合适的;
2.如果一个字符的值小于0x20或大于0xF7,则这个字符被推荐显示为一个空格(0x20);
3.为了兼容性,最好定义字符单元格的大小为8x8或8x16(宽度x高度)。
应用程序扩展(Application Extension)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
这是提供给应用程序自己使用的(需要89a版本),应用程序可以在这里定义自己的标识、信息等,组成:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 扩展块标识 | Extension Introducer - 标识这是一个扩展块,固定值0x21 | |||||||
2 | 图形控制扩展标签 | Application Extension Label - 标识这是一个应用程序扩展块,固定值0xFF | |||||||
3 | 块大小 | Block Size - 块大小,固定值11 | |||||||
4 | 应用程序标识符 | Application Identifier - 用来鉴别应用程序自身的标识(8个连续ASCII字符) | |||||||
5 | |||||||||
6 | |||||||||
7 | |||||||||
8 | |||||||||
9 | |||||||||
10 | |||||||||
11 | |||||||||
12 | 应用程序鉴别码 | Application Authentication Code - 应用程序定义的特殊标识码(3个连续ASCII字符) | |||||||
13 | |||||||||
14 | |||||||||
N | ... | 应用程序自定义数据块 - 一个或多个数据块(Data Sub-Blocks)组成,保存应用程序自己定义的数据 | |||||||
应用程序数据 | |||||||||
... | |||||||||
N+1 | 块终结器 | lock Terminator - 标识注释块结束,固定值0 |
文件结尾部分
~~~~~~~~~~~
文件终结器(Trailer)
~~~~~~~~~~~~~~~~
这一部分只有一个值为0的字节,标识一个GIF文件结束.
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | |
1 | 文件终结 | GIF Trailer - 标识GIF文件结束,固定值0x3B |
2.LZW算法和GIF数据压缩
~~~~~~~~~~~~~~~~~~~~~~~~~~~
GIF文件的图象数据使用了可变长度编码的LZW压缩算法(Variable-Length_Code LZW Compression),这是从LZW(Lempel Ziv Compression)压缩算法演变过来的,通过压缩原始数据的重复部分来达到减少文件大小的目的。
标准的LZW压缩原理:
~~~~~~~~~~~~~~~~~~
先来解释一下几个基本概念:
LZW压缩有三个重要的对象:数据流(CharStream)、编码流(CodeStream)和编译表(String Table)。在编码时,数据流是输入对象(图象的光栅数据序列),编码流就是输出对象(经过压缩运算的编码数据);在解码时,编码流则是输入对象,数据流是输出对象;而编译表是在编码和解码时都须要用借助的对象。
字符(Character):最基础的数据元素,在文本文件中就是一个字节,在光栅数据中就是一个像素的颜色在指定的颜色列表中的索引值;
字符串(String):由几个连续的字符组成;
前缀(Prefix):也是一个字符串,不过通常用在另一个字符的前面,而且它的长度可以为0;
根(Root):单个长度的字符串;
编码(Code):一个数字,按照固定长度(编码长度)从编码流中取出,编译表的映射值;
图案:一个字符串,按不定长度从数据流中读出,映射到编译表条目.
LZW压缩的原理:提取原始图象数据中的不同图案,基于这些图案创建一个编译表,然后用编译表中的图案索引来替代原始光栅数据中的相应图案,减少原始数据大小。看起来和调色板图象的实现原理差不多,但是应该注意到的是,我们这里的编译表不是事先创建好的,而是根据原始图象数据动态创建的,解码时还要从已编码的数据中还原出原来的编译表(GIF文件中是不携带编译表信息的),为了更好理解编解码原理,我们来看看具体的处理过程:
编码器(Compressor)
~~~~~~~~~~~~~~~~
编码数据,第一步,初始化一个编译表,假设这个编译表的大小是12位的,也就是最多有4096个单位,另外假设我们有32个不同的字符(也可以认为图象的每个像素最多有32种颜色),表示为a,b,c,d,e...,初始化编译表:第0项为a,第1项为b,第2项为c...一直到第31项,我们把这32项就称为根。
开始编译,先定义一个前缀对象Current Prefix,记为[.c.],现在它是空的,然后定义一个当前字符串Current String,标记为[.c.]k,[.c.]就为Current Prefix,k就为当前读取字符。现在来读取数据流的第一个字符,假如为p,那么Current String就等于[.c.]p(由于[.c.]为空,实际上值就等于p),现在在编译表中查找有没有Current String的值,由于p就是一个根字符,我们已经初始了32个根索引,当然可以找到,把p设为Current Prefix的值,不做任何事继续读取下一个字符,假设为q,Current String就等于[.c.]q(也就是pq),看看在编译表中有没有该值,当然。没有,这时我们要做下面的事情:将Current String的值(也就是pq)添加到编译表的第32项,把Current Prefix的值(也就是p)在编译表中的索引输出到编码流,修改Current Prefix为当前读取的字符(也就是q)。继续往下读,如果在编译表中可以查找到Current String的值([.c.]k),则把Current String的值([.c.]k)赋予Current Prefix;如果查找不到,则添加Current String的值([.c.]k)到编译表,把Current Prefix的值([.c.])在编译表中所对应的索引输出到编码流,同时修改Current Prefix为k ,这样一直循环下去直到数据流结束。伪代码看起来就像下面这样:
|
来看一个具体的例子,我们有一个字母表a,b,c,d.有一个输入的字符流abacaba。现在来初始化编译表:#0=a,#1=b,#2=c,#3=d.现在开始读取第一个字符a,[.c.]a=a,可以在在编译表中找到,修改[.c.]=a;不做任何事继续读取第二个字符b,[.c.]b=ab,在编译表中不能找,那么添加[.c.]b到编译表:#4=ab,同时输出[.c.](也就是a)的索引#0到编码流,修改[.c.]=b;读下一个字符a,[.c.]a=ba,在编译表中不能找到:添加编译表#5=ba,输出[.c.]的索引#1到编码流,修改[.c.]=a;读下一个字符c,[.c.]c=ac,在编译表中不能找到:添加编译表#6=ac,输出[.c.]的索引#0到编码流,修改[.c.]=c;读下一个字符a,[.c.]c=ca,在编译表中不能找到:添加编译表#7=ca,输出[.c.]的索引#2到编码流,修改[.c.]=a;读下一个字符b,[.c.]b=ab,编译表的#4=ab,修改[.c.]=ab;读取最后一个字符a,[.c.]a=aba,在编译表中不能找到:添加编译表#8=aba,输出[.c.]的索引#4到编码流,修改[.c.]=a;好了,现在没有数据了,输出[.c.]的值a的索引#0到编码流,这样最后的输出结果就是:#0#1#0#2#4#0.
解码器(Decompressor)
~~~~~~~~~~~~~~~~~~
好了,现在来看看解码数据。数据的解码,其实就是数据编码的逆向过程,要从已经编译的数据(编码流)中找出编译表,然后对照编译表还原图象的光栅数据。
首先,还是要初始化编译表。GIF文件的图象数据的第一个字节存储的就是LZW编码的编码大小(一般等于图象的位数),根据编码大小,初始化编译表的根条目(从0到2的编码大小次方),然后定义一个当前编码Current Code,记作[code],定义一个Old Code,记作[old]。读取第一个编码到[code],这是一个根编码,在编译表中可以找到,把该编码所对应的字符输出到数据流,[old]=[code];读取下一个编码到[code],这就有两种情况:在编译表中有或没有该编码,我们先来看第一种情况:先输出当前编码[code]所对应的字符串到数据流,然后把[old]所对应的字符(串)当成前缀prefix [...],当前编码[code]所对应的字符串的第一个字符当成k,组合起来当前字符串Current String就为[...]k,把[...]k添加到编译表,修改[old]=[code],读下一个编码;我们来看看在编译表中找不到该编码的情况,回想一下编码情况:如果数据流中有一个p[...]p[...]pq这样的字符串,p[...]在编译表中而p[...]p不在,编译器将输出p[...]的索引而添加p[...]p到编译表,下一个字符串p[...]p就可以在编译表中找到了,而p[...]pq不在编译表中,同样将输出p[...]p的索引值而添加p[...]pq到编译表,这样看来,解码器总比编码器『慢一步』,当我们遇到p[...]p所对应的索引时,我们不知到该索引对应的字符串(在解码器的编译表中还没有该索引,事实上,这个索引将在下一步添加),这时需要用猜测法:现在假设上面的p[...]所对应的索引值是#58,那么上面的字符串经过编译之后是#58#59,我们在解码器中读到#59时,编译表的最大索引只有#58,#59所对应的字符串就等于#58所对应的字符串(也就是p[...])加上这个字符串的第一个字符(也就是p),也就是p[...]p。事实上,这种猜测法是很准确(有点不好理解,仔细想一想吧)。上面的解码过程用伪代码表示就像下面这样:
|
GIF数据压缩
~~~~~~~~~~~
下面是GIF文件的图象数据结构:
BYTE | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | BIT |
1 | 编码长度 | LZW Code Size - LZW压缩的编码长度,也就是要压缩的数据的位数 | |||||||
... | 数据块 | ||||||||
块大小 | 数据块,如果需要可重复多次 | ||||||||
编码数据 | |||||||||
... | 数据块 | ||||||||
块终结器 | 一个图象的数据编码结束,固定值0 |
把光栅数据序列(数据流)压缩成GIF文件的图象数据(字符流)可以按下面的步骤进行:
1.定义编码长度
GIF图象数据的第一个字节就是编码长度(Code Size),这个值是指要表现一个像素所需要的最小位数,通常就等于图象的色深;
2.压缩数据
通过LZW压缩算法将图象的光栅数据流压缩成GIF的编码数据流。这里使用的LZW压缩算法是从标准的LZW压缩算法演变过来的,它们之间有如下的差别:
[1]GIF文件定义了一个编码大小(Clear Code),这个值等于2的『编码长度』次方,在从新开始一个编译表(编译表溢出)时均须输出该值,解码器遇到该值时意味着要从新初始化一个编译表;
[2]在一个图象的编码数据结束之前(也就是在块终结器的前面),需要输出一个Clear Code+1的值,解码器在遇到该值时就意味着GIF文件的一个图象数据流的结束;
[3]第一个可用到的编译表索引值是Clear Code+2(从0到Clear Code-1是根索引,再上去两个不可使用,新的索引从Clare Code+2开始添加);
[4]GIF输出的编码流是不定长的,每个编码的大小从Code Size + 1位到12位,编码的最大值就是4095(编译表需要定义的索引数就是4096),当编码所须的位数超过当前的位数时就把当前位数加1,这就需要在编码或解码时注意到编码长度的改变。
3.编译成字节序列
因为GIF输出的编码流是不定长的,这就需要把它们编译成固定的8-bit长度的字符流,编译顺序是从右往左。下面是一个具体例子:编译5位长度编码到8位字符
0 | b | b | b | a | a | a | a | a |
1 | d | c | c | c | c | c | b | b |
2 | e | e | e | e | d | d | d | d |
3 | g | g | f | f | f | f | f | e |
4 | h | h | h | h | h | g | g | g |
... | ||||||||
N |
4.打包
前面讲过,一个GIF的数据块的大小从0到255个字节,第一个字节是这个数据块的大小(字节数),这就需要将编译编后的码数据打包成一个或几个大小不大于255个字节的数据包。然后写入图象数据块中。
Keil C51开发系统基本知识
1. 第一节 系统概述
Keil C51是美国Keil Software公司出品的51系列兼容单片机C语言软件开发系统,与汇编相比,C语言在功能上、结构性、可读性、可维护性上有明显的优势,因而易学易用。用过汇编语言后再使用C来开发,体会更加深刻。
Keil C51软件提供丰富的库函数和功能强大的集成开发调试工具,全Windows界面。另外重要的一点,只要看一下编译后生成的汇编代码,就能体会到Keil C51生成的目标代码效率非常之高,多数语句生成的汇编代码很紧凑,容易理解。在开发大型软件时更能体现高级语言的优势。
下面详细介绍Keil C51开发系统各部分功能和使用。
2. 第二节 Keil C51单片机软件开发系统的整体结构
C51工具包的整体结构,如图(1)所示,其中uVision与Ishell分别是C51 for Windows和for Dos的集成开发环境(IDE),可以完成编辑、编译、连接、调试、仿真等整个开发流程。开发人员可用IDE本身或其它编辑器编辑C或汇编源文件。然后分别由C51及A51编译器编译生成目标文件(.OBJ)。目标文件可由LIB51创建生成库文件,也可以与库文件一起经L51连接定位生成绝对目标文件(.ABS)。ABS文件由OH51转换成标准的Hex文件,以供调试器dScope51或tScope51使用进行源代码级调试,也可由仿真器使用直接对目标板进行调试,也可以直接写入程序存贮器如EPROM中。
3. 第三节 存储模式
存储模式决定了没有明确指定存储类型的变量,函数参数等的缺省存储区域,共三种:
1. 1. Small模式
所有缺省变量参数均装入内部RAM,优点是访问速度快,缺点是空间有限,只适用于小程序。
2. 2. Compact模式
所有缺省变量均位于外部RAM区的一页(256Bytes),具体哪一页可由P2口指定,在STARTUP.A51文件中说明,也可用pdata指定,优点是空间较Small为宽裕速度较Small慢,较large要快,是一种中间状态。
3. 3. large模式
所有缺省变量可放在多达64KB的外部RAM区,优点是空间大,可存变量多,缺点是速度较慢。
提示:存储模式在C51编译器选项中选择。
4. 第四节 存储类型声明
变量或参数的存储类型可由存储模式指定缺省类型,也可由关键字直接声明指定。各类型分别用:code,data,idata,xdata,pdata说明,例:
data uar1
char code array[ ]=“hello!”;
unsigned char xdata arr[10][4][4];
5. 第五节 变量或数据类型
C51提供以下几种扩展数据类型:
bit 位变量值为0或1
sbit 从字节中定义的位变量 0或1
sfr sfr字节地址 0~255
sfr16 sfr字地址 0~65535
其余数据类型如:char,enum,short,int,long,float等与ANSI C相同。
6. 第六节 位变量与声明
1. 1. bit型变量
bit型变量可用变量类型,函数声明、函数返回值等,存贮于内部RAM20H~2FH。
注意:
(1) 用#pragma disable说明函数和用“usign”指定的函数,不能返回bit值。
(2) 一个bit变量不能声明为指针,如bit *ptr;是错误的
(3) 不能有bit数组如:bit arr[5];错误。
2. 2. 可位寻址区说明20H-2FH
可作如下定义:
int bdata i;
char bdata arr[3],
然后:
sbit bito=in0;sbit bit15=I^15;
sbit arr07=arr[0]^7;sbit arr15=arr[i]^7;
7. 第七节 Keil C51指针
C51支持一般指针(Generic Pointer)和存储器指针(Memory_Specific Pointer).
1. 1. 一般指针
一般指针的声明和使用均与标准C相同,不过同时还可以说明指针的存储类型,例如:
long * state;为一个指向long型整数的指针,而state本身则依存储模式存放。
char * xdata ptr;ptr为一个指向char数据的指针,而ptr本身放于外部RAM区,以上的long,char等指针指向的数据可存放于任何存储器中。
一般指针本身用3个字节存放,分别为存储器类型,高位偏移,低位偏移量。
2. 2. 存储器指针
基于存储器的指针说明时即指定了存贮类型,例如:
char data * str;str指向data区中char型数据
int xdata * pow; pow指向外部RAM的int型整数。
这种指针存放时,只需一个字节或2个字节就够了,因为只需存放偏移量。
3. 3. 指针转换
即指针在上两种类型之间转化:
l 当基于存储器的指针作为一个实参传递给需要一般指针的函数时,指针自动转化。
l 如果不说明外部函数原形,基于存储器的指针自动转化为一般指针,导致错误,因而请用“#include”说明所有函数原形。
l 可以强行改变指针类型。
8. 第八节 Keil C51函数
C51函数声明对ANSI C作了扩展,具体包括:
1. 1. 中断函数声明:
中断声明方法如下:
void serial_ISR () interrupt 4 [using 1]
{
/* ISR */
}
为提高代码的容错能力,在没用到的中断入口处生成iret语句,定义没用到的中断。
/* define not used interrupt, so generate "IRET" in their entrance */
void extern0_ISR() interrupt 0{} /* not used */
void timer0_ISR () interrupt 1{} /* not used */
void extern1_ISR() interrupt 2{} /* not used */
void timer1_ISR () interrupt 3{} /* not used */
void serial_ISR () interrupt 4{} /* not used */
2. 2. 通用存储工作区
3. 3. 选通用存储工作区由using x声明,见上例。
4. 4. 指定存储模式
由small compact 及large说明,例如:
void fun1(void) small { }
提示:small说明的函数内部变量全部使用内部RAM。关键的经常性的耗时的地方可以这样声明,以提高运行速度。
5. 5. #pragma disable
在函数前声明,只对一个函数有效。该函数调用过程中将不可被中断。
6. 6. 递归或可重入函数指定
在主程序和中断中都可调用的函数,容易产生问题。因为51和PC不同,PC使用堆栈传递参数,且静态变量以外的内部变量都在堆栈中;而51一般使用寄存器传递参数,内部变量一般在RAM中,函数重入时会破坏上次调用的数据。可以用以下两种方法解决函数重入:
a、在相应的函数前使用前述“#pragma disable”声明,即只允许主程序或中断之一调用该函数;
b、将该函数说明为可重入的。如下:
void func(param...) reentrant;
KeilC51编译后将生成一个可重入变量堆栈,然后就可以模拟通过堆栈传递变量的方法。
由于一般可重入函数由主程序和中断调用,所以通常中断使用与主程序不同的R寄存器组。
另外,对可重入函数,在相应的函数前面加上开关“#pragma noaregs”,以禁止编译器使用绝对寄存器寻址,可生成不依赖于寄存器组的代码。
7. 7. 指定PL/M-51函数
由alien指定。
4. 第四章 Keil C51高级编程
本章讨论以下内容:
l 绝对地址访问
l C与汇编的接口
l C51软件包中的通用文件
l 段名转换与程序优化
1. 第一节 绝对地址访问
C51提供了三种访问绝对地址的方法:
1. 1. 绝对宏:
在程序中,用“#include<absacc.h>”即可使用其中定义的宏来访问绝对地址,包括:
CBYTE、XBYTE、PWORD、DBYTE、CWORD、XWORD、PBYTE、DWORD
具体使用可看一看absacc.h便知
例如:
rval=CBYTE[0x0002];指向程序存贮器的0002h地址
rval=XWORD [0x0002];指向外RAM的0004h地址
2. 2. _at_关键字
直接在数据定义后加上_at_ const即可,但是注意:
(1)绝对变量不能被初使化;
(2)bit型函数及变量不能用_at_指定。
例如:
idata struct link list _at_ 0x40;指定list结构从40h开始。
xdata char text[25b] _at_0xE000;指定text数组从0E000H开始
提示:如果外部绝对变量是I/O端口等可自行变化数据,需要使用volatile关键字进行描述,请参考absacc.h。
3. 3. 连接定位控制
此法是利用连接控制指令code xdata pdata \data bdata对“段”地址进行,如要指定某具体变量地址,则很有局限性,不作详细讨论。
2. 第二节 Keil C51与汇编的接口
1. 1. 模块内接口
方法是用#pragma语句具体结构是:
#pragma asm
汇编行
#pragma endasm
这种方法实质是通过asm与ndasm告诉C51编译器中间行不用编译为汇编行,因而在编译控制指令中有SRC以控制将这些不用编译的行存入其中。
2. 2. 模块间接口
C模块与汇编模块的接口较简单,分别用C51与A51对源文件进行编译,然后用L51将obj文件连接即可,关键问题在于C函数与汇编函数之间的参数传递问题,C51中有两种参数传递方法。
(1) 通过寄存器传递函数参数
最多只能有3个参数通过寄存器传递,规律如下表:
参数数目 char int long,float 一般指针
123 R7R5R3 R6 & R7R4 & R5R2 & R3 R4~R7R4~R7 R1~R3R1~R3R1~R3
(2) 通过固定存储区传递(fixed memory)
这种方法将bit型参数传给一个存储段中:
?function_name?BIT
将其它类型参数均传给下面的段:?function_name?BYTE,且按照预选顺序存放。
至于这个固定存储区本身在何处,则由存储模式默认。
(3) 函数的返回值
函数返回值一律放于寄存器中,有如下规律:
return type Registev 说明
bit 标志位 由具体标志位返回
char/unsigned char 1_byte指针 R7 单字节由R7返回
int/unsigned int 2_byte指针 R6 & R7 双字节由R6和R7返回,MSB在R6
long&unsigned long R4~R7 MSB在R4, LSB在R7
float R4~R7 32Bit IEEE格式
一般指针 R1~R3 存储类型在R3 高位R2 低R1
(4) SRC控制
该控制指令将C文件编译生成汇编文件(.SRC),该汇编文件可改名后,生成汇编.ASM文件,再用A51进行编译。
3. 第三节 Keil C51软件包中的通用文件
在C51\LiB目录下有几个C源文件,这几个C源文件有非常重要的作用,对它们稍事修改,就可以用在自己的专用系统中。
1. 1. 动态内存分配
init_mem.C:此文件是初始化动态内存区的程序源代码。它可以指定动态内存的位置及大小,只有使用了init_mem( )才可以调回其它函数,诸如malloc calloc,realloc等。
calloc.c:此文件是给数组分配内存的源代码,它可以指定单位数据类型及该单元数目。
malloc.c:此文件是malloc的源代码,分配一段固定大小的内存。
realloc.c:此文件是realloc.c源代码,其功能是调整当前分配动态内存的大小。
2. 2. C51启动文件STARTUP.A51
启动文件STARTUP.A51中包含目标板启动代码,可在每个project中加入这个文件,只要复位,则该文件立即执行,其功能包括:
l 定义内部RAM大小、外部RAM大小、可重入堆栈位置
l 清除内部、外部或者以此页为单元的外部存储器
l 按存储模式初使化重入堆栈及堆栈指针
l 初始化8051硬件堆栈指针
l 向main( )函数交权
开发人员可修改以下数据从而对系统初始化
常数名 意义
IDATALEN 待清内部RAM长度
XDATA START 指定待清外部RAM起始地址
XDATALEN 待清外部RAM长度
IBPSTACK 是否小模式重入堆栈指针需初始化标志,1为需要。缺省为0
IBPSTACKTOP 指定小模式重入堆栈顶部地址
XBPSTACK 是否大模式重入堆栈指针需初始化标志,缺省为0
XBPSTACKTOP 指定大模式重入堆栈顶部地址
PBPSTACK 是否Compact重入堆栈指针,需初始化标志,缺省为0
PBPSTACKTOP 指定Compact模式重入堆栈顶部地址
PPAGEENABLE P2初始化允许开关
PPAGE 指定P2值
PDATASTART 待清外部RAM页首址
PDATALEN 待清外部RAM页长度
提示:如果要初始化P2作为紧凑模式高端地址,必须:PPAGEENAGLE=1,PPAGE为P2值,例如指定某页1000H-10FFH,则PPAGE=10H,而且连接时必须如下:
L51<input modules> PDATA(1080H),其中1080H是1000H-10FFH中的任一个值。
以下是STARTUP.A51代码片断,红色是经常可能需要修改的地方:
;------------------------------------------------------------------------------
; This file is part of the C51 Compiler package
; Copyright KEIL ELEKTRONIK GmbH 1990
;------------------------------------------------------------------------------
; STARTUP.A51: This code is executed after processor reset.
;
; To translate this file use A51 with the following invocation:
;
; A51 STARTUP.A51
;
; To link the modified STARTUP.OBJ file to your application use the following
; L51 invocation:
;
; L51 <your object file list>, STARTUP.OBJ <controls>
;
;------------------------------------------------------------------------------
;
; User-defined Power-On Initialization of Memory
;
; With the following EQU statements the initialization of memory
; at processor reset can be defined:
;
; ; the absolute start-address of IDATA memory is always 0
IDATALEN EQU 80H ; the length of IDATA memory in bytes.
;
XDATASTART EQU 0H ; the absolute start-address of XDATA memory
XDATALEN EQU 0H ; the length of XDATA memory in bytes.
;
PDATASTART EQU 0H ; the absolute start-address of PDATA memory
PDATALEN EQU 0H ; the length of PDATA memory in bytes.
;
; Notes: The IDATA space overlaps physically the DATA and BIT areas of the
; 8051 CPU. At minimum the memory space occupied from the C51
; run-time routines must be set to zero.
;------------------------------------------------------------------------------
;
; Reentrant Stack Initilization
;
; The following EQU statements define the stack pointer for reentrant
; functions and initialized it:
;
; Stack Space for reentrant functions in the SMALL model.
IBPSTACK EQU 0 ; set to 1 if small reentrant is used.
IBPSTACKTOP EQU 0FFH+1 ; set top of stack to highest location+1.
;
; Stack Space for reentrant functions in the LARGE model.
XBPSTACK EQU 0 ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 0FFFFH+1; set top of stack to highest location+1.
;
; Stack Space for reentrant functions in the COMPACT model.
PBPSTACK EQU 0 ; set to 1 if compact reentrant is used.
PBPSTACKTOP EQU 0FFFFH+1; set top of stack to highest location+1.
;
;------------------------------------------------------------------------------
;
; Page Definition for Using the Compact Model with 64 KByte xdata RAM
;
; The following EQU statements define the xdata page used for pdata
; variables. The EQU PPAGE must conform with the PPAGE control used
; in the linker invocation.
;
PPAGEENABLE EQU 0 ; set to 1 if pdata object are used.
PPAGE EQU 0 ; define PPAGE number.
;
;------------------------------------------------------------------------------
3. 3. 标准输入输出文件
putchar.c
putchar.c是一个低级字符输出子程,开发人员可修改后应用到自己的硬件系统上,例如向CLD或LEN输出字符。
缺省:putchar.c是向串口输出一个字符XON&line;XOFF是流控标志,换行符“\*n”自动转化为回车/换行“\r\n”。
getkey.c
getkey函数是一个低级字符输入子程,该程序可用到自己硬件系统,如矩阵键盘输入中,缺省时通过串口输入字符。
4. 4. 其它文件
还包括对Watch-Dog有独特功能的INIT.A51函数以及对8×C751适用的函数,可参考源代码。
4. 第四节 段名协定与程序优化
1. 1. 段名协定(Segment Naming Conventions)
C51编译器生成的目标文件存放于许多段中,这些段是代码空间或数据空间的一些单元,一个段可以是可重定位的,也可以是绝对段,每一个可重定位的段都有一个类型和名字,C51段名有以下规定:
每个段名包括前缀与模块名两部分,前缀表示存储类型,模块名则是被编译的模块的名字,例如:
?CO?main1 :表示main1模块中的代码段中的常数部分
?PR?function1?module 表module模块中函数function1的可执行段,具体规定参阅手册。
2. 2. 程序优化
C51编译器是一个具有优化功能的编译器,它共提供六级优化功能。确保生成目标代码的最高效率(代码最少,运行速度最快)。具体六级优化的内容可参考帮助。
在C51中提供以下编译控制指令控制代码优化:
OPTIMIZE(SJXE):尽量采用子程序,使程序代码减少。
NOAREGS:不使用绝对寄存器访问,程序代码与寄存器段独立。
NOREGPARMS:参数传递总是在局部数据段实现,程序代码与低版本C51兼容。
OPTIMIZE(SIZE)AK OPTIMIZE(speed)提供6级优化功能,缺省为: OPTIMIZE(6,SPEED)。
5. 第五章 Keil C51库函数参考
C51强大功能及其高效率的重要体现之一在于其丰富的可直接调用的库函数,多使用库函数使程序代码简单,结构清晰,易于调试和维护,下面介绍C51的库函数系统。
1. 第一节 本征库函数(intrinsic routines)和非本征证库函数
C51提供的本征函数是指编译时直接将固定的代码插入当前行,而不是用ACALL和LCALL语句来实现,这样就大大提供了函数访问的效率,而非本征函数则必须由ACALL及LCALL调用。
C51的本征库函数只有9个,数目虽少,但都非常有用,列如下:
_crol_,_cror_:将char型变量循环向左(右)移动指定位数后返回
_iror_,_irol_:将int型变量循环向左(右)移动指定位数后返回
_lrol_,_lror_:将long型变量循环向左(右)移动指定位数后返回
_nop_: 相当于插入NOP
_testbit_: 相当于JBC bitvar测试该位变量并跳转同时清除。
_chkfloat_: 测试并返回源点数状态。
使用时,必须包含#inclucle <intrins.h>一行。
如不说明,下面谈到的库函数均指非本征库函数。
图(1) C51工具包整体结构图
3. 第三节 Keil C51工具包的安装
1. 1. C51 for Dos
在Windows下直接运行软件包中DOS\C51DOS.exe然后选择安装目录即可。完毕后欲使系统正常工作须进行以下操作(设C:\C51为安装目录):
修改Autoexec.bat,加入
path=C:\C51\Bin
Set C51LIB=C:\C51\LIB
Set C51INC=C:\C51\INC
然后运行Autoexec.bat
2. 2. C51 for Windows的安装及注意事项:
在Windows下运行软件包中WIN\Setup.exe,最好选择安装目录与C51 for Dos相同,这样设置最简单(设安装于C:\C51目录下)。然后将软件包中crack目录中的文件拷入C:\C51\Bin目录下。
4. 第四节 Keil C51工具包各部分功能及使用简介
1. 1. C51与A51
1. (1) C51
C51是C语言编译器,其使用方法为:
C51 sourcefile[编译控制指令]
或者
C51 @ commandfile
其中sourcefile为C源文件(.C)。大量的编译控制指令完成C51编译器的全部功能。包控C51输出文件C.LST,.OBJ,.I和.SRC文件的控制。源文件(.C)的控制等,详见第五部分的具体介绍。
而Commandfile为一个连接控制文件其内容包括:.C源文件及各编译控制指令,它没有固定的名字,开发人员可根据自己的习惯指定,它适于用控制指令较多的场合。
2. (2) A51
A51是汇编语言编译器,使用方法为:
A51 sourcefile[编译控制指令]
或A51 @ commandfile
其中sourcefile为汇编源文件(.asm或.a51),而编译控制指令的使用与其它汇编如ASM语言类似,可参考其他汇编语言材料。
Commandfile同C51中的Commandfile类似,它使A51使用和修改方便。
2. 2. L51和BL51
1. (1) L51
L51是Keil C51软件包提供的连接/定位器,其功能是将编译生成的OBJ文件与库文件连接定位生成绝对目标文件(.ABS),其使用方法为:
L51 目标文件列表[库文件列表] [to outputfile] [连接控制指令]
或 L51 @Commandfile
源程序的多个模块分别经C51与A51编译后生成多个OBJ文件,连接时,这些文件全列于目标文件列表中,作为输入文件,如果还需与库文件(.LiB)相连接,则库文件也必须列在其后。outputfile为输文件名,缺少时为第一模块名,后缀为.ABS。连接控制指令提供了连接定位时的所有控制功能。Commandfile为连接控制文件,其具体内容是包括了目标文件列表,库文件列表及输出文件、连接控制命令,以取代第一种繁琐的格式,由于目标模块库文件大多不止1个,因而第2种方法较多见,这个文件名字也可由使用者随意指定。
2. (2) Bl51
BL51也是C51软件包的连接/定位器,其具有L51的所有功能,此外它还具有以下3点特别之处:
a. 可以连接定位大于64kBytes的程序。
b. 具有代码域及域切换功能(CodeBanking & Bank Switching)
c. 可用于RTX51操作系统
RTX51是一个实时多任务操作系统,它改变了传统的编程模式,甚至不必用main( )函数,单片机系统软件向RTOS发展是一种趋势,这种趋势对于186和386及68K系列CPU更为明显和必须,对8051因CPU较为简单,程序结构等都不太复杂,RTX51作用显得不太突出,其专业版软件PK51软件包甚至不包括RTX51Full,而只有一个RTX51TINY版本的RTOS。RTX51 TINY适用于无外部RAM的单片机系统,因而可用面很窄,在本文中不作介绍。Bank switching技术因使用很少也不作介绍。
3. 3. DScope51,Tscope51及Monitor51
1. (1) dScope51
dScope51是一个源级调试器和模拟器,它可以调试由C51编译器、A51汇编器、PL/M-51编译器及ASM-51汇编器产生的程序。它不需目标板(for windows也可通过mon51接目标板),只能进行软件模拟,但其功能强大,可模拟CPU及其外围器件,如内部串口,外部I/O及定时器等,能对嵌入式软件功能进行有效测试。
其使用方法为:
DS51[debugfile][INIT(initfile)]
其中debugfile是一个Hex格式的8051文件,即待调试的文件其为可选的,可在进入dScope51后用load命令装入。
Initfile为一个初使化文件,它在启动dScope51后,在debugfile装入前装入,装有一些dScope的初使化参数及常用调试函数等。下面是一个dScope.ini文件(for dos)的内容:
Load ..\..\ds51\8051.iof
Map 0,0xffff
dScope51 for Windows则直接用鼠标进入,然后用load装入待调文件。
2. (2) tScope51
与dScope51不同的是Scope51必须带目标板,目前它可以通过两种方式访问目标板。(1) 通过EMul51在线仿真器,tScope51为该仿真器准备了一个动态连接文件EMUL51.IOT,但该方法必须配合该仿真器。(2) 通过Monitov51监控程序,这种方法是可行的,tScope51为访问Monitor51专门带有MON51.IOT连接程序,使用时可通过串口及监控程序来调试目标板。
其使用方法为:
TS51[INIT(file_name.ini)]
其中file_name.ini为一个初使化文件。
进入TS51后,必须装入IOT文件,可用的有MON51.IOT及EMUL51.IOT两种,如装入MON51.IOT:
Load.C:\C51\TS51\MON51.IOT CPUTYPE(80517)
可惜的是tScope51只有for Dos的版本。
3. (3) Monitor 51
Monitor51是一个监控程序通过PC机的串口与目标板进行通信,Monitor操作需要MON51或dScope51 for Windows,后面部分将对Monitor51做较为详细的介绍。
4. 4. Ishell及uVision
1. (1) Ishell for Dos
这是一个for Dos的IDE,直接在命令行键入Ishell,则进入该环境,它使用简单方便。其命令行与DOS命令行具有同样的功能,对单模块的Project直接由菜单进行编译连接,对多模块的project。则通过批处理,BAT文件进行编译连接,然后通过菜单控制由dScope51或tScope51对程序进行调试,因为是for dos的,不做太详细介绍。
2. (2) uVision for Windows
uVision for Windows是一个标准的Windows应用程序,它是C51的一个集成软件开发平台,具有源代码编辑、project管理、集成的make等功能,它的人机界面友好,操作方便,是开发者的首选,具体配置及使用见第五部分。
2. 第二章 Keil C51软件使用详解
1. 第一节 Keil C51编译器的控制指令
C51编译器的控制指令分为三类:源文件控制类,目标文件控制类及列表控制类。
1. 1. 源文件控制类
NOEXTEND:C51源文件不允许使用ANSI C扩展功能。
DEFINE(DF):定义预处理(在C51命令行)。
2. 2. 目标文件(Object)控制类:
COMPACT LARGE SMALL 选编译模式
DEBUG(DB) 包含调试信息,以供仿真器或dSCope51使用。
NOAMAKE(NOAM) 禁止AutoMake信息记录
NOREGPARMS 禁止用寄存器传递参数
OBJECTEXTEND(OE) Object文件包含附加变量类型信息
OPTIMIZE(OT) 指定优化级别
REGFILE(RF) 指定一个寄存器使用的文件以供整体优化用
REGISTERBANK(RB) 指定一个供绝对寄存器访问的寄存器区名
SRC 不生成目标文件只生成汇编源文件
其它控件不常用。
3. 3. 列表文件(listing)控制类:
CODE(CD):向列表文件加入汇编列表
LISTINCLUDE(LC):显示indude文件
SYMBOLS(SB):列表文件包括模块内所有符号的列表
WARNINGLEVEL(WL):选择“警告”级别
2. 第二节 dScope51的使用
1. 1. dScope51 for Dos
总的来说dScope51具有以下特性:
l 高级语言显示模式
l 集成硬件环境模拟
l 单步或“GO”执行模式
l 存储器、寄存器及变量访问
l Watch表达式之值
l 函数与信号功能
下面,具体说明在进入dScope51 for Dos之后,如何实现上述功能,dScope51采用下拉菜单格式和窗口显示控制,共有language、serial、exe、register四个窗口,其中exe为命令行窗口,language为程序窗口,serial为串口窗,register为寄存器窗。
1. (1) 高级语言显示模式
单击主菜单中的“View”,第一栏中的三条命令“Highlevel”、“Mixed”、“Assembly”分别对所装入的程序按照“高级”、“混合级”及“汇编级”三种方式显示,以方便调试使用。
2. (2) 集成硬件环境模拟显示
主菜单中“Peripheral”各条能显示模拟硬件环境的状态,其中:
i/o Port:显示各I/O口之值,对8031而言SFR中的P1、P2、P3、P0与引脚之值分别列出:
Interrupt:显示5个中断源的入口模式是否允许,优先级等中断状态。
Timer:显示各定时/计数器的模式,初始值状态等。
int Message:中断信息允许,如为允许(“>>”出现),则当中断申请时,显示中断源信息。比如当中断发生时会显示:
“interrupt Timer 0 occured”等
A/D converter:
显示A/D转换器状态无时,则提示“无”。
Serial:串口信息显示,包括串口模式、波特产等
Other:其它器件,如为8031则显示“ 无”
3. (3) 单步或“Go”执行
“F8”单步执行,“F5”全速执行到断点。或选主菜单中Trace单步执行CPU中的Go全速执行。
4. (4) 存储器寄存器及变量访问
外部存储器管理MAP菜单:设置(set)、取消(reset)、显示(Display)处理可用存储空间。
修改Code代码:ASM命令
存储器显示命令:D 类别为(X、D、I、B、C)
修改存储器命令:E 有以下几种命令EB、EC、EI、EL、EF、EP
复杂数据类型显示:Object命令;用以显示结构或数组的内容。欲使此命令有效,C51编译器必须有DB及OBJECTEXTEND两条。
反汇编命令:U
5. (5) “Watch”表达式之值
在View菜单的“Watch”一栏中有四项:其中包括定义Watch Point(Define)、删除Watch Point(remove,kill all),及自动更新选项。
也可用WS、WK等命令代替,下面具体看“表达式”类型:
dScope51一次最多可设16个WtchPoint表达式,显示于Watch Window之中,表达式可以是简单变量,也可是复杂数据类型如结构、数组和指向结构的指针等,例如:
>WS *ptime
>WS ptime→hour
>WS some_record[o],analog等等
6. (6) 关于.IOF文件
启动DS51后必须装入.IOF文件才能使CPU及Peripheral各项起作用,这个函数的使用是依据8051系列CPU的不同特点,装入8051各CPU硬件设备模拟驱动文件,比如8031CPU就必须load DS51目录下的8051.IOF。
2. 2. dScope for Windows
dScope for windows具有dScope for dos的全部功能,此外,它还具有以下明显的优点:
(1) 标准的Windows界面,操作更容易更简单;
(2) 常用操作多用对话框,而非Dos的行命令方式;
(3) 窗口资源更加丰富:存储器窗口、覆盖率分析、运行状态分析窗口,加强了调试功能;
因为dScope for Windows功能强大,具体操作在第八章详细介绍。
3. 第三节 Monitor51及其使用
1. 1. Monitor51对硬件的要求
(1) 硬件系统为51系列CPU;
(2) 带5K外部程序存储器(从O地址开始),存放Monitor51程序;
(3) 256Bytes的外部数据存储器以及5K的跟踪缓冲区,此外,外部数据存储器必须足够容纳所有应用程序代码及数据,且所有外部数据存储器必须为冯·诺伊曼存储器,即能一致访问XDATA与Code空间。
(4) 一个定时器作为波特率发生器供串口使用;
(5) 6 Bytes的空余堆栈。
2. 2. Mon51的使用
Mon51的使用途径有三种方式:
(1) Dos行命令方式
即先用install对MON51进行配置,然后用MON51进入Monitor状态,启用各种命令对Monitor51进行调试。
(2) tScope51方式
启动tScope51装入TS51目录下的MON51.IOT驱动文件,与目标板通信。
(3) dScope51 for Windows方式
在选CPU驱动文件时,选“MON51.dll”,则检查目标板并进入MON51状态。
3. 3. MON51的配置
(1) MON51 for Dos的配置
运行install文件(在MON51目录下),不同的参数可以配置不同的硬件环境。INSTALL Serialtype [xdstastart[codestart[bank][PROMCHECK]]],具体说明见MON51帮助文件或使用手册。
(2) MON51 for Windows的配置
在启用MON51.dll时,会使得系统自动检查目标板连接,如配置不对,则弹出“Configuration”对话框,设置PC串口,波特率等,完毕单击“apply”有效。
4. 4. 串口连接图:
收发交叉互连,RTS、CTS直连,DSR、DTR直连,具体引脚排列参考串口资料。
5. 5. MON51命令及使用
详细的MON51命令可参阅帮助。
4. 第四节 集成开发环境(IDE)的使用
1. 1. Ishell for Dos的使用
进入Ishell之后看到两个窗口:一个是文件窗口,一个是Dos命令行窗口,窗口上方是下拉式的命令菜单,其中的Files控制文件窗口的显隐。
使用Ishell,第一步就是配置系统,即要学习两个文件的修改与创建:
1. (1) Ishell.CFG文件
每一个project都有一个Ishell.CFG,其中存放有“Option菜单和Setup菜单下的部分信息;Bell enabled、Monochrome enabled、Editor Selected、CRT Lines、target enviroment、name of user edit、Automatic load for configuration enabled、file window enabled、file specification for file window、translate command line controls、project name等。
对每个project都必须设置以上信息,然后存盘“setup”的的“save”,这样才可正式开始下面工作。
2. (2) IShell.col文件
对IDE颜色设置,如不改动,可以缺省为主。
3. (3) CDF文件
该文件位于BIN目录下,每一文件定义一组外部函数工具包,即定义外部环境如8051.CDF,USER.CDF等,开发者可修改CDF文件,供自己使用,至于CDF文件内容可查看一下8051.CDF即可知道。注意.CDF文件是Ishell系统的核心所在,不同的CDF文件可使本IDE适用于不同的编译、连接系统,即本IDE并不仅适于C51。
下面谈一谈Automake工具:
C51的Automake是一个project管理器,在8051工具包中以OBJECT文件形式保留了一个project的信息,AutoMake用这些信息来进行project管理,一旦手工建立一个project,Automake可生成一个新的OBJECT,AutoMake利用此文件来编译那些修改过的文件。
Automake支持C51、A51、L51/BL51、C166、A166、L166等编译连接器。点中主菜单中的Automake即运行本工具。
Ishell for Dos使用比较繁琐,推荐使用uVision for windows。
2. 2. uVision for windows的使用
uVision是一个标准的windows应用程序,其编译功能、文件处理功能、project处理功能、窗口功能以及工具引用功能(如A51、C51、PL/M41、BL51 dScope等)等都较Ishell for Dos要强得多。
uVision采用BL51作连接器,因为BL51兼容L51,所以一切能在Dos下工作的project都可以到uVision中进行连接调试。
uVision采用dScope for windows作调试器,该调试器支持MON51及系统模拟两种方式,功能较for DOS要强大好用,调试功能强大。
注意:
(1) Option菜单下的各项要会使用,其中A51、C51、PL/M51、BL51定义各文件所使用的编译、连接控制指令,dScope定义一个dScope初始化文件。Make则是定义一个make文件。
(2) 进入调试是在RUN菜单下运行dScope。
(3) project中包括新建、打开、修改、更新、编译、连接等poject处理,具体使用可参考后面的例子。
3. 第三章 Keil C51 vs 标准C
深入理解并应用C51对标准ANSIC的扩展是学习C51的关键之一。因为大多数扩展功能都是直接针对8051系列CPU硬件的。大致有以下8类:
l 8051存储类型及存储区域
l 存储模式
l 存储器类型声明
l 变量类型声明
l 位变量与位寻址
l 特殊功能寄存器(SFR)
l C51指针
l 函数属性
具体说明如下(8031为缺省CPU)。
1. 第一节 Keil C51扩展关键字
C51 V4.0版本有以下扩展关键字(共19个):
_at_ idata sfr16 alien interrupt small
bdata large _task_ Code bit pdata
using reentrant xdata compact sbit data sfr
2. 第二节 内存区域(Memory Areas):
1. 1. Pragram Area:
由Code说明可有多达64kBytes的程序存储器
2. 2. Internal Data Memory:
内部数据存储器可用以下关键字说明:
data:直接寻址区,为内部RAM的低128字节 00H~7FH
idata:间接寻址区,包括整个内部RAM区 00H~FFH
bdata:可位寻址区, 20H~2FH
3. 3. External Data Memory
外部RAM视使用情况可由以下关键字标识:
xdata:可指定多达64KB的外部直接寻址区,地址范围0000H~0FFFFH
pdata:能访问1页(25bBytes)的外部RAM,主要用于紧凑模式(Compact Model)。
4. 4. Speciac Function Register Memory
8051提供128Bytes的SFR寻址区,这区域可位寻址、字节寻址或字寻址,用以控制定时器、计数器、串口、I/O及其它部件,可由以下几种关键字说明:
sfr:字节寻址 比如 sfr P0=0x80;为PO口地址为80H,“=”后H~FFH之间的常数。
sfr16:字寻址,如sfr16 T2=0xcc;指定Timer2口地址T2L=0xcc T2H=0xCD
sbit:位寻址,如sbit EA=0xAF;指定第0xAF位为EA,即中断允许
还可以有如下定义方法:
sbit 0V=PSW^2;(定义0V为PSW的第2位)
sbit 0V=0XDO^2;(同上)
或bit 0V-=0xD2(同上)。
2. 第二节 几类重要库函数
1. 1. 专用寄存器include文件
例如8031、8051均为REG51.h其中包括了所有8051的SFR及其位定义,一般系统都必须包括本文件。
2. 2. 绝对地址include文件absacc.h
该文件中实际只定义了几个宏,以确定各存储空间的绝对地址。
3. 3. 动态内存分配函数,位于stdlib.h中
4. 4. 缓冲区处理函数位于“string.h”中
其中包括拷贝比较移动等函数如:
memccpy memchr memcmp memcpy memmove memset
这样很方便地对缓冲区进行处理。
5. 5. 输入输出流函数,位于“stdio.h”中
流函数通8051的串口或用户定义的I/O口读写数据,缺省为8051串口,如要修改,比如改为LCD显示,可修改lib目录中的getkey.c及putchar.c源文件,然后在库中替换它们即可。
3. 第三节 Keil C51库函数原型列表
1. 1. CTYPE.H
bit isalnum(char c);
bit isalpha(char c);
bit iscntrl(char c);
bit isdigit(char c);
bit isgraph(char c);
bit islower(char c);
bit isprint(char c);
bit ispunct(char c);
bit isspace(char c);
bit isupper(char c);
bit isxdigit(char c);
bit toascii(char c);
bit toint(char c);
char tolower(char c);
char __tolower(char c);
char toupper(char c);
char __toupper(char c);
2. 2. INTRINS.H
unsigned char _crol_(unsigned char c,unsigned char b);
unsigned char _cror_(unsigned char c,unsigned char b);
unsigned char _chkfloat_(float ual);
unsigned int _irol_(unsigned int i,unsigned char b);
unsigned int _iror_(unsigned int i,unsigned char b);
unsigned long _irol_(unsigned long l,unsigned char b);
unsigned long _iror_(unsigned long L,unsigned char b);
void _nop_(void);
bit _testbit_(bit b);
3. 3. STDIO.H
char getchar(void);
char _getkey(void);
char *gets(char * string,int len);
int printf(const char * fmtstr[,argument]…);
char putchar(char c);
int puts (const char * string);
int scanf(const char * fmtstr.[,argument]…);
int sprintf(char * buffer,const char *fmtstr[;argument]);
int sscanf(char *buffer,const char * fmtstr[,argument]);
char ungetchar(char c);
void vprintf (const char *fmtstr,char * argptr);
void vsprintf(char *buffer,const char * fmtstr,char * argptr);
4. 4. STDLIB.H
float atof(void * string);
int atoi(void * string);
long atol(void * string);
void * calloc(unsigned int num,unsigned int len);
void free(void xdata *p);
void init_mempool(void *data *p,unsigned int size);
void *malloc (unsigned int size);
int rand(void);
void *realloc (void xdata *p,unsigned int size);
void srand (int seed);
5. 5. STRING.H
void *memccpy (void *dest,void *src,char c,int len);
void *memchr (void *buf,char c,int len);
char memcmp(void *buf1,void *buf2,int len);
void *memcopy (void *dest,void *SRC,int len);
void *memmove (void *dest,void *src,int len);
void *memset (void *buf,char c,int len);
char *strcat (char *dest,char *src);
char *strchr (const char *string,char c);
char strcmp (char *string1,char *string2);
char *strcpy (char *dest,char *src);
int strcspn(char *src,char * set);
int strlen (char *src);
char *strncat (char 8dest,char *src,int len);
char strncmp(char *string1,char *string2,int len);
char strncpy (char *dest,char *src,int len);
char *strpbrk (char *string,char *set);
int strpos (const char *string,char c);
char *strrchr (const char *string,char c);
char *strrpbrk (char *string,char *set);
int strrpos (const char *string,char c);
int strspn(char *string,char *set);
6. 第六章 Keil C51例子:Hello.c
Hello位于\C51\excmples\Hello\目录,其功能是向串口输出“Hello,world”整个程序如下:
#pragma DB OE CD
#indule <reg51.h>
#include<stdio.h>
void main(void)
{
SCOn=0x50;
TMOD=0x20
TH1=0xf3;
Tri=1;
TI=1;
printf(“Hello,world \n”);
while(1) { }
}
1. 第一节 uVision for Windows的使用步骤
(1) file_new新建一个hello.c文件,输入如上内容或直接用目录下源文件。
(2) file_save或工具栏将文件存盘。
(3) project_new project创建一个project名为hello,并在其中加入hello.c。
这时该project已是打开状态,或用open project打开已存在的project。
(4) option_C51 compiler中选出至少包括两项DB OE。
(5) option_dscope Debugger选中hello\DS51.INI
查看DS51.INI看其是否为:
“load…\…\BIN\8051.DLL
map 0, 0xffff”
否则修改。
(6) 在option_make选make文件顺序。
(7) project选Build project,看是否有语法错误,若无则生成HEX文件,若有则修改源文件后重复以上部分步骤。
(8) run_dScope debugger进入dScope51后装入hello则可用go直接运行看serial窗口有无输出,正常每系统运行一次,serial窗口均出现一个“Hello,world”表明运行无误。
2. 第二节 Ishell for Dos使用步骤
(1) 进入Ishell 用Setup editer选择编辑器。
然后单击Edit或用Edit命令编辑hello.c源文件,存盘,也可以在files窗口中直接选中hello.c。
(2) 用cd改换project目录至hello目录。
(3) 在setup_target一项目选8051。
(4) 在setup_C51中输出DB OE。
(5) 在setup_project输入project名hello。
(6) 在setup_save保存Ishell.CFG文件。
(7) 编辑一个Link文件hello.lin中有“hell.obj”一行。
(8) 由光标落在files菜单中的Hello.c上,单击“translate”,如无语法错,再击“link”,则Hex文件生成。
(9) 单击Simulate如在8051.CDF中选Simulate为dScope则进入dScope调试直接“Go”,看serial窗口输出为“Hello.world”。
(10) 如程序有误修改源代码后不必再translate或link了,只要一步Amake即可。
若project中包括不止一个文件,在DOS的Ishell中不能用Translate编译,而应建立bat文件,直接在命令窗编译,然后link连接。
如还需用Translate则只能多个文件分别编译,然后连接。
7. 第七章 Keil C51的代码效率
C51程序编译生成汇编代码的效率,是由许多因素共同决定的,对于Keil C51,主要受以下两种因素影响:
1. 第一节 存储模式的影响
存储模式决定了缺省变量的存储空间,而访问各空间变量的汇编代码的繁简程度决定了代码率的高低。
例如:一个整形变量i,如放于内存18H、19H空间,则++i的操作编译成四条语句:
INC 0x19
MOV A,0x19
JNZ 0x272D
INC 0x18
0x272D:
而如果放于外存空间0000H、0001H则++i的操作编译成九条语句:
MOV DPTR,0001
MOVX A,@ DPTR
INC A
MOVX @ DPTR,A
JNz #5
MOV OPTR,#0000
MOVX A,@DPTR
INC A
MOVX @ DPTR,A
就汇编之后的语句而言,对外部存储器的操作较内部存储器操作代码率要低得多,生成的语句为内存的两倍以上,而程序中有大量的这种操作,可见存储模式对代码率的响了。
因此程序设计的原则是
1、存储模式从small-Compact-large依次选择,实在是变量太多,才选large模式。
2、即使选择了large模式,对一些常用的局部的或者可放于内存中的变量,最好放于内存中,以尽量提高程序的代码率。
2. 第二节 程序结构的影响
程序的结构单元包括模块、函数等等。同样的功能,如果结构越复杂,其所涉及的操作、变量、功能模块函数等就越多,较之结构性好,代码简单的程序其代码率自然就低得多。
此外程序的运行控制语句,也是影响代码率的关键因素,例如:switch -case语句,许多编译器都把它们译得非常复杂,Keil C51也不例外,相对较为简易的Switch-case语句,编译成跳转指令形式,代码率较高,但对较为复杂的Switch-Case,则要调用一个系统库函数?C?ICASE进行处理,非常复杂。
再如if( ),while( ),等语句也是代码相对较低的语句,但编译以后比switch-case要高得多。
因此建议设计者尽量少用switch-case之类语句来控制程序结构,以提高代码率。
除以上两点外,其它因素也会对代码率产生影响,例如:
是否用寄存器传递参数 即NOAREGS选项是否有
是否包括调试信息:即DEBUG选项
是否包括扩展的调试信息:即BJECTEXTEND
8. 第八章 dScope for Windows使用详解
1. 第一节 概述
1. 1. 主窗口(Mainframe Window)
可设置其它各种调试窗口,设置断点、观察点,修改地址空间,加载文件等等;
2. 2. 调试窗口(DEBUG Window)
支持用户程序的各种显示方式,可连续运行,单步运行用户程序,并可在线 汇编;
3. 3. 命令窗口(Command Window)
支持命令行的输入;
4. 4. 观察窗口(Watch Window)
可设置所要观察的变量、表达式等;
5. 5. 寄存器窗口(Registe Window)
显示内部寄存器的内容,程序运行次数等;
6. 6. 串口窗口(Serical Windows)
显示串口接收和发送的数据;
7. 7. 性能分析窗口
显示所要观察的各程序段占用CPU的空间;
8. 8. 内存窗口(Memory Window)
显示所选择的内存中的数据;
9. 9. 符号浏览窗口(Symbol Browser Window)
显示各种符号名称,包括专有符号,用户自定义符号(函数名、变量、标号)等;
10. 10. 调用线窗口(Call-Stack Window)
动态显示当前执行的程序段的函数调用关系;
11. 11. 代码覆盖窗口
提供当前模块内各程序段中被执行代码的比率;。
12. 12. 外围设备窗口(peripherals)
可显示I/O口,定时器,中断,串口等外围设备状态;
2. 第二节 dScope for Windows基本操作
1. 1. 指定初始化文件
在uVision的Option菜单dScope Debugger中指定dScope的初始化文件,用uVision的RUN启动dScope将自动加载此初始化文件,自动执行其中命令;
下面是一个例子,可以看出调入一个调试代码的过程。Ds51.ini:
load 8051.dll
load test
slog>>test.log
xtal=11.0592
define button "go to main","g,main"
ws RevCounter
ws rm.r
g,main
PA RESET
PA serial
PA timer0
2. 2. 观察变量
方法1:命令行
WS expression [, numberbase ] [ LINE ]
其中numberbase为显示数制,10对应10进制,16对应16进制,缺省为16进制。LINE为单行显示,缺省为多行显示。
方法2:setup->Watchpoints,在对话框中输入变量
3. 3. 显示RAM的值
d i(x,d):起始地址,终止地址
d 变量名
4. 4. 观察堆栈
View->Call-stack->Show invocation,可以跟踪调用过程;
5. 5. 中断处理程序调试
在装入8051.dll后,在dScope的主菜单中将增加Peripherial,其有4个字菜单:
I/0 port:Pi端口状态
Interrupt:中断设置
Timer:定时器中断状态
Serial:串口中断状态
设置相应的中断请求标志位即可产生中断。
6. 6. 性能分析(Performance Analyzer:PA)
PA用来分析一段代码执行占用CPU的百分比。定义:
命令行 PA func_name
3. 第三节 dScope for Windows命令文件的编制
dScope除了用命令行的方式进行调试以外,还可将各种调试命令汇集于一个调试文件中,然后调用该文件,就可达到自动测试用户源代码的目的。dScope的命令文件支持C/PL/M的格式,因而编制调试命令文件与编制C语言程序有些类似。
1. 1. 地址空间及地址空间类型
1. (1) 地址空间分段
dScope提供的最大可用空间为16M,实际上我们只用以下三段:
① 内部数据空间段(0X00段或D段)
0X00:0X0000~0X00:0XFFFF(对MSC51而言为0X00:0X00FF)
② 外部数据空间段(0X01段式或X段)
0X01:0X0000~0X01~0XFFFF
③ 程序空间段(0XFF段或C段)
0XFF:0X0000~0XFF:0XFFFF
2. (2) 地址空间类型
C:代码空间
D:内部直接寻址空间
I: 内部间接寻址空间
X:外部数据空间
B:位寻址空间
P:I/O口
EB:扩展的位寻址空间(MCS251专有)
ED:扩展的数据空间(MCS251专有)
CO:常数空间(MCS251专有)
HC:正常数空间(MCS251专有)
2. 2. 常量
dScope支持十六进制、八进制、十进制、二进制常数,其后缀分别为H、Q(O)、T(或无)、Y;
dScope不区分常量的大、小写。
1. (1) 整型常量
分为整型(int),无符号整型(uint,00rd),长整型(long),无符号长整型(Wlong、Word)。
2. (2) 浮点型常量
与ANSI C相同。
3. (3) 字符串常量
与ANSI C相同
4. (4) 字符常量
分为字符型(Char)和无符号字符型(Uchar)一种。
5. (5) 行号常数
指用户程序中的行号,实际上是个地址
6. (6) 位常量(Bit):
0和1
7. (7) 地址常数
地址常数的种类很多,地址常数不同于行号常数,行号常数就是一个地址,而地址数被引用时,实际上是取该地址中的数据。
C:代码地址常数,如C:0X0012或0XFF:0X0012
D:内部直接寻址地址常数,如D:0X0068或0X00:0X0068
I:内部间按寻址地址常数,如I:0X0010或0X00:0X0010
X:外部数据空间地址常数,如X:0X0028或0X01:0X0028
B:位地址常数,如B:0X20或B:0X24.0
EB:扩展的位地址常数(MCS251专有),
ED:扩展的数据空间地址常数(MCS251专有)
CO:常数空间地址常数(MCS251专有)
HC:正常数空间地址常数(MCS251专有)
8. (8) 标识符常量
即用户源程序中的标号、函数名等,实际上代表某一地址。
9. (9) 用户源程序中定义的常数
3. 3. 变量
dScope所支持的变量名或标识符最多可由31个字符组成,第一个字母为A~Z,a~z,下划线或问号,后续字符可为字母、数字、下划线和问号。除CPU变量和系统变量外,dScope不支持全局变量,但可视“define”命令定义的变量为全局变量。
Dscope所支持的变量分为以下几种(变量名称不区分大、小写),支持类型转换:
1. (1) 整型变量
分为整型变量(int)、无符号整型变量(uint/word),长整型(Long) 、无符号长整型(Ulong/dword)。
2. (2) 浮点型变量(float)
与ANSI C相同。
3. (3) 字符型变量L
分为字符型(char)变量和无符号字符型(Uchar)
4. (4) 位变量(Bit)
5. (5) 系统变量
dScope自己定义了一系列内部变量,用户可对这些变量进行读或读/写操作, 可被用户自定义数所引用。
a. Cycles (Read Only)
32位变量(Ulong),指示当前程序执行已花费的指令周期(cycle)。
b. Ramsize(R/W)
16位变量(Uint),指示内部可直接寻址的数据空间大小。
c. Radix(R/N)
8位变量(Uchar),决定输出的数制
Radix=0X0A (10进制),Radix=0X10 (16进制)
d. -IIP-(R/W)
8位变量(Uchar),指示当前的中断嵌套数目。
e. $ (R/W)
32位变量(Ulong),指出PC值,通过对其进行写操作,可改变程序执行的流程。
f. Itrace (R/W)
8位变量(Uchar),决定是否对程序运行情况进行记录
Itrace=1,使能记录操作
Itrace=0,根本上记录操作
g. __Break__(R/W)
8位变量(Uchar) __Break__=1,中止程序的运行
h. __Mode__和__Frame size__是MCS 251专有的变量。
6. (6) CPU变量
即R0~R7、A、C(位变量)、B、DPTR及特殊功能寄存器变量,对这些变量均可进行读、写操作。
7. (7) 用户源程序中定义的变量、数组、结构等
4. 4. 运算符
dScope支持ANSI C的运算符,包括算术运算符,逻辑运算符,关系运算符。
5. 5. 表达式
以运算符将dScope所支持的常量、变量、函数等连接在一起,就构成了dScope的表达式。
6. 6. 数组
dScope不支持在命令文件中定义数组,但可引用用户程序中的数组,引用方式如同C。
7. 7. 结构和联合
dScope不支持在命令文件中定义结构和联合,但可引用用户程序中的结构和联合,引用方式如同C,但如要输出整个结构或联合的结果,就要用命令“OBJ”。
8. 8. 指针:
不可自定义指针,但支持用户源程序中的指针变量。
9. 9. dScope命令语句
dScope提供了一系列调试命令。在命令文件中,dScope只支持这些语句及前述定义的表达式,C语言的语句均不被支持,但在命令文件所包含的用户自定义函数(非用户源程序中的函数)中支持C语句,但用户自定义函数中同样不支持数组、结构、联合和指针。
1. (1) ASM
在线汇编命令,格式如下:
ASM C:0Xnnnn (或标号);设定插入汇编指令的地址
ASM 汇编指令
ASM 汇编指令
插入完毕后,在debug窗口内选择“Assemble->Assemble”完成编译。
2. (2) Assign
串行口分配指令,格式如下:
Assign channel<unreg>outreg
对MCS51为:Assign Win<SOIN> Soot
但目前的dScope版本并未提供完整串口窗口功能。
3. (3) Define
用户自定义变量指令,格式如下:
Define <类型> <变量名>
类型一为如前所述的变量类型,Define指令定义的变量可能为全局变量,可为用户自定义函数所引用。
4. (4) Display
内存显示命令,格式如下二:
D 起始地址,结束地址
地址如前所述的地址常数,标识符常量。
5. (5) Enter
内存修改指令,格式如下:
E 类型地址=表达式 [表达式2],[……]
类型如前所述,地址如前所述的地址常数。表达式如前所述,但如果是函数名称(含标号、指针变量),则关键字E→EP
6. (6) Map/Reset map
Map为内存段修改指令,Reset map将内存段复位或缺省值。
7. (7) Object
用以引用用户源程序中的结构(联合)、数组、格式如下:
Obj表达式 [n,],[Line]
表达式为用户源程序中的数组,结构(联合)名称。当Line缺省时,数目、结构(联合)的内容按n行输出;如有Line,则单行输出。
8. (8) U
反汇编命令,格式如下:
U [地址]
地址包括地址常 数及标识符常量,指明反汇编的起始地址。
9. (9) WK
观察点删除命令,格式如下:
WK n1[n2 ],[……] ;删除指定的观察点,n为字符型,整型
常数
WK * ;删除所有的观察点
10. (10) WS
观察点设置命令,格式如下:
WS 表达式[,n][LINE]
关键字LINE存在时,观察点表达式单行输出
LINE缺省时,观察点表达式n行输出。
11. (11) G
连续运行命令,格式如下:
G [起始地址],[终止地址]
地址为标识符常量或地址常数,地址缺省时,为连续运行。
12. (12) T/P
单步运行指令,格式如下:
T/P n ;n指至单行运行的步数,P指给用户当调用某函数时,把它作为一步处理,并不进入该函数运行。
13. (13) PA
性能分析操作指令,其分以下几种:
PA
显示当前所设置的性能分析程度段
PA Kill<SPAN style="mso-s