物联网通信之以太网通讯—ETH

1 基本介绍

1.1 OSI理论模型

1.2 常见的网络协议

1.2.1 IP 协议

IP层接收由更低层(网络接口层例如以太网设备驱动程序)发来的数据包,并把该数据包发送到更高层—TCP或UDP层;相反,IP层也把从TCP或UDP层接收来的数据包传送到更低层。IP数据包是不可靠的,因为IP并没有做任何事情来确认数据包是否按顺序发送的或者有没有被破坏,IP数据包中含有发送它的主机的地址(源地址)和接收它的主机的地址(目的地址)。

1.2.2 TCP协议

TCP是面向连接的通信协议,通过三次握手建立连接,通讯完成时要拆除连接,由于TCP是面向连接的所以只能用于端到端的通讯。

TCP提供的是一种可靠的数据流服务,采用“带重传的ACK”技术来实现传输的可靠性。TCP还采用一种称为“滑动窗口”的方式进行流量控制,所谓窗口实际表示接收能力,用以限制发送方的发送速度。

1.2.3 UDP协议

UDP是面向无连接的通讯协议,UDP数据包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送

UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象,实际应用中要求程序员编程验证。

UDP与TCP位于同一层,但UDP不管数据包的顺序、错误或重发。

2 以太之王 —- W5500芯片

  • 是韩国半导体公司WIZnet提供的一款高性价比的以太网芯片。全球独一无二的全硬件TCPIP协议栈专利技术。支持 TCP,UDP,IPv4,ICMP,ARP,IGMP以及PPPoE协议。W5500内嵌32K字节片上缓存以供以太网包处理。
  • 用户可以同时使用8个硬件Socket独立通讯。 W5500 提供了SPI(外设串行接口)从而能够更加容易与外设 MCU 整合。W5500的使用了新的高效SPI协议支持80MHz 速率。
  • 2.1 W5500特点

    官方网址:https://www.w5500.com/index.html

    里面有数据手册。

    (1)支持硬件TCP/IP协议:TCP,UDP,ICMP,IPv4,ARP,IGMP,PPPoE。

    (2)支持8个独立端口(Socket)同时通讯。

    (3)支持掉电模式。

    (4)支持网络唤醒。

    (5)支持高速串行外设接口(SPI模式 0,3)。

    (6)内部32K字节收发缓存。

    (7)内嵌10BaseT/100BaseTX 以太网物理层(PHY)。

    (8)支持自动协商(10/100-Based 全双工/半双工)。

    (9)不支持 IP 分片。

    (10)3.3V工作电压,I/O 信号口5V耐压。

    (11)LED状态显示(全双工/半双工,网络连接,网络速度,活动状态)。

    (12)LQFP48无铅封装(7x7mm,间距 0.5mm)

    2.2 接入框图

    2.3 主控芯片与W5500交互

    1)SPI连接

  • W5500 提供了SPI(串行外部接口)作为外设主机接口,有SCSn、SCLK、MOSI、MISO 共4路信号,且作为SPI从机工作。
  • 在W5500中只支持工作模式0和3,在这两种模式下数据总是在SCLK信号的上升沿被锁存,在SCLK信号的下降沿被输出。
  • MOSI和MISO信号无论是接收或发送,均遵从最高标志位(MSB)到最低标志位(LSB)的传输序列。
  • 2)固定数据长度模式和可变数据长度模式

    如果SPI工作模式设置为可变数据长度模式(VDM),SPI 的SCSn信号需要由外部主机通过SPI帧控制。

    在可变数据长度模式下,SCSn控制SPI帧的开始和停止:SCSn信号拉低(高电平到低电平),即代表W5500的SPI帧开始(地址段);SCSn信号拉高(低电平到高电平),即代表W5500的 SPI帧结束(数据段的随机N字节数据结尾);

    在我们的电路图设计中,ScSn接的是片选信号,所以我们应该选择可变数据长度模式

    3)W5500的内部存储器

    (1)1个普通寄存器block:这里配置了W5500的一些基本信息,如网络配置(IP,MAC地址,Socket中断配置等)。

    (2)8个Socket寄存器block:这里配置了每个Socket对应的信息,如Socket的模式,命令,状态,中断信息等。

    (3)8个Socket对应的接收缓冲寄存器block(共16k):初始时每个Socket分配为2k的缓存,用户可以自己重新通过修改相应的配置寄存器进行修改,但是要保证分配给8个Socket的缓冲大小之和不能超过16k,否则会报错。

    (4)8个Socket对应的发送缓冲寄存器block(共16k)。

    3 案例

    3.1 案例1:网络搭建

  • 需求描述:驱动W5500芯片,设置好IP,测试网络是否连通。

  • 电路连线:

  • 引脚说明:

    (1)W5500-RST:重置硬件,重置(Reset)低电平有效;该引脚需要保持低电平至少 500 us,才能重置 W5500;(正常使用应该高电平,需要重置芯片的时候置为低电平不少500us)。连接的是PG7。

    (2)W5500-INT:中断输出(Interrupt output)低电平有效;低电平:W5500的中断生效。高电平:无中断;连接的是PG6。

    (3)W5500-CS片选引脚。连接的是PD3

    连接的是STM32的SPI2外设。

    3.1.1 W5500官方库的移植
  • 官方库地址: https://github.com/Wiznet/ioLibrary_Driver

  • 官方库的结构

  • 移植后的目录
  • eth
  • w5500
  • 3.1.2 修改移植文件
  • wizchip_conf.h
    1. 找到宏定义_WIZCHIP_,如果不是W5500,就改成W5500。

    1. 修改工作模式为可变数据长度模式

  • wizchip_conf.c
    1. 导入spi头文件:
    1. 补充官方提供的一些接口。
    //这两个不用补充
    void wizchip_cris_enter(void) {}
    void wizchip_cris_exit(void) {}
    
    void wizchip_cs_select(void)
    {
       Driver_SPI_Start();
    }
    void wizchip_cs_deselect(void)
    {
       Driver_SPI_Stop();
    }
    uint8_t wizchip_spi_readbyte(void)
    {
       return Driver_SPI_SwapByte(0xFF);
    }
    void wizchip_spi_writebyte(uint8_t wb) 
    {
       Driver_SPI_SwapByte(wb);
    }
    
    1. 实现注册函数,并在头文件声明
    void user_register_function(void)
    {
       // 注册需要使用的6个函数
       reg_wizchip_cris_cbfunc(wizchip_cris_enter,wizchip_cris_exit);
       // 片选注册
       reg_wizchip_cs_cbfunc(wizchip_cs_select,wizchip_cs_deselect);
       // 读写数据注册
       reg_wizchip_spi_cbfunc(wizchip_spi_readbyte,wizchip_spi_writebyte);
    }
    
    3.1.3 代码编写
  • spi.c
  • #include "Driver_spi.h"
    
    void Driver_SPI_Init(void)
    {
        /* 1. 打开时钟  PB PD SPI2的时钟*/
        RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
        RCC->APB2ENR |= RCC_APB2ENR_IOPDEN;
        RCC->APB1ENR |= RCC_APB1ENR_SPI2EN;
    
        /* 2. 配置引脚模式 */
        // MISO PB14 浮空输入 0100
        GPIOB->CRH &= ~(GPIO_CRH_CNF14_1 | GPIO_CRH_MODE14);
        GPIOB->CRH |= GPIO_CRH_CNF14_0;
    
        // CS PD3 通用推挽输出 0011
        GPIOD->CRL &= ~GPIO_CRL_CNF3;
        GPIOD->CRL |= GPIO_CRL_MODE3;
    
        // SCK PB13 MOSI PB15 复用推挽输出 1011
        GPIOB->CRH |= GPIO_CRH_CNF13_1 | GPIO_CRH_CNF15_1 | GPIO_CRH_MODE13 | GPIO_CRH_MODE15;
        GPIOB->CRH &= ~(GPIO_CRH_CNF13_0 | GPIO_CRH_CNF15_0);
    
        /* 3. 配置SPI2 */
        // 3.1 设置为主设备
        SPI2->CR1 |= SPI_CR1_MSTR;
        // 3.2 波特率 BR 010 8分频
        SPI2->CR1 &= ~SPI_CR1_BR;
        SPI2->CR1 |= SPI_CR1_BR_1;
        // 3.3 SPI模式0
        SPI2->CR1 &= ~(SPI_CR1_CPOL | SPI_CR1_CPHA);
    
        // 3.4 数据帧 8位数据
        // 3.5 高位优先
        SPI2->CR1 &= ~(SPI_CR1_DFF | SPI_CR1_LSBFIRST);
    
        // 3.6 选择使用软件IO开控制从设备选择
        SPI2->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI;
        SPI2->CR2 &= ~(SPI_CR2_SSOE);
    
        // 3.7 打开总开关
        SPI2->CR1 |= SPI_CR1_SPE;
    }
    
    void Driver_SPI_Start(void)
    {
        CS_LOW;
    }
    void Driver_SPI_Stop(void)
    {
        CS_HIGH;
    }
    
    // 一定要在SPI_START之后调用
    uint8_t Driver_SPI_SwapByte(uint8_t byte)
    {
        uint8_t rbyte = 0;
        // 等待能够发送数据
        while ((SPI2->SR & SPI_SR_TXE) == 0)
        {
        }
        // 发送数据
        SPI2->DR = byte;
        // 等待接收数据
        while ((SPI2->SR & SPI_SR_RXNE) == 0)
        {
        }
        // 接收数据
        rbyte = SPI2->DR;
        return rbyte;
    }
    
    
  • spi.h
  • ...
        // 片选信号翻转  PD3
    #define CS_HIGH (GPIOD->ODR |= GPIO_ODR_ODR3)
    #define CS_LOW  (GPIOD->ODR &= ~GPIO_ODR_ODR3)
    
  • Inf_ETH.c
  • 设置ip信息和mac地址:要确保单片机和电脑处于同一个局域网

    #include "Inf_ETH.h"
    
    uint8_t mac[6] = {10, 11, 12, 145, 21, 20};
    uint8_t ip[4] = {192, 168, 19, 170};
    uint8_t sub[4] = {255, 255, 255, 0};
    uint8_t gw[4] = {192, 168, 23, 1};
    
    void Inf_ETH_Reset(void)
    {
        // W5500的初始化引脚  PG7 500us
        /* 1. 打开PG7时钟 */
        RCC->APB2ENR |= RCC_APB2ENR_IOPGEN;
        /* 2. 设置PG7引脚模式 通用推挽模式 0011 */
        GPIOG->CRL &= ~GPIO_CRL_CNF7;
        GPIOG->CRL |= GPIO_CRL_MODE7;
        /* 3. 置为低电平500us */
        GPIOG->ODR &= ~GPIO_ODR_ODR7;
        Delay_ms(1);
        /* 4. 复位高电平 */
        GPIOG->ODR |= GPIO_ODR_ODR7;
    }
    
    void Inf_ETH_SetMac(void)
    {
        printf("开始设置mac地址\n");
        setSHAR(mac);
        Delay_ms(1);
        printf("设置mac地址完成\n");
    }
    void Inf_ETH_SetIP(void)
    {
        // 1. 设置ip地址
        printf("开始设置ip地址\n");
        setSIPR(ip);
        Delay_ms(1);
        printf("设置ip地址完成\n");
        // 2. 设置子网掩码
        printf("开始设置子网掩码\n");
        setSUBR(sub);
        Delay_ms(1);
        printf("设置子网掩码完成\n");
        // 3. 设置网关
        printf("开始设置网关\n");
        setGAR(gw);
        Delay_ms(1);
        printf("设置网关完成\n");
    }
    
    void Inf_ETH_Init(void)
    {
        /* 1. 初始化spi */
        Driver_SPI_Init();
        /* 2. 调用用户注册函数 */
        user_register_function();
        /* 3. 重置W5500 */
        Inf_ETH_Reset();
        /* 4. 设置MAC地址 */
        Inf_ETH_SetMac();
        /* 5. 设置IP信息 */
        Inf_ETH_SetIP();
    }
    
    
  • Inf_ETH.h
  • #include "w5500.h"
    #include "Driver_USART1.h"
    #include "Delay.h"
    #include "Driver_spi.h"
    ...
    
  • main.c
  • int main(void)
    {
    
    	/*  使用can的环回静默模式完成数据的自发自收 测试stm32的can功能 */
    	Driver_USART1_Init();
    	printf("hello eth...\n");
    
    	Inf_ETH_Init();
    	while (1)
    	{
    	}
    }
    

    实验结果:

  • 注意:电脑和单片机需连接在同一个局域网里。
  • 3.2 案例2:搭建TCP服务端

    (1) 读取需要使用的socket状态

    (2) SOCK_CLOSED -> 关闭状态 -> 初始化流程 -> 选择对应的socket 使用协议 确定端口号

    (3) SOCK_INIT -> 初始化状态 -> 选择作为服务端 -> 进入监听状态

    (4) 等待时间 -> 等待客户端连接

    (5) SOCK_ES -> 连接完成状态 -> 打印客户端的IP和port -> 完成收发数据

    (6) 接收数据 -> Sn_IR_RECV 需要先知道数据的长度 -> Sn_Rx_RSR中 -> recv读取数据

    (7) 发送数据 -> send发送

    (8) 在收不到数据的时候 -> 可能连接中断了 -> 关闭socket 退出循环

    代码编写:

    在案例1基础上添加即可。

  • Tcp_server.c
  • #include "Tcp_server.h"
    
    uint8_t rBuff[128];
    
    void TCP_server_socket0(void)
    {
        // 1. 获取当前socket0的状态
        uint8_t socket_sr = getSn_SR(0);
        if (socket_sr == SOCK_CLOSED)
        {
            // 2. 当前为关闭状态 ->  表示服务端还没有初始化 socket没人用
            int8_t socket_r = socket(0, Sn_MR_TCP, 8080, SF_TCP_NODELAY);
            if (socket_r == 0)
            {
                printf("初始化socket0成功\n");
            }
            else
            {
                printf("初始化socket0失败\n");
            }
        }
        else if (socket_sr == SOCK_INIT)
        {
            // 3. 初始化完成 -> 进入监听状态 等待客户端连接
            int8_t listen_r = listen(0);
            if (listen_r == SOCK_OK)
            {
                printf("进入到监听状态成功\n");
            }
            else if (listen_r == SOCKERR_SOCKCLOSED)
            {
                // 手动关闭套接字  重新初始化
                close(0);
                printf("进入监听状态失败\n");
            }
            else
            {
                // 手动关闭套接字  重新初始化
                close(0);
                printf("未知错误,进入监听状态失败\n");
            }
        }
        else if (socket_sr == SOCK_ESTABLISHED)
        {
            // 4.说明已经连接成功 -> 可以进行收发数据了
            // 打印客户端的ip地址和端口号
            uint8_t client_ip[4] = {0};
            getSn_DIPR(0, client_ip);
            uint16_t client_port = getSn_DPORT(0);
            printf("连接成功,对应客户端的ip:port为%d.%d.%d.%d:%d\n", client_ip[0], client_ip[1], client_ip[2], client_ip[3], client_port);
            while (1)
            {
                // 5. 完成收发数据
                // 判断当前是否收到数据
                while ((getSn_IR(0) & Sn_IR_RECV) == 0)
                {
                    // 还没有收到数据   挂起等待
                    // 单独判断是否连接已经关闭
                    if (getSn_SR(0) != SOCK_ESTABLISHED)
                    {
                        // 接收不到数据啦  连接都关闭啦
                        close(0);
                        printf("连接关闭,会自动创建\n");
                        return;
                    }
                }
    
                // 有数据进来  -> 清空中断标志位
                // 如果想要清空标准位  按照产品手册写1
                setSn_IR(0, Sn_IR_RECV);
    
                // 读取数据  ->  先读取接收的数据长度
                uint16_t rDataLength = getSn_RX_RSR(0);
    
                recv(0, rBuff, rDataLength);
                
                printf("接收到客户端发送的数据,len:%d,data:%s\n", rDataLength, rBuff);
                // 原封不动发送回去
                send(0, rBuff, rDataLength);
            }
        }
    }
    
    
  • Tcp_server.h
  • #include "w5500.h"
    #include "socket.h"
    #include "Driver_USART1.h"
    #include "httpParser.h"
    #include "httpServer.h"
    ...
    
  • main.c
  • int main(void)
    {
    
    	/*  创建tcp服务端 使用电脑的功能作为客户端 连接使用 */
    	Driver_USART1_Init();
    	printf("hello eth...\n");
    
    	Inf_ETH_Init();
    	while (1)
    	{
    		TCP_server_socket0();
    	}
    }
    
    实验现象:

    单片机作为TCP服务端,电脑作为TCP客户端。当服务端处于监听状态时,客户端向服务端发送连接请求。连接成功服务端串口助手最下面会显示已连接。然后向服务端发送消息,服务端接收到并返回。

    3.3 案例3:搭建TCP客户端

    和客户端类似,将Tcp_server.c换成TCP_client.c即可,然后主函数初始化也改成客户端的就完成了。

    (1) 读取需要使用的socket状态

    (2) SOCK_CLOSED -> 关闭状态 -> 初始化流程 -> 选择对应的socket 使用协议 确定端口号

    (3) SOCK_INIT -> 初始化状态 -> 选择作为客户端 -> 主动连接

    (4) 连接超时 -> 等待2s再去连接 连接失败 -> 关闭socket 重新创建

    (5) SOCK_ES -> 连接完成状态 -> 主动发送消息hello -> 完成收发数据

    (6) 接收数据 -> Sn_IR_RECV 需要先知道数据的长度 -> Sn_Rx_RSR中 -> recv读取数据

    (7) 发送数据 -> send发送

    (8) 在收不到数据的时候 -> 可能连接中断了 -> 关闭socket 退出循环

    代码编写:
  • TCP_client.c
  • 注意:在写客户端程序的时候一定要把连接目标的ip及端口写准确

    #include "TCP_client.h"
    
    void TCP_client_socket0(void)
    {
        // 1. 读取socket0的状态
        uint8_t socket_sr = getSn_SR(0);
    
        if (socket_sr == SOCK_CLOSED)
        {
            // 2. 处于关闭状态 -> 还没有创建客户端
            int8_t socket_r = socket(0, Sn_MR_TCP, 8080, SF_TCP_NODELAY);
            if (socket_r == 0)
            {
                printf("客户端创建成功\n");
            }
            else
            {
                printf("客户端创建失败\n");
            }
        }
        else if (socket_sr == SOCK_INIT)
        {
            // 3. 初始化状态 -> 选择作为客户端 -> 主动连接
            uint8_t serverIP[4] = {192, 168, 19, 100};
            uint16_t serverPort = 8080;
            int8_t connect_r = connect(0, serverIP, serverPort);
            if (connect_r == SOCK_OK)
            {
                printf("连接服务端%d.%d.%d.%d:%d成功\n", serverIP[0], serverIP[1], serverIP[2], serverIP[3], serverPort);
            }
            else if (connect_r == SOCKERR_SOCKCLOSED)
            {
                printf("客户端的socket0关闭了,需要重启\n");
                close(0);
                // return;
            }
            else if (connect_r == SOCKERR_TIMEOUT)
            {
                // 等待2s再去连接
                printf("连接服务端超时  等待2s后再去连接\n");
                Delay_ms(2000);
            }
            else
            {
                printf("连接服务端失败,未知问题\n");
            }
        }
        else if (socket_sr == SOCK_ESTABLISHED)
        {
            // 4. 连接状态 -> 主动发送hello
            send(0, "hello\n", 6);
    
            while (1)
            {
                // 等待接收数据  -> 原封不动返回数据
                while ((getSn_IR(0) & Sn_IR_RECV) == 0)
                {
                    if (getSn_SR(0) != SOCK_ESTABLISHED)
                    {
                        printf("断开连接\n");
                        close(0);
                        return;
                    }
                }
    
                // 复位中断标志位
                // 如果需要清0   置为1
                setSn_IR(0,Sn_IR_RECV);
    
                // 获取接收数据的长度
                uint16_t rDataLength = getSn_RX_RSR(0);
                uint8_t rBuff[128];
                // 接收数据
                recv(0,rBuff,rDataLength);
                printf("客户端接收到数据,len:%d,data:%s\n",rDataLength,rBuff);
                // 返回数据
                send(0,rBuff,rDataLength);
            }
        }
    }
    
    
    实验现象:

    单片机充当客户端,电脑充当服务端。单片机初始化完成后就开始请求代码中给定的IP端口服务端连接,此时我们服务端需要开始侦听客户端才能连接到,连接到后单片机的客户端向我们电脑服务端发送一条hello,我们回了一条good,也接收到相同数据。

    3.4 案例4:UDP通讯

    udp是不区分服务端和客户端的。在eth里添加个UDP.c即可,最后在main函数里换一下初始化即可。

    由于UDP是无连接的,任意两方都能通信。在发第一条消息前不知道和谁通信,所以在编写函数时需要先等待挂起接收数据,接收的数据中会包含对方的IP地址端口号,然后将其存储下来。

    (1) 读取需要使用的socket状态

    (2) SOCK_CLOSED -> 关闭状态 -> 初始化流程 -> 选择对应的socket 使用UDP协议 确定端口号

    (3) SOCK_UDP -> 不知道接收数据的长度 -> 不知道对应的ip和端口号

    (4) 需要先挂起等到接收一个数据 -> Sn_IR_RECV 需要先知道数据的长度 -> Sn_Rx_RSR中 -> recvFrom读取数据 -> 长度会多8字节 -> 判断当前接收数据长度大于0 -> 使用数据的时候长度 – 8

    (5) 接收数据 -> 能够读取到对方的IP和端口号

    (6) 发送数据 -> sendTo方法 填写对应的IP和端口号进行发送

    (7) 在收不到数据的时候 -> 可能连接中断了 -> 关闭socket 退出循环

    函数编写:
  • UDP.c
  • 
    #include "UDP.h"
    
    void UDP_socket0(void)
    {
        // 1. 获取socket0状态
        uint8_t socket_sr = getSn_SR(0);
        if (socket_sr == SOCK_CLOSED)
        {
            // 2. 关闭状态  ->  选择使用UDP协议
            //不选择特殊功能,最后一个参数填0
            int8_t socket_r = socket(0, Sn_MR_UDP, 8888, 0);
            if (socket_r == 0)
            {
                printf("创建UDP服务成功\n");
            }
            else
            {
                printf("创建UDP服务失败\n");
            }
        }
        else if (socket_sr == SOCK_UDP)
        {
            // 3. 处于UDP创建完成状态
            // 不知道对方的IP和端口号
            // 等待挂起接收数据  ->  接收的数据中会包含对方的IP地址端口号
            uint8_t rBuff[128] = {0};
            uint16_t rDataLength = 0;
            uint8_t udp_IP[4] = {0};
            uint16_t udp_Port = 0;
    
            // 不知道接收数据的长度
            while (1)
            {
                while ((getSn_IR(0) & Sn_IR_RECV) == 0)
                {
                    if (getSn_SR(0) != SOCK_UDP)
                    {
                        printf("UDP服务状态变化,需要重启\n");
                        close(0);
                        return;
                    }
                }
                // 清空标志位
                setSn_IR(0, Sn_IR_RECV);
    
                // 接收到数据
                // 获取接收数据的长度  ->  UDP接收到的数据会多8字节 空的  标志
                rDataLength = getSn_RX_RSR(0);
                if (rDataLength > 0)
                {
                    recvfrom(0, rBuff, rDataLength, udp_IP, &udp_Port);
                    printf("接收到UDP服务发送的数据,IP:port为 %d.%d.%d.%d:%d,len:%d,data:%s\n", udp_IP[0], udp_IP[1], udp_IP[2], udp_IP[3], udp_Port, rDataLength - 8, rBuff);
    
                    // 原路返回对应数据
                    if (rDataLength - 8 > 0)
                    {
                        sendto(0, rBuff, rDataLength - 8, udp_IP, udp_Port);
                    }
                }
            }
        }
    }
    
    
    实验现象:

    作者:徐嵌

    物联沃分享整理
    物联沃-IOTWORD物联网 » 物联网通信之以太网通讯—ETH

    发表回复