در آموزشهای قبلی در مورد پروتکلهای ارتباط سریال صحبت کردیم. در این قسمت میخواهیم در مورد یک پروتکل ارتباط سریال دیگر، یعنی Inter-Integrated Circuit یا همان I2C صحبت کنیم. این پروتکل، همانطور که از نام آن مشخص است، برای ارتباط چیپهای مختلف بهکار میرود. در I2C تنها از دو سیم برای ارتباط استفاده میشود و نسبت به USART سرعت بالاتری دارد (در بعضی IC ها تا 3.4 Mbit/s) اما از پیچیدگیهای راهاندازی بیشتری نیز برخوردار است و برای مسافتهای خیلی کوتاه مناسب است. در ادامه با جزییات این ارتباط و نحوه راهاندازی آن بیشتر آشنا میشویم.
باما همراه باشید.
شکل بالا ارتباط I2C بین یک میکروکنترلر و سه دستگاه دیگر را نشان میدهد. همانطور که دیده میشود، دراینارتباط از یک سیم برای انتقال کلاک (SCL) و یک سیم برای انتقال اطلاعات داده (SDA) استفاده میشود. در این شکل میکروکنترلر وظیفه کنترل ارتباط را به عهده دارد و نقش Master را ایفا میکند. به همین دلیل کلاک نیز توسط میکرو تأمین میشود. هرچند در این شکل تنها یک Master وجود دارد، اما بیش از یک Master نیز میتواند وجود داشته باشد. بااینحال در هر زمان تنها یک دستگاه کنترل ارتباط را در دست میگیرد.
منظور از کنترل ارتباط، انتخاب Slave و همچنین جهت انتقال داده است. همانطور که در شکل دیده میشود، برای خط SDA از یک مقاومت Pull-up استفاده شده است. درصورتیکه بیش از Master وجود داشته باشد و یا از Clock stretching استفاده شود، در خط SCL نیز از مقاومت Pull-up استفاده میکنیم.
برای شروع ارتباط و تبادل داده، دستگاه Master باید شرط شروع ارتباط را ایجاد کند، سپس آدرس دستگاه موردنظر برای تبادل اطلاعات و همچنین جهت تبادل داده (خواندن یا نوشتن) را بفرستد. پسازاینکه دستگاه Slave بیت تأیید یا ACK را فرستاد. فرستادن یا دریافت اطلاعات شروع میشود. برای درک بهتر به شکل زیر توجه کنید:
در ادامه به نحوه راهاندازی این ارتباط در میان Blue Pill و یک حافظه EEPROM میپردازیم.
ایجاد پروژه
در محیط Cube MX مثل گذشته تنظیمات کلاک، دیباگ و همچنین واحد USART1 (برای دیدن نتایج اجرا) را انجام میدهیم. سپس واحد I2C1 را مانند شکل زیر تنظیم میکنیم:
بعد تنظیمات درایورها بر روی LL کد پروژه را ایجاد میکنیم.
نوشتن کد پروژه
در ابتدا کتابخانهها و ثابتهای مورد نیاز را اضافه میکنیم؛
#include "stdbool.h" #include "stdio.h"
#define X_NOP() {__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();} #define Y_NOP() X_NOP();X_NOP();X_NOP();X_NOP();X_NOP();X_NOP();X_NOP();X_NOP();X_NOP();X_NOP(); #define Z_NOP() Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP();Y_NOP(); #define Z2_NOP() Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP();Z_NOP(); #define I2C_REQUEST_WRITE 0x00 #define I2C_REQUEST_READ 0x01 #define I2C_Slave_Adr (0xA0)
سپس مثل قبل توابع مربوط به ریدایرکت Printf را در این پروژه نیز کپی میکنیم و تغییرات لازم را انجام میدهیم. حالا باید توابع موردنیاز برای خواندن و نوشتن توسط I2C را بنویسیم؛
/* Function for transmitting 8bit data via I2C */ void write_i2c(uint8_t Data) { LL_I2C_TransmitData8(I2C1, Data); while(!LL_I2C_IsActiveFlag_TXE(I2C1)); }
/* Function for receiving 8bit data via I2C */ uint8_t read_i2c(bool IsAck) { if(!IsAck) LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_NACK); else LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK); uint8_t Data; while(!LL_I2C_IsActiveFlag_RXNE(I2C1)); Data = LL_I2C_ReceiveData8(I2C1); return Data; }
اکنون میتوانیم با استفاده از این توابع، دو تابع جدید برای نوشتن روی حافظه EEPROM و خواندن از آن، بنویسیم. برای این عمل نیاز به دیتاشیت حافظهداریم. زیرا باید از نحوه آدرسدهی، تأخیر نوشتن و جزییات دیگر مربوط به حافظه اطلاع داشته باشیم. ما از یک حافظه 265kb با تأخیر نوشتن 5ms استفاده میکنیم.
کدهای زیر را قبل از int main مینویسیم؛
bool E2Prom_Write(uint16_t adr,uint8_t data) { LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK); LL_I2C_GenerateStartCondition(I2C1); Z_NOP(); LL_I2C_TransmitData8(I2C1, I2C_Slave_Adr | I2C_REQUEST_WRITE); // Set Address of the slave, Enable Write mode while(!LL_I2C_IsActiveFlag_ADDR(I2C1)) {}; // Loop until ADDR flag is raised LL_I2C_ClearFlag_ADDR(I2C1); // Clear ADDR flag value in ISR register /// memory address write_i2c(adr>>8); write_i2c(adr&0xFF); write_i2c(data); LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_NACK); while(!LL_I2C_IsActiveFlag_TXE(I2C1)); LL_I2C_GenerateStopCondition(I2C1); return true; }
/* Function for reading 8bit data from the specified address in EEPROM via I2C */ uint8_t E2Prom_Read(uint16_t adr) { uint8_t data; LL_I2C_GenerateStartCondition(I2C1); Z_NOP(); LL_I2C_TransmitData8(I2C1, I2C_Slave_Adr | I2C_REQUEST_WRITE); // Set Address of the slave, Enable Write mode while(!LL_I2C_IsActiveFlag_ADDR(I2C1)) {}; // Loop until ADDR flag is raised LL_I2C_ClearFlag_ADDR(I2C1); // Clear ADDR flag value in ISR register /// memory address write_i2c(adr>>8); write_i2c(adr&0xFF); LL_I2C_GenerateStartCondition(I2C1); Z2_NOP(); LL_I2C_TransmitData8(I2C1, I2C_Slave_Adr | I2C_REQUEST_READ); // Set Address of the slave, Enable Write mode while(!LL_I2C_IsActiveFlag_ADDR(I2C1)) {}; // Loop until ADDR flag is raised LL_I2C_ClearFlag_ADDR(I2C1); // Clear ADDR flag value in ISR register data = read_i2c(false); LL_I2C_GenerateStopCondition(I2C1); return data; }
اکنون میتوانیم بهوسیله توابع نوشتهشده، اطلاعات دلخواهمان را روی حافظه بنویسیم و از آن بخوانیم. قبل از آن، واحد I2C را فعال میکنیم و برای دریافت اطلاعات خواندهشده متغیرهای موردنیازمان را تعریف میکنیم:
uint8_t buffer1, buffer2; LL_I2C_Enable(I2C1);
حالا برای نمونه در آدرس 0000 مقدار 24 و در آدرس 0001 عدد 25 را مینویسیم. سپس این دو خانهی حافظه را میخوانیم.
E2Prom_Write(0x0000,0x24); // write 0x24 to the address 0000 of the EEPROM LL_mDelay(5); // Duration of the EEPROM internal write cycle E2Prom_Write(0x0001,0x25); LL_mDelay(5); buffer1 = E2Prom_Read(0x0000); // read from the address 0000 of the EEPROM LL_mDelay(1); // Delay needed for each read cycle buffer2 = E2Prom_Read(0x0001); LL_mDelay(1);
درستی عملیات نوشتن و خواندن را میتوانیم در Logic Analyzer بررسی کنیم؛
همچنین میتوانیم برای اطمینان از درست انجام شده عمل نوشتن اطلاعات خوانده شده را توسط USART ارسال کنیم؛
printf("buffer1 is: %x\r\n", buffer1); printf("buffer2 is: %x\r\n", buffer2);
در ترمینال سریال میبینیم؛
منبع: سیسوگ