shuichan_old/V1.5/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c

471 lines
16 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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 <string.h>
#include <stdio.h>
/* 查询秤的 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+字节数04485 回声或前导垃圾可能混在帧头前面 */
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 状态机上报 */
}
}