一、简介
1.TCP协议
传输控制协议在LWIP协议栈中占据了大半的代码,它是最常见的传输层协议,也是最稳定的传输层协议,很多上层应用都是依赖 TCP 协议进程传输数据,如 SMTP,FTP 等等。
TCP 与 UDP 一样,都是传输层的协议,但是提供的服务却不相同,UDP 为上层应用提供的是一种不可靠的,无连接的服务,而 TCP 提供一种面向连接,可靠的字节传输服务,TCP 让两个主机建立连接的关系,应用数据以数据流的形式进行传输,先后发出的数据在网络中虽然也是互不干扰的传输,但是这些数据本身携带的信息确实紧密联系的,TCP 协议会给每个传输的字号进行编号,当然了,两个主机方向上的数据编号是彼此独立的,在传输的过程中,发送方把数据的起始编号与长度放在 TCP 报文中,接收方将所有数据按照编号组装起来,然后返回一个确认,当所有的数据接收完成后才将数据递交到应用层。
2.TCP 建立连接
- 客户端发送含 SYN 标志的报文,表示要建立一个连接, 初始化连接的同步序号 seq;
- 服务器端接收到含 SYN 的报文后, 也将一条含 SYN 标志报文, 作为 ACK 回复给客户端, 一则表示收到客户端创建连接的请求,二则表示服务端也同时创建与客户端的连接;
- 客户端收到回复后再发送一条 ACK 报文, 表示收到服务端的创建连接请求。
不论是客户端还是服务端, 创建连接发送数据的时候数据包中都包含一个初始化序列号 seq, 对方收到数据并回复时会将收到的 seq 加 1 作为 ACK 的数据。 这样发送端就可以判断是不是对本次数据的回复。 以后每次发数据都会在前一次 seq 的基础上 +1 作为本次的 seq (注意, 建立连接和断开连接时 seq 是加 1, 传输应用数据时 seq 加的是上次发送的数据长度 )。
tcp_connect实现的,大概流程如下:
服务端连接流程:
3.TCP断开连接
TCP断开连接时需要 4 个步骤来完成断开, 俗称四次挥手:
- 已经建立连接的其中一段想要断开连接,称为主动关闭方。其发送一个含 FIN 标志、seq 值(假设为 Q)和 ACK 值(假设为 K, 来自上次收到的 seq)的报文到连接对方,称为被关闭方;
- 被关闭方收到含 FIN 标志的报文后,将 K 作为 seq 值,Q 加 1 后作为 ACK 值回复给主动关闭方。
- 被关闭方同时检查自身是否还有处理完的数据包,若没有则开始启动关闭操作,身份转变为主动关闭方,同样发送一个含 FIN、seq(值仍为 K)和 ACK 值(值仍为 Q+1)初始化的报文到连接对方。
- 原主动关闭方转变为被关闭方,收到报文后,回复一条 seq 为 Q+1,ACK 为 K+1 的报文。至此,完成 TCP 连接的断开。
正常的TCP断开连接一般是从ESTABLISHED状态开始, 就是说两个网络端已经建立了连接, 断开流程如下:
主动关闭方流程:
被动关闭方流程:
4.TCP 超时重传
TCP 在发起一次传输时会开启一个定时器,如果在定时器超时时未收到应答就会进行重传。一个 TCP 连接的往返时间为 RTT(Round-Trip Time),然后根据 RTT 来设置重传时间就是 RTO(Retransmission TimeOut)。 如果 RTO 较大,由于等待时间过长会影响 TCP 的效率;较小则可能使系统来不及响应而认为数据传输失败而重传。所以 RTO 是随着网络的状态动态调整的,而网络状态可以通过测量报文段的往返时间来反应。
二、ETH基础配置
1.cubemx配置
配置时钟,一般都使用最大时钟频率即可。
配置网口引脚设置,具体如下:
2.board.h配置
/** if you want to use eth you can use the following instructions.
*
* STEP 1, define macro related to the eth
* such as BSP_USING_ETH
*
* STEP 2, copy your eth init function from stm32xxxx_hal_msp.c generated by stm32cubemx to the end if board.c file
* such as void HAL_ETH_MspInit(ETH_HandleTypeDef* heth)
*
* STEP 3, modify your stm32xxxx_hal_config.h file to support eth peripherals. define macro related to the peripherals
* such as #define HAL_ETH_MODULE_ENABLED
*
* STEP 4, config your phy type
* such as #define PHY_USING_LAN8720A
* #define PHY_USING_DM9161CEP
* #define PHY_USING_DP83848C
* STEP 5, implement your phy reset function in the end of board.c file
* void phy_reset(void)
*
* STEP 6, config your lwip or other network stack
*
*/
#define BSP_USING_ETH
#ifdef BSP_USING_ETH
#define PHY_USING_LAN8720A
/*#define PHY_USING_DM9161CEP*/
/*#define PHY_USING_DP83848C*/
#endif
3.rtthread settings配置
(1)勾选以太网驱动配置
(2)勾选网络层驱动
(3)设置控制板IP信息
4.以太网复位引脚设置
/**
* @brief ETH初始化
* @param ethHandle句柄
*/
void HAL_ETH_MspInit(ETH_HandleTypeDef* heth)
{
GPIO_InitTypeDef GPIO_InitStruct = {
0};
if(heth->Instance==ETH)
{
/* USER CODE BEGIN ETH_MspInit 0 */
/* USER CODE END ETH_MspInit 0 */
/* Peripheral clock enable */
__HAL_RCC_ETH_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
/**ETH GPIO Configuration
PC1 ------> ETH_MDC
PA1 ------> ETH_REF_CLK
PA2 ------> ETH_MDIO
PA7 ------> ETH_CRS_DV
PC4 ------> ETH_RXD0
PC5 ------> ETH_RXD1
PB11 ------> ETH_TX_EN
PB12 ------> ETH_TXD0
PB13 ------> ETH_TXD1
*/
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_4|GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_11|GPIO_PIN_12|GPIO_PIN_13;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF11_ETH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
/* USER CODE BEGIN ETH_MspInit 1 */
/* USER CODE END ETH_MspInit 1 */
}
}
//定义的复位引脚
#define RESET_IO GET_PIN(C, 6)
/**
* @brief phy复位引脚初始化
*/
void phy_reset(void)
{
rt_pin_mode(RESET_IO, PIN_MODE_OUTPUT);
rt_pin_write(RESET_IO, PIN_HIGH);
rt_thread_mdelay(50);
rt_pin_write(RESET_IO, PIN_LOW);
rt_thread_mdelay(50);
rt_pin_write(RESET_IO, PIN_HIGH);
}
5.修改rtthread源码
修改 eth_demo\rt-thread\components\drivers\include\drivers\phy.h
文件,注释掉32行。图中 drivers 文件夹下的 drv_eth.c 报错是因为这个变量重定义了。
6.修改 cubemx 生成的 main 函数
此时编译,应该是没有报错能通过的。cubemx中的main函数会用到我们删掉的那两个函数,所有会有警告,不想看可以在main函数中也注释或者删掉。
三、初始化配置
在控制台执行函数中,将广播函数进行调用,从而实现将数据通过网口进行通信。
四、完整代码
1.tcp_server.c
#if 1
#include <rtthread.h>
#include "board.h"
#ifdef BSP_USING_ETH
#include "tcp_server.h"
int server_state_flag = 0;
/**
* @brief 广播数据
* @param buffer 发送的数据
* @param len 数据长度
*/
void broadcast(char *buffer, int len)
{
for(int i=0; i < MAXCLIENT; i++)
{
if(g_client[i] < 0)
continue;
send(g_client[i], buffer, len, 0);
}
}
/**
* @brief 设置服务器参数
*/
static void set_server_parameter(void)
{
struct netdev *netdev = RT_NULL;
ip_addr_t addr;
netdev = netdev_get_by_name("e0");
if (netdev == RT_NULL)
{
rt_kprintf("netdev is null\n");
return;
}
// 设置IP地址
inet_aton(serverInfo.serverIpAddr, &addr);
netdev_set_ipaddr(netdev, &addr);
// 设置网关
inet_aton(serverInfo.serverGateway, &addr);
netdev_set_gw(netdev, &addr);
// 设置子网掩码
inet_aton(serverInfo.serverMask, &addr);
netdev_set_netmask(netdev, &addr);
// 重启控制板
rt_thread_delay(RT_TICK_PER_SECOND / 100);
rt_hw_cpu_reset();
}
/**
* @brief 创建TCP服务器连接的线程
* @param parameter 传入的参数
*/
static void server_thread_entry(void* parameter)
{
int ret, maxFd, i=0;
int optval = 1;
char buff[] = {
0};
int j = 0;
int z = 0;
struct sockaddr_in clientAddr; // 存储连接上来的客户端的IP地址和端口号
struct sockaddr_in serverAddr;
int newClientFd = -1; // 新的套接字
uint32_t len = sizeof