← กลับหน้าบทความ

Arduino ภาค 3

ถ้าไม่ใช้ digitalWrite() แล้วเราจะสั่งขา Arduino ยังไง?

จากภาคที่แล้วเราทิ้งคำถามเอาไว้ว่า ถ้า digitalWrite() เป็นเพียงฟังก์ชันที่ Arduino เตรียมไว้ให้ แล้วถ้าเราไม่ใช้มัน เราจะยังสามารถสั่งให้ขา Digital ของ Arduino เป็น HIGH หรือ LOW ได้หรือไม่

คำตอบคือ ได้

และจริง ๆ แล้วการทำแบบนั้นไม่ได้ซับซ้อนอย่างที่คิด เพียงแต่ก่อนจะไปถึงคำสั่งที่ใช้ควบคุมขาโดยตรง เราต้องแยกเรื่องหนึ่งให้ออกก่อน เพราะเลข Pin ที่เราเห็นอยู่บนบอร์ด Arduino กับขาของไมโครคอนโทรลเลอร์ที่อยู่ตรงกลางบอร์ดนั้น ไม่ได้เป็นเลขเดียวกัน

ถ้าเราใช้ Arduino UNO แล้วเขียนแบบนี้

digitalWrite(13, HIGH);

เลข 13 ในที่นี้หมายถึง Arduino Digital Pin 13 ซึ่งเป็นชื่อที่ Arduino กำหนดขึ้นมาเพื่อให้เราใช้งานง่าย แต่ไมโครคอนโทรลเลอร์ ATmega328P ไม่ได้รู้จักขานี้ในชื่อ Digital Pin 13

ถ้ามองลงไปถึงตัว ATmega328P ขานี้คือ PB5 หรือ Bit 5 ของ PORTB

ตรงนี้เป็นจุดที่สำคัญ เพราะหลังจากนี้เราจะเริ่มไม่ได้คุยกับ Arduino เพียงอย่างเดียวแล้ว แต่กำลังจะคุยกับตัว ATmega328P โดยตรง

แล้ว PORTB คืออะไร?

ATmega328P แบ่งขา I/O ออกเป็นกลุ่มหรือที่เรียกว่า Port โดยแต่ละ Port จะประกอบด้วยขาหลายขา เช่น PORTB, PORTC และ PORTD

ถ้าเราดูเฉพาะขาที่ Arduino นำออกมาใช้งาน จะเห็นความสัมพันธ์คร่าว ๆ แบบนี้

Arduino Pin ATmega328P
D0 PD0
D1 PD1
D2 PD2
D3 PD3
D4 PD4
D5 PD5
D6 PD6
D7 PD7
D8 PB0
D9 PB1
D10 PB2
D11 PB3
D12 PB4
D13 PB5

พอมาถึงตรงนี้จะเห็นอะไรบางอย่างที่ตอนใช้ Arduino ปกติเราแทบไม่ต้องสนใจเลย คือ D0 ถึง D7 อยู่ใน PORTD แต่พอขึ้นมาถึง D8 กลับไปเริ่มใหม่ที่ PB0

ดังนั้นเลขที่พิมพ์อยู่บนบอร์ด Arduino จึงไม่ได้เป็นเลข Hardware Port ของ ATmega328P โดยตรง

Arduino เป็นคนจัดการเรื่องนี้ให้เรา

เวลาเราเขียน

digitalWrite(13, HIGH);

Arduino Core จะต้องรู้ก่อนว่า Pin 13 ที่เราพูดถึงนั้นอยู่ที่ Port ไหนและ Bit ไหน ก่อนที่จะไปสั่ง Hardware จริงอีกทีหนึ่ง

แต่ถ้าเราจะข้าม digitalWrite() แล้วลงไปควบคุม Hardware เอง เราก็ต้องรู้ข้อมูลนี้ด้วยตัวเอง

สำหรับ Pin 13 คำตอบก็คือ

Arduino D13 = PB5

หรือก็คือ Bit 5 ของ PORTB

ทีนี้คำถามต่อมาคือ เมื่อเรารู้แล้วว่า LED ที่ต้องการควบคุมอยู่ที่ PB5 เราจะสั่ง PB5 ได้อย่างไร?

ตรงนี้เราจะเริ่มเจอกับสิ่งที่เรียกว่า Register

Register คือจุดที่ Software เริ่มคุยกับ Hardware

เวลาพูดถึง Register ครั้งแรกมันอาจฟังดูเป็นเรื่องค่อนข้างลึก แต่ถ้ามองเฉพาะสิ่งที่เรากำลังจะทำตอนนี้ แนวคิดไม่ได้ซับซ้อนมาก

เราต้องการทำอยู่แค่สองเรื่อง

เรื่องแรก บอก ATmega328P ว่า PB5 จะใช้เป็น Output

เรื่องที่สอง บอกว่า Output ตัวนั้นจะเป็น HIGH หรือ LOW

ถ้าเป็น Arduino เราเขียน

pinMode(13, OUTPUT);
digitalWrite(13, HIGH);

แต่ถ้าเป็น ATmega328P โดยตรง เราจะใช้ Register ที่เกี่ยวข้องกับ PORTB ซึ่งตอนนี้มีอยู่สองตัวที่เราต้องรู้จักก่อน คือ

DDRB ใช้กำหนดว่าขาแต่ละขาของ PORTB จะเป็น Input หรือ Output

PORTB ใช้กำหนดสถานะของขา Output ว่าจะเป็น HIGH หรือ LOW

ลองเริ่มจาก DDRB ก่อน

DDRB เป็น Register ขนาด 8 Bit เพราะฉะนั้นเราสามารถมองมันได้แบบนี้

Bit     7   6   5   4   3   2   1   0
       -------------------------------
DDRB   |   |   |   |   |   |   |   |
       -------------------------------

แต่ละ Bit จะสัมพันธ์กับขาของ PORTB

Bit 0 = PB0
Bit 1 = PB1
Bit 2 = PB2
Bit 3 = PB3
Bit 4 = PB4
Bit 5 = PB5

ถ้า Bit ไหนใน DDRB เป็น 0 ขานั้นจะทำงานเป็น Input และถ้าเป็น 1 ขานั้นจะทำงานเป็น Output

เราต้องการให้ PB5 เป็น Output เพราะฉะนั้นสิ่งที่เราต้องทำก็คือทำให้ Bit 5 ของ DDRB เป็น 1

เขียนแบบตรง ๆ ได้ว่า

DDRB = 0b00100000;

ตอนนี้ Bit 5 เป็น 1 แล้ว ดังนั้น PB5 จึงถูกกำหนดให้เป็น Output

แต่มีปัญหาเล็กน้อยกับวิธีนี้ เพราะคำสั่งดังกล่าวไม่ได้เปลี่ยนเฉพาะ Bit 5 มันเขียนค่าทั้ง Register ใหม่ทั้งหมด นั่นหมายความว่า Bit อื่นใน DDRB จะถูกเปลี่ยนเป็น 0 ไปพร้อมกันด้วย

ถ้าโปรแกรมของเรากำลังใช้งานขาอื่นของ PORTB อยู่ เราอาจไปเปลี่ยนการทำงานของขาเหล่านั้นโดยไม่ตั้งใจ

ดังนั้นปกติเราจะใช้การจัดการ Bit แทน

DDRB |= (1 << 5);

หน้าตาเริ่มแปลกขึ้นแล้ว

แต่ลองแยกมันออกทีละส่วน

1 << 5

หมายถึงเอาเลข 1 เลื่อนไปทางซ้าย 5 ตำแหน่ง

จาก

00000001

กลายเป็น

00100000

จากนั้นเครื่องหมาย |= จะนำค่านี้ไป OR กับค่าที่มีอยู่เดิมใน DDRB ผลคือ Bit 5 จะกลายเป็น 1 โดยที่ Bit อื่นยังคงค่าเดิมเอาไว้

ตอนนี้เราจึงได้สิ่งที่เทียบเท่ากับ

pinMode(13, OUTPUT);

โดยไม่ได้เรียก pinMode() เลย

แล้วจะทำให้ไฟติดได้ยังไง?

เมื่อ PB5 เป็น Output แล้ว ขั้นต่อไปก็คือทำให้ขานี้เป็น HIGH

คราวนี้เราจะใช้ PORTB

หลักการเหมือนกับเมื่อกี้ เราต้องการเปลี่ยน Bit 5 ของ PORTB ให้เป็น 1

PORTB |= (1 << 5);

เมื่อคำสั่งนี้ทำงาน PB5 จะเป็น HIGH และ LED ที่ต่ออยู่กับ D13 บน Arduino UNO ก็จะติดขึ้นมา

ถ้าต้องการปิด ก็ทำให้ Bit 5 กลับเป็น 0

PORTB &= ~(1 << 5);

ดังนั้น Blink แบบเดิมที่เราเคยเขียนว่า

void setup() {
  pinMode(13, OUTPUT);
}

void loop() {
  digitalWrite(13, HIGH);
  delay(1000);
  digitalWrite(13, LOW);
  delay(1000);
}

ตอนนี้เราสามารถเขียนใหม่ได้เป็น

void setup() {
  DDRB |= (1 << 5);
}

void loop() {
  PORTB |= (1 << 5);
  delay(1000);

  PORTB &= ~(1 << 5);
  delay(1000);
}

ผลที่ได้เหมือนเดิม

LED ดวงเดิมติดหนึ่งวินาที ดับหนึ่งวินาที แล้ววนซ้ำไปเรื่อย ๆ

แต่สิ่งที่เกิดขึ้นข้างในไม่เหมือนเดิมเสียทีเดียว เพราะคราวนี้เราไม่ได้บอก Arduino ว่า "ช่วยทำให้ Pin 13 เป็น HIGH ให้หน่อย" เรากำลังแก้ Register ของ ATmega328P โดยตรง

และตรงนี้ทำให้เราเริ่มเห็นแล้วว่า pinMode() กับ digitalWrite() ที่เราใช้กันมาตลอด จริง ๆ แล้วกำลังช่วยซ่อนอะไรเอาไว้ให้เรา

แล้วแบบไหนดีกว่ากัน?

พอมาถึงตรงนี้หลายคนน่าจะเริ่มคิดว่า ถ้าเขียน Register ได้แล้ว เราควรเลิกใช้ digitalWrite() ไปเลยหรือเปล่า

จริง ๆ แล้วไม่จำเป็น

ถ้าเรากำลังเขียนโปรแกรมทั่วไป และไม่ได้มีข้อจำกัดอะไร การเขียน

digitalWrite(13, HIGH);

อ่านง่ายกว่า

อีกคนมาเปิดโค้ดดูก็รู้ทันทีว่าเรากำลังทำอะไร และถ้าวันหนึ่งเปลี่ยนบอร์ดหรือเปลี่ยนไมโครคอนโทรลเลอร์ โค้ดที่อาศัย Arduino API ก็มีโอกาสย้ายตามไปได้ง่ายกว่า

ในขณะที่

PORTB |= (1 << 5);

ผูกอยู่กับ Hardware มากกว่า เราต้องรู้ว่า PB5 คือขาไหน ต้องรู้ว่าไมโครคอนโทรลเลอร์ตัวนั้นมี Register อะไร และถ้าเปลี่ยนไปใช้ไมโครคอนโทรลเลอร์อีกตระกูลหนึ่ง วิธีนี้อาจใช้ไม่ได้เลย

แต่ข้อดีของการลงมาระดับ Register คือ เรารู้แน่นอนว่าเรากำลังสั่ง Hardware อะไร และสามารถควบคุมมันได้ละเอียดกว่าเดิม

ที่สำคัญ ตอนนี้คำสั่งที่เราเคยมองว่าเป็นเรื่องธรรมดาอย่าง

digitalWrite(13, HIGH);

เริ่มมีความหมายไม่เหมือนเดิมแล้ว

เพราะเรารู้แล้วว่าเลข 13 ไม่ใช่เลขขาของ ATmega328P แต่ Arduino ต้องเอาเลขนี้ไปหาก่อนว่ามันคือ PB5 จากนั้นจึงไปจัดการกับ Register ที่เกี่ยวข้องอีกที

ทีนี้ลองคิดต่ออีกนิด

ถ้าเราต้องการเปิดขาเดียว เรื่องนี้อาจไม่ได้สำคัญมากนัก แต่ PORTB เป็น Register ขนาด 8 Bit นั่นหมายความว่า CPU สามารถจัดการสถานะของหลายขาผ่าน Register ตัวเดียวได้

ถ้าเราต้องการเปลี่ยนสถานะของหลายขาพร้อมกันล่ะ?

เราจะยังเรียก digitalWrite() ทีละขา หรือเขียนค่าไปที่ Register ครั้งเดียว?

แล้วสองวิธีนี้ใช้เวลาทำงานเท่ากันจริงหรือ?

ที่ผ่านมาพอเราเขียน digitalWrite() หนึ่งบรรทัด เราเห็นเพียงผลลัพธ์สุดท้ายว่าขาเปลี่ยนเป็น HIGH หรือ LOW แต่เราไม่เคยถามเลยว่า กว่าคำสั่งนั้นจะเดินทางไปถึง Hardware จริง ๆ ไมโครคอนโทรลเลอร์ต้องทำงานไปกี่ขั้นตอน

ถ้า digitalWrite() กับการเขียน Register ให้ผลเหมือนกัน แต่ใช้เวลาไม่เท่ากัน ความต่างนั้นมากแค่ไหน?

และถ้าเราสั่งขาให้ HIGH แล้ว LOW เร็วที่สุดเท่าที่ทำได้...

Arduino UNO จะทำให้ขาหนึ่งกะพริบได้เร็วที่สุดแค่ไหนกัน?

ภาคต่อไปเราจะลองวัดกันจริง ๆ ว่า digitalWrite() กับการสั่ง PORT โดยตรงต่างกันแค่ไหน และดูว่าโค้ดเพียงไม่กี่บรรทัดที่เราเขียนนั้น เมื่อถูกแปลงลงไปให้ ATmega328P ทำงานจริง ๆ แล้ว CPU ต้องทำอะไรบ้าง