STM32+enc28j60+uip 实现 单片机 ping PC端
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 請求回顯報文的發送
構造請求回顯報文,主要有以下幾個方面:
詳細代碼如下:
/************************ 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 回顯應答報文的接收與校驗
由于一些原因,接收方并不能在收到回顯請求后,立即發送回顯應答,而且發送方也不能在發送后,就立刻能收到,所以對于回顯應答報文的接收,要設置一個輪詢機制,在一定時間內反復查詢是否有收到回顯應答,如果收到了就進行回顯應答校驗,沒收到就輪詢直到定時器超時。
對于回顯應答報文的數據校驗,主要有以下幾個方面:
詳細代碼如下:
/******************** 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.對于實驗中的技術出錯與改錯方法,這里就不一一說明了,如果你也遇到什么技術問題,可以在評論區發布,我會與大家解決。
最后,感謝大家看完這篇文章,如有不足,望大家積極指出,我也會改進,與 大家共勉!
總結
以上是生活随笔為你收集整理的STM32+enc28j60+uip 实现 单片机 ping PC端的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【HTML CSS】笔记4日 [ 学成在
- 下一篇: 宝宝点点滴滴