Declarative vs. Imperative command handling in axis state machines — should mapp Framework be rewritten?

Declarative vs. Imperative command handling in axis state machines — should mapp Framework be rewritten?

Hi everyone,

I’d like to open a discussion about two fundamentally different approaches to structuring axis state machines when using MpAxisBasic (or similar function blocks). I’ve seen both patterns in real projects and I think the difference is worth talking about — and I have some uncomfortable questions at the end.

Approach 1: Imperative (commands set inside states)

This is the pattern used in the mapp Framework. Commands like .Power, .Home, .MoveAbsolute, .Stop, etc. are set to TRUE or FALSE inside the individual states of the CASE block:

CASE State OF
    STATE_POWER_ON:
        MpAxisBasic.Power := AxisControl.Command.Power;

    STATE_REFERENCE:
        MpAxisBasic.Home := TRUE;

    STATE_MOVE_TO_HOME_POSITION:
        MpAxisBasic.MoveAbsolute := TRUE;

    STATE_STOPPING:
        MpAxisBasic.Stop := MpAxisBasic.PowerOn;
    ...
END_CASE

Because commands are scattered across states and set imperatively, a dedicated reset action (ResetAllAxisCommands) is needed to clear all commands on every state transition. Forgetting to call it can leave a command “stuck” on TRUE.

Approach 2: Declarative (commands derived from state)

In this approach, the CASE block only handles state transitions — it decides when to move to the next state. All MpAxisBasic commands are then assigned in one centralized place after the CASE block, as boolean expressions derived from the current state:

CASE State OF
    (* only transition logic here — no direct FB command manipulation *)
END_CASE

(* All commands in one place, derived from state *)
MpAxisBasic.Power        := State >= AXIS_STATE_POWER_ON;
MpAxisBasic.Home         := State = AXIS_STATE_HOME;
MpAxisBasic.MoveAbsolute := State = AXIS_STATE_MOVEABSOLUTE;
MpAxisBasic.MoveAdditive := State = AXIS_STATE_MOVEADDITIVE;
MpAxisBasic.MoveVelocity := State = AXIS_STATE_MOVEVELOCITY;
MpAxisBasic.Stop         := State = AXIS_STATE_STOP_MOVEMENT;
MpAxisBasic.JogPositive  := State = AXIS_STATE_JOGPOSITIVE;
MpAxisBasic.JogNegative  := State = AXIS_STATE_JOGNEGATIVE;
MpAxisBasic.ErrorReset   := State = AXIS_STATE_ERROR_RESET;
MpAxisBasic.AutoTune     := State = AXIS_STATE_AUTOTUNE;

MpAxisBasic();

No ResetAllAxisCommands is needed — if the state changes, the expressions automatically evaluate to FALSE. There is zero risk of a “dangling” command.

Comparison

Aspect Imperative (inside states) Declarative (derived from state)
Where commands are set Spread across individual states One centralized block
Command reset Explicit reset action required Automatic — expression evaluates to FALSE
Bug risk Higher (forgotten reset = stuck command) Lower (state unambiguously determines outputs)
Readability Must scan entire CASE to see what’s active Complete command mapping visible at a glance
Adding a new state Must remember to update reset logic Just add one line with a condition
Flexibility Easier to do conditional/partial commands within a state Slightly more verbose for edge cases

The declarative approach essentially follows the Moore machine pattern — outputs are a pure function of the current state. The imperative approach is closer to a Mealy machine, where outputs depend on transitions and actions within states.

Questions for discussion

  1. Which pattern do you use in your projects, and why?
  2. Have you ever been bitten by a “stuck command” bug in the imperative approach?
  3. Are there situations where the imperative approach is genuinely better (e.g., complex sequences where a command needs to be set conditionally within a state)?
  4. For those using mapp Framework — have you considered refactoring to the declarative pattern, or does the framework’s structure make that impractical?

Now, let me raise some more provocative points:

  1. The mapp Framework is marketed as a “best practice” template that many developers blindly copy into their projects. If the imperative pattern is inherently more error-prone, are we propagating a flawed design pattern across the entire B&R ecosystem?

  2. Should the mapp Framework be rewritten to use the declarative approach? The ResetAllAxisCommands action is essentially a band-aid for a problem that the declarative pattern eliminates entirely. Why maintain a workaround when you can remove the root cause?

  3. How many undiscovered bugs are lurking in production machines right now because someone extended the mapp Framework state machine with a new state and forgot to call ResetAllAxisCommands at the right place? The imperative pattern essentially requires the developer to be perfect — every time, in every state, for every command. The declarative pattern requires no such perfection.

  4. If safety is our top priority in machine control, shouldn’t we choose the architecture that is inherently safer by design — where it’s structurally impossible to leave a motion command active by accident? Or are we comfortable relying on code reviews and testing to catch what the architecture could prevent?

  5. Is the B&R community too attached to “this is how we’ve always done it”? The declarative pattern isn’t new, isn’t exotic, and isn’t harder to understand — it’s arguably easier. So why isn’t it the standard?

I’m not trying to bash the mapp Framework — it does many things well. But I think this particular design choice deserves scrutiny, especially when it directly impacts motion safety. A stuck MoveVelocity := TRUE is not a theoretical concern — it’s a motor running when it shouldn’t be.

What do you think — is it time for a paradigm shift?

3 Likes

From project owner (a.i.) perspective I can tell you only this. I think both approaches are okay, and it is up to the team of developers which way they would like to go. Mapp Framework is smartly designed, and it shows one way how the solution can look like (imperative way it is easy to use and easy to read). At the moment, we are testing the release candidate of Mapp Framework 6 (no change in architecture, only migration to AS6). After release, we have to make a decision on which way we will go and this will be good time to collect feedback from users.

1 Like

Hello,

I personaly don’t like doing Approach 2, were cyclicaly Values/Commands are written.
This make testing via a Watch Window impossible, it needs cyclic plc performace for the copy action. Considering that i need a Home Command in more than one Step, becuase i need different Home-States for Commissioning or Restoring , makes this BOOL Logic more complex.

I prefer the Event based Approach to trigger actions in my Steps and then wait for the reaction of the System. To prevent “Deadlocks” due to previous not reset Flags/Commands you have to Program you Code in a way where you consider all possibilities which can happen. Also that someone can have operated Watch-Window and a Command was left behind and not reset.
I do also like the PLCopen API more than the MpAxisBasic API but i use both depending on the coustomer needs.

Example for a code which considers the possibility of a left over Command.

IF MpAxisBasic.MoveAbsolut = TRUE THEN 
  // reaction to not expected State of Command 
  MpAxisBasic.MoveAbsolut := FALSE; 
  // stay in State to redo in next cycle, when Command was Reset 
ELSE 
  // Start my Command 
  MpAxisBasicPar.Position := TargetPosition; 
  ....
  MpAxisBasic.MoveAbsolut := TRUE; 
  // go to wait State 
END_IF 

I personaly like if we have discussions like this, because you indeed do get sometimes stuck in your own patterns. So this helps to rethink why you do it like the way you do it.

Greetings
Michael

5 Likes

I agree with Michael for the same reasons. I personally think it’s more readable to handle the state’s actions within the state itself rather than outside of it.

However, I think that settings statuses outside of the state machine makes a lot of sense because some statuses may be true in multiple states. For example, I usually write a motion state machine in a way similar to this:

// Copy global program inputs into a local copy of the structure
Interface.Command := gAxisCtrl.Command;
Interface.Parameter := gAxisCtrl.Parameter;

CASE State OF
       STATE_OFF
       IF Interface.Command.Power THEN
            State := STATE_POWER_ON;
       END_IF    

       STATE_POWER_ON:
       MpAxisBasic.Power := TRUE;
       IF MpAxisBasic.PowerOn THEN
          MpAxisBasic.Power := FALSE;
          State := STATE_REFERENCE;
       END_IF
    
    STATE_REFERENCE:
       MpAxisBasic.Home := TRUE;
       MpAxisBasic.Home := FALSE;
       IF MpAxisBasic.Homed THEN
          State := STATE_READY;
       END_IF
    ...
END_CASE

// Set statuses
Interface.Status.PowerOn := MpAxisBasic.PowerOn;
Interface.Status.Ready := (State = STATE_READY);
...

// Write local status to the global control variable
gAxisCtrl.Status := Interface.Status


2 Likes

25 years I used imperative approach, because I quess it is easy and eveyrobody understand. But 2 years ago I started use declarative aproach
Personal point forced me to change:

  1. only one place for change->A lot of customers project, where was a lot of spaghetti code
  2. easy to read big projects → no need to search where somenone’s This.CMD is set to true/false or whatever
  3. clean state machine logic → Only transition is visible
  4. cooperation of more lone wolfs at one project become smoother:-)

And new important points:

  • much more easier that Agent understand whole logic an can easily update your code-> coupled with point 1.
  • agent can do change only at necessary place → no touch to wrong place-> coupled with point 2
  • perfect documentation generated automatically

@michael.bertsch has good point regarding changes of “outputs” at Watch windows. It is true. I also had problems with that in past, it forced me to look to code more deeper and prepare test cases before launch on real machine, what I didnt seen as important in past.

@marcusbnr setting of statuses is nice clean approach. Much better then set statuses inside state machine steps.

My current state is 90% of declarative aproach. Rest 10% I use where it is quick fix.
And last important issue as my personal observation> Declarative used ONLY what is “state machine” in default.

For example, at mapp framework is AuditMgr.st and there is no need to be de declarative and I dont use it either. But for task ExecuteQuerry.st I would use it for 100%.

It will be good more guys will contribute for their Con/Pro at this topic.

1 Like

I use the approach of setting values exactly then, when I need them.
I do not like the other approach, because then if you have to set stuff in multiple places, you’d have

MyFub.FirstBool := state = THIS_STATE OR state = THIS_OTHER_STATE OR state = THIS_THIRD_STATE;

and I do not like that, at all.

Your recent “new important points” I very much disagree with. I am not programming in a way for agents to understand it - the agent is there to support me, not vice-versa!

Rest of it, I agree with Michael and Marcus (though Marcus’ example basically shuts off the axis right after turning it on, so I disagree with that :stuck_out_tongue: )

Best regards

1 Like

@michael_w I understand the aesthetic objection againts:
MyFub.FirstBool := state = THIS_STATE OR state = THIS_OTHER_STATE OR state = THIS_THIRD_STATE;
, but this line is actually self-documenting — you can see exactly in which states the command is active in one place. In the imperative approach, you have to search the entire CASE block to find the same thing.
Moreover — if the OR string is really long, it’s a signal that the state machine design deserves a review.
The long OR string is not a problem of the declarative approach — it’s a problem of the state design, which would be hidden even in the imperative approach.

Here is real anonnymized example, for controlling the valve 5/3 (signal on coil, only when needed, locked position), for machine with 5 modes(Stop, Manual,Diagnostic, Calibration and Automatic):

ACTION Action_OutA_OutB: // Function Clamping valves

    // Initialize output and input addresses
    Output_Control_0.Out_Addr[1] := 0;
    Output_Control_0.Out_Addr[2] := 0;
    Output_Control_0.In_Addr[1] := 0;
    Output_Control_0.In_Addr[2] := 0;

    // Copy working mode for output group control from global variable
    Output_Control_0.Mode := ADR(Mode);

    // Output[1] - ValveA - Actuator group 2 - clamp UP
    Output_Control_0.Out_Addr[1] := ADR(ValveA);
    Output_Control_0.Safety_Select[1] := SAFETY_GLOBAL;
    Output_Control_0.Output_Enable[1] := TRUE;
    Output_Control_0.Stop[1] := FALSE;
    Output_Control_0.Man_Enable[1] := TRUE;
    Output_Control_0.Man_Override[1] := FALSE;
    Output_Control_0.Man[1] := FALSE;
    Output_Control_0.Man_Off_Default[1] := FALSE;
    Output_Control_0.Safety_Off_Default[1] := FALSE;
    Output_Control_0.Diag[1] := Mode.Diag.Step = 200;
    Output_Control_0.Calib[1] := FALSE;//Mode.Calib.Step = 1111;
    Output_Control_0.Auto[1] := Mode.Auto.Step >= 300 AND Mode.Auto.Step < 9000;
    Output_Control_0.In_Addr[1] := ADR(SensorA);
    Output_Control_0.In_Polarity[1] := TRUE;

    // Output[2] -  ValveB - Actuator group 2 - Clamp DOWN
    Output_Control_0.Out_Addr[2] := ADR(ValveB);
    Output_Control_0.Safety_Select[2] := SAFETY_GLOBAL;
    Output_Control_0.Output_Enable[2] := TRUE;
    Output_Control_0.Stop[2] := FALSE;
    Output_Control_0.Man_Enable[2] := TRUE;
    Output_Control_0.Man_Override[2] := FALSE;
    Output_Control_0.Man[2] := FALSE;
    Output_Control_0.Man_Off_Default[2] := FALSE;
    Output_Control_0.Safety_Off_Default[2] := FALSE;
    Output_Control_0.Diag[2] := Mode.Diag.Step = 100 OR Mode.Diag.Step = 300;
    Output_Control_0.Calib[2] := FALSE;//Mode.Calib.Step = 1111;
    Output_Control_0.Auto[2] := Mode.Auto.Step < 300 OR Mode.Auto.Step >= 9000;
    Output_Control_0.In_Addr[2] := ADR(SensorB);
    Output_Control_0.In_Polarity[2] := TRUE;

    // Call output group control routine
    Output_Control_0();

END_ACTION

And IO mapping is:

Cyclic#2.Pos120B::ValveA.Out AT %QX.BU120_A32.DigitalOutput04; (*Valve A - Clamp up*)
Cyclic#2.Pos120B::ValveB.Out AT %QX.BU120_A32.DigitalOutput03; (*Valve B - Clamp down*)

You can see, that there is clearly defined, when output/command/action is set regarding machine mode.
And doesnt matter it is physical output, or Command(in our case at mpAxis).

And error at @marcusbnr example is exactly the case, what declarative aproach will avoid :slight_smile:

The declarative approach guarantees that the same state always produces the same outputs, regardless of the path the automaton took to get there. In the imperative approach, it may matter whether the state was entered from one branch or another — because the previous state may have left the statement at TRUE.

When all commands are generated in one place, it is easy to add:

  • log “DesiredCmd” vs. “EffectiveCmd” (what the logic wanted vs. what went through the interlocks)
  • counters for how long the state has been active
  • audit trail of reasons (why something didn’t activate)
  • Mapping becomes a truth table (better review and tests)
  • during code review, the table “State → Commands” is checked (and not scattered actions)
  • unit tests can be table-based: for each state, verify the expected commands without simulating transitions
  • you can easily search for “in which states can MoveAbsolute be TRUE?” – the answer is in one block

This is often more valuable than being able to “quickly force something” - because you are diagnosing systemically, not ad-hoc.

I agree, neither you nor I program for agents. However, Agent is like a colleague. He should support you in your work, but if you explain it to him badly or incorrectly, it is absolutely 100% SISO → Sht In Sit Out.

When I think back to the first version of IMM SW around 2005, it was a nightmare. :slight_smile: The declarative approach at that time could solve almost all the problems that a given design had in the past and that the programmer had to solve at field.

better is to keep a bug for discussion :grinning_face:

@Vratislav good idea. I’ve reverted my edit so that the rest of the thread makes more sense to anyone who finds it later.

1 Like

I also prefer the declarative approach and if there is a need for a long OR statement, I break it up to multiple lines.

MyFub.FirstBool := state = THIS_STATE
                OR state = THIS_OTHER_STATE
                OR state = THIS_THIRD_STATE
                ...
                OR state = THIS_NTH_STATE;

I prefer the readability and automatic reset of commands from this approach.

1 Like

I prefer code that is only executed when necessary. I don’t like executing code in every cycle.

If I need to know where something is set, I prefer highlighting in the editor and using shortcuts (which B&R doesn’t support as far as I know), like Ctrl+Shift+F in TIA Portal to jump to the next location.

→ This is an IDE issue, not a programming style problem.

→ With a suitable IDE (hopefully AS Code), the elements are highlighted, so you can quickly see where something is set.

1 Like

Probably not exactly what you mean by shortcuts, but you can use bookmarks which allows you to jump between previously bookmarked lines in the code:

image

Personally, I too prefer to only execute code cyclically where it is necessary. So I have states where the “Enable” or “Execute” commands are only set for as many cycles as required, i.e. until getting a response from the function block. I find this approach more logical and easier to read. I understand the advantage of the other approach, just don’t find it as elegant.

Perhaps a bit off-topic, but I always build a massive generic “wrapper” state machine / motion task that contains everything and can be reused for every axis. This is then usually in a library so the customer can reuse it for every project. Bugs are usually caught early on.

1 Like

Looking forward to how ST-OOP will reshuffle the cards on this topic…

1 Like