Driver Documentation
Database
A very common way to integrate your own I/O drivers is to use an FB interface inside of a library. A much better way, and much more convenient from the usability is the integration in form of a real I/O driver. This approach should always been choosen when you decide to share your driver with others.
To implement an I/O driver in CODESYS you need mainly two things:
In the following you will find some general rules to follow while implementing your driver. Beside this guide, you should check out the different templates for different kinds of drivers to start from. Because in detail the implementation will be diffferent if you implement an SPI driver or you read a value from a sensor using a REST API.
But independent of the kind of driver, which you plan to implement. If you are not routined in writing such software, it is a good idea to prove the access of the hardware directly in an application.
Every device, which you can add to your CODESYS project, is described in an XML format inside of a Device Description.
For finetuning and to get a deeper understanding of the format, please check out the schema file. In the following we will just explain the basics to enable you to write your first description.
The XML schema can be downloaded under the URI, which is defined in every XML file:
http://www.3s-software.com/schemas/DeviceDescription-1.0.xsd
That the device can be uniquely identified in the device repository on every computer in the world, we need to maintain a few IDs. Please register the ID, which you will be using in the Device Database.
Note: The combination of all IDs, described above has to be globally unique. Official versions of a driver with the same IDs, but different content shall never exist.
A device or module, which you can add to the device tree in CODESYS contains at least one connector. This connector is used to attach the module to a parent device.
<Connector moduleType="8023" interface="Common.PCI" role="child" explicit="false" connectorId="1" hostpath="-1">
<InterfaceName name="local:PCI">PCI-Bus</InterfaceName>
<Slot count="1" allowEmpty="false"></Slot>
</Connector>
Interfaces are symbolic names to match compatible connectors, so to find compatible devices or modules. We have some different concepts to configure our device tree in CODESYS.
<Connector moduleType="8023" interface="OpenSource:Internal" role="parent" explicit="false" connectorId="2" hostpath="1">
<InterfaceName name="local:internal">Internal</InterfaceName>
<Var max="4"></Var>
<Connector moduleType="8023" interface="OpenSource:FixConnectorInterface" role="parent" explicit="false" connectorId="2" hostpath="1">
<InterfaceName name="local:internal">Internal</InterfaceName>
<Fixed>
<!-- This is an example of a fixed module specified in the same file -->
<Module>
<LocalModuleId>8023</LocalModuleId>
</Module>
<!-- This is an example of a fixed module specified by DeviceIdentification. The defininition of this module is in another *.devdesc.xml -->
<Module>
<DeviceIdentification deviceType="40107" deviceId="0001 bcde" version="3.5.9.0"/>
</Module>
</Fixed>
This was a huge preamble for an easy topic. But it was important that you get an idea of how the result will look like.
Interfaces are easy. You define one interface per child connector and one for every parent connector. There are no fix restrictions for the interface names. But it is an unofficial rule, that they should be prefixed with a short or full name of the vendor.
<Connector ConnectorId="2" HostPath="-1" interface="MyCompany.A" moduleType="40101"
role="parent">
<Slot allowEmpty="false" count="16"/>
</Connector>
<Connector ConnectorId="3" HostPath="-1" interface="MyCompany.B" moduleType="40102"
role="parent">
<Slot allowEmpty="false" count="16"/>
</Connector>
...
<Modules>
<Module>
<ModuleId>1701</ModuleId>
<DeviceInfo>
<Name name="localStrings:Name1704">Digital Input</Name>
<Description name="localStrings:Desc1704"/>
<Vendor name="localStrings:3S">3S-Smart Software Solutions</Vendor>
<OrderNumber/>
</DeviceInfo>
<Connector ConnectorId="2" HostPath="-1" interface="MyCompany.A" moduleType="41101"
role="child">
<Slot allowEmpty="false" count="1"/>
<HostParameterSet>
<Parameter ParameterId="1000" type="std:BIT">
<Attributes channel="input" download="true" functional="false" offlineaccess="readwrite"
onlineaccess="readwrite"/>
<Default>0</Default>
<Name name="local:in1">in1</Name>
</Parameter>
</HostParameterSet>
</Connector>
</Module>
<Module>
<ModuleId>1702</ModuleId>
<DeviceInfo>
<Name name="localStrings:Name1705">Digital Output</Name>
<Description name="localStrings:Desc1705"/>
<Vendor name="localStrings:3S">3S-Smart Software Solutions</Vendor>
<OrderNumber/>
</DeviceInfo>
<Connector ConnectorId="3" HostPath="-1" interface="MyCompany.A" moduleType="41102"
role="child">
<Slot allowEmpty="false" count="1"/>
<HostParameterSet>
<Parameter ParameterId="1000" type="std:BIT">
<Attributes channel="output" download="true" functional="false" offlineaccess="readwrite"
onlineaccess="readwrite"/>
<Default>0</Default>
<Name name="local:in1">in1</Name>
</Parameter>
</HostParameterSet>
</Connector>
</Module>
<Module>
<ModuleId>1703</ModuleId>
<DeviceInfo>
<Name name="localStrings:Name1705">Digital Output</Name>
<Description name="localStrings:Desc1705"/>
<Vendor name="localStrings:3S">3S-Smart Software Solutions</Vendor>
<OrderNumber/>
</DeviceInfo>
<Connector ConnectorId="4" HostPath="-1" interface="MyCompany.B" moduleType="41103"
role="child">
<Slot allowEmpty="false" count="1"/>
<HostParameterSet>
<Parameter ParameterId="1000" type="std:BIT">
<Attributes channel="output" download="true" functional="false" offlineaccess="readwrite"
onlineaccess="readwrite"/>
<Default>0</Default>
<Name name="local:in1">in1</Name>
</Parameter>
</HostParameterSet>
</Connector>
</Module>
<Module>
<ModuleId>1704</ModuleId>
<DeviceInfo>
<Name name="localStrings:Name1706">PWM Output</Name>
<Description name="localStrings:Desc1706"/>
<Vendor name="localStrings:3S">3S-Smart Software Solutions</Vendor>
<OrderNumber/>
</DeviceInfo>
<Connector ConnectorId="5" HostPath="-1" interface="MyCompany.B" moduleType="41104"
role="child">
<Slot allowEmpty="false" count="1"/>
</Connector>
</Module>
</Modules>
A device can roughly hold two kinds of parameters:
Data types, default values, etc. are defined equally for all kinds of parameters. But there is a significant difference in how the different kinds of parameters are processed.
I/O channels are processed in the same way as configuration parameters, thus they have additional mapping information assigned to it, so that they are also passed on to IoDrvReadInputs/-WriteOutputs.
I/O channels are marked with the "channel" attribute.
<Attributes channel="input" download="true" functional="false" offlineaccess="write" onlineaccess="readwrite" />
Both can be configured with the same structured datatypes.
They can be located globally on device level, then they are called DeviceParameters. If they are part of the device that the user adds to the tree, then, they are called HostParameters and attached to one of its connectors.
You can use virtually every datatype for a parameter, which can be used in IEC61131. This includes especially arrays and structures.
But you have to be aware that you need to define those datatypes consistently twice (in your driver library and your device description). There is no extra check. If they don't match between your device description and your code, it will just not work, crash, or whatever...
Some examples...
Basic datatypes don't need a declaration. They can be used with the namespace prefix "std". So "std:BOOL" is the equivalent of a BOOL datatype in IEC.
DevDesc:
<Parameter ParameterId="1000" type="std:DWORD">
<Attributes channel="input" download="true" functional="false" offlineaccess="write" onlineaccess="readwrite" />
<Default>0</Default>
<Name name="local:DWIN">DWORD Input</Name>
<Description name="local:DWIN.Desc">DWORD Input</Description>
</Parameter>
IEC:
TYPE DUT :
STRUCT
BYTE1 : BYTE;
BYTE2 : BYTE;
BYTE3 : BYTE;
BYTE4 : BYTE;
END_STRUCT
END_TYPE
DevDesc:
<Types namespace="local">
<StructType name="Channel4Byte">
<Component identifier="Byte0" type="std:BYTE">
<Default />
<VisibleName name="local:Byte0">Byte0</VisibleName>
</Component>
<Component identifier="Byte1" type="std:BYTE">
<Default />
<VisibleName name="local:Byte1">Byte1</VisibleName>
</Component>
<Component identifier="Byte2" type="std:BYTE">
<Default />
<VisibleName name="local:Byte2">Byte2</VisibleName>
</Component>
<Component identifier="Byte3" type="std:BYTE">
<Default />
<VisibleName name="local:Byte3">Byte3</VisibleName>
</Component>
</StructType>
</Types>
...
<Parameter ParameterId="1000" type="local:Channel4Byte">
<Attributes channel="input" download="true" functional="false" offlineaccess="write" onlineaccess="readwrite" />
<Default>0</Default>
<Name name="local:DWIN">DWORD Input</Name>
<Description name="local:DWIN.Desc">DWORD Input</Description>
<DefaultMapping>
<Element name="Byte0">Byte0</Element>
<Element name="Byte1">Byte1</Element>
<Element name="Byte2">Byte2</Element>
<Element name="Byte3">Byte3</Element>
</DefaultMapping>
</Parameter>
IEC:
TYPE DUT :
STRUCT
BYTE1 : BYTE;
BYTE2 : BYTE;
BYTE3 : BYTE;
BYTE4 : BYTE;
END_STRUCT
END_TYPE
DevDesc:
<BitfieldType basetype="std:BYTE" name="Bitfield">
<Component identifier="Bit0" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit0">Bit0</VisibleName>
</Component>
<Component identifier="Bit1" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit1">Bit1</VisibleName>
</Component>
<Component identifier="Bit2" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit2">Bit2</VisibleName>
</Component>
<Component identifier="Bit3" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit3">Bit3</VisibleName>
</Component>
<Component identifier="Bit4" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit4">Bit4</VisibleName>
</Component>
<Component identifier="Bit5" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit5">Bit5</VisibleName>
</Component>
<Component identifier="Bit6" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit6">Bit6</VisibleName>
</Component>
<Component identifier="Bit7" type="std:BOOL">
<Default />
<VisibleName name="local:Bitfield.Bit7">Bit7</VisibleName>
</Component>
</BitfieldType>
<StructType name="local:Channel4Byte">
<Component identifier="Byte0" type="local:Bitfield">
<Default />
<VisibleName name="local:Byte0">Byte0</VisibleName>
</Component>
<Component identifier="Byte1" type="local:Bitfield">
<Default />
<VisibleName name="local:Byte1">Byte1</VisibleName>
</Component>
<Component identifier="Byte2" type="local:Bitfield">
<Default />
<VisibleName name="local:Byte2">Byte2</VisibleName>
</Component>
<Component identifier="Byte3" type="local:Bitfield">
<Default />
<VisibleName name="local:Byte3">Byte3</VisibleName>
</Component>
</StructType>
...
<Parameter ParameterId="1000" type="local:Channel4Byte">
<Attributes channel="input" download="true" functional="false" offlineaccess="write" onlineaccess="readwrite" />
<Default>0</Default>
<Name name="local:DWIN">DWORD Input</Name>
<Description name="local:DWIN.Desc">DWORD Input</Description>
</Parameter>
IEC:
abyBuffer : ARRAY [0..10] OF BYTE;
DevDesc:
<ArrayType name="myArray" basetype="std:BYTE">
<FirstDimension>
<LowerBorder>0</LowerBorder>
<UpperBorder>10</UpperBorder>
</FirstDimension>
</ArrayType>
Please always start with a template, when you start writing a driver. This will give you a good skeleton of your driver. Anyway, this chapter contains a few basic informations about specific interface functions.
This is always the first entry point when you start writing a driver. It is called when a program is loaded, and gives all drivers the chance to register itself for specific connectors of the device tree.
Additionally it gives the driver the chance to prepare itself and to configure the I/O system.
This function is called with pConnectorList set to 0, when the application is deleted or reseted. So all drivers need to handle that.
IF (pConnectorList = 0) THEN
RETURN;
END_IF
Check the template for details. In general you search for a connector by its "Module ID" . Then you register your base interface pointer at this connector.
This is enough, that you are called by the I/O manager for every I/O update.
IF m_pConnector^.hIoDrv = 0 THEN
m_pConnector^.hIoDrv := m_hInterface;
You get the whole built time configuration of the application passed to this function. Read those values with IoMgrConfigReadParameter to configure your subsystem.
The I/O channels are also just a special type of parameter, and can be read in this early stage. You can use the value "dwDriverSpecific" to prepare your I/Os in a way that you can quickly access them later on.
The best way is to store a pointer to the I/O data there. Those can be perfectly used with IoMgrCopy... in IoDrvReadInputs/-WriteOutputs.
FOR i:=0 TO 7 DO
pParameter := IoMgrConfigGetParameter(m_pConnector, 1000 + i);
IF (pParameter <> 0) THEN
pParameter^.dwDriverSpecific := ADR(_MCP3008.auiValue[i]);
END_IF
END_FOR
CODESYS reduces the updates of the I/O to the bare minimum. The compiler detects already which I/O is used in which task, and calls the drivers from those task contexts in which they are used.
To allow the driver to update only the necessary I/O variables, you get a list of mappings passes to those functions.
FOR i:=0 TO nCount - 1 DO
IF (pConnectorMapList[i].dwNumOfChannels = 0) THEN
CONTINUE;
END_IF
FOR j:= 0 TO UDINT_TO_UINT(pConnectorMapList[i].dwNumOfChannels) - 1 DO
IoMgrCopyInputLE(ADR(pConnectorMapList[i].pChannelMapList[j]), pConnectorMapList[i].pChannelMapList[j].pParameter^.dwDriverSpecific);
END_FOR
END_FOR
Hi Ingo,
Thank you for your nice introduction. I have created a library successfully based on your prepared templates and now, I have following questions:
1- How could I possibly debug my developed FB library that implementing the I/O driver interface library. This also can help me to check the data that are going back and forth between device description and the FB library to know better how things are working and which other options I have.
2- I found out that some other device drivers have more tab pages in Codesys (comparing to default 5 tab pages in for example IoDrvFB), how could I generate and configure data in these pages?
Thank you in advance for your kind answer.
Last edit: alimans 2023-09-18
Hi Alimans,
thanks for the feedback!
1) To debug, you can easily set the compiler define "IoConfigLateInit". That will instruct our device object to execute the Init code of your driver in the first Task-Cycle. This way, you can easily set a breakpoint in your library and debug it.
2) Which pages to you mean in detail? Most of them are just coming from specific configurator plugins. So when you see the configuration pages in EtherCAT or Profinet or s.th., those are developed specifically for this fieldbus.
Cheers,
Ingo
Thank you for your kind respond, Ingo!
1- The "IoConfigLateInit" trick got the job done! But, still for some functions like "IoDrvWriteParameter" I could not place break point.
2- Regarding the page, as you set it correctly, I mean pages like EtherCAT and so on. A sample page for this you can find as attachment. So how could I write a plugin and use it in my IO Driver? Because my device uses several configuration and also has some diagnostic registers that would be perfect if I could show them to the user in a designed UI.
3- In case of any problem in device or driver (for example disconnecting or cable unplug) How could I send errors to the codesys? What is the best practice to handle such a situation?
Thank you again for your instructions!
Last edit: alimans 2023-09-21
Hi everybody,
As I did not still findout a proper answer for my question no.3 above, it would be highly appreciated if anybody can answer my question. For reference I repeated my question as bellow:
In case of any problem in device or driver (for example disconnecting or cable unplug) How could I send errors to the codesys? What is the best practice to handle such a situation?
Hi, Ingo!
Im very glad to see this page and your work. But I cant understand many of things.
For example: in the IoDrvFb:
cDriverName
In the device description I see only libname, and Device name, what the drivername ?:) and many other fields I dont know what to set and how to start tesing them and what for my attention the first time.
And other fields in the iodrv system libs. Where I can learn this? I need to understand every step, every field in the xml and in the Fbs. Thanks you!
And what are u thinking about book of codesys that written by Gary Pratt? Is there information I need about system libs and drivers?
Thank you :)