本帖最后由 xukai871105 于 2013-2-12 23:02 编辑
1.前言 嵌入式以太网开发是一个很有挑战性的工作。通过几个月的学习,我个人觉得大致有两条途径。第一条途径,先通过高级语言熟悉socket编程,例如C#或C++,对bind,listen,connect,accept等函数熟悉之后,上手lwIP或uIP。第二种途径,通过分析嵌入式以太网代码,结合TCPIP协议栈规范一点一点实践代码。第一种途径效率高,开发周期短,编写的代码性能稳定,第二种途径花费时间长,代码功能不完善,但是由于紧紧结合TCPIP规范,可以了解的内容较多,适合学习。本文通过分析和修改AVRNET源码,逐步实现TCPIP协议栈的各个子部分,包括ETHERNET部分,ARP部分,IP部分,ICMP部分,UDP部分,TCP部分和HTTP部分。 本文先实现ethernet部分和ARP部分。 1.2 其他说明硬件平台 Atmega32 + proteus 7.10 sp0+WinPcap 编译平台 AVR Studio 6 仿真设置 在仿真的过程中,需要保持有线网卡处于连接状态。通过不断的尝试我发现使用proteus仿真以太网时并不能抓取无线网卡的数据,使用无线网卡将得不到正确的仿真结果。在实践之前,需要通过ipconfig命令查看PC机的IP地址和MAC地址,AVR的IP地址设定必须和PC机在同一个网段中。例如 PC机IP:192.168.1.101 AVR IP: 192.168.1.15 局域网访问 如果有AVR开发板或者其他CPU的开发板的话,可以把开发板的以太网端口连接到路由器LAN端口,只要保证开发板的IP地址和PC机的IP地址在同一个网段,无论有线还是无线都可以获得仿真结果。 广域网访问 如果有固定的电信网IP地址的话,可以在路由器中设置静态端口映射,把某个端口映射成局域网内的IP地址和端口号。若没有固定IP地址的话,可使用花生壳虚拟一个域名。由于设置步骤较多,再次不详细介绍。 2.初始化- // MAC地址结构体
- typedef struct _MAC_ADDR
- {
- BYTE byte[6];
- }MAC_ADDR;
- // IP地址结构体
- typedef struct _IP_ADDR
- {
- BYTE byte[4];
- }IP_ADDR;
- // 初始化MAC地址
- avr_mac.byte[0] = 'A';
- avr_mac.byte[1] = 'V';
- avr_mac.byte[2] = 'R';
- avr_mac.byte[3] = 'P';
- avr_mac.byte[4] = 'O';
- avr_mac.byte[5] = 'R';
-
- // 初始化IP地址
- avr_ip.byte[0] = 192;
- avr_ip.byte[1] = 168;
- avr_ip.byte[2] = 1;
- avr_ip.byte[3] = 105;
复制代码 MAC地址和IP地址均为自定义的结构体,结构体中为一个字节数组。严格来说,MAC地址不能胡乱定义,应严格遵守相关规范,如果条件允许的话可以使用带有全球唯一的MAC地址的EEPROM芯片。局域网仿真时,AVR的IP地址必须要和PC机IP地址处于同一个网段,即使是广域网实现AVR的IP地址也应和路由器的IP在同一个网段中。 3.实现ETHERNET TCPIP是一系列协议的组合,其中最有名的为TCP协议和IP协议。但是千万不要忽视最底层的协议结构——ETHERNET。ETHERNET包括14个字节,可以称之为以太网首部,其中前六个字节为目标MAC地址,紧着的6个字节为源MAC地址,最后的两个字节为协议类型。以太网的实现通信时必须要知道双方的MAC地址,发送方不明确接收方的地址可以通过ARP协议寻找目标MAC地址,如果依然没有结果则可只能把该报文转发给路由器,让路由器处理该报文。协议类型只需关心两种,0800的IP协议和0806的ARP协议。 AVRNET项目中通过以下代码实现: - // 协议类型 ARP报文
- #define ETH_TYPE_ARP_V 0x0806
- #define ETH_TYPE_ARP_H_V 0x08
- #define ETH_TYPE_ARP_L_V 0x06
- // 协议类型 以太网报文
- #define ETH_TYPE_IP_V 0x0800
- #define ETH_TYPE_IP_H_V 0x08
- #define ETH_TYPE_IP_L_V 0x00
- // 以太网报文头部长度 14
- #define ETH_HEADER_LEN 14
- // 目标MAC地址
- #define ETH_DST_MAC_P 0
- // 源MAC地址
- #define ETH_SRC_MAC_P 6
- // 协议类型
- #define ETH_TYPE_H_P 12
- #define ETH_TYPE_L_P 13
- void eth_generate_header ( BYTE *rxtx_buffer, WORD_BYTES type, BYTE *dest_mac )
- {
- BYTE i;
- // 配置以太网报文 目标MAC地址和源MAC地址
- for ( i=0; i<sizeof(MAC_ADDR); i++)
- {
- rxtx_buffer[ ETH_DST_MAC_P + i ] = dest_mac[i];
- // avr_mac为全局变量
- rxtx_buffer[ ETH_SRC_MAC_P + i ] = avr_mac.byte[i];
- }
- // 配置协议类型 IP报文或ARP报文
- rxtx_buffer[ ETH_TYPE_H_P ] = type.byte.high;
- rxtx_buffer[ ETH_TYPE_L_P ] = type.byte.low;
- }
复制代码eth_generate_header函数实现了填充以太网首部的功能,第一个输入参数为发送接收缓冲区。第二个参数为IP类型,在AVRNET项目中传入的参数不是0800的IP协议类型就是0806的ARP协议类型。第三个参数为目标MAC地址,由于本机MAC地址作为了全局变量,可以在函数内部填充到缓冲区中。 4.实现ARP 为了使用最少的代码实现TCPIP功能,假设通过IP发送报文时已经确认了目标的IP地址,AVRNET总是先被动的通过ARP先让PC机知道其MAC地址,这样当PC机发送UDP或者TCP报文时,在报文中已经包含了PC机的IP地址,AVR仅需从rxtx_buffer中取出PC机IP地址。如果没有这样的假设的话,AVR需要有一张MAC表,每次发送报文时需要查找该表并不断更新。ARP协议是一个找邻居的过程,是一个广播找MAC的过程。发出者通过广播报文确认某个IP的MAC地址。ARP首部包括,2字节硬件类型,2字节协议类型,1字节硬件长度,1字节协议长度,2字节操作码,6字节发送者硬件地址,4字节发送者IP地址,6字节目标硬件地址和4字节目标IP地址。 在使用ARP协议时需要注意三点,第一,操作码分为两种——ARP请求和ARP响应,ARP请求的编码为1,ARP响应的编码为2,先有请求后有响应。第二,发送ARP协议请求时请求方明确对方IP地址,但是不明确对方MAC地址,所以在请求报文中MAC地址全部以0替代。第三,由于不知道对方的MAC地址,所以只能通过广播帧发送以太网数据,所以以太网首部的前6个字节被FF填充。 为了便于ARP功能的实现,AVRNET在arp.h文件中定义了以下宏定义 - #define ARP_PACKET_LEN 28
- // ARP请求
- #define ARP_OPCODE_REQUEST_V 0x0001
- #define ARP_OPCODE_REQUEST_H_V 0x00
- #define ARP_OPCODE_REQUEST_L_V 0x01
- // ARP响应
- #define ARP_OPCODE_REPLY_V 0x0002
- #define ARP_OPCODE_REPLY_H_V 0x00
- #define ARP_OPCODE_REPLY_L_V 0x02
- // 硬件类型 10M以太网
- #define ARP_HARDWARE_TYPE_H_V 0x00
- #define ARP_HARDWARE_TYPE_L_V 0x01
- // 协议类型 IPV4
- #define ARP_PROTOCOL_H_V 0x08
- #define ARP_PROTOCOL_L_V 0x00
- // 硬件地址长度
- #define ARP_HARDWARE_SIZE_V 0x06
- // 协议地址长度
- #define ARP_PROTOCOL_SIZE_V 0x04
- // 硬件类型 2字节
- #define ARP_HARDWARE_TYPE_H_P 0x0E
- #define ARP_HARDWARE_TYPE_L_P 0x0F
- // 协议类型 2字节
- #define ARP_PROTOCOL_H_P 0x10
- #define ARP_PROTOCOL_L_P 0x11
- // 硬件地址 1字节
- #define ARP_HARDWARE_SIZE_P 0x12
- // 协议地址长度 1字节
- #define ARP_PROTOCOL_SIZE_P 0x13
- // 操作码 2字节
- #define ARP_OPCODE_H_P 0x14
- #define ARP_OPCODE_L_P 0x15
- // 发送者硬件地址 6字节
- #define ARP_SRC_MAC_P 0x16
- // 发送者IP地址 4字节
- #define ARP_SRC_IP_P 0x1C
- // 目标硬件地址 6字节
- #define ARP_DST_MAC_P 0x20
- // 目标IP地址 6字节
- #define ARP_DST_IP_P 0x26
复制代码 在没有操作系统的支持下,一般通过一个无线循环实现子功能的实现。AVRNET项目中,通过某个process不断查询是否存在网卡数据,如果有网卡数据则立刻保存源MAC地址。,因为AVRNET项目中没有维护ARP表,所以必须及时记录发送发的MAC地址,方便向它返回数据。紧着便是查询该报文是否为ARP请求,如果是ARP请求则返回ARP响应。具体代码如下 - void server_process ( void )
- {
- MAC_ADDR client_mac;
- IP_ADDR client_ip;
- BYTE rxtx_buffer[MAX_RXTX_BUFFER];
- WORD plen;
-
- /* 获得新的IP报文 */
- plen = enc28j60_packet_receive( (BYTE*)&rxtx_buffer, MAX_RXTX_BUFFER );
-
- if(plen==0)
- return;
- /* 保存该报文的源MAC地址,即保存客服端的MAC地址 */
- memcpy ( (BYTE*)&client_mac, &rxtx_buffer[ ETH_SRC_MAC_P ], sizeof(MAC_ADDR) );
- /* 检查该报文是不是ARP请求 */
- if ( arp_packet_is_arp( rxtx_buffer, (WORD_BYTES){ARP_OPCODE_REQUEST_V} ) )
- {
- /*返回ARP响应 */
- arp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac );
- return;
- }
- }
复制代码 4.1 查询ARP报文 查询该报文是否是针对AVR的ARP报文需要确认三点,第一:确认以太网首部中的协议类型是否为ARP协议类型,ARP协议类型的值为0806H。第二,查询该ARP报文是否为ARP请求,该步骤需要到ARP首部中查询ARP操作码,ARP请求的操作码为1。第三,查询该ARP请求中的MAC地址是否和本机MAC匹配。 最后通过宏定义ARP_DEBUD决定是否通过串口输出发起者IP地址和MAC地址。通过串口打印可以确认该ARP报文的发起者。 - BYTE arp_packet_is_arp ( BYTE *rxtx_buffer, WORD_BYTES opcode )
- {
- BYTE i;
-
- // 该报文为ARP报文
- if( rxtx_buffer[ ETH_TYPE_H_P ] != ETH_TYPE_ARP_H_V || rxtx_buffer[ ETH_TYPE_L_P ] != ETH_TYPE_ARP_L_V)
- return 0;
- // 确认ARP操作码 ARP请求 1 ARP应答2
- if ( rxtx_buffer[ ARP_OPCODE_H_P ] != opcode.byte.high || rxtx_buffer[ ARP_OPCODE_L_P ] != opcode.byte.low )
- return 0;
- // 匹配IP地址
- for ( i=0; i<sizeof(IP_ADDR); i++ )
- {
- if ( rxtx_buffer[ ARP_DST_IP_P + i] != avr_ip.byte[i] )
- return 0;
- }
-
- // 通过串口输出
- #ifdef ARP_DEBUG
- printf("ARP Message!\n");
- printf("Source IP:%d.%d.%d.%d\n",\
- rxtx_buffer[ARP_SRC_IP_P+0],rxtx_buffer[ARP_SRC_IP_P+1],\
- rxtx_buffer[ARP_SRC_IP_P+2],rxtx_buffer[ARP_SRC_IP_P+3]);
- printf("Source MAC:%02X-%02X-%02X-%02X-%02X-%02X",\
- rxtx_buffer[ARP_SRC_MAC_P+0],rxtx_buffer[ARP_SRC_MAC_P+1],\
- rxtx_buffer[ARP_SRC_MAC_P+2],rxtx_buffer[ARP_SRC_MAC_P+3],\
- rxtx_buffer[ARP_SRC_MAC_P+4],rxtx_buffer[ARP_SRC_MAC_P+5]);
-
- #endif
- return 1;
- }
复制代码 4.2 生成ARP首部 生成ARP首部还是紧紧围绕两个地址展开,即目标MAC地址和目标IP地址,在ARP响应过程中,源MAC地址和IP地址现在转变为了目标MAC地址和IP地址。 - void arp_generate_packet ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )
- {
- unsigned char i;
-
- // 硬件类型 0001 10M以太网
- rxtx_buffer[ ARP_HARDWARE_TYPE_H_P ] = ARP_HARDWARE_TYPE_H_V;
- rxtx_buffer[ ARP_HARDWARE_TYPE_L_P ] = ARP_HARDWARE_TYPE_L_V;
-
- // 协议类型
- rxtx_buffer[ ARP_PROTOCOL_H_P ] = ARP_PROTOCOL_H_V;
- rxtx_buffer[ ARP_PROTOCOL_L_P ] = ARP_PROTOCOL_L_V;
-
- // 硬件地址长度
- rxtx_buffer[ ARP_HARDWARE_SIZE_P ] = ARP_HARDWARE_SIZE_V;
-
- // 协议地址长度
- rxtx_buffer[ ARP_PROTOCOL_SIZE_P ] = ARP_PROTOCOL_SIZE_V;
-
- // 目标硬件地址和源硬件地址
- for ( i=0; i<sizeof(MAC_ADDR); i++)
- {
- rxtx_buffer[ ARP_DST_MAC_P + i ] = dest_mac[i];
- rxtx_buffer[ ARP_SRC_MAC_P + i ] = avr_mac.byte[i];
- }
-
- // 目标IP地址和源IP地址
- for ( i=0; i<sizeof(IP_ADDR); i++)
- {
- rxtx_buffer[ ARP_DST_IP_P + i ] = dest_ip[i];
- rxtx_buffer[ ARP_SRC_IP_P + i ] = avr_ip.byte[i];
- }
- }
复制代码 4.3 响应ARP请求 ARP响应可以体现出TCP IP报文产生的基本过程,即层层包装。先包装以太网首部,在包装ARP首部,最后通过ENC28J60发送即可。 - void arp_send_reply ( BYTE *rxtx_buffer, BYTE *dest_mac )
- {
- // 生成以太网报文
- eth_generate_header ( rxtx_buffer, (WORD_BYTES){ETH_TYPE_ARP_V}, dest_mac );
-
- // ARP报文类型为ARP响应
- rxtx_buffer[ ARP_OPCODE_H_P ] = ARP_OPCODE_REPLY_H_V;
- rxtx_buffer[ ARP_OPCODE_L_P ] = ARP_OPCODE_REPLY_L_V;
- // 生成ARP请求,传入请求者MAC地址,请求者IP地址
- arp_generate_packet ( rxtx_buffer, dest_mac, &rxtx_buffer[ ARP_SRC_IP_P ] );
-
- // 发送以太网报文
- enc28j60_packet_send ( rxtx_buffer, sizeof(ETH_HEADER) + sizeof(ARP_PACKET) );
- }
复制代码 5.实例 通过一个实例让PC机确认AVR的MAC地址,PC机试图通过ping命令发送一个ICMP报文,ping命令时非常常用的确认网络是否连接的命令,例如发送ping192.168.1。105,由于PC机不明确该IP地址的MAC地址,所以会先发送一个ARP请求。AVR可以捕获这一ARP请求,并通过串口输出PC机的IP地址和MAC地址。如果正确输出了PC机的IP地址和MAC地址,并且通过arp命令可以查看到AVR的网卡即说明以上代码运行成功。此时先不用理会是否可以ping通,这个会在以后的文章中实现。 在开始之前可以通过ipconfig /all指令查询本机的IP地址和MAC地址,通过arp -a指令查询PC机中ARP缓冲表。如果有必要可以使用arp –d清除缓冲表的所有内容。 图通过ipconfig /all 查询PC机IP地址和MAC地址 图通过arp命令查询arp缓存表 图 仿真结果1 图 串口打印结果 图 更新之后的ARP缓冲区 从仿真的结果可以看出,AVR成功收到了ARP请求,并返回了ARP响应,PC机更新了ARP缓冲表,加入了AVR的IP地址和MAC地址。即图中的192.1。168.105,对应的MAC地址为41-56-52-50-4f-52,正是AVRNET的ASCII编码的16进制表达。 (工程文件中,icmp,udp,http均已实现,还包括enc28j60的驱动)
|