Arduino C/C++ versus MicroPython

ตัวอย่างสำหรับการเปรียบเทียบรูปแบบการเขียนโค้ดด้วยภาษา Arduino C/C++ และไมโครไพธอนสำหรับบอร์ดไมโครคอนโทรลเลอร์ ESP32

ตัวอย่างที่ 1: การคำนวณเลขยกกำลังฐานสอง

ถ้าเราต้องการเขียนโค้ดเพื่อคำนวณค่าของเลขจำนวนเต็ม เช่น "สองยกกำลังสี่สิบลบด้วยหนึ่ง" ให้เป็นค่าของตัวแปร x และแสดงค่าของตัวแปรให้เป็นเลขฐานสิบ (decimal) และฐานสิบหก (hexadecimal) เราจะเขียนโค้ดอย่างไร ?

x=2401x = 2^{40} -1

ในกรณีนี้ การเก็บข้อมูลสำหรับตัวแปร x จะต้องมีขนาดหน่วยความจำอย่างน้อย 40 บิต หรือ 5 ไบต์

ข้อความเอาต์พุตจากการทำงานของโค้ดสำหรับค่าของตัวแปร x จะต้องได้ตามรูปแบบต่อไปนี้

x = 1099511627775 (dec)
x = 0xffffffffff (hex)

ถ้าเขียนโค้ดด้วยไมโครไพธอน ก็สามารถเขียนโดยใช้คำสั่งต่อไปนี้

x = 2**40 - 1
print( 'x = {} (dec)'.format(x) )
print( 'x = 0x{:x} (hex)'.format(x) )

การเขียนนิพจน์ 2**40 หมายถึง 2 ยกกำลัง 40 โดยที่ ** เป็นโอเปอร์เรเตอร์ (Operator) สำหรับเลขยกกำลังในภาษาไพธอน

การแสดงข้อความเอาต์พุตสำหรับค่าของตัวแปร ทำได้ โดยใช้คำสั่ง print() ร่วมกับคำสั่ง format() เมื่อใช้กับข้อมูลแบบstring

ถัดไปลองมาดูโค้ดสำหรับ Arduino-ESP32 เปรียบเทียบกัน

void setup() {
    Serial.begin( 115200 );
    uint64_t x = (1LLU << 40) - 1;
    
    Serial.printf( "x = %llu (dec)\n", x );
    Serial.printf( "x = 0x%llx (hex)\n", x );      
}

void loop() { }

ในกรณีนี้ เราจะใช้โอเปอร์เรเตอร์เลื่อนบิต (Shift Operator) ไปทางซ้าย (<<) แทนการคำนวณเลขยกกำลังสำหรับฐานสอง และเขียนนิพจน์ในภาษา C/C++ ได้เป็น (1LLU << 40) และจะต้องใช้ชนิดของข้อมูลแบบ unsigned long long หรือ uint64_t ซึ่งมีขนาด 8 ไบต์ (64 บิต) ในกรณีตัวอย่างนี้ เราไม่สามารถใช้ชนิดข้อมูลเลขจำนวนเต็มขนาด 32 บิต เนื่องจากต้องใช้อย่างน้อย 40 บิต

การแสดงข้อความเอาต์พุตสำหรับค่าของตัวแปร ทำได้โดยใช้คำสั่ง Serial.printf()

โครงสร้างสำหรับการเขียนโค้ด Arduino Sketch จะต้องมีฟังก์ชัน void setup() {...} สำหรับโค้ดที่ต้องทำเมื่อเริ่มต้น และ void loop() {...} สำหรับโค้ดที่ต้องทำซ้ำไปเรื่อย ๆ หลังจากนั้น

หรืออีกกรณีหนึ่ง เราอาจใช้ฟังก์ชัน pow() สำหรับการคำนวณเลขยกกำลังดังนี้

แต่มีข้อสังเกตและข้อควรระวังในการใช้งานคือ ฟังก์ชัน pow() ของ Arduino API นั้น จะได้ผลลัพธ์จากการคำนวณเป็น double ที่มีขนาด 8 ไบต์ (สำหรับ ESP32 หรือตัวประมวลผลขนาด 32 บิต) ตามรูปแบบมาตรฐาน IEEE 754 Double-Precision ประกอบด้วย

  • 1 บิต: Sign Bit (S)

  • 11 บิต: Exponent (e)

  • 52 บิต: Fraction (F)

(1)S(1.b51b50...b1b0F)2×2e1023(-1)^{S}(1.\underbrace{b_{51}b_{50}\,...\,b_{1}b_{0}}_{F})_{\,2} \times 2^{\,e-1023}

ในกรณีตัวอย่างนี้ ถ้าจะคำนวณค่า "2 ยกกำลัง 40 แล้วลบด้วย 1" ก็สามารถเก็บค่าจำนวนเต็มในส่วนของ Fraction ขนาด 52 บิต ร่วมกับส่วนที่เป็น Exponent ขนาด 11 บิต ของ double ได้ และค่าที่ได้จะต้องถูกแปลงชนิดข้อมูล (Type Casting) จาก double ให้เป็น uint64_t เพื่อให้ได้เลขจำนวนเต็มบวก

ตัวอย่างที่ 2: การหาค่าเชิงสถิติจากรายการข้อมูลตัวเลข

ในตัวอย่างถัดไป มีโจทย์ดังนี้ ถ้ามีตัวเลขจำนวนเต็ม เช่น 5, 13, 33, 46, 50, 54, 67, 87, 95 และต้องการหาค่าผลรวม (Summation) ค่าต่ำสุด (Minimum) ค่าสูงสุด (Maximum) และค่าเฉลี่ย (Average) ของข้อมูลชุดนี้ แล้วแสดงเป็นข้อความเอาต์พุตสำหรับผลลัพธ์ที่ได้ จะเขียนโค้ดอย่างไร ?

กำหนดให้ตัวอย่างข้อความเอาต์พุตสำหรับข้อมูลตัวเลขดังกล่าว เป็นดังนี้

ในกรณีของไมโครไพธอน เราก็สามารถเขียนโค้ดได้ โดยสร้างรายการหรือลิสต์ (List) ของเลขจำนวนเต็ม จากนั้นก็ใช้คำสั่ง min(), max(), len(), sum() หาค่าต่ำสุด ค่าสูงสุด จำนวนข้อมูลสมาชิกทั้งหมดในรายการ และผลรวมของข้อมูลเหล่านั้น

ถ้าจะเขียนโค้ด Arduino Sketch ให้ได้ผลลัพธ์เหมือนกัน อาจเขียนโค้ดตามรูปแบบต่อไปนี้ โดยใช้ชนิดข้อมูลเป็น int32_t (ข้อมูลแบบ signed integer ขนาด 32 บิต หรือ 4 ไบต์)

ตัวอย่างที่ 3: การคำนวณค่า CRC8 สำหรับลำดับข้อมูลไบต์

การคำนวณค่า Cyclic Redundancy Check (CRC) เป็นวิธีการในการตรวจสอบความผิดพลาดของข้อมูล (Error Detection) เช่น ถ้ามีข้อมูลไบต์ตามลำดับอยู่จำนวนหนึ่ง เราสามารถนำไปคำนวณค่า CRC (มีขนาด n บิต เช่น n=8, 16, 32 สำหรับ CRC-8, CRC-16, CRC-32 เป็นต้น) โดยใช้อัลกอริธึม หรือวิธีการคำนวณแบบใดแบบหนึ่ง (มีหลายรูปแบบให้เลือกใช้) ค่าที่ได้นี้เรียกว่า CRC checksum

ถ้ามีการเปลี่ยนแปลงของข้อมูลเกิดขึ้น เช่น ระหว่างการสื่อสารรับส่งข้อมูล เราก็สามารถคำนวณค่า CRC checksum ของข้อมูลที่ได้รับ แล้วเปรียบเทียบกับค่า CRC checksum ที่ได้มีการคำนวณไว้ก่อนแล้วและนำมาเปรียบเทียบกัน

โดยทั่วไป ข้อมูลจะถูกมองว่าเป็นลำดับบิตหรือเลขฐานสอง (Bitstream) ถ้ามีหลายไบต์ก็นำมาเรียงต่อกัน บิตในแต่ลำตำแหน่งจะถูกใช้เป็นค่าสัมประสิทธิ์แบบไบนารี (Binary Coefficient) ของพหุนาม (Polynomial) ที่มีดีกรีเท่ากับจำนวนบิตทั้งหมด

จากนั้นจึงนำไปหารด้วยพหุนามฐานสองที่มีดีกรีเท่ากับ n (เรียกว่า Polynomial Divisor หรือ Generator Polynomial) เพื่อคำนวณหาเศษที่เหลือจากการหาร (Remainder) การหารในเลขฐานสองทำได้ง่าย โดยใช้โอเปอร์เรเตอร์พื้นฐาน 2 ชนิด คือ Exclusive-OR (XOR) และการเลื่อนบิต (Bit Shifting)

ตัวอย่างของหหุนามตัวหารสำหรับ CRC-8 (n=8) เช่น

  • x^8+x^2+x^1+1 : เขียนสัมประสิทธิ์เป็นเลขฐานสองได้เป็น "100000111" (0x107)

  • x^8+x^5+x^4+1 : เขียนสัมประสิทธิ์เป็นเลขฐานสองได้เป็น "100110001" (0x131)

รูปแบบการคำนวณค่า CRC มีหลายวิธี เช่น ขึ้นอยู่กับหหุนามตัวหารที่ได้เลือกมาใช้งาน และทิศทางของบิต (Bit Ordering) เช่น MSB First / normal order หรือ LSB First / reverse order

ในตัวอย่างนี้ สมมุติว่า เราต้องการคำนวณค่า CRC-8 สำหรับลำดับข้อมูลไบต์ดังนี้ 0x34, 0xff, 0x40, 0x52, 0x67, 0x01 และเลือกใช้วิธีการตามรูปแบบที่เรียกว่า CRC-8 Dallas/Maxim ซึ่งจะได้ผลลัพธ์เท่ากับ 0x46 และตัวอย่างข้อความเอาต์พุตมีดังนี้

การคำนวณ CRC-8 ตามรูปแบบที่กำหนดโดยบริษัท Dallas/Maxim (อ้างอิงจากเอกสาร Maxim Application Note 27: "Understanding and Using Cyclic Redundancy Checks with Maxim 1-Wire and iButton Products", 2001) มีการใช้พหุนามตัวหาร x^8 + x^5 + x^4 + 1 (0x131) แต่ใช้ทิศทางหรือลำดับของบิตแบบ LSB First / Reverse Order

โค้ดไมโครไพธอนมีดังนี้

จากตัวอย่างโค้ดในภาษาไมโครไพธอน ถ้าจะเขียนด้วย Arduino Sketch ก็มีตัวอย่างโค้ดเอาไว้เปรียบเทียบกันดังนี้

ตัวอย่างที่ 4: การสร้างเลขสุ่มขนาด 32 บิต แสดงเป็นเลขฐานสอง

ตัวอย่างถัดไปเป็นการสาธิตการสร้างเลขสุ่ม (Pseudo-Random Number Generation) ให้เป็นเลขจำนวนเต็มขนาด 32 บิต และแสดงเป็นข้อความเอาต์พุตให้เป็นเลขฐานสอง จะมีวิธีการเขียนโค้ดอย่างไร ?

ในตัวอย่างนี้ การตั้งค่าเริ่มต้นหรือ Seed ให้กับตัวสร้างเลขสุ่ม เราจะใช้วิธีการอ่านค่าจากขาแอนะล็อกอินพุตที่ไม่ได้ต่อใช้งาน (Floating Input) กับวงจรหรือสัญญาณใด ๆ ของบอร์ด ESP32 และค่าที่อ่านได้เมื่อโปรแกรมทำงานในแต่ละครั้ง ควรจะได้ไม่ซ้ำเดิม

ตัวอย่างการเขียนโค้ดไมโครไพธอนมีดังนี้

ในโค้ดตัวอย่าง ได้เลือกใช้ขา GPIO-32 / ADC1_CH4 และใช้คำสั่งของคลาส ADC อ่านค่าอินพุตแล้วนำมาใช้เป็นค่า Seed สำหรับการทำงานของคลาส random

คำสั่ง random.getrandbits() จะสร้างเลขสุ่มตามจำนวนบิตที่ต้องการ (ในกรณีนี้คือ 32 บิต) และได้เป็ข้อมูลเลขจำนวนเต็ม จากนั้นจึงใช้คำสั่ง bin() แปลงเป็นข้อความเลขฐานสอง (32-bit Binary String)

แต่ถ้าเขียนโค้ด Arduino Sketch ก็มีตัวอย่างดังนี้

คำสั่ง analogRead() ใช้สำหรับการอ่านค่าจากขาแอนะล็อกอินพุต คำสั่ง randomSeed() กำหนดค่าเริ่มต้นสำหรับตัวสร้างเลขสุ่ม และการใช้คำสั่ง random(0,2) จะสุ่มตัวเลขได้เป็น 0 หรือ 1

ESP32 มีวงจรภายในที่เรียกว่า Hardware Random Number Generator (RNG) และสามารถนำมาใช้สร้างเลขสุ่มขนาด 32 บิต แต่จะต้องเปิดใช้งาน Wi-Fi หรือ Bluetooth ร่วมด้วย และฟังก์ชันสำหรับสร้างเลขสุ่มโดยใช้ RNG คือ esp_random() ซึ่งมีการประกาศไว้ในไฟล์ esp_system.h)

เราสามารถเขียนโค้ดอีกกรณีหนึ่งได้ดังนี้

ตัวอย่างโค้ดที่ 5: การทำให้ LED กระพริบ

ในตัวอย่างนีัสาธิตการทำให้ LED กระพริบ โดยเลือกใช้ขา GPIO ของ ESP32 เช่น GPIO-22 หรือ GPIO-5 เป็นเอาต์พุต (เลือกขาให้ตรงกับ Onboard LED) และมีการกำหนดสถานะเอาต์พุต High / Low สลับกันไปเรื่อย ๆ โดยเว้นระยะเวลาประมาณ 500 มิลลิวินาที

ถ้าเขียนโค้ดด้วยไมโครไพธอน ก็ทำได้ดังนี้

หรือถ้าจะเขียนแบบไม่ใช้คำสั่ง time.sleep_ms() ก็ทำได้โดยใช้วิธีตรวจสอบระยะเวลาในการอัปเดตสถานะของ LED ในครั้งถัดไป

แต่ถ้าจะเขียนโค้ด Arduino Sketch ก็มีตัวอย่างดังนี้

หรือใช้วิธีตรวจสอบระยะเวลาในการอัปเดตสถานะของ LED ในครั้งถัดไป

โค้ดตัวอย่างที่ 6: การแปลงค่า BCD ให้เป็นข้อมูลบิตสำหรับ 7-Segment Display

ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อแปลงค่าแบบ BCD (Binary-Coded Decimal) ขนาด 4 บิต ให้กลายเป็นข้อมูลขนาด 7 บิต (หรือขนาด 1 ไบต์) เพื่อแสดงตัวเลข 0..9 สำหรับ 7-Segment Display (สำหรับเซ็กเมนต์ 7 ส่วน ได้แก่ a, b, c,..., g ) จำนวนหนึ่งหลัก (ทำงานหรือต่อวงจรแบบ Common-Cathode) โดยใช้บิตที่ 0 (LSB) สำหรับเซ็กเมนต์ a, บิตที่ 1 สำหรับเซ็กเมนต์ b ไปตามลำดับ

ค่าตัวเลข BCD (4 บิต)

ฐานสิบหก (gfedcba)

ฐานสอง (gfedcba)

0

0x3f

0b00111111

1

0x06

0b00000110

2

0x5b

0b01011011

3

0x4f

0b01001111

4

0x66

0b01100110

5

0x6d

0b01101101

6

0x7d

0b01111101

7

0x07

0b00000111

8

0x7f

0b01111111

9

0x6f

0b01101111

10 .. 15

0x00

0b00000000

ลองมาดูโค้ดสำหรับไมโครไพธอน เราสร้างฟังก์ชัน bcd2seg() และใช้ประโยคคำสั่งแบบ if-elif ตรวจสอบค่า bcd ที่รับมาเป็นอาร์กิวเมนต์ ไปทีละกรณี ซึ่งมีทั้งหมด 16 กรณี เพื่อระบุค่า value ที่ได้จากการทำงานของฟังก์ชันนี้

ตัวอย่างข้อความเอาต์พุต

หรือจะใช้วิธีอ่านค่าจากตารางค่าคงที่ (Lookup Table) จากอาร์เรย์ ขนาด 16 ไบต์

ในการเขียนโค้ดสำหรับ Arduino Sketch เพื่อตรวจสอบเงื่อนไข เราสามารถใช้ประโยค switch-case ได้แทนการใช้ประโยค if-else ในกรณีตัวอย่างนี้

หรือเขียนโค้ดโดยใช้วิธีอ่านค่าจากตารางค่าคงที่ มึจำนวนข้อมูล 16 ไบต์

โค้ดตัวอย่างที่ 7: การตรวจสอบอินเทอร์รัพท์ภายนอกจากปุ่มกด

ตัวอย่างถัดไป สาธิตการเขียนโค้ดเพื่อตรวจสอบดูว่า มีการกดปุ่มหรือไม่ โดยเปิดใช้งานอินเทอร์รัพท์สำหรับขา GPIO ที่เกี่ยวข้อง เช่น เลือกใช้ขา GPIO-23

การเขียนโค้ดไมโครไพธอนในตัวอย่างนี้ จะต้องมีการใช้คลาส machine.Pin โดยสร้างอ็อปเจกต์จากคลาสนี้ เพื่อใข้งานในโหมดอินพุต และเปิดใช้งานวงจรภายในสำหรับตัวต้านทานแบบ Pull-Up

ถัดไปมีการเปิดใช้งาน IRQ (Interrupt Request) สำหรับขา GPIO ดังกล่าว และจะต้องระบุว่า ให้ตรวจสอบการเกิดเหตุการณ์แบบใด ในกรณีนี้คือ ขอบขาลง หรือ Falling Edge และสร้างฟังก์ชัน btn_callback() เพื่อทำหน้าที่เป็น Callback Function หรือ Interrupt Handler ทุกครั้งที่เกิดเหตุการณ์ ฟังก์ชันนี้จะทำให้ค่าของตัวแปร num_presses เพิ่มขึ้นทีละหนึ่ง

ในส่วนของ Main Loop ที่ใช้ประโยคแบบ while True จะต้องมีการตรวจสอบค่าของตัวแปร num_presses ถ้ามีค่ามากกว่า 0 แสดงว่า มีการกดปุ่มเกิดขึ้น

ถ้าเกิดเหตุการณ์สองครั้งตามลำดับ มีระยะห่างอย่างน้อย 100 มิลลิวินาที และค่าของอินพุตจะต้องคงที่ (stable) เป็น 0 ในช่วงเวลาสั้น ๆ หลังเกิดเหตุการณ์จากการกดปุ่ม จะถือว่าเป็นการกดปุ่มที่ถูกต้อง เพื่อลดปัญหาของ Button Bouncing และถ้ากดปุ่มไปทั้งหมด 10 ครั้งแล้ว ให้ปิดการทำงานของ IRQ สำหรับขาอินพุตดังกล่าว

ลองมาดูตัวอย่างการเขียนโค้ด Arduino C/C++ ในเชิงเปรียบเทียบกัน

ในการเปิดหรือปิดการใช้งานอินเทอร์รัพท์ที่ขา GPIO ของ ESP32 เราใช้คำสั่ง attachInterrupt() และ detachInterrupt() ตามลำดับ (ผู้อ่านสามารถศึกษาการทำงานของคำสั่งนี้ได้จากโค้ด esp32-hal-gpio.h และ esp32-hal-gpio.c หรือโค้ดตัวอย่าง GPIOInterrupt.ino)

โค้ดตัวอย่างที่ 8: การเรียงลำดับข้อมูลในอาร์เรย์

ตัวอย่างนี้สาธิตการสุ่มตัวเลขซึ่งมีทั้งหมด N (เช่น N=32) และเป็นเลขจำนวนเต็มในช่วงที่กำหนด [0, N-1] นำไปใส่ลงในอาร์เรย์ แล้วแสดงค่าตัวเลข ก่อนและหลังการเรียงข้อมูลจากน้อยไปมาก

โค้ดไมโครไพธอนมีดังนี้

ถ้าจะเขียนโค้ดสำหรับ Arduino ESP32 ก็มีแนวทางดังนี้

สำหรับการเรียงข้อมูลในอาร์เรย์ สามารถใช้คำสั่ง qsort() ได้ นอกจากตัวแปรแบบพอยน์เตอร์ที่อ้างอิงอาร์เรย์ของข้อมูล จำนวนข้อมูล และขนาดของข้อมูลแต่ละตัว ยังจะต้องระบุฟังก์ชันในการเปรียบเทียบข้อมูลด้วย ในตัวอย่างนี้ เราสร้างและใช้ฟังก์ชันแบบที่เรียกว่า Lambda (Anonymous) Function

โค้ดตัวอย่างที่ 9: การใช้งาน Timer

ESP32 มีวงจร Timer อยู่ภายใน จำนวน 4 ชุด (หมายเลข 0...3) ตัวอย่างต่อไปนี้สาธิตการเปิดใช้งานวงจร Timer และสร้างอินเทอร์รัพท์จากการนับครบหนึ่งช่วงเวลา (Time Interval) เช่น กำหนดให้คาบเวลาเท่ากับ 500 มิลลิวินาที และสลับสถานะลอจิกของ LED เมื่อเกิดอินเทอร์รัพท์จาก Timer และฟังก์ชัน Callback ทำงานในแต่ละครั้ง

การเขียนโค้ดไมโครไพธอน มีดังนี้

สำหรับการเขียนโค้ด Arduino ESP32 เพื่อใช้งาน Timer ก็มีคำสั่งที่เกี่ยวข้องดังนี้ (ศึกษาเพิ่มเติ่มได้จากไฟล์ esp32-hal-timer.c)

  • timerBegin() สร้างไทม์เมอร์ (hw_timer_t *) อ้างอิงโดยใช้ตัวแปรแบบพอยน์เตอร์ กำหนดค่าตัวหารความถี่ (Prescaler) และเลือกโหมดการนับ

  • timerAlarmWrite() กำหนดค่าสูงสุดของการนับ (หรือคาบของการนับ) และจะให้เกิดซ้ำหรือทำเพียงครั้งเดียว

  • timerAlarmEnable() เปิดการใช้งานอินเทอร์รัพท์ของไทม์เมอร์

  • timerAttachInterrupt()ระบุฟังก์ชันสำหรับทำหน้าที่ Callback เมื่อเกิดอินเทอร์รัพท์แบบ edge

ในตัวอย่างนี้ ตั้งค่าอัตราการนับให้เท่ากับ 1MHz (ตั้งค่า Prescaler = 80 เพื่อหารความถี่ 80MHz ของบัส APB) และมีคาบหรือช่วงเวลาในการนับเท่ากับ 500,000 หรือ 500 msec

กล่าวสรุป

การศึกษาและเปรียบเทียบโค้ดในภาษาไมโครไพธอนและภาษา C/C++ (สำหรับ Arduino) โดยใช้บอร์ดไมโครคอนโทรลเลอร์ ESP32 เป็นกรณีศึกษา จะช่วยให้เห็นความเหมือนและความแตกต่างในการเขียนโค้ดในแต่ละภาษาได้ดีขึ้น รวมถึงการใช้คำสั่งต่าง ๆ ที่เกี่ยวข้อง

Last updated

Was this helpful?