3.3. State management

Motor control algorithms require maintenance of numerous state variables, for things like control loops and estimators (integrator and filter state), state machines (the state of the state machine), schedulers (countdown timers), configuration parameters, and so on.

The motor control application framework defines several data structures for managing this state data. The two top-level data structures are MCAF_SYSTEM_DATA and MCAF_MOTOR_DATA, used for storing per-system and per-motor state variables, respectively.

We used an approach consisting of three major aspects:

  • Structure — how are state variables organized?
    • Group data in appropriate C structures
    • Follow the Goldilocks principle: not too big or too small
  • Allocation — how are state variables allocated?
    • Minimize the number of independent global variables
    • Put them in easy-to-find places (no easter egg hunts)
  • Access — how are state variables accessed by functions?
    • Functions (with limited exceptions, such as main(), ISRs and the HAL) do not directly access global variables
    • Primitive types are passed in directly as arguments
    • Structures are passed in via a pointer or const pointer, and the smallest applicable structure is used.

Our goal is to follow the principle of least knowledge in the design of our system state; the top-level module functions get access to the entire data structure, whereas lower-level functions get access to appropriate pieces.

A simplified example may help explain this.

typedef struct tagMOTOR
{
  // Current loop forward path
  MC_DQ_T vdqCmd; // desired dq-frame voltage, direct output of current loop
  MC_DQ_T vdq; // desired dq-frame voltage
  MC_ALPHABETA_T valphabeta; // desired alphabeta-frame voltage
  MC_ABC_T vabc; // desired phase voltage
  MC_DUTYCYCLEOUT_T pwmDutycycle; // PWM count
  // Angle and speed, including estimators
  int16_t thetaElectrical; // electrical angle
  int16_t omegaElectrical; // electrical frequency
  MC_SINCOS_T sincos; // sine and cosine of electrical angle
  ...
} MCAF_MOTOR_DATA;

The top-level module functions look like the following. Note that the function MCAF_FocStepIsr() does have access to the entire MCAF_MOTOR_DATA structure, but it calls other functions, such as MC_CalculateSineCosine_Assembly_Ram() and MC_TransformParkInverse_Assembly() which only have access to appropriate information within that structure.

void MCAF_FocStepIsr(MCAF_MOTOR_DATA *pmotor)
{
  ...
  /* Calculate Sine and Cosine from pmotor->theta_e */
  MC_CalculateSineCosine_Assembly_Ram(pmotor->thetaElectrical, &pmotor->sincos);
  /* Calculate vAlpha, Vbeta from Sine, Cosine, Vd and Vq */
  MC_TransformParkInverse_Assembly(&pmotor->vdq, &pmotor->sincos, &pmotor->valphabeta);
  ...
}

This frees many of the modules from having to #include the main system state and reduces inter-module dependency. Changes in overall application behavior do not generally affect individual module behavior, so recompilation and testing are required less often.

Note that the module functions with this approach do not “own” their data. They do not allocate any global variables, do not access any global variables, and do not maintain any permanent pointers to external data. Instead, data is passed in as an argument. The module functions will operate on any data passed in. They don’t care whether they operate on data from one motor or another, or whether it’s mock data from a testing program or real data from an ADC.

As a result, the module functions are easily covered by unit tests. We just create sample inputs, call the appropriate function, and examine the outputs.

The main application is responsible for allocating program state variables for the entire system, and for each motor.

One consequence of this philosophy is that many of the motor control parameters cannot be hard-coded in the application; instead, they must be configurable parameters. The reason is that we wish to support control of two different motors at once using the same module functions, and each motor may have different software parameters, so these parameters need to be removed from the code and kept in state variables.

3.3.1. Miscellaneous Guidelines

  • Related state variables belong together. If a group of variables are always found and used together, they are probably best off if declared as members in a structure.

  • Hierarchy can assist and simplify naming: if there are 5 counters e.g. counterError, counterSuccess, counterResets, counterButtonPresses, and counterOvercurrent, these could be combined into a counters structure with the members not needing the word counter because they are now contained in a context that implies they are counters:

    typedef struct tagCOUNTERS
    {
      uint16_t error;
      uint16_t success;
      uint16_t resets;
      uint16_t buttonPresses;
      uint16_t overcurrent;
    } COUNTER_DATA;
    
    COUNTER_DATA counters;
    ++counters.resets;
    
  • Embedded structures (structures which contain other structures) indicate ownership: the contained structure is owned by, and is an inherent part of, the containing structure.

  • Referenced structures (structures which contain a pointer to other structures) indicate association and non-exclusivity: a contained pointer allows access to other data.