در قسمت قبلی با ADC آشنا شدیم و در آموزشهای قبلی در مورد پروتکلهای ارتباط سریال صحبت کردیم. در این قسمت میخواهیم در مورد یک پروتکل ارتباط سریال دیگر، یعنی 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 کد پروژه را ایجاد میکنیم.
نوشتن کد پروژه
در ابتدا کتابخانهها و ثابتهای مورد نیاز را اضافه میکنیم؛
1 2 | #include "stdbool.h" #include "stdio.h" |
1 2 3 4 5 6 7 8 | #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 را بنویسیم؛
1 2 3 4 5 6 | /* Function for transmitting 8bit data via I2C */ void write_i2c(uint8_t Data) { LL_I2C_TransmitData8(I2C1, Data); while(!LL_I2C_IsActiveFlag_TXE(I2C1)); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* 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 مینویسیم؛
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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; } |
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 | /* 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 را فعال میکنیم و برای دریافت اطلاعات خواندهشده متغیرهای موردنیازمان را تعریف میکنیم:
1 2 | uint8_t buffer1, buffer2; LL_I2C_Enable(I2C1); |
حالا برای نمونه در آدرس 0000 مقدار 24 و در آدرس 0001 عدد 25 را مینویسیم. سپس این دو خانهی حافظه را میخوانیم.
1 2 3 4 5 6 7 8 | 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 ارسال کنیم؛
1 2 | printf("buffer1 is: %x\r\n", buffer1); printf("buffer2 is: %x\r\n", buffer2); |
در ترمینال سریال میبینیم؛
با عرض سلام و وقت بخیر
آموزش بسیار خوبی ارائه داده اید فقط یک نکته اینکه برای بهبود کیفیت کد می توانید به جای استفاده از وقفه زمانی Z_NOP بعد از خطوط LL_I2C_GenerateStartCondition(I2C1)
از عبارت { } while (!LL_I2C_IsActiveFlag_SB(I2C1))
استفاده نمایید. این را من از چندین منبع متوجه شدم مثل لینک زیر
https://community.st.com/t5/stm32-mcus-products/busy-bus-after-i2c-reading/td-p/327215
سلام
ممنون بابت نوشتن تنها آموزش فارسی توابع LL
من یه سنسور aht10/aht20 رو با توابع Hal راه اندازی کردم… حالا میخوام تبدیلش کنم به LL ولی نمیدونم چرا کدی که نوشتم به مشکل خورده و اجرا نمیشه…
اگه ممکنه منو راهنمایی کنید.در ادامه کدی که با استفاده از Hal نوشتم رو قرار میدم:
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM16)
{
/* Set every 100ms */
T_100ms = 255;
}
}
void AHT10_GET()
{
if(T_100ms)
{
if(AHT10_Switcher)
{
HAL_I2C_Master_Transmit_IT(&hi2c1, AHT10_ADRESS, (uint8_t*)AHT10_TmpHum_Cmd, 3); /* Send command (trigger measuremetns) + parameters */
}
else
{
HAL_I2C_Master_Receive_IT(&hi2c1, AHT10_ADRESS, (uint8_t*)AHT10_RX_Data, 6); /* Receive data: STATUS[1]:HIMIDITY[2.5]:TEMPERATURE[2.5] */
}
if(~AHT10_RX_Data[0] & 0x80)
{
/* Convert to Temperature in °C */
AHT10_ADC_Raw = (((uint32_t)AHT10_RX_Data[3] & 15) << 16) | ((uint32_t)AHT10_RX_Data[4] << 8) | AHT10_RX_Data[5];
AHT10_Temperature = (float)(AHT10_ADC_Raw * 200.00 / 1048576.00) – 50.00;
/* Convert to Relative Humidity in % */
AHT10_ADC_Raw = ((uint32_t)AHT10_RX_Data[1] << 12) | ((uint32_t)AHT10_RX_Data[2] <> 4);
AHT10_Humidity = (float)(AHT10_ADC_Raw*100.00/1048576.00);
}
AHT10_Switcher = ~AHT10_Switcher; /* Invert */
//GPIOC->ODR ^= GPIO_ODR_ODR13; /* Green LED */
T_100ms = 0; /* Nulify */
}
}
کد بالا رو تبدیل به کد زیر با استفاده از LL کردم البته از chatgpt هم کمک گرفتم:
#define AHT20_ADDRESS 0x38 << 1
#define AHT20_INIT_CMD 0xBE
#define AHT20_MEASURE_CMD 0xAC
#define AHT20_SOFT_RESET_CMD 0xBA
void AHT20_Init(I2C_TypeDef *I2Cx) {
uint8_t init_cmd[] = {AHT20_INIT_CMD, 0x08, 0x00};
while (LL_I2C_IsActiveFlag_BUSY(I2Cx))
;
LL_I2C_HandleTransfer(I2Cx, AHT20_ADDRESS, LL_I2C_ADDRSLAVE_7BIT, sizeof(init_cmd), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
for (uint8_t i = 0; i < sizeof(init_cmd); i++) {
LL_I2C_TransmitData8(I2Cx, init_cmd[i]);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
}
while (!LL_I2C_IsActiveFlag_STOP(I2Cx))
;
LL_I2C_ClearFlag_STOP(I2Cx);
LL_mDelay(20);
}
void AHT20_SoftReset(I2C_TypeDef *I2Cx) {
uint8_t reset_cmd = AHT20_SOFT_RESET_CMD;
while (LL_I2C_IsActiveFlag_BUSY(I2Cx))
;
LL_I2C_HandleTransfer(I2Cx, AHT20_ADDRESS, LL_I2C_ADDRSLAVE_7BIT, 1, LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
LL_I2C_TransmitData8(I2Cx, reset_cmd);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
while (!LL_I2C_IsActiveFlag_STOP(I2Cx))
;
LL_I2C_ClearFlag_STOP(I2Cx);
LL_mDelay(20);
}
void AHT20_TriggerMeasurement(I2C_TypeDef *I2Cx) {
uint8_t measure_cmd[] = {AHT20_MEASURE_CMD, 0x33, 0x00};
while (LL_I2C_IsActiveFlag_BUSY(I2Cx))
;
LL_I2C_HandleTransfer(I2Cx, AHT20_ADDRESS, LL_I2C_ADDRSLAVE_7BIT, sizeof(measure_cmd), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_WRITE);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
for (uint8_t i = 0; i < sizeof(measure_cmd); i++) {
LL_I2C_TransmitData8(I2Cx, measure_cmd[i]);
while (!LL_I2C_IsActiveFlag_TXIS(I2Cx))
;
}
while (!LL_I2C_IsActiveFlag_STOP(I2Cx))
;
LL_I2C_ClearFlag_STOP(I2Cx);
LL_mDelay(80);
}
void AHT20_ReadData(I2C_TypeDef *I2Cx, float *temperature, float *humidity) {
uint8_t data[6];
while (LL_I2C_IsActiveFlag_BUSY(I2Cx))
;
LL_I2C_HandleTransfer(I2Cx, AHT20_ADDRESS, LL_I2C_ADDRSLAVE_7BIT, sizeof(data), LL_I2C_MODE_AUTOEND, LL_I2C_GENERATE_START_READ);
for (uint8_t i = 0; i < sizeof(data); i++) {
while (!LL_I2C_IsActiveFlag_RXNE(I2Cx))
;
data[i] = LL_I2C_ReceiveData8(I2Cx);
}
while (!LL_I2C_IsActiveFlag_STOP(I2Cx))
;
LL_I2C_ClearFlag_STOP(I2Cx);
uint32_t raw_humidity = ((uint32_t)data[1] << 12) | ((uint32_t)data[2] <> 4);
uint32_t raw_temperature = ((uint32_t)(data[3] & 0x0F) << 16) | ((uint32_t)data[4] << 8) | data[5];
*humidity = (float)raw_humidity * 100 / (1 << 20);
*temperature = (float)raw_temperature * 200 / (1 << 20) – 50;
}
ممنون میشم راهنمایی کنید. واقعا رفرنس خوبی برای LL تو نت نیست…🤯🥵🙏🙏
با سلام و تبریک سال نو.
ممنون بابت سایت و مطالب فوق العاده تون _\/_
دوستان کسی میتونه یه توضیح کوتاه در مورد ()define Y_NOP و علت فراخوانیشون قبل وضعیت استارت بده؟
برای ایجاد تاخیره
سلام. با سپاس فراوان از آموزش فوق العاده شما
من با همین برد میخوام ماژول ADS1117 رو راه اندازی کنم. امکانش هست راهنمایی بفرمایید؟
تشکر بابت آموزشهای مفید و کاربردی