Assembler na Raspberry Pico
a měření časování instrukcí
Přestože je pro Raspberry Pico k dipozici dostatek dokumentací, soubor instrukcí assembleru může být matoucí a nejasný. V tomto článku si přehledně shrneme instrukce procesoru Raspberry Pico a změříme jejich časování.
Procesor RP2040, použitý v Raspberry Pico, je dvoujádrový Cortex M0+ ve verzi ARMv6-M, běžící standardně na 125 MHz (zaručovaná frekvence 133 MHz). Sada instrukcí je Thumb-1, s několika doplněnými instrukcemi z Thumb-2. Nenajdete tu tedy 32-bitové ARM instrukce a bohužel ani instrukci podmíněného bloku if-then IT (ta se vyskytuje až u verze ARMv6T2).
Původní procesory ARM obsahují 32-bitovou sadu instrukcí ARM (každá instrukce má 32 bitů). Z ARM sady instrukcí byla odvozena omezená sada 16-bitových instrukcí Thumb-1, která je podmnožinou instrukcí ARM a byla určena pro menší procesory. Podporuje-li některý procesor obě sady intrukcí, lze mezi nimi přepínat nastavením bitu 0 registru PC. Nastavením bitu 0 se interpretují Thumb instrukce, vynulováním bitu 0 se použije ARM sada instrukcí.
Procesor Raspberry Pico podporuje pouze Thumb-1 sadu instrukcí. Bit 0 registru PC proto musí být vždy nastaven. Pokus o přepnutí na ARM sadu instrukcí (skokem na adresu s vynulovaným bitem 0) skončí hard-faultem.
Všechny registry jsou 32-bitové.
R0..R12 ... registry pro všeobecné použití. Ve většině instrukcí Thumb-1 jsou podporované jen registry R0..R7.
Některé registry jsou překladačem C použity pro speciální účely. Registr R12 je Intra Procedure Call Scratch Register (IPC). Registr R11 se používá jako Frame Pointer FP. Registr R10 je Stack Limit SL. Registr R9 se používá jako Static Base SB.
R13 (SP) ... Stack Pointer. Procesor disponuje dvěma ukazateli zásobníku. MSP je Main stack. Slouží jako hlavní ukazatel zásobníku při startu procesoru a v přerušení. PSP je Process stack, používá se pro procesy v multitaskovém systému. Aktuální ukazatel zásobníku, přiřazený k R13 (SP), se volí v CONTROL registru. Ukazatel zásobníku ukazuje na spodní okraj zásobníku. Při uložení registru do zásobníku se hodnota ukazatele SP sníží.
R14 (LR) ... Link Register. Během volání podprogramů v procesorech ARM se návratová adresa neuchovává do zásobníku, ale do link registru LR. Při návratu z podprogramu se provede skok zpět na obsah link registru. To má výhodu v rychlejším provedení podprogramu, protože se nemusí manipulovat se zásobníkem. Ovšem jsou-li volány podprogramy do větší hloubky, je nutné zajistit uchování a obnovení registru LR. Většinou lze využít volný registr a tak i v takovém případě může být volání podprogramu rychlé.
R15 (PC) ... Program Counter. Programový čítač PC ukazuje na aktuální adresu v programové paměti. Instrukce v Thumb módu musí být zarovnané na násobek 2 (tj. sudá adresa), protože každá instrukce zabírá 2 nebo 4 bajty. Nejnižší nevyužitý bit registru PC je proto využit k indikaci, zda se při zpracování programu používá ARM sada instrukcí (bit 0 je vynulován) nebo Thumb sada instrukcí (bit 0 je nastaven). Přečtete-li v Pico registr PC, bude mít bit 0 vždy nastaven.
PSR ... Program Status Register je speciální registr, obsahující stav procesoru. Přistupuje se k němu přes 3 podregistry: APSR (Application Program Status Register, obsahuje flagy), IPSR (Interrupt Program Status Register) a EPSR (Execution Program Status Register).
PRIMASK ... je registr maskování přerušení. V Pico obsahuje pouze nejnižší bit 0, indikující globální zákaz přerušení.
CONTROL ... je registr pro řízení procesoru. Nastavuje privilegovaný mód a mód ukazatele zásobníku.
Při volání funkce v gcc obsahují registry R0 až R3 vstupní argumenty funkce a také navrácenou hodnotu z funkce. Je-li potřeba více vstupních argumentů, předají se další argumenty přes zásobník.
Volaná funkce musí zachovat obsahy registrů R4 až R11 a registr R13 (SP). Naopak funkce nemusí zachovat registry R0 až 3, R12 (IPC) a R14 (LR). Obzvláště na registr R14 (LR) je potřeba dát si pozor a jeho obsah před voláním funkce mít uchovaný, aby byl možný návrat z podprogramu zpět.
Registr APSR (Application Program Status Register) obsahuje ve svých nejvyšších 4 bitech flagy výsledku operací.
N, bit 31 ... Negace. Flag je kopií nejvyššího bitu 31 registru s výsledkem operace a indikuje negativní číslo.
Z, bit 30 ... Zero, nula. Je-li flag Z nastaven na 1, indikuje nulový výsledek operace (resp. shodu při operaci porovnání).
C, bit 29 ... Carry, přenos. Flag C nastavený na 1 indikuje přetečení výsledku neznaménkové operace.
V, bit 28 ... Overflow, přetečení. Flag V nastavený na 1 indikuje přetečení výsledku znaménkové operace.
Nad flagy C a V se pozastavíme blíže. Flagy si představte jako rozšíření registrů o 1 nejvyšší bit. Přenos C je nastaven tehdy, dojde-li k nastavení rozšiřujícího bitu. Při součtu spolu se vstupním přenosem C je rozhodující jen konečný výsledek, ne mezikroky. Příklad:
[0] 0xFFFFFFFE ... první operand
+ [0] 0x00000001 ... druhý operand, přičtením by byl
mezivýsledek [0] 0xFFFFFFFF
+ carry C=1 ... přičtení vstupního přenosu
Výsledkem je [1] 0x00000000. Rozšiřující bit je nastavený, výstupní flag C má tedy hodnotu 1.
Při odčítání je situace jiná. V procesorech ARM se odčítání realizuje jako přičtení invertované hodnoty (NOT) plus 1, tj, přičtení záporného operandu. Je-li flag C nastaven, neznamená to tedy podtečení výsledku pod 0 (tj. flag "borrow"), ale invertovaný příznak podtečení. Příklad:
[0] 0x00000002 ... první operand
- [0] 0x00000001 ... druhý operand
Druhý operand při odčítání nejdříve bitově invertujeme a změníme operaci na sčítání.
+ [0] 0xFFFFFFFE ... invertovaný druhý operand, přičtením by byl mezivýsledek [1] 0x00000000.
Ještě musíme přičíst 1, abychom dosáhli negace druhého operandu.
+ [0] 0x00000001 ... výsledkem je [1] 0x00000001
Jak vidíte, výsledkem operace je sice správná hodnota 1, ale byl nastaven příznak C. Na C musíme u odčítání hledět jako na invertovaný příznak Borrow (podtečení). Přičítáme-li v operaci ještě vstupní přenos C, děláme to stejně jako u přičítání a nevadí že má význam invertovaného podtečení.
V chápání flagu overflow V si můžeme opět pomoci rozšiřujícím bitem, kterým rozšíříme (zduplikujeme) znaménko operandu. Flag přetečení V bude nastaven, dojde-li k přetečení výsledku mimo rozsah - což bude tehdy, pokud se rozšiřující bit bude lišit od znaménkového bitu. Příklad:
[1] 0x80000000 ... první operand, to je maximální záporné číslo -2147483648
+ [1] 0xFFFFFFFF ... přičteme druhý operand, číslo -1
Výsledkem je [1] 0x7FFFFFFF, tj. číslo -2147483649. Z rozdílnosti rozšiřujícího a znaménkového bitu vidíme, že došlo k přetečení výsledku mimo rozsah znaménkového čísla a proto bude nastaven příznak přetečení V.
U každé instrukce je uveden počet hodinových taktů T a délka instrukce v ns, vztahující se k Raspberry Pico běžící na implicitních 125 MHz. U instrukcí Load a Store závisí doba na přistupované paměti. Při přístupu k běžné RAM je doba typicky 2 takty. Při přístupu k SIO registrům (GPIO) je doba přístupu 1 takt. Při přístupu k některým registrům a k externí Flash může být doba delší, kvůli přídavným wait-state.
Je-li na konci názvu instrukce uveden znak "S" znamená to, že instrukce ovlivňuje flagy (netýká se instrukcí CMP a TST, které vždy ovlivní flagy).
Není-li uvedeno jinak, instrukce podporuje pouze registry R0..R7. Rozšířenou sadu registrů R0..R15 podporují jen instrukce: MOV Rd,Rm, MOV PC,Rm, ADD Rd,Rm, ADD PC,Rm, CMP Rn,Rm, BX Rm, BLX Rm, MRS Rd,<reg> a MSR <reg>,Rd.
Subtract instrukce používají Carry ve významu inverze podtečení Borrow.
Při zápisu instrukcí assembleru se před číselnou konstantou uvádí znak #.
Instrukce nesmí přistupovat na nezarovnanou adresu (jinak bude hard-fault) - tj. instrukce pro 16-bitový přístup nesmí přistoupit na lichou adresu a instrukce s 32-bitovým přístupem musí přístupovat jen na adresu zarovnanou na 32 bitů.
V instrukcích se označuje 32 bitů jako word, 16 bitů je half-word a 8 bitů byte.
Podmínky podmíněného skoku b<cc>:
Skupina | Instrukce | Časování T | Flagy | Význam | Poznámka |
---|---|---|---|---|---|
Move | MOVS Rd,#<imm> | 1 (8ns) | N Z - - | Rd <- imm | imm je konstanta 0..255 |
MOVS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rm | ||
MOV Rd,Rm | 1 (8ns) | - - - - | Rd <- Rm | Registry R0..R15. Je-li Rd=PC, Rm nesmí být PC. Je-li Rd=SP, Rm nesmí být SP. | |
MOV PC,Rm | 2 (16ns) | - - - - | PC <- Rm | Registry R0..R14. Provede skok na adresu v registru Rm. Bit 0 adresy se ignoruje. | |
Add | ADDS Rd,Rn,#<imm> | 1 (8ns) | N Z C V | Rd <- Rn + imm | imm je konstanta 0..7 |
ADDS Rd,Rn,Rm | 1 (8ns) | N Z C V | Rd <- Rn + Rm | ||
ADD Rd,Rm | 1 (8ns) | - - - - | Rd <- Rd + Rm | Registry R0..R14 | |
ADD PC,Rm | 2 (16ns) | - - - - | PC <- PC + Rm | Registry R0..R14. Provede skok relativně k následující adrese + 2 (např. při #0 přeskočí 1 následující instrukci). Bit 0 výsledné adresy se ignoruje. | |
ADDS Rd,#<imm> | 1 (8ns) | N Z C V | Rd <- Rd + imm | imm je konstanta 0..255 | |
ADCS Rd,Rm | 1 (8ns) | N Z C V | Rd <- Rd + Rm + C | součet s carry | |
ADD SP,#<imm> | 1 (8ns) | - - - - | SP <- SP + imm | imm je konstanta 0..508, musí být násobek 4 | |
ADD Rd,Sp,#<imm> | 1 (8ns) | - - - - | Rd <- SP + imm | imm je konstanta 0..1020, musí být násobek 4 | |
ADR Rd,<label> | 1 (8ns) | - - - - | Rd <- label | label je v rozsahu PC..PC+1020. Adresa musí být zarovnána na 32 bitů (tj. násobek 4). Při překladu se nahradí instrukcí ADD Rd,PC,#<imm>. | |
ADD Rd,PC,#<imm> | 1 (8 ns) | - - - - | Rd <- PC + imm | imm je konstanta 0..1020, musí být násobek 4. PC má hodnotu následující adresy + 2 zarovnané dolů na násobek 4. | |
Subtract | SUBS Rd,Rn,Rm | 1 (8ns) | N Z C V | Rd <- Rn - Rm | |
SUBS Rd,Rn,#<imm> | 1 (8ns) | N Z C V | Rd <- Rn - imm | imm je konstanta 0..7 | |
SUBS Rd,#<imm> | 1 (8ns) | N Z C V | Rd <- Rd - imm | imm je konstanta 0..255 | |
SBCS Rd,Rm | 1 (8ns) | N Z C V | Rd <- Rd - Rm - not C | ||
SUB SP,#<imm> | 1 (8ns) | - - - - | SP <- SP - imm | imm je konstanta 0..508, musí být násobek 4 | |
Negate | NEGS Rd,Rn | 1 (8ns) | N Z C V | Rd <- 0 - Rn | negace, synonymum instrukce RSBS Rd,Rn,#0 |
RSBS Rd,Rn,#0 | 1 (8ns) | N Z C V | Rd <- 0 - Rn | negace, synonymum instrukce NEGS Rd,Rn | |
Multiply | MULS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rd * Rm | Flagy C a V zůstanou nezměněné (u starších verzí ARMů jsou nedefinované) |
Compare | CMP Rn,Rm | 1 (8ns) | N Z C V | Rn - Rm ? | Registry R0..R15. |
CMPN Rn,Rm | 1 (8ns) | N Z C V | Rn + Rm ? | porovnání s negací Rm | |
CMP Rn,#<imm> | 1 (8ns) | N Z C V | Rn - imm ? | imm je konstanta 0..255 | |
Logical | ANDS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rd and Rm | |
EORS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rd xor Rm | exclusive or | |
ORRS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rd or Rm | ||
BICS Rd,Rm | 1 (8ns) | N Z - - | Rd <- Rd and not Rm | bit clear | |
MVNS Rd,Rm | 1 (8ns) | N Z - - | Rd <- not Rm | move not | |
TST Rn,Rm | 1 (8ns) | N Z - - | Rn and Rm ? | AND test | |
Shift | LSLS Rd,Rm,#<shift> | 1 (8ns) | N Z C - | Rd <- Rm << shift | Logický posun doleva. shift je konstanta 0..31. Do dolních bitů se vloží 0. Carry je přenos z posledního nejvyššího bitu. |
LSLS Rd,Rs | 1 (8ns) | N Z C - | Rd <- Rd << Rs | Logický posun doleva. Bity 0..7 v Rs obsahují počet posunů. Do dolních bitů se vloží 0. Carry je přenos z posledního nejvyššího bitu. | |
LSRS Rd,Rm,#<shift> | 1 (8ns) | N Z C - | Rd <- Rm >> shift | Logický posun doprava. shift je konstanta 0..31. Do horních bitů se vloží 0. Carry je přenos z posledního nejnižšího bitu. | |
LSRS Rd,Rs | 1 (8ns) | N Z C - | Rd <- Rd >> Rs | Logický posun doprava. Bity 0..7 v Rs obsahují počet posunů. Do horních bitů se vloží 0. Carry je přenos z posledního nejnižšího bitu. | |
ASRS Rd,Rm,#<shift> | 1 (8ns) | N Z C - | Rd <- Rm >> shift | Aritmetický posun doprava. shift je konstanta 0..31. Bit 31 (znaménko) se rozmnoží do horních bitů. Carry je přenos z posledního nejnižšího bitu. | |
ASRS Rd,Rs | 1 (8ns) | N Z C - | Rd <- Rd >> Rs | Aritmetický posun doprava. Bity 0..7 v Rs obsahují počet posunů. Bit 31 (znaménko) se rozmnoží do horních bitů. Carry je přenos z posledního nejnižšího bitu. | |
Rotate | RORS Rd,Rs | 1 (8ns) | N Z C - | Rd <- Rd >> Rs, wrap | Rotace doprava. Bity 0..7 v Rs obsahují počet posunů. Uvolněné nižší bity se přenesou do horních bitů. Carry je přenos z posledního nejnižšího bitu. |
Load | LDR Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + imm] | imm je offset 0..124, musí být násobek 4. |
LDRH Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + imm] [16] | imm je offset 0..62, násobek 2. Načte 16 bitů, do horních 16 bitů doplní 0. | |
LDRB Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + imm] [8] | imm je offset 0..31. Načte 8 bitů, do horních 24 bitů doplní 0. | |
LDR Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + Rm] | ||
LDRH Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + Rm] [16] | Načte 16 bitů. Do horních 16 bitů doplní 0. | |
LDRSH Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + Rm] [16] | Načte 16 bitů. Horních 16 bitů rozšíří ze znaménkového 15. bitu. | |
LDRB Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + Rm] [8] | Načte 8 bitů. Do horních 24 bitů doplní 0. | |
LDRSB Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd <- [Rn + Rm] [8] | Načte 8 bitů. Horních 24 bitů rozšíří ze znaménkového 7. bitu. | |
LDR Rd,<label> | 1-2 (8-16ns) | - - - - | Rd <- [label] | Načte 32-bitů z uvedené adresy. Adresa musí ležet v rozsahu PC..PC+1020 a musí být zarovnaná na 4 bajty. | |
LDR Rd,[SP,#<imm>] | 1-2 (8-16ns) | - - - - | Rd <- [SP + imm] | imm je konstanta 0..1020, musí být násobek 4. | |
LDM Rn!,{<reglist>} | 1+N (8+N*8ns) |
- - - - | load <reglist> | Načte N registrů podle seznamu reglist, z bázové adresy Rn, vyjma registru Rn. Registr Rn bude po operaci inkrementován. Registry musí být v seznamu uvedené v zestupném pořadí. Jiný název instrukce je LDMIA (Load Multiply and Increment After). | |
LDM Rn,{<reglist>} | 1+N (8+N*8ns) |
- - - - | load <reglist> | Načte N registrů podle seznamu reglist, z bázové adresy Rn, včetně registru Rn. Registr Rn musí být zadán v seznamu registrů. Registry musí být v seznamu uvedené v zestupném pořadí. Jiný název instrukce je LDMIA (Load Multiply and Increment After). | |
Store | STR Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + imm] | imm je offset 0..124, musí být násobek 4. |
STRH Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + imm] [16] | imm je offset 0..62, násobek 2. Uloží 16 bitů. | |
STRB Rd,[Rn,#<imm>] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + imm] [8] | imm je offset 0..31. Uloží 8 bitů. | |
STR Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + Rm] | ||
STRH Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + Rm] [16] | Uloží 16 bitů. | |
STRB Rd,[Rn,Rm] | 1-2 (8-16ns) | - - - - | Rd -> [Rn + Rm] [8] | Uloží 8 bitů. | |
STR Rd,[SP,#<imm>] | 1-2 (8-16ns) | - - - - | Rd -> [SP + imm] | imm je konstanta 0..1020, musí být násobek 4. | |
STM Rn!,{<reglist>} | 1+N (8+N*8ns) |
- - - - | store <reglist> | Uloží N registrů podle seznamu reglist, z bázové adresy Rn, vyjma registru Rn. Registr Rn bude po operaci inkrementován. Registry musí být v seznamu uvedené v zestupném pořadí. Jiný název instrukce je STMIA (Store Multiply and Increment After). | |
Push | PUSH {<reglist>} | 1+N (8+N*8ns) |
- - - - | push <reglist> | Do zásobníku uloží registry podle seznamu. |
PUSH {<reglist>,LR} | 1+N (8+N*8ns) |
- - - - | push <reglist,LR> | Do zásobníku uloží registry podle seznamu, včetně registru LR. | |
Pop | POP {<reglist>} | 1+N (8+N*8ns) |
- - - - | pop <reglist> | Ze zásobníku obnoví registry podle seznamu. |
POP {<reglist>,PC} | 1+N (8+N*8ns) |
- - - - | pop <reglist,PC> | Ze zásobníku obnoví registry podle seznamu, včetně registru PC. Tím provede skok na původní adresu LR. Adresa pro PC musí mít nastavený nejnižší bit 0 (jinak hardfault). | |
Branch | B<cc> <label> | 1-2 (8-16ns) | - - - - | if (cc) then PC <- label | Skok na dané návěští v případě splnění podmínky. Návěští musí být vzdálené max. -256..+254 od následující instrukce + 2. Při splnění podmínky (a provedení skoku) instrukce zabere 2 takty. Při nesplnění (a pokračování) zabere 1 takt. |
B <label> | 2 (16ns) | - - - - | PC <- label | Nepodmíněný skok. Návěští musí být v rozsahu max. +- 2 KB. | |
BL <label> | 3 (24ns) | - - - - | LR <- PC, PC <- label | Úschova následující adresy do LR a skok na návěští. Musí být v rozsahu +- 4 MB. | |
BX Rm | 2 (16ns) | - - - - | PC <- Rm | Nepřímý skok na adresu z registru Rm. Bit 0 adresy musí být nastaven (jinak hardfault). Registry R0..R15. | |
BLX Rm | 2 (16ns) | - - - - | LR <- PC, PC <- Rm | Úschova následující adresy do LR a nepřímý skok na adresu z registru Rm. Bit 0 adresy musí být nastaven (jinak hardfault). Registry R0..R15. | |
Extend | SXTH Rd,Rm | 1 (8ns) | - - - - | Rd <- Rm [16] | Rozšíření 16 dolních bitů z Rm do Rd, horních 16 bitů doplní znaménkovým bitem. |
SXTB Rd,Rm | 1 (8ns) | - - - - | Rd <- Rm [8] | Rozšíření 8 dolních bitů z Rm do Rd, horních 24 bitů doplní znaménkovým bitem. | |
UXTH Rd,Rm | 1 (8ns) | - - - - | Rd <- Rm [16] | Rozšíření 16 dolních bitů z Rm do Rd, horních 16 bitů vynuluje. | |
UXTB Rd,Rm | 1 (8ns) | - - - - | Rd <- Rm [8] | Rozšíření 8 dolních bitů z Rm do Rd, horních 24 bitů vynuluje | |
Reverse | REV Rd,Rm | 1 (8ns) | - - - - | Rd <- revb Rm | Zamění pořadí bajtů registru Rm |
REV16 Rd,Rm | 1 (8ns) | - - - - | Rd <- revh Rm | Zamění pořadí bajtů polovin Rm (b[0] <-> b[1], b[2] <-> b[3]) | |
REVSH Rd,Rm | 1 (8ns) | - - - - | Rd <- revsh Rm | Zamění pořadí bajtů dolní poloviny Rm a rozšíří znaménko na horní polovinu. | |
State | SVC #<imm> | - | - - - - | Supervisor Call, imm je konstanta 0..255 | |
CPSID i | 1 (8ns) | - - - - | Globální zákaz přerušení | ||
CPSIE i | 1 (8ns) | - - - - | Globální povolení přerušení | ||
MRS Rd,<reg> | 3 (24ns) | - - - - | Čtení systémového registru. Registry R0..R15 | ||
MSR <reg>,Rd | 3 (24ns) | - - - - | Zápis systémového registru. Registry R0..R15 | ||
BKPT #<imm> | - | - - - - | Breakpoint. imm je číslo 0..255. | ||
Hint | SEV | 1 (8ns) | - - - - | Odeslání události. | |
WFE | 2 (16ns) | - - - - | Čekání na událost. Čas může být prodloužen dokud nepřijde událost. | ||
WFI | 2 (16ns) | - - - - | Čekání na přerušení. Čas může být prodloužen dokud nepřijde přerušení. | ||
YIELD | 1 (8ns) | - - - - | Indikace v mutlithread, že se provádí task. | ||
NOP | 1 (8ns) | - - - - | Žádná operace | ||
Barriers | ISB | 3 (24ns) | - - - - | Instruction synchronization | |
DMB | 3 (24ns) | - - - - | Data memory barrier | ||
DSB | 3 (24ns) | - - - - | Data synchronization barrier |
Ano, v datasheetu jsou uvedeny takty, jak dlouho která instrukce trvá. Ale přesto, není nad to, si instrukce "osahat" :-), ověřit si uváděná časování a vyzkoušet si funkce instrukcí.
Dobu provádění instrukcí nemůžeme měřit přímo v taktech nebo nanosekundách, na to nemáme dostatečně přesné prostředky. Budeme měřit dobu opakovaného provádění více stejných instrukcí. K tomu potřebujeme přesné hodiny. V Pico máme k dispozici čítač, který se inkrementuje po 1 us a má rozlišení 64 bitů. V Pico SDK jeho hodnotu přečteme funkcí time_us_64. Při měření časového intervalu si uchováme stav čítače jednak na začátku, a jednak na konci. Z rozdílu časů zjistíme uběhlý čas v us.
Zde je důležité si uvědomit, že časovač běží stále dokola, s přetečením 64 bitů. I když časovač přeteče přes horní hranici, stále platí, že odečtením koncového a počátečního stavu zjistíme uběhlý interval. To je výhoda proti čítači, který by měl nastavenou horní hranici na které by se resetoval - s takovým čítačem bychom nemohli měřit čas bez zohlednění přetečení konce. K našemu měření nám dokonce stačí jen dolních 32 bitů. Měření můžeme provést následující smyčkou:
int t = (int)time_us_64();
for (i = 1000; i > 0; i--) fnc();
t = (int)time_us_64() - t;
Měřená funkce se opakuje 1000x. Tím, že změříme v us čas smyčky s opakováním 1000x, získáme výsledné číslo představující dobu jednoho kroku smyčky v ns.
V tomto místě by bylo nepraktické vložit do smyčky přímo testovanou instrukci. Smyčka má nějakou režii, obsahuje několik instrukcí které by nám výsledek zkreslily. Nejdříve vložíme do smyčky prázdnou funkci a změříme základní čas smyčky bez přidaných instrukcí, tento čas budeme později odečítat od naměřeného času.
Jako měřenou funkci použijeme funkci napsanou v assembleru, která bude obsahovat posloupnost 100 měřených instrukcí. Tím se minimalizují vlivy okolních instrukcí potřebných k režii. Instrukce napíšeme ve zdrojovém kódu assembleru. Samozřejmě nemusíme instrukci zapisovat 100x, protože překladač má k dispozici makro příkaz k opakování. Kód pro měření instrukce "MOVS Rd,#<imm>" bude vypadat takto:
.syntax unified .cpu cortex-m0plus .thumb // use 16-bit instructions .section .text // code section .global TestMove1 TestMove1: .rept 100 movs r0,#243 // [1] .endr bx lr // [2]
Můžeme jistě předpokládat, že s jiným registrem a jinou konstantou se časování instrukce nezmění. Instrukce se provede 100x. Je to podprogram, proto v závěru musí být instrukce BX LR, k zajištění návratu z podprogramu. Jak můžeme zjistit z datasheetu (a jak si i později ověříme), instrukce pro návrat BX LR trvá 2 takty. Instrukce pro obsluhu smyčky a zavolání podprogramu trvají dalších 7 taktů. Od naměřeného času jednoho kroku smyčky tedy odečteme celkem 9 taktů. Ale i kdybychom je neodečetli, 100 kroků testované instrukce zajistí, že by se jednalo jen o malou nepřesnost, která se ztratí zaokrouhlením.
Při měření jsme tedy změřili čas celé smyčky v us. Pro 1000 kroků smyčky to představuje čas jednoho kroku smyčky v ns. Údaj přepočteme na takty procesoru vynásobením frekvencí procesoru, tedy clk = ns/1000000*khz (khz je frekvence procesoru v kHz, kterou jsme buď zadali ručně nebo zjistili funkcí frequency_count_khz). Od taktů jedné smyčky odečteme 9 taktů potřebných pro režii. Získaný údaj vydělíme počtem testovaných instrukcí, tedy číslem 100. Tím získáváme čas jedné instrukce v hodinových taktech.
Samozřejmě ne všechny instrukce můžeme jednoduše uvést v testovací smyčce. Instrukce se zavoláním vykonají a proto musíme připravit program tak, aby se instrukce sice provedla, ale aby se zajistil konzistentní chod programu. Např. testujeme-li instrukci PUSH, můžeme ji sice 100x provést, ale v závěru funkce uvedeme instrukci ADD SP,400, která zajistí obnovení ukazatele zásobníku. Můžeme opakovat i posloupnost více instrukcí, je-li to nutné. Např. při testu instrukce LDM musíme opakovaně nastavovat bázový registr pomocí instrukce LDR. Ale to není problém, protože časování pomocné instrukce LDR známe z dříve a tak ji můžeme z výsledku odečíst.
A jak můžeme (možná s překvapením) zjistit, naměřené výsledky časování instrukcí skutečně přesně odpovídají údajům v datasheetu. :-) Zbytečná práce? Myslím že ne, přineslo to mnoho užitečných poznatků.
Zdrojový kód programu pro měření časování instrukcí je ke stažení zde. Obsahuje verzi programu jak pro výstup na UART port, tak výstup na virtuální USB COM port. Součástí balíku je kompletní překladové prostředí pro Windows, je potřeba pouze GCC-ARM překladač.
Tato www stránka je k dispozici ke stažení zde.
Miroslav Němeček