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() สำหรับการคำนวณเลขยกกำลังดังนี้

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

void loop() { }

แต่มีข้อสังเกตและข้อควรระวังในการใช้งานคือ ฟังก์ชัน 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) ของข้อมูลชุดนี้ แล้วแสดงเป็นข้อความเอาต์พุตสำหรับผลลัพธ์ที่ได้ จะเขียนโค้ดอย่างไร ?

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

Sum: 450
 Min: 5
 Max: 95
Avg.: 50.000

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

data = [5,13,33,46,50,54,67,87,95]
n    = len(data)
_min = min(data)
_max = max(data)
_sum = sum(data)

print( ' Sum: {}'.format( _sum ) )
print( ' Min: {}'.format( _min ) )
print( ' Max: {}'.format( _max ) )
print( 'Avg.: {:.3f}'.format( _sum/n ) )

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

void setup() {
   Serial.begin( 115200 );
   int32_t data[] = { 5,13,33,46,50,54,67,87,95 };
   int32_t _min=data[0], _max=data[0];
   int32_t _sum = 0;
   int n = sizeof(data)/sizeof(data[0]);
   
   for (int i=0; i < n; i++) {
     _min = min( data[i],_min );
     _max = max( data[i],_max );
     _sum += data[i];
   }
   Serial.printf( " Sum: %d\n", _sum );
   Serial.printf( " Min: %d\n", _min );
   Serial.printf( " Max: %d\n", _max );
   Serial.printf( "Avg.: %.3lf\n", ((double)_sum)/n );
}

void loop() { } // no action

ตัวอย่างที่ 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 และตัวอย่างข้อความเอาต์พุตมีดังนี้

data = [0x34,0xff,0x40,0x52,0x67,0x01]
crc8 = 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

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

def CRC8(data,num_bytes):
    crc = 0x00 # initialize the remainder
    for i in range(num_bytes):
        x = data[i]
        for j in range(8):
            b = (crc ^ x) & 0x01
            crc >>= 1
            if b != 0:
                crc ^= 0x8C
            x >>= 1
    return crc

data = [ 0x34,0xff,0x40,0x52,0x67,0x01 ]
s = ','.join( ['0x{:02x}'.format(x) for x in data] )
print( 'data = [{}]'.format( s ) )
crc8 = CRC8( data, len(data) )
print( 'crc8 = 0x{:02x}'.format(crc8) )

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

byte CRC8( const byte *data, byte num_bytes ) {
  byte crc = 0x00; // initialize the remainder
  for ( int i=0; i < num_bytes; i++ ) {
    byte x = data[i];
    for ( byte j=0; j < 8; j++ ) {
      byte b = (crc ^ x) & 0x01;
      crc >>= 1;
      if (b) {
        crc ^= 0x8C;
      }
      x >>= 1;
    }
  }
  return crc;
}

void setup() {
   Serial.begin( 115200 );
   byte data[] = { 0x34,0xff,0x40,0x52,0x67,0x01 };
   byte n = sizeof(data)/sizeof(byte);
   byte crc8 = CRC8( data, n );
   
   Serial.print( "data = [" );
   for ( int i=0; i < n; i++ ) {
      if (i != 0) {
         Serial.print( "," );
      }
      Serial.printf( "0x%02x", data[i] );
   }
   Serial.println( "]" );
   Serial.printf( "crc8 = 0x%02X\n", crc8 );
}

void loop() { }

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

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

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

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

import urandom as random
from machine import Pin, ADC

adc = ADC(Pin(32))  # use GPIO-32 pin
seed = adc.read()   # read analog value
print( 'Seed: {}'.format(seed) )
random.seed( seed ) # set the seed value
bin_str = bin( random.getrandbits(32) )[2:]
print( bin_str )    # show the binary string

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

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

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

void setup() {
   Serial.begin(115200);
   int seed = analogRead(32); // read analog GPIO-32 pin
   Serial.printf( "Seed = %d\n", seed );
   
   // initialize the pseudo-random number generator
   randomSeed( seed ); // set the seed value
   
   String bin_str = "";
   for ( int i=0; i < 32; i++ ) { // 32 bits
       int value = random(0,2); // either 0 or 1
       bin_str += value;
   }
   Serial.printf( "%s\n", bin_str.c_str() );
}

void loop() { }

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

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

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

#include <WiFi.h> // for WiFi.begin()

void setup() {
   Serial.begin(115200);
   WiFi.begin(); // enable WiFi
   
   String bin_str = "";
   for ( int i=0; i < 32; i++ ) { // 32 bits
       int value = esp_random() % 2;
       bin_str += value;
   }
   Serial.printf( "%s\n", bin_str.c_str() );
}

void loop() { }

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

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

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

from micropython import const
import time
from machine import Pin

LED_PIN = const(5) # use GPIO-5 for LED output
led = Pin( LED_PIN, Pin.OUT )
print( "LED Pin:", LED_PIN )

while True:
    state = not led.value() # read value and invert it
    print( 'LED state:', int(state) )
    led.value( state )  # update LED output
    time.sleep_ms( 500 )

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

from micropython import const
import time
from machine import Pin

LED_PIN = const(5) # use GPIO-5 for LED output
led = Pin( LED_PIN, Pin.OUT )
print( "LED Pin:", LED_PIN )

last_update = time.ticks_ms()
while True:
    now = time.ticks_ms()
    if time.ticks_diff( now, last_update ) >= 500:
        last_update = now # save last update time
        state = not led.value() # read value and invert it
        print( 'LED state:', int(state) )
        led.value( state ) # update LED output

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

const int LED_PIN = 22;

void setup() {
   Serial.begin( 115200 );
   pinMode( LED_PIN, OUTPUT );
   Serial.printf( "LED Pin: %d\n", LED_PIN ); 
}

void loop() {
   int state = !digitalRead( LED_PIN );
   digitalWrite( LED_PIN, state );
   Serial.printf( "LED: %d\n", state );
   delay( 500 );
}

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

const int LED_PIN = 22;

uint32_t last_update, now;

void setup() {
   Serial.begin( 115200 );
   pinMode( LED_PIN, OUTPUT );
   Serial.printf( "LED Pin: %d\n", LED_PIN ); 
   last_update = millis();
}

void loop() {
   now = millis();
   if ( now - last_update >= 500 ) {
      last_update = now; 
      int state = ! digitalRead( LED_PIN );
      digitalWrite( LED_PIN, state );
      Serial.printf( "LED: %d\n", state );
   }
}

โค้ดตัวอย่างที่ 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 ที่ได้จากการทำงานของฟังก์ชันนี้

def bcd2seg( bcd, invert=False ): # BCD to 7-segment decode
    if bcd == 0:
        value = 0x3f
    elif bcd == 1:
        value = 0x06
    elif bcd == 2:
        value = 0x5b
    elif bcd == 3:
        value = 0x4f
    elif bcd == 4:
        value = 0x66
    elif bcd == 5:
        value = 0x6d
    elif bcd == 6:
        value = 0x7d
    elif bcd == 7:
        value = 0x07
    elif bcd == 8:
        value = 0x7f
    elif bcd == 9:
        value = 0x6f
    else:
        value = 0x00
    if invert:
        value ^= 0xff
    return value
    
for i in range(16):
   value = bcd2seg(i)
   bin_str = '0b{:08b}'.format(value)
   hex_str = '0x{:02x}'.format(value)
   print( "{:2d} => {}, {}".format(i, hex_str, bin_str) )

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

 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 => 0x00, 0b00000000
11 => 0x00, 0b00000000
12 => 0x00, 0b00000000
13 => 0x00, 0b00000000
14 => 0x00, 0b00000000
15 => 0x00, 0b00000000

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

LOOKUP_TABLE = [
    0x3f, 0x06, 0x5b, 0x4f, 
    0x66, 0x6d, 0x7d, 0x07, 
    0x7f, 0x6f, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, ]
    
def bcd2seg( bcd, invert=False ):
    return LOOKUP_TABLE[bcd & 0x0f]
    
for i in range(16):
   value = bcd2seg(i)
   bin_str = '0b{:08b}'.format(value)
   hex_str = '0x{:02x}'.format(value)
   print( "{:2d} => {}, {}".format(i, hex_str, bin_str) )

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

byte bcd2seg( byte bcd, bool invert=false ) {
    byte value;
    switch (bcd) {
      case 0: value = 0x3f; break; // 0b00111111 
      case 1: value = 0x06; break; // 0b00000110 
      case 2: value = 0x5b; break; // 0b01011011
      case 3: value = 0x4f; break; // 0b01001111 
      case 4: value = 0x66; break; // 0b01100110
      case 5: value = 0x6d; break; // 0b01101101 
      case 6: value = 0x7d; break; // 0b01111101 
      case 7: value = 0x07; break; // 0b00000111
      case 8: value = 0x7f; break; // 0b01111111
      case 9: value = 0x6f; break; // 0b01101111
      default:
        value = 0x00; 
    }
    if (invert) { 
      value ^= 0xff; // bit-inverting
    }
    return value;
}

void byte2bin_str( byte x, String& s ) {
    s = "0b";
    for ( int i=7; i >= 0; i-- ) {
       s += (x & 0x80) ? 1 : 0;
       x <<= 1;
    }
}

void setup() {
   Serial.begin(115200);
   String s;
   for ( int i=0; i < 16; i++ ) {
      byte value = bcd2seg(i);      
      byte2bin_str( value, s );
      Serial.printf( "%2d => 0x%02x, %s\n",
      i, value, s.c_str() );
   }
}

void loop() { }

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

const byte LOOKUP_TABLE[16] = {
    0x3f, 0x06, 0x5b, 0x4f, 
    0x66, 0x6d, 0x7d, 0x07, 
    0x7f, 0x6f, 0x00, 0x00, 
    0x00, 0x00, 0x00, 0x00, 
};

byte bcd2seg( byte bcd, bool invert=false ) {
    byte value = LOOKUP_TABLE[ bcd & 0x0f ];
    if (invert) { 
      value ^= 0xff; // bit-inverting
    }
    return value;
}

void byte2bin_str( byte x, String& s ) {
    s = "0b";
    for (int i=7; i >= 0; i--) {
       s += (x & 0x80) ? 1 : 0;
       x <<= 1;
    }
}

void setup() {
   Serial.begin(115200);
   String s;
   for ( int i=0; i < 16; i++ ) {
      byte value = bcd2seg(i);      
      byte2bin_str( value, s );
      Serial.printf( "%2d => 0x%02x, %s\n",
        i, value, s.c_str() );
   }
}

void loop() { }

โค้ดตัวอย่างที่ 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 สำหรับขาอินพุตดังกล่าว

from micropython import const
from machine import Pin
import machine, time

BTN_PIN = const(23) # use GPIO-23 for input

# global variables
num_presses = 0 
last_event = time.ticks_ms()
cnt = 0

def btn_callback(pin): # IRQ handler
    global num_presses
    num_presses += 1
    
btn = Pin( BTN_PIN, Pin.IN, Pin.PULL_UP )
btn.irq( trigger=Pin.IRQ_FALLING, handler=btn_callback )

while True:
    if num_presses > 0:
        is_input_low = True
        for i in range(5):
            if btn.value() != 0: # input not stable
                is_input_low = False
                break
            time.sleep_ms(2)
            
        now   = time.ticks_ms()
        delta = time.ticks_diff( now, last_event )
        
        if delta >= 100 and is_input_low:
           cnt += 1
           print( 'Button pressed: #{}'.format(cnt) )
           if cnt == 10:
             btn.irq( handler=None ) # disable IRQ
             print('Disabled IRQ for push button')
             
        last_event = now
        num_presses = 0

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

const int BTN_PIN = 23; // use GPIO-23 for input

// global varaibles
uint32_t last_event;
int cnt = 0;

volatile int num_presses = 0;

void IRAM_ATTR btn_callback() { // Interrupt handler
   num_presses += 1;
}

void setup() {
   Serial.begin( 115200 );
   pinMode( BTN_PIN, INPUT_PULLUP );
   attachInterrupt( BTN_PIN, btn_callback, FALLING );
   last_event = millis();
}

void loop() {
   if ( num_presses > 0 ) {
      bool is_input_low = true;
      for ( int i=0; i < 5; i++ ) {
         if ( digitalRead(BTN_PIN)  ) {
             is_input_low = false; // input not stable
             break;
         }
         delay(2); 
      }
      
      uint32_t now   = millis();
      uint32_t delta = now - last_event;
      
      if ( delta >= 100 && is_input_low ) {
        cnt += 1;
        Serial.printf( "Button pressed: #%d\n", cnt );
        if ( cnt == 10 ) {
          detachInterrupt( BTN_PIN ); // disable IRQ
          Serial.println( "Disabled IRQ for push button" );
        }
      }
      last_event = now; 
      num_presses = 0;
   }
}

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

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

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

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

import urandom as random
from machine import Pin, ADC

def printData(data):
   print( ','.join( [str(x) for x in data]) )
    
adc = ADC(Pin(32))  # use GPIO-32 pin
seed = adc.read()   # read analog value
print( 'Seed: {}'.format(seed) )
random.seed( seed ) # set the seed value

N = 32
# create an array of N randomized integers
data = [ random.randint(0,N) for i in range(N) ]
# show data before sorting 
printData( data )
# sort data
data = sorted(data)
# show data after sorting 
printData( data )

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

void setup() {
   Serial.begin(115200);
   int seed = analogRead(32); // read analog GPIO-32 pin
   Serial.printf( "Seed = %d\n", seed );
   // initialize the pseudo-random number generator
   randomSeed( seed ); // set the seed value
   
   int N = 32; // the number of integers
   int *data = new int[N]; // allocate memory
   
   if ( data != NULL ) {
      // generate randomized integers
      for ( int i=0; i < N; i++ ) { 
         data[i] = random(0,N);
      }
      // show data before sorting
      printData( data, N ); 
      // sort data 
      qsort( data, N, sizeof(int),
        [] (const void *arg1, const void *arg2) {
         int a = *(int *) arg1; 
         int b = *(int *) arg2;
         return (a - b); // is a less than b?
        } // end of lambda function 
      );
      // show data after sorting
      printData( data, N ); 
      // delete data (free allocated memory)
      delete [] data; 
   } else {
      Serial.println("Memory allocation failed!");
   } 
}

void loop() { }

void printData( const int*data, size_t data_len ) {
  String s;
  s.reserve( 2*data_len );
  for ( int i=0; i < data_len; i++ ) { 
      if (i != 0) {
        s += ',';
      }
      s += data[i];
   }
   Serial.println( s.c_str() );
}

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

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

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

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

from micropython import const
from machine import Pin, Timer

LED_PIN = const(5) # use GPIO-5 for LED output
led = Pin( LED_PIN, Pin.OUT )
state = False

def timer_callback(timer): # callback for IRQ timer
    global state
    state = not state  # toggle state
    led.value( state ) # update LED output

timer = Timer( 0 ) # create a Timer object (id = 0)
# use timer in periodic mode (period = 500 msec)
timer.init( period=500, 
            mode=Timer.PERIODIC,
            callback=timer_callback )
try:
    while True:
        pass
except Exception:
    pass
finally:
    timer.deinit() # stop timer

สำหรับการเขียนโค้ด 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

const int LED_PIN = 22; // use GPIO-22 pin for LED
const int TIMER_ID = 0; // select id from {0,1,2,3}
hw_timer_t *timer = NULL;

volatile bool state = 0;

void IRAM_ATTR timer_callback() {
  state = ! state; // toggle state 
  digitalWrite( LED_PIN, state ); // update LED
}

void setup() {
  Serial.begin(115200);
  pinMode( LED_PIN, OUTPUT );
  // create a timer, count-up mode (true)
  // set 16-bit prescaler to 80 => 80MHz/80 = 1MHz
  timer = timerBegin( TIMER_ID, 80, true );
  
  // set alarm interval: 500,000 usec 
  // auto-reload (periodic): true
  timerAlarmWrite( timer, 500000, true );
  
  // enable timer alarm interrupt
  timerAlarmEnable( timer );
  
  // set function callback for the timer interrupt
  // edge type interrupt: true
  timerAttachInterrupt( timer, &timer_callback, true );
}

void loop() {}

กล่าวสรุป

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

Last updated