继电器输出控制

继电器模块通过 Modbus 主站(USART2)控制,从站地址 0x02,16 通道(CH1~CH16)。

线圈映射

线圈地址通道功能码
0x0000CH1FC05(写单个线圈)
0x0001CH2FC05
0x000FCH16FC05

FC05 写入值:0xFF00 = ON,0x0000 = OFF。

保持寄存器

寄存器地址名称功能码说明
0x0032RELAY_REG_INPUT_PACKFC04DI 输入位图
0x0034RELAY_REG_BATCH_CTRLFC06全开/全关控制
0x0035RELAY_REG_OUTPUT_MASKFC06输出位图直接写入

三种控制范式

1. 单线圈翻转(Relay_ToggleOutput

读取当前状态 → 反转 → 写入,整个操作在单次总线锁内完成:

void Relay_ToggleOutput(uint8_t channel)
{
    BusService_Lock(1000);
    uint8_t current = Relay_ReadCoil_Unlocked(channel);
    Relay_WriteCoil_Unlocked(channel, !current);
    BusService_Unlock();
}

保证了读-改写的原子性,不会与其他总线使用者产生竞态。

2. 位图命令(Relay_SetOutputMask

单次 FC06 写入寄存器 0x0035,一次性设置 16 位输出掩码。用于云端命令(通过 RELAY_CMD_BITS 寄存器 0x0015 触发)。

3. 位图读取(Relay_ReadAllCoils

FC01 读取 16 个线圈,返回 2 字节打包位图:((uint16_t)rx_buf[4] << 8) | rx_buf[3]

云端继电器命令处理

App_HandleCloudRelayCommands 消费两个寄存器变更:

寄存器行为
REG_RELAY_CTRL (0x0010)0全部关闭
REG_RELAY_CTRL (0x0010)1~16翻转对应通道
REG_RELAY_CMD_BITS (0x0015)16 位位图直接设置输出掩码

两者仅在手动模式下生效。自动模式下变更被记录日志但拒绝执行。

DI 输入去抖

DI(数字输入)去抖在传感器线程中实现,避免多线程竞争。

状态结构

typedef struct {
    uint8_t  initialized;
    uint16_t last_raw;          // 上一次原始采样
    uint16_t stable;            // 去抖后的输出状态
    uint32_t last_change[16];   // 每个 bit 最后一次原始变化的时间戳
} AppDiState;

状态结构由传感器线程独占访问,不需要额外同步。

去抖算法

对 16 个通道逐位处理:

首次调用:
  snapshot di_mask → last_raw 和 stable
  所有时间戳初始化为 HAL_GetTick()

后续调用(每 20ms 一次):
  for i in 0..15:
    当前原始 bit = di_mask & (1 << i)

    // 1. 检测原始值变化
    if 当前 bit != last_raw bit:
      更新 last_raw
      记录 last_change[i] = now

    // 2. 检测稳定值更新
    if 当前 bit != stable bit AND (now - last_change[i]) >= debounce_ms:
      接受变化,更新 stable

    // 3. 上升沿触发(仅手动模式)
    if stable 从 0 变为 1 AND 手动模式:
      Relay_ToggleOutput(i + 1)  // 翻转对应继电器

去抖参数

  • 去抖时间runtime_config->di_debounce_ms,默认 50ms,有效范围 10~5000ms
  • 可远程配置:通过寄存器 0x0022 写入,暂存后需应用/保存
  • 边沿检测:仅上升沿(0→1),下降沿不触发动作

DI 与继电器的联动

DI 上升沿自动触发对应继电器翻转,提供本地现场联锁行为——物理输入直接驱动继电器输出,无需云端参与。这种设计在现场调试和应急控制场景下非常实用。

手动/自动模式

模式定义

#define G780S_MODE_MANUAL  0x0000
#define G780S_MODE_AUTO    0x0001

默认模式:手动G780s_SetDefaults 中设置)。

模式切换

仅通过远程配置寄存器写入:

  1. 向 0x0030 写入 0xA55A(解锁维护窗口)
  2. 向 0x0028 写入 0(手动)或 1(自动)
  3. 向 0x0031 写入 0x0001(应用/保存)

新模式在 G780s_ApplyConfig 时立即生效,复制到 g_active_config

行为影响

操作手动模式自动模式
本地按键翻转正常执行拒绝,打印日志
DI 上升沿翻转正常执行拒绝
云端 RELAY_CTRL正常执行拒绝
云端 RELAY_CMD_BITS正常执行拒绝
JSON relay_set正常执行拒绝

自动模式当前阻止所有手动控制,为未来的自动逻辑集成预留。自动/手动状态通过系统状态寄存器(REG_SYSTEM_STATUS bit3)上报云端。

本地按键处理

硬件

按键引脚电平通道映射
KEY0PE4低有效(上拉)CH1
KEY1PE3低有效(上拉)CH2
WKUPPA0高有效(下拉)未使用

扫描实现

应用层未使用 ALIENTEK 库的 Key_Scan 函数,而是在 App_HandleKeys 中直接读取 GPIO 做边沿检测:

// 每 10ms 调用一次(maint 线程周期)
key0_now = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_4);
key1_now = HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_3);

// 下降沿检测:last==1 && now==0
if (state->key0_last == 1u && key0_now == 0u) {
    if (App_IsManualMode(runtime_config)) {
        Relay_ToggleOutput(1);  // KEY0 → CH1
    } else {
        printf("[KEY] KEY0 ignored in AUTO mode");
    }
}

按键特性

  • 仅短按:检测下降沿(高→低),长按不重复触发
  • 无长按检测:按住按键只在首次按下时触发一次翻转
  • 模式门控:自动模式下完全忽略按键

优先级设计

模式作为主门控

手动/自动模式是所有继电器控制操作的第一道门。自动模式阻止一切本地控制(按键、DI、云端命令)。

手动模式下的竞争处理

在手动模式下,按键、DI 上升沿、云端命令共享相同的控制路径:

按键 ──→ Relay_ToggleOutput() ──→ BusService_Lock() ──→ 总线操作
DI   ──→ Relay_ToggleOutput() ──→ BusService_Lock() ──→ 总线操作
云端 ──→ Relay_ToggleOutput() / Relay_SetOutputMask() ──→ BusService_Lock() ──→ 总线操作

所有操作通过 BusService 互斥锁序列化,没有固有的优先级顺序。谁先获取到锁谁先执行。

RT-Thread 的互斥锁支持优先级继承:当高优先级线程等待锁时,持有锁的低优先级线程会临时提升优先级,避免优先级反转问题。

Relay_ToggleOutput 的原子性

Relay_ToggleOutput 在单次锁获取内完成读-改写:

BusService_Lock(1000);
uint8_t current = Relay_ReadCoil_Unlocked(channel);
Relay_WriteCoil_Unlocked(channel, !current);
BusService_Unlock();

即使两个线程同时尝试翻转同一通道,也不会出现中间状态——互斥锁保证了原子性。

线程优先级与调度

线程优先级周期说明
sensor8(更高)20ms传感器轮询、DI 去抖、数据推送
maint6(较低)10ms按键、云端命令、G780s 处理、看门狗

sensor 线程优先级高于 maint 线程,确保传感器数据采集的实时性。但两者通过互斥锁共享总线,高优先级线程在等待锁时会临时提升持锁线程的优先级。

DI 去抖的线程安全性

AppDiState 结构体由 sensor 线程独占访问,去抖逻辑和继电器翻转都在 sensor 线程上下文中执行,不需要额外的同步机制。这避免了多线程同时操作去抖状态可能导致的竞态条件。