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.

  1. Översikt - Helhetsbilden, varför assembly, och filosofin bakom Coves design
  2. ISA - En djupdykning av ISA:n och varför den ser ut som den gör
  3. Assembler - En koll på assemblern och hur den fungerar
  4. Interrupts - Vad är det, hur fungerar det, och vad används det till
  5. Debugger - Hur du använder den för att felsöka ett program
  6. 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