โปรเจกต์ ตัวตั้งค่า Arduino Real Time Clock (RTC)
การตั้งค่าแบบง่ายๆ โดยใช้ Arduino Nano และ LCD Keypad Shield เพื่อตั้งค่าวันที่ เวลา และการแจ้งเตือนสำหรับ DS3231 Real Time Clock (RTC)
การตั้งค่าแบบง่ายๆ โดยใช้ Arduino Nano และ LCD Keypad Shield เพื่อตั้งค่าวันที่ เวลา และการแจ้งเตือนสำหรับ DS3231 Real Time Clock (RTC)
▶ กดเพื่อดูวิดีโอสาธิตโปรเจกต์
เมื่อเร็วๆ นี้ ผมได้พัฒนาโปรเจกต์ใหม่ที่ต้องมีการเช็ค Sensor ในช่วงเวลาที่กำหนดของวัน ตัวเครื่องจะใช้พลังงานจาก Battery ผมไม่อยากให้มีการนับเวลาอย่างต่อเนื่อง จึงเลือกใช้ Real Time Clock (RTC) เพื่อกระตุ้น Interrupt pin ในเวลาที่เจาะจงของแต่ละวัน
ผมมองเห็นแอปพลิเคชันมากมายที่สามารถนำ RTC ไปใช้งานได้ และเริ่มสงสัยว่าจะจัดการพวกมันอย่างไรดี
เช่นเดียวกับโปรเจกต์ Arduino ทั่วไปที่ไม่มี User Interface อัตโนมัติ และผมต้องการวิธีตั้งค่านาฬิกาโดยไม่ต้องประกอบ Board และเสียบเข้ากับ Laptop ทุกครั้ง ผมจึงคิดว่าจะใช้ LCD พร้อม Keypad และสร้าง Interface ง่ายๆ เพื่อตั้งค่า RTC ขึ้นมา
บางทีผมอาจจะใช้ Code นี้ในโปรเจกต์อื่นๆ หรืออาจจะแค่ตั้งค่า RTC ก่อนจะนำไปใส่ในโปรเจกต์ก็ได้ ใครจะรู้ แต่มันดูเหมือนจะเป็นโปรเจกต์ที่ดี
ในการทำโปรเจกต์นี้มีสิ่งที่ต้องเรียนรู้ (Learning Curve) ดังนี้:
ตอนนี้ผมมี Code ที่ใช้งานได้แล้ว พร้อมกับ Housing ที่พิมพ์จาก 3D printed แบบง่ายๆ เพื่อยึด Shield, Arduino Nano และ Breadboard
DS3231 มีความสามารถในการจัดเก็บข้อมูลดังนี้:
(ดูข้อมูลรายละเอียดเพิ่มเติมได้ที่ https://www.digikey.co.uk/en/datasheets/maxim-integrated/maxim-integrated-ds3231-ds3231s)
สำหรับการใช้งานของผม ผมได้ตัดสินใจบางอย่าง:
การตัดสินใจแรกที่ต้องทำคือลำดับของ Option สำหรับผู้ใช้ หน้าจอ LCD มี 2 บรรทัด ดังนั้นแต่ละขั้นตอนต้องรองรับสิ่งนั้น ผมไม่อยากให้ LCD เลื่อนบรรทัดใหม่ขึ้นหรือลงทุกครั้ง ผมเลยคิดว่ามันจะดีกว่าถ้าแต่ละหน้าจอที่แสดงออกมามีหัวข้อ (Theme) ของตัวเอง
ตอนเริ่มสร้างโปรเจกต์ครั้งแรก ผมใช้ลำดับดังนี้:
แต่วิธีนี้ใช้หลายขั้นตอนมากในการกลับจากหน้าแสดง Alarm 1 ไปยัง Date & Time และจากการแสดง Alarm 2 ไปยังการตั้งค่า Alarm 2 ผมจึงเปลี่ยนลำดับใหม่:
สำหรับโปรเจกต์นี้ ผมใช้ Library ดังต่อไปนี้:
ผมยังคงการต่อสายไฟสำหรับ LCD เหมือนกับตอนที่เสียบเข้ากับ Arduino UNO ดังนั้น Code สำหรับการกำหนดค่าเริ่มต้นจะเป็นดังนี้:
#include <DS3231.h>
#include <Wire.h>
#include <LiquidCrystal.h>
DS3231 Clock;
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);
void setup() {
\t// Start the lcd and set the cursor
\tlcd.begin(16,2);
\tlcd.setCursor(0,0);
\t// Start the I2C interface
\tWire.begin();
}
โชคดีที่ผมสามารถหา Code ทั้งหมดที่จำเป็นสำหรับ Keypad ได้จาก Datasheet ที่ https://media.digikey.com/pdf/Data%20Sheets/DFRobot%20PDFs/DFR0009_Web.pdf. มีวิดีโอมากมายบน YouTube ที่อธิบายการทำงานของ Keypad เหล่านี้ แต่โดยพื้นฐานแล้ว แต่ละปุ่มจะให้แรงดันไฟฟ้าที่แตกต่างกันไปยัง Analogue Pin A0 และจากค่าของ Pin นี้ Code จะสามารถระบุได้ว่าปุ่มใดถูกกด
ผมใช้ Variable ชุดเดิมสำหรับแต่ละปุ่มที่ถูกกด มีข้อมูลมากมายบนอินเทอร์เน็ตเกี่ยวกับการจัดการ Button Bounce ผมควบคุมมันด้วย Debounce Delay ขนาด 50ms หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับ Debounce ลองดูวิดีโอนี้:
ผมยังเพิ่ม Variable เพื่อเก็บค่าปุ่มล่าสุดที่กด เพื่อหลีกเลี่ยงปัญหาการกดปุ่มค้างไว้ เนื่องด้วย Arduino Loop ทำงานบ่อยมาก ทุกครั้งที่เข้าสู่ Loop หลังจาก Debounce Delay หากไม่มี Code ส่วนนี้ มันจะทำงานเสมือนว่ามีการกดปุ่มใหม่ตลอดเวลา
// check the key press against the previous key press to avoid issues from long key presses
if (oldKey!=lcd_key) {
// depending on which button was pushed, we perform an action
switch (lcd_key)
{
case btnRIGHT:
ด้านล่างนี้คือ Code เริ่มต้นสำหรับการลำดับหน้าจอแสดงผล บล็อกแรกนำมาจาก Datasheet โดยตรง:
/ define some values used by the panel and buttons
int lcd_key = 0;
int adc_key_in = 0;
const int btnRIGHT = 0;
const int btnUP =1;
const int btnDOWN =2;
const int btnLEFT =3;
const int btnSELECT =4;
const int btnNONE =5;
int bounceDelay;
int oldKey = 0;
/***************************************************************
* Functions to read the buttons
***************************************************************/
// read the buttons
int read_LCD_buttons()
{
adc_key_in = analogRead(0); // read the value from the sensor
// my buttons when read are centered at these valies: 0, 144, 329, 504, 741
// we add approx 50 to those values and check to see if we are close
if (adc_key_in > 1000) return btnNONE; // We make this the 1st option for speed reasons since it will be the most likely result
// For V1.1 us this threshold
if (adc_key_in < 50) return btnRIGHT;
if (adc_key_in < 250) return btnUP;
if (adc_key_in < 450) return btnDOWN;
if (adc_key_in < 650) return btnLEFT;
if (adc_key_in < 850) return btnSELECT;
return btnNONE; // when all others fail, return this...
}
void loop() {
//Set the menu item or display text for the user
setMenu();
// read the buttons
lcd_key = read_LCD_buttons();
// check the key press against the previous key press to avoid issues from long key presses
if (oldKey!=lcd_key) {
// depending on which button was pushed, we perform an action
switch (lcd_key)
{
case btnRIGHT:
{
oldKey = btnRIGHT;
if (currentMode==modeSHOWDATETIME){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSHOWDATETIME;}
delay(50);
break;
}
case btnLEFT:
{
oldKey = btnLEFT;
delay(50);
break;
}
case btnUP:
{
oldKey = btnUP;
delay(50);
break;
}
case btnDOWN:
{
oldKey = btnDOWN;
delay(50);
break;
}
case btnSELECT:
{
\t\toldKey = btnSELECT;
if (currentMode==modeSHOWDATETIME){currentMode=modeSETDATE;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSETALARM1ON;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSETALARM2ON;}
else if (currentMode==modeSETDATE){currentMode=modeSETTIME;}
else if (currentMode==modeSETTIME){currentMode=modeSHOWDATETIME;}
else if (currentMode==modeSETALARM1ON && Clock.checkAlarmEnabled(1)){currentMode=modeSETALARM1;}
else if (currentMode==modeSETALARM1ON && !Clock.checkAlarmEnabled(1)){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM1){currentMode=modeSETALARM1METHOD;}
else if (currentMode==modeSETALARM1METHOD){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM2ON && Clock.checkAlarmEnabled(2)){currentMode=modeSETALARM2;}
else if (currentMode==modeSETALARM2ON && !Clock.checkAlarmEnabled(2)){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSETALARM2){currentMode=modeSETALARM2METHOD;}
else if (currentMode==modeSETALARM2METHOD){currentMode=modeSHOWALARM2;};
break;
}
case btnNONE:
{
oldKey=btnNONE;
break;
}
}
}
}
เมื่อผมเข้าสู่โหมดการปรับแต่ง ผมต้องการวิธีที่จะแสดงให้ผู้ใช้เห็นว่าส่วนไหนของหน้าจอที่พวกเขาสามารถปรับได้ด้วยปุ่มลูกศรขึ้นหรือลง ผมแก้ปัญหานี้ด้วย 4 ส่วน:
Global variables:
bool blinkNow=false;
int blinkInt=1;
int maxBlinkInt=3;
uint32_t blinkStart=0;
uint32_t blinkDelay=500;
Code ที่เพิ่มเข้าไปใน Loop() function:
void loop() {
//Set the menu item or display text for the user
setMenu();
// read the buttons
lcd_key = read_LCD_buttons();
// check the blink counter isn't too high
if (blinkInt > maxBlinkInt){blinkInt=maxBlinkInt;}
// Set the current blink status
if (currentMode>modeSHOWALARM2 && ((millis()-blinkStart)>blinkDelay)){
blinkNow=!blinkNow;
blinkStart=millis();
}
else if (currentMode<=modeSHOWALARM2)
{blinkNow=false;}
// check the key press against the previous key press to avoid issues from long key presses
if (oldKey!=lcd_key) {
สังเกตจาก Code ด้านบน ผมต้องพิจารณาสถานะการกะพริบก็ต่อเมื่อโหมดปัจจุบันไม่ใช่แค่โหมดแสดงผล (currentMode>modeSHOWALARM2)
นี่คือฟังก์ชัน Setup และ Loop แบบเต็ม:
void setup() {
// Start the serial port
Serial.begin(9600);
Serial.println("Starting");
// Set the bounce delay
bounceDelay=50;
// Start the lcd and set the cursor
lcd.begin(16,2);
lcd.setCursor(0,0);
// Start the I2C interface
Wire.begin();
}
void loop() {
//Set the menu item or display text for the user
setMenu();
// read the buttons
lcd_key = read_LCD_buttons();
// check the blink counter isn't too high
if (blinkInt > maxBlinkInt){blinkInt=maxBlinkInt;}
// Set the current blink status
if (currentMode>modeSHOWALARM2 && ((millis()-blinkStart)>blinkDelay)){
blinkNow=!blinkNow;
blinkStart=millis();
}
else if (currentMode<=modeSHOWALARM2)
{blinkNow=false;}
// check the key press against the previous key press to avoid issues from long key presses
if (oldKey!=lcd_key) {
// depending on which button was pushed, we perform an action
switch (lcd_key)
{
case btnRIGHT:
{
oldKey = btnRIGHT;
if (blinkInt<maxBlinkInt) {blinkInt+=1;}
if (currentMode==modeSHOWDATETIME){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSHOWDATETIME;}
delay(50);
break;
}
case btnLEFT:
{
oldKey = btnLEFT;
if (blinkInt>1) {blinkInt-=1;}
delay(50);
break;
}
case btnUP:
{
oldKey = btnUP;
if (currentMode==modeSETDATE){
if (blinkInt==1){increaseDate();}
if (blinkInt==2){increaseMonth();}
if (blinkInt==3){increaseYear();}
}
if (currentMode==modeSETTIME){
if (blinkInt==1){increaseHour();}
if (blinkInt==2){increaseMinute();}
}
if (currentMode==modeSETALARM1ON) {
if (Clock.checkAlarmEnabled(1)) {AlarmOn(1, false);}
}
if (currentMode==modeSETALARM1) {
if (blinkInt==1){changeAlarmDayOption(1);}
if (blinkInt==2){ChangeAlarm(1, 1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(1, 0, 1, 0,0);}
if (blinkInt==4){ChangeAlarm(1, 0, 0, 1,0);}
if (blinkInt==5){ChangeAlarm(1, 0, 0, 0,1);}
}
if (currentMode==modeSETALARM1METHOD) {
changeAlarmMethod(1, 1);
}
if (currentMode==modeSETALARM2ON) {
if (Clock.checkAlarmEnabled(2)) {AlarmOn(2, false);}
}
if (currentMode==modeSETALARM2) {
if (blinkInt==1){changeAlarmDayOption(2);}
if (blinkInt==2){ChangeAlarm(2, 1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(2, 0, 1, 0,0);}
if (blinkInt==4){ChangeAlarm(2, 0, 0, 1,0);}
if (blinkInt==5){ChangeAlarm(2, 0, 0, 0,1);}
}
if (currentMode==modeSETALARM2METHOD) {
changeAlarmMethod(2, 1);
}
delay(50);
break;
}
case btnDOWN:
{
oldKey = btnDOWN;
if (currentMode==modeSETDATE){
if (blinkInt==1){decreaseDate();}
if (blinkInt==2){decreaseMonth();}
if (blinkInt==3){decreaseYear();}
}
if (currentMode==modeSETTIME){
if (blinkInt==1){decreaseHour();}
if (blinkInt==2){decreaseMinute();}
}
if (currentMode==modeSETALARM1ON) {
if (!Clock.checkAlarmEnabled(1)) {AlarmOn(1, true);}
}
if (currentMode==modeSETALARM1) {
if (blinkInt==1){changeAlarmDayOption(1);}
if (blinkInt==2){ChangeAlarm(1, -1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(1, 0, -1, 0,0);}
if (blinkInt==4){ChangeAlarm(1, 0, 0, -1,0);}
if (blinkInt==5){ChangeAlarm(1, 0, 0, 0,-1);}
}
if (currentMode==modeSETALARM1METHOD) {
changeAlarmMethod(1, 0);
}
if (currentMode==modeSETALARM2ON) {
if (!Clock.checkAlarmEnabled(2)) {AlarmOn(2, true);}
}
if (currentMode==modeSETALARM2) {
if (blinkInt==1){changeAlarmDayOption(2);}
if (blinkInt==2){ChangeAlarm(2, -1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(2, 0, -1, 0,0);}
if (blinkInt==4){ChangeAlarm(2, 0, 0, -1,0);}
if (blinkInt==5){ChangeAlarm(2, 0, 0, 0,-1);}
}
if (currentMode==modeSETALARM2METHOD) {
changeAlarmMethod(2, 0);
}
delay(50);
break;
}
case btnSELECT:
{
blinkInt=1;
oldKey = btnSELECT;
if (currentMode==modeSHOWDATETIME){currentMode=modeSETDATE;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSETALARM1ON;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSETALARM2ON;}
else if (currentMode==modeSETDATE){currentMode=modeSETTIME;}
else if (currentMode==modeSETTIME){currentMode=modeSHOWDATETIME;}
else if (currentMode==modeSETALARM1ON && Clock.checkAlarmEnabled(1)){currentMode=modeSETALARM1;}
else if (currentMode==modeSETALARM1ON && !Clock.checkAlarmEnabled(1)){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM1){currentMode=modeSETALARM1METHOD;}
else if (currentMode==modeSETALARM1METHOD){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM2ON && Clock.checkAlarmEnabled(2)){currentMode=modeSETALARM2;}
else if (currentMode==modeSETALARM2ON && !Clock.checkAlarmEnabled(2)){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSETALARM2){currentMode=modeSETALARM2METHOD;}
else if (currentMode==modeSETALARM2METHOD){currentMode=modeSHOWALARM2;};
break;
}
case btnNONE:
{
oldKey=btnNONE;
break;
}
}
}
}
จากการอ่าน Tips and Tricks ของ BaldEngineer ผมสังเกตเห็นว่าทางที่ดีที่สุดคือการส่งข้อความทั้งบรรทัดไปยัง LCD เพื่อเขียนทับข้อมูลเดิมทั้งหมด เพราะบางครั้งข้อมูลเก่าอาจค้างอยู่ในเซลล์ ตัวอย่างเช่น ถ้าผมส่งบรรทัดที่มีค่า 108 แล้วอัปเดตด้วยข้อความว่า 22 มันจะกลายเป็น 228 เพราะ LCD ไม่ได้เขียนทับเลข 8 ที่เซลล์ที่ 3
วิธีแก้คือสร้างฟังก์ชันสำหรับเขียนข้อความลงบน LCD:
void displayText(String line0Text, String line1Text){
lcd.setCursor(0,0);
sprintf(line0,"%-21s", line0Text.c_str());
lcd.print(String(line0));
lcd.setCursor(0,1);
sprintf(line1,"%-21s", line1Text.c_str());
lcd.print(String(line1));
}
ค่า "%-21s" จะสร้างช่องว่างต่อท้ายให้ครบ 21 ตัวอักษร
Code line1Text.c_str() จะแปลง String ให้เป็นตัวอักษรที่จำเป็นสำหรับ sprintf function
นอกจากนี้ยังมีปัญหาในการส่งตัวเลขไปยัง LCD ซึ่งมักจะแสดงผลเป็นเลข 10 เสมอ ปัญหานี้แก้ได้ด้วยอีกฟังก์ชันที่ผมคัดลอกมาจากอินเทอร์เน็ตซึ่งมีการอ้างอิงถึงบ่อยและช่วยแก้ปัญหานี้ได้:
String twoDigitNumber(byte number)
{
char buffer[3];
snprintf(buffer,sizeof(buffer), "%02d", number );
return String(buffer);
}
การแสดงและปรับวันที่และเวลาโดยใช้ DS3231 และ LiquidCrystal Library ตอนนี้ค่อนข้างทำได้ง่ายขึ้น
มี 3 ฟังก์ชันสำหรับแสดงข้อความ:
ที่เป็นแบบนี้เพื่อให้ DateText และ TimeText สามารถนำกลับมาใช้ใหม่ได้เมื่อมีการปรับวันที่และเวลา ฟังก์ชันเหล่านี้จะอ่านค่าของ blinkInt และสถานะของ blinkNow เพื่อกำหนดว่าควรซ่อนข้อความส่วนใดเพื่อให้ดูเหมือนมีการกะพริบ
String dateText() {
String result="Date: ";
if (blinkInt!=1 || blinkNow==false)
{result+=twoDigitNumber(Clock.getDate());}
else
{result+=" ";}
result+="/";
if (blinkInt!=2 || blinkNow==false)
{result+=twoDigitNumber(Clock.getMonth(Century));}
else
{result+=" ";}
result+="/";
if (blinkInt!=3 || blinkNow==false)
{result+=twoDigitNumber(Clock.getYear());}
else
{result+=" ";}
return result;
}
String timeText() {
String result="Time: ";
if (blinkInt!=1 || blinkNow==false)
{result+=twoDigitNumber(Clock.getHour(h12, PM));}
else
{result+=" ";}
result+=":";
if (blinkInt!=2 || blinkNow==false)
{result+=twoDigitNumber(Clock.getMinute());}
else
{result+=" ";}
result+=":";
result+=twoDigitNumber(Clock.getSecond());
return result;
}
void showDateTime(){
displayText(dateText(), timeText());
}
สองฟังก์ชันแยกกันที่ชื่อ setDate และ setTime จะทำหน้าที่ปรับการแสดงผลและตั้งค่า Variable maxBlink สำหรับเวลา ผมไม่ได้เผื่อไว้สำหรับการตั้งค่าวินาทีด้วยตัวเอง เมื่อมีการเปลี่ยนนาที วินาทีจะกลายเป็น 0 โดยอัตโนมัติ ผมไม่ต้องการให้นาฬิกาแม่นยำขนาดนั้น หากคุณต้องการ คุณสามารถแก้ไขได้ง่ายๆ แต่อาจจะต้องคำนึงถึงความล่าช้า (Lag) ในการประมวลผลคำสั่งด้วย
void setDate(){
displayText("Set the date:", dateText());
maxBlinkInt=3;
}
void setTime(){
displayText("Set the time:", timeText());
maxBlinkInt=2;
}
ปุ่มขึ้น, ลง และขวาจะเป็นตัวเรียกฟังก์ชันที่จำเป็นในการเปลี่ยนวันที่และเวลา ปุ่มขวาจะเปลี่ยนค่า blinkInt
case btnRIGHT:
{
oldKey = btnRIGHT;
if (blinkInt<maxBlinkInt) {blinkInt+=1;}
if (currentMode==modeSHOWDATETIME){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSHOWDATETIME;}
delay(50);
break;
}
ปุ่มขึ้นและลงจะทำหน้าที่ปรับค่า
case btnUP:
{
oldKey = btnUP;
if (currentMode==modeSETDATE){
if (blinkInt==1){increaseDate();}
if (blinkInt==2){increaseMonth();}
if (blinkInt==3){increaseYear();}
}
if (currentMode==modeSETTIME){
if (blinkInt==1){increaseHour();}
if (blinkInt==2){increaseMinute();}
}
case btnDOWN:
{
oldKey = btnDOWN;
if (currentMode==modeSETDATE){
if (blinkInt==1){decreaseDate();}
if (blinkInt==2){decreaseMonth();}
if (blinkInt==3){decreaseYear();}
}
if (currentMode==modeSETTIME){
if (blinkInt==1){decreaseHour();}
if (blinkInt==2){decreaseMinute();}
}
ฟังก์ชันในการเพิ่มส่วนประกอบต่างๆ ของวันที่และเวลาจะใช้ DS3231 Library หนึ่งในการตัดสินใจของผมคือการให้มันวนกลับมาที่ค่าเริ่มต้น (Scrolling) แทนที่จะหยุดอยู่ที่ค่าสุดท้าย ตัวอย่างเช่น สำหรับนาที แทนที่จะหยุดที่ 59 ผมอนุญาตให้มันวนจาก 59 ไปที่ 0 ได้เลย นอกจากนี้ผมยังคำนึงถึงจำนวนวันสูงสุดในแต่ละเดือน ไม่ใช่แค่ตอนตั้งค่าวัน แต่รวมถึงตอนตั้งค่าเดือนหรือปีด้วย เผื่อในกรณีที่วันถูกตั้งไว้ที่ 31 แล้วผู้ใช้ปรับเปลี่ยนเดือนเป็นเดือนที่มีเพียง 30 วันหรือน้อยกว่า รวมถึงมีการปรับแก้สำหรับปีอธิกสุรทิน (Leap Years) ด้วย
/***************************************************************
* Functions to increase and decrease time elements
***************************************************************/
void increaseYear(){
Year=Clock.getYear();
if (Year<99)
{Year = Year + 1;}
else
{Year = 00;}
Clock.setYear(Year);
if (Clock.getDate()>monthMaxDays(Clock.getMonth(Century))){
Clock.setDate(monthMaxDays(Clock.getMonth(Century)));
}
}
void decreaseYear(){
Year=Clock.getYear();
if (Year>1)
{Year = Year - 1;}
else
{Year = 99;}
Clock.setYear(Year);
if (Clock.getDate()>monthMaxDays(Clock.getMonth(Century))){
Clock.setDate(monthMaxDays(Clock.getMonth(Century)));
}
}
void increaseMonth(){
Month=Clock.getMonth(Century);
if (Month<12) {
Month = Month + 1;
}
else
{
Month = 1;
}
Clock.setMonth(Month);
if (Clock.getDate()>monthMaxDays(Clock.getMonth(Century))){
Clock.setDate(monthMaxDays(Clock.getMonth(Century)));
}
}
void decreaseMonth(){
Month=Clock.getMonth(Century);
if (Month>1) {
Month = Month - 1;
}
else
{
Month = 12;
}
Clock.setMonth(Month);
if (Clock.getDate()>monthMaxDays(Clock.getMonth(Century))){
Clock.setDate(monthMaxDays(Clock.getMonth(Century)));
}
}
void increaseDate(){
Date=Clock.getDate();
if (Date<monthMaxDays(Clock.getMonth(Century))) {
Date = Date + 1;
}
else
{
Date = 1;
}
Clock.setDate(Date);
}
void decreaseDate(){
Date=Clock.getDate();
if(Date>1) {
Date = Date- 1;
}
else {
Date = monthMaxDays(Clock.getMonth(Century));
}
Clock.setDate(Date);
}
void increaseHour(){
Hour=Clock.getHour(h12, PM);
if (Hour<24)
{Hour = Hour + 1;}
else
{Hour = 1;}
Clock.setHour(Hour);
}
void decreaseHour(){
Hour=Clock.getHour(h12, PM);
if (Hour>1)
{Hour = Hour - 1;}
else
{Hour = 24;}
Clock.setHour(Hour);
}
void increaseMinute(){
Minute=Clock.getMinute();
if (Minute<60)
{Minute = Minute + 1;}
else
{Minute = 1;}
Clock.setMinute(Minute);
Clock.setSecond(0);
}
void decreaseMinute(){
Minute=Clock.getMinute();
if (Minute>0)
{Minute = Minute - 1;}
else
{Minute = 60;}
Clock.setMinute(Minute);
Clock.setSecond(0);
}
int monthMaxDays(int monthNumber){
switch (monthNumber){
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
int remainingYears;
remainingYears=((Clock.getYear()-2020)%4);
if (remainingYears==0){
return 29;
}
else{
return 28;
}
default:
return 0;
}
}
การตั้งค่าเปิดหรือปิด Alarm นั้นค่อนข้างง่าย มี 3 ฟังก์ชันใน DS3231 Library คือ turnOnAlarm, turnOffAlarm และ checkAlarmEnabled ผมใช้ฟังก์ชัน checkAlarmEnabled เพื่อตัดสินใจว่าผู้ใช้จำเป็นต้องเห็นหน้าจอเพื่อตั้งค่า Alarm ส่วนที่เหลือหรือไม่ เพราะถ้าผู้ใช้ปิด Alarm ไปแล้ว การตั้งค่าอื่นๆ ก็ดูจะไม่จำเป็น
case btnSELECT:
{
blinkInt=1;
oldKey = btnSELECT;
if (currentMode==modeSHOWDATETIME){currentMode=modeSETDATE;}
else if (currentMode==modeSHOWALARM1){currentMode=modeSETALARM1ON;}
else if (currentMode==modeSHOWALARM2){currentMode=modeSETALARM2ON;}
else if (currentMode==modeSETDATE){currentMode=modeSETTIME;}
else if (currentMode==modeSETTIME){currentMode=modeSHOWDATETIME;}
else if (currentMode==modeSETALARM1ON && Clock.checkAlarmEnabled(1)){currentMode=modeSETALARM1;}
else if (currentMode==modeSETALARM1ON && !Clock.checkAlarmEnabled(1)){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM1){currentMode=modeSETALARM1METHOD;}
else if (currentMode==modeSETALARM1METHOD){currentMode=modeSHOWALARM1;}
else if (currentMode==modeSETALARM2ON && Clock.checkAlarmEnabled(2)){currentMode=modeSETALARM2;}
else if (currentMode==modeSETALARM2ON && !Clock.checkAlarmEnabled(2)){currentMode=modeSHOWALARM2;}
else if (currentMode==modeSETALARM2){currentMode=modeSETALARM2METHOD;}
else if (currentMode==modeSETALARM2METHOD){currentMode=modeSHOWALARM2;};
break;
}
การกะพริบถูกตั้งค่าให้ตรงกับ Option การเปิดหรือปิดที่เลือกอยู่ในปัจจุบัน
void setAlarmOnOff(int alarmNum){
if (alarmNum>0 && alarmNum<3) {
maxBlinkInt=1;
if(Clock.checkAlarmEnabled(alarmNum)){
blinkInt=2;}
else {
blinkInt=1;}
if (blinkInt==1 && blinkNow==true)
{displayText("", "Alarm" + String(alarmNum) + ": ON");}
else if (blinkInt==2 && blinkNow==true)
{displayText("Alarm" + String(alarmNum) + ": OFF", "");}
else
{ displayText("Alarm" + String(alarmNum) + ": OFF", "Alarm" + String(alarmNum) + ": ON");}
}
}
จากนั้น Alarm จะถูกตั้งค่า:
void AlarmOn(int alarmNum, bool setOn){
if (alarmNum>0 && alarmNum<3) {
if (setOn){Clock.turnOnAlarm(alarmNum);}
else {Clock.turnOffAlarm(alarmNum);}
}
}
ตอนนี้เหลือเพียงสอง Option สำหรับแต่ละ Alarm ที่เราสามารถใช้เพื่อช่วยในการแสดงผลและตั้งค่า Alarm ได้:
getA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);
setA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);
getA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);
setA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);
คุณจะเห็นว่าความแตกต่างเพียงอย่างเดียวคือฟังก์ชัน A2 ไม่มี Variable สำหรับวินาที
ในการเปลี่ยน Alarm เราต้องรวบรวมการตั้งค่าปัจจุบันก่อน จากนั้นจึงเพิ่มค่าที่ถูกต้องเข้าไป ก่อนจะส่งคืนค่าเก่าทั้งหมดรวมถึงค่าใหม่หนึ่งค่า
ฟังก์ชัน setAlarm นั้นง่ายพอ ซึ่งทำหน้าที่เพียงเริ่มต้นการทำงานเท่านั้น:
void setAlarm(int alarmNum){
if (alarmNum>0 && alarmNum<3) {
if (alarmNum==1)
{
maxBlinkInt=5;
showAlarm1();
}
if (alarmNum==2)
{
maxBlinkInt=4;
showAlarm2();
}
}
}
เมื่อผู้ใช้กดปุ่มขึ้น เราจะเรียกใช้:
if (currentMode==modeSETALARM1) {
if (blinkInt==1){changeAlarmDayOption(1);}
if (blinkInt==2){ChangeAlarm(1, 1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(1, 0, 1, 0,0);}
if (blinkInt==4){ChangeAlarm(1, 0, 0, 1,0);}
if (blinkInt==5){ChangeAlarm(1, 0, 0, 0,1);}
}
และสำหรับปุ่มลง:
if (currentMode==modeSETALARM2) {
if (blinkInt==1){changeAlarmDayOption(2);}
if (blinkInt==2){ChangeAlarm(2, 1, 0, 0,0);}
if (blinkInt==3){ChangeAlarm(2, 0, 1, 0,0);}
if (blinkInt==4){ChangeAlarm(2, 0, 0, 1,0);}
if (blinkInt==5){ChangeAlarm(2, 0, 0, 0,1);}
}
ผมสร้างฟังก์ชันแยกกันสองฟังก์ชันสำหรับการเปลี่ยน Option วัน/วันที่ และสำหรับการเปลี่ยนเวลา เพื่อความง่ายในการอ่าน Code:
void changeAlarmDayOption(int alarmNum){
byte ADay, AHour, AMinute, ASecond, ABits;
bool ADy, A12h, Apm;
//Collect the current alarm settings
if (alarmNum==1){Clock.getA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);}
if (alarmNum==2){Clock.getA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);}
ADy=!ADy;
if (ADy && ADay>7) {ADay=7;}
//Reset the alarm settings
if (alarmNum==1){Clock.setA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);}
if (alarmNum==2){Clock.setA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);}
}
void ChangeAlarm(int alarmNum, int dayAdjust, int hourAdjust, int minAdjust,int secAdjust){
byte ADay, AHour, AMinute, ASecond, ABits;
bool ADy, A12h, Apm;
//Collect the current alarm settings
if (alarmNum==1){Clock.getA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);}
if (alarmNum==2){Clock.getA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);}
//Adjust the date
ADay+=dayAdjust;
if (ADy){
if (ADay<1){ADay=7;}
if (ADay>7){ADay=1;}
}
else {
if (ADay<1){ADay=31;}
if (ADay>31){ADay=1;}
}
//Adjust the hour
AHour+=hourAdjust;
if (AHour<0){AHour=23;}
if (AHour>23){AHour=0;}
//Adjust the minute
AMinute+=minAdjust;
if (AMinute<0){AMinute=59;}
if (AMinute>59){AMinute=0;}
//Adjust the second
if (alarmNum==1){
ASecond+=secAdjust;
if (ASecond<0){ASecond=59;}
if (ASecond>59){ASecond=0;}
}
//Reset the alarm settings
if (alarmNum==1){Clock.setA1Time(ADay, AHour, AMinute, ASecond, ABits, ADy, A12h, Apm);}
if (alarmNum==2){Clock.setA2Time(ADay, AHour, AMinute, ABits, ADy, A12h, Apm);}
}
ขั้นตอนก่อนหน้านี้ทั้งหมดค่อนข้างราบรื่น เป็นเรื่องของการค้นคว้าและนำสิ่งที่ได้เรียนรู้มาปรับใช้ แต่ไม่ใช่กับการตั้งค่าโหมดการแจ้งเตือน (Alarm Modes) นี่เป็นเรื่องที่น่าปวดหัวทีเดียว
โหมด Alarm จะถูกเก็บไว้ในหน่วยความจำในตำแหน่งต่างๆ และ DS3231 จะรวบรวมข้อมูลนี้ เก็บไว้ใน Byte และส่งคืนให้ผู้ใช้ ใน Code ก่อนหน้านี้ ข้อมูลนี้คือ Variable ABits
มีชุดของ Mask ที่ระบุไว้ใน Datasheet และผมได้คัดลอกค่าคงที่มาจาก https://github.com/mlepard/ArduinoChicken/blob/master/roboCoop/alarmControl.ino
// These are the ALARM Bits that can be used
// They need to be combined into a single value (see below)
// Found here: https://github.com/mlepard/ArduinoChicken/blob/master/roboCoop/alarmControl.ino
#define ALRM1_MATCH_EVERY_SEC 0b1111 // once a second
#define ALRM1_MATCH_SEC 0b1110 // when seconds match
#define ALRM1_MATCH_MIN_SEC 0b1100 // when minutes and seconds match
#define ALRM1_MATCH_HR_MIN_SEC 0b1000 // when hours, minutes, and seconds match
#define ALRM1_MATCH_DY_HR_MIN_SEC 0b0000 // when hours, minutes, and seconds match
#define ALRM2_ONCE_PER_MIN 0b111 // once per minute (00 seconds of every minute)
#define ALRM2_MATCH_MIN 0b110 // when minutes match
#define ALRM2_MATCH_HR_MIN 0b100 // when hours and minutes match
บางแห่งบนอินเทอร์เน็ตแนะนำว่าคุณต้องสร้าง Byte ขนาด 7 หลักและตั้งค่าทั้ง Alarm 1 และ 2 พร้อมกัน ผมพบว่ามันไม่เป็นความจริงจากการอ่าน Library ของ DS3231 โดย Alarm 1 จะอ่านสี่หลักแรก (จากทางขวา) ของ Byte ที่ส่งไป ส่วน Alarm 2 จะอ่านหลักที่ 5, 6 และ 7
สิ่งที่ผมพบจากการลองผิดลองถูกคือไม่ควรใช้ Global variables ผมไม่เข้าใจว่าทำไมในตอนแรก แต่มันดูเหมือนจะใช้งานได้
แต่ผมก็ยังประสบปัญหาอยู่ บางครั้งการเลื่อนดูสถานะปัจจุบันก็ใช้งานได้ บางครั้งสถานะก็อัปเดต แต่บางครั้งก็ไม่
หลังจากโพสต์ถามในฟอรัมออนไลน์ (https://forum.arduino.cc/index.php?topic=719176.0) cattledog ได้ชี้ให้เห็นว่าผมอาจจะมี "Dirty Bytes" ซึ่งอาจทำให้สิ่งที่ผมส่งไปยัง DS3231 Library ผิดเพี้ยนไป ผมจึงตั้งค่า Byte อย่างชัดเจน และแน่ใจว่าไม่ได้เรียกใช้ Variable ของ Byte ตัวเก่า และทุกอย่างก็ใช้งานได้
void setAlarmMethod(int alarmNum){
if (alarmNum>0 && alarmNum<3) {
if (alarmNum==1)
{
maxBlinkInt=1;
showAlarmMethod(1);
}
if (alarmNum==2)
{
maxBlinkInt=1;
showAlarmMethod(2);
}
}
}
void showAlarmMethod(int alarmNum) {
String myString1="";
String myString2="";
byte ADay, AHour, AMinute, ASecond, ABitsOP=0b0;
bool ADy, A12h, Apm;
if (alarmNum==1){
myString1 = "Alarm 1 Method:";
Clock.getA1Time(ADay, AHour, AMinute, ASecond, ABitsOP, ADy, A12h, Apm);
ABitsOP = ABitsOP & 0b1111;
if (ABitsOP==ALRM1_MATCH_EVERY_SEC) {myString2 = "Once per Second";}
else if (ABitsOP==ALRM1_MATCH_SEC) {myString2 = "Seconds Match";}
else if (ABitsOP==ALRM1_MATCH_MIN_SEC) {myString2 = "Min & Secs Match";}
else if (ABitsOP==ALRM1_MATCH_HR_MIN_SEC) {myString2 = "Hr, Min & Sec Match";}
else if (ABitsOP==ALRM1_MATCH_DY_HR_MIN_SEC) {myString2 = "Dy, Hr, Mn & Sec";}
} else {
Clock.getA2Time(ADay, AHour, AMinute, ABitsOP, ADy, A12h, Apm);
myString1 = "Alarm 2 Method:";
if ((ABitsOP>>4)==ALRM2_ONCE_PER_MIN) {myString2 = "Once per Minute";}
else if ((ABitsOP>>4)==ALRM2_MATCH_MIN) {myString2 = "Match Minute";}
else {myString2 = "Match Hour & Min";}
}
displayText(myString1 , myString2);
}
ในฟังก์ชัน changeAlarmMethod คุณจะเห็นว่า:
void changeAlarmMethod(int alarmNum, int dir) {
byte ADay1, AHour1, AMinute1, ASecond1, ADay2, AHour2, AMinute2, ABits=0b0;
bool ADy1, A12h1, Apm1, ADy2, A12h2, Apm2;
int AlarmBits;
if (alarmNum==1){
Clock.getA1Time(ADay1, AHour1, AMinute1, ASecond1, ABits, ADy1, A12h1, Apm1);
ABits = ABits & 0b1111;
if (dir == 1) {
if (ABits==ALRM1_MATCH_EVERY_SEC) {AlarmBits |= ALRM1_MATCH_SEC;}
else if (ABits==ALRM1_MATCH_SEC) {AlarmBits |= ALRM1_MATCH_MIN_SEC;}
else if (ABits==ALRM1_MATCH_MIN_SEC) {AlarmBits |= ALRM1_MATCH_HR_MIN_SEC;}
else if (ABits==ALRM1_MATCH_HR_MIN_SEC) {AlarmBits |= ALRM1_MATCH_DY_HR_MIN_SEC;}
else if (ABits==ALRM1_MATCH_DY_HR_MIN_SEC) {AlarmBits |= ALRM1_MATCH_EVERY_SEC;}
}
else if (dir == 0) {
if (ABits==ALRM1_MATCH_EVERY_SEC) {AlarmBits |= ALRM1_MATCH_DY_HR_MIN_SEC;}
else if (ABits==ALRM1_MATCH_SEC) {AlarmBits |= ALRM1_MATCH_EVERY_SEC;}
else if (ABits==ALRM1_MATCH_MIN_SEC) {AlarmBits |= ALRM1_MATCH_SEC;}
else if (ABits==ALRM1_MATCH_HR_MIN_SEC) {AlarmBits |= ALRM1_MATCH_MIN_SEC;}
else {AlarmBits |= ALRM1_MATCH_HR_MIN_SEC;}
}
else {AlarmBits |= ABits;}
Clock.setA1Time(ADay1, AHour1, AMinute1, ASecond1, AlarmBits, ADy1, A12h1, Apm1);
} else {
Clock.getA2Time(ADay2, AHour2, AMinute2, ABits, ADy2, A12h2, Apm2);
ABits = ABits >> 4;
if (dir == 1) {
if (ABits==ALRM2_ONCE_PER_MIN) {AlarmBits = ALRM2_MATCH_MIN;}
else if (ABits==ALRM2_MATCH_MIN) {AlarmBits = ALRM2_MATCH_HR_MIN;}
else {AlarmBits = ALRM2_ONCE_PER_MIN;}
}
if (dir == 0) {
if (ABits==ALRM2_ONCE_PER_MIN) {AlarmBits = ALRM2_MATCH_HR_MIN;}
else if (ABits==ALRM2_MATCH_HR_MIN) {AlarmBits = ALRM2_MATCH_MIN;}
else {AlarmBits = ALRM2_ONCE_PER_MIN;}
}
AlarmBits = AlarmBits << 4;
Clock.setA2Time(ADay2, AHour2, AMinute2, AlarmBits, ADy2, A12h2, Apm2);
byte newBits;
Clock.getA2Time(ADay2, AHour2, AMinute2, newBits, ADy2, A12h2, Apm2);
}
}
ค่าคงที่ที่ใช้สำหรับสถานะปัจจุบันแสดงอยู่ด้านล่าง
// define some values used by the menu controller
const int modeSHOWDATETIME = 0;
const int modeSHOWALARM1 = 1;
const int modeSHOWALARM2 = 2;
const int modeSETDATE = 3;
const int modeSETTIME = 4;
const int modeSETALARM1ON = 5;
const int modeSETALARM1 = 6;
const int modeSETALARM1METHOD = 7;
const int modeSETALARM2ON = 8;
const int modeSETALARM2 = 9;
const int modeSETALARM2METHOD = 10;
int currentMode = modeSHOWDATETIME;
ผมได้สร้าง Casing หรือที่ยึดแบบง่ายๆ สำหรับ Shield และ Breadboard มันถูกแยกเป็น 2 ชิ้นด้วยเหตุผลสองประการ ประการแรกคือกรอบรอบ LCD จำเป็นต้องมีส่วนรองรับ (Supports) และประการที่สอง ผมพบว่าตำแหน่งของปุ่มต่างๆ ค่อนข้างไม่แน่นอน ดังนั้นการออกแบบนี้จึงช่วยให้ใครก็ตามที่นำไปทำตามสามารถปรับแต่งหน้าจอ LCD ได้ ไฟล์ STL รวมอยู่ในการดาวน์โหลดแล้ว




หนึ่งในสิ่งที่ผมสังเกตเห็นกับ Code นี้คือปริมาณการใช้หน่วยความจำ ผมไม่เก่งเรื่องการทำความเข้าใจวิธีควบคุมหน่วยความจำนัก แต่ผมก็ได้ทำบางอย่างหลังจากเขียนคำแนะนำข้างต้น:
โดยรวมแล้ว ผมลดการใช้พื้นที่เก็บข้อมูลจาก 55% และ Global Variables ที่ใช้ Dynamic Memory 39% มาเป็นพื้นที่เก็บข้อมูล 53% และ Global Variables ยังคงใช้ Dynamic Memory 39% เหมือนเดิม
ขั้นตอนต่อไปเกี่ยวกับหน่วยความจำคือการลบการอ้างอิงถึง Serial ออกจากฟังก์ชัน SetUp สิ่งนี้ช่วยลดการใช้หน่วยความจำลงเหลือ 50% ของพื้นที่เก็บข้อมูล และ Global Variables ใช้ Dynamic Memory 30% ดังนั้นนี่จึงเป็นบทเรียนว่าควรใส่คอมเมนต์หรือลบ Debugging Statements ออกทั้งหมดเมื่อทำโปรเจกต์เสร็จเรียบร้อยแล้ว
Code ที่เสร็จสมบูรณ์แสดงอยู่ด้านล่าง หากคุณมีคำแนะนำในการปรับปรุงให้ดีขึ้น โปรดบอกให้ผมทราบด้วยนะครับ
เมื่อผมใช้ RTC ในโปรเจกต์ต่างๆ ผมพบปัญหาอยู่ 2 ประการ:
สนับสนุนเพื่อรับ Source Code หรือแอปพลิเคชันสำหรับโปรเจกต์นี้