分享

ENC28J60驱动移植

 心不留意外尘 2016-06-29

http://blog.csdn.net/xukai871105/article/details/13931833

0.相关资料

    相关资料中包括其他精彩博文和代码仓库
    【嵌入式 TCP IP 资料汇总】   
    【ENC28J60学习笔记——索引】 【第1部分】【第2部分】【第3部分】【第4部分
    【uIP学习笔记

1前言

    嵌入式以太网开发,可以分为两个部分,一个是以太网收发芯片的使用,一个是嵌入式以太网协议栈的实现。以太网收发芯片的使用要比串口收发芯片的使用复杂的多,市面上流通比较广泛的以太网收发芯片种类还不少,有SPI接口的ENC28J60,也有并口形式的RTL8019S,CS8900A等。嵌入式以太网协议栈有著名的uIP协议栈,Lwip协议栈,还有其他嵌入式高手开发的协议栈。无论是硬件还是软件,都无法分出高低,适合项目需求的才是最好的。

1.1 写作理由

    再说明一下我写作的理由。以前从淘宝上购买过ENC28J60,店家信誓旦旦地说能提供51AVR LPC STM32等多个平台的代码,可以实现一个网页控制LED。头脑一热买了回来,买回来才发现,店家提供的资料零零散散,不易弄懂。几经周转,发现原来这些ENC28J60的代码都出自一个地方——AVRNET,源自老外的一个开源项目。把最原始的代码拿来细细品味,以太网协议就不那么神秘了。在这里说一下ENC28J60的使用,熟悉了ENC28J60的驱动可以分几步走。第一步,通过ENC28J60移植uIP或者lwIP协议栈,实现TCP或是UDP通信,第二,顺着AVRNET项目走,实现一个简单的web服务器,运行静态或者动态网页。嵌入式以太网和计算机以太网开发不同,对于TCP通信而言没有socket套接字,对于网页编程而言也没有IIS或PHP,所示实现起来会相对麻烦,但是也非常有乐趣。

1.2 资料准备

    嵌入式以太网开发是非常复杂的工作,在开始之前最好先大致浏览ENC28J60的使用手册。除此之外,需要认真阅读TCP IP相关知识,推荐一本图书《嵌入式Internet TCP/IP基础、实现和应用》。嵌入式开发是一个反复借鉴的过程,该部分代码参考了AVRNET项目和奋斗开发板的相关范例。AVRNET项目网址链接

虽然AVRNET项目所使用的MCU为ATmega32,但是认真阅读源代码之后也可以方便的移植到其他的MCU平台,例如STM8、STM32和MSP430等。

2 寄存器和寄存器操作

    ENC28J60的寄存器很多,操作这些寄存器需要一个良好的代码组织工作。在AVRNET项目中,把ENC28J60的驱动分解成ENC28J60.h文件和ENC28J60.c文件。H文件中主要描述ENC28J60寄存器的基本定义,而C文件主要实现了这些寄存器的操作。

2.1 寄存器定义

    首先分析一下ENC28J60.h这个头文件。阅读数据手册之后,会发现ENC28J60寄存器数量较多,通过分析和整理,操作ENC28J60的寄存器需要注意以下3点。

【1】共有三种不同形式的寄存器——控制寄存器以太网寄存器PHY寄存器,不同的寄存器以不同的字母开头,以E、 MA和MI加以区分。操作这三种不同的寄存器需要不同的组合命令。

【2】 寄存器被分布在4个不同的bank中,也就是说存在地址相同的寄存器,但是这些寄存器却位于不同的分区中,在操作寄存器之前必须选中正确的bank

【3】 虽然存在4个bank,但是有5个寄存器在4个bank的位置相同,它们是EIE、 EIR、ESTAT、ECON1、ECON2

AVRNET项目中,寄存器被定义成8位长度,而这8位长度包含了三个部分,地址bit7(最高位)用以区分PHY和MAC寄存器,PHY寄存器的操作最为特殊;地址bit6和bit5用以区分BANK,2位空间正好区分4个BANK;地址的最后5位才是寄存器的地址。通过这种方式就可以区分所有的寄存器了。列举了几行代码。由于头文件很长,所以不全部列出。

  1. // bank0 寄存器  
  2. #define ERDPTL            (0x00|0x00)  
  3. #define ERDPTH            (0x01|0x00)  
  4. #define EWRPTL            (0x02|0x00)  
  5. // bank1 寄存器  
  6. #define EHT0              (0x00|0x20)  
  7. #define EHT1              (0x01|0x20)  
  8. #define EHT2              (0x02|0x20)  
  9. // bank2 寄存器  
  10. #define MACON1           (0x00|0x40|0x80)  
  11. #define MACON2           (0x01|0x40|0x80)  
  12. #define MACON3           (0x02|0x40|0x80)  
  13. //bank3 寄存器  
  14. #define MAADR1           (0x00|0x60|0x80)  
  15. #define MAADR0           (0x01|0x60|0x80)  
  16. #define MAADR3           (0x02|0x60|0x80)  


例如ERDPTH为位于BANK0的以太网寄存器,第一个数字0x01代表BANKx中的具体地址,该地址为0x01,第二个数字0x00代表BANK编号,该BANK地址为0EHT1为位于BANK1中的控制寄存器,第一个0x01代表BANKx中的具体地址,该地址为0x01,第二个0x20代表BANK编号,此处BANK编号为1。请注意由于BANK编号被保存在BIT6BIT5,所以此处为0x20而不是0x10MACON2为位于BANK2的以太网寄存器,第一个数字0x01代表在该BANKx中的寄存器地址,第二个数字0x40代表BANK编号,此处BANK编号为2,而第三个数字0x80代表该寄存器为以太网寄存器或是PHY寄存器,该寄存器的操作比较特殊。

为了方便寄存器操作,头文件中还定义了寄存器地址操作的掩码,简单而言就是需要查看哪些位,不需要查看哪些位。

  1. /* 寄存器地址掩码 */  
  2. #defineADDR_MASK        0x1F  
  3. /* 存储区域掩码 */  
  4. #defineBANK_MASK        0x60  
  5. /* MAC和MII寄存器掩码*/  
  6. #defineSPRD_MASK         0x80  

另外还有比较特殊的5个控制寄存器,EIE,EIR,ESTAT,ECON2和ECON1

  1. /* 关键寄存器 */  
  2. #defineEIE                     0x1B  
  3. #defineEIR                     0x1C  
  4. #defineESTAT                   0x1D  
  5. #defineECON2                  0x1E  
  6. #defineECON1                  0x1F  

2.2 寄存器操作命令

寄存器操作命令也可称为寄存器操作码。为了实现寄存器的操作,ENC28J60定义了6+1个寄存器操作命令(操作码)。操作相关寄存器至少有读寄存器命令,写寄存器命令;发送或接收以太网数据则必有写缓冲区命令或读缓冲区命令;为了加快操作,对于某些控制寄存器而言还可以有置位或者清零某位的命令;最后加上一个软件复位命令,锦上添花。

  1. /* 读控制寄存器 */  
  2. #define ENC28J60_READ_CTRL_REG         0x00  
  3. /* 读缓冲区 */  
  4. #define ENC28J60_READ_BUF_MEM          0x3A  
  5. /* 写控制寄存器 */  
  6. #define ENC28J60_WRITE_CTRL_REG        0x40  
  7. /* 写缓冲区 */  
  8. #define ENC28J60_WRITE_BUF_MEM         0x7A  
  9. /* 位域置位 */  
  10. #define ENC28J60_BIT_FIELD_SET         0x80  
  11. /* 位域清零 */  
  12. #define ENC28J60_BIT_FIELD_CLR         0xA0  
  13. /* 系统复位 */  
  14. #define ENC28J60_SOFT_RESET            0xFF  

2.3 接收和发送缓冲区分配

以太网数据的接收和发送离不开驱动芯片内部的RAM,也可称之为硬件缓冲区。ENC28J60包括8K 的硬件缓冲区,该硬件缓冲区一部分被接收缓冲区使用,另一部分为发送缓冲区使用。控制ENC28J60的最终目的为操作该硬件缓冲区。执行以太网发送命令时,向发送缓冲区中填充数据,并触发相关寄存器发送以太网数据;执行以太网接收命令时,通过查询相关寄存器或者外部中断的方式获得以太网数据输入事件,接着从接收缓冲区中读取相关数据。

(1) 把缓冲区划分为两个部分。把8K的硬件缓冲区划分为两个部分至少需要四个参数,接收缓冲区需要一个起始地址和一个结束地址加以描述,发送缓冲区也需要一个起始地址和一个结束地址加以描述。最理想的方式,两个缓冲区完全占据了8K的硬件缓冲区,完美地利用这一空间。由于ENC28J60的寄存器长度为8位,而硬件缓冲区的大小为8K,所以前面提到的4个地址需要8个寄存器才可以完全描述,需要把单个地址分为高8位和低8位。在AVRNET项目中,接收缓冲区较大,而发送缓冲区较小。在以太网协议中,最大的报文长度为1518字节,而最小报文长度为60字节。发送缓冲区等于或略大于1518字节,剩余的部分全部分配给接收缓冲区。接收缓冲区较大也是考虑到AVR的处理能力有限,若某个时间点收到多个以太网报文,可以先把报文闲置与硬件缓冲区中,待MCU空闲时再从缓冲区中取出。

  1. /* 接收缓冲区起始地址 */  
  2. #define RXSTART_INIT                0x00  
  3. /* 接收缓冲区停止地址 */  
  4. #define RXSTOP_INIT                 (0x1FFF - 0x0600 - 1)  
  5. /* 发送缓冲区起始地址 发送缓冲区大小约1500字节*/  
  6. #define TXSTART_INIT                (0x1FFF - 0x0600)  
  7. /* 发送缓冲区停止地址 */  
  8. #define TXSTOP_INIT                 0x1FFF  

图2.1 硬件缓冲区结构

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


图2.2 接收数据包结构


3 寄存器操作实现

ENC28j60的寄存器操作分为2+2+2部分,分别为写寄存器和读寄存器部分,读缓冲区和写缓冲区部分,写PHY寄存器和读PHY寄存器部分。

3.1 读写寄存器

读或写寄存器的函数如下:

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. unsigned char enc28j60Read(unsigned char address)  
  2. {  
  3.     /* 设定寄存器地址区域 */  
  4.     enc28j60SetBank(address);  
  5.     /* 读取寄存器值 发送读寄存器命令和地址 */  
  6.     return enc28j60ReadOp(ENC28J60_READ_CTRL_REG, address);  
  7. }  
  8. void enc28j60Write(unsigned char address, unsigned char data)  
  9. {  
  10.     /* 设定寄存器地址区域 */  
  11.     enc28j60SetBank(address);  
  12.     /* 写寄存器值 发送写寄存器命令和地址 */  
  13.     enc28j60WriteOp(ENC28J60_WRITE_CTRL_REG, address, data);  
  14. }  

读写寄存器的分为两步,第一步为选定寄存器的BANK编号,第二步使用写命令或读命令,操作指定地址的寄存器。在ENC28J60中,由ECON1中的低两位(BIT1-BIT1)保存BANK编号,ECON1是比较特殊的控制寄存器, 4个BANK均具有该寄存器且该寄存器的地址相同。Enc28j60Bank为全局变量,用于保存当前的BANK编号,如果两次操作控制寄存器在同一个BANK时,该变量保持不变,若两次操作的控制寄存器位于不同的BANK,那么BANK的值会变为新的BANK编号。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void enc28j60SetBank(unsigned char address)  
  2. {  
  3.     /* 计算本次寄存器地址在存取区域的位置 */  
  4.     if((address & BANK_MASK) != Enc28j60Bank)  
  5.     {  
  6.         /* 清除ECON1的BSEL1 BSEL0 详见数据手册15页 */  
  7.         enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1|ECON1_BSEL0));  
  8.         /* 请注意寄存器地址的宏定义,bit6 bit5代码寄存器存储区域位置 */  
  9.         enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK)>>5);  
  10.         /* 重新确定当前寄存器存储区域 */  
  11.         Enc28j60Bank = (address & BANK_MASK);  
  12.     }  
  13. }  
  14.   
  15.   
  16. unsigned char enc28j60ReadOp(unsigned char op, unsigned char address)  
  17. {  
  18.         unsigned char dat = 0;  
  19.       
  20.     /* CS拉低 使能ENC28J60 */  
  21.     ENC28J60_CSL();  
  22.     /* 操作码和地址 */  
  23.     dat = op | (address & ADDR_MASK);  
  24.         /* 通过SPI写数据*/  
  25.     spi_sendbyte(dat);  
  26.         /* 通过SPI读出数据 */  
  27.     dat = spi_sendbyte(0xFF);  
  28.       
  29.         /* 如果是MAC和MII寄存器,第一个读取的字节无效,该信息包含在地址的最高位 */  
  30.     if(address & 0x80)  
  31.     {  
  32.         /* 再次通过SPI读取数据 */  
  33.                 dat = spi_sendbyte(0xFF);  
  34.     }  
  35.       
  36.         /* CS拉高 禁止ENC28J60 */  
  37.     ENC28J60_CSH();  
  38.   
  39.         /* 返回数据 */  
  40.     return dat;  
  41. }  

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

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. unsigned char enc28j60ReadOp(unsigned char op, unsigned char address)  
  2. {  
  3.     unsigned char dat = 0;  
  4.       
  5.     /* CS拉低 使能ENC28J60 */  
  6.     ENC28J60_CSL();  
  7.     /* 操作码和地址 */  
  8.     dat = op | (address & ADDR_MASK);  
  9.         /* 通过SPI写数据*/  
  10.     spi_sendbyte(dat);  
  11.         /* 通过SPI读出数据 */  
  12.     dat = spi_sendbyte(0xFF);  
  13.       
  14.         /* 如果是MAC和MII寄存器,第一个读取的字节无效,该信息包含在地址的最高位 */  
  15.     if(address & 0x80)  
  16.     {  
  17.         /* 再次通过SPI读取数据 */  
  18.                 dat = spi_sendbyte(0xFF);  
  19.     }  
  20.       
  21.         /* CS拉高 禁止ENC28J60 */  
  22.     ENC28J60_CSH();  
  23.   
  24.         /* 返回数据 */  
  25.     return dat;  
  26. }  
  27. void enc28j60WriteOp(unsigned char op, unsigned char address, unsigned char data)  
  28. {  
  29.     unsigned char dat = 0;  
  30.         /* 使能ENC28J60 */                                    
  31.     ENC28J60_CSL();        
  32.         /* 通过SPI发送 操作码和寄存器地址 */                           
  33.     dat = op | (address & ADDR_MASK);  
  34.         /* 通过SPI1发送数据 */  
  35.     spi_sendbyte(dat);  
  36.         /* 准备寄存器数值 */                   
  37.     dat = data;  
  38.         /* 通过SPI发送数据 */  
  39.     spi_sendbyte(dat);  
  40.        /* 禁止ENC28J60 */                
  41.     ENC28J60_CSH();   
  42. }  


3.2 读写缓冲区

读写缓冲区的操作也易于理解的。需要说明的是,两个函数具有相同的输入参数,参数len代表被操作数据的长度,pdata为被操作数据的指针。和寄存器读写函数相似,发送或接收数据之前需要发送特定的操作码。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void enc28j60ReadBuffer(unsigned int len, unsigned char* pdata)  
  2. {  
  3.         /* 使能ENC28J60 */  
  4.         ENC28J60_CSL();  
  5.     /* 通过SPI发送读取缓冲区命令*/  
  6.     spi_sendbyte(ENC28J60_READ_BUF_MEM);  
  7.         /* 循环读取 */  
  8.     while(len)  
  9.     {  
  10.             len--;  
  11.             /* 读取数据 */  
  12.             *pdata = (unsigned char)spi_sendbyte(0);  
  13.             /* 地址指针累加 */  
  14.             pdata++;  
  15.     }  
  16.         /* 禁止ENC28J60 */  
  17.     ENC28J60_CSH();  
  18. }  
  19. void enc28j60WriteBuffer(unsigned int len, unsigned char* pdata)  
  20. {  
  21.         /* 使能ENC28J60 */  
  22.         ENC28J60_CSL();  
  23.         /* 通过SPI发送写取缓冲区命令*/  
  24.     spi_sendbyte(ENC28J60_WRITE_BUF_MEM);  
  25.       
  26.         /* 循环发送 */  
  27.     while(len)  
  28.     {  
  29.         len--;  
  30.                 /* 发送数据 */  
  31.         spi_sendbyte(*pdata);  
  32.                 /* 地址指针累加 */  
  33.         pdata++;  
  34.     }  
  35.     
  36.         /* 禁止ENC28J60 */  
  37.     ENC28J60_CSH();  
  38. }  

3.3 读写PHY寄存器

PHY寄存器和被ENC28J60控制的LED指示灯有关,控制该寄存器可以控制LED驱动方式和发生相应事件时LED显示方式。一般情况下,一个LED指示灯用于指示网络状态(常亮可理解为网络接通),另一个LED指示灯显示接收活动,有数据输入时产生一个点亮脉冲。PHY是比较特殊的寄存器,先要想一个控制寄存器写入PHY寄存器的地址,再向两个控制寄存器依次写入PHY寄存器的具体数据的高8位和低8位,最后等待PHY寄存器操作完成。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. void enc28j60PhyWrite(unsigned char address, unsigned int data)  
  2. {  
  3.     /* 向MIREGADR写入地址 详见数据手册19页*/  
  4.     enc28j60Write(MIREGADR, address);  
  5.     /* 写入低8位数据 */  
  6.     enc28j60Write(MIWRL, data);  
  7.         /* 写入高8位数据 */  
  8.     enc28j60Write(MIWRH, data>>8);  
  9.     /* 等待PHY寄存器写入完成 */  
  10.     while(enc28j60Read(MISTAT) & MISTAT_BUSY);  
  11. }  

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多