以太网基础知识(了解即可,不要求掌握):
Ethernet (以太网):是目前应用最为广泛的构建计算机局域网的底层技术,遵循IEEE组织的IEEE802.3标准(规定线缆,编码等)
以太网结构:总线结构,和CAN总线本质一样,一根线所有的结点都挂在线上,但只能一个设备的占用总线,交换机就解决总线有限问题,通过交换机就达到数据转发的效果,就发展成了星形拓扑结构,但由于交换机并不是发送和接收数据的从机设备,所以从物理底层上还是总线结构
同时也使用CSMA/CD(载波监听:依靠载波传输信号,发送数据时监听总线是否空闲就可以发送数据,如果存在载波就不空闲,等待发送数据;多路访问:结点挂在总线上都可以访问总线;碰撞检测:多节点同时访问且同时发送数据就会出现冲突,所以就一边发送数据一边监听冲突,一旦发现冲突就停止发送数据,直至监听不冲突,就继续发送数据)
以太网如何兼顾长距离传输大量数据避免干扰:差分信号线,CAN就采用了位时序的保证时钟的一致性但是过于麻烦,而以太网则考虑到了CAN的冗余而在数据的编码表达上做创新:物理层在电缆上用跳变沿表示0或1,我们称之为曼切斯特编码,虽然比传统的电平表示的数据少,

载波:

差分曼切斯特编码:
但是随着对传输速率的追求就不用 曼切斯特编码,但是基于该思路发展出4B,5B....8B
以太网和互联网的区别:
互联网:覆盖全球的网络集合体,主要就实现各个地区的信息交流和资源共享,比如网页浏览,社交媒体
以太网:不是基于应用去定义的网络,就是很底层的局域网,连接几台计算机通信,以太网主要通过网卡上网,就通过网线进行连接,速度快,稳定,成本低,主要实现局部范围的设备互联和资源共享,比如文件共享,打印机共享,内部通信
以太网的技术特点:
采用总线型或星型拓扑结构,使用CSMA/CD或者CSMA/CA等协议来控制网络中的数据传输,传输介质主要有双绞线(568B,568A(被淘汰)),光纤等,不同的传输介质支持不同的传输速率:10Mbps(10Base-T,CAT3类线),100Mbps(CAT5类线),1000Mbps(网卡和网线(CAT6)都需要支持千兆网才能真正构建千兆网络成功)......,以太网的设备通过交换机或集线器连接在一起
应用场景(生活的一部分,及其重要):
以太网主要应用于企业内部网络,学校,医院等局部范围内的网络建设。满足内部设备之间的高速数据传输和资源共享。也是作为接入互联网的一种方式,我们先通过以太网进行连接形成局部通信网络后再通过以太网到路由器或者调制解调器,接入互联网服务供应商的网络,从而实现与互联网连接,那么我们所有用局部以太网连接起来的设备就能上互联网了
以太网层次和OSI7层模型:
以太网采用无源的介质,按广播方式传播信息
以太网通常分为两个主要层次:物理层(底层物理结构:接口向下对接网线,所有它是接口规定了数据编码,时标和电频等,以比特作为传输单位,规定什么形式表示数据0或1,为原始的数据比特流传输提供了一个传输接口向上连接数据链路层)和数据链路层(依据自己的规则包装物理层稳定传输比特流可靠的传给网络层用)
一个结点的网卡分层级:网络层,数据链路层,物理层,

OSI7层模型
数据链路层完成:CSMA/CD等
传输层:经典协议TCP协议,建立高速通道传输即可,底层要是要一层一层往下走,这些网络层不管
在实际使用中不用这么麻烦,这个只是国际协议,会话层和表示层没必要直接包装在应用层就行
所以构建出简单的分层结构:TCP/IP 4层模型,在现实生活中铺开广泛践行了

常见的网络协议:
IP协议:网络层最重要的协议,主要接收数据链路层的数据包给传输层,TCP要传输数据就会给IP打包,但是IP数据包不可靠(不会对数据包做顺序或数据正确性的校验),IP数据包一定要包含源地址和目的地址,为了让它可靠就在上层建立TCP建立连接(三次握手和四次挥手,数据应答,滑动窗口做流量控制(接收能力,避免接收方控不住过载))UDP就是面向无连接的,包括目的端口号和源端口号,源地址和目的地址,因为不需要连接所以就是广播发送数据,也不用数据应答,虽然不可靠但是因为规则限制不多所以速度快,那么就用在视频或者语言等要求快速性的应用
HTTP和HTTPS协议:超文本传输协议
是用于从万维网服务器传输超文本到本地浏览器的传送协议,HTTPS主要是提供网站服务器的身份认证交换资料的隐私
在嵌入式开发中如何搭建以太网,STM32全系列有互联型产品,但是以太网本身就是设备互联的方式,F103系列没有这个以太网模块,所以我们就用外设芯片来实现功能,配一个网卡上网
我们选择W5500(在嵌入式中应用最广泛的芯片,高性价比,全球独一无二的全硬件TCPIP协议栈专利技术,避免我们不停的占用CPU处理TCP IP协议处理,只需要处理上层软件逻辑)
W5500芯片基本介绍:
W5500集成了TCP/IP协议栈,10/100M以太网数据链路层(MAC)及物理层(PHY),使得用户使用单芯片就能够在他们的应用中拓展网络连接。
久经市场考验的WIZnet全硬件TCP/IP协议栈支持TCP、UDP、IPv4、ICMP(网络控制报文协议:负责消息控制,网络通不通),ARP(地址解析协议:获取底层MAC地址)IGMP以及PPPoE协议(建立在以太网的点对点协议,做用户的身份认证等)。W5500内嵌32K字节片上缓存以供以太网包处理(32KB)
内嵌片上缓存供数据包处理-有接收就放置该缓存,用就拿,发送数据如果忙,就放至缓存,不忙就发
使用Socket编程实现以太网应用,支持8个独立端口同时通信,及多进程通信,提供SPI(支持0,3模式)和外设MCU连接。W5500使用新的高效SPI支持80Mhz速率,为了减少系统能耗,还提供了网络唤醒模式(WOL)及掉电模式供用户选择
Socket编程:管理我们所指明给它指明的通信协议,源设备和目的设备ID 和端口号(端口号(软件层):为了做跨电脑的进程间的通信为每一个进程分配对应的端口号资源)所以我们在通信的时候就要带着ID找到设备再根据端口号锁定进程
IPC:进程间的通信(设备内部进程通信,不同的主机间进行进程的通信)
W5500接入框图:

主控芯片和W5500交互:
1.SPI连接
W5500提供了SPI(串行外部接口)作为外设主机接口,有SCSn(低电平有效的片选信号线)、SCLK、MOSI、MISO 共4路信号,且只能作为SPI从机工作。
在W5500中只支持工作模式0和3,在这两种模式下数据总是在SCLK信号的上升沿做数据采样即接收数据,在SCLK信号的下降沿(发送方)做数据的翻转和(接收方)做数据的发送。
MOSI和MISO信号无论是接收或发送,均遵从最高标志位(MSB)到最低标志位(LSB)的传输序列。

W5500控制显示图,两种方式:可变数据长度,即通过控制片选信号实现;而固定数据长度模式,则因为片选信号长期使能所以就一直接收数据。

W5500内部存储器:查数据手册
(1)1个普通寄存器block:这里配置了W5500的一些基本信息,如工作模式,网络配置(IP、MAC地址(写6个字节)、子网掩码等)。

(2)8个Socket寄存器block:这里配置了每个Socket对应的信息,如Socket的模式、命令、状态、中断信息等。
(3)8个Socket对应的接收缓冲寄存器block(共16k):初始时每个Socket分配为2k的缓存,用户可以自己重新通过修改相应的配置寄存器进行修改,但是要保证分配给8个Socket的缓冲大小之和不能超过16k,否则会报错。
(4)8个Socket对应的发送缓冲寄存器block(共16k)。
STM32和W5500硬件电路搭建:

引脚说明:
(1)W5500-RST:重置硬件,重置(Reset)低电平有效;该引脚需要保持低电平至少500us,才能重置W5500;(数据手册给出:正常使用应该高电平,需要重置芯片的时候置为低电平不少500us)。连接的是PG7。因为我们烧写代码每次都只是控制我们STM32,为了控制W5500,我们就在软件初始化那手动给它写个复位
(2)W5500-INT:中断输出(Interrupt output),低电平有效。低电平,W5500的中断生效;高电平,无中断。连接的是PG6。
(3)W5500-CS片选引脚。连接的是PD3
(4)W5500与STM32使用SPI协议进行通讯,连接的是STM32的SPI2外设。
以太网通讯案例1:给W5500搭建网络
需求描述:根据芯片的手册说明驱动W5500芯片,给它设置基本属性(下图黑框起的),给它连到我们电脑同网段的局域网,测试网络是否连通(ping IP4)(ipconfig是查询自己的)

直接给W5500的寄存器写值


和32用SPI连接,用什么指令控制W5500?调用W5500的官方库就行(类似HAL库)
官方库地址:https://gitcode.net/mirrors/Wiznet/ioLibrary_Driver
将Ethernet内部对应芯片给复制到我们针对以太网创建的工程包下(记得到KEIL中导入这些文件,要不然用不了)

关于SPI的配置可以看我专栏内的SPI专题,我这个专题侧重以太网W5500的搭建所以就不将重心向说明SPI倾斜了
我们新建一个文件(eth.c eth.h)写我们调用W5500功能给32的封装函数,至于你要用寄存器方式还是HAL库方式写SPI这个都无所谓,因为调用W5500的底层逻辑都是一样的,这里我就用寄存器方式为例子:
1.芯片选择
在W5500的配置文件了拉到选择芯片类型的地方改成我们选用的芯片类型即W5500

2.配芯片数据长度工作模式,选变长数据模式,就是我们32拉个引脚来软件控制W5500的片选引脚,拉低了W5500就可以收数据了,一旦拉高那么W5500就收不到数据,不就是变长数据了

3.片选使能

这些都是W5500给我们提供的空实现函数,我们要自己填充函数体,看W5500给定函数解释就可以看出是针对片选信号的,就融合我们自己的32和W5500连接引脚给写就行
这我定义在SPI.h里的,给wizchip_conf.h导个头文件然后用上就行。
4.SPI读写数据,这SPI_SwapByte()回看我SPI专题的收发数据函数,如果你自己有封装好的SPI函数也可以用自己的

5.给CRIS,CS,SPI注册回调函数就为了用这结构体变量里的函数。
填充了该函数后并非就能调用了,该芯片在配置.c文件下还定义了一个全局变量内部定义了函数的实现,但没有SPI相关的函数,它这里默认的是bus,所以我们就要给SPI相关的函数做一个注册

我们跳到_WIZCHIP自定义类型看看怎么个事:发现_WIZCHIP内有定义了结构体(CRIS灵临界区,CS片选,IF)这个IF用的union类型就是说下面的多个子结构体变量只能二选一来用,我们还发现的信息是全部结构体变量都是指针函数

官方给的注册SPI回调函数:关于CRIS,CS这些都是异曲同工我这边不展示

我们现在就要调用以上的CRIS,CS,SPI回调函数 ,那我就在这个wizchip_conf.c最后给他们归纳起来,包装成一个函数(方便),你一个一个子啊初始化的时候调用也行,这个看自己
cpp
//我们要子啊eth.c那里初始化的时候调用,所以要在.h那里声明一下,我这边不展示
void user_register_function(void)
{
/*这些参数就是我们上面提到的函数,就是这么用的CRIS,CS的回调函数可有可无,因为官方的结构体定义就默认用的CRIS,CS,所以也能注释掉,但是为了美观我这里就写*/
reg_wizchip_cris_cbfunc(wizchip_cris_enter, wizchip_cris_exit);
reg_wizchip_cs_cbfunc(wizchip_cs_select, wizchip_cs_deselect);
//关键就是SPI要自己单独给它写出来,因为官方默认的调用bus
reg_wizchip_spi_cbfunc(wizchip_spi_readbyte, wizchip_spi_writebyte);
}
8.根据自己板子和W5500的连线自己改成对应的引脚,我这边省略,但是也要关注一下不同芯片的电气要求上有没有频率的要求等,这里说33.3MHz一下的频率芯片数据能确保不失真,所以我们SPI要分频,别给个36MHz了

7.写eth.c和eth.h的代码,调用W5500封装的函数,一般设置都是Set后跟寄存器名字就是个封装函数可以用vscode的代码提示和查找定义定位到W5500文件看看官方给的注释告诉我们怎么用,按要求会用就行
cpp
#ifndef __ETH_H
#define __ETH_H
#include "w5500.h"
#include <stdio.h>
// 初始化
void ETH_Init(void);
#endif
cpp
#include "eth.h"
#include "delay.h"
// 定义W5500的IP地址MAC地址,子网掩码和网关地址
uint8_t ip[4] = {192, 168, 44, 222};
uint8_t mac[6] = {110, 120, 130, 140, 150, 160};
uint8_t submask[4] = {255, 255, 255, 0};
uint8_t gateway[4] = {192, 168, 44, 1};
// 复位W5500
static void ETH_Reset(void);
// 设置MAC 地址
static void ETH_SetMac(void);
// 设置IP ַ地址
static void ETH_SetIP(void);
// 初始化
void ETH_Init(void)
{
// 0. SPI ʼ
SPI_Init();
// 1. 注册回调函数
user_register_function();
// 2. 复位W5500
ETH_Reset();
// 3. 设置 MAC 地址
ETH_SetMac();
// 4. 设置 IP 地址,网关,子网掩码
ETH_SetIP();
}
// 复位W5500
static void ETH_Reset(void)
{
// 1. 复位引脚:RST -PG7
// 1.1 开时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
// 1.2 配工作模式 通用推挽输出 MODE - 11 CNF - 00
GPIOG->CRL |= GPIO_CRL_MODE7;
GPIOG->CRL &= ~GPIO_CRL_CNF7;
// 2. 复位引脚拉低 RST 500us
GPIOG->ODR &= ~GPIO_ODR_ODR7;
Delay_us(800);
GPIOG->ODR |= GPIO_ODR_ODR7;
printf("W5500 复位成功\n");
}
// 设置 MAC ַ地址
static void ETH_SetMac(void)
{
printf("开始设置 MAC地址 ַ:\n");
//官方库包装的函数,看数据收据手册
setSHAR(mac);
printf("MAC 地址设置完成 : %X-%X-%X-%X-%X-%X\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
// W5500的IP地址,子网掩码和网关地址
static void ETH_SetIP(void)
{
printf("开始设置 IP地址 ַ:\n");
// 设置 IP
setSIPR(ip);
// 设置子网掩码
setSUBR(submask);
// 设置网关
setGAR(gateway);
printf("IP 地址设置完成 ַ : %d.%d.%d.%d\n", ip[0], ip[1], ip[2], ip[3]);
}
main.c
cpp
#include "usart.h"
#include "eth.h"
int main(void)
{
USART_Init();
printf("测试网络搭建\n");
ETH_Init();
printf("\n以太网初始化完成\n");
while (1)
{
}
}
最终效果展示就是:我用了串口给我打印测试

以太网通讯案例2:搭建TCP服务器和客户端(经典客户端(电脑)+服务器(32单片机))
案例1我们解决了数据链路层和网络层,那案例2就使用TCP建立一个点对点的连接
在TCP通讯的时候,客户端必须主动联系服务器,这样才能实现通讯。服务器与客户端之间的连接是一种长连接,一旦连上是不会断开的。
互联网常用的架构是BS架构,在自己浏览器里发起请求获取服务器数据就直接在本机浏览器里渲染就行(就像你现在在网页看我的文章一样,或者4399小游戏)
CS架构就是我本机先下载个客户端程序,然后我对远端的服务器发起请求,服务器传回数据,就像手游,手机要下载的那种

我们用串口做测试的时候:在STM32上启动一个TCP的服务端,在电脑上用TCP客户端去连接服务端。客户端给服务端发送数据后,服务端再原封不动的返回给客户端。你要是有别的业务,什么外设状态信息通过以太网的方式控制其他设备的外设等都行,本质就是通过以太网收发数据,然后判断数据,根据不同的数据信息在代码里给外设做处理,从而实现相应功能,就跟我们学的SPI,IIC,USART等都一样就是一种不同的数据传输方式而已
在W5500启动TCP服务端要做的操作就是操作底层寄存器然后我们在写代码的时候用它封装号的函数:涉及到TCP就是涉及端口号,就要用到Socket
自己在工程代码里定义两文件tcp.c,tcp.h
tcp.h
cpp
#ifndef __TCP_H
#define __TCP_H
#include "eth.h"
#include "socket.h"
// 宏定义,我这Socket端口选的0,那想选哪个都一样(0~7),我这里就一例子
#define SN 0
#define CLIENT 0
#define SERVER 1
#define ROLE CLIENT//这角色的宏定义能改主要看后面在主函数中测试选的角色是什么
// 启动TCP服务器
void TCP_ServerStart(void);
// 启动TCP客户端
void TCP_ClientStart(void);
// 接收数据
void TCP_RecvData(uint8_t buff[], uint16_t *len);
// 发送数据
void TCP_SendData(uint8_t data[], uint16_t len);
#endif
1.Socket配置成TCP模式,用上TCP无延时ACK



2.1 32作为服务器:
连接成功后断开连接:
控制位发展:OPEN->LISTEN->(DISCONT)->CLOSE
状态位 的发展:SOCK_INIT->SOCK_LISTEN->SOCK_ESTABLIESHE-> SOCK_CLOSE_WAIT->SOCK_CLOSED
连接不成功:
控制位发展:OPEN->LISTEN->CLOSE
状态位 的发展:SOCK_INIT->SOCK_LISTEN-> SOCK_CLOSED
注意:DISCON和CLOSE不同点在于:DISCON要进行4次挥手,而CLOSE直接简单粗暴给关了
所以在效果上一样,那么我这里写代码就简单粗暴用CLOSE了




tcp.c:通过判断W5500的状态来做下一步操作命令
cpp
// 定义全局变量,保存客户端的IP和端口号
uint8_t clientIP[4];
uint16_t clientPort;
// 启动TCP服务器
void TCP_ServerStart(void)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断当前状态,执行相应操作,进入下一个状态
if (status == SOCK_CLOSED)
{
// 2.1 如果是关闭状态,就打开socket
int8_t n = socket(SN, Sn_MR_TCP, 8080, SF_TCP_NODELAY);
if (n == SN)
{
printf("socket %d 打开成功!\n", SN);
}
else
{
printf("socket %d 打开失败,错误码:%d\n", SN, n);
}
// isSaved = 0;
}
else if (status == SOCK_INIT)
{
// 2.2 如果是INIT状态,就执行监听,启动服务端
int8_t res = listen(SN);
if (res == SOCK_OK)//SOCK_OK就是socket里的宏定义
{
printf("socket %d 监听成功!\n", SN);
}
else
{
printf("socket %d 监听失败,错误码:%d\n", SN, res);
}
// isSaved = 0;
}
// else if (status == SOCK_LISTEN)
// {
// }
else if (status == SOCK_ESTABLISHED)
{
if ( getSn_IR(SN) & Sn_IR_CON )
{
getSn_DIPR(SN, clientIP);
clientPort = getSn_DPORT(SN);
printf("客户端连接成功,IP:%d.%d.%d.%d, Port: %d\n",
clientIP[0], clientIP[1], clientIP[2], clientIP[3], clientPort);
// 清零标志位(写1清0)
setSn_IR(SN, Sn_IR_CON);
}
}
else if (status == SOCK_CLOSE_WAIT)
{
// 2.3 如果是半关闭状态,就直接关闭socket
printf("失去客户端的连接,准备关闭socket并重新打开...\n");
close(SN);
// isSaved = 0;
}
}
2.2 32作为客户端:LISTEN换成CONNECT就行,其他都一样。
连接成功后断开连接:
控制位发展:OPEN->CONNECT->(DISCONT)->CLOSE
状态位 的发展:SOCK_INIT->SOCK_CONNECT->SOCK_ESTABLIESHE->SOCK_CLOSE_WAIT-> SOCK_CLOSED
连接不成功:
控制位发展:OPEN->CONNECT->CLOSE
状态位 的发展:SOCK_INIT->SOCK_CONNECT-> SOCK_CLOSED

tcp.c
cpp
// 定义要连接的服务器IP和端口号
uint8_t serverIP[4] = {192, 168, 44, 53};
uint16_t serverPort = 8888;
// 启动TCP客户端
void TCP_ClientStart(void)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断当前状态,执行相应操作,进入下一个状态
if (status == SOCK_CLOSED)
{
// 2.1 如果是关闭状态,就打开socket,这里写9999,不写8080,因为两端口号不能一样
int8_t n = socket(SN, Sn_MR_TCP, 9999, SF_TCP_NODELAY);
if (n == SN)
{
printf("socket %d 打开成功!\n", SN);
}
else
{
printf("socket %d 打开失败,错误码:%d\n", SN, n);
}
}
else if (status == SOCK_INIT)
{
// 2.2 如果是INIT状态,就连接服务器
int8_t res = connect(SN, serverIP, serverPort);
if (res == SOCK_OK)
{
printf("客户端连接服务器成功!\n");
// 向服务器发送初始消息
TCP_SendData("Hello, this is STM32 TCP Client!", 32);
}
else
{
printf("客户端连接服务器失败!错误码: %d\n", res);
}
}
// else if (status == SOCK_ESTABLISHED)
// {
// }
else if (status == SOCK_CLOSE_WAIT)
{
// 2.3 如果是半关闭状态,就直接关闭socket
printf("失去服务端的连接,准备关闭socket并重新打开...\n");
close(SN);
}
}
3.接收数据:收发数据方一旦建立连接就会发送数据,RECV本质操作是对缓存区的数据进行处理

状态位:



相应位的执行中断函数的条件:

我们可以通过读取该位知道缓存区的数据大小:

cpp
// 接收数据
void TCP_RecvData(uint8_t buff[], uint16_t *len)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是建立连接状态,就接收数据
if (status == SOCK_ESTABLISHED)
{
// // 判断是否保存过,如果没有就从寄存器中提取连接的客户端IP和端口号
// if (isSaved == 0 && ROLE == SERVER)
// {
// getSn_DIPR(SN, clientIP);
// clientPort = getSn_DPORT(SN);
// printf("客户端连接成功,IP:%d.%d.%d.%d, Port: %d\n",
// clientIP[0], clientIP[1], clientIP[2], clientIP[3], clientPort);
// isSaved = 1;
// }
// 根据事件标志位,判断是否接收到数据;如果接收到,就读取到缓冲区
if (getSn_IR(SN) & Sn_IR_RECV)
{
// 清零标志位(写1清0)
setSn_IR(SN, Sn_IR_RECV);
// 读取缓存区里数据长度
*len = getSn_RX_RSR(SN);
// 读取数据,把缓存区的数据喂给buff,也要告诉这函数想获取的数据长度
recv(SN, buff, *len);
}
}
}
// 发送数据
void TCP_SendData(uint8_t data[], uint16_t len)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是建立连接状态,就接收数据
if (status == SOCK_ESTABLISHED)
{
//发送什么数据多长的数据给哪个端口号,就是这函数要做的事, 我们传参数就行
send(SN, data, len);
}
}
4.配置Socket的一些基本参数


完整的tcp.c
cpp
#include "tcp.h"
// uint8_t isSaved;
// 定义全局变量,保存客户端的IP和端口号
uint8_t clientIP[4];
uint16_t clientPort;
// 启动TCP服务器
void TCP_ServerStart(void)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断当前状态,执行相应操作,进入下一个状态
if (status == SOCK_CLOSED)
{
// 2.1 如果是关闭状态,就打开socket
int8_t n = socket(SN, Sn_MR_TCP, 8080, SF_TCP_NODELAY);
if (n == SN)
{
printf("socket %d 打开成功!\n", SN);
}
else
{
printf("socket %d 打开失败,错误码:%d\n", SN, n);
}
// isSaved = 0;
}
else if (status == SOCK_INIT)
{
// 2.2 如果是INIT状态,就执行监听,启动服务端
int8_t res = listen(SN);
if (res == SOCK_OK)
{
printf("socket %d 监听成功!\n", SN);
}
else
{
printf("socket %d 监听失败,错误码:%d\n", SN, res);
}
// isSaved = 0;
}
// else if (status == SOCK_LISTEN)
// {
// }
else if (status == SOCK_ESTABLISHED)
{
if (getSn_IR(SN) & Sn_IR_CON)
{
getSn_DIPR(SN, clientIP);
clientPort = getSn_DPORT(SN);
printf("客户端连接成功,IP:%d.%d.%d.%d, Port: %d\n",
clientIP[0], clientIP[1], clientIP[2], clientIP[3], clientPort);
// 清零标志位(写1清0)
setSn_IR(SN, Sn_IR_CON);
}
}
else if (status == SOCK_CLOSE_WAIT)
{
// 2.3 如果是半关闭状态,就直接关闭socket
printf("失去客户端的连接,准备关闭socket并重新打开...\n");
close(SN);
// isSaved = 0;
}
}
// 定义要连接的服务器IP和端口号
uint8_t serverIP[4] = {192, 168, 44, 53};
uint16_t serverPort = 8888;
// 启动TCP客户端
void TCP_ClientStart(void)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断当前状态,执行相应操作,进入下一个状态
if (status == SOCK_CLOSED)
{
// 2.1 如果是关闭状态,就打开socket
int8_t n = socket(SN, Sn_MR_TCP, 9999, SF_TCP_NODELAY);
if (n == SN)
{
printf("socket %d 打开成功!\n", SN);
}
else
{
printf("socket %d 打开失败,错误码:%d\n", SN, n);
}
}
else if (status == SOCK_INIT)
{
// 2.2 如果是INIT状态,就连接服务器
int8_t res = connect(SN, serverIP, serverPort);
if (res == SOCK_OK)
{
printf("客户端连接服务器成功!\n");
// 向服务器发送初始消息
TCP_SendData("Hello, this is STM32 TCP Client!", 32);
}
else
{
printf("客户端连接服务器失败!错误码: %d\n", res);
}
}
// else if (status == SOCK_ESTABLISHED)
// {
// }
else if (status == SOCK_CLOSE_WAIT)
{
// 2.3 如果是半关闭状态,就直接关闭socket
printf("失去服务端的连接,准备关闭socket并重新打开...\n");
close(SN);
}
}
// 接收数据
void TCP_RecvData(uint8_t buff[], uint16_t *len)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是建立连接状态,就接收数据
if (status == SOCK_ESTABLISHED)
{
// // 判断是否保存过,如果没有就从寄存器中提取连接的客户端IP和端口号
// if (isSaved == 0 && ROLE == SERVER)
// {
// getSn_DIPR(SN, clientIP);
// clientPort = getSn_DPORT(SN);
// printf("客户端连接成功,IP:%d.%d.%d.%d, Port: %d\n",
// clientIP[0], clientIP[1], clientIP[2], clientIP[3], clientPort);
// isSaved = 1;
// }
// 根据事件标志位,判断是否接收到数据;如果接收到,就读取到缓冲区
if (getSn_IR(SN) & Sn_IR_RECV)
{
// 清零标志位(写1清0)
setSn_IR(SN, Sn_IR_RECV);
// 读取数据长度
*len = getSn_RX_RSR(SN);
// 读取数据
recv(SN, buff, *len);
}
}
}
// 发送数据
void TCP_SendData(uint8_t data[], uint16_t len)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是建立连接状态,就接收数据
if (status == SOCK_ESTABLISHED)
{
send(SN, data, len);
}
}
main.c
cpp
#include "usart.h"
#include "eth.h"
#include "tcp.h"
// 定义全局变量,接收数据缓冲区和长度
uint8_t rxBuff[1024];//给1024是因为Socket默认就是2K(1024B)
uint16_t rxLen;
int main(void)
{
// 1. 初始化
USART_Init();
printf("尚硅谷以太网实验:测试网络搭建\n");
ETH_Init();
printf("\n以太网初始化完成!\n");
/*循环判断32以太网的状态,不停根据状态做出对应的命令,不停的接收数据,有数据就原封不动的给发回去,你管我什么身份,本质都一样都要收发数据,所以主函数的差异就一个身份替换的差别*/
while (1)
{
TCP_ServerStart();
//TCP_ClientStart();
TCP_RecvData(rxBuff, &rxLen);
// 判断如果长度大于0,表示读取到数据,就原样发回去
if (rxLen > 0)
{
printf("收到数据:%.*s\n", rxLen, rxBuff);
TCP_SendData(rxBuff, rxLen);
// 数据长度清0
rxLen = 0;
}
}
}
效果:作为服务器时:那个COM5的是32,用串口助手测试,远程得写服务器的IP和端口号
要是按那个断开连接我们32就会给出相应信息,就我们软件代码里处理的部分

作为客户端以此类推。
以太网通讯案例2:UDP通讯
UDP是一种面向无连接的协议,一方发出,如果另外一方正在监听端口,则可以接收到,否则数据就会丢失。UDP没有客户端和服务器的区分,因为压根没有建立连接的过程,就相互发数据就行,所以每次设备通过UDP的方式接收到别人的数据都会返回来源的IP和端口号(这要区别于TCP啊,比如我们设备通过TCP和别人设备建立连接就知道别人设备的ID和端口号了,而且因为不会轻易断开连接,所以也不用一直给我们设备发它IP和端口号信息)
1.初始化UDP:判断设备Socket有没有开,没开就初始化后打开





2.打开后,判断下状态,看看是不是UDP的状态


3.如果接收到数据就给放到我们定义的数组里
3.1判断有无数据

3.2判断数据长度:

3.3接收数据


3.5给接收到数据的标志位写1清零,好接收下一次数据
4.如果我们要发送数据就直接判断下是不是UDP状态,就直接给发了就行和TCP的思路一样的。
udp.h
cpp
#ifndef __UDP_H
#define __UDP_H
#include "eth.h"
#include "socket.h"
// 宏定义
#define SN 0
// 启动UDP模式socket
void UDP_Start(void);
// 接收数据,需要增加远程的IP和端口号
void UDP_RecvData(uint8_t buff[], uint16_t *len, uint8_t *srcIP, uint16_t *srcPort);
// 发送数据
void UDP_SendData(uint8_t data[], uint16_t len, uint8_t *dstIP, uint16_t dstPort);
#endif
udp.c
cpp
#include "udp.h"
// 启动UDP模式socket
void UDP_Start(void)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断当前状态,执行相应操作,进入下一个状态
if (status == SOCK_CLOSED)
{
// 2.1 如果是关闭状态,就打开socket
int8_t n = socket(SN, Sn_MR_UDP, 9999, 0);
if (n == SN)
{
printf("socket %d 打开成功!\n", SN);
}
else
{
printf("socket %d 打开失败,错误码:%d\n", SN, n);
}
}
}
// 接收数据
void UDP_RecvData(uint8_t buff[], uint16_t *len, uint8_t *srcIP, uint16_t *srcPort)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是UDP状态,就接收数据
if (status == SOCK_UDP)
{
// 根据事件标志位,判断是否接收到数据;如果接收到,就读取到缓冲区
if (getSn_IR(SN) & Sn_IR_RECV)
{
// 清零标志位(写1清0)
setSn_IR(SN, Sn_IR_RECV);
// 读取数据长度,包含了8字节的首部
uint16_t tmp = getSn_RX_RSR(SN);
/* 判断如果tmp > 8,表示接收到数据,因为UPD每次收数据会先把发来8个字节的首部,后面才是数据*/
if (tmp > 8)
{
*len = tmp - 8;
// 读取数据,每次设备通过UDP的方式接收到别人的数据都会返回来源的IP和端口号
recvfrom(SN, buff, *len, srcIP, srcPort);
}
}
}
}
// 发送数据
void UDP_SendData(uint8_t data[], uint16_t len, uint8_t *dstIP, uint16_t dstPort)
{
// 1. 获取socket状态
uint8_t status = getSn_SR(SN);
// 2. 判断如果是UDP状态,就发送数据
if (status == SOCK_UDP)
{
sendto(SN, data, len, dstIP, dstPort);
printf("发送完毕!数据: %.*s\n", len, data);
}
}
main.c
cpp
#include "usart.h"
#include "eth.h"
#include "udp.h"
// 定义全局变量,接收数据缓冲区和长度
uint8_t rxBuff[1024];
uint16_t rxLen;
// 对端IP和端口号
uint8_t srcIP[4];
uint16_t srcPort;
int main(void)
{
// 1. 初始化
USART_Init();
printf("UDP传输数据\n");
ETH_Init();
printf("\n以太网初始化完成!\n");
while (1)
{
UDP_Start();
UDP_RecvData(rxBuff, &rxLen, srcIP, &srcPort);
// 判断如果长度大于0,表示读取到数据,就原样发回去
if (rxLen > 0)
{
printf("收到数据:%.*s\n", rxLen, rxBuff);
UDP_SendData(rxBuff, rxLen, srcIP, srcPort);
// 数据长度清0
rxLen = 0;
}
}
}
32是通过交换机和我们电脑连接在一起的,又因为UDP不是面向连接的,所以挺容易在串口测试的时候因为丢失数据所以电脑端或者32端接收不到,那我们可以用个网线把32和电脑直接给连起来形成一个局域网:
1.在自己电脑上找到以太网高级设置:DNS我是用的公用的,你也可以写家用的以太网DNS
2.用网线把32和电脑连起来,用串口测试通讯就行
以太网通讯案例4:简易的Web服务器
通过网页控制灯的开与关。
白话逻辑:利用以太网封装的函数,给该函数喂你设计好的html网页代码,那么就会生成一个网页地址,我们就可以在网站上搜索创建好的网页地址,看网站上有什么操作,比如html设计了开关灯的操作,那么我们点击开灯网页就会通过以太网给我们单片机传递参数,单片机判断参数后就会执行封装好的代码
实现思路:
(1)在STM32上开启一个http服务器,首页地址 http://192.168.11.222:80/index.html
192.168.11.222:80:是我们给32单片机的以太网喂的IP地址
(2)当点击开灯按钮的时候,传递参数:http://192.168.11.222:80/index.html?action=1
?action=1:?后面跟的是我们在网页中操作的行为,那么电脑就会将行为对用的参数返回给32单片机,注意还是该网页这是信号发生了改变,你可以理解为网页套网页
(3)当点击关灯按钮的时候,传递参数:http://192.168.11.222:80/index.html?action=2
(4)收到不同的参数执行不同的操作。

我们平常打开网站头上就有个网址,就是和这服务器建立连接的过程,然后网址给我们反馈个html文件
-

代码封装,你要在W5500的封装的文件里找到,包装到我们工程文件夹里

html
//就是我们创建的html网页代码
uint8_t content[] = "<!doctype html>\n"
"<html lang=\"en\">\n"
"<head>\n"
" <meta charset=\"GBK\">\n"
" <meta name=\"viewport\"\n"
" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0\">\n"
" <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n"
" <title>以太网开关灯实验测试</title>\n"
"\n"
" <style type=\"text/css\">\n"
" #open_red{\n"
" color: red;\n"
" width: 100px;\n"
" height: 40px;\n"
"\n"
"\n"
" }\n"
" #close_red{\n"
" color: black;\n"
" width: 100px;\n"
" height: 40px;\n"
" }\n"
" </style>\n"
"</head>\n"
"<body>\n"
"<a href=\"/index.html?action=1\"><button id=\"open_red\" >����</button></a>\n"
"<a href=\"/index.html?action=2\"><button id=\"close_red\" >�ص�</button></a>\n"
"<a href=\"/index.html?action=3\"><button id=\"close_red\" >��ת</button></a>\n"
"</body>\n"
"</html>";
cpp
#include "httpSever.h"
//有多个客户端都等打开网页进行操作,所以要开多个端口Sorket
void WebServer_Init(void);
以太网初始化:
cpp
uint8_t txBuff[2048] = {0};
uint8_t rxBuff[2048] = {0};
uint8_t socketCount = 8;
uint8_t socketList[] = {0,1,2,3,4,5,6,7};
uint8_t *contentName = "index.html";
void WebServer_Init(void)
{
// 2.这里就是网页创建的函数
//txBuff:发送给客户端的数据寄存到该数组中
//rxBuff:将从客户端处接收到的数据存入该数组中
//socketCount:表示同时处理的Socket连接数,符合W5500硬件特性(通常支持4-8个独立Socket)
//socketList:作为端口数组传递是合理设计,可能用于存储Socket状态(如端口号、连接状态)
httpServer_init(txBuff, rxBuff, socketCount, socketList);
// 3. 是一个用于注册HTTP服务器动态内容的函数,内容的标识符或路径
//(如"/index.html"、"/api/data"),用于匹配客户端请求的URL
/*实际返回的内容,可能是:
- HTML/CSS/JS代码片段
- JSON数据(如API响应)
- 文件路径(如图片、配置文件)
- 二进制数据(如固件更新包)*/
reg_httpServer_webContent(contentName, content);
}
启动
cpp
void WebServer_Start(void)
{
for (uint8_t i = 0; i < sizeof(socketList); i++)
{
httpServer_run(i);
}
}
操作函数
找到httpServer_run的定义器内部的处理状态

cpp
void handler_user_function(uint8_t *url)
{
//查找子字符串的位置
uint8_t *pAction = (uint8_t *)strstr((char *)url, "action=");
if (pAction == NULL)
{
return '0';
}
else
{
//就要=后面的值
return *(pAction + 7);
}
//LED执行
if (action == '1')
{
LED_On(LED_2);
}
else if (action == '2')
{
LED_Off(LED_2);
}
else if (action == '3')
{
LED_Toggle(LED_2);
}
}