|
|
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的网络设计再到模糊控制希望你从
本书中学到的知识对你今后的系统设计有所帮助 |
|