开头的话,好吧,我只能说写驱动程序的人都是疯子,疯子才能进入这个领域,或者一开始你没疯,后来你疯了。真的,当问题出现的时候,有可能你无数次相信硬件没问题,是自己的程序有问题,也可能最后真的是硬件本来就是坏的。或者你到最后都没搞清楚到底是硬件的问题还是软件的问题。(按理说现在程序界这么火,说明底层支持着的硬件还是很可靠的,去找自己的问题吧!我也不知道我在说什么。
进入正题。
第一部分,如果你不知道单片机和DM9000网卡的以太网通讯思路,请赶紧百度上查阅DM9000的以太网设计,如果直接看DM9000数据手册,真的会看死人。建议先看看整体思路,再然后有时间最好通看一下DM9000的数据手册。
第二部分,2440单片机的知识至少要掌握中断和串口(当然越多越好)。搞清楚中断入口程序的地址和中断函数的联系。
第三部分,DM9000这部分程序的编写。主要部分分三块。
第一块,DM9000的初始化
void DM9000_init(void)
{
uint32 i;
Test_DM9000AE();
IOSetInit();
dm9000_reg_write(DM9000_IMR, 0x80); //中断关闭
//初始化设置步骤: 1
dm9000_reg_write(DM9000_GPCR, 0x01); //设置 GPCR(1EH) bit[0]=1,使DM9000的GPIO3为输出。
dm9000_reg_write(DM9000_GPR, 0x00); //GPR bit[0]=0 使DM9000的GPIO3输出为低以激活内部PHY。
udelay(500); //延时2ms以上等待PHY上电。
//初始化设置步骤: 2
dm9000_reg_write(DM9000_NCR, 0x03); //软件复位
udelay(300); //延时20us以上等待软件复位完成
dm9000_reg_write(DM9000_NCR, 0x00); //复位完成,设置正常工作模式。
dm9000_reg_write(DM9000_NCR, 0x03); //第二次软件复位,为了确保软件复位完全成功。此步骤是必要的。
udelay(300);
dm9000_reg_write(DM9000_NCR, 0x00);
//初始化设置步骤: 3
dm9000_reg_write(DM9000_NSR, 0x2c); //清除各种状态标志位
dm9000_reg_write(DM9000_ISR, 0xbf); //清除所有中断标志位
//初始化设置步骤: 4
<span style="white-space:pre"> </span>
//初始化设置步骤: 5
for(i=0; i<6; i++)
dm9000_reg_write(DM9000_PAR + i, mac_addr[i]);//mac_addr[]自己定义一下吧,6个字节的MAC地址
//初始化设置步骤: 6
dm9000_reg_write(DM9000_NSR, 0x2c); //清除各种状态标志位
dm9000_reg_write(DM9000_ISR, 0x3f); //清除所有中断标志位
//初始化设置步骤: 7
dm9000_reg_write(DM9000_IMR, 0x81); //中断使能}
第二块,发送程序的编写
void DM9000_sendPcket(uint8 *datas, uint32 length)
{
uint32 len,i;
//uint8 tmp;
//Printf("发送数据\r\n");
dm9000_reg_write(DM9000_IMR,0x80); //先禁止网卡中断,防止在发送数据时被中断干扰
len = length; //把发送长度写入
dm9000_reg_write(DM9000_TXPLH, (len>>8) & 0x0ff);
dm9000_reg_write(DM9000_TXPLL, len & 0x0ff);
DM_ADD = DM9000_MWCMD;
for(i=0; i<len; i+=2) //16 bit mode
{
udelay(2);
DM_CMD = datas[i] | (datas[i+1]<<8);
}
dm9000_reg_write(DM9000_TCR, 0x01); //发送数据到以太网上
while(1)//等待数据发送完成
{
uint8 data;
data = dm9000_reg_read(DM9000_TCR);//DM9000_NSR
if((data&0x01) == 0x00) break;
}
/* tmp = dm9000_reg_read(DM9000_NSR);
if((tmp & 0x01) == 0x04)
{
if((dm9000_reg_read(DM9000_TSR1)&0xfc) == 0x00)
Printf("TSR1成功\r\n");
else
Printf("TSR1失败r\n");
}
else
{
if((dm9000_reg_read(DM9000_TSR2)&0xfc) == 0x00)
Printf("TSR2成功\r\n");
else
Printf("TSR2失败r\n");
}
*/
dm9000_reg_write(DM9000_NSR, 0x2c); //清除状态寄存器,由于发送数据没有设置中断,因此不必处理中断标志位
dm9000_reg_write(DM9000_IMR, 0x81); //DM9000网卡的接收中断使能
//Printf("发送数据完成\r\n");
}
第三块,接受程序的编写,最难的就是接收程序了,因为这里涉及到中断,自己之前配置好外部中断,中断程序中接收DM9000的接收包。然后坑爹的是以太网帧开始位置的确定,到底从DM9000接收缓存的那个地方把数据复制到单片机内存中。手册上说的是(0x01,状态字节,长度低字节,长度高字节)这四个字节作为识别区,先用预读取指令读取缓冲区的字节,判断是0x00还是0x01,否则就复位DM9000网卡,尼玛坑爹就在这里啊,复位还是不好使啊。然后预读取完只能直接读取下面的字节了,但这样会导致读取的数据不对,可能读到的数据并不是以真正的目的物理地址开头的数据,可能错位,前移或者后移(我不知道是不是网卡初始化对不对)。所以我只能这么做了,直接检测0x01字节,加上判断目的物理地址是否正确来确定以太网帧的位置,以及这个帧是否是传给自己的。理论上这是可靠的。以下为代码。
void IOSetInit(void)
{
rGPFCON = (rGPFCON & (~(0x03<<14))) | (0x02<<14); //GPF7设置为EINT7
rEXTINT0 = (rEXTINT0 & (~(0x07<<28))) | (0x01<<28);
rEINTMASK = rEINTMASK & (~(0x01<<7));
ClearPending(BIT_EINT4_7);
pISR_EINT4_7 = (U32)Eint7_ISR;
rINTMSK = rINTMSK & (~(BIT_EINT4_7));
}
static void __irq Eint7_ISR(void)
{
uint32 i;
uint16 type,m,n;
//Printf("Eint7中断服务\r\n");
//VAR_RETURN VARRETURN;
//VAR_RETURN *var_re=&VARRETURN;
Buffer[12]=0,Buffer[13]=0;
len = receivepacket(Buffer);
//后面的可以自己添加,这里省略
}
uint32 receivepacket(uint8 *datas)
{
uint16 i,tmp,len=0,status=0;
uint8 ready=0;
//ready = 0; //希望读取到"01H"
//status = 0; //数据包状态
//len = 0; //数据包长度
if(dm9000_reg_read(DM9000_ISR) & 0x01)
{
dm9000_reg_write(DM9000_ISR, 0x01); //清除接收中断标志位
}
else return 0;
//else { Printf("你好\r\n");return 0;}
//DM_ADD=DM9000_MRCMDX;
//ready = DM_CMD; // 第一次读取,一般读取到的是 00H
//Printf("预读取第一次:%x\r\n",ready);
//ready = DM_CMD ; // 第一次读取,一般读取到的是 01H
//Printf("预读取第二次:%x\r\n",ready);
DM_ADD = DM9000_MRCMD;
status=DM_CMD ;//读状态字节
while((status&0xff)!=0x01)
{
status=DM_CMD ;
}
//status=(status&0xff00)>>8;
len = DM_CMD; //读数据包长度
//Printf("st=%x status=%x len= %x\r\n",st,status,len);
if( (len < 1522))//!(status & 0xbf) &&
{
for(i=0; i<6; i+=2)// 这个FOR语句是为了判断数据包是否传给自己,即检查MAC地址
{
//udelay(20);
tmp = DM_CMD;
datas[i] = tmp & 0x0ff;
if((datas[i]!=0xff)&&(datas[i]!=mac_addr[i])) return 0;
datas[i + 1] = (tmp >> 8) & 0x0ff;
if((datas[i+1]!=0xff)&&(datas[i+1]!=mac_addr[i+1])) return 0;
}
//Printf("状态字:%x\r\n",status);
for(i=6; i<len+1; i+=2)// 16 bit mode
{
//udelay(20);
tmp = DM_CMD;
datas[i] = tmp & 0x0ff;
datas[i + 1] = (tmp >> 8) & 0x0ff;
}
}
else return 0;
// if(len > 1000) return 0;
// if( (HON( ETHBUF->type ) != ETHTYPE_ARP) && (HON( ETHBUF->type ) != ETHTYPE_IP) )
// return 0;
return len;
}
首先是TCP/IP一些报头的结构体:
typedef struct eth_hdr //以太网头部结构,为了以后使用方便
{
uint8 d_mac[6]; //目的地址
uint8 s_mac[6]; //源地址
uint16 type; //协议类型
}ETH_HDR;
typedef struct arp //ARP首部结构
{
uint16 hwtype; //硬件类型(1表示传输的是以太网MAC地址)
uint16 protocol; //协议类型(0x0800表示传输的是IP地址)
uint8 hwlen; //硬件地址长度(6)
uint8 protolen; //协议地址长度(4)
uint16 opcode; //操作(1表示ARP请求,2表示ARP应答)
uint8 smac[6]; //发送端MAC地址
uint8 sipaddr[4]; //发送端IP地址
uint8 dmac[6]; //目的端MAC地址
uint8 dipaddr[4]; //目的端IP地址
}ARP;
typedef struct ip //IP首部结构
{
uint8 vhl; //4位版本号4位首部长度(0x45)
uint8 tos; //服务类型(0)
uint16 len; //整个IP数据报总字节长度
uint16 ipid; //IP标识
uint16 ipoffset; //3位标识13位偏移
uint8 ttl; //生存时间(32或64)
uint8 proto; //协议(1表示ICMP,2表示IGMP,6表示TCP,17表示UDP)
uint16 ipchksum; //首部校验和
uint8 srcipaddr[4]; //源IP
uint8 destipaddr[4]; //目的IP
}IP;
typedef struct tcp //IP首部结构
{
uint16 sport; //源端口号
uint16 dport; //目的端口号
uint32 sequencenum; //顺序号
uint32 acknowledgenum; //确认号
uint8 tcplength; //低四位为TCP报头字(32位)的个数,高四位必须为0,是保留位
uint8 flags; //低两位为保留位,高6位为标志位[2:7]依次为
//URG:紧急指针。用到的时候值为1,用来处理避免TCP数据流中断
//ACK:置1时表示确认号为合法,为0的时候表示数据段不包含确认信息,确认号被忽略。
//PSH:置1时请求的数据段在接收方得到后就可直接送到应用程序,而不必等到缓冲区满时才传送。
//RST:用于复位因某种原因引起出现的错误连接,也用来拒绝非法数据和请求。
//SYN:在连接请求中,SYN=1,ACK=0,连接响应时,SYN=1,ACK=1。
//FIN:用来释放连接,表明发送方已经没有数据发送了。
uint16 window; //指定关于发送端能传输的下一段的大小的指令,表示想收到的每个TCP数据段的大小。
uint16 tcpchksum; //TCP校验和
uint16 urgentpoint; //紧急指针16位,紧急指针指出在本报文段中的紧急数据的位置,在URG标志设置了时才有效。
}TCP;
typedef struct udp //UDP首部结构
{
uint16 sport; //源端口号 (34 35)
uint16 dport; //目的端口号 (36 37)
uint16 length; //UDP数据包报总长度 (38 39)
uint16 udpchksum; //UDP校验和(可选项) (40 41)
}UDP;
//这是一个物理层的程序,微数据加上MAC报头
//by hongfangyu 2016/1/9
#include "dm9000.h"
void MAC_pack(uint8 *datas,uint16 length,uint16 type)
{
ETH_HDR *MACBUF;
uint8 packhead[14];
MACBUF=(ETH_HDR *)&packhead;
memcpy(MACBUF->d_mac, host_mac_addr, 6);
memcpy(MACBUF->s_mac, mac_addr, 6);
MACBUF->type = H( type );
backwards(datas,length,14); //数据包在Buffer中后移8字节
memcpy(datas,packhead,14); //将14字节MAC报头复制到Buffer前面
packet_len=length+14;
}
//这是一个网络层的程序,为数据包加上IP或者ARP报头
//by hongfangyu 2016/1/9
#include"dm9000.h"
//函数:计算IP报头的校验和
uint16 ipchecksum(uint8 *dp)
{
uint32 sum=0;
uint8 di=0;
for(di=0;di!=20;di=di+2) sum+=dp[di];
sum=sum<<8;
for(di=1;di!=21;di=di+2) sum+=dp[di];
sum=(sum&0xffff)+(sum>>16);
return ~((uint16)sum);
}
uint16 IP_pack(uint8 *datas,uint16 length,uint8 type)
{
IP *IPBUF;
uint8 packhead[20];
IPBUF=(IP *)&packhead;
IPBUF->vhl=0x45;
IPBUF->tos=0;
IPBUF->len=H(length+20);
IPBUF->ipid=H(0x0);
IPBUF->ipoffset=0;
IPBUF->ttl=0x80;
IPBUF->proto=type;
IPBUF->ipchksum=0x0; //检验和初始化为0
memcpy(IPBUF->srcipaddr, ip_addr, 4);
memcpy(IPBUF->destipaddr, host_ip_addr, 4);
IPBUF->ipchksum=H(ipchecksum(packhead));
backwards(datas,length,20); //数据包在Buffer中后移20字节
memcpy(datas,packhead,20); //将20字节IP报头复制到Buffer前面
packet_len=length+20;
return 0x0800; //将网络层的协议类型返回给下一层使用
}
uint16 ARP_pack(uint8 *datas,uint16 length,uint8 type)
{
ARP *ARPBUF;
uint8 packhead[28];
ARPBUF=(ARP *)&packhead;
ARPBUF->hwtype = H( 1 ); //硬件类型(1表示传输的是以太网MAC地址)
ARPBUF->protocol = H( 0x0800 ); //协议类型(0x0800表示传输的是IP地址)
ARPBUF->hwlen = 6;
ARPBUF->protolen = 4;
ARPBUF->opcode = H( type );
memcpy(ARPBUF->smac, mac_addr, 6);
memcpy(ARPBUF->sipaddr, ip_addr, 4);
memcpy(ARPBUF->dipaddr, host_ip_addr, 4);
//if(type==2)
backwards(datas,length,28); //数据包在Buffer中后移28字节
memcpy(datas,packhead,28); //将28字节IP报头复制到Buffer前面
packet_len=length+28;
return 0x0806; //将网络层的协议类型返回给下一层使用
}
//这是一个传输层的程序
//主要完成了接受数据层和网络层的数据,然后对TCP和UDP报头的封装和分用
//函数:计算UDP检验和
uint16 udpchecksum(uint8 *head,uint8 *datah, uint16 lengthh)
{
uint32 summ=0,suml=0,sumh=0;
uint16 pi=0;
//UDP伪首部和首部校验和
//这个伪首部指,源地址、目的地址、UDP数据长度、协议类型(0x11),协议类型就一个字节,但需要补一个字节的0x0,构成12个字节。
suml=ip_addr[1]+ip_addr[3]+host_ip_addr[1]+host_ip_addr[3]+head[5]+0x11 +head[1]+head[3]+head[5]+head[7];
sumh=ip_addr[0]+ip_addr[2]+host_ip_addr[0]+host_ip_addr[2]+head[4]+ 0 +head[0]+head[2]+head[4]+head[6];
//数据部分校验和
for(pi=1;pi!=lengthh+1;pi+=2)
suml=suml+datah[pi];
for(pi=0;pi!=lengthh;pi+=2)
sumh=sumh+datah[pi];
//UDP伪首部+UDP首部+数据 一起计算校验和,校验和为低16位,高出16位部分右移16位后加到原来低16位上
summ=(sumh<<8)+suml;
summ=(summ&0xffff)+(summ>>16);
//取反过程,并返回
return (~(uint16)summ);
}
uint8 UDP_pack(uint8 *datas,uint16 length)
{
UDP *UDPBUF;
uint8 packhead[8];
UDPBUF =(UDP*)&packhead; //这里用不用取址符号都是对的,正常理解应该不要取址符号
UDPBUF->sport=H(1024); //源端口号设置 1024=0x0400,直接写1024的话,小端模式中第一个字节为0,第二个字节为4,
//但是接收方会认为先收到的为高位,结果成了0x0004,所以应该先互换高低位写进去
UDPBUF->dport=H(2844); //目的端口号设置
UDPBUF->length=H(length+8); //UDP包长度=数据部分长度+UDP报头长度
UDPBUF->udpchksum=0; //UDP校验和初始化为0
UDPBUF->udpchksum=H(udpchecksum(packhead,datas,length)); //UDP检验和
backwards(datas,length,8); //数据包在Buffer中后移8字节
memcpy(datas,packhead,8); //将8字节UDP报头复制到Buffer前面
packet_len=length+8;
return 0x11; //返回传输层的协议为UDP,以供下一层使用
}
第五部分 之后就可以根据具体的项目需要进行不同的设计了,可以自己定义以太网帧中数据部分的协议,进行数据的交换。
结尾的话。。。。。。师兄一:这年头会一门语言真的挺重要的。
师兄二:没关系,我会中文就已经够努力了,还得天天看英文论文。
师兄一:你觉得学C++好还是学JAVA好?
师兄二:我选择狗带。。。。。。