Communication with Siemens S7 Plc with C# and S7.Net plc driver
In this article I explain how to implement a Siemens S7 plc driver by using the open source driver S7.Net.
You can find S7.Net sources on GitHub: https://github.com/killnine/s7netplus.
How to video
Summary
- Why S7.Net: features and capabilities of the driver
- Documentation on the driver and S7 protocol
- Getting started with S7.Net (how to compile and add the driver in a C# application)
- A simple HMI application with S7.Net
Why S7.Net: features and capabilities of the driver
S7.Net is a plc driver written in C#, this means that you don’t have to handle any interoperability with native code, but it just use OO programming and all the .Net features that you are familiar with.
Basic capabilities:
• Can connect and disconnect with the plc using sockets
• Can read and write bytes from a single area of memory, given a starting address and the number of bytes.
High level features:
• Can directly map DBs to classes and structs
• The types of C# are mapped into types of S7 and there are converters for every type (double to REAL, int to DEC, etc)
• It is easy to use, well written and perfectly readable
• It’s open source, MIT license permit you to use it in every commercial application
• Did I already say that it’s written in C#, no interop at all?
What it’s not good about S7.Net, roadmap for future upgrades:
• Lack of documentation
• Lack of a function that permit to read/write multiple non-connected variables with a single request to the plc.
Documentation on the driver and S7 protocol
The documentation of S7.Net is here.
S7.Net exposes a class called Plc that contains all the methods that we can use to communicate with the plc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public
class
Plc
{
public
string
IP {
get
;
set
; }
public
bool
IsConnected {
get
; }
public
CpuType CPU {
get
;
set
; }
public
Int16 Rack {
get
;
set
; }
public
Int16 Slot {
get
;
set
; }
public
string
Name {
get
;
set
; }
public
object
Tag {
get
;
set
; }
public
bool
IsAvailable {
get
; }
public
ErrorCode Open(){...}
public
void
Close(){...}
public
byte
[] ReadBytes(DataType dataType,
int
DB,
int
startByteAdr,
int
count){...}
public
object
Read(DataType dataType,
int
db,
int
startByteAdr, VarType varType,
int
varCount){...}
public
object
Read(
string
variable){...}
public
object
ReadStruct(Type structType,
int
db){...}
public
void
ReadClass(
object
sourceClass,
int
db){...}
public
ErrorCode WriteBytes(DataType dataType,
int
db,
int
startByteAdr,
byte
[] value){...}
public
object
Write(DataType dataType,
int
db,
int
startByteAdr,
object
value){...}
public
object
Write(
string
variable,
object
value){...}
public
ErrorCode WriteStruct(
object
structValue,
int
db){...}
public
ErrorCode WriteClass(
object
classValue,
int
db){...}
public
string
LastErrorString {
get
; }
public
ErrorCode LastErrorCode {
get
; }
}
|
To connect and disconnect you can use the Open() and Close() functions, to communicate you can use any of the methods to read and write variables from the plc memory.
Every method return an ErrorCode, or the object that it’s supposed to return if there are no errors.
Unfortunately this is quite a messy concept, because usually drivers throws exceptions in case of errors, or returns a value indicating the error and put the requested values inside a memory area passing a pointer to that area.
Here there is the documentation of the protocol, in german: http://www.bj-ig.de/service/verfuegbare-dokumentationen/s7-kommunikation/index.html
Some more documentation about the S7 protocol can be found at the snap7 website: http://snap7.sourceforge.net/ or inside the package of Snap7 full (latest full: http://sourceforge.net/projects/snap7/files/1.2.1/ )
Also some documentation on S7 protocol can be found inside LibNoDave.
Getting started with S7.Net
This paragraph explains how to get S7.Net driver and all the steps from compile the driver to add it into your application.
You can choose to add download it from NuGet (fastest way) or to include the full sources in your project.
The steps to download from NuGet are:
1) Right click on the project and select “Manage NuGet packages”
2) Search for S7.Net in the online tab, then install it.
3) Repeat for every project where you need the dll.
These steps are to include the sources in your application:
1) Download the driver from the Github repository https://github.com/killnine/s7netplus.
2) Copy the S7.Net project folder in the folder that contain your project
3) Add the S7.Net project to the solution
4) Add the references
A simple application with S7.Net
I know that create an application to showcase the use of the driver is difficult and will not meet everyone requirements, that’s why i tried to keep it as simple as possible, just to show how to create a PLC object, how to handle a polling to refresh the data read from the PLC and how to visualize the data around the application in a simple way. Also how to write data to the PLC.
The sample application is a WPF application that can connect and disconnect from the PLC, read and write the DB1 variables on the PLC and visualize the values.
The application contains 2 projects, one is a wrapper for the plc driver, the other one is a project that contains all the graphic pages.
You can download the sample application on GitHub: https://github.com/mesta1/S7.Net-example.
Creating a wrapper of S7.Net
Using a wrapper for the plc driver is a strategy that I usually use to implement features that the plc driver doesn’t contain, and to abstract the most of the code required to interface the application with the plc driver.
The wrapper exposes an interface IPlcSyncDriver that contains all the methods needed to communicate with the PLC:
- connect/disconnect and connection state
- read/write single and multiple variables, called Tags (a Tag is an object that contains an address and a value)
- read/write a class (special feature of the S7.Net protocol)
1
2
3
4
5
6
7
8
9
|
public
interface
IPlcSyncDriver
{
ConnectionStates ConnectionState {
get
; }
void
Connect();
void
Disconnect();
List<Tag> ReadItems(List<Tag> itemList);
void
ReadClass(
object
sourceClass,
int
db);
void
WriteItems(List<Tag> itemList);
}
|
As you can see this methods are less and differents from the ones named inside the IPlc interface of the driver, and the reason is because I handle the communication errors by throwing exceptions, so the returned values can be just the objects that i need.
Also I just use the highest level features of the driver, and handle all the complexity inside the wrapper.
I use the concept of Tags inside my wrapper, where a Tag is an object containing an address and a value. This is familiar if you already used OPC Servers and Clients, but here it is much more simple and basic.
Creation of PLC class inside the main project
In the main project I usually define the class that contains the PLC values and communication thread in a singleton.
You can read more about it in this article:
https://www.mesta-automation.com/writing-your-first-hmi-project-in-c-wpf/
You can find the plc class inside PlcConnectivity folder.
The class exposes the properties and methods that are used in all application to communicate with the PLC:
1
2
3
4
5
6
7
8
|
public
ConnectionStates ConnectionState
public
DB1 Db1
public
TimeSpan CycleReadTime
public
void
Connect(
string
ipAddress)
public
void
Disconnect()
public
void
Write(
string
name,
object
value)
public
void
Write(List<Tag> tags)
|
Inside the class there is a multi-threaded timer that will poll the plc once every 100 ms (see constructor).
The timer callback is responsible to refresh the tags and to calculate the time passed after every read.
You can of course use multiple timers, with different Interval value, to better manage the network resources.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
private
void
timer_Elapsed(
object
sender, System.Timers.ElapsedEventArgs e)
{
if
(plcDriver ==
null
|| plcDriver.ConnectionState != ConnectionStates.Online)
{
return
;
}
timer.Enabled =
false
;
CycleReadTime = DateTime.Now - lastReadTime;
try
{
RefreshTags();
}
finally
{
timer.Enabled =
true
;
lastReadTime = DateTime.Now;
}
}
|
To read values from the PLC I use the feature that read a class directly from a DB.
1
2
3
4
|
private
void
RefreshTags()
{
plcDriver.ReadClass(Db1, 1);
}
|
Write values on the PLC
To write the variables I use the method that permit to write a single object by giving an address and a value:
1
2
|
Plc.Instance.Write(PlcTags.DwordVariable, dwordVar);
|
You can read an example of write and read in the handlers of the MainWindow page.
PLC data visualization
To visualize the values I used a DispatcherTimer (but you can use also MVVM and DataBinding):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
void
timer_Tick(
object
sender, EventArgs e)
{
btnConnect.IsEnabled = Plc.Instance.ConnectionState == ConnectionStates.Offline;
btnDisconnect.IsEnabled = Plc.Instance.ConnectionState != ConnectionStates.Offline;
lblConnectionState.Text = Plc.Instance.ConnectionState.ToString();
ledMachineInRun.Fill = Plc.Instance.Db1.BitVariable0 ? Brushes.Green : Brushes.Gray;
lblSpeed.Content = Plc.Instance.Db1.IntVariable;
lblTemperature.Content = Plc.Instance.Db1.RealVariable;
lblAutomaticSpeed.Content = Plc.Instance.Db1.DIntVariable;
lblSetDwordVariable.Content = Plc.Instance.Db1.DWordVariable;
// statusbar
lblReadTime.Text = Plc.Instance.CycleReadTime.TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
}
|
Sample code and testing
You can download the sample application on GitHub: https://github.com/mesta1/S7.Net-example.
You can also download the sample application for plc created with Step7 5.5: S7_pro1
To test the application you can use Step7 PLCSim, the guide to connect to it via NetToPlcSim is here: https://www.mesta-automation.com/nettoplcsim-how-to-connect-step-7-plc-sim-to-scadas/