本系列前面几篇介绍了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)循环前,都会有这样一个配置流程:
在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_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):
思考与改良
在了解了pbuf的相关知识后,我们回过头来审视一下UDP的发送函数,不禁思考这样的写法是否合理?我们的目标是定期发送hello world这样一个简单的字符,而程序中每次发送都重复申请、释放pubf、申请、释放、…。申请pbuf时选择的类型为PBUF_POOL是否又合理(当然总是选择PBUF_POOL是一种很省心的方法)呢?
编程改动工作就留给各位读者,接下来还会继续探讨UDP的使用方法,包括sendto的使用和接收回调机制。
---------------------