shuichan_new/STM32F103_App1/HardWare/FEED_SCALE/feed_scale.c

503 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 "FreeRTOS.h"
#include "task.h"
#include "relay.h"
#include "ext_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;
/* HR_IN 缺料检测状态 */
static uint8_t hr_high_cnt = 0; /* 连续高电平计数(去抖) */
static uint8_t hr_empty_latched = 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 应答中解析重量,应答 9 字节: 01 03 04 41 97 30 33 0B F6 */
volatile uint32_t g_parse_cnt = 0; /* 解析被调次数(排障) */
static int feed_scale_parse_weight(const uint8_t *buf, uint16_t len, uint16_t *weight_100g)
{
g_parse_cnt++;
/* 485 回声(自己发出去的查询被 RX 听到):静默丢弃,不打日志不报错 */
if (len == sizeof(RS485_WEIGHT_CMD) &&
memcmp(buf, RS485_WEIGHT_CMD, sizeof(RS485_WEIGHT_CMD)) == 0) {
return -7;
}
/* 定位应答帧头(站号+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) {
buf += hdr;
len -= hdr;
} else if (hdr < 0 && len >= 9) {
return -6;
}
if (len < 9) {
return -1;
}
if (buf[0] != RS485_WEIGHT_CMD[0]) {
return -3;
}
if (buf[1] == (RS485_WEIGHT_CMD[1] | 0x80)) {
return -4;
}
if (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) {
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;
return 0;
}
/* 通过 USART2 发送 485 查询命令 */
static void feed_scale_send_query(void)
{
/* 与上一帧至少间隔 50ms从机 485 发送器释放/回声沉降需要时间。
* 当前单秤查询间隔 ≥1s 不会触发;以后总线加传感器、连续查询时保护生效 */
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 返回帧 */
static int feed_scale_read_frame(uint8_t *buf, uint16_t max_len)
{
return (int)USART2_PollFrame(buf, max_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);
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);
}
/* HR_IN(PA8): 红外对管缺料检测, 下拉输入(不接传感器=低=有料, 不影响投料) */
{
GPIO_InitTypeDef g = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
g.Pin = HR_IN_PIN;
g.Mode = GPIO_MODE_INPUT;
g.Pull = GPIO_PULLDOWN;
HAL_GPIO_Init(HR_IN_PORT, &g);
}
}
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();
}
/* HR_IN 缺料检测: 高电平(去抖≈500ms)判缺料 -> 停止投料并关闭继电器1;
* 与秤模式无关, 只要继电器1处于吸合就关; 只记日志不上报 */
static void feed_scale_hr_check(void)
{
if (HAL_GPIO_ReadPin(HR_IN_PORT, HR_IN_PIN) == HR_EMPTY_LEVEL) {
if (hr_high_cnt < HR_DEBOUNCE_CNT) { hr_high_cnt++; return; }
if (hr_empty_latched) return;
hr_empty_latched = 1;
if (feed_state == FEED_RUNNING) {
log_warn("> FEED: 检测到缺料(HR_IN高电平), 停止投料");
feedScaleStop();
} else if (MqttInfoStr.Relay_State[1]) {
log_warn("> FEED: 检测到缺料(HR_IN高电平), 关闭继电器1");
relaySet(1, 0);
eepromWriteData(102, &MqttInfoStr.Relay_State[0], 7);
relayRequestReport();
} else {
log_warn("> FEED: 检测到缺料(HR_IN高电平)");
}
} else {
hr_high_cnt = 0;
hr_empty_latched = 0;
}
}
/* 称重秤应答帧处理(站号 0x01); 返回 void, 原 feedScaleProcess 尾部逻辑平移至此 */
static void feed_scale_on_scale_frame(const uint8_t *rx_buf, uint16_t flen)
{
uint16_t cur_weight;
int32_t diff;
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=回声帧:不影响状态显示 */
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;
/* 条件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;
}
}
}
}
}
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 状态机上报 */
}
}