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
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 服务器
port: 监听端口,默认 5002
timeout: 下载超时时间(秒),默认 30 秒
sizeof: 读取缓冲区的数据块大小,默认 255 字节
+ spi_obj: SPI 对象,用于发送数据。如果为 None,则自动创建 SPI(1, 0, 1)
"""
self.host = host
self.port = port
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"""
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:
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:
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)
# 显示进度(每发送一定数据量后打印一次)
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):
except:
pass
self.sock = None
+ # 关闭SPI对象
+ self.close()
clog.info("HTTP file download TCP server stopped")
def is_running(self):
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: 使用类下载并保存文件
#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)
{
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