V1.7.1: 485扩展继电器板支持(4路带反馈,Modbus RTU从机0x10)
- 新增ext_relay.c: FC05控制+FC03读状态,回显/反馈校验,2s轮询+离线判定 - 485总线多从机: 收帧按站号分发(0x01秤/0x10扩展板),uart.c加rs485_bus时间戳共用50ms静默保护 - 平台标识符sw5~sw8(扩展板在线才上报);蓝牙命令R5ON~R8OFF - 协议文档Docs/RS485扩展继电器协议.md(从机板按此实现)
This commit is contained in:
parent
dbf424bb96
commit
af5fbbe1e3
|
|
@ -66,11 +66,13 @@
|
||||||
| KEY1~4 | **PC6/PC7/PB14/PB15 上拉输入,功能预留** |
|
| KEY1~4 | **PC6/PC7/PB14/PB15 上拉输入,功能预留** |
|
||||||
| 取消 | PA8 手动模式开关(MANUAL_MODE 已删) |
|
| 取消 | 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. 云平台协议
|
## 4. 云平台协议
|
||||||
|
|
||||||
- 上报:`{"header":"iot.prop.post","body":{...}}`;4G 通道 AT 报文里引号用**字面三字符 `\22`**(C 源码写 `\\22`),不能丢反斜杠(踩过两次)
|
- 上报:`{"header":"iot.prop.post","body":{...}}`;4G 通道 AT 报文里引号用**字面三字符 `\22`**(C 源码写 `\\22`),不能丢反斜杠(踩过两次)
|
||||||
- 下行:`iot.prop.set`(控制终端继电器)、`iot.ota.upgrade.post`(OTA)
|
- 下行:`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
|
- 传感器标识符(采集终端):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 库)
|
- 协议解析助手在 `m_app/proto.c`(不用完整 JSON 库)
|
||||||
|
|
||||||
|
|
@ -89,7 +91,7 @@
|
||||||
|
|
||||||
RESET / INFO / MQTTINFO / STATUS / HELP(逐条中文)
|
RESET / INFO / MQTTINFO / STATUS / HELP(逐条中文)
|
||||||
SETMQTT id user pass(保存后自动重启)/ SETIP ip port / FACTORY / SETWIFI ssid pass / WIFIINFO / WIFICLEAR / WIFIRESET
|
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
|
SCALE/SCALE0/SCALE1(控制终端)/ GPS/GPS0/GPS1 / ENCLR(控制终端电能清零)/ HISTORY(控制终端重启记录)/ BLESET / LOGTRACE~LOGFATAL / OTA
|
||||||
|
|
||||||
## 7. OTA 架构(两终端相同)
|
## 7. OTA 架构(两终端相同)
|
||||||
|
|
|
||||||
|
|
@ -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,两者互不丢帧
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
#include "task.h"
|
#include "task.h"
|
||||||
#include "relay.h"
|
#include "relay.h"
|
||||||
|
#include "ext_relay.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "network.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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 最后一次收到 485 帧的时刻(发查询前的总线间隔保护用) */
|
|
||||||
static uint32_t last_frame_tick = 0;
|
|
||||||
|
|
||||||
/* 通过 USART2 发送 485 查询命令 */
|
/* 通过 USART2 发送 485 查询命令 */
|
||||||
static void feed_scale_send_query(void)
|
static void feed_scale_send_query(void)
|
||||||
{
|
{
|
||||||
/* 与上一帧至少间隔 50ms:从机 485 发送器释放/回声沉降需要时间。
|
/* 与上一帧至少间隔 50ms:从机 485 发送器释放/回声沉降需要时间。
|
||||||
* 当前单秤查询间隔 ≥1s 不会触发;以后总线加传感器、连续查询时保护生效 */
|
* 当前单秤查询间隔 ≥1s 不会触发;以后总线加传感器、连续查询时保护生效 */
|
||||||
uint32_t dt = HAL_GetTick() - last_frame_tick;
|
uint32_t dt = HAL_GetTick() - rs485_bus_last_tick();
|
||||||
if (dt < 50u) vTaskDelay(50u - dt);
|
if (dt < 50u) vTaskDelay(50u - dt);
|
||||||
USART2_FlushRxBuf(); /* 只清软件缓冲(残留回声/垃圾),硬件接收常开不动 */
|
USART2_FlushRxBuf(); /* 只清软件缓冲(残留回声/垃圾),硬件接收常开不动 */
|
||||||
HAL_UART_Transmit(&huart2, (uint8_t *)RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD), 100);
|
HAL_UART_Transmit(&huart2, (uint8_t *)RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD), 100);
|
||||||
|
rs485_bus_touch();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 读取 485 返回帧 */
|
/* 读取 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;
|
uint16_t cur_weight;
|
||||||
int32_t diff;
|
int32_t diff;
|
||||||
|
int len = (int)flen;
|
||||||
/* 原子取帧在 feed_scale_read_frame 内完成,无需再 Tick */
|
(void)len;
|
||||||
|
if (len > 0) { /* 供发查询前的总线间隔保护 */
|
||||||
/* 空闲时 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 prc = feed_scale_parse_weight(rx_buf, len, &cur_weight);
|
int prc = feed_scale_parse_weight(rx_buf, len, &cur_weight);
|
||||||
if (prc == 0) { await_reply_tick = 0; }
|
if (prc == 0) { await_reply_tick = 0; }
|
||||||
if (prc != -7) { /* -7=回声帧:不影响状态显示 */
|
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) {
|
if (feed_state == FEED_DONE || feed_state == FEED_ERROR) {
|
||||||
/* 状态保持一次,由 relay 状态机上报 */
|
/* 状态保持一次,由 relay 状态机上报 */
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
#include "relay.h"
|
#include "relay.h"
|
||||||
|
#include "ext_relay.h"
|
||||||
#include "proto.h"
|
#include "proto.h"
|
||||||
#include "24c02.h"
|
#include "24c02.h"
|
||||||
#include "FreeRTOS.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 (g_net_tx_busy) { log_info("> NET: 状态上报被丢弃 (发送忙)"); return -1; }
|
||||||
if (net_at_trylock() != 0) { log_info("> NET: 状态上报被丢弃 (AT 忙)"); 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 回退 */
|
/* ver 字段:固件版本号,平台据此可识别设备是否发生了 OTA 回退 */
|
||||||
/* sw1 与 feeding 都映射继电器 1 状态(无秤模式两者等效),
|
/* sw1 与 feeding 都映射继电器 1 状态(无秤模式两者等效),
|
||||||
* 两个字段都上报,平台的 sw1 开关和 feeding 状态才能同步刷新 */
|
* 两个字段都上报,平台的 sw1 开关和 feeding 状态才能同步刷新 */
|
||||||
/* 无秤模式(PRODUCT_WITH_FEED_SCALE=0)不上报 zero/onceWt 字段 */
|
/* 无秤模式(PRODUCT_WITH_FEED_SCALE=0)不上报 zero/onceWt 字段 */
|
||||||
if (s_backend == NET_BACKEND_4G) {
|
if (s_backend == NET_BACKEND_4G) {
|
||||||
if (g_feed_scale_on) {
|
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}}\"",
|
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);
|
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_esc);
|
||||||
} else {
|
} 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}}\"",
|
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, (double)hlw8032_get_energy_kwh());
|
MqttInfoStr.ClientID, (unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_esc, (double)hlw8032_get_energy_kwh());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
char json[256];
|
char json[256];
|
||||||
if (g_feed_scale_on) {
|
if (g_feed_scale_on) {
|
||||||
snprintf(json, sizeof(json),
|
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}}",
|
"{\"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);
|
(unsigned)MqttInfoStr.Ver, zero_kg, once_wt_kg, weight_kg, sw1, feeding, sw2, sw3, sw4, ext_plain);
|
||||||
} else {
|
} else {
|
||||||
snprintf(json, sizeof(json),
|
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}}",
|
"{\"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, (double)hlw8032_get_energy_kwh());
|
(unsigned)MqttInfoStr.Ver, zero_kg, sw1, feeding, sw2, sw3, sw4, ext_plain, (double)hlw8032_get_energy_kwh());
|
||||||
}
|
}
|
||||||
net_wifi_publish_up(json);
|
net_wifi_publish_up(json);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#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闲 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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#ifndef EXT_RELAY_H
|
||||||
|
#define EXT_RELAY_H
|
||||||
|
|
||||||
|
#include "main.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/* 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 */
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
#include "ext_relay.h"
|
||||||
#include "relay.h"
|
#include "relay.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "network.h"
|
#include "network.h"
|
||||||
|
|
@ -134,7 +135,7 @@ GPIO_PinState relayReadFb(uint8_t ch)
|
||||||
/* 设置指定继电器开关状态: 只写 GPIO 和 Relay_State, 不碰 EEPROM/网络 */
|
/* 设置指定继电器开关状态: 只写 GPIO 和 Relay_State, 不碰 EEPROM/网络 */
|
||||||
void relaySet(uint8_t ch, uint8_t on)
|
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;
|
MqttInfoStr.Relay_State[ch] = on ? 1 : 0;
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case 1: if (on) Relay1_ON; else Relay1_OFF; break;
|
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);
|
fb_before = relayReadFb(ch);
|
||||||
|
|
||||||
if (g_feed_scale_on) {
|
if (g_feed_scale_on) {
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
#include "app_common.h"
|
#include "app_common.h"
|
||||||
#include "user_cmd.h"
|
#include "user_cmd.h"
|
||||||
#include "relay.h"
|
#include "relay.h"
|
||||||
|
#include "ext_relay.h"
|
||||||
#include "network.h"
|
#include "network.h"
|
||||||
#include "led.h"
|
#include "led.h"
|
||||||
#include "adc.h"
|
#include "adc.h"
|
||||||
|
|
@ -232,6 +233,9 @@ static void app_task(void *arg)
|
||||||
|
|
||||||
/* ιÁϳÓÂÖѯÓëͶι±£»¤ */
|
/* ιÁϳÓÂÖѯÓëͶι±£»¤ */
|
||||||
feedScaleProcess();
|
feedScaleProcess();
|
||||||
|
|
||||||
|
/* 485 扩展继电器板: 命令下发/应答校验/2s 轮询(帧分发在 feedScaleProcess 内) */
|
||||||
|
ext_relay_process();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -457,3 +457,18 @@ const char *USART1_GetCommand(void)
|
||||||
|
|
||||||
return NULL;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,10 @@ void USART2_Tick(void); /*
|
||||||
uint16_t USART2_GetFrame(uint8_t *out, uint16_t out_size);
|
uint16_t USART2_GetFrame(uint8_t *out, uint16_t out_size);
|
||||||
uint16_t USART2_PollFrame(uint8_t *out, uint16_t out_size); /* 原子取帧(判静默+拷出一步完成) */
|
uint16_t USART2_PollFrame(uint8_t *out, uint16_t out_size); /* 原子取帧(判静默+拷出一步完成) */
|
||||||
void USART2_FlushRxBuf(void);
|
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;
|
extern volatile uint32_t g_uart2_rx_byte_cnt;
|
||||||
uint16_t USART2_GetRxIndex(void); /* USART2 收字节计数(485 排障) */ /* 清 USART2 软件接收缓冲(485 查询前调用,不动硬件) */ /* 从 USART2 取一帧原始字节 */
|
uint16_t USART2_GetRxIndex(void); /* USART2 收字节计数(485 排障) */ /* 清 USART2 软件接收缓冲(485 查询前调用,不动硬件) */ /* 从 USART2 取一帧原始字节 */
|
||||||
int USART1_CheckCommand(const char *cmd); /* 检查是否收到指定命令(不区分大小写) */
|
int USART1_CheckCommand(const char *cmd); /* 检查是否收到指定命令(不区分大小写) */
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
#include "ble_at.h"
|
#include "ble_at.h"
|
||||||
#include "app_common.h"
|
#include "app_common.h"
|
||||||
#include "relay.h"
|
#include "relay.h"
|
||||||
|
#include "ext_relay.h"
|
||||||
#include "24c02.h"
|
#include "24c02.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
#include "log.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, "R3OFF")) { relay_set(3, 0); }
|
||||||
else if (cmd_is(cmd, "R4ON")) { relay_set(4, 1); }
|
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, "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")) {
|
else if (cmd_is(cmd, "ALLON")) {
|
||||||
relay_set(1, 1); relay_set(2, 1); relay_set(3, 1); relay_set(4, 1);
|
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[2] ? "开" : "关",
|
||||||
MqttInfoStr.Relay_State[3] ? "开" : "关",
|
MqttInfoStr.Relay_State[3] ? "开" : "关",
|
||||||
MqttInfoStr.Relay_State[4] ? "开" : "关");
|
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")) {
|
else if (cmd_is(cmd, "OTA")) {
|
||||||
log_info("> BT OTA: 重启进入 BootLoader 检查升级");
|
log_info("> BT OTA: 重启进入 BootLoader 检查升级");
|
||||||
|
|
|
||||||
|
|
@ -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 切换) */
|
#define PRODUCT_WITH_FEED_SCALE 1 /* 默认带秤(EEPROM 172 可改,蓝牙 SCALE0/1 切换) */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -617,6 +617,11 @@
|
||||||
<FileType>1</FileType>
|
<FileType>1</FileType>
|
||||||
<FilePath>..\HardWare\RELAY\relay.c</FilePath>
|
<FilePath>..\HardWare\RELAY\relay.c</FilePath>
|
||||||
</File>
|
</File>
|
||||||
|
<File>
|
||||||
|
<FileName>ext_relay.c</FileName>
|
||||||
|
<FileType>1</FileType>
|
||||||
|
<FilePath>..\HardWare\RELAY\ext_relay.c</FilePath>
|
||||||
|
</File>
|
||||||
<File>
|
<File>
|
||||||
<FileName>log.c</FileName>
|
<FileName>log.c</FileName>
|
||||||
<FileType>1</FileType>
|
<FileType>1</FileType>
|
||||||
|
|
|
||||||
19
版本说明.md
19
版本说明.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)
|
## V1.7.0 —— 新硬件板型引脚重排(2026-09-04,不兼容老板,仓库迁至 shuichan_new)
|
||||||
|
|
||||||
### 背景
|
### 背景
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue