目 录
适用于客户要求SylixOS不使用网络协议栈的情形下,网卡驱动通过字符设备驱动实现。
本次移植是以普通版本的SylixOS CPSW网卡,移植步骤如下:
1) 移植前先搭建测试环境,查看网络协议栈网卡驱动是否正常。
2) 了解带网络协议栈网卡驱动代码的驱动框架和寄存器作用。
3) 修改驱动代码,将所有涉及到lwip协议栈的头文件和结构体都注释掉,lwip协议栈的函数尽量重新实现完成相应功能。
4) 实现字符设备操作函数:open、close、read、write和ioctl等函数。
5) 修改收发中断处理函数,描述符的维护。
6) 编写测试程序,测试字符网卡的基本功能。
7) 记录BUG问题,进行分析和总结。
搭建普通版本SylixOS测试环境。目的:测试准备移植的带网络协议栈网卡驱动功能是否正常。
根据bsp工程的README文件来进行搭建环境,烧录SylixOS镜像到板子上。
查看网卡是否配置成功图例如图 3-1所示:
图 3-1 查看网络设备
查看带网络协议栈网卡驱动是否能和电脑进行互通,设置好板子和电脑同一网段,并且板子和电脑可以互通则网络协议栈网卡驱动正常,如图 3-2所示:
图 3-2 查看网卡功能是否正常
目的:了解网卡驱动框架和工作原理。
CPU:为MAC控制器提供描述符,以及负责在网络协议栈和网络驱动之间传送数据。
MAC:通过描述符来控制DMA发送和接收数据。
DMA:用来在缓存区和MAC之间搬送数据,一般是MAC控制器内部的DMA,而不是SOC上的通用DMA。
PHY:用于将数据编码发送或者接收数据的器件。
RJ45:网络接口,比如常见的电口或者光口。
一个网卡就对应着一个struct netdev,如图 4-1 所示。也就是说struct netdev是网卡硬件的抽象。netdev中有两个重要的功能,transmit和receive。当需要发送数据时,协议栈调用transmit方法通知驱动来进行数据发送,当网卡接收到数据时,系统通过receive方法接收数据并通知协议栈进行解析,如程序清单 4-1 网络协议栈网卡驱动的功能函数所示。
struct netdev_funcs _G_cpswNetDevFunc[MAX_CPSW_INS*MAX_SLAVEPORT_PER_INST] = { .init = cpswInit, /* 初始化网卡 */ .transmit = __cpswIfCoreTx, /* 发送函数 */ .receive = __cpswIfCoreRx, /* 接收函数 */ }; |
图 4-1 netdev结构体
当需要发送数据时,协议栈调用transmit方法通知驱动来进行数据发送。
数据经过两次拷贝由应用层复制到协议栈,再由协议栈复制到DMA缓冲区,然后设置网卡从DMA缓冲区中取数据发送。协议栈中传递下来的pbuf中的数据缓冲区是带cache的,且物理地址是连续的,同时物理地址和虚拟地址也是一一对应的,发送流程如图 4-2 所示:
图 4-2 发送数据流程
当网络发数据完成产生中断后,可以将耗时的处理放入网络异步处理作业队列t_netjob中进行排队处理,发送中断流程如图 4-3 所示:
图 4-3 发送中断流程
当网络收发数据完成产生中断后,可以将耗时的处理放入网络异步处理作业队列t_netjob中进行排队处理。接收中断流程如图 4-4 所示:
图 4-4 接收中断流程
当网卡接收到数据时,系统通过receive方法接收数据并通知协议栈进行解析。接收的时候,当网卡收到一帧数据后,将数据保存在DMA缓冲区中,然后通知cpu从缓冲区中取数据处理。cpu将DMA缓冲区中的数据经过两次拷贝之后传送到应用程序。接收数据流程如图 4-5 所示:
图 4-5 接收数据流程
接收因为网卡不知道何时回接收到数据,数据的到来。对于系统来说是一个异步事件,所以必须在驱动初始化的阶段就设置好各个缓冲区描述符中buffer的地址,这样当接收到数据的时候,网卡就将数据放到缓冲区描述符对应的buffer中。为 RX 描述符分配 PBUF,如图 4-6 所示:
图 4-6 为 RX 描述符分配 PBUF
目的:将lwip协议栈的东西都(netdev和pbuf)改掉,实现字符设备操作函数(open,close,read,write,ioctl等)。
使用__NGBE_MAC 来替代 netdev,使用__BUFF来替代 pbuf。如程序清单 4-2 所示:
程序清单 4-2 定义结构体和宏
#ifdef CFG_NET_CHAR_DEV typedef struct { LW_DEV_HDR devHdr; /* 设备头 */
UINT8 cDevNam[32]; /* 设备名称 */ UINT8 hwaddr[8]; /* MAC 地址 */ UINT8 hwaddr_len; /* MAC 地址字节数 */
UINT32 iFlag; /* 文件标志 */ UINT32 iIndex; /* 设备号 */ UINT32 mtu; /* MAC 帧最大长度 */
LW_SEL_WAKEUPLIST selWuList; /* select 函数等待链 */
volatile UINT32 DESC_pRxdataBase; /* 接收缓冲描述符数据指针 */ volatile UINT32 DESC_pTxdataBase; /* 发送缓冲描述符数据指针 */
LW_OBJECT_HANDLE semMRdLock; /* 读数据互斥信号量 */ LW_OBJECT_HANDLE semBRdSync; /* 同步信号量 */ LW_OBJECT_HANDLE semMWrLock; /* 写数据互斥信号量 */
PVOID priv; /* CPSW PORTIF */ } __CPSW_MAC;
typedef __CPSW_MAC *__PNGBE_MAC; /******************************************************************************************* pbuf *******************************************************************************************/ typedef struct {
PVOID payload;
UINT16 len; }__BUFF; typedef __BUFF *__P__BUFF;
/******************************************************************************************* 协议栈的宏进行实现 *******************************************************************************************/ #define __P__BUFF_LEN_MAX 2048
#define ERR_ARG PX_ERROR #define ERR_OK 0 #define ERR_CONN -11 #define ERR_VAL -6 #define ERR_MEM -1 #define err_t CHAR
#define CDNS_BUF_ALIGN 4 #endif |
注册字符网卡设备,将网络协议栈的代码注掉,实现字符网卡的设备创建和操作函数,初始化select等待链如程序清单 4-3 所示:
程序清单 4-3
if (_G_iEthDrvNum == PX_ERROR) {
_G_iEthDrvNum = iosDrvInstall(__cpswCharOpen, LW_NULL, __cpswCharOpen, __cpswCharClose, __cpswIfCoreRx, __cpswIfCoreTx, __cpswIfCoreIoctl); if (_G_iEthDrvNum == PX_ERROR) { return (PX_ERROR); } } _PrintFormat("####pCpswdev->cDevNam= %s_G_iEthDrvNum= %d\r\n",pCpswdev->cDevNam,_G_iEthDrvNum);
if (iosDevAddEx(&pCpswdev->devHdr, pCpswdev->cDevNam, _G_iEthDrvNum, DT_CHR) !=ERROR_NONE) { _PrintFormat("####iosDevAddEx error \r\n"); return (PX_ERROR); }
SEL_WAKE_UP_LIST_INIT(&pCpswdev->selWuList); |
open和close函数中主要就是实现了设备使用次数的计数,open函数根据标志判断是否设置非阻塞模式,如程序清单 4-4 所示:
程序清单 4-4
/******************************************************************************************* ** 函数名称: __cpswCharOpen ** 功能描述: 网卡打开 ** 输 入 : pZYNQMac 设备 ** pcName 设备名称 ** iFlags 打开设备时使用的标志 ** iMode 打开的方式,保留 ** 输 出 : PX_ERROR or ERROR_NONE ** 全局变量: ** 调用模块: *******************************************************************************************/ #ifdef CFG_NET_CHAR_DEV static LONG __cpswCharOpen (__PNGBE_MAC pCpswMac, PCHAR pcName, INT iFlags, INT iMode) { if (pcName == LW_NULL) { _DebugHandle(__ERRORMESSAGE_LEVEL, "device name invalidate.\r\n"); _ErrorHandle(ERROR_IO_NO_DEVICE_NAME_IN_PATH); return (PX_ERROR);
} else { if (iFlags & O_CREAT) { _ErrorHandle(ERROR_IO_FILE_EXIST); /* 不能重复创建 */ return (PX_ERROR); } if (iFlags & O_NONBLOCK) { pCpswMac->iFlag |= O_NONBLOCK; /* 非阻塞模式 */ }
LW_DEV_INC_USE_COUNT(&pCpswMac->devHdr); }
return ((LONG)pCpswMac); } #endif /******************************************************************************************* ** 函数名称: __cdnsCharClose ** 功能描述: 网卡关闭 ** 输 入 : pDwMac 设备 ** 输 出 : ERROR_NONE or PX_ERROR ** 全局变量: ** 调用模块: *******************************************************************************************/ #ifdef CFG_NET_CHAR_DEV static INT __cpswCharClose (__PNGBE_MAC pCpswMac) { if (pCpswMac) { LW_DEV_DEC_USE_COUNT(&pCpswMac->devHdr); /* 减少使用计数 */ } else { return (PX_ERROR); }
return (ERROR_NONE); } #endif |
write函数复用了网卡设备的发送函数__cpswIfCoreTx,修改了函数入口,注释掉了网卡设备相关代码,根据字符设备驱动修改了发送数据部分代码,检查网络连接状态,若网络断开则返回错误,网络协议栈驱动的发送函数修改如图 4-7 所示:
图 4-7 发送函数修改
发送函数在发送数据时,应用层将数据传递到驱动层,需要经过拷贝将数据复制到DMA缓冲区中,使用DMA空间来传数据。主要是因为CPU运算速度与内存读写速度不匹配CPU运算速度要比内存读写速度快很多,这样会使CPU花费很长时间等待数据到来或把数据写入内存。Cache的功能是:提高CPU数据输入输出的速率。
因为程序使用虚拟地址,而且一般使用Cache地址,所以虚拟地址中的内容与其物理地址上的内容不一定一致,所以在启动DMA传输之前一定要将该地址的CACHE刷新,即写入内存。
在进行DMA 操作时,如果没有对Cache 进行适当的操作,将可能产生以下两种错误:
1、DMA 从外设读取数据到供处理器使用。DMA 将外部数据直接传到内存中,但cache 中仍然保留的是旧数据,这样处理器在访问数据时直接访问缓存将得到错误的数据。
API_CacheInvalidate(DATA_CACHE,(PVOID)pBuf,(size_t)(PBUF_LEN_MAX+SIZEOF_STRUCT_PBUF));
2、DMA 向外设写入由处理器提供的数据。处理器在处理数据时数据会先存放到cache 中,此时cache 中的数据有可能还没来得及写回到内存中的数据。如果这时DMA 直接从内存中取出数据传送到外设,外设将可能得到错误的数据。
API_CacheFlush(DATA_CACHE, (PVOID)pBuf, (size_t)SIZEOF_STRUCT_PBUF);
为了正确进行DMA 传输,必须进行必要的cache 操作。cache 操作主要分为 invalidate (作废)和writeback (写回) ,有时也将两着放在一起使用。
申请DMA空间组成一个链表来供MAC控制器的描述符使用,如程序清单 4-5 所示:
程序清单 4-5
#ifdef CFG_NET_CHAR_DEV struct luckDog { void * dogNum; struct luckDog * next; }; static struct luckDog* __TXHead ; static struct luckDog* __TXTail ; /******************************************************************************************* ** 函数名称: __cpsw_TxAlloc ** 功能描述: CPSW 发送描述符链表分配DMA内存 ** 输 入 : pcpswInst CPSW 结构体 ** 输 出 : NONE ** 返 回 : NONE *******************************************************************************************/ static VOID __cpsw_TxAlloc (struct cpsw_inst *pcpswInst) {
UINT Num = 203; struct luckDog*p,*q;
p = (struct luckDog *)malloc(sizeof(struct luckDog));
p->dogNum=API_VmmDmaAllocAlign(__P__BUFF_LEN_MAX,4); p->next = (struct luckDog *)malloc(sizeof(struct luckDog));
q=p; __TXHead = p; p = p->next;
while(Num) { p->dogNum=API_VmmDmaAllocAlign(__P__BUFF_LEN_MAX,4); p->next = (struct luckDog *)malloc(sizeof(struct luckDog)); q=p; p=p->next; Num--; } __TXTail=q; __TXTail->next=__TXHead;//将链表头尾相连,实现链表循环。
} #endif |
网络协议栈驱动的接收中断函数修改,接收中断服务函数就是释放读同步信号量、唤醒读等待链表,如图 4-8 所示:
图 4-8 接收中断函数修改
read函数复用了网卡设备的接收函数__cpswIfCoreRx,修改了函数入口参数,注释掉了网卡设备相关代码,根据字符设备驱动修改了接收数据部分代码,增加了检查连接状态处理,通过读数据互斥锁实现并发对资源互斥访问,接收函数修改如图 4-9 所示:
图 4-9 接收函数修改
ioctl函数来提供给应用层操作select的读非阻塞,获得MAC地址和设置MAC地址等,如程序清单 4-6所示:
程序清单 4-6
/********************************************************************************************************* ** 函数名称: __cdnsCharIoctl ** 功能描述: zynq 设备控制 ** 输 入 : pZynqMac 设备 ** iCmd 控制命令 ** lArg 参数 ** 输 出 : ERROR_NONE or PX_ERROR ** 全局变量: ** 调用模块: *********************************************************************************************************/ #ifdef CFG_NET_CHAR_DEV static INT __cpswIfCoreIoctl (__PNGBE_MAC pCpswMac, INT iCmd, LONG lArg) { __PNGBE_MAC pcpswwMac = pCpswMac; PLW_SEL_WAKEUPNODE pselwunNode;
switch (iCmd) {
case FIOSELECT: pselwunNode = (PLW_SEL_WAKEUPNODE)lArg; SEL_WAKE_NODE_ADD(&pcpswwMac->selWuList, pselwunNode);
switch (pselwunNode->SELWUN_seltypType) {
case SELREAD: { API_SemaphoreMPend(pcpswwMac->semMRdLock, LW_OPTION_WAIT_INFINITE); if (__RxOWNER()) { /* 是否有接收到数据 */ SEL_WAKE_UP(pselwunNode); /* 唤醒 select 等待链 */ } API_SemaphoreMPost(pcpswwMac ->semMRdLock);
break; }
case SELWRITE:
break; case SELEXCEPT:
break; } break;
case FIOUNSELECT: SEL_WAKE_NODE_DELETE(&pcpswwMac->selWuList, (PLW_SEL_WAKEUPNODE)lArg); break;
case CPSW_MAC_SET: memcpy(_G_Cpsw_Mac_Set, (PUCHAR *) lArg, 6); __cpsw_mac_set(pcpswwMac, _G_Cpsw_Mac_Set); cpswInit (pCpswMac);
break; case CPSW_MAC_GET: __cpsw_mac_get(pcpswwMac, _G_Cpsw_Mac_Get); lib_memcpy((PUCHAR *) lArg, _G_Cpsw_Mac_Get, 6);
break;
default: _ErrorHandle(ERROR_IO_UNKNOWN_REQUEST); return (PX_ERROR); }
return (ERROR_NONE); } #endif |
下面主要对select接口读文件集处理流程概述如下:
1) select接口中会调用两次ioctl函数,第一次是执行FIOSELECT命令,第二次是执行FIOUNSELECT命令;
2) 在执行FIOSELECT命令时,首先会把唤醒节点加上到select等待链中,然后检查网络连接状态和接收描述符是否收到数据,如果连接正常并收到数据,直接唤醒等待链,如果连接断开或未收到数据,则执行下一个步骤;
3) 阻塞等待信号量释放,如果上一步直接唤醒等待链,则直接执行下一个步骤,否则会一直阻塞等待信号量释放。在中断服务程序中判断如果有接收中断产生,会唤醒select等待链。
4) 信号量释放后会再次调用ioctl函数执行FIOUNSELECT命令,删除加入的唤醒节点;
5) 更新读文件描述符集标志,退出select函数;
目的:测试网卡驱动基本功能。
测试程序大部分都是写好的只需要对其进行修改:
1) 发送方 IP 地址(板子) 与 接收方 IP(电脑)同网段。
2) 打开的网卡名需一致 可用shell命令 devs查看网卡驱动是否被加载。
3) 发送方 MAC 地址需与驱动层一致。
4) 打开设备,设备名要一致 (如:cpsw1)。
5) 进行收发一次 ARP 报文
6) 收发四次 ICMP 报文
7) 关闭设备。
在发送ICMP包之前,会先调用arp发送arp地址解析协议,然后根据收到的arp的包得知主机的mac地址。然后再发送icmp包,因此在这里首先要看如何实现arp包的发送,Wireshark抓包如图 5-1所示:
图 5-1 Wireshark抓包
ARP包讲解:
Wireshark抓包举例详细分析:以ARP发第一包为例,以太网目的地址:(ff:ff:ff:ff:ff:ff)为广播包,以太网源地址:(02:02:03:04:05:05),网络字节序为大端对齐,帧类型:0x0806,硬件类型:1,协议类型:IPv4,硬件地址长度:6,协议地址长度:4,请求次数:1,发送者硬件地址:(02:02:03:04:05:05),发送者IP地址:192.168.1.236,目标硬件地址:(00:00:00:00:00:00),目标IP地址(192.168.1.10)。如图 5-2 所示:
图 5-2 ARP分析
目的:找BUG。
如果出现接收加打印,造成接收无中断产生,如图 6-1所示:
图 6-1 无接收中断
出现此种情况,第一是因为加了打印,正常情况下,网卡的收发速度都是1ms以下的,在字符网卡中,收发没有像协议栈网卡将读取报文函数 放入了网络异步处理作业队列中一样,字符网卡读取报文应用层是用while循环来读取,并且使用了select唤醒读等待链。所以,当系统在接收数据时因为打印耗费时间片,产生问题。
记录 pBuf 长度
如果记录的 pBuf 长度小于60,即描述符缓冲区偏移量长度小于60,网卡将不发送报文,最大帧大小2016字节(带VLAN 2020)。在看芯片手册,网卡中会有提到发送报文的最小长度和最大长度。如图 6-2 所示:
图 6-2 最大帧大小
加入检测是否连接成功是在热插拔函数中,经过暴力拔插网线,发现重插网线并没有发生重新连接成功提示,于是查看热插拔任务优先级和测试字符网卡任务优先级,发现测试字符网卡任务的优先级为150,热插拔任务的优先级为250,所以热插拔任务是在运行测试字符网卡任务时,不会被调用的。最终对测试字符网卡任务进行优先级的修改即可解决,如图 6-3 所示:
图 6-3 热插拔任务和测试任务
在接收函数中使用while,将出现卡住,是因为接收描述符是循环链表,如果对OWNER位进行判断,当MAC控制器向描述符写入报文之后,又向下一个写入报文,当读取时,会出现驱动层把两个报文都读出来了,可却没有将报文信息传递给应用层,就将数据覆盖掉了,产生数据的丢失,如图 6-4 所示:
图 6-4 接收函数使用while出现问题示例
设置MAC地址,Mac地址就是在媒体接入层上使用的地址,通俗点说就是网卡的物理地址,现在的Mac地址一般都采用6字节48bit。
此程序出现寄存器MAC地址无法修改的问题如图 6-5所示,于是就使用修改驱动的存储MAC的结构体。都是可以的,Mac地址是保存在网卡的EPROM里面,通过网卡生产厂家提供的修改程序可以更改存储器里的地址,即使网卡没有这样的设置我们也可以通过间接的方法修改,一般网卡发出的包的源Mac地址并不是网卡本身写上去的,而是应用程序提供的,只是在通常的实现中,应用程序先从网卡上得到Mac地址,每次发送的时候都用这个Mac做为源Mac而已。
图 6-5 寄存器MAC地址无法修改示例
在进行字符网卡移植的时候,必须要将带网络协议栈的网卡代码看透了,描述符的运行机制和内部的结构是怎么样的,这个是一定要清楚的,因为这个是网卡发送和接受函数的数据结构的核心。这个sync的网卡代码的描述符就很特别,还会将描述符循环链表打断,在进行复原处理,看了寄存器的讲解就知道是因为要搭配寄存器的各个功能配置。
细心细心再细心!!!
《字符设备网卡移植》网讯网卡移植字符设备网卡驱动笔记