CircuitPython for Pico RP2040

แนะนำการใช้งาน CircuitPython สำหรับบอร์ด Raspberry Pi Pico (RP2040 MCU) ในเบื้องต้น

บอร์ด Raspberry Pi Pico

บอร์ด Raspberry Pi Pico ใช้ตัวประมวลผลแบบ 32 บิต ภายในมีซีพียู Arm Cortex-M0+ Dual-Core และทีมพัฒนาได้มีการเปิดตัวบอร์ดดังกล่าวในเดือนมกราคม พ.ศ. 2564 ผู้ใช้สามารถเขียนโปรแกรมในเบื้องต้น โดยใช้ภาษา C และ MicroPython

บริษัท Adafruit ได้พัฒนาบอร์ดไมโครคอนโทรลเลอร์ที่ใช้ชิป RP2040 SoC เช่นเดียวกัน และได้พัฒนา CircuirPython v6.x ให้รองรับการใช้งานตัวประมวลผลดังกล่าวด้วย และผู้ที่สนใจสามารถดาวน์โหลดไฟล์ .UF2 จากเว็บไซต์ต่อไปนี้เพื่อนำมาติดตั้ง CircuitPython สำหรับ Raspberry Pi Pico

ตัวอย่างไฟล์ที่ได้นำมาทดลองใช้งานจากเว็บ https://circuitpython.org/board/raspberry_pi_pico/

adafruit-circuitpython-raspberry_pi_pico-en_US-6.2.0-rc.0.uf2

การติดตั้งก็ทำได้ง่าย เริ่มต้นโดยการกดปุ่ม BOOTSEL บนบอร์ดค้างไว้ แล้วเสียบสาย USB เชื่อมต่อกับคอมพิวเตอร์ จากนั้นปล่อยปุ่มดังกล่าว เพื่อให้อุปกรณ์เริ่มทำงานและเข้าสู่โหมด USB UF2 Bootloader จากนั้นจะมองเห็นไดรฟ์ใหม่ปรากฏขึ้น (RPI-RP2) จากนั้นให้คลิกเลือกและลากไฟล์ .UF2 ไปยังไดรฟ์ดังกล่าว

เมื่อได้ติดตั้งไฟล์เฟิร์มแวร์สำหรับ CircuitPython ได้สำเร็จแล้ว บอร์ด Pico จะเริ่มต้นทำงานใหม่อีกครั้งและปรากฏไดรฟ์ใหม่ชื่อว่า CIRCUITPY และพร้อมใช้งาน ผู้ใช้สามารถเปิดใช้งานโปรแกรมอย่างเช่น Thonny IDE เชื่อมต่อกับ CircuitPython ผ่านทาง Serial

เมื่อเริ่มต้นทำงาน CircuitPython จะมองเห็นไฟล์ code.py ใน Flash File Storage ของบอร์ด Pico หรือไม่ ถ้ามีไฟล์ดังกล่าว CircuitPython ก็จะทำคำสั่งของโค้ดไพธอนภายในไฟล์ดังกล่าว

ในส่วนของ Shell จะมีข้อความเริ่มต้นซึ่งแสดงข้อมูลเกี่ยวกับเวอร์ชันของ CircuitPython ดังนี้

Adafruit CircuitPython 6.2.0-rc.0 on 2021-04-01; Raspberry Pi Pico with rp2040

ลองแก้ไขโค้ดในไฟล์ code.py ตามตัวอย่างต่อไปนี้

import board
import digitalio
import time

# use the onboard LED pin as output
LED_PIN = board.GP25 # the same as board.LED
led = digitalio.DigitalInOut(LED_PIN)
led.direction = digitalio.Direction.OUTPUT
 
while True:
    led.value = not led.value # toggle the LED output
    time.sleep(0.5) # sleep for 0.5 seconds

led.deinit() # turn off and release the LED pin

การทำงานของโค้ดนี้สาธิตการทำให้ LED ที่ขา GP25 กระพริบได้ โดยการเปิดใช้งานขา GPIO ดังกล่าวในทิศทางเอาต์พุต และใช้คำสั่งจากคลาส DigitalInOut จากโมดูล digitalio ของ CircuitPython

ให้บันทึกการแก้ไขโค้ดลงไฟล์ดังกล่าวโดยกดปุ่ม Ctrl+S จากนั้นกดปุ่ม Ctrl+D เพื่อให้โค้ดดังกล่าวเริ่มทำงานใหม่ ซึ่งจะทำให้ LED (ขา GP25) บนบอร์ดไมโครคอนโทรลเลอร์กระพริบต่อเนื่องไป ถ้าต้องการหยุดการทำงานของโค้ดดังกล่าว ให้กดปุ่ม Ctrl+C (สองครั้ง) ในหน้าต่าง Shell

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

ถ้ดไปเป็นการลองอ่านสถานะอินพุตจากขา GP15 ที่มีการต่อวงจรปุ่มกดภายนอกแบบ Active-Low และมีการทำคำสั่งซ้ำ เพื่อตรวจสอบสถานะของอินพุตจากปุ่มกด ถ้ามีการกดปุ่ม จะได้สถานะเป็น Low หรือ 0 และทำให้ LED หยุดกระพริบ และจบการทำงานของโค้ด

import board
import digitalio
import time

led = digitalio.DigitalInOut(board.GP25)
led.direction = digitalio.Direction.OUTPUT

button = digitalio.DigitalInOut(board.GP15)
button.switch_to_input(pull=digitalio.Pull.UP)
 
while True:
    if button.value == 0:
        print('Button pressed...')
        break
    led.value = not led.value
    time.sleep(0.2)

led.deinit()
button.deinit()

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

ลองเปลี่ยนมาสร้างสัญญาณแบบ PWM (Pulse Width Modulation) โดยใช้คลาส PWMOut ของโมดูล pwmio ตามตัวอย่างต่อไปนี้

import board
import digitalio
import pwmio
import time
import math

pwm_led = pwmio.PWMOut(board.GP25, frequency=500,
             duty_cycle=0, variable_frequency=False)
 
button = digitalio.DigitalInOut(board.GP15)
button.switch_to_input(pull=digitalio.Pull.UP)

index = 0
N = 32
k = (2*math.pi/N)
while True:
    if button.value == 0:
        print('Button pressed...')
        break
    value = (1 + math.sin(k*index))/2
    pwm_led.duty_cycle = (int)((2**16-1)*value)
    index = (index+1) % N
    time.sleep(0.05)

pwm_led.deinit()
button.deinit()

เมื่อโค้ดทำงาน สามารถสังเกตเห็นได้ว่า LED จะค่อย ๆ สว่างขึ้นแล้วดับลงอย่างช้า ๆ (LED Dimming) แล้วเกิดซ้ำไปเรื่อย ๆ

ความสว่าง (LED Brightness) เปลี่ยนแปลงได้ (ตามฟังก์ชันรูปไซน์) และเกิดจากการปรับค่าความกว้างหรือ Duty Cycle ของสัญญาณพัลส์แบบ PWM ที่มีค่าอยู่ในช่วง 0..65535 หรือ 16 บิต และตั้งค่าความถี่ไว้ที่ 500Hz

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

ตัวอย่างถัดไปเป็นการใช้งานโมดูล Rotary Encoder ที่ให้สัญญาณดิจิทัลแบบพัลส์จำนวน 2 ช่อง (A และ B) เมื่อมีการหมุนเกิดขึ้น จะทำให้เกิดสัญญาณพัลส์ทั้งสองช่อง และสามารถตรวจสอบทิศทางการหมุนและการเปลี่ยนแปลงตำแหน่งตามจำนวนสเต็ป (Steps) ที่เกิดขึ้นได้

ผู้พัฒนา CircuitPython ได้สร้างคลาส IncrementalEncoder ของโมดูล rotaryio ไว้สำหรับการใช้งานในลักษณะนี้แล้ว มาดูตัวอย่างการเขียนโค้ดดังนี้

import time
import board
import rotaryio

print('Rotary Encoder demo...')
rotary_pins = (board.GP16, board.GP17)
encoder = rotaryio.IncrementalEncoder( *rotary_pins )
last_position = None
try:
    while True:
        if encoder.position < 0:
            encoder.position = 0
        elif encoder.position > 100:
            encoder.position = 100
        position = encoder.position
        if last_position is None or position != last_position:
            print(position)
        last_position = position
        time.sleep(0.1)
except KeyboardInterrupt:
    print('Terminated...')
encoder.deinit()
del encoder

ในโค้ดตัวอย่างนี้ มีการเลือกใช้ขา GP16 และ GP17 สำหรับสัญญาณอินพุตของ Incremental Rotary Encoder ซึ่งใช้แรงดันไฟเลี้ยง 3.3V

และได้มีการสร้างอ็อปเจกต์จากคลาส rotaryio.IncrementalEncoder และใช้ตัวแปร encoder ในการอ้างอิง ถ้าต้องการอ่านค่าของตัวนับที่ระบุตำแหน่งในขณะนั้น ก็ให้อ่านค่าจาก encoder.position หรือจะเขียนค่าลงในตัวแปรดังกล่าวก็ได้ ในตัวอย่างนี้มีการตรวจสอบค่าโดยการอ่านค่าซ้ำไปเรื่อย ๆ เว้นระยะเวลาประมาณ 0.1 วินาที และมีการจำกัดค่าให้อยู่ในช่วง 0..100

โค้ดตัวอย่างที่ 5: NeoPixel RGB LED - Adjust Brightness

ตัวอย่างถัดไปเป็นการนำโมดูล NeoPixel / WS2812B RGB LEDs (มีจำนวน 8 ตำแหน่ง หรือ จำนวนพิกเซล NUM_PIXELS เท่ากับ 8) มาต่อเพิ่ม ที่ขา GP18 ของบอร์ด Pico โดยใช้โมดูล Rotary Encoder เป็นอุปกรณ์อินพุตเพื่อใช้ในการปรับระดับความสว่างของ LEDs (เลือกแสงสีแดง)

import time
import board
import rotaryio
import neopixel # requires the CircuitPython Neopixel library

print('Rotary Encoder + NeoPixel demo...')
pixel_pin   = board.GP18
rotary_pins = (board.GP16, board.GP17)
NUM_PIXELS  = 8
color = (255,0,0) # use red color

pixels = neopixel.NeoPixel(pixel_pin,
    NUM_PIXELS, brightness=0.0, auto_write=False)

encoder = rotaryio.IncrementalEncoder( *rotary_pins )
last_position = None
try:
    pixels.fill( color ) # use red color
    pixels.show()
    while True:
        if encoder.position < 0:
            encoder.position = 0
        elif encoder.position > 100:
            encoder.position = 100
        position = encoder.position
        if last_position is None or position != last_position:
            pixels.brightness = position/100.0
            pixels.show()
            print(position)
        last_position = position
        time.sleep(0.1)
except KeyboardInterrupt:
    print('Terminated...')
pixels.deinit()
encoder.deinit()
del encoder, pixels

ค่าของตัวนับที่อ่านได้อยู่ในช่วง 0..100 จะถูกนำมาหารด้วย 100.0 เพื่อให้ได้ตัวเลขในช่วง 0.0 ถึง 1.0 และใช้กำหนดระดับความสว่างของโมดูล NeoPixel

สำหรับการใช้งานโมดูล NeoPixel จะต้องติดตั้งไลบรารีเพิ่ม โดยใช้ไฟล์ที่มีชื่อว่า neopixel.mpy ซึ่งสามารถดาวน์โหลดได้จาก https://circuitpython.org/libraries (อยู่ในไฟล์ .zip รวมกับไฟล์ต่าง ๆ ที่เป็นไลบรารีของ CircuitPython) แล้วนำไปใส่ลงในไดเรกทอรี lib ภายในไดรฟ์ CIRCUITPY (Flash Storage) ของบอร์ด Pico

ข้อสังเกต: โมดูล NeoPixel / WS2812B โดยทั่วไปแล้ว จะใช้แรงดันไฟเลี้ยง +5V แต่ก็สามารถใช้ +3.3V ได้เช่นกัน

โค้ดตัวอย่างที่ 6: NeoPixel RGB LEDs - Turn On/Off Pixels

ตัวอย่างถัดไปสาธิตการใช้งานโมดูล Rotary Encoder กับโมดูล NeoPixel ในลักษณะที่แตกต่างจากตัวอย่างที่แล้ว โดยใช้ตำแหน่งที่ได้จาก Rotary Encoder มากำหนดจำนวนของ RGB LED ของโมดูล NeoPixel ที่ให้แสงสีแดง ซึ่งจะอยู่ระหว่าง 0 ถึง 8 (RGB LED ในทุกตำแหน่ง ให้แสงสีแดง)

import time
import board
import rotaryio
import neopixel # requires the CircuitPython Neopixel library

print('Rotary Encoder + NeoPixel demo...')
pixel_pin   = board.GP18
rotary_pins = (board.GP16, board.GP17)
NUM_PIXELS  = 8
color = (255,0,0) # use red color

pixels = neopixel.NeoPixel(pixel_pin,
    NUM_PIXELS, brightness=1.0, auto_write=False)

encoder = rotaryio.IncrementalEncoder( *rotary_pins )
last_position = None

def set_color(level):
    global pixels
    pixels[0:NUM_PIXELS] = level*[color] + (NUM_PIXELS-level)*[(0,0,0)] 
    pixels.show()

try:
    set_color(0)
    while True:
        if encoder.position < 0:
            encoder.position = 0
        elif encoder.position > NUM_PIXELS:
            encoder.position = NUM_PIXELS
        position = encoder.position
        if last_position is None or position != last_position:
            set_color(position)
            print(position)
        last_position = position
        time.sleep(0.1)
except KeyboardInterrupt:
    print('Terminated...')
pixels.deinit()
encoder.deinit()
del encoder, pixels

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

ตัวอย่างถัดไปเป็นการสาธิตการใช้งานโมดูล SH1106 I2C OLED ขนาด 128 x 64 พิกเซล โดยเชื่อมต่อผ่านบัส I2C ความเร็ว 400kHz และเลือกใช้ขา GP18 และ GP19 ของบอร์ด Pico สำหรับขาสัญญาณ SDA และ SCL ของบัส I2C

import time
import board
import busio
from sh1106 import SH1106_I2C

i2c_pins = (board.GP19,board.GP18)
i2c = busio.I2C( scl=i2c_pins[0], sda=i2c_pins[1], frequency=400000 )
while not i2c.try_lock():
    pass
# scan I2C devices
print( [hex(x) for x in i2c.scan()] )

WIDTH  = 128
HEIGHT = 64
disp = SH1106_I2C( WIDTH, HEIGHT, i2c, 0x3c )
disp.poweron()
disp.framebuf.rect(0, 0, WIDTH, HEIGHT//2, 1) # draw a frame
m = 4
disp.framebuf.fill_rect(m, m, WIDTH-2*m, HEIGHT//2-2*m, 1) # fill area
disp.framebuf.text("CircuitPython", 24, 12, 0) # show text 
disp.framebuf.text(" Pico RP2040 ", 24, 42, 1) # show text 
disp.framebuf.text("SH1106 I2C LCD", 20, 54, 1) # show text 
disp.show()
invert = 0
for i in range(5):
    disp.invert( invert ) # invert display
    invert = not invert
    time.sleep(1.0)
disp.poweroff()
i2c.unlock()
i2c.deinit()
print('done')

นอกจากนั้นได้สร้างไฟล์ sh1106.py เพื่อใช้งานเป็นไลบรารีสำหรับการใช้งานโมดูล SH1106 ในเบื้องต้น การทำงานของคลาส SH1106 ต้องใช้งานร่วมกับ FrameBuffer จากโมดูล adafruit_framebuf ดังนั้นจึงต้องติดตั้งไฟล์ adafruit_framebuf.mpy ลงใน/lib และไฟล์ font5x8.bin ด้วย

ไฟล์ sh1106.py

# This is a modified version of the SH1106 MicroPython library
# https://github.com/robert-hh/SH1106/blob/master/sh1106.py

from micropython import const
import adafruit_framebuf as framebuf

# dependencies:
# https://github.com/adafruit/Adafruit_CircuitPython_framebuf
# https://github.com/adafruit/Adafruit_CircuitPython_framebuf/blob/master/examples/font5x8.bin

_SET_CONTRAST        = const(0x81)
_SET_NORM_INV        = const(0xa6)
_SET_DISP            = const(0xae)
_SET_SCAN_DIR        = const(0xc0)
_SET_SEG_REMAP       = const(0xa0)
_LOW_COLUMN_ADDRESS  = const(0x00)
_HIGH_COLUMN_ADDRESS = const(0x10)
_SET_PAGE_ADDRESS    = const(0xB0)

class SH1106_I2C:
    def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
        self.width  = width
        self.height = height
        self.i2c    = i2c
        self.addr   = addr
        self.external_vcc = external_vcc
        self.pages  = self.height//8
        self.buffer = bytearray(self.pages * self.width)
        fb = framebuf.FrameBuffer(self.buffer, self.width, self.height,
                                  framebuf.MVLSB)
        self.framebuf = fb     
        self.framebuf.fill(0) # fill with black color
        self.show()

    def write_cmd(self, cmd):
        buf = bytearray(2)
        buf[0] = 0x80  # Co=1, D/C#=0
        buf[1] = cmd
        self.i2c.writeto(self.addr, buf)

    def write_data(self,buf):
        self.i2c.writeto(self.addr, b'\x40'+buf)

    def contrast(self, contrast):
        self.write_cmd(_SET_CONTRAST)
        self.write_cmd(contrast)

    def invert(self, invert):
        self.write_cmd(_SET_NORM_INV | (invert & 1))

    def rotate(self, flag, update=True):
        if flag:
            self.write_cmd(_SET_SEG_REMAP | 0x01)  # mirror display vertically
            self.write_cmd(_SET_SCAN_DIR  | 0x08)  # mirror display horizontally
        else:
            self.write_cmd(_SET_SEG_REMAP | 0x00)
            self.write_cmd(_SET_SCAN_DIR  | 0x00)
        if update:
            self.show()

    def poweroff(self):
        self.write_cmd(_SET_DISP | 0x00)

    def poweron(self):
        self.write_cmd(_SET_DISP | 0x01)

    def show(self):
        for page in range(self.pages):
            self.write_cmd(_SET_PAGE_ADDRESS | page)
            self.write_cmd(_LOW_COLUMN_ADDRESS  | 2)
            self.write_cmd(_HIGH_COLUMN_ADDRESS | 0)
            self.write_data(
                self.buffer[ self.width*page : self.width*(page+1)] )

######################################################################

โค้ดตัวอย่างที่ 8: DHT22 Temperature & Humidity Sensor

ถัดไปเป็นตัวอย่างการอ่านค่าอุณหภูมิและความชื้นสัมพัทธ์จากโมดูล DHT22 และนำมาแสดงเป็นข้อความบนโมดูล SH1106 I2C OLED

โมดูล DHT22 ใช้แรงดันไฟเลี้ยง +3.3V และในการต่อวงจร ขา DATA ของโมดูลนี้ ต่อเข้ากับขา GP16 ของบอร์ด Pico

การเขียนโค้ด CircuitPython เพื่อใช้งานโมดูล DHT22 ก็ทำได้ไม่ยาก เนื่องจากมีไลบรารีไว้ให้ใช้งาน แต่จะต้องเพิ่มไฟล์ adafruit_dht.mpy (หาได้จาก CircuitPython Library Bundle)

import time
import board
import busio
from sh1106 import SH1106_I2C
from adafruit_dht import DHT22

dht = DHT22(board.GP16)

i2c_pins = (board.GP19,board.GP18)
i2c = busio.I2C( scl=i2c_pins[0], sda=i2c_pins[1], frequency=400000 )
while not i2c.try_lock():
    pass
# scan I2C devices
print( [hex(x) for x in i2c.scan()] )

WIDTH  = 128
HEIGHT = 64
disp = SH1106_I2C( WIDTH, HEIGHT, i2c, 0x3c )
disp.poweron()
disp.framebuf.rect(0, 0, WIDTH, HEIGHT, 1) # draw a frame

while True:
    try:
        dht.measure()
        disp.framebuf.fill_rect(1, 1, WIDTH-2, HEIGHT-2, 0)
        text = " Temperature: {:.1f}".format(dht.temperature)
        print(text)
        disp.framebuf.text(text, 4, 20, 1) # show text
        text = " Rel.Humdity: {:.1f}".format(dht.humidity)
        print(text)
        disp.framebuf.text(text, 4, 40, 1) # show text
        disp.show()
        time.sleep(2.0)
    except RuntimeError as ex:
        pass # DHT checksum error
    except KeyboardInterrupt:
        break

dht.exit()
i2c.unlock()
i2c.deinit()
print('done')

กล่าวสรุป

เนื้อหาในส่วนนี้แนะนำการใช้งาน CircuitPython สำหรับบอร์ด Pico RP2040 ในเบื้องต้น พร้อมโค้ดตัวอย่างเพื่อทดสอบการทำงานร่วมกับโมดูลหรือวงจรอื่น

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

Last updated