Skip to content

Latest commit

 

History

History
160 lines (110 loc) · 9.08 KB

TripleAxisPipe.md

File metadata and controls

160 lines (110 loc) · 9.08 KB

TripleAxisPipe

An interchange interface and utility classes for exchange and processing of vector data.

#define

  • 'Efferent:' Moving away from a specified point-of-reference. That is: an output. Efferent "exits".
  • 'Afferent:' Moving toward a specified point-of-reference. That is: an input. Afferent "arrives".
  • 'Source:' A link in the pipeline that only has efferent connections. Generally, a sensor driver of some sort.
  • 'Sink:' A link in the pipeline that only has afferent connections. Generally, the application itself.

Common problems in 3-axis sensors

There are number of sensor modalities that return sophisticated data in three axes. Not only does this data typically share similar constraints and processing requirements, but it also relates to physical space and is used in the same kinds of pipelines.

The drivers for these sensors are organized as "feed-forward" pipelines, which means that data is emitted once from the source, and is processed in stages which may be parallel. Execution flow through the pipeline is rooted in the driver's intake function. This typically means the I/O callback or application-accessible poll() function.

Suppose we had a 9-DOF sensor (Mag, Gyro, Accelerometer), and our application needs orientation, with configurable filtering...

graph TD
  A(Hardware Driver) -->|A/G/M data in SI units| B(Orientation Filter)
  B -->|Orientation data| C(Median Filter)
  C -->|Filtered Orientation| D(Application)
Loading

Pipelines are 1:1 with respect to their efferent connections to other nodes in the pipeline. But there is a special-case pipeline class called TripleAxisFork. The only purpose of this class is to bifurcate the data flow to different filters and processing functions.

Suppose we had a 9-DOF sensor (Mag, Gyro, Accelerometer), and our application needs a compass and orientation. That is: two separate pieces of information from the same root data. You might have a pipeline like this...

graph TD
  A(Hardware Driver) -->|A/G/M data in SI units| B(TripleAxisFork)
  B -->|A/G/M data in SI units| C(Orientation Filter)
  B -->|A/G/M data in SI units| D(Compass)
  C -->|Orientation| E(Median Filter)
  D -->|Bearing data| Z(Application)
  E -->|Filtered Orientation| Z(Application)
Loading

The data structure underlying pipeline assignments is an implicit single-linked list with the source of the data at the root. But afferent references to a given node may be replicated across data sources that feed that class. Thus, there is no need for a 2:1 afferent-to-efferent analog of this class.

Suppose we had a simple accelerometer, and a discrete magnetometer and our application is a tilt-compensated compass...

graph TD
  A(Accelerometer Driver) -->|Acc data in SI units| C(Compass)
  B(Magnetometer Driver) -->|Mag data in SI units| C(Compass)
  C -->|Bearing data| Z(Application)
Loading

In practice, two discrete sensors feeding 3-axis data to a class that reports data to a user will not match in the orientation of their returned data, nor the unit. Thus, there is a class (TripleAxisConvention) for doing axis reflection and 90-degree rotations, so that downstream classes don't need to handle this common logic explicitly. By the time data is given to the application layer, it ought to be in a uniform representation. By necessity, our frame-of-reference is the unit itself, and by convention is represented as aircraft principal axes. With the exception of THIS class, this convention is an assumption throughout the pipeline.

Extending the prior example to control for hardware relationships and differences in mounting...

graph TD
  A(Accelerometer Driver) -->|Acc data in SI units| C(TripleAxisConvention)
  B(Magnetometer Driver) -->|Mag data in SI units| D(TripleAxisConvention)
  C -->|Acc data in SI units| E(Compass)
  D -->|Mag data in SI units| E(Compass)
  E -->|Bearing data| Z(Application)
Loading

Memory conventions in the pipeline

To reduce the memory load of simply having the pipeline, vectors are referred to, rather than passed on the stack as a float would be. Afferent data should never be mutated, nor copied needlessly. It should be assumed that the entire pipeline exists in a single thread, and that from any given stage, other stages in the efferent direction will never store pointers to data they receive on their afferent sides. Thus, it is safe for any stage in the pipeline to copy an afferent vector in its local execution stack, and pass a pointer to it downstream with pushVector().

Any classes or use-cases that need to violate these assumptions must account for the memory implications in their own local scope. TripleAxisTerminus is an example that violates the local-storage assumption, and TripleAxisConvention that violates the mutation assumption.


Propagation of error

All sensors have non-zero error in their measurements. And that error might be reduced or magnified by any stage of the pipeline that handles the data. So the pipeline supports parallel propagation of an optional error vector. If the vector is non-null, it should be handled appropriately in the class, and then relayed down the pipeline. This means that the application will only receive error information if every step in the pipeline handles it, starting at the sensor. So read those datasheets carefully.


Pipeline sources

All data that moves through the pipeline is assumed to be given in SI units. Data must have a corresponding enum in the library to be accepted. There is also a type SpatialSense::UNITLESS to accommodate special cases.

Sensors are generally sources but (apart from terminology) there is no reason that they can't have afferent connections from other classes. Such a case might arise with sensors that have "integrated hubs", as do some Bosch and ST parts.

Sources need not themselves implement any interfaces from the pipeline. They only need to call pushVector() with conforming parameters.


Pipeline sinks

Writing a sink class is trivial, but there is a pipeline sink class called TripleAxisTerminus that makes a convenient end-point for delivery of vectors to the application layer. This sink holds a local copy of the last-known data and error, and provides a sequence number and timestamp of the last update.

By defining a function with the signature...

int8_t my_callback_fxn(SpatialSense s, Vector3f* dat, Vector3f* err, uint32_t seq_num);

...the application can be informed of outputs from the pipeline without having to poll for them.

/*
* Pipeline sinks that have a callback configured will call this function on
*   value updates. If this function returns zero, the terminating class will not
*   be marked "fresh", since we have presumably already noted the value.
*/
int8_t callback_3axis(SpatialSense s, Vector3f* dat, Vector3f* err, uint32_t seq_num) {
  switch (s) {
    case SpatialSense::ACC:
      printf("%u  \tX: %.4f \t Y: %.4f \t Z: %.4f\n", seq_num, data->x, data->y, data->z);
      if (nullptr != err) {
        printf("  \tX(err): %.4f \t Y(err): %.4f \t Z(err): %.4f\n", data->x, data->y, data->z);
      }
      break;
    default:
      printf("Unhandled SpatialSense in callback: %s\n", TripleAxisPipe::spatialSenseStr(s));
      break;
  }
  return 0;
}

TripleAxisTerminus down(SpatialSense::ACC, callback_3axis);  // The tilt sensor's best-estimate of "down".

An instance of TripleAxisTerminus that is constructed without a callback will need to be polled to determine changes...

TripleAxisTerminus down(SpatialSense::EULER_ANG);  // The tilt sensor's best-estimate of "down".

...

// Somewhere in a service loop...
if (down.dataFresh()) {
  Vector3f data;
  Vector3f err;
  uint32_t seq_num;
  getDataWithErr(&data, &err, &seq_num);
  printf("%u  \tX: %.4f \t Y: %.4f \t Z: %.4f\n", seq_num, data->x, data->y, data->z);
  if (nullptr != err) {
    printf("  \tX(err): %.4f \t Y(err): %.4f \t Z(err): %.4f\n", data->x, data->y, data->z);
  }
}

A note about TripleAxisCompass and SpatialSense::BEARING

The TripleAxisCompass class generates three data points from magnetometer and (optionally) accelerometer and orientation data. The afferents to the compass class are all proper vectors in 3-space. But the efferents are discrete pieces of information. Their positions in the vector are arbitrary and are defined the convention:

Vector.x --> Magnetic North
Vector.y --> Magnetic Dip
Vector.z --> True North

Values that TripleAxisCompass is not able to calculate will be fixed at 0. This is an epistemological weak-point that may be addressed by other means in the future.


Dependencies

  • This class needs StringBuilder for output. Excision of the print functions will eliminate the dependency.
  • This class uses the Vector3 template to represent data.
  • The platform must supply (or alias) an accurate millis() function.