How To Blink LEDs with ESP32

ตัวอย่างสาธิตการเขียนโค้ดไมโครไพธอนสำหรับ ESP32 เพื่อทำให้ LED กระพริบ โดยใช้วิธีที่แตกต่างกันไป

เนื้อหาในส่วนนี้ มีโจทย์ง่าย ๆ เพื่อทดลองเขียนโค้ดที่จะทำให้ LED เช่น หนึ่งดวงที่อยู่บนบอร์ด ESP32 (หรือจะเลือกใช้ขา GPIO ขาใดขาหนึ่งแล้วต่อวงจร LED เพิ่มเองก็ได้) สามารถกระพริบได้ (LED Blink)

คำถามคือ แล้วมีวิธีการเขียนโค้ดอย่างไรได้บ้างโดยใช้ภาษาไมโครรไพธอน ลองมาดูตัวอย่างและศึกษาการทำงานของโค้ด อาจมองว่าเป็นการทบทวนการใช้ภาษาไพธอน สำหรับผู้ที่เคยเขียนโค้ดในภาษานี้มาบ้างแล้ว แต่อาจจะยังไม่เคยใช้งานไพธอนแบบสัมผัสฮาร์ดแวร์ (Python for Microcontrollers / Physical Computing)

LED Blink: Demo 1

จากเนื้อหาที่ผ่านมา เราได้เรียนรู้การใช้คลาส Pin จากไลบรารีหรือโมดูล machine เพื่อใช้งานขา GPIO ของบอร์ด ESP32

ตัวอย่างนี้ได้เลือกใช้ขา GPIO-5 และกำหนดทิศทางให้เป็นเอาต์พุต ฟังก์ชัน value() ที่ของอ็อปเจกต์ที่สร้างมาจากคลาส Pin มีไว้สำหรับอ่านหรือเขียนค่าลอจิก (0 หรือ 1)

เมื่อกำหนดสถานะของขา GPIO ได้แล้ว จะต้องมีการหน่วงเวลา เราก็ใช้คำสั่งจากโมดูล time เช่น time.sleep() , time.sleep_ms() หรือ time.sleep_us() ก็ได้ ซึ่งแตกต่างกันตรงที่หน่วยของเวลาสำหรับอาร์กิวเมนต์ เช่น วินาที มิลลิวินาที และไมโครวินาที ตามลำดับ

from micropython import const
from machine import Pin
import utime as time

LED_OFF_ON = [1,0] # active-low LED: OFF=1, ON=0
LED_GPIO = const(5) # use GPIO5 for LED output

def main_loop( led ):
    while True:
        # toggle LED: read, modify, write LED state
        led.value( not led.value() )
        # delay for 0.1 seconds
        time.sleep_ms( 100 )
        
try:
    # create a LED object for LED output 
    led = Pin( LED_GPIO, Pin.OUT )
    # enter main loop (press Ctrl+C to stop)
    main_loop( led )  
except KeyboardInterrupt:
    pass
    
led.value( LED_OFF_ON[0] ) # turn off LED

คำสั่งในฟังก์ชัน main_loop() จะทำให้ LED กระพริบซ้ำไปเรื่อย ๆ โดยเว้นระยะเวลาในการสลับสถานะทุก ๆ 100 วินาที ถ้าทดลองรันโค้ดนี้ผ่าน REPL Shell ใน Thonny IDE และต้องการจะหยุดการทำงาน ก็ให้กดคีย์ Ctrl+C

ข้อสังเกต: ในตัวอย่างนี้ LED บนบอร์ด ESP32 ที่ได้นำมาใช้งาน จะสว่าง (ON) ก็ต่อเมื่อเขียนสถานะลอจิกเป็น 0 (False) และจะดับ (OFF) เมื่อเป็น 1 (True)

LED Blink: Demo 2

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

คำสั่ง time.ticks_diff() ใช้สำหรับการหาผลต่างของตัวเลขเวลาที่ได้บันทึกไว้ในตัวแปร t2 และ t1

from micropython import const
from machine import Pin
import utime as time

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

def main_loop( led ):
    t1 = time.ticks_ms() # read time ticks (in msec)
    try:
        while True:
            t2 = time.ticks_ms()
            if time.ticks_diff(t2, t1) >= 100:
                # update the timestamp
                t1 = t2
                # toggle LED state
                led.value( not led.value() )
                
    except KeyboardInterrupt:
        pass
        
led = Pin( LED_GPIO, Pin.OUT )
main_loop( led ) # enter main loop
led.value( LED_OFF_ON[0] ) # turn off LED

LED Blink: Demo 3

ตัวอย่างที่สาม เป็นการสาธิตการใช้งาน Python Generator Function โดยสร้างฟังก์ชันชื่อ next_state_generator() ภายในมีการใช้คำสั่ง yield ดังนั้นจึงนำไปใช้เป็น Generator Iterator ได้ เมื่อเรียกฟังก์ชันนี้ในแต่ละครั้ง จะได้สถานะลอจิก (0 หรือ 1) ถัดไป เช่น สลับ 0 และ 1 ไปเรื่อย ๆ และมีการหน่วงเวลาด้วยคำสั่ง time.sleep_ms() ด้วยเช่นกัน

from micropython import const
from machine import Pin
import utime as time

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

def next_state_generator(sleep_interval):
    while True:
        for i in range(2):
            yield i
            time.sleep_ms( sleep_interval )

def main_loop( led ):
    for state in next_state_generator(100):
        led.value( state )
        
try:
    led = Pin( LED_GPIO, Pin.OUT )
    main_loop( led )
except KeyboardInterrupt:
    pass
    
led.value( LED_OFF_ON[0] ) # turn off LED

LED Blink: Demo 4

ตัวอย่างถัดไปสาธิตการใช้ไทม์เมอร์ หรือตัวนับเวลา Timer ของโมดูล machine ซึ่งมีสองประเภทให้ใช้งานคือ Hardware Timer และ Software Timer แต่เนื่องจาก Hardware Timer สำหรับ ESP32 มีจำนวนจำกัด

โดยทั่วไปเราก็จะใช้ Software Timer (ระบุหมายเลขเป็น -1) เหมาะสำหรับกรณีที่มีความถี่หรืออัตราการนับจังหวะไม่สูงมากนัก แต่ถ้าจะใช้กับความถี่สูง ก็ให้ใช้ Hardware Timer (ระบุหมายเลขเป็น 0 – 3) สำหรับ ESP32

ในตัวอย่างนี้ เราใช้งาน Software Timer และกำหนดคาบในการทำงาน (Period) เท่ากับ 100 มิลลิวินาที และเลือกโหมดแบบทำซ้ำ (Timer.PERIODIC) แต่ถ้าเลือกเป็น Timer.ONE_SHOT จะทำเพียงครั้งเดียวแล้วจบการทำงานของไทม์เมอร์

นอกจากนั้นจะต้องกำหนดฟังก์ชันที่เรียกว่า Callback Function ซึ่งจะถูกเรียกให้ทำงานเมื่อเวลาผ่านไปครบหนึ่งคาบในแต่ละครั้ง ในตัวอย่างนี้ได้ระบุฟังก์ชันเป็นแบบ lambda ซึ่งจะทำหน้าที่สลับสถานะของ LED

from micropython import const
from machine import Pin, Timer
import utime as time

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

def main_loop( ):
    while (True):
        print( 'Main loop' )
        time.sleep(-1) # wait forever

try:
    # create a Pin object for LED 
    led = Pin( LED_GPIO, Pin.OUT )
    
    # create a software-based (virtual) timer 
    timer = Timer(-1)
    
    # start the timer in periodic mode (100msec period)
    timer.init( mode=Timer.PERIODIC, period=100,
        callback=lambda t: led.value(not led.value()) )
    
    main_loop( ) # enter main loop
    
except KeyboardInterrupt:
    pass
    
led.value( LED_OFF_ON[0] ) # turn off the LED
timer.deinit() # disable timer

ข้อสังเกต: ในตัวอย่างนี้ ขณะที่ Timer กำลังทำงานเป็นอิสระ การทำงานของลูปภายในฟังก์ชัน main_loop() เป็นเพียงการรอให้จบการทำงานของโปรแกรม โดยการกดคีย์ Ctrl+C

LED Blink: Demo 5

ตัวอย่างถัดไปสาธิตการสร้างสัญญาณแบบ PWM (Pulse Width Modulation) ให้มีความถี่ต่ำ (5 Hz หรือมีคาบเท่ากับ 200 msec) สำหรับนำไปใช้กับ LED และกำหนดค่า Duty Cycle ให้เท่ากับ 50% โดยระบุค่าเป็น 511 ซึ่งเป็นค่ากลางในช่วง 0..1023 ดังนั้นจึงได้สัญญาณเอาต์พุตแบบมีคาบ ในช่วงที่เป็น 0 (Low) และ 1 (High) มีความกว้างช่วงละ 100 มิลลิวินาที

from micropython import const
from machine import Pin, PWM
import utime as time

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

def main_loop():
    while True:
        time.sleep(-1) # wait forever
        
try:
    led = Pin( LED_GPIO, Pin.OUT )
    # create a PWM object for the specified pin
    pwm = PWM( led )
    pwm.duty(511) # 50% (10-bit resolution: 0..1023)
    pwm.freq(5)   # note: 1Hz is the lowest frequency
    main_loop( )  # enter main loop
    
except KeyboardInterrupt:
    pass
    
led.value( LED_OFF_ON[0] ) # turn off LED 
pwm.deinit() # turn off PWM output

LED Blink: Demo 6

ตัวอย่างถัดไปสาธิตการสร้าง 'เธรด' (Thread) ที่ทำงานได้อิสระจาก Main Thread โดยใช้คำสั่งของโมดูลหรือไลบรารี _thread

เธรดที่ถูกสร้างขึ้นมานั้น ทำหน้าที่คอยสลับสถานะลอจิกของ LED และเว้นระยะเวลา 100 มิลลิวินาที แล้วทำขั้นตอนซ้ำ

ในตัวอย่างนี้ ฟังก์ชัน led_thread_func() จะถูกกำหนดให้เป็น Thread entry function (ฟังก์ชันที่ทำงานโดยเธรดที่ถูกสร้างขึ้นใหม่) โดยใช้คำสั่ง _thread.start_new_thread()

ตัวแปรภายนอก finished แบบบูลีน (Boolean) จะถูกใช้ในการตรวจสอบเงื่อนไขการออกจากลูปการทำงานของเธรด ซึ่งจะจบการทำงานเมื่อมีการกดคีย์ Ctrl+C ใน REPL Shell

from micropython import const
from machine import Pin
import utime as time
import _thread

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

# global variable
finished = False

def main_loop( ):
    global finished
    print( 'Thread "main": id=%d' % _thread.get_ident() )
    
    try:
        while not finished:
            pass
            
    except KeyboardInterrupt:
        finished = True
        led.value( LED_OFF_ON[0] ) # turn off LED
        _thread.exit() # force thread to exit

def led_thread_func(led, delay):
    global finished
    print( 'Thread  "led": id=%d' % _thread.get_ident() )
    
    try:
        while not finished:
            led.value( not led.value() )
            time.sleep_ms( delay )
            
    except KeyboardInterrupt:
        finished = True
        _thread.exit() # force thread to exit

led = Pin( LED_GPIO, Pin.OUT )
_thread.start_new_thread( led_thread_func, (led,100,) )
main_loop() # enter main loop

LED Blink: Demo 7

ตัวอย่างถัดไปเป็นการใช้เธรด เหมือนตัวอย่างแล้ว แต่สาธิตการสร้างเธรด 2 ชุด (หมายเลข id คือ 0 และ 1) โดยให้แต่ละเธรดทำหน้าที่กำหนดสถานะลอจิกตาม id ของตัวเอง กล่าวคือ ถ้า id เป็น 0 ให้เขียนสถานะเป็น 0 แต่ถ้า id เป็น 1 ให้เขียนสถานะเป็น 1 แล้วเว้นระยะเวลา 100 มิลลิวินาที

แต่เนื่องจากว่า เธรดทั้งสองชุดใช้ฟังก์ชันเดียวกันคือ led_thread_func() ดังนั้นจึงใช้ id เป็นตัวระบุการทำงานของแต่ละเธรด

ทั้งสองเธรดนั้น ต้องเข้าถึงอ็อปเจกต์ที่เป็น Pin สำหรับ LED ร่วมกัน ดังนั้นจึงมีการสร้างและใช้ lock (Mutex Lock) ในการป้องกันและจัดลำดับการเข้าถึงทรัพยากรที่ใช้ร่วมกัน

from micropython import const
from machine import Pin
import utime as time
import _thread

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

# global variable
finished = False

def main_loop( ):
    global finished    
    try:
        while not finished:
            pass
            
    except KeyboardInterrupt:
        finished = True
        led.value( LED_OFF_ON[0] )
        _thread.exit()
        
def led_thread_func(id, led, delay, lock):
    global finished
    try:
        while not finished:
            lock.acquire() # try to acquire lock
            led.value( (id==1) ) # set LED value
            time.sleep_ms( delay )
            lock.release() # release lock
            
    except KeyboardInterrupt:
        finished = True
        _thread.exit()

# create a pin object for LED output
led = Pin( LED_GPIO, Pin.OUT )

# create a mutex lock
lock = _thread.allocate_lock()

# create and start two new threads
_thread.start_new_thread( led_thread_func, (0,led,100,lock,) )
_thread.start_new_thread( led_thread_func, (1,led,100,lock,) )

# enter the main loop
main_loop() 

LED Blink: Demo 8

ตัวอย่างนี้สาธิตการใช้วงจร RMT (และใช้ได้เฉพาะกับ ESP32 เท่านั้น) เหมาะสำหรับสร้างสัญญาณพัลส์เป็นชุด (Pulse Train Generation) เช่น การสร้างสัญญาณควบคุมสำหรับรีโมทแสงอินฟราเรด (Infrared Remote Control) หรือใช้ควบคุมหรือกำหนดค่าสีให้กับโมดูล WS2812B Neopixel RGB LED

เราสามารถใช้ RMT มาสร้างสัญญาณพัลส์ เพื่อทำให้ LED กระพริบได้เช่นกัน (แต่มีข้อจำกัดเรื่องช่วงความถี่ที่สามารถใช้งานได้) วงจร RMT ภายใน ESP32 รับสัญญาณ Clock ที่มีความถี่ 80 MHz ไปผ่านวงจรหารความถี่ ขนาด 8 บิต (clock_div) ซึ่งเลือกค่าได้ในช่วง 0..255 แล้วนำไปสร้างสัญญาณพัลส์

ความกว้างของพัลส์จะถูกกำหนดโดยค่าที่เป็นเลขจำนวนเต็มขนาด 15 บิต (0..32,767) เช่น ถ้าเลือกตัวหารความถี่เป็น 250 และเลือกตัวเลข 32,000 สำหรับความกว้างของพัลส์ (Bit Width) เราจะได้ความกว้างเท่ากับ (1/80 MHz) * 250 * 32000 = 100 msec

\mbox{Pulse Width} = \frac{32000}{80 MHz / 250} = 0.1\,sec

from micropython import const
from machine import Pin
import utime as time
import esp32 # for RMT module

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output
led = Pin( LED_GPIO, Pin.OUT )

# Use the RMT module to generate pulses
# The freq. of input clock to RMT is always 80MHz.
# 80MHz/250/32000 = 10Hz
rmt = esp32.RMT(id=0, pin=led, clock_div=250)

# Generate pulses repeatedly
rmt.loop(True)

# Send 0 first, followed by 1
# The pulse width for 1 and 0 is 32000 (15-bit value).
rmt.write_pulses( [32000,32000], start=0 )

try:
    while True:
        time.sleep(-1)
    
except KeyboardInterrupt:
    pass
    
rmt.loop(False) # stop RMT loop
rmt.deinit()    # turn off RMT
led.init(mode=Pin.IN, value=LED_OFF_ON[0]) # turn off LED 

LED Blink: Demo 9

ตัวอย่างถัดไปเป็นการสร้างฟังก์ชันแบบ Coroutine ซึ่งมีความคล้ายกับ Python Generator ที่ภายในมีคำสั่ง yield และเราสามารถนำมาใช้ร่วมกับคำสั่ง async และ await ได้เช่นกัน

from micropython import const
from machine import Pin
import utime as time

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

async def next_state(led, delay):
    state = 0
    try:
        while True:
            state = not state
            yield state
            time.sleep_ms( delay )
        
    except KeyboardInterrupt:
        print('Terminated')
    
async def coro(): # coroutine
    return await next_state(led, 100)

led = Pin( LED_GPIO, Pin.OUT )
c = coro() # create a Coroutine object

try:
    while True:
        led.value( c.send(None) )
    
except:
    c.close() # stop coroutine
    led.value( LED_OFF_ON[0] ) # turn off LED

LED Blink: Demo 10

ตัวอย่างนี้สาธิตการใช้คำสั่งจากไลบรารี uasyncio และลองสร้างฟังก์ชันที่มีชื่อว่า led_toggle() ให้ทำหน้าที่เป็น Coroutine (โดยการเขียนคำว่า async ไว้นำหน้าเมื่อประกาศและสร้างฟังก์ชันดังกล่าว) และจะต้องมีคำสั่ง await อยู่ภายในฟังก์ชัน

ในกรณีนี้ การทำงานของฟังก์ชัน เป็นการรอให้เวลาผ่านไป โดยใช้คำสั่ง asyncio.sleep_ms() เช่น 100 มิลลิวินาที ก่อนที่จะสลับสถานะเอาต์พุตของ LED

ฟังก์ชันดังกล่าวจะถูกนำไปใช้ในการสร้างทาสก์ (Task) ที่ทำงานต่อเนื่องไปเรื่อย ๆ และถูกจัดการโดย Event Loop ของ asyncio

from micropython import const
from machine import Pin
import utime as time
import uasyncio as asyncio

LED_OFF_ON = [1,0]  # for active-low LED
LED_GPIO = const(5) # use GPIO5 for LED output

led = Pin( LED_GPIO, Pin.OUT ) # LED pin object

async def led_toggle(led,duration):
    while True:
        await asyncio.sleep_ms( duration )
        led.value( not led.value() )
    
try:
    loop = asyncio.get_event_loop()
    # create a task for LED blink
    led_task = led_toggle(led, 100)
    loop.run_until_complete( led_task )
    
except:
    led_task.close() # close LED task
    
led.value( LED_OFF_ON[0] ) # turn off LED

ตัวอย่างโค้ดถัดไปสาธิตการใช้ SoftSPI (ไม่ใช่ Hardware SPI หรือ HSPI ของ ESP32) ในการเลื่อนบิตข้อมูลออกไปที่ขา GPIO ที่ได้เลือกมาเป็นขาสำหรับส่งข้อมูลขาออก MOSI ของบัส SPI

การส่งข้อมูลไบต์ มีจำนวน 2 ไบต์ ได้แก่ 0xff (High Pulse) และ 0x00 (Low Pulse) ตามลำดับ วนซ้ำไปเรื่อย ๆ และถ้าใช้ความถี่ของ SCK ต่ำ และใช้ขา MOSI ต่อกับวงจร LED ก็จะเห็นแสงไฟ LED กระพริบ

ในตัวอย่างนี้ได้เลือกขา GPIO-5 เป็นขาสำหรับสัญญาณ MOSI ซึ่งนำไปต่อกับวงจร LED และใช้ขา GPIO-0 สำหรับ SCK และ GPIO-4 สำหรับ MISO ซึ่งในความเป็นจริงไม่ได้ใช้งานขาทั้งสองกับวงจรใด ๆ ภายนอก

from machine import Pin, SPI

# use GPIO-5 pin for LED output / MOSI signal
led_pin = Pin(5, Pin.OUT)

# use Soft-SPI
spi = SPI(baudrate=80,
          polarity=0, phase=0,
          sck=Pin(0), mosi=led_pin, miso=Pin(4))
try:
    while True:
        spi.write( bytes([0x00,0xff]) )
 
except KeyboardInterrupt:
    pass

spi.deinit() # disable SPI

กล่าวสุรป

โดยสรุป จากตัวอย่างที่ได้นำเสนอไปนั้น เราได้เห็นรูปแบบการเขียนโค้ดภาษาไมโครไพธอนสำหรับ ESP32 โดยใช้เทคนิคที่แตกต่างกัน เพื่อวัตถุประสงค์เดียวกันคือ ทำให้ LED กระพริบได้

เผยแพร่ภายใต้ลิขสิทธิ์ Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)

Last updated