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.
- Functions (with limited exceptions, such as
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
, andcounterOvercurrent
, these could be combined into acounters
structure with the members not needing the wordcounter
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.