1. คำแนะนำและเคล็ดลับ#
ต่อไปนี้เป็นตัวอย่างการใช้งาน inline assembler และข้อมูลเกี่ยวกับวิธีหลีกเลี่ยงข้อจำกัดต่าง ๆ ในเอกสารนี้ คำว่า "assembler function" หมายถึงฟังก์ชันที่ประกาศใน Python ด้วย decorator @micropython.asm_thumb ส่วน "subroutine" หมายถึงโค้ด assembler ที่ถูกเรียกใช้จากภายใน assembler function
1.1. การแตกสาขาและ subroutine ของโค้ด#
สิ่งสำคัญที่ต้องเข้าใจคือ label มีขอบเขตเฉพาะภายใน assembler function เดียวกัน ขณะนี้ยังไม่มีวิธีที่จะเรียก subroutine ที่กำหนดในฟังก์ชันหนึ่งจากอีกฟังก์ชันหนึ่ง
ในการเรียก subroutine จะใช้คำสั่ง bl(LABEL) ซึ่งจะโอนการควบคุมไปยังคำสั่งที่อยู่ถัดจาก directive label(LABEL) และเก็บที่อยู่ผู้ส่งคืนไว้ใน link register (lr หรือ r14) สำหรับการส่งคืน จะใช้คำสั่ง bx(lr) ซึ่งทำให้การดำเนินการดำเนินต่อไปที่คำสั่งถัดจากการเรียก subroutine กลไกนี้หมายความว่าหาก subroutine จะเรียก subroutine อื่น ต้องบันทึก link register ก่อนการเรียกและกู้คืนก่อนสิ้นสุด
ตัวอย่างที่ค่อนข้างซับซ้อนต่อไปนี้แสดงการเรียกฟังก์ชัน โปรดทราบว่าจำเป็นต้องแตกสาขาข้ามการเรียก subroutine ทั้งหมดตั้งแต่เริ่มต้น: subroutine จบการทำงานด้วย 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))
ตัวอย่างโค้ดต่อไปนี้แสดงการเรียกแบบซ้อน (recursive): ลำดับ Fibonacci แบบดั้งเดิม ที่นี่ ก่อนการเรียก recursive 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. การส่งผ่านอาร์กิวเมนต์และการส่งคืน#
Assembler function รองรับอาร์กิวเมนต์ได้ตั้งแต่ศูนย์ถึงสามตัว ซึ่งต้องตั้งชื่อ (หากใช้) ว่า 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 array ของจำนวนเต็มเป็นอาร์กิวเมนต์ให้กับ assembler function ฟังก์ชันจะได้รับที่อยู่ของชุดจำนวนเต็มที่อยู่ติดกัน ดังนั้นอาร์กิวเมนต์หลายตัวจึงสามารถส่งผ่านเป็นองค์ประกอบของ array เดียวได้ ในทำนองเดียวกัน ฟังก์ชันสามารถส่งคืนค่าหลายค่าโดยกำหนดให้กับองค์ประกอบของ array Assembler function ไม่มีวิธีกำหนดความยาวของ array: จำเป็นต้องส่งค่านี้ให้กับฟังก์ชัน
การใช้ array นี้สามารถขยายเพื่อให้ใช้ array มากกว่าสามชุดได้ ทำได้โดยใช้ indirection: โมดูล uctypes รองรับ addressof() ซึ่งจะคืนที่อยู่ของ array ที่ส่งเป็นอาร์กิวเมนต์ ดังนั้นคุณสามารถเติมค่า integer array ด้วยที่อยู่ของ array อื่น ๆ:
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. ชนิดข้อมูลที่ไม่ใช่จำนวนเต็ม#
ชนิดข้อมูลเหล่านี้อาจจัดการได้โดยใช้ array ของชนิดข้อมูลที่เหมาะสม ตัวอย่างเช่น ข้อมูล floating point ความแม่นยำเดี่ยวอาจประมวลผลได้ดังนี้ ตัวอย่างโค้ดนี้รับ array ของ float และแทนที่เนื้อหาด้วยกำลังสองของแต่ละค่า
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 รองรับการใช้โครงสร้างข้อมูลนอกเหนือจาก array ธรรมดา มันทำให้สามารถแมปโครงสร้างข้อมูล Python กับอินสแตนซ์ bytearray ซึ่งสามารถส่งไปยัง assembler function ได้
1.3. ค่าคงที่ที่มีชื่อ#
โค้ด assembler สามารถทำให้อ่านง่ายและบำรุงรักษาได้ง่ายขึ้นโดยใช้ค่าคงที่ที่มีชื่อแทนการใช้ตัวเลขกระจายอยู่ในโค้ด ซึ่งสามารถทำได้ดังนี้:
MYDATA = const(33)
@micropython.asm_thumb
def foo():
mov(r0, MYDATA)
โครงสร้าง const() ทำให้ MicroPython แทนที่ชื่อตัวแปรด้วยค่าของมันในเวลา compile หากค่าคงที่ถูกประกาศในขอบเขต Python ภายนอก สามารถแชร์ระหว่าง assembler function หลายฟังก์ชันและกับโค้ด Python ได้
1.4. โค้ด Assembler ในรูปแบบ class method#
MicroPython ส่งที่อยู่ของอินสแตนซ์ object เป็นอาร์กิวเมนต์แรกให้กับ class method ปกติแล้วสิ่งนี้มีประโยชน์น้อยมากสำหรับ assembler function สามารถหลีกเลี่ยงได้โดยการประกาศฟังก์ชันเป็น static method ดังนี้:
class foo:
@staticmethod
@micropython.asm_thumb
def bar(r0):
add(r0, r0, r0)
1.5. การใช้คำสั่งที่ไม่รองรับ#
คำสั่งเหล่านี้สามารถเขียนโค้ดได้โดยใช้ data statement ดังที่แสดงด้านล่าง แม้ว่า push() และ pop() จะรองรับ แต่ตัวอย่างด้านล่างแสดงหลักการดังกล่าว machine code ที่จำเป็นอาจพบได้ใน ARM v7-M Architecture Reference Manual โปรดทราบว่าอาร์กิวเมนต์แรกของการเรียก data เช่น
data(2, 0xe92d, 0x0f00) # push r8,r9,r10,r11
ระบุว่าอาร์กิวเมนต์ถัดไปแต่ละตัวเป็นปริมาณสองไบต์
1.6. การเอาชนะข้อจำกัดจำนวนเต็มของ MicroPython#
จำนวนเต็มขนาดเล็กของ MicroPython บนพอร์ต 32 บิตไม่สามารถเก็บค่าที่บิต 30 และ 31 แตกต่างกันได้ (เช่น 0x80000000) ดังนั้น assembler routine ที่ให้ผลลัพธ์ 32 บิตเต็มจึงไม่สามารถส่งคืนโดยตรงได้ ข้อจำกัดนี้สามารถเอาชนะได้ด้วยโค้ดต่อไปนี้ ซึ่งใช้ assembler เพื่อใส่ผลลัพธ์ลงใน array และโค้ด 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