Skip to content

Commit 491aeed

Browse files
Baekalfenthejomas
andcommitted
Implementing serial support over IP socket
Co-authored-by: thejomas <[email protected]>
1 parent 4dad520 commit 491aeed

File tree

5 files changed

+145
-4
lines changed

5 files changed

+145
-4
lines changed

pyboy/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ def valid_file_path(path):
8888
parser.add_argument("--sound", action="store_true", help="Enable sound (beta)")
8989
parser.add_argument("--no-renderer", action="store_true", help="Disable rendering (internal use)")
9090

91+
parser.add_argument("--serial-bind", action="store_true", help="Bind to this TCP addres for using Link Cable")
92+
parser.add_argument(
93+
"--serial-address", default=None, type=str, help="Connect (or bind) to this TCP addres for using Link Cable"
94+
)
95+
9196
gameboy_type_parser = parser.add_mutually_exclusive_group()
9297
gameboy_type_parser.add_argument(
9398
"--dmg", action="store_const", const=False, dest="cgb", help="Force emulator to run as original Game Boy (DMG)"
@@ -106,6 +111,9 @@ def valid_file_path(path):
106111
def main():
107112
argv = parser.parse_args()
108113

114+
if argv.serial_bind and not argv.serial_address:
115+
parser.error("--serial-bind requires --serial-address")
116+
109117
print(
110118
"""
111119
The Game Boy controls are as follows:

pyboy/core/mb.pxd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ from pyboy.utils cimport IntIOInterface, WindowEvent
2222
cdef Logger logger
2323

2424
cdef uint16_t STAT, LY, LYC
25-
cdef short VBLANK, LCDC, TIMER, SERIAL, HIGHTOLOW
2625
cdef int INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW
2726
cdef int STATE_VERSION
2827

@@ -36,6 +35,7 @@ cdef class Motherboard:
3635
cdef pyboy.core.timer.Timer timer
3736
cdef pyboy.core.sound.Sound sound
3837
cdef pyboy.core.cartridge.base_mbc.BaseMBC cartridge
38+
cdef object serial
3939
cdef bint bootrom_enabled
4040
cdef char[1024] serialbuffer
4141
cdef uint16_t serialbuffer_count

pyboy/core/mb.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pyboy import utils
77
from pyboy.utils import STATE_VERSION
88

9-
from . import bootrom, cartridge, cpu, interaction, lcd, ram, sound, timer
9+
from . import bootrom, cartridge, cpu, interaction, lcd, ram, serial, sound, timer
1010

1111
INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW = [1 << x for x in range(5)]
1212
OPCODE_BRK = 0xDB
@@ -27,6 +27,8 @@ def __init__(
2727
sound_emulated,
2828
cgb,
2929
randomize=False,
30+
serial_address=None,
31+
serial_bind=None,
3032
):
3133
if bootrom_file is not None:
3234
logger.info("Boot-ROM file provided")
@@ -42,6 +44,7 @@ def __init__(
4244
self.bootrom = bootrom.BootROM(bootrom_file, cgb)
4345
self.ram = ram.RAM(cgb, randomize=randomize)
4446
self.cpu = cpu.CPU(self)
47+
self.serial = serial.Serial(serial_address, serial_bind)
4548

4649
if cgb:
4750
self.lcd = lcd.CGBLCD(
@@ -204,6 +207,7 @@ def buttonevent(self, key):
204207

205208
def stop(self, save):
206209
self.sound.stop()
210+
self.serial.stop()
207211
if save:
208212
self.cartridge.stop()
209213

@@ -298,10 +302,11 @@ def tick(self):
298302
min(
299303
self.lcd.cycles_to_interrupt(),
300304
self.timer.cycles_to_interrupt(),
301-
# self.serial.cycles_to_interrupt(),
305+
self.serial.cycles_to_transmit(),
302306
mode0_cycles
303307
)
304308
)
309+
cycles = 4
305310

306311
#TODO: Support General Purpose DMA
307312
# https://gbdev.io/pandocs/CGB_Registers.html#bit-7--0---general-purpose-dma
@@ -315,6 +320,8 @@ def tick(self):
315320

316321
if self.timer.tick(cycles):
317322
self.cpu.set_interruptflag(INTR_TIMER)
323+
if self.serial.tick(cycles):
324+
self.cpu.set_interruptflag(INTR_SERIAL)
318325

319326
lcd_interrupt = self.lcd.tick(cycles)
320327
if lcd_interrupt:
@@ -364,7 +371,13 @@ def getitem(self, i):
364371
elif 0xFEA0 <= i < 0xFF00: # Empty but unusable for I/O
365372
return self.ram.non_io_internal_ram0[i - 0xFEA0]
366373
elif 0xFF00 <= i < 0xFF4C: # I/O ports
367-
if i == 0xFF04:
374+
if i == 0xFF01:
375+
logger.info(f"get SB {self.serial.SB}")
376+
return self.serial.SB
377+
elif i == 0xFF02:
378+
logger.info(f"get SC {self.serial.SC}")
379+
return self.serial.SC
380+
elif i == 0xFF04:
368381
return self.timer.DIV
369382
elif i == 0xFF05:
370383
return self.timer.TIMA
@@ -478,10 +491,16 @@ def setitem(self, i, value):
478491
if i == 0xFF00:
479492
self.ram.io_ports[i - 0xFF00] = self.interaction.pull(value)
480493
elif i == 0xFF01:
494+
self.serial.SB = value
495+
logger.info(f"SB: {value:02x}")
496+
481497
self.serialbuffer[self.serialbuffer_count] = value
482498
self.serialbuffer_count += 1
483499
self.serialbuffer_count &= 0x3FF
484500
self.ram.io_ports[i - 0xFF00] = value
501+
elif i == 0xFF02:
502+
self.serial.SC = value
503+
logger.info(f"SC: {value:02x}")
485504
elif i == 0xFF04:
486505
self.timer.reset()
487506
elif i == 0xFF05:

pyboy/core/serial.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import logging
2+
import os
3+
import socket
4+
import sys
5+
6+
logger = logging.getLogger(__name__)
7+
8+
SERIAL_FREQ = 8192 # Hz
9+
CPU_FREQ = 4213440 # Hz
10+
11+
12+
class Serial:
13+
def __init__(self, serial_address, serial_bind, serial_interrupt_based=True):
14+
self.SB = 0
15+
self.SC = 0
16+
self.connection = None
17+
18+
self.trans_bits = 0
19+
self.cycles_count = 0
20+
self.cycles_target = CPU_FREQ // SERIAL_FREQ
21+
self.serial_interrupt_based = serial_interrupt_based
22+
23+
if not serial_address:
24+
logger.info("No serial address supplied. Link Cable emulation disabled.")
25+
return
26+
27+
if not serial_address.count(".") == 3 and serial_address.count(":") == 1:
28+
logger.info("Only IP-addresses of the format x.y.z.w:abcd is supported")
29+
return
30+
31+
address_ip, address_port = serial_address.split(":")
32+
address_tuple = (address_ip, int(address_port))
33+
34+
if serial_bind:
35+
self.binding_connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
36+
self.binding_connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
37+
logger.info(f"Binding to {serial_address}")
38+
self.binding_connection.bind(address_tuple)
39+
self.binding_connection.listen(1)
40+
self.connection, _ = self.binding_connection.accept()
41+
logger.info(f"Client has connected!")
42+
else:
43+
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
44+
logger.info(f"Connecting to {serial_address}")
45+
self.connection.connect(address_tuple)
46+
logger.info(f"Connection successful!")
47+
# self.connection.setblocking(False)
48+
49+
def tick(self, cycles):
50+
# if self.serial_interrupt_based:
51+
# if self.SC & 1: # Master
52+
# if self.SC & 0x80:
53+
# logger.info(f'Master sending!')
54+
# self.connection.send(bytes([self.SB]))
55+
# # self.connection.setblocking(True)
56+
# data = self.connection.recv(1)
57+
# self.SB = data[0]
58+
# self.SC &= 0b0111_1111
59+
# return True
60+
# else:
61+
# try:
62+
# if self.SC & 0x80:
63+
# # self.connection.setblocking(False)
64+
# logger.info(f'Slave recv!')
65+
# self.connection.send(bytes([self.SB]))
66+
# data = self.connection.recv(1)
67+
# self.SB = data[0]
68+
# self.SC &= 0b0111_1111
69+
# return True
70+
# except BlockingIOError:
71+
# pass
72+
# return False
73+
# return False
74+
# else:
75+
# Check if serial is in progress
76+
77+
if self.connection is None:
78+
return
79+
80+
if self.SC & 0x80 == 0:
81+
return False
82+
83+
self.cycles_count += 1
84+
85+
if (self.cycles_to_transmit() == 0):
86+
# if self.SC & 1: # Master
87+
send_bit = bytes([(self.SB >> 7) & 1])
88+
self.connection.send(send_bit)
89+
90+
data = self.connection.recv(1)
91+
self.SB = ((self.SB << 1) & 0xFF) | data[0] & 1
92+
93+
logger.info(f"recv sb: {self.SB:08b}")
94+
self.trans_bits += 1
95+
96+
self.cycles_count = 0
97+
98+
if self.trans_bits == 8:
99+
self.trans_bits = 0
100+
self.SC &= 0b0111_1111
101+
return True
102+
return False
103+
104+
def cycles_to_transmit(self):
105+
if self.SC & 0x80:
106+
return max(self.cycles_target - self.cycles_count, 0)
107+
else:
108+
return 1 << 16
109+
110+
def stop(self):
111+
if self.connection:
112+
self.connection.close()

pyboy/pyboy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ def __init__(
137137
sound_emulated,
138138
cgb,
139139
randomize=randomize,
140+
serial_address=kwargs["serial_address"],
141+
serial_bind=kwargs["serial_bind"],
140142
)
141143

142144
# Validate all kwargs

0 commit comments

Comments
 (0)