spi传输文件测试用例 2831/1 2831/head
author孙春阳 <snickersun@163.com>
Mon, 1 Dec 2025 01:46:20 +0000 (09:46 +0800)
committer孙春阳 <snickersun@163.com>
Mon, 1 Dec 2025 01:46:20 +0000 (09:46 +0800)
cat1modem/src/LeiNiao/httpfiledownload.py
cat1modem/src/LeiNiao/main_init.py
code/sdk/build/config/target_config/brandy/config.py
code/sdk/zhihui/utils/custom_utils_at_cmd.c
code/sdk/zhihui/utilscpp/custom_utilscpp.cpp
code/sdk/zhihui/utilscpp/custom_utilscpp.h

index 67a52d6..b5bc29f 100644 (file)
@@ -24,6 +24,36 @@ import urandom
 from usr.EventMesh import eventMesh
 import usocket
 import _thread
+from machine import SPI
+
+# spi - 通道选择[0,1],int类型。
+
+# mode - SPI 的工作模式,int类型,说明如下:
+# 时钟极性CPOL:即SPI空闲时,时钟信号SCLK的电平(0:空闲时低电平; 1:空闲时高电平)
+# 0 : CPOL=0, CPHA=0
+# 1 : CPOL=0, CPHA=1
+# 2 : CPOL=1, CPHA=0
+# 3 : CPOL=1, CPHA=1
+
+# clk - 时钟频率,说明如下:
+# 0 : 100kHz
+# 1 : 200kHz
+# 2 : 300MHz
+# 3 : 400MHz
+# 4 : 500MHz
+# 5 : 600MHz
+# 6 : 700MHz
+# 7 : 812.5kHz
+# 8 : 1.625MHz
+# 9 : 3.25MHz
+# 10 : 6.5MHz
+# 11 : 13MHz
+# 12 : 26MHz
+# 13 : 52MHz
+
+# 52MHz时钟频率对硬件要求较高。
+# spi_obj = SPI(1, 0, 1)
+
 
 REQUESTS_AVAILABLE = True
 
@@ -221,10 +251,10 @@ class HttpFileDownloader:
 class HttpFileDownloadTcpServer:
     """
     HTTP 文件下载 TCP 服务器
-    监听 TCP 连接,接收 URL 请求,下载文件并通过 socket 发送数据
+    监听 TCP 连接,接收 URL 请求,下载文件并通过 SPI 发送数据
     """
     
-    def __init__(self, host='172.16.1.2', port=5002, timeout=30, sizeof=255):
+    def __init__(self, host='172.16.1.2', port=5002, timeout=30, sizeof=255, spi_obj=None):
         """
         初始化 TCP 服务器
         
@@ -233,6 +263,7 @@ class HttpFileDownloadTcpServer:
             port: 监听端口,默认 5002
             timeout: 下载超时时间(秒),默认 30 秒
             sizeof: 读取缓冲区的数据块大小,默认 255 字节
+            spi_obj: SPI 对象,用于发送数据。如果为 None,则自动创建 SPI(1, 0, 1)
         """
         self.host = host
         self.port = port
@@ -241,6 +272,15 @@ class HttpFileDownloadTcpServer:
         self.sock = None
         self.running = False
         self.downloader = HttpFileDownloader(timeout=timeout, sizeof=sizeof)
+        
+        # 初始化 SPI 对象
+        if spi_obj is None:
+            # 默认创建 SPI(1, 0, 1): 通道1, 模式0, 时钟频率1(1.625MHz)
+            self.spi_obj = SPI(2, 0, 9)
+            clog.info("Created default SPI object: SPI(2, 0, 9)")
+        else:
+            self.spi_obj = spi_obj
+            clog.info("Using provided SPI object")
     
     def __socket_init(self):
         """初始化 TCP socket"""
@@ -294,10 +334,10 @@ class HttpFileDownloadTcpServer:
             
             clog.info("Received download request for URL: {}".format(url))
             
-            # 发送确认消息
-            conn.send(b'OK: Starting download...\n')
+            # 发送确认消息(通过 socket)
+            conn.send(b'OK: Starting download via SPI...\n')
             
-            # 下载文件并通过 socket 发送
+            # 下载文件并通过 SPI 发送(数据通过 SPI,状态信息通过 socket)
             success, download_time, file_size = self.__download_and_send(conn, url)
             
             if success:
@@ -316,12 +356,33 @@ class HttpFileDownloadTcpServer:
                 pass
             clog.info("Client connection closed: {}:{}".format(addr[0], addr[1]))
     
+    def close(self):
+        """
+        关闭SPI对象,释放资源
+        """
+        if self.spi_obj is not None:
+            try:
+                # 尝试调用close方法
+                if hasattr(self.spi_obj, 'close'):
+                    self.spi_obj.close()
+                    clog.info("SPI object closed via close() method")
+                # 尝试调用deinit方法(MicroPython常用)
+                elif hasattr(self.spi_obj, 'deinit'):
+                    self.spi_obj.deinit()
+                    clog.info("SPI object closed via deinit() method")
+                else:
+                    clog.warning("SPI object does not have close() or deinit() method")
+            except Exception as e:
+                clog.error("Error closing SPI object: {}".format(e))
+            finally:
+                self.spi_obj = None
+    
     def __download_and_send(self, conn, url):
         """
-        下载文件并通过 socket 发送数据
+        下载文件并通过 SPI 发送数据
         
         Args:
-            conn: 客户端连接对象
+            conn: 客户端连接对象(用于接收 URL 请求,数据通过 SPI 发送)
             url: 要下载的文件 URL
             
         Returns:
@@ -351,19 +412,41 @@ class HttpFileDownloadTcpServer:
                 except (ValueError, TypeError):
                     total_size = 0
             
-            # 发送文件大小信息(可选)
+            # 通过 socket 发送文件大小信息(可选,用于客户端确认)
             if total_size > 0:
                 size_info = "Content-Length: {}\n".format(total_size).encode('utf-8')
-                conn.send(size_info)
+                try:
+                    conn.send(size_info)
+                except:
+                    pass  # 如果 socket 发送失败,继续通过 SPI 发送数据
             
-            # 下载数据并通过 socket 发送
+            # 下载数据并通过 SPI 发送
             downloaded_size = 0
             
             # response.content 是 bytes 类型生成器
             for chunk in response.content:
                 if chunk:  # 过滤掉空块
                     try:
-                        conn.send(chunk)
+                        # 通过 SPI 发送数据块
+                        # SPI API: write(data, datasize) 或 write_read(read_buf, write_data, datasize, timeout)
+                        chunk_len = len(chunk)
+                        if hasattr(self.spi_obj, 'write'):
+                            # write(data, datasize) - datasize 参数必需
+                            self.spi_obj.write(chunk, chunk_len)
+                        elif hasattr(self.spi_obj, 'write_read'):
+                            # write_read(read_buf, write_data, datasize, timeout)
+                            # 或者 write_read(read_buf, write_data, datasize)
+                            read_buf = bytearray(chunk_len)
+                            try:
+                                # 尝试带timeout的调用
+                                self.spi_obj.write_read(read_buf, chunk, chunk_len, 5)
+                            except TypeError:
+                                # 尝试不带timeout的调用
+                                self.spi_obj.write_read(read_buf, chunk, chunk_len)
+                        else:
+                            clog.error("SPI object does not have write or write_read method")
+                            return False, 0.0, -1
+                        
                         downloaded_size += len(chunk)
                         
                         # 显示进度(每发送一定数据量后打印一次)
@@ -374,20 +457,23 @@ class HttpFileDownloadTcpServer:
                                 clog.info("Progress: {:.2f}% ({}/{}) bytes".format(
                                     progress, downloaded_size, total_size))
                     except Exception as e:
-                        clog.error("Error sending data: {}".format(e))
+                        clog.error("Error sending data via SPI: {}".format(e))
                         return False, 0.0, -1
             
             # 记录结束时间并计算耗时
             end_time = utime.time()
             download_time = end_time - start_time
             
-            clog.info("Download and send completed: {} bytes in {:.3f}s".format(
+            clog.info("Download and send via SPI completed: {} bytes in {:.3f}s".format(
                 downloaded_size, download_time))
             
+            # 注意:TCP服务器模式下,SPI对象在服务器运行期间保持打开,不在这里关闭
+            # 只有在服务器停止时才关闭SPI
+            
             return True, download_time, downloaded_size
             
         except Exception as e:
-            clog.error("Error downloading and sending file: {}".format(e))
+            clog.error("Error downloading and sending file via SPI: {}".format(e))
             return False, 0.0, -1
     
     def __server_loop(self):
@@ -435,6 +521,8 @@ class HttpFileDownloadTcpServer:
             except:
                 pass
             self.sock = None
+        # 关闭SPI对象
+        self.close()
         clog.info("HTTP file download TCP server stopped")
     
     def is_running(self):
@@ -442,9 +530,186 @@ class HttpFileDownloadTcpServer:
         return self.running
 
 
-# 保持原有功能的简单函数(向后兼容)
+# ==================== SPI传输速度测试模式 ====================
+# 直接从固定URL下载文件并通过SPI发送,用于测试SPI传输速度
+# 不再通过TCP接收URL指令
+
+# 固定下载地址(与MCU侧AT指令AT^HTTPDLCLIENT使用的地址一致)
+DEFAULT_TEST_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/StraightEyes/Amber/WorkAndStudy.mp4"
+
+class HttpFileDownloadSpiTest:
+    """
+    HTTP文件下载SPI传输速度测试
+    直接从固定URL下载文件并通过SPI发送,用于测试SPI传输速度
+    """
+    
+    def __init__(self, url=None, timeout=30, sizeof=255, spi_obj=None):
+        """
+        初始化SPI传输测试
+        
+        Args:
+            url: 下载URL,默认使用DEFAULT_TEST_URL
+            timeout: 下载超时时间(秒),默认30秒
+            sizeof: 读取缓冲区的数据块大小,默认255字节
+            spi_obj: SPI对象,用于发送数据。如果为None,则自动创建SPI(1, 0, 12)
+        """
+        self.url = url if url else DEFAULT_TEST_URL
+        self.timeout = timeout
+        self.sizeof = sizeof
+        self.downloader = HttpFileDownloader(timeout=timeout, sizeof=sizeof)
+        
+        # 初始化SPI对象
+        if spi_obj is None:
+            # 默认创建SPI(2, 0, 1): 通道2, 模式0, 时钟频率1(26MHz)
+            # 注意:Python端SPI2对应MCU端SPI1
+            # 频率参数1表示26MHz(根据实际SPI驱动实现)
+            self.spi_obj = SPI(2, 0, 9)
+            clog.info("Created default SPI object: SPI(2, 0, 9) for 26MHz")
+        else:
+            self.spi_obj = spi_obj
+            clog.info("Using provided SPI object")
+    
+    def close(self):
+        """
+        关闭SPI对象,释放资源
+        """
+        if self.spi_obj is not None:
+            try:
+                # 尝试调用close方法
+                if hasattr(self.spi_obj, 'close'):
+                    self.spi_obj.close()
+                    clog.info("SPI object closed via close() method")
+                # 尝试调用deinit方法(MicroPython常用)
+                elif hasattr(self.spi_obj, 'deinit'):
+                    self.spi_obj.deinit()
+                    clog.info("SPI object closed via deinit() method")
+                else:
+                    clog.warning("SPI object does not have close() or deinit() method")
+            except Exception as e:
+                clog.error("Error closing SPI object: {}".format(e))
+            finally:
+                self.spi_obj = None
+    
+    def start_download_and_send(self):
+        """
+        开始下载文件并通过SPI发送(用于测试SPI传输速度)
+        
+        Returns:
+            tuple: (success: bool, download_time: float, file_size: int)
+        """
+        try:
+            clog.info("Starting SPI speed test: {}".format(self.url))
+            
+            # 记录开始时间
+            start_time = utime.time()
+            
+            # 发送GET请求
+            response = request.get(
+                self.url,
+                sizeof=self.sizeof,
+                decode=False  # 不进行UTF-8解码,直接获取bytes
+            )
+            
+            # 检查响应状态码
+            if response.status_code != 200:
+                clog.error("Error: HTTP error {}: {}".format(response.status_code, self.url))
+                self.close()  # 关闭SPI
+                return False, 0.0, -1
+            
+            # 获取文件总大小(如果服务器提供了Content-Length)
+            total_size = 0
+            if 'Content-Length' in response.headers:
+                try:
+                    total_size = int(response.headers['Content-Length'])
+                    clog.info("File size: {} bytes ({:.2f} MB)".format(
+                        total_size, total_size / 1024.0 / 1024.0))
+                except (ValueError, TypeError):
+                    total_size = 0
+            
+            # 下载数据并通过SPI发送
+            downloaded_size = 0
+            
+            # response.content是bytes类型生成器
+            for chunk in response.content:
+                if chunk:  # 过滤掉空块
+                    try:
+                        # 通过SPI发送数据块
+                        # SPI API: write(data, datasize) 或 write_read(read_buf, write_data, datasize, timeout)
+                        chunk_len = len(chunk)
+                        if hasattr(self.spi_obj, 'write'):
+                            # write(data, datasize) - datasize 参数必需
+                            self.spi_obj.write(chunk, chunk_len)
+                        elif hasattr(self.spi_obj, 'write_read'):
+                            # write_read(read_buf, write_data, datasize, timeout)
+                            # 或者 write_read(read_buf, write_data, datasize)
+                            read_buf = bytearray(chunk_len)
+                            try:
+                                # 尝试带timeout的调用
+                                self.spi_obj.write_read(read_buf, chunk, chunk_len, 5)
+                            except TypeError:
+                                # 尝试不带timeout的调用
+                                self.spi_obj.write_read(read_buf, chunk, chunk_len)
+                        else:
+                            clog.error("SPI object does not have write or write_read method")
+                            self.close()  # 关闭SPI
+                            return False, 0.0, -1
+                        
+                        downloaded_size += len(chunk)
+                        
+                        # 显示进度(每发送64KB打印一次,减少打印数量)
+                        if total_size > 0:
+                            progress = (downloaded_size / total_size) * 100
+                            # 每发送64KB打印一次进度
+                            if downloaded_size % (64 * 1024) < self.sizeof:
+                                clog.info("SPI TX Progress: {:.2f}% ({}/{}) bytes".format(
+                                    progress, downloaded_size, total_size))
+                    except Exception as e:
+                        clog.error("Error sending data via SPI: {}".format(e))
+                        self.close()  # 关闭SPI
+                        return False, 0.0, -1
+            
+            # 记录结束时间并计算耗时
+            end_time = utime.time()
+            download_time = end_time - start_time
+            
+            # 计算传输速度
+            speed_kbps = (downloaded_size * 8.0) / download_time / 1024.0  # Kbps
+            speed_mbps = speed_kbps / 1024.0  # Mbps
+            
+            if speed_mbps >= 1.0:
+                clog.info("SPI speed test completed: {} bytes in {:.3f}s, Speed: {:.2f} Mbps ({:.2f} Kbps)".format(
+                    downloaded_size, download_time, speed_mbps, speed_kbps))
+            else:
+                clog.info("SPI speed test completed: {} bytes in {:.3f}s, Speed: {:.2f} Kbps".format(
+                    downloaded_size, download_time, speed_kbps))
+            
+            # 传输完成后关闭SPI
+            self.close()
+            
+            return True, download_time, downloaded_size
+            
+        except Exception as e:
+            clog.error("Error in SPI speed test: {}".format(e))
+            self.close()  # 确保异常时也关闭SPI
+            return False, 0.0, -1
+
+# 创建并启动SPI传输速度测试(自动执行)
+def start_spi_speed_test():
+    """启动SPI传输速度测试"""
+    test = HttpFileDownloadSpiTest()
+    try:
+        success, download_time, file_size = test.start_download_and_send()
+        if success:
+            clog.info("SPI speed test PASSED")
+        else:
+            clog.error("SPI speed test FAILED")
+        return success, download_time, file_size
+    finally:
+        # 确保SPI对象被关闭(start_download_and_send内部已关闭,这里作为双重保险)
+        test.close()
 
-HttpFileDownloadTcpServer().start()
+# 自动启动SPI传输速度测试(替代原来的TCP服务器)
+start_spi_speed_test()
 # 示例用法
 # if __name__ == '__main__':
 #     # 方式1: 使用类下载并保存文件
index 065fe3b..7e8138a 100644 (file)
@@ -33,11 +33,11 @@ try:
         print("检测文件成功!!!!!!!\r\n",'cat1service')
     except Exception as e:
         print("检测文件失败!!!!!!!{}\r\n",'cat1service')
-    try:
-        from usr.httpfiledownload import HttpFileDownloadTcpServer
-        print("检测文件成功!!!!!!!\r\n",'httpfiledownload')
-    except Exception as e:
-        print("检测文件失败!!!!!!!{}\r\n",'httpfiledownload')
+    # try:
+    #     from usr.httpfiledownload import HttpFileDownloadTcpServer
+    #     print("检测文件成功!!!!!!!\r\n",'httpfiledownload')
+    # except Exception as e:
+    #     print("检测文件失败!!!!!!!{}\r\n",'httpfiledownload')
         
     ota_flag = 1
     print("检测文件成功!!!!!!!\r\n")
index 40bd6a5..dcb5428 100644 (file)
@@ -248,6 +248,7 @@ target = {
                     '_ALL_SOURCE', 'SUPPORT_CXX', 'I2C_SLAVE_REG_ADDR_4BYTE', 'CFG_DRIVERS_NANDFLASH',
                     "-:TARGET_CHIP_BRANDY=1", "-:BRANDY_CHIP_V100=1", 'ENABLE_UIKIT', 'SW_UART_DEBUG', "HASH_MEM_COPY",
                     'ENABLE_ECC', 'SUPPORT_RECOVERY_DISPLAY',
+                    'CONFIG_SPI_SUPPORT_DMA=1', 'CONFIG_SPI_SUPPORT_POLL_AND_DMA_AUTO_SWITCH', 'CONFIG_SPI_AUTO_SWITCH_DMA_THRESHOLD=0x8',
                     'MBEDTLS_NO_PLATFORM_ENTROPY', 'MBEDTLS_ENTROPY_HARDWARE_ALT', 'MARKET_ENABLE'],
         'ram_component': ["algorithm", "dfx_port_brandy", "dfx_update", "osal", "arch_port", "-:testsuite",
                           "liteos_port", 'lcd', 'qspi_display', 'dfx_file_operation',
@@ -277,7 +278,7 @@ target = {
                     'PRE_ASIC', 'BRANDY_PRODUCT_EVB4',
                     '_ALL_SOURCE', '__LITEOS__', 'SUPPORT_CXX', 'ENABLE_UIKIT',
                     "-:TARGET_CHIP_BRANDY=1", "-:BRANDY_CHIP_V100=1", 'MULTIPLEXING_UART',
-                    'CONFIG_SPI_SUPPORT_POLL_AND_DMA_AUTO_SWITCH', 'CONFIG_SPI_AUTO_SWITCH_DMA_THRESHOLD=0x8',
+                    'CONFIG_SPI_SUPPORT_DMA=1', 'CONFIG_SPI_SUPPORT_POLL_AND_DMA_AUTO_SWITCH', 'CONFIG_SPI_AUTO_SWITCH_DMA_THRESHOLD=0x8',
                     'CONFIG_I2C_SUPPORT_POLL_AND_DMA_AUTO_SWITCH', 'CONFIG_I2C_POLL_AND_DMA_AUTO_SWITCH_THRESHOLD=0x8'],
         'ram_component': ["osal", "arch_port", "liteos_port", 'psram', 'pm_service', 'pm_brandy',
                           'hal_mips', 'lcd', 'qspi_display', 'hal_dma_v100', "dfx_port_brandy",
index f10d4c0..e8c4d3d 100644 (file)
@@ -52,6 +52,9 @@
 #include "ppg_hal.h"
 #endif
 
+/* Include custom_utilscpp.h for SPI1 receive functions */
+#include "custom_utilscpp.h"
+
 #define ARRAY_COUNT(array) (sizeof(array) / sizeof(array[0]))
 #define CUSTOM_UTILS_MAX_ARGC            8
 #define CUSTOM_UTILS_MAX_ARGV_LEN        256
@@ -1912,6 +1915,69 @@ static uint32_t custom_lowpower_alarm_test(uint8_t *para, uint32_t paraLen, char
     return ERRCODE_SUCC;
 }
 
+/* ==================== SPI1 Client Receive Test AT Commands ==================== */
+
+/**
+ * @brief AT^SPI1RXSTART - Start SPI1 client receive data test
+ * Usage: AT^SPI1RXSTART
+ */
+static uint32_t custom_spi1_rx_start(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    UNUSED(para);
+    UNUSED(paraLen);
+    UNUSED(strCommonCmd);
+    
+    wstp_print("AT^SPI1RXSTART: Starting SPI1 client receive test...\n");
+    custom_spi1_client_receive_start();
+    wstp_print("AT^SPI1RXSTART: SPI1 receive started successfully\n");
+    
+    return ERRCODE_SUCC;
+}
+
+/**
+ * @brief AT^SPI1RXSTOP - Stop SPI1 client receive data test
+ * Usage: AT^SPI1RXSTOP
+ */
+static uint32_t custom_spi1_rx_stop(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    UNUSED(para);
+    UNUSED(paraLen);
+    UNUSED(strCommonCmd);
+    
+    wstp_print("AT^SPI1RXSTOP: Stopping SPI1 client receive test...\n");
+    custom_spi1_client_receive_stop();
+    wstp_print("AT^SPI1RXSTOP: SPI1 receive stopped successfully\n");
+    
+    return ERRCODE_SUCC;
+}
+
+/**
+ * @brief AT^SPI1RXSTAT - Get SPI1 receive statistics
+ * Usage: AT^SPI1RXSTAT
+ */
+static uint32_t custom_spi1_rx_stat(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    UNUSED(para);
+    UNUSED(paraLen);
+    UNUSED(strCommonCmd);
+    
+    uint32_t total_received = custom_spi1_client_get_total_received();
+    float total_kb = total_received / 1024.0f;
+    float total_mb = total_kb / 1024.0f;
+    
+    if (total_mb >= 1.0f) {
+        wstp_print("AT^SPI1RXSTAT: Total received: %u bytes (%.2f MB, %.2f KB)\n", 
+                   total_received, total_mb, total_kb);
+    } else if (total_kb >= 1.0f) {
+        wstp_print("AT^SPI1RXSTAT: Total received: %u bytes (%.2f KB)\n", 
+                   total_received, total_kb);
+    } else {
+        wstp_print("AT^SPI1RXSTAT: Total received: %u bytes\n", total_received);
+    }
+    
+    return ERRCODE_SUCC;
+}
+
 static const at_cmd_table_t g_custom_utils_cmd[] = {
     { (char *)"AT^CUSTOM_SN",      custom_utils_setsn, (char *)"123456789ABCDEF2345678" },
     { (char *)"AT^CUSTOM_SN_REQ",  custom_utils_getsn, (char *)" " },
@@ -2018,6 +2084,11 @@ static const at_cmd_table_t g_custom_utils_cmd[] = {
     { (char *)"AT^SETUART",      custom_set_uart_status, (char *)" " },
     { (char *)"AT^GETBTMAC",      custom_get_btmac, (char *)" " },
     { (char *)"AT^LOWPOWER",      custom_lowpower_alarm_test, (char *)" " },
+    
+    /* SPI1 client receive test AT commands */
+    { (char *)"AT^SPI1RXSTART",   custom_spi1_rx_start, (char *)" " },
+    { (char *)"AT^SPI1RXSTOP",    custom_spi1_rx_stop, (char *)" " },
+    { (char *)"AT^SPI1RXSTAT",    custom_spi1_rx_stat, (char *)" " },
 };
 
 
index bc7395d..df6a294 100644 (file)
@@ -80,13 +80,28 @@ extern "C" {
 
 #include "upg_alloc.h"
 
-
+/* SPI driver includes */
+#include "driver/spi.h"
+#include "hal_spi.h"
+#include "driver/tcxo.h"
+#include "thread_init.h"
+#include "spi_porting.h"
 
 #define IN_BUF_SIZE (1 << 8)
 #define OUT_BUF_SIZE (1 << 8)
 
 static osTimerId_t g_bt_scan_timer = NULL;
 
+/* SPI1 client receive data variables */
+static osTimerId_t g_spi1_stats_timer = NULL;
+static uint8_t g_spi1_receive_running = 0;
+static uint32_t g_spi1_total_received = 0;
+static uint32_t g_spi1_last_received = 0;
+static uint32_t g_spi1_last_print_time = 0;
+#define SPI1_RECV_BUFFER_SIZE 4096  // DMA模式下使用4K缓冲区,提高传输效率,减少DMA配置次数
+#define SPI1_BUS_ID SPI_BUS_1
+#define SPI1_RECV_TIMEOUT_MS 50  // 减小超时时间到50ms,提高读取频率,避免FIFO溢出
+
 static SRes Decode2(CLzmaDec *state, ISeqOutStream *outStream, ISeqInStream *inStream,
     UInt64 unpackSize)
 {
@@ -569,4 +584,408 @@ int custom_stop_play_audio(void)
     
     printf("=== AudioPlayer stop process completed ===\n");
     return 0;
+}
+
+/* ==================== SPI1 Client Receive Data Functions ==================== */
+
+/**
+ * @brief SPI1 statistics print callback (called by timer every 1 second)
+ */
+static void custom_spi1_stats_callback(void)
+{
+    if (!g_spi1_receive_running) {
+        return;
+    }
+
+    uint32_t current_time = uapi_tcxo_get_us() / 1000; // Convert to milliseconds
+    uint32_t bytes_received_this_second = g_spi1_total_received - g_spi1_last_received;
+    
+    // Only print if there's data received (rate > 0)
+    if (bytes_received_this_second > 0) {
+        float rate_kbps = (bytes_received_this_second * 8.0f) / 1024.0f; // Convert to Kbps
+        float rate_mbps = rate_kbps / 1024.0f; // Convert to Mbps
+        
+        if (rate_mbps >= 1.0f) {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+                "SPI1 RX: Total=%u bytes, Rate=%.2f Mbps (%.2f Kbps), This second=%u bytes",
+                g_spi1_total_received, rate_mbps, rate_kbps, bytes_received_this_second);
+        } else {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+                "SPI1 RX: Total=%u bytes, Rate=%.2f Kbps, This second=%u bytes",
+                g_spi1_total_received, rate_kbps, bytes_received_this_second);
+        }
+    }
+    
+    // Update last received count for next calculation
+    g_spi1_last_received = g_spi1_total_received;
+    g_spi1_last_print_time = current_time;
+}
+
+/**
+ * @brief SPI1 receive data task function
+ */
+static void custom_spi1_receive_task_func(void *pvParameters)
+{
+    UNUSED(pvParameters);
+    
+    spi_bus_t bus = SPI1_BUS_ID;
+    uint8_t spi_buffer[SPI1_RECV_BUFFER_SIZE];
+    errcode_t ret;
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 receive task started");
+    
+    // Initialize SPI1 as Slave (Client) mode
+    spi_attr_t attr = {0};
+    spi_extra_attr_t extra_attr = {0};
+    
+    attr.is_slave = true;              // Slave模式(Client)
+    attr.slave_num = 0;                 // Slave编号
+    attr.bus_clk = 32000000;           // 总线时钟32MHz
+    attr.freq_mhz = 0;                  // Slave模式下由Master控制(26MHz由Master设置)
+    attr.clk_polarity = SPI_CFG_CLK_CPOL_0;  // CPOL=0
+    attr.clk_phase = SPI_CFG_CLK_CPHA_0;    // CPHA=0 (Mode 0)
+    attr.frame_format = SPI_CFG_FRAME_FORMAT_MOTOROLA_SPI;
+    attr.spi_frame_format = (uint32_t)HAL_SPI_FRAME_FORMAT_STANDARD;  // 标准SPI格式(非QUAD)
+    attr.frame_size = (uint32_t)HAL_SPI_FRAME_SIZE_8;  // 8位数据(与Python端匹配)
+    attr.tmod = HAL_SPI_TRANS_MODE_RX;      // 只接收模式
+    attr.sste = SPI_CFG_SSTE_DISABLE;
+    attr.ndf = 1;
+    
+    // 启用DMA模式,避免FIFO溢出(必须使用DMA,否则FIFO会一直full)
+    extra_attr.tx_use_dma = false;
+    extra_attr.rx_use_dma = true;  // 启用DMA接收模式,避免FIFO溢出
+    memset(&extra_attr.qspi_param, 0, sizeof(extra_attr.qspi_param));
+    memset(&extra_attr.sspi_param, 0, sizeof(extra_attr.sspi_param));
+    
+    // Deinitialize SPI first (if already initialized)
+    uapi_spi_deinit(bus);
+    
+    // Initialize SPI
+    ret = uapi_spi_init(bus, &attr, &extra_attr);
+    if (ret != ERRCODE_SUCC) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "SPI1 init failed: 0x%x", ret);
+        g_spi1_receive_running = 0;
+        return;
+    }
+    
+    // Enable DMA mode for receiving (to avoid FIFO overflow)
+    // DMA mode allows data to be transferred directly to buffer without CPU intervention
+    // 必须使用DMA,否则FIFO会一直full
+#if defined(CONFIG_SPI_SUPPORT_DMA) && (CONFIG_SPI_SUPPORT_DMA == 1)
+    // 检查缓冲区地址对齐(DMA通常要求4字节对齐)
+    uint32_t buffer_addr = (uint32_t)(uintptr_t)spi_buffer;
+    if (buffer_addr % 4 != 0) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 buffer not 4-byte aligned: addr=0x%x, alignment=%u", 
+            buffer_addr, buffer_addr % 4);
+    }
+    
+    // 检查缓冲区大小是否满足DMA要求(对于8-bit宽度,应该是1字节对齐,但通常4字节对齐更好)
+    if (SPI1_RECV_BUFFER_SIZE % 4 != 0) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 buffer size not 4-byte aligned: size=%u", SPI1_RECV_BUFFER_SIZE);
+    }
+    
+    spi_dma_config_t dma_cfg = {
+        .src_width = 0,      // 8-bit data width (0 = 8-bit, 1 = 16-bit, 2 = 32-bit)
+        .dest_width = 0,     // 8-bit data width
+        .burst_length = 0,   // Burst length (0 = single transfer, 1 = 4, 2 = 8, 3 = 16)
+        .priority = 0        // DMA priority (0 = lowest)
+    };
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+        "SPI1 Setting DMA mode: src_width=%u, dest_width=%u, burst_length=%u, priority=%u, buffer_addr=0x%x, buffer_size=%u",
+        dma_cfg.src_width, dma_cfg.dest_width, dma_cfg.burst_length, dma_cfg.priority,
+        buffer_addr, SPI1_RECV_BUFFER_SIZE);
+    
+    ret = uapi_spi_set_dma_mode(bus, true, &dma_cfg);
+    if (ret != ERRCODE_SUCC) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 DMA mode enable failed: 0x%x, buffer_addr=0x%x, buffer_size=%u", 
+            ret, buffer_addr, SPI1_RECV_BUFFER_SIZE);
+        // 如果DMA配置失败,尝试使用默认配置(NULL参数)
+        ret = uapi_spi_set_dma_mode(bus, true, NULL);
+        if (ret != ERRCODE_SUCC) {
+            WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                "SPI1 DMA mode enable failed with default config: 0x%x", ret);
+            // DMA配置失败,但继续运行(可能会在读取时失败,但至少可以尝试)
+        } else {
+            WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 DMA mode enabled with default config");
+        }
+    } else {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 DMA mode enabled for RX (prevents FIFO overflow)");
+    }
+#endif  /* CONFIG_SPI_SUPPORT_DMA */
+    
+    // Verify SPI configuration after initialization
+    spi_attr_t verify_attr = {0};
+    ret = uapi_spi_get_attr(bus, &verify_attr);
+    if (ret == ERRCODE_SUCC) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 config verification: is_slave=%d, tmod=%u, clk_polarity=%u, clk_phase=%u, frame_size=%u",
+            verify_attr.is_slave, verify_attr.tmod, verify_attr.clk_polarity, 
+            verify_attr.clk_phase, verify_attr.frame_size);
+    } else {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "Failed to get SPI1 attributes: 0x%x", ret);
+    }
+    
+    // Verify device mode via porting layer
+    spi_mode_t device_mode = spi_porting_get_device_mode(bus);
+    if (device_mode == SPI_MODE_SLAVE) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 device mode verified: SLAVE mode");
+    } else {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "SPI1 device mode MISMATCH! Expected SLAVE, got mode=%d", device_mode);
+    }
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 initialized (Slave mode, 26MHz expected)");
+    
+    // Reset statistics for this session
+    g_spi1_total_received = 0;
+    g_spi1_last_received = 0;
+    g_spi1_last_print_time = uapi_tcxo_get_us() / 1000;
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 receive session started, statistics reset");
+    
+    // Start statistics timer (print every 1 second)
+    if (g_spi1_stats_timer == NULL) {
+        g_spi1_stats_timer = osTimerNew((osTimerFunc_t)custom_spi1_stats_callback, osTimerPeriodic, NULL, NULL);
+    }
+    if (g_spi1_stats_timer) {
+        osTimerStart(g_spi1_stats_timer, 1000); // 1 second interval
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 statistics timer started");
+    }
+    
+    // Main receive loop
+    uint32_t timeout_count = 0;
+    uint32_t success_count = 0;
+    uint32_t error_count = 0;
+    
+    while (g_spi1_receive_running) {
+        spi_xfer_data_t spi_data = {
+            .rx_buff = spi_buffer,
+            .rx_bytes = sizeof(spi_buffer)  // 期望接收的最大字节数(4K),但实际接收可能小于此值
+        };
+        
+        // Receive data via SPI1 (Slave mode) using DMA
+        // 注意:
+        // 1. uapi_spi_slave_read 返回后,spi_data.rx_bytes 会被更新为实际接收的字节数
+        // 2. 即使缓冲区是4K,如果Master只发送了部分数据,rx_bytes会反映实际接收的字节数
+        // 3. DMA会在以下情况返回:
+        //    - 缓冲区填满(rx_bytes == 4K)
+        //    - Master停止发送,DMA检测到传输结束(rx_bytes < 4K,但传输完成)
+        //    - 超时(50ms内没有数据或传输未完成)
+        ret = uapi_spi_slave_read(bus, &spi_data, SPI1_RECV_TIMEOUT_MS);
+        
+        if (ret == ERRCODE_SUCC) {
+            // 检查实际接收的字节数(函数返回后,rx_bytes 会被更新为实际接收的字节数)
+            if (spi_data.rx_bytes > 0) {
+                // Data received successfully (可能小于4K,这是正常的)
+                // 使用实际接收的字节数,而不是缓冲区大小
+                g_spi1_total_received += spi_data.rx_bytes;
+                success_count++;
+                
+                // 打印接收到的数据信息(每接收一定数据量后打印一次,减少打印)
+                // 注意:即使只接收到部分数据(小于4K),也会正常处理
+                if (g_spi1_total_received % (64 * 1024) < sizeof(spi_buffer)) {
+                    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 RX: Received %u bytes in this call (buffer=%u), Total: %u bytes", 
+                        spi_data.rx_bytes, sizeof(spi_buffer), g_spi1_total_received);
+                }
+            } else {
+                // 返回成功但没有数据(rx_bytes == 0),可能是FIFO为空或传输已完成
+                // 继续等待下一次传输(不打印,避免刷屏)
+                continue;
+            }
+        } else if (ret == ERRCODE_SPI_TIMEOUT) {
+            // Timeout is normal when no data is being sent or Master stops sending
+            // 超时是正常的,说明在50ms内没有接收到数据或传输未完成
+            // 不打印timeout信息,避免刷屏
+            timeout_count++;
+            continue;
+        } else {
+            // Other error, log error information
+            error_count++;
+            const char* error_name = "UNKNOWN";
+            bool should_disable_dma = false;
+            
+            if (ret == 0x80001332) {  // ERRCODE_SPI_MODE_MISMATCH
+                error_name = "SPI_MODE_MISMATCH";
+            } else if (ret == 0x8000133E) {  // ERRCODE_SPI_INVALID_TMODE
+                error_name = "SPI_INVALID_TMODE";
+            } else if (ret == 0x80001334) {  // ERRCODE_SPI_RX_FIFO_FULL
+                error_name = "SPI_RX_FIFO_FULL";
+                // FIFO满可能是配置不匹配或读取不及时,打印一次详细诊断信息
+                if (error_count == 1) {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 RX_FIFO_FULL: Check SPI config match!");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 MCU config: frame_size=7(8bit), mode=0(CPOL=0,CPHA=0), slave");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 Python should: SPI(2, 0, 9) - mode=0, 8bit, master");
+                }
+            } else if (ret == ERRCODE_SPI_TIMEOUT) {
+                error_name = "SPI_TIMEOUT";
+            } else if (ret == ERRCODE_SPI_DMA_CONFIG_ERROR || ret == 0x80001336) {  // ERRCODE_SPI_DMA_CONFIG_ERROR
+                error_name = "SPI_DMA_CONFIG_ERROR";
+                // DMA配置错误,可能是handshaking配置失败或DMA通道不可用
+                // 检查可能的原因:缓冲区对齐、DMA通道、handshaking配置
+                if (error_count == 1) {
+                    uint32_t buffer_addr = (uint32_t)(uintptr_t)spi_buffer;
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 DMA_CONFIG_ERROR: DMA configuration failed!");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 Possible causes: buffer alignment, DMA channel unavailable, or handshaking config");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 Buffer: addr=0x%x (align=%u), size=%u, bus=SPI_BUS_1", 
+                        buffer_addr, buffer_addr % 4, SPI1_RECV_BUFFER_SIZE);
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 DMA config: src_width=0(8bit), dest_width=0(8bit), burst_length=0");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 Expected handshaking: HAL_DMA_HANDSHAKING_SPI_MS0_RX for SPI_BUS_1");
+                }
+            } else if (ret == ERRCODE_SPI_DMA_TRANSFER_ERROR || ret == 0x80001337) {  // ERRCODE_SPI_DMA_TRANSFER_ERROR
+                error_name = "SPI_DMA_TRANSFER_ERROR";
+                // DMA传输错误,但不禁用DMA(因为禁用DMA会导致FIFO一直full)
+                // 注意:DMA传输错误在某些情况下是正常的(如数据少、传输间隙等),不应该重新配置
+                if (error_count == 1) {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 DMA_TRANSFER_ERROR: DMA transfer error (may be normal in some cases, will continue)");
+                }
+            } else if (ret == ERRCODE_INVALID_PARAM || ret == 0x80000001) {  // ERRCODE_INVALID_PARAM
+                error_name = "INVALID_PARAM";
+                // 无效参数错误,可能是DMA配置参数有问题或数据对齐问题
+                // 注意:在某些情况下(如数据量少、传输间隙等),这个错误可能是正常的
+                // 不应该重新配置DMA,应该继续尝试下一次读取
+                if (error_count == 1) {
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 INVALID_PARAM: Invalid parameter (may be normal in some cases, will continue)");
+                    WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, 
+                        "SPI1 Buffer size: %u bytes, rx_bytes=%u", SPI1_RECV_BUFFER_SIZE, spi_data.rx_bytes);
+                }
+            }
+            
+            // 打印错误信息(前5次,之后每100次打印一次,减少刷屏)
+            if (error_count <= 5 || error_count % 100 == 0) {
+                WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, 
+                    "SPI1 read error: 0x%x (%s), rx_bytes=%u (error_count=%u)", 
+                    ret, error_name, spi_data.rx_bytes, error_count);
+            }
+            // 继续尝试接收
+            continue;
+        }
+    }
+    
+    // 打印最终统计
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+        "SPI1 receive loop ended: success_count=%u, timeout_count=%u, error_count=%u", 
+        success_count, timeout_count, error_count);
+    
+    // Stop statistics timer
+    if (g_spi1_stats_timer) {
+        osTimerStop(g_spi1_stats_timer);
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 statistics timer stopped");
+    }
+    
+    // Print final statistics for this session
+    uint32_t session_total = g_spi1_total_received;
+    float total_kb = session_total / 1024.0f;
+    float total_mb = total_kb / 1024.0f;
+    
+    if (total_mb >= 1.0f) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 receive session ended. Total received in this session: %u bytes (%.2f MB, %.2f KB)",
+            session_total, total_mb, total_kb);
+    } else if (total_kb >= 1.0f) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 receive session ended. Total received in this session: %u bytes (%.2f KB)",
+            session_total, total_kb);
+    } else {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 receive session ended. Total received in this session: %u bytes",
+            session_total);
+    }
+    
+    // Deinitialize SPI
+    uapi_spi_deinit(bus);
+}
+
+/**
+ * @brief Start SPI1 client receive data task
+ */
+void custom_spi1_client_receive_start(void)
+{
+    if (g_spi1_receive_running) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "SPI1 receive is already running");
+        return;
+    }
+    
+    g_spi1_receive_running = 1;
+    
+    // Create task for SPI receive
+    app_task_definition_t taskcfg = {0};
+    taskcfg.create_queue_handle = NULL;
+    taskcfg.attr.name = "spi1_client_rx";
+    taskcfg.attr.priority = TASK_PRIORITY_APP;
+    taskcfg.attr.stack_size = 0x2000; // 8KB stack
+    taskcfg.func = custom_spi1_receive_task_func;
+    taskcfg.use_mem = USE_EXTEND_MEM;
+    
+    errcode_t ret = uapi_create_osThread_custom(&taskcfg);
+    if (ret != ERRCODE_SUCC) {
+        WEARABLE_LOGE(WEARABLE_LOG_MODULE_APP, "Failed to create SPI1 receive task: 0x%x", ret);
+        g_spi1_receive_running = 0;
+        return;
+    }
+    
+    WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, "SPI1 client receive started");
+}
+
+/**
+ * @brief Stop SPI1 client receive data task
+ */
+void custom_spi1_client_receive_stop(void)
+{
+    if (!g_spi1_receive_running) {
+        WEARABLE_LOGW(WEARABLE_LOG_MODULE_APP, "SPI1 receive is not running");
+        return;
+    }
+    
+    // Save total received before stopping
+    uint32_t session_total = g_spi1_total_received;
+    
+    g_spi1_receive_running = 0;
+    
+    // Stop and delete statistics timer
+    if (g_spi1_stats_timer) {
+        osTimerStop(g_spi1_stats_timer);
+        osTimerDelete(g_spi1_stats_timer);
+        g_spi1_stats_timer = NULL;
+    }
+    
+    // Print final statistics
+    float total_kb = session_total / 1024.0f;
+    float total_mb = total_kb / 1024.0f;
+    
+    if (total_mb >= 1.0f) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 client receive stopped. Total received in this session: %u bytes (%.2f MB, %.2f KB)",
+            session_total, total_mb, total_kb);
+    } else if (total_kb >= 1.0f) {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 client receive stopped. Total received in this session: %u bytes (%.2f KB)",
+            session_total, total_kb);
+    } else {
+        WEARABLE_LOGI(WEARABLE_LOG_MODULE_APP, 
+            "SPI1 client receive stopped. Total received in this session: %u bytes",
+            session_total);
+    }
+}
+
+/**
+ * @brief Get total bytes received in current SPI1 receive session
+ * @return Total bytes received (0 if not running or no data received)
+ */
+uint32_t custom_spi1_client_get_total_received(void)
+{
+    return g_spi1_total_received;
 }
\ No newline at end of file
index 47b1593..eadcb92 100644 (file)
@@ -37,6 +37,11 @@ void custom_factory_bt_scan_stop(void);
 int custom_play_audio(const char* filePath, float volume);
 int custom_stop_play_audio(void);
 
+/* SPI1 client receive data functions */
+void custom_spi1_client_receive_start(void);
+void custom_spi1_client_receive_stop(void);
+uint32_t custom_spi1_client_get_total_received(void);  // Get total bytes received in current session
+
 /**
  * input: 
  *     func  函数指针