第一章介绍,广州周立功单片机发展有限公司

太大 3
Tel02038730916387309173873097638730977Fax:38730925 第一章介绍这是一本关于Intel80C51以及广大的51系列单片机的书这本书介绍给读者一些新的技术使你的8051工程和开发过程变得简单请注意这本书的目的可不是教你各种8051嵌入式系统的解决方法为使问题讨论更加清晰在适当的地方给出了程序代码我们以讨论项目的方法来说明每章碰到的问题所有的代码都可在附带的光盘上找到你必须熟系C和8051汇编因为本书不是一本C和汇编的指导书你可以买到不少关于ANSIC的书最佳选择当然是Intel的数据书可从你的芯片供应商处免费索取和随编译工具附送的手册附送光盘中有我为这本书编写和收集的程序这些程序已经通过测试这并不意味着你可以随时把这些程序加到你的应用系统或工程中有些地方必须首先经过修改才能结合到你的程序中这本书将教你充分使用你的工具如果你只有8051的汇编程序你也可以学习该书和使用这些例子但是你必须把C语言的程序装入你的汇编程序中这对懂得C语言和8051汇编程序指令的人来说并不是一件困难的事如果你有C编译器的话那恭喜你使用C语言进行开发是一个好的决定你会发现使用C进行开发将使你的工程开发和维护的时间大大减少如果你已经拥有KeilC51那你已经选择了一个非常好的开发工具我发现Keil软件包能够提供最好的支持本书支持KeilC的扩展如果你有其它的开发工具像Archimedes和Avocet这本书也能很好地为你服务但你必须根据你所用的开发工具改变一些Keil的特殊指令在书的一些地方有硬件图实例程序在这些硬件上运行这些图绘制地不是很详细主要是方框图但足以使读者明白软件和硬件之间的接口读者应该把这本书看成工具书而不是用来学习各种系统设计通过本书你可以了解给定一定的硬件和软件设计之后8051的各种性能希望你能从本书中获取灵感并有助于你的设计使你豁然开朗当然我希望你也能够从本书中学到有用的知识使之能够提升你的设计
1 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 第二章硬件1概述 8051系列微处理器基于简化的嵌入式控制系统结构被广泛应用于从军事到自动控制再到PC机上的键盘上的各种应用系统上仅次于Motorola68HC11在8位微控制器市场上的销量很多制造商都可提供8051系列单片机像IntelPhilipsSiemens等这些制造商给51系列单片机加入了大量的性能和外部功能像I2C总线接口模拟量到数字量的转换看门狗PWM输出等不少芯片的工作频率达到40M工作电压下降到1.5V基于一个内核的这些功能使得8051单片机很适合作为厂家产品的基本构架它能够运行各种程序而且开发者只需要学习这一个平台 8051系列的基本结构如下1一个8位算术逻辑单元232个I/O口4组8位端口可单独寻址3两个16位定时计数器4全双工串行通信56个中断源两个中断优先级6128字节内置RAM7独立的64K字节可寻址数据和代码区每个8051处理周期包括12个振荡周期每12个振荡周期用来完成一项操作如取指令和计算指令执行时间可把时钟频率除以12取倒数然后指令执行所须的周期数因此如果你的系统时钟是11.059MHz除以12后就得到了每秒执行的指令个数为921583条指令取倒数将得到每条指令所须的时间1.085ms
2 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 2存储区结构8051结构提供给用户3个不同的存储空间如图A-1每个存储空间包括从0到最大 存储范围的连续的字节地址空间通过利用特定地址的寻址指令解决了地址重叠的问题三个地址空间的功能如图所示 图A-1-8051存储结构2.1CODE区 第一个存储空间是代码段用来存放可执行代码被16位寻址空间可达64K代码段是只读的当要对外接存储器件如EPROM进行寻址时处理器会产生一个信号但这并不意味着代码区一定要用一个EPROM目前一般使用EEPROM作为外接存储器可以被外围器件或8051进行改写这使系统更新更加容易新的软件可以下载到EEPROM中而不用拆开它然后装入一个新的EEPROM另外带电池的SRAMs也可用来代替EPROM他可以像EEPROM一样进行程序的更新并且没有像EEPROM那样读写周期的限制但是当电源耗尽时存储在SRAMs中的程序也随之丢失使用SRAMs来代替EPROM时允许快速下载新程序到目标系统中这避免了编程/调试/擦写这样一个循环过程不再需要使用昂贵的在线仿真器 除了可执行代码还可在代码段中存储查寻表为达此目的8051提供了通过数据指针DPTR或程序计数器加上由累加器提供的偏移量进行寻址的指令这样就可以把表头地址装入DPTR中把表中要寻址的元素的偏移量装入累加器中8051在执行指令时的过程中把这两者相加由此可节省不少指令周期在以后的例子中我们会看到这点
3 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 2.2DATA区第二个存储区是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 或0AFHSFRs控制定时/ 计数器串行口中断源 及中断优先级等这些寄 存器的寻址方式和DATA 取中的其它字节和位一样 可位寻址SFR如表A-1所示 可进行位寻址的SFR 表A-
1 这些寄存器被称
4 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 2.4IDATA区8051系列的一些单片机如8052有附加的128字节的内部RAM位于从80H开始的地址 空间中被称为IDATA因为IDATA区的地址和SFRs的地址是重叠的通过区分所访问的存储区来解决地址重叠问题因为IDATA区只能通过间接寻址来访问2.5XDATA区 8051的最后一个存储空间为64K和CODE区一样采用16位地址寻址称作外部数据区简称XDATA区这个区通常包括一些RAM如SRAM或一些需要通过总线接口的外围器件对XDATA的读写操作需要至少两个处理周期使用DPTRR0或DPTRR1对DPTR来说至少需要两个处理周期来装入地址而读写又需要两个处理周期同样对于R0或R1装入需要一个以上的处理周期而读写又需两个周期由此可见处理XDATA中的数据至少要花3个指令周期因此使用频繁的数据应尽量保存在DATA区中 如果不需要和外部器件进行I/O操作或者希望在和外部器件进行I/O操作时开关RAM则XDATA可全部使用64KRAM关于这方面的应用将在以后介绍
5 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 3位操作和布尔逻辑 8051可分别对BDATA和SFRs中128个可寻址位32个I/O口进行位逻辑操作可对 这些位进行与或异或求补置位清零等操作并可像转移字节那样转移位 列表A-
1 MOVC22H 把位地址22H中的数移入进位位中 ORLC23H 把位地址23H中的数和进位位中的数相或 MOV24HC 把进位位中的数移入位地址24H中 可寻址位也可作为条件转移的条件一条很有用的指令就是JBC通过判断可寻址位是否置位来决定是否进行转移如果该位置位则转移并清零该位这条指令能够在两个处理周期中完成比在两个代码段中分别使用跳转和清零指令要节省一到两个处理周期比如说你要编写一个过程等待P0.0置位然后跳转但是等待有时间限制这样就需要设置一个时间时间到达后跳出查询检测到P0.0置位后跳出并清零P0.0一般的逻辑流程如下 例A-2MOV L2JBDJNZ L1CLRRET timeout#TO_VALUEP0.0L1 timeoutL2P0.0 设置查询时间P0.0置位则跳转查询时间计数P0.0清零退出 当使用JBC时程序如下例A-
3 MOVtimeout#TO_VALUEL2JBCP0.0L1 DJNZtimeoutL2L1RET 利用JBC不但节省了代码长度使用这条指令 设置查询时间P0.0置位则跳转并清零查询时间计数退出 而且使程序更加简洁美观以后在编制代码时要习惯
6 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 4寻址方式 8051可对存储区直接或间接寻址这些是典型的寻址方式直接寻址是在指令中直接 包含所须寻址的字节地址直接寻址只能在DATA区和SFR中进行如下例 列表A-
4 MOVA03H 把地址03H中的数移入累加器 MOV43H22H 把地址22H中的数移入地址43H中 MOV02HC 把C中的数移入位地址02H中 MOV42H#18 把立即数18移入地址42H中 MOV09HSBUF 把串行缓冲区中的数移入地址09H中 间接寻址要使用DPTRPCR0R1寄存器用来存放所要访问数据的地址指令使用 指针寄存器而不是直接使用地址用间接寻址方式可访问CODEIDATAXDATA存储区 对DATA存储区也可进行间接寻址只能用直接寻址方式对位地址进行寻址 在进行块移动时用间接寻址十分方便能用最少的代码完成操作可以利用循环过 程使指针递增对CODE区进行寻址时将基址存入DPTR或PC中把变址存入累加器中 这种方法在查表时十分有用举例如下 例A-
5 DATA和IDATA区寻址 MOVR1#22H 设置R1为指向DATA区内的地址22H的指针 MOVR0#0A9H 设置R0为指向IDATA区内的地址0A9H的指针 MOVA@R1 读入地址22H的数据 MOV@R0A 将累加器中的数据写入地址A9H INCR0 RO中的地址变为AAH INCR1 R1中的地址变为23H MOV34H@R0 将地址AAH中的数据写入34H MOV@R1#67H 把立即数写入地址23H XDATA区寻址MOVDPTR#3048HMOVXA@DPTRINCDPTRMOVA#26HMOVX@DPTRAMOVR0#87HMOVXA@R0 DPTR指向外部存储区读入外部存储区地址3048H中的数指针加一立即数26H写入A中将26H写入外部存储区地址3049H中R0指向外部存储区地址87H将外部存储区地址87H中的数读入累加器中 代码区寻址MOVDPTR#TABLE_BASEMOVAindexMOVCA@A+DPTR DPTR指向表首地址把偏移量装入累加器中从表中读入数据到累加器中
7 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 5处理器状态处理器的状态保存在状态寄存器PSW中状态字中包括进位位用于BCD码处理的辅 助进位位奇偶标志位溢出标志位还有前面提到的用于寄存器组选择的RS0和RS10组从地址00H开始1组从地址08H开始2组从地址10H开始3组从地址18H开始这些地址都可通过直接或间接方式进行寻址PSW的结构如下 CY AC F0 RS1RS0OVUSRP CY进位标志位 AC辅助进位标志位 F0通用标志位 RS1寄存器组选择位高位 RS0寄存器组选择位低位 OV溢出标志位 USR用户定义标志位
P 奇偶标志位 6电源控制 8051的CHMOS版本可通过软件设置两种节电方式空闲模式和低功耗模式设置电源 控制寄存器PCON的相应位来进入节电方式置位IDLE进入空闲模式空闲模式将停止程 序执行RAM中的数据仍然保持晶振继续工作但与CPU断开定时器和串行口继续工 作发生中断将退出中断模式执行完中断程序后将从程序停止的地方继续指令的执行 通过置位PDWN位来进入低功耗模式低功耗模式中晶振将停止工作因此定时器和 串行口都将停止工作至少有两伏的电压加在芯片上因此RAM中的数据仍将保存退 出低功耗模式只有两种方式上电或复位 SMOD位可控制串行通信的波特率将使由定时器1的溢出率或晶振频率产生的波特率 翻倍置位SMOD可使工作于方式123定时器产生的波特率翻倍当使用定时器2产生 波特率时SMOD将不影响波特率 电源控制寄存器不可位寻址 SMOD- - - GF1GF0PDWNIDLE SMOD串行口通信波特率控制位置位使波特率翻倍 - 保留 - 保留 - 保留 GF1 通用标志位 GF0 通用标志位 PDWN低功耗标志位置位进入低功耗模式 IDLE空闲标志位置位进入空闲模式 表A-
3 6中断系统基本的8051支持6个中断源两个外部中断两个定时/计数器中断一个串行口输 入/输出中断中断发生后处理器转到将五个中断入口处之一执行中断处理程序中断向量位于代码段的最低地址出串行口输入输出中断共用一个中断向量中断服务程序必须在中断入口处或通过跳转分支转移到别处8051/8052的中断向量表A-
4 8 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 8051支持两个中断优先级有标准的中断机制低优先级的中断只能被高优先级的 中断源上电复位 中断向量0000H 中断所中断而高优先级的中断不能被中断 6.1中断优先级寄存器每个中断源都可通过设置中断优先级寄存 器IP来单独设置中断优先级如果每个中断源的相应位被置位则该中断源的优先级为高 外部中断0定时器0溢出外部中断1定时器1溢出串行口中断定时器2溢出 0003H000BH0013H001BH0023H002BH 如果相应的位被复位则该中断源的优先级为低如果你觉得两个中断源不够用别急 以后我会教你如何增加中断优先级表A-5示出了IP寄存器的各位此寄存器可位寻址 IP寄存器可位寻址 - - PT2PS PT1PX1PT0PX0 -保留 -保留 PT2定时器2中断优先级 PS串行通信中断优先级 PT1定时器1中断优先级 PX1外部中断1优先级 PT0定时器0中断优先级 PX0外部中断0优先级 表A-
5 6.2中断使能寄存器 通过设置中断使能寄存器IE的EA位使能所有中断每个中断源都有单独的使能位 可通过软件设置IE中相应的使能位在任何时候使能或禁能中断中断使能寄存器IE的各 位如下所示 中断使能寄存器IE可位寻址 EA-ET2 ESET1EX1ET0EX0 EA 使能标志位置位则所有中断使能复位则禁止所有中断 - 保留 ET2 定时器2中断使能 ES 串行通信中断使能 ET1 定时器1中断使能 EX1 外部中断1使能 ET0 定时器0中断使能 EX0 外部中断0使能 6.3中断延迟8051在每个处理周期查询中断标志确定是否有中断请求当发生中断时置位相应 的标志处理器将在下个周期查询到中断标志位这样从发生中断到确认中断之间有一个指令周期的延时这时处理器将用两个周期的时间来调用中断服务程序总共要花3个时钟周期在理想情况下处理器将在3个指令周期内响应中断这使得用户能很快响应系统事件 不可避免地系统有可能在3个处理周期能不能响应中断请求特别是当有同级或更
9 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 高级的中断服务程序正在执行的时候因此中断的延迟主要取决于正在执行的程序另外一种大于3个周期的中断延迟是程序正在执行一条多周期指令要等到当前的 指令执行完后处理器才会处理中断事件这将在原来的基础上至少增加一个周期的延时假设在执行完多周期指令的第一个周期后发现中断除被其它中断所阻的情况中断不 被响应的最长延时为6个处理周期3个周期的多周期指令执行时间3个周期的指令响应时间
4 最后一种大于3个指令周期的中断延迟是当检测到中断时正在执行写IPIE或RETI指令 6.4外部中断信号8051支持两个外部中断信号这使外部器件能请求中断从而得到相应的服务外部 中断由外部中断引脚外部中断0为P3.2外部中断1为P3.3电平为低或电平由高到低跳变引起由电平触发还是跳变触发取决于寄存器TCON的ITX位见A-
7 电平触发时当检测到中断引脚电平为低时将产生中断低电平应至少保持一个指令周期或12个时钟周期因为处理器每个指令周期检测一次引脚跳变触发时当在连续的两个周期中检测到由高到低的电平跳变时将产生中断而电平的0状态应至少保持一个周期 7内置定时/计数器标准的8051有两个定时/计数器每个定时器有16位定时/计数器既可用来作为定 时器对机器周期计数也可用来对相应I/0口TOT1上从高到低的跳变脉冲计数当用作计数器时脉冲频率不应高于指令的执行频率的1/2因为每周期检测一次引脚电平而判断一次脉冲跳变需要两个指令周期如果需要的话当脉冲计数溢出时可以产生一个中断 TCON特殊功能寄存器timercontroller用来控制定时器的工作起停和溢出标志位通过改变定时器运行位TR0和TR1来启动和停止定时器的工作TCON中还包括了定时器T0和T1的溢出中断标志位当定时器溢出时相应的标志位被置位当程序检测到标志位从0到1的跳变时如果中断是使能的将产生一个中断注意中断标志位可在任何时候置位和清除因此可通过软件产生和阻止定时器中断 定时器控制寄存器TCON可位寻址 TF1TR1TF0TR0IE1IT1IE0IT0 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的高
10 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 位控制定时器1低四位控制定时器0TMOD的结构如下 定时器控制寄存器TMOD-不可位寻址 GATEC/TM1M0GATEC/TM1M0 定时器
1 定时器
0 GATE 当GATE置位时定时器仅当TR=1并且INT=1时才工作 置位TR定时器就开始工作 C/T 定时器方式选择如果C/T=1定时器以计数方式工作 定时方式工作 M1 模式选择位高位 M0 模式选择位低位 表A-
8 如果GATE=0C/T=0时以 可通过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和TL0TH0对应于TMOD中定 时器0的控制位而TL0占据了TMOD中定时器1的控制位这样定时器1将不能产生溢出中断了但可用于其它不需产生中断的场合如作为波特率发生器或作为定时计数器被软件查询当系统需要用定时器1来产生波特率而又同时需要两个定时/计数器时这种工作方式十分有用当定时器1设置为工作方式3时将停止工作 7.4定时器
2 51系列单片机如8052第三个定时/计数器定时器2他的控制位在特殊功能寄存器 T2CON中结构如下 定时器2控制寄存器可位寻址 TF2EXF2 RCLKTCLKEXEN2 TR2 C/T2CP/RL2 TF2 定时器2溢出标志位定时器2溢出时将置位当TCLK或RCLK为1时 将不会置位 EXF2 定时器2外部标志当EXEN2=1并在引脚T2EX检测到负跳变时置位 如果定时器2中断被允许将产生中断 11 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 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=1T2EX引脚的负跳变 将造成捕捉如果CP/RL2=0T2EX引脚的负跳变将造成重装 通过由软件设置T2CON可使定时/计数器以三种基本工作方式之一工作第一种为捕 捉方式设置为捕捉方式时和定时器0或定时器1一样以16位方式工作这种方式通过 复位EXEN2来选择当置位EXEN2时如果T2EX有负跳变电平将把当前的数锁存在RCAP2H 和RCAP2L中这个事件可用来产生中断 第二种工作方式为自动重装方式其中包含了两个子功能由EXEN2来选择当EXEN2 复位时16位定时器溢出将触发一个中断并将RCAP2H和RCAP2L中的数装入定时器中当 EXEN2置位时除上述功能外T2EX引脚的负跳变将产生一次重装操作 最后一种方式用来产生串行口通讯所需的波特率这通过同时或分别置位RCLK和TCLK 来实现在这种方式中每个机器周期都将使定时器加1而不像定时器0和1那样需 要12个机器周期这使得串行通讯的波特率更高 8内置UART8051有一个可通过软件控制的内置全双工串行通讯接口由寄存器SCON来进行设 置可选择通讯模式允许接收检查状态位SCON的结构如下串行控制寄存器SCON-可位寻址 SM0SM1SM2RENTB8RB8TIRI SM0串行模式选择SM1串行模式选择SM2多机通讯允许位当模式0时此位应该为0模式1时当接收到停止位时 该位将置位模式2或模式3时当接收的第9位数据为1时将置位REN串行接收允许位TB8在模式2和模式3中将被发送数据的第9位RB8在模式0中该位不起作用在模式1中该位为接收数据的停止位在模 式2和模式3中为接收数据的第9位TI串行中断标志位由软件清零RI接收中断标志位有软件清零 表A-10UART有一个接收数据缓冲区当上一个字节还没被处理下一个数据仍然可以缓冲区接收进来但如果接收完这个字节如果上个字节还没被处理上个字节将被覆盖因此软件必须在此之前处理数据当连续发送字节时也是如此8051支持10位和11位数据模式11数据模式用来进行多机通讯并支持高速8位移位寄存器模式模式1和模式3中波特率可变 12 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 8.1UART模式0模式0时UART作为一个8位的移位寄存器使用波特率为fosc/12数据由RXD从 低位开始收发TXD用来发送同步移位脉冲因此方式0不支持全双工这种方式可用来和像某些具有8位串行口的EEPROM之类的器件通讯 当向SBUF写入字节时开始发送数据数据发送完毕时TI位将置位置位REN时将开始接收数据接收完8位数据时RI位将置位 8.2UART模式1工作于模式1时传输的是10位1个起始位8个数据位1个停止位这种方式 可和包括PC机在内的很多器件进行通讯这种方式中波特率是可调的而用来产生波特率的定时器的中断应该被禁止PCON的SMOD位为1时可使波特率翻倍 TI和RI在发送和接收停止位的中间时刻被置位这使软件可以响应中断并装入新的数据数据处理时间取决于波特率和晶振频率 如果用定时器1来产生波特率应通过下式来计算TH1的装入值TH1=256-K*OscFreq/384*BaudRateK=1ifSMOD=0K=2ifSMOD=1重装值要小于256非整数的重装值必须和下一个整数非常接近通常产生的波特率都能使系统正常的工作这点需要开发者把握这样如果你使用9.216M晶振想产生9600的波特率第一步设K=1分子为9216000分母为3686400相除结果为2.5不是整数设K=2分子为18432000分母为3686400相除结果为5可得TH1=251或0FBH如果用8052的定时器2产生波特率RCAP2H和RCAP2L的重装值也需要经过计算根据需要的波特率用下式计算[RCAP2HRCAP2L]=65536-OsFreq/32*BaudRate假设你的系统使用9.216M晶振你想产生9600的波特率用上式产生的结果必须是正的而且接近整数最后得到结果30重装值为65506或FFE2H 8.3UART模式2模式2的数据以11位方式发送1位起始位8位数据位第九位1位停止位发 送数据时第九位为SCON中的TB8接收数据的第九位保存在RB8中第九位一般用来多机通信仅在第九位为1时单片机才接收数据多机通信用SCON的SM2来控制当SM2置位时仅当数据的第九位为1时才引发通讯中断当SM2为0时只要接收完11位就产生一次中断 第九位可在多机通讯中避免不必要的中断在传送地址和命令时第九位置位串行总线上的所有处理器都产生一个中断处理器将决定是否继续接收下面的数据如果继续接收数据就清零SM2否则SM2置位以后的数据流将不会使他产生中断 SMOD=O时模式2的波特率为1/64OscSMOD=1时波特率为1/32Osc因此使用模式2当晶振频率为11.059M时将有高达345K的波特率模式3和模式2的差别在于可变的波特率 9其它功能很多51系列的单片机有了许多新增加的功能 其它功能如下 使之更适合于嵌入式应用 51系列的 13 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 9.1I2CI2C是一种新的芯片间的通讯方式由PHILIPS开发和推广I2C通讯采用两条线进行 通讯一条数据线一条时钟线可进行多器件通讯总线上的每个器件都有自己的地址数据传送是双向的总线支持多主机8051上I2C总线的接口为P0端口的两根线有专门的特殊功能寄存器来控制总线的工作和执行传输协议 9.2A/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空间这时就要用外围器件SRAMEPROM等许多外围器件能被51系列的内部功能和相应的软件代替这将在以后讨论 经常要考虑系统的功耗问题如果处理器有很多工作要做而不能进入低功耗和空闲模式应选择3.6V的工作电压以降低功耗如果有足够的空闲时间的话可以考虑关闭晶振降低功耗 设计者必须仔细选择晶振频率确保标准的通讯波特率12004800960019.2K等你不妨先列出可供选择的晶振所能产生的波特率然后根据需要的波特率和系统要求选择晶振有时也不必过分考虑晶振问题因为可以定制晶振当晶振频率超过20M时必须确保总线上的其它器件能够在这种频率下工作一般EPROMSRAM高速CMOS版的锁存器都支持51的工作频率当工作频率增加时功耗也会增加这点在使用电池作为电源的系统中应充分考虑 11实现当选择好单片机和外围器件后下一步就是设计和分配系统I/O地址代码段在从地 址零开始的连续空间内外部数据存储空间地址一般和RAM和器件地址相连RAM一般在从地址0000H或8000H开始的连续空间内一种比较有用的处理方法是SRAM的地址也从0000H开始用A15使能RAMRAM的0E和WE线分别和单片机的RD和WR线相连这种方法可使RAM区超过32K这足够嵌入式系统使用此外32K的地址也可分配给I/O器件大多数情况下I/O器件是比较少的所以地址线的高位可接解码器工作给外围器件提 14 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 供使能信号一个为系统I/O分配地址的例子如A-2-8051总线I/O所示可以看到通过减少地址解码器的数量简化了硬件设计因为在I/O操作中不用装载DPTR的低8位使软件设计也得到简化 图A-2-8051总线I/O 对输入输出锁存器的寻址如下例 列表A-
6 MOVDPTR#09000H 设置指针 MOVXA@DPTR MOVDPH#080H MOVX@DPTRA 可以看到因为电路设计连续的I/O操作将被简化 字节第一条指令也可用MOVDPH#090H代替 软件不需要考虑数据指针的低 12结论我希望上面所讲的关于8051的基本知识能给你一些启发但这不能代替8051厂商提 供的数据书因为每款芯片都有其自身的特点下面我们将开始讨论8051的软件设计包括用C进行软件开发 15 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 第二章用C对8051编程1为什么要用高级语言 当设计一个小的嵌入式系统时一般我们都用汇编语言在很多工程中这是一个很好的方法因为代码一般都不超过8K而且都比较简单如果硬件工程师要同时设计软件和硬件经常会采用汇编语言来做程序我的经验告述我硬件工程师一般不熟系像C一类的高级语言 使用汇编的麻烦在于它的可读性和可维护性特别当程序没有很好的标注的时候代码的可重用性也比较低如果使用C的话可以很好的解决这些问题 用C编写的程序因为C语言很好的结构性和模块化更容易阅读和维护而且由于模块化用C语言编写的程序有很好的可移植性功能化的代码能够很方便的从一个工程移植到另一个工程从而减少了开发时间 用C编写程序比汇编更符合人们的思考习惯开发者可以更专心的考虑算法而不是考虑一些细节问题这样就减少了开发和调试的时间 使用像C这样的语言程序员不必十分熟系处理器的运算过程这意味着对新的处理器也能很快上手不必知道处理器的具体内部结构使得用C编写的程序比汇编程序有更好的可移植性很多处理器支持C编译器 所有这些并不说明汇编语言就没了立足之地很多系统特别是实时时钟系统都是用C和汇编语言联合编程对时钟要求很严格时使用汇编语言成了唯一的方法除此之外根据我的经验包括硬件接口的操作都应该用C来编程C的特点就是可以使你尽量少地对硬件进行操作是一种功能性和结构性很强的语言 2C语言的一些要点这里不是教你如何使用C语言关于C语言的书有很多像Kernighan和Ritchie所 著的C编程语言等这本书被认为是C语言的权威著作Keil的C51完全支持C的标准指令和很多用来优化8051指令结构的C的扩展指令 我们将复习关于C的一些概念如结构联合和类型定义可能会使一些人伤脑筋 2.1结构结构是一种定义类型它允许程序员把一系列变量集中到一个单元中当某些变量相 关的时候使用这种类型是很方便的例如你用一系列变量来描述一天的时间你需要定义时分秒三个变量 unsighedcharhour,min,sec;还要定义一个天的变量unsighedintdays;通过使用结构你可以把这四个变量定义在一起给他们一个共同的名字声明结构的语法如下structtime_str{ unsignedcharhour,min,sec;unsignedintdays;}time_of_day;这告述编译器定义一个类型名为time_str的结构并定义一个名为time_of_day的结构变量变量成员的引用为结构变量名.结构成员time_of_day.hour=XBYTE[HOURS];time_of_day.days=XBYTE[DAYS]; 16 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 time_of_day.min=time_of_day.sec curdays=time_of_day.days; 成员变量和其它变量是一样的但前面必须有结构名你可以定义很多结构变量编 译器把他们看成新的变量例如 structtime_stroldtime,newtime; 这样就产生了两个新的结构变量这些变量都是相互独立的就像定义了很多int类 型的变量一样结构变量可以很容易的复制 oldtime=time_of_day; 这使代码很容易阅读也减少了打字的工作量当然你也可以一句一句的复制 oldtime.hour=newtime.hour; oldtime.days=newtime.days-1; 在KeilC和大多数C编译器中结构被提供了连续的存储空间成员名被用来对结构 内部进行寻址这样结构time_str被提供了连续5个字节的空间空间内的变量顺序和定义时 Offset0 MemberBytes hour
1 的变量顺序一样如表0-1:
1 min
1 如果你定义了一个结构类型它就像一个变量
2 sec
1 3 days
2 新的变量类型你可建立一个结构数组包含结构 表0-
1 的结构和指向结构的指针 2.2联合 联合和结构很相似它由相关的变量组成这些变量构成了联合的成员但是这些成 员只能有一个起作用联合的成员变量可以是任何有效类型包括C语言本身拥有的类型 和用户定义的类型如结构和联合一个定义联合的类型如下 uniontime_type{ unsignedlongsecs_in_year; structtime_strtime; }mytime; 用一个长整形来存放从这年开始到现在的秒数另一个可选项是用time_str结构来存 储从这年开始到现在的时间 不管联合包含什么可在任何时候引用他的成员如下例 mytime.secs_in_year=JUNEIST; mytime.time.hour=5; curdays=mytime.time.days; 像结构一样联合也以连续的空间存储空间大小等于联合中最大的成员所需的空间 Offset Member Bytes
0 Secs_in_year4
0 Mytime
5 表0-
2 因为最大的成员需要5个字节联合的存储大小为5个字节当联合的成员为 secs_in_year时第5个字节没有使用 联合经常被用来提供同一个数据的不同的表达方式例如假设你有一个长整型变量 用来存放四个寄存器的值如果希望对这些数据有两种表达方法可以在联合中定义一个 长整型变量同时再定义一个字节数组如下例 unionstatus_type{ unsignedcharstatus[4]; 17 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 unsignedlongstatus_val;}io_status;io_status.status_val=0x12345678;if(i0_status.status[2]&0x10){ …} 2.3指针 指针是一个包含存储区地址的变量因为指针中包含了变量的地址它可以对它所指 向的变量进行寻址就像在8051DATA区中进行寄存器间接寻址和在XDATA区中用DPTR 进行寻址一样使用指针是非常方便的因为它很容易从一个变量移到下一个变量所以 可以写出对大量变量进行操作的通用程序 指针要定义类型说明指向何种类型的变量假设你用关键字long定义一个指针
C 就把指针所指的地址看成一个长整型变量的基址这并不说明这个指针被强迫指向长整型 的变量而是说明C把该指针所指的变量看成长整型的下面是一些指针定义的例子 unsignedchar*my_ptr,*anther_ptr; unsignedint*int_ptr; float*float_ptr; time_str*time_ptr; 指针可被赋予任何已经定义的变量或存储器的地址 My_ptr=&char_val; Int_ptr=&int_array[10]; Time_str=&oldtime; 可通过加减来移动指针
指向不同的存储区地址在处理数组的时候这一点特别有 用当指针加1的时候它加上指针所指数据类型的长度 Time_ptr=(timestr*)(0x10000L);//指向地址
0 Time_ptr++; //指向地址
5 指针间可像其它变量那样互相赋值指针所指向的数据也可通过引用指针来赋值 time_ptr=oldtime_ptr //两个指针指向同一地址 *int_ptr=0x4500 //把0X4500赋给int_ptr所指的变量 当用指针来引用结构或联合的成员时可用如下方法 time_ptr->days=234; *time_ptr.hour=12; 还有一个指针用得比较多的场合是链表和树结构假设你想产生一个数据结构可以 进行插入和查询操作一种最简单的方法就是建立一个双向查询树你可以像下面那样定 义树的节点 structbst_node{ unsignedcharname[20]; //存储姓名 structbst_node*left,right;//分别指向左右子树的指针 }; 可通过定位新的变量并把他的地址赋给查询树的左指针或右指针来使双向查询树变 长或缩短有了指针后对树的处理变得简单 18 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 2.4类型定义在C中进行类型定义就是对给定的类型一个新的类型名换句话说就是给类型一个新 的名字例如你想给结构time_str一个新的名字typedefstructtime_str{unsignedcharhour,min,sec;unsignedintdays;}time_type;这样就可以像使用其它变量那样使用time_type的类型变量time_typetime,*time_ptr,time_array[10];类型定义也可用来重新命名C的标准类型typedefunsignedcharUBYTE;typedefchar*strptr;strptrname;使用类型定义可使你的代码的可读性加强节省了一些打字的时间但是很多程序员 大量的使用类型定义别人再看你的程序时就十分困难了 3KeilC和ANSIC下面将介绍KeilC的主要特点和它与ANSIC的不同之处并给你一些对8051使用
C 的启发Keil编译器除了少数一些关键地方外基本类似于ANSIC差异主要是Keil可以让 户针对8051的结构进行程序设计其它差异主要是8051的一些局限引起的 3.1数据类型 KeilC有ANSIC的所有标准数据类型除此之外为了更加有利的利用8051的结构 还加入了一些特殊的数据类型下表显示了标准数据类型在8051中占据的字节数注意 整型和长整型的符号位字节在最低的地址中除了这些标准数据类型外编译器还支持 数据类型char/unsignedchar 大小8bit 一种位数据类型一个位变量存在于内部RAM的可位寻址区中可像操作其它变量那样对位变量进行操作而位数组和位指针是违法的 int/unsignedcharlong/unsignedlongfloat/doublegenericpointer 16bit32bit32bit24bit 3.2特殊功能寄存器 表0-
3 特殊功能寄存器用sfr来定义而sfr16用来定义16位的特殊功能寄存器如DPTR 通过名字或地址来引用特殊功能寄存器地址必须高于80H可位寻址的特殊功能寄存器 的位变量定义用关键字sbitSFR的定义如列表0-1所示对于大多数8051成员Keil 提供了一个包含了所有特殊功能寄存器和他们的位的定义的头文件通过包含头文件可以 很容易的进行新的扩展 列表0-
1 sfrSCON=0X98; //定义SCON sbitSM0=0X9F; //定义SCON的各位 sbitSM1=0X9E; sbitSM2=0X9D; sbitREN=0x9C; sbitTB8=0X9B; 19 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 sbitRB8=0X9A;sbitTI=0X99;sbitRI=0X98; 4存储类型 Keil允许使用者指定程序变量的存储区这使使用者可以控制存储区的使用 可识别以下存储区 存储区 描述 DATA RAM的低128个字节可在一个周期内直接寻址 BDATA DATA区的16个字节的可位寻址区 IDATA RAM区的高128个字节必须采用间接寻址 PDATA 外部存储区的256个字节通过P0口的地址对其寻址 使用指令MOVX@Rn,需要两个指令周期 XDATA 外部存储区使用DPTR寻址 CODE 程序存储区使用DPTR寻址 编译器 4.1DATA区对DATA区的寻址是最快的所以应该把使用频率高的变量放在DATA区由于空间有 限必须注意使用DATA区除了包含程序变量外还包含了堆栈和寄存器组DATA区的声明如列表0-2列表0-2 unsignedchardatasystem_status=0;unsignedintdataunit_id[2];chardatainp_string[16];floatdataoutp_value;mytypedatanew_var;标准变量和用户自定义变量都可存储在DATA区中只要不超过DATA区的范围因为C51使用默认的寄存器组来传递参数你至少失去了8个字节另外要定义足够大的堆栈空间当你的内部堆栈溢出的时候你的程序会莫名其妙的复位实际原因是8051系列微处理器没有硬件报错机制堆栈溢出只能以这种方式表示出来 4.2BDATA区你可以在DATA区的位寻址区定义变量这个变量就可进行位寻址并且声明位变量 这对状态寄存器来说是十分有用的因为它需要单独的使用变量的每一位不一定要用位变量名来引用位变量下面是一些在BDATA段中声明变量和使用位变量的例子列表0-3 unsignedcharbdatastatus_byte;unsignedintbdatastatus_word;unsignedlongbdatastatus_dword;sbitstat_flag=status_byte^4;if(status_word^15){ …}stat_flag=1;编译器不允许在BDATA段中定义float和double类型的变量如果你想对浮点数的每 20 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 位寻址可以通过包含float和long的联合来实现 列表0-
4 typedefunion{ //定义联合类型 unsignedlonglvalue; //长整型32位 floatfvalue; //浮点数32位 }bit_float; //联合名 bit_floatbdatamyfloat; //在BDATA段中声名联合 sbitfloat_ld=myfloat^31 //定义位变量名 下面的代码访问状态寄存器的特定位把访问定义在DATA段中的一个字节和通过位名 和位号访问同样的可位寻址字节的位的代码对比注意对变量位进行寻址产生的汇编代 码比检测定义在DATA段的状态字节位所产生的汇编代码要好如果你对定义在BDATA段中 的状态字节中的位采用偏移量进行寻址而不是用先前定义的位变量名时编译后的代码 是错误的下面的例子中use_bitnum_status的汇编代码比use_byte_status的代码要 大 列表0-
5 1 //定义一个字节宽状态寄存器
2 unsignedchardatabyte_status=0x43;
3 4 //定义一个可位寻址状态寄存器
5 unsignedcharbdatabit_status=0x43;
6 //把bit_status的第3位设为位变量
7 sbitstatus_3=bit_status^3;
8 9 bituse_bit_status(void); 10 11 bituse_bitnum_status(void); 12 13 bituse_byte_status(void); 14 15 voidmain(void){ 16 unsignedchartemp=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 bituse_bit_status(void){ 29 return(bit)(status_3); 21 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 30 } 31 32 bituse_bitnum_status(void){ 33 return(bit)(bit_status^3); 34 } 35 36 bituse_byte_status(void){ 37 returnbyte_status&0x04; 38 } 目标代码列表 ;FUNCTIONmain(BEGIN) ;SOURCELINE#15 ;SOURCELINE#16 0000E4 CLRA 0001F500R MOVtemp,
A ;SOURCELINE#17 0003120000R LCALLuse_bit_status 00065002 JNC?
C0001 ;SOURCELINE#18 00080500R INCtemp ;SOURCELINE#19 000A ?
C0001: ;SOURCELINE#20 000A120000R LCALLuse_byte_status 000D5002 JNC?
C0002 ;SOURCELINE#21 000F0500R INCtemp ;SOURCELINE#22 0011 ?
C0002: ;SOURCELINE#23 0011120000R LCALLuse_bitnum_status 00145002 JNC?
C0004 ;SOURCELINE#24 00160500R INCtemp ;SOURCELINE#25 ;SOURCELINE#26 0018 ?
C0004: 001822 RET ;FUNCTIONmain(END) ;FUNCTIONuse_bit_status(BEGIN) ;SOURCELINE#28 ;SOURCELINE#29 0000A200R MOVC,status_
3 ;SOURCELINE#30 22 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 0002 ?
C0005: 000222 RET ;FUNCTIONuse_bit_status(END) ;FUNCTIONuse_bitnum_status(BEGIN) pilerobtainsthedesiredbitbyusingtheentirebyteinsteadofusing abitaddress. ;SOURCELINE#32 ;SOURCELINE#33 0000E500R MOVA,bit_status 00026403 XRLA,#03H 000424FF ADDA,#0FFH ;SOURCELINE#34 0006 ?
C0006: 000622 RET ;FUNCTIONuse_bitnum_status(END) ;FUNCTIONuse_byte_status(BEGIN) ;SOURCELINE#36 ;SOURCELINE#37 0000E500R MOVA,byte_status 0002A2E2 MOVC,ACC.2 ;SOURCELINE#38 0004 ?
C0007: 000422 RET ;FUNCTIONuse_byte_status(END) 记住在处理位变量时要使用声明的位变量名而不要使用偏移量 4.3IDATA段IDATA段也可存放使用比较频繁的变量使用寄存器作为指针进行寻址在寄存器中 设置8位地址进行间接寻址和外部存储器寻址比较它的指令执行周期和代码长度都比较短 unsignedcharidatasystem_status=0;unsignedintidataunit_id[2];charidatainp_string[16];floatidataoutp_value; 4.4PDATA和XDATA段在这两个段声明变量和在其它段的语法是一样的PDATA段只有256个字节而XDATA 段可达65536个字节下面是一些例子unsignedcharxdatasystem_status=0;unsignedintpdataunit_id[2];charxdatainp_string[16];floatpdataoutp_value;对PDATA和XDATA的操作是相似的对PDATA段寻址比对XDATA段寻址要快因为 对PDATA段寻址只需要装入8位地址而对XDATA段寻址需装入16位地址所以尽量把外 23 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 部数据存储在PDATA段中对PDATA和XDATA寻址要使用MOVX指令 列表0-
6 1#include
2 3unisgnedcharpdatainp_reg1;
4 5unsignedcharxdatainp_reg2;
6 7voidmain(void){
8 inp_reg1=P1;
9 inp_reg2=P3; 10} 产生的目标代码列表 ;FUNCTIONmain(BEGIN) ;SOURCELINE#
7 ;SOURCELINE#
8 需要两个处理周期 注意'inp_reg1=P1'需要4个指令周期 00007800RMOV R0,#inp_reg1 0002E590 MOV
A,P1 0004F2 MOVX@R0,
A ;SOURCELINE#
9 注意'inp_reg2=P3'需要5个指令周期 0005900000RMOV DPTR,#inp_reg2 0008E5B0 MOV
A,P3 000AF0 MOVX@DPTR,
A ;SOURCELINE#10 000B22 RET ;FUNCTIONmain(END) 经常外部地址段中除了包含存储器地址外还包含I/O器件的地址对外部器件寻址 可通过指针或C51提供的宏我建议使用宏对外部器件进行寻址因为这样更有可读性 宏定义使得存储段看上去像char和int类型的数组下面是一些绝对寄存器寻址的例子 列表0-
7 inp_byte=XBYTE[0x8500];//从地址8500H读一个字节 inp_word=XWORD[0x4000];//从地址4000H读一个字和2001H c=*((charxdata*)0x0000);//从地址0000读一个字节 XBYTE[0x7500]=out_val; //写一个字节到7500H 可对除BDATA和BIT段之外的其它数据段采用以上方法寻址通过包含头文件.h 来进行绝对地址访问 4.5CODE段代码段的数据是不可改变的8051的代码段不可重写一般代码段中可存放数据表 24 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 跳转向量和状态表对CODE段的访问和对XDATA段的访问的时间是一样的代码段中的对象在编译的时候初始化否则你就得不到你想要的值下面是代码段的声明例子 unsignedintcodeunit_id[2]=1234;unsignedchar 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x10,0x11,0x12,0x13,0x14,0x15}; 5指针 C51提供一个3字节的通用存储器指针通用指针的头一个字节表明指针所指的存储 区空间另外两个字节存储16位偏移量对于DATAIDATA和PDATA段只需要8位偏移量 Keil允许使用者规定指针指向的存储段这种指针叫具体指针使用具体指针的好处是节省了存储空间编译器不用为存储器选择和决定正确的存储器操作指令产生代码这样就使代码更加简短但你必须保证指针不指向你所声明的存储区以外的地方否则会产生错误 指针类型 大小 通用指针 3字节 XDATA指针 2字节 CODE指针 2字节 IDATA指针 1字节 DATA指针 1字节 PDATA指针 1字节 表0-
5 而且很难调试 下面的例子反映出使用具体指针比使用通用指针更加高效使用通用指针的第一个循 环需要378个处理周期使用具体指针只需要151个处理周期 列表0-
8 1 #include<.h>
2 3 char*generic_ptr;
4 5 chardata*xd_ptr;
6 7 charmystring[]="Testoutput";
8 9 main(){ 101 generic_ptr=mystring; 111 while(*generic_ptr){ 122 XBYTE[0x0000]=*generic_ptr; 132 generic_ptr++; 142 } 151 161 xd_ptr=mystring; 171 while(*xd_ptr){ 182 XBYTE[0x0000]=*xd_ptr; 192 xd_ptr++; 202 } 211} 编译产生的汇编代码 25 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 0000750004000375000000067500000009 0009000B000D000F00120013 AB00AA00A900120000FF6011 00159000000018F0 00197401001B2500001DF500001FE4002035000022F500 002480E30026 00267500000029 0029A800002BE6002CFF002D6008 002F9000000032F0 00330500 003580F2 0037 ;FUNCTIONmain(BEGIN);SOURCELINE#9;SOURCELINE#10 RMOVgeneric_ptr,#04HRMOVgeneric_ptr+01H,#HIGHmystringRMOVgeneric_ptr+02H,#LOWmystring?
C0001: ;SOURCELINE#11RMOVR3,generic_ptrRMOVR2,generic_ptr+01HRMOVR1,generic_ptr+02HELCALL?
C_CLDPTR MOVR7,AJZ?
C0002 ;SOURCELINE#12MOVDPTR,#00HMOVX@DPTR,
A ;SOURCELINE#13MOVA,#01HRADDA,generic_ptr+02HRMOVgeneric_ptr+02H,ACLRARADDCA,generic_ptr+01HRMOVgeneric_ptr+01H,
A ;SOURCELINE#14SJMP?
C0001?
C0002: ;SOURCELINE#16RMOVxd_ptr,#LOWmystring?
C0003: ;SOURCELINE#17RMOVR0,xd_ptr MOVA,@R0MOVR7,AJZ?
C0005 ;SOURCELINE#18MOVDPTR,#00HMOVX@DPTR,
A ;SOURCELINE#19RINCxd_ptr ;SOURCELINE#20SJMP?
C0003 ;SOURCELINE#21?
C0005: 26 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 003722 RET ;FUNCTIONmain(END) 由于使用具体指针能够节省不少时间所以我们一般都不使用通用指针 6中断服务 8051的中断系统十分重要,C51使你能够用C来声明中断和编写中断服务程序(当然你 也可以用汇编来写)中断过程通过使用interrupt关键字和中断号(0到31)来实现.中断 号告述编译器中断程序的入口地址中断号对应着IE寄存器中的使能位换句话说IE 寄存器中的0位对应着外部中断0相应的外部中断0的中断号是0表0-6反映了这种关 系一个中断过程并不一定带上所有参数可 以没有返回值有了这些限制编译器不须要担心寄存器组参数的使用和对累加器状态寄存器B寄存器数据指针和默认的寄存器的保护只要他们在中断程序中被用到编译的时候会把他们入栈在中断程序结束时将他们恢复中断程序的入口地址被编译器放在中断向量中C51支持所有5个8051/8052标准中 IE寄存器中的使能位和C中的中断号 012345表0-
6 中断源 外部中断0定时器0溢出外部中断1定时器1溢出串行口中断定时器2溢出 断从0到4和在8051系列中多达27个中断源一个中断服务程序的例子如下 列表0-
9 1#include 2#include
3 4#defineRELOADVALH0x3C 5#defineRELOADVALL0xB0
6 7externunsignedinttick_count;
8 9voidtimer0(void)interrupt1{ 101TR0=0; //停止定时器
0 111TH0=RELOADVALH; //50ms后溢出 121TL0=RELOADVALL; 131TR0=1; //启动T0 141tick_count++; //时间计数器加
1 151printf("tick_count=%05u\n",tick_count); 161} 编译后产生的汇编代码 ;FUNCTIONtimer0(BEGIN)0000C0E0PUSHACC0002C0F0PUSHB0004C083PUSHDPH0006C082PUSHDPL 27 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 0008C0D0PUSHPSW 000AC000PUSHAR0 000CC001PUSHAR1 000EC002PUSHAR2 0010C003PUSHAR3 0012C004PUSHAR4 0014C005PUSHAR5 0016C006PUSHAR6 0018C007PUSHAR7 ;SOURCELINE#
9 ;SOURCELINE#10 001AC28CCLRTR0 ;SOURCELINE#11 001C758C3CMOVTH0,#03CH ;SOURCELINE#12 001F758AB0MOVTL0,#0B0H ;SOURCELINE#13 0022D28CSETBTR0 ;SOURCELINE#14 0024900000EMOVDPTR,#tick_count+01H 0027E0 MOVXA,@DPTR 002804 INCA 0029F0 MOVX@DPTR,
A 002A7006JNZ?
C0002 002C900000EMOVDPTR,#tick_count 002FE0 MOVXA,@DPTR 003004 INCA 0031F0 MOVX@DPTR,
A 0032?
C0002: ;SOURCELINE#15 00327B05MOVR3,#05H 00347A00RMOVR2,#HIGH?
SC_
0 00367900RMOVR1,#LOW?
SC_
0 0038900000EMOVDPTR,#tick_count 003BE0 MOVXA,@DPTR 003CFF MOVR7,
A 003DA3 INCDPTR 003EE0 MOVXA,@DPTR 003F900000EMOVDPTR,#?
_printf?
BYTE+03H 0042CF XCHA,R7 0043F0 MOVX@DPTR,
A 0044A3 INCDPTR 0045EF MOVA,R7 0046F0 MOVX@DPTR,
A 28 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 0047120000ELCALL_printf ;SOURCELINE#16 004AD007POPAR7 004CD006POPAR6 004ED005POPAR5 0050D004POPAR4 0052D003POPAR3 0054D002POPAR2 0056D001POPAR1 0058D000POPAR0 005AD0D0POPPSW 005CD082POPDPL 005ED083POPDPH 0060D0F0POPB 0062D0E0POPACC 006432 RETI ;FUNCTIONtimer0(END) 在上面的例子中调用printf函数使得编译器把所有的工作寄存器入栈因为调用本 身和非再入函数printf的处理过程中要使用到这些寄存器如果在C源程序中把调用语句 去掉的话编译出来的代码就小得多了 列表0-10
1 #include
2 3 #defineRELOADVALH0x3C
4 #defineRELOADVALL0xB0
5 6 externunsignedinttick_count;
7 8 voidtimer0(void)interrupt1using0{ 91 TR0=0; //停止定时器
0 101 TH0=RELOADVALH; //设定溢出时间50ms 111 TL0=RELOADVALL; 121 TR0=1; //启动T0 131 tick_count++; //时间计数器加
1 141} 编译后产生的汇编代码 ;FUNCTIONtimer0(BEGIN) 0000C0E0 PUSHACC Pushandpopofregisterbank0andtheBregisteriseliminatedbecauseprintfwas usingtheregistersforparametersandusingBinternally. 0002C083 PUSHDPH 0004C082 PUSHDPL 29 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 ;SOURCELINE#
8 ;SOURCELINE#
9 0006C28C CLRTR0 ;SOURCELINE#10 0008758C3CMOVTH0,#03CH ;SOURCELINE#11 000B758AB0MOVTL0,#0B0H ;SOURCELINE#12 000ED28C SETBTR0 ;SOURCELINE#13 0010900000EMOVDPTR,#tick_count+01H 0013E0 MOVXA,@DPTR 001404 INCA 0015F0 MOVX@DPTR,
A 00167006 JNZ?
C0002 0018900000EMOVDPTR,#tick_count 001BE0 MOVXA,@DPTR 001C04 INCA 001DF0 MOVX@DPTR,
A 001E ?
C0002: ;SOURCELINE#14 001ED082 POPDPL 0020D083 POPDPH 0022D0E0 POPACC 002432 RETI ;FUNCTIONtimer0(END) 6.1指定中断服务程序使用的寄存器组 当指定中断程序的工作寄存器组时保护工作寄存器的工作就可以被省略使用关键 字using后跟一个0到3的数对应着4组工作寄存器当指定工作寄存器组的时候默 认的工作寄存器组就不会被推入堆栈这将节省32个处理周期因为入栈和出栈都需要
2 个处理周期为中断程序指定工作寄存器组的缺点是所有被中断调用的过程都必须使用 同一个寄存器组否则参数传递会发生错误下面的例子给出了定时器0的中断服务程序 但我已经告述编译器使用寄存器组
0 列表0-11
1 #include
2 #include
3 4 #defineRELOADVALH0x3C
5 #defineRELOADVALL0xB0
6 7 externunsignedinttick_count;
8 9 voidtimer0(void)interrupt1using0{ 30 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 101111121131141151161 TR0=0; //停止定时器
0 TH0=RELOADVALH;//设置溢出时间为50ms TL0=RELOADVALL; TR0=1; //启动T0 tick_count++; //时间计数器加
1 printf("tick_count=%05u\n",tick_count); } 编译后产生的汇编代码 ;FUNCTIONtimer0(BEGIN) 0000C0E0 PUSHACC 0002C0F0 PUSHB Pushandpopofregisterbank0hasbeeneliminatedbecausepilerassumes thatthisISR'owns'RB0. 0004C083 PUSHDPH 0006C082 PUSHDPL 0008C0D0 PUSHPSW 000A75D000 MOVPSW,#00H ;SOURCELINE#
9 ;SOURCELINE#10 000DC28C CLRTR0 ;SOURCELINE#11 000F758C3C MOVTH0,#03CH ;SOURCELINE#12 0012758AB0 MOVTL0,#0B0H ;SOURCELINE#13 0015D28C SETBTR0 ;SOURCELINE#14 0017900000E MOVDPTR,#tick_count+01H 001AE0 MOVXA,@DPTR 001B04 INCA 001CF0 MOVX@DPTR,
A 001D7006 JNZ?
C0002 001F900000E MOVDPTR,#tick_count 0022E0 MOVXA,@DPTR 002304 INCA 0024F0 MOVX@DPTR,
A 0025 ?
C0002: ;SOURCELINE#15 00257B05 MOVR3,#05H 00277A00R MOVR2,#HIGH?
SC_
0 00297900R MOVR1,#LOW?
SC_
0 002B900000E MOVDPTR,#tick_count 31 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 002EE0 MOVXA,@DPTR 002FFF MOVR7,
A 0030A3 INCDPTR 0031E0 MOVXA,@DPTR 0032900000E MOVDPTR,#?
_printf?
BYTE+03H 0035CF XCHA,R7 0036F0 MOVX@DPTR,
A 0037A3 INCDPTR 0038EF MOVA,R7 0039F0 MOVX@DPTR,
A 003A120000E LCALL_printf ;SOURCELINE#16 003DD0D0 POPPSW 003FD082 POPDPL 0041D083 POPDPH 0043D0F0 POPB 0045D0E0 POPACC 004732 RETI ;FUNCTIONtimer0(END) 7再入函数因为8051内部堆栈空间的限制C51没有像大系统那样使用调用堆栈一般C语言 中调用过程时会把过程的参数和过程中使用的局部变量入栈为了提高效率C51没有提供这种堆栈而是提供一种压缩栈每个过程被给定一个空间用于存放局部变量过程中的每个变量都存放在这个空间的固定位置当递归调用这个过程时会导致变量被覆盖 在某些实时应用中非再入函数是不可取的因为函数调用时可能会被中断程序中断而在中断程序中可能再次调用这个函数所以C51允许将函数定义成再入函数再入函数可被递归调用和多重调用而不用担心变量被覆盖因为每次函数调用时的局部变量都会被单独保存因为这些堆栈是模拟的再入函数一般都比较大运行起来也比较慢模拟栈不允许传递bit类型的变量也不能定义局部位标量 8使用KeilC时应做的和应该避免的Keil编译器能从你的C程序源代码中产生高度优化的代码 更好的代码下面将讨论这方面的一些问题 但你可以帮助编译器产生 8.1采用短变量一个提高代码效率的最基本的方式就是减小变量的长度使用C编程时我们都习惯 于对循环控制变量使用int类型这对8位的单片机来说是一种极大的浪费你应该仔细考虑你所声明的变量值可能的范围然后选择合适的变量类型很明显经常使用的变量应该是unsignedchar只占用一个字节 8.2使用无符号类型为什么要使用无符号类型呢原因是8051不支持符号运算 符号变量的外部代码除了根据变量长度来选择变量类型自外 程序中也不要使用含有带你还要考虑是否变量是否 32 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 会用于负数的场合如果你的程序中可以不需要负数那么把变量都定义成无符号类型的 8.3避免使用浮点指针在8位操作系统上使用32位浮点数是得不偿失的你可以这样做但会浪费大量的时 间所以当你要在系统中使用浮点数的时候你要问问自己这是否一定需要可以通过提高数值数量级和使用整型运算来消除浮点指针处理ints和longs比处理doubles和floats要方便得多你的代码执行起来会更快也不用连接处理浮点指针的模块如果你一定要采用浮点指针的话你应该采用西门子80517和达拉斯半导体公司的80320这些已经对数处理进行过优化的单片机 如果你不得不在你的代码中加入浮点指针那么你的代码长度会增加程序执行速度也会比较慢如果浮点指针运算能被中断的话你必须确保要么中断中不会使用浮点指针运算要么在中断程序前使用fpsave指令把中断指针推入堆栈在中断程序执行后使用fprestore指令把指针恢复还有一种方法是当你要使用像sin()这样的浮点运算程序时,禁止使用中断在运算程序执行完之后再使能它列表0-12#include voidtimer0_isr(void)interruptstructFPBUFfpstate;... fpsave(&fpstate);... fprestore(&fpstate); ... } 1{ //初始化代码或//非浮点指针代码//保留浮点指针系统//中断服务程序代码,//浮点指针代码//复位浮点指针//系统状态//非浮点指针中断//服务程序代码 包括所有 floatmy_sin(floatarg){ floatal; bitold_ea; old_ea=EA; //保留当前中断状态 EA=0; //关闭中断 al=sin(arg); //调用浮点指针运算程序 EA=old_ea; //恢复中断状态 returnal; } 你还要决定所需要的最大精度一旦你计算出你所需要的浮点运算的最多的位数应 该通知编译器知道它将把处理的复杂度控制在最低的范围内 8.4使用位变量对于某些标志位应使用位变量而不是unsignedchar这将节省你的内存你不用 33 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 多浪费7位存储区而且位变量在RAM中访问他们只需要一个处理周期 8.5用局部变量代替全局变量把变量定义成局部变量比全局变量更有效率编译器为局部变量在内部存储区中分配 存储空间而为全局变量在外部存储区中分配存储空间这会降低你的访问速度另一个避免使用全局变量的原因是你必须在你系统的处理过程中调节使用全局变量因为在中断系统和多任务系统中不止一个过程会使用全局变量 8.6为变量分配内部存储区局部变量和全局变量可被定义在你想要的存储区中根据先前的讨论,当你把经常使用 的变量放在内部RAM中时,可使你的程序的速度得到提高,除此之外,你还缩短了你的代码,因为外部存储区寻址的指令相对要麻烦一些考虑到存储速度按下面的顺序使用存储器DATAIDATAPDATAXDATA当然你要记得留出足够的堆栈空间 8.7使用特定指针当你在程序中使用指针时你应指定指针的类型确定它们指向哪个区域如XDATA或 CODE区这样你的代码会更加紧凑因为编译器不必去确定指针所指向的存储区因为你已经进行了说明 8.8使用调令 对于一些简单的操作如变量循环位移编译器提供了一些调令供用户使用许多调 令直接对应着汇编指令而另外一些比较复杂并兼容ANSI所有这些调令都是再入函数 你可在任何地方安全的调用他们 和单字节循环位移指令RLA和RRA相对应的调令是_crol_循环左移和_cror_(循 环右移)如果你想对int或long类型的变量进行循环位移调令将更加复杂而且执行的 时间会更长对于int类型调令为_irol_,_iror_,对于long类型调令为_lrol_,_lror_ 在C中也提供了像汇编中JBC指令那样的调令_testbit_如果参数位置位他将返回
1 否则将返回0这条调令在检查标志位时十分有用而且使C的代码更具有可读性调令 将直接转换成JBC指令 列表0-13 #include voidserial_intr(void)interrupt4{ if(!
_testbit_(TI)){//是否是发送中断 P0=1; //翻转P0.0 _nop_(); //等待一个指令周期 P0=0; ... } if(!
_testbit_(RI)){ test=_cror_(SBUF,1);//将SBUF中的数据循环 //右移一位 ... } } 34 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 8.8使用宏替代函数对于小段代码像使能某些电路或从锁存器中读取数据你可通过使用宏来替代函数 使得程序有更好的可读性你可把代码定义在宏中这样看上去更像函数编译器在碰到宏时按照事先定义的代码去替代宏宏的名字应能够描述宏的操作当需要改变宏时你只要修该宏定义处列表0-14#defineled_on(){\ led_state=LED_ON;\XBYTE[LED_CNTRL]=0x01;} #defineled_off(){\led_state=LED_OFF;\XBYTE[LED_CNTRL]=0x00;} #definecheckvalue(val)\((valMAXVAL)?
0:1) 宏能够使得访问多层结构和数组更加容易可以用宏来替代程序中经常使用的复杂语句以减少你打字的工作量且有更好的可读性和可维护性 9存储器模式C51提供了3种存储器模式来存储变量过程参数和分配再入函数堆栈你应该尽量 使用小存储器模式很少应用系统需要使用其它两种模式像有大的再入函数堆栈系统那样一般来说如果系统所需要的内存数小于内部RAM数时都应以小存储模式进行编译在这种模式下DATA段是所有内部变量和全局变量的默认存储段所有参数传递都发生在DATA段中如果有函数被声明为再入函数编译器会在内部RAM中为他们分配空间这种模式的优势就是数据的存取速度很快但只有120个字节的存储空间供你使用总共有128个字节但至少有8个字节被寄存器组使用你还要为程序调用开辟足够的堆栈 如果你的系统有256字节或更少的外部RAM你可以使用压缩存储模式这样一来如果不加说明变量将被分配在PDATA段中这种模式将扩充你能够使用的RAM数量对XDATA段以外的数据存储仍然是很快的变量的参数传递将在内部RAM中进行这样存储速度会比较快对PDATA段的数据的寻址是通过R0和R1进行间接寻址比使用DPTR要快一些 在大存储模式中所有变量的默认存储区是XDATA段KeilC尽量使用内部寄存器组进行参数传递在寄存器组中可以传递参数的数量和和压缩存储模式一样再入函数的模拟栈将在XDATA中对XDATA段数据的访问是最慢的所以要仔细考虑变量应存储的位置使数据的存储速度得到优化 10混合存储模式Keil允许使用混合的存储模式这点在大存储模式中是非常有用的在大存储器模式 下有些过程对数据传递的速度要求很高我就把过程定义在小存储模式寄存器中这使得编译器为该过程的局部变量在内部RAM中分配存储空间并保证所有参数都通过内部RAM进行传递尽管采用混合模式后编译的代码长度不会有很大的改变但这种努力是值得的 就像能在大模式下把过程声明为小模式一样你像能在小模式下把过程声明为压缩模式或大模式这一般使用在需要大量存储空间的过程上这样过程中的局部变量将被存储在外部存储区中你也可以通过过程中的变量声明把变量分配在XDATA段中 35 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 11运行库 运行库中提供了很多短小精悍的函数你可以很方便的使用他们你自己很难写出更 好的代码了值得注意的是库中有些函数gets atof 不是再入函数如果在执行这些函数的时printf atol atan2cosh 候被中断而在中断程序中又调用了该函数将得到意想不到的结果而且这种错误很难找出来表0-7列出了非再入型的库函数使用这些函数时最好禁止使用 sprinfscanfsscanfpystrcat atoiexploglog10sqrt sinhtanhcallocfreeInit_mempool 这些函数的中断 strncatstrncmp srandcos mallocrealloc 12动态存储分配 strncpy sin strspn tan 通过标准C的功能函数malloc和freestrcspn acos ceilfloormodf KeilC提供了动态存储分配功能对大多strpbrk asin pow 数应用来说应尽可能在编译的时候确定所需要的内存空间并进行分配但是对 strrpbrk atan表0-
7 于有些需要使用动态结构如树和链表的应用来说这种方式就不再适用了KeilC对这种 应用提供了有力的支持 动态分配函数要求用户声明一个字节数组作为堆根据所需要动态内存的大小来决定 数组的长度作为堆被声明的数组在XDATA区中因为库函数使用特定指针来进行寻址 此外也没有必要在DATA区中动态分配内存因为DATA区的空间本身就很小 一旦在XDATA区中声明了这个块指向块的指针和块的大小要传递给初始化函数 init_mempool,他将设置一些内部变量和进行一些准备工作并对动态存储空间进行初始 化一旦初始化工作完成可在任何系统中调用动态分配函数动态分配的函数包括 malloc(接受一个描述空间大小的unsignedint参数,返回一个指针),calloc(接受一个描 述数量和一个描述大小的unsignedint参数,返回一个指针),realloc(接受一个指向块的 指针和一个描述空间大小的unsignedint参数,返回一个指向按给出参数分配的空间的指 针),free(接受一个指向块的指针,使这个空间可以再次被分配)所有这些函数都将返回指 向堆的指针如果失败的话将返回NULL下面是一个动态分配存储区的例子 列表0-15 #include #include //代码中利用特定指针来提高效率 typedefstructentry_str{structentry_strxdata*next;chartext[33]; }entry; //定义队列元素结构//指向下一个元素//结构中的字符串 voidinit_queue(void);voidinsert_queue(entryxdata*);voiddisplay_queue(entryxdata*);voidfree_queue(void);entryxdata*pop_queue(void); 36 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 entryxdata*root=NULL; //设置队列为空 voidmain(void){ entryxdata*newptr; init_queue(); //设置队列 ... newptr=malloc(sizeof(entry));//分配一个队列元素 sprintf(newptr->text,"entrynumberone"); insert_queue(newptr); //放入队列 ... newptr=malloc(sizeof(entry)); sprintf(newptr->text,"entrynumbertwo"); insert_queue(newptr); //插入另一个元素 ...display_queue(root);...newptr=pop_queue();printf("%s\n",newptr->text);free(newptr);...free_queue();} //显示队列//弹出头元素//删除它//释放整个队列空间 voidinit_queue(void){staticunsignedcharmemblk[1000];init_mempool(memblk,sizeof(memblk)); } //这部分空间将作为堆//建立堆 voidinsert_queue(entryxdata*ptr){entryxdata*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;} //把元素插入队尾 37 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 voiddisplay_queue(entryxdata*ptr){//显示队列entryxdata*fptr;fptr=ptr;while(fptr!
=NULL){printf("%s\n",fptr->text);fptr=fptr->next;} } voidfree_queue(void){entryxdata*temp;while(root!
=NULL){temp=root;root=root->next;free(temp); }} //释放队列空间 entryxdata*pop_queue(void){entryxdata*temp;if(root==NULL){returnNULL;}temp=root;root=root->next;temp->next=NULL;returntemp; }可见使用动态分配函数就像ANSI //删除队列C一样十分方便 13结论使用C来开发你的系统将更加方便快捷他既不会降低你对硬件的控制能力也不会使 你的代码长度增加多少如果你运用得好的话你能够开发出非常高效的系统并且非常利于维护 38 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 第三章使用软件补充硬件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如何使用局部存储区很多时候根据调用结构一个存储地址将存储不同的局部变量所以要多使用局部变量当然不可避免的有一些全局变量像标志位保存每日时间的变量也有可能在指定的函数中定义静态变量编译器会把他们当成全局变量一样处理 39 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 图0-2扩展电路为了节省RAM要尽可能少的调用库函数一些库函数要占用大量的RAM并且这些函数的范围和功能都超出了所需比如printf函数包含了时钟不需要的很多初始化功能应考虑是否要专门写一个程序来替代标准的printf函数这样会占用更少的资源 表0-1 40 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 3使用液晶驱动这个项目所选择的液晶驱动芯片为GMD16202有2x16段它的接口十分简单表0-
1 中列出了对芯片操作的简单的指令上电后必须初始化显示包括总线的宽度线的数量输入模式等每个命令之间要查询显示是否准备好接收下一个数据执行每条指令一般需要40ms时间有些只需要1.64ms 3.1LCD驱动接口我们通过减少元件来降低成本,从液晶驱动接口可以很容易的看出这点,驱动芯片的
8 位数据线和P1口相连用软件来控制显示和产生正确的使能信号脉冲序列锁住输入输出的数据而典型的系统驱动芯片和8051的总线相连软件只需要用XBYTE[]对芯片寻址就可以了当把工作交由软件来完成之后就不再需要解码器和一些支持芯片这就降低了速度因为软件要完成8051和LCD驱动芯片之间的数据传输工作代码的长度和执行时间都会比较长对时钟系统来说有大量的EPROM空间剩余代码的长度不是问题而由以后的分析我们会发现执行的时间长短也不是问题一旦理解了LCD驱动芯片所需的信号和时序之后显示的接口函数就很容易写了软件只须要3个基本功能写入一个命令写入下一个字符读显示状态寄存器这些操作的时序关系见图0-3和0-4在每个信号之间允许有很长的时间间隔信号有效或无效的时间可以毫秒来计算而不像系统总线那样以钠秒来计算I/0函数只需要按照时序图来操作就可以了 列表0-
1 voiddisp_write(unsignedcharvalue){ DISPDATA=value;//发送数据 REGSEL=1; //选择数据寄存器 RDWR=0; //选择写模式 ENABLE=1; //发送数据给LCD ENABLE=0; } disp_write的功能是送一个字符给LCD显示 经准备好接收数据 列表0-
2 voiddisp_cmd(unsignedcharcmd){ 在送数之前应查看LCD驱动芯片是否已 41 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 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 unsignedchardisp_read(void){ unsignedcharvalue; 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 charputchar(charc){ staticunsignedcharflag=0; if(!
flag||c==255){ //显示是否应该回到原位 42 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 disp_cmd(DISP_HOME); flag=0; if(c==255){ returnc; } } if(flag==16){ //是否使用下一个显示行 disp_cmd(DISP_POS|DISP_LINE2);//显示移到第二行 } disp_write(c); //送一个字符显示 while(disp_read()&DISP_BUSY);//等待显示 flag++;//incrementthelineflag if(flag>=32){flag=0;} //显示完之后清除 return(c); } 如你所见函数十分简单它调用一些低层的I/O过程向显示写入数据如果写入成 功的话返回所传送的字符它假设显示工作正常所以总是返回所写入的字符 4.1定制printf函数 C51的库函数中包含了printf函数该函数格式化字符串并把他们输出到标准输出 设备对PC来说标准输出设备就是你的显示设备对8051来说是串行口在这里只有
个显示就本质来说printf函数是通过不断的调用putchar函数来输出字符串的这样通 过重新定义putchar函数就可以改变printf函数连接器在连接的时候将使用源代码中 的putchar函数而不是运行函数库中的函数下面的功能将调用printf函数来格式化时 间串并发送显示 列表0-
5 voiddisp_time(void){ //显示保存的当前时间 //当时间数据使用完毕后才清除使用标志位 //这避免了数据在使用中被修改 printf("\xFFTIMEOFDAYIS:%B02u:%B02u:%B02u", timeholder.hour,timeholder.min,timeholder.sec); disp_update=0; //清除显示更新标志位 } 5使用定时计数器来计时不少嵌入式系统特别是那些低成本的系统没有实时时钟来提供时间信号然而这些 系统一般都要在某个时间或在系统事件的某段时间之后执行某段任务这些任务包括以一定的时间间隔显示数据和以一定的频率接收数据一般设计者会通过循环来延时这种做法的缺点是对不同的延时时间要做不同的延时程序很多延时程序是通过NOP和DJNZ指令来进行延时的这对于使用电池的系统来说是一种消耗 一种好得多的方法是用内置定时器来产生系统时钟定时器不断的溢出重装并在指定的时间产生中断中断程序重装定时器分配定时时间并执行指定的过程这种方法的好处是很多的首先处理器不必一直执行计时循环他可在各个中断之间处于idle 43 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 模式或执行其它指令其次所有控制都在ISR中进行如果系统频率改变了或定时时间 需要改变软件只需要更改一个地方第三所有的代码都可用C来编写你可以通过观 察汇编后的代码来计算定时器溢出到定时器重装并开始运行所需的时间进一步根据重装 值来计算定时的时间 我所作过的没有外部时间输入却要有系统时间的嵌入式系统都采用了这种方法下面 将介绍如何每隔50ms产生一个时钟信号在编写软件之前你首先要明确你的要求如果你 最快的任务执行速度是3ms一次那么就以这个时间为准发生频率比较慢的事件可以很 好的被驱动如果你的系统时间不能很好的兼容你可以考虑使用两个定时器 决定了系统的时间标志后就需要算出按所需频率产生时标的定时器重装值为此 你要知道你的晶振频率用它来得到指令周期的执行时间如果你要产生一个50ms的时标 你的系统频率是12MHz你的指令执行频率就是1MHz,每条指令的执行时间就是1us 有了指令的执行时间就可以计算出每个系统时间标志所需要的指令周期数根据前面 的条件需要50000个指令周期来获得50ms一次的系统频率标志65536减去50000得到 155363CB0的重装值如果你的要求不是那么精确的话可把这个值直接装入定时器中 下面的例子用定时器0产生系统时标定时器1用来产生波特率或其它定时功能 列表0-
6 #defineRELOAD_HIGH0x3C #defineRELOAD_LOW0xB0 voidsystem_tick(void)interrupt1{ TR0=0; //停止定时器 TH0=RELOAD_HIGH;//设置重装值 TL0=RELOAD_LOW; TR0=1; //重新启动定时器 //执行中断操作 } 以上为过程的一个基本结构一旦定时器重装并开始工作之后你就可以进行一些操 作如保存时标数事件操作置位标志位你必须保证这些操作的时间不超过定时器的 溢出的时间否则将丢失时标数 可以很容易的让系统在一定的时标数之后执行某些操作这通过设置一个时标计数变 量来完成这个全球变量在每个时标过程中减一当它为0时将执行操作例如你有一个 和引脚相连的LED希望它亮2秒钟然后关掉代码如下 if(led_timer){ //时间计数器不为
0 led_timer--; //减时间计数器 if(!
led_timer){ //显示时间到... LED=OFF;//turnofftheLED } } 虽然上面一段代码很简单却可以用在大多数嵌入式系统中当有更复杂的功能需要 执行时这段代码可放置在定时器中断程序中这样在检查完一个定时时间之后可以接 着检查下一个定时时间并决定是否执行相应的操作共用一个时标的定时操作可被放入 一个只有时标被某个特定数整除才有效的空间中 假设你需要以不少于1秒的间隔时间执行一些功能使用上面的时标过程你只要保 存一个计数器仅当计数器变为0的时候查询那些基于秒的定时操作而不需要系统每 隔50ms就查询一次 44 广州周立功单片机发展有限公司Tel02038730916387309173873097638730977Fax:38730925 t--; //减时标计数器 if(!
t){ //一秒钟过去了... ... //进行相应的操作 t=20; //重新定时1秒 } 注意你的中断服务程序所需的执行时间如果执行时间超过50ms你会发现将丢失时 标在这种情况下你把一些操作移出中断程序放到主程序中通过设置标志位来告诉 主程序是否要执行相应的功能但操作的时间精度就不够高了因此对时间精度要求很高 的操作还是要放在中断程序中 可用上面的时标过程来做成时钟它将记录每天的时间并在需要显示时间的时候置 位标志位主程序将监视标志位在时间更新的时候显示新的时间定时器0中断程序还 将对按键延迟计时 6使用系统时标做用户接口 用户接口相对来说比较简单但并不说明这里讲到的不能用到大系统中设置键用来 击活设置模式更改时间当进入设置模式后设置键将用来增加光标处的数值选择键 将使光标移到下一个位置当光标移过最后一个位置时设置模式结束每次设置键或选 择键被击活后设置模式计数器被装入最大值每个时标来临时减1当减到0时结束 设置模式 每隔50ms在中断中查询按键这种查询速度对人来说已经足够了有时侯甚至0.2秒 都可以对8051来说人是一个慢速的I/O器件当检测到有键按下时将设置一个计数器 以防按键抖动这个计数器在每次中断到来时减1直到计数器为0时才再次查询按键 当设置模式被击活时软件必须控制光标在显示器上的位置让操作者知道要设置哪 个位置cur_field变量指向当前的位置set_cursor函数将打开关闭光标或把它移到 所选择的位置为了简化用户设置的工作和同步时钟当进行设置时计时被挂起这也 避免了在设置时程序用printf更新时间在进行时间更新时也不允许进入设置模式 这也将避免pirntf函数在同一时间被多个中断调用 下面是系统时标程序对许多系统来说这个程序已经足够可把它作为你应用程序的 模块 列表0-
7 voidsystem_tick(void)interrupt1{ staticunsignedchart=20;//时间计数器顶事为1秒 TR0=0; //停止定时器 TH0=RELOAD_HIGH; //设定重装值 TL0=RELOAD_LOW; TR0=1; //启动定时器 if(switch_debounce) switch_debounce--; } if(!
switch_debounce){ if(!
SET){ //如

标签: #文件 #保存文件 #文件 #电子版 #文件 #换行 #文件 #压缩文件