Abstract Industrial I/O: Process Image Handling with libpv

Documentation for libpv 1.1.6

Jürgen Beisert

Robert Schwebel


Table of Contents

1. About libpv
2. Requirements
3. Compiling libpv
4. Using libpv
4.1. Instanciating a pv_engine
4.2. Registering Process Variables
4.3. Locking
4.4. Destructing a pv_engine
4.5. Special Backing Stores
4.6. Event Handling, Change Notification
4.7. Events generated for Hardware interaction
5. Examples
5.1. Simple example with one client using one process image, only local
5.2. Complex example with three clients sharing one process image and using events
5.3. Complex example with more than one client using one process image, connected to a JVisy host
6. XML based process definition
6.1. The whole construct - abstract
6.2. The detailed construct
6.3. More complex IO systems
7. Internals of libpv
7.1. How it works
7.2. Special handling of unmappable backing store
7.3. Internal handling of resources
7.4. Event handling
8. Frequently asked questions

1. About libpv

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.

2. Requirements

libpv requires some other libraries:

  • liblist >= 1.0.0: Another library project by Pengutronix. See http://www.pengutronix.de/software/liblist/.

  • libxmlconfig >= 1.0.5: 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: - gcc 3.x

3. Compiling libpv

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
    Enable debug mode; the library is compiled with debug symbols and with -O1.

4. Using libpv

4.1. Instanciating a pv_engine

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.

4.2. Registering Process Variables

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.

4.3. Locking

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);

4.4. Destructing a pv_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.

4.5. Special Backing Stores

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.

4.6. Event Handling, Change Notification

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.

FIXME FIXME FIXME

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);

4.7. Events generated for Hardware interaction

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);
      }
    }
  
  

5. Examples

5.1. Simple example with one client using one process image, only local

TBD

5.2. Complex example with three clients sharing one process image and using events

In this example three independent clients sharing one process image to control a process that communicates with different kind of hardware.

  • the first client controls a CAN bus to send and receive CAN messages to or from other parts of the whole system.
  • the second client polls a position encoder.
  • the third client polls an I²C bus to read out sensors measuring temperatures.

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:

  • The CAN client registers a change notification to 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 */
     }
      ....
    
  • The encoder client uses two threads: One periodically sleeps for a while, reads in current position and if it differs from the last read, generates a change notification.
    
     ....
     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);
      }
     ....
    
  • The sensor client sleeps for a while, reads in current temperatures and if it detects a difference since the last read, it stores the new value in 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 */
        }
       }
     }
     ....
    

5.3. Complex example with more than one client using one process image, connected to a JVisy host

TBD

6. XML based process definition

An XML file is used by libpv and describes the IO model.

6.1. The whole construct - abstract

Char "|" means one of the listed items, "()" means required but more than one choice, "[]" means optional


	<pv id="yourUniqueIdentifier" [val="defaultValue"]>
		<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>

6.2. The detailed construct

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:

  • the <type> tag supported types are:
    • Unsigned values: uint8_t, uint16_t, uint32_t, uint64_t
    • Signed values: int8_t, int16_t, int32_t, int64_t
    • Float values: float, double
    • Character values: string
    • Bit values: bool
    Example: Define an unsigned integer with at least 8 bits:
    
    <type>uint8_t</type>
    			
  • the <offset> tag
    It defines the offset in the process image for this resource. You can ommit the offset with an <offset/> tag. But the behaviour is unpredictable. Example: Locate this variable at the beginning of the process image:
    
    <offset>0x0</offset>
    			

Within this tag you may specify also:

  • the <device> tag
    Specifies the device that should be used with this resource. Use internally a read/write function to ensure consistency between the process variable and the external device <device [prefix="string1"] [index="string2"] [suffix="string3"]/> Example: Use serial device #2 as an update source:
    
    <device prefix="/dev/ttyS" index="2">
    			
    This opens automatically the file with the name "/dev/ttyS2". Use the named pipe "/tmp/blablub.jbe" as an update source:
    
    <device prefix="/tmp" index="/blablub" suffix=".jbe">
    			
  • the <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:
    
    <name>Structure Identifier</name>
    			
    Provide an Englisch and German description for this variable:
    
    <name xml:lang="en">Structure Identifier</name>
    <name xml:lang="de">Struktur-Identifikator</name>
    			
  • the <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!

  • the <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!

  • the <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.

6.3. More complex IO systems

To describe a more complex IO system extend the XML files specified above with the following tags:

  1. Group your resources with the <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>
    ....
    	
  2. Create a <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>
    ....
    	
  3. Every <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. So the full description of one resource looks like that:
    
    <group id="resource1">
    	<port id="myfirstresource" index="1">
    		<device prefix="" index="" suffix="" direction="in"/>
    		<pv id="myInputDevice" val="0">
    			<type>bool</type>
    			<offset/>
    		</pv>
    	</port>
    ......
    	

7. Internals of libpv

7.1. How it works

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.

Note:

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.

7.2. Special handling of unmappable backing store

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 IO based on a device node. More complex systems are imaginable. At this point only you are knowing how to deal with your hardware.

7.3. Internal handling of resources

TBD

7.4. Event handling

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 do to distribute change notifications to all interested clients.

Note:

The server has to start first, because all other clients need a valid named message queue the server creates.

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.

8. Frequently asked questions

Q: What does the val attribute mean in a read only process variable?
Q: What do the attributs prefix, index and suffix in the <device> tag mean?
Q: What does happen if I omit the <offset> tag in more than one process variables or define more than one process variable with the same offset?
Q: Can I use pv_flush() at zero cost in my code?
Q: Is it mandatory to hold process images locked when I only want to read process variables?
Q:

What does the val attribute mean in a read only process variable?

A:

The val attribute is an optional attribute. In this case it doesn't matter, so you can omit it.

Q:

What do the attributs prefix, index and suffix in the <device> tag mean?

A:

Use them to build a device name libpv should open. If you define prefix="/dev", index="/ttyS" and suffix="1" a device node with the name /dev/ttyS1 will be opened.

Q:

What does happen if I omit the <offset> tag in more than one process variables or define more than one process variable with the same offset?

A:

Funny things will happen. Every process variable with no <offset> tag will reside at offset 0 in the process image. So all process variables without this tag will shadow each other. Maybe you want this behaviour to build some kind of union. Maybe it happens accidently. The behaviour is unpredictable.

Q:

Can I use pv_flush() at zero cost in my code?

A:

pv_flash() is a library function. It gets always called if you place it in your code. It's not usefull if you are using a full read-/writeable backing store, so it returns immediately, but there is always a small overhead per call. So the costs are low but not zero!

Q:

Is it mandatory to hold process images locked when I only want to read process variables?

A:

It's a good question. ;-)