STM32+enc28j60+uip 实现单片机 ping PC端
- 1. 前言
- 2. 实验简介
- 3. uip简介
- 4. icmp简介
- 4.1 icmp介绍
- 4.2 请求回显或回显应答报文格式介绍
- 5. 实验环境
- 6. 实验内容
- 6.1 实验方案
- 6.2 请求回显报文的发送
- 6.3 回显应答报文的接收与校验
- 7. 实验结果
- 8. 实验结果分析
- 9. 总结
1. 前言
临近毕业,多年在csdn等各大论坛闯荡(学习)的我,终于下定决心,开始写自己人生中的第一篇博客。
在学习了一段时间的uip协议栈后,走了很多弯路,所以想与大家分享自己的学习经历。本人没啥文笔,只能将自己所学所感与大家分享,本文的部分内容也是通过csdn等各大论坛收集整理而来,忠心希望大家能将意见或者建议在评论区与我分享,与大家共勉。
2. 实验简介
本次实验主要采用stm32最小系统开发板,MCU为stm32f103c8t6,搭载了enc28j60以太网模块,工程代码基于uip协议栈,实现了实现单片机 ping PC端。
3. uip简介
关于uip的学习,可参考xukai871105大神的博客—【uIP学习笔记】
4. icmp简介
4.1 icmp介绍
ICMP(Internet Control Message Protocol),网络控制消息协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。ICMP的协议号为1。
ICMP协议的功能主要有:
(1)确认IP包是否成功到达目标地址。
(2)通知在发送过程中IP包被丢弃的原因。
ICMP报文是在IP报文内部的!!!
ICMP报文分为查询报文和差错报文。
4.2 请求回显或回显应答报文格式介绍
注:本次实验主要实现的是ping功能,用到的是ICMP查询报文中的请求回显或回显应答报文(Echo or Echo Reply Message),所以对icmp其他报文类型不做展开。
报文内容的开始,是以太网帧头,包括目的主机的mac地址,源主机的mac地址,协议类型,共14Bytes(PC端的网卡MAC地址可通过cmd命令:ipconfig/all 查看,由于enc28j60没有唯一的mac标识,在实验时可随机设置)。
其次是报文类型,该处字符若为0x8000,说明该报文是IPv4类型。
接着是IP首部字段(IP Header),总长20Bytes。首部字段包括:
(1)IP Version:4,说明是IPv4),1Bytes;
(2)包头长度(Header Length),1Bytes;
(3)区分服务领域(Differentiated Services Field),1Bytes;
(4)总长度(Total Length),1Bytes;
(5)标识符(Identification),2Bytes;
(6)标记字段(Flags),2Bytes;
(8)报文生存时间(TTL),1Bytes;
(9)报文所用协议类型(Protocol),1Bytes;
(10)IP包头检验和(IP Header checksum),2Bytes,
(11)源IP(发送方IP),4Bytes;
(12)目的IP(接收方IP),4Bytes。
其中,IP包头检验和计算方法如下:
1.checksum的初始值自动被设置为0
2.接着,以16bit为单位,两两相加,对于该例子,即为:E34F + 2396 + 4427 + 99F3 = 1E4FF
3.若计算结果大于0xFFFF,则将,高16位加到低16位上,对于该例子,即为0xE4FF + 0x0001 = E500
注:校验和部分很重要,如果校验和出错,会导致报文被过滤,从而使得接收方接收不到该报文。
再接着是ICMP字段,总长40Bytes。其中包括:
(1)类型Type(Type: 8 表示icmp echo request,请求回显),1Bytes;
(2)代码值(code,code: 0x00表示请求回显),1Bytes;
(3)校验和(checksum),2Bytes;
(4)Identifier(用于区分不同的PING进程),2Bytes,对于unix以及类unix操作系统来说,icmp Identifier的内容就是ping的进程号,对于windows系统来说,具体参考如下:
Microsoft Windows NT - 256
Microsoft Windows 98/98SE - 512
Microsoft Windows 2000 - 512
Microsoft Windows ME - 768
Microsoft Windows 2000 Family with SP1 - 768
既然windows系统的icmp Identifier是固定不变的,那么系统如何区别不同的Ping进程呢?实际上windows系统就不在根据Identifier来区别ping进程了,它是根据Sequence Number field来区分的。
(5)序列号(Sequence number),2Bytes,区分发送顺序,与IP Header中的标识符类似。
(6)数据段(data),32Bytes,作为icmp 请求回显或回显应答报文的话,发送数据Data的内容可以是随机的。
看完了格式内容之后,同学们可以动动手,用wireshark抓取icmp包,看看报文中各个部分的具体内容。
5. 实验环境
单片机部分:stm32+enc28j60
PC端部分:win10,串口调试助手,wireshark
其他:单片机与PC端网线直连(并保证单片机与PC在同一网段)
单片机IP: 192.168.1.8
PC端IP: 192.168.1.5
网关: 192.168.1.1
6. 实验内容
6.1 实验方案
本次实验,主要分为请求回显报文的发送和回显应答报文的接收两部分,已经知道了报文的具体内容之后,我们便可以自己构建报文内容。模仿uip协议栈的uip_buf机制,构建请求回显报文内容,往uip_buf(或者自己定义的buf变量)中填充数据,再通过enc20j60底层发送函数进行发送;对于接收回显应答报文,可以分步对其进行数据解析,最后通过串口打印ping的结果。
6.2 请求回显报文的发送
构造请求回显报文,主要有以下几个方面:
- 定义相关结构体,这些结构体中的变量是根据报文的格式内容来定义的;
- 声明相关全局变量,如报文各个部分的长度;
- 校验和函数的定义;
- 报文内容的封装;
- 对封装好的报文进行预发送处理,在预发送过程中,要判断在arp表中是否有目的ip的mac地址,如果有,则以封装好的请求回显报文进行发送;如果没有,就要构造ARP请求进行发送。
详细代码如下:
/************************ icmp ***************************************************/struct ethip_headr
{struct uip_eth_hdr ethhdr;/* IP header. */u8_t vhl,tos,len[2],ipid[2],ipoffset[2],ttl,proto;u16_t ipchksum;u16_t srcipaddr[2],destipaddr[2];
};struct arp_header
{struct uip_eth_hdr ethhdr;u16_t hwtype;u16_t protocol;u8_t hwlen;u8_t protolen;u16_t opcode;struct uip_eth_addr shwaddr;u16_t sipaddr[2];struct uip_eth_addr dhwaddr;u16_t dipaddr[2];};struct icmp_header
{u8_t type; //icmp 类型u8_t code; //代码值u16_t icmpchksum; //校验和u8_t ide[2]; //用于区分不同ping进程u8_t seq[2]; //echo 序列号char data[28]; //数据段
};/**********************************************************************/
/******************** icmp echo request ************************/
#define UIP_ICMP_BUFSIZE 200
#define ICMP_DATA_SIZE 32
#define ICMP_IPD_LLH_LEN 17 //以太网+IP
#define ICMP_ETH_LEN 14 //以太网帧头长度
#define ICMP_IPH_LEN 20 //IPHead长度
#define UIP_ICMP_LEN 40 //ICMP帧长度
u8_t uip_icmp_buf[UIP_ICMP_BUFSIZE + 2];
u16_t uip_icmp_len;#define ICMP_ARP_BUF ((struct arp_header *)&uip_icmp_buf[0]) //主动连接时,替换ICMP_IP_BUF
#define ICMP_IP_BUF ((struct ethip_headr *)&uip_icmp_buf[0])
#define ICMP_BUF ((struct icmp_header *)&uip_icmp_buf[ICMP_ETH_LEN + ICMP_IPH_LEN])/**********************************************************************/
volatile u8_t FLAG_icmp_arpout = 0;
extern u16_t chksum(u16_t sum, const u8_t *sdata, u16_t len);
static u16_t icmp_ipid;
static u16_t icmp_seq;
static u8_t j;
u16_t icmp_ide = 0;/**********************************************************************/
//iphead check
static u16_t short_checksum(u16_t sum, const u8_t *sdata, u16_t len)
{u16_t t;const u8_t *dataptr;const u8_t *last_byte;dataptr = sdata;last_byte = sdata + len - 1;while(dataptr < last_byte) { /* At least two more bytes */t = (dataptr[0] << 8) + dataptr[1];sum += t;if(sum < t) {sum++; /* carry */}dataptr += 2;}if(dataptr == last_byte) {t = (dataptr[0] << 8) + 0;sum += t;if(sum < t) {sum++; /* carry */}}/* Return sum in host byte order. */return sum;
}/***************************** icmp IP checksum *********************************/
u16_t icmp_ipchksum(void)
{u16_t sum;sum = short_checksum(0, &uip_icmp_buf[ICMP_ETH_LEN], ICMP_IPH_LEN);return (sum == 0) ? 0xffff : htons(sum);
}/***************************** icmp checksum *********************************/u16_t icmp_icmpchksum(void)
{u16_t sum;sum = short_checksum(0, &uip_icmp_buf[ICMP_ETH_LEN + ICMP_IPH_LEN], UIP_ICMP_LEN);return (sum == 0) ? 0xffff : htons(sum);
}
/*****************************************************************************//******************************************************************************
* @brief 构造ip包头 *
* @param 接收方ip:a.b.c.d *
* @retval void. *
******************************************************************************/
void icmp_iphead(u8_t a, u8_t b, u8_t c, u8_t d)
{uip_ipaddr_t ipaddr;//构造icmp IPv4 headerICMP_IP_BUF->vhl = 0x45;ICMP_IP_BUF->tos = 0;uip_icmp_len = 0x3c;ICMP_IP_BUF->len[0] = (uip_icmp_len >> 8);ICMP_IP_BUF->len[1] = (uip_icmp_len & 0xff);ICMP_IP_BUF->ipoffset[0] = ICMP_IP_BUF->ipoffset[1] = 0;++icmp_ipid;ICMP_IP_BUF->ipid[0] = icmp_ipid >> 8;ICMP_IP_BUF->ipid[1] = icmp_ipid & 0xff;ICMP_IP_BUF->ttl = UIP_TTL;ICMP_IP_BUF->proto = UIP_PROTO_ICMP; //0x01ICMP_IP_BUF->ipchksum = 0; //计算校验和之前,先将校验和清0ICMP_IP_BUF->ipchksum = ~(icmp_ipchksum());uip_ipaddr(ipaddr,a,b,c,d);uip_ipaddr_copy(ICMP_IP_BUF->destipaddr, (ipaddr));uip_ipaddr_copy(ICMP_IP_BUF->srcipaddr, uip_hostaddr);
}const char icmp_data[ICMP_DATA_SIZE] =
{0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69
};void icmp_arp(void)
{struct arp_entry *tabptr;FLAG_icmp_arpout = 0;/* 在ARP表中找到目标IP地址并构造以太网头。如果目标IP地址不在本地网络,我们使用默认路由器的IP地址。如果找不到ARP表项,我们用对IP地址的ARP请求覆盖原始IP包 *//* 首先检查目的地是否是本地广播。 */if(uip_ipaddr_cmp(ICMP_IP_BUF->destipaddr, broadcast_ipaddr)) {memcpy(ICMP_IP_BUF->ethhdr.dest.addr, broadcast_ethaddr.addr, 6); } else {/* 检查目标地址是否在本地网络上 */if(!uip_ipaddr_maskcmp(ICMP_IP_BUF->destipaddr, uip_hostaddr, uip_netmask)) {/* 目标地址不在本地网络上,因此在确定MAC地址时,我们需要使用默认路由器的IP地址而不是目标地址。 */uip_ipaddr_copy(ipaddr, uip_draddr);} else {/* 否则,我们使用目标IP地址 */uip_ipaddr_copy(ipaddr, ICMP_IP_BUF->destipaddr);}for(j = 0; j < UIP_ARPTAB_SIZE; ++j) {tabptr = &arp_table[j];if(uip_ipaddr_cmp(ipaddr, tabptr->ipaddr)) {break;}
}if(j == UIP_ARPTAB_SIZE) {/* 目的地地址不在我们的ARP表中,所以我们用ARP请求覆盖IP包。 */FLAG_icmp_arpout++ ;memset(BUF->ethhdr.dest.addr, 0xff, 6);memset(BUF->dhwaddr.addr, 0x00, 6);memcpy(BUF->ethhdr.src.addr, uip_ethaddr.addr, 6);memcpy(BUF->shwaddr.addr, uip_ethaddr.addr, 6);uip_ipaddr_copy(BUF->dipaddr, ipaddr);uip_ipaddr_copy(BUF->sipaddr, uip_hostaddr);BUF->opcode = HTONS(ARP_REQUEST); /* ARP request. */BUF->hwtype = HTONS(ARP_HWTYPE_ETH);BUF->protocol = HTONS(UIP_ETHTYPE_IP);BUF->hwlen = 6;BUF->protolen = 4;BUF->ethhdr.type = HTONS(UIP_ETHTYPE_ARP);uip_appdata = &uip_buf[UIP_TCPIP_HLEN + UIP_LLH_LEN];uip_len = sizeof(struct arp_hdr);return;}/* 构建以太网标头。 */memcpy(ICMP_IP_BUF->ethhdr.dest.addr, tabptr->ethaddr.addr, 6);}memcpy(ICMP_IP_BUF->ethhdr.src.addr, uip_ethaddr.addr, 6);ICMP_IP_BUF->ethhdr.type = HTONS(UIP_ETHTYPE_IP);uip_icmp_len += sizeof(struct uip_eth_hdr);}/******************************************************************************
* @brief 构造icmp包头 *
* @param void *
* @retval void. *
******************************************************************************/
void icmp_icmphead(void)
{//构造icmp echo request字段//icmp echo headerICMP_BUF->type = 0x08; //echo 类型ICMP_BUF->code = 0x00; //该字段用来查找错误原因icmp_ide = 0x01;ICMP_BUF->ide[0] = (icmp_ide >> 8); //区分不同的ping进程ICMP_BUF->ide[1] = (icmp_ide & 0xff);++ icmp_seq;ICMP_BUF->seq[0] = (icmp_seq >> 8); //echo 序列号ICMP_BUF->seq[1] = (icmp_seq & 0xff);memcpy(ICMP_BUF->data, icmp_data, ICMP_DATA_SIZE);ICMP_BUF->icmpchksum = 0; //校验和ICMP_BUF->icmpchksum = ~(icmp_icmpchksum());
}/******************************************************************************
* @brief icmp request报文发送 *
* @param 接收方ip:a.b.c.d *
* @retval void. *
******************************************************************************/
void icmp_out(u8_t a, u8_t b, u8_t c, u8_t d)
{printf("Ping %d.%d.%d.%d \r\n", a, b, c, d); //打印正在ping的ipicmp_iphead(a, b, c, d); //构造icmp IPv4 header icmp_arp(); //如果arp table中没有目标ip的mac地址,就要构造ARP请求,加以太网头结构if(FLAG_icmp_arpout == 0) //如果arp table中有目标ip的mac地址{icmp_icmphead(); //构造icmp报头 enc28j60PacketSend(uip_icmp_len, (uchar *)uip_icmp_buf); //发送报文}else //如果构造的是arp request包{enc28j60PacketSend(uip_len,uip_buf); //发送arp request包到以太网printf("ip:%d.%d.%d.%d no mac addr,arp request sent!\r\n", a, b, c, d);}}
6.3 回显应答报文的接收与校验
由于一些原因,接收方并不能在收到回显请求后,立即发送回显应答,而且发送方也不能在发送后,就立刻能收到,所以对于回显应答报文的接收,要设置一个轮询机制,在一定时间内反复查询是否有收到回显应答,如果收到了就进行回显应答校验,没收到就轮询直到定时器超时。
对于回显应答报文的数据校验,主要有以下几个方面:
- 通过底层的enc28j60PacketReceive函数,看是否有收到数据,如果有,在进行下一步的判断;
- 判断是否是ip包,是的话,进行下一步判断;
- 判断icmp字段的内容长度是否正确,正确的话再进行下一步判断;
- 查看icmp字段的类型,如果是icmp echo reply,再接着下一步;
- 判断报文中的目标ip是否是本机ip,如果上述条件都符合,则确认是发送给本机的icmp echo reply;
- 然后判断icmpchksum是否正确,icmp id标识符是否相同,icmp seq序列号是否相同,这三者的判断顺序无先后关系,1-5点要从1至5依次判断。
详细代码如下:
/******************** icmp echo reply ***********************/
u8_t icmp_reply_buf[UIP_ICMP_BUFSIZE + 2];
u16_t icmp_reply_len;#define ICMP_REPLY_IP_BUF ((struct ethip_headr *)&icmp_reply_buf[0])
#define ICMP_REPLY_BUF ((struct icmp_header *)&icmp_reply_buf[ICMP_ETH_LEN + ICMP_IPH_LEN])volatile u8_t flag_icmp_reply_outtimes = 0;
volatile u8_t flag_icmp_reply_checkOK = 0;
volatile u8_t flag_icmp_reply_run = 0;
extern void loop_feed_softdog(void);
/***************************** icmp REPLY IP checksum *********************************/
u16_t icmp_reply_ipchksum(void)
{u16_t sum;sum = short_checksum(0, &icmp_reply_buf[ICMP_ETH_LEN], ICMP_IPH_LEN);return (sum == 0) ? 0xffff : htons(sum);
}/***************************** icmp REPLY checksum *********************************/u16_t icmp_reply_icmpchksum(void)
{u16_t sum;sum = short_checksum(0, &icmp_reply_buf[ICMP_ETH_LEN + ICMP_IPH_LEN], UIP_ICMP_LEN);return (sum == 0) ? 0xffff : htons(sum);
}/******************************************************************************
* @brief icmp_reply_check icmp 请求应答报文中的数据校验 *
* @param void *
* @retval void. *
******************************************************************************/
void uip_icmp_reply_check(void)
{u16_t icmp_chksum = 0;u16_t new_icmpchksum = 0;//接收数据icmp_reply_len = enc28j60PacketReceive(UIP_ICMP_BUFSIZE, icmp_reply_buf);if(icmp_reply_len > 0) //有收到数据{ //处理IP数据包(只有校验通过的IP包才会被接收)if(ICMP_REPLY_IP_BUF->ethhdr.type == htons(UIP_ETHTYPE_IP)) //判断是否是IP包? {if(icmp_reply_len < sizeof(struct icmp_header)) {icmp_reply_len = 0;return;}icmp_reply_len = 0;switch(ICMP_REPLY_BUF->type) {case HTONS(0): //收到的包是icmp echo replyflag_icmp_reply_run = 1;/* 首先,判断报文中的目标ip是否是本机ip*/if(uip_ipaddr_cmp(ICMP_REPLY_IP_BUF->destipaddr, ICMP_IP_BUF->srcipaddr)){//上述条件都符合,则确认是发送给本机的icmp echo replyicmp_chksum = ICMP_REPLY_BUF->icmpchksum; //其次,判断 icmpchksum 是否正确ICMP_REPLY_BUF-> icmpchksum = 0;new_icmpchksum = ~(icmp_reply_icmpchksum());if(icmp_chksum == new_icmpchksum){flag_icmp_reply_checkOK++ ;}else{printf("reply icmp_chksum ERROR!\r\n");}if(ICMP_REPLY_BUF->ide[0] == ICMP_BUF->ide[0] && //接着判断icmp id标识符是否相同ICMP_REPLY_BUF->ide[1] == ICMP_BUF->ide[1]){flag_icmp_reply_checkOK++ ;}else{printf("icmp id is ERROR!\r\n"); }if(ICMP_REPLY_BUF->seq[0] == ICMP_BUF->seq[0] && //再接着判断 icmp seq序列号是否相同ICMP_REPLY_BUF->seq[1] == ICMP_BUF->seq[1]){flag_icmp_reply_checkOK++ ;}else{printf("icmp seq is ERROR!\r\n");}return;}break;default:break; }}}return;
}/******************************************************************************
* @brief icmp reply 报文处理结果判断 *
* @param ip 地址 a.b.c.d *
* @retval void. *
******************************************************************************/
void uip_icmp_reply_in(u8_t a, u8_t b, u8_t c, u8_t d)
{struct timer icmp_timer; //只用在icmp_reply 校验,函数运行结束释放,所以不声明为staic变量timer_set(&icmp_timer, CLOCK_SECOND / 2); //创建1个0.5秒的定时器while(1){if(FLAG_icmp_arpout > 0){break;}loop_feed_softdog(); //喂软件看门狗uip_icmp_reply_check(); //icmp reply 报文校验if(timer_expired(&icmp_timer)) //0.5s秒定时器超时{timer_reset(&icmp_timer); //复位定时器if(flag_icmp_reply_run == 0){ //没接收到icmp echo reply 报文flag_icmp_reply_outtimes ++;printf("no receive icmp echo reply!\r\n");}break;}if((flag_icmp_reply_checkOK > 0) || (flag_icmp_reply_outtimes > 0)){break;}} if(flag_icmp_reply_checkOK == 3){printf("Ping %d.%d.%d.%d is SUCCESS !!\r\n", a, b, c, d);}else{printf("Ping %d.%d.%d.%d is ERROR !!\r\n", a, b, c, d);}FLAG_icmp_arpout = 0; //计数标志清零flag_icmp_reply_run = 0;flag_icmp_reply_checkOK = 0; flag_icmp_reply_outtimes = 0;return;
}
7. 实验结果
1.依次ping5个不同的ip地址
网页输入5个ip地址(网页部分代码根据例程修改得到的,这里就不展开说明了):

图1 网页输入ip

图2 串口打印信息

图3 wireshark抓包结果
2.连续ping5次相同的ip

图4 网页输入5个相同ip

图5 串口打印信息

图6 wireshark抓包结果
8. 实验结果分析
1.依次ping5个不同的ip地址
从图2和图3来看,192.168.1.5是PC端的IP,其余IP均无具体主机。所以在发送icmp请求时,对于无具体主机的ip的icmp请求报文改写成了arp请求报文,并以广播的形式发送;对于192.168.1.5,因为未收到pc端的回复,所以发送no receive icmp reply,并返回ping 失败。
2.连续ping5次相同的ip地址
从图6的结果来看,均成功发送icmp请求,并成功返回icmp 应答。
9. 总结
1.对于嵌入式以太网的学习,一开始是从直接从例程入手,学习缓慢;随后开始在csdn等论坛查阅资料,一步一个脚印,但也走了很多弯路,有时候解决一个问题也花费了好些天,但最终靠着耐心和坚持,收获良多。
2.对实验例程和源码详细阅读很重要,可以加快实验的理解,也可以让实验代码功能更加完善。
3.对于实验中的技术出错与改错方法,这里就不一一说明了,如果你也遇到什么技术问题,可以在评论区发布,我会与大家解决。
最后,感谢大家看完这篇文章,如有不足,望大家积极指出,我也会改进,与 大家共勉!















