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

	//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主动发送数据长度
/*** @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.mzph.cn/news/83748.shtml

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

相关文章

利用Linux虚拟化技术实现资源隔离和管理

在现代计算机系统中,资源隔离和管理是非常重要的,特别是在多租户环境下。通过利用Linux虚拟化技术,我们可以实现对计算资源(如CPU、内存和存储)的隔离和管理,以提供安全、高效、稳定的计算环境。下面将详细…

如何将内网ip映射到外网?快解析内网穿透

关于内网ip映射到外网的问题,就是网络地址转换,私网借公网。要实现这个,看起来说得不错,实际上是有前提条件的。要实现内网ip映射到外网,首先要有一个固定的公网IP,可以从运营商那里得到。当你得到公网IP后…

Flink——Flink检查点(checkpoint)、保存点(savepoint)的区别与联系

Flink checkpoint Checkpoint是Flink实现容错机制最核心的功能,能够根据配置周期性地基于Stream中各个Operator的状态来生成Snapshot,从而将这些状态数据定期持久化存储下来,从而将这些状态数据定期持久化存储下来,当Flink程序一…

FPGA设计时序约束一、主时钟与生成时钟

​目录 一、主时钟create_clock 1.1 定义 1.2 约束设置格式 1.3 Add this clock to the existing clock 1.4 示例 1.5 差分信号 二、生成时钟generate_clock 2.1 定义 2.2 格式 2.2.1 by clock frequency 2.2.2 by clock edges 2.2.3 示例 2.2.4 自动生成时钟 2.…

MongoDB-1入门介绍

NoSQL NoSQL(NoSQL Not Only SQL),意即反SQL运动,指的是非关系型的数据库 优点 1、对数据库高并发读写。 2、对海量数据的高效率存储和访问。 3、对数据库的高可扩展性和高可用性。 弱点: 1、数据库事务一致性需求 2、数据库的写实时性…

flink集群与资源@k8s源码分析-集群

0 介绍 本文是flink集群与资源@k8s源码分析系列的第二篇-集群 1 场景 下面详细分析各用例 2 启动k8s集群 k8s集群支持session和application模式,job模式将会被废弃,本文分析session模式集群 Configuration作为配置容器,几乎所有的构建需要从配置类获取配置项,这里不显示…

将docker镜像打成tar包

# 打包 docker save -o zookeeper.tar bitnami/zookeeper:3.9.0-debian-11-r11# 解压 docker load -i zookeeper.tar

day27IO(异常File综合案例)

1. 异常 1.1 异常概念 异常,就是不正常的意思。在生活中:医生说,你的身体某个部位有异常,该部位和正常相比有点不同,该部位的功能将受影响.在程序中的意思就是: 异常 :指的是程序在执行过程中,出现的非正常的情况,最…

事务碰上锁好似那油锅里进了火

目录 前言 场景 代码复现 提出疑问 该怎么解决呢 1.使用编程式事务 2.将事务独立出一个方法 前言 很多时候我们谈起事务都是如虎色变,一想起来都是脑袋懵懵的 事务的隔离级别及传播机制是什么Spring的事务底层实现原理了解吗哪几种情况下事务会失效 …

【探索C++】C++对C语言的扩展

(꒪ꇴ꒪ ),Hello我是祐言QAQ我的博客主页:C/C语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP🌍快上🚘,一起学习,让我们成为一个强大的攻城狮&#xff0…

JavaScript基础知识12——运算符:算数运算符,比较运算符

哈喽,大家好,我是雷工。 以下为JavaScript基础知识学习笔记。 一、算数运算符 1、算术运算符:即进行数学计算的符号。 2、有哪些算数运算符: :加法 -:减法 *:乘法 /:除法 %:取余(…

系统架构设计师-数据库系统(3)

目录 一、数据控制 1、安全性 2、完整性 3、并发控制 4、故障恢复 二、数据库设计概述 1、数据库设计关注的问题 2、数据库性能优化 3、规范化与反规范化 一、数据控制 1、安全性 2、完整性 (1)实体完整性约束:规定基本关系的主属性不能取空…

Python开发利器之VS Code

Python官方提供了一个Python集成开发环境(IDE): IDLE (Integrated Development and Learning Environment)。 它提供了一个图形用户界面,可以让开发者编写、调试和执行Python程序。IDLE包含Python解释器、代码编辑器、调试器和文件…

RK3568平台开发系列讲解(工具命令篇)ADB的安装

🚀返回专栏总目录 文章目录 一、ADB介绍二、Windows 下安装 adb 工具沉淀、分享、成长,让自己和他人都能有所收获!😄 一、ADB介绍 adb 全称 Android Debug Bridge,直译过来就是 Android 调试桥,它是一个通用的命令行工具。adb 做为 Android 设备与 PC 端连接的一个桥梁…

揭秘:Wasserstein GAN与梯度惩罚(WGAN-GP)

一、说明 什么是梯度惩罚?为什么它比渐变裁剪更好?如何实施梯度惩罚?在提起GAN对抗网络中,就不能避免Wasserstein距离的概念,本篇为系列读物,目的是揭示围绕Wasserstein-GAN建模的一些重要概念进行探讨。 图…

浅谈DBT的一些不足之处

DBT的好处是显而易见的,它支持连接多达41种数据库。而且不需要你写DDL语句,只要写select语句,DBT会自动帮你推断schema结构,将数据写入到数据库中: 但是使用了一段时间之后,发现DBT也存在着如下这些不足之处…

/usr/bin/ld: cannot find -lmysqlcllient

文章目录 1. question: /usr/bin/ld: cannot find -lmysqlcllient2. solution 1. question: /usr/bin/ld: cannot find -lmysqlcllient 2. solution 在 使用编译命令 -lmysqlclient时,如果提示这个信息。 先确认一下 有没有安装mysql-devel 执行如下命令 yum inst…

【Linux】Ubuntu美化主题【教程】

【Linux】Ubuntu美化主题【教程】 文章目录 【Linux】Ubuntu美化主题【教程】1. 安装优化工具Tweak2.下载自己喜欢的主题3. 下载自己喜欢的iconReference 1. 安装优化工具Tweak 首先安装优化工具Tweak sudo apt-get install gnome-tweak-tool安装完毕后在菜单中打开Tweak 然后…

**20.迭代器模式(Iterator)

意图:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。 上下文:集合对象内部结构常常变化各异。对于这些集合对象,能否在不暴露其内部结构的同时,让外部Client透明地访问其中包含的元素…

nbcio-boot移植到若依ruoyi-nbcio平台里一formdesigner部分(三)

因为这个版本的若依plus不支持本地文件上传&#xff0c;所以需要增加这些本地上传文件的后端代码 和前端代码修改。 1、后端部分 先配置跳过测试吧&#xff0c;平时编译也不需要这个 <!--添加配置跳过测试--><plugin><groupId>org.apache.maven.plugins<…