程序主体
第一步,要在控制台上实现一个初始画面,假定这个仿真PLC具有8个输入点,8个输出点,为X0-X7,Y0-Y7,我希望实现如图1的界面:
第一行的 * 表示 数字量输入点 的信号,同时也表示该点的指示灯,第二行 * 表示 数字量输出点 的信号。
需要使用到 printf()函数,代码如下:
#include <stdio.h>
void initial()
{
printf("########################################\n");
printf("\n");
printf("\n");
printf(" PLC SIMULATOR \n");
printf("\n");
printf("\n");
printf(" * * * * * * * * \n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" # #\n");
printf(" # #\n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" * * * * * * * * \n");
printf("\n");
printf("\n");
printf("****************************************");
}
int main()
{
initial();
//gotoxy(0, 0);
return 0;
}
接下来,定义两个长度为8的数组,以这两个数组作为输入输出的数据存储区,如下:
int input[8];
int ouput[8];
当值为 0 时表示对应的 IO 点的信号为 FALSE,当值为 1 时表示对应的 IO 点的信号为 TRUE;
同时,将 initial() 函数修改为用于打印显示的 show() 函数,并重写一个 initial() 函数来进行数据存储区的初始化,然后调用 show() 函数进行打印。
另外,也要编写一个 gotox() 函数来将光标移动到界面的第一行第一个列,这样在循环打印时就相当于完成了界面的刷新,程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
// IO array
int input[8];
int output[8];
void show()
{
system("cls");
printf("########################################\n");
printf("\n");
printf("\n");
printf(" PLC SIMULATOR \n");
printf("\n");
printf("\n");
printf(" ");
for(int i=0; i<8; i++)
{
if(!input[i])
printf(" ");
else
printf("* ");
}
printf(" \n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" # #\n");
printf(" # #\n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" ");
for(int i=0; i<8; i++)
{
if(!output[i])
printf(" ");
else
printf("* ");
}
printf(" \n");
printf("\n");
printf("\n");
printf("****************************************");
}
void initial()
{
for(int i=0; i<8; i++)
{
input[i]= 0;
output[i]= 0;
}
show();
}
int main()
{
initial();
return 0;
}
运行后的初始化界面如图2:
因为程序在控制台上执行,所以如果要做按键监控来监测输入信号比较麻烦,所以这里不做监测,仅简单的等待键盘输入信号即可。
首先实现一个按键的信号,程序如下:
#include <stdio.h> // printf()
#include <stdlib.h> // system()
#include <conio.h> // getch()
#include <windows.h>
// IO array
int input[8];
int output[8];
void show()
{
system("cls");
printf("########################################\n");
printf("\n");
printf("\n");
printf(" PLC SIMULATOR \n");
printf("\n");
printf("\n");
printf(" ");
for(int i=0; i<8; i++)
{
if(input[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" # #\n");
printf(" # #\n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" ");
for(int i=0; i<8; i++)
{
if(output[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf("\n");
printf("\n");
printf("****************************************\n");
}
void initial()
{
for(int i=0; i<8; i++)
{
input[i] = 0;
output[i] = 0;
}
show();
}
int checkInput(int inSignal)
{
inSignal = inSignal - 48;
if(inSignal<0 || inSignal>7)
return 0;
input[inSignal] = !input[inSignal];
return 1;
}
int main()
{
initial();
int inSignal;
int tmp=1;
inSignal = getch();
tmp = checkInput(inSignal);
show();
return 0;
}
以上程序仅实现输入信号的打印,下一步是实现输入信号接通,对应序号的输出信号也接通的效果,类似于三菱PLC执行程序:
LD X0
OUT Y0
LD X1
OUT Y1
···
LD X7
OUT Y7
实现代码如下:
#include <stdio.h> // printf()
#include <stdlib.h> // system()
#include <conio.h> // getch()
#include <windows.h>
// IO array
int input[8];
int output[8];
void show()
{
system("cls");
printf("########################################\n");
printf("\n");
printf("\n");
printf(" PLC SIMULATOR \n");
printf("\n");
printf("\n");
printf(" ");
for(int i=0; i<8; i++)
{
if(input[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" # #\n");
printf(" # #\n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" ");
for(int i=0; i<8; i++)
{
if(output[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf("\n");
printf("\n");
printf("****************************************\n");
}
void initial()
{
for(int i=0; i<8; i++)
{
input[i] = 0;
output[i] = 0;
}
show();
}
int checkInput(int inSignal)
{
inSignal = inSignal - 48;
if(inSignal<0 || inSignal>7)
return 0;
input[inSignal] = !input[inSignal];
return 1;
}
void programRun()
{
for(int i=0; i<8; i++)
{
output[i] = input[i];
}
}
int main()
{
initial();
int inSignal;
int tmp=1;
inSignal = getch();
tmp = checkInput(inSignal);
programRun();
show();
return 0;
}
到此,程序在按下0-7按键之后,对应的输入、输出点会接通,但是还无法循环执行,现在将程序修改为循环执行的程序,按下0-7后对应的点会取反,按下其他按键后程序退出,仅需要修改 main()函数,如下:
int main()
{
initial();
int inSignal;
int tmp=1;
while(tmp)
{
inSignal = getch();
tmp = checkInput(inSignal);
programRun();
show();
}
return 0;
}
解释器
接下来是程序语句解释器的部分。
以指令表(IEC61131-3标准中称为 IL 语言)为基础,定义 FNC值 与 指令助记符 的对应关系,如下表:
· 0 - END
· 1 - LD
· 2 - OUT
目前先实现这两个指令的解释器,来将上一步的程序完善好。
作为测试程序,这里就不采用申请内存空间的做法来定义程序存储区,而是直接用一个长度为 100 的数组替代,如:int proData[100];
按三菱 PLC 的编程习惯,取 X0 的指令为 LD X0
,输出 Y0 的指令为 OUT Y0
,那么它们的数据应该分别为 1, x
和 2, x
。
这里有个问题,如果直接将 X0 和 Y0 都定为 0,这样是不行的,所以要么将指令扩展为 1, 0, 0
和 2, 1, 0
来区分 X0 和 Y0,要么是像 PLC 中的存储一样,将IO点映射到内存中,就像 0000~0007 表示 X0X7,00080015表示 Y0~Y7 这样的定义方式。
这里采用第一种方式,即三个数据,一个表示指令,一个表示数据类型,一个表示数据。
· 0 - 输入位
· 1 - 输出位
这样,我们要写一个输入映射到输出的程序,指令表的程序代码为:
LD X0
OUT Y0
LD X1
OUT Y1
LD X2
OUT Y2
LD X3
OUT Y3
LD X4
OUT Y4
LD X5
OUT Y5
LD X6
OUT Y6
LD X7
OUT Y7
如果写一个编译器,编译出来的程序应该是这样的:
1 0 0
2 1 0
1 0 1
2 1 1
1 0 2
2 1 2
1 0 3
2 1 3
1 0 4
2 1 4
1 0 5
2 1 5
1 0 6
2 1 6
1 0 7
2 1 7
为了方便测试起见,直接定义程序代码,为:
int programZrea[1000] = {
1, 0, 0,
2, 1, 0,
1, 0, 1,
2, 1, 1,
1, 0, 2,
2, 1, 2,
1, 0, 3,
2, 1, 3,
1, 0, 4,
2, 1, 4,
1, 0, 5,
2, 1, 5,
1, 0, 6,
2, 1, 6,
1, 0, 7,
2, 1, 7
};
这个就作为程序存储区,相当于编译好的PLC程序。
然后为了翻译程序语句,实现一个虚拟机功能,按汇编语言的处理方式,需要定义一个寄存器,如下:
int regA;
仿真工作的顺序是,先判断输入区信号,然后扫描PLC程序,这里使用 programRun() 函数来读取PLC程序用于扫描,扫描PLC程序的时候按行执行,即读一行程序,然后调用解析器函数 Language(int command, int type, int data),返回后再读下一行程序,这样循环,直到解析器判断到读取的程序指令是 0,表示 END,结束循环,programRun() 函数返回。
解析器函数 Language(int command, int type, int data) 先判断第一个数据,即指令,根据指令调用对应的处理函数。
指令 0 表示 END,程序结束,直接返回;
指令 1 表示 LD 指令,调用取指令处理函数 LDcommand(int type, int data),该函数返回取得的数值;
指令 2 表示 OUT 指令,调用输出指令处理函数 OUTcommand(int type, int data),如果正常返回 0,如果错误返回 1。
这部分的程序代码如下:
// 取指令处理函数
int LDcommand(int type, int data)
{
int reData;
switch(type)
{
case 0: reData = input[data]; break; // 取输入寄存器的数值
case 1: reData = output[data]; break; // 取输出寄存器的数值
default: reData = 0;
}
return reData;
}
// 输出指令处理函数
int OUTcommand(int type, int data)
{
int reData = 0;
switch(type)
{
case 0: reData = 1; break; // 输入寄存器的数值是不能修改的
case 1: output[data] = regA; break; // 将寄存器A的数值输出到输出寄存器
default: reData = 1;
}
return reData;
}
// 解析器
int Language(int command, int type, int data)
{
int endFlag = 1;
switch(command)
{
case 0: endFlag = 0; break; // END 指令
case 1: regA = LDcommand(type, data); break; // LD 取指令 调用 取指令处理函数
case 2: OUTcommand(type, data); break; // OUT 输出指令 调用 输出指令处理函数
default: endFlag = 0;
}
return endFlag;
}
// 扫描程序
void programRun()
{
int pointToPro = 0;
int proCommand;
int proType;
int proData;
int endFlag = 1;
while(endFlag)
{
// 取一行程序
proCommand = programZrea[pointToPro];
proType = programZrea[pointToPro+1];
proData = programZrea[pointToPro+2];
// 调用解析器
endFlag = Language(proCommand, proType, proData);
// 指针下移
pointToPro = pointToPro + 3;
}
}
最后一步是加入文件读取功能,完成后的整体程序如下:
#include <stdio.h> // printf()
#include <stdlib.h> // system()
#include <conio.h> // getch()
#include <windows.h>
// 输入输出存储区
int input[8];
int output[8];
// 程序存储区
int programZrea[1000] = {
1, 0, 0,
2, 1, 0,
1, 0, 1,
2, 1, 1,
1, 0, 2,
2, 1, 2,
1, 0, 3,
2, 1, 3,
1, 0, 4,
2, 1, 4,
1, 0, 5,
2, 1, 5,
1, 0, 6,
2, 1, 6,
1, 0, 7,
2, 1, 7
};
int regA; // 寄存器A
// 刷新屏幕函数
void show()
{
#ifdef _WIN32
system("cls");
#endif
printf("########################################\n");
printf("\n");
printf("\n");
printf(" PLC SIMULATOR \n");
printf("\n");
printf("\n");
printf(" ");
for(int i=0; i<8; i++)
{
if(input[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" # #\n");
printf(" # #\n");
printf(" # 0 1 2 3 4 5 6 7 # \n");
printf(" ");
for(int i=0; i<8; i++)
{
if(output[i])
printf("* ");
else
printf(" ");
}
printf(" \n");
printf("\n");
printf("\n");
printf("****************************************\n");
}
// 初始化函数
void initial()
{
for(int i=0; i<8; i++)
{
input[i] = 0;
output[i] = 0;
}
// 初始化完成后要刷新一次
show();
}
// 检查输入
int checkInput(int inSignal)
{
inSignal = inSignal - 48;
if(inSignal<0 || inSignal>7)
return 0;
input[inSignal] = !input[inSignal];
return 1;
}
// LD 指令执行函数
int LDcommand(int type, int data)
{
int reData;
switch(type)
{
case 0: reData = input[data]; break;
case 1: reData = output[data]; break;
default: reData = 0;
}
return reData;
}
// OUT 指令执行函数
int OUTcommand(int type, int data)
{
int reData = 0;
switch(type)
{
case 0: reData = 1; break; // input register cannot out
case 1: output[data] = regA; break; // out value to output register
default: reData = 1;
}
return reData;
}
// 解析器
int Language(int command, int type, int data)
{
int endFlag = 1;
switch(command)
{
case 0: endFlag = 0; break;
case 1: regA = LDcommand(type, data); break;
case 2: OUTcommand(type, data); break;
default: endFlag = 0;
}
return endFlag;
}
// 扫描器
void programRun()
{
int pointToPro = 0;
int proCommand;
int proType;
int proData;
int endFlag = 1;
while(endFlag)
{
proCommand = programZrea[pointToPro];
proType = programZrea[pointToPro+1];
proData = programZrea[pointToPro+2];
endFlag = Language(proCommand, proType, proData);
pointToPro = pointToPro + 3;
}
}
// 读取程序文件
int openPro()
{
char path[50];
printf("Please Input Your Program File Path: ");
scanf("%s", &path);
FILE *fp;
if((fp=fopen(path, "r"))== NULL)
return 1;
char ch;
int num;
int proPoint = 0;
while((ch=fgetc(fp))!=EOF)
{
num = ch - 48;
if(num>9 || num<0)
continue;
programZrea[proPoint] = num;
proPoint++;
}
fclose(fp);
return 0;
}
int main()
{
// 打开文件
int reError =openPro();
while(reError)
{
printf("Path ERROR!\n");
printf("Please Input a Program File Path: ");
reError =openPro();
}
// 初始化
initial();
int inSignal;
int tmp=1;
while(tmp)
{
// 获取按键信号
inSignal = getch();
// 检查输入
tmp = checkInput(inSignal);
// 扫描 PLC 程序
programRun();
// 刷新
show();
}
return 0;
}
然后,写一个 txt 文档作为 PLC 程序,内容为:
1, 0, 0,
2, 1, 0,
1, 0, 1,
2, 1, 1,
1, 0, 2,
2, 1, 2,
1, 0, 3,
2, 1, 3,
1, 0, 4,
2, 1, 4,
1, 0, 5,
2, 1, 5,
1, 0, 6,
2, 1, 6,
1, 0, 7,
2, 1, 7,
2, 1, 4
执行程序进行测试,输入 PLC程序 文件路径,通过 0-7 按键来接通、断开输入信号,修改 PLC程序,可以看到 PLC逻辑也产生对应的变化。
程序和源码:PLC SIMULATOR