ตัวอย่างที่ 1: การคำนวณเลขยกกำลังฐานสอง
ถ้าเราต้องการเขียนโค้ดเพื่อคำนวณค่าของเลขจำนวนเต็ม เช่น "สองยกกำลังสี่สิบลบด้วยหนึ่ง" ให้เป็นค่าของตัวแปร x และแสดงค่าของตัวแปรให้เป็นเลขฐานสิบ (decimal ) และฐานสิบหก (hexadecimal ) เราจะเขียนโค้ดอย่างไร ?
x = 2 40 − 1 x = 2^{40} -1 x = 2 40 − 1 ในกรณีนี้ การเก็บข้อมูลสำหรับตัวแปร x จะต้องมีขนาดหน่วยความจำอย่างน้อย 40 บิต หรือ 5 ไบต์
ข้อความเอาต์พุตจากการทำงานของโค้ดสำหรับค่าของตัวแปร x จะต้องได้ตามรูปแบบต่อไปนี้
Copy x = 1099511627775 (dec)
x = 0x ffffffffff ( hex )
ถ้าเขียนโค้ดด้วยไมโครไพธอน ก็สามารถเขียนโดยใช้คำสั่งต่อไปนี้
Copy 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 เปรียบเทียบกัน
Copy void setup () {
Serial . begin ( 115200 );
uint64_t x = ( 1 LLU << 40 ) - 1 ;
Serial . printf ( "x = %llu (dec)\n" , x );
Serial . printf ( "x = 0x %llx (hex)\n" , x );
}
void loop () { }
ในกรณีนี้ เราจะใช้โอเปอร์เรเตอร์เลื่อนบิต (Shift Operato r) ไปทางซ้าย (<<
) แทนการคำนวณเลขยกกำลังสำหรับฐานสอง และเขียนนิพจน์ในภาษา C/C++ ได้เป็น (1LLU << 40)
และจะต้องใช้ชนิดของข้อมูลแบบ unsigned long long
หรือ uint64_t
ซึ่งมีขนาด 8 ไบต์ (64 บิต) ในกรณีตัวอย่างนี้ เราไม่สามารถใช้ชนิดข้อมูลเลขจำนวนเต็มขนาด 32 บิต เนื่องจากต้องใช้อย่างน้อย 40 บิต
การแสดงข้อความเอาต์พุตสำหรับค่าของตัวแปร ทำได้โดยใช้คำสั่ง Serial.printf()
โครงสร้างสำหรับการเขียนโค้ด Arduino Sketch จะต้องมีฟังก์ชัน void setup() {...} สำหรับโค้ดที่ต้องทำเมื่อเริ่มต้น และ void loop() {...} สำหรับโค้ดที่ต้องทำซ้ำไปเรื่อย ๆ หลังจากนั้น
หรืออีกกรณีหนึ่ง เราอาจใช้ฟังก์ชัน pow()
สำหรับการคำนวณเลขยกกำลังดังนี้
Copy 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 ประกอบด้วย
ในกรณีตัวอย่างนี้ ถ้าจะคำนวณค่า "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 ) ของข้อมูลชุดนี้ แล้วแสดงเป็นข้อความเอาต์พุตสำหรับผลลัพธ์ที่ได้ จะเขียนโค้ดอย่างไร ?
กำหนดให้ตัวอย่างข้อความเอาต์พุตสำหรับข้อมูลตัวเลขดังกล่าว เป็นดังนี้
Copy Sum: 450
Min: 5
Max: 95
Avg.: 50.000
ในกรณีของไมโครไพธอน เราก็สามารถเขียนโค้ดได้ โดยสร้างรายการหรือลิสต์ (List ) ของเลขจำนวนเต็ม จากนั้นก็ใช้คำสั่ง min()
, max()
, len()
, sum()
หาค่าต่ำสุด ค่าสูงสุด จำนวนข้อมูลสมาชิกทั้งหมดในรายการ และผลรวมของข้อมูลเหล่านั้น
Copy 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 ไบต์)
Copy 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 และตัวอย่างข้อความเอาต์พุตมีดังนี้
Copy data = [ 0x 34 , 0x ff , 0x 40 , 0x 52 , 0x 67 , 0x 01 ]
crc8 = 0x 46
การคำนวณ 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
โค้ดไมโครไพธอนมีดังนี้
Copy def CRC8 ( data , num_bytes ):
crc = 0x 00 # initialize the remainder
for i in range (num_bytes):
x = data [ i ]
for j in range ( 8 ):
b = (crc ^ x) & 0x 01
crc >>= 1
if b != 0 :
crc ^= 0x 8C
x >>= 1
return crc
data = [ 0x 34 , 0x ff , 0x 40 , 0x 52 , 0x 67 , 0x 01 ]
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 ก็มีตัวอย่างโค้ดเอาไว้เปรียบเทียบกันดังนี้
Copy byte CRC8 ( const byte * data , byte num_bytes ) {
byte crc = 0x 00 ; // 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) & 0x 01 ;
crc >>= 1 ;
if (b) {
crc ^= 0x 8C ;
}
x >>= 1 ;
}
}
return crc;
}
void setup () {
Serial . begin ( 115200 );
byte data[] = { 0x 34 , 0x ff , 0x 40 , 0x 52 , 0x 67 , 0x 01 };
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 และค่าที่อ่านได้เมื่อโปรแกรมทำงานในแต่ละครั้ง ควรจะได้ไม่ซ้ำเดิม
ตัวอย่างการเขียนโค้ดไมโครไพธอนมีดังนี้
Copy 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 ก็มีตัวอย่างดังนี้
Copy 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
)
เราสามารถเขียนโค้ดอีกกรณีหนึ่งได้ดังนี้
Copy #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 มิลลิวินาที
ถ้าเขียนโค้ดด้วยไมโครไพธอน ก็ทำได้ดังนี้
Copy 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 ในครั้งถัดไป
Copy 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 ก็มีตัวอย่างดังนี้
Copy 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 ในครั้งถัดไป
Copy 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 ไปตามลำดับ
ลองมาดูโค้ดสำหรับไมโครไพธอน เราสร้างฟังก์ชัน bcd2seg()
และใช้ประโยคคำสั่งแบบ if-elif ตรวจสอบค่า bcd
ที่รับมาเป็นอาร์กิวเมนต์ ไปทีละกรณี ซึ่งมีทั้งหมด 16 กรณี เพื่อระบุค่า value
ที่ได้จากการทำงานของฟังก์ชันนี้
Copy def bcd2seg ( bcd , invert = False ): # BCD to 7-segment decode
if bcd == 0 :
value = 0x 3f
elif bcd == 1 :
value = 0x 06
elif bcd == 2 :
value = 0x 5b
elif bcd == 3 :
value = 0x 4f
elif bcd == 4 :
value = 0x 66
elif bcd == 5 :
value = 0x 6d
elif bcd == 6 :
value = 0x 7d
elif bcd == 7 :
value = 0x 07
elif bcd == 8 :
value = 0x 7f
elif bcd == 9 :
value = 0x 6f
else :
value = 0x 00
if invert :
value ^= 0x ff
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) )
ตัวอย่างข้อความเอาต์พุต
Copy 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 ไบต์
Copy LOOKUP_TABLE = [
0x 3f , 0x 06 , 0x 5b , 0x 4f ,
0x 66 , 0x 6d , 0x 7d , 0x 07 ,
0x 7f , 0x 6f , 0x 00 , 0x 00 ,
0x 00 , 0x 00 , 0x 00 , 0x 00 , ]
def bcd2seg ( bcd , invert = False ):
return LOOKUP_TABLE [ bcd & 0x 0f ]
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 ในกรณีตัวอย่างนี้
Copy byte bcd2seg ( byte bcd , bool invert = false ) {
byte value;
switch (bcd) {
case 0 : value = 0x 3f ; break ; // 0b00111111
case 1 : value = 0x 06 ; break ; // 0b00000110
case 2 : value = 0x 5b ; break ; // 0b01011011
case 3 : value = 0x 4f ; break ; // 0b01001111
case 4 : value = 0x 66 ; break ; // 0b01100110
case 5 : value = 0x 6d ; break ; // 0b01101101
case 6 : value = 0x 7d ; break ; // 0b01111101
case 7 : value = 0x 07 ; break ; // 0b00000111
case 8 : value = 0x 7f ; break ; // 0b01111111
case 9 : value = 0x 6f ; break ; // 0b01101111
default :
value = 0x 00 ;
}
if (invert) {
value ^= 0x ff ; // bit-inverting
}
return value;
}
void byte2bin_str ( byte x , String & s ) {
s = "0b" ;
for ( int i = 7 ; i >= 0 ; i -- ) {
s += (x & 0x 80 ) ? 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 ไบต์
Copy const byte LOOKUP_TABLE [ 16 ] = {
0x 3f , 0x 06 , 0x 5b , 0x 4f ,
0x 66 , 0x 6d , 0x 7d , 0x 07 ,
0x 7f , 0x 6f , 0x 00 , 0x 00 ,
0x 00 , 0x 00 , 0x 00 , 0x 00 ,
};
byte bcd2seg ( byte bcd , bool invert = false ) {
byte value = LOOKUP_TABLE [ bcd & 0x 0f ];
if (invert) {
value ^= 0x ff ; // bit-inverting
}
return value;
}
void byte2bin_str ( byte x , String & s ) {
s = "0b" ;
for ( int i = 7 ; i >= 0 ; i -- ) {
s += (x & 0x 80 ) ? 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 สำหรับขาอินพุตดังกล่าว
Copy 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++ ในเชิงเปรียบเทียบกัน
Copy 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] นำไปใส่ลงในอาร์เรย์ แล้วแสดงค่าตัวเลข ก่อนและหลังการเรียงข้อมูลจากน้อยไปมาก
โค้ดไมโครไพธอนมีดังนี้
Copy 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 ก็มีแนวทางดังนี้
Copy 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 ทำงานในแต่ละครั้ง
การเขียนโค้ดไมโครไพธอน มีดังนี้
Copy 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
Copy 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 เป็นกรณีศึกษา จะช่วยให้เห็นความเหมือนและความแตกต่างในการเขียนโค้ดในแต่ละภาษาได้ดีขึ้น รวมถึงการใช้คำสั่งต่าง ๆ ที่เกี่ยวข้อง