CPU-EC20 (8051)


概述

单套CPU-EC20 (8051)仿真器可应用于:
PLC学习、51单片机学习、PLC开发。
作为初学者学习PLC的硬件仿真器,CPU-EC20 (8051)可以配合软件GUTTA Ladder Editor 1.0进行PLC程序的下载和运行(编译型不支持程序的上传和PLC指令的单步调试)。CPU-EC20 (8051)采用51内核的IAP12C5A60AD作为PLC处理器核心,并且外扩了EEPROM、USB、RS232、RS485、LED数码管、数字量输入输出、模拟量输入等硬件外围。由于CPU-EC20 (8051)采用开放式PLC系统,您可以通过宏晶的STC-ISP软件,编写和调试自己的单片机系统。同时光盘中提供完整的PLC固件,您也可以根据需要在进行单片机试验后将CPU-EC20 (8051)恢复为PLC系统。对于具有一定基础的读者,您可以参照文档《AN2103 基于GUTTA一步一步实现一个最小PLC系统》,配合本学习套件,开发自己的PLC!

点击查看大图

特点

系统

关键指标

  
核心8051
频率11.0592MHz
SRAM1.25K (1024+256)
FLASH60K
PLC名称CPU-EC20-51(C)
PLC信息CPU-EC20 (8051,Compile)
系统页大小(字节)50
数据页数量16
数据页数据项数量16
中断程序个数8
子程序个数8
中断程序参数个数32
子程序参数个数32
常数区大小(字节)128
指令区大小(字节)12800 (12.5K)
通讯包有效数据长度64
最大程序嵌套层数4
是否支持单步调试

内存使用

CPU-EC20 (8051,Compile) 变量分区

MODBUS地址 槽号 区域标识 区域说明 变量偏移单位 位访问 字节访问 字访问 双字访问 取地址 取值 取指针
输入线圈(1x) 0 I 数字量输入 BYTE    
保持线圈(0x) 1 Q 数字量输出 BYTE    
输入寄存器(3x) 2 AI 模拟量输入 BYTE      
保持寄存器(4x) 3 AQ 模拟量输出 BYTE      
4 M 普通内存 BYTE
5 T 定时器专用 WORD          
6 C 计数器专用 WORD          
7 SM 系统内存 BYTE  
8 J 流程控制专用 BYTE          
常数区域 9 K 常数区域 BYTE    
临时区域 10 L 临时区域 BYTE

CPU-EC20 (8051,Compile) 变量分区大小

区域 MODBUS地址开始 MODBUS地址结束 长度(字节) 范围
I 100001 100064 8 IB0~IB7
Q 000001 000064 8 QB0~QB7
AI 300001 300004 8 AIB0~AIB7
AQ 400001 400004 8 AQB0~AQB7
M 400005 400068 128 MB0~MB127
T 400069 400084 32 T0~T15
C 400085 400092 16 C0~C7
SM 400093 400100 16 SMB0~SMB15
J 400101 400108 16 JB0~JB15
K 128 KB0~KB127
L 32 LB0~LB31

电路板结构及其说明

连接器

连接器PCB功能描述
CN1COM0 RS232RS232连接器,对应PLC中的通讯端口0(用JP6跳线来选择)。
CN2COM0 USBUSB连接器,同时给开发板供电。对应PLC中的通讯端口0(用JP6跳线来选择)。
CN3COM0 RS485RS485连接器,对应PLC中的通讯端口0(用JP6跳线来选择)。
CN4IO-PORT可与继电器I/O底板相连,将输出变成真正的继电器动作。

跳线

跳线PCB功能描述
JP1JP1USB转UASRT模块的供电选择。L:采用IO底板供电。R:采用USB供电。
JP2JP2仿真器(除USB转UASRT模块)的供电选择。L:采用IO底板供电。R:采用USB供电。
JP3JP3USB通讯功能的使能。L:使能USB通讯。R:禁止USB通讯,USB只供电。
JP4JP4IAP12C5A60AD的启动选择。L:P1.1接地。R:P1.1上拉(利用I2C的10K电阻)。
JP5JP5IAP12C5A60AD的启动选择。L:P1.0接地。R:P1.0上拉(利用I2C的10K电阻)。
JP6JP6IAP12C5A60AD的通讯方式。
  • USB:USART转换为USB通讯;
  • RS232:USART转换为RS232通讯;
  • RS485:USART转换为RS485通讯。

下面给出跳线的出厂设置:

注意这里JP4和JP5都选择为10K上拉。这是因为IAP12C5A60AD在进行ISP下载的时候,可以配置为下次下载不需要关联P1.0、P1.1管脚。仿真器在出厂时都采用这种配置,这样就在ISP下载结束后,不需要更改跳线就可以直接使用仿真器的I2C通讯了。

硬件功能

原理图

(见附录)

仿真器用于PLC学习

PLC基础应用

仿真器CPU-EC20 (8051)实现了一个完整的PLC系统,可配合软件GUTTA Ladder Editor进行PLC程序的开发和调试。软件GUTTA Ladder Editor的详细信息可参考网页:GUTTA Ladder Editor。

如果您是PLC的初学者,您可以通过阅读文档《IN1003 PLC编程初级教程》来建立基本的PLC概念。同时这篇文档在第2章详细介绍了如何连接软件GUTTA Ladder Editor与仿真器、如何进行程序的下载和调试。由于教程是以仿真器CPU-EC20 (Cortex-M3)为对象介绍具体操作的,而我们这里使用的仿真器是基于51单片机的CPU-EC20 (8051),因此在某些细节之处存在差异。首先,在进行PLC类型选择的时候,必须选择CPU-EC20 (8051)。又由于IAP12C5A60AD的可用资源远远少于STM32F103,在仿真器CPU-EC20 (8051)中只实现了编译型PLC,而编译型PLC不支持指令的单步调试,故教程中第2章中“更加深入的调试”对本仿真器不适用。

IN1003 PLC编程初级教程》是PLC的基础教程,因此里面的例子都尽量避免使用特殊的硬件特性。大部分范例程序都可以在软件模拟器下运行,您也可以将范例程序中的CPU类型CPU-EC20 (Cortex-M3)修改为CPU-EC20 (8051,Compile)并下载到本仿真器中运行。

PLC硬件测试

IO测试

过查看仿真器CPU-EC20 (8051)的电路图我们知道,对于PLC类型CPU-EC20 (8051,Compile)来说,可以由梯形图直接控制的IO(输入输出)主要包括以下几个部分:输入来说,有6个带自锁的按键、4个不带自锁的按键、两路模拟量输入;输出来说,有6个普通LED灯、4个LED数码管。这里我们编写一个梯形图程序演示如何使用这些外围硬件。

PLC梯形图程序下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_plc/io.vcw

通过阅读这个梯形图程序我们知道:只要有任意自锁按键按下,对应的普通LED也被点亮。若第1个非自锁按键被按下,程序将第1路模拟量输入的值输出到数码管上。若第2个非自锁按键被按下,程序将第2路模拟两输入的值输出到数码管上。若第3个自锁按键被按下,数码管显示4个1。若第4个自锁按键被按下,数码管显示4个2。

EEPROM测试

EEPROM最典型的应用可以参考文档《利用EEPROM实现可保存菜单》,这篇文档提供的例子程序给出了一个典型的菜单模式。您可以通过按键修改若干参数,并在按下SET按键后将修改后的参数保存在EEPROM中。不过对于PLC初学者来说,这个PLC程序可能过于冗长(文档默认的PLC类型为CPU-EC20 (Cortex-M3),需要修改PLC类型为CPU-EC20 (8051,Compile)才能使用)。为了测试硬件,我们这里提供一个更为简单的例子。

PLC梯形图程序下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_plc/eeprom.vcw

这个程序只实现了1个参数的修改和保存。轻触按键从左到右被命名为FUN(I1.0)、UP(I1.1)、DOWN(I1.2)、SET(I1.3)。FUN用于进入参数修改模式,而在参数修改模式按下FUN,则退出参数修改模式,同时修改不被记录。UP、DOWN用于在参数修改模式修改参数。在参数修改模式按下SET,退出参数修改模式,此时修改过的参数被写入EEPROM。

时钟测试
PLC梯形图程序下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_plc/rtc.vcw

这个程序很简单,将PCF8563P中的当前时间读出来,并将分和秒显示在LED数码管上。

PLC高级应用

函数的使用

PLC类型CPU-EC20 (8051,Compile)实现了基本函数功能的支持:最多8个中断函数、最多8个子函数、以及最多4层函数钳套。GUTTA系统实现的函数不仅仅是单纯意义上的子过程调用,还为每个函数分配了临时变量区间L。这个区间在系统维护的栈上。临时变量区间的使用,使函数可以使用输入参数、输出参数、局部参数。由于临时变量在栈上分配,这些带参数的函数是可以重入甚至被递归调用的。

递归调用典型的例子可参考文档《AN2004 在GUTTA中使用递归调用》。

指针的使用

指针的详细说明可以参考文档《IN1001 GUTTA内存使用》。

MODBUS通讯的使用

MODBUS通讯的详细说明可以参考文档《AN2002 通讯系统的应用》。

GUTTA Flash Utility用于PLC程序清除

由于编译型的PLC每次下载的都是可以直接运行的二进制机器码,PLC系统不能保证PLC用户程序的绝对安全。由于PLC运行后PLC系统需要和PLC用户程序协同运行,若PLC用户程序发生了崩溃,会导致PLC系统也无法正常工作,甚至是无法通讯。PLC一旦无法通讯,就不能被GUTTA Ladder Editor软件控制并清除错误的程序。一种方法就是通过STC-ISP工具重新下载PLC固件(全片擦除必然也擦除了PLC用户程序),另一种方法就是借助软件GUTTA Flash Utility

仿真套件在上电启动时,会对串口进行200ms左右的等待。若串口没有接收到指定的数据,则进入正常运行模式。这个等待使仿真器在正式运行前获得了一次重新配置的机会。配置工具为GUTTA Flash Utility。

(由于IAP12C5A60AD有专用的FLASH编程工具STC-ISP,故以上三个按钮对本访真器无效)

由于仿真器只在启动时等待200ms。若需要对仿真器进行运行模式的配置,需要先运行配置工具GUTTA Flash Utility。连接电脑和仿真器的串口。点击配置工具的按钮发起通讯。在配置工具不断尝试连接的同时,复位仿真器,这样仿真器在启动的瞬间就能发现串口的连接信号,从而进入配置阶段。

由于本仿真器之支持编译型PLC,故按钮[Erase As Explain]和按钮[Erase As Compile]功能完全一致,擦除后永远配置为编译型PLC。

仿真器用于单片机学习

单片机程序的下载

CPU-EC20 (8051)仿真器的主控芯片IAP12C5A60AD支持所谓的在系统编程ISP(即在电路板编程)。这就意味着不需要特别的编程器就能进行单片机程序的下载。IAP12C5A60AD在上电后,执行FLASH程序之前,会先检测USART通讯口是否有正确的下载请求,如果有,则进入ISP下载模式,若一段时间内没有发现正确的下载请求,则开始运行用户程序。对IAP12C5A60AD进行ISP编程,需要使用STC的编程软件STC-ISP。STC-ISP软件可以在宏晶的主页上找到下载连接。

安装好STC-ISP编程软件之后,运行软件,一切顺利的话,出现下面的对话框:

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

计算机没有串口的读者,也可以通过仿真器CPU-EC20 (8051)自带的USB接口来下载程序,步骤和上面略有不同。仿真器CPU-EC20 (8051)采用Prolific公司的PL2303来进行USB到RS232的转换。要使用这个芯片,必须先安装Prolific对应的驱动程序。这个驱动程序识别和管理计算机到PL2303之间的USB通讯。同时这个驱动程序在计算机上安装了一个虚拟的COM设备,其它软件对这个COM设备的读写操作,以USB通讯的方式传递到PL2303,并由PL2303执行对应的读写操作。因此,其他计算机软件就可以像操作本地串口一样操作PL2303的串口收发了。

如果是第一次使用仿真器CPU-EC20 (8051),需要先安装PL2303的USB驱动程序。这个驱动程序可以在光盘中找到或者点击这里在网站上下载。安装好之后,我们先看看硬件是否能够正确识别。将仿真器跳线JP3放在USB通讯使能端(DP管脚1.5K电阻上拉)。然后仿真器跳线JP1放在USB供电端。这样,理论上,只要将开发板连接到计算机,USB设备就应该被识别。

将开发板连接到计算机后,如果USB设备被正确识别,计算机的COM设备中会多出一个虚拟的COM设备。设备号由计算机自动分配,例如在我的计算机中,USB设备被分配为Prolific USB-to-Serial Comm Port (COM8)。

到这里,说明USB驱动已经安装正确,计算机就能够像使用内部COM一样使用PL2303虚拟的COM了。不过具体到ISP编程软件STC-ISP,还要需要注意下面的问题。IAP12C5A60AD必须在上电复位的瞬间才有可能进入ISP编程模式(经过我的反复试验,发现带电复位是不会进入ISP编程模式的)。若USB到USART转换模块和IAP12C5A60AD是一个电源,则IAP12C5A60AD在上电复位时,PL2303也在上电复位。而且由于PL2303上电需要完成USB设备的识别和配置等一些列复杂的操作,等PL2303正常工作并开始发送STC-ISP的下载指令时,IAP12C5A60AD早已经完成复位进入正常工作模式了。因此,我们需要将PL2303的供电和IAP12C5A60AD的供电用不同的跳线隔离开,在下载的时候,先给PL2303供电,计算机正确识别USB设备后,操作软件STC-ISP开始下载,然后给IAP12C5A60AD供电。完整的下载过程应该是这样子的:

如何恢复为PLC系统

仿真器CPU-EC20 (8051)出厂时为PLC系统,可以直接配合软件GUTTA Ladder Editor进行PLC程序的下载和调试。一旦使用仿真器CPU-EC20 (8051)进行51单片机程序的下载后,原有的PLC系统固件程序将被新的程序所取代。若需要将仿真器恢复为PLC系统,很简单,只需要将PLC系统固件程序重新下载一次即可。PLC系统固件程序和普通程序的下载没有任何区别(PLC系统固件本身也就是一个51单片机程序而已)。

PLC系统固件程序可以在产品光盘中找到,由于每个开发板的PLC系统固件都不一样(和IAP12C5A60AD的芯片ID号有关系),因此这里也无法提供下载地址。

IAP12C5A60AD芯片介绍

IAP12C5A60AD是宏晶的一款增强型51单片机。如果不是用其增强特性,可以和标准的Intel 8051单片机完全兼容。IAP12C5A60AD的详细信息请参考宏晶的官方手册《STC12C5A60AD系列单片机器件手册》。这里对IAP12C5A60AD做一些简单的介绍。

在这个手册中,我们无法直接找到IAP12C5A60AD这个型号。其实IAP12C5A60AD和芯片STC12C5A60AD基本相同。只是IAP型号的单片机可以对整个FLASH进行编程,而STC型号的单片机只能对EEPROM(其实本质还是FLASH)进行编程。不论是IAP还是STC,单片机的FLASH都有两个区域,即程序代码区(手册中所谓的FLASH)和用户数据区(手册中所谓的EEPROM)。在STC单片机中,代码区不能在应用中编程(只能通过ISP编程),而数据区可以在应用编程,但不能运行程序。在IAP单片机中,代码区和数据区统一编址,代码区的在应用编程和数据区的在应用编程方法完全一致(数据区地址在代码区地址的后面),不过数据区依然不能运行程序,程序一旦跑入数据区,单片机会发生复位。

根据手册中的选型表,我们知道,IAP12C5A60AD具有以下资源:

除了这些,IAP12C5A60AD在很多细节上都对标准的8051进行了增强和扩展。仔细阅读手册就能发现,在不对扩展的寄存器进行设置的情况下,绝大部分扩展功能是被禁止的,即和标准的8051兼容,因此对于单片机的学习者,可以就把它看作标准的8051单片机来使用。

使用SDCC编译器

出于多方面的考虑,这里推荐大家使用SDCC编译器。在文档《AN2103 基于GUTTA一步一步实现一个最小PLC系统》中,就是采用SDCC编译器进行的PLC系统开发的。这篇文档同时有对SDCC编译器安装使用的详细说明。SDCC编译器最权威的文档是官方的《SDCC Compiler User Guide》,这篇文档可以在SDCC编译器的安装目录中找到。

这里假设您已经安装好了SDCC编译器,并且在计算机环境变量中正确的设置了SDCC可执行程序的路径(在我的电脑图标>右键菜单属性>高级>环境变量中设置全局PATH变量)。那么我们就可以开始单片机的开发了。用任意编辑器编写一个main.c文件:


#include <8051.h>

int i = 0;
int j = 0;
int k = 0;

void loop_delay(void);

void main(void) {
   while (1) {
       loop_delay();
       P3_5 = 0;
       loop_delay();
       P3_5 = 1;
       }
   }

void loop_delay(void) {
   for (i = 0; i != 1000; ++ i)
       for (j = 0; j != 100; ++ j)
           ++ k;
   }

然后,在main.c文件所在的目录下,使用命令:

sdcc main.c

如果没有错误信息输出,即表示编译通过。用STC-ISP将编译器生成的main.ihex文件下载到仿真器,观察仿真器,运行指示灯是否交替闪烁呢?您也可以将i、j、k变量的定义中加上“__xdata”前缀(使用IAP12C5A60AD内置的XRAM),重新编译main.c文件后下载,看看执行速度是否有变化?

SDCC项目的下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_sdcc/sdcc.zip

使用KEIL编译器

KEIL编译器由于配备了图形界面的uVision2集成环境,使用上更为直观。由于uVision2采用项目作为最小的管理单元,要编译main.c文件,必须先创建一个项目,然后将所有的硬件配置,编译选项记录在项目文件中。在创建项目的时候,KEIL的硬件列表中没有宏晶的芯片支持,我们选用和IAP12C5A60AD相类似的芯片Atmel AT89C52即可。其余的项目选项采用默认配置,唯一需要修改的地方就是在“Output”选项卡中,将“Create HEX File”前面的可选框勾上。这样在编译时,KEIL软件会自动生成一个HEX文件供STC-ISP下载。和前面一样,我们来编写一个驱动RUN灯闪烁的程序,在新建的项目中加入下面的main.c文件:


#include <reg51.h>

int i = 0;
int j = 0;
int k = 0;

void loop_delay(void);

sbit P3_5 = P3^5;

void main(void) {
   while (1) {
       loop_delay();
       P3_5 = 0;
       loop_delay();
       P3_5 = 1;
       }
   }

void loop_delay(void) {
   for (i = 0; i != 1000; ++ i)
       for (j = 0; j != 100; ++ j)
           ++ k;
   }

这个文件和SDCC中的文件略有不同。首先,寄存器定义的头文件由“8051.h”变成了“reg51.h”,而且在KEIL中,必须手工定义一个位变量P3_5,而在SDCC中,P3_5位变量是已经预定义了的。同样的,您也可以将i、j、k变量的定义中加上“xdata”前缀(使用IAP12C5A60AD内置的XRAM),重新编译main.c文件后下载,看看执行速度是否有变化?

SDCC项目的下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_keil/keil.zip

演示程序

IO演示程序

通过查看仿真器CPU-EC20 (8051)的电路图我们知道,对于单片机IAP12C5A60AD来说,我们可以操作的IO(输入输出)主要包括包括以下几个部分:输入来说,有6个带自锁的按键、4个不带自锁的按键、两路模拟量输入;输出来说,有3个状态LED灯(RUN、COM、ERR)、6个普通LED灯、4个LED数码管。这里我们编写一个程序演示如何使用这些外围硬件,首先我们需要编写这些硬件的驱动程序,然后编写应用程序来操纵这些硬件。

SDCC项目的下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_sdcc/sdcc_io.zip

函数IOGetLockedInput用于捕获自锁按键的输入:

bit IOGetLockedInput(uint8_t arSlot) {
   switch (arSlot) {
       case 0: P3_3 = 1; return !P3_3;
       case 1: P3_4 = 1; return !P3_4;
       case 2: P0_0 = 1; return !P0_0;
       case 3: P0_1 = 1; return !P0_1;
       case 4: P0_2 = 1; return !P0_2;
       case 5: P0_3 = 1; return !P0_3;
       }
   return 0;
   }

函数IOGetUnlockedInput用于捕获非自锁按键的输入:

bit IOGetUnlockedInput(uint8_t arSlot) {
   switch (arSlot) {
       case 0: P2_0 = 1; return !P2_0;
       case 1: P2_1 = 1; return !P2_1;
       case 2: P2_2 = 1; return !P2_2;
       case 3: P2_3 = 1; return !P2_3;
       }
   return 0;
   }

函数IOGetAnalogInput用于捕获模拟量输入:

uint16_t IOGetAnalogInput(uint8_t arSlot) {
   switch (arSlot) {
       case 0:
           ADC_CONTR = (_BV(7) | _BV(6) | _BV(5) | 2);
           _asm
               nop nop nop nop
           _endasm;
           ADC_CONTR |= _BV(3);
           _asm
               nop nop nop nop
           _endasm;
           while (!(ADC_CONTR & _BV(4)))
               ;
           ADC_CONTR &= ~(_BV(3) | _BV(4));
           return ADC_RES << 8 | ADC_RESL;
       case 1:
           ADC_CONTR = (_BV(7) | _BV(6) | _BV(5) | 3);
           _asm
               nop nop nop nop
           _endasm;
           ADC_CONTR |= _BV(3);
           _asm
               nop nop nop nop
           _endasm;
           while (!(ADC_CONTR & _BV(4)))
               ;
           ADC_CONTR &= ~(_BV(3) | _BV(4));
           return ADC_RES << 8 | ADC_RESL;
       }
   return 0;
   }

函数IOSetStateOutput用于设置状态LED输出:

void IOSetStateOutput(uint8_t arSlot, bit arState) {
   switch (arSlot) {
       case 0: P3_5 = !arState; return;
       case 1: P3_6 = !arState; return;
       case 2: P3_7 = !arState; return;
       }
   }

函数IOSetPlainOutput用于设置普通LED输出:

void IOSetPlainOutput(uint8_t arSlot, bit arState) {
   switch (arSlot) {
       case 0: P2_4 = !arState; return;
       case 1: P2_5 = !arState; return;
       case 2: P2_6 = !arState; return;
       case 3: P2_7 = !arState; return;
       case 4: P0_5 = !arState; return;
       case 5: P0_4 = !arState; return;
       }
   }

数码管的驱动相对复杂一些,我们先看看数码管对应的电路图:

IAP12C5A60AD的操作步骤如下:

IAP12C5A60AD按照上面的顺序,依次点亮这4个数码管。只要执行速度足够快,人眼是无法察觉到数码管的闪烁的。为了使刷新速度恒定,就必须借助IAP12C5A60AD的定时器中断。

首先我们建立一个全局变量,用于记录驱动数码管到了哪一步:

uint16_t theIOSegStep = 0;

然后建立一个数据缓冲,告诉定时器中断服务程序,4个数码管应该是什么状态:

uint8_t theIOSegBuffer[4] = {0, 0, 0, 0};

函数IOSetSegOutput用于更新这个数据缓冲的数据:

void IOSetSeg(uint8_t arSlot, uint8_t arVal) {
   theIOSegBuffer[arSlot] = arVal;
   }

在实际使用时,应用程序只需要使用IOSetSegOutput操作这个数据缓冲的数据就可以了,数码管刷新的具体细节在中时间中断完成,应用程序不需要关心其细节。在1ms时间中断中:

void ISR_TIMER1_OVF(void) __interrupt(3) __using(0) {
   TL1 = TL1_VALUE;
   TH1 = TH1_VALUE;
   // ----
   DISC0_CLR();
   DISC1_CLR();
   DISC2_CLR();
   DISC3_CLR();
   ++ theIOSegStep;
   if ((theIOSegStep & 0x0F)) {
       switch ((theIOSegStep >> 4 & 0x3)) {
          case 0:
               __ShiftData(theIOSegBuffer[0]);
               DISC0_SET();
               break;
           case 1:
               __ShiftData(theIOSegBuffer[1]);
               DISC1_SET();
               break;
           case 2:
               __ShiftData(theIOSegBuffer[2]);
               DISC2_SET();
               break;
           case 3:
               __ShiftData(theIOSegBuffer[3]);
               DISC3_SET();
               break;
           }
       }
   }

我们知道,驱动每过1ms就将全局变量theIOSegStep自加一次。theIOSegStep每自加16次会进入一次死区时间,在这个时间内,由于所有的片选都被拉低,故所有的数码管都不显示。驱动每次切换数码管时,都会进入一次1ms的死区时间,以防止相邻的数码管发生干扰:

每进入一次死区时间,驱动交替拉高CS0、CS1、CS2、CS3,同时在拉高前驱动164,使对应数码管显示正确的值。以上是驱动程序的介绍,现在我们看看应用程序如何使用这些驱动:

void main(void) {
   IOInit();
   EA = 1;
   while (1) {
       uint16_t lcAi[2];
       lcAi[0] = IOGetAnalogInput(0);
       lcAi[1] = IOGetAnalogInput(1);
       IOSetPlainOutput(0, IOGetLockedInput(0));
       IOSetPlainOutput(1, IOGetLockedInput(1));
       IOSetPlainOutput(2, IOGetLockedInput(2));
       IOSetPlainOutput(3, IOGetLockedInput(3));
       IOSetPlainOutput(4, IOGetLockedInput(4));
       IOSetPlainOutput(5, IOGetLockedInput(5));
       if (IOGetUnlockedInput(0))
           theKey = 0;
       if (IOGetUnlockedInput(1))
           theKey = 1;
       if ((theIOSegStep & 0x3FF) == 0)
           IOSetSegVal(lcAi[theKey]);
       }
   }

由上面的应用程序我们知道,IAP12C5A60AD在启动后进入运行状态。只要有任意自锁按键按下,对应的普通LED也被点亮。若第1个非自锁按键被按下,程序将第1路模拟量输入的值输出到数码管上。若第2个非自锁按键被按下,程序将第2路模拟两输入的值输出到数码管上。

EEPROM演示程序

通过查看仿真器CPU-EC20 (8051)的电路图我们知道,单片机IAP12C5A60AD通过一条I2C总线与ATMEL的EEPROM存储器AT24C04B相连。通过I2C通讯,我们可以将数据存储在掉电记忆的EEPROM中,在下次上电的时候,我们又能将数据从EEPROM中读取出来。通过AT24C04B,我们就能够纪录一些配置参数,大部分仪器仪表都是通过EEPROM来保存参数的。使用I2C对EEPROM进行读写,是一项基本的单片机应用。

由于IAP12C5A60AD没有I2C外围硬件,我们只能通过GPIO模拟的办法,来实现一个I2C主机,对AT24C04B从设备进行读写。

SDCC项目的下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_sdcc/sdcc_eeprom.zip

我们先来分析本项目中的I2C驱动。I2C总线一共有2根通讯线(不算电源和地),按照I2C协议标准,这两根线必须通过10K电阻上拉到VCC。不论是I2C主设备还是I2C从设备,连接到I2C通讯线的管脚都必须是开漏输出。也就是任何设备都能选择将I2C通讯线“接地”。任何设备都不能将I2C通讯线推免输出(若其他设备以将通讯线“接地”,则发生短路)。多个设备将通讯线接地是逻辑或的关系,也就是说,只要有一个设备将通讯线接地,则通讯线电平变为低。不论是I2C主设备还是I2C从设备,都能对通讯线管脚电平进行读操作。I2C总线一共有2根通讯线,一根一般命名为SCL控制线,一根一般命名为SDA数据线。I2C通讯一般由主机发起,主机通过控制SCL和SDA两根线的“接地”的时序,向从设备发起通讯。通讯过程中,也需要从机参与控制SDA线,例如写数据的应答和读数据。I2C通讯协议的具体格式不是本文的重点,这里只对演示程序的具体实现作写说明。

函数IOI2CStart用于产生一个I2C通讯开始信号:

void IOI2CStart(void) {
   SDA = 1;
   SCL = 1;
   NOP();
   SDA = 0;
   NOP();
   SCL = 0;
   NOP();
   }

所谓的I2C通讯开始信号,也就是在SCL为高电平时,SDA发生了一次负向转换。需要注意的是,IOI2CStart在SDA负向转换后,也将SCL拉低,以配合之后的数据发送。

函数IOI2CStop用于产生一个I2C通讯停止信号:

void IOI2CStop(void) {
   SDA = 0;
   SCL = 1;
   NOP();
   SDA = 1;
   NOP();
   SCL = 0;
   NOP();
   }

所谓的I2C通讯停止信号,也就是在SCL为高电平时,SDA发生了一次正向转换。需要注意的是,IOI2CStart在SDA负向转换后,也将SCL拉低(若系统不止一个主机,不能进行这个操作)。

函数IOI2CAck、IOI2CNoAck用于向从设备发送应答:

void IOI2CAck(void) {
   SDA = 0;
   NOP();
   SCL = 1;
   NOP();
   SCL = 0;
   NOP();
   }

void IOI2CNoAck(void) {
   SDA = 1;
   NOP();
   SCL = 1;
   NOP();
   SCL = 0;
   NOP();
   }

主机在读完一个字节的数据后,需要向从机发送一个应答,以确定是否继续读取下一个字节的数据。发送应答和发送一个数据位一样,都是先设置SDA数据线的电平,然后发送一个SCL控制线脉冲,触发从设备读取SDA数据线。

函数IOI2CCheckAck用于接收从设备的应答:

uint8_t IOI2CCheckAck(void) {
   SDA = 1;
   SCL = 1;
   NOP();
   if (SDA) {
       SCL = 0;
       NOP();
       return 0;
       }
   SCL = 0;
   NOP();
   return 1;
   }

主机在写完一个字节的数据后,需要从从机读取一个应答,以确定字节是否被从设备正确读取。接收应答和读取一个数据位一样,都是先设置SDA为高点平(弱上拉),从而判断冲设备是否将SDA“接地”,读取完SDA后,依然需发送一个SCL控制线脉冲,告诉从设备应答已接收,请释放SDA线。

函数IOI2CWriteByte用于向从设备写入一个字节的数据,IOI2CReadByte用于从从设备读取一个字节的数据。

void IOI2CWriteByte(uint8_t arByte) {
   uint8_t lcBit = 8;
   for (; lcBit != 0; -- lcBit) {
       SDA = (arByte & _BV(7));
       NOP();
       SCL = 1;
       NOP();
       SCL = 0;
       NOP();
       arByte <<= 1;
       }
   }

uint8_t IOI2CReadByte(void) {
   uint8_t lcBit = 8, lcByte = 0;
   for (; lcBit != 0; -- lcBit) {
       lcByte <<= 1;
       SDA = 1;
       NOP();
       SCL = 1;
       NOP();
       if (SDA)
           lcByte |= 1;
       SCL = 0;
       NOP();
       }
   return lcByte;
   }

函数IOI2CRead从某个I2C从设备中,从某个地址开始读取若干个数据。

void IOI2CRead(void) {
   uint8_t lcRc = theIOI2CState.itBufferSize;
   theIOI2CState.itEndFlag = 0;

   if (!(lcRc > 0)) {
       theIOI2CState.itEndFlag = -1;
       return;
       }

   // [START]
   IOI2CStart();

   // [SLA+W]
   IOI2CWriteByte(theIOI2CState.itSlaveAddr | TW_WRITE);
   if (!IOI2CCheckAck())
       goto _I2C_Error;

   // [ADDR]
   IOI2CWriteByte(theIOI2CState.itSubAddr);
   if (!IOI2CCheckAck())
       goto _I2C_Error;

   // [RESTART]
   IOI2CStart();

   // [SLA+R]
   IOI2CWriteByte(theIOI2CState.itSlaveAddr | TW_READ);
   if (!IOI2CCheckAck())
       goto _I2C_Error;

   // [DATA]
   for (; lcRc; -- lcRc) {
       *theIOI2CState.itBufferPtr++ = IOI2CReadByte();
       ++ theIOI2CState.itEndFlag;
       if (lcRc != 1)
           IOI2CAck();
       else
           IOI2CNoAck();
       }

整个函数分为以下几个基本操作:

函数IOI2CWrite从某个I2C从设备中,从某个地址开始写入若干个数据。

_I2C_Quit:
   IOI2CStop();
   return;

_I2C_Error:
   IOI2CStop();
   theIOI2CState.itEndFlag = -1;
   return;
   }

void IOI2CWrite(void) {
   uint8_t lcRc = theIOI2CState.itBufferSize;
   theIOI2CState.itEndFlag = 0;

   // [START]
   IOI2CStart();

   // [SLA+W]
   IOI2CWriteByte(theIOI2CState.itSlaveAddr | TW_WRITE);
   if (!IOI2CCheckAck())
       goto _I2C_Error;

   // [ADDR]
   IOI2CWriteByte(theIOI2CState.itSubAddr);
   if (!IOI2CCheckAck())
       goto _I2C_Error;

   // [DATA]
   for (; lcRc; -- lcRc) {
       IOI2CWriteByte(*theIOI2CState.itBufferPtr++);
       ++ theIOI2CState.itEndFlag;
       if (!IOI2CCheckAck())
           goto _I2C_Error;
       }

_I2C_Quit:
   IOI2CStop();
   return;

_I2C_Error:
   IOI2CStop();
   theIOI2CState.itEndFlag = -1;
   return;
   }

整个函数分为以下几个基本操作:

函数IOEEPROMRead和函数IOEEPROMWrite用于AT24C04B的读写。这两个函数其实就是对IOI2CRead和IOI2CWrite的进一步封装。参数arAddr表示需要操作EEPROM数据的地址;参数arPtr表示需要操作RAM数据的地址,由于这个项目只使用IAP12C5A60AD内部的256字节,故在指针前加上“__idata”修饰;参数arNum表示读写的数据个数。

uint8_t IOEEPROMRead(uint16_t arAddr, __idata uint8_t* arPtr, uint8_t arNum) {
   theIOI2CState.itSlaveAddr = 0xa0 + ((arAddr>>7)&0x0e);
   theIOI2CState.itSubAddr = (arAddr&0xff);
   theIOI2CState.itBufferPtr = arPtr;
   theIOI2CState.itBufferSize = arNum;
   theIOI2CState.itEndFlag = 0;
   IOI2CRead();
   return theIOI2CState.itEndFlag == arNum ? 0 : -1;
   }

uint8_t IOEEPROMWrite(uint16_t arAddr, __idata uint8_t* arPtr, uint8_t arNum) {
   theIOI2CState.itSlaveAddr = 0xa0 + ((arAddr>>7)&0x0e);
   theIOI2CState.itSubAddr = (arAddr&0xff);
   theIOI2CState.itBufferPtr = arPtr;
   theIOI2CState.itBufferSize = arNum;
   theIOI2CState.itEndFlag = 0;
   IOI2CWrite();
   return theIOI2CState.itEndFlag == arNum ? 0 : -1;
   }

为了能够将参数显示在LED数码管上,同时为了通过按键完成对参数的修改和保存,这个项目同样需要对基本IO进行操作。这个项目IO的驱动部分,和前面IO演示程序的IO驱动完全相同。下面我们看看应用程序部分是如何使用这些驱动的。

void main(void) {
   IOInit();
   EA = 1;

   // Load EditVal
   IOEEPROMRead(0, (__idata uint8_t*)&theEditVal, 2);

   // Init EditKeySave
   theEditKeySave = 0;
   if (IOGetUnlockedInput(0)) theEditKeySave |= _BV(0);
   if (IOGetUnlockedInput(1)) theEditKeySave |= _BV(1);
   if (IOGetUnlockedInput(2)) theEditKeySave |= _BV(2);
   if (IOGetUnlockedInput(3)) theEditKeySave |= _BV(3);

   while (1) {

       // Init EditKey
       theEditKey = 0;
       if (IOGetUnlockedInput(0)) theEditKey |= _BV(0);
       if (IOGetUnlockedInput(1)) theEditKey |= _BV(1);
       if (IOGetUnlockedInput(2)) theEditKey |= _BV(2);
       if (IOGetUnlockedInput(3)) theEditKey |= _BV(3);

       // Init EditKeyEu
       theEditKeyEu = ~theEditKeySave & theEditKey;

       // Key Proc : begin
       if (theEditing) {
           if (theEditKeyEu & _BV(KEY_FUN)) {
               theEditing = 0;
               theEditVal = theEditValSave;
               }
           else if (theEditKeyEu & _BV(KEY_UP)) {
               ++ theEditVal;
               }
           else if (theEditKeyEu & _BV(KEY_DOWN)) {
               -- theEditVal;
               }
           else if (theEditKeyEu & _BV(KEY_SET)) {
               theEditing = 0;
               IOEEPROMWrite(0, (__idata uint8_t*)&theEditVal, 2);
               }
       } else {
           if (theEditKeyEu & _BV(KEY_FUN)) {
               theEditing = 1;
               theEditValSave = theEditVal;
               }
           }
       // Key Proc : end

       // Init EditKeySave
       theEditKeySave = theEditKey;

       // Just show
       if (theEditing) {
           if (theIOSegStep & _BV(11)) {
               IOSetSeg(0, 0);
               IOSetSeg(1, 0);
               IOSetSeg(2, 0);
               IOSetSeg(3, 0);
           } else {
               IOSetSegVal(theEditVal);
               }
       } else {
           IOSetSegVal(theEditVal);
           }
       }
   }

和所有的单片机程序一样,主函数main中都有一个超级循环。单片机在进入这个超级循环之前,完成了4件事。首先调用IOInit函数初始化IO硬件(可参考IO演示程序)。然后打开全局中断控制位EA,允许全局中断(LED数码管扫描程序开始工作)。然后将EEPROM中的参数读入变量theEditVal。最后初始化按键状态保存变量theEditKeySave,使其值等于当前的按键状态。

我们需要通过按键来完成一些操作,但是仅仅能够读取按键的状态是不够的。例如我们需要自加参数,这个动作只能在按键按下的瞬间完成,而不是只要按键被按下就自加参数。这是应为单片机主循环的循环速度是非常快的,您也许只轻轻按了一下按键,主循环却在这个短暂的接通瞬间,将参数反复自加了很多次。

一个简单的办法就是判断按键的变化。主循环需要知道上一次按键的值,按后根据按键的当前值theEditKey,判断按键是否发生了变化。将变化存入变量theEditKeyEu后,根据theEditKeyEu完成对应的操作。最后,将theEditKey赋值给theEditKeySave,以方便下一次循环判断按键是否发生了变化。

那么程序根据按键执行什么操作呢?按键从左到右被命名为FUN、UP、DOWN、SET。FUN用于进入参数修改模式,在参数修改模式按下FUN,则退出参数修改模式,修改不被记录。UP、DOWN用于在参数修改模式修改参数。在参数修改模式按下SET,则退出参数修改模式,同时修改过的参数被写入EEPROM。

最后程序根据是否在参数修改模式决定如何在LED数码管上显示参数。在参数修改模式需要交替闪烁数字,这里通过借用了时间中断中不断自加的theIOSegStep变量来完成。

时钟演示程序

由于时钟芯片PCF8563P也是作为I2C从设备和AT24C04B挂在同一条总线上的,因此,对PCF8563P的读写可以完全借用EEPROM演示程序中的I2C驱动。

SDCC项目的下载:
  http://www.visiblecontrol.com/products/boardof8051/sample_sdcc/sdcc_rtc.zip

这个程序将PCF8563P中的时间读出来,显示在LED数码管上。

仿真器用于PLC开发

这部分内容请参考文档《AN2103 基于GUTTA一步一步实现一个最小PLC系统》,这篇文档详细介绍了如何基于CPU-EC20 (8051)仿真器实现一个自己的PLC系统。对于有能力的读者,甚至可以在这个最小PLC系统的基础上,逐步加入更多的功能,从而实现一个完备的PLC系统。

附录

CPU-EC20 (8051) 原理图
   PDF格式完整版本下载