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 不通,截图发过来,老师陪你一句一句排查。慢慢来,你一定能跑通!