หน้าแรก ดูโปรเจกต์ทั้งหมด
Intermediate

โปรเจกต์ ตัวตั้งค่า Arduino Real Time Clock (RTC)

การตั้งค่าแบบง่ายๆ โดยใช้ Arduino Nano และ LCD Keypad Shield เพื่อตั้งค่าวันที่ เวลา และการแจ้งเตือนสำหรับ DS3231 Real Time Clock (RTC)

โปรเจกต์ ตัวตั้งค่า Arduino Real Time Clock (RTC)

วิดีโอสาธิต

Video

▶ กดเพื่อดูวิดีโอสาธิตโปรเจกต์

รายการอุปกรณ์และเครื่องมือ

1x LCD Keypad Shield
-
1x DS3231M - ±5ppm, I2C Real-Time Clock
🛒 สั่งซื้อ
1x Solderless Breadboard Half Size
🛒 สั่งซื้อ
}

รายละเอียดและวิธีทำ

เมื่อเร็วๆ นี้ ผมได้พัฒนาโปรเจกต์ใหม่ที่ต้องมีการเช็ค Sensor ในช่วงเวลาที่กำหนดของวัน ตัวเครื่องจะใช้พลังงานจาก Battery ผมไม่อยากให้มีการนับเวลาอย่างต่อเนื่อง จึงเลือกใช้ Real Time Clock (RTC) เพื่อกระตุ้น Interrupt pin ในเวลาที่เจาะจงของแต่ละวัน

ผมมองเห็นแอปพลิเคชันมากมายที่สามารถนำ RTC ไปใช้งานได้ และเริ่มสงสัยว่าจะจัดการพวกมันอย่างไรดี

เช่นเดียวกับโปรเจกต์ Arduino ทั่วไปที่ไม่มี User Interface อัตโนมัติ และผมต้องการวิธีตั้งค่านาฬิกาโดยไม่ต้องประกอบ Board และเสียบเข้ากับ Laptop ทุกครั้ง ผมจึงคิดว่าจะใช้ LCD พร้อม Keypad และสร้าง Interface ง่ายๆ เพื่อตั้งค่า RTC ขึ้นมา

บางทีผมอาจจะใช้ Code นี้ในโปรเจกต์อื่นๆ หรืออาจจะแค่ตั้งค่า RTC ก่อนจะนำไปใส่ในโปรเจกต์ก็ได้ ใครจะรู้ แต่มันดูเหมือนจะเป็นโปรเจกต์ที่ดี

ในการทำโปรเจกต์นี้มีสิ่งที่ต้องเรียนรู้ (Learning Curve) ดังนี้:

  • ผมต้องเรียนรู้เกี่ยวกับ LCD
  • ผมต้องเรียนรู้เกี่ยวกับ Keypad ที่เชื่อมต่อกับ Analogue Pin เพียง Pin เดียว
  • ผมพบปัญหาเกี่ยวกับ LCD Keypad shields ราคาถูกทั่วไป
  • ผมต้องเรียนรู้เกี่ยวกับการจัดการ Byte และการกำหนดค่าเริ่มต้น (Initiating) ของพวกมัน

ตอนนี้ผมมี Code ที่ใช้งานได้แล้ว พร้อมกับ Housing ที่พิมพ์จาก 3D printed แบบง่ายๆ เพื่อยึด Shield, Arduino Nano และ Breadboard

DS3231 RTC

DS3231 มีความสามารถในการจัดเก็บข้อมูลดังนี้:

  • วันที่ โดยใช้ปีแบบเลขสองหลัก
  • วันปัจจุบันของสัปดาห์
  • เวลาในหน่วย ชั่วโมง, นาที และ วินาที
  • รูปแบบเวลาทั้งแบบ 24 ชั่วโมง หรือ 12 ชั่วโมง (พร้อมข้อมูล AM และ PM)
  • Alarm (Alarm 1) ที่สามารถตั้งค่าได้ตั้งแต่วันจนถึงวินาที และมีวิธีการกระตุ้น (Triggering) ที่แตกต่างกัน 5 วิธี
  • Alarm (Alarm 2) ที่สามารถตั้งค่าได้ตั้งแต่วันจนถึงนาที และมีวิธีการกระตุ้นที่แตกต่างกัน 3 วิธี

(ดูข้อมูลรายละเอียดเพิ่มเติมได้ที่ https://www.digikey.co.uk/en/datasheets/maxim-integrated/maxim-integrated-ds3231-ds3231s)

สำหรับการใช้งานของผม ผมได้ตัดสินใจบางอย่าง:

  • ผมไม่ค่อยสนใจนาฬิกาแบบ 12 ชั่วโมง ดังนั้นผมจะใช้เฉพาะโหมด 24 ชั่วโมงเท่านั้น
  • ผมอ่านวันที่ในรูปแบบ DD/MM/YY เท่านั้น ดังนั้นผมจึงไม่ให้ Option แก่ผู้ใช้ในการแสดงวันที่ในรูปแบบ MM/DD/YY หากคุณชอบแบบนั้น Code นี้ก็ปรับแก้ได้ง่าย แต่คุณต้องใส่ใจกับลำดับ (Sequencing) ในระหว่างขั้นตอนการตั้งค่าวันที่

Sequencing

การตัดสินใจแรกที่ต้องทำคือลำดับของ Option สำหรับผู้ใช้ หน้าจอ LCD มี 2 บรรทัด ดังนั้นแต่ละขั้นตอนต้องรองรับสิ่งนั้น ผมไม่อยากให้ LCD เลื่อนบรรทัดใหม่ขึ้นหรือลงทุกครั้ง ผมเลยคิดว่ามันจะดีกว่าถ้าแต่ละหน้าจอที่แสดงออกมามีหัวข้อ (Theme) ของตัวเอง

ตอนเริ่มสร้างโปรเจกต์ครั้งแรก ผมใช้ลำดับดังนี้:

  • Date & Time
  • Alarm 1
  • Alarm 2
  • Set Date
  • Set Time
  • Set Alarm 1 On or Off
  • Set Alarm 1 Date and/or time
  • Set Alarm 1 Method
  • Set Alarm 2 On or Off
  • Set Alarm 2 Date and/or time
  • Set Alarm 2 Method

แต่วิธีนี้ใช้หลายขั้นตอนมากในการกลับจากหน้าแสดง Alarm 1 ไปยัง Date & Time และจากการแสดง Alarm 2 ไปยังการตั้งค่า Alarm 2 ผมจึงเปลี่ยนลำดับใหม่:

Menu Sequence

Libraries

สำหรับโปรเจกต์นี้ ผมใช้ Library ดังต่อไปนี้:

  • DS3231 (สำหรับ RTC)
  • Wire (สำหรับ RTC ในการสื่อสารผ่าน I2C)
  • LiquidCrystal

Initiating the Clock and LCD

ผมยังคงการต่อสายไฟสำหรับ 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();
}

Keypad

โชคดีที่ผมสามารถหา 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 ลองดูวิดีโอนี้:

Button Debouncing

ผมยังเพิ่ม 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;
}
}
}
}

Blinking

เมื่อผมเข้าสู่โหมดการปรับแต่ง ผมต้องการวิธีที่จะแสดงให้ผู้ใช้เห็นว่าส่วนไหนของหน้าจอที่พวกเขาสามารถปรับได้ด้วยปุ่มลูกศรขึ้นหรือลง ผมแก้ปัญหานี้ด้วย 4 ส่วน:

  • Integer ที่เปลี่ยนไปตามเซกชันต่างๆ ของหน้าจอที่ผู้ใช้เลือก (ด้วยปุ่มซ้ายหรือขวา)
  • Integer สำหรับเก็บค่าสูงสุดของตัวเลขกะพริบ เพื่อไม่ให้เกินจำนวนขั้นตอนการกะพริบสูงสุด (จำนวนรายการบนหน้าจอ)
  • Variable สำหรับเก็บสถานะการกะพริบ (เป็น true หรือ false)
  • Variable สำหรับเก็บเวลาเป็นมิลลิวินาทีตั้งแต่สถานะการกะพริบล่าสุดเปลี่ยนไป เมื่อเกินค่าที่กำหนด สถานะการกะพริบจะเปลี่ยนจาก true เป็น false หรือ false เป็น true ตามต้องการ

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;
}
}
}
}

Writing to the LCD

จากการอ่าน 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);
}

Showing & Adjusting Date and Time

การแสดงและปรับวันที่และเวลาโดยใช้ DS3231 และ LiquidCrystal Library ตอนนี้ค่อนข้างทำได้ง่ายขึ้น

มี 3 ฟังก์ชันสำหรับแสดงข้อความ:

  • DateText
  • TimeText
  • ShowDateTime

ที่เป็นแบบนี้เพื่อให้ 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;
}
}

Adjusting Alarm On/Off and Timings

การตั้งค่าเปิดหรือปิด 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);}
}

Adjusting Alarm Modes

ขั้นตอนก่อนหน้านี้ทั้งหมดค่อนข้างราบรื่น เป็นเรื่องของการค้นคว้าและนำสิ่งที่ได้เรียนรู้มาปรับใช้ แต่ไม่ใช่กับการตั้งค่าโหมดการแจ้งเตือน (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 คุณจะเห็นว่า:

  • Abits ถูกตั้งค่าเป็นศูนย์อย่างชัดเจน
  • สำหรับ Alarm 1 ค่า Abits จะถูก Mask เพื่อให้คืนค่าสี่หลักแรก (&0b1111)
  • สำหรับ Alarm 2 ค่า Abits จะถูกเลื่อนไปทางขวาสี่ตำแหน่งเพื่อคืนค่าหลักที่ 5, 6 และ 7 เท่านั้น (>> 4)
  • สำหรับทั้งสอง Alarm จะมีการสร้าง Variable ใหม่เพื่อเก็บโหมด Alarm ใหม่ เพื่อให้แน่ใจว่าจะไม่มีการปนเปื้อนของข้อมูลจาก Variable ตัวเก่า
  • สำหรับ Alarm 2 Variable ใหม่จะถูกเลื่อนไปทางซ้ายสี่ตำแหน่งเพื่อตั้งค่าหลักที่ 5, 6 และ 7 ให้ถูกต้อง (<< 4)
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);
}
}

Constants

ค่าคงที่ที่ใช้สำหรับสถานะปัจจุบันแสดงอยู่ด้านล่าง

 // 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.

ผมได้สร้าง Casing หรือที่ยึดแบบง่ายๆ สำหรับ Shield และ Breadboard มันถูกแยกเป็น 2 ชิ้นด้วยเหตุผลสองประการ ประการแรกคือกรอบรอบ LCD จำเป็นต้องมีส่วนรองรับ (Supports) และประการที่สอง ผมพบว่าตำแหน่งของปุ่มต่างๆ ค่อนข้างไม่แน่นอน ดังนั้นการออกแบบนี้จึงช่วยให้ใครก็ตามที่นำไปทำตามสามารถปรับแต่งหน้าจอ LCD ได้ ไฟล์ STL รวมอยู่ในการดาวน์โหลดแล้ว

การออกแบบแยกเป็น 2 ชิ้น

ฝาครอบ LCD เลื่อนเข้าที่

ที่ยึดเมื่อมองจากด้านหน้า

ที่ยึดเมื่อมองจากด้านหลัง - RTC สามารถถอดออกได้ง่าย

Memory

หนึ่งในสิ่งที่ผมสังเกตเห็นกับ Code นี้คือปริมาณการใช้หน่วยความจำ ผมไม่เก่งเรื่องการทำความเข้าใจวิธีควบคุมหน่วยความจำนัก แต่ผมก็ได้ทำบางอย่างหลังจากเขียนคำแนะนำข้างต้น:

  • ผมเปลี่ยน #Define ทั้งหมดเป็น const เพราะผมได้อ่านมาว่ามันดีกว่าสำหรับหน่วยความจำ แต่มันมีผลค่อนข้างจำกัด
  • ผมเปลี่ยน Integer Flags ทั้งหมดเป็น Byte เพื่อลดการใช้หน่วยความจำ ซึ่งมีผลอยู่บ้าง
  • ผมลบ Global Variables ออกในจุดที่สามารถใช้เป็น Local Variables ได้ ซึ่งมันก็ส่งผลค่อนข้างจำกัดอีกเช่นกัน

โดยรวมแล้ว ผมลดการใช้พื้นที่เก็บข้อมูลจาก 55% และ Global Variables ที่ใช้ Dynamic Memory 39% มาเป็นพื้นที่เก็บข้อมูล 53% และ Global Variables ยังคงใช้ Dynamic Memory 39% เหมือนเดิม

ขั้นตอนต่อไปเกี่ยวกับหน่วยความจำคือการลบการอ้างอิงถึง Serial ออกจากฟังก์ชัน SetUp สิ่งนี้ช่วยลดการใช้หน่วยความจำลงเหลือ 50% ของพื้นที่เก็บข้อมูล และ Global Variables ใช้ Dynamic Memory 30% ดังนั้นนี่จึงเป็นบทเรียนว่าควรใส่คอมเมนต์หรือลบ Debugging Statements ออกทั้งหมดเมื่อทำโปรเจกต์เสร็จเรียบร้อยแล้ว

Code ที่เสร็จสมบูรณ์แสดงอยู่ด้านล่าง หากคุณมีคำแนะนำในการปรับปรุงให้ดีขึ้น โปรดบอกให้ผมทราบด้วยนะครับ

Further Reading

เมื่อผมใช้ RTC ในโปรเจกต์ต่างๆ ผมพบปัญหาอยู่ 2 ประการ:

Code

🔒 ปลดล็อก Code

สนับสนุนเพื่อรับ Source Code หรือแอปพลิเคชันสำหรับโปรเจกต์นี้

รหัสอ้างอิงโปรเจกต์: arduino-real-time-clock-rtc-setter-698981
1120 บาท
PromptPay QR Code