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

news2024/12/31 4:34:32

    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". */之间,添加如下代码:

	//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变量自行定义或替换监听端口号,但是其他端口也能监听)

/**
  * @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)这个函数,参考奋斗的程序,如下:

/**
  * @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流程,然后在回调函数中,注入要发送的数据和长度,之后就发出去了。逻辑就是这么简单,代码如下:

先定义俩个全局变量

u8 *pActiveSendData;  //UDP主动发送,数据指针
u16 ActiveSendLen;    //UDP主动发送数据长度

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

/**
  * @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()宏定义的那个要自己加的函数

/**
  * @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

/**
  * @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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1036031.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

20230224_HDR-ISP_环境配置

https://github.com/JokerEyeAdas/HDR-ISP/tree/main 一、环境配置 *.VS2015编译不过;VS2017没问题 *.将红框部分复制到工程下面 *.添加头文件路径 *.添加原文件

【C#】Redis在net core下使用教程

系列文章 文章目录 系列文章前言一、Redis 简介1.1 Redis 优势1.2 Redis与其他key-value存储有什么不同? 二、Redis安装步骤2.1 下载链接2.2 安装测试 三、Redis修改帐户密码四、Redis写成Windows服务五、.net core - 使用CSRedisCore操作redis 前言 官方教程&…

【C语言】模拟实现内存函数

本篇文章目录 相关文章1. 模拟 memcpy 内存拷贝2. 模拟 memmove 内存移动 相关文章 【C语言】数据在内存中是以什么顺序存储的?【C语言】整数在内存中如何存储?又是如何进行计算使用的?【C语言】利用void*进行泛型编程【C语言】4.指针类型部…

相机有俯仰角时如何将像素坐标正确转换到其他坐标系

一般像素坐标系转相机坐标系都是默认相机是水平的,没有考虑相机有俯仰角的情况,大致的过程是:像素坐标系统-->图像坐标系-->相机坐标系 ->世界坐标系或雷达坐标系: 像素坐标系 像素坐标系(u,v)是…

【Java 基础篇】Java函数式接口详解

Java是一门强类型、面向对象的编程语言,但在Java 8引入了函数式编程的概念,这为我们提供了更多灵活的编程方式。函数式接口是函数式编程的核心概念之一,本文将详细介绍Java函数式接口的概念、用法以及一些实际应用。 什么是函数式接口&#…

JUC第八讲:Condition源码分析

JUC第八讲:Condition源码分析 本文是JUC第八讲,Condition详解。任意一个Java对象,都拥有一组监视器方法(定义在java.lang.Object上),主要包括 wait()、wait(long timeout)、notify()以及notifyAll()方法&am…

【LeetCode-中等题】513. 找树左下角的值

文章目录 题目方法一:前序递归方法二:层序遍历 题目 方法一:前序递归 在递归遍历到叶子结点时,对比此时的节点深度,若当前节点深度大于当前最大深度,就更新value值,最后记录下的value即为最下最…

elementui 菜单选中优化

/** 父级菜单悬浮样式**/ .el-submenu__title:hover {color:#1890ff!important; } /** 父级菜单箭头悬浮样式**/ .el-submenu__title:hover>.el-submenu__icon-arrow{font-size: 13px!important;} /** 子菜单悬浮样式**/ .el-menu-item:hover{color:#1890ff!important; } /*…

Linux操作系统基础详解,计算机专业必看!

目录 Linux操作系统 Linux 简介 Linux 接口 Linux 组成部分 Shell Linux 应用程序 Linux 内核结构 Linux 进程和线程 基本概念 Linux 进程间通信 Linux 中进程管理系统调用 Linux 进程和线程的实现 Linux 调度 Linux 启动 Linux 内存管理 基本概念 Linux 内存…

【owt】 Intel® Media SDK for Windows: MSDK2021R1

https://www.intel.com/content/www/us/en/developer/articles/tool/media-sdk.html官方网不提供下载了: 2021地址 直接下载: MSDK2021R1.exe老版本 Intel Media SDK(Windows版本) 大神的介绍:owt-client-native 需要 https://github.com/open-webrtc-toolkit/owt-client…

spring security auth2.0实现

OAuth 2.0 的认证/授权流程 jwt只是认证中的一步 4中角色 资源拥有者(resource owner)、客户端(client 第三方)、授权服务器(authorization server)和资源服务器(resource server)。…

vue 使用cornerstone解析 .dcm 文件

// 首先下载依赖 npm install --save cornerstone-core cornerstone-math cornerstone-tools hammerjs cornerstone-web-image-loader 下载之后再package.json中可以看到最后图片的依赖// 下面是完成的组件代码 <template><div id"dicom_canvas" refdicom_c…

FL Studio21.1无限试用版体验新功能变化介绍

许多刚刚接触音乐创作的新朋友&#xff0c;通过各种渠道了解到FL Studio&#xff0c;但并不知道我们的历史以及在音乐创作方面所产生的影响&#xff0c;今天分享一篇来自coco玛奇朵博主Rio的深度科普文章&#xff0c;相信对新人会有很大启发。 FL Studio 21.1 通过钢琴卷中的音…

C++:类中的静态成员函数以及静态成员变量

一、静态成员变量 静态成员&#xff1a;在类定义中&#xff0c;它的成员&#xff08;包括成员变量和成员函数&#xff09;&#xff0c;这些成员可以用关键字static声明为静态的&#xff0c;称为静态成员。 静态成员变量需要在类外分配空间&#xff0c;static 成员变量是在初始…

速码!!BGP最全学习笔记:IBGP和EBGP基本配置

实验1&#xff1a;配置IBGP和EBGP 实验目的 熟悉IBGP和EBGP的应用场景掌握IBGP和EBGP的配置方法 实验拓扑 想要华为数通配套实验拓扑和配置笔记的朋友们点赞关注&#xff0c;评论区留下邮箱发给你! 实验步骤 1.IP地址的配置 R1的配置 <Huawei>system-view …

基于单片机火灾报警器仿真设计

一、系统方案 1、本设计采用51单片机作为主控器。 2、DS18B20采集温度值送到液晶1602显示。 3、MQ2采集烟雾值&#xff0c;送到液晶1602显示。 4、按键设置温度报警值&#xff0c;大于报警值&#xff0c;声光报警。 二、硬件设计 原理图如下&#xff1a; 三、单片机软件设计…

微信小程序快速入门01(含案例)

文章目录 前言一、组件1.常用视图容器类组件viewscroll-viewswiper、swiper-item 2.text、rich-text3.其他常用组件buttonimagenavigator 二、小程序API三、数据绑定1.定义页面数据2.绑定数据 四、事件绑定1.什么是事件2.小程序中常用的事件3.事件对象 的属性列表target和curre…

Seata--分布式事务

1 分布式事务基础 1.1 事务 事务指的就是一个操作单元&#xff0c;在这个操作单元中的所有操作最终要保持一致的行为&#xff0c;要么所有操作都成功&#xff0c;要么所有的操作都被撤销。简单地说&#xff0c;事务提供一种“要么什么都不做&#xff0c;要么做全套”机制。 1…

代码随想录算法训练营day60|84.柱状图中最大的矩形 |完结撒花~

84.柱状图中最大的矩形 力扣题目链接 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出来的矩形的最大面积。 1 < heights.length <10^5 0 < heights[i] < 10^…

ChatGPT WPS AI 一键核对两表数据差异

业务需求,找出两个表中不相同的内容。如下图: 像这样的表格中,要找出不同的值,手动核对效率不高。 现在我们有了ChatGPT,可以由人工智能来完成这一操作,高效,快速,准确定位差异值。 指令:请找出A1:G14 单元格区域和I1:O14单元格区域的不相同部分,将两部数据区域不相…