diff --git a/.gitignore b/.gitignore index 7ce84d4a..47459a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,9 @@ simulator/SDL2/ # 内容页黄金参考(ref/*.bin)与目检 PNG 均为本地生成物,不入库: # 首次/换机后先跑 check_content_pages.py --update 生成 ref/*.bin,再跑 regen_ref_png.py 出 PNG simulator/tests/ref/ +# 架构图 PNG 预览(源为 docs/*.svg,PNG 本地渲染生成,不入库) +docs/项目架构总览.png +docs/就绪屏障.png *.pyc # Python venv (unified env, see AGENTS.md "Python 环境"; rebuild via requirements.txt) diff --git a/AGENTS.md b/AGENTS.md index 1951f0a3..6e04d248 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,12 +41,13 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart - **app_main_init 顺序**(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟基准)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → SD2506 诊断 → `tpafe5160_start_conv_irq` - **就绪屏障**(`App/sys_events.h` Event Flags,取代魔法延时/全局轮询):`SYS_EVT_NET_READY`(bit0, netTask 置位) / `SYS_EVT_STORAGE_READY`(bit1, defaultTask 置位) / `SYS_EVT_FTP_READY`(bit2) / `SYS_EVT_TIMESYNC_READY`(bit3);消费者用 `osEventFlagsWait` 阻塞等待 - **defaultTask**:FatFS 挂载(`f_mount` forced,无卷自动 `f_mkfs` FAT32)→ 置 `SYS_EVT_STORAGE_READY` → LED 闪烁循环 -- **netTask**:等 `SYS_EVT_STORAGE_READY`(≤10s)→ `net_stack_bring_up()`(`net_cfg_load` 读 Flash 网口配置 → `net_init(ip,mask,gw,mac)` + PHY 强制 100M 全双工 + 等链路 ≤3s → 回写芯片实际 MAC 影子)→ 置 `SYS_EVT_NET_READY` → 循环 `net_poll()` + `net_process_messages()` + `osDelay(1)`(tick=1ms) +- **netTask**:等 `SYS_EVT_STORAGE_READY`(≤10s)→ `net_stack_bring_up()`(`net_cfg_load` 读 Flash 网口配置 → `net_init(ip,mask,gw,mac)` + PHY 强制 100M 全双工 + 等链路 ≤3s → 回写芯片实际 MAC 影子 → 递增网络代次号)→ 置 `SYS_EVT_NET_READY` → 循环 `net_poll()` + `net_process_messages()` + `osDelay(1)`(tick=1ms);循环内消费 `SYS_EVT_NET_RECONFIG`(UI 保存网口配置后请求)→ 立即重建网络(新 IP 生效,无需重启;各 TCP 服务据代次号重建监听) - **ftpTask**:`osEventFlagsWait(NET_READY | STORAGE_READY, WaitAll)` 后 `lftpd_start("/", 21, &ftp)`(已无 7s 魔法延时) - **testTask**:测试套件入口(`test/test_config.h` 选择;产品构建下空转) - **uiTask**:等 `SYS_EVT_NET_READY` 后 `ui_port_init` → `ui_menu_init` → 启动 UI net sink - **timeSyncTask**:TCP 墙钟时间同步(PC 推送) - 功能编译开关:`App/app_cfg.h`(`APP_NET_ENABLE` / `APP_ADC_ENABLE` / `APP_RS485_ENABLE`) +- **全景架构图**:`docs/项目架构总览.md`(六层分层 / 任务与就绪屏障 / 启动时序 / 存储·网络·UI 栈 / 目录组织) ### 任务栈 @@ -102,6 +103,8 @@ Src/main.c → CubeMX 外设初始化 → app_main_init → osKernelStart ## 驱动关键点 详见 `docs/` 目录: +- `docs/项目架构总览.md` — 六层全景架构(分层/任务/启动时序/存储·网络·UI 栈) +- `docs/网络配置说明.md` — 网口设置页配置(UI 可改的 IP/掩码/网关/MAC:持久化 / 立即生效 / 页面操作) - `docs/问题记录/CH395F_Trap_Records.md` — CH395F 已知陷阱 - `docs/问题记录/GD5F2GQ5UE_Trap_Records.md` — GD5F2GQ5UE 已知陷阱 - `docs/问题记录/SD2506_Trap_Records.md` — SD2506 RTC 已知陷阱 diff --git a/App/sys_events.h b/App/sys_events.h index f930f3cd..11583cd3 100644 --- a/App/sys_events.h +++ b/App/sys_events.h @@ -27,6 +27,7 @@ extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */ #define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成 */ #define SYS_EVT_TIMESYNC_READY (1u << 3) /* timeSyncTask:首次时间同步完成 */ +#define SYS_EVT_NET_RECONFIG (1u << 4) /* UI:请求立即应用新网口配置(netTask 消费后重建网络) */ #ifdef __cplusplus } diff --git a/App/task/net_task.c b/App/task/net_task.c index 59e7a691..bc5e9355 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -24,6 +24,20 @@ #define DBG_TAG "[NET_TASK]" #include "dbg_log.h" +/* + * 函数功能:"立即应用网口配置"平台回调(注册给 net_cfg) + * 入口参数:无 + * 返回值:无 + * 限定条件:UI 保存成功后经 net_cfg_request_apply() 调用(任意任务上下文) + * 函数说明:只置事件位;真正的重建在主循环内完成(CH395F SPI 归 netTask 独占, + * UI 不得直接操作) + */ +static void net_reconfig_request_cb(void) { + if (sys_evtHandle != NULL) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_RECONFIG); + } +} + /* * 函数功能:CH395F 协议栈一次性初始化(两种构建模式共用) * 入口参数:无 @@ -96,6 +110,10 @@ int net_stack_bring_up(void) { (phy == CH395F_PHY_10M_HALF) ? "10M HALF" : (phy == CH395F_PHY_DISCONN) ? "DISCONNECT" : "UNKNOWN"); + /* 网络已重建完成、对外可用:递增代次号,通知各 TCP 服务(FTP/对时/远程显示) + * 旧 Socket 已随芯片复位失效、应重建监听 */ + net_generation_inc(); + return 0; } @@ -142,10 +160,26 @@ void net_task_func(void const *arg) { osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY); } + /* 注册"立即应用网口配置"回调:UI 保存成功后经 net_cfg_request_apply() 触发 */ + net_cfg_set_apply_cb(net_reconfig_request_cb); + DBG_INFO("netTask: started (single-socket mode)"); /* ==================== 主轮询循环 ==================== */ for (;;) { + /* UI 保存网口配置后请求立即应用:在此重建网络(新 IP/掩码/网关/MAC)。 + * 重建完成后代次号已递增,各 TCP 服务据此重建监听;期间连接会断开(预期)。 */ + if (sys_evtHandle != NULL + && (osEventFlagsGet(sys_evtHandle) & SYS_EVT_NET_RECONFIG) != 0u) { + (void)osEventFlagsClear(sys_evtHandle, SYS_EVT_NET_RECONFIG); + DBG_INFO("netTask: apply new net config (re-init)"); + if (net_stack_bring_up() == 0) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY); + } else { + DBG_ERROR("netTask: re-init FAIL"); + } + } + net_poll(); net_process_messages(); osDelay(1); diff --git a/App/time_sync.c b/App/time_sync.c index 5ea15c22..9815f8ed 100644 --- a/App/time_sync.c +++ b/App/time_sync.c @@ -114,6 +114,9 @@ void time_sync_task(void *argument) { } for (;;) { + /* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */ + uint32_t gen = net_get_generation(); + int ls = time_sync_open_listener(); if (ls < 0) { DBG_ERROR("time_sync: open listener FAIL, retry later"); @@ -126,10 +129,19 @@ void time_sync_task(void *argument) { do { conn = net_accept(ls, NULL, NULL); if (conn < 0) { + if (net_get_generation() != gen) { + net_close(ls); /* 网络重建:旧监听失效,回外层重建 */ + break; + } vTaskDelay(pdMS_TO_TICKS(200)); } } while (conn < 0); + if (conn < 0) { + DBG_INFO("time_sync: network re-init, relisten"); + continue; + } + time_sync_handle_conn(conn); /* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */ diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index ed557c09..9d43ca70 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -57,11 +57,19 @@ int lftpd_inet_get_socket_port(int socket) int lftpd_inet_accept(int socket) { + /* 网络重建(net_init)后旧 Socket 已失效:检测到代次变化即返回 -1, + * 由 lftpd_start 外层循环重建监听(网络未就绪时其 listen 会重试) */ + uint32_t gen = net_get_generation(); + while (1) { int s = net_accept(socket, NULL, NULL); if (s >= 0) { return s; } + if (net_get_generation() != gen) { + DBG_ERROR("network re-init: abort accept, relisten"); + return -1; + } vTaskDelay(pdMS_TO_TICKS(100)); } } diff --git a/Drivers/BSP/NET/net_config.h b/Drivers/BSP/NET/net_config.h index 3d61cb85..0f1fdbe8 100644 --- a/Drivers/BSP/NET/net_config.h +++ b/Drivers/BSP/NET/net_config.h @@ -1,11 +1,14 @@ /* * 模块名称:Network Configuration - * 模块功能:网络子系统配置宏定义,包括最大Socket数量、缓冲区大小、 - * 超时时间等参数配置 + * 模块功能:网络子系统编译期参数——Socket 数量、收发超时、TCP KeepAlive。 + * 仅保留**实际被引用**的宏;未使用项已清理。 * 适用平台:STM32F4 系列(CH395F 以太网芯片) * 作者:王建锋 * 创建日期:2026-07-18 * 修改记录: + * 2026-09-10 王建锋 清理未引用宏(TCP Server 监听 Socket 索引、收发缓冲、 + * 连接/接受超时、select/队列/轮询);KeepAlive 三宏改为 + * 实际生效值 60s/5s/3 并在 net_socket.c 引用(原宏为无效遗留值) */ #ifndef NET_CONFIG_H @@ -22,37 +25,10 @@ extern "C" { #define NET_MAX_SOCKETS 8 #endif -/* - * TCP Server 多连接模式配置 - * 监听 Socket 索引和数据 Socket 起始索引 - */ -#ifndef NET_TCP_SERVER_LISTEN_SOCK -#define NET_TCP_SERVER_LISTEN_SOCK 0 /* 监听 Socket 索引 */ -#endif - -/* - * 缓冲区大小配置 - */ -#ifndef NET_SEND_BUF_SIZE -#define NET_SEND_BUF_SIZE 4096 /* 发送缓冲区大小(字节) */ -#endif - -#ifndef NET_RECV_BUF_SIZE -#define NET_RECV_BUF_SIZE 4096 /* 接收缓冲区大小(字节) */ -#endif - /* * 超时时间配置(毫秒) * 0 = 无限等待(阻塞模式) */ -#ifndef NET_CONNECT_TIMEOUT_MS -#define NET_CONNECT_TIMEOUT_MS 5000 /* 连接超时 */ -#endif - -#ifndef NET_ACCEPT_TIMEOUT_MS -#define NET_ACCEPT_TIMEOUT_MS 0 /* 接受连接超时(0=无限等待) */ -#endif - #ifndef NET_SEND_TIMEOUT_MS #define NET_SEND_TIMEOUT_MS 1000 /* 发送超时 */ #endif @@ -62,39 +38,19 @@ extern "C" { #endif /* - * TCP KeepAlive 配置 + * TCP KeepAlive 配置(单位 ms;须为 500 的倍数且 IDLE > INTVL) + * 由 net_socket.c 的 net_init() 在 ch395f_init() 之前下发 */ #ifndef NET_KEEPALIVE_IDLE_MS -#define NET_KEEPALIVE_IDLE_MS 7200000 /* KeepAlive 空闲时间(默认2小时) */ +#define NET_KEEPALIVE_IDLE_MS 60000 /* 空闲 60 秒后开始探测 */ #endif #ifndef NET_KEEPALIVE_INTVL_MS -#define NET_KEEPALIVE_INTVL_MS 75000 /* KeepAlive 探测间隔(默认75秒) */ +#define NET_KEEPALIVE_INTVL_MS 5000 /* 5 秒探测一次 */ #endif #ifndef NET_KEEPALIVE_CNT -#define NET_KEEPALIVE_CNT 9 /* KeepAlive 探测次数(默认9次) */ -#endif - -/* - * select/poll 最大 fd 数量 - */ -#ifndef NET_SELECT_MAX_FDS -#define NET_SELECT_MAX_FDS NET_MAX_SOCKETS -#endif - -/* - * 轮询延时配置(毫秒) - */ -#ifndef NET_POLL_DELAY_MS -#define NET_POLL_DELAY_MS 1 /* 非阻塞轮询延时 */ -#endif - -/* - * 消息队列配置 - */ -#ifndef NET_MSG_QUEUE_LENGTH -#define NET_MSG_QUEUE_LENGTH 8 /* 消息队列深度 */ +#define NET_KEEPALIVE_CNT 3 /* 最多探测 3 次 */ #endif #ifdef __cplusplus diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 943f9fe4..b26ecc18 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -40,6 +40,7 @@ static int s_net_errno = 0; /* 最后错误码 */ static uint8_t s_net_initialized = 0; /* 初始化标志 */ static uint8_t s_local_ip[4]; /* 本机 IP 缓存(网络序,net_getsockname 用) */ static uint16_t s_dynamic_port = 49152; /* 动态端口分配起始值 */ +static volatile uint32_t s_net_generation = 0; /* 网络代次号:网络重建完成时递增(服务据此重建监听) */ /* * 消息队列 - 线程安全核心 @@ -142,10 +143,11 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { ch395f_set_retrans_count(8); /* 重传 8 次*/ ch395f_set_retrans_period(500); /* 每次间隔 500ms,总计 4s 重传窗口 */ - /* 设置 TCP KeepAlive 参数(必须在 INIT_CH395 之前,单位 ms,必须为 500 的倍数,且 IDLE > INTVL)*/ - ch395f_set_keepalive_idle(60000); /* 空闲 60 秒后开始探测*/ - ch395f_set_keepalive_intvl(5000); /* 5 秒探测一次*/ - ch395f_set_keepalive_cnt(3); /* 最多探 3 次*/ + /* 设置 TCP KeepAlive 参数(必须在 INIT_CH395 之前,单位 ms,必须为 500 的倍数,且 IDLE > INTVL) + * 值来自 net_config.h 的 NET_KEEPALIVE_*(60s/5s/3)*/ + ch395f_set_keepalive_idle(NET_KEEPALIVE_IDLE_MS); /* 空闲 60 秒后开始探测*/ + ch395f_set_keepalive_intvl(NET_KEEPALIVE_INTVL_MS); /* 5 秒探测一次*/ + ch395f_set_keepalive_cnt(NET_KEEPALIVE_CNT); /* 最多探 3 次*/ /* 设置网络参数(必须在 INIT_CH395 之前调用 */ if (ip != NULL) { @@ -207,6 +209,28 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { return 0; } +/* + * 函数功能:取网络代次号 + * 入口参数:无 + * 返回值:代次号(每次网络重建完成递增) uint32_t + * 限定条件:无 + * 函数说明:见 net_socket.h 同名函数说明 + */ +uint32_t net_get_generation(void) { + return s_net_generation; +} + +/* + * 函数功能:递增网络代次号(网络重建完成、对外可用时调用) + * 入口参数:无 + * 返回值:无 + * 限定条件:net_init() 成功且 PHY/链路就绪后调用(当前仅 net_stack_bring_up) + * 函数说明:见 net_socket.h 同名函数说明 + */ +void net_generation_inc(void) { + s_net_generation++; +} + /* * 函数功能:轮询 CH395F 中断状态,更新 Socket 连接状态并处理事件 * 返回值:本次轮询中发生事件的 Socket 数量 int >= 0 diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 0c55707d..d4b2f0ff 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -62,6 +62,27 @@ typedef struct { */ int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac); +/* + * 函数功能:取网络代次号 + * 入口参数:无 + * 返回值:代次号(每次网络重建完成递增) uint32_t + * 限定条件:无 + * 函数说明:TCP 服务(FTP / 对时 / 远程显示)据此感知"网络已重建"——代次变化 + * 表示旧 Socket 已随芯片复位失效,服务应放弃旧监听并重建。 + * 递增点在 net_stack_bring_up() 末尾(PHY/链路就绪后),避免服务在网络 + * 尚未可用时抢先重建、也避免 uiTask 阻塞在消息队列上。 + */ +uint32_t net_get_generation(void); + +/* + * 函数功能:递增网络代次号(网络重建完成、对外可用时调用) + * 入口参数:无 + * 返回值:无 + * 限定条件:net_init() 成功且 PHY/链路就绪后调用(当前仅 net_stack_bring_up) + * 函数说明:见 net_get_generation() + */ +void net_generation_inc(void); + /* * 函数功能:轮询所有 Socket 状态(必须在主循环中调用) * 返回值:有事件发生的 socket 数量 diff --git a/Drivers/BSP/NETCFG/net_cfg.c b/Drivers/BSP/NETCFG/net_cfg.c index c1a514ce..f9c81d08 100644 --- a/Drivers/BSP/NETCFG/net_cfg.c +++ b/Drivers/BSP/NETCFG/net_cfg.c @@ -58,6 +58,7 @@ static net_cfg_t s_cfg = { }; static uint8_t s_active_mac[6]; /* 芯片实际 MAC 影子(netTask 回写) */ static uint8_t s_active_valid; /* 影子有效标志(0 = 尚未回写) */ +static net_cfg_apply_cb_t s_apply_cb; /* "立即应用"平台回调(未注册 = 空操作) */ /* 函数定义区 */ @@ -178,10 +179,34 @@ int net_cfg_save(void) { DBG_ERROR("save: store write FAILED"); return -1; } - DBG_INFO("save: OK (effective at next power-on)"); + DBG_INFO("save: OK (file written)"); return 0; } +/* + * 函数功能:注册"立即应用"回调 + * 入口参数:cb - 回调函数(NULL = 注销) net_cfg_apply_cb_t + * 返回值:无 + * 限定条件:应用启动早期注册一次即可 + * 函数说明:见 net_cfg.h 同名函数说明 + */ +void net_cfg_set_apply_cb(net_cfg_apply_cb_t cb) { + s_apply_cb = cb; +} + +/* + * 函数功能:请求立即应用当前 RAM 配置 + * 入口参数:无 + * 返回值:无 + * 限定条件:无(未注册回调时空操作) + * 函数说明:见 net_cfg.h 同名函数说明 + */ +void net_cfg_request_apply(void) { + if (s_apply_cb != NULL) { + s_apply_cb(); + } +} + /* * 函数功能:取 RAM 配置副本(只读指针) * 入口参数:无 diff --git a/Drivers/BSP/NETCFG/net_cfg.h b/Drivers/BSP/NETCFG/net_cfg.h index 5ebcd210..6eaadbeb 100644 --- a/Drivers/BSP/NETCFG/net_cfg.h +++ b/Drivers/BSP/NETCFG/net_cfg.h @@ -57,6 +57,35 @@ int net_cfg_load(void); */ int net_cfg_save(void); +/* + * "立即应用"回调类型:平台挂钩(可选) + * 入口参数:无 + * 返回值:无 + * 函数说明:MCU 端由 netTask 注册——置 SYS_EVT_NET_RECONFIG 事件,由 netTask + * 重建网络;模拟器无网络栈,不注册(空操作)。经回调而非直接调用, + * 避免 NETCFG(驱动层)反向依赖 App 层。 + */ +typedef void (*net_cfg_apply_cb_t)(void); + +/* + * 函数功能:注册"立即应用"回调 + * 入口参数:cb - 回调函数(NULL = 注销) net_cfg_apply_cb_t + * 返回值:无 + * 限定条件:应用启动早期注册一次即可 + * 函数说明:见 net_cfg_apply_cb_t + */ +void net_cfg_set_apply_cb(net_cfg_apply_cb_t cb); + +/* + * 函数功能:请求立即应用当前 RAM 配置 + * 入口参数:无 + * 返回值:无 + * 限定条件:无(未注册回调时空操作) + * 函数说明:UI"网口设置"页保存行写盘成功后调用;真正生效由平台回调完成 + * (MCU:netTask 重建网络,无需重启) + */ +void net_cfg_request_apply(void); + /* * 函数功能:取 RAM 配置副本(只读指针) * 入口参数:无 diff --git a/Drivers/BSP/UI/ui_net_sink.c b/Drivers/BSP/UI/ui_net_sink.c index fe172787..062930d7 100644 --- a/Drivers/BSP/UI/ui_net_sink.c +++ b/Drivers/BSP/UI/ui_net_sink.c @@ -35,6 +35,8 @@ static uint8_t g_rx[64]; static int g_listen = -1; static int g_client = -1; +static uint16_t g_port; /* 监听端口(网络重建后重监听用) */ +static uint32_t g_gen; /* 建立监听时的网络代次号(变化 = 网络已重建) */ /* * 函数功能:计算 RemoDispBus 数据段异或校验值 @@ -204,6 +206,7 @@ static void remo_handle(const uint8_t *f, int n) */ int ui_net_sink_start(uint16_t port) { + g_port = port; g_listen = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (g_listen < 0) { return -1; @@ -225,6 +228,7 @@ int ui_net_sink_start(uint16_t port) return -1; } g_client = -1; + g_gen = net_get_generation(); /* 成功才记录代次;失败保持旧值以便下轮重试 */ return 0; } @@ -236,9 +240,21 @@ int ui_net_sink_start(uint16_t port) * 函数说明:1. 无客户端时尝试接受连接(不主动上报 INIT,由客户端按需请求) * 2. 有客户端时非阻塞收帧并解析 * 3. 连接关闭或异常时复位,等待重监听,不阻塞界面刷新 + * 4. 网络重建(net_init 代次变化)时关闭旧监听并重建(IP 变更后自愈) */ void ui_net_sink_poll(void) { + /* 网络重建(net_init 代次变化):旧监听/客户端已随芯片复位失效,重建监听 */ + if (net_get_generation() != g_gen) { + if (g_listen >= 0) { + net_close(g_listen); + g_listen = -1; + } + g_client = -1; + (void)ui_net_sink_start(g_port); /* 失败则下轮再试(g_gen 未更新) */ + return; + } + if (g_listen < 0) { return; } diff --git a/Drivers/BSP/UI/ui_page_net_cfg.c b/Drivers/BSP/UI/ui_page_net_cfg.c index 0368a9ed..c50d2b1b 100644 --- a/Drivers/BSP/UI/ui_page_net_cfg.c +++ b/Drivers/BSP/UI/ui_page_net_cfg.c @@ -2,13 +2,14 @@ * 模块名称:UI Page Net Cfg (网口设置内容页) * 模块功能:网口设置叶子节点的内容页数据 + 行为——5 行地址列表(IP 地址/子网 * 掩码/默认网关/MAC 地址 + 保存行,5 行每页共 1 页); - * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=6 / 值 x=60; + * 行槽顶 = 24+14i、选中白带 x=2..156 高 13、名 x=4 / 值 x=54; * 值取自 net_cfg RAM 副本(netTask 上电自 Flash 加载并回写实际 MAC); * 编辑模式:ENTER 进选中地址行(字节光标 = 活动字节,反显标记), * ↑↓调字节 0..255(环绕、值列实时刷新)、←→移字节光标(首↔末环绕)、 * ENTER 确认退出编辑; * 权限:edit_capable=1——查看密码 → 只读(编辑/保存键无效), - * 修改密码 → 可编辑;保存行 ENTER → net_cfg_save()(下次上电生效); + * 修改密码 → 可编辑;保存行 ENTER → net_cfg_save() 写盘 + + * net_cfg_request_apply() 通知 netTask 立即重建网络(无需重启); * 向框架 ui_content_page 提供页面描述符(零 LVGL 对象) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 @@ -29,8 +30,9 @@ /* * 布局常量(设备像素;1bpp 屏 160x160): * 5 行共 1 页(行槽顶 y = 24+14i,末行止于 y=93,与页脚 y=143 无冲突); - * 名 x=6 / 值 x=60(值列须 ≥60:4 汉字名占 48px + 间隙;值列宽—— - * IP/掩码/网关 11 ASCII 字符 = 66px 止于 126,MAC 12 hex 字符 = 72px 止于 132) + * 名 x=4 / 值 x=54(值列须 ≥54:4 汉字名占 48px + 间隙;值列宽—— + * IP/掩码/网关 11 ASCII 字符 = 66px 止于 119,MAC 17 字符(冒号分隔)= 102px 止于 155, + * 均在选中白带 x=2..156 内) */ #define NC_ROW_COUNT 5u /* 4 地址行 + 1 保存行 */ #define NC_ADDR_ROW_MAX 4u /* 地址行范围 0..3(IP/掩码/网关/MAC) */ @@ -38,8 +40,8 @@ #define NC_TOP_Y 24 /* 首行槽顶 y */ #define NC_PITCH 14 /* 行距 */ #define NC_BAND_H 13 /* 选中白带高 */ -#define NC_LEFT_X 6 /* 左列 x(名) */ -#define NC_RIGHT_X 60 /* 右列 x(值) */ +#define NC_LEFT_X 4 /* 左列 x(名) */ +#define NC_RIGHT_X 54 /* 右列 x(值) */ #define NC_ROW_BUF_LEN 32u /* 行缓冲长度(与框架 CP_ROW_BUF_LEN 一致) */ /* 静态变量定义区 */ @@ -84,7 +86,7 @@ static uint8_t nc_octet_count(uint16_t row) { * 返回值:无 * 限定条件:p_buf 非空 * 函数说明:IP/掩码/网关 = "192.168.001.100"(每段 3 位补零定长,字节 i 恒在 - * 字符 4i 起);MAC = "A494B6123456"(无分隔 hex,字节 i 恒在字符 2i 起) + * 字符 4i 起);MAC = "A4:94:B6:12:34:56"(冒号分隔,字节 i 恒在字符 3i 起) */ static void nc_fmt_value(uint16_t row, char *p_buf) { const net_cfg_t *cfg = net_cfg_get(); @@ -93,7 +95,7 @@ static void nc_fmt_value(uint16_t row, char *p_buf) { uint8_t mac[6]; net_cfg_get_mac(mac); - (void)lv_snprintf(p_buf, NC_ROW_BUF_LEN, "%02X%02X%02X%02X%02X%02X", + (void)lv_snprintf(p_buf, NC_ROW_BUF_LEN, "%02X:%02X:%02X:%02X:%02X:%02X", (unsigned)mac[0], (unsigned)mac[1], (unsigned)mac[2], (unsigned)mac[3], (unsigned)mac[4], (unsigned)mac[5]); return; @@ -196,7 +198,7 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { } nc_fmt_value(sel, value); - start = (uint32_t)s_cursor * ((sel == 3u) ? 2u : 4u); + start = (uint32_t)s_cursor * ((sel == 3u) ? 3u : 4u); /* 字节 i 字符偏移:IP/掩码/网关 4i,MAC 3i(冒号分隔) */ seg_len = (sel == 3u) ? 2u : 3u; slot_y = NC_TOP_Y + (int32_t)sel * NC_PITCH; @@ -223,10 +225,17 @@ static void nc_draw_cb(lv_layer_t *layer, const lv_area_t *clip) { { lv_draw_label_dsc_t ld; + char seg[4]; /* 段文本:IP/掩码/网关 3 字符、MAC 2 字符,+ '\0' */ + + /* 只绘制"段文本"本身:若传整串(value + start),LVGL 在未置 EXPAND 标志时 + * 按 coords 宽度(此处为段宽)折行,会逐行向盒下方绘制、溢出到其它行/空白区。 + * 传段文本(宽度 ≤ 盒宽)则单行绘制,恰好落在光标盒内。 */ + memcpy(seg, value + start, seg_len); + seg[seg_len] = '\0'; lv_draw_label_dsc_init(&ld); ld.base.layer = layer; - ld.text = value + start; + ld.text = seg; ld.text_static = 0u; /* 栈缓冲(同帧有效) */ ld.color = lv_color_white(); ld.ofs_x = 1; /* box.x1 = x0-1,+1 = x0(与框架行文本原点一致) */ @@ -295,6 +304,9 @@ static bool nc_key_cb(lv_key_t key) { } if (sel == NC_SAVE_ROW) { s_saved = (net_cfg_save() == 0) ? 1u : 2u; + if (s_saved == 1u) { + net_cfg_request_apply(); /* 立即应用:通知 netTask 重建网络(无需重启) */ + } ui_content_page_invalidate(); return true; } diff --git a/README.md b/README.md index 2914856b..a75810b3 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ flowchart TB APP --> BSP --> MW --> HAL --> HW ``` +> 更详细的全景架构(六层分层 + 任务与就绪屏障 + 启动时序 + 存储/网络/UI 栈 + 目录组织)见 **`docs/项目架构总览.md`**。 + ### 启动流程 ``` @@ -369,9 +371,11 @@ pre-commit run --all-files | 文档 | 说明 | |------|------| +| `docs/项目架构总览.md` | **项目全景架构**(六层分层 + 任务/就绪屏障 + 启动时序 + 存储/网络/UI 栈 + 目录组织) | | `docs/STM32F4-Base存储架构说明.md` | 三层存储软件栈详解(FatFS → dhara FTL → NAND) | | `docs/项目启动顺序管理.md` | Event Flags 就绪屏障设计 | | `docs/系统墙钟时间维护说明.md` | RTC + 墙钟时间子系统 | +| `docs/网络配置说明.md` | 网口设置页配置(UI 可改的 IP/掩码/网关/MAC:持久化 + 立即生效 + 页面操作) | | `docs/UI 设计/UI_设计架构.md` | LVGL UI 系统架构(菜单 / 内容页 / 内存预算) | | `docs/UI 设计/菜单页面设计.md` | 菜单导航行为、数据流与内容页设计 | | `docs/UI 设计/UI_界面制作流程.md` | 参考图 → HTML 样机 → LVGL 代码管线(P0~P5 门禁) | diff --git a/docs/UI 设计/内容页架构.md b/docs/UI 设计/内容页架构.md index 12d421eb..1dbeb164 100644 --- a/docs/UI 设计/内容页架构.md +++ b/docs/UI 设计/内容页架构.md @@ -216,6 +216,8 @@ flowchart TD - **文本**:静态行 `text_static=1`(Flash 字符串不拷贝);动态行与页脚页码用栈缓冲 (`lv_draw_label` 在同一刷新周期内**同步**光栅化,指针仅需调用期间有效) - **页脚**:`lv_snprintf` 现算(MSVC 对 `snprintf` 中文格式串误报 C4819,故用 LVGL 自带 `lv_snprintf`) + +> ⚠️ **`lv_draw_label` 折行陷阱**:`lv_draw_label` 的换行宽度取 **`coords` 宽度**(未置 `LV_TEXT_FLAG_EXPAND` 时 `w = lv_area_get_width(coords)`)。在**窄区域**(光标盒 / 小方框)里传入**长于该区域的文本**时,LVGL 会按该宽度**折行并逐行向下绘制**,溢出到下方其它行与空白区(表现为"下方弹出一串字符")。规则:窄区域只传"恰好该区域宽度的文本"(如网口设置光标段只传 3 个字符),长文本请在整行宽条带内绘制。 - 容器样式为**常量样式**(`LV_STYLE_CONST_INIT`,零 LVGL 池分配),见 `UI_设计架构.md` §5.6 ## 四、页面描述符(content_page_def_t) @@ -473,3 +475,5 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash | 2026-09-09 | 密码页落地(§4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;新模块 `ui_page_password`(3 数字框反显光标 + arm API,draw_cb+key_cb);菜单受保护叶子路由(arm→开密码页,确认一致后 `menu_password_ok` **先关页再续做进入动作**);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿 | | 2026-09-09 | 权限模型(替代"每页专属密码",§4.1):表行尾注记 ` #P`(需密码,无类别);`ui_pwd_def.h` 两个编译期密码(查看 000 / 修改 123)+ 新 `ui_perm_t` 权限枚举——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);生成器同步改造并重新生成(pwd 列 = 0/1 布尔,`ui_menu_tab_has_pwd`);框架 `ui_content_page_open(id, perm)` 携授予权限 + 描述符 `edit_capable` + 新 API `ui_content_page_is_editable`(页面钩子据以判只读/可编辑);路由密码检查提到内容页判断之前(修复带内容页需密码叶子绕过密码的路由缺口);无密码叶子直接进、默认只读。§4.1 功能规则/待确认表/落地要点同步。验证:五门禁全绿(菜单 8 态 + 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) | | 2026-09-10 | 网口设置内容页:新增 `Drivers/BSP/NETCFG/`(Flash 持久化 magic 'NCFG'+CRC-16/MODBUS,工厂默认 192.168.1.100/24、gw .1、MAC 全零=芯片内置)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节 0..255 环绕 / ←→移光标 / ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增描述符钩子 `init_cb`(开页重置运行态)+ API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 改为等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;模拟器 main 调 `net_cfg_load` 使参考图显示工厂默认值;字体修复:lv_font_conv 丢弃低码位符号(U+FF0C 全角逗号 < 0x4E2D 范围起点 → 页脚逗号长期空白)改由 `merge_extra_symbols()` 补回 + 原本缺字(存/密/户/本/用/钥)→ 111 字 / 208 字形;回归扩至 11 页(网口设置 4 态:可编辑/只读/编辑光标反显/保存 OK),五门禁全绿 | +| 2026-09-10 | 修复网口设置页编辑态渲染 bug:光标段原传整串 `value + start`,而 `lv_draw_label` 按 `coords`(光标盒,仅段宽)折行 → 逐行向盒下方绘制、溢出到其它行与空白区("下方弹出一串字符")。改为只传**段文本**(IP/掩码/网关 3 字符、MAC 2 字符);§3.5 补"`lv_draw_label` 折行陷阱"说明 | +| 2026-09-10 | 网口设置页 MAC 值改常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符 = 102px);布局调整 名 x=6→4、值 x=60→54(MAC ink 止于 155,仍在白带 2..157 内);编辑光标段偏移 MAC 2i→3i | diff --git a/docs/UI 设计/菜单页面设计.pdf b/docs/UI 设计/菜单页面设计.pdf deleted file mode 100644 index 88234b6b..00000000 Binary files a/docs/UI 设计/菜单页面设计.pdf and /dev/null differ diff --git a/docs/就绪屏障.svg b/docs/就绪屏障.svg new file mode 100644 index 00000000..ada0a81e --- /dev/null +++ b/docs/就绪屏障.svg @@ -0,0 +1,89 @@ + + +就绪屏障(Readiness Barrier)· 启动依赖链 +CMSIS-RTOS2 Event Flags · 生产者置位声明「我好了」,消费者阻塞等待 + +生产者(set) + +消费者(wait) + +事件位(Event Flag) + +All 汇合(等全部位) + +预留 / 未接入 + +sys_evtHandle(Event Flags 事件组) + + +1 +bit1 · STORAGE_READY +defaultTask 置位 · netTask / ftpTask 等待 + + +0 +bit0 · NET_READY +netTask 置位 · 4 个任务等待 + + +2 +bit2 · FTP_READY +ftpTask 置位 · 预留消费者 + + +3 +bit3 · TIMESYNC_READY +预留(尚未接入调度) + +defaultTask +FatFS 挂载(f_mount / f_mkfs) + + +set bit1 + +netTask +net_stack_bring_up() + + +set bit0 + +ftpTask +lftpd_start("/", 21) + + +set bit2 + +netTask +wait bit1 · ≤10s + +ftpTask +wait bit0 + bit1 · All + +uiTask +wait bit0 · Forever + +timeSyncTask +wait bit0 · Forever + +testTask +wait bit0 · NET 模式 + + +wait + + +All + + + + + + + + + + + +wait bit0 +生产者与消费者互不知晓,只经事件组解耦;新增子系统 = 新 bit + 各自 wait/set,不改已有任务。详见 docs/项目启动顺序管理.md + diff --git a/docs/网络配置说明.md b/docs/网络配置说明.md new file mode 100644 index 00000000..4011206e --- /dev/null +++ b/docs/网络配置说明.md @@ -0,0 +1,252 @@ +# 网口设置页配置说明 + +> 版本:v2.0 日期:2026-09-10 +> 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网控制器) +> 范围:UI「网口设置」页可配置的网口参数(IP 地址 / 子网掩码 / 默认网关 / MAC 地址)—— +> 参数与默认值、持久化、生效链路、页面操作与权限。 + +--- + +## 0. 速览 + +| 项目 | 内容 | +|---|---| +| **可配置** | IP 地址、子网掩码、默认网关、MAC 地址 | +| **入口** | 屏幕:`通讯参数 → 网口设置`(修改密码 `123` 可编辑;查看密码 `000` 只读) | +| **存储** | NAND Flash 文件 `/cfg/netcfg.bin`(掉电不丢) | +| **生效** | 按「保存」后**立即生效**(网络重建约 1~3s,既有 TCP 连接会断开,无需重启) | +| **工厂默认** | `192.168.1.100` / `255.255.255.0` / `192.168.1.1` / 芯片内置 MAC | + +**典型操作(改 IP)**: + +1. 屏幕进入 `通讯参数 → 网口设置`,输入修改密码 `123` +2. 选中 `IP地址` 行 → 回车进编辑 → `↑/↓` 调数字、`←/→` 移光标 → 回车确认 +3. 选中 `保存` 行 → 回车 → 显示 `OK` +4. 约 1~3 秒后设备在新 IP 上恢复服务(FTP / 对时 / 远程显示自动重建监听) + +--- + +## 1. 可配置参数与工厂默认值 + +定义在 `Drivers/BSP/NETCFG/net_cfg.h` 的 `net_cfg_t`: + +| 参数 | 字段 | 工厂默认 | 说明 | +|---|---|---|---| +| IP 地址 | `ip[4]` | **192.168.1.100** | 静态 IP(不启用 DHCP) | +| 子网掩码 | `mask[4]` | **255.255.255.0** | /24 | +| 默认网关 | `gw[4]` | **192.168.1.1** | | +| MAC 地址 | `mac[6]` | **全零** | 全零 = 不覆盖芯片内置 MAC(见 §2) | + +工厂默认值是**单一来源**:`net_cfg.c` 顶部 `NET_CFG_DEF_*` 宏,同时用于 `s_cfg` 静态初始化与 `s_cfg_defaults()`,避免两处漂移。 + +> **RAM 副本自启动即有效**:`s_cfg` 以工厂默认值**静态初始化**(落 `.data`),不依赖 `net_cfg_load()` 是否被调用——即便存储异常,页面与网络拿到的也是有效配置而非全零。 + +--- + +## 2. MAC 地址语义 + +| 存储值 | `net_cfg_get_mac()` 返回 | 网络初始化时 | +|---|---|---| +| 非全零 | 存储的 MAC | 写入芯片 EEPROM | +| 全零(默认) | 芯片实际 MAC 影子(已回写时);否则全零 | **不写**,保持芯片内置 MAC | + +- 芯片实际 MAC 由 netTask 在网络初始化成功后读回,经 `net_cfg_set_active_mac()` 存入**影子区**——供页面只读显示(页面不直接触 SPI)。 +- MAC 写芯片 EEPROM,**每上电重放同值幂等**。 + +--- + +## 3. 持久化 + +### 3.1 磁盘格式(`/cfg/netcfg.bin`) + +`net_cfg.c` 的 `net_cfg_blob_t`(两端布局一致,小端): + +| 偏移 | 字段 | 大小 | 说明 | +|---|---|---|---| +| 0 | `magic` | 4 | `0x4647434E`(ASCII `'NCFG'`) | +| 4 | `ver` | 2 | 格式版本 = 1 | +| 6 | `crc` | 2 | **CRC-16/MODBUS**,覆盖 ip..mac 共 18B | +| 8 | `ip[4]` | 4 | | +| 12 | `mask[4]` | 4 | | +| 16 | `gw[4]` | 4 | | +| 20 | `mac[6]` | 6 | | +| — | 对齐填充 | 2 | 结构体 `sizeof = 28B` | + +- **加载校验**:magic / ver / CRC 任一不符 → 回退工厂默认并打印 `DBG_ERROR`(坏配置不带崩网络)。 +- **保存**:先建 `/cfg` 目录(`f_mkdir`,已存在不影响),再 `FA_CREATE_ALWAYS` 覆盖写。 + +### 3.2 存储后端(双端) + +`net_cfg_store.h` 定义接口,两平台各一份实现(互斥编译): + +| 平台 | 文件 | 位置 | +|---|---|---| +| MCU | `net_cfg_store_mcu.c` | NAND 上 FatFS 文件 `/cfg/netcfg.bin` | +| 模拟器 | `net_cfg_store_sim.c` | 工作目录本地文件 `sim_netcfg.bin`(`fopen`) | + +磁盘格式两端一致,文件可互读。 + +--- + +## 4. 生效链路 + +### 4.1 上电启动 + +```mermaid +sequenceDiagram + autonumber + participant DT as defaultTask + participant NT as netTask + participant CFG as net_cfg + participant CH as CH395F + + Note over DT,NT: 上电:CubeMX 已创建任务与事件组,调度器启动 + DT->>DT: FatFS 挂载(f_mount;无卷则 f_mkfs) + DT-->>NT: 置 SYS_EVT_STORAGE_READY(就绪屏障) + NT->>NT: 等 SYS_EVT_STORAGE_READY(≤10s;超时用工厂默认继续) + NT->>CFG: net_cfg_load() + Note over CFG: 读 /cfg/netcfg.bin;缺失/损坏 → 工厂默认 + CFG-->>NT: RAM 副本(有效) + NT->>CFG: net_cfg_get() → net_cfg_fmt_ipv4() + CFG-->>NT: "192.168.001.100" + NT->>CH: 初始化网络(下发 IP / 掩码 / 网关 / MAC) + CH-->>NT: 芯片实际 MAC + NT->>CFG: net_cfg_set_active_mac()(MAC 影子) + NT->>NT: 置 SYS_EVT_NET_READY(广播网络就绪) +``` + +### 4.2 保存后立即生效 + +保存网口参数后,设备立即按新参数重建网络,无需重启。 + +**机制**:`请求 → netTask 重建 → 代次号 → 服务自愈` + +**约束与对策**:网络重建会复位 CH395F,所有监听 Socket 随之失效;而三个 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)的监听由各自任务持有。为此引入"网络代次号"——各服务检测到代次变化后主动重建监听,在新 IP 上恢复服务。 + +```mermaid +sequenceDiagram + participant UI as 网口设置页 + participant CFG as net_cfg + participant NT as netTask + participant NS as net_socket + participant SVC as FTP/对时/远程显示 + + UI->>CFG: 保存行 ENTER → net_cfg_save()(写盘) + CFG->>NT: net_cfg_request_apply() → 回调置 SYS_EVT_NET_RECONFIG + NT->>NT: 主循环检测到该位 → 重建网络(新 IP/掩码/网关/MAC) + NT->>NS: net_generation_inc()(代次号 +1,此刻网络已可用) + NT->>SVC: 重新广播 SYS_EVT_NET_READY + SVC->>SVC: 轮询发现代次变化 → 关闭旧监听 → 重建监听(新 IP) +``` + +**三个服务的自愈点**: + +| 服务 | 端口 | 检测位置 | 自愈方式 | +|---|---|---|---| +| FTP | 21 | `lftpd_inet_accept()` | 代次变化 → 返回 -1 → `lftpd_start` 外层循环重建监听 | +| 对时 | 8888 | `time_sync_task()` 的 accept 循环 | 代次变化 → 关旧监听 → 回外层重建 | +| 远程显示 | 7003 | `ui_net_sink_poll()` | 代次变化 → 关旧监听/客户端 → 重建 | + +**关键设计点**: + +1. **SPI 归 netTask 独占**:UI 只置事件位,不直接操作 CH395F。 +2. **代次号在链路就绪后才递增**:避免服务在网络未就绪时抢先重建,也避免 uiTask 阻塞在消息队列上冻结界面。 +3. **无回调 = 空操作**:模拟器不注册应用回调,行为退化为"仅写盘"。 +4. **既有连接必断**:IP 变更导致连接不可达,属网络层语义。 + +--- + +## 5. 页面操作(网口设置页) + +菜单路径:`通讯参数 → 网口设置`(菜单节点 30,行尾 ` #P` 需密码)。页面模块 `Drivers/BSP/UI/ui_page_net_cfg.c`。 + +### 5.1 页面布局 + +| 行 | 名称 | 值格式 | +|---|---|---| +| 0 | IP地址 | `192.168.001.100`(每段 `%03u` 补零,**定长 11 字符**) | +| 1 | 子网掩码 | `255.255.255.000` | +| 2 | 默认网关 | `192.168.001.001` | +| 3 | MAC地址 | `A4:94:B6:12:34:56`(6 字节十六进制,冒号分隔) | +| 4 | 保存 | `ENTER` → 保存后显示 `OK` / `ERR` | + +### 5.2 权限 + +`ui_pwd_def.h` 编译期密码: + +| 输入的密码 | 授予权限 | 页面行为 | +|---|---|---| +| 查看密码 `000` | `UI_PERM_VIEW` | **只读**:ENTER 无效,仅可浏览 | +| 修改密码 `123` | `UI_PERM_MODIFY` | **可编辑**:ENTER 进编辑模式 | + +### 5.3 编辑模式 + +ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**)、`←/→` 移字节光标(首↔末环绕)、ENTER 确认退出。调整**实时落 RAM 副本**(`net_cfg_set`),**只有保存行 ENTER 才写盘并应用**(`net_cfg_save` + `net_cfg_request_apply`);ESC 退出不写盘、改动丢弃(重启后恢复上次保存值)。 + +> 值格式与解析约定:页面写定长 `%03u` 字符串 → 网络初始化按十进制逐段解析、容忍前导零(如 `"001"` → 1)。 + +--- + +## 6. 文件与 API 索引 + +| 文件 | 职责 | +|---|---| +| `Drivers/BSP/NETCFG/net_cfg.h/.c` | 配置核心:RAM 副本 + 工厂默认 + 加载/保存编排 + MAC 影子 + IPv4 格式化 | +| `Drivers/BSP/NETCFG/net_cfg_store.h` | 存储后端接口(读/写) | +| `Drivers/BSP/NETCFG/net_cfg_store_mcu.c` | MCU 后端:FatFS `/cfg/netcfg.bin` | +| `Drivers/BSP/NETCFG/net_cfg_store_sim.c` | 模拟器后端:本地 `sim_netcfg.bin` | +| `Drivers/BSP/UI/ui_page_net_cfg.c` | 网口设置内容页(显示 / 编辑 / 保存) | +| `Drivers/BSP/UI/ui_pwd_def.h` | 编译期密码(查看 000 / 修改 123) | +| `App/task/net_task.c` | 上电加载配置 + 消费"立即应用"请求重建网络 | +| `Drivers/BSP/NET/net_socket.h/.c` | 网络代次号(服务自愈依据) | + +**对外 API**(`net_cfg.h`): + +| API | 说明 | +|---|---| +| `net_cfg_load()` | 从存储加载到 RAM 副本;文件缺失/损坏回退工厂默认(恒返回 0) | +| `net_cfg_save()` | 组装磁盘格式写盘;0 成功 / -1 失败 | +| `net_cfg_request_apply()` | 请求立即应用(页面保存成功后调用) | +| `net_cfg_set_apply_cb()` | 注册"立即应用"平台回调 | +| `net_cfg_get()` | 取 RAM 副本只读指针(恒非空) | +| `net_cfg_set()` | 更新 RAM 副本(不写盘) | +| `net_cfg_set_active_mac()` | 回写芯片实际 MAC 影子(netTask 调) | +| `net_cfg_get_mac()` | 取显示用 MAC(存储非零 → 存储;否则影子;否则全零) | +| `net_cfg_fmt_ipv4()` | 4 字节 → `"192.168.001.100"` 定长字符串 | + +--- + +## 7. 陷阱 + +1. **保存即生效,但连接会断**:保存后重建网络(约 1~3s),既有 TCP 连接(FTP / 对时 / 远程显示)会断开并自动重建监听——IP 变更的必然结果,非缺陷。 +2. **MAC 全零是哨兵**,不是"MAC 为 0":要恢复芯片内置 MAC,把 MAC 字段改回全零并保存。 +3. **配置存在 NAND 上**:存储挂载失败时 `net_cfg_load` 回退工厂默认,网络仍能起,但保存会失败(页面显示 `ERR`)。 +4. **改配置需"修改密码"**:查看密码 `000` 进页面是只读,ENTER 无效(防误改)。 +5. **新增 TCP 服务需处理重建**:须在其监听循环中检测 `net_get_generation()` 变化并重建监听,否则网络重建后不再可达(见 §4.2)。 + +--- + +## 8. 相关文档 + +| 主题 | 文档 | +|---|---| +| CH395F 已知陷阱 | `docs/问题记录/CH395F_Trap_Records.md` | +| 网络测试指南 | `docs/测试说明/CH395F_Test_Guide.md` | +| 启动顺序 / 就绪屏障 | `docs/项目启动顺序管理.md` | +| 项目分层架构 | `docs/项目架构总览.md` | +| 内容页框架(含网口设置页) | `docs/UI 设计/内容页架构.md` | + +--- + +## 9. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-10 | 初版:网口参数 + 协议栈参数 + 陷阱 | +| 2026-09-10 | `net_config.h` 收敛:删除 8 个从未引用的遗留宏;KeepAlive 三宏改为实际生效值并由 `net_socket.c` 引用 | +| 2026-09-10 | **保存后立即生效**:新增"应用回调 + 网络代次号"机制 | +| 2026-09-10 | §0 速览重写为并列对比表 + 两条生效时机 + 分步操作 | +| 2026-09-10 | §1.7 改为客观描述当前行为,补"三个服务的自愈点"表 | +| 2026-09-10 | **v2.0 聚焦 UI 页面配置**:删除"协议栈参数(② 编译期常量)"整章;标题 / 范围 / 速览 / 索引 / 陷阱相应收敛为"网口设置页"内容 | +| 2026-09-10 | §4.1 上电启动由文本流改为**时序图**(与 §4.2 一致,任务先后关系与配置回退更直观) | +| 2026-09-10 | MAC 显示改为常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符);页面布局左移(名 x=4 / 值 x=54)以容纳 102px 宽度,编辑光标段偏移随之为 3i | diff --git a/docs/项目启动顺序管理.md b/docs/项目启动顺序管理.md index d28526e0..ce7f607e 100644 --- a/docs/项目启动顺序管理.md +++ b/docs/项目启动顺序管理.md @@ -39,6 +39,11 @@ 这与 CubeMX"一次性创建全部任务"的模式完全兼容:任务仍由 `MX_FREERTOS_Init` 统一创建,但各自在入口 `wait`,相当于"创建即挂起、就绪才放行"。 +![就绪屏障(Event Flags)三栏依赖图](就绪屏障.svg) + +> 上图:**左 = 生产者(set)· 中 = 事件组 `sys_evtHandle` · 右 = 消费者(wait)**;任务卡与事件位对齐同一行,`bit1 → ftpTask` 的额外依赖用 **All 汇合点**(`osFlagsWaitAll`)表达。 +> 源码 `tools/arch_diagram/gen_barrier_svg.py`(改数据重跑即可),PNG 预览本地渲染、不入库。 + ### 为什么用 Event Flags 而非全局变量 | 维度 | 全局标志 `g_net_ready` | Event Flags | |------|----------------------|-------------| @@ -65,6 +70,7 @@ extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */ #define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成 */ #define SYS_EVT_TIMESYNC_READY (1u << 3) /* timeSyncTask:首次时间同步完成 */ +#define SYS_EVT_NET_RECONFIG (1u << 4) /* UI:请求立即应用新网口配置(netTask 消费后重建网络) */ /* 后续新增子系统继续左移分配 bit,32 位上限内即可 */ ``` @@ -75,6 +81,7 @@ extern osEventFlagsId_t sys_evtHandle; | `SYS_EVT_STORAGE_READY` | `defaultTask`(挂载成功后) | 依赖文件系统的测试套件(如 STORAGE/GD5F) | | `SYS_EVT_FTP_READY` | `ftpTask`(`lftpd_start` 成功后) | 无(示例,预留给需要 FTP 就绪的后续功能) | | `SYS_EVT_TIMESYNC_READY` | (预留,暂无生产者置位) | 可选:对时精度敏感的业务 | +| `SYS_EVT_NET_RECONFIG` | UI 网口设置页保存成功(经 `net_cfg` 应用回调置位) | `netTask` 主循环:消费后 `net_stack_bring_up()` 重建网络(新网口参数立即生效),并递增网络代次号 | ### 3.3 事件组创建时机 `sys_evtHandle` 必须在**任何任务 `wait` 之前**存在。`main.c` 调用顺序为: diff --git a/docs/项目架构总览.md b/docs/项目架构总览.md new file mode 100644 index 00000000..cb3b6c09 --- /dev/null +++ b/docs/项目架构总览.md @@ -0,0 +1,307 @@ +# STM32F4-Base 项目架构总览 + +> 版本:v1.0 日期:2026-09-10 +> 平台:STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2)+ FreeRTOS V10.3.1(CMSIS-RTOS2) +> 用途:一页看清项目的分层、任务、数据流与目录组织。细节以各模块专项文档为准(见 §9)。 + +--- + +## 1. 全景分层架构 + +![STM32F4-Base 项目分层架构](项目架构总览.svg) + +> 上图由 `tools/arch_diagram/gen_arch_svg.py` 生成(**手绘 SVG**,布局确定、零连线交叉);改架构后编辑脚本顶部 `ROWS` 数据重跑即可,源数据与 §1.1 表一致。 +> 需要 PNG 预览(PPT/聊天分享):用 Edge 无头渲染 `docs/项目架构总览.svg` 即可(PNG 不入库)。 + +> 分层原则:**上层只依赖相邻下层(单向)**;**① 业务层为顶层**(仅 `net_cfg`,被 UI 与基础服务共用),**② UI 层与 ③ 基础服务层同级并列**。跨层的具体调用关系见 §1.2 表。 + +### 1.1 各层职责与关键模块 + +> ① 业务层(顶层,仅 `net_cfg`);② UI 层与 ③ 基础服务层**同级并列**(均使用 `net_cfg`);④ 中间件层 → ⑤ 驱动层 → ⑥ 内核/HAL → ⑦ 硬件 依次向下。 + +| 层 | 目录 | 职责 | 关键模块 | +|---|---|---|---| +| ① 业务层 | `Drivers/BSP/NETCFG/` | 顶层共享配置(被 UI 与基础服务共用) | `net_cfg` | +| ② UI 层 | `Drivers/BSP/UI/` | LVGL 界面(与 PC 模拟器共用源码) | `ui_port` `ui_menu` `ui_menu_view` `ui_menu_tab` `ui_content_page` `ui_page_*` `ui_net_sink` `ui_font_cn_12` `ui_deco` | +| ③ 基础服务层 | `App/`、`Drivers/BSP/NET/lftpd/` | 应用侧服务与编排 | `app_main` `sys_clock` `sys_events` `app_cfg` `task/*` `lftpd` `time_sync` | +| ④ 中间件层 | `Drivers/BSP/NET/`、`Lib/`、`App/util/` | 通用库 / 协议栈 | `net_socket` `net_select` `FatFs` `nand_ftl(dhara)` `LVGL` `util(crc·ringbuf)` | +| ⑤ 驱动层 | `Drivers/BSP/{CH395F,GD5F2GQ5UE,TPAFE5160,SD2506,RS485}/` | 芯片寄存器 / 时序 / 中断 | `ch395f` `gd5f2gq5ue` `tpafe5160` `sd2506` `rs485` | +| ⑥ 内核 / HAL | `Middlewares/`、`Drivers/STM32F4xx_HAL_Driver/`、`Drivers/CMSIS/` | 任务调度、外设 HAL、启动 | FreeRTOS V10.3.1 · STM32 HAL · CMSIS | +| ⑦ 硬件 | — | STM32F407ZGTx @168MHz | SPI1 / SPI2 / I2C1 / UART5 / USART1 / DMA / GPIO / TIM7 | +| ⑥ 硬件 | — | STM32F407ZGTx @168MHz | SPI1 / SPI2 / I2C1 / UART5 / USART1 / DMA / GPIO / TIM7 | + +### 1.2 关键跨层依赖(对应上图未画出的连线) + +| 依赖 | 说明 | +|---|---| +| `ui_page_net_cfg` → `net_cfg` | 网口设置页显示/编辑 RAM 副本(不直接触 CH395F SPI) | +| `netTask` → `net_cfg` → `net_socket` | 上电 `net_cfg_load` 后 `net_init(ip,mask,gw,mac)` | +| `lftpd` / `net_cfg` / 测试 → `FatFs` → `nand_ftl` → `gd5f2gq5ue` | 文件与配置落 NAND | +| `ui_net_sink` / `time_sync` → `net_socket` | 远程显示 :7003 / 对时 :8888 共用同一 TCP 栈 | +| `ui_menu` → `ui_content_page` → `ui_page_*` | 框架 + 页面插件(描述符解耦) | +| `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC | +| `ui_port` / `ui_page_*` → `LVGL` | UI 全部绘制走 LVGL | + +> `*` `adc_task.c` / `rs485_task.c` 任务体已实现,但**当前未在 `freertos.c` 创建**(预留/待接入)。 + +--- + +## 2. 运行时任务与就绪屏障 + +### 2.1 任务一览(`Src/freertos.c` 创建) + +| 任务 | 栈 (字节) | 优先级 | 职责 | 入口 | +|---|---|---|---|---| +| `defaultTask` | 2048 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ 置 `STORAGE_READY` → LED 闪烁 | `StartDefaultTask` | +| `netTask` | 4096 | Normal | 等存储 → `net_stack_bring_up()` → 置 `NET_READY` → `net_poll` 轮询 | `StartNetTask` | +| `ftpTask` | 4096 | Normal | 等网络+存储 → `lftpd_start("/", 21)` | `StartFtpTask` | +| `testTask` | 4096 | BelowNormal | 测试套件入口(`test/test_config.h` 选择;产品构建空转) | `start_test_task` | +| `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_net_sink` → 10ms 刷新 | `ui_task_func` | +| `timeSyncTask` | 2048 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` | + +> `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 48KB heap_4 堆分配。 + +### 2.2 就绪屏障(Event Flags,取代魔法延时) + +![就绪屏障(Event Flags)](就绪屏障.svg) + +> 三栏式:**左 = 生产者(set)· 中 = 事件组 `sys_evtHandle` · 右 = 消费者(wait)**。 +> 任务卡与事件位对齐同一行,箭头 = `set` / `wait` 关系;`bit1 → ftpTask` 的额外依赖用 **All 汇合点**表达(`osFlagsWaitAll`)。 +> 源码 `tools/arch_diagram/gen_barrier_svg.py`(改数据重跑即可);PNG 预览本地渲染、不入库。 + +**设计原则**:生产者与消费者互不知晓,只经事件组解耦;新增子系统只需定义新 bit + 各自 wait/set,不改已有任务。 +(详见 `docs/项目启动顺序管理.md`) + +### 2.3 线程间通信 + +| 机制 | 对象 | 用途 | +|---|---|---| +| Event Flags | `sys_evtHandle` | 启动就绪屏障(§2.2) | +| Message Queue | `netMsgQueue`(深度 8,`net_msg_t`) | 所有 CH395F 操作经队列**串行化**到 netTask 执行,保证 SPI 单线程访问 | + +--- + +## 3. 启动时序 + +```mermaid +sequenceDiagram + autonumber + participant M as main + participant A as app_main_init + participant K as osKernelStart + participant D as defaultTask + participant N as netTask + participant F as ftpTask + participant U as uiTask + participant T as timeSyncTask + + M->>M: HAL_Init + CubeMX 外设初始化
(SPI1/SPI2/I2C1/UART5/USART1/DMA/GPIO/TIM7) + M->>A: app_main_init() + Note over A: 时间子系统优先,使后续日志带时间戳 + A->>A: sd2506_init() → sys_clock_init()(墙钟基准) + A->>A: rs485_init() → gd5f2gq5ue_init()(BBT 扫描) + A->>A: tpafe5160_init() → SD2506 诊断 → start_conv_irq() + A-->>M: "App init OK" + M->>K: osKernelStart() + Note over K: MX_FREERTOS_Init 已创建 6 任务 + 事件组 + 队列 + + K->>D: 调度 + D->>D: f_mount(无卷则 f_mkfs FAT32)→ LED 循环 + D-->>N: set SYS_EVT_STORAGE_READY + + N->>N: wait STORAGE_READY(≤10s,超时回退工厂默认) + N->>N: net_cfg_load() → net_init(ip,mask,gw,mac)
→ PHY 强制 100M 全双工 → 等链路 ≤3s → MAC 回写影子 + N-->>F: set SYS_EVT_NET_READY + N->>N: 循环 net_poll() + net_process_messages() + osDelay(1) + + F->>F: wait NET_READY|STORAGE_READY(All)→ lftpd_start("/", 21) + U->>U: wait NET_READY → ui_port_init → ui_menu_init → ui_net_sink_start(7003) + T->>T: wait NET_READY → TCP listen :8888 +``` + +--- + +## 4. 存储栈 + +```mermaid +flowchart LR + FS["FatFs (Lib/FatFs)
f_mount / f_open / f_mkfs"] + DIO["diskio.c
BSP 适配层"] + FTL["nand_ftl.c
dhara FTL(磨损均衡 + 逻辑块映射)"] + NAND["gd5f2gq5ue.c
页读写 / ECC / BBT 坏块表"] + CHIP[("GD5F2GQ5UE
NAND 2Gbit")] + + subgraph USERS["使用方"] + NC["net_cfg
/cfg/netcfg.bin"] + LF["lftpd
FTP 文件读写"] + ST["测试套件
STORAGE/GD5F"] + end + + NC --> FS + LF --> FS + ST --> FS + FS --> DIO --> FTL --> NAND --> CHIP + + subgraph RTC["独立时间源"] + SD["SD2506 RTC (I2C1)"] --> SC["sys_clock
墙钟 epoch"] + end +``` + +- 掉电持久化:`net_cfg` 配置(magic `'NCFG'` + ver + CRC-16/MODBUS),**保存后下次上电生效**。 +- NAND 坏块表(BBT)持久化在保留块池,不交给 dhara 管理。 + +--- + +## 5. 网络栈与 TCP 消费者 + +```mermaid +flowchart TB + CHIP[("CH395F
MAC+PHY · SPI2 + DMA")] --> DRV["ch395f.c
寄存器/命令 + 中断集中读取"] + DRV --> NS["net_socket.c
BSD Socket API(单连接模式)"] + DRV --> NSEL["net_select.c
select/poll 多路复用"] + NS --> Q["netMsgQueue
(串行化到 netTask)"] + Q --> NT["netTask
net_poll() + net_process_messages()"] + NS --> C1["lftpd
FTP :21"] + NS --> C2["time_sync
对时 :8888"] + NS --> C3["ui_net_sink
RemoDispBus :7003"] + C3 -.->|"回送 1bpp 帧 + 接收远端按键"| CLIENT["DTU-RemoteLCD 客户端"] + + NC["net_cfg
IP/掩码/网关/MAC"] --> NS + style NT fill:#efe,stroke:#484 +``` + +**单连接约束**:CH395F 配置为多 Socket + TCP Server 单连接(`set_fun_para(0x09)`),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示三个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。 + +--- + +## 6. UI 层架构(框架 + 页面插件) + +```mermaid +flowchart TB + subgraph LOGIC["逻辑层"] + UM["ui_menu.c
级联框状态机 + 按键分发"] + MT["ui_menu_tab.c
菜单树生成物(ui_menu_tree.txt)"] + end + subgraph VIEW["显示层"] + MV["ui_menu_view.c
框体/行/装饰自绘"] + DECO["ui_deco.c
浅 V 装饰"] + end + subgraph FRAME["内容页框架"] + CP["ui_content_page.c
屏生命周期 + 描述符驱动绘制/按键"] + end + subgraph PLUGIN["页面插件(每页一文件)"] + P1["ui_page_device_info"] + P2["ui_page_ac"] + P3["ui_page_dc"] + P4["ui_page_password"] + P5["ui_page_net_cfg"] + end + subgraph PORT["显示端口"] + UP["ui_port.c
160x160 1bpp 缓冲"] + SINK["ui_net_sink.c
远程推流"] + end + + UM --> MT + UM --> MV + UM --> CP + MV --> DECO + CP --> P1 & P2 & P3 & P4 & P5 + MV --> UP + SINK --> UP + P5 -.->|"net_cfg RAM 副本"| CFG["net_cfg"] + P4 -.->|"ui_pwd_def.h 编译期密码"| PWD["查看 000 / 修改 123"] +``` + +- 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),ESC 返回菜单屏。 +- 菜单受保护叶子(`ui_menu_tree.txt` 行尾 ` #P`):先弹密码页,授权后 `menu_password_ok` **先关页再续进入动作**。 +- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,111 字/208 字形)。 + +--- + +## 7. 目录组织 + +``` +STM32F4-Base/ +├── Src/ Inc/ CubeMX 生成:main.c / freertos.c / HAL 外设 / 中断 / 时基 +│ ├── freertos.c 6 任务 + netMsgQueue + sys_evt 创建(USER CODE 区自定义) +│ └── main.c 入口 → app_main_init → osKernelStart +├── App/ 应用层(业务编排,非 CubeMX) +│ ├── app_main.c 启动编排(时间子系统优先) +│ ├── sys_clock.c 墙钟基准 / Unix epoch +│ ├── time_sync.c TCP 对时服务 +│ ├── sys_events.h 就绪屏障 bit 定义 +│ ├── app_cfg.h 功能编译开关(APP_NET/ADC/RS485_ENABLE) +│ ├── task/ net_task / ui_task / adc_task* / rs485_task* +│ └── util/ crc(CRC-16/8)、ringbuf +├── Drivers/BSP/ 纯手工驱动/服务(不受 CubeMX 保护) +│ ├── CH395F/ 以太网 MAC+PHY +│ ├── GD5F2GQ5UE/ NAND + dhara FTL +│ ├── TPAFE5160/ ADC +│ ├── SD2506/ RTC +│ ├── RS485/ 半双工串口 +│ ├── NET/ net_socket / net_select / lftpd +│ ├── NETCFG/ 网口配置持久化(MCU + 模拟器双后端) +│ ├── UI/ LVGL UI(与模拟器共用) +│ └── dbg_log.h/dbg_cfg.h 分级日志 +├── Lib/ 第三方:LVGL v9.2、FatFs、FreeRTOS、CMSIS +├── Middlewares/ ST 官方中间件 +├── MDK-ARM/ Keil 工程(uvprojx + scatter + build/download 脚本) +├── simulator/ PC 模拟器(CMake + SDL2,共用 UI 源码 + 无头回归测试) +├── test/ 测试套件(test_config.h 互斥选择,产品构建空转) +├── tools/ font_gen(字体生成)/ menu_gen(菜单表生成) +├── scripts/ 代码规范检查、RAM 报告等 +└── docs/ 设计/测试/问题记录文档 +``` + +--- + +## 8. 构建与双端 + +```mermaid +flowchart LR + SRC["共享源码
Drivers/BSP/UI/*
Drivers/BSP/NETCFG/net_cfg.c"] + SRC --> MCU["MCU 端
Keil MDK-ARM (ARMCC v5)
@build → BUILD_OK"] + SRC --> SIM["PC 模拟器
CMake + SDL2 (MSVC)
lvgl_sim.exe --keys"] + MCU --> TEST["@test:串口采集 + 烧录 + 日志校验"] + SIM --> REG["UI 回归:
check_menu_rows.py
check_content_pages.py"] +``` + +| 目标 | 工具链 | 验证 | +|---|---|---| +| MCU 固件 | Keil MDK-ARM v5(`MDK-ARM/build.bat`) | `@build` 0 错 0 警 → `@test` 串口闭环 | +| PC 模拟器 | CMake + SDL2 + MSVC(`simulator/build/`) | `check_menu_rows.py` + `check_content_pages.py`(文本断言 + 像素黄金参考) | + +**功能编译开关**:`App/app_cfg.h`(网络/ADC/RS485);`test/test_config.h` 测试套件互斥选择(产品构建全注释)。 + +--- + +## 9. 相关专项文档 + +| 主题 | 文档 | +|---|---| +| 启动顺序 / 就绪屏障 | `docs/项目启动顺序管理.md` | +| 存储架构(NAND/FTL/FatFs) | `docs/STM32F4-Base存储架构说明.md` | +| 墙钟时间维护 | `docs/系统墙钟时间维护说明.md` | +| RAM 预算与优化 | `docs/内存管理/RAM使用分析与优化方案.md` | +| UI 设计架构 / 内容页 / 菜单 | `docs/UI 设计/UI_设计架构.md` · `内容页架构.md` · `菜单页面设计.md` | +| UI 自动化测试 | `docs/UI 设计/UI_自动化测试流程.md` | +| 驱动已知陷阱 | `docs/问题记录/{CH395F,GD5F2GQ5UE,SD2506}_Trap_Records.md` | +| 各模块测试指南 | `docs/测试说明/` | +| 代码规范 | `docs/代码规范/` | + +--- + +## 10. 变更记录 + +| 日期 | 变更 | +|---|---| +| 2026-09-10 | 初版:六层全景架构 + 任务/就绪屏障 + 启动时序 + 存储栈 + 网络栈 + UI 分层 + 目录组织 | +| 2026-09-10 | §1 全景分层架构重绘:由"36 节点 + 跨层虚线"改为**分层色带(每层一框)+ 职责表 + 跨层依赖表**,消除连线交叉、提升可读性 | +| 2026-09-10 | §1 再改为**手绘 SVG**(`tools/arch_diagram/gen_arch_svg.py` → `项目架构总览.svg`:分层色带 + 模块芯片 + 单向下箭头,布局确定);PNG 预览本地渲染、不入库 | +| 2026-09-10 | `net_cfg` 分层定位修正:按项目规划归入 **① 应用层**(原列在 ③ 服务/中间件);SVG 与 §1.1 表同步(模块物理路径仍在 `Drivers/BSP/NETCFG/`) | +| 2026-09-10 | §1 布局修正:**应用层与 UI 层同级并列**(同一行并排,均向下依赖);生成器数据由"每层一行"改为 `ROWS`(行内可放多个并列层),顶层两框各引一条向下箭头 | +| 2026-09-10 | §1 层级重定(按项目规划):**① 业务层**(改名,原"应用层")为**顶层**,含 `net_cfg`(被 UI 与基础服务共用)+ 业务编排;**② UI 层与 ③ 基础服务层同级并列**其下;`time_sync` 归基础服务层 | +| 2026-09-10 | §1 定稿(严格版):**① 业务层顶层仅 `net_cfg` 一块**;`app_main`/`sys_clock`/`sys_events`/`app_cfg`/`task/*`/`util/*` 并入 **③ 基础服务层**(任务编排 + 网络/存储/图形);② UI 层与 ③ 同级并列 | +| 2026-09-10 | §1 新增 **④ 中间件层**(通用库 / 协议栈:`net_socket`·`net_select`·`lftpd`·`FatFs`·`nand_ftl`·`LVGL`·`util`),位于 ③ 基础服务层与驱动层之间;③ 基础服务层收敛为**应用侧服务与编排**(`app_main`·`sys_clock`·`sys_events`·`app_cfg`·`task/*`·`time_sync`);层号顺延至 ⑦ 硬件 | +| 2026-09-10 | `lftpd` 由 ④ 中间件层移入 **③ 基础服务层**(FTP 视为应用侧服务);中间件层余 `net_socket`·`net_select`·`FatFs`·`nand_ftl`·`LVGL`·`util` | +| 2026-09-10 | §2.2 就绪屏障图由 Mermaid 改为**手绘 SVG**(`tools/arch_diagram/gen_barrier_svg.py` → `就绪屏障.svg`):三栏(生产者 / 事件组 / 消费者)+ 水平对齐 + All 汇合点,消除原图 9 条边的交叉;PNG 预览本地渲染、不入库 | diff --git a/docs/项目架构总览.svg b/docs/项目架构总览.svg new file mode 100644 index 00000000..9fa61714 --- /dev/null +++ b/docs/项目架构总览.svg @@ -0,0 +1,111 @@ + + +STM32F4-Base 项目分层架构 +STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层 + +① 业务层 +顶层共享配置 · 被 UI 与基础服务共用 + +net_cfg(网口配置:IP/掩码/网关/MAC + Flash 持久化) + +② UI 层 +Drivers/BSP/UI · 与 PC 模拟器共用 + +ui_port(显示端口) + +ui_menu / ui_menu_view(级联菜单) + +ui_menu_tab(菜单表) + +ui_content_page(内容页框架) + +ui_page_*(5 页插件) + +ui_net_sink(远程显示 :7003) + +ui_font_cn_12(中文字体) + +ui_deco(浅 V 装饰) + +③ 基础服务层 +应用侧服务 / 编排 + +app_main(启动编排) + +sys_clock(墙钟基准) + +sys_events(就绪屏障) + +app_cfg(编译开关) + +task/(net · ui · adc* · rs485*) + +lftpd(FTP :21) + +time_sync(TCP 对时 :8888) + +④ 中间件层 +通用库 / 协议栈 + +net_socket / net_select(BSD API) + +FatFs(文件系统) + +nand_ftl(dhara FTL) + +LVGL v9.2(图形库) + +util(crc · ringbuf) + +⑤ 驱动层 +Drivers/BSP · 芯片时序/中断 + +ch395f(SPI2 + DMA) + +gd5f2gq5ue(SPI1) + +tpafe5160(并行 16 位) + +sd2506(I2C1) + +rs485(UART5 + DE) + +⑥ 内核 / HAL +调度 · 外设抽象 + +FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4) + +STM32 HAL(时基 TIM7) + +CMSIS / 启动文件 + +⑦ 硬件 +STM32F407ZGTx @168MHz + +CH395F 以太网(SPI2) + +GD5F2GQ5UE NAND(SPI1) + +TPAFE5160 ADC(并行) + +SD2506 RTC(I2C1) + +RS-485(UART5) + +USART1 调试(115200) + +6x LED + + + + + + + + + + + + +上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/项目架构总览.md §1.2 * 预留未接入调度 + diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index d5c84abc..7b97fbcf 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -71,7 +71,7 @@ TEXTS = { ("r0名", "IP地址", (24, 38), True), ("r0值", "192.168.001.100", (24, 38), True), ("r1名", "子网掩码", (38, 52), False), ("r1值", "255.255.255.000", (38, 52), False), ("r2名", "默认网关", (52, 66), False), ("r2值", "192.168.001.001", (52, 66), False), - ("r3名", "MAC地址", (66, 80), False), ("r3值", "000000000000", (66, 80), False), + ("r3名", "MAC地址", (66, 80), False), ("r3值", "00:00:00:00:00:00", (66, 80), False), ("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False), ("页脚", "第 1 页,共 1 页", (140, 160), False), ], @@ -80,7 +80,7 @@ TEXTS = { ("r0名", "IP地址", (24, 38), True), ("r0值", "192.168.001.100", (24, 38), True), ("r1名", "子网掩码", (38, 52), False), ("r1值", "255.255.255.000", (38, 52), False), ("r2名", "默认网关", (52, 66), False), ("r2值", "192.168.001.001", (52, 66), False), - ("r3名", "MAC地址", (66, 80), False), ("r3值", "000000000000", (66, 80), False), + ("r3名", "MAC地址", (66, 80), False), ("r3值", "00:00:00:00:00:00", (66, 80), False), ("r4名", "保存", (80, 95), False), ("r4值", "ENTER", (80, 95), False), ("页脚", "第 1 页,共 1 页", (140, 160), False), ], diff --git a/tools/arch_diagram/gen_arch_svg.py b/tools/arch_diagram/gen_arch_svg.py new file mode 100644 index 00000000..83ad76cb --- /dev/null +++ b/tools/arch_diagram/gen_arch_svg.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/项目架构总览.svg —— 分层架构图(纯标准库,无需第三方依赖)。 + +为什么不用 Mermaid:自动布局不可控,多节点/跨层依赖会画成交叉的"意大利面"。 +本脚本手工排布:每层一个圆角框(标题在框内顶部 + 模块芯片自动换行), +同一"行"可放多个并列层(如 应用层 与 UI 层 同级并排),行间单向下箭头。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_arch_svg.py +输出: + docs/项目架构总览.svg +改架构时:编辑本文件顶部的 LAYERS / ROWS 数据后重跑即可。 +""" +import html +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "项目架构总览.svg") + +# ----------------------------- 画布与尺寸参数 ----------------------------- +W = 1560 +PAD = 26 +TITLE_FONT = 20 +SUB_FONT = 13 +BAND_TITLE_FONT = 17 +BAND_SUB_FONT = 12 +CHIP_FONT = 13 + +CHIP_H = 38 # 芯片盒高 +CHIP_GAP = 10 # 芯片水平间距 +CHIP_ROW_GAP = 10 # 芯片行间距 +CHIP_PAD = 16 # 芯片内左右留白 +BAND_PAD_X = 18 # 框内左右留白 +BAND_TITLE_H = 38 # 框内标题区高 +BAND_PAD_BOTTOM = 16 +BAND_H_GAP = 26 # 同一行内并列框的间距 +BAND_V_GAP = 44 # 行间距(放箭头) +TITLE_H = 78 # 顶部标题区高 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +# ----------------------------- 架构数据(改这里) ----------------------------- +L_BIZ = dict( + title="① 业务层", sub="顶层共享配置 · 被 UI 与基础服务共用", color="#2563eb", bg="#eff6ff", + chips=["net_cfg(网口配置:IP/掩码/网关/MAC + Flash 持久化)"], +) +L_UI = dict( + title="② UI 层", sub="Drivers/BSP/UI · 与 PC 模拟器共用", color="#7c3aed", bg="#f5f3ff", + chips=["ui_port(显示端口)", "ui_menu / ui_menu_view(级联菜单)", "ui_menu_tab(菜单表)", + "ui_content_page(内容页框架)", "ui_page_*(5 页插件)", + "ui_net_sink(远程显示 :7003)", "ui_font_cn_12(中文字体)", "ui_deco(浅 V 装饰)"], +) +L_SVC = dict( + title="③ 基础服务层", sub="应用侧服务 / 编排", color="#0d9488", bg="#f0fdfa", + chips=["app_main(启动编排)", "sys_clock(墙钟基准)", "sys_events(就绪屏障)", + "app_cfg(编译开关)", "task/(net · ui · adc* · rs485*)", + "lftpd(FTP :21)", "time_sync(TCP 对时 :8888)"], +) +L_MW = dict( + title="④ 中间件层", sub="通用库 / 协议栈", color="#16a34a", bg="#f0fdf4", + chips=["net_socket / net_select(BSD API)", + "FatFs(文件系统)", "nand_ftl(dhara FTL)", "LVGL v9.2(图形库)", + "util(crc · ringbuf)"], +) +L_DRV = dict( + title="⑤ 驱动层", sub="Drivers/BSP · 芯片时序/中断", color="#d97706", bg="#fffbeb", + chips=["ch395f(SPI2 + DMA)", "gd5f2gq5ue(SPI1)", "tpafe5160(并行 16 位)", + "sd2506(I2C1)", "rs485(UART5 + DE)"], +) +L_KERN = dict( + title="⑥ 内核 / HAL", sub="调度 · 外设抽象", color="#475569", bg="#f1f5f9", + chips=["FreeRTOS V10.3.1(CMSIS-RTOS2 · heap_4)", "STM32 HAL(时基 TIM7)", + "CMSIS / 启动文件"], +) +L_HW = dict( + title="⑦ 硬件", sub="STM32F407ZGTx @168MHz", color="#e11d48", bg="#fff1f2", + chips=["CH395F 以太网(SPI2)", "GD5F2GQ5UE NAND(SPI1)", "TPAFE5160 ADC(并行)", + "SD2506 RTC(I2C1)", "RS-485(UART5)", "USART1 调试(115200)", "6x LED"], +) + +# 每一行可放多个"同级并列"的层(上层只依赖下方各层) +ROWS = [ + [L_BIZ], # 顶层:业务层(仅共享配置 net_cfg) + [L_UI, L_SVC], # UI 层与基础服务层同级并排,均使用 net_cfg + [L_MW], # 中间件层(通用库 / 协议栈) + [L_DRV], + [L_KERN], + [L_HW], +] + +FOOT = "上层只依赖相邻下层(单向);UI 层与基础服务层同级并列,均使用顶层 net_cfg。跨层调用细节见 docs/项目架构总览.md §1.2 * 预留未接入调度" + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em。 + + 取偏大估计(宁可芯片略宽,不可文字溢出盒外)。 + """ + w = 0.0 + for ch in s: + w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58) + return w + + +def chip_w(label): + return text_w(label, CHIP_FONT) + 2 * CHIP_PAD + + +def wrap_chips(chips, avail): + rows, cur, cur_w = [], [], 0.0 + for c in chips: + cw = chip_w(c) + need = cw if not cur else cur_w + CHIP_GAP + cw + if cur and need > avail: + rows.append(cur) + cur, cur_w = [c], cw + else: + cur.append(c) + cur_w = need + if cur: + rows.append(cur) + return rows + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + # 布局:逐行排布(行内可并列多个框,行高取该行最高框) + placements = [] # (layer, chip_rows, x, y, w, h) + y = TITLE_H + for row in ROWS: + n = len(row) + bw = (W - 2 * PAD - BAND_H_GAP * (n - 1)) // n + bands = [] + for i, layer in enumerate(row): + bx = PAD + i * (bw + BAND_H_GAP) + avail = bw - 2 * BAND_PAD_X + rows = wrap_chips(layer["chips"], avail) + bh = BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM + bands.append([layer, rows, bx, y, bw, bh]) + row_h = max(b[5] for b in bands) + for b in bands: + b[5] = row_h # 同行等高(视觉"同级") + placements.extend(bands) + y += row_h + BAND_V_GAP + total_h = y - BAND_V_GAP + 54 + + add('' % (W, total_h)) + add('%s' + % (PAD, 40, TITLE_FONT, esc("STM32F4-Base 项目分层架构"))) + add('%s' + % (PAD, 62, SUB_FONT, + esc("STM32F407ZGTx @168MHz · FreeRTOS V10.3.1 (CMSIS-RTOS2) · 上层只依赖相邻下层"))) + + for (layer, rows, bx, by, bw, bh) in placements: + # 框背景 + 边框 + add('' % (bx, by, bw, bh, layer["bg"], layer["color"])) + # 标题(层名 + 目录/职责说明) + add('%s' + % (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"]))) + tw = text_w(layer["title"], BAND_TITLE_FONT) + add('%s' + % (bx + BAND_PAD_X + tw + 12, by + 26, BAND_SUB_FONT, esc(layer["sub"]))) + # 芯片 + cy = by + BAND_TITLE_H + for row in rows: + cx = bx + BAND_PAD_X + for c in row: + cw = chip_w(c) + add('' % (cx, cy, cw, CHIP_H, layer["color"])) + add('%s' + % (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c))) + cx += cw + CHIP_GAP + cy += CHIP_H + CHIP_ROW_GAP + + # 行间箭头:每个上层框向下引一条(顶层两框 → 两条,体现"都依赖下层") + row_first = [] # 每行首个 placement 的下标 + idx = 0 + for row in ROWS: + row_first.append(idx) + idx += len(row) + for r in range(len(ROWS) - 1): + y0 = placements[row_first[r]][3] + placements[row_first[r]][5] + 6 + y1 = placements[row_first[r + 1]][3] - 8 + for i in range(row_first[r], row_first[r] + len(ROWS[r])): + (_, _, bx, _, bw, _) = placements[i] + ax = bx + bw / 2 + add('' % (ax, y0, ax, y1 - 7)) + add('' % (ax, y1)) + + add('%s' + % (PAD, total_h - 18, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, total_h + + +def main(): + svg, h = build() + with open(OUT, "w", encoding="utf-8", newline="\n") as f: + f.write(svg) + import xml.etree.ElementTree as ET + ET.fromstring(svg) + print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT)) + + +if __name__ == "__main__": + main() diff --git a/tools/arch_diagram/gen_barrier_svg.py b/tools/arch_diagram/gen_barrier_svg.py new file mode 100644 index 00000000..cb6569a8 --- /dev/null +++ b/tools/arch_diagram/gen_barrier_svg.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/就绪屏障.svg —— 启动就绪屏障(Event Flags)依赖图(纯标准库)。 + +为什么不用 Mermaid:原图 9 条边全挤在单一中心节点上,画成交叉的"意大利面"。 +本脚本手工排布为三栏: + 左 = 生产者(set) 中 = 事件组 sys_evtHandle 右 = 消费者(wait) +任务卡与事件位各自对齐到同一水平行,箭头基本水平/正交;bit1 → ftpTask 的 +"与"依赖用汇合点(All)表达,避免跨行连线。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_barrier_svg.py +输出: + docs/就绪屏障.svg +改依赖时:编辑本文件顶部的 BITS / PRODUCERS / CONSUMERS 数据后重跑。 +""" +import html +import math +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "就绪屏障.svg") + +# ----------------------------- 画布与尺寸 ----------------------------- +W = 1400 +PAD = 40 +TITLE_FONT = 20 +SUB_FONT = 13 +CARD_TITLE_FONT = 15 +CARD_NOTE_FONT = 12 +BIT_TITLE_FONT = 14 +BIT_NOTE_FONT = 12 +LABEL_FONT = 11.5 +FOOT_FONT = 12 + +H_CARD = 84 # 任务卡 / 事件位行高 +GAP = 26 # 行间距 +MAIN_Y = 200 # 首行 y +CARD_W_L = 290 # 生产者卡宽 +CARD_W_R = 450 # 消费者卡宽 +PANEL_X, PANEL_W = 400, 420 +PROD_X = 40 +CONS_X = 910 +TAP_X = 832 # bit1 → All 汇合点的下引线 x +BUS_X = 862 # bit0 分发总线 x +ARROW_W = 2.2 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +C_PROD, BG_PROD = "#2563eb", "#eff6ff" +C_CONS, BG_CONS = "#7c3aed", "#f5f3ff" + +# ----------------------------- 数据(改这里) ----------------------------- +# 事件位(中间栏),自上而下 = 时序先后 +BITS = [ + dict(bit="bit1", flag="STORAGE_READY", color="#16a34a", bg="#f0fdf4", + note="defaultTask 置位 · netTask / ftpTask 等待"), + dict(bit="bit0", flag="NET_READY", color="#0d9488", bg="#f0fdfa", + note="netTask 置位 · 4 个任务等待"), + dict(bit="bit2", flag="FTP_READY", color="#d97706", bg="#fffbeb", + note="ftpTask 置位 · 预留消费者"), + dict(bit="bit3", flag="TIMESYNC_READY", color="#94a3b8", bg="#f8fafc", + note="预留(尚未接入调度)"), +] + +# 生产者(左栏):row = 其置位的 bit 行 +PRODUCERS = [ + dict(name="defaultTask", note="FatFS 挂载(f_mount / f_mkfs)", row=0, label="set bit1"), + dict(name="netTask", note="net_stack_bring_up()", row=1, label="set bit0"), + dict(name="ftpTask", note='lftpd_start("/", 21)', row=2, label="set bit2"), +] + +# 消费者(右栏):row = 卡片排布行(前两行与 bit1 / bit0 对齐) +CONSUMERS = [ + dict(name="netTask", note="wait bit1 · ≤10s", row=0), + dict(name="ftpTask", note="wait bit0 + bit1 · All", row=1), + dict(name="uiTask", note="wait bit0 · Forever", row=2), + dict(name="timeSyncTask", note="wait bit0 · Forever", row=3), + dict(name="testTask", note="wait bit0 · NET 模式", row=4), +] + +FOOT = ("生产者与消费者互不知晓,只经事件组解耦;新增子系统 = 新 bit + 各自 wait/set," + "不改已有任务。详见 docs/项目启动顺序管理.md") + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。""" + w = 0.0 + for ch in s: + w += fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) + return w + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + def row_y(r): + return MAIN_Y + r * (H_CARD + GAP) + + def row_cy(r): + return row_y(r) + H_CARD / 2.0 + + def poly(points, color, dash=False, head=True, width=ARROW_W): + pts = " ".join("%.1f,%.1f" % p for p in points) + d = ' stroke-dasharray="7 5"' if dash else '' + add('' % (pts, color, width, d)) + if head: + (x1, y1), (x2, y2) = points[-2], points[-1] + ang = math.atan2(y2 - y1, x2 - x1) + hl, hw = 11.0, 5.5 + bx, by = x2 - hl * math.cos(ang), y2 - hl * math.sin(ang) + px, py = -math.sin(ang) * hw, math.cos(ang) * hw + add('' + % (x2, y2, bx + px, by + py, bx - px, by - py, color)) + + def card(x, y, w, title, note, color, bg): + add('' % (x, y, w, H_CARD, bg, color)) + add('%s' + % (x + 16, y + 34, CARD_TITLE_FONT, esc(title))) + add('%s' + % (x + 16, y + 58, CARD_NOTE_FONT, esc(note))) + + # 行/画布尺寸 + n_rows = max(len(CONSUMERS), len(BITS)) + main_bottom = row_y(n_rows - 1) + H_CARD + total_h = main_bottom + 78 + + add('' % (W, total_h)) + + # ---- 标题 ---- + add('%s' + % (PAD, TITLE_FONT, esc("就绪屏障(Readiness Barrier)· 启动依赖链"))) + add('%s' + % (PAD, SUB_FONT, + esc("CMSIS-RTOS2 Event Flags · 生产者置位声明「我好了」,消费者阻塞等待"))) + + # ---- 图例 ---- + lx, ly = PAD, 108 + for kind, color, label in [ + ("rect", C_PROD, "生产者(set)"), + ("rect", C_CONS, "消费者(wait)"), + ("rect", "#16a34a", "事件位(Event Flag)"), + ("dot", "#0d9488", "All 汇合(等全部位)"), + ("dash", "#94a3b8", "预留 / 未接入"), + ]: + if kind == "rect": + add('' % (lx, ly - 12, color)) + elif kind == "dot": + add('' % (lx + 8, ly - 4, color)) + else: + add('' % (lx, ly - 4, lx + 16, ly - 4, color)) + lx += 24 + add('%s' % (lx, ly, esc(label))) + lx += text_w(label, 12) + 34 + + # ---- 事件组面板 ---- + py0 = MAIN_Y - 34 + py1 = row_y(len(BITS) - 1) + H_CARD + 14 + add('' % (PANEL_X, py0, PANEL_W, py1 - py0)) + head = "sys_evtHandle(Event Flags 事件组)" + add('%s' + % (PANEL_X + 16, py0 + 26, esc(head))) + for i, b in enumerate(BITS): + by = row_y(i) + add('' % (PANEL_X + 14, by, PANEL_W - 28, H_CARD, b["bg"], b["color"])) + add('' + % (PANEL_X + 28, by + (H_CARD - 30) / 2, b["color"])) + add('%s' + % (PANEL_X + 43, by + H_CARD / 2 + 5, esc(b["bit"][3]))) + tx = PANEL_X + 70 + add('%s' + % (tx, by + 36, BIT_TITLE_FONT, b["color"], esc(b["bit"] + " · " + b["flag"]))) + add('%s' + % (tx, by + 58, BIT_NOTE_FONT, esc(b["note"]))) + + # ---- 生产者卡 + set 箭头 ---- + for p in PRODUCERS: + y, cy = row_y(p["row"]), row_cy(p["row"]) + card(PROD_X, y, CARD_W_L, p["name"], p["note"], C_PROD, BG_PROD) + poly([(PROD_X + CARD_W_L, cy), (PANEL_X, cy)], BITS[p["row"]]["color"]) + add('%s' + % ((PROD_X + CARD_W_L + PANEL_X) / 2.0, cy - 8, LABEL_FONT, + BITS[p["row"]]["color"], esc(p["label"]))) + + # ---- 消费者卡 ---- + for c in CONSUMERS: + card(CONS_X, row_y(c["row"]), CARD_W_R, c["name"], c["note"], C_CONS, BG_CONS) + + # ---- wait 连线:bit1 → netTask(水平) ---- + c0, c1 = row_cy(0), row_cy(1) + col1, col0 = BITS[0]["color"], BITS[1]["color"] + poly([(PANEL_X + PANEL_W, c0), (CONS_X, c0)], col1) + add('%s' + % ((PANEL_X + PANEL_W + CONS_X) / 2.0, c0 - 8, LABEL_FONT, col1, esc("wait"))) + + # ---- wait 连线:bit1 → All 汇合点(下引,与 bit0 相与) ---- + poly([(TAP_X, c0), (TAP_X, c1)], col1, head=False) + add('' % (TAP_X, c1, col0)) + add('All' + % (TAP_X + 9, c1 - 8, LABEL_FONT, col0)) + + # ---- wait 连线:bit0 → 总线 → 4 个消费者 ---- + c_last = row_cy(len(CONSUMERS) - 1) + poly([(PANEL_X + PANEL_W, c1), (BUS_X, c1)], col0) + poly([(BUS_X, c1), (BUS_X, c_last)], col0, head=False) + for c in CONSUMERS[1:]: + poly([(BUS_X, row_cy(c["row"])), (CONS_X, row_cy(c["row"]))], col0) + mid_y = (c1 + c_last) / 2.0 + add('%s' + % (BUS_X - 12, mid_y, BUS_X - 12, mid_y, LABEL_FONT, col0, esc("wait bit0"))) + + add('%s' + % (PAD, total_h - 18, FOOT_FONT, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, total_h + + +def main(): + svg, h = build() + with open(OUT, "w", encoding="utf-8", newline="\n") as f: + f.write(svg) + import xml.etree.ElementTree as ET + ET.fromstring(svg) + print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT)) + + +if __name__ == "__main__": + main()