MicroPython for STM32F411 Black Pill: Embedded Programming Style

<rawat.s>
14 min readMay 13, 2020

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍

ในบทความนี้ เราลองมาศึกษาตัวอย่างวิธีการเขียนโค้ด 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 รวมไว้บนบอร์ดแล้ว แต่ก็มีราคาสูงกว่าและมีขนาดใหญ่กว่า ไม่เหมาะสำหรับการนำมาต่อใช้งานบนเบรดบอร์ด

รูปบอร์ด WeAct STM32F411CEU Black Pill มุมมองด้านบน และด้านล่าง (Top / Bottom View)
แผนผังแสดงตำแหน่งขาต่าง ๆ ของบอร์ด STM32F411 Black Pill (Pinout PDF File)
แผนผังแสดงโครงสร้างภายใน (Block Diagram) ของ STM32F411CE

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

การติดตั้ง 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
ตัวอย่างอุปกรณ์ ST-Link V2
ตัวอย่างอุปกรณ์: การเชื่อมต่อระหว่างบอร์ด STM32 และ ST-Link USB Dongle

เมื่อเชื่อมต่อกับบอร์ด STM32 ผ่านทาง ST-Link V2 มายังพอร์ต USB ของคอมพิวเตอร์แล้ว (ยังไม่จำเป็นต้องเสียบสาย USB-C กับบอร์ด Black Pill) ให้เปิดโปรแกรม STM32 ST-Link Utility จากนั้นไปที่เมนู Target > Connect ถ้าสามารถเชื่อมต่อได้ จะปรากฏข้อความระบุ Device ที่ตรวจพบ (ดูรูปตัวอย่างประกอบ)

ตัวอย่างข้อความที่ปรากฏเมื่อเชื่อมต่อกับบอร์ด STM32F411CEU6 ได้

ถัดไปให้เปิดไฟล์ .hex โดยทำคำสั่งจากเมนู File > Open File แล้วเลือกไฟล์ .hex ที่ต้องการจะโปรแกรมไปยังบอร์ดไมโครคอนโทรลเลอร์ จากนั้นทำขั้นตอน Target > Program & Verify (หรืออาจทำขั้นตอน Erase Chip ก่อนก็ได้ เพื่อเคลียร์หน่วยความจำ Flash ทั้งหมด)

หลังจากเปิดไฟล์ .hex
เมื่อทำขั้นตอน Program & Verify ได้สำเร็จแล้ว

การโปรแกรมด้วยวิธี 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

เมื่อมองเห็นอุปกรณ์ในโหมด DFU

กดปุ่ม Choose… ในส่วนของ Upgrade or Verify Action เลือกไฟล์ .dfu แล้วกดปุ่ม Upgrade เพื่อทำขั้นตอนสุดท้าย

เมื่เปิดไฟล์ .dfu เพื่อโหลดข้อมูล
ขณะทำขั้นตอน Upgrade (เขียนข้อมูลไปยังหน่วยความจำของ MCU)

เพิ่มเติม: การใช้ซอฟต์แวร์ STM32CubeProgrammer

‍‍‍‍‍‍ ‍‍ ‍‍‍‍‍‍

เราอาจใช้ซอฟต์แวร์ STM32CubeProgrammer ซึ่งรองรับการใช้งานทั้ง SWD และ USB (DFU) ได้ด้วย สามารถใช้ไฟล์ .hex ได้เลย ไม่ต้องแปลงเป็น .dfu

ตัวอย่างการใช้ STM32CubeProgrammer อัปโหลดโปรแกรม .hex ในโหมด DFU USB

การใช้งาน Thonny IDE

เมื่อเปิดโปรแกรมแล้ว ให้ทำเมนูคำสั่ง Run > Select Interpreter เลือก MicroPython (Generic) และเลือกพอร์ตที่กำลังเชื่อมต่อกับบอร์ด

เลือก Python Interpreter: MicroPython (Generic)
เมื่อ Thonny IDE สามาารถเชื่อมต่อกับบอร์ดและพร้อมใช้งาน MicroPython ได้แล้ว

ถ้าต้องการเขียนโค้ด ก็ให้สร้างไฟล์ใหม่ แล้วบันทึกลงไฟล์ ซึ่งมีสองตัวเลือก คือ เก็บลงในคอมพิวเตอร์ของผู้ใช้ หรือเก็บลงใน 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 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
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 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
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 Pill
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')

ตัวอย่างโค้ด: 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 Pill
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')

ตัวอย่างโค้ด: 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 Pill
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')

ตัวอย่างโค้ด: 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 Pill
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

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

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 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')

ตัวอย่างโค้ด: 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 board
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()
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 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')
ตัวอย่างการต่อวงจรทดลอง: RGB LED

ตัวอย่างโค้ด: 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 pyb
clicked = False # global variabledef 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')
ตัวอย่างการต่อวงจรทดลอง: Push Button

ตัวอย่างโค้ด: 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
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')
ตัวอย่างการต่อวงจรทดลอง: Rotary Encoder Module

ตัวอย่างโค้ด: 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 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 = 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 on timers
timer2.deinit()
timer3.deinit()
print('Done')

โดยสรุป เราได้เห็นตัวอย่างการเขียนโค้ด MicroPython สำหรับ STM32 โดยใช้บอร์ด STM32F411CEU Black Pill และจะเห็นได้ว่า แม้ว่าจะเขียนภาษา Python ซึ่งเป็นภาษาคอมพิวเตอร์ระดับสูงกว่า C/C++ เราก็สามารถเรียนรู้หลักการทำงานของฮาร์ดแวร์ เช่น ไมโครคอนโทรลเลอร์ได้เช่นกัน

--

--