1.8.2. Main I/O Driver Interfaces¶
To understand how an I/O driver works internally, we need to take a look at the interfaces that an I/O driver has to implement.
If the driver is written in C/C++ you can find the available interfaces in the directory components. The interface header files always begin with CmpIpDrv and ends (like every interface) with Itf.h:
CmpIoDrv…Itf.h
If you intend to write an I/O driver in IEC, you can find the interface in the corresponding libraries with the following structure:
IioDrv… .library
1.8.2.1. IBase¶
The IBase interface is the mandatory interface that every I/O driver has to implement! It Contains the following functions:
IBase (C/C++, the Parameter pIBase is only needed for C-drivers):
void *QueryInterface (Ibase *pIBase, ITFID ItfId, RTS_RESULT *pResult)
int AddRef(Ibase *pIBase)
int Release(Ibase *pIBase)
IBase (IEC):
PointER TO BYTE QueryInterface (ITFID ItfId, PointER TO Udint *pResult)
Dint AddRef()
Dint Release()
With the QueryInterface() function, an I/O driver can be requested for an interface, that the I/O driver implements. The corresponding interface pointer is returned by this function.
The AddRef() function is always called implicitly, if a QueryInterface call was successful to increase a reference counter of this object.
The Release() can be called to release an interface pointer, that was provided by QueryInterface. This function decrements the reference counter of an object.
If the reference counter is 0, the object will be deleted.
1.8.2.2. ICmpIoDrv¶
The most important interface, that an I/O driver can implement, is the IcmpIoDrv interface. It contains the following functions for different issues:
Identification functions:
typedef struct tagIoDrvInfo
{
RTS_IEC_WORD wId; // Index of the instance
RTS_IEC_WORD wModuleType; // Supported module type
RTS_IEC_DWORD hSpecific; // Specific handle
RTS_IEC_string szDriverName[32]; // driver name
RTS_IEC_string szVendorName[32]; // vendor name
RTS_IEC_string szDeviceName[32]; // device name
RTS_IEC_string szFirmwareVersion[64]; // Firmware version
RTS_IEC_DWORD dwVersion; // Version of the driver
} IoDrvInfo;
RTS_RESULT IoDrvGetInfo(IoDrvInfo **ppIoDrv):
With this function, some generic information can be requested like driver name (see structure above), device name that is supported by the driver, vendor name and firmware number (if the supported device has an own firmware on it like the hilscher cards).
RTS_RESULT IoDrvIdentify(IoConfigConnector *pConnector):
By calling this function, the I/O driver should identify itfs device, like blinking some LEDs, stopping the bus, etc. It is planned to call this function by a menu action on the device in the plc-configuration to physically identify this device. This could be very useful, if there are several identical cards plugged in the plc and the assignment in the plc-configuration in CODESYS is unclear.
Configuration: (called during application download)
RTS_RESULT IoDrvUpdateConfiguration(IoConfigConnector *pConnectorList, int nCount):
This function is called at download of the application that contains the I/O-configuration. Each driver instance gets the complete list of connectors!
The first thing that must be done in this function is to detect the connector that is supported by the I/O-driver. For this, the I/O driver can request the Io-manager for the first connector with the specified type Id, like:
pConnector = CAL_IoMgrConfigGetFirstConnector(pConnectorList, &nCount, 0x0020);
Here, the first connector with the Id 0x0020 (=CT_PROFIBUS_MASTER) is searched. See _dev_io_conf_device_descriptions for detailed information.
If the first connector with the matching type is found (pConnector is unequal 0), it must be checked:
if it is correct supported device
if the connector is not supported already by a previous instance
To check, if is the correct device, typically some additional parameters are used to detect this like vendor name, device name or specific device id.
To check if the connector is free and can be used and it is not occupied by another instance, therefore the connector entry hIoDrv must be checked for 0 or -1. In both cases, the I/O-connector is free and can be used. To occupy the connector, the driver has to write ist handle into the connector.
So typical sequence of IoDrvUpdateConfiguration looks like in C:
IBase *pIBase;
IoConfigConnector *pConnector = CAL_IoMgrConfigGetFirstConnector(pConnectorList, &nCount, CT_PROFIBUS_MASTER);
while (pConnector != NULL)
{
IoDrvInfo *pInfo;
IoConfigParameter *pParameter;
char *pszVendorName = NULL;
char *pszDeviceName = NULL;
IoDrvGetInfo(hIoDrv, &pInfo);
pParameter = CAL_IoMgrConfigGetParameter(pConnector, 393218);
if (pParameter != NULL && pParameter->dwFlags & PVF_Pointer)
pszVendorName = (char *)pParameter->dwValue;
pParameter = CAL_IoMgrConfigGetParameter(pConnector, 393219);
if (pParameter != NULL && pParameter->dwFlags & PVF_Pointer)
pszDeviceName = (char *)pParameter->dwValue;
if (pConnector->hIoDrv == 0 &&
pszVendorName != NULL && strcmp(pszVendorName, pInfo->szVendorName) == 0 &&
pszDeviceName != NULL && strcmp(pszDeviceName, pInfo->szDeviceName) == 0)
{
pConnector->hIoDrv = (RTS_IEC_DWORD)pIBase;
In IEC you can find the appropriate sequence in the template driver. It looks quite the same.
After detecting the right connector, the next step in the function IoDrvUpdateConfiguration is to configure the physical device with the connector parameters and optional to detect all slaves (if is a fieldbus master).
To detect the slaves, the I/O-manager provides some interface functions too:
pChild = CAL_IoMgrConfigGetFirstChild(pConnectorList, &nCount, pConnectorFather);
With this function, the first child of the father connector pConnectorFather was returned.
The next child (slave) can be requested by:
pChild = CAL_IoMgrConfigGetNextChild(pChild, &nCount, pConnectorFather);
Note
ATTENTION: The driver must register its instance at each supported connector (also PCI connectors, slaves, etc.). This must be done in the hIoDrv component of the corresponding connector, like:
pChild->hIoDrv = (RTS_IEC_DWORD)pIBase;
RTS_RESULT IoDrvUpdateMapping(IoConfigTaskMap *pTaskMapList, int nCount):
The driver is called with the so called task map list. A Task map contains the following information:
Element |
IEC Data type |
|---|---|
Task ID |
DWORD |
Type (Input or Output) |
WORD |
Number of connector maps |
WORD |
Pointer to connector map list |
PointER |
A connector map list contains the following information:
Element |
IEC Data type |
|---|---|
Pointer to a connector |
DWORD |
Number of channel maps |
WORD |
Pointer to channel map list |
POINTER |
The complete I/O-mapping structure is shown in the following picture:
These are the missing bricks to understand the I/O-mapping.
The task map contains all mapping information for each task, that means, all I/O-channel that are used by a task. For each task you got one entry for all inputs and one entry for all outputs.
The task map contains a list of connectors maps, that means on which connectors the I/O-channels are residing.
And at least, the connector map entry contains a list of channel maps, which includes the real mapping information, where to copy the inputs to which offset on the device and where to copy the outputs from the device to which offset in the application.
In the mapping table, the I/O driver can sort or rearrange entries to optimize later cyclic access. E.g. several byte channels can be collected to one byte stream, to use one memcpy at the cyclic update.
Cyclic Calls:
RTS_RESULT IoDrvReadInputs(IoConfigConnectorMap *pConnectorMapList, int nCount):
This interface function is called at the beginning of a task to read in all referred input values of this driver. Only one call is done for each task.
RTS_RESULT IoDrvWriteOutputs(IoConfigConnectorMap *pConnectorMapList, int nCount):
This interface function is called at the end of a task to write out all referred output values of this driver. Only one call is done for each task.
RTS_RESULT IoDrvStartBusCycle(IoConfigConnector *pConnector):
This function is used to trigger a bus cycle (if necessary on the device). This can be specified in the device description as followed, if a bus cycle is necessary (see driver_info):
<DriverInfo needsBusCycle="true">
. . .
</DriverInfo>
RTS_RESULT IoDrvWatchdogTrigger(IoConfigConnector *pConnector):
This function is called cyclically to retrigger a watchdog on the device. The cycle time must be calculated in the I/O-driver.
Scanning sub devices/modules:
RTS_RESULT IoDrvScanModules(IoConfigConnector *pConnector, IoConfigConnector **ppConnectorList, int *pnCount):
This function is called to scan sub devices. This can be used to scan physically available slaves of fieldbus master, that are connected to one fieldbus.
It is necessary to enable the scan mechanism in the device description file. This is done by an additional xml element in the <DriverInfo> section.
Example:
<DriverInfo>
<Scan supported="true" identify="true"></Scan>
</DriverInfo>
The scan function is enabled for the device and the command is enabled in the context menu if the device is selected in the device tree.
The identify attribute enables the call of IoDrvIdentify to identify a scanned device. In most cases a LED is blinking to show the user the selected device. It is helpful for bus systems without DIP switches for address setting like sercos or ProfiNet.
In the method IoDrvScanModules the connected devices have to be returned.
With versions before V3.5 SP2 the method is called only once and therefore all devices must be scanned at once.
If flag ConnectorOptions.CO_SCAN_PENDING_SUPPORTED is set in pConnector^.wOptions then it is possible to use the pending functionality.
Descriptions of parameters:
IoConfigConnector *pConnector
It contains the connector for the device (for example master) and the parameters for starting the scan function.
IoConfigConnector **ppConnectorList
int *pnCount
The method has to set the number of connectors (=devices + sub devices) stored in the ppConnectorList.
Devices and sub devices could be returned in the connector list. The sub devices must be directly copied to the memory behind the devices then the scan mechanism could automatically assign the sub devices.
Example in IEC working with all versions:
// Member variables for function block:
m_pScanConnector: POINTER TO IoConfigConnector; // only necessary to free allocated
// memory
m_diScannedSlaves: DINT;
Declaration:
METHOD IoDrvScanModules : UDINT
VAR_INPUT
pConnector : POINTER TO IoConfigConnector;
ppConnectorList : POINTER TO POINTER TO IoConfigConnector;
pnCount : POINTER TO DINT;
END_VAR
VAR
pSlaveConnector: POINTER TO IoConfigConnector;
pSlaveParameters: POINTER TO IoConfigParameter;
behavior: DINT;
dwParamCount : DWORD;
stComponent : STRING :='Test';
wSlaves: WORD;
wLen: WORD;
bFailed: BOOL;
dwVendorID: DWORD :=0;
dwDeviceId: DWORD := 0;
dwRevision : DWORD := 0;
udiResult: UINT;
stModuleID: STRING;
wConnectorCount: WORD;
bScanWithPending : BOOL;
END_VAR
Implementation:
IoDrvScanModules_Count := IoDrvScanModules_Count + 1;
// Counter for debugging. Shows that IoDrvScanModules is called
{IF defined (variable:ConnectorOptions)}
bScanWithPending := pConnector^.wOptions = ConnectorOptions.CO_SCAN_PENDING_SUPPORTED;
// Check for version V3.5 SP2. If flag is set the return value ERR_PENDING could be used.
{END_IF}
IF m_pScanConnector <> 0 AND m_diScannedSlaves > 0 THEN
// free memory from the last scan
pSlaveConnector := m_pScanConnector;
FOR behavior := 1 TO m_diScannedSlaves DO
pSlaveParameters := pSlaveConnector^.pParameterList;
FOR dwParamCount := 1 TO pSlaveConnector^.dwNumOfParameters DO
// Free the memory for the parameters
IF (pSlaveParameters^.dwFlags AND 16#2) = 16#2 THEN
// dwValue is pointer
IF pSlaveParameters^.dwValue <> 0 THEN
SysMemFreeData(stComponent,pSlaveParameters^.dwValue);
END_IF
END_IF
pSlaveParameters := pSlaveParameters + SIZEOF(IoConfigParameter);
END_FOR
// Free the parameter list
SysMemFreeData(stComponent,pSlaveConnector^.pParameterList);
pSlaveConnector := pSlaveConnector + SIZEOF(IoConfigConnector);
END_FOR
// Free the connectors
SysMemFreeData(stComponent,m_pScanConnector);
m_pScanConnector := 0; // Mark memory as freed
m_diScannedSlaves:=0;
IF bScanWithPending THEN
// Scan return ERR_PENDING to free the allocated memory
// now return ERR_OK to finish the scan process
IoDrvScanModules := Errors.ERR_OK;
END_IF
END_IF
// to-do: get the number of slaves
wSlaves := 1;
// Example for one device
// Number of Slaves is now known -> allocate memory
pSlaveConnector^ := SysMemAllocData(stComponent,wSlaves*SIZEOF(IoConfigConnector),ADR(udiResult));
// For old version allocate the necessary memory for all device.
// With V3.5 SP2 it is possible to return only one device for each call of IoDrvScanModules
// Therefore it is not necessary to allocate memory dynamically.
It could be also a member variable of the function block
IF ppConnectorList = 0 THEN
// Not enough memory
IoDrvScanModules := Errors.ERR_FAILED;
RETURN;
END_IF
ppConnectorList^ := pSlaveConnector;
// Set the return value of the method to the IoConfigConnector memory.
// Store the memory pointer and size for freeing the memory after scan
m_pScanConnector := pSlaveConnector;
m_diScannedSlaves := wSlaves;
wConnectorCount := 0;
FOR behavior := 1 TO WORD_TO_DINT(wSlaves) DO
pSlaveParameters := SysMemAllocData(stComponent, 4 * SIZEOF(IoConfigParameter),ADR(udiResult));
// At least 4 parameters have ehav returned for each connector.
IF pSlaveParameters <> 0 THEN
bFailed := FALSE;
// to-do: get information from device, vendor id, product, revision etc.
// anything that is needed to find the matching device description in the
// repository
IF NOT bFailed THEN
// device information successfully read
pSlaveConnector^.wType := 32768; // DeviceID of device as in device
//Description <DeviceIdentification><Type>
pSlaveConnector^.dwNumOfParameters := 4; // 4 parameters minimum
pSlaveConnector^.pParameterList := pSlaveParameters
// store the parameters vendor id
pSlaveParameters^.dwParameterId := 1; // Vendor ID is always 1
pSlaveParameters^.dwValue := dwVendorID;
pSlaveParameters^.wLen := 32; // Bitlength
pSlaveParameters^.wType := TypeClass.TYPE_DWORD;
pSlaveParameters^.dwFlags := 16#34; // Value is a direct value
// next parameter device id
pSlaveParameters := pSlaveParameters + SIZEOF(IoConfigParameter);
pSlaveParameters^.dwParameterId := 2; // Product ID is always 2
pSlaveParameters^.dwValue := dwDeviceId;
pSlaveParameters^.wLen := 32; // Bitlength
pSlaveParameters^.wType := TypeClass.TYPE_DWORD;
pSlaveParameters^.dwFlags := 16#34; // Value is a direct value
// next parameter revision
pSlaveParameters := pSlaveParameters + SIZEOF(IoConfigParameter);
pSlaveParameters^.dwParameterId := 3; // Revision ID is always 3
pSlaveParameters^.dwValue := dwRevision;
pSlaveParameters^.wLen := 32; // Bitlength
pSlaveParameters^.wType := TypeClass.TYPE_DWORD;
pSlaveParameters^.dwFlags := 16#34; // Value is a direct value
// next parameter devicestring for search of corresponding device in the
// repository
stModuleID :='0000 0001';
// It is the same string as the <DeviceIdentification><ID> element.
wLen := INT_TO_WORD(len(stModuleID))+1;
pSlaveParameters := pSlaveParameters + SIZEOF(IoConfigParameter);
pSlaveParameters^.dwValue :=
SysMemAllocData(stComponent,wLen,ADR(udiResult));
IF pSlaveParameters^.dwValue <> 0 THEN
pSlaveParameters^.dwParameterId := 4; // Device ID is always 4
SysMemCpy(pSlaveParameters^.dwValue,ADR(stModuleID),wLen);
pSlaveParameters^.wLen := wLen * 8; // Bitlength
pSlaveParameters^.wType := TypeClass.TYPE_STRING; // type string
pSlaveParameters^.dwFlags := 16#32; // Pointer to data
END_IF
pSlaveConnector := pSlaveConnector + SIZEOF(IoConfigConnector);
wConnectorCount := wConnectorCount + 1;
END_IF
END_IF
END_FOR
pnCount^ := wConnectorCount;
// Set the number of devices successfully found
IF bScanWithPending THEN
// Pending is supported then just return the found number of devices and set ERR_PENDING
// IoDrvScanModules is called again to free the allocated memory.
IoDrvScanModules := Errors.ERR_PENDING;
ELSE
IoDrvScanModules := Errors.ERR_OK;
// Pending is not available. Return all found devices. Allocated memory will be freed either in the next scan call or must be
// freed in FB_Exit to prevent a memory loss.
END_IF
If only v3.5SP2 and later should be supported then this could be used:
Member variables for function block
m_ScanConnector: IoConfigConnector;
m_aSlaveParameters: ARRAY[0..3] OF IoConfigParameter;
m_stDeviceId : STRING;
m_ScanConnector.pParameterList := ADR(m_aSlaveParameters[0]);
m_aSlaveParameters[3].dwValue := ADR(stDeviceId);
ppConnectorList^ := ADR(m_ScanConnector);
pnCount^ := 1;
The method has to return Errors.ERR_OK if all devices are done.
Information to the parameters:
Fixed parameter ids returned by IoDrvScanModules
Parameter ID |
Type |
Description |
Mandatory/Optional |
|---|---|---|---|
1 |
DWORD |
Vendor ID |
M |
2 |
DWORD |
Product number |
M |
3 |
DWORD |
Revision |
M |
4 |
STRING |
<DeviceDescription><ID> |
M |
5 |
DWORD |
Slot index for slot devices (0 first slot) |
O |
6 |
STRING |
Reserved for special data types |
O |
7 |
BOOL |
Used for IoMgrIdentify True, if identify enabled. |
O |
8 |
WORD |
ModuleTypeCode Used for module type code of connector to find the correct connector if more than one connector is possible to add the devices |
O |
Additional parameters:
It is possible to add additional parameters starting with ID 10. For example the station name or node id could be passed to the device scan dialog. Additional parameters will be shown in an extra column. If the parameter ID is also available in the device description then the parameter values will automatically copied to the devices after inserting the devices. The access rights of the parameter are set to readwrite then the column is editable.
Diagnostic information:
RTS_RESULT IoDrvGetModuleDiagnosis(IoConfigConnector *pConnector):
With this function, device specific diagnostic information are stored in the connector (diagnostic flags).
1.8.2.3. ICmpIoDrvParameter¶
This interface is used to get access to the system parameter of a device.
RTS_RESULT IoDrvReadParameter(IoConfigConnector *pConnector, IoConfigParameter *pParameter, void *pData, unsigned long ulBitSize, unsigned long ulBitOffset):
With this function, the I/O-manager reads the value of a device parameter. This function is typically called, if an online-service with a parameter read request is sent to the I/O-manager.
RTS_RESULT IoDrvWriteParameter(IoConfigConnector *pConnector, IoConfigParameter *pParameter, void *pData, unsigned long ulBitSize, unsigned long ulBitOffset):
With this function, the I/O-manager writes the value of a device parameter. This function is typically called, if an online-service with a parameter write request is sent to the I/O-manager.

