STM32+enc28j60+uip 实现单片机 ping PC端
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