1.前言
嵌入式以太網(wǎng)開發(fā)是一個很有挑戰(zhàn)性的工作。通過幾個月的學(xué)習(xí),個人覺得大致有兩條途徑。第一條途徑,通過高級語言熟悉socket編程,例如C#或C++,熟悉bind,listen,connect,accept等函數(shù),在嵌入式系統(tǒng)中應(yīng)用 lwIP協(xié)議棧。第二種途徑,通過分析嵌入式以太網(wǎng)代碼,結(jié)合TCPIP協(xié)議棧規(guī)范逐步實踐協(xié)議棧代碼。第一種途徑效率高,開發(fā)周期短,編寫出來的代碼性能穩(wěn)定,第二種途徑花的時間長,開發(fā)出來的代碼功能不完善,但是由于緊緊結(jié)合TCPIP規(guī)范,可以了解的內(nèi)容較多,適合學(xué)習(xí)。本文通過分析和修改AVRNET源碼并移植到STM32平臺,逐步實現(xiàn)TCPIP協(xié)議棧的各個子部分,包括ETHERNET部分,ARP部分,IP部分,ICMP部分,UDP部分,TCP部分和HTTP部分。
【 STM32NET學(xué)習(xí)筆記——索引】【代碼倉庫】
本文先實現(xiàn)ethernet部分和ARP部分。
1.2 其他說明
【硬件平臺】 STM32+ENC28J60
【編譯平臺】 IAR 6.5
【IP地址】在實踐之前,需要通過ipconfig命令查看PC機的IP地址和MAC地址,AVR的IP地址設(shè)定必須和PC機在同一個網(wǎng)段中。例如 :
PC機IP:192.168.1.102
AVR IP: 192.168.1.115
【局域網(wǎng)訪問 】
如果有STM32開發(fā)板或者其他CPU的開發(fā)板的話,可以把開發(fā)板的以太網(wǎng)端口連接到路由器LAN端口,只要保證開發(fā)板的IP地址和PC機的IP地址在同一個網(wǎng)段。
【廣域網(wǎng)訪問 】
如果有固定的電信網(wǎng)IP地址的話,可以在路由器中設(shè)置靜態(tài)端口映射,把某個端口映射成局域網(wǎng)內(nèi)的IP地址和端口號。若沒有固定IP地址的話,可使用花生殼軟件虛擬一個域名。
1.3 代碼倉庫
【代碼倉庫】——CSDN Code代碼倉庫。
2.初始化
以太網(wǎng)協(xié)議棧的實現(xiàn)離不開以太網(wǎng)驅(qū)動芯片。以太網(wǎng)驅(qū)動如何實現(xiàn)請參考——ENC28J60學(xué)習(xí)筆記。TCPIP的實現(xiàn)離不開兩個基本地址,IP地址和MAC地址。在本例中通過以下代碼定義和實現(xiàn)。
struct.h頭文件中 相關(guān)定義:
[cpp] view plain copy// MAC地址結(jié)構(gòu)體
#pragma pack(1)
typedef struct _MAC_ADDR
{
BYTE byte[6];
}MAC_ADDR;
// IP地址結(jié)構(gòu)體
#pragma pack(1)
typedef struct _IP_ADDR
{
BYTE byte[4];
}IP_ADDR;
main.c函數(shù)中的初始化代碼:
[cpp] view plain copy// 初始化MAC地址
stm32_mac.byte[0] = ‘S’;
stm32_mac.byte[1] = ‘T’;
stm32_mac.byte[2] = ‘M’;
stm32_mac.byte[3] = ‘N’;
stm32_mac.byte[4] = ‘E’;
stm32_mac.byte[5] = ‘T’;
// 初始化IP地址,固定IP地址
stm32_ip.byte[0] = 192;
stm32_ip.byte[1] = 168;
stm32_ip.byte[2] = 1;
stm32_ip.byte[3] = 115;
MAC地址和IP地址均為自定義的結(jié)構(gòu)體,結(jié)構(gòu)體中為一個字節(jié)數(shù)組。嚴格來說,MAC地址不能胡亂定義,應(yīng)嚴格遵守相關(guān)規(guī)范,如果條件允許的話可以使用帶有全球唯一的MAC地址的EEPROM芯片。
3.實現(xiàn)ETHERNET
TCPIP是一系列協(xié)議的組合,其中最有名的為TCP協(xié)議和IP協(xié)議。但是千萬不要忽視最底層的協(xié)議結(jié)構(gòu)——ETHERNET。ETHERNET包括14個字節(jié),稱之為以太網(wǎng)首部,其中前六個字節(jié)為目標MAC地址,緊著的6個字節(jié)為源MAC地址,最后的兩個字節(jié)為協(xié)議類型。以太網(wǎng)的實現(xiàn)通信時必須要知道雙方的MAC地址,發(fā)送方不明確接收方的地址便通過ARP協(xié)議尋找目標MAC地址,如果依然沒有結(jié)果則可只能把該報文轉(zhuǎn)發(fā)給路由器,讓路由器處理該報文。協(xié)議類型只需關(guān)心兩種,0800的IP協(xié)議和0806的ARP協(xié)議。
ethernet.h中相關(guān)宏定義
[cpp] view plain copy// 協(xié)議類型 ARP報文
#define ETH_TYPE_ARP_V 0x0806
#define ETH_TYPE_ARP_H_V 0x08
#define ETH_TYPE_ARP_L_V 0x06
// 協(xié)議類型 以太網(wǎng)報文
#define ETH_TYPE_IP_V 0x0800
#define ETH_TYPE_IP_H_V 0x08
#define ETH_TYPE_IP_L_V 0x00
// 以太網(wǎng)報文頭部長度 14
#define ETH_HEADER_LEN 14
// 目標MAC地址
#define ETH_DST_MAC_P 0
// 源MAC地址
#define ETH_SRC_MAC_P 6
// 協(xié)議類型
#define ETH_TYPE_H_P 12
#define ETH_TYPE_L_P 13
ethernet.c中相關(guān)函數(shù)
[cpp] view plain copyvoid eth_generate_header ( BYTE *rxtx_buffer, WORD_BYTES type, BYTE *dest_mac )
{
BYTE i;
// 配置以太網(wǎng)報文 目標MAC地址和源MAC地址
for ( i=0; i《sizeof(MAC_ADDR); i++)
{
rxtx_buffer[ ETH_DST_MAC_P + i ] = dest_mac[i];
// avr_mac為全局變量
rxtx_buffer[ ETH_SRC_MAC_P + i ] = stm32_mac.byte[i];
}
// 配置協(xié)議類型 IP報文或ARP報文
rxtx_buffer[ ETH_TYPE_H_P ] = type.byte.high;
rxtx_buffer[ ETH_TYPE_L_P ] = type.byte.low;
}
eth_generate_header函數(shù)實現(xiàn)了填充以太網(wǎng)首部的功能,第一個輸入參數(shù)為發(fā)送接收緩沖區(qū)。第二個參數(shù)為IP類型,在AVRNET項目中傳入的參數(shù)不是0800的IP協(xié)議類型就是0806的ARP協(xié)議類型。第三個參數(shù)為目標MAC地址,由于本機MAC地址作為了全局變量,可以在函數(shù)內(nèi)部填充到緩沖區(qū)中。
4.實現(xiàn)ARP
為了使用最少的代碼實現(xiàn)TCPIP功能,假設(shè)通過IP發(fā)送報文時已經(jīng)確認了目標的IP地址,設(shè)備總是先被動的通過ARP先讓PC機知道其MAC地址,這樣當PC機發(fā)送UDP或者TCP報文時,在報文中已經(jīng)包含了PC機的IP地址,設(shè)備僅需從rxtx_buffer中取出PC機IP地址。ARP協(xié)議是一個找鄰居的過程,是一個廣播找MAC的過程。發(fā)出者通過廣播報文確認某個IP的MAC地址。ARP首部包括,2字節(jié)硬件類型,2字節(jié)協(xié)議類型,1字節(jié)硬件長度,1字節(jié)協(xié)議長度,2字節(jié)操作碼,6字節(jié)發(fā)送者硬件地址,4字節(jié)發(fā)送者IP地址,6字節(jié)目標硬件地址和4字節(jié)目標IP地址。
在使用ARP協(xié)議時需要注意三點:
第一,操作碼分為兩種——ARP請求和ARP響應(yīng),ARP請求的編碼為1,ARP響應(yīng)的編碼為2,先有請求后有響應(yīng)。第二,發(fā)送ARP協(xié)議請求時請求方明確對方IP地址,但是不明確對方MAC地址,所以在請求報文中MAC地址全部以0替代。第三,由于不知道對方的MAC地址,所以只能通過廣播幀發(fā)送以太網(wǎng)數(shù)據(jù),所以以太網(wǎng)首部的前6個字節(jié)被FF填充。
為了便于ARP功能的實現(xiàn),在arp.h文件中定義了以下宏定義
[cpp] view plain copy#define ARP_PACKET_LEN 28
// ARP請求
#define ARP_OPCODE_REQUEST_V 0x0001
#define ARP_OPCODE_REQUEST_H_V 0x00
#define ARP_OPCODE_REQUEST_L_V 0x01
// ARP響應(yīng)
#define ARP_OPCODE_REPLY_V 0x0002
#define ARP_OPCODE_REPLY_H_V 0x00
#define ARP_OPCODE_REPLY_L_V 0x02
// 硬件類型 10M以太網(wǎng)
#define ARP_HARDWARE_TYPE_H_V 0x00
#define ARP_HARDWARE_TYPE_L_V 0x01
// 協(xié)議類型 IPV4
#define ARP_PROTOCOL_H_V 0x08
#define ARP_PROTOCOL_L_V 0x00
// 硬件地址長度
#define ARP_HARDWARE_SIZE_V 0x06
// 協(xié)議地址長度
#define ARP_PROTOCOL_SIZE_V 0x04
// 硬件類型 2字節(jié)
#define ARP_HARDWARE_TYPE_H_P 0x0E
#define ARP_HARDWARE_TYPE_L_P 0x0F
// 協(xié)議類型 2字節(jié)
#define ARP_PROTOCOL_H_P 0x10
#define ARP_PROTOCOL_L_P 0x11
// 硬件地址 1字節(jié)
#define ARP_HARDWARE_SIZE_P 0x12
// 協(xié)議地址長度 1字節(jié)
#define ARP_PROTOCOL_SIZE_P 0x13
// 操作碼 2字節(jié)
#define ARP_OPCODE_H_P 0x14
#define ARP_OPCODE_L_P 0x15
// 發(fā)送者硬件地址 6字節(jié)
#define ARP_SRC_MAC_P 0x16
// 發(fā)送者IP地址 4字節(jié)
#define ARP_SRC_IP_P 0x1C
// 目標硬件地址 6字節(jié)
#define ARP_DST_MAC_P 0x20
// 目標IP地址 6字節(jié)
#define ARP_DST_IP_P 0x26
在沒有操作系統(tǒng)的支持下,一般通過一個無限循環(huán)實現(xiàn)子功能的實現(xiàn)。項目中通過某個process不斷查詢是否存在網(wǎng)卡數(shù)據(jù),如果有網(wǎng)卡數(shù)據(jù)則立刻保存源MAC地址。因為項目中沒有維護ARP表,所以必須及時記錄發(fā)送方的MAC地址,以便向它返回數(shù)據(jù)。緊著便是查詢該報文是否為ARP請求,如果是ARP請求則返回ARP響應(yīng)。具體代碼如下 :
[cpp] view plain copyvoid server_process ( void )
{
MAC_ADDR client_mac;
IP_ADDR client_ip;
WORD plen;
// 獲得新的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;
}
}
4.1 查詢ARP報文
查詢該報文是否是針對設(shè)備的ARP報文需要確認三點,第一:確認以太網(wǎng)首部中的協(xié)議類型是否為ARP協(xié)議類型,ARP協(xié)議類型的值為0806H。第二,查詢該ARP報文是否為ARP請求,該步驟需要到ARP首部中查詢ARP操作碼,ARP請求的操作碼為1。第三,查詢該ARP請求中的MAC地址是否和本機MAC匹配。
最后通過宏定義ARP_DEBUD決定是否通過串口輸出發(fā)起者IP地址和MAC地址。通過串口打印可以確認該ARP報文的發(fā)起者。
[cpp] view plain copyBYTE arp_packet_is_arp ( BYTE *rxtx_buffer, WORD_BYTES opcode )
{
BYTE i;
// 該報文為ARP報文
if( rxtx_buffer[ ETH_TYPE_H_P ] != ETH_TYPE_ARP_H_V || rxtx_buffer[ ETH_TYPE_L_P ] != ETH_TYPE_ARP_L_V)
return 0;
// 確認ARP操作碼 ARP請求 1 ARP應(yīng)答2
if ( rxtx_buffer[ ARP_OPCODE_H_P ] != opcode.byte.high || rxtx_buffer[ ARP_OPCODE_L_P ] != opcode.byte.low )
return 0;
// 匹配IP地址
for ( i=0; i《sizeof(IP_ADDR); i++ )
{
if ( rxtx_buffer[ ARP_DST_IP_P + i] != stm32_ip.byte[i] )
return 0;
}
// 通過串口輸出
#if ARP_DEBUG
printf(“ARP Message! ”);
printf(“Source IP:%d.%d.%d.%d ”,
rxtx_buffer[ARP_SRC_IP_P+0],rxtx_buffer[ARP_SRC_IP_P+1],
rxtx_buffer[ARP_SRC_IP_P+2],rxtx_buffer[ARP_SRC_IP_P+3]);
printf(“Source MAC:%02X-%02X-%02X-%02X-%02X-%02X ”,
rxtx_buffer[ARP_SRC_MAC_P+0],rxtx_buffer[ARP_SRC_MAC_P+1],
rxtx_buffer[ARP_SRC_MAC_P+2],rxtx_buffer[ARP_SRC_MAC_P+3],
rxtx_buffer[ARP_SRC_MAC_P+4],rxtx_buffer[ARP_SRC_MAC_P+5]);
#endif
return 1;
}
4.2 生成ARP首部
生成ARP首部還是緊緊圍繞兩個地址展開,即目標MAC地址和目標IP地址,在ARP響應(yīng)過程中,源MAC地址和IP地址現(xiàn)在轉(zhuǎn)變?yōu)榱四繕薓AC地址和IP地址。
[cpp] view plain copyvoid arp_generate_packet ( BYTE *rxtx_buffer, BYTE *dest_mac, BYTE *dest_ip )
{
unsigned char i;
// 硬件類型 0001 10M以太網(wǎng)
rxtx_buffer[ ARP_HARDWARE_TYPE_H_P ] = ARP_HARDWARE_TYPE_H_V;
rxtx_buffer[ ARP_HARDWARE_TYPE_L_P ] = ARP_HARDWARE_TYPE_L_V;
// 協(xié)議類型
rxtx_buffer[ ARP_PROTOCOL_H_P ] = ARP_PROTOCOL_H_V;
rxtx_buffer[ ARP_PROTOCOL_L_P ] = ARP_PROTOCOL_L_V;
// 硬件地址長度
rxtx_buffer[ ARP_HARDWARE_SIZE_P ] = ARP_HARDWARE_SIZE_V;
// 協(xié)議地址長度
rxtx_buffer[ ARP_PROTOCOL_SIZE_P ] = ARP_PROTOCOL_SIZE_V;
// 目標硬件地址和源硬件地址
for ( i=0; i《sizeof(MAC_ADDR); i++)
{
rxtx_buffer[ ARP_DST_MAC_P + i ] = dest_mac[i];
rxtx_buffer[ ARP_SRC_MAC_P + i ] = stm32_mac.byte[i];
}
// 目標IP地址和源IP地址
for ( i=0; i《sizeof(IP_ADDR); i++)
{
rxtx_buffer[ ARP_DST_IP_P + i ] = dest_ip[i];
rxtx_buffer[ ARP_SRC_IP_P + i ] = stm32_ip.byte[i];
}
}
4.3 響應(yīng)ARP請求
ARP響應(yīng)可以體現(xiàn)出TCP IP報文產(chǎn)生的基本過程,即層層包裝。先包裝以太網(wǎng)首部,在包裝ARP首部,最后通過ENC28J60發(fā)送即可。
[cpp] view plain copyvoid arp_send_request ( BYTE *rxtx_buffer, BYTE *dest_ip )
{
unsigned char i;
MAC_ADDR dest_mac;
// generate ethernet header
for ( i=0; i《sizeof(MAC_ADDR); i++)
dest_mac.byte[i] = 0xff;
eth_generate_header ( rxtx_buffer, (WORD_BYTES){ETH_TYPE_ARP_V}, (BYTE*)&dest_mac );
// generate arp packet
for ( i = 0 ; i 《 sizeof(MAC_ADDR) ; i++)
dest_mac.byte[i] = 0x00;
// set arp opcode is request
rxtx_buffer[ ARP_OPCODE_H_P ] = ARP_OPCODE_REQUEST_H_V;
rxtx_buffer[ ARP_OPCODE_L_P ] = ARP_OPCODE_REQUEST_L_V;
arp_generate_packet ( rxtx_buffer, (BYTE*)&dest_mac, dest_ip );
// send arp packet to network
enc28j60_packet_send ( rxtx_buffer, sizeof(ETH_HEADER) + sizeof(ARP_PACKET) );
}
5.測試
PC機通過ping命令發(fā)送一個ICMP報文,ping命令是確認網(wǎng)絡(luò)是否連接的命令,例如發(fā)送ping 192.168.1.115,由于PC機不明確該IP地址的MAC地址,所以會先發(fā)送一個ARP請求。STM32設(shè)備可捕獲該ARP請求,并通過串口輸出發(fā)送ARP請求的設(shè)備的IP地址和MAC地址。此時先不用理會是否可以ping通,因為會在以后的文章中實現(xiàn)。
在開始之前可以通過ipconfig /all指令查詢本機的IP地址和MAC地址,通過arp -a指令查詢PC機中ARP緩沖表。如果有必要可使用arp –d清除緩沖表的所有內(nèi)容。
評論