Data Acquisition (DAQ) and Control from Microstar Laboratories

Coding the QDECODE command

data acquisition filtering data acquisition software channel architecture enclosures services

State-driven processing in a custom command

encoded gearworks icon

(View more Software Techniques or download the command module.)

This article provides a detailed tour through the complete coding for a non-trivial custom processing command. Along the way, it also provides a quick introduction to quadrature encoding and how to build state-driven processing in a custom processing command.

If you wish, you can view the final code listing. Complete details about using the finished command are documented in the QDECODE Command Reference Page.

To build a downloadable binary module, all you need is a copy of the Developer's Toolkit for DAPL from the DAPtools Professional CD — and the compiler of course. If you prefer, you can download a pre-compiled module for testing on your DAP system.

Quadrature encoding and decoding

The purpose of the QDECODE command is to monitor and decode pulse sequences that arrive on a pair of digital lines on the Data Acquisition Processor digital input port. These particular digital logic signals are produced by a quadrature encoder attached to a device that can rotate forward or backward. The QDECODE command must determine the current angular position by monitoring the digital pulses. You can do this monitoring using special hardware devices, with excellent efficiency at high speeds. When you don't have the high speeds, however, this command might allow you to dispense with that extra hardware.

Because of the way that the encoder signals are generated, when the device rotates in the positive direction, the pulse sequence observed is:

  1. with bit 1 holding low, bit 0 goes high
  2. with bit 0 holding high, bit 1 goes high
  3. with bit 1 holding high, bit 0 goes low
  4. with bit 0 holding low, bit 1 goes low

and this cycle repeats. But when the direction of rotation reverses:

  1. with bit 0 holding low, bit 1 goes high
  2. with bit 1 holding high, bit 0 goes high
  3. with bit 0 holding high, bit 1 goes low
  4. with bit 1 holding low, bit 0 goes low

In these sequences, each of the four possible bit patterns has three possible bit changes: first bit changes, second bit changes, or nothing changes. Each has a unique interpretation as a movement forward, a movement backward, or no movement. All of this is summarized in the following state table.

//   State 1 (Previously L-L)      State 2 (Previously L-H)
//     L-L / State 1 ( +0 )          L-H / State 2 ( +0 )
//     L-H / State 2 ( +1 )          H-H / State 3 ( +1 )
//     H-L / State 4 ( -1 )          L-L / State 1 ( -1 )
//
//   State 3 (Previously H-H)      State 4 (Previously H-L)
//     H-H / State 3 ( +0 )          H-L / State 4 ( +0 )
//     H-L / State 4 ( +1 )          L-L / State 1 ( +1 )
//     L-H / State 2 ( -1 )          H-H / State 3 ( -1 )

Using the state table, each time a new report of the digital port status is received, the bit pattern can be classified as an incremental move upward, or downward, or no change. If there is a change, the table gives the state for analyzing the next change. The accumulation of incremental changes indicates the current angular position.

Structuring the QDECODE command

We will need the following sections in the command code.

  1. Command interface – identify the command to the DAPL system
  2. State machine mechanisms – define elements needed for state table processing
  3. State table – encode the state table using the state machine mechanisms
  4. Command body – define the run-time processing sequence

The first three sections are declarative: they define what to do but not when to do it. The last section is active: it invokes previously defined actions in the proper sequence.

Command identification

The command will need to use features of the Developer's Toolkit for DAPL to run within the DAPL embedded environment, so it must include the DTD.H header file. The command will need to be "mounted" as part of the DAPL system, so it will need the registration functions declared in the DTDMOD.H header file. We will also define two convenient notations for later use.

#include  "DTDMOD.H"
#include  "DTD.H"
#define    BUFFER_LENGTH   256
#define    FOREVER         1

The registration process will invoke a callback function. This function must be present to tell the DAPL system the command's name and entry point. This is ugly boilerplate coding, but fortunately there is not very much of it.

extern "C" int __stdcall   QDECODE_entry (PIB **plib);

// Define the system callback function for identifying
// the command prior to execution runtime.
//
extern "C" __declspec(dllexport)
int    __stdcall  ModuleInstall(void *hModule)
{   return ( CommandInstall(
       hModule, "QDECODE", QDECODE_entry, NULL)) ; }

State processing definitions

For processing in a state machine table, you need to know:

Which is the current operating state?

    typedef  struct st_trans  STATE;

This is a forward type declaration. We temporarily defer the details about what this element contains.

Given the input, which state is next?

    typedef  long int   STATE_TRANSITION ( unsigned short, STATE * );

Once again, this is only a data type declaration, and we defer the details for a moment.

How should the accumulated count be adjusted?

The state transition functions return an integer value that reports the adjustment to apply to the accumulator.

State table representation

Now we will code the state table, and define how to respond to each possible input pattern.

Each state transition function receives access to the current state and the current bit pattern from the digital port. To classify the input combinations that could cause changes in state, define the four possible bit combinations. Also define a mask pattern to help isolate these bits from all of the others on the digital input port.

#define  STATEMASK    ( 0x0003 )
#define  STATE1_BITS    ( 0x00 )
#define  STATE2_BITS    ( 0x01 )
#define  STATE3_BITS    ( 0x03 )
#define  STATE4_BITS    ( 0x02 )

What to do in each state is decided by a function. Every state will need one of these state transition functions. In addition, declare an artificial transition function for establishing the starting state.

static long int    STATE0_TRANSITION (
  unsigned short inbits, STATE * pState );
static long int    STATE1_TRANSITION (
  unsigned short inbits, STATE * pState );
static long int    STATE2_TRANSITION (
  unsigned short inbits, STATE * pState );
static long int    STATE3_TRANSITION (
  unsigned short inbits, STATE * pState );
static long int    STATE4_TRANSITION (
  unsigned short inbits, STATE * pState );

Now we can code the state transition functions according to the state table. A switch statement classifies the input bit pattern as one of the defined input patterns. Then the appropriate action for that bit pattern is applied. For the example State 1, we know that the previously observed bit pattern was LOW-LOW, so the current observed patterns, could be LOW-LOW (no change), LOW-HIGH (forward a step), or HIGH-LOW (backward a step). If there has been a change, the current state is adjusted according to the actions that will be needed next time. The transition function also reports the appropriate output adjustment: +1, -1, or 0.

static long int  STATE1_TRANSITION (
  unsigned short inbits, STATE * pState )
{
    long int   count_update;
    switch ( inbits )
    {
      case  STATE1_BITS:
      case  STATE3_BITS:
        count_update = 0L;
        break;
      case  STATE2_BITS:
        pState->pTransition = &STATE2_TRANSITION;
        count_update = 1L;
        break;
      case  STATE4_BITS:
        pState->pTransition = &STATE4_TRANSITION;
        count_update = -1L;
        break;
    }
    return count_update;
}

We can now review how the state updates work in full detail. The runtime code determines which state transition function to call by fetching the function pointer from its STATE variable. As it calls this function, it also passes the location of the STATE variable, via the second calling parameter pState. Let us assume that the state pointer originally points to the STATE1_TRANSITION function, as seen above. As a side effect of analyzing the bit pattern, the STATE1_TRANSITION function can select a new transition function, either STATE2_TRANSITION or STATE4_TRANSITION, replacing the pointer currently contained in the STATE variable. The runtime code will fetch the modified pointer and invoke that new transition function to analyze the next input bit pattern.


The code for the other three transition functions is almost the same and will be omitted here. You can see this other code in the complete code listing. Also omitted here is the coding for the artificial STATE0_TRANSITION function. It has the same form, but for all input patterns it establishes the state and returns a value of zero as if a "no change" pattern just occurred.

We temporarily overlooked one detail. The state transition functions all presume that the encoder bits are in the 0-1 bit positions. We can prepare for this by defining a small utility function to force this presumption to be true.

inline unsigned short   get_inbits(
    unsigned short inval, unsigned short inshift )
{
    inval >>= inshift;
    return (inval &= STATEMASK);
}

Runtime code

The rest of the code is for the runtime processing. It begins when the DAPL system receives a START command (and activates processing tasks such as QDECODE). The runtime code can be organized into four sections.

  1. Local storage allocations – declare local working variables for the task
  2. Establish system connections – open connections to pipes and set up buffering for them
  3. State initialization – a one time operation to start state processing
  4. Main processing loop – receive inputs and dispatch state updates

Local allocations

Each task has a completely isolated namespace, so it does not hurt to define variables with module scope. Most of the time it is easiest to declare variables "on the stack" with auto storage class. A QDECODE task will use the following local variables.

  • "Boilerplate" for accessing task parameters.
    void **argv;         // list of task parameter handles
    int argc;            // number of parameters in list
  • Variables for accessing input data from the digital port.
    PIPE            * pDigPipe;     // digital port values
    PBUF            * pDigHandle;   // buffer control
    unsigned short  * pBufStorage;  // storage location
  • Variable for receiving the bit address parameter information.
    short unsigned    in_shift;     // bit addressing
  • The address of the globally-defined 32-bit accumulator for posting the current output position count.
    long volatile   * pOutVar;      // accumulator
  • Finally, some utility variables are defined for operating the main runtime loop. The STATE variable is also defined here.
    STATE     current_state;
    int       iFetched, iNext;

Establishing system connections

This is not exactly "boilerplate" code, but it is at least stereotypical. First, invoke a DTD function to establish a list of locally-valid pointers, given the list of task parameter handles provided by the DAPL system when the task starts. There is a minimum of 3 and maximum of 3 parameters, so we don't need to do anything further with the argument count argc.

    argv =  param_process( plib, &argc, 3, 3,
      T_PIPE_W,         // pipe to access bit patterns
      T_CONST_W,        // address of bit pair to use
      T_VAR_L };        // output accumulator

Use the returned list of locally-valid pointers to obtain access to the system connections.

  • Connect to the input data stream. Set up a pipe management buffer and locate the data storage area of this buffer.
      pDigPipe  = (PIPE *)(argv[1]);
      pipe_open(pDigPipe,P_READ);
      pDigHandle = pbuf_open(pDigPipe,BUFFER_LENGTH);
      pBufStorage = (unsigned short *)
          pbuf_get_data_ptr(pDigHandle);
  • Fetch the digital port bit address. Validation should be applied here, but it is omitted for brevity. Force the address into an even-value positive number in the range 0 to 14, and record this value.
      in_shift  = *(unsigned short *)(argv[2]);
      in_shift &= 0x000E;
  • Load the address of the shared 32-bit accumulator.
      pOutVar   = (long volatile *)(argv[3]);

State initialization

One input pattern from the digital port is required to initialize the state. Take this value directly from the pipe, without buffering a block of values in memory. Apply the get_inbits utility to align the desired bits and suppress the others. Call STATE0_TRANSITION to establish the state. The return value is always zero, so use that as an initializer for the globally-shared 32-bit accumulator variable.

    *pOutVar  =  STATE0_TRANSITION (
      get_inbits((unsigned short)pipe_get(pDigPipe), in_shift),
      &current_state
     );

Run-time loop

The run-time loop looks like a continuously-running infinite loop. Samples must be very fast to capture all of the transitions, but the counting can be slightly delayed, resulting in a small backlog of data. Each pass through the loop receives and processes one block of data sampled from the digital port.

    while (FOREVER)
    {
        // Fetch and buffer any available data from digital port
        iFetched = pbuf_get(pDigHandle);

        // Process the data block
        ....

The loop is not really continuously executing, however. When all available input data are processed, the next pbuf_get operation cannot finish. The QDECODE task will be "blocked" on a data resource. The DAPL system will suspend the QDECODE task and schedule other tasks to run.

Within the "process the data block" section of the main runtime loop, values received from the digital port are processed in sequence by invoking the transition function for the current state. The returned increments are applied to update the globally-visible accumulator. Each state transition function prepares the next state automatically, so there is nothing more that needs to be done. The following is the complete runtime loop.

      while (FOREVER)
      {
          // Fetch any available buffered data from digital port
          iFetched = pbuf_get(pDigHandle);

          // Process each received sample
          for  (iNext=0; iNext<iFetched; ++iNext)
          {
              *pOutVar += (current_state.pTransition) (
                  get_inbits(pBufStorage[iNext], in_shift),
                  &current_state );
          }
      }
      return 0;

This particular command never reaches the dummy return 0 statement, but the compiler does not know that. It might diagnose an error if it thinks the function does not return an integer value as its declaration promises.

Variations

As described, this command is what you would want for a processing configuration in which a QDECODE command monitors the encoder, with other tasks operating on their own time schedules able to read the encoder value any time they need it. The drawback of having only the most current information, however, is that you can't know precisely when a result was posted, and you can't know the positions along the way. For example, if you are logging data, you might want to record the position values at regular time intervals consistent with other measurements. A variant of this command that provides updates in an output pipe at regular intervals requires adjustments to the command parameter processing, initialization, and run-time loop code.

The fundamental goal of the command, decoding quadrature encoded signal pairs, remains the same however. The DAPL system allows a single processing command to accept both variants. The details of doing this are not difficult but beyond the scope of this note. You can see how this is done in the code listings provided with the pre-compiled module download.

Conclusions

You have just seen full implementation details of a non-trivial custom processing command. The result is a potentially useful command for monitoring the position of a rotating device or a device driven by rotating gears, decoding quadrature encoder signals without using any specialized quadrature decoding hardware. A useful technique for coding a state machine in software is shown. There are also examples of accessing DAPL system pipes and variables from a custom processing command. Worthy of notice is the fact that no direct hardware control functions are necessary.

The coding style mostly avoids advanced features of C++. There are some reasons for this. Within the microcosm of one independent custom command, there is not much incremental benefit from advanced C++ features (classes, references, templates, overloaded operators, constructors, etc.). Staying mostly with C-compatible language features makes development more accessible to embedded systems programmers familiar with C but not full C++. And finally, some of the alternative schemes in C++ for representing state machines are truly awful. Be careful. Making states into polymorphic variants of a generic state class can cause the compiler to invoke operator new and constructor calls, sometimes multiple times, for each update of the state. Memory management operations are complicated and can cause an unpredictable amount of overhead and delay. Compared to that, a simple function pointer assignment is very efficient.


 

Return to the Software Techniques page or download the command module.