设计以太网驱动
2006-07-27潘晓岚杨斌王海花
潘晓岚 杨 斌 王海花
文章以S3C4510B的以太网驱动程序为例,给出了驱动程序的一般设计方法,具体描述了驱动程序的初始化,数据接收和数据发送过程。
本文设计了基于S3C4510B的以太网驱动程序,并通过串口输出。文章给出了对一般性网卡驱动程序的编写,但在调试中,有两点需要注意:一是ARM板是处于大端方式还是小端方式;二是注意字符串的定义,如设备名等。本文适用于所有与NE2000兼容的以太网控制器在uclinux操作系统上驱动程序的开发,并可以供嵌入式系统中驱动程序的开发者参考。
S3c4510b内嵌一个以太网控制器,支持媒体独立接口(Media Independent Interface MII)和带缓冲DMA接口(Buffer DMA Interface,BDI)。可在半双工或全双工模式下提供10M/100Mbps的以太网接入。在半双工模式下,控制器支持CSMA/CD协议,在全双工模式下支持IEEE802.3MAC控制层协议。
因此,S3C4510B内部实际上已经包含了以太网MAC控制,但并未提供物理层接口,因此,需外接一片物理芯片以提供以太网的接入通道。在该系统中,使用RTL8201作为以太网的物理层接口。
以太帧格式
以太网采用广播机制,所有与网络连接的工作站都可以看到网络上传递的数据。它们通过查看包含在帧中的目标地址,确定是否进行接受或放弃。如果确定数据是发给自己的,工作站就会接受数据并传递给高层协议进行处理。
标准IEEE802.3帧结构由以下几部分组成:帧头(Preamble)、帧的起始定界标志(SFD-Start of Frame Delimiter)、目的地址(Destination)、源地址(source)、数据长度(Length)、数据(Data)和帧校验序列(FCS)
在帧结构中,除了数据域的长度不固定外,其他域的长度都是固定不变的。在数据发送时,帧头、帧起始定界符与校验和都是由NIC自动填加的。在接收数据过程中,帧头和帧起始定界符将由NIC跳过,即NIC一旦检测到有效帧头和帧起始定界符,就认为有效数据开始,并将有效数据存入接收缓冲环。存入接收缓冲环的数据包括:目的地址、源地址、数据域长度、数据域及校验和。
帧头是62位的1、0交替的位序列,即1010101010……10共62位。使用这一序列的目的是为了取得接收的串行数据的位同步信号。提取位同步信号的功能由SNI完成。
当发送祯时,每一祯都包含了62位的帧头,在接收祯时,帧头的62位1、0序列则跳过。即使在网络数据传输时丢掉一些1、0序列,也不会影响有效数据的正确接收。
祯起始定界符负责检测有效帧的字节起始位置,由连续2位1组成。一旦NIC的定界逻辑检测到两个连续的1,就认为有效帧已到,把接收到的串行数据以字节方式计数,并将数据送入FIFO(First In First Out)先入先出寄存器。
在网络上传输数据时,由于某种原因,使帧头中的某一位由0变为1,NIC就会接收到错误的帧(由CRC校验逻辑完成),从而拒绝接收该帧数据。
下面定义了两个结构体来描述以太帧头和以太网帧。
/*以太网帧头*/
typedef struct {
BYTE dest[MACLEN];
BYTE srce[MACLEN];
WORD ptype;
}ETHERHDR;
/*以太网接收帧的最大长度,包括校验和CRC在内*/
#define MAXFRAMEC 1518 /*最大帧长度(包括CRC)*/
#define MINFRAMEC 64 /*最小帧长度(包括CRC)*/
/*高层驱动采用以太网的帧长度减去帧头和校验和的长度*/
#define ETHERMTU (MAXFRAME-sizeof(ETHERHDR)) //数据长度
type struct {
ETHERHDR h; /*帧头*/
BYTE data[ETHERMTU]; /*数据*/
LWORD crc; /*CRC*/
}ETHERFRAME;
以太网卡初始化
驱动程序必须有一个初始化方法。在把驱动程序载入系统的时候会调用这个初始化程序。它做以下几方面的工作:检测设备,在初始化程序里可以根据硬件的特征检查硬件是否存在,然后决定是否启动这个驱动程序。配置和初始化硬件,在初始化程序可以完成对硬件资源的配置配置或协商好硬件占用的资源以后,就可以向系统申请这些资源。有些资源是可以和别的设备共享的,如中断。有些是不能共享的,如IO、DMA。接下来要初始化device结构中的变量。最后,可以让硬件正式开始工作。
为了使网卡处于在线工作状态,能够接收或发送数据,首先必须对相关的寄存器进行初始化。这些寄存器包括BDMATXCON、BDMARXCON、BDMATXPTR、BDMARXPTR、BDMARXLST、BDMASTAT、CAM、BDMATXBUF、BDMARXBUF等。
首先对以太网卡的寄存器进行初始化,并设置以太物理地址,参考程序如下:
int s3c4510_eth_init(unsigned char *mac_addr)
{
int i;
// reset BDMA and MAC
outl(BRxRS, BDMARXCON);
outl(BTxRS, BDMATXCON);
outl(MaxRxFrameSize, BDMARXLSZ);
outl(Reset, MACON);
outl(gMACCON, MACON);
s3c4510_eth_fd_init();
for(i = 0; i < 4; i++)
CAM_Reg(0) = (CAM_Reg(0) < < 8) | mac_addr[i];
for(i = 4; i < 6; i++)
CAM_Reg(1) = (CAM_Reg(1) < < 8) | mac_addr[i];
CAM_Reg(1) = (CAM_Reg(1) < < 16);
outl(0x0001, CAMEN);
outl(gCAMCON, CAMCON);
outl(gBDMATXCON, BDMATXCON);
outl(gMACTXCON, MACTXCON);
outl(gBDMARXCON, BDMARXCON);
outl(gMACRXCON, MACRXCON);
return 0;
}
数据发送
在网络中,数据传输的过程是,发送方将待发送的数据按帧格式要求封装成帧,然后通过网卡将帧发送到网络的传输线上,接收方根据接收到的帧的目的地址来确定时候将该帧提交给上层应用程序。本地DMA通道使用缓冲环结构(Buffer Ring Structure)来提供对接收的帧进行缓存。该缓冲环由一系列固定长度的缓冲区组成,每一个缓冲区的长度位256字节,并将它称为一页。因此,也可以说缓冲环是由一系列的页组成,每页的容量为256字节。缓冲环用来存放接收到的帧。接收缓冲环的地址可以由起始页(PAGE STAR)和终止页(PAGE STOP)寄存器来指定。为了将待发送的帧送入网卡的发送缓冲区,必须使用NIC的远程DMA写操作来完成。
帧的发送是指将待发送的数据以帧的形式发送到网络传输线上的过程,因此,数据的发送过程应包括以下几个大步骤:得到Tx帧描述符;装入以太帧;发送以太帧;改变BDMA所有权,能够接收下一个帧。其流程如图所示。参考程序如下:
int s3c4510_eth_send(unsigned char *data, int len)
{
struct frame_desc_struct *fd_ptr;
volatile unsigned long *fb_ptr;
unsigned char *fb_data;
int i;
// 1. Get Tx frame descriptor & data pointer
fd_ptr = (struct frame_desc_struct *)gtx_ptr;
fb_ptr = (unsigned long *)&fd;_ptr-> frame_data_ptr;
fb_data = (unsigned char *)fd_ptr->frame_data_ptr;
// 2. Check BDMA ownership
if(*fb_ptr & BDMA_owner)
return -1;
// 3. Prepare Tx Frame data to Frame buffer
memcpy(fb_data, data, len);
if (len < 60) {
for (i = len; i < 60; i++)
fb_data[i] = 0x00;
len = 60;
}
// 4. Set Tx Frame flag & Length Field
fd_ptr->reserved = (Padding | CRCMode | FrameDataPtrInc | LittleEndian | WA00 | MACTxIntEn);
fd_ptr->status_and_frame_lenght = (len & 0xFFFF);
// 5. Change ownership to BDMA
fd_ptr->frame_data_ptr |= BDMA_owner;
// 6. Enable MAC and BDMA Tx control register
outl(gBDMATXCON, BDMATXCON);
outl(gMACTXCON, MACTXCON);
// 7. Change the Tx frame descriptor for next use
gtx_ptr = (unsigned long)(fd_ptr-> next_frame_desc);
return 0;
}
数据接收
数据接收是指将网络上的数据帧接收并缓存于网卡的接收缓冲环中,然后由主机程序将缓存环的帧读走并存入内存中以备程序使用。从中可以看出,帧的接收过程分成两步:第一步通过本地DMA将帧存入接收缓冲环;第二步是通过远程DMA并在主机的配合下将接收缓冲环中的帧读入内存。
一般设备收到数据后都会产生一个中断,在中断处理程序中驱动程序申请一块sk_buff(skb),从硬件读出数据放置到申请好的缓冲区里。接下来填充sk_buff中的一些信息。skb->dev = dev,判断收到帧的协议类型,填入skb->protocol(多协 议的支持)。把指针skb->mac.raw指向硬件数据然后丢弃硬件帧头(skb_pull)。还要设置skb->pkt_type,标明第二层(链路层)数据类型。可以是以下类型:PACKET_BROADCAST,链路层广播;PACKET_MULTICAST,链路层组播;PACKET_SELF,发给自己的帧;PACKET_OTHERHOST,发给别人的帧(监听模式时会有这种帧);最后调用netif_rx()把数据传送给协议层。netif_rx()里数据放入处理队列然后返回,真正的处理是在中断返回以后,这样可以减少中断时间 (下面的参考程序只是中断之后的部分程序)。部分参考程序如下:
int s3c4510_eth_rcv(unsigned char *data, int *len)
{
struct frame_desc_struct *fd_ptr;
unsigned long rx_status;
unsigned long bdma_status;
unsigned char *tmp;
// 1. Get Rx Frame Descriptor
fd_ptr = (struct frame_desc_struct *)grx_ptr;
if (fd_ptr->frame_data_ptr & BDMA_owner)
return -1;
rx_status = (fd_ptr->status_and_frame_lenght >> 16) & 0xffff;
// 2. Get current frame descriptor and status
bdma_status = inl(BDMASTAT);
// 3. Clear BDMA status register bit by write 1
outl(bdma_status | S_BRxRDF, BDMASTAT);
// 4. If Rx frame is good, then process received frame
*len = 0;
if (rx_status & Good) {
*len = (fd_ptr->status_and_frame_lenght & 0xffff) - 4;
tmp = (unsigned char *)fd_ptr->frame_data_ptr + 2;
// 6. Get received frame to memory buffer
memcpy(data, tmp, *len);
}
// 5. Change ownership to BDMA for next use
fd_ptr->frame_data_ptr |= BDMA_owner;
// Save Current Status and Frame Length field, and clear
fd_ptr->status_and_frame_lenght = 0x0;
// 6. Get Next Frame Descriptor pointer to process
grx_ptr = (unsigned long)(fd_ptr->next_frame_desc);
// 7. Check Notowner status
if (inl(BDMASTAT) & S_BRxNO) {
outl(S_BRxNO, BDMASTAT);
}
if ((inl(MACRXSTAT) & 0x400) == 0x400) {
outl(gBDMARXCON, BDMARXCON);
outl(gMACRXCON, MACRXCON);
}
return 0;
}
到此程序设计部分已经基本完成。
烧写入内核
最后,我把程序烧写入内核来验证本次设计。首先将上述文件拷贝到drivers/net,然后编译uClinux内核: 键入命令:make menuconfig,内核配置; 键入命令:make dep,来寻找依存关系;键入命令:make clean, 清除以前构造内核时生成的所有目标文件,模块文件和一些临时文件; 键入命令:make lib_only,编辑库文件;键入命令:make user_only,编辑用户应用程序;键入命令:make romfs,生成rom文件;键入命令:make image ,做到这一步的时候可能会出现错误的信息提示,这是因为第一次编译时还没有romfs.o,所以出错,等romfs.o编译好了以后,如果再进行内核的编译,就不会出现这个错误信息了,它完全不影响内核的编译,可以完全不必理会这个错误信息,继续进行编译工作; 键入命令:make,通过各个目录的Makefile文件进行,会在各目录下生成一大堆目标文件。
上述步骤完成后,就完成了对uClinux源码的编译工作。