继电器输出控制
继电器模块通过 Modbus 主站(USART2)控制,从站地址 0x02,16 通道(CH1~CH16)。
线圈映射
| 线圈地址 | 通道 | 功能码 |
|---|---|---|
| 0x0000 | CH1 | FC05(写单个线圈) |
| 0x0001 | CH2 | FC05 |
| … | … | … |
| 0x000F | CH16 | FC05 |
FC05 写入值:0xFF00 = ON,0x0000 = OFF。
保持寄存器
| 寄存器地址 | 名称 | 功能码 | 说明 |
|---|---|---|---|
| 0x0032 | RELAY_REG_INPUT_PACK | FC04 | DI 输入位图 |
| 0x0034 | RELAY_REG_BATCH_CTRL | FC06 | 全开/全关控制 |
| 0x0035 | RELAY_REG_OUTPUT_MASK | FC06 | 输出位图直接写入 |
三种控制范式
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 中设置)。
模式切换
仅通过远程配置寄存器写入:
- 向 0x0030 写入 0xA55A(解锁维护窗口)
- 向 0x0028 写入 0(手动)或 1(自动)
- 向 0x0031 写入 0x0001(应用/保存)
新模式在 G780s_ApplyConfig 时立即生效,复制到 g_active_config。
行为影响
| 操作 | 手动模式 | 自动模式 |
|---|---|---|
| 本地按键翻转 | 正常执行 | 拒绝,打印日志 |
| DI 上升沿翻转 | 正常执行 | 拒绝 |
| 云端 RELAY_CTRL | 正常执行 | 拒绝 |
| 云端 RELAY_CMD_BITS | 正常执行 | 拒绝 |
| JSON relay_set | 正常执行 | 拒绝 |
自动模式当前阻止所有手动控制,为未来的自动逻辑集成预留。自动/手动状态通过系统状态寄存器(REG_SYSTEM_STATUS bit3)上报云端。
本地按键处理
硬件
| 按键 | 引脚 | 电平 | 通道映射 |
|---|---|---|---|
| KEY0 | PE4 | 低有效(上拉) | CH1 |
| KEY1 | PE3 | 低有效(上拉) | CH2 |
| WKUP | PA0 | 高有效(下拉) | 未使用 |
扫描实现
应用层未使用 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();
即使两个线程同时尝试翻转同一通道,也不会出现中间状态——互斥锁保证了原子性。
线程优先级与调度
| 线程 | 优先级 | 周期 | 说明 |
|---|---|---|---|
| sensor | 8(更高) | 20ms | 传感器轮询、DI 去抖、数据推送 |
| maint | 6(较低) | 10ms | 按键、云端命令、G780s 处理、看门狗 |
sensor 线程优先级高于 maint 线程,确保传感器数据采集的实时性。但两者通过互斥锁共享总线,高优先级线程在等待锁时会临时提升持锁线程的优先级。
DI 去抖的线程安全性
AppDiState 结构体由 sensor 线程独占访问,去抖逻辑和继电器翻转都在 sensor 线程上下文中执行,不需要额外的同步机制。这避免了多线程同时操作去抖状态可能导致的竞态条件。