Motor da FSM: transições, deadlines, retentativas e estado seguro
Agora a gente completa a parte principal: o “motor” que faz a FSM funcionar. Vou manter em C puro, com código legível e bem comentado, e já embutindo a lógica de ciclo (rodar continuamente) e recuperação (timeout → erro → retentativa → safe).
A estratégia:
- Cada iteração a SM recebe um evento (ou cria um
EVT_TICKse não houver). - Se estivermos em
ST_WAIT_IO, a SM checa se o deadline expirou. Se sim, cria umEVT_TIMEOUT. - O
switch(ctx.st)processa o evento e decide próximo estado. - Transições que precisam “fazer algo” (ex.: enviar IOREQ, armar deadline) fazem isso explicitamente.
Abaixo vai o bloco que substitui o “placeholder” dentro de StateMachineTask().
/* =========================
* Helpers de transição
* ========================= */
/**
* @brief Entra em um novo estado (ponto único para ações de entrada).
* Em firmware real você pode registrar log, métricas, etc.
*/
static void sm_enter_state(SmContext *ctx, SmState next) {
ctx->st = next;
}
/**
* @brief Arma um deadline para o estado ST_WAIT_IO (timeout relativo).
*/
static void sm_arm_io_deadline(SmContext *ctx, uint32_t timeout_ms) {
TickType_t now = xTaskGetTickCount();
ctx->io_deadline_tick = now + pdMS_TO_TICKS(timeout_ms);
}
/**
* @brief Verifica se o deadline de I/O expirou.
*/
static bool sm_io_deadline_expired(const SmContext *ctx) {
if (ctx->io_deadline_tick == 0) return false;
/* Comparação segura para overflow de ticks: diferença em signed */
TickType_t now = xTaskGetTickCount();
return ((int32_t)(now - ctx->io_deadline_tick) >= 0);
}
/**
* @brief Envia uma requisição de leitura para a IO task e atualiza o contexto.
*/
static void sm_request_io_read(SmContext *ctx) {
ctx->pending_req_id++;
IoRequest req = {
.type = IOREQ_READ_SAMPLE,
.req_id = ctx->pending_req_id
};
/* Em produção, trate falha de fila cheia como erro recuperável também. */
(void)io_send_req(&req, pdMS_TO_TICKS(5));
/* Arma deadline para aguardar resposta */
sm_arm_io_deadline(ctx, IO_DEADLINE_MS);
}
/**
* @brief Faz checagens de sanidade nos dados recebidos.
* Retorna false se os dados forem incoerentes.
*/
static bool sm_validate_io_payload(const SmEvent *ev) {
if (ev->type != EVT_IO_DONE) return false;
if (ev->u.io.len < 3) return false;
/* Exemplo didático de framing: AA ... 55 */
if (ev->u.io.data[0] != 0xAA) return false;
if (ev->u.io.data[ev->u.io.len - 1] != 0x55) return false;
return true;
}
/**
* @brief Emite erro no contexto e vai para ST_ERROR.
*/
static void sm_raise_error(SmContext *ctx, SmError err) {
ctx->last_error = err;
sm_enter_state(ctx, ST_ERROR);
}
/* =========================
* (Opcional) Backoff simples
* ========================= */
static void sm_backoff_delay(uint32_t retry_count) {
/* 20ms, 40ms, 80ms... limitado para não “sumir” do sistema */
uint32_t ms = 20u << (retry_count > 4 ? 4 : retry_count);
vTaskDelay(pdMS_TO_TICKS(ms));
}
/* =========================================================
* Motor da FSM (coloque dentro de StateMachineTask loop)
* ========================================================= */
static void sm_step(SmContext *ctx, SmEvent *ev) {
/* Timeout interno: se estamos esperando IO e expirou, substitui evento por TIMEOUT */
if (ctx->st == ST_WAIT_IO && sm_io_deadline_expired(ctx)) {
ev->type = EVT_TIMEOUT;
ev->timestamp_ms = now_ms();
ev->u.fault.cause = ERR_TIMEOUT;
}
switch (ctx->st) {
case ST_BOOT:
if (ev->type == EVT_START || ev->type == EVT_RESET_LOGIC) {
ctx->retry_count = 0;
ctx->cycle_count = 0;
ctx->last_error = ERR_NONE;
ctx->io_deadline_tick = 0;
sm_enter_state(ctx, ST_INIT);
}
break;
case ST_INIT:
/* Aqui seria inicialização real de periféricos, buffers, etc. */
if (ev->type == EVT_TICK) {
/* Sucesso na init (simulado) */
sm_enter_state(ctx, ST_IDLE);
}
break;
case ST_IDLE:
if (ev->type == EVT_START || ev->type == EVT_TICK) {
/* Neste exemplo, a operação é cíclica: a cada tick, inicia um ciclo. */
sm_enter_state(ctx, ST_CYCLE_START);
}
break;
case ST_CYCLE_START:
/* Ação de entrada do ciclo: incrementa contagem e pede IO */
ctx->cycle_count++;
sm_request_io_read(ctx);
sm_enter_state(ctx, ST_WAIT_IO);
break;
case ST_WAIT_IO:
if (ev->type == EVT_IO_DONE) {
/* Recebemos resposta: desarma deadline */
ctx->io_deadline_tick = 0;
/* Vai para PROCESS para validar e usar dados */
sm_enter_state(ctx, ST_PROCESS);
} else if (ev->type == EVT_IO_FAIL) {
ctx->io_deadline_tick = 0;
sm_raise_error(ctx, ERR_IO_FAIL);
} else if (ev->type == EVT_TIMEOUT) {
/* Timeout interno declarando falha */
ctx->io_deadline_tick = 0;
sm_raise_error(ctx, ERR_TIMEOUT);
} else {
/* Ignora outros eventos enquanto espera */
}
break;
case ST_PROCESS:
if (ev->type == EVT_IO_DONE) {
/* PROCESS espera o IO_DONE “que nos trouxe aqui”.
* Mas perceba: quando mudamos para ST_PROCESS,
* o evento atual ainda é EVT_IO_DONE, então podemos processar já.
*/
if (!sm_validate_io_payload(ev)) {
sm_raise_error(ctx, ERR_BAD_DATA);
break;
}
/* Sucesso: zera retentativas e volta ao ciclo (ou IDLE) */
ctx->retry_count = 0;
/* Ciclo contínuo: volta para IDLE (que por tick vai iniciar novo ciclo) */
sm_enter_state(ctx, ST_IDLE);
} else {
/* Se por algum motivo chegar evento inesperado, mantenha previsível */
sm_enter_state(ctx, ST_IDLE);
}
break;
case ST_ERROR:
/* Centraliza a decisão: retentar? reinit? safe? */
if (ctx->retry_count < MAX_RETRIES) {
sm_enter_state(ctx, ST_RECOVER);
} else {
sm_enter_state(ctx, ST_SAFE);
}
break;
case ST_RECOVER:
/* Recuperação: pode ser reset de barramento, reinit parcial, etc. */
ctx->retry_count++;
/* Backoff para não “martelar” o periférico */
sm_backoff_delay(ctx->retry_count);
/* Exemplo: pedir para IO resetar o “bus” (simulado) */
{
IoRequest r = {.type = IOREQ_RESET_BUS, .req_id = ctx->pending_req_id + 1};
(void)io_send_req(&r, pdMS_TO_TICKS(5));
}
/* Depois da recuperação, tenta novamente um ciclo */
sm_enter_state(ctx, ST_CYCLE_START);
break;
case ST_SAFE:
/* Estado seguro: não opera normalmente. Aguarda intervenção/condição. */
if (ev->type == EVT_FAULT_CLEAR || ev->type == EVT_RESET_LOGIC) {
sm_enter_state(ctx, ST_BOOT);
}
break;
default:
/* Estado inválido -> falha controlada */
sm_enter_state(ctx, ST_SAFE);
break;
}
}
E agora você só precisa chamar sm_step(&ctx, &ev); dentro do loop da StateMachineTask(), substituindo o placeholder. Fica assim:
/* Dentro do loop da StateMachineTask() */
for (;;) {
SmEvent ev = {0};
if (xQueueReceive(smQueue, &ev, pdMS_TO_TICKS(SM_EVENT_WAIT_MS)) != pdPASS) {
ev.type = EVT_TICK;
ev.timestamp_ms = now_ms();
}
sm_step(&ctx, &ev);
}
O que foi garantido aqui (na prática)
- Cíclico:
ST_IDLE -> ST_CYCLE_START -> ST_WAIT_IO -> ST_PROCESS -> ST_IDLE ... - Recuperável:
WAIT_IOexpira ou falha →ST_ERROR→ST_RECOVER(com backoff) → tenta de novo. - Falha controlada: após
MAX_RETRIES, entra emST_SAFE. - Sem “bloqueio sujo”: a SM não chama IO e fica travada; ela governa por eventos.