Clone wiki

roseline / An API for timing information in control design

Roseline API for Control Design

Motivation

The performance of a controller often depends crucially on accurate time data in its sensors and actuators.

In this page we lay out a software architecture for providing a flexible framework for creating, managing, and delivering timestamped sensor and actuator values.

High-level Ideas

  • I should be able to set the frequency with which a sensor value is retrieved and stored.

  • The time of a sensor reading may only be known to lie within an interval. So let's permit time values to be intervals as well.

  • A sensor collects samples in a buffer that can be accessed in a few ways:

  1. Get the most recent sample
  2. Get all samples since time t
  3. Get the first sample after time t
  4. Get all samples since I last polled
  5. Get all samples in the whole buffer.
  • It seems reasonable to delete old samples once newer ones have been retrieved. Just store them all in a circular buffer?

  • An actuator can be commanded not just with a single value to assign immediately, but rather a whole "schedule" of (time, value) pairs that specify what value to apply and when.

  • If it doesn't matter when exactly the value is applied, the "time" that a value should be applied at may be an interval. This gives the actuator some flexibility in obeying a control schedule.

  • Error reporting: Modules should be able to log or notify their owner if something goes wrong, e.g., if the actuator is unable to stick to its schedule.

  • Joao initially envisioned using Linux character devices to buffer sensor and actuator data. This will permit us to swap hardware (sensors) without recompiling the kernel. Also, modeling time-stamped sensors as a stream of measurements seems to fit well in the Unix model of a device.

An Object-oriented design

class TimeStamp

A precise (scalar) moment in time.

  • time()

class TimeInterval : public TimeStamp

An interval of time.

  • min()
  • max()
  • time() returns the center of the interval.

class Sensor

Abstract class for packaging sensor data.

Methods:

  • time get_most_recent()
  • vector<time> get_all()
  • vector<time> get_all_since(time t)
  • set_buffer_size(int n)
    • Set the buffer size of the circular buffer within the character device.
  • set_read_rate(int hz)
    • Set the rate at which the sensor should read new measurements.

class Actuator

Methods:

  • set_value(v)
  • set_value_at_time(v,t)
  • set_value_in_interval(v,t)

A Design Inspired by Character-devices

Sensors that produce streams of measurements and actuators that are driven by streams of control signals are viewed as time-stamped virtual files (TSVFs). These files differ from usual files in that data is aggregated into atomic units of the same size, each one of them tagged with a time stamp. For sensors these files are read-only and each atomic unit correspond to a set of measurements collected by the physical sensor at a particular time instant, whereas for actuators these files are write-only and each atomic unit corresponds to a control value that should be sent to a physical device at a particular time instant.

TSVF share 5 basic operations with regular files -- open, close, read, write, and poll -- and, like special device files, uses ioctl for device-specific interactions with the physical device.

open

Open a connection to a TSVF.

handle=open(device,parameters)

close

Close a connection to a TSVF.

close(handle)

Time types and functions

exactTime_t

Data structure that stores a time instant.

    exactTime_t time;

uncertainTime_t

Data structure that stores the time stamp of an event, with associated uncertainty.

    typedef struct uncertainTime_s {
      exactTime_t minTime;
      exactTime_t maxTime;
    } uncertainTime_t;

timeCopy

Copies the content of src to dest.

    timeCopy(exactTime_t *dest,exactTime_t *src);
    timeCopy(uncertainTime_t *dest,uncertainTime_t *src);

timePlus

Computes and stores in t3 the sum of the times stores in t0 and t1 (i.e.,t3=t0+t1), propagating uncertainty appropriately.

For the 1st form, it is safe to have t3 equal to t0 or t1.

    timePlus(exactTime_t *t0,exactTime_t *t1,exactTime_t *t3);
    timePlus(uncertainTime_t *t0,exactTime_t *t1,uncertainTime_t *t3);
    timePlus(uncertainTime_t *t0,uncertainTime_t *t1,uncertainTime_t *t3);

timeDiff

Computes and stores in dt the different between the times stores in t1 and t0 (i.e., dt=t1-t0), propagating uncertainty appropriately. For the 1st form, it is safe to have dt equal to t0 or t1.

    timeDiff(exactTime_t *t0,exactTime_t *t1,exactTime_t *dt);
    timeDiff(uncertainTime_t *t0,uncertainTime_t *t1,uncertainTime_t *dt);

getTimePre, getTimePost

These commands store in the variable time the time at which an event takes place. getTimePre should be called before the event and getTimePost should be called after the event to facilitate quantifying the uncertainty about the time stamp.

    getTimePre(uncertainTime_t *time);
    getTimePost(uncertainTime_t *time);

Example

    uncertainTime_t timeStamp;

    getTimePre(&timeStamp);
    // execute code
    getTimePost(&timeStamp);

waitUntil

Waits (roughly) until the time specified by time. Returns 0 is all goes well, or -1 if the call is interrupted by a signal handler.

    int waitUntil(exactTime_t *time);
    int waitUntil(uncertainTime_t *time);

Sensor data types and functions

Sensor data are stored in circular buffers. The buffers have

circularInt16Buffer_t

Data structure that stores a stream of time-stamped measurements. Each measurement is an array of int16_t.

        typedef struct circularInt16Buffer_s {
          int lengthMeasurement;
          int maxMeasurements;
          int count;             // cumulative number of measurements read to buffer
          int validMeasurements; // # of valid measurements in the buffer (redundant)
          int oldestFull;        // index of oldest unprocessed measurement
          int firstEmpty;        // index of first empty position in the buffer
          int32_t *counts;
          uncertainTime_t *timeStamps;
          int16_t *data;
        } circularInt16Buffer_t;

newCircularBuffer

This function allocates a buffer that can store up to maxMeasurements measurements, each consisting of lengthMeasurement int16_t. This function returns a pointer to the newly allocated buffer or NULL, in case the memory allocation failed.

        circularInt16Buffer_t *newCircularBuffer(int lengthMeasurement,
                                                 int maxMeasurements);

deleteCircularBuffer

This function deletes a buffer, freeing the memory allocated by newCircularBuffer. It does nothing in case buffer is NULL.

        void deleteCircularBuffer(circularInt16Buffer_t *buffer);

lenCircularBuffer

This function returns the number data measurements currently in the buffer.

        int lenCircularBuffer(circularInt16Buffer_t *buffer);

pushCircularBuffer

This function adds a new measurement to the buffer by coping to the buffer the measurement in data. It returns the number of data measurements left in the buffer if all goes well or -1 if the buffer was full and an old measurement was discarded to make space for the new one.

    int pushCircularBuffer(
          circularInt16Buffer_t *buffer,
          uncertainTime_t *timeStamp,
          int16_t *data);

popCircularBuffer

This function copies to data the “oldest” measurement in the buffer and removes it from the buffer. It returns the number of data measurements left in the buffer if there was a measurement in the buffer or -1 if the buffer was empty and no measurement could be removed. “Oldest” refers to the order in which measurements were added to the buffer and not to the time stamps.

    int popCircularBuffer(
          circularInt16Buffer_t *buffer,
          int32_t *count,
          uncertainTime_t *timeStamp,
          int16_t *data);

Low-level functions to access sensing devices

openI2CDevice

This function opens communication with a device in a I2C bus and initializes n 8-bit registers.

    int openI2Cdevice(
          int i2cBus,
          int deviceAddress,
          int n,
          uint8 *init);

The input init points to an array with 2n elements, of the form

   { register address 1, initialization value 1,
     register address 2, initialization value 2,
     ...
     register address n, initialization value n}

The function return the file descriptor that should be used for communication or -1 in case the operation fails.

Updated