1. Gợi ý và mẹo

Sau đây là một số ví dụ về cách sử dụng trình lắp ráp nội tuyến và một số thông tin về cách khắc phục các hạn chế của nó. Trong tài liệu này, thuật ngữ "hàm assembler" đề cập đến hàm được khai báo trong Python với decorator @micropython.asm_thumb, còn "chương trình con" đề cập đến mã assembler được gọi từ bên trong một hàm assembler.

1.1. Các nhánh mã và chương trình con

Điều quan trọng cần hiểu là nhãn có phạm vi cục bộ trong một hàm assembler. Hiện tại không có cách nào để gọi một chương trình con được định nghĩa trong một hàm từ một hàm khác.

Để gọi một chương trình con, lệnh bl(LABEL) được sử dụng. Lệnh này chuyển quyền điều khiển đến lệnh tiếp theo sau chỉ thị label(LABEL) và lưu địa chỉ trả về vào thanh ghi liên kết (lr hay r14). Để trả về, lệnh bx(lr) được sử dụng, khiến quá trình thực thi tiếp tục với lệnh sau lời gọi chương trình con. Cơ chế này ngụ ý rằng nếu một chương trình con cần gọi một chương trình con khác, nó phải lưu thanh ghi liên kết trước khi gọi và khôi phục trước khi kết thúc.

Ví dụ hơi giả tạo sau đây minh họa một lời gọi hàm. Lưu ý rằng cần phải rẽ nhánh xung quanh tất cả các lời gọi chương trình con ở đầu: các chương trình con kết thúc thực thi bằng bx(lr) trong khi hàm ngoài chỉ đơn giản là "kết thúc tự nhiên" theo phong cách của các hàm 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))

Ví dụ mã sau đây minh họa một lời gọi lồng nhau (đệ quy): chuỗi Fibonacci cổ điển. Ở đây, trước một lời gọi đệ quy, thanh ghi liên kết được lưu cùng với các thanh ghi khác mà logic chương trình yêu cầu phải được bảo toàn.

@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. Truyền đối số và trả về

Các hàm assembler có thể hỗ trợ từ không đến ba đối số, các đối số này (nếu được sử dụng) phải được đặt tên là r0, r1r2. Khi mã thực thi, các thanh ghi sẽ được khởi tạo với các giá trị đó.

Các kiểu dữ liệu có thể được truyền theo cách này là số nguyên và địa chỉ bộ nhớ. Với firmware hiện tại, tất cả các giá trị 32 bit có thể được truyền và trả về. Nếu giá trị trả về có thể có bit có nghĩa nhất được đặt, thì nên dùng gợi ý kiểu Python để cho phép MicroPython xác định xem giá trị đó nên được hiểu là số nguyên có dấu hay không dấu: các kiểu là int hoặc uint.

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

hex(uadd(0x40000000,0x40000000)) sẽ trả về 0x80000000, minh họa việc truyền và trả về số nguyên khi các bit 30 và 31 khác nhau.

Các hạn chế về số lượng đối số và giá trị trả về có thể được khắc phục bằng cách sử dụng mô-đun array, cho phép truy cập vào bất kỳ số lượng giá trị nào thuộc bất kỳ kiểu nào.

1.2.1. Nhiều đối số

Nếu một mảng Python gồm các số nguyên được truyền làm đối số cho một hàm assembler, hàm sẽ nhận địa chỉ của một tập hợp số nguyên liên tiếp. Do đó, nhiều đối số có thể được truyền dưới dạng các phần tử của một mảng duy nhất. Tương tự, một hàm có thể trả về nhiều giá trị bằng cách gán chúng vào các phần tử mảng. Các hàm assembler không có cách xác định độ dài của mảng: điều này sẽ cần được truyền cho hàm.

Cách sử dụng mảng này có thể được mở rộng để cho phép sử dụng nhiều hơn ba mảng. Điều này được thực hiện bằng cách sử dụng gián tiếp: mô-đun uctypes hỗ trợ addressof(), hàm này sẽ trả về địa chỉ của một mảng được truyền làm đối số. Do đó bạn có thể điền vào một mảng số nguyên với các địa chỉ của các mảng khác:

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. Các kiểu dữ liệu không phải số nguyên

Chúng có thể được xử lý bằng cách sử dụng các mảng của kiểu dữ liệu phù hợp. Ví dụ, dữ liệu dấu phẩy động độ chính xác đơn có thể được xử lý như sau. Ví dụ mã này nhận một mảng float và thay thế nội dung của nó bằng bình phương của chúng.

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)

Mô-đun uctypes hỗ trợ việc sử dụng các cấu trúc dữ liệu ngoài các mảng đơn giản. Nó cho phép ánh xạ cấu trúc dữ liệu Python lên một thể hiện bytearray, sau đó có thể được truyền cho hàm assembler.

1.3. Hằng số có tên

Mã assembler có thể được làm cho dễ đọc và dễ bảo trì hơn bằng cách sử dụng các hằng số có tên thay vì rải rác mã với các con số. Điều này có thể đạt được như sau:

MYDATA = const(33)

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

Cấu trúc const() khiến MicroPython thay thế tên biến bằng giá trị của nó tại thời điểm biên dịch. Nếu các hằng số được khai báo trong phạm vi Python bên ngoài, chúng có thể được chia sẻ giữa nhiều hàm assembler và với mã Python.

1.4. Mã assembler như phương thức lớp

MicroPython truyền địa chỉ của thể hiện đối tượng làm đối số đầu tiên cho các phương thức lớp. Điều này thường ít hữu ích đối với một hàm assembler. Nó có thể được tránh bằng cách khai báo hàm là phương thức tĩnh như sau:

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

1.5. Sử dụng các lệnh không được hỗ trợ

Chúng có thể được mã hóa bằng cách sử dụng câu lệnh data như được hiển thị bên dưới. Mặc dù push()pop() được hỗ trợ, ví dụ bên dưới minh họa nguyên tắc này. Mã máy cần thiết có thể được tìm thấy trong ARM v7-M Architecture Reference Manual. Lưu ý rằng đối số đầu tiên của các lời gọi data như

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

cho biết rằng mỗi đối số tiếp theo là một đại lượng hai byte.

1.6. Khắc phục hạn chế số nguyên của MicroPython

Các số nguyên nhỏ của MicroPython trên các cổng 32-bit không thể giữ giá trị có các bit 30 và 31 khác nhau (ví dụ 0x80000000), vì vậy một routine assembler tạo ra kết quả 32-bit đầy đủ không thể trả về trực tiếp. Hạn chế này được khắc phục bằng đoạn mã sau, sử dụng assembler để đặt kết quả vào một mảng và mã Python để ép kiểu kết quả thành số nguyên không dấu có độ chính xác tùy ý.

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