3、SECS/GEM裸机实现分析:消息队列的轮询处理、单线程下的HSMS连接管理、裸机下的超时处理机制
在裸机(bare-metal)环境中,SECS/GEM协议栈的实现面临资源极度受限的挑战:无操作系统、无多线程、无内核定时器服务。本章深入分析在单线程、无RTOS条件下,如何通过消息队列轮询、状态机驱动的HSMS连接管理以及软件定时器链来构建一个可工作的SECS/GEM通信节点。
核心认知:裸机SECS/GEM的本质是一个超级循环(Super Loop)驱动的有限状态机。所有协议层(HSMS、消息队列、超时管理)都在同一个线程内分时复用CPU,通过轮询和状态切换来模拟并发。
3.1 消息队列的轮询处理机制
裸机下没有阻塞式IPC(进程间通信),消息队列必须实现为无锁环形缓冲区,由主循环主动轮询。队列用于解耦HSMS底层接收与上层SECS消息解析。
3.1.1 环形消息队列数据结构
| 字段 | 类型 | 说明 |
|---|---|---|
buffer |
MsgNode_t[MQ_SIZE] |
消息节点数组,每个节点包含消息类型、长度、数据指针 |
head |
uint16_t |
生产者(中断/接收任务)写入位置 |
tail |
uint16_t |
消费者(主循环)读取位置 |
count |
uint16_t |
当前队列中待处理消息数 |
// 裸机消息队列核心结构
#define MQ_SIZE 32
typedef struct {
uint8_t msg_type; // 0:HSMS数据, 1:连接事件, 2:超时事件
uint8_t *data; // 指向消息负载
uint16_t length;
} MsgNode_t;
typedef struct {
MsgNode_t buffer[MQ_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
volatile uint16_t count;
} MQueue_t;
// 轮询处理函数——在主循环中调用
void MQ_Poll(MQueue_t *mq) {
while (mq->count > 0) {
MsgNode_t msg = mq->buffer[mq->tail];
// 根据msg_type分发处理
switch (msg.msg_type) {
case 0: // HSMS数据消息
SECS_ProcessMessage(msg.data, msg.length);
break;
case 1: // 连接状态变化
HSMS_StateMachine_Event(msg.data[0]);
break;
case 2: // 超时事件
Timer_HandleTimeout(msg.data[0]);
break;
}
// 更新tail(环形回绕)
mq->tail = (mq->tail + 1) % MQ_SIZE;
// 原子减count(裸机关中断保护)
__disable_irq();
mq->count--;
__enable_irq();
}
}
设计要点:裸机队列的
head和tail必须声明为volatile,防止编译器优化导致中断与主循环之间的数据不一致。count的增减操作需在关中断下执行,保证原子性。
3.1.2 轮询处理流程图
3.2 单线程下的HSMS连接管理
HSMS(High-Speed SECS Message Services)连接管理在裸机下必须实现为有限状态机(FSM)。主循环每次迭代调用状态机一次,根据当前状态和事件决定下一步动作。
3.2.1 HSMS连接状态定义
| 状态 | 枚举值 | 描述 |
|---|---|---|
HSMS_NOT_CONNECTED |
0 | TCP连接未建立,等待连接或监听 |
HSMS_CONNECTED |
1 | TCP已连接,等待Select/分离测试 |
HSMS_SELECTED |
2 | HSMS会话已建立,可收发SECS消息 |
HSMS_SELECT_PENDING |
3 | 已发送Select.req,等待Select.rsp |
HSMS_DESELECTED |
4 | 会话已分离,TCP连接仍存在 |
3.2.2 状态机实现核心代码
// HSMS状态机事件枚举
typedef enum {
EVT_TCP_CONNECTED, // TCP连接建立
EVT_TCP_DISCONNECTED, // TCP连接断开
EVT_SELECT_RSP_RCVD, // 收到Select.rsp
EVT_SELECT_REQ_RCVD, // 收到Select.req(被动方)
EVT_DESELECT_REQ_RCVD, // 收到Deselect.req
EVT_TIMEOUT_T3, // T3连接超时
EVT_TIMEOUT_T5, // T5分离超时
EVT_DATA_RCVD // 收到数据消息
} HSMS_Event_t;
// 状态机处理函数——每次主循环调用一次
void HSMS_StateMachine_Run(HSMS_Event_t event) {
switch (g_hsms_state) {
case HSMS_NOT_CONNECTED:
if (event == EVT_TCP_CONNECTED) {
// 发送Select.req,进入Select Pending
HSMS_SendSelectReq();
g_hsms_state = HSMS_SELECT_PENDING;
Timer_Start(T3_TIMER, T3_TIMEOUT_MS);
}
break;
case HSMS_SELECT_PENDING:
if (event == EVT_SELECT_RSP_RCVD) {
// 检查Select.rsp状态码
if (g_select_rsp_status == 0) {
g_hsms_state = HSMS_SELECTED;
Timer_Stop(T3_TIMER);
} else {
// 连接拒绝,关闭TCP
TCP_Close();
g_hsms_state = HSMS_NOT_CONNECTED;
}
} else if (event == EVT_TIMEOUT_T3) {
// T3超时,重试或断开
if (++g_t3_retry_count <= MAX_T3_RETRY) {
HSMS_SendSelectReq();
Timer_Start(T3_TIMER, T3_TIMEOUT_MS);
} else {
TCP_Close();
g_hsms_state = HSMS_NOT_CONNECTED;
}
}
break;
case HSMS_SELECTED:
if (event == EVT_TCP_DISCONNECTED) {
g_hsms_state = HSMS_NOT_CONNECTED;
} else if (event == EVT_DESELECT_REQ_RCVD) {
HSMS_SendDeselectRsp();
g_hsms_state = HSMS_DESELECTED;
Timer_Start(T5_TIMER, T5_TIMEOUT_MS);
}
// 正常数据收发在此状态下进行
break;
// 其他状态处理...
}
}
裸机陷阱:单线程下,状态机处理期间不能阻塞等待网络响应。所有网络收发必须设置为非阻塞模式,或使用DMA+中断完成。状态机只做状态迁移和命令发起,实际的TCP数据收发由中断服务程序(ISR)完成并放入消息队列。
3.2.3 HSMS状态机迁移图
3.3 裸机下的超时处理机制
SECS/GEM协议定义了多个超时定时器(T1~T8),裸机下没有RTOS的软件定时器服务,必须自行实现基于硬件定时器中断的软件定时器链。
3.3.1 软件定时器链设计
使用一个单向链表管理所有活跃定时器。每个定时器节点包含剩余时间、超时回调函数指针和标志位。硬件定时器(如SysTick)以1ms为周期产生中断,中断服务程序遍历链表,递减所有节点的计数值。
// 软件定时器节点
typedef struct TimerNode {
uint32_t remain_ms; // 剩余毫秒数
uint32_t reload_ms; // 重装载值(用于周期定时器)
void (*callback)(void *arg); // 超时回调
void *arg; // 回调参数
uint8_t is_periodic; // 是否周期定时器
uint8_t is_active; // 是否激活
struct TimerNode *next; // 链表指针
} TimerNode_t;
// 硬件定时器中断服务程序(1ms周期)
void SysTick_Handler(void) {
TimerNode_t *node = timer_list_head;
while (node) {
if (node->is_active) {
if (--node->remain_ms == 0) {
// 超时:将超时事件放入消息队列
MQ_PutEvent(EVENT_TIMEOUT, (uint8_t*)&node, sizeof(node));
if (node->is_periodic) {
node->remain_ms = node->reload_ms; // 重装载
} else {
node->is_active = 0; // 单次定时器自动停用
}
}
}
node = node->next;
}
}
3.3.2 SECS/GEM标准超时参数表
| 定时器 | 默认值 | 用途 | 裸机实现方式 |
|---|---|---|---|
| T1 | 1.0 s | 等待Reply超时 | 软件定时器链,精度1ms |
| T2 | 10.0 s | 等待通信超时 | 软件定时器链 |
| T3 | 45.0 s | HSMS Select超时 | 软件定时器链,支持重试 |
| T4 | 45.0 s | HSMS分离超时 | 软件定时器链 |
| T5 | 10.0 s | 连接响应超时 | 软件定时器链 |
| T6 | 5.0 s | 等待控制事务超时 | 软件定时器链 |
| T7 | 10.0 s | NOT_SELECTED状态超时 | 软件定时器链 |
| T8 | 5.0 s | 网络间隔超时 | 软件定时器链 |
3.3.3 超时处理与主循环的协作
超时事件不直接在中断中处理,而是通过消息队列传递给主循环。这种设计保证了中断服务程序保持短小,且超时处理与状态机在同一上下文执行,避免了竞态条件。
// 主循环中的超时事件处理
void Timer_HandleTimeout(uint8_t timer_id) {
switch (timer_id) {
case T3_TIMER:
// 通知HSMS状态机T3超时
HSMS_StateMachine_Run(EVT_TIMEOUT_T3);
break;
case T5_TIMER:
HSMS_StateMachine_Run(EVT_TIMEOUT_T5);
break;
case T1_TIMER:
// SECS消息Reply超时,重发或报错
SECS_RetransmitMessage();
break;
default:
break;
}
}
裸机超时设计黄金法则:中断只做“计数递减+事件入队”,主循环做“超时逻辑处理”。这确保了超时精度(1ms)不受主循环执行时间影响,同时避免了中断上下文调用复杂状态机带来的风险。
3.3.4 超时处理时序图
3.4 裸机实现的整体架构
将上述三个机制整合,裸机SECS/GEM协议栈的主循环结构如下:
// 裸机SECS/GEM主循环
int main(void) {
// 初始化硬件:UART/SPI、以太网MAC、SysTick定时器
HW_Init();
// 初始化协议栈模块
MQ_Init(); // 消息队列初始化
HSMS_Init(); // HSMS状态机初始化
Timer_Init(); // 软件定时器链初始化
SECS_Init(); // SECS消息层初始化
// 启动硬件定时器(1ms中断)
SysTick_Enable(1000);
while (1) {
// 1. 轮询消息队列,处理所有待处理事件
MQ_Poll(&g_msg_queue);
// 2. 轮询网络接口(非阻塞)
TCP_Poll();
// 3. 执行HSMS状态机(空闲状态下的心跳维护)
HSMS_StateMachine_Run(EVT_IDLE);
// 4. 执行SECS事务管理(重发、超时检查等)
SECS_TransactionManager();
// 5. 执行其他低优先级任务
Idle_Tasks();
}
}
裸机实现总结:
- 消息队列轮询:无锁环形缓冲区 + 主循环主动拉取,实现中断与主循环的解耦。
- HSMS连接管理:有限状态机 + 非阻塞网络收发,单线程内完成连接建立、会话选择、分离等全部状态迁移。
- 超时处理:硬件定时器驱动软件定时器链,中断入队 + 主循环处理,兼顾精度与安全性。