Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/endurodave/c_statemachinewiththreads
C Language State Machine with Threads
https://github.com/endurodave/c_statemachinewiththreads
asynchronous-callbacks c c-language cross-platform multithreading state-machine threads
Last synced: about 2 months ago
JSON representation
C Language State Machine with Threads
- Host: GitHub
- URL: https://github.com/endurodave/c_statemachinewiththreads
- Owner: endurodave
- License: mit
- Created: 2019-03-02T18:24:56.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2024-11-13T14:23:31.000Z (about 2 months ago)
- Last Synced: 2024-11-13T15:27:51.123Z (about 2 months ago)
- Topics: asynchronous-callbacks, c, c-language, cross-platform, multithreading, state-machine, threads
- Language: C
- Homepage:
- Size: 229 KB
- Stars: 2
- Watchers: 0
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# C Language State Machine with Threads
A framework combining C language state machines and multicast asynchronous callbacks.# Table of Contents
- [C Language State Machine with Threads](#c-language-state-machine-with-threads)
- [Table of Contents](#table-of-contents)
- [Introduction](#introduction)
- [Project Build](#project-build)
- [Windows Visual Studio](#windows-visual-studio)
- [Linux Make](#linux-make)
- [SelfTestEngine](#selftestengine)
- [CentrifugeTest](#centrifugetest)
- [Timer](#timer)
- [Run-Time](#run-time)
- [Conclusion](#conclusion)# Introduction
A software-based Finite State Machines (FSM) is an implementation method used to decompose a design into states and events. Simple embedded devices with no operating system employ single threading such that the state machines run on a single “thread”. More complex systems use multithreading to divvy up the processing.
Many FSM implementations exist including one I wrote about here entitled “State Machine Design in C”. The article covers how to create C state machines. What is missing, however, is how to integrate multiple state machines into the context of a multithreaded environment.
“Asynchronous Callbacks in C” is another article I wrote. This design provides a simple, portable callback mechanism that handles the low-level details of asynchronously invoking a callback with event data on a client-specified thread of control.
This article combines the two previously described techniques, state machines and asynchronous callbacks, into a single project. In the previous articles, it may not be readily apparent using simple examples how multiple state machines coordinate activities and dispatch events to each other. The goal for the article is to provide a complete working project with threads, timers, events, and state machines all working together. To illustrate the concept, the example project implements a state-based self-test engine utilizing asynchronous communication between threads.
The primary focus is on how to combine the state machine and asynchronous callbacks into a single framework.
A CMake build offers easy experimentation in Windows, Linux and other platforms. No platform specific API's are utilized so any compiler toolchain will suffice.
Related articles:
-
State Machine Design in C - by David Lafreniere -
Asynchronous Callbacks in C - by David Lafreniere -
A Fixed Block Allocator in C - by David Lafreniere -
State Machine Design in C++ - by David Lafreniere
# Project Build
CMake is used to create the build files. CMake is free and open-source software. Windows, Linux and other toolchains are supported. Example CMake console commands executed inside the project root directory:
## Windows Visual Studio
cmake -G "Visual Studio 17 2022" -A Win32 -B ../C_StateMachineWithThreadsBuild -S .
After executed, open the Visual Studio project from within the C_StateMachineWithThreadsBuild
directory.
## Linux Make
cmake -G "Unix Makefiles" -B ../C_StateMachineWithThreadsBuild -S .
After executed, build the software from within the C_StateMachineWithThreadsBuild directory using the command make
. Run the console app using ./C_StateMachineWithThreadsApp
.
# SelfTestEngine
SelfTestEngine
is thread-safe and the main point of contact for client’s utilizing the self-test subsystem. CentrifugeTest
and PressureTest
are members of SelfTestEngine
. SelfTestEngine
is responsible for sequencing the individual self-tests in the correct order as shown in the state diagram below.
Figure 2: SelfTestEngine State Machine
The STE_Start
event initiates the self-test engine.
// Start external event
EVENT_DEFINE(STE_Start, NoEventData)
{
// Given the Start event, transition to a new state based upon
// the current state of the state machine
BEGIN_TRANSITION_MAP // - Current State -
TRANSITION_MAP_ENTRY(ST_START_CENTRIFUGE_TEST) // ST_Idle
TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Completed
TRANSITION_MAP_ENTRY(CANNOT_HAPPEN) // ST_Failed
TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_StartCentrifugeTest
TRANSITION_MAP_ENTRY(EVENT_IGNORED) // ST_StartPressureTest
END_TRANSITION_MAP(SelfTestEngine, pEventData)
}
SelfTestEngine
has one public event function, STE_Start()
, that starts the self-tests. STE_StatusCb
is an asynchronous callback allowing client’s to register for status updates during testing.
#include "DataTypes.h"
#include "StateMachine.h"
#include "callback.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
BOOL testActive;
} SelfTestStatus;
// Declare public callback interfaces
CB_DECLARE(STE_StatusCb, const SelfTestStatus*)
CB_DECLARE(STE_CompletedCb, void*)
CB_DECLARE(STE_FailedCb, void*)
// Declare the private instance of SelfTestEngine state machine
SM_DECLARE(SelfTestEngineSM)
// State machine event functions
EVENT_DECLARE(STE_Start, NoEventData)
EVENT_DECLARE(STE_Cancel, NoEventData)
void STE_Init();
As mentioned previously, the SelfTestEngine
registers for asynchronous callbacks from each sub self-tests as shown below.
void STE_Init()
{
// Register with CentrifugeTest and PressureTest state machines
CB_Register(CFG_CompletedCb, STE_CompletedCallback, DispatchCallbackThread1, NULL);
CB_Register(CFG_FailedCb, STE_FailedCallback, DispatchCallbackThread1, NULL);
CB_Register(PRE_CompletedCb, STE_CompletedCallback, DispatchCallbackThread1, NULL);
CB_Register(PRE_FailedCb, STE_FailedCallback, DispatchCallbackThread1, NULL);
}
One might ask why the state machines use asynchronous callbacks. If the state machines are on the same thread, why not use a normal, synchronous callback instead? The problem to prevent is a callback into a currently executing state machine, that is, the call stack wrapping back around into the same class instance. For example, the following call sequence should be prevented: SelfTestEngine
calls CentrifugeTest
calls back SelfTestEngine
. An asynchronous callback allows the stack to unwind and prevents this unwanted behavior.
# CentrifugeTest
The CentrifugeTest
state machine diagram show below implements the centrifuge self-test described in "State Machine Design in C". The difference here is that the Timer
class is used to provide Poll
events via asynchronous callbacks.
Figure 3: CentrifugeTest State Machine
# Timer
The Timer
class provides a common mechanism to receive function callbacks by registering with Expired
. TMR_Start()
starts the callbacks at a particular interval. TMR_Stop()
stops the callbacks.
void TMR_Init();
void TMR_Term();
BOOL TMR_Start(CB_CallbackFuncType cbFunc, CB_DispatchCallbackFuncType cbDispatchFunc, DWORD timeout);
void TMR_Stop(CB_CallbackFuncType cbFunc, CB_DispatchCallbackFuncType cbDispatchFunc);
// Called periodically by a thread to process all timers
void TMR_ProcessTimers();
# Run-Time
The program’s main()
function is shown below. It creates the threads, registers for callbacks from SelfTestEngine
, then calls STE_Start()
to start the self-tests.
int main()
{
// Initialize modules
ALLOC_Init();
TMR_Init();
CB_Init();
STE_Init();
CreateThreads();
// Register for SelfTestEngine callbacks on DispatchCallbackThread2
CB_Register(STE_StatusCb, STE_StatusCallback, DispatchCallbackThread2, NULL);
CB_Register(STE_CompletedCb, STE_CompletedCallback, DispatchCallbackThread2, NULL);
CB_Register(STE_FailedCb, STE_FailedCallback, DispatchCallbackThread2, NULL);
// Start SelfTestEngine
SM_Event(SelfTestEngineSM, STE_Start, NULL);
// Wait for SelfTestEngine to complete
while (!selfTestEngineCompleted)
this_thread::sleep_for(1s);
// Cleanup before exit
ExitThreads();
CB_Term();
TMR_Term();
ALLOC_Term();
this_thread::sleep_for(1s);
return 0;
}
# Conclusion
The C state machine and asynchronous callbacks implementations can be used separately. Each is useful unto itself. However, combining the two offers a novel framework for multithreaded state-driven application development. The article has shown how to coordinate the behavior of state machines when multiple threads are used, which may not be entirely obvious when looking at simplistic, single threaded examples.
I’ve successfully used ideas similar to this on many different PC and embedded projects. The code is portable to any platform with a small amount of effort. I particularly like idea of asynchronous callbacks because it effectively hides inter-thread communication and the organization of the state machines makes creating and maintaining self-tests easy.