Step 2 is where architecture becomes visible. We introduce the simplest possible structure that can survive failure: a single, explicit state machine owned by the application.
Part A – English Version
Why Step 2 Exists
After Step 1, we accepted a critical truth:
A device is not a script. It is a system that runs forever and must survive failure.
Step 2 answers the next obvious question:
Where does the system decide what to do next?
If you do not answer this explicitly, the answer becomes:
- callbacks
- timers
- globals
- side effects
That is how firmware becomes unreviewable.
The Core Rule of Step 2
There must be exactly one place that decides application control flow.
Not:
- drivers
- callbacks
- interrupts
- background threads
One place.
We will call it the application state machine.
Why a State Machine (and Not “Just a Loop”)
Many engineers resist state machines because they imagine:
- complex diagrams
- switch-case monsters
- unreadable code
But the alternative is worse.
Without an explicit state machine:
- state still exists
- transitions still happen
- but nobody owns them
A state machine does not add complexity. It reveals complexity that already exists.
Defining Explicit States
We start with states that represent lifecycle, not features:
enum app_state {
APP_STATE_BOOT,
APP_STATE_SENSOR_INIT,
APP_STATE_NET_INIT,
APP_STATE_IDLE,
APP_STATE_SAMPLE,
APP_STATE_SEND,
APP_STATE_WAIT,
APP_STATE_ERROR,
};
Notice what is missing:
- no “RETRY” state
- no “WIFI_FAIL” state
- no “HTTP_TIMEOUT” state
States represent what the system is doing, not why it failed.
The Application Context (Ownership Made Concrete)
All mutable application state lives in one struct:
struct app_ctx {
enum app_state state;
/* bookkeeping */
uint32_t retry_count;
int last_error;
/* placeholders for future subsystems */
void *sensor;
void *net;
};
This struct is:
- owned by the application
- passed explicitly
- never global by accident
If a variable does not fit here, it probably does not belong.
The Coordinator Function
This is the heart of the system:
static void app_run(struct app_ctx *ctx)
{
switch (ctx->state) {
case APP_STATE_BOOT:
ctx->retry_count = 0;
ctx->state = APP_STATE_SENSOR_INIT;
break;
case APP_STATE_SENSOR_INIT:
ctx->state = APP_STATE_NET_INIT;
break;
case APP_STATE_NET_INIT:
ctx->state = APP_STATE_IDLE;
break;
case APP_STATE_IDLE:
ctx->state = APP_STATE_SAMPLE;
break;
case APP_STATE_SAMPLE:
ctx->state = APP_STATE_SEND;
break;
case APP_STATE_SEND:
ctx->state = APP_STATE_WAIT;
break;
case APP_STATE_WAIT:
ctx->state = APP_STATE_IDLE;
break;
case APP_STATE_ERROR:
ctx->retry_count++;
ctx->state = APP_STATE_WAIT;
break;
}
}
Nothing interesting happens yet — and that is perfect.
Why This Function Is Intentionally Boring
This function:
- does not block
- does not sleep
- does not retry
- does not log
It only:
- looks at state
- decides the next state
This boredom is discipline.
The Main Loop (Yes, Still while(1))
The main loop remains simple:
void main(void)
{
static struct app_ctx app = {
.state = APP_STATE_BOOT,
};
while (1) {
app_run(&app);
k_sleep(K_MSEC(50));
}
}
Key observation:
while(1)is not the problem. What you put inside it is.
Why We Avoid Multiple Threads
At this stage, one thread is a feature:
- no races
- no locks
- no priority bugs
- easier debugging
Concurrency is added later only if required.
ERROR Is a State, Not a Side Effect
Notice:
case APP_STATE_ERROR:
ctx->retry_count++;
ctx->state = APP_STATE_WAIT;
break;
ERROR:
- is explicit
- is visible
- is unavoidable
We do not “handle” errors inline. We transition to them.
What Step 2 Deliberately Does NOT Do
Step 2 does not yet:
- talk to hardware
- talk to network
- retry intelligently
- sleep intelligently
This is intentional.
We are building a skeleton that will not change later.
A Reviewer’s Perspective
A senior reviewer looking at this code can immediately answer:
- Where is control flow decided? ✔
- Where is state stored? ✔
- Where do retries live? ✔ (not yet, but planned)
- Can this grow without chaos? ✔
That is the real goal.
Final Thought (English)
If you cannot point to the one place that decides behavior, you do not control your firmware.
Step 2 gives you that control.
Part B – Phiên bản tiếng Việt
Vì sao cần Step 2
Sau Step 1, chúng ta đã chấp nhận:
Thiết bị không phải script, mà là hệ thống phải sống sót.
Câu hỏi tiếp theo là:
Ai quyết định hệ thống sẽ làm gì tiếp theo?
Nếu bạn không chỉ ra rõ ràng, câu trả lời sẽ là:
- callback
- timer
- biến global
- side effect
Và đó là con đường dẫn đến firmware rối.
Quy tắc cốt lõi của Step 2
Chỉ có một nơi duy nhất quyết định luồng điều khiển của ứng dụng.
Không phải driver. Không phải callback.
Một nơi duy nhất.
Vì sao dùng state machine
Không có state machine không có nghĩa là không có state.
Nó chỉ có nghĩa là:
- state bị ẩn
- transition không ai chịu trách nhiệm
State machine không làm code phức tạp hơn. Nó làm mọi thứ rõ ràng hơn.
Định nghĩa state rõ ràng
enum app_state {
APP_STATE_BOOT,
APP_STATE_SENSOR_INIT,
APP_STATE_NET_INIT,
APP_STATE_IDLE,
APP_STATE_SAMPLE,
APP_STATE_SEND,
APP_STATE_WAIT,
APP_STATE_ERROR,
};
State mô tả:
- hệ thống đang làm gì
Không mô tả:
- lỗi gì xảy ra
app_ctx – ownership cụ thể hóa
struct app_ctx {
enum app_state state;
uint32_t retry_count;
int last_error;
void *sensor;
void *net;
};
Mọi state mutable đều nằm ở đây.
Không có magic.
Hàm điều phối trung tâm
static void app_run(struct app_ctx *ctx)
{
switch (ctx->state) {
case APP_STATE_BOOT:
ctx->retry_count = 0;
ctx->state = APP_STATE_SENSOR_INIT;
break;
/* ... */
}
}
Hàm này:
- không sleep
- không block
- không retry
Chỉ quyết định state tiếp theo.
Vòng lặp main
while (1) {
app_run(&app);
k_sleep(K_MSEC(50));
}
while(1) không xấu.
Logic bên trong mới là vấn đề.
ERROR là state, không phải hack
ERROR là một phần của thiết kế.
Không phải trường hợp đặc biệt.
Step 2 CHƯA làm gì
Chưa:
- giao tiếp hardware
- retry
- quản lý năng lượng
Và đó là chủ ý.
Góc nhìn reviewer
Reviewer có thể đọc và hiểu ngay:
- luồng điều khiển ở đâu
- state nằm ở đâu
- hệ thống có mở rộng được không
Lời kết (Tiếng Việt)
Nếu bạn không chỉ ra được nơi quyết định hành vi, firmware không còn nằm trong tay bạn.
Step 2 trao lại quyền kiểm soát đó.