diff --git a/AI项目上下文.md b/AI项目上下文.md index d18503a..56a6b60 100644 --- a/AI项目上下文.md +++ b/AI项目上下文.md @@ -66,11 +66,13 @@ | KEY1~4 | **PC6/PC7/PB14/PB15 上拉输入,功能预留** | | 取消 | PA8 手动模式开关(MANUAL_MODE 已删) | +**485 总线多从机(V1.7.1 起)**:称重秤 0x01(Modbus 读重量)+ 扩展继电器板 0x10(FC05 控制/FC03 读状态,协议见 `Docs/RS485扩展继电器协议.md`);收帧统一在 feedScaleProcess 按站号分发,发送共用 uart.c 的 rs485_bus 时间戳做 50ms 静默保护;平台标识符 sw5~sw8,蓝牙 R5ON~R8OFF + ## 4. 云平台协议 - 上报:`{"header":"iot.prop.post","body":{...}}`;4G 通道 AT 报文里引号用**字面三字符 `\22`**(C 源码写 `\\22`),不能丢反斜杠(踩过两次) - 下行:`iot.prop.set`(控制终端继电器)、`iot.ota.upgrade.post`(OTA) -- 标识符:sw1~sw4/feeding(继电器)、weight/zero/onceWt(秤)、energy/pow、power="220V,0.21A,46.2W"字符串、GPS("经度,纬度"高德格式)、iccid、fwver("1.6.5:3"版本串+序号)、ver(数字版本序号)、BAT_VAL(采集终端电量) +- 标识符:sw1~sw4/feeding(板载继电器)、**sw5~sw8(485 扩展继电器板,V1.7.1 起,在线才上报)**、weight/zero/onceWt(秤)、energy/pow、power="220V,0.21A,46.2W"字符串、GPS("经度,纬度"高德格式)、iccid、fwver("1.6.5:3"版本串+序号)、ver(数字版本序号)、BAT_VAL(采集终端电量) - 传感器标识符(采集终端):PH/EC/BS_T/BS_RH/BS_DO/DS_T/DO/BY_TP/BY_RH/BY_HV/BY_CO2/YSMD_TP/YSMD_RH/SOIL_TP/RH/NV/PV/KV/PH/RV/WIND - 协议解析助手在 `m_app/proto.c`(不用完整 JSON 库) @@ -89,7 +91,7 @@ RESET / INFO / MQTTINFO / STATUS / HELP(逐条中文) SETMQTT id user pass(保存后自动重启)/ SETIP ip port / FACTORY / SETWIFI ssid pass / WIFIINFO / WIFICLEAR / WIFIRESET -R1ON~R4OFF/ALLON/ALLOFF(控制终端)/ FEEDSTART kg / FEEDSTOP / FEED(控制终端投喂) +R1ON~R4OFF/ALLON/ALLOFF(控制终端板载)/ **R5ON~R8OFF(485 扩展继电器板,V1.7.1 起)** / FEEDSTART kg / FEEDSTOP / FEED(控制终端投喂) SCALE/SCALE0/SCALE1(控制终端)/ GPS/GPS0/GPS1 / ENCLR(控制终端电能清零)/ HISTORY(控制终端重启记录)/ BLESET / LOGTRACE~LOGFATAL / OTA ## 7. OTA 架构(两终端相同) diff --git a/Docs/RS485扩展继电器协议.md b/Docs/RS485扩展继电器协议.md new file mode 100644 index 0000000..4e42929 --- /dev/null +++ b/Docs/RS485扩展继电器协议.md @@ -0,0 +1,126 @@ +# RS485 扩展继电器板通信协议 V1.0 + +> 适用:控制终端(主控,V1.7.1 起)外接级联继电器扩展板(从机)。 +> 从机板硬件/固件按本文档实现即可与主控直接对接。 + +## 1. 概述 + +主控板载 4 路继电器(sw1~sw4)不够用时,通过 RS485 总线级联一块扩展板,再扩 4 路带反馈继电器(平台标识符 sw5~sw8)。 + +- 主从结构:控制终端 = 主机(唯一主站),扩展板 = 从机 +- 协议:**Modbus RTU**,从机地址固定 **0x10**(称重秤占用 0x01,勿冲突) +- 与称重秤**共用同一条 485 总线**(主控 USART2,PA2/PA3) + +## 2. 物理层 / 总线规则 + +| 项 | 值 | +| --- | --- | +| 波特率 | 9600 bps,8N1 | +| 接口 | RS485 半双工 | +| 帧定界 | 空闲间隔(主控侧 DMA+IDLE,≥1 帧间隙判一帧) | +| **帧间隔** | **总线上任意两帧之间必须 ≥50ms**(从机 485 释放/回声沉降时间,实测踩过坑) | +| 应答时限 | 从机收到完整请求后 **≤100ms** 内应答(主机 300ms 超时,补发 1 次) | +| CRC | Modbus CRC16(多项式 0xA001,初值 0xFFFF),**低字节在前** | + +## 3. 功能码 + +从机只需实现两个功能码: + +### 3.1 FC05(0x05)写单线圈 —— 控制继电器 + +线圈地址 0~3 = 继电器 1~4(对应平台 sw5~sw8)。 + +**请求(8 字节)**: + +| 字节 | 内容 | +| --- | --- | +| 0 | 0x10(从机地址) | +| 1 | 0x05(功能码) | +| 2~3 | 线圈地址,大端:0x0000~0x0003 | +| 4~5 | 0xFF 0x00 = 吸合;0x00 0x00 = 断开 | +| 6~7 | CRC16 低字节、高字节 | + +**示例**:继电器 2 吸合 → `10 05 00 01 FF 00 DE BB` + +**应答**:从机执行动作后**原样回显请求帧**。回显内容与请求不一致(线圈号/通断值不符)主机判失败。 + +### 3.2 FC03(0x03)读保持寄存器 —— 状态/反馈/故障/版本 + +固定读 0x0000 起 4 个寄存器。 + +**请求(8 字节)**:`10 03 00 00 00 04 47 48` + +**应答(13 字节)**: + +| 字节 | 内容 | +| --- | --- | +| 0 | 0x10 | +| 1 | 0x03 | +| 2 | 0x08(数据字节数) | +| 3~4 | REG0:继电器输出状态,bit0~3 = 继电器 1~4(1=吸合) | +| 5~6 | REG1:反馈状态,bit0~3(1=负载得电) | +| 7~8 | REG2:故障标志,bit0~3(1=该路输出与反馈不一致持续 >1s) | +| 9~10 | REG3:从机固件版本,如 0x0100 = V1.0 | +| 11~12 | CRC16 低字节、高字节 | + +寄存器 16 位大端;状态位放**低字节**即可,高字节填 0。 + +### 3.3 异常应答 + +从机遇到非法功能码/地址/数据时回标准 Modbus 异常帧: + +`10 | (功能码 | 0x80) | 异常码 | CRC_lo | CRC_hi`(共 5 字节) + +异常码:0x01 非法功能,0x02 非法地址,0x03 非法数据。 + +## 4. 从机行为要求 + +1. **上电默认 4 路全部断开**(主控不负责恢复扩展板断电前状态) +2. FC05 收到并校验通过(地址/功能码/线圈范围/CRC)后**立即执行**再回显 +3. 反馈输入:每路继电器配负载反馈检测(同主控板载继电器:吸合后负载得电=高)。从机自行维护 REG2 故障位:某路输出状态与反馈不一致**持续超过 1s** 置 1,恢复一致后清 0 +4. 反馈采样建议 10~50ms 周期,加简单消抖(如连续 3 次一致才更新 REG1) +5. 只响应地址 0x10 的帧;**不要响应广播/其它地址**(总线上还有秤,地址 0x01) +6. 收帧间隔不足 50ms 也要能正常解析(主控保证发送间隔,从机只做鲁棒接收) + +## 5. 主机行为(联调参考) + +主控侧已实现(`HardWare/RELAY/ext_relay.c`): + +- **命令流程**:平台/蓝牙下发 → FC05 → 等回显(300ms 超时,补发 1 次)→ 延时 150ms 等触点稳定 → FC03 读状态 → 校验"输出位=目标状态 且 故障位=0" → 应答平台 OK/失败 → 上报全状态 +- **周期轮询**:空闲每 2s 发一次 FC03,兼在线检测;连续 3 次无应答判离线(日志提示) +- **离线行为**:离线期间下发命令直接回复平台 "ext relay board offline";板子恢复应答后自动上线并补一次状态上报 +- 上电 3s 内不发帧(避开上电窗口) + +## 6. 平台/蓝牙映射 + +| 项 | 映射 | +| --- | --- | +| MQTT 下发 | `iot.prop.set` 的 **sw5~sw8**(true/false),扩展板在线时状态大报文携带 sw5~sw8 字段 | +| 平台应答 | 命令完成(含反馈校验)后回 `{"id":..,"code":0,"message":"OK"}` 或失败原因 | +| 蓝牙命令 | **R5ON ~ R8OFF**(不应答平台,只执行+记日志);STATUS 显示扩展板状态 | +| 平台物模型 | 需新增 4 个布尔字段 sw5~sw8 | + +## 7. CRC16 参考实现(C) + +```c +static uint16_t crc16_modbus(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; /* 发送时低字节在前 */ +} +``` + +## 8. 联调检查清单 + +- [ ] 从机地址 0x10,9600-8N1 +- [ ] FC05 回显与请求逐字节一致 +- [ ] FC03 应答 13 字节,寄存器大端,状态位在低字节 +- [ ] 应答延迟 ≤100ms +- [ ] 上电 4 路全断 +- [ ] 拔掉反馈线后操作对应继电器,REG2 对应位约 1s 后置 1 +- [ ] 与秤同时挂总线:主控发帧间隔 ≥50ms,两者互不丢帧 diff --git a/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c b/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c index 43bcae1..f782810 100644 --- a/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c +++ b/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c @@ -11,6 +11,7 @@ #include "FreeRTOS.h" #include "task.h" #include "relay.h" +#include "ext_relay.h" #include "uart.h" #include "log.h" #include "network.h" @@ -133,18 +134,16 @@ static int feed_scale_parse_weight(const uint8_t *buf, uint16_t len, uint16_t *w return 0; } -/* һյ 485 ֡ʱ̣ѯǰ߼ã */ -static uint32_t last_frame_tick = 0; - /* ͨ USART2 485 ѯ */ static void feed_scale_send_query(void) { /* һ֡ټ 50msӻ 485 ͷ/Ҫʱ䡣 * ǰӲѯ 1s ᴥԺ߼ӴѯʱЧ */ - uint32_t dt = HAL_GetTick() - last_frame_tick; + uint32_t dt = HAL_GetTick() - rs485_bus_last_tick(); if (dt < 50u) vTaskDelay(50u - dt); USART2_FlushRxBuf(); /* ֻ壨/Ӳճ */ HAL_UART_Transmit(&huart2, (uint8_t *)RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD), 100); + rs485_bus_touch(); } /* ȡ 485 ֡ */ @@ -386,45 +385,14 @@ static void feed_scale_hr_check(void) } } -void feedScaleProcess(void) +/* سӦ֡(վ 0x01); void, ԭ feedScaleProcess β߼ƽ */ +static void feed_scale_on_scale_frame(const uint8_t *rx_buf, uint16_t flen) { - feed_scale_hr_check(); /* ȱϱ, ģʽ/ѯ״̬޹ */ - - if (!g_feed_scale_on) return; - - uint8_t rx_buf[32]; - int len; uint16_t cur_weight; int32_t diff; - - /* ԭȡ֡ feed_scale_read_frame ɣ Tick */ - - /* ʱ 10s ѯһ; Ͷιʱ 1s ѯһ; - ۼӦֶʱ©Ҳһֱˢʱ */ - uint32_t query_interval = (feed_state == FEED_RUNNING) ? FEED_SCALE_QUERY_RUN_MS : FEED_SCALE_QUERY_IDLE_MS; - - /* ״βѯӳ 3sܿϵ細ڣBLE /ʼ/״ϱԴ - * ôڷ׸Ӧױײ */ - if (HAL_GetTick() < 3000u) return; - - if (first_query_sent == 0 || (HAL_GetTick() - last_query_tick) >= query_interval) { - last_query_tick = HAL_GetTick(); - feed_scale_send_query(); - await_reply_tick = HAL_GetTick(); /* ǵȴӦ1s ֡־ */ - if (first_query_sent == 0) { - first_query_sent = 1; - } - } - - /* ѯ 1s Ч֡־Զ */ - if (await_reply_tick && (HAL_GetTick() - await_reply_tick) > 1000u) { - await_reply_tick = 0; - s_last_err = 1; - } - - /* ȡ 485 */ - len = feed_scale_read_frame(rx_buf, sizeof(rx_buf)); - if (len > 0) { last_frame_tick = HAL_GetTick(); /* ѯǰ߼ */ + int len = (int)flen; + (void)len; + if (len > 0) { /* ѯǰ߼ */ int prc = feed_scale_parse_weight(rx_buf, len, &cur_weight); if (prc == 0) { await_reply_tick = 0; } if (prc != -7) { /* -7=֡Ӱ״̬ʾ */ @@ -480,6 +448,53 @@ void feedScaleProcess(void) } } +} + +void feedScaleProcess(void) +{ + feed_scale_hr_check(); /* ȱϱ, ģʽ/ѯ״̬޹ */ + + + /* 485 ֡ͳһڴȡ, վŷַ: 0x01=س, 0x10=չ̵ */ + { + uint8_t rx_buf[32]; + int len = feed_scale_read_frame(rx_buf, sizeof(rx_buf)); + if (len > 0) { + rs485_bus_touch(); /* ˢ߻ʱ(50ms Ĭ) */ + if (rx_buf[0] == EXT_RELAY_ADDR) { + ext_relay_on_frame(rx_buf, (uint16_t)len); + } else if (g_feed_scale_on) { + feed_scale_on_scale_frame(rx_buf, (uint16_t)len); + } + } + } + + if (!g_feed_scale_on) return; + + + /* ʱ 10s ѯһ; Ͷιʱ 1s ѯһ; + ۼӦֶʱ©Ҳһֱˢʱ */ + uint32_t query_interval = (feed_state == FEED_RUNNING) ? FEED_SCALE_QUERY_RUN_MS : FEED_SCALE_QUERY_IDLE_MS; + + /* ״βѯӳ 3sܿϵ細ڣBLE /ʼ/״ϱԴ + * ôڷ׸Ӧױײ */ + if (HAL_GetTick() < 3000u) return; + + if (first_query_sent == 0 || (HAL_GetTick() - last_query_tick) >= query_interval) { + last_query_tick = HAL_GetTick(); + feed_scale_send_query(); + await_reply_tick = HAL_GetTick(); /* ǵȴӦ1s ֡־ */ + if (first_query_sent == 0) { + first_query_sent = 1; + } + } + + /* ѯ 1s Ч֡־Զ */ + if (await_reply_tick && (HAL_GetTick() - await_reply_tick) > 1000u) { + await_reply_tick = 0; + s_last_err = 1; + } + /* ״̬ⲿģһѭǰ */ if (feed_state == FEED_DONE || feed_state == FEED_ERROR) { /* ״̬һΣ relay ״̬ϱ */ diff --git a/STM32F103_App1/HardWare/NETWORK/network.c b/STM32F103_App1/HardWare/NETWORK/network.c index 79fc350..7b46d9b 100644 --- a/STM32F103_App1/HardWare/NETWORK/network.c +++ b/STM32F103_App1/HardWare/NETWORK/network.c @@ -20,6 +20,7 @@ #include "main.h" #include "uart.h" #include "relay.h" +#include "ext_relay.h" #include "proto.h" #include "24c02.h" #include "FreeRTOS.h" @@ -282,28 +283,40 @@ int NET_publish_status(int sw1, int feeding, int sw2, int sw3, int sw4, { if (g_net_tx_busy) { log_info("> NET: ״̬ϱ (æ)"); return -1; } if (net_at_trylock() != 0) { log_info("> NET: ״̬ϱ (AT æ)"); return -1; } + /* 485 չ̵ʱ sw5~sw8(ƽ̨ģӶӦֶ) */ + char ext_plain[80] = ""; + char ext_esc[112] = ""; + if (ext_relay_online()) { + uint8_t m = ext_relay_state_mask(); + snprintf(ext_plain, sizeof(ext_plain), + ",\"sw5\":%d,\"sw6\":%d,\"sw7\":%d,\"sw8\":%d", + (m>>0)&1, (m>>1)&1, (m>>2)&1, (m>>3)&1); + snprintf(ext_esc, sizeof(ext_esc), + ",\\22sw5\\22:%d,\\22sw6\\22:%d,\\22sw7\\22:%d,\\22sw8\\22:%d", + (m>>0)&1, (m>>1)&1, (m>>2)&1, (m>>3)&1); + } /* ver ֶΣ̼汾ţƽ̨ݴ˿ʶ豸Ƿ OTA */ /* sw1 feeding ӳ̵ 1 ״̬޳ģʽߵЧ * ֶζϱƽ̨ sw1 غ feeding ״̬ͬˢ */ /* ޳ģʽPRODUCT_WITH_FEED_SCALE=0ϱ zero/onceWt ֶ */ if (s_backend == NET_BACKEND_4G) { if (g_feed_scale_on) { - CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22onceWt\\22:%.1f,\\22zero\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d}}\"", - MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4); + CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22onceWt\\22:%.1f,\\22zero\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d%s}}\"", + MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_esc); } else { - CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d,\\22energy\\22:%.2f}}\"", - MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, (double)hlw8032_get_energy_kwh()); + CAT1_printf("AT+MPUB=\"/iot/data/up/%s\",0,0,\"{\\22header\\22:\\22iot.prop.post\\22,\\22body\\22:{\\22ver\\22:%u,\\22weight\\22:%.1f,\\22sw1\\22:%d,\\22feeding\\22:%d,\\22sw2\\22:%d,\\22sw3\\22:%d,\\22sw4\\22:%d%s,\\22energy\\22:%.2f}}\"", + MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_esc, (double)hlw8032_get_energy_kwh()); } } else { char json[256]; if (g_feed_scale_on) { snprintf(json, sizeof(json), - "{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"onceWt\":%.1f,\"zero\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d}}", - (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4); + "{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"onceWt\":%.1f,\"zero\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d%s}}", + (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_plain); } else { snprintf(json, sizeof(json), - "{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d,\"energy\":%.2f}}", - (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, (double)hlw8032_get_energy_kwh()); + "{\"header\":\"iot.prop.post\",\"body\":{\"ver\":%u,\"weight\":%.1f,\"sw1\":%d,\"feeding\":%d,\"sw2\":%d,\"sw3\":%d,\"sw4\":%d%s,\"energy\":%.2f}}", + (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_plain, (double)hlw8032_get_energy_kwh()); } net_wifi_publish_up(json); } diff --git a/STM32F103_App1/HardWare/RELAY/ext_relay.c b/STM32F103_App1/HardWare/RELAY/ext_relay.c new file mode 100644 index 0000000..b98c9d5 --- /dev/null +++ b/STM32F103_App1/HardWare/RELAY/ext_relay.c @@ -0,0 +1,291 @@ +/* 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 "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; +} + +/* ߷: 50ms Ĭ + ջ(¾֡) + + ˢʱ */ +static void ext_tx(const uint8_t *frame, uint16_t len) +{ + 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 uint32_t s_last_poll = 0; + +/* ״̬ */ +static uint8_t cmd_pending = 0; /* */ +static uint8_t cmd_stage = 0; /* 0 1FC05 2ȶʱ 3FC03Ӧ */ +static uint8_t cmd_ch = 0, cmd_on = 0, cmd_retry = 0; +static uint8_t cmd_is_poll = 0; /* ǰѯ(Ӧƽ̨) */ +static uint32_t cmd_tick = 0; +static char cmd_id[20] = {0}; + +/* ֡¼(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; + +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(void) +{ + s_miss = 0; + if (!s_online) { + s_online = 1; + log_info("> EXT: չ̵ (ӻ汾 V%u.%02X)", s_ver >> 8, s_ver & 0xFF); + relayStatueUpdata(); /* ߺһȫ״̬, ƽ̨ˢ sw5~sw8 */ + } +} + +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(); + 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); + ext_respond(0, "ext relay no response"); + 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(); + if (cmd_is_poll) { + cmd_is_poll = 0; + if (s_fault) { + log_warn("> EXT: 쳣־=%02X (=%02X =%02X)", s_fault, s_state, s_fb); + } + } else { + uint8_t bit = (uint8_t)(1u << (cmd_ch - 1)); + int ok = (((s_state & bit) != 0) == cmd_on) && ((s_fault & bit) == 0); + 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, "ext relay failed / load unpowered"); + } + 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) { + log_warn("> EXT: SW%d ״̬ȷӦ", cmd_ch + 4); + ext_respond(0, "ext relay no response"); + } + cmd_is_poll = 0; + ext_note_fail(); + cmd_stage = 0; + } + } + 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; + } +} diff --git a/STM32F103_App1/HardWare/RELAY/ext_relay.h b/STM32F103_App1/HardWare/RELAY/ext_relay.h new file mode 100644 index 0000000..04dad15 --- /dev/null +++ b/STM32F103_App1/HardWare/RELAY/ext_relay.h @@ -0,0 +1,26 @@ +#ifndef EXT_RELAY_H +#define EXT_RELAY_H + +#include "main.h" +#include + +/* 485 չ̵(4 ·), Modbus RTU ӻ, سӹ USART2 + * Э Docs/RS485չ̵Э.md */ +#define EXT_RELAY_ADDR 0x10 /* ӻַ(س 0x01) */ +#define EXT_RELAY_CH 4 + +/* app_task 10ms ڵ: /ӦУ/ѯ */ +void ext_relay_process(void); + +/* 485 ַ֡(feed_scale ͳһ֡վŷַ) */ +void ext_relay_on_frame(const uint8_t *buf, uint16_t len); + +/* ·: ch=1~4, on=1 0 Ͽ; + * cmd_id Ϊƽָ̨ id(ɺӦ), NULL=/(Ӧ) */ +void ext_relay_cmd(uint8_t ch, uint8_t on, const char *cmd_id); + +uint8_t ext_relay_online(void); /* չ(ϵйɹӦ) */ +uint8_t ext_relay_state_mask(void); /* bit0~3 = ̵1~4 ״̬(1=) */ +uint8_t ext_relay_fault_mask(void); /* bit0~3 = 쳣־ */ + +#endif /* EXT_RELAY_H */ diff --git a/STM32F103_App1/HardWare/RELAY/relay.c b/STM32F103_App1/HardWare/RELAY/relay.c index 0633d6a..892266c 100644 --- a/STM32F103_App1/HardWare/RELAY/relay.c +++ b/STM32F103_App1/HardWare/RELAY/relay.c @@ -1,3 +1,4 @@ +#include "ext_relay.h" #include "relay.h" #include "main.h" #include "network.h" @@ -134,7 +135,7 @@ GPIO_PinState relayReadFb(uint8_t ch) /* ̵ָ״̬: ֻд GPIO Relay_State, EEPROM/ */ void relaySet(uint8_t ch, uint8_t on) { - if (ch < 1 || ch > 4) return; + if (ch < 1 || ch > 8) return; MqttInfoStr.Relay_State[ch] = on ? 1 : 0; switch (ch) { case 1: if (on) Relay1_ON; else Relay1_OFF; break; @@ -200,6 +201,12 @@ void relayAction(uint8_t *data) } /* Ƽ̵ǰȶȡƽ */ + /* sw5~sw8: 485 չ̵(첽·, Ӧ ext_relay_process ) */ + if (ch >= 5) { + ext_relay_cmd((uint8_t)(ch - 4), (uint8_t)on, CmdId); + return; + } + fb_before = relayReadFb(ch); if (g_feed_scale_on) { diff --git a/STM32F103_App1/HardWare/SYSINIT/app_loop.c b/STM32F103_App1/HardWare/SYSINIT/app_loop.c index 7333bf0..b43b3ba 100644 --- a/STM32F103_App1/HardWare/SYSINIT/app_loop.c +++ b/STM32F103_App1/HardWare/SYSINIT/app_loop.c @@ -17,6 +17,7 @@ #include "app_common.h" #include "user_cmd.h" #include "relay.h" +#include "ext_relay.h" #include "network.h" #include "led.h" #include "adc.h" @@ -232,6 +233,9 @@ static void app_task(void *arg) /* ιϳѯͶι */ feedScaleProcess(); + + /* 485 չ̵: ·/ӦУ/2s ѯ(ַ֡ feedScaleProcess ) */ + ext_relay_process(); } diff --git a/STM32F103_App1/HardWare/UART/uart.c b/STM32F103_App1/HardWare/UART/uart.c index 8d1ec2e..d57b456 100644 --- a/STM32F103_App1/HardWare/UART/uart.c +++ b/STM32F103_App1/HardWare/UART/uart.c @@ -457,3 +457,18 @@ const char *USART1_GetCommand(void) return NULL; } + +/*==== RS485 ߻ʱ(չ̵干 USART2) ==== + * κһ֡շ touch һ; ͷ߾Ĭ >=50ms ٷ, + * ֹӻ 485 ͷ/ײ֡(ɼնʵȿ) */ +static volatile uint32_t s_rs485_bus_tick = 0; + +void rs485_bus_touch(void) +{ + s_rs485_bus_tick = HAL_GetTick(); +} + +uint32_t rs485_bus_last_tick(void) +{ + return s_rs485_bus_tick; +} diff --git a/STM32F103_App1/HardWare/UART/uart.h b/STM32F103_App1/HardWare/UART/uart.h index 0b7a287..b7dc256 100644 --- a/STM32F103_App1/HardWare/UART/uart.h +++ b/STM32F103_App1/HardWare/UART/uart.h @@ -38,6 +38,10 @@ void USART2_Tick(void); /* uint16_t USART2_GetFrame(uint8_t *out, uint16_t out_size); uint16_t USART2_PollFrame(uint8_t *out, uint16_t out_size); /* ԭȡ֡оĬ+һɣ */ void USART2_FlushRxBuf(void); + +/* RS485 ߻ʱ(/չ̵干, ǰ農Ĭ>=50ms) */ +void rs485_bus_touch(void); +uint32_t rs485_bus_last_tick(void); extern volatile uint32_t g_uart2_rx_byte_cnt; uint16_t USART2_GetRxIndex(void); /* USART2 ֽڼ485 ϣ */ /* USART2 ջ壨485 ѯǰãӲ */ /* USART2 ȡһ֡ԭʼֽ */ int USART1_CheckCommand(const char *cmd); /* ǷյָִСд */ diff --git a/STM32F103_App1/HardWare/USER_CMD/user_cmd.c b/STM32F103_App1/HardWare/USER_CMD/user_cmd.c index 01a90ce..ae2d516 100644 --- a/STM32F103_App1/HardWare/USER_CMD/user_cmd.c +++ b/STM32F103_App1/HardWare/USER_CMD/user_cmd.c @@ -25,6 +25,7 @@ #include "ble_at.h" #include "app_common.h" #include "relay.h" +#include "ext_relay.h" #include "24c02.h" #include "uart.h" #include "log.h" @@ -329,6 +330,14 @@ void process_usart1_command(void) else if (cmd_is(cmd, "R3OFF")) { relay_set(3, 0); } else if (cmd_is(cmd, "R4ON")) { relay_set(4, 1); } else if (cmd_is(cmd, "R4OFF")) { relay_set(4, 0); } + else if (cmd_is(cmd, "R5ON")) { ext_relay_cmd(1, 1, NULL); } /* 485 չ̵ */ + else if (cmd_is(cmd, "R5OFF")) { ext_relay_cmd(1, 0, NULL); } + else if (cmd_is(cmd, "R6ON")) { ext_relay_cmd(2, 1, NULL); } + else if (cmd_is(cmd, "R6OFF")) { ext_relay_cmd(2, 0, NULL); } + else if (cmd_is(cmd, "R7ON")) { ext_relay_cmd(3, 1, NULL); } + else if (cmd_is(cmd, "R7OFF")) { ext_relay_cmd(3, 0, NULL); } + else if (cmd_is(cmd, "R8ON")) { ext_relay_cmd(4, 1, NULL); } + else if (cmd_is(cmd, "R8OFF")) { ext_relay_cmd(4, 0, NULL); } else if (cmd_is(cmd, "ALLON")) { relay_set(1, 1); relay_set(2, 1); relay_set(3, 1); relay_set(4, 1); } @@ -341,6 +350,17 @@ void process_usart1_command(void) MqttInfoStr.Relay_State[2] ? "" : "", MqttInfoStr.Relay_State[3] ? "" : "", MqttInfoStr.Relay_State[4] ? "" : ""); + if (ext_relay_online()) { + uint8_t m = ext_relay_state_mask(); + log_info("> BT STATUS: չ SW5=%s, SW6=%s, SW7=%s, SW8=%s (=%02X)", + (m & 1) ? "" : "", + (m & 2) ? "" : "", + (m & 4) ? "" : "", + (m & 8) ? "" : "", + ext_relay_fault_mask()); + } else { + log_info("> BT STATUS: չ/δ"); + } } else if (cmd_is(cmd, "OTA")) { log_info("> BT OTA: BootLoader "); diff --git a/STM32F103_App1/Inc/main.h b/STM32F103_App1/Inc/main.h index bb6f007..71b1cde 100644 --- a/STM32F103_App1/Inc/main.h +++ b/STM32F103_App1/Inc/main.h @@ -31,7 +31,7 @@ extern UART_HandleTypeDef huart3; * ƽ̨мʱ */ /*===== ̼汾ţӡãİ汾ֻ =====*/ -#define FIRMWARE_VERSION_STR "1.7.0" +#define FIRMWARE_VERSION_STR "1.7.1" #define PRODUCT_WITH_FEED_SCALE 1 /* ĬϴӣEEPROM 172 ɸģ SCALE0/1 л */ diff --git a/STM32F103_App1/MDK-ARM/STM32F103rb_App1.uvprojx b/STM32F103_App1/MDK-ARM/STM32F103rb_App1.uvprojx index 7ec19fd..4fd5b28 100644 --- a/STM32F103_App1/MDK-ARM/STM32F103rb_App1.uvprojx +++ b/STM32F103_App1/MDK-ARM/STM32F103rb_App1.uvprojx @@ -617,6 +617,11 @@ 1 ..\HardWare\RELAY\relay.c + + ext_relay.c + 1 + ..\HardWare\RELAY\ext_relay.c + log.c 1 diff --git a/版本说明.md b/版本说明.md index fef4a78..f312bbe 100644 --- a/版本说明.md +++ b/版本说明.md @@ -1,5 +1,24 @@ # 版本说明 +## V1.7.1 —— 485 扩展继电器板支持(2026-09-04) + +### 新增 +- **485 级联扩展继电器板(4 路带反馈,Modbus RTU 从机 0x10)**,与称重秤共用 USART2 总线: + - 新模块 `HardWare/RELAY/ext_relay.c`:FC05 写线圈控制 + FC03 读 4 寄存器(输出/反馈/故障/版本) + - 命令闭环:FC05 回显校验(300ms 超时补发 1 次)→ 150ms 触点稳定 → FC03 校验输出位+故障位 → 应答平台 → 全状态上报 + - 空闲 2s 轮询兼在线检测,连续 3 次无应答判离线;离线期间命令直接拒绝;恢复应答自动上线补报 + - 反馈异常(故障位非 0)打 WARN 日志 +- **485 总线多从机改造**:USART2 收帧统一在 feedScaleProcess 收取后按站号分发(0x01 秤 / 0x10 扩展板); + 总线活动时间戳移到 uart.c(`rs485_bus_touch/last_tick`),秤查询与扩展板收发共用 50ms 静默保护 +- **平台标识符新增 sw5~sw8**:扩展板在线时状态大报文附加(4G/WiFi、带秤/无秤 4 条格式串均改); + 平台物模型需加 4 个布尔字段 +- **蓝牙命令新增 R5ON~R8OFF**;STATUS 显示扩展板在线状态与各路开关/故障位 +- 协议文档:`Docs/RS485扩展继电器协议.md`(从机板按此实现) + +### 说明 +- 扩展板不参与断电记忆恢复(从机上电默认全断);不挂扩展板时功能无影响(轮询无应答记离线日志) +- 无秤模式同样可用扩展板 + ## V1.7.0 —— 新硬件板型引脚重排(2026-09-04,不兼容老板,仓库迁至 shuichan_new) ### 背景