以太网

1、前言

STM32H7 以太网的 MMC(MAC management counter)中断是个有点特别的中断。特殊之处在于它是默认使能。如果我们在代码里不针对 MMC 进行相关处理,就会造成一些异常现象。我们先来看一个真实的客户案例。

2、客户案例

客户使用 STM32H750 作为主控,与其他设备之间进行以太网通讯。

客户在压力测试中发现:

• 设备从第一次通讯开始,累计 7 到 8 天,就会发现 STM32H750 不再响应用户的请求。

• 客户通过使用 IDE 和添加辅助代码可以发现,STM32H750 会不停地进入以太网中断,导致所使用的操作系统无法进行有效的系统调度。

• 问题发生后,客户无论拔下网线或者再次连上网线,STM32H750 依然会不停的进入以太网中断。

• 客户尝试使用 IDE 查看所有以太网寄存器,会发现有时侯能够让系统恢复正常。

3、分析

系统不停的进入以太网中断,说明某个中断在被某种条件下被不停的触发,或者中断触发后没有被处理。进一步,当系统出现异常状况后,拔掉网线,中断依然不断的进入,说明该异常并不需要外界不停的输入,也就说明可能是中断没有被处理所导致。所以,客户首先想到的是补全所有使能的以太网中断的清除代码。然而,客户再次测试,却发现累计 7 到 8 天,问题再次发生。

在这种情况下,为了深刻了解该状况的原因,我们建议客户,抓取异常时的寄存器现场,然后和正常状态时的寄存器进行对比。我们在设备未发生异常前,抓取了以太网的三组寄存器 DMA、 MTL 和 MAC。同时,我们在发生异常后,在同一设备再次进行这三组寄存器的抓取。然后,我们使用文本比较工具,对两次的寄存器进行比较。我们很快就可以发现,MAC 寄存器存在值得关注的差异。MAC 寄存器对比如下:

“STM32H7

我们可以看到在系统异常情况下下,MMCRXIS 和 MMCIS 被置位了。

我们从参考手册 RM0433 (STM32H742, STM32H743/753 and STM32H750 Value line advanced Arm®-based 32-bit MCUs)(直接搜索关键子 MMCRXIS)中可以看到 MMCRXIS 和 MMCIS 表示系统收到了 MMC 接收中断。

“STM32H7

在两次三组寄存器的比较中,我们看到系统生成了 MMC 接收中断(MMC_RX_INTERRUPT 中 RXUCGPIS)。这个符合前文的 MMCRXIS 和 MMCIS 的状态。

“STM32H7

从参考手册 RM0433 中我们可以看到,只要 MMC 选项使能,该中断标志就为有效。但是我们并没有使能 MMC 选项,甚至我们都没有使能 MMC 中断,为什么还是有中断产生呢?

4、MMC 中断的特点

MMC 选项其实是默认使能。我们可以从参考手册 RM0433 中看到这一点。

“STM32H7

在 MMC 默认使能的情况下,什么情况下会产生中断呢?

让我们在 RM0433 里搜索下两次寄存器比较发现的 RXUCGPIS 寄存器:

“STM32H7

综合这两点,我们可以认为,在长时间以太网收发包之后,MMC 中断几乎一定会发生。这符合客户案例的场景,例如,重现这个问题需要 7 到 8 天。当然从这里我们也可以推断出,我们如果加快测试数据包收发的发送,MMC 中断会发生更早。那么,如何避免在产品应用中这种问题发生呢?

5、解决方案

1.1. 使用 MMC 中断

MMC 中断是个有用的功能。如果我们要使用的话,可以参考 MMC Rx interrupt register (ETH_MMC_RX_INTERRUPT)和 MMC Tx interrupt register (ETH_MMC_TX_INTERRUPT)的描述。我们需要对 MMC 进行一个读的操作。

“STM32H7

“STM32H7

这也解释了,客户为什么发现,通过调试器一个一个去读取以太网寄存器,会在某个操作时让异常状态恢复到正常。

1.2. 关闭 MMC 中断

在很多情况下,MMC 中断对实际产品没有意义。例如,在这个案例中,我们可以选择关闭 MMC中断。这就需要用到 MMC 中断的 mask 寄存器:

• MMC Rx interrupt mask register (ETH_MMC_RX_INTERRUPT_MASK)

• MMC Tx interrupt mask register (ETH_MMC_TX_INTERRUPT_MASK)

我们可以添加以下代码到我们的应用代码里

“STM32H7

客户反馈找不到 ETH 的定义。其实在 STM32H7 的例程里,我们可以很容易发现 ETH 定义在

STM32Cube\Repository\STM32Cube_FW_H7_V1.8.0\Drivers\CMSIS\Device\ST\STM32H7xx\Include\stm32h750xx.h:

“STM32H7

也就是说,如果你的工程代码源自 STM32Cube 例程,你应该能够加入以上代码并且能够成功运行。

在加入上述代码或者类似操作后,客户反馈,再次进行超过 7 天以上的压力测试,系统运行正常。

6、总结

STM32H7 的 MMC 中断需要加以注意,如果不使用 MMC,需要确保它已经关闭;否则在经过长时间网络收发后,系统会产生并非用户所期望的中断,导致系统假死。另外,我们也看到了调试STM32 以太网的常规方式,也就是借助工具而不需要写代码就可以进行寄存器的比较。这种方法值得使用 STM32 以太网的用户进行调试时参考。

来源:STM32单片机
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 13

STM32H7 以太网的MMC(MAC management counter)中断是个有点特别的中断。特殊之处在于它是默认使能。如果我们在代码里不针对MMC 进行相关处理,就会造成一些异常现象。我们先来看一个真实的客户案例。

详阅请点击下载《STM32H7以太网的MMC中断》

来源:ST
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 12

HTTP(Hyper Text Transfer Protocol超文本传输协议)是用于从万维网服务器传输超文本到本地浏览器的传输协议,它基于TCP/IP协议通信,因此也是基于<客户端-服务器>模型运作的。HTTP属于应用层协议,我们可以用它来传输服务器的各种资源,如文本、图片、音频等,具有简单、快捷、灵活、无连接、无状态等优点。

在一次完整的HTTP通信过程中,浏览器与服务器之间大致可以分为七个步骤:

01、建立TCP连接

HTTP工作前,浏览器首先要通过网络与服务器建立连接,该连接通过TCP完成。HTTP属于应用层协议,根据规则只有低层协议建立之后才能进行更深层协议的连接。因此,首先要建立TCP连接,端口号为80。

02、浏览器向服务器发送请求命令

TCP连接成功建立后,浏览器就会向服务器发送请求命令。例如:GET/sample/hello.jsp HTTP/1.1

03、浏览器发送请求头信息

浏览器发送请求命令后,还要以头信息的形式向服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。

04、服务器应答

客户机向服务器发出请求后,服务器会客户机回送应答,例如:HTTP/1.1 200 OK

应答的第一部分是协议的版本号和应答状态码。

05、服务器发送应答头信息

正像客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。

06、服务器向浏览器发送数据

服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着会按照应答头信息所描述的格式发送用户所请求的实际数据。

07、服务器关闭TCP连接

一般情况下,一旦服务器完成了数据,就会关闭TCP连接。

在创建工程前我们还需要了解HTTP请求格式,HTTP请求由三部分构成:请求方法URI协议、请求头、请求正文。

请求方法URI(URL相当于URI的子集)协议

我们使用前文中的例子“GET/sample.jsp HTTP/1.1”。“GET“代表请求方法,“/sample.jsp”表示URI,“HTTP/1.1代表协议和协议的版本。

请求头

请求头包含许多有关的客户端环境和请求正文的有用信息。例如,请求头可以声明浏览器所用的语言,请求正文的长度等。

请求正文

请求头和请求正文之间是一个空行,这个行非常重要,它表示请求头已经结束,接下来的是请求正文,请求正文中可以包含客户提交的查询字符串信息。

实验使用MB-039开发板,在工程中使用LwIP+FreeRTOS,实验展示如何实现HTTP服务器,实验使用到的硬件如下:

“基于MM32F3270

如图是MB-039(完整原理图可以通过MM32官网下载)的ETH部分。

各个信号引脚对应如下:

“基于MM32F3270

“基于MM32F3270

我们创建HTTP工程:

static void
http_server_netconn_thread(void* arg)
{
*********************************************************
 do {
        err = netconn_accept(conn, &newconn);     //(1)
        if (err == ERR_OK) {
            http_server_netconn_serve(newconn);    //(2)
            netconn_delete(newconn);            
        }
    } while(err == ERR_OK);
*********************************************************
}
static void http_server_netconn_serve(struct netconn* conn)
{
    struct netbuf* inbuf;
    char* buf;
    u16_t buflen;
err_t err;

    err = netconn_recv(conn, &inbuf);           //(3)
    if (err == ERR_OK) {
        netbuf_data(inbuf, (void**)&buf, &buflen);
        if (buflen >= 5 &&
                buf[0] == 'G' &&
                buf[1] == 'E' &&
                buf[2] == 'T' &&
                buf[3] == ' ' &&
                buf[4] == '/' ) {        //(4)

            netconn_write(conn, http_html_hdr, sizeof(http_html_hdr) - 1, NETCONN_NOCOPY);              //(5)

            netconn_write(conn, http_index_html, sizeof(http_index_html) - 1, NETCONN_NOCOPY);           //(6)
        }
    }

    netconn_close(conn);      //(7)

    netbuf_delete(inbuf);
}

01、等待连接请求

02、执行数据发送的主要函数,工程的主要部分

03、接收客户端(浏览器)发送的数据

04、通过前面五个字节判断是否为请求方法,我们只是为例简单的功能展示不需要去关注正文部分

05、发送应答头信息

06、发送网页信息(html)

07、关闭连接

到这里已经完成了工程的创建,看一下PC的IP地址,设备需要处于同一网段方便测试。打开命令行窗口输入:ipconfig

“基于MM32F3270

PC的地址为:192.168.105.34,在sys_arch.h文件中对DEST_IP_ADDR0 、DEST_IP_ADDR1、DEST_IP_ADDR2、DEST_IP_ADDR3进行修改,DEST_PORT 随意修改。

#define LOCAL_PORT                 2021

#define IP_ADDR0                    192
#define IP_ADDR1                    168
#define IP_ADDR2                    105
#define IP_ADDR3                    35

将程序下载入开发板中,打开浏览器,输入设备地址:192.168.105.35

“基于MM32F3270

(1)点击灵动微电子即可跳转到我们的官网

“基于MM32F3270

(2)我们按F12来查看网页的信息

“基于MM32F3270

关注画圈部分,点击灵动微电子可以执行跳转的原因是他的href属性指向的超链接目标的URL为我们的官网地址。同样,使图片显示的方式是类似的,我们先找到一张图片,将他的地址填入img src中,此时图片并非存储在MCU中,是通过网络访问读取的。

我们也可以使用Wireshark进行监视抓取整个过程,选择IP过滤ip.addr==192.168.105.35。

“基于MM32F3270

可以看到整个过程和前文介绍的步骤完全一致。

实验程序请登录我们的官网(https://www.mindmotion.com.cn/products/mm32mcu/mm32f/mm32f_mainstream/mm32f3270/)下载MM32F3270 SDK,工程路径如下:

~\MM32F3270_Lib_Samples_V0.90\Demo_app\Ethernet_Demo\ETH_RTOS\Freertos_http

到这里已经完成了以太网专题的介绍,在样例包中,还提供了其他的样例工程,如:mqtt_onenet、mqtt_baidu、Freertos_dns、Freertos_dhcp、TFTP等。以太网协议本身非常庞大,需要用户花费时间去研究。专题通过对六个简单实验的介绍,起到抛砖引玉的作用,更大的意义在于使刚接触以太网的用户有个着手点,实现更复杂的功能。

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 36

前面重点对Client的创建方式及使用方式进行了介绍,本节通过Server实验对TCP通信过程进行一次介绍。

在TCP/IP协议中,传输层及以下层的机制是由内核提供的。应用层由用户提供,应用层程序对通信数据进行解析处理,传输层及以下层处理通信的细节(将数据从一端传入另外一端)。应用层数据通过协议栈发送到网络上时,每层协议都要增加一个数据部首(header),进行一次封装。其中不同的协议层对数据包有不同的称谓,在传输层叫段(segment),在网络层叫做数据报(datagram),在链路层称为帧(frame)。

“基于MM32F3270

“基于MM32F3270

在通信过程中,发送端执行以下动作:首先程序进行编码,确定通信的建立连接、发送数据的时间。接着建立TCP连接,TCP根据应用指示负责建立连接、发送数据及断开连接。TCP首部包括源端口号和目标端口号、序号和校验和,加完首部后数据包继续往下传递到IP层,IP层加上IP首部包括地址等信息用于寻址操作,之后将数据继续往下传递附加数据链路层首部。最后发送时的分组数据包会加上以太网包尾(用于循环冗余校验)。

主机端:收到数据包后会在以太网数据包中找到MAC地址,判断是否为自己的数据包,如果不是则丢弃。如果是传递给IP层处理,以此类推,不断往上传递到TCP层。在TCP层通过校验和判断数据是否损坏,然后检查是否按序号接收数据,最后检查端口号。处理完成这一切后数据包继续往上层发送,即应用层。如果出现主机空间已满等情况,主机则会发送“处理异常”通知发送端。

“基于MM32F3270

实验使用MB-039开发板,在工程中使用LwIP+FreeRTOS,实验展示如何制作一个TCP Server,并收发数据,实验使用到的硬件如下:

“基于MM32F3270

如图是MB-039(完整原理图可以通过MM32官网下载)的ETH部分。

各个信号引脚对应如下:

“基于MM32F3270

“基于MM32F3270

在进行Server实验前,我们先了解需要使用到的API:

1)netconn_bind ()

2)netconn_listen ()

3)netconn_accept ()

以下分API展开介绍:

01、netconn_bind ()

从源码中可以看出其主要功能:为conn(服务器端)绑定地址与端口号。

err_t netconn_bind(struct netconn* conn, const ip_addr_t* addr, u16_t port)
{
    API_MSG_VAR_DECLARE(msg);
    err_t err;

    LWIP_ERROR("netconn_bind: invalid conn", (conn != NULL), return ERR_ARG;);
#if LWIP_IPV4
    /* Don't propagate NULL pointer (IP_ADDR_ANY alias) to subsequent functions */
    if (addr == NULL) {
        addr = IP4_ADDR_ANY;
    }
#endif /* LWIP_IPV4 */
#if LWIP_IPV4 && LWIP_IPV6
    if ((netconn_get_ipv6only(conn) == 0) &&
            ip_addr_cmp(addr, IP6_ADDR_ANY)) {
        addr = IP_ANY_TYPE;
    }
#endif /* LWIP_IPV4 && LWIP_IPV6 */
    API_MSG_VAR_ALLOC(msg);
    API_MSG_VAR_REF(msg).conn = conn;
    API_MSG_VAR_REF(msg).msg.bc.ipaddr = API_MSG_VAR_REF(addr);
    API_MSG_VAR_REF(msg).msg.bc.port = port;
    err = netconn_apimsg(lwip_netconn_do_bind, &API_MSG_VAR_REF(msg));
    API_MSG_VAR_FREE(msg);
    return err;
}

02、netconn_listen ()

netconn_listen指向的函数是:netconn_listen_with_backlog,作用:使服务器进入监听状态,等待远端的连接请求。

err_t netconn_listen_with_backlog(struct netconn* conn, u8_t backlog)
{
#if LWIP_TCP
    API_MSG_VAR_DECLARE(msg);
    err_t err;

    /* This does no harm. If TCP_LISTEN_BACKLOG is off, backlog is unused. */
    LWIP_UNUSED_ARG(backlog);

    LWIP_ERROR("netconn_listen: invalid conn", (conn != NULL), return ERR_ARG;);

    API_MSG_VAR_ALLOC(msg);
    API_MSG_VAR_REF(msg).conn = conn;
#if TCP_LISTEN_BACKLOG
    API_MSG_VAR_REF(msg).msg.lb.backlog = backlog;
#endif /* TCP_LISTEN_BACKLOG */
    err = netconn_apimsg(lwip_netconn_do_listen, &API_MSG_VAR_REF(msg));
    API_MSG_VAR_FREE(msg);
    return err;
#else /* LWIP_TCP */
    LWIP_UNUSED_ARG(conn);
    LWIP_UNUSED_ARG(backlog);
    return ERR_ARG;
#endif /* LWIP_TCP */
}

03、netconn_accept ()

netconn_accept(代码较长,这里不进行粘贴)用于TCP服务器中,等待着远端主机的连接请求,并且建立一个新的TCP连接,在调用这个函数之前需要通过调用 listen()函数让服务器进入监听状态。accept()函数的调用会阻塞应用线程直至与远程主机建立TCP连接。参数addr是一个返回结果参数,它的值由accept()函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,能够标识连接对象。

了解了以上3个API,我们开始创建Server工程:

static void server(void* thread_param)
{
    struct netconn* conn, *newconn;
    err_t err;
    LWIP_UNUSED_ARG(arg);

#if LWIP_IPV6
    conn = netconn_new(NETCONN_TCP_IPV6);
    netconn_bind(conn, IP6_ADDR_ANY, LOCAL_PORT);         
#else /* LWIP_IPV6 */
    conn = netconn_new(NETCONN_TCP);               //①
    netconn_bind(conn, IP_ADDR_ANY, LOCAL_PORT);      //②
#endif /* LWIP_IPV6 */
    LWIP_ERROR("tcpecho: invalid conn", (conn != NULL), return;);

    printf("The local port number is%d\n\n", LOCAL_PORT);
    netconn_listen(conn);                              //③
    while (1) {
        err = netconn_accept(conn, &newconn);         //④
        if (err == ERR_OK) {
            struct netbuf* buf;
            void* data;
            u16_t len;

            while ((err = netconn_recv(newconn, &buf)) == ERR_OK) {    //⑤
                do {
                    netbuf_data(buf, &data, &len);
                    err = netconn_write(newconn, data, len, NETCONN_COPY);  //⑥
                } while (netbuf_next(buf) >= 0);
                netbuf_delete(buf);                 //⑦
            }
            netconn_close(newconn);         //⑧
            netconn_delete(newconn);        //⑨
        }
    }
}

1、申请一个连接结构,指定参数是NETCONN_TCP,即TCP连接

2、绑定本地的IP地址与端口号

3、使TCP服务器进入监听状态

4、处理客户端的连接请求,当只有当有客户端发送连接请求的时候才会处理,否则将进入阻塞态,而客户端的信息保存在newconn连接结构中

5、接收数据,并装填进buf

6、对接收的数据进行转发(指定为不拷贝方式NETCONN_COPY)

7、释放数据空间

8、主动关闭客户端的连接

9、释放newconn空间

到这里已经完成了工程的创建,看一下PC的IP地址,设备需要处于同一网段,以方便测试。

打开命令行窗口输入:ipconfig

“基于MM32F3270

PC的地址为:192.168.105.34,在sys_arch.h文件中对DEST_IP_ADDR0 、DEST_IP_ADDR1、DEST_IP_ADDR2、DEST_IP_ADDR3进行修改,DEST_PORT 随意修改。

#define LOCAL_PORT                 2021

#define IP_ADDR0                   192
#define IP_ADDR1                   168
#define IP_ADDR2                   105
#define IP_ADDR3                   21

将程序下载入开发板中,使用NetAssist进行如下设置:

1)协议设置,此时设备为Server,则PC为Client

2)设置远程主机地址(即设备地址)

3)端口号

“基于MM32F3270

点击连接,若提示连接失败,则Ping一下开发板地址,可以正常Ping通则检查端口号;如果无法Ping通则需要对工程进行检查。

“基于MM32F3270

任意输入字符进行发送。

“基于MM32F3270

通过上图可以观察到发送成功,并且设备返回数据与发送数据一致,表明实验成功。实验程序请登录我们的官网(https://www.mindmotion.com.cn/download.aspx?cid=2542&page=2)下载MM32F3270 SDK,工程路径如下:~\MM32F3270_Lib_Samples_V0.90\Demo_app\Ethernet_Demo\ETH_RTOS\Freertos_Server。

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 37

UDP(User Datagram Protocol用户数据报协议)是一种无连接、不可靠的协议。UDP协议没有流控制,没有应答确认机制,不能解决丢包、重发、错序问题。它只是简单地实现从一端主机到另一端主机的数据传输功能,数据通过IP层发送,在网络中传输,到达目标主机的顺序是无法预知的,因此需要应用程序对这些数据进行排序处理。在发送端,UDP只是把上层应用的数据封装到UDP报文中;在差错检测方面,仅仅是对数据进行了简单的校验,然后将其封装到IP数据报中发送出去。在接收端,无论是否收到数据,都不会产生一个应答返回送给源主机。如果接收到数据校验错误,接收端丢弃该UDP报文,也不会告诉源主机,这样数据是无法保障其准确性的。但是在如今的网络环境下,UDP协议传输出现错误的概率很小,并且它的实时性是非常好,常用于实时视频的传输,比如直播、网络电话等,即使是出现了数据丢失的情况,导致视频卡帧,也不会产生大的影响。UDP被广泛应用于与对传输速度有要求,并且可以容忍出现差错的数据传输中。

对于UDP通信来说,可以没有服务器,服务器只是一种规定。主动发送的一方为客户端,被动接受的一方为服务器(下图为通信过程,程序也是按照这个步骤进行设计)。

“基于MM32F3270以太网

实验使用MB-039开发板,在工程中使用LwIP+FreeRTOS,实验展示如何实现UDP数据收发,实验使用到的硬件如下:

“基于MM32F3270以太网

如图是MB-039(完整原理图可以通过MM32官网下载)的ETH部分接口电路。

各个信号引脚对应如下:

“基于MM32F3270以太网

“基于MM32F3270以太网

实验用到的API前面几节已经进行了讲解,我们直接创建UDP工程:

static void udpecho_thread(void* arg)
{
    struct netconn* conn;
    struct netbuf* buf;
    char buffer[4096];
    err_t err;
    LWIP_UNUSED_ARG(arg);

    conn = netconn_new(NETCONN_UDP);        // (1)
    netconn_bind(conn, IP_ADDR_ANY, LOCAL_PORT);   // (2)

    while (1) {
        err = netconn_recv(conn, &buf);          // (3)
        if (err == ERR_OK) {
            if(netbuf_copy(buf, buffer, sizeof(buffer)) != buf->p->tot_len) {   // (4)
                LWIP_DEBUGF(LWIP_DBG_ON, ("copy failed\n"));
            }
            else {
                buffer[buf->p->tot_len] = '\0';
                err = netconn_send(conn, buf);                 // (5)
                if(err != ERR_OK) {
                   LWIP_DEBUGF(LWIP_DBG_ON, ("netconn_send failed: %d\n", (int)err));
                }
                else {
                    LWIP_DEBUGF(LWIP_DBG_ON, ("got %s\n", buffer));
                }
            }
            netbuf_delete(buf);
        }
    }
}

1)创建一个UDP类型的连接结构

2)绑定IP地址与端口

3)接收客户端发送的数据(值得指出的是UDP可以一对一,也可以一对多以广播的形式发送)

4)拷贝出接收的数据

5)将数据发送给目标主机

到这里已经完成了工程的创建,看一下PC的IP地址,设备需要处于同一网段,以方便测试。打开命令行窗口输入:ipconfig

“基于MM32F3270以太网

PC的地址为:192.168.105.34,在sys_arch.h文件中对DEST_IP_ADDR0 、DEST_IP_ADDR1、DEST_IP_ADDR2、DEST_IP_ADDR3进行修改,DEST_PORT 随意修改。

#define LOCAL_PORT                 2021

#define IP_ADDR0                    192
#define IP_ADDR1                    168
#define IP_ADDR2                    105
#define IP_ADDR3                    35

将程序下载入开发板中,使用NetAssist进行如下设置:

1)协议设置,选择UDP

2)本机主机地址(即PC地址)

3)端口号

4)设置远程主机地址(即设备地址)

“基于MM32F3270以太网

点击打开,在进行这一步前可以则Ping一下开发板地址,可以正常Ping通则检查端口号;如果无法Ping通则需要对工程进行检查。任意输入字符进行发送。

“基于MM32F3270以太网

可以发现对接收到的数据进行了正确的转发,我们也可以使用Wireshark进行监视抓取整个过程,选择IP过滤:ip.addr==192.168.105.35。

“基于MM32F3270以太网

通过上图表明实验成功。

实验程序请登录我们的官网(https://www.mindmotion.com.cn/download.aspx?cid=2542&page=2)下载MM32F3270 SDK,工程路径如下:

~\MM32F3270_Lib_Samples_V0.90\Demo_app\Ethernet_Demo\ETH_RTOS\Freertos_UDP

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 39

上一节我们对TCP的报文和连接过程做了介绍,本节通过Socket的方式对整个通信过程再次进行一次整理(使用Socket方式易于加深对以太网通信过程的理解,在此讲解使用的是完整版Socket)。

“基于MM32F3270

服务器端初始化

1)调用socket,向内核申请一个套接字sock

2)调用bind将sock与服务器端的IP与PORT绑定

3)调用listen将套接字设为监听模式,准备接收客户端连接请求

4)调用accept等待并接收客户端的连接请求,建立好TCP连接后,该函数会返回一个新的已连接套接字connected

创建连接

1)客户端调用socket创建套接字

2)调用connect,向服务器发送连接请求

3)connect会发送一个请求SYN段并阻塞等待服务器应答(第一次握手)

4)服务器收到SYN,会给客户端发送一个确认应答ACK,同时发送一个请求(SYN)建立连接(第二次握手)

5)客户端收到服务器发的SYN+ACK段,表明客户端连接已建立成功,进入已连接状态。客户端再向服务器发送一个ACK段,服务器收到后则服务器连接成功

数据传输

1)服务器端使用accept连接建立成功后(通信双方可同时写数据,支持全双工),调用read开始读数据,若没有数据则阻塞等待

2)客户端调用write向服务器发送数据请求,客户端收到之后调用read处理请求,此过程服务器调用read阻塞等待

3)服务器调用write将处理好的请求发送给客户端,再次调用read等待下一个请求

4)服务器收到SYN,会给客户端发送一个确认应答ACK,同时发送一个请求(SYN)建立连接(第二次握手)

4)客户端收到后从read返回,发送下一条请求,如此循环下去

断开连接

1)没有数据,则客户端调用close关闭连接,给服务器发送一个断开连接请求FIN段(第一次握手)

2)服务器收到客户端的FIN段,给客户端发送一个确认应答ACK段,表明同意断开连接。客户端收到ACK段并调用read返回0,表明客户端连接已经断开(第二次握手)

3)read返回0后,服务器知道客户端已经断开连接,它也调用close关闭连接,给客户端发送一个断开连接请求FIN段(第三次握手)

4)客户端收到服务器发送的FIN段,就给服务器一个确认应答ACK段,表明同意断开连接。客户端进入TIME_WAIT状态,服务器收到客户端的ACK段后也断开连接

“基于MM32F3270

实验使用MB-039开发板,在应用工程中使用LwIP+FreeRTOS,实验展示如何制作一个TCP Server_socket,并收发数据,实验使用到的硬件如下:

“基于MM32F3270

如图是MB-039(完整原理图可以通过MM32官网下载)的Ethermac部分。

各个信号引脚对应如下:

“基于MM32F3270

“基于MM32F3270

Server_socket实验用到的API大部分在前面已经进行讲解(只是对NETCONN接口编辑方式进行二次封装),本节只介绍一个比较关键的API:setsockopt(s,level,optname,opval,optlen)。

从名称中就可以看出函数功能用于设置套接字的一些选项,我们关注一下参数:

(1)level有多个常用的选项

SOL_SOCKET:表示在Socket层
IPPROTO_TCP:表示在TCP层
IPPROTO_IP:表示在IP层

(2)optname 表示该层的具体选项名称

level为SOL_SOCKET时,有以下选项:SO_REUSEADDR(允许重用本地地址和端口)、

SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、SO_RCVBUF(设置发送数据缓冲区大小)等。

level为IPPROTO_TCP时,有以下选项:TCP_NODELAY(不使用Nagle算法)、TCP_KEEPALIVE(设置TCP保活时间)等。

level为IPPROTO_IP选项,有以下选项:IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等。

实现Server_socket函数:

static void server_socket(void* thread_param)
{ 
    int sock = -1, connected;
    char* recv_data;
    struct sockaddr_in server_addr, client_addr;
    socklen_t sin_size;
    int recv_data_len;

    printf("The local port number is%d\n\n", LOCAL_PORT);
    recv_data = (char*)pvPortMalloc(RECV_DATA);
    if (recv_data == NULL) {
        printf("No memory\n");
        goto __exit;
    }
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        printf("Socket error\n");
        goto __exit;
    }
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(LOCAL_PORT);
    memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));
    if (bind(sock, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) == -1) {
        printf("Unable to bind\n");
        goto __exit;
    }
    if (listen(sock, 5) == -1) {                         // (1)
        printf("Listen error\n");
        goto __exit;
    }
    while(1) {
        sin_size = sizeof(struct sockaddr_in);
        connected = accept(sock, (struct sockaddr*)&client_addr, &sin_size);  // (2)
        printf("new client connected from (%s, %d)\n",
               inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
        {
            int flag = 1;
            setsockopt(connected,
                       IPPROTO_TCP,     /* set option at TCP level */
                       TCP_NODELAY,     /* name of option */
                       (void*) &flag,   /* the cast is historical cruft */
                       sizeof(int));    /* length of option value */      // (3)
        }
        while(1) {
            recv_data_len = recv(connected, recv_data, RECV_DATA, 0);    // (4)
            if (recv_data_len <= 0)
                break;
            printf("recv %d len data\n", recv_data_len);
            write(connected, recv_data, recv_data_len);   // (5)
        }
        if (connected >= 0)
            closesocket(connected);                  //  (6)
        connected = -1;
    }
__exit:
    if (sock >= 0) closesocket(sock);
    if (recv_data) free(recv_data);
}

1)进入监听状态

2)阻塞应用线程直至与远端主机建立TCP连接,建立成功后远程主机的信息将保持在连接句柄中(connected)

3)对套接字connected进行设置:在TCP层,不使用Nagle算法

4)处理客户端的连接请求,接收远程主机信息

5)将接收的数据进行转发

6)主动关闭客户端的连接

到这里已经完成了Server_socket函数的创建,看一下PC的IP地址,设备需要处于同一网段方便测试。打开命令行窗口输入:ipconfig

“基于MM32F3270

PC的地址为:192.168.105.34,在sys_arch.h文件中对DEST_IP_ADDR0 、DEST_IP_ADDR1、DEST_IP_ADDR2、DEST_IP_ADDR3进行修改,DEST_PORT 随意修改。

#define LOCAL_PORT                 2021

#define IP_ADDR0                    192
#define IP_ADDR1                    168
#define IP_ADDR2                    105
#define IP_ADDR3                    26

将程序下载入开发板中,使用NetAssist进行如下设置:

1)协议设置,此时设备为Server,则PC为Client

2)设置远程主机地址(即设备地址)

3)端口号

“基于MM32F3270

点击连接,若提示连接失败,则Ping一下开发板地址,可以正常Ping通则检查端口号;如果无法Ping通则需要对工程进行检查。

“基于MM32F3270

任意输入字符进行发送。

“基于MM32F3270

通过上图可以观察到发送成功,并且设备返回数据与发送数据一致,表明实验成功。

实验程序请登录我们的官网(http://www.mindmotion.com.cn/download.aspx?cid=2542&page=2)下载MM32F3270 SDK,工程路径如下:

~\MM32F3270_Lib_Samples_V0.90\Demo_app\Ethernet_Demo\ETH_RTOS\Freertos_Server_socket

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 18

在进行本节之前,首先解决大家的一个疑惑点:Client和Client_Socket有什么区别或分别代表的含义?

Socket标准定义为套接字,应用于主流的网络设计程序,具有使用简单,多平台移植方便的特点。在Socket应用中,使用一个套接字来记录网络的一个连接,套接字是一个整数,就像操作文件一样,利用一个文件描述符,进行打开、读、写、关闭等操作。在网络中,可以对Socket 套接字进行类似的操作,比如开启一个网络的连接、读取连接主机发送来的数据、向连接的主机发送数据、终止连接等操作。LwIP设计目的主要应用于嵌入式平台,对于Socket的支持并不完全,只是通过对netconn进行封装实现部分功能,使得LwIP也具有多平台应用的特性,通过Socket方式的了解能够极大简化通信过程的理解,快速实现应用开发。

Demo应用中,使用的开发板为MB-039,在工程中使用LwIP+FreeRTOS,实验展示如何制作一个客户端并发送数据,板载Ethernet相关的硬件部分电路如下:

“基于MM32F3270

MB-039 完整原理图可以通过MM32官网下载。

各个信号引脚对应如下:

“基于MM32F3270

“基于MM32F3270

通过配置复用相关引脚为RMII相关的功能,初始化以太网功能,执行FreeRTOS的启动。具体过程可参考样例初始化程序中代码。

在进行Client_Socket实验前,我们先了解需要使用到的应用功能函数:

(1)socket ()

(2)connect ()

(3)write ()

(1) socket ()

Socket()指向lwip_socket(),功能为申请一个套接字,lwip_socket()源码如下:

int
lwip_socket(int domain, int type, int protocol)
{
  struct netconn *conn;
  int i;

  LWIP_UNUSED_ARG(domain); /* @todo: check this */

  /* create a netconn */
  switch (type) {
    case SOCK_RAW:
      conn = netconn_new_with_proto_and_callback(DOMAIN_TO_NETCONN_TYPE(domain, NETCONN_RAW),
             (u8_t)protocol, DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_RAW, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
      break;
    case SOCK_DGRAM:
      conn = netconn_new_with_callback(DOMAIN_TO_NETCONN_TYPE(domain,
                                       ((protocol == IPPROTO_UDPLITE) ? NETCONN_UDPLITE : NETCONN_UDP)),
                                       DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_DGRAM, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
#if LWIP_NETBUF_RECVINFO
      if (conn) {
        /* netconn layer enables pktinfo by default, sockets default to off */
        conn->flags &= ~NETCONN_FLAG_PKTINFO;
      }
#endif /* LWIP_NETBUF_RECVINFO */
      break;
    case SOCK_STREAM:
      conn = netconn_new_with_callback(DOMAIN_TO_NETCONN_TYPE(domain, NETCONN_TCP), DEFAULT_SOCKET_EVENTCB);
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%s, SOCK_STREAM, %d) = ",
                                  domain == PF_INET ? "PF_INET" : "UNKNOWN", protocol));
      break;
    default:
      LWIP_DEBUGF(SOCKETS_DEBUG, ("lwip_socket(%d, %d/UNKNOWN, %d) = -1\n",
                                  domain, type, protocol));
      set_errno(EINVAL);
      return -1;
  }

  if (!conn) {
    LWIP_DEBUGF(SOCKETS_DEBUG, ("-1 / ENOBUFS (could not create netconn)\n"));
    set_errno(ENOBUFS);
    return -1;
  }

  i = alloc_socket(conn, 0);

  if (i == -1) {
    netconn_delete(conn);
    set_errno(ENFILE);
    return -1;
  }
  conn->socket = i;
  done_socket(&sockets[i - LWIP_SOCKET_OFFSET]);
  LWIP_DEBUGF(SOCKETS_DEBUG, ("%d\n", i));
  set_errno(0);
  return i;
}

从源码中我们可以看出,本质上是对netconn_new()进行封装。我们关注一下其参数,domain表示协议簇,对于IP/TCP来说该值始终为AF_INET。重点需要关注一下type,我们查看API手册对于几种类型的解释如下:

1. SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接Socket服务,多用于资料(如文件)传输,如TCP协议。

2. SOCK_DGRAM:是提供无保障的面向消息的Socket服务,主要用于在网络上发广播信息,如UDP协议,提供无连接不可靠的数据报交付服务。

3. SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。

Protocol指定套接字使用的协议,对于IPv4,TCP协议提供SOCK_STREAM服务,只有UDP协议提供SOCK_DGRAM服务。

(2) connect ()

connect()指向lwip_connect()(源码较长,就不进行粘贴了),函数的作用与前文介绍netconn_connect功能一致,通过源码可以知道其是通过对netconn_connect的封装实现。在TCP客户端连接中,调用这个函数将发生握手过程,并最终建立新的TCP连接。对于UDP来说调用这个函数只是在UDP控制块中记录远端IP地址与端口号。

(3) write ()

Write()指向lwip_write,源码如下,其通过调用lwip_send实现,flags为0。

ssize_t
lwip_write(int s, const void *data, size_t size)
{
  return lwip_send(s, data, size, 0);
}

了解了以上3个API,接下来开始创建Client_Socket工程:

static void client(void *thread_param)
{
  int sock = -1;
  struct sockaddr_in client_addr;  
  ip4_addr_t ipaddr;  
  uint8_t send_buf[]= " This is MM32F3270 TCP Client_Socket Demo \n";

  IP4_ADDR(&ipaddr,DEST_IP_ADDR0,DEST_IP_ADDR1,DEST_IP_ADDR2,DEST_IP_ADDR3);
  while(1)
  {
    sock = socket(AF_INET, SOCK_STREAM, 0);    //(1)
    if (sock < 0)
    {
      vTaskDelay(10);
      continue;
    } 

    client_addr.sin_family = AF_INET;           //(2)
    client_addr.sin_port = htons(DEST_PORT);   //(3)
    client_addr.sin_addr.s_addr = ipaddr.addr;   //(4)
    memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));    

    if (connect(sock, 
               (struct sockaddr *)&client_addr, 
                sizeof(struct sockaddr)) == -1)    //(5)
    {
        printf("Connect failed!\n");
        closesocket(sock);
        vTaskDelay(10);
        continue;
    }                                               
    while (1)
    {
      if(write(sock,send_buf,sizeof(send_buf)) < 0)   //(6)
        break; 
      vTaskDelay(1000);
    } 
    closesocket(sock);
  }
}

(1)申请一个套接字:socket

(2)协议簇类型(AF_INET用于TCP/IP协议)

(3)将端口赋值给client_addr的sin_port成员

(4)将地址赋值给client_addr的sin_addr.s_addr成员

(5)创建连接,将sock与地址端口进行绑定,建立连接

(6)发送数据

到这里已经完成了Client Socket工程的创建,还有一步比较重要的是配置Client与Server端的IP,将数据发送给服务器端。

在Windows下,通过打开命令行窗口输入:ipconfig可以获取本机地址与服务器的地址。

“基于MM32F3270

可以观察到PC地址为:192.168.105.34,在sys_arch.h文件中对DEST_IP_ADDR0 、DEST_IP_ADDR1、DEST_IP_ADDR2、DEST_IP_ADDR3进行修改,DEST_PORT可选用空闲端口,设备IP需要设置在同一个网段内通信才能进行IP_ADDR0、IP_ADDR1 、IP_ADDR2,需要与PC地址保持一致,IP_ADDR3可以随意设置(和PC地址不一致即可)。

#define DEST_IP_ADDR0               192
#define DEST_IP_ADDR1               168
#define DEST_IP_ADDR2               105
#define DEST_IP_ADDR3               34

#define DEST_PORT                  5001

#define IP_ADDR0                    192
#define IP_ADDR1                    168
#define IP_ADDR2                    105
#define IP_ADDR3                    130

将程序下载入开发板中,使用SSCOM工具进行如下设置:

“基于MM32F3270以太网

点击侦听:

“基于MM32F3270以太网

可以观察到正常侦听并接收到数据,表明实验成功。Demo程序可登录MindMotion的官网(http://www.mindmotion.com.cn/download.aspx?cid=2542&page=2)下载MM32F3270 lib_Samples,工程路径如下:

~\MM32F3270_Lib_Samples_V0.90\Demo_app\Ethernet_Demo\ETH_RTOS\Freertos_Client_socket

来源:灵动MM32MCU
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 25

前言

网络中传递着各种各样的数据包,当设备连接到网络后,为了减少对接收到的数据进行处理的负荷,就需要对设备接收到的数据包进行过滤。STM32MCU的以太网外设提供多种数据包过滤的模式。可以根据以太网帧的目标MAC 地址,源 MAC地址进行过滤,STM32H7系列还提供对 VLANtag和 IP地址,UDP/TCP端口的过滤。

拿 MAC地址过滤来说,SM32MCU支持:单播目标地址过滤,多播目标地址过滤,单播源地址过滤和广播地址过滤。单播目标地址过滤和多播目标地址过滤又分为:Perfect地址过滤和 Hash地址过滤。

perfect地址过滤就是把接收到的以太网帧中的目标地址与 MAC地址寄存器中保存的地址进行比较,如果匹配,数据包就被接受,否则就被丢掉。还可以通过设置“反向过滤”,来翻转过滤的结果,接收到的以太网帧中的目标地址与MAC地址寄存器中保存的地址如果不匹配,数据包就被接收,否则就被丢掉。

Hash地址过滤不是直接比较 MAC地址,而是计算目标 MAC地址的 CRC32值,取其高 6位作为索引去查询 Hash表寄存器中对应的值,来判断是否接收该数据帧。Hash地址过滤的方法稍微复杂,本文接下来将基于STM32H743Nucleo板,通过具体的例程介绍如何实现 Hash地址过滤。

MAC 地址Hash 过滤

过滤原理

在 Hash地址过滤模式下,以太网 MAC通过一张 64位的 Hash表来进行过滤。这张表存储在两个 32位的寄存器中。STM32H743的寄存器 ETH_MACHT0R 保存着 Hash表的前 32位,ETH_MACHT1R中保存着 Hash表的后 32位值。

MAC接收到以太网帧后,会自动计算目标 MAC地址的 CRC值,然后用该 CRC值的高 6位,作为索引号去前面提到的 Hash表寄存器中查找对应位,如果该位的值是 1,则收到的以太网帧通过。否则就丢掉。例如,计算出的 CRC高6位是 0,则对应 ETH_MACHT0R的 bit0,如果该位是 1,则通过。

在初始化的时候,应该根据想要接收的目标 MAC地址,先设置好 ETH_MACHT0R和 ETH_MACHT1R寄存器的值。Hash地址过滤将 48位的 MAC地址,对应到 6位的 Hash值,肯定会出现多个MAC地址对应到一个 6位 Hash值的情况,所以这种过滤方式也被称作 imperfect过滤模式。

Hash值的计算方法

Hash地址过滤模式,最关键的是如何计算6位的Hash值。在RM0433中介绍了 Hash的产生方法,具体如下:

1. 计算目标 MAC地址的 CRC32值。计算 CRC32的方法参见 IEEE802.3的第 3.2.8章中FCS的说明 。根据IEEE802.3中 CRC值的计算要求,和以太网帧中 MAC地址传输的顺序,MAC地址的 CRC值计算方法如下:

  • 第一个 32位数据进行补码运算
  • 输入的数据都进行按位反转顺序
  • 进行 CRC32计算,多项式为 0x4C11DB7
  • 对最终输出数据进行补码运算

2. 对第一步的计算值进行按位反转顺序

3. 取第二步计算值的高 6位

然后就可以根据计算出来的 Hash值,去设置 ETH_MACHT0R和 ETH_MACHT1R寄存器了。

MAC地址过滤的寄存器配置

目标 MAC地址过滤的寄存器配置见下表:

“STM32以太网MAC

例程说明

下面我们将用一个例子来说明如何配置Hash地址过滤。

在该例程中,我们希望 STM32H743Nucleo板只接收广播,发往自己的单播 MAC地址的消息,以及两个特定多播MAC地址的消息。

单播 MAC地址为:00:80:E1:00:00:00,

多播 MAC地址为:01:0c:0d:01:01:03和 01: 00: 5e: a8: 00: 0a。

例程中,我们需要做以下设置:

1. 设置数据包过滤寄存器 ETH_MACPFR中相关位设置,使能单播perfect过滤,多播 Hash过滤,不屏蔽广播消息。

“STM32以太网MAC

2.将单播地址设置到 ETH_MACA0HR和 ETH_MACA0LR中,并使能该地址。那么所有发往00:80:E1:00:00:00的单播数据包都能被收到,其他的单播数据包将被丢掉。

3.设置 Hash过滤表寄存器。在初始化以太网外设时,利用 STM32H743的 CRC外设自动计算 MAC地址的 CRC32值,再得到对应的 Hash值,根据该值去初始化ETH_MACHT0R和 ETH_MACHT1R寄存器。H743Nucleo将可以接收发往 01:0c:0d:01:01:03和 01:00: 5e: a8: 00: 0a MAC地址的多播消息,其他的多播消息都被丢掉。

CRC外设初始化代码:

“STM32以太网MAC

计算并使能 HashMAC地址过滤的代码:

“STM32以太网MAC

运行结果

将附件的例程烧录到H743Nucleo板,通过 XCAP连续发送下面的 6条消息。

“STM32以太网MAC

包括:

两条单播消息,目标MAC地址分别是:00:80:E1:00:00:00和 02:00:00:00:00:00。

三条多播消息,目标 MAC地址分别是:01:0c:0d:01:01:03,01: 00: 5e: a8: 00:0a和 01:0c:0d:01:01:ff。

一条广播消息。

从程序的打印信息里可以看到,H743Nucleo板接收到了其中的 4条消息,MAC地址没有设置的一条单播消息

(02:00:00:00:00:00)和一条多播消息(01:0c:0d:01:01:ff)都被过滤掉了。

“STM32以太网MAC

来源:STM32单片机
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 216

页面

订阅 RSS - 以太网