Zynq-7000基于zynq平台裸跑LWIP协议栈的详解(万字长文)
1. LWIP协议栈·· 11.1 LWIP库·· 21.2 LwIP原理分析·· 31.2.1 动态内存管理·· 31.2.2 数据包pbuf 41.2.3 网络接口·· 71.3 PS的千兆以太网控制器·· 72. 硬件部署·· 92.1 Ethernet硬件设计·· 92.2 Vivado工程创建·· 103. 软件设计·· 133.1 LwIP echo Server 133.2 Ethe
1. LWIP协议栈
LwIP(LightWeight IP)是TCP/IP协议栈的一个实现。优势在于内存使用率和代码量较小。适用在资源受限的情况下实现和处理Internet协议。TCP/IP协议即传输控制协议/因特网互联协议,是Internet最基本的协议,由网络层的IP协议和传输层的TCP协议组成。其协议内部采用层级结构,每一层通过呼叫下一层所提供的协议来完成数据传输,通俗来说就是根据层级不同不停的分包解包,直至数据顺利传输。IP则提供一个互联网地址给接入网络的地址。
需要注意的是TCP/IP协议不是TCP和IP两个协议的合称。而是因特网整个TCP/IP协议族。TCP/IP协议族以分层的方式设计,好处在于每一种协议可以被单独实现,但是如果严格按照分层实现,协议层之间的通讯会拉低总体性能,所以在保证框架的前提下,重要的的信息可以在各层之间进行共享。目前大部分TCP/IP的实现方式为应用层和底层协议层进行严格划分,底层协议之间存在交叉存取。
网络协议分层如下表:
OSI****七层模型 | ||
---|---|---|
应用层 | 针对特定应用的协议 | |
表示层 | 设备固有格式转网络标准格式 | |
会话层 | 通信管理建立和断开通信连接 | |
传输层 | 管理两节点之间的数据传输,负责可靠传输 | |
网络层 | 地址管理,路由器管理 | |
数据链路层 | 互连设备之间的传送和识别数据帧 | |
物理层 | 界定连接器和网线的规格 | |
TCP/IP四层(五层)模型 | ||
应用层 | 应用程序 | DNS/HTTP/SMTP POP/MIME/SSH SIP/TLS/SSL |
传输层 | 操作系统 | TCP/UDP/UDP_lite ARP/IP/ICMP |
网络层 | 操作系统 | |
物理接口层 | 网卡层 | 设备驱动以太网协议 |
物理层 | 网络接口 | 硬件 |
大部分操作系统中,底层协议族作为拥有应用层进程通讯入口的操作系统内核的一部分被实现。而LwIP在各层之间使用比较松散的通讯机制,通过共享内存的方式实现应用层和底层协议族之间通讯,应用层了解底层协议使用的缓冲机制使应用层更加有效的使用缓冲区,应用层与网络层可以使用相同的内存区且可以直接读写内部缓冲区,从而减少内存复制产生的性能损失。
1.1 LWIP库
LwIP协议栈作为轻量级IP协议,不依赖于操作系统的支持,从指标上来看其减少了对RAM的占用,运行仅需10几KB的RAM和40K左右的ROM。LwIP主要特征如下:
- IGMP协议,用于网络组管理,可以实现多播数据的接收
- Internet协议(IP),包括IPv4和IPv6,支持IP分片与重装,包括通过多个网络接口的数据包转发
- 用于网络维护和调试的Internet控制消息协议ICMP
- 用户数据报协议UDP
- 传输控制协议TCP拥塞控制,往返时间(RTT)估计,快速重传和恢复
- DNS,域名解析
- SNMP,简单网络管理协议
- 动态主机配置协议DHCP
- 以太网地址解析协议ARP
- AUTOIP,IP地址自动配置
- PPP,点对点协议,支持PPPoE
Vivado2017.4提供版本为lwip1.4.1的SDK库,lwip1.4.1为Ethernetlite(axi_ethernetlite)、TEMAC(axi_ethernet)、MAC(GigE)和千兆以太网控制器提供适配器。此库可以在MicroBlaze、ARM Cortex-A9、ARM Cortex-A53、ARM Cortex-R5处理器上运行。Ethernetlite 和 TEMAC 核心适用于 MicroBlaze 系统。千兆以太网控制器和 MAC(GigE)内核仅适用于 ARM Cortex-A9(Zynq-7000处理器设备)、ARM Cortex-A53 和 ARM Cortex-R5(Zynq UltraScale + MPSoC)。
几种硬件环境如下表,纯FPGA使用的处理器是软核MicroBlaze,以太网控制器是软核axi_Ethernet和axi_Ethernetlite。Zynq平台使用的是硬核Crotex-A9,以太网是GigE。
Lwip1.4.1提供两套用户编程接口方式:raw API 和 socket API。
Raw API:为高性能和低内存开销定制。特点是单线程,即网络协议和应用程序存在于一个线程,通过回调函数的进行实现。当接收数据时,应用程序会首先向协议栈注册一个回调函数,当关联的连接有一个信息到达,该回调函数就会被协议栈调用。此种实现方式优点在于执行速度快,消耗内存少,缺点在于网络协议的处理和运算是在同一进程中完成,这就导致应用程序无法连续运算,无法并行。
Socket API:提供一个基于 open – read – write – close模块的BSD socket-style接口,需要操作系统支持,移植性更好,但是内存开销大且性能不如Raw API。
Raw API是在没有操作系统的情况下运行LwIP唯一可用的API,所以本文将以Raw API搭建网络服务,实现向flash写入bin文件。
1.2 LwIP原理分析
一般来说LwIP的运行都存在操作系统的支持,因为会存在进程间通信。最简单的即信号量和邮箱机制。当存在操作系统时,LwIP使用邮箱和信号量实现应用层、协议栈、下层驱动、协议栈间的信息交互。LwIP从本质上讲只是模拟了TCP/IP协议的分层思想,其只是在一个进程中完成了各个层次的所有工作。首先LwIP完成初始化后,阻塞在一个邮箱上,等待接收数据进行处理,数据来源为底层硬件驱动接收到的数据或者应用程序。当在该邮箱获取到数据就,lwIP进行数据解析,依次调用协议栈内部上层相关处理函数处理数据,结束后LwIP继续阻塞等待数据,这个过程期间需要内存管理机制进行辅助,避免时间、内存开销过大。总之,典型LwIP应用系统至少包括三个进程,上层应用程序进程,LwIP协议栈进程,底层硬件数据包收发进程。LwIP协议栈进程在应用进程中调用初始化函数创建且拥有最高优先级,目的在于实时正确的对数据进行响应。
裸机运行时,机制与上述过程类似,只是实现方式上存在差异,是利用回调函数进行实现。
1.2.1 动态内存管理
LWIP协议栈动态内存管理机制分三种:
- C运行库自带的内存分配策略
- 动态内存堆(HEAP)分配策略
- 动态内存池(POOL)分配策略
最常用的方式为动态内存堆(HEAP)分配策略.
其原理是在事先定义好大小的内存块中进行管理, 其内存分配的策略是First Fit方式,只要找到一个比所请求的内存大的空闲块,就从其中切割出合适的块,并把剩余的部分返回到动态内存堆中. 分配的内存块的大小最小会按MIN_SIZE进行分配 (一般MIN_SIZE = 12byte). 12字节中前几字节会存放管理器管理的私有数据,用户不可见.
内存释放是相反过程, 同时分配器会检查其相邻内存块是否空闲,是则合并.。优点是内存浪费小,比较简单,适合用于小内存管理,缺点是如果频繁的动态分配释放,内存随便严重.所以尽量保持分配释放同步。
LWIP对内存管理的实现:
- mem_init() 内存堆得初始化函数, 告知内存堆起止地址, 以及初始化空闲列表,由LWIP初始化时自动调用,内部私有接口,不开放用户层
- mem_malloc() 申请分配内存, 将总共需要的字节数作为参数传递给该函数,返回值是指向最新分配的内存的指针, 如果内存没有分配好, 返回值为空, 分配的空间大小会受到内存对齐的影响,可能会比申请的略大. 并且申请的内存并没有进行初始化. 内存的分配和释放, 不能在中断函数中进行, 内存堆是全局变量. 因此内存的申请\释放必须做线程安全保护,如果有多个线程在同时进行内存得申请和释放,那么可能会因为信号量等待而导致申请耗时较长
- mem_calloc() 对malloc的简单封装,参数为元素数目和每个元素大小,此函数在分配空间的同时对申请的内存去进行清0.
动态内存池(POOL)分配策略:
POOL的分类根据LWIP的配置不同而不同, 定义LWIP_UDP为1,则在编译的时候与UDP相关的内存池就会被建立 ; 定义LWIP_TCP为1, 则在编译的时候建立与TCP类型内存池就会被建立 ; 存放网络包数据信息的 pbuf_pool等.
值得注意的是某种类型的POOL其单个大小是固定的,而分配的个数是由用户决定,把协议栈中的所有的POOL挨个放到一起, 并组成一片连续的内存区域,整合起来就是一个缓冲池.缓冲池必须是以单个缓冲池为基本单位。
LwP常用的内存分配策略两种结合使用:内存堆分配(优点随便分配合适的内存块、缺点内存堆会存在内存碎片,此时有可能在申请较大内存块时申请失败) 内存池分配(优点简单链表操作,分配速度快,缺点是因为提前建立各种POOL,会浪费一定内存空间)。
1.2.2 数据包pbuf
内存管理和数据包的管理密不可分。协议栈各层交互数据种类繁复,大小不定,且数据包来源不定。此时要极力禁止内存拷贝就需要一个标准且高效的数据包管理核心,LwIP数据包管理核心采用pbuf结构体描述数据包。
每个pbuf管理的数据不能覆盖整个数据包,所以需要链表结构.
- next 链表实现结构
- payload 数据指针, 指向该pbuf管理的数据起始地址. 数据的起始地址可以是紧跟在pbuf之后堆RAM,也可以是ROM上的某个地址. 地址位置在哪决定于pbuf类型,
- len字段表示当前pbuf中的有效数据长度
- tot_len字段是pbuf和其后有pbuf有效数据长度,所以链表第一个元素的的tot_len表示整个数据包的长度,最后一个pbuf的tot_len字段必等于len
- ref字段表示pbuf被引用的次数,初始化时被置为 1,当有其他pbuf的next指针指向该buf时,该buf的ref字段值加一,所以要删除一个pbuf,ref的值必须为1时才可以
图1-1 Pbuf结构及分配方式结构图
Pbuf****的创建:
PBUF_RAM :
该类型使用最多,常用于协议栈要发送的数据和应用程序要传递的数据。分配过程会分配相应的大小,从内存堆分配,大小包括结构头大小。申请函数如下:
p = (struct pbuf*)mem_malloc(LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset)+ LWIP_MEM_ALIGN_SIZE(length));
分配空间的大小包括:pbuf 结构头大小 SIZEOF_STRUCT_PBUF,需要的数据存储空间大小 length,还有一个 offset。位置存在于连续的内存空间。
分配成功的pbuf_ram结构如图1-2。
图1-2 Pbuf_Ram结构
PBUF_POOL:
可以在极短时间内完成分配,因为该类型主要通过内存池分配,常用于接收数据包时。申请该类型时协议栈会在内存池中分配满足申请大小的内存池个数。申请函数如下:
p = memp_malloc(MEMP_PBUF_POOL);
申请成功的pbuf_pool结构为链表结构,如图1-3。
图1-3 Pbuf_Pool结构
PBUF_ROM:
在内存堆分配一个相应的pbuf结构头,不申请数据区的空间。申请函数如下:
p = memp_malloc(MEMP_PBUF);
此时申请的内存池类型是MEMP_PBUF,而不是MEMP_PBUF_POOL,因为MEMP_PBUF的类型内存池大小刚好是一个pbuf的大小,该类型的内存池是LwIP为PBUF_ROM和PBUF_REF定制,并且LwIP会为不同的数据结构定制不同类型的内存池。正确分配的PBUF_POOL的结构如图1-4。
图1-4 Pbuf_RAM结构
PBUF_REF:
与PBUF_ROM类似,区别只在于PBUF_ROM指向ROM空间内的某段数据,PBUF_REF指向RAM空间内的某段数据。
总结如下:
任意类型的pbuf可以随意组合,通常情况下是很多不同类型的pbuf组成链表用来保存一段数据。
pbuf****类型 | 分配方式 | 分配函数 | 特征 |
---|---|---|---|
PBUF_RAM | 内存堆 | mem_malloc() pbuf****结构头、数据length、offset | 连续的内存区 |
PBUF_ROM | 内存堆 | memp_malloc(MEMP_PBUF); | 只分配pbuf头不申请空间 |
PBUF_REF | 内存堆 | memp_malloc(MEMP_PBUF); | 只分配pbuf头不申请空间 |
PBUF_POOL | 内存池 | memp_malloc(MEMP_PBUF_POOL); | 分配时间极短 |
Pbuf****的释放:
Pbuf可以被释放的前提是当前pbuf结构体的ref字段为1,该字段表示当前pbuf被引用的次数,当pbuf被创建时初始化为1。也就是说能删除的节点必然是当前链表的首节点,当首节点被删除,LwIP会访问第二个节点如ref为1则继续删除,以此类推,反之此节点之后pbuf链在别处被引用,不在进行后续删除。
当要删除某个pbuf链表时,LwIP首先检查当前pbuf类型,根据类型不同调用不同的内存释放函数进行删除,除PBUF_RAM调用mem_free()删除,其余3类pbuf均调用memp_free()。当然调用函数释放回内存堆涉及内存管理机制,此处是产生内存堆碎片的根源问题,简单来说当内存被释放时为防止产生内存碎片会检查上一个和下一个分配块的使用标志,如其一未被使用就会将释放内存与其合并,组成更大的未使用内存块,使用标志的维护是在分配内存时向数据区后插入的结构体同样为链式结构,已经分配的内存回收后会将标志used清除,但是如果上下内存块都被使用此时就会释放的内存就会成为一个小且独立的内存碎片,此乃根源。释放回内存池类似链表的一套操作。
1.2.3 网络接口
如图1-5,LwIP协议栈通过netif结构体描述一个硬件网络接口。
图1-5 网络接口结构图
网络接口初始化,以太网数据传输等将在软件设计模块根据源码进行解析。
1.3 PS的千兆以太网控制器
根据OSI模型,以太网卡工作在最后两层,物理层和数据链路层。其中物理层定义了数据发送及接受所需要的光电信号,线路状态,时钟基准,数据编码,电路等,同时向数据链路层设备提供标准接口。物理层芯片即为PHY芯片。PHY芯片提供和对端设备连接的功能,同时通过一定的LED显示表示当前连接状态。网卡插入网线,PHY不断发出脉冲信号检测对端设备,协商连接速度,工作模式,是否流控。通常情况下,协商的结果是两个设备中能同时支持的最大速度和最好的双工模式,即AutoNegotiation,自协商。
数据链路层则提供寻址机构、数据帧的构建、数据差错检查、传送控制、向网络层提供标准的数据接口等功能,以太网卡中数据链路层芯片即MAC控制器。
MAC控制芯片与PHY芯片通过MII(Medium Independent Interface)接口进行连接,千兆以太网常用MII接口为GMII(Gigabit Medium Independent Interface)或RGMII(Reduced Gigabit Media Independent Interface)接口连接。
图1-6 GMII接口
如图1-6所示,GMII接口提供了8位数据通道,125MHz的时钟频率,从而具有1000Mbps的数据传输速率。其24根信号线具体功能如下表所示。
信号名称 | 数量 | 功能 | 备注 |
---|---|---|---|
GTX_CLK | 1 | 发送时钟信号 | MAC****到PHY的发送数据接口 |
TXD[7:0] | 8 | 发送数据信号 | |
TX_ER | 1 | 发送错误提示信号 | |
TX_EN | 1 | 发送使能信号 | |
RX_CLK | 1 | 接收时钟信号 | PHY****到MAC的接收数据信号 |
RXD[7:0] | 8 | 接收数据信号 | |
RX_ER | 1 | 接受错误提示信号 | |
RX_DV | 1 | 接收数据有效信号 | |
COL | 1 | 冲突检测信号 | PHY****到MAC的状态指示信号接口 |
CRS | 1 | 载波侦听信号 | |
MDC | 1 | Management Clock | MAC****和PHY间传送控制和状态信息接口 |
MDIO | 1 | management Data IO |
RGMII接口即Reduced GMII,GMII的简化版。GMII的引脚为16个。如图1-7,TX_CTL为信号线,传输TX_EN和TX_ER,上升沿发送TX_EN,下降沿发送TX_ER。RX_CTL信号线上传送RX_DV信号和RX_ER信号。上升沿发送RX_DV,下降沿发送RX_EV。其余同GMII。
图1-7 GMII接口
PS的以太网控制器(GEM)如图1-8所示。PS的千兆以太网兼容上述3种速率的以太网MAC,在这三种速率下可以工作全双工或者半双工模式。DMA控制器通过AHB总线接口连接到存储器,MAC控制器与FIFO接口的连接作为为系统提供scatter-gather类型的功能(scatter-gather DMA 和 block DMA)。
当通过MIO口连接至PS的以太网PHY芯片,则每个控制器使用RGMII接口,目的在于节省引脚,如果使用EMIO;连接至PL端的以太网,每个接口使用GMII接口,可以通过APB总线访问千兆以太网控制器的寄存器,寄存器用于配置MAC的功能,选择不同的操作模式,以及启动和监控网络哦管理统计信息。控制器为管理PHY芯片提供MDIO接口,可以从MDIO接口控制PHY芯片。
图1-8 以太网控制器
2. 硬件部署
2.1 Ethernet硬件设计
本平台搭载两个RJ45以太网接口用于连接以太网线,本次只是用其中XS001,原理图如图2-1。
图2-1 RJ45接口原理图
数据的传输需要依靠PHY芯片,平台网口挂在于PS,所以控制接口为RGMII。其数据流程如图2-2。原理图如图2-3。
图2-2 以太网数据流向
图2-3 88E1518原理图
其与MIO口的具体连接参考核心板的原理图设计。**需要特别注意的是在该平台PHY芯片的复位信号需要PL****给出。**具体引脚约束将在vivado环境搭建时给出。
2.2 Vivado工程创建
本工程使用版本为vivado2017.4。
选择create project 创建工程名称,选择存储位置
图2-4 创建工程
选择RTL project,勾选下方选项会省略添加源文件和约束文件步骤,之后直接进行芯片的选型,对照芯片丝印或者电路图选择合适的芯片型号即可。(在选型时可先选封装类型)工程创建完成。
图2-5 选择工程类型及器件选型
点击Create Block Design 并设置Design name。在Digram点击“+”,输入Zynq添加Zynq7。
图2-6 创建block design及添加zynq IP
之后可以双击Zynq7 根据项目进行相应的配置,注意DDR的型号以及时钟频率的对应即可。配置完成后按F6 Validata Design。有错误需要仔细检查配置问题。
相比于之前重配置工程的配置,网口工程配置略有不同。首先双击打开zynq7 重定义串口后,在peripheral I/O pins界面勾选如下:
图2-7 网口参数配置
图2-8 Zynq外设配置
除红框内必须配置外其余外设根据需求可自由发挥。此工程不在需要添加其他IP。
注意因为PHY芯片的复位信号在本平台上由PL给出所以在顶层文件中需要拉出复位引脚eth_reset,拉出信号后在约束文件config_pins.xdc添加如下约束语句。
图2-9 引脚约束
set_property IOSTANDARD LVCMOS33 [get_ports UART_0_0_txd]
set_property IOSTANDARD LVCMOS33 [get_ports UART_0_0_rxd]
set_property PACKAGE_PIN AA22 [get_ports UART_0_0_txd]
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
set_property PACKAGE_PIN AA23 [get_ports UART_0_0_rxd]
set_property IOSTANDARD LVCMOS18 [get_ports FCLK_RESET0_N_0]
set_property IOSTANDARD LVCMOS18 [get_ports FCLK_CLK0_0]
set_property PACKAGE_PIN AA25 [get_ports led1]
set_property IOSTANDARD LVCMOS33 [get_ports led1]
set_property PACKAGE_PIN L3 [get_ports eth_reset]
set_property IOSTANDARD LVCMOS18 [get_ports eth_reset]
约束完成过后正常综合,生成bitstream,之后export handware,启动SDK,进行软件设计。
3. 软件设计
在完成服务器代码的重构和写入flash功能前可以先利用echo测试网络通路,保证最基本的网络畅通。
3.1 LwIP echo Server
软件设计基于官方LwIP Echo Server工程模板实现。Echo工程模板创建步骤如下:
Step1: file -> new ->application project 输入工程名后 -> next ->finish
图3-1 LwIP echo server 创建
之后点击右击“工程名_bsp”,点击board support package setting进行如下的配置,确认后等待编译完成。
图3-2 LwIP协议栈参数配置
编译完成后修改main.c,修改板子IP和主机在同一局域网下,strat_application()函数修改端口号(不修改默认为7)
图3-3 板子IP设置
修改后编译下载程序。如果一切顺利,在串口打印出PHY信息后会等待几秒打印DHCP Timeout后打印板子IP信息,利用网口助手配置如下,发送信息即可接受相同的信息,完成echo 测试。
图3-4 串口、网口调试信息
注意:
如果不想等待几秒,可以在板级LwIP配置关掉DHCP。
如果卡在xamac_add()函数,尝试关掉自协商,上述配置时是关掉的。
如果网口灯在下载程序后熄灭,检查复位信号是否成功引出。
如果网络连接失败,先检查硬件连接,之后检查IP配置,尤其注意子网掩码和本机IP。
不要进行太过细节的找错,因为echo测试是一个很简单的测试,不涉及性能等配置,所以LwIP参数修改对其影响不大。
3.2 Ethernet Server构建
3.2.1 系统平台
首先和echo server不同的是为提高传输文件效率需要对LwIP板级支持做出一些参数调整。如图
图3-5 LwIP参数配置
主要参数设置如下表所示:
mem_size | mem_n_pbuf | memp_n_tcp_seg |
---|---|---|
可得到的总的对空间 | pbuf****数 | 同时排队的TCP段数 |
pbuf_pool_size | tcp_snd_buf | tcp_wnd |
pbuf****池中的缓冲区数量 | tcp****发送缓冲区空间 | tcp****窗口大小 |
在按照上述参数对LwIP协议进行初始化等待编译完成后,通过对echo server实验的功能提取,LwIP平台初始化可封装成如下的函数:
此函数是裸跑LwIP对平台初始化,LwIP初始化的一个通用设置函数,通过改变ETH_CHANCE的值建立相应的连接(ETH_CHANCE为0:TCP反之UDP)。总的来说一个网卡结构通过如下的源码进行初始化,完成数据包的接收和发送:
static struct netif server_netif; (1)
struct ip_addr ipaddr, netmask, gw; (2)
IP4_ADDR(&gw, 10,129,15,10); (3)
IP4_ADDR(&ipaddr, 10,129,0,1); (4)
IP4_ADDR(&netmask, 255,255,0,0); (5)
netif_init(); (6)
netif_add(&server_netif, &ipaddr, &netmask, &gw,
NULL, ethernetif_init, tcpip_input); (7)
netif_set_default(&server_netif); (8)
netif_set_up(&server_netif); (9)
(1) 声明netif结构体变量
(2) 声明IP地址、子网掩码、网关地址的变量、
(3) 对IP的初始化
(4) 对网关的初始化
(5) 对掩子网掩码的初始化
(6) 初始化全局变量netif_list : netif_list = NULL;
(7) 调用netif_add()函数初始化变量server_netif ,其中ethernetif_init为用户定义的底层接口初始化函数,tcpip_input函数是IP层递交数据包的函数,该值会被传递给server_netif的input字段。
在本平台中ethernetif_init函数替换为xemacpsif_init,tcpip_input替换为Ethernet_input。以下将根据源码对网络接口初始化进行说明:
对源码进行简单的梳理可得到如下netif_add()函数
struct netif *netif_add(struct netif *netif, struct ip_addr *ipaddr,
struct ip_addr *netmask,struct ip_addr *gw,void *state,
err_t (* init)(struct netif *netif),
err_t (* input)(struct pbuf *p, struct netif *netif))
{
static u8_t netifnum = 0;
netif->ip_addr.addr = 0; //复位变量网络接口结构体中各字段的值
netif->netmask.addr = 0;
netif->gw.addr = 0;
netif->flags = 0; //该网卡不允许任何功能使能
netif->state = state; //指向用户关心的信息,这里为 NULL
netif->num = netifnum++; //设置 num 字段,
netif->input = input; //如前所述, input 函数被赋值
netif_set_addr(netif, ipaddr, netmask, gw); //设置变量 enc28j60 的三个地址
if (init(netif) != ERR_OK) { //用户自己的底层接口初始化函数
return NULL;
}
netif->next = netif_list; //将初始化后的节点插入链表 netif_list
netif_list = netif; // netif_list 指向链表头
return netif;
}
上述init()函数为用户自定义函数,在本平台即为xemacpsif_init(),源码:
err_t xemacpsif_init(struct netif *netif)
{
netif->name[0] = IFNAME0; //‘t’初始化结构体name字段
netif->name[1] = IFNAME1; //‘e’这个值不需要关心
netif->output = xemacpsif_output; //IP层发送数据包函数
netif->linkoutput = low_level_output; //ARP模块发送数据包函数
low_level_init(netif); //底层硬件初始化函数
return ERR_OK;
}
其中又进行了底层的low_level_init()函数调用,此函数是底层硬件的初始化函数,主要是对netif结构体的MAC地址字段、mtu字段、flags字段、硬件驱动等进行初始化,其中mtu为最大允许传输单元,flags字段一般设置为开启网卡广播、ARP、并允许有硬件链路连接。至此,网络接口初始化完毕。
(8) 调用netif_set_default函数初始化缺省网络接口。在协议栈中netif_list指向netif网络接口结构体链表,netif_default指向缺省的网络接口结构。当IP层数据需要发送,netif_default会以netif_list为索引选择满足需求的网络接口发送数据包,如果链表中没有可发送数据的接口,则调用缺省的网络接口直接发送数据包。此语句执行的效果就是该接口设置为缺省的网络结构。
(9) 使能网络接口函数。使能成功后网络接口即可正常收发数据包。
网口初始化完成后就可以使用一些应用程序完成功能。在编写应用程序,最好是对网络的数据传输、数据帧格式、IP分片重装、ICMP处理等一些网络知识有一定了解。
3.2.2 TCP Server
TCP是LwIP很庞大且繁复的一个部分,所以本节只是基于协议栈建立一个TCP服务并简单设计一个应用程序。在RAW API编程中常用TCP函数如下:
组别 | API | 功能描述 |
---|---|---|
TCP****建立连接 | tcp_new() | 创建一个TCP的PCB控制块 |
tcp_bind() | 为TCP的PCB控制块绑定本地IP和端口 | |
tcp_listen() | 开启TCP的侦听 | |
tcp_accept() | 控制块accept字段注册的回调函数,侦听到连接时被调用 | |
tcp_conect() | 连接远程主机 | |
发送TCP数据 | tcp_write() | 构造一个报文并放到控制块的发送缓冲队列中 |
tcp_sent() | 控制块sent字段注册的回调函数,数据发送成功后被回调 | |
tcp_output() | 将发送缓冲队列中的数据发送出去 | |
接收TCP数据 | tcp_recv() | 控制块recv字段注册的回调函数,当接收到新数据时被调用 |
tcp_recved() | 当程序处理完数据后必须调用的函数,通知内核更新接收窗口 | |
轮训函数 | tcp_poll() | 控制块poll字段注册的回调函数,实现周期性调用 |
关闭和中止连接 | tcp_close() | 关闭一个TCP连接 |
tcp_err() | 控制块err字段注册的回调函数,遇到错误时被调用 | |
tcp_abort() | 中断TCP连接 |
在上述内容中提到,裸核运行LwIP是基于回调函数实现,回调即是在此处回调。建立一个TCP连接可封装成如下的函数:
int new_tcp_connect()
{
struct tcp_pcb *pcb;
err_t err;
pcb = tcp_new(); //创建PCB
if(!pcb){
xil_printf(“error createing pcb\r\n”);
return -1;
}
err = tcp_bind(pcb,IP_ADDR_ANY,ETH_PORT); //绑定端口号
if(err != ERR_OK){
xil_printf(“unable to bind to port\r\n”);
return -2;
}
tcp_arg(pcb,NULL); //可调用相应的回调函数,关联相应的PCB
pcb = tcp_listen(pcb); //监听连接
if(!pcb){
xil_printf(“out of memory while tcp_listen\r\n”);
return -3;
}
tcp_accept(pcb,(tcp_accept_fn) accept_callback_tcp);
xil_printf(“tcp server started @port %d\r\n”,ETH_PORT);
return 0;
}
accept_callback_tcp()函数为侦听到连接时的回调函数,accept()会分配资源给此次连接,同时回调。“DDOS攻击”就是利用accept(),发起大量客户端请求耗空服务器资源。回调函数如下:
int accept_callback_tcp(void *arg, struct tcp_pcb *newpcb, err_t err)
{
xil_printf(“tcp_server : connection accepted\r\n”);
c_t_pcb = newpcb;
//设置接收回调
tcp_recv(c_t_pcb,recv_callback_tcp);
tcp_arg(c_t_pcb,NULL);
return ERR_OK;
}
此时会继续回调接收回调函数recv_callback_tcp(),注意这两次回调的意义并不一样,区别在于accept()和recive()的区别。在接收回调函数里会对接收的数据进行简单的分析处理,recv_callback_tcp实现如下:
static err_t recv_callback_tcp(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err)
{
struct pbuf *q;
if (!p) {
tcp_close(tpcb);
tcp_recv(tpcb, NULL);
xil_printf(“tcp connection closed\r\n”);
return ERR_OK;
}
q = p;
if (q->tot_len == 6 && !(memcmp(“update”, p->payload, 6))) {
start_update_flag = 1;
sent_msg_tcp("\r\nStart QSPI Update\r\n");
} else if (q->tot_len == 5 && !(memcmp(“clear”, p->payload, 5))) {
start_update_flag = 0;
total_bytes = 0;
sent_msg_tcp(“Clear received data\r\n”);
xil_printf(“Clear received data\r\n”);
} else {
while (q->tot_len != q->len) {
memcpy(&rxbuffer[total_bytes], q->payload, q->len);
total_bytes += q->len;
q = q->next;
}
memcpy(&rxbuffer[total_bytes], q->payload, q->len);
total_bytes += q->len;
}
tcp_recved(tpcb, p->tot_len);
pbuf_free§;
return ERR_OK;
}
在接收回调函数内完成对数据的接收和处理,这里的数据处理是总体来说对上文提到的pbuf的处理。之后程序进入while(1)循环执行数据包的接收,以及在transfer_data_tcp()函数里执行写入flash操作。值得注意的是while循环里面的TcpFastTmrFlag 和 TcpSlowTmrFlag是保证TCP传输的标志位(目前测试结果显示并不影响点对点连接),定时器中断分别以250ms和500ms的周期来改变这两个标志位。数据包接收函数xemacif_input函数将接收的数据包传递给LwIP,LwIP调用相关回调处理程序。
LwIP使用两个周期性定时器,周期250ms和500ms,这点类似于BSD中的TCP。这个两个定时器同时会被用于实现更复杂的逻辑定时器,比如重发定时器,TIME_WAIT定时器以及延迟ACK定时器。这只是TCP处理的很小的一个部分,因为对于LwIP而言,50%的代码量都是在维护TCP,目的在于为应用层提供可靠地字节流服务,包括但不限于糊涂窗口的避免、快速重发、往返时间估计、拥塞控制等等。
本demo的最终目的在于实现网口写入flash操作,所以当完成TCP服务的搭建后即需要完成对flash的初始化、擦除、写入,本平台flash大小16MB。如上文所说,更新flash的操作在transfer_data_tcp()完成,具体实现如下:
int transfer_data_tcp()
{
if (start_update_flag) {
xil_printf("\r\nStart QSPI Update!\r\n");
xil_printf(“file size of BOOT.bin is %lu Bytes\r\n”, total_bytes);
if(qspi_update(&(initps_para_ptr->Xqspi),total_bytes,rxbuffer)
!= XST_SUCCESS)
{
sent_msg_tcp(“Update Qspi Error!\r\n”);
xil_printf(“Update Qspi Error!\r\n”);
}
else
total_bytes = 0;
}
start_update_flag = 0;
return 0;
}
在flash操作中,主要实现以下几个函数来实现功能:
s32 qspi_init(XQspiPs* _XQspiPs_Ptr);
void FlashErase(XQspiPs* _XQspiPs_Ptr, u32 Address, u32 ByteCount);
void FlashRead(XQspiPs* _XQspiPs_Ptr, u32 Address, u32 ByteCount, u8 Command);
void FlashWrite(XQspiPs* _XQspiPs_Ptr, u32 Address, u32 ByteCount, u8 Command);
int qspi_update(XQspiPs* _XQspiPs_Ptr,u32 total_bytes, const u8 *flash_data);
3.2.3 UDP Server
UDP服务的创建相比于TCP在理论上简单许多,实现上类似。系统平台设计不需要变更,只需要注意将UDP选项打开即可。建立服务的过程封装成如下的函数:
int new_udp_connect(){
struct udp_pcb *pcb;
err_t err;
unsigned port = ETH_PORT;
//创建pcb
pcb = udp_new();
if(!pcb)
{
xil_printf(“Error creating PCB. Out of Memory\r\n”);
return -1;
}
//绑定端口
err = udp_bind(pcb,IP_ADDR_ANY,port);
if(err != ERR_OK)
xil_printf(“error on udp_connect: %x\n\r”, err);
//设置接收回调函数
udp_recv(pcb,(udp_recv_fn)recv_callback_udp,NULL);
return 0;
}
同样是基于回调函数,但是UDP是直接回调接收回调函数,这与TCP略有差异,UDP接收回调函数如下:
回调函数同样是对接收数据进行简单的处理。While(1)循环内部transfer_data_udp()与TCP相同,也可自行实现其他应用程序。
4. 下载测试
对于搭建TCP服务器更新flash来说,下载验证只需要串口调试助手和网络调试助手,辅助以内部实现的进度打印函数即可很容易的进行验证。配置参数,连接成功后如图4-1。
图4-1 测试设置
勾选启动文件下载,选择合适bin文件发送,发送完毕后发送“update”,等待片刻即可看到图4-2更新flash成功的串口和网口信息
图4-2 更新flash成功
通过改变应用函数可以进行功能的改变,以网口传输bin文件实现PS重配置PL为例,只需要将重配置函数进行封装,将接受地址指针作为参数传入重配置函数即可,但是需要注意的是因为跟新PL所以网口必须断开,然后进行重配置,如果新bin文件配置了网口,那么可以重新new一个服务出来进行连接.
DDR_ANY,port);
if(err != ERR_OK)
xil_printf(“error on udp_connect: %x\n\r”, err);
//设置接收回调函数
udp_recv(pcb,(udp_recv_fn)recv_callback_udp,NULL);
return 0;
}
同样是基于回调函数,但是UDP是直接回调接收回调函数,这与TCP略有差异,UDP接收回调函数如下:
[外链图片转存中…(img-6Y7ydUFi-1607853535569)]
回调函数同样是对接收数据进行简单的处理。While(1)循环内部transfer_data_udp()与TCP相同,也可自行实现其他应用程序。
4. 下载测试
对于搭建TCP服务器更新flash来说,下载验证只需要串口调试助手和网络调试助手,辅助以内部实现的进度打印函数即可很容易的进行验证。配置参数,连接成功后如图4-1。
[外链图片转存中…(img-R4oPqjT6-1607853535571)]
图4-1 测试设置
勾选启动文件下载,选择合适bin文件发送,发送完毕后发送“update”,等待片刻即可看到图4-2更新flash成功的串口和网口信息
[外链图片转存中…(img-1xpycC0S-1607853535574)]
图4-2 更新flash成功
通过改变应用函数可以进行功能的改变,以网口传输bin文件实现PS重配置PL为例,只需要将重配置函数进行封装,将接受地址指针作为参数传入重配置函数即可,但是需要注意的是因为跟新PL所以网口必须断开,然后进行重配置,如果新bin文件配置了网口,那么可以重新new一个服务出来进行连接.
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)