Using Siemens PLC's

In pvbrowser we use a separate daemon to connect to an individual hardware resource. The daemon reads input signals cyclic from the hardware and stores it in a shared memory. Also the daemon is waiting on a mailbox for output messages. Thus pvserver can easily communicate with the hardware. This can be illustrated by the following picture.

Interfacing to Hardware

It is not necessary to write the daemon manually. Instead you can use a generator to build the daemon from a INI file. Within the generated files we heavily use rllib.

Using Siemens PLC's there are 2 different interfaces available. You may connect either using a serial interface (PPI) or via ethernet (ISO on TCP). We know PPI, which is a trivial serial line protocol. But we can't implement this because the Siemens License rules forbid this. Thus we use the reverse enigineered libnodave which also works well. For ISO on TCP we implemented the protocol in rllib using RFC 1006 and protocol description for fetch/write from Siemens But this works for S5, S7_400 and S7_300 only. For S7_200 we took libnodave. Only S7_200 connectivity could be tested up to now.

Connecting Siemens PLC's using a serial line with PPI protocol

You have to write a small ini file for communicating with Siemens PLC's using the PPI protocol. Here is an example:

############################################
# Project file for generating ppi daemon   #
# mkppi name.mkppi                         #
############################################
shared_memory=/srv/automation/shm/ppi.shm
mailbox=/srv/automation/mbx/ppi.mbx
tty=/dev/ttyS0
baudrate=9600
idletime=10 milliseconds
# area :=
# daveSD
# daveInputs
# daveOutputs
# daveFlags
# daveDB         //data blocks
# daveDI         //not tested
# daveLocal      //not tested
# daveV          //don't know what it is
# daveCounter    //not tested
# daveTimer      //not tested
# cycle := area, dbnum, start, len
cycle1 slave=2 area=daveDB dbnum=1 start=0  len=64
cycle2 slave=2 area=daveDB dbnum=1 start=64 len=64
target=ppidaemon

Now you can build the daemon using the following command:

mkppi ppidaemon.mkppi
g++ ppidaemon.cpp -o ppidaemon -I/usr/local/include/rllib -I/usr/local/include/libnodave -L/usr/lib/ /usr/lib/librllib.so /usr/lib/libpthread.so /usr/lib/libnodavePPI.so

The generated file (ppidaemon.cpp) looks as follows:

//
// PPI daemon template                                 (C) R. Lehrig 2004
//
//
// Attention: this program must be run as super user
//            because libnodave only supports unix, you can't use this program on windows

#include 
#include 
#include "rlsharedmemory.h"
#include "rlmailbox.h"
#include "rlthread.h"
#include "rlcutil.h"
#include "setport.c"
extern "C"
{
#include "nodavePPI.h"
};

#define IDLETIME 10
#define SERIAL_DEVICE "/dev/ttyS0"
#define BAUDRATE "9600"
rlSharedMemory shm("/srv/automation/shm/ppi.shm",128);
rlMailbox      mbx("/srv/automation/mbx/ppi.mbx");
rlThread       thread;
rlThread       watchdog;

static daveConnection   *dc[32+1];
static daveInterface    *di;
static _daveOSserialType fds;

// watchdog
static char *av0 = "";
static int watchcnt1 = 0;
void *watchdogthread(void *arg)
{
  int cnt1 = -1;

  while(1)
  {
    rlsleep(5000);
    if(cnt1 == watchcnt1) break;
    cnt1 = watchcnt1;
  }
  rlsleep(100);
#ifdef unix
  rlexec(av0);
#endif
  exit(0); // pcontrol may start me again if rlexec fails
  return arg;
}

// read mailbox and write to modbus
void *reader(void *arg)
{
  int ret,slave,area,dbnum,start,len,buflen;
  unsigned char buf[1024+1];

  mbx.clear(); // clear old messages
  while((buflen = mbx.read(buf,sizeof(buf))) > 0)
  {
    slave = buf[0];
    area  = buf[1];
    dbnum = buf[2];
    start = buf[3];
    len   = buf[4];
    if(len > 0 && len < 256 && slave >=0 && slave < 32)
    {
      thread.lock();
      //int daveWriteBytes(daveConnection * dc,int area, int DBnum, int start,int len, void * buffer);
      ret = daveWriteBytes(dc[slave],area,dbnum,start,len,&buf[5]);
      thread.unlock();
      if(ret!=0) printf("daveWriteBytes ret=%d\n",ret);
    }
  }
  return arg;
}

// read cycle on ppi
int ppiCycle(int offset, daveConnection *dc, int area, int dbnum, int start, int len)
{
  unsigned char data[1024+1];
  int ret;

  if(len > 256) return -1;
  watchcnt1++;
  if(watchcnt1 > 10000) watchcnt1 = 0;
  thread.lock();
  ret = daveReadBytes(dc,area,dbnum,start,len,data);
  thread.unlock();
  if(ret == 0) shm.write(offset,data,len);
  if(ret != 0) printf("daveReadBytes ret=%d\n",ret);
  return len;
}

int main(int ac, char **av)
{
  int offset,ret,adr;

  if(ac > 0) av0 = av[0];
  fds.rfd=setPort(SERIAL_DEVICE,BAUDRATE,'E');
  fds.wfd=fds.rfd;
  if(fds.rfd>0)
  {
    printf("\nstarting ppidaemon\n");
    di = daveNewInterface(fds,"IF1",0,daveProtoPPI);
    for(adr=1; adr<32; adr++) dc[adr] = daveNewConnection(di,adr,0,0);
    thread.create(reader,NULL);
    watchdog.create(watchdogthread,NULL);
    while(1)
    {
      rlsleep(IDLETIME);
      offset = 0;
      //    ppiCycle(offset, dc, area, dbnum, start, len);
      ret = ppiCycle(offset, dc[2], daveDB, 1, 0, 64);
      if(ret>0) offset += ret; else continue;
      ret = ppiCycle(offset, dc[2], daveDB, 1, 64, 64);
      if(ret>0) offset += ret; else continue;
    }
  }
  printf("\nerror opening SERIAL_DEVICE\n");  return 0;
}

Connecting Siemens PLC's using ethernet with ISO on TCP

You write a small ini file like this.

################################################
# Project file for generating siemens daemon   #
# mksiemens name.mksiemens                     #
################################################
shared_memory=/srv/automation/shm/siemens.shm
mailbox=/srv/automation/mbx/siemens.mbx
# type := S7_200 | S7_300 | S7_400 | S5
slave=0 adr=192.168.1.101 type=S7_200
slave=1 adr=192.168.1.102 type=S7_200
idletime=50 milliseconds
#eventlog host=localhost port=6000
# ORG := ORG_DB | ORG_M | ORG_E | ORG_A | ORG_PEPA | ORG_Z | ORG_T
# cycle := slave + org + dbnum + start + len
cycle1 slave=0 org=ORG_E  dbnum=0 start=0 len=8
cycle2 slave=0 org=ORG_A  dbnum=0 start=0 len=8
cycle3 slave=0 org=ORG_DB dbnum=0 start=0 len=32
target=siemensdaemon

Now you can generate your daemon. The result looks like this.

//
// SiemensTCP daemon template                                 (C) R. Lehrig 2004
//
//
// Attention: this program must be run as super user
//

#include 
#include 
#include "rlsiemenstcp.h"
#include "rlsharedmemory.h"
#include "rlmailbox.h"
#include "rlthread.h"
#include "rlcutil.h"

#define CYCLE_TIME 50

rlSiemensTCP   slave0("192.168.1.101",rlSiemensTCP::S7_200);
rlSiemensTCP   slave1("192.168.1.102",rlSiemensTCP::S7_200);
rlSharedMemory shm("/srv/automation/shm/siemens.shm",48);
rlMailbox      mbx("/srv/automation/mbx/siemens.mbx");
rlThread       thread;
rlSiemensTCP   *slave_array[256];

// read mailbox and write to siemensTCP
void *reader(void *arg)
{
  int buflen,slave,org,dbnr,start_adr,len;
  unsigned char buf[2048+8];

  mbx.clear(); // clear old messages
  while((buflen = mbx.read(buf,sizeof(buf))) > 0)
  {
    slave     = buf[0];
    org       = buf[1];
    dbnr      = buf[2]*256 + buf[3];
    start_adr = buf[4]*256 + buf[5];
    len       = buf[6]*256 + buf[7];
    if(slave >= 0 && slave < 256)
    {
      thread.lock();
      slave_array[slave]->write(org,dbnr,start_adr,len,&buf[8]);
      thread.unlock();
    }
  }
  return arg;
}

// read cycle on SiemensTCP
int siemensTCPCycle(int slave, int offset, int org, int dbnr, int start_adr, int len)
{
  unsigned char data[4096];
  int ret;

  thread.lock();
  ret = slave_array[slave]->fetch(org,dbnr,start_adr,len,data);
  thread.unlock();
  if(ret > 0) shm.write(offset,data,ret);
  //printf("ret=%d\n",ret);
  return len;
}

int main()
{
  int offset,ret;
  slave_array[0] = &slave0;
  slave_array[1] = &slave1;

  //rlSetDebugPrintf(1); // set debugging on
  thread.create(reader,NULL);
  while(1)
  {
    rlsleep(CYCLE_TIME);
    offset = 0;
    // SiemensTCP.Cycle.fetch(slave,offset,fetch(int org, int dbnr, int start_adr, int len);
    ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_E, 0, 0, 8);
    if(ret>0) offset += ret; else continue;
    ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_A, 0, 0, 8);
    if(ret>0) offset += ret; else continue;
    ret = siemensTCPCycle(0,offset, rlSiemensTCP::ORG_DB, 0, 0, 32);
    if(ret>0) offset += ret; else continue;
  }

  return 0;
}