OpenSCADA Documentation
Release 1.0
Vignesh Babu
Sep 27, 2020
Contents
1 About 1
1.1 Motivation . . . 1
2 Installation 3 2.1 Installing Kronos . . . 3
2.2 Installing Dependencies . . . 3
2.3 Installing Bazel . . . 3
2.4 Installing OpenSCADA . . . 4
2.5 Installing CORE Network Emulator (Optional) . . . 4
2.6 Ready to use VM . . . 5
2.7 Uninstalling OpenSCADA . . . 5
2.8 Uninstalling CORE. . . 5
3 User Guide 7 3.1 Prerequisites . . . 7
3.2 Creating a PLC . . . 7
3.3 System Specification . . . 8
3.4 Resource Specification . . . 11
3.5 Instruction List Programming . . . 14
3.5.1 DataTypes. . . 14
3.5.1.1 Illustrations . . . 16
3.5.2 Program Organization Units (POUs) . . . 19
3.5.2.1 Supported Instructions. . . 25
3.5.2.2 Supported System Functions . . . 25
3.5.2.3 Supported System Function Blocks . . . 25
3.5.3 Tasks . . . 26
3.5.3.1 Attaching POUs to Tasks . . . 26
3.6 Starting the PLC . . . 27
3.7 PLC I/O . . . 27
3.7.1 Simulating Sensors . . . 28
3.7.1.1 Illustration . . . 28
3.7.2 Simulating Actuators . . . 29
3.7.2.1 Illustration . . . 29
3.8 PLC Communication . . . 29
3.8.1 Starting the Modbus Communication Module . . . 30
3.8.2 Illustration. . . 30
3.9 Simulating Physical Systems. . . 30
i
3.10.2 Starting dilated processes . . . 32
3.10.3 Running the co-simulation . . . 33
3.10.4 Stopping the co-simulation . . . 33
3.11 Quick Start Example . . . 34
3.11.1 Description . . . 34
3.11.2 Stating the example . . . 34
3.11.3 Viewing output . . . 34
3.11.4 Advanced Example on CORE . . . 35
4 Advanced Topics 37 4.1 Access Variables . . . 37
4.2 Extending Communication Modules. . . 39
5 Doxygen Reference Documentation 41
6 Contact 43
CHAPTER 1
About
OpenSCADA is platform which supports high fidelity emulation of IEC 61131-3 compliant PLCs and SCADA pro- tocols. It can be used to build and simulate complex environments involving cyber-physical systems. OpenSCADA PLCs can be run inside network emulators like CORE/Mininet and interact with other emulated processes. Open- SCADA also provides python interfaces to link the network emulation with a physical system simulator. Using these interfaces OpenSCADA PLCs can interact with physical system simulators like Matpower, Simulink, ManPy etc.
OpenSCADA also ships with support for virtual time based control which is handled byKronos. This allows tight time-synchronization between the physical system simulation and OpenSCADA network emulation.
1.1 Motivation
As manufacturing systems and smart grid networks become increasingly networked with intelligent electronic devices (IEDs) and PLCs, it becomes necessary to understand how these devices interact with the physical system. Several scenarios need to be simulated to analyse the control system and study how changes to cyber components affect the physical environment. OpenSADA enables protoyping high fidelity SCADA emulation scenarios in a safe virtualized environment.
OpenSCADA implements a generic Instruction List processing backend which can be used to emulate different classes of PLCs. It allows users to specify a hardware spec of the PLC they want to emulate which includes the number of CPUs in the PLC, its RAM, Input and Output memory size and execution time of each supported instruction. Users can then leverage OpenSCADA to write Instruction List PLC programs which are interpreted and executed by a backend engine implemented in C++. With this feature, users can emulate acurately, the functionality and timing behaviour of vendor specific PLCs like Siemens, ModiCon, SEL etc. OpenSCADA also proves a generic interface to interact with these soft-PLCs. Emulations of sensors and actuators can talk to the PLC using this generic interface and specific SCADA protocols can be implemented to use the interface to exchange data with the PLC.
We envision that users can leverage this high fidelity emulation platform for various tasks like:
• Building performance models of PLC programs, e.g to study how changes to the program would impact its timing and subsequently the underlying physical system
• Understand how changes to the cyber network affects the control system, e.g how does adding a layer of encryption impact the system
1
• Load-test the cyber-physical system under various scenarios to improve coverage
• Perform what-if cost analysis, e.g what would be the impact of replacing one PLC with another for a particular task, or, what is the best/cheapest PLC to buy to execute a specifc program etc.
The possibilities are huge and we welcome further contributions to improve the project!
CHAPTER 2
Installation
To setup OpenSCADA, Kronos, grpc and bazel package manager need to be first installed.
2.1 Installing Kronos
To install Kronos, follow steps included in thedocumentation. To install all the other dependencies, follow the steps given below.
2.2 Installing Dependencies
• Ensure default python is python 3.6 or above. Follow instructions: https://linuxconfig.org/
how-to-change-from-default-to-alternative-python-version-on-debian-linux
• Install pip:
sudo apt-get install python3-pip pip3 install --upgrade pip
• To install grpc and other dependencies for python execute the following commands:
sudo apt-get install python-tk
sudo python -m pip install grpcio grpcio-tools numpy opencv-python matplotlib
2.3 Installing Bazel
• Install OpenJDK:
sudo apt-get install openjdk-8-jdk
3
• Install Bazel 0.23.1:
wget https://github.com/bazelbuild/bazel/releases/download/0.23.1/bazel-0.23.1-
˓→installer-linux-x86_64.sh
chmod +x bazel-0.23.1-installer-linux-x86_64.sh ./bazel-0.23.1-installer-linux-x86_64.sh
Make sure the version of bazel is atleast 0.23.1 (run command: bazel version)
2.4 Installing OpenSCADA
• Clone the git repository to $HOME directory:
cd ~/ && git clone https://github.com/Vignesh2208/OpenSCADA.git && cd OpenSCADA
• Run the installation setup script. This would take a while the first time it is run because bazel downloads and installs other dependencies:
sudo ./setup.sh install
• Update environment variables. Add OSCADA_INSTALLATION and update PYTHONPATH in bashrc:
export OSCADA_INSTALLATION=<path to installation directory>
export OSCADA_PROTO_PATH=${OSCADA_INSTALLATION}/bazel-out/k8-fastbuild/genfiles/
˓→py_access_service_proto_pb
export PYTHONPATH="${PYTHONPATH}:${OSCADA_INSTALLATION}:${OSCADA_PROTO_PATH}"
• Preserve some environment variables across sudo. Append the following lines after running command sudo visudo:
Defaults env_keep += PYTHONPATH
Defaults env_keep += OSCADA_INSTALLATION
2.5 Installing CORE Network Emulator (Optional)
This step is optional. To run example on a CORE network follow these steps.
• Install iproute2 4.5+. Clone and install from:https://github.com/shemminger/iproute2
• Install CORE emulator: Follow instructions from here: http://coreemu.github.io/core/install.html. Note: On ubuntu 16.04, the installation process fails to install OSPF-MDR because of conflicting header files. This limits CORE functionality to some extent.
• Edit /usr/local/bin/core-python script by pre-pending the following two lines before the exec statement:
export PYTHONPATH="${PYTHONPATH}:${HOME}/.local/lib/python3.x/site-packages"
export PYTHONPATH="${PYTHONPATH}:/usr/local/lib/python3.x/dist-packages"
where python3.x should be changed according to the python version installed on your system. This ensures that core-python can find other packages installed on your system outside the virtual environment created by CORE.
OpenSCADA Documentation, Release 1.0
2.6 Ready to use VM
A link to use VM containing OpenSCADA and Kronos will be provided upon request. Please contact project- [email protected].
2.7 Uninstalling OpenSCADA
• To uninstall/cleanup run the following command:
sudo ./setup.sh uninstall
2.8 Uninstalling CORE
To uninstall CORE, follow instructions described here:http://coreemu.github.io/core/install.html
2.6. Ready to use VM 5
CHAPTER 3
User Guide
3.1 Prerequisites
The user guide assumes that the reader has working knowledge of Instruction List programming (IL) and its termi- nologies. Interested readers are encouraged to check out the reference book included inreference. Throughout this guide, we will use IEC 61131-3 specific terminologies like resources, configuration, tasks, functions, function blocks and programs which are described in detail in the referenced book.
The user guide also assumes that the reader is familar with google protobufs and GRPC and their associated terminol- ogy.
• For additional reference on protocol buffers please referhere.
• For additional reference on GRPC please referhere.
3.2 Creating a PLC
A PLC in OpenSCADA comprises of a RAM module and one or more CPUs. Each CPU module encompasses an Input and Output Memory module and a description of the program or set of programs to execute on that CPU. Thus a single PLC can define and run multiple Instruction List (IL) programs. To completely describe a PLC, a user would need to provide two types of configuration files:
• A system specification: The system specification file includes hardware specific details about the PLC which includes the number of CPUs, the size of RAM memory and the mean and standard deviation of instruction execution times. The system specification file also declares data-types which are used by all IL programs and global variables which may be used by them. Access variables (described in Advanced Topics) can also be declared and made accesible outside the PLC to communication modules.
• A resource specification: A separate resource specification should be defined for each CPU on the PLC. The resource specification includes the size of Input and Output memory attached to the CPU as well as all global variables specific to the CPU. Periodic and interrupt tasks can be defined in the resource specification. Functions, Function blocks and Programs to be run on that CPU are also defined and attached to tasks.
In subsequent sections, we describe each configuration in detail with examples.
7
3.3 System Specification
The system level specification of the PLC is specified as a prototxt file of the SystemConfiguration proto message format specified insystem_spectification.protofile. In this guide we will use an example of the proto definition given here.
configuration_name: A name given to identify this PLC. In this example it is Pendulum_PLC:
configuration_name: "Pendulum_PLC"
log_level: Logging level. 5 types are supported: LOG_NONE, LOG_NOTICE, LOG_INFO, LOG_ERROR, LOG_VERBOSE.
log_level: LOG_NOTICE
log_file_path: (optional) If specified the logs are stored here.
run_time_secs: (optional) Total time for which the PLC should run. If unspecified, will run forever untill interrupted.
hardware_spec: Description about the PLC’s hardware according to HardwareSpecification message defined insys- tem_spectification.proto. (Full example is not included here for brevity). In this hardware_spec, num_resources spec- ifies the number of CPUs attached to the PLC. Here it is 1. The hardware_spec also includes the mean and standard deviation of execution times of all instructions, system functions and system function blocks. The mean and standard deviation execution times are all specified in nanoseconds:
hardware_spec { num_resources: 1
ram_mem_size_bytes: 10000
# Instruction mean, std execution times, used only in virtual time mode ins_spec {
ins_name: "ADD"
mu_exec_time_ns: 1000 sigma_exec_time_ns: 100 }
ins_spec {
ins_name: "AND"
mu_exec_time_ns: 1000 sigma_exec_time_ns: 100 }
...
# SFCs (System functions) - mean, std of execution times sfc_spec {
sfc_name: "ABS"
mu_exec_time_ns: 2000 sigma_exec_time_ns: 0 }
...
# SFBs (System function blocks) - mean, std of execution times sfb_spec {
sfb_name: "TON"
mu_exec_time_ns: 1500
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) sigma_exec_time_ns: 0
} ...
}
datatype_declaration: Multiple datatypes can be declared in the system specification and later used inside PLC programs or function blocks. A datatype can belong to one of the categories belonging to DataTypeCategory enum defined inconfiguration.proto. Full explanation of the datatype_declaration message will be included in subsequent sections. In this example 4 datatypes are declared.
• An integer typedef datatype called INT_TYPE_DEF. Any variable of this datatype will be an integer with an initial value of 10:
datatype_declaration { name: "INT_TYPE_DEF"
datatype_category: INT datatype_spec {
initial_value: "10"
} }
• A 1-D integer array datatype called INT_1DARR_TYPE_DEF. Any variable of this data type will be a 10 dimensional array initialized to [-1,0,1,2,3,4,5,6,7,8]:
datatype_declaration {
name: "INT_1DARR_TYPE_DEF"
datatype_category: INT datatype_spec {
initial_value: "{-1,0,1,2,3,4,5,6,7,8}"
dimension_1: 10 }
}
• A 2-D integer array datatype called INT_2DARR_TYPE_DEF. Any variable of this data type will be a 2 x 2 matrix initialized to [[0,1],[2,3]]:
datatype_declaration {
name: "INT_2DARR_TYPE_DEF"
datatype_category: INT datatype_spec {
initial_value: "{{0,1},{2,3}}"
dimension_1: 2 dimension_2: 2 }
}
• A complex structure datatype called COMPLEX_STRUCT. It has multiple fields and each field is specified with the datatype name of the field. This could inturn be an elementary/inbuilt datatype or a complex datatype. The list of inbuilt elementary datatypes are described in the IL programming section. In this example the structure has 5 fields with three of them using the 3 previously declared datatypes we just looked at:
datatype_declaration { name: "COMPLEX_STRUCT"
datatype_category: DERIVED datatype_field {
(continues on next page)
3.3. System Specification 9
(continued from previous page) field_name: "string_field"
# String is an elementary datatype. It is a char array of size 1000 field_datatype_name: "STRING"
}
datatype_field {
field_name: "int_field"
field_datatype_name: "INT_TYPE_DEF"
}
datatype_field {
field_name: "real_field"
# Real is an elementary datatype. It is equivalent to float.
field_datatype_name: "REAL"
initial_value: "0.1"
}
datatype_field {
field_name: "oned_arr_field"
field_datatype_name: "INT_1DARR_TYPE_DEF"
}
datatype_field {
field_name: "twod_arr_field"
field_datatype_name: "INT_2DARR_TYPE_DEF"
} }
var_global: Variables which can be used by all programs running on this PLC can be declared in the system specifica- tion. All PLC level global variables are declared in the var_global section. Variable declaration is similar to datatype declaration with an optional interface_type and storage_specification. Interface types assign meaning to the way the variable is interpreted by the PLC program. For instance a variable of interface_type VAR_INPUT is equivalent to an INPUT variable defined in IEC 61131-3 specification. A variable declared inside the var_global section can only optionally have VAR_EXPLICIT_STORAGE interface type.
Storage specifications denote where the variable is stored, i.e whether the address of the variable is backed by RAM/IO memory. They can only be present if the interface type is VAR_EXPLICIT_STORAGE, otherwise the variable is allocated statically and does not point to any byte in the PLC’s RAM or IO memory. Further explation of variable interface_types and storage_specifications are described in the IL programming section.
In this example, 4 PLC level global variables are declared:
• “global_bool_var” is a boolean whose value is stored in RAM byte number 3 and bit number 1 within the byte.
• “global_int_var” is an integer variable which is stored in RAM starting at byte number 4.
• “start_int” is of type INT_TYPE_DEF declared before but it is statically and its memory location is not address- ible.
• “complex_global” is a global variable of type COMPLEX_STRUCT and is stored in RAM memory starting at byte 30:
var_global {
name: "__CONFIG_GLOBAL__"
datatype_field {
field_name: "global_bool_var"
field_datatype_name: "BOOL"
intf_type : VAR_EXPLICIT_STORAGE field_storage_spec {
mem_type: RAM_MEM byte_offset: 3 bit_offset: 1
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) }
}
datatype_field {
field_name: "global_int_var"
field_datatype_name: "INT"
intf_type : VAR_EXPLICIT_STORAGE field_storage_spec {
full_storage_spec: "%MW4"
} }
datatype_field {
field_name: "start_int"
field_datatype_name: "INT_TYPE_DEF"
}
datatype_field {
field_name: "complex_global"
field_datatype_name: "COMPLEX_STRUCT"
intf_type: VAR_EXPLICIT_STORAGE field_storage_spec {
full_storage_spec: "%MW30"
} } }
resource_file_path: Indicates where to find specification this PLC first and only CPU. Since the PLC can have multi- ple CPUs, this field can be repeated:
resource_file_path: "~/OpenSCADA/examples/inverted_pendulum/CPU_001.prototxt"
3.4 Resource Specification
The specification of a PLC’s CPU is provided as a prototxt file of the ResourceSpecification proto message format specified inconfiguration.protofile. In this guide we will use an example of the proto definition givenhere.
resource_name: Name of the CPU:
resource_name: "CPU_001"
input_mem_size_bytes: Input memory size of the CPU. Sensors may be connected to this:
input_mem_size_bytes: 10000
output_mem_size_bytes: Output memory size of the CPU. Actuators may be connected to this:
output_mem_size_bytes: 10000
resource_global_var: Global variables which are visible to only Program Organization Units (POU) defined in this resource: Function blocks (FB), Functions (FN) and Programs (PROGRAM). In this example two CPU level global variables are defined. “current_theta” is stored in byte number 1 of the input memory whereas “force” is stored at byte number 1 of the output memory:
resource_global_var {
name: "__RESOURCE_GLOBAL__"
datatype_field {
(continues on next page)
3.4. Resource Specification 11
(continued from previous page) field_name: "current_theta"
field_datatype_name: "REAL"
intf_type : VAR_EXPLICIT_STORAGE field_storage_spec {
mem_type: INPUT_MEM byte_offset: 1 bit_offset: 0 }
}
datatype_field {
field_name: "force"
field_datatype_name: "REAL"
intf_type : VAR_EXPLICIT_STORAGE field_storage_spec {
mem_type: OUTPUT_MEM byte_offset: 1 bit_offset: 0 }
} }
pou_var: A CPU can define multiple pou_var messages. These describe a POU. A POU could be of type FB, FN or PROGRAM and they contain and interface declaration and a code body to hold all the IL instructions associated with POU. POUs are equivalent to functions in the traditional programming sense but FBs and PROGRAMs can hold and update some internal state which can be used each time they are invoked. For further details on the expected behaviour of FBs, FNs and PROGRAMs, please refer to the included book. More details on how to define POUs and their interfaces and code bodies would be included in the IL programming section.
In this example, 3 POUs are defined. Two of them are Function Blocks, while one of them is a PROGRAM. Each POU has its interface specified as a set of datatype_field and code specified in a code_body section:
pou_var {
name: "DivideFB"
pou_type: FB datatype_field {
...
}
datatype_field { ...
} ...
code_body {
insn: "LD 0.0"
...
} }
pou_var {
name: "GetControlInputFB"
pou_type: FB datatype_field {
...
} ...
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) code_body {
...
} }
pou_var {
name: "PID_CONTROL"
pou_type: PROGRAM datatype_field {
...
} ...
code_body { ...
} }
Note: POUs defined in each resource must have unique names. This is not an IEC 61131-3 requirement but rather a current limitation in OpenSCADA design for simplicity.
Note: POUs defined in one resource specification cannot be referenced or used in another resource specification.
interval_task: This field is used to specify a task which gets invoked periodically according to the specified period.
Programs and Function Blocks could be attached to this task and periodically invoked. In OpenSCADA, for each CPU, only one interval task can be specified. In this example, the interval task is called “CYCLIC_TASK” and it executes one every 10ms.:
interval_task {
task_name: "CYCLIC_TASK"
priority: 1
interval_task_params { interval_ms: 10 }
}
programs: Used to attach POUs to tasks. POUs can be attached to tasks with the programs field. It is a message of type ProgramSpecification described inconfiguration.proto. Each programs field defines a mapping between a POU of interest and a task of interest. Multiple POUs can be attached to the same task. A separate programs field is used for each attachement. In this example the POU “PID_CONTROL” is attached with the task “CYCLIC_TASK”. Thus the program “PID_CONTROL” gets invoked every 10ms.:
programs {
program_name: "PID_CONTROL"
pou_variable_type: "PID_CONTROL"
task_name: "CYCLIC_TASK"
initialization_maps {
pou_variable_field_name: "dummy_in"
mapped_variable_field_name: "start_int"
}
initialization_maps {
pou_variable_field_name: "dummy_out"
mapped_variable_field_name: "global_int_var"
(continues on next page)
3.4. Resource Specification 13
(continued from previous page) }
}
Arguments to each invocation of the POU can be passed through initialization_maps (a subfield of the ProgramSpeci- fication message). Initialization maps can be used to initialize the POUs input/inout variables as well as specify where output variables could be stored after the invocation. In this example, the program “PID_CONTROL“‘s input variable
“dummy_in” at the start of every invocation is assigned the value of the global variable “start_int” (which was specified in the System specification i.e, in the previous section). At the end of the invocation, the value of the output variable of the PROGRAM called “dummy_out” is copied to “global_int_var” (which was created in the previous section as a PLC level global variable). It must be noted that the mapped_variable_field_name for an VAR_INPUT variable could also be an immediate value. For instance in the above example, if “dummy_in” needs to be initialized with 10 for every invocation, the initialization map could be modified to:
initialization_maps {
pou_variable_field_name: "dummy_in"
mapped_variable_field_name: "10"
}
3.5 Instruction List Programming
This section of the guide describes how Instruction List (IL) programs can be written in OpenSCADA. It assumes that the reader has some familiarity with IL programs and is not designed to be a tutorial for IL programming. It must be noted that the IL backend in OpenSCADA is designed to be simple and it does not exhaustively include all features specified in IEC-61131-3. A Few of them have been omitted in the implementation for convenience and they are documented here.
We first describe the fundamental building blocks of IL programs in OpenSCADA: DataTypes, Program Organization Units (POUs) and Tasks. Then, the capabilities of the current implementation are described including the set of supported IL instructions, System Functions and System Function Blocks. Finally, some of the missing IL specific features are documented for future work.
3.5.1 DataTypes
DataTypes are a fundamental part of any IL program. Each datatype in OpenSCADA is associated with a datatype category, datatype name and optional dimension attributes. If a dimension attribute is specified with the datatype definition, then it is interpreted as an array.
DataTypes are primarily two categories: Elementary and Non-Elementary. Each category has many subcategories.
Elementary data types have been implicitly defined and they do not need to be declared during system specification.
The table given below lists all the elementary data type names, their categories and size in bits. It also includes an example string representation of a value of the data type. (Note that strings in OpenSCADA are character arrays with fixed length of 1000)
OpenSCADA Documentation, Release 1.0
DataType Name Category Size in Bits Example
BOOL BOOL 1 “TRUE”
BYTE BYTE 8 “16#A1”
WORD WORD 16 “16#A102”
DWORD DWORD 32 “16#A1010101”
LWORD LWORD 64 “16#B101010101010101”
CHAR CHAR 8 ‘a’
USINT USINT 8 “123”
SINT SINT 8 “-123”
UINT UINT 16 “123”
INT INT 16 “123”
UDINT UDINT 32 “123”
DINT DINT 32 “123”
ULINT ULINT 64 “123”
LINT LINT 64 “123”
REAL REAL 32 “0.2”
LREAL LREAL 64 “0.234”
TIME TIME 64 “t#1.2s”
DATE DATE 96 “d#02-01-2010”
TOD TIME_OF_DAY 96 “tod#23:59:59”
DT DATE_AND_TIME 192 “dt#02-01-2010 23:59:59”
STRING CHAR 8000 “Hello how are you!”
Declaring a data type: Custom data types can be declared in the system specification protoxt file using a datatype_declaration field of type DataType message defined inconfiguration.proto. Its fields are explained below:
• name: Name of the datatype
• datatype_category: Category of the datatype. If a custom datatype is to be defined, the category needs to be specified as DERIVED. A DERIVED datatype cannot have datatype field of POU category. By default, it is set to POU.
• pou_type: If the datatype_category is POU, this field denotes the type of POU: i.e Function (FN), Function Block (FB) or Program (PROGRAM)
• datatype_field: Must be specified only if datatype_category is POU or DERIVED. This describes an individual field of the complex datatype.
– field_name: Name of the field
– field_datatype_name: Name of the datatype of the field. It cannot be a POU category datatype if the parent datatype_category is not POU
– initial_value: (string) May only be specified if the category of this field’s datatype belongs to one of the categories in the table listed above
– dimension_1: must only be specified if this field is supposed to be an array – dimension_2: must only be specified if this field is supposed to be a 2d-array
– intf_type: Refers to interface type of the field. It may only be specified if the parent datatype_category is POU. The interface type of a field changes the way it is interpreted upon POU’s invocation. Since POUs contain the execution logic and need to be invoked, its fields are treated as arguments and return values. Field interface types decide which fields are input arguments, return values, both or neither. There are several supported field interface types which are described here:
* VAR_INPUT: The field is treated as an input variable. Its value can only be read within the called POU and can only be set from the calling POU
* VAR_OUTPUT: The field is treated as an output variable. Its value can only be set within the called POU and it can only be read from the calling POU
3.5. Instruction List Programming 15
* VAR_IN_OUT: The field is treated as both input/output variable. During the time of POU invocation, another variable from calling POU’s scope is assigned to this field. It could be modified within the called POU and these modifications reflect changes in the calling POU as well.
* VAR: The field is internal to the POU and equivalent to a local variable. But unlike local variable in the traditional sense, its value is retained across invocations if the PoUType is a PROGRAM or FB. It cannot be used in a FN.
* VAR_TEMP: The field is internal to the POU and equivalent to a local variable.
* VAR_EXTERNAL: The field is equivalent to an extern variable. It cannot be accessed/assigned during POU invocation. It points to global variables declared at the resource/PLC level. The field name and field datatype names must match these previously declared global variables.
The field can be accessed for read/write operations within the called POU.
* VAR_EXPLICIT_STORAGE: The field is backed by an address in memory (RAM or IO). It is equivalent to a directly represented variable in IL terminology. These variables cannot be accessed/assigned during POU invocation but any changes to the associated memory location is reflected in the affected logic.
* VAR_ACCESS: Discussed in Advanced Topics
– field_storage_spec: If the intf_type is VAR_EXPLICIT_STORAGE, then details on the backing mem- ory address is specified here. It includes memory type, byte offset and bit offset. The bit offset is ignored unless the field’s datatype category is BOOL. The field_storage_spec may also be specified as a string in short form: e.g “%M4.1” to denote byte 4, bit 1 in RAM or “%I2.0” to denote byte 2, bit 0 in Input memory or “%Q3.0” to denote byte 3, bit 0 in Output memory.
– field_qualifier: Field qualifiers are additional optional attributes which could assigned to (1) VAR_INPUT or VAR_EXPLICIT_STORAGE boolean fields (2) VAR_ACCESS fields. For VAR_INPUT/VAR_EXPLICIT_STORAGE boolean fields, two qualifiers are allowed R_EDGE and F_EDGE which denote rising edge and falling edge respectively. When a boolean field with a R_EDGE qualifier is read inside the called POU, the read operation returns TRUE iff the field ex- perienced a rising edge transition. Similarly, when a boolean field with a F_EDGE qualifier is read inside the called POU, the read operation returns TRUE iff the field experienced a falling edge transi- tion. Field qualifiers for VAR_ACCESS fields are discussed later in Advanced Topics.
• datatype_spec: A datatype declaration may also optionally have a datatype_spec field. datatype_spec may only be specified for DataTypes where datatype_category is not in {POU, DERIVED}. It could be used to assign initial_values and convert this datatype into an array:
– initial_value: Specified as a string only if the datatype_category is present in the table listed above – dimension_1: Must only be specified if this datatype is supposed to be a typedef of an ARRAY of
[datatype_category] i.e if datatype_category is INT and if datatype_spec with dimension_1 = 10 is specified, then this datatype is a typedef of an integer array of size 10 (ARRAY[10] of INT)
– dimension_2: Must only be specified if this datatype is typedeffing a 2d-array
• code_body: It contains the set of instructions to execute upon a POU’s invocation. Thus it may only be specified if the dataype_category is POU
3.5.1.1 Illustrations
Declaring a new datatype called “TIME_TYPE_DEF” which is a typedef of the elementary TIME datatype. The initial value of any field of this datatype would be “t#1s”:
OpenSCADA Documentation, Release 1.0
datatype_declaration { name: "TIME_ALIAS"
datatype_category: TIME datatype_spec {
initial_value: "t#1s"
} }
Declaring a new datatype called “INT_1DARR” which is a 1-D INT array of size 10 with initial values: {- 1,0,1,2,3,4,5,6,7,8}:
datatype_declaration { name: "INT_1DARR"
datatype_category: INT datatype_spec {
initial_value: "{-1,0,1,2,3,4,5,6,7,8}"
dimension_1: 10 }
}
Declaring a new datatype called “INT_2DARR” which is a 2-D INT array of size 2 x 2 with initial values:
{{0,1},{2,3}}:
datatype_declaration { name: "INT_2DARR"
datatype_category: INT datatype_spec {
initial_value: "{{0,1},{2,3}}"
dimension_1: 2 dimension_2: 2 }
}
Declaring a structure called “COMPLEX_STRUCT_1” with multiple fields:
datatype_declaration { name: "COMPLEX_STRUCT_1"
datatype_category: DERIVED datatype_field {
# This field is a character array of length 1000 field_name: "string_field"
field_datatype_name: "STRING"
}
datatype_field {
# This field is an integer field_name: "int_field"
field_datatype_name: "INT"
}
datatype_field {
# This field is a REAL number and its initial value is 0.1 field_name: "real_field"
field_datatype_name: "REAL"
initial_value: "0.1"
}
datatype_field {
# This field is a 1D integer array of size 10 with initial values {-1,0,1,2,3,
˓→4,5,6,7,8}
(continues on next page)
3.5. Instruction List Programming 17
(continued from previous page) field_name: "oned_arr_field"
field_datatype_name: "INT_1DARR"
}
datatype_field {
# This field is a 2D integer array of size 2 x 2 with initial values {{0,1},
˓→{2,3}}
field_name: "twod_arr_field"
field_datatype_name: "INT_2DARR"
} }
Declaring a nested structure with mixed type of fields i.e both elementary and non-elementary:
datatype_declaration { name: "COMPLEX_STRUCT_2"
datatype_category: DERIVED datatype_field {
# Note: this is a field of the previously declared COMPLEX_STRUCT_1 datatype field_name: "complex_field"
field_datatype_name: "COMPLEX_STRUCT_1"
}
datatype_field {
field_name: "int_field"
field_datatype_name: "INT_TYPE_DEF"
}
datatype_field {
field_name: "word_field"
field_datatype_name: "WORD"
initial_value: "16#1"
}
datatype_field {
field_name: "time_field"
field_datatype_name: "TIME"
}
datatype_field {
field_name: "date_field"
field_datatype_name: "DATE"
}
datatype_field {
field_name: "date_tod_field"
field_datatype_name: "DT"
}
datatype_field {
field_name: "tod_field"
field_datatype_name: "TOD"
} }
Declaring a nested structure with fields which are arrays of derived datatypes:
datatype_declaration { name: "COMPLEX_STRUCT_3"
datatype_category: DERIVED datatype_field {
# Note that this field is a 1-D ARRAY of [COMPLEX_STRUCT_2] with two elements field_name: "complex_vector"
field_datatype_name: "COMPLEX_STRUCT_2"
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) dimension_1: 2
} }
3.5.2 Program Organization Units (POUs)
Program Organization Units are special categories of DataTypes which include program logic as well. POUs are blocks of code with a calling interface. The fields of a POU form its calling interface and each field is associated with an interface type (as discussed in the previous section) which decides whether the field is INPUT/OUTPUT or INOUT.
There are three types of POUs: Functions (FN), Function Blocks (FBs) and PROGRAMS. Some of the IL specific rules about these POUs are re-stated here briefly for completeness. For further details, please refer the attached book.
Functions are stateless i.e every invocation of an Function with the same input produces the same output. Functions cannot have fields with interface type VAR. Function Blocks and Programs are stateful operations. Each FB, PRO- GRAM is like a class and variables of their type are like objects of the class. Invocations of a variable of type FB or PROGRAM can store state which may be used by the logic during its next invocation.
Here, we illustrate how POUs can be written and how then can invoke other POUs to perform various tasks. We used the PID control example givenhere. The first POU we look at is a Function Block called DivideFB which takes a Dividend, Divisor as inputs and returns a Quotient and Reminder. It also sets an error flag if the divisor is 0:
pou_var {
name: "DivideFB"
pou_type: FB datatype_field {
field_name: "Dividend"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field {
field_name: "Divisor"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field {
field_name: "Quotient"
field_datatype_name: "INT"
intf_type: VAR_OUTPUT }
datatype_field {
field_name: "DivRem"
field_datatype_name: "REAL"
intf_type: VAR_OUTPUT }
datatype_field {
field_name: "DivError"
field_datatype_name: "BOOL"
intf_type: VAR_OUTPUT }
code_body {
insn: "LD 0.0"
insn: "EQ Divisor"
insn: "JMPC Error"
insn: "LD Dividend"
(continues on next page)
3.5. Instruction List Programming 19
(continued from previous page) insn: "DIV Divisor"
insn: "REAL_TO_INT"
insn: "ST Quotient"
insn: "MUL Divisor"
insn: "ST DivRem"
insn: "LD Dividend"
insn: "SUB DivRem"
insn: "ST DivRem"
insn: "LD FALSE"
insn: "ST DivError"
insn: "JMP End"
insn: "Error: LD 0"
insn: "ST Quotient"
insn: "ST DivRem"
insn: "LD TRUE"
insn: "ST DivError"
insn: "End: RET"
} }
The fields Dividend, Divisor, Quotient, DivRem and DivError together form the Calling Interface of the FB. Divided and Divisor have intf_type set to VAR_INPUT which means that a POU which calls a variable of type DivideFB, needs to set/assign values to these two fields. The FB inturn outputs three fields Quotient, DivRem and DivError which can be read by the calling POU as we show next. We do not explain the logic of the FB here but we highly encourage the reader to do so.
Note: One caveat needs to pointed out with respect to Labels embedded inside instructions: The labels should be inline with the instruction i.e. there cannot be a Label with no associated instruction in the same line. In the above example “Error: LD 0” cannot be split into two separate lines with one line containing the label “Error” and the other line containing the instruction “LD 0”.
In the next step, we look at one other FB and a PROGRAM which invokes both. The GetControlInputFB given below has 4 INPUT, 1 INOUT, 1 OUTPUT and 5 TEMP fields. The TEMP fields are local variables with constant values.
We omit its code here for brevity.:
pou_var {
name: "GetControlInputFB"
pou_type: FB datatype_field {
field_name: "time_delta"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field {
field_name: "curr_error"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field {
field_name: "prev_error"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field {
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) field_name: "integral"
field_datatype_name: "REAL"
intf_type: VAR_INPUT }
datatype_field { field_name: "g"
field_datatype_name: "REAL"
initial_value: "9.81"
intf_type: VAR_TEMP }
datatype_field { field_name: "Kp"
field_datatype_name: "REAL"
initial_value: "-150.0"
intf_type: VAR_TEMP }
datatype_field { field_name: "Kd"
field_datatype_name: "REAL"
initial_value: "-20.0"
intf_type: VAR_TEMP }
datatype_field { field_name: "Ki"
field_datatype_name: "REAL"
initial_value: "-20.0"
intf_type: VAR_TEMP }
datatype_field {
field_name: "derivative"
field_datatype_name: "REAL"
intf_type: VAR_TEMP }
datatype_field {
field_name: "integral"
field_datatype_name: "REAL"
intf_type: VAR_IN_OUT }
datatype_field {
field_name: "force"
field_datatype_name: "REAL"
intf_type: VAR_OUTPUT }
code_body { ...
} }
In the calling interface of the PID_CONTROL program, notice two fields “div” and “get_control” whose datatypes are the two previously defined Function Blocks. This is an example of an instantiation of a POU in IL. The fields
“div” and “get_control” are objects of type “DivideFB” and “GetControlInputFB” respectively. These objects can be invoked/called within the POU to execute their embedded logic. Also notice, two other fields “current_theta” and
3.5. Instruction List Programming 21
“force” which are declared as VAR_EXTERNAL. These are global variables which are defined at the resource level in the resource specificationfile:
pou_var {
name: "PID_CONTROL"
pou_type: PROGRAM datatype_field {
field_name: "dummy_in"
field_datatype_name: "INT"
intf_type: VAR_INPUT }
datatype_field {
field_name: "dummy_out"
field_datatype_name: "INT"
intf_type: VAR_OUTPUT }
datatype_field {
field_name: "prev_time"
field_datatype_name: "TIME"
intf_type: VAR }
datatype_field {
field_name: "prev_theta"
field_datatype_name: "REAL"
intf_type: VAR }
datatype_field {
field_name: "prev_error"
field_datatype_name: "REAL"
intf_type: VAR }
datatype_field {
field_name: "prev_integral"
field_datatype_name: "REAL"
initial_value: "0.0"
intf_type: VAR }
datatype_field { field_name: "div"
field_datatype_name: "DivideFB"
intf_type: VAR }
datatype_field {
field_name: "get_control"
field_datatype_name: "GetControlInputFB"
intf_type: VAR }
datatype_field {
field_name: "started"
field_datatype_name: "BOOL"
initial_value: "False"
intf_type: VAR }
datatype_field {
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) field_name: "curr_time"
field_datatype_name: "TIME"
intf_type: VAR_TEMP }
datatype_field {
field_name: "time_delta"
field_datatype_name: "REAL"
intf_type: VAR_TEMP }
datatype_field {
field_name: "two_pi"
field_datatype_name: "REAL"
initial_value: "6.28"
intf_type: VAR_TEMP }
datatype_field { field_name: "tmp"
field_datatype_name: "REAL"
initial_value: "0.0"
intf_type: VAR_TEMP }
datatype_field {
field_name: "curr_error"
field_datatype_name: "REAL"
intf_type: VAR_TEMP }
datatype_field {
field_name: "current_theta"
field_datatype_name: "REAL"
intf_type: VAR_EXTERNAL }
datatype_field {
field_name: "force"
field_datatype_name: "REAL"
intf_type: VAR_EXTERNAL }
code_body {
# If not started, store prev_time, prev_error insn: "LD started"
insn: "JMPC already_started"
insn: "LD TRUE"
insn: "ST started"
insn: "GTOD"
insn: "ST prev_time"
insn: "CAL div(Dividend:= current_theta, Divisor:= two_pi)"
insn: "LD div.DivRem"
insn: "ST tmp"
insn: "LD 3.14"
insn: "LT tmp"
insn: "JMPCN else1"
insn: "LD tmp"
insn: "SUB two_pi"
insn: "ST tmp"
insn: "else1: LD tmp"
insn: "ST prev_error"
(continues on next page)
3.5. Instruction List Programming 23
(continued from previous page) insn: "RET"
insn: "already_started: GTOD"
insn: "ST curr_time"
insn: "SUB prev_time"
insn: "TIME_TO_REAL"
insn: "ST time_delta"
insn: "LD curr_time"
insn: "ST prev_time"
insn: "CAL div(Dividend:= current_theta, Divisor:= two_pi)"
insn: "LD div.DivRem"
insn: "ST tmp"
insn: "LD 3.14"
insn: "LT tmp"
insn: "JMPCN else"
insn: "LD tmp"
insn: "SUB two_pi"
insn: "ST tmp"
insn: "else: LD tmp"
insn: "ST curr_error"
insn: "CAL get_control(time_delta:= time_delta, curr_error:= curr_error, prev_
˓→error:= prev_error, integral:= prev_integral)"
insn: "LD get_control.force"
insn: "ST force"
insn: "LD curr_error"
insn: "ST prev_error"
insn: "RET"
} }
The DivideFB object is called in the line:
CAL div(Dividend:= current_theta, Divisor:= two_pi)
Its INPUT variables are set to current_theta and two_pi which are both fields in the calling POU i.e PID_CONTROL.
After the invocation, one of its output is read and subsequently stored in another variable called tmp:
LD div.DivRem ST tmp
Instead, this can also be accomplished in one line as follows:
CAL div(Dividend:= current_theta, Divisor:= two_pi, DivRem=>tmp) Similarly, the GetControlInputFB object is called in the line:
CAL get_control(time_delta:= time_delta, curr_error:= curr_error, prev_error:= prev_
˓→error, integral:= prev_integral)
Its INPUT and INOUT variables are set during the invocation and its output field is read in the next line:
LD get_control.force
On the contrary to Function Blocks and Programs, Functions cannot be instantiated with variables. Functions like TIME_TO_REAL and REAL_TO_INT (which are SFCs both used in these FBs and PROGRAM) are simply invoked as:
OpenSCADA Documentation, Release 1.0
FUNCTION_NAME operand1, operand2, ..., operandN
and the result of the Function can be loaded from the Accumulator after its invocation. Functions in OpenSCADA can return only one value and the specified operands are mapped to corresponding fields of the function in the same order.
3.5.2.1 Supported Instructions
OpenSCADA ships with the following IL instructions. Some of these instructions can take multiple operands or a variable number of operands. For further details on each instruction, please consult the included references:
Instructions
ADD GE LT NOT
AND GT MOD OR
DIV LD MUL SHL
EQ LE NE SHR
ST SUB XOR CAL
JMP
3.5.2.2 Supported System Functions
OpenSCADA ships with the following System Functions (SFCs). Some of these SFCs can take multiple operands or a variable number of operands. For further details on each instruction, please consult the included references:
System Functions (SFCs)
ABS ACOS ASIN ANY_TO_ANY
ASIN ATAN COS EXP
GTOD LIMIT LN LOG
MAX MIN MUX SEL
SIN SQRT TAN
Note: The ANY_TO_ANY SFC is not actually a single SFC but a set of SFCs which are used for type casting. For example the TIME_TO_REAL and REAL_TO_INT SFCs used in the previous example are a part of this class of SFCs. For a full list of allowed type casting SFCs, consult the included references.
3.5.2.3 Supported System Function Blocks
OpenSCADA ships with following System Function Blocks (SFBs). For further details on each instruction, please consult the included references:
System Function Blocks (SFBs)
SR RS R_TRIG F_TRIG
CTU CTD CTUD TP
TON TOFF
3.5. Instruction List Programming 25
3.5.3 Tasks
POUs merely contain the calling interface and the logic to be executed upon invocation. Tasks specify conditions underwhich a POU is invoked. There are type types of tasks:
• Interval Tasks: Interval Tasks are executed periodically at a specified period. In OpenSCADA, each CPU can have atmost one associated interval task. Interval tasks are specified using the interval_task field in Resource Specification. The following example defines an interval_task called CYCLIC_TASK which has a period of 10ms:
interval_task {
task_name: "CYCLIC_TASK"
priority: 1
interval_task_params { interval_ms: 10 }
}
• Interrupt Tasks: Interrupt Tasks are configured with a boolean trigger variable which could be a PLC level global variable or a resource level global variable. Interrupt tasks are invoked when the trigger variable goes from FALSE to TRUE i.e during a positive transition. In OpenSCADA, each CPU can have multiple associated interrupt tasks and interrupt tasks always have a higher priority over interval tasks and can pre-empt any currently running interval task. If multiple interrupt tasks become simultaneously active, they are executed in the order of their priority. The trigger condition is checked after each instruction execution. The following example defines an interrupt_task called INTERRUPT_TASK which is triggered by “global_bool_var” (which is global variable) defined in System Specification:
interrupt_task {
task_name: "INTERRUPT_TASK"
priority: 1
interrupt_task_params {
trigger_variable_field: "global_bool_var"
} }
3.5.3.1 Attaching POUs to Tasks
Task definition by itself does not specify which POUs are attached to a task. In OpenSCADA, POUs of type FBs and PROGRAMS can be attached to Tasks using the programs field of the Resource Specification. It is a message of type ProgramSpecification described inconfiguration.proto. Each programs field defines a mapping between a POU of interest and a task of interest. Multiple POUs can be attached to the same task. A separate programs field is used for each attachement. In this example the POU “PID_CONTROL” is attached with the task “CYCLIC_TASK”. Thus the program “PID_CONTROL” gets invoked every 10ms.:
programs {
program_name: "PID_CONTROL"
pou_variable_type: "PID_CONTROL"
task_name: "CYCLIC_TASK"
initialization_maps {
pou_variable_field_name: "dummy_in"
mapped_variable_field_name: "start_int"
}
initialization_maps {
pou_variable_field_name: "dummy_out"
mapped_variable_field_name: "global_int_var"
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page) }
}
Arguments to each invocation of the POU can be passed through initialization_maps (a subfield of the ProgramSpeci- fication message). Initialization maps can be used to initialize the POUs input/inout variables as well as specify where output variables could be stored after the invocation. In this example, the program “PID_CONTROL“‘s input variable
“dummy_in” at the start of every invocation is assigned the value of the global variable “start_int” (which was specified in the System specification i.e, in the previous section). At the end of the invocation, the value of the output variable of the PROGRAM called “dummy_out” is copied to “global_int_var” (which was created in the previous section as a PLC level global variable). It must be noted that the mapped_variable_field_name for an VAR_INPUT variable could also be an immediate value. For instance in the above example, if “dummy_in” needs to be initialized with 10 for every invocation, the initialization map could be modified to:
initialization_maps {
pou_variable_field_name: "dummy_in"
mapped_variable_field_name: "10"
}
3.6 Starting the PLC
The PLC can be started after the System Specification file and referenced Resource specification files are provided. A PLC can be started in normal mode or in virtual time mode under Kronos’s control. The PLC can be started as follows:
plc_runner [Options [-ens]] -f <path_to_system_spec prototxt file>
where the following options are optional:
• -e: 1 or 0 to enable/disable Kronos. Default is 0.
• -n: num_insns_per_round - only valid if Kronos is enabled. Please refer Kronos documentationhere.
• -s: relative cpu speed - only valid if Kronos is enabled. Please refer Kronos documentationhere.
The Kronos specific options are used to compute the virtual timestep size for each round. Before starting a PLC in vir- tual time mode, Kronos needs to be installed and loaded. In a subsequent section titled “Building Co-Simulations”, we discuss in detail, the steps involved in starting a PLC in virtual time mode and subsequently advancing the emulation in virtual time.
Starting a PLC in normal mode would be useful for debugging purposes. The PLC can be stopped at any time in normal mode by pressing Ctrl-C. Otherwise it would run for the specified run_time_secs duration configured in the System specification.
3.7 PLC I/O
This section discusses how to interact with a PLC to read its CPU’s output memory and set values to its CPU’s input memory. The ability to write values to the input memory of a PLC’s CPU is necessary to simulate sensors in the physical environment. Sensors typically measure quantities in the physical system and these measured values appear in the input memory. The ability to read values from the output memory of a PLC’s CPU is necessary to simulate actuators. The PLC’s logic typically writes values to the output memory and this is sent to one or more connected actuators. To write/read from input/output memories of a PLC’s CPU, OpenSCADA provides a unified grpc interface.
A grpc server is started and pointed to a directory containing the system specification files of all interested PLCs:
3.6. Starting the PLC 27
pc_grpc_server <Path to directory containing system specification files of all PLCs>
The GRPC server binds to localhost and listens on port 50051. It can be quit any time by pressing Ctrl-C (i.e by sending an interrupt signal).
For every PLC, the GRPC server upon startup reads its system spec file and creates a memory mapped files to store the contents of the input/output memories of each CPU. The PLC itself doesn’t need to be running at this time and it could even be started after the GRPC server has been setup. When the PLC is launched, it will re-use the already created memory mapped files for the input/output memories of each CPU. Through memory mapped I/O, the GRPC server would be able to write to a CPU’s input memory or read from its output memory. GRPC requests can be sent by external processes to write(read) to(from) the input(output) memory of a specific CPU in a particular PLC. In the following sections, we show how to construct grpc requests to write to input memory and read from output memory
3.7.1 Simulating Sensors
Sensors are attached to a specific CPU of a PLC and write to its input memory. To simulate a sensor, the SetSensorInput defined inmem_access.protocan be used. It needs a request message of type SensorInput with the following fields which need to be set:
• PLC_ID: ID of the PLC or the configuration_name specified in its system specification
• ResourceName: CPU Name inside the PLC
• MemType: Should typically be 0 - to denote Input Memory
• ByteOffset: Starting byte address inside the memory
• BitOffset: Starting bit address inside the memory. Ignored unless variable datatype is BOOL.
• VariableDataTypeName: Name of the variable data type. For instance if the variable data type is INT, then the memory location referred to by the ByteOffset is interpreted as an INT pointer. The value passed in the ValueToSet field is storedover 2 bytes (since sizeof(INT) is 2 bytes) starting at ByteOffset. Only elementary data type names specified in configuration.proto are allowed.
• ValueToSet: String representation of the value to set. Note that each datatype has a unique string representation as discussed in the IL-Programming section.
3.7.1.1 Illustration
The following python function accepts a floating point argument called “curr_theta” and copies it to the input memory of CPU: “CPU_001” belonging a PLC called “Pendulum_PLC”:
def set_sensor_input(self, curr_theta):
try:
with grpc.insecure_channel('localhost:50051') as channel:
stub = mem_access_grpc.AccessServiceStub(channel) response = stub.SetSensorInput(
mem_access_proto.SensorInput(
PLC_ID='Pendulum_PLC', ResourceName='CPU_001', MemType=0,
ByteOffset=1, BitOffset=0,
VariableDataTypeName="REAL", ValueToSet = str(curr_theta))) except Exception as e:
pass
OpenSCADA Documentation, Release 1.0
3.7.2 Simulating Actuators
Actuators are attached to a specific CPU of a PLC and read from its output memory. To simulate an actuator, the GetActuatorOutput defined inmem_access.protocan be used. It needs a request message of type ActuatorOutput with the following fields which need to be set:
• PLC_ID: ID of the PLC or the configuration_name specified in its system specification
• ResourceName: CPU Name inside the PLC
• MemType: Should typically be 0 - to denote Input Memory
• ByteOffset: Starting byte address inside the memory
• BitOffset: Starting bit address inside the memory. Ignored unless variable datatype is BOOL.
• VariableDataTypeName: Name of the variable data type. For instance if the variable data type is INT, then the memory location referred to by the ByteOffset is interpreted as an INT pointer. The content of memory location over 2 bytes (since sizeof(INT) is 2 bytes) starting at ByteOffset is returned. Only elementary data type names specified in configuration.proto are allowed.
3.7.2.1 Illustration
The following python function returns a floating point number starting at byte 1 of the output memory of CPU:
“CPU_001” belonging to a PLC called “Pendulum_PLC”:
def get_actuator_output(self):
try:
with grpc.insecure_channel('localhost:50051') as channel:
stub = mem_access_grpc.AccessServiceStub(channel) response = stub.GetActuatorOutput(
mem_access_proto.ActuatorOutput(
PLC_ID='Pendulum_PLC', ResourceName='CPU_001', MemType=1,
ByteOffset=1, BitOffset=0,
VariableDataTypeName="REAL")) if response.status == "SUCCESS":
return float(response.value) return 0.0
except Exception as e:
return 0.0
3.8 PLC Communication
OpenSCADA ships with a modbus communication module which can be attached to a specific CPU in a PLC. The built-in modbus communication module connects to the input memory of the CPU and RAM memory of the PLC. It listens for modbus requests to read from the input registers and read/write to the RAM memory. A modifiedlibmodbus library included with the repository can be used to build hmi clients which send modbus requests to the communication module.
3.8. PLC Communication 29
3.8.1 Starting the Modbus Communication Module
The built-in modbus communication module used the CommModule Interface definedhere. In the Advanced Topics section we will describe how this interface can be used to build more complex communication modules.
The simple modbus communication module included with the installation can be started with the command:
modbus_comm_module [Options [-ipr]] -f <path_plc_spec_prototxt>
where the following options are optional:
• -i: IP address to bind to. Default is localhost
• -p: Port to listen on. Default is 1502
• -r: Resource Name/CPU Name to attach to. This CPU must be present in the PLC.
This command starts a modus server listening on at the specified ip address, port and attaches creates a memory mapped file of the PLC’s RAM and the CPU’s input memory if not already present. If the PLC has already been started, it just attaches to the already created memory mapped files.
3.8.2 Illustration
An example hmi client is includedhere. It uses a slightly modified libmodbus library which is documentedhere. The example hmi client sends a modbus read request every 100 milliseconds to read from input registers 0 to 3. In Modbus terminology, each register is 2 bytes and thus register 0 refers to input memory bytes 0 and 1:
// reads registers starting at 0 and ending at 3 i.e registers 0,1 and 2 or total of
˓→6 bytes
rc = modbus_read_input_registers(ctx, 0, 3, input_mem_registers);
From the read input registers, it extracts floating point value (of size 4 bytes) starting from byte number 1:
input_mem_bytes = (uint8_t *) input_mem_registers;
memcpy(¤t_theta, &input_mem_bytes[1], sizeof(float));
Note: input_mem_registers which is a (unit16_t*) array needs to casted into a (unit8_t*) array to interpret read values as bytes instead of registers.
Further examples on how to design clients to write values can be foundhere.
3.9 Simulating Physical Systems
OpenSCADA PLCs can communicate with simulations of physical systems or processes. To interact with PLCs, a proxy Sensor/Actuator driver can be built using the GRPC API already discussed in the Section PLC I/O.
An abstract python class called PhysicalSystemSim is defined inphysical_system_sim.pyand every physical system simulation must implement an object derived from this class:
class PhysicalSystemSim:
"""Generic Physical system simulation abstract class.
Any physical system simulator which needs to interact with OpenSCADA PLCs needs to implement this Class.
(continues on next page)
OpenSCADA Documentation, Release 1.0
(continued from previous page)
"""
__metaclass__ = ABCMeta def __init__(self, **kwargs):
pass
@abstractmethod
def progress(self, timestep_secs):
"""This method is called internally to advance the simulation by a step size.
The OpenSCADA emulation_driver.py invokes this function. A physical system simulator should implement the logic to advance its simulation by
timestep_secs. During this progress, it may set sensor input's of PLCs and get Actuator outputs from PLC's and act accordingly.
Args:
timestep_secs (float): Timestep to advance in secs.
Returns:
None Raises:
None
"""
pass
A specific implementation of a physical system simulation must inherit from this abstract class and register itself with an object of type EmulationDriver which is a built-in class defined insideemulation_driver.py. The progress method needs to be implemented by the inherited object. It is called implicitly by the emulation_driver and when upon each call, the logic embedded in the physical system simulation should be advance the simulation for the specified duration in seconds and pause it. During this progress, the logic may set sensor input’s of PLCs and get Actuator outputs from PLCs.
An example pendulum simulator is included with the installation for reference. It is implemented inpendulum_sim.py file located inside examples/inverted_pendulum. This example implements a cart pole which can move left or right inresponse to an applied force. A PLC reads the current angle of the pendulum and applies force on the cart to move it left or right so that the pendulum stays upright.
The pendulum simulator implements the progress method. Inside this method, it gets the actuator output (which is force applied on the cart) and applies it in the simulation. It then sets the current pendulum angle as a sensor input to the PLC. It uses the GRPC API described in Section PLC I/O to interact with the attached PLC.
In the next section, we build on this discussion and show how to use the built-in EmulationDriver class to configure and run time synchronized OpenSCADA PLCs and physical system simulations.
3.10 Kronos driven co-simulation
In this section, we discuss how Kronos can be used to run time synchronized OpenSCADA PLCs and physical system simulations. This discussion is based on theinverted_pendulumexample included with the installation. We will refer the scriptsimulation.pyin the rest of this discussion.
3.10.1 Initializing the co-simulation
• The first step in running a time synchronized co-simulation involves creating a physical system simulator ob- ject. As discussed in Section, Simulating Physical Systems, this object must inherit from PhysicalSystemSim
3.10. Kronos driven co-simulation 31
abstract class defined inphysical_system_sim.py. In the running example, a class called PendulumSimulator is implemented and an object of this class is instantiated and used for this purpose:
pendulum_sim = PendulumSimulator()
• The next step involves initializing an EmulationDriver object (defined inemulation_driver.py) and register the previously created physical system simulator object. This step also assumes that Kronos module is installed and loaded:
emulation = EmulationDriver(number_dilated_nodes=num_dilated_nodes, \ is_virtual=is_virtual, \
n_insns_per_round=num_insns_per_round, rel_cpu_speed=rel_cpu_speed, \
physical_system_sim_driver=pendulum_sim)
The emulation driver class takes in some Kronos specific arguments which are briefly described here. For a more thorough discussion, please referKronosdocumentation.
• is_virtual: If True Kronos is initialized
• physical_system_driver: An object which implements PhysicalSystemSim abstract class defined in con- tib/physical_system_sim.py. If it is None, then it denotes that this co-simulation has no attached physical simulator.
• number_dilated_nodes: Ignored unless is_virtual is True. Denotes number of nodes under Kronos control.
Note that each PLC’s CPU counts as a separate node. So if there are two PLCs each with 2 CPUs, then there are 4 dilated_nodes in total.
• rel_cpu_speed: Ignored unless is_virtual is True. Denotes the relative cpu speed / (equivalent to TDF). In Kronos it represents the number of instructions that can be executed in 1ns of virtual time.
• n_insns_per_round: Number of instructions to execute per round. When divided by the rel_cpu_speed, it denotes amount of time the co-simulation would advance in one-round. In the running example, n_insns_per_round is 1000000 and rel_cpu_speed is 1 which implies that in each round, the co-simulation would run for 1000000 ns or 1ms.
• The next step involves starting the GRPC server to initialize all memory mapped files and serve as interface to query PLCs which would be subsequently started:
print "Starting PC GRPC Server ..."
grpc_server_pid = start_grpc_server(args.plc_spec_dir, fd1)
Note: The GRPC server does not count as a dilated node and should not be added to Kronos’s control
3.10.2 Starting dilated processes
In this subsection, we discuss the next stage of building a time synchronized co-simulation which involves launching PLCs, communication modules and HMI clients and adding them to Kronos’s control. The rest of this discussion assumes that Kronos is installed and loaded and the previous stage is complete.
To launch any process under Kronos’s control, it has to launched by a tracer binary which ships with Kronos instal- lation. For example, let us consider a simple command with arguments ls -al. It can be added to Kronos’s control as follows:
tracer -c "ls -al" -r <rel_cpu_speed> -n <n_insns_per_round>