[EN] ulab v3.0

From the previous ulab article, it was found that Micropython can implement the same dataset processing instructions as used in Numpy through the previous ulab library v.0.54.0 which is the older version of ulab (currently v.3.0.1) brought up this article. This article describes how to create a Micropython that integrates the ulab library and uses it with SPIRAM versions of esp32.

(Figure. 1 Module list of ulab)

ulab3

From Figure 1, it can be seen that the structure of the ulab library has changed from the original. This causes the programming from the previous example to have to be modified. Under ulab there are libraries of numpy and scipy. The details of numpy that are supported are as follows.

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>

Under numpy, there is an fft module available. This module has the following functions:

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

And under the linalg module which works on linear algebra the following functions are available.

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

Supported scipy functions are

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

Supported linalg module commands are

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

The commands in the optimize module are:

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

The commands under the signal module are:

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

The commands under the special module are

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

The commands in the ulab utisl group are:

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>

Create ulab library

Creating a ulab library to integrate into Micropython requires the esp-idf tool mentioned earlier. Steps to create ulab library with board esp32 with SPIRAM and when ordering the following code will get the result as shown in Figure 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()
(Figure. 2 When recall Micropython with ulab v.3.0.1 detail)

Source code download

First thing you need to do is download the source code for ulab and Micropython, compile mpy-cross and prepare the esp32 submodules.

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

Create partition information

Create a partitions_ulab.csv file using nano and stored in ~/src/micropython/ports/esp32 by typing the following command

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

Type the following code and save with Ctrl+O and exit nano with 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,

Edit sdkconfig

When the partition master file is obtained, the next step is to edit the sdkconfig file for the required board which is stored in the folder ~/src/micropython/ports/esp32/boards. In addition to the board that our team chooses to use as a board with SPIRAM, we have to edit the file sdkconfig.spiram by adding the following 2 lines to it. If it’s on a board without SPIRAM, edit it in the file sdkconfig.base.

CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions_ulab.csv"

Complie

Compiling requires two things:

  • We choose GENERIC_SPIRAM Because the board esp32 used has more RAM than the normal model. But if used with normal version, change to GENERIC which can compile as well.
  • Functional modules written in c or ulab.

The command is as follows and when it starts to compile, it will look like Figure 4, and when the compilation process is complete (and no errors) are displayed as in Figure 5.

cd ~/src/micropython/ports/esp32
make BOARD=GENERIC_SPIRAM USER_C_MODULES=~/src/ulab/code/micropython.cmake 
(Figure. 4 When compiling)
(Figure.5 Result)

Installation

The last step is to install it to the board. We have connected the board to the machine successfully. Therefore, delete and write the firmware from the command line by running the following commands: If it is used with board esp32 without SPIRAM, choose BOARD as GENERIC. The writing process on the board is as shown in Figure 6 and 7.

make erase
make deploy BOARD=GENERIC_SPIRAM
(Figure. 6 Deploying)
(Figure. 7 Result)

Or the reader uses the thonny program to write the file ~/src/micropython/ports/esp32/build-GENERIC_SPIRAM/micropython.bin. instead of commands with the command line as well

Testing

Tested by modifying code18-1.py to use with new ulab and the result is as shown in Figure 8.

(Figure. 8 Result from code18-1a.py)

Rotate square

From the example program in the article ESP32 : Display of rotation squares with application ulab. It is an application of ulab in calculating the rotation of squares because ulab 3 has been changed. The sample code for this version of ulab is as follows:

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

Conclusion

Through this article, readers can compile and install the ulab library that is integrated with Micropython for use with esp32 boards with SPIRAM. Henceforth, coding must be adjusted since there’s some change. However, we believe that readers will be able to adapt the code from the ulab article that the team has written before for sure. Or if there is a suitable opportunity, we will write an article for the initial use of ulab 3 as a guideline for further use. But from testing, it was found that the added SPIRAM or PSRAM caused the performance to be slower up to 50%, but the amount of storage was increased. Finally, have fun programming.

If you want to talk with us, feel free to leave comments below!!

Reference

  1. micropython+ulab

(C) 2021, By Jarut Busarathid and Danai Jedsadathitikul
Updated 2021-11-18