417 lines
14 KiB
C
417 lines
14 KiB
C
/* 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 "network.h"
|
||
#include "led.h"
|
||
#include "sensor.h"
|
||
#include "adc.h"
|
||
#include "iic.h"
|
||
#include "24c02.h"
|
||
#include "uart.h"
|
||
#include "log_cn.h"
|
||
#include "ble_at.h"
|
||
#include "oled.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);
|
||
}
|
||
}
|
||
|
||
/*==== 业务模块初始化(调度器启动前调用,与裸机版一致)====*/
|
||
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);
|
||
log_info("> 客户端ID: %s", MqttInfoStr.ClientID);
|
||
log_info("> 用户名: %s", MqttInfoStr.Username);
|
||
log_info("> 版本序号: %u", MqttInfoStr.Ver);
|
||
|
||
/* W25Q64 外部 Flash,用于 OTA 固件暂存 */
|
||
W25Q64_Init();
|
||
|
||
/* 采集终端:无断电记录/继电器/电量计量,相关初始化全部移除 */
|
||
|
||
gps_load_mode(); /* GPS 定位开关(EEPROM 173,蓝牙 GPS0/1 切换) */
|
||
|
||
/* 电池电量检测(PA0 ADC) */
|
||
adcInit();
|
||
|
||
/* RS485 传感器采集 */
|
||
sensor_init();
|
||
|
||
/* LED(LED2 心跳同时兼任外部看门狗 TPL5010 喂狗) */
|
||
ledInit();
|
||
|
||
/* OLED 屏幕(128x64 + GB2312 字库 IC) */
|
||
oled_init();
|
||
oled_locate(1, 1); oled_print("数据采集终端");
|
||
oled_locate(2, 2); oled_print("V" FIRMWARE_VERSION_STR " RTOS");
|
||
log_info("> OLED: 初始化完成");
|
||
|
||
/* 网络模块初始化:按 EEPROM 记忆的上次成功方式优先(4G 或 WiFi) */
|
||
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 (;;) {
|
||
NET_process();
|
||
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 首次就绪后上报一次固件版本,让平台刷新版本序号 */
|
||
if (g_net_mqtt_ready && !mqtt_first_status_reported) {
|
||
mqtt_first_status_reported = 1;
|
||
{ /* 每次重启联网后上报一次代码版本:"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)");
|
||
sensor_notify_ota_confirmed(); /* 锚定传感器上传计划 */
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 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,复位");
|
||
W25Q64_OTA_IncTrial(); /* 软件复位 BL 不计次,这里自行消耗一次试错 */
|
||
HAL_Delay(100);
|
||
NVIC_SystemReset();
|
||
}
|
||
|
||
/* 传感器轮询与上报 */
|
||
sensor_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 (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_net_mqtt_ready) {
|
||
/*---- 已联网界面(四行布局) ----*/
|
||
/* R0: 左=网络类型,右=电池电量(10s 刷一次 ADC) */
|
||
{
|
||
static uint32_t bat_tick = 0;
|
||
static int bat_val = 0;
|
||
if (HAL_GetTick() - bat_tick >= 10000u) {
|
||
bat_tick = HAL_GetTick();
|
||
bat_val = battery_percent();
|
||
}
|
||
char left[8], right[8];
|
||
char r0[40];
|
||
const char *nt = (NET_GetActiveBackend() == NET_BACKEND_WIFI) ? "WiFi" : "4G";
|
||
snprintf(left, sizeof(left), "%s", nt);
|
||
snprintf(right, sizeof(right), "%d%%", bat_val);
|
||
/* 左网络右电量(空格 8px/个,允许少量过冲)。
|
||
* 整行在防烧屏偏移基础上再右移 4px:贴边 x=0 的字符会被屏幕物理裁边 */
|
||
int wl = oled_text_w(left), wr = oled_text_w(right);
|
||
int pos;
|
||
strcpy(r0, left); pos = wl;
|
||
int right_pos = 104 - wr; /* 右边界内收:内容顶满 128 时防烧屏钳位会把 xoff 锁死 */
|
||
while (pos < right_pos) { strcat(r0, " "); pos += 8; }
|
||
strcat(r0, right);
|
||
oled_row(0, (uint8_t)(xoff + 4), r0);
|
||
}
|
||
|
||
/* R1: 运行时间 天:时:分 */
|
||
{
|
||
uint32_t mins = up_ms / 60000u;
|
||
snprintf(buf, sizeof(buf), "%02u:%02u:%02u",
|
||
(unsigned)(mins / 1440u),
|
||
(unsigned)((mins % 1440u) / 60u),
|
||
(unsigned)(mins % 60u));
|
||
oled_row(1, xoff, buf);
|
||
}
|
||
|
||
/* R2/R3: 在线传感器翻页显示(一页一路,5s 翻页) */
|
||
{
|
||
char l1[24], l2[24];
|
||
if (sensor_oled_summary(l1, l2, sizeof(l1))) {
|
||
oled_row(2, xoff, l1);
|
||
oled_row(3, xoff, l2);
|
||
} else {
|
||
oled_row(2, xoff, "无传感器应答");
|
||
oled_row(3, xoff, "");
|
||
}
|
||
}
|
||
} 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, "");
|
||
}
|
||
}
|
||
}
|
||
|
||
/*==== sys_task:系统辅助(LED/电量/蓝牙命令/运行时长)====*/
|
||
static void sys_task(void *arg)
|
||
{
|
||
(void)arg;
|
||
uint32_t up_last = HAL_GetTick();
|
||
uint32_t up_print = HAL_GetTick();
|
||
uint32_t disp_tick = 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();
|
||
|
||
/* OLED 界面 1s 刷新 */
|
||
if (HAL_GetTick() - disp_tick >= 1000) {
|
||
disp_tick = HAL_GetTick();
|
||
oled_home_refresh(up_ms);
|
||
}
|
||
|
||
/* 运行时长心跳:联网后每 60s 串口输出一次(天/时/分);
|
||
* 未联网(配网/重连中)不打印,避免干扰配网倒计时日志。
|
||
* 64 位累计,HAL_GetTick 约 49 天回绕也不受影响 */
|
||
{
|
||
uint32_t now = HAL_GetTick();
|
||
up_ms += (uint32_t)(now - up_last); /* 32位差值天然容忍回绕 */
|
||
up_last = now;
|
||
if (now - up_print >= 60000) {
|
||
up_print = now;
|
||
if (g_net_mqtt_ready) {
|
||
uint32_t mins = (uint32_t)(up_ms / 60000);
|
||
log_info(FMT_UPTIME,
|
||
(unsigned)(mins / 1440),
|
||
(unsigned)((mins % 1440) / 60),
|
||
(unsigned)(mins % 60));
|
||
}
|
||
}
|
||
}
|
||
|
||
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 (;;) { }
|
||
}
|