feat: minimal instruction set for IBM logo program

main
Pau Costa 2024-08-04 12:17:21 +02:00
parent ba28865248
commit 1e06ca350a
No known key found for this signature in database
GPG Key ID: 47F74D9EF51E54FA
5 changed files with 259 additions and 34 deletions

View File

@ -1,39 +1,46 @@
package emulator
import "errors"
import (
"encoding/json"
"errors"
"fmt"
)
type CHIP8State struct {
Memory [4096]byte `json:"memory"`
V [16]byte `json:"v"`
I uint16 `json:"i"`
ProgramCounter uint16 `json:"program_counter"`
StackPointer uint16 `json:"stack_pointer"`
DelayTimer uint8 `json:"delay_timer"`
SoundTimer uint8 `json:"sound_timer"`
Stack [16]uint16 `json:"stack"`
Keypad [16]bool `json:"keypad"`
Graphics [64 * 32]bool `json:"graphics"`
}
type CHIP8 struct {
memory [4096]byte
v [16]byte
i uint16
pc uint16
sp uint16
delayTimer uint8
soundTimer uint8
stack [16]uint16
keypad [16]bool
// To transform the linear Memory to a 2D array for easier drawing,
// having a (x, y) coordinate then (x, y) = graphics[x + (y * 64)]
graphics [64 * 32]bool
opcode uint16
fonts [80]byte
state *CHIP8State
// A flag to check if the screen needs to be redrawn
DrawFlag bool
}
func NewChip8() *CHIP8 {
chip8 := &CHIP8{
memory: [4096]byte{},
v: [16]byte{},
i: 0,
state: &CHIP8State{
Memory: [4096]byte{},
V: [16]byte{},
I: 0,
// The original 0x200 code of the CHIP-8 interpreter was from 0x0 to 0x200,
pc: 0x200,
sp: 0,
delayTimer: 0,
soundTimer: 0,
stack: [16]uint16{},
keypad: [16]bool{},
graphics: [64 * 32]bool{},
opcode: 0,
fonts: [80]byte{},
ProgramCounter: 0x200,
StackPointer: 0,
DelayTimer: 0,
SoundTimer: 0,
Stack: [16]uint16{},
Keypad: [16]bool{},
Graphics: [64 * 32]bool{false},
},
DrawFlag: true,
}
chip8.Initialize()
return chip8
@ -61,17 +68,121 @@ func (c *CHIP8) Initialize() {
// Char map goes from 0x050 to 0x9F
for i, charByte := range charMap {
c.memory[i+0x50] = charByte
c.state.Memory[i+0x50] = charByte
}
}
func (c *CHIP8) StateToJSON() string {
jsonData, err := json.Marshal(c.state)
if err != nil {
fmt.Println("Something went wrong marshalling CHIP8 state to JSON")
return ""
}
return string(jsonData)
}
func (c *CHIP8) LoadROMIntoMemory(dat []byte) error {
if len(dat) > len(c.memory)-0x200 {
if len(dat) > len(c.state.Memory)-0x200 {
return errors.New("ROM is too large to fit into memory")
}
// Roms start from 0x200
for i, datByte := range dat {
c.memory[i+0x200] = datByte
c.state.Memory[i+0x200] = datByte
}
return nil
}
func (c *CHIP8) GetPixelAtCoordinate(x, y int) bool {
return c.state.Graphics[x+y*64]
}
func (c *CHIP8) setPixelAtCoordinate(x, y int, newState bool) {
c.state.Graphics[x+y*64] = newState
}
func (c *CHIP8) EmulateCycle() {
/*
Fetch an instruction from the memory that the PC is currently pointing to.
An instruction is 2 bytes long and the CHIP8 is big endian. So we fetch
two consecutive bytes, and increment the PC by 2.
*/
instruction := uint16(c.state.Memory[c.state.ProgramCounter])<<8 | uint16(c.state.Memory[c.state.ProgramCounter+1])
c.state.ProgramCounter += 2
/*
The first four bits of the instruction represent the type of operation. And
the rest of the bits represent the operands. So we mask the first four bits,
and then use that as an index to our instructionSet. We also mask some extra
nibbles to have them ready.
*/
// X, The second nibble, represents one of the 16 registers
x := (instruction & 0x0F00) >> 8
// Y, The third nibble, represents one of the 16 registers
y := (instruction & 0x00F0) >> 4
// N, The least significant nibble, represents a number between 0 and 0xF, 4 bit number
n := instruction & 0x000F
// NN, The two least significant bytes of the instruction, representing a 16-bit number
nn := byte(instruction & 0x00FF)
// NNN, The three least significant bytes of the instruction, representing a 16-bit number
nnn := instruction & 0x0FFF
maskedInstruction := instruction & 0xF000
if instruction == 0x00E0 {
// Clear the screen
c.state.Graphics = [64 * 32]bool{false}
c.DrawFlag = true
} else if maskedInstruction == 0x1000 {
// Jump to a new location
c.state.ProgramCounter = nnn
} else if maskedInstruction == 0x2000 {
// Call a subroutine at a given address, storing the current PC on the stack
c.state.Stack[c.state.StackPointer] = c.state.ProgramCounter
c.state.StackPointer++
c.state.ProgramCounter = nnn
} else if instruction == 0x00EE {
// Return from a subroutine
c.state.StackPointer--
c.state.ProgramCounter = c.state.Stack[c.state.StackPointer]
} else if maskedInstruction == 0x6000 {
// Set VX to NN
c.state.V[x] = nn
} else if maskedInstruction == 0x7000 {
// Add NN to VX, and store the result in VX
c.state.V[x] += nn
} else if maskedInstruction == 0xA000 {
// Set I to the address NNN
c.state.I = nnn
} else if maskedInstruction == 0xD000 {
xCoord := int(c.state.V[x] % 64)
yCoord := int(c.state.V[y] % 32)
c.state.V[0xF] = 0
for rowIndex := 0; rowIndex < int(n); rowIndex++ {
spriteData := c.state.Memory[int(c.state.I)+rowIndex]
for colIndex := 0; colIndex < 8; colIndex++ {
spritePixel := (spriteData & (1 << (7 - colIndex))) >> (7 - colIndex)
spritePixelBool := spritePixel == 1
screenPixel := c.GetPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex)
if spritePixelBool && screenPixel {
c.setPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex, false)
c.state.V[0xF] = 1
} else if spritePixelBool {
c.setPixelAtCoordinate(xCoord+colIndex, yCoord+rowIndex, true)
}
if colIndex >= 64 {
break
}
}
if rowIndex >= 32 {
break
}
}
c.DrawFlag = true
} else {
panic("uff")
}
}

5
go.mod
View File

@ -2,7 +2,10 @@ module github.com/micosilent/go_chip8
go 1.22.3
require github.com/hajimehoshi/ebiten/v2 v2.7.7
require (
github.com/hajimehoshi/ebiten/v2 v2.7.7
github.com/gin-gonic/gin v1.10.0
)
require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect

1
go.sum
View File

@ -4,6 +4,7 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/hajimehoshi/ebiten/v2 v2.7.7 h1:FyiuIOZqKU4aefYVws/lBDhTZu2WY2m/eWI3PtXZaHs=
github.com/hajimehoshi/ebiten/v2 v2.7.7/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA=
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=

48
main.go
View File

@ -5,22 +5,59 @@ import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/micosilent/go_chip8/emulator"
"image/color"
"log"
"os"
"time"
)
const (
screenWidth = 640
screenHeight = 320
gridWidth = 64
gridHeight = 32
)
type Game struct {
counter int
lastFrameTime time.Time
debugText string
emulator *emulator.CHIP8
screenImage *ebiten.Image
}
func (g *Game) Update() error {
// We need the emulator to process around 700 instructions per second
for i := 0; i < 1; i++ {
g.emulator.EmulateCycle()
if g.emulator.DrawFlag {
g.screenImage = g.DrawPixels()
g.emulator.DrawFlag = false
}
}
return nil
}
func (g *Game) DrawPixels() *ebiten.Image {
img := ebiten.NewImage(gridWidth, gridHeight)
white := color.White
black := color.Black
// Draw the boolean array to the image
for x := 0; x < gridWidth; x++ {
for y := 0; y < gridHeight; y++ {
if g.emulator.GetPixelAtCoordinate(x, y) {
img.Set(x, y, white)
} else {
img.Set(x, y, black)
}
}
}
return img
}
func newGame() *Game {
return &Game{
counter: 1,
@ -43,10 +80,17 @@ func (g *Game) Draw(screen *ebiten.Image) {
ebitenutil.DebugPrint(screen, g.debugText)
g.counter++
g.lastFrameTime = time.Now()
scaleX := float64(screenWidth) / float64(gridWidth)
scaleY := float64(screenHeight) / float64(gridHeight)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scaleX, scaleY)
screen.DrawImage(g.screenImage, op)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return 320, 240
return screenWidth, screenHeight
}
func (g *Game) LoadROM(path string) {
@ -60,7 +104,7 @@ func (g *Game) LoadROM(path string) {
func main() {
game := newGame()
game.LoadROM("roms/ibm_logo.ch8")
ebiten.SetWindowSize(480, 320)
ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Go CHIP8 Emulator")
err := ebiten.RunGame(game)

View File

@ -0,0 +1,66 @@
## IBM Logo program
Theorically we only need these instructions to decode the whole program:
- 00E0 (clear screen)
- 1NNN (jump)
- 6XNN (set register VX)
- 7XNN (add value to register VX)
- ANNN (set index register I)
- DXYN (display/draw)
- This is the most involved instruction. It will draw an N pixels tall sprite
from the memory location that the I index register is holding to the screen,
at the horizontal X coordinate in VX and the Y coordinate in VY. All the pixels
that are “on” in the sprite will flip the pixels on the screen that it is drawn
to (from left to right, from most to least significant bit). If any pixels on
the screen were turned “off” by this, the VF flag register is set to 1.
Otherwise, its set to 0.
## Binary
1. 00E0 Clear SCREEN
2. A22A Set Register I to 0x22A
3. 600C Set Register 0 to 0xC
4. 6108 Set Register 1 to 0x8
5. D01F Draw
- Register[0] = 0xC
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
- Register[I] = 0x22A
6. 7009 Add 0x9 to Register 0 (new value is 0x15)
7. A239 Set Register I to 0x239
8. D01F Draw
- Register[0] = 0x15
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
- Register[I] = 0x239
9. A248 Set Register I to 0x248
10. 7008 Add 0x8 to Register 0 (new value is 0x1D)
11. D01F Draw
- Register[0] = 0x1D
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
- Register[I] = 0x248
12. 7004 Add 0x4 to Register 0 (new value is 0x21)
13. A257 Set Register I to 0x257
14. D01F Draw
- Register[0] = 0x21
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
- Register[I] = 0x257
15. 7008
16. A266 Set Register I to 0x266
17. D01F Draw
- Register[0] = 0x21
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
Register[I] = 0x266
18. 7008 Add 0x8 to Register 0 (new value is 0x29)
19. A275 Set Register I to 0x275
20. D01F Draw
- Register[0] = 0x29
- Register[1] = 0x8
- N ( how tall is the sprite ) = 0xF
- Register[I] = 0x275
21. 1228 Set program counter to 0x228
22. From this point onwards I think it is sprite data