光电工程师社区
标题:
Keil C51 使用技巧及实战
[打印本页]
作者:
suncon
时间:
2003-4-8 05:46
标题:
Keil C51 使用技巧及实战
Keil C51 使用技巧及实战
1
第一章介绍
这是一本关于Intel 80C51 以及广大的51 系列单片机的书这本书介绍给读者一些
新的技术使你的8051 工程和开发过程变得简单请注意这本书的目的可不是教你各种
8051 嵌入式系统的解决方法
为使问题讨论更加清晰在适当的地方给出了程序代码我们以讨论项目的方法来说
明每章碰到的问题所有的代码都可在附带的光盘上找到你必须熟系C 和8051 汇编因
为本书不是一本C 和汇编的指导书你可以买到不少关于ANSI C 的书最佳选择当然是Intel
的数据书可从你的芯片供应商处免费索取和随编译工具附送的手册
附送光盘中有我为这本书编写和收集的程序这些程序已经通过测试这并不意味着
你可以随时把这些程序加到你的应用系统或工程中有些地方必须首先经过修改才能结合
到你的程序中
这本书将教你充分使用你的工具如果你只有8051 的汇编程序你也可以学习该书和
使用这些例子但是你必须把C 语言的程序装入你的汇编程序中这对懂得C 语言和8051
汇编程序指令的人来说并不是一件困难的事
如果你有C 编译器的话那恭喜你使用C 语言进行开发是一个好的决定你会发现
使用C 进行开发将使你的工程开发和维护的时间大大减少如果你已经拥有Keil C51 那
你已经选择了一个非常好的开发工具我发现Keil 软件包能够提供最好的支持本书支持
Keil C 的扩展如果你有其它的开发工具像Archimedes 和Avocet 这本书也能很好地为
你服务但你必须根据你所用的开发工具改变一些Keil 的特殊指令
在书的一些地方有硬件图实例程序在这些硬件上运行这些图绘制地不是很详细
主要是方框图但足以使读者明白软件和硬件之间的接口
读者应该把这本书看成工具书而不是用来学习各种系统设计通过本书你可以了
解给定一定的硬件和软件设计之后8051 的各种性能希望你能从本书中获取灵感并有助
于你的设计使你豁然开朗当然我希望你也能够从本书中学到有用的知识使之能够
提升你的设计
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
2
第二章硬件
1 概述
8051 系列微处理器基于简化的嵌入式控制系统结构被广泛应用于从军事到自动控制
再到PC 机上的键盘上的各种应用系统上仅次于Motorola 68HC11 在8 位微控制器市场
上的销量很多制造商都可提供8051 系列单片机像Intel Philips Siemens 等这些
制造商给51 系列单片机加入了大量的性能和外部功能像I2C 总线接口模拟量到数字量
的转换看门狗PWM 输出等不少芯片的工作频率达到40M 工作电压下降到1.5V 基
于一个内核的这些功能使得8051 单片机很适合作为厂家产品的基本构架它能够运行各种
程序而且开发者只需要学习这一个平台
8051 系列的基本结构如下
1 一个8 位算术逻辑单元
2 32 个I/O 口4 组8 位端口可单独寻址
3 两个16 位定时计数器
4 全双工串行通信
5 6 个中断源两个中断优先级
6 128 字节内置RAM
7 独立的64K 字节可寻址数据和代码区
每个8051 处理周期包括12 个振荡周期每12 个振荡周期用来完成一项操作如取指
令和计算指令执行时间可把时钟频率除以12 取倒数然后指令执行所须的周期数
因此如果你的系统时钟是11.059MHz 除以12 后就得到了每秒执行的指令个数为921583
条指令取倒数将得到每条指令所须的时间1.085ms
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
3
2 存储区结构
8051 结构提供给用户3 个不同的存储空间如图A-1 每个存储空间包括从0 到最大
存储范围的连续的字节地址空间通过利用特定地址的寻址指令解决了地址重叠的问题
三个地址空间的功能如图所示
图A-1-8051 存储结构
2.1 CODE 区
第一个存储空间是代码段用来存放可执行代码被16 位寻址空间可达64K 代码
段是只读的当要对外接存储器件如EPROM 进行寻址时处理器会产生一个信号但这并
不意味着代码区一定要用一个EPROM 目前一般使用EEPROM 作为外接存储器可以被外
围器件或8051 进行改写这使系统更新更加容易新的软件可以下载到EEPROM 中而不
用拆开它然后装入一个新的EEPROM 另外带电池的SRAMs 也可用来代替EPROM 他可
以像EEPROM 一样进行程序的更新并且没有像EEPROM 那样读写周期的限制但是当电
源耗尽时存储在SRAMs 中的程序也随之丢失使用SRAMs 来代替EPROM 时允许快速下
载新程序到目标系统中这避免了编程/调试/擦写这样一个循环过程不再需要使用昂贵
的在线仿真器
除了可执行代码还可在代码段中存储查寻表为达此目的8051 提供了通过数据指
针DPTR 或程序计数器加上由累加器提供的偏移量进行寻址的指令这样就可以把表头地址
装入DPTR 中把表中要寻址的元素的偏移量装入累加器中8051 在执行指令时的过程中
把这两者相加由此可节省不少指令周期在以后的例子中我们会看到这点
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
4
2.2 DATA 区
第二个存储区是8051 内128 字节的内部RAM 或8052 的前128 字节内部RAM 这部分
主要是作为数据段称为DATA 区指令用一个或两个周期来访问数据段访问DATA 区比
访问XDATA 区要快因为它采用直接寻址方式而访问XDATA 须采用间接寻址必须先初
始化DPTR 通常我们把使用比较频繁的变量或局部变量存储在DATA 段中但是必须节省
使用DATA 段因为它的空间毕竟有限
在数据段中也可通过R0 和R1 采用间接寻址R0 和R1 被作为数据区的指针将要恢
复或改变字节的地址放入R0 或R1 中根据源操作数和目的操作数的不同执行指令需要
一个或两个周期
数据段中有两个小段第一个子段包含四组寄存器组每组寄存器组包含八个寄存器
共32 个寄存器可在任何时候通过修改PSW 寄存器的RS1 和RS0 这两位来选择四组寄存器
的任意一组作为工作寄存器组8051 也可默认任意一组作为工作寄存器组工作寄存器组
的快速切换不仅使参数传递更为方便而且可在8051 中进行快速任务转换
另外一个子段叫做位寻址段BDATA 包括16 个字节共128 位每一位都可单独寻
址8051 有好几条位操作指令这使得程序控制非常方便并且可帮助软件代替外部组合
逻辑这样就减少了系统中的模块数位寻址段的这16 个字节也可像数据段中其它字节一
样进行字节寻址
2.3 特殊功能寄存器
中断系统和外部功能控制寄存器位于从地址80H 开始的内部RAM 中这些寄存器被称
做特殊功能寄存器简称
SFR 其中很多寄存器都
可位寻址可通过名字进
行引用如果要对中断使
能寄存器中的EA 位进行
寻址可使用EA 或IE.7
或0AFH SFRs 控制定时/
计数器串行口中断源
及中断优先级等这些寄
存器的寻址方式和DATA
取中的其它字节和位一样
可位寻址SFR 如表A-1 所示可进行位寻址的SFR 表A-1
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
5
2.4 IDATA 区
8051 系列的一些单片机如8052 有附加的128 字节的内部RAM 位于从80H 开始的地址
空间中被称为IDATA 因为IDATA 区的地址和SFRs 的地址是重叠的通过区分所访问的
存储区来解决地址重叠问题因为IDATA 区只能通过间接寻址来访问
2.5 XDATA 区
8051 的最后一个存储空间为64K 和CODE 区一样采用16 位地址寻址称作外部数
据区简称XDATA 区这个区通常包括一些RAM 如SRAM 或一些需要通过总线接口的外
围器件对XDATA 的读写操作需要至少两个处理周期使用DPTR R0 或DPTR R1 对DPTR
来说至少需要两个处理周期来装入地址而读写又需要两个处理周期同样对于R0
或R1 装入需要一个以上的处理周期而读写又需两个周期由此可见处理XDATA 中的数
据至少要花3 个指令周期因此使用频繁的数据应尽量保存在DATA 区中
如果不需要和外部器件进行I/O 操作或者希望在和外部器件进行I/O 操作时开关RAM
则XDATA 可全部使用64K RAM 关于这方面的应用将在以后介绍
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
6
3 位操作和布尔逻辑
8051 可分别对BDATA 和SFRs 中128 个可寻址位32 个I/O 口进行位逻辑操作可对
这些位进行与或异或求补置位清零等操作并可像转移字节那样转移位
列表A-1
MOV C 22H 把位地址22H 中的数移入进位位中
ORL C 23H 把位地址23H 中的数和进位位中的数相或
MOV 24H C 把进位位中的数移入位地址24H 中
可寻址位也可作为条件转移的条件一条很有用的指令就是JBC 通过判断可寻址位
是否置位来决定是否进行转移如果该位置位则转移并清零该位这条指令能够在两个
处理周期中完成比在两个代码段中分别使用跳转和清零指令要节省一到两个处理周期
比如说你要编写一个过程等待P0.0 置位然后跳转但是等待有时间限制这样就需
要设置一个时间时间到达后跳出查询检测到P0.0 置位后跳出并清零P0.0 一般的
逻辑流程如下
例A-2
MOV timeout #TO_VALUE 设置查询时间
L2 JB P0.0 L1 P0.0 置位则跳转
DJNZ timeout L2 查询时间计数
L1 CLR P0.0 P0.0 清零
RET 退出
当使用JBC 时程序如下
例A-3
MOV timeout #TO_VALUE 设置查询时间
L2 JBC P0.0 L1 P0.0 置位则跳转并清零
DJNZ timeout L2 查询时间计数
L1 RET 退出
利用JBC 不但节省了代码长度而且使程序更加简洁美观以后在编制代码时要习惯
使用这条指令
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
7
4 寻址方式
8051 可对存储区直接或间接寻址这些是典型的寻址方式直接寻址是在指令中直接
包含所须寻址的字节地址直接寻址只能在DATA 区和SFR 中进行如下例
列表A-4
MOV A 03H 把地址03H 中的数移入累加器
MOV 43H 22H 把地址22H 中的数移入地址43H 中
MOV 02H C 把C 中的数移入位地址02H 中
MOV 42H #18 把立即数18 移入地址42H 中
MOV 09H SBUF 把串行缓冲区中的数移入地址09H 中
间接寻址要使用DPTR PC R0 R1 寄存器用来存放所要访问数据的地址指令使用
指针寄存器而不是直接使用地址用间接寻址方式可访问CODE IDATA XDATA 存储区
对DATA 存储区也可进行间接寻址只能用直接寻址方式对位地址进行寻址
在进行块移动时用间接寻址十分方便能用最少的代码完成操作可以利用循环过
程使指针递增对CODE 区进行寻址时将基址存入DPTR 或PC 中把变址存入累加器中
这种方法在查表时十分有用举例如下
例A-5
DATA 和IDATA 区寻址
MOV R1 #22H 设置R1 为指向DATA 区内的地址22H 的指针
MOV R0 #0A9H 设置R0 为指向IDATA 区内的地址0A9H 的指针
MOV A @R1 读入地址22H 的数据
MOV @R0 A 将累加器中的数据写入地址A9H
INC R0 RO 中的地址变为AAH
INC R1 R1 中的地址变为23H
MOV 34H @R0 将地址AAH 中的数据写入34H
MOV @R1 #67H 把立即数写入地址23H
XDATA 区寻址
MOV DPTR #3048H DPTR 指向外部存储区
MOVX A @DPTR 读入外部存储区地址3048H 中的数
INC DPTR 指针加一
MOV A #26H 立即数26H 写入A 中
MOVX @DPTR A 将26H 写入外部存储区地址3049H 中
MOV R0 #87H R0 指向外部存储区地址87H
MOVX A @R0 将外部存储区地址87H 中的数读入累加器中
代码区寻址
MOV DPTR #TABLE_BASE DPTR 指向表首地址
MOV A index 把偏移量装入累加器中
MOVC A @A+DPTR 从表中读入数据到累加器中
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
8
5 处理器状态
处理器的状态保存在状态寄存器PSW 中状态字中包括进位位用于BCD 码处理的辅
助进位位奇偶标志位溢出标志位还有前面提到的用于寄存器组选择的RS0 和RS1 0
组从地址00H 开始1 组从地址08H 开始2 组从地址10H 开始3 组从地址18H 开始这
些地址都可通过直接或间接方式进行寻址PSW 的结构如下
CY AC F0 RS1 RS0 OV USR P
CY 进位标志位
AC 辅助进位标志位
F0 通用标志位
RS1 寄存器组选择位高位
RS0 寄存器组选择位低位
OV 溢出标志位
USR 用户定义标志位
P 奇偶标志位
6 电源控制
8051 的CHMOS 版本可通过软件设置两种节电方式空闲模式和低功耗模式设置电源
控制寄存器PCON 的相应位来进入节电方式置位IDLE 进入空闲模式空闲模式将停止程
序执行RAM 中的数据仍然保持晶振继续工作但与CPU 断开定时器和串行口继续工
作发生中断将退出中断模式执行完中断程序后将从程序停止的地方继续指令的执行
通过置位PDWN 位来进入低功耗模式低功耗模式中晶振将停止工作因此定时器和
串行口都将停止工作至少有两伏的电压加在芯片上因此RAM 中的数据仍将保存退
出低功耗模式只有两种方式上电或复位
SMOD 位可控制串行通信的波特率将使由定时器1 的溢出率或晶振频率产生的波特率
翻倍置位SMOD 可使工作于方式1 2 3 定时器产生的波特率翻倍当使用定时器2 产生
波特率时SMOD 将不影响波特率
电源控制寄存器不可位寻址
SMOD - - - GF1 GF0 PDWN IDLE
SMOD 串行口通信波特率控制位置位使波特率翻倍
- 保留
- 保留
- 保留
GF1 通用标志位
GF0 通用标志位
PDWN 低功耗标志位置位进入低功耗模式
IDLE 空闲标志位置位进入空闲模式
表A-3
6 中断系统
基本的8051 支持6 个中断源两个外部中断两个定时/计数器中断一个串行口输
入/输出中断中断发生后处理器转到将五个中断入口处之一执行中断处理程序中断向
量位于代码段的最低地址出串行口输入输出中断共用一个中断向量中断服务程序必
须在中断入口处或通过跳转分支转移到别处8051/8052 的中断向量表A-4
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
9
8051 支持两个中断优先级有标准的中
断机制低优先级的中断只能被高优先级的
中断所中断而高优先级的中断不能被中断
6.1 中断优先级寄存器
每个中断源都可通过设置中断优先级寄存
器IP 来单独设置中断优先级如果每个中断
源的相应位被置位则该中断源的优先级为高
如果相应的位被复位则该中断源的优先级为低如果你觉得两个中断源不够用别急
以后我会教你如何增加中断优先级表A-5 示出了IP 寄存器的各位此寄存器可位寻址
IP 寄存器可位寻址
- - PT2 PS PT1 PX1 PT0 PX0
- 保留
- 保留
PT2 定时器2 中断优先级
PS 串行通信中断优先级
PT1 定时器1 中断优先级
PX1 外部中断1 优先级
PT0 定时器0 中断优先级
PX0 外部中断0 优先级
表A-5
6.2 中断使能寄存器
通过设置中断使能寄存器IE 的EA 位使能所有中断每个中断源都有单独的使能位
可通过软件设置IE 中相应的使能位在任何时候使能或禁能中断中断使能寄存器IE 的各
位如下所示
中断使能寄存器IE 可位寻址
EA - ET2 ES ET1 EX1 ET0 EX0
EA 使能标志位置位则所有中断使能复位则禁止所有中断
- 保留
ET2 定时器2 中断使能
ES 串行通信中断使能
ET1 定时器1 中断使能
EX1 外部中断1 使能
ET0 定时器0 中断使能
EX0 外部中断0 使能
6.3 中断延迟
8051 在每个处理周期查询中断标志确定是否有中断请求当发生中断时置位相应
的标志处理器将在下个周期查询到中断标志位这样从发生中断到确认中断之间有一
个指令周期的延时这时处理器将用两个周期的时间来调用中断服务程序总共要花3
个时钟周期在理想情况下处理器将在3 个指令周期内响应中断这使得用户能很快响
应系统事件
不可避免地系统有可能在3 个处理周期能不能响应中断请求特别是当有同级或更
中断源中断向量
上电复位0000H
外部中断0 0003H
定时器0 溢出000BH
外部中断1 0013H
定时器1 溢出001BH
串行口中断0023H
定时器2 溢出002BH
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
10
高级的中断服务程序正在执行的时候因此中断的延迟主要取决于正在执行的程序
另外一种大于3 个周期的中断延迟是程序正在执行一条多周期指令要等到当前的
指令执行完后处理器才会处理中断事件这将在原来的基础上至少增加一个周期的延时
假设在执行完多周期指令的第一个周期后发现中断除被其它中断所阻的情况中断不
被响应的最长延时为6 个处理周期3 个周期的多周期指令执行时间3 个周期的指令响应
时间4
最后一种大于3 个指令周期的中断延迟是当检测到中断时正在执行写IP IE 或RETI
指令
6.4 外部中断信号
8051 支持两个外部中断信号这使外部器件能请求中断从而得到相应的服务外部
中断由外部中断引脚外部中断0 为P3.2 外部中断1 为P3.3 电平为低或电平由高到低
跳变引起由电平触发还是跳变触发取决于寄存器TCON 的ITX 位见A-7
电平触发时当检测到中断引脚电平为低时将产生中断低电平应至少保持一个指
令周期或12 个时钟周期因为处理器每个指令周期检测一次引脚跳变触发时当在连
续的两个周期中检测到由高到低的电平跳变时将产生中断而电平的0 状态应至少保持
一个周期
7 内置定时/计数器
标准的8051 有两个定时/计数器每个定时器有16 位定时/计数器既可用来作为定
时器对机器周期计数也可用来对相应I/0 口TO T1 上从高到低的跳变脉冲计数当
用作计数器时脉冲频率不应高于指令的执行频率的1/2 因为每周期检测一次引脚电平
而判断一次脉冲跳变需要两个指令周期如果需要的话当脉冲计数溢出时可以产生一
个中断
TCON 特殊功能寄存器timer controller 用来控制定时器的工作起停和溢出标志位
通过改变定时器运行位TR0 和TR1 来启动和停止定时器的工作TCON 中还包括了定时器T0
和T1 的溢出中断标志位当定时器溢出时相应的标志位被置位当程序检测到标志位从
0 到1 的跳变时如果中断是使能的将产生一个中断注意中断标志位可在任何时候
置位和清除因此可通过软件产生和阻止定时器中断
定时器控制寄存器TCON 可位寻址
TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0
TF1 定时器1 溢出中断标志响应中断后由处理器清零
TR1 定时器1 控制位置位时定时器1 工作复位时定时器1 停止工作
TF0 定时器0 溢出标志位定时器0 溢出时置位处理器响应中断后清除该位
TR0 定时器0 控制位置位时定时器0 工作复位时定时器0 停止工作
IE1 外部中断1 触发标志位当检测到P3.3 有从高到低的跳变电平时置位处
理器响应中断后由硬件清除该位
IT1 中断1 触发方式控制位置位时为跳变触发复位时为低电平触发
IE0 外部中断1 触发标志位当检测到P3.3 有从高到低的跳变电平时置位处
理器响应中断后由硬件清除该位
IT0 中断1 触发方式控制位置位时为跳变触发复位时为低电平触发
表A-7
定时器的工作方式由特殊功能寄存器TMOD 来设置通过改变TMOD 软件可控制两个
定时器的工作方式和时钟源是I/0 口的触发电平还是处理器的时钟脉冲TMOD 的高四
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
11
位控制定时器1 低四位控制定时器0 TMOD 的结构如下
定时器控制寄存器TMOD-不可位寻址
GATE C/T M1 M0 GATE C/T M1 M0
定时器1 定时器0
GATE 当GATE 置位时定时器仅当TR=1 并且INT=1 时才工作如果GATE=0
置位TR 定时器就开始工作
C/T 定时器方式选择如果C/T=1 定时器以计数方式工作C/T=0 时以
定时方式工作
M1 模式选择位高位
M0 模式选择位低位
表A-8
可通过C/T 位的设置来选择定时器的时钟源C/T=1 定时器以计数方式工作对I/0
引脚脉冲计数C/T=0 时以定时方式工作对内部时钟脉冲计数当定时器用来对内
部时钟脉冲计数时可通过硬件或软件来控制GATE=0 为软件控制置位TR 定时器就开
始工作GATE=1 为硬件控制当TR=1 并且INT=1 时定时器才工作当INT 脚给出低电平
时定时器将停止工作这在测量INT 脚的脉冲宽度时十分有用当然INT 脚不作为外
部中断使用
7.1 定时器工作方式0 和方式1
定时器通过软件控制有四种工作方式方式0 为十三位定时/计数器方式定时器溢出
时置位TF0 或TF1 并产生中断方式1 将以十六位定时/计数器方式工作除此之外和方
式0 一样
7.2 定时器工作方式2
方式2 为8 位自动重装工作方式定时器的低8 位TL0 或TL1 用来计数高8 位TH0
或TH1 用来存放重装数值当定时器溢出时TH 中的数值被装入TL 中定时器0 和定时
器1 在方式2 时是同样的定时器1 常用此方式来产生波特率
7.3 定时器工作方式3
方式3 时定时器0 成为两个8 位定时/计数器TH0 和TL0 TH0 对应于TMOD 中定
时器0 的控制位而TL0 占据了TMOD 中定时器1 的控制位这样定时器1 将不能产生溢出
中断了但可用于其它不需产生中断的场合如作为波特率发生器或作为定时计数器被软
件查询当系统需要用定时器1 来产生波特率而又同时需要两个定时/计数器时这种工
作方式十分有用当定时器1 设置为工作方式3 时将停止工作
7.4 定时器2
51 系列单片机如8052 第三个定时/计数器定时器2 他的控制位在特殊功能寄存器
T2CON 中结构如下
定时器2 控制寄存器可位寻址
TF2 EXF2 RCLK TCLK EXEN2 TR2 C/T2 CP/RL2
TF2 定时器2 溢出标志位定时器2 溢出时将置位当TCLK 或RCLK 为1 时
将不会置位
EXF2 定时器2 外部标志当EXEN2=1 并在引脚T2EX 检测到负跳变时置位
如果定时器2 中断被允许将产生中断
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
12
RCLK 接收时钟标志当串行口以方式1 或3 工作时将使用定时器2 的溢出
率作为串行口接收时钟频率
TCLK 发送时钟标志位当串行口以方式1 或3 工作时将使用定时器2
的溢出率作为串行口接收时钟频率
EXEN2 定时器2 外部允许标志当EXEN2=1 时在T2EX 引脚出现负跳变时将造
成定时器2 捕捉或重装并置位EXF2 产生中断
TR2 定时器运行控制位置位时定时器2 将开始工作否则定时器2 停
止工作
C/T2 定时器计数方式选择位如果C/T2=1 定时器2 将作为外部事件计数器
否则对内部时钟脉冲计数
CP/RL2 捕捉/重装标志位当EXEN2=1 时如果CP/RL2=1 T2EX 引脚的负跳变
将造成捕捉如果CP/RL2=0 T2EX 引脚的负跳变将造成重装
通过由软件设置T2CON 可使定时/计数器以三种基本工作方式之一工作第一种为捕
捉方式设置为捕捉方式时和定时器0 或定时器1 一样以16 位方式工作这种方式通过
复位EXEN2 来选择当置位EXEN2 时如果T2EX 有负跳变电平将把当前的数锁存在RCAP2H
和RCAP2L 中这个事件可用来产生中断
第二种工作方式为自动重装方式其中包含了两个子功能由EXEN2 来选择当EXEN2
复位时16 位定时器溢出将触发一个中断并将RCAP2H 和RCAP2L 中的数装入定时器中当
EXEN2 置位时除上述功能外T2EX 引脚的负跳变将产生一次重装操作
最后一种方式用来产生串行口通讯所需的波特率这通过同时或分别置位RCLK 和TCLK
来实现在这种方式中每个机器周期都将使定时器加1 而不像定时器0 和1 那样需
要12 个机器周期这使得串行通讯的波特率更高
8 内置UART
8051 有一个可通过软件控制的内置全双工串行通讯接口由寄存器SCON 来进行设
置可选择通讯模式允许接收检查状态位SCON 的结构如下
串行控制寄存器SCON -可位寻址
SM0 SM1 SM2 REN TB8 RB8 TI RI
SM0 串行模式选择
SM1 串行模式选择
SM2 多机通讯允许位当模式0 时此位应该为0 模式1 时当接收到停止位时
该位将置位模式2 或模式3 时当接收的第9 位数据为1 时将置位
REN 串行接收允许位
TB8 在模式2 和模式3 中将被发送数据的第9 位
RB8 在模式0 中该位不起作用在模式1 中该位为接收数据的停止位在模
式2 和模式3 中为接收数据的第9 位
TI 串行中断标志位由软件清零
RI 接收中断标志位有软件清零
表A-10
UART 有一个接收数据缓冲区当上一个字节还没被处理下一个数据仍然可以缓冲区
接收进来但如果接收完这个字节如果上个字节还没被处理上个字节将被覆盖因此
软件必须在此之前处理数据当连续发送字节时也是如此
8051 支持10 位和11 位数据模式11 数据模式用来进行多机通讯并支持高速8 位移
位寄存器模式模式1 和模式3 中波特率可变
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
13
8.1 UART 模式0
模式0 时UART 作为一个8 位的移位寄存器使用波特率为fosc/12 数据由RXD 从
低位开始收发TXD 用来发送同步移位脉冲因此方式0 不支持全双工这种方式可用
来和像某些具有8 位串行口的EEPROM 之类的器件通讯
当向SBUF 写入字节时开始发送数据数据发送完毕时TI 位将置位置位REN 时
将开始接收数据接收完8 位数据时RI 位将置位
8.2 UART 模式1
工作于模式1 时传输的是10 位1 个起始位8 个数据位1 个停止位这种方式
可和包括PC 机在内的很多器件进行通讯这种方式中波特率是可调的而用来产生波特率
的定时器的中断应该被禁止PCON 的SMOD 位为1 时可使波特率翻倍
TI 和RI 在发送和接收停止位的中间时刻被置位这使软件可以响应中断并装入新的
数据数据处理时间取决于波特率和晶振频率
如果用定时器1 来产生波特率应通过下式来计算TH1 的装入值
TH1=256- K*OscFreq / 384*BaudRate
K=1 if SMOD=0
K=2 if SMOD=1
重装值要小于256 非整数的重装值必须和下一个整数非常接近通常产生的波特率
都能使系统正常的工作这点需要开发者把握
这样如果你使用9.216M 晶振想产生9600 的波特率第一步设K=1 分子为9216000
分母为3686400 相除结果为2.5 不是整数设K=2 分子为18432000 分母为3686400
相除结果为5 可得TH1=251 或0FBH
如果用8052 的定时器2 产生波特率RCAP2H 和RCAP2L 的重装值也需要经过计算根
据需要的波特率用下式计算
[RCAP2H RCAP2L]=65536-OsFreq/ 32*BaudRate
假设你的系统使用9.216M 晶振你想产生9600 的波特率用上式产生的结果必须是
正的而且接近整数最后得到结果30 重装值为65506 或FFE2H
8.3 UART 模式2
模式2 的数据以11 位方式发送1 位起始位8 位数据位第九位1 位停止位发
送数据时第九位为SCON 中的TB8 接收数据的第九位保存在RB8 中第九位一般用来多
机通信仅在第九位为1 时单片机才接收数据多机通信用SCON 的SM2 来控制当SM2
置位时仅当数据的第九位为1 时才引发通讯中断当SM2 为0 时只要接收完11 位就产
生一次中断
第九位可在多机通讯中避免不必要的中断在传送地址和命令时第九位置位串行
总线上的所有处理器都产生一个中断处理器将决定是否继续接收下面的数据如果继续
接收数据就清零SM2 否则SM2 置位以后的数据流将不会使他产生中断
SMOD=O 时模式2 的波特率为1/64Osc SMOD=1 时波特率为1/32Osc 因此使用
模式2 当晶振频率为11.059M 时将有高达345K 的波特率模式3 和模式2 的差别在于
可变的波特率
9 其它功能
很多51 系列的单片机有了许多新增加的功能使之更适合于嵌入式应用51 系列的
其它功能如下
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
14
9.1 I2C
I2C 是一种新的芯片间的通讯方式由PHILIPS 开发和推广I2C 通讯采用两条线进行
通讯一条数据线一条时钟线可进行多器件通讯总线上的每个器件都有自己的地址
数据传送是双向的总线支持多主机8051 上I2C 总线的接口为P0 端口的两根线有专门
的特殊功能寄存器来控制总线的工作和执行传输协议
9.2 A/D 转换
并不是所有51 系列单片机都带A/D 转换但A/D 转换的使用非常普遍A/D 转换一般
由寄存器ADCON 来控制用户通过ADCON 来选择A/D 转换的通道开始转换检查转换状
态一般A/D 转换的过程不多于40 个指令周期转换完成后产生中断中断程序将处理转
换结果A/D 转换需要处理器一直处于工作状态转换结果保存于特殊功能寄存器中
9.3 看门狗
大多数51 系列单片机都有看门狗当看门狗没有被定时清零时将引起复位这可防
止程序跑飞设计者必须清楚看门狗的溢出时间以决定在合适的时候清看门狗清看门
狗也不能太过频繁否则会造成资源浪费
51 系列有专门的看门狗定时器对系统频率进行分频计数定时器溢出时将引起复
位看门狗可设定溢出率也可单独用来作为定时器使用
10 设计
51 系列单片机有着各种具有不同的外设功能的成员可适用于各方面的应用选择一
款合适的单片机是十分重要的考虑到电路板空间和成本应使外围部件尽可能少51 系
列最多512 字节的RAM 和32K 字节的EPROM 有时只要使用系统内置的RAM 和EPROM 就
可以了应充分利用这些部件不再需要外接EPROM 和RAM 这样就省下了I/0 口可用
来和其它器件相连当不需要扩展I/0 口并且程序代码较短时使用28 脚的51 单片机可
节省不少空间但很多应用需要更多的RAM 和EPROM 空间这时就要用外围器件SRAM EPROM
等许多外围器件能被51 系列的内部功能和相应的软件代替这将在以后讨论
经常要考虑系统的功耗问题如果处理器有很多工作要做而不能进入低功耗和空闲
模式应选择3.6V 的工作电压以降低功耗如果有足够的空闲时间的话可以考虑关闭晶
振降低功耗
设计者必须仔细选择晶振频率确保标准的通讯波特率1200 4800 9600 19.2K
等你不妨先列出可供选择的晶振所能产生的波特率然后根据需要的波特率和系统要求
选择晶振有时也不必过分考虑晶振问题因为可以定制晶振当晶振频率超过20M 时
必须确保总线上的其它器件能够在这种频率下工作一般EPROM SRAM 高速CMOS 版的
锁存器都支持51 的工作频率当工作频率增加时功耗也会增加这点在使用电池作为电
源的系统中应充分考虑
11 实现
当选择好单片机和外围器件后下一步就是设计和分配系统I/O 地址代码段在从地
址零开始的连续空间内外部数据存储空间地址一般和RAM 和器件地址相连RAM 一般在
从地址0000H 或8000H 开始的连续空间内一种比较有用的处理方法是SRAM 的地址也从
0000H 开始用A15 使能RAM RAM 的0E 和WE 线分别和单片机的RD 和WR 线相连这种方
法可使RAM 区超过32K 这足够嵌入式系统使用此外32K 的地址也可分配给I/O 器件
大多数情况下I/O 器件是比较少的所以地址线的高位可接解码器工作给外围器件提
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
15
供使能信号一个为系统I/O 分配地址的例子如A-2-8051 总线I/O 所示可以看到通
过减少地址解码器的数量简化了硬件设计因为在I/O 操作中不用装载DPTR 的低8 位使
软件设计也得到简化
图A-2-8051 总线I/O
对输入输出锁存器的寻址如下例
列表A-6
MOV DPTR #09000H 设置指针
MOVX A @DPTR
MOV DPH #080H
MOVX @DPTR A
可以看到因为电路设计连续的I/O 操作将被简化软件不需要考虑数据指针的低
字节第一条指令也可用MOV DPH #090H 代替
12 结论
我希望上面所讲的关于8051 的基本知识能给你一些启发但这不能代替8051 厂商提
供的数据书因为每款芯片都有其自身的特点下面我们将开始讨论8051 的软件设计
包括用C 进行软件开发
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
16
第二章用C 对8051 编程
1 为什么要用高级语言
当设计一个小的嵌入式系统时一般我们都用汇编语言在很多工程中这是一个很
好的方法因为代码一般都不超过8K 而且都比较简单如果硬件工程师要同时设计软
件和硬件经常会采用汇编语言来做程序我的经验告述我硬件工程师一般不熟系像C
一类的高级语言
使用汇编的麻烦在于它的可读性和可维护性特别当程序没有很好的标注的时候代
码的可重用性也比较低如果使用C 的话可以很好的解决这些问题
用C 编写的程序因为C 语言很好的结构性和模块化更容易阅读和维护而且由于
模块化用C 语言编写的程序有很好的可移植性功能化的代码能够很方便的从一个工程
移植到另一个工程从而减少了开发时间
用C 编写程序比汇编更符合人们的思考习惯开发者可以更专心的考虑算法而不是考
虑一些细节问题这样就减少了开发和调试的时间
使用像C 这样的语言程序员不必十分熟系处理器的运算过程这意味着对新的处理
器也能很快上手不必知道处理器的具体内部结构使得用C 编写的程序比汇编程序有更
好的可移植性很多处理器支持C 编译器
所有这些并不说明汇编语言就没了立足之地很多系统特别是实时时钟系统都是用
C 和汇编语言联合编程对时钟要求很严格时使用汇编语言成了唯一的方法除此之外
根据我的经验包括硬件接口的操作都应该用C 来编程C 的特点就是可以使你尽量少
地对硬件进行操作是一种功能性和结构性很强的语言
2 C 语言的一些要点
这里不是教你如何使用C 语言关于C 语言的书有很多像Kernighan 和Ritchie 所
著的C 编程语言等这本书被认为是C 语言的权威著作Keil 的C51 完全支持C 的标准指
令和很多用来优化8051 指令结构的C 的扩展指令
我们将复习关于C 的一些概念如结构联合和类型定义可能会使一些人伤脑筋
2.1 结构
结构是一种定义类型它允许程序员把一系列变量集中到一个单元中当某些变量相
关的时候使用这种类型是很方便的例如你用一系列变量来描述一天的时间你需要定
义时分秒三个变量
unsighed char hour,min,sec;
还要定义一个天的变量
unsighed int days;
通过使用结构你可以把这四个变量定义在一起给他们一个共同的名字声明结构
的语法如下
struct time_str{
unsigned char hour,min,sec;
unsigned int days;
}time_of_day;
这告述编译器定义一个类型名为time_str 的结构并定义一个名为time_of_day 的结
构变量变量成员的引用为结构变量名.结构成员
time_of_day.hour=XBYTE[HOURS];
time_of_day.days=XBYTE[DAYS];
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
17
time_of_day.min=time_of_day.sec
curdays=time_of_day.days;
成员变量和其它变量是一样的但前面必须有结构名你可以定义很多结构变量编
译器把他们看成新的变量例如
struct time_str oldtime,newtime;
这样就产生了两个新的结构变量这些变量都是相互独立的就像定义了很多int 类
型的变量一样结构变量可以很容易的复制
oldtime=time_of_day;
这使代码很容易阅读也减少了打字的工作量当然你也可以一句一句的复制
oldtime.hour=newtime.hour;
oldtime.days=newtime.days-1;
在Keil C 和大多数C 编译器中结构被提供了连续的存储空间成员名被用来对结构
内部进行寻址这样结构time_str 被提供了连
续5 个字节的空间空间内的变量顺序和定义时
的变量顺序一样如表0-1:
如果你定义了一个结构类型它就像一个变量
新的变量类型你可建立一个结构数组包含结构
的结构和指向结构的指针
2.2 联合
联合和结构很相似它由相关的变量组成这些变量构成了联合的成员但是这些成
员只能有一个起作用联合的成员变量可以是任何有效类型包括C 语言本身拥有的类型
和用户定义的类型如结构和联合一个定义联合的类型如下
union time_type {
unsigned long secs_in_year;
struct time_str time;
}mytime;
用一个长整形来存放从这年开始到现在的秒数另一个可选项是用time_str 结构来存
储从这年开始到现在的时间
不管联合包含什么可在任何时候引用他的成员如下例
mytime.secs_in_year=JUNEIST;
mytime.time.hour=5;
curdays=mytime.time.days;
像结构一样联合也以连续的空间存储空间大小等于联合中最大的成员所需的空间
Offset Member Bytes
0 Secs_in_year 4
0 Mytime 5
表0-2
因为最大的成员需要5 个字节联合的存储大小为5 个字节当联合的成员为
secs_in_year 时第5 个字节没有使用
联合经常被用来提供同一个数据的不同的表达方式例如假设你有一个长整型变量
用来存放四个寄存器的值如果希望对这些数据有两种表达方法可以在联合中定义一个
长整型变量同时再定义一个字节数组如下例
union status_type{
unsigned char status[4];
Offset Member Bytes
0 hour 1
1 min 1
2 sec 1
3 days 2
表0-1
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
18
unsigned long status_val;
}io_status;
io_status.status_val=0x12345678;
if(i0_status.status[2]0x10){
…
}
2.3 指针
指针是一个包含存储区地址的变量因为指针中包含了变量的地址它可以对它所指
向的变量进行寻址就像在8051 DATA 区中进行寄存器间接寻址和在XDATA 区中用DPTR
进行寻址一样使用指针是非常方便的因为它很容易从一个变量移到下一个变量所以
可以写出对大量变量进行操作的通用程序
指针要定义类型说明指向何种类型的变量假设你用关键字long 定义一个指针C
就把指针所指的地址看成一个长整型变量的基址这并不说明这个指针被强迫指向长整型
的变量而是说明C 把该指针所指的变量看成长整型的下面是一些指针定义的例子
unsigned char *my_ptr,*anther_ptr;
unsigned int *int_ptr;
float *float_ptr;
time_str *time_ptr;
指针可被赋予任何已经定义的变量或存储器的地址
My_ptr=char_val;
Int_ptr=int_array[10];
Time_str=oldtime;
可通过加减来移动指针指向不同的存储区地址在处理数组的时候这一点特别有
用当指针加1 的时候它加上指针所指数据类型的长度
Time_ptr=(time str *) (0x10000L); //指向地址0
Time_ptr++; //指向地址5
指针间可像其它变量那样互相赋值指针所指向的数据也可通过引用指针来赋值
time_ptr=oldtime_ptr //两个指针指向同一地址
*int_ptr=0x4500 //把0X4500 赋给int_ptr 所指的变量
当用指针来引用结构或联合的成员时可用如下方法
time_ptr-days=234;
*time_ptr.hour=12;
还有一个指针用得比较多的场合是链表和树结构假设你想产生一个数据结构可以
进行插入和查询操作一种最简单的方法就是建立一个双向查询树你可以像下面那样定
义树的节点
struct bst_node{
unsigned char name[20]; //存储姓名
struct bst_node *left, right; //分别指向左右子树的指针
};
可通过定位新的变量并把他的地址赋给查询树的左指针或右指针来使双向查询树变
长或缩短有了指针后对树的处理变得简单
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
19
2.4 类型定义
在C 中进行类型定义就是对给定的类型一个新的类型名换句话说就是给类型一个新
的名字例如你想给结构time_str 一个新的名字
typedef struct time_str{
unsigned char hour,min,sec;
unsigned int days;
}time_type;
这样就可以像使用其它变量那样使用time_type 的类型变量
time_type time,*time_ptr,time_array[10];
类型定义也可用来重新命名C 的标准类型
typedef unsigned char UBYTE;
typedef char *strptr;
strptr name;
使用类型定义可使你的代码的可读性加强节省了一些打字的时间但是很多程序员
大量的使用类型定义别人再看你的程序时就十分困难了
3 Keil C 和ANSI C
下面将介绍Keil C 的主要特点和它与ANSI C 的不同之处并给你一些对8051 使用C
的启发
Keil 编译器除了少数一些关键地方外基本类似于ANSI C 差异主要是Keil 可以让
户针对8051 的结构进行程序设计其它差异主要是8051 的一些局限引起的
3.1 数据类型
Keil C 有ANSI C 的所有标准数据类型除此之外为了更加有利的利用8051 的结构
还加入了一些特殊的数据类型下表显示了标准数据类型在8051 中占据的字节数注意
整型和长整型的符号位字节在最低的地址中
除了这些标准数据类型外编译器还支持
一种位数据类型一个位变量存在于内部RAM
的可位寻址区中可像操作其它变量那样对位
变量进行操作而位数组和位指针是违法的
3.2 特殊功能寄存器
特殊功能寄存器用sfr 来定义而sfr16 用来定义16 位的特殊功能寄存器如DPTR
通过名字或地址来引用特殊功能寄存器地址必须高于80H 可位寻址的特殊功能寄存器
的位变量定义用关键字sbit SFR 的定义如列表0-1 所示对于大多数8051 成员Keil
提供了一个包含了所有特殊功能寄存器和他们的位的定义的头文件通过包含头文件可以
很容易的进行新的扩展
列表0-1
sfr SCON=0X98; //定义SCON
sbit SM0=0X9F; //定义SCON 的各位
sbit SM1=0X9E;
sbit SM2=0X9D;
sbit REN=0x9C;
sbit TB8=0X9B;
数据类型大小
char/unsigned char 8 bit
int/unsigned char 16 bit
long/unsigned long 32 bit
float/double 32 bit
generic pointer 24 bit
表0-3
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
20
sbit RB8=0X9A;
sbit TI=0X99;
sbit RI=0X98;
4 存储类型
Keil 允许使用者指定程序变量的存储区这使使用者可以控制存储区的使用编译器
可识别以下存储区
存储区描述
DATA RAM 的低128 个字节可在一个周期内直接寻址
BDATA DATA 区的16 个字节的可位寻址区
IDATA RAM 区的高128 个字节必须采用间接寻址
PDATA 外部存储区的256 个字节通过P0 口的地址对其寻址
使用指令MOVX @Rn,需要两个指令周期
XDATA 外部存储区使用DPTR 寻址
CODE 程序存储区使用DPTR 寻址
4.1 DATA 区
对DATA 区的寻址是最快的所以应该把使用频率高的变量放在DATA 区由于空间有
限必须注意使用DATA 区除了包含程序变量外还包含了堆栈和寄存器组DATA 区的声
明如列表0-2
列表0-2
unsigned char data system_status=0;
unsigned int data unit_id[2];
char data inp_string[16];
float data outp_value;
mytype data new_var;
标准变量和用户自定义变量都可存储在DATA 区中只要不超过DATA 区的范围因为
C51 使用默认的寄存器组来传递参数你至少失去了8 个字节另外要定义足够大的堆
栈空间当你的内部堆栈溢出的时候你的程序会莫名其妙的复位实际原因是8051 系列
微处理器没有硬件报错机制堆栈溢出只能以这种方式表示出来
4.2 BDATA 区
你可以在DATA 区的位寻址区定义变量这个变量就可进行位寻址并且声明位变量
这对状态寄存器来说是十分有用的因为它需要单独的使用变量的每一位不一定要用位
变量名来引用位变量下面是一些在BDATA 段中声明变量和使用位变量的例子
列表0-3
unsigned char bdata status_byte;
unsigned int bdata status_word;
unsigned long bdata status_dword;
sbit stat_flag=status_byte^4;
if(status_word^15){
… }
stat_flag=1;
编译器不允许在BDATA 段中定义float 和double 类型的变量如果你想对浮点数的每
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
21
位寻址可以通过包含float 和long 的联合来实现
列表0-4
typedef union{ //定义联合类型
unsigned long lvalue; //长整型32 位
float fvalue; //浮点数32 位
}bit_float; //联合名
bit_float bdata myfloat; //在BDATA 段中声名联合
sbit float_ld=myfloat^31 //定义位变量名
下面的代码访问状态寄存器的特定位把访问定义在DATA 段中的一个字节和通过位名
和位号访问同样的可位寻址字节的位的代码对比注意对变量位进行寻址产生的汇编代
码比检测定义在DATA 段的状态字节位所产生的汇编代码要好如果你对定义在BDATA 段中
的状态字节中的位采用偏移量进行寻址而不是用先前定义的位变量名时编译后的代码
是错误的下面的例子中use_bitnum_status 的汇编代码比use_byte_status 的代码要
大
列表0-5
1 //定义一个字节宽状态寄存器
2 unsigned char data byte_status=0x43;
3
4 //定义一个可位寻址状态寄存器
5 unsigned char bdata bit_status=0x43;
6 //把bit_status 的第3 位设为位变量
7 sbit status_3=bit_status^3;
8
9 bit use_bit_status(void);
10
11 bit use_bitnum_status(void);
12
13 bit use_byte_status(void);
14
15 void main(void){
16 unsigned char temp=0;
17 if (use_bit_status()){ //如果第3 位置位temp 加1
18 temp++;
19 }
20 if (use_byte_status()){ //如果第3 位置位temp 再加1
21 temp++;
22 }
23 if (use_bitnum_status()){ //如果第3 位置位temp 再加1
24 temp++;
25 }
26 }
27
28 bit use_bit_status(void){
29 return(bit)(status_3);
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
22
30 }
31
32 bit use_bitnum_status(void){
33 return(bit)(bit_status^3);
34 }
35
36 bit use_byte_status(void){
37 return byte _status&0x04;
38 }
目标代码列表
; FUNCTION main (BEGIN)
; SOURCE LINE # 15
; SOURCE LINE # 16
0000 E4 CLR A
0001 F500 R MOV temp,A
; SOURCE LINE # 17
0003 120000 R LCALL use_bit_status
0006 5002 JNC ?C0001
; SOURCE LINE # 18
0008 0500 R INC temp
; SOURCE LINE # 19
000A ?C0001:
; SOURCE LINE # 20
000A 120000 R LCALL use_byte_status
000D 5002 JNC ?C0002
; SOURCE LINE # 21
000F 0500 R INC temp
; SOURCE LINE # 22
0011 ?C0002:
; SOURCE LINE # 23
0011 120000 R LCALL use_bitnum_status
0014 5002 JNC ?C0004
; SOURCE LINE # 24
0016 0500 R INC temp
; SOURCE LINE # 25
; SOURCE LINE # 26
0018 ?C0004:
0018 22 RET
; FUNCTION main (END)
; FUNCTION use_bit_status (BEGIN)
; SOURCE LINE # 28
; SOURCE LINE # 29
0000 A200 R MOV C,status_3
; SOURCE LINE # 30
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
23
0002 ?C0005:
0002 22 RET
; FUNCTION use_bit_status (END)
; FUNCTION use_bitnum_status (BEGIN)
The compiler obtains the desired bit by using the entire byte instead of using
a bit address.
; SOURCE LINE # 32
; SOURCE LINE # 33
0000 E500 R MOV A,bit_status
0002 6403 XRL A,#03H
0004 24FF ADD A,#0FFH
; SOURCE LINE # 34
0006 ?C0006:
0006 22 RET
; FUNCTION use_bitnum_status (END)
; FUNCTION use_byte_status (BEGIN)
; SOURCE LINE # 36
; SOURCE LINE # 37
0000 E500 R MOV A,byte_status
0002 A2E2 MOV C,ACC.2
; SOURCE LINE # 38
0004 ?C0007:
0004 22 RET
; FUNCTION use_byte_status (END)
记住在处理位变量时要使用声明的位变量名而不要使用偏移量
4.3 IDATA 段
IDATA 段也可存放使用比较频繁的变量使用寄存器作为指针进行寻址在寄存器中
设置8 位地址进行间接寻址和外部存储器寻址比较它的指令执行周期和代码长度都
比较短
unsigned char idata system_status=0;
unsigned int idata unit_id[2];
char idata inp_string[16];
float idata outp_value;
4.4 PDATA 和XDATA 段
在这两个段声明变量和在其它段的语法是一样的PDATA 段只有256 个字节而XDATA
段可达65536 个字节下面是一些例子
unsigned char xdata system_status=0;
unsigned int pdata unit_id[2];
char xdata inp_string[16];
float pdata outp_value;
对PDATA 和XDATA 的操作是相似的对PDATA 段寻址比对XDATA 段寻址要快因为
对PDATA 段寻址只需要装入8 位地址而对XDATA 段寻址需装入16 位地址所以尽量把外
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
24
部数据存储在PDATA 段中对PDATA 和XDATA 寻址要使用MOVX 指令需要两个处理周期
列表0-6
1 #include reg51.h
2
3 unisgned char pdata inp_reg1;
4
5 unsigned char xdata inp_reg2;
6
7 void main(void){
8 inp_reg1=P1;
9 inp_reg2=P3;
10 }
产生的目标代码列表
; FUNCTION main (BEGIN)
; SOURCE LINE # 7
; SOURCE LINE # 8
注意’inp_reg1=P1’ 需要4个指令周期
0000 7800 R MOV R0,#inp_reg1
0002 E590 MOV A,P1
0004 F2 MOVX @R0,A
; SOURCE LINE # 9
注意’inp_reg2=P3’ 需要5个指令周期
0005 900000 R MOV DPTR,#inp_reg2
0008 E5B0 MOV A,P3
000A F0 MOVX @DPTR,A
; SOURCE LINE # 10
000B 22 RET
; FUNCTION main (END)
经常外部地址段中除了包含存储器地址外还包含I/O 器件的地址对外部器件寻址
可通过指针或C51 提供的宏我建议使用宏对外部器件进行寻址因为这样更有可读性
宏定义使得存储段看上去像char 和int 类型的数组下面是一些绝对寄存器寻址的例子
列表0-7
inp_byte=XBYTE[0x8500]; // 从地址8500H读一个字节
inp_word=XWORD[0x4000]; // 从地址4000H读一个字和2001H
c=*((char xdata *) 0x0000); // 从地址0000读一个字节
XBYTE[0x7500]=out_val; // 写一个字节到7500H
可对除BDATA 和BIT 段之外的其它数据段采用以上方法寻址通过包含头文件absacc.h
来进行绝对地址访问
4.5 CODE 段
代码段的数据是不可改变的8051 的代码段不可重写一般代码段中可存放数据表
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
25
跳转向量和状态表对CODE 段的访问和对XDATA 段的访问的时间是一样的代码段中的对
象在编译的时候初始化否则你就得不到你想要的值下面是代码段的声明例子
unsigned int code unit_id[2]=1234;
unsigned char
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15
};
5 指针
C51 提供一个3 字节的通用存储器指针通用指针的头一个字节表明指针所指的存储
区空间另外两个字节存储16 位偏移量对于
DATA IDATA 和PDATA 段只需要8 位偏移量
Keil 允许使用者规定指针指向的存储段
这种指针叫具体指针使用具体指针的好处是
节省了存储空间编译器不用为存储器选择和
决定正确的存储器操作指令产生代码这样就
使代码更加简短但你必须保证指针不指向你
所声明的存储区以外的地方否则会产生错误
而且很难调试
下面的例子反映出使用具体指针比使用通用指针更加高效使用通用指针的第一个循
环需要378 个处理周期使用具体指针只需要151 个处理周期
列表0-8
1 #include absacc.h
2
3 char *generic_ptr;
4
5 char data *xd_ptr;
6
7 char mystring[]=Test output;
8
9 main() {
10 1 generic_ptr=mystring;
11 1 while (*generic_ptr) {
12 2 XBYTE[0x0000]=*generic_ptr;
13 2 generic_ptr++;
14 2 }
15 1
16 1 xd_ptr=mystring;
17 1 while (*xd_ptr) {
18 2 XBYTE[0x0000]=*xd_ptr;
19 2 xd_ptr++;
20 2 }
21 1 }
编译产生的汇编代码
指针类型大小
通用指针3 字节
XDATA 指针2 字节
CODE 指针2 字节
IDATA 指针1 字节
DATA 指针1 字节
PDATA 指针1 字节
表0-5
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
26
; FUNCTION main (BEGIN)
; SOURCE LINE # 9
; SOURCE LINE # 10
0000 750004 R MOV generic_ptr,#04H
0003 750000 R MOV generic_ptr+01H,#HIGH mystring
0006 750000 R MOV generic_ptr+02H,#LOW mystring
0009 ?C0001:
; SOURCE LINE # 11
0009 AB00 R MOV R3,generic_ptr
000B AA00 R MOV R2,generic_ptr+01H
000D A900 R MOV R1,generic_ptr+02H
000F 120000 E LCALL ?C_CLDPTR
0012 FF MOV R7,A
0013 6011 JZ ?C0002
; SOURCE LINE # 12
0015 900000 MOV DPTR,#00H
0018 F0 MOVX @DPTR,A
; SOURCE LINE # 13
0019 7401 MOV A,#01H
001B 2500 R ADD A,generic_ptr+02H
001D F500 R MOV generic_ptr+02H,A
001F E4 CLR A
0020 3500 R ADDC A,generic_ptr+01H
0022 F500 R MOV generic_ptr+01H,A
; SOURCE LINE # 14
0024 80E3 SJMP ?C0001
0026 ?C0002:
; SOURCE LINE # 16
0026 750000 R MOV xd_ptr,#LOW mystring
0029 ?C0003:
; SOURCE LINE # 17
0029 A800 R MOV R0,xd_ptr
002B E6 MOV A,@R0
002C FF MOV R7,A
002D 6008 JZ ?C0005
; SOURCE LINE # 18
002F 900000 MOV DPTR,#00H
0032 F0 MOVX @DPTR,A
; SOURCE LINE # 19
0033 0500 R INC xd_ptr
; SOURCE LINE # 20
0035 80F2 SJMP ?C0003
; SOURCE LINE # 21
0037 ?C0005:
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
27
0037 22 RET
; FUNCTION main (END)
由于使用具体指针能够节省不少时间所以我们一般都不使用通用指针
6 中断服务
8051 的中断系统十分重要,C51 使你能够用C 来声明中断和编写中断服务程序(当然你
也可以用汇编来写) 中断过程通过使用interrupt 关键字和中断号(0 到31)来实现.中断
号告述编译器中断程序的入口地址中断号对应着IE 寄存器中的使能位换句话说IE
寄存器中的0 位对应着外部中断0 相应的外部中断0 的中断号是0 表0-6 反映了这种关
系
一个中断过程并不一定带上所有参数可
以没有返回值有了这些限制编译器不须要
担心寄存器组参数的使用和对累加器状态寄
存器B 寄存器数据指针和默认的寄存器的
保护只要他们在中断程序中被用到编译的
时候会把他们入栈在中断程序结束时将他们
恢复中断程序的入口地址被编译器放在中断
向量中C51 支持所有5 个8051/8052 标准中
断从0 到4 和在8051 系列中多达27 个中断源一个中断服务程序的例子如下
列表0-9
1 #include reg51.h
2 #include stdio.h
3
4 #define RELOADVALH 0x3C
5 #define RELOADVALL 0xB0
6
7 extern unsigned int tick_count;
8
9 void timer0(void) interrupt 1 {
10 1 TR0=0; // 停止定时器0
11 1 TH0=RELOADVALH; // 50ms后溢出
12 1 TL0=RELOADVALL;
13 1 TR0=1; // 启动T0
14 1 tick_count++; // 时间计数器加1
15 1 printf(tick_count=%05u\n, tick_count);
16 1 }
编译后产生的汇编代码
; FUNCTION timer0 (BEGIN)
0000 C0E0 PUSH ACC
0002 C0F0 PUSH B
0004 C083 PUSH DPH
0006 C082 PUSH DPL
IE 寄存器中的使能
位和C 中的中断号
中断源
0 外部中断0
1 定时器0 溢出
2 外部中断1
3 定时器1 溢出
4 串行口中断
5 定时器2 溢出
表0-6
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
28
0008 C0D0 PUSH PSW
000A C000 PUSH AR0
000C C001 PUSH AR1
000E C002 PUSH AR2
0010 C003 PUSH AR3
0012 C004 PUSH AR4
0014 C005 PUSH AR5
0016 C006 PUSH AR6
0018 C007 PUSH AR7
; SOURCE LINE # 9
; SOURCE LINE # 10
001A C28C CLR TR0
; SOURCE LINE # 11
001C 758C3C MOV TH0,#03CH
; SOURCE LINE # 12
001F 758AB0 MOV TL0,#0B0H
; SOURCE LINE # 13
0022 D28C SETB TR0
; SOURCE LINE # 14
0024 900000 E MOV DPTR,#tick_count+01H
0027 E0 MOVX A,@DPTR
0028 04 INC A
0029 F0 MOVX @DPTR,A
002A 7006 JNZ ?C0002
002C 900000 E MOV DPTR,#tick_count
002F E0 MOVX A,@DPTR
0030 04 INC A
0031 F0 MOVX @DPTR,A
0032 ?C0002:
; SOURCE LINE # 15
0032 7B05 MOV R3,#05H
0034 7A00 R MOV R2,#HIGH ?SC_0
0036 7900 R MOV R1,#LOW ?SC_0
0038 900000 E MOV DPTR,#tick_count
003B E0 MOVX A,@DPTR
003C FF MOV R7,A
003D A3 INC DPTR
003E E0 MOVX A,@DPTR
003F 900000 E MOV DPTR,#?_printf?BYTE+03H
0042 CF XCH A,R7
0043 F0 MOVX @DPTR,A
0044 A3 INC DPTR
0045 EF MOV A,R7
0046 F0 MOVX @DPTR,A
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
29
0047 120000 E LCALL _printf
; SOURCE LINE # 16
004A D007 POP AR7
004C D006 POP AR6
004E D005 POP AR5
0050 D004 POP AR4
0052 D003 POP AR3
0054 D002 POP AR2
0056 D001 POP AR1
0058 D000 POP AR0
005A D0D0 POP PSW
005C D082 POP DPL
005E D083 POP DPH
0060 D0F0 POP B
0062 D0E0 POP ACC
0064 32 RETI
; FUNCTION timer0 (END)
在上面的例子中调用printf 函数使得编译器把所有的工作寄存器入栈因为调用本
身和非再入函数printf 的处理过程中要使用到这些寄存器如果在C 源程序中把调用语句
去掉的话编译出来的代码就小得多了
列表0-10
1 #include reg51.h
2
3 #define RELOADVALH 0x3C
4 #define RELOADVALL 0xB0
5
6 extern unsigned int tick_count;
7
8 void timer0(void) interrupt 1 using 0 {
9 1 TR0=0; // 停止定时器0
10 1 TH0=RELOADVALH; // 设定溢出时间50ms
11 1 TL0=RELOADVALL;
12 1 TR0=1; // 启动T0
13 1 tick_count++; // 时间计数器加1
14 1 }
编译后产生的汇编代码
; FUNCTION timer0 (BEGIN)
0000 C0E0 PUSH ACC
Push and pop of register bank 0 and the B register is eliminated because printf was
usingthe registers for parameters and using B internally.
0002 C083 PUSH DPH
0004 C082 PUSH DPL
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
30
; SOURCE LINE # 8
; SOURCE LINE # 9
0006 C28C CLR TR0
; SOURCE LINE # 10
0008 758C3C MOV TH0,#03CH
; SOURCE LINE # 11
000B 758AB0 MOV TL0,#0B0H
; SOURCE LINE # 12
000E D28C SETB TR0
; SOURCE LINE # 13
0010 900000 E MOV DPTR,#tick_count+01H
0013 E0 MOVX A,@DPTR
0014 04 INC A
0015 F0 MOVX @DPTR,A
0016 7006 JNZ ?C0002
0018 900000 E MOV DPTR,#tick_count
001B E0 MOVX A,@DPTR
001C 04 INC A
001D F0 MOVX @DPTR,A
001E ?C0002:
; SOURCE LINE # 14
001E D082 POP DPL
0020 D083 POP DPH
0022 D0E0 POP ACC
0024 32 RETI
; FUNCTION timer0 (END)
6.1 指定中断服务程序使用的寄存器组
当指定中断程序的工作寄存器组时保护工作寄存器的工作就可以被省略使用关键
字using 后跟一个0 到3 的数对应着4 组工作寄存器当指定工作寄存器组的时候默
认的工作寄存器组就不会被推入堆栈这将节省32 个处理周期因为入栈和出栈都需要2
个处理周期为中断程序指定工作寄存器组的缺点是所有被中断调用的过程都必须使用
同一个寄存器组否则参数传递会发生错误下面的例子给出了定时器0 的中断服务程序
但我已经告述编译器使用寄存器组0
列表0-11
1 #include reg51.h
2 #include stdio.h
3
4 #define RELOADVALH 0x3C
5 #define RELOADVALL 0xB0
6
7 extern unsigned int tick_count;
8
9 void timer0(void) interrupt 1 using 0 {
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
31
10 1 TR0=0; // 停止定时器0
11 1 TH0=RELOADVALH; // 设置溢出时间为50ms
12 1 TL0=RELOADVALL;
13 1 TR0=1; // 启动T0
14 1 tick_count++; // 时间计数器加1
15 1 printf(tick_count=%05u\n, tick_count);
16 1 }
编译后产生的汇编代码
; FUNCTION timer0 (BEGIN)
0000 C0E0 PUSH ACC
0002 C0F0 PUSH B
Push and pop of register bank 0 has been eliminated because the compiler assumes
that thisISR ’owns’ RB0.
0004 C083 PUSH DPH
0006 C082 PUSH DPL
0008 C0D0 PUSH PSW
000A 75D000 MOV PSW,#00H
; SOURCE LINE # 9
; SOURCE LINE # 10
000D C28C CLR TR0
; SOURCE LINE # 11
000F 758C3C MOV TH0,#03CH
; SOURCE LINE # 12
0012 758AB0 MOV TL0,#0B0H
; SOURCE LINE # 13
0015 D28C SETB TR0
; SOURCE LINE # 14
0017 900000 E MOV DPTR,#tick_count+01H
001A E0 MOVX A,@DPTR
001B 04 INC A
001C F0 MOVX @DPTR,A
001D 7006 JNZ ?C0002
001F 900000 E MOV DPTR,#tick_count
0022 E0 MOVX A,@DPTR
0023 04 INC A
0024 F0 MOVX @DPTR,A
0025 ?C0002:
; SOURCE LINE # 15
0025 7B05 MOV R3,#05H
0027 7A00 R MOV R2,#HIGH ?SC_0
0029 7900 R MOV R1,#LOW ?SC_0
002B 900000 E MOV DPTR,#tick_count
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
32
002E E0 MOVX A,@DPTR
002F FF MOV R7,A
0030 A3 INC DPTR
0031 E0 MOVX A,@DPTR
0032 900000 E MOV DPTR,#?_printf?BYTE+03H
0035 CF XCH A,R7
0036 F0 MOVX @DPTR,A
0037 A3 INC DPTR
0038 EF MOV A,R7
0039 F0 MOVX @DPTR,A
003A 120000 E LCALL _printf
; SOURCE LINE # 16
003D D0D0 POP PSW
003F D082 POP DPL
0041 D083 POP DPH
0043 D0F0 POP B
0045 D0E0 POP ACC
0047 32 RETI
; FUNCTION timer0 (END)
7 再入函数
因为8051 内部堆栈空间的限制C51 没有像大系统那样使用调用堆栈一般C 语言
中调用过程时会把过程的参数和过程中使用的局部变量入栈为了提高效率C51 没有
提供这种堆栈而是提供一种压缩栈每个过程被给定一个空间用于存放局部变量过程
中的每个变量都存放在这个空间的固定位置当递归调用这个过程时会导致变量被覆盖
在某些实时应用中非再入函数是不可取的因为函数调用时可能会被中断程序中
断而在中断程序中可能再次调用这个函数所以C51 允许将函数定义成再入函数再
入函数可被递归调用和多重调用而不用担心变量被覆盖因为每次函数调用时的局部变量
都会被单独保存因为这些堆栈是模拟的再入函数一般都比较大运行起来也比较慢
模拟栈不允许传递bit 类型的变量也不能定义局部位标量
8 使用Keil C 时应做的和应该避免的
Keil 编译器能从你的C 程序源代码中产生高度优化的代码但你可以帮助编译器产生
更好的代码下面将讨论这方面的一些问题
8.1 采用短变量
一个提高代码效率的最基本的方式就是减小变量的长度使用C 编程时我们都习惯
于对循环控制变量使用int 类型这对8 位的单片机来说是一种极大的浪费你应该仔
细考虑你所声明的变量值可能的范围然后选择合适的变量类型很明显经常使用的变
量应该是unsigned char 只占用一个字节
8.2 使用无符号类型
为什么要使用无符号类型呢原因是8051 不支持符号运算程序中也不要使用含有带
符号变量的外部代码除了根据变量长度来选择变量类型自外你还要考虑是否变量是否
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
33
会用于负数的场合如果你的程序中可以不需要负数那么把变量都定义成无符号类型的
8.3 避免使用浮点指针
在8 位操作系统上使用32 位浮点数是得不偿失的你可以这样做但会浪费大量的时
间所以当你要在系统中使用浮点数的时候你要问问自己这是否一定需要可以通过提
高数值数量级和使用整型运算来消除浮点指针处理ints 和longs 比处理doubles 和floats
要方便得多你的代码执行起来会更快也不用连接处理浮点指针的模块如果你一定要
采用浮点指针的话你应该采用西门子80517 和达拉斯半导体公司的80320 这些已经对数
处理进行过优化的单片机
如果你不得不在你的代码中加入浮点指针那么你的代码长度会增加程序执行速
度也会比较慢如果浮点指针运算能被中断的话你必须确保要么中断中不会使用浮点指
针运算要么在中断程序前使用fpsave 指令把中断指针推入堆栈在中断程序执行后使
用fprestore 指令把指针恢复还有一种方法是当你要使用像sin()这样的浮点运算程
序时,禁止使用中断在运算程序执行完之后再使能它
列表0-12
#include math.h
void timer0_isr(void) interrupt 1 {
struct FPBUF fpstate;
... // 初始化代码或
// 非浮点指针代码
fpsave(fpstate); // 保留浮点指针系统
... // 中断服务程序代码, 包括所有
// 浮点指针代码
fprestore(fpstate); // 复位浮点指针
// 系统状态
... // 非浮点指针中断
// 服务程序代码
}
float my_sin(float arg) {
float retval;
bit old_ea;
old_ea=EA; // 保留当前中断状态
EA=0; // 关闭中断
retval=sin(arg); // 调用浮点指针运算程序
EA=old_ea; // 恢复中断状态
return retval;
}
你还要决定所需要的最大精度一旦你计算出你所需要的浮点运算的最多的位数应
该通知编译器知道它将把处理的复杂度控制在最低的范围内
8.4 使用位变量
对于某些标志位应使用位变量而不是unsigned char 这将节省你的内存你不用
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
34
多浪费7 位存储区而且位变量在RAM 中访问他们只需要一个处理周期
8.5 用局部变量代替全局变量
把变量定义成局部变量比全局变量更有效率编译器为局部变量在内部存储区中分配
存储空间而为全局变量在外部存储区中分配存储空间这会降低你的访问速度另一个
避免使用全局变量的原因是你必须在你系统的处理过程中调节使用全局变量因为在中断
系统和多任务系统中不止一个过程会使用全局变量
8.6 为变量分配内部存储区
局部变量和全局变量可被定义在你想要的存储区中根据先前的讨论,当你把经常使用
的变量放在内部RAM 中时,可使你的程序的速度得到提高,除此之外,你还缩短了你的代码,
因为外部存储区寻址的指令相对要麻烦一些考虑到存储速度按下面的顺序使用存储器
DATA IDATA PDATA XDATA 当然你要记得留出足够的堆栈空间
8.7 使用特定指针
当你在程序中使用指针时你应指定指针的类型确定它们指向哪个区域如XDATA 或
CODE 区这样你的代码会更加紧凑因为编译器不必去确定指针所指向的存储区因为你
已经进行了说明
8.8 使用调令
对于一些简单的操作如变量循环位移编译器提供了一些调令供用户使用许多调
令直接对应着汇编指令而另外一些比较复杂并兼容ANSI 所有这些调令都是再入函数
你可在任何地方安全的调用他们
和单字节循环位移指令RL A 和RR A 相对应的调令是_crol_ 循环左移和_cror_(循
环右移) 如果你想对int 或long 类型的变量进行循环位移调令将更加复杂而且执行的
时间会更长对于int 类型调令为_irol_,_iror_ ,对于long 类型调令为_lrol_,_lror_
在C 中也提供了像汇编中JBC 指令那样的调令_testbit_ 如果参数位置位他将返回1
否则将返回0 这条调令在检查标志位时十分有用而且使C 的代码更具有可读性调令
将直接转换成JBC 指令
列表0-13
#include instrins.h
void serial_intr(void) interrupt 4 {
if (!_testbit_(TI)) { // 是否是发送中断
P0=1; // 翻转P0.0
_nop_(); // 等待一个指令周期
P0=0;
...
}
if (!_testbit_(RI)) {
test=_cror_(SBUF, 1); // 将SBUF中的数据循环
// 右移一位
...
}
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
35
8.8 使用宏替代函数
对于小段代码像使能某些电路或从锁存器中读取数据你可通过使用宏来替代函数
使得程序有更好的可读性你可把代码定义在宏中这样看上去更像函数编译器在碰到
宏时按照事先定义的代码去替代宏宏的名字应能够描述宏的操作当需要改变宏时
你只要修该宏定义处
列表0-14
#define led_on() {\
led_state=LED_ON; \
XBYTE[LED_CNTRL] = 0x01;}
#define led_off() {\
led_state=LED_OFF; \
XBYTE[LED_CNTRL] = 0x00;}
#define checkvalue(val) \
( (val MINVAL || val MAXVAL) ? 0 : 1 )
宏能够使得访问多层结构和数组更加容易可以用宏来替代程序中经常使用的复杂语
句以减少你打字的工作量且有更好的可读性和可维护性
9 存储器模式
C51 提供了3 种存储器模式来存储变量过程参数和分配再入函数堆栈你应该尽量
使用小存储器模式很少应用系统需要使用其它两种模式像有大的再入函数堆栈系统那
样一般来说如果系统所需要的内存数小于内部RAM 数时都应以小存储模式进行编译
在这种模式下DATA 段是所有内部变量和全局变量的默认存储段所有参数传递都发生在
DATA 段中如果有函数被声明为再入函数编译器会在内部RAM 中为他们分配空间这种
模式的优势就是数据的存取速度很快但只有120 个字节的存储空间供你使用总共有128
个字节但至少有8 个字节被寄存器组使用你还要为程序调用开辟足够的堆栈
如果你的系统有256 字节或更少的外部RAM 你可以使用压缩存储模式这样一来
如果不加说明变量将被分配在PDATA 段中这种模式将扩充你能够使用的RAM 数量对
XDATA 段以外的数据存储仍然是很快的变量的参数传递将在内部RAM 中进行这样存储
速度会比较快对PDATA 段的数据的寻址是通过R0 和R1 进行间接寻址比使用DPTR 要快
一些
在大存储模式中所有变量的默认存储区是XDATA 段Keil C 尽量使用内部寄存器组
进行参数传递在寄存器组中可以传递参数的数量和和压缩存储模式一样再入函数的模
拟栈将在XDATA 中对XDATA 段数据的访问是最慢的所以要仔细考虑变量应存储的位置
使数据的存储速度得到优化
10 混合存储模式
Keil 允许使用混合的存储模式这点在大存储模式中是非常有用的在大存储器模式
下有些过程对数据传递的速度要求很高我就把过程定义在小存储模式寄存器中这使
得编译器为该过程的局部变量在内部RAM 中分配存储空间并保证所有参数都通过内部RAM
进行传递尽管采用混合模式后编译的代码长度不会有很大的改变但这种努力是值得的
就像能在大模式下把过程声明为小模式一样你像能在小模式下把过程声明为压缩模
式或大模式这一般使用在需要大量存储空间的过程上这样过程中的局部变量将被存储
在外部存储区中你也可以通过过程中的变量声明把变量分配在XDATA 段中
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
36
11 运行库
运行库中提供了很多短小精悍的函数你可以很方便的使用他们你自己很难写出更
好的代码了值得注意的是库中有些函数
不是再入函数如果在执行这些函数的时
候被中断而在中断程序中又调用了该函
数将得到意想不到的结果而且这种错
误很难找出来表0-7 列出了非再入型的
库函数使用这些函数时最好禁止使用
这些函数的中断
12 动态存储分配
通过标准C 的功能函数malloc 和free
Keil C 提供了动态存储分配功能对大多
数应用来说应尽可能在编译的时候确定
所需要的内存空间并进行分配但是对
于有些需要使用动态结构如树和链表的应用来说这种方式就不再适用了Keil C 对这种
应用提供了有力的支持
动态分配函数要求用户声明一个字节数组作为堆根据所需要动态内存的大小来决定
数组的长度作为堆被声明的数组在XDATA 区中因为库函数使用特定指针来进行寻址
此外也没有必要在DATA 区中动态分配内存因为DATA 区的空间本身就很小
一旦在XDATA 区中声明了这个块指向块的指针和块的大小要传递给初始化函数
init_mempool ,他将设置一些内部变量和进行一些准备工作并对动态存储空间进行初始
化一旦初始化工作完成可在任何系统中调用动态分配函数动态分配的函数包括
malloc(接受一个描述空间大小的unsigned int 参数,返回一个指针),calloc(接受一个描
述数量和一个描述大小的unsigned int 参数,返回一个指针),realloc(接受一个指向块的
指针和一个描述空间大小的unsigned int 参数,返回一个指向按给出参数分配的空间的指
针),free(接受一个指向块的指针,使这个空间可以再次被分配) 所有这些函数都将返回指
向堆的指针如果失败的话将返回NULL 下面是一个动态分配存储区的例子
列表0-15
#include stdio.h
#include stdlib.h
// 代码中利用特定指针来提高效率
typedef struct entry_str { // 定义队列元素结构
struct entry_str xdata *next; // 指向下一个元素
char text[33]; // 结构中的字符串
} entry;
void init_queue(void);
void insert_queue(entry xdata *);
void display_queue(entry xdata *);
void free_queue(void);
entry xdata *pop_queue(void);
gets atof atan2
printf atol cosh
sprinf atoi sinh
scanf exp tanh
sscanf log calloc
memccpy log10 free
strcat sqrt Init_mempool
strncat srand malloc
strncmp cos realloc
strncpy sin ceil
strspn tan floor
strcspn acos modf
strpbrk asin pow
strrpbrk atan
表0-7
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
37
entry xdata *root=NULL; // 设置队列为空
void main(void) {
entry xdata *newptr;
init_queue(); // 设置队列
...
newptr=malloc(sizeof(entry)); // 分配一个队列元素
sprintf(newptr-text, entry number one);
insert_queue(newptr); // 放入队列
...
newptr=malloc(sizeof(entry));
sprintf(newptr-text, entry number two);
insert_queue(newptr); // 插入另一个元素
...
display_queue(root); // 显示队列
...
newptr=pop_queue(); // 弹出头元素
printf(%s\n, newptr-text);
free(newptr); // 删除它
...
free_queue(); // 释放整个队列空间
}
void init_queue(void) {
static unsigned char memblk[1000]; // 这部分空间将作为堆
init_mempool(memblk, sizeof(memblk)); // 建立堆
}
void insert_queue(entry xdata *ptr) { // 把元素插入队尾
entry xdata *fptr, *tptr;
if (root==NULL){
root=ptr;
} else {
fptr=tptr=root;
while (fptr!=NULL) {
tptr=fptr;
fptr=fptr-next;
}
tptr-next=ptr;
}
ptr-next=NULL;
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
38
void display_queue(entry xdata *ptr) {// 显示队列
entry xdata *fptr;
fptr=ptr;
while (fptr!=NULL) {
printf(%s\n, fptr-text);
fptr=fptr-next;
}
}
void free_queue(void) { // 释放队列空间
entry xdata *temp;
while (root!=NULL) {
temp=root;
root=root-next;
free(temp);
}
}
entry xdata *pop_queue(void) { // 删除队列
entry xdata *temp;
if (root==NULL) {
return NULL;
}
temp=root;
root=root-next;
temp-next=NULL;
return temp;
}
可见使用动态分配函数就像ANSI C 一样十分方便
13 结论
使用C 来开发你的系统将更加方便快捷他既不会降低你对硬件的控制能力也不会使
你的代码长度增加多少如果你运用得好的话你能够开发出非常高效的系统并且非常
利于维护
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
39
第三章使用软件补充硬件
1 介绍
这章将展示用软件来提升你系统整体性能的方法通过这些软件方法将提供用户接
口时钟系统并能够减少不必要的硬件下面将举一个使用8051 作时钟的例子系统用
一个接在单片机端口上的标准2x16 的LCD 来显示时间按第一个按钮将进入模式设置状态
并在相应的地方显示光标按第二个按钮将增加数值15 秒之后如果无键按下将回到正
常状态
为了降低成本,用微处理器来仿真实时时钟芯,并且液晶片将接在微处理器的一个口上.
用软件仿真实时时钟并直接控制液晶片的接口这样就不再需要使用译码芯片和实时时钟
芯片了为了进一步减少元器件将采用内部RAM 程序能够使用的RAM 就被控制在128
个字节以内
做软件的时候要认真考虑RAM 的用法充分利用RAM 的空间系统接线图见图0-1
系统使用了带内部EPROM 的8051 这样就省去了外部EPROM 和用来做为接口的74373 口
0 和口2 保留用做系统扩展之需
为了有一个比较图0-2 给了传
统设计方法的接线图处理器对
实时时钟芯片和LCD 驱动芯片进
行寻址这需要一个地址译码器
和一个与非门这个设计还使用
了外部SRAM 注意两种设计的不
同
图0-1 时钟电路
2 使用小存储模式
为了不使用SRAM 就要使用小存储模式这把能够使用的RAM 数量限制在128 个字节
内处理器内部堆栈压缩栈所有程序变量和所有包含进来的库函数都将使用这些数量
有限的RAM
编译器可以通过覆盖技术来优化RAM 的使用所以应尽量使用局部变量通过覆盖分
析编译器决定哪些变量被分配在一起哪些不能在同一时间存在这些分析告诉L51 如
何使用局部存储区很多时候根据调用结构一个存储地址将存储不同的局部变量所
以要多使用局部变量当然不可避免的有一些全局变量像标志位保存每日时间的变
量也有可能在指定的函数中定义静态变量编译器会把他们当成全局变量一样处理
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
40
图0-2 扩展电路
为了节省RAM 要尽可能少的调用库函数一些库函数要占用大量的RAM 并且这些函
数的范围和功能都超出了所需比如printf 函数包含了时钟不需要的很多初始化功能
应考虑是否要专门写一个程序来替代标准的printf 函数这样会占用更少的资源
表0-1
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
41
3 使用液晶驱动
这个项目所选择的液晶驱动芯片为GMD16202 有2x16 段它的接口十分简单表0-1
中列出了对芯片操作的简单的指令上电后必须初始化显示包括总线的宽度线的数
量输入模式等每个命令之间要查询显示是否准备好接收下一个数据执行每条指令一
般需要40ms 时间有些只需要1.64ms
3.1 LCD 驱动接口
我们通过减少元件来降低成本,从液晶驱动接口可以很容易的看出这点,驱动芯片的8
位数据线和P1 口相连用软件来控制显示和产生正确的使能信号脉冲序列锁住输入输出
的数据而典型的系统驱动芯片和8051 的总线相连软件只需要用XBYTE[]对芯片寻址
就可以了当把工作交由软件来完成之后就不再需要解码器和一些支持芯片这就降低
了速度因为软件要完成8051 和LCD 驱动芯片之间的数据传输工作代码的长度和执行时
间都会比较长对时钟系统
来说有大量的EPROM 空间剩
余代码的长度不是问题
而由以后的分析我们会发现
执行的时间长短也不是问
题一旦理解了LCD 驱动芯
片所需的信号和时序之后
显示的接口函数就很容易写
了软件只须要3 个基本功
能写入一个命令写入下
一个字符读显示状态寄存
器这些操作的时序关系见
图0-3 和0-4 在每个信号
之间允许有很长的时间间
隔信号有效或无效的时间
可以毫秒来计算而不像系
统总线那样以钠秒来计算
I/0 函数只需要按照时序图
来操作就可以了
列表0-1
void disp_write(unsigned char value) {
DISPDATA=value; // 发送数据
REGSEL=1; // 选择数据寄存器
RDWR=0; // 选择写模式
ENABLE=1; // 发送数据给LCD
ENABLE=0;
}
disp_write 的功能是送一个字符给LCD 显示在送数之前应查看LCD 驱动芯片是否已
经准备好接收数据
列表0-2
void disp_cmd(unsigned char cmd) {
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
42
DISPDATA=cmd; // 发送命令
REGSEL=0; // 选择命令寄存器
RDWR=0; // 选择写模式
ENABLE=1; // 发送命令给LCD
ENABLE=0;
TH1=0; // 定时85ms
TL1=0;
TF1=0;
TR1=1;
while (!TF1 disp_read() DISP_BUSY);// 等待显示
// 结束命令
TR1=0;
}
disp_cmd 函数的时序和disp_write 一样但只有到LCD 驱动芯片准备好接收下一个
数据时才结束函数
列表0-3
unsigned char disp_read(void) {
unsigned char value;
DISPDATA=0xFF; // 为所有输入设置端口
REGSEL=0; // 选择命令寄存器
RDWR=1; // 选择读模式
ENABLE=1; // 使能LCD输出
value=DISPDATA; // 读入数据
ENABLE=0; // 禁止LCD输出
return(value);
}
disp_read 函数的功能是锁住显示状态寄存器中的数根据上面的时序进行操作同
时读出P1 中的数据数据被保存并作为调用结果返回
如你所见从控制器的端口控制显示是十分简单的缺点是所花的时间要长一些另
外代码也比较长但是系统的成本却降低了
4 显示数据
当初始化完成之后就可以进行显示了写入字符十分简单要告诉驱动芯片所接收
到字符的显示地址然后发送所要显示的字符当接收下一个显示字符时芯片的内部显
示地址将自动加一
为了正确显示信息和与用户之间相互作用系统需要一个函数能够完成上述功能,并能
清除显示.我们重新定义putchar 函数来向LCD 输出显示字符因此我们必须知道如何使用
前面所写的函数来完成字符的输出过程除此之外还在其它一些地方作了改动当过程检
测到255 时将发出命令清除显示并返回putchar 函数从清除显示开始对写入的数据进
行计数从而决定是否开始在显示的第二行写入函数如下
列表0-4
char putchar(char c) {
static unsigned char flag=0;
if (!flag || c==255) { // 显示是否应该回到原位
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
43
disp_cmd(DISP_HOME);
flag=0;
if (c==255) {
return c;
}
}
if (flag==16) { // 是否使用下一个显示行
disp_cmd(DISP_POS | DISP_LINE2); // 显示移到第二行
}
disp_write(c); // 送一个字符显示
while (disp_read() DISP_BUSY); // 等待显示
flag++; // increment the line flag
if (flag=32) { flag=0; } // 显示完之后清除
return(c);
}
如你所见函数十分简单它调用一些低层的I/O 过程向显示写入数据如果写入成
功的话返回所传送的字符它假设显示工作正常所以总是返回所写入的字符
4.1 定制printf 函数
C51 的库函数中包含了printf 函数该函数格式化字符串并把他们输出到标准输出
设备对PC 来说标准输出设备就是你的显示设备对8051 来说是串行口在这里只有一
个显示就本质来说printf 函数是通过不断的调用putchar 函数来输出字符串的这样通
过重新定义putchar 函数就可以改变printf 函数连接器在连接的时候将使用源代码中
的putchar 函数而不是运行函数库中的函数下面的功能将调用printf 函数来格式化时
间串并发送显示
列表0-5
void disp_time(void) {
// 显示保存的当前时间
// 当时间数据使用完毕后才清除使用标志位
// 这避免了数据在使用中被修改
printf(\xFFTIME OF DAY IS: %B02u:%B02u:%B02u ,
timeholder.hour, timeholder.min, timeholder.sec);
disp_update=0; // 清除显示更新标志位
}
5 使用定时计数器来计时
不少嵌入式系统特别是那些低成本的系统没有实时时钟来提供时间信号然而这些
系统一般都要在某个时间或在系统事件的某段时间之后执行某段任务这些任务包括以一
定的时间间隔显示数据和以一定的频率接收数据一般设计者会通过循环来延时这种
做法的缺点是对不同的延时时间要做不同的延时程序很多延时程序是通过NOP 和DJNZ
指令来进行延时的这对于使用电池的系统来说是一种消耗
一种好得多的方法是用内置定时器来产生系统时钟定时器不断的溢出重装并在
指定的时间产生中断中断程序重装定时器分配定时时间并执行指定的过程这种方
法的好处是很多的首先处理器不必一直执行计时循环他可在各个中断之间处于idle
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
44
模式或执行其它指令其次所有控制都在ISR 中进行如果系统频率改变了或定时时间
需要改变软件只需要更改一个地方第三所有的代码都可用C 来编写你可以通过观
察汇编后的代码来计算定时器溢出到定时器重装并开始运行所需的时间进一步根据重装
值来计算定时的时间
我所作过的没有外部时间输入却要有系统时间的嵌入式系统都采用了这种方法下面
将介绍如何每隔50ms 产生一个时钟信号在编写软件之前你首先要明确你的要求如果你
最快的任务执行速度是3ms 一次那么就以这个时间为准发生频率比较慢的事件可以很
好的被驱动如果你的系统时间不能很好的兼容你可以考虑使用两个定时器
决定了系统的时间标志后就需要算出按所需频率产生时标的定时器重装值为此
你要知道你的晶振频率用它来得到指令周期的执行时间如果你要产生一个50ms 的时标
你的系统频率是12MHz 你的指令执行频率就是1MHz,每条指令的执行时间就是1us
有了指令的执行时间就可以计算出每个系统时间标志所需要的指令周期数根据前面
的条件需要50000 个指令周期来获得50ms 一次的系统频率标志65536 减去50000 得到
15536 3CB0 的重装值如果你的要求不是那么精确的话可把这个值直接装入定时器中
下面的例子用定时器0 产生系统时标定时器1 用来产生波特率或其它定时功能
列表0-6
#define RELOAD_HIGH 0x3C
#define RELOAD_LOW 0xB0
void system_tick(void) interrupt 1 {
TR0=0; // 停止定时器
TH0=RELOAD_HIGH; // 设置重装值
TL0=RELOAD_LOW;
TR0=1; // 重新启动定时器
// 执行中断操作
}
以上为过程的一个基本结构一旦定时器重装并开始工作之后你就可以进行一些操
作如保存时标数事件操作置位标志位你必须保证这些操作的时间不超过定时器的
溢出的时间否则将丢失时标数
可以很容易的让系统在一定的时标数之后执行某些操作这通过设置一个时标计数变
量来完成这个全球变量在每个时标过程中减一当它为0 时将执行操作例如你有一个
和引脚相连的LED 希望它亮2 秒钟然后关掉代码如下
if (led_timer) { // 时间计数器不为0
led_timer--; // 减时间计数器
if (!led_timer) { // 显示时间到...
LED=OFF; // turn off the LED
} }
虽然上面一段代码很简单却可以用在大多数嵌入式系统中当有更复杂的功能需要
执行时这段代码可放置在定时器中断程序中这样在检查完一个定时时间之后可以接
着检查下一个定时时间并决定是否执行相应的操作共用一个时标的定时操作可被放入
一个只有时标被某个特定数整除才有效的空间中
假设你需要以不少于1 秒的间隔时间执行一些功能使用上面的时标过程你只要保
存一个计数器仅当计数器变为0 的时候查询那些基于秒的定时操作而不需要系统每
隔50ms 就查询一次
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
45
second_cnt--; // 减时标计数器
if (!second_cnt) { // 一秒钟过去了...
... // 进行相应的操作
second_cnt=20; // 重新定时1秒
}
注意你的中断服务程序所需的执行时间如果执行时间超过50ms 你会发现将丢失时
标在这种情况下你把一些操作移出中断程序放到主程序中通过设置标志位来告诉
主程序是否要执行相应的功能但操作的时间精度就不够高了因此对时间精度要求很高
的操作还是要放在中断程序中
可用上面的时标过程来做成时钟它将记录每天的时间并在需要显示时间的时候置
位标志位主程序将监视标志位在时间更新的时候显示新的时间定时器0 中断程序还
将对按键延迟计时
6 使用系统时标做用户接口
用户接口相对来说比较简单但并不说明这里讲到的不能用到大系统中设置键用来
击活设置模式更改时间当进入设置模式后设置键将用来增加光标处的数值选择键
将使光标移到下一个位置当光标移过最后一个位置时设置模式结束每次设置键或选
择键被击活后设置模式计数器被装入最大值每个时标来临时减1 当减到0 时结束
设置模式
每隔50ms 在中断中查询按键这种查询速度对人来说已经足够了有时侯甚至0.2 秒
都可以对8051 来说人是一个慢速的I/O 器件当检测到有键按下时将设置一个计数器
以防按键抖动这个计数器在每次中断到来时减1 直到计数器为0 时才再次查询按键
当设置模式被击活时软件必须控制光标在显示器上的位置让操作者知道要设置哪
个位置cur_field 变量指向当前的位置set_cursor 函数将打开关闭光标或把它移到
所选择的位置为了简化用户设置的工作和同步时钟当进行设置时计时被挂起这也
避免了在设置时程序用printf 更新时间在进行时间更新时也不允许进入设置模式
这也将避免pirntf 函数在同一时间被多个中断调用
下面是系统时标程序对许多系统来说这个程序已经足够可把它作为你应用程序的
模块
列表0-7
void system_tick(void) interrupt 1 {
static unsigned char second_cnt=20; // 时间计数器顶事为1秒
TR0=0; // 停止定时器
TH0=RELOAD_HIGH; // 设定重装值
TL0=RELOAD_LOW;
TR0=1; // 启动定时器
if (switch_debounce)
switch_debounce--;
}
if (!switch_debounce) {
if (!SET) { // 如果设置键被按下...
switch_debounce=DB_VAL;
if (!set_mode !disp_update) { // 如果时钟不在设置模式
set_mode=1; // 进入设置模式
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
46
set_mode_to=TIMEOUT; // 设置间隔时间
cur_field=HOUR; // 选择第一个位置
set_cursor(ON, HOUR); // 使能光标
}else {
cur_field++; // 移到下一个位置
if (cur_fieldSEC) { // 如果移过最后一个位置
// 结束设置模式
set_mode=0; // 离开设置模式
set_mode_to=0;
set_cursor(OFF, HOME); // 禁能光标
}else {
set_cursor(ON, cur_field); // 光标移到下一个位置
set_mode_to=TIMEOUT;
}
}
}
if (set_mode !SELECT) { // 如果按下选择键
set_mode_to=TIMEOUT;
incr_field(); // 选择下一个位置
disp_time(); // 显示更新的时间
}
}
if (!set_mode) { // 当处于设置模式时停止时钟
second_cnt--; // 时间计数器减1
if (!second_cnt) { // 如果过了1秒种...
second_cnt=20; // 重置计数器
second_tick();
}
}
}
7 改进时钟软件
在这里你可以开始消除系统时标中的误差你应该记得误差是由从定时器溢出到定时
器重装并开始运行之间的代码延时引起的为了消除误差先用C51 代码选项汇编这段
函数然后计算启动定时器所需要的时钟周期数最后再加上进入中断所需的2 个周期数
你可能会决得当处理器在进行DIV 或MUL 操作时检测到中断要花3 个或更多的周期但是
毕竟没有快速而可靠的方法来确定处理器检测到中断的准确时间下面是汇编后的指令列
表我已经加入了指令计数
列表0-8
; FUNCTION system_tick (BEGIN)
0000 C0E0 PUSH ACC 2, 2
0002 C0F0 PUSH B 2, 4
0004 C083 PUSH DPH 2, 6
0006 C082 PUSH DPL 2, 8
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
47
0008 C0D0 PUSH PSW 2, 10
000A C000 PUSH AR0 2, 12
000C C001 PUSH AR1 2, 14
000E C002 PUSH AR2 2, 16
0010 C003 PUSH AR3 2, 18
0012 C004 PUSH AR4 2, 20
0014 C005 PUSH AR5 2, 22
0016 C006 PUSH AR6 2, 24
0018 C007 PUSH AR7 2, 26
; SOURCE LINE # 332
; SOURCE LINE # 335
001A C28C CLR TR0 1, 27
; SOURCE LINE # 336
001C 758C3C MOV TH0,#03CH 2, 29
; SOURCE LINE # 337
001F 758AAF MOV TL0,#0AFH 2, 31
; SOURCE LINE # 338
0022 D28C SETB TR0 1, 32
; SOURCE LINE # 340
从指令计数可以知道一共损失了34 32+2 个指令周期我们注意到大部分损失的时
间是由于把寄存器入栈因为每个入栈指令又要对应一条出栈指令这样就要花去52 个指
令周期这使编译器所做的一种数据保护措施我们可通过指定寄存器组来消除这种保护
措施
另一个耗时的功能是printf 函数仿真显示当准备好接收显示字符时传送字符串进
行显示需要消耗6039 个指令周期我们因此认为printf 和putchar 函数的执行时间是
6039 个指令周期相当于6.093ms 在每次中断之间执行这个过程并不会导致系统的不稳
定为了确认这点我们对中断程序进行仿真当时间从23 59 59 变为00 00 00 时
这代表了非设置模式的中断最长执行时间中断的执行时间是207 个处理周期相当
于.207ms,当没有时间改变时中断的时间为.076ms
因为是每50ms 进行一次中断那么进行时间更新和显示的时间加起来不过是6.246ms
在下一次进行中断之前有43.754ms 是在空闲模式如果你的功能只有这些或许你的系
统是用电池供电减少处理器工作时间的最佳方法是用一个更加精简的函数替代printf 函
数
因为系统除了显示时间外不需显示其它信息你可以大大的简化printf 函数它不需
要处理串行格式化字符格式化整型长整型或浮点数你可假定只有某一部分的数值
需要改变printf 的替代函数对一个缓冲区进行处理这个缓冲区包括已经格式化过的字符
串只要把更新的字符插入真确的位置就可以了为了加快执行的时间通过查表来得到
要显示的字符这是执行时间和存储空间的交换因为这个程序比较小2000 字节以内
有充足的空间如果不是这样的话你就需要在中断中进行BCD 码和ASCII 码的转化
这样中断程序将占用超过76 个指令周期的时间
disp_time 函数将代替printf 函数我们不再需要进行字符串初始化和3 个参数的传递
只需在缓冲区中修改显示字符并把一个字节传送给putchar 函数编程的复杂程度增加
了但即使在增加了120 个字节的字符表后代码的长度仍然从1951 个字节减少到1189
字节printf 函数占用了811 个字节而disp_time 函数占用了105 个字节下面是disp_time
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
48
函数
列表0-9
void disp_time(void) {
// 显示保存的当前时间
// 当时间数据使用完毕后才清除使用标志位
// 这避免了数据在使用中被修改
static char time_str[32]=TIME OF DAY IS: XX:XX:XX ;
unsigned char I;
time_str[T_HOURT]=bcdmap[timeholder.hour][0];
time_str[T_HOUR]=bcdmap[timeholder.hour][1];
time_str[T_MINT]=bcdmap[timeholder.min][0];
time_str[T_MIN]=bcdmap[timeholder.min][1];
time_str[T_SECT]=bcdmap[timeholder.sec][0];
time_str[T_SEC]=bcdmap[timeholder.sec][1];
putchar(0xFF);
for (i=0; i32; i++) {
putchar(time_str
);
}
disp_update=0; // 清除显示更新标志位
}
disp_time 的处理时间为2238 个指令周期对12MHz 系统来说就是2.238ms 清除显
示要花1.64ms 把32 个字符送显示要花1.28ms 每次更新的显示的延时是2.92ms 如果
每秒刷新一次显示的话则每秒的中断处理时间为6.866ms 其中包括76x19 周期(每秒中
有19 次中断)的中断执行时间207 周期的时间数据更新时间2238 周期的显示时间再加
上2.92ms 的显示延迟时间可以看出系统在大部分时间处于空闲模式
8 优化内部RAM 的使用
这个系统还没有考虑的另一个缺点是它还没有优化内部RAM 的使用通过M51 得到
的数据段存储区列表文件如下
TYPE BASE LENGTH RELOCATION SEGMENT NAME
-----------------------------------------------------
* * * * * * * D A T A M E M O R Y * * * * * * *
REG 0000H 0008H ABSOLUTE REG BANK 0
DATA 0008H 0002H UNIT DATA_GROUP
000AH 0016H *** GAP ***
BIT 0020H.0 0000H.2 UNIT ?BI?CH4CLOCK
BIT 0020H.2 0000H.1 UNIT BIT_GROUP
0020H.3 0000H.5 *** GAP ***
DATA 0021H 002BH UNIT ?DT?CH4CLOCK
IDATA 004CH 0001H UNIT ?STACK
似乎不是很明显,数据段的0AH 到位寻址段的开始位置20H 的22 个字节寄存器没有被
利用这是因为连接器不能把CH4CLOCK 模块的变量放在这个这个数据段中这样就使
得堆栈变小了系统更容易发生溢出错误
之所以发生这种情况是因为你把所有变量都定义在一个文件当中ch4clock.c 一种
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
49
解决办法是在连接的时候使用指令指定哪些变量将存储在数据段的底部直接的方法是告
诉连接器这个文件的所有变量都存储在数据段的底部我们在连接选择对话框中选择tab
项然后在预编译控制中写入下面的数据
DT ch4clock
当你所指定的存储变量不超过寄存器组的最高地址的寄存器和位寻址区的最低地址
时这样做是最好的但是假设你定义了位变量而变量的存储区又超出了上面所说的
范围那么你的位变量就将被覆盖并发生连接错误为此你必须把一部分变量移到另外
一个文件中这将产生两个小的数据段然后你可以使用连接指令定义他们的存储位置
通过这种方法你可以完全消除数据沟
另一个文件单独编译并和主文件一起连接在这里22 个字节的变量被移到另一个文件
中在时钟系统这个小程序中只有9 个字节的变量被移到另一个文件中结果如下
TYPE BASE LENGTH RELOCATION SEGMENT NAME
-----------------------------------------------------
* * * * * * * D A T A M E M O R Y * * * * * * *
REG 0000H 0008H ABSOLUTE REG BANK 0
DATA 0008H 0022H UNIT ?DT?CH4NEW
BIT 002AH.0 0000H.2 UNIT ?BI?CH4NEW
BIT 002AH.2 0000H.1 UNIT _BIT_GROUP_
002AH.3 0000H.5 *** GAP ***
DATA 002BH 0009H UNIT ?DT?VARS
DATA 0034H 0004H UNIT _DATA_GROUP_
IDATA 0038H 0001H UNIT ?STACK
从上面可以看出编译器流下了72 个字节的堆栈空间80H-28H 数据沟也不见了
你现在必须确认72 个字节的空间对你的系统已经足够我们可以算一下从前面可知
disp_time 调用需要花去13 个字节把PC 入栈要2 个字节中断调用花去2 个字节disp_time
调用putchar,而putchar 又调用disp_cmd disp_cmd 再调用disp_read 这又需要4 个字节
总共花去25 个字节仍然有47 字节的空间剩余这说明连接器给出的堆栈空间是足够的
9 完整的程序
到此为止,这个时钟程序算是完成了实现了对硬件的简化整个程序如下所示
列表0-10
#includereg51.h
#includestdio.h
//定义定时器0 的重装值
#define RELOAD_HIGH 0x3C
#define RELOAD_LOW 0xD2
//定义按键弹跳时间
#define DB_VAL
//定义设置模式的最大时间间隔
#define TIMEOUT 200
//定义光标位置常数
#define HOME 0
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
50
#define HOUR 1
#define MIN 2
#define SEC 3
//定义光标状态常数
#define OFF 0
#define ON 1
//定义显示命令常数
#define DISP_BUSY 0x80
#define DISP_FUNC 0x38
#define DISP_ENTRY 0x06
#define DISP_CNTL 0x08
#define DISP_ON 0x04
#define DISP_CURSOR 0x02
#define DISP_CLEAR 0x01
#define DISP_HOME 0x02
#define DISP_POS 0x80
#define DISP_LINE2 0x40
sbit SET=P3^4; //设置按键输入
sbit SELECT=P3^5; //选择按键输入
sbit ENABLE=P3^1; //显示使能输出
sbit REGSEL=P3^7; //显示寄存器选择输出
sbit RDWR=P3^6; //显示模式输出
sfr DISPDATA=0x90; //显示8 位数据总线
typedef struct { //定义存储每日时间的结构
unsigned char hour,min,sec;
}timestruct;
bit set_mode=0; //进入设置模式时置位
disp_updata=0; //需要刷新显示时置位
unsigned char set_mode_to=0; //为每次按键操作的时间间隔计时
switch_debounce=0; //按键跳动计时
cur_field=HOME; //设置模式的当前位置选择
timestruct curtime; //存放当前的时间
timeholder; //存放显示时间
unsigned char code fieldpos[3]={ //
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
51
DISP_LINE2|0x01;
DISP_LINE2|0x04;
DISP_LINE2|0x07;
};
#define T_HOURT 16
#define T_HOUR 17
#define T_MINT 19
#define T_MIN 20
#define T_SECT 22
#define T_SEC 23
char code bcdmap[60][2]={
“00”,”01”,”02”,”03”,”04”,”05”,”06”,”07”,”08”,”09”,
“10”,”11”,”12”,”13”,”14”,”15”,”16”,”17”,”18”,”19”,
“20”,”21”,”22”,”23”,”24”,”25”,”26”,”27”,”28”,”29”,
“30”,”31”,”32”,”33”,”34”,”35”,”36”,”37”,”38”,”39”,
“40”,“41”,”42”,”43”,”44”,”45”,”46”,”47”,”48”,”49”,
“50”,”51”,”52”,”53”,”54”,”55”,”56”,”57”,”58”,”59”,
};
//函数声明
void disp_cmd(unsigned char);
void disp_init(void);
unsigned char disp_read(void);
void disp_time(void);
void disp_write(unsigned char);
void incr_field(void);
void second_tick(void);
void set_cursor(bit,unsigned char);
/*****************************************************
功能:主函数
描述:程序入口函数,初始化8051,开中断,进入空闲模式每次中断之后查询标
志位是否刷新显示
参数无
返回无
*****************************************************/
void main(void){
disp_init(); //显示初始化
TMOD=0x11; //设置定时器模式
TCON=0x15;
IE=0x82;
For(;;)
{
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
52
if (disp_updata){
disp_time( ); //显示新时间
PCON=0x01;
}
}
/**************************************************
功能disp_cmd
描述向lcd 驱动器写入命令并等待命令被执行
参数命令
返回无
*************************************************/
void disp_cmd(unsigned char cmd){
DISPDATA=cmd; //锁住命令
REGSEL=0; //选择命令寄存器
RDWR=0; //选择写模式
ENABLE=1;
ENABLE=0;
TH1=0; //定时85ms
TL1=0;
TF1=0;
TR1=1;
while(!TF1disp_read()DISP_BUSY); //等待命令被执行
TR1=0;
}
/****************************************************
功能:disp_init
描述:初始化显示
参数:无
返回:无
****************************************************/
void disp_init(void){
TH1=0;
TL1=0;
TF1=0;
TR1=1;
while (!TF1disp_read()DISP_BUSY);
TR1=0;
disp_cmd(DISP_FUNC); //设置显示格式
disp_cmd(DISP_ENTRY); //每输入一个字符,显示地址加1
disp_cmd(DISP_CNTL|DISP_ON); //打开显示,关闭光标
disp_cmd(DISP_CLEAR); //清除显示
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
53
}
/*******************************************************
功能:disp_read
描述:读显示状态寄存器
参数:无
返回:从状态寄存器中读回的数据
*********************************************************/
unsigned char disp_read(void){
unsigned char value;
DISPDATA=0XFF;
REGSEL=0; //选择命令寄存器
RDWR=1; //选择读模式
ENABLE=1; //使能LCD 输出
value=DISPDATA; //读数据
ENABLE=0;
retrun(value);
}
/**********************************************************
功能:disp_time
描述:取显示数据进行格式化
参数:无
返回:无
******************************************************/
void disp_time(void){
static char time_str[32]= “TIME OF DAY IS:XX:XX:XX ”;
unsigned char I;
time_str[T_HOURT]=bcdmap[timeholder.hour][0];
time_str[T_HOUR]=bcdmap[timeholder.hour][1];
time_str[T_MINT]=bcdmap[timeholder.min][0];
time_str[T_MIN]=bcdmap[timeholder.min][1];
time_str[T_SECT]=bcdmap[timeholder.sec][0];
time_str[T_SEC]=bcdmap[timeholder.sec][1];
putchar(0xFF);
for(i=0;i32;i++){
putchar(time_str
);
}
disp_updata=0;
}
/***************************************************
功能:disp_write
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
54
描述:写入一个字节数据
参数:要写入的字节
返回:无
****************************************************/
void disp_write(unsigned char value){
DISPDATA=value;
REGSEL=1;
RDWR=0;
ENABLE=1;
ENABLE=0;
}
/*************************************************
功能:incr_field
描述:增加数值
参数:无
返回:无
**********************************************/
void incr_field(void){
if (cur_field= =SEC){
curtime.sec++;
if(curtime.sec59){
curtime.sec=0;
}
}
if (cur_field= =MIN){
curtime.min++;
if(curtime.min59){
curtime.min=0;
}
}
if (cur_field= =HOUR){
curtime.hour++;
if(curtime.hour23){
curtime.hour=0;
}
}
}
/***********************************************************
功能:putchar
描述:替代标准putchar 函数,输出字符
参数:要显示的字符
返回:刚刚被写的字符
************************************************************/
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
55
char putchar(char c){
static unsigned char flag=0;
if(!flag||c= =255){
disp_cmd(DISP_HOME);
flag=0;
if(c= =255){
return c;
}
}
if(flag= =16){
disp_cmd(DISP_POS|DISP_LINE2);
}
disp_write(c);
while(disp_read( )DISP_BUSY);
flag++;
if (flag=32){flag=0};
return(c);
}
/*************************************************************
功能:second_tick
描述:每秒钟执行一次函数功能,时间更新
参数:无
返回:无
*************************************************************/
void second_tick(void){
curtime.sec++; //秒种加1
if (curtime.sec59){ //检测是否超出范围
curtime.sec=0;
crutime.min++; //分钟加1
if (curtime.min59){ //检测是否超出范围
curtime.min=0;
curtime.hour++; //小时数加1
if(curtime.hour23){ //检测是否超出范围
curtime.hour=0;
}
}
}
if(!disp_updata){ //确信timeholder 没有被显示
timeholder=curtime; //装入新时间
disp_updata=1; //更新显示
}
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
56
/***************************************************
功能;set_cursor
描述:显示或关闭光标,并把光标移到特定的位置
参数:new_mode 位,隐藏光标时置位
field 显示光标的位置
返回:无
***************************************************/
void set_cursor(bit new_mode,unsigned char field){
unsigned char mask;
mask=DISP_CNTL|DISP_ON;
if(new_mode){
mask|=DISP_CURSOR;
}
disp_cmd(mask);
if (field= =HOME){
mask=DISP_HOME;
}else{
mask=DISP_POS|fieldpos[field-1];
}
disp_cmd(mask);
}
/*******************************************************
功能: system_tick
描述:定时器0 的中断服务程序,每50ms 重装一次定时器
参数:无
返回:无
*******************************************************/
void system_tick(void) interrupt1{
static unsigned char second_cnt=20;
TR0=0;
TH0=RELOAD_HIGH; //设定重装值
TL0=RELOAD_LOW;
TR0=1; //开始定时
if(switch_debounce){ //按键抖动
switch_debounce--;
}
if (!switch_debounce){
if(!SET){ //如果设置按钮被按下
switch_debounce=DB_VAL; //设置消抖时间
if(!set_mode!disp_updata){ //如果不是设置模式
set_mode=1; //进入设置模式
set_mode_to=TIMEOUT; //设置空闲时间
cur_field=HOUR; //选择光标起始位置
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
57
set_cursor(ON,HOUR); //使能光标
}else{
cur_field++; //光标位置前进
if(cur_fieldSEC){ //光标是否超出范围
set_mode=0; //退出设置模式
set_mode_to=0;
set_cursor(OFF,HOME); //禁能光标
}else{
set_cursor(ON,cur_field); //光标移到下一个位置
set_mode_to=TIMEOUT;
}
}
}
if(set_mode!SELECT){ //如果按下选择按钮
set_mode_to=TIMEOUT;
incr_field( ); //所选择处数值增加
disp_time( ); //显示时间
}
}
if(!set_mode){ //设置模式停止时钟
second_cnt- -; //计数值减1
if(!second_cnt){ //如果经过1 秒
second_cnt=20; //设置计数值
second_tick( );
}
}
}
10 使用看门狗定时器
很多嵌入式系统利用查询,等待的方法和外部设备进行通信或花大量的时间在循环中
处理数据一直在这种状态下运行对系统来说是很苛刻的嵌入式系统不应该陷入死循环
中否则将影响系统的正常工作引起死循环的原因有很多如I/O 设备的错误接收了
错误的输入或软件设计中的bug 不管原因是什么它都将使你的系统不稳定
作为一种保护很多设计者都使用看门狗定时器看门狗定时器从某一个值开始计时
在它溢出前必须由软件重装否则将认为软件运行已经进入死循环或其它一些意想不到
的情况系统将自动复位设计者编写软件来处理看门狗并在看门狗定时器溢出之前调
用它这些软件相对来说是比较容易编写的但必须按照特定的规则下面是一个初始化
和重装Pilips80C550 看门狗定时器的例子
void wd_init(unsigned prescale){
WDL=0xFF; //把重装值设置为最大
WDCON=(prescale0xE0)|0x05; //定时器预分频为最慢并启动看门狗
wd_reload();
}
void wd_reload(void){
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
58
EA=0; //关闭所有中断
WPEED1=0xA5 //喂看门狗第一步
WFEED2=0x5A //喂看门狗第二步
EA=1 //开中断
}
一般来说你可以把这段程序放在有周期性中断的系统的主循环中如果你的系统能
被其它的中断打断并影响到你主循环的执行从而不能够定时的重装你的看门狗定时器
这时你应该在中断程序中也放置清看门狗程序然而不是每次执行中断时都执行看门狗
程序而是应该对中断执行的次数计数当达到一定的数值时再执行看门狗程序如下
面的例子
列表0-12
void main(void){
… //初始化
for(;;){ //主循环等待中断
…
wd_reload( ); //重装看门狗
PCON=0x80; //进入空闲模式等待中断
}
}
void my_ISR(void)interrupt 0{
static unsigned char int_count=0; //中断次数计数
int_count++;
if(int_countMAXINTS){ //中断次数到了
int_count=0;
wd_reload( ); //重装看门狗
} …
}
看门狗定时器的复位和正常的上电复位时是不同的如果你的程序执行过程中产生了
数据你应该在外部RAM 中倍份它们除非你确定每次程序开始执行时不需要初始化它们
系统应该知道在何时保存正常运行时产生的数据
12 保存系统数据
系统应根据先前的状态决定不同的复位方式例如你的程序运行正常但是被看门
狗或外部复位键复位你应该采取和上电复位不同的初始化过程一般来说看门狗复位
和用户复位是是热启动在8051 系统中没有任何RAM 的备用电池这种复位很容易通过检
测标志位来区分
当系统首次执行代码时标志位检测为特定值如果值不对的话就将进行上电初始
化如果值是对的就将只进行所需要的初始化一旦系统被初始化热启动标志被设置
成特定值值的选择应避免使用00 或FF 否则就难以区分冷启动和热启动我们应选择
像AA 或CC 这样的值对必须在内部RAM 中保存数据的系统必须在编译的启动代码中检
测标志这意味着你必须修改startup.a51 对可以在外部RAM 中保存数据的系统来说
如果你的标志位保存在外部RAM 中你就不需要改动startup.a51 了因为默认时由
startup.a51 编译过来的代码只会初始化内部RAM 中的数据而不会置0 外部RAM 中的数
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
59
据如果你把标志位保存在内部RAM 中而没有在内部RAM 被置0 前检测它将导致系统
冷启动
下面是一个启动时对标志位作检测的例子
列表0-13
unsigned char xdata bootflag;
void main(void){
…
if (bootflag!=0xAA){ //系统是否冷启动
init_lcd(); //初始化显示
init_rtc(); //初始化时钟
init_hw(); //设置I/O 端口
reset_queue(); //复位数据结构
bootflag=0xAA; //设置热启动标志
}else{
clear_lcd(); //清除显示
} …
}
对只能在内部RAM 中保存数据的系统来说必须修改startup.a51 文件以确保程序只
清除被编译器使用的和不需要被系统记住的区域被修改的startup.a51 如下所示
列表0-14
;-----------------------------------------------------------------
; This file is part of the C-51 Compiler package
; Copyright (c) KEIL ELEKTRONIK GmbH and Keil Software, Inc.,
; 1990-1992
;-----------------------------------------------------------------; 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:
;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
60
EXTRN DATA (bootflag)
;
; 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 C-51 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.
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
61
;
PPAGEENABLE EQU 0 ; set to 1 if pdata object are used.
PPAGE EQU 0 ; define PPAGE number.
;
;-----------------------------------------------------------------
NAME ?C_STARTUP
?C_C51STARTUP SEGMENT CODE
?STACK SEGMENT IDATA
RSEG ?STACK
DS 1
EXTRN CODE (?C_START)
PUBLIC ?C_STARTUP
CSEG AT 0
?C_STARTUP: LJMP STARTUP1
RSEG ?C_C51STARTUP
STARTUP1:
MOV A, bootflag ; check if RAM is good
CJNE A, #0AAH, CLRMEM
SJMP CLRCOMP ; RAM is good, clear only
; compiler owned locations
CLRMEM: ; RAM was not good,
; zero it all
IF IDATALEN 0
MOV R0,#IDATALEN - 1
CLR A
IDATALOOP: MOV @R0,A
DJNZ R0,IDATALOOP
JMP CLRXDATA
ENDIF
CLRCOMP: CLR A ; zero out compiler owned
; areas
MOV 20H, A
MOV R0, #3EH
L1: MOV @R0, A
INC R0
CJNE R0, #76H, L1
CLRXDATA:
IF XDATALEN 0
MOV DPTR,#XDATASTART
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
62
MOV R7,#LOW (XDATALEN)
IF (LOW (XDATALEN)) 0
MOV R6,#(HIGH XDATALEN) +1
ELSE
MOV R6,#HIGH (XDATALEN)
ENDIF
CLR A
XDATALOOP: MOVX @DPTR,A
INC DPTR
DJNZ R7,XDATALOOP
DJNZ R6,XDATALOOP
ENDIF
IF PDATALEN 0
MOV R0,#PDATASTART
MOV R7,LOW (PDATALEN)
CLR A
PDATALOOP: MOVX @R0,A
INC R0
DJNZ R7,PDATALOOP
ENDIF
IF IBPSTACK 0
EXTRN DATA (?C_IBP)
MOV ?C_IBP,#LOW IBPSTACKTOP
ENDIF
IF XBPSTACK 0
EXTRN DATA (?C_XBP)
MOV ?C_XBP,#HIGH XBPSTACKTOP
MOV ?C_XBP+1,#LOW XBPSTACKTOP
ENDIF
IF PBPSTACK 0
EXTRN DATA (?C_PBP)
MOV ?C_PBP,#LOW PBPSTACKTOP
ENDIF
IF PPAGEENABLE 0
MOV P2,#PPAGE
ENDIF
MOV SP,#?STACK-1
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
63
LJMP ?C_START
END
检测启动标志,如果标志符合那么只清除编译器使用到的那部分存储区程序的所
有局部变量必须在用户产生代码中清晰的处理库函数的地址通过检察连接输出文件和清
除那些存储段来决定正如你所见地址20H 的位变量3EH 到75H 的存储区必须被清零
上面startup.a51 的连接输出文件如下所示
TYPE BASE LENGTH RELOCATION SEGMENT NAME
-----------------------------------------------------
* * * * * * * D A T A M E M O R Y * * * * * * *
REG 0000H 0008H ABSOLUTE REG BANK 0
DATA 0008H 0012H UNIT ?DT?VARS
DATA 001AH 0001H UNIT ?DT?PUTCHAR
001BH 0005H *** GAP ***
DATA 0020H 0001H BIT_ADDR ?C_LIB_DBIT
BIT 0021H.0 0000H.5 UNIT ?BI?COINOP
BIT 0021H.5 0001H.2 UNIT BIT_GROUP
0022H.7 0000H.1 *** GAP ***
DATA 0023H 001BH UNIT ?DT?COINOP
DATA 003EH 000FH UNIT ?C_LIB_DATA
DATA 004DH 0029H UNIT DATA_GROUP
IDATA 0076H 001EH UNIT ?ID?COINOP
IDATA 0094H 0001H UNIT ?STACK
另外一种存储你的内部变量而不用去考虑哪里是安全的哪里会被清零是把变量存储
在外部RAM 中这当然是指你有外部RAM 的情况下如果没有也可以用EEPROM 或flash 存
储器代替这样会更加可靠但一般都会使用RAM 因为RAM 比EEPROM 要快当处理器接
收到关闭中断时系统要把所有有效的变量都存储到外部RAM 中中断被击活时系统有
足够的时间把变量存入SRAM 中并进入低功耗模式而EEPROM 则是一个很慢的器件不
能满足这个要求如果你需要保存的数据不会经常改变,那么可在存储区中倍份这个数据
当源数据改变时倍份数据也要改变如果数据经常被改变的话这种方法就不可行了
不管采用何种方法当系统重新上电后检测一个数据字节像前面所讨论的启动标
志如果数据正确就恢复内部变量这些都在系统初始化时的条件循环中完成
13 结论
这一章展示了一些如何减少硬件并减轻硬件工作压力的方法当然方法远远不止这
些这里只是告诉你一些技巧在不少情况下可以用软件来代替硬件的工作因此可以
简化硬件的设计要完全掌握这些方法要花大量的时间你应该不断的学习以提高自己的
水平
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
64
第四章在8051 上使用汇编和C
1 介绍
在一些时候你会发现不得不使用汇编来编写程序而不是使用高级语言而大多数情
况下汇编程序能和用C 编写的程序很好的结合在一起这章将告诉你如何进行汇编和C
的混合编程并且如何修改由C 程序编译后的汇编代码从而精确的控制时间
2 增加段和局部变量
要把汇编程序加入到C 程序中你必须使你的汇编程序像C 程序这就是说要和C 程
序一样有明确的边界参数返回值和局部变量
一般来说用汇编编写的程序变量的传递参数所使用的寄存器是无规律的这使得在用
汇编语言编写的函数之间传递参数变得混乱难以维护使的汇编功能函数看上去像C 函
数并按照C51 的参数传递标准可让你的程序有很好的可读性并有利于维护而且你会
发现这样编写出来的函数很容易和C 编写的函数进行连接如果你用汇编编写的函数和C
编译器编译出来的代码风格一样的话连接器将能够对你的数据段进行覆盖分析
汇编程序中你的每一个功能函数都有自己的代码段如果有局部变量的话他们也
都有相应的存储空间DATA XDATA 等例如你有一个需要快速寻址的变量你可把它
声明在DATA 段中如果你有函数查寻表格的话你可把它们声明在CODE 段中关键是局
部变量只对当前使用他们的程序段是可见的下面的例子中一个功能段在DATA 区中定义
了几个局部变量
列表0-1
; declare the code segment for the function
?PR?IDCNTL?IDCNTL SEGMENT CODE
; declare the data segment for local storage
; this segment is overlayable for linker optimization
; of memory usage
?DT?IDCNTL?IDCNTL SEGMENT DATA OVERLAYABLE
PUBLIC idcntl
PUBLIC ?idcntl?BYTE
; define the layout of the local data segment
RSEG ?DT?IDCNTL?IDCNTL
?idcntl?BYTE:
TEMP: DS 1
COUTNT: DS 1
VAL1: DS 2
VAL2: DS 2
RSEG ?PR?IDCNTL?IDCNTL
idcntl: ... ; function code begins here
RET
DATA数据段中的标号就像汇编程序中的变量一样,连接器在连接的时候会赋予它们物理
地址段的覆盖属性将允许连接器进行覆盖分析没有这个属性?idcntl?BYTE段中的变
量将一直占用这些空间就像C中的静态变量一样这样将使内存的效率降低
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
65
3 设置变量地址
有时候我们希望把变量存储在指定的地点特别是在主控制器初始化SRAM之后从8051
系统才开始工作的情况在这种情况下两个系统必须在存储器分配上达成一致否则当8051
使用不正确地使用初始化过的数据时将导致数据丢失因此8051必须确保变量被存储在正
确的区域如果你不想在编译时才给变量分配地址Keil C可以让你指定变量的存储地址
例如你想定义一个整型变量并把它初始化为0x4050 用C是不能够把变量指定在某个地
址的另外你也不能指定位变量的地址但是对于不需要初始化的变量你可以使用关
键字_at_来指定地址
type [memory_space] variable_name _at_ constant;
如果不指定地址的话,将由选择编译的模式来指定默认的地址假设你以小模式编译
你的变量将分配在DATA段中下面是一个指定地址的例子
unsigned char data byteval _at_ 0x32;
关键字_at_的另一个有趣的功能是能通过给I/O器件指定变量名为你的输入输出器件指
定变量名例如你在XDATA段的地址0x4500处有一个输入寄存器你可以通过下面的代码为
它指定变量名
unsigned char xdata inpreg _at_ 0x4500;
以后在读该输入寄存器的时候只要使用变量名inpreg就可以了当然你也可以用Keil
C提供的宏来完成如列表0-2的例子
当你想为指定地址的变量初始化时你可使用传统汇编的方法有时候需要查表如
果把表的基址定义在某个地址的话可以简化你的寻址过程但由于在代码段中它的地址
在编译的时候决定假设你有一个256字节的表想对它进行快速寻址你可以使用列表0-3
的方法
列表0-2
void myfunc(void) {
unsigned char inpval;
inpval=inpreg; // 这行和下行是一样的
inpval=XBYTE[0x4500];
...
if (inpreg 0x40) { 根据输入的值做决定
...
}
}
列表0-3
; 取得表地址的高字节
MOV DPH, #HIGH mytable
MOV DPL, index
CLR A
MOVC A, @A+DPTR ;读数
把变量地址放在给定段中是一种简单的方法来定义那些不能被连接器重定位的段并
且指定它的起始地址上例中的表头地址可被定义在8000H中另外还可在DATA段中放置
变量
列表0-4
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
66
;定义代码段
CSEG AT 8000H
mytable: DB 1, 2, 3, 4, 5, 6, 7, 8
... ;剩下的表格定义
DSEG AT 70H
cur_field: DS 1
...
END ;
用这种方法,一个变量可被安置在任何地方如果你在你的C代码中用extern申明了这
些变量那么你的C代码可对他们进行寻址有了这些信息之后连接器将能够定位你的
变量
还有一种方法用来定位中断服务程序可以把中断入口向量或中断程序放在一个绝对
段中如果你所有的中断服务程序都是用C写的但是有一个中断程序必须用汇编来写
最好的方法就是把中断程序定位在正确的位置上,这和给变量设置地址很相似
列表0-5
CSEG AT 023H
LJMP serial_intr
由中断向量调用的中断服务程序就像其它过程一样在代码段中
列表0-6
; 定义可重定位段
RSEG ?PR?serial_intr?SERIAL
USING 0 ; 使用寄存器组0
serial_intr: PUSH ACC ; 中断服务程序
...
RETI
4 结合C和汇编
假设你要执行的操作很难用C代码来完成如使用BCD码你会觉得用汇编来编写代码
比用C更加有效率还有就是对时间要求很严格的功能用C来编程不是很保险你希望用
汇编来做但是又不愿意仅仅因为这么一小部分就把整个程序都用汇编来做这样你就必
须学会把汇编编写的程序和C编写的程序连接起来
给用汇编编写的程序段指定段名和进行定义这将使汇编程序段和C程序兼容如果
你希望在它们之间传递函数那你必须保证汇编程序用来传递函数的存储区和C函数使用
的存储区是一样的下面是一个典型的可被C程序调用的汇编函数该函数不传递参数
列表0-7
;申明代码段
?PR?clrmem?LOWLVL SEGMENT CODE
;输出函数名
PUBLIC clrmem
;这个函数可被连接器放置在任何地方
RSEG ?PR?clrmem?LOWLVL
;*****************************************************************
; Function: CLRMEM
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
67
; Description: 清除内部RAM区
; Parameters: 无
; Returns: 无.
; Side Effects: 无.
;*****************************************************************
clrmem: MOV R0,#7FH
CLR A
IDATALOOP: MOV @R0,A
DJNZ R0,IDATALOOP
RET
END
汇编文件的格式化是很简单的给存放功能函数的段一个段名因为是在代码区内
所以段名的开头为PR 这头两个字符是为了和C51的内部命名转换兼容见表0-1
段名被赋予了RSEG的属性这意味着连接
器可把该段放置在代码区的任意位置一旦段
名被确定文件必须申明公共符号然后编写代
码对于传递参数的功能函数必须符合参数的
传递规则Keil C在内部RAM中传递参数时一般
都是用当前的寄存器组当你的功能函数接收3个表0-1
以上参数时存储区中的一个默认段将用来传递剩
余的参数用做接收参数的寄存器如下表
表0-2
汇编功能函数要得到参数值时就访问这些寄存器如果这些值被使用并保存在其它地
方或已经不再需要了那么这些寄存器可被用做其它用途下面是一个C程序和汇编程序
的接口例子你应该注意到通过内部RAM传递参数的功能函数将使用规定的寄存器汇编
功能函数将使用这些寄存器接收参数对于要传递多于3个参数的函数剩余的参数将在
默认的存储器段中进行
列表0-8
C code
// C 程序中汇编函数的申明
bit devwait(unsigned char ticks, unsigned char xdata *buf);
// invocation of assembly function
if (devwait(5, outbuf)) {
bytes_out++;
列表0-9
汇编代码
; 在代码段中定义段
?PR?_devwait?LOWLVL SEGMENT CODE
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
68
; 输出函数名
PUBLIC _devwait
;这个函数可被连接器放置在任何地方
RSEG ?PR?_devwait?LOWLVL
;*****************************************************************
; Function: _devwait
; Description: 等待定时器0溢出向外部器件表明P1中的数据是有效的如果定时器尚
; 未溢出被写入XDATA的指定地址中
; Parameters: R7 – 存放要等待的时标数
; R4|R5 – 存放要写入的XDATA区地址
; Returns: 读数成功返回1,时间到返回0
; Side Effects: none.
;*****************************************************************
_devwait: CLR TR0 ;设置定时器0
CLR TF0
MOV TH0, #00
MOV TL0, #00
SETB TR0
JBC TF0, L1 ; 检测时标
JB T1, L2 ; 检测数据是否准备就绪
L1: DJNZ R7, _devwait ; 时标数减1
CLR C
CLR TR0 ; 停止定时器0
RET
L2: MOV DPH, R4 ; 取地址并放入DPTR
MOV DPL, R5
PUSH ACC
MOV A, P1 ; 得到输入数据
MOVX @DPTR, A
POP ACC
CLR TR0 ; 停止定时器0
SETB C ; 设置返回位
RET
END
上面的代码中有些我们没有讨论的问题返回值在这里函数返回一个位变量如
果时间到将返回0 如果输入字节被写入指定的地址中将返回1
当从功能函数中返回值时C51通过转换使用
内部存储区编译器将使用当前寄存器组来传递
返回参数返回参数所使用的寄存器见表0-3
返回这些类型的功能函数可使用这些寄存器
来存储局部变量直到这些寄存器被用来返回参
数假使你有一个函数要返回一个长整型你可以表0-3
使用R4到R7这4个寄存器这样你就不需要声明一个段来存放局部变量存储区就更加优
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
69
化了功能函数不应随意使用没有被用来传递参数的寄存器
5 内联汇编代码
有时候,你的程序需要使用汇编语言来编写,像对硬件进行操作或一些对时钟要求很严
格的场合,但你又不希望用汇编语言来编写全部程序或调用用汇编语言编写的函数那么
你可以通过预编译指令”asm”在C代码中插入汇编代码
列表0-10
#include reg51.h
extern unsigned char code newval[256];
void func1(unsigned char param) {
unsigned char temp;
temp=newval[param];
temp*=2;
temp/=3;
#pragma asm
MOV P1, R7 ; 输出temp中的数
NOP ;
NOP
NOP
MOV P1, #0
#pragma endasm
}
当编译器在命令行加入”src”选项时,在”asm”和”endasm”中的代码将被复制到输出的SRC
文件中如果你不指定”src”选项编译器将忽略在”asm”和”endasm”中的代码很重要的一
点是编译器不会编译你的代码并把它放入它所产生的目标文件中必须用得到的.src文
件经过编译后再得到.obj文件从上面的文件将得到下面的.src文件
列表0-11
; ASMEXAM.SRC generated from: ASMEXAM.C
$NOMOD51
NAME ASMEXAM
P0 DATA 080H
P1 DATA 090H
P2 DATA 0A0H
P3 DATA 0B0H
T0 BIT 0B0H.4
AC BIT 0D0H.6
T1 BIT 0B0H.5
EA BIT 0A8H.7
IE DATA 0A8H
RD BIT 0B0H.7
ES BIT 0A8H.4
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
70
IP DATA 0B8H
RI BIT 098H.0
INT0 BIT 0B0H.2
CY BIT 0D0H.7
TI BIT 098H.1
INT1 BIT 0B0H.3
PS BIT 0B8H.4
SP DATA 081H
OV BIT 0D0H.2
WR BIT 0B0H.6
SBUF DATA 099H
PCON DATA 087H
SCON DATA 098H
TMOD DATA 089H
TCON DATA 088H
IE0 BIT 088H.1
IE1 BIT 088H.3
B DATA 0F0H
ACC DATA 0E0H
ET0 BIT 0A8H.1
ET1 BIT 0A8H.3
TF0 BIT 088H.5
TF1 BIT 088H.7
RB8 BIT 098H.2
TH0 DATA 08CH
EX0 BIT 0A8H.0
IT0 BIT 088H.0
TH1 DATA 08DH
TB8 BIT 098H.3
EX1 BIT 0A8H.2
IT1 BIT 088H.2
P BIT 0D0H.0
SM0 BIT 098H.7
TL0 DATA 08AH
SM1 BIT 098H.6
TL1 DATA 08BH
SM2 BIT 098H.5
PT0 BIT 0B8H.1
PT1 BIT 0B8H.3
RS0 BIT 0D0H.3
TR0 BIT 088H.4
RS1 BIT 0D0H.4
TR1 BIT 088H.6
PX0 BIT 0B8H.0
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
71
PX1 BIT 0B8H.2
DPH DATA 083H
DPL DATA 082H
REN BIT 098H.4
RXD BIT 0B0H.0
TXD BIT 0B0H.1
F0 BIT 0D0H.5
PSW DATA 0D0H
?PR?_func1?ASMEXAM SEGMENT CODE
EXTRN CODE (newval)
PUBLIC _func1
;
; #include reg51.h
;
; extern unsigned char code newval[256];
;
; void func1(unsigned char param) {
RSEG ?PR?_func1?ASMEXAM
USING 0
_func1:
;---- Variable ’param?00’ assigned to Register ’R7’ ----
; SOURCE LINE # 6
; unsigned char temp;
;
; temp=newval[param];
; SOURCE LINE # 9
MOV A,R7
MOV DPTR,#newval
MOVC A,@A+DPTR
MOV R7,A
;---- Variable ’temp?01’ assigned to Register ’R7’ ----
; temp*=2;
; SOURCE LINE # 10
ADD A,ACC
MOV R7,A
; temp/=3;
; SOURCE LINE # 11
MOV B,#03H
DIV AB
MOV R7,A
;
; #pragma asm
MOV P1, R7 ; write the value of temp out
NOP ; allow for hardware delay
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
72
NOP
NOP
MOV P1, #0 ; clear P1
; #pragma endasm
; }
; SOURCE LINE # 20
RET
; END OF _func1
END
正如你所见在”asm”和”endasm”中的代码被复制到输出的SRC文件中然后这个文件
被编译并和其它的目标文件连接后产生最后的可执行文件
6 提高编译器的汇编能力
很多软件设计者都相信他们所编写的汇编代码比编译器所产生的代码效率更高因此
他们认为用汇编语言所做的项目比用高级语言所做的项目要好对这些工程师来说汇编
语言所带来的高效比前面所讨论的C语言的优点重要得多我相信如果这些工程师把他们
所编写的汇编代码和用C语言编写的程序通过编译后产生的代码比较一下他们肯定会非
常吃惊用高级语言来开发项目的速度和效率都比用汇编好
对于那些现在还难以决定用汇编还是C的开发者来说让我给你提供一个选择Keil C
编译器提供一个参数使生成的文件为汇编代码把这些汇编代码可用A51编译并和其它模
块连接这和直接用编译器产生目标文件是一样的这种做法的优点是可对产生的汇编代
码进行编辑这样可对你的代码进行优化然后再把修改后的代码进行编译和连接
决大多数情况下你不必对汇编代码进行修改因为这些代码都是经过了优化的但
有时候还是要修改的前面的一个例子告诉你如何在代码段定位表格当需要查表时只
需要计算DPTR的低字节我们再引用以前时钟系统的例子
列表0-12
char code bcdmap[60][2]={
00, 01, 02, 03, 04, 05, 06, 07, 08, 09,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
30, 31, 32, 33, 34, 35, 36, 37, 38, 39,
40, 41, 42, 43, 44, 45, 46, 47, 48, 49,
50, 51, 52, 53, 54, 55, 56, 57, 58, 59
};
void disp_time(void) {
static char time_str[32]=TIME OF DAY IS: XX:XX:XX ;
unsigned char i;
time_str[T_HOURT]=bcdmap[timeholder.hour][0];
time_str[T_HOUR]=bcdmap[timeholder.hour][1];
time_str[T_MINT]=bcdmap[timeholder.min][0];
time_str[T_MIN]=bcdmap[timeholder.min][1];
time_str[T_SECT]=bcdmap[timeholder.sec][0];
time_str[T_SEC]=bcdmap[timeholder.sec][1];
putchar(0xFF);
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
73
for (i=0; i32; i++) {
putchar(time_str
);
}
disp_update=0; // 清除显示更新标志位
}
正如你所看到的bcdmap包括120个字节因此只用一个字节就可以包含偏移量时钟
系统中表的存放地址并不在256个字节之内我们必须得到这个表的基址再加上表内
数据的偏移量下面是编译器得到的寻址汇编代码
列表0-13
; time_str[T_HOURT]=bcdmap[timeholder.hour][0];
; SOURCE LINE # 214
MOV A,timeholder
ADD A,ACC
ADD A,#LOW bcdmap
MOV DPL,A
CLR A
ADDC A,#HIGH bcdmap
MOV DPH,A
CLR A
MOVC A,@A+DPTR
MOV time_str?42+010H,A
这段代码在程序中重复了6次你可以看到编译器产生的代码在bcdmap的地址上加上
偏移量在寄存器DPTR中得到新的地址一种简化寻址过程的方法是把表格放置在代码段
的每页的顶端这样只需要一个寻址字节就可以对表内的数据进行寻址可通过产生一个
小的汇编代码文件见表0-14 并把它和现存的C程序文件连接来实现原来C文件中的
初始化表格就要去掉了现在C文件要包含一个外部声明的bcdmap
列表0-14
CSEG AT 0400H
bcdmap: DB ’0’ ,’0’
DB ’0’ ,’1’
DB ’0’ ,’2’
...
DB ’5’ ,’7’
DB ’5’ ,’8’
DB ’5’ ,’9’
END
产生的汇编代码将使用新的寻址方式见表0-15
列表0-15
; time_str[T_HOURT]=bcdmap[timeholder.hour][0];
; SOURCE LINE # 214
MOV A,timeholder
ADD A,ACC
MOV DPL,A
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
74
MOV DPH,#HIGH bcdmap
MOVC A,@A+DPTR
MOV time_str?42+010H,A
表寻址的前一种方法需要11个处理周期,17个代码的存储空间相比之下第二种方
法只需要8个处理周期和12个字节存储空间如果你的目的是优化速度那么你已经作到
了但是当你的目的是优化代码空间可以把6个寻址代码段合并成一个功能段在程
序中调用它6次这可以大大的减少代码长度功能段代码见列表0-16
列表0-16
getbcd: ADD A,ACC
MOV DPL,A
MOV DPH,#HIGH bcdmap
MOVC A,@A+DPTR
RET
; time_str[T_HOURT]=bcdmap[timeholder.hour][0];
; SOURCE LINE # 214
MOV A,timeholder
LCALL getbcd
MOV time_str?42+010H,A
“getbcd”功能函数代码在”disp_time”函数代码段中这样就只有”disp_time”函数能
调用它
除了进行优化还可以对编译后的文件进行修改消除编译器输出文件中不必要的功
能调用我们在看一下前面的时钟例子其中包括一段更新显示的代码存放时间的结构
定义如下
typedef struct
unsigned char hour, min, sec;
} timestruct;
结构中的数据只有3个字节我们看一看编译后的结构数据的复制代码
列表0-17
; timeholder=curtime;
; SOURCE LINE # 327
MOV R0,#LOW timeholder
MOV R4,#HIGH timeholder
MOV R5,#04H
MOV R3,#04H
MOV R2,#HIGH curtime
MOV R1,#LOW curtime
MOV R6,#00H
MOV R7,#03H
LCALL ?C_COPY
这段代码需要16个处理周期和11个字节的存储空间而对C_COPY的调用又要花去70个
处理周期而仅仅只为了复制3个字节这时我们对代码做如下修改对这写字节进行手
工复制
列表0-18
; timeholder=curtime;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
75
; SOURCE LINE # 327
MOV timeholder, curtime
MOV timeholder+1, curtime+1
MOV timeholder+2, curtime+2
这段代码同样可以完成上面的工作只需要6个处理周期和6个字节存储空间
编辑产生的汇编代码使你得到很好的速度和代码空间让你使用C来进行产品开发更
加得心应手使你最终得到的代码像汇编高手编出的代码那样紧凑而高效
7 仿真多级中断
很多时候,我希望嵌入式系统的中断级别多于两级因为一般来说系统都有掉电中
断并且都被置为高优先级这样的话其它中断都共用一个低优先级在Intel 8051的数
据书中介绍了一种通过软件来扩充3个中断优先级的方法这种方法要求首先按正常方式
设置前两个中断优先级然后把要设置为最高级的那个中断设置为中断优先级1 并且在
原先中断优先级为1的中断服务程序中使能它下面是一个例子
列表0-19
PUSH IE ; 保存当前IE值
MOV IE, #LVL2INTS ; 使能中断优先级为2的中断
CALL DUMMY_LBL ; 伪RETI
... ; 中断服务程序
POP IE ; 恢复IE
RET
DUMMY_LBL: RETI
原理是很简单的首先保存IE的状态然后给IE送数使得只有中断优先级为2的中
断被使能然后调用伪RETI指令允许硬件产生中断
这样就可不必使用硬件如PICs(programmable interrupt controllers)等就可扩充
中断优先级新增加的代码不会对ISR对中断事件的响应有什么大的影响在中断程序前
面多了10个处理周期的时间这对一般系统来说都是可以忍受的这种方法可进行扩展使
每个中断都有自己的优先级
如果系统要求每个中断都有自己的优先级假设你
的中断优先级如表0-4所示那么系统就需要5个中断优
先级
按照前面所讲的方法你必须仔细选择ISR中IE的
屏蔽值只允许更高优先级的中断像串行口中断服务表0-4
程序中只能允许定时器1中断和外部中断0 而外部中断0的中断优先级最高定时器0的中
断优先级最低它们的中断服务程序无须做变动
在初始化程序中必须将定时器0的中断优先级设置为0 而其它所有中断的优先级被设
置为1 对中断优先级1到3 在它们的中断服务程序设置如下屏蔽位
列表0-20
EX1_MASK EQU 99H ; 允许串行口中断定时器1中断外部中断0
SER_MASK EQU 89H ; 允许定时器1中断外部中断0
T1_MASK EQU 81H ; 允许外部中断0
现在,在中断服务程序中加入仿真代码
列表0-21
?PR?EXT1?LOWLVL SEGMENT CODE
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
76
EXT1: PUSH IE ; 保存IE值
MOV IE, #EX1_MASK ; 使能串行口中断定时器1中断外部中断0
CALL DUMMY_EX1 ; 伪RETI
LCALL ex1_isr ; 用C代码编写的中断服务程序
POP IE ; 恢复IE
RET
DUMMY_EX1: RETI
?PR?SINTR?LOWLVL SEGMENT CODE
SINTR: PUSH IE ; 保存IE值
MOV IE, #SER_MASK ; 使能定时器1中断外部中断0
CALL DUMMY_SER ; 伪RETI
LCALL ser_isr ; 用C代码编写的中断服务程序
POP IE ; 恢复IE
RET
DUMMY_SER: RETI
?PR?TMR1?LOWLVL SEGMENT CODE
TMR1: PUSH IE ; 保存IE值
MOV IE, #T1_MASK ; 使能外部中断0
CALL DUMMY_T1 ; 伪RETI
LCALL tmr1_isr ; 用C代码编写的中断服务程序
POP IE ; 恢复IE
RET
DUMMY_T1: RETI
用少量的汇编代码使系统对硬件的功能进行了扩展系统的主要代码功能还是用C编
写的
8 时序问题
有时,代码要执行的任务有严格的时间要求这些代码必须用汇编来完成时间的精
确度要达到一两个处理周期像这种情况一种最简单的方法就是在注释区中加上指令周
期计数这给代码的编写带来很大的方便当代码改变时时序也跟着改变有了指令周
期计数后我们很容易计算时序
例如你在引脚T1按一定的时序输出数据另外一个系统监视输出并以100KHz的速率进
行采样每位数据之前都有一个2us的起始信号然后是宽度为3us的数据位其它时间T1
被置低时序如图0-1
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
77
图0-1
系统时钟为12MHz 所以指令周期为1us 使用C语言很难保证时序所以必须用汇编
语言函数接收作为参数传递过来的字节数据并从高位到低位向外发送程序见列表0-22
列表0-22
; 该函数有如下声明
; void sendbyte(unsigned char);
?PR?_sendbyte?SYS_IO SEGMENT CODE
?DT?_sendbyte?SYS_IO SEGMENT DATA OVERLAYABLE
PUBLIC _sendbyte
PUBLIC ?_sendbyte?BYTE
RSEG ?DT?_sendbyte?SYS_IO
?_sendbyte?BYTE:
BITCNT: DS 1
RSEG ?PR?_sendbyte?SYS_IO
_sendbyte: PUSH ACC ; 保存累加器
MOV BITCNT, #8 ; 发送8位数据
MOV A, R7 ; 获取参数
RLC A ; 得到第一位要发送的数据
LOOPSTRT: JC SETHIGH ; 2, 9 确认输出值
SETB T1 ; 1, 0
CLR T1 ; 1, 1
RLC A ; 1, 2 得到下一位数据
NOP ; 1, 4
NOP ; 1, 5
NOP ; 1, 6
DJNZ BITCNT, LOOPSTRT; 2, 7 是否发送完毕
SETHIGH: SETB T1 ; 1, 0
CLR T1 ; 1, 1
SETB T1 ; 1, 2 数据位置1
RLC A ; 1, 3 得到下一位数据
NOP ; 1, 4
CLR T1 ; 1, 5 清除输出
DJNZ BITCNT, LOOPSTRT; 2, 7 是否发送完毕
POP ACC ; 恢复累加器
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
78
RET
END
可以看到每条指令后面都有指令执行所需要周期数和到目前所消耗的指令周期数
每10个指令周期发送1位数据周期计数从0到9 你选择从哪条指令开始计数都没关系
在这里我选择了起始位置高的那条只作为参考指令当你的循环中有两条分支的时候你
应保证这两条分支所需要的时间是一样的这可通过使用NOP指令来平衡
当系统晶振频率不变的时候上面的程序完全可以胜任但是假使你并不生产监视
T1脚输出的模块而生产这个模块的厂家做不到以100KHz的频率进行采样这时你必须改
变你数据的输出速率为了不经常的改动程序你需要对程序重新做调整使用户能够指
定数据的输出速率
这样程序会变得复杂一些我们使用循环来消耗时间从而改变数据输出速率
列表0-23
?PR?_sendbyte?SYS_IO SEGMENT CODE
?DT?_sendbyte?SYS_IO SEGMENT DATA OVERLAYABLE
?BI?_sendbyte?SYS_IO SEGMENT BIT OVERLAYABLE
PUBLIC _sendbyte
PUBLIC ?_sendbyte?BYTE
PUBLIC ?_sendbyte?BIT
RSEG ?DT?_sendbyte?SYS_IO
?_sendbyte?BYTE:
BITCNT: DS 1
DELVAL: DS 1
RSEG ?BI?_sendbyte?SYS_IO
?_sendbyte?BIT:
ODD: DBIT 1
RSEG ?PR?_sendbyte?SYS_IO
_sendbyte: PUSH ACC ; 保存累加器
MOV BITCNT, #8 ; 发送8位数据
CLR C
MOV A, R5 ; 得到延时周期数
CLR ODD ; 延时为偶数
JNB ACC.0, P_EVEN
SETB ODD ; 延时为奇数
DEC ACC ; 对偶数的延时减去一个周期
P_EVEN: SUBB A, #4 ; 减去前面4个周期的延时
RR A ; 除2 得到所需执行DJNZs的数
MOV DELVAL, A
MOV R5, A
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
79
JNB ODD, SEND_EVEN
SEND_ODD: MOV A, R7 ; 要输出的数据
RLC A ; 第一位
LOOP_ODD: JC SETHIGH_O ; 2, 9 检测数据值
SETB T1 ; 1, 0
CLR T1 ; 1, 1
RLC A ; 1, 2 下一位
NOP ; 1, 3
NOP ; 1, 4
MOV R5, DELVAL ; 2, 6
DJNZ R5, $ ; 2, 8
NOP ; 1, 9
DJNZ BITCNT, LOOP_ODD ; 2, 11 是否传输完毕
SETHIGH_O: SETB T1 ; 1, 0
CLR T1 ; 1, 1
SETB T1 ; 1, 2
RLC A ; 1, 3 下一位
NOP ; 1, 4
MOV R5, DELVAL ; 2, 6
DJNZ R5, $ ; 2, 8
CLR T1 ; 1, 9 清除输出
DJNZ BITCNT, LOOP_ODD ; 2, 11 数据是否发送完毕
POP ACC ; 恢复累加器
RET
SEND_EVEN: MOV A, R7 ; 要输出的数据
RLC A ; 要发送的第一位
LOOP_EVEN: JC SETHIGH_E ; 2, 9 检测输出值
SETB T1 ; 1, 0
CLR T1 ; 1, 1
RLC A ; 1, 2 下一位
MOV R5, DELVAL ; 2, 4
DJNZ R5, $ ; 2, 6
NOP ; 1, 7
NOP ; 1, 8
DJNZ BITCNT, LOOP_EVEN ; 2, 10 数据是否发送完毕
SETHIGH_E: SETB T1 ; 1, 0
CLR T1 ; 1, 1
SETB T1 ; 1, 2
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
80
RLC A ; 1, 3 下一位
MOV R5, DELVAL ; 2, 5
DJNZ R5, $ ; 2, 7
CLR T1 ; 1, 8 清楚输出
DJNZ BITCNT, LOOP_EVEN ; 2, 10 数据是否发送完毕
POP ACC ; 恢复累加器
RET
END
函数首先确认所要延时的周期数的奇偶性然后决定DJNZ的执行次数我们要减去延
时循环前面所消耗的指令周期数偶数减4 奇数减5 剩下的除2就得到了要执行DJNZ的
次数DJNZ要消耗两个指令周期这样功能函数的最小延时为6个指令周期
现在你可通过在C中改变参数来改变数据的传输速率了而无须去更改程序
9 结论
这章向你说明了汇编语言在系统开发中仍然有不可替代的作用用高级语言可使你产
品的开发更加快速而稳定这并不说明你不可以把C和汇编结合起来使用汇编的确能够
完成一些高级语言不能做到的事情
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
81
第五章系统调试
1 介绍
调试嵌入式系统没有什么固定可寻的方法硬件接口所带来的复杂和时钟的限制使嵌
入式系统比PC或对应的主机应用更加复杂这些系统可以很方便的用软件进行调试能够
按使用者的步伐进行单步运行而嵌入式系统在目标板上全速运行进行调试这意味着要
控制调试你不得不使用ICE 在你的代码时间要求很严格的部分不能使用单步运行那
样会使你的程序运行出现混乱对于使用时标或看门狗的系统的调试更加困难
因为嵌入式系统的复杂性很多方法都不能使用这章将探索这些方法使你对设计
和调试你的系统有个初步的了解
2 通过系统设计来帮助调试
在你系统的设计阶段如果你的系统规划得好的话会给你将来的调试带来很大的方
便我们可以通过串行口来输出调试信息换句话说就是通过一系列I/O口来反映程序
在不同的执行阶段时的程序状态和变量状态这种方法的缺点是会增加不必要的硬件但
也可以为系统将来的扩充留下余地也可通过显示板输出调试信息还可以把调试信息存
储在RAM中当程序执行完成后再下载这些信息
不管你用什么方法调试程序当使用I/O作为调试用时好处很多在你设计系统的时
候就应该考虑这些方面并进行各种整体功能调试当然在PCB板作成之后还要做各种调
试但这时你应该已经排除了大多数的问题用PCB板进行调试时你可能会发现它像逻
辑分析仪之类的仪器你不应该完全依赖于ICE 尽管那是最方便的调试方法但不是那
么容易得到的所以应该学会如何在没有这种奢侈工具的帮助下进行调试
在没有ICE的情况下进行调试可使你很快擅长使用数字存储技术这对你调试系统很
有帮助如果你对系统在什么时候做什么事情很了解的话就可以知道在什么地方程序运
行开始出错当你发现了出错的地方后你就可以在这些地方加入调试语句把调试信息
通过显示串行口或I/O发送出来
3 使用调试端口
在没有ICE时进行调试的最有效的一种手段是通过调试端口输出数据一般来说这
些数据包括系统事件反映程序运行到某一点的调试状态变量值等调试端口一般是串
行口串行口要么完全作为调试用要么在调试端口和数据接口间时分复用而对8051来
说麻烦在于一般只有一个串行口这意味着要进行时分复用如果你有两个串行口的话
那就幸运多了不必担心调试数据会影响正常数据
当你用10个数据位向PC发送数据的时候串行调试端口会出错所以你最好使用其它
模式另外向外输出数据多出来的这部分调试代码会改变你程序的进程而且会产生一
些莫名其妙的问题
调试端口适用于那些对时间要求不严格并且用多余串行口的系统从这些讨论可以看
出第4章所说的实时时钟系统就很适合它有多余的串行口和大量的空闲时间如果你
要在这个系统上使用调试端口代码由中断进行驱动并将缓冲区中的调试数据从数据调试
端口送出
列表0-1
#include reg51.h
#include intrins.h
#ifndef NULL
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
82
#define NULL ((void *) 0L)
#endif
#define DB_MAXSIZE 0x20
unsigned char db_head, db_tail, db_buffer[DB_MAXSIZE];
/*****************************************************************
Function: debug_init
Description: 将串行口设置为调试端口把缓冲区指针设为0
Parameters: 无
Returns: 无
*****************************************************************/
void debug_init(void) {
SCON=0x90; 使用串行通讯模式2
db_head=db_tail=0;
ES=1;
}
/*****************************************************************
Function: debug_insert
Description: 把所指向的存储区中的数据拷贝到缓冲区中
Parameters: base – 指针指向要拷贝数据的头地址
size – 所要拷贝数据的数量
Returns: 无
*****************************************************************/
void debug_insert(unsigned char data *base, unsigned char size) {
bit sendit=0; // 标志位表明是否要进行串行传输初始化
unsigned char i=0;
if (!size || base==NULL) { return; }//测试参数是否有效
if (db_tail==db_head)
sendit=1;
}
while (db_tail!=db_head isize) {// 当缓冲区有空间且数据区中还有数据时
// 进行拷贝
db_buffer[db_tail]=base
; // 拷贝当前字节
i++;
db_tail++; // 移动指针
if (db_tail==DB_MAXSIZE) { // 指针是否超出范围
db_tail=0;
}
}
if (sendit) { // 是否要传输一个字节
SBUF=db_buffer[db_head];
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
83
}
/*****************************************************************
Function: debug_output
Description: 串行口中断服务程序增加缓冲区头指针如果头指针不等与尾
指针输出下一个字节
Parameters: 无
Returns: 无
*****************************************************************/
void debug_output(void) interrupt 4 {
RI=0;
if (_testbit_(TI))
db_head++;
if (db_head==DB_MAXSIZE) { // 是否超出范围
db_head=0;
}
if (db_head!=db_tail)
SBUF=db_buffer[db_head]; // 送下一个字节
}
}
}
通过调用功能函数把数据块插入缓冲区中,功能函数中包含一个指针指向待传送的数
据,还包含一个计数器表明要传送字节的数量数据被拷贝到缓冲区中缓冲区中的尾指
针被相应的更新你可以根据RAM的大小和需要传送调试数据的多少调整缓冲区的大小
第4章的实时时钟系统没有外部RAM 你必须把缓冲区设置在内部RAM中这限制了一次传
送数据的数量
4 使用Monitor-51
有些时候我们设计系统既希望从代码段读出数据也希望往代码段写入数据这使
得系统相信自己只有一个存储段而不是两个这样做的好处是你可以使用一个简单的8051
程序把代码下载到存储区中这使你避免不断的转换编译烧写EPROM和测试
如果你的系统能够向代码区写入数据你应该考虑使用Keil C51自带的软件包
Monitor-51 这个程序允许你在目标板上运行代码和调试功能它要求你在代码区装入通
信和控制模块通信模块将通过串行口和PC进行通信你在PC运行另外一个程序
MON51.EXE 这个程序作为你目标板和PC之间的接口这样就相当有了一台仿真器
监视程序将使你可以看到各个存储段并改变他们的内容你可以查看SFRs的的值禁
能你的代码加入新的代码你还可以加入断点当你的系统挂起时你可以单步运行你
的程序所有这些的前提是你的系统必须可以对代码区进行写操作
监视程序可在没有SRAM的系统上通过设置在代码区内运行但这样的话你就不能
设置断点单步运行改变代码区的内容Monitor-51将控制一个串行口和一个定时器
同时还占用了2816字节的代码空间和256字节的XDATA区
调整你的Monitor-51 install.a51文件来适应你的系统当你的系统频率为11.059MHz
时它设置串行口的波特率为9600 此外它还将把在地址8000H以上的中断向量入栈如
果你不想这样可以更改install.a51文件头的常数来进行调整对于PC上的软件接口可
以查询手册在这里就不详细介绍了
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
84
5 利用I/0端口进行调试
如果你不能使用串行口做为调试端口你可以利用分立的I/O口进行调试把I/0口和
锁存器相连锁存器被分配一个外部地址我想至少可以找到一些空余的脚来显示系统的
执行点最为理想的方法是使用8个引脚在同一时间显示一个字节把这写引脚连接到LED
上这样易于观察此外你也可以使用示波器
我所做的很多系统都使用输出引脚来显示状态一般来说一个引脚来显示系统正
在执行引脚电平以一定的频率进行翻转你通过检查这个引脚以确定系统工作正常一
个引脚用来显示程序已经运行过某一点或程序正在等待输入等你也可以把寄存器的内容
发送到引脚上然后程序进入等待状态并观察引脚的数值来确定程序运行是否正常你必
须自己确定每个引脚在程序运行的每个状态的作用调试程序时要一段一段的进行这样
才便于你进行观察这和使用串行口进行调试是不同的
如果你有逻辑分析仪的话就再好不过了把用作调试的输出口和逻辑分析仪连起来
逻辑分析仪将记下他所见到的数据对输出的数据进行分析后你可以知道你程序运行的
状况
6 使用ICE
8051的在线仿真器的种类有很多这里不对他的使用方法做讨论关于这方面的书层
出不穷我们将对一些要点做一些说明
第一点对你将要进行仿真的代码段使用”debug”选项进行再编译对于包含了结构或
数组的C程序如果要对这些数据进行访问更改或检测须在汇编时加”objectextend”参
数这样将使系统在目标文件和以后产生的可执行文件中加入调试信息如果不这样做的
话你在调试窗口中看到的将不是C程序而是一些没有标号的汇编代码
在系统中安装ICE时要特别注意设置这些设置可使仿真器以系统晶振频率运行或
以仿真器内部时钟频率运行如果你的系统有外接晶振并且系统的运行完全依赖于这个
频率这时如果以仿真器频率运行程序的话程序将不会按你想象的那样工作了时钟发
生器将以错误的频率振荡串行通信将发生错误因为baud率已经改变了
同样你还可以使能仿真器的电源和复位功能这可使你对硬件做更多的测试
调试有看门狗或运行定时器的系统时要记住当你检查代码或数据时或单步运行时
时钟并没有停止运行它们还是会溢出并产生中断有时候给程序的调试带来很大的不便
所以测试的时候最好关闭看门狗定时器而对于时标的产生最好就是禁能定时器中断
如果你想购买仿真器我建议你买带有跟踪缓冲区的那种很多仿真器都带有从16K
到128K的跟踪缓冲区这些缓冲区存储执行的指令指令指针的值引脚和仿真器相连
的引脚的输入输出值分析这些数据可以发现系统的问题出自那里这样跟踪缓冲区的
功能相当于逻辑分析仪
7 结论
使用仿真器是对系统进行测试和集成最有效的方法但是你在进行系统调试时不要完
全依赖仿真器你有可能碰到没有仿真器或仿真器的作用不大的时候像一些对外围器件
如EEPROM 进行控制的系统硬件接口时序信号等这使你应该使用示波器或其它
一些仪器进行系统测试
这章给出了一些对你的工程进行调试的方法值得重声的一点是对实时时钟之类的系
统的调试不能光从书本上学还要多多积累经验书本只能给你指明方向你应根据不同
的情况采用不同的解决方法
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
85
第六章中断系统
1 介绍
这一章将讨论设计实时时钟系统中的一些问题其中很多都和软件有关其它一些和
硬件设计有关这一章的硬件设计集中在中断系统上将讨论如何使用中断事件和查询方
法来触发操作软件的讨论将集中在程序结构上
2 中断驱动系统和查寻系统
如果你的嵌入式系统是基于对输入信号做处理的基础上的那你就要决定你的系统
是采用中断的方式来接收数据好呢还是采用查询的方式好呢这需要你考虑两方面的问
题第一点是你系统对输入信号变化所作出的反应要多快如果要求在很短的时间内作出
反应那么最好使用中断的方式不管你系统的查询速度有多快它总是比中断的反应要
慢一些第二点你要知道你的输入信号的变化有多快如果它以接近1/10指令周期的频率
变化就要用中断查询方式了
有些系统有很多输入源而且看上去每个输入源都需要中断这就需要考虑如何在他
们之中分配中断或者对其中的一些输入源使用查询的方法换句话说就是必须对这些输入
源建立优先级举个例子有一个电机控制系统对电机传感器送过来的信号进行监视同
时又要接收主CPU送过来的状态查询请求我想你一定会把前者的优先级定得比后者高
因为丢失一个查询请求信号不会对系统造成什么很大的影响而丢失传感器信号可能会对
电机造成很大的影响如何建立优先级要从系统的高度去考虑
当你决定了输入源的优先级之后下一步要决定如何把信号引入处理器对输入信号
进行查询时那些需要快速查询的信号应直接接到端口引脚上处理器对端口的寻址只需
要一个指令周期对于查询速度要求不高的信号可通过锁存器由系统总线接入处理器从
前面的讨论知道设置DPTR需要两个指令周期对信号读写至少需要两个周期对信号的
查询相对就慢一些决定了信号的布局之后接下来要决定对信号的查询频率如果一个
信号每秒钟查询10000次另一个信号每秒钟查询一次根据总的信号查询时间把第一个
信号接到总线上把第二个信号接到处理器的引脚上是没有意义的一般说来如果要对
信号状态的改变作出反应信号的查询频率应是你所估计频率的两倍
对信号进行查询应根据不同的情况采取不同的方法如果输入信号是人发出的那么
10Hz的查询频率就够了人相当于一个很慢的I/O器件当他们使用按键向系统传送信息
时10Hz的查询速率和更高的查询速率没什么分别用户接口信号可在定时器中断或主循
环中查询
当查询变化频率很快或十分重要的信号时有两种方法可以在定时器中断中查询但
你中断的发生频率要很快此外你可以不断的对信号进行查询而不干任何其它事情这两
者的缺点是系统资源消耗太大如果你的系统是用电池供电的那么这两种方法都不是很
好这种情况下要把信号的重要程度加以区分重要的高速的信号接到外部中断口上
剩下的用查询的方法
这样做的目的是可以建立中断和查询输入触发事件之间的优先级我把重要的输入接
到INT0和INT1 你也可以用其它端口引脚来扩展外部中断或者共用外部中断8051对中断
的响应延时是很短的另外对那些变化很慢的信号也应通过中断接入处理器而不需要
用程序对它进行定期的查询
3 中断的电平和边沿触发
8051的外部中断支持两种触发方式一种是电平出发一种是边沿触发应该根据信号
的类型选择那种触发方式
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
86
3.1 电平触发中断
电平触发方式比较好理解处理器每个指令周期查询中断引脚当发现引脚电平为低
时触发中断如果信号从1边为0 一个周期后又变为1 中断并不会被清除直到中断执
行完毕并用RETI指令返回之后但是如果输入信号一直为低那么将一直触发中断当要
求中断服务的器件在中断服务结束一段时间之后才释放信号线时就会发生这种情况这时
你会发现中断被执行了多次所消耗的时间比预期的要长很多这时应使用边沿触发方式
3.2 边沿触发方式
当外部中断引脚电平由高向低变化时将触发中断处理器每个指令周期查询中断引
脚当前一个指令周期是引脚电平为高紧接着下一个指令周期检测到引脚电平为低时将
触发中断像前面所提到的那样这种方法适用于请求中断服务的器件在中断服务结束一
段时间之后才释放信号线时的情况因为这时只有下降沿才会触发中断如果你还想触发
下一个中断就必须把电平先置高
当设计中断结构时,你要记住边沿触发适用于那些器件发出的中断请求信号不需要软
件清除的场合最为普遍的例子是系统的时标这种信号一般由实时时钟电路产生这些
器件一般提供一个占空比为50%的信号即信号的一半是高电平另一半为低电平如果
使用电平触发将将产生很多中断这样即使不扰乱程序的运行也将浪费系统的资源
还有一种类似的情况是解码器系统通过解码器电路采样串行输入信号并把它转化
成并行输出每当信号达到某个标准的时候将产生一个中断信号问题在于达到标准的
信号是一个持久的信号如果设置成电平触发会引发一连串的中断
在器件要求中断很频繁的时候电平触发方式就比较好假使一个器件周期性的有
高频率的中断请求在你这个中断服务程序还没完成的时候下一个中断请求又来了这
样就不必把中断请求信号线置高如果设置为边沿出发方式你就检测不到中断信号
电平方式在多个器件共用一个中断入口的情况下比较有用当正在执行一个中断服务
程序的时候另外一个中断请求又来了这样信号线一直被置低边沿触发方式将检测不
到这个中断这时用电平触发方式就比较好因为信号线一直被置低当上一个中断服务
程序完成之后将立即执行下一个服务程序只要有中断请求这可使程序提供任何中断
服务这个过程将重复直到执行完所有的中断服务
经常性的你的系统所要处理的中断信号比现有的中断引脚要多这种情况在扩充了
中断引脚之后仍然存在这时利用一些方法来共用中断引脚你需要知道中断源的数量
中断触发的速度和在你的系统中加入什么器件等
4 共用中断
至少有3种方法来在多个输入之间共用中断信号每种方法都需要增加相应的组件
假使有两个输入信号当它们请求中断服务时把信号线电平置低当中断服务程序完
成之后再把信号线置高用与门把这两个信号连起来再把输出接到INT1 为了让处理器
分辨中断请求来自哪个信号分别把这两个信号接到控制器输入端口的引脚上下面的例
子采用的是P1.0和P1.1
这里设要求中断服务的器件直到中断服务完成之后才将信号线置高,因为在第一个器
件要求中断服务之后,第二个器件还可以申请中断这要求把INT1设置为电平触发或在
中断程序结束前检测P1.1和P1.0口这样两个中断都将被执行如果使用边沿触发的话
当一个中断正在执行时又产生另一个中断如果在中断程序结束前不检测P1.1和P1.0口
这个中断将不会被执行
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
87
图0-1 INT1被共用
像上面的例子把中断设置为电平触发中断服务程序见表0-1 注意如何通过改变
检测顺序来建立中断的优先级另外一旦完成了第一个中断服务程序之后将检测低优
先级的输入因为中断为电平触发如果在高优先级的中断执行期间又来一次这个中断
那么将始终执行这个中断
列表0-1
sbit SLAVE1 = P1^0; // 输入信号命名
sbit SLAVE2 = P1^1;
void int1_isr(void) interrupt 2 {
if (!SLAVE1) { // 先检测slave1
slave1_service();
}
if (!SLAVE2)
slave2_service();
}
}
可以更改中断服务程序通过加入do…while循环语句只要中断申请存在就不退出
中断服务程序这将导致一直中断系统将不能进行其它工作我们在设计系统时要整
体的进行考虑合理的执行中断而不应让中断占据所有的系统资源因为系统还要做其
它工作
前面的共享中断的方法还可以进行扩展把所有输入信号接到一个与门上并给每个
信号分配一个端口引脚如果碰到引脚不够用的情况可把引脚接到数据锁存器上还是
以电平方式触发中断这将使系统在软件和硬件上都变得复杂主要不同的是将通过数据
锁存器来读取输入信号
锁存器的优点是可让你加入一些新的硬件产生中断的信号被硬件记录下来然后通
过软件从锁存器中读取记录采取什么中断方式决定于接到中断输入口上信号的性质
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
88
图0-2 共享外部中断
在这个结构中中断服务程序从地址为8000H的锁存器中读入数据以决定哪个中断源
要求中断并可根据查寻的先后次序决定那个中断被优先执行下面是中断服务程序
列表0-2
#define INTREG 0x8000
unsigned char bdata intmask; // 声明一个可位寻址变量存放中断请求记录
sbit signal0 = intmask^0; // 设置位变量访问记录
sbit signal1 = intmask^1;
sbit signal2 = intmask^2;
sbit signal3 = intmask^3;
sbit signal4 = intmask^4;
sbit signal5 = intmask^5;
sbit signal6 = intmask^6;
sbit signal7 = intmask^7;
void int1_isr(void) interrupt 2 {
intmask=XBYTE[INTREG]; //读锁存器数据以决定
//中断的原因
if (signal0) { // 检测所有的中断源
signal0_isr();
}
...
if (signal7) {
signal7_isr();
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
89
reset_int(); // 执行中断逻辑的复位功能
}
中断逻辑控制的硬件要根据系统而定有些像上面的例子一样中断请求直到中断服
务程序完成之后才被释放而有些中断请求信号只是一个脉冲信号这样硬件必须锁住下
降沿脉冲并产生中断请求信号当中断执行完毕之后软件清除中断逻辑电路中的中
断请求信号只要有中断请求没有被响应并清除相应的中断信号中断请求逻辑电路就
将一直发出中断请求这样中断触发方式应该被设置成电平触发
上面例子中中断逻辑的实现很像商业性的中断控制主要的区别是上面的中断逻辑
每个输入信号的中断触发方式将不能改变如果你要把第3个信号的中断触发方式有电平
触发改变成边沿触发那你的硬件电路就要该变一个好的中断系统应该允许改变每个中
断的触发方式就像8051可以设置INT0和INT1那样
中断控制器应该可以通过硬件来使能或禁能中断当你要关闭某个中断时不再需要
修改软件或增加硬件这个功能已经包含在中断控制器中使用这种模块的系统要通过系
统总线和它接口如果一个系统使用了中断控制器那么系统在决定中断源时会花较长的
时间这样就增加了中断的延时所以对中断反应速度要求很高的输入应该直接接到处理
器上
6 扩充外部中断数
尽管Intel认为8051的外部中断数不应超过两个但肯定有方法可以使你的外部中断
数超过5个有两个简单的方法一是把定时/计数器中断做成外部中断二是把串行口中
断做成外部中断当然如果你还要使用他们以前的中断功能就不应这样做如果你需要一
个定时器和串行口那在设计系统时可把另一个定时器作为外部中断
扩展外部中断最简单的方法就是把定时器设置为计数模式然后把信号接到计数器相
应的引脚上T0或T1 为了使每出现一个从高到低的脉冲的时候产生一个中断把定时
器设置为自动重装模式令重装值为FFH 当计时器检测到从高到低的脉冲时定时器将
溢出这时将产生一个中断请求代码如下
列表0-3
#include reg51.h
void main(void) {
...
TMOD=0x66; // 两个定时/计数器都设置成8位模式
TH1=0xFF; // 设定重装值
TH0=0xFF;
TL0=0xFF;
TL1=0xFF;
TCON=0x50; // 开始计数
IE=0x9F; // 中断使能
...
}
/*****************************************************************
定时器0中断服务程序
*****************************************************************/
void timer0_int(void) interrupt 1 {
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
90
...
}
/*****************************************************************
定时器1中断服务程序
*****************************************************************/
void timer1_int(void) interrupt 3 {
while (!T1) { // 确保中断被清除
...
}
}
这种方法还是有一定的限制的第一点它只能是边沿出发所以当你需要的是一个
电平触发的中断时就要在中断中不断的对T0或T1进行采样直到它们变为高第二点
检测到下降沿和产生中断之间有一个指令周期的延时这是因为在检测到下降沿一个指令
周期之后计时器才加1
如果你使用的8052或8051单片机有多个定时器而且有外部引脚可以用这种方法来
扩充边沿触发的外部中断值得重申的一点是当使用定时器作为外部中断时它以前的
功能将不能使用了除非你用软件对它进行复用
使用串行口作为外部中断不像使用定时器那样直接RXD引脚将变成输入信号检测
从高到低的电平跳变把串行口设置为模式2 当检测到从高到低的电平跳变是8位数据
传输时间过后将产生中断当中断发生后由软件把RI清零下面是对UART设置和ISR结构
的代码
列表0-4
#include reg51.h
void main(void) {
...
SCON=0x90; // 模式2 允许接收
IE=0x9F; // 中断使能
...
}
void serial_int(void) interrupt 4 {
if (!_testbit_(RI)) {
...
}
}
像定时器系统一样用串行口中断作为外部中断也有它的缺点第一中断只能是边
沿触发第二输入信号必须保持5/8位传输时间为低因为串行口必须确认输入信号是
一个起始位第三检测到电平跳变之后要等8个位传输时间后UART才请求中断还有
信号为低的时间不应超过9位数据传输时间对UART来说这种方法相当于从RXD脚传送进
一个无效字节这样对时间的要求更高了这些限制取决于你的系统的的频率因为传输
的波特率取决于系统频率当UART的模式改变和使用内部定时器时会有不同的时间限制
但延时只会加长不会缩短
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
91
7 中断服务程序
很多新手在设计中断服务程序时不知道该注意些什么主要问题是哪些功能应该放在
中断程序中哪些功能应该放在主程序中要把握好这一点可不是那么容易一般来说中
断服务程序应该做最少量的工作这样作有很多好处首先你的系统对中断的反应面更
宽了有些系统如果丢失中断或对中断反应太慢将产生十分严重的后果这时有充足的
时间等待中断是十分重要的其次它可使你的中断服务程序的结构简单不容易出错
中断程序中放入的东西越多它们之间越容易起冲突简化中断服务程序意味着你的软件
中将有更多的代码段但你可把这些都放入主循环中中断服务程序的设计对你系统的成
败有至关重要的作用你要仔细考虑各中断之间的关系和每个中断执行的时间特别要注
意那些对同一个数据进行操作的ISR
假设你的系统从UART接收了一系列数据需要从中得到重要的信息并响应它们中断
服务程序从SBUF中读取数据并把它放到循环队列中软件的主调用层负责检查队列取得
数据进行分析当信息接收完毕然后进行相应的处理也可有ISR进行数据分析再
把数据放入队列中由主程序进行处理但我不主张使用第二种方法那样将花费很多时间
有些时候由于时间的限制或和其它中断的关系的原因无法将一些操作从ISR中分
离出来例如有一个系统当外围电路接收到数据之后申请中断并且每20ms 向处理器
发送一个数据单位我想你应该会接收完所有的数据后才离开中断否则很容易丢失数据
可以利用8051的中断优先级来解决这个问题
另外一个留给ISR做的应该是对共享数据的操作举个例子如果一个系统有好几个
中断其中有两个中断有同样的优先级并对同一个数据结构进行操作当A/D转换单元
完成转换之后将引发其中一个中断每10ms发生一次系统记录转换结果并把结果串行
输出另一个中断是系统时标检查共用的数据结构中是否有新的转换数据当有新的数
据出现时把数据放入打包并初始化串行传输可以看出这两个ISR不应同时使用队列
在这个例子中输入ISR读取数据并完成数据的入队列操作另一个ISR从队列中取数据并构
造消息初始化串行口
8 结论
这章主要讨论了如何增强8051的中断功能.把这些技巧和以前的讨论结合起来如仿真
外部中断优先级可使你拥有比8051设计者想到的更多功能在设计系统的中断系统的时
候应该注意输入信号和8051中断源的匹配同时还有软件的设计像选择中断优先级
中断服务程序的设计软件和硬件应该结合起来设计总之中断系统的设计对实时时钟
嵌入式系统来说是十分关键的
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
92
第7章串行口
1 介绍
8051系统的主要传输方式不是并行口或共享存储区而是8051的串行传输方式第二
章提到过内置的UART是十分灵活的可以和其它系统进行高速的通信这章将讨论在系
统间进行数据传输的软件设计方法
2 慢速串行口和PC的接口
在很多嵌入式应用中处理器把时间和数据报告给主机通常是PC 主机也通过串行
连接向处理器传输命令和数据通常使用像RS-232这样的电压标准在8051系统和通用系统
间进行通信如果通信线路不是很长的话8051可以不需要RS-232驱动器而只需要简单
的电路就可与PC通信很多PC系统并不完全遵循RS-232电压标准这将简化电路接口从
PC输出的12V电压数据通过降压变为5V以下
图0-1 PC接口
当简单的接口电路设计好了之后要设计相应的软件来控制数据的传输处理输入数
据最简单的方法是假设你的传输协议传输的第一个字节是要传输的字节数接收完第一个
字节产生串行传输中断然后以查询的方式接收输入数据对输出数据也用相似的方法
当串行传输开始时向SBUF中写入一个字节数然后查询SCON看什么时候开始传送下一个
数据当所有字节传送完毕后结束循环
上面的软件设计适用于只处理串行通信的系统这种软件设计结构比较简单但是对
于复杂的系统查询方式就不适用了下面的设计更好接收数据时每个输入字节产生一
个串行中断中断服务程序从SBUF中读取数据并确认数据的有效性当数据有效时把
数据放入队列中由主程序去执行发送数据用类似的方法把要发送的数据放入队列中
第一个字节发送完后产生中断只要队列中还有数据中断服务程序从队列中读出一个字
节写入SBUF
这个系统允许处理器除了串行传输之外还可处理其它任务一般来说串行口和其它外
围器件比起来是一个很慢的设备只要你的串行波特率不是特别快如300K 每个字节
间就有足够的时间处理其它任务
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
93
设前面第4章所讲的时钟系统是监控系统的一部分该监控系统通过PC查询其它设备
如图0-2所示在这里我们只关心时钟部分
图0-2主/从串行通信
在这个新的监控系统中时钟通过RS-232和PC进行通信时钟的设计如图0-3
图0-3 时钟作为从设备
PC从时钟处读取数据设置时钟的时间复位时间为0 传送32个字符信号进行显示
应该注意串行通信线路上不止时钟一个设备要把自己的数据和其它设备的数据区分开
所以被传送数据的结构应该使设备可以鉴别数据是否是自己的被传送数据的第一个字节
是同步信号包含了被寻址器件的地址时钟的地址是43H 信息中还包含了命令字节
数据的多少数据本身和一个校验字节典型的信息结构如下所示
表0-1
对所有从PC传送过来的命令必须返回一个应答信号时钟对上面的列出的4个信号
负责(时间请求,时间设置,时间复位,时间显示).信息的格式如下:
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
94
表0-2
表0-3
表0-4
表0-5
表0-6
表0-7
明白了数据流和时钟处理器的责任现在可以设计中断服务程序了中断服务程序对
数据流进行分析在此可以使用一个简单的有限状态图(FSA) FSA根据输入从一个状态转
移到另一个状态的软件FSA有一个初始状态它寻找同步字节和与下部分信号相关的中
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
95
间信号初始状态将读取校验和字节并修改它如果所接收字节不遵守有效信号的结构
FSA就回到初始状态并开始寻找下一个同步字节
这种串行接收的原理很容易实现如果用C来写ISR的话我们要声明一些变量来保存
系统当前的状态给每个状态一个号码把号码保存在变量中当输入字节引发中断后FSA
根据保存的系统状态决定系统的下一状态把状态变量的类型声明为unsigned char 如
果用汇编编写程序的话可以通过更加有效的跳转查表来完成但你会发现程序大小和执
行速度不比用C编写的程序好多少但如果你处理的是高速的串行通信系统就另当别论
图0-4 接收FSA
在本例中时钟以波特率9600传送数据传送一个字节只需要1.042ms 晶振频率为
11.059MHz 指令执行周期为1.085us 在每个中断之间有960个指令周期有足够的时间
保存FSA 下面是串行ISR的代码
列表0-1
// 定义FSA状态常量
#define FSA_INIT 0
#define FSA_ADDRESS 1
#define FSA_COMMAND 2
#define FSA_DATASIZE 3
#define FSA_DATA 4
#define FSA_CHKSUM 5
// 定义信号分析常量
#define SYNC 0x33
#define CLOCK_ADDR 0x43
// 定义输入命令
#define CMD_RESET 0x01
#define CMD_TIMESYNC 0x02
#define CMD_TIMEREQ 0x03
#define CMD_DISPLAY 0x04
#define CMD_ACK 0xFF
#define RECV_TIMEOUT 10 /* define the interbyte timeout */
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
96
unsigned char
recv_state=FSA_INIT, // 当前状态
recv_timer=0, // 时间计数
recv_chksum, // 保存当前输入的校验值
recv_ctr, // 接收数据缓冲区的索引
recv_buf[35]; // 保存接受数据
unsigned char code valid_cmd[256]={ // 数组决定当前的命令字节是否有效
// 如果相应的输入是1 那么命令字节
// 有效
0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00 - 0F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 - 1F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20 - 2F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 30 - 3F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40 - 4F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50 - 5F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60 - 6F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70 - 7F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 8F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90 - 9F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A0 - AF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B0 - BF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C0 - CF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D0 - DF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E0 - EF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F0 - FF
};
/*****************************************************************
功能: serial_int
描述: 运行串行口FSAs.
参数: none.
返回: nothing.
影响: none.
*****************************************************************/
void serial_int(void) interrupt 4 {
unsigned char data c;
if (_testbit_(TI)) {
// 处理发送任务
}
if (_testbit_(RI)) {
c=SBUF;
switch (recv_state) {
case FSA_INIT: // 是否是同步字节
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
97
if (c==SYNC) { // 是同步字节
recv_state=FSA_ADDRESS; // 进入下一个状态
recv_timer=RECV_TIMEOUT; // 最大间隔时间
recv_chksum=SYNC; // 设置初始化校验值
}
break;
case FSA_ADDRESS: // 是否是地址
if (c==CLOCK_ADDR) { // 是时钟地址
recv_state=FSA_COMMAND; // 进入下一个状态
recv_timer=RECV_TIMEOUT; // 最大时间间隔
recv_chksum+=c; // 保存校验值
} else { // 信息不是给时钟的
recv_state=FSA_INIT; // 回到初始状态
recv_timer=0; // 清除最大时间间隔
}
break;
case FSA_COMMAND: // 是否是命令
if (!valid_cmd[c]) { // 确认命令是否有效
recv_state=FSA_INIT; // 复位FSA
recv_timer=0;
} else {
recv_state=FSA_DATASIZE; // 进入下一个状态
recv_chksum+=c; // 更新校验值
recv_buf[0]=c; // 保存命令
recv_timer=RECV_TIMEOUT; // 设置时间间隔
}
break;
case FSA_DATASIZE: // 发送的字节数
recv_chksum+=c; // 更新校验值
recv_buf[1]=c; // 保存字节数
if (c) { // 如果有数据段
recv_ctr=2; // 设置查询字节
recv_state=FSA_DATA; // 进入下一个状态
} else {
recv_state=FSA_CHKSUM;
}
recv_timer=RECV_TIMEOUT;
break;
case FSA_DATA: // 读入数据
recv_chksum+=c; // 更新校验值
recv_buf[recv_ctr]=c; // 保存数据
recv_ctr++; // 数据计数值
if ((recv_ctr-2)==recv_buf[1]) { // 接收数据计数器减偏移量
// 是否等于datasize
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
98
recv_state=FSA_CHECKSUM; // 数据接收完毕
}
recv_timer=RECV_TIMEOUT; // 设置时间间隔
break;
case FSA_CHECKSUM: // 读校验字节
if (recv_chksum==c) { // 核对校验字
c=1; // 用c表明是否要建立应答信号
switch (recv_buf[0]) { // 按指令执行
case CMD_RESET: // 复位时钟为0
break;
case CMD_TIMESYNC: // 设置时钟
break;
case CMD_TIMEREQ: // 报告系统
break;
case CMD_DISPLAY: // 显示ASCII 信息
break;
}
if (c) {
// 应答
}
}
default:
recv_timer=0; // 复位FSA
recv_state=FSA_INIT;
break;
}
}
}
所运行的代码充分反应了图0-4中所展示的模型当然应该还有指令的执行代码和输
出数据的代码这里只是给出你接收数据代码的结构
向PC回传数据更加简单由串行中断服务程序完成时钟假设,同一时刻PC只会传送
一个有效命令给它这样就不必担心维护一大堆输出数据了这个假设简化了这个例子
当需要发送数据的时候只需要把数据放入发送缓冲区中并设置一个变量保存发送的字
节数把第一个字节写入SBUF并设置校验字节向SBUF写第一个字节就像启动了水泵一样
它将产生第一个中断当触发了第一个中断之后串行中断将自动完成数据的发送下面
是串行口中断的代码结构
列表0-2
// 定义信号分析常量
#define SYNC 0x33
#define CLOCK_ADDR 0x43
unsigned char
trans_buf[7], // 保存输出数据
trans_ctr, // 数据缓冲区索引
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
99
trans_size, // 发送数据的个数
trans_chksum; // 计算输出校验
/*****************************************************************
功能: serial_int
描述运行串行口FSAs
参数: 无.
返回: 无.
影响: 无.
*****************************************************************/
void serial_int(void) interrupt 4 {
unsigned char data c;
if (_testbit_(TI)) { // 发送中断
trans_ctr++; // 增加数据索引
if (trans_ctrtrans_size) { // 数据是否发送完毕
if (trans_ctr==(trans_size-1)) { // 输出校验字节
SBUF=trans_chksum;
} else {
SBUF=trans_buf[trans_ctr]; // 发送当前字节
trans_chksum+=trans_buf[trans_ctr]; // 更新校验字节
}
}
}
if (_testbit_(RI)) {
c=SBUF;
switch (recv_state) {
// 接收FSAs
case FSA_CHECKSUM: // 读校验字节
if (recv_chksum==c) { // 核对校验字节
c=1; // 用c表明是否要建立应答信号
switch (recv_buf[0]) { // 执行指令
case CMD_RESET: // 复位时钟
break;
case CMD_TIMESYNC: // 设置时钟
break;
case CMD_TIMEREQ: // 报告时间
c=0;
break;
case CMD_DISPLAY: // 显示ASCII 信息
break;
}
if (c) { // 建立应答
trans_buf[0]=SYNC; // 信息头
trans_buf[1]=CLOCK_ADDR;
trans_buf[2]=CMD_ACK;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
100
trans_buf[3]=1;
trans_buf[4]=recv_buf[1];// 被回应的命令
trans_ctr=0; // 设置缓冲区指针到第一个字节
trans_size=6; // 总共发送6个字节
SBUF=SYNC; // 发送起始字节
trans_chksum=SYNC; // 初始化校验值
}
}
default:
recv_timer=0;
recv_state=FSA_INIT;
break;
}
}
}
如你所见输出数据的ISR代码十分简单代码所占的空间也很小和接收数据一样
校验字节也在数据的传输过程中建立
ISR程序在时钟系统的限制下顺利的运行注意中断中命令的执行可以避免设计上的
很多问题串行中断服务程序和定时器中断服务程序一样都可以修改时间把这两个中断
的优先级设为一样这样任何一个中断在修该时间的时候都不用担心另一个也在这么做
对数据的处理可以在中断程序中但是命令的执行应该放到主程序中去执行因为这些命
令的执行会花去太多的时间这样就很可能丢失其它的中断假设PC送一系列字符给LCD
显示时钟就有可能丢失一两个时标中断因为显示要花去很多时间在一些更复杂的系
统中应该把输入数据放入队列中然后由主程序去处理在这里我们认为PC直到被
告知对方已收到当前信息后才发送下一个信息这样队列就仅仅是一个当前信息的缓冲
区
新的中断服务程序和以前的很相似不同之处在于当接收完毕数据之后它会把数据
拷贝到另一个缓冲区中并置位缓冲区有效标志位以前在中断服务程序中的命令执行代码
现在放到新的功能段中当主程序检测到第二个缓冲区中有数据的时候调用该功能段下
面是新的ISR代码
列表0-3
// 定义状态常量
#define FSA_INIT 0
#define FSA_ADDRESS 1
#define FSA_COMMAND 2
#define FSA_DATASIZE 3
#define FSA_DATA 4
#define FSA_CHKSUM 5
// 定义信号分析常量
#define SYNC 0x33
#define CLOCK_ADDR 0x43
// 定义命令常量
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
101
#define CMD_RESET 0x01
#define CMD_TIMESYNC 0x02
#define CMD_TIMEREQ 0x03
#define CMD_DISPLAY 0x04
#define CMD_ACK 0xFF
#define RECV_TIMEOUT 10 /*定义字节间的最大时间间隔*/
unsigned char
recv_state=FSA_INIT, // 当前状态
recv_timer=0, // 时间间隔计数
recv_chksum, // 输入数据的校验字节
recv_size, // 输入数据字节数
recv_ctr // 数据缓冲区指针
recv_buf[35]; // 输入数据缓冲区
unsigned char
trans_buf[7], // 输出数据缓冲区
trans_ctr, // 输出数据指针
trans_size, // 输出数据字节数
trans_chksum; // 输出数据的校验字节
unsigned char code valid_cmd[256]={ // 如果输入命令有效则为1
0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00 - 0F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 - 1F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20 - 2F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 30 - 3F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40 - 4F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50 - 5F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60 - 6F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70 - 7F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 8F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90 - 9F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A0 - AF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B0 - BF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C0 - CF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D0 - DF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E0 - EF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F0 - FF
};
/*****************************************************************
功能: serial_int
描述: 运行串行口FSAs.
参数: 无.
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
102
返回无.
影响无.
*****************************************************************/
void serial_int(void) interrupt 4 {
unsigned char data c;
if (_testbit_(TI)) { // 输出中断
trans_ctr++; // 输出缓冲区指针加1
if (trans_ctrtrans_size) { // 数据是否输出完毕
if (trans_ctr==(trans_size-1)) { // 输出校验字节
SBUF=trans_chksum;
} else {
SBUF=trans_buf[trans_ctr]; // 输出当前字节
trans_chksum+=trans_buf[trans_ctr]; // u更新校验字节
}
}
}
if (_testbit_(RI)) {
c=SBUF;
switch (recv_state) {
case FSA_INIT:
if (c==SYNC) { // 同步字节
recv_state=FSA_ADDRESS; // 下一个状态
recv_timer=RECV_TIMEOUT;
recv_chksum=SYNC;
}
break;
case FSA_ADDRESS:
if (c==CLOCK_ADDR) { // 时钟地址
recv_state=FSA_COMMAND;
recv_timer=RECV_TIMEOUT;
recv_chksum+=c;
} else { // 不是给时钟的
recv_state=FSA_INIT; // 返回初始状态
recv_timer=0;
}
break;
case FSA_COMMAND:
if (!valid_cmd[c]) { //
recv_state=FSA_INIT; /
recv_timer=0;
} else {
recv_state=FSA_DATASIZE;
recv_chksum+=c;
recv_buf[0]=c; // s保存命令
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
103
recv_timer=RECV_TIMEOUT;
}
break;
case FSA_DATASIZE: // 字节的个数
recv_chksum+=c;
recv_buf[1]=c;
if (c) { // 是否有数据
recv_ctr=2;
recv_state=FSA_DATA;
} else {
recv_state=FSA_CHKSUM;
}
recv_timer=RECV_TIMEOUT;
break;
case FSA_DATA: // 读取数据
recv_chksum+=c;
recv_buf[recv_ctr]=c; // 保存数据
recv_ctr++;
if ((recv_ctr-2)==recv_buf[1]) { //数据接收完毕
recv_state=FSA_CHECKSUM;
}
recv_timer=RECV_TIMEOUT;
break;
case FSA_CHECKSUM: // reading in checksum
if (recv_chksum==c) { // 校验字节核对正确
memcpy(msg_buf, recv_buf, recv_buf[1]+2);
msg_buf_valid=1;
}
default: //复位
recv_timer=0;
recv_state=FSA_INIT;
break;
}
}
}
现在ISR只负责分析输入数据和从如何缓冲区发送数据命令的执行代码被放入功能
段中由主程序去调用下面为该功能段和主程序的代码
列表0-4
/*****************************************************************
Function: execute_cmd
Description: 命令执行
Parameters: 无.
Returns: 无.
Side Effects: 无.
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
104
*****************************************************************/
void execute_cmd(void) {
bit need_ack=1;
switch (recv_buf[0])
case CMD_RESET: // 复位
EA=0; // 禁止中断
curtime.sec=curtime.min=curtime.hour=0;
timeholder=curtime;
EA=1; // 开放中断
break;
case CMD_TIMESYNC: // 设置时间
EA=0;
curtime.hour=recv_buf[3];
curtime.min=recv_buf[4];
curtime.sec=recv_buf[5];
timeholder=curtime;
EA=1;
break;
case CMD_TIMEREQ: // 报告时间
trans_buf[0]=SYNC;
trans_buf[1]=CLOCK_ADDR;
trans_buf[2]=CMD_TIMEREQ; // 发送当前时间
trans_buf[3]=3;
EA=0;
trans_buf[4]=curtime.hour;
trans_buf[5]=curtime.min;
trans_buf[6]=curtime.sec;
EA=1;
trans_ctr=0;
trans_size=8; // 发送8个字节
need_ack=0;
break;
case CMD_DISPLAY: // 显示ASCII
recv_buf[34]=0; // 在字符串最后设置一个空字符以结束显示
printf(\xFF%s, recv_buf[2]); // 显示字符串
display_time=100;
break;
}
if (need_ack) { // 建立应答消息
trans_buf[0]=SYNC;
trans_buf[1]=CLOCK_ADDR;
trans_buf[2]=CMD_ACK;
trans_buf[3]=1;
trans_buf[4]=recv_buf[0];
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
105
trans_ctr=0; // 发送数据指针
trans_size=6;
}
SBUF=SYNC; // 开始发送
trans_chksum=SYNC; // 校验字节
/*****************************************************************
Function: 主程序
Description: 初始化8051 使能相应的中断源进入空闲模式
每次进入空闲模式之前是否要运行命令或更新显示
Parameters: 无.
Returns: 无.
*****************************************************************/
void main(void) {
disp_init(); // 初始化显示
TMOD=0x21; // 定时器016位模式定时器1
// 为波特率发生器
TCON=0x55; // 开启定时器
TH1=0xFD; // 定时器1波特率为9600
SCON=0x50; // 串行口模式1
IE=0x92; // 使能定时器0中断
for (;;) {
if (_testbit_(msg_buf_valid)) { // 有没有新的数据
execute_cmd(); // 执行命令
}
if (disp_update) {
disp_time(); // 更新显示
}
PCON=0x01; // 进入空闲模式
}
}
上面在主程序中处理信息的方法实现起来十分简单在由输入输出驱动的中断系统中
十分重要因为和PC之间的接口相对比较慢主循环有足够的时间去执行其它的任务不
可否认有很多系统的中断比时钟系统要多得多下面将讨论高速串行数据传输的应用
3 高速串行I/O
前面所讨论的系统对时钟定期的查询从PC来的串行信号并非每次都是针对时钟的因
为总线上还有其它器件假设系统的设计者认为PC正在忙于处理用户接口和数据所以和
器件的通信很慢为了改进它设计一个基于8051的简单系统这个电路的工作是代替PC
作为主控器这个新器件称为系统监控器系统的框图看起来复杂一些原来的时钟系统
不变
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
106
图0-5 新的串行网络
仍然通过串行连接进行查询但波特率更高一些系统中的所有器件都使用11.059MHz
晶振串行口可彼此直接连接这意味着和PC连接时所需要的分压和反向电路不再需要了
电路图如下
图0-6 8051作为高速从器件
因为是和其它8051相连UART可用模式2进行通信波特率将达到345593.75baud 发
送每个字节的时间为31.829us 不到30个指令周期而波特率为9600时每发送一个字
节需要1.04ms 因为发送时间缩短了器件之间的接口要稍做改动使总线上可容纳多个
器件8051的串行口被初始化为模式2 一个起始位8个数据位一个校验位一个停止
位当处理器接收完前面的数据时校验位用来产生中断每个器件都要按这种格式输出
数据还要依照以下条件第一每个信息的同步字节校验位必须置位第二信息中的其
它字节的校验位必须清零当收到校验位为1的字节时所有器件都产生中断
因为数据传送速度有了很大的提高时钟的串行接收过程将要有所改变当校验位置
位的字节引发中断时其余的数据将被保存在数据队列中
修改时钟的主程序来适应串行通信模式注意定时器0不用来产生波特率可用做
其它用途我们可以用它来对每个字节之间的时间间隔计时确保时钟不做无限制等待
列表0-5
// 信号常量
#define SYNC 0x33
#define CLOCK_ADDR 0x43
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
107
// 命令常量
#define CMD_RESET 0x01
#define CMD_TIMESYNC 0x02
#define CMD_TIMEREQ 0x03
#define CMD_DISPLAY 0x04
#define CMD_ACK 0xFF
// 字节间的最大时间间隔128指令周期
#define TO_VAL 0x80
unsigned char data recv_chksum, // 校验字节
recv_buf[35]; // 输入数据
unsigned char trans_buf[7], // 输出数据
trans_ctr, // 输出数据缓冲区指针
trans_size, // 输出字节数
trans_chksum; // 输出校验字节
unsigned char code
0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00 - 0F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 10 - 1F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20 - 2F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 30 - 3F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40 - 4F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 50 - 5F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60 - 6F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 70 - 7F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 80 - 8F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 90 - 9F
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A0 - AF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B0 - BF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // C0 – CF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // D0 - DF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // E0 - EF
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F0 - FF
};
/*****************************************************************
Function: ser_xmit
Description: 处理串行输出中断.
Parameters: none.
Returns: nothing.
Side Effects: none.
*****************************************************************/
void ser_xmit(void) {
trans_ctr++; // 移动输出缓冲区指针
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
108
if (trans_ctrtrans_size) { // 数据是否发送完毕
if (trans_ctr==(trans_size-1)) { // 发送校验字节
SBUF=trans_chksum;
} else {
SBUF=trans_buf[trans_ctr]; // 发送当前字节
trans_chksum+=trans_buf[trans_ctr]; // 更新校验字节
}
}
}
/*****************************************************************
Function: push_msg
Description: 把当前数据入队列
Parameters: none.
Returns: nothing.
Side Effects: none.
*****************************************************************/
void push_msg(void) {
memcpy(msg_buf, recv_buf, recv_buf[1]+2);
msg_buf_valid=1; // 缓冲区数据有效
recv_chksum=SYNC+CLOCK_ADDR;
}
/*****************************************************************
Function: execute_cmd
Description: 执行发送过来的命令
Parameters: none.
Returns: nothing.
Side Effects: none.
*****************************************************************/
void execute_cmd(void) {
bit need_ack=1;
switch (recv_buf[1])
case CMD_RESET: // 复位时钟
EA=0; // 更改时间是禁止中断
curtime.sec=curtime.min=curtime.hour=0;
timeholder=curtime;
EA=1;
break;
case CMD_TIMESYNC: // 设置时钟
EA=0;
curtime.hour=recv_buf[3];
curtime.min=recv_buf[4];
curtime.sec=recv_buf[5];
timeholder=curtime;
EA=1;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
109
break;
case CMD_TIMEREQ: // 报告时间
trans_buf[0]=SYNC;
trans_buf[1]=CLOCK_ADDR;
trans_buf[2]=CMD_TIMEREQ;
trans_buf[3]=3;
EA=0;
trans_buf[4]=curtime.hour;
trans_buf[5]=curtime.min;
trans_buf[6]=curtime.sec;
EA=1;
trans_ctr=0;
trans_size=8; // 一共8位
need_ack=0;
break;
case CMD_DISPLAY: // 显示ASCII 字符
recv_buf[34]=0;
printf(\xFF%s, recv_buf[2]); // 显示字符串
display_time=100; // 设置显示时间为5秒
break;
}
if (need_ack) { // 建立应答消息
trans_buf[0]=SYNC;
trans_buf[1]=CLOCK_ADDR;
trans_buf[2]=CMD_ACK;
trans_buf[3]=1;
trans_buf[4]=recv_buf[1];
trans_ctr=0;
trans_size=6; // 一共发送6个字节
}
SBUF=SYNC; // 发送第一个字节
trans_chksum=SYNC; // 初始化校验字节
}
/****************************************************************
Function: main
Description: 主程序的入口初始化8051 设置中断
进入空闲模式每次中断后查看是否要
更新显示
Parameters: None.
Returns: Nothing.
****************************************************************/
void main(void) {
disp_init(); // 设置显示
TH1=TO_VAL; // 设置时间间隔
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
110
TMOD=0x21; // 定时器1为8位模式
// 定时器0为16位模式
TCON=0x15;
SCON=0xB0; // UART模式2
IE=0x92; // 使能串行口和定时器中断
for (;;) {
if (_testbit_(msg_buf_valid))
execute_cmd();
}
if (disp_update) {
disp_time(); // 显示新时间
}
PCON=0x01; // 进入空闲模式
}
}
图0-7 新的FSA
主程序的改变相对比较简单另外处理串行传输中断的代码在新的串行中断服务程
序中功能函数push_message 提供给ISR使用它把目前的数据拷贝到队列中供以后使
用如果说这些改变都比较简单的话中断程序的改变就比较多了现在中断函数必须用
汇编语言编写当TI引起中断时将调用C函数ser_xmit发送下一个字节当接收数据时
因为每个字节之间相隔的时间很短所以要用汇编来编写
下面是用汇编编写的新的ISR的列表注意定时器0现在用来为每个接收字节间隔时间
的定时如果定时器溢出当前的接收过程停止FSA回到初始状态如图0-7 避免无限
制的等待
用汇编来编写代码保证了不会因为代码过长而丢失数据UART在模式2下每个字节
之间间隔的时间很短不到30个指令周期如果你在从SBUF中读取数据前用去了超过30
个指令周期那么你就丢失了一个字节因此每个指令周期都很宝贵下面是新的代码
列表0-6
EXTRN DATA (recv_chksum) ; 用来校验输入数据
EXTRN DATA (recv_buf) ; 存放输入数据
EXTRN CODE (ser_xmit) ; 处理发送中断
EXTRN CODE (valid_cmd) ; 执行命令
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
111
EXTRN CODE (push_msg) ; 把数据拷贝到队列中
SYNC EQU 33H
CLK_ADDR EQU 43H
CSEG AT 23H
ORG 23H
LJMP SER_INTR ; 装载中断服务程序
PUBLIC SER_INTR
?PR?SER_INTR?SER_INTR SEGMENT CODE
RSEG ?PR?SER_INTR?SER_INTR
;*****************************************************************
; Function: readnext
; Description: 从串行口读入数据如果超时置位进位标志
; Parameters: none.
; Returns: 读取的数据放入R0所指向的地址中
; Side Effects: none.
;*****************************************************************
readnext: CLR C ; 清除返回标志位
MOV TL1, TH1 ; 使用T1作为时间间隔计时器
SETB TR1
RN_WAIT: JBC RI, RN_EXIT ; 如果RI 置位, 接收到一个字节
JBC TF1, RN_TO ; 等待时间是否溢出
RN_TO: SETB C ; 置位时间溢出标志位
CLR TR1
RET
RN_EXIT: MOV A, SBUF
CLR TR1
RET
;*****************************************************************
; Function: SER_INTR
; Description: 8051 的串行口中断
; Parameters: none.
; Returns: nothing.
; Side Effects: none.
;*****************************************************************
SER_INTR: JBC RI, RECV_INT ; 是否是接收中断
CHK_XMIT: JBC TI, XMIT_INT ; 是否是发送中断
RETI
XMIT_INT: LCALL ser_xmit
RETI
; 根据堆栈中的数据多少
; 跳转到下面代码段相应
; 的位置
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
112
CHK_XMIT3: POP DPL
POP DPH
CHK_XMIT2: POP 00H
CHK_XMIT1: POP ACC
SETB SM2
JMP CHK_XMIT ; 恢复堆栈后检测发送中断
RECV_INT: PUSH ACC
MOV A, SBUF ; 保存接收的数据
CJNE A, #SYNC, CHK_XMIT1 ; 如果不是同步字节
; 则退出接收程序
CLR SM2 ; 清除校检位
PUSH 00H
MOV R0, #recv_buf ; 把接收缓冲区的基址装入R0
CALL readnext ; 读下一个字节
JC CHK_XMIT2 ; 定时器溢出
CJNE A, #CLK_ADDR, CHK_XMIT2 ; 一定要是时钟地址
CALL readnext
JC CHK_XMIT2
PUSH DPH
PUSH DPL
MOV DPTR, #valid_cmd ; 确认命令字节的有效性
MOVC A, @A+DPTR ; 用命令字节作为偏移量
JZ CHK_XMIT3 ; 如果表中的值是1
; 那么命令有效
MOV @R0, A ; 保存命令字节
ADD A, recv_chksum ; 更新校验字节
MOV recv_chksum, A
INC R0 ; 移动数据指针
CALL readnext ; 接收字节数量
JC CHK_XMIT3
MOV @R0, A ; 保存数据
ADD A, recv_chksum
MOV recv_chksum, A
MOV A, @R0 ; 如果字节数为0 接收
; 校验位
JZ RECV_CHK
MOV DPL, A ; 保存字节数作为计数器
RECV_DATA: INC R0 ; 移动指针
CALL readnext
JC CHK_XMIT3
ADD A, recv_chksum
MOV recv_chksum, A
DJNZ DPL, RECV_DATA ; 是否还有要接收的字节
RECV_CHK: CALL readnext
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
113
JC CHK_XMIT3
CJNE A, recv_chksum, CHK_XMIT3 ; 接收数据有效
CALL push_msg ; 数据正确放入队列
JMP CHK_XMIT3
END
上面的代码很小有两个优点第一每个字节之间花费的指令周期很少第二代
码简洁好维护查询和接收字节判断间隔时间溢出都被放入功能_readnext中这个
功能等待下一个字节如果间隔时间溢出的话置位无效标志位接收到的字节被放入累加
器中检查标志位就可知道累加器中的数据是否有效如果readnext返回无效值就退出
数据接收过程用累加器来返回接收值可直接对该值进行很多检测操作如果要把值存
到其它地方只须直接从累加器中移出
在这个主从系统中时钟是作为从设备对从设备来说因为总线由主设备控制所
以通信控制比较简单对主设备来说控制也不复杂从设备只有在主设备的命令下才能
进行通信这样就避免了冲突下一章我们将讨论在没有主设备的串行通信系统中如
何避免冲突
4 结论
本章介绍了如何在8051的模块之间8051和PC之间进行通信的简单串行连接PC的接
口很简单但可以完成这个工作如果你想要更加完整的RS-232接口有很多专门的接口
芯片像National Semiconductor公司的芯片等可以进行接口电平转换下一章我们
将讨论比现在更加复杂的利用UART进行网络通信设计如果你的系统需要进行大量通信
的话将会对此很感兴趣
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
114
第八章8051的网络设计
1 复合串行端口
假设第8章的串行系统的主控器需要把从设备的数据传送到监视器中又因为接口的
复杂性监视器是一台PC PC将定期的送数据和命令给串行通信控制器系统监视器
然后系统监视器返回PC所需要的数据系统监视器和PC之间的连接就和从设备与系统监视
器的连接一样但有两处不同第一PC初始化所有的串行通信系统监视器初始化所有
挂接在它上面的从设备的串行通信第二PC使用标准的RS-232串行通信协议这样系统
监视器准备在任何时候接收从PC传送过来的波特率为9600的10位结构数据这对它来说是
个挑战因为它还要不断的查询它的从设备
对系统监视器来说可以采取两种方法第一种是增加一个RS-232类型的UART接口当
有数据输入或输出时它将产生一个中断第二种方法是复用8051的UART接口这个接口
同时为PC和系统监视器的从设备提供通信服务一般来说那些认为越便宜部件越少就
越好的人都会选择第二种方法毕竟这省去了一个外部UART接口且不用为它提供12V的
电压
然而第二种方法却使软件的设计变得十分的复杂软件虽然能够解决问题但不是
最好的当系统监视器正在和它的从设备通信时将不能接收从PC发送过来的消息为了
改进这种情况PC必须能够识别系统监视器不能接收消息的情况发送方将再传送一遍数
据同时系统监视器的串行控制也将保证PC有最高的通信优先级当系统监视器没有在和
从设备通信时它将准备接收从PC发送过来的数据除了改变串行通信模式和波特率来适
应同时和PC 从设备通信外系统监视器的设计还要有门限制确保同一时间内只和一个
设备传输数据系统监视器的设计见图0-1
图0-1 复用串行口
系统监视器的软件的串行中断服务程序接收从PC发过来的数据时采用的波特率为
9600 这部分代码可以使用第8章的时钟程序代码为了简单我们设PC和系统监视器像
第8章那样采用同样的数据格式
串行中断服务程序的编写一定要高效第8章所使用的两种串行中断服务程序中接收
数据的方法都要使用当串行口的波特率为9600时接收过程是由中断驱动的一小段汇
编代码在进入中断后根据标志位决定调用什么处理程序当标志位表明串行口波特率应使
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
115
用9600时执行用C编写的程序这时系统很多时间处于空闲模式等待中断当波特率
应为300K时则调用汇编编写的处理程序
你应该记得汇编代码不断查询输入的数据因为每个字节间的时间间隔不足30个指
令周期当波特率为9600时每个字节之间的时间间隔比较长所以使用中断驱动方式
当数据传输速度很快时就要采用汇编编写的程序了
列表0-1
/*****************************************************************
Function: ser_9600
Description: 当串行口波特率为9600时使用该接收程序
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_9600(void) {
if (_testbit_(TI)) { // 调用发送处理程序
ser_xmit();
}
if (_testbit_(RI)) { // 调用接收处理程序
ser_recv();
}
}
/*****************************************************************
Function: ser_recv
Description: 当波特率为9600时在中断服务程序中接收数据
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_recv(void) {
unsigned char c, temp;
c=SBUF;
switch (recv_state) {
case FSA_INIT: // 接收同步字节
if (c==SYNC) { // 检查同步字节
recv_state=FSA_ADDRESS; // 下一个状态
recv_timeout=RECV_TIMEOUT; // 最大间隔时间
recv_chksum=SYNC; // 初始化校验值
}
break;
case FSA_ADDRESS: // 接收地址
if (c==SM_ADDR) { // 确认地址
recv_state=FSA_COMMAND; // 下一个状态
recv_timeout=RECV_TIMEOUT; // 最大时间间隔
recv_chksum+=c; // 维护校验值
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
116
} else { // 数据不是给时钟的
recv_state=FSA_INIT; // 回到初始状态
recv_timeout=0;
}
break;
case FSA_COMMAND: // 接收命令字节
if (!valid_cmd[c]) { // 命令字节是否有效
recv_state=FSA_INIT; // 命令字节无效
recv_timeout=0;
} else {
recv_state=FSA_DATASIZE; // 下一个
recv_chksum+=c; // 更新校验字节
recv_buf[0]=c; // 保存命令
recv_timeout=RECV_TIMEOUT; // 最大时间间隔
}
break;
case FSA_DATASIZE: // 接收字节的数量
recv_chksum+=c;
recv_buf[1]=c; // 保存字节数
if (c) { // 是否有数据
recv_ctr=2; // 设置接收缓冲区
recv_state=FSA_DATA; // 进入接收数据状态
}else {
recv_state=FSA_CHKSUM; // 进入校验状态
}
recv_timeout=RECV_TIMEOUT;
break;
case FSA_DATA: // 读取数据
recv_chksum+=c;
recv_buf[recv_ctr]=c; // 保存数据save data byte
recv_ctr++; // 更新缓冲区更新缓冲区指针
if ((recv_ctr-2)==recv_buf[1]) { // 数据是否接收完毕
recv_state=FSA_CHKSUM;
}
recv_timeout=RECV_TIMEOUT;
break;
case FSA_CHKSUM:
if (recv_chksum==c)
push_msg();
}
default:
recv_timeout=0;
recv_state=FSA_INIT;
break;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
117
}
}
/*****************************************************************
Function: ser_xmit
Description: 处理串行发送中断
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_xmit(void) {
trans_ctr++; // 输出数据缓冲区指针加1
// 数据是否发送完毕
if (trans_ctr ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].size) {
// 最后发送校验字节
if (trans_ctr == (ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.size-1)) {
SBUF=trans_chksum;
} else {
// 发送当前字节
SBUF=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].buf[trans_ctr];
// 更新校验字节
trans_chksum+=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.buf[trans_ctr];
}
} else { // 数据发送完毕
// 如果没有应答
// 再次发送
if (!ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].retries) {
if (queue_pop(XMIT_QUEUE)) { // 输出后面的数据
check_stat();
}
} else {
xmit_timeout=XMIT_TIMEOUT; // 设置应答时间间隔计数
}
}
}
处理高速串行口中断代码的汇编源程序如下
列表0-2
; 文件名: SERINTR.A51
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
118
EXTRN DATA (uart_mode) ; 写入SCON 的值
EXTRN BIT (baud_9600) ; 波特率为9600时置位
EXTRN DATA (recv_chksum) ; 计算输入数据的校验值
EXTRN DATA (recv_buf) ; 存放输入数据
EXTRN CODE (ser_xmit) ; 处理串行发送中断子程序
EXTRN CODE (valid_cmd) ; 决定命令是否有效的表格
EXTRN CODE (push_msg) ; 把完整的消息放入消息队列中
EXTRN CODE (ser_9600) ; 处理慢速串行通信
SYNC EQU 33H ; 同步字节
SM_ADDR EQU 40H ; 时钟地址
CSEG AT 23H
ORG 23H
LJMP SER_INTR
PUBLIC SER_INTR
?PR?SER_INTR?SER_INTR SEGMENT CODE
RSEG ?PR?SER_INTR?SER_INTR
;*****************************************************************
; Function: readnext
; Description: 从串行口中读入一个字节
; returns. 时间溢出置位标志位
; Parameters: 无.
; Returns: R0所指向地址内的字节
; Side Effects: 无.
;*****************************************************************
readnext: CLR C ; clear the return flag
MOV TL1, TH1 ; T1为时间间隔定时器
SETB TR1 ; 开始定时
RN_WAIT: JBC RI, RN_EXIT ; 等待接收的数据
JBC TF1, RN_TO ; 时间间隔溢出
RN_TO: SETB C ; 时间溢出置位标志位
CLR TR1 ; 停止定时器
RET
RN_EXIT: MOV A, SBUF
CLR TR1
RET
;*****************************************************************
; Function: SER_INTR
; Description: 8051的串行口中断服务程序
; Parameters: 无.
; Returns: 无.
; Side Effects: 无.
;*****************************************************************
SER_INTR: JNB baud_9600, FAST_ISR ; 选择正确的处理过程
LCALL ser_9600
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
119
RETI
FAST_ISR: JBC RI, RECV_INT ; 接收中断
CHK_XMIT: JBC TI, XMIT_INT ; 发送中断
RETI
XMIT_INT: LCALL ser_xmit ; 处理发送中断
RETI
; 根据推入堆栈中的
; 数据多少跳转到下面的
; 相应的位置
CHK_XMIT3: POP DPL
POP DPH
CHK_XMIT2: POP 00H
CHK_XMIT1: POP ACC
MOV SCON, uart_mode ; 确保恢复校检位
JMP CHK_XMIT
RECV_INT: PUSH ACC ; 用累加器来保存数据
MOV A, SBUF ; 保存输入数据
CJNE A, #SYNC, CHK_XMIT1 ; 如果不是同步字节
; 退出接收程序
PUSH 00H ; 保存R0
MOV R0, #recv_buf ; 把接收缓冲区地址的基址保存在R0中
CALL readnext ; 读下一个数据
CJNE A, #SM_ADDR, CHK_XMIT2 ; 一定要是时钟的地址
CLR SM2 ; 校检位
CALL readnext ; 取下一个字节
PUSH DPH ; 保存DPTR
PUSH DPL
MOV DPTR, #valid_cmd ; 准备确认命令是否有效
MOVC A, @A+DPTR
JZ CHK_XMIT3 ; 如果表中相应的位为0
; 这是一个无效的命令
MOV @R0, A ; 保存命令
ADD A, recv_chksum ; 更新校验字节
MOV recv_chksum, A
INC R0 ; 移动缓冲区指针
CALL readnext ; 下一个字节
MOV @R0, A ; 保存要接收的字节的个数
ADD A, recv_chksum ; 更新校验字节
MOV recv_chksum, A
MOV A, @R0 ; 如果字节个数是0
; 进入校验状态
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
120
JZ RECV_CHK
MOV DPL, A ; 设置计数器
RECV_DATA: INC R0 ; 缓冲区指针加1
CALL readnext
ADD A, recv_chksum
MOV recv_chksum, A
DJNZ DPL, RECV_DATA ; 数据是否接收完毕
RECV_CHK: CALL readnext ; 接收校验字节
CJNE A, recv_chksum, CHK_XMIT3 ; 接收的数据是否正确
LCALL push_msg ; 接收数据正确
; 把数据保存到队列
JMP CHK_XMIT3
END
系统监视器软件将使用时钟的时标过程当计时到一定的数量后系统监视器开始查
询它的从设备时间一到如果总线可用作高速波特率传输时系统监视器将初始化和第
一个串行从设备之间的通信否则把数据存入队列等待串行总线空闲
列表0-3
/****************************************************************
Function: system_tick
Description: 定时器0的中断服务程序每50ms中断一次
在中断程序中对那些需要定时的功能函数
进行计数
Parameters: 无.
Returns: 无.
*****************************************************************/
void system_tick(void) interrupt 1 {
TR0=0; // 停止定时器
TH0=RELOAD_HIGH; // 重装定时器
TL0=RELOAD_LOW;
TR0=1; // 开始计时
if (poll_time) { // 是否开始查询从器件
poll_time--;
if (!poll_time) {
poll_time=POLL_RATE;
start_poll(); // 开始查询
}
}
if (xmit_timeout) { // 在发送队列中的最前面的消息
// 是否还没收到应答
xmit_timeout--;
if(!xmit_timeout) { // 重试
ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].retries--;
if (ser_queue[XMIT_QUEUE]
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
121
.entry[ser_queue[XMIT_QUEUE].head].retries) {
// 重新发送一遍消息
SCON=uart_mode=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.uart_mode;
ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.status=STAT_SENDING;
SBUF=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].buf[0];
trans_ctr=0; // 缓冲区指针指向第一个字节
// 设置发送字节数
trans_size=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].size;
// 设置消息校验字节
trans_chksum=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.buf[0];
} else { // 可重复次数为0
//这个消息已经处理完毕
if (queue_pop(XMIT_QUEUE)) { //清除该消息
// 是否还有待处理的消息
// 确认该消息可以使用串行口
// 如果该消息的UART模式和
// 当前UART的模式一样或接收状态空闲
// 则该消息可使用UART
check_stat(); // 开始消息发送
} else { // 设置UART模式
// 从PC中接收数据
TH1=TO9600_VAL;
TR1=0;
TL1=TH1; // 重装定时器UART马上有正确
// 的波特率输出
TF1=0;
TR1=1;
baud_9600=1;
P1=0x09; // 允许PC I/O
}
}
}
}
if (recv_timeout) { // 检测接收间隔时间溢出
recv_timeout--;
if (!recv_timeout) { // 超过间隔时间...
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
122
recv_state=FSA_INIT; // 重新置位FSA
check_stat();
}
}
}
/*****************************************************************
Function: start_poll
Description: 把对系统从器件查询所需要的消息装入发送队列中
Parameters: 无.
Returns: 无.
*****************************************************************/
void start_poll(void) {
unsigned char i, temp;
// 为每个从器件建立一个消息
for (i=0; iNUM_SLAVES; i++) {
temp=queue_push(XMIT_QUEUE); // 得到队列元素指针
if (temp!=0xFF) { // 存储区分配成功
// 建立消息
memcpy(ser_queue[XMIT_QUEUE].entry[temp].buf,
slave_buf
,
slave_buf
[3]+4);
// 设置消息大小字节
ser_queue[XMIT_QUEUE].entry[temp].size=slave_buf
[3]+5;
// 消息重发次数为3
ser_queue[XMIT_QUEUE].entry[temp].retries=3;
// 设置串行通信模式
ser_queue[XMIT_QUEUE].entry[temp].uart_mode=BAUD_300K;
// 设置消息状态
ser_queue[XMIT_QUEUE].entry[temp].status=STAT_WAITING;
}
}
check_stat(); // 下一个消息是否可以
// 开始使用UART
}
在两个串行通信模式中都使用了定时器1 在波特率为9600的模式下定时器1作为串
行口的波特率发生器接收字节之间的时间间隔由定时器0产生的时标来计算在300K波
特率模式下定时器1用作接收字节间的时间间隔计算定时器1的复用意味着当串行口的
模式改变时必须仔细的改变定时器1的重装值很重要的一点是当改变定时器的工作
方式使之作为9600的波特率发生器时当模式一改变马上把TH1中的数装入定时器1的
低字节中还有就是你不会立即得到9600的波特率如果这个小细节被忽视的话你的
第一个字节可能会丢失
串行口在这里作为系统资源每个要发送的数据都在发送队列中排队等待发送队列
中的头消息在下列情况下可以使用串行口一当串行口空闲时如发送和接收都在空闲
状态时二当发送状态是空闲时而串行口正在调整接收消息的波特率并且一个消息正
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
123
在被接收
每次当一个接收消息被推入发送队列时因为接收状态那时为空闲每个接收消
息被弹出队列时因为它可能是发送队列中消息的应答需要清除和消息超时时都要
检查发送队列的头消息这样确保了UART及时的被每个消息所使用执行这项任务的功能
见表0-4
Listing 0-4
/*****************************************************************
Function: check_stat
Description: 检测发送缓冲区头信号是否正在等待串行口
和是否可以开始发送如果是的话就开始发送
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void check_stat(void) {
if (ser_queue[XMIT_QUEUE].head!=UNUSED) {// 是否有消息
// 正在等待发送...
// 检测它的状态
if (ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.status == STAT_WAITING) {
// 正处于等待状态
// 可否占用串行口
if (recv_state==FSA_INIT ||
(uart_mode == ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.uart_mode)) {
// 开始传送消息
SCON=uart_mode=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.uart_mode;
if (uart_mode==BAUD_300K) { // 确认定时器1正常
// 重装
TH1=TO300K_VAL;
baud_9600=0;
P1=0x06; // 使能从器件I/O
} else {
TH1=TO9600_VAL;
TR1=0;
TL1=TH1; // 重装定时器UART马上有正确
// 的波特率输出
TF1=0;
TR1=1;
baud_9600=1;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
124
P1=0x09; // 使能PC I/O
}
ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head]
.status=STAT_SENDING;
SBUF=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].buf[0];
trans_ctr=0; // 缓冲区指针指向第一个字节
trans_size=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].size;
trans_chksum=ser_queue[XMIT_QUEUE]
.entry[ser_queue[XMIT_QUEUE].head].buf[0];
}
}
}
}
当接收成功时所有接收来的消息都被推入接收队列中主循环检测接收队列是否还
有没被接收完毕的消息如果有,则调用相关的函数对队列的头消息进行操作从PC传送
过来的命令一般让串行监视器传送数据给从器件然后发送应答信号给PC 这些应答信号
在接收过程中被建立然后推入传送队列中如果消息是从PC或从器件传送来的应答或数
据将检测消息是否符合在发送队列头中的消息如果符合发送队列头被弹出开始传
输下一个消息这个功能的基本结构见列表0-5
Listing 0-5
/*****************************************************************
Function: push_msg
Description: 把当前消息放入串行消息队列中确保波特率为9600,
T1处于自动重装模式
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void push_msg() {
unsigned char temp;
temp=queue_push(RECV_QUEUE); // 在队列中分配存储空间
if (temp!=0xFF) { // 存储空间分配成功
// 拷贝数据
memcpy(ser_queue[RECV_QUEUE].entry[temp].buf, recv_buf,
recv_buf[1]+2);
// 消息大小
ser_queue[RECV_QUEUE].entry[temp].size=recv_buf[1]+2;
// 消息状态
ser_queue[RECV_QUEUE].entry[temp].status=STAT_IDLE;
// 重发次数为0
ser_queue[RECV_QUEUE].entry[temp].retries=0;
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
125
// 记录当前串行模式
ser_queue[RECV_QUEUE].entry[temp].uart_mode=uart_mode;
}
recv_chksum=SYNC+SM_ADDR; // 设置校验字节
recv_state=FSA_INIT; // 初始化接收状态
check_stat(); // 是否可以开始发送下一个数据
}
/*****************************************************************
Function: ser_exec
Description: 处理所有的输入消息.
Parameters: 无.
Returns: 无.
*****************************************************************/
void ser_exec() {
#ifdef USEEXEC
do {
switch (ser_queue[RECV_QUEUE].entry[head].buf[1]) {
...
}
} while (ser_queue(RECV_QUEUE)); // 是否处理完所有消息
#endif
check_stat(); // 是否可以开始发送
}
2 队列实行
因为串行监视器必须快速的输入输出消息所以建立一组”entries”的消息队列每个
entry存放一个消息分别用一个字节表示消息的大小如果没有应答的话重发送消息的
次数UART的模式及相关的状态状态变量决定消息什么时候使用UART 在发送模式下什
么时候释放UART 在接收模式下状态字节没什么用这些消息的数组有一定的大小并
提供了指向消息的头指针和尾指针通过分配一个字节给头指针和尾指针避免了所有的
指针运算
列表0-6
typedef struct { // 结构定义
unsigned char buf[MSG_SIZE]; // 消息数据
unsigned char size, // 消息大小
retries, // 重复次数
uart_mode, // UART模式
status; // 当前消息的状态
} entry_type;
typedef struct { // 定义队列
unsigned char head, // 队列头
tail; // 队列尾
entry_type entry[QUEUE_SIZE]; // 队列的元素为entry结构
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
126
} queue_type;
extern queue_type ser_queue[2];// 一个发送和一个接收队列
正常情况下像队列这种数据结构应该从堆中动态的分配和释放存储空间Keil在它
的软件包中提供动态分配函数但是应该尽量避免使用它
其实队列就是分配了大小的数组这样推入和弹出操作就很容易实现使用指针对对
列中的元素进行访问消息从队列头弹出从队列尾压入用一些简单的方法防止队列溢
出
列表0-7
/*****************************************************************
Function: queue_push
Description: 在循环队列中分配一个entry结构
Parameters: queue - unsigned char. 可用的队列.
Returns: 如果分配成功返回指针否则返回0xFF
Side Effects: none.
*****************************************************************/
unsigned char queue_push(unsigned char queue) {
unsigned char temp;
if (ser_queue[queue].head==UNUSED) { // 如果队列是空的
// 分配头指针为0
ser_queue[queue].head=ser_queue[queue].tail=0;
return 0;
}
temp=ser_queue[queue].tail; // 保存尾指针值
// 尾指针值加1
ser_queue[queue].tail=(ser_queue[queue].tail+1) % QUEUE_SIZE;
// 确保尾指针和头指针不重叠
if (ser_queue[queue].head == ser_queue[queue].tail) {
ser_queue[queue].tail=temp; // 没有可分配的空间
return 0xFF;
}
return ser_queue[queue].tail; // 返回分配存储空间的地址
}
/*****************************************************************
Function: queue_pop
Description: 把数据从循环队列中弹出
Parameters: queue - unsigned char. 可用队列
Returns: 如果队列已空返回0 如果队列中还有数据返回1
Side Effects: 无.
*****************************************************************/
bit queue_pop(unsigned char queue) {
// 头指针加1
ser_queue[queue].head=(ser_queue[queue].head+1) % QUEUE_SIZE;
// 如果头指针等于尾指针
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
127
// 队列已空
if (((ser_queue[queue].head-ser_queue[queue].tail)==1) ||
(!ser_queue[queue].head
(ser_queue[queue].tail==QUEUE_SIZE-1))) {
ser_queue[queue].head=ser_queue[queue].tail=UNUSED;
return 0;
}
return 1;
}
响应输入消息的代码留给读者自己去设计根据你项目的需要写入相应的代码每个
输入的消息将和发送队列的头消息该消息正在等待应答进行比较如果相符合就清
除发送队列中的头消息
但接收队列中的消息并非都是发送队列中头消息的应答它有可能是来自PC的命令
这时可能需要发送一些数据给从器件和发送应答消息给PC 在发送队列中建立这些消息
并排队等待发送
3 使用内置定时器作TDMA控制
很多通信系统不能简单采用像上面那种使用系统监视器的查询方式采用查询方式的
缺点在于当网络上的器件很多时查询方式将浪费很多时间将有大量的数据从主设备
发送到从器件当从器件之间需要通信时也要经过主控器我们要通过一种新的串行网
络设计来解决这些问题
新设计的主要不同点是从器件之间可以直接进行通信而不再是只和系统监视器进行
数据传输在网络上传输的所有消息都能被从器件监听到这将影响从器件的设计方式
首先它们的通信能力必须得到增强因为它们将直接彼此进行通信而不再需要主控器作为
过度此外每个器件上串行中断的数量将大大增多连接这些器件的网络拓扑结构如下
图0-2 网络拓扑结构
和前面的网络结构相比不见了系统监视器PC也直接连接在网络中它将从网络中收
集它所需要的数据因为PC连接在网络上所以网络的通信波特率采用9600 网络上的器
件可以和任何其它器件进行通信
通信权轮流分配首先是节点1 然后是节点2…节点n 再是节点1 这样无限循环
当轮到某个器件通信时它能发送数据给其它任何器件当通信时间结束后必须释放总
线的控制权这是TDMA Time Divion-Multiple Access 网络通信的基本原则
设计中每个节点都被分配了一个时间段号码用于通信时间段是一个基本单位时间
基本单位时间的选择根据所要发送数据量的大小来决定在这里我们把时间段设置成
50ms 知道了时间段号码总的时间段数量和时间段的大小可以很容易用软件跟踪每
个时间段并对时间段计数当计到自己的号码时就开始发送数据用51系列单片机来实
现这个简单的TDMA网络现假设所有器件同时启动这样在上电时使得系统取得同步在
实际系统中可以使用很多方法来取得系统同步
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
128
TDMA网络节点基本硬件的设计十分简单在这个例子中一排8脚DIP开关和P1端口相
连端口的低位决定器件的时段号高位决定网络的器件号这个TDMA网络的时段宽度为
50ms
网络上两个器件之间的交谈是很简单的假设从器件1想从从器件2处取得一些数据
从器件1在它的的时段中向从器件2发送请求数据消息从器件2接收并分析从器件2发送过
来的消息当确认消息有效时把消息放入接收队列中处理从器件2的代码将产生相应
的应答消息把这个应答消息放入发送队列中当从器件2的时间段来临时把消息发送出
去与此同时从器件1计时等待从器件2反馈的数据这样就避免了无限制的等待
当网络上节点的时间段来临的时候它会发送尽可能多的数据例如如果发送队列
中有5个消息它不会仅仅只发送一个消息而会充分利用这50ms的时间把尽可能多的消
息发送出去如果这个时间段中只能发送3个消息另外两个消息就等到下一个时间段再
发送
网络节点的通信程序比较简单可从系统监视器网络的代码演变过来下面是系统初
始化的主程序
列表0-8
/*****************************************************************
Function: main
Description: 程序入口.初始化8051,使能中断源
然后进入空闲模式
Parameters: 无.
Returns: 无.
*****************************************************************/
void main(void) {
slotnum=P1 & 0x0F; // 得到节点的时段号码
slottot=P1 / 16; // 得到总的节点数
TH1=TO9600_VAL; // 设置定时器1的重装值
TH0=RELHI_50MS; // 设置定时器0的值
TL0=RELLO_50MS;
TMOD=0x21; // 定时器0为16位工作方式
// 定时器为8位自动重装方式
TCON=0x55; // 定时器开始运行两个外部中断都为
// 边沿触发方式
SCON=BAUD_9600; // UART 工作为模式2
IE=0x92; // 使能定时器0中断
// 和串行口中断
init_queue(); // 清空所有队列
for (;;) {
if (tick_flag) { // 检测系统时标
system_tick();
}
if (rcv_queue.head!=UNUSED) { // 如果接收队列中有消息
ser_exec(); // 就对它进行处理
}
PCON=0x01; // 进入空闲模式
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
129
}
}
每当定时器0中断把标志位置位后从主程序中调用’system_tick’ 时标函数不再在
定时器0的中断服务程序中执行因为ISR还要对系统时段计数而且要非常精确保证不
溢出时段的边界中断程序用汇编编写每条指令的执行周期都要精确的计算
列表0-9
EXTRN BIT (tick_flag) ; 表明一个中断已经产生
EXTRN CODE (start_xmitt) ; 节点时段到了...
EXTRN XDATA (curslot) ; 跟踪当前时段
; 基于系统工作频率11.059MHz的定时器重装值使定时器每50ms溢出一次
; 注意重装要延时9个时钟周期
REL_HI EQU 04CH
REL_LOW EQU 007H
SEG AT 0BH
ORG 0BH
LJMP T0_INTR ; 中断服务程序
PUBLIC T0_INTR
?PR?T0_INTR?T0INT SEGMENT CODE
RSEG ?PR?T0_INTR?T0INT
;*****************************************************************
; Function: T0_INTR
; Description: 定时器0的中断服务程序定时器每50ms溢出一次
; 重装定时器并检测节点时间段是否到来
; Parameters: 无.
; Returns: 无.
; Side Effects: 无.
;*****************************************************************
T0_INTR: CLR TR0 ; 1, 3 重装定时器0
MOV TH0, #REL_HI ; 2, 5
MOV TL0, #REL_LOW ; 2, 7
CLR TF0 ; 1, 8
SETB TR0 ; 1, 9
SETB tickflag ; 置位标志位
LCALL check_slot ; 是不是我的时段
PUSH ACC
PUSH B
PUSH DPH
PUSH DPL
MOV DPTR, #curslot ; 把当前时段数读入累加器
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
130
MOVX A, @DPTR
INC A ; 时段数加1
MOV B, A
MOV A, P1 ; 读入总的时段数
SWAP A
ANL A, #00FH
XCH A, B
CLR C
SUBB A, B ; 当前时段数是不是
; = 总的时段数
JC L1
CLR A ; 是清除curslot
MOVX @DPTR, A
L1: MOVX A, @DPTR ; 读入当前时段数
MOV B, A
MOV A, P1 ; 该器件的时段号
ANL A, #00FH
CLR C
SUBB A, B ; curslot==slotnum
JNZ L2 ; 不等于
LCALL start_xmit ; curslot==slotnum, 开始发送
L2: POP DPL
POP DPH
POP B
POP ACC
RETI
END
下面是用C编写的新的定时器溢出服务程序
列表0-10
/*****************************************************************
Function: system_tick
Description: 定时器溢出的服务程序. 在该程序中对那些需要
时间限制的函数计数
Parameters: 无.
Returns: 无.
*****************************************************************/
void system_tick(void) {
unsigned char i;
tick_flag=0; // 清除标志位
for (i=0; iMAX_MSG; i++)
if (xmit_timeout
[0]) { // see if the msg timed out
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
131
xmit_timeout
[0]--;
if (!xmit_timeout
[0]) { // if so, check retries
check_msg(xmit_timeout
[1]);
}
}
}
if (recv_timeout) { // 检测字节间隔时间是否
// 溢出
recv_timeout--;
if (!recv_timeout) { // 溢出...
recv_state=FSA_INIT;
check_stat();
}
}
}
系统时标函数有很多系统监视器项目中的功能但它更像时钟项目中的时标函数都是
从主循环中调用它的主要功能是维护消息定时器允许节点从网络通信错误中恢复过来
定时器0的中断服务程序跟踪系统时间段当本节点的时间段到了时它调用发送功
能函数发送正在等待发送的消息功能函数尽可能多的为队列中等待发送的消息建立缓冲
区串行发送中断服务程序把这些消息发送出去当受到应答消息后消息执行代码将把
消息从发送队列中清除没有接收到应答消息的消息由从系统时标函数中调用的时间溢出
代码进行处理串行口中断服务程序除了数据发送部分的代码外和系统监视器的代码一
样数据不再从发送队列中直接发出而是从从一个新建的缓冲区中发出
列表0-11
/*****************************************************************
Function: start_xmit
Description: 把尽可能多的消息放入发送缓冲区中然后发送第一个消息
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void start_xmit(void) {
unsigned char maxbytes=45, // 一个时间段中总共发送的字节数
msgnum=0, // 发送缓冲区中装入的总的消息数
i;
if (tq_head == UNUSED) { // 如果队列是空的就不浪费时间
return;
}
while (maxbytes) { // 当发送缓冲区中有空间
if (maxbytes=tq[temp].size) { // 确定能装下下一个字节
// 拷入缓冲区并建立校验字节
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
132
for (i=0, chksum=0, trans_size=0;
itq[temp].size;
i++, trans_size++) {
trans_buf[trans_size]=tq[temp].buf
;
chksum+=tq[temp].buf
;
}
trans_buf[trans_size]=chksum; // 保存校验字节
xmit_timeout[msgnum][0]=MSG_TIMEOUT; // 保存时间间隔信息
xmit_timeout[msgnum][1]=tq[temp].retries;
msgnum++; // 缓冲区中消息数加1
maxbytes-=tq[temp].size+1; // reduce amount remaining by
// amount used
temp=tq[temp].next;
} else {
maxbytes=0; // 跳出循环
}
}
}
/*****************************************************************
Function: ser_xmit
Description: 处理发送中断
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_xmit(void) {
trans_ctr++; // 发送指针加1
// 数据是否发送完毕
if (trans_ctr trans_size) {
// 最后一个字节发送校验位
if (trans_ctr==trans_size-1)) {
SBUF=trans_chksum;
} else {
// 发送当前字节
SBUF=trans_buf[trans_ctr];
// 更新校验字节
trans_chksum+=trans_buf[trans_ctr];
}
}
}
现在发送队列的结构和接收队列的结构不再一样了因为队列头不再需要应答如果
在该节点的时间段发送出了3个消息只有第2个和第3个消息得到了应答队列必须保存
第1个消息而清除第2个和第3个消息新的队列结构必须更加复杂来完成这种功能
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
133
发送队列还将使用一定数量的entry结构在每个结构间有一定的联系而不是简单的
按照顺序关系排列entris数组有两个连接列表一个使用一个未被使用当需要新的
存储结构时从自由的列表中获取一个结构并把它连接到使用列表中当需要删除一个
结构时把该结构从使用列表中取出并放回到自由列表中新发送队列的源代码见列表
0-12
列表0-12
/*****************************************************************
Function: tq_init
Description: 为发送队列设置列表
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void tq_init(void) {
tq_head=tq_tail=UNUSED; // 将头尾指针置为空
tq_free=0; // 初始化空列表
for (i=0; iQUEUE_SIZE; i++) {
tq
.next=i+1;
}
tq[QUEUE_SIZE-1].next=UNUSED; // 列表循环
}
/*****************************************************************
Function: tq_push
Description: 分配发送队列中的自由结构给调用者
Parameters: 无.
Returns: 返回被分配的结构单元如果没有可分配的单元返回0xFF
Side Effects: 无.
*****************************************************************/
unsigned char tq_push(void) {
unsigned char temp;
if (tq_free==UNUSED) { // 如果没有空余的存储区...
return UNUSED; // 告知调用者
}
temp=tq_free; // 得到第一个空结构
tq_free=tq[tq_free].next; // 自由列表头指向下一个空结构
tq[temp].next=UNUSED; // 该结构是使用列表中的
// 最后一个结构
tq[tq_tail].next=temp; // 当前正在使用的列表尾指向
// 新结构
tq_tail=temp;
return temp; // 返回新结构
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
134
/*****************************************************************
Function: tq_pop
Description: 从发送队列中弹出指定的结构.
Parameters: entry - unsigned char. 指定要弹出的结构
Returns: 0 如果队列为空, 1 如果队列中还有数据
Side Effects: 无.
*****************************************************************/
bit tq_pop(unsigned char entry) {
unsigned char temp, trail;
if (tq_head==UNUSED || entry(QUEUE_SIZE-1)) { // 队列是否为空
// 或该结构无效
return (tq_head==UNUSED) ? 0 : 1;
}
if (entry==tq_head) { // 如果弹出的是队列头
// 作特殊处理
temp=tq_head;
tq_head=tq[tq_head].next; // 移动头指针
tq[temp].next=tq_free; // 把旧的结构放入自由队列头中
tq_free=temp;
} else {
temp=trail=tq_head; // 设置跟踪指针
while (temp!=entry temp!=UNUSED) { // 查表直到找到该结构
// 或表被查遍
trail=temp;
temp=tq[temp].next;
}
if (temp!=UNUSED) { // 找到结构...
tq[trail].next=tq[temp].next; // 删除该结构
tq[temp].next=tq_free; // 把结构放入空表中
tq_free=temp;
if (temp==tq_tail) {
tq_tail=trail;
}
}
}
return (tq_head==UNUSED) ? 0 : 1;
}
3 保持节点器件同步
处理网络工作的代码相对来说是比较简单的TDMA网络通信确保了每个节点都能得到
同等的时间来发送数据但是像前面所提到的如果每个节点不是精确的在同一时刻复
位那么是不能保持同步的确保同步最简单的方法是发给每个节点器件一个同步信号
这需要重新设计网络在每个时段循环的开始我们用PC发出一个由高到低的跳变脉
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
135
冲这个脉冲使每个节点器件都调整到时段0 每个节点都用一个中断服务程序来处理这
个信号因此把信号接到8051的INT0引脚上ISR将重装并启动定时器0 而不是通过主程
序中的代码来完成定时器将负责时间的复位当PC再次发出同步脉冲时各节点又将恢
复到时段0
列表0-13
/*****************************************************************
Function: start_tdma
Description: 中断服务程序应答从网络主程序发送过来的信号重新
开始时段计数启动定时器0
Parameters: 无.
Returns: 无.
*****************************************************************/
void start_tdma(void) interrupt 0 {
TH0=RELHI_50MS; // 设置定时器0
TL0=RELLO_50MS;
TF0=0;
TR0=1;
curslot=0xFF; // 从时段0重新开始
}
这样做有两个好处第一这使得网络中的节点较容易保持同步因为它们都参照同
一个启动信号第二它使PC有能力控制网络通信你修改一下定时器0的中断服务程序
当时段计数完成一个循环后就停止定时器这样只有收到PC发送的同步信号才能开始重新
通信假设PC想发一个很长的信号给网络中的某个节点但是又不想等好几个时段来发
送这时PC就可以停止网络时段计数等它把数据发送完毕后再重新开始网络通信
另外一个网络协议的小改动是让PC能够给那些需要立即应答的节点发送消息换句话
说就是不必把消息放入队列中等待时段进行发送而是直接发送这个网络就成为TDMA系
统和查询系统的混合系统这种设计使得网络节点向PC传输的数据最大化
4 CSMA网络
当网络中的所有器件都充分利用了自己的时段发送数据时前面所讲的TDMA网络是十
分灵活而高效的无疑是网络通信中一个很好的解决方案
然而并不是所有的系统都适用TDMA方案有些节点并不传送很多消息这样分配给
该节点的时段并没有被充份应用而有些节点的数据很多时段对他们来说不够用这时
TDMA方法显然不再适用
要解决这个问题需要每个节点在需要的时候都能够进行通信但是不可避免有两个
节点同时需要通信的情况发生这时将产生冲突要解决冲突节点需要某种方法来探测
网络是否被使用和当自己在传输数据的时候是否有冲突发生具有这种功能的网络称为
CSMA Carrier Sense-Multiple Access 网络CSMA网络的关键问题是有一套底层的指
令可使设备不产生冲突的联入网络
把8051接入CSMA网络网络节点的硬件必须使内置8051的串行口能够从所有的节点传
送过来的数据包括他自己其次处理器必须使用8052 这样就多了一个定时器新类型
的网络的通信方式比较简单首先TDMA网络中定时器0的时标功能函数原封不动的放入
定时器2中断服务程序中定时器0用来从接受到最后一个字节开始计时每次RI引起中断
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
136
时定时器0都要重装计数值同时置位网络忙标志位当定时器溢出时执行定时器0中
断服务程序停止定时器清零标志位发送消息的程序代码将检测这个标志位如果该
标志位清零才开始发送消息
定时器0中断服务程序将作为CSMA网络的核心程序每当从网络中接收到一个字节时
定时器都要重装此外当发生多个节点的冲突时定时器将进行随机延时为了执行这
两个功能用一个标志位来决定是否将随机数装入定时器0中当定时器溢出后它将重
新开始发送消息如果网络空闲的话并装入定时器的正常值定时器0的中断服务程序
见列表0-14
列表0-14
/****************************************************************
Function: network_timer
Description:定时器0中断服务程序当字节间最大限制时间溢出或
网络隔离过程结束时引发中断
Parameters: none.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void network_timer(void) interrupt 1 {
TR0=0; // 停止定时器
if (delay_wait) { // 是否因为网络冲突正在等待
delay_wait=0; // 清除辨证外标志位
trans_restart(); // 重新开始发送
}
network_busy=0; // 网络不再繁忙
check_status(); // 是否开始发送消息
}
/*****************************************************************
Function: trans_restart
Description:开始发送缓冲区中的消息假设消息正确
重复变量和消息大小变量已经设置好了
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void trans_restart(void) {
SBUF=trans_buf[0]; // 输出第一个字节
last_out=trans_buf[0]; // 保存作为冲突检测
trans_ctr=0; // 缓冲区指针指向第一个字节
trans_chksum=trans_buf[0]; // 设置校验字节
}
每个写入SBUF的字节将被存储在一个临时地址中当产生接收中断时和接收到的数据
相比较如果临时地址中的数据和SBUF中的数据不符就认为数据发送中出现了问题这
个节点将随机等待一端时间再重新发送消息下面是处理发送和接收中断的代码
列表0-15
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
137
/*****************************************************************
Function: ser_xmit
Description: 处理串行发送中断.
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_xmit(void) {
trans_ctr++; // 数据输出指针加1
// 数据是否发送完毕
if (trans_ctr trans_size) {
// 最后发送校验字节
if (trans_ctr==trans_size-1)) {
SBUF=trans_chksum;
last_out=trans_chksum;
} else {
// 发送当前字节
SBUF=trans_buf[trans_ctr];
last_out=trans_buf[trans_ctr];
// 更新校验字节
trans_chksum+=trans_buf[trans_ctr];
}
}
}
/*****************************************************************
Function: ser_recv
Description: 当系统波特率为9600时处理串行接收中断.
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void ser_recv(void) {
unsigned char c, temp;
c=SBUF;
if (TH0 NET_DELAY_HI)
TR0=0; // 设置延迟时间
TH0=NET_DELAY_HI;
TL0=NET_DELAY_LO;
TR0=1;
}
if (transmitting) { // 如果这个节点正在发送
// 消息...
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
138
if (c!=last_out) { // 当前字节应该和上次写入SBUF
// 的字节一样
trans_hold(); // 不一样网络传输发生错误
}
} else {
switch (recv_state) {
... // 分析输入数据
}
}
}
/*****************************************************************
Function: trans_hold
Description: 设置一个随机网络隔离时间从2.0ms到11.76ms
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
void trans_hold(void) {
unsigned int holdoff;
trans_chksum=trans_ctr=0; // 复位发送计数器
holdoff=(unsigned int) rand(); // 得到随机数
holdoff/=3; // 把随机数控制在需要的范围
holdoff+=TWO_MS; // 增加一个常数确保延时2ms
holdoff=(0xFFFF-holdoff)+1; // 转换成重装值
TR0=0; // 重新启动定时器
TL0=(unsigned char) (holdoff 0x00FF);
TH0=(unsigned char) (holdoff / 256);
delay_wait=1; // 表明节点因为网络冲突
// 正处于等待状态
TR0=1;
}
可以看到处理发送中断的代码没什么变化最大的不同就在于变量last_out 必须
设为最后写入SBUF的值记住这个值将作为下一个完整字节进入串行口如果不这样的
话就会出现网络错误
CSMA网络节点的其它代码和系统监视器中的代码很像网络中传送的消息要么是命令
或对另一个节点的请求它需要一个应答要么是对网络中其它节点的回复这样系统监
视器中的数据结构和命令代码稍微改动一下就可以重用
5 结论
这章介绍了几种使用8051控制器进行网络工作的方法这些并不是唯一的几种方法
如果你需要更多关于网络设计和分析的信息可以查阅其它书籍
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
139
第九章控制编译和连接
1 把C代码转变成Keil C代码
当你把对其它处理器操作的已存在的代码移植到8051上时或把基于8051的代码进
行转变使之符合Keil C开发工具这无疑是明智之举因为8051和Keil C相结合的功能
是十分强大的把现有的C代码转化成Keil C代码是很简单的工作因为C51编译器完全支
持C语言的ANSI标准只要你的代码中不存在非ANSI的语句直接用C51编译器进行编译就
没有问题
当开始进行代码转换时必须注意几个问题声明的变量和代码结构应该适合在8051
上运行根据这个原则你应该确保代码转换向着8051的发方向进行
如果你的代码以前是用在其它控制器上的就应该特别注意第三章说讲的如何优化你
的代码第一点应该注意的就是8051是8位微控制器尽量把所有变量和数据元素的存储
范围控制在8位的范围内对那些作为标志位的变量应声明为位变量如果经常要对这个
位变量寻址就用bdata来声明它
还应注意的一点是指针的使用这在第三章也提到过但值得重声如果在声明指针
的时候把它限制在某一存储区域并通知编译器那么代码的长度和代码执行的时间都会都
会缩短不少编译器会为使用这些指针的原代码写出更好的汇编代码
一旦已经完成了上面所提到的优化过程就要开始检查软件的结构确定哪些是中断
服务程序那些被主函数调用的程序当建立了中断服务程序之后对它进行编译和连接
连接器将对那些有多重中断调用的函数产生警告信息这些警告信息使你知道哪些代码是
有潜在的错误的这些部分可能是由于递归调用或中断结构在同一时间被调用多次由于
8051的结构C51编译器不会自动产生代码通过单独的调用树去处理这些递归和多重调用
如果你使用的是像80x86这样的处理器就能为每个功能调用建立相应的调用结构但是
8051的堆栈空间没有这么大对于那些必须递归调用的功能函数可以把他们定义成再入
函数这时C51编译器将使用一定的堆栈空间建立一个模拟栈这时会占用内存和延长处
理时间因此要尽量少的使用关键字’reentrant’
并不是所有连接器产生的警告都会导致错误有时候连接器警告某个功能函数被多
个中断调用了但实际上却不可能例如有个函数被定时器0和外部中断1的ISR调用
但这两个中断被设为同一个中断优先级因此在同一时间只能执行一个中断服务程序在
执行中断服务程序的过程中不会被同级中断所中断因此是十分安全的一种除去连接警
告的方法是从一个调用树中删除参考这样就不会产生你不想要的再入栈关于这点我
们将在后面仔细讨论
当上面所有一切都完成之后你要考虑对外部存储区的寻址方式了很多C程序员
当他们需要对某个物理地址进行寻址的时候都会声明一个指针用指针对这个物理地址
进行操作这种方法在C51中仍然适用但最好使用像CBYTE CWORD XBYTE XWORD DBYTE
DWORD POBYTE PWORD 这些由absacc.h提供的宏定义它们使外部存储区看起来像一个
char,int,long 的数组使程序更具有可读性另外如果你的硬件结构有点特殊
不能简单使用MOVX对外部存储区进行寻址你可以重新改写宏定义来适应新的寻址方式
如果你的代码以前用的是像Archimedes或Avocet这样的编译包你必须把关键字转换
成Keil的形式因为其它的编译器不支持像bdata,variables,reentrant函数和特殊功能
寄存器组这些特征转化后的代码应该充分利用Keil支持的这些功能我曾经把一个项目
从Archimedes转而使用C51 结果不但节省了CODE和XDATA空间而且速度也大大加快了
以至于不得不想办法把速度降下来从这个例子可以看出如果使用得好的话C51确实
可以让你获益非浅
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
140
2 把汇编代码转换成Keil汇编代码
把汇编代码转换成Keil汇编代码中要注意的问题不是很多主要一点是使段名和Keil
段名的命名规则兼容这样和Keil C连接起来更加简单如果程序是用C和汇编共同编写
的请参考第三章关于C和汇编联合编程的叙述
我很少在使用Keil进行反汇编时碰到问题实际上唯一碰到的问题是删除从Avocet
汇编程序转化过来的程序PCON寄存器的定义原因是Avocet汇编太老了它是在节电模式
引入8051之前产生的需要用PCON的地址直接定义
3 使用”using”关键字
你应该记得8051系列微处理器有4个寄存器组每组有8个寄存器这32个字节位于DATA
存储区的最底层每个寄存器组都有一个号码从0到3 PSW SFR中的RS0和RS1的默认值
是0 选择寄存器组0 软件可以改变RS0和RS1的值选择四组寄存器中的任意一组第三
章讨论了在中断服务程序中使用寄存器组的问题比较了使用using和不使用using选项时
所产生的汇编代码的不同处当使用了using选项时寄存器不会被压入堆栈这里我们
将讨论如何利用这一点
第三章表明通过为中断服务程序指定寄存器组在中断调用时可以节省32个指令周
期为了利用这点建议在程序中为每个中断级指定一个寄存器组例如主循环程序和初
始化代码将使用默认寄存器组0 中断优先级为0的中断服务程序将使用寄存器组0 中断
优先级为1的中断服务程序将使用寄存器组2 任何被中断服务程序调用的功能要么必须使
用和调用者相同的寄存器组要么使用汇编指令NOAREGS 使之不受当前寄存器组的影响
下面的代码说明了为ISR选择寄存器组的基本设计方法
列表0-1
void main(void) {
IP=0x11; // 串行中断和外部中断0有
// 高优先级
IE=0x97; // 使能串行中断,外部中断1
// 定时器0和外部中断0
init_system();
...
for (;;) {
PCON=0x81; // 进入空闲模式
}
}
void serial_intr(void) interrupt 4 using 2 {
// 串行口中断有高优先级
// 使用寄存器组2
if (_testbit_(RI)) {
recv_fsa();
}
if (_testbit_(TI)) {
xmit_fsa();
}
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
141
void recv_fsa(void) using 2 { // recv_fsa 必须使用和串行中断
// 同样的寄存器组因为串行中断
// 将调用它
...
}
void xmit_fsa(void) using 2 { // xmit_fsa 必须使用和串行中断
// 同样的寄存器组因为串行中断
// 将调用它
...
}
void intr_0(void) interrupt 0 using 2 {
// 高中断优先级– 使用
// 寄存器组2
handle_io();
...
}
void handle_io(void) using 2 { // 被使用RB2的中断服务程序调用
// 必须使用RB2
...
}
void timer_0(void) interrupt 1 using 1 {
// 低优先级中断– 使用
// 寄存器组1
...
}
void intr_1(void) interrupt 2 using 1 {
// 低优先级中断– 使用
// 寄存器组1
...
}
ISR和ISR调用的程序使用同一个寄存器组任何被主程序调用的功能函数不需要指定
寄存器组因为C51会自动使用寄存器组0 下面是这个简单例子的调用树分支并不交叉
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
142
图0-1 简单调用树
很多实时时钟系统的调用树并不像上面那样简单有些程序除了被主程序调用外还
被多个程序调用如下面的代码显示功能函数被主函数和两个中断级调用
列表0-2
void main(void) {
IP=0x11; // 串行中断和外部中断0
// 为高优先级
IE=0x97; // 使能串行中断,外部中断1
// 定时器0中断和外部中断0
init_system();
...
display(); // 向显示板发送一个
// 消息
for (;;) {
PCON=0x81; // 进入空闲模式
}
}
void serial_intr(void) interrupt 4 using 2{
// 串行口中断有高优先级
// 使用寄存器组2
if (_testbit_(RI)) {
recv_fsa();
}
if (_testbit_(TI)) {
xmit_fsa();
}
}
void recv_fsa(void) using 2 { // recv_fsa 必须使用和串行中断
// 同样的寄存器组因为串行中断
// 将调用它
...
display(); // 向显示板写入一个
// 状态
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
143
}
void xmit_fsa(void) using 2 { // xmit_fsa 必须使用和串行中断
// 同样的寄存器组因为串行中断
// 将调用它
...
}
void intr_0(void) interrupt 0 using 2 {
// 高优先级-使用
// 寄存器组2
handle_io();
...
}
void handle_io(void) using 2 { // 被使用RB2的中断程序调用
// 必须使用RB2
...
}
void timer_0(void) interrupt 1 using 1 {
// 低中断优先级– 使用
// 寄存器组1
...
display(); // 向显示控制器写入一个
// 时间溢出消息
}
void intr_1(void) interrupt 2 using 1 {
// 低优先级中断– 使用
// 寄存器组1
...
}
void display(void) {
...
}
display函数被8051的各个执行级调用这意味着display函数可被其它调用display
函数的中断中断记住每个中断函数都有它自己的寄存器组因此不会保存当前寄存器组
中的任何数据默认时编译器将使用寄存器组0绝对寻址对display函数进行编译这意
味着编译器将不再产生RO…R7类似的寄存器寻址方式而是代以绝对地址在这里将使
用定时器0的绝对地址00…07
这时问题就产生了当中断服务程序调用display函数的时候那些使用寄存器组0的
代码的数据会被破坏如果display函数仅仅被一个中断服务程序调用那还好办只要
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
144
指定它和中断服务程序使用同样的寄存器组就可以了但在这个例子中它却被多个中断
函数调用
多个中断级调用display函数的时候还会使连接器产生警告信息这些我们留到以
后再处理目前重要的是如何让编译器处理寄存器组的冲突方法就是让编译器使用当
前正在使用的寄存器组而不是寄存器组0 通过对display函数使用编译控制指令NOAREGS
来实现这时编译器产生的代码将使用R0…R7来对寄存器进行寻址而不是绝对地址
display功能函数本身不变在他前面加上一条NOAREGS 编译指令使它对寄存器组的变
化不敏感在它后面的编译指令AREGS 允许文件中的其它函数按照C51的默认值进行编译
#pragma NOAREGS
void display(void) {
...
}
#pragma AREGS
图0-2 多层中断级调用的调用树
现在还有另外一个功能假设display使用几个局部变量来完成它的工作C51将在压
缩栈中为这些变量分配空间根据编译器优化的结果这些空间可能是存储器段或一个
寄存器然而不管处于调用树的什么位置每次调用都是使用同一存储空间这是因为8051
没有像80x86或680x0堆栈那样的功能堆栈一般情况下这不是什么问题但当递归调用
或使用再入函数时将不可避免的出现局部变量冲突
假设定时器0中断执行时调用display函数在函数的执行过程中发生了一个串行中
断中断调用了recv_fsa 函数而该函数又需要display函数display函数执行完之后
局部变量的值也改变了因为寄存器组的切换那些使用寄存器的变量不会被破坏而那
些没有使用寄存器的变量就被覆盖了当串行中断服务程序执行完毕之后控制权交回定
时器中断服务程序这时正处于display程序的调用过程中所有在默认存储段中的局部
变量都已经改变了
为了解决这个问题Keil C51允许使用者把display函数定义成再入函数编译器将
为它产生一个模拟栈每次调用这个函数都会在模拟栈中为它的局部变量分配存储空间
我们按下面的形式定义display函数
#pragma NOAREGS
void display(void) reentrant {
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
145
...
}
#pragma AREGS
如果定义了再入函数程序的存储空间和执行时间都会增加因此要谨慎使用除此
之外还要为再入函数划分足够多的模拟栈空间模拟栈空间的大小通过估计同一时间内
调用再入函数的次数来决定C51可让你来决定所需模拟栈的大小栈的设计是从顶部如
XDATA的0FFFFH 开始向你的变量发展被分配在程序存储区的底部当你编译和连接完
你的程序后应该仔细观察’.M51’文件确保有足够的再入栈空间
4 控制连接覆盖过程
可能出现这种情况因为C51没有真正的堆栈不能实现从多个调用树中调用功能函
数看下面的例子
列表0-3
void main(void) {
IP=0x00; // 所有中断有相同的优先级
init_system();
...
display(0);
IE=0x8A; // 使能定时器0中断和外部中断0
for (;;) {
PCON=0x81; // 进入空闲模式
}
}
void timer_0(void) interrupt 1 using 1 {
// 低优先级中断– 使用
// 寄存器组1
...
display(1);
}
void intr_1(void) interrupt 2 using 1 {
// 低优先级中断– 使用
// 寄存器组1
...
display(2);
}
void display(unsigned char x) {
...
}
因为函数display除了被主函数调用外还被定时器0中断和外部中断1调用产生了
冲突连接器将给出警告
*** WARNING 15: MULTIPLE CALL TO SEGMENT
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
146
SEGMENT: ?PR?_DISPLAY?INTEXAM
CALLER1: ?PR?TIMER_0?INTEXAM
CALLER2: ?PR?INTR_1?INTEXAM
*** WARNING 15: MULTIPLE CALL TO SEGMENT
SEGMENT: ?PR?_DISPLAY?INTEXAM
CALLER1: ?PR?INTR_1?INTEXAM
CALLER2: ?C_C51STARTUP
连接器警告你display函数可能被中断服务程序中断而中断服务程序也调用display
函数这就导致了局部变量冲突第一个警告说定时器0中断和外部中断1都调用了display
函数第二个警告说主函数和外部中断1之间也存在冲突仔细检查代码结构后可把
display函数定义成再入函数
定时器0和外部中断1具有同样的中断优先级这两者之间不会导致冲突可以不用考
虑第一个警告主程序中调用display函数时可被中断这也不要紧因为当主程序调用
display函数时中断还没被使能这两个警告都被证明是安全的这并不说明连接器出
错了它已经作了自己的工作那就是当没有把多重调用的函数声明为再入函数时给出警
告信息连接器不会为你作代码分析哪个中断会发生在什么时候这是工程师的工作
当确认不必担心警告后该怎样做呢最简单的方法就是忽略不管但这会影响连接
器连接模块和为可重定位目标分配地址虽然连接器还是会输出一个可执行文件但却没
有充分利用存储空间因为连接器不能正确的进行覆盖分析了所以不应忽略警告
有两种方法可以除去警告一种是告诉连接器不进行覆盖分析这会使连接出来的代
码使用很多不必要的DATA空间但很容易实现第二种是帮助连接器进行覆盖分析迫使
他忽略由调用树产生的参考信息一旦你告诉它只保留一棵树的参考信息就不会在产生
警告覆盖分析也能正常进行了显然第二种方法是比较好的但如果你时间不多且
存储空间比较大的时候也可选择第一种方法
我们用L51这个连接命令进行代码的连接
L51 example.obj
要让L51不进行覆盖分析只要在连接选项对话框中取消”enable variable overlaying”
就可以了
第二种方法有点麻烦但是值得你需要去掉三个功能调用中两个产生的调用参考信
息这需要使用命令行中的覆盖选项display函数被’main’,’timer_0’,’intr_1’三者调用
你必须去掉其中两个产生的参考信息一般来说留下调用次数最多的那一项在这里外
部中断1很少发生定时器0是系统时标经常产生中断因此留下定时器0调用树中的参
考项新的L51命令如下命令行中的覆盖部分应该被输入连接设置对话框中的”Additonal”
框中
L51 example.obj overlay(main ~ _display, intr_1 ~ _display)
很多代码在第一次连接的时候都会产生多重调用警告信息采用上面提到的方法或声
明再入函数可以消除这些警告信息你不可能一步消除所有的警告信息多试几次确保
把它们都消除
5 使用64K 或更多RAM
如果你用8051开发复杂的系统有可能不得不使用64K字节的RAM 而且还要进行I/O
寻址操作这时I/O器件地址和RAM地址将重叠可使用端口1的引脚或通过锁存器的引脚
使能或禁能RAM 下面是一个例子
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
147
图0-3 RAM和I/O重叠
要对RAM操作时先把P0.1置高然后再进行寻址当8051复位时P0.1应该为高
使能RAM 如果软件要通过总线对I/O器件寻址只需要把P1.0拉低禁能RAM并使能地址解
码器就可以对器件寻址了
软件正常执行时RAM被使能如果要对外部I/O器件进行操作时调用一个特殊功能
禁能外部RAM 通过内部RAM输入输出数据当操作完成后再使能RAM 软件继续正常运
行进行I/O操作的功能函数见列表0-4 这个功能通过内部RAM传递参数不需要使能外
部RAM
列表0-4
#include reg51.h
#include absacc.h
sbit SRAM_ON = P1^0;
/*****************************************************************
功能: output
描述: 向指定XDATA地址写入数据
参数: 地址- unsigned int. 要写入数据的地址
数据- unsigned char. 保存需要输出的数据
返回: 无
负面影响: 外部RAM被禁能这时不能发生中断因此中断系统暂时被挂起
*****************************************************************/
void output(unsigned int address, unsigned char value) {
EA=0; // 禁止所有中断
SRAM_ON=0; // 禁能外部RAM
XBYTE[address]=value; // 输出数据
SRAM_ON=1; // 使能RAM
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
148
EA=1; // 使能中断
}
/*****************************************************************
功能: input
描述: 从XDATA地址中读入数据
参数: 地址- unsigned int.读取数据的地址
返回: 读取的数据.
负面影响: 外部RAM被禁能这时不能发生中断因此中断系统暂时被挂起
*****************************************************************/
unsigned char input(unsigned int address) {
unsigned char data value;
EA=0; // 禁止所有中断
SRAM_ON=0; // 禁能RAM
value=XBYTE[address]; // 读入数据
SRAM_ON=1; // 使能RAM
EA=1; // 允许中断
return value;
}
禁能和使能RAM的这种概念可以被扩展使你的RAM超过64K 在大多数情况下64K RAM
对8051系统来说已经足够了但是如果碰到大量的操作或存储大量数据的情况时所需要
的RAM可能就不止64K了可把RAM的特殊地址线接到P1口或74HC373上用软件来选择所需
的RAM页面这个例子中第0页大多时候被使能对其它页面的操作像上面的系统中对I/0
器件的操作一样RAM页面0用来存储程序变量传递参数作为压缩栈等其它的RAM页
面用作存放系统事件表查询表和一些不经常使用的数据系统连接见图0-4 和前面
不同的是P1.1和P1.2用来作为256K RAM的高两位地址线
图0-4 页寻址RAM
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
149
连接在总线上的I/O器件的寻址方法和前面所讲的一样再重申一遍系统默认的状
态是P1.0为高以使能RAM P1.1和P1.2为低选择RAM页面0 如果在地址选择线上面加了
反向器那么上电复位时将自动选择页面0 否则的话如果你XDATA中有变量要在编译的
时候初始化则需要在startup.a51中加入代码清零P1.1和P1.2
向其它RAM页面写入数据或从中读取数据是以块的方式使用一个内部RAM缓冲区这
就避免了页面间的频繁的切换缩短的操作的时间但是要占用一定的内部RAM空间并
且每次传输的最大数据量有一个限制RAM页面寻址操作的代码见列表0-5
列表0-5
#include reg51.h
#include absacc.h
sbit SRAM_ON = P1^0;
unsigned char data xfer_buf[32];
/*****************************************************************
功能: page_out
描述: 向指定XDATA地址写入数据
参数: 地址- unsigned int. 数据写入地址
页面- unsigned char. 使用的RAM页面
数量- unsigned char. 写入的字节数
返回: 无
负面影响:外部RAM禁能因此允许发生中断
中断系统被暂时挂起
*****************************************************************/
void page_out(unsigned int address, unsigned char page,
unsigned char num) {
unsigned char data i;
unsigned int data mem_ptr; // 通过移动指针来进行数据拷贝
mem_ptr=address;
num&=0x1F; // 最大字节数为32
page&=0x03; // 页选面为0..3
page=1;
page|=0x01; // 外部RAM使能
EA=0; // 关闭所有中断
P1=page; // 选择新页面
for (i=0; inum; i++) {
XBYTE[mem_ptr]=xfer_buf
; // 向指定地址写入数据
mem_ptr++;
}
P1=1; // 选择页面0
EA=1; // 使能中断
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
150
/*****************************************************************
功能: page_in
描述: 从指定RAM页面的地址处读取数据
参数: 地址- unsigned int. 读取数据的地址
页面- unsigned char. 使用的页面
数量- unsigned char. 读取数据的字节数
返回: 无
负面影响: 外部RAM禁能因此允许发生中断
中断系统被暂时挂起
*****************************************************************/
void page_input(unsigned int address, unsigned char page,
unsigned char num) {
unsigned char data i;
unsigned int data mem_ptr; // 通过移动指针来进行数据拷贝
mem_ptr=address;
num&=0x1F; // 限制最大字节数为32
page&=0x03; // 页面选择从0..3
page=1;
page|=0x01; // 使能外部RAM
EA=0; // 关闭所有中断
P1=page; // 页面选择
for (i=0; inum; i++) {
xfer_buf
=XBYTE[mem_ptr]; // 读取下一个地址数据
mem_ptr++;
}
P1=1; // 选择页面0
EA=1; // 使能中断
}
这里我采用了局部变量’mem_ptr’ for循环每次使地址加1 而汇编后产生的代码将把
这个地址存储在XDATA区中为了避免对XDATA区进行寻址就在DATA区声明局部变量来保
存地址
上面的页面功能在很多情况下都是可行的一些程序员可能希望有像C51库函数那样
提供一套存储区操作功能如’memcpy’ 如果通用指针能对RAM页面进行寻址的话可编写
出一套和函数库功能相似的函数Keil C的通用指针包含3个字节两个字节存放地址
一个选择字节确定指针选择的存储空间根据存储空间的不同选择字节的范围从1到5
这样字节的前几位没有被用到现在当寻址空间是XDATA区时将用它们来表示指针
所指的RAM页面这个简单的改动将使新的库函数和以前的很像我们在这里给出’mempcy’
的代码其余的留给你们去编写’page_mempcy’的声明如下
void *page_memcpy(void *dest,void *source,int num);
由于要在页面间进行快速的切换所以用汇编来写并使用大存储模式
列表0-6
?PR?PAGE_MEMCPY?PAGE_IO SEGMENT CODE
?XD?PAGE_MEMCPY?PAGE_IO SEGMENT XDATA OVERLAYABLE
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
151
PUBLIC _page_memcpy, ?_page_memcpy?BYTE
RSEG ?XD?PAGE_MEMCPY?PAGE_IO
?_page_memcpy?BYTE:
dest: DS 3
src: DS 3
num: DS 2
;*****************************************************************
; 功能: _page_memcpy
; 描述: 从源指针所指的地址拷贝一定数量的字节到目的指针所指的地址处
; 允许xdata区指针通过使用指针区域选择字节的高位指定RAM页面
; 参数: 目的– 可选择RAM页面的通用指针通过R1..R3传递指明拷贝数据
; 的目的地址
; 源- 可选择RAM页面的通用指针指明被拷贝数据的开始地址
; 数量- unsigned integer. 指明拷贝的字节数
; 返回: 目的地址
; 负面影响: 无
;*****************************************************************
RSEG ?PR?PAGE_MEMCPY?PAGE_IO
_page_memcpy: PUSH 07 ; 保存寄存器数据
PUSH 06
PUSH 02
PUSH 01
PUSH 00
PUSH ACC
PUSH B
MOV DPTR, #?_page_memcpy?BYTE+6
MOVX A, @DPTR ; 取拷贝字节数
MOV 06, A
INC DPTR
MOVX A, @DPTR
MOV 07, A
ORL A, 06
JZ GTFO ; if (!num) { return }
MOV DPTR, #?_page_memcpy?BYTE
MOV A, 03 ; 装入目的指针
MOVX @DPTR, A
INC DPTR
MOV A, 02
MOVX @DPTR, A
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
152
INC DPTR
MOV A, 01
MOVX @DPTR, A
L1: LCALL GETSRC ; 取下一个源字节
LCALL PUTDEST ; 写入下一个字节
MOV A, 07 ; num--
CLR C
SUBB A, #1
MOV 07, A
MOV A, 06
SUBB A, #0
MOV 06, A
ORL A, 07
JZ GTFO ; if (!num) { return }
JMP L1
GTFO: POP B ; 恢复所有寄存器
POP ACC
POP 00
POP 01
POP 02
POP 06
POP 07
RET
;*****************************************************************
; 功能: GETSRC
; 描述: 从源指针所指的地址读入数据,指针加1
; 返回所读数据
; 参数: 无
; 返回: 把数据读入A
; 负面影响: 无
;*****************************************************************
GETSRC: MOV DPTR, #?_page_memcpy?BYTE+3
MOVX A, @DPTR ; 得到源地址页面选择字节
MOV B, A ; 保存
DEC A ; scale selector to 0..4
ANL A, #00FH ; 除去RAM页面
MOV DPTR, #SEL_TABLE1
RL A
JMP @A+DPTR ; 存储器类型选择
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
153
SEL_TABLE1: AJMP SEL_IDATA1 ; idata
AJMP SEL_XDATA1 ; xdata
AJMP SEL_PDATA1 ; pdata
AJMP SEL_DATA1 ; data or bdata
AJMP SEL_CODE1 ; code
SEL_PDATA1: MOV 00, #00 ; 对于pdata, 地址头字节
; 必定为00
MOV DPTR, #?_page_memcpy?BYTE+5
JMP L2
SEL_XDATA1: MOV DPTR, #?_page_memcpy?BYTE+4
MOVX A, @DPTR ; 读入地址
MOV 00, A
INC DPTR
L2: MOVX A, @DPTR
MOV DPH, 00 ; set DPTR to XDATA address
MOV DPL, A
MOV A, B ; 得到RAM页面地址
ANL A, #0F0H
SWAP A
RL A
ORL A, #01H
MOV P1, A ; 选择RAM页面
MOVX A, @DPTR ; 读入字节
MOV 01, A ; 保存
MOV P1, #01H ; 恢复RAM page
INC DPTR
MOV 00, DPL
MOV A, DPH
; 保存新的地址
MOV DPTR, #?_page_memcpy?BYTE+4
MOVX @DPTR, A
INC DPTR
MOV A, 00
MOVX @DPTR, A
MOV A, 01 ; 把返回字节存入A中
RET
SEL_CODE1: MOV DPTR, #?_page_memcpy?BYTE+4
MOVX A, @DPTR ; 取得当前源地址
MOV 00, A
INC DPTR
MOVX A, @DPTR
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
154
MOV DPH, 00 ; 用当前地址设置DPTR
MOV DPL, A
CLR A
MOVC A, @A+DPTR ; 读入字节
MOV 01, A
INC DPTR ; 指针加1
MOV 00, DPL
MOV A, DPH
MOV DPTR, #?_page_memcpy?BYTE+4
MOVX @DPTR, A ; 保存指针
INC DPTR
MOV A, 00
MOVX @DPTR, A
MOV A, 01 ; 返回字节
RET
SEL_IDATA1:
SEL_DATA1: MOV DPTR, #?_page_memcpy?BYTE+5
MOVX A, @DPTR ; 取一个字节地址
MOV 00, A
MOV A, @R0
INC R0 ; 指针加1
XCH A, 00
MOVX @DPTR, A ; 保存指针
XCH A, 00 ; 返回字节
RET
;*****************************************************************
; 功能: PUTDEST
; 描述: 将A中的字节写入目的指针所指向的地址
; 然后指针加1
; 参数: 无.
; 返回: 无.
; 负面影响:无.
;*****************************************************************
PUTDEST: MOV 02, A ; 保存输出数据
MOV DPTR, #?_page_memcpy?BYTE
MOVX A, @DPTR ; 取得目的指针类型字节
MOV B, A ; 保存类型字节
DEC A
ANL A, #00FH
MOV DPTR, #SEL_TABLE2
RL A
JMP @A+DPTR ; 类型选择
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
155
SEL_TABLE2: AJMP SEL_IDATA2 ; idata
AJMP SEL_XDATA2 ; xdata
AJMP SEL_PDATA2 ; pdata
AJMP SEL_DATA2 ; data or bdata
AJMP SEL_CODE2 ; code
SEL_PDATA2: MOV 00, #00 ; 对pdata区地址高字节
; 必定为0
MOV DPTR, #?_page_memcpy?BYTE+2
JMP L4
SEL_XDATA2: MOV DPTR, #?_page_memcpy?BYTE+1
MOVX A, @DPTR ; 读入地址
MOV 00, A
INC DPTR
L4: MOVX A, @DPTR
MOV DPH, 00 ; 设置DPTR为外部地址
MOV DPL, A
MOV A, B ; 取得RAM页面
ANL A, #0F0H
SWAP A
RL A
ORL A, #01H
MOV P1, A ; 选择RAM页面
MOV A, 02
MOVX @DPTR, A ; 输出数据
MOV 01, A ; 保存
MOV P1, #01H ; 恢复RAM页面
INC DPTR ; 指针加1
MOV 00, DPL
MOV A, DPH
; 保存新地址
MOV DPTR, #?_page_memcpy?BYTE+1
MOVX @DPTR, A
INC DPTR
MOV A, 00
MOVX @DPTR, A
RET
SEL_CODE2: RET ; 不能写入CODE区
SEL_IDATA2:
SEL_DATA2: MOV DPTR, #?_page_memcpy?BYTE+2
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
156
MOVX A, @DPTR ; 取一个字节地址
MOV 00, A
MOV @R0, 02
INC R0 ;指针加1
XCH A, 00
MOVX @DPTR, A ;保存指针
RET
END
改变以上功能的存储模式,只需要修改地址寻址的代码就可以了以上面为例其它
的存储区操作功能函数也可很容易编写出来
6 使用64K以上的代码空间
在十分复杂的8051控制系统中,软件的规模随着功能的加强而不断的扩大可执行代
码的长度也不断的增加当代码的长度超过64K时问题就变得复杂了你将面临几个选
择
第一个选择就是把增强的功能去掉这可能会影响产品的市场第二个选择就是通过
优化程序代码来腾出空间存放新增代码但这是比较困难的特别对那些已经优化过了的
代码这个方法虽然可行但不是长久之计第三个选择就是重新设计系统起用新的可
支持大代码空间的控制器这意味着重新更新你的软硬件和使用新的开发工具----非常糟
糕的选择最后一个选择就是稍微改变一下硬件设计并增加系统中EPROM的数量使之超
过64K 你可能会产生疑虑因为8051最大的寻址范围才64K呀不用担心Keil提供的功
能包可使8051寻址的代码空间达到1MB
使用Keil的BL51 可使用类似于前面RAM页面寻址的方式来增加代码空间EPROM被分
页每页的大小和在页间进行跳转的方式取决于你的应用还有就是要有一个共用空间
这个空间是处理器在任何时候都能够寻址的这个区域存储包括中断向量中断功能函数
可能调用其它EPROM页面的函数C51库函数在页面间跳转的代码和被多个页面代码
使用的常量有两种方法提供共用空间一是使用单独的EPROM 二是在每页的底部都复
制公共代码我是比较倾向于第二种方法因为这可以最大限度的使用页面空间
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
157
图0-5 页寻址EPROM
假设你有一个20K的公共区域如果使用第二种方法则每页仍有44K的可用代码区
如果使用第一种方法则需要一个32K的EPROM 这样你每页的可用的代码区最大只有32K
把EPROM的地址线或使能端连接到P1口或数据锁存器的引脚上方法和RAM页面寻址一
样如图0-5所示这里没有使用8个64K字节的EPROM 而是512K字节的EPROM 我们所作
的一切就是换个大点的EPROM并接上几根地址线就行了
使用新增的程序代码存储器是很简单的像以往一样编写软件你只需要把相关的功
能放在一个文件中这样当用连接器连接的时候就可以把他们放在同一个CODE页面中这
样在程序执行过程中可以尽量减少页面中的切换页面切换的操作越少处理其它任务的
时间就越多除此之外还应该尽量减少常量或变量的使用他们被单独保存在一个文件
中这样可以减少公共区的大小使得每页有更多的代码空间
如何对你的功能函数进行分类呢最好的分析方法就是建立每个中断的调用树这样
你就可以知道那些功能应该放在一起以减少不需要的页面跳转然后建立另外一个列表
显示功能函数使用那些变量和常量被一组功能使用的常量被放在同一个文件中这个文
件和这组功能函数又在同一个代码页面中这时你将不得不在公共区大小和页面跳转次数
之间进行权衡当你的代码空间足够时可以进行功能组合优化否则应该减少常量和
变量的分布
结束功能和模块的分组之后你需要修改L51_BANK.A51文件告诉它代码页的数量
在页面间跳转的方式等这些修改见列表0-7
列表0-7
$NOCOND DEBUGPUBLICS
;------------------------------------------------------------------------
; This file is part of the BL51 Banked Linker/Locater package
; Copyright (c) KEIL ELEKTRONIK and Keil Software GmbH 1989-1994
; Version 1.2
;------------------------------------------------------------------------
;********************** Configuration Section ***************************
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
158
change one - ensure that the proper number of code banks is set
?B_NBANKS EQU 8 ; Define max. Number of Banks *
; *
?B_MODE EQU 0 ; 0 for Bank-Switching via 8051 Port *
; ; 1 for Bank-Switching via XDATA Port *
; *
IF ?B_MODE = 0; *
;-----------------------------------------------------------------------*
; if ?BANK?MODE is 0 define the following values *
; For Bank-Switching via 8051 Port define Port Address / Bits *
?B_PORT EQU P1 ; default is P1 *
change two - set the bank switching LSB
?B_FIRSTBIT EQU 0 ; default is Bit 3 *
;-----------------------------------------------------------------------*
ENDIF;
将修改后的L51_BANK.A51文件编译后产生的目标文件和你的源代码目标文件连接所
使用的命令不再是L51 而是BL51 BL51是Keil提供的增强连接器可进行多代码页面和
实时操作系统的处理
BL51支持L51使用的命令还有一些命令可以指定如何在代码页面中安排模块和段
这点在功能集成中十分重要
运行BL51时需要提供一些参数使BL51能够对你的各个段正确定位你需要使用BL51
的第一条指令是’BANKAREA’,’BANKAREA’告诉BL51代码页面的物理地址在本例中为0000HFFFFH
’COMMON’指令告诉连接器那些功能和模块放入公共区域并被装入到每个页面
中’COMMON’指令在BL51手册中定义你还能使用’BANKx’指令指定装入各个代码页面中的功
能段和模块下面是本例中的BL51连接指令
BL51 COMMON{C_ROOT.OBJ},
BANK0{BANK0.OBJ},
BANK1{BANK1.OBJ},
BANK2{BANK2.OBJ},
BANK3{BANK3A.OBJ,BANK3B.OBJ},
BANK4{BANK4.OBJ},
BANK5{?PR?MYFUNC?MISC, BANK5.OBJ},
BANK6{TABLES.OBJ,BANK6.OBJ},
BANK7{BANK7.OBJ,MISC.OBJ}
BANKAREA(0000H,0FFFFH)
你可以在一个页面中放入多个模块也可指定模块中的某一段对公共区域也是这样
最后一点是连接器会在每个页面中根据各段列出的顺序为它们分配地址如果你想一
个功能段在代码段的低地址把它作为BANKx指令的第一个参数
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
159
7 结论
这章我们讨论了如何使用Keil提供的开发包来提高你的程序给中断服务程序分配寄
存器组控制连接器的覆盖可以提高代码的性能还可以对系统的RAM和ROM进行扩展希
望读者能够充分的利用这些功能下一章我们将讨论设计方面的技术这些技术可以说是
未来的趋势
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
160
第十章8051的模糊控制
1 介绍
软件开发就和社会一样都是向前发展的在嵌入式控制系统特殊控制系统中最
新的发展趋势是模糊逻辑控制美国的工程师已经开始对模糊逻辑进行研究发现它在解
决某些问题方面是一个非常好的工具但是现在有不少人鼓吹模糊逻辑能够解决任何系统
问题要你们买他的开发包你应该清醒的认识到模糊逻辑虽然对不少系统来说是个非
常好的解决方案但对很多系统来说帮助不大这章你将了解不需要去购买昂贵的模糊逻
辑开发工具有一种简单的方法可在8051上使用模糊控制在此之前你要先了解一下模
糊控制
2 什么是模糊逻辑
我们认识事物的一般逻辑是要么对要么错不可能两者都是举个例子5比10
小是对的这种逻辑和很多情况如线形问题是相符合的而且也可用于曲线的情况
它的优点就是很适用于计算机这种使用二进制数的机器但是在很多情况下这种逻辑并
不适用
在真实的世界中很多事物在某种程度上
是真确的或是错误的一部份真并且一部份假
在模糊逻辑中是基本概念模糊逻辑中是用一
个数据点在某个指定范围内出现的程度来表示
这个概念的1表示一定在范围内0表示一定
不在范围内在0和1之间有无限种程度如.25 .5 .75等表0-1
假设如果室外温度为90度那么对于这个温度用能用表0-1列出的天气类型来描述
在这里每种天气类型可以看成是一个范围数据点90度属于这个范围的程度被
列了出来对于一个数据点它属于某个范围的程度是有严格定义的这个例子用一张图
表给出了它们之间的关系cold,chilly,mild,warm,hot分别对应着一个函数
图0-1 温度函数
从图中可以看出90度两个函数warm,hot 有交点,因此它们的模糊度不为0 而
其它的函数没有交点所以模糊度为0 90度和warm函数的交点的纵坐标为0.25 即它的
模糊度同样在hot范围内的模糊度为1.00
功能函数曲线可以为任何形状最为常见的为梯形其它形状都可以从梯形演变过来
下面是一些常见的模糊功能函数图
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
161
图0-2 一些模糊函数类型
前两个曲线形状singleton和crisp 类似于布尔逻辑要么u=0 要么u=1 其余的
函数曲线u可以为任何值u为模糊度改变trapezoidal曲线的曲度可以使之成为
singleton crisp triangular型曲线这样就可以用一种模糊曲线表示四种模糊设置
trapezoidal triangular crisp和singleton 另外两种函数比较难表示
模糊分析最基本的要素就是模糊函数类型一个模糊系统由模糊函数和相应的操作组
成例如一个基于温度的风扇调速模糊控制系统遵循这样的原则如果温度升高则风
扇速度加快通过现在的温度得到一个热的模糊度然后将这个模糊度和标准值进行比较
以决定是否是热的状态然后采取相应的行动
一般来说一个模糊逻辑规则包括’if’部分和’then’部分’if’部分可包含多个条件,’then’
部分也可包含多个结果条件和结果都可用AND OR NOT这样的逻辑操作联系起来当然
还有其它的模糊操作但这三种是最常见的这三种操作的算术意义见表0-2
一个逻辑系统由一系列逻辑规则组成而每个逻辑规
则又可有多个条件和结果使用逻辑规则的数量由系统决
定这一系列逻辑规则被称为规则基还可以为每个规则
提供一个权在大多数模糊系统中每个规则的权都被置1
表明每个规则的重要性都是一样的但是有些系统中其中表0-2
一些规则比其它的规则重要那么它的权可以取大一些例如把更加重要的规则的权值取
为1 其它规则的权值小于1 这是因为模糊逻辑中处理的值一般都是从0到1
规则基的大小取决于要解决问题的大小一般的模糊系统的规则基都比较小大约15
个规则越复杂的系统规则越多但是即使系统是大系统规则的数量都控制在60以下
因为规则越多系统作出决定所需要的时间就越长你不必把所有可能的规则都放到系统
中去一个小的规则基一样可以控制系统的操作但规则越多系统就越稳定这使模糊逻
辑系统能够容忍异常的输入信号
3 模糊系统的结构
一个模糊逻辑系统需要三个操作阶段输入预处理模糊推断反模糊处理三者的
关系见图0-3
图0-3 模糊系统结构
预处理阶段进行数据采集这个阶段包括测量输入的数据得到模糊函数规定范围内
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
162
的值然后用模糊规则进行评估如果需要的话要对输入进行计算例如有一个用于
汽车的模糊系统把加速度作为一个输入则需要每隔一定的时间对速度进行采样然后计
算加速度
模糊推断估计每个模糊规则并计算它对输出结果的影响用前面所提供的方法来得
到模糊度由条件得到的u值反映出结果是真的程度当规则的输出u值大于0时就称这
个规则触发了
每个规则的结果的输出是遵循模糊设置的评估阶段存储每个模糊输出在每个可能设
置下的最大的u值用上面的例子如果温度是热那么风扇转速是高设输入温度为90
度则对应的u值为1.00 因为90度完全在热的范围内因此风扇转速是高的正确程
度是1.00 如果当前风扇转速是高的模糊度是0 那么现在就变成了1 但是风扇转
速是高的模糊度是1并不意味着风扇转速就将置高这还要看其它模糊功能输出函数的
结果
反模糊处理阶段利用所得到的各种模糊输出值和数学方法计算最终的系统输出值有
几个常用方法最简单的是最大值法最大值法规定用给定输出的u的最大值决定与输出
相关的操作例如给出输出转速的真实度
ulow = 0.00
umedium = 0.57
uhigh = 1.00
因此转速将被置高因为设置high对应着最高值最大值法很容易实现但是当数
据点落在多个区域中的话这种方法就没有体现模糊控制的优点
反模糊输出的一般方法是重心法还是利用前面的条件可以作出下面的图
图0-4 反模糊处理
计算阴影部分的重心这个重心值就为反模糊处理的输出值这种方法虽然好但是
计算非常复杂重心法一种简单的处理是把图形当成矩形处理有下面的公式
这里n是输出的设置数Vi是定义的矩形的长度Ui是每种设置的真实度由这个公式
得到的结果和重心法得到的结果很相似而且容易实现
4 模糊控制使用的场合
讲到这里有读者可能会问哪些场合适合使用模糊控制哪些场合不适合使用模糊
控制一般准则是如果你的系统已经有了一个精确的能够使用传统逻辑有效处理的数
学模型的时候就不要使用模糊控制了而对一些不能够准确描述的系统但是又能够凭经
验控制这一般都是一些复杂的非线形系统这时可以请一些有经验的专家来指定系统
的操作规则模糊控制系统能够根据这些规则得到正确的输出
模糊控制的一个优点就是你能够根据用语言来表达系统的解决方案这使得解决方
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
163
案更加符合人的思考习惯除此之外模糊系统很容易通过修改规则和模糊函数来进行调
整模糊控制的一个缺点是你必须证明你的方案对所有的输入都是有效的就像传统的
逻辑方案一样
5 进行模糊控制
有了一些关于模糊的基本概念之后我们来设计一个小的系统从中可以学到一些模
糊控制系统的设计方法进行设计之前先用简单的语言来描述这个系统这意味着你要
知道什么是输入量什么是输出量和它们的类型
假如你设计一个系统控制一辆自动力汽车在某一点停下来为了实现这个目的需
要知道汽车现在离这一点的距离和汽车现在的速度而输出将控制刹车的力度
用上述语言描述系统之后我们确定了两个输入量汽车离停车点的距离和汽车现在
的速度系统的输出量为刹车的程度下面的任务就是要定义输入和输出的模糊范围你
先不要考虑具体的数值而是做一些基本的描述
根据对系统的感性认识用语言来描述它这种认识可以来自专家或通过调查和研究
举个例子可以找一个驾驶汽车有20年之久的司机从他那里了解操作汽车的各种参数
在这里我根据驾驶汽车一般的经验来进行设计
首先考虑离停车点的距离当汽车和停车点的距离达到一定的程度之后才启动系统
因为驾驶员不会在汽车离停车点还距离一公里的时候就去考虑放慢速度准备刹车的当
汽车离停车点还有几百米的时候才会开始减速我们可以把汽车离停车点的距离语言符
号为DISTANCE 分为几类FAR NEAR CLOSE VCLOSE(very close) 其中VCLOSE设置中
包括汽车已经到达停车点
第二个输入量是汽车的速度语言符号是VELOCITY 它也被分成几类VSLOW FAST
MEDIUM SLOW VSLOW 其中VSLOW设置中包括速度0
输出量为刹车的力度用符号BRAKE表示也被分为了几类NONE LIGHT MEDIUM VHARD
它们的数学意义将在以后定义
有了输入输出量之后就可以定义规则了有些模糊控制系统的设计者认为成员函数
应该在规则定义之前定义不过这只是个人喜好问题我先定义规则的原因是这样可以
更加全面的了解系统
定义规则基最简单的办法是用输入量建立一个表格然后填入输出量这使系统更加
具体但要注意表格只适用于输入量为AND操作的情况如下表
表0-3
一旦建立了上面的表格之后就把它看成真值表把结果填进去举个例子如果速
度为VFAST并且距离为FAR 那么结果为MEDIUM 下面为完成的表格
表0-4
有些时候一些规则可被简化成一个规则假设规则如果VELOCITY是VSLOW并且
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
164
DISTANCE是VCLOSE 那么BRAKE是LIGHT 改成如果VELOCITY是VSLOW并且DISTANCE是
VCLOSE 那么BRAKE是NONE 表的最底层可简化为如果VELOCITY是VSLOW 那么BRAKE是
NONE
系统规则被建立起来之后就开始建立每个模糊设置的成员函数你要知道每个输入
的范围例如要建立VELOCITY的模糊成员函数你要知道它的范围是从0MPH到25MPH 下
面是VELOCITY的成员函数
图0-5 VELOCITY的成员函数
成员函数的定义没有一定的规则根据实际系统的具体情况而定不一定要包含横坐
标上面所有的点也不一定非要有数据点同时对应着两个函数下面是DISTANCE和BRAKE的
成员函数
图0-6 DISTANCE的成员函数
图0-7 BRAKE的成员函数
当所有这些完成之后就开始编写程序来实现这些模糊功能
6 模糊功能的实现
用8位控制器进行逻辑控制首先对规则和成员函数进行定义定义可以手工进行也
可以使用买来的工具这里我们使用手工的方法
在8位机上实现逻辑控制要考虑如何在系统中表达这些逻辑规则最好把条件和结果
都用8位数据表示这样就不能使用规则的权和使用括号把一些操作放入条件中这里讨论
的模糊逻辑不提供AND和OR操作以及超过8位数据的输入量和输出量处理
逻辑规则基被放入一个存在代码区的数组中数组中的每个元素为一个字节包含了
逻辑规则的一个分支如果存储空间够的话也可以把数据放在一个结构中这能使你快
速的得到分支的信息以字节存储分支的结构如下
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
165
表0-5
如果第7位为0就认为这个分支为条件分支2到0位的值就是输入数据6到4位的值就
是输入量的成员函数第3位表示对这个条件得到的u值是进行或操作还是与操作
如果第7位为1就认为这个分支为结果分支2到0位的值就是输出数据第3位对结果分
析没有影响
知道了分支的内部结构之后就很容易建立规则了还是汽车的例子设输入0为速度
输入1为距离输出只有一个刹车力度设为输出0 功能函数也从0到n 本例中n的最大值
为5 因为速度输入有5个成员函数在代码中为每个可能的分支定义常量例如VELOCITY
IS VFAST 的常量名为VEL_VFAST 列表0-1为常量定义
列表0-1
// 定义速度输入常量
#define VEL_VSLOW 0x00
#define VEL_SLOW 0x10
#define VEL_MEDIUM 0x20
#define VEL_FAST 0x30
#define VEL_VFAST 0x40
// 定义距离输入常量
#define DIST_VCLOSE 0x01
#define DIST_CLOSE 0x11
#define DIST_NEAR 0x21
#define DIST_FAR 0x31
// 定义刹车输出常量
#define BRAKE_NONE 0x80
#define BRAKE_LIGHT 0x90
#define BRAKE_MEDIUM 0xA0
#define BRAKE_HARD 0xB0
#define BRAKE_VHARD 0xC0
有了上面的常量定义后规则的描述就很简单了对于汽车系统来说规则的形式为
如果输入x1为y1且输入x2为y2 那么输出x3为y3 你也可以通过分配分支的位置使用像
AND和OR这样的连接词例如要表达如果velocity为vfast或velocity为slow且距离为far
那么刹车为none 可用下面的方法
VEL_FAST, VEL_SLOW | 0x08, DIST_FAR, BRAKE_NONE
把建立的所有规则存入数组中在汽车的例子中所有可能的规则都存在一个规则基
数组中
列表0-2
unsigned char code rules[RULE_TOT]={ // 模糊系统规则
// if... and... then...
VEL_VSLOW, DIST_VCLOSE, BRAKE_LIGHT,
VEL_VSLOW, DIST_CLOSE, BRAKE_NONE,
VEL_VSLOW, DIST_NEAR, BRAKE_NONE,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
166
VEL_VSLOW, DIST_FAR, BRAKE_NONE,
VEL_SLOW, DIST_VCLOSE, BRAKE_MEDIUM,
VEL_SLOW, DIST_CLOSE, BRAKE_LIGHT,
VEL_SLOW, DIST_NEAR, BRAKE_NONE,
VEL_SLOW, DIST_FAR, BRAKE_NONE,
VEL_MEDIUM, DIST_VCLOSE, BRAKE_HARD,
VEL_MEDIUM, DIST_CLOSE, BRAKE_HARD,
VEL_MEDIUM, DIST_NEAR, BRAKE_MEDIUM,
VEL_MEDIUM, DIST_FAR, BRAKE_LIGHT,
VEL_FAST, DIST_VCLOSE, BRAKE_VHARD,
VEL_FAST, DIST_CLOSE, BRAKE_VHARD,
VEL_FAST, DIST_NEAR, BRAKE_HARD,
VEL_FAST, DIST_FAR, BRAKE_MEDIUM,
VEL_VFAST, DIST_VCLOSE, BRAKE_VHARD,
VEL_VFAST, DIST_CLOSE, BRAKE_VHARD,
VEL_VFAST, DIST_NEAR, BRAKE_HARD,
VEL_VFAST, DIST_FAR, BRAKE_MEDIUM
};
规则建立完毕下面开始定义模糊成员函数我们认为你的输入功能函数要么是梯形
的要么可以从梯形转变过来而为了简化反模糊处理输出功能函数都为矩形
当输入功能函数为梯形时用4个字节就可以描述它我们把梯形看成一个切去头部
的三角形软件通过存储折点和斜率来描述这个三角形图0-8是一个例子
图0-8
存储点1和点3 斜率1和斜率2 有了这4个值软件就可以得到u值u值用一个无符号
字节来表示FFH为全真OOH为全假用整型来计算u值以防范围超过范围当超过范围
时可以把数值截取在00H到FFH之内功能函数值被存储在一个3维数组中见列表0-3
列表0-3
unsigned char code input_memf[INPUT_TOT][MF_TOT][4]={
// 输入功能函数以点斜式方式存储. 第一维是输入号
// 第二维是成员函数标号第三维是点斜式数据
// 速度功能函数
{
{ 0x00, 0x00, 0x1E, 0x09 }, // VSLOW
{ 0x1E, 0x0D, 0x50, 0x09 }, // SLOW
{ 0x50, 0x0D, 0x96, 0x0D }, // MEDIUM
{ 0x8C, 0x06, 0xC8, 0x09 }, // FAST
{ 0xC8, 0x0D, 0xFF, 0x00 } // VFAST
},
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
167
// 距离功能函数
{
{ 0x00, 0x00, 0x2B, 0x0A }, // VCLOSE
{ 0x33, 0x08, 0x80, 0x0A }, // CLOSE
{ 0x6E, 0x07, 0xC7, 0x08 }, // NEAR
{ 0xC7, 0x0A, 0xFF, 0x00 } // FAR
}
};
表中的值由你的功能函数决定当功能函数的取值范围确定以后把它们转换成新的
范围从0到FF 4个点按照这个范围进行转换当你有了这4个点的16进制值之后可用
下面的程序把他们在转化成所需要的斜率程序的列表如下
列表0-4
#include stdio.h
void main(void) {
unsigned char val[4], output[4], ans, flag;
do {
printf(\n\nenter 4 hex points: );
scanf( %x %x %x %x, val[0], val[1], val[2], val[3]);
output[0]=val[0];
output[2]=val[2];
if (val[1]-val[0]) {
output[1]=(0xFF+((val[1]-val[0])/2))/(val[1]-val[0]);
} else {
output[1]=0;
}
if (val[3]-val[2]) {
output[3]=(0xFF+((val[3]-val[2])/2))/(val[3]-val[2]);
} else {
output[3]=0x00;
}
printf(\nThe point-slope values are: %02X %02X %02X
%02X\n\n,output[0], output[1], output[2], output[3]);
do {
flag=1;
printf(run another set of numbers? );
while (!kbhit());
ans=getch();
if (ans!=’y’ ans!=’Y’ ans!=’n’ ans!=’N’) {
flag=0;
printf(\nhuh?\n);
}
} while (!flag);
} while (ans==’y’ || ans==’Y’);
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
168
printf(\nlater, hosehead!\n);
}
这个小程序可以帮你建立系统的输入功能函数输入功能函数建立起来了后就该建
立输出功能函数了把输出功能函数看成矩形并选择函数体的中点这样做可以简化反
模糊处理的数学过程
图0-9 输出功能函数
这些输出值被存储在一张表中
列表0-5
unsigned char code output_memf[OUTPUT_TOT][MF_TOT]={
// 输出成员函数
// 第一维是输出号,第二维是成员函数标号
{ 15, 67, 165, 220, 255, 0, 0, 0 } // braking force singletons:
// NONE, LIGHT, MEDIUM, HARD,
// VHARD
};
模糊控制函数通过遍历规则基数组进行估计分析条件时把当前规则中的u值保存在
变量’if_val’中条件检测结束后开始估计结果,模糊控制函数通过比较’if_val’和当前输出
的参考u值来得出结果如果当前保存在’if_val’中的数大于参考的输出值则就把’if_val’
中的值作为新的输出值一旦结果分析完毕开始一个新的规则查询时恢复’if_val’值
模糊控制的源代码见列表0-6 当前的正在进行分析的分支被保存在可位寻址区以便
对里面的位进行快速寻址
列表0-6
/*****************************************************************
Function: fuzzy_engine
Description: 实施规则基中的规则
Parameters: 无
Returns: 无.
Side Effects: 无
*****************************************************************/
unsigned char bdata clause_val; // 保存当前的分支进行
// 快速访问
sbit operator = clause_val^3; // 这位表示所使用的模糊操作
sbit clause_type = clause_val^7; // 表示分支是否是条件分支
// 或者是结果分支
void fuzzy_engine(void) {
bit then; // 当正在分析结果时
// 置位
unsigned char if_val, // 保存当前规则中条
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
169
// 件分支中的值
clause, // 规则基中当前的分支
mu, // 保存当前分支中的值
inp_num, // 当前条件使用的输入
label; // 被条件使用的成员函数
then=0; // 设第一个分支是条件分支
if_val=MU_MAX; // max out mu for the first rule
for (clause=0; clauseRULE_TOT; clause++) { // 遍历每条规则
clause_val=rules[clause]; // 读入当前的分支
if (!clause_type) { // 当前的分支是不是条件分支
if (then) { // 是否正在分析结果...
then=0;
if_val=MU_MAX; // 复位mu
}
inp_num=clause_val IO_NUM; // 得到当前输入号
label=(clause_val LABEL_NUM) / 16; // 得到功能函数
mu=compute_memval(inp_num, label); // 得到条件分支的值
if (operator) { // 如果是OR
// 操作...
if (mu if_val) { // 取最大值
if_val=mu;
}
} else { // 如果是AND操作
if (mu if_val) { // 取最小值
if_val=mu;
}
}
} else { // 当前分支是结果
then=1; // 置位标志位
// 如果当前规则的mu比参考的值要大,保存这个值作为新的模糊输出
if (outputs[clause_val IO_NUM]
[(clause_val LABEL_NUM) / 16] if_val) {
outputs[clause_val IO_NUM]
[(clause_val LABEL_NUM) / 16]=if_val;
}
}
}
defuzzify(); // 用COG方法计算模糊输出
// 和反模糊输出
}
通过调用’compute_memval’函数来估计每个给定输入的分支的u值把这段代码放在一
个函数中是为了当功能函数改变时可以很方便的修该其代码
列表0-7
/*****************************************************************
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
170
Function: compute_memval
Description: 计算条件分支的mu值,设功能函数是以点斜式方式存储的
Parameters: inp_num - unsigned char. 使用的输入号
label - unsigned char. 输入使用的功能函数
Returns: unsigned char. 计算的mu值
Side Effects: 无
*****************************************************************/
unsigned char compute_memval(unsigned char inp_num,
unsigned char label) {
int data temp;
if (input[inp_num] input_memf[inp_num][label][0]) {
// 如果输入不在曲线下
// u值为0
return 0;
} else {
if (input[inp_num] input_memf[inp_num][label][2]) {
temp=input[inp_num]; // 用点斜式计算mu
temp-=input_memf[inp_num][label][0];
if (!input_memf[inp_num][label][1]) {
temp=MU_MAX;
} else {
temp*=input_memf[inp_num][label][1];
}
if (temp 0x100) { // 如果结果不超过1
return temp; // 返回计算结果
} else {
return MU_MAX; // 确保mu值在范围内
}
} else { // 输入落在第二条斜线上
temp=input[inp_num]; // 用点斜式方法
// 计算mu
temp-=input_memf[inp_num][label][2];
temp*=input_memf[inp_num][label][3];
temp=MU_MAX-temp;
if (temp 0) { // 确保结果不小于0
return 0;
} else {
return temp; // mu为正– 返回结果
}
}
}
return 0;
}
当遍历完所有规则后相应的输出被保存在outputs数组中模糊控制函数调用
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
171
defuzzify功能把数组中的输出值转变成可被系统使用的COG输出值计算的方式采用我们
以前所讨论过的简化了的重心法这个过程占用了模糊控制中的大部分时间下面是该函
数的代码
列表0-8
/*****************************************************************
Function: defuzzify
Description: 计算模糊输出的重心并调用函数把它
转换成可被系统使用的输出量
Parameters: 无.
Returns: 无.
Side Effects: outputs[][] 数组被清零.
*****************************************************************/
void defuzzify(void) {
unsigned long numerator, denominator;
unsigned char i, j;
for (i=0; iOUTPUT_TOT; i++) { // 对所有的输出...
numerator=0; // 恢复总数值
denominator=0;
for (j=0; jMF_TOT; j++) { // 计算总和值
numerator+=(outputs
[j]*output_memf
[j]);
denominator+=outputs
[j];
outputs
[j]=0; // 清零输出作为参考使用
}
if (denominator) { // 确保分母是0的情况不发生
fuzzy_out
=numerator/denominator; // 确定COG
} else {
fuzzy_out
=DEFAULT_VALUE; // 没有规则被触发
}
}
normalize(); // 把模糊输出作为正常输出
}
7 方案调整
前面所描述的模糊控制系统使用的存储空间相对较少不进行优化地编译这些代码
发现只使用了3字节的内部RAM 80字节的外部RAM和1290字节的代码空间其中380字节用
来存放数组对于它较强的功能来说这些存储空间显得很小我试着运行了这个系统得
到了下面的结果
我们看到程序执行的时间是很长
的如果你的存储空间不大的话就不
得不忍受这种速度而节省空间如果你
认为这种速度太慢了那么就不得不牺
牲一些存储空间以获得速度上的提高
最快速的改善系统的方法就是改变
模糊功能函数的实现方式以前采用的点斜式存储方式这种方式可以节省存储空间但
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
172
处理起来的时间较长如果你愿意放弃一些存储空间可以把输入范围内的256个数据点存
储下来避免进行数值计算在汽车系统中将占用2304个字节的EPROM空间除此之外还
要限制输入和输出数组的表长度这样汽车模糊系统中就有一个输出数组两个输入数
组和五个功能函数做了这个改变后系统的处理速度将更快得到u值所需的时间也会缩
短以前所用的’compute_memval’函数被下面一行代码所替代
mu=input_memf[inp_num][label][input[inp_num]];
做了以上改动后就可以得到如下效果
系统的执行速度差不多提高了4倍7500
个指令周期的执行时间比33000个指令周期的
执行时间更容易被系统所接受新的系统使
用了1个字节内部RAM 8个字节外部RAM和3010
个字节的EPROM 其中2625个字节用来存放功
能函数表
还可以对系统进行改进因为在这个例子中不需要使用OR操作这部分代码可以去掉
一旦一个条件的u值为0 后面的条件和结果的处理就可以跳过去改进后的系统使用一个
字节内部RAM 8个字节外部RAM和3031个字节的EPROM 其中2625个字节用来保存表格系
统的执行效果如下表
列表0-9是新的汽车模糊处理的源代码
所有的代码都保存在一个文件中为它定义
一个头文件以便和系统的其他部分进行接
口
列表0-9
#define OUTPUT_TOT 1
#define MF_TOT 5
#define INPUT_TOT 2
#define MU_MAX 0xFF
#define IO_NUM 0x07
#define LABEL_NUM 0x70
#define DEFAULT_VALUE 0x80
unsigned char outputs[MF_TOT], // 模糊输出mu值
fuzzy_out; // 模糊控制值
unsigned char input[INPUT_TOT] ={ // 模糊输入
0, 0
};
unsigned char code input_memf[INPUT_TOT][MF_TOT][256]={
// 输入功能函数
{
{ // velocity: VSLOW
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xF6,
0xED, 0xE4, 0xDB, 0xD2, 0xC9, 0xC0, 0xB7, 0xAE, 0xA5, 0x9C, 0x93, 0x8A, 0x81, 0x78,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
173
0x6F, 0x66,
0x5D, 0x54, 0x4B, 0x42, 0x39, 0x30, 0x27, 0x1E, 0x15, 0x0C, 0x03, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // velocity: SLOW
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0D,
0x1A, 0x27, 0x34, 0x41, 0x4E, 0x5B, 0x68, 0x75, 0x82, 0x8F, 0x9C, 0xA9, 0xB6, 0xC3,
0xD0, 0xDD,
0xEA, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xF6, 0xED, 0xE4, 0xDB, 0xD2, 0xC9, 0xC0, 0xB7, 0xAE, 0xA5, 0x9C, 0x93, 0x8A,
0x81, 0x78,
0x6F, 0x66, 0x5D, 0x54, 0x4B, 0x42, 0x39, 0x30, 0x27, 0x1E, 0x15, 0x0C, 0x03, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
174
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // velocity: MEDIUM
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x0D, 0x1A, 0x27, 0x34, 0x41, 0x4E, 0x5B, 0x68, 0x75, 0x82, 0x8F, 0x9C, 0xA9,
0xB6, 0xC3,
0xD0, 0xDD, 0xEA, 0xF7, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF2, 0xE5, 0xD8, 0xCB, 0xBE, 0xB1, 0xA4,
0x97, 0x8A,
0x7D, 0x70, 0x63, 0x56, 0x49, 0x3C, 0x2F, 0x22, 0x15, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
175
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // velocity: FAST
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06,
0x0C, 0x12,
0x18, 0x1E, 0x24, 0x2A, 0x30, 0x36, 0x3C, 0x42, 0x48, 0x4E, 0x54, 0x5A, 0x60, 0x66,
0x6C, 0x72,
0x78, 0x7E, 0x84, 0x8A, 0x90, 0x96, 0x9C, 0xA2, 0xA8, 0xAE, 0xB4, 0xBA, 0xC0, 0xC6,
0xCC, 0xD2,
0xD8, 0xDE, 0xE4, 0xEA, 0xF0, 0xF6, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF6, 0xED, 0xE4, 0xDB, 0xD2,
0xC9, 0xC0,
0xB7, 0xAE, 0xA5, 0x9C, 0x93, 0x8A, 0x81, 0x78, 0x6F, 0x66, 0x5D, 0x54, 0x4B, 0x42,
0x39, 0x30,
0x27, 0x1E, 0x15, 0x0C, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // velocity: VFAST
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
176
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x1A, 0x27, 0x34, 0x41,
0x4E, 0x5B,
0x68, 0x75, 0x82, 0x8F, 0x9C, 0xA9, 0xB6, 0xC3, 0xD0, 0xDD, 0xEA, 0xF7, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF
}
},
{
{ // distance: VCLOSE
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF5, 0xEB,
0xE1, 0xD7,
0xCD, 0xC3, 0xB9, 0xAF, 0xA5, 0x9B, 0x91, 0x87, 0x7D, 0x73, 0x69, 0x5F, 0x55, 0x4B,
0x41, 0x37,
0x2D, 0x23, 0x19, 0x0F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
177
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // distance: CLOSE
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x48, 0x50,
0x58, 0x60,
0x68, 0x70, 0x78, 0x80, 0x88, 0x90, 0x98, 0xA0, 0xA8, 0xB0, 0xB8, 0xC0, 0xC8, 0xD0,
0xD8, 0xE0,
0xE8, 0xF0, 0xF8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xF5, 0xEB, 0xE1, 0xD7, 0xCD, 0xC3, 0xB9, 0xAF, 0xA5, 0x9B, 0x91, 0x87, 0x7D,
0x73, 0x69,
0x5F, 0x55, 0x4B, 0x41, 0x37, 0x2D, 0x23, 0x19, 0x0F, 0x05, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
178
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // distance: NEAR
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07,
0x0E, 0x15, 0x1C, 0x23, 0x2A, 0x31, 0x38, 0x3F, 0x46, 0x4D, 0x54, 0x5B, 0x62, 0x69,
0x70, 0x77,
0x7E, 0x85, 0x8C, 0x93, 0x9A, 0xA1, 0xA8, 0xAF, 0xB6, 0xBD, 0xC4, 0xCB, 0xD2, 0xD9,
0xE0, 0xE7,
0xEE, 0xF5, 0xFC, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF7, 0xEF, 0xE7, 0xDF, 0xD7, 0xCF,
0xC7, 0xBF,
0xB7, 0xAF, 0xA7, 0x9F, 0x97, 0x8F, 0x87, 0x7F, 0x77, 0x6F, 0x67, 0x5F, 0x57, 0x4F,
0x47, 0x3F,
0x37, 0x2F, 0x27, 0x1F, 0x17, 0x0F, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
179
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
},
{ // distance: FAR
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x14, 0x1E, 0x28, 0x32, 0x3C,
0x46, 0x50,
0x5A, 0x64, 0x6E, 0x78, 0x82, 0x8C, 0x96, 0xA0, 0xAA, 0xB4, 0xBE, 0xC8, 0xD2, 0xDC,
0xE6, 0xF0,
0xFA, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF
}
}
};
unsigned char code output_memf[MF_TOT]={
15, 67, 165, 220, 255 // braking force singletons:
// NONE, LIGHT, MEDIUM, HARD,
// VHARD
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
180
};
//***************************************************************
// 规则基定义如下每个分支可以是条件也可以是结果如果第7位为0
// 则为条件反之为结果6到4位确定规则对应的功能函数标号第3位表
// 明条件的操作规则为0说明是AND操作为1说明是OR操作2到0位确定
// 分支使用的输入或输出函数号
//****************************************************************
// define constants for the velocity input
#define VEL_VSLOW 0x00
#define VEL_SLOW 0x10
#define VEL_MEDIUM 0x20
#define VEL_FAST 0x30
#define VEL_VFAST 0x40
// define constants for the distance input
#define DIST_VCLOSE 0x01
#define DIST_CLOSE 0x11
#define DIST_NEAR 0x21
#define DIST_FAR 0x31
// define constants for the brake output
#define BRAKE_NONE 0x80
#define BRAKE_LIGHT 0x81
#define BRAKE_MEDIUM 0x82
#define BRAKE_HARD 0x83
#define BRAKE_VHARD 0x84
#define RULE_TOT 60
unsigned char code rules[RULE_TOT]={ // 模糊系统规则
// if... and... then...
VEL_VSLOW, DIST_VCLOSE, BRAKE_LIGHT,
VEL_VSLOW, DIST_CLOSE, BRAKE_NONE,
VEL_VSLOW, DIST_NEAR, BRAKE_NONE,
VEL_VSLOW, DIST_FAR, BRAKE_NONE,
VEL_SLOW, DIST_VCLOSE, BRAKE_MEDIUM,
VEL_SLOW, DIST_CLOSE, BRAKE_LIGHT,
VEL_SLOW, DIST_NEAR, BRAKE_NONE,
VEL_SLOW, DIST_FAR, BRAKE_NONE,
VEL_MEDIUM, DIST_VCLOSE, BRAKE_HARD,
VEL_MEDIUM, DIST_CLOSE, BRAKE_HARD,
VEL_MEDIUM, DIST_NEAR, BRAKE_MEDIUM,
VEL_MEDIUM, DIST_FAR, BRAKE_LIGHT,
VEL_FAST, DIST_VCLOSE, BRAKE_VHARD,
VEL_FAST, DIST_CLOSE, BRAKE_VHARD,
VEL_FAST, DIST_NEAR, BRAKE_HARD,
VEL_FAST, DIST_FAR, BRAKE_MEDIUM,
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
181
VEL_VFAST, DIST_VCLOSE, BRAKE_VHARD,
VEL_VFAST, DIST_CLOSE, BRAKE_VHARD,
VEL_VFAST, DIST_NEAR, BRAKE_HARD,
VEL_VFAST, DIST_FAR, BRAKE_MEDIUM
};
/*****************************************************************
Function: defuzzify
Description: 计算模糊输出的重心并把结果转化成系统控制量
Parameters: 无
Returns: 无.
Side Effects: outputs[] 数组被清零
*****************************************************************/
void defuzzify() {
unsigned long numerator, denominator;
unsigned char j;
numerator=0; // 和值清零
denominator=0;
for (j=0; jMF_TOT; j++) { // 累加结果
numerator+=(outputs[j]*output_memf[j]);
denominator+=outputs[j];
outputs[j]=0; // 结果使用完毕后被清零
}
if (denominator) { // 确认分母不为0
fuzzy_out=numerator/denominator; // 计算重心
} else {
fuzzy_out=DEFAULT_VALUE; // 没有规则被触发
// 输出为默认值
}
normalize(); // 将模糊输出转变为
// 控制量
}
/*****************************************************************
Function: fuzzy_engine
Description: 处理逻辑规则基
Parameters: 无.
Returns: 无.
Side Effects: 无.
*****************************************************************/
unsigned char bdata clause_val; // 对当前分支进行快速寻址
sbit operator = clause_val^3; // 定义寻址位
sbit clause_type = clause_val^7; // 该位表明分支是条件
// 还是结果
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
182
void fuzzy_engine() {
bit then; // 进行结果处理时置位
unsigned char if_val, // 保存当前规则中的u值
clause, // 当前规则基中的分支
mu, // 当前分支中的mu 值
inp_num, // 条件所使用的输入号
label; // 条件使用的成员函数
// 的标号
then=0; // 确认第一个分支是条件分支
if_val=MU_MAX; // 输出值初始化为最大值
for (clause=0; clauseRULE_TOT; clause++) { // 遍历所有规则
clause_val=rules[clause]; // 把当前分支读入
// bdata区
if (!clause_type) { // 如果当前分支是条件...
if (then)
then=0;
if_val=MU_MAX;
}
inp_num=clause_val IO_NUM; // 得到输入号
label=(clause_val LABEL_NUM) / 16; // 所使用的功能函数
mu=input_memf[inp_num][label][input[inp_num]];// 得到该条件的值
if (!mu) { // 如果条件没有被触发
do { // 跳过这个条件
clause++;
} while (clauseRULE_TOT !(rules[clause]&0x80));
// 跳过结果
while (clause+1RULE_TOT (rules[clause+1]0x80)) {
clause++;
}
if_val=MU_MAX; // 为下一个规则设定
} else {
if (mu if_val) { // 取最小值
if_val=mu;
}
}
} else { // 当前分支是结果分支
then=1; // 进行结果处理
// 标志位置1
// 如果当前规则的mu值比参考值大,就保存这个值
// 作为新的模糊输出值
if (outputs[clause_val 0x07] if_val) {
outputs[clause_val 0x07]=if_val;
}
}
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
183
}
defuzzify(); // 计算模糊输出值和系统的
// 控制值
}
8 结论
模糊控制是一种新的控制方法它并不能解决所有的问题但确实可以使一些问题解
决起来更加方便当你需要设计一个逻辑控制工程的时候可以借助一些工具来设计功能
函数和规则基有些工具还能在PC上对你的模糊控制系统进行仿真和测试在有了一定的
设计经验后这些工具使用起来是十分方便的记住不要去购买那些昂贵的进行模糊控
制应用设计的软件
广州周立功单片机发展有限公司http://www.zlgmcu.com Keil C51 使用技巧及实战
184
总结
这本书向你展示了用8051进行工程设计时的许多问题希望你读完本书后对8051的
认识能有较大的提高如果你现在还没有够买C编译器你应该马上去买一个采用C语言
可是你的系统设计更简单维护更方便
这本书覆盖面较大从C和汇编的代码优化到8051的网络设计再到模糊控制希望你从
本书中学到的知识对你今后的系统设计有所帮助
作者:
suncon
时间:
2003-4-8 06:00
标题:
Keil C51 使用技巧及实战
Keil software
C51系列微控制器的开发工具
uVision2入门教程
使用指南
从这里开始创建你的应用2 Keil Software声明
本文档所述信息不属于我公司的承诺范围其内容的变化也不会另行通知本文档所述软件的出售必须经过授权或签订特别协议本文档所述软件的使用必须遵循协议约定在协议约定以外的任何媒体上复制本软件将触犯法律购买者可以备份为目的而做一份拷贝在未经书面许可之前本手册的任何一部分都不允许为了购买者个人使用以外的目的而以任何形式和任何手段(电子的机械的)进行复制或传播
版权1997-2001 所有者Keil Elektronik GmbH 和Keil Software公司
Keil C51TM和uVisionTM是Keil Elektronik GmbH的商标
MicrosoftR和WindowsTM是Microsoft Corporation的商标或注册商标
PCR是International Business Machines Corporation的注册商标
注意
本手册假定你已经熟悉微软操作系统和8051系列产品的硬件和指令集
我们尽全力去做来保证这本手册的正确从而保证我们个人公司和在此提及的商标的形象
2
从这里开始创建你的应用3 前言
这本手册是Keil Software 公司关于8051系列MCU的开发工具的介绍它向新用户和有兴趣的读者介绍本公司的产品这本使用指南包含下列各章
第1 章简介概述并描述了Keil Software 为8051系列MCU提供的不同产品
第2 章安装描述了该如何安装软件以及如何设置工具的操作环境
第3 章开发工具描述了集成有调试器C编译器汇编器的uVision2 IDE的主要特性和用途
第4 章建立应用描述该如何建立项目编辑源文件编译并报告语法错误产生运行代码
第5 章测试程序描述了如何使用Vision2 debugger模拟并测试你的整个应用
第6 章调试功能讨论了扩展uVision2 debugger功能的各种函数
第7 章示例程序提供几个示例程序以说明该如何使用Keil 8051开发工具
第8 章实时操作系统讨论了RTX-51 Tiny版和RTX-51 Full版并提供一个示例程序
第9 章使用片上外围设备描述了如何使用C51编译器访问片上外围设备本章也包括几个应用注意事项
第10 章CPU和程序启动代码描述了如何为你的应用设置8051CPU
第11 章使用Monitor-51讨论该如何初始化Monitor并把它安装到你的目标板上
第12 章命令参考简单地介绍了Keil 8051开发工具的命令和控制
3
从这里开始创建你的应用4
本文档中使用如下约定:
举例描述
README.TXT 黑粗体用来表示执行文件数据文件源文件环境变量和你在命令提示行键入的命令
这些文字往往表示你必须按照字面的字符键入如CLS DIR BL51.EXE
Courier 这种形式的字体用来表示在屏幕或打印机上出现的信息.
Variables 斜体字表示必须由你提供的信息如在语法字符串中的projectfile表示你必须提供实际的项目名称少数情况下斜体字也用来表示强调
Elements that 省略号表示一个你可以替换的内容
Repeat
Omitted code 垂直的省略号用来在源程序列表中表示一段被忽略的程序如Void main (void) {
while (1);
[Optional Items] 方括号表示命令行或输入域中的可选项如
C51 TEST.C PRINT [(filename)]
{ opt1 | opt2 } 包括在大括号中的被’’|’’分开的文字表示一组选项必须从中选一
Keys 以sans serif字体出现的字符表示键盘上实际的键,如:
Press Enter to continue. 中的Enter表示键盘上的回车键.
Point 移动鼠标直到光标直到期望的条目上
Click 单击鼠标.
Drag 鼠标拖动操作.
Double-Click 双击鼠标.
4
从这里开始创建你的应用5
目录
第1 章简介.......................................................... ......... ......... ......... .........9
手册主题...................................................... ………………………….10
本文档的修改.................................................. ……………………….10
测试版和产品工具包............................................ …………………...11
用户类型...................................................... …………………………..11
请求援助...................................................... …………………………..12
软件开发流程.................................................. ………………………..13
产品一览...................................................... …………………………..16
第2 章安装.......................................................... ………………………..19
系统要求...................................................... …………………………..19
安装详细信息.................................................. ………………………..19
文件的组织结构................................................ ………………………20
第3 章开发工具
uVision2 集成开发环境.......................................……………………. 21
C51 优化C交叉编译器........................................... …………………32
A51 宏汇编器.................................................. ………………………..49
BL51 代码连接定位器........................................... ……………………51
LIB51 库管理器.................................................. ………………………54
OC51 分块目标文件转换器....................................... 55
OH51 目标文件到HEX格式的转换器................................ 55
第4 章建立应用
创建项目...................................................... 57
项目对象和文件组.............................................. 64
配置对话框................................................. 66
代码分块...................................................... 67
uVision2 功能.................................................. 69
编写优化代码.................................................. 78
技巧.......................................................... 82
第5 章测试程序
uVision2 调试器................................................ 93
调试命令....................................................... 107
表达式......................................................... 110
技巧........................................................... 126
第6 章uVision2的调试功能
创建函数....................................................... 131
5
从这里开始创建你的应用6 调用函数....................................................... 133
函数类型....................................................... 133
调试函数与C函数的差异.......................................... 147
dScope和uVision2调试器的差异.................................. 148
第7 章示例程序
HELLO 你的第一个8051C程序..................................... 150
MEASURE 一个远端测量系统.......................................155
第8 章单片机实时操作系统
介绍............................................................169
单片机实时操作系统技术数据.....................................173
实时操作系统线程浏览............................................174
TRAFFIC 小型实时操作系统示例...................................176
实时操作系统涉及的调试.........................................180
第9 章使用片上外围设备
特殊功能寄存器.................................................183
寄存器组.......................................................184
中断服务程序....................................................185
中断使能寄存器..................................................187
并行I/O口......................................................187
定时/记数器.....................................................189
串行接口.......................................................190
看门狗定时器...................................................193
数/模转换......................................................194
模/数转换......................................................195
低功耗模式......................................................196
第10 章CPU和程序启动代码..............................................197
第11 章使用Monitor-51 ................................................199
警告............................................................199
硬件和软件要求..................................................200
串口线..........................................................201
uVision2 Monitor 驱动..........................................201
使用Monitor-51时uVision2的限制................................202
使用Monitor-51时的工具配置......................................204
Monitor-51 配置.................................................206
冲突的解决.....................................................208
使用Monitor-51调试..............................................209
第12 章命令参考.......................................................211
uVision 2 命令行参数...........................................211
6
从这里开始创建你的应用7 A51/A251 宏汇编参数............................................212
C51/C251 编译器................................................213
L51/BL51 连接/重定位器..........................................215
L251 连接/重定位器.............................................216
LIB51/L251 库管理命令...........................................218
OC51 分块目标文件转换器........................................219
OH51 目标文件到HEX格式的转换器.................................219
OH251目标文件到HEX格式的转换器.................................219
索引....................................................................222
7
从这里开始创建你的应用8
8
从这里开始创建你的应用9
第1章简介
感谢您允许Keil Software为您提供8051系列单片机的软件开发工具利用本工具您可以开发所有8051系列单片机的嵌入式应用
注意
尽管我们在本手册中称它为8051开发工具其实它支持所有的由8051家族派生而来的类型
Keil Software 的8051开发工具提供以下程序你可以用它们来编译你的C源码汇编你的汇编源程序连接和重定位你的目标文件和库文件创建HEX文件调试你的目标程序从21页开始的第三章开发工具一章中将对每一个程序进行详细描述
Windows应用程序uVision2是一个集成开发环境它把项目管理源代码编辑程序调试等集成到一个功能强大的环境中
C51美国标准优化C交叉编译器从你的C源代码产生可重定位的目标文件
A51宏汇编器从你的8051汇编源代码产生可重定位的目标文件
BL51连接/重定位器组合你的由C51和A51产生的可重定位的目标文件生成绝对目标文件
LIB51库管理器组合你的目标文件生成可以被连接器使用的库文件
OH51目标文件到HEX格式的转换器从绝对目标文件创建Intel HEX 格式的文件
RTX-51实时操作系统简化了复杂和对时间要求敏感的软件项目
在16页的产品一览中将对由这些工具组成的开发套件进行描述它们是为专业开发人员而设计的但所有层次的编程人员都可以用它们来获得8051微控制器的绝大部分应用
9
从这里开始创建你的应用10
手册主题
本手册讨论的主题有
怎样为你的应用选择最好的工具包参照16页的产品一览
怎样在你的系统上安装本软件参照16页安装
本开发工具的特征页
怎样用uVision2 IDE创建一个完整的应用57页
怎样调试程序怎样用uVision2调试器模拟你的目标硬件93页
在C51编译器中该如何访问片上外围设备和8051派生系列产品的特殊功能114页
怎样运行示例程序149页
注意
为了立即开始请参照第二章安装软件然后参照第七章运行示例程序
本文档的最后改动
本软件和手册最后一刻的变化和修改在RELEASE.TXT中位于\KEIL\UV2和\KEIL\C51\HLP文件夹中
花点时间读一下这些文件看看这些变化和修改是否对安装产生影响
10
从这里开始创建你的应用11
测试版工具包和产品工具包
Keil Software把软件分成两种类型测试版和正式版
测试版包括8051工具的测试版本和本用户手册你可以用它们产生目标代码小于2K字节的应用
此套件主要是让你测试我们产品的效力并产生小的应用
正式版在16页讨论包括没有限制的8051工具和全套手册(含本手册)正式版套件包含1年的免费技术支持和产品升级升级通过www.keil.com提供
用户类型
本手册针对三种用户测试用户新用户有经验的用户
测试用户是那些还没有购买本软件但已经要求使用测试开发包以进一步了解本工具和本工具的性能的用户测试开发包包括有2K字节目标代码限制的工具和几个为8051MCU系列产品而创建的应用即使你是一个测试用户你最好也花点时间阅读本手册它解释了怎样安装本软件为你提供本开发工具的初步信息并介绍了示例程序
新用户是那些第一次购买本开发工具的用户你所购买的软件为你提供最新的开发工具技术手册和示例程序如果你对8051或本工具比较生疏花点时间学习本手册中描述的示例程序它们为新用户和没有经验的用户快速起步提供了一个指南和帮助
有经验的用户是指那些以前已经用过Keil 8051开发工具现在升级到最新版本的用户升级软件产品包含最新的开发工具和示例程序
11
从这里开始创建你的应用12
请求援助
Keil Software 的全体员工专注于为您提供最好的开发工具和文档资料如果你对本手册有建议的话请跟我们联系如果你认为你发现了一个软件上问题请在联系技术支持中心前按下面的步骤做
1阅读与你试图完成的工作或任务相关的章节
2确定你所用的是最新的版本到www.keil.com核对升级内容以确定你使用的是最新版本
3分析所发现的问题确定它是汇编器的问题还是编译器连接器库管理器或其他的开发工具的问题
4进一步通过减少你的代码到几行使问题更明确
如果你在经过上述步骤后问题仍然存在请你向我们技术支持中心报告请包含你的产品序列号和版本号我们倾向于你通过E-mail的方式发送如果你通过FAX联系请确定包含我们可以与你联系上的你的名字和电话号码(电话和传真)
请尽可能详细地描述你所遇到的问题你描述的越详细我们就能越快地找到解决办法如果你能用仅仅一页的代码描述你遇到的问题请把它E-mail给我们如果可能请确定你的问题能够在开发工具上重复出现请避免发送整个应用代码或很长的代码给我们以免延误我们对你的答复
注意
你总是可以从www.keil.com/support获得技术支持,产品升级,应用笔记和示例程序
12
从这里开始创建你的应用13
软件开发流程
当你使用Keil Software工具时你的项目开发流程和其它软件开发项目的流程极其相似
1创建一个项目从器件库中选择目标器件配置工具设置
2用C语言或汇编语言创建源程序
3用项目管理器生成你的应用
4修改源程序中的错误
5测试连接应用
一个完整的8051工具集的框图可以最好地表述此开发流程每一个组件在下面详细描述
uVision2 IDE
uVision2 集成开发环境集成了一个项目管理器一个功能丰富有错误提示的编辑器以及设置选项生成工具在线帮助利用uVision2创建你的源代码并把它们组织到一个能确定你的目标应用的项目中去uVision2自动编译汇编连接你的嵌入式应用并为你的开发提供一个单一的焦点
13
从这里开始创建你的应用14 C51编译器和A51汇编器
源代码由uVision2 IDE创建并被C51编译或A51汇编编译器和汇编器从源代码生成可重定位的目标文件
Keil C51编译器完全遵照ANSI C语言标准支持C语言的所有标准特性另外直接支持8051结构的几个特性被添加到里面
Keil A51宏汇编器支持8051及其派生系列的全部指令集
LIB51 库管理器
LIB51库管理器允许你从由编译器或汇编器生成的目标文件创建目标库库是一种被特别地组织过并在以后可以被连接重用的对象模块当连接器处理一个库时仅仅那些被使用的目标模块才被真正使用
BL51 连接器/定位器
BL51 连接器/定位器利用从库中提取的目标模块和由编译器或汇编器生成的目标模块创建一个绝对地址的目标模块一个绝对地址目标模块或文件包含不可重定位的代码和数据所有的代码和数据被安置在固定的存储器单元中此绝对地址目标文件可以用来
写入EPROM或其它存储器件
由uVision2调试器使用来模拟和调试
由仿真器用来测试程序
14
从这里开始创建你的应用15
uVision2 调试器
uVision2源代码级调试器是一个理想地快速可靠的程序调试器此调试器包含一个高速模拟器能够让你模拟整个8051系统包括片上外围器件和外部硬件当你从器件库中选择器件时这个器件的特性将自动配置
uVision2调试器为你在实际目标板上测试你的程序提供了几种方法
安装MON51目标监控器到你的目标系统并且通过Monitor-51接口下载你的程序
利用高级的GDIAGDI接口把uVision2调试器绑定到你的目标系统
Monitor-51
uVision2调试器支持用Monitor-51进行目标板调试此监控程序驻留在你的目标板的存储器里它利用串口和uVision2调试器进行通信利用Monitor-51uVision2调试器可以对你的目标硬件实行源代码级的调试
RTX51实时操作系统
RTX51实时操作系统是一个针对8051系列的多任务核RTX51实时内核从本质上简化了对实时事件反应速度要求高的复杂应用系统的设计编程和调试RTX51实时内核是完全集成到C51编译器中的从而方便使用任务描述表和操作系统的连接由BL51连接器/定位器自动控制
15
从这里开始创建你的应用16
产品一览
Keil Software提供第一流的8051系列开发工具我们把我们的开发工具捆绑到不同的开发包或工具套件17页的对照表说明了整个Keil Software 8051开发工具每一个套件及其内容描述如下
PK51 专业开发套件
PK51专业开发套件包括了所有专业开发人员创建和调试复杂8051嵌入式应用系统所要用到的一切工具PK51专业开发套件可以针对所有的8051及其派生系列进行配置使用
DK51开发套件
DK51开发套件是PK51专业开发套件的精简版本它不包括小型RTX51实时操作系统此套件可以针对所有的8051及其派生系列进行配置使用
CA51编译套件
CA51编译套件是那些需要C编译器而不需要调试系统的开发人员的最好选择CA51开发包仅仅包含uVision2 IDEuVision2调试器不包括在内此套件可以针对所有的8051及其派生系列进行配置使用
16
从这里开始创建你的应用17
A51汇编套件
A51汇编套件包括一个汇编器和你创建嵌入式应用所需要的所有功能此套件可以针对所有的8051及其派生系列进行配置使用
RTX51 实时操作系统FR51
RTX51实时操作系统是一个8051系列MCU的实时内核RTX51 FULL提供RTX51 TINY的所有功能和一些扩展功能并且包括CAN通信协议接口
开发套件和工具的对照表
利用此表选择你所需要的开发套件.
17
从这里开始创建你的应用18
18
从这里开始创建你的应用19
第2章安装
本章解释如何设置操作环境以及如何在你的硬盘上安装本软件在开始安装程序之前请
确认你的计算机系统符合最小的需求
制作一份安装盘的副本
系统需求
为了取得比较好的运行效果最低的硬件和软件配置必须满足
具有奔腾奔腾II或兼容的处理器的个人计算机
操作系统为WIN95WIN98WINNT4.0或更高
RAM大于16MB
20MB 的硬盘空余空间
安装详细说明
所有的Keil产品都带有一个安装程序安装方便8051开发工具的安装步骤如下
插入Keil开发工具光盘
从CD浏览界面选择安装软件
跟随提示进行安装操作
注意
当你插入CD时你的计算机可能会自动浏览CD如果没有运行\KEIL\SETUP\SETUP.EXE安装软件
19
从这里开始创建你的应用20
文件夹组织结构
安装程序复制开发工具到基本目录的各个子目录中默认的基本目录是C:\KEIL下表列出的文件夹结构是包括所有8051开发工具的全部安装信息你的安装信息由你购买的开发套件决定
文件夹描述
C:\KEIL\C51\ASM 汇编SFR定义文件和模板源程序文件
C:\KEIL\C51\BIN 8051工具的执行文件
C:\KEIL\C51\EXAMPLES 示例应用
C:\KEIL\C51\RTX51 完全实时操作系统文件
C:\KEIL\C51\RTX_TINY 小型实时操作系统文件
C:\KEIL\C51\INC C编译器包含文件
C:\KEIL\C51\LIB C编译器库文件启动代码和常规I/O资源
C:\KEIL\C51\MONITOR 目标监控文件和用户硬件的监控配置
C:\KEIL\UV2 普通uVision2文件
在本使用指南中我们假定用户采用默认的文件夹结构如果你安装你的软件到一个不同的文件夹你必须调整路径名以适应你的安装
20
从这里开始创建你的应用21
第3章开发工具
Keil 8051开发工具提供数个十分有用的特性可以帮助你快速地成功开发嵌入式应用这些工具使用简单并保证你达到你的设计目的
uVision2 集成开发环境
uVision2 IDE 是一个基于Window的开发平台包含一个高效的编辑器一个项目管理器和一个MAKE工具
uVision2支持所有的KEIL 8051工具包括C编译器宏汇编器连接/定位器目标代码到HEX的转换器uVision2通过以下特性加速你的嵌入式系统的开发过程
全功能的源代码编辑器
器件库用来配置开发工具设置
项目管理器用来创建和维护你的项目
集成的MAKE工具可以汇编编译和连接你的嵌入式应用
所有开发工具的设置都是对话框形式的
真正的源代码级的对CPU和外围器件的调试器
高级GDIAGDI接口用来在目标硬件上进行软件调试以及和Monitor-51进行通信
与开发工具手册和器件数据手册和用户指南有直接的链接
注意
uVision2调试器的特性只有PK51和DK51套件具备
21
从这里开始创建你的应用22
关于开发环境
uVision2 界面提供一个菜单一个工具条以便你快速选择命令按钮另外还有源代码的显示窗口对话框和信息显示uVision2允许同时打开浏览多个源文件
22
从这里开始创建你的应用23
菜单条工具条和快捷键
菜单条提供各种操作菜单如编辑操作项目维护开发工具选项设置调试程序窗口选择和处理在线帮助工具条按钮允许你快速地执行uVision2命令键盘快捷键你自己可以配置允许你执行uVision2命令下面的表格列出了uVision2菜单项命令工具条图标默认的快捷键以及他们的描述
文件菜单和命令File
菜单工具条快捷键描述
New Ctrl+N 创建新文件Open Ctrl+O 打开已经存在的文件Close 关闭当前文件
Save Ctrl+S 保存当前文件Save all 保存所有文件Save as 另外取名保存
Device Database 维护器件库
Print Setup 设置打印机
Print Ctrl+P 打印当前文件Print Preview 打印预览
1-9 打开最近用过的文件
Exit 退出uVision2提示是否保存文件
23
从这里开始创建你的应用24 编辑菜单和编辑器命令(Edit)
菜单工具条快捷键描述
Home 移动光标到本行的开始
End 移动光标到本行的末尾
Ctrl+Home 移动光标到文件的开始
Ctrl+End 移动光标到文件的结束
Ctrl+- 移动光标到词的左边
Ctrl+- 移动光标到词的右边
Ctrl+A 选择当前文件的所有文本内容
Undo Ctrl+Z 取消上次操作Redo Ctrl+Shift+Z 重复上次操作Cut Ctrl+X 剪切所选文本Ctrl+Y 剪切当前行的所有文本
Copy Ctrl+C 复制所选文本Paste Ctrl+V 粘贴Indent 将所选文本右移一个制表键的距离Selected Text
Unindent 将所选文本左移一个制表键的距离Selected Text
Toggle Bookmark Ctrl+F2 设置/取消当前行的标签Goto Next Bookmark F2 移动光标到下一个标签处Goto Previous Bookmark Shift+F2 移动光标到上一个标签处Clear All Bookmarks 清除当前文件的所有标签Find Ctrl+F 在当前文件中查找文本F3 向前重复查找
Shift+F3 向后重复查找
Ctrl+F3 查找光标处的单词
Ctrl+] 寻找匹配的大括号圆括号方括号
用此命令将光标放到大括号圆括号或方括号的前面
Replace Ctrl+H 替换特定的字符
Find in Files 在多个文件中查找
/****************************译者注-开始*************************************/
Ctrl+]命令在我的uVision2 2.20a中好象没有作用另外我的uVision2的Edit菜单中还有一个Goto Matching brace 命令在最后功能是选择匹配的一对大括号圆括号或方括号中的内容但是在操作之前你必须把光标置于其中一个括号的旁边前或后都可以但是要注意必须紧靠
/****************************译者注-结束*************************************/
24
从这里开始创建你的应用25 选择文本命令
在uVision2中你可以通过按住Shift键和相应的光标操作键来选择文本如Ctrl+- 是移动光标到下一个词那么Ctrl+Shift+- 就是选择当前光标位置到下一个词的开始位置间的文本
当然你也可以用鼠标来选择文本操作如下
要选择鼠标操作
任意数量的文本在你要选择的文本上拖动鼠标
一个词双击此词
一行文本移动鼠标到此行的最左边直到鼠标变成右指向的箭头然后单击
多行文本移动鼠标到此行的最左边直到鼠标变成右指向的箭头然后相应拖动
一个距形框中的文本按住Alt键然后相应拖动鼠标
25
从这里开始创建你的应用26 视图菜单View
菜单工具条快捷键描述
Status Bar 显示/隐藏状态条
File Toolbar 显示/隐藏文件菜单条
Build Toolbar 显示/隐藏编译菜单条
Debug Toolbar 显示/隐藏调试菜单条
Project Window 显示/隐藏项目窗口Output Window 显示/隐藏输出窗口Source Browser 打开资源浏览器Disassembly Window 显示/隐藏反汇编窗口Watch Call 显示/隐藏观察和堆栈窗口Stack Window
Memory Window 显示/隐藏存储器窗口Code Coverage Window 显示/隐藏代码报告窗口Performance
Analyzer Window 显示/隐藏性能分析窗口Symbol Window 显示/隐藏字符变量窗口
Serial Window #1 显示/隐藏串口1的观察窗口Serial Window #2 显示/隐藏串口2的观察窗口
Toolbox 显示/隐藏自定义工具条Periodic Window Update 程序运行时刷新调试窗口
Workbook Mode 显示/隐藏窗口框架模式
Options 设置颜色字体快捷键和编辑器的选项
26
从这里开始创建你的应用27 项目菜单和项目命令Project
菜单工具条快捷键描述
New Project 创建新项目
Import Vision1
Project 转化uVision1的项目
Open Project 打开一个已经存在的项目
Close Project 关闭当前的项目
Target Environment 定义工具包含文件和库的路径
Targets, Groups,Files 维护一个项目的对象文件组和文件
Select Device 选择对象的CPU
for Target
Remove 从项目中移走一个组或文件.
Options Alt+F7 设置对象组或文件的工具选项
File Extensions 选择不同文件类型的扩展名
Build Target F7 编译修改过的文件并生成应用Rebuild Target 重新编译所有的文件并生成应用Translate Ctrl+F7 编译当前文件Stop Build 停止生成应用的过程1-9 打开最近打开过的项目
27
从这里开始创建你的应用28 调试菜单和调试命令Debug
菜单工具条快捷键描述
Start/Stop Ctrl+F5 开始/停止调试模式Debugging
Go F5 运行程序直到遇到一个中断Step F11 单步执行程序遇到子程序则进入Step over F10 单步执行程序跳过子程序Step out of Ctrl+F11 执行到当前函数的结束Current function
Stop Running ESC 停止程序运行Breakpoints 打开断点对话框
Insert/Remove 设置/取消当前行的断点Breakpoint
Enable/Disable 使能/禁止当前行的断点Breakpoint
Disable All 禁止所有的断点Breakpoints
Kill All 取消所有的断点Breakpoints
Show Next 显示下一条指令Statement
Enable/Disable 使能/禁止程序运行轨迹的标识Trace Recording
View Trace 显示程序运行过的指令Records
Memory Map 打开存储器空间配置对话框
Performance 打开设置性能分析的窗口
Analyzer
Inline Assembly 对某一个行重新汇编可以修改汇编代码
Function Editor 编辑调试函数和调试配置文件
28
从这里开始创建你的应用29 外围器件菜单Peripherals
菜单工具条快捷键描述
Reset CPU 复位CPU
Interrupt, 打开片上外围器件的设置对话框I/O-Ports, 对话框的种类及内容依赖于你选择的CPU
Serial,
Timer,
A/D Converter,
D/A Converter,
I2C Controller,
CAN Controller,
Watchdog
工具菜单Tool
利用工具菜单你可以配置运行Gimpel PC-LintSiemens Easy-Case和用户程序通过Customize Tools Menu菜单你可以添加你想要添加的程序更详细的信息请参考72页的Using the Tools Menu
菜单工具条快捷键描述
Setup PC-Lint 配置Gimpel Software的PC-Lint程序
Lint 用PC-Lint处理当前编辑的文件
Lint all C Source Files 用PC-Lint处理你项目中所有的C源代码文件
Setup Easy-Case 配置Siemens的Easy-Case程序
Start/Stop Easy-Case 运行/停止Siemens的Easy-Case程序
Show File (Line) 用Easy-Case处理当前编辑的文件
Customize Tools Menu 添加用户程序到工具菜单中
29
从这里开始创建你的应用30 软件版本控制系统菜单SVCS
用此菜单来配置和添加软件版本控制系统的命令更详细的信息参见76页的Using the SVCS Menu
菜单工具条快捷键描述
Configure 配置软件版本控制系统的命令
Version Control
视窗菜单Window
菜单工具条快捷键描述
Cascade 以互相重叠的形式排列文件窗口
Tile Horizontally 以不互相重叠的形式水平排列文件窗口
Tile Vertically 以不互相重叠的形式垂直排列文件窗口
Arrange Icons 排列主框架底部的图标
Split 把当前的文件窗口分割为几个
1-9 激活指定的窗口对象
30
从这里开始创建你的应用31 帮助菜单Help
菜单工具条快捷键描述
Help topics 打开在线帮助
About Vision 显示版本信息和许可证信息
uVision2 有两种操作模式
创建模式让你编译应用中所有的文件以产生执行程序此模式的特性在57页Creating Applications中描述
调试模式提供一个非常强劲的调试器你可以用它来调试你的程序此模式的特性在93页Testing Programs中描述
在两种模式下你都可以用源文件编辑器来编辑你的源代码
31
从这里开始创建你的应用32
C51优化的C语言交叉编译器
Keil C51交叉编译器是一个基于ANSI C标准的针对8051系列MCU的C编译器生成的可执行代码快速紧凑在运行效率和速度上可以和汇编程序得到的代码相媲美
和汇编语言相比用C语言这样的高级语言有很多优势比如
对处理器的指令集不必了解8051 CPU的基本结构可以了解但不是必须的
寄存器的分配以及各种变量和数据的寻址都由编译器完成
程序拥有了正式的结构由C语言带来的并且能被分成多个单独的子函数这使整个应用系统的结构变得清晰同时让源代码变得可重复使用
选择特定的操作符来操作变量的能力提高了源代码的可读性
可以运用和人的思维很接近的词汇和算法表达式
编写程序和调试程序的时间得到很大程度的缩短
C运行连接库包含一些标准的子程序如格式化输出数字转换浮点运算
由于程序的模块结构技术使得现有的程序段可以很容易的包含到新的程序中去
ANSI 标准的C语言是一种丰常方便的获得广泛应用的在绝大部分系统中都能够很容易得到的语言
因此如果需要现有的程序可以很快地移植到其他的处理器上节省投资
32
从这里开始创建你的应用33
C51 语言的扩展
虽然C51是一个兼容ANSI的编译器但为了支持8051系列MCU还是加入了一些扩展的内容C51编译器的扩展内容包括
数据类型
存储器类型
指针
重入函数
中断服务程序
实时操作系统
和PL/M及A51源程序的接口
以下各节简单地描述了上述的扩展特性
数据类型
本C51编译器支持下表列出的各种规格的数据类型.除了这些数据类型以外变量可以组合成结构联合及数组除非特别说明这些变量都可以用指针存取
注* bit, sbit sfr,和sfr16为8051硬件和C51及C251编译器所特有它们不是ANSI C 的一部分也不能用指针对它们进行存取
33
从这里开始创建你的应用34 这些sbit sfr和sfr16类型的数据使你能够操作8051MCU所提供的特殊功能寄存器例如下面的表达式
sfr P0 = 0x80; /* Define 8051 P0 SFR */
声明了一个变量P0并且把它和位于0x808051的端口0处的特殊功能寄存器联系在一起
当结果的数据类型和源数据类型不同时C51编译器在数据类型间自动进行转换例如一个bit变量赋值给一个interger变量时将会被转换为integer当然你可以用类型表示进行强制转换数据转换时要注意有符号变量的转换其符号是自动扩展的
存储器类型
本C51编译器支持8051及其派生类型的结构能够访问8051的所有存储器空间具有下表列出的存储器类型的变量都可以被分配到某个特定的存储器空间
存储器类型描述
code 程序空间64 Kbytes通过MOVC @A+DPTR 访问
data 直接访问的内部数据存储器访问速度最快128 bytes
idata 间接访问的内部数据存储器可以访问所有的内部存储器空间256 bytes
bdata 可位寻址的内部数据存储器可以字节方式也可以位方式访问16 bytes
xdata 外部数据存储器64 Kbytes通过MOVX @DPTR访问
pdata 分页的外部数据存储器256 bytes通过MOVX @Rn 访问
访问内部数据存储器将比访问外部数据存储器快的多由于这个原因你应该把频繁使用的变量放置在内部数据存储器中把很少使用的变量放在外部数据存储器中这通过使用SMALL模式将很容易就做到通过定义变量时包括存储器类型你可以定义此变量存储在你想要的存储器中
34
从这里开始创建你的应用35 在变量的声明中你可以包括存储器类型和signed或unsigned属性
char data var1;
char code text[] = ENTER PARAMETER;
unsigned long xdata array[100];
float idata x,y,z;
unsigned int pdata dimension;
unsigned char xdata vector[10][4][4];
char bdata flags;
如果在变量的定义中没有包括存储器类型将自动选用默认或暗示的存储器类型暗示的存储器类型适用于所有的全局变量和静态变量还有不能分配在寄存器中的函数参数和局部变量默认的存储器类型由编译器的参数SMALLCOMPACT及LARGE决定这些参数定义了编译时使用的存储模式
存储模式
存储模式决定了默认的存储器类型此存储器类型将应用于函数参数局部变量和定义时未包含存储器类型的变量你可以在命令行用SMALLCOMPACT和LARGE参数定义存储模式定义变量时使用存储器类型显式定义将屏蔽默认存储器类型
小(SMALL)模式
所有变量都默认在8051的内部数据存储器中这和用data显式定义变量起到相同的作用在此模式下变量访问是非常快速的然而所有数据对象包括堆栈都必须放在内部RAM中堆栈空间面临溢出因为堆栈所占用多少空间依赖于各个子程序的调用嵌套深度在典型应用中如果具有代码分段功能的BL51连接/定位器被配置成覆盖内部数据存储器中的变量时此SMALL模式是最好的选择
35
从这里开始创建你的应用36 紧凑COMPACT模式
此模式中所有变量都默认在8051的外部数据存储器的一页中地址的高字节往往通过Port 2输出其值必须由你在启动代码中设置编译器不会为你设置这和用pdata显式定义变量起到相同的作用此模式最多只能提供256字节的变量这种限制来自于间接寻址所使用的R0,R1MOVX @R0/R1这种模式不如SMALL模式高效所以变量的访问不够快不过它比LARGE模式要快
大LARGE模式
在大模式下所有的变量都默认在外部存储器中xdata这和用xdata显式定义变量起到相同的作用数据指针DPTR用来寻址通过DPTR进行存储器的访问的效率很低特别是在对一个大于一个字节的变量进行操作时尤为明显此数据访问类型比SMALL和COMPACT模式需要更多的代码
注意
你或许应该一直使用小模式它产生最快最紧凑效率最高的代码
你最好显式定义你的变量的存储器类型只有当你的应用不能在SMALL模式下操作时你才需要往上增加你的存储模式
36
从这里开始创建你的应用37
指针
C51编译器支持用星号*进行指针声明你可以用指针完成在标准C语言中有的所有操作
另外由于8051及其派生系列所具有的独特结构C51编译器支持两种不同类型的指针存储器指针和通用指针
通用指针
通用或未定型的指针的声明和标准C语言中一样如
char *s; /* string ptr */
int *numptr; /* int ptr */
long *state; /* long ptr */
通用指针总是需要三个字节来存储第一个字节是用来表示存储器类型第二个字节是指针的高字节第三字节是指针的低字节
通用指针可以用来访问所有类型的变量而不管变量存储在哪个存储空间中因而许多库函数都使用通用指针通过使用通用指针一个函数可以访问数据而不用考虑它存储在什么存储器中
通用指针很方便但是也很慢在所指向目标的存储空间不明确的情况下它们用的最多
37
从这里开始创建你的应用38
存储器指针
存储器指针或类型确定的指针在定义时包括一个存储器类型说明并且总是指向此说明的特定存储器空间例如
char data *str; /* ptr to string in data */
int xdata *numtab; /* ptr to int(s) in xdata */
long code *powtab; /* ptr to long(s) in code */
正是由于存储器类型在编译时已经确定通用指针中用来表示存储器类型的字节就不再需要了
指向idatadatabdata和pdata的存储器指针用一个字节保存指向code和xdata的存储器指针用两个字节保存使用存储器指针比通用指针效率要高速度要快当然存储器指针的使用不是很方便在所指向目标的存储空间明确并不会变化的情况下它们用的最多
存储器指针和通用指针的比较
使用存储器指针可以显著的提高8051 C程序的运行速度
下面的示例程序说明了使用不同的指针在代码长度占用数据空间和运行时间上的不同
Description Idata Pointer Xdata Pointer Generic Pointer
C源程序idata *ip; char xdata *xp; char *p;
char val; har val; char val;
val = *ip; val = *xp; val = *xp;
编译后的
代码MOV R0,ip MOV DPL,xp +1 MOV R1,p + 2
MOV val,@R0 MOV DPH,xp MOV R2,p + 1
MOV A,@DPTR MOV R3,p
MOV val,A CALL CLDPTR
指针大小1 byte 2 byte 3 byte
代码长度4 bytes 9 bytes 11 bytes + library call
执行时间4 cycles 7 cycles 13 cycles
38
从这里开始创建你的应用39
重入函数
多个进程可以同时使用一个重入函数当一个重入函数被调用运行时另外的一个进程可能中断此运行过程然后再次调用此重入函数通常情况下C51函数不能被递归调用也不能应用导致递归调用的结构有此限制是由于函数参数和局部变量是存储在固定的地址单元中重入函数特性允许你声明一个重入函数即可以被递归调用的函数如
int calc (char i, int b) reentrant
{
int x;
x = table
;
return (x * b);
}
重入函数可以被递归调用也可以同时被两个或更多的进程调用重入函数在实时应用中及中断服务程序代码和非中断程序代码必须共用一个函数的场合中经常用到
对每一个重入函数来说根据存储模式重入堆栈被安置在内部或外部单元中
注意
在一个基本函数的基础上添加reentrant说明从而使它具有重入特性
需要注意的是,你可以选择哪些必须的函数为重入函数而不需将全部程序声明为重入函数
把全部程序声明为重入函数将增加目标代码的长度并减慢运行速度
39
从这里开始创建你的应用40
中断服务程序
C51编译器允许你用C语言创建中断服务程序你仅仅需要关心中断号和寄存器组的选择编译器自动产生中断向量和程序的入栈及出栈代码在函数声明时包括interrupt将把所声明的函数定义为一个中断服务程序另外你可以用using定义此中断服务程序所使用的寄存器组
unsigned int interruptcnt;
unsigned char second;
void timer0 (void) interrupt 1 using 2
{
if (++interruptcnt == 4000)
{ /* count to 4000 */
second++; /* second counter */
interruptcnt = 0; /* clear int counter */
}
}
参数传递
C51编译器能在CPU寄存器中传递最多三个参数由于不用从存储器中读出和写入参数从而显著提高了系统性能参数传递由REGPARMS和NOREGPARMS编译参数所控制下表列出了不同的参数和数据类型所占用的寄存器
参数char, int, long, generic
数目1-byte pointer 2-byte pointer float pointer
1 R7 R6 R7 R4 R7 R1 R3
2 R5 R4 R5
3 R3 R2 R3
如果没有CPU寄存器供参数传递所用或太多的参数需要传递时地址固定的存储器将用来存储这些额外的参数
40
从这里开始创建你的应用41
函数返回值
函数返回值总是通过CPU寄存器进行下表列出了返回各种数据时所用的CPU寄存器
返回数据类型寄存器描述
bit Carry Flag
char,unsigned char,1-byte pointer R7
int, unsigned int, 2-byte pointer R6 R7 MSB in R6, LSB in R7
long, unsigned long R4 R7 MSB in R4, LSB in R7
float R4 R7 32-Bit IEEE format
generic pointer R1 R3 Memory type in R3, MSB R2, LSB R1
寄存器优化
根据程序前后的联系C51编译器分配最多7个寄存器来存储寄存器变量C51编译器能分析每个程序模块中对寄存器的修改连接程序产生一个全局的项目级的寄存器文件此文件包含被外部程序改变的所有寄存器的信息因而C51编译器知道整个应用中每个函数所使用的寄存器并能为每个C函数优化分配CPU寄存器
对实时操作系统的支持
C51编译器很好地集成了RTX-51 Full和RTX-51 Tiny 多任务实时操作系统任务描述表在连接过程中控制和产生关于RTX实时操作系统的详细信息请参考169页开始的RTX-51 Real-Time Operating System一章
41
从这里开始创建你的应用42
和汇编语言的接口
你可以很容易在C程序中调用汇编程序反之依然函数参数通过CPU寄存器传递或使用NOREGPARMS参数指示编译器通过固定的存储器传递从函数返回的值总是通过CPU寄存器传递除了直接产生目标代码外你还可以用SRC编译参数指示编译器产生汇编源代码文件供A51 汇编器使用例如下面的C语言源代码
unsigned int asmfunc1 (unsigned int arg)
{
return (1 + arg);
}
用SRC指示C51编译器编译时产生以下汇编文件
?PR?_asmfunc1?ASM1 SEGMENT CODE
PUBLIC asmfunc1
RSEG ?PR?_asmfunc1?ASM1
USING 0
asmfunc1:
;---- Variable ’’arg?00’’ assigned to Register ’’R6/R7’’ ----
MOV A,R7 ; load LSB of the int
ADD A,#01H ; add 1
MOV R7,A ; put it back into R7
CLR A
ADDC A,R6 ; add carry R6
MOV R6,A
?C0001:
RET ; return result in R6/R7
你可以用#pragma asm 和#pragma endasm 预处理指示器来在你的C语言程序中插入汇编指令
42
从这里开始创建你的应用43
和PL/M-51的接口
Intel的PL/M-51是一种流行的编程语言在很多方面和C语言类似你很容易就可以将C程序和PL/M-51程序联接起来
在你用alien声明PL/M-51函数后你就可以从C语言中调用它们所有在PL/M-51模块中定义的全局变量都可以在C语言程序中使用例如
extern alien char plm_func (int, char);
PL/M-51编译器和Keil Software工具都产生OMF51格式的目标文件连接程序使用OMF51文件来处理外部字符变量而不管它们在什么地方声明和使用
代码优化
C51是一个杰出的优化编译器它通过很多步骤以确保产生的代码是最有效率的最小和/或最快编译器通过分析初步的代码产生最终的最有效率的代码序列以此来保证你的C语言程序占用最少空间的同时运行的快而有效
C51编译器提供9个优化级别每个高一级的优化级别都包括比它低的所有优化级别的优化内容以下列出的是目前C51编译器提供的所有优化级别的内容
常量折叠在表达式及寻址过程中出现的常量被综合为一个单个的常量
跳转优化采用反转跳转或直接指向最终目的的跳转从而提升了程序的效率
哑码消除永远不可能执行到的代码将自动从程序中剔除
寄存器变量只要可能局部变量和函数参数被放在CPU寄存器中不需要为这些变量再分配存储器空间
43
从这里开始创建你的应用44 通过寄存器传递参数最多三个参数通过寄存器传递
消除全局公用的子表达式只要可能程序中多次出现的相同的子表达式或地址计算表达式将只计算一次
合并相同代码利用跳转指令相同的代码块被合并
重复使用入口代码需要多次使用的共同代码被移到子程序的前面以缩减代码长度
公共块子程序需要重复使用的多条指令被提取组成子程序指令被重新安排以最大化一个共用子程序的长度
/********************译者注-开始****************************/
对于操作硬件有时序要求的应用,慎用第9级优化.
因为它将可能调整你的指令顺序.
/********************译者注-结束****************************/
对8051的特殊优化
窥孔优化当能够缩小代码空间或执行时间时复杂的操作被简单的操作代替
访问优化常量和变量被计算后直接包含在操作中
扩展访问优化用DPTR做存储器指针来增加代码的密度
数据覆盖一个函数的数据和位变量空间是可覆盖的BL51连接器将采用覆盖技术来分配变量空间
Case/Switch优化根据使用的数字序列和位置用跳转表或一连串的跳转指令来优化switch及case结构
代码生成选项
空间优化公共C操作被子程序代替以程序执行速度的降低来换取程序代码空间的缩减
时间优化公共C操作被嵌入到程序中以程序代码空间的增加来换取程序执行速度的提高
不用绝对寄存器不用绝对寄存器地址访问程序代码依赖于寄存器的分段
不用寄存器传递参数用局部数据段来传递参数而不用寄存器这是为了兼容早期版本的C51编译器PL/M-51编译器和ASM-51汇编器
44
从这里开始创建你的应用45
调试
C51编译器使用Intel目标格式OMF51来产生目标文件和全部的字符变量信息而且编译器还包含所有必要的信息如变量名函数名行数以及uVision2调试器或任何兼容Intel格式的仿真器用来逐条彻底地调试和分析程序所需要的信息
另外使用OBJECTEXTEND参数可以指示编译器产生附加的变量类型信息到目标文件中这样利用相应的仿真器就可以显示变量和结构的数据信息你可以向你的仿真器供应商询问是否支持Intel OMF51格式和Keil软件生成的目标模块
库函数
C51编译器包含有ANSI标准的7个不同的的编译库从而满足不同功能的需要
库文件描述
C51S.LIB 小模式库不支持浮点运算
C51FPS.LIB 小模式库支持浮点运算
C51C.LIB ` 紧凑模式库不支持浮点运算
C51FPC.LIB 紧凑模式库支持浮点运算
C51L.LIB 大模式库不支持浮点运算
C51FPL.LIB 大模式库支持浮点运算
80C751.LIB Philips 8xC751及其派生系列使用的库
和硬件相联系的输入/输出操作的库函数模块的源代码文件位于\KEIL\C51\LIB文件夹中你可以利用这些文件来修改你的库以适应你目标板上的任何器件的输入/输出操作
45
从这里开始创建你的应用46
内连的库函数
本编译器的库中包含一定数量的函数是内连函数内连函数不产生ACALL或LCALL指令来执行库函数以下的内连函数产生内连的代码因而它比一个调用函数要快而有效
内连函数描述
_crol_ 字节左移
_cror_ 字节右移
_irol_ 整数左移
_iror_ 整数右移
_lrol_ 长整数左移
_lror_ 长整数右移
_nop_ 空操作
_testbit_ 判断并清除8051 JBC 指令
编译器的调用
通常情况下当你创建你的项目时C51编译器由uVision2 IDE调用当然你也可以在DOS方式在命令行键入C51来运行你的C源程序文件名必须和编译控制参数一起在命令行输入如
C51 MODULE.C COMPACT PRINT (E:M.LST) DEBUG SYMBOLS
C51 COMPILER V6.00
C51 COMPILATION COMPLETE. 0 WARNING(S), 0 ERROR(S)
编译器控制参数可以在命令行输入也可以在文件中的开头用#pragma 定义要想知道全部的编译器控制参数请参考213页的C51/C251 Compiler
46
从这里开始创建你的应用47
示例程序
下面的程序显示了C51编译器的一些功能特性本C51编译器根据编译控制参数产生相应的OMF51格式的目标文件
编译器报告所有必要的信息如变量名函数名行数以及uVision2调试器或其它仿真器用来详细调试和分析程序所需要的信息
编译后C51编译器产生一个列表文件文件中包含源代码指示信息汇编清单和字符表
下页中展示了一个C51编译器生成的示例列表文件
47
从这里开始创建你的应用48
C51编译器产生行号编译时的时间和日期
编译器的运行和产生的目标文件的信息被记录在案
列表文件在每个源代码前面包含行号和{}的嵌套层数
如果错误或可能错误的代码存在一个错误或告警信息将显示出来
选择在Vision2-Options for Target – Listing 中的Assembly Code 代码指示选项将在列表文件的汇编代码处加入原代码所在的行号
存储器一览表提供了8051存储器占用信息
程序中的错误和告警总数包括在文件的结尾处.
48
从这里开始创建你的应用49
A51宏汇编器
本A51是一个8051MCU系列的宏汇编器它把汇编语言翻译成机器代码本A51汇编器允许你定义你程序中的每一个指令在需要极快的运行速度很小的代码空间精确的硬件控制时使用本汇编器的宏特性让公共代码只需要开发一次从而节约了开发和维护的时间
源码级调试
本A51汇编器在生成的目标文件中包含全部的行号字符及其类型的信息这让你的调试器能够精确显示程序变量行号是为了uVision2调试器或第三方仿真器源代码级调试你汇编过的程序时使用的
功能一览
本A51汇编器翻译汇编源程序为可重定位的目标代码它产生一个列表文件其中可以包含或不包含字符表及交叉参考信息
本A51汇编器支持两种宏处理
标准的宏处理
这是一个比较容易使用的宏处理它允许你在你的8051汇编代码中定义和使用宏它标准的宏语法和其它许多汇编器中使用的相同
宏处理语言(MPL)
是一个和Intel ASM51宏处理兼容的字符串替换工具MPL有几个预先定义好的宏处理功能来执行一些有用的操作如字符串处理或数字处理
本A51汇编器宏处理的另一个有用的特性是根据命令行参数或汇编符号进行条件汇编代码段的条件汇编能帮助你实现最紧凑的代码
它也让你可以从一个汇编源代码文件产生不同的应用
49
从这里开始创建你的应用50
列表文件
下面是汇编器产生的列表文件的例子
A51汇编器产生一个列表文件包括行号汇编时的时间和日期关于汇编器运行和目标文件产生的信息被记录下来
通常情况下程序从EXTERNPUBLIC and SEGMENT 指示器开始列表文件包含了每个源代码的行号及每行产生的代码
列表文件包含了错误和告警信息错误和告警的位置被明显的标识出来
选择在Vision2-Options for Target – Listing 中的Cross Reference 选项将在列表文件列出源程序中所用到的所有字符
存储器组的占用信息和程序中的错误和告警总数包括在文件的结尾处.
50
从这里开始创建你的应用51
BL51具有代码分段功能的连接/重定位器
本BL51是具有代码分段功能的连接/重定位器它组合一个或多个目标模块成一个8051的执行程序此连接器处理外部和全局数据并将可重定位的段分配到固定的地址上
本BL51连接器处理由Keil C51编译器A51汇编器和Intel PL/M-51编译器ASM-51汇编器产生的目标模块连接器自动选择适当的运行库并连接那些用到的模块
你也可以在命令行上输入相应的目标模块的名字的组合来运行本连接器BL51连接器默认的控制参数是经过仔细选择的因而不需要定义附加的控制参数就可以适应大多数应用当然你可以很容易为你的应用作特定的设置
数据地址管理
本连接器通过覆盖那些不会同时使用的函数变量的技术来管理8051有限的内部存储器资源这极大地降低了大多数应用对存储器的需求本BL51连接器分析函数间的引用以决定存储的覆盖策略你可以用OVERLAY指示器来人为控制函数间的引用这些引用被连接器用来确定哪些存储器单元是独占的NOOVERLAY指示器让BL51不进行覆盖连接这在使用间接调用的函数时或为了调试而禁止覆盖时比较有用.
代码分段
本BL51连接器支持创建程序空间大于64KB的应用既然8051不能直接操作大于64KB的代码地址空间就必须由外部硬件来交换代码段完成此功能的硬件必须要在8051中运行的程序的控制中这就是大家所知的段(块)切换
51
从这里开始创建你的应用52 本BL51连接器让你管理一个公共的区域和32个最大64KB空间的块从而达到总共2MB的分段程序空间支持外部硬件块切换的软件包括的一个汇编程序可以由你来编辑以适应你应用中的特定硬件平台
本BL51连接器让你定义哪个段装载哪个特定的程序模块通过仔细考虑把各个函数分配到不同的段中来创建一个非常大而有效的应用
公共段
段切换程序中的公共段是一块在任何时候在所有的段中都可以访问的存储器此公共段在物理上就不能切换出局或变换地址空间
在公共段中的代码可以复制到每个段中如果切换整个程序空间或驻留在一个独立的地址空间或器件中公共段不用切换
公共段包含那些必须在所有时候都要用到的程序段和常量它还可以包括经常使用的代码默认情况下以下的代码内容将自动分配在公共段中
复位和中断向量
代码常量
C51 中断服务进程
分段开关跳转表
一些C51实时运行库函数
执行其它段中的程序
分段代码空间是通过附加的由软件控制的地址线控制的这些地址线可以是由8051的I/O口或位于存储器空间的锁存器来模拟本BL51连接器为位于其他段中的函数生成一个跳转表当你用C语言调用一个位于不同的段中的函数时它要先切换段再跳到目标程序运行完成后再回到调用的那个段中去并继续往下执行这种段切换处理需要附加的50个CPU指令周期和占用2Bytes堆栈空间
如果你把相关的函数分配在相同的段中将显著的提高系统的性能那些需要从多个段中经常调用的函数应该位于公共段中
52
从这里开始创建你的应用53
映象文件
下面是BL51产生的一个例子文件:
BL51产生一个包含连接时的时间和日期的映象文件(*.M51)
BL51 显示调用的命令和存储模式
应用中包含的每个模块和库模块被列出来
存储器映象文件包含8051实际存储器的使用信息
覆盖映象图显示了程序结构和每个函数的数据和位段
错误和告警总数包括在文件的结尾处这些映象图指出在连接定位时可能面临的问题
53
从这里开始创建你的应用54
LIB51库管理器
本库管理器让你建立和维护库文件一个库文件是格式化的目标模块由编译器或汇编器产生的集合库文件提供了一个方便的方法来组合和使用大量的连接程序可能用到的目标模块利用uVision2 项目管理器的Options for Target– Output – Create Library选项可以建造一个库你也可以从命令行运行LIB51程序命令行参数参照218页LIB51 / L251 Library Manager Commands
使用库有一系列优点安全高速和减少磁盘空间仅仅是使用库的一小部分原因另外库提供了一个好的分发大量函数而不用分发大量函数源代码的手段例如ANSI C的库是作为一套库文件提供的
uVision2 项目C:\KEIL\C51\RTX_TINY\RTX_TINY.UV2允许你修改和创建RTX51小型实时操作系统库你很容易创建你自己的库用来包括象串行I/OCAN和闪存操作这样一些你自己一次又一次要用到的流程一旦这些流程调试无误后你就可以把它们转换成库由于库只包含目标模块不用在每个项目中重新编译这些模块所以生成应用的时间将缩短
连接定位程序连接最终应用是用到的库文件库中的模块仅仅在需要的时候才被提取加到程序中没有被你的应用调用的库函数不会出现在你的最终结果中连接器把从库中提取出来的模块和其他目标模块做同样的处理
54
从这里开始创建你的应用55
OC51 分段目标文件转换器
此OC51转换器为一个分段目标模块中的每一个代码段创建绝对的目标模块分段目标模块是你生成一个分段代码切换应用时由BL51创建的字符变量的调试信息被拷贝到转换后的绝对目标模块中以便给uVision2调试器和其他仿真器使用
你可以从命令行用OC51去为你分段目标模块中的每一个代码段创建绝对目标模块
然后你还可以用OH51目标代码到HEX文件的转换器为每一个绝对目标模块产生相应的Intel HEX格式的文件
OH51 目标代码到HEX文件的转换器
此转换器为绝对目标模块创建Intel HEX格式的文件绝对目标模块可以由BL51或OC51产生Intel HEX文件是ASCII文件它用十六进制的数表示你的应用系统的目标模块它们可以很容易的下载到编程器以便写入EPROMS器件
55
从这里开始创建你的应用56
56
从这里开始创建你的应用57
第4章创建应用
我们提供了一个测试版本测试版本中包含示例程序和我们工具的受限使用版本从而使你很容易的评估熟悉我们的系列产品测试版本中的示例程序同样包含在正式版本中
注意
Keil C51测试版本在功能和你能够创建的应用的代码长度上都有限制关于这点的详细信息请参考版本发行说明要创建大的应用系统你必须购买其中一个开发包关于开发包套件的详细描述参考P16的产品一览
本章描述了uVision2的创建模式并向你演示如何使用它来创建一个简单的程序(译者注原文为示例程序)以及生成和维护项目的一些选项包括文件输出选项C51编译器的关于代码优化的配置uVision2项目管理器的特性等
创建项目
uVision2包括一个项目管理器它可以使你的8051应用系统设计变得简单要创建一个应用你需要按下列步骤进行操作
启动uVision2新建一个项目文件并从器件库中选择一个器件
新建一个源文件并把它加入到项目中
增加并配置你选择的器件的启动代码
针对目标硬件设置工具选项
编译项目并生成可以编程PROM的HEX文件
这里将一步一步的进行描述从而指引你如何去创建一个简单的uVision2项目
57
从这里开始创建你的应用58
启动uVision2并创建一个项目
uVision2是一个标准Windows应用程序直接点击程序图标就可以启动它要新建一个项目文件从uVision2的Project菜单中选择New Project这将打开一个标准的Windows对话框此对话框要求你输入项目文件名
我们建议你为每个项目建一个单独的文件夹你可以在弹出的对话框中点击新建文件夹的图标来得到一个空的文件夹然后选择子文件夹并键入项目的名称如Project1uVision2将创建一个文件名为Project1.UV2的新项目文件新的项目文件包含了一个以默认的文件名命名的目标和文件组你可以在项Project Window – Files看到这些名字
现在从菜单Project – Select Device for Target为你的项目选择一个CPU弹出的对话框中显示的是器件数据库你只要选择所需要的MCU就可以了在例子程序中我们选择Philips 80C51RD + CPU该选择就为80C51RD+器件设置了工具选项这种方式简化了工具的配置
注意:
对于一些器件uVision2环境需要你手工输入一些附加的参数仔细阅读此选择器件对话框中Description下面的信息它可能提供了你所选择器件的一些附加的说明
58
从这里开始创建你的应用59
一旦你从器件库中选择了一个CPU你就可以在项目窗口的Books页打开此CPU的用户手册这些用户手册是Keil开发工具光盘中的一部分
新建一个源文件你可以用菜单选项File-New来新建一个源文件这将打开一个空的编辑窗口让你输入你的源代码当你把此文件另存为*.C的文件后uVision2将高亮显示C语言语法字符我们把我们的例子程序保存为MAIN.C
一旦你创建了源文件你就可以把它加入到你的项目中uVision2提供了几种手段让你把源文件加入到项目中例如你可以右击Project窗口– Files页中的文件组来弹出快捷菜单菜单中的Add Files选项打开一个标准的文件对话框从对话框中选择你刚刚生成的文件MAIN.C
59
从这里开始创建你的应用60
增加和配置启动代码.
文件STARTUP.A51是大多数不同的8051CPU准备的启动代码启动代码清除数据存储器并初始化硬件和重入函数堆栈指针另外一些8051派生产品要求初始化CPU来迎合你设计中的相应的硬件例如,Philips 8051RD+提供的片上xdata RAM应该在启动代码中启用假如你需要修改启动文件来迎合你的目标硬件你应该把文件STARTUP.A51复制一份到你的项目文件夹中
为你选择的CPU的配置文件创建一个文件组是一个良好的习惯用菜单Project – Targets, Groups, Files 打开对话框来添加一个名为System Files的文件组到你的目标中也在此对话框中你用Add Files to Group按钮把文件STARTUP.A51添加到你的项目中
60
从这里开始创建你的应用61
项目窗口的文件页列出了你项目的所有条目.
现在,你的uVision2的Project Window – Files应该显示上图中的文件结构在项目窗口中双击文件名STARTUP.A51你就可以把它在编辑器中打开然后按照P197页的第十章(CPU and C Startup Code)的描述来配置启动代码如果你使用你所选择器件的片上RAM在启动代码中的设置必须匹配Options – Target对话框中的设置下面来讨论Options – Target对话框
61
从这里开始创建你的应用62
为目标设置工具选项
uVision2 允许你为你的目标硬件设置选项Options for Target对话框可以通过工具条图标打开在目标的各个页中你可以定义和你的目标硬件及你所选器件的片上元件相关的所有参数下图显示了我们例子的设置
下表描述了目标对话框的一些选项
对话框条目描述Xtal 定义CPU时钟对于大多数应用中和实际的XTAL频率相同Memory Model 定义编译器的存储模式对于一个新的应用默认的是SMALL模式参照P78 存储模式和存储器类型的讨论Allocate On-chip
Use multiple DPTR registers 定义在启动代码中使能的片上元器件的使用如果你使用片上xdata RAM那你应该在文件STARTUP.A51中使能XRAM的访问Off-chip Memory 在此定义你目标硬件上所有的外部存储器区域Code Banking
Xdata Banking 为代码和数据的分段Banking定义参数详细信息参照P67 代码分段Code Banking
注意
有些选项只有在你使用LX51连接器时才有用LX51连接器只在PK51中提供.
62
从这里开始创建你的应用63
Build项目并生成HEX文件
通常情况下在Options – Target对话框中的设置已经足够使你开始一个新的应用通过单击工具条上的Build目标的图标你可以编译所有的源文件并生成应用当你的应用中有语法错误时uVision2将在Output Window – Build页显示这些错误和告警信息双击一个信息将打开此信息对应的文件并定位到语法错误处
一旦你成功的生成了你的应用你就可以开始调试了uVision2调试器功能的讲述请参照P93页Chapter 5的Testing Programs在你调试完你的应用后需要创建一个HEX文件来烧片子或软件模拟当Options for Target – Output中的输出HEX文件使能时uVision2每进行一次Build都生成HEX文件如果定义了Options for Target – Output中的Run User Program #1 选项时在生成操作完成后将自动运行此处定义的操作如编程PROM器件
63
从这里开始创建你的应用64
现在你能够修改已经存在的源文件或添加一个新的源文件到项目中Build目标工具条按钮只编译修改过的或新加进来的文件然后生成执行文件uVision2维护一个文件包含清单从而知道某个源文件用到的所有的包含文件而且工具配置的选项也被保存在此清单中所以uVision2能够只编译那些需要重新编译的文件利用Rebuild All Target(原文为Rebuild Target)命令所有的文件都被重新编译而不论是否被修改过
项目目标和文件组
通过使用不同的项目目标uVision2允许你为一个应用创建几个不同的程序你也许需要一个目标用来测试另一个目标是你应用系统的发行版本在同一个项目文件中允许每个目标进行独立的工具设置
文件组是用来让你把项目中相关的文件放在一组这在要求把文件按功能块组织和确定你的软件开发团体中的工程师们的职责时特别有用我们已经在本例子中使用了文件组去分离与CPU相联系的文件和其它文件利用这个技术可以轻松的管理拥有几百个文件的复杂项目
Project – Targets, Groups, Files 对话框允许你创建项目目标和文件组我们已经用此对话框增加了系统配置文件下图中显示了一个例子项目的结构
项目窗口向你展示所有的组和相联系的文件文件按照显示的顺序编译和连接你可以通过拖放来移动文件的位置你也可以单击目标或组的名字去修改它右击将打开弹出菜单弹出菜单可以让你
1)设置工具选项
2)删除条目
3)添加文件到组
4)打开文件
在Build工具条上你可以快速改变当前的目标
64
从这里开始创建你的应用65
浏览项目窗口中的文件和文件组的属性
在项目窗口的文件页中用不同的图标来标识不同的文件和文件组属性下面是这些图标及其对应的属性的描述
此图标是在文件图标上加一箭头而成用来表示被编译连接到项目中去的文件
文件图标用来表示不被连接到项目中去的文件典型的如文档文件另外在文件的属性窗口中取消Include in Target Build复选项,将使该文件剔除出项目剔除出项目的文件也用此图标参照87页的文件和文件组的详细选项– 属性对话框
图标上有一把钥匙用来表示只读文件典型的如被软件版本控制系统(SVCS)控制的文件因为SVCS把他所控制的文件的本地拷贝设置为只读属性参照76页的使用SVCS菜单
此图标左边有三个点用来表示有特殊选项的文件(图4)或文件组(图5)参照87页的文件和文件组的详细选项– 属性对话框
注意
不同的图标让你能够快速浏览到一个项目不同的目标中的工具设置
你所看到的图标总是反映当前所选目标的属性例如你在一个目标中对一个文件或文件组设置了特殊选项那只有在你选择了此目标时那些你设置了特殊选项的文件的图标上才会有三个点
/*********************************译者注-开始**********************************/
第二个图标为文件图标所有属性文件的图标都以此为基础再加上箭头钥匙三点中的零到三个来表示文件对于所在目标的属性更进一步说只读是属于文件自己的即一个文件具有只读属性那它在任何目标中都具有但是否包含在项目中是否设置了特殊选项,是文件对于目标的属性即在一个目标中的一个文件的图标上有箭头和(或)三点,在另一个目标中并不一定如此
/*********************************译者注-结束**********************************/
65
从这里开始创建你的应用66
浏览配置对话框
此选项对话框让你设置所有的工具选项通过Project Window – Files中的弹出式菜单你可以为文件组或者一个单独的文件设置不同的选项在此处的描述中你仅仅能够获得对话框的相应的标签页利用对话框中的帮助按钮你可以获得大多数对话框条目的帮助下表描述了目标对话框的选项
对话框的标签页描述Target 定义你应用的硬件详细信息参见第62页Output 定义Keil工具的输出文件并让你定义生成处理后执行的用户程序更多信息参见P82 Listing 定义Keil工具输出的所有列表文件C51 设置C51编译器的特别的工具选项如代码优化或变量分配更多信息参见P80 A51, AX51 设置汇编器的特别的工具选项如宏处理L51 Locate
LX51 Locate 定义不同类型的存储器和存储器的不同段的位置典型情况下你可以选择Memory Layout from Target Dialog来获得自动设置如下图所示更多信息参见P86 L51 Misc
LX51 Misc 其它的与连接器相关的设置如告警或存储器指示Debug Vision2 Debugger的设置更多信息参见P101 Properties 文件和文件组的文件信息和特别选项参照P87的File and Group Specific Options – Properties Dialog
在L51标签页中一旦你使能Use Memory Layout from Target Dialog选项uVision2使用从你所选择器件和目标标签页中所得到的存储器信息你还可以添加附加的段到这些设置中
66
从这里开始创建你的应用67 代码分块Code Banking
一个标准的8051器件能寻址64KB的代码空间为了突破64KB程序空间的限制Keil 8051工具支持代码分块这个技术让你管理一个公共的区域和32个每个最大达64KB的块,从而达到总共2MB的代码切换空间
例如你的硬件设计可以包括一个位于0x0000-0x7fff空间的32KB的ROM如你所知的公共区域或公共ROM和四块位于0x8000-0xffff空间的32KB的ROM如你所知的代码块ROM通常情况下代码块通过端口线选择右图显示了此应用的存储器结构
代码分段在Options for Target – Target标签页中使能和配置在此对话框中输入你硬件支持的代码分块的个数和分块的区域上例中的存储器分块的配置如右图所示
为了配置你的分块的硬件你需要将文件C51\LIB\L51_BANK.A51加入到你的项目中复制此文件到你的项目文件夹把它和你项目中的其它源文件放在一起并添加它到你项目的一个文件组文件C51\LIB\L51_BANK.A51必须修改以便迎合你的目标硬件
每个文件或文件组通过其Options – Properties对话框可以被定义到一个特定的代码块中
67
从这里开始创建你的应用68
来打开此文件或文件组的Options – Properties对话框此对话框允许你选择哪个代码块或公共区域共区域可以在所有的代码块中访问,此公共区域常常包括那些必须一直需要访问的
通过右击项目窗口中的文件或文件组
公
进程和数据常量如中断进程中断和复位向量字符串常量和块切换进程所以连接器只把一个模块中程序段定位到相应的块区域中的数据常量放在公共区域中如果你能够确定某些常量段的信息只被某个特定的代码块访问你可以在Options forarget – L51 Misc中用BANKx来指示连接器把这些常量段定位到此特定的代码块中T 以上的步骤完成了你的代码分块应用
用你的PROM编程器把这些HEX文件写到你的的配置uVision2调试器完全支持代码分
块从而让你能够调试程序如果你在Options for Target –Outpt对话框中使能Create EuHX File项
选uVision2将为每个代码块生成一个从地址0开始的64KB的物理映像你需要EPROM中相应的存储空间中去
68
从这里开始创建你的应用69 uVision2 功能的功能uVision2包含许多强大
为你的整个软件项目中提供帮助这些功能将在下面几节描述
多个文件中查找in Files打开一个对话框菜单Edit – Find实现在所有特定的文件中查找一个字符查找结果显示在输出窗口的Find in Files页双击输出窗口的Find in Files页中的某个输出
编辑器将定位到匹配此字符串的文本行
资源浏览器此资源浏览器显示你程序中的所用的符号的信息如果Options for Target – Output中Browser Information选项被选中编译器编译时将把浏览信息包含到项目文件中用菜V
单iew – Source Browser去打开资源浏览器窗口
窗口列出了符号名
此浏览
类类型存储器空间和使用的次数单击列名将按此列序显示信息排
你可以用下表描述的选项来过滤浏览的信息
69
从这里开始创建你的应用70
资源浏览器选项描述
Symbol 定义一个屏蔽格式用来显示与其相匹配的符号此屏蔽格式可以包含实际的字符和以下通配字符
#匹配数字0-9
$ 匹配任意字符
* 匹配任意字符串
选择需要显示的符号的类型
选择一个文件显示此文件中的符号资源Memory Spaces 型
定义需要显示的符号的存储类
包括NULL Filter on File Outline 下表中列出了一些符号代码的例子代表的符号名
代码* 可代表任何符号在符号浏览器中是默认的代码
*#* 代表的符号为下划线开头接字母a,任一字母任一数字可加任何字符_*ABC 下划线加任意字符或不加然后接ABC 右击资浏览器Definitions and references框中显可代表有一个数字的符号数字在符号的任何位置_a$#* 最后可不加字符也源示的义或引用
定弹出的菜单可以让你在编辑器打开相应的内容对于函数你还可以看到调用和被调用的关系图在此Definitions ad references框中还为你提供了一些附加的信息n 符号描述
[D] 定义处
[R] 引用处
[r] 读操作处
[w] r/w]/写操作处[] 地
址引用处写操作处[ 读
70
从这里开始创建你的应用71
目或者使用下列键盘快捷键
快捷键描述
你可以使用编辑窗口中的浏览信息选中你要找的条并用鼠标右键打开局部菜单F12 跳转到定义处将光标置到符号定义处
Shift+F12 引用处
跳转到Ctrl+Num+
跳转到下一处的引用或定义处Ctrl+Num–
跳转到前一处的引用或定义处将光标置到符号引用处
开发工具参数的键序列值表示2开发环境向外部用户程序传递参数
一个键序列可以用来从uVision
键序列可以应用在工具菜单SVCS 菜单以及用户程序在Options for Target-Output 对话框中定义的参数传递中一个键序列是键码和文件码的组合下面列出有效的键码和文件码键码定义由文件码所确定的文件的路径
% 包含扩展名的文件名但是不包含路径如PROJECT1.UV2
# 包含绝对路径的文件
名如C:\MYPROJECT\PROJECT% 包含扩展名的文件名,是不包含路径
ROJECT1.UV2 @ 不包含路径和扩展名的文件名如PROJECT1 $ 文件夹名如C:\MYPROJECT ~ 当前光标所在位置的行号仅仅在文件码为F时效^ 当前光标所在位置的列号仅仅在文码文件码为F时有效要在用户程序的命令行使用$, #, , @, 1.UV2 但如有件码为F时有效注键~和^只在%~ 或^用$$, #, %%, @@, ~~ 或^^格式
#
例如@@在用户程序的命令行上提供一个单独的@字符定义插入在用户程序命令行上的文件名或参数
文件码
F 回项目文件如果选择的文件组名则返回当前活动编辑器
P 中的文件当前项目名如PROJECT.UV21 连接器输出文H HEX 应用文件PROJECT1.H86 uVision2 可执行文件如C:\KEIL\UV2\UV2.EXE
在Project Window - Files 页中选定的文件如MEASURE.C如果选择的目标名则返L 件通常为调试而生成的可执行文件如PROJECT1 如71
从这里开始创建你的应用72 SVCS系统中的文件码
以下是应用在
更详细的信息参照P76的使用SVCS菜单文
件码定义插入在用户程序命令行上的文件名或参数
Q 为SVCS系统保持注释的文件名
R 为SVCS系统保
持修正码的字符串C 为SVCS系统保持校验码的字符串
U 用户名在SVCS-Configure Version CoV 文件名在SVCS-Configure VersionQ, U 和V能和键码%组合使用ntrol -User Name中定义Control -Database中定义R, C, 只
菜单
使用工具
通过工具菜单你可以运行外部程序要添加自定义程序到工具菜单可以通过菜单Tools – Customizeools Menu...打开对话框T在此对话框中配置外部用户应用的参数右边的对话框示例了一个工具设置上图所示的话对框的设置将展工具菜单
扩
如右所示对话框的选项说明如下
此描述
对话框条目
Menu Content 工具菜单上显示的文本其中的每行都可以包括键码和文件码用与号()来定义一个快捷键你可以定义当的菜单行的在下面列出的各个选
前选择项如果选中那么在你点击此菜单时一个对话框将弹出提示你输入用户程序的Run Minimized
Command 如果选中将使此应用程序运行时的窗口最小化输入你点击此菜单时运Initial Folder 应用程序的当前工作目录如果为空uVision2 Arguments 传递给应用程序的命令行参数
Prompt for Arguments 命令行参数72 行的程序的路径用当前项目所在的目录
从这里开始创建你的应用73 基于应用程序个中
的命令行输出被复制到一临时文件当此应用执行完成后此临时文件的内容将在Ou
tput Window -Build页列出
73
从这里开始创建你的应用74 运行PC-Lint 的PC-Lint 核对你应用中的所有模块C源代码的语法和语义
Gimpel Software
PC-Lint识出可能的错误和矛盾的地方并停在模糊的错误的或无效的C代码处PC-Lint可以标
显著地减少你花费在目标应用上的调试精力在你的PC机上安装PC-Lint然
后在Tools –Setup PC Lint对话框中
输入参数本例显示了一个典型的PC-Lint的配置为了在Ouutpt Window-Build页的输出,你应该使用位于
lint你的源代码了菜单Tools – Lint运行PC-Lint得到正确
KEIL\C51\BIN文件夹中的配置文件
PC-Lint后你就可以检查
在安装
来检查当前编辑器中的文件菜单Tools – Lint All C Source Files运行PC-Lint来检查你项目
中所有的C源文件PC-Lint的输出信息被重定向到Output Window-Build页双击PC-Lint的输出信息将使编辑器定位到相应的位置
为了在Output Window-Build页获得正确的结果
PC-Lint需要在配置文件中包含以下选项的
74
从这里开始创建你的应用75 配置文件C:\KEIL\C51\BIN\CO-KC51.LNT已经包含这些行强烈推荐使用此配文件因为它还包含Keil 51编译器需要其它的PC-Lint选项
置
Siemens Easy-Case
Vision2 为Siemens Easy-Case提供了一个直接的接口Easy-Case是一个图形和程序
文档编辑器你可以用Easy-Case编辑源程序代码另外一些Vision2调试器中的命令在环境中也有效
Easy-Case
E 安装asy-Case:为了在Easy-Case中使用Visio调试器命令n2文件C:\KEIL\UV2\UV2EASYP-CP.INI中的配置设置应该增加到WINDOWS系统目录中的文件P.INI中
EASY-CP
这可以通过任何文本编辑器或DOS的copy命令来实现copy命令如下C:\CD C:\WINNT C:\WINNTCOPY EASY-CPP.INI+C:\KEIL\UV2\UV2EASY-CPP.INI EASY-CPP.INI
在Vision2的Tools – Setup .EXE的路径
Easy-Case对话框中输入
EASYCPP
到此就完成Siemens Easy-Case了的配置Easy-Case浏览源代码
你可以用Tools – Start/Stop Easy-Case来启动在当前位置打开Vison2中活动编辑器中的文件利用asy-Case菜单项目Tools – Show
Easy-Case的菜单Vision2提供几个调试命令以允许程序在Vision2调试器中执行
E
75
从这里开始创建你的应用76 uVision2 为软件版本控制系统使用SVCS菜单
SVCS提供了一个可配置的接口为以下几个SVCS文件提供预先配置的模板
Intersolv PVCS,Microsoft SourceSafe, and MKS Source Integrity 通过SVCS菜单
描述调用你的版本制系统的命令行工具SVCS菜单的置存储在一个模板文件中控
配
过进此菜单的配置通SVCS –Customize SVCS Menu对话框行
此对话框的选项解释如下
对话框条目
Template File SVCS菜单配置文件的名字推荐一个项目开发组的所有成员使用相同的模板文件所以模板文件应该复制到文件服务器中U用户名
ser Name 在参数Database SVCS系统使用的数据库的文件名或路径通过%V文件码传递显示在SVC菜单中的文本行可以包括键码和文件码快捷键对于当前被选中的菜单行你可以定义以下列出的选项Query for
允许你在执行此SVCS命令前询问一些附加的信息注释将被复制到一
Comment Revision
个临时文件中此文件通过文件码%Q作为一个参数传递给SVCS命令版本和校验信息通过文件码%R和%C作为字符串传递
用来登录到SVCS系统行它是通过%U文件码传递的Menu Content S用字符’’’’定义76
从这里开始创建你的应用77 CheckPoint Run Minimized Command Argument如果你想以最小化的窗口来执行应用使能此选项当你点击此SVCS菜单项时将调用的程序文件Environment 在执行此SVCS程序前需要设置的环境变量命令行SV应用程序的输出被复制到一个临时文件中件的内容被列在Output Window – Build页s 传递给此SVCS程序文件的命令行参数CS当SVCS命令完成后此临时文出
如右图所示是一个
SVCS菜单在Project Files页一个选中的文件是一个
Window –两个单独的文件项目设置保存在*.UV2文件中此文件将被指导编译生成应用代码uVision2的本地配置被保存在*.OPT文n来SVCS参数目标名使用*.UV2格式的项目名如你所见的文件的本地拷贝是一个只读文件所以它的图标上有一个钥匙样符号uVisio项目被保存为SVCS查找并且可以用
件中包含视窗位置和调试设置
下表列出了典型的SVCS菜单项
根据你配置的不同你的菜单项也许和此不同或还有项包含文件可以作为文档资料添加到项目中以便SVCS可以迅速访问到它们其它增加的
SVCS菜单项描述Explorer 打开交互式的SVCS浏览器Check In 把文件保存到SVCS的数据库中并把本地文件设置为只读属性
Check Out 从SVCS中取得最新版本的文件
并把ndo Check取消Check Out操作Out Put Current 把文件保存到SVCS的数据库中但对本地文件仍然可以修改Version Version Project ifferencesHistory
本地文件设置为可修改属性U 77 Get Actual 从SVCS中取得一个只读文件的最新版本Add file to 添加文件到SVCS项目D, 显示关于每个特定文件的SVCS信息Create Project 创建一个和本地uVision2项目文件名相同的SVCS项目
从这里开始创建你的应用78
注意用一个文本编辑器修改预先配置好的*.SVCS文件来适应程序路径和工具参数
你也许要
在你选择一个新的项目后Microsoft SourceSafe要求运行设置当前项目命令删除SSUSER环境变量使用工作站的登录名MKS Source Integrity 的默认配置是在服务器上预先创建一个项目数据库工作站
这边是一个本地的发送邮箱形式的工作空间Intersolv PVCS 在创建和维护项目上都没有预先配置
写优化代码许多配置参数影响你的8051应用的代码质量
编
然而对于绝大部分应用使用默认的好的代码工具设置都能产生良
你应该知道这些改善代码空间和执行速度的参数这一节描述代码的优化技术储模式和存储类型对代码空间和执行速度影响最大的是存储模式
存
存储模式影响变量的存取详细信息参照35页的存储模式
存储模式的选择在Options for Target – Target dialog 页
78
从这里开始创建你的应用79 全局寄存器优化Keil 8051 工具支持大范围的寄存器优化此选项的使能对应Options for Target – C51 对话框中的Global Register Optimization选项利用大范围寄存器优化C51编译器知道哪些寄存器被外部程序修改那些没有被外部程序修改的寄存器将被用来存储寄存器变量这样C编译器产生的代码将占用较少的空间并且执行速度更快为了改善寄存器的分配uVision2在Build时对C语言源程序自动进行多次编译下例中input和output是外部程序只需要很少的寄存器
79
从这里开始创建你的应用80
其它C51编译器指示参数
还有好几个其它的C51编译参数能够改善代码质量这些参数的选择由Options – C51对话框提供在一个应用中你可以用不同的编译设置来编译C程序模块通过列表文件你可以比较由不同的编译设置编译生成的代码的质量
下表描述了C51对话框页的选项对话框条目描述Define 定义预处理符号Undefine 仅仅在组或文件的Options中有效它允许你删除高一级的Options中定义的预处理符号Code Optimization Level 定义C51优化级别通常情况下你不用改变默认的值利用其最高级别的优化9公共块提取为子程序编译器检测多次使用的指令块
并把它们提取到一个子程序中在分析代码时编译器也试图用简单的指重新安排指令序列
令既然编译器插入子程序和CALL指令优化后的代码的运行速度也许会变慢此种优化往往能够显著提高代码密度Code Optimization Emphasis 你可以根据代码空间或执行速度为目的来选择优化利用Favor Code SizeC51编译器用库函数调用代替运行速度快的内联代码Global Register Optimization 使能”Global Register Optimization”参见79页的详细描述
80
从这里开始创建你的应用81 对话框条目描述
D’’t use absolute register accesses禁止使用寄存器R0-R7的绝对地址访on 问由于C51不能使用ARx符号所以代码将有一点点增加比如用PUSH和POP指令时需要插入替代代码然而代码也将不依赖于寄存器段Warnings 选择C51输出告警信息的级别级别0禁止所有告警Bits to round for float compare 决定浮点数比较的位数
Interrupt vectors at address 通知C51编译器为中断函数产生中断向量并定义中断向量表的基本地址Keep Variables in Order 通知C51编译器根据变量在C语言源程序中的定义来顺序分配变量地址此选项不影响代码质量Enable ANSI ger promotion rules 当为进行比较而将比整型表达式短的表达式类型转换为整型表达式时进行符号位扩展的表达式
inter
此选项通常将增加代码长度但是需要兼容ANSI标准时必须这样做Misc Controls 允许你输入特别的C51编译指导参数当你使用一个非常新的8051器件而需要特别的编译指导参数时使用此选项Compiler Control 显示C51编译器运行时的编译指导参数的名称String 的源文件的各个让你能够立即确认你编译选项
数据类型8位微
8051 CPU数或长是一个控制器用8位字节如char和unsigned char的操作比用型的操作要更有效整整数类
81
从这里开始创建你的应用82
技巧和窍门讨论一些关于项目管理的一些高级技巧
接下来的一节你不会常用入uVision1的目到uVision2 你可以用下面的步骤来把在uVision1中创建的项目导入到uVision2中
isi很重要的一点是新的uVision2项目必须创建在你已经存在的uVision1项目文件夹中
t 已经存在的uV n1项目只有新项目的文件列表是空的时此菜单才是可用的Options for Tat-Target对话框选项来定义你目标硬件的存储器结构一旦你这样做了ns for Target – L51 Locate对话框中的UseMarget Dialog选项并在此对话框中删除用户类型和用户区域的设置经到以下的特性但是读了这一节你将对uVision2的性能有更深的了解导项创建一个新的uVon2项目文件并按照58页的描述从器件数据库中选择一个CPU注意单击菜单Projec– Import uVision1 Project在弹出的对话框中选择上述文件夹中一个isio导入命令同时也把旧连接器的设置导入到连接对话框中但是我们推荐你用uVision2的rge你就可以使能Optio emory Layout from T 仔细核对是否所有的设置都正确的复制到了新的uVision2项目文件中
注意由于uVision2在许多方面和以的前版本不一样所以不可能100%的把uVision1项目转uVision2项目
换为
在你导入uVision1项目后仔细核对工具设置是否正确转换一些uVision1项目设置
如
用户编译器和库模块列表不能转换到uVision2项目中还有dScope调试器设置也不uVision2项目文件中能复制到
现在你可以在新uVision2项目中创建文件组如64页Project Targets and File Groups叙述的那样然后你就可以把文件拖放到此新文件组中
82
从这里开始创建你的应用83 /******************************译者注-开始******************************/ 步骤2中的描述与我的uVision2菜单的实际情况不一致我的uVision2中的菜单点击后话框
弹出一个提示对
内容与本页的描述部分内容相似问你是否继续继续后提示选择新项目和旧项目而且菜单一直有效步骤3中我的版本中的L51 Locate对话框中没有用户类型和用户区域的设置条目因无而从删除也许原文相关部分应翻译为并且此对话框中已经删除了用户类型和用户区域的设置/******************************译者注-结束******************************/
Build后运行外部程序
Options for Target – Output对话框允许你输入最多两个用户程序在Build成功后开始执行使用键序列码你可以从uVision2项目管理中传递参数到这些用户程序中
你可以在编辑器窗口中使用浏览器的信息选择你想要寻找的条目右击打开弹出式菜单或使用下面的快捷键
快捷键描述F
12 跳转到定义处跳转到引用处将光标置到符号引Ctrl+Num+ 将光标置到符号定义处Shift+F12 用处跳转到下一处的引用或定义处Ctrl+Num– 跳转到前一处的引用或定义处工具参数的键序列参照71页的描述
83
从这里开始创建你的应用84 上例中User Program #1的调用参数是带绝对路径的Hex格式的输出文件如
C:\MYPROJECT\PROJECT1.HEXUser Program #的用参数仅仅是连接器的输出文件PROJECT1的名调字不带后缀和参数-p表示的向项目的路径:\MYPRJECT指CO如果你的文件夹名字中包含如~,#等特字别符你应该用引号把键序列括起来你可以指定工具的输出文件到不同的文件夹
为列表文件和目标文件指定单独的文件夹
Output对话框让你选择目标文件的文件夹 Options for Target –
当你为项目的各个目标的目标文件指定一个独立的文件夹时uVision2仍然把在此之前的生成过程产生的目标文件视为有效即使你修改了你的项目目标Build Target命令也只是重新编译那些修改过的文件
Options for Target – Listing对话框利用Select Folder for List Files按钮为列表文件
提供同样的功能
使n2器uVision2器件库包含所有的标准的8051产品
用uVisio器件库中没有列出的微控制
然而也有一些用户定做的器件和未来器件目前没有包含进来的
如果你需要使用这些没有列出的CPU你有两种选择选择Generic中列出的一个器件
8051all Variants器件允许你配置所有的工具参数所以支持所有类型的CPU在Options for Target – Target对话框中定义片上存储器和扩展的存储器 输入一个新的CPU到uVision2器件库中点击菜单File – Device Database
打开对话框选择一个和你想添加的器件最相似的CPU然后修改参数在选项框中的CPU设置定义了基本的工具设置这些参数如下表所述参数详细定义IRAM (range) 片上IRAM的地址空间XRAM (range) 片上XRAM的地址空间IROM (range) 片上ROMflash的地址范围起始地址必须是0 CLOCK (val) 当你选用此器件时的默认时钟频率MODA2 Atmel各种器件的双数据指针MODDP2 Dallas各种器件的双数据指针MODDPX 使能扩展的24位的数据指针寄存器如ADuC812 84
从这里开始创建你的应用85 MODP2 Philips and Temic各种器件的双数据指针MOD517DP Infineon C500列器件的多用途数据指针系MOD517AU Inineon C500系列器件的算术逻辑单元f MOD_CONT 使能支持Dallas 390 Contigious模式
MX 使能支持Philips 80C51MX 其它的选项变量定义CPU数据手册和uVision2调试DLL文件当增加一个新的器件到数据库中时保持这些变量不变
创建一个库文件在Options for Target – Output对话框中选择创建库文件uVision2将调用库管理器是连接/定位器而不此时由于库中的代码将不需要连接和定位所以在L51 Locate和
L51 Misc选项页的输入将被忽略同时在目标页中的CPU和储存器的设置也无关紧要
如果你计划将你的代码应用到不同的8051器件中你可以选择器件库中Generic下的一个PUC
复制工具设置到一个新的目标中当你在Project – Targets, Groups, Files
对话框中增加一个新目标时使能选项Copy all Settings from Current Target从一个已经存在的目标复制工具设置到当前的目标的步骤如下
用Remove Target命令在Targets,Groups,Files对话框中删除当前的目标1
2选择你想要复制工具设置的目标为当前目标能Copy all Settings from Current Target后再把刚刚删除的目标加进来3使
85
从这里开始创建你的应用86 放置存储有时代码到绝对器空间需要把一些址
代码段放在特定的存储器地
下例中名叫alarm_control的结构体将要放在0xC000地址处这个结构体在文件ALMCTRL.C中定义并且这个模块只包含此结构体的声明
struct alarm_st { unsigned int alarm_number; ar enable flag; unsigned int time_delay;
unsigned ch
unsigned char status; }; struct alarm_st xdata alarm_control;
C51编译器为ALMCTRL.C产生目标文件包含一个存储在xdata存储空间中的变量
段
变量alarm_control是放置在?XD?ALMCTRL段中在Options for Target – L51 Locate页中uVision2允许你定义任何段的起始地址下例中连接/定位器将把名?XD?ALMCTRL空间的从0xC000起始的地方为的段放在xdata储存
C51提供关键词_at_和一些能够存取
更详细的信息参见第六章的C51 User’’s Guide
注意绝对地址的宏
86
从这里开始创建你的应用87 Path显示所选文件的信息, Type, Size
置文件和文件组的特选项
文件和文件组的特定选项- 属性对话框uVision2允许你通过Project Window – Files页的弹出式菜单设
操作如下选择一个文件或文件组右击选择Options for 菜单然后你就可浏览或设置刚才所选择的文件或文件组的特定的选项定
以
此对话框中有三态选择如果一个的选项为灰色
默认值表明包含上一级文件组或目标的同样的设置下表描述了属性对话中的各个选项框
对话框条目描述
Last Change Include in 禁止此选项87 Target Build 将把此文件或文件组排除在目标之外如果此选项没有选上uVision2将不编译和连接此文件或文件组当你为几个不同的硬件系统使用此相同的项目文件时这对那些配置文件很有用Always Build 使能此选项后在每一次生成过程中都将从新编译此源程序模块而不管是否修改过这对包含__DATE__ 和__TIME__ 宏的文件很有用这些宏用来记录应用程序的版本信息Generate Assembler SRC File 通知C51编译器为C源程序产生汇编语言代码源程序通常当C语言源程序包含#pragma asm / endasm项时选用此项Assemble SRC File 此选项和Generate Assembler SRC File选项一起使用将C51产生的汇编语言源程序汇编为目标文件连接到应用中
从这里开始创建你的应用88
对话框条目描述Link Publics Only 此选项只对Lx51有用它通知连接器只包含此模块中的公共符号通常在你想使用其它不同的应用入口地址或其中的变量地址时使用此选项在项目包含一个绝对目标文件时的绝大多数情况下适应Stop on Exit Code 定义一个基于编译信息的生成退出代码默认时uVision2在一次生成过程中编译所有文件而忽略错误或告警信息Select Modules to Always Include 允许你一直包含库中某些特定的模块更多信息请参见89页的一直包含特定的库模块Custom Arguments 当你的项目包含需要其它编译器编译的文件时此行才需要更详细的内容参见90页的使用用户编译器
例中我们为文件
FILE1.C定义的属性为编译时遇到告警即停止编译过程无论此文件是否修改在每一次生成过程中都要编译此文件
本
88
从这里开始创建你的应用89 编译带asm / endasm项的C语言模块如果你在你的C语言源程序中包含了汇编语句
C51编译器要求你先产生一个汇编语言源文件再译此汇编源文件编请核对你是否能你的C提供了几个内联的函数不需要插入汇编指令C语言源代码
在此情况下使能属性对话框中的Generate Assembler SRC File和Assembler SRC File选项注意够用内联的内部函数代替汇编代码由于这样做后语言源程序将不容易移植到其它平台上所以通常情况下避免汇编代码片段比较好C51编译器为你从而允许你访问所有的特殊外围器件通常情况下
一直包含特定的库模块属性对话框允许你定义那些需要一直包含在项目中的库模块这在你生成一个应用的基本部分时也许用到此基本部分包含未来加载的程序用到的一些基本子程序这儿先增加包含需要的目标模块的库文件在Project Window – Files页右击此库文件打开弹出式菜单点击Options for...在属性窗口选择那些需要一直包含的模块仅仅选择那些个你想在任何情况下都包含在你的目标应用中的模块
89
从这里开始创建你的应用90
使用用户编译器如果你增加一个带未知文件后缀的文件到你的项目中uVision2要求你定义此文件的文类型
件
你可以选择用户文件并用一个用户编译器来处理此文件用户编译器和其命令行参数在Options –Properties对话框中的Custom Arguments行一起定义通常情况下用户你需要增加此源文件到你的项目中并用A51或C51编译器将从用户文件产生一个源文件
将此源文件编译成能够连接到你的应用中的目标文件在此例中我们为文件CUSTOM.PRE定义了用带-X参数的C:\UTILITIES\PRETRANS.EXE程序来编译注意我们使用了Always Build选项来确保每次Build时都编译此文件
90
从这里开始创建你的应用91
使用Intel PL/M-51
如果你还有Intel PL/M-51源文件需要包含到你的uVision2项目中你可以以用户文件的形式增加这些源文件到uVision2项目树中另外你也必须插入由PL/M51生成的*.OBJ文件和Intel PLM51.LIB 针对PL/M-51编译器的选项在Options – Properties对话框中设置这个对话框用右击此源文件来打开针对PL/M-51编译器的选项在Options – Properties对话框的Custom Arguments行中输入
91
从这里开始创建你的应用92 文件扩展名tensions对话框允许你为一个项目设置默认的文件扩展名
Project – File Ex
你可以输入几个扩展名彼此用分号隔开文件扩展名是与项目相关的和汇编器设置不同的编译器
通过Project Window – Files页的弹出式菜单你可以为一不同选项个文件组或一个文件设置在对话框页中有三态选择如果一个选项为灰色那么高一层的设置将被继承
你可以利用这种方法为整个文件组定义一定的工具设置并能够为此组中单独的一个源文件变设置
该
版本和序列号信息当你Help – About
打开菜单时将列出所有工具的详细的信息在你向我们报告遇到的问题时请提供这些信息
92
作者:
时尚胡
时间:
2003-4-9 21:29
标题:
Keil C51 使用技巧及实战
说明得很明白。
作者:
loveme
时间:
2003-4-9 22:10
标题:
Keil C51 使用技巧及实战
thanks suncon
作者:
wyn
时间:
2003-4-10 00:39
标题:
Keil C51 使用技巧及实战
热心人,谢谢!
作者:
suncon
时间:
2003-4-10 04:40
标题:
Keil C51 使用技巧及实战
51的特殊功能寄存器详细列表
SPECIAL FUNCTION REGISTER ??
Register (MSB) (LSB) Byte
Symbol b7 b6 b5 b4 b3 b2 b1 b0 Address
P0 P0.7 P0.6 P0.5 P0.4 P0.3 P0.2 P0.1 P0.0 80H(128)
SP 81H(129)
DPL 82H(130)
DPH 83H(131)
PCON SMOD - - - GF1 GF0 PD IDL 87H(135)
*PCON SMOD - - WLE GF1 GF0 PD IDL 87H(135)
TCON TF1 TR1 TF0 TR0 IE1 IT1 IE0 IT0 88H(136)
TMOD GATE C/T M1 M0 GATE C/T M1 M0 89H(137)
TL0 8AH(138)
TL1 8BH(139)
TH0 8CH(140)
TH1 8DH(141)
P1 P1.7 P1.6 P1.5 P1.4 P1.3 P1.2 T2EX T2 90H(144)
*P1 SDA SCL RT2 T2 CT3I CT2I CT1I CT0I 90H(144)
SCON SM0 SM1 SM2 REN TB8 RB8 TI RI 98H(152)
SBUF 99H(153)
P2 P2.7 P2.6 P2.5 P2.4 P2.3 P2.2 P2.1 P2.0 0A0H(208)
IE EA - ET2 ES ET1 EX1 ET0 EX0 0A8H(168)
*IEN0 EA EAD ES1 ES0 ET1 EX1 ET0 EX0 0A8H(168)
+CML0 0A9H(169)
+CML1 0AAH(170)
+CML2 0ABH(171)
+CTL0 0ACH(172)
+CTL1 0ADH(173)
+CTL2 0AEH(174)
+CTL3 0AFH(175)
P3 RD WR T1 T0 INT1 INT0 TXD RXD 0B0H(176)
IP - - PT2 PS PT1 PX1 PT0 PX0 0B8H(184)
*IP0 - PAD PS1 PS0 PT1 PX1 PT0 PX0 0B8H(184)
+P4 CMT1 CMT0 CMSR5 CMSR4 CMSR3 CMSR2 CMSR1 CMSR0 0C0H(192)
+P5 ADC7 ADC6 ADC5 ADC4 ADC3 ADC2 ADC1 ADC0 0C4H(196)
+ADCON ADC.1 ADC.0 ADEX ADCI ADCS AADR2 AADR1 AADR0 0C5H(197)
+ADCH 0C6H(198)
T2CON TF2 EXF2 RCLK TCLK 矱XEN2 TR2 C/T2 矯P/RL2 0C8H(200)
*TM2IR T2OV CMI2 CMI1 CMI0 CTI3 CTI2 CTI1 CTI0 0C8H(200)
+CMH0 0C9H(201)
RCAP2L 0CAH(202)
*CMH1 0CAH(202)
RCAP2H 0CBH(203)
*CMH2 0CBH(203)
TL2 0CCH(204)
*CTH0 0CCH(204)
TH2 0CDH(205)
*CTH1 0CDH(205)
+CTH2 0CEH(206)
+CTH3 0CFH(207)
PSW CY AC F0 RS1 RS0 OV F1 P 0D0H(208)
+S1CON ENS1 STA STO SI AA CR1 CR0 0D8H(216)
+S1STA SC4 SC3 SC2 SC1 SC0 0 0 0 0D9H(217)
+S1DAT 0DAH(218)
+S1ADR SLAVE ADDRESSGC 0DBH(219)
ACC ACC.7 ACC.6 ACC.5 ACC.4 ACC.3 ACC.2 ACC.1 ACC.0 0E0H(224)
+IEN1 ET2 ECM2 ECM1 ECM0 ECT3 ECT2 ECT1 ECT0 0E8H(232)
+TM2CON T2IS1 T2IS0 T2ER T2B0 T2P1 T2P0 T2MS1 T2MS0 0EAH(234)
+CTCON CTN3 CTP3 CTN2 CTP2 CTN1 CTP1 CTN0 CTP0 0EBH(235)
+TML2 0ECH(236)
+TMH2 0EDH(237)
+STE TG47 TG46 SP45 SP44 SP43 SP42 SP41 SP40 0EEH(238)
+RTE TP47 TP46 RP45 RP44 RP43 RP42 RP41 RP40 0EFH(239)
B B.7 B.6 B.5 B.4 B.3 B.2 B.1 B.0 0F0H(240)
+IP1 PT2 PCM2 PCM1 PCM0 PCT3 PCT2 PCT1 PCT0 0F8H(248)
+PWM0 0FCH(252)
+PWM1 0FDH(253)
+PWMP 0FEH(254)
+T3 0FFH(255)
Notes: 1. * denotes the difference between 80C552 and 8051
2. + denotes the addition of 80C552
作者:
helsev
时间:
2003-6-3 05:31
标题:
Keil C51 使用技巧及实战
这本书在网上可以直接下载其全文,发贴者何不直接给出其网址?
作者:
suncon
时间:
2003-6-3 16:08
标题:
Keil C51 使用技巧及实战
方便读者。
这种网不一定任何时间都可以打开。
作者:
hustph
时间:
2003-7-9 22:06
标题:
Keil C51 使用技巧及实战
这样看很累的,直接让人家下载好些
the final word on 8051
很多地方都有下载的
老古上面下载速度不错还比较稳定
www.laogu.com
欢迎光临 光电工程师社区 (http://bbs.oecr.com/)
Powered by Discuz! X3.2