工程源码下载:基于裸机和Freertos的W5500网络通信工程
目录
1. TCP和UDP的区别
2. 【TCP Server】TCP服务器实验
3. 【TCP Client】TCP客户端实验
4. TCP Server状态机程序
4.1 获取状态标志
4.2 遍历执行
4.2.1 case SOCK_CLOSED: 判断socket是否为关闭状态
4.2.2 SOCK_INIT,socket处于初始化完成(打开)状态。转入监听模式listen(sn)。
4.2.3 读取当前端口的中断寄存器值【getSn_IR(sn)】并判断当前中断寄存器值是否是处于SN_IR_CON位,如果成立清除中断标志位。
4.2.4 心跳包检测
4.2.5 执行回调函数执行收发任务
4.2.6 SOCK_CLOSE_WAIT: 当socket处于等待关闭的状态时
5. TCP Client状态机程序
6. TCPCallback回调函数
6.1 TCP发送数据send
6.2 TCP接收数据回调函数recv
7. 其它函数讲解
7.1 断开TCP链接disconnect
7.2 关闭socketclose
TCP | UDP |
是以数据流的方式进行通信 | 是以数据包的方式进行通信 |
TCP是有向连接协议 | UDP是无向连接协议 |
当tcp-client和服务器建立连接时,它们需要三个握手协议 | UDP不需要握手,直接发送数据包 |
TCP通信不会丢失数据 | UDP通信会丢失数据包 |
在通信可靠性方面,TCP比UDP更可靠 | 但实时性强、常用于视频传输、网络电话等容差错数据传输。 |
安全性上,TCP安全保密要比UDP高 | 注:端口号是标识上层应用的不同线程。 |
在程序上最大的区别就是编辑状态机do_tcp_server和do_tcp_client在
socket处于初始化完成(打开)状态(case SOCK_INIT)执行的程序区别。
do_tcp_server建立监听listen(sn);
do_tcp_client利用socket连接服务器。
简单阐述一下通信原理,开发板作为服务器监听客户端发送的信息。客户端首先要获取到服务器的IP地址和端口号,然后向服务器发送数据。服务器收到数据,回传回客户端,并通过调试串口打印。
TCP状态机概念:
首先在本程序中状态机的意思可以理解为是具备遍历该系统所列出的状态,并进行相应处理的机制。通过描述系统某个模块的状态,通过状态机能够查找到相应的状态并提供执行机制的程序流程。
接线:H743战舰开发板
PC10-SCLK
PC11-MISO
PB2-MOSI
PA4-CS
uint8_t do_tcp_server(uint8_t sn, uint16_t port)
{
uint8_t state;
getsockopt(sn, SO_STATUS, (void *)&state); //获取socket的状态
switch (state)
{
case SOCK_CLOSED: //socket处于关闭状态
if(socket(sn,Sn_MR_TCP,port,Sn_MR_ND) == sn){ /*打开socket*/ /**< No Delayed Ack(TCP) flag */
printf("set tcp socket success\r\n");// 打开成功
}else{
printf("set tcp socket fail\r\n");// 打开失败
}
break; //socket关闭状态
case SOCK_INIT: //socket处于初始化完成(打开)状态
listen(sn); //socket建立监听
break; //监听本地端口,等待客户端连接
case SOCK_ESTABLISHED: //socket处于连接建立状态
if (getSn_IR(sn) & Sn_IR_CON)
{
setSn_IR(0, Sn_IR_CON); //Sn_IR的CON位置1,通知W5500连接已建立 清除接收中断标志位
}
setSn_KPALVTR(sn, 2); // 设置心跳包自动发送间隔,单位时间为5s,所以这里设置为10s。为0则不启用。
TCPCallback(sn); //回调函数中执行收发任务
break;
case SOCK_CLOSE_WAIT: //当socket处于等待关闭的状态时
disconnect(sn); //socket处于等待关闭状态
break;
}
return state;
}
同样,我们对上述状态机程序进行分解。
利用getsockopt函数加获取状态类型标志SO_STATUS来获取当前通信状态state。
根据获取的状态值state用switch语句来遍历在状态机中列出的状态是否与之相匹配,并执行相应的程序。
有以下几种状态:
if(socket(sn,Sn_MR_TCP,port,SF_TCP_NODELAY | SF_IO_NONBLOCK) == 1){
printf("set tcp socket success");// 打开成功
}else{
printf("set tcp socket fail");// 打开失败
}
SF_TCP_NODELAY 指定socket在收到对方的数据包后应该没有延时尽快答复ACK包,否则需要超时时间做延时。
SF_IO_NONBLOCK 用于控制socket.h中函数的行为,如启用这一选项,对这一socket调用socket.h中大部分函数不会阻塞等待调用结果,而是会在确认发出指令后尽快返回。
#define Sn_IR_CON 0x01
通知W5500连接已建立 清除接收中断标志位,在将Sn_IR寄存器设置为0x01。
//#define setSn_IR(sn, ir)
setSn_IR(0, Sn_IR_CON); //Sn_IR的CON位置1,通知W5500连接已建立
为什么要有心跳包机制?
对于TCP链接,如果意外断连,W5500没法知道实际已经没有链接了,然后就认为自己还连着,一直等待着数据。为此,需要加入心跳检测来保证通讯还正常。在socket初始化为TCP模式后,开启自动心跳检测。
setSn_KPALVTR(sn,2); // 设置心跳包自动发送间隔,单位时间为5s,所以这里设置为10s。为0则不启用。
//等价于
uint8_t t = 2;
setsockopt(sn, SO_KEEPALIVEAUTO, (void*)&t);
同时即可执行TCP数据处理回调函数。
TCPCallback(sn); //回调函数中执行收发任务
回调函数TCPCallback稍后介绍。
disconnect(sn);
但socket的时候内部会先调用一次close函数,所以不需要调用close来关闭socket。
整体程序框架和TCP Server相同,但是注意,在第4.2.2处不在执行监听步骤,而是通过事先设定好的远端IP值,和端口号值。直接与远端服务器建立链接。
printf("PC -gw:%d.%d.%d.%d\r\n", remote_ip[0], remote_ip[1], remote_ip[2], remote_ip[3]);
printf("PC -port:%d\r\n", port);
connect(sn,remote_ip,port); //socket连接服务器
有D友问我这个客户端实验容易出错。我没理解他具体的意思。但是我大概能明白它的疑问。
首先这个实验是基于服务器端IP地址已经确定的状态进行的,在这里我们已经定义了远端服务器的IP地址。
uint8_t remote_ip[4] = {192,168,0,100}; //远端IP“TCP_Client”
因此当我们执行,客户端状态机的时候,就会执行这个链接函数:
connect(sn,remote_ip,port); //socket连接服务器
这个函数包含的信息主要是,socket号,远端IP地址,端口号。
我们打开这个函数看一下:
int8_t connect(uint8_t sn, uint8_t * addr, uint16_t port)
{
CHECK_SOCKNUM();
CHECK_SOCKMODE(Sn_MR_TCP);
CHECK_SOCKINIT();
//M20140501 : For avoiding fatal error on memory align mismatched
//if( *((uint32_t*)addr) == 0xFFFFFFFF || *((uint32_t*)addr) == 0) return SOCKERR_IPINVALID;
{
uint32_t taddr;
taddr = ((uint32_t)addr[0] & 0x000000FF);
taddr = (taddr << 8) + ((uint32_t)addr[1] & 0x000000FF);
taddr = (taddr << 8) + ((uint32_t)addr[2] & 0x000000FF);
taddr = (taddr << 8) + ((uint32_t)addr[3] & 0x000000FF);
if( taddr == 0xFFFFFFFF || taddr == 0) return SOCKERR_IPINVALID;
} //
if(port == 0) return SOCKERR_PORTZERO;
setSn_DIPR(sn,addr);
setSn_DPORT(sn,port);
setSn_CR(sn,Sn_CR_CONNECT);
while(getSn_CR(sn));
if(sock_io_mode & (1<
这个函数执行完之后还要判断一个状态:case SOCK_ESTABLISHED: //socket处于连接建立状态
如果这个socket链接程序没有成功建立链接。其寄存器的状态如下:
如果成功建立链接:即表明其链接请求被成功接收。
所以在实验的时候,我们需要用服务器上位机点击开始监听,便可以成功建立链接。
答: 我将状态机放在了循环里面,这样就会一直去执行建立Socket,只有当我们主机进行监听之后,即服务器的SYN请求包,或者CONNECT命令配置成功时,双方才会建立连接,不然的话客户端状态机就会一直在循环请求建立socket。
void TCPCallback(uint8_t sn)
{
uint8_t len = getSn_RX_RSR(sn); //定义len为已接收数据的长度
if(len>0)
{
recv(sn, (uint8_t *)rxbuf, len); //获取上位机发送指令
if (sn == 0)
{
printf("%s\r\n",rxbuf);
send(sn,rxbuf,len);
}
else if (sn == 1)
{
printf("%s\r\n",rxbuf);
send(sn,rxbuf,len);
}
}
}
int32_t send(uint8_t sn, uint8_t * buf, uint16_t len);
// 描述: 发送数据给TCP socket上连接的对象
// 参数: sn socket号(0-7)
// buf 指向要发送的数据的缓冲区
// len 缓冲区中数据的字节长度
// 返回: 发送的字节长度 如果成功
// 注意:1. 仅在tcp服务器或客户端模式下有效,且无法发送大于socket发送缓冲区大小的数据
void send_str(uint8_t sn, char *str)
{
send(sn, (uint8_t *)str, strlen(str));//发送字符串
}
如往已经建立了tcp链接的socket 1上发送hello world:
char buf[] = "hello world!";
send(1,buf,strlen(buf));
int32_t recv(uint8_t sn, uint8_t * buf, uint16_t len);
// 描述: 接收TCP socket上连接的对象发来的数据
// 参数: sn socket号(0-7)
// buf 指向要接收数据的缓冲区
// len 缓冲区的最大长度
// 返回: 接收的字节长度 如果成功
// 注意:1. 仅在tcp服务器或客户端模式下有效,且无接收大于socket接收缓冲区大小的数据
例如:
如果缓冲区大小比socket接收缓冲区小的话并不能保证一次调用就能接收完所有数据。所以常会循环接收并处理,直到返回值小于等于0。
int32_t len;
uint8_t buf[BUF_SIZE];
while((len = recv(1,buf,BUF_SIZE)) > 0){
// 对刚收到的数据进行处理
}
在socket上的tcp链接成功建立后,可以调用recv函数来获得收到的数据。
void TCPCallback(uint8_t sn)
{
static int count;
getSn_DIPR(sn, remoteConfig[sn].ip);
uint8_t len = getSn_RX_RSR(sn);
recv(sn, (uint8_t *)rxbuf, len); //获取上位机发送指令
if (sn == 0)
{
disconnect(sn);
}
else if (sn == 1)
{
}
}
如需要主动断开TCP链接,或者更多情况下是因为发现socket的状态为SOCK_CLOSE_WAIT半关闭状态,而需要断开连接。则调用disconnect函数:
int8_t disconnect(uint8_t sn);
// 描述: 断开一个连接着的socket
// 参数: sn socket号(0-7)
// 注意:1. 仅在tcp服务器或客户端模式下有效
调用close函数关闭socket:
int8_t close(uint8_t sn);
// 描述: 关闭一个socket
// 参数: sn socket号(0-7)
// 返回: SOCK_OK 如果成功
// SOCKERR_SOCKNUM 无效的socket号