学会Zynq(14)UDP发送Hello World

本系列前面几篇介绍了lwIP的相关知识和官方给出的应用实例。从本文开始将进入“实操”阶段,详细介绍Zynq如何使用UDP和TCP两种协议进行通信。建议阅读本文前先了解lwIP相关知识,重复的内容在本文只会简单讲述。

总的来说UDP使用起来比TCP要简单的多。上手UDP可能有两个难点:1.对pbuf的操作感到陌生;2.对UDP接收回调的使用不够灵活。其实TCP的回调机制要更复杂,因此我们先以UDP为例,进一步学习TCP要轻松一些。

在Vivado中搭建硬件环境,启用UART和以太网外设,UART用来发送一些状态信息。本文及后面的所有UDP/TCP工程使用这一个硬件平台就够了。本文先从“UDP发送Hello World”的实例来体会lwIP的使用。

SDK程序设计
按照前文方法,新建工程后启用lwIP 1.4.1库,其余配置都保持默认即可(使用RAW API)。虽然前文详细描述了每个配置的含义,但大多数情况下使用默认值即可。除非用户需要非常高的传输速度,才需要设置一些参数来调优。

使用lwIP需要启动中断系统,从第(8)篇的程序中将中断配置相关的代码摘取出来。sys_intr.h文件的代码如下:

#ifndef SYS_INTR_H_
#define SYS_INTR_H_

#include "xparameters.h"
#include "xil_exception.h"
#include "xdebug.h"
#include "xscugic.h"

#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID

int Init_Intr_System(XScuGic * IntcInstancePtr);
void Setup_Intr_Exception(XScuGic * IntcInstancePtr);

#endif /* SYS_INTR_H_ */

sys_intr.c文件的代码如下:
#include "sys_intr.h"

//---------------------------------------------------------
// 设置中断异常
//---------------------------------------------------------
void Setup_Intr_Exception(XScuGic * IntcInstancePtr)
{
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
(void *)IntcInstancePtr);
Xil_ExceptionEnable();
}

//---------------------------------------------------------
// 初始化中断系统
//---------------------------------------------------------
int Init_Intr_System(XScuGic * IntcInstancePtr)
{
int Status;

XScuGic_Config *IntcConfig;
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
if (NULL == IntcConfig) {
return XST_FAILURE;
}

Status = XScuGic_CfgInitialize(IntcInstancePtr, IntcConfig,
IntcConfig->CpuBaseAddress);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
return XST_SUCCESS;
}

我们先看mian.c文件中的代码,看下RAW模式下使用lwIP的“套路”:
//--------------------------------------------------
// blog.csdn.net/FPGADesigner
// copyright by CUIT Qi Liu
// Zynq Lwip UDP Communication Test Program
//--------------------------------------------------

#include "sleep.h"
#include "user_udp.h"
#include "sys_intr.h"

extern unsigned udp_connected_flag;
static XScuGic Intc; //GIC

int main(void)
{
struct netif *netif, server_netif;
struct ip_addr ipaddr, netmask, gw;

/* 开发板MAC地址 */
unsigned char mac_ethernet_address [] =
{0x00, 0x0a, 0x35, 0x00, 0x01, 0x02};
/* 开启中断系统 */
Init_Intr_System(&Intc);
Setup_Intr_Exception(&Intc);
netif = &server_netif;
IP4_ADDR(&ipaddr, 192, 168, 1, 10);
IP4_ADDR(&netmask, 255, 255, 255, 0);
IP4_ADDR(&gw, 192, 168, 1, 1);

lwip_init(); //初始化lwIP库
/* 添加网络接口并将其设置为默认接口 */
if (!xemac_add(netif, &ipaddr, &netmask, &gw, mac_ethernet_address, XPAR_XEMACPS_0_BASEADDR)) {
xil_printf("Error adding N/W interface\r\n");
return -1;
}
netif_set_default(netif);
netif_set_up(netif); //启动网络
user_udp_init(); //初始化UDP

while(1)
{
/* 将MAC队列中的包传输的LwIP/IP栈中 */
xemacif_input(netif);
if (udp_connected_flag) { //发送
sleep(1);
udp_printf();
}
}
return 0;
}

但凡使用lwIP的程序,无论TCP还是UDP,在进入while(1)循环前,都会有这样一个配置流程:

  • 设置开发板MAC地址
  • 开启中断系统
  • 设置本地IP地址
  • 初始化lwIP
  • 添加网络接口
  • 设置默认网络接口
  • 启动网络
  • 初始化TCP或UDP连接(自定义函数)
  • 在while(1)循环中,第一件事必然是使用xemacif_input函数将MAC队列中的包传输到lwIP栈中,这是Xilinx适配器提供的函数。再之后才是用户代码。我们继续看UDP相关文件中是如何进行连接初始化和“Hello World”字符输出的。

    user_udp.h文件代码如下(为了避免混淆,尽量不要取名为lwIP库中已用过的udp.h/c和tcp.h/c):

    #ifndef SRC_USER_UDP_H_
    #define SRC_USER_UDP_H_

    #include "lwip/err.h"
    #include "lwip/udp.h"
    #include "lwip/init.h"
    #include "lwipopts.h"
    #include "lwip/err.h"
    #include "lwipopts.h"
    #include "netif/xadapter.h"
    #include "xil_printf.h"

    int user_udp_init(void);
    void udp_printf(void);

    #endif /* SRC_USER_UDP_H_ */

    user_udp.c文件代码如下:
    #include "user_udp.h"

    //---------------------------------------------------------
    // 变量定义
    //---------------------------------------------------------
    struct udp_pcb *connected_pcb = NULL;
    static struct pbuf *pbuf_to_be_sent = NULL;

    static unsigned local_port = 7; //本地端口
    static unsigned remote_port = 8080; //远程端口
    volatile unsigned udp_connected_flag = 0; //连接标志

    //---------------------------------------------------------
    // UDP连接初始化函数
    //---------------------------------------------------------
    int user_udp_init(void)
    {
    struct udp_pcb *pcb;
    struct ip_addr ipaddr;
    err_t err;
    udp_connected_flag = 0;

    /* 创建UDP控制块 */
    pcb = udp_new();
    if (!pcb) {
    xil_printf("Error Creating PCB.\r\n");
    return -1;
    }
    /* 绑定本地端口 */
    err = udp_bind(pcb, IP_ADDR_ANY, local_port);
    if (err != ERR_OK) {
    xil_printf("Unable to bind to port %d\r\n", local_port);
    return -2;
    }
    /* 连接远程地址 */
    IP4_ADDR(&ipaddr, 192, 168, 1, 100);
    err = udp_connect(pcb, &ipaddr, remote_port);
    if (err != ERR_OK) {
    xil_printf("Unable to connect remote port.\r\n");
    return -3;
    }
    else {
    xil_printf("Connected Success.\r\n");
    connected_pcb = pcb;
    udp_connected_flag = 1;
    }

    return 0;
    }

    //---------------------------------------------------------
    // UDP发送数据函数
    //---------------------------------------------------------
    void udp_printf(void)
    {
    err_t err;
    char send_buff[14] = "Hello World!\r\n"; //待发送字符
    struct udp_pcb *tpcb = connected_pcb;
    if (!tpcb) {
    xil_printf("error connect.\r\n");
    }
    /* 申请pbuf资源 */
    pbuf_to_be_sent = pbuf_alloc(PBUF_TRANSPORT, 14, PBUF_POOL);
    memset(pbuf_to_be_sent->payload, 0, 14);
    memcpy(pbuf_to_be_sent->payload, (u8 *)send_buff, 14);
    /* 发送字符串 */
    err = udp_send(tpcb, pbuf_to_be_sent);
    if (err != ERR_OK) {
    xil_printf("Error on udp send : %d\r\n", err);
    pbuf_free(pbuf_to_be_sent);
    return;
    }
    pbuf_free(pbuf_to_be_sent); //释放pbuf
    }

    user_udp_init函数中配置远程主机的IP地址和端口号,并与之连接,其中用到的UDP函数已经在前面的文章中介绍过了,相当熟悉。udp_printf函数中申请pbuf资源并发送“Hello World”,这部分的相关操作我们可能第一次见,详细解释看下面的“相关API函数”部分。

    测试结果
    网线连接开发板和电脑,将以太网的IPv4地址修改为UDP初始化函数中设置的地址。打开网络调试助手,选择UDP协议、IP地址和程序中设置的端口号。下载程序,开发板和电脑完成连接,串口打印消息如下:

    在网络调试助手的远程主机部分可以看到程序中给开发板设置的IP地址和端口。每秒收到一个“Hello World!”。

    相关API函数
    lwIP中UDP相关函数的使用已经在前面文章中介绍过,这里不再赘述。我们这里主要了解一下lwIP的err机制和pbuf的操作方法。

    1. lwip/err.h
    程序中我们经常看到ERR_OK、ERR_MEM等字眼,这些都是err.h文件中的宏定义。很多lwIP的函数都会返回一个err_t类型的变量,其实质就是signed char,不同的值代表不同的执行结果。各种错误类型总结如下表:

    程序中在执行关键步骤时,可以打印相关信息,在发送错误时查看错误原因,帮助我们调试程序。示例如下:
    err = udp_send(tpcb, pbuf_to_be_sent);
    if (err != ERR_OK) {
    xil_printf("Error on udp send : %d\r\n", err);
    return;
    }

    2. IP_4ADDR
    我们习惯用4个分开的字节表示IP地址,比如192.168.1.10,但在程序处理时需要将它转换为一个完整的无符号整数类型。lwIP的ip_addr.h中便提供了IP4_ADDR这样一个宏定义,让代码可视性更强。等效的C语言接口如下:
    IP4_ADDR(ipaddr, a,b,c,d)

    通过宏定义我们还可以选择小端模式或大端模式。

    3. pbuf
    如果数据是煤炭,pbuf就是运煤车,而且一列运煤车要有好几节(数据链)。pbuf的相关操作在pbuf.h中。我们先看pbuf的结构体,各成员变量列于下表:

    本例程序中,我们定义了一个pbuf来存储要发送的字符串。

    4. pbuf_alloc
    该函数用于分配指定类型的pbuf,分配的实际内存由设置的pbuf层和请求的大小决定。其函数原型如下:
    struct pbuf * pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)

    第一个参数pbuf层定义头大小;第二个参数length决定pbuf有效载荷的大小;第三个参数pbuf类型决定pbuf的方式和位置。pbuf类型共有四种:

  • PBUF_RAM:以大块(chunk)的形式为pubf分配缓冲区内存和协议头。
  • PBUF_ROM:不会为pbuf分配缓冲区内存和协议头,在分配另一个pbuf并链到ROM型pbuf前时,必须为其预先分配协议头。这种pbuf使用的内存和ROM很相似,不可变且不会被更改。动态内存应该使用PBUF_REF。
  • PBUF_REF:通用不会为pbuf分配缓冲区内存和协议头。假设pbuf只在单个线程中使用,当pbuf进入队列时要调用pbuf_take来复制缓冲区。
  • PBUF_POOL:pbuf被分配作为一个pbuf链,大量数据时使用。
  • pbuf_alloc会返回分配的pbuf。如果分配了多个pbuf,则返回pbuf链中的第一个pbuf。

    5. memset和memcpy
    这两个函数来自string.h。这是两个经典的C语言中的内存操作函数。下面先给出pbuf的操作过程:
    pbuf_to_be_sent = pbuf_alloc(PBUF_TRANSPORT, 14, PBUF_POOL);
    memset(pbuf_to_be_sent->payload, 0, 14);
    memcpy(pbuf_to_be_sent->payload, (u8 *)send_buff, 14);

    memset函数是将某一块内存中的内容全部设置为指定的值,通常为新申请的内存做初始化操作。其原型如下,将s中当前位置后面的n个字节用ch替换。
    void *memset(void *s, int ch, size_t n);

    memcpy函数用于内存拷贝,其原型如下,在src所指的内存地址的起始位置开始,拷贝n个字节到目标dest所指内存地址的起始位置中。
    void *memcpy(void *dest, const void *src, size_t n);

    lwIP的pbuf.c中提供了大量pbuf的操作函数,如查找、比较等,将其熟练掌握将大大提高灵活使用pbuf的编程能力。本系列后面的文章也会给出完整的总结。

    6. pbuf_free
    pbuf的引用计数器相当于指向pbuf的指针数量。pbuf_free函数将减少对pbuf链或队列的引用次数。引用次数减到0时会释放pbuf。对于一个数据链,该函数对链中的每个pbuf都重复这个过程,直到第一个pbuf在递减后引用次数为非0位置。当链中所有的引用次数都是1时,则整个链被释放。

    该函数会返回链中从头开始释放的pbuf数量。比如一个链“a->b->c”,调用pbuf_free(a):

  • 当前引用次数为“3->3->3”,结果为“2->3->3”;
  • 当前引用次数为“1->2->3”,结果为“free->3->3”;
  • 当前引用次数为“1->1->2”,结果为“free->free->1”;
  • 当前引用次数为“2->1->1”,结果为“1->1->1”;
  • 当前引用次数为“1->1->1”,结果为“free->free->free”;
  • 思考与改良
    在了解了pbuf的相关知识后,我们回过头来审视一下UDP的发送函数,不禁思考这样的写法是否合理?我们的目标是定期发送hello world这样一个简单的字符,而程序中每次发送都重复申请、释放pubf、申请、释放、…。申请pbuf时选择的类型为PBUF_POOL是否又合理(当然总是选择PBUF_POOL是一种很省心的方法)呢?

    编程改动工作就留给各位读者,接下来还会继续探讨UDP的使用方法,包括sendto的使用和接收回调机制。
    ---------------------


    文章来源:FPGADesigner的博客
    *本文由作者授权转发,如需转载请联系作者本人

    推荐阅读