找回密码
 注册

QQ登录

只需一步,快速开始

搜索

ENC28J60学习笔记——AVRNET项目

[复制链接]
山海致远 发表于 2013-7-13 11:18:56 | 显示全部楼层 |阅读模式
1前言
嵌入式以太网开发,可以分为两个部分,一个是以太网收发芯片的使用,一个是嵌入式以太网协议栈的实现。以太网收发芯片的使用要比串口收发芯片的使用复杂的多,市面上流通比较广泛的以太网收发芯片种类还不少,有SPI接口的ENC28J60,也有并口形式的RTL8019S,CS8900A等。嵌入式以太网协议栈有著名的uIP协议栈,Lwip协议栈,还有其他嵌入式高手开发的协议栈。无论是硬件还是软件,都无法分出高下,适合项目需求的才是最好的。
1.1 写作理由
        在前言的最后,再说明一下我写作的理由。以前从淘宝上购买过ENC28J60,店家信誓旦旦地说能提供51 AVR LPC STM32等多个平台的代码,可以实现一个网页控制LED。头脑一热买了回来,买回来才发现,店家提供的资料零零散散,非常难懂,虽然不贵仅仅需要40多元,现在只需要20多元。但是总感觉有欺骗的嫌疑,这也可以映射出中国人做技术买卖的原则,产品多是实物而非服务。几经周转,发现原来这些ENC28J60的代码都出自一个地方——AVRNET,源自老外的一个开源项目。把最原始的代码拿来细细品味,以太网协议就不那么神秘了。在这里说一下ENC28J60的使用,熟悉了ENC28J60的驱动可以分几步走。第一步,通过ENC28J60移植uIP或者lwIP协议栈,实现TCP或是UDP通信,第二,顺着AVRNET项目走,实现一个简单的web服务器,运行静态或者动态网页。嵌入式以太网和计算机以太网开发不同,对于TCP通信而言没有windwos socke用,对于网页编程而言也没有ISS或PHP,所示实现起来会比较麻烦,但是也非常有乐趣。
1.2 平台说明
硬件平台 Atmega32 + proteus 7.10+WinPcap
编译平台 AVR Studio 6
        关于硬件平台,由于AVRNET项目采用ATmega32,分析的时候也采用Atmega32。就ENC28J60而言,对于其他的平台,例如STM32或是MSP而言只需要修改SPI操作即可。由于没有硬件平台,所以使用proteus仿真,注意仿真以太网是proteus需要安装WinPcap。
        关于编译平台,AVRNET项目使用的是AVR Stdui 4.XX。这个版本稍显老旧,我就进行了相关修改,在AVR Studio 6中重新编译,并修正了几个错误。当然其他的编译平台也适用。
        总结一句,平台选用原则——“求同存异”。
1.3 资料准备
        以太网开发是非常复杂的工作,在开始之前最好先大致浏览一些ENC28J60的使用手册,MICROCHIP可以下载,中文版本阅读非常方便。除此之外,需要认真阅读TCP IP相关知识,推荐一本图书《嵌入式Internet TCP/IP基础、实现和应用》。
        嵌入式开发总是一个反复借鉴的过程。该部分代码参考了AVRNET项目和奋斗开发板的相关范例。AVRNET项目网址链接http://www.avrportal.com/?page=avrnet
2 寄存器和寄存器操作
       ENC28J60的寄存器很多,操作这些寄存器需要一个良好的代码组织工作。在AVRNET项目中,把ENC28J60的驱动分解成ENC28J60.h文件和ENC28J60.c文件。H文件中主要描述ENC28J60寄存器的基本定义,而C文件主要实现了这些寄存器的操作。
2.1 寄存器定义
首先分析一下ENC28J60头文件。阅读数据手册之后,会发现ENC28J60寄存器数量较多,通过分析和整理,操作ENC28J60的寄存器需要注意以下3点。
(1)   共有三种不同形式的寄存器——控制寄存器,以太网寄存器 和PHY寄存器,不同的寄存器以不同的字母开头,以E、 MA和MI加以区分。操作这三种不同的寄存器需要不同的组合命令。
(2)   寄存器被分布在4个不同的bank中,也就是说存在地址相同的寄存器,但是这些寄存器却位于不同的分区中,在操作寄存器之前必须选中正确的bank。
(3)  虽然存在4个bank,但是有5个寄存器在4个bank的位置相同,它们是EIE、 EIR、ESTAT、ECON1、ECON2。不言而喻,这5个寄存器将会非常重要。
AVRNET项目中,寄存器被定义成8位长度,而这8位长度包含了三个部分,地址bit7(最高位)用以区分PHY和MAC寄存器;地址bit6和bit5用以区分BANK,2位空间正好区分4个BANK;地址的最后5位才是寄存器的地址。通过这种方式就可以区分所有的寄存器了。列举了几行代码。由于头文件很长,所以不全部列出。
// bank0 寄存器
#define ERDPTL            (0x00|0x00)
#define ERDPTH            (0x01|0x00)
#define EWRPTL            (0x02|0x00)
// bank1 寄存器
#define EHT0              (0x00|0x20)
#define EHT1              (0x01|0x20)
#define EHT2              (0x02|0x20)
// bank2 寄存器
#define MACON1           (0x00|0x40|0x80)
#define MACON2           (0x01|0x40|0x80)
#define MACON3           (0x02|0x40|0x80)
//bank3 寄存器
#define MAADR1           (0x00|0x60|0x80)
#define MAADR0           (0x01|0x60|0x80)
#define MAADR3           (0x02|0x60|0x80)
       例如ERDPTH为位于BANK0的以太网寄存器,第一个数字0x01代表BANK0中的地址,该地址为0x01,第二个数字0x00代表BANK编号,该编号为0,意味第0个BANK;EHT1为位于BANK1中的控制寄存器,第二个0x20代表BANK地址为1,请注意由于BANK编号被保存在bit6和bit5,所以此处为0x20,绝不是0x10;MACON2为位于bank2的以太网寄存器,第一个数字0x01代表在该BANK中的寄存器地址,第二个数字0x40代表BANK编号,而第三个数字0x80代表该寄存器为以太网寄存器或是PHY寄存器,这些寄存器的操作和控制寄存器有区别。
       为了方便寄存器操作,h文件中还定义了寄存器地址操作的掩码,简单而言就是需要查看哪些位,不需要查看哪些位。
/* 寄存器地址掩码 */
#defineADDR_MASK        0x1F
/* 存储区域掩码 */
#defineBANK_MASK        0x60
/* MAC和MII寄存器掩码*/
#defineSPRD_MASK        0x80
       另外还有比较特殊的5个控制寄存器,EIE,EIR,ESTAT,ECON2和ECON1
/* 关键寄存器 */
#defineEIE                     0x1B
#defineEIR                     0x1C
#defineESTAT                   0x1D
#defineECON2                  0x1E
#defineECON1                  0x1F
2.2 寄存器操作命令
       寄存器操作命令也可称为寄存器操作码。为了实现寄存器的操作,ENC28J60定义了6+1个寄存器操作命令(操作码)。操作相关寄存器至少有读寄存器命令,写寄存器命令;发送或接收以太网数据则必有写缓冲区命令或读缓冲区命令;为了加快操作,对于某些控制寄存器而言还可以有置位或者清零某位的命令;最后加上一个软件复位命令,锦上添花。
<font size="3">/* 读控制寄存器 */
#define ENC28J60_READ_CTRL_REG          0x00
/* 读缓冲区 */
#define ENC28J60_READ_BUF_MEM          0x3A
/* 写控制寄存器 */
#define ENC28J60_WRITE_CTRL_REG          0x40
/* 写缓冲区 */
#define ENC28J60_WRITE_BUF_MEM          0x7A
/* 位域置位 */
#define ENC28J60_BIT_FIELD_SET              0x80
/* 位域清零 */
#define ENC28J60_BIT_FIELD_CLR                     0xA0
/* 系统复位 */
#define ENC28J60_SOFT_RESET                        0xFF</font>
复制代码
2.3 接收和发送缓冲区分配
       以太网数据的接收和发送离不开驱动芯片内部的RAM,也可称之为硬件缓冲区。ENC28J60包括8K 的硬件缓冲区,该硬件缓冲区一部分被接收缓冲区使用,另一部分为发送缓冲区使用。操作ENC28J60的最终目的为操作该硬件缓冲区。执行以太网发送命令时,向发送缓冲区中填充数据,并触发相关寄存器发送以太网数据;执行以太网接收命令时,通过查询相关寄存器或者外部中断的方式获得以太网数据输入事件,接着从接收缓冲区中读取相关数据。
(1)   把缓冲区划分为两个部分。把8K的硬件缓冲区划分为两个部分至少需要四个参数,接收缓冲区需要一个起始地址和一个结束地址加以描述,发送缓冲区也需要一个起始地址和一个结束地址加以描述。最理想的方式,两个缓冲区完全占据了8K的硬件缓冲区,完美地利用这一空间。由于ENC28J60的寄存器长度为8位,而硬件缓冲区的大小为8K,所以前面提到的4个地址需要8个寄存器才可以完全描述,需要把单个地址分为高8位和低8位。在AVRNET项目中,接收缓冲区较大,而发送缓冲区较小。在以太网协议中,最大的报文长度为1518字节,而最小报文长度为60字节。发送缓冲区等于或略大于1518字节,剩余的部分全部分配给接收缓冲区。接收缓冲区较大也是考虑到AVR的处理能力有限,若某个时间点收到多个以太网报文,可以先把报文闲置与硬件缓冲区中,待空闲时再从缓冲区中取出。
/* 接收缓冲区起始地址 */
#define RXSTART_INIT                0x00
/* 接收缓冲区停止地址 */
#define RXSTOP_INIT                 (0x1FFF - 0x0600 - 1)
/* 发送缓冲区起始地址 发送缓冲区大小约1500字节*/
#define TXSTART_INIT                (0x1FFF - 0x0600)
/* 发送缓冲区停止地址 */
#define TXSTOP_INIT                 0x1FFF



图硬件缓冲区结构
(2)   对于发送缓冲区而言,需要指定发送缓冲区写指针,使用写缓冲区命令操作该部分缓冲区,写指针的地址会不断增长,若遇到结束地址会重新返回起始地址。对于接收缓冲区而言就稍微复杂一点,每次读取之前必须明确该次操作时的读指针位置,根据前文的代码,缓冲区读指针的起始地址为0,在第一次读操作发生之后需要立即设置下次读操作的读指针地址。ENC28J60读缓冲区时,读取的数据并不全是以太网的数据,在以太网数据之前还有下一个数据包的地址指针占两个字节,接收状态向量占4个字节,接着才是以太网数据包,该数据包包括目标MAC地址,源MAC地址,数据包类型等等;最后为CRC校验和。在接收状态向量的起始2个字节为该以太网数据包的长度,该参数也是非常有用的参数。



图接收数据包结构
对于发送缓冲区而言,需要指定发送缓冲区写指针,使用写缓冲区命令操作该部分缓冲区,写指针的地址会不断增长,若遇到结束地址会重新返回起始地址。对于接收缓冲区而言就稍微复杂一点,每次读取之前必须明确该次操作时的读指针位置,根据前文的代码,缓冲区读指针的起始地址为0,在第一次读操作发生之后需要立即设置下次读操作的读指针地址。ENC28J60读缓冲区时,读取的数据并不全是以太网的数据,在以太网数据之前还有下一个数据包的地址指针占两个字节,接收状态向量占4个字节,接着才是以太网数据包,该数据包包括目标MAC地址,源MAC地址,数据包类型等等;最后为CRC校验和。在接收状态向量的起始2个字节为该以太网数据包的长度,该参数也是非常有用的参数。
3 寄存器操作实现
      ENC28j60的寄存器操作分为2+2+2部分,分别为写寄存器和读寄存器部分,读缓冲区和写缓冲区部分,写PHY寄存器和读PHY寄存器部分。
3.1 读写寄存器
       读或写寄存器的函数如下
unsigned char enc28j60Read
       /* 设定寄存器地址区域 */
       enc28j60SetBank(address);
       /* 读取寄存器值 发送读寄存器命令和地址 */
       return enc28j60ReadOp(ENC28J60_READ_CTRL_REG, address);
}
void enc28j60Write(unsigned char address, unsigned char data)
{
       /* 设定寄存器地址区域 */
       enc28j60SetBank(address);
       /* 写寄存器值 发送写寄存器命令和地址 */
       enc28j60WriteOp(ENC28J60_WRITE_CTRL_REG, address, data);
}</font>
复制代码
       读写寄存器的分为两步,第一步为选定寄存器的BANK编号,第二步为使用写命令或读命令,操作指定地址的寄存器。在ENC28J60中,由ECON1中的某两位保存BANK编号,ECON1是比较特殊的控制寄存器,在4个BANK中具有该寄存器且该寄存器的地址相同。Enc28j60Bank为全局变量,用于保存当前的BANK编号,如果两次操作控制寄存器在同一个BANK时,该变量保持不变,若两次操作的控制寄存器位于不同的BANK,那么BANK的值会变为新的BANK编号。
<font size="3">void enc28j60SetBank(unsigned char address)
{
       /* 计算本次寄存器地址在存取区域的位置 */
       if((address & BANK_MASK) != Enc28j60Bank)
       {
    /* 清除ECON1的BSEL1 BSEL0 详见数据手册15页 */
    enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1|ECON1_BSEL0));
    /* 请注意寄存器地址的宏定义,bit6 bit5代码寄存器存储区域位置 */
    enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK)>>5);
    /* 重新确定当前寄存器存储区域 */
    Enc28j60Bank = (address & BANK_MASK);
       }
}</font>
复制代码
<font size="3">unsigned char enc28j60ReadOp(unsigned char op, unsigned char address)
{
       unsigned char dat = 0;

       /* CS拉低 使能ENC28J60 */
       ENC28J60_CSL();
       /* 操作码和地址 */
       dat = op | (address & ADDR_MASK);
  /* 通过SPI写数据*/
       spi_sendbyte(dat);
  /* 通过SPI读出数据 */
       dat = spi_sendbyte(0xFF);

  /* 如果是MAC和MII寄存器,第一个读取的字节无效,该信息包含在地址的最高位 */
       if(address & 0x80)
       {
              /* 再次通过SPI读取数据 */
    dat = spi_sendbyte(0xFF);
       }

  /* CS拉高 禁止ENC28J60 */
       ENC28J60_CSH();

  /* 返回数据 */
       return dat;
}</font>
复制代码
读控制寄存器实际上就是严格遵守数据手册的操作要求,一次编写程序。在这里由于读MAC和MII寄存器时,第一个接收到的字节为无效字节,第二个字节才为有效字节。程序通过寄存器地址的最高位来判断是否为MAC或MII寄存器。写寄存器函数较为简单,第一次字节包括操作码和寄存器地址,第二个字节则为数据。在这两个函数中参数op为ENC28J60的指令,或称之为操作码,该指令占据了SPI第一个字节的前3位,参数address为寄存器地址,参数data为寄存器的具体值。
这两个函数和硬件发生某些关系,ENC28J60_CSL()和ENC28J60_CSH()为操作CS端口的操作宏,而spi_sendbyte()可通过SPI发送一个字节。修改这些函数即可在其他平台上使用ENC28J60。不过请特别注意,在使用其他开发板时由于SPI总线上可能挂载多个设备,单独使用ENC28J60时需要把其他设备的CS端口拉高,或安装一个上拉电阻。
<font size="3">unsigned char enc28j60ReadOp(unsigned char op, unsigned char address)
{
       unsigned char dat = 0;

       /* CS拉低 使能ENC28J60 */
       ENC28J60_CSL();
       /* 操作码和地址 */
       dat = op | (address & ADDR_MASK);
  /* 通过SPI写数据*/
       spi_sendbyte(dat);
  /* 通过SPI读出数据 */
       dat = spi_sendbyte(0xFF);

  /* 如果是MAC和MII寄存器,第一个读取的字节无效,该信息包含在地址的最高位 */
       if(address & 0x80)
       {
              /* 再次通过SPI读取数据 */
    dat = spi_sendbyte(0xFF);
       }

  /* CS拉高 禁止ENC28J60 */
       ENC28J60_CSH();

  /* 返回数据 */
       return dat;
}
void enc28j60WriteOp(unsigned char op, unsigned char address, unsigned char data)
{
       unsigned char dat = 0;
  /* 使能ENC28J60 */                                                   
       ENC28J60_CSL();        
  /* 通过SPI发送 操作码和寄存器地址 */                        
       dat = op | (address & ADDR_MASK);
  /* 通过SPI1发送数据 */
       spi_sendbyte(dat);
  /* 准备寄存器数值 */                             
       dat = data;
  /* 通过SPI发送数据 */
       spi_sendbyte(dat);
  /* 禁止ENC28J60 */                     
       ENC28J60_CSH();   
}</font>
复制代码
3.2 读写缓冲区
       读写缓冲区的操作也是易于理解的。需要说明的是,两个函数具有相同的输入参数,参数len代表被操作数据的长度,pdata为被操作数据的指针。和寄存器读写函数相似,发送或接收数据之前需要发送特定的操作码。
<font size="3">void enc28j60ReadBuffer(unsigned int len, unsigned char* pdata)
{
  /* 使能ENC28J60 */
  ENC28J60_CSL();
       /* 通过SPI发送读取缓冲区命令*/
       spi_sendbyte(ENC28J60_READ_BUF_MEM);
  /* 循环读取 */
       while(len)
       {
    len--;
    /* 读取数据 */
    *pdata = (unsigned char)spi_sendbyte(0);
    /* 地址指针累加 */
    pdata++;
       }
  /* 增加字符串结尾 便于操作 */
       *pdata='\0';
  /* 禁止ENC28J60 */
       ENC28J60_CSH();
}
void enc28j60WriteBuffer(unsigned int len, unsigned char* pdata)
{
  /* 使能ENC28J60 */
  ENC28J60_CSL();
       /* 通过SPI发送写取缓冲区命令*/
       spi_sendbyte(ENC28J60_WRITE_BUF_MEM);

  /* 循环发送 */
       while(len)
       {
              len--;
    /* 发送数据 */
              spi_sendbyte(*pdata);
    /* 地址指针累加 */
              pdata++;
       }

  /* 禁止ENC28J60 */
       ENC28J60_CSH();
}</font>
复制代码
3.3 读写PHY寄存器
       PHY寄存器和由ENC28J60控制的LED指示灯有关,控制这些寄存器可以控制这两个LED的驱动方式,和发生相应事件时LED的显示方式。一般情况下,一个LED指示灯常亮,显示接收和发送活动,另一个LED指示灯显示接收活动,有数据输入时产生一个点亮脉冲。PHY是比较特殊的寄存器,先要想一个控制寄存器写入PHY寄存器的地址,再向两个控制寄存器依次写入PHY寄存器的具体数据的高8位和低8位,最后等待PHY寄存器操作完成。
<font size="3">void enc28j60PhyWrite(unsigned char address, unsigned int data)
{
       /* 向MIREGADR写入地址 详见数据手册19页*/
       enc28j60Write(MIREGADR, address);
       /* 写入低8位数据 */
       enc28j60Write(MIWRL, data);
  /* 写入高8位数据 */
       enc28j60Write(MIWRH, data>>8);
       /* 等待PHY寄存器写入完成 */
       while(enc28j60Read(MISTAT) & MISTAT_BUSY);
}</font>
复制代码
4 ENC28J60写操作
       ENC28J60的寄存器操作时ENC28J60初始化,发送以太网数据和接收以太网数据的基础。通过ENC28J60进行以太网发送数据操作,本质上为操作硬件缓冲区的发送缓冲区部分。每次发送时总是从发送缓冲区的起始地址开始填充数据,数据填充的结束地址和数据的输入长度有关。操作完发送缓冲区的大小之后可向发送缓冲区填充数据,即调用ENC28J60_WRITE_BUF_MEM操作码,接着置位ECON1中的 ECON1_TXRTS位启动发送,并使用等待法不断查询是否发送完毕。基本的思路还是和SPI或UART发送数据相似,即填充数据,启动发送,查询发送完成。写操作的输入参数为数据包的长度len和数据包指针packet,该参数正好和uIP的网络层操作函数相对应。若是LwIP协议,输入参数将会是pBuf这种自定义数据结构,需要经过适当的修改才应用于lwIP协议栈。
<font size="3">void enc28j60PacketSend(unsigned int len, unsigned char* packet)
{
       /* 查询发送逻辑复位位 */
       while((enc28j60Read(ECON1) & ECON1_TXRTS)!= 0);

  /* 设置发送缓冲区起始地址 */   
       enc28j60Write(EWRPTL, TXSTART_INIT & 0xFF);
       enc28j60Write(EWRPTH, TXSTART_INIT >> 8);

       /* 设置发送缓冲区结束地址 该值对应发送数据包长度 */  
       enc28j60Write(ETXNDL, (TXSTART_INIT + len) & 0xFF);
       enc28j60Write(ETXNDH, (TXSTART_INIT + len) >>8);

       /* 发送之前发送控制包格式字 */
       enc28j60WriteOp(ENC28J60_WRITE_BUF_MEM, 0, 0x00);

       /* 通过ENC28J60发送数据包 */
       enc28j60WriteBuffer(len, packet);

       /* 开始发送 */
       enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_TXRTS);

  /* 复位发送逻辑的问题 */
       if( (enc28j60Read(EIR) & EIR_TXERIF) )
       {
              enc28j60SetBank(ECON1);
    enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, ECON1_TXRTS);
  }
}</font>
复制代码
5 ENC28J60读操作
       读操作要略比写操作复杂。写操作时每次总是从硬件发送缓冲区的起始地址开始操作,而读操作时需要不断修改接收缓冲区的读指针地址,该参数需要通过NextPacketPtr完成,该变量为长度为16的全局变量。读操作时,先通过寄存器查看是否存在以太网数据包,读EPKTCNT寄存器便可返回以太网数据包的个数;若存在以太网数据包则设定读指针的地址,执行读缓冲区操作,ENC28J60的以太网数据包中前两个字节为下一个以太网数据包的起始地址,立即保存该参数至NextPacketPtr全局变量中;以太网数据包中的后两个字节为该数据包的长度,该长度只从目标MAC地址开始的数据包的长度,进行处理时还应该舍弃最后的4字节CRC校验结果;最重要的事情便是通过读缓冲区操作码把len长度的以太网数据读出,读出的目标应为软件缓冲区,例如定义在程序中的rxtx_buf。最后根据NextPacketPtr移动读指针以便下次操作,并通过操作ECON2的ECON2_PKTDEC位递减了以太网数据包。
<font size="3">unsigned int enc28j60PacketReceive(unsigned int maxlen, unsigned char* packet)
{
       unsigned int rxstat;
       unsigned int len;

       /* 是否收到以太网数据包 */
       if( enc28j60Read(EPKTCNT) == 0 )
       {
              return(0);
    }

       /* 设置接收缓冲器读指针 */
       enc28j60Write(ERDPTL, (NextPacketPtr));
       enc28j60Write(ERDPTH, (NextPacketPtr)>>8);

  /* 接收数据包结构示例 数据手册43页 */

       /* 读下一个包的指针 */
       NextPacketPtr  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
       NextPacketPtr |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;

       /* 读包的长度 */
       len  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
       len |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0)<<8;

   /* 去除CRC校验部分 */
   len-= 4;

       /* 读取接收状态 */
       rxstat  = enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0);
       rxstat |= enc28j60ReadOp(ENC28J60_READ_BUF_MEM, 0) << 8;

       /* 限制检索的长度  */
  if (len > maxlen-1)
       {
    len = maxlen-1;
  }
  /* 检查CRC和符号错误 */
  /* ERXFCON.CRCEN是默认设置。通常我们不需要检查 */
  if ((rxstat & 0x80)==0)
       {
          //无效的
          len = 0;
       }
       else
       {
    /* 从接收缓冲器中复制数据包 */
    enc28j60ReadBuffer(len, packet);
  }

  /* 移动接收缓冲区 读指针*/
       enc28j60Write(ERXRDPTL, (NextPacketPtr));
       enc28j60Write(ERXRDPTH, (NextPacketPtr)>>8);

       /* 数据包递减 */
       enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON2, ECON2_PKTDEC);

  /* 返回长度 */
       return(len);
}</font>
复制代码
6 ENC28J60初始化操作
       ENC28J60的操作比较琐碎。第一,进行CS端口的相关配置,即把该端口设置为输出状态,该部分代码可以出现在任何硬件初始化代码中,例如可以把所有的IO操作放入gpio_config中;第二,进行软件复位,并通过查询ESTAT的ESTAT_CLKRDY标志位确定是否复位完成;第二,初始化NextPacketPtr变量,该变量的初值为发送缓冲区的起始地址;第三,配置发送和接收缓冲区的区间;第四,若干参数配置,请看代码注释部分,ENC28J60具有自动填充0 的功能,即发送报文长度低于以太网最小报文长度时可以填充0至最小长度;第五,写入MAC地址,由于ENC28J60内部没有全球唯一的MAC地址,所以该地址需要软件填写。但是这种软件填写方式存在缺陷,实际应用中可以含有全球唯一的MAC地址的EEPROM,从EERPOM读取MAC地址并用该地址初始化ENC28J60;第六,初始化中断,并使能接收,ENC28J60含有多个中断,最重要的有全局中断和数据包带接收中断。
<font size="3">void enc28j60Init(unsigned char* macaddr)
{
  /* CS端口为输出 */
  DDRB |= (1<<4);

  /* 禁止ENC28J60 */
  ENC28J60_CSH();
       /* ENC28J60软件复位 该函数可以改进*/
       enc28j60WriteOp(ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);
  /*查询ESTAT.CLKRDY位*/
       while(!(enc28j60Read(ESTAT) & ESTAT_CLKRDY));

       /* 设置接收缓冲区起始地址 该变量用于每次读取缓冲区时保留下一个包的首地址 */
       NextPacketPtr = RXSTART_INIT;

  /* 设置接收缓冲区 起始指针*/
       enc28j60Write(ERXSTL, RXSTART_INIT & 0xFF);
       enc28j60Write(ERXSTH, RXSTART_INIT >> 8);

  /* 设置接收缓冲区 读指针*/
       enc28j60Write(ERXRDPTL, RXSTART_INIT&0xFF);
       enc28j60Write(ERXRDPTH, RXSTART_INIT>>8);

  /* 设置接收缓冲区 结束指针 */
       enc28j60Write(ERXNDL, RXSTOP_INIT&0xFF);
       enc28j60Write(ERXNDH, RXSTOP_INIT>>8);

       /* 设置发送缓冲区 起始指针 */
       enc28j60Write(ETXSTL, TXSTART_INIT&0xFF);
       enc28j60Write(ETXSTH, TXSTART_INIT>>8);
       /* 设置发送缓冲区 结束指针 */
       enc28j60Write(ETXNDL, TXSTOP_INIT&0xFF);
       enc28j60Write(ETXNDH, TXSTOP_INIT>>8);

  /* 使能单播过滤 使能CRC校验 使能 格式匹配自动过滤*/
       enc28j60Write(ERXFCON, ERXFCON_UCEN|ERXFCON_CRCEN|ERXFCON_PMEN);
       enc28j60Write(EPMM0, 0x3f);
       enc28j60Write(EPMM1, 0x30);
       enc28j60Write(EPMCSL, 0xf9);
       enc28j60Write(EPMCSH, 0xf7);

  /* 使能MAC接收 允许MAC发送暂停控制帧 当接收到暂停控制帧时停止发送*/
  /* 数据手册34页 */
       enc28j60Write(MACON1, MACON1_MARXEN|MACON1_TXPAUS|MACON1_RXPAUS);

  /* 退出复位状态 */
       enc28j60Write(MACON2, 0x00);

  /* 用0填充所有短帧至60字节长 并追加一个CRC 发送CRC使能 帧长度校验使能 MAC全双工使能*/
       /* 提示 由于ENC28J60不支持802.3的自动协商机制, 所以对端的网络卡需要强制设置为全双工 */
       enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, MACON3, MACON3_PADCFG0|MACON3_TXCRCEN|MACON3_FRMLNEN|MACON3_FULDPX);

  /* 填入默认值 */
       enc28j60Write(MAIPGL, 0x12);
  /* 填入默认值 */
       enc28j60Write(MAIPGH, 0x0C);
  /* 填入默认值 */
       enc28j60Write(MABBIPG, 0x15);

  /* 最大帧长度 */
       enc28j60Write(MAMXFLL, MAX_FRAMELEN & 0xFF);
       enc28j60Write(MAMXFLH, MAX_FRAMELEN >> 8);

  /* 写入MAC地址 */
       enc28j60Write(MAADR5, macaddr[0]);     
       enc28j60Write(MAADR4, macaddr[1]);
       enc28j60Write(MAADR3, macaddr[2]);
       enc28j60Write(MAADR2, macaddr[3]);
       enc28j60Write(MAADR1, macaddr[4]);
       enc28j60Write(MAADR0, macaddr[5]);

       /* 配置PHY为全双工  LEDB为拉电流 */
       enc28j60PhyWrite(PHCON1, PHCON1_PDPXMD);

  /* LED状态 */
  enc28j60PhyWrite(PHLCON,0x0476);   

  /* 半双工回环禁止 */
       enc28j60PhyWrite(PHCON2, PHCON2_HDLDIS);

  /* 返回BANK0 */     
       enc28j60SetBank(ECON1);

  /* 使能中断 全局中断 接收中断 接收错误中断 */
       enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, EIE, EIE_INTIE|EIE_PKTIE|EIE_RXERIE);

  /* 接收使能位 */
       enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, ECON1_RXEN);
}</font>
复制代码

7 总结
ENC28J60的驱动编写算是比较复杂的。但是回过头来看看,其他的以太网驱动芯片的操作和ENC28J60的操作类似,其操作的核心即时数KB的硬件缓冲区。本例不能给出合适的运行范例,因为以太网驱动芯片要配合以太网协议栈来实现,而以太网协议栈内容很多,即使通过uIP或是lwIP也必须面对繁多的基础知识。ENC28J60的驱动是以太网协议栈实现的基础,通过ENC28J60还将会分析uIP协议栈,lwIP协议栈的应用。在实现TCP通信之后,还将会结合AVRNET或uIP,lwIP协议栈实现web服务器,通过网页交换数据。

您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|手机版|小黑屋|ELEOK |网站地图

GMT+8, 2024-11-21 19:12

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表