How Event‑Driven State Machines Power Embedded Systems: From ISR to Message Queues
This article explains the principles of event‑driven programming and state‑machine design for microcontrollers, compares them with interrupt handling, introduces a message‑based architecture with concrete data structures and APIs, and shows how to build a reusable framework that solves ordering and loss problems in embedded applications.
Concept of Event‑Driven Programming
Event‑driven programming treats external stimuli as events that wake a dormant object, similar to how a microcontroller interrupt awakens a routine. The article starts with everyday analogies (e.g., sneaking a nap during study time) to illustrate the idea of detecting and responding to events without being constantly active.
Event‑Driven vs. Microcontroller Interrupts
The author maps the event‑driven model onto microcontroller interrupt mechanisms, noting that both involve a dormant state, external detection, and a wake‑up routine that processes the event. The similarity is highlighted by comparing the event‑driven flag group g_u8EvntFlgGrp with interrupt flags.
Basic ISR‑Based Event Handling (List 9)
#define FLG_UART 0x01
#define FLG_TMR 0x02
#define FLG_EXI 0x04
#define FLG_KEY 0x08
volatile INT8U g_u8EvntFlgGrp = 0; /* event flag group */
void main(void) {
INT8U u8FlgTmp = 0;
sys_init();
while (1) {
u8FlgTmp = read_envt_flg_grp();
if (u8FlgTmp) {
if (u8FlgTmp & FLG_UART) action_uart();
if (u8FlgTmp & FLG_TMR) action_tmr();
if (u8FlgTmp & FLG_EXI) action_exi();
if (u8FlgTmp & FLG_KEY) action_key();
} else {
; /* idle code */
}
}
}
INT8U read_envt_flg_grp(void) {
INT8U tmp = 0;
gbl_int_disable();
tmp = g_u8EvntFlgGrp;
g_u8EvntFlgGrp = 0;
gbl_int_enable();
return tmp;
}This flag‑based approach works but has two major drawbacks: (1) when multiple events fire close together, the order of occurrence is lost; (2) repeated occurrences of the same event can be missed because the flag is simply set, not counted.
Message‑Based Event Handling
To solve the ordering and loss problems, the article proposes converting each event into a message and storing it in a FIFO message queue. A message is a memory block that carries a type identifier and optional payload, making the event’s data explicit and preserving the exact sequence of occurrence.
Message Data Structures (List 10)
typedef union msg_arg {
INT8U u8Arg; /* 8‑bit unsigned */
INT8U s8Arg; /* 8‑bit signed */
#if CFG_MSG_ARG_INT16_EN>0
INT16U u16Arg;
INT16S s16Arg;
#endif
#if CFG_MSG_ARG_INT32_EN>0
INT32U u32Arg;
INT32S s32Arg;
#endif
#if CFG_MSG_ARG_FP32_EN>0
FP32 f32Arg; /* 32‑bit float */
#endif
#if CFG_MSG_ARG_PTR_EN>0
void *pArg; /* generic pointer */
#endif
} MSG_ARG;
typedef struct _msg {
INT8U u8MsgID; /* message type */
#if CFG_MSG_USR_SUM>1
INT8U u8UsrID; /* consumer ID */
#endif
MSG_ARG uMsgArg; /* payload */
} MSG;
typedef struct msg_box {
INT8U u8MBLock; /* lock flag */
INT8U u8MsgSum; /* current length */
INT8U u8MQHead; /* head index */
INT8U u8MQTail; /* tail index */
MSG arMsgBox[CFG_MSG_SUM_MAX];
} MB;
static MB g_stMsgUnit; /* global message buffer */The union MSG_ARG can store various data types (8‑ to 32‑bit integers, floats, pointers) while keeping memory usage configurable via compile‑time flags. The msg_box implements a circular buffer, with head/tail indices and a lock to protect concurrent ISR/main‑thread access.
Message Queue API
void mq_init(void)– initialise the queue. void mq_clear(void) – empty the queue. void mq_lock(void) / void mq_unlock(void) – protect concurrent access. BOOL mq_is_empty(void) – test emptiness. INT8U mq_get_msg_cur_sum(void) – current message count. INT8U mq_get_msg_sum_max(void) – maximum capacity. INT8U mq_msg_post_fifo(MSG *pMsg) – ISR‑side post (re‑entrant). INT8U mq_msg_req_fifo(MSG *pMsg) – main‑thread retrieval.
ISR code posts a message after setting the appropriate flag, and the main loop consumes messages in FIFO order, guaranteeing correct chronological processing.
State‑Machine Engine (SME) and GF1.0 Framework
The SME repeatedly pulls messages from the queue and dispatches them to the appropriate state‑machine node. If the queue is empty, an idle task runs (e.g., watchdog feeding). The core loop looks like:
void sme_kernel(void) {
while (1) {
if (!mq_is_empty()) {
mq_msg_req_fifo(&stMsgTmp);
u8CurStat = get_cur_state();
stNodeTmp = g_arFsmDrvTbl[u8CurStat];
if (stNodeTmp.u8StatChk == u8CurStat) {
u8CurStat = stNodeTmp.fpAction(&stMsgTmp);
set_cur_state(u8CurStat);
} else {
state_crash(u8CurStat);
}
} else {
idle_task();
}
}
}This engine, together with the message queue, forms the "GF1.0" bare‑metal framework: multiple ISRs → message buffer → single main state machine.
Serial Port Example
The article demonstrates how to apply the framework to a UART driver. Two designs are compared:
ISR + message per byte (simple but high overhead and possible overflow).
ISR + receive buffer + message after a full frame (efficient, preserves ordering, and isolates low‑level handling).
Frames are described with a typical structure (header, address, length, payload, checksum). The ISR implements a small state machine to assemble a frame, lock the buffer, and post a MSG containing the completed frame to the queue. The main state machine then validates the checksum and processes the payload.
Broader Applications
Beyond UART, the same pattern can be used for single‑wire protocols, software‑implemented I²C, multiplexed 7‑segment displays, keyboard scanning, and any peripheral where deterministic, ordered handling of asynchronous events is required.
Conclusion
The GF1.0 framework demonstrates that a clean separation of concerns—ISR as fast producers, a FIFO message buffer as a temporal bridge, and a single application‑level state machine as the consumer—yields a robust, maintainable architecture for embedded firmware. It resolves the two key shortcomings of flag‑based designs (lost order and missed duplicate events) while keeping ISR latency minimal.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
