Gnistan
Som säkerligen många andra som läst programmering (särskilt på lågnivå), så började jag undra hur fungerar en dator egentligen. Och sen ser man videor på internet om folk som skapar spel för NES, och jag tänker att det finns en charm med att arbeta med såpass (i dagen mått) begränsad utrustning. Därav så skapade jag Cove.
Vad är Cove?
Cove är en komplett 16-bitars fantasikonsol med 64 KiB minne, en 128x128 pixels skärm med stöd för 16 olika färger samtidigt, 4 ljudkanaler. Och allt programmeras i Coves egna assembly (Cove assembly), som jag skulle säga är primärt inspirerat av ARM.
Ursprungligen var idén för Cove att bara vara en utmaning, kan jag designa en CPU? Tanken var alltid ursprungligen att jag skulle stödja ett högnivå-språk, möjligen ett BASIC liknande som jag skulle designa själv, men allteftersom började jag förstå att målet med detta projekt var inte ett högnivå-språk, det var assembly.
Hello World som förändrade mitt perspektiv
I samband med en kurs i högskolan som hette datorteknik så fick man lära sig lite om hur en dator och processor fungerar, ALU, pipelining, IEEE 754, och så klart lite grundläggande assembly, och i samband med det var väl första gången jag såg “Hello world!” i assembly.
section .data
msg db "Hello, World!", 0xA
len equ $-msg
section .text
global _start
_start:
; write(1, msg, len)
mov rax, 1 ; sys_write
mov rdi, 1 ; file descriptor (stdout)
mov rsi, msg ; address of the string
mov rdx, len ; length of the string
syscall
; exit(0)
mov rax, 60 ; sys_exit
xor rdi, rdi ; status 0
syscall
När jag första gången såg den här koden så tänkte jag, okej jag kan typ förstå
hur det här fungerar, men det var section .data som förvirrade mig. Nu från
min egna 16-bitars dator förstår jag precis varför nasm ser ut så här. För
jämförelse så är sättet du skriver “Hello world!” i Cove assembly följande.
.org 0x0000
.dw start ; reset vector, sets the entry point of the program
.org 0x0050 ; by convention programs should start at 0x0050, lower addresses are reserved for zero page and interrupt vectors
msg: .db "Hello world!\n" ; the actual text string to be printed now lives at the address of msg
start:
MOV R0, #0 ; SYS_DEBUG, prints to console
MOV R1, msg ; start address of the string
MOV R2, #13 ; length of the string to be printed
SYS ; runs the syscall
MOV R0, #13 ; SYS_EXIT, to cleanly exit the program
SYS
Saken att ta ifrån sig av detta är att även Cove assembly delar mycket av
logiken, .db skriver efterföljande som bytes direkt in i binaryn, MOV fungerar
nästan exakt likadant, men jag använder andra namn för registren, och det är
andra värden för de olika syscalls som finns, men annars finns den grundläggande
strukturen där.
Designmål
Mitt mål för Cove var att efterlikna utvecklingsprocessen för en gammal spelkonsol som NES, men samtidigt bidra med något mer användarvänliga verktyg. Därav alla syscalls som är till för att enkelt rita ut till skärmen.
När jag började först att Cove kommer vara konsol som du programmerar i assembly
kom jag till en intressant insikt, jag vill uppmuntra användarna till att tänka
på korrekt sätt även för denna fantasikonsol, därav introducerade jag
clock-cycles, en sak som man måste vara medveten av om man programmerar riktig
assembly. Till exempel så har jag gjort så att MUL kostar 3 cyklar medan SHL
bara kostar 1. Detta lär dem att använda shifts när de multiplicerar/dividerar
med exponenter av 2, en optimering man använder även för riktiga datorer.
Men, jag ville även uppmuntra till “ren” kod, därav gjorde jag instruktioner som
CALL och RET billigare än vad de realistiskt bör vara, för att inte straffa
användare för att använda funktioner.
Kort och gott, Cove är en idealiserad spelkonsol som vill fånga känslan av gamla spelkonsoler men som samtidigt gör vissa val som från ett hårdvara-perspektiv är dåliga, men som gör den enklare att programmera för.
Manualen på 2100+ rader
Det här var en av de roligaste delarna av projektet, assembly är inte lika
enkelt som Python eller ens C, jag kände att det behövdes en manual som gick
igenom allt man behöver veta och som man snabbt kan kolla tillbaks på för att
kolla vart låg paletten i minnet, vilket nummer var SYS_SPR, etc…
Med manualen så ville jag fånga känslan av att läsa igenom manualen till, till exempel Commodore 64. Och, jag må säga, jag är så nöjd med den, särskilt om man faktiskt skriver ut den, då får man verkligen den där autentiska känslan.
Vad som kommer i den här serien
I den här serien tänker jag gå igenom Cove och varför jag gjorde de val jag gjorde, så se fram emot följande delar.
- Översikt - Helhetsbilden, varför assembly, och filosofin bakom Coves design
- ISA - En djupdykning av ISA:n och varför den ser ut som den gör
- Assembler - En koll på assemblern och hur den fungerar
- Interrupts - Vad är det, hur fungerar det, och vad används det till
- Debugger - Hur du använder den för att felsöka ett program
- Tillbakablick - Vad lärde jag mig, vad skulle jag göra annorlunda
Användning av AI
Med anledning av det klimat som finns kring AI just nu så är det bäst att ta upp det på en gång, jag har under utvecklingen av Cove använt LLMs för att hjälpa med att hitta lösningar när man satt fast och som ett bollplank för att diskutera designval. Men jag förstår samtliga delar av kodbasen och är den som gjorde alla designval.
Prova själv
Du kan se all source code och instruktioner för hur du kör Cove här https://git.jimmy-b.se/JimBer110/Cove