最近的目标是完成FreeModbus在以太网上面的移植,计划用1块stm32F103的开发板+ENC28J60的以太网控制器来实现。
购买的ENC28J60上面带有一个在stm32上的web浏览器的demo例程,所以参考模块的例程,完成一个arp协议和ICMP协议下的ping测试,用来测试一下ENC28J60模块是否可以正常工作,也顺便分析一下ENC28J60的用法,为后面移植FreeModbus做准备。
main.c
从main.c出发
首先定义工程相关变量
/* mac地址和ip地址在局域网内必须唯一,否则将与其他主机冲突,导致连接不成功 */
static unsigned char mymac[6] = {0x54, 0x55, 0x58, 0x10, 0x00, 0x24};
static unsigned char myip[4] = {192, 168, 1, 15};
/* 发送数据缓冲区 */
#define BUFFER_SIZE 1500
static unsigned char buf[BUFFER_SIZE + 1];
接下来是main函数
int main(void)
{
/* 配置系统时钟为72M */
SystemInit();
/* ENC28J60 SPI 接口初始化 */
SPI_Enc28j60_Init();
/* 初始化 enc28j60 的MAC地址(物理地址),这个函数必须要调用一次 */
enc28j60Init(mymac);
/* 初始化以太网 IP 层 */
init_ip_arp_udp_tcp(mymac, myip);
while (1)
{
unsigned int plen = 0;
// get the next new packet:
plen = enc28j60PacketReceive(BUFFER_SIZE, buf);
// plen will ne unequal to zero if there is a valid packet (without crc error)
if (plen == 0)
{
continue;
}
// arp is broadcast if unknown but a host may also
// verify the mac address by sending it to
// a unicast address.
if (eth_type_is_arp_and_my_ip(buf, plen))
{
make_arp_answer_from_request(buf);
continue;
}
if (buf[IP_PROTO_P] == IP_PROTO_ICMP_V && buf[ICMP_TYPE_P] == ICMP_TYPE_ECHOREQUEST_V)
{
// a ping packet, let's send pong DOS 下的 ping 命令包
make_echo_reply_from_request(buf, plen);
continue;
}
}
}
下面分析一下每个函数的作用。
SystemInit()
在野火开发板的main函数里面都会有这个函数,作用是将系统频率设定为72M
SPI_Enc28j60_Init()
enc28j60芯片与主控芯片的通讯接口为SPI,这个函数的作用是初始化stm32的SPI接口,工程里面用到的是SPI2
enc28j60Init(mymac)
这个函数的作用是初始化enc28j60芯片,直接调用的是enc28j60的库文件,其中mymac是上面定义的以太网卡的mac地址
init_ip_arp_udp_tcp(mymac, myip)
这个函数的作用是初始化EncEthernet协议栈,
至此enc28j60的移植已经完成。接下来就是在while(1)循环里面做应用层。因为终极目标是基于LWIP和FreeModbus的Modbus TCP,所以这里的应用层只做了ARP协议和ICMP协议的ping的测试
enc28j60PacketReceive(BUFFER_SIZE, buf);
plen = enc28j60PacketReceive(BUFFER_SIZE, buf);
在while(1)循环里面首先调用enc28j60PacketReceive(BUFFER_SIZE, buf)函数获取存放在enc28j60缓冲区的数据包,存放在buf数组里面,返回值是数据包的大小,单位是byte,若返回值为0则代表未接收到数据。
下面就是对以太网数据包也就是buf数据里面的数据进行解析
ARP协议的实现
if (eth_type_is_arp_and_my_ip(buf, plen))
{
make_arp_answer_from_request(buf);
continue;
}
为什么还要加上arp协议
以太网的OSI模型把网络工作分为七层,彼此不直接打交道,只通过接口(layer interface)。IP地址工作在第三层,MAC地址工作在第二层。当协议在发送数据包时,需要先封装第三层IP地址,第二层MAC地址的报头,但协议只知道目的节点的IP地址,不知道目的节点的MAC地址,又不能跨第二、三层,所以得用ARP协议服务,来帮助获取到目的节点的MAC地址。
主机在第一次向目标IP发送数据包前,需要用到arp协议获取目标IP的物理地址(MAC地址),之后会把该IP地址与MAC地址的对应关闭保存在arp缓存表中,下一次再向该IP地址发送数据时,则不需要再次发送arp请求。
eth_type_is_arp_and_my_ip(buf, plen)
这个函数是判断arp请求,以及请求的IP地址是否为板卡的IP地址
unsigned char eth_type_is_arp_and_my_ip(unsigned char *buf,unsigned int len)
{
unsigned char i=0;
if (len<41)
{
return(0);
}
if(buf[ETH_TYPE_H_P] != ETHTYPE_ARP_H_V || buf[ETH_TYPE_L_P] != ETHTYPE_ARP_L_V) //判断是否为arp协议
{
return(0);
}
while(i<4)
{
if(buf[ETH_ARP_DST_IP_P+i] != ipaddr[i]) //判断IP地址是否为板卡的IP的地址
{
return(0);
}
i++;
}
return(1);
}
make_arp_answer_from_request(buf)
若判断接收到的数据包为对板卡IP地址的arp请求,则调用该函数构建arp响应帧。
void make_arp_answer_from_request(unsigned char *buf)
{
unsigned char i=0;
//
make_eth(buf);
buf[ETH_ARP_OPCODE_H_P]=ETH_ARP_OPCODE_REPLY_H_V; //arp
buf[ETH_ARP_OPCODE_L_P]=ETH_ARP_OPCODE_REPLY_L_V;
// fill the mac addresses:
while(i<6)
{
buf[ETH_ARP_DST_MAC_P+i]=buf[ETH_ARP_SRC_MAC_P+i];
buf[ETH_ARP_SRC_MAC_P+i]=macaddr[i];
i++;
}
i=0;
while(i<4)
{
buf[ETH_ARP_DST_IP_P+i]=buf[ETH_ARP_SRC_IP_P+i];
buf[ETH_ARP_SRC_IP_P+i]=ipaddr[i];
i++;
}
// eth+arp is 42 bytes:
enc28j60PacketSend(42,buf);
}
ping请求及其实现
ping的工作原理
利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,再要求对方返回一个同样大小的数据包来确定两台网络机器是否连接相通,时延是多少。
ICMP报文通用格式如下:
类型:1个字节。8表示回显请求报文,0表示回显响应报文。
代码:1个字节。回显请求报文、回显响应报文 时均为0。
校验和:2个字节。非重点,略过。
标识符:2个字节。发送ICMP报文的客户端进程的id,服务端会回传给客户端。因为同一个客户端可能同时运行多个ping程序,这样客户端收到回响报文,可以知道是响应给哪个客户端进程的。
序列号:2个字节。从0开始,客户端每次发送新的回显请求时+1。服务端原样会传。
数据:6个字节。客户端记录回显请求的发送时间,服务端记录响应的发送时间
ping请求响应的实现
if (buf[IP_PROTO_P] == IP_PROTO_ICMP_V && buf[ICMP_TYPE_P] == ICMP_TYPE_ECHOREQUEST_V)
//首先判断以太网帧的IP_PROTO_P(0x17)位和ICMP_TYPE_P(0x22)位
//buf[IP_PROTO_P] == IP_PROTO_ICMP_V:0x17位等于1时,表示为ICMP协议
//buf[ICMP_TYPE_P] == ICMP_TYPE_ECHOREQUEST_V):0x22等于8时表示为ICMP请求帧,0x22等于0时表示为响应帧
{
//构建ICMP响应帧
make_echo_reply_from_request(buf, plen);
continue;
}
void make_echo_reply_from_request(unsigned char *buf,unsigned int len)
{
make_eth(buf);//构建以太网帧头
make_ip(buf);//构建IP帧头
buf[ICMP_TYPE_P]=ICMP_TYPE_ECHOREPLY_V;//将ICMP帧类型修改为响应帧
//更改校验位
if (buf[ICMP_CHECKSUM_P] > (0xff-0x08))
{
buf[ICMP_CHECKSUM_P+1]++;
}
buf[ICMP_CHECKSUM_P]+=0x08;
enc28j60PacketSend(len,buf);//发送以太网帧
}
make_echo_reply_from_request(buf, plen) 构建ICMP响应帧
ICMP的ping命令请求帧与响应帧的数据段内容是一样的,需要更改的地方有几点
1、以太网帧头:源MAC地址与目标MAC地址交换
2、IP帧头:源IP地址与目标IP地址交换
3、ICMP帧类型:将请求帧标志(8)改为(0)
4、更改校验和
关于ping响应帧校验和的计算
先贴一下TCP/IP中校验和的计算方法:
(1)把校验和字段置为0;
(2)把需校验的数据看成以16位为单位的数字组成,依次进行二进制反码求和;
(3)把得到的结果存入校验和字段中。
而在这个ping响应帧的校验和计算用了一个比较巧妙方法,这也是这个工程里面一个比较有意思的地方。
//更改校验位
if (buf[ICMP_CHECKSUM_P] > (0xff-0x08))
{
buf[ICMP_CHECKSUM_P+1]++;
}
buf[ICMP_CHECKSUM_P]+=0x08;
在ping请求帧与响应帧的数据段里面,可以发现唯一不同的地方就在于响应帧的帧类型位是0而请求帧的帧类型是8。换言之,只要将请求帧的校验和+0x08,就得到了响应帧的校验和,而不需要重新再计算校验和。还有一种情况是当buf[ICMP_CHECKSUM_P]+0x08 > 0xff,即当校验和的低八位 + 0x08后溢出,此时则要再校验和的高位+1.
ping测试
以太网控制器的网口使用网线与PC的网口连接,更改PC的网络设置,将PC的IP地址改成跟控制器的设定IP地址在同一个网段,但不能一样,否则会造成IP地址冲突,无法正常建立网络连接。
这是我的电脑网络设置。
进入电脑的控制台尝试ping 开发板的IP地址,成功ping通
为了了解整个ping通的过程,再深入一点,使用wireshark抓取网口的数据流。
通过抓包可以发现,在控制台发出ping请求后,电脑首先发出一个arp请求板卡IP地址(192.168.1.15)对应的MAC地址,板卡进行arp响应发送本地的MAC地址,arp过程完成后才进行ping的ICMP请求。若板卡不带arp协议,无法响应PC的arp请求,PC无法获取MAC地址,后面的ICMP请求就不会发出,即无法ping通。
最后贴一下整个ping过程的数据报文。
arp请求:
arp响应:
ping请求:
ping响应: