[Advanced Arduino] เมื่อ digitalWrite() digitalRead() ช้าเกินไป

เมื่อไม่นานมานี้ผมได้ศึกษาเกี่ยวกับการสื่อสารข้อมูลแบบ One Wire กับไอซีวัดอุณหภูมิ DHT11 แล้วก็พบว่ามันเป็นโปรโตคอลสื่อสารที่มีรูปแบบการรับ – ส่งข้อมูลที่ใช้ยากพอสมควร จะต้องเข้าใจทามมิ่งไดอแกรม และต้องใช้ความเร็วสูงมากในการอ่านค่าข้อมูล ซึ่งผมได้ลองทำไลบารี่ขึ้นมาแล้วใช้คำสั่ง digitalWrite() digitalRead() ไม่ Work อย่างมากครับ ทั้ง 2 คำสั่งนี้ใช้เวลาในการอ่านค่าและเขียนค่านานมากๆ ต้องใช้คลื่นหลายไซเคิลมากกว่าจะสามารถอ่าน – เขียนได้ ดังนั้นเพื่อความรวดเร็วจึงต้องหันไปใช้งานรีจิสเตอร์แทน ซึ่งผมทดสอบแล้วได้ความเร็วที่สุดยอดมากครับ ใช้ไซเคิลเดียวในการอ่านค่า หรือเขียนค่า ทำให้สามารถนำไปทำไลบารี่ One Wire ได้ครับ

ทำไม digitalWrite() digitalRead() ถึงได้ช้า ?

หากเราเข้าไปดูที่ไฟล์ wiring_digital.c ซึ่งอยู่ในโฟลเดอร์ cores เราจะพบว่าฟังก์ชั่น digitalWrite() นั้นมีโค้ดประมาณด้านล่างนี้

ซึ่งถ้าถามว่าต้องการจะเขียนสถานะลอจิกไปที่ขา จำเป็นต้องใช้โค้ดทุกบรรทัดที่เขาได้เขียนไว้หรือไม่ ผมสามารถตอบได้เต็มปากเต็มคำว่า ไม่ จึงมีคำถามต่อมาว่าแล้วโค้ดที่มันเยอะๆคืออะไร ผมเข้าใจว่า Arduino เป็นสิ่งที่ต้องการให้ผู้ใช้ทุกเพศทุกวัยสามารถเข้าใจได้ง่าย ใช้งานได้ง่าย ดังนั้นโค้ดที่อยู่ในฟังก์ชั่น digitalWrite() ส่วนใหญ่จึงเป็นโค้ดที่เขียนขึ้นมาเพื่อป้องกันความผิดพลาดจากการเขียนโค้ดของผู้ใช้งานเอง ตัวอย่างเช่น if ในบรรทัดที่ 4 ใช้ตรวจเช็คว่า PIN ที่มีการกรอกเข้ามานั้นมีอยู่จริงหรือไม่ ถ้าไม่มีอยู่จริง (เช่น ใน Uno กรอก 51 เข้าไป ซึ่งมันไม่มีอยู่) ก็จะทำเนียนเฉยๆไปเลย (return; ออกไปเลย) หรือดูในบรรทัดที่ 15 จะเช็คว่าได้ใช้ฟังก์ชั่น pinMode() บอกไว้ว่า PIN นั้น เป็น INPUT หรือ OUTPUT ซึ่งถ้าเป็นอินพุต ก็เปิดใช้ pull-up จากภายใน ถ้าไม่ใช่ก็ค่อยตั้งค่าสถานะออกพอร์ตไปผ่านฟังก์ชั่น PIO_SetOutput()

เรามีดูฟังก์ชั่น digitalRead() กันบ้าง ไฟล์เดียวกัน จะพบกับคำสั่งประมาณด้านล่างนี้

ในฟังก์ชั่น digitalRead() ก็จะคล้ายๆกับฟังก์ชั่น digitalWrite() ด้านบน ด้วยเหตุผลเรื่องการป้องกันความผิดพลาดของผู้ใช้ จึงมีโค้ดในในบรรทัด 3 7 และ 11 เพิ่มขึ้นมา ทำให้ฟังก์ชั่นนี้มันทำงานได้ช้านั่นเอง (ขอไม่เจาะลึกตรงส่วนนี้)

รีจิสเตอร์ทำงานได้เร็วกว่าเยอะ

รีจิสเตอร์คืออะไรละ ? แล้วทำไมมันถึงได้เร็วกว่า ? จริงๆแล้วรีจิสเตอร์ถือเป็นสิ่งที่เรียกว่าเป็นแกนหลักของฟังก์ชั่น digitalWrite() digtalRead() เมื่อไปเรียกฟังก์ชั่นหล่านี้มาใช้ สุดท้ายแล้วมันก็จะมาจบด้วยการไปใช้รีจิสเตอร์อยู่ดี ถ้าจะให้อธิบายตามที่ผมเข้าใจ รีจิสเตอร์เป็นเสมือนตัวแปรที่ส่งเข้า CPU โดยตรง ซึ่งการเซ็ตค่ารีจิสเตอร์ถือเป็นคำสั่งที่เด็ดขาดที่สุด ไม่มีตุกติกอย่างคำสั่ง digital บราๆๆ ที่เราใช้อยู่ประจำ เช่น การเซ็ตรีจิสเตอร์ให้พินมีเอาต์พุตเป็น 1 มันก็จะทำตามทันทีโดยไม่มีการไปเช็คว่าคุณทำให้พอร์ตนั้นเป็นเอาต์พุตแล้วหรือยัง ทันทีที่สั่ง มันจะทำตามคำสั่งทุกๆอย่าง หรือถ้าต้องการอ่านสถานะจาก PIN ใดๆก็ตาม มันก็จะคืนค่าสถานะของ PIN นั้นๆมาให้ทันที โดยไม่ได้สนใจว่าได้ใช้คำสั่ง pinMode() เซ็ตให้ PIN นั้นเป็นอินพุตแล้วหรือยัง ด้วยเหตุผลที่มันส่งคำสั่งตรงไปที่ CPU จึงทำให้มันสามารถทำงานได้เร็วมากๆนั้นเอง

มารู้จักชื่อรีจิสเตอร์ที่ใช้คุม Digital Pin กันหน่อย

ต้องบอกไว้ก่อนเลยว่าบอร์ด Arduino แต่ละตัวนั้นจะมีชื่อรีจิสเตอร์ที่ไว้ควบคุมที่แตกต่างกัน แล้วแต่ว่าแกนกลางของบอร์ดใช้ไมโครคอนโทรลเลอร์เบอร์อะไร ซึ่งในบทความนี้ผมจะอ้างอิงไมโครคอนโทรลเลอร์ตะกูล AVR เป็นหลัก ที่ใช้ในบอร์ด Arduino UNO Mega Nano ProMini etc. ซึ่งใช้ไอซี ATmega328p-pu เหมือนกัน จัดอยู่ในตะกูล AVR เหมือนกัน ทำให้บอร์ดเหล่านี้มีชื่อรีจิสเตอร์ที่เหมือนกันนั้นเอง

ก่อนที่จะไปรู้จักกับชื่อรีจิสเตอร์ ต้องเข้าใจก่อนว่า PIN 13 12 11 10 …. A0 A1 … มันคือฉากบังหน้าที่กำหนดมาสำหรับใช้กับฟังก์ชั่น digital บราๆ เท่านั้น ชื่อของรีจิสเตอร์จะสัมพันธ์กับชื่อขาจริงๆของบอร์ด (หรือชื่อขาไอซี) ซึ่งชื่อขาจริงๆสามารถดูได้จากรูป Pinout ด้านล่างนี้

atmega328w

ดูเฉพาะกรอบเหลืองๆ เราจะเห็นชื่อที่แท้จริง ที่มีอีกชื่อเป็นชื่อของ Arduino เท่านั้นในกรอบสีชมพู ตัวอย่างขาที่ 14 ของไอซี ชื่อขาจริงๆคือ PB0 หรือชื่อเต็ม PORTB0 แต่ชื่อขาที่ Arduino กำหนดคือ PIN 8

หรือในขาที่ 11 ของไอซี ชื่อขาจริงๆคือ PD5 (PORTD5) และชื่อขาที่ Arduino ตั้งให้คือ PIN 5

ทีนี้ชื่อของรีจีสเตอร์ที่ใช้ควบคุมจะเป็นชื่อคล้ายๆกับชื่อขา ซึ่งรีจิสเตอร์ที่ใช้ควบคุมมีดังนี้

  • DDR… – ใช้กำหนดว่าให้ขาไหนในพอร์ตมีสถานะเป็นอินพุต หรือเอาต์พุตอย่างไรบ้าง
  • PORT… – ใช้สั่งให้ขาไหนของพอร์ตมีสถานะลอจิกเป็นอะไรบ้าง
  • PIN… – ใช้อ่านค่าว่าในพอร์ตนั้นขาไหนมีสถานะอะไรบ้าง

การใช้งานรีจิสเตอร์มีข้อดีตรงที่เราสามารถควบคุมหลายๆขาได้โดยใช้เพียงคำสั่งเดียว ตัวอย่างเช่น หากเราต้องการควบคุมพอร์ต D ให้มีสถานะทางลอจิกเป็น 1 ทั้งหมด (PD0 – PD7) สามารถทำได้โดยใช้คำสั่ง

PORTD = 0xFF;

แค่นี้ตั้งแต่ PD0 – PD7 ก็มีสถานะเป็นลอจิก 1 ทั้งหมดแล้ว

การใช้การเขียนค่าที่พอร์ตอื่นก็ทำได้แบบเดียวกัน เช่น PORTB = 0xFF (PB0 – PB5 เป็นลอจิก 1 ทั้งหมด โดย PB6 PB7 ไม่สามารถควบคุมได้เนื่องจากต่ออยู่กับคริสตอล) หรือ PORTC = 0xFF (PC0 – PC5 เป็นลอจิก 1 ทั้งหมด โดย PC6 ไม่สามารถควบคุมได้เนื่องจากเป็นขารีเซ็ต และ PC7 ไม่มีอยู่)

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

การเซ็ตรีจิสเตอร์ควบคุม Digital PIN แบบ Arduino

ใน Arduino จะมีคำสั่งที่ใช้เรียกตำแหน่งของรีจิสเตอร์ของขาดิจิตอลออกมา 4 คำสั่ง ดังนี้

byte digitalPinToBitMask(int pin_number)

คำสั่งนี้ใช้อ่านบิตของขาแบบ Arduino ว่าอยู่บิตที่เท่าไหร่ แล้วเซ็ตให้บิตนั้นเป็นลอจิก 1 อธิบายแบบนี้อาจจะไม่เห็นภาพ ลองย้อนกลับไปดูในภาพ Pinout ตัวอย่างหากใส่ pin_number เป็น 8 (Digital PIN 8) จะให้ค่าเอาต์พุตบิตที่ 0 (จากชื่อขาจริงๆ PB0) เป็นลอจิก 1 ออกมา (ซึ่งจะมีค่าเป็น 2^0 = 1 ในเลขฐาน 10 หรือ 0x01 ในเลขฐาน 16) หรือหากใส่ pin_number เป็น 13 (Digital PIN 13) จะให้ค่าเอาต์พุตบิตที่ 5 (จากชื่อขาจริงๆ PB5) เป็นลอจิก 1 ออกมา (ซึ่งจะมีค่าเป็น 2^5 = 32 ในเลขฐาน 10 หรือ 0x20 ในเลขฐาน 16)

byte digitalPinToPort(int pin_number);

คำสั่งนี้ใช้คืนค่าหมายเลขพอร์ตจากหมายเลขขาของ Arduino

volatile uint8_t *portInputRegister(byte port);

เป็นคำสั่งที่ใช้ดึงตำแหน่งรีจิสเตอร์ PIN… ออกมาจากหมายเลขพอร์ต ซึ่งหมายเลขพอร์ตจะได้มาจากการใช้คำสั่ง digitalPinToPort()

volatile uint8_t *portOutputRegister(byte port);.

เป็นคำสั่งที่ใช้ดึงตำแหน่งรีจิสเตอร์ PORT… ออกมาจากหมายเลขพอร์ต ซึ่งหมายเลขพอร์ตจะได้มาจากการใช้คำสั่ง digitalPinToPort()

ตัวอย่างการเขียน-อ่านสถานะจากตำแหน่งรีจิสเตอร์โดยตรง

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

ตัวอย่าง อ่านค่าสถานะ Digital PIN 2 จากตำแหน่งรีจิสเตอร์

เปิด มาเทสดูผลได้เลยครับ

12-5-2559 23-08-13

ตัวอย่าง การเขียนค่าไปที่ Digital PIN 13 จากตำแหน่งรีจิสเตอร์

อัพโหลดแล้ว LED ที่ D13 ก็จะกระพริบ

ทดสอบความเร็วจากการใช้รีจิสเตอร์

การใช้ทำไฟกระพริบ หรืออ่านค่าสวิตซ์อาจจะธรรมดาเกินไป ไม่มีความจำเป็นที่จะต้องใช้รีจิสเตอร์ ดังนั้นเรามาลองทำอะไรที่มันจำเป็นต้องใช้รีจิสเตอร์เท่านั้นในการทำงาน จึงจะบรรลุเป้าหมายที่ได้ตั้งไว้ครับ

ทดสอบสร้างความถี่ที่เร็วที่สุดเท่าที่จะเป็นไปได้

โค้ดแบบใช้ digitalWrite() ครับ

ใช้สโครปวัดแล้วได้ความถี่ประมาณ 88KHz


ส่วนโค้ดแบบใช้รีจิสเตอร์

ใช้สโครปวัดแล้ว สามารถทำความถี่ขึ้นไปสูงถึง 300KHz เลยทีเดียวครับ ส่วนค่าดิวตี้ไซเคิลไม่เท่ากันเพราะตอนเซ็ตเป็น HIGH มันง่ายกว่าเซ็ตเป็น LOW ครับ (ตอนเป็น LOW ต้องใช้ทั้ง AND และ NOT ส่วนตอนเป็น HIGH ใช้แค่ OR)

digitalRead() vs Register ไม่สามารถทดสอบได้

แน่นอนว่าการใช้ Register ต้องเร็วกว่าแน่นอนเพราะผมทดสอบเอาไปอ่านค่าสัญญาณจาก DHT11 แล้ว การใช้ digitalRead() มันทำงานไม่ทัน แต่ก็คิดวิธีการทดสอบเทียบกันตรงๆไม่ออกเหมือนกันครับ ดังนั้นแนะนำให้ลองเอาไปใช้จริงด้วยตัวเองเลยดีกว่าครับ

ส่งท้าย

การใช้ Register สามารถใช้งานได้กับงานที่ต้องการความรวดเร็วอย่างมาก โดยเฉพาะไมโครคอนโทรลเลอร์ความถี่แค่ 16MHz การนำไปอ่านค่าสัญญาณที่มีหน่วยความเปลี่ยนแปลงเป็น uS นั้น จะต้องใช้คำสั่งที่ CPU ทำงานน้อยที่สุดเพื่อที่จะได้อ่านค่าสัญญาณที่ส่งมาได้ทันเวลาครับ

สำหรับการใช้งานอื่นๆที่ไม่ต้องการความเร็วนัก เช่น การเปลี่ยนสถานะหลอด LED หรือการควบคุมรีเลย์ งานเหล่านี้ การใช้คำสั่ง digitalRead() digitalWrite() ยังถือว่าเหมาะสมอยู่ครับ 🙂

  • Vichagorn Lupponglung

    ขอสอบถามหน่อยครับ

    bit = digitalPinToBitMask(2) : ทำให้ตัวแปร bit มีค่าเท่ากับ 0000 0010
    port = digitalPinToPort(pin_bus) : ทำให้ตัวแปร port เก็บค่า PORTD
    rPIN = portInputRegister(port) : ทำให้ rPIN ดึงสถานะปัจุบันของ PORTD คือ 0000 0000

    *rPIN |= bit : ทำให้ PORTD มีสถานะ (0000 0000)|(0000 0010) เท่ากับ 0000 0010
    *rPIN &= ~bit; ทำให้ PORTD มีสถานะ (0000 0010)&(1111 1101) เท่ากับ 0000 0000

    ใช่ไหมครับ?
    บทความดีมากเลยครับ Advanced ดีเล่นกันระดับ Register เลยย
    จากสโครปเร็วจนอยากเข้าไปแก้ code หลักบ้างตัวเล่นๆที่ใช้งานเลยครับ ขอบคุณมากครับ

    • http://www.elec-za.com/ Sonthaya Nongnuch

      ถูกต้องครับ แต่ว่าขา 2 อยู่บิตที่ 3 ครับ ขา 1 อยู่บิตที่ 1 และขา 0 อยู่บิตที่ 1 ครับ ดังนั้น bit จึงมีค่า 0000 0100 ครับ