Micro-course on the programming of SCADAPack controllers on the C

Micro-course on the programming of SCADAPack controllers on the C
 
On the harbor, there are frankly few articles about ACS TP. Moreover, I suspect that programming in the field of industrial automation for most Khabrovians is a magical dark forest with strange legends and creatures. And so I wanted to spend a short excursion in this forest for cognitive purposes, but we will not limit ourselves to cognitive purposes, and we will try to make this material useful to people just starting their way in the process control system or for the first time faced with the type of controllers under consideration.
 
ozna.forum24.ru/?1-11-0-00000001-000-0-0#003
 
 

The second way is


 
Call explicit read functions that store data in the specified modbus registers.
 
These can be functions of ioRead8Din, ioRead8Ain, ioRead16Din, ioRead16Ain, ioRead5604Inputs, ioWrite16Dout, ioWrite5604Outputs, ioRead4Counter (for counting inputs), ioReadSP2 (for reading the built-in inputs, depending on the controller model). Details and syntax of these commands can be found in the documentation for C Tools, usually the first argument is the module address (0 for the built-in inputs), and the second and further are the addresses of the modbus registers, starting from which you need to store the signal values ​​(or where to get them for recording in the output channels).
 
Example:
 
//address of the module 5607 (? since it's a built-in daughter board)
int Module5607Addr = 0;
//grab the resource IO_SYSTEM
request_resource (IO_SYSTEM);
//request updating the data from the
modules. ioRequest (MT_5607Inputs, Module5607Addr);
ioRequest (MT_SP2Inputs, 0);
//wait for the successful read event
//if desired, you can perform other procedures, periodically checking the result (instead of wait_event using poll_event)
ioNotification (IO_COMPLETE);
wait_event (IO_COMPLETE);
//save the data from the module 560? discrete signals - starting with register 1000? analog - starting with register 30001
ioRead5607Inputs (Module5607Addr, 1000? 30001);
//save data from the built-in scadapac inputs, discrete signals - starting from register 1010? analog - starting from register 30101
ioReadSP2Inputs (1010?30101);
//release the resource IO_SYSTEM
release_resource (IO_SYSTEM);

 
Calling the functions ioWrite * will similarly set the desired values ​​from the modbus registers to the output channels of the controller and modules.
 

The third way is


 
The same, but with saving results not in modbus registers, but in variables or array. Functions are also called, but they have an overloaded implementation with other arguments.
 
int Module5607Addr = 0;
//grab the resource IO_SYSTEM, this must be done before all I /O operations
request_resource (IO_SYSTEM);
//an array where the values ​​will be put after reading discrete signals from board 5607
UCHAR DIData[3];
//an array where the values ​​will be put after reading analog signals from board 5607
INT16 AIData[8];
//array for reading discrete signals from the built-in channels of the controller
UCHAR DIData2[2];
//array for reading analog signals from the built-in channels of the controller
INT16 AIData2[8];
//request update of the data
ioRequest (MT_5607Inputs, Module5607Addr);
ioRequest (MT_SP2Inputs, 0);
//wait for the successful read event
//if desired, you can perform other procedures, periodically checking the result (instead of wait_event using poll_event)
ioNotification (IO_COMPLETE);
wait_event (IO_COMPLETE);
//save the read data where we need
ioRead5607Inputs (Module5607Addr, DIData, AIData);
ioReadSP2Inputs (DIData? AIData2);
release_resource (IO_SYSTEM);

 
 
Something similar can be done with the data that we want to write to the output channels (for example, to close the relay output):
 
UINT16 InputType[8];
(int i = 0; i < 8; i++)
.InterType= 3;
UINT16 InputFilter = 3;
UINT16 ScanFrequency = 0;
UINT16 OutputType = 1;
UINT16 Mask2 = 0;
ioWrite5607Outputs (Module5607Addr, DOData, AOData, InputType, InputFilter, ScanFrequency, OutputType)
ioRequest (MT_5607Outputs, Module5607Addr);
.
.
.
 
 
Specific functions and constants for reading and writing data to used modules should be looked at in the documentation and in the header files of C Tools. For example, the functions for the module 560? as can be noted above, have additional options for setting the input type, filter parameters, etc. They are also described in the documentation.
 
 
There are also functions for obtaining the current temperature of the controller and the voltage on the battery - readThermistor (T_CELSIUS), readBattery (). A trifle, but useful.
 
 

Scaling of AI


 
Analog input channels of the controller modules can work in different modes (for example, with input ranges of 0-20 or 4-20 mA, this is determined by jumpers on the module). They give the data in ADC units, and to convert them into the desired scale (for example 4-20 mA will correspond to 0-100%) is quite simple:
 
//register in which we find the value of the input AI, which we will convert
#define AIWaterLevelReg 30001
float WaterLevelScaled = ((float) dbase (MODBUS, AIWaterLevelReg) - 6554) /26214 * 100; //for the range 4-20 mA
//or
float WaterLevelScaled = (float) dbase (MODBUS, AIWaterLevelReg) /32767 * 100; //for the range 0-20 mA

 
Naturally, instead of 10? you can multiply the number by the upper limit of the scale you need.
 
 

Configuring the RS-232 /RS-485 ports


 
Here, again, no magic:
 
PROTOCOL_SETTINGS comsettings;
pconfig portSettings;
//read the current COM2 port settings to change them
getProtocolSettings (com? & comsettings);
//modbus address, by which the controller will respond on this port
comsettings.station = 1;
comsettings.type = MODBUS_RTU;
comsettings.mode = AM_standard;
get_port (com? & portSettings);
//the exchange rate, 38400
portSettings.baud = BAUD38400;
portSettings.duplex = HALF;
portSettings.parity = PARITY_NONE;
portSettings.data_bits = DATA8;
portSettings.stop_bits = STOP1;
portSettings.flow_tx = TFC_NONE;
portSettings.flow_rx = RFC_MODBUS_RTU;
portSettings.type = RS232;
setProtocolSettings (com? & comsettings);
set_port (com? & portSettings);

 
 
The port settings are stored in the EEPROM, however, depending on the controller configuration, library versions, etc., it is possible to read from the same modbus registers for a certain event (command, jumper closure, etc.) to automatically reconfigure and update them.
 
 

Interrogation of devices by modbus


 
If you want to not only operate as a slave, but also to interrogate other controllers or sensors, there are functions of the form master_message (), which allows you to poll the external devices by modbus, and save the results to yourself in registers, where they can then be read and used in the algorithm (or simply provide the upper level). Examples are in the documentation, only take into account two nuances: you must always check the result of the function with the command get_protocol_status (), before sending a second request or working with the data received, and the second nuance: you either need to disable the modbus handler on the port in use, so that its address does not match the address of the device being polled (otherwise you can get undefined behavior or strange errors).
 
 
extern unsigned master_message (FILE * stream, unsigned function, unsigned slave_station, unsigned slave_address, unsigned master_address, unsigned length);
 
stream - the port through which the data will be exchanged (for example, com1), function - the number of the modbus function for the request (for example 3), slave_station - the address of the RTU of the device being polled, slave_address is the start address of the registers in the remote device that we want to read, master_address - the starting address of the registers on our controller, where the data will be written, length - the number of registers for reading).
 
 
Example:
 
request_resource (IO_SYSTEM);
//read the com2 port of the 3rd modbus function (reading Holding zone) from the device with address 1 starting from register 0001 (40001) 17 registers, write to register 3 of our controller
master_message (com? ? ? 4000? 4051? 17);
//in the subsequent cycles we check the result
struct prot_status polling_status;
polling_status = get_protocol_status (
com2);
if (polling_status.command == MM_SENT)
{
//the request was sent, but the answer has not yet been received.
//it is also recommended to remember the time of sending the request, and to control the timeout if the response was not received within a certain time
}
else
if (polling_status.command == MM_RECEIVED)
{
//the request was sent, the answer was received and successfully decrypted!
//you can work with the received data and send the next request
}
else
{
//Something went wrong. See the error code and find out what the
is about.}
release_resource (IO_SYSTEM);

 
 

Maintenance of archives


 
The SCADAPack documentation for these purposes suggests using the DataLog from the C Tools library, which, unfortunately, has a lot of drawbacks, the main one being that it does not provide direct, inconsistent access to the records in the archive. Some developers manage to store archives directly in register memory, but given that the number of Holding and even Inputs (if you want to use them, too, yes) in the controller is limited, this solution also does not always work.
 
 
In fact, given that the operating system in the controller is quite POSIX-compatible, and the controller carries a completely normal file system on its board (plus there is an opportunity to insert USB flash-drives), it is possible to store archives in files on a flash drive on board controller, which is quite casually mentioned in the documentation.
 
 
You can work with files just like in any C program:
 
FILE * mdata;
char * file_mdata = "/d0/logs.dat";
mdata = fopen (file_mdata, "w");
fputs ("test log string", mdata);
fclose (mdata);

 
That is, there are no obstacles to write (as an option - to keep a cyclic archive) to a file serialized structures, and then freely navigate through them and give them to the user either by mapping a certain part of the archive into the space of modbus registers, or by giving them a custom modbus- function by large blocks.
 
 

 
The /d0 /mounts the file system, and /bd0 /- an external USB flash drive. Again, there is no problem to implement copying the archive from the built-in memory to the USB flash drive when you click on the button on the controller, and many other options.
 
In TelePACE, the file system is viewed quite calmly, which can help to debug or collect data.
 
 

Working with real time clock


 
The controller also has a real-time clock on board.
 
You can get the current time and install it using getclock and setclock functions, the documentation has detailed examples.
 
 

An example of a simple algorithm is


 
Suppose we have a controller with an I /O board 560? an analog level sensor in the tank is connected to AI2 channel, a relay of the pump starter is connected to the DO1 channel.
 
It is necessary to maintain a specified liquid level in the tank (the required level is set from SCADA or from the HMI panel), that is, if the pump is too high, the pump pumping out the excess should be turned on and off when the required level is reached.
 
In register 0020 of the Holding zone (40020), we will record the given level value for display on the SCADA or HMI panel.
 
It is also necessary to provide a small hysteresis to protect against "clicking" of the pump relay in case the level fluctuates near the desired mark.
 
The controller will poll the COM2 port.
 
#include
#include "nvMemory.h"
/**
* @brief Initialize the scanning of the input channels of the 5604 board into the address space of the modbus registers
* /
void initialize_io ()
{
request_resource (IO_SYSTEM);
clearRegAssignment ();
addRegAssignment (SCADAPack_5604IO, ? ? 1000? 3000? 40001);
release_resource (IO_SYSTEM);
}
/**
* @brief Initialize COM2 port to work as Modbus Slave, address = ? speed = 9600
* /
void initialize_ports ()
{
request_resource (IO_SYSTEM);
PROTOCOL_SETTINGS comsettings;
pconfig portSettings;
getProtocolSettings (com? & comsettings);
comsettings.station = 1;
comsettings.type = MODBUS_RTU;
comsettings.mode = AM_standard;
get_port (com? & portSettings);
portSettings.baud = BAUD9600;
portSettings.duplex = HALF;
portSettings.parity = PARITY_NONE;
portSettings.data_bits = DATA8;
portSettings.stop_bits = STOP1;
portSettings.flow_tx = TFC_NONE;
portSettings.flow_rx = RFC_MODBUS_RTU;
portSettings.type = RS232;
setProtocolSettings (com? & comsettings);
set_port (com? & portSettings);
request_resource (IO_SYSTEM);
}
/**
* @brief Scaling function for AI signal
* @param ai_value The "raw" value of AI in ADC units
* @param max The maximum reduced value of AI (upper limit of the sensor scale)
* @return The value given in the measured units is
* /
float scale_ain (int ai_value, float max)
{
return ((float) ai_value - 6554) /26214 * max;
}
/* Constants. They should be put into a separate file and connected via #include * /
#define AILevelReg 30002 //register in which the AI ​​signal from the level sensor (2nd AI channel)
will be read. #define LevelScaledReg 40020 //register in which we will write down the given value of liquid level in centimeters
#define PumpLevelTriggerReg 40050 //register in which the required liquid level (in centimeters)
should be recorded from the SCADA or HMI panel. #define PumpOutReg 1 //DO output register to which the pump starter relay (1st DO channel)
will be connected. #define LevelHyst 50 //Level insensitivity zone in centimeters. permissible deviation of the level, in order to avoid chatter of the starter
#define MaxLevelMeters 500 //maximum liquid level in the tank (upper limit of the level sensor), in centimeters
int main (void)
{
//initialize COM port
initialize_ports ();
//configure the update of the discrete and analog inputs and outputs
initialize_io ();
while (TRUE)
{
request_resource (IO_SYSTEM);
//read the AI ​​value of the liquid level, convert it to units of measurement and write it to the register
int level_raw = dbase (MODBUS, AILevelReg);
float level_scaled = scale_ain (level_raw, MaxLevelMeters);
setdbase (MODBUS, LevelScaledReg, (int) level_scaled);
//if the level is higher than normal, turn on the pump
if (level_scaled> (dbase (MODBUS, PumpLevelTriggerReg) + LevelHyst))
setdbase (MODBUS, PumpOutReg, 1);
else
//and if below the norm - turn off
if (level_scaled < (dbase(MODBUS, PumpLevelTriggerReg) - LevelHyst))
.setdbase (MODBUS, PumpOutReg, 0);
.
release_resource (IO_SYSTEM);
.
release_processor ();
}
}
.
.
.
 
 

That's all


 
A description of all functions and examples of their use is in the documentation for C Tools.
 
It is very good to have a complete knowledge of C (for example, when implementing algorithms, this refers to casting types) to write code nicely and without errors.
 
 
Study, experiment, and everything will turn out!
 
 
[i] A huge thanks to Denis Hizbatov for help with the preparation of the material and valuable comments :)
+ 0 -

Add comment