为什么需要三线程

早期版本把从站处理、传感器采集、Flash日志写在同一个 while(1) 里,导致几个问题:

  • Modbus 从站响应被传感器采集(PT100/Weight/Relay 三次阻塞读)拖住,G780S 主站轮询超时
  • Flash 扇区擦除(400ms)会阻塞整个循环,看门狗被迫拉长到危险边界
  • 改一个采集周期要牵动从站响应逻辑

拆分后三个线程各自独立调度,通过 RT-Thread IPC 原语协作。

线程划分

线程优先级栈大小周期职责
maint6409610msG780S 从站处理、按键、云端命令、继电器控制、升级确认、LED、喂狗
sensor8409620ms四路流量计高频轮询、PT100/Weight/Relay 周期采集、规则引擎、DI去抖
logger1220481s消费日志消息队列 → 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_sendmaint 发送健康标志位,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),maintsensor 线程各自每轮循环喂狗:

#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 在工业控制器上的典型实践。