您的当前位置:首页正文

PLC+单片机

2024-03-22 来源:步旅网
isibleontrol AN2103

AN2103 基于GUTTA一步一步实现一个最小PLC系统

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

2009/03/25

写在前面的话...................................................................................................................................4

为什么要做自己的PLC系统...................................................................................................4 这里实现了什么样的PLC系统...............................................................................................4 第1章 前期准备.............................................................................................................................5

电脑...........................................................................................................................................5 CPU-EC20 (8051)仿真器.........................................................................................................5 安装8051的C编译器SDCC....................................................................................................6 安装软件GUTTA Ladder Editor..............................................................................................6 ISP下载软件STC-ISP..............................................................................................................7 第2章 规划.....................................................................................................................................7

内存系统...................................................................................................................................7 指令系统...................................................................................................................................8 运行模式.................................................................................................................................10 通讯系统.................................................................................................................................15 第3章 添加CPU类型...................................................................................................................16

配置类型(PlcType.XML)..................................................................................................17 配置变量系统(ManagerVar.XML)...................................................................................19 配置指令集(ManagerFun.XML).......................................................................................22 CPU类型的测试.....................................................................................................................22 第4章 完成仿真器固件...............................................................................................................22

熟悉我们的编译器.................................................................................................................22

编译单个文件.................................................................................................................23 Intel Hex文件的对齐......................................................................................................23 编译多个文件.................................................................................................................23 命令行参数.....................................................................................................................24 变量空间分配的扩展.....................................................................................................25 基本定义.................................................................................................................................26 硬件系统.................................................................................................................................30

内存.................................................................................................................................30 I/O操作...........................................................................................................................32 闪存管理.........................................................................................................................36 时钟节拍.........................................................................................................................40 串口通讯.........................................................................................................................43 软件系统.................................................................................................................................49

通讯协议.........................................................................................................................49 指令集支持.....................................................................................................................58 运行系统.........................................................................................................................66

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

1

isibleontrol AN2103

第5章 完成CPU类型的配置.......................................................................................................72

完成文件CompileInfor.XML.................................................................................................73

..........................................................................................................................74 .....................................................................................................................74 ....................................................................................................................74 完成文件swap_auto.h.............................................................................................................76 第6章 综合调试...........................................................................................................................78

编译系统固件.........................................................................................................................78 下载系统固件.........................................................................................................................80 最简单的程序.........................................................................................................................81

逻辑指令的测试.............................................................................................................81 定时器指令的测试.........................................................................................................81 计数器指令的测试.........................................................................................................82

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 2

isibleontrol AN2103

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 3

isibleontrol AN2103

写在前面的话

为什么要做自己的PLC系统

实现一个自己的PLC其实不难(当然要实现一个功能全面的商业化的PLC还是有难度的),只要你懂C语言、掌握一种单片机的应用、熟悉基本的I/O电路、同时有这方面的兴趣(这个最关键)。在您看完这篇文章后,就能具备一个大致的概念。在您按照这篇文章一步一步动手实践后,相信您就知道怎么去实现自己的PLC系统了。 为什么要实现自己的PLC系统呢?抛开商业意义不说,如果您对PLC不是很了解,那么实现一个自己的PLC系统,您对PLC的认识肯定就能上升一个层次。如果您对单片机不是很了解,那么经过这个项目的训练,自己动手基于单片机实现了一个自己的PLC,恭喜你,你就已经是一个单片机熟手啦!因为PLC的开发不是针对具体的应用,而是一个平台的规划和建设。因此这个项目对单片机各个功能的挖掘也是最深入的。哪怕仅仅只是本文介绍的这个最小PLC系统,就涉及到了单片机的在系统编程(ISP)、在应用编程(IAP)、部分编译、异步串口通讯、I2C通讯、FLASH读取等等技巧。若您对单片机和PLC都不是太熟,建议您还是先打打基础吧,这篇文章可能暂时还不适合您。 知道怎么做和真正做好是两个概念。不论是学习单片机还是学习PLC,自己动手实践,是最好的学习方法!考虑到在工业控制领域中,目前51系列的8位单片机依然是最为大家所熟知的一款。这里我们就以CPU-EC20 (8051)仿真器为硬件基础,一步一步实现一个最小的PLC系统。

这里实现了什么样的PLC系统

从70年代第一台PLC到目前为止,出现过各种形式的PLC,实现方法也各不相同。从规模来看,PLC一般按下面标准分类:

1. 微型机:控制点数一般在几点、十几点、几十点。典型的代表是西门子的LOGO系列控

制器、OMRON的ZEN系列控制器、斯耐德的Zelio Logic控制器。这类PLC一般具有以下特点:体积小、允许的IO电流很大(甚至达到8A)、自带液晶面板可以现场编程、价格便宜。这类PLC主要设计目标就是替代旧式的继电器电路,因此这类PLC也叫做PLR(可编程继电器或智能继电器)。

2. 小型机:控制点数可达100多点。典型的代表是西门子的S7-200系列控制器、OMRON

的CPM2A、CP1H、CQM1H系列控制器、斯耐德的Twido系列控制器。这种PLC应该也是目前使用最多,大家也最为熟悉的PLC。

3. 大型机:控制点数一般在1000点以上。典型的代表是西门子的S7-400系列控制器、施

耐德的Quantum系列PLC。这类PLC可能更像计算机。在抗干扰、启动速度上一般还不如小型机,但这种PLC一般有很强的数据处理能力、有丰富的通讯接口、自带冗余系统,一般作为大型项目的控制核心。 这部分就不多介绍了,因为目前我们不需要知道如何去采购PLC,我们是要自己来实现一个PLC!从PLC开发人员来看,目前PLC的实现有下面几种形式: 1. 解释型还是编译型。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

4

isibleontrol AN2103

这是从工作方式上来看的。解释型PLC将程序分为解释系统和用户指令两个截然不同的部分。解释系统类似于一个虚拟机,通过逐条翻译用户指令执行对应的操作。用户指令仅仅是解释系统可以识别的数据,和处理器指令系统无关,可以自行定义。编译型PLC不存在解释系统和用户指令的划分。编译的结果就是处理器能够执行的二进制指令,而这些指令的执行能够实现梯形图的图形化逻辑。解释系统比较灵活,但是效率不高,常在小型PLC中使用。编译系统具有更高的定制能力,效率也很高,常在大型PLC中使用。 由于CPU-EC20 (8051)所用的51单片机IAP12C5A60AD只有1280(1024+256)字节的RAM。内存比较紧张,因此我们这个项目实现的是编译型PLC。 2. 硬解码还是软解码? 这是从硬件角度来看的。PLC最开始出现就是为了取代开关继电器电路。由于那个年代通用处理器价格昂贵,PLC一般采用自己专用的位处理器。位处理器设计上比较简单可靠,且能够和PLC指令表指令一一对应。随着控制技术和芯片制造技术的发展,一方面控制系统希望PLC除了逻辑处理,还要有更强的数据处理能力,更强的通讯能力等。另一方面,通用控制器处理器价格不断下降。随着工艺的提升,处理器的价格除了由处理器的复杂程度(门电路的多少)来决定,更多的由处理器的使用量来决定(分摊了处理器的研发成本)。不少PLC制造商不再单独的为自己的PLC开发处理器,而是采用通用的处理器。那么对于PLC特殊的逻辑指令,必须采用软解码的方式来实现。 不容置疑,我们这里使用单片机来开发PLC,也就是所谓的软解码。 3. 扫描指图还是扫描指令? 这是从最小的执行单元来看的。扫描图指PLC直接分析梯形图上的能流来进行逻辑运算。扫描指令需要软件先将梯形图转换成等效的指令表,然后一条一条执行指令表完成梯形图的逻辑。扫描图更加灵活,图上的元件可以任意放置,同时比较容易实现运行中编程。扫描指令对梯形图上的元件放置有一定的要求,不过效率较高。 由于目前我们需要借助软件GUTTA Ladder Editor来实现梯形图的编辑(当然读者有精力的话也可以自己开发一个),而这个软件是只支持扫描指令的,故我们的目标PLC采用扫描指令的工作方式。

第1章 前期准备

正所谓工欲善其事,必先利其器。这里把我们将来肯定需要用到工具介绍一下:

电脑

配置要求不高(独立或者集显均可……),新旧不限,但是必须能跑Windows XP或者更高版本。因为GUTTA Ladder Editor这个软件暂时还没有Windows以外操作系统的版本。串口不是必需的(CPU-EC20 (8051)自带USB到COM的转换器),当然有是最好的,原生的东西毕竟最稳定可靠。

CPU-EC20 (8051)仿真器

它实际上是51单片机学习和PLC学习二合一的仿真器。使用自带固件的时候,它就是

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 5

isibleontrol AN2103

一台PLC,可以用来验证读者编写的PLC程序。若不使用自带固件,它也可用于51单片机的开发。由于我们这里是自己来实现一个PLC,因此把它当作51单片机学习仿真器来使用,即不用它的自带固件,而是自己来实现一个这样的固件。建议读者使用这个仿真器,否则硬件上的不一致会造成一些软件资源不可用。购买办法可以访问我的网站

。当然如果您手头有现成的51开发板,并对自己做好移植(http://www.visiblecontrol.com/)

有信心的话,也可以不必重复购买。需要注意的是,由于51处理器型号上的不一致,您需要确认您的51单片机是否支持至少1024字节的扩展RAM(片内或片外都可),是否支持在应用中编程(IAP)。因为本项目采用部分编译的形式,IAP是必需的。

安装8051的C编译器SDCC

SDCC是一款目标设备可变、优化的ANSI C编译器。目前支持Intel 8051、Maxim 80DS390、Zilog Z80、Motorola 68HC08。SDCC是自由开放源码软件,遵循GPL协议。 SDCC的主页:http://sdcc.sourceforge.net/ SDCC的下载地址:http://sourceforge.net/project/showfiles.php?group_id=599 为什么是SDCC而不是KEIL?虽然从各种途径获得的信息是,KEIL的编译效率要比SDCC高(感觉上确实如此),但KEIL是商业软件。由于我们实现的是编译型PLC,在进行发布时,必然需要将编译器嵌入软件GUTTA Ladder Editor中(软件提供了开放接口)。然而未经许可将一款商业软件的二进制文件作为自己的产品发布是不妥的。另外在我看来,SDCC简单却实用(该有的功能都有,暂时用不到的功能都没有),非常利于学习单片机。还有一种可行的办法就是,因为我们是采用的部分编译的PLC实现,可以先用KEIL来编译PLC的系统固件,这部分写入单片机FLASH后保持不变,然后用SDCC来编译用户逻辑部分,SDCC随GUTTA Ladder Editor软件一起发布。由于本文档介绍的项目主要是以教学为目的,对效率的要求并不是特别苛刻,为了方便起见,这里不论是PLC系统固件,还是PLC用户逻辑部分,统一采用SDCC做为C编译器。

安装软件GUTTA Ladder Editor

这是一款用户可配置的PLC梯形图编辑环境。在我们这个项目看来,它就完成了一件事:读取您事先定义好的内存区域和指令集,提供一个梯形图编辑环境。在用户完成梯形图编辑后,将梯形图转化成指令表指令的形式传递给单片机PLC系统。 在软件中,窗口框架的设计是比较繁琐的。所幸的是,这些脏活累活都已经由GUTTA Ladder Editor完成了。并且GUTTA Ladder Editor在设计之初就充分考虑到了PLC类型的扩展。绝大部分和PLC相关的定义都以XML文件的形式存在,以方便您添加PLC类型(XML文件格式可能对没有接触过软件开发的人来说会比较陌生,其实它很简单,就是用文本纪录一些信息而已,可以用Windows自带的记事本打开和编辑)。 GUTTA Ladder Editor的主页:http://www.visiblecontrol.com/ GUTTA Ladder Editor的下载地址:http://www.visiblecontrol.com/download/software.htmCOPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 6

isibleontrol AN2103

ISP下载软件STC-ISP

由于CPU-EC20 (8051)使用的是宏晶(STC)增强型的51单片机(IAP12C5A60AD),那就使用宏晶提供的下载器吧。

STC-ISP的主页:http://www.mcu-memory.com/ STC-ISP的下载地址: http://www.mcu-memory.com/datasheet/stc/stc-isp-v4.79/stc-isp-v4.79-not-setup.EXE第2章 规划

内存系统

由于CPU-EC20 (8051)所用的芯片IAP12C5A60AD只有1280(1024+256)字节的RAM。内存系统所有的文章,便只能做在这1280字节的RAM上。目前市面上不同公司的PLC内存系统是不一样的。但有一点差不多,那就是内存基本上是分区管理的。例如在西门子s7-200中,用I0.0表示一个输入线圈,用Q0.0表示一个输出线圈。我认为,一个内存分区,主要定义了以下几方面的内容:

1. 访问宽度:例如西门子s7-200中,I0.0表示一个位,IB0表示一个字节。

2. 偏移量单位:例如西门子s7-200中,MW0和MW1是重叠的,因为偏移量单位是

字节(8bit),而变量长度是字(16bit)。例如在施耐德的TWIDO中,MW0和MW1是不重叠的,因为偏移量单位是字(16bit),而变量长度也是字(16bit)。

3. 起始地址:例如西门子PLC中,MB0表示M区域中的第一个字节。而在MODION

系统中,40001表示保持寄存器区域的第一个字。

4. 特殊用途:例如西门子PLC中,I区域用于输入映射,Q区域用于输出映射。 为了尽量简化我们的PLC系统,我们采用最少的变量区域,以下几个变量区域我认为对于一个PLC来说是必须的:

1. 数字量输入映射区域,我们取名为I。 2. 数字量输出映射区域,我们取名为Q。 3. 模拟量输入映射区域,我们取名为AI。 4. 模拟量输出映射区域,我们取名位AQ。 5. 中间变量区域,我们取名为M。 为了使模型简单,以上这5个变量分区除了特殊用途不一样,访问宽度、偏移量单位、起始地址采用一样的配置,即:

1. 都可以进行位、字节、字、双字长度的访问。例如I0.0表示一个位;IB0表示一个

字节;AIW0表示一个字;AQD0表示一个双字。

2. 变量偏移单位统一为字节。位偏移以点加数字的形式表达。 3. 起始地址都从0开始。 区域标示 I

区域说明

变量偏移单位字节

位访问 √

字节访问√

字访问 √

双字访问√

取地址

取值

取指针

7

数字量输入映射 √

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

isibleontrol AN2103

Q AI AQ M

数字量输出映射 模拟量输入映射 模拟量输出映射 中间变量区域

字节字节字节字节

√ √ √ √

√ √ √ √

√ √ √ √

√ √ √ √

√ √ √ √

下一步我们需要规划变量区域的大小。由于总共只有1280(1024+256)字节的RAM,PLC用户的变量区域的总大小不能大于这个值。同时PLC自身固件还需要使用很多其它的变量。我们暂时将外部扩展RAM的一半分配为PLC用户的变量区域,即XRAM的前512个字节。外部扩展的RAM的后半部分保留给PLC自身固件内部使用。整个IAP12C5A60AD单片机内存做如下规划:

256字节idatadata1024字节xdatabitRegister栈PLC用户使用PLC系统使用I16字节Q16字节AI16字节AQ16字节M448字节第1部分程序使用384字节第2部分程序使用128字节

如图,IAP12C5A60AD的1280(1024+256)字节RAM分为两大部分,一部分是可以通过r0寄存器间接寻址的内部idata。一部分是通过dptr寄存器间接寻址的外部xdata。其中idata又分为两部分。前128个字节既可以直接寻址,也可以间接寻址。后128个字节只能间接寻址。整个idata除了寄存器区间和位寻址区间,其余部分都用于栈空间。栈用来保存所有函数的返回地址、函数中使用的局部变量、并用于函数参数的传递。外部扩展RAM被分成两大部分,第一部分是PLC用户使用的变量,包含I变量、Q变量、AI变量、AQ变量、M变量。其余部分是PLC系统使用的变量。PLC系统使用的变量分又细分为两大部分,即第1部分程序使用和第2部分程序使用。这两部分的使用形式在后面再做详细说明。 按照图中规划,我们可以得到5个区域的绝对地址如下: 区域标示

区域长度

起始地址(xdata)结束地址(xdata)寻址方式

000FH 001FH 002FH 003FH 01FFH

通过dptr寄存器

间接寻址

I 16字节 0000H Q 16字节 0010H IA 16字节 0020H QA 16字节 0030H M 448字节 0040H

指令系统

为了使我们的这个最小系统看起来尽量简单,我决定只实现最基本的逻辑指令。逻辑指令可以用来完成最基本的逻辑运算(与、或、非)。除了逻辑指令,我们同时实现部分最常用的定时器指令和计数器指令。同时,作功能块指令的演示,我们再实现几条数据移动指令。 由于GUTTA Ladder Editor支持梯形图和指令表两种编程方式,这就意味着我们不但需

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

8

isibleontrol AN2103

要通过XML配置文本定义梯形图指令,还需要通过XML配置文本定义指令表指令,同时还需要定义他们之间的转换关系。XML配置文本的详细格式请参考文档《UM4003 指令描述文件规范》。完整的阅读这个规范定然会让您觉得枯燥乏味。没有关系,后面会给出这个最小指令集的完整XML文件。而且绝大多数时候,我们并不需要从头到尾书写这个XML文件;更多的时候,我们只需要知道如何看懂和修改而已。在本章,我们只需要在概念上明确,我们需要实现哪些指令。 1. 基本的逻辑指令:

在梯形图模式下我们需要实现: -| |- 常开触点指令 -|/|- 常闭触点指令 -|NOT|- 取反指令 -|P|- 上升沿转换指令 -|N|- 下降沿转换指令 -( ) 输出线圈指令 -(S) 置位线圈指令 -(R) 复位线圈指令

对应的,在指令表模式下,我们需要实现: ALD 数据栈与指令 [*] OLD 数据栈或指令 [*] LPS 辅助进栈指令 [*] LRD 辅助读栈指令 [*] LPP 辅助出栈指令 [*] LD BIT 装载指令 A BIT 与装载指令 O BIT 或装载指令 LDN BIT 非装载指令 AN BIT 与非装载指令 ON BIT 或非装载指令 NOT 取反指令 EU BIT 上升沿转换指令 ED BIT 下降沿转换指令 = BIT 输出指令 S BIT置位指令 R BIT 复位指令 2. 计数器指令:

在梯形图模式下我们需要实现: -[CTU] 增计数器指令 -[CTD] 减计数器指令 -[CTUD] 增减计数器指令

对应的,在指令表模式下,我们需要实现: CTU C,PV,Q增计数器指令 CTD C,PV,Q减计数器指令 CTUD C,PV,QU,QD增减计数器指令 3. 定时器指令:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

9

isibleontrol AN2103

在梯形图模式下我们需要实现: -[TON] 接通延时定时器指令 -[TONR] 有保持接通延时定时器指令 -[TOF] 关断延时定时器指令

对应的,在指令表模式下,我们需要实现: TON T,PT,Q 接通延时定时器指令 TONR T,PT,Q 有保持接通延时定时器指令 TOF T,PT,Q 关断延时定时器指令 4. 移动指令

在梯形图模式下我们需要实现: -[MOVE_B] 字节移动指令 -[MOVE_W] 字移动指令 -[MOVE_D] 双字移动指令

对应的,在指令表模式下,我们需要实现: MOVB IN,OUT字节移动指令 MOVW IN,OUT字移动指令 MOVD IN,OUT双字移动指令

运行模式

单片机是如何最终实现用户在电脑上绘制的梯形图逻辑呢? 如果是扫描图的方式,就需要将梯形图原封不动的(或者采用某种压缩)按照某种格式下载到单片机中去。单片机可以通过扫描这些数据,分析得到虚拟能流应当的流向,从而做出相应的逻辑动作或者是功能块操作。一般这种实现方式对梯形图的每个梯级大小有一定的限制(最大行数和最大列数)。这是因为资源和效率的因素,能流分析只有在指定行数和列数的情况下才能得到较好的优化(例如行数限制为最多8行的话,那么显然前导列能流就可以用一个8位的字节来表示)。有的甚至直接采用定制的逻辑处理器来进行能流分析,同样对行数和列数有明确的要求。基于这种方式的运行模式,对梯形图中元件的放置一般没有特别规定。 如果是扫描指令的方式,需要PC软件先对用户绘制的梯形图进行分析。按照一定的规则,将梯形图数据转换成指令表数据。这个转换过程为2步:

1. 分析梯形图中所有的元件和连线,根据连线确定逻辑关系(基本的串并联规则、多

输出规则等)。同时需要纪录所有输出线圈、功能块的执行顺序。 2. 根据每个输出线圈、功能块的出现顺序,用指令表指令操作所谓的数据栈和辅助栈,

然后根据栈顶的数据,确定输出线圈、功能块是否执行。指令表对两个栈的操作必须最终实现前面分析梯形图得到的逻辑关系。

由于指令表是更加易于处理器处理的数据,对于硬解析PLC,一条指令表指令甚至就是一条处理器指令。由于我们采用单片机来实现,就必须为每一条指令表指令实现一段单片机的服务子程序。所谓的PLC用户程序,其实就是定义了这些服务子程序的调用参数和顺序。 例如我们在软件GUTTA Ladder Editor中编写了如下的梯形图:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 10

isibleontrol AN2103

翻译成指令表就是:

NETWORK 0 LD I0.0 O I0.1 AN I0.2 = Q0.0

单片机最终根据上面的梯形图完成下面的操作: 1. 将输入端口I0.0的值载入数据栈。

2. 如果输入端口I0.1的值为1,则置位数据栈栈顶的值。 3. 如果输入端口I0.2的值为1,则复位数据栈栈顶的值。 4. 将数据栈栈顶的值输出到输出端口Q0.0。

通过这4步操作,单片机最终完成了梯形图所表达的逻辑:

Q0.0=(I0.0+I0.1)•I0.2

具体到我们的编译型PLC,GUTTA Ladder Editor给出的C语言描述就是:

void AutoExcuteInt_L0(void) { // NETWORK: [NETWORK]

theRes.itState.itStackData = 0; theRes.itState.itStackLogic = 0; LgcExcuteInterruptCheck();

// INSTRUCTION: [LD I0.0]

theRes.itPar[0].itPtr = &theRes.itDI.itI[0]; theRes.itPar[0].itBit = 0; LgcExcuteDispatch_LD(); LgcExcuteInterruptCheck();

// INSTRUCTION: [O I0.1]

theRes.itPar[0].itPtr = &theRes.itDI.itI[0]; theRes.itPar[0].itBit = 1; LgcExcuteDispatch_O(); LgcExcuteInterruptCheck();

// INSTRUCTION: [AN I0.2]

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

11

isibleontrol AN2103

theRes.itPar[0].itPtr = &theRes.itDI.itI[0]; theRes.itPar[0].itBit = 2; LgcExcuteDispatch_AN(); LgcExcuteInterruptCheck();

// INSTRUCTION: [= Q0.0]

theRes.itPar[0].itPtr = &theRes.itDO.itQ[0]; theRes.itPar[0].itBit = 0; LgcExcuteDispatch_EQ(); LgcExcuteInterruptCheck();

}

如果在超级循环中不断的调用这段代码,单片机也就完成了梯形图中需要实现的逻辑。不过在实际应用中,要求用户将这段代码加入工程,然后重新编译整个项目是很不方便的。一种办法就是先将整个项目打包成库的形式(已编译,但未链接地址)。用户在编辑完梯形图程序后,下载PLC程序时:

1. GUTTA Ladder Editor根据梯形图,得到指令表。

2. 根据指令表,生成C语言文件,包含了所有的用户程序实现。

3. 调用SDCC编译器,根据上面生成的C语言文件,编译得到用户程序目标文件。 4. 将用用户程序目标文件和系统库一起链接,生成单片机执行代码。 5. 利用单片机的ISP功能,将执行代码下载到单片机的FLASH中。 在这种模式下,第1步已经由软件GUTTA Ladder Editor完成,我们需要完成的工作就是后面4步。不过这里我并不打算采用这种方式,相对于部分编译,全编译模式有以下缺点:

1. 全编译下载数据量大,因此下载速度较慢。

2. 全编译下载需要借助第三方ISP工具,给使用者带来不便。

3. 全编译加密困难,由于整个系统固件每次都需要重新下载,任何人只需要复制您的

硬件就能仿制您的产品。

那么什么是部分编译呢?所谓的部分编译就是一个完整的单片机应用分为两部分程序。一部分为系统固件,编译一次下载后保持不变。另一部分为用户程序,每次下载PLC程序时都需要重新编译并且重新下载。一个完整的单片机应用分解成两个独立部分并且独立编译独立下载是否可行呢?让我们先看看一个典型的单片机程序吧:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 12

isibleontrol AN2103

FLASH

XRAM

0x000000

中断向量表设置栈寄存器启动代码初始化零内存初始化常数内存CRT其它初始化调用main()入口函数main()中断服务函数各种函数其它各种函数_NO_INIT_STACK_DATA_BSS0x000000

_HEAP常数区域,用于初始化常数内存

如上图,51单片机在上电后,会产生复位中断信号。根据中断向量表中的转跳地址,开始执行启动代码。启动代码一般用汇编语言编写,由编译器自动链接。启动代码一般说来完成以下工作:

z 设置栈寄存器:这是整个C环境的基础。一切的函数调用、参数传递、局部变量都

必须依靠栈来实现。

z 初始化零内存:将XRAM的_BSS区域全部设置为0。

z 初始化常数内存:将FLASH常数区域中的数据拷贝到XRAM的_DATA区域。 z CRT其它初始化:根据CRT的具体实现来定义,不一定有。

z 调用main函数:一般说来,main函数应该是一个超级循环不会返回。假设main

函数返回了,一般在main函数调用之后,启动代码还提供了一个无限循环,当然这个循环什么事情也不做。仅仅只是防止程序跑飞而发生误动作。

在FLASH区域中,紧跟main函数之后的是各种中断服务函数,他们的地址被记录在中断向量表中。之后是其他各种子函数,有可能被main调用,也有可能被中断服务函数调用,它们之间也可能互相钳套调用。现在假设我们编译了两个独立的程序,并且把他们放在不同的地址上,他们是否能够协同工作呢:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 13

isibleontrol AN2103

FLASH

XRAM

0x000000中断向量表设置栈寄存器初始化零内存启动代码初始化常数内存CRT其它初始化调用main()入口函数main()中断服务函数...各种函数其它各种函数0x000200_DATA_BSS_HEAP_NO_INIT_STACK常数区域,用于初始化常数内存..._NO_INIT_STACK_HEAP_DATA_BSS0x000000

0x008000

中断向量表设置栈寄存器初始化零内存启动代码初始化常数内存CRT其它初始化调用main()入口函数main()中断服务函数各种函数其它各种函数常数区域,用于初始化常数内存

如图,我们将IAP12C5A60AD单片机的FLSAH分成两部分:前32K(000000H~007FFFH)为第1部分;后32K(008000H~00FFFFH)为第2部分。可以通过特定的编译器选项,将第1个程序链接在第1部分的地址中,将第2个程序链接在第2部分的地之中。RAM也一样,第1个程序所使用的变量连接在前512字节(000000H~0001FFH),第2个程序所使用的变量连接在后512字节(000200H~0003FFH)。这样首先两个程序在FLASH空间和内存空间上是没有冲突的。 虽然空间上没有冲突,但是正常情况下,第2部分程序是永远得不到执行的,同时第2部分内存空间也永远得不到使用,这是因为单片机的复位入口还是只有一个,那就是000000H上的转跳地址,这个地址始终对应的第1部分程序的启动代码。由于第1部分程序在编译和链接的时候根本就不知道第2部分程序的存在,因此单片机永远运行在第1部分程序中运行且永远只使用第1部分内存。 假如我们在第1部分程序中强制使用函数转跳指令,转跳到第2部分中的函数去执行,是否可行呢?执行完后又是否能够正确返回呢?由于这两部分程序是独立编译的、在不同时间、在不同地点、在不同的电脑上编译的,他们是否能够彼此兼容合作愉快呢? 我认为,只要满足以下两点,就没有问题。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

14

isibleontrol AN2103

1. 函数地址正确。在第1部分程序中,转跳到第2部分的地址确实是一个函数的开始地址。

由于是分开独立编译的,这个地址必须手工给出并且正确无误。

2. 编译选项完全一致。主要是指栈的模型一致、参数传递的模型一致。具体到51单片机的SDCC编译器,就意味着模式一致(小型模式、中型模式、巨型模式);参数是通过全局变量来传递还是通过栈来传递,传递顺序一致。 对于51单片机来说,一个技术难点就是函数的参数传递问题。大家知道,不论是KEIL的51编译器,还是SDCC的51编译器,默认将参数放在全局变量中来传递。这是因为51没有快速的寄存器相对偏移寻址指令。编译器需要在编译的时候为每个函数的参数和局部变量分配全局空间,然后在链接时分析整个函数调用树,将不会互相干扰的参数和局部变量重叠起来以节约空间(同时这些函数都是不可重入的)。通过这种方式,每个函数的参数和局部变量都有绝对地址,从而提高了单片机的整体运行效率。但是强制跳转使函数调用树的分析不能有效的使用,因为第1部分编译器根本不知道目标地址的函数有哪些局部变量,函数的参数地址应该放在什么地方(这些都由第2部分程序决定)。因此在我们这个特殊的单片机系统中,推荐两部分程序都将函数参数和局部变量放在栈中。 具体到我们的PLC系统中,第1部分就是我们的PLC系统固件,第一次下载以后就不需要更改了。这部分主要负责单片机外围的外围驱动、通讯系统、PLC初始化、扫描循环、同时提供PLC指令集的实现。当然还有最重要的一点就是,第1部分能够完成对第2部分的更新。通过通讯的方式从GUTTA Ladder Editor得到程序数据,同时将程序数据写入第2部分FLASH中(因此要求必须支持IAP)。第2部分程序执行梯形图的具体逻辑。也就是说,第2部分程序在每次PLC程序被下载的时候更新。 在编译型PLC中,由于两部分程序是联合运行的,其中任何一部分程序不稳定,会导致整个系统不稳定。例如在某个PLC逻辑中,编译好的第2部分程序中有致命的运行错误,会导致整个系统崩溃(安全性不如解释型)。一旦包含致命运行错误的第2部分程序下载到单片机中,由于PLC一启动就开始运行PLC用户程序(主扫描循环),然后PLC立即崩溃,由于这个时候通讯已经不能使用,错误的程序将不能通过GUTTA通讯协议清除。 一种解决方案就是使用运行停止开关。若PLC在停止档上电的时候则不运行,既不执行第2部分代码,同时可以下载程序,就能够将错误程序删除。另一种解决方案就是PLC在启动后,不立即运行,而是等待一段很小的时间(500ms以内),观察串口是否有指定的通讯数据流,如果有,进入PLC的高级配置模式,然后进行PLC程序的清除(同时清除了致命错误)。其实也就是在单片机崩溃之前,给了一个清除错误的机会。我们使用软件GUTTA Flash Utility来发起这个通讯。

通讯系统

完整GUTTA通讯协议可以参考文档《UM4001 GUTTA通讯协议》,对于我们的这个最小系统,我们不可能实现协议中定义的全部通讯报文格式。我们只需要实现其中必须的一部分即可。请详细阅读《UM4001 GUTTA通讯协议》中概述和封包结构这两章。具体到我们这个最小系统,我决定在通讯协议上和GUTTA通讯协议完全兼容,但是只实现以下子集: 功能码

缩写

说明 清除PLC 连接PLC 断开PLC 获得PLC名称

15

PLC基本控制

0100H PLC_CLEAR 0110H PLC_ATTACH 0111H PLC_DETACH 0120H GET_PLC_NAME

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

isibleontrol AN2103

0121H GET_PLC_INFOR 0A00H STATUS_READ 0A01H STATUS_WRITE 0A02H STATUS_SCAN 0A03H STATUS_RESET

获得PLC信息 读PLC状态 写PLC状态 强制扫描 强制复位

PLC程序的下载PLC状态的读写

0B00H* BINARY_INSTRUCTION_ASK 查询指令长度 0B01H* BINARY_INSTRUCTION_READ 读指令数据 0B02H* BINARY_INSTRUCTION_WRITE

写指令数据

其中,星号标记的指令不属于标准GUTTA通讯指令,由于GUTTA通讯协议中,PLC程序是分为系统块、数据块、程序块3部分下载的,同时程序块又划分为常数、参数、指令3部分。这种繁琐的格式且并不适合编译型PLC。为了简单起见,这里将所有需要下载的数据综合成一个大块一起读写。

第3章 添加CPU类型

在完成仿真器固件之前,我们需要进行GUTTA Ladder Editor软件的配置。当然您也可以在完成仿真器固件之后来做这件事,或者是同时进行。由于软件的配置,需要通过XML描述语言给出PLC类型的完整定义。这个定义可以成为我们仿真器固件的实现标准。因此在这里我们先来实现好定义,然后在下一章来讨论如何实现仿真器固件。 正如文档《IN1004 GUTTA跨平台的实现细节》所指出的,在GUTTA Ladder Editor软件中配置一个全新的PLC类型,需要创建下面5个文件:

1. 基本配置文件:PlcType.XML。 2. 指令配置文件:ManagerFun.XML。 3. 内存配置文件:ManagerVar.XML。 4. 系统块编辑器:SystemBlock.DLL。 5. 下载器:Communication.DLL。 每个文件的作用请详细参考文档《IN1004 GUTTA跨平台的实现细节》。具体到我们这个PLC最小系统,PlcType.XML、ManagerFun.XML、ManagerVar.XML都可以用Windows自带的记事本阅读和编辑。SystemBlock.DLL和Communication.DLL是两个可以被GUTTA Ladder Editor软件调用的动态链接库。一方面由于修改这两个文件需要一定的软件知识;另一方由于面目前还没有解释这两个动态库接口的文档,因此这里我们暂时不做修改,而是采用仿真器CPU-EC20 (8051,Compile) PLC类型下默认这两个文件。所以我们的新PLC类型系统块数据的定义和CPU-EC20 (8051,Compile)一致。至于Communication.DLL,由于这个库文件的行为还可以通过同文件夹下的CompileInfor.XML文件进行配置,我们可以通过这个配置文件方便的添加指令。 哦,在开始动手之前,需要先给我们的PLC类型取个好听的名字。就叫CPU-EC20-M(C)吧,完整信息名为CPU-EC20 (Mini,Compile)。大意就是:我一个迷你系统,编译型! 由于CPU类型的识别码(名称)不一致,考虑到软件兼容性,我们还是需要修改CPU-EC20 (8051,Compile)下这两个动态链接库的识别码(名称)部分。其实也就是将SystemBlock.DLL和Communication.DLL这两个项目的文件中,所有字符串CPU-EC20-51(C)修改为CPU-EC20-M(C)然后重新编译即可。修改好的这两个动态链接库在项目光盘中提供。我们只要知道,本项目的SystemBlock.DLL和Communication.DLL这两个文件和CPU-EC20 (8051,Compile)类型下的这两个文件除了PLC类型识别码(名称)不一致之外,其余行为完

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

16

isibleontrol AN2103

全一致就可以了。 接下来的工作,就是拷贝修改好的SystemBlock.DLL和Communication.DLL到我们这个项目中来。复制CPU-EC20 (8051,Compile)类型下的PlcType.XML、ManagerFun.XML、ManagerVar.XML、CompileInfor.XML这4个配置文件到我们的项目中来并完成其修改。一般说来,在安装好GUTTA Ladder Editor软件的完整版本之后,安装文件夹下面会有这些文件:

打开GuttaLad文件夹,里面的每一个子文件夹都实现了一个CPU类型:

新建一个文件夹CPU-EC20 (Mini,Compile),开始我们配置之行:

配置类型(PlcType.XML)

将CPU-EC20 (8051,Compile)文件夹下的PlcType.XML文件拷贝到CPU-EC20 (Mini,Compile) 文件夹下,并用文本编辑器打开这个文件,进行下面的编辑: 将“Name”字段的值修改为“CPU-EC20-M(C)”;同时将“Information”字段的值修改为“CPU-EC20 (Mini,Compile)”。 “CPU-EC20-M(C)”是我们这个PLC的识别名,“CPU-EC20 (Mini,Compile)”是我们这个PLC的信息名。 保留“SystemBlockDll”字段为“SystemBlockDll.dll”;同时保留 “SystemBlockBinarySize”字段的值为“50”。 因为系统不支持数据块,将“DataBlockPageSize”字段的值修改为“0”;同时 将

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

17

isibleontrol AN2103

“DataBlockPageItemSize”字段的值修改为“0”。 因为系统只支持一个主循环且不支持中断函数,将“ProgramBlockPageIntSize”字段的值修改为“1”;同时将“ProgramBlockPageSbrSize”字段的值修改为“0”。 因为系统不支持临时变量,将“ProgramBlockPageArgIntSize”字段的值修改为“1”;同时将“ProgramBlockPageArgSbrSize”字段的值修改为“1”。照道理应该设置为0,但是GUTTA Ladder Editor在读到0的时候会产生逻辑错误,这里暂时设置为1。我们可以通过设置临时变量空间的长度为0,来避免用户输入临时变量。 保留“ProgramBlockConstBinarySize”字段的值为“128”。 保留“ProgramBlockInstructionBinarySize” 字段的值为“12800”。表示用户代码空间的大小为12.5K字节。 保留“CommunicationDll”字段的值为“CommunicationDll.dll”;同时保留“ExchPackSize”字段的值修改为“64”;将“ExchSupport”字段的值修改为“FF00|0100|FF00|0A00|FF00|0B00”,表示通讯系统支持0100H、0110H、0111H、0120H、0121H、0A00H、0A01H、0A02H、0A03H、

) 0B00H、0B01H、0B02H这几条指令(详细说明请参考文档《UM4001 GUTTA通讯协议》

修改过的XML文件看起来应该是这个样子:

Name=\"CPU-EC20-M(C)\"

Information=\"CPU-EC20 (Mini,Compile)\" SystemBlockDll=\"SystemBlockDll.dll\" SystemBlockBinarySize=\"50\" DataBlockPageSize=\"0\" DataBlockPageItemSize=\"0\" ProgramBlockPageIntSize=\"1\" ProgramBlockPageSbrSize=\"0\" ProgramBlockPageArgIntSize=\"1\" ProgramBlockPageArgSbrSize=\"1\" ProgramBlockConstBinarySize=\"128\"

ProgramBlockInstructionBinarySize=\"12800\" CommunicationDll=\"CommunicationDll.dll\" ExchPackSize=\"64\"

ExchSupport=\"FF00|0100|FF00|0A00|FF00|0B00\" >

保存之后,PlcType.XML就修改完毕了。为了验证这个文件的格式是否正确,我们先将CPU-EC20 (8051,Compile)文件夹下的ManagerChs子文件夹、ManagerEnu子文件夹、CommunicationDll.DLL文件、SystemBlockDll.DLL文件、CompileInfor.XML文件都拷贝到CPU-EC20 (Mini,Compile) 文件夹下(即PlcType.XML所在的文件夹)。运行我们的GUTTA Ladder Editor软件,点击主菜单PLC项下的类型命令,如果一切顺利,可以看到我们的PLC类型CPU-EC20 (Mini,Compile)已经被GUTTA Ladder Editor收录了。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 18

isibleontrol AN2103

转换类型后,项目名会以这个PLC类型为后缀:

由于我们还没有修改ManagerVar.XML文件和ManagerFun.XML文件,这个PLC类型的变量系统和指令集看起来与CPU-EC20 (8051,Compile)的没有什么变化。是的,表面上看起来,似乎就是改了个名字而已。紧接着,我们开始配置一个PLC类型最关键的两部分了:PLC变量系统和PLC指令集。

配置变量系统(ManagerVar.XML)

ManagerVar.XML文件格式标准请参考文档《UM4002 变量描述文件规范》。由前面章节的内存规划我们知道,我们的这个PLC系统有5个用户可用的变量域,即I、Q、AI、AQ、M。根据这5个变量域自身的基本属性,我们可以写出如下描述:

Comment=\"Discrete inputs\">

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

19

isible

ontrol AN2103

Comment=\"Discrete outputs\">

Comment=\"Analog inputs\">

Comment=\"Analog outputs\">

Use=\"Address|Value|Pointer\" Comment=\"Internal memory\">

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

20

isibleontrol AN2103

Comment=\"Const memory\">

Use=\"Address|Value|Pointer\" Comment=\"Local variable memory\">

每个字段的定义在《UM4002 变量描述文件规范》里面都有详细的说明。细心的读者可能会发现,除了我们最开始规划的I、Q、AI、AQ、M这5个区域,我们在描述文件里面还添加了K区域和L区域。I、Q、AI、AQ、M这5个区域都分布在所谓的DI、DO、RI、RO区间上,即MODBUS定义的数字量输入(1x)、数字量输出(0x)、模拟量输入(3x)、模拟量输出(4x)。除了这几个区域,我们还需要定义一个常数区域“K”(GUTTA Ladder Editor要求必须定义)。那么这个域名为“K”的常数区域是做什么用的呢?这是因为GUTTA Ladder Editor出于效率的考虑,指令中不能带常数。例如我们在程序中书写MOVW 16#1234 MW10指令时,GUTTA Ladder Editor会将这条指令自动处理成MOVW KW?, MW10。这里的“?”号表示地址暂时不能确定。GUTTA Ladder Editor在编译程序的时候会将程序中出现的所有常数数据进行压缩,然后根据压缩好的常数区域来确定每条指令的KW?(偏移量)。这也就意味着PLC在启动后,运行程序前,需要首先初始化这个所谓的常数区域。目前我们给常数区域K分配了128个字节的空间,这也就意味着,我们在程序中能够使用的常数数据是有限的,常数数据压缩后不能超过128个字节。 除了多出来的K区间,最后我们还定义了L区间。我们的这个最小系统并不支持临时变量,但是由于GUTTA Ladder Editor要求必须定义,于是我们就定义一个长度为0的区间,这样一方面避免了GUTTA Ladder Editor软件报错,同时保证了用户在输入任何L变量都是

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

21

isibleontrol AN2103

非法的,软件GUTTA Ladder Editor会产生编译错误。

配置指令集(ManagerFun.XML)

ManagerFun.XML文件格式标准请参考文档《UM4003 指令描述文件规范》。由前面章节的指令规划我们知道,我们的这个PLC系统有4大类指令,即基本的逻辑指令、计数器指令、定时器指令、数据移动指令。由于ManagerFun.XML文件体积很大,这里就不具体展示了,读者可以通过参照文档《UM4003 指令描述文件规范》来查看光盘中附带的ManagerFun.XML文件并细细品味。 需要注意的是,不论是ManagerVar.XML文件还是ManagerFun.XML文件,都存在中文和英文两个版本,分别位于ManagerChs文件夹和ManagerEnu文件夹下。如果不需要考虑多语言版本的支持,我们保持两个文件夹下的文件完全一致即可。

CPU类型的测试

完成上面的工作后,PLC的配置就基本完成了。如果以后需要添加新的PLC变量域或者是PLC指令,只需要修改对应的ManagerVar.XML文件和ManagerFun.XML文件即可。上面提到了Communication.DLL的配置文件CompileInfor.XML,这个文件的修改会在我们完成好了PLC系统固件之后详细说明。CompileInfor.XML详细定义了C语言描述文件的生成规则,编译器的调用等。对于编译型PLC,在PLC类型文件夹下面一般还有一个文件夹CompileFiles。这个文件夹用于存放所有的编译文件以及中间文件,其中最重要的文件是编译头文件swap_auto.h,同样,这个文件的修改会在会在我们完成好了PLC系统固件之后详细说明。 虽然没有完全完成软件的配置,但是至少GUTTA Ladder Editor已经能够识别我们的最小系统CPU-EC20 (Mini, Compile)了。我们可以在这个PLC类型下进行简单的PLC程序编辑,并且完成指令表到梯形图的互相转化。

第4章 完成仿真器固件

熟悉我们的编译器

由于我们要实现一个编译型的PLC,因此,除了PLC系统固件的一次性编译,每次PLC的使用者在编写完PLC程序后,下载之前,都还要对用户程序再进行一次编译。GUTTA Ladder Editor软件先根据PLC程序生成对应的C语言描述,并调用编译器将对应的C语言编译链接成处理器可以执行的二进制代码。GUTTA Ladder Editor编译完成后再与PLC系统固件进行通讯,PLC系统固件利用单片机的IAP功能,将二进制代码写入单片机的FLASH中。PLC系统固件在确认二进制代码更新完成后,将会根据需要执行二进制代码。用户在GUTTA Ladder Editor上用梯形图编辑的逻辑,才最终被PLC实现。由此可见,在编译型PLC模式下,每次梯形图的下载,都需要调用编译器,对编译器的掌握和灵活运用,至关重要。 下面对我们的C编译器SDCC做一些简单的介绍,如果您对这款编译器很熟悉,可以

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 22

isibleontrol AN2103

跳过这一章。如果您需要了解更全面的内容,请参考SDCC安装好后自带的帮助手册:《SDCC Compiler User Guide》。

SDCC提供源代码形式的发布和二进制形式的发布。如果没有特别的需要,可以直接选择二进制形式的安装,在Windows下,二进制的安装差不多就是将需要用到的文件解压到指定的文件夹中。例如将SDCC安装在文件夹“E:\\Program Files\\SDCC”下,那么您需要确保环境变量PATH中包含“E:\\Program Files\\SDCC\\bin”这个字符串。至于其它环境变量,可以不用修改,用SDCC的默认值即可。(可以通过右键单击桌面上我的电脑图标,在属性菜单项弹出的话框高级页中找到环境变量设置按钮。) 如果安装了GUTTA Ladder Editor软件并且安装了PLC类型CPU-EC20 (8051,Compile)。这个PLC类型文件夹下有SDCC编译器的一份拷贝,在文件夹CPU-EC20 (8051,Compile)\\ CompileFiles\\SDCC文件夹下。对于我们这个PLC类型CPU-EC20 (Mini,Compile),有两种选择,要么也将SDCC拷贝一份到CPU-EC20 (Mini,Compile)\\CompileFiles\\SDCC文件夹下,要么在电脑的环境变量中设置PATH变量,共用1份SDCC。方法在解释CompileInfor.XML文件的时候会有说明。

编译单个文件

编译单个文件十分简单,只需要在命令行下执行“sdcc sourcefile.c”就可以了。这一条命令完成了编译、汇编、链接3个操作,同时输出以下文件: z sourcefile.asm 编译器生成的汇编文件。 z sourcefile.lst 编译器生成的汇编信息。

z sourcefile.rst 被连接器更新后的汇编信息。 z sourcefile.sym 包含所有符号的定位地址。

z sourcefile.rel 编译器生成的目标文件,之后传递给连接器。 z sourcefile.map 按照符号地址排列的内存使用情况。 z sourcefile.mem 内存使用情况的总体描述。

z sourcefile.ihx 最终生成的Intel Hex二进制数据文件。

Intel Hex文件的对齐

需要注意的是,SDCC生成的Intel Hex文件每行不是等长的。这是符合文件标准且在大多数场合可以使用。不过某些软件可能会发生兼容性的问题,如有必要,可以使用“packihex”命令来对齐SDCC生成的Intel Hex文件: packihx sourcefile.ihx >sourcefile.hex

编译多个文件

如果项目比较大,一个目标程序可能由多个源文件生成。和绝大部分编译器一样,SDCC可以先单个单个的编译源文件。先由源文件生成对应的目标文件,然后利用连接器将多个目标文件链接成一个程序。假设我们的项目有这么几个文件: foo1.c (包含一些函数的实现)

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

23

isibleontrol AN2103

foo2.c (包含一些函数的实现) foomain.c (包含一些函数及程序入口main函数的实现) 我们可以先单独编译前面的两个源文件: sdcc -c foo1.c sdcc -c foo2.c 然后编译包含程序入口main函数的文件,同时将这个文件和前面编译好的2个文件一起链接: sdcc foomain.c foo1.ref foo2.ref 另一种替代的方案是,独立编译包含含main函数的文件,然后将这3个文件一起链接: sdcc -c foomain.c sdcc foomain.ref foo1.ref foo2.ref 由于连接器是按照目标文件(*.ref)的顺序安排所有函数的位置,因此必须保证包含程序入口main函数的目标文件出现在目标文件列表的首位。

命令行参数

1. 处理器参数:

z -mmcs51 生成Intel MCS51单片机代码,编译器的默认选项。 关于其它处理器的支持这里就不介绍了,感兴趣话可以去查阅SDCC的相关文档。 2. 编译参数

z -I 当编译器寻找“<..h>”中的“.h”文件时,会在这个附加的路径中去寻找。 z -D 将一个宏定义成某值,并传递给编译器。

z -M 生成一段文本描述目标文件的依赖关系,这段文本用于makefile文件。编译器

分析源文件所包含的所有“#include”项,并对其进行递归展开,以帮助make在修改部分源文件时分析得到需要重新编译的目标文件。“-M”选项通过“-E”选项来实现。

z -E 对当前源文件进行展开,参数将所有的“#include”包含文件展开,并将其显示

在标准输出上。

z -C 告诉编译器在执行“-E”参数时不要忽略注释文件。

z -MM 功能和“-M”类似,考虑到系统文件一般是不变的,本命令生成的依赖关系

不对“#include ”进行展开。 z -Umacro 删除某个宏的定义。 3. 链接参数

z -L --lib-path 指定库文件的查找路径,这个路径必须是绝对路径。库文件在

命令行中指定。

z --xram-loc 外部RAM的开始地址,默认值是0。地址的值可以是16进制,

也可以是10进制,例如--xram-loc 0x8000或--xram-loc 32768。

z --code-loc 代码段的开始地址,默认值是0。需要注意的是,当这个值被指

定后,中断向量表也随之移动。地址的值可以是16进制,也可以是10进制,例如--code-loc 0x8000或--code-loc 32768。

z --stack-loc 在默认情况下,栈的开始地址紧随data段后(使用所有的空闲

的内部RAM)。使用这个参数可以将栈的开始地址指定到任意内部RAM地址上。地址的值可以是16进制,也可以是10进制,例如--stack-loc 0x20或--stack-loc 32。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

24

isibleontrol AN2103

由于寄存器SP是先加,后压数据,故SP的初始化值实际上比参数提供的值小1个字节的偏移。如果自己指定栈的开始地址,需要确保栈在增长后,不与其它数据段重叠。

z --xstack-loc 在默认情况下,外部栈的开始地址紧随pdata段后(使用所有

的空闲外部RAM)。使用这个参数可以将栈的开始地址指定到任意外部RAM地址上。地址的值可以是16进制,也可以是10进制,例如--xstack-loc 0x8000或--xstack-loc 32768。如果自己指定外部栈的开始地址,需要确保外部栈在增长后,不与其它数据段重叠。

z --data-loc 指定内部RAM的起始地址。地址的值可以是16进制,也可以是

10进制,例如--data-loc 0x20或--data-loc 32。在默认情况下,内部RAM的起始地址在不与寄存器组和内部位寻址RAM冲突的情况下,被设置得尽量小。例如我们只使用了寄存器组BANK0和BANK1,同时没有使用任何位变量,在--data-loc参数没有被使用的情况下,起始地址被设置为0x10。

z --idata-loc 指定间接寻址内部RAM的起始地址。地址的值可以是16进制,

也可以是10进制,例如--idata-loc 0x88或--idata-loc 136。

z --bit-loc 指定内部位寻址RAM的起始地址。并未被实现。 z --out-fmt-ihx 输出代码的格式为Intel Hex格式。这个是默认选项。 z --out-fmt-s19 输出代码的格式为Motorola S19格式。 z --out-fmt-elf 输出代码的格式为ELF格式。 4. MCS51参数

z --model-small 使用小型程序模式,这个是默认选项。 z --model-medium 使用中型程序模式,如果这个参数被使用,那么所有源文件的编译

都需要使用这个参数,同时连接器也要使用这个参数。

z --model-large 使用大型程序模式,如果这个参数被使用,那么所有源文件的编译都

需要使用这个参数,同时连接器也要使用这个参数。 z --xstack 这个选项将把栈放入外部RAM中,默认情况下栈位于内部RAM的空闲区

域中(小于256字节)。

z --iram-size 这个参数将使编译器进行内部RAM的大小检查。 z --xram-size 这个参数将使编译器进行外部RAM的大小检查。 z --code-size 这个参数将使编译器进行代码段的大小检查。 z --stack-size 这个参数将使编译器进行栈大小的检查。

z --acall-ajmp 将3字节指令lcall/ljmp替换成2字节指令acall/ajmp。只有在代码段在

2K以内,或者某些8051系统不支持lcall/ljmp指令时使用。

变量空间分配的扩展

1. data/near 被修饰的变量被分配在直接寻址内部RAM中。在小型程序模式下,这个是变量的默认分配。下面是一个8051的例子: __data unsigned char test_data; 将0x01写入这个变量将使编译器生成如下代码: mov _test_data, #0x01 2. xdata/far

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

25

isibleontrol AN2103

被修饰的变量被分配在外部RAM中。在大型程序模式下,这个是变量的默认分配。下面是一个8051的例子: __xdata unsigned char test_xdata; 将0x01写入这个变量将使编译器生成如下代码: mov dptr, #_test_xdata mov a,#0x01 movx @dptr, a 3. idata 被修饰的变量被分配在间接寻址内部RAM中。下面是一个8051的例子: __idata unsigned char test_idata; 将0x01写入这个变量将使编译器生成如下代码: mov r0, #_test_idata mov @r0, #0x01 4. code 被修饰的变量将被分配在代码段中(一般为常数数据)。 __code unsigned char test_code; 访问这个变量将使编译器生成如下代码: mov dptr, #_test_code clr a movc a, @a+dtpr 5. bit 被修饰的变量将被分配在位寻址内部RAM中。下面是一个8051的例子: __bit test_bit; 置位这个变量将使编译器生成如下代码: setb _test_bit 6. sfr/sfr16/sfr32/sbit 用于定义特殊功能寄存器或特殊位。

基本定义

和本工程所有相关的宏定义都位于plc_type.h文件中。首先是基本数据类型定义:

typedef signed char int8_t; typedef unsigned char uint8_t; typedef signed int int16_t; typedef unsigned int uint16_t; typedef signed long int32_t; typedef unsigned long uint32_t;

由于C标准没有明确规定int、long等数据类型的数据宽度,因此其宽度根据编译器和处理器的不同而不同。具体到我们这个PLC单片机系统,我们希望所有的数据宽度都是精确可控的,于是,根据SDCC文档对int、long等数据类型的规定,做出了上面定义。

z int8_t表示有符号8位整型 z uint8_t表示无符号8位整型 z int16_t表示有符号16位整型

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

26

isibleontrol AN2103

z uint16_t表示无符号16位整型 z int32_t表示有符号32位整型 z uint32_t表示无符号32位整型 在后面,所有的数据都不会采用int、long这种宽度和编译器相关的关键字来定义数据。同时出于效率的考虑,引入了寄存器宽度定义:

typedef uint8_t register_t; typedef uint8_t register8_t; typedef uint16_t register16_t; typedef uint32_t register32_t; typedef uint16_t flash_addr_t; #define __packed

register_t表示定义一个寄存器宽度的无符号整型。在8位处理器的51单片机中,寄存器的宽度就是8位。register8_t表示定义一个至少8位宽度的数据,如果寄存器的数据宽度大于8位,则采用寄存器宽度,否则依然使用8位宽度。register16_t和register32_t依此类推。使用这个定义主要是为了在8位系统向16位系统移植的时候,避免16位系统做出不必要的数据裁减(向32位系统移植也有这个问题)。由于我们的最小PLC系统是基于8位处理器的51单片机,故register_t定义为uint8_t。register8_t定义为uint8_t。reigster16_t定义为uint16_t。同理register32_t定义为uint32_t。flash_addr_t表示用来表示FLASH地址的数据类型。我们知道,51单片机最多有64K的FLASH空间,也就是16位的地址宽度。故flash_addr_t定义为uint16_t。 然后是与PLC类型相关的定义,其实也就是PlcType.XML文件字段值的拷贝:

//File: \"PlcType.xml\"

///////////////////////////////////////////////////////////////////////////////

#define PLC_TYPE_NAME \"CPU-EC20-M(C)\"

#define PLC_TYPE_INFORMATION \"CPU-EC20 (Mini,Compile)\"#define PLC_TYPE_SYSTEM_BLOCK_BINARY_SIZE 50

#define PLC_TYPE_SYSTEM_BLOCK_DLL \"SystemBlockDll.dll\" #define PLC_TYPE_DATA_BLOCK_PAGE_SIZE 0 #define PLC_TYPE_DATA_BLOCK_PAGE_ITEM_SIZE 0 #define PLC_TYPE_PROGRAM_BLOCK_PAGE_INT_SIZE 1 #define PLC_TYPE_PROGRAM_BLOCK_PAGE_SBR_SIZE 0 #define PLC_TYPE_PROGRAM_BLOCK_PAGE_ARG_INT_SIZE 1 #define PLC_TYPE_PROGRAM_BLOCK_PAGE_ARG_SBR_SIZE 1 #define PLC_TYPE_PROGRAM_BLOCK_CONST_BINARY_SIZE 128 #define PLC_TYPE_PROGRAM_BLOCK_INSTRUCTION_BINARY_SIZE 12800

#define PLC_TYPE_COMMUNICATION_DLL \"CommunicationDll.dll\" #define PLC_TYPE_EXCH_PACK_SIZE 64 #define PLC_TYPE_EXCH_SUPPORT \"FF00|0100|FF00|0A00|FF00|0B00\"

///////////////////////////////////////////////////////////////////////////////

然后是每个变量域的宏定义:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 27

isibleontrol AN2103

//File: \"ManagerVar.xml\"

///////////////////////////////////////////////////////////////////////////////

#define VAR_I_SLOT (0) #define VAR_I_BEGIN (0) #define VAR_I_END (16)

#define VAR_I_SIZE (VAR_I_END - VAR_I_BEGIN) // =16

#define VAR_Q_SLOT (1) #define VAR_Q_BEGIN (0) #define VAR_Q_END (16)

#define VAR_Q_SIZE (VAR_Q_END - VAR_Q_BEGIN) // =16

#define VAR_AI_SLOT (2) #define VAR_AI_BEGIN (0) #define VAR_AI_END (16)

#define VAR_AI_SIZE (VAR_AI_END - VAR_AI_BEGIN) //=16

#define VAR_AQ_SLOT (3) #define VAR_AQ_BEGIN (0) #define VAR_AQ_END (16)

#define VAR_AQ_SIZE (VAR_AQ_END - VAR_AQ_BEGIN) //=16

#define VAR_M_SLOT (4) #define VAR_M_BEGIN (16) #define VAR_M_END (464)

#define VAR_M_SIZE (VAR_M_END - VAR_M_BEGIN) //=448

#define VAR_K_SLOT (5) #define VAR_K_BEGIN (0) #define VAR_K_END (128)

#define VAR_K_SIZE (VAR_K_END - VAR_K_BEGIN) //=128

///////////////////////////////////////////////////////////////////////////////

其中,VAR_I_SLOT表示I变量域的内部识别号(只在解释系统中有用)。VAR_I_BEGIN表示I变量域在DI中的起始地址,VAR_I_END表示I变量域在DI中的结束地址(不包含)。VAR_I_SIZE表示I变量域一共包含多少个字节。其余变量域的定义和变量域I类似。 接下来,是所有指令集的宏定义:

//File: \"ManagerFun.xml\"

///////////////////////////////////////////////////////////////////////////////

#define FUN_USE_BIT_LOGIC #define FUN_USE_COUNTERS #define FUN_USE_MOVE

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

28

isibleontrol AN2103

#define FUN_USE_TIMERS

#ifdef FUN_USE_BIT_LOGIC

#define FUN_INS_ALD 0 #define FUN_INS_OLD 1 #define FUN_INS_LPS 2 #define FUN_INS_LRD 3 #define FUN_INS_LPP 4 #define FUN_INS_LD 5 #define FUN_INS_A 6 #define FUN_INS_O 7 #define FUN_INS_LDN 8 #define FUN_INS_AN 9 #define FUN_INS_ON 10 #define FUN_INS_NOT 11 #define FUN_INS_EU 12 #define FUN_INS_ED 13 #define FUN_INS_C 14 #define FUN_INS_S 15 #define FUN_INS_R 16 #endif // FUN_USE_BIT_LOGIC

#ifdef FUN_USE_COUNTERS

#define FUN_INS_CTU 86 #define FUN_INS_CTD 87 #define FUN_INS_CTUD 88 #endif // FUN_USE_COUNTERS

#ifdef FUN_USE_MOVE

#define FUN_INS_MOVB 119 #define FUN_INS_MOVW 120 #define FUN_INS_MOVD 121 #endif // FUN_USE_MOVE

#ifdef FUN_USE_TIMERS

#define FUN_INS_TON 149 #define FUN_INS_TONR 150 #define FUN_INS_TOF 151 #endif // FUN_USE_TIMERS

///////////////////////////////////////////////////////////////////////////////

宏的值表示当前指令的识别号。最后是两个中断开关的宏:

// Other:

////////////////////////////////////////////////////////////////////////////

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

29

isible///

ontrol AN2103

#define IntEnable() EA = 1 #define IntDisable() EA = 0

///////////////////////////////////////////////////////////////////////////////

宏IntEnable表示允许中断,宏IntDisable表示禁止中断。

硬件系统 内存

在本项目中,内存的分配通过结构体来实现。文件plc_res.h包含了所有的结构体定义并且声明了一个全局结构体变量theRes。文件plc_res.c没有定义任何函数的具体实现,仅仅只是定义了全局结构体变量theRes。 全局结构体变量theRes又由若干个子结构构成,如代码所示:

typedef struct _RES_BLOCK { RES_BLOCK_DI itDI; RES_BLOCK_DO itDO; RES_BLOCK_RI itRI; RES_BLOCK_RO itRO; RES_BLOCK_CONST itConst; RES_BLOCK_SYS itSys; RES_BLOCK_LGC itLgc; RES_BLOCK_STATE itState; RES_BLOCK_PARAM itPar[4]; } RES_BLOCK;

__xdata extern RES_BLOCK theRes;

由前面的内存规划我们可以知道,所有的系统全局变量和PLC用户变量都必须分配在外部存储空间上(XRAM)。故这里使用了关键字“__xdata”显式的要求编译器将这个结构体分配在分配在XRAM上。同时,根据我们的规划,XRAM的地址0000H到01FFH为用户变量对应的5个基本域I、Q、AI、AQ、M。因此,我们将itDI、itDO、itRI、itRO这4个子结构放在结构体的最前面。 本来寄希望使用“__at(0x0000)”关键字将这个结构体定义在地址0000H上。经过实践发现一旦使用“__at(0x0000)”关键字,SDCC编译器就不会为这个结构体分配空间!其他任何未指定地址的全局变量都有可能覆盖在0000H地址上。不过SDCC编译器似乎是按照全局变量的声明顺序来排列空间的,因此还是很容易通过调整头文件顺序使theRes结构体落在地址0000H上,这一点可以通过SDCC的*.map文件来确认。

RES_BLOCK_DI、RES_BLOCK_DO、RES_BLOCK_RI、RES_BLOCK_RO这4个结构体分别代表了数字量输入区间、数字量输出区间、模拟量输入区间、模拟量输出区间。

typedef struct _RES_BLOCK_DI {

uint8_t itI[VAR_I_SIZE];

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

30

isibleontrol AN2103

} RES_BLOCK_DI;

typedef struct _RES_BLOCK_DO {

uint8_t itQ[VAR_Q_SIZE]; } RES_BLOCK_DO;

typedef struct _RES_BLOCK_RI {

uint8_t itAI[VAR_AI_SIZE]; } RES_BLOCK_RI;

typedef struct _RES_BLOCK_RO {

uint8_t itAQ[VAR_AQ_SIZE]; uint8_t itM[VAR_M_SIZE]; } RES_BLOCK_RO;

RES_BLOCK_SYS是我们的系统变量结构体。itReset用于配置复位时需要进行哪些操作。itState用于记录PLC的基本状态,例如运行、停止、出错等。itTimer用于记录这次主循环扫描相对上次主循环扫描经过的时间(ms为单位)。定时器指令就是通过这个值来进行时间分析。

typedef struct _RES_BLOCK_SYS { register_t itReset; register_t itState; register_t itScanLeft; register16_t itTimer; } RES_BLOCK_SYS;

RES_BLOCK_LGC是我们的逻辑变量结构体。这个结构体只有一个变量itError。itError用于记录逻辑错误的错误号。

typedef struct _RES_BLOCK_LGC { register_t itError; } RES_BLOCK_LGC;

RES_BLOCK_STATE是我们的状态变量结构体。这个结构体有两个16位的变量。itStackData就是PLC程序的数据位栈;itStackLogic就是PLC程序的辅助位栈。位栈的增长和减少都通过数据的左移和右移来实现。16位变量的第0位就是栈顶的值。若栈的数据超过16个位,最先压入栈的位会被丢弃。

typedef struct _RES_BLOCK_STATE { register16_t itStackData; register16_t itStackLogic; } RES_BLOCK_STATE;

RES_BLOCK_PARAM是我们的参数缓冲变量结构体。虽然在编译型PLC中每一条指令的执行实际上都是一个函数的调用,但是为了保持一致性,指令的参数并不是作为调用函数的参数传递的,而是把它放在一个特殊的缓冲中。缓冲中每个一个参数就是一个RES_BLOCK_PARAM结构体。这个结构体有2个数据。一个指向XRAM(使用了“__xdata”关键字)的指针,一个位偏移量(如果是位变量)。

typedef struct _RES_BLOCK_PARAM {

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 31

isibleontrol AN2103

__xdata uint8_t* itPtr; register_t itBit; } RES_BLOCK_PARAM;

整个系统的内存就基本上声明完了,最后在plc_res.c文件中具体定义:

#include \"plc_includes.h\"

// ------ Public Member Define ------ __xdata RES_BLOCK theRes;

// ------ Protected Member Define ------

// ------ Protected Function Declare ------

// ------ Public Function Define ------

// ------ Protected Function Define ------

这里去掉了头文件的extern关键字,于是编译器真正为这个结构体变量分配空间。

I/O操作

在单片机程序设计中,一切与单片机相关的数据交换都可以归纳为I/O操作中。例如模拟量的读取、I2C的操作、扩展模块的识别和读写等。出于简单的考虑,我们这个最小系统中只实现最简单的一种I/O形式,即数字量输入和数字量输出。具体到我们的仿真学习板,我们PLC系统能够读取10个按键(4个轻触按键、6个自锁按键)的状态,写入输入映射区I;并通过输出映射区Q,控制6个发光二极管的亮灭。 我们先看看文件plc_io.h中函数的声明:

#ifndef __hdw_io_h__ #define __hdw_io_h__

// ------ Macro ------

// ------ Define ------

// ------ Public Memeber Declare ------

// ------ Public Function Declare ------ void IOInit(void); void IOUninit(void); void IOReset(void);

void IOGetInput(void);

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

32

isibleontrol AN2103

void IOSetOutput(void);

void IOSetRun(register_t arVal); void IOSetCom(register_t arVal); void IOSetErr(register_t arVal);

void IOScan(void);

void IOPort0TxEnable(void); void IOPort0TxDisable(void);

#endif //__hdw_io_h__

函数IOInit执行I/O硬件的初始化,函数IOUninit执行I/O硬件的释放,函数IOReset执行I/O硬件的重新配置。当PLC程序的配置数据发生了改变,就需要调用函数IOReset来重新配置硬件。在我们的这个最小PLC系统中,由于只操作51单片机的基本I/O管脚,也没有配置数据,故函数IOInit、IOUninit、IOReset都是空函数。 函数IOGetInput用于在执行主循环扫描之前将当前的单片机输入管脚状态刷新到数字量输入映射区域I中。函数IOSetInput用于在执行主循环扫描之后将当数字量输出映射区域Q中的数据刷新为单片机输出管脚的状态。

void IOGetInput(void) { // I0.0 P3_3 = 1;

_BS(theRes.itDI.itI[0], 0, !P3_3);

// I0.1 P3_4 = 1;

_BS(theRes.itDI.itI[0], 1, !P3_4);

// I0.2 P0_0 = 1;

_BS(theRes.itDI.itI[0], 2, !P0_0);

// I0.3 P0_1 = 1;

_BS(theRes.itDI.itI[0], 3, !P0_1);

// I0.4 P0_2 = 1;

_BS(theRes.itDI.itI[0], 4, !P0_2);

// I0.5 P0_3 = 1;

_BS(theRes.itDI.itI[0], 5, !P0_3);

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

33

isible // I1.0

ontrol AN2103

P2_0 = 1;

_BS(theRes.itDI.itI[1], 0, !P2_0);

// I1.1 P2_1 = 1;

_BS(theRes.itDI.itI[1], 1, !P2_1);

// I1.2 P2_2 = 1;

_BS(theRes.itDI.itI[1], 2, !P2_2);

// I1.3 P2_3 = 1;

_BS(theRes.itDI.itI[1], 3, !P2_3); }

void IOSetOutput(void) { // Q0.0

if (theRes.itDO.itQ[0] & _BV(0)) P2_4 = 0; else

P2_4 = 1;

// Q0.1

if (theRes.itDO.itQ[0] & _BV(1)) P2_5 = 0; else

P2_5 = 1;

// Q0.2

if (theRes.itDO.itQ[0] & _BV(2)) P2_6 = 0; else

P2_6 = 1;

// Q0.3

if (theRes.itDO.itQ[0] & _BV(3)) P2_7 = 0; else

P2_7 = 1;

// Q0.4

if (theRes.itDO.itQ[0] & _BV(4))

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

34

isibleontrol AN2103

P0_7 = 0; else

P0_7 = 1;

// Q0.5

if (theRes.itDO.itQ[0] & _BV(5)) P0_6 = 0; else

P0_6 = 1; }

函数IOSetRun、IOSetCom、IOSetErr分别用于设置运行、通讯、错误这3个发光二极管的亮与灭。

void IOSetRun(register_t arVal) { if (arVal) P3_5 = 0; else

P3_5 = 1; }

void IOSetCom(register_t arVal) { if (arVal) P3_6 = 0; else

P3_6 = 1; }

void IOSetErr(register_t arVal) { if (arVal) P3_7 = 0; else

P3_7 = 1; }

这3个LED发光二极管的亮灭是独立于I/O系统的(哪怕PLC停止,也要进行状态刷新)。运行、错误这2个发光二极管的刷新独立成一个函数IOScan。通讯发光二极管则在其他地方刷新。

void IOScan(void) {

IOSetRun(_BT(theRes.itSys.itState, _STATE_RUN)); IOSetErr(_BT(theRes.itSys.itState, _STATE_ERROR)); }

最后是RS485通讯的收发控制。默认状态下,RS485为接收状态,只有在本机有数据需要发送的时候,才起用发送,字符发送结束后,关闭发送。起用发送和关闭发送分别使用函数IOPortTxEnable和IOPortTxDisable来控制MAX485芯片的发送使能管脚。

void IOPort0TxEnable(void) {

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 35

isibleontrol AN2103

P4_3 = 1; }

void IOPort0TxDisable(void) { P4_3 = 0; }

闪存管理

通过前面的运行方式的介绍我们知道,编译型的PLC程序是分成两大部分的。第一部分是PLC系统固件程序,第二部分是PLC用户程序。第一部程序负责整个PLC系统并且能够与上位机软件GUTTA Ladder Editor通讯从而更新第二部分程序。在PLC正常运行的时候,第一部程序又可以转跳到第二部分程序去执行用户的主循环扫描逻辑。这就要求单片机必须支持所谓的IAP(In Application Programming),即在应用编程。 目前以FLASH闪存作为指令代码存储媒介的单片机已经越来越普及,其中大部分支持IAP。也就是说,单片机在FLASH上执行程序的同时能够修改自己的FLASH数据。宏晶的51系列单片机中,一部分型号是将FLASH分为两大部分,一部分用于执行单片机指令代码,另一部分用于存放用户数据。这两部分FLASH空间是独立寻址的,读取FLASH中的用户数据需要操作特殊的寄存器。单片机也不能运行在FLASH中的用户数据空间上。另一部分分型号不区分程序空间和用户空间,修改过的FLASH空间同样也可以运行单片机程序。我们选用的IAP12C5A60AD就属于后者。 在宏晶的文档《STC12C5A60AD系列单片机手册》中,没有对IAP型号做充分的说明。而在选型手册中可以看出,IAP12C5A60AD还是有2K的用户数据区空间的。所以总的FLASH空间大小还是62K。这62K的编程和STC单片机一致,区别只在于最后的2K空间不能够运行程序。单片机的程序一旦进入这2K区域,单片机会自动复位。 文件plc_flash.h和文件plc_flash.c这主要实现了FLASH的基本操作。包括对IAP寄存器操作的封装、整个FLASH空间的分区、用户FLASH空间的数据擦除、数据写入等基本操作。 和I/O操作一样,FLASH的声明也有FlashInit、FlashUinit、FlashReset这3个函数,分别进行FLASH的硬件初始化、硬件释放、硬件重新配置这3种操作。在我们这个最小系统中,由于我们没有使用FLASH缓冲机制,FLASH的操作相对简单,故这3个函数都是空函数。 首先我们看看IAP操作的基本封装:

//

uint8_t __iap_byte_read(flash_addr_t arAddr);

void __iap_byte_program(flash_addr_t arAddr, uint8_t arData); void __iap_sector_erase(flash_addr_t arAddr); //

这3个函数对IAP12C5A60AD的3种FLASH操作(读FLASH数据、写FLASH数据、擦除FLASH页)进行了封装。IAP12C5A60AD的FLASH操作需要通过读写特殊的寄存器来进

。 行,具体方法请参考宏晶的官方文档《STC12C5A60AD系列单片机器件手册》

//

#define IAP_WAIT_TIME 3

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

36

isibleontrol AN2103

#define IAP_BYTE_READ 1 #define IAP_BYTE_PROGRAM 2 #define IAP_SECTOR_ERASE 3

uint8_t __iap_byte_read(flash_addr_t arAddr) { IAP_ADDRL = ((arAddr>>0)&0xff); IAP_ADDRH = ((arAddr>>8)&0xff); IAP_CONTR = 0x83; IAP_CMD = 0x01; IAP_TRIG = 0x5a; IAP_TRIG = 0xa5; return IAP_DATA; }

void __iap_byte_program(flash_addr_t arAddr, uint8_t arData) { IAP_ADDRL = ((arAddr>>0)&0xff); IAP_ADDRH = ((arAddr>>8)&0xff); IAP_DATA = arData; IAP_CONTR = 0x83; IAP_CMD = 0x02; IAP_TRIG = 0x5a; IAP_TRIG = 0xa5; }

void __iap_sector_erase(flash_addr_t arAddr) { IAP_ADDRL = ((arAddr>>0)&0xff); IAP_ADDRH = ((arAddr>>8)&0xff); IAP_CONTR = 0x83; IAP_CMD = 0x03; IAP_TRIG = 0x5a; IAP_TRIG = 0xa5; }

//

代码中的IAP_DATA、IAP_ADDRL、IAP_ADDRH、IAP_CONTR、IAP_CMD、IAP_TRIG都是特殊寄存器变量。根据宏晶的手册在文件stc12c5a60s2.h中定义:

__sfr __at(0xC2) IAP_DATA; __sfr __at(0xC3) IAP_ADDRH; __sfr __at(0xC4) IAP_ADDRL; __sfr __at(0xC5) IAP_CMD; __sfr __at(0xC6) IAP_TRIG; __sfr __at(0xC7) IAP_CONTR;

下面我们看看我们的FLASH的分区。首先,我们知道,FLASH至少分成了2大部分。一部分是PLC系统固件;另一部分是PLC用户程序。同时用户PLC程序也被GUTTA Ladder Editor分成了好几个部分:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

37

isibleontrol AN2103

保留配置参数A800H+0.5KA800H+000HA800H+080HA800H+100HA800H+180HSYSDATAARGJMPA800HGUTTA Ladder Editor插入常数0000H(0K)函数地址K初始化A800H+2.5KPLC系统固件A800H+4.5K中断向量启动代码__target_entrance() 调用入口A800H(42K)SDCC编译数据A800H+5.5KPLC用户程序INS函数F000H(60K)

如图所示。首先,FLASH被分成了两个部分。0000H~A7FFH这42K数据为PLC系统固件(不一定用完)。A800H~EFFFH这18K数据为PLC用户程序。这18K PLC用户程序可以通过PLC系统固件来擦除和更新。PLC用户程序又分成两部分:一部分是GUTTA Ladder Editor插入的常数数据;一部分是由SDCC编译器编译生成的代码数据。 常数数据分为SYS(系统块数据)、DATA(数据块数据)、ARG(函数参数表)、JMP(转跳地址表)这4大部分。由于本系统不支持数据块,所以DATA数据被忽略。由于本系统只有一个主循环,且不支持函数,故ARG数据被忽略。由于本系统不支持转跳指令,故JMP数据被忽略。于是在图中DATA、ARG、JMP都用灰色来表示。SYS数据被分成4个等大小的块(都是128字节),前3个块我们这个最小系统都用不到,唯一用到的就是第4个块:K初始化块。在系统复位的时候,我们需要将K初始化块的数据拷贝到我们K内存区域中去。 其中SYS的第1个保留块也不是完全没有用处,GUTTA Ladder Editor写入了两个数据,第1个地址被定义成宏_SYS_TAG_LEN,记录PLC用户程序的有效数据长度。这个长度从A800H开始,到PLC用户程序的结束(因为一般说来PLC用户程序没有使用全部的18K空间)。第2个地址被定义成宏_SYS_TAG_CRC,记录从SYS的第2个块(配置参数块)开始到PLC用户程序结束的CRC校验值。这两个16位的值在下载的时候由Communication.DLL负责加入,PLC在运行前可以通过这两个值对整个PLC用户程序进行校验,如果校验成功,才能正式进入PLC运行模式。 SDCC编译数据根据前面的分析我们知道,分为中断向量、启动代码、函数等几个区域。由于这段代码不是独立运行的,中断向量、启动代码对于我们来说是无用数据。我们唯一需要关心的就是调用的入口地址,即函数__target_entrance的地址。 也许有些读者会问,既然很多数据块我们是不用的,为什么还给他们保留FLASH空间呢,这岂不是很浪费?其实我也不想,由于我们是借用的CPU-EC20 (8051,Compile)的Communication.DLL文件(只修改了类型名称)。而这个文件是按照CPU-EC20 (8051,Compile)的配置生成SYS、DATA、ARG、JMP这4个区域的。虽然很多数据对我们来说没有用,但是我们不能够修改Communication.DLL的行为,因此保留了上述几个块的空间。如果以后您需要加入更多的功能,可以直接利用这几个块的数据,只是目前我们这个最小系统暂时不去理会这些数据而已。根据上面的分区,再来看看文件plc_flash.h中的宏定义,相信就会清晰不少。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

38

isibleontrol AN2103

关于FLASH的读写函数,读FLASH通过3个宏来实现:

#define FlashReadByte(addr) (*(__code uint8_t*)(addr)) #define FlashReadWord(addr) (*(__code uint16_t*)(addr)) #define FlashReadDword(addr) (*(__code uint32_t*)(addr))

这里使用了SDCC的关键字“__code”,告诉编译器数据位于代码空间上。最终编译器会使用“movc a, @a+dptr”指令来读取代码中的数据。实际上也可以采用IAP12C5A60AD的IAP接口来读取FLASH,其结果和使用 “movc a, @a+dptr”指令是一样的。但是采用IAP接口需要一系列的操作,加上还要进行一次函数调用,其效率远远低于这种宏的格式。其实这个IAP读数据的接口是被设计为在用户FLASH和代码FLASH独立寻址的时候使用,IAP12C5A60AD使用这个接口是多余的。 关于FLASH的读写函数,写FLASH通过3个函数来实现:

void FlashWriteByte(flash_addr_t arAddr, uint8_t arData); void FlashWriteWord(flash_addr_t arAddr, uint16_t arData); void FlashWriteDword(flash_addr_t arAddr, uint32_t arData);

它们分别是写一个字节、写一个字、写一个双字。

void FlashWriteByte(flash_addr_t arAddr, uint8_t arData) { __iap_byte_program(arAddr, arData); }

void FlashWriteWord(flash_addr_t arAddr, uint16_t arData) { __iap_byte_program(arAddr + 0, ((arData >> 0) & 0xff)); __iap_byte_program(arAddr + 1, ((arData >> 8) & 0xff)); }

void FlashWriteDword(flash_addr_t arAddr, uint32_t arData) { __iap_byte_program(arAddr + 0, ((arData >> 0) & 0xff)); __iap_byte_program(arAddr + 1, ((arData >> 8) & 0xff)); __iap_byte_program(arAddr + 2, ((arData >> 16) & 0xff)); __iap_byte_program(arAddr + 3, ((arData >> 24) & 0xff)); }

可以从函数的定义中看出,由于IAP接口一次只能写一个字节,写一个字和写一个双字无非就是进行多次写字节的操作,以达到写字和写双字的效果。 接下来我们看看这两个函数:

void FlashBegin(void); void FlashEnd(void);

这两个函数分别在进入FLASH编程和退出FLASH编程的时候调用。由于我们这个最小系统不涉及FLASH缓冲的问题,FlashBegin只需要进行PLC用户程序的擦除,而FlashEnd只是一个空函数(如果有Flash缓冲,FlashBegin还需要进行缓冲必要的初始化工作,而FlashEnd还需要将缓冲的数据写入Flash)。

void FlashBegin(void) { FlashInit(); if (1) {

flash_addr_t lcPageAddr = _FLASH_PAGE_BEGIN;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 39

isibleontrol AN2103

for (; lcPageAddr != _FLASH_PAGE_END; lcPageAddr += _FLASH_PAGE_SIZE) __iap_sector_erase(lcPageAddr); } }

void FlashEnd(void) { }

最后我们介绍一下FLASH的CRC运算函数。

#define __CRC(b,c) {\\

register_t t,i=0;\\ c^=(uint16_t)(b);\\ for(;i!=8;++i) {\\

t=c&1;c>>=1;c&=0x7ffff;\\ if(t)\\

c^=0xa001;\\ }\\ }

uint16_t FlashCrc(void) { uint16_t lcCrc = 0xffff;

flash_addr_t lcFirst = _SYS_x(0);

flash_addr_t lcLast = _FLASH_PAGE_BEGIN + FlashReadWord(_SYS_TAG_LEN); for (; lcFirst < lcLast; ++ lcFirst) __CRC(FlashReadByte(lcFirst), lcCrc); return lcCrc; }

__CRC(b,c)是一个宏,用于计算单个字节的CRC。这个函数首先创建了一个变量指向SYS数据第2个块(配置参数块)的开始地址。然后根据SYS数据第1个块记录的数据长度,计算整个PLC用户FLASH的CRC校验,并将这个值做为函数的返回值返回。

时钟节拍

PLC系统至少要有一个时钟节拍。根据这个时钟节拍,PLC系统才能够实现有确切时间要求的应用。在我们这个最小PLC系统中,就有两个地方是离不开时间判断的。第一个地方就是串口通讯的帧判断。按照MODBUS RTU的通讯协议,一个通讯帧的开始和结束是根据总线的静寂时间来判断的。第二个地方就是PLC的定时器指令。不论是延时接通还是延时断开,都需要判断时间。

51单片机有两个定时器。但是需要使用定时器的地方往往多于两个。一种简单的方法就是利用定时器产生所谓的系统节拍。其他的任何时间相关的应用都根据这个系统节拍来做判断。管理和维护这个系统节拍,是文件plc_time.h和文件plc_time.c的主要工作。同时,时钟节拍系统还完成其他一些硬件的周期性驱动(例如串口扫描),这也是PLC系统正常工作的保障。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

40

isibleontrol AN2103

我们先看看文件plc_timer.h中函数的声明:

#ifndef __plc_timer_h__ #define __plc_timer_h__

// ------ Macro ------

// ------ Define ------

// ------ Public Memeber Declare ------ typedef struct _TIMER_BLOCK { register16_t itTick; register16_t itTickLast; } TIMER_BLOCK;

__xdata extern TIMER_BLOCK theTimer;

// ------ Public Function Declare ------ void TimerInit(void); void TimerUninit(void); void TimerReset(void);

void TimerScanCheckFirst(void); void TimerScanCheck(void);

#endif //__plc_timer_h__

时钟节拍系统有个全局的结构体变量theTimer。theTimer可以被其他任何文件访问,并且位于XRAM中(下面声明为“__xdata extern”)。这个结构体包含两个16位的无符号整数数据itTick和itTickLast。itTick就是当前的时钟节拍计数,以毫秒(ms)为单位,每过1毫秒这个计数就被定时器中断服务程序加1。PLC系统复位后从0开始自加,到65535后翻转为0。即经过65536次自加后发生翻转,也就是65.536秒。theTimerLast用于记录上一次函数TimerScanCheck调用的时间,函数TimerScanCheck根据上一次的调用时间,确定当前调用和上一次的时间差,并将这个时间差更新到theRes.itSys.itTimer变量中去(theRes.itSys.itTimer的使用方法在后面会有详细说明)。 函数TimerInit执行定时器硬件的初始化,函数TimerUninit执行定时器硬件的释放,函数TimerReset执行定时器硬件的重新配置。我们这个最小系统定时器的配置不受PLC程序的影响,故函数TimerUninit和TimerReset都是空函数。

void TimerInit(void) { theTimer.itTick = 0; theTimer.itTickLast = 0;

ET0 = 1; TMOD |= T0_M0; TL0 = TL0_VALUE;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

41

isibleontrol AN2103

TH0 = TH0_VALUE; TR0 = 1; }

void TimerUninit(void) { }

void TimerReset(void) { }

函数TimerInit首先将theTimer.itTick、theTimer.itTickLast这两个变量清0。然后根据51单片机的定时器0的寄存器定义,配置定时器0。在打开中断后,定时器0每过1毫秒产生一次定时中断,并且调用中断服务函数。 函数TimerScanCheck需要和TimerScanCheckFirst配合运行。函数TimerScanCheck首先将当前的时钟节拍theTimer.itTick和上一次调用本函数的时钟节拍相减,求出这两次调用的时间差。并将这个时间差存入全局变量theRes.itSys.itTimer(提供给主扫描循环的定时器指令使用)。最后,更新theTimer.itTickLast变量为当前时间。 在第一次调用TimerScanCheck的时候,有可能theTimer.itTickLast并没有被正确的初始化,故在第一次调用TimerScanCheck之前,需要先执行一次TimerScanCheckFirst。

void TimerScanCheckFirst(void) {

theTimer.itTickLast = theTimer.itTick; }

void TimerScanCheck(void) {

register16_t lcTickSave = theTimer.itTick; theRes.itSys.itTimer = lcTickSave;

theRes.itSys.itTimer -= theTimer.itTickLast; theTimer.itTickLast = lcTickSave; }

最后,是中断函数的定义。单片机硬件系统每隔1ms就产生一个定时器中断。在中断里:首先更新theTimer.itTick的值;之后调用函数IOScan驱动I/O硬件;之后调用函数UsartScan驱动串口硬件。

void ISR_TIMER0_OVF(void) __interrupt(1) __using(0) { TL0 = TL0_VALUE; TH0 = TH0_VALUE; //----

++ theTimer.itTick; //---- IOScan(); UsartScan(); }

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 42

isibleontrol AN2103

串口通讯

通过前面的规划我们知道,PLC类型CPU-EC20 (Mini,Compile)并不支持完整的GUTTA通讯协议,而且程序的上传和下载也被极大的简化。通过阅读《UM4001 GUTTA通讯协议》我们知道,要实现GUTTA通讯协议,就必须先实现MODBUS协议,因为GUTTA通讯协议由MODBUS协议封装。这里我们暂时不讨论通讯数据的详细解析流程(由其他文件实现),而是从最基本的硬件驱动层面上,研究如何得收发串口数据,如何定义帧的开始和结束,以及这些数据是通过什么接口被其他模块解析。 出于简单的考虑,PLC类型CPU-EC20 (Mini,Compile)的串口只有1个且只支持MODBUS子站。这就意味着PLC系统的通讯口总是处于等待状态,直到串口接收到了字符。接收到字符后,系统并不是马上开始解析,而是字符据暂时存放在一个接收缓冲中,同时发起对通讯口的扫描,如果在帧间隔时间内又接收到了字符,说明这帧没有结束,继续将接收到的字符存入接收缓冲,直到接收某个字符后的帧间隔时间内一直没有继续接收到字符,就说明一个MODBUS通讯帧结束。系统开始解析接收缓冲区中的数据,解析的结果决定是否需要应答,如果需要应答,则将需要应答的数据放入一个发送缓冲并启动发送。发送结束后,通讯口继续进入等待状态,回到接收数据模式。 因此,一个通讯口在同一时间内,要么处于接收状态,要么处于发送状态,不可能同时发送和接收。这也就是所谓的半双工通讯方式,这也是MODBUS协议能够工作于RS485半双工物理链路上的原因。根据上面的分析,我们建立通讯口的状态机如图:

[2] 在时间中断中发现帧间隔时间内没有继续接收到字节接收数据完成接收数据中[6] 分析串口数据若不需要从站应答[3] 分析串口数据若需要从站应答[1] 在串口接收中断中接收到一个字节空闲[5] 回到空闲状态[4] 在时间中断中发现帧间隔时间内没有继续发送字节发送数据完成发送数据中

如图,系统有5个基本状态,正如文件plc_usart.h中定义的一样。

#define _USART_STATE_IDLE 0x00 #define _USART_STATE_SLAVE_RXING 0x01 #define _USART_STATE_SLAVE_RXED 0x02 #define _USART_STATE_SLAVE_TXING 0x03 #define _USART_STATE_SLAVE_TXED 0x04

其中:

00H表示空闲状态; 01H表示接收数据中;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

43

isibleontrol AN2103

02H表示接收数据完成; 03H表示发送数据中; 04H表示发送数据完成。 弄清楚串口何时在这5个基本状态中切换,在哪里切换,为什么切换,就弄清楚了文件plc_usart.h和文件plc_usart.c的全部内容。通过上面的分析,我们知道,串口至少需要一个变量才存储当前的串口状态,同时需要一个接收数据缓冲和一个发送数据缓冲,这些变量都定义在结构体USART_BLOCK_PORT中:

typedef struct _USART_BLOCK_PORT { register_t itState; register16_t itTick; // ----------------------------- register_t itAddr;

register16_t itResponseTimeout; register16_t itIntervalFrameDelay; // ----------------------------- __xdata uint8_t* itRxReadPtr; __xdata uint8_t* itRxWritePtr;

uint8_t itRx[_USART_BUFFER_SIZE]; // ----------------------------- __xdata uint8_t* itTxReadPtr; __xdata uint8_t* itTxWritePtr;

uint8_t itTx[_USART_BUFFER_SIZE]; } USART_BLOCK_PORT;

typedef struct _USART_BLOCK { USART_BLOCK_PORT itPort[1]; } USART_BLOCK;

__xdata extern USART_BLOCK theUsart;

如代码所示,除了串口状态变量itState、接收缓冲区itRx[]、发送缓冲区itTx[],我们还定义了一些其他变量:

itTick:一个和时钟节拍有关的辅助变量,帮助系统判断帧间隔时间。itTick在接收(发送)任何字节后,被设置为一个特定的不为0的值。系统在1ms定时中断中会调用UsartScan扫描函数。这个函数在itTick不为0时将itTick自减。若发现itTick自减到了0,则可以做出在帧间隔时间内串口没有继续接收(发送)数据的判断,从而进行必要的串口状态切换。 itAddr:MODBUS的从站地址,默认为1。

itResponseTimeout:MODBUS的从站的应答时间,用于串口作MODBUS自主站时,判断是否继续等待从站回应。由于这里自己永远为从站,故这个变量没有被我们使用。 itIntervalFrameDelay:帧间隔时间,即itTick的自减前的初始化值。 不论是接收缓冲,还是发送缓冲,都有对应的读指针和写指针。接收缓冲对应的读指针是itRxReadPtr,对应的写指针是itRxWritePtr。发送缓冲对应的读指针是itTxReadPtr,对应的写指针是itTxWritePtr。 需要注意的是,我们并没有将结构体USART_BLOCK_PORT做为一个全局变量声明,而是将USART_BLOCK_PORT以结构体数组的形式包含在结构体USART_BLOCK中。这

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

44

isibleontrol AN2103

样做是因为在PLC系统中,可能存在多个串口。我们这个最小系统只有一个串口,故数组的长度是1。 下面我们看看串口模块的函数定义:

void UsartInit(void); void UsartUninit(void); void UsartReset(void);

void UsartResetDefualt(void); void UsartScan(void); void UsartScanCheck(void); void UsartScanCheckSwitch(void);

函数UsartInit执行串口硬件的初始化,函数UsartUninit执行串口硬件的释放,函数TimerReset执行串口硬件的重新配置。我们这个最小系统串口的配置不受PLC程序的影响,故函数UsartUninit和UsartReset都是空函数。 正如规划中所指出的,PLC在上点后,正式运行之前,需要对串口有一个短时间的等待,以便进行更高级的PLC配置(例如清空程序)。在这个很短的等待时间内,串口的配置(波特率、停止位、奇偶校验)必须和PLC配置软件GUTTA Flash Utility保持一致。系统使用函数UsartResetDefault来进行配置。不过在我们这个最小系统中,串口永远只有一种配置,故函数UsartResetDefault也是空函数。

#ifndef F_CPU

#define F_CPU 11059200UL #endif // F_CPU

void UsartInit(void) {

theUsart.itPort[0].itState = _USART_STATE_IDLE; theUsart.itPort[0].itTick = 0;

theUsart.itPort[0].itAddr = 1;

theUsart.itPort[0].itResponseTimeout = 1000; theUsart.itPort[0].itIntervalFrameDelay = 10;

theUsart.itPort[0].itRxReadPtr = theUsart.itPort[0].itRx; theUsart.itPort[0].itRxWritePtr = theUsart.itPort[0].itRx;

theUsart.itPort[0].itTxReadPtr = theUsart.itPort[0].itTx; theUsart.itPort[0].itTxWritePtr = theUsart.itPort[0].itTx;

theUsart.itPort[0].itAddr = 1;

theUsart.itPort[0].itResponseTimeout = 1000; theUsart.itPort[0].itIntervalFrameDelay = 10;

// SM0 = 1; SM1 = 1; REN = 1;

BRT = 256UL - F_CPU/32UL/38400UL;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

45

isibleontrol AN2103

AUXR = 0x15; ES = 1;

// }

void UsartUninit(void) { }

void UsartReset(void) { }

void UsartResetDefualt(void) { }

根据函数UsartInit的寄存器初始化部分,对照《STC12C5A60AD系列单片机器件手册》,我们可以知道,串口被配制成8位数据位、1位校验位(奇还是偶有软件实现)、波特率是19200,同时串口中断被打开。 函数UsartScan是串口状态的时间驱动函数。该函数在1ms定时中断服务程序中被调用。我们来看看这个函数具体执行什么操作:

void UsartScan(void) {

if (theUsart.itPort[0].itTick != 0) -- theUsart.itPort[0].itTick;

IOSetCom(theUsart.itPort[0].itState != _USART_STATE_IDLE); }

函数UsartScan有两个操作,首先判断itTick变量是否为0,如果不为0,就将itTick减1。由于UsartScan每隔1ms就被定时器中断调用一次,故若将itTick赋为一个不为0的值N,那么itTick会在N个ms后递减到0。串口状态机的切换函数就是通过设置itTick的值,然后根据itTick值得变化来实现状态切换步骤[2]、[4]中的时间判断。 函数UsartScanCheck是串口状态机的切换函数。UsartScan函数中不做任何等待,PLC系统可以也应当经尽可能快的重复调用UsartScanCheck来对串口进行扫描。后面我们会知道,PLC系统在每次执行主循环扫描之前,都会调用一次UsartScanCheck。UsartScanCheck完成了串口状态切换中的步骤[2]、[3]、[4]、[5]、[6]:

void UsartScanCheck(void) {

switch (theUsart.itPort[0].itState) { case _USART_STATE_IDLE: break;

case _USART_STATE_SLAVE_RXING:

if (theUsart.itPort[0].itTick == 0)

theUsart.itPort[0].itState = _USART_STATE_SLAVE_RXED; //[2] break;

case _USART_STATE_SLAVE_RXED:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 46

isibleontrol AN2103

if (PtlSlaveQuestReply() & RMODBUS_TX_NEED) {

theUsart.itPort[0].itState = _USART_STATE_SLAVE_TXING; //[3] theUsart.itPort[0].itTick = theUsart.itPort[0].itIntervalFrameDelay; UsartPortTxEnable(); UsartPortTxStart(); } else {

theUsart.itPort[0].itState = _USART_STATE_IDLE; //[6] } break;

case _USART_STATE_SLAVE_TXING:

if (theUsart.itPort[0].itTick == 0) {

theUsart.itPort[0].itState = _USART_STATE_SLAVE_TXED; //[4] UsartPortTxDisable(); } break;

case _USART_STATE_SLAVE_TXED:

theUsart.itPort[0].itState = _USART_STATE_IDLE; //[5] break; } }

函数UsartScanCheckSwitch的结构和函数UsartScanCheck的结构完全一致,唯一的不同就是在分析串口数据的时候。函数UsartSacnCheck调用的是函数PtlSlaveQuestReply,而函数UsartScanCheckSwitch调用的是函数PtlSwitchQuestReply。前者用于PLC的正常通讯,而后者用于PLC上电时极短时间内的高级配置通讯尝试。这两个通讯数据分析函数将会在后面详细介绍。 除了在文件plc_usart.h中定义的上述函数,plc_usart.c还实现了部分自有函数(其他文件不可见):

void UsartPortTxEnable(void); void UsartPortTxDisable(void); void UsartPortTxStart(void);

uint8_t UsartGetSBUF(void); void UsartSetSBUF(uint8_t arData);

函数UsartPortTxEnable用于打开 RS485发送线,UsartPortTxDisable用于关闭RS485发送线。UsartPortTxStart用于起动串口发送缓冲区中的数据发送。通过仔细阅读UsartPortTxStart我们可以知道,这个函数实际上只发送了发送缓冲区的第1个字节,第1个字节一旦被发送,发送缓冲后面的数据会自动在发送中断中完成。 函数UsartGetSBUF和函数UsartSetSBUF用于操作接收(发送)寄存器SBUF。我们要使用偶校验,而51的串口不能通过硬件自动生成校验位,因此需要用软件来操作。这里把对寄存器SBUF的读写作了一个小小的包装:接收字符的时候,暂时不检查校验位(因为MODBUS协议里面有CRC校验,这里省略是安全的);在发送字符的时候,利用累加器

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

47

isibleontrol AN2103

ACC来生成校验位,然后将这个校验位赋给第8位数据位TB8:

uint8_t UsartGetSBUF(void) { return SBUF; }

void UsartSetSBUF(uint8_t arData) { ACC = arData; TB8 = P; // EVEN SBUF = arData; }

最后,文件plc_usart.c还实现了串口中断服务函数:

void ISR_USART0(void) __interrupt(4) __using(0) { uint8_t lcData; if (RI) { RI = 0;

lcData = UsartGetSBUF();

switch (theUsart.itPort[0].itState) { case _USART_STATE_IDLE:

theUsart.itPort[0].itState = _USART_STATE_SLAVE_RXING; case _USART_STATE_SLAVE_RXING: theUsart.itPort[0].itTick = theUsart.itPort[0].itIntervalFrameDelay;

if (theUsart.itPort[0].itRxWritePtr != &theUsart.itPort[0].itRx[0] + _USART_BUFFER_SIZE)

*theUsart.itPort[0].itRxWritePtr ++ = lcData; break; }

} else if (TI) { TI = 0;

switch (theUsart.itPort[0].itState) { case _USART_STATE_SLAVE_TXING:

if (theUsart.itPort[0].itTxReadPtr != theUsart.itPort[0].itTxWritePtr) {

theUsart.itPort[0].itTick = theUsart.itPort[0].itIntervalFrameDelay;

lcData = *theUsart.itPort[0].itTxReadPtr ++; UsartSetSBUF(lcData); } break; } } }

因为51单片机的串口字符接收中断和串口字符发送中断共用同一个优先级,且共用同一个入口,所以在中断中首先根据RI和TI标志位判断应该进行接收中断服务还是应该进行发送中断服务。在接收中断服务中,若当前串口处于空闲状态,则修改状态进入接收数据状

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

48

isibleontrol AN2103

态,同时设置延时判断变量itTick。然后从串口接收缓冲SBUFF中读出接收的字符,存入接收缓冲内。在发送中断服务中,若当前串口处于发送数据状态,则设置延时判断变量itTick。然后将发送缓冲中的字符写入串口发送缓冲SBUFF。

软件系统 通讯协议

我们知道,GUTTA通讯协议是由MODBUS通讯协议封装的,故要实现GUTTA通讯协议,

。必须首先实现MODBUS通讯协议。GUTTA通讯协议可以参考《UM4001 GUTTA通讯协议》

根据前面的介绍我们知道,所有的通讯是由函数PtlSlaveQuestReply来处理的。在函数UsartScanCheck中,若发现帧时间间隔内没有继续接收到串口数据,则调用函数PtlSlaveQuestReply来处理接收缓冲中的数据。函数PtlSlaveQuestReply分析串口接收缓冲的数据,如果需要应答这帧通讯,则需要初始化并添加串口输出缓冲数据,同时在函数返回中包含RMODBUS_TX_NEED位。 在我们这个最小系统中,文件swap_modbus.h和文件swap_mudbus.c用于实现MODBUS协议。文件swap_protocol.h和文件swap_protocol.c用于实现GUTTA通讯协议。由于文件swap_protocol.c用到了大量文件swap_modbus.h中定义的宏,故我们先来看看文件swap_modbus.h。文件swap_modbus.h和文件swap_mudbus.c的MODBUS实现为了可以被方便的移植到其他各种平台上去,和硬件相关的部分都用宏来表示:

// <@interface>

#define RMODBUS_SUPPORT_01 #define RMODBUS_SUPPORT_02 #define RMODBUS_SUPPORT_03 #define RMODBUS_SUPPORT_04 #define RMODBUS_SUPPORT_05 #define RMODBUS_SUPPORT_06 #define RMODBUS_SUPPORT_15 #define RMODBUS_SUPPORT_16

#define RMODBUS_MAX_BYTE(p) (64)

#define RMODBUS_ADDR (theUsart.itPort[0].itAddr)

#define RMODBUS_DI_SIZE (sizeof(theRes.itDI)) #define RMODBUS_DO_SIZE (sizeof(theRes.itDO)) #define RMODBUS_RI_SIZE (sizeof(theRes.itRI)) #define RMODBUS_RO_SIZE (sizeof(theRes.itRO))

#define RMODBUS_DI_PTR(x) ((__xdata void*)((__xdata uint8_t*)(&theRes.itDI)+(x)))

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

49

isibleontrol AN2103

#define RMODBUS_DO_PTR(x) ((__xdata void*)((__xdata uint8_t*)(&theRes.itDO)+(x)))

#define RMODBUS_RI_PTR(x) ((__xdata void*)((__xdata uint8_t*)(&theRes.itRI)+(x)))

#define RMODBUS_RO_PTR(x) ((__xdata void*)((__xdata uint8_t*)(&theRes.itRO)+(x)))

#define RMODBUS_RX_SIZE (_USART_BUFFER_SIZE)

#define RMODBUS_RX_USED (theUsart.itPort[0].itRxWritePtr - theUsart.itPort[0].itRx)

#define RMODBUS_RX_BYTE(x) (theUsart.itPort[0].itRx[x])

#define RMODBUS_RX_INC(x) (*theUsart.itPort[0].itRxWritePtr++=(x)) #define RMODBUS_RX_DEC(x) (*theUsart.itPort[0].itRxWritePtr--=(x))

#define RMODBUS_TX_SIZE (_USART_BUFFER_SIZE)

#define RMODBUS_TX_USED (theUsart.itPort[0].itTxWritePtr - theUsart.itPort[0].itTx)

#define RMODBUS_TX_BYTE(x) (theUsart.itPort[0].itTx[x])

#define RMODBUS_TX_INC(x) (*theUsart.itPort[0].itTxWritePtr++=(x)) #define RMODBUS_TX_DEC(x) (*theUsart.itPort[0].itTxWritePtr--=(x))

//

RMODBUS_SUPPORT宏用于MODBUS功能码的裁减。RMODBUS_SUPPORT_01被定义表示MODBUS协议中的01功能码被支持,以此类推。在这个实现中,根据宏定义,我们知道MODBUS协议中的01、02、03、04、05、06、15、16这8种功能码被支持。 RMODBUS_MAX_BYTE宏用于定义大量数据传送时,数据区数据的最大长度,以字节记。在我们这个系统中,串口的接收和发送缓冲长度都是96,而这里区数据的最大长度为64。64加上头和尾以及CRC校验无论如何都不会大于96,因此是安全的。

RMODBUS_ADDR宏用于定义MODBUS通讯的从站地址。在解析MODBUS通讯数据时,首先就要判断这帧通讯是否给我的(从站地址是否相同)。若不相同,则忽略这帧通讯,同时也不需要应答。

RMODBUS_DI_SIZE宏到RMODBUS_RO_SIZE宏分别用于定义离散量输入、离散量输出、模拟量输入、模拟量输出的宽度(即1x、0x、3x、4x四个区间的宽度)这四个宏用于主站需要进行读访问时,从站判断访问范围是否超出了从站的区间宽度。

RMODBUS_DI_PTR宏到RMODBUS_RO_PTR宏分别用于得到指向离散量输入、离散量输出、模拟量输入、模拟量输出的数据指针。MODBUS实现最终会使用这几个宏来得到数据指针,并且用数据指针来操作这四个区间的数据。 RMODBUS_RX_SIZE宏用于定义接收缓冲区的长度。RMODBUS_RX_USED宏用于定义当前接收缓冲区有效数据的长度。RMODBUS_RX_BYTE宏用于提取缓冲区中的第x个字节。RMODBUS_RX_INC宏将x添加到缓冲区有效数据的后面。RMODBUS_RX_DEC宏将缓冲区有效数据的最后数据删除,并将值赋给x。

RMODBUS_TX_SIZE宏用于定义发送缓冲区的长度。RMODBUS_TX_USED宏用于定义当前发送缓冲区有效数据的长度。RMODBUS_TX_BYTE宏用于提取缓冲区中的第x个字节。RMODBUS_TX_INC宏将x添加到缓冲区有效数据的后面。RMODBUS_TX_DEC宏

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

50

isibleontrol AN2103

将缓冲区有效数据的最后数据删除,并将值赋给x。 函数RModbusSqrRhmb01到函数RModbusSqrWhmw16分别实现了01到16这8种功能码的从站应答。详细的实现请读者对照MODBUS通讯协议阅读光盘中的源代码。MODBUS

。如果协议的最权威的文档应该是MODICON的《Modicon Modbus Protocol Reference Guide》

不太喜欢阅读英文资料的话,也可以阅读本网站的标准化文档《AN2002 通讯系统的应用》中的MUDBUS部分。 下面我们来详细的看看文件swap_protocal.h:

#ifndef __sfw_protocol_h__ #define __sfw_protocol_h__

// ------ Macro ------

// ------ Define ------

#define _PTL_PACK_SIZE PLC_TYPE_EXCH_PACK_SIZE

///////////////////////////////////////////////////////////// #define _PTL_PLC_CLEAR (0x0100) #define _PTL_PLC_ATTACH (0x0110) #define _PTL_PLC_DETACH (0x0111) #define _PTL_GET_PLC_NAME (0x0120) #define _PTL_GET_PLC_INFOR (0x0121)

/////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////// #define _PTL_STATUS_READ (0x0a00) #define _PTL_STATUS_WRITE (0x0a01) #define _PTL_STATUS_SCAN (0x0a02) #define _PTL_STATUS_RESET (0x0a03)

/////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////// #define _PTL_BINARY_INSTRUCTION_ASK (0x0b00) #define _PTL_BINARY_INSTRUCTION_READ (0x0b01) #define _PTL_BINARY_INSTRUCTION_WRITE (0x0b02)

/////////////////////////////////////////////////////////////

// ------ Public Memeber Declare ------

// ------ Public Function Declare ------

// -------------------------------------------------------------- register_t PtlSwitchQuestReply(void); register_t PtlSwitchQuestReplyNolrc(void);

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

51

isible

ontrol AN2103

// -------------------------------------------------------------- register_t PtlSlaveQuestReply(void); register_t PtlSlaveQuestReplyNocrc(void);

// -------------------------------------------------------------- register_t PtlSqr(void);

// -------------------------------------------------------------- register_t PtlSqrPlcClear(register16_t arLen); // No attach, No stop

register_t PtlSqrPlcAttach(register16_t arLen); register_t PtlSqrPlcDetach(register16_t arLen);

register_t PtlSqrGetPlcName(register16_t arLen); register_t PtlSqrGetPlcInfor(register16_t arLen);

register_t PtlSqrStatusRead(register16_t arLen); register_t PtlSqrStatusWrite(register16_t arLen);

register_t PtlSqrStatusScan(register16_t arLen);

register_t PtlSqrStatusReset(register16_t arLen); // No attach, No stop

// -------------------------------------------------------------- register_t PtlSqrBinaryInstructionAsk(register16_t arLen); register_t PtlSqrBinaryInstructionRead(register16_t arLen); register_t PtlSqrBinaryInstructionWrite(register16_t arLen);

// -------------------------------------------------------------- void PtlTxIncRx(register_t arFirst, register_t arLast); void PtlTxIncRxHead(void);

void PtlTxIncRxHeadWidthLength(register_t arLength);

// -------------------------------------------------------------- uint8_t PtlRxCodeHi(void); uint8_t PtlRxCodeLo(void);

uint16_t PtlRxIndexHi(void); uint16_t PtlRxIndexLo(void);

#endif //__sfw_protocol_h__

这个文件首先定义了我们需要实现的12条GUTTA通讯指令的功能码。每个功能码都对应了后面的一个函数,这个函数具体实现了这个功能码对应的通讯应答。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

52

isibleontrol AN2103

_PTL_PLC_CLEAR对应函数PtlSqrPlcClear; _PTL_PLC_ATTACH对应函数PtlSqrPlcAttach; _PTL_PLC_DETACH对应函数PtlSqrPlcDetach;

_PTL_GET_PLC_NAME对应函数PtlSqrGetPlcName; _PTL_GET_PLC_INFOR对应函数PtlSqrGetPlcInfor; _PTL_STATUS_READ对应函数PtlSqrStatusRead; _PTL_STATUS_WRITE对应函数PtlSqrStatusWrite; _PTL_STATUS_SCAN对应函数PtlSqrStatusScan; _PTL_STATUS_RESET对应函数PtlSqrStatusReset;

_PTL_BINARY_INSTRUCTION_ASK对应函数PtlSqrBinaryInstructionAsk; _PTL_BINARY_INSTRUCTION_READ对应函数PtlSqrBinaryInstructionRead; _PTL_BINARY_INSTRUCTION_WRITE对应函数PtlSqrBinaryInstructionWrite; 下面我们按照实际的通讯数据处理流程来分析。通过串口的硬件驱动介绍我们知道,在接收到一帧数据后,串口驱动调用函数PtlSlaveQuestReply来处理数据:

register_t PtlSlaveQuestReply(void) { register_t lcResult;

theUsart.itPort[0].itTxReadPtr = &theUsart.itPort[0].itTx[0]; theUsart.itPort[0].itTxWritePtr = &theUsart.itPort[0].itTx[0];

if (RModbusCrcDec()) {

lcResult = PtlSlaveQuestReplyNocrc(); if (lcResult & RMODBUS_TX_NEED) if (!RModbusCrcInc())

lcResult = RMODBUS_RX_DONE|RMODBUS_RX_FAIL; } else {

lcResult = RMODBUS_RX_DONE|RMODBUS_RX_FAIL; }

theUsart.itPort[0].itRxWritePtr = &theUsart.itPort[0].itRx[0];

return lcResult; }

函数PtlSlaveQuestReply首先重置读缓冲区的读指针和写缓冲区的写指针,然后进行MODBUS协议中的CRC校验。若校验不通过(RModbusCrcDec返回为0),则忽略当前帧(也不需要做出任何应答),重置读缓冲区写指针后返回。若校验通过(RModbusCrcDec返回为非0),调用PtlSwitchQuestReplyNolrc继续分析数据。若PtlSwitchQuestReplyNolrc需要应答,则调用RModbusCrcInc在接收缓冲中添加CRC校验,重置读缓冲区写指针后返回。可以看出,PtlSlaveQuestReply主要是完成了通讯数据分析前后缓冲区指针的处理和CRC校验,数据分析的核心部分还是在函数PtlSwitchQuestReplyNolrc中。

register_t PtlSlaveQuestReplyNocrc(void) { if (RMODBUS_RX_USED < 2)

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

53

isibleontrol AN2103

if (RMODBUS_RX_BYTE(0) != 0 &&

RMODBUS_RX_BYTE(0) != RMODBUS_ADDR) return RMODBUS_RX_DONE;

switch (RMODBUS_RX_BYTE(1)) {

#if defined RMODBUS_SUPPORT_01 case 1: return RModbusSqrRhmb01(); #endif // RMODBUS_SUPPORT_01

#if defined RMODBUS_SUPPORT_02 case 2: return RModbusSqrRimb02(); #endif // RMODBUS_SUPPORT_02

#if defined RMODBUS_SUPPORT_03 case 3: return RModbusSqrRhmw03(); #endif // RMODBUS_SUPPORT_03

#if defined RMODBUS_SUPPORT_04 case 4: return RModbusSqrRimw04(); #endif // RMODBUS_SUPPORT_04

#if defined RMODBUS_SUPPORT_05 case 5: return RModbusSqrWhsb05(); #endif // RMODBUS_SUPPORT_05

#if defined RMODBUS_SUPPORT_06 case 6: return RModbusSqrWhsw06(); #endif // RMODBUS_SUPPORT_06

#if defined RMODBUS_SUPPORT_15 case 15: return RModbusSqrWhmb15(); #endif // RMODBUS_SUPPORT_15

#if defined RMODBUS_SUPPORT_16 case 16: return RModbusSqrWhmw16(); #endif // RMODBUS_SUPPORT_16

// <@addition>

case 13: return PtlSqr(); //

default:

if (RMODBUS_RX_BYTE(0) != 0) {

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

54

isibleontrol AN2103

RMODBUS_TX_INC(RMODBUS_RX_BYTE(0)); RMODBUS_TX_INC(RMODBUS_RX_BYTE(1)|0x80); RMODBUS_TX_INC(RMODBUS_ILLEGAL_FUNCTION);

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL|RMODBUS_TX_NEED; } else

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL; } }

函数PtlSwitchQuestReplyNolrc较为冗长。函数首先判断接收缓冲区的有效数据长度是否小于2,若小于2,则说明数据不合法,直接返回。然后判断从站地址是否相符,或者说是否是广播地址。如果上面两点都不符合,也直接返回。然后根据功能码,分别调用RModbusSqrRhmb01、……、RModbusSqrWhmw16来实现各个MODBUS应答。特别的、如果功能码是13,则说明是一条GUTTA通讯指令,调用PtlSqr来处理通讯数据。最后,如果功能码不能被处理,返回一条标准的MODBUS错误信息,告知主站,我不支持这个功能码。到这里我们知道,GUTTA通讯协议是通过PtlSqr来处理的。下面我们来看看PtlSqr函数:

register_t PtlSqr(void) { register16_t lcLen; register16_t lcCode; register_t lcResult;

if (RMODBUS_RX_USED < 8)

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL;

lcLen = ((register16_t)RMODBUS_RX_BYTE(2)<<8)|RMODBUS_RX_BYTE(3); if (lcLen + 4 != RMODBUS_RX_USED)

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL;

lcCode = ((register16_t)RMODBUS_RX_BYTE(4)<<8)|RMODBUS_RX_BYTE(5); lcResult = RMODBUS_RX_DONE|RMODBUS_RX_FAIL|RMODBUS_TX_NEED; switch (lcCode & 0xff00) { case 0x0100:

switch (lcCode & 0xffff) { case _PTL_PLC_CLEAR:

lcResult = PtlSqrPlcClear(lcLen); break; case _PTL_PLC_ATTACH:

lcResult = PtlSqrPlcAttach(lcLen); break; case _PTL_PLC_DETACH:

lcResult = PtlSqrPlcDetach(lcLen); break; case _PTL_GET_PLC_NAME:

lcResult = PtlSqrGetPlcName(lcLen); break; case _PTL_GET_PLC_INFOR:

lcResult = PtlSqrGetPlcInfor(lcLen); break;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

55

isibleontrol AN2103

} break; case 0x0a00:

switch (lcCode & 0xffff) { case _PTL_STATUS_READ:

lcResult = PtlSqrStatusRead(lcLen); break; case _PTL_STATUS_WRITE:

lcResult = PtlSqrStatusWrite(lcLen); break; case _PTL_STATUS_SCAN:

lcResult = PtlSqrStatusScan(lcLen); break; case _PTL_STATUS_RESET:

lcResult = PtlSqrStatusReset(lcLen); break; } break; case 0x0b00:

switch (lcCode & 0xffff) {

case _PTL_BINARY_INSTRUCTION_ASK:

lcResult = PtlSqrBinaryInstructionAsk(lcLen); break; case _PTL_BINARY_INSTRUCTION_READ:

lcResult = PtlSqrBinaryInstructionRead(lcLen); break; case _PTL_BINARY_INSTRUCTION_WRITE:

lcResult = PtlSqrBinaryInstructionWrite(lcLen); break; } break; }

if (lcResult == (RMODBUS_RX_DONE|RMODBUS_RX_FAIL|RMODBUS_TX_NEED)) { PtlTxIncRxHeadWidthLength(4); RMODBUS_TX_BYTE(4) |= 0x80; }

return lcResult; }

函数PtlSqr更为冗长。函数首先判断接收数据缓冲中的有效数据长度是否小于8,若小于8,则直接返回。因为根据GUTTA通讯协议,CRC解包后的通讯数据至少包含索引、代码、长度这3个段(都是2字节数据),加上MODBUS的站地址(1字节)、功能码(1字节),故至少应该是8个字节的长度。之后函数将GUTTA通讯中的长度段提取出来,确认其正确后存入临时变量。然后根据代码段,选择对应的GUTTA通讯协议应答函数。最后如果此代码不被支持,将返回一个错误给主机。 下面是整个通讯协议处理的流程:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 56

isibleontrol AN2103

驱动层UsartScanCheck()扫描到一帧通讯数据软件层PtlSlaveQuestReply()CRC校验成功PtlSlaveQuestReplyNocrc()若为MODBUS通讯数据若为GUTTA通讯数据RModbusSqrRhmb01()RModbusSqrRimb02()RModbusSqrRhmw03()RModbusSqrRimw04()RModbusSqrWhsb05()RModbusSqrWhsw06()RModbusSqrWhmb15()RModbusSqrWhmw16()PtlSqr()PtlSqrPlcClear()PtlSqrPlcAttach()PtlSqrPlcDetach()PtlSqrGetPlcName()PtlSqrGetPlcInfor()PtlSqrStatusRead()PtlSqrStatusWrite()PtlSqrStatusScan()PtlSqrStatusReset()PtlSqrBinaryInstructionAsk()PtlSqrBinaryInstructionRead()PtlSqrBinaryInstructionWrite() GUTTA通讯协议的具体应答比较繁琐,篇幅问题,这里不做一一解释。详细的实现请读者结合标准化文档《UM4001 GUTTA通讯协议》来阅读光盘中的源代码。这里只举例说明:

// _PTL_GET_PLC_NAME

register_t PtlSqrGetPlcName(register16_t arLen) { register_t lcIter;

if (_BF(theRes.itSys.itState, _STATE_ATTACH))

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL|RMODBUS_TX_NEED;

if (arLen != 4 + 0)

return RMODBUS_RX_DONE|RMODBUS_RX_FAIL|RMODBUS_TX_NEED;

PtlTxIncRxHeadWidthLength(4 + sizeof(thePlcName));

for (lcIter = 0; lcIter != sizeof(thePlcName); ++ lcIter) RMODBUS_TX_INC(thePlcName[lcIter]);

return RMODBUS_RX_DONE|RMODBUS_TX_NEED; }

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

57

isibleontrol AN2103

函数PtlSqrGetPlcName完成_PTL_GET_PLC_NAME通讯指令的应答。首先,函数检查theRes.itSys.itState变量中的_STATE_ATTACH位是否被置位,如果被置位,说明PLC已经被登陆,可以响应_PTL_GET_PLC_NAME通讯指令。如果没有被置位,则返回错误代码,上一级函数PtlSqr将根据这个错误代码补充发送缓冲区中的数据并发送。接下来,函数判断通讯数据的长度是否正确,上位机在请求PLC返回PLC名称时,数据区数据的长度应该是0。如果不是0,则返回错误代码。同样的,上一级函数PtlSqr将根据这个错误代码补充发送缓冲区中的数据并发送。如果长度正确,调用函数PtlTxIncRxHeadWidthLength将接收缓冲中的部分数据拷贝到发送缓冲中去(因为接收和发送很多数据都是一致的,例如功能码、站号、GUTTA通讯代码等)。后面利用RMODBUS_TX_INC宏将我们这个最小系统PLC的名称字符(即“CPU-EC20-M(C)”)一个一个压入发送数据缓冲。发送数据缓冲数据处理完毕后返回一个包含RMODBUS_TX_NEED位的字节,告诉上一级的函数PtlSqr我添加了发送数据缓冲,请帮我添加CRC并发送。

指令集支持

下面我们来看看PLC系统固件的指令集支持。我们知道,在编译型PLC中,单片机FLASH分为两大部分:一部分是PLC系统固件;一部分是PLC用户程序。PLC用户程序的格式请参考第2章。通过阅读GUTTA Ladder Editor软件自动生成的C代码我们能发现,PLC用户程序也就是初始化了参数,然后调用对应的PLC指令处理函数。而这些PLC指令处理函数还是在PLC系统固件中实现的。 也就是说,在PLC系统固件在需要运行PLC主循环扫描的时候,通过直接地址转跳到PLC用户程序区去执行。而PLC用户程序在实现用户逻辑的时候,仅仅只是初始化了指令的参数,然后进行相关指令的调用。指令的具体实现还是位于PLC系统固件区,于是单片机必须又转跳回PLC系统固件区执行指令的具体操作。PLC用户区程序是通过指令参数的初始化值以及指令的调用序列来最终实现梯形图程序的逻辑。PLC用户区程序并不具体实现每条指令的功能。每条指令的功能还是在PLC系统固件中实现。 于是PLC系统固件必须提供一个接口以方便PLC程序调用具体的指令实现。这个接口函数就是LgcExcuteDispatch。LgcExcuteDispatch在文件swap_logic.h中声明并在文件swap_logic.c中定义。与此同时,所有和PLC指令集相关的函数都在这两个文件实现。 我们先看看文件swap_logic.h中几个比较重要的宏:

#define _ADDR_PTR(x) (theRes.itPar[x].itPtr)

#define _ADDR_VAL_INT8(x) (*(__packed __xdata int8_t*)_ADDR_PTR(x)) #define _ADDR_VAL_INT16(x) (*(__packed __xdata int16_t*)_ADDR_PTR(x)) #define _ADDR_VAL_INT32(x) (*(__packed __xdata int32_t*)_ADDR_PTR(x)) #define _ADDR_VAL_UINT8(x) (*(__packed __xdata uint8_t*)_ADDR_PTR(x)) #define _ADDR_VAL_UINT16(x) (*(__packed __xdata uint16_t*)_ADDR_PTR(x)) #define _ADDR_VAL_UINT32(x) (*(__packed __xdata uint32_t*)_ADDR_PTR(x))

#define _ADDR_VAL_BIT(x) (theRes.itPar[x].itBit)

#define _ADDR_VAL_BIT_BT(x) _BT(_ADDR_VAL_INT8(x),_ADDR_VAL_BIT(x)) #define _ADDR_VAL_BIT_BF(x) _BF(_ADDR_VAL_INT8(x),_ADDR_VAL_BIT(x)) #define _ADDR_VAL_BIT_BS(x,var) _BS(_ADDR_VAL_INT8(x),_ADDR_VAL_BIT(x),var)#define _ADDR_VAL_BIT_BST(x) _BST(_ADDR_VAL_INT8(x),_ADDR_VAL_BIT(x))

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

58

isibleontrol AN2103

#define _ADDR_VAL_BIT_BSF(x) _BSF(_ADDR_VAL_INT8(x),_ADDR_VAL_BIT(x))

上面这几个宏用于在指令集中使用指令的参数。通过前面对文件plc_res.h的分析我们知道,指令的参数存储在theRes.itPar[x]结构体中,其中x表示第几个参数。theRes.itPar[x]结构体在执行指令调用前被PLC用户程序初始化,在指令集中,我们只需要知道如何使用即可。theRes.itPar[x]结构体中最关键的变量就是itPtr,表示参数数据的指针。于是有了下面几个宏的定义:

_ADDR_VAL_INT8表示8位有符号参数; _ADDR_VAL_INT16表示16位有符号参数; _ADDR_VAL_INT32表示32位有符号参数; _ADDR_VAL_UINT8表示8位有符号参数; _ADDR_VAL_UINT16表示16位有符号参数; _ADDR_VAL_UINT32表示32位有符号参数。 通过宏的实现我们知道,这些不同数据格式的参数仅仅是参数数据指针itPtr的不同使用方式而已。至于下面的位变量参数宏:

_ADDR_VAL_BIT表示当前参数的位偏移;

_ADDR_VAL_BIT_BT表示当前位参数是否为真; _ADDR_VAL_BIT_BF表示当前位参数是否为假; _ADDR_VAL_BIT_BS表示设置当前位参数;

_ADDR_VAL_BIT_BST表示设置当前位参数为真; _ADDR_VAL_BIT_BSF表示设置当前位参数为假。 更是结合了theRes.itPar[x]结构体中的位变量itBit来对itPtr指向的数据进行位提取而已。 文件swap_logic.h实现的函数很少,所有的函数声明如下:

void LgcExcuteDispatch(register_t arSlot);

void LgcExcuteCompileRun(void); void LgcExcuteCompileLocalSave(void); void LgcExcuteCompileLocalRestore(void); void LgcExcuteCompileInterruptCheck(void);

void LgcSetError(register_t arError);

#ifdef FUN_INS_ALD

void LgcExcuteIns_ALD(void); #endif //FUN_INS_ALD #ifdef FUN_INS_OLD

void LgcExcuteIns_OLD(void); #endif //FUN_INS_OLD #ifdef FUN_INS_LPS

void LgcExcuteIns_LPS(void); #endif //FUN_INS_LPS #ifdef FUN_INS_LRD

void LgcExcuteIns_LRD(void); #endif //FUN_INS_LRD

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

59

isibleontrol AN2103

#ifdef FUN_INS_LPP

void LgcExcuteIns_LPP(void); #endif //FUN_INS_LPP #ifdef FUN_INS_LD

void LgcExcuteIns_LD(void); #endif //FUN_INS_LD #ifdef FUN_INS_A

void LgcExcuteIns_A(void); #endif //FUN_INS_A #ifdef FUN_INS_O

void LgcExcuteIns_O(void); #endif //FUN_INS_O #ifdef FUN_INS_LDN

void LgcExcuteIns_LDN(void); #endif //FUN_INS_LDN #ifdef FUN_INS_AN

void LgcExcuteIns_AN(void); #endif //FUN_INS_AN #ifdef FUN_INS_ON

void LgcExcuteIns_ON(void); #endif //FUN_INS_ON #ifdef FUN_INS_NOT

void LgcExcuteIns_NOT(void); #endif //FUN_INS_NOT #ifdef FUN_INS_EU

void LgcExcuteIns_EU(void); #endif //FUN_INS_EU #ifdef FUN_INS_ED

void LgcExcuteIns_ED(void); #endif //FUN_INS_ED #ifdef FUN_INS_C

void LgcExcuteIns_C(void); #endif //FUN_INS_C #ifdef FUN_INS_S

void LgcExcuteIns_S(void); #endif //FUN_INS_S #ifdef FUN_INS_R

void LgcExcuteIns_R(void); #endif //FUN_INS_R #ifdef FUN_INS_CTU

void LgcExcuteIns_CTU(void); #endif //FUN_INS_CTU #ifdef FUN_INS_CTD

void LgcExcuteIns_CTD(void);

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

60

isibleontrol AN2103

#endif //FUN_INS_CTD #ifdef FUN_INS_CTUD

void LgcExcuteIns_CTUD(void); #endif //FUN_INS_CTUD #ifdef FUN_INS_MOVB

void LgcExcuteIns_MOVB(void); #endif //FUN_INS_MOVB #ifdef FUN_INS_MOVW

void LgcExcuteIns_MOVW(void); #endif //FUN_INS_MOVW #ifdef FUN_INS_MOVD

void LgcExcuteIns_MOVD(void); #endif //FUN_INS_MOVD #ifdef FUN_INS_TON

void LgcExcuteIns_TON(void); #endif //FUN_INS_TON #ifdef FUN_INS_TONR

void LgcExcuteIns_TONR(void); #endif //FUN_INS_TONR #ifdef FUN_INS_TOF

void LgcExcuteIns_TOF(void); #endif //FUN_INS_TOF

#define __target_entrance (*(lgc_funptr_vr_t)0xbe64) // > _INS_x(0)

函数LgcExcuteDispatch就是指令集调用接口。这个函数有一个参数,为指令的识别号。PLC用户程序在初始化好指令参数后,使用指令号来告诉PLC系统固件:我下面需要运行哪条PLC指令。函数的具体实现如下:

void LgcExcuteDispatch(register_t arSlot) { switch (arSlot) { #ifdef FUN_INS_ALD case FUN_INS_ALD: LgcExcuteIns_ALD(); break;

#endif //FUN_INS_ALD #ifdef FUN_INS_OLD case FUN_INS_OLD: LgcExcuteIns_OLD(); break;

#endif //FUN_INS_OLD #ifdef FUN_INS_LPS case FUN_INS_LPS: LgcExcuteIns_LPS(); break;

#endif //FUN_INS_LPS

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

61

isibleontrol AN2103

#ifdef FUN_INS_LRD case FUN_INS_LRD: LgcExcuteIns_LRD(); break;

#endif //FUN_INS_LRD #ifdef FUN_INS_LPP case FUN_INS_LPP: LgcExcuteIns_LPP(); break;

#endif //FUN_INS_LPP #ifdef FUN_INS_LD case FUN_INS_LD: LgcExcuteIns_LD(); break;

#endif //FUN_INS_LD #ifdef FUN_INS_A case FUN_INS_A: LgcExcuteIns_A(); break;

#endif //FUN_INS_A #ifdef FUN_INS_O case FUN_INS_O: LgcExcuteIns_O(); break;

#endif //FUN_INS_O #ifdef FUN_INS_LDN case FUN_INS_LDN: LgcExcuteIns_LDN(); break;

#endif //FUN_INS_LDN #ifdef FUN_INS_AN case FUN_INS_AN: LgcExcuteIns_AN(); break;

#endif //FUN_INS_AN #ifdef FUN_INS_ON case FUN_INS_ON: LgcExcuteIns_ON(); break;

#endif //FUN_INS_ON #ifdef FUN_INS_NOT case FUN_INS_NOT: LgcExcuteIns_NOT(); break;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

62

isibleontrol AN2103

#endif //FUN_INS_NOT #ifdef FUN_INS_EU case FUN_INS_EU: LgcExcuteIns_EU(); break;

#endif //FUN_INS_EU #ifdef FUN_INS_ED case FUN_INS_ED: LgcExcuteIns_ED(); break;

#endif //FUN_INS_ED #ifdef FUN_INS_C case FUN_INS_C: LgcExcuteIns_C(); break;

#endif //FUN_INS_C #ifdef FUN_INS_S case FUN_INS_S: LgcExcuteIns_S(); break;

#endif //FUN_INS_S #ifdef FUN_INS_R case FUN_INS_R: LgcExcuteIns_R(); break;

#endif //FUN_INS_R #ifdef FUN_INS_CTU case FUN_INS_CTU: LgcExcuteIns_CTU(); break;

#endif //FUN_INS_CTU #ifdef FUN_INS_CTD case FUN_INS_CTD: LgcExcuteIns_CTD(); break;

#endif //FUN_INS_CTD #ifdef FUN_INS_CTUD case FUN_INS_CTUD: LgcExcuteIns_CTUD(); break;

#endif //FUN_INS_CTUD #ifdef FUN_INS_MOVB case FUN_INS_MOVB: LgcExcuteIns_MOVB();

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

63

isibleontrol AN2103

break;

#endif //FUN_INS_MOVB #ifdef FUN_INS_MOVW case FUN_INS_MOVW: LgcExcuteIns_MOVW(); break;

#endif //FUN_INS_MOVW #ifdef FUN_INS_MOVD case FUN_INS_MOVD: LgcExcuteIns_MOVD(); break;

#endif //FUN_INS_MOVD #ifdef FUN_INS_TON case FUN_INS_TON: LgcExcuteIns_TON(); break;

#endif //FUN_INS_TON #ifdef FUN_INS_TONR case FUN_INS_TONR: LgcExcuteIns_TONR(); break;

#endif //FUN_INS_TONR #ifdef FUN_INS_TOF case FUN_INS_TOF: LgcExcuteIns_TOF(); break;

#endif //FUN_INS_TOF default:

LgcSetError(_LGC_ERR_INSTRUCTION); return; } }

函数根据参数传递过来的指令号,派发调用指令的具体实现。如果某个指令号不被支持,则使用函数LgcSetError设置一个逻辑错误_LGC_ERR_INSTRUCTION。 函数LgcExcuteCompileRun就是我们的主循环扫描调用。

void LgcExcuteCompileRun(void) {

if (FlashReadByte(_INS_x(0)) != 0xff) __target_entrance(0); }

首先使用FlashReadByte宏读取_INS_x(0)上的FLASH数据,判断程序是否为空(擦除后的空程序数据必然为FFH)。如果不为空,就转跳到PLC用户程序去执行。__target_entrance是一个宏,定义如下:

#define __target_entrance (*(lgc_funptr_vr_t)0xbe64) // > _INS_x(0)

这个宏将绝对地址BE64H转化成lgc_funptr_vr_t数据类型。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

64

isibleontrol AN2103

lgc_funptr_vr_t的定义如下:

typedef void (*lgc_funptr_vv_t)(void); typedef void (*lgc_funptr_vr_t)(register_t); typedef register_t (*lgc_funptr_rv_t)(void); typedef register_t (*lgc_funptr_rr_t)(register_t);

lgc_funptr_vr_t是一个函数指针类型。这个函数没有返回值且有一个参数。结合__target_entrance的定义,我们就明白了:函数LgcExcuteCompileRun最后进行了一个函数调用,而这个函数被人为的指定到了地址BE64H上(不由编译器决定)。这个地址位于PLC用户程序FLASH区间上。具体的实现由用户PLC程序决定。 由于我们这个最小PLC系统没有子程序调用且不支持中断系统,故函数 LgcExcuteCompileLocalSave、 LgcExcuteCompileLocalRestore、 LgcExcuteCompileInterruptCheck 都是空函数。保留其在FLASH中的实现仅仅只是为了兼容CPU-EC20 (8051,Compile)。因为GUTTA Ladder Editor软件生成的PLC用户程序C文件依然有这几个函数的调用。 函数LgcSetError的实现很简单,就是根据函数的参数设置错误号,同时设置全局变量theRes.itSys.itState中的_STATE_ERROR位。

void LgcSetError(register_t arError) { _BST(theRes.itSys.itState, _STATE_ERROR); theRes.itLgc.itError = arError; }

虽然是一个最小系统,但也实现了一共26条基本指令。篇幅有限,这里不打算将所有指令的实现都以代码的形式列出,我们选取几条有代表性的指令来研究。其余的指令实现格式其实也都差不多,只要明白指令的含义(可以阅读软件GUTTA Ladder Editor自带的帮助文档),相信看懂光盘中的源代码并且添加自己的指令都不是什么难事。 我们先看一条最基础的位逻辑指令LD。LD指令用于将操作数的值压入数据栈,那么具体是怎么实现的呢?

#ifdef FUN_INS_LD

void LgcExcuteIns_LD(void) {

theRes.itState.itStackData <<= 1; if (_ADDR_VAL_BIT_BT(0))

_BST(theRes.itState.itStackData, 0); }

#endif //FUN_INS_LD

由于在PLC系统中,所谓的数据栈和逻辑栈都是位栈(栈的数据是位变量),且都是用一个字(16位变量)实现。那么这里将theRes.itState.itStackData变量逻辑左移一位就表示将0压入了这个位栈。栈的深度加1。然后利用宏_ADDR_VAL_BIT_BT判断LD指令的参数是否为1,如果是,则需要修改栈顶数据为1,否则,继续保留栈顶数据为0。 下面我们看看数据移动指令MOVW。MOVW指令在数据栈栈顶数据为1时,将输入字(IN)移至输出字(OUT),不改变原来的数值。

#ifdef FUN_INS_MOVW

void LgcExcuteIns_MOVW(void)

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 65

isible {

ontrol AN2103

if (_BT(theRes.itState.itStackData, 0)) _ADDR_VAL_UINT16(1) = _ADDR_VAL_UINT16(0); }

#endif //FUN_INS_MOVW

很简单,也就是在theRes.itState.itStackData第0位(数据栈栈顶数据)为1时,将第0个参数(IN)的值以字的形式赋值给第1个参数(OUT)。

运行系统

终于讲到了PLC最核心的部分,运行系统。运行系统由文件swap_operator.h和文件swap_operator.c实现。运行系统具体定义了PLC启动、PLC复位、PLC运行的行为。我们知道,单片机程序的复位向量指向C语言的运行时启动函数,而这个函数最终会转跳到main函数执行。main函数是用户C语言程序的入口。在我们这个最小系统中,main函数在文件plc_main.c中。文件plc_main.c看起来是这个样子的:

#include \"plc_includes.h\"

void main(void) {

OperSystemIdle((__xdata register_t*)0); }

#include \"plc_res.c\" #include \"plc_io.c\" #include \"plc_flash.c\" #include \"plc_timer.c\" #include \"plc_usart.c\"

#include \"swap_modbus.c\" #include \"swap_protocol.c\" #include \"swap_logic.c\" #include \"swap_operator.c\"

文件plc_includes.h是总的头文件。由于整个项目不大,我们没有采用逐个C文件单独编译然后一起链接的方法,而是将各个模块的C文件都包含在文件plc_main.c中。这样在编译整个项目的时候只需要编译文件plc_main.c就可以了。一般说来,单片机程序要在main函数中设置所谓的超级循环,而在这个项目中,超级循环没有直接放在main函数中。main函数只是调用了函数OperSystemIdle而已,当然,调用这个函数的同时指定了一个值为0的指针作为参数。 整个系统的入口就是函数OperSystemIdle。明确这一点就可以了。当然运行系统不止一个函数,我们先看看文件swap_operator.h的函数声明:

#ifndef __sfw_operator_h__

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

66

isibleontrol AN2103

#define __sfw_operator_h__

// ------ Macro ------

// ------ Define ------

// ------ Public Memeber Declare ------

// ------ Public Function Declare ------

void OperSystemIdle(__xdata register_t* arExit);

void OperResetHardware(void); void OperResetDebug(void); void OperResetAttach(void); void OperResetRetentive(void); void OperResetInitial(void); void OperCheckSwitch(void);

register_t OperPortWorking(void);

#endif // __sfw_operator_h__

是的,第一个函数就是所谓的系统入口函数OperSystemIdle。后面5个函数的名称都是以OperReset开头,分别代表PLC复位时需要执行的几种操作: OperResetHardware:硬件的重新配置。

void OperResetHardware(void) { IOReset(); FlashReset(); TimerReset(); UsartReset(); }

由于在我们这个最小系统中,基本上忽略了系统块的配置数据。硬件在初始化的时候就配置完毕,在此之后硬件配置不需要随着系统块数据的改变而改变。故OperResetHardware调用的几个函数IOReset、FlashReset、TimerReset、UsartReset都是空函数。如果将来需要实现系统块的硬件配置,在对应的函数中添加代码既可。 OperResetDebug:调试模式的重新配置。

void OperResetDebug(void) {

theRes.itSys.itState |= _BV(_STATE_RUN); }

这个函数就执行了一个操作,将全局变量theRes.itSys.itState中的_STATE_RUN位置位。调试模式的复位和运行位有什么关系呢?如果在调试状态下(所谓的单步运行),要求PLC复位后运行停止状态保持不变(_STATE_RUN位保持不变)。而正常模式下的复位,要求PLC复位后立即处于运行状态(_STATE_RUN位被置位)。因此两种模式下复位的差别在于是否执行OperResetDebug函数。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

67

isibleontrol AN2103

OperResetAttach:登陆状态的重新配置。

void OperResetAttach(void) {

theRes.itSys.itState &= ~_BV(_STATE_ATTACH); }

复位后,要求清除以前的登陆状态已保护被加密的程序。由于我们这个最小系统没有实现系统块数据中的密码保护(可以被无条件登陆),故实际上还是没有密码保护。 OperResetRetentive:掉电数据的重新配置。

void OperResetRetentive(void) { }

如果是第一次下载程序,就需要在这里清除所谓的掉电保持数据。由于本硬件系统没有掉电保持数据的功能,这个函数是空函数。 OperResetInitial:内存的重新配置。

void OperResetInitial(void) { register16_t lcIter;

// [0]

theRes.itSys.itReset = 0;

theRes.itSys.itState &= ~_BV(_STATE_RESET); theRes.itSys.itState &= ~_BV(_STATE_ERROR); theRes.itSys.itScanLeft = 0; theRes.itSys.itTimer = 0;

// [1]

theRes.itLgc.itError = 0;

// [2]

memset(&theRes.itDI, 0, sizeof(theRes.itDI)); memset(&theRes.itDO, 0, sizeof(theRes.itDO)); memset(&theRes.itRI, 0, sizeof(theRes.itRI)); memset(&theRes.itRO, 0, sizeof(theRes.itRO));

// [3]

for (lcIter = 0; lcIter != _SIZE_CONST; ++ lcIter)

theRes.itConst.itK[lcIter] = FlashReadByte(_CONST_x(lcIter));

// [4]

memset(&theRes.itState, 0, sizeof(theRes.itState));

// [5]

memset(&theRes.itPar, 0, sizeof(theRes.itPar)); }

函数OperResetInitial包含以下几步操作: [0] 系统状态的复位配置。 [1] 逻辑状态的复位配置。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

68

isibleontrol AN2103

[2] 将DI、DO、RI、RO(1x、0x、3x、4x)几个内存区域初始化为0。 [3] 将FLASH中K区域副本数据拷贝到K区域中。 [4] 程序状态的初始化。 [5] 所有指令参数的初始化。 将所有的复位配置分配成若干个函数来实现是有目的的。因为PLC的复位形式有很多种,例如冷启动复位、热启动复位、程序下载启动复位、调试启动复位等等。在不同的启动形式下,有选择的调用这几个启动函数,就能轻易的实现不同的启动配置。 再看看函数OperCheckSwitch的实现。

void OperCheckSwitch(void) { UsartResetDefualt(); if (1) {

register16_t lcTickSave = theTimer.itTick; theRes.itSys.itState = _SW_STATE_DTTACH;

while (((theRes.itSys.itState == _SW_STATE_DTTACH) ? (theTimer.itTick - lcTickSave < 200) // 200ms : (theRes.itSys.itState != _SW_STATE_DTTACH_DONE)) || OperPortWorking()) { UsartScanCheckSwitch(); }

theRes.itSys.itState = 0; } }

这个函数执行的操作如下:首先,运行串口默认配置函数初始化串口(波特率、校验位、停止位等配置,本系统为空函数)。然后,将系统时钟节拍theTimer.itTick变量用临时变量记录下来。并设置系统状态机状态为_SW_STATE_DTTACH。接下来的wihile语句表示:在接下来的200ms内扫描串口,尝试进入FLASH高级配置(由软件GUTTA Flash Utility发起)。若尝试不成功,200ms内串口没有有效的通讯帧来改变系统状态机状态,则恢复数据,函数返回。这个函数PLC运行主循环扫描之前给了PLC清除程序的一个机会。如果发生了PLC用户程序的崩溃,可以使用软件GUTTA Flash Utility来重新配置FLASH,从而清除包含致命错误的PLC程序。 函数OperPortWorking很简单,就是判断当前串口是否在工作。

register_t OperPortWorking(void) {

return (theUsart.itPort[0].itState != _USART_STATE_IDLE); }

上面介绍了几个基本函数的功能和实现。现在进入关键部分了,请集中注意力,让我们看看所谓的系统入口函数

void OperSystemIdle(__xdata register_t* arExit) { // [0]

OperInitHardware();

// [1]

OperInitSoftware();

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 69

isible // [2]

ontrol AN2103

IntEnable();

// [3]

OperCheckSwitch();

// [4]

theRes.itSys.itReset = _BV(_RESET_HARDWARE) | _BV(_RESET_DEBUG) | _BV(_RESET_ATTACH) | _BV(_RESTE_INITIAL);

_Reset:

if (_BT(theRes.itSys.itReset, _RESET_HARDWARE)) OperResetHardware();

if (_BT(theRes.itSys.itReset, _RESET_DEBUG)) OperResetDebug();

if (_BT(theRes.itSys.itReset, _RESET_ATTACH)) OperResetAttach();

if (_BT(theRes.itSys.itReset, _RESET_RETENTIVE)) OperResetRetentive();

if (_BT(theRes.itSys.itReset, _RESTE_INITIAL)) OperResetInitial();

// [5]

TimerScanCheckFirst();

// [6:LOOP]

if (1) { // 1 : Compile Mode

// CRC:

if (FlashReadWord(_SYS_TAG_LEN) != 0xffff && FlashReadWord(_SYS_TAG_CRC) != FlashCrc()) { LgcSetError(_LGC_ERR_FLASH_CRC); } else {

// call \"__segment_init()\" by \"__target_entrance()\". if (FlashReadByte(_INS_x(0)) != 0xff) __target_entrance((register_t)-1); }

do {

UsartScanCheck();

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

70

isibleontrol AN2103

OperResetCheck();

if (theRes.itSys.itReset) { goto _Reset; }

if (_BF(theRes.itSys.itState, _STATE_ERROR)) { if (_BT(theRes.itSys.itState, _STATE_RUN)) { OperExcuteCompileRun();

} else if (theRes.itSys.itScanLeft) { -- theRes.itSys.itScanLeft; OperExcuteCompileRun(); } }

} while (!arExit || *arExit); }

// [7]

OperUninitSoftware();

// [8]

OperUninitHardware();

整个函数有0~8共9个基本操作,让我来解释: [0] 硬件初始化。 [1] 软件初始化。 [2] 打开中断。

[3] 尝试进入FLASH配置(可由上位机软件GUTTA Ladder Editor发起)。 [4] 根据全局变量theRes.itSys.itReset,进行必要的复位配置。 [5] 时钟节拍初始化。 [6] 超级循环。

[7] 软件的释放(只在模拟器中有意义)。 [8] 硬件的释放(只在模拟器中有意义)。 在正常运行的情况下,超级循环的工作流程:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 71

isibleontrol AN2103

如果需要复位,设置复位选项,跳出循环UsartScanCheck()串口扫描执行一次扫描后,继续循环OperResetCheck()复位标志检查不需要运行,继续循环OperExcuteCompileRun()运行主循环LgcExcuteCompileRun()执行PLC用户逻辑OperExcuteCompileRunEnd()I/O刷新IOSetOutput()将Q区更新到输出管脚IOGetInput()将输入管脚更新到I区TimerScanCheck()更新时钟节拍的增量

第5章 完成CPU类型的配置

第4章详细解释了PLC系统固件的实现,并列举了部分PLC系统固件的源代码。由于我们这个项目的特殊性,在编译项目的时候,还有一些需要注意的地方。通过第4章的介绍我们也知道,本项目一个完整的应用包含PLC系统固件和PLC用户程序这两部分。PLC系统固件代码已经准备就绪,马上就可以编译出结果。但是PLC用户程序在哪里呢?PLC用户程序由软件GUTTA Ladder Editor根据梯形图生成。为了支持多PLC类型,软件GUTTA Ladder Editor其实是通过调用CPU-EC20 (Mini,Compile)文件夹下的Communication.DLL文件来生成PLC用户程序C代码的。而Communication.DLL的行为是通过CompileInfor.XML文件来定义的。同时,Communication.DLL只生成一个文件swap_auto.c,这个文件对应的头文件swap_auto.h是保持不变的,一些特殊的配置也在这个头文件中定义。 是的,完成CPU类型的配置,主要就是完成两个任务:完成CompileInfor.XML配置文件;同时完成swap_auto.h头文件。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

72

isibleontrol AN2103

完成文件CompileInfor.XML

配置文件CompileInfor.XML配置以下几方面的内容: PLC用户程序如何使用变量、 PLC用户程序如何调用指令、 PLC用户程序如何完成C语言的编译。

在我们这个系统中,CompileInfor.XML文件是这样的:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

73

isibleontrol AN2103

DirectoryExcute=\"\\CompileFiles\\SDCC\\\" OperateFile=\"\\CompileFiles\\swap_auto\" OperateFileSrc=\"\\CompileFiles\\swap_auto.c\" OperateFileObj=\"\\CompileFiles\\swap_auto.ref\" OperateFileHex=\"\\CompileFiles\\swap_auto.ihx\">

条目是整个XML文件的根条目。它包含这3个子条目,分别用于描述变量、指令集、编译这3方面的内容。

条目描述PLC用户程序如何使用变量。包含若干个子条目。每个子条目有Id属性和Name属性。每个子条目代表一个域。Id属性的值是这个域的槽号(识别号),Name属性的值这是这个域对应的C语言变量名称。

条目描述PLC用户程序如何调用指令。包含若干个子条目。每个子条目有Id属性和Name属性。每个子条目代表一条指令。Id属性的值是这条指令的槽号(识别码),Name属性的值是这条指令对应的C语言调用名称。

条目有若干属性:

Directory属性的值为C文件所在的文件夹;

DirectoryExcute属性的值为编译器所在的文件夹; OperateFile属性的值为C文件的文件名(无后缀); OperateFileSrc属性的值为C文件的文件名; OperateFileObj属性的值为OBJ文件的文件名; OperateFileHex属性的值为HEX文件的文件名。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

74

isibleontrol AN2103

条目有两个的子条目:描述下载前,Communication.DLL需要进行的外部调用。描述下载后,Communication.DLL需要进行的外部调用。一般说来描述的外部调用完成下载前的编译工作。编译得到的二进制数据下载完成后,由描述的外部调用完成下载后的文件清理工作。 不论是还是条目,都只能包含一个或多个子条目。条目有且只能有一个属性Value。Value的值表示需要进行外部调用的命令名。例如在本项目中只有一个,这个的Value属性为:

第1个“#”字符代表编译器所在的文件夹。如果省略“#”字符的话,命令将使用计算机默认的PATH环境变量。如果您的电脑已经安装过了SDCC,并且SDCC的调用路径已经被包含在计算机的PATH环境变量中,为节约空间,您可以删除PLC类型自带的SDCC。同时将Value修改成下面的值:

很明显,这条命令的操作是调用SDCC编译器,编译软件根据梯形图生成的swap_auto.c文件,尝试生成swap_auto.ihx文件。 选项--code-loc 0xbe00 –code-size 0x3200告诉编译器,这段程序的代码空间由BE00H开始,长度为3200H(12.5K字节)。为什么是BE00H和3200H这两个数值呢?请回顾这张图:

保留配置参数A800H+0.5KA800H+000HA800H+080HA800H+100HA800H+180HSYSDATAARGJMPA800HGUTTA Ladder Editor插入常数0000H(0K)函数地址K初始化A800H+2.5KPLC系统固件A800H+4.5K中断向量启动代码__target_entrance() 调用入口A800H(42K)SDCC编译数据A800H+5.5KPLC用户程序INS函数F000H(60K)

指令区域INS的开始地址是A800H加上5.5K即BE00H。而INS区域到F000H结束,F000H减去BE00H就是INS区域的大小3200H。 选项--xram-loc 0x0380 --xram-size 0x0080告诉编译器,这段程序的内存空间由0380H开始,长度为0080H(128字节)。为什么是0380H和0080H这两个数值呢?请回顾这张图:

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 75

isibleontrol AN2103

256字节idatadata1024字节xdatabitRegister栈PLC用户使用PLC系统使用I16字节Q16字节AI16字节AQ16字节M448字节第1部分程序使用384字节第2部分程序使用128字节

第2部分程序使用的空间位于xdata的最后128个字节。由于xdata的总长度是1024字节,1023减去128即地址0380H。

完成文件swap_auto.h

软件GUTTA Ladder Editor根据PLC程序自动生成的C语言文件swap_auto.c自动包含了文件swap_auto.h(通过#include关键字)。swap_auto.c中的绝大部分变量和宏都在文件swap_auto.h中定义。swap_auto.h可以分为以下几部分。

1. PLC系统固件中文件plc_type.h中的所有宏定义。

PLC用户程序需要和PLC系统程序一起配合运行,因此PLC用户程序所有的宏定义和数据类型定义必须和文件plc_type.h保持一致。将来对文件plc_type.h的任何修改,都必须同步更新到文件swap_auto.h中去。

2. PLC系统固件中文件plc_res.h中的所有结构体定义。

PLC用户程序需要和PLC系统程序一起配合运行,因此PLC用户程序所有的结构体定义必须和文件plc_res.h保持一致。将来对文件plc_res.h的任何修改,都必须同步更新到文件plc_res.h中去。 由于PLC用户变量已经在PLC系统固件中保留空间,故在编译PLC用户程序的时候,不需要再重新为其分配空间。PLC用户程序的全局变量theRes由__at关键字直接指定地址: __xdata __at(0x0000) RES_BLOCK theRes;

3. 除了全局变量theRes,文件swap_auto.h还需要指定以下几个函数的地址:

#define LgcExcuteDispatch (*(lgc_funptr_vr_t)0x2805) #define LgcExcuteLocalSave (*(lgc_funptr_vv_t)0x290A) #define LgcExcuteLocalRestore (*(lgc_funptr_vv_t)0x290B) #define LgcExcuteInterruptCheck (*(lgc_funptr_vv_t)0x290C)

由于SDCC编译器不能导入符号地址,因此这里采用宏的形式,即先将某个数值强制转换成函数指针,然后调用这个函数指针指向的函数。

4. 定义并实现了接口函数__target_entrance:

void __target_entrance(register_t arIndex) { switch (arIndex) {

case 0x00: AutoExcuteInt_L0(); break; case 0x01: AutoExcuteInt_L1(); break;

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

76

isibleontrol AN2103

case 0x02: AutoExcuteInt_L2(); break; case 0x03: AutoExcuteInt_L3(); break; case 0x04: AutoExcuteInt_L4(); break; case 0x05: AutoExcuteInt_L5(); break; case 0x06: AutoExcuteInt_L6(); break; case 0x07: AutoExcuteInt_L7(); break; case 0x08: AutoExcuteSbr_L0(); break; case 0x09: AutoExcuteSbr_L1(); break; case 0x0a: AutoExcuteSbr_L2(); break; case 0x0b: AutoExcuteSbr_L3(); break; case 0x0c: AutoExcuteSbr_L4(); break; case 0x0d: AutoExcuteSbr_L5(); break; case 0x0e: AutoExcuteSbr_L6(); break; case 0x0f: AutoExcuteSbr_L7(); break; case 0x10: AutoExcuteUser_L0(); break; case 0x11: AutoExcuteUser_L1(); break; case 0x12: AutoExcuteUser_L2(); break; case 0x13: AutoExcuteUser_L3(); break; case 0x14: AutoExcuteUser_L4(); break; case 0x15: AutoExcuteUser_L5(); break; case 0x16: AutoExcuteUser_L6(); break; case 0x17: AutoExcuteUser_L7(); break;

case (register_t)-1: __segment_init(); break; } }

在前面章节的介绍中,多次提到过这个PLC用户程序入口函数__target_entrance。的确如此,PLC系统固件在进行PLC用户程序调用的时候,唯一的入口就是__target_entrance。通过__target_entrance的实现可以看出,根据入口参数的不同,__target_entrance的调用也不同。

00H~07H:调用对应的中断程序(INT):AutoExcuteInt_L0~ AutoExcuteInt_L7。 08H~0FH:调用对应的子程序(SBR):AutoExcuteSbr_L0~ AutoExcuteSbr_L7。

10H~17H:调用对应的用户自定义程序(USER):AutoExcuteUser_L0~ AutoExcuteUser_L7。

-1: 调用用户自定义程序的C环境初始化程序。 由于我们这个最小系统不支持用户中断程序和子程序,故__target_entrance的入口参数只可能是00H和-1。参数00H对应函数AutoExcuteInt_L0调用,也就是所谓的PLC主循环扫描(参考函数OperExcuteCompileRun的实现)。至于-1对应的函数__segment_init调用,函数__segment_ini原本设计为实现用户自定义C语言功能块C环境初始化。由于我们这个最小系统不支持USER指令,故函数__segment_ini是空函数。 函数AutoExcuteInt_L0~ AutoExcuteInt_L7、函数AutoExcuteSbr_L0~ AutoExcuteSbr_L7的实现在文件swap_auto.c中,由GUTTA Ladder Editor软件根据PLC梯形图程序自动生成。

5. 函数AutoExcuteUser_L0~ AutoExcuteUser_L7的定义。 由于我们这个最小系统不支持USER指令,故函数AutoExcuteUser_L0~ AutoExcuteUser_L7都是空函数。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

77

isibleontrol AN2103

第6章 综合调试

编译系统固件

回顾前面的内容。 我们知道在PLC系统固件调用PLC用户程序时,必须通过__target_entrance接口。在PLC系统固件项目中,__target_entrance定义如下:

#define __target_entrance (*(lgc_funptr_vr_t)0xbe64) // > _INS_x(0) 我们知道在PLC用户程序调用PLC系统固件时(例如指令派发、中断处理等),也是使用宏来实现。

#define LgcExcuteDispatch (*(lgc_funptr_vr_t)0x2805) #define LgcExcuteLocalSave (*(lgc_funptr_vv_t)0x290A) #define LgcExcuteLocalRestore (*(lgc_funptr_vv_t)0x290B) #define LgcExcuteInterruptCheck (*(lgc_funptr_vv_t)0x290C)

代码中的地址是如何确定的呢?其实,正常的情况下,这些地址应该在开发这两部分系统前就约定好。不过遗憾的是,我没有在SDCC编译器文档中找到如何强制定义函数实现地址的方法。(例如GCC可以用过__attribute__((section (\"\")))关键字来指定函数的段名,然后通过链接选项指定段地址的方法来间接指定函数地址。)既然没有办法来指定函数地址,那就不能完成开始的约定。 暂时的解决办法就是不要约定。先随便写个值吧。 我们先编译我们的PLC系统固件。编译方法很简单,只要您安装了编译器SDCC。并在本项目的工程文件夹下输入:

如果没有错误信息,则表示编译通过。然后查看编译器输出的文件plc_main.map:

Area Addr Size Decimal Bytes (Attributes) -------------------------------- ---- ---- ------- ----- ------------ XSEG 0000 036A = 874. bytes (REL,CON,XDATA)

Value Global

-------- -------------------------------- 0D:0000 _theRes 0D:0296 _theTimer 0D:029A _theUsart ...

先找到我们的theRes全局变量。和我们设想的一样,theRes全局变量被定义在XRAM的0000H地址上。然后找到这几个函数:

Area Addr Size Decimal Bytes (Attributes) -------------------------------- ---- ---- ------- ----- ------------ CSEG 0087 3AAE = 15022. bytes (REL,CON,CODE)

Value Global

-------- --------------------------------

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

78

isible ...

ontrol AN2103

0C:27D2 _PtlRxIndexHi 0C:27EB _PtlRxIndexLo 0C:2805 _LgcExcuteDispatch 0C:28FA _LgcExcuteCompileRun 0C:290A _LgcExcuteCompileLocalSave 0C:290B _LgcExcuteCompileLocalRestore 0C:290C _LgcExcuteCompileInterruptCheck 0C:290D _LgcSetError ...

将这几个函数的地址更新到PLC用户程序头文件swap_auto.h就可以了。 接下来开始编译我们的PLC用户程序。其实,每次下载程序的时候,GUTTA Ladder Editor软件都会调用Communication.DLL文件,根据CompileInfor.XML文件的配置,自动编译文件swap_auto.c。因此我们要做的就是运行软件GUTTA Ladder Editor,将PLC类型修改为CPU-EC20 (Mini,Compile)后,新建一个空程序,点击下载:

在下载对话框中出现上面的信息时,就表示编译完成了。打开PLC类型文件夹下的文件swap_auto.map,找到入口函数:

Area Addr Size Decimal Bytes (Attributes) -------------------------------- ---- ---- ------- ----- ------------ CSEG BE64 018C = 396. bytes (REL,CON,CODE)

Value Global

-------- -------------------------------- 0C:BE64 ___target_entrance

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

79

isibleontrol AN2103

0C:BF48 ___segment_init ...

现在我们知道,___target_entrance的地址就是BE64H。将这个值写入PLC系统固件项目文件中,再次编译PLC系统固件,这个固件就算真正完成了。

下载系统固件

将程序下载到硬件CPU-EC20 (8051,Compile),可以通过宏晶自带的编程软件STC-ISP。双击软件图标可以运行软件STC-ISP。软件STC-ISP的桌面图表看起来是这个样子的:

一切顺利的话,出现下面的对话框:

这个软件的操作步骤如下:

1. 选择单片机类型为IAP12C5A60AD。

2. 选择单片机代码文件,导入开始编译好的文件swap_auto.ihx。 3. 采用默认的下载选项:

z 采用外部晶振

z RESET管脚依然为RESET管脚。 z 使用复位延时。 z 使用晶振增益。

z 下次下载的时候,不关联P1.0/P1.1管脚。 z 下次下载的时候,不擦除EEPROM。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

80

isibleontrol AN2103

4. 配置选项,每次下载的时候都重新导入文件。 5. 在STC-ISP中选择计算机串口。

6. 连接计算机串口和仿真器CPU-EC20 (8051,Compile)的通讯口。 7. 点击软件的ISP下载按钮。

8. 给仿真器CPU-EC20 (8051,Compile)上电。 计算机没有串口的读者,也可以通过仿真器CPU-EC20 (8051,Compile)自带的USB接口来下载程序。步骤和上面略有不同,具体可参考仿真器CPU-EC20 (8051,Compile)自带的说明文档。

最简单的程序 逻辑指令的测试

程序:

一个简单的逻辑指令测试就是起跑停的应用。 将这个程序下载到仿真器,是不是按下I1.0,Q0.0就被置位;按下I1.1,Q0.0就被复位呢。

定时器指令的测试

程序:

注意,和标准的CPU-EC20指令系统不一样,我们这个最小系统没有定时器变量T区域。故定时器变量使用通用变量M区域。定时器都是16位的,这里使用MW0。通过阅读

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM

81

isibleontrol AN2103

定时器指令的实现我们发现,由于TOF定时器指令需要存储一些额外的数据,这些额外的数据被自动存放在定时器变量之后的一个字节中,在定时器变量是MW0的情况下,MB2也有可能被指令使用,因此在PLC程序的其他地方不要再使用MB2。为了简单起见所有定时器采用统一的基时,即系统节拍1毫秒。

计数器指令的测试

程序:

注意,和标准的CPU-EC20指令系统不一样,我们这个最小系统没有计数器变量C区域。故计数器变量使用通用变量M区域。计数器都是16位的,这里使用MW0。通过阅读计数器指令的实现我们发现,由于CTUD计数器指令需要存储一些额外的数据,这些额外的数据被自动存放在计数器变量之后的一个字节中,在计数器变量是MW0的情况下,MB2也有可能被指令使用,因此在PLC程序的其他地方不要再使用MB2。

COPYRIGHT © 2008 WWW.VISIBLECONTROL.COM 82

因篇幅问题不能全部显示,请点此查看更多更全内容