[TH] ulab v3.0

จากบทความ ulab ก่อนหน้านี้จะพบว่า Micropython สามารถใช้งานคำสั่งเกี่ยวกับการประมวลผลชุดข้อมูลเหมือนกับใช้ใน Numpy ได้ผ่านทางไลบรารี ulab ซึ่งก่อนหน้านี้ทีมผู้เขียนใช้งานรุ่น 0.54.0 ซึ่งเก่ากว่ารุ่นปัจจุบัน คือ 3.0.1 ทำให้เกิดบทความนี้ขึ้นมา โดยบทความกล่าวถึงวิธีการสร้าง Micropython ที่ผนวกไลบรารี ulab เข้าไป และใช้งานกับ esp32 รุ่นที่มี SPIRAM

ภาพที่ 1 รายการโมดูลของ ulab

ulab3

จากภาพที่ 1 จะพบว่า โครงสร้างไลบรารีของ ulab เปลี่ยนแปลงไปจากเดิม ทำให้การเขียนโปรแกรมจากตัวอย่างก่อนหน้านี้ต้องมีการปรับเปลี่ยน ซึ่งภายใต้ ulab จะมีไลบรารีของ numpy และ scipy เข้ามา ซึ่งรายละเอียดของ numpy ที่รองรับเป็นดังนี้

object <module 'numpy'> is of type module
  __name__ -- numpy
  ndarray -- <class 'ndarray'>
  array -- <function>
  frombuffer -- <function>
  e -- 2.718282
  inf -- inf
  nan -- nan
  pi -- 3.141593
  bool -- 63
  uint8 -- 66
  int8 -- 98
  uint16 -- 72
  int16 -- 104
  float -- 102
  fft -- <module 'fft'>
  linalg -- <module 'linalg'>
  set_printoptions -- <function>
  get_printoptions -- <function>
  ndinfo -- <function>
  arange -- <function>
  concatenate -- <function>
  diag -- <function>
  empty -- <function>
  eye -- <function>
  interp -- <function>
  trapz -- <function>
  full -- <function>
  linspace -- <function>
  logspace -- <function>
  ones -- <function>
  zeros -- <function>
  clip -- <function>
  equal -- <function>
  not_equal -- <function>
  isfinite -- <function>
  isinf -- <function>
  maximum -- <function>
  minimum -- <function>
  where -- <function>
  convolve -- <function>
  all -- <function>
  any -- <function>
  argmax -- <function>
  argmin -- <function>
  argsort -- <function>
  cross -- <function>
  diff -- <function>
  dot -- <function>
  trace -- <function>
  flip -- <function>
  max -- <function>
  mean -- <function>
  median -- <function>
  min -- <function>
  roll -- <function>
  sort -- <function>
  std -- <function>
  sum -- <function>
  polyfit -- <function>
  polyval -- <function>
  acos -- <function>
  acosh -- <function>
  arctan2 -- <function>
  around -- <function>
  asin -- <function>
  asinh -- <function>
  atan -- <function>
  atanh -- <function>
  ceil -- <function>
  cos -- <function>
  cosh -- <function>
  degrees -- <function>
  exp -- <function>
  expm1 -- <function>
  floor -- <function>
  log -- <function>
  log10 -- <function>
  log2 -- <function>
  radians -- <function>
  sin -- <function>
  sinh -- <function>
  sqrt -- <function>
  tan -- <function>
  tanh -- <function>
  vectorize -- <function>

ภายใต้ numpy มีโมดูล fft ให้ใช้งาน ซึ่งโมดูลลนี้มีฟังก์ชันสำหรับใช้งานได้แก่

object <module 'fft'> is of type module
  __name__ -- fft
  fft -- <function>
  ifft -- <function>

และภายใต้โมดูล linalg ซึ่งทำงานเกี่ยวกับ linear algebra มีฟังก์ชันต่อไปนี้ให้ใช้งาน

object <module 'linalg'> is of type module
  __name__ -- linalg
  cholesky -- <function>
  det -- <function>
  eig -- <function>
  inv -- <function>
  norm -- <function>

การทำงานของ scipy ที่รองรับคือ

object <module 'scipy'> is of type module
  __name__ -- scipy
  linalg -- <module 'linalg'>
  optimize -- <module 'optimize'>
  signal -- <module 'signal'>
  special -- <module 'special'>

คำสั่งของโมดูล linalg ที่รองรับคือ

object <module 'linalg'> is of type module
  __name__ -- linalg
  solve_triangular -- <function>
  cho_solve -- <function>

คำสั่งในโมดูล optimize คือ

object <module 'optimize'> is of type module
  __name__ -- optimize
  bisect -- <function>
  fmin -- <function>
  newton -- <function>

คำสั่งภายใต้โมดูล signal ได้แก่

object <module 'signal'> is of type module
  __name__ -- signal
  spectrogram -- <function>
  sosfilt -- <function>

คำสั่งภายใต้โมดูล special คือ

object <module 'special'> is of type module
  __name__ -- special
  erf -- <function>
  erfc -- <function>
  gamma -- <function>
  gammaln -- <function>

คำสั่งในกลุ่ม utisl ของ ulab ได้แก่

object <module 'utils'> is of type module
  __name__ -- utils
  from_int16_buffer -- <function>
  from_uint16_buffer -- <function>
  from_int32_buffer -- <function>
  from_uint32_buffer -- <function>

สร้างไลบรารี ulab

การสร้างไลบรารี ulab ให้รวมในตัว Micropython จะต้องมีเครื่องมือ esp-idf ที่ได้กล่าวไปก่อนหน้านี้ ขั้นตอนการสร้างไลบรารี ulab กับบอร์ด esp32 ที่มี SPIRAM และเมื่อสั่งแสดงผลตามโค้ดต่อไปนี้จะได้ผลลัพธ์ดังภาพที่ 2

import sys
if sys.platform != 'esp32':
    print("esp32 only!")
    sys.exit(0)
    
import gc
import os
import esp
import esp32
import time
import machine as mc
import ulab

def show_hw_info():
    uname = os.uname()
    mem_total = gc.mem_alloc()+gc.mem_free()
    free_percent = "("+str((gc.mem_free())/mem_total*100.0)+"%)"
    alloc_percent = "("+str((gc.mem_alloc())/mem_total*100.0)+"%)"
    stat = os.statvfs('/flash')
    block_size = stat[0]
    total_blocks = stat[2]
    free_blocks  = stat[3]
    rom_total = (total_blocks * block_size)/1024
    rom_free = (free_blocks * block_size)/1024
    rom_usage = (rom_total-rom_free)
    rfree_percent = "("+str(rom_free/rom_total*100.0)+"%)"
    rusage_percent = "("+str(rom_usage/rom_total*100.0)+"%)"
    print("ID ............:",mc.unique_id())
    print("Platform ......:",sys.platform)
    print("Version .......:",sys.version)
    print("Memory")
    print("   total ......:",mem_total/1024,"KB")
    print("   usage ......:",gc.mem_alloc()/1024,"KB",alloc_percent)
    print("   free .......:",gc.mem_free()/1024,"KB",free_percent)
    print("ROM")
    print("   total ......:", rom_total,"KB" )
    print("   usage ......:", rom_usage,"KB",rfree_percent )
    print("   Free .......:", rom_free,"KB",rusage_percent )
    print("system name ...:",uname.sysname)
    print("node name .....:",uname.nodename)
    print("release .......:",uname.release)
    print("version .......:",uname.version)
    print("machine .......:",uname.machine)

def show_ulab():
    print("ulab version {}.".format(ulab.__version__))

if __name__=="__main__":
    show_hw_info()
    show_ulab()
ภาพที่ 2 ตัวอย่างการเรียกดูรายละเอียดของ Micropython ที่ผนวกกับ ulab v.3.0.1

ดาวน์โหลด source code

สิ่งแรกที่ต้องทำคือดาวน์โหลดโคดต้นฉบับของ ulab และ Micropython แล้วคอมไพล์ mpy-cross และเตรียมการโมดูลย่อยของ esp32 ก่อน โดยสคริปต์คำสั่งเป็นดังนี้

mkdir ~/src
cd ~/src
git clone https://github.com/v923z/micropython-ulab.git ulab
git clone https://github.com/micropython/micropython.git
cd micropython
git submodule update --init
cd mpy-cross
make
cd ../ports/esp32
make submodules

สร้างข้อมูลพาร์ทิชัน

ให้สร้างไฟล์ partitions_ulab.csv โดยใช้โปรแกรม nano และเก็บไว้ใน ~/src/micropython/ports/esp32 ด้วยการพิมพ์คำสั่งดังนี้

cd ~/src/micropython/ports/esp32
nano partitions_ulab.csv

ให้พิมพ์โค้ดต่อไปนี้ลงไป และบันทึกด้วย Ctrl+O และออกจาก nano ด้วย Ctrl+X

# Notes: the offset of the partition table itself is set in
# $ESPIDF/components/partition_table/Kconfig.projbuild and the
# offset of the factory/ota_0 partition is set in makeimg.py
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 0x200000,
vfs,      data, fat,     0x220000, 0x180000,

แก้ไข sdkconfig

เมื่อได้ไฟล์ต้นแบบของพาร์ทิชัน ขั้นตอนถัดไปคือทำการแก้ไขไฟล์ sdkconfig สำหรับบอร์ดที่ต้องใช้งาน ซึ่งจัดเก็บอยู่ในโฟลเดอร์ ~/src/micropython/ports/esp32/boards ประกอบกับบอร์ดที่ทีมงานเราเลือกใช้เป็นบอร์ดที่มี SPIRAM จึงต้องแก้ไขไฟล์ sdkconfig.spiram โดยเพิ่ม 2 บรรทัดต่อไปนี้ลงไป แต่ถ้าใช้กับบอร์ดที่ไม่มี SPIRAM ให้แก้ไขในไฟล์ sdkconfig.base

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_ulab.csv"

คอมไพล์

การคอมไพล์ต้องกำหนด 2 สิ่ง คือ

  • ประเภทของบอร์ด ซึ่งในที่นี้เลือกใช้ GENERIC_SPIRAM เนื่องจากบอร์ด esp32 ที่ใช้นั้นมีหน่วยความแรมเพิ่มจากรุ่นปกติ แต่ถ้าใช้กับรุ่นปกติให้เปลี่ยนเป็น GENERIC ซึ่งสามารถคอมไพล์ได้เหมือนกัน
  • โมดูลเสริมการทำงานที่เขียนจากภาษา c หรือ ulab

คำสั่งเป็นดังนี้ และเมื่อเริ่มมีการคอมไพล์จะเป็นดังตัวอย่างภาพที่ 4 และเมื่อเสร็จกระบวนการคอมไพล์ (และไม่มีข้อผิดพลาด) จะแสดงผลตามตัวอย่างในภาพที่ 5

cd ~/src/micropython/ports/esp32
make BOARD=GENERIC_SPIRAM USER_C_MODULES=~/src/ulab/code/micropython.cmake 
ภาพที่ 4 ขณะคอมไพล์
ภาพที่ 5 ผลจากการคอมไพล์

ติดตั้ง

ขั้นตอนสุดท้ายคือทำการติดตั้งลงบอร์ด โดยทีมงานเราได้เชื่อมต่อบอร์ดเข้ากับเครื่องเรียบร้อย จึงสั่งลบและเขียนเฟิร์มแวร์จากบรรทัดคำสั่งโดยสั่งงานดังนี้ ซึ่งถ้าใช้กับบอร์ด esp32 ที่ไม่มี SPIRAM ให้เลือก BOARD เป็น GENERIC โดยกระบวนการเขียนลงบอร์ดจะเป็นตามตัวอย่างในภาพที่ 6 และ 7

make erase
make deploy BOARD=GENERIC_SPIRAM
ภาพที่ 6 เริ่ม deploy
ภาพที่ 7 ผลลัพธ์จากการ deploy

หรือผู้อ่านใช้โปรแกรม thonny เขียนไฟล์ ~/src/micropython/ports/esp32/build-GENERIC_SPIRAM/micropython.bin แทนการสั่งด้วยบรรทัดคำสั่งได้เช่นกัน

ทดสอบ

ทดสอบด้วยการปรับแก้ code18-1.py ให้ใช้กับ ulab ตัวใหม่ และผลลัพธ์เป็นดังภาพที่ 8

ภาพที่ 8 ผลลัพธ์จาก code18-1a.py

หมุนสี่เหลี่ยม

จากตัวอย่างโปรแกรมในบทความ ESP32 : Display of rotation squares with application ulab. เป็นการประยุกต์ใช้ ulab ในการคำนวณการหมุนของสี่เหลี่ยม เนื่องจาก ulab 3 มีความเปลี่ยนแปลงไปจากเดิม ทำให้ได้โค้ดตัวอย่างสำหรับ ulab ในรุ่นนี้เป็นดังนี้

import gc
import os
import sys
import ulab
import time
import math
import machine as mc
from ulab import numpy as np
from st7735 import TFT
from sysfont import sysfont
from machine import SPI,Pin

gc.enable()
gc.collect()
mc.freq(240000000)

minX = -10.0
maxX = 10.0
minY = -5.0
maxY = 5.0
scrWidth = const(160)
scrHeight = const(80)
ratioX = float(scrWidth)/(math.fabs(minX)+math.fabs(maxX)+1)
ratioY = float(scrHeight)/(math.fabs(minY)+math.fabs(maxY)+1)
centerX = const(scrWidth >> 1)
centerY = const(scrHeight >> 1)

spi = SPI(2, baudrate=27000000,
          sck=Pin(14), mosi=Pin(12),
          polarity=0, phase=0)
# dc, rst, cs
tft=TFT(spi,15,13,2)
tft.init_7735(tft.GREENTAB80x160)
tft.fill(TFT.BLACK)

def rotate(pX,pY,angle):
    rad = math.radians(angle)
    xCos = pX*np.cos(rad)
    ySin = pY*np.sin(rad)
    xSin = pX*np.sin(rad)
    yCos = pY*np.cos(rad)
    newX = xCos - ySin
    newY = xSin + yCos
    return (newX, newY)

def draw(pX, pY,aColor=tft.WHITE):
    newPx = np.array(pX*ratioX+centerX,dtype=np.uint16)
    newPy = np.array(pY*ratioY+centerY,dtype=np.uint16)
    tft.line((newPx[0],newPy[0]),(newPx[1],newPy[1]),aColor)
    tft.line((newPx[1],newPy[1]),(newPx[2],newPy[2]),aColor)
    tft.line((newPx[2],newPy[2]),(newPx[3],newPy[3]),aColor)
    tft.line((newPx[3],newPy[3]),(newPx[0],newPy[0]),aColor)

def show_hw_info():
    uname = os.uname()
    mem_total = gc.mem_alloc()+gc.mem_free()
    free_percent = "("+str((gc.mem_free())/mem_total*100.0)+"%)"
    alloc_percent = "("+str((gc.mem_alloc())/mem_total*100.0)+"%)"
    stat = os.statvfs('/flash')
    block_size = stat[0]
    total_blocks = stat[2]
    free_blocks  = stat[3]
    rom_total = (total_blocks * block_size)/1024
    rom_free = (free_blocks * block_size)/1024
    rom_usage = (rom_total-rom_free)
    rfree_percent = "("+str(rom_free/rom_total*100.0)+"%)"
    rusage_percent = "("+str(rom_usage/rom_total*100.0)+"%)"
    print("ID ............:",mc.unique_id())
    print("Platform ......:",sys.platform)
    print("Version .......:",sys.version)
    print("Memory")
    print("   total ......:",mem_total/1024,"KB")
    print("   usage ......:",gc.mem_alloc()/1024,"KB",alloc_percent)
    print("   free .......:",gc.mem_free()/1024,"KB",free_percent)
    print("ROM")
    print("   total ......:", rom_total,"KB" )
    print("   usage ......:", rom_usage,"KB",rusage_percent )
    print("   Free .......:", rom_free,"KB",rfree_percent )
    print("system name ...:",uname.sysname)
    print("node name .....:",uname.nodename)
    print("release .......:",uname.release)
    print("version .......:",uname.version)
    print("machine .......:",uname.machine)

def show_ulab():
    print("ulab version {}.".format(ulab.__version__))
    
# main program
if __name__=="__main__":
    show_hw_info()
    show_ulab()
    tft.rotation(1)
    tft.fill(tft.BLACK)
    t0 = time.ticks_us()
    pX = np.array([-2,2,2,-2],dtype=np.float)
    pY = np.array([2,2,-2,-2],dtype=np.float)
    for degree in range(360):
        newP = rotate(pX,pY,degree)
        draw(newP[0],newP[1],tft.WHITE)
        #time.sleep_ms(100)
        tft.fill(0)
    for degree in range(360):
        newP = rotate(pX,pY,-degree)
        draw(newP[0],newP[1],tft.CYAN)
        #time.sleep_ms(100)
        tft.fill(0)
    print("ulab: Delta = {} usec".format(time.ticks_us()-t0))

    # endof program
    time.sleep_ms(2000)
    tft.on(False)
    spi.deinit()

สรุป

จากบทความนี้ ผู้อ่านสามารถคอมไพล์และติดตั้งไลบรารี ulab ที่ผนวกเข้ากับ Micropython เพื่อใช้งานกับบอร์ด esp32 ที่มี SPIRAM ได้ ซึ่งหลังจากนี้การเขียนโปรแกรมจะต้องมีการปรับแก้ไขโค้ดเนื่องจากไลบรารี ulab ได้เปลี่ยนแปลงโครงสร้างของการใช้งานไปจากเดิม แต่อย่างไรก็ดี ทีมงานเชื่อว่าผู้อ่านสามารถประยุกต์ปรับปรุงโค้ดจากบทความที่เกี่ยวกับ ulab ที่ทีมงานได้เขียนไว้ก่อนหน้านี้ได้แย่างแน่นอน หรือถ้ามีโอกาสที่เหมาะสม ทางทีมงานจะเขียนบทความสำหรับใช้งาน ulab 3 เบื้องต้นเพื่อเป็นแนวทางการใช้งานต่อไป แต่จากการทดสอบพบว่า SPIRAM หรือ PSRAM ที่เพิ่มเข้ามานั้นทำให้ประสิทธิภาพการทำงานช้าลงกว่า 50% แต่ได้ปริมาณที่เก็บข้อมูลปริมาณที่มากขึ้น สุดท้าย ขอให้สนุกกับการเขียนโปรแกรมครับ

ท่านใดต้องการพูดคุยสามารถคอมเมนท์ไว้ได้เลยครับ

แหล่งอ้างอิง

  1. micropython+ulab

(C) 2021, โดย อ.ดนัย เจษฎาฐิติกุล/อ.จารุต บุศราทิจ
ปรับปรุงเมื่อ 2021-06-20, 2021-08-11, 2021-11-17