الحيل التي تعلمتها حول استخدام DS3231 Real Time Clock لمقاطعة Arduino
تريد تحديد الأوقات الدقيقة التي سيقوم فيها Arduino بتشغيل مقاطع تعليمات برمجية خاصة في رسم تخطيطي، دون استخدام الموقتات والعدادات في أجهزة Arduino. لديك بعض المهارة والخبرة في كتابة التعليمات البرمجية باستخدام Arduino IDE، وترغب في تجنب استخدام عبارات التعليمات البرمجية مثل delay()
.
بدلاً من ذلك، قد ترغب في توصيل وحدة DS3231 Real Time Clock الدقيقة للغاية كمصدر للمقاطعات الخارجية. قد يكون من المهم بالنسبة لك أن يستخدم DS3231 بطارية للحفاظ على الوقت الدقيق حتى لو فقد Arduino الطاقة مؤقتًا.
أخيرًا، تريد معرفة كيفية استخدام مكتبة DS3231.h بواسطة Andrew Wickert والمشار إليها في مرجع Arduino عبر الإنترنت: https://www.arduino.cc/reference/en/libraries/ds3231/. فهو يحتوي على بعض الحيل التي يمكن أن تثبت أنها تستحق الجهد المبذول لإتقانها. يمكنك استيراد هذه المكتبة إلى Arduino IDE الخاص بك باستخدام مدير المكتبة (الأدوات > إدارة المكتبات...).
اذهب خطوة بخطوة. يوضح هذا البرنامج التعليمي الخطوات التالية:
إذا كنت تريد تشغيل التعليمات البرمجية الخاصة أكثر من مرة، على فترات زمنية تحددها، فيمكن للتعليمات البرمجية الخاصة بك القيام بالخطوة 3 مرة أخرى وتعيين تنبيه جديد. الرسم التوضيحي المصاحب لهذا البرنامج التعليمي يقاطع Arduino بشكل متكرر، بفواصل زمنية مدتها 10 ثوانٍ.
هذا البرنامج التعليمي مستمد من المراجع "الرسمية"، بما في ذلك:
attachInterrupt()
: https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/. الكود الخاص في هذا المثال تافه: فهو يطبع الوقت الحالي فقط من DS3231. مثال من الحياة الواقعية قد يفعل شيئًا مفيدًا مثل سقي نبات أو تسجيل قياس درجة الحرارة. مهما كانت المهمة، يجب أن ينتقل الكود إلى كتلة خاصة به ليتم تشغيله فقط عندما يقاطع إنذار DS3231 الاردوينو.
أعتقد أنه من الممارسات الجيدة تقسيم التعليمات البرمجية إلى وظائف قصيرة، حيث تتعامل كل وظيفة مع مهمة واحدة فقط أو مجموعة واحدة من المهام ذات الصلة. يمكن أن يكون اسم الوظيفة أي شيء؛ لماذا لا تجعلها تصف ما تفعله الوظيفة؟ إليك جزء من وظيفتي التي تقوم بتشغيل الكود الخاص في هذا المثال.
void runTheSpecialCode() {
// get the current time
// using the DateTime and RTClib classes
// defined in DS3231.h
DateTime dt = RTClib::now();
// print the current time
Serial.print(dt.hour()); Serial.print(":");
if (dt.minute() < 10) Serial.print("0");
Serial.print(dt.minute()); Serial.print(":");
if (dt.second() < 10) Serial.print("0");
Serial.println(dt.second());
// There will be more to do here, as you will see.
// This is enough, for now, to illustrate the idea:
// put special code in its own, special function
}
سوف تقوم بتشغيل الأسلاك بين خمسة أزواج من المسامير. يؤدي كل زوج غرضًا كهربائيًا واحدًا ويطابق دبوسًا في Arduino مع دبوس مماثل في DS3231. خذ الأمر ببطء، وقم بتوصيل كل زوج، ثم تحقق من كلا الطرفين للتأكد من أن كل سلك يسير في مكانه الصحيح. يسرد الجدول الأزواج بالترتيب عند ربطها، من اليسار إلى اليمين، على DS3231 من Arduino Uno.
غاية | DS3231 دبوس | اردوينو دبوس |
---|---|---|
إنذار | SQW | 3* |
SCL | SCL | SCL** |
SDA | SDA | سدا** |
قوة 5 فولت | VCC | 5 فولت |
أرضي | أرض | أرض |
كما قلت، خذ وقتك في إجراء هذه الروابط. غالبًا ما يكون البطء والتأكد هو أسرع طريقة لإكمال أي شيء بشكل صحيح.
سنتحدث إلى وحدة DS3231 عن طريق "كائن" DS3231، وهو نوع من صندوق أدوات البرنامج الذي يحمل اسمًا عليه. تحدد مكتبة DS3231 الكثير من الوظائف - فكر فيها كأدوات - داخل الصندوق. عندما نريد استخدام دالة، نكتب اسم صندوق الأدوات، متبوعًا بنقطة، ثم اسم الأداة. ينشئ المثال التخطيطي متغير "الساعة" لهذا الغرض. ثم يمكن للرسم الوصول إلى الأدوات الموجودة في مربع "الساعة". تم الإعلان عن كافة الأدوات في الملف DS3231.h المذكور أعلاه.
#include <DS3231.h>
DS3231 clock;
Serial.println(clock.getMinute()); // the current minute, 0..59
سوف نستخدم الأدوات الموجودة في كائن "الساعة" الخاص بنا لضبط المنبه. لكن علينا أولاً حساب وقت التنبيه.
تفترض هذه الخطوة أنك قمت مسبقًا بضبط الوقت الفعلي على DS3231. يحتوي الرسم التوضيحي على رمز يمكنك استخدامه لضبط الوقت على مدار الساعة، في حالة احتياجك إليه. ما عليك سوى إزالة محددات التعليق، /* و*/، المحيطة به.
يحسب الرسم التوضيحي الموجود في هذا البرنامج التعليمي وقت التنبيه في المستقبل عن طريق إضافة فاصل زمني، على شكل عدد من الثواني، إلى الوقت الحالي. يضيف المثال عشر ثوان. الدقيقة ستضيف 60 ثانية. الساعة ستضيف 3600 ثانية. اليوم 86400 ثانية. وهكذا دواليك.
تحتوي مكتبة DS3231 على "خدعة" مخفية تجعل من السهل إضافة الوقت بعدد الثواني. لن تجد هذه الخدعة مدرجة ضمن الوظائف المتاحة في صفحة README الخاصة بمكتبة DS3231. إنه ليس واضحًا تمامًا من خلال النظر إلى ملف DS3231.h أيضًا. تنتظر بعض التفاصيل العثور عليها في ملف التعليمات البرمجية DS3231.cpp. فيما يلي خطوات إجراء الحساب.
now()
، والتي يجب الوصول إليها بطريقة خاصة.يمكن الجمع بين الخطوتين 4 و5.
const Uint32_t interval = 10; // number of seconds to add
DateTime currentTime; // default declaration
currentTime = RTClib::now(); // RTClib is defined in DS3231.h
uint32_t currentSeconds = currentTime.unixtime(); // express the date in seconds
DateTime alarmTime(currentSeconds + interval); // add 10 seconds and create a new date
على الرغم من أن كائن AlarmTime يتم إنشاؤه استنادًا إلى عدد الثواني، إلا أنه يوفر أدوات في صندوق الأدوات الخاص به للتعبير عن السنة والشهر واليوم والساعة والدقيقة والثانية. قمنا بضبط وقت التنبيه على DS3231، كما هو موضح بعد ذلك، باستخدام كائن AlarmTime كمصدر للقيم التي سنحتاجها.
لنفترض، على سبيل المثال، أن الوقت الحالي الذي تم الإبلاغ عنه بواسطة الوحدة النمطية DS3231 كان 7 ثوانٍ بعد الساعة 10:42 صباحًا يوم الأربعاء 27 أكتوبر 2021. سيكون وقت التنبيه المحسوب أعلاه هو 10:42:17 في نفس اليوم، بعد عشر ثوانٍ.
يوفر DS3231 إنذارين مختلفين: الإنذار رقم 1 (A1) والإنذار رقم 2 (A2). يمكن تحديد كلا المنبهين باليوم والوقت، وصولاً إلى دقيقة واحدة. والفرق هو أنه يمكن تحديد A1 بشكل أكبر حتى ثانية واحدة. يحتوي كل إنذار على زوج من الوظائف الخاصة به في مكتبة DS3231 لتحديد وقت التنبيه وقراءة ذلك الوقت. يتم الوصول إلى جميع الوظائف من خلال كائن DS3231، على سبيل المثال، الكائن الذي أطلقنا عليه اسم "الساعة":
Clock.setA1Time() وclock.getA1Time() وclock.setA2Time() وclock.getA2Time()
تأخذ الدالة setA1Time() ثمانية معلمات، كما هو موضح في هذا الاقتباس من ملف DS3231.h:
void setA1Time(byte A1Day, byte A1Hour, byte A1Minute, byte A1Second, byte AlarmBits, bool A1Dy, bool A1h12, bool A1PM);
يمكن للقراءة الدقيقة لملف رأس DS3231.h أن تشرح المعلمات. ما يلي هنا هو محاولاتي لإعادة شرحها لنفسي بكلماتي الخاصة. إذا وجد القارئ أي تناقض بين الإصدار الخاص بي وملف الرأس، فافترض أن الرأس صحيح.
المعلمات الخمس الأولى من النوع "بايت". يحدد موقع الويب cppreference نوع البايت بهذه الطريقة https://en.cppreference.com/w/cpp/types/byte:
std::byte هو نوع مميز يطبق مفهوم البايت كما هو محدد في تعريف لغة C++.
مثل char وunsigned char، يمكن استخدامه للوصول إلى الذاكرة الأولية التي تشغلها كائنات أخرى (تمثيل الكائن)، ولكن على عكس تلك الأنواع، فهو ليس نوعًا من الأحرف وليس نوعًا حسابيًا. البايت هو مجرد مجموعة من البتات، والعوامل الوحيدة المحددة لها هي تلك التي تعتمد على البتات.
يمكننا أن نسمح لأنفسنا بالتفكير في متغيرات نوع البايت لليوم والوقت كما لو كانت أعدادًا صحيحة غير موقعة، في هذه الحالة بالذات. يمكنهم الاحتفاظ بقيمة عددية بين 0 و255. تنبيه: يجب على كاتب التعليمات البرمجية تجنب القيم غير المنطقية. على سبيل المثال، القيمة 102 لا معنى لها بالنسبة لأي من هذه المعلمات. إنها وظيفتك ككاتب الكود أن تقدم قيمًا معقولة.
لنواصل المنبه الذي تم إنشاؤه في الخطوة السابقة: اليوم السابع والعشرون من الشهر، عند الساعة 10:42 صباحًا بعد 17 ثانية. توضح القائمة أدناه كيف يمكنك توفير هذه القيم في الوظيفة. أقوم بإدراج كل معلمة في سطر خاص بها، لجعلها أكثر قابلية للقراءة من قبل البشر ولإتاحة مساحة للتعليقات. المثال هنا غير مكتمل؛ فهو يوضح فقط قيم نوع البايت للتاريخ والوقت. تتطلب الوظيفة المزيد من المعلمات، كما هو موضح أدناه، ولن تعمل بالشكل الموضح هنا.
بالمناسبة، لاحظ أن متغيرات "الساعة" و"alarmTime" هي كائنات، أي أنها عبارة عن صناديق أدوات برمجية. كما ترون، نستخدم أدوات من داخل صناديق الأدوات المعنية للوصول إلى المعلومات التي تحتوي عليها الكائنات.
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
// ... the remaining parameters are explained below
);
المعلمة التالية من نوع البايت، والتي تسمى AlarmBits، هي في الحقيقة مجرد مجموعة من البتات. تحتوي البتات على أسماء كما هو محدد في ورقة البيانات DS3231 (في الصفحة 11).
بت 7 | بت 6 | بت 5 | بت 4 | بت 3 | بت 2 | بت 1 | بت 0 |
---|---|---|---|---|---|---|---|
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
معًا، تشكل البتات "قناعًا" أو نمطًا، والذي يخبر DS3231 متى وكم مرة يتم الإشارة إلى الإنذار. يعطي الجدول الموجود في الصفحة 12 من ورقة البيانات المعنى لمجموعات مختلفة من البتات. بناءً على هذا الجدول، يستخدم المثال التخطيطي في هذا البرنامج التعليمي مجموعة البتات التالية:
-- | -- | -- | DyDt | A1M4 | A1M3 | A1M2 | A1M1 |
---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
يمكن التعبير عن هذا الترتيب للبتات بشكل صريح في الكود: 0x00001110
. فهو يخبر DS3231 بأن يرسل إنذارًا عندما تتطابق "الثواني"، أي عندما تتطابق قيمة "الثواني" لإعداد المنبه مع قيمة "الثواني" للوقت الحالي.
المعلمات الثلاثة الأخيرة للدالة setA1Time()
هي قيم منطقية أو قيم صحيحة/خطأ. يخبرون DS3231 بمزيد من المعلومات حول كيفية تقييم إعداد التنبيه. يعرض مقطع التعليمات البرمجية التالي الاستدعاء المكتمل لـ setA1Time()، استمرارًا للمثال الذي بدأ أعلاه:
clock.setA1Time(
alarmTime.day(), // the day of the month: 27
alarmTime.hour(), // the hour of the day: 10
alarmTime.minute(), // the minute of the hour: 42
alarmTime.second(), // the second of the minute: 17
0x00001110, // AlarmBits = signal when the seconds match
false, // A1Dy false = A1Day means the date in the month;
// true = A1Day means the day of the week
false, // A1h12 false = A1Hour in range 0..23;
// true = A1Hour in range 1..12 AM or PM
false // A1PM false = A1Hour is a.m.;
// true = A1Hour is p.m.
);
في الرسم التوضيحي للمثال، حيث قمنا بضبط المنبه للمقاطعة كل 10 ثوانٍ، لا يهم سوى معلمات A1Second وAlarmBits. ومع ذلك، نحتاج إلى توفيرها جميعًا عندما نستدعي الدالة setA1Time()
. إن توفير القيم الصحيحة ليس أكثر صعوبة من توفير القيم غير المرغوب فيها؛ يمكننا كذلك أن نهتم بهم.
تعمل الدالة setA2Time()
بشكل مشابه، ولكن بدون معلمة للثواني. خذ بعض الوقت لمراجعة الأسطر من 119 إلى 145 من ملف DS3231.h الموجود في المكتبة والصفحات من 11 إلى 12 في ورقة البيانات. اصبر على هذه المراجع حتى تجد فيها المعلومات التي تحتاجها لضبط وقت المنبه.
بعد ضبط الوقت، يجب أن يتخذ الرسم إجراءً إضافيًا لتمكين التنبيه في DS3231. أنا أشجع القارئ باستمرار على اتباع تسلسل من ثلاث خطوات، حتى لو كانت بعض الخطوات قد تبدو أقل أهمية في وقت ما لسبب ما. إذا كان يجب أن يخطئ الكود الخاص بك، دعه يخطئ على جانب اليقين.
بالنسبة للإنذار A1، ستكون التعليمات الموجودة في مكتبة DS3231 كما يلي:
turnOffAlarm(1); // clear the A1 enable bit in register 0Eh
checkIfAlarm(1); // clear the A1 alarm flag bit in register 0Fh
turnOnAlarm(1); // set the A1 enable bit in register 0Eh
بالنسبة للإنذار A2، ما عليك سوى تغيير المعلمة إلى 2. على سبيل المثال: checkIfAlarm(2); // clear A2 flag bit in register 0Fh
.
تؤكد المشكلة الموضحة في هذا الريبو بواسطة @flowmeter على أنه يجب مسح علامتي الإنذار قبل أن يتمكن DS3231 من الإشارة إلى إنذار. للتأكد، فكر في استدعاء checkIfAlarm() مرتين، مرة واحدة لكل إنذار، حتى لو كنت تستخدم واحدًا فقط من الإنذارات :
checkIfAlarm(1);
checkIfAlarm(2);
لماذا يختار مؤلفو الأكواد "التحقق" من الإنذار الذي يعتقدون أنه لا يرسل إشارة حاليًا؟ والسبب هو أن الدالة checkIfAlarm()
لها آثار جانبية غير واضحة. يقوم بمسح بت إشارة الإنذار. نستخدم الدالة checkIfAlarm()
لأنها الوحيدة في مكتبة DS3231 التي تقوم بالعملية اللازمة.
فكر في الأمر. للأسباب التي سيتم شرحها أدناه، تحتاج أجهزة استشعار المقاطعة في Arduino إلى أن يكون الجهد الكهربي الموجود على طرف SQW الخاص بـ DS3231 مرتفعًا قبل لحظة حدوث الإنذار. يغير حدث التنبيه شيئين داخل DS3231:
سيظل طرف SQW منخفضًا طالما بقيت إحدى بتات إشارة الإنذار هذه مضبوطة. طالما أن بت إشارة الإنذار داخل DS3231 تحمل دبوس SQW LOW، فلن يتمكن Arduino من استشعار المزيد من الإنذارات. يجب مسح بتات إشارة الإنذار حتى يتمكن DS3231 من استعادة الجهد العالي على دبوس إنذار SQW الخاص به. راجع مناقشة البتين 1 و0 في "سجل الحالة (0Fh)" في الصفحة 14 من ورقة البيانات DS3231.
يحتوي كل إنذار على وحدة إشارة إنذار خاصة به داخل DS3231. يمكن لأي من بتات إشارة الإنذار أن تحمل دبوس SQW LOW. لن يقوم DS3231 بمسح بت إشارة الإنذار من تلقاء نفسه. إن مهمة كاتب الكود هي مسح بتات إشارة الإنذار بعد حدوث الإنذار .
لا تحتاج حلقتك الرئيسية إلى قياس الوقت. يحتاج فقط إلى التحقق من العلم لمعرفة ما إذا كان الإنذار قد حدث أم لا. في الرسم التوضيحي المثال، هذه العلامة عبارة عن متغير منطقي يسمى "alarmEventFlag":
if (alarmEventFlag == true) {
// run the special code
}
في معظم الأحيان، ستكون العلامة خاطئة ، وستتخطى الحلقة الكود الخاص. كيف يقوم الرسم بإعداد العلم؟ ثلاث خطوات:
bool alarmEventFlag = false;
void rtcISR() {alarmEventFlag = true;}
attachInterrupt()
التي يوفرها Arduino IDE بجمع كل ذلك معًا. يخبر المثال التالي أجهزة Arduino بتشغيل وظيفة rtcISR()
فورًا عندما تكتشف إشارة "سقوط" على دبوس رقمي معين.attachInterrupt(digitalPinToInterrupt(dataPin), rtcISR, FALLING);
لأسباب عميقة وغامضة، استخدم دائمًا الوظيفة الخاصة، digitalPinToInterrupt()
، عند تحديد الرقم السري للمقاطعة. أترك الأمر كتمرين للقارئ ليكتشف سبب حاجتنا إلى هذه الوظيفة.
ما هي إشارة السقوط؟ إنه يعني تغير الجهد من المستوى المنخفض إلى المستوى العالي، كما تم اكتشافه بواسطة المنفذ الرقمي للاردوينو. من أين يأتي تغير الجهد؟ ينشأ على دبوس التنبيه لوحدة DS3231. يُسمى هذا الدبوس بـ SQW، وهو يصدر جهدًا عاليًا بالقرب من مستوى إمداد VCC (أي 5 فولت على Uno) في معظم الأوقات. يتسبب الإنذار في قيام DS3231 بتغيير الجهد الكهربي على طرف SQW إلى LOW. يستشعر Arduino الجهد القادم من طرف SQW ويلاحظ التغيير. نطلب من Arduino أن يلاحظ السقوط لأن هذا الحدث يحدث مرة واحدة فقط لكل إنذار، في حين أن المستوى المنخفض يمكن أن يستمر ويربك Arduino مما يؤدي إلى حدوث العديد من المقاطعات.
أي طرف يمكنه استشعار التغير الهبوطي في الجهد؟ بالنسبة لـ Unos، يمكنك اختيار أحد الأطراف 2 أو 3. بالنسبة لليوناردو، يمكن أن يكون أيًا من الأطراف 0 أو 1 أو 7. (نعم، أعلم أن ليوناردو يستشعر المقاطعات على الأطراف 2 و3 أيضًا. ومع ذلك، فهذه هي دبابيس I2C الخاصة بـ ليوناردو، مما يعني أن وحدة DS3231 ستستخدمها، سأبدأ بالدبوس 7 للمقاطعات على ليوناردو.) يحدد الرسم التوضيحي أ dataPin ويقوم بتهيئة قيمته إلى 3 للتشغيل على Uno بهذه الطريقة:
int dataPin = 3;
يمكن للرمز الخاص أيضًا ضبط وقت تنبيه جديد، إذا كنت تريد تكرار الدورة. ابدأ بحساب وقت التنبيه الجديد، كما هو موضح في الخطوة 3، واتبع تسلسل الخطوات من هناك.
أتوقع أن ينتج عن الرسم التوضيحي مخرجات مشابهة للرسم التوضيحي أدناه، عندما يتم تشغيله على Arduino Uno المتصل بشكل صحيح بوحدة DS3231، بالطريقة التي أصفها بها في هذا البرنامج التعليمي. لا تتفاجأ إذا كانت الأوقات المعروضة مختلفة. يجب عليك العمل مع الأوقات الخاصة بك، على أي حال.