/* 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 次; 空闲每 2s 轮询一次状态兼在线检测 */ #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 #include #define EXT_RESP_TIMEOUT_MS 300u /* 应答超时 */ #define EXT_FB_SETTLE_MS 150u /* 命令回显后等触点/负载稳定再读反馈 */ #define EXT_POLL_INTERVAL_MS 2000u /* 空闲轮询周期 */ #define EXT_OFFLINE_MISS 3u /* 连续失败次数判离线 */ /* 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; } /* 帧事件(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; /* 总线发送: 等 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_miss = 0; /* 连续无应答计数 */ static uint8_t s_fault_logged = 0; /* 已打印的故障位(变化才重打) */ static uint32_t s_last_poll = 0; /* 命令状态机 */ static uint8_t cmd_pending = 0; /* 有命令待发送 */ static uint8_t cmd_stage = 0; /* 0闲 1等FC05回显 2触点稳定延时 3等FC03应答 */ static uint8_t cmd_ch = 0, cmd_on = 0, cmd_retry = 0; static uint8_t cmd_is_poll = 0; /* 当前事务是轮询(不应答平台) */ static uint8_t cmd_is_restore = 0;/* 当前事务是断电恢复(不应答平台) */ static uint32_t cmd_tick = 0; static char cmd_id[20] = {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; } static void ext_note_success(int fresh_state) { s_miss = 0; 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_miss < 255) s_miss++; if (s_online && s_miss >= EXT_OFFLINE_MISS) { s_online = 0; log_warn("> EXT: 扩展继电器板离线(连续 %u 次无应答)", EXT_OFFLINE_MISS); } } 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; if (!s_online) { log_warn("> EXT: 扩展板离线, 拒绝 SW%d 命令", ch + 4); if (id) NET_publish_response(id, 0, "ext relay board offline"); return; } if (cmd_pending || cmd_stage != 0) { 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); } void ext_relay_process(void) { uint32_t now = HAL_GetTick(); if (now < 3000u) return; /* 上电稳定期, 与秤首次查询对齐 */ /* 取新命令(仅空闲时) */ if (cmd_pending && cmd_stage == 0) { cmd_pending = 0; cmd_is_poll = 0; cmd_retry = 0; cmd_stage = 1; ext_send_fc05(cmd_ch, cmd_on); cmd_tick = now; return; } if (cmd_stage == 1) { /* 等 FC05 回显 */ if (rx_echo_ok) { rx_echo_ok = 0; ext_note_success(0); cmd_stage = 2; cmd_tick = now; } else if (rx_exc || (now - cmd_tick) > EXT_RESP_TIMEOUT_MS) { rx_exc = 0; if (cmd_retry < 1) { cmd_retry++; ext_send_fc05(cmd_ch, cmd_on); cmd_tick = now; } else { log_warn("> EXT: SW%d 命令无应答", cmd_ch + 4); if (!cmd_is_restore) ext_respond(0, "ext relay no response"); else { cmd_is_restore = 0; restore_next_tick = now + 2000; } ext_note_fail(); cmd_stage = 0; } } return; } if (cmd_stage == 2) { /* 触点/负载稳定延时 */ if ((now - cmd_tick) >= EXT_FB_SETTLE_MS) { cmd_retry = 0; cmd_stage = 3; ext_send_fc03(); cmd_tick = now; } return; } if (cmd_stage == 3) { /* 等 FC03 状态应答(命令收尾或周期轮询共用) */ if (rx_state_ok) { rx_state_ok = 0; ext_note_success(1); if (cmd_is_restore) { /* 断电恢复的一路已完成: 清位, 全部完成才统一保存+上报 */ cmd_is_restore = 0; restore_diff &= (uint8_t)~(1u << (cmd_ch - 1)); if (!restore_diff) { log_info("> EXT: 断电状态恢复完成 (输出=%02X)", s_state); ext_save_state(); relayStatueUpdata(); } } else if (cmd_is_poll) { cmd_is_poll = 0; if (s_fault != s_fault_logged) { /* 故障位变化才打印, 防止每 2s 刷屏 */ s_fault_logged = s_fault; if (s_fault) { log_warn("> EXT: 反馈异常标志=%02X (输出=%02X 反馈=%02X)", s_fault, s_state, s_fb); } } } else { /* 与板载继电器反馈同语义: 输出位和反馈位都要等于目标状态才算成功 * (不等 1s 故障位, 150ms 时直接比 REG1 反馈, 漏判"吸合但负载没电") */ uint8_t bit = (uint8_t)(1u << (cmd_ch - 1)); int ok = (((s_state & bit) != 0) == cmd_on) && (((s_fb & bit) != 0) == cmd_on); 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)", cmd_ch + 4, cmd_on ? "开" : "关", s_state, s_fb, s_fault); ext_respond(0, "Load unpowered / relay failure !"); /* 与板载失败文案一致 */ } ext_save_state(); /* 断电记忆: 以实际输出状态落盘 */ relayStatueUpdata(); /* 命令完成后上报全状态 */ } cmd_stage = 0; } else if (rx_exc || (now - cmd_tick) > EXT_RESP_TIMEOUT_MS) { rx_exc = 0; if (cmd_retry < 1) { cmd_retry++; ext_send_fc03(); cmd_tick = now; } else { if (!cmd_is_poll && !cmd_is_restore) { log_warn("> EXT: SW%d 状态确认无应答", cmd_ch + 4); ext_respond(0, "ext relay no response"); } cmd_is_poll = 0; if (cmd_is_restore) { cmd_is_restore = 0; restore_next_tick = now + 2000; } ext_note_fail(); cmd_stage = 0; } } return; } /* 断电恢复: 逐路补发 FC05 使扩展板回到 EEPROM 记忆状态 */ if (restore_diff && now >= restore_next_tick) { uint8_t bit = (uint8_t)(restore_diff & (uint8_t)(-(int)restore_diff)); /* 最低待恢复位 */ uint8_t ch = 1; while (!(bit & 1)) { bit >>= 1; ch++; } cmd_is_poll = 0; cmd_is_restore = 1; cmd_ch = ch; cmd_on = (uint8_t)((restore_target >> (ch - 1)) & 1u); cmd_retry = 0; cmd_stage = 1; ext_send_fc05(cmd_ch, cmd_on); cmd_tick = now; log_info("> EXT: 恢复 SW%d -> %s", ch + 4, cmd_on ? "开" : "关"); return; } /* 空闲: 周期轮询(兼在线检测) */ if ((now - s_last_poll) >= EXT_POLL_INTERVAL_MS) { s_last_poll = now; cmd_is_poll = 1; cmd_retry = 0; cmd_stage = 3; ext_send_fc03(); cmd_tick = now; } }