โปรเจกต์นี้คือการใช้อุปกรณ์ตรวจจับสนามแม่เหล็ก (Hall effect sensors) มาหาทิศทางการหมุนและความเร็วรอบ (RPM) ของมอเตอร์กระแสตรง (DC motor) ... และที่นี่เราจัดหนัก วัดพร้อมกันทีละ 2 ตัวเลยจ้า!
ด้วยการตั้งค่าให้เกิดการขัดจังหวะจากภายนอก (external interrupt) เราจะตรวจจับสถานะของเซนเซอร์ตัวแรกในแต่ละคู่ จากนั้นก็บันทึกสถานะของเซนเซอร์ตัวที่สองที่วางห่างกัน 90 องศา แล้วใช้โค้ดวิเคราะห์ว่ามอเตอร์แต่ละตัวกำลังหมุนไปทางไหน นอกจากนี้ ยังใช้การขัดจังหวะภายใน (Timer1) ในการคำนวณช่วงเวลา (period) เพื่อหาความถี่ (รอบต่อวินาที) และ RPM ของมอเตอร์แต่ละตัวอีกด้วย
บนเพลาของมอเตอร์แต่ละตัว พี่ได้ทำตัวเข้ารหัส (encoder) แบบชั่วคราวขึ้นมาโดยใช้แม่เหล็กนีโอไดเมียม
สำหรับการควบคุมทิศทางการหมุนของมอเตอร์แต่ละตัว พี่ใช้ Op-Amp (LM324N) ร่วมกับจอยสติ๊กและไดรเวอร์ (L293N และ L298N)
ส่วนการแปลงสัญญาณอนาล็อกจากเซนเซอร์แต่ละตัวให้เป็นดิจิตอล พี่ใช้ Op-Amp ทำงานในโหมดคอมพาเรเตอร์
และที่ขาดไม่ได้เลย พี่ใช้ชิป Schmitt Trigger Inverter (SN74LS14N) เพื่อกำจัดสัญญาณรบกวนชั่วขณะ (transient effect) จากเซนเซอร์แต่ละตัว ป้องกันการอ่านค่าผิดพลาด ทำให้ได้ค่าความเร็วรอบและทิศทางการหมุนของมอเตอร์แต่ละตัวที่แม่นยำพอสมควร
ไมโครคอนโทรลเลอร์ที่ใช้คือ ATmega32u4 (Beatle BadUSB) ซึ่งทำงานเหมือน Arduino Leonardo แต่โค้ดนี้สามารถนำไปใช้กับ Arduino รุ่นอื่นๆ ส่วนใหญ่ได้
จอแสดงผลเป็น ST7735 ขนาด 128x128 พิกเซล ใช้บัส SPI ใช้ง่ายมาก
ลงลึกกันหน่อยดีกว่า (Technical Deep-Dive)
โปรเจกต์ "Mag-Tach" นี้คือคลาสเรียนขั้นสูงเรื่อง การตรวจจับแม่เหล็กแบบละเอียดยิบ และ ความสมบูรณ์ของสัญญาณ ในขณะที่มาตรวัด RPM ทั่วไปใช้เซนเซอร์อินฟราเรดแบบจับพัลส์เดียว โปรเจกต์นี้ใช้สถาปัตยกรรม Quadrature Hall Effect แบบสองช่องสัญญาณ โดยการจับการเปลี่ยนแปลงของฟลักซ์แม่เหล็กจากแม่เหล็กนีโอไดเมียมผ่านเซนเซอร์เชิงเส้น และปรับแต่งสัญญาณด้วย Schmitt trigger ทำให้ Mag-Tach สามารถดึงข้อมูลได้ทั้งขนาด (RPM) และทิศทาง (+/-) ของมอเตอร์ DC สองตัวที่ทำงานพร้อมกันได้ ให้ข้อมูลเทเลเมทรีคุณภาพสูงสำหรับระบบหุ่นยนต์หรือระบบขับเคลื่อน
- การวิเคราะห์การเข้ารหัสแบบ Quadrature:
- การเลื่อนเฟส 90 องศา: เซนเซอร์ Hall effect (KY-035) สองตัวจะถูกวางห่างกันเป็นระยะเชิงมุม 90 องศาเทียบกับขั้วแม่เหล็ก ดังที่เห็นในภาพแรก สิ่งนี้สร้างคลื่นสี่เหลี่ยมสองช่อง (ช่อง A และ B) ที่เลื่อนเฟสกัน
- ตรรกะหาทิศทางเวกเตอร์: โดยการตรวจสอบว่าช่องสัญญาณไหนเกิดขอบขาลง (
FALLINGedge) ก่อน ATmega32U4 จะสามารถระบุทิศทางการหมุนได้ ถ้า A นำหน้า B มอเตอร์จะหมุนตามเข็ม (CW, +) ถ้า B นำหน้า A มอเตอร์จะหมุนทวนเข็ม (CCW, -)
- การปรับแต่งสัญญาณและกำจัดสัญญาณรบกวน:
- Schmitt Trigger (SN74LS14N): เซนเซอร์ Hall effect มักจะสร้าง "การกระเพื่อม" ชั่วคราว (jitter) ที่ขอบเขตของสนามแม่เหล็ก โปรเจกต์นี้ใช้ Schmitt trigger ที่มี ฮิสเทอรีซิส ในการแปลงสัญญาณอนาล็อกให้เป็นดิจิตอล สิ่งนี้ทำให้พินอินเตอร์รัปต์เห็นเฉพาะคลื่นสี่เหลี่ยมที่ "สะอาด" เท่านั้น กำจัดสัญญาณเท็จที่เกิดจากสัญญาณรบกวนแม่เหล็ก
- การเปรียบเทียบด้วย Op-Amp: LM324N ที่เห็นในภาพที่สอง ทำหน้าที่เป็นคอมพาเรเตอร์ความเร็วสูง แปลงแรงดันอนาล็อกเชิงเส้นจากเซนเซอร์ฮอลล์ให้เป็นการเปลี่ยนแปลงระดับลอจิกที่ Schmitt trigger ต้องการ
- การหาความถี่ด้วย Timer1:
- การวัดแบบใช้คาบเวลา: แทนที่จะนับพัลส์ต่อวินาที (ซึ่งไม่แม่นยำที่ RPM ต่ำ) Mag-Tach วัด เวลาระหว่างพัลส์ โดยใช้ไลบรารี
TimerOneด้วยการคำนวณส่วนกลับของคาบเวลา ระบบจะได้การอัปเดตค่า RPM ที่เกือบจะทันที แม้ในช่วงสตาร์ตช้าหรือการชะลอตัวอย่างรวดเร็ว
- การวัดแบบใช้คาบเวลา: แทนที่จะนับพัลส์ต่อวินาที (ซึ่งไม่แม่นยำที่ RPM ต่ำ) Mag-Tach วัด เวลาระหว่างพัลส์ โดยใช้ไลบรารี
วิศวกรรมและการนำไปใช้
สถาปัตยกรรมแบบ Interrupt-Driven:
- การวัดความเร็วรอบสูงๆ ต้องใช้ตรรกะแบบไม่บล็อก CPU นะน้อง โปรเจคนี้ใช้
attachInterruptบนขา 2 กับ 3 เพื่อจัดการพัลส์จากมอเตอร์ A/B วิธีนี้ทำให้ลูปหลักของ CPU ไปโฟกัสที่การวาดกราฟฟิคบนหน้าจอ ST7735 TFT ได้สบายๆ ส่วนเรื่องจับเวลาที่แม่นยำระดับมิลลิวินาทีปล่อยให้ฮาร์ดแวร์จัดการในแบ็กกราวด์ไปเลย
- การวัดความเร็วรอบสูงๆ ต้องใช้ตรรกะแบบไม่บล็อก CPU นะน้อง โปรเจคนี้ใช้
การวิเคราะห์ข้อมูลด้วยภาพ (ST7735):
- หน้าจอสีขนาด 128x128 ถูกใช้เพื่อสร้าง กราฟแท่งสดๆ เหมือนในรูปสุดท้ายเลย โค้ดจะแมปค่า
rpmไปเป็นแกน Y บนหน้าจอ ทำให้เห็นภาพการทำงานของมอเตอร์แบบอนาล็อกแบบเรียลไทม์ไปพร้อมๆ กับตัวเลข RPM แบบดิจิตอล ดูแล้วเข้าใจง่ายดี
- หน้าจอสีขนาด 128x128 ถูกใช้เพื่อสร้าง กราฟแท่งสดๆ เหมือนในรูปสุดท้ายเลย โค้ดจะแมปค่า
การป้องกันข้อมูลแบบอะตอมมิก:
- เพื่อป้องกัน "Race Conditions" เวลาที่ลูปหลักกำลังอ่านค่าตัวแปรหลายไบต์ แต่ Interrupt กำลังอัปเดตค่ามันอยู่ โค้ดเลยใช้บล็อก
ATOMIC()ตัวนี้จะล็อคไม่ให้ Interrupt มารบกวนชั่วคราวตอนคำนวณค่า RPM พอเสร็จแล้วค่อยปล่อย รับรองข้อมูลไม่เพี้ยนแน่นอน
- เพื่อป้องกัน "Race Conditions" เวลาที่ลูปหลักกำลังอ่านค่าตัวแปรหลายไบต์ แต่ Interrupt กำลังอัปเดตค่ามันอยู่ โค้ดเลยใช้บล็อก
สรุป
Mag-Tach นี่ถือเป็นก้าวสำคัญในวงการ Propulsion Telemetry เลยนะเว้ย การที่เราเข้าใจการผสมผสาน Quadrature Phase Logic, Hysteresis-based Debouncing และ Atomic Data Management อย่างลงตัว จะทำให้เราสร้างเอนโคเดอร์ระดับอุตสาหกรรมสำหรับหุ่นยนต์ที่งานต้องเป๊ะๆ ได้เลย
Vector Precision: การไขปริศนาการหมุนผ่านแม่เหล็กแบบ Quadrature
ในวิดีโอต่อไปนี้จะเห็นการทำงานและการทดสอบโค้ด:
/*
RPM Meter Direction
Use Square Encoder with Hall effect sensors
By DrakerDG (c)
https://www.youtube.com/user/DrakerDG
*/
#include <SPI.h>
#include <TFT_ST7735.h>
#include <SimplyAtomic.h>
#include <TimerOne.h>
#define GREEN 0x07E0
#define YELLOW 0x07FF
#define DC A0 //9
#define RS A1 //10
#define CS A2 //11
// Sensor pins
const byte PinX[4] = {2, 3, 10, 11};
// Limit of microseconds
const long uSeg = 100000;
// Left position RPM labels
const byte x1 = 75;
// Pulse timer counters
volatile unsigned long pwc[2];
// PWM periods
volatile unsigned long pwm[2];
// RPM values
unsigned long rpm[2];
// States of the second sensors
volatile bool Sen[2];
// Count variable printing millis
unsigned long prT = 0;
TFT_ST7735 tft = TFT_ST7735(CS, DC, RS);
void CountSA(void);
void CountSB(void);
void RPMc(void);
void Draw_Table(void);
void Print_Data(void);
void setup(){
Serial.begin(9600);
tft.begin();
tft.setRotation(1);
tft.clearScreen();
tft.setTextWrap(true);
tft.setTextColor(YELLOW, BLACK);
tft.setCursor(0, 0);
Draw_Table();
for (byte i=0; i<4; i++){
pinMode(PinX[i], INPUT);
}
for (byte i=0; i<2; i++){
pwc[i] = uSeg;
pwm[i] = uSeg;
rpm[i] = 0;
}
// Interrupt to count period time
Timer1.initialize(100);
Timer1.attachInterrupt(RPMc);
// Interrupt of Sensor 1 of Motor A
attachInterrupt(digitalPinToInterrupt(PinX[0]), CountSA, FALLING);
// Interrupt of Sensor 1 of Motor B
attachInterrupt(digitalPinToInterrupt(PinX[1]), CountSB, FALLING);
}
void loop(){
Print_Data();
}
void CountSA(){
// 2nd sensor value
Sen[0] = 1 & (PINB >> 7);
pwm[0] = pwc[0]; // Save the period
pwc[0] = 0; // Reset the timer
}
void CountSB(){
// 2nd sensor value
Sen[1] = 1 & (PINB >> 6);
pwm[1] = pwc[1]; // Save the period
pwc[1] = 0; // Reset the timer
}
void RPMc(){
for (byte i=0; i<2; i++){
// Increase the time counter
pwc[i]++;
if (pwc[i] > (uSeg)){
// Limit the timer & period
pwc[i] = uSeg;
pwm[i] = uSeg;
}
}
}
void Draw_Table(){
// Code to draw the table on screen
tft.drawFastVLine(22, 0, 128, WHITE);
for ( int i=0; i<11; i+=1 ){
tft.drawFastHLine( 20, 5+i*12, 4, WHITE);
if (!(i&1)){
tft.setCursor( 0, i*12 + 2);
tft.print((10.0-i)*0.5, 1);
}
}
tft.drawFastHLine( 20, 125, 128, WHITE);
tft.setTextSize(1);
tft.setCursor(x1, 10);
tft.print("Motor A");
tft.setCursor(x1+30, 45);
tft.print("RPM");
tft.setCursor(x1, 70);
tft.print("Motor B");
tft.setCursor(x1+30, 105);
tft.print("RPM");
tft.setTextSize(2);
}
void Print_Data(){
unsigned long nwT = millis();
// Calculations and prints every 10ms
if ((nwT - prT) > 10){
prT = nwT;
char sRPM[10];
for (byte i=0; i<2; i++){
//RPM
tft.setCursor(x1, 25+60*i);
// Protects math calculation
ATOMIC()
{
// Detect Rotation Decrease
if (pwc[i]>(pwm[i]*2)){
pwm[i] *= 2;
pwm[i] = constrain(pwm[i], 0, uSeg);
pwc[i] = pwc[i]*2;
}
/* detects or not the
rotation of the motors */
if (pwm[i] < uSeg) rpm[i] = 6*uSeg/pwm[i]; // Detects rotation
else if ((rpm[i] > 0)&&(pwm[i] == uSeg)) rpm[i] = int(rpm[i]/2); // No rotatiom
// Limits the value of RPMs
rpm[i] = constrain(rpm[i], 0, 9999);
}
dtostrf(rpm[i], 4, 0, sRPM);
// Print the RPMs
tft.print(sRPM);
int valX = rpm[i]*120/500;
// Prints the RPM bars
tft.fillRect(30+15*i, 0, 10, 125 - valX, BLACK);
tft.fillRect(30+15*i, 125 - valX, 10, valX, GREEN);
tft.setCursor(x1-15, 25+60*i);
/* Determines the direction
of rotation of the motors.
Prints + if it turns CW and
- if it turns CCW */
if ((rpm[i] == 0)&&(pwm[i] == uSeg)) tft.print(" ");
else if (Sen[i]) tft.print("-");
else tft.print("+");
}
}
}




