lwIP 是用于嵌入式系统的开源TCP / IP网络协议栈。针对嵌入式设备上有没有跑操作系统,lwip提供了两套api:
底层的东西待会再说,赛灵思又两个文档,一个旧的XAPP1026(2014.11.21)和一个新的XAPP1306(2017.08.08)个人觉得旧文档参考价值大一点1
在这里为了方便 我就取了官方的例程(lwip_example)来说明,
pl端只有一个zynq的ip,里面需要配置一点东西ddr,时钟那些就不说了
需要在zynq的ip核中写明自己的板子的MAC IC 的io 注意输入输出和接口速度
要在这里根据板子来设定io
如果用的是赛灵思家的软核那也可以,不过就自己折腾吧
官方文档中给了4个应用实例:
但是这些我们都先不管,我们先来看看官方文档中给出的步骤是怎样的(下面就是翻译):
lwIP Socket API与Berkeley / BSD套接字非常相似。 因此,编写应用程序本身应该没有问题。 唯一的区别在于与lwIP 1.4.1库和xilkernel(或FreeRTOS)耦合的初始化过程
lwIP RAW API更加复杂,因为它需要lwIP内部的知识。原始模式程序的典型结构如下:
上面的过程我想大家已经看出,这里补充一些lwip的小东西,以帮助理解为什么要这样做:
下面说明部分
修改bsp(board support package),加入lwip
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t71PTlps-1578416417687)(https://i.loli.net/2020/01/07/LPfojuJgT6EzYKp.png)]
在lwip设置页,设置成RAW API 和使能dhcp
这里我们就只用RAW API了,因为看了应用用途,要用到socket API那天我还是想想要不要上linux吧
下面我们根据文档的步骤来说明一下,每一步的细节
因为在这里我们用到了dhcp,而且需要先初始化硬件.
这里不讲具体的操作了,我之前的博客有写,这里我们要留意一下他的回调函数timer_callback
void
timer_callback(XScuTimer * TimerInstance)
{
/* we need to call tcp_fasttmr & tcp_slowtmr at intervals specified
* by lwIP. It is not important that the timing is absoluetly accurate.
*/
static int odd = 1;
#if LWIP_DHCP==1
static int dhcp_timer = 0;
#endif
TcpFastTmrFlag = 1;
odd = !odd;
#ifndef USE_SOFTETH_ON_ZYNQ
ResetRxCntr++;
#endif
if (odd) {
#if LWIP_DHCP==1
dhcp_timer++;
dhcp_timoutcntr--;
#endif
TcpSlowTmrFlag = 1;
#if LWIP_DHCP==1
dhcp_fine_tmr();
if (dhcp_timer >= 120) {
dhcp_coarse_tmr();
dhcp_timer = 0;
}
#endif
}
这里主要做了两个操作,一个是dhcp的超时处理,另外一个就是第四步中两个标志位的判断,他们分别驱动一个定时器(1个250ms -> tcp_fasttmr 一个500ms -> tcp_slowtmr)主要用于协议中各个定时器的更新.
这里需要先了解一个结构体 netif:
官方描述: Generic data structure used for all lwIP network interfaces.
别的先不管,看到我们需要在这里注册他的ip地址,网关和子网掩码
所以再看一个结构体 ip_addr:
struct ip_addr {
u32_t addr;
};
这里为了对齐,官方特意整了这个东西…
如果存在dhcp,而且dhcp成功的话,我们只要把ip,子网掩码,网关设置成0,先绑定好网卡地址(MAC),再去创建一个dhcp客户端获取ip地址就可以了.
//指定MAC地址
struct ip_addr ipaddr, netmask, gw;
unsigned char mac_ethernet_address[] =
{ 0x00, 0x0a, 0x35, 0x00, 0x01, 0x02 };
ipaddr.addr = 0;
gw.addr = 0;
netmask.addr = 0;
print_app_header();
lwip_init();
/* Add network interface to the netif_list, and set it as default */
if (!xemac_add(echo_netif, &ipaddr, &netmask,
&gw, mac_ethernet_address,
PLATFORM_EMAC_BASEADDR)) {
xil_printf("Error adding N/W interface\n\r");
return -1;
}
netif_set_default(echo_netif);
/* specify that the network if is up */
netif_set_up(echo_netif);
然后在网络中注册dhcp 客户端:
dhcp_start(echo_netif);
dhcp_timoutcntr = 24;
while(((echo_netif->ip_addr.addr) == 0) && (dhcp_timoutcntr > 0))
xemacif_input(echo_netif);
if (dhcp_timoutcntr <= 0) {
if ((echo_netif->ip_addr.addr) == 0) {
xil_printf("DHCP Timeout\r\n");
xil_printf("Configuring default IP of 192.168.1.10\r\n");
IP4_ADDR(&(echo_netif->ip_addr), 192, 168, 1, 10);
IP4_ADDR(&(echo_netif->netmask), 255, 255, 255, 0);
IP4_ADDR(&(echo_netif->gw), 192, 168, 1, 1);
}
}
ipaddr.addr = echo_netif->ip_addr.addr;
gw.addr = echo_netif->gw.addr;
netmask.addr = echo_netif->netmask.addr;
这里又回到第一步中我们所注册的定时器,里面所指定的dhcp定时器,在这里一超时就会手动设置成静态ip,正好省了我解释怎么设置静态ip
那么 上述的步骤相当于在网络中"站稳了脚步",也可以理解为完成了网络层的配置,下面要进入运输层和应用层的注册.函数是start_application()
所以这里就要用到上面说到的protocol control block 协议控制块来指定一个tcp/udp中各个连接,这里tcp的pcb着实是太长太长了,我们来看看udp_pcb这个结构体的定义:
struct udp_pcb {
/* Common members of all PCB types */
IP_PCB;
/* Protocol specific PCB members */
struct udp_pcb *next;
u8_t flags;
/** ports are in host byte order */
u16_t local_port, remote_port;
/** receive callback function */
udp_recv_fn recv;
/** user-supplied argument for the recv callback */
void *recv_arg;
};
你大概就知道他是在干啥了.而且注册也不需要我们自己来干(因为tcp真的太复杂了)
我们只需要简单地:
~~~~start_application()~~~~
...
pcb = tcp_new();
err = tcp_bind(pcb, IP_ADDR_ANY, port);
...
/* we do not need any arguments to callback functions */
tcp_arg(pcb, NULL);
/* listen for connections */
pcb = tcp_listen(pcb);
...
剩下的也不解读了,都是白菜级的函数调用
重头戏不在这里,在各种回调函数的注册,下面我们来分析一下:
void tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)
{
/* This function is allowed to be called for both listen pcbs and
connection pcbs. */
pcb->accept = accept;
}
这个时候我们去看tcp_pcb是压根没有accept 这个属性的,因为他被定义在TCP_PCB_COMMON(type)这个宏定义里面的DEF_ACCEPT_CALLBACK.
#define DEF_ACCEPT_CALLBACK tcp_accept_fn accept;
所以我们到 tcp_accept_fn 中看看他是怎么定义这个回调函数的:
typedef err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err);
/** Function prototype for tcp receive callback functions. Called when data has
* been received.
* * @param arg Additional argument to pass to the callback function (@see tcp_arg())
* @param tpcb The connection pcb which received data
* @param err An error code if there has been an error receiving
* Only return ERR_ABRT if you have called tcp_abort from within the
* callback function! */
err_t accept_callback(void *arg, struct tcp_pcb *newpcb, err_t err)
{
static int connection = 1;
/* set the receive callback for this connection */
tcp_recv(newpcb, recv_callback);
/* just use an integer number indicating the connection id as the
callback argument */
tcp_arg(newpcb, (void*)(UINTPTR)connection);
/* increment for subsequent accepted connections */
connection++;
return ERR_OK;
}
在这个我们需要先跳进tcp_recv这个函数,肯定优势个注册接收函数的回调注册
void tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv)
{
LWIP_ASSERT("invalid socket state for recv callback", pcb->state != LISTEN);
pcb->recv = recv;
}
果不其然,然后这里我们再看看回调函数的格式定义:
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err);
/** Function prototype for tcp sent callback functions. Called when sent data has
* been acknowledged by the remote side. Use it to free corresponding resources.
* This also means that the pcb has now space available to send new data.
*
* @param arg Additional argument to pass to the callback function (@see tcp_arg())
* @param tpcb The connection pcb for which data has been acknowledged
* @param len The amount of bytes acknowledged
* @return ERR_OK: try to send some data by calling tcp_output
* Only return ERR_ABRT if you have called tcp_abort from within the
* callback function!
*/
这里终于出现了我们的tcp的帧结构了, 上文也介绍过这个pbuf 就是我们所想拿到的"信息"啦
3. 基于此,我们的接收回调函数是这样的:
err_t recv_callback(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err)
{
/* do not read the packet if we are not in ESTABLISHED state */
if (!p) {
tcp_close(tpcb);
tcp_recv(tpcb, NULL);
return ERR_OK;
}
/* indicate that the packet has been received */
tcp_recved(tpcb, p->len);
/* echo back the payload */
/* in this case, we assume that the payload is < TCP_SND_BUF */
if (tcp_sndbuf(tpcb) > p->len) {
err = tcp_write(tpcb, p->payload, p->len, 1);
} else
xil_printf("no space in tcp_sndbuf\n\r");
/* free the received pbuf */
pbuf_free(p);
return ERR_OK;
}
其中 err = tcp_write(tpcb, p->payload, p->len, 1); 就是我们所说的回传了
!千万不要忘记 pbuf_free();
static int connection = 1;
tcp_arg(newpcb, (void*)(UINTPTR)connection);
connection++;
那我们也是一步一步跳进去,看见:
/**
* Used to specify the argument that should be passed callback
* functions.
*
* @param pcb tcp_pcb to set the callback argument
* @param arg void pointer argument to pass to callback functions
*/
void tcp_arg(struct tcp_pcb *pcb, void *arg)
{
/* This function is allowed to be called for both listen pcbs and
connection pcbs. */
pcb->callback_arg = arg;
}
那他这里说,我们可以用这个东西来传递给回调函数,…这不也是云里雾里的,所以我们继续跳进去,发现他也是定义在TCP_PCB_COMMON 中的参数,所以现在我们来看看这到底是什么神仙东西:
#define TCP_PCB_COMMON(type) \
type *next; /* for the linked list */ \
void *callback_arg; \
/* the accept callback for listen- and normal pcbs, if LWIP_CALLBACK_API */ \
DEF_ACCEPT_CALLBACK \
enum tcp_state state; /* TCP state */ \
u8_t prio; \
/* ports are in host byte order */ \
u16_t local_port
这里还是没解释清楚,但是他说了 需要 LWIP_CALLBACK_API 支持才会起效,那我们继续看使能这个宏定义下面的注释:
/* Function to call when a listener has been connected.
* @param arg user-supplied argument (tcp_pcb.callback_arg)
* @param pcb a new tcp_pcb that now is connected
* @param err an error argument (TODO: that is current always ERR_OK?)
* @return ERR_OK: accept the new connection,
* any other err_t abortsthe new connection
*/
好 这里了,这个参数就是个用户提供的参数…想干啥干啥.
5. 好啦 醉翁之意不在酒
其实绕来绕去是为了让大家看到LWIP_CALLBACK_API 这个使能位下的注释,他解释了为什么在accept_callback这个回调中需要重新建立一个新的tcp_pcb的原因.免得一些朋友看得乱了.
切勿 囫囵吞枣 穿凿附会
如果你觉得有丶收获的话
官方docs
步骤和参数
lwip_wiki
XAPP1026翻译
lwip api介绍
LWIP使用经验—变态级(好文章)