Obsah
1. Neférový benchmark na úvod: rychlost realizace vnořených programových smyček
2. Vylepšený způsob zobrazení strojového kódu přeložené funkce
3. Počítaná programová smyčka s testem provedeným na začátku
4. Zbývající instrukce ze sady Thumb implementované a podporované MicroPythonem
5. Zpracování osmibitových a šestnáctibitových dat
7. Ukázka použití vybraných bitových operací
9. Ukázka použití operací posunu a rotace
12. Porovnání rychlosti výpočtu součinu MicroPythonem
13. Instrukce pro porovnání dvou hodnot
14. Zbylé instrukce pro práci na úrovni jednotlivých bitů
15. Prefix IT v instrukční sadě Thumb-2
16. Realizace prefixu IT v MicroPythonu
18. Popsané instrukce a způsob jejich zápisu v MicroPythonu
19. Repositář s demonstračními příklady
1. Neférový benchmark na úvod: rychlost realizace vnořených programových smyček
Hned na začátku tohoto článku se pokusíme porovnat rychlost dvou implementací vnořených programových smyček. První implementaci jsme si již ukázali minule – je založena na podmíněných skocích. Připomeňme si, že se v tomto případě jedná o smyčky, jejichž počitadla jsou uložena v registrech R1 a R2. Tělo vnitřní smyčky se vykoná 100×100=10000 krát, což znamená, že návratovou hodnotou této funkce bude taktéž hodnota 10000 (návratová hodnota je předána v pracovním registru R0):
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 100) # počáteční hodnota počitadla vnější smyčky label(outer_loop) # označení začátku vnější programové smyčky mov(r2, 100) # počáteční hodnota počitadla vnitřní smyčky label(inner_loop) # označení začátku vnitřní programové smyčky add(r0, r0, 1) # tělo smyčky sub(r2, r2, 1) # snížení hodnoty počitadla + nastavení příznaků bne(inner_loop) # opakování vnitřní smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků bne(outer_loop) # opakování vnější smyčky
Dobu trvání si necháme změřit v mikrosekundách:
import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
S výsledkem:
4704
Druhá verze benchmarku je naprogramována v čistém a idiomatickém Pythonu, a to bez použití optimalizací:
def loop(): x = 0 for i in range(100): for j in range(100): x+=1 return x import utime t1 = utime.ticks_us() loop() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Výsledkem je v tomto případě kód, který je přibližně 12× pomalejší:
59022
2. Vylepšený způsob zobrazení strojového kódu přeložené funkce
V předchozím článku jsme si taktéž ukázali funkci sloužící k zobrazení strojového kódu přeložené funkce, což má význam zejména pro funkce s dekorátorem @micropython.asm_thumb. Připomeňme si, že takové funkce vždy začínají instrukcí:
push {r1, r4, r5, r6, r7, lr}
a končí instrukcí:
pop {r1, r4, r5, r6, r7, pc}
Důležitá je druhá instrukce, která je zakódována do dvojice bajtů:
f2 bd
Tyto dva bajty budeme hledat v paměťovém bloku se strojovým kódem. Nemáme totiž k dispozici jinou informaci o tom, na které adrese strojový kód končí (tato informace bude existovat, ovšem s verzemi Pythonu se mění). Upravená varianta pomocné funkce sloužící pro analýzu a výpis funkce přeložené do strojového kódu tedy může vypadat například takto (realizací je samozřejmě větší množství):
import machine import array def inspect(f): baddr = bytes(array.array("O", [f])) addr = int.from_bytes(baddr, "little") print("function object at: 0x%08x" % addr) print("number of args: %u" % machine.mem32[addr + 4]) code_addr = machine.mem32[addr + 8] print("machine code at: 0x%08x" % code_addr) previous = -1 size = 0 while True: current = machine.mem8[code_addr + size] size += 1 if current == 0xbd and previous == 0xf2: break previous = current print(f"machine code size: {size} bytes") print("-- code --") for i in range(size): print(f"{machine.mem8[code_addr + i]:02x}", end=" ") print("\n----------")
3. Počítaná programová smyčka s testem provedeným na začátku
Navažme nyní na téma, kterému jsme se věnovali na konci předchozího článku. Ukážeme si další počítanou programovou smyčku, v níž se ovšem test na ukončení bude pro změnu provádět na jejím začátku ihned po odečtení jedničky od počitadla (a samozřejmě se otočí podmínka ve skoku – nyní bude skok proveden při nulovém počitadle). Tento příklad je vlastně v mnoha ohledech totožný s původní smyčkou s testem na konci:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 100) # počáteční hodnota počitadla label(loop) # označení začátku programové smyčky add(r0, r0, 4) # tělo smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků bne(loop) # skok v případě že se nedosáhlo nuly
Ovšem vzhledem k tomu, že změna stavu počitadla (snížení jeho hodnoty o jedničku) a následný test je proveden na začátku smyčky, je nutné při inicializaci počitadla do něj vložit hodnotu 101 a nikoli 100:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 101) # počáteční hodnota počitadla + 1 label(loop) # označení začátku programové smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků beq(break_loop) # skok v případě že se dosáhlo nuly add(r0, r0, 4) # tělo smyčky b(loop) label(break_loop)
Pokud si jsme jisti, že hodnota počitadla před vstupem do smyčky nebude nulová, lze dokonce i snížení hodnoty počitadla (s nastavením příznaků) přenést na konec smyčky. Smyčka v tomto případě tedy skutečně začíná podmíněným skokem:
@micropython.asm_thumb def loop(): mov(r0, 0) mov(r1, 101) # počáteční hodnota počitadla + 1 label(loop) # označení začátku programové smyčky beq(break_loop) # skok v případě že se dosáhlo nuly add(r0, r0, 4) # tělo smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků b(loop) label(break_loop)
Výsledek si ověříme přímo v REPLu MicroPythonu. Měla by se vypsat hodnota 400, protože se stokrát zvýší hodnota v registru R0 o 4:
>>> loop() 400
Překlad do strojového kódu vypadá takto:
>>> inspect(loop) function object at: 0x20008f20 number of args: 0 machine code at: 0x200090e0 machine code size: 16 bytes -- code -- f2 b5 00 20 65 21 49 1e 01 d0 00 1d fb e7 f2 bd ----------
Jedná se o strojový kód s osmi instrukcemi Thumb uloženými v šestnácti bajtech.
4. Zbývající instrukce ze sady Thumb implementované a podporované MicroPythonem
V navazujících kapitolách si popíšeme všechny další instrukce ze sady Thumb, které jsou podporované MicroPythonem. U některých instrukcí si ukážeme demonstrační příklady, další instrukce budou pouze popsány (v případě, že se podobají instrukcím jiným). Na tomto místě je vhodné doplnit, že mapování mezi strojovými instrukcemi sady Thumb a MicroPythonem není ve všech případech stoprocentní, protože se například odlišuje způsob zápisu operandů apod. Na tyto rozdíly pochopitelně upozorníme.
5. Zpracování osmibitových a šestnáctibitových dat
Poměrně často se v programech manipuluje s osmibitovými či šestnáctibitovými hodnotami (ASCII znaky, zvukové vzorky atd.). To je pro čistě 32bitové čipy, mezi něž patří i mikrořadiče Cortex-M, poměrně problematické, protože většina operací je provedena s 32bitovými registry. Ovšem máme k dispozici alespoň specializované instrukce určené pro načtení osmibitové či šestnáctibitové hodnoty, přičemž zbytek 32bitového registru je vynulován. Podobně existují i instrukce pro uložení pouze osmi či šestnácti bitů původně 32bitového registru:
Instrukce | Stručný popis |
---|---|
ldrb(Rt, [Rn, imm5]) | načtení bajtu, zbylých 24 bitů se vynuluje |
ldrh(Rt, [Rn, imm6]) | načtení 16bitového slova, zbylých 16 bitů se vynuluje |
ldr(Rt, [Rn, imm7]) | načtení celého 32bitového slova (již známe) |
strb(Rt, [Rn, imm5]) | uložení bajtu (spodních 8 bitů registru) |
strh(Rt, [Rn, imm6]) | uložení 16bitového slova (spodních 16 bitů registru) |
str(Rt, [Rn, imm7]) | uložení celého 32bitového slova |
Vyzkoušejme si tedy, jak se načte jediný bajt, nezávisle na původním obsahu registru (tj. do osmi nejnižších bitů registru se načte hodnota bajtu a zbylých 24 bitů bude vynulováno):
from array import array control = array('B', [255, 255, 100, 255, 255]) @micropython.asm_thumb def load_byte(r0): mov(r1, 0) # vynulujeme R1 mvn(r1, r1) # nyni bude R1 obsahovat same jednicky ldrb(r1, [r0, 2]) # nacteme jediny bajt z pole (ten s hodnotou 100) mov(r0, r1) # navratova hodnota load_byte(control)
Tento prográmek po svém spuštění vypíše hodnotu 100. To znamená, že z pole, které obsahuje bajty s různými hodnotami, se načte skutečně pouze jediný bajt, a to konkrétně bajt s offsetem 2 (schválně jsou okolo tohoto bajtu další hodnoty 255 a nikoli nulové hodnoty).
Podobně lze realizovat načtení šestnáctibitového slova:
from array import array control = array('B', [255, 255, 100, 0, 255, 255]) @micropython.asm_thumb def load_halfword(r0): mov(r1, 0) # vynulujeme R1 mvn(r1, r1) # nyni bude R1 obsahovat same jednicky ldrh(r1, [r0, 2]) # nacteme jedine 16bitove slova z pole (to s hodnotou 100 + 0*256) mov(r0, r1) load_halfword(control)
6. Bitové operace
Další skupinou instrukcí, které nalezneme v instrukční sadě Thumb a které jsou podporovány i MicroPythonem, jsou instrukce určené pro provádění základních bitových operací. Všechny tyto instrukce mají jednotný formát s dvojicí vstupních registrů, přičemž jeden z těchto registrů bude obsahovat i výsledek výpočtu – nejedná se tedy o „tříadresový kód“, který jsme mohli vidět zejména u instrukcí ADD a SUB:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | 0 | 0 | 0 | operace | Rs | Rd | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Jedná se o tyto instrukce:
Kód | Operace | Ekvivalent ARM | Význam |
---|---|---|---|
0000 | AND Rd, Rs | ANDS Rd, Rd, Rs | Rd:= Rd AND Rs |
1100 | ORR Rd, Rs | ORRS Rd, Rd, Rs | Rd := Rd OR Rs |
0001 | EOR Rd, Rs | EORS Rd, Rd, Rs | Rd:= Rd EOR Rs (EOR=XOR) |
1000 | TST Rd, Rs | TST Rd, Rs | Nastavení příznaků podle operace Rd AND Rs |
1110 | BIC Rd, Rs | BICS Rd, Rd, Rs | Rd := Rd AND NOT Rs |
1111 | MVN Rd, Rs | MVNS Rd, Rs | Rd := NOT Rs |
Způsob zápisu těchto instrukcí v MicroPythonu je následující:
Instrukce | Zápis v MicroPythonu |
---|---|
AND Rd, Rs | and_(Rd, Rs) |
ORR Rd, Rs | orr(Rd, Rs) |
EOR Rd, Rs | eor(Rd, Rs) |
TST Rd, Rs | tst(Rd, Rs) |
BIC Rd, Rs | bic(Rd, Rs) |
MVN Rd, Rs | mvn(Rd, Rs) |
7. Ukázka použití vybraných bitových operací
Některé z výše popsaných bitových operací si otestujeme na jednoduchých příkladech.
Instrukce AND:
@micropython.asm_thumb def and_operation(r0, r1): and_(r0, r1) print(hex(and_operation(0x00, 0x00))) print(hex(and_operation(0xff, 0xaa))) print(hex(and_operation(0x18, 0xf0))) print(hex(and_operation(0x18, 0x0f)))
Výsledky:
0x0 0xaa 0x10 0x8
Instrukce ORR:
@micropython.asm_thumb def or_operation(r0, r1): orr(r0, r1) print(hex(or_operation(0x00, 0x00))) print(hex(or_operation(0xff, 0xaa))) print(hex(or_operation(0x18, 0xf0))) print(hex(or_operation(0x18, 0x0f)))
Výsledky:
0x0 0xff 0xf8 0x1f
Instrukce BIC:
@micropython.asm_thumb def bic_operation(r0, r1): bic(r0, r1) print(hex(bic_operation(0x00, 0x00))) print(hex(bic_operation(0xff, 0xaa))) print(hex(bic_operation(0x18, 0xf0))) print(hex(bic_operation(0x18, 0x0f)))
Výsledky:
0x0 0x55 0x8 0x10
A konečně instrukce MVN:
@micropython.asm_thumb def mvn_operation(r0): mvn(r0, r0) print(hex(mvn_operation(0x00))) print(hex(mvn_operation(0xff))) print(hex(mvn_operation(0x18))) print(hex(mvn_operation(0x0f)))
Výsledky:
-0x1 -0x100 -0x19 -0x10
8. Bitové posuny a rotace
Do další skupiny instrukcí patří instrukce provádějící bitové posuny a rotace. Nalezneme zde čtveřici instrukcí, a to konkrétně bitový posun doleva i doprava, dále aritmetický posun doprava (zachová se hodnota znaménka) a taktéž bitovou rotaci. Všechny čtyři zmíněné instrukce existují ve variantě s jedním cílovým registrem a dvěma zdrojovými registry, přičemž jeden ze zdrojových registrů je současně i registrem cílovým. Tento formát instrukcí již známe:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 1 | 0 | 0 | 0 | 0 | operace | Rs | Rd | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Konkrétně se jedná o tyto instrukce:
Kód | Operace | Ekvivalent ARM | Význam |
---|---|---|---|
0010 | LSL Rd, Rs | MOVS Rd, Rd, LSL Rs | Rd := Rd << Rs (bitový posun) |
0011 | LSR Rd, Rs | MOVS Rd, Rd, LSR Rs | Rd := Rd >> Rs (bitový posun) |
0100 | ASR Rd, Rs | MOVS Rd, Rd, ASR Rs | Rd := Rd ASR Rs (aritmetický posun) |
0111 | ROR Rd, Rs | MOVS Rd, Rd, ROR Rs | Rd := Rd ROR Rs (rotace) |
Instrukce | Zápis v MicroPythonu |
---|---|
LSL Rd, Rs | lsl(Rd, Rs) |
LSR Rd, Rs | lsr(Rd, Rs) |
ASR Rd, Rs | asr(Rd, Rs) |
ROR Rd, Rs | ror(Rd, Rs) |
Aritmetické a bitové posuny (ovšem nikoli rotace) existují taktéž ve vlastní variantě, kdy je počet bitů 0..31 zakódován přímo v instrukčním slovu:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 | 0 | 0 |operace| posun 0-31 bitů | Rs | Rd | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
Konkrétně se jedná o tyto instrukce:
Operace | Význam |
---|---|
LSL Rd, Rs, #shift | Rd := Rs << #shift (bitový posun) |
LSR Rd, Rs, #shift | Rd := Rs >> #shift (bitový posun) |
ASR Rd, Rs, #shift | Rd := Rs ASR #shift (aritmetický posun) |
9. Ukázka použití operací posunu a rotace
Opět si ukažme, jaký vliv mají výše popsané instrukce na hodnoty předané do testovacích funkcí.
Logický posun doleva:
@micropython.asm_thumb def lsl_operation(r0, r1): lsl(r0, r1) print(hex(lsl_operation(0x01, 0))) print(hex(lsl_operation(0x01, 1))) print(hex(lsl_operation(0x01, 2))) print(hex(lsl_operation(0x01, 3)))
Výsledky (jedná se o násobení mocninami dvojky):
0x1 0x2 0x4 0x8
Logický posun doprava:
@micropython.asm_thumb def lsr_operation(r0, r1): lsr(r0, r1) print(hex(lsr_operation(0x100, 0))) print(hex(lsr_operation(0x100, 1))) print(hex(lsr_operation(0x100, 2))) print(hex(lsr_operation(0x100, 3)))
Výsledky (jedná se o dělení mocninami dvojky):
0x100 0x80 0x40 0x20
Aritmetický posun doprava:
@micropython.asm_thumb def asr_operation(r0, r1): asr(r0, r1) print(hex(asr_operation(-0x100, 0))) print(hex(asr_operation(-0x100, 1))) print(hex(asr_operation(-0x100, 2))) print(hex(asr_operation(-0x100, 3)))
Výsledky (jedná se o dělení mocninami dvojky se zachováním znaménka):
-0x100 -0x80 -0x40 -0x20
A konečně rotace doprava či doleva (podle znaménka druhého operandu):
@micropython.asm_thumb def ror_operation(r0, r1): ror(r0, r1) print(hex(ror_operation(1, -3))) print(hex(ror_operation(1, -2))) print(hex(ror_operation(1, -1))) print(hex(ror_operation(1, 0))) print(hex(ror_operation(1, 1))) print(hex(ror_operation(1, 2))) print(hex(ror_operation(1, 3)))
Výsledky (první tři rotace jsou de facto doleva, poslední tři doprava):
0x8 0x4 0x2 0x1 -0x80000000 0x40000000 0x20000000
10. Operace součinu
V instrukční sadě Thumb se nachází i instrukce provádějící operaci součinu. Tato instrukce se jmenuje MUL a je zajímavá ve třech ohledech:
- násobí se dvě 32bitové hodnoty, ovšem výsledkem je taktéž pouze 32bitová hodnota (horních 32 bitů je tedy useknuto, 64bitové výsledky produkuje instrukce, která na jádrech Cortex-M0+ atd. není dostupná)
- cílový registr Rd je totožný se druhým zdrojovým registrem (to platí i pro mnoho dalších ALU operací kromě součtu a rozdílu)
- nastavují se příznaky N a Z, nikoli C a V (přitom C by měl svůj význam)
Pozor ovšem na to, že instrukce MUL se v MicroPythonu musí zapsat jen se dvěma operandy, protože první registr je současně registrem zdrojovým i cílovým:
@micropython.asm_thumb def mul(r0, r1): mul(r0, r1)
Způsob překladu této funkce do strojového kódu vypadá následovně:
push {r1, r4, r5, r6, r7, lr} muls r0, r1, r0 pop {r1, r4, r5, r6, r7, pc}
Ověřme si, jaké výsledky instrukce MUL produkuje.
Násobení nulou:
>>> mul(0, 0) 0 >>> mul(0, 1000) 0 >>> mul(1000, 0) 0
Kladné a záporné činitele:
>>> mul(2, 3) 6 >>> mul(-2, 3) -6 >>> mul(2, -3) -6 >>> mul(-2, -3) 6
Přetečení výsledků přes 32 bitů:
>>> mul(1<<29, 2) 1073741824 >>> mul(1<<30, 2) -2147483648 >>> mul(1<<31, 2) 0 >>> mul(1<<29, -2) -1073741824 >>> mul(1<<30, -2) -2147483648 >>> mul(1<<31, -2) 0
11. Rychlost instrukce mul
V úvodním článku o MicroPythonu a instrukční sadě Thumb jsme si mj. řekli, že v instrukční sadě Thumb nalezneme i instrukci MULS určenou pro násobení dvou 32bitových operandů, přičemž výsledek je taktéž 32bitový (zapamatuje se jen spodních 32bitů výsledku – to jsme si ostatně již prakticky ukázali před chvílí). Při implementaci mikroprocesoru je možné zvolit, jakým typem násobičky se tato instrukce bude provádět. V případě, že se má jednat o výkonnější čip (a aplikace operaci násobení skutečně využije), může se použít rychlá násobička, která celou operaci dokáže provést v jediném taktu (samozřejmě se měření provádí při postupně zaplňované pipeline). Pokud se ovšem má jednat o levnější a méně výkonný čip, lze násobení implementovat po krocích, což sice trvá celých 32 taktů, ovšem potřebná plocha čipu a i energetická náročnost je mnohem menší, než v případě jednocyklové násobičky.
Zkusme si tedy změřit rychlost instrukce MULS. Víme již, že dvojice vnořených smyček, v jejichž těle se provádí stotisíckrát součet, trvá přibližně 4704 mikrosekund. Nahraďme tedy instrukci ADDS za MULS (s tím, že měříme i dobu trvání instrukcí zajišťujících iteraci atd.):
@micropython.asm_thumb def loop_mul(): mov(r0, 10) mov(r1, 100) # počáteční hodnota počitadla vnější smyčky label(outer_loop) # označení začátku vnější programové smyčky mov(r2, 100) # počáteční hodnota počitadla vnitřní smyčky label(inner_loop) # označení začátku vnitřní programové smyčky mul(r0, r0) # tělo smyčky sub(r2, r2, 1) # snížení hodnoty počitadla + nastavení příznaků bne(inner_loop) # opakování vnitřní smyčky sub(r1, r1, 1) # snížení hodnoty počitadla + nastavení příznaků bne(outer_loop) # opakování vnější smyčky import utime t1 = utime.ticks_us() loop_mul() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
V případě běhu na RP2040 bude výsledek prakticky totožný (v rámci přesnosti měření času):
4676
Z toho plyne, že operace MULS je na tomto konkrétním čipu provedena stejně rychle jako instrukce ADDS. Pokud by MULS trvala významně déle, bylo by to patrné na výsledku měření, a to i přesto, že měříme i čas trvání SUBS a podmíněného skoku BNE.
12. Porovnání rychlosti výpočtu součinu MicroPythonem
Opět se pokusme přepsat opakující se operaci násobení do MicroPythonu. Nejedná se o příliš férové srovnání, protože kód není optimalizován. Na druhou stranu je však napsán idiomaticky:
def loop_mul(): x = 10 for i in range(100): for j in range(100): x*=1 return x import utime t1 = utime.ticks_us() loop_mul() t2 = utime.ticks_us() print(utime.ticks_diff(t2, t1))
Výsledek bude získán za 63364 časových jednotek:
63364
To vlastně odpovídá srovnání rychlosti operací součtu: 4704 vs 59022.
13. Instrukce pro porovnání dvou hodnot
S instrukcí CMP určenou pro porovnání dvou hodnot jsme se již setkali. Tato instrukce ve skutečnosti existuje ve dvou variantách – porovnání dvou registrů a porovnání registru s krátkou osmibitovou konstantou (což se velmi často využije, protože se typicky porovnává s nulou či jedničkou). A zajímavá je i existence instrukce pro součet obsahu dvou registrů a nastavení všech příznaků:
Instrukce | Zápis v MicroPythonu | Význam |
---|---|---|
CMP Rd, #konstanta | cmp(Rd, konstanta) | porovnání dvou hodnot s nastavením příznaků |
CMP Rd, Rs | cmp(Rd, Rs) | porovnání dvou hodnot s nastavením příznaků |
CMN Rd, Rs | cmn(Rd, Rs) | součet dvou 32bitových hodnot s nastavením příznaků |
14. Zbylé instrukce pro práci na úrovni jednotlivých bitů
V MicroPythonu nalezneme i dvě instrukce ze sady Thumb-2, které pracují na úrovni jednotlivých bitů. Jedná se o tyto instrukce:
Instrukce | Zápis v MicroPythonu | Význam |
---|---|---|
CLZ Rd, Rn | clz(Rd, Rn) | detekce délky sekvence nulových bitů v registru (hledáno od nejvyššího bitu) |
RBIT Rd, Rn | rbit(Rd, Rn) | otočení bitů v předaném vstupním registru |
15. Prefix IT v instrukční sadě Thumb-2
Tvůrci instrukční sady Thumb-2 se taktéž snažili nějakým způsobem nahradit kdysi populární kombinaci příznakových bitů, které byly součástí většiny instrukcí klasické RISCové instrukční sady A32. Z tohoto důvodu do instrukční sady Thumb-2 přidali jednu z nejzajímavějších instrukcí, které kdy pro RISCové mikroprocesory vznikly (a otázkou je, do jaké míry se jedná o RISCovou instrukci).
Jedná se vlastně o instrukční prefix nazvaný IT podle sousloví if-then. Tento prefix může být aplikován na jednu až čtyři instrukce následující za prefixem. Ihned za prefixem IT se (bez mezery) udává, zda má být daná instrukce provedena při splnění podmínky (T – then) či naopak při jejím nesplnění (E – else). U první instrukce je automaticky předpokládáno T, tudíž se uvádí maximálně tři kombinace znaků T/E. Samozřejmě je taktéž nutné zapsat i testovanou podmínku – může se jednat o kódy používané jak u podmíněných skoků (Thumb, AArch64), tak i v podmínkových bitech (ARM32):
Kód | Význam | Předchozí operace porovnání |
---|---|---|
EQ | Z==1 (rovno) | signed i unsigned |
NE | Z==0 (nerovno) | signed i unsigned |
CS | C==1 (větší nebo rovno) | unsigned |
CC | C==0 (menší než) | unsigned |
MI | N==1 (záporný výsledek) | signed |
PL | N==0 (kladný nebo nulový výsledek) | signed |
VS | V==1 (přetečení) | signed |
VC | V==0 (nedošlo k přetečení) | signed |
HI | C==1 & Z==0 (vetší než) | unsigned |
LS | C==0 | Z==1 (menší nebo rovno) | unsigned |
GE | N==V (větší nebo rovno) | signed |
LT | N!=V (menší než) | signed |
GT | Z==0 & N==V (větší než) | signed |
LE | Z==1 N!=V (menší nebo rovno) | signed |
V praxi to může znamenat, že zápis v assembleru:
ITEEE EQ
značí, že pokud je nastaven příznak zero (rovnost), je provedena jen první instrukce následující za prefixem, kdežto další tři instrukce nebudou provedeny (třikrát „else“).
Pokud by se měly provést tři instrukce v případě kladného výsledku předchozího porovnání, použil by se zápis:
ITTT PL
Porovnejme si nyní tři identické algoritmy. První z nich je implementovaný s využitím instrukcí A32 s podmínkovými bity:
LDREQ r0,[r1] ; if EQ then LDR LDRNE r0,[r2] ; if NE then LDR ADDEQ r0, r3, r0 ; if EQ then ADD ADDNE r0, r4, r0 ; if NE then ADD
V případě použití instrukční sady Thumb musíme využít podmíněné skoky se všemi nepříjemnostmi, které z toho plynou:
BNE L1 ; opačná podmínka - přeskočení instrukce LDR r0, [r1] ADD r0, r3, r0 ; máme štěstí: můžeme prohodit pořadí operandů B L2 L1 LDR r0, [r2] ADD r0, r4, r0 L2
U instrukční sady Thumb-2 lze v tomto případu s výhodou použít prefixovou instrukci IT:
ITETE EQ LDR r0, [r1] LDR r0, [r2] ADD r0, r3, r0 ADD r0, r4, r0
Na závěr si ještě všechny tři implementace pro zajímavost porovnáme, a to jak z hlediska velikosti programového kódu, tak i z hlediska celkové doby trvání výpočtu:
Instrukční sada | Velikost kódu | Počet cyklů |
---|---|---|
ARM (RISC) | 16 bajtů | 4 cykly |
Thumb | 12 bajtů | 4–20 cyklů |
Thumb-2 | 10 bajtů | 4–5 cyklů |
Výsledek: pokud máme kvalitní překladač, bude instrukční sada Thumb-2 s velkou pravděpodobností lepší, než snaha o implementaci A32. Tento rozdíl bude ještě více patrný na mikrořadičích a tedy i jádrech Cortex-M, protože zde může být program uložen v relativně pomalé Flash ROM.
16. Realizace prefixu IT v MicroPythonu
MicroPython prefix IT podporuje, a to ve všech variantách, tedy například:
ite itt itee itete iteee ittte
atd.
Do zvoleného prefixu se zapíše podmínka:
ite(eq) ite(lt) ittte(ne)
A potom již následuje jedna až čtyři instrukce vykonané v případě, že podmínka je či naopak není splněna. Například můžeme vrátit výsledek 100 či 200 na základě porovnání dvou argumentů funkce:
cmp(r0, r1) ite(eq) mov(r0, 100) # if r0 == r1 mov(r0, 200) # if r0 != r1
17. Speciální instrukce
A konečně nám zbývá několik speciálních instrukcí, resp. instrukcí, které nespadají do žádné výše uvedené kategorie:
Instrukce | Zápis v MicroPythonu | Význam |
---|---|---|
NOP | nop() | pouze zvýšení PC (pseudoinstrukce) |
WFI | wfi() | pozastavení MCU dokud nepřijde přerušení |
CPSID příznaky | cpsid(příznaky) | zákaz přerušení |
CPSIE příznaky | cpsie(příznaky) | povolení přerušení |
MRS(Rd, APRS/PSR) | mrs(Rd, special_reg) | přesun obsahu speciálního registru do pracovního registru |
18. Doposud popsané instrukce a způsob jejich zápisu v MicroPythonu
Prozatím jsme se seznámili se zápisem následujících instrukcí v MicroPythonu. I když se to nezdá, jedná se o většinu instrukcí ze sady Thumb:
Instrukce | Zápis v MicroPythonu | Stručný popis |
---|---|---|
MOV r0, #0 | mov(r0, 0) | uložení krátké konstanty do pracovního registru |
MOVW r0, #1000 | movw(r0, 1000) | uložení 16bitového slova do pracovního registru |
LDR r0, [r0, 0] či ldr r0, =adresa | ldr(r0, [r0, 0]) | přečtení 32bitového slova ze zadané adresy |
LDRB r0, [r0, 0] či ldrb r0, =adresa | ldrb(r0, [r0, 0]) | přečtení bajtu ze zadané adresy |
LDRW r0, [r0, 0] či ldrw r0, =adresa | ldrw(r0, [r0, 0]) | přečtení 16bitového slova ze zadané adresy |
ADDS r1, r1, #1 | add(r0, r0, 1) | součet registru s konstantou, zápis do obecně jiného registru |
SUBS r1, r1, #1 | sub(r1, r1, 1) | rozdíl, zde konkrétně snížení hodnoty registru R1 o jedničku s nastavením příznaků |
CMP Rd, #konstanta | cmp(Rd, konstanta) | porovnání dvou hodnot s nastavením příznaků |
CMP Rd, Rs | cmp(Rd, Rs) | porovnání dvou hodnot s nastavením příznaků |
CMN Rd, Rs | cmn(Rd, Rs) | součet dvou 32bitových hodnot s nastavením příznaků |
AND Rd, Rs | and_(Rd, Rs) | operace logického součinu bit po bitu |
ORR Rd, Rs | orr(Rd, Rs) | operace logického součtu bit po bitu |
EOR Rd, Rs | eor(Rd, Rs) | operace nonekvivalence bit po bitu |
TST Rd, Rs | tst(Rd, Rs) | nastavení příznaků podle operace logického součinu bit po bitu |
BIC Rd, Rs | bic(Rd, Rs) | operace AND NOT |
MVN Rd, Rs | mvn(Rd, Rs) | negace všech bitů |
LSL Rd, Rs | lsl(Rd, Rs) | bitový posun doleva |
LSR Rd, Rs | lsr(Rd, Rs) | bitový posun doprava |
ASR Rd, Rs | asr(Rd, Rs) | aritmetický posun doprava |
ROR Rd, Rs | ror(Rd, Rs) | rotace |
CLZ Rd, Rn | clz(Rd, Rn) | detekce délky sekvence nulových bitů v registru (hledáno od nejvyššího bitu) |
RBIT Rd, Rn | rbit(Rd, Rn) | otočení bitů v předaném vstupním registru |
B cil_skoku | b(cil_skoku) | nepodmíněný skok na zadanou adresu (omezený rozsah, typicky pro jednu funkci/subrutinu) |
BNE cil_skoku či b.ne cil_skoku | bne(cil_skoku) | podmíněný skok za podmínky, že příznak Z (zero) není nastaven |
BEQ cil_skoku či b.ne cil_skoku | bne(cil_skoku) | podmíněný skok za podmínky, že příznak Z (zero) je nastaven |
PUSH {registry} | push({registry}) | uložení vybraných pracovních registrů na zásobník |
POP {registry} | push({registry}) | obnovení vybraných pracovních registrů ze zásobníku |
NOP | nop() | pouze zvýšení PC (pseudoinstrukce) |
WFI | wfi() | pozastavení MCU dokud nepřijde přerušení |
CPSID příznaky | cpsid(příznaky) | zákaz přerušení |
CPSIE příznaky | cpsie(příznaky) | povolení přerušení |
MRS(Rd, APRS/PSR) | mrs(Rd, special_reg) | přesun obsahu speciálního registru do pracovního registru |
19. Repositář s demonstračními příklady
Zdrojové kódy všech prozatím popsaných demonstračních příkladů určených pro MicroPython běžící na čipech s architekturou Cortex-M0+, popř. Cortex-M3/M4 (a otestovaných na RP2040) byly uloženy do repositáře dostupného na adrese https://github.com/tisnik/most-popular-python-libs:
20. Odkazy na Internetu
- Online ARM converter
https://armconverter.com/?disasm - Fast Filters for the Pyboard
https://github.com/peterhinch/micropython-filters - How to load 32 bit constant from assembler with @micropython.asm_thumb
https://forum.micropython.org/viewtopic.php?f=21&t=12931&sid=25de8871fa9cfcf8cafb6318f9d8ba3a - Pi pico, micropython.asm_thumb: ADR Rd, <label> and LDR Rd, <label> not implemented?
https://github.com/orgs/micropython/discussions/12257 - MicroPython documentation
https://docs.micropython.org/en/latest/index.html - Inline assembler for Thumb2 architectures
https://docs.micropython.org/en/latest/reference/asm_thumb2_index.html - Inline assembler in MicroPython
https://docs.micropython.org/en/latest/pyboard/tutorial/assembler.html#pyboard-tutorial-assembler - MCU market turns to 32-bits and ARM
http://www.eetimes.com/document.asp?doc_id=1280803 - Cortex-M0 Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0.php - Cortex-M0+ Processor (ARM Holdings)
http://www.arm.com/products/processors/cortex-m/cortex-m0plus.php - ARM Processors in a Mixed Signal World
http://www.eeweb.com/blog/arm/arm-processors-in-a-mixed-signal-world - RISCové mikroprocesory s komprimovanými instrukčními sadami
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami/ - RISCové mikroprocesory s komprimovanými instrukčními sadami (2)
https://www.root.cz/clanky/riscove-mikroprocesory-s-komprimovanymi-instrukcnimi-sadami-2/ - ARM Architecture (Wikipedia)
https://en.wikipedia.org/wiki/ARM_architecture - Cortex-M0 (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M0 - Cortex-M0+ (Wikipedia)
https://en.wikipedia.org/wiki/ARM_Cortex-M#Cortex-M0.2B - Improving ARM Code Density and Performance
New Thumb Extensions to the ARM Architecture Richard Phelan - The ARM Processor Architecture
http://www.arm.com/products/processors/technologies/instruction-set-architectures.php - Thumb-2 instruction set
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344c/Beiiegaf.html - Introduction to ARM thumb
http://www.eetimes.com/discussion/other/4024632/Introduction-to-ARM-thumb - ARM, Thumb, and ThumbEE instruction sets
http://www.keil.com/support/man/docs/armasm/armasm_CEGBEIJB.htm - An Introduction to ARM Assembly Language
http://dev.emcelettronica.com/introduction-to-arm-assembly-language - Processors – ARM
http://www.arm.com/products/processors/index.php - The ARM Instruction Set
http://simplemachines.it/doc/arm_inst.pdf - The Thumb instruction set
http://apt.cs.manchester.ac.uk/ftp/pub/apt/peve/PEVE05/Slides/05_Thumb.pdf - Why Learn Assembly Language?
http://www.codeproject.com/Articles/89460/Why-Learn-Assembly-Language - Is Assembly still relevant?
http://programmers.stackexchange.com/questions/95836/is-assembly-still-relevant - Why Learning Assembly Language Is Still a Good Idea
http://www.onlamp.com/pub/a/onlamp/2004/05/06/writegreatcode.html - Assembly language today
http://beust.com/weblog/2004/06/23/assembly-language-today/ - Assembler: Význam assembleru dnes
http://www.builder.cz/rubriky/assembler/vyznam-assembleru-dnes-155960cz - Assembler pod Linuxem
http://phoenix.inf.upol.cz/linux/prog/asm.html - AT&T Syntax versus Intel Syntax
https://www.sourceware.org/binutils/docs-2.12/as.info/i386-Syntax.html - Linux Assembly website
http://asm.sourceforge.net/