Tiếp tục chuỗi bài về giao thức arp, hôm nay chúng ta sẽ hoàn thành nốt chương trình gửi request, nhận reponse và tạo table để lưu các địa chỉ MAC của thiết bị trong mạng
Nếu chưa đọc bài trước các bạn có thể theo dõi tại đây
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
void ARP_send_request(uint8_t *ip_dest) { struct { uint8_t MAC_dich[6]; // MAC dich uint8_t MAC_nguon[6]; //MAC nguon uint16_t Ethernet_type; //ethernet type uint16_t Hardwave_type; //hardwave type uint16_t Protocol_type; //protocol type (ARP) uint16_t Size; //size uint16_t Opcode; //opcode uint8_t MAC_sender[6]; //sender MAC uint8_t IP_sender[4]; //sender IP uint8_t MAC_target[6]; // Target MAC uint8_t IP_target[4]; // Target IP }ARP_request; uint8_t MAC_dest[6]={0xFF,0xFF,0xFF,0xFF,0xFF,0xFF}; uint8_t MAC_target[6]={0x00,0x00,0x00,0x00,0x00,0x00}; memcpy(ARP_request.MAC_dich,MAC_dest,6); memcpy(ARP_request.MAC_target,MAC_target,6); memcpy(ARP_request.IP_target,ip_dest,4); memcpy(ARP_request.MAC_nguon,macaddr,6); memcpy(ARP_request.MAC_sender,macaddr,6); memcpy(ARP_request.IP_sender,ip,4); ARP_request.Ethernet_type=0x0608; ARP_request.Hardwave_type=0x0100; ARP_request.Protocol_type=0x0008; ARP_request.Size=0x0406; ARP_request.Opcode=0x0100; //request UART_putString("Gui gon tin arp request\r\n"); NET_SendFrame((uint8_t *)&ARP_request,42); //gui goi ARP reponse di } |
Hàm ARP request tương tự như reponse thôi, có opcode là khác. Gọi nó liên tục trong while(1) của hàm main để test thử xem đã gửi được đi chưa nhé !
1 2 3 |
//NET_loop(); ARP_send_request(ip_dest); delay_ms(500); |
Tuyệt vời, gói tin đã được gửi đi và wireshark đã bắt được các bản tin arp request
Bây giờ chúng ta sẽ tiếp tục viết mã đọc gói tin request trong hàm ARP_read_packet
- Kiểm tra opcode, nếu đúng bằng 0x0002 thì đây là bản tin request
- Kiểm tra target ip xem bản này có đúng là gửi cho mình không
- Lấy MAC và ip của thiết bị đã gửi reponse và lưu lại vào bảng lưu trữ
Mình sẽ khởi tạo 1 table để lưu trữ ip và mac. Do bộ nhớ chip có hạn nên mình sẽ tạo bảng lưu với tối đa 5 địa chỉ MAC !
1 2 3 4 5 6 |
struct { uint8_t ip[4]; uint8_t mac[6]; }ARP_table[5]; uint8_t ARP_table_index=0; |
Viết thêm 1 số hàm để truy xuất bảng
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int8_t ARP_table_checkIP(uint8_t *ip_check) //kiem tra xem co ton tai ip trong bang chua { int i; for(i=0;i<5;i++) { if(ip_check[0] == ARP_table[i].ip[0] && ip_check[1] == ARP_table[i].ip[1] && ip_check[2] == ARP_table[i].ip[2] && ip_check[3] == ARP_table[i].ip[3] ) return i+1; //tra ve vi tri cua ip trong table } return -1; } void ARP_table_setIP(uint8_t *ip_set, uint8_t *mac_set) { if(ARP_table_checkIP(ip_set) == -1) //neu chua ton tai IP trong table { sprintf(debug_string,"Da luu ip %i.%i.%i.%i = %02X:%02X:%02X:%02X:%02X:%02X\r\n",ip_set[0],ip_set[1],ip_set[2],ip_set[3],mac_set[0],mac_set[1],mac_set[2],mac_set[3],mac_set[4],mac_set[5]); UART_putString(debug_string); memcpy(ARP_table[ARP_table_index].ip,ip_set,4); memcpy(ARP_table[ARP_table_index].mac,mac_set,6); ARP_table_index++;if(ARP_table_index==5)ARP_table_index=0; //neu so luong ip vuot qua muc cho phep thi quay lai } else UART_putString("IP da ton tai trong bang\r\n"); } |
Trong hàm ARP_read_packet
1 2 3 4 5 |
else if(ARP_Buff[20] == 0x00 && ARP_Buff[21] == 0x02) //neu Opcode = 0x0001 thi day la ban tin ARP reponse { UART_putString("Da nhan 1 arp reponse\r\n"); ARP_table_setIP(IP_source,MAC_source); } |
Do cả ARP request hay ARP reponse đều cần kiểm tra IP đích xem có phải là của mình không nên mình sẽ bỏ nó lên đầu hàm luôn
1 2 |
//kiem tra xem co trung voi IP cua minh khong if(IP_target[0] != ip[0] || IP_target[1] != ip[1] || IP_target[2] != ip[2] || IP_target[3] != ip[3] ) return; |
Bây giờ mình sẽ cho gửi các bản tin ARP_request đến cho máy tính để xem đã nhận được gói tin phản hồi và lưu vào bảng chưa nhé ( các bạn vào cmd -> gõ ipconfig để lấy ip của máy tính, như máy tính mình là 192.168.1.15)
Mình đã nhận thành công MAC của máy tính có IP 192.168.1.15
Phần mềm WireShark cũng đã bắt được toàn bộ gói tin gửi nhận
Nhận các gói tin bằng ngắt
ENC28J60 hỗ trợ chân ngắt để báo có gói tin mới, khi có 1 gói tin mới chân ngắt sẽ kéo xuống mức 0,mình sẽ tận dụng điều này để nhận gói ttin cho thuận tiện thay vì hỏi vòng ở trong while(1), mình sẽ nối chân ngắt này với ngắt ngoài 1 của vi điều khiển ATmega328
Các bạn viết thêm hàm khởi tạo ngắt ngoài 1 trong file main
1 2 3 4 5 6 7 |
void INT1_init() { DDRD.3=0;// input mode EICRA=0x08; //INT1 Mode: Falling Edge EIMSK=0x02; EIFR=0x02; //kich hoat ngat chan int1 } |
Nhớ gọi nó trong hàm main để khởi tạo ngắt nhé
Chúng ta sẽ gọi hàm NET_loop trong ngắt
1 2 3 4 5 |
// External Interrupt 1 service routine interrupt [EXT_INT1] void ext_int1_isr(void) { NET_loop(); } |
Mọi thứ vẫn hoạt động trơn tru
Tới lúc này các bạn có thể xóa các dòng UART_putString ở các file arp.c net.c enc28j60.c đi để tránh in debug nhiều rối mắt !
Vấn đề về thời gian tồn tại các bảng lưu trữ arp
Trong mạng LAN, IP của các thiết bị không cố định mà thường hay thay đổi, do đó, mình sẽ xóa các bảng lưu thường xuyên để tránh các ip lỗi thời. Thời gian này có thể từ 1 phút đến vài phút hoặc vài giờ. Mình sẽ sử dụng timer1 để tạo ra 1 bộ hẹn giờ xóa bảng lưu trong vòng 10 phút
Trước tiên mình tạo thêm hàm phục vụ clear bảng ở trong thư viện arp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uint32_t ARP_clear_time; void ARP_clear_table(void) { uint8_t *end; uint8_t *start; if(++ARP_clear_time > COUNT_TICK) { ARP_clear_time=0; //xoa bang start = (uint8_t *)&ARP_table; end = start + sizeof(ARP_table); for(;start<end;start++) *start=0; } } |
Thêm định nghĩa thời gian xóa vào file .h
1 2 3 4 5 |
//dinh nghia thoi gian xoa bang arp #define MAX_TIME_SAVE 10 //(phut) //------------------------------------------------- #define COUNT_TICK (MAX_TIME_SAVE*60000) //------------------------------------------------- |
Trong file main, mình tạo file khởi tạo time1
1 2 3 4 5 6 7 |
uint32_t sys_tick; void TIMER1_init_1ms() { sys_tick=0; TCCR1B=0x02; // timer 1 2Mhz TIMSK1=0x01; // Timer/Counter 1 Interrupt(s) initialization } |
Và gọi hàm xóa bảng trong ngắt
1 2 3 4 5 6 7 |
interrupt [TIM1_OVF] void timer1_ovf_isr(void) // Timer1 overflow interrupt service routine { TCNT1H=0xF8; TCNT1L=0x2D; sys_tick++; ARP_clear_table(); } |
Kết luận
Như vậy chúng ta đã hoàn thành chương trình gửi nhận các gói tin ARP, ARP là giao đầu tiên chúng ta cần phải hoàn thành vì có nó chúng ta mới sử dụng tiếp các giao thức trong bộ giao thức IP được
Các bạn tải source cho bài này tại đây
Trong phần tiếp của chuỗi tutorila tự học lập trình enc28j60, chúng ta sẽ làm quen với giao thức ICMP và học cách trả lời lệnh ping