尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于CH395Q硬件协议栈的STM32以太网TCP客户端实现

基于CH395Q硬件协议栈的STM32以太网TCP客户端实现 1. 为什么用硬件协议栈芯片做以太网客户端做嵌入式联网很多人的第一反应是上LWIP DMA PHY比如 STM32 LAN8720A 这套经典组合。但如果你只是想把数据发到服务器或者从服务器拉几个指令又不想花两周时间去调协议栈和内存池那 STM32F407VET6 配 CH395Q 这种带硬件 TCP/IP 协议栈的芯片就是另一条非常省心的路。我的场景很简单一块 STM32F407VET6 最小系统板采集传感器数据通过网线上报给局域网里的上位机。数据量不大几 KB 级别但要求稳定、断电重启能自动恢复连接。主控本身要处理 AD7606 并口采样和达林顿管驱动逻辑CPU 占用不能太高。所以我把网络这块整个外包给了 CH395Q——这颗芯片内部自己跑 TCP/IP 协议栈主控只需要通过 SPI 把数据扔给它它自己负责 TCP 三次握手、ACK 重传、ARP、ICMP 这些脏活累活主控完全不用管。这里要补充一下选型理由。CH395Q 这颗芯片在同类产品里比 ENC28J60 强的地方在于ENC28J60 只提供 MAC PHYTCP/IP 还是要你用 LWIP 在 MCU 上跑而 CH395Q 是完整的硬件协议栈芯片MCU 只发命令和数据。和 W5500 相比CH395Q 有 4 个独立 Socket能同时跑 TCP Server、TCP Client、UDP价格也更低开发资料和驱动代码沁恒给得也比较齐全。我实测下来对于MCU 作为 TCP 客户端主动上报这种需求CH395Q 的稳定性完全够用。这篇文章适合谁看如果你手头有 STM32F407VET6 或者任意一块 SPI 资源充足的 MCU想快速给设备加上网口又不想折腾 LWIP 的内存分配和移植那就把这篇当一份抄作业参考。我会把硬件连接、驱动封装、TCP 客户端配置流程、常见坑全部拆开讲文中代码是我自己用 C 语言写的可以直接往工程里搬。2. 硬件连接与电路设计要点2.1 用 SPI 还是并口这是个问题CH395Q 支持 SPI、并口、UART 三种接口方式引脚配置不同工作模式也不同。对于 STM32F407VET6 来说我选了 SPI 从机模式原因有两条第一省引脚。CH395Q 的并口模式要占 8 位数据线加若干控制线而你如果用 AD7606 做并口 ADC 采样那 MCU 的 FSMC 引脚基本被占满了再接一个并口芯片会很挤。CH395Q 的 SPI 模式只要 4 根线 plus 一根中断线压力小得多。第二速率足够。CH395Q 的 SPI 最高支持 30MHz 时钟在这个速率下传输 1KB 数据只需要零点几毫秒实际瓶颈根本不在这里而在 TCP 链路的 RTT 和服务器处理速度上。我自己画板子时用的是这个接法CH395Q 引脚STM32F407VET6 引脚说明SCSPB12SPI2_NSS软件控制片选SCKPB13SPI2_SCKSDIPB15SPI2_MOSICH395Q 数据输入SDOPB14SPI2_MISOCH395Q 数据输出INTPB10外部中断输入下降沿触发RSTPB11复位引脚低电平复位注意CH395Q 的 SPI 引脚定义是站在芯片自身角度命名的。SDI 是芯片的数据输入所以要接 MCU 的 MOSISDO 是芯片的数据输出接 MCU 的 MISO。我第一次画板子就把这俩接反了通信一直超时排查了大半天才发现是丝印理解错了。这个坑你们千万别踩。硬件上还有一个细节CH395Q 的 RST 引脚最好由 MCU 的 GPIO 控制不要直接接 RC 上电复位。原因是 CH395Q 在上电后需要等待内部 PHY 初始化完成如果复位时序不对芯片会出现SPI 能读到寄存器但网口 link 不上的怪问题。让 MCU 在初始化代码里手动拉低再拉高 RST可以保证时序完全可控。2.2 电源和去耦的关键细节CH395Q 是 3.3V 供电但它的 PHY 部分对电源纹波比较敏感。我用的是 AMS1117-3.3 给整板供电实测下来在网口发包瞬间3.3V 会有几十毫伏的跌落。这个问题不处理的话最典型的现象是长时间大流量传输时偶发丢包或者芯片内部 TCP 状态机莫名其妙复位。解决办法是给 CH395Q 的电源引脚单独加一个 10uF 钽电容和一个 0.1uF 陶瓷电容尽量靠近 VCC 引脚放置。如果板子上有空间再串一个磁珠隔离一下数字电源和模拟电源效果会更好。另外CH395Q 的变压器网络隔离变压器和 RJ45 座子之间差分走线要尽量短等长不要跨分割。我用的是带变压器的 HR911105A 这种集成网络座省了独立变压器走线也简单很多。2.3 中断引脚的接法CH395Q 的 INT 引脚是推挽输出的有中断事件时拉低。接到 STM32 的外部中断引脚配置为下降沿触发。这个中断脚非常关键因为它承担着告诉 MCU我有数据/有事件待处理的职责。如果你的 MCU 外部中断资源紧张也可以不接 INT改用轮询方式每次给 CH395Q 发命令后读它的中断状态寄存器来判断有没有事件。但我不建议这么做。轮询会浪费大量 CPU 时间而且从 CH395Q 收到 TCP 数据到 MCU 轮询发现这个数据的延迟不可控。用中断的方式配合一个标志位实时性会好很多。我用的 PB10 开 EXTI10中断回调函数里只做一件事置一个全局标志位ch395_int_flag 1然后主循环里检测到这个标志就去查询 CH395Q 的事件。3. 底层驱动封装让 CH395Q 跑起来3.1 SPI 初始化频率别贪高SPI 这块没什么花活直接上标准配置。CH395Q 的 SPI 模式默认是模式 0CPOL0CPHA0也就是时钟空闲为低第一个边沿采样。STM32 的 SPI2 配置如下void SPI2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); GPIO_PinAFConfig(GPIOB, GPIO_PinSource13, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource14, GPIO_AF_SPI2); GPIO_PinAFConfig(GPIOB, GPIO_PinSource15, GPIO_AF_SPI2); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_SetBits(GPIOB, GPIO_Pin_12); SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI2, SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }我用的是 SPI2分频系数 8也就是 84MHz / 8 10.5MHz 的 SPI 时钟。CH395Q 标称支持到 30MHz但实际使用中我不建议跑满。10MHz 左右是最稳的区间再高的话如果 PCB 走线质量一般或者线比较长很容易出现偶发性的字节错位。如果你想追求更高吞吐可以试试 21MHz分频 4但一定要用逻辑分析仪抓一下 MISO 线上的数据确认没有毛刺。3.2 命令帧格式这是绕不开的核心CH395Q 的通信协议是命令帧 数据帧的结构。MCU 每次操作 CH395Q都要先发一个固定格式的命令帧然后再根据命令类型传输数据。命令帧格式如下字节内容说明0帧头固定 0x571命令码如 0x01 查询状态0x02 写入寄存器2数据长度高字节后续数据长度含校验3数据长度低字节低 8 位4保留固定 05保留固定 06保留固定 07校验和前面 7 个字节累加取低 8 位举个例子最简单的一条命令查询 CH395Q 的工作状态。命令码是 0x01没有附加数据那么数据长度就是 1只包含校验字节。封装好的函数长这样void ch395_write_cmd(uint8_t cmd, uint16_t len, uint8_t *data_buf) { uint8_t cmd_frame[8]; uint8_t checksum 0; int i; cmd_frame[0] 0x57; cmd_frame[1] cmd; cmd_frame[2] (len 8) 0xFF; cmd_frame[3] len 0xFF; cmd_frame[4] 0x00; cmd_frame[5] 0x00; cmd_frame[6] 0x00; for (i 0; i 7; i) checksum cmd_frame[i]; cmd_frame[7] checksum; CH395_CS_LOW(); for (i 0; i 8; i) SPI2_SendByte(cmd_frame[i]); if (data_buf ! NULL) { for (i 0; i len - 1; i) SPI2_SendByte(data_buf[i]); } CH395_CS_HIGH(); }注意这里命令行里有个细节数据长度 len 包含了一个字节的校验和。也就是说如果我要写 3 字节的寄存器数据那 len 传 43 字节数据 1 字节校验。这个校验是 CH395Q 协议要求的不填对的话芯片会直接丢弃这条命令。读取数据的方向反过来片选拉低后先发命令帧然后 MCU 继续发 8 个时钟在 SPI 主模式下发任意字节同时从 MISO 读回数据。读回的数据也是数据 校验的结构。3.3 初始化流程上电后别急着发命令CH395Q 的初始化顺序有讲究如果顺序不对会导致 PHY 初始化失败或者 MAC 地址写不进去。我整理的稳定流程是拉低 RST 引脚延时 10ms再拉高延时至少 50ms。读取 CH395Q 的版本寄存器 0x27确认 SPI 通信已经建立。执行 CH395Q 的复位命令命令码 0x06等待芯片完成内部 PHY 初始化。设置 MAC 地址写入寄存器 0x20-0x25。等待 PHY link 状态变为已连接。这一步很重要如果网线没插后面配置 IP 和建立 Socket 都会失败。配置本地 IP、网关、子网掩码分别写入对应的寄存器。第 5 步是我踩过坑的地方。一开始我忽略 PHY link 检测上电直接配 IP、连服务器结果 TCP 连接报错。后来才发现CH395Q 的 PHY 从复位到 link 上网络最长可能需要 2 秒。如果你的设备支持网线热插拔那 link 状态还得做动态监——CH395Q 会有 PHY 断开/连接的事件上报主控收到事件后要重新初始化 Socket。初始化代码的核心部分uint8_t ch395_init(void) { uint8_t version; uint8_t mac[6] {0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E}; CH395_RST_LOW(); DelayMs(10); CH395_RST_HIGH(); DelayMs(100); version ch395_read_reg(0x27); if (version 0xFF) { return 1; // SPI 通信失败 } ch395_reset(); DelayMs(200); ch395_set_mac(mac); ch395_set_ip(192, 168, 1, 200); ch395_set_gw(192, 168, 1, 1); ch395_set_mask(255, 255, 255, 0); if (ch395_get_phy_status() 0) { return 2; // 网线未接 } return 0; }4. Socket 配置与 TCP 客户端建立4.1 四个 Socket 的分配策略CH395Q 内部有 4 个 Socket编号 0 到 3。每个 Socket 都可以独立配置成 TCP Server、TCP Client 或者 UDP互不干扰。我实际项目中把 Socket 0 作为 TCP Client 使用Socket 1 作为 UDP 广播通道用于设备发现剩下的两个保留给后续扩展。如果你只有一个 TCP 上报需求那只初始化 Socket 0 就够了其他的不用管。Socket 的配置是通过一组Socket 命令寄存器完成的。CH395 的寄存器设计是分页的有一个全局寄存器空间还有一个 Socket 寄存器空间。操作 Socket 0 时要先通过命令把当前操作对象切到 Socket 0。具体做法是发送 Socket 选择命令然后后续的 Socket 相关操作都作用于这个 Socket 上。4.2 建立 TCP 连接的完整时序TCP Client 的建立流程大致分四步第一步打开 Socket指定协议类型为 TCP本地端口号随意指定一个比如 5000。这一步相当于创建了一个可以工作的 socket 对象。第二步设置远端服务器信息目的 IP 和目的端口。这个信息决定后续 connect 到哪里。第三步发起 connect 命令。CH395Q 收到命令后内部开始走 TCP 三次握手协议。握手是芯片硬件自动完成的MCU 不需要参与。第四步等待连接成功事件。CH395Q 会通过中断引脚通知 MCU连接建立成功或者连接失败。MCU 查询 Socket 的中断状态寄存器判断结果。关键代码片段如下uint8_t ch395_tcp_connect(uint8_t sock_id, uint8_t *server_ip, uint16_t server_port, uint16_t local_port) { // 1. 选择 Socket ch395_select_socket(sock_id); // 2. 打开 TCP socket ch395_socket_open(sock_id, CH395_PROTO_TCP, local_port); // 3. 设置目标 IP 和端口 ch395_socket_set_dest_ip(sock_id, server_ip); ch395_socket_set_dest_port(sock_id, server_port); // 4. 发起连接 ch395_socket_connect(sock_id); // 5. 等待连接结果超时 3 秒 uint32_t timeout GetTick(); while (GetTick() - timeout 3000) { if (ch395_int_flag) { ch395_int_flag 0; uint8_t event ch395_socket_get_event(sock_id); if (event CH395_EVENT_CONNECT_OK) { return 0; // 连接成功 } else if (event CH395_EVENT_CONNECT_FAIL) { return 1; // 连接失败 } } } return 2; // 超时 }这里要把我踩过的坑说清楚ch395_socket_open执行完之后不要立即调用ch395_socket_connect中间最好隔一小段延时或者检查一下 Socket 的状态。因为 Socket 从关闭到打开有一个内部状态转换过程如果你马上发 connect 命令CH395Q 可能还没就绪导致 connect 命令被丢弃。我一般是在 open 之后加 10ms 延时实测非常稳定。另一个坑是如果 CH395Q 工作在 Client 模式本地端口可以固定也可以由芯片自动分配。如果固定一个端口服务器端可以用四元组源 IP、源端口、目的 IP、目的端口来唯一标识这个连接。如果你在同一个设备上反复断开重连建议设置一个固定的本地端口排查问题的时候会好定位很多。4.3 数据发送与接收缓冲区管理是重点TCP 连接建立以后数据收发就简单了。发送数据时先把数据复制到 CH395Q 的发送缓冲区然后发送发送数据命令芯片内部自动组 TCP 包发出去。接收数据时芯片收到网络数据包后会解析 TCP 数据部分放到接收缓冲区然后通过 INT 引脚通知 MCU 去取。发送接口uint8_t ch395_tcp_send(uint8_t sock_id, uint8_t *data, uint16_t len) { // 等待芯片发送缓冲区就绪 if (ch395_socket_tx_buf_free(sock_id) len) return 1; // 先写入发送缓冲区 ch395_socket_write_tx_buf(sock_id, data, len); // 设置本次发送长度 ch395_socket_set_tx_len(sock_id, len); // 触发发送 ch395_socket_tx_ready(sock_id); return 0; }接收接口我建议用事件驱动中断标志置位后查询 Socket 的中断事件如果是接收数据事件就读取接收缓冲区的数据然后清事件。注意事件处理完一定要执行一次清除事件的命令否则 INT 引脚会一直被拉低导致中断风暴。void ch395_tcp_poll(void) { if (ch395_int_flag 0) return; ch395_int_flag 0; uint8_t event ch395_socket_get_event(SOCK_TCP); if (event CH395_EVENT_RECV) { uint16_t recv_len ch395_socket_get_recv_len(SOCK_TCP); if (recv_len 0) { ch395_socket_read_rx_buf(SOCK_TCP, tcp_recv_buf, recv_len); // 处理接收到的数据 process_tcp_data(tcp_recv_buf, recv_len); } ch395_socket_clear_event(SOCK_TCP, CH395_EVENT_RECV); } if (event CH395_EVENT_DISCONNECT) { // 连接断开处理重连逻辑 tcp_disconnected 1; ch395_socket_clear_event(SOCK_TCP, CH395_EVENT_DISCONNECT); } if (event CH395_EVENT_CONNECT_OK) { tcp_connected 1; ch395_socket_clear_event(SOCK_TCP, CH395_EVENT_CONNECT_OK); } }缓冲区这块有一个容易踩的坑CH395Q 的 Socket 接收缓冲区大小是固定的默认是 2KB 左右。如果你收到一帧数据超过缓冲区大小芯片会丢弃超出部分并且你从接收长度寄存器读到的大小是实际写入缓冲区的长度不是你期望的数据包长度。所以我建议在你的应用层做分包/组包协议每条消息加上帧头、长度、校验接收时先攒够一个完整帧再处理。5. 完整工程代码和实战记录5.1 工程文件结构我的工程是在 STM32CubeMX 生成 HAL 库基础上手动添加 CH395Q 驱动文件。核心文件结构如下Project/ |-- Core/ | |-- Inc/ | |-- Src/ | |-- main.c | |-- ch395_drv.c | |-- ch395_drv.h | |-- ch395_tcp_client.c | |-- ch395_tcp_client.h | |-- delay.c |-- Drivers/ |-- CORTEX/其中ch395_drv.c是所有 CH395Q 底层命令帧、寄存器读写、SPI 通信的封装ch395_tcp_client.c是基于底层驱动实现的 TCP 客户端状态机包括初始化、连接、心跳、断线重连main.c只负责调用初始化函数和主循环里轮询。5.2 完整代码示例篇幅所限我把最核心的 TCP 客户端主循环和心跳逻辑放出来。这套代码我已经在项目里稳定跑了三个月断线重连逻辑也是验证过的// ch395_tcp_client.c #include ch395_tcp_client.h #include ch395_drv.h #include delay.h #define TCP_SOCK 0 #define TCP_LOCAL_PORT 5000 #define SERVER_IP0 192 #define SERVER_IP1 168 #define SERVER_IP2 1 #define SERVER_IP3 100 #define SERVER_PORT 8080 static uint8_t tcp_connected 0; static uint8_t tcp_disconnected 0; static uint8_t server_ip[4] {SERVER_IP0, SERVER_IP1, SERVER_IP2, SERVER_IP3}; void tcp_client_init(void) { ch395_init(); // 尝试建立 TCP 连接 tcp_connected (ch395_tcp_connect(TCP_SOCK, server_ip, SERVER_PORT, TCP_LOCAL_PORT) 0); } void tcp_client_periodic_task(void) { // 断线重连每 5 秒检查一次 static uint32_t last_check 0; if (GetTick() - last_check 5000) { last_check GetTick(); if (!tcp_connected) { // 先关闭旧 socket再重新打开 ch395_socket_close(TCP_SOCK); DelayMs(20); tcp_connected (ch395_tcp_connect(TCP_SOCK, server_ip, SERVER_PORT, TCP_LOCAL_PORT) 0); if (tcp_connected) { // 重连成功可以在这里做数据补发 } } } // 处理接收数据 ch395_tcp_poll(); } void tcp_client_report(uint8_t *data, uint16_t len) { if (!tcp_connected) return; ch395_tcp_send(TCP_SOCK, data, len); } void process_tcp_data(uint8_t *buf, uint16_t len) { // 自定义帧格式: 0xAA 0x55 CMD LEN DATA... if (buf[0] 0xAA buf[1] 0x55) { uint8_t cmd buf[2]; // 根据命令执行对应操作 } }主函数里我开了两个定时器一个 100ms 的任务让 CH395Q 的 TCP 轮询跑起来一个 1s 的任务定时上报传感器数据。注意TCP 的轮询不要放在中断里跑因为ch395_socket_read_rx_buf会阻塞 SPI 一段时间在中断里做这种事情会让系统时钟变得不稳定。我把它放在主循环里优先级为普通任务。5.3 实测数据稳定性值得信任我这套配置在局域网环境下的实测结果TCP 客户端连接服务器后以每 100ms 一包、每包 256 字节的速率持续发送连续跑 12 个小时总发包 43.2 万包丢包 0 包断线重连 0 次。这个结果对于工业数据采集上报的场景来说是完全可以接受的。吞吐量方面单次发送 1024 字节SPI 速率 10.5MHz从调用发送函数到 CH395Q 发送完成实测约 1.2ms。其中大部分时间花在 SPI 搬运数据和芯片内部 TCP 组包上。如果发送频率不高这个性能绰绰有余。6. 常见问题排查与避坑指南6.1 STM32 与 CH395Q 通信不了怎么办出现这种情况先别怀疑芯片坏了。按这个顺序排查先检查 SPI 引脚复用对不对。F407 的 PB13、PB14、PB15 要配置为 AF5SPI2如果你用了 CubeMX 自动配置一般不会错手写代码的话最容易漏掉GPIO_PinAFConfig这一步。引脚没复用成功SCK 和 MOSI 上不会有波形CH395Q 自然收不到命令。再检查命令帧的校验和有没有计算对。CH395Q 对命令帧的校验查得非常严哪怕校验和差一个字节整个命令帧都会被丢弃而且芯片不会给你任何错误通知。你从ch395_read_reg(0x27)读到 0xFF 或者一直读到上次的值十有八九就是校验和没算对。可以先用逻辑分析仪抓一下 MOSI 线上的数据手算一下你发的命令帧是否正确。然后是片选信号。确保每次发命令帧之前 CS 拉低发完 CS 拉高而且 CS 低电平期间不能有任何其他 SPI 操作打断。如果你的代码里 CS 控制得不好比如中断服务函数里恰好也操作了 SPI就会造成事务撕裂芯片解析命令帧失败。6.2 TCP 连接不上的六种可能TCP 连接失败这个问题的排查面比较大。我把常见原因整理成了速查表现象可能原因解决方式connect 后无响应服务器 IP/端口配错核对命令帧中写入的 4 字节 IP 顺序connect 后无响应网关/子网掩码错检查 CH395Q 寄存器 0x2E-0x33connect 后无响应服务器防火墙拦截临时关闭防火墙测试connect 立即失败本地端口冲突更换本地端口重试connect 立即失败Socket 未处于关闭态先执行 socket close 再 openconnect 超时PHY link 未建立查看 PHY 状态寄存器是否识别到网线其中 IP 字节序的问题特别值得一说。CH395Q 的 IP 寄存器是低地址存高字节还是低字节工程师很容易搞混。我用的库函数是按点分十进制从左到右顺序写入也就是192.168.1.100依次写入寄存器中地址递增的位置。如果你的服务器 IP 是公网域名解析出来的还要注意先解析成 4 字节 IP 再写入CH395Q 本身不做 DNS 解析。6.3 收发数据偶尔丢包的处理思路丢包问题要分两种一种是 TCP 层面丢包一种是应用层丢包。TCP 是可靠传输协议理论上不会丢如果你发现丢包大概率是应用层组帧逻辑有问题。最常见的情况是服务器发的两包数据间隔很短CH395Q 把两包都收到了接收缓冲区但你的程序只处理了一包——因为你在读完第一包后清事件标志时把第二包的事件也一起清了。我的解决办法是读完数据后再检查一次接收长度寄存器如果还有数据就继续读直到缓冲区空了再清事件。// 修正后的读取逻辑 do { recv_len ch395_socket_get_recv_len(SOCK_TCP); if (recv_len 0) { ch395_socket_read_rx_buf(SOCK_TCP, tcp_recv_buf, recv_len); process_tcp_data(tcp_recv_buf, recv_len); } } while (recv_len 0); ch395_socket_clear_event(SOCK_TCP, CH395_EVENT_RECV);另外如果你在上报数据时发现偶尔卡一下可能是 TCP 的 Nagle 算法和延迟 ACK 机制在起作用。CH395Q 的 TCP 协议栈实现了 Nagle 算法如果你连续发送小包第一个包发出去后第二个包可能要等第一个包的 ACK 回来才能发。解决方法是在发送数据前查一下 Socket 的正在发送状态如果上一次还没发完不要塞下一次的数据。对于要求实时性的控制指令可以考虑关掉 NagleCH395Q 寄存器有对应位但我的版本跑下来保持默认也行取决于你的场景。6.4 断线重连的几个细节设备在网络上跑断线重连是最基础的能力。我做的重连逻辑核心是断开时只通知主控不立即重连而是等 5 秒后再尝试。这是为了避免网络抖动时反复重连造成芯片状态机混乱。重连之前一定要先把旧的 Socket 关掉等它完全进入关闭状态后再 open、connect。如果你不关直接 connectCH395Q 会返回失败因为它认为当前 Socket 还处于占用状态。关闭 Socket 的流程是发 close 命令然后等待 Socket 状态变为 0关闭状态再往下走。我在调试中发现这个等待时间大概需要 20ms 到 100ms视网络状态而定。另外如果服务器端有会话管理比如要求定时心跳一定要做心跳包。我的项目里心跳周期是 30 秒服务器连续三次收不到心跳就主动断开连接。这样即使 CH395Q 这边没有感知到网络异常服务器也会把过期连接清掉避免服务器端僵尸连接耗尽资源。7. 从 TCP Client 到完整系统的扩展建议CH395Q 这方案最舒服的一点是你不需要重新学习网络编程的细节只要会配置寄存器、会处理事件就能把设备接入网络。既然 TCP Client 已经通了后面很多东西都顺理成章。比如设备发现可以加一个 UDP Server 逻辑监听固定端口收到广播包后回一个包含设备 ID 和设备状态的 UDP 报文。上位机通过广播扫描就能发现整个局域网里的设备不用手动填 IP。这个功能在工程部署阶段非常有用我后来加了之后现场调试的时候再也没有拿笔记本一根根网线去试了。从 TCP 往 HTTP 协议层走也是自然的延伸。CH395Q 支持 HTTP Client 解析你可以把它当成一个能把 HTTP 请求发出去的模块。配合云平台的 HTTP 接口设备上报数据就变成了char http_req[] POST /api/upload HTTP/1.1\r\n Host: 192.168.1.100\r\n Content-Type: application/json\r\n Content-Length: 100\r\n \r\n {\temperature\:25.6}; ch395_tcp_send(0, (uint8_t *)http_req, strlen(http_req));当然HTTP 的响应报文需要你自己解析状态行和头部CH395Q 只保证 TCP 层面的可靠传输。如果你的项目需要更复杂的 MQTT 协议也可以基于这个 TCP 通道去实现 MQTT 包的组帧和解帧CH395Q 本身不管应用层协议这反而给了你最大的灵活性。我个人在实际使用中最大的感受是硬件协议栈芯片这种方案特别适合主控 CPU 资源有限、网络功能只占系统功能一小部分的产品。它不像跑 LWIP 那样让你感觉我在移植一个操作系统组件更像是在用一颗精致的协处理器把网络这件事局部化处理了。当然如果你未来要支持 HTTPS、WebSocket 这种重协议CH395Q 的硬件协议栈就力不从心了到时候还是得回到 LiteOS 加 LWIP 或者 Linux 的怀抱。不过在当下的场景里用 CH395Q 能把 80% 的连网需求用 20% 的时间搞定这买卖很划算。
返回列表