MicroPython for STM32F411 Black Pill: Embedded Programming Style
ในบทความนี้ เราลองมาศึกษาตัวอย่างวิธีการเขียนโค้ด MicroPython สำหรับ STM32 โดยเน้นการใช้งานวงจรภายในของไมโครคอนโทรลเลอร์ อย่างเช่น การใช้ขา GPIO การเปิดใช้งานอินเทอร์รัพท์ภายนอก และ การใช้วงจร Timer ในโหมดแบบต่าง ๆ เป็นต้น
Low-Cost STM32 Board for MicroPython
บอร์ดที่เราจะนำมาใช้งานนี้ เป็นบอร์ดราคาถูก (ผลิตในประเทศจีน โดย WeAct Studio) ใช้ไมโครคอนโทรลเลอร์รุ่น STM32F411CEU6 (Datasheet) ภายในมีตัวประมวลผลหรือซีพียู ARM Cortex-M4F หน่วยความจำ Flash 512 KB และ หน่วยความจำ SRAM 128 KB สามารถใช้ความถี่ในการทำงานของซีพียูได้สูงถึง 100MHz เนื่องจากบอร์ดนี้มีสีดำ จึงมีการตั้งชื่อหรือเรียกกันว่า STM32 Black Pill
ข้อสังเกต: ยังมีบอร์ด Black Pill STM32F401CCU6 ที่มีลักษณะเหมือนกันให้เลือกใช้งานได้ แต่มีขนาดของหน่วยความจำ Flash น้อยกว่า และใช้ความถี่ได้น้อยกว่า (84 MHz, 256 KB Flash, 64KB SRAM) ดังนั้นจึงมีราคาถูกว่า STM32F411CEU เล็กน้อย
บอร์ดนี้ใช้คอนเนกเตอร์ USB Type-C สำหรับป้อนแรงดันไฟเลี้ยงจาก USB (5V) และเชื่อมต่อกับคอมพิวเตอร์ได้ ไมโครคอนโทรลเลอร์ STM32F411CEU รองรับการใช้งานของ USB OTG FS/HS เช่น ในโหมด USB-CDC (Virtual Com Port), USB Mass Storage และ USB HID (Keyboard or Mouse)
การโปรแกรมไฟล์เฟิร์มแวร์ไปยังหน่วยความจำ Flash ภายใน สามารถทำได้โดยใช้ SWD (Serial wire debug) Interface แต่ต้องใช้ร่วมกับอุปกรณ์ภายนอก เช่น ST-Link USB Programmer/Debugger หรือ Segger J-Link เป็นต้น แต่เนื่องจากรองรับการทำงานในโหมด DFU Bootloader จึงสามารถโปรแกรมผ่าน USB ได้เช่นกัน
เนื่องจากบอร์ดมีราคาถูก และมีขนาดเล็ก (2.1" x 0.8") สามารถเสียบขาลงบนเบรดบอร์ดได้ จึงเหมาะสำหรับนำมาใช้ในการเรียนรู้หรือทำอุปกรณ์ต้นแบบ (Prototyping)
เมื่อเปรียบเทียบกับบอร์ด ST NUCLEO F411RE และบอร์ด STM32F411 Discovery Kit แม้ว่าจะใช้ชิป STM32F411RET6 หรือ STM32F411VET6 ที่มี ARM Cortex-M4F CPU เหมือนกัน มีขนาดหน่วยความจำเท่ากัน (512 KB Flash และ 128 KB SRAM) มีจำนวนขา I/O มากกว่า (มี 64 ขา) และมีวงจร ST-Link V2 รวมไว้บนบอร์ดแล้ว แต่ก็มีราคาสูงกว่าและมีขนาดใหญ่กว่า ไม่เหมาะสำหรับการนำมาต่อใช้งานบนเบรดบอร์ด
ในแง่ของการเขียนโปรแกรมสำหรับบอร์ดไมโครคอนโทรลเลอร์อันนี้ ก็ตัวเลือกที่น่าสนใจ เช่น
- Embedded C/C++ ร่วมกับซอฟต์แวร์ประเภท IDE และ GNU ARM Embedded Toolchain ที่หลากหลาย เช่น STM32 CubeIDE และ VS Code IDE + PlatformIO extension เป็นต้น
- Arduino IDE + Arduino Core for STM32 (STM32F411CE Generic Board)
- MicroPython หรือ CircuitPython (Adafruit)
การติดตั้ง MicroPython Firmware สำหรับ STM32
ก่อนอื่นเรามาเตรียมความพร้อมของอุปกรณ์ ฮาร์ดแวร์และซอฟต์แวร์ที่จำเป็น มีขั้นตอนดังนี้
- ดาวน์โหลดและติดตั้งไฟล์ MicroPython Firmware (.hex) จาก Github สำหรับบอร์ด WeAct STM32F411
Hex File: firmware_internal_rom_stm32f411_v1.12–35.hex - ดาวน์โหลดและติดตั้งโปรแกรม STM32 ST-Link Utility ถ้าใช้วิธีโปรแกรมผ่าน SWD (วิธีที่ 1) หรือโปรแกรม DfuSe ถ้าโปรแกรมด้วยวิธี DFU (วิธีที่ 2)
- เปิดใช้งาน Thonny Python IDE (Open Source) และเชื่อมต่อกับบอร์ด STM32 ทางพอร์ต USB (Virtual COM port)
การโปรแกรมด้วยวิธี SWD
ถ้ามีอุปกรณ์ ST-Link-V2 Programmer สำหรับการโปรแกรมด้วยวิธี SWD ก็สามารถใช้ไฟล์ .hex ได้เลย ในเครื่องคอมพิวเตอร์ (Windows) จะต้องมีการติดตั้งโปรแกรม STM32 ST-Link Utility ของบริษัท ST ก่อน สามารถดาวน์โหลดสำหรับการติดตั้ง (ไฟล์ .zip) ได้จากเว็บไซต์ของทางบริษัท (จะต้องรีจิสเตอร์ผู้ใช้ก่อนจึงจะดาวน์โหลดไฟล์ได้)
การเชื่อมต่อด้วยวิธี SWD จะใช้สายไฟเชื่อมต่อ 4 เส้น ระหว่างบอร์ด STM32 กับอุปกรณ์ ST-Link V2
STM32 (SWD) | ST-Link V2
GND <----> GND
SCK <----> SWCLK
DIO <----> SWDIO
3V3 <----> 3.3V
เมื่อเชื่อมต่อกับบอร์ด STM32 ผ่านทาง ST-Link V2 มายังพอร์ต USB ของคอมพิวเตอร์แล้ว (ยังไม่จำเป็นต้องเสียบสาย USB-C กับบอร์ด Black Pill) ให้เปิดโปรแกรม STM32 ST-Link Utility จากนั้นไปที่เมนู Target > Connect ถ้าสามารถเชื่อมต่อได้ จะปรากฏข้อความระบุ Device ที่ตรวจพบ (ดูรูปตัวอย่างประกอบ)
ถัดไปให้เปิดไฟล์ .hex โดยทำคำสั่งจากเมนู File > Open File แล้วเลือกไฟล์ .hex ที่ต้องการจะโปรแกรมไปยังบอร์ดไมโครคอนโทรลเลอร์ จากนั้นทำขั้นตอน Target > Program & Verify (หรืออาจทำขั้นตอน Erase Chip ก่อนก็ได้ เพื่อเคลียร์หน่วยความจำ Flash ทั้งหมด)
การโปรแกรมด้วยวิธี DFU
บนหน้าเว็บไซต์ของบริษัท ST สามารถดาวน์โหลดโปรแกรมชื่อ DfuSe (for Windows) สำหรับการอัปโหลดไฟล์เฟิร์มแวร์ด้วยวิธี DFU (USB device firmware upgrade) ไฟล์สำหรับการติดตั้งเป็นไฟล์ .zip ให้แตกไฟล์ แล้วเรียกโปรแกรมเพื่อทำขั้นตอนการติดตั้งตามปรกติ
ถัดไปให้เตรียมอุปกรณ์ฮาร์ดแวร์ดังนี้
- เชื่อมต่อขา A10 (PA10/USB_FS_ID) ด้วยสาย Jumper Wire กับตัวต้านทาน 10k แบบ Pullup ไปยัง 3.3V
- เสียบสาย USB-C เชื่อมต่อบอร์ดไมโครคอนโทรลเลอร์กับคอมพิวเตอร์
- กดปุ่ม BOOT0 กดค้างไว้ กดปุ่ม RESET แล้วจึงปล่อยปุ่ม RESET และ BOOT0 ตามลำดับ
การแปลงไฟล์ .hex เป็น .dfu เราจะใช้โปรแกรมชื่อ DFU File Manager ของ DfuSe กดปุ่ม S19 or Hex แล้วเลือกไฟล์ .hex แล้วกดปุ่ม Generate
ถัดไปให้เปิดโปรแกรม DfuSe Demo ถ้าเชื่อมต่อบอร์ด STM32 แล้วอยู่ในโหมด DFU จะมองเห็น Vendor ID: 0483, Product ID: DF11 และ Version: 2200
กดปุ่ม Choose… ในส่วนของ Upgrade or Verify Action เลือกไฟล์ .dfu แล้วกดปุ่ม Upgrade เพื่อทำขั้นตอนสุดท้าย
เพิ่มเติม: การใช้ซอฟต์แวร์ STM32CubeProgrammer
เราอาจใช้ซอฟต์แวร์ STM32CubeProgrammer ซึ่งรองรับการใช้งานทั้ง SWD และ USB (DFU) ได้ด้วย สามารถใช้ไฟล์ .hex ได้เลย ไม่ต้องแปลงเป็น .dfu
การใช้งาน Thonny IDE
เมื่อเปิดโปรแกรมแล้ว ให้ทำเมนูคำสั่ง Run > Select Interpreter เลือก MicroPython (Generic) และเลือกพอร์ตที่กำลังเชื่อมต่อกับบอร์ด
ถ้าต้องการเขียนโค้ด ก็ให้สร้างไฟล์ใหม่ แล้วบันทึกลงไฟล์ ซึ่งมีสองตัวเลือก คือ เก็บลงในคอมพิวเตอร์ของผู้ใช้ หรือเก็บลงใน Flash Drive ของบอร์ด Micropython-STM32
ถ้าต้องการรันโค้ด ก็ให้กดปุ่ม Run ถ้าจะหยุดการทำงานของโค้ดที่กำลังรันอยู่ ก็ให้ปุ่ม Ctrl+C หรือถ้าจะรีเซตบอร์ด (soft reboot) ก็ให้กดปุ่ม Ctrl+D ในส่วนรับคำสั่งของ Shell
ตัวอย่างโค้ด: LED Blink (I/O Polling)
ตัวอย่างแรกสาธิตการใช้คำสั่งจากคลาส pyb.Switch
และ pyb.LED
สำหรับปุ่มกด (Push Button, KEY Switch) และ LED (Blue) ที่มีอยู่บนบอร์ด
หลักการทำงานคือ ให้มีการทำให้ LED กระพริบด้วยอัตราคงที่ (ให้สลับสถานะลอจิกทุก ๆ 500 มิลลิวินาที) และทำขั้นตอนตรวจสอบค่า อินพุตจากปุ่มกด ถ้าอ่านค่าได้ True
หมายถึง มีการกดปุ่มค้างไว้ในขณะนั้น ให้จบการทำงานของโปรแกรม
import utime as time
import pybsw = 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
delta = time.ticks_diff( now, last_time )
if delta >= 500:
led.toggle() # toggle LED
last_time = now # update timestamp
except KeyboardInterrupt:
pass
finally:
led.off() # turn off LED
print('Done')
ตัวอย่างโค้ด: LED Toggle (Event-Triggered)
ตัวอย่างที่ 2 มีหลักการทำงานของโค้ดดังนี้ ถ้ากดปุ่มแล้วปล่อย จะทำให้เกิดการสลับสถานะลอจิกของ LED หนึ่งครั้ง โดยมีการตรวจสอบสถานะของปุ่มกดโดยอัตโนมัติและมีการกำหนดฟังก์ชันสำหรับ Callback ซึ่งจะถูกเรียกให้ทำงาน เมื่อมีเหตุการณ์ที่เกิดขึ้น (Event-triggered) โดยการกดปุ่มแล้วปล่อยในแต่ละครั้ง ในกรณีนี้คือ จะส่งผลทำให้ LED สลับสถานะ
แต่ถ้ากดปุ่มค้างไว้สัก 2–3 วินาที จะทำให้จบการทำงานของโปรแกรม หรือถ้ารันโค้ดผ่าน REPL และกดปุ่ม Ctrl+C จะทำให้จบการทำงานของโปรแกรมเช่นกัน
# file: switch_led_demo-1.py
# Micropython-STM31F411CE Black Pill import pyb
import utime as timesw = 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
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')
ตัวอย่างโค้ด: LED Blink using Hardware Timer
ตัวอย่างถัดไปสาธิตการใช้งาน Hardware Timer ของ STM32F4 จากคลาส pyb.Timer
โดยสามารถเลือกใช้ Timer จากหมายเลข TIM1 .. TIM11 ที่มีขนาด 16 บิต (ยกเว้น TIM2 และ TIM5 ที่มีขนาด 32 บิต) และมีตัวหารความถี่ขนาด 16 บิต
ในตัวอย่างนี้ ความถี่ของการตัวนับ (เลือกโหมดนับขึ้น pyb.Timer.UP
) ได้ถูกตั้งค่าให้เท่ากับ 10 Hz มีการกำหนดฟังก์ชันให้ทำงานที่เรียกว่า Callback Function เมื่อนับได้ครบหนึ่งรอบหรือหนึ่งคาบ จะเรียกฟังก์ชันดังกล่าวให้ทำงานโดยอัตโนมัติ และในกรณีคือ ทำให้ LED สลับสถานะลอจิกหนึ่งครั้ง ดังนั้นเราจะเห็น LED กระพริบที่อัตราคงที่
# File: hw_timer_demo-1.py
# Micropython-STM31F411CE Black Pillimport utime as time
from machine import Pin
import pybled = 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')
ตัวอย่างโค้ด: LED Blink using Software Timer in Periodic Mode
ตัวอย่างถัดไปสาธิตการใช้งาน Software Timer (หรือเรียกว่า Virtual Timer สำหรับ MicroPython) จากคลาส machine.Timer
โดยใช้ FreeRTOS เป็นตัวจัดการเชิงเวลา และจะต้องกำหนดหมายเลข Timer ID ให้เท่ากับ -1
ในตัวอย่างนี้ได้ตั้งค่าให้มีคาบเท่ากับ 500 มิลลิวินาที เมื่อตัวนับเริ่มนับขึ้นจาก 0 จนครบเวลาหนึ่งคาบของการนับ (Count Overflow Event) จะมีการเรียกฟังก์ชันสำหรับ Callback และจะเกิดซ้ำไปเรื่อย ๆ (Periodic Mode)
# File: sw_timer_demo-1.py
# Micropython-STM31F411CE Black Pillimport pyb
from machine import Timer
import utime as timesw = 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')
ตัวอย่างโค้ด: 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 ครั้ง จากนั้นจึงจบการทำงานของโปรแกรม
# File: sw_timer_demo-2.py
# Micropython-STM31F411CE Black Pillimport pyb
from machine import Timer
import utime as timedone = 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 = Truetim = Timer(-1) # create a virtual timer
sw = pyb.Switch() # use onboard button
sw.callback( start_timer ) # set callback for buttontry:
while not done:
pass # do nothing in the main loop
except KeyboardInterrupt:
pass
finally:
print('Done')
ตัวอย่างโค้ด: LED Blink using Hardware Timer in PWM mode
ตัวอย่างถัดไปสาธิตการเปิดใช้งาน Hardware Timer ของ STM32F4 จากคลาส pyb.Timer
เพื่อสร้างสัญญาณ PWM (Pulse Width Modulation) ที่จะต้องกำหนดความถี่ (Frequency, Hz) และความกว้างของพัลส์ (Pulse Width) ในหน่วยเป็นไมโครวินาที สำหรับช่วงที่ลอจิกของสัญญาณเป็น High
การใช้งาน 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()
จะให้ตัวเลขเป็นคาบเวลาของตัวนับ
# File: hw_timer_pwm_demo-1.py
# Micropython,-STM31F411CE Black Pillimport 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 cycleprint( '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
จากตัวเลขเหล่านี้ เราสามารถระบุความสัมพันธ์ได้ดังนี้
PWM freq.(MHz) = TIM4 freq.(MHz) /( (Prescaler+1)*(Period+1) )
5 Hz = 48 MHz /( (624+1)*(15359+1) )
ข้อสังเกต: วงจร 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
ตัวอย่างโค้ด: Dual-LED Blink using Hardware Timer in Output-Compare (OC) mode
ตัวอย่างนี้ สาธิตการทำให้ 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 Hzhalf_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')
ตัวอย่างโค้ด: LED Fading using Hardware Timer in PWM Mode
ตัวอย่างถัดไป สาธิตการใช้ Hardware Timer จากคลาส pyb.Timer
ในโหมด PWM (เลือกใช้ TIM4 ช่องสัญญาณ CH3) ตั้งค่าความถี่ให้เท่ากับ 1000 Hz และปรับค่า Duty Cycle ให้เปลี่ยนแปลงได้โดยใช้ค่าตัวเลขที่คำนวณเก็บไว้ในอาร์เรย์ เพื่อใช้กำหนดความกว้างของพัลส์
การตั้งค่าความถี่ในตัวอย่างนี้ เราไม่ได้กำหนดค่าโดยตรง แต่ใช้อีกวิธีหนึ่งคือ กำหนดค่า Prescaler ให้เท่ากับ 47 และค่า Period ให้เท่ากับ 999
PWM freq.(MHz) = TIM4 freq.(MHz) /( (Prescaler+1)*(Period+1) )
1000 Hz = 48 MHz /( (47+1)*(999+1) )
สัญญาณ PWM ที่ได้ (ขา PB9) จะถูกนำไปใช้ขับวงจร LED และจะเห็นได้ว่า ความสว่างของ LED เปลี่ยนแปลงตามค่า Duty Cycle ของสัญญาณ
# timer_pwm_led_fading.py
# Micropython, STM31F411CE Black Pill boardimport 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_CH4pwm = 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()
print('Done')
ตัวอย่างโค้ด: Multi-LED Blink using Software Timers
ตัวอย่างนี้สาธิตการทำงาน 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 pybLED_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 )
breakfor 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')
ตัวอย่างโค้ด: Button Click Counter using External Interrupt
ตัวอย่างนี้สาธิตการเขียนโค้ดเพื่อตรวจสอบการกดปุ่มภายนอก โดยใช้หลักการทำงานของไมโครคอนโทรลเลอร์ที่เรียกว่า อินเทอร์รัพท์สำหรับขา GPIO (หรือเรียกที่ว่า External Interrupt) และใช้คลาส pyb.ExtInt
มีการจำแนกเหตุการณ์ได้เป็น 3 กรณีคือ ขอบขาขึ้น (Rising Edge) ขอบขาลง (Falling Edge) และทั้งขอบขาขึ้นและขาลง
ในตัวอย่างนี้ เราเลือกใช้ขอบขาลง และใช้ขา PA0 ที่ต่อกับวงจรปุ่มกดภายนอก (ทำงานแบบ Active-Low) เป็นอินพุต ทุกครั้งที่เกิดเหตุการณ์ขอบขาลงที่สัญญาณอินพุต จะมีการเรียกฟังก์ชันสำหรับ Callback ซึ่งจะทำให้ตัวแปร clicked
มีค่าเป็น True
และปิดการทำงานของอินเทอร์รัพท์ดังกล่าวชั่วคราว
ค่าของตัวแปร clicked
จะถูกตรวจสอบใน main loop ถ้ามีค่าเป็นจริง ก็ให้เพิ่มค่าของตัวนับและแสดงข้อความเอาต์พุต จากนั้นกำหนดให้ค่าตัวแปร clicked
เป็น False
และเปิดการทำงานของอินเทอร์รัพท์อีกครั้ง
ข้อสังเกต: การต่อวงจรปุ่มกด เมื่อกดปุ่มแล้วปล่อย อาจเกิดการกระเด้งของปุ่ม (Bouncing) ทำให้เกิดขอบขาขึ้นหรือขาลง ที่สัญญาณอินพุตมากกว่าหนึ่งครั้งได้ วิธีแก้ไขปัญหานี้อย่างง่ายในเบื้องต้นคือ เราสามารถเลือกใช้ตัวเก็บประจุ เช่น 0.1uF มาต่อคร่อมที่ขาสัญญาณกับ GND
import utime as time
from machine import Pin, Timer
import pybclicked = False # global variabledef ext_int_cb(irq_line):
global ext_irq, clicked
ext_irq.disable() # disable interrupt temporarily
clicked = True # set flagbtn_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')
ตัวอย่างโค้ด: 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 constPOS_MAX = const(100)
POS_MIN = const(0)# global variables
cnt = 0
pos = 0def 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//4def 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//4a_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')
ตัวอย่างโค้ด: 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 constargs = {'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 countertry:
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()
print('Done')
ตัวอย่างโค้ด: 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 ไมโครวินาที)
ในตัวอย่างนี้ เราต้องการจะวัดความกว้างของพัลส์ช่วงที่เป็น High ในหน่วยเป็นไมโครวินาที ทุก ๆ ครั้งที่เกิดขอบขาขึ้น หรือขาลง เราจะอ่านค่าตัวนับที่ถูกบันทึกไว้ตอนเกิดเหตุการณ์ดังกล่าว และนำมาคำนวณหาผลต่างซึ่งจะได้เป็นความกว้างของสัญญาณพัลส์
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 = 0def 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 on timers
timer2.deinit()
timer3.deinit()
print('Done')
โดยสรุป เราได้เห็นตัวอย่างการเขียนโค้ด MicroPython สำหรับ STM32 โดยใช้บอร์ด STM32F411CEU Black Pill และจะเห็นได้ว่า แม้ว่าจะเขียนภาษา Python ซึ่งเป็นภาษาคอมพิวเตอร์ระดับสูงกว่า C/C++ เราก็สามารถเรียนรู้หลักการทำงานของฮาร์ดแวร์ เช่น ไมโครคอนโทรลเลอร์ได้เช่นกัน