uart 测试
authorhcj <1224667075@qq.com>
Fri, 13 Mar 2026 08:00:50 +0000 (16:00 +0800)
committerhcj <1224667075@qq.com>
Fri, 13 Mar 2026 08:00:50 +0000 (16:00 +0800)
code/sdk/zhihui/uarttest/CMakeLists.txt [new file with mode: 0644]
code/sdk/zhihui/uarttest/include/uarttest.h [new file with mode: 0644]
code/sdk/zhihui/uarttest/include/uarttest_http.h [new file with mode: 0644]
code/sdk/zhihui/uarttest/include/uarttest_tcp.h [new file with mode: 0644]
code/sdk/zhihui/uarttest/src/uarttest.c [new file with mode: 0644]
code/sdk/zhihui/uarttest/src/uarttest_api_at.c [new file with mode: 0644]
code/sdk/zhihui/uarttest/src/uarttest_http.c [new file with mode: 0644]
code/sdk/zhihui/uarttest/src/uarttest_tcp.c [new file with mode: 0644]

diff --git a/code/sdk/zhihui/uarttest/CMakeLists.txt b/code/sdk/zhihui/uarttest/CMakeLists.txt
new file mode 100644 (file)
index 0000000..db0a391
--- /dev/null
@@ -0,0 +1,47 @@
+cmake_minimum_required(VERSION 3.16)\r
+\r
+# 简单 UART 自测模块\r
+set(COMPONENT_NAME "uarttest")\r
+\r
+set(SOURCES\r
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/uarttest.c\r
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/uarttest_tcp.c\r
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/uarttest_http.c\r
+    ${CMAKE_CURRENT_SOURCE_DIR}/src/uarttest_api_at.c\r
+)\r
+\r
+set(PUBLIC_HEADER\r
+    ${CMAKE_CURRENT_SOURCE_DIR}/include\r
+    ${ROOT_DIR}/open_source/openharmony/third_party/lwip/lwip_v2.1.3/src/include\r
+    ${ROOT_DIR}/open_source/openharmony/third_party/lwip/lwip_adapter/liteos/src/include\r
+    ${ROOT_DIR}/middleware/utils/at/wstp_at/inc\r
+    ${ROOT_DIR}/open_source/openharmony/foundation/ace/ace_engine_lite/modules/netstack/http/include\r
+)\r
+\r
+set(PRIVATE_HEADER\r
+)\r
+\r
+set(PUBLIC_DEFINES\r
+)\r
+\r
+set(PRIVATE_DEFINES\r
+)\r
+\r
+set(COMPONENT_PUBLIC_CCFLAGS\r
+)\r
+\r
+set(COMPONENT_CCFLAGS\r
+    -Wall\r
+    -Wextra\r
+)\r
+\r
+set(WHOLE_LINK\r
+    false\r
+)\r
+\r
+set(MAIN_COMPONENT\r
+    false\r
+)\r
+\r
+build_component()\r
+\r
diff --git a/code/sdk/zhihui/uarttest/include/uarttest.h b/code/sdk/zhihui/uarttest/include/uarttest.h
new file mode 100644 (file)
index 0000000..9065d8b
--- /dev/null
@@ -0,0 +1,32 @@
+#ifndef UARTTEST_H\r
+#define UARTTEST_H\r
+\r
+#include "errcode.h"\r
+\r
+#ifdef __cplusplus\r
+extern "C" {\r
+#endif\r
+\r
+/**\r
+ * @brief 启动 UART 测试线程\r
+ *\r
+ * 线程内部调用 uarttest_loop(),周期性在 SIO UART 上收发数据,\r
+ * 用于验证串口波特率/流控/稳定性。若线程已启动,再次调用无效。\r
+ */\r
+void uarttest_start_thread(void);\r
+\r
+/* TCP 吞吐测试 */\r
+void uarttest_tcp_start(void);\r
+\r
+/* HTTP 测速:通过 AT 指定 URL 下载,不保存文件,仅测速 */\r
+void uarttest_http_start(const char *url);\r
+\r
+/* AT 指令注册:AT^UARTTEST / AT^TCPSPEED / AT^HTTPSPEED */\r
+errcode_t uarttest_at_init(void);\r
+\r
+#ifdef __cplusplus\r
+}\r
+#endif\r
+\r
+#endif /* UARTTEST_H */\r
+\r
diff --git a/code/sdk/zhihui/uarttest/include/uarttest_http.h b/code/sdk/zhihui/uarttest/include/uarttest_http.h
new file mode 100644 (file)
index 0000000..deac2e1
--- /dev/null
@@ -0,0 +1,20 @@
+#ifndef UARTTEST_HTTP_H\r
+#define UARTTEST_HTTP_H\r
+\r
+#ifdef __cplusplus\r
+extern "C" {\r
+#endif\r
+\r
+/**\r
+ * @brief Start HTTP speed test thread.\r
+ * Downloads from url without saving; measures throughput and prints Mbps.\r
+ * If url is NULL or empty, uses default URL (e.g. speedtest.tele2.net/1MB.zip).\r
+ * @param url Full URL (e.g. "http://host/path") or NULL for default.\r
+ */\r
+void uarttest_http_start(const char *url);\r
+\r
+#ifdef __cplusplus\r
+}\r
+#endif\r
+\r
+#endif /* UARTTEST_HTTP_H */\r
diff --git a/code/sdk/zhihui/uarttest/include/uarttest_tcp.h b/code/sdk/zhihui/uarttest/include/uarttest_tcp.h
new file mode 100644 (file)
index 0000000..7ecaf05
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef UARTTEST_TCP_H\r
+#define UARTTEST_TCP_H\r
+\r
+#ifdef __cplusplus\r
+extern "C" {\r
+#endif\r
+\r
+/**\r
+ * @brief 启动 TCP 吞吐测试线程(默认作为 client 向模组发送数据)\r
+ *\r
+ * 默认参数:\r
+ * - 目标 IP:172.16.1.2\r
+ * - 目标端口:5001\r
+ * - 发送时长:10s\r
+ * - 单次 send 大小:1460 bytes\r
+ *\r
+ * 线程启动后会周期性打印吞吐(Mbps)。\r
+ */\r
+void uarttest_tcp_start(void);\r
+\r
+#ifdef __cplusplus\r
+}\r
+#endif\r
+\r
+#endif /* UARTTEST_TCP_H */\r
+\r
diff --git a/code/sdk/zhihui/uarttest/src/uarttest.c b/code/sdk/zhihui/uarttest/src/uarttest.c
new file mode 100644 (file)
index 0000000..30c664c
--- /dev/null
@@ -0,0 +1,151 @@
+#include <stdint.h>\r
+#include <stdbool.h>\r
+#include <string.h>\r
+\r
+#include "uart.h"\r
+#include "uart_porting.h"\r
+#include "cmsis_os2.h"\r
+#include "custom_utils.h"      /* 使用 app_task_definition_ext_t / uapi_create_osThread_custom_with_args */\r
+#include "uarttest.h"\r
+\r
+/* 本测试直接使用 UART_BUS_0 + 921600 波特率,避免依赖 lwipopts_default.h 的 SIO 宏 */\r
+#ifndef UARTTEST_BUS\r
+#define UARTTEST_BUS   UART_BUS_0\r
+#endif\r
+\r
+#ifndef UARTTEST_BAUD\r
+#define UARTTEST_BAUD  921600U\r
+#endif\r
+\r
+/* 来自 uart_porting.c 的串口打印封装,这里声明后直接使用 */\r
+extern void print_str(const char *str, ...);\r
+\r
+/* 简单延时(粗略 ms 级,依赖于系统 tick) */\r
+static void uarttest_delay_ms(uint32_t ms)\r
+{\r
+    /* 使用 CMSIS-RTOS2 的 osDelay,单位 ms */\r
+    osDelay(ms);\r
+}\r
+\r
+/* 发送一段字符串到测试 UART(默认为 UART_BUS_0) */\r
+static void uarttest_send_string(const char *s)\r
+{\r
+    if (s == NULL) {\r
+        return;\r
+    }\r
+    size_t len = strlen(s);\r
+    if (len == 0) {\r
+        return;\r
+    }\r
+    (void)uapi_uart_write(UARTTEST_BUS, (const uint8_t *)s, (uint16_t)len, 1000);\r
+}\r
+\r
+/* 对当前测试 UART 做一次基础配置(波特率 / 数据位等) */\r
+static void uarttest_config_uart(void)\r
+{\r
+    uart_attr_t attr;\r
+    attr.baud_rate = UARTTEST_BAUD;\r
+    attr.data_bits = UART_DATA_BIT_8;\r
+    attr.parity    = UART_PARITY_NONE;\r
+    attr.stop_bits = UART_STOP_BIT_1;\r
+    (void)uapi_uart_set_attr(UARTTEST_BUS, &attr);\r
+}\r
+\r
+/*\r
+ * 简单 UART 自测:\r
+ * 1. 发送一行测试字符串\r
+ * 2. 等待回环/对端回 echo 的数据,并打印收到的内容长度\r
+ *\r
+ * 使用方式:在合适的位置调用 uarttest_run_once() 即可(例如某个命令或测试入口)。\r
+ */\r
+void uarttest_run_once(void)\r
+{\r
+    uint8_t rx_buf[64] = {0};\r
+\r
+    uarttest_config_uart();\r
+\r
+    print_str("\r\n[UARTTEST] start, bus=%d, baud=%u\r\n", (int)UARTTEST_BUS, (unsigned int)UARTTEST_BAUD);\r
+    uarttest_send_string("[UARTTEST] hello from device\r\n");\r
+\r
+    /* 简单等待一会儿,看是否有数据回环回来 */\r
+    uarttest_delay_ms(50);\r
+\r
+    int32_t ret = uapi_uart_read(UARTTEST_BUS, rx_buf, sizeof(rx_buf), 100);\r
+    if (ret > 0) {\r
+        print_str("[UARTTEST] recv %d bytes: ", (int)ret);\r
+        for (int i = 0; i < ret; ++i) {\r
+            print_str("%02X ", rx_buf[i]);\r
+        }\r
+        print_str("\r\n");\r
+    } else {\r
+        print_str("[UARTTEST] no data received (ret=%d)\r\n", (int)ret);\r
+    }\r
+}\r
+\r
+/*\r
+ * 连续收发模式:周期性发送计数值,并尝试读回数据。\r
+ * 可选调用,方便做长时间稳定性测试。\r
+ */\r
+void uarttest_loop(void)\r
+{\r
+    uint32_t counter = 0;\r
+    uint8_t rx_buf[64];\r
+\r
+    uarttest_config_uart();\r
+    print_str("\r\n[UARTTEST] loop mode start, bus=%d, baud=%u\r\n",\r
+              (int)UARTTEST_BUS, (unsigned int)UARTTEST_BAUD);\r
+\r
+    while (1) {\r
+        // char line[64];\r
+        // int len = snprintf(line, sizeof(line), "[UARTTEST] counter=%lu\r\n", (unsigned long)counter++);\r
+        // if (len > 0) {\r
+        //     (void)uapi_uart_write(UARTTEST_BUS, (const uint8_t *)line, (uint16_t)len, 100);\r
+        // }\r
+        memset_s(rx_buf,64,1,64);\r
+        int32_t ret = uapi_uart_read(UARTTEST_BUS, rx_buf, sizeof(rx_buf), 10);\r
+        if (ret > 0) {\r
+            printf("[UARTTEST] loop recv %d bytes\r\n", (int)ret);\r
+            printf("%s",rx_buf);\r
+        }\r
+\r
+        uarttest_delay_ms(10);\r
+    }\r
+}\r
+\r
+/* 线程入口:直接跑 uarttest_loop(死循环收发测试) */\r
+static void uarttest_thread(void *argument)\r
+{\r
+    (void)argument;\r
+    uarttest_loop();\r
+}\r
+\r
+static uint32_t g_uarttest_thread_id = 0;\r
+\r
+void uarttest_start_thread(void)\r
+{\r
+    if (g_uarttest_thread_id != 0) {\r
+        print_str("[UARTTEST] thread already started\r\n");\r
+        return;\r
+    }\r
+\r
+    app_task_definition_ext_t task_cfg = {\r
+        .attr = {\r
+            .name       = "uarttest",\r
+            .stack_size = 0x2000,\r
+            .priority   = osPriorityNormal,\r
+        },\r
+        .func               = (osThreadFunc_t)uarttest_thread,\r
+        .task_handle        = &g_uarttest_thread_id,\r
+        .create_queue_handle= NULL,\r
+        .use_mem            = USE_EXTEND_MEM,\r
+        .inst_args          = NULL,\r
+    };\r
+\r
+    if (uapi_create_osThread_custom_with_args(&task_cfg) != ERRCODE_SUCC) {\r
+        print_str("[UARTTEST] failed to create thread\r\n");\r
+    } else {\r
+        print_str("[UARTTEST] thread created\r\n");\r
+    }\r
+}\r
+\r
+\r
diff --git a/code/sdk/zhihui/uarttest/src/uarttest_api_at.c b/code/sdk/zhihui/uarttest/src/uarttest_api_at.c
new file mode 100644 (file)
index 0000000..af2ff53
--- /dev/null
@@ -0,0 +1,70 @@
+#include <stdint.h>\r
+#include <string.h>\r
+#include <stdio.h>\r
+\r
+#include "errcode.h"\r
+#include "at_cmd_api.h"\r
+\r
+#include "uarttest.h"\r
+\r
+#define UARTTEST_UNUSED(a) ((void)(a))\r
+\r
+/* AT^UARTTEST - 启动 uarttest_start_thread() */\r
+static uint32_t uarttest_at_cmd(uint8_t *para, uint32_t para_len, char *str_cmd)\r
+{\r
+    UARTTEST_UNUSED(para);\r
+    UARTTEST_UNUSED(para_len);\r
+    UARTTEST_UNUSED(str_cmd);\r
+\r
+    printf("[UARTTEST][AT] start uart thread\r\n");\r
+    uarttest_start_thread();\r
+    return ERRCODE_SUCC;\r
+}\r
+\r
+/* AT^TCPSPEED - 启动 uarttest_tcp_start() */\r
+static uint32_t uarttest_tcpspeed_at_cmd(uint8_t *para, uint32_t para_len, char *str_cmd)\r
+{\r
+    UARTTEST_UNUSED(para);\r
+    UARTTEST_UNUSED(para_len);\r
+    UARTTEST_UNUSED(str_cmd);\r
+\r
+    printf("[UARTTEST][AT] start tcp speed test\r\n");\r
+    uarttest_tcp_start();\r
+    return ERRCODE_SUCC;\r
+}\r
+\r
+/* AT^HTTPSPEED 或 AT^HTTPSPEED=<url> - 启动 HTTP 测速(不保存文件) */\r
+#define UARTTEST_HTTP_URL_BUF_MAX 256\r
+static uint32_t uarttest_httpspeed_at_cmd(uint8_t *para, uint32_t para_len, char *str_cmd)\r
+{\r
+    UARTTEST_UNUSED(str_cmd);\r
+\r
+    static char url_buf[UARTTEST_HTTP_URL_BUF_MAX];\r
+    url_buf[0] = '\0';\r
+    if (para != NULL && para_len > 0) {\r
+        uint32_t copy_len = para_len;\r
+        if (copy_len >= UARTTEST_HTTP_URL_BUF_MAX) {\r
+            copy_len = UARTTEST_HTTP_URL_BUF_MAX - 1;\r
+        }\r
+        memcpy(url_buf, para, copy_len);\r
+        url_buf[copy_len] = '\0';\r
+        printf("[UARTTEST][AT] start http speed test, url=%.*s\r\n", (int)copy_len, para);\r
+        uarttest_http_start(url_buf);\r
+    } else {\r
+        printf("[UARTTEST][AT] start http speed test (default url)\r\n");\r
+        uarttest_http_start(NULL);\r
+    }\r
+    return ERRCODE_SUCC;\r
+}\r
+\r
+static at_cmd_table_t g_uarttest_at_table[] = {\r
+    { "AT^UARTTEST",   uarttest_at_cmd,         NULL },\r
+    { "AT^TCPSPEED",   uarttest_tcpspeed_at_cmd, NULL },\r
+    { "AT^HTTPSPEED",  uarttest_httpspeed_at_cmd, NULL },\r
+};\r
+\r
+errcode_t uarttest_at_init(void)\r
+{\r
+    return at_cmd_add_moudle(g_uarttest_at_table, (uint32_t)(sizeof(g_uarttest_at_table) / sizeof(g_uarttest_at_table[0])));\r
+}\r
+\r
diff --git a/code/sdk/zhihui/uarttest/src/uarttest_http.c b/code/sdk/zhihui/uarttest/src/uarttest_http.c
new file mode 100644 (file)
index 0000000..23971a5
--- /dev/null
@@ -0,0 +1,160 @@
+\r
+/**\r
+ * @file uarttest_http.c\r
+ * @brief HTTP speed test: download via AT command, measure throughput, do not save file.\r
+ * Uses openharmony httpclient.h (HttpClientConn, HttpClientSend, HttpClientRecvResponse).\r
+ */\r
+\r
+#include <stdint.h>\r
+#include <stdbool.h>\r
+#include <string.h>\r
+#include <stdio.h>\r
+\r
+#include "cmsis_os2.h"\r
+#include "custom_utils.h"\r
+#include "httpclient.h"\r
+\r
+#include "uarttest_http.h"\r
+\r
+#include "pm.h"\r
+\r
+extern void print_str(const char *str, ...);\r
+\r
+#ifndef UARTTEST_HTTP_DEFAULT_URL\r
+#define UARTTEST_HTTP_DEFAULT_URL "http://ambybox-api.test.leiniao.com/ambybox-device-api/file/minimax_tts_7324f8e915f0_1773299663021.mp3"\r
+#endif\r
+\r
+#ifndef UARTTEST_HTTP_HEADER_SIZE\r
+#define UARTTEST_HTTP_HEADER_SIZE 2048\r
+#endif\r
+\r
+#ifndef UARTTEST_HTTP_RESP_BUF_SIZE\r
+#define UARTTEST_HTTP_RESP_BUF_SIZE 4096\r
+#endif\r
+\r
+#ifndef UARTTEST_HTTP_URL_MAX\r
+#define UARTTEST_HTTP_URL_MAX 256\r
+#endif\r
+\r
+static uint32_t g_uarttest_http_thread_id = 0;\r
+static char g_uarttest_http_url[UARTTEST_HTTP_URL_MAX];\r
+\r
+static void uarttest_http_thread(void *argument)\r
+{\r
+    (void)argument;\r
+\r
+    const char *url = g_uarttest_http_url[0] != '\0' ? g_uarttest_http_url : UARTTEST_HTTP_DEFAULT_URL;\r
+\r
+    HttpClient client = {0};\r
+    HttpClientData clientData = {0};\r
+\r
+    HTTPC_RESULT res = HttpClientPrepare(&clientData, UARTTEST_HTTP_HEADER_SIZE, UARTTEST_HTTP_RESP_BUF_SIZE);\r
+    if (res != HTTP_SUCCESS) {\r
+        print_str("[UARTTEST][HTTP] Prepare failed: %d\r\n", (int)res);\r
+        return;\r
+    }\r
+\r
+    res = HttpClientConn(&client, url);\r
+    if (res != HTTP_SUCCESS) {\r
+        print_str("[UARTTEST][HTTP] Conn failed: %d\r\n", (int)res);\r
+        HttpClientUnprepare(&clientData);\r
+        return;\r
+    }\r
+\r
+    res = HttpClientSend(&client, url, HTTP_GET, &clientData);\r
+    if (res != HTTP_SUCCESS && res != HTTP_EAGAIN) {\r
+        print_str("[UARTTEST][HTTP] Send failed: %d\r\n", (int)res);\r
+        HttpClientClse(&client);\r
+        HttpClientUnprepare(&clientData);\r
+        return;\r
+    }\r
+\r
+    uint32_t total_bytes = 0;\r
+    uint32_t start_tick = osKernelGetTickCount();\r
+    uint32_t last_tick = start_tick;\r
+    uint32_t last_total = 0;\r
+\r
+    while (1) {\r
+        HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData);\r
+\r
+        if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) {\r
+            int chunk = clientData.contentBlockLen > 0 ? clientData.contentBlockLen : clientData.responseBufLen;\r
+            if (chunk > 0) {\r
+                total_bytes += (uint32_t)chunk;\r
+            }\r
+\r
+            uint32_t now = osKernelGetTickCount();\r
+            if (now - last_tick >= 1000) {\r
+                uint32_t dbytes = total_bytes - last_total;\r
+                float mbps = (float)dbytes * 8.0f / 1000000.0f;\r
+                print_str("[UARTTEST][HTTP] rx=%lu bytes, +%lu bytes/s (%.3f Mbps)\r\n",\r
+                          (unsigned long)total_bytes, (unsigned long)dbytes, mbps);\r
+                last_total = total_bytes;\r
+                last_tick = now;\r
+            }\r
+\r
+            if (!clientData.isMore) {\r
+                break;\r
+            }\r
+        } else {\r
+            print_str("[UARTTEST][HTTP] RecvResponse: %d\r\n", (int)recv_res);\r
+            break;\r
+        }\r
+        osDelay(1);\r
+    }\r
+\r
+    uint32_t elapsed_ms = osKernelGetTickCount() - start_tick;\r
+    if (elapsed_ms > 0 && total_bytes > 0) {\r
+        float avg_kbps = (float)total_bytes * 8.0f / (float)elapsed_ms;\r
+        print_str("[UARTTEST][HTTP] done: %lu bytes in %lu ms, avg %.2f Kbps\r\n",\r
+                  (unsigned long)total_bytes, (unsigned long)elapsed_ms, avg_kbps);\r
+    } else {\r
+        print_str("[UARTTEST][HTTP] done: %lu bytes\r\n", (unsigned long)total_bytes);\r
+    }\r
+\r
+    HttpClientClse(&client);\r
+    HttpClientUnprepare(&clientData);\r
+    g_uarttest_http_thread_id = 0;\r
+    print_str("[UARTTEST][HTTP] closed\r\n");\r
+}\r
+\r
+void uarttest_http_start(const char *url)\r
+{\r
+    if (g_uarttest_http_thread_id != 0) {\r
+        print_str("[UARTTEST][HTTP] thread already running\r\n");\r
+        return;\r
+    }\r
+\r
+    g_uarttest_http_url[0] = '\0';\r
+    if (url != NULL && url[0] != '\0') {\r
+        size_t len = strlen(url);\r
+        if (len >= UARTTEST_HTTP_URL_MAX) {\r
+            len = UARTTEST_HTTP_URL_MAX - 1;\r
+        }\r
+        memcpy(g_uarttest_http_url, url, len);\r
+        g_uarttest_http_url[len] = '\0';\r
+    }\r
+\r
+    app_task_definition_ext_t task_cfg = {\r
+        .attr = {\r
+            .name = "uarttest_http",\r
+            .stack_size = 0x3000,\r
+            .priority = osPriorityNormal,\r
+        },\r
+        .func = (osThreadFunc_t)uarttest_http_thread,\r
+        .task_handle = &g_uarttest_http_thread_id,\r
+        .create_queue_handle = NULL,\r
+        .use_mem = USE_EXTEND_MEM,\r
+        .inst_args = NULL,\r
+    };\r
+\r
+    if (uapi_create_osThread_custom_with_args(&task_cfg) != ERRCODE_SUCC) {\r
+        print_str("[UARTTEST][HTTP] failed to create thread\r\n");\r
+        g_uarttest_http_thread_id = 0;\r
+    } else {\r
+        print_str("[UARTTEST][HTTP] thread created, url=%s\r\n",\r
+                  g_uarttest_http_url[0] != '\0' ? g_uarttest_http_url : UARTTEST_HTTP_DEFAULT_URL);\r
+    }\r
+    int ret = uapi_pm_request_mode(PM_ID_SPI, PM_MODE_PERFORMACE);\r
+    printf("uapi_pm_request_mode ret : %X\n",ret);\r
+}\r
diff --git a/code/sdk/zhihui/uarttest/src/uarttest_tcp.c b/code/sdk/zhihui/uarttest/src/uarttest_tcp.c
new file mode 100644 (file)
index 0000000..6f9d7a9
--- /dev/null
@@ -0,0 +1,129 @@
+#include <stdint.h>\r
+#include <stdbool.h>\r
+#include <string.h>\r
+\r
+#include "cmsis_os2.h"\r
+#include "custom_utils.h"\r
+\r
+#include "lwip/sockets.h"\r
+#include "lwip/inet.h"\r
+#include "lwip/def.h"\r
+#include "lwip/errno.h"\r
+\r
+#include "uarttest_tcp.h"\r
+\r
+extern void print_str(const char *str, ...);\r
+\r
+#ifndef UARTTEST_TCP_SERVER_IP\r
+#define UARTTEST_TCP_SERVER_IP   "172.16.1.2"\r
+#endif\r
+\r
+#ifndef UARTTEST_TCP_SERVER_PORT\r
+#define UARTTEST_TCP_SERVER_PORT 5001\r
+#endif\r
+\r
+#ifndef UARTTEST_TCP_SEND_SECONDS\r
+#define UARTTEST_TCP_SEND_SECONDS 10\r
+#endif\r
+\r
+#ifndef UARTTEST_TCP_PAYLOAD_SIZE\r
+#define UARTTEST_TCP_PAYLOAD_SIZE 1460\r
+#endif\r
+\r
+static uint32_t g_uarttest_tcp_thread_id = 0;\r
+\r
+static void uarttest_tcp_thread(void *argument)\r
+{\r
+    (void)argument;\r
+\r
+    int fd = -1;\r
+    struct sockaddr_in addr;\r
+    memset(&addr, 0, sizeof(addr));\r
+    addr.sin_family = AF_INET;\r
+    addr.sin_port = PP_HTONS(UARTTEST_TCP_SERVER_PORT);\r
+    addr.sin_addr.s_addr = inet_addr(UARTTEST_TCP_SERVER_IP);\r
+\r
+    fd = lwip_socket(AF_INET, SOCK_STREAM, 0);\r
+    if (fd < 0) {\r
+        print_str("[UARTTEST][TCP] socket failed\r\n");\r
+        return;\r
+    }\r
+\r
+    print_str("[UARTTEST][TCP] connect %s:%d ...\r\n", UARTTEST_TCP_SERVER_IP, UARTTEST_TCP_SERVER_PORT);\r
+    if (lwip_connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {\r
+        int err = errno;\r
+        print_str("[UARTTEST][TCP] connect failed, errno=%d\r\n", err);\r
+        lwip_close(fd);\r
+        return;\r
+    }\r
+    print_str("[UARTTEST][TCP] connected\r\n");\r
+\r
+    static uint8_t payload[UARTTEST_TCP_PAYLOAD_SIZE];\r
+    for (uint32_t i = 0; i < sizeof(payload); ++i) {\r
+        payload[i] = (uint8_t)(i & 0xFF);\r
+    }\r
+\r
+    uint32_t total = 0;\r
+    uint32_t last_total = 0;\r
+    uint32_t start_tick = osKernelGetTickCount();\r
+    uint32_t last_tick = start_tick;\r
+\r
+    while (1) {\r
+        int ret = lwip_send(fd, payload, sizeof(payload), 0);\r
+        if (ret <= 0) {\r
+            print_str("[UARTTEST][TCP] send ret=%d, stop\r\n", ret);\r
+            break;\r
+        }\r
+        total += (uint32_t)ret;\r
+\r
+        uint32_t now = osKernelGetTickCount();\r
+        if (now - last_tick >= 1000) {\r
+            uint32_t dbytes = total - last_total;\r
+            float mbps = (float)dbytes * 8.0f / 1000000.0f;\r
+            print_str("[UARTTEST][TCP] tx=%lu bytes, +%lu bytes/s (%.3f Mbps)\r\n",\r
+                      (unsigned long)total, (unsigned long)dbytes, mbps);\r
+            last_total = total;\r
+            last_tick = now;\r
+        }\r
+\r
+        if (UARTTEST_TCP_SEND_SECONDS > 0) {\r
+            uint32_t elapsed_ms = now - start_tick;\r
+            if (elapsed_ms >= (uint32_t)UARTTEST_TCP_SEND_SECONDS * 1000U) {\r
+                print_str("[UARTTEST][TCP] done %u seconds\r\n", (unsigned)UARTTEST_TCP_SEND_SECONDS);\r
+                break;\r
+            }\r
+        }\r
+        osDelay(1);\r
+    }\r
+\r
+    lwip_close(fd);\r
+    print_str("[UARTTEST][TCP] closed\r\n");\r
+}\r
+\r
+void uarttest_tcp_start(void)\r
+{\r
+    if (g_uarttest_tcp_thread_id != 0) {\r
+        print_str("[UARTTEST][TCP] thread already started\r\n");\r
+        return;\r
+    }\r
+\r
+    app_task_definition_ext_t task_cfg = {\r
+        .attr = {\r
+            .name = "uarttest_tcp",\r
+            .stack_size = 0x3000,\r
+            .priority = osPriorityNormal,\r
+        },\r
+        .func = (osThreadFunc_t)uarttest_tcp_thread,\r
+        .task_handle = &g_uarttest_tcp_thread_id,\r
+        .create_queue_handle = NULL,\r
+        .use_mem = USE_EXTEND_MEM,\r
+        .inst_args = NULL,\r
+    };\r
+\r
+    if (uapi_create_osThread_custom_with_args(&task_cfg) != ERRCODE_SUCC) {\r
+        print_str("[UARTTEST][TCP] failed to create thread\r\n");\r
+    } else {\r
+        print_str("[UARTTEST][TCP] thread created\r\n");\r
+    }\r
+}\r
+\r