/* app_loop.c - FreeRTOS 任务调度封装 * * 任务划分(裸机主循环轮询项拆分而来): * net_task(优先级3, 栈3KB) - 网络状态机 NET_process:4G/WiFi 建链、收发、OTA 下载 * app_task(优先级2, 栈2KB) - 业务:继电器命令/反馈状态机、投喂秤、状态上报、OTA 确认 * sys_task(优先级1, 栈1KB) - 系统:LED、电量、蓝牙调试命令、运行时长打印 * idle 钩子 - 喂 IWDG(只要调度器正常,看门狗就不会断粮; * 有任务死循环占 CPU 时 idle 饿死 → 复位,正是想要的行为) * * 共享资源互斥: * g_net_at_mutex - NET_publish_* 与长 AT 会话(建链/重连)互斥,拿锁失败直接丢弃 * s_storage_mutex - EEPROM(软件I2C) / W25Q64(软件SPI) 跨任务访问互斥(storage_lock) * log 输出 - log.c 内部互斥,多任务日志不交织 */ #include "app_loop.h" #include "app_common.h" #include "user_cmd.h" #include "relay.h" #include "ext_relay.h" #include "network.h" #include "led.h" #include "adc.h" #include "feed_scale.h" #include "iic.h" #include "24c02.h" #include "uart.h" #include "log_cn.h" #include "ble_at.h" #include "hlw8032.h" #include "oled.h" #include "reset_log.h" #include "../W25Q64/w25q64.h" #include "FreeRTOS.h" #include "task.h" #include "semphr.h" /*==== 存储总线互斥锁(EEPROM/W25Q64 共用一把递归锁)====*/ static SemaphoreHandle_t s_storage_mutex = NULL; void storage_lock_init(void) { s_storage_mutex = xSemaphoreCreateRecursiveMutex(); } void storage_lock(void) { if (s_storage_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { xSemaphoreTakeRecursive(s_storage_mutex, portMAX_DELAY); } } void storage_unlock(void) { if (s_storage_mutex && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { xSemaphoreGiveRecursive(s_storage_mutex); } } /*==== 业务模块初始化(调度器启动前调用,与裸机版一致)====*/ static volatile uint8_t s_qr_showing = 0; /* 开机二维码页显示中: 抑制预调度周期刷新 */ /*==== 开机联网方式选择(V1.7.1 新板按键): 10s 倒计时 ==== * KEY1(PC6)=4G KEY2(PC7)=WiFi固定热点 KEY3(PB14)=SmartConfig 配网, 低电平=按下; * 超时未按 = EEPROM 记忆的上次选择(首次默认 4G); 选择变化才写 EEPROM(偏移130); * 选定后不再自动切换通道, 连不上原地重试 */ /* 按键扫描: 返回 0=无 1~3=键号; 连续 2 次(约100ms)确认消抖 */ static int key_scan_once(void) { static uint8_t last = 0, cnt = 0; uint8_t cur = 0; if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_6) == GPIO_PIN_RESET) cur = 1; else if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_7) == GPIO_PIN_RESET) cur = 2; else if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_14) == GPIO_PIN_RESET) cur = 3; if (cur && cur == last) { cnt++; if (cnt >= 2) { cnt = 0; last = 0; return cur; } } else { cnt = 0; last = cur; } return 0; } static uint8_t net_mode_select(void) { uint8_t saved = NET_MODE_4G; eepromReadData(EEPROM_NETMODE_ADDR, &saved, 1); if (saved > NET_MODE_WIFI_SMART) saved = NET_MODE_4G; uint8_t mode = saved; static const char *const names[3] = { "4G", "WiFi热点", "智能配网" }; oled_clear(); oled_locate(0, 1); oled_print("选择联网方式"); oled_locate(1, 0); oled_print("1:4G 2:WiFi热点"); oled_locate(2, 0); oled_print("3:智能配网"); uint32_t start = HAL_GetTick(); int last_sec = -1; int key = 0; for (;;) { uint32_t elapsed = HAL_GetTick() - start; int remain = (int)(10000u - elapsed) / 1000; if (remain < 0) remain = 0; if (remain != last_sec) { char buf[24]; last_sec = remain; snprintf(buf, sizeof(buf), "上次:%s %2ds", names[mode], remain); oled_locate(3, 0); oled_print_line(buf); } key = key_scan_once(); if (key) break; if (elapsed >= 10000u) break; bg_delay(50); } if (key) { mode = (uint8_t)(key - 1); /* 键1->0(4G) 键2->1(WiFi固定) 键3->2(配网) */ if (mode != saved) { eepromWriteData(EEPROM_NETMODE_ADDR, &mode, 1); /* 记忆本次选择 */ } } log_info("> NET: 联网方式选择: %s (%s)", names[mode], key ? "按键选定" : "超时默认"); oled_locate(3, 0); oled_print_line("已选:"); oled_print((char *)names[mode]); bg_delay(800); oled_clear(); return mode; } void app_init(void) { log_info(""); log_info("* * * * * * * * * * * * * * * * * *"); log_info("* 版本: %s (RTOS) *", FIRMWARE_VERSION_STR); log_info("* * * * * * * By Helei * * * *"); /* EEPROM 中的 MQTT/版本配置 */ iicInit(); eepromReadInfo(); log_load_level(); /* 恢复保存的日志等级 */ log_info("> 服务器: %s:%d", MqttInfoStr.ServerIP, MqttInfoStr.ServerPort); if (g_mqtt_configured) { log_info("> 客户端ID: %s", MqttInfoStr.ClientID); log_info("> 用户名: %s", MqttInfoStr.Username); } else { log_warn("> 设备身份未配置:请用蓝牙发送 SETMQTT <用户名> <密码>"); } log_info("> 版本序号: %u", MqttInfoStr.Ver); /* W25Q64 外部 Flash,用于 OTA 固件暂存 */ W25Q64_Init(); /* 生成本次重启记录(原因+上轮运行时长)并打印最近 10 条 */ reset_log_boot(); /* 继电器 GPIO(控制输出 + 反馈输入) */ relayInit(); /* 产品模式(带秤/无秤,EEPROM 可切,默认跟编译宏) */ feed_scale_load_mode(); gps_load_mode(); /* GPS 定位开关(EEPROM 173,蓝牙 GPS0/1 切换) */ /* RS485 喂料秤控制模块 */ feedScaleInit(); /* LED(LED2 心跳同时兼任外部看门狗 TPL5010 喂狗) */ ledInit(); /* ADC 电量检测 */ adcInit(); update_batter(); /* HLW8032 电能计量(UART5 PD2,只累计电量) */ hlw8032_init(); /* OLED 屏幕(128x64 + GB2312 字库 IC) */ oled_init(); if (g_mqtt_configured) { /* 开机显示设备 ID 二维码 10s(内容=ClientID, 右侧附 ID 文本), 便于现场扫码绑定 */ s_qr_showing = 1; oled_show_qrcode(MqttInfoStr.ClientID); log_info("> OLED: 开机显示设备ID二维码 10s"); bg_delay(1000); s_qr_showing = 0; oled_clear(); } if (g_feed_scale_on) { oled_locate(1, 1); oled_print("智能水产投料机"); } else { oled_locate(1, 1); oled_print("继电器控制终端"); } oled_locate(2, 2); oled_print("V" FIRMWARE_VERSION_STR " RTOS"); if (!g_mqtt_configured) { oled_locate(3, 1); oled_print("等待蓝牙配置"); } log_info("> OLED: 初始化完成"); /* 网络模块初始化:按 EEPROM 记忆的上次成功方式优先(4G 或 WiFi)。 * 未配置身份时跳过:避免 4G/WiFi 用空身份空转 */ /* 已配置身份: 开机 10s 按键选择联网方式(超时=EEPROM 记忆/默认 4G), 选定后不再自动切换 */ if (g_mqtt_configured) { NET_SetForceMode(net_mode_select()); NET_init(); } /* 启动各路串口接收 */ USART2_StartRx(); /* RS485 喂料秤 */ USART3_StartRx(); /* 4G MQTT 消息,DMA+IDLE */ USART1_StartRx(); /* 蓝牙调试命令 */ /* 上电把 HLK-B40 蓝牙名称设置为 MQTT ClientID,多台设备可区分 */ ble_at_init(); ble_setup_name(); } /*==== net_task:网络状态机(4G/WiFi 建链、收发、OTA)====*/ static void net_task(void *arg) { (void)arg; for (;;) { if (g_mqtt_configured) NET_process(); /* 未配置身份:空转等待蓝牙 SETMQTT 后重启 */ vTaskDelay(5); } } /*==== app_task:业务逻辑(继电器/秤/上报/OTA 确认)====*/ static void app_task(void *arg) { (void)arg; /* 主循环状态变量 */ static uint8_t ota_confirmed = 0; static uint8_t ota_simulate_fail = 0; /* =1测试:模拟 OTA 确认失败(验证回退) */ static uint8_t ota_fail_logged = 0; static uint32_t ota_confirm_tick = 0; static uint8_t ota_report_pending = 0; static uint32_t ota_report_tick = 0; static uint32_t mqtt_ready_tick = 0; static uint8_t mqtt_ready_recorded = 0; static uint8_t mqtt_first_status_reported = 0; for (;;) { /* 记录 MQTT 首次就绪时刻 */ if (!mqtt_ready_recorded && g_net_mqtt_ready) { mqtt_ready_recorded = 1; mqtt_ready_tick = HAL_GetTick(); } /* MQTT 首次就绪后:先按 EEPROM 恢复继电器上次状态(断电记忆), * 再上报一次完整状态,让平台刷新为真实状态 */ if (g_net_mqtt_ready && !mqtt_first_status_reported) { mqtt_first_status_reported = 1; relayRestoreState(); relayStatueUpdata(); { /* 每次重启联网后上报一次代码版本:"1.5.0:15"(字符串版本 + EEPROM 版本序号) */ char verbuf[24]; snprintf(verbuf, sizeof(verbuf), "%s:%u", FIRMWARE_VERSION_STR, (unsigned)MqttInfoStr.Ver); NET_publish_version(verbuf); } log_info("> Main: MQTT Ready 后首次状态上报"); ota_confirm_tick = HAL_GetTick(); } /* 首次发布成功后延迟 9s 再 OTA 确认(避开开机上报风暴:pow/ICCID/状态/电能) */ if (!ota_confirmed && g_net_mqtt_ready && g_net_first_publish_ok && (HAL_GetTick() - ota_confirm_tick) > 9000) { if (ota_simulate_fail) { if (!ota_fail_logged) { ota_fail_logged = 1; log_info("> OTA 确认跳过(模拟失败,用于回退测试)"); } /* 不置位 ota_confirmed,让 1 分钟健康超时触发复位 */ } else { ota_confirmed = 1; if (NET_ota_confirm() < 0) { /* <0 才是未送达;1=无挂起 OTA 属正常 */ ota_report_pending = 1; /* 通知未送达,30s 后重试 */ ota_report_tick = HAL_GetTick(); log_warn("> OTA 确认通知未送达,稍后重试"); } else { log_info("> OTA 已确认(MQTT Ready + 首次发布成功 + 延迟9s)"); } } } /* OTA 确认通知未送达:每 30s 重试直到成功(本地确认标志已落盘,仅补平台通知) */ if (ota_report_pending && g_net_mqtt_ready && (HAL_GetTick() - ota_report_tick) > 30000) { ota_report_tick = HAL_GetTick(); if (NET_ota_confirm() >= 0) { ota_report_pending = 0; log_info("> OTA 确认通知重试成功"); } } /* MQTT 就绪后 1 分钟仍未首次 MPUB,主动复位 */ if (!ota_confirmed && mqtt_ready_recorded && (HAL_GetTick() - mqtt_ready_tick) > OTA_HEALTH_TIMEOUT_MS) { log_warn("> OTA 健康超时: MQTT Ready 但 1 分钟内无 MPUB,复位"); reset_log_mark(RSN_OTA_HEALTH); W25Q64_OTA_IncTrial(); /* 软件复位 BL 不计次,这里自行消耗一次试错 */ HAL_Delay(100); NVIC_SystemReset(); } /* 待配置模式(未灌身份):继电器状态机/秤轮询等业务全部暂停, * 只保留蓝牙命令/LED看门狗/断电检测(sys_task 侧),安静等待 SETMQTT; * OTA 下载期间(g_ota_active)同样暂停,升级时专注升级 */ if (g_mqtt_configured && !g_ota_active) { /* 继电器命令响应状态机 */ relayCmdProcess(); /* 延迟状态上报 */ if (relay_report_pending && (HAL_GetTick() - relay_report_tick) > 100) { relay_report_pending = 0; relayStatueUpdata(); } /* 喂料秤轮询与投喂保护 */ feedScaleProcess(); /* 485 扩展继电器板: 命令下发/应答校验/2s 轮询(帧分发在 feedScaleProcess 内) */ ext_relay_process(); } vTaskDelay(10); } } /*==== OLED 界面刷新(1s 一次) * 已联网:电压电流/功率电能/开关状态/网络+运行时间 * 未联网:按 g_net_ui_phase 显示 4G/WiFi 联网进度(WiFi 带倒计时) * 防烧屏:每 10s 内容整体右移 0/2/4px 轮换 ====*/ static void oled_home_refresh(uint32_t up_ms); /* 调度器启动前的阻塞建链阶段(app_init 里的 4G 初始化), * sys_task 还没跑,由 bg_delay 每 500ms 调一次刷新联网进度页 */ void app_oled_presched_tick(void) { static uint32_t last = 0; if (s_qr_showing) return; if (HAL_GetTick() - last < 500) return; last = HAL_GetTick(); oled_home_refresh(0); } /* 写一整行(带防烧屏偏移):先写空格清掉左移腾出的列,再从偏移处写内容 */ static void oled_row(uint8_t row, uint8_t xoff, const char *s) { /* 按内容宽度钳位:内容宽就少移,保证不超出右边界 */ uint8_t w = oled_text_w(s); if (xoff > 0 && (uint16_t)xoff + w > 128) xoff = 128 - w; if (xoff) { oled_locate(row, 0); for (uint8_t c = 0; c < xoff; c += 8) oled_print(" "); /* 清空左侧腾出的列,防残留 */ oled_locate(row, 0); } oled_locate(row, 0); oled_xoff(xoff); oled_print_line(s); } static void oled_home_refresh(uint32_t up_ms) { char buf[24]; /* 防烧屏:每 10s 换一档,左右往返(0→24→0,按内容宽度自动钳位) */ uint8_t phase = (uint8_t)((HAL_GetTick() / 10000u) % 8u); uint8_t xoff = (phase <= 4u) ? phase * 6u : (8u - phase) * 6u; if (!g_mqtt_configured) { /*---- 待配置界面:身份未灌入,提示蓝牙操作 ----*/ oled_row(0, xoff, "未配置身份"); oled_row(1, xoff, "蓝牙APP发送:"); oled_row(2, xoff, "SETMQTT ID"); oled_row(3, xoff, "用户 密码"); } else if (g_net_ui_phase == NET_UI_OTA) { /*---- OTA 下载进度页(优先级最高,4G OTA 时 MQTT 仍在线,须先于已联网界面) ----*/ int pct = g_ota_pct; char bar[11]; for (int i = 0; i < 10; i++) bar[i] = (i < pct / 10) ? '#' : '-'; bar[10] = 0; oled_row(0, xoff, "OTA升级中"); snprintf(buf, sizeof(buf), "下载固件 %d%%", pct); oled_row(1, xoff, buf); snprintf(buf, sizeof(buf), "[%s]", bar); oled_row(2, xoff, buf); oled_row(3, xoff, ""); } else if (g_net_mqtt_ready) { /*---- 已联网界面 ----*/ /* R0: 网络 + 运行时间(天:时:分) */ uint32_t mins = up_ms / 60000u; const char *nt = (NET_GetActiveBackend() == NET_BACKEND_WIFI) ? "WiFi" : "4G"; snprintf(buf, sizeof(buf), "%s %02u:%02u:%02u", nt, (unsigned)(mins / 1440u), (unsigned)((mins % 1440u) / 60u), (unsigned)(mins % 60u)); oled_row(0, xoff, buf); /* R1/R2: 电能参数 */ if (hlw8032_online()) { snprintf(buf, sizeof(buf), "%.1fV %.2fA", (double)hlw8032_get_voltage_v(), (double)hlw8032_get_current_a()); oled_row(1, xoff, buf); snprintf(buf, sizeof(buf), "%.1fW %.2fkWh", (double)hlw8032_get_power_w(), (double)hlw8032_get_energy_kwh()); oled_row(2, xoff, buf); } else { oled_row(1, xoff, "计量芯片离线"); oled_row(2, xoff, ""); } /* R3: 带秤模式显示重量(读不到显示原因),无秤模式显示开关状态 */ if (g_feed_scale_on) { if (feedScaleGetLastErr() == 0 && (HAL_GetTick() - feedScaleGetLastOkTick()) < 40000u) { snprintf(buf, sizeof(buf), "重量:%.1fKG", (double)(feedScaleGetWeight() / 10.0f)); } else { const char *reason; switch (feedScaleGetLastErr()) { case 2: reason = "帧短"; break; case 3: reason = "CRC错"; break; case 4: reason = "秤异常帧"; break; case 5: reason = "站号不符"; break; default: reason = "无应答"; break; } snprintf(buf, sizeof(buf), "重量:%s", reason); } oled_row(3, xoff, buf); } else { snprintf(buf, sizeof(buf), "开关:%s%s%s%s", MqttInfoStr.Relay_State[1] ? "开" : "关", MqttInfoStr.Relay_State[2] ? "开" : "关", MqttInfoStr.Relay_State[3] ? "开" : "关", MqttInfoStr.Relay_State[4] ? "开" : "关"); oled_row(3, xoff, buf); } } else { /*---- 未联网界面:联网进度 ----*/ net_ui_phase_t ph = g_net_ui_phase; oled_row(0, xoff, "断网"); if (ph == NET_UI_WIFI_FIXED || ph == NET_UI_WIFI_SMART || NET_GetActiveBackend() == NET_BACKEND_WIFI) { if (ph == NET_UI_WIFI_FIXED) { oled_row(1, xoff, "WiFi固定模式"); oled_row(2, xoff, "正在搜索热点"); } else if (ph == NET_UI_WIFI_SMART) { oled_row(1, xoff, "智能配网"); oled_row(2, xoff, "请打开APP广播"); } else { oled_row(1, xoff, "WiFi模式"); oled_row(2, xoff, "初始化中..."); } snprintf(buf, sizeof(buf), "剩余 %ds", g_net_ui_countdown); oled_row(3, xoff, g_net_ui_countdown > 0 ? buf : ""); } else { /* 4G 进度:显示最近完成的阶段 */ const char *step = "初始化中"; switch (ph) { case NET_UI_4G_RESET: step = "模块复位成功"; break; case NET_UI_4G_AT: step = "AT指令成功"; break; case NET_UI_4G_SIM: step = "SIM卡检测成功"; break; case NET_UI_4G_ATTACH: step = "附着网络成功"; break; case NET_UI_4G_MQTT: step = "连接服务器中"; break; case NET_UI_4G_NOSIM: step = "无SIM卡!"; break; default: break; } oled_row(1, xoff, "正在等待4G联网"); oled_row(2, xoff, step); oled_row(3, xoff, ""); } } } /* 电能上报策略:无论功率多少,每 3 分钟上报一次(energy+power 同一条报文)。 * 串口日志固定 10s 一条 */ #define ENERGY_REPORT_INTERVAL_MS 180000u /*==== sys_task:系统辅助(LED/电量/蓝牙命令/运行时长)====*/ static void sys_task(void *arg) { (void)arg; uint32_t batt_tick = HAL_GetTick(); uint32_t batt_fast_tick = HAL_GetTick(); uint32_t up_last = HAL_GetTick(); uint32_t en_print = HAL_GetTick(); uint64_t up_ms = 0; for (;;) { /* 蓝牙调试命令 */ process_usart1_command(); /* 联网指示:连接中闪烁,连上常亮(led.c activeEvents 据此控制 PA1) */ g_net_led_connected = g_net_mqtt_ready ? 1 : 0; /* LED 心跳/联网指示 */ activeEvents(); /* 断电检测 50ms 快轮询(PA0 掉电要抢在法拉电容耗尽前发消息) */ if (HAL_GetTick() - batt_fast_tick >= 50) { batt_fast_tick = HAL_GetTick(); update_batter(); } /* 运行秒数 1s 节流更新 */ if (HAL_GetTick() - batt_tick >= 1000) { batt_tick = HAL_GetTick(); if (g_mqtt_configured) hlw8032_tick(); /* 攒够约 0.1kWh 写一次 EEPROM;待配置模式不计量 */ oled_home_refresh(up_ms); /* OLED 界面 1s 刷新 */ reset_log_tick((uint32_t)(up_ms / 1000)); /* 共享 RAM 更新运行秒数,热复位不丢 */ } /* 运行时长累计(OLED 显示用):64 位累计,HAL_GetTick 约 49 天回绕也不受影响。 * 不再 60s 串口打印(OLED 实时可见) */ { uint32_t now = HAL_GetTick(); up_ms += (uint32_t)(now - up_last); /* 32位差值天然容忍回绕 */ up_last = now; } /* 电能:10s 检查一次(仅用于上报判断,不打印);上报为联网后首次一条,之后固定每 3 分钟一条 */ if (g_net_mqtt_ready && hlw8032_online() && (HAL_GetTick() - en_print) >= 10000u) { en_print = HAL_GetTick(); static uint8_t energy_first_reported = 0; static uint32_t energy_report_tick = 0; float kwh = hlw8032_get_energy_kwh(); float pw = hlw8032_get_power_w(); uint8_t report = 0; if (!energy_first_reported) { report = 1; energy_first_reported = 1; } else if ((HAL_GetTick() - energy_report_tick) >= ENERGY_REPORT_INTERVAL_MS) { report = 1; } if (report) { energy_report_tick = HAL_GetTick(); NET_publish_energy(kwh, pw); } /* 电能值不再 10s 打串口(OLED 实时可见,3 分钟上报平台) */ } vTaskDelay(20); } } /*==== FreeRTOS 钩子 ====*/ /* idle 钩子:喂独立看门狗。有任务死循环占 CPU 时 idle 饿死 → IWDG 复位 */ void vApplicationIdleHook(void) { IWDG_Feed(); } void vApplicationMallocFailedHook(void) { log_error("> RTOS: malloc 失败(堆耗尽)"); taskDISABLE_INTERRUPTS(); for (;;) { } } void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; log_error("> RTOS: 任务 %s 栈溢出", pcTaskName); taskDISABLE_INTERRUPTS(); for (;;) { } } /*==== 任务创建与调度器启动(main 调用,不再返回)====*/ void app_rtos_start(void) { /* 互斥锁先于任务创建 */ log_lock_init(); storage_lock_init(); NET_LockInit(); xTaskCreate(net_task, "net", 768, NULL, 3, NULL); /* 栈 3KB:AT/OTA 有大的 sprintf 缓冲 */ xTaskCreate(app_task, "app", 512, NULL, 2, NULL); /* 栈 2KB */ xTaskCreate(sys_task, "sys", 512, NULL, 1, NULL); /* 栈 2KB:电能日志一行 4 个浮点格式化很吃栈 */ vTaskStartScheduler(); /* 不会走到这里(堆不足创建失败才会) */ log_error("> RTOS: 调度器启动失败"); for (;;) { } }