#include <MD_MAX72xx.h> // ไลบรารีสำหรับควบคุม LED matrix (MD_MAX72xx)
#include <SPI.h> // ไลบรารี SPI (การสื่อสารแบบ SPI ที่ MD_MAX72xx ใช้)
#define USE_POT_CONTROL 0 // ถ้า=1 จะใช้ potentiometer ปรับความเร็วสโครล, ถ้า=0 ใช้คงที่
#define PRINT_CALLBACK 0 // ถ้า=1 จะพิมพ์ข้อมูล callback ของการเลื่อนคอลัมน์ (debug)
#define PRINT(s, v) { Serial.print(F(s)); Serial.print(v); } // มาโครช่วยพิมพ์ข้อความ+ค่า
#define HARDWARE_TYPE MD_MAX72XX::PAROLA_HW // ประเภทฮาร์ดแวร์ไดรเวอร์ (กำหนดฟอนต์/แม็ทริกซ์) MD_MAX72XX::GENERIC or MD_MAX72XX::FC16.
#define MAX_DEVICES 4 // จำนวนโมดูล MAX7219/7221 ต่อกัน (มากสุดที่ออกแบบไว้)
#define CLK_PIN 13 // ขา Clock ของ SPI เชื่อมกับ Arduino pin 13
#define DATA_PIN 11 // ขา Data (MOSI) เชื่อมกับ Arduino pin 11
#define CS_PIN 10 // ขา CS (Chip Select) เชื่อมกับ Arduino pin 10
// สร้างอ็อบเจ็กต์ mx สำหรับควบคุมไลบรารี MD_MAX72XX
MD_MAX72XX mx = MD_MAX72XX(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);
#if USE_POT_CONTROL
#define SPEED_IN A5 // เมื่อใช้ POT: กำหนดขาอนาล็อกที่อ่านความเร็ว
#else
#define SCROLL_DELAY 75 // เมื่อไม่ใช้ POT: ความล่าช้า (มิลลิวินาที) ระหว่างการเลื่อนคอลัมน์
#endif // USE_POT_CONTROL
#define CHAR_SPACING 1 // ระยะว่าง (คอลัมน์) ระหว่างตัวอักษรในสโครล
#define BUF_SIZE 75 // ขนาดบัฟเฟอร์ข้อความ (รวม NUL terminator)
char curMessage[BUF_SIZE]; // ข้อความกำลังแสดง
char newMessage[BUF_SIZE]; // ข้อความใหม่ที่อ่านมาจาก Serial รอคัดลอก
bool newMessageAvailable = false; // แฟลกบอกว่ามีข้อความใหม่เข้ามาหรือไม่
uint16_t scrollDelay; // ค่าหน่วงเวลาในการสโครล (อาจอ่านจาก POT หรือค่าคงที่)
// อ่านข้อมูลจาก Serial แล้วเก็บใน newMessage จนเจอ newline '\n'
void readSerial(void)
{
static uint8_t putIndex = 0; // ดัชนีตำแหน่งเขียนใน newMessage (คงสถานะระหว่างเรียก)
while (Serial.available()) // ขณะที่มีข้อมูลใน buffer ของ Serial
{
newMessage[putIndex] = (char)Serial.read(); // อ่านไบต์เดียวและเก็บ
if ((newMessage[putIndex] == '\n') || (putIndex >= BUF_SIZE - 3))
{
// ถ้าเจอ newline หรือใกล้เต็ม กำหนดช่องว่างท้ายข้อความและ NUL-terminator
newMessage[putIndex++] = ' '; // เติมช่องว่างแทน newline
newMessage[putIndex] = '\0'; // จบสตริง
putIndex = 0; // รีเซ็ตดัชนีกลับไปเริ่มใหม่
newMessageAvailable = true; // ตั้งแฟลกว่ามีข้อความใหม่ให้ใช้
}
else if (newMessage[putIndex] != '\r')
// ข้าม carriage return ('\r') เพื่อหลีกเลี่ยงปัญหาจาก CRLF
putIndex++; // เพิ่มดัชนีเมื่อเป็นตัวอักษรปกติ
}
}
// ฟังก์ชัน callback สำหรับรับข้อมูลคอลัมน์ที่ถูก shift ออก (sink)
// (ฟังก์ชันนี้จะถูกเรียกเมื่อไลบรารีต้องการส่งข้อมูลออก — ใช้เพื่อตรวจสอบ/debug)
void scrollDataSink(uint8_t dev, MD_MAX72XX::transformType_t t, uint8_t col)
{ // <-- เพิ่มเปิดบล็อกฟังก์ชันที่หายไป (เพื่อให้โค้ดคอมไพล์)
#if PRINT_CALLBACK
// ถ้าเปิด PRINT_CALLBACK จะพิมพ์รายละเอียดของ callback แต่ละคอลัมน์ (debug)
Serial.print("\n cb ");
Serial.print(dev);
Serial.print(' ');
Serial.print(t);
Serial.print(' ');
Serial.println(col);
#endif
} // ปิดฟังก์ชัน scrollDataSink
// ฟังก์ชัน callback ที่ให้ไลบรารีเรียกเมื่อมันต้องการคอลัมน์ถัดไปจากแหล่งข้อมูล (source)
// คืนค่าเป็นค่าไบต์ 0-255 ที่แทนข้อมูลคอลัมน์ของแมทริกซ์
uint8_t scrollDataSource(uint8_t dev, MD_MAX72XX::transformType_t t)
{
static char *p = curMessage; // พอยน์เตอร์ตำแหน่งตัวอักษรปัจจุบันใน curMessage
static uint8_t state = 0; // สเตทของ state machine (0=load char,1=send columns,2=spacing)
static uint8_t curLen, showLen; // curLen = คอลัมน์ปัจจุบันของตัวอักษร, showLen = จำนวนคอลัมน์ของตัวอักษร
static uint8_t cBuf[8]; // buffer เก็บคอลัมน์ของตัวอักษร (max 8 คอลัมน์)
uint8_t colData; // ค่าที่จะคืนเป็นข้อมูลคอลัมน์
switch (state)
{
case 0:
// โหลดฟอนต์ของตัวอักษรถัดไปลงใน cBuf และรับจำนวนคอลัมน์ที่ต้องแสดง
showLen = mx.getChar(*p++, sizeof(cBuf) / sizeof(cBuf[0]), cBuf);
curLen = 0; // เริ่มนับคอลัมน์จาก 0
state++; // ไปสถานะต่อไป (ส่งคอลัมน์)
// ถ้าถึงจบสตริงแล้ว ให้วนกลับไปที่จุดเริ่ม (หรือถ้ามีข้อความใหม่ ให้คัดลอกเข้า curMessage)
if (*p == '\0')
{
p = curMessage; // กลับไปเริ่มต้นของ curMessage
if (newMessageAvailable)
{
strcpy(curMessage, newMessage); // ถ้ามีข้อความใหม่ คัดลอกเข้า curMessage
newMessageAvailable = false; // รีเซ็ตแฟลก
}
}
// หมายเหตุ: ไม่มี break; ที่นี่เพราะต้องการให้ไหลเข้า case 1 ทันที (intentional fall-through)
case 1:
// ส่งคอลัมน์ถัดไปของตัวอักษรจาก cBuf
colData = cBuf[curLen++]; // เอาค่าจาก buffer แล้วเพิ่มดัชนี
if (curLen == showLen) // ถ้าส่งครบทุกคอลัมน์ของตัวอักษรแล้ว
{
showLen = CHAR_SPACING; // เปลี่ยนเป็นระยะว่าง (spacing) ระหว่างตัวอักษร
curLen = 0; // รีเซ็ตดัชนี
state = 2; // ไปสถานะทำ spacing
}
break;
case 2:
// ระหว่าง spacing ให้คืนค่า 0 (คอลัมน์ว่าง)
colData = 0;
if (curLen == showLen) // ถ้าส่งช่องว่างครบตาม showLen แล้ว
state = 0; // กลับไปโหลดตัวอักษรถัดไป
curLen++;
break;
default:
state = 0; // ป้องกันกรณีผิดพลาด ให้รีเซ็ต state
}
return (colData); // คืนค่าคอลัมน์ให้ไลบรารีแสดงผล
}
// ฟังก์ชันเรียกเพื่อขยับการแสดงผล (เรียก mx.transform ตามเวลา)
void scrollText(void)
{
static uint32_t prevTime = 0; // เก็บเวลาครั้งสุดท้ายที่ทำการเลื่อน (millis)
if (millis() - prevTime >= scrollDelay) // ถ้าผ่านเวลาที่กำหนดแล้ว
{
mx.transform(MD_MAX72XX::TSL); // ทำการ shift-left (TSL = transform shift left) หนึ่งคอลัมน์
prevTime = millis(); // อัปเดต prevTime
}
}
// อ่านค่าหน่วงเวลาที่จะใช้ (จาก POT ถา้เปิด หรือค่าคงที่)
uint16_t getScrollDelay(void)
{
#if USE_POT_CONTROL
uint16_t t;
t = analogRead(SPEED_IN); // อ่านค่าอนาล็อกจาก POT (0..1023)
t = map(t, 0, 1023, 25, 250); // แม็ปค่าให้อยู่ในช่วงเวลาที่ต้องการ (25..250 ms)
return (t);
#else
return (SCROLL_DELAY); // คืนค่าคงที่
#endif
}
void setup()
{
mx.begin(); // เริ่มต้นไลบรารี MD_MAX72XX (initialise SPI, chip init)
mx.setShiftDataInCallback(scrollDataSource); // ตั้ง callback แหล่งข้อมูล (source)
mx.setShiftDataOutCallback(scrollDataSink); // ตั้ง callback สำหรับข้อมูลที่ถูก shift ออก (sink)
#if USE_POT_CONTROL
pinMode(SPEED_IN, INPUT); // กำหนดขาอ่าน POT เป็น INPUT ถ้าใช้
#else
scrollDelay = SCROLL_DELAY; // ถ้าไม่ใช้ POT กำหนดค่า scrollDelay เริ่มต้น
#endif
strcpy(curMessage, "Hello! "); // ข้อความเริ่มต้นที่จะแสดง
newMessage[0] = '\0'; // กำหนดให้ newMessage ว่าง
Serial.begin(57600); // เริ่ม Serial เพื่อรับข้อความจากคอมพ์ (57600 bps)
Serial.print("\n[MD_MAX72XX Message Display]\nType a message for the scrolling display\nEnd message line with a newline");
// แสดงข้อความแนะนำใน Serial Monitor
}
void loop()
{
scrollDelay = getScrollDelay(); // อัปเดต scrollDelay ตามค่า POT หรือค่าคงที่
readSerial(); // อ่าน Serial เพื่อรับข้อความใหม่ (ถ้ามี)
scrollText(); // ทำการเลื่อนข้อความตามเวลาที่กำหนด
}