Já se přiznám, že mě systém v této instrukční sadě překvapil. Přijde mi to jako velmi, velmi zajímavé - princip je krásně jednoduchý a jednotný. Na druhou stranu asi by nebylo triviální v tom psát přímo, muselo by se minimálně přes makroassembler, protože v té záplavě LD a MV něco číst musí být velmi nepohodlné. Makra JMP, RTI, SUB apod. by čitelnosti hodně napomohla, myslím.
Jak by se např. kopíroval blok paměti o dané délce? Např. na 6502 byl adresní mód zákl. adresa plus registr X. Takže stačilo naplnit X požadovanou délkou, dekrementovat X a smyčka na zero flag už fungovala "sama". Tady sice krásně můžu inkrementovat nebo dekrementovat přímo registry obsahující src a dst, ale zase přijdu o tu "indikaci nulou". To budu muset použít další registr ukazatel konce a v každém kroku smyčky to s ním porovnávat??
Na druhou stranu asi by nebylo triviální v tom psát přímo, muselo by se minimálně přes makroassembler
O makroassembleru bude v dalších pokračováních. Nebo se podívejte na GitHub: definice základních maker (pseudoinstrukcí).
Jak by se např. kopíroval blok paměti o dané délce?
Asi jsem kopírování bloků paměti nepotřeboval, ale podprogram memset_b je podobný algoritmus. Kdyby v r2 byla adresa zdrojového bloku, tak by stačilo do cyklu přidat instrukce LD a INC1:
memcpy: .testz r1 .retz ld r10, r2 stob r0, r10 inc1 r2, r2 inc1 r0, r0 dec1 r1, r1 .jmp memset_b
Díky. A na ten další díl se opravdu těším. Nechcete je vydávat častěji? :)
Takhle musím "podvádět" a koukat napřed na github :) Moc pěkně jsou tam věci zdokumentované, všechna čest. Bohužel jsem zatím neměl dost času to pročíst pořádně, tak se právě těším na ten "sumář" vždy tady v seriálu.
Dál jsou moje úvahy, neberte to jako kritiku, ale jako důkaz toho, jak zajímavě jste to všechno podal, že jste mě přiměl se nad tím trochu víc zamyslet :)
S těmi makry jsem to tak nějak myslel. Jen tedy nevím, jestli implementace všeho je korektní. Např. ten nop pomocí mv nemění náhodou flagy?
Druhý problém je pak s disassemblerem: musí být mnohem složitější. Pokud bych po něm chtěl i převod na ta makra, tak už možná bude tak složitý, že se nevejde ani do celé paměti toho počítače (32kB) a bude muset běhat jen externě na nějakém "větším bráškovi". Zatímco na osmibitech zabíral tak neznatelně paměti, že se s většinou programů vešel do RAM naráz a umožňoval jejich základní debugging bez pomoci jiného stroje.
Kopírování paměti jste nepotřeboval? Asi jste na tom ještě nedělal nějak více grafiku, že? :) A opět: takto nějak jsem si tu smyčku představoval, a přijde mi to trošku jako "plýtvání" - možná je to ale tím, jak jsem "odkojený" M6502: kdybych na takovouto operaci potřeboval čtyři 16bitové registry, tak nevím, co bych tehdy dělal, krom pláče v koutě :) Musely mi stačit dva 8bitové, a z toho ani jeden plnohodnotný (univerzální). A ve smyčce jen tři instrukce místo sedmi. Ale to je právě asi tím úhlem pohledu: když o registry není nouze a jsou "přirozeně" 16bitové, tak se asi programuje jiným stylem...
Např. ten nop pomocí mv nemění náhodou flagy?
Instrukce MV (a podobně např. EXCH, LD, STO) nemění flagy. Z tohoto pohledu může být problematické makro set0 implementované pomocí XOR, které flagy nastavuje.
Druhý problém je pak s disassemblerem
Disassembler jsem vůbec neřešil.
...a bude muset běhat jen externě na nějakém "větším bráškovi"
Celé to mám postavené tak, že assembler a debugger běží vedle na Linuxu a komunikují přes sériový port. Navíc debugger je momentálně jediný způsob, jak do MB50 nahrát a spustit program.
Asi jste na tom ještě nedělal nějak více grafiku, že?
Ne, dělal jsem jen zobrazování textu. Tam je kopírování specifické, protože se kopíruje blok 8 B z fontu do videopaměti s krokem 32 B.
přijde mi to trošku jako "plýtvání" - možná je to ale tím, jak jsem "odkojený" M6502
Já jsem začínal na Z80. Ta má sice registrů víc, ale i tak je potřeba hodně přemýšlet a šetřit. Ale zrovna na kopírování bloků paměti tam byly speciální instrukce. Tady jsem snažil zjednodušit programování právě tím, že bude k dispozici hodně registrů a všechny s plnou šířkou slova.
Dál jsou moje úvahy, neberte to jako kritiku, ale jako důkaz toho, jak zajímavě jste to všechno podal, že jste mě přiměl se nad tím trochu víc zamyslet
Já (konstruktivní) kritiku vítám. A Vaše úvahy, dotazy a připomínky jdou k jádru věci. Já jsem nad návrhem taky docela dlouho přemýšlel, než jsem dospěl k řešení, které mi připadalo rozumné. To ale neznamená, že je nejlepší nebo jediné možné. Je tam dost kompromisů a taky občas pokusy udělat věci jinak, než je obvyklé. Určitě je tam i dost chyb, o některých vím, o jiných ne. Např. už minule se tu diskutovalo, že registr ia neměl být obecný registr, ale CSR. A taky už bylo zmíněno, že při pokusu o implementaci pipeliningu nebo dalších optimalizací známých z výkonnějších CPU by se nejspíš celý návrh na různých místech rozsypal.
Těch věcí tam bude víc:
- Třeba při programování na vyšší úrovni bude běžný zápis a čtení ze struktury. Na to je důležitá relativní adresace. Pokud mi něco neuniklo, tak aktuálně by bylo potřeba LDIS, ADD a LD/ST, místo jednoho LD/ST s relativním offsetem. Ta by taky eliminovala potřebu LDIS/DTSTO - ten příklad se stack se v jazycích vyšší úrovně stejně nepoužívá.
- Malé konstanty pro operace obecně. I když je třeba říct, že jednoduchý LDIS (či ekvivalent) výše tu potřebu trochu eliminuje.
- Na rozšíření by se dal eliminovat část conditions u FLAGS - reálně se používají stejně jen čtyři (? - carry, zero, overflow, ...).
Ale chápu to tak, že cílem bylo postavit jednoduchý CPU, který bude relativně jednoduché implementovat, což se asi podařilo.
Třeba při programování na vyšší úrovni bude běžný zápis a čtení ze struktury.
Tohle už je za hranicí mého projektu. Kdybych takové operace chtěl dělat, ať už v assembleru nebo v kompilátoru nějakého vyššího programovacího jazyka, tak bych si na ty tři instrukce (LDIS+ADD+LD/STO) vyrobil makro. Teprve, kdybych to potřeboval zrychlit, tak bych uvažoval o přidání dalších instrukcí.
ten příklad se stack se v jazycích vyšší úrovně stejně nepoužívá
Tohle jsem nepochopil: tvrdíte, že ve vyšších jazycích se nepoužívá stack?
Malé konstanty pro operace obecně.
Chtěl jsem udržet jednotný dvoubajtový formát instrukcí obsahující opcode a dva registry. Tam už na konstanty není místo.
Na rozšíření by se dal eliminovat část conditions u FLAGS
Redukce počtu flagů, které lze testovat z 8 na 4 by v kódu instrukce ušetřila jeden bit, navíc jen v podmíněných instrukcích.
Ale chápu to tak, že cílem bylo postavit jednoduchý CPU
Přesně tak. A každá další instrukce znamená vymyslet, jak se má chovat, přidat její popis do README, implementovat v CPU (přidat do ALU a/nebo do konečného automatu řadiče) a přidat do assembleru.
Předpokládám, že je to míněno tak, že se k zásobníku nepřistupuje přes operace push a pop. Ono tím, jak vytvářejí řetězce závislostí instrukcí, tak jsou vysloveně problematické na out-of-order zpracování. Vždy se tedy zásobníkem posunuje o větší inkrement s rezervou na přípravu argumentů funkcí. Na x86_64 je podle System V Application Binary Interface AMD64 Architecture Processor Supplement je třeba povinné zarovnání stacku na 16 byte a dokonce 128 byte nad, tedy adresami pod, koncem stacku je rezervovaný po přípravu argumentům funkcím a lokální proměnné které nepřežijí volání funkce.
Pokud pak není využité alloca (allocate memory that is automatically freed tedy s opuštěním funkce) nebo lokální pole s délkou určenou argumenty, tak se lokální proměnné adresují relativně přímo proti zásobníku. Pokud se předpokládá, že se ukazatel zásobníku bude relativně k zásobníkovému rámci funkce pohybovat nebo jsou standardní rámce vyžadované pro snadnější ladění, tak je na začátku na pozici zásobníku nad (pod) návratovou adresu z funkce uložený předchozí stav frame pointeru (fp), ten pak ukazuje na tuto pozici a adresuje se proti němu (na x86 registr bottom pointer - bp). Na RISC architekturách se pak návratová adresa z funkce neukládá instrukcí call (/bsr/jal jak jí kdo pojmenuje) na zásobník, ale do k tomu určenému registru - return address ra, link registr lr atd...
Takže relativní adresování v nějakém rozumném rozsahu proti fp a sp je nutností. Přitom i při přístupu přes ukazatele do struktur je to také potřeba. RISC-V rozsah offsetů nechává na -2048 až +2047.
Dokonce i v 16 bitovém, compressed RIC-V "C" kódování jsou obsažené instrukce pro zarovaná čtení a zápisy přes registr sp pro načtení 32-bit, 64-bit (na 128-bitové verzi i pro 128 bit) hodnoty dané dokonce 6 bitovým ofsetem k sp, který je škálovaný délkou přístupu. Přes subset dalších 8 registrů (s0, s1, a0 až a5) je pak možné v komresené sadě přistupovat jen s offsetem 5 bitů. Pro běžnou aritmetiku je pak nabízený šestibitový immediate. Je celkem zajímavé, co do 16 bitů kódování dokázali dostat, ale na druhou stranu kompletní sada to není, je potřeba mít k dispozici i 32-bit kódované instrukce, třena pro řízení stavu a přístupy k CSR.
Díky za upřesnění, s tím stack jsem to skutečně myslel tak, že se alokuje jednorázově.
Zaujal mě ten offset u RISC-V - skoro bych zvážil udělat ten rozsah asymetricky. Typicky se přistupuje ke kladným offsetům a záporné možná v rosahu 1-2 prvků (krát jejich velikost). U těch 12-bitových je to možná jedno, ale v compressed jenom 5 bitů bude často limitující. Nebo udělat compressed pouze kladné (možná už to tak je?)...
Je to tak, offset u komprimovaných instrukcí pracujících proti zásobníku je zero extended
C.LWSP loads a 32-bit value from memory into register rd. It computes an effective address by adding the zero-extended offset, scaled by 4, to the stack pointer, x2. It expands to lw rd, offset(x2). C.LWSP is only valid when rd≠x0 the code points with rd=x0 are reserved.
Zásobník je pak při vstupu do podprogramu povinně zarovnaný na 128 bitů, tedy 16 byte. Přitom prostor nad (v adresách pod) sp funkci nepatří. U x86 to asi i celkem dost pro to offsetování s krátkou hodnotou řeší tour red-zone nad stackem. Ale to je dost komplikací jinde.
Zajímavé jsou pak na optimalizaci délky kódu navržené instrukce v rozšíření Zcmp: push, pop and paired register atomic move. I když zde to již trochu přestává být RISC architektura. Jedná se o kompromis pro embedded svět, kde se každý byte Flash počítá.