前言
本文是学习之余的记录,后续内容有时间补充
代码:git地址
一、LWIP配置
1.1 Project Manager
填写名称和选择路径,然后IDE选择MDK-ARM
Code Generator下选择.c和.h文件分开编译
1.2 基本外设选择
可以自行添加其他外设,本文主要进行基本配置
1.2.1 SYS与RCC
sys选择SWD接口
1.2.1 ETH
选择RMII,后进行ETH对应的GPIO配置
GPIO Settings 9个引脚一个一个对比,下图是正常情况下的配置,根据自己开发板的原理图修改对应配置
/* RMII引脚配置(一般是7个引脚) */
RMII_TXD0 PB12 //发送数据位0
RMII_TXD1 PB13 //发送数据位1
RMII_TX_EN PB11 //发送数据的有效信号
RMII_RXD0 PC4 //接收数据位0
RMII_RXD1 PC5 //接收数据位1
RMII_CRS_DV PA7 //接收数据的有效信号
RMII_REF_CLK PA1 //参考时钟
/* 两个SMI引脚 */
MDC PC1 //为PHY芯片提供参考信号
MDIO PA2 //MCU与PHY芯片之间传输数据
下图是我自己开发板(STM32F407ZGT6)的配置
1.2.3 USART
串口主要用来测试使用,尽量配置一下(点击配置为异步模式即可)
1.3 LWIP
配置其PHY芯片,大部分单片机都是LAN8742,芯片是LAN后缀不是8742的也可以选择LAN8742
1.失能DHCP
2.配置IP
IP_ADDRESS 单片机IP
NETMAS 子网掩码
GATEWAY 路由IP
配置完上文的就可以生成keil文件然后打开
1.4 keil与电脑IP
在usart.c下加入头文件和串口重定向代码,然后再到main函数中添加打印测试代码
#include <stdio.h>
//串口重定向代码
int fputc(int ch, FILE *f)
{
while ((USART1->SR & 0x40) == 0);
USART1->DR = (uint8_t) ch;
return ch;
}
假如电脑网络连接显示如下,则表明需要在软件上进行复位操作,如果显示有连接则直接进行下一步
以太网未识别到网线
查询原理图中ETH_RESET对应的引脚(我的板子是PE2),在STM32CubeMX和keil中的配置如下所示:
//软件复位操作---ETH_RESET
HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_RESET);
HAL_Delay(50);
HAL_GPIO_WritePin(ETH_RESET_GPIO_Port, ETH_RESET_Pin, GPIO_PIN_SET);
以太网识别到网络电缆显示未识别的网络或者其他内容
配置以太网的IPV4与单片机的IP在用一个网段即可,上文配置单片机IP为192.168.1.100
main.c的主循环中加入这句MX_LWIP_Process(); 然后下载程序到单片机中。win+R输入cmd,在窗口下ping单片机的IP,成功则表示LWIP配置成功
1.4 其他补充
//加快烧录速度
Debug //生成可执行文件
Create //生成HEX文件
Browse //生成调试信息用于跳转
二、LWIP实现
2.1 TCP回响服务器
1.移植tcp_echoserver.c和tcp_echoserver.h文件到程序文件中
2.加入函数tcp_echoserver_init();到主函数中初始化中(需要先添加头文件)
3.打开串口调试工具,因为单片机是作为TCP Server端,所以电脑作为TCP Client端,配置IP和端口号
4.发送消息回传一样的,则TCP回响实现成功
2.2 加入Free RTOS
sys的时基选择TIM1
选择CMSIS_V1,然后配置栈空间为1024`在这里插入代码片`
配置完后,ETH的Rx Mode会自动变为Interrupt Mode,中断也会打开在这里插入代码片
2.3 TCP_Server
1.单片机socket_tcp_server.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vTCPServer_Task();
3.打开电脑端调试助手--选择TCP Client ip和端口选择代码设置的
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为服务器 IP:192.168.1.100 端口3333
* 电脑端网络调试助手选择TCP Client,让其连接服务器(单片机)
* 所以此时电脑端的IP和端口号则和单片机相同
*/
/* socket_tcp_server.c */
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>
char ReadBuff[BUFF_SIZE];
/* TCP服务器端(server)任务 */
void vTCPServer_Task(void)
{
int sfd, cfd;
int read_len; // 读的数据长度
struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
socklen_t client_addr_len;
// 创建socket
sfd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
// 绑定socket
bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值
// 监听socket
listen(sfd, 5); // 文件描述符 握手队列总和
// 等待客户端连接
client_addr_len = sizeof(client_addr);
cfd = accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len); // 文件描述符 IP地址加端口号 返回值
printf("client is connect cfd = %d
", cfd);
while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
{
read_len = read(cfd, ReadBuff, BUFF_SIZE);
for (int i = 0; i < read_len; i++)
{
ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
}
write(cfd, ReadBuff, read_len);
}
}
/* socket_tcp_server.h */
#ifndef __SOCKET_TCP_SERVER_H
#define __SOCKET_TCP_SERVER_H
#define SERVER_PORT 3333 // 端口号
#define BUFF_SIZE 1024
#define SERVER_IP "192.168.1.100"
void vTCPServer_Task(void);
#endif
void StartDefaultTask(void const * argument)
{
/* init code for LWIP */
MX_LWIP_Init();
/* USER CODE BEGIN StartDefaultTask */
//运行成功
puts("lwip and freertos init is ok");
/* Infinite loop */
for(;;)
{
vTCPServer_Task(); //加入任务的代码
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
运行成功显示如下:
2.4 TCP_Client
1.单片机socket_tcp_client.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vTCPClient_Task();
3.打开电脑端调试助手--选择TCP Server ip和端口选择与电脑网线连接的IPV4
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为客户端,所以要选启动电脑端的网络调试助手
* 选择TCP Server 端口选择代码中设置的,IP选择单片机与电脑相连接的以太网的IPV4
* 单片机作为客户端 IP:192.168.1.100 端口3333
* 代码中提前写好了单片机需要连接的客户端IP和端口号,上电单片机会自动连接
*/
/* socket_tcp_client.c */
#include "socket_tcp_client.h"
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>
static char ReadBuff[BUFF_SIZE];
/* TCP客户端端(client)任务 */
void vTCPClient_Task(void)
{
int cfd;
int read_len; // 读的数据长度
struct sockaddr_in server_addr; // 服务器结构体
// 创建socket
cfd = socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机IPV4
// 连接到服务器
connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 ip地址和端口号 大小
printf("server is connect ok");
printf("server is connect cfd = %d
", cfd);
while (1)
{
read_len = read(cfd, ReadBuff, BUFF_SIZE); // 等待服务器发送数据
for (int i = 0; i < read_len; i++)
{
ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
}
write(cfd, ReadBuff, read_len); // 再写回服务器
}
}
/* socket_tcp_client.h */
#ifndef __SOCKET_TCP_CLIENT_H
#define __SOCKET_TCP_CLIENT_H
void vTCPClient_Task(void);
#endif
void StartDefaultTask(void const * argument)
{
/* init code for LWIP */
MX_LWIP_Init();
/* USER CODE BEGIN StartDefaultTask */
//运行成功
puts("lwip and freertos init is ok");
/* Infinite loop */
for(;;)
{
vTCPClient_Task(); //加入任务的代码
osDelay(1);
}
/* USER CODE END StartDefaultTask */
}
运行成功显示如下:
2.5 问题改进
此时对于单片机TCP连接的客户端和服务端都可以实现,但是代码中并没有处理好,开始连接和断开连接等细节问题
我做的处理是封装一个文件用于进行处理
下文分别是:
socket_wrap.c socket_wrap.h ---封装的代码
socket_tcp_server.c socket_tcp_server.h ---改进后server代码
socket_tcp_client.c socket_tcp_client.h ---改进后client代码
/* socket_wrap.c */
#include "socket_wrap.h"
/**
* @brief 创建套接字
* @param domain: 协议族 选择IPV4或IPV6
* @param type: 协议类型 选择TCP、UDP或RAW
* @param protocol: 协议版本 一般为0
* @retval 失败: -1(申请创建套接字失败)
* 成功: 非负整数(socket描述符可以链接到lwip_sock)
*/
int My_Socket(int domain, int type, int protocol)
{
int fd; // socket的返回值
fd = socket(domain, type, protocol);
// 当返回值为-1,大概率是lwip的内存不够
if (fd == -1)
{
puts("create socket error");
vTaskDelete(NULL); // 删除自身进行任务调度
}
return fd;
}
/**
* @brief 绑定套接字
* @param s: 要绑定的socket套接字
* @param name: 指向sockaddr的结构体指针(包含网卡IP、端口号等)
* @param namelen: name结构体长度
* @retval 失败: -1
* 成功: 0
*/
int My_Bind(int s, const struct sockaddr *name, socklen_t namelen)
{
int ret;
ret = bind(s, name, namelen);
// 绑定失败
if (ret == -1)
{
puts("bind socket error");
vTaskDelete(NULL); // 删除自身进行任务调度
}
return ret;
}
/**
* @brief 在客户端中向服务器建立连接
* @param s: 要连接的socket套接字
* @param name: 指向sockaddr的结构体指针(包含网卡IP、端口号等)
* @param namelen: name结构体长度
* @retval 失败: -1
* 成功: 0
*/
int My_Connect(int s, const struct sockaddr *name, socklen_t namelen)
{
int ret;
ret = lwip_connect(s, name, namelen);
// 连接失败
if (ret == -1)
{
puts("connect socket error");
close(s); // 连接失败关闭当前的socket(内部会自动删除socket内存块)
}
return ret;
}
/**
* @brief 监听套接字
* @param s: 要监听的socket
* @param backlog: 请求队列的大小
* @retval 失败: -1
* 成功: 0
*/
int My_Listen(int s, int backlog)
{
int ret;
ret = listen(s, backlog);
// 监听失败
if (ret == -1)
{
puts("listen socket error");
vTaskDelete(NULL); // 删除自身进行任务调度
}
return ret;
}
/**
* @brief 等待客户端连接请求
* @param s: 请求的socket
* @param addr: 绑定的客户端地址、端口号等信息
* @param addrlen: 地址结构体长度
* @retval 失败: -1
* 成功: 非负整数(socket描述符可以链接到lwip_sock)
*/
int My_Accept(int s, struct sockaddr *addr, socklen_t *addrlen)
{
int fd;
again_accept:
// 阻塞函数,错误返回或者连接成功返回
fd = accept(s, addr, addrlen);
// 客户端连接错误
if (fd == -1)
{
puts("accept connect error");
goto again_accept; // 重新请求连接
}
return fd;
}
/**
* @brief 向套接字发送数据
* @param s: 发送数据到socket
* @param data: 发送的缓冲区
* @param size: 发送数据大小(单位字节)
* @retval 失败: -1
* 成功: 返回一个新的套接字描述符(用于和已连接的客户端通信)
*/
int My_Write(int s, const void *data, size_t size)
{
int ret;
ret = write(s, data, size);
// 发送失败(socket错误或者对方关闭连接)
if (ret < 0)
{
puts("write data to socket error");
close(s);
}
return ret;
}
/**
* @brief 向套接字读取数据
* @param s: 接收数据的socket
* @param data: 接受的缓冲区
* @param size: 接收数据大小(单位字节)
* @retval 失败: -1
* 成功: 返回已经接收的数据长度
*/
int My_Read(int s, void *mem, size_t len)
{
int ret;
ret = read(s, mem, len);
// 读取失败(socket错误或者对方关闭连接)
if (ret <= 0)
{
puts("read data to socket error");
close(s);
}
return ret;
}
/* socket_wrap.h */
#ifndef __SOCKET_WRAP_H
#define __SOCKET_WRAP_H
#include "lwip/sockets.h"
#include "FreeRTOS.h"
#include "task.h"
int My_Socket(int domain, int type, int protocol); //创建套接字
int My_Bind(int s, const struct sockaddr *name, socklen_t namelen); //绑定套接字
int My_Connect(int s, const struct sockaddr *name, socklen_t namelen); //在客户端中向服务器建立连接
int My_Listen(int s, int backlog); //监听套接字
int My_Accept(int s, struct sockaddr *addr, socklen_t *addrlen); //等待客户端连接请求
int My_Write(int s, const void *data, size_t size); //向套接字发送数据
int My_Read(int s, void *mem, size_t len); //向套接字读取数据
#endif
/* socket_tcp_server.c */
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>
char ReadBuff[BUFF_SIZE];
/* TCP服务器端(server)任务 */
void vTCPServer_Task(void)
{
int sfd, cfd;
int read_len; // 读的数据长度
int write_len;
struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
socklen_t client_addr_len;
// 创建socket
sfd = My_Socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
// 绑定socket
My_Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值
// 监听socket
My_Listen(sfd, 5); // 文件描述符 握手队列总和
// 等待客户端连接
client_addr_len = sizeof(client_addr);
again:
cfd = My_Accept(sfd, (struct sockaddr *)&client_addr, &client_addr_len); // 文件描述符 IP地址加端口号 返回值
printf("client is connect cfd = %d
", cfd);
while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
{
read_len = My_Read(cfd, ReadBuff, BUFF_SIZE);
if (read_len == -1)
{
goto again;
}
for (int i = 0; i < read_len; i++)
{
ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
}
write_len = My_Write(cfd, ReadBuff, read_len);
if (write_len == -1)
{
goto again;
}
}
}
/* socket_tcp_server.h */
#ifndef __SOCKET_TCP_SERVER_H
#define __SOCKET_TCP_SERVER_H
#include "socket_wrap.h"
#define SERVER_PORT 5555 // 端口号
#define BUFF_SIZE 1024
/* 单片机作为客户端需要连接服务器,这是服务器的IP(socker_tcp_client被调用) */
#define SERVER_IP "192.168.1.10"
void vTCPServer_Task(void);
#endif
/* socket_tcp_client.c */
#include "socket_tcp_client.h"
#include "socket_tcp_server.h"
#include "lwip/sockets.h"
#include <ctype.h>
static char ReadBuff[BUFF_SIZE];
/* TCP客户端端(client)任务 */
void vTCPClient_Task(void)
{
int cfd;
int read_len; // 读的数据长度
int write_len; // 写的数据长度
struct sockaddr_in server_addr; // 服务器结构体
int ret; // 连接状态
again:
// 创建socket
cfd = My_Socket(AF_INET, SOCK_STREAM, 0); // IPV4 socker连接 默认协议
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 本机IPV4
// 连接到服务器
ret = My_Connect(cfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 ip地址和端口号 大小
if (ret == -1)
{
vTaskDelay(100); // 100ms连接一次服务器
goto again;
}
printf("server is connect ok");
printf("server is connect cfd = %d
", cfd);
while (1)
{
read_len = My_Read(cfd, ReadBuff, BUFF_SIZE); // 等待服务器发送数据
if (read_len == -1)
{
goto again;
}
for (int i = 0; i < read_len; i++)
{
ReadBuff[i] = toupper(ReadBuff[i]); // 小写转换为大写
}
write_len = My_Write(cfd, ReadBuff, read_len); // 再写回服务器
if (write_len == -1)
{
goto again;
}
}
}
/* socket_tcp_client.h */
#ifndef __SOCKET_TCP_CLIENT_H
#define __SOCKET_TCP_CLIENT_H
void vTCPClient_Task(void);
#endif
2.6 UDP_Server
1.单片机socket_wrap.c(添加封装的读写代码)和socket_udp_server.c代码编写
2.在freeRTOS的任务代码中加入TCP的代码函数vUDPServer_Task();
3.打开电脑端调试助手--选择UDP ip选择与单片机同一网段的,端口随意写
4.发送小写转换为大写则服务端代码成功
/* 此时单片机作为UDP服务器 IP:192.168.1.100 端口3333
*/
/**
* @brief 发送数据到指定地址
* @param s: 发送数据的套接字
* @param data: 发送数据的起始地址
* @param size: 数据长度
* @param flags: 发送时的处理(默认为0)
* @param to: 远端主机的IP和端口号
* @param tolen: 信息长度
* @retval 失败: -1
* 成功: 返回已经发送的数据长度
*/
int My_Sendto(int s, const void *data, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen)
{
int ret;
again_sendto:
ret = sendto(s, data, size, flags, to, tolen);
if (ret == -1)
{
puts("sendto socket error");
goto again_sendto;
}
return ret;
}
/**
* @brief 接收数据到指定地址
* @param s: 接收数据的套接字
* @param mem: 接收数据的缓冲起始地址
* @param len: 数据长度
* @param flags: 接收时的处理(默认为0)
* @param from: 远端主机的IP和端口号
* @param fromlen: 信息长度
* @retval 失败: -1
* 成功: 返回已经接收的数据长度
*/
int My_Recvfrom(int s, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen)
{
int ret;
again_recvfrom:
ret = recvfrom(s, mem, len, flags, from, fromlen);
if (ret == -1)
{
puts("sendto socket error");
goto again_recvfrom;
}
return ret;
}
/* socket_udp_server.c */
#include "socket_udp_server.h"
#include "socket_tcp_server.h"
#include <ctype.h>
char UDPServer_ReadBuff[BUFF_SIZE];
/* UDP服务器端(server)任务 */
void vUDPServer_Task(void)
{
int sfd;
int read_len; // 读的数据长度
struct sockaddr_in server_addr, client_addr; // 服务器和客户端的结构体
socklen_t client_addr_len;
// 创建socket
sfd = My_Socket(AF_INET, SOCK_DGRAM, 0); // IPV4 通信方式 版本(默认0)
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 从本机获取
// 绑定socket
My_Bind(sfd, (struct sockaddr *)&server_addr, sizeof(server_addr)); // 文件描述符 IP地址加端口号 addr长度 返回值
client_addr_len = sizeof(client_addr);
// 等待客户端连接
while (1) // 等待客户端发送数据,然后进行大小写转换,再写回客户端
{
read_len = My_Recvfrom(sfd, UDPServer_ReadBuff, BUFF_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len);
for (int i = 0; i < read_len; i++)
{
UDPServer_ReadBuff[i] = toupper(UDPServer_ReadBuff[i]); // 小写转换为大写
}
My_Sendto(sfd, UDPServer_ReadBuff, BUFF_SIZE, 0, (struct sockaddr *)&client_addr, client_addr_len);
}
}
/* socket_udp_server.h */
#ifndef __SOCKET_UDP_SERVER_H
#define __SOCKET_UDP_SERVER_H
void vUDPServer_Task(void);
#endif