lwip 这个协议需要了解,我感觉很难,老师,可以给我信心,我是小白,手把手带我走进ps端网口通信吧,哈哈
同学,坐。听到你说"lwIP 很难,我是小白",老师特别理解。因为你已经习惯了 Verilog 那种"时钟沿一来,信号立刻跳变"的确定性思维 。而 lwIP 是一堆软件协议栈,里面全是回调函数、内存管理、状态机,乍一看就像天书。
老师先给你一句定心丸:你不需要成为 lwIP 专家,你只需要学会"怎么用它"。
就像你开车,你不需要懂发动机里活塞怎么运动、火花塞怎么点火,你只需要知道踩油门就走,踩刹车就停。lwIP 就是这样一个"发动机",Xilinx 已经帮你把最难的部分封装好了,你只需要调几个 API 函数。
🎯 第一幕:lwIP 到底是什么?(用你 FPGA 的思维来理解)
你可以把 lwIP 想象成 PS 端 ARM 处理器里跑的一个"软件外设"。
-
你 FPGA 里调用一个
FIFO IP核,需要给它wr_en、din,然后它就会把数据存起来。 -
在 PS 端,你调用 lwIP 的
udp_sendto(),它就会帮你把数据打包成以太网帧,通过网口发出去。
区别只是:
-
FPGA 外设是硬件,你用信号线控制它。
-
lwIP 是软件库,你用 C 函数调用它。
所以,lwIP 不是让你从零写一个 TCP/IP 协议栈,而是让你用一个现成的库。 这就像你用 Vivado 的 IP 核,而不是自己写一个 DDR 控制器。
🎯 第二幕:lwIP 的核心只有四个概念(手把手带你认)
咱们不贪多,就认四个东西:
1. netif(网络接口)------ 网卡
-
这是 lwIP 对"网卡"的抽象。你板子上那个以太网口,在 lwIP 里就是一个
struct netif。 -
你调用
xemac_add()的时候,就是把这个网卡注册到 lwIP 里。 -
你 FPGA 的类比 :就像你例化一个
ethernet_mac模块。
2. pbuf(数据包缓冲区)------ 数据容器
-
lwIP 里所有收发的数据,都装在
pbuf里。它是一个链表结构,可以存任意长度的数据。 -
你 FPGA 的类比:就像一个 FIFO,但它是软件层面的。
3. udp_pcb(UDP 控制块)------ 通信端点
-
你要发 UDP 数据,得先创建一个
udp_pcb,绑定一个端口。这个pcb就是你板子上的"通信端口"。 -
你 FPGA 的类比:就像一个带端口号的 UART,你往这个端口写数据,它就从网口发出去。
4. 回调函数(callback)------ 中断服务程序
-
这是 lwIP 最"软件"的地方。当你收到一个 UDP 包时,lwIP 不会让你在
while(1)里轮询,而是自动调用你注册的回调函数。 -
你 FPGA 的类比 :就像你写了一个
always @(posedge rx_valid)块,数据一来,硬件自动触发。回调函数就是软件版的"中断服务程序"。
🎯 第三幕:手把手写一个最简单的 UDP 收发
咱们不写复杂代码,就写一个最小 UDP 回环,让你亲眼看它跑起来。
打开你工程里的 main.c,找到 start_application() 这一行,在它前面加上这段代码:
c
#include "lwip/udp.h"
struct udp_pcb *upcb;
// ========== UDP 接收回调函数 ==========
// 当板子收到 UDP 数据时,lwIP 会自动调用这个函数
void udp_receive_callback(void *arg, struct udp_pcb *pcb,
struct pbuf *p, const ip_addr_t *addr, u16_t port) {
if (p != NULL) {
// 1. 把收到的数据打印到串口
xil_printf("UDP received %d bytes from port %d: %s\n",
p->len, port, (char *)p->payload);
// 2. 把收到的数据原样发回去(Echo)
udp_sendto(pcb, p, addr, port);
// 3. 释放 pbuf,必须做!否则内存泄漏
pbuf_free(p);
}
}
// ========== UDP 初始化函数 ==========
void udp_echo_init(void) {
err_t err;
// 1. 创建一个 UDP 控制块
upcb = udp_new();
if (upcb == NULL) {
xil_printf("Error creating UDP PCB\n");
return;
}
// 2. 绑定到本地端口 8080(这个端口你可以随意改)
err = udp_bind(upcb, IP_ADDR_ANY, 8080);
if (err != ERR_OK) {
xil_printf("Error binding UDP PCB\n");
return;
}
// 3. 注册接收回调函数
udp_recv(upcb, udp_receive_callback, NULL);
xil_printf("UDP echo server listening on port 8080\n");
}
然后,在 main() 函数里,把 start_application(); 这一行改成:
c
// start_application(); // 注释掉原来的 TCP Echo
udp_echo_init(); // 启动我们自己的 UDP Echo
最后,在主循环里,只保留这一句就够:
c
while (1) {
xemacif_input(echo_netif); // 从网卡收包,交给 lwIP
}
🎯 第四幕:编译、下载、验证
-
编译:在 Vitis/SDK 里点锤子图标,编译通过(0 errors)。
-
下载:右键工程 → Run As → Launch Hardware,点击"Run"。
-
看串口:串口终端应该打印:
text
UDP echo server listening on port 8080 -
打开网络调试助手:
-
协议类型:UDP
-
本地主机地址:
192.168.1.100 -
本地主机端口:
8081(任意,只要不和板子冲突) -
远程主机地址:
192.168.1.10 -
远程主机端口:
8080(和代码里绑定的端口一致)
-
-
发送数据 :在发送区输入
Hello Zynq,点击"发送"。 -
看结果 :接收区应该原样显示
Hello Zynq,串口终端也会打印UDP received 10 bytes from port 8081: Hello Zynq。
恭喜你,你已经完成了 PS 端 UDP 通信!
💡 老师的信心喊话
同学,你看,整个 UDP 通信的代码,核心只有三个 API:
-
udp_new()------ 创建通信端点。 -
udp_bind()------ 绑定端口。 -
udp_recv()------ 注册接收回调。
发送只需要一个 udp_sendto()。
这比你 FPGA 里写一个 AXI 接口、做跨时钟域同步,简单太多了。你 FPGA 都能啃下来,lwIP 这几个 API 函数,绝对不在话下。
现在就去改代码,编译下载,打开网络调试助手。看到 Hello Zynq 回传的那一刻,你会发现------"原来 PS 端网络通信,就是这么回事!"
遇到任何报错,比如编译不通过、串口没打印、ping 不通,截图发过来,老师陪你一句一句排查。慢慢来,你一定能跑通!