feat: minimal instruction set for IBM logo program
parent
ba28865248
commit
1e06ca350a
|
|
@ -1,39 +1,46 @@
|
||||||
package emulator
|
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 {
|
type CHIP8 struct {
|
||||||
memory [4096]byte
|
state *CHIP8State
|
||||||
v [16]byte
|
// A flag to check if the screen needs to be redrawn
|
||||||
i uint16
|
DrawFlag bool
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChip8() *CHIP8 {
|
func NewChip8() *CHIP8 {
|
||||||
chip8 := &CHIP8{
|
chip8 := &CHIP8{
|
||||||
memory: [4096]byte{},
|
state: &CHIP8State{
|
||||||
v: [16]byte{},
|
Memory: [4096]byte{},
|
||||||
i: 0,
|
V: [16]byte{},
|
||||||
// The original 0x200 code of the CHIP-8 interpreter was from 0x0 to 0x200,
|
I: 0,
|
||||||
pc: 0x200,
|
// The original 0x200 code of the CHIP-8 interpreter was from 0x0 to 0x200,
|
||||||
sp: 0,
|
ProgramCounter: 0x200,
|
||||||
delayTimer: 0,
|
StackPointer: 0,
|
||||||
soundTimer: 0,
|
DelayTimer: 0,
|
||||||
stack: [16]uint16{},
|
SoundTimer: 0,
|
||||||
keypad: [16]bool{},
|
Stack: [16]uint16{},
|
||||||
graphics: [64 * 32]bool{},
|
Keypad: [16]bool{},
|
||||||
opcode: 0,
|
Graphics: [64 * 32]bool{false},
|
||||||
fonts: [80]byte{},
|
},
|
||||||
|
DrawFlag: true,
|
||||||
}
|
}
|
||||||
chip8.Initialize()
|
chip8.Initialize()
|
||||||
return chip8
|
return chip8
|
||||||
|
|
@ -61,17 +68,121 @@ func (c *CHIP8) Initialize() {
|
||||||
|
|
||||||
// Char map goes from 0x050 to 0x9F
|
// Char map goes from 0x050 to 0x9F
|
||||||
for i, charByte := range charMap {
|
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 {
|
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")
|
return errors.New("ROM is too large to fit into memory")
|
||||||
}
|
}
|
||||||
// Roms start from 0x200
|
// Roms start from 0x200
|
||||||
for i, datByte := range dat {
|
for i, datByte := range dat {
|
||||||
c.memory[i+0x200] = datByte
|
c.state.Memory[i+0x200] = datByte
|
||||||
}
|
}
|
||||||
return nil
|
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
5
go.mod
|
|
@ -2,7 +2,10 @@ module github.com/micosilent/go_chip8
|
||||||
|
|
||||||
go 1.22.3
|
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 (
|
require (
|
||||||
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
|
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
|
||||||
|
|
|
||||||
1
go.sum
1
go.sum
|
|
@ -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/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 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
|
||||||
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
|
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 h1:FyiuIOZqKU4aefYVws/lBDhTZu2WY2m/eWI3PtXZaHs=
|
||||||
github.com/hajimehoshi/ebiten/v2 v2.7.7/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA=
|
github.com/hajimehoshi/ebiten/v2 v2.7.7/go.mod h1:Ulbq5xDmdx47P24EJ+Mb31Zps7vQq+guieG9mghQUaA=
|
||||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
|
|
|
||||||
48
main.go
48
main.go
|
|
@ -5,22 +5,59 @@ import (
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
"github.com/micosilent/go_chip8/emulator"
|
"github.com/micosilent/go_chip8/emulator"
|
||||||
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
screenWidth = 640
|
||||||
|
screenHeight = 320
|
||||||
|
gridWidth = 64
|
||||||
|
gridHeight = 32
|
||||||
|
)
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
counter int
|
counter int
|
||||||
lastFrameTime time.Time
|
lastFrameTime time.Time
|
||||||
debugText string
|
debugText string
|
||||||
emulator *emulator.CHIP8
|
emulator *emulator.CHIP8
|
||||||
|
screenImage *ebiten.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
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
|
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 {
|
func newGame() *Game {
|
||||||
return &Game{
|
return &Game{
|
||||||
counter: 1,
|
counter: 1,
|
||||||
|
|
@ -43,10 +80,17 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
ebitenutil.DebugPrint(screen, g.debugText)
|
ebitenutil.DebugPrint(screen, g.debugText)
|
||||||
g.counter++
|
g.counter++
|
||||||
g.lastFrameTime = time.Now()
|
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) {
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
return 320, 240
|
return screenWidth, screenHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) LoadROM(path string) {
|
func (g *Game) LoadROM(path string) {
|
||||||
|
|
@ -60,7 +104,7 @@ func (g *Game) LoadROM(path string) {
|
||||||
func main() {
|
func main() {
|
||||||
game := newGame()
|
game := newGame()
|
||||||
game.LoadROM("roms/ibm_logo.ch8")
|
game.LoadROM("roms/ibm_logo.ch8")
|
||||||
ebiten.SetWindowSize(480, 320)
|
ebiten.SetWindowSize(screenWidth, screenHeight)
|
||||||
ebiten.SetWindowTitle("Go CHIP8 Emulator")
|
ebiten.SetWindowTitle("Go CHIP8 Emulator")
|
||||||
|
|
||||||
err := ebiten.RunGame(game)
|
err := ebiten.RunGame(game)
|
||||||
|
|
|
||||||
|
|
@ -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, it’s 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
|
||||||
|
|
||||||
Loading…
Reference in New Issue