shuichan_new/STM32F103_App1/HardWare/RELAY/ext_relay.c

411 lines
14 KiB
C

/* ext_relay.c - 485 扩展继电器板(4 路带反馈)主机侧支持
*
* 硬件: 与称重秤共用 USART2 RS485 总线(9600-8N1, DMA+IDLE 定帧)
* 协议: Modbus RTU, 从机地址 0x10, 详见 Docs/RS485扩展继电器协议.md
* FC05 写单线圈(线圈0~3=继电器1~4, 0xFF00吸合/0x0000断开), 从机回显
* FC03 读保持寄存器 0x0000 起 4 个: 输出状态/反馈状态/故障标志/固件版本
* 时序: 总线静默 >=50ms 才发帧(与秤查询共用 rs485_bus 时间戳); 应答超时 300ms 补发 1 次
* 检测策略: 平时零流量(无周期轮询), 只在以下时机收发:
* 1) 上电 3s 后一次性探测(确认在线 + 触发断电状态恢复)
* 2) 平台/蓝牙控制命令: FC03 读当前态(快照命令前反馈) -> FC05 -> 150ms -> FC03 校验跳变
* 3) 断电恢复: 离线->上线时比对 EEPROM 记忆, 不一致的回路逐路补发 FC05
*/
#include "ext_relay.h"
#include "relay.h"
#include "24c02.h"
#include "uart.h"
#include "log.h"
#include "network.h"
#include "FreeRTOS.h"
#include "task.h"
#include <string.h>
#include <stdio.h>
#define EXT_RESP_TIMEOUT_MS 300u /* 应答超时 */
#define EXT_FB_SETTLE_MS 150u /* 命令回显后等触点/负载稳定再读反馈 */
/* 帧事件(on_frame 置位, process 消费; 同在 app_task 单线程) */
static uint8_t rx_echo_ok = 0;
static uint8_t rx_state_ok = 0;
static uint8_t rx_exc = 0;
/* Modbus CRC16(与秤同款多项式) */
static uint16_t ext_crc16(const uint8_t *data, uint16_t len)
{
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
crc = (crc & 0x0001) ? ((crc >> 1) ^ 0xA001) : (crc >> 1);
}
}
return crc;
}
/* 总线发送: 清残留应答标志 + 等 50ms 静默 + 清接收缓冲 + 发送 + 刷新时间戳 */
static void ext_tx(const uint8_t *frame, uint16_t len)
{
rx_echo_ok = 0;
rx_state_ok = 0;
rx_exc = 0;
uint32_t dt = HAL_GetTick() - rs485_bus_last_tick();
if (dt < 50u) vTaskDelay(50u - dt);
USART2_FlushRxBuf();
HAL_UART_Transmit(&huart2, (uint8_t *)frame, len, 100);
rs485_bus_touch();
}
static void ext_send_fc05(uint8_t ch, uint8_t on)
{
uint8_t f[8];
f[0] = EXT_RELAY_ADDR;
f[1] = 0x05;
f[2] = 0x00;
f[3] = (uint8_t)(ch - 1); /* 线圈地址 0~3 */
f[4] = on ? 0xFF : 0x00;
f[5] = 0x00;
uint16_t c = ext_crc16(f, 6);
f[6] = (uint8_t)(c & 0xFF);
f[7] = (uint8_t)(c >> 8);
ext_tx(f, 8);
}
static void ext_send_fc03(void)
{
uint8_t f[8];
f[0] = EXT_RELAY_ADDR;
f[1] = 0x03;
f[2] = 0x00; f[3] = 0x00; /* 起始寄存器 0x0000 */
f[4] = 0x00; f[5] = 0x04; /* 读 4 个 */
uint16_t c = ext_crc16(f, 6);
f[6] = (uint8_t)(c & 0xFF);
f[7] = (uint8_t)(c >> 8);
ext_tx(f, 8);
}
/* 扩展板状态(最近一轮 FC03 应答) */
static uint8_t s_online = 0;
static uint8_t s_state = 0; /* REG0 输出状态 bit0~3 */
static uint8_t s_fb = 0; /* REG1 反馈状态 */
static uint8_t s_fault = 0; /* REG2 故障标志 */
static uint16_t s_ver = 0; /* REG3 从机版本 */
static uint8_t s_fault_logged = 0; /* 已打印的故障位(变化才重打) */
/* 事务阶段 */
#define ST_IDLE 0 /* 空闲 */
#define ST_READ 1 /* 独立读: 上电探测 / 命令前读取 */
#define ST_ECHO 2 /* 等 FC05 回显 */
#define ST_SETTLE 3 /* 触点稳定延时 */
#define ST_CONFIRM 4 /* 命令后读校验 */
/* 事务类型 */
#define TXN_NONE 0 /* 上电探测 */
#define TXN_CMD 1 /* 平台/蓝牙命令 */
#define TXN_RESTORE 2 /* 断电恢复补发 */
static uint8_t cmd_stage = ST_IDLE;
static uint8_t cmd_txn = TXN_NONE;
static uint8_t cmd_pending = 0; /* 有用户命令待发送 */
static uint8_t cmd_ch = 0, cmd_on = 0, cmd_retry = 0;
static uint32_t cmd_tick = 0;
static char cmd_id[20] = {0};
static uint8_t cmd_state_before = 0; /* 命令前输出位(FC03 预读值) */
static uint8_t cmd_fb_before = 0; /* 命令前反馈位(FC03 预读值) */
static uint8_t boot_probe_done = 0; /* 上电一次性探测已发 */
#define EXT_STATE_EEPROM_IDX 5 /* MqttInfoStr.Relay_State[5] 原保留字节: 扩展板状态位掩码 */
/* 断电恢复状态 */
static uint8_t restore_diff = 0; /* 待恢复位(实际!=记忆) */
static uint8_t restore_target = 0; /* 恢复目标掩码(EEPROM 记忆) */
static uint32_t restore_next_tick = 0; /* 恢复失败后的重试退避 */
/* EEPROM 记忆的扩展板状态(0xFF=空白视为全断) */
static uint8_t ext_saved_mask(void)
{
uint8_t v = MqttInfoStr.Relay_State[EXT_STATE_EEPROM_IDX];
if (v == 0xFF) return 0;
return (uint8_t)(v & 0x0F);
}
/* 以 FC03 读到的实际输出状态刷新 EEPROM(变化才写) */
static void ext_save_state(void)
{
uint8_t nv = (uint8_t)(s_state & 0x0F);
if (MqttInfoStr.Relay_State[EXT_STATE_EEPROM_IDX] != nv) {
MqttInfoStr.Relay_State[EXT_STATE_EEPROM_IDX] = nv;
eepromWriteData(102, &MqttInfoStr.Relay_State[0], 7);
}
}
uint8_t ext_relay_online(void) { return s_online; }
uint8_t ext_relay_state_mask(void) { return s_state; }
uint8_t ext_relay_fault_mask(void) { return s_fault; }
/* 一次事务成功: fresh_state=1 表示刚收到 FC03(状态是最新的) */
static void ext_note_success(int fresh_state)
{
if (!s_online) {
s_online = 1;
log_info("> EXT: 扩展继电器板上线 (从机版本 V%u.%02X)", s_ver >> 8, s_ver & 0xFF);
relayStatueUpdata(); /* 上线后补一次全状态, 平台刷新 sw5~sw8 */
/* 断电恢复: 刚上线且手头是最新状态时, 比对 EEPROM 记忆, 不一致的回路待逐路补发 */
if (fresh_state) {
uint8_t saved = ext_saved_mask();
uint8_t diff = (uint8_t)((s_state ^ saved) & 0x0F);
if (diff) {
restore_diff = diff;
restore_target = saved;
log_warn("> EXT: 扩展板状态与断电记忆不符(实际=%02X 记忆=%02X), 逐路恢复", s_state, saved);
}
}
}
}
static void ext_note_fail(void)
{
if (s_online) {
s_online = 0;
log_warn("> EXT: 扩展继电器板离线(命令无应答)");
}
}
static void ext_respond(int ok, const char *msg)
{
if (cmd_id[0]) {
NET_publish_response(cmd_id, ok, msg);
cmd_id[0] = 0;
}
}
void ext_relay_cmd(uint8_t ch, uint8_t on, const char *id)
{
if (ch < 1 || ch > EXT_RELAY_CH) return;
/* 不在线也照常受理: 命令链路第一步就是 FC03 探测, 不通才应答离线(平时无轮询) */
if (cmd_pending || cmd_stage != ST_IDLE) {
log_warn("> EXT: 上一命令未完成, 拒绝 SW%d 命令", ch + 4);
if (id) NET_publish_response(id, 0, "ext relay busy");
return;
}
cmd_ch = ch;
cmd_on = on ? 1 : 0;
if (id) {
strncpy(cmd_id, id, sizeof(cmd_id) - 1);
cmd_id[sizeof(cmd_id) - 1] = 0;
} else {
cmd_id[0] = 0;
}
cmd_pending = 1;
log_info("> EXT: SW%d %s (下发扩展板)", ch + 4, on ? "" : "");
}
/* 485 帧处理: feed_scale 收帧后按站号 0x10 分发到这里 */
void ext_relay_on_frame(const uint8_t *b, uint16_t len)
{
if (len < 5) return;
uint16_t c = ext_crc16(b, (uint16_t)(len - 2));
uint16_t r = (uint16_t)(((uint16_t)b[len - 1] << 8) | b[len - 2]);
if (c != r) {
log_warn("> EXT: 帧 CRC 错误 (len=%u)", (unsigned)len);
return;
}
if (b[1] & 0x80) { /* Modbus 异常应答 */
log_warn("> EXT: 异常应答 FC=%02X 码=%02X", b[1] & 0x7F, b[2]);
rx_exc = 1;
return;
}
if (b[1] == 0x05 && len == 8) {
/* 回显校验: 线圈地址与通断值必须与请求一致 */
if (b[2] == 0x00 && b[3] == (uint8_t)(cmd_ch - 1) &&
((b[4] == 0xFF && cmd_on) || (b[4] == 0x00 && !cmd_on)) && b[5] == 0x00) {
rx_echo_ok = 1;
} else {
log_warn("> EXT: FC05 回显与请求不符");
}
return;
}
if (b[1] == 0x03 && len == 13 && b[2] == 0x08) {
/* 4 个寄存器大端: 输出/反馈/故障只用低字节 */
s_state = b[4] & 0x0F;
s_fb = b[6] & 0x0F;
s_fault = b[8] & 0x0F;
s_ver = (uint16_t)(((uint16_t)b[9] << 8) | b[10]);
rx_state_ok = 1;
return;
}
log_warn("> EXT: 未识别帧 FC=%02X len=%u", b[1], (unsigned)len);
}
/* 当前事务的最终失败收尾 */
static void ext_txn_failed(const char *respond_msg)
{
if (cmd_txn == TXN_CMD) {
log_warn("> EXT: SW%d 命令失败(%s)", cmd_ch + 4, respond_msg);
ext_respond(0, respond_msg);
} else if (cmd_txn == TXN_RESTORE) {
restore_next_tick = HAL_GetTick() + 2000; /* 退避后重试, 位保留 */
} else {
log_warn("> EXT: 上电探测无应答(未接扩展板?)");
}
ext_note_fail();
cmd_txn = TXN_NONE;
cmd_stage = ST_IDLE;
}
/* 事务超时/异常统一处理: 补发 1 次, 仍失败则收尾 */
static void ext_txn_retry_or_fail(uint8_t resend_fc05, const char *respond_msg)
{
if (cmd_retry < 1) {
cmd_retry++;
if (resend_fc05) ext_send_fc05(cmd_ch, cmd_on);
else ext_send_fc03();
cmd_tick = HAL_GetTick();
return;
}
ext_txn_failed(respond_msg);
}
void ext_relay_process(void)
{
uint32_t now = HAL_GetTick();
if (now < 3000u) return; /* 上电稳定期, 与秤首次查询对齐 */
/* 上电一次性探测: 确认在线 + 触发断电恢复比对 */
if (!boot_probe_done && cmd_stage == ST_IDLE && !cmd_pending) {
boot_probe_done = 1;
cmd_txn = TXN_NONE;
cmd_retry = 0;
cmd_stage = ST_READ;
ext_send_fc03();
cmd_tick = now;
return;
}
/* 用户命令: 先 FC03 预读(判定在线+快照命令前状态) */
if (cmd_pending && cmd_stage == ST_IDLE) {
cmd_pending = 0;
cmd_txn = TXN_CMD;
cmd_retry = 0;
cmd_stage = ST_READ;
ext_send_fc03();
cmd_tick = now;
return;
}
if (cmd_stage == ST_READ) {
if (rx_state_ok) {
rx_state_ok = 0;
ext_note_success(1);
if (s_fault != s_fault_logged) { /* 故障位变化才打印 */
s_fault_logged = s_fault;
if (s_fault) {
log_warn("> EXT: 反馈异常标志=%02X (输出=%02X 反馈=%02X)", s_fault, s_state, s_fb);
}
}
if (cmd_txn == TXN_CMD) {
/* 快照命令前状态(与板载一致: 校验反馈跳变, 防从机反馈卡死误判成功) */
cmd_state_before = (uint8_t)((s_state >> (cmd_ch - 1)) & 1u);
cmd_fb_before = (uint8_t)((s_fb >> (cmd_ch - 1)) & 1u);
cmd_retry = 0;
cmd_stage = ST_ECHO;
ext_send_fc05(cmd_ch, cmd_on);
cmd_tick = now;
} else {
cmd_txn = TXN_NONE;
cmd_stage = ST_IDLE; /* 上电探测完成 */
}
} else if (rx_exc || (now - cmd_tick) > EXT_RESP_TIMEOUT_MS) {
rx_exc = 0;
if (cmd_txn == TXN_CMD) {
ext_txn_retry_or_fail(0, "ext relay board offline");
} else {
ext_txn_retry_or_fail(0, "");
}
}
return;
}
if (cmd_stage == ST_ECHO) { /* 等 FC05 回显 */
if (rx_echo_ok) {
rx_echo_ok = 0;
ext_note_success(0);
cmd_stage = ST_SETTLE;
cmd_tick = now;
} else if (rx_exc || (now - cmd_tick) > EXT_RESP_TIMEOUT_MS) {
rx_exc = 0;
ext_txn_retry_or_fail(1, "ext relay no response");
}
return;
}
if (cmd_stage == ST_SETTLE) { /* 触点/负载稳定延时 */
if ((now - cmd_tick) >= EXT_FB_SETTLE_MS) {
cmd_retry = 0;
cmd_stage = ST_CONFIRM;
ext_send_fc03();
cmd_tick = now;
}
return;
}
if (cmd_stage == ST_CONFIRM) { /* 命令后读校验 */
if (rx_state_ok) {
rx_state_ok = 0;
ext_note_success(1);
if (cmd_txn == TXN_RESTORE) {
cmd_txn = TXN_NONE;
restore_diff &= (uint8_t)~(1u << (cmd_ch - 1));
if (!restore_diff) {
log_info("> EXT: 断电状态恢复完成 (输出=%02X)", s_state);
ext_save_state();
relayStatueUpdata();
}
} else {
/* 与板载继电器反馈同语义: 输出位=目标 且 反馈位=目标 且 反馈有跳变
* (命令前!=目标, 命令后=目标); 重复命令原本就在目标态的豁免跳变要求。
* 反馈位卡死(悬空/极性反)时无跳变 -> 判失败, 不会误报 OK */
uint8_t bit = (uint8_t)(1u << (cmd_ch - 1));
int state_ok = (((s_state & bit) != 0) == cmd_on);
int fb_ok = (((s_fb & bit) != 0) == cmd_on);
int fb_trans = (cmd_fb_before != cmd_on);
int was_already = (cmd_state_before == cmd_on);
int ok = state_ok && fb_ok && (fb_trans || was_already);
if (ok) {
log_info("> EXT: SW%d %s成功 (反馈=%02X)", cmd_ch + 4, cmd_on ? "" : "", s_fb);
ext_respond(1, "OK");
} else {
log_warn("> EXT: SW%d %s未生效 (输出=%02X 反馈=%02X 故障=%02X, 命令前输出=%d 反馈=%d)",
cmd_ch + 4, cmd_on ? "" : "", s_state, s_fb, s_fault,
cmd_state_before, cmd_fb_before);
ext_respond(0, "\xE8\xB4\x9F\xE8\xBD\xBD\xE6\x96\xAD\xE5\xBC\x80\xE6\x88\x96\xE7\xBB\xA7\xE7\x94\xB5\xE5\x99\xA8\xE6\x8D\x9F\xE5\x9D\x8F"); /* 负载断开或继电器损坏(UTF-8 字节转义, 绕开源码 GBK; 平台乱码则改 GBK 字节) */
}
cmd_txn = TXN_NONE;
ext_save_state(); /* 断电记忆: 以实际输出状态落盘 */
relayStatueUpdata(); /* 命令完成后上报全状态 */
}
cmd_stage = ST_IDLE;
} else if (rx_exc || (now - cmd_tick) > EXT_RESP_TIMEOUT_MS) {
rx_exc = 0;
ext_txn_retry_or_fail(0, "ext relay no response");
}
return;
}
/* 断电恢复: 逐路补发 FC05 使扩展板回到 EEPROM 记忆状态(空闲且无争用时) */
if (restore_diff && now >= restore_next_tick && cmd_stage == ST_IDLE && !cmd_pending) {
uint8_t bit = (uint8_t)(restore_diff & (uint8_t)(-(int)restore_diff)); /* 最低待恢复位 */
uint8_t ch = 1;
while (!(bit & 1)) { bit >>= 1; ch++; }
cmd_txn = TXN_RESTORE;
cmd_ch = ch;
cmd_on = (uint8_t)((restore_target >> (ch - 1)) & 1u);
cmd_retry = 0;
cmd_stage = ST_ECHO;
ext_send_fc05(cmd_ch, cmd_on);
cmd_tick = now;
log_info("> EXT: 恢复 SW%d -> %s", ch + 4, cmd_on ? "" : "");
return;
}
}