CH32V307以太网(ETH)实战:从初始化到TCP/UDP通信全解析

张开发
2026/4/13 6:42:10 15 分钟阅读

分享文章

CH32V307以太网(ETH)实战:从初始化到TCP/UDP通信全解析
1. CH32V307以太网功能快速入门第一次拿到CH32V307开发板时最让我惊喜的就是它内置的10M以太网PHY。这意味着我们不需要额外购买PHY芯片直接用网线连接电脑就能开始网络通信开发。相比其他需要外接PHY的MCU这个设计对初学者特别友好。这块开发板的以太网功能有两个工作模式内部10M PHY模式最常用外部1G PHY模式需要接PHY芯片我建议新手先从10M模式开始因为硬件连接简单只需要一根普通网线。实际测试中10M的传输速度对于大多数物联网应用完全够用比如传感器数据上传、设备控制等场景。2. 硬件初始化全流程解析2.1 时钟配置要点以太网模块需要精确的50MHz时钟CH32V307通过内部PLL产生这个时钟。初始化时最容易出错的就是时钟配置我建议直接使用官方库函数void ETH_SetMCO(void) { GPIO_InitTypeDef GPIO_InitStructure {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); RCC_MCOConfig(RCC_MCO_SYSCLK); }这个函数会自动配置PA8引脚输出时钟信号。如果遇到连接不稳定的问题可以用示波器检查这个引脚是否有50MHz方波输出。2.2 PHY连接检测初始化完成后一定要检查PHY连接状态uint16_t phyStatus WCHNET_GetPHYStatus(); if(phyStatus PHY_Linked_Status) { printf(网线已连接\n); } else { printf(请检查网线连接\n); }我遇到过好几次因为网线接触不良导致初始化失败的情况这个检查步骤能帮你快速定位问题。3. 网络参数配置实战3.1 MAC地址处理技巧CH32V307的MAC地址可以从芯片唯一ID自动生成这是我常用的生成函数void Get_MAC_Address(uint8_t *mac) { uint32_t uid[3]; uid[0] *(uint32_t*)(0x1FFFF7E8); uid[1] *(uint32_t*)(0x1FFFF7EC); uid[2] *(uint32_t*)(0x1FFFF7F0); mac[0] 0x00; // 本地管理地址 mac[1] 0x80; mac[2] (uid[0] 16) 0xFF; mac[3] (uid[0] 8) 0xFF; mac[4] uid[0] 0xFF; mac[5] (uid[1] 24) 0xFF; }这样生成的MAC地址既唯一又规范避免了地址冲突的问题。3.2 IP地址设置指南静态IP配置是最常用的方式这是我的典型配置uint8_t ip_addr[4] {192,168,1,100}; // 设备IP uint8_t gw_addr[4] {192,168,1,1}; // 网关 uint8_t netmask[4] {255,255,255,0}; // 子网掩码 uint8_t mac_addr[6]; // MAC地址 Get_MAC_Address(mac_addr); WCHNET_Init(ip_addr, gw_addr, netmask, mac_addr);如果网络中有DHCP服务器也可以启用动态IP获取WCHNET_DHCPStart(DHCP_Callback); void DHCP_Callback(uint8_t status) { if(status DHCP_SUCCESS) { printf(DHCP获取成功\n); } else { printf(DHCP失败使用备用IP\n); // 设置备用静态IP } }4. TCP通信实现详解4.1 客户端连接步骤建立TCP客户端需要以下几个关键步骤创建socket配置连接参数实现回调函数这是我优化过的客户端初始化代码void TCP_Client_Init(void) { SOCK_INF socketInfo; memset(socketInfo, 0, sizeof(SOCK_INF)); // 服务器地址和端口 uint8_t server_ip[4] {192,168,1,2}; memcpy(socketInfo.IPAddr, server_ip, 4); socketInfo.DesPort 8080; // 服务器端口 socketInfo.SourPort 1234; // 本地端口 socketInfo.ProtoType PROTO_TYPE_TCP; // 创建socket uint8_t socketId; uint8_t ret WCHNET_SocketCreat(socketId, socketInfo); if(ret WCHNET_ERR_SUCCESS) { printf(Socket创建成功ID: %d\n, socketId); // 设置回调函数 WCHNET_SetSocketOpt(socketId, SO_SET_CONN_CALLBACK, TCP_Connected); WCHNET_SetSocketOpt(socketId, SO_SET_DISCONN_CALLBACK, TCP_Disconnected); WCHNET_SetSocketOpt(socketId, SO_SET_RECV_CALLBACK, TCP_RecvData); // 开始连接 WCHNET_SocketConnect(socketId); } }4.2 数据收发处理数据接收回调函数的实现要点void TCP_RecvData(uint8_t socketId, int index, uint8_t *data, uint32_t len) { // 1. 处理接收到的数据 printf(收到%d字节数据: %s\n, len, data); // 2. 回传数据示例 char reply[] Message received; WCHNET_SocketSend(socketId, (uint8_t*)reply, strlen(reply)); // 3. 重要必须释放接收缓冲区 WCHNET_SocketRecvRelease(socketId, len); }发送数据时要注意分包处理void Send_Large_Data(uint8_t socketId, uint8_t *data, uint32_t totalLen) { uint32_t sentLen 0; uint32_t chunkSize 1024; // 每次发送1KB while(sentLen totalLen) { uint32_t remain totalLen - sentLen; uint32_t toSend (remain chunkSize) ? chunkSize : remain; uint32_t actualSent toSend; uint8_t ret WCHNET_SocketSend(socketId, data sentLen, actualSent); if(ret ! WCHNET_ERR_SUCCESS) { printf(发送失败错误码: %d\n, ret); break; } sentLen actualSent; if(actualSent toSend) { // 网络拥堵稍等再试 delay_ms(10); } } }5. UDP通信实战技巧5.1 UDP服务端配置UDP相比TCP更简单适合对可靠性要求不高的场景void UDP_Server_Init(uint16_t port) { SOCK_INF socketInfo; memset(socketInfo, 0, sizeof(SOCK_INF)); socketInfo.SourPort port; // 监听端口 socketInfo.ProtoType PROTO_TYPE_UDP; uint8_t socketId; if(WCHNET_SocketCreat(socketId, socketInfo) WCHNET_ERR_SUCCESS) { printf(UDP服务端启动成功端口: %d\n, port); WCHNET_SetSocketOpt(socketId, SO_SET_RECV_CALLBACK, UDP_RecvData); } }5.2 UDP数据收发UDP接收回调需要处理对端地址void UDP_RecvData(uint8_t socketId, int index, uint8_t *data, uint32_t len) { // 获取发送方地址 uint8_t remoteIp[4]; uint16_t remotePort; WCHNET_GetSocketRemoteInfo(socketId, remoteIp, remotePort); printf(收到来自 %d.%d.%d.%d:%d 的UDP数据\n, remoteIp[0], remoteIp[1], remoteIp[2], remoteIp[3], remotePort); // 发送回复 char reply[100]; sprintf(reply, 已收到你的%d字节消息, len); WCHNET_SocketUdpSend(socketId, remoteIp, remotePort, (uint8_t*)reply, strlen(reply)); // 释放缓冲区 WCHNET_SocketRecvRelease(socketId, len); }UDP发送可以直接指定目标地址void UDP_Send_Message(uint8_t socketId, uint8_t *ip, uint16_t port, uint8_t *data, uint32_t len) { uint32_t sent len; uint8_t ret WCHNET_SocketUdpSend(socketId, ip, port, data, sent); if(ret ! WCHNET_ERR_SUCCESS || sent ! len) { printf(UDP发送不完全已发送: %d/%d\n, sent, len); } }6. 常见问题排查指南6.1 连接失败排查遇到连接问题时可以按照以下步骤检查用ping命令测试网络连通性检查网线指示灯是否正常确认IP地址没有冲突查看PHY连接状态寄存器检查防火墙设置是否阻止了端口6.2 性能优化建议增大缓冲区在ETH_LibInit中调整HeapSize建议不小于8KB调整MSS值根据网络状况设置合适的TCP MSS值启用DMA优化确保使用了链式DMA传输模式合理设置超时调整TCP重传时间和次数这是我常用的优化配置cfg.TCPMss 1460; // 最大分段大小 cfg.HeapSize 8192; // 8KB内存池 cfg.ARPTableNum 16; // ARP缓存条目7. 进阶开发技巧7.1 多连接处理CH32V307支持多个socket同时工作这是管理多个连接的技巧#define MAX_CLIENTS 4 struct { uint8_t socketId; uint8_t isUsed; uint32_t lastActive; } clientPool[MAX_CLIENTS]; uint8_t Get_Free_Client_Slot(void) { for(int i0; iMAX_CLIENTS; i) { if(!clientPool[i].isUsed) { clientPool[i].isUsed 1; clientPool[i].lastActive get_tick_count(); return i; } } return 0xFF; // 无可用槽位 } void Handle_Multiple_Connections(void) { uint8_t socketEvent; for(int i0; iWCHNET_MAX_SOCKET_NUM; i) { socketEvent WCHNET_GetSocketInt(i); if(socketEvent) { // 查找对应的客户端槽位 int clientIdx Find_Client_By_SocketId(i); if(clientIdx 0) { clientPool[clientIdx].lastActive get_tick_count(); Process_Client_Data(clientIdx, socketEvent); } } } }7.2 保活机制实现TCP保活功能对维持长连接非常重要void Enable_KeepAlive(uint8_t socketId) { _KEEP_CFG_VALUE cfg; cfg.KLIdle 30000; // 30秒空闲后开始探测 cfg.KLIntvl 10000; // 每10秒探测一次 cfg.KLCount 5; // 最多探测5次 WCHNET_ConfigKeepLive(cfg); WCHNET_SocketSetKeepLive(socketId, 1); }在实际项目中我发现合理设置保活参数可以显著提高连接稳定性特别是在移动网络环境下。

更多文章