STM32 Code Examples

ตัวอย่างโค้ดไมโครไพธอนสำหรับ STM32 และการใช้งาน Thonny Python IDE

เนื้อหาในส่วนนี้มีตัวอย่างโค้ดในภาษาไมโครไพธอนเพื่อนำไปทดลองใช้กับบอร์ด STM32F411CE ได้นำเสนอไว้เพื่อเป็นแนวทางการเรียนรู้ และสาธิตให้เห็นว่า ภาษาไมโครไพธอน ซึ่งแม้ว่าเป็นภาษาคอมพิวเตอร์ระดับสูงกว่า C/C++ ก็สามารถนำมาใช้ในการเรียนรู้หลักการทำงานของไมโครคอนโทรลเลอร์ได้เช่นกัน

ผู้อ่านควรมีความรู้พื้นฐานเกี่ยวกับการเขียนโค้ดภาษาไพธอนมาบ้างแล้ว และการทำความเข้าใจโค้ดตัวอย่างสำหรับ STM32 เกี่ยวข้องกับวงจรภายในไมโครคอนโทรลเลอร์ตระกูล STM32F4 และการทำงานของฮาร์ดแวร์ประเภทต่าง ๆ ที่นำมาต่อเพิ่ม

การใช้งาน Thonny IDE สำหรับไมโครไพธอน

ซอฟต์แวร์ประเภท Python IDE ที่นำมาใช้ในการเขียนโค้ดสำหรับไมโครไพธอน มีอยู่หลายตัวเลือก เอกสารนี้แนะนำการใช้งาน Thonny IDE (Python IDE for beginners) ในเบื้องต้น ซึ่งเป็นซอฟต์แวร์ประเภท Open Source และนำไปติดตั้งใช้งานได้หลายระบบปฏิบัติการ (Windows, Mac OS และ Linux)

ถ้าใช้ Linux เช่น Ubuntu หรือ Raspbian OS (สำหรับบอร์ด Raspberry Pi) ในโหมด GUI Desktop ก็ให้ทำคำสั่งต่อไปปนี้เพื่อติตตั้งใช้งาน (ใช้ Python 3 และคำสั่ง pip3)

$ sudo apt-get install python3-tk

$ pip3 install thonny

และเรียกใช้ โดยทำคำสั่ง

$ thonny &

เริ่มต้นการใช้งานโดยเปิดโปรแกรม Thonny IDE แล้ว ให้ทำเมนูคำสั่ง Run > Select Interpreter เลือก MicroPython (generic) และเลือกพอร์ต (Port) ที่กำลังเชื่อมต่อกับบอร์ด STM32 ในขณะใช้งาน

เมื่อเชื่อมต่อได้แล้ว จะมองเห็นข้อความในบริเวณ Shell (REPL) ในรูปภาพตัวอย่างแสดงให้เห็นว่า สามารถเชื่อมต่อกับไมโครไพธอน (เช่น เวอร์ชัน v1.12) สำหรับบอร์ด STM32F411CE ได้แล้ว ผู้ใช้สามารถลองพิมพ์และรันคำสั่งของไมโครไพธอนผ่าน REPL Shell (>>>)

ถ้าต้องการเขียนโค้ด แนะนำให้สร้างและบันทึกเป็นไฟล์ใหม่ ซึ่งมีสองตัวเลือกคือ เก็บลงในคอมพิวเตอร์ของผู้ใช้ หรือเก็บลงใน Flash Drive ของบอร์ด STM32

ถ้าจะลองรันโค้ดที่เปิดอยู่ในบริเวณ Code Editor ก็ให้กดปุ่ม Run หรือถ้าจะหยุดการทำงานของโค้ดที่กำลังรันอยู่ ก็ให้ปุ่ม Ctrl+C หรือถ้าจะรีเซตบอร์ด (Soft Reboot) ก็ให้กดปุ่ม Ctrl+D

ข้อมูลต่าง ๆ เกี่ยวกับบอร์ด WeAct Studio Mini-F4 (STM32F4x1) สามารถศึกษาได้จาก URL ต่อไปนี้

ตัวอย่างโค้ดแรกนี้ สาธิตการทำงาน LED ที่ต่อกับขา PC13 ของ STM32F411CE กระพริบได้ โดยเว้นระยะเวลา 100 มิลลิวินาที (msec)

ถ้าต้องการหยุดการทำงานของโค้ด ให้กดปุ่ม Ctrl+C เมื่อใช้ Thonny IDE ในการเขียนและรันโค้ดตัวอย่าง

ภายใต้ /flash ซึ่งเป็น Flash Filesystem ของไมโครไพธอน เราจะเห็นไฟล์ชื่อ boot.py และ main.py เมื่อบอร์ดถูกรีเซตและเริ่มทำงาน จะมีการทำคำสั่งต่าง ๆ ในไฟล์ boot.py

ถ้าในไฟล์ boot.py มีคำสั่ง pyb.main('main.py') จึงจะรันโค้ดในไฟล์ main.py โดยอัตโนมัติ

import machine
import time

LED_OFF = 1
# use the onboard Blue LED
led = machine.Pin('PC13', machine.Pin.OUT)
try:
    while True:
        led.value( not led.value() )
        time.sleep_ms( 100 )
except KeyboardInterrupt:
    pass
finally:
    led.value( LED_OFF )
    print('Done')   

โค้ดตัวอย่างนี้ สาธิตการใช้คำสั่งจากคลาส pyb.Switch และ pyb.LED สำหรับปุ่มกด (Push Button หรือ KEY Switch) และ LED (Blue) ที่มีอยู่บนบอร์ด

หลักการทำงานคือ ให้มีการทำให้ LED กระพริบด้วยอัตราคงที่ (ให้สลับสถานะลอจิก สำหรับเอาต์พุต ทุก ๆ 500 มิลลิวินาที) และทำขั้นตอนตรวจสอบค่า อินพุตจากปุ่มกด ถ้าอ่านค่าได้ True หมายถึง มีการกดปุ่มค้างไว้ในขณะนั้น ให้จบการทำงานของโปรแกรม

คำสั่ง time.ticks_ms() ใช้อ่านค่าเวลาของระบบ เป็นเลขจำนวนเต็มบวก และมีหน่วยเป็นมิลลิวินาที

คำสั่ง time.ticks_diff() ใช้ในการคำนวณผลต่างหรือช่วงเวลา (Time Interval) ระหว่างการบันทึกเวลา 2 ครั้ง (เวลาล่าสุดกับเวลาก่อนหน้านั้น) และมีหน่วยเป็นมิลลิวินาที

import utime as time
import pyb
sw  = pyb.Switch() # user push button
led = pyb.LED(1)   # on-board LED (blue), PC13 pin
    
try:
    last_time = time.ticks_ms() # save timestamp
    while not sw.value():       # is button pressed ?
        now = time.ticks_ms()   # read current timestamp
        time_diff = time.ticks_diff( now, last_time )
        if time_diff >= 500:
            led.toggle()        # toggle LED
            last_time = now     # update timestamp
except KeyboardInterrupt:
    pass
finally:
    led.off() # turn off LED
    print('Done')

โค้ดตัวอย่างที่ 3: LED Toggle (Event-Triggered)

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

เมื่อมีเหตุการณ์ที่เกิดขึ้น (Event-triggered) โดยการกดปุ่มแล้วปล่อยในแต่ละครั้ง ในกรณีนี้คือ จะส่งผลทำให้ LED สลับสถานะ แต่ถ้ากดปุ่มค้างไว้สัก 2–3 วินาที จะทำให้จบการทำงานของโปรแกรม หรือถ้ารันโค้ดผ่าน REPL และกดปุ่ม Ctrl+C จะทำให้จบการทำงานของโปรแกรมเช่นกัน

import pyb
import utime as time

sw  = pyb.Switch() # user push button
led = pyb.LED(1)   # on-board LED (blue), PC13 pin

# set callback function for the push button.
# toggle the LED if the button switch is pressed.
sw.callback( lambda: led.toggle() )

try:
    last_time = time.ticks_ms() # save timestamp
    while True: # main loop
        now = time.ticks_ms() # get current time (msec)
        if sw.value(): # button hold pressed
            if time.ticks_diff( now, last_time ) >= 2000:
                print( 'button long pressed' )
                break # exit the while loop
        else:
            last_time = now # update timestamp
except KeyboardInterrupt: # interrupted by Ctrl+C
    pass
finally:
    sw.callback(None) # disable callback for button
    led.off()         # turn off LED
    print('Done')

ตัวอย่างถัดไปสาธิตการใช้งาน Hardware Timer ของ STM32F4 จากคลาส pyb.Timer โดยสามารถเลือกใช้ Timer จากหมายเลข TIM1..TIM11 ที่มีขนาด 16 บิต (ยกเว้น TIM2 และ TIM5 ที่มีขนาด 32 บิต) และมีตัวหารความถี่ (Frequency Divider) ขนาด 16 บิต

ในตัวอย่างนี้ ความถี่ของการนับ (เลือกโหมดการนับขึ้น pyb.Timer.UP) ได้ถูกตั้งค่าให้เท่ากับ 10 Hz และมีการกำหนดฟังก์ชันให้ทำงานในรูปแบบที่เรียกว่า Callback Function

เมื่อนับได้ครบหนึ่งรอบหรือหนึ่งคาบ จะมีการเรียกฟังก์ชันดังกล่าวให้ทำงานโดยอัตโนมัติ ในกรณีคือ การทำให้ LED สลับสถานะลอจิกหนึ่งครั้ง ดังนั้นเราจะเห็น LED กระพริบในอัตราคงที่

import utime as time
from machine import Pin
import pyb

led = pyb.LED(1) # on-board LED (blue)
# create Timer (select from TIM1..TIM11),
# set timer frequency = 10 Hz (for fast LED blink)
tim = pyb.Timer( 2, mode=pyb.Timer.UP, freq=10 )
tim.callback( lambda t: led.toggle() )
try:
    while True: # main loop
        pass # do nothing in main loop
except KeyboardInterrupt:
    pass
finally:
    tim.callback(None) # disable timer callback
    tim.deinit() # turn off the timer
    led.off()    # turn off the LED
    print('Done')

ตัวอย่างถัดไปสาธิตการใช้งาน Software Timer (หรือเรียกว่า Virtual Timer สำหรับไมโครไพธอน) จากคลาส machine.Timer โดยใช้ FreeRTOS เป็นตัวจัดการเชิงเวลา และจะต้องกำหนดหมายเลข Timer ID ให้เท่ากับ -1

ในตัวอย่างนี้ได้ตั้งค่าให้มีคาบการนับ เท่ากับ 500 มิลลิวินาที เมื่อตัวนับเริ่มนับขึ้นจาก 0 จนครบเวลาหนึ่งคาบของการนับ (Count Overflow Event) จะมีการเรียกฟังก์ชันสำหรับ Callback และจะเกิดซ้ำไปเรื่อย ๆ (Periodic Mode)

import pyb
from machine import Timer
import utime as time

sw  = pyb.Switch()  # user push button
led = pyb.LED(1)    # on-board LED (blue)
# create a software timer in periodic mode 
tim = Timer(-1)
tim.init( mode=Timer.PERIODIC,
          period=500, # period in msec
          callback=lambda t: led.toggle() )
try:
    while True:
        if sw.value(): # check the button's state
            break
except KeyboardInterrupt:
    pass
finally:
    sw.callback(None)
    led.off()
    tim.deinit()
    print('Done')

โค้ดตัวอย่างที่ 6: LED Pulsing using Software Timer in One-Shot Mode

ตัวอย่างถัดไปเป็นการใช้งาน Software Timer (ไทม์เมอร์แบบซอฟต์แวร์) จากคลาส machine.Timer แต่เลือกใช้โหมด One Shot แทน Periodic ซึ่งหมายความว่า ถ้าครบคาบเวลา จะมีการเรียกฟังก์ชันสำหรับ Callback หนึ่งครั้ง และทำเพียงครั้งเดียว (ไม่ทำซ้ำ)

แต่ให้สังเกตว่า การเปิดใช้งาน Software Timer ในกรณีนี้ จะเกิดขึ้นเมื่อมีการกดปุ่มบนบอร์ดหนึ่งครั้ง เมื่อปุ่มถูกกดจะทำให้ฟังก์ชัน start_timer() ทำงาน แล้วเปิดใช้งานไทม์เมอร์ในโหมด One Shot และจะต้องรอให้ผ่านไปครบหนึ่งคาบก่อน (ซึ่งในกรณีคือ 1000 มิลลิวินาที) จึงจะมีการเรียกฟังก์ชัน led_pulses() ให้ทำงานในลำดับถัดไป เมื่อทำให้ LED กระพริบไปจนครบ 10 ครั้ง แล้วจึงจบการทำงานของโปรแกรม

import pyb
from machine import Timer
import utime as time

done = False # global variable

def start_timer(): # callback for button
    sw.callback( None ) # disable callback for button
    tim.init( mode=Timer.ONE_SHOT,
          period=1000, # in msec
          callback=led_pulses )
          
def led_pulses(t): # callback for timer
    global done
    led = pyb.LED(1) # on-board LED (blue)
    # blink the LED for 10 times 
    for i in range(20):
        led.toggle()
        time.sleep_ms(100)
    t.deinit() # disable timer 
    done = True
    
tim = Timer(-1)    # create a virtual timer 
sw  = pyb.Switch() # use onboard button
sw.callback( start_timer ) # set callback for button
try:
    while not done:
        pass # do nothing in the main loop
except KeyboardInterrupt:
    pass
finally:
    print('Done')

ตัวอย่างถัดไปสาธิตการเปิดใช้งาน Hardware Timer ของ STM32F4 จากคลาส pyb.Timer เพื่อสร้างสัญญาณ PWM (Pulse Width Modulation) ที่จะต้องกำหนดความถี่ (Frequency) ในหน่วยเป็น Hz และความกว้างของพัลส์ (Pulse Width) ในหน่วยเป็นไมโครวินาที (usec) สำหรับช่วงที่ลอจิกของสัญญาณเป็น High (ซึ่งหมายถึง การกำหนดค่า Duty Cycle ของสัญญาณ PWM)

การใช้งาน Hardware Timer เพื่อสร้างสัญญาณ PWM เป็นเอาต์พุตได้ จะต้องเลือกจาก TIM1..TIM11 และเลือกช่องสัญญาณ (Channel) ให้ถูกต้องได้ ในตัวอย่างนี้ เราได้เลือกใช้ TIM4 (ขนาด 16 บิต) ที่มีช่อง CH1 .. CH4 ให้เลือกใช้ได้ ถ้าเลือก TIM4_CH3 จะตรงกับขา PB8 แต่ถ้าเลือก TIM4_CH4 จะตรงกับขา PB9 เป็นต้น (ดูแผนผัง PinOut ของบอร์ด)

ในตัวอย่างนี้ ได้เลือกใช้ TIM4_CH3 สำหรับขา PB8 ซึ่งจะต้องนำไปต่อกับวงจร LED ภายนอก มีการตั้งความถี่ให้เท่ากับ 5 Hz (มีคาบเท่ากับ 200 มิลลิวินาที) และค่า Pulse Width ให้เท่ากับครึ่งหนึ่งของคาบเวลา หรือจะได้ค่า Duty Cycle เท่ากับ 50%

คำสั่งที่เกี่ยวข้องกับ pyb.Timer เช่น คำสั่ง freq() จะให้ค่าความถี่ที่ได้ตั้งค่าไว้ใช้ (มีหน่วยเป็น Hz) คำสั่ง prescaler() จะให้ตัวเลขสำหรับตัวหารความถี่ (Prescaler) และคำสั่ง period() จะให้ตัวเลขเป็นคาบเวลาของตัวนับ

import utime as time
from machine import Pin
import pyb

# print system frequencies
freq = pyb.freq() 
print( 'CPU  freq. [Hz]:', freq[0] )  # 96 MHz
print( 'AHB  freq. [Hz]:', freq[1] )  # 96 MHz
print( 'APB1 freq. [Hz]:', freq[2] )  # 24 MHz
print( 'APB2 freq. [Hz]:', freq[3] )  # 48 MHz

# create Timer (use TIM4)
tim = pyb.Timer( 4, freq=5 ) # 5 Hz (for LED blink)
# Choose PB8 pin for TIM4_CH3 or PB9 pin for TIM4_CH4
pwm = tim.channel( 3, pyb.Timer.PWM,
         pin=pyb.Pin.board.PB8, pulse_width=0 )
pwm.pulse_width( tim.period()//2 ) # 50% duty cycle
print( 'prescaler   : {:>8}'.format( tim.prescaler()) )
print( 'frequency   : {:>8} [Hz]'.format( tim.freq()) )
print( 'source freq.: {:>8} [Hz]'.format( tim.source_freq()) )
print( 'period      : {:>8} [us]'.format( tim.period()) )
print( 'pulse width : {:>8} [us]'.format( pwm.pulse_width()) )

try:
    while True:
        pass # do nothing in the main loop
except KeyboardInterrupt:
    pass
finally:
    tim.deinit()
    print('Done')

ถ้าลองรันโค้ดตัวอย่างนี้ เราจะได้ข้อความเอาต์พุตและตัวเลขดังนี้

CPU freq. [Hz]: 96000000 AHB freq. [Hz]: 96000000 APB1 freq. [Hz]: 24000000 APB2 freq. [Hz]: 48000000 prescaler : 624 frequency : 5 [Hz] source freq.: 48000000 [Hz] period : 15359 [us] pulse width : 7679 [us]

จากข้อความเอาต์พุต มีความหมายดังนี้

  • ความถี่ของซีพียู (CPU) หรือ SysClk เท่ากับ 96 MHz

  • ความถี่ของการอินเทอร์เฟสด้วยบัส AHB เท่ากับ 96 MHz = SysClk/1

  • ความถี่ของการอินเทอร์เฟสด้วยบัส APB1 จะได้ 24 MHz = SysClk/4

  • ความถี่ของการอินเทอร์เฟสด้วยบัส APB2 จะได้ 48 MHz = SysClk/2

  • ความถี่ของ Timer (TIM4) ได้ตั้งค่าให้นับด้วยความถี่เท่ากับ 5 Hz

  • ตัวหารความถี่ (Prescaler) เท่ากับ 624 และคาบ (Period) เท่ากับ 15359

จากตัวเลขเหล่านี้ เราสามารถระบุความสัมพันธ์ได้ดังนี้

PWMfrequency=Timerfrequency(prescaler+1)×(period+1)PWM\, frequency = \frac{ Timer\, frequency}{(prescaler+1) \times (period+1)}

หรือ คำนวณเป็นตัวเลขได้ดังนี้

PWMfrequency=48MHz(624+1)×(15359+1)=5HzPWM\, frequency = \frac{48\, MHz}{(624+1)\times(15359+1)} = 5\,Hz

ข้อสังเกต: วงจรไทม์เมอร์ TIM4 ภายใน STM32F411CE เชื่อมต่อโดยใช้บัส APB1 ที่มีความถี่ 24 MHz (= 96MHz /4) แต่เนื่องจากว่า APB1 Prescaler=4 ซึ่งมากกว่า 1 จึงมีการเพิ่มความถี่เป็น 2 เท่า สำหรับใช้เป็นความถี่ของตัวนับ (APB Clock Timers) และได้ความถี่เท่ากับ 48 MHz (รายละเอียดศึกษาได้จาก Clock Tree ในเอกสาร Reference Manual) และไฟล์ timer.c ของ Micropython สำหรับ STM32 port)

ถ้าลองเปลี่ยนจาก TIM4 เป็น TIM1 (และใช้ช่อง CH3 ซึ่งตรงกับขา PA10) ก็สามารถทำงานได้เช่นกัน แต่มีความแตกต่างคือ TIM1 เชื่อมต่อกับบัส APB2 ที่ใช้ความถี่ 48MHz ความถี่ของตัวนับ (เป็น 2 เท่า) จะเท่ากับ 96 MHz และถ้ากำหนดความถี่ให้ได้ 5 Hz เหมือนเดิม จะได้ค่าสำหรับ Period ในกรณีนี้เท่ากับ 30719

ตัวอย่างนี้ สาธิตการทำให้ LED จำนวน 2 ดวง (ที่นำมาต่อวงจรเพิ่มบนเบรดบอร์ด) กระพริบได้ด้วยอัตราคงที่ โดยใช้ Hardware Timer ที่ทำงานในโหมด Output Compare (OC) และกำหนดให้ขา I/O สำหรับ OC Output สลับสถานะได้โดยอัตโนมัติ เมื่อตัวนับมีค่าเท่าค่าเปรียบเทียบที่ได้กำหนดไว้ (Compare Value)

ในตัวอย่างนี้ เราได้เลือกใช้ Timer 3 (TIM3) และช่องสัญญาณ 1 และ 2 (T3_CH1 และ T3_CH2) ซึ่งตรงกับขา PB4 (LED1) และ PB5 (LED2) ตามลำดับ ความถี่ของ TIM3 เท่ากับ 2 Hz และจะทำให้ LED ทั้งสองดวง สลับสถานะทุก ๆ 500 มิลลิวินาที แต่ช่วงเวลาที่เกิดการสลับสถานะจะไม่พร้อมกัน

import pyb

# T3_CH1 -> PB4 pin, T3_CH2 -> PB5 pin
timer = pyb.Timer(3, freq=2) # use TIM3, freq. 2 Hz
half_period = (timer.period()+1)//2
ch1 = timer.channel(1, mode=pyb.Timer.OC_TOGGLE,
            pin=pyb.Pin.board.PB4, compare=0)
ch2 = timer.channel(2, mode=pyb.Timer.OC_TOGGLE,
            pin=pyb.Pin.board.PB5, compare=half_period)
try:
    while True: 
        pass # do nothing in main loop
except KeyboardInterrupt:
    pass
finally:
    timer.deinit() # turn off timer
    print('Done')

โค้ดตัวอย่างที่ 9: LED Fading using Hardware Timer in PWM Mode

ตัวอย่างถัดไป สาธิตการใช้ Hardware Timer จากคลาส pyb.Timer ในโหมด PWM (เลือกใช้ TIM4 ช่องสัญญาณ CH3) ตั้งค่าความถี่ให้เท่ากับ 1000 Hz และปรับค่า Duty Cycle ให้เปลี่ยนแปลงได้โดยใช้ค่าตัวเลขที่คำนวณเก็บไว้ในอาร์เรย์ เพื่อใช้กำหนดความกว้างของพัลส์

การตั้งค่าความถี่ในตัวอย่างนี้ เราไม่ได้กำหนดค่าโดยตรง แต่ใช้อีกวิธีหนึ่งคือ กำหนดค่า Prescaler ให้เท่ากับ 47 และค่า Period ให้เท่ากับ 999

PWMfrequency=Timerfrequency(prescaler+1)×(period+1)PWM\,frequency = \frac{Timer\,frequency}{(prescaler+1)\times(period+1)}

หรือ คำนวณเป็นตัวเลขได้ดังนี้

PWMfrequency=48MHz(47+1)×(999+1)=1000HzPWM\,frequency = \frac{48\,MHz}{(47+1)\times(999+1)} = 1000\,Hz

สัญญาณ PWM ที่ได้ (ขา PB9) จะถูกนำไปใช้ขับวงจร LED และจะเห็นได้ว่า ความสว่างของ LED เปลี่ยนแปลงตามค่า Duty Cycle ของสัญญาณ

import utime as time
from machine import Pin
import pyb
import math

# create a hardware Timer (use TIM4)
tim = pyb.Timer( 4, prescaler=47, period=999 ) # TIM4
# Freq.(Hz) = APB2 freq. (Hz)/(prescaler+1)/(period+1)
#           = 48 MHz /48 /1000 = 1 kHz or 1000 Hz
# choose PB8 pin for TIM4_CH3, or PB9 pin for TIM4_CH4
pwm = tim.channel(4, pyb.Timer.PWM,
         pin=pyb.Pin.board.PB9, pulse_width=0)
print( 'PWM period   :', tim.period() )
print( 'PWM frequency:', tim.freq()   )
try:
    P = tim.period() # get PWM period
    N = 16
    steps = [int(P*math.sin(math.pi*i/N)) for i in range(N)]
    while True:
        for pw in steps:
           pwm.pulse_width( pw ) # change pulse width
           time.sleep_ms( 100 )
except KeyboardInterrupt:
    pass
finally:
    tim.deinit() # disable timer
    print('Done')

ตัวอย่างนี้สาธิตการทำงาน LED จำนวน 3 ดวง กระพริบได้ด้วยอัตราที่ไม่เท่ากัน (เช่น 1 Hz, 2 Hz และ 4 Hz เป็นต้น) โดยใช้ Software Timer เป็นตัวช่วยดำเนินการ

วงจร LED ที่นำมาต่อเพิ่มบนเบรดบอร์ด จำนวน 3 ชุด (ต่อที่ขา PB7, PB8 และ PB9 ตามลำดับ) ทำงานแบบ Active-Low ซึ่งหมายความว่า ถ้าให้เอาต์พุตเป็น 0 จะทำให้ LED อยู่ในสถานะ ON แต่ถ้าเป็น 1 จะได้สถานะเป็น OFF

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

LED_ON  = const(0)
LED_OFF = const(1)
pin_names = ['PB7', 'PB8', 'PB9'] # output pins
leds   = []
timers = []

def timer_cb(t): # timer callback function
    for i in range(len(leds)):
        if t is timers[i]:
             # toggle: read-modify-write
             x = leds[i].value()
             leds[i].value( not x )
             break
             
for pin in pin_names:      # create Pin objects
    leds.append( Pin(pin,mode=Pin.OUT_PP,value=LED_OFF) )
for i in range(len(leds)): # create Timer objects
    timers.append( Timer(-1, freq=(1<<i), callback=timer_cb) )

try:
    while True:
        pass # do nothing in the main loop
except KeyboardInterrupt:
    pass
finally:
    for led in leds:   # turn off all LEDs
        led.value(LED_OFF)
    for tim in timers: # turn off all timers
        tim.deinit() 
    print('Done')

โค้ดตัวอย่างที่ 11: Button Click Counter using External Interrupt

ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อตรวจสอบการกดปุ่มภายนอก โดยใช้หลักการทำงานของไมโครคอนโทรลเลอร์ที่เรียกว่า อินเทอร์รัพท์สำหรับขา GPIO (หรือเรียกที่ว่า External Interrupt) และใช้คลาส pyb.ExtInt มีการจำแนกเหตุการณ์ได้เป็น 3 กรณีคือ ขอบขาขึ้น (Rising Edge) ขอบขาลง (Falling Edge) และเป็นได้ทั้งขอบขาขึ้นและขาลง

ในตัวอย่างนี้ เราเลือกใช้เฉพาะขอบขาลง และใช้ขา PA0 ที่ต่อกับวงจรปุ่มกดภายนอก (ทำงานแบบ Active-Low) เป็นอินพุต ทุกครั้งที่เกิดเหตุการณ์ขอบขาลงที่สัญญาณอินพุต จะมีการเรียกฟังก์ชันสำหรับ Callback ซึ่งจะทำให้ตัวแปร clicked มีค่าเป็น True และปิดการทำงานของอินเทอร์รัพท์ดังกล่าวชั่วคราว

ค่าของตัวแปร clicked จะถูกตรวจสอบในเงื่อนไขสำหรับการทำซ้ำ ถ้ามีค่าเป็นจริง ก็ให้เพิ่มค่าของตัวนับและแสดงข้อความเอาต์พุต จากนั้นกำหนดให้ค่าตัวแปร clicked เป็น False และเปิดการทำงานของอินเทอร์รัพท์อีกครั้ง

ข้อสังเกต: การต่อวงจรปุ่มกด เมื่อกดปุ่มแล้วปล่อย อาจเกิดการกระเด้งของปุ่ม (Bouncing) ทำให้เกิดขอบขาขึ้นหรือขาลงที่สัญญาณอินพุตมากกว่าหนึ่งครั้งได้ วิธีแก้ไขปัญหานี้อย่างง่ายในเบื้องต้นคือ เราสามารถเลือกใช้ตัวเก็บประจุ เช่น 0.1uF มาต่อคร่อมที่ขาสัญญาณกับ GND

import utime as time
from machine import Pin, Timer
import pyb

clicked = False # global variable

def ext_int_cb(irq_line):
    global ext_irq, clicked
    ext_irq.disable() # disable interrupt temporarily
    clicked = True    # set flag

btn_pin = Pin('PA0', mode=Pin.IN)

ext_irq = pyb.ExtInt( btn_pin,
            pyb.ExtInt.IRQ_FALLING,
            pyb.Pin.PULL_UP, ext_int_cb )
            
try:
    cnt = 0  # initialize click counter
    while True: # main loop
        if clicked: # the button was clicked
            cnt += 1  # increment click counter
            print('Button clicked', cnt)
            clicked = False  # clear flag
            ext_irq.enable() # re-enable interrupt
        time.sleep_ms(200)
        
except KeyboardInterrupt:
    pass
finally:
    ext_irq.disable()
    print('Done')

โค้ดตัวอย่างที่ 12: Rotary Encoder Reading using External Interrupt

ตัวอย่างถัดไป สาธิตการประยุกต์ใช้งานอินเทอร์รัพท์ภายนอกที่ขาอินพุต 2 ขา คือ การนำไปต่อกับโมดูลที่เรียกว่า Incremental Rotary Encoder โดยใช้สัญญาณ 2 เส้นคือ A, B (หรือบางที ก็ตั้งชื่อว่า CLK, DATA) ตามลำดับ

เมื่อมีการเปลี่ยนตำแหน่งเชิงมุมของแกนหมุนที่ตัวโมดูล จะทำให้เกิดสัญญาณพัลส์ที่ขา A, B โดยมีเฟสต่างกัน 90 องศา (Phase Shift) ความกว้างของพัลส์ขึ้นอยู่กับอัตราความเร็วในการหมุน และมีทิศทางการหมุนได้สองทิศทาง (หมุนทวนหรือหมุนตามเข็มนาฬิกา)

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

ในการตรวจสอบการเปลี่ยนแปลงที่สัญญาณอินพุตทั้งสอง เราสามารถเปิดใช้งานอินเทอร์รัพท์ภายนอกได้ โดยเลือกชนิดของขอบเหตุการณ์เป็นทั้งแบบ Rising และ Falling Edge

ในตัวอย่างนี้ ได้เลือกใช้ขา PB4 และ PB5 ที่นำไปต่อกับโมดูล Rotary Encoder เมื่อมีการเปลี่ยนขอบสัญญาณใด ๆ ที่ขาทั้งสอง จะมีการเรียกฟังก์ชันสำหรับ Callback ที่เกี่ยวข้องกับแต่ละขา และจะตรวจสอบว่า จะต้องเพิ่มหรือลดค่าของตัวนับ (ใช้ตัวแปรชื่อ cnt)

การหมุนเชิงมุมไปหนึ่งตำแหน่ง จะทำให้ตัวนับ เพิ่มขึ้นหรือลดลง ครั้งละ 4 ดังนั้นค่าของตัวนับจะถูกหารด้วย 4 เพื่อใช้เป็นค่าของตำแหน่ง (pos) นอกจากนั้นยังมีการกำหนดค่าต่ำสุดและสูงสุดไว้สำหรับค่าของตัวนับ

import utime as time
from machine import Pin
import pyb
from micropython import const
# constants
POS_MAX = const(100)
POS_MIN = const(0)
# global variables 
cnt = 0
pos = 0

def ext_a_cb(irq_line): # callback for pin A
    global cnt, pos
    a, b = a_pin.value(), b_pin.value()
    step = 1 if a^b else -1
    new_cnt = cnt+step
    new_cnt = max(4*POS_MIN,new_cnt)
    new_cnt = min(4*POS_MAX,new_cnt)
    cnt = new_cnt
    pos = cnt//4

def ext_b_cb(irq_line): # callback for pin B
    global cnt, pos
    a, b = a_pin.value(), b_pin.value()
    step = -1 if a^b else +1
    new_cnt = cnt+step
    new_cnt = max(4*POS_MIN,new_cnt)
    new_cnt = min(4*POS_MAX,new_cnt)
    cnt = new_cnt
    pos = cnt//4

a_pin = Pin('PB4', mode=Pin.IN) # pin A
b_pin = Pin('PB5', mode=Pin.IN) # pin B

# enable external interrupt for pin A
ext_a = pyb.ExtInt( a_pin,
            pyb.ExtInt.IRQ_RISING_FALLING,
            pyb.Pin.PULL_UP, ext_a_cb )

# enable external interrupt for pin B
ext_b = pyb.ExtInt( b_pin,
            pyb.ExtInt.IRQ_RISING_FALLING,
            pyb.Pin.PULL_UP, ext_b_cb )

try:
    last_pos = pos
    while True: # main loop
        if last_pos != pos: # position changed ?
            print('Position: {}'.format(pos))
            last_pos = pos
        time.sleep_ms(20)
except KeyboardInterrupt:
    pass
finally:
    # disable external interrupts 
    pyb.ExtInt( a_pin,
            pyb.ExtInt.IRQ_RISING_FALLING,
            pull=pyb.Pin.PULL_NONE, callback=None)
    pyb.ExtInt( b_pin,
            pyb.ExtInt.IRQ_RISING_FALLING,
            pull=pyb.Pin.PULL_NONE, callback=None)
    print('Done')

โค้ดตัวอย่างที่ 13: Rotary Encoder Reading using Timer in Quadrature-Encoder Mode

ตัวอย่างถัดไปสาธิตการใช้ Hardware Timer ในโหมดการนับ โดยใช้สัญญาณอินพุตแบบ Quadrature Encoder เหมือนในกรณีของโมดูล Incremental Rotary Encoder ซึ่งมี 2 ช่องสัญญาณ

ทุกครั้งมีการเปลี่ยนแปลงที่ขาอินพุต จะมีการเพิ่มหรือลดค่าของตัวนับโดยอัตโนมัติ ซึ่งขึ้นอยู่กับทิศทางการหมุน ตัวนับภายในจะมีค่าเพิ่มขึ้นหรือลดลงครั้งละ 4 เมื่อหมุนไปหนึ่งตำแหน่ง ดังนั้นเราจึงหารด้วย 4 แล้วนำผลลัพธ์ที่ได้มาใช้ระบุตำแหน่ง (Position)

นอกจากนั้นยังมีการกำหนดช่วงของตำแหน่งในตัวอย่างนี้ ให้อยู่ในช่วง 0..99 และถ้านับเกิน จะเกิด Rollover โดยอัตโนมัติ เช่น ถ้านับไปถึง 0 ถัดไปจะเป็น 99 หรือในทางตรงข้าม ถ้านับถึง 99 แล้ว ถัดไปคือ 0

เนื่องจากเราได้เลือกใช้ขา PB4 และ PB5 ถ้าจะใช้งานร่วมกับ Timer ก็จะตรงกับ TIM3 และช่อง 1 และ 2 (TIM3_CH1 และ TIM3_CH2)

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

args = {'pull': pyb.Pin.PULL_NONE, 'af': pyb.Pin.AF2_TIM3}
a_pin = Pin('PB4', mode=pyb.Pin.AF_PP, **args)
b_pin = Pin('PB5', mode=pyb.Pin.AF_PP, **args)

NUM_STEPS = const(100)
timer = pyb.Timer(3, prescaler=1, period=(4*NUM_STEPS-1))
channel = timer.channel(1, pyb.Timer.ENC_AB)
timer.counter(0) # reset counter

try:
    saved_cnt = timer.counter()//4
    while True:
        cnt = timer.counter()//4
        if saved_cnt != cnt:
            saved_cnt = cnt
            print( 'Position: {}'.format(cnt) )
        time.sleep_ms(10)
except KeyboardInterrupt:
    pass
finally:
    timer.deinit() # disable timer
    print('Done')

โค้ดตัวอย่างที่ 14: Frequency Measurement using Timer in Input Capture (IC) Mode

วงจร Hardware Timer ของ STM32 สามารถทำงานในโหมดที่เรียกว่า Input Capture (IC) ตัวนับจะทำงานด้วยความถี่คงที่ เมื่อมีเหตุการณ์ เช่น ขอบขาขึ้นหรือขาลง ตามที่กำหนดไว้ จะมีการอ่านค่าของตัวนับในขณะนั้น และนำไปเก็บใส่ลงในรีจิสเตอร์ที่เกี่ยวข้อง (Input Capture Register) ด้วยหลักการทำงานในลักษณะ เราสามารถนำมาใช้วัดความกว้างของสัญญาณพัลส์ วัดคาบของสัญญาณแบบมีคาบ เป็นต้น

ในตัวอย่างนี้ เราจะสร้างสัญญาณ PWM สำหรับ R/C Servo ที่ขา PB4 โดยใช้ Timer 3 ช่องหมายเลข 1 (TIM3_CH1) ให้มีความถี่ 50 Hz และมีความกว้างของพัลส์อยู่ในช่วง 1000 ถึง 2000 ไมโครวินาที

ในการทดสอบการทำงานของโค้ด ขา PB4 จะถูกเชื่อมต่อทางไฟฟ้าด้วยสายไฟภายนอก (Jumper Wire) กับขา PB3 เป็นอินพุตสำหรับ Timer 2 (TIM2 มีขนาด 32 บิต) ช่องหมายเลข 2 (TIM2_CH2) ที่ทำงานในโหมด Input Capture ตัวนับ TIM2 จะทำงานด้วยความถี่ 1 MHz (นับขึ้นทุก ๆ 1 ไมโครวินาที)

import pyb
import utime as time

# Use TIM3_CH1 / PB4 to create PWM output
# Frequency = 50 Hz, period=20000 usec, pulse width 1500 usec
servo_pin = pyb.Pin.board.PB4 # PWM output pin

timer3 = pyb.Timer(3, mode=pyb.Timer.UP, prescaler=83, period=19999)

servo  = timer3.channel(1, mode=pyb.Timer.PWM, pin=servo_pin)
servo.pulse_width(0)

saved_capture = 0
t_pulse = 0

def ic_cb(timer):
    global saved_capture, t_pulse
    if ic_pin.value(): # rising edge
        saved_capture = ic.capture()
    else: # falling edge
        t_pulse = (ic.capture() - saved_capture)
        t_pulse &= 0x0fffffff

# Use TIM2_CH2/ PB3 for input capture
# Frequency = 1MHz (1 usec resolution)
ic_pin = pyb.Pin.board.PB3 # Input capture pin
timer2 = pyb.Timer(2, prescaler=83, period=0x0fffffff)
print( hex(timer2.period()), timer2.prescaler() )

ic = timer2.channel(2, mode=pyb.Timer.IC,
    pin=ic_pin, polarity=pyb.Timer.BOTH, callback=ic_cb)

try:
    values = [1000, 1250, 1500, 1750, 2000]
    while True:
        for pw in values:
            servo.pulse_width( pw )
            time.sleep_ms(100)
            print( 'Pulse width {} usec'.format(t_pulse) )
            t_pulse = 0 # clear measurement value
        time.sleep_ms(500)
except KeyboardInterrupt:
    pass
finally:
    # turn off timers
    timer2.deinit() 
    timer3.deinit() 
    print('Done')

ในตัวอย่างนี้ เราต้องการจะวัดความกว้างของพัลส์ช่วงที่เป็น High ในหน่วยเป็นไมโครวินาที ทุก ๆ ครั้งที่เกิดขอบขาขึ้น หรือขาลง เราจะอ่านค่าตัวนับที่ถูกบันทึกไว้ตอนเกิดเหตุการณ์ดังกล่าว และนำมาคำนวณหาผลต่างซึ่งจะได้เป็นความกว้างของสัญญาณพัลส์ (Pulse Width)

โค้ดตัวอย่างที่ 15: DHT22 Temperature and Relative Humidity Sensor

โมดูล DHT22 (AM2302) ที่ผลิตโดยบริษัท Aosong Electronics (China) เป็นอุปกรณ์ประเภทเซ็นเซอร์สำหรับวัดค่าอุณหภูมิ (Temperature) และความชื้นสัมพัทธ์ (Relative Humidity) ในอากาศ และมีราคาไม่แพง จึงเหมาะสำหรับนำมาฝึกเขียนโปรแกรมไมโครคอนโทรลเลอร์เพื่อการเชื่อมต่อและอ่านข้อมูล อุปกรณ์นี้ใช้เพียงสัญญาณข้อมูลเพียง 1 เส้น แบบสองทิศทาง (Bidirectional Digital I/O)

ไมโครไพธอนสำหรับ STM32 ได้ร่วมไลบรารี dht มาให้แล้ว สามารถใช้คำสั่งที่เกี่ยวข้องเพื่ออ่านค่าจากโมดูล DHT22 ได้สะดวก

โค้ดตัวอย่างนี้เลือกใช้ขา B5 สำหรับเชื่อมต่อกับขา DATA ของโมดูล DHT22 (ใช้แรงดันไฟเลี้ยง 3.3V) และที่ขา DATA จะต้องมีตัวต้านทานแบบ Pull up เช่น 4.7k โอห์ม ต่อไปยัง VCC

from machine import Pin
import time
import dht

sensor = dht.DHT22( Pin('B5') )

def read_dht22():
    try:
        # start measurement
        sensor.measure()
        # read measurement results
        t,h = sensor.temperature(), sensor.humidity()
        return (t,h)
    except OSError:
        # checksum eror
        return None

try:
    t_last = time.ticks_ms()
    while True:
        t_now = time.ticks_ms()
        if time.ticks_diff( t_now, t_last ) >= 2000:
            t_last = t_now
            results = read_dht22()
            if results:
                t,h = results
                print( 'T={:.1f} deg.C, H={:.1f} %'.format(t,h))
            else:
                print( 'DHT22 reading error...')
except KeyboardInterrupt:
    pass
finally:
    print('Done')

โค้ดตัวอย่างที่ 16: DS18B20 Temperature Sensor (OneWire)

ตัวอย่างอย่างถัดไปสาธิตการอ่านค่าจากโมดูลเซ็นเซอร์สำหรับวัดอุณหภูมิ โดยใช้ไอซี DS18B20 (Datasheet) ของบริษัท MAXIM Integrated และใช้รูปแบบการเชื่อมต่อและสื่อสารข้อมูลที่เรียกว่า OneWire

การต่อใช้งานไอซีหรือโมดูล DS18B20 ให้ใช้แรงดันไฟเลี้ยง +3.3V และที่ขา DATA (DQ) ของโมดูลนี้ ให้ต่อตัวต้านทาน 4.7k หรือ 10k แบบ Pull-up ไปยังขา VCC (+3.3V)

สำหรับการเขียนโค้ด ผู้พัฒนาไมโครไพธอนได้จัดทำไลบรารีสำหรับ DS18B20 ไว้ให้ลองใช้งานแล้ว มีอยู่ 2 ไฟล์ที่จะต้องใช้ร่วมกัน คือ onewire.py และ ds18x20.py ดังนั้นให้ดาวน์โหลดไฟล์ทั้งสองและนำไปใส่ลงใน MicroPython Flash Drive ของบอร์ด STM32 จึงจะสามารถทดลองรันโค้ดตัวอย่างต่อไปนี้ได้

ขั้นตอนการทำงานของโค้ดตัวอย่างเริ่มต้นด้วยการเปิดใช้งานขา B5 เป็นขา DATA สำหรับ OneWire และใช้กับโมดูลเซ็นเซอร์ DS18B20 (ใช้คลาสชื่อ OneWire และ DS18X20 ตามลำดับ) จากนั้นมีการสแกนหรือตรวจดูว่า พบอุปกรณ์ DS18B20 หรือไม่ (อุปกรณ์แต่ละตัวจะมีหมายเลขที่ไม่ซ้ำกัน เรียกว่า ROM ID หรือ Serial Code มีขนาด 64 บิต) ถ้าพบว่า มีอย่างน้อย 1 ตัว ก็จะทำการอ่านค่าอุณหภูมิ แลัวจะได้ค่าตัวเลขที่มีหน่วยเป็นเซลเซียส (Celsius) มีความละเอียด 0.5°C แสดงผลเป็นข้อความเอาต์พุต

import machine
import time
from onewire import OneWire
from ds18x20 import DS18X20

data_pin = machine.Pin('B5')
ow = OneWire( data_pin )
ow.reset()
ds = DS18X20( ow )
ds18b20_list = ds.scan() 

if len(ds18b20_list) == 0:
    raise RuntimeError('No DS18B20 found!!!')

try:
    while True:
        for rom_addr in ds18b20_list:
            ds.convert_temp()
            time.sleep_ms(750)
            temp = ds.read_temp( rom_addr )
            print('Temperature: {:.1f} deg.C'.format(temp) )
except KeyboardInterrupt:
    pass
finally:
    print('Done')

โค้ดตัวอย่างที่ 17: BH1750 Ambient Light Sensor (I2C)

โค้ดตัวอย่างนี้สาธิตการอ่านค่าจากโมดูลเซ็นเซอร์แสงแบบดิจิทัล (Digital Ambient Light Sensor) โดยให้ค่าความสว่าง (ความเข้มแสง) เป็นข้อมูลตัวเลขจำนวนเต็มขนาด 16 บิต และมีหน่วยเป็น "ลักซ์" (Lux)

ไมโครคอนโทรลเลอร์ STM32 จะทำหน้าที่เป็นอุปกรณ์ I2C Master เพื่อสื่อสารกับโมดูล BH1750 ที่เป็นอุปกรณ์ I2C Slave ผ่านทางบัส I2C

ในการสื่อสารข้อมูลด้วบัส I2C จะต้องระบุแอดเดรส (I2C address) ในการสื่อสารข้อมูลกัน โมดูล BH1750 มีแอดเดรสที่เลือกใช้ได้ 2 ค่าคือ 0x23 และ 0x5C

การเขียนโค้ดไมโครไพธอนเพื่อใช้งานบัส I2C นั้น ก็ใช้คำสั่งในกลุ่ม machine.I2C เช่น คำสั่ง writeto() และ readfrom() เพื่อเขียนหรืออ่านข้อมูลจากอุปกรณ์ I2C Slave เป็นต้น

การทำงานของโค้ดตัวอย่าง เริ่มต้นด้วยการเปิดใช้งาน I2C1 ซึ่งเป็น I2C Bus หมายเลข 1 ของ STM32F411CE (หรือจะลองเปลี่ยนไปใช้ I2C2 แทนก็ได้)

  • I2C1: SCL = PB6 pin, SDA = PB7 pin

  • I2C2: SCL = PB10 pin, SDA = PB3 pin

จากนั้นจึงเริ่มสแกนหาอุปกรณ์ที่เชื่อมต่อกับบัส I2C และดูว่า มีแอดเดรสอยู่ในรายการของโมดูล BH1750 หรือไม่ ถ้าใช่ ก็ให้เขียนข้อมูลไปยังอุปกรณ์ดังกล่าวเพื่อกำหนดค่าเริ่มต้นในการใช้งาน และหลังจากนั้น จึงเป็นการอ่านค่าจากโมดูล BH1750 แล้วแสดงค่าที่ได้เป็นข้อความโดยใช้คำสั่ง print()

import machine
import time 

BH1750_ADDR_LIST = [0x23, 0x5C]

# use I2C1 (SCL1=PB6 and SDA1=PB7 pins on STM32F411CEU6)
i2c = machine.I2C( 1, freq=100000 ) # use I2C1 pins
# OR use explicit pins
# i2c = machine.I2C( scl=machine.Pin("PB6"), sda=machine.Pin("PB7") )
# i2c = machine.I2C( scl=machine.Pin("PB10"), sda=machine.Pin("PB3") )

def bh1750_init(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(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

bh1750_devices = []

# scan I2C slave devices 
found_devices = i2c.scan()
if len(found_devices) == 0:
    print('No I2C devices found..')
else:
    for addr in found_devices:
        print( 'Found device 0x{:02X}h.'.format(addr) )
        if addr in BH1750_ADDR_LIST:
            # initialize BH1750 devices
            bh1750_init( addr )
            bh1750_devices.append( addr )

try:
    while True:
        results = []
        for addr in bh1750_devices:
            # read sensor value
            value = int( bh1750_read(addr) )
            if value:
                s = '0x{:02X}: [{:5d}]'.format(addr,value)
                results.append( s )
        print( ', '.join(results) )
        time.sleep_ms(1000)
except KeyboardInterrupt:
    pass
finally:
    print('Done')

โค้ดตัวอย่างที่ 18: SSD1306 OLED Display (I2C)

ตัวอย่างถัดไปสาธิตการเขียนโค้ดเพื่อแสดงข้อความบนโมดูลแสดงผลแบบ OLED ที่ใช้ชิป SSD1306 Driver และเชื่อมต่อผ่านทางบัส I2C

โมดูล SSD1306 OLED สามารถนำมาใช้สำหรับแสดงผลแบบกราฟิก Monochrome มีขนาดเล็ก เช่น 128x64, 128x32, 96x32 หรือ 64x48 พิกเซล เป็นต้น และเชื่อมต่อกับไมโครคอนโทรลเลอร์ได้ในสองรูปแบบคือ ใช้บัส I2C หรือ SPI ถ้าใช้ในโหมด I2C อุปกรณ์นี้มีแอดเดรสเท่ากับ 0x3C

ในการเขียนโค้ดไมโครไพธอน ก็ได้มีการพัฒนาไลบรารีสำหรับโมดูลนี้ไว้แล้ว สามารถดาวน์โหลดไฟล์ ssd1306.py จาก Github ของ MicroPython ได้ แล้วนำไปใส่ลงใน MicroPython Flash Drive ซึ่งก็ทำได้ง่าย โดยสำเนำไฟล์ดังกล่าวไปใส่ลงในไดร์ฟชื่อ STM32F411CE

ในโค้ดตัวอย่าง จะเห็นได้ว่า ได้เลือกใช้บัส I2C1 ของ STM32F411CE เพื่อใช้งานร่วมกับโมดูล OLED SSD1306 ที่มีขนาด 128x32 พิกเซล มีการแสดงข้อความ และเลื่อนข้อความไปทางซ้าย

import machine
import time
from ssd1306 import *

I2C_ADDR = 0x3C
W, H = 128,32
BLACK, WHITE = 0,1

i2c = machine.I2C( scl=machine.Pin("PB6"), sda=machine.Pin("PB7") )
# scan for I2C devices
if 0x3C not in i2c.scan():
    raise RuntimeError('OLED 1306 not found!!!')

display = SSD1306_I2C(W, H, i2c, addr=I2C_ADDR)

# fill the entire display
display.fill( BLACK )
display.rect(0, 0, W, H, WHITE ) 
# write some text lines (using the default font)
xpos,ypos = 0, 2
text_lines = ["Hi!", "MicroPython", "STM32F411CE"]
for line in text_lines:
    display.text( '{:^16s}'.format(line), xpos, ypos )
    ypos += 10
display.show() # update the display 
time.sleep_ms(1000)
display.rect(0, 0, W, H, BLACK )

STEPS = 2
for i in range(W//STEPS):
    # scroll display to the left
    display.scroll(-STEPS, 0)
    display.show() # update the display 
    time.sleep_ms(100)

display.poweroff()
print('Done')

โค้ดตัวอย่างต่อไปนี้ ใช้ทดสอบการทำงานสำหรับโมดูล 0.66" OLED shield for Wemos D1 Mini ที่มีขนาด 64x48 พิกเซล และใช้แอดเดรส 0x3C

โมดูลนี้ใช้แรงดันไฟเลี้ยง 3.3V และขา D1 และ D2 ของโมดูลดังกล่าว ตรงกับขา SCL และ SDA ตามลำดับ

import machine
import time
from ssd1306 import *

W, H = 64,48
BLACK, WHITE = 0,1

# 0.66" 64x48 OLED shield for Wemos D1 Mini
i2c = machine.I2C( scl=machine.Pin("PB6"),
                   sda=machine.Pin("PB7"),
                   freq=400000 )
# scan for I2C devices
i2c_devices = i2c.scan()
print( [hex(addr) for addr in i2c_devices])

if 0x3C not in i2c_devices:
    raise RuntimeError('OLED 1306 not found!!!')

display = SSD1306_I2C( W, H, i2c, addr=0x3C )
display.fill( BLACK )
display.text( '{:^9s}'.format('0LED'),  0, 6 )
display.text( '{:^9s}'.format('0.66"'), 0, 22 )
display.text( '{:^9s}'.format('64x48'), 0, 38 )
display.show() # update the display 

โค้ดตัวอย่างที่ 19: SHT31 Sensor + SSD1306 OLED Display (I2C)

โค้ดในตัวอย่างนี้สาธิตการอ่านค่าจากโมดูลเซ็นเซอร์ SHT31-DIS สำหรับวัดอุณหภูมิและความชื้นสัมพัทธ์ (ใช้ไอซีที่ผลิตโดยบริษัท SENSIRION) และแสดงค่าที่อ่านได้เป็นข้อความโดยใช้โมดูลแสดงผลแบบกราฟิก I2C OLED SSD1306 ทั้งสองอุปกรณ์นี้เชื่อมต่อแบบบัส I2C ( โมดูล SHT31-DIS มีแอดเดรสเท่ากับ 0x44 ในขณะที่โมดูล I2C OLED SSD1306 มีแอดเดรสตรงกับ 0x3C )

import machine
import time
from ssd1306 import *
from sht3x import *

W, H = 128,32
BLACK, WHITE = 0,1

i2c = machine.I2C( scl=machine.Pin("PB6"),
                   sda=machine.Pin("PB7"),
                   freq=400000 )

# scan for I2C devices
i2c_devices = i2c.scan()
print( [hex(addr) for addr in i2c_devices])

if 0x3C not in i2c_devices:
    raise RuntimeError('OLED 1306 not found!!!')
if 0x44 not in i2c_devices:
    raise RuntimeError('SHT31-DIS not found!!!')

sht3x = SHT3x( i2c, 0x44 )
try:
    sht3x.reset()
except Exception as ex:
    print('Error', ex)

display = SSD1306_I2C(W, H, i2c, addr=0x3C)
# fill the entire display
display.fill( BLACK )

display.text( '{:^16s}'.format('SHT31-DIS'), 0, H//2 )
display.show() # update the display 
time.sleep_ms(1000)
display.fill( BLACK )

try:
    state = 0
    t_last = time.ticks_ms()
    while True:
        t_now = time.ticks_ms()
        if time.ticks_diff( t_now, t_last ) >= 500:
            t_last = t_now
            if state == 0: # start measurement
                sht3x.measure()
            else: # read the measurement result
                t,h = sht3x.read()
                text = "T: {:.1f} deg.C".format(t)
                display.fill( BLACK )
                display.text( '{:^16s}'.format(text), 0, 8 )
                text = "H: {:.1f} % RH ".format(h)
                display.text( '{:^16s}'.format(text), 0, 24 )
                display.show() # update the display
            state = (state+1) % 2
except KeyboardInterrupt:
    pass
except Exception as ex:
    print('Error', ex)
finally:
    print('Done')
    

โค้ดไมโครไพธอนที่จะต้องนำไปบันทึกลงในไฟล์ sht3x.py สำหรับ SHT31-DIS เพื่อใช้กับโค้ดตัวอย่างข้างบน

# File: sht3x.py
from micropython import const
import time

class SHT3x:
    def __init__(self, i2c, addr):
        self.i2c  = i2c
        self.addr = addr
    
    def reset(self):
        try: 
            self.i2c.writeto( self.addr, bytearray([0x30,0xa2]) )
        except OSError:
            raise RuntimeError('SHT3x: I2C write failed!')

    def measure(self):
        try: 
            # send command: measurement, high repeatability, with clock stretching 
            self.i2c.writeto( self.addr, bytearray([0x2c,0x06]) )
        except OSError:
            raise RuntimeError('SHT3x: I2C write failed!')
 
    def read(self):
        try:
            raw = self.i2c.readfrom(self.addr, 6)
        except OSError:
            raise RuntimeError('SHT3x: I2C write failed!')
        
        # is CRC8 mismatch ?
        if (self.crc8(raw[0:2]) != raw[2]) or (self.crc8(raw[3:5]) != raw[5]):
            raise RuntimeError('SHT3x: CRC mismatch!')
        
        temp = -45 + (175 * ((raw[0] << 8) + raw[1]) / 65535.0)
        humid = 100 * ((raw[3] << 8) + raw[4]) / 65535.0
        return (temp,humid)

    def crc8(self,buf):
        """ Polynomial 0x31 (x8 + x5 +x4 +1) """
        polynom = const(0x31)
        crc = 0xff;
        for i in range(0, len(buf)):
            crc ^= buf[i]
            for j in range(8):
                if crc & 0x80:
                    crc = (crc << 1) ^ polynom
                else:
                    crc = (crc << 1)
        return (crc & 0xff)

โค้ดตัวอย่างที่ 20: WS2812B RGB LEDs

อุปกรณ์ RGB LED ที่ใช้ไอซี WS2812B หรือบางทีก็เรียกว่า NeoPixel ใช้สัญญาณอินพุตรับข้อมูลเพียง 1 เส้น สำหรับการเลื่อนบิตข้อมูลเข้าไปเพื่อใช้ในการกำหนดค่าสี RGB (Red, Green, Blue) อย่างละ 8 บิต หรือ 24 บิต ต่อ RGB หนึ่งตำแหน่ง (พิกเซล) ถ้ามี RGB LED หลายตำแหน่ง เช่น มีลักษณะเป็นแถบเรียงต่อกันแบบอาร์เรย์ (REB LED Strip) ก็จะส่งข้อมูลต่อกันไป

บริษัท WorldSemi ผู้ออกแบบและพัฒนาไอซี WS2812B ได้กำหนดวิธีการส่งข้อมูลหรือโพรโทคอล (Protocol) เอาไว้แล้ว ซึ่งจะต้องเป็นไปตามนั้น มิเช่นนั้นการรับข้อมูลอาจไม่ถูกต้อง หรือไม่ได้รับข้อมูล

อ้างอิงตามเอกสาร Datasheet การจำแนกข้อมูลบิตแต่ละบิต จะใช้ความกว้างของสัญญาณพัลส์ (Pulse) เป็นตัวกำหนด (หน่วยเป็นไมโครวินาที) ดังนี้

บิตที่มีค่าเป็น 1 (ช่วง High กว้างกว่าช่วง Low)

  • T1H: เริ่มต้นด้วยช่วง High กว้างประมาณ 0.8us และ

  • T1L: ตามด้วยช่วง Low กว้างประมาณ 0.45us

บิตที่มีค่าเป็น 0 (ช่วง Low กว้างกว่าช่วง High)

  • T0H: เริ่มต้นด้วยช่วงช่วง High กว้างประมาณ 0.4 usec และ

  • T0L: ตามด้วยช่วง Low กว้างประมาณ 0.85 usec

และมีความคลาดเคลื่อนได้ไม่เกิน +/- 150 usec

ลำดับของบิตในแต่ละไบต์ จะเป็นแบบ MSB First และลำดับข้อมูลไบต์ที่จะถูกส่งออกไป เป็นแบบ GRB Format คือ ไบต์แรกสำหรับสีเขียว (G) ไบต์ที่สองสำหรับสีแดง (R) และไบต์ที่สามสำหรับสีเขียว (B) เพื่อใช้กับ RGB LED ในตำแหน่งแรก

ในการส่งข้อมูลไบต์เหล่านี้ เราจะใช้วงจร SPI ที่มีให้เลือกใช้มากกว่าหนึ่งชุด (เช่น เลือกใช้ SPI Bus 1 หรือ SPI Bus 2) ของ STM32F411CE เป็นตัวดำเนินการ และข้อมูลจะถูกส่งออกไปผ่านขา SPI MOSI

การส่งข้อมูลโดยใช้ SPI จะต้องกำหนดความถี่ SCLK และเป็นตัวกำหนดความกว้างของเวลาสำหรับแต่ละบิตในหนึ่งไบต์ ถ้ามีข้อมูลหลายไบต์ ก็จะถูกส่งออกไปตามลำดับแบบต่อเนื่องจนกว่าจะครบ

เราจะใช้ข้อมูล 4 บิต สำหรับเป็นตัวกำหนดค่าบิต 0 หรือ 1 ของค่าสีแต่ละบิต ดังนี้

  • 1110” (ช่วง High กว้างกว่าช่วง Low) หมายถึง บิต 1

  • 1000” (ช่วง Low กว้างกว่าช่วง High) หมายถึง บิต 0

ถ้าเรามีค่าสี เช่น 3 ไบต์ หรือ 24 บิต (เรียกว่า Color Bits) ต่อหนึ่งตำแหน่ง เราจะต้องสร้างสัญญาณบิตสำหรับ SPI ที่ขา MOSI ทั้งหมด 24*4 = 96 บิต (เรียกว่า SPI Data Bits) หรือ 12 ไบต์

คำถามคือ แต่ละบิตที่ถูกส่งออกไปทาง SPI จะต้องมีความกว้างเท่าไหร่ ? ถ้าเราลองเลือกความถี่ SCK ให้เท่ากับ 3.2 MHz จะได้ความกว้างของบิตเท่ากับ 1/3.2MHz = 0.3125 usec (ไมโครวินาที) และจะได้ความกว้างดังนี้ (หน่วยเป็น usec)

  • 1110” (บิต 1): T1H=3*0.3125=0.9375 , T1L=0.3125

  • 1000” (บิต 0): T0H=0.3125, T0L=3*0.3125 = 0.9375

ซึ่งยังถือว่า ค่าเหล่านี้ยังอยู่ในช่วงที่รับได้ (เมื่อพิจารณา +/-150 usec แล้ว)

ลองมาดูโค้ดสาธิตการทำงาน โดยเราจะเลือกใช้บัส SPI หมายเลข 1 และจะต้องใช้ขา A7 ของ STM32F411CE เป็นขาเอาต์พุต นำไปต่อกับขา DIN ของโมดูล WS2812B เลือกใช้แบบ RGB LED Bar มีทั้งหมด 8 ดวง (8 ตำแหน่ง)

import pyb
import utime as time
from ws2812b import *

# set the number of RGB LEDs 
NUM_LEDS = 8

# use SPI bus 1, use "A7" pin as SPI1 MOSI pin
# use SCK freq. = 3.2MHz
spi_bus = 1 
spi = pyb.SPI(spi_bus, pyb.SPI.MASTER,
              baudrate=3200000,
              polarity=0, phase=1)

neopixel = WS2812B( spi, NUM_LEDS )

data = NUM_LEDS * [(0,0,0)]
data[0:5] = [
    (255,0,0),(150,0,0),(20,0,0),
    (10,0,0),(5,0,0) ]
# write data to WS2812B and update colors
neopixel.write( data )
neopixel.show()

try:
    while True:
        # rotate the elements in the list
        data.append( data.pop(0) )
        # write data to WS2812B and update colors
        neopixel.write( data )
        neopixel.show()
        time.sleep_ms(250)
        
except KeyboardInterrupt:
    pass
finally:
    # turn all the RGB LED off
    neopixel.write( NUM_LEDS*[(0,0,0)] )
    neopixel.show()
    print('Done')

โค้ดไมโครไพธอนที่จะต้องนำไปบันทึกลงไฟล์ ws2812b.py เพื่อใช้ร่วมกับโค้ดตัวอย่างข้างบน

# lookup table for bit pattern conversion
BITS_CONV = (0x88, 0x8e, 0xe8, 0xee)

class WS2812B():

    def __init__(self, spi, num_leds):
        self.spi = spi
        self.num_leds = num_leds
        # SPI data buffer
        buf_size = self.num_leds * 3 * len(BITS_CONV)
        self.buf = bytearray( buf_size )
        
    def write(self, colors, start=0 ):
        i = start
        for r,g,b in colors:
            for c in (g,r,b): # for GRB format
                # convert 2 color bits into 8 SPI data bits
                self.buf[i+0] = BITS_CONV[c >> 6 & 0b11]
                self.buf[i+1] = BITS_CONV[c >> 4 & 0b11]
                self.buf[i+2] = BITS_CONV[c >> 2 & 0b11]
                self.buf[i+3] = BITS_CONV[c & 0b11]
                i += 4
            
    def show(self):
        self.spi.send(self.buf) # send data buffer to the SPI bus

โค้ดตัวอย่างที่ 21: SPI SSD1306 OLED

โค้ตัวอย่างนี้สาธิตการใช้งานโมดูล SSD1306 OLED ขนาด 128x64 พิกเซล แต่ใช้วิธีการเชื่อมต่อแบบ SPI และเชื่อมต่อขากับขา I/O ของ STM32 ดังนี้

SPI OLED STM32F411CEU6 GND GND VCC 3.3V D0 (SCLK) SCK1 / PA5 D1 (MOSI) MOSI1 / PA7 RST (/RESET) PB6 DC (DATA/CMD) PB7 CS (/Chip Select) PA4

หลักการทำงานของโค้ดมีดังนี้ เริ่มต้นด้วยการเลือกใช้ SPI Bus 1 (SPI1) ซึ่งมีขาที่เกี่ยวข้องได้แก่ SCK1, MOSI1, MISO1 มีการตั้งความถี่ของ SPI Bus Frequency ให้เท่ากับ 8 MHz และทำงานโหมด (0,0)

ถัดไปเป็นการใช้งานโมดูล SSD1306_SPI จากไฟล์ ssd1306.py และให้ใช้ขาของ SPI1 (ใช้แค่ขา SCK1 และ MOSI1) และ I/O อีก 3 ขา สำหรับ RST, DC, CS

ข้อมูลที่จะถูกนำมาแสดงผลบนโมดูลกราฟิก OLED นั้น ได้จากการสุ่มตัวเลขในช่วง 0 ถึง 100 เก็บไว้ในอาร์เรย์ แล้วนำมาแสดงค่าในรูปของกราฟแท่ง

ในการทำงานแต่ละรอบของ while loop จะมีการลบข้อมูลออกหนึ่งค่าที่ตำแหน่งสุดท้ายของอาร์เรย์ สุ่มตัวเลขใหม่และใส่ลงในอาร์เรย์ในตำแหน่งแรก

from machine import Pin, SPI
from ssd1306 import SSD1306_SPI as ssd
import time
import random

spi = SPI(1, baudrate=8000000, polarity=0, phase=0)
rst = Pin('B6', mode=Pin.OUT) 
dc  = Pin('B7', mode=Pin.OUT) 
cs  = Pin('A4', mode=Pin.OUT)
W, H = 128, 64
display = ssd( W, H, spi, dc, rst, cs )
display.poweron()
display.fill(0)

rand_next = lambda: random.randint(1,100)

N = 32
try:
    data = [rand_next() for i in range(N)]
    while True:
        x = 1
        y = H-2
        display.fill(0)
        for value in data:
            h = int((H-4)*value/100)
            display.fill_rect(x,y-h,3,h,1)
            x = x + 4            
        display.show()
        time.sleep(1.0)
        data.pop(-1)
        data.insert(0, random.randint(1,100)) 
except KeyboardInterrupt:
    print('Done')
finally:
    spi.deinit()

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

โค้ดตัวอย่างนี้สาธิตการอ่านค่าจากโมดูลเซ็นเซอร์แสงแบบแอนะล็อก (CJMCU TEMT6000 Ambient Light Sensor Module) ที่มี 3 ขา คือ V (VCC), G (GND), S (SIGNAL)

ขา S เป็นขาสัญญาณเอาต์พุต-แอนะล็อกจากโมดูลดังกล่าว เชื่อมต่อกับขา PA1 ของ STM32 และมีการเปิดใช้งานวงจร ADC (Analog-to-Digtal Converter) ภายใน เพื่อแปลงค่าสัญญาณอินพุต (ระดับแรงดันไฟฟ้าในช่วง 0V ถึง 3.3V) ให้เป็นค่าเลขจำนวนเต็ม

ไมโครไพธอนมีคำสั่งที่เกี่ยวข้องกับ ADC ไว้ให้ใช้งานคือ machine.ADC โดยจะต้องสร้างออบเจกต์จากคลาสดังกล่าว และระบุชื่อขาที่จะใช้งาน (ในกรณีนี้คือ "A1") จากนั้นจึงใช้คำสั่ง read_u16() เพื่ออ่านค่าอินพุตที่ขาดังกล่าว และจะได้เป็นเลขจำนวนเต็มในช่วง 0 ถึง 65535 ค่าที่อ่านได้จะถูกนำมาแสดงผลแบบกราฟแท่งบนจอ OLED และอัปเดตทุก ๆ 0.1 วินาที

from machine import Pin, SPI, ADC
from ssd1306 import SSD1306_SPI as ssd
import time

ain = Pin('A1') # use PA1 pin as analog input pin
adc = ADC(ain)

spi = SPI(1, baudrate=8000000, polarity=0, phase=0)
rst = Pin('B6', mode=Pin.OUT) 
dc  = Pin('B7', mode=Pin.OUT) 
cs  = Pin('A4', mode=Pin.OUT)
W, H = 128, 64
display = ssd( W, H, spi, dc, rst, cs )
display.poweron()
display.fill(0)

N = 32
try:
    data = []
    while True:
        if len(data) > N:
            data.pop(-1)
        value = adc.read_u16()//256           
        data.insert(0, value) 
        x = 1
        y = H-2
        display.fill(0)
        for value in data:
            h = int((H-4)*value/256)
            display.fill_rect(x,y-h,3,h,1)
            x = x + 4            
        display.show()
        time.sleep(0.1)
except KeyboardInterrupt:
    print('Done')
finally:
    spi.deinit()

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

Last updated