基于ENC28J60+uIP1.0+STM32的UDP Server实现,服务器主动发送数据的实现,几个关键的问题可算整明白了!

ENC28J60,是一款SPI接口的以太网PHY+MAC芯片,实现以太网物理层和MAC层硬件通信。uIP是一个TCP/IP软件协议栈,实现TCP、UDP、ARP、ICMP等网络协议。STM32F103RCT6通过SPI接口与ENC28J60通讯,并移植uIP协议,实现一个小型的UDP服务器。

严格来说,UDP是面向无连接的对等通讯协议,不存在服务器与客户端之说,这里的服务器只是来形容STM32 SERVER系统的UDP的使用方式,即STM32充当服务器角色,接收同网段的不同IP和端口的UDP数据包,并将数据返回。

至于ENC28J60+uIP移植,相信不少资料与例子可供参考,比如奋斗嵌入式,正点原子等等,本文不再赘述。这里只说我在移植uIP,调试软件时发现的几个问题。

1、不要TCP,只需要UDP、ARP、ICMP协议,移植哪些文件。

下图是移植uIP后实现上述协议的截图,没错,只要"uip.c"和"uip_arp.c"和必要的头文件就足够了!多一个文件都是浪费!头文件途径也是添加uip和unix文件夹就好了。

2、UDP服务器,接收同网段不同IP和端口,要改uip.c文件的位置,以及怎么用。

打开"uip.c"文件,约1100行左右,#endif /* UIP_UDP_CHECKSUMS */和/* Demultiplex this UDP packet between the UDP "connections". */之间,添加如下代码:

cpp 复制代码
	//UDP SERVER补丁,客户端IP和端口不受限制
	if((uip_udp_conn != 0)
		&&((uip_udp_conn->rport != UDPBUF->srcport)
		 ||!uip_ipaddr_cmp(uip_udp_conn->ripaddr, UDPBUF->srcipaddr)))  	//如果是已经连接并且和接收到的端口号或者IP地址不一致
	{
		uip_udp_remove(uip_udp_conn);											//删除连接
	  uip_udp_conn->rport = UDPBUF->srcport;            //将目的端口设置为收到的远端UDP包的源端口
		uip_udp_conn->lport = UDPBUF->destport;           //将本地端口设置为收到的远端UDP包的目的端口
		memcpy(uip_udp_conn->ripaddr, UDPBUF->srcipaddr, sizeof(uip_ipaddr_t ));	 //将目的IP地址设置为收到的远端UDP包的源IP地址
  }
	
  if(uip_udp_conn->rport == 0)									   		//如果首次接收到某个远端UDP包
	{
    uip_udp_conn->rport = UDPBUF->srcport;            //将目的端口设置为收到的远端UDP包的端口
		memcpy(uip_udp_conn->ripaddr,UDPBUF->srcipaddr,sizeof(uip_ipaddr_t ));	 //将目的IP地址设置为收到的远端UDP包的源IP地址
	}
	
	//uip_udp_conn = uip_udp_new(&ipaddr, HTONS(1000));	//建立到远程ipaddr,端口为1000的连接 
	if(uip_udp_conn != 0)  
	{
		uip_udp_bind(uip_udp_conn, UDPBUF->destport);//绑定本地端口为LPORT,也就是LPORT-->RPORT 发数据 
	}
  //end 补丁

改好后是这样的:

这段代码参考了奋斗的程序,奋斗的程序有BUG,最开始的那个if语句

if((uip_udp_conn != 0)

&&((uip_udp_conn->rport != UDPBUF->srcport)

||!uip_ipaddr_cmp(uip_udp_conn->ripaddr, UDPBUF->srcipaddr))) //如果是已经连接并且和接收到的端口号或者IP地址不一致

红的标记的部分,原来的奋斗的程序是"||uip_udp_conn->ripaddr!=UDPBUF->srcipaddr)",ripaddr和srcipaddr是u16类型的指针地址,判断的是指针的地址是否相等,那么必然不等,所以即使同样的IP地址发来的包都会判断为真跳进if的执行语句中。改正后已经解决。

最后的if语句中:uip_udp_bind(uip_udp_conn, UDPBUF->destport);

目的是将收到的UDP包中,目的端口作为服务器的监听端口,每次都会根据客户端的UDP端口改变,即实现不同的IP、不同的源端口、目的端口发给服务器的UDP包,服务器都能处理后原路返回。如果需要固定服务器的监听端口,只需要将UDPBUF->destport改为固定的端口号即可,例如端口号2000,改为uip_udp_bind(uip_udp_conn, HTONS(2000));

再讲讲怎么用。在main函数初始化ENC28J60和uIP之后,随便监听一个端口就可以了,调用如下函数(NetParam.lUdpPort变量自行定义或替换监听端口号,但是其他端口也能监听)

cpp 复制代码
/**
  * @brief  UDP服务器模式
  * @param  None
  * @retval None
*/
void udp_server_creat(void)
{
	uip_listen(HTONS(NetParam.lUdpPort));
	uip_udp_bind(uip_udp_conn, htons(NetParam.lUdpPort));//绑定本地端口为LPORT
}

3、STM32断电再上电后,PC发给STM32的第一包数据,PC收不到STM32回复的原因。

阿莫有个帖子:为什么uip中udp第一次主动发送数据的时候PC没有收到呢,第二次以后后就正常了 (amobbs.com 阿莫电子论坛 - 东莞阿莫电子网站)

之前我也被这个问题困扰了一段时间,我做的udp server demo,在用PC端用网络调试助手随便发一个什么数据给STM32 SERVER,SERVER收到后,原路回复上电后收到数据的序号,以及收到数据的字节长度,但是每次下载程序后测试,第一个数据都不回复,第二个才开始回复,序号也是跳过0直接回1。

原因是:STM32 SERVER端的ARP列表上电后被清空,而电脑端的ARP列表又是上一次缓存了STM32 SERVER的IP和MAC,所以电脑给STM32 SERVER发送UDP包时直接就把UDP发过来了而没有发起ARP,这是一方面。另一方面,void UipPro(void)这个函数,参考奋斗的程序,如下:

cpp 复制代码
/**
  * @brief  中断触发读取网络接收缓存
  * @param  None
  * @retval None
*/
void UipPro(void)
{
	if(ETH_INT == 1){					//当网络接收到数据时,会产生中断
rep:;
		ETH_INT = 0;
		uip_len = tapdev_read();	//从网络设备读取一个IP包,返回数据长度
		if(uip_len > 0)			    //收到数据
		{
			/* 处理IP数据包(只有校验通过的IP包才会被接收) */
			if(BUF->type == htons(UIP_ETHTYPE_IP))   //是IP包吗?
			{
				uip_arp_ipin();		   //去除以太网头结构,更新ARP表
				uip_input();		   //IP包处理
				/*
					当上面的函数执行后,如果需要发送数据,则全局变量 uip_len > 0
					需要发送的数据在uip_buf, 长度是uip_len  (这是2个全局变量)
				*/
				if (uip_len > 0)		//有带外回应数据
				{
					uip_arp_out();		//加以太网头结构,在主动连接时可能要构造ARP请求
					tapdev_send();		//发送数据到以太网(设备驱动程序)
				}
			}
			/* 处理arp报文 */
			else if (BUF->type == htons(UIP_ETHTYPE_ARP))	//是ARP请求包
			{
				uip_arp_arpin();		//如是是ARP回应,更新ARP表;如果是请求,构造回应数据包
				/*
					当上面的函数执行后,如果需要发送数据,则全局变量 uip_len > 0
					需要发送的数据在uip_buf, 长度是uip_len  (这是2个全局变量)
				*/
				if (uip_len > 0)		//是ARP请求,要发送回应
				{
					tapdev_send();		//发ARP回应到以太网上
				}
			}
		}
  }
  else{	  //防止大包造成接收死机,当没有产生中断,而ENC28J60中断信号始终为低说明接收死机
  	 if(ENC28J60_INT_STA == 0) goto rep; 	
  }
}

uip_arp_ipin(); //去除以太网头结构,更新ARP表

这一行本意是,直接从收到的IP包里面获取源IP和源MAC,加入到本地ARP表中,然而全局搜索一下这个函数,发现其在uip_arp.h中是空定义的,上面的函数声明被注释了,

再看uip_arp.c文件,#if 0也是将此函数注释了。

有可能uIP作者调试后忘记将其解除注释,因此,uip_arp_ipin(); //去除以太网头结构,更新ARP表预期没有实现。

因此,对于STM32来说,它直接收到一个UDP包,要回复给发来这个包的IP地址,但是它的ARP列表是空的,STM32直接就懵了,不知道对方的MAC是啥,于是STM32就放弃发送这包数据,改为发送ARP查询包,询问对方IP。在uip_arp_out()函数中有如下注释就是说了这个意思:

了解原因后,将其释放出来,再编译,发现STM32 SERVER重新上电后第一包数据不回复问题解决。

uIP之uip_arp_ipin();函数的作用。-OpenEdv-开源电子网 这个帖子也是问了这个问题,我没有ID,只能在这里回答了。

4、主动发送UDP数据包的原理及实现。

uIP协议栈用于"收到后发送"的逻辑很好用,但是主动发送要费一番周折,看一遍uip.c这个文件,其发送的大概流程是

uip_process函数会周期调用,flag变量如果为"UIP_UDP_TIMER"状态,就会进入到回调函数UIP_UDP_APPCALL();,然后goto udp_send;进入发送数据流程,如果缓存数据长度变量uip_slen非0,则发送数据。

因此,想要主动发送UDP包,需要让uip_process函数进入UIP_UDP_TIMER流程,然后在回调函数中,注入要发送的数据和长度,之后就发出去了。逻辑就是这么简单,代码如下:

先定义俩个全局变量

cpp 复制代码
u8 *pActiveSendData;  //UDP主动发送,数据指针
u16 ActiveSendLen;    //UDP主动发送数据长度

实现主动发送函数,主要是配置一下端口,然后调用uip_process函数,使其能跳转到回调函数。

cpp 复制代码
/**
  * @brief  主动发送udp包
  * @param  data 数据
  * @param  len  数据长度
  * @retval None
*/
void udp_active_send(u8 *data, u16 len)
{
	pActiveSendData = data;
	ActiveSendLen = len;
	
	//设置目标ip和端口
	uip_udp_conn->rport = HTONS(NetParam.rUdpPort);    //将目的端口设置为收到的远端UDP包的源端口
	uip_udp_conn->lport = HTONS(NetParam.lUdpPort);    //将本地端口设置为收到的远端UDP包的目的端口
	uip_ipaddr_t ipaddr;
	uip_ipaddr(ipaddr, NetParam.rIP[0], NetParam.rIP[1], NetParam.rIP[2], NetParam.rIP[3]);	
	memcpy(uip_udp_conn->ripaddr, ipaddr, sizeof(uip_ipaddr_t ));
	
	//唤起UDP发送处理
	uip_process(UIP_UDP_TIMER);
	
	/* 如果上面的函数调用导致数据应该被发送出去,全局变量uip_len设定值> 0 */
	if(uip_len > 0)
	{
		uip_arp_out();		//加以太网头结构,在主动连接时可能要构造ARP请求
		tapdev_send();		//发送数据到以太网(设备驱动程序)
		ActiveSendLen = 0;//发送完成数据清零
	}
}

回调函数向缓冲区写入数据,就是用UIP_UDP_APPCALL()宏定义的那个要自己加的函数

cpp 复制代码
/**
  * @brief  UDP主函数
  * @param  None
  * @retval None
*/
void udp_server_appcall(void)
{
	//接收到一个新的udp数据包
	if(uip_newdata())//收到客户端发过来的数据
	{
		UDP_newdata();
	}
	else if(uip_poll())//主动发送数据
	{
		if(ActiveSendLen)
		{
			udp_send(pActiveSendData, ActiveSendLen);
		}
	}
}

发送函数udp_send

cpp 复制代码
/**
  * @brief  UDP 数据包发送
  * @param  str:数据
  * @retval None
*/
void udp_send(u8 *str, u16 Len)
{
   struct udp_server_appstate *s = (struct udp_server_appstate *)&uip_udp_conn->appstate;

   s->textptr =(u8*)str;
   s->textlen =Len;
   uip_send(s->textptr, s->textlen);//发送udp数据包
//   uip_udp_send(s->textlen);
}

怎么用?主动发送直接调用udp_active_send,如果是先收再发建议还是用udp_send

udp_active_send("OK", 2);

最后附上测试图,1S间隔主动发送

两个PC客户端,不同IP和源目的端口,轮流发送测试。

本例程中ENC28J60与STM32F103RCT6硬件接线:

SPI4根线:CS:PA3

SCK,MISO MOSI :PA5 PA6 PA7

ENC28J60中断 INT:PC4

IP参数定义位于udp_server.c文件中,根据需要自己改,lIP就是STM32 SERVER的IP,rIP是电脑的IP。

最后的最后,放上程序:

链接:https://pan.baidu.com/s/1JmLrI4zj7Bs5ifaHQ3OxAg?pwd=ldp9

提取码:ldp9

如果链接失效记得评论区喊我更新!

相关推荐
禾仔仔6 小时前
USB MSC从理论到实践(模拟U盘为例)——从零开始学习USB2.0协议(六)
嵌入式硬件·mcu·计算机外设
The Electronic Cat8 小时前
树莓派使用串口启动死机
单片机·嵌入式硬件·树莓派
阿华hhh10 小时前
Linux系统编程(网络udp)
linux·服务器·c语言·网络·网络协议·udp
先知后行。10 小时前
常见元器件
单片机·嵌入式硬件
恒锐丰小吕11 小时前
屹晶微 EG2302 600V耐压、低压启动、带SD关断功能的高性价比半桥栅极驱动器技术解析
嵌入式硬件·硬件工程
Dillon Dong11 小时前
按位或(|=)的核心魔力:用宏定义优雅管理嵌入式故障字
c语言·stm32
Free丶Chan12 小时前
dsPIC系列-1:dsPIC33点灯 [I/O、RCC、定时器]
单片机·嵌入式硬件
恒锐丰小吕14 小时前
屹晶微 EG2106D 600V耐压、半桥MOS/IGBT驱动芯片技术解析
嵌入式硬件·硬件工程
Ghost Face...14 小时前
U-Boot与PMON:配置与设备树解析对比
linux·单片机·嵌入式硬件