1. רמזים וטיפים

להלן מספר דוגמאות לשימוש באסמבלר המוטמע ומידע על אופן ההתמודדות עם מגבלותיו. במסמך זה המונח ”פונקציית אסמבלר“ מתייחס לפונקציה המוצהרת ב-Python בעזרת הדקורטור @micropython.asm_thumb, בעוד ש“שגרה“ (subroutine) מתייחסת לקוד אסמבלר הנקרא מתוך פונקציית אסמבלר.

1.1. הסתעפויות קוד ושגרות

חשוב להבין שתוויות הן מקומיות לפונקציית אסמבלר. נכון לעכשיו אין דרך לקרוא לשגרה המוגדרת בפונקציה אחת מתוך פונקציה אחרת.

כדי לקרוא לשגרה משתמשים בפקודה bl(LABEL). פקודה זו מעבירה את השליטה לפקודה שאחרי ההנחיה label(LABEL) ושומרת את כתובת החזרה ב-link register‏ (‏lr או r14). כדי לחזור משתמשים בפקודה bx(lr) הגורמת להמשך הביצוע מהפקודה שאחרי קריאת השגרה. מנגנון זה מרמז שאם שגרה אמורה לקרוא לשגרה אחרת, עליה לשמור את ה-link register לפני הקריאה ולשחזר אותו לפני הסיום.

הדוגמה המעט מלאכותית הבאה ממחישה קריאה לפונקציה. שימו לב שבתחילתה נחוץ להסתעף סביב כל קריאות השגרות: שגרות מסיימות את ביצוען עם bx(lr) בעוד שהפונקציה החיצונית פשוט ”נופלת מהקצה“ בסגנון של פונקציות Python.

@micropython.asm_thumb
def quad(r0):
    b(START)
    label(DOUBLE)
    add(r0, r0, r0)
    bx(lr)
    label(START)
    bl(DOUBLE)
    bl(DOUBLE)

print(quad(10))

דוגמת הקוד הבאה מדגימה קריאה מקוננת (רקורסיבית): סדרת פיבונאצ’י הקלאסית. כאן, לפני קריאה רקורסיבית, ה-link register נשמר יחד עם אוגרים נוספים שלוגיקת התוכנית דורשת לשמר.

@micropython.asm_thumb
def fib(r0):
    b(START)
    label(DOFIB)
    push({r1, r2, lr})
    cmp(r0, 1)
    ble(FIBDONE)
    sub(r0, 1)
    mov(r2, r0) # r2 = n -1
    bl(DOFIB)
    mov(r1, r0) # r1 = fib(n -1)
    sub(r0, r2, 1)
    bl(DOFIB)   # r0 = fib(n -2)
    add(r0, r0, r1)
    label(FIBDONE)
    pop({r1, r2, lr})
    bx(lr)
    label(START)
    bl(DOFIB)

for n in range(10):
    print(fib(n))

1.2. העברת ארגומנטים והחזרה

פונקציות אסמבלר יכולות לתמוך בין אפס לשלושה ארגומנטים, אשר חייבים (אם נעשה בהם שימוש) להיקרא r0, ‏r1 ו-r2. כאשר הקוד מתבצע האוגרים יאותחלו לערכים אלו.

סוגי הנתונים שניתן להעביר בדרך זו הם מספרים שלמים וכתובות זיכרון. עם הקושחה הנוכחית ניתן להעביר ולהחזיר את כל ערכי 32 הביט האפשריים. אם בערך המוחזר עשוי להיות מוגדר הביט המשמעותי ביותר, יש להשתמש ברמז סוג (type hint) של Python כדי לאפשר ל-MicroPython לקבוע אם יש לפרש את הערך כמספר שלם עם סימן או ללא סימן: הסוגים הם int או uint.

@micropython.asm_thumb
def uadd(r0, r1) -> uint:
    add(r0, r0, r1)

hex(uadd(0x40000000,0x40000000)) יחזיר 0x80000000, ובכך ידגים את ההעברה וההחזרה של מספרים שלמים שבהם הביטים 30 ו-31 שונים.

ניתן להתגבר על המגבלות במספר הארגומנטים וערכי ההחזרה בעזרת המודול array המאפשר גישה למספר בלתי מוגבל של ערכים מכל סוג.

1.2.1. ארגומנטים מרובים

אם מערך Python של מספרים שלמים מועבר כארגומנט לפונקציית אסמבלר, הפונקציה תקבל את הכתובת של קבוצה רציפה של מספרים שלמים. כך ניתן להעביר ארגומנטים מרובים כאיברים של מערך יחיד. באופן דומה, פונקציה יכולה להחזיר ערכים מרובים על ידי השמתם לאיברי מערך. לפונקציות אסמבלר אין אמצעי לקבוע את אורך המערך: יהיה צורך להעביר זאת לפונקציה.

שימוש זה במערכים ניתן להרחבה כדי לאפשר שימוש ביותר משלושה מערכים. הדבר נעשה באמצעות הפניה עקיפה (indirection): המודול uctypes תומך ב-addressof() שיחזיר את הכתובת של מערך המועבר כארגומנט שלו. כך תוכלו לאכלס מערך מספרים שלמים בכתובות של מערכים אחרים:

from uctypes import addressof
@micropython.asm_thumb
def getindirect(r0):
    ldr(r0, [r0, 0]) # Address of array loaded from passed array
    ldr(r0, [r0, 4]) # Return element 1 of indirect array (24)

def testindirect():
    a = array.array('i',[23, 24])
    b = array.array('i',[0,0])
    b[0] = addressof(a)
    print(getindirect(b))

1.2.2. סוגי נתונים שאינם מספרים שלמים

ניתן לטפל בהם באמצעות מערכים מסוג הנתונים המתאים. לדוגמה, ניתן לעבד נתוני נקודה צפה ברמת דיוק יחיד באופן הבא. דוגמת קוד זו לוקחת מערך של מספרים בנקודה צפה ומחליפה את תוכנו בריבועיהם.

from array import array

@micropython.asm_thumb
def square(r0, r1):
    label(LOOP)
    vldr(s0, [r0, 0])
    vmul(s0, s0, s0)
    vstr(s0, [r0, 0])
    add(r0, 4)
    sub(r1, 1)
    bgt(LOOP)

a = array('f', (x for x in range(10)))
square(a, len(a))
print(a)

המודול uctypes תומך בשימוש במבני נתונים מעבר למערכים פשוטים. הוא מאפשר למפות מבנה נתונים של Python על מופע bytearray אשר ניתן לאחר מכן להעביר לפונקציית האסמבלר.

1.3. קבועים בעלי שם

ניתן להפוך קוד אסמבלר לקריא וקל יותר לתחזוקה על ידי שימוש בקבועים בעלי שם במקום לפזר בקוד מספרים. ניתן להשיג זאת כך:

MYDATA = const(33)

@micropython.asm_thumb
def foo():
    mov(r0, MYDATA)

המבנה const() גורם ל-MicroPython להחליף את שם המשתנה בערכו בזמן ההידור. אם קבועים מוצהרים בהיקף Python חיצוני, ניתן לשתף אותם בין מספר פונקציות אסמבלר וכן עם קוד Python.

1.4. קוד אסמבלר כמתודות של מחלקה

‏MicroPython מעביר את הכתובת של מופע האובייקט כארגומנט הראשון למתודות מחלקה. לרוב זה חסר תועלת עבור פונקציית אסמבלר. ניתן להימנע מכך על ידי הצהרה על הפונקציה כמתודה סטטית כך:

class foo:
  @staticmethod
  @micropython.asm_thumb
  def bar(r0):
    add(r0, r0, r0)

1.5. שימוש בפקודות שאינן נתמכות

ניתן לקודד אותן באמצעות הצהרת data כמוצג להלן. בעוד ש-push() ו-pop() נתמכות, הדוגמה שלהלן ממחישה את העיקרון. את הקוד הבינארי הנחוץ ניתן למצוא ב-ARM v7-M Architecture Reference Manual. שימו לב שהארגומנט הראשון של קריאות data כגון

data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11

מציין שכל ארגומנט עוקב הוא כמות של שני בייטים.

1.6. התגברות על מגבלת המספרים השלמים של MicroPython

מספרים שלמים קטנים של MicroPython ביציאות 32 ביט אינם יכולים להחזיק ערך שבו הביטים 30 ו-31 שונים (לדוגמה 0x80000000), ולכן שגרת אסמבלר המייצרת תוצאה מלאה של 32 ביט אינה יכולה פשוט להחזיר אותה ישירות. ניתן להתגבר על מגבלה זו בעזרת הקוד הבא, המשתמש באסמבלר כדי להכניס את התוצאה למערך, וב-Python כדי לכפות את התוצאה למספר שלם ללא סימן בעל דיוק שרירותי.

from array import array

@micropython.asm_thumb
def getval(r0):
    movwt(r1, 0x80000000)  # a 32-bit value whose bits 30 and 31 differ
    str(r1, [r0, 0])

def get():
    a = array('i', [0])
    getval(a)
    return a[0] & 0xffffffff  # coerce to arbitrary precision

print(hex(get()))  # 0x80000000