1. 힌트 및 팁¶
다음은 인라인 어셈블러 사용 예제 몇 가지와 그 한계를 우회하는 방법에 대한 정보입니다. 이 문서에서 “어셈블러 함수”라는 용어는 @micropython.asm_thumb 데코레이터를 사용하여 Python에서 선언된 함수를 가리키며, “서브루틴”은 어셈블러 함수 내부에서 호출되는 어셈블러 코드를 가리킵니다.
1.1. 코드 분기 및 서브루틴¶
레이블은 어셈블러 함수에 대해 지역적이라는 점을 이해하는 것이 중요합니다. 현재로서는 한 함수에서 정의된 서브루틴을 다른 함수에서 호출할 방법이 없습니다.
서브루틴을 호출하려면 bl(LABEL) 명령을 사용합니다. 이는 label(LABEL) 지시문 다음 명령으로 제어를 이전하고 반환 주소를 링크 레지스터(lr 또는 r14)에 저장합니다. 반환하려면 bx(lr) 명령을 사용하며, 이는 서브루틴 호출 다음 명령으로 실행을 계속하게 합니다. 이 메커니즘은 어떤 서브루틴이 다른 서브루틴을 호출하려면 호출 전에 링크 레지스터를 저장하고 종료 전에 복원해야 함을 의미합니다.
다음의 다소 작위적인 예제는 함수 호출을 보여줍니다. 시작 부분에서 모든 서브루틴 호출을 건너뛰도록 분기해야 한다는 점에 유의하십시오. 서브루틴은 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))
다음 코드 예제는 중첩(재귀) 호출을 보여줍니다. 고전적인 피보나치 수열입니다. 여기서는 재귀 호출 전에 프로그램 로직이 보존하도록 요구하는 다른 레지스터들과 함께 링크 레지스터를 저장합니다.
@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. 인수 전달 및 반환¶
어셈블러 함수는 0개에서 3개의 인수를 지원할 수 있으며, (사용하는 경우) 반드시 r0, r1, r2로 이름을 지정해야 합니다. 코드가 실행될 때 레지스터는 해당 값들로 초기화됩니다.
이 방식으로 전달할 수 있는 데이터 타입은 정수와 메모리 주소입니다. 현재 펌웨어에서는 가능한 모든 32비트 값을 전달하고 반환할 수 있습니다. 반환 값에 최상위 비트가 설정될 수 있는 경우, MicroPython이 해당 값을 부호 있는 정수로 해석할지 부호 없는 정수로 해석할지 판단할 수 있도록 Python 타입 힌트를 사용해야 합니다. 타입은 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 배열을 어셈블러 함수에 인수로 전달하면, 함수는 연속된 정수 집합의 주소를 받습니다. 따라서 여러 인수를 단일 배열의 요소로 전달할 수 있습니다. 마찬가지로 함수는 배열 요소에 값을 할당하여 여러 값을 반환할 수 있습니다. 어셈블러 함수는 배열의 길이를 판단할 방법이 없으므로, 이를 함수에 전달해야 합니다.
이러한 배열 사용은 세 개를 초과하는 배열을 사용할 수 있도록 확장될 수 있습니다. 이는 간접 참조를 사용하여 이루어집니다. 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. 정수가 아닌 데이터 타입¶
이러한 타입은 적절한 데이터 타입의 배열을 사용하여 처리할 수 있습니다. 예를 들어 단정밀도 부동 소수점 데이터는 다음과 같이 처리할 수 있습니다. 이 코드 예제는 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 모듈은 단순한 배열을 넘어서는 데이터 구조의 사용을 지원합니다. 이를 통해 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
이후의 각 인수가 2바이트 크기임을 나타냅니다.
1.6. MicroPython의 정수 제한 극복¶
32비트 포트에서 MicroPython의 작은 정수는 비트 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