STM32NET学习笔记 IP ICMP部分

1.前言 
    嵌入式以太网开发是一个很有挑战性的工作。通过几个月的学习,我个人觉得大致有两条途径。第一条途径,先通过高级语言熟悉socket编程,例如C#或C++,对bind,listen,connect,accept等函数熟悉之后,应用 lwIP。第二种途径,通过分析嵌入式以太网代码,结合TCPIP协议栈规范逐步实践代码。第一种途径效率高,开发周期短,编写出来的代码性能稳定,第二种途径花的时间长,开发出来的代码功能不完善,但是由于紧紧结合TCPIP规范,可以了解的内容较多,适合学习。本文通过分析和修改AVRNET源码,逐步实现TCPIP协议栈的各个子部分,包括ETHERNET部分,ARP部分,IP部分,ICMP部分,UDP部分,TCP部分和HTTP部分。
 本文将实现IP部分和ICMP部分。

1.2 相关资料
【 ENC28J60学习笔记】
【 STM32NET学习笔记 ARP和Ethernet部分】
【 AVRNET项目(国外)  
【 AVR webserver项目(国外)  

1.3 代码仓库
    【 代码仓库】——CSDN Code代码仓库。

2.IP部分实现
    IP层是TCP和UDP实现的基础。IP首部紧跟以太网首部,长度为20字节。IP首部具有最基本的两个任务,
    【第一】定义IP包的具体协议类型,例如ICMP,TCP或UDP等;
    【第二】定义IP报文从哪个IP地址来和到哪个IP地址去。
    需要强调,在同一个子网中即同一个物理网络中, IP报文中的目标IP地址和以太网首部中的目标MAC地址相对应,若不在同一个物理网路中, 目标IP地址和目标MAC地址不同,目标MAC地址被路由器的MAC地址替代,意味着通过路由器转发报文。在IP首部中还包括很多其他内容,需要注意的是IP标识符,该标识符主要用于区分IP报文,最简单的算法即每发送一个IP报文后IP标识符累加。具体通过以下代码实现IP首部的填充。

2.1 IP首部填充
// IP首部总长度
#define IP_HEADER_LEN 20
// 协议类型
// ICMP协议
#define IP_PROTO_ICMP_V 0x01
// TCP协议
#define IP_PROTO_TCP_V 0x06
// UDP协议
#define IP_PROTO_UDP_V 0x11
// IPV4版本
#define IP_V4_V 0x40
#define IP_HEADER_LENGTH_V 0x05
// IP版本号位置 以太网首部2+6+6
#define IP_P 0x0E
// 首部长度
#define IP_HEADER_VER_LEN_P 0x0E
// 服务类型
#define IP_TOS_P 0x0F
// IP总长度
#define IP_TOTLEN_H_P 0x10
#define IP_TOTLEN_L_P 0x11
// IP标识
#define IP_ID_H_P 0x12
#define IP_ID_L_P 0x13
//
#define IP_FLAGS_H_P 0x14
#define IP_FLAGS_L_P 0x15
// TTL生存时间
#define IP_TTL_P 0x16
// IP协议类型 例如ICMP TCP UDP
#define IP_PROTO_P 0x17
// 首部校验和
#define IP_CHECKSUM_H_P 0x18
#define IP_CHECKSUM_L_P 0x19
// 源IP地址
#define IP_SRC_IP_P 0x1A
// 目标IP地址
#define IP_DST_IP_P 0x1E
void ip_generate_header ( BYTE *rxtx_buffer, WORD_BYTES total_length, BYTE protocol, BYTE *dest_ip )
{
  BYTE i;
  // 校验结果
  WORD_BYTES ck;
 
  // 版本号和首都长度
  rxtx_buffer[ IP_P ] = IP_V4_V | IP_HEADER_LENGTH_V;
  // 服务类型
  rxtx_buffer[ IP_TOS_P ] = 0x00;
  // 总长度
  rxtx_buffer [ IP_TOTLEN_H_P ] = total_length.byte.high;
  rxtx_buffer [ IP_TOTLEN_L_P ] = total_length.byte.low;
 
  // IP标识
  rxtx_buffer [ IP_ID_H_P ] = ip_identfier >> 8;
  rxtx_buffer [ IP_ID_H_P ] = ip_identfier & 0x00ff;
  // 累加
  ip_identfier++;
 
  // 标志和分片偏移
  rxtx_buffer [ IP_FLAGS_H_P ] = 0x00;
  rxtx_buffer [ IP_FLAGS_L_P ] = 0x00;
 
  // 生存时间
  rxtx_buffer [ IP_TTL_P ] = 128;
 
  // set ip packettype to tcp/udp/icmp...
  rxtx_buffer [ IP_PROTO_P ] = protocol;
 
  // 设定目标地址和源地址
  for ( i = 0; i < 4; i++ )
  {
    rxtx_buffer[ IP_DST_IP_P + i ] = dest_ip[i];
    rxtx_buffer[ IP_SRC_IP_P + i ] = avr_ip.byte[ i ];
  }
 
  // 校验结果
  rxtx_buffer[ IP_CHECKSUM_H_P ] = 0;
  rxtx_buffer[ IP_CHECKSUM_L_P ] = 0;
  ck.word = software_checksum ( &rxtx_buffer[ IP_P ], sizeof(IP_HEADER), 0 );
  rxtx_buffer[ IP_CHECKSUM_H_P ] = ck.byte.high;
  rxtx_buffer[ IP_CHECKSUM_L_P ] = ck.byte.low;
}


2.2 IP报文查询
    IP报文查询功能对应于ARP报文查询功能,通过以太网首部中的最后2个字节判断该报文是否为IP报文;如果是IP报文则继续和本机IP地址相比较。如果两步检查均通过则认为是合法的IP报文,当然这其中舍弃了IP版本号和首部校验和的检查,虽然存在某些隐患但并不妨碍实现基本功能。
BYTE ip_packet_is_ip ( BYTE *rxtx_buffer )
{
  unsigned char i;
 
  // 检查该报文是否为IP报文
  if ( rxtx_buffer[ ETH_TYPE_H_P ] != ETH_TYPE_IP_H_V || rxtx_buffer[ ETH_TYPE_L_P ] != ETH_TYPE_IP_L_V)
    return 0;
 
  // 检查该报文的IP地址是否为本机IP地址,逐个检查
  for ( i=0; i<sizeof(IP_ADDR); i++ )
  {
    if ( rxtx_buffer[ IP_DST_IP_P + i ] != avr_ip.byte[i] )
      return 0;
  }
 
  // 若该报文为IP报文,且目标IP地址为本机地址,返回1
  return 1;
}


3.ICMP部分实现
    虽然ICMP具有很多的子协议,但是其中最著名的要数ping程序, 即ICMP回显请求和应答报文。通过使用ping命令来判断报文是否可以到达目标地址。ICMP的实现是一个逐步遵守规则的过程,即向固定的字节填充数据。
// 回显应答
#define ICMP_TYPE_ECHOREPLY_V 0
// 回显请求
#define ICMP_TYPE_ECHOREQUEST_V 8
// ICMP首部长度
#define ICMP_PACKET_LEN 40
// ICMP类型
#define ICMP_TYPE_P 0x22
// ICMP代码
#define ICMP_CODE_P 0x23
// ICMP首部校验和
#define ICMP_CHECKSUM_H_P 0x24
#define ICMP_CHECKSUM_L_P 0x25
// ICMP标识符
#define ICMP_IDENTIFIER_H_P 0x26
#define ICMP_IDENTIFIER_L_P 0x27
// ICMP序号
#define ICMP_SEQUENCE_H_P 0x28
#define ICMP_SEQUENCE_L_P 0x29
#define ICMP_DATA_P 0x2A


3.1 ICMP首部填充
ICMP首部填充需要根据协议类型填充不同的内容,对于回显请求而言只需在ICMP协议类型部分填充0即可,当然ICMP部分也包括ICMP首部校验和。
void icmp_generate_packet ( BYTE *rxtx_buffer ,BYTE type)
{
  BYTE i;
  WORD_BYTES ck;
 
  // ICMP回显请求
  if( type == ICMP_TYPE_ECHOREQUEST_V )
  {
    rxtx_buffer[ ICMP_TYPE_P ] == ICMP_TYPE_ECHOREQUEST_V;
    rxtx_buffer[ ICMP_CODE_P ] = 0;
    rxtx_buffer[ ICMP_IDENTIFIER_H_P ] = icmp_id;
    rxtx_buffer[ ICMP_IDENTIFIER_L_P ] = 0;
    rxtx_buffer[ ICMP_SEQUENCE_H_P ] = icmp_seq;
    rxtx_buffer[ ICMP_SEQUENCE_L_P ] = 0;
    icmp_id++;
    icmp_seq++;
    for ( i=0; i<ICMP_MAX_DATA; i++ )
    {
      rxtx_buffer[ ICMP_DATA_P + i ] = 'A' + i;
    }
  }
  // ICMP回显
  if(type == ICMP_TYPE_ECHOREPLY_V)
  {
    rxtx_buffer[ ICMP_TYPE_P ] = ICMP_TYPE_ECHOREPLY_V;
  }
 
  // ICMP首部校验和
  rxtx_buffer[ ICMP_CHECKSUM_H_P ] = 0;
  rxtx_buffer[ ICMP_CHECKSUM_L_P ] = 0;
  ck.word = software_checksum ( &rxtx_buffer[ ICMP_TYPE_P ], sizeof(ICMP_PACKET), 0 );
  rxtx_buffer[ ICMP_CHECKSUM_H_P ] = ck.byte.high;
  rxtx_buffer[ ICMP_CHECKSUM_L_P ] = ck.byte.low;
}


3.2 ICMP回显应答
 ICMP回显应答需要做好两步,第一步检查IP首部中的协议类型是否为ICMP报文;第二,检查ICMP首部中的ICMP类型是否为ICMP请求,如果是则生成ICMP回显应答并通过以太网驱动芯片发送。为了便于调试,在接收到ICMP回显请求时通过串口输出发起方的IP地址,ping命令发起方的IP地址存在于IP首部中的源IP地址部分。

BYTE icmp_send_reply ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )
{
 
  // IP首部中为ICMP协议类型
  if ( rxtx_buffer [ IP_PROTO_P ] != IP_PROTO_ICMP_V )
    return 0;
 
  // 是否为ICMP回显请求
  if ( rxtx_buffer [ ICMP_TYPE_P ] != ICMP_TYPE_ECHOREQUEST_V )
return 0;
#ifdef ICMP_DEBUG
  printf("ICMP Request!\n");
  printf("Ping from:%d.%d.%d.%d\n",\
            rxtx_buffer[IP_SRC_IP_P+0],rxtx_buffer[IP_SRC_IP_P+1],\
            rxtx_buffer[IP_SRC_IP_P+2],rxtx_buffer[IP_SRC_IP_P+3]);
#endif
  // 生成以太网首部
  eth_generate_header ( rxtx_buffer, (WORD_BYTES){ETH_TYPE_IP_V}, dest_mac );
 
  // 生成IP首部
  ip_generate_header ( rxtx_buffer, (WORD_BYTES){(rxtx_buffer[IP_TOTLEN_H_P]<<8)|rxtx_buffer[IP_TOTLEN_L_P]}, IP_PROTO_ICMP_V, dest_ip );
  // 生成ICMP首部
  icmp_generate_packet ( rxtx_buffer ,(BYTE)ICMP_TYPE_ECHOREPLY_V);
  // 发送报文
  enc28j60_packet_send ( rxtx_buffer, sizeof(ETH_HEADER) + sizeof(IP_HEADER) + sizeof(ICMP_PACKET) );
  return 1;
}

4.实例
    通过ping命令可以测试报文是否可以到达目标主机。例如发送ping 192.168.1.115。
    在程序的无线循环中,需要层层进行查询。
【1】查询以太网中是否有数据,若无数据则返回。
【2】保存源MAC地址,待返回时使用。
【3】查询是否为ARP报文并返回ARP报文。
【4】保存源IP地址,待返回时使用。
【5】查询是否为IP报文,若非IP报文返回。
【6】查询是否为ICMP报文并返回ICMP回显应答。

 /* 获得新的IP报文 */
  plen = enc28j60_packet_receive( (BYTE*)&rxtx_buffer, MAX_RXTX_BUFFER );
  if(plen==0) return;
  /* 保存客服端的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;
  }
  /* 保存客服端的IP地址 */
  memcpy ( (BYTE*)&client_ip, &rxtx_buffer[ IP_SRC_IP_P ], sizeof(IP_ADDR) );
  /* 检查该报文是否为IP报文 */
  if ( ip_packet_is_ip ( (BYTE*)&rxtx_buffer ) == 0 )
  {
    return;
  }
 
  /* 如果是ICMP报文 向发起方返回数据 */
  if ( icmp_send_reply ( (BYTE*)&rxtx_buffer, (BYTE*)&client_mac, (BYTE*)&client_ip ) )
  {
    return;
  }


【实验结果】
  STM32NET学习笔记 IP ICMP部分_第1张图片
图1 PING命令
  STM32NET学习笔记 IP ICMP部分_第2张图片
图2 串口输出


你可能感兴趣的:(stm32,嵌入式以太网)