*Always*. They act as a sort of "application specific language" to codify
behavior in a high level form.
I always implement them with an interpreter that takes a "current state"
and some number of "input variables", along with a set of tables defining
the transitions from each potential "current state" to other "next states"
(along with "action routines" to be executed when the transition is
made.
The manner in which the tables are encode varies based on what I expect
the needs of the machine being modeled to be.
For example, to accept a decimal value from a "digit source" (which could be
a keypad or a character stream received from a UART) I might code a little
machine:
STATE IDLE
; prompt and default value are displayed, here, and the FIRST digit is expected
CASE "CLEAR" GOTO IDLE EXECUTING no_op()
CASE '0' GOTO ENTRY EXECUTING first_digit()
CASE '1' GOTO ENTRY EXECUTING first_digit()
...
CASE '9' GOTO ENTRY EXECUTING first_digit()
CASE "ENTER" GOTO CHECK EXECUTING completed()
ORELSE GOTO same EXECUTING complain()
STATE ENTRY
; first digit has been entered, prompt and default value have been removed
CASE "CLEAR" GOTO IDLE EXECUTING restore_default()
CASE '0' GoTO DIGITS EXECUTING accept_digit()
CASE '1' GOTO DIGITS EXECUTING accept_digit()
...
no_op()
return
first_digit()
accumulator() = digit;
remove_prompt();
printf("%s: %d", shortened_prompt, accumulator)
return
accept_digit()
accumlator = 10 * accumulator + digit;
printf("%s: %d", shortened_prompt, accumulator)
return
complain()
beep()
return
restore_default()
accumulator = default_value;
printf("%s: %d", long_prompt, accumulator);
return
...
Obviously, another encoding might support a syntax like:
STATE ENTRY
; first digit has been entered, prompt and default value have been removed
CASE "CLEAR" GOTO IDLE EXECUTING no_op()
CASE '0' THRU '9' GOTO ENTRY EXECUTING first_digit()
...
CASE "ENTER" GOTO CHECK EXECUTING completed()
ORELSE GOTO same EXECUTING complain()
How you acquire "inputs" is also variable. You can force each input to appear
in a particular "variable" that the FSM interpreter polls (different variables
for different FSM's -- one interpreteer can handle multiple FSM's).
I prefer having a LIST of "input variables" that the FSM interpreter examines
sequentially. Individual "tasks" can then maintain their own "variables"
without having to coordinate a SHARED variable among many competing tasks.
E.g., the "keypad" task can have a "key_detected" variable that is LISTED
in those variables consumed by the above FSM. The example suggests this
variable would be a char -- though there is no requirement that it be so.
The task responsible for polling and debouncing keystrokes from the keypad
can watch the "key_detected" variable and, any time it is found to be
"empty" (NUL, by convention), can place the next enqueued keystroke into
that variable. It can then continue scanning/debouncing the keypad
while queuing subsequent keystrokes until the "key_detected" variable
is "not busy".
The FSM interpreter can then, by convention, CLEAR any variable that it
has acted upon to remove the stimulus from subsequent FSM iterations
AND "signal" the producer task that the consumer (the FSM!) has processed
the previous "event/datum" and is ready for another FROM THAT DATA SOURCE.
[This opens up lots of hacks -- like allowing a stimulus to be propagated
to the next state -- by NOT clearing it!]
Note that you can also deviate from conventional FSM implementations by
supporting "special" next state codes (e.g., "same" refers to the *current*
state -- don't transition to another state, just stay here!)
The number of variations in syntax and mechanisms is far too many to enumerate
here -- as are the encodings for the "state tables" illustrated above.
But, the point is, you can cleanly codify the behavior of an algorithm
without resorting to lots of spaghetti code and conditionals.