محسن نوشته

نگاهی عمیق به پشته شبکه (بخش سوم)
منتشر شده در: — Jul 28, 2020

توی دو بخش قبلی تمرکزمون بیشتر روی سخت افزار بود. اینکه ENC28J60 چیه و چرا ما دنبال شناختشیم. از اینجا به بعد از سخت افزار فاصله میگیریم و آروم آروم میریم سراغ نرم افزار. نکته مهمی که باید برای ادامه مسیر گوشه ذهنمون نگه داریم اینه که نرم افزارهایی که با هم بررسی میکنیم مبتنی بر معماری میکروکنترلرهای 8 بیتی نوشته شدن. این قید یه سری محدودیت های اساسی برای ما ایجاد میکنه، اما آگاه شدن به این محدودیت ها به ما هینت کافی میده تا آسیب پذیری های پشته شبکه را بهتر درک کنیم. نمونه کدهایی که از این به بعد برای تشریح جزئیات با هم بررسی میکنیم مبتنی بر میکروکنترلرهای AVR و خاصه ATMega128 قابل استفاده هستن. خوب مقدمه کافیه بریم که کلی چیز خوشمزه توی این پست قراره یاد بگیریم.

نکته: لازمه یه تشکر ویژه از همسرم، ثریا داشته باشم. ثریا توی مسیر ریسرچ و مطالعه همیشه پشتیبان من بوده و هر کاری که از دستش میومده برای این هدف من انجام داده. تصاویری که توی این مجموعه از پست ها میبینید را ثریا طراحی کرده و این زحمتش UX جذابی حین مطالعه برامون ایجاد میکنه. ثریا جان سپاس.

مبادله داده از طریق SPI

میدونیم ENC28J60 با استفاده از باس SPI با دنیای بیرون خودش ارتباط میگیره. یا اگه دیوایسی میخواد بهش وصل بشه باید بتونه با این باس کار کنه. کارکردن با این باس هم قواعد خودشو داره. اولین و اصلی ترین قاعده حداکثر نرخ مبادله داده روی این باس هستش. ENC28J60 نمیتونه بیشتر از 10Mbps روی باس SPI داده مبادله کنه، پس حواسمون به این محدودیت باشه. روی باس SPI پین SCK سرعت مبادله داده را مشخص میکنه. حالا ببینیم موقع ارسال و دریافت داده پینهای باس SPI چه اتفاقی باید براشون بیفته.

سناریوی اول: ارسال داده به ENC28J60

قسمت اول شکل زیر زمانبندی پالسهای ساعت و داده را روی پینهای باس SPI نمایش میده. اول از همه باید ولتاژ پین CS یا (Chip Select) به مقدار 0 ولت تنظیم بشه. چون منطق این پین معکوسه (NOT) ولتاژ 0 یعنی انتخاب! این کار به ENC28J60 اطلاع میده که MCU میخواد باهاش مبادله داده داشته باشه. بعد از این کار پالس های ساعت بار نرخ منظم مثلا 6 میلیون پالس در ثانیه روی پین SCK به ENC28J60 ارسال میشه. با اولین پالس ساعت مقدار یک بیت روی پین SI ارسال میشه و بعد از 8 پالس ساعت یک بایت داده از MCU به ENC28J60 ارسال میشه. حواسمون باشه که یک بایت از سمت پر ارزشش روی پین SI ارسال میشه. تا زمانی که داده روی SI ارسال میشه پین SO توی وضعیت High Impedance قرار داره و این یعنی برامون مهم نیست.

سناریوی دوم: دریافت داده از ENC28J60

قسمت دوم شکل زیر زمانبندی پاسهای ساعت و داده را روی پینهای باس ENC28J60 نمایش میده. مطابق سناریو اول برای مبادله داده ابتدا باید ولتاژ پین CS به 0 تنظیم بشه تا ENC28J60 برای مبادله داده روی باس SPI آماده بشه. بعد از اون تا زمانی که پالسهای ساعت روی پین SCK ارسال میشن بیت های داده روی پین SO خارج میشن. و البته حواسمون هست که وضعیت پین SI در حالت High Impedance قرار میگیره و این یعنی وضعیت این پین توی این سناریو برامون مهم نیست. وقتی 8 تا پالس ساعت روی SCK ارسال شد یک بایت داده روی SO از سمت بیت پر ارزش خارج میشه.

ENC28J60

خوب حالا درک کردیم که مبادله داده با پالس های الکتریکی و روی پین های باس SPI چطوریه. با این روش میشه جریانی از بایت ها را با ENC28J60 مبادله کرد. برای اینکه بتوانیم با اصول درست کار مشخصی از ENC28J60 درخواست کنیم، مجموعه ای دستورالعمل ENC28J60 ارائه میده و ما موظفیم بر اساس این دستورالعمل یا پروتکل ها باهاش مبادله داده داشته باشیم. این دستورالعمل ها عبارتند از:

دستورالعمل RBM: این دستور نحوه خواندن (دریافت) داده از بافر را تعریف میکنه.

دستورالعمل WBM: این دستور نحوه نوشتن (ارسال) داده به بافر را تعریف میکنه.

دستورالعمل RCR: این دستور نحوه خواندن (دریافت) داده از ثبات ها را تعریف میکنه.

دستورالعمل WCR: این دستور نحوه نوشتن (ارسال) داده به ثبات ها را تعریف میکنه.

دستورالعمل BFS: این دستور بازنشانی به مقدار 1 را برای ثبات ها تعریف میکنه.

دستورالعمل BFC: این دستور بازنشانی به مقدار 0 را برای ثبات ها تعریف میکنه.

دستورالعمل سیستمی (SYSCMD): این دستور نحوه ریستارت کردن کردن ENC28J60 را توسط MCU تعریف میکنه.


قالب بندی دستورات ENC28J60

توی این بخش میخواهیم با هم بررسی کنیم که قالب (Format) هر کدوم از دستورات ENC28J60، پالس های روی باس و کدی که این پالسها را تولید میکنه چطوریه. قبل از همه یه نگاه به شکل زیر بندازیم. توی این شکل قالب اصلی دستورات نمایش داده شده. دستورات یک یا دو بایتی هستن. دستوراتی مثل: RCR, RBM, SRC یک بایت طول دارند. اما دستوراتی مثل: WBM, WCR, BFC, BFS دو بایتی هستن. بایت اول دو بخش میشه. بخش اول که سه بیت هستش کد دستورالعمل (OPCode) و پنج باتی بعدی آدرس مقصد یا ثبات مورد نظر را حمل می کنه. چون توی هر بانک 32 تا ثبات داشتیم پس با این 5 بیت میتونیم کل ثبات ها را آدرس دهی کنیم. دستوراتی که دو بایتی هستن بایت دومشون مقداری که باید به ENC28J60 داده بشه را حمل می کنن. خوب حالا بریم سراغ تشریح دستورالعمل ها.

ENC28J60

پالس و کد مربوط به دستورالعمل RCR:

با دستور RCR میتونیم مقدار ثبات های ETH ,MAC ,MII را بخونیم. کد دستور RCR مقدار ‍‍‍‍000 هستش و وقتی میخواهیم ثباتی از گروه ETH را بخونیم، ابتدا روی پین SI کد دستور و بعد آدرس ثبات مورد نظر را ارسال میکنیم. بعد خاتمه ارسال 8 تا پالس ساعت (پالس های روی پین SCK) لازم داریم تا محتوای ثبات مورد نظر را روی پین SO دریافت کنیم.

ENC28J60

نکته اینجاست که برای خوندن ثبات های گروه MAC و MII یه مقدار باید متفاوت تر عمل کنیم. شکل زیر این تفاوت را نمایش میده.

ENC28J60

اینجا به جای 8 تا پالس ساعت به 16 تا نیاز داریم. وقتی 16 تا پالس میفرستیم قاعدتا باید 16 تا بیت یا دو تا بایت روی SO دریافت کنیم. از این دو بایت، بایت اول بیهوده است و بدرد ما نمیخوره (Dummy Byte) اما بایت دوم دقیقا مقدار موجود توی ثبات مورد نظر هستش.

همه داستانی که تا اینجا داشتیم توی قطعه کد زیر خلاصه شده. توضیحات کافی روی کد گذاشتم و فکر میکنم کفایت میکنه. چیزی که مهمه اینه که ما یه تابع داریم که کد دستورالعمل و آدرس ثبات را میگیره و مقدار ثبات را برمیگردونه! به همین راحتی! حواسمون باشه که اون IF هم گروهی که آدرس بهش تعلق داره را چک میکنه.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
u08 enc28j60ReadOp(u08 op, u08 address){
	u08 data;  
	// assert CS
	CS = 0;
	// issue read command
	SPDR = op | (address & ADDR_MASK);
	// Wait for command transmission
	while(!(SPSR & (1<<SPIF)));
	//read data
	SPDR = 0x00;
	while(!(SPSR & (1<<SPIF)));
	//do dummy read if needed
	if(address & 0x80){
		SPDR = 0x00;
		while(!(inb(SPSR) & (1<<SPIF)));
	}
	data = SPDR;
	//release CS
	CS = 1;
	return data;
}

کد مربوط به دستورالعمل RBM:

با دستور RBM میتونیم محتوای بافر اترنت را بخونیم. یادمونه ثبات ERDPT به آخرین بایتی اشاره میکنه که میخواهیم مقدارشو بخونیم. از طرفی بیت AUTOINC از ثبات ECON2 بعد از ریست شدن ENC28J60 مقدار پیشفرض 1 داره. بنابراین هر بار که با این دستورالعمل بایتی که ERDPT بهش اشاره میکنه را میخونیم یک واحد به مقدار ERDPT اضافه میشه و دفعه دفعه بعد که این دستورالعمل را اجرا کنیم بایت بعدی را میتونیم بخونیم. نکته ای که باید یادمون بمونه اینه که بافر دریافت فریم های اترنت یه بافر FIFO چرخشی هستش و وقتی ERDPT به انتهای بافر رسید ENC28J60 خودکار اونو با آدرس ابتدای بافر بروزرسانی میکنه. قطعه کد زیر پیاده سازی این داستان را نمایش میده.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void enc28j60ReadBuffer(u16 len, u08* data){
	// assert CS
	CS = 0;
	// issue read command
	SPDR = 0b0010000 | 0x1A;
	while(!(SPSR & (1<<SPIF)));
	while(len--){
		SPDR = 0x00;			 
		while(!(SPSR & (1<<SPIF)));
		*data++ = SPDR;		 
	}
	// release CS
	CS = 1;
}

پالس و کد مربوط به دستورالعمل WCR:

با این دستورالعمل ما میتونیم توی ثباتهای گروه: ETH ,MAC ,PHY داده بنویسیم. کد این دستورالعمل ‍‍010 هستش. اگه توضیحات بالا را دیده باشید براحتی متوجه میشید که شکل زیر داره چیکار میکنه.

ENC28J60

روی پین SI بترتیب: کد دستور آدرس ثبات و مقدار مورد نظر با 16 تا پالس ساعت ارسال میشه. توی این 16 تا پالس روی SO چیزی بدردبخوری دریافت نمیکنیم و هرچی گرفتیم دور میریزیم! یادتونه توی پست قبلی راجع به نوشتن توی ثبات های PHY توضیح دادم. با استناد به اون توضیح توابعی که توی قطعه کد های زیر اومدن پیاده سازی دستورالعمل WCR را نمایش میدن.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void enc28j60ReadBuffer(u16 len, u08* data){
	// assert CS
	CS = 0;
	// issue read command
	SPDR = 0b0010000 | 0x1A;
	while(!(SPSR & (1<<SPIF)));
	while(len--){
		SPDR = 0x00;			 
		while(!(SPSR & (1<<SPIF)));
		*data++ = SPDR;		 
	}
	// release CS
	CS = 1;
}

1
2
3
4
5
6
7
8
9
void enc28j60PhyWrite(u08 address, u16 data){
	//set the PHY register address
	enc28j60Write(MIREGADR, address);
	//write the PHY data
	enc28j60Write(MIWRL, data);	
	enc28j60Write(MIWRH, data>>8);
	//wait until the PHY write completes
	while(enc28j60Read(MISTAT) & MISTAT_BUSY);
}

پالس و کد مربوط به دستورالعمل WBM:

با این دستور هم میتونیم داده توی بافر اترنت بنویسیم. مثلا فریم های اترنتی که باید ارسال بشن با همین دستور به ENC28J60 تحویل داده میشه. اشاره گر بافر اترنت EWEPT هستش و وقتی بیت AUTOINC از ثبات ECON2 مقدار 1 داشته باشه، با هربار فراخوانی این دستورالعمل مقدار این اشاره گر یک واحد افزایش پیدا میکنه. حواسمون باشه که MCU باید ابتدا این اشاره گر را تنظیم کنه و بعد شروع به نوشتن کنه. شکل زیر مبادله پالس های الکتریکی روی پین های باس SPI را نمایش میده.

ENC28J60

قطعه کد زیر هم داستان این دستورالعمل را پیاده سازی میکنه. آرگومان های ورودی تابع تعداد بایتها و اشاره گر به اولین بایت مورد نظر هستن. کامنت ها هم که شفاف!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void enc28j60WriteBuffer(u16 len, u08* data){
	//assert CS
	PORTD |= (1<<PD7);
	ENC28J60_CONTROL_PORT &= ~(1<<ENC28J60_CONTROL_CS);
	//issue write command
	SPDR = ENC28J60_WRITE_BUF_MEM;
	while(!(SPSR & (1<<SPIF)));
	while(len--)	{
		//write data
		SPDR = *data++;
		while(!(SPSR & (1<<SPIF)));
	}	
	//release CS
	PORTD &= ~(1<<PD7);
	ENC28J60_CONTROL_PORT |= (1<<ENC28J60_CONTROL_CS);
}

دستورالعمل BFS:

این دستور کمکمون میکنه که یک یا چند بیت از یک ثبات گروه ETH را ‍1 کنیم. ماجراش اینجوریه که ما آدرس و یک بایت داده به ENC28J60 میدیم. ENC28J60 میره محتوای جاری ثباتی که آدرشسو دادیم با داده ای که گرفته OR میکنه و توی همون ثبات ذخیره میکنه. این دستور رو میشه با تابع enc28j60WriteOp مشابه کد زیر پیاده سازی کرد.

1
enc28j60WriteOp (ENC28J60-BIT-FIELD-SET, u08 address, u08 data);

دستورالعمل BFC:

دقیقا مشابه BFS فقط اینجا NOTAND میشه. این دستور رو میشه با تابع enc28j60WriteOp مشابه کد زیر پیاده سازی کرد.

1
enc28j60WriteOp (ENC28J60-BIT-FIEID-CLR, u08 address, u08 data);

دستورالعمل SYS-CMD:

با این دستور MCU میتونه ENC28J60 را ریست کنه. کد این دستور 111 هستش و دیتا دستور هم 11111 یا به عبارتی دیگه اگه 8 بیت یک متوالی به ENC28J60 بدیم میره خودشو ریست میکنه. این دستور رو میشه با تابع enc28j60WriteOp مشابه کد زیر پیاده سازی کرد.

1
enc28j60WriteOp (ENC28J60_SOFT_RESET, 0, ENC28J60_SOFT_RESET);

انتخاب بانک ثبات ها:

شکل زیر اطلاعات کافی درباره بیت های ثبات ECON1 نمایش میده. دو بیت کم ارزش این ثبات برای انتخاب بانک جاری استفاده میشه.

ENC28J60

کد زیر بهمون نشون میده چطور میشه از توابع بالا برای تنظیم بانک جاری استفاده کنیم.

1
2
3
4
5
6
7
8
9
void enc28j60SetBank(u08 address){
    //set the bank (if needed)
    if((address & BANK_MASK) != Enc28j60Bank){
        //set the bank
        enc28j60WriteOp(ENC28J60_BIT_FIELD_CLR, ECON1, (ECON1_BSEL1|ECON1_BSEL0));
        enc28j60WriteOp(ENC28J60_BIT_FIELD_SET, ECON1, (address & BANK_MASK)>>5);
        Enc28j60Bank = (address & BANK_MASK);
    }
}

نقشه ثبات ها:

اینجا میخواهیم یه درد اساسی رو درمان کنیم. به یاد سپردن این همه آدرس ثبات از یک طرف سخته و از یک طرف خیلی خطا خیزه! (Error Prone). برای این کار میتونیم یه توافق کنیم. بیاییم فضای بک بایت را پارتیشن بندی کنیم. چیزی مشابه شکل زیر.

ENC28J60

پنج بیت برای آدرس ثبات، دو بیت برای شماره بانک و یک بیت برای گروه ثبات در نظر بگیریم. با این تفاسیر تمام آدرس های بانک اول با 0X00، آدرس های بانک دوم با 0X10ُ، بانک سوم با 0X40 و بانک چهارم با 0X60 شروع میشن. شکل زیر نقشه پیشنهادی را نماشی میده و میتونیم توی یه ‍‍فایل h. بنویسیمش و راحت توی برنومه ازش استفاده کنیم. حواسمون باشه که توی کد های بالا از این نقشه استفاده شده بود (Macro).

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Bank 0 registers
#define ERDPTL           (0x00|0x00)
#define ERDPTH           (0x01|0x00)
#define EWRPTL           (0x02|0x00)
#define EWRPTH           (0x03|0x00)
#define ETXSTL           (0x04|0x00)
#define ETXSTH           (0x05|0x00)
#define ETXNDL           (0x06|0x00)
#define ETXNDH           (0x07|0x00)
#define ERXSTL           (0x08|0x00)
#define ERXSTH           (0x09|0x00)
#define ERXNDL           (0x0A|0x00)
#define ERXNDH           (0x0B|0x00)
#define ERXRDPTL         (0x0C|0x00)
#define ERXRDPTH         (0x0D|0x00)
#define ERXWRPTL         (0x0E|0x00)
#define ERXWRPTH         (0x0F|0x00)
#define EDMASTL          (0x10|0x00)
#define EDMASTH          (0x11|0x00)
#define EDMANDL          (0x12|0x00)
#define EDMANDH          (0x13|0x00)
#define EDMADSTL         (0x14|0x00)
#define EDMADSTH         (0x15|0x00)
#define EDMACSL          (0x16|0x00)
#define EDMACSH          (0x17|0x00)
// Bank 1 registers
#define EHT0             (0x00|0x20)
#define EHT1             (0x01|0x20)
#define EHT2             (0x02|0x20)
#define EHT3             (0x03|0x20)
#define EHT4             (0x04|0x20)
#define EHT5             (0x05|0x20)
#define EHT6             (0x06|0x20)
#define EHT7             (0x07|0x20)
#define EPMM0            (0x08|0x20)
#define EPMM1            (0x09|0x20)
#define EPMM2            (0x0A|0x20)
#define EPMM3            (0x0B|0x20)
#define EPMM4            (0x0C|0x20)
#define EPMM5            (0x0D|0x20)
#define EPMM6            (0x0E|0x20)
#define EPMM7            (0x0F|0x20)
#define EPMCSL           (0x10|0x20)
#define EPMCSH           (0x11|0x20)
#define EPMOL            (0x14|0x20)
#define EPMOH            (0x15|0x20)
#define EWOLIE           (0x16|0x20)
#define EWOLIR           (0x17|0x20)
#define ERXFCON          (0x18|0x20)
#define EPKTCNT          (0x19|0x20)
// Bank 2 registers
#define MACON1           (0x00|0x40|0x80)
#define MACON2           (0x01|0x40|0x80)
#define MACON3           (0x02|0x40|0x80)
#define MACON4           (0x03|0x40|0x80)
#define MABBIPG          (0x04|0x40|0x80)
#define MAIPGL           (0x06|0x40|0x80)
#define MAIPGH           (0x07|0x40|0x80)
#define MACLCON1         (0x08|0x40|0x80)
#define MACLCON2         (0x09|0x40|0x80)
#define MAMXFLL          (0x0A|0x40|0x80)
#define MAMXFLH          (0x0B|0x40|0x80)
#define MAPHSUP          (0x0D|0x40|0x80)
#define MICON            (0x11|0x40|0x80)
#define MICMD            (0x12|0x40|0x80)
#define MIREGADR         (0x14|0x40|0x80)
#define MIWRL            (0x16|0x40|0x80)
#define MIWRH            (0x17|0x40|0x80)
#define MIRDL            (0x18|0x40|0x80)
#define MIRDH            (0x19|0x40|0x80)
// Bank 3 registers
#define MAADR1           (0x00|0x60|0x80)
#define MAADR0           (0x01|0x60|0x80)
#define MAADR3           (0x02|0x60|0x80)
#define MAADR2           (0x03|0x60|0x80)
#define MAADR5           (0x04|0x60|0x80)
#define MAADR4           (0x05|0x60|0x80)
#define EBSTSD           (0x06|0x60)
#define EBSTCON          (0x07|0x60)
#define EBSTCSL          (0x08|0x60)
#define EBSTCSH          (0x09|0x60)
#define MISTAT           (0x0A|0x60|0x80)
#define EREVID           (0x12|0x60)
#define ECOCON           (0x15|0x60)
#define EFLOCON          (0x17|0x60)
#define EPAUSL           (0x18|0x60)
#define EPAUSH           (0x19|0x60)

جمع بندی:

حقیقتا کارسنگینی تا حالا انجام دادیم. من برای درک همه اینها چند سال وقت گذاشتم و خوشحال میشم کمکی کرده باشم تا راه یه نفر دیگه کوتاه شه. توی این پست شروع کردیم به کد زنی و بیشتر با کد و ثبات ها کار کردیم. توی پست بعدی یکم شبکه داریم و آماده میشیم تا بریم سراغ مبادله فریم های اترنت.

پیروز باشید.