مقدمه
شاید تاکنون در انجام پروژههایتان به چالشی با عنوان سنجش زمان، تاریخ و دما برخورد کرده باشید، بهطورمثال زمانسنجی مقدار عملکرد دستگاه، انجام یک عملکرد خاص طبق زمانبدی تعیینشده، چککردن دما و گزارش هفتگی، ماهانه و سالیانه عملکرد دستگاه و…
اما مطمئنا اگر در پروژههایتان با مواردبالا برخوردکردهباشید. حتما با سوالاتی همچون مواردزیر برخورد کردهاید.
- از چه آیسی باید استفادهکنیم؟ که قابلیت دریافت همزمان تاریخ، زمان و دما را داشتهباشد؟
- چطور باید آنرا راهاندازی کنیم؟
در این مقاله میخواهیم به این سوالات پاسخدهیم و درنهایت ماژول معرفیشده را توسط برد آردوینو راهاندازی کنیم.
از چه آیسی باید استفادهکنیم؟ که قابلیت دریافت همزمان تاریخ، زمان و دما را داشتهباشد؟
درواقع یکیاز بزرگترین ضعفهای مهندسان الکترونیک، انتخاب قطعه مناسب برای انجام کار موردنیاز است، که در این بخش میخواهیم باتوجهبه پروژهای که تعریف کردهایم، قطعه موردنظر خود را انتخابکنیم.
خب اگر دقتکنید در مقدمه مقاله گفتیم که نیاز به سنجش زمان، تاریخ و همینطور دما را داریم. برای اینکه بتوانیم قطعه موردنظر را پیدا کنیم نیاز به یک جستجو با عنوان ماژول یا آیسی ساعت در گوگل را داریم که پساز این جستجو، با آیسی به نام DS3231 آشناخواهیمشد. البته دقتکنید که آیسی DS1307 هم موجود میباشد که این آیسی دقت کمتری نسبتبه DS3231 را دارد.
حال مرحله بعد نیاز به بررسی دیتاشیت این ماژول داریم. که ببینیم قابلیتهایی که مدنظر ما است را دارد یا خیر؟
خب پساز بررسی دیتاشیت ماژول DS3231، مواردی که مدنظرما در این پروژه بود را ازجمله مواردزیر را دارا میباشد.
- تنظیم سال
- تنظیم ماه
- تنظیم روز
- تنظیم روزهای هفته
- تنظیم ساعت
- تنظیم دقیقه
- تنظیم ثانیه
- داشتن یک سنسور داخلی که قابلیت اندازهگیری دما از -۴۰ تا ۸۵ درجه سانتیگراد را دارد
خب پساز اینکه موفق به انتخاب آیسی موردنظر شدیم. باید آنرا راهاندازی کنیم.
نحوه راهاندازی ماژول DS3231
خب در این بخش قبلاز راهاندازی نیاز به یک آشنایی جزئی با این ماژول را داریم. که این آشنایی جز با مراجعه به دیتاشیت بدست نخواهد آمد.
نحوه صحیح دیتاشیت خوانی چیست؟
یکیدیگر از مواردی که مهندسان الکترونیک در آن ضعف دارند، خواندن صحیح دیتاشیت قطعه موردنظر است که در این بخش ابتدا میخواهیم بهصورت کاملا مختصر نحوه صحیح خواندن دیتاشیت را توضیحدهیم و نکتهای که وجود دارد خیلیاز دوستان فکر میکنند که حتما باید تسلط کامل به زبانانگلیسی داشتهباشند تا بتوانند تسلط کافی را به دیتاشیت داشتهباشند و درنهایت بتوانند قطعه موردنظر را راهاندازی کنند. خیر، برای راهاندازی قطعه موردنظر باید نکات کلیدی که نیاز است را از دیتاشیت جمعآوری کرد و درنهایت از آن نکات استفادهکرد البته از این نکته هم غافل نباشیم که تسلط به زبان انگلیسی بسیار میتواند در این روند کمککننده محسوب شود.
خب فرضکنید ما تابهحال حتی اسم این قطعه را هم نشنیدهایم و فقط تنها برنامهنویسی را میدانیم، حال باید برای راهاندازی، چه کاری را انجامدهیم.
اولین کاری که باید انجامدهیم، این است که بدانیم آیسی موردنظر از چه پروتکلی پشتیبانی میکند. خب اگر به عکسی که در ادامه قرار دارد دقتکنید این آیسی از پروتکل I2C جهت برقراری با ماژول استفاده میکند.
دومین مرحله یادگیری پروتکل I2C است که دقیقا چه عملیاتی را باید جهت برقراری ارتباط با میکروکنترلر انجام داد.
پروتکل I2C چیست؟
اگر بهصورت کاملا مختصر توضیحدهیم، یک راه ارتباطی جهت انتقال دیتا بین میکروکنترلر و آیسی موردنظر است که نحوه عملکرد آن بهصورت شکلزیر میباشد.
پروتکل I2C را با پینهای زیر میشناسند.
- (SDA (Serial Data: برای ارسال و دریافت داده بین master و slave
- (SCL (Serial Clock: خطی که حامل سیگنال کلاک می باشد.
نحوه عملکرد پروتکل I2C
در I2C دادهها بهصورت پیامهایی فرستاده میشود که خود این پیامها خود به چند بخش تقسیم میشوند. هر پیام شامل یک بخش آدرس است، که همان آدرس باینری مربوط به slave موردنظر است. یک یا دو بخش مربوط به دیتا هم در پیامهای ارسالی نیز وجود دارد. پیام همچنین شامل شرایط اولیه و پایانی(دستور شروع و پایان پروتکل)، بیتهای read/write و بیتهای ACK / NACK بین هربخش از پیام است.
حال پساز اینکه یک دیدکلی نسبتبه پروتکل پیداکردیم نیاز است که دستورات ماژول DS3231 را مطالعهکنیم و ببینیم که هر دستور دقیقا چه کاربردی دارد و دقیقا کجا مورداستفاده قرار میگیرد.
خب پساز اینکه این دستورات را موردمطالعه قراردادیم نوبتبه انتقال همین دستورات و انتقال دیتا توسط پروتکل I2C است که برای این بخش توصیه میکنیم به کتابخانهای که در جدول پایین توضیحدادیم مراجعهکنید.
درواقع روندی که توضیحدادهشد یک حالت کاملا کلی از خواندن دیتاشیت بود که در ادامه نحوه کدنویسی ماژول DS3231 را موردمطالعه قرار میدهیم.
کدنویسی ماژول DS3231
در این بخش میخواهیم علاوهبر کد آردوینو ماژول، کتابخانهای هم که از آن استفاده میکنیم موردمطالعه قراردهیم و از عملکرد آن مطلع شویم که اگر خودمان نیاز به نوشتن یه کتابخانه بهصورت اصولی داشتیم باید چهکاری را انجامدهیم و صرفا از کتابخانههای آماده استفادهنکنیم (چون امکانش هست یه جایی گیر کنیم و اگه واقعا همش از توابع آماده استفادهکنیم آخرش به مشکل بخوریم و… پس بهتره که هر چیزی رو اصولی یادبگیریم و بعدش حداقل از توابع آمادش استفادهکنیم)
کتابخانه نوشتهشده ماژول DS3231
#include <Wire.h> #include <avr/pgmspace.h> #include "Sodaq_DS3231.h" #include "Arduino.h" #define EPOCH_TIME_OFF 946684800 // This is 2000-jan-01 00:00:00 in epoch time #define SECONDS_PER_DAY 86400L این خط ارسال دستور اماده باش به ماژول است که با دستور 0x68 شناخته میشود #define DS3231_ADDRESS 0x68 //I2C Slave address تعریف یک سری دستورات ماکرو که دقیقا از دیتاشیت آیسی انتخاب شده و کاملا مطابق با دستورات دیتاشیت میباشد. که کاملا هم خوانا نوشته شده است. نکته اول: در نوشتن کتابخانه همیشه منظم و خوانا عمل کنید. #define DS3231_SEC_REG 0x00 #define DS3231_MIN_REG 0x01 #define DS3231_HOUR_REG 0x02 #define DS3231_WDAY_REG 0x03 #define DS3231_MDAY_REG 0x04 #define DS3231_MONTH_REG 0x05 #define DS3231_YEAR_REG 0x06 #define DS3231_AL1SEC_REG 0x07 #define DS3231_AL1MIN_REG 0x08 #define DS3231_AL1HOUR_REG 0x09 #define DS3231_AL1WDAY_REG 0x0A #define DS3231_AL2MIN_REG 0x0B #define DS3231_AL2HOUR_REG 0x0C #define DS3231_AL2WDAY_REG 0x0D #define DS3231_CONTROL_REG 0x0E #define DS3231_STATUS_REG 0x0F #define DS3231_AGING_OFFSET_REG 0x10 #define DS3231_TMP_UP_REG 0x11 #define DS3231_TMP_LOW_REG 0x12 //////////////////////////////////////////////////////////////////////////////// // utility code, some of this could be exposed in the DateTime API if needed در این بخش هم به نحوه راهاندازی دستورات ماژول و همینطور نحوه محاسبه زمان و تاریخ و همینطور محاسبه دما پرداخته شده است. static const uint8_t daysInMonth [] PROGMEM = { 31,28,31,30,31,30,31,31,30,31,30,31 }; // number of days since 2000/01/01, valid for 2001..2099 static uint16_t date2days(uint16_t y, uint8_t m, uint8_t d) { if (y >= 2000) y -= 2000; uint16_t days = d; for (uint8_t i = 1; i < m; ++i) days += pgm_read_byte(daysInMonth + i - 1); if (m > 2 && y % 4 == 0) ++days; return days + 365 * y + (y + 3) / 4 - 1; } static uint32_t time2long(uint16_t days, uint8_t h, uint8_t m, uint8_t s) { return ((days * 24L + h) * 60 + m) * 60 + s; } static uint8_t conv2d(const char* p) { uint8_t v = 0; if ('0' <= *p && *p <= '9') v = *p - '0'; return 10 * v + *++p - '0'; } //////////////////////////////////////////////////////////////////////////////// // DateTime implementation - ignores time zones and DST changes // NOTE: also ignores leap seconds, see http://en.wikipedia.org/wiki/Leap_second DateTime::DateTime (long t) { ss = t % 60; t /= 60; mm = t % 60; t /= 60; hh = t % 24; uint16_t days = t / 24; uint8_t leap; for (yOff = 0; ; ++yOff) { leap = yOff % 4 == 0; if (days < 365 + leap) break; days -= 365 + leap; } for (m = 1; ; ++m) { uint8_t daysPerMonth = pgm_read_byte(daysInMonth + m - 1); if (leap && m == 2) ++daysPerMonth; if (days < daysPerMonth) break; days -= daysPerMonth; } d = days + 1; wday = 0; // FIXME This is not properly initialized } DateTime::DateTime (uint16_t year, uint8_t month, uint8_t date, uint8_t hour, uint8_t min, uint8_t sec, uint8_t wd) { if (year >= 2000) year -= 2000; yOff = year; m = month; d = date; hh = hour; mm = min; ss = sec; wday = wd; } // A convenient constructor for using "the compiler's time": // DateTime now (__DATE__, __TIME__); // NOTE: using PSTR would further reduce the RAM footprint DateTime::DateTime (const char* date, const char* time) { // sample input: date = "Dec 26 2009", time = "12:34:56" yOff = conv2d(date + 9); // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec switch (date[0]) { case 'J': m = date[1] == 'a' ? 1 : date[2] == 'n' ? 6 : 7; break; case 'F': m = 2; break; case 'A': m = date[2] == 'r' ? 4 : 8; break; case 'M': m = date[2] == 'r' ? 3 : 5; break; case 'S': m = 9; break; case 'O': m = 10; break; case 'N': m = 11; break; case 'D': m = 12; break; } d = conv2d(date + 4); hh = conv2d(time); mm = conv2d(time + 3); ss = conv2d(time + 6); wday = 0; // FIXME This is not properly initialized } uint32_t DateTime::get() const { uint16_t days = date2days(yOff, m, d); return time2long(days, hh, mm, ss); } uint32_t DateTime::getEpoch() const { return get() + EPOCH_TIME_OFF; } /* * Format an integer as %0*d * * Arduino formatting sucks. */ static void add0Nd(String &str, uint16_t val, size_t width) { if (width >= 5 && val < 1000) { str += '0'; } if (width >= 4 && val < 100) { str += '0'; } if (width >= 3 && val < 100) { str += '0'; } if (width >= 2 && val < 10) { str += '0'; } str += val; } static inline void add04d(String &str, uint16_t val) { add0Nd(str, val, 4); } static inline void add02d(String &str, uint16_t val) { add0Nd(str, val, 2); } void DateTime::addToString(String & str) const { add04d(str, year()); str += '-'; add02d(str, month()); str += '-'; add02d(str, date()); str += ' '; add02d(str, hour()); str += ':'; add02d(str, minute()); str += ':'; add02d(str, second()); } static uint8_t bcd2bin (uint8_t val) { return val - 6 * (val >> 4); } static uint8_t bin2bcd (uint8_t val) { return val + 6 * (val / 10); } //////////////////////////////////////////////////////////////////////////////// // RTC DS3231 implementation uint8_t Sodaq_DS3231::readRegister(uint8_t regaddress) { Wire.beginTransmission(DS3231_ADDRESS); Wire.write((byte)regaddress); Wire.endTransmission(); Wire.requestFrom(DS3231_ADDRESS, 1); return Wire.read(); } void Sodaq_DS3231::writeRegister(uint8_t regaddress,uint8_t value) { Wire.beginTransmission(DS3231_ADDRESS); Wire.write((byte)regaddress); Wire.write((byte)value); Wire.endTransmission(); } uint8_t Sodaq_DS3231::begin(void) { unsigned char ctReg=0; Wire.begin(); ctReg |= 0b00011100; writeRegister(DS3231_CONTROL_REG, ctReg); //CONTROL Register Address delay(10); // set the clock to 24hr format uint8_t hrReg = readRegister(DS3231_HOUR_REG); hrReg &= 0b10111111; writeRegister(DS3231_HOUR_REG, hrReg); delay(10); return 1; } //set the time-date specified in DateTime format //writing any non-existent time-data may interfere with normal operation of the RTC void Sodaq_DS3231::setDateTime(const DateTime& dt) { Wire.beginTransmission(DS3231_ADDRESS); Wire.write((byte)DS3231_SEC_REG); //beginning from SEC Register address Wire.write((byte)bin2bcd(dt.second())); Wire.write((byte)bin2bcd(dt.minute())); Wire.write((byte)bin2bcd((dt.hour()) & 0b10111111)); //Make sure clock is still 24 Hour Wire.write((byte)dt.dayOfWeek()); Wire.write((byte)bin2bcd(dt.date())); Wire.write((byte)bin2bcd(dt.month())); Wire.write((byte)bin2bcd(dt.year() - 2000)); Wire.endTransmission(); } DateTime Sodaq_DS3231::makeDateTime(unsigned long t) { if (t < EPOCH_TIME_OFF) return DateTime(0); return DateTime(t - EPOCH_TIME_OFF); } // Set the RTC using timestamp (seconds since epoch) void Sodaq_DS3231::setEpoch(uint32_t ts) { setDateTime(makeDateTime(ts)); } //Read the current time-date and return it in DateTime format DateTime Sodaq_DS3231::now() { Wire.beginTransmission(DS3231_ADDRESS); Wire.write((byte)0x00); Wire.endTransmission(); Wire.requestFrom(DS3231_ADDRESS, 8); uint8_t ss = bcd2bin(Wire.read()); uint8_t mm = bcd2bin(Wire.read()); uint8_t hrreg = Wire.read(); uint8_t hh = bcd2bin((hrreg & ~0b11000000)); //Ignore 24 Hour bit uint8_t wd = Wire.read(); uint8_t d = bcd2bin(Wire.read()); uint8_t m = bcd2bin(Wire.read()); uint16_t y = bcd2bin(Wire.read()) + 2000; return DateTime (y, m, d, hh, mm, ss, wd); } //Enable periodic interrupt at /INT pin. Supports only the level interrupt //for consistency with other /INT interrupts. All interrupts works like single-shot counter //Use refreshINTA() to re-enable interrupt. void Sodaq_DS3231::enableInterrupts(uint8_t periodicity) { unsigned char ctReg=0; ctReg |= 0b00011101; writeRegister(DS3231_CONTROL_REG, ctReg); //CONTROL Register Address switch(periodicity) { case EverySecond: writeRegister(DS3231_AL1SEC_REG, 0b10000000 ); //set AM1 writeRegister(DS3231_AL1MIN_REG, 0b10000000 ); //set AM2 writeRegister(DS3231_AL1HOUR_REG, 0b10000000 ); //set AM3 writeRegister(DS3231_AL1WDAY_REG, 0b10000000 ); //set AM4 break; case EveryMinute: writeRegister(DS3231_AL1SEC_REG, 0b00000000 ); //Clr AM1 writeRegister(DS3231_AL1MIN_REG, 0b10000000 ); //set AM2 writeRegister(DS3231_AL1HOUR_REG, 0b10000000 ); //set AM3 writeRegister(DS3231_AL1WDAY_REG, 0b10000000 ); //set AM4 break; case EveryHour: writeRegister(DS3231_AL1SEC_REG, 0b00000000 ); //Clr AM1 writeRegister(DS3231_AL1MIN_REG, 0b00000000 ); //Clr AM2 writeRegister(DS3231_AL1HOUR_REG, 0b10000000 ); //Set AM3 writeRegister(DS3231_AL1WDAY_REG, 0b10000000 ); //set AM4 break; } } //Enable HH/MM/SS interrupt on /INTA pin. All interrupts works like single-shot counter void Sodaq_DS3231::enableInterrupts(uint8_t hh24, uint8_t mm, uint8_t ss) { unsigned char ctReg=0; ctReg |= 0b00011101; writeRegister(DS3231_CONTROL_REG, ctReg); //CONTROL Register Address writeRegister(DS3231_AL1SEC_REG, 0b00000000 | bin2bcd(ss) ); //Clr AM1 writeRegister(DS3231_AL1MIN_REG, 0b00000000 | bin2bcd(mm)); //Clr AM2 writeRegister(DS3231_AL1HOUR_REG, (0b00000000 | (bin2bcd(hh24) & 0b10111111))); //Clr AM3 writeRegister(DS3231_AL1WDAY_REG, 0b10000000 ); //set AM4 } //Disable Interrupts. This is equivalent to begin() method. void Sodaq_DS3231::disableInterrupts() { begin(); //Restore to initial value. } //Clears the interrrupt flag in status register. //This is equivalent to preparing the DS3231 /INT pin to high for MCU to get ready for recognizing the next INT0 interrupt void Sodaq_DS3231::clearINTStatus() { // Clear interrupt flag uint8_t statusReg = readRegister(DS3231_STATUS_REG); statusReg &= 0b11111110; writeRegister(DS3231_STATUS_REG, statusReg); } //force temperature sampling and converting to registers. If this function is not used the temperature is sampled once 64 Sec. void Sodaq_DS3231::convertTemperature() { // Set CONV uint8_t ctReg = readRegister(DS3231_CONTROL_REG); ctReg |= 0b00100000; writeRegister(DS3231_CONTROL_REG,ctReg); //wait until CONV is cleared. Indicates new temperature value is available in register. do { //do nothing } while ((readRegister(DS3231_CONTROL_REG) & 0b00100000) == 0b00100000 ); } //Read the temperature value from the register and convert it into float (deg C) float Sodaq_DS3231::getTemperature() { float fTemperatureCelsius; uint8_t tUBYTE = readRegister(DS3231_TMP_UP_REG); //Two's complement form uint8_t tLRBYTE = readRegister(DS3231_TMP_LOW_REG); //Fractional part if(tUBYTE & 0b10000000) //check if -ve number { tUBYTE ^= 0b11111111; tUBYTE += 0x1; fTemperatureCelsius = tUBYTE + ((tLRBYTE >> 6) * 0.25); fTemperatureCelsius = fTemperatureCelsius * -1; } else { fTemperatureCelsius = tUBYTE + ((tLRBYTE >> 6) * 0.25); } return (fTemperatureCelsius); } Sodaq_DS3231 rtc;
درواقع یکیاز مزیتهایی که میتواند بررسی کتابخانهها داشتهباشد. نحوه اصولی کدنویسی است که خود میتواند بسیار در کدنویسی موثر واقعشود.
خب حالا نوبتبه استفادهاز این کتابخانه میرسد البته با این دید که میدانیم از چه توابعی استفادهشده و چطور مورداستفاده قرار گرفتهشدهاست.
راهاندازی ماژول DS3231 با آردوینو
خب این بخش به دو قسمت شماتیک و کدنویسی تقسیم میشود.
شماتیک راهاندازی ماژول DS3231 با آردوینو
کدنویسی ماژول DS3231 با آردوینو
ابتدا باید کتابخانه مربوطه را نصب کنیم، که برای این کار باید طبق شکلهایی که در ادامه میگذاریم عمل نمایید.
حال پساز نصب کتابخانه نوبت به نوشتن کد موردنظر برای راهاندازی ماژول موردنظر است که در این قسمت میخواهیم از مثالهای آماده این کتابخانه استفادهکنیم. برای این کار باید بهصورتزیر عملکنیم.
خب همونطوریکه میبینید این کتابخانه دارای ۴نمونه کد راهانداز است که میخواهیم کد NOW را امتحان و درنهایت روی سریال مانیتور اطلاعات را نمایشدهیم.
کد مثال NOW
// Date and time functions using RX8025 RTC connected via I2C and Wire lib #include <Wire.h> #include "Sodaq_DS3231.h" char weekDay[][4] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; void setup () { Serial.begin(57600); Wire.begin(); rtc.begin(); } uint32_t old_ts; void loop () { DateTime now = rtc.now(); //get the current date-time uint32_t ts = now.getEpoch(); if (old_ts == 0 || old_ts != ts) { old_ts = ts; Serial.print(now.year(), DEC); Serial.print('/'); Serial.print(now.month(), DEC); Serial.print('/'); Serial.print(now.date(), DEC); Serial.print(' '); Serial.print(now.hour(), DEC); Serial.print(':'); Serial.print(now.minute(), DEC); Serial.print(':'); Serial.print(now.second(), DEC); Serial.print(' '); Serial.print(weekDay[now.dayOfWeek()]); Serial.println(); Serial.print("Seconds since Unix Epoch: "); Serial.print(ts, DEC); Serial.println(); } delay(1000); }
درنهایت اگر تمامی مراحلی که توضیح دادیم بهدرستی انجامشدهباشد باید خروجی سربال مانیتور مطابق اطلاعاتی که در ادامه قرار میدهیم، باشد.
2002/2/5 7:12:58 Wed Seconds since Unix Epoch: 1012893178 2002/2/5 7:12:59 Wed Seconds since Unix Epoch: 1012893179 2002/2/5 7:13:0 Wed Seconds since Unix Epoch: 1012893180 2002/2/5 7:13:1 Wed Seconds since Unix Epoch: 1012893181 2002/2/5 7:13:2 Wed Seconds since Unix Epoch: 1012893182 2002/2/5 7:13:3 Wed Seconds since Unix Epoch: 1012893183 2002/2/5 7:13:4 Wed Seconds since Unix Epoch: 1012893184 2002/2/5 7:13:5 Wed Seconds since Unix Epoch: 1012893185 2002/2/5 7:13:6 Wed Seconds since Unix Epoch: 1012893186 2002/2/5 7:13:7 Wed Seconds since Unix Epoch: 1012893187 2002/2/5 7:13:8 Wed Seconds since Unix Epoch: 1012893188 2002/2/5 7:13:9 Wed Seconds since Unix Epoch: 1012893189 2002/2/5 7:13:10 Wed Seconds since Unix Epoch: 1012893190 2002/2/5 7:13:11 Wed Seconds since Unix Epoch: 1012893191 2002/2/5 7:13:12 Wed
امیدوارم که این آموزش هم برای شما مفید واقعشدهباشد.
منبع: سیسوگ