/* feed_scale.c - 485 喂料秤控制 * * 硬件: USART2 接 RS485 磅秤传感器,PA2(TX), PA3(RX),9600-8N1 * 协议: 查询命令 {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B} * 应答: 9 字节(地址1 + 功能码1 + 字节数1 + 数据4 + CRC2),重量值在数据字节[3..6],小端 float,单位 KG。 * 业务: 平台下发单次投喂量 onceWt(0.1KG 单位),继电器 1 吸合开始投喂, * 当 起始剩余重量 - 当前重量 >= 单次投喂量 时停止。 */ #include "feed_scale.h" #include "relay.h" #include "uart.h" #include "log.h" #include "network.h" #include "24c02.h" #include #include /* 查询秤的 Modbus 命令 */ static const uint8_t RS485_WEIGHT_CMD[8] = {0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B}; /* 运行状态 */ static FeedScaleState feed_state = FEED_IDLE; static uint16_t once_weight = 0; /* 单次投喂量,0.1KG 单位,从 EEPROM 读取 */ static uint16_t start_weight = 0; /* 本次投喂开始时的剩余重量 */ static uint16_t last_weight = 0; /* 最近一次有效净重(已去皮) */ static uint16_t last_raw_weight = 0; /* 最近一次 485 原始毛重(未去皮) */ static uint16_t reduced_weight = 0; /* 本次投喂减少的重量 */ static uint16_t zero_weight = 0; /* 去皮重量,EEPROM 掉电保持 */ /* 轮询计时 */ static uint32_t last_query_tick = 0; static uint32_t feed_start_tick = 0; /* 投喂开始时刻,用于超时保护 */ static uint8_t first_query_sent = 0; /* 首次上电查询已发送 */ static uint32_t await_reply_tick = 0; /* 查询发出时刻,0=无等待中的查询 */ static uint8_t first_query_reported = 0; /* 首次上电有效重量已上报 */ /* 重量异常增加计数 */ static uint8_t add_weight_count = 0; #define FEED_ADD_WEIGHT_LIMIT 20u /* 连续约 10s 重量反而增加,结束投喂 */ /* 单次投喂超时 10 分钟 */ #define FEED_TIMEOUT_MS (10u * 60u * 1000u) /* EEPROM 中保存去皮重量和单次投喂量的偏移(在 MqttInfoStr 之外) */ #define FEED_SCALE_ZERO_ADDR 120u #define FEED_SCALE_ONCEWT_ADDR 122u #define FEED_SCALE_INIT_FLAG_ADDR 124u #define FEED_SCALE_INIT_FLAG 0xA5u /* 本地 CRC16 校验 (Modbus) */ 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++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; } else { crc >>= 1; } } } return crc; } /* 485 原始帧 hex 打印(排查用:收到什么先原样打出来) */ static void feed_scale_dump_frame(const uint8_t *buf, uint16_t len) { char hex[3 * 24 + 1] = {0}; uint16_t n = len > 24 ? 24 : len; for (uint16_t i = 0; i < n; i++) { snprintf(hex + i * 3, 4, "%02X ", buf[i]); } log_info("> FEED: 485 收到 %d 字节: %s", (int)len, hex); } /* 从 485 应答中解析重量,应答 9 字节: 01 03 04 41 97 30 33 0B F6 */ static int feed_scale_parse_weight(const uint8_t *buf, uint16_t len, uint16_t *weight_100g) { /* 485 回声(自己发出去的查询被 RX 听到):静默丢弃,不打日志不报错 */ if (len == sizeof(RS485_WEIGHT_CMD) && memcmp(buf, RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD)) == 0) { return -7; } feed_scale_dump_frame(buf, len); /* 定位应答帧头(站号+03+字节数04):485 回声或前导垃圾可能混在帧头前面 */ int hdr = -1; for (int i = 0; i + 8 < len; i++) { if (buf[i] == RS485_WEIGHT_CMD[0] && buf[i+1] == 0x03 && buf[i+2] == 0x04) { hdr = i; break; } } if (hdr > 0) { log_info("> FEED: 跳过前导 %d 字节(回声/干扰)", hdr); buf += hdr; len -= hdr; } else if (hdr < 0 && len >= 9) { log_warn("> FEED: 解析失败-找不到帧头 01 03 04"); return -6; } if (len < 9) { log_warn("> FEED: 解析失败-帧长度过短(%d<9),可能波特率不对或线路干扰", (int)len); return -1; } if (buf[0] != RS485_WEIGHT_CMD[0]) { log_warn("> FEED: 解析失败-站号不符(收0x%02X 期望0x%02X)", buf[0], RS485_WEIGHT_CMD[0]); return -3; } if (buf[1] == (RS485_WEIGHT_CMD[1] | 0x80)) { log_warn("> FEED: 解析失败-秤返回异常帧(功能码0x%02X 异常码0x%02X),寄存器地址/数量不对?", buf[1], buf[2]); return -4; } if (buf[1] != RS485_WEIGHT_CMD[1]) { log_warn("> FEED: 解析失败-功能码不符(收0x%02X 期望0x%02X)", buf[1], RS485_WEIGHT_CMD[1]); return -5; } uint16_t calc_crc = crc16_modbus(buf, 7); /* 前 7 字节参与 CRC 计算 */ uint16_t recv_crc = (uint16_t)buf[8] << 8 | buf[7]; /* 最后两字节为 CRC */ if (calc_crc != recv_crc) { log_warn("> FEED: 解析失败-CRC 校验错误(calc=0x%04X recv=0x%04X),可能字节序/帧边界错位或干扰", calc_crc, recv_crc); return -2; } /* 大端 float: 数据字节[3]=MSB, [6]=LSB,参考 APP12 解析方式 */ float fweight; uint8_t *p = (uint8_t *)&fweight; p[0] = buf[6]; p[1] = buf[5]; p[2] = buf[4]; p[3] = buf[3]; /* 转成 0.1KG 整数单位,限制范围 */ int32_t wt = (int32_t)(fweight * 10.0f); if (wt < 0) wt = 0; if (wt > FEED_SCALE_WEIGHT_MAX) wt = FEED_SCALE_WEIGHT_MAX; *weight_100g = (uint16_t)wt; log_info("> FEED: 485 原始毛重=%.1fKG (0.1kg=%d)", fweight, *weight_100g); return 0; } /* 通过 USART2 发送 485 查询命令 */ static void feed_scale_send_query(void) { USART2_FlushRxBuf(); /* 只清软件缓冲(残留回声/垃圾),硬件接收常开不动 */ HAL_UART_Transmit(&huart2, (uint8_t *)RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD), 100); } /* 读取 485 返回帧 */ static int feed_scale_read_frame(uint8_t *buf, uint16_t max_len) { uint16_t len = USART2_GetFrame(buf, max_len); if (len == 0) return 0; return (int)len; } /* 上报当前状态到平台: 开关、重量、单次投料量、去皮重量。返回 0=已发出 */ static int feed_scale_report_weight(const char *header) { (void)header; /* 云平台物模型字段:weight=剩余料重(净重),zero=空载重量(去皮),onceWt=单次投料量 */ /* sw1 与 feeding 均上报继电器 1 状态,平台两个开关都能同步刷新 */ int rc = NET_publish_status( MqttInfoStr.Relay_State[1] ? 1 : 0, MqttInfoStr.Relay_State[1] ? 1 : 0, MqttInfoStr.Relay_State[2] ? 1 : 0, MqttInfoStr.Relay_State[3] ? 1 : 0, MqttInfoStr.Relay_State[4] ? 1 : 0, (float)zero_weight / 10.0f, (float)once_weight / 10.0f, (float)last_weight / 10.0f); log_info("> FEED: 净重=%.1fKG 单次投料=%.1fKG 去皮=%.1fKG", (float)last_weight / 10.0f, (float)once_weight / 10.0f, (float)zero_weight / 10.0f); return rc; } static uint32_t s_last_ok_tick = 0; static int s_last_err = 1; /* 上电默认"无应答" */ int feedScaleGetLastErr(void) { return s_last_err; } uint32_t feedScaleGetLastOkTick(void) { return s_last_ok_tick; } uint8_t g_feed_scale_on = 0; /* 加载产品模式:EEPROM 空白时用编译宏做默认值 */ void feed_scale_load_mode(void) { uint8_t v = 0xFF; eepromReadData(FEED_SCALE_MODE_ADDR, &v, 1); if (v > 1) v = PRODUCT_WITH_FEED_SCALE; g_feed_scale_on = v; log_info("> 产品模式: %s", v ? "称重投料(带秤)" : "四路继电器(无秤)"); } /* 蓝牙命令切换产品模式:写 EEPROM(重启后完全生效) */ void feed_scale_set_mode(uint8_t on) { on = on ? 1 : 0; g_feed_scale_on = on; eepromWriteData(FEED_SCALE_MODE_ADDR, &on, 1); } void feedScaleInit(void) { uint8_t tmp[2] = {0}; /* 检测是否是空白/新刷机单片机,是则强制写入默认 1.0KG */ eepromReadData(FEED_SCALE_INIT_FLAG_ADDR, tmp, 1); if (tmp[0] != FEED_SCALE_INIT_FLAG) { tmp[0] = FEED_SCALE_INIT_FLAG; eepromWriteData(FEED_SCALE_INIT_FLAG_ADDR, tmp, 1); zero_weight = 10; /* 默认去皮 1.0KG */ once_weight = 10; /* 默认单次投料 1.0KG */ tmp[0] = (uint8_t)(zero_weight & 0xFF); tmp[1] = (uint8_t)(zero_weight >> 8); eepromWriteData(FEED_SCALE_ZERO_ADDR, tmp, 2); tmp[0] = (uint8_t)(once_weight & 0xFF); tmp[1] = (uint8_t)(once_weight >> 8); eepromWriteData(FEED_SCALE_ONCEWT_ADDR, tmp, 2); log_info("> FEED: 首次初始化, 设置默认 去皮=1.0KG, 单次投料=1.0KG"); } else { eepromReadData(FEED_SCALE_ZERO_ADDR, tmp, 2); zero_weight = (uint16_t)tmp[0] | ((uint16_t)tmp[1] << 8); if (zero_weight == 0 || zero_weight > FEED_SCALE_WEIGHT_MAX) { zero_weight = 10; /* 默认去皮 1.0KG */ } eepromReadData(FEED_SCALE_ONCEWT_ADDR, tmp, 2); once_weight = (uint16_t)tmp[0] | ((uint16_t)tmp[1] << 8); if (once_weight == 0 || once_weight > FEED_SCALE_WEIGHT_MAX) { once_weight = 10; /* 默认单次投料 1.0KG */ } } if (g_feed_scale_on) { log_info("> FEED: 初始化, 去皮=%.1fKG, 单次投料=%.1fKG", (float)zero_weight / 10.0f, (float)once_weight / 10.0f); } } void feedScaleSetZero(uint16_t zero_100g) { if (zero_100g > FEED_SCALE_WEIGHT_MAX) return; zero_weight = zero_100g; /* 平台下发新空载重量后,用最新原始毛重重新计算剩余料重 */ if (last_raw_weight >= zero_weight) { last_weight = last_raw_weight - zero_weight; } else { last_weight = 0; } uint8_t tmp[2] = { (uint8_t)(zero_weight & 0xFF), (uint8_t)(zero_weight >> 8) }; eepromWriteData(FEED_SCALE_ZERO_ADDR, tmp, 2); log_info("> FEED: 设置去皮=%.1fKG, 重算净重=%.1fKG", (float)zero_weight / 10.0f, (float)last_weight / 10.0f); } void feedScaleSetOnceWeight(uint16_t once_wt_100g) { if (once_wt_100g > FEED_SCALE_WEIGHT_MAX) return; once_weight = once_wt_100g; uint8_t tmp[2] = { (uint8_t)(once_weight & 0xFF), (uint8_t)(once_weight >> 8) }; eepromWriteData(FEED_SCALE_ONCEWT_ADDR, tmp, 2); log_info("> FEED: 设置单次投料=%.1fKG", (float)once_weight / 10.0f); } uint16_t feedScaleGetOnceWeight(void) { return once_weight; } uint16_t feedScaleGetWeight(void) { return last_weight; } uint16_t feedScaleGetZero(void) { return zero_weight; } /* 获取最近一次 485 原始毛重(未去皮,0.1KG 单位) */ uint16_t feedScaleGetRawWeight(void) { return last_raw_weight; } FeedScaleState feedScaleGetState(void) { return feed_state; } uint16_t feedScaleGetReduced(void) { return reduced_weight; } void feedScaleStart(uint16_t once_wt_100g) { if (once_wt_100g < FEED_SCALE_ONCEWT_MIN || once_wt_100g > FEED_SCALE_ONCEWT_MAX) { log_warn("> FEED: 单次投料量无效=%.1fKG", (float)once_wt_100g / 10.0f); return; } if (feed_state == FEED_RUNNING) { log_warn("> FEED: 正在投喂中, 忽略本次请求"); return; } if (first_query_reported == 0) { log_warn("> FEED: 秤未就绪, 无法开始投喂"); return; } if (last_weight == 0) { log_warn("> FEED: 剩余重量为 0, 无法开始投喂"); return; } once_weight = once_wt_100g; start_weight = last_weight; /* 取最近一次重量作为起始重量 */ reduced_weight = 0; add_weight_count = 0; feed_start_tick = HAL_GetTick(); feed_state = FEED_RUNNING; relaySet(1, 1); eepromWriteData(102, &MqttInfoStr.Relay_State[0], 7); log_info("> FEED: 开始投喂 单次=%.1fKG, 起始=%.1fKG, 继电器1=开", (float)once_weight / 10.0f, (float)start_weight / 10.0f); /* 投喂开始时立即上报一次状态 */ feed_scale_report_weight("iot.prop.post"); } void feedScaleStop(void) { if (feed_state == FEED_IDLE) return; relaySet(1, 0); eepromWriteData(102, &MqttInfoStr.Relay_State[0], 7); feed_state = FEED_DONE; log_info("> FEED: 停止投喂, 已减少=%.1fKG", (float)reduced_weight / 10.0f); relayRequestReport(); } static void feed_scale_finish_error(const char *reason) { relaySet(1, 0); eepromWriteData(102, &MqttInfoStr.Relay_State[0], 7); feed_state = FEED_ERROR; log_warn("> FEED: 异常停止, %s", reason); relayRequestReport(); } void feedScaleProcess(void) { if (!g_feed_scale_on) return; uint8_t rx_buf[32]; int len; uint16_t cur_weight; int32_t diff; /* 触发 USART2 帧超时自动提交 */ USART2_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; log_info("> FEED: 首次查询已发送"); } } /* 查询发出后 1s 仍无任何应答帧:打日志(有无应答一目了然) */ if (await_reply_tick && (HAL_GetTick() - await_reply_tick) > 1000u) { await_reply_tick = 0; log_warn("> FEED: 查询无应答(秤未接/线序/波特率/站号不对)"); s_last_err = 1; } /* 读取 485 返回 */ len = feed_scale_read_frame(rx_buf, sizeof(rx_buf)); if (len > 0) { if (!(len == sizeof(RS485_WEIGHT_CMD) && memcmp(rx_buf, RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD)) == 0)) { await_reply_tick = 0; /* 收到非回声帧:本次查询有应答 */ } int prc = feed_scale_parse_weight(rx_buf, len, &cur_weight); if (prc != -7) { /* -7=回声帧:不影响状态显示 */ s_last_err = (prc == 0) ? 0 : (prc == -1 ? 2 : (prc == -2 ? 3 : (prc == -4 ? 4 : (prc == -6 ? 6 : 5)))); } if (prc == 0) s_last_ok_tick = HAL_GetTick(); if (prc == 0) { last_raw_weight = cur_weight; /* 保存原始毛重 */ last_weight = cur_weight > zero_weight ? (cur_weight - zero_weight) : 0; /* 净重 */ if (!first_query_reported) { /* 首次上电: 查询到一次有效重量后上报平台。 * 发布成功才置标志——发布被丢弃(AT忙)时下轮查询继续重试, * 避免重启后平台重量一直是 0 直到下次碰巧发出去 */ if (feed_scale_report_weight("iot.prop.post") == 0) { first_query_reported = 1; log_info("> FEED: 首次重量已上报"); } } if (feed_state == FEED_RUNNING) { /* 投喂期间:每次有效重量都上报平台,保证实时 */ feed_scale_report_weight("iot.prop.post"); /* 计算减少量,去零后比较 */ diff = (int32_t)start_weight - (int32_t)last_weight; if (diff < 0) diff = -diff; reduced_weight = (uint16_t)diff; log_info("> FEED: 投喂中 起始=%.1f 当前=%.1f 已减=%.1f 单次=%.1f", (float)start_weight / 10.0f, (float)last_weight / 10.0f, (float)reduced_weight / 10.0f, (float)once_weight / 10.0f); /* 条件1: 达到/超过单次投喂量,停止 */ if (reduced_weight >= once_weight) { log_info("> FEED: 达到目标投喂量, 停止"); feedScaleStop(); return; } /* 条件2: 重量反而增加,可能有人在加料,连续超过阈值后结束 */ if (last_weight > start_weight) { add_weight_count++; if (add_weight_count >= FEED_ADD_WEIGHT_LIMIT) { feed_scale_finish_error("重量反增"); return; } } else { add_weight_count = 0; } /* 条件3: 投喂超时 5 分钟 */ if (HAL_GetTick() - feed_start_tick > FEED_TIMEOUT_MS) { feed_scale_finish_error("投喂超时"); return; } } } } /* 清理已完成状态,让外部模块在下一循环前处理 */ if (feed_state == FEED_DONE || feed_state == FEED_ERROR) { /* 状态保持一次,由 relay 状态机上报 */ } }