Table of Contents
For industrial applications I/O interfaces (like digital inputs, relais outputs, 4-20 mA analog inputs etc.) often have to be accessed by control and measurement software components. Linux and other POSIX operating systems do not have a generic software API for industrial I/O components; most vendors have their own ideas about how their driver interface looks like.
libpv is a library that implements a "Process Image" abstraction for industrial I/O in userspace. The process image is a memory representation of the values of input and output ports, abstracted as "Process Variables". For example in common PLC-like applications the input process variables are usually being updated from the hardware at the beginning of a cycle; the control algorithms are working on the process image then. At the end of a cycle the hardware output ports are being updated from the process variables.
[figure: process image, PLC cycle]
The process image is implemented as an object with file representation. For example in common embedded control hardware it can be a file on a battery backed static memory device. libpv offers mmap() based access to the file's content, so each process variable being written by the application modifies directly the content of the persistent file. In case of power failure the contents of the file are not modified and when power comes back the application comes up in the same state.
Files which contain a process image are referenced to as a "Backing Store" by libpv. Unfortunately it is not possible to mmap() all kinds of backing stores for read/write access; for example on JFFS2 flash discs mmap() is only possible for read access. Other problems come up with storage devices which do not offer continuous mapping, like 8 bit devices which are connected to a 16 bit memory bus; such devices show up in the application's memory space as a sequence of 8 bit storage data, followed by 8 bit holes.
For human machine interfacing it is often useful to be able to visualize process variables with a graphical application. libpv has an optional integrated JVisu (http://www.jvisu.org) socket server. So visualizing a process image is also possible between different hosts and architectures.
libpv requires some other libraries:
libptxlist >= 1.0.3: Another library project by Pengutronix. See http://www.pengutronix.de/software/liblist/.
libxmlconfig >= 1.0.2: Another library project by Pengutronix. It depends strongly on libxml2 and its configuration. See http://www.pengutronix.de/software/libxmlconfig/.
libxml2 >= 2.6.19: Required by libxmlconfig
libmqueue: If you want to use event support (process variable change notification) you need at least Kernel 2.6.6rc1 running with GNU libc 2.3.4. If you are using an earlier GNU libc you also need libmqueue in addition.
Other tools:
Linux kernel features:
liblist, libxml2 and libxmlconfig should be ready to use. All libraries use pkg-config, so if they have been installed to a non-standard location PKG_CONFIG_PATH has to be set to the directory containing the .pc files.
Like with all autotool based projects, libpv has to be built with the sequence
./configure && make && make install
Additional features may be switched on or off while running configure:
Enable debug mode; the library is compiled with debug symbols and with -O1.
To be able to work with a process image we first have to create a description file. The description file specifies which process variables do exist, which types they have and it also specifies the layout of the process images. Section XML process definition contains more information about the details of the description file.
libpv is written in an object oriented way. As C does not offer native objects we use a struct for the storage of the object variables.
Control applications often have the requirement that no dynamic memory
has to be allocated during normal operation; although libpv doesn't strictly
follow this paradigm internally, it does for the object struct. The constructor
(pv_init()
) doesn't dynamically allocate memory for the object
struct, so the calling process has to take care of the memory management here.
For example, it can just allocate it on the stack:
pv_engine_t engine;
The constructor needs some parameters. To have the API independend of future extensions, libpv uses an attribute struct which also has to be prepared by the calling process:
pv_engine_attr_t attr;
Here is a list of the attribute fields which have to be prepared:
desc_filename
File name of the XML description file.
store_filename
File name of the backing store to be used.
pv_store_type_t
Leave unchanged for a standard filesystem on writeable (and mappable) media. Otherwise 1 if special handling is required to read or write the media. A change here hardly depends on the media type you want to use as backing store. Change this value only if you know what you are doing.
jvisu_port
0 if you don't want to visualize process variables between hosts, set to default TCP port 2202 if you want to start a JVisu server thread.
Now we can instanciate our pv_engine object:
err = pv_init(&engine, &attr);
If the return value of pv_init()
is != 0 an error is indicated.
Usually the function just returns 0 and the object can be used.
Before process variables can be used in our program they have to be registered with our pv_engine:
pointer_to_processvar = pv_register_<type>(&engine, ID-string);
It is important that process variables are registered with the correct
pv_register_<type>()
function, corresponding to which type
has been specified in the XML description file.
All process variables which shall be used in our application have to be
registered first. The pv_register_<type>() functions directly return
pointers to the process variables in the mmap()
ed process image.
Modern Unix systems like Linux offer concurrent execution of processes and threads. For ressources which can be shared between different execution contexts it is important that the access to the shared ressource is properly serialized.
libpv implements a locking scheme for the process variable engine objects. So whenever the process image is accessed it has to be locked first:
pv_lock(&engine);
When pv_lock()
returns we have exclusive access to the engine and
can read or write process variables, for example like this:
*pointer_to_processvar=<newValue> (*pointer_to_processvar)++; calcNewValue(pointer_to_processvar); ....
After all changes have been done, unlock the engine:
pv_unlock(&engine);
When we don't need the engine any more, for example when the program is finished, the pv_engine object has to be destroyed:
pv_destroy(&engine);
Note that, even when the pv_engine object has been destructed, the backing store and the process image stay intact.
Special backing stores may need a special handling while reading or
writing data from it or to it. In this case libpv is not directly working on
the mmap()ed backing store but uses a shared memory copy. To achieve
persistence it is necessary to call pv_flush(&engine, ...);
to
ensure the backing store updated with the new value. pv_flush()
works on a per process variable base.
This changes the workflow to:
pv_lock(&engine);
change anything
pv_flush(&engine, pointer_to_resource, size_of_resource);
pv_unlock(&engine);
To be independent of any backing store and it's capabilities use
pv_flush()
every time a process variable is changed. The function
does nothing if there is no need for a flush and it does the right thing if the
currently used backing store needs special handling.
Sometimes it is interesting to know when a process variable has changed. libpv has an event notification mechanism which can also be used to relay events with associated data to some caller. An application can register itself to change events for as much process variables as it is interested in.
To register for an event notification we have to call
pv_event_register(const pv_engine_t *engine_anon, const void *pv_var)
The first argument is the pv_engine handle, the second one a process variable
pointer which must have been acquired before with
pv_register_<type>()
. Now we are registered for change
notifications on this process variable and can wait for events:
pv_event_await_syncronous(const pv_engine_t *engine_anon, void **pv_var)
This call blocks until a notification arrives. It returns immediately if there
remain old notifications, so it is not possible to miss an
event.
As we can register for changes on as many process variables we need, this
function returns independently for each notification. Due to the fact that the
call to pv_event_await_syncronous()
blocks until a notification
arrives it should be called it in a separate thread, so that the rest of the
program can do other things.
Now we block until we receive change notifications. But if you change
any process variable another process is registered for a change notification
to, you must generate this kind of event. To do so you must call
pv_event_generate()
. This is required when you change the process
variable without the help of the libpv.
Change the process variable and notify others about that fact:
*process_var = newValue;
pv_event_generate(..,process_var);
The easiest way to update a piece of hardware is to register a write function to process variable of interest. This write function knows how to translate the process variable's contents into something device specific.
After that register also a change notification function to the same process variable.
Then call pv_event_await_syncronous()
, to wait for a notification. When
this call returns, simply call pv_to_device()
.
Here is a very simple example. It doesn' show the initializing of libpv.
static int write_var1_into_hardware(pv_engine_t *engine,struct pv *pv,unsigned long data)
{
.... /* so something device specific */
}
void observe_var(void)
{
uint8_t *var1;
var1 = pv_register_uint8(&engine,"ucResetCounter");
pv_event_register(&engine,var1);
pv_set_fn_write(&engine,var1,write_var1_into_hardware);
while(1) {
pv_event_await_syncronous(&engine);
pv_to_device(&engine,var1);
}
}
FIXME: This chapter is a should be, currently not realized in this way.
In this example three independent clients sharing one process image to control a process that communicates with different kind of hardware.
They are sharing a process image with the definition shown below:
<oio>
<group id="can_bus">
<port id="can_message" index="1">
<portPvChannel>
<pv id="position_request" val="0">
<type>bool</type>
<offset>0</offset>
</pv>
</portPvChannel>
</port>
<port id="can_message" index="2">
<portPvChannel>
<pv id="overheating" val="0">
<type>bool</type>
<offset>4</offset>
</pv>
</portPvChannel>
</port>
</group>
<group id="pos_encoder">
<port id="pos_handling" index="1">
<portPvChannel>
<pv id="current_pos" val="0">
<type>uint32_t</type>
<offset>8</offset>
</pv>
</portPvChannel>
</port>
</group>
<group id="i2c_bus">
<port id="temperature" index="1">
<portPvChannel>
<pv id="motor_temp" val="0">
<type>uint32_t</type>
<offset>12</offset>
</pv>
</portPvChannel>
</port>
</group>
</oio>
The CAN client should send CAN messages to notify other CAN devices about the position encoder's current value and power consumption control. But it makes only sense to send a new message, if there is a difference in any value since the last CAN message sent. Besides this some CAN devices can query for the encoder's current position. The encoder client periodically reads in the encoder's current position, the same does the sensor client but with the motor's current temperature.
Every time the encoder client reads in a different position than the last read, it
stores the new value into process variable current_pos
. Also it generates
an event as a change notification. The sensors client does it similarly, but stores new
temperature values into process variable motor_temp
and also generates
a change notification. If the motor temperature exceeds a critical limit, it
sets process variable overheating
and also generates a change notification.
If the CAN client receives a query for current position message, it sets and generates
a change notification on process variable position_request
.
Make the system work:
current_pos
,
overheating
and motor_temp
. Everytime it receives a change
notification, it generates a matching CAN message.
....
pv=pv_event_await_syncronous(...);
if (pv == pv_current_pos) {
.... /* send CAN message with new position */
}
else if (pv == pv_overheating) {
if (*pv_overheating == TRUE) {
.... /* send CAN message to power down system */
}
else {
.... /* send CAN message to power up system */
}
}
else if (pv == pv_motor_temp) {
.... /* send CAN message with new motor temperature */
}
else {
.... /* error handling */
}
....
....
sleep(1);
.... /* read current encoder position */
if (new_value != *pv_current_pos) {
*pv_current_pos = new_value;
pv_event_generate(...,pv_current_pos);
}
....
The second thread waits for a change notification of process variable position_request
.
If it arrives it reads the current encoder position, stores it and generates a change
notification as an answer. So this client has to register a change notification to process
variable position_request
.
....
pv=pv_event_await_syncronous(...);
if (pv == pv_position_request) {
if (*pv_position_request == TRUE) {
.... /* read current encoder position */
*pv_current_pos = new_value;
pv_event_generate(...,pv_current_pos);
}
....
motor_temp
and generates a change notification. Also it checks whether
this temperature exceeds a limit or not and if it does so, it generates a change
notification on process variable overheating
.
....
sleep(1);
.... /* read current temperature */
if (new_value != *pv_motor_temp) {
*pv_motor_temp = new_value;
pv_event_generate(...,pv_motor_temp);
if (*pv_motor_temp > MAXIMUM_TEMP) {
if (*pv_overheating != TRUE) {
*pv_overheating = TRUE;
pv_event_generate(...,pv_overheating); /* temperature too high. Stop anything */
}
}
else {
if (*pv_overheating != FALSE) {
*pv_overheating = FALSE;
pv_event_generate(...,pv_overheating); /* temperature ok. Continue */
}
}
}
....
In this example a libpv based client on a target controls some LEDs. You can modify the amount of LEDs with a JVISU applet on your host.
To control the LEDs itself this example uses GPIO framwork based on sysfs entries to set and clear the LEDs. To control the LEDs from outside, there are also some process variables to calculate how many LEDs should be on or off. These are the vars, the JVISU is working with.
<processimage>
<!-- physical outputs -->
<port id="leds_set">
<device prefix="/sys/devices/platform/nxdkn-gpio.0/led_set"/>
<portPvChannel>
<pv id="leds_set" val="0">
<name></name>
<type>uint32_t</type>
<offset>0x0<offset>
<pv>
</portPvChannel>
<port>
<port id="leds_clear">
<device prefix="/sys/devices/platform/nxdkn-gpio.0/led_clear"/>
<portPvChannel>
<pv id="leds_clear" val="0">
<name></name>
<type>uint32_t</type>
<offset>0x4</offset>
</pv>
</portPvChannel>
</port>
<!-- logical PVs -->
<pv id="level">
<name></name>
<type>uint8_t</type>
<offset>0xC</offset>
</pv>
<pv id="fill">
<name></name>
<type>uint8_t</type>
<offset>0xD</offset>
</pv>
</processimage>
Make the system work:
pv_leds_set = pv_register_uint32(&pv_engine, "leds_set");
pv_leds_clear = pv_register_uint32(&pv_engine, "leds_clear");
pv_level = pv_register_uint8(&pv_engine, "level");
pv_fill = pv_register_uint8(&pv_engine, "fill");
pc_leds_set
content is written into "/sys/devices/platform/nxdkn-gpio.0/led_set"
pc_leds_clear
content is written into "/sys/devices/platform/nxdkn-gpio.0/led_clear"
pv_set_fn_write(&pv_engine, pv_leds_set, pv_device_write_int_file_ascii);
pv_set_fn_write(&pv_engine, pv_leds_clear, pv_device_write_int_file_ascii);
pv_fill
and pv_level
, setting the new values to the LEDs hardware
and unlocking the engine.
In this example pv_fill
decides to switch on all LEDs up to the pv_level
or to only switch on one LED at pv_level
's value.
while (!shutdown) {
pv_lock(&pv_engine);
if (*pv_level > 15)
*pv_level=15;
*pv_leds_set = (1<<(*pv_level));
if (*pv_fill) {
for (i=0; i<(*pv_level); i++) {
(*pv_leds_set) |= (1<<i);
}
}
(*pv_leds_clear) = (-(*pv_leds_set + 1)) & 0xffff;
pv_to_device(&pv_engine, pv_leds_set);
pv_to_device(&pv_engine, pv_leds_clear);
pv_unlock(&pv_engine);
usleep(10000);
}
New values into pv_level
and pv_fill
are injected by JVISU.
examples/jvisu
for the rest of this example.
Char "|" means one of the listed items, "()" means required but more than one choice, "[]" means optional
<pv id="yourUniqueIdentifier" [val="defaultValue"]>
[<repeat>Count</repeat>]
<type>(uint8_t | uint16_t | uint32_t | uint64_t |
int8_t | int16_t | int32_t | int64_t |
float | double | string | bool) </type>
(<offset>Value</offset> | <offset/>)
<name [language 1]>text</name>
[<name [language 2]>text</name>]
....
[<scale>
<poly>
<coeff order=order1>factor</coeff>
[<coeff order=order2>factor</coeff>]
....
</poly>
</scale>]
[<unit><factor [scale=value] [name=char]>UnitChar</factor>]
[<min>val</min>]
[<max>val</max>]
</pv>
libpv handles all <pv>
tags in it. To define a process variable
use the <pv>
tag for one resource.
<pv id="yourUniqueIdentifier" [val="defaultValue"]> </pv>
Within this tag you have to specifiy this resource:
<type>
tag
supported types are:
uint8_t, uint16_t, uint32_t, uint64_t
int8_t, int16_t, int32_t, int64_t
float, double
string
bool
<type>uint8_t</type>
<offset>
tag
It defines the offset in the process image for this resource. You cannot ommit offset's value. But the behaviour would be unpredictable so libpv will complain about this. Example: Locate this variable at the beginning of the process image:<offset>0x0</offset>
<name>
tag
Give a textual description of this resource. You can also differentiate between user languages here.<name [language definition]>Your Structure Identifier</name>
Example: Add the description "Structure Identifier" to this variable:Provide an Englisch and German description for this variable:<name>Structure Identifier</name>
<name xml:lang="en">Structure Identifier</name> <name xml:lang="de">Struktur-Identifikator</name>
Within this tag you may specify also:
<repeat>
tag
If you want to define an array of elements, use this tag to create an array with n elements instead of only one. Example: Create an array for a 20 character string:<repeat>20</repeat>
<scale>
tag
You can define this tag to linearize values read back from a nonlinear resource Example: Values should be linearized with f(x)=-5+17x<scale> <poly> <coeff order="0">-5</coeff> <coeff order="1">17</coeff> </poly> </scale>
Note:
This tag is not supported yet!
<unit>
tag
Define a Unit and maybe a simple scaling factor for this resource. Example: The variable should be Voltage but displayed as "mV"<unit> <factor scale="0.001" name="m"/>V </unit>
Note:
This tag is not supported yet!
<max>
or <min>
tag
You can define limits to the resource. Example: This variable should be at least 0 and and most 25:<max>25</max> <min>0</min>
Note:
This tag is not supported yet!
You can add as much own tags as you want or need. It depends on
your application what other information is needed. But you should locate
all this additional information outside the <pv>
tag. So the
<pv>
tag is one of the inner levels, everything beside it on
the same level is up to you.
To describe a more complex I/O system extend the XML files specified above with the following tags:
<group>
tag around your process variables.
Organize them as resources with the same type (for example: Lamps, buttons,
sensors etc.). "id" should be a unique group identifier on the same level.
<group id="resource1">
</group>
<group id="resource2">
</group>
<group id="resource3">
</group>
....
<port>
tag for every single resource in each group.
"id" should be a port identifier in your group definition, at least "index"
should be unique if "id" isn't unique on the same level.
<group id="resource1">
<port id="myfirstresource" index="1">
</port>
<port id="myfirstresource" index="2">
</port>
</group>
<group id="resource2">
<port id="mysecondresource" index="1">
</port>
<port id="mysecondresource" index="2">
</port>
</group>
....
<port>
is represented by a <device>
and a
<portPvChannel>
The <device>
tag defines the device on your system behind this kind of resource.
With the three attributes prefix, index and
suffix you can define the path and name to the device node.
They are concatenated to build the full filename.
direction restricts the data direction if this resource does not
support read/write. Supported restricted directions are in (in
only), out (out only) and inout.
At last the <portPvChannel>
tag comprises the resource description with the
<pv>
tag.
<group id="resource1">
<port id="myfirstresource" index="1">
<device prefix="/dev" index="/ttyS" suffix="0" direction="in"/>
<portPvChannel>
<pv id="myInputDevice" val="0">
<type>uint8_t</type>
<offset>0</offset>
</pv>
</portPvChannel>
</port>
......
Every client that calls libpv's pv_init()
will connect itself to a
process image.
At this point libpv reads in the process description within the XML
process description file on a per client basis.
From this file libpv creates an internal representation with
a linked list of process variable elements and calculates the needed
size to store all of these elements.
At this point of time all process variables are only known as their types, their
sizes and their offset they will later reside at.
To breath some life into it (e.g. to give each process variable a value) a real memory area to store process variable values is needed. To do so libpv opens a data file from a backing store and maps this file into the client's memory space and also shares this chunk of memory globally. This chunk of memory is called the process image and the calling client is now connected to it.
When this is done the first time, all process variable values from the
last run are restored. But this is only done when the first client calls
pv_init()
. Any further client connecting to the same process image only
uses the globally shared memory and start to use the current process variable
values.
When the file is successfully mapped, accessing every process variable in this process image is possible, while the operating system is responsible to write back this memory area to the backing store every time something was changed.
With this procedure, in the case of a power fail and after a restart of the system, the last process variable values written to the backing store are restored automatically and the client can continue at this point just before the fail.
There are maybe special environments and backing stores where this procedure cannot be used. If your backing store is a flash memory device and you are using a filesystem on it that can't handle writeable mappings (JFFS2 is such a filesystem) libpv automatically uses a different way: It also shares the process image as a chunk of memory between all connected clients. But it's really only memory. In this case the client is responsible to tell libpv that it has changed something in the process image to handle the write back in a correct manner by libpv. So for each client's modification in the process image a pc_flush() is required.
Process variables also can represent hardware states. Their state can represent a piece of control hardware (e.g. write only) or some kind of sensors (e.g. read only). But they reside in a chunk of memory and that means some kind of connection or update mechanism between a process variable and the hardware is required. At first there is a polling mechanism based on the library functions:
pv_to_device()
to update hardware based on the process variable's value
pv_from_device()
to update the process variable based on hardware state
Call these functions whenever you need an update of your process variable or
external hardware.
But to let it work someone has to translate the process variable's value into device
dependend value and vice versa. This can be done by a read or write function
registered to a process variable.
To register a read function (to update from external hardware values into
process variable) use pv_set_fn_read()
. To register a write function (to
update from process variable into external hardware) use pv_set_fn_write()
These device dependent update functions are defined to:
write_fn(pv_engine_t *engine, struct pv *pv, unsigned long data);
read_fn(pv_engine_t *engine, struct pv *pv, unsigned long data);
To read or write data from or to a device you can use the file handle pv->device_file
.
It's already opened and usable. Do everything here to read or feed your device.
You gain access to the process variable by using pv_to_abs_addr(engine,pv)
to get a pointer to it. It's up to yourself to cast this pointer to its right size.
pv->type
may help in this case.
Currently the data parameter is not used and always 0.
This is an easy way to communicate with a hardware driver. It uses data I/O based on a device node. More complex systems are imaginable. At this point only you are knowing how to deal with your hardware.
For change notification (events) there is a dedicated server required. It manages all clients that share this process image and also handles all the required work to distribute change notifications to all interested clients.
When the event server starts, it creates a message queue and waits for messages. Whenever a new client starts, it sends a message to the server and registers itself. Nothing else happens until the client also registers for notification. This is an additional message from the client to the server. In this case the server adds this request to a list linked to the process variable of interest.
Details: The server creates an unique ID first. It uses IPC's ftok() function to create this
unique ID from the name of the given process image file and the number "43".
With this unique ID it creates a message queue name with the pattern
/pve_<ID as decimal>
(see pv_types.h, macro QUEUE_NAME for current
implementation). So there is exactly one message queue per process image. If any client
generates this unique ID in the same way, it can open a connection to the server of the
given process image file.
On the other hand, the client does a similar thing: But it doesn't use an unique ID, it
uses its process ID instead. So it creates a message queue with the name
/clnt_<PID as decimal>
to wait for notifications (see pv_types.h, macro
QUEUE_CLIENT for current implementation).
Whenever a client changes a process variable and calls pv_event_generate() this also sends a message to the server. The server only follows the list linked to this process variable and sends a change notification to each registered client in the list.
Removing a notification and removing the whole client are also simple messages to the server. Removing a client forces a complete deregister of notifications of all managed process variables.
The server can handle unlimited process variables, registerd client and registered notifications, because all are linked lists. The only limit is the available memory space and - maybe - computing power.
Bugs: If a client quits without the "Deregister Client" message, the server handles this client further on as active!