nongye/STM32F103_App1/HardWare/SENSOR/sensor.c

396 lines
14 KiB
C
Raw Permalink 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.

/* sensor.c - 485 传感器采集(采集终端核心模块)
*
* 硬件: USART2 接 485 总线PA2(TX), PA3(RX)9600-8N1DMA+IDLE 定帧接收。
* 协议: Modbus RTU 读保持寄存器(0x03),各传感器地址/寄存器见 sensor.h 表头注释。
* 业务: 每 10s 本地查询一遍传感器刷新缓存(供 OLED 实时显示);
* 上传调度锚定在"OTA 已确认"(联网就绪)时刻:+5s/+30s/+60s 各一次,之后每 5 分钟一次。
* 传感器无应答不影响其它传感器,离线传感器跳过不上报。
*/
#include "sensor.h"
#include "adc.h"
#include "uart.h"
#include "log.h"
#include "network.h"
#include "FreeRTOS.h"
#include "task.h"
#include <string.h>
#include <stdio.h>
#include <math.h>
sensor_data_t g_sensor;
uint16_t g_sensor_online = 0;
/*==== 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++) {
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : (crc >> 1);
}
}
return crc;
}
/* 发 8 字节查询并等一帧应答IDLE 定帧)。
* 返回帧长(>0)0=超时无帧。自动跳过自己发出去的 485 回声帧。 */
static int sensor_query(const uint8_t *req, uint8_t *resp, uint16_t max_len, uint32_t timeout_ms)
{
USART2_FlushRxBuf(); /* 清软件缓冲残留(回声/垃圾),硬件接收常开 */
HAL_UART_Transmit(&huart2, (uint8_t *)req, 8, 100);
uint32_t t0 = HAL_GetTick();
while ((HAL_GetTick() - t0) < timeout_ms) {
int n = USART2_PollFrame(resp, max_len);
if (n > 0) {
if (n == 8 && memcmp(resp, req, 8) == 0) continue; /* 回声,继续等真应答 */
return n;
}
vTaskDelay(2);
}
return 0;
}
static void dump_frame(const char *tag, const uint8_t *buf, int len)
{
char hex[3 * 32 + 1] = {0};
int n = len > 32 ? 32 : len;
for (int i = 0; i < n; i++) snprintf(hex + i * 3, 4, "%02X ", buf[i]);
log_info("> SENSOR: %s 收 %d 字节: %s", tag, len, hex);
}
/* 读保持寄存器addr 从机地址start 起始寄存器num 寄存器数。
* 成功返回 0 且 resp 为完整应答帧/长度 resp_len失败返回非 01=无应答 2=CRC错 3=帧异常) */
static int modbus_read(uint8_t addr, uint16_t start, uint8_t num,
uint8_t *resp, uint16_t *resp_len, const char *tag)
{
uint8_t req[8] = { addr, 0x03,
(uint8_t)(start >> 8), (uint8_t)(start & 0xFF),
0x00, num, 0x00, 0x00 };
uint16_t crc = crc16_modbus(req, 6);
req[6] = (uint8_t)(crc & 0xFF);
req[7] = (uint8_t)(crc >> 8);
uint16_t expect = (uint16_t)(5 + num * 2);
int n = sensor_query(req, resp, 40, 400);
if (n <= 0) {
log_warn("> SENSOR: %s 无应答", tag);
return 1;
}
dump_frame(tag, resp, n);
if (n != (int)expect || resp[1] != 0x03) {
log_warn("> SENSOR: %s 帧异常(长%d 期望%d)", tag, n, (int)expect);
return 3;
}
uint16_t calc = crc16_modbus(resp, (uint16_t)(n - 2));
uint16_t recv = (uint16_t)resp[n - 1] << 8 | resp[n - 2];
if (calc != recv) {
log_warn("> SENSOR: %s CRC错(calc=%04X recv=%04X)", tag, calc, recv);
return 2;
}
*resp_len = (uint16_t)n;
return 0;
}
/* 有符号温度解析:高字节 0xFF 为负(参考工程按补码处理) */
static float parse_temp(uint8_t hi, uint8_t lo)
{
if (hi == 0xFF) {
lo = (uint8_t)(~lo + 1);
return -(float)(lo / 10.0f);
}
return (float)((hi * 256 + lo) / 10.0f);
}
/*==== 溶解氧 DO59 专用UNESCO 公式,饱和度%换算 mg/L盐度0====*/
static float do59_mg_l(float per, float temp, float pressure)
{
float T = 273.15f + temp;
float lnX1 = -173.4292f + 249.6339f * (100.0f / T) + 143.3483f * logf(T / 100.0f) + (-21.8492f) * (T / 100.0f);
float X1 = expf(lnX1);
float U = powf(10.0f, 8.10765f - 1750.286f / (235.0f + temp));
float Phmg = pressure * 760.0f / 101.325f;
float X2 = (Phmg - U) / (760.0f - U);
return per * X1 * X2 * 1.4276f / 100.0f;
}
/* DO59 两阶段:先"启动测量"(0x2500),下次再"读数据"(0x2600 温度+饱和度)。
* 返回 0=本次读到有效数据1=无数据(未启动完成/无应答/校验错) */
static int do59_read(void)
{
static uint8_t started = 0;
uint8_t resp[16];
uint16_t rlen = 0;
int rc;
if (!started) {
rc = modbus_read(0xFF, 0x2500, 1, resp, &rlen, "溶解氧启动");
if (rc == 0) {
started = 1;
log_info("> SENSOR: 溶解氧已启动测量");
}
return 1;
}
rc = modbus_read(0xFF, 0x2600, 4, resp, &rlen, "溶解氧");
if (rc != 0) {
started = 0; /* 下次重新启动 */
return 1;
}
float temp, sat;
memcpy(&temp, &resp[3], 4); /* 温度 float 小端 */
memcpy(&sat, &resp[7], 4); /* 饱和度% float 小端 */
float press = ((g_sensor_online & (1u << 1)) && g_sensor.air_press > 0.0f)
? g_sensor.air_press : 101.325f; /* 大气压传感器在线用实测,否则标准大气压 */
g_sensor.water_t = temp;
g_sensor.do_sat = sat;
g_sensor.do_mgl = do59_mg_l(sat, temp, press);
return 0;
}
/*==== 各传感器读取 + 解码0=成功)====*/
static int read_phec(void) /* 0x01 水体PH-EC */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x01, 0x0000, 2, r, &rl, "PH-EC") != 0) return 1;
g_sensor.ph = (float)(r[3] * 256 + r[4]) / 10.0f;
g_sensor.ec = (float)(r[5] * 256 + r[6]) / 10.0f;
return 0;
}
static int read_airpress(void) /* 0x02 大气压温湿度(王子壳),读 12 个寄存器 */
{
uint8_t r[40]; uint16_t rl;
if (modbus_read(0x02, 0x0000, 12, r, &rl, "大气压") != 0) return 1;
g_sensor.air_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
g_sensor.air_t = parse_temp(r[5], r[6]);
/* 气压在字节 24~263 字节大端,单位 0.01mbar */
uint32_t ph = ((uint32_t)r[24] << 16) | ((uint32_t)r[25] << 8) | r[26];
g_sensor.air_press = ph / 100.0f / 10.0f; /* -> kPa */
return 0;
}
static int read_byx(void) /* 0x06 大气百叶箱,读 9 个寄存器 */
{
uint8_t r[32]; uint16_t rl;
if (modbus_read(0x06, 0x0000, 9, r, &rl, "百叶箱") != 0) return 1;
g_sensor.by_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
g_sensor.by_t = parse_temp(r[5], r[6]);
g_sensor.by_co2 = (uint16_t)(r[13] * 256 + r[14]);
g_sensor.by_hv = (uint16_t)(r[19] * 256 + r[20]);
return 0;
}
static int read_leaf(void) /* 0x07 叶面温湿度 */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x07, 0x0020, 2, r, &rl, "叶面温湿") != 0) return 1;
g_sensor.leaf_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
g_sensor.leaf_t = parse_temp(r[5], r[6]);
return 0;
}
static int read_soilth(void) /* 0x08 土壤温湿度 */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x08, 0x0012, 2, r, &rl, "土壤温湿") != 0) return 1;
g_sensor.soil_rh = (float)(r[3] * 256 + r[4]) / 10.0f;
g_sensor.soil_t = parse_temp(r[5], r[6]);
return 0;
}
static int read_npk(void) /* 0x09 土壤氮磷钾 */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x09, 0x001E, 3, r, &rl, "氮磷钾") != 0) return 1;
g_sensor.soil_n = (float)(r[3] * 256 + r[4]);
g_sensor.soil_p = (float)(r[5] * 256 + r[6]);
g_sensor.soil_k = (float)(r[7] * 256 + r[8]);
return 0;
}
static int read_soilph(void) /* 0x0A 土壤PH */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x0A, 0x0006, 1, r, &rl, "土壤PH") != 0) return 1;
g_sensor.soil_ph = (float)(r[3] * 256 + r[4]) / 100.0f;
return 0;
}
static int read_soilec(void) /* 0x0B 土壤电导率 */
{
uint8_t r[16]; uint16_t rl;
if (modbus_read(0x0B, 0x0015, 1, r, &rl, "土壤EC") != 0) return 1;
g_sensor.soil_ec = (float)(r[3] * 256 + r[4]);
return 0;
}
/*==== 轮询调度 ====*/
typedef struct {
const char *name; /* 日志/OLED 用名 */
int (*read)(void); /* 读取+解码0=成功 */
void (*report)(void); /* 组包上报 */
} sensor_entry_t;
static void report_phec(void) { char b[64]; snprintf(b, sizeof(b), "\"PH\":%.1f,\"EC\":%.1f", g_sensor.ph, g_sensor.ec); NET_publish_props(b); }
static void report_air(void) { char b[96]; snprintf(b, sizeof(b), "\"BS_T\":%.2f,\"BS_RH\":%.2f,\"BS_DO\":%.2f", g_sensor.air_t, g_sensor.air_rh, g_sensor.air_press); NET_publish_props(b); }
static void report_do(void) { char b[64]; snprintf(b, sizeof(b), "\"DS_T\":%.2f,\"DO\":%.2f", g_sensor.water_t, g_sensor.do_mgl); NET_publish_props(b); }
static void report_byx(void) { char b[112]; snprintf(b, sizeof(b), "\"BY_TP\":%.1f,\"BY_RH\":%.1f,\"BY_HV\":%u,\"BY_CO2\":%u", g_sensor.by_t, g_sensor.by_rh, g_sensor.by_hv, g_sensor.by_co2); NET_publish_props(b); }
static void report_leaf(void) { char b[80]; snprintf(b, sizeof(b), "\"YSMD_TP\":%.1f,\"YSMD_RH\":%.1f", g_sensor.leaf_t, g_sensor.leaf_rh); NET_publish_props(b); }
static void report_sth(void) { char b[80]; snprintf(b, sizeof(b), "\"SOIL_TP\":%.1f,\"SOIL_RH\":%.1f", g_sensor.soil_t, g_sensor.soil_rh); NET_publish_props(b); }
static void report_npk(void) { char b[112]; snprintf(b, sizeof(b), "\"SOIL_NV\":%.1f,\"SOIL_PV\":%.1f,\"SOIL_KV\":%.1f", g_sensor.soil_n, g_sensor.soil_p, g_sensor.soil_k); NET_publish_props(b); }
static void report_sph(void) { char b[48]; snprintf(b, sizeof(b), "\"SOIL_PH\":%.2f", g_sensor.soil_ph); NET_publish_props(b); }
static void report_sec(void) { char b[48]; snprintf(b, sizeof(b), "\"SOIL_RV\":%d", (int)g_sensor.soil_ec); NET_publish_props(b); }
static const sensor_entry_t s_table[] = {
{ "PH-EC", read_phec, report_phec },
{ "大气压", read_airpress, report_air },
{ "溶解氧", do59_read, report_do },
{ "百叶箱", read_byx, report_byx },
{ "叶面温湿", read_leaf, report_leaf },
{ "土壤温湿", read_soilth, report_sth },
{ "氮磷钾", read_npk, report_npk },
{ "土壤PH", read_soilph, report_sph },
{ "土壤EC", read_soilec, report_sec },
};
#define SENSOR_COUNT (sizeof(s_table) / sizeof(s_table[0]))
static int online_count(void)
{
int n = 0;
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
if (g_sensor_online & (1u << i)) n++;
}
return n;
}
/* 本地读取周期10s只更新缓存/OLED不上报 */
#define SENSOR_READ_MS 10000u
/* 上传调度:锚定"OTA 已确认"(联网就绪),+5s/+30s/+60s 暖机三次,之后每 SENSOR_CYCLE_MS */
static const uint16_t s_pub_warmup_s[3] = {5, 30, 60};
static uint32_t s_read_tick = 0;
static uint32_t s_pub_anchor = 0; /* 0=未锚定OTA 未确认/未联网) */
static uint8_t s_pub_stage = 0; /* 0~2=暖机阶段3=常规 5 分钟周期 */
static uint32_t s_pub_last = 0; /* 常规阶段上次上传时刻 */
void sensor_init(void)
{
log_info("> SENSOR: 初始化, %d 种传感器, 10s 本地刷新, 联网后 +5s/+30s/+60s 上传, 之后每 %u 分钟",
(int)SENSOR_COUNT, (unsigned)(SENSOR_CYCLE_MS / 60000u));
}
/* 由 app_task 在"OTA 已确认"时调用,锚定上传计划 */
void sensor_notify_ota_confirmed(void)
{
if (s_pub_anchor == 0) {
s_pub_anchor = HAL_GetTick();
s_pub_stage = 0;
log_info("> SENSOR: 上传计划已锚定(+5s/+30s/+60s, 之后每 %u 分钟)",
(unsigned)(SENSOR_CYCLE_MS / 60000u));
}
}
/* 全量查询一遍传感器,只更新数值缓存和在线掩码 */
static void scan_sensors(void)
{
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
if (s_table[i].read() == 0) {
g_sensor_online |= (1u << i);
} else {
g_sensor_online &= ~(1u << i);
}
}
}
/* 上传当前缓存的在线传感器数据 + 电池电量 */
static void publish_all(void)
{
if (!g_net_mqtt_ready) return;
for (uint16_t i = 0; i < SENSOR_COUNT; i++) {
if (g_sensor_online & (1u << i)) {
s_table[i].report();
vTaskDelay(300); /* 上报之间留间隙,避免 AT 通道挤压 */
}
}
/* 电池电量随每轮上传一次 */
{
int bat = battery_percent();
char b[32];
snprintf(b, sizeof(b), "\"BAT_VAL\":%d", bat);
NET_publish_props(b);
log_info("> SENSOR: 电池电量 %d%%", bat);
}
}
void sensor_process(void)
{
uint32_t now = HAL_GetTick();
/* 10s 本地读取刷新未联网也读OLED 实时显示用) */
if ((now - s_read_tick) >= SENSOR_READ_MS) {
s_read_tick = now;
scan_sensors();
}
/* 上传调度 */
if (s_pub_anchor == 0) return;
if (s_pub_stage < 3) {
/* 暖机阶段:+5s/+30s/+60s */
if ((now - s_pub_anchor) >= (uint32_t)s_pub_warmup_s[s_pub_stage] * 1000u) {
publish_all();
s_pub_stage++;
if (s_pub_stage == 3) s_pub_last = now;
}
} else if ((now - s_pub_last) >= SENSOR_CYCLE_MS) {
s_pub_last = now;
publish_all();
log_info("> SENSOR: 周期上传完成, 在线 %d/%d", online_count(), (int)SENSOR_COUNT);
}
}
/*==== OLED 摘要(每 5s 翻一页一页一路传感器R2/R3 两行)====
* 行宽约束: 128px全角 16px/字,半角 8px/字,每行不超过 128px */
int sensor_oled_summary(char *l1, char *l2, int cap)
{
int total = online_count();
if (total == 0) return 0;
int want = (int)((HAL_GetTick() / 5000u) % (uint32_t)total);
int idx = -1;
for (int i = 0, seen = 0; i < (int)SENSOR_COUNT; i++) {
if (g_sensor_online & (1u << i)) {
if (seen == want) { idx = i; break; }
seen++;
}
}
if (idx < 0) return 0;
l1[0] = l2[0] = 0;
switch (idx) {
case 0: snprintf(l1, cap, "水体PH:%.1f", g_sensor.ph);
snprintf(l2, cap, "电导率:%.0fuS/cm", g_sensor.ec); break;
case 1: snprintf(l1, cap, "大气温度:%.1fC", g_sensor.air_t);
snprintf(l2, cap, "湿%.0f%% 压%.0fkPa", g_sensor.air_rh, g_sensor.air_press); break;
case 2: snprintf(l1, cap, "溶解氧:%.2fmg/L", g_sensor.do_mgl);
snprintf(l2, cap, "水温%.1fC 饱%.0f%%", g_sensor.water_t, g_sensor.do_sat); break;
case 3: snprintf(l1, cap, "温%.1fC 湿%.0f%%", g_sensor.by_t, g_sensor.by_rh);
snprintf(l2, cap, "CO2:%u 光:%u", g_sensor.by_co2, g_sensor.by_hv); break;
case 4: snprintf(l1, cap, "叶面温度:%.1fC", g_sensor.leaf_t);
snprintf(l2, cap, "叶面湿度:%.1f%%", g_sensor.leaf_rh); break;
case 5: snprintf(l1, cap, "土壤温度:%.1fC", g_sensor.soil_t);
snprintf(l2, cap, "土壤湿度:%.1f%%", g_sensor.soil_rh); break;
case 6: snprintf(l1, cap, "氮:%.0f 磷:%.0f", g_sensor.soil_n, g_sensor.soil_p);
snprintf(l2, cap, "钾:%.0fmg/kg", g_sensor.soil_k); break;
case 7: snprintf(l1, cap, "土壤PH:%.2f", g_sensor.soil_ph); break;
case 8: snprintf(l1, cap, "土壤电导率:%.0f", g_sensor.soil_ec); break;
default: return 0;
}
return 1;
}