خب برگردیم سراغ پروژه مون؛ تا اونجا رفتیم جلو که ارتباط SPI بین میکرو و ENC رو برقرار کردیم. پینهای RST,CS تراشه ENC رو هم دادیم به دوتا از پینهای میکرو که بهصورت خروجی تعریف شده بودن. در نرمافزار STMCubeMX هم تنظیمات اولیه رو انجام دادیم و برای کامپایلر KEIL ازش خروجی گرفتیم. شما با هر میکرو و کامپایلری میتونید کار کنید، فقط این کارها رو احتمالاً باید خودتون هندل کنید و بعضی کدها رو هم بنا به نیاز تغییر بدید. برگردیم سر وقت دیتاشیت ENC28J60
اینجا بگیم که ما نمیخوایم دیتاشیت ENC رو به طور کامل ترجمه کنیم. فقط اون قسمتهایی که برامون لازمه رو توضیح میدیم و با میکرو راه میندازیم، مابقی تواناییها و قابلیتهای ENC مثل خروجی INT برای وقفه؛ میمونه به عهده خودتون که ازش استفاده بکنید یا نه!
یه اعترافی هم اینجا بکنم؛ حوالی سال 90 نیاز داشتم در پروژهای، دادههایی رو تحت پروتکل TCP برای کامپیوتر ارسال کنم. مداری که بستم و کدی که از نت برداشتم (برای میکروی STM32f107 که خودش کنترلر اترنت داره) جواب نداد و چون اطلاعاتم در مورد شبکه خیلی ناقص بود، عیبیابی هم نتونستم بکنم. مجبور شدم اون برد رو رها کنم و از ماژول آماده ایکه TCP/IP رو بدون درگیرشدن با مفاهیم شبکه به ارتباط سریال USART تبدیل میکرد؛ استفاده کنم. سالها از این ماژول استفاده میکردم تا اینکه بالاخره دوباره خواستم برگردم و خودم یه تراشه کنترلر اترنت رو راه بندازم. از اونجاییکه میخواستم بعدها بتونم کد رو روی هر میکروی دیگه ای هم پیادهسازی کنم؛ تراشه ENC رو انتخاب کردم؛ طبیعتاً کلی کد آماده برای انواع میکرو وجود داشت؛ ولی من دوست داشتم این بار اگه مشکی پیش اومد یا خواستم تغییراتی بدم، بدونم دارم چکار میکنم. اینه که با جستجو بالاخره تونستم سایتی رو پیدا کنم که همینطور که من دارم الان توضیح میدم؛ مفاهیم رو توضیح داده بود؛ البته بسیار خلاصهتر و کمحجمتر از این که میبینید (در 5 صفحه اینترنتی). بهعلاوه از دو پروتکل اصلی UDP و TCP هم فقط UDP رو کار کرده بود، ولی طریقه حل مسئله و روش آموزشش، سر نخ (getting started) خوبی بود برای من؛ لذا با همون روش سعی دارم به طور کاملتر و البته مفصلتری این مسیر رو ادامه بدم و TCP, DHCP, HTTP رو هم با همین روش کار کنم. آدرس اون سایت هست microtechnics.ru کدهای مورد نیازتون رو البته تا پروتکل UDP (بدون DHCP,TCP, HTTP) میتونید از این سایت دانلود کنید؛ همینجا هم تجربه خودم رو بگم که بعد از دانلود و کامپایل برنامه؛ تعداد زیادی خطای نحوی (Syntax Error) داشت که باید اونها رو رفع کنید. مهمترینش تا اونجایی که یادمه؛ فاصله بین اشارهگر به ساختمان، یعنی عملگر -> بود که بین – و > یه فاصله افتاده بود؛ لذا تعداد زیادی خطا ایجاد میکرد. لینک دانلود بخش اصلی نرمافزار بدون TCP, DHCP,HTTP رو در انتهای لينك زير می تونید پیدا کنید:
https://microtechnics.ru/stm32-i-ethernet-chast-5-transportnyj-uroven-protokol-udp/
خب کجا بودیم؟! ارتباط ENC رو برقرار کردیم. با کدویزارد هسته اولیه نرمافزار رو آماده کردیم و آمادهایم که ENC رو راه بندازیم و باهاش از طریق پورت شبکه به یه کامپیوتر وصل بشیم و اطلاعاتی رو ردوبدل کنیم.
ابتدا دو فایل با نامهای enc28j60.c و enc28j60.h رو به برنامه تون در محیط KEIL اضافه کنید (امیدوارم حداقل با برنامهنویسی C آشنا باشید. اگر آشنا نیستید؛ این فایل رو ببندید و اول اون رو یاد بگیرید). در هدر فایل؛ اعلانها (declaration) رو قرار میدیم و در فایل C، تعریف (definition) متغیرها و توابع رو.
طبق گفته دیتاشیت برای ارتباط SPI باید پین CS تراشه در منطق LOW قرار بگیره. اولین تابعی که مینویسیم تابعی برای کنترل پین CS تراشه ENC یه همچین چیزیه:
1 2 3 4 5 6 7 8 9 10 11 | Static void SetCS(ENC28J60_CS_State state) { HAL_Delay(1); HAL_GPIO_WritePin(ENC28J60_CS_PORT,ENC28J60_CS_PIN, (GPIO_PinState)state ); HAL_Delay(1); } |
و در فایل هدر (header) مشخص میکنیم پینهای CS,RST کجاست. همچین enum مورد نظر برای ENC28J60_CS_State رو هم اعلان میکنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #define ENC28J60_CS_PORT GPIOA #define ENC28J60_CS_PIN GPIO_PIN_4 #define ENC28J60_RESET_PORT GPIOB #define ENC28J60_RESET_PIN GPIO_PIN_0 typedef enum { CS_LOW = 0, CS_HIGH = 1, } ENC28J60_CS_State; |
توابع زیر برای ارسال یا دریافت بایت یا بایتهایی از پورت SPI تعریف میشن.
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 | Static void WriteBytes(uint8_t* data, uint16_t size) { HAL_StatusTypeDef res = HAL_SPI_Transmit(&hspi1, data, size, ENC28J60_SPI_TIMEOUT); } /*----------------------------------------------------------------------------*/ static void WriteByte(uint8_t data) { HAL_StatusTypeDef res = HAL_SPI_Transmit(&hspi1, &data, 1, ENC28J60_SPI_TIMEOUT); } /*----------------------------------------------------------------------------*/ static uint8_t ReadByte() { uint8_t txData = 0x00; uint8_t rxData = 0x00; HAL_StatusTypeDef res = HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, ENC28J60_SPI_TIMEOUT); return rxData; } |
جهت کنترل ENC و برقراری ارتباط با آن مکانیزم سادهای در نظر گرفته شده؛ بدینطریق که رجیستر (ثبات) های کنترلی در چهار بانک تقسیم شدهاند. این چهار بانک شامل رجیسترهای کنترلی در سه گروه هستند که عبارتاند از:
نامگذاری رجیسترها (Register ، ثبات) هم شامل قاعده ساده ایست. ثباتهای کنترل اترنت با حرف E ؛ رجیسترهای مرتبط با بخش MAC با حروف MA و رجیسترهای مرتبط با ارتباط MII با حروف MI مشخص شدهاند.
تصویر زیر نمایی از این رجیسترهاست.
شکل 9 ثباتهای کنترلی در ENC28J60
بنا به گفته دیتاشیت، برای دسترسی به هر رجیستر، ابتدا باید بانک موردنظر انتخاب شود. بیتهای کنترلی انتخاب بانک، در ثبات ECON1 در آدرس 0x1F قرار گرفتهاند. از طرفی چنانچه با دقت به شکل 9 نگاه کنیم متوجه میشویم که در انتهای تمام بانکها، 5 ثبات با نام مشابه قرار دارند. در واقع این آدرسها به یک ثبات ارجاع میدهند؛ درنتیجه در هنگام دسترسی به این رجیسترها، انتخاب یا تغییر بانک نیاز نیست؛ بهعنوانمثال، چنانچه قصد ارتباط با EIR در آدرس 0x1C را داشته باشیم، نیاز نیست که بانک فعلی را تغییر دهیم و تنها کافیست اطلاعات مورد نیاز را از آدرس 0x1C بخوانیم یا بنویسیم.
برای ارتباط با ENC تنها 7 نوع دستورالعمل تعریف شده است. چهار دستور برای خواندن/نوشتن رجیسترهای کنترلی و حافظه بافر؛ دو دستور برای تغییرات بیتی و یک دستور برای ریست نرم افزاری (جهت ریست سخت افزاری نیز از پین RST استفاده خواهیم کرد).
شکل 10 دستورات ارتباطی با ENC
شکل 10 دستورات ارتباطی با ENC
شکل 12 نوشتن در ثباتهای کنترلی
باتوجهبه نوع دستورها میبینیم که هر دستور از 3 بیت برای نوع دستور (Opcode) و 5 بیت که یا جهت آدرس دهی یا (با مقدار ثابت) جهت دستورات خاص؛ تشکیل شده. تنها تفاوت بین خواندن یا نوشتن در جهت بایت دوم است که یا ما برای ENC از پین MOSI ارسال خواهیم کرد یا از پین MISO از ENC خواهیم خواند.
برای اجرای عملیات خواندن/نوشتن ENC یک مدل ساده برای نرم افزارمون طراحی کردیم بهاینترتیب که از یک متغیر یک بایتی برای مشخصکردن رجیستر مد نظرمون استفاده میکنیم. ساختار این بایت در جدول زیر نشون داده شده:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
آدرس | بانک | نوع رجیستر | |||||
00=Bank 0 01=Bank 1 10=Bank 2 11=Bank 3 | 0= Ethernet 1=MAC,MII |
این مدل بهصورت زیر در هدر فایل آمده است.
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 | typedef enum { BANK_0, BANK_1, BANK_2, BANK_3, } ENC28J60_RegBank; typedef enum { ETH_REG, MAC_MII_REG, } ENC28J60_RegType; |
بعلاوه ثابتهایی که با استفاده از #define تعریف شدهاند.
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 | #define ENC28J60_REG_BANK_OFFSET 5 #define ENC28J60_REG_TYPE_OFFSET 7 #define ENC28J60_REG_BANK_MASK 0x60 #define ENC28J60_REG_TYPE_MASK 0x80 #define ENC28J60_REG_ADDR_MASK 0x1F #define ENC28J60_BANK_0_BITS (BANK_0 << ENC28J60_REG_BANK_OFFSET) #define ENC28J60_BANK_1_BITS (BANK_1 << ENC28J60_REG_BANK_OFFSET) #define ENC28J60_BANK_2_BITS (BANK_2 << ENC28J60_REG_BANK_OFFSET) #define ENC28J60_BANK_3_BITS (BANK_3 << ENC28J60_REG_BANK_OFFSET) #define ENC28J60_BANK_COMMON_BITS (BANK_0 << ENC28J60_REG_BANK_OFFSET) #define ENC28J60_COMMON_REGS_ADDR 0x1B #define ENC28J60_ETH_REG_BIT (ETH_REG << ENC28J60_REG_TYPE_OFFSET) #define ENC28J60_MAC_MII_REG_BIT (MAC_MII_REG << ENC28J60_REG_TYPE_OFFSET) |
در واقع مدل ما در هنگام دسترسی به یک ثبات یه همچین چیزی هست:
1 | Register = Type | Bank | Address |
از طرفی نیاز داریم با تعدادی از بیتها در بعضی از رجیسترها، بهصورت مستقیم ارتباط داشته باشیم که اون ها رو هم به طور ثابت در برنامه تعریف میکنیم؛ مثل:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define ECON1_TXRST_BIT (1 << 7) #define ECON1_RXRST_BIT (1 << 6) #define ECON1_DMAST_BIT (1 << 5) #define ECON1_CSUMEN_BIT (1 << 4) #define ECON1_TXRTS_BIT (1 << 3) #define ECON1_RXEN_BIT (1 << 2) #define ECON1_BSEL1_BIT (1 << 1) #define ECON1_BSEL0_BIT (1 << 0) |
خب بعد از انجام تعاریف (Define) حالا آمادهایم تعدادی تابع برای بررسی نوع رجیسترها، همچنین مشخصشدن بانک و آدرسشون بنویسیم. قبلتر گفتیم که هرگاه بخواهیم به یک ثبات دسترسی داشته باشیم (به جز بایتهای مشترک در انتهای بانکها)؛ اول باید مطمئن شیم که در بانک مورد نظر هستیم یا نه!
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 | Static ENC28J60_RegType getRegType(uint8_t reg) { ENC28J60_RegType type = (ENC28J60_RegType)((reg & ENC28J60_REG_TYPE_MASK) >> ENC28J60_REG_TYPE_OFFSET); return type; } /*----------------------------------------------------------------------------*/ static ENC28J60_RegBank getRegBank(uint8_t reg) { ENC28J60_RegBank bank = (ENC28J60_RegBank)((reg & ENC28J60_REG_BANK_MASK) >> ENC28J60_REG_BANK_OFFSET); return bank; } /*----------------------------------------------------------------------------*/ static uint8_t getRegAddr(uint8_t reg) { uint8_t addr = (reg & ENC28J60_REG_ADDR_MASK); return addr; } |
در توابعی که در ادامه پیادهسازی شدن (Implementation) و هنگام دسترسی به هر رجیستری، ابتدا بررسی میکنیم که آیا در بانک مدنظر قرار داریم یا نه؟ و اگر نیاز بود بانک رو تغییر میدیم. اگه یادتون باشه؛ گفتیم بیتهای انتخاب بانک در ثبات ECON1 قرار دارند (که خود این ثبات هم در بخش انتهایی و مشترک تمام بانکها هست؛ لذا برای دسترسی به این ثبات، دیگه لازم نیست بانک رو تغییر بدیم).
از اونجاییکه ما بهدفعات نیاز داریم بدونیم که در کدام بانک هستیم؛ بهجای اینکه هر بار محتویات ECON1 رو بخونیم؛ بانک فعلی رو در یک متغیر ذخیره میکنیم. از شکل بالا هم متوجه میشیم که مقدار اولیه بانک بعد از ریست، روی بانک صفر هست، لذا مقدار اولیه متغیرمون رو هم روی Bank0 تنظیم کردیم.
1 | Static ENC28J60_RegBank curBank = BANK_0; |
حالا یه تابع مینویسیم که بانک یک رجیستر رو چک کنه؛ اگر بانک این رجیستر متفاوت از بانک فعلی بود؛ بانک رو تغییر میدیم:
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 | static void CheckBank(uint8_t reg) { uint8_t regAddr = getRegAddr(reg); if (regAddr < ENC28J60_COMMON_REGS_ADDR) { ENC28J60_RegBank regBank = getRegBank(reg); if (curBank != regBank) { uint8_t econ1Addr = getRegAddr(ECON1); // Clear bank bits SetCS(CS_LOW); WriteCommand(BIT_FIELD_CLEAR, econ1Addr); WriteByte(ECON1_BSEL1_BIT | ECON1_BSEL0_BIT); SetCS(CS_HIGH); // Set bank bits SetCS(CS_LOW); WriteCommand(BIT_FIELD_SET, econ1Addr); WriteByte(regBank); SetCS(CS_HIGH); curBank = regBank; } } } |
این تابع مقدار یک رجیستر رو به فرمتی که تعریف کردیم دریافت میکنه؛ اول چک میکنه که این رجیستر از رجیسترهای مشترک نباشه، سپس اگر بانک فعلی از بانکی که رجیستر مدنظرمون توش هست؛ متفاوت بود با دستورات بیتی بانک رو به بانک مدنظر تغییر میده و در نهایت مقدار متغیر CurBank هم بروز میشه.
همونطور که در کد تابع ChekBank میبینید از توابعی استفاده شده که هنوز نمیدونیم چی هستن.
حالا بریم سر وقت دستورالعملها؛ یادمون هست که کلاً 7 نوع دستور بیشتر نداشتیم. مجدداً طبق روال برنامه مون؛ اونها رو با یک enum و یک آرایه تعریف میکنیم:
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 | typedef enum { READ_CONTROL_REG, READ_BUFFER_MEM, WRITE_CONTROL_REG, WRITE_BUFFER_MEM, BIT_FIELD_SET, BIT_FIELD_CLEAR, SYSTEM_RESET, COMMANDS_NUM, } ENC28J60_Command; static uint8_t commandOpCodes[COMMANDS_NUM] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x07}; |
و تعریف تابعی برای ارسال یک دستور به ENC به همراه داده مطلوب:
1 2 3 4 5 6 7 8 9 10 11 | static void WriteCommand(ENC28J60_Command command, uint8_t argData) { uint8_t data = 0; data = (commandOpCodes[command] << ENC28J60_OP_CODE_OFFSET) | argData; WriteByte(data); } |
حالا بر اساس این تابع؛ توابع اصلی رو پیادهسازی میکنیم، بهعنوانمثال تابع خواندن یک رجیستر کنترلی:
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 | static uint8_t ReadControlReg(uint8_t reg) { uint8_t data = 0; ENC28J60_RegType regType = getRegType(reg); uint8_t regAddr = getRegAddr(reg); CheckBank(reg); SetCS(CS_LOW); WriteCommand(READ_CONTROL_REG, regAddr); if (regType == MAC_MII_REG) { ReadByte(); } data = ReadByte(); SetCS(CS_HIGH); return data; } |
همونطور که مشخص هست؛ ابتدا بانک رو چک (و تنظیم) میکنیم؛ بعد دستور خواندن رجیستر صادر میشه و بعد رجیستر خونده میشه. البته از دیتاشیت میدونیم که اگر نیاز به خواندن یک رجیستر از نوع MAC/MII هست در ابتدای پروسه خواندن داده اصلی، باید یک بایت dumy (الکی) رو بخونیم! و بعد داده اصلی از ENC خارج میشه؛ تنها نکته این تابع همینه.
توابع اصلی به علاوه توابعی که برای دسترسی به رجیسترهای دوبایتی مثل EWRPRL و EWRPRH نیاز هست در ادامه اومدن. یک تابع هم برای ریست نرمافزاری ENC نوشتیم.
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 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | Static void BitFieldSet(uint8_t reg, uint8_t regData) { uint8_t regAddr = getRegAddr(reg); CheckBank(reg); SetCS(CS_LOW); WriteCommand(BIT_FIELD_SET, regAddr); WriteByte(regData); SetCS(CS_HIGH); } /*----------------------------------------------------------------------------*/ static void BitFieldClear(uint8_t reg, uint8_t regData) { uint8_t regAddr = getRegAddr(reg); CheckBank(reg); SetCS(CS_LOW); WriteCommand(BIT_FIELD_CLEAR, regAddr); WriteByte(regData); SetCS(CS_HIGH); } /*----------------------------------------------------------------------------*/ static uint8_t ReadControlReg(uint8_t reg) { uint8_t data = 0; ENC28J60_RegType regType = getRegType(reg); uint8_t regAddr = getRegAddr(reg); CheckBank(reg); SetCS(CS_LOW); WriteCommand(READ_CONTROL_REG, regAddr); if (regType == MAC_MII_REG) { ReadByte(); } data = ReadByte(); SetCS(CS_HIGH); return data; } /*----------------------------------------------------------------------------*/ static void WriteControlReg(uint8_t reg, uint8_t regData) { uint8_t regAddr = getRegAddr(reg); CheckBank(reg); SetCS(CS_LOW); WriteCommand(WRITE_CONTROL_REG, regAddr); WriteByte(regData); SetCS(CS_HIGH); } /*----------------------------------------------------------------------------*/ static void WriteControlRegPair(uint8_t reg, uint16_t regData) { WriteControlReg(reg, (uint8_t)regData); WriteControlReg(reg + 1, (uint8_t)(regData >> 8)); } /*----------------------------------------------------------------------------*/ static void WriteBufferMem(uint8_t *data, uint16_t size) { SetCS(CS_LOW); WriteCommand(WRITE_BUFFER_MEM, ENC28J60_BUF_COMMAND_ARG); WriteBytes(data, size); SetCS(CS_HIGH); } /*----------------------------------------------------------------------------*/ static void ReadBufferMem(uint8_t *data, uint16_t size) { SetCS(CS_LOW); WriteCommand(READ_BUFFER_MEM, ENC28J60_BUF_COMMAND_ARG); for (uint16_t I = 0; I < size; i++) { *data = ReadByte(); data++; } SetCS(CS_HIGH); } /*----------------------------------------------------------------------------*/ static void SystemReset() { SetCS(CS_LOW); WriteCommand(SYSTEM_RESET, ENC28J60_RESET_COMMAND_ARG); SetCS(CS_HIGH); curBank = BANK_0; HAL_Delay(100); } |
یک نکته دیگه هم اینکه به رجیسترهای بخش PHY دسترسی مستقیم (مثل بقیه رجیسترهایی که تا الان گفتیم) نداریم و طبق گفته دیتاشیت، باید یک روال طی بشه. برای نوشتن در این رجیسترها، باید آدرس رو در رجیستر MIREGADR بنویسم؛ در ادامه داده رو بهصورت دوبایتی در ثباتهای MIWRL و MIWRH مینویسم و صبر میکنیم تا نوشتن داده تموم بشه (با بررسی بیت MISTAT_BUSY_BIT از ثبات MISTAT)
نویسنده شو !
سیسوگ با افتخار فضایی برای اشتراک گذاری دانش شماست. برای ما مقاله بنویسید.