为什么需要三线程
早期版本把从站处理、传感器采集、Flash日志写在同一个 while(1) 里,导致几个问题:
- Modbus 从站响应被传感器采集(PT100/Weight/Relay 三次阻塞读)拖住,G780S 主站轮询超时
- Flash 扇区擦除(400ms)会阻塞整个循环,看门狗被迫拉长到危险边界
- 改一个采集周期要牵动从站响应逻辑
拆分后三个线程各自独立调度,通过 RT-Thread IPC 原语协作。
线程划分
| 线程 | 优先级 | 栈大小 | 周期 | 职责 |
|---|---|---|---|---|
maint | 6 | 4096 | 10ms | G780S 从站处理、按键、云端命令、继电器控制、升级确认、LED、喂狗 |
sensor | 8 | 4096 | 20ms | 四路流量计高频轮询、PT100/Weight/Relay 周期采集、规则引擎、DI去抖 |
logger | 12 | 2048 | 1s | 消费日志消息队列 → DataLogger → LittleFS → W25Q128 |
优先级数字越小越高。maint 最高,保证从站响应不被采集阻塞;logger 最低,Flash 慢操作让步给业务。
#define APP_MAINT_THREAD_PRIORITY 6u
#define APP_SENSOR_THREAD_PRIORITY 8u
#define APP_LOGGER_THREAD_PRIORITY 12u
启动顺序也有讲究:先起 maint,再起 sensor/logger,保证从站链路优先可用。
线程间通信
三线程之间通过三种 IPC 原语协作,互不直接调用:
sensor ──(mailbox health)──> maint
其他线程 ──(mq logmq)──> logger
maint/sensor ──(mutex appst)──> 共享配置快照
互斥锁保护配置快照
云端下发的 G780sRemoteConfig 是多线程共享资源,maint 负责接收命令更新,sensor 负责读取执行。用 rt_mutex 保护一份运行期配置快照:
typedef struct {
struct rt_mutex lock;
G780sRemoteConfig runtime_config;
rt_bool_t ready;
} AppSharedState;
/* 写:maint 线程收到新配置后整体替换 */
static void App_StateSetRuntimeConfig(const G780sRemoteConfig *config) {
if (rt_mutex_take(&s_app_state.lock, RT_WAITING_FOREVER) == RT_EOK) {
s_app_state.runtime_config = *config;
(void)rt_mutex_release(&s_app_state.lock);
}
}
/* 读:sensor 线程拷贝一份本地副本使用,避免持锁访问字段 */
static void App_StateGetRuntimeConfig(G780sRemoteConfig *config) {
if (rt_mutex_take(&s_app_state.lock, RT_WAITING_FOREVER) == RT_EOK) {
*config = s_app_state.runtime_config;
(void)rt_mutex_release(&s_app_state.lock);
}
}
关键是”拷贝副本再使用”,避免传感器线程长时间持锁。
邮箱上报健康标志
sensor 完成一次采集后通过 rt_mb_send 给 maint 发送健康标志位,maint 据此判断是否可以确认升级槽位:
/* sensor 侧:采集成功对应位置1 */
health_flags |= 0x01u; /* PT100 OK */
health_flags |= 0x02u; /* Weight OK */
health_flags |= 0x04u; /* Relay DO OK */
(void)rt_mb_send(&s_health_mailbox, health_flags);
/* maint 侧:非阻塞接收,累计健康采样数 */
while (rt_mb_recv(&s_health_mailbox, &health_flags, RT_WAITING_NO) == RT_EOK) {
if (health_flags != 0u && healthy_samples < 0xFFFFu) {
healthy_samples++;
}
}
邮箱深度只有 8,用 RT_WAITING_NO 非阻塞发送,满了就丢——健康标志是连续上报的,丢一两条不影响判断。
消息队列异步日志
业务线程提交日志用 rt_messagequeue,详见 日志持久化博客。核心是提交不阻塞业务,Flash 写入在 logger 线程完成。
关键业务逻辑
继电器规则引擎
sensor 线程每个采集周期执行 App_ApplyRelayRules,根据配置的规则把传感器数据映射到继电器输出:
/* 6种数据源 */
switch (rule->source) {
case G780S_RULE_SOURCE_TEMP_CH4_X10: /* 温度×10 */
case G780S_RULE_SOURCE_WEIGHT_CH3_G: /* 重量(克) */
case G780S_RULE_SOURCE_FLOW_X100: /* 流量×100 */
case G780S_RULE_SOURCE_TOTAL_X1000: /* 累计×1000 */
case G780S_RULE_SOURCE_DI_BASE ... G780S_RULE_SOURCE_DI_MAX: /* DI位 */
}
/* 6种比较操作 */
case G780S_RULE_OP_GE: return (value >= threshold);
case G780S_RULE_OP_LE: return (value <= threshold);
case G780S_RULE_OP_GT: case G780S_RULE_OP_LT:
case G780S_RULE_OP_EQ: case G780S_RULE_OP_NE:
规则支持 hold_ms 延迟激活:条件满足后不立即触发,持续满足超过 hold_ms 才动作,防止瞬时抖动。
if (states[i].active == 0u) {
states[i].active = 1u;
states[i].since_tick = now;
}
if ((now - states[i].since_tick) < rule->hold_ms) {
continue; /* 持续时间不够,暂不动作 */
}
DI 去抖
16 路 DI 逐位独立去抖,每位维护自己的 last_change 时间戳:
for (i = 0; i < 16; i++) {
uint16_t bit = (uint16_t)(1u << i);
uint8_t raw = (di_mask & bit) ? 1u : 0u;
uint8_t stable = (state->stable & bit) ? 1u : 0u;
if (raw != last_raw) {
state->last_change[i] = now; /* 原始值变化,重置计时 */
}
if (raw != stable && (now - state->last_change[i]) >= debounce_ms) {
state->stable = ...; /* 稳定持续时间够,更新稳定值 */
if (raw != 0u && App_IsManualMode(runtime_config)) {
(void)Relay_ToggleOutput((uint8_t)(i + 1)); /* 上升沿翻转输出 */
}
}
}
去抖状态只归 sensor 线程维护,避免多线程同时操作输入边沿。
现场设备故障冷却
PT100、Weight、Relay 现场设备通讯失败连续 3 次后进入 5 秒冷却,期间不再尝试访问,避免阻塞采集循环:
#define APP_FIELD_DEVICE_FAIL_THRESHOLD 3u
#define APP_FIELD_DEVICE_COOLDOWN_MS 5000u
static rt_bool_t App_FieldDeviceIsCooling(AppFieldDeviceState *state, uint32_t now) {
if (state->cooling == 0u) return RT_FALSE;
if ((now - state->retry_tick) >= APP_FIELD_DEVICE_COOLDOWN_MS) {
state->cooling = 0u;
return RT_FALSE; /* 冷却结束 */
}
return RT_TRUE; /* 仍在冷却 */
}
冷却机制让传感器线程在总线异常时不会反复重试拖慢整体周期。
A/B 槽位升级确认
这是最关键的安全逻辑。设备重启后不能立即确认运行槽位,必须先观察一段时间确认新固件运行正常,否则 Bootloader 下次启动会回滚到旧槽位:
#define APP_UPGRADE_CONFIRM_DELAY_MS 60000u /* 启动后等60秒 */
#define APP_UPGRADE_CONFIRM_MIN_HEALTHY_SAMPLES 3u /* 至少3次健康采样 */
#define APP_UPGRADE_CONFIRM_RETRY_MS 5000u /* 确认失败5秒后重试 */
static void App_TryConfirmRunningSlot(...) {
if (*confirmed != 0u) return; /* 已确认 */
if ((now - *confirm_start_tick) < 60000u) return; /* 时间未到 */
if (*healthy_samples < 3u) return; /* 健康采样不够 */
if (now < *confirm_next_retry_tick) return; /* 重试间隔未到 */
if (Upgrade_ConfirmRunningSlot() == 0u) {
*confirmed = 1u; /* 写入确认标志到 Flash */
} else {
*confirm_next_retry_tick = now + 5000u; /* 失败重试 */
}
}
确认逻辑放在 maint 线程而非 sensor 线程:健康标志由 sensor 通过邮箱上报,maint 集中判定,避免两个线程同时操作升级状态。
配置热更新
sensor 线程每个周期检测云端配置的 sequence 字段,变化时热替换流量计参数和规则,不需要重启线程或设备:
if (latest_config.sequence == *last_config_seq) {
return RT_FALSE; /* 序号没变,跳过 */
}
*runtime_config = latest_config;
flow_meter[0].sample_ms = runtime_config->flow_sample_period_ms;
flow_meter[0].pulses_per_liter = App_ConfigToFloat2(...);
/* ...四路流量计参数全部热替换 */
看门狗策略
IWDG 超时约 6 秒(40kHz / 256 × 1875),maint 和 sensor 线程各自每轮循环喂狗:
#define APP_IWDG_RELOAD 1875u
static void App_IwdgInit(void) {
s_iwdg_handle.Init.Prescaler = IWDG_PRESCALER_256;
s_iwdg_handle.Init.Reload = APP_IWDG_RELOAD;
HAL_IWDG_Init(&s_iwdg_handle);
}
特别的是 W25Q128 驱动在 Flash 等待繁忙期间(扇区擦除 400ms、全片擦除 200s)也会直接写寄存器喂狗,防止长时间 Flash 操作触发复位:
/* W25Q128.c 中 Flash 等待循环 */
IWDG->KR = 0xAAAA; /* 直接写键值喂狗 */
Bootloader 侧也持续喂狗,因为 App 已开启 IWDG,复位后 Bootloader 必须接力。
启动流程
rt_err_t AppThreads_Create(void) {
Modbus_MasterInit();
G780s_Init();
G780s_GetActiveConfig(&runtime_config); /* 加载云端配置 */
rt_mutex_init(&s_app_state.lock, "appst", RT_IPC_FLAG_PRIO); /* 配置锁 */
BusService_Init(); /* Modbus 总线服务 */
LogService_Init(); /* 日志服务(含消息队列) */
rt_mb_init(&s_health_mailbox, "health", ...); /* 健康邮箱 */
rt_thread_init(&s_maint_thread, "maint", ...); /* 优先级6 */
rt_thread_init(&s_sensor_thread, "sensor", ...); /* 优先级8 */
rt_thread_init(&s_logger_thread, "logger", ...); /* 优先级12 */
rt_thread_startup(&s_maint_thread); /* 先起维护线程 */
rt_thread_startup(&s_sensor_thread); /* 再起采集线程 */
rt_thread_startup(&s_logger_thread); /* 最后起日志线程 */
}
总结
三线程架构把”从站响应""采集计算""持久化”三个时间尺度差异巨大的职责彻底分离:
- 从站响应要求毫秒级,
maint线程 10ms 周期保证 - 传感器采集秒级即可,
sensor线程按配置周期运行 - Flash 持久化是百毫秒级,
logger线程低优先级让步
IPC 原语的选择遵循”最小耦合”原则:互斥锁只保护数据,邮箱只传标志,消息队列只传日志,线程之间没有直接函数调用。这让每个线程可以独立调试和修改,是 RT-Thread 在工业控制器上的典型实践。