沁恒以太网库使用指南
1.以太网初始化
WCHNET_Init 为库初始化函数。关于 WCHNET_Init 函数使用方法请参考 3.2。对 WCHNET 初始
化后,应用层需要开启以太网中断,并在相应的中断函数中调用中断服务函数
WCHNET_ETHIsr;另外库函数需要外部提供时钟,用于给时间相关的任务提供时钟源,例如
刷新 ARP 列表,TCP 超时等,通过调用 WCHNET_TimeIsr 函数更新时间,该函数传递的参数
为最近一次调用的时间差值,单位毫秒。
综上,在调用 WCHNET_Init 进行库初始化后,应用层需要调用 ETH_Init 初始化以太网物
理层。
2.以太网配置
通过 WCHNET_ConfigLIB 将配置信息传递库,详细配置信息请参考 2.1,必须在WCHNET_Init 之前调用。
(1) WCHNET_NUM_IPRAW
用于配置 IPRAW(IP 原始套接字)连接的个数,最小值为 1。用于 IPRAW 通讯。
(2) WCHNET_NUM_UDP
用于配置 UDP 连接的个数,最小值为 1。用于 UDP 通讯。
(3) WCHNET_NUM_TCP
用于配置 TCP 连接的个数,最小值为 1。用于 TCP 通讯。
(4) WCHNET_NUM_TCP_LISTEN
用于配置 TCP 监听的个数,最小值为 1。TCP 监听的 socket 仅仅用于监听,一旦监听到
TCP 连接,会立即分配一个 TCP 连接,占用 WCHNET_NUM_TCP 的个数。
(5) WCHNET_MAX_SOCKET_NUM
用于 配置 socket 个数 ,等于 WCHNET_NUM_IPRAW、WCHNET_NUM_UDP、WCHNET_NUM_TCP、WCHNET_NUM_TCP_LISTEN 之和。
(6) WCHNET_NUM_PBUF
用于配置 PBUF 结构的个数 ,PBUF 结构主要用于管理内存分配,包括申请 UDP,TCP,IPRAW
内存以及收发内存,如果应用程序需要较多的 socket 连接和有大批量的数据收发, 此值要
设置大些。
(7) WCHNET_NUM_POOL_BUF
POOL BUF 的个数 ,POOL BUF 主要是用来接收数据时使用, 如果接收大批数据,此值要设
置大些。
(8) WCHNET_NUM_TCP_SEG
TCP Segments 的个数 ,WCHNET 每次发送一个 TCP 数据包时,都要先申请一个 TCP Segments。
如果 TCP 连接数较多时,收发数据量较大时,此值应设置大些。 例如当前 TCP 连接有 4 个,
每个接收缓冲区设置为 2 个 TCP_MSS,假设每次收到一包数据后都会进行一次 ACK,则
WCHNET_NUM_TCP_SEG 应该配置大于(4*2),这只是最严重的情况,实际上每次 ACK(或者发送的数据)被收到后,都会释放此数据的 Segments。
(9) WCHNET_NUM_IP_REASSDATA
IP 分片的包个数 。每个包的大小为 WCHNET_SIZE_POOL_BUF,此值最小可以设置为 1。
(10) WCHNET_TCP_MSS
TCP 最大报文段的长度 ,此值最大为 1460,最小为 60。综合传输和资源考虑,建议此值
不要小于 536 字节 。
(11) WCHNET_MEM_HEAP_SIZE
堆分配内存大小 ,主要用于一些不定长度的内存分配,例如发送数据。如果 TCP 有大批
量数据收发,则此值应该设置大些。若发送数据时希望使用应用层内存,参考本节
CFG0_TCP_SEND_COPY。
(12) WCHNET_NUM_ARP_TABLE
ARP 缓存,存放 IP 和 MAC ,此值最小可以设置为 1,最大为 0x7F。如果 WCHNET 需要和 4
台 PC 进行网络通讯,其中两台会大批量收发数据,则建议设置为 4。如果小于 2,则会严重
影响通讯效率。
(13) CFG0_TCP_SEND_COPY
该配置仅在 TCP 通讯时有效。
CFG0_TCP_SEND_COPY 为 1 时表示开启发送复制功能,WCHNET 将应用层数据复制到内部的
堆内存中,然后进行打包发送。
CFG0_TCP_SEND_COPY 为 0 时表示关闭发送复制功能,那么库就不会申请新的缓存空间转
存数据,而是使用作为函数参数的数据指针直接传入库中用于发送数据,因此这个数据指针
指向的数据在数据包被应答之前,不能发生变化。
(14) CFG0_TCP_RECV_COPY
调试使用,默认为开启,此值为 1 速度会加快。
(15) CFG0_TCP_OLD_EDLETE
CFG0_TCP_OLD_EDLETE 为 1 时,WCHNET 申请不到 TCP 连接时, 会删除较老的 TCP 连接,默认关闭此功能。
TCP
TCP客户端
TCP服务器端
基于CH32V307
单片机作为客户端,上位机作为服务器端,服务器上传数据到客户端,客户端将接收到的数据透传回服务器。
UDP
实践
配置网络
设置以太网为静态IP:设置如下

v307IP设置如下:

打开网络调试助手
本机地址就是选择我们设置的IP地址,端口选择307配置的目标端口。远程主机选择,307端口。我们选择发送,即可将消息发送到307,之后307将消息再发回来。可以看到消息是可以正常接收发送。当我们将电脑本地端口改为1001时,本地发送出去的消息,307会上来,本地是无法接收到。
将307目标端口改为1001时,本地又可以正常接收了。
当把307的端口和IP改了,调试界面下的远程主机会改变吗?
当我们改变IP地址之后,网络就无法正常通讯
当我们将远程主机修改之后又可以正常通讯了
修改原始代码代码解析
UDP客户端
客户端需要配置好固定的服务器IP地址,和服务器端口号

int main(void) { u8 i; SystemCoreClockUpdate(); Delay_Init(); USART_Printf_Init(115200); //USART initialize printf("UDPClient Test\r\n"); printf("SystemClk:%d\r\n", SystemCoreClock); printf("ChipID:%08x\r\n", DBGMCU_GetCHIPID()); printf("net version:%x\n", WCHNET_GetVer()); if (WCHNET_LIB_VER != WCHNET_GetVer()) { printf("version error.\n"); } WCHNET_GetMacAddr(MACAddr); //get the chip MAC address printf("mac addr:"); for(i = 0; i < 6; i++) printf("%x ", MACAddr[i]); printf("\n"); TIM2_Init(); i = ETH_LibInit(IPAddr, GWIPAddr, IPMask, MACAddr); //Ethernet library initialize mStopIfError(i);//检查 if (i == WCHNET_ERR_SUCCESS) printf("WCHNET_LibInit Success\r\n"); for (i = 0; i < WCHNET_MAX_SOCKET_NUM; i++) WCHNET_CreateUdpSocket(); //Create UDP Socket while(1) { /*Ethernet library main task function, * which needs to be called cyclically*/ WCHNET_MainTask(); /*Query the Ethernet global interrupt, * if there is an interrupt, call the global interrupt handler*/ if(WCHNET_QueryGlobalInt()) { WCHNET_HandleGlobalInt(); } } }1.首先是获取以太网库版本
WCHNET_GetVer(void);
2.获取MACAddr,将MACAddr地址保存到一个6字节数组中
3.配置一个10ms的定时器,在中断中一直调这个函数WCHNET_TimeIsr(WCHNETTIMERPERIOD);
官方描述如下:
WCHNET_Init 为库初始化函数。关于 WCHNET_Init 函数使用方法请参考 3.2。对WCHNET 初始 化后,应用层需要开启以太网中断,并在相应的中断函数中调用中断服务函数
WCHNET_ETHIsr; 另外库函数需要外部提供时钟,用于给时间相关的任务提供时钟源,例如刷新 ARP 列表,TCP 超时等,通过调用 WCHNET_TimeIsr 函数更新时间,该函数传递的参数为最近一次调用的时间差值,单位毫秒 。4.以太网库初始化
首先进行以太网库配置
以太网库初始化
5.创建UDP套接字
WCHNET_MAX_SOCKET_NUM是一个红包含了UDP,TCP套接字数量,可以进行配置
套接字创建
官方描述:
socketinf 仅作为变量传递, WCHNET_SocketCreat 对列表信息进行分析,如果信息合法,
则会从 SocketInf[WCHNET_MAX_SOCKET_NUM]中找到一个空闲的列表 n,将 socketinf 复制到 SocketInf[n]中,将 SocketInf[n]锁定并创建相应的 UDP、TCP 或者 IPRAW 连接。如果创建成功,将 n 写入到 socketid 中并返回成功 。
在创建 UDP、TCP 客户端和 IPRAW 时,应该在创建之前分配好接收缓冲区和接收缓冲区大
小。 TCP 服务器分配的方式则不同,应该在接收到连接成功中断后调用函数
WCHNET_ModifyRecvBuf 来分配接收缓冲区
修改 socket 接收缓冲区描述:
为了使应用层方便灵活的处理数据,库允许动态修改 socket 接收缓冲区的地址和大小,
在修改接收缓冲区前最好调用 WCHNET_SocketRecvLen 来检查缓冲区中是否有剩余数据,一 旦调用 WCHNET_ModifyRecvBuf,原缓冲区的数据将会被清除。 在 TCP 模式下,如果连接已经建立,调用 WCHNET_ModifyRecvBuf,库会向远端通告当前窗口大小。6.在主循环调用
CHNET_NetInput
WCHNET_PeriodicHandleWCHNET_HandlePhyNegotiation()
void WCHNET_HandlePhyNegotiation(void) { if(phyLinkReset) /* After the PHY link is disconnected, wait 500ms before turning on the PHY clock*/ { if( LocalTime - phyLinkTime >= 500 ) { phyLinkReset = 0; EXTEN->EXTEN_CTR |= EXTEN_ETH_10M_EN; PHY_LINK_RESET(); } } else { if( !phyStatus ) /* Handling PHY Negotiation Exceptions */ { ACCELERATE_LINK_PROCESS(); if( LocalTime - phyLinkTime >= LinkTaskPeriod ) { UPDATE_LINKTASKPERIOD(); phyLinkTime = LocalTime; WCHNET_LinkProcess( ); } if(ReInitMACFlag) ReInitMACFlag = 0; } else{ /* PHY link complete */ if(ReInitMACFlag) { if( LocalTime - phyLinkTime >= 5 * PHY_LINK_TASK_PERIOD ){ u32 phy_stat; ReInitMACFlag = 0; phy_stat = ETH_ReadPHYRegister( gPHYAddress, PHY_BMSR); if((phy_stat&PHY_Linked_Status) == 0) { WCHNET_PhyStatus( phy_stat ); PHY_LINK_RESET(); } } } if(PhyPolarityDetect) { if( LocalTime - LinkSuccTime >= 2 * PHY_LINK_TASK_PERIOD ) { WCHNET_PhyPNProcess(); } } } } }这个函数的作用如下:
WCHNET_RecProcess
接收相关处理
重新初始化以太网MAC控制器 的函数,在PHY链路状态变化或网络异常时恢复MAC正常工作。它通过保存配置→软件复位→恢复配置→重启外设的流程实现无损恢复。
WCHNET_HandleGlobalInt
WCH以太网协议栈的全局中断处理函数,负责响应和处理所有网络相关的中断事件。它通过读取全局中断标志,分类处理PHY状态变化、IP冲突、不可达报文等事件,并将Socket中断分发给具体处理函数。
根据接收到的数据执行不同的动作
只需要修改一个函数,就可以实现收到数据后,根据消息内容执行不同的动作
void WCHNET_DataLoopback(u8 id)
{
//根据接收到的数据进行响应
#if 1
u32 recvLen;
u8 responseBuffer[UDP_RECE_BUF_LEN];
u32 responseLen;
// 获取接收数据长度
recvLen = WCHNET_SocketRecvLen(id, NULL);
if(recvLen > 0) {
// 读取接收的数据到缓冲区
WCHNET_SocketRecv(id, MyBuf, &recvLen);
// 根据接收的数据内容构建响应
if(MyBuf[0] == 0x01) { // 示例:如果第一个字节是0x01
strcpy((char*)responseBuffer, "Command 1 received");
responseLen = strlen("Command 1 received");
}
else if(MyBuf[0] == 0x02) { // 示例:如果第一个字节是0x02
const u8 responseData[] = {0xAA, 0xBB, 0xCC, 0xDD};
memcpy(responseBuffer, responseData, sizeof(responseData));
responseLen = sizeof(responseData);
}
else {
strcpy((char*)responseBuffer, "Unknown command");
responseLen = strlen("Unknown command");
}
// 发送响应数据
WCHNET_SocketSend(id, responseBuffer, &responseLen);
}
#endif
#if 0
// 自定义要发送的数据
u8 customData[] = "Hello from CH32V307!"; // 您想要发送的自定义数据
u32 len = sizeof(customData) - 1; // 不包括字符串结束符
// 发送自定义数据
u8 result = WCHNET_SocketSend(id, customData, &len);
if (result == WCHNET_ERR_SUCCESS) {
printf("Sent custom data, length: %d\n", len);
}
// 清除接收缓冲区中的原始数据
len = SocketInf[id].RecvRemLen;
WCHNET_SocketRecv(id, NULL, &len);
#endif
#if 0
// 原始代码保留作为参考
u32 len, totallen;
u8 *p = MyBuf;
len = WCHNET_SocketRecvLen(id, NULL);
printf("Receive Len = %02x\n", len);
totallen = len;
WCHNET_SocketRecv(id, MyBuf, &len);
while(1){
len = totallen;
WCHNET_SocketSend(id, p, &len);
totallen -= len;
p += len;
if(totallen)continue;
break;
}
#endif
}
UDP服务器端
服务器端是先接收,再处理,所以服务器端不需要知道发给谁,接收到谁的套接字,就向这个套接字的iP和端口发送数据。

创建套接字

相比于客户端,不需要设置目标IP和端口号,和修改接收缓冲区大小
加入了接收回调处理函数和接收缓冲区的起始指针


发送数据

发送函数使用了 WCHNET_SocketUdpSendTo(socinf->SockIndex, buf, &len, ip_addr, port);
与客户端有所不同
根据接收到的数据执行不同的动作
void WCHNET_UdpServerRecv(struct _SOCK_INF *socinf, u32 ipaddr, u16 port, u8 *buf, u32 len)
{
u8 ip_addr[4], i;
u8 response[128]; // 响应缓冲区,可根据实际需求调整大小
u32 response_len = 0;
// 解析并存储IP地址
printf("Remote IP:");
for (i = 0; i < 4; i++) {
ip_addr[i] = (ipaddr >> (i * 8)) & 0xff;
printf("%d ", ip_addr[i]);
}
printf("srcport = %d len = %d socketid = %d\r\n", port, len, socinf->SockIndex);
// 如果没有接收到有效数据,直接返回
if (len == 0) {
return;
}
// 根据接收数据执行不同操作
switch (buf[0]) {
case 'L': // LED控制命令
if (len > 1) {
if (buf[1] == '0') {
// LED关闭操作 (需实现实际硬件控制)
strcpy((char*)response, "LED turned OFF");
} else if (buf[1] == '1') {
// LED开启操作 (需实现实际硬件控制)
strcpy((char*)response, "LED turned ON");
} else {
strcpy((char*)response, "Invalid LED command");
}
response_len = strlen((char*)response);
}
break;
case 'R': // 读取系统信息
{
// 获取系统信息 (需实现实际数据获取)
}
break;
case 'C': // 自定义命令处理
if (len > 1 && buf[1] == 'H') {
// 帮助命令
strcpy((char*)response, "Commands: L0/L1=LED, R=status, C=custom");
response_len = strlen((char*)response);
}
break;
default:
// 未知命令处理
response_len = sprintf((char*)response, "Unknown command: %c", buf[0]);
printf("Unknown command received: 0x%02X\r\n", buf[0]);
break;
}
// 如果有响应数据,发送回客户端
if (response_len > 0) {
WCHNET_SocketUdpSendTo(socinf->SockIndex, response, &response_len, ip_addr, port);
}
}
解惑
1.为什么UDP服务端和客户端接收数据方式不同?
或者叫初始化的方式不同。
在沁恒官方历程中,UDP的客户端采用了轮询的方式进行数据接收发送,服务器端采用了中断回调的方式。
解释:在UDP协议栈中,客户端和服务器端没有明确的角色关系,发送数据时就是客户端,接收数据时就是服务器端。如果想要及时的处理接收到的数据并作出相应,就需要采用中断回调的方式进行数据接收处理。在工程应用中,往往MCU是需要接收数据的一方,因此MCU配置为服务器的方式,及时监听数据。
客户端是知道数据什么时候大概会来,比如:客户端发送数据到服务器,那客户端只需在一定的时间内进行等待服务器端发来的数据接收即可。
2.为什么UDP服务器端不需要配置发送端端口号
服务器会接收各个客户端消息,在接收到客户端套接字后,套接字中包含了客户端的IP地址和端口号,在沁恒官方历程中,服务端和客户端的发送函数也不同。
官方说明:
在 UDP 模式下 WCHNET_SocketSend 和 WCHNET_SocketUdpSendTo 的区别在于,前者只能向 创建 socket 时指定的目标 IP 和端口发送数据,后者可以向任意的 IP 和端口发送数据。
WCHNET_SocketUdpSendTo 一般用在 UDP 服务器模式。
基于CH32V307
原文连接:单片赤菟V307实现八串口服务器 - Wahahahehehe - 博客园
赤菟V307是搭载沁恒自研RISC-V内核青稞V4F的高性能互联型MCU,主频支持144MHz,支持硬件浮点运算(FPU),提供八个UART接口、USB2.0高速接口(480Mbps)并内置了PHY收发器、千兆以太网MAC并集成10M PHY、2个CAN接口等丰富的外设资源。


























