RPi Pico RP2040 Code Examples

โต้ดตัวอย่างสาธิตการใช้งานไมโครไพธอนร่วมกับบอร์ด Raspberry Pi Pico (RP2040) -- เริ่มต้นเขียนเมื่อวันที่ 12 กุมภาพันธ์ พ.ศ. 2564

คำแนะนำ

ในการต่อวงจรเพื่อทดลองการทำงานของโค้ดตัวอย่างโดยใช้บอร์ดไมโครคอนโทรลเลอร์ RPi Pico ร่วมกับวงจรหรือโมดูลต่าง ๆ ควรมีความรู้พื้นฐานในการต่อวงจรและระมัดระวังเพื่อมิให้เกิดความเสียหายกับอุปกรณ์

โดยทั่วไปแล้วบอร์ด RPi Pico ใช้แรงดันไฟเลี้ยงจากพอร์ต USB ซึ่งจะได้ประมาณ 5V แต่ไม่เกิน 5.5V แต่ระดับแรงดันไฟฟ้าที่ขา I/O จะอยู่ที่ 3.3V เท่านั้น และไม่สามารถใช้งานได้กับวงจรที่มีระดับแรงดันไฟฟ้าสูงกว่า เช่น 5V (Not 5V tolerant)

การต่อวงจรที่ขา I/O ของบอร์ด ควรตรวจสอบจาก Pinout Diagram ของบอร์ดทุกครั้ง

โค้ดตัวอย่างแรก สาธิตการทำให้ LED กระพริบได้ โดยเลือกใช้ LED บนบอร์ด Pico ซึ่งต่อวงจรอยู่กับขา GPIO-25 และสาธิตการใช้คลาส machine.Pin ที่เกี่ยวข้องกับขา GPIO

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

ถ้าต้องการจบการทำงานของโค้ดนี้เมื่อรันผ่านทาง MicroPython REPL ให้กดคีย์ Ctrl+C

import machine
import utime as time
# use the on-board LED (GP25 pin)
led = machine.Pin( 25, machine.Pin.OUT )
try: 
    while True:
        state = not led.value()
        led.value( state )
        print('LED state:', state)
        time.sleep(0.5) 
except KeyboardInterrupt:
    pass
led.value(0) # turn off LED

โค้ดตัวอย่างที่ 2: Input Button

โค้ดตัวอย่างนี้ สาธิตการอ่านค่าจากขา GPIO ที่ต่อรับสัญญาณอินพุตจากวงจรปุ่มกด (Active-Low Push Button) โดยกำหนดให้ขา GPIO-16 เป็นขาอินพุตและเปิดใช้งาน Internal Pull-Up สำหรับขาดังกล่าว และใช้ตัวแปร button ในการอ้างอิงขาดังกล่าว ในตัวอย่างนี้ เมื่อกดปุ่มจะอ่านค่าโดยใช้คำสั่ง button.value() ได้เป็น 0 หรือลอจิก LOW และจบการทำงานของโปรแกรม

import utime as time
from machine import Pin

BTN_PIN = 16 # use GP16 for Button pin
button = Pin( BTN_PIN, Pin.IN, Pin.PULL_UP )
while True:
    if not button.value():
        print('pressed')
        break
    time.sleep(0.1)

หรือจะเปลี่ยนไปใช้วิธีการตรวจสอบอินเทอร์รัพท์ (Interrupt) ที่ขาอินพุตตามตัวอย่างต่อไปนี้ โดยการตรวจสอบดูว่า มีการเปลี่ยนแปลงสถานะที่ขาอินพุตหรือไม่ เหตุการณ์ที่จะทำให้เกิดอินเทอร์รัพท์ในกรณีนี้คือ เมื่อมีการเปลี่ยนจาก HIGH เป็น LOW หรือ Falling-Edge Transition

ตัวแปร button ใช้ในการอ้างอิงขา GPIO-16 (GP16) สำหรับวงจรปุ่มกด และการใช้คำสั่ง button.irq() เป็นการเปิดใช้งานและกำหนดรูปแบบของอินเทอร์รัพท์ เช่น เลือกรูปแบบเป็น Pin.IRQ_FALLING และระบุฟังก์ชันที่ใช้สำหรับทำหน้าที่เป็น Callback (Interrupt Handler)

import utime as time
from machine import Pin

BTN_PIN = 16 # use GP16 for Button pin
button  = Pin( BTN_PIN, Pin.IN, Pin.PULL_UP ) 
done = False

def button_handler(pin):
    global done
    # disable external interrupt on the Button pin
    button.irq( handler=None )
    done = True

button.irq( trigger=Pin.IRQ_FALLING, handler=button_handler )
print('Press the button!')
while not done:
    time.sleep(0.1)

โค้ดตัวอย่างที่ 3: Push Button + Toggle LED

ตัวอย่างนี้สาธิตการเปิดใช้งานอินเทอร์รัพท์ที่ขา GPIO-14 (GP14) ที่ต่อกับวงจรปุ่มกดบนเบรดบอร์ด เมื่อมีการกดปุ่มแล้วปล่อยแต่ละครั้ง จะทำให้ LED บนบอร์ดสลับสถานะหนึ่งครั้ง

import utime as time
from machine import Pin

BTN_PIN = 16
LED_PIN = 25
button  = Pin( BTN_PIN, Pin.IN, Pin.PULL_UP ) 
led     = Pin( LED_PIN, Pin.OUT )
led.low() # turn the LED off

def button_handler(pin):
    global pressed
    # disable external interrupt on the Button pin
    button.irq( handler=None )
    pressed = True
    print( time.ticks_ms() ) # show time ticks (in msec)

pressed = False
button.irq( trigger=Pin.IRQ_FALLING, handler=button_handler )
print('Press the button!')
try:
    while True:
        if pressed:
            while not button.value():
                time.sleep(0.1)
            led.toggle() # toggle LED output
            button.irq( trigger=Pin.IRQ_FALLING, handler=button_handler )
            pressed = False
        time.sleep(0.1)
except KeyboardInterrupt:
    pass
led.low() # turn off LED

โค้ดตัวอย่างที่ 4: PWM-based LED Fading

ตัวอย่างนี้สาธิตการสร้างสัญญาณแบบ PWM (Pulse Width Modulation) และให้เป็นเอาต์พุตที่ขา GPIO-25 กำหนดความถี่ให้เท่ากับ 1000 Hz (1 kHz) และเริ่มต้นให้มีค่า Duty Cycle เป็น 0 การกำหนดค่า Duty Cycle สำหรับ PWM นั้น จะต้องเป็นเลขจำนวนเต็มขนาด 16 บิต อยู่ในช่วง 0..65535

โค้ดตัวอย่างนี้ จะเพิ่มค่า Duty Cycle จาก 0 และไปจนถึงค่าเท่ากับ 255*256 จากนั้นจะลดลงไปจนถึง 0 อีกครั้ง แล้วจะวนซ้ำในลักษณะนี้ไปเรื่อย ๆ

import utime as time
from machine import Pin, PWM
# use onboard LED and PWM output pin 
pwm = PWM( Pin(25) )
# set PWM freq. to 1kHz
pwm.freq(1000)
# set counter to 0
cnt = 0
try:
    while True:
        if cnt < 256:
            duty = cnt
        else:
            duty = 511-cnt
        pwm.duty_u16( duty * 256 )
        cnt = (cnt+1) % 512
        time.sleep(0.01)
except KeyboardInterrupt:
    pass
# set duty cycle to 0 (ns)
pwm.duty_ns(0)
# deinitialize the GPIO pin used for PWM output
pwm.deinit()

ถ้าทดสอบการทำงานของโค้ดตัวอย่างนี้กับบอร์ด RPi Pico จะเห็น LED บนบอร์ดค่อย ๆ สว่างขึ้นและดับลงซ้ำไปเรื่อย ๆ

ถ้าจะลองใช้โมดูล RGB LED ที่ใช้ขาสัญญาณควบคุมแบบ 3 ขา ก็สามารถทำได้เช่นกัน โค้ดตัวอย่างนี้สาธิตการใช้ขา GPIO-11, 12, 13 (GP11,GP12,GP13) เป็นขาสัญญาณเอาต์พุต นำไปต่อกับขาสัญญาณของโมดูล RGB LED (Active-High) เพื่อเปลี่ยนค่า Duty Cycle ของสัญญาณแต่ละช่องไปตามลำดับ

import utime as time
from machine import Pin, PWM

pwm_list = [ PWM( Pin(p) ) for p in [11,12,13] ]
for pwm in pwm_list:
    pwm.freq(1000) # set freq. to 1kHz
    pwm.duty_ns(0) # set duty cycle (high pulse) = 0ns

try:
    while True:
        time.sleep(0.1)
        for pwm in pwm_list:
            for cnt in range(512):
                if cnt < 256:
                    duty = cnt
                else:
                    duty = (511-cnt)
                pwm.duty_u16( duty * 256 )
                time.sleep(0.005)
except KeyboardInterrupt:
    pass
for pwm in pwm_list:
    pwm.duty_ns(0)
    pwm.deinit()

ตัวอย่างนี้สาธิตการใช้งาน Timer (ตอนนี้ใช้ได้เฉพาะ Software Timer และใช้ Timer id เท่ากับ -1) เพื่อทำคำสั่งหรือเรียกใช้ฟังก์ชันตามคาบเวลาหรือความถี่ที่ได้กำหนดไว้ ซึ่งเป็นเหตุการณ์ที่เกิดขึ้นซ้ำ (Periodic) เช่น การสลับสถานะเอาต์พุตของ LED ที่อัตรา 10 Hz

from machine import Pin, Timer

led = Pin( 25, Pin.OUT ) # use onboard LED

def tick(timer):
    global led
    led.toggle()

# create the hardware timer object
timer = Timer(-1) 
# configure the timer object, 10Hz tick rate, periodic mode
timer.init( freq=10, mode=Timer.PERIODIC, callback=tick )
try:
    while True:
        pass
except KeyboardInterrupt:
    pass
timer.deinit()

ตัวอย่างนี้สาธิตการใช้คำสั่ง _thread.start_new_thread() เพื่อสร้าง"เธรด" (Thread) ใหม่ให้ทำงานบน CPU Core อีกอันหนึ่ง โดยให้ทำหน้าที่สลับสถานะลอจิกของ LED ตามข่วงเวลาที่กำหนดไว้

แต่มีข้อจำกัดคือ เนื่องจาก MicroPython สำหรับ RP2040 ไม่ได้ทำงานโดยใช้ RTOS หรือระบบปฏิบัติการเวลาจริง ดังนั้นการใช้งาน "เธรด" ใหม่ จะถูกกำหนดให้รันบน CPU Core 1 ในขณะที่โค้ดของเธรดหลัก (Main Thread) จะรันบน CPU Core 0

เนื่องจากมีการทำงานพร้อมกันโดยใช้ CPU Core ทั้งสองแกน และต้องมีการใช้วิธีการป้องกันที่เรียกว่า Mutex Lock โดยใช้คำสั่ง _thread.allocate_lock() เพื่อสร้าง Lock Object มาใช้งาน และอ้างอิงโดยใช้ตัวแปร lock จากนั้นจึงใช้คำสั่ง lock.acquire() และ lock.release() ตามลำดับ ก่อนและหลังการใช้คำสั่งที่ต้องใช้ทรัพยากรของระบบที่อาจต้องใช้ร่วมกันระหว่างเธรดในระบบ เช่น การใช้คำสั่ง print() เพื่อส่งข้อความออกทาง REPL

from machine import Pin
import utime as time
import _thread

lock = None
done = False

def led_task(led_pin, n, delay):
    global done
    lock.acquire()
    print( 'LED Task: Thread ID=%d' % _thread.get_ident() )
    lock.release()
    led = Pin( led_pin, Pin.OUT )
    try:
        for i in range(2*n):
            led.toggle()
            time.sleep(delay)
    except KeyboardInterrupt:
        pass
    done = True
    lock.acquire()
    print('Task done')
    lock.release()

# create a lock object
lock = _thread.allocate_lock()
# create a new thread and run it on the second CPU core
# blink the LED on GPIO-25 pin 10 times with 500ms delay time
_thread.start_new_thread( led_task, (25, 10, 0.5) )

try:
    lock.acquire()
    print( 'Main: Thread ID=%d' %_thread.get_ident() )
    lock.release()
    while not done:
        pass
except KeyboardInterrupt:
    pass
finally:
    lock.acquire()
    print('Main thread done')
    lock.release()

โค้ดตัวอย่างที่ 7: OLED I2C Display

โค้ดตัวอย่างนี้สาธิตการใช้งานบัส I2C และเชื่อมต่อกับโมดูล OLED I2C Display (SSD1306 Driver) โดยใช้งานร่วมกับไลบรารี ssd1306 ซึ่งสามารถดาวน์โหลดไฟล์ ssd1306.py ได้จาก Github แล้วนำไปใส่ลงในไดรฟ์ของไมโครไพธอน

เนื่องจาก RP2040 มีบัส I2C ให้ใช้งาน 2 ชุด ในตัวอย่างนี้ได้เลือกใช้ I2C1 และใช้ขา GPIO-14 และ GPIO 15 ซึ่งสามารถใช้เป็นขา I2C1_SDA และ I2C1_SCL ได้

ขนาดของโมดูล OLED I2C ได้เลือกมาลองใช้งาน มีขนาด 128 x 32 พิกเซล และมีแอดเดรสตรงกับ 0x3C

from machine import Pin, I2C
from ssd1306 import SSD1306_I2C

# use I2C1 and GPIO 14,15 for SDA and SCL pins
i2c = I2C(1, scl=Pin(15), sda=Pin(14), freq=400000)
dev_addr_list = i2c.scan() # scan I2C devices 
for addr in dev_addr_list:
    print('Found: 0x{:02x}'.format(addr) )

oled = SSD1306_I2C( 128, 32, i2c, addr=0x3c)
oled.fill(0)
oled.text("Raspberry Pi",5,5)
oled.text("Pico",5,15)
oled.show()

โค้ดตัวอย่างที่ 8: SSD1306 OLED + BH1750 Light Sensor

ตัวอย่างนี้เป็นการใช้งานบัส I2C โดยนำบอร์ด RP2040 ไปเชื่อมต่อกับโมดูล SSD1306 OLED Display และโมดูลเซ็นเซอร์แสง BH1750 ทั้งสองโมดูลนี้ใช้บัส I2C1 ร่วมกัน และใช้แรงดันไฟเลี้ยง +3.3V จากบอร์ด RP2040

แอดเดรสของโมดูล SSD1306 และ BH1750 ตรงกับ 0x3C และ 0x23 ตามลำดับ

from machine import Pin, I2C
import utime as time
from ssd1306 import SSD1306_I2C

BH1750_ADDR  = 0x23
SSD1306_ADDR = 0x3C

def bh1750_init( i2c, addr ):
    try:
        # power on the BH1750
        i2c.writeto( addr, bytearray([0x01]) )
        # reset the BH1750
        i2c.writeto( addr, bytearray([0x07]) )
        time.sleep_ms(200)
        # set mode to 1.0x high-resolution,
        # continuous measurement
        i2c.writeto( addr, bytearray([0x10]) )
        time.sleep_ms(150)
        return True
    except Exception:
        return False

def bh1750_read( i2c, addr ):
    try:
        data  = i2c.readfrom(addr, 2) # read two bytes
        value = (data[0]<<8 | data[1])/(1.2)
        return value
    except Exception as ex:
        print( 'BH1750 reading error:', ex )
        return None

# use I2C1 and GPIO 14,15 for SDA and SCL pins
i2c = I2C( 1, scl=Pin(15), sda=Pin(14), freq=400000 )
#print( [hex(addr) for addr in i2c.scan()] )
oled = SSD1306_I2C( 128, 32, i2c, addr=SSD1306_ADDR )
oled.fill(0)

try:
    bh1750_init( i2c, BH1750_ADDR )
    while True:
        # read sensor value
        value = int( bh1750_read( i2c, BH1750_ADDR ) )
        if value:
            text = 'BH1750 (0x{:02x})'.format(BH1750_ADDR)
            oled.fill(0)
            oled.text(text,5,5)
            text = '{:5d} Lux'.format(value)
            oled.text(text,20,24)
            oled.show()
        time.sleep_ms(200)
except KeyboardInterrupt:
    pass

ตัวอย่างการสร้างคลาสในภาษาไมโครไพธอนสำหรับ BH1750 แล้วนำมาใช้งาน โดยบันทึกลงในไฟล์ bh1750.py

from utime import sleep_ms

class BH1750():
    """Micropython BH1750 ambient light sensor driver."""
    PWR_OFF = 0x00
    PWR_ON  = 0x01
    RESET   = 0x07
    CONT_LOWRES  = 0x13
    CONT_HIRES_1 = 0x10
    CONT_HIRES_2 = 0x11
    ONCE_HIRES_1 = 0x20
    ONCE_HIRES_2 = 0x21
    ONCE_LOWRES  = 0x23

    def __init__(self, bus, addr=0x23):
        self.bus  = bus
        self.addr = addr
        self.reset()

    def reset(self): # reset sensor
        self.on()
        self.set_mode(self.RESET)

    def off(self): # turn device off
        self.set_mode(self.PWR_OFF)

    def on(self): # turn device on
        self.set_mode(self.PWR_ON)

    def set_mode(self, mode): # set sensor mode
        self.mode = mode
        self.bus.writeto(self.addr, bytes([self.mode]))

    def read(self): # read sensor value (luminance in Lux)
        sleep_ms(24 if self.mode in (0x13, 0x23) else 180)
        data = self.bus.readfrom(self.addr, 2)
        factor = 2.0 if self.mode in (0x11, 0x21) else 1.0
        return (data[0]<<8 | data[1])/(1.2 * factor)

และโค้ดสาธิตการใช้งานไฟล์ bh1750.py

from machine import Pin, I2C
import utime as time
from bh1750 import BH1750 # import from file bh1750.py

BH1750_ADDR = 0x23
# use I2C1 and GPIO 14,15 for SDA and SCL pins
i2c = I2C( 1, scl=Pin(15), sda=Pin(14), freq=400000 )
print( [hex(addr) for addr in i2c.scan()] )

try:
    dev = BH1750(i2c, addr=BH1750_ADDR)
    dev.on()
    dev.set_mode( BH1750.CONT_HIRES_1 )
    while True:
        value = int(dev.read()) # read sensor value (luminance in Lux)
        text = 'BH1750 (0x{:02x}): '.format(BH1750_ADDR)
        text += '{:5d} Lux'.format(value)
        print(text)
        time.sleep_ms(200)
except KeyboardInterrupt:
    pass

โค้ดตัวอย่างที่ 9: WDT (Watchdog Timer)

โค้ดตัวอย่างนี้สาธิตการเปิดใช้งาน Watchdog Timer (WDT) ซึ่งเป็นวงจรภายใน RP2040 และเมื่อเปิดการทำงานของ WDT แล้ว จะต้องมีการป้อนค่าให้ WDT (WDT feeding) ภายในระยะเวลาที่กำหนด มิฉะนั้นแล้ว วงจร WDT จะรีเซตการทำงานของไมโครไพธอน

ในตัวอย่างนี้ จะต้องมีการต่อวงจรปุ่มกดภายนอกที่ขา GPIO-16 ด้วย และเมื่อโค้ดนี้เริ่มทำงาน จะต้องมีการกดปุ่มค้างไว้ในช่วงเวลาดังกล่าว จึงจะเปิดการทำงานของ WDT

import utime as time
from machine import Pin, WDT

print('Press the button on GPIO-16 to enable WDT.')
button = Pin( 16, mode=Pin.IN, pull=Pin.PULL_UP )
time.sleep_ms(1000)
wdt = None
if button.value() == 0:
    # enable WDT with timeout of 2000 msec 
    wdt = WDT(timeout=2000) 
    # Note that once the WDT is running the timeout cannot be
    # changed and it cannot be stopped either.
if wdt is None:
    print('WDT is disabled.')
try:
    while wdt is not None:
        # feed the WDT to prevent it from resetting the system. 
        print('feed WDT @{} ms'.format( time.ticks_ms() ) )
        wdt.feed() 
        time.sleep(1.0)
except KeyboardInterrupt:
    pass

โค้ดตัวอย่างที่ 10: System Info

โค้ดตัวอย่างนี้สาธิตการใช้คำสั่งของโมดูล uos เพื่อสอบถามข้อมูลเกี่ยวกับระบบและการใช้งานระบบไฟล์ในหน่วยความจำแฟลช (Flash File System) ของไมโครไพธอน

import uos as os

names = ['sysname', 'nodename', 'release', 'version', 'machine']
results = os.uname()
for name,value in zip(names,results):
    print('{:<8s} = "{}"'.format(name, value))

# list all files 
files = os.listdir()
print('Python Script File(s):')
for file in files:
    if file.endswith('.py'):
        print('->',file) # show only Python script file

info = os.statvfs('/')
fs_total = info[0]*info[2]
fs_free  = info[0]*info[3]
fs_used  = fs_total - fs_free
text = 'File System (total/free/used): {:,} / {:,} / {:,} bytes'
print( text.format(fs_total,fs_free,fs_used) )

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

sysname  = "rp2"
nodename = "rp2"
release  = "1.13.0"
version  = "v1.13-290-g556ae7914 on 2021-01-21 (GNU 10.2.0 MinSizeRel)"
machine  = "Raspberry Pi Pico with RP2040"
Python Script File(s):
-> ssd1306.py
File System (total/free/used): 1,441,792 / 1,425,408 / 16,384 bytes

โค้ดตัวอย่างที่ 11: On-Chip Temperature Reading with ADC

ตัวอย่างนี้สาธิตการใช้งานวงจร ADC (Analog-to-Digital Converter) ภายใน RP2040 เพื่ออ่านค่าจากเซ็นเซอร์วัดอุหภูมิที่อยู่ภายในชิป และต่อสัญญาณแบบแอนะล็อกเข้าช่องหมายเลข 4 ของวงจร ADC

from machine import ADC
import utime as time

sensor_temp = ADC(4)  # use on-chip temperature sensor
conversion_factor = 3.3 / 65535 # 3.3V -> 16-bit value

while True:
    reading = sensor_temp.read_u16() * conversion_factor
    # Typical value: 0.706V at 27 degrees C
    # with a slope of -1.721mV (0.001721) per degree. 
    temperature = 27 - (reading - 0.706)/0.001721
    print('On-chip tempreature: {:.2f} deg.C'.format(temperature) )
    time.sleep(2.0)

วงจร ADC ภายใน RP2040 มีช่องอินพุตทั้งหมด 5 ช่อง (ช่องหมายเลข 0 ถึง 4) และมีความละเอียดในการแปลงข้อมูลเท่ากับ 12 บิต (0..4095) แต่ไมโครไพธอนจะแปลงให้เป็นข้อมูลขนาด 16 บิต (0..65535)

ช่องอินพุต 3 ช่องแรกของ ADC บนบอร์ด Pico ตรงกับขา GP26, GP27, GP28 ตามลำดับ แต่ช่องที่ 4 จะใช้สำหรับอ่านแรงดันไฟฟ้าที่ขา VSYS/3 ของบอร์ด (แรงดันไฟฟ้าที่วัดได้จะเท่ากับ VSYS / 3) และถ้าใช้แรงดันไฟเลี้ยงจาก VUSB แรงดันไฟฟ้าที่วัดได้จาก ADC ที่ขา VSYS จะเท่ากับ (VBUS / 3) ซึ่งได้จากวงจรหารความถี่โดยใช้ตัวต้านทาน

ข้อสังเกต: VBUS ต่อผ่านไดโอด Schottky ไปยัง VSYS (ต้องอยู่ในช่วง 1.8V .. 5.5V) และต่อไปยังวงจร Switching Power Supply (RT6150 buck-boost SMPS) เพื่อสร้างแรงดันไฟฟ้า +3.3V

โค้ดตัวอย่างนี้สาธิตการอ่านค่าอินพุต 4 ช่องของ ADC ตามลำดับ

import utime as time
from machine import Pin,ADC

adc_pins = [ 26,27,28,29 ]
adc_units = [ ADC(pin) for pin in adc_pins ]
vsys_adc = adc_units[-1]

try:
    values = []
    while True:
        # read the first 3 ADC inputs 
        for adc in adc_units[:-1]:
            values.append( (adc.read_u16()/65535)*3.3 )
        # read the voltage on the VSYS pin
        values.append( 3*vsys_adc.read_u16()/65535*3.3 )
        print( ['{:.3f}V'.format(v) for v in values] )
        values = []
        time.sleep(2.0)
except KeyboardInterrupt:
    pass

โค้ดตัวอย่างที่ 12: TM1637 4-Digit 7-Segment Display

ตัวอย่างนี้สาธิตการใช้งานไลบรารีสำหรับ TM1637 ซึ่งเป็นไอซีควบคุมการทำงานของโมดูล 4-Digit 7-Segment Display

ไฟล์ tm637.py ซึ่งเป็นไลไบรารีและมีโค้ดที่เขียนด้วยภาษาไมโครไพธอนสำหรับ TM1637 สามารถดาวน์โหลดได้จาก Github ดังนั้นต้องดาวน์โหลดไฟล์นี้แล้วนำไปใส่ไว้ใน File System ของบอร์ด RP2040

การทำงานของโค้ดนี้ได้เลือกใช้ขา GP15 และ GP14 สำหรับขาสัญญาณ CLK แลพ DIO ของโมดูล TM1637 ตามลำดับ (ใช้แรงดันไฟเลี้ยง 3.3V) และจะแสดงผลตัวเลขเป็นนาฬิกาที่นับถอยหลัง เริ่มต้นที่ 2:00 นาที แล้วลดลงทีละหนึ่งทุก ๆ หนึ่งวินาที

import utime as time
from machine import Pin
import tm1637

CLK, DIO = 15, 14
disp = tm1637.TM1637( clk=Pin(CLK), dio=Pin(DIO) )
disp.brightness(7) # set brightness to 7 (max.)

def show_time(sec, colon=True):
    global disp
    text = '{:02d}{:02d}'.format(sec//60, sec%60)
    data = disp.encode_string(text)
    data[1] |= int(colon)*0x80 # show a blinking colon
    disp.write( data )

try:
    seconds = 120 # seconds
    show_time( seconds )
    saved_time = time.ticks_ms()
    cnt = 0;
    while seconds > 0:
        now = time.ticks_ms()
        if time.ticks_diff( now, saved_time ) >= 500:
            saved_time = now
            cnt += 1
            if cnt == 2:
                cnt = 0 
                seconds -= 1
            show_time( seconds, bool(cnt%2) )
except KeyboardInterrupt:
    pass

โค้ดตัวอย่างที่ 13: UART Loopback Test

ตัวอย่างนี้สาธิตการรับส่งข้อมูลผ่านทาง UART ของ RP2040 ในลักษณะ Serial Loopback คือ ส่งข้อมูลออกไปทางขา Tx แล้วก็รับข้อมูลนั้นวกกลับเข้ามาที่ขา Rx

RP2040 มีวงจร UART จำนวน 2 ชุด คือ UART0 และ UART1 และสามารถเลือกใช้ขา GPIO ได้แตกต่างกัน (ดูได้จาก Pico PinMap) แต่ในตัวอย่างนี้ได้เลือกใช้ UART0 และขา GP0 และ GP1 สำหรับขา Tx และ Rx ตามลำดับ และตั้งค่า Baudrate ไว้เท่ากับ 115200

import utime as time
from machine import Pin, UART

# UART0_TX/RX = GPIO-0 / GPIO-1 pins
# UART1 TX/RX = GPIO-4 / GPIO-5 pins

uart = UART(0, baudrate=115200, bits=8, parity=None, stop=1,
            tx=Pin(0), rx=Pin(1) )
# (115200, bits=8, parity=None, stop=1, timeout=1000 )
message = 'Hello'
uart.write( bytes( ord(ch) for ch in message) )
time.sleep_ms(1)
if uart.any():
    data = uart.read( len(message) )
    print( data.decode() )

โค้ดตัวอย่างที่ 14: Rotary Encoder Input

โค้ดตัวอย่างนี้ สาธิตการใช้งานโมดูล Rotary Encoder Switch ซึ่งจะได้สัญญาณดิจิทัล A, B และมีการเปลี่ยนสถานะลอจิกเมื่อมีการหมุนตามเข็มหรือทวนเข็มนาฬิกา (Clockwise / Anti-Clockwise Rotation) นอกจากนั้นแล้ว ยังมีอีกหนึ่งสัญญาณ SW ให้ผลเหมือนกับวงจรกดปุ่มหรือสวิตช์ปุ่มกด

การตรวจสอบดูว่า มีการหมุนเปลี่ยนตำแหน่งหรือไม่ และเปลี่ยนไปในทิศทางใด เราจะใช้อินเทอร์รัพท์เพื่อดูการเปลี่ยนสถานะที่ขา A ทั้งขอบขาขึ้นและขาลง (Both Rising Edge & Falling Edge) ถ้าไม่มีการเปลี่ยนตำแหน่ง สถานะลอจิกของขา A และ B จะเป็น High

เมื่อเกิดเหตุการณ์ในแต่ละครั้ง ก็ทำให้ฟังก์ชันที่เป็น ISR Handler ที่เกี่ยวข้องทำงาน โดยตรวจสอบดูว่า สถานะของขา A และ B เป็นอย่างไร แล้วใช้ในการเพิ่มหรือลดค่าของตัวนับ (Incremental / Decremental Counter)

ในตัวอย่างนี้ได้เลือกใช้ขา GPIO-16, 17, 18 (GP16,GP17,GP18) ตรงกับขาสัญญาณ A, B, SW ของโมดูล Rotary Encoder Switch ที่ได้นำมาต่อวงจรร่วมกับบอร์ด RPi Pico ถ้าหมุนครบหนึ่งรอบจะได้ทั้งหมด 20 ตำแหน่ง (Positions per Revolution)

ในการต่อวงจรบนเบรดบอร์ด แนะนำให้เพิ่มวงจร R-C Filter (low-pass) ที่ขาสัญญาณ A และ B เพื่อลดปัญหาที่เกิดการกระเด้งของสวิตช์ (Switch Bouncing)

from machine import Pin

pin_sw = Pin(18, Pin.IN)
pin_a, pin_b = Pin(16, Pin.IN), Pin(17, Pin.IN)
print('AB state:', pin_a.value(), pin_b.value())

# pulse sequence on pins A and B
# clockwise:      AB = 11 -> 01 -> 00 -> 10 -> 11
# anti-clockwise: AB = 11 -> 10 -> 00 -> 01 -> 11

cnt = 0 # set the rotary counter to 0

def pin_a_handler(pin):
    global cnt, pin_a, pin_b
    a,b = pin_a.value(), pin_b.value()
    if a != b:
        cnt += 1
    else:
        cnt -= 1

def pin_sw_handler(pin):
    global cnt
    cnt = 0 # reset counter

# enable IRQ pin-change on the A pin
pin_a.irq( trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING,
    handler=pin_a_handler )     
# enable IRQ pin-change on the SW pin
pin_sw.irq( trigger=Pin.IRQ_FALLING, 
    handler=pin_sw_handler) 

try:
    position = cnt//2
    while True:
        if position != cnt//2:
            position = cnt//2
            print('position:', position)
        pass
except KeyboardInterrupt:
    pass

โค้ดตัวอย่างที่ 15: Memory Info

ตัวอย่างนี้สาธิตการใช้คำสั่งจากโมดูล gc (Garbage Collector) ของไมโครไพธอน และใช้คำสั่งเพื่อตรวจสอบดูปริมาณการใช้หน่วยความจำ หรือที่ยังเหลืออยู่

import gc

mem_free  = gc.mem_free() 
mem_alloc = gc.mem_alloc()
mem_total = mem_free + mem_alloc

gc.collect() # call garbage collector
text_fmt = 'Memory (total/alloc/free): {:,} / {:,} / {:,} bytes'
print( text_fmt.format(mem_total, mem_alloc, mem_free) )

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

Memory (total/alloc/free): 192,080 / 60,368 / 131,712 bytes

โค้ดตัวอย่างที่ 16: SH1106 OLED I2D Display

ตัวอย่างนี้สาธิตการใช้โมดูล SH1106 OLED Display ที่เชื่อมต่อด้วยบัส I2C โดยเลือกใช้ I2C0 ของ RP2040 และใช้ขา GP16 / GP17 สำหรับขา SDA/SCL ตามลำดับ และนำมาแสดงข้อความเป็นตัวอย่าง

การใช้งานโมดูล SH1106 ทำได้ไม่ยากเนื่องจากมีไลบรารีไว้ให้ใช้งาน สามารถดาวน์โหลดไฟล์ sh1106.py จาก Github แล้วนำไปใส่ลงในไดรฟ์ของไมโครไพธอนสำหรับบอร์ด RPi Pico

from machine import Pin, I2C
import utime as time
from sh1106 import *

# use I2C0 and GPIO 16,17 for SDA and SCL pins
i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=400000)

print(i2c.scan())

I2C_ADDR = 0x3C   # the I2C address of SH1106 display module
W, H = 128, 64    # the screen size (128 x 64 pixels)
BLACK, WHITE = 0,1

# scan for I2C devices
if I2C_ADDR not in i2c.scan():
    raise RuntimeError('OLED SH1106 not found!!!')

# create an SH1106 object 
disp = SH1106_I2C( W, H, i2c, addr=I2C_ADDR )
# rotate the screen
disp.rotate( True )
# fill the entire display
disp.fill( BLACK )
# draw a white frame 
disp.rect( 0, 0, W, H, WHITE )
# write some text lines (using the default font)
xpos,ypos = 0, 6
text_lines = ["Hi!", "MicroPython", "Raspberry Pi", "RP2040"]
for line in text_lines:
    disp.text( '{:^16s}'.format(line), xpos, ypos )
    ypos += 12
disp.show() # update the display 
time.sleep_ms(1000)
#disp.poweroff()

โค้ดตัวอย่างที่ 17: Ultrasonic Distance Sensor

โค้ดนี้สาธิตการสร้างสัญญาณเอาต์พุตแบบพัลส์ เพื่อส่งไปยังโมดูลเซนเซอร์วัดระยะห่างจากสิ่งกีดขวางด้วยคลื่นอัลตร้าโซนิก (Ultrasonic Distance Sensor Module) และวัดความกว้างของสัญญาณพัลส์ที่ได้จากโมดูลเพื่อนำมาคำนวณระยะห่าง

โมดูลเซนเซอร์มีขา Trig (Trigger) เป็นอินพุต และขา Echo เป็นเอาต์พุต (เลือกใช้รุ่นที่ทำงานได้โดยใช้แรงดันไฟเลี้ยง +3.3V) ไมโครคอนโทรลเลอร์จะต้องสร้างสัญญาณพัลส์ เช่น มีความกว้างช่วง High อย่างน้อย 10 usec ไปยังขา Trig จากนั้นให้วัดความกว้างของสัญญาณพัลส์ที่ขา Echo

การวัดความกว้างของสัญญาณพัลส์ที่ขา Echo จะต้องรอให้สัญญาณนั้น เปลี่ยนจาก Low เป็น High แล้วเปลี่ยนจาก High เป็น Low ตามลำดับ

ในตัวอย่างนี้ได้เลือกใช้ขา GP19 และ GP18 สำหรับขา Trig และ Echo ตามลำดับ

import utime as time
from machine import Pin

trig = Pin( 19, Pin.OUT )
echo = Pin( 18, Pin.IN  )

saved_times = [0,0]

def pin_handler(pin):
    global saved_times
    # save the timestamp
    saved_times[ pin.value() ] = time.ticks_us()

def measure():
    # generate a short HIGH pulse on Trig pin
    trig.high()
    time.sleep_us(10)
    trig.low()
    # enable IRQ on Echo pin
    echo.irq( trigger=Pin.IRQ_FALLING|Pin.IRQ_RISING, handler=pin_handler)
    # wait for 30 msec before reading timestamps
    time.sleep_ms(30)
    # disable IRQ on Echo pin
    echo.irq(None)
    dt = time.ticks_diff( saved_times[0], saved_times[1] )
    distance = (340*dt/2/10000)
    if distance < 0:
        return -1 # invalid value
    else:
        return distance

try:
    while True:
        d = measure()
        if d > 400:
            print( 'Out of range (>400cm)')
        elif d > 0:
            print( 'Distance: {:.1f} cm.'.format(d) )
        time.sleep_ms(200)
except KeyboardInterrupt:
    pass
finally:
    echo.irq(None) 

โค้ดตัวอย่างที่ 18: 74HC595 Bit Shifting with SPI

ตัวอย่างนี้สาธิตการส่งข้อมูลไปยังไอซี 74HC595 ที่มีขาเอาต์พุต 8 ขา ต่อกับโมดูล 8x LED Bar สำหรับแสดงสถานะลอจิก และการส่งข้อมูลจะใช้วิธีเลื่อนบิตทีละบิตตามจังหวะสัญญาณ Clock

การทำงานของ 74HC595 จะใช้ขาสัญญาณดังนี้ (ศึกษาเพิ่มเติมได้จาก Datasheet [1][2])

  • Serial Clock (SCLK) เป็นขาอินพุต รับสัญญาณ Clock กำหนดจังหวะการเลื่อนบิต

  • Serial Data (SDA) เป็นขาอินพุต รับสัญญาณข้อมูลบิตเข้ามา

  • Load (LOAD) เป็นขาสัญญาณอินพุต รับสัญญาณพัลส์ เพื่อทำให้วงจรภายในไอซี นำข้อมูลที่ได้รับ ไปอัปเดตสถานะบิตที่ขาเอาต์พุต Q0..Q7 หลังจากที่ได้เลื่อนข้อมูลเข้าไปครบแล้ว

  • Q0..Q7 เป็นขาสัญญาณเอาต์พุต สำหรับข้อมูล 8 บิตแบบขนาน

  • Q7S เป็นขาสัญญาณเอาต์พุตสำหรับข้อมูลบิตที่ถูกเลื่อนออกมา (ใช้สำหรับการต่อไอซีแบบ Cascade หรือ Daisy Chain)

  • Master Clear (/MCLR) -- Active-low (ต่อตัวต้านทาน Pullup) ใช้สำหรับรีเซตข้อมูลภายใน

  • Output Enable (/OE) -- Active-low (ต่อตัวต้านทาน Pullup) เปิดหรือปิดขาเอาต์พุต Q0.. Q7

โค้ดนี้ใช้ขา GPIO ในการสร้างสัญญาณควบคุมและส่งข้อมูลไปยัง 74HC595 โดยเลือกใช้ขาดังนี้

  • GP9 = LOAD

  • GP10 = SCK

  • GP11 = SDA

ฟังก์ชัน send_byte() ใช้สำหรับการส่งข้อมูลขนาดหนึ่งไบต์เท่านั้นไปยัง 74HC595 และเลือกได้ว่าจะเลื่อนข้อมูลออกแบบ MSB First หรือ LSB First

import utime as time
from machine import Pin

load_pin  = Pin( 9, Pin.OUT ) # load pin
sda_pin   = Pin(11, Pin.OUT ) # serial data 
sclk_pin  = Pin(10, Pin.OUT ) # serial clock

def send_byte( data, msb_first=False ):
    sda_pin.low()
    sclk_pin.low()
    load_pin.low()
    for i in range(8):
        # shift-out data to the SDA pin, LSB first
        if msb_first:
            bit = (data & 0x80) == 0x80
            data <<= 1
        else:
            bit = (data & 0x01) == 0x01
            data >>= 1
        # send data bit 
        if bit:
            sda_pin.high()
        else:
            sda_pin.low()
        # send a clock pulse 
        sclk_pin.high()
        sclk_pin.low()
    # send a pulse to load pin
    load_pin.high()
    load_pin.low()

for data in [0x00,0x01,0x81,0x42,0x24,0x18,0x55,0xaa,0xff]:
    send_byte( data ^ 0xff, True ) # MSB first, use inverted bits
    time.sleep(0.5)

อีกแนวทางหนึ่งคือ เปลี่ยนมาใช้ Hardware SPI ของ RP2040 ก็มีโค้ดตัวอย่างดังนี้ แต่มีข้อจำกัดของไมโครไพธอน "rp2" ในเวอร์ชันที่ใช้งานคือ สามารถใช้ได้เพียงโหมด MSB First เท่านั้น

โค้ดนี้ใช้ขา GPIO เหมือนเดิม เมื่อเปลี่ยนมาใช้ SPI ก็จะตรงกับ SPI1 ของ RP2040 ( ขา GP10 ตรงกับ SPI1_SCK และ ขา GP11 ตรงกับ SPI1_MOSI )

from machine import Pin, SPI
import utime as time

# SDA=11 (SPI1_MOSI), SCK=10 (SPI1_SCK), LOAD=9
# use SPI bus id=1, SPI mode (0,0), clock frequency = 1 MHz
spi = SPI(1, baudrate=1_000_000, polarity=0, phase=0, bits=8,
          firstbit=SPI.MSB,
          sck=Pin(10), mosi=Pin(11), miso=None )
load = Pin(9,Pin.OUT)

def send_byte( spi, load, data ):
    load.low()
    # send byte, MSB first
    spi.write( bytes([data ^ 0xff]) ) # use inverted bits 
    load.high()
    load.low()

for data in [0x00,0x01,0x81,0x42,0x24,0x18,0x55,0xaa,0xff]:
    send_byte(spi, load, data )
    time.sleep(0.5)
data = 0x01
try:
    while True:
        send_byte( spi, load, data )
        data = ((data << 1) | (data >> 7)) & 0xff
        time.sleep(0.2)
except KeyboardInterrupt:
    pass
finally:
    send_byte( spi, load, 0x00 )
    spi.deinit()

กล่าวสรุป

เนื้อหาในส่วนนี้ ได้นำเสนอตัวอย่างโค้ดไมโครไพธอนที่สามารถนำไปทดสอบการทำงาน โดยใช้บอร์ดไมโครคอนโทรลเลอร์ RPi Pico (RP2040) และจะเห็นได้ว่า มีการนำไปใช้งานร่วมกับโมดูลอิเล็กทรอนิกส์แบบต่าง ๆ ตัวอย่างเหล่านี้ จะช่วยให้ผู้เริ่มศึกษา เห็นรูปแบบการใช้คำสั่งต่าง ๆ ของไมโครไพธอน และเป็นแนวทางในการนำไปประยุกต์ใช้งานต่อไป

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

Last updated