From 4e9ac0d9c317d6babfede6eca954f5eaad7a1e8a Mon Sep 17 00:00:00 2001 From: wandering1 <1624155937@qq.com> Date: Fri, 28 Aug 2026 16:42:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=B6=E9=97=B4=E5=90=8C=E6=AD=A5=E5=B7=B2?= =?UTF-8?q?=E7=BB=8F=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/sys_clock.c | 76 +- App/sys_clock.h | 10 + App/task/net_task.c | 2 +- App/time_sync.c | 138 +++ App/time_sync.h | 40 + Drivers/BSP/NET/lftpd/lftpd_inet.c | 2 +- Drivers/BSP/NET/net_socket.c | 25 +- Drivers/BSP/NET/net_socket.h | 3 +- MDK-ARM/STM32F407-Demo.uvprojx | 5 + MDK-ARM/build_log.txt | 91 +- Src/freertos.c | 12 + docs/BSD_Socket_API_使用指南.md | 1212 -------------------------- docs/CH395F_Test_Guide.md | 6 +- docs/系统墙钟时间维护说明.md | 348 ++++++++ docs/{ => 芯片手册}/GD5F2GQ5UExxG.md | 0 docs/{ => 芯片手册}/RTL8305NBI-CG.md | 0 docs/{ => 芯片手册}/SD2506API-G.md | 0 docs/{ => 芯片手册}/TPAFE5160.md | 0 test/net_test_task.c | 8 +- test/time_sync_server.py | 75 ++ 20 files changed, 771 insertions(+), 1282 deletions(-) create mode 100644 App/time_sync.c create mode 100644 App/time_sync.h delete mode 100644 docs/BSD_Socket_API_使用指南.md create mode 100644 docs/系统墙钟时间维护说明.md rename docs/{ => 芯片手册}/GD5F2GQ5UExxG.md (100%) rename docs/{ => 芯片手册}/RTL8305NBI-CG.md (100%) rename docs/{ => 芯片手册}/SD2506API-G.md (100%) rename docs/{ => 芯片手册}/TPAFE5160.md (100%) create mode 100644 test/time_sync_server.py diff --git a/App/sys_clock.c b/App/sys_clock.c index 2de4ea3..2389f6e 100644 --- a/App/sys_clock.c +++ b/App/sys_clock.c @@ -44,7 +44,16 @@ static volatile uint32_t s_base_unix_secs = UINT32_MAX; * 内部辅助函数:Unix time ↔ sd2506_time_t 转换 * ============================================================ */ -/* 计算星期(Zeller 公式,0=Sunday),与下方 unix_to_sd2506 中算法一致 */ +/* + * 函数功能:根据公历日期计算星期几 + * 入口参数:y - 年份 uint16_t 1970~2099 + * m - 月份 uint8_t 1~12 + * d - 日 uint8_t 1~31 + * 返回值:星期编号 uint8_t 0=Sunday, 1=Monday, ... 6=Saturday + * 限定条件:输入为公历合法日期 + * 函数说明:1. 采用 Zeller 公式(变种),结果 0 表示周日 + * 2. 与 unix_to_sd2506() 内星期算法保持一致 + */ static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) { uint32_t wy = y; @@ -54,10 +63,22 @@ static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) wy--; wm += 12u; } + /* Zeller 公式:世纪项 + 月项 + 日项,模 7 得星期(0=周日) */ return (uint8_t)(((uint32_t)(wy % 100u + wy / 100u / 4u - wy / 100u / 15u) + (uint32_t)((26u * (wm + 1u)) / 10u) + (uint32_t)(d % 100u)) % 7u); } +/* + * 函数功能:将 Unix 时间戳(自 1970-01-01 00:00:00 起的秒数)转换为 SD2506 RTC 时间结构 + * 入口参数:unix_secs - Unix 时间戳 uint32_t 0~0x7FFFFFFF(~2038) + * p_sd_time - 输出时间结构指针 sd2506_time_t* 不得为 NULL + * 出口参数:p_sd_time - 填充年/月/日/时/分/秒/星期 + * 返回值:无 + * 限定条件:p_sd_time 必须指向有效缓冲区 + * 函数说明:1. 先取模拆分时分秒,再按累计天数推算年月日 + * 2. 星期由 Zeller 公式(与 rtc_calc_week 同算法)得出 + * 3. SD2506 年份字段为相对值(0~99,+2000),故回写时减 2000 + */ static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) { uint32_t y, m, d; @@ -108,6 +129,7 @@ static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) wy--; wm += 12uL; } + /* Zeller 公式同 rtc_calc_week():世纪项 + 月项 + 日项,模 7 得星期(0=周日) */ uint8_t week = ((uint32_t)(wy % 100uLL + wy / 100uLL / 4uLL - wy / 100uLL / 15uLL) + (uint32_t)(((uint64_t)26 * (wm + 1uL)) / 10uLL) + (uint32_t)(d % 100uLL)) % 7uLL; @@ -129,6 +151,17 @@ static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) * 系统墙钟时间维护 * ============================================================ */ +/* + * 函数功能:初始化系统墙钟时间基准 + * 入口参数:无 + * 返回值:0 - 成功(始终返回 0) + * 限定条件:CubeMX 外设初始化完成、SD2506 RTC 驱动已初始化 + * 函数说明:1. 从 SD2506 读取初始时间,必要时按编译期默认宏校时(RTC_DEFAULT_INIT_*) + * 2. 将 RTC 时间换算为 Unix epoch 秒数 + * 3. 记录当前 HAL Tick 与 Unix 秒数为基准,供 sys_clock_get() 后续推算 + * 4. 注意:sd2506_get_time() 返回的 year 已是完整年份(驱动内 +2000), + * 换算时不得再加 2000,否则年份虚增导致 Unix 秒数超 uint32 回绕 + */ int sys_clock_init(void) { sd2506_time_t sd_time; @@ -216,6 +249,15 @@ int sys_clock_init(void) return 0; } +/* + * 函数功能:获取当前系统墙钟时间(Unix 时间戳) + * 入口参数:无 + * 返回值:当前 Unix 秒数 uint32_t 未初始化时返回 UINT32_MAX + * 限定条件:sys_clock_init() 已成功调用 + * 函数说明:1. 以基准 Tick 与基准 Unix 秒数为起点,按当前 HAL Tick 与 1000ms + * 的整除差推算流逝秒数(避免每毫秒累加,减少漂移与溢出风险) + * 2. 若推算结果为负(基准未建立),返回 UINT32_MAX 表示无效 + */ uint32_t sys_clock_get(void) { uint32_t now_tick = HAL_GetTick(); @@ -228,6 +270,17 @@ uint32_t sys_clock_get(void) return (uint32_t)unix_secs; } +/* + * 函数功能:获取当前墙钟时间的可读字符串(YYYY-MM-DD HH:MM:SS) + * 入口参数:buf - 输出字符串缓冲区 char* 不得为 NULL + * buf_size - 缓冲区大小 size_t 须 ≥ 20(含结尾 '\0') + * 出口参数:buf - 填充格式化时间字符串 + * 返回值:buf 指针(便于链式调用) + * 限定条件:buf 指向至少 buf_size 字节的有效缓冲区;sys_clock_init() 已调用 + * 函数说明:1. 内部先取 Unix 秒数,未初始化时输出占位串 "----/--/-- --:--:--" + * 2. 通过 unix_to_sd2506() 转换为 SD2506 结构后格式化 + * 3. 使用 snprintf 防止缓冲区溢出 + */ char *sys_clock_get_str(char *buf, size_t buf_size) { uint32_t unix_secs = sys_clock_get(); @@ -245,6 +298,16 @@ char *sys_clock_get_str(char *buf, size_t buf_size) return buf; } +/* + * 函数功能:设置(更新)系统墙钟时间基准,并可写回 SD2506 RTC + * 入口参数:unix_secs - 新的 Unix 时间戳 uint32_t 0~0x7FFFFFFF + * sd_time - 对应的 RTC 时间结构指针 sd2506_time_t* 可为 NULL(仅更新基准) + * 出口参数:无 + * 返回值:0 - 成功(始终返回 0) + * 限定条件:unix_secs 为合法 Unix 时间戳 + * 函数说明:1. 重置基准 Tick 与基准 Unix 秒数,使后续 sys_clock_get() 以此为准 + * 2. 若 sd_time 非 NULL,将其写回 SD2506 RTC,实现断电保持 + */ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) { /* Step 1: 更新墙钟基准 */ @@ -261,3 +324,14 @@ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) return 0; } + +int sys_clock_set_unix(uint32_t unix_secs) +{ + sd2506_time_t sd; + + /* 内部静态函数:将 Unix 秒数拆分为 SD2506 时间结构 */ + unix_to_sd2506(unix_secs, &sd); + + /* 更新运行期基准并写回 RTC,实现掉电保持 */ + return sys_clock_set(unix_secs, &sd); +} diff --git a/App/sys_clock.h b/App/sys_clock.h index 22d7b35..3e28c10 100644 --- a/App/sys_clock.h +++ b/App/sys_clock.h @@ -58,6 +58,16 @@ char *sys_clock_get_str(char *buf, size_t buf_size); */ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time); +/* + * 函数功能:以 Unix 时间戳设置系统墙钟并写回 SD2506 RTC + * 入口参数:unix_secs - Unix epoch time (秒,本地时间) uint32_t 0~0x7FFFFFFF + * 返回值:0 - 成功 + * 限定条件:sys_clock_init() 已调用 + * 函数说明:内部将 Unix 秒数转换为 sd2506_time_t 后调用 sys_clock_set(), + * 便于时间同步模块(如 PC 推送)直接以时间戳校时 + */ +int sys_clock_set_unix(uint32_t unix_secs); + #ifdef __cplusplus } #endif diff --git a/App/task/net_task.c b/App/task/net_task.c index f5208bf..772cdc1 100644 --- a/App/task/net_task.c +++ b/App/task/net_task.c @@ -109,6 +109,6 @@ void net_task_func(void const *arg) for (;;) { net_poll(); net_process_messages(); - osDelay(10); + osDelay(1); } } diff --git a/App/time_sync.c b/App/time_sync.c new file mode 100644 index 0000000..0ddbb32 --- /dev/null +++ b/App/time_sync.c @@ -0,0 +1,138 @@ +/* + * 模块名称:Time Sync — PC 本地时间推送接收(TCP) + * 模块功能:在独立任务 timeSyncTask 中以 TCP Server 方式监听端口,接收 PC 推送的本地时间包, + * 调用 sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC + * 适用平台:STM32F407ZGTx + CH395F + * 作者:王建锋 + * 创建日期:2026-08-27 + * 修改记录: + * v1.0 2026-08-27 王建锋 创建初始版本(UDP 推送,netTask 内轮询) + * v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask, + * 所有 socket 操作经 net_socket 线程安全 API 走消息队列, + * netTask 仅负责 net_poll/net_process_messages,多任务并发安全 + */ + +#include +#include "time_sync.h" +#include "net_socket.h" +#include "sys_clock.h" + +#define DBG_TAG "[TIME_SYNC]" +#include "dbg_log.h" + +/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */ +#define TIME_SYNC_PORT 8888 + +/* + * 函数功能:创建并监听 TCP 时间同步服务端口 + * 入口参数:无 + * 返回值:监听 Socket 描述符(>=0),失败返回 -1 + * 限定条件:net_init() 已成功;可在非 netTask 上下文调用 + * 函数说明:net_socket/net_bind/net_listen 均为线程安全 API,内部经消息队列由 netTask 串行处理。 + * 单连接模式下监听 Socket 在收到连接后会转为数据通道,连接关闭后即被释放, + * 因此每次调用都会新建监听 Socket(与 lftpd 同模式),失败自动重试。 + */ +static int time_sync_open_listener(void) +{ + int retry; + int s = -1; + + for (retry = 0; retry < 5; retry++) { + s = net_socket(NET_AF_INET, NET_SOCK_STREAM); + if (s < 0) { + DBG_ERROR("time_sync: net_socket FAIL"); + vTaskDelay(pdMS_TO_TICKS(500)); + continue; + } + + struct net_sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = NET_AF_INET; + addr.sin_port = net_htons(TIME_SYNC_PORT); + addr.sin_addr.s_addr = NET_INADDR_ANY; + + if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { + DBG_ERROR("time_sync: bind port %d FAIL", TIME_SYNC_PORT); + net_close(s); + vTaskDelay(pdMS_TO_TICKS(2000)); + continue; + } + + if (net_listen(s) != 0) { + DBG_ERROR("time_sync: listen FAIL"); + net_close(s); + vTaskDelay(pdMS_TO_TICKS(2000)); + continue; + } + + DBG_INFO("time_sync: TCP listen on :%d (sock=%d)", TIME_SYNC_PORT, s); + return s; + } + + return -1; +} + +/* + * 函数功能:处理一个已建立的时间同步连接 + * 入口参数:conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身) + * 返回值:无 + * 函数说明:阻塞读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后写回 RTC,然后关闭连接 + */ +static void time_sync_handle_conn(int conn_sock) +{ + uint8_t buf[16]; + int len; + + len = net_recv(conn_sock, buf, sizeof(buf), 0); + if (len >= 8) { + if (buf[0] == 'T' && buf[1] == 'I' && buf[2] == 'M' && buf[3] == 'E') { + uint32_t epoch = ((uint32_t)buf[4] << 24) | + ((uint32_t)buf[5] << 16) | + ((uint32_t)buf[6] << 8) | + ((uint32_t)buf[7]); + sys_clock_set_unix(epoch); + DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch); + } + } + + net_close(conn_sock); +} + +/* + * 函数功能:时间同步任务主体(独立 FreeRTOS 任务) + * 入口参数:argument - 未使用 + * 返回值:无 + * 限定条件:net_init() 已由 netTask 完成;netTask 正在处理消息队列 + * 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。 + * 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。 + */ +void timeSyncTask(void *argument) +{ + (void)argument; + + /* 等网络栈起来(与 StartFtpTask 的 7s 延迟同风格,确保 netTask 已开始处理消息) */ + vTaskDelay(pdMS_TO_TICKS(8000)); + + for (;;) { + int ls = time_sync_open_listener(); + if (ls < 0) { + DBG_ERROR("time_sync: open listener FAIL, retry later"); + vTaskDelay(pdMS_TO_TICKS(2000)); + continue; + } + + /* 等待 PC 连接:net_accept 在无连接时立即返回 -1,循环重试同一监听 Socket */ + int conn = -1; + do { + conn = net_accept(ls, NULL, NULL); + if (conn < 0) { + vTaskDelay(pdMS_TO_TICKS(200)); + } + } while (conn < 0); + + time_sync_handle_conn(conn); + /* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */ + + vTaskDelay(pdMS_TO_TICKS(50)); + } +} diff --git a/App/time_sync.h b/App/time_sync.h new file mode 100644 index 0000000..8cbe711 --- /dev/null +++ b/App/time_sync.h @@ -0,0 +1,40 @@ +/* + * 模块名称:Time Sync — PC 本地时间推送接收(TCP) + * 模块功能:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包, + * 更新系统墙钟并写回 SD2506 RTC + * 适用平台:STM32F407ZGTx + CH395F + * 作者:王建锋 + * 创建日期:2026-08-27 + * 修改记录: + * v1.0 2026-08-27 王建锋 创建初始版本(UDP 推送) + * v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask + */ + +#ifndef __TIME_SYNC_H +#define __TIME_SYNC_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 监听端口:PC 推送端(TCP 客户端)需连到本端口 */ +#define TIME_SYNC_PORT 8888 + +/* + * 函数功能:时间同步任务主体(独立 FreeRTOS 任务) + * 入口参数:argument - FreeRTOS 传入参数(未使用) + * 返回值:无 + * 限定条件:net_init() 已由 netTask 完成;网络消息队列由 netTask 处理 + * 函数说明:以 TCP Server 方式监听 TIME_SYNC_PORT,接收 PC 推送的本地时间包, + * 经消息队列(由 netTask 串行执行)完成 socket 操作,更新系统墙钟。 + * 该任务与 FTP 等其它网络任务并发安全。 + */ +void timeSyncTask(void *argument); + +#ifdef __cplusplus +} +#endif + +#endif /* __TIME_SYNC_H */ diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c index 4b2854b..3dd7b89 100644 --- a/Drivers/BSP/NET/lftpd/lftpd_inet.c +++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c @@ -11,7 +11,7 @@ int lftpd_inet_listen(int port) int s; for (retry = 0; retry < 5; retry++) { - s = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + s = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (s < 0) { if (retry == 0) DBG_ERROR("create socket"); vTaskDelay(pdMS_TO_TICKS(500)); diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index e67d99b..c88994c 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -123,7 +123,7 @@ int net_init(const char *ip, const char *mask, const char *gateway) { ch395f_set_retrans_count(8); /* 重传 8 次*/ ch395f_set_retrans_period(500); /* 每次间隔 500ms,总计 4s 重传窗口 */ - /* 设置 TCP KeepAlive 参数(必须在 INIT_CH395 之前,单位 ms,必须为 500 的倍数,IDLE > INTVL�?*/ + /* 设置 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 次*/ @@ -362,12 +362,11 @@ void net_process_messages(void) { * 函数功能:创建 Socket,分配空闲 Socket 控制块并初始化状态 * 入口参数:domain - 地址族,仅支持 NET_AF_INET int * type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP) int - * protocol - 通常为 0 int * 返回值:Socket 描述符 (0~7),失败返回 -1 int * 限定条件:net_init() 已调用成功;s_net_initialized == 1 * 函数说明:自动分配一个空闲的 Socket 控制块(in_use=0),初始化为 CREATED 状态 */ -int net_socket(int domain, int type, int protocol) { +int net_socket(int domain, int type) { int sockfd; net_sock_t *p_sock; @@ -411,7 +410,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { net_sock_t *p_sock; const struct net_sockaddr_in *p_addr_in; - /* 参数检�?*/ + /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { s_net_errno = NET_ERR_BADF; return -1; @@ -1257,7 +1256,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, int sent = 0; uint32_t tick_start; - /* 参数检�?*/ + /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { s_net_errno = NET_ERR_BADF; return -1; @@ -1292,7 +1291,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, tick_start = HAL_GetTick(); - /* 发送数�?*/ + /* 发送数据 */ while (sent < len) { int chunk = len - sent; if (chunk > NET_SEND_CHUNK_MAX) { @@ -1315,7 +1314,7 @@ int net_sendto(int sockfd, const void *buf, int len, int flags, sent += chunk; p_sock->send_bytes += (uint32_t)chunk; - /* 等待发送完�?*/ + /* 等待发送完成 */ tick_start = HAL_GetTick(); while (!p_sock->send_ready) { net_poll(); @@ -1346,7 +1345,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, int recv_len; uint32_t tick_start; - /* 参数检�?*/ + /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { s_net_errno = NET_ERR_BADF; return -1; @@ -1370,7 +1369,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, recv_len = ch395f_get_recv_len(sockfd); if (recv_len > 0) { - /* UDP Server 模式:前 8 字节为固定格式(IP + 端口 + 长度�?*/ + /* UDP Server 模式:前 8 字节为固定格式(IP + 端口 + 长度)*/ if (p_sock->type == NET_SOCK_DGRAM && recv_len >= 8) { uint8_t header[8]; ch395f_read_recv_buf(sockfd, header, 8); @@ -1381,7 +1380,7 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, *addrlen >= (int)sizeof(struct net_sockaddr_in)) { struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; p_addr_in->sin_family = NET_AF_INET; - /* CH395F UDP �? [0-1] reserved [2-3] src_port(LE) [4-7] src_ip */ + /* CH395F UDP 头 [0-1] reserved [2-3] src_port(LE) [4-7] src_ip */ p_addr_in->sin_port = net_htons((uint16_t)(header[2] | (header[3] << 8))); p_addr_in->sin_addr.s_addr = ((uint32_t)header[4]) | ((uint32_t)header[5] << 8) | @@ -1400,13 +1399,13 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, } } - /* 非阻塞模式检�?*/ + /* 非阻塞模式检查 */ if (flags & NET_MSG_DONTWAIT) { s_net_errno = NET_ERR_WOULDBLOCK; return -1; } - /* 超时检�?*/ + /* 超时检查 */ if (NET_RECV_TIMEOUT_MS > 0 && (HAL_GetTick() - tick_start) >= NET_RECV_TIMEOUT_MS) { s_net_errno = NET_ERR_TIMEDOUT; @@ -1482,7 +1481,7 @@ int net_close_sock(net_sock_t *p_sock) { int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg) { net_sock_t *p_sock; - /* 参数检�?*/ + /* 参数检查 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { s_net_errno = NET_ERR_BADF; return -1; diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index 8ccb188..4a338d5 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -76,12 +76,11 @@ int net_poll(void); * 函数功能:创建 socket * 入口参数:domain - 地址族,仅支持 AF_INET * type - SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP) - * protocol - 通常为 0 * 返回值:socket 描述符 (0~7),失败返回 -1 * 限定条件:net_init() 已调用 * 函数说明:自动分配空闲 Socket 控制块 */ -int net_socket(int domain, int type, int protocol); +int net_socket(int domain, int type); /* * 函数功能:绑定本地地址和端口 diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index d7cb044..37ffcf6 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -765,6 +765,11 @@ 1 ..\App\sys_clock.c + + time_sync.c + 1 + ..\App\time_sync.c + net_task.c 1 diff --git a/MDK-ARM/build_log.txt b/MDK-ARM/build_log.txt index 1389fc3..b66aae7 100644 --- a/MDK-ARM/build_log.txt +++ b/MDK-ARM/build_log.txt @@ -4,71 +4,72 @@ assembling startup_stm32f407xx.s... compiling ringbuf.c... compiling crc.c... compiling stm32f4xx_hal_flash_ramfunc.c... -compiling stm32f4xx_hal_gpio.c... -compiling i2c.c... -compiling stm32f4xx_hal_msp.c... -compiling stm32f4xx_hal_timebase_tim.c... -compiling dma.c... -compiling stm32f4xx_hal_dma_ex.c... -compiling spi.c... compiling stm32f4xx_hal_flash_ex.c... -compiling stm32f4xx_hal_dma.c... -compiling stm32f4xx_it.c... +compiling stm32f4xx_hal_timebase_tim.c... +compiling spi.c... +compiling gpio.c... +compiling dma.c... +compiling i2c.c... +compiling stm32f4xx_hal_rcc.c... compiling stm32f4xx_hal_flash.c... +compiling adc_task.c... +compiling stm32f4xx_hal_msp.c... +compiling stm32f4xx_it.c... +compiling usart.c... +compiling app_main.c... +compiling net_task.c... compiling sys_clock.c... +compiling rs485_task.c... compiling stm32f4xx_hal_rcc_ex.c... compiling stm32f4xx_hal_tim_ex.c... -compiling app_main.c... -compiling gpio.c... -compiling usart.c... -compiling stm32f4xx_hal_rcc.c... -compiling rs485_task.c... -compiling adc_task.c... -compiling freertos.c... +compiling time_sync.c... compiling main.c... -compiling net_task.c... -compiling lftpd_string.c... +compiling freertos.c... +compiling stm32f4xx_hal_gpio.c... compiling stm32f4xx_hal_tim.c... -compiling croutine.c... -compiling event_groups.c... -compiling list.c... -compiling queue.c... -compiling stream_buffer.c... +compiling stm32f4xx_hal_dma_ex.c... +compiling lftpd_string.c... compiling stm32f4xx_hal_pwr.c... -compiling timers.c... +compiling stm32f4xx_hal_dma.c... +compiling croutine.c... +compiling list.c... +compiling event_groups.c... +compiling stream_buffer.c... +compiling queue.c... compiling tasks.c... +compiling timers.c... +compiling stm32f4xx_hal_cortex.c... +compiling stm32f4xx_hal_pwr_ex.c... compiling heap_4.c... +compiling system_stm32f4xx.c... compiling port.c... compiling journal.c... compiling map.c... -compiling ff.c... -compiling stm32f4xx_hal_pwr_ex.c... -compiling stm32f4xx_hal_exti.c... -compiling stm32f4xx_hal_i2c_ex.c... -compiling stm32f4xx_hal_cortex.c... -compiling stm32f4xx_hal.c... -compiling system_stm32f4xx.c... -compiling sd2506.c... -compiling rs485.c... -compiling stm32f4xx_hal_spi.c... -compiling ch395f.c... -compiling cmsis_os2.c... compiling lftpd_io.c... -compiling tpafe5160.c... -compiling nand_ftl.c... -compiling gd5f2gq5ue.c... -compiling net_select.c... -compiling stm32f4xx_hal_uart.c... +compiling stm32f4xx_hal.c... +compiling stm32f4xx_hal_i2c_ex.c... +compiling stm32f4xx_hal_exti.c... +compiling ch395f.c... +compiling rs485.c... compiling lftpd_inet.c... +compiling ff.c... +compiling tpafe5160.c... +compiling sd2506.c... +compiling gd5f2gq5ue.c... +compiling nand_ftl.c... +compiling stm32f4xx_hal_uart.c... +compiling stm32f4xx_hal_spi.c... +compiling net_select.c... compiling lftpd.c... compiling net_socket.c... compiling stm32f4xx_hal_i2c.c... -compiling ch395f_test_task.c... -compiling net_test_task.c... +compiling cmsis_os2.c... compiling gd5f_test_task.c... +compiling net_test_task.c... +compiling ch395f_test_task.c... compiling storage_test_task.c... linking... -Program Size: Code=58880 RO-data=2436 RW-data=408 ZI-data=51872 +Program Size: Code=59744 RO-data=2556 RW-data=412 ZI-data=51868 FromELF: creating hex file... ".\STM32F407-Demo\STM32F407-Demo.axf" - 0 Error(s), 0 Warning(s). -Build Time Elapsed: 00:00:17 +Build Time Elapsed: 00:00:15 diff --git a/Src/freertos.c b/Src/freertos.c index bc0d77c..516c610 100644 --- a/Src/freertos.c +++ b/Src/freertos.c @@ -36,6 +36,7 @@ #include "lftpd.h" #include "../test/ch395f_test_task.h" #include "../test/ch395f_test.h" /* ENABLE_NET_LAYER_TESTS:netTask 条件创建 */ +#include "time_sync.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -55,6 +56,12 @@ /* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN Variables */ /* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/storage_test_task.c */ +osThreadId_t timeSyncTaskHandle; +const osThreadAttr_t timeSyncTask_attributes = { + .name = "timeSyncTask", + .stack_size = 512 * 4, + .priority = (osPriority_t) osPriorityNormal, +}; /* USER CODE END Variables */ /* Definitions for defaultTask */ osThreadId_t defaultTaskHandle; @@ -141,6 +148,8 @@ void MX_FREERTOS_Init(void) { /* creation of ftpTask */ ftpTaskHandle = osThreadNew(StartFtpTask, NULL, &ftpTask_attributes); + + /* creation of ch395fTestTask */ ch395fTestTaskHandle = osThreadNew(StartCh395fTestTask, NULL, &ch395fTestTask_attributes); @@ -150,6 +159,9 @@ void MX_FREERTOS_Init(void) { /* NET 层测试模式(阶段 6~10):完整网络栈,netTask 负责轮询与消息处理 */ netTaskHandle = osThreadNew(StartNetTask, NULL, &netTask_attributes); #endif + + /* 本地时间同步任务(PC 推送 TCP):依赖 netTask 处理消息队列 */ + timeSyncTaskHandle = osThreadNew(timeSyncTask, NULL, &timeSyncTask_attributes); /* USER CODE END RTOS_THREADS */ /* USER CODE BEGIN RTOS_EVENTS */ diff --git a/docs/BSD_Socket_API_使用指南.md b/docs/BSD_Socket_API_使用指南.md deleted file mode 100644 index ed891bb..0000000 --- a/docs/BSD_Socket_API_使用指南.md +++ /dev/null @@ -1,1212 +0,0 @@ -# BSD Socket API 使用指南 - -## 目录 - -1. [概述](#1-概述) -2. [架构说明](#2-架构说明) -3. [线程安全设计](#3-线程安全设计) -4. [快速开始](#4-快速开始) -5. [TCP 编程](#5-tcp-编程) -6. [UDP 编程](#6-udp-编程) -7. [I/O 多路复用](#7-io-多路复用) -8. [事件回调机制](#8-事件回调机制) -9. [字节序处理](#9-字节序处理) -10. [API 参考](#10-api-参考) -11. [完整示例](#11-完整示例) -12. [常见问题](#12-常见问题) - ---- - -## 1. 概述 - -本项目基于 CH395F 以太网协议栈芯片,提供了一套兼容 Linux BSD Socket API 的网络编程接口。这套接口屏蔽了底层硬件细节,让嵌入式网络编程与标准 Linux 网络编程保持一致。 - -### 1.1 支持的功能 - -| 功能 | 说明 | -|------|------| -| TCP Client | 主动连接远程服务器 | -| TCP Server(单连接) | 支持 1 个客户端连接 | -| UDP Client | 发送 UDP 数据报 | -| UDP Server | 接收 UDP 数据报 | -| select/poll | I/O 多路复用 | -| 事件回调 | 异步事件通知 | - -### 1.2 与 Linux Socket 的差异 - -| 特性 | Linux Socket | 本实现 | -|------|--------------|--------| -| Socket 数量 | 无限制 | 最多 8 个 | -| 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 | -| 错误码 | errno | `net_get_errno()` | -| 头文件 | `` | `"net_socket.h"` | - ---- - -## 2. 架构说明 - -### 2.1 分层架构 - -``` -┌──────────────────────────────────────────────────────────────┐ -│ 应用层任务 (多个 FreeRTOS Task) │ -│ net_send() / net_recv() / net_close() (线程安全封装) │ -├──────────────────────────────────────────────────────────────┤ -│ 消息队列 (osMessageQueue) │ -│ 由 CubeMX 生成,在 MX_FREERTOS_Init() 中创建 │ -├──────────────────────────────────────────────────────────────┤ -│ netTask (网络任务线程) │ -│ net_process_messages() + net_poll() — 串行化执行 │ -│ net_send_sock() / net_recv_sock() / net_close_sock() │ -├──────────────────────────────────────────────────────────────┤ -│ net_socket.c (BSD Socket API 核心) │ -│ Socket 状态机 + CH395F 命令封装 │ -├──────────────────────────────────────────────────────────────┤ -│ ch395f.c/h (底层 SPI 驱动) │ -│ 所有 SPI 事务必须由 netTask 发起 │ -├──────────────────────────────────────────────────────────────┤ -│ CH395F 硬件 (SPI2) │ -└──────────────────────────────────────────────────────────────┘ -``` - -### 2.2 文件说明 - -| 文件 | 说明 | -|------|------| -| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) | -| `net_types.h` | 类型定义(地址结构、Socket 控制块等) | -| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 | -| `net_socket.c` | API 核心实现,含消息队列处理、线程安全封装、内部 `_sock()` 版本 | -| `net_select.h` | select/poll API 头文件 | -| `net_select.c` | select/poll 实现 | - -### 2.4 Socket 状态机 - -``` - net_socket() - │ - ▼ - ┌──────────── CREATED ────────────┐ - │ │ │ - │ net_bind() │ - │ │ │ - │ ▼ │ - │ BOUND ─────────────────┤ - │ │ │ │ - │ net_listen() net_connect() │ - │ │ │ │ - │ ▼ ▼ │ - │ LISTENING CONNECTING │ - │ │ │ │ - │ net_accept() │ (连接成功) │ - │ │ │ │ - │ ▼ ▼ │ - │ ESTABLISHED ◄──────────────┘ - │ │ │ - │ net_accept() 返回监听 Socket 自身 │ - │ │ │ - │ net_send/recv() │ - │ │ │ - │ net_close() │ - │ │ │ - │ ▼ │ - └────── CLOSED ──────────────────┘ -``` - -> **单连接 Server 自动重监听(`auto_relisten`)**:`net_listen()` 会为 TCP 监听 Socket 置 `auto_relisten = 1`。连接断开(`net_recv` 收到 EOF / 空闲 `net_accept`)时,net 层**自动**重新 `OPEN + TCP_LISTEN`,状态由 `ESTABLISHED`/`CLOSED` 自动回到 `LISTENING`,**应用层无需调用 `net_close()` + `net_listen()`**。这与 Linux BSD Socket“监听 Socket 常驻”语义一致——`net_accept()` 在每条新连接到达时返回,服务端循环不必维护重监听逻辑。 - ---- - -## 3. 线程安全设计 - -### 3.1 设计背景 - -NET 层(`net_socket.c` + `ch395f.c`)不是线程安全的: -- Socket 控制块(`s_net_socks[]`)无互斥保护 -- CH395F SPI 事务不重入(STM32 HAL SPI 无内部锁) -- 状态机在多个任务中并发访问会崩溃 - -引入消息队列将所有的 CH395F 操作串行化到 `netTask` 中执行。 - -### 3.2 消息队列模式 - -``` - App Task A App Task B netTask - | | | - net_send() net_recv() net_process_messages() - | | + net_poll() - ▼ ▼ │ - ┌─────────┐ ┌─────────┐ │ - │send msg │ │recv msg │ │ - └────┬────┘ └────┬────┘ │ - │ │ │ - └─────────┬─────────┘ │ - ▼ ▼ - ┌──────────────────┐ ┌──────────────┐ - │ 消息队列 │──────────│ 取出消息 │ - │ osMessageQueue │ dequeue │ 执行操作 │ - └──────────────────┘ │ 写结果 │ - ▲ │ 通知调用方 │ - │ └──────┬───────┘ - │ ulTaskNotifyTake() │ - │ (阻塞等待结果) │ xTaskNotifyGive() - │ │ - ┌─────┴──────┐ │ - │ 返回结果给 │◄──────────────────────┘ - │ 调用方 │ - └────────────┘ -``` - -### 3.3 关键规则 - -| 规则 | 说明 | -|------|------| -| **`netTask` 外调用** | 使用 `net_send()` / `net_recv()` / `net_close()` 公共 API | -| **`netTask` 内调用** | 使用 `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` 内部 API | -| **创建/绑定/监听** | 在 `main()` 初始化阶段完成(`osKernelStart()` 之前),无需队列 | -| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环中调用 | - -### 3.4 内部 API 说明 - -内部 API(带 `_sock` 后缀)直接操作 Socket 控制块和 CH395F,不经过消息队列。用于 `netTask` 中的网络业务逻辑,避免向自身发送消息造成死锁: - -```c -/* 在 netTask 中直接操作 Socket,绕过消息队列 */ -int net_send_sock(net_sock_t *p_sock, const void *buf, int len); -int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags); -int net_close_sock(net_sock_t *p_sock); -``` - -### 3.5 注意事项 - -- **消息队列由 CubeMX 创建**:在 `MX_FREERTOS_Init()`(`osKernelInitialize()` 之后)中调用 `osMessageQueueNew(8, sizeof(net_msg_t), NULL)` 创建 -- **队列句柄全局可见**:通过 `extern osMessageQueueId_t netMsgQueueHandle` 引用 -- **队列满时**:`net_send()` / `net_recv()` 会阻塞等待,直到 `netTask` 处理完消息腾出空间 -- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 -- **`net_socket()` / `net_bind()` / `net_listen()` 无需线程安全**:这些仅在初始化阶段调用,消息队列仅保护数据收发和连接关闭 - ---- - -## 4. 快速开始 - -### 4.1 包含头文件 - -```c -#include "net_socket.h" -#include "net_select.h" -``` - -### 4.2 初始化网络 - -```c -int main(void) -{ - /* 初始化网络子系统 - * 参数:IP地址, 子网掩码, 网关 - * 传入 NULL 使用 DHCP 自动获取 - */ - int ret = net_init("192.168.1.100", "255.255.255.0", "192.168.1.1"); - if (ret != 0) { - printf("网络初始化失败!\r\n"); - return -1; - } - - /* 其他初始化... */ - - while (1) - { - net_poll(); /* 必须在主循环中调用 */ - /* 其他任务... */ - } -} -``` - -### 4.3 主循环要求 - -**重要**:本项目使用 FreeRTOS 多任务架构,`net_poll()` 和 `net_process_messages()` 均在 `netTask`(FreeRTOS 线程)中定期调用,而非主循环。 - -- **主循环**:系统初始化完成后进入空循环,等待 `osKernelStart()` 启动调度器 -- **netTask**:在任务函数中持续运行,每 10ms 调用一次 `net_poll()` + `net_process_messages()`,负责: - - 轮询 CH395F INT# 引脚电平(GPIO 轮询,无需 EXTI 中断) - - 通过 `GET_GLOB_INT_STATUS_ALL`(2字节版)读取所有 8 个 Socket 的中断状态 - - 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT) - - 触发事件回调 - - 处理数据接收 - -```c -int main(void) -{ - /* 外设初始化... */ - - /* BSD Socket API 初始化 */ - net_init("192.168.1.100", "255.255.255.0", "192.168.1.1"); - - /* 创建 Socket、绑定、监听... */ - - osKernelInitialize(); - MX_FREERTOS_Init(); /* 创建 netTask 等 FreeRTOS 对象 */ - osKernelStart(); /* 启动调度器 */ - - while (1) { } /* 空循环,控制流已交给调度器 */ -} - -/* netTask 中(freertos.c): */ -void StartNetTask(void *argument) -{ - for (;;) - { - net_poll(); /* CH395F 中断轮询 + Socket 状态更新 */ - net_process_messages(); /* 处理其他任务发送的网络操作请求 */ - - /* 应用层网络业务逻辑... */ - - osDelay(10); /* 10ms 周期 */ - } -} -``` - ---- - -## 5. TCP 编程 - -### 5.1 TCP Server(单连接) - -单连接模式下,1 个 Socket 既做监听又做数据通信: - -```c -#include "net_socket.h" - -void tcp_server_single(void) -{ - int listen_sock; - struct net_sockaddr_in addr; - struct net_sockaddr_in client_addr; - int client_addr_len; - char buf[256]; - int len; - - /* 1. 创建 Socket */ - listen_sock = net_socket(AF_INET, SOCK_STREAM, 0); - if (listen_sock < 0) { - printf("创建 Socket 失败\r\n"); - return; - } - - /* 2. 绑定地址和端口 */ - addr.sin_family = AF_INET; - addr.sin_port = net_htons(8080); /* 端口号 8080 */ - addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); /* 监听所有网卡 */ - - if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { - printf("绑定失败\r\n"); - net_close(listen_sock); - return; - } - - /* 3. 开始监听 */ - if (net_listen(listen_sock) != 0) { - printf("监听失败\r\n"); - net_close(listen_sock); - return; - } - - printf("TCP Server 启动,监听端口 8080\r\n"); - - /* 4. 主循环 */ - while (1) - { - net_poll(); - - /* 尝试接受新连接 */ - client_addr_len = sizeof(client_addr); - int client_sock = net_accept(listen_sock, - (struct net_sockaddr *)&client_addr, &client_addr_len); - - if (client_sock >= 0) - { - char ip_str[16]; - printf("新连接: %s:%d\r\n", - net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str), - net_ntohs(client_addr.sin_port)); - - /* 接收和发送数据(单连接模式:client_sock 即 listen_sock 自身) */ - while (1) - { - len = net_recv(client_sock, buf, sizeof(buf) - 1, 0); - if (len <= 0) { - /* 对端关闭:net 层已在 EOF 时自动重监听, - 直接 break,外层 net_accept 会接下一个连接 */ - printf("连接断开,net 层自动重监听\r\n"); - break; - } - - buf[len] = '\0'; - printf("收到: %s\r\n", buf); - - /* 回显数据 */ - net_send(client_sock, buf, len, 0); - } - - /* 单连接模式不要在此 net_close(client_sock):它即监听 Socket, - 一旦关闭需重新 net_socket+net_bind+net_listen;重监听已由 net 层自动完成 */ - } - } -} -``` - -### 5.2 TCP Client - -```c -#include "net_socket.h" - -void tcp_client(void) -{ - int sock; - struct net_sockaddr_in addr; - char buf[256]; - int len; - - /* 1. 创建 Socket */ - sock = net_socket(AF_INET, SOCK_STREAM, 0); - if (sock < 0) { - printf("创建 Socket 失败\r\n"); - return; - } - - /* 2. 设置服务器地址 */ - addr.sin_family = AF_INET; - addr.sin_port = net_htons(8080); - addr.sin_addr.s_addr = net_inet_addr("192.168.1.200"); - - /* 3. 连接服务器 */ - printf("正在连接服务器...\r\n"); - if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { - printf("连接失败\r\n"); - net_close(sock); - return; - } - - /* 4. 等待连接建立 */ - while (1) - { - net_poll(); - - net_sock_t *sock_info = net_get_sock(sock); - if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED) { - printf("连接成功!\r\n"); - break; - } - - HAL_Delay(10); - } - - /* 5. 发送数据 */ - const char *msg = "Hello, CH395F!"; - net_send(sock, msg, strlen(msg), 0); - printf("发送: %s\r\n", msg); - - /* 6. 接收数据 */ - len = net_recv(sock, buf, sizeof(buf) - 1, 0); - if (len > 0) { - buf[len] = '\0'; - printf("收到: %s\r\n", buf); - } - - /* 7. 关闭连接 */ - net_close(sock); -} -``` - ---- - -## 6. UDP 编程 - -### 6.1 UDP Client - -```c -#include "net_socket.h" - -void udp_client(void) -{ - int sock; - struct net_sockaddr_in local_addr; - struct net_sockaddr_in dest_addr; - char buf[256]; - int len; - - /* 1. 创建 UDP Socket */ - sock = net_socket(AF_INET, SOCK_DGRAM, 0); - if (sock < 0) { - printf("创建 Socket 失败\r\n"); - return; - } - - /* 2. 绑定本地端口(可选) */ - local_addr.sin_family = AF_INET; - local_addr.sin_port = net_htons(12345); - local_addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); - - net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr)); - - /* 3. 设置目标地址 */ - dest_addr.sin_family = AF_INET; - dest_addr.sin_port = net_htons(8888); - dest_addr.sin_addr.s_addr = net_inet_addr("192.168.1.200"); - - /* 4. 发送数据 */ - const char *msg = "UDP Hello!"; - net_sendto(sock, msg, strlen(msg), 0, - (struct net_sockaddr *)&dest_addr, sizeof(dest_addr)); - printf("发送: %s\r\n", msg); - - /* 5. 接收数据 */ - struct net_sockaddr_in src_addr; - int src_addr_len = sizeof(src_addr); - - len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0, - (struct net_sockaddr *)&src_addr, &src_addr_len); - - if (len > 0) { - buf[len] = '\0'; - char ip_str[16]; - printf("收到: %s (from %s:%d)\r\n", buf, - net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str), - net_ntohs(src_addr.sin_port)); - } - - net_close(sock); -} -``` - -### 6.2 UDP Server - -```c -#include "net_socket.h" - -void udp_server(void) -{ - int sock; - struct net_sockaddr_in addr; - char buf[256]; - - /* 1. 创建 UDP Socket */ - sock = net_socket(AF_INET, SOCK_DGRAM, 0); - if (sock < 0) { - printf("创建 Socket 失败\r\n"); - return; - } - - /* 2. 绑定本地端口 */ - addr.sin_family = AF_INET; - addr.sin_port = net_htons(8888); - addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); - - if (net_bind(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { - printf("绑定失败\r\n"); - net_close(sock); - return; - } - - printf("UDP Server 启动,监听端口 8888\r\n"); - - /* 3. 循环接收数据 */ - while (1) - { - net_poll(); - - struct net_sockaddr_in src_addr; - int src_addr_len = sizeof(src_addr); - - int len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0, - (struct net_sockaddr *)&src_addr, &src_addr_len); - - if (len > 0) { - buf[len] = '\0'; - char ip_str[16]; - printf("收到: %s (from %s:%d)\r\n", buf, - net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str), - net_ntohs(src_addr.sin_port)); - - /* 回显数据 */ - net_sendto(sock, buf, len, 0, - (struct net_sockaddr *)&src_addr, src_addr_len); - } - } -} -``` - ---- - -## 7. I/O 多路复用 - -### 7.1 select 使用 - -`net_select()` 可以同时监控多个 Socket 的读写事件: - -```c -#include "net_select.h" - -void select_example(void) -{ - int sock1, sock2; - net_fd_set readfds; - struct net_timeval timeout; - - /* 创建并连接两个 Socket ... */ - - while (1) - { - NET_FD_ZERO(&readfds); - NET_FD_SET(sock1, &readfds); - NET_FD_SET(sock2, &readfds); - - timeout.tv_sec = 0; - timeout.tv_usec = 500000; /* 500ms 超时 */ - - int ret = net_select(sock2 + 1, &readfds, NULL, NULL, &timeout); - - if (ret > 0) { - if (NET_FD_ISSET(sock1, &readfds)) { - char buf[128]; - int len = net_recv(sock1, buf, sizeof(buf), 0); - /* 处理数据... */ - } - - if (NET_FD_ISSET(sock2, &readfds)) { - char buf[128]; - int len = net_recv(sock2, buf, sizeof(buf), 0); - /* 处理数据... */ - } - } else if (ret == 0) { - /* 超时 */ - } - } -} -``` - -### 7.2 poll 使用 - -`net_poll_events()` 使用 `pollfd` 结构体数组: - -```c -#include "net_select.h" - -void poll_example(void) -{ - net_pollfd fds[2]; - - fds[0].fd = sock1; - fds[0].events = NET_POLLIN; - - fds[1].fd = sock2; - fds[1].events = NET_POLLIN; - - while (1) - { - int ret = net_poll_events(fds, 2, 500); /* 500ms 超时 */ - - if (ret > 0) { - if (fds[0].revents & NET_POLLIN) { - char buf[128]; - int len = net_recv(fds[0].fd, buf, sizeof(buf), 0); - /* 处理数据... */ - } - - if (fds[1].revents & NET_POLLIN) { - char buf[128]; - int len = net_recv(fds[1].fd, buf, sizeof(buf), 0); - /* 处理数据... */ - } - } - } -} -``` - -### 7.3 事件掩码说明 - -| 事件 | 说明 | -|------|------| -| `NET_POLLIN` | 有数据可读,或有新连接到达(监听 Socket) | -| `NET_POLLOUT` | 发送缓冲区有空间,可以写入数据 | -| `NET_POLLERR` | 发生错误 | -| `NET_POLLHUP` | 连接挂起(对端关闭) | -| `NET_POLLNVAL` | 无效的文件描述符 | - ---- - -## 8. 事件回调机制 - -可以通过注册回调函数,异步获取 Socket 事件通知: - -### 8.1 定义回调函数 - -```c -#include "net_socket.h" - -void my_event_callback(int sockfd, net_event_t event, void *arg) -{ - switch (event) - { - case NET_EVENT_CONNECTED: - printf("Socket %d: 连接建立\r\n", sockfd); - break; - - case NET_EVENT_DISCONNECTED: - printf("Socket %d: 连接断开\r\n", sockfd); - break; - - case NET_EVENT_DATA_RECEIVED: - printf("Socket %d: 收到数据\r\n", sockfd); - /* 注意:这里只是通知,需要调用 net_recv() 读取 */ - break; - - case NET_EVENT_SEND_COMPLETE: - printf("Socket %d: 发送完成\r\n", sockfd); - break; - - case NET_EVENT_TIMEOUT: - printf("Socket %d: 操作超时\r\n", sockfd); - break; - - case NET_EVENT_ERROR: - printf("Socket %d: 发生错误\r\n", sockfd); - break; - } -} -``` - -### 8.2 注册回调 - -```c -void event_callback_example(void) -{ - int sock; - - /* 创建 Socket ... */ - - /* 注册事件回调 */ - net_set_event_cb(sock, my_event_callback, NULL); - - /* 主循环 */ - while (1) - { - net_poll(); - } -} -``` - ---- - -## 9. 字节序处理 - -网络协议使用**大端序**(网络字节序),而 STM32 使用**小端序**(主机字节序)。发送和接收数据时需要进行字节序转换。 - -### 9.1 端口号转换 - -```c -uint16_t port = 8080; - -/* 主机序 -> 网络序(发送前转换) */ -uint16_t net_port = net_htons(port); - -/* 网络序 -> 主机序(接收后转换) */ -uint16_t host_port = net_ntohs(net_port); -``` - -### 9.2 IP 地址转换 - -```c -/* 字符串转网络序 IP */ -uint32_t ip = net_inet_addr("192.168.1.100"); - -/* 网络序 IP 转字符串 */ -char ip_str[16]; -net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.1.100" */ -``` - -### 9.3 填充地址结构体 - -```c -struct net_sockaddr_in addr; - -addr.sin_family = AF_INET; -addr.sin_port = net_htons(8080); /* 端口转换 */ -addr.sin_addr.s_addr = net_inet_addr("192.168.1.100"); /* IP 转换 */ - -/* 如果要绑定任意地址 */ -addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); -/* 或者直接 */ -addr.sin_addr.s_addr = INADDR_ANY; -``` - -### 9.4 读取地址信息 - -```c -struct net_sockaddr_in client_addr; -int addr_len = sizeof(client_addr); - -int client_sock = net_accept(listen_sock, - (struct net_sockaddr *)&client_addr, &addr_len); - -if (client_sock >= 0) -{ - /* 转换端口到主机序 */ - uint16_t port = net_ntohs(client_addr.sin_port); - - /* 转换 IP 到字符串 */ - char ip_str[16]; - net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str); - - printf("客户端: %s:%d\r\n", ip_str, port); -} -``` - ---- - -## 10. API 参考 - -### 10.1 核心 API - -#### net_init - 网络初始化 - -```c -int net_init(const char *ip, const char *mask, const char *gateway); -``` - -| 参数 | 说明 | -|------|------| -| `ip` | IP 地址字符串,NULL 使用 DHCP | -| `mask` | 子网掩码字符串,NULL 使用默认 | -| `gateway` | 网关地址字符串,NULL 使用默认 | -| 返回值 | 0 成功,-1 失败 | - -#### net_poll - 状态轮询 - -```c -int net_poll(void); -``` - -| 参数 | 说明 | -|------|------| -| 返回值 | 有事件发生的 socket 数量 | - -**注意**:必须在主循环中定期调用。 - -#### net_socket - 创建 Socket - -```c -int net_socket(int domain, int type, int protocol); -``` - -| 参数 | 说明 | -|------|------| -| `domain` | 地址族,仅支持 `AF_INET` | -| `type` | `SOCK_STREAM`(TCP) 或 `SOCK_DGRAM`(UDP) | -| `protocol` | 通常为 0 | -| 返回值 | socket 描述符 (0~7),失败返回 -1 | - -#### net_bind - 绑定地址 - -```c -int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | socket 描述符 | -| `addr` | 地址结构体指针 | -| `addrlen` | 地址结构体长度 | -| 返回值 | 0 成功,-1 失败 | - -#### net_listen - 开始监听 - -```c -int net_listen(int sockfd); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | socket 描述符 | -| 返回值 | 0 成功,-1 失败 | - -> **自动重监听**:`net_listen()` 对 TCP 监听 Socket 置 `auto_relisten = 1`。连接结束后 net 层自动回到 `LISTENING`,应用层无需手动 `net_close()` + `net_listen()`。 - -#### net_accept - 接受连接 - -```c -int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | 监听 socket 描述符 | -| `addr` | 输出:客户端地址 | -| `addrlen` | 输入输出:地址长度 | -| 返回值 | 新连接描述符(单连接模式下 == 监听 `sockfd`),无连接时返回 -1(可循环重试) | - -#### net_connect - 连接服务器 - -```c -int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | socket 描述符 | -| `addr` | 服务器地址 | -| `addrlen` | 地址长度 | -| 返回值 | 0 成功,-1 失败 | - -#### net_send - 发送数据 - -```c -int net_send(int sockfd, const void *buf, int len, int flags); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | socket 描述符 | -| `buf` | 数据缓冲区 | -| `len` | 数据长度 | -| `flags` | 通常为 0 | -| 返回值 | 实际发送字节数,失败返回 -1 | - -#### net_recv - 接收数据 - -```c -int net_recv(int sockfd, void *buf, int len, int flags); -``` - -| 参数 | 说明 | -|------|------| -| `sockfd` | socket 描述符 | -| `buf` | 接收缓冲区 | -| `len` | 缓冲区大小 | -| `flags` | 0=阻塞,`NET_MSG_DONTWAIT`=非阻塞 | -| 返回值 | 实际接收字节数;`0`=对端已关闭(net 层随即自动重监听);`-1`=错误(非阻塞且无数据时返回 `NET_ERR_WOULDBLOCK`) | - -#### net_sendto - 发送 UDP 数据 - -```c -int net_sendto(int sockfd, const void *buf, int len, int flags, - const struct net_sockaddr *dest_addr, int addrlen); -``` - -#### net_recvfrom - 接收 UDP 数据 - -```c -int net_recvfrom(int sockfd, void *buf, int len, int flags, - struct net_sockaddr *src_addr, int *addrlen); -``` - -#### net_close - 关闭 Socket - -```c -int net_close(int sockfd); -``` - -### 10.2 消息队列 API - -#### net_process_messages - 处理消息队列 - -```c -void net_process_messages(void); -``` - -处理其他任务通过 `net_send()` / `net_recv()` / `net_close()` 发送到消息队列的请求。必须在 `netTask` 主循环中调用。 - -#### net_send_sock - 发送数据(内部版本) - -```c -int net_send_sock(net_sock_t *p_sock, const void *buf, int len); -``` - -直接操作 Socket 控制块进行发送,不经过消息队列。**只能在 `netTask` 中调用**,用于 `netTask` 内的网络业务逻辑。 - -#### net_recv_sock - 接收数据(内部版本) - -```c -int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags); -``` - -直接操作 Socket 控制块进行接收,不经过消息队列。**只能在 `netTask` 中调用**。 - -#### net_close_sock - 关闭 Socket(内部版本) - -```c -int net_close_sock(net_sock_t *p_sock); -``` - -直接操作 Socket 控制块关闭连接,不经过消息队列。**只能在 `netTask` 中调用**。 - -### 10.3 辅助 API - -#### net_set_event_cb - 注册事件回调 - -```c -int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg); -``` - -#### net_get_sock - 获取 Socket 控制块 - -```c -net_sock_t *net_get_sock(int sockfd); -``` - -#### net_get_errno - 获取错误码 - -```c -int net_get_errno(void); -``` - -### 10.4 字节序转换 - -```c -uint16_t net_htons(uint16_t hostshort); /* 主机序 -> 网络序 */ -uint16_t net_ntohs(uint16_t netshort); /* 网络序 -> 主机序 */ -uint32_t net_htonl(uint32_t hostlong); /* 主机序 -> 网络序 */ -uint32_t net_ntohl(uint32_t netlong); /* 网络序 -> 主机序 */ -uint32_t net_inet_addr(const char *cp); /* 字符串 -> 网络序 IP */ -char *net_inet_ntoa(uint32_t addr, char *buf); /* 网络序 IP -> 字符串 */ -``` - -### 10.5 select/poll - -```c -int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds, - net_fd_set *exceptfds, net_timeval *timeout); - -int net_poll_events(net_pollfd *fds, int nfds, int timeout); -``` - -### 10.6 fd_set 操作宏 - -```c -NET_FD_ZERO(fdset) /* 清空集合 */ -NET_FD_SET(fd, fdset) /* 添加 fd */ -NET_FD_CLR(fd, fdset) /* 移除 fd */ -NET_FD_ISSET(fd, fdset) /* 检查 fd 是否在集合中 */ -``` - ---- - -## 11. 完整示例 - -### 11.1 TCP Client 自动重连 - -```c -#include "net_socket.h" -#include -#include - -#define SERVER_IP "192.168.1.200" -#define SERVER_PORT 8080 - -void tcp_client_reconnect(void) -{ - int sock = -1; - struct net_sockaddr_in addr; - uint32_t last_try = 0; - uint32_t reconnect_interval = 5000; /* 5 秒重连间隔 */ - - /* 初始化网络 */ - net_init("192.168.1.100", "255.255.255.0", "192.168.1.1"); - - while (1) - { - net_poll(); - - /* 尝试重连 */ - if (sock < 0 && (HAL_GetTick() - last_try) >= reconnect_interval) - { - last_try = HAL_GetTick(); - - sock = net_socket(AF_INET, SOCK_STREAM, 0); - if (sock >= 0) { - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = net_htons(SERVER_PORT); - addr.sin_addr.s_addr = net_inet_addr(SERVER_IP); - - if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) == 0) { - printf("正在连接服务器...\r\n"); - } else { - net_close(sock); - sock = -1; - } - } - } - - /* 检查连接状态 */ - if (sock >= 0) - { - net_sock_t *sock_info = net_get_sock(sock); - - if (sock_info != NULL && sock_info->state == NET_SOCK_STATE_ESTABLISHED) - { - /* 已连接,发送数据 */ - static uint32_t last_send = 0; - if ((HAL_GetTick() - last_send) >= 1000) { - last_send = HAL_GetTick(); - net_send(sock, "heartbeat", 9, 0); - } - - /* 检查接收 */ - char buf[128]; - int len = net_recv(sock, buf, sizeof(buf) - 1, NET_MSG_DONTWAIT); - if (len > 0) { - buf[len] = '\0'; - printf("收到: %s\r\n", buf); - } else if (len == 0) { - /* 连接断开 */ - printf("连接断开\r\n"); - net_close(sock); - sock = -1; - } - } - else if (sock_info != NULL && - (sock_info->state == NET_SOCK_STATE_CLOSED)) - { - /* 连接失败或断开 */ - printf("连接失败,准备重连...\r\n"); - net_close(sock); - sock = -1; - } - } - } -} -``` - ---- - -## 12. 常见问题 - -### 12.1 net_poll() 必须调用吗? - -**是的**。`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。 - -建议至少每 10ms 调用一次。 - -### 12.2 Socket 数量有限制吗? - -是的,CH395F 最多支持 8 个 Socket(索引 0~7)。其中: -- TCP Server(单连接):1 个监听 Socket 即数据通道 -- TCP Client:最多 8 个 -- UDP:最多 8 个 - -### 12.3 发送数据有什么限制? - -- 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节) -- TCP 需要等待发送缓冲区空闲(`send_ready` 标志) -- UDP 单包载荷最大 1472 字节(写入超过此值,CH395F 会自动分片为多个 UDP 包发送) - -### 12.4 如何处理连接断开? - -**方式一:阻塞接收** -```c -int len = net_recv(sock, buf, sizeof(buf), 0); -if (len <= 0) { - /* 连接断开或错误 */ - net_close(sock); -} -``` - -**方式二:非阻塞接收** -```c -int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT); -if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { - /* 没有数据 */ -} else if (len <= 0) { - /* 连接断开 */ -} -``` - -**方式三:事件回调** -```c -void callback(int sockfd, net_event_t event, void *arg) -{ - if (event == NET_EVENT_DISCONNECTED) { - /* 连接断开 */ - } -} -``` - -### 12.5 select 和 poll 的区别? - -| 特性 | select | poll | -|------|--------|------| -| 描述符集合 | fd_set(位掩码) | pollfd 数组 | -| 最大 fd 数 | 32 | 无限制 | -| 使用方式 | NET_FD_SET 宏 | 直接操作结构体 | - -两者功能相同,推荐使用 `net_select()`,更接近 Linux 编程习惯。 - -### 12.6 错误码说明 - -| 错误码 | 宏定义 | 说明 | -|--------|--------|------| -| 0 | NET_OK | 成功 | -| -1 | NET_ERR | 通用错误 | -| -2 | NET_ERR_BADF | 无效的 socket 描述符 | -| -3 | NET_ERR_INVAL | 无效参数 | -| -4 | NET_ERR_NOMEM | 内存不足 | -| -5 | NET_ERR_NOTCONN | 未连接 | -| -6 | NET_ERR_ISCONN | 已连接 | -| -7 | NET_ERR_WOULDBLOCK | 非阻塞模式下操作未完成 | -| -8 | NET_ERR_CONNRESET | 连接被重置 | -| -9 | NET_ERR_TIMEDOUT | 操作超时 | -| -10 | NET_ERR_NOSPACE | 发送缓冲区无空间 | -| -11 | NET_ERR_BUSY | 操作未完成(BUSY) | - -### 12.7 CH395F 命令执行状态码 - -CH395F 底层驱动返回的状态码(`ch395f_get_cmd_status()` 或 `ch395f_poll_cmd_status()`): - -| 代码 | 宏定义 | 说明 | -|------|--------|------| -| 0x00 | CH395F_ERR_SUCCESS | 成功 | -| 0x10 | CH395F_ERR_BUSY | 忙,命令正在执行 | -| 0x11 | CH395F_ERR_MEM | 内存管理错误 | -| 0x12 | CH395F_ERR_BUF | 缓冲区错误 | -| 0x13 | CH395F_ERR_TIMEOUT | 超时 | -| 0x14 | CH395F_ERR_RTE | 路由错误 | -| 0x15 | CH395F_ERR_ABRT | 连接中止 | -| 0x16 | CH395F_ERR_RST | 连接复位 | -| 0x17 | CH395F_ERR_CLSD | 连接关闭 | -| 0x18 | CH395F_ERR_CONN | 无连接 | -| 0x19 | CH395F_ERR_VAL | 值错误 | -| 0x1A | CH395F_ERR_ARG | 参数错误 | -| 0x1B | CH395F_ERR_USE | 已被使用(常见于重复 OPEN 或冲突配置) | -| 0x1C | CH395F_ERR_IF | MAC 错误 | -| 0x1D | CH395F_ERR_ISCONN | 已连接 | -| 0x20 | CH395F_ERR_OPEN | 已打开 | -| 0x5F | CH395F_CMD_RET_ABORT | 命令中止 | -| 0xFA | CH395F_ERR_UNKNOW | 未知错误 | - -### 12.8 故障排查 - -**问题:Socket 4~7 的 CONNECT/RECV 中断不触发** -- 需使用 `GET_GLOB_INT_STATUS_ALL`(命令码 0x19,返回 2 字节)才能检测 Socket 4~7 的中断 -- 1 字节版 `GET_GLOB_INT_STATUS`(命令码 0x29)仅支持 Socket 0~3 - -**问题:CH395F 初始化超时** -- 芯片 INIT 需要约 200ms,轮询间隔应为 20ms(过短的查询可能干扰内部处理) -- 超时值建议设为 4 秒以上 - -### 12.9 线程安全问题 - -**问题:`net_send()` / `net_recv()` / `net_close()` 可以在任意任务中调用吗?** - -可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行,自身阻塞等待结果,是线程安全的。 - -**问题:`netTask` 内可以直接调用 `net_send()` / `net_recv()` 吗?** - -不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。内部版本直接操作 Socket 控制块,不经过消息队列。 - -**问题:`net_socket()` / `net_bind()` / `net_listen()` 线程安全吗?** - -这些函数仅在系统初始化阶段(`osKernelStart()` 之前)调用,不存在并发访问。消息队列仅保护数据收发和连接关闭操作。 - -**问题:消息队列由谁创建?在哪里创建?** - -由 CubeMX 在 `MX_FREERTOS_Init()` 中创建(`osMessageQueueNew(8, sizeof(net_msg_t), NULL)`),在 `osKernelInitialize()` 之后执行,确保 FreeRTOS 内核已就绪。 diff --git a/docs/CH395F_Test_Guide.md b/docs/CH395F_Test_Guide.md index 57997b4..edbecec 100644 --- a/docs/CH395F_Test_Guide.md +++ b/docs/CH395F_Test_Guide.md @@ -719,7 +719,7 @@ MCU (TCP Client, ch395f_* 裸命令) PC (tcp_bulk --port 8081) ### 8.1 架构与会话结构 ``` -net_socket(NET_AF_INET, NET_SOCK_STREAM, 0) // 取空闲 Socket(非监听 Socket 0) +net_socket(NET_AF_INET, NET_SOCK_STREAM) // 取空闲 Socket(非监听 Socket 0) → net_connect(PC:8081) // 异步下发,net_poll 推进状态机 → net_select(sockfd+1, NULL, &wfds, NULL, &tv) // 等可写 = ESTABLISHED → net_send(64B) → net_recv(64B) 校验 // 主路径(701) @@ -782,7 +782,7 @@ net_socket(NET_AF_INET, NET_SOCK_STREAM, 0) // 取空闲 Socket(非监听 | 步 | 操作 | 成功标准 | 失败标准 | |----|------|----------|----------| -| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM, 0)` 创建 TCP socket | 返回 sockfd(`0~7`),非 `-1` | 返回 `-1`(无空闲 Socket 或 net 层未初始化) | +| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM)` 创建 TCP socket | 返回 sockfd(`0~7`),非 `-1` | 返回 `-1`(无空闲 Socket 或 net 层未初始化) | | 2 | 构造 `struct net_sockaddr_in`(sin_family=`NET_AF_INET`,sin_port=`net_htons(8081)`,sin_addr=`net_inet_addr("192.168.1.2")`)后 `net_connect(sockfd, &addr, len)` | 返回 `0`(连接命令已异步下发) | 返回 `-1`(地址非法或协议栈异常) | | 3 | 在 netTask 周期 `net_poll()` 推进下,调用 `net_select(sockfd+1, NULL, &wfds, NULL, &tv)` 等待可写事件,超时 10s | 返回正数且 `NET_FD_ISSET(sockfd)` 指示连接建立(ESTABLISHED) | 超时未连接,`net_errno == NET_ERR_WOULDBLOCK` 持续 | | 4 | `net_send(sockfd, buf, 64, 0)` 发送 64 字节递增模式数据(`0x00..0x3F`) | 返回值 == `64`(全部发出) | 返回值 `< 0`(发送失败)或 `≠ 64`(未完全发出) | @@ -807,7 +807,7 @@ net_socket(NET_AF_INET, NET_SOCK_STREAM, 0) // 取空闲 Socket(非监听 | 步 | 操作 | 成功标准 | 失败标准 | |----|------|----------|----------| -| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM, 0)` 创建 socket | 返回 sockfd 非 `-1` | 返回 `-1`(无空闲 Socket) | +| 1 | `net_socket(NET_AF_INET, NET_SOCK_STREAM)` 创建 socket | 返回 sockfd 非 `-1` | 返回 `-1`(无空闲 Socket) | | 2 | 构造地址 + `net_connect(sockfd, &addr, len)`,再 `net_select` 等待可写确认 ESTABLISHED | 连接建立成功 | 连接失败或超时未建立 | | 3 | `net_send(sockfd, buf, 64, 0)` → `net_recv(sockfd, buf, 64, 0)` | 收发各 `64` 字节、固件字节级比对 + 累加和 `TEST_CHECK` 通过 | 收发失败 / 数据不符 / 固件 `TEST_CHECK` 失败 | | 4 | `net_close(sockfd)` 关闭并释放控制块 | 返回 `0` | 返回 `-1` | diff --git a/docs/系统墙钟时间维护说明.md b/docs/系统墙钟时间维护说明.md new file mode 100644 index 0000000..757b251 --- /dev/null +++ b/docs/系统墙钟时间维护说明.md @@ -0,0 +1,348 @@ +# 系统墙钟时间维护 设计说明 + +> 模块:`App/sys_clock.c` / `App/sys_clock.h` +> 依赖:SD2506API-G RTC 驱动(`Drivers/BSP/SD2506/`)、HAL(`HAL_GetTick()`) +> 适用平台:STM32F407ZGTx(Cortex-M4,168MHz),I2C1 接 SD2506 +> 作者:王建锋 创建日期:2026-07-19 + +--- + +## 1. 概述 + +系统需要一个"墙钟时间"(wall clock,即真实日历时间 `YYYY-MM-DD HH:MM:SS`),用于: + +- 日志/调试信息打时间戳; +- 文件系统的文件创建/修改时间(FatFS 需要); +- 业务逻辑中基于真实时间的判定(定时、超时、调度等); +- 对外接口(如 FTP `LIST` 返回的文件时间、SNTP/NTP 校时等)。 + +本模块在 **SD2506 硬件 RTC(掉电保持)** 与 **HAL 系统节拍(运行期计时)** 之间做桥接: + +- 启动时从 SD2506 读取一次真实时间,换算为 Unix 时间戳,建立"墙钟基准"; +- 运行期间不再频繁访问 I2C RTC,而是以 **HAL Tick 流逝量** 推算当前时间,避免每次取时都走 I2C 总线; +- 仅在 `sys_clock_set()` 被调用(如用户校时、SNTP 同步)时,才把时间写回 SD2506 实现掉电保持。 + +--- + +## 2. 角色与依赖 + +| 角色 | 提供方 | 说明 | +|------|--------|------| +| 硬件实时钟(RTC) | SD2506API-G | 温补晶振,带备份电池,掉电后继续走时;I2C1(PB6-SCL/PB7-SDA) | +| 系统节拍 | `HAL_GetTick()` | 毫秒级计数,HAL 时基由 TIM7 提供(168MHz 主频派生,精度远高于走时需求) | +| Unix 时间戳 | 本模块计算 | 自 1970-01-01 00:00:00 UTC 起的秒数(`uint32_t`) | +| 时间结构 | `sd2506_time_t` | SD2506 驱动定义,`year` 为**完整年份(2000~2099)**,含 `week` | + +**调用顺序(见 `Src/main.c`):** + +``` +app_main_init(); /* 外设/日志等前置初始化 */ +sd2506_init(); /* 初始化 RTC(I2C 通信、24h 制、充电配置) */ +sys_clock_init(); /* 读取 RTC → 建立墙钟基准 */ +``` + +> 必须在 `sd2506_init()` 之后调用 `sys_clock_init()`;其余 `sys_clock_*` 接口必须在 `sys_clock_init()` 之后使用。 + +--- + +## 3. 核心思想:基准法(drift-free 维持) + +为什么不每次都读 RTC? + +- SD2506 走 I2C,单次读取有总线开销与阻塞延迟; +- RTC 自身已足够精确,无需运行时反复校准; +- 运行期真正变化的只是"过了多少时间",由 HAL Tick 提供。 + +因此模块只在初始化(`sys_clock_init`)与显式设置(`sys_clock_set`)时接触 RTC,其余时刻用以下公式维持时间: + +``` +当前 Unix 秒数 = s_base_unix_secs + (HAL_GetTick() - s_base_tick) / 1000 +``` + +其中 `(now_tick - s_base_tick)` 为 **unsigned 减法**,在 Tick 每约 49 天回绕一次时依然正确(只要两次采样间隔 < 2³² ms ≈ 49 天,单次运行必然满足),所以连续上电不超过 49 天的场景无回绕问题。 + +--- + +## 4. 内部数据结构 + +`App/sys_clock.c` 中定义两个静态变量作为墙钟基准: + +```c +static volatile uint32_t s_base_tick = 0; /* HAL_GetTick() 基准(建立基准时的 Tick 值) */ +static volatile uint32_t s_base_unix_secs = UINT32_MAX; /* 基准对应的 Unix 秒数;初始为 UINT32_MAX 表示"未建立" */ +``` + +- `s_base_tick`:`sys_clock_init()` / `sys_clock_set()` 时采样一次。 +- `s_base_unix_secs`:与 `s_base_tick` 同一时刻的 Unix 秒数;`sys_clock_get()` 以它为锚推算当前值。 +- 二者均带 `volatile`,因为时间是跨任务共享的状态(虽更新在 nets/默认任务中,但读取可能发生在任意任务上下文)。 + +> 注:当前工程仅 `main.c` 调用了 `sys_clock_init()`;`sys_clock_get()` / `sys_clock_get_str()` / `sys_clock_set()` 是供应用层(日志、文件时间、校时协议等)调用的 API,尚未被其他模块引用。 + +--- + +## 5. 对外 API + +| 函数 | 功能 | 返回值 | 限定条件 | +|------|------|--------|----------| +| `int sys_clock_init(void)` | 从 SD2506 读取初始时间,建立墙钟基准(含可选校时) | `0`(始终成功) | `sd2506_init()` 已调用 | +| `uint32_t sys_clock_get(void)` | 获取当前 Unix 时间戳(秒) | 当前秒数;未初始化返回 `UINT32_MAX` | `sys_clock_init()` 已调用 | +| `char *sys_clock_get_str(char *buf, size_t buf_size)` | 获取格式化字符串 `YYYY-MM-DD HH:MM:SS` | `buf` 指针;未初始化输出 `----/--/-- --:--:--` | `buf` 有效且 `buf_size ≥ 20` | +| `int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time)` | 更新基准,可写回 SD2506 | `0`(始终成功) | `unix_secs` 为合法时间戳 | + +> 头文件 `sys_clock.h` 中注释的返回值含 `-1` 失败码,但实际实现恒返回 `0`,以"成功"语义对外;调用方按 `0` 成功处理即可。 + +--- + +## 6. 初始化流程(`sys_clock_init`) + +``` +Step 1 从 SD2506 读取初始时间 sd2506_get_time(&sd_time) + ├─ 注意:sd_time.year 已是完整年份(驱动内已 +2000), + │ 后续换算【不得】再加 2000,否则年份虚增 → Unix 秒数超 uint32 回绕 + └─ 可选校时(见第 9 节 RTC_DEFAULT_INIT_* 宏) + +Step 2 将 RTC 时间换算为 Unix epoch 秒数 + ├─ 累计 1970 至当前年份的整年天数(含闰年 366 天判定) + ├─ 加上当年 1 月至当前月的累计天数 + ├─ 加当月已过天数、时/分/秒 + └─ 用 int64_t 累加,避免中间溢出;结果截断存入 uint32_t(受 2038 限制,见第 12 节) + +Step 3 建立基准 + s_base_tick = HAL_GetTick(); + s_base_unix_secs = (uint32_t)unix_secs; +``` + +初始化后,所有后续取时都基于 Step 3 的基准推算,不再访问 RTC。 + +--- + +## 7. 时间获取与漂移维持(`sys_clock_get`) + +```c +uint32_t now_tick = HAL_GetTick(); +int64_t unix_secs = (int64_t)s_base_unix_secs + + (int64_t)((uint32_t)(now_tick - s_base_tick) / 1000uL); +if (unix_secs < 0) return UINT32_MAX; /* 基准未建立(理论上不会发生) */ +return (uint32_t)unix_secs; +``` + +要点: + +- 用 **整除 1000** 把毫秒 Tick 折算为秒,**不**做每毫秒累加,避免累加误差累积与频繁除法; +- `now_tick - s_base_tick` 为无符号减法,自动正确处理 Tick 回绕; +- 若基准未建立(`s_base_unix_secs` 仍为 `UINT32_MAX` 且未初始化),返回 `UINT32_MAX` 作为"无效"哨兵。 + +### 7.1 字符串格式化(`sys_clock_get_str`) + +内部先 `sys_clock_get()`,若无效则输出占位串 `----/--/-- --:--:--`;否则用 `unix_to_sd2506()` 转回 `sd2506_time_t` 后,以 `snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d", ...)` 格式化(年份显示时 `+2000` 还原为完整年份)。使用 `snprintf` 防止缓冲区溢出,调用方需保证 `buf_size ≥ 20`。 + +--- + +## 8. 时间设置与回写(`sys_clock_set`) + +```c +s_base_tick = HAL_GetTick(); +s_base_unix_secs = unix_secs; +if (sd_time != NULL) { + sd2506_set_time(sd_time); /* 写回 RTC,实现掉电保持 */ +} +``` + +- 仅更新"基准",之后 `sys_clock_get()` 即以此为准; +- 若传入 `sd_time != NULL`,同步写回 SD2506,使断电后时间不丢失; +- `sd_time` 为 `NULL` 时只改运行期基准、不动 RTC(适用于临时/网络校时尚未落盘的场景)。 + +典型应用:SNTP/NTP 同步得到 Unix 秒数 → 转成 `sd2506_time_t` → `sys_clock_set(secs, &sd_time)` 一次性更新运行基准与掉电保持。 + +--- + +## 9. 开发期校时宏(`sys_clock.c` 顶部) + +```c +#define RTC_DEFAULT_INIT_ENABLE 1 +#define RTC_DEFAULT_INIT_FORCE 1 +#define RTC_DEFAULT_YEAR 2026 +#define RTC_DEFAULT_MONTH 8 +#define RTC_DEFAULT_DAY 24 +#define RTC_DEFAULT_HOUR 0 +#define RTC_DEFAULT_MINUTE 0 +#define RTC_DEFAULT_SECOND 0 +``` + +行为(`sys_clock_init` 内,`#if defined(RTC_DEFAULT_INIT_ENABLE)` 包裹): + +- 判定 RTC 时间是否"无效":`year` 越界(<2000 或 >2099)、`month` 越界、`day` 越界; +- `RTC_DEFAULT_INIT_FORCE == 1`:强制每次启动都用上述宏写入默认时间; +- 否则仅在 RTC 时间无效时写入; +- 写入后重新 `sd2506_get_time()` 读取校准后的值,再进入 Step 2 换算。 + +> ⚠️ **使用注意**:`FORCE=1` 会**每次开机覆盖用户/网络已设的时间**,仅用于首次烧录或硬件时钟丢失时的开发期校时。校时完成后应将 `RTC_DEFAULT_INIT_FORCE` 置 `0`(或注释 `ENABLE`),否则会抹掉正常运行中通过 SNTP 等写入的正确时间。 + +--- + +## 10. 时间换算算法 + +### 10.1 Unix 秒 ↔ SD2506(`unix_to_sd2506`) + +- 时分秒:对 `unix_secs` 连续 `%60 /60 /24` 拆分; +- 年月日:先算总天数 `unix_secs / 86400`,再从 1970 年起逐年减去平/闰年天数,定位到具体年;再按月累减定位月、日; +- 星期:由 Zeller 公式(见 10.2)计算,结果 `0=Sunday`; +- SD2506 年份字段为 0~99(+2000),故回写时 `year - 2000`。 + +### 10.2 星期计算(Zeller 公式) + +`rtc_calc_week()` 与 `unix_to_sd2506()` 内使用同一套算法: + +``` +week = (century_term + month_term + day_term) % 7 /* 0=Sunday */ +``` + +- `1、2 月视作上一年的 13、14 月`(公式要求),对应代码里 `wm<=2` 时 `wy--, wm+=12`; +- 计算与闰年无关,纯日历代数,结果稳定。 + +--- + +## 11. 集成与调用示例 + +`Src/main.c` 中的初始化顺序(节选): + +```c +HAL_Delay(100); +app_main_init(); +sd2506_init(); /* 先初始化 RTC */ +sys_clock_init(); /* 再建立墙钟基准:所有后续日志即带正确时间戳 */ +``` + +应用层取时间示例: + +```c +char ts[24]; +DBG_INFO("now: %s", sys_clock_get_str(ts, sizeof(ts))); + +uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、超时判定等 */ +``` + +--- + +## 12. 关键设计要点与陷阱 + +1. **`year` 已经 `+2000`,禁止二次加 2000** + `sd2506_get_time()` 返回的 `year` 是完整年份(如 2026)。`sys_clock_init` 的 Step 2 直接把它当完整年份参与 Unix 换算;若再 `+2000` 会得到 4026 年,换算出的 Unix 秒数远超 `uint32_t` 上限,发生截断/回绕,时间彻底错乱。代码中相关注释已明确标注。 + +2. **`uint32_t` Unix 时间戳的 2038 上限** + 基准与返回值均为 `uint32_t`,时间范围约为 `1970-01-01 ~ 2038-01-19`。超过 2038 年换算会回绕。当前设备生命周期内可接受;若需长期运行,需改用 `uint64_t` 或 `time_t`。 + +3. **Tick 回绕安全** + `(now_tick - s_base_tick)` 为无符号减法,Tick 每 ~49 天回绕一次时计算依然正确,无需特殊处理。 + +4. **运行期不访问 RTC** + `sys_clock_get()` 无任何 I2C 操作,零阻塞、可随时在任意任务/中断上下文安全调用(仅读 `volatile` 基准)。 + +5. **校时宏的副作用** + 见第 9 节 `FORCE=1` 会覆盖既有正确时间,仅限开发期使用。 + +--- + +## 13. 已知限制与可扩展方向 + +| 项目 | 现状 | 可扩展方向 | +|------|------|------------| +| 时间范围 | 受 `uint32_t` 限制至 2038 | 改用 `uint64_t`/`time_t` 扩展 | +| 运行期校准 | 仅初始化时读 RTC | 可定时(如每天)重新 `sd2506_get_time()` 校准,消除 RTC 与 Tick 的微小累积偏差 | +| 网络校时 | 已实现(PC 推送 / TCP,见第 15 节) | SNTP/NTP 公网校时仍可作为后续扩展 | +| 时区 | 按 RTC 本地时间处理 | 如需 UTC/时区,可在 API 层增加偏移参数 | + +--- + +## 14. 相关文件 + +| 路径 | 说明 | +|------|------| +| `App/sys_clock.c` | 墙钟维护实现(基准法、换算、校时宏) | +| `App/sys_clock.h` | 对外 API 声明 | +| `Drivers/BSP/SD2506/sd2506.c` / `.h` | SD2506 RTC I2C 驱动(`sd2506_get_time` / `sd2506_set_time`) | +| `Src/main.c` | 初始化调用顺序:`sd2506_init()` → `sys_clock_init()` | +| `App/time_sync.c` / `time_sync.h` | 时间同步实现(TCP Server,`timeSyncTask`) | +| `test/time_sync_server.py` | PC 端时间推送客户端(TCP) | + +--- + +--- + +## 15. 时间同步(PC 推送 / TCP) + +除 RTC 自身走时与开发期校时宏外,系统支持由 **PC 通过 TCP 主动推送本地墙钟** 来同步板子时间,使板子时间与开发电脑一致(无需公网 NTP)。 + +### 15.1 模块与角色 + +| 角色 | 提供方 | 说明 | +|------|--------|------| +| 时间同步服务端(板子) | `App/time_sync.c` / `time_sync.h` | TCP Server,监听 `TIME_SYNC_PORT`(默认 8888) | +| 同步任务 | `timeSyncTask`(FreeRTOS 任务) | 独立任务,由 `Src/freertos.c` 创建(优先级 `osPriorityNormal`,栈 2KB) | +| 时间推送端(PC) | `test/time_sync_server.py` | Python TCP 客户端,周期推送本机本地时间 | +| 落盘接口 | `sys_clock_set_unix()` | 收到时间包后调用,内部转 `sd2506_time_t` 并写回 SD2506 | + +### 15.2 协议 + +PC 每轮发送固定 8 字节帧: + +``` +字节 0..3 : 魔术字 "TIME"(0x54 0x49 0x4D 0x45) +字节 4..7 : 本地 Unix 时间戳,uint32,大端(PC 本地时间,含时区,与电脑显示一致) +``` + +板子 `time_sync_handle_conn()` 读取后校验魔术字,取大端 uint32 调用 `sys_clock_set_unix(epoch)` 更新运行基准并写回 RTC。 + +### 15.3 任务流程 + +`timeSyncTask` 启动后延迟 8s(等 netTask 完成 `net_init` 与 PHY 协商),进入循环: + +```c +for (;;) { + ls = time_sync_open_listener(); /* net_socket + net_bind + net_listen,失败重试 */ + conn = net_accept(ls, NULL, NULL); /* 阻塞等待 PC 连接(无连接立即返回 -1,循环重试) */ + time_sync_handle_conn(conn); /* net_recv 读到 8 字节 → sys_clock_set_unix → net_close */ + /* conn(单连接模式下即 ls)被 net_close 释放,下次循环重建监听 */ +} +``` + +> 单连接模式下监听 Socket 收到连接后即转为数据通道,连接关闭后被释放;因此每轮循环都**重新创建监听 Socket**(与 `lftpd` 同模式),与 FTP 不冲突。 + +### 15.4 为什么用 TCP 而非 UDP + +- 所有 socket 操作(`net_socket` / `net_bind` / `net_listen` / `net_accept` / `net_recv` / `net_close`)均走 `net_socket` 的**线程安全 API**,内部经消息队列由 `netTask` 串行执行; +- netTask 因此只需做 `net_poll()` + `net_process_messages()`,不掺杂任何应用层网络逻辑; +- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现),而 UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` 绕过队列。选用 TCP 即可让时间同步作为独立任务运行,且**无需改动 `net_socket.c` 的 UDP 路径**。 + +### 15.5 Socket 分配与冲突 + +- `net_socket()` 从 0 开始分配第一个空闲 Socket 控制块(`alloc_socket()`),并把硬件 Socket 号设为该下标;调用时的第三个参数(0)仅为协议占位,不影响分配。 +- 当前启动顺序:FTP 任务(延迟 7s)先监听 → 占用 **socket 0**(控制)+ **socket 1**(数据);时间同步任务(延迟 8s)后监听 → 占用 **socket 1**(或下一个空闲)。二者不冲突。 +- 单连接模式允许**任意** Socket 作为"监听 + 数据复用"通道,且总共 8 个 Socket,FTP 用 2 个、时间同步用 1 个,余量充足。 + +### 15.6 客户端实现要点(避坑) + +PC 端 `test/time_sync_server.py` 发送 8 字节后**必须保持连接打开**,等待板子读取(或短延时)后再 `close()`: + +- 若发送后立刻关闭,CH395F 在收到 FIN 时会**丢弃接收缓冲里尚未被板子读出的 8 字节**; +- 板子 `net_recv` 经消息队列异步执行(有 ~10ms+ 调度延迟),等它去读时连接已 DISCONNECT、缓冲已清空,`net_recv` 走 CLOSED 分支返回 0,时间包丢失、同步失败; +- 客户端改为 `sendall` 后 `recv` 直到板子主动关闭(EOF)或 2s 超时,即可保证板子先读到数据。 + +### 15.7 验证 + +板子日志应出现: + +``` +[TIME_SYNC] time_sync: TCP listen on :8888 (sock=1) +[NET] sock1 standalone accept +[NET] recv sock1 len=8 +[CLK] sys_clock_set: wall clock updated to +[CLK] sys_clock_set: SD2506 RTC written +[TIME_SYNC] time_sync: wall clock updated (epoch=) +``` + +PC 端 `python test/time_sync_server.py` 每 `interval` 秒推送一次,epoch 随电脑本地时间递增即表示同步正常。 + +--- + +*文档依据 `App/sys_clock.c` / `App/time_sync.c` 实现与《嵌入式C语言代码规范(V1.0)》整理。* diff --git a/docs/GD5F2GQ5UExxG.md b/docs/芯片手册/GD5F2GQ5UExxG.md similarity index 100% rename from docs/GD5F2GQ5UExxG.md rename to docs/芯片手册/GD5F2GQ5UExxG.md diff --git a/docs/RTL8305NBI-CG.md b/docs/芯片手册/RTL8305NBI-CG.md similarity index 100% rename from docs/RTL8305NBI-CG.md rename to docs/芯片手册/RTL8305NBI-CG.md diff --git a/docs/SD2506API-G.md b/docs/芯片手册/SD2506API-G.md similarity index 100% rename from docs/SD2506API-G.md rename to docs/芯片手册/SD2506API-G.md diff --git a/docs/TPAFE5160.md b/docs/芯片手册/TPAFE5160.md similarity index 100% rename from docs/TPAFE5160.md rename to docs/芯片手册/TPAFE5160.md diff --git a/test/net_test_task.c b/test/net_test_task.c index b2b30b5..3017277 100644 --- a/test/net_test_task.c +++ b/test/net_test_task.c @@ -121,7 +121,7 @@ static uint32_t phase7_cksum(const uint8_t *p, uint32_t n) { * 返回 1 表示全流程通过,0 表示中途失败(已记录 TEST_CHECK)。 */ static int32_t phase7_session(uint16_t local_port) { - int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (sockfd < 0) { TEST_CHECK(0, "TC701 socket create"); return 0; @@ -220,7 +220,7 @@ static int32_t phase7_session(uint16_t local_port) * 701/702 要求服务端在线,703 要求服务端离线,故 703 应在独立的一轮中运行)。 */ static void phase7_test_timeout(void) { - int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + int32_t sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (sockfd < 0) { TEST_CHECK(0, "TC703 socket create"); return; } struct net_sockaddr_in raddr; @@ -271,7 +271,7 @@ static int32_t phase7_session_multi(void) /* 1) 打开并连接所有 Socket(各自绑定不同本地端口规避 TIME_WAIT) */ for (int32_t i = 0; i < PHASE7_MULTI_COUNT; i++) { - sockfd[i] = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + sockfd[i] = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (sockfd[i] < 0) { TEST_CHECK(0, "TC704 socket create #%d", i); for (int32_t j = 0; j < i; j++) net_close(sockfd[j]); @@ -822,7 +822,7 @@ void net_layer_test_main(void) struct net_sockaddr_in client_addr; int32_t addrlen = sizeof(client_addr); - sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM, 0); + sockfd = net_socket(NET_AF_INET, NET_SOCK_STREAM); if (sockfd < 0) { DBG_ERROR("Test task: net_socket failed"); return; diff --git a/test/time_sync_server.py b/test/time_sync_server.py new file mode 100644 index 0000000..c276527 --- /dev/null +++ b/test/time_sync_server.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +本地时间同步服务(PC -> MCU 板子) + +功能: + 周期性把本机(PC)的本地时间,通过 TCP 推送到 STM32 板子。 + 板子端在 timeSyncTask 中以 TCP Server 方式监听端口,收到后调用 + sys_clock_set_unix() 更新系统墙钟并写回 SD2506 RTC,使板子时间 == 你电脑的时间。 + +协议(板子端实现见 App/time_sync.c 的 time_sync_handle_conn): + 字节 0..3 : 魔术字 "TIME" (0x54 0x49 0x4D 0x45) + 字节 4..7 : 本地 Unix 时间戳,uint32,大端(主机字节序转换后由板子分解) + +用法: + python time_sync_server.py # 默认推送到 192.168.1.100:8888 + python time_sync_server.py 192.168.1.100 # 指定板子 IP + python time_sync_server.py 192.168.1.100 9000 3 # 指定端口与间隔(秒) + +按 Ctrl+C 停止。 +""" + +import socket +import struct +import sys +import time + +DEFAULT_MCU_IP = "192.168.1.100" +DEFAULT_PORT = 8888 +DEFAULT_INTERVAL = 5.0 + + +def main(): + mcu_ip = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_MCU_IP + port = int(sys.argv[2]) if len(sys.argv) > 2 else DEFAULT_PORT + interval = float(sys.argv[3]) if len(sys.argv) > 3 else DEFAULT_INTERVAL + + print("[time_sync] 本地时间推送服务已启动(TCP 客户端)") + print(f" 目标板子 : {mcu_ip}:{port}") + print(f" 推送间隔 : {interval}s") + print(f" 协议帧 : 'TIME' + uint32(BE) 本地 Unix 时间戳") + print("[time_sync] 按 Ctrl+C 停止") + print("-" * 48) + + while True: + try: + # 本地 wall-clock 的 epoch(与电脑显示一致;板子按本地时间分解) + local_epoch = int(time.mktime(time.localtime())) + pkt = b"TIME" + struct.pack(">I", local_epoch) + + with socket.create_connection((mcu_ip, port), timeout=5) as sock: + sock.sendall(pkt) + # 关键:发送后保持连接打开,等板子读完再关。 + # 若立即 close,CH395F 收到 FIN 会丢弃接收缓冲里尚未被板子读出的 + # 8 字节时间包,导致板子 net_recv 看到 CLOSED 而无数据、时间同步失败。 + # 这里 recv 直到板子主动关闭(EOF)或 2s 超时,确保板子先读到数据。 + sock.settimeout(2.0) + try: + sock.recv(64) + except (socket.timeout, OSError): + pass + + now_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + print(f"[{time.strftime('%H:%M:%S')}] -> {now_str} (epoch={local_epoch})") + except OSError as e: + print(f"[{time.strftime('%H:%M:%S')}] 连接/发送失败: {e}") + + time.sleep(interval) + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n[time_sync] 已停止")