/* sensor.c - 485 传感器采集(采集终端核心模块) * * 硬件: USART2 接 485 总线,PA2(TX), PA3(RX),9600-8N1,DMA+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 #include #include 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;失败返回非 0(1=无应答 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~26(3 字节大端,单位 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; }