Skip to content

Commit 31e7308

Browse files
Baekalfenthejomas
andcommitted
Implementing serial support over IP socket
Co-authored-by: thejomas <[email protected]>
1 parent 376fc98 commit 31e7308

File tree

5 files changed

+146
-4
lines changed

5 files changed

+146
-4
lines changed

pyboy/__main__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ def valid_sample_rate(freq):
107107
help="Add GameShark cheats on start-up. Add multiple by comma separation (i.e. '010138CD, 01033CD1')",
108108
)
109109

110+
parser.add_argument("--serial-bind", action="store_true", help="Bind to this TCP addres for using Link Cable")
111+
parser.add_argument(
112+
"--serial-address", default=None, type=str, help="Connect (or bind) to this TCP addres for using Link Cable"
113+
)
114+
110115
gameboy_type_parser = parser.add_mutually_exclusive_group()
111116
gameboy_type_parser.add_argument(
112117
"--dmg", action="store_const", const=False, dest="cgb", help="Force emulator to run as original Game Boy (DMG)"
@@ -160,6 +165,9 @@ def valid_sample_rate(freq):
160165
def main():
161166
argv = parser.parse_args()
162167

168+
if argv.serial_bind and not argv.serial_address:
169+
parser.error("--serial-bind requires --serial-address")
170+
163171
print(
164172
"""
165173
The Game Boy controls are as follows:

pyboy/core/mb.pxd

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ cdef Logger logger
2323

2424
cdef int64_t MAX_CYCLES
2525
cdef uint16_t STAT, LY, LYC
26-
cdef int INTR_TIMER, INTR_HIGHTOLOW
26+
cdef int INTR_TIMER, INTR_HIGHTOLOW, INTR_SERIAL
2727
cdef uint16_t OPCODE_BRK
2828
cdef int STATE_VERSION
2929

@@ -37,6 +37,7 @@ cdef class Motherboard:
3737
cdef pyboy.core.timer.Timer timer
3838
cdef pyboy.core.sound.Sound sound
3939
cdef pyboy.core.cartridge.base_mbc.BaseMBC cartridge
40+
cdef object serial
4041
cdef bint bootrom_enabled
4142
cdef char[1024] serialbuffer
4243
cdef uint16_t serialbuffer_count

pyboy/core/mb.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@
1010
PyBoyOutOfBoundsException,
1111
INTR_TIMER,
1212
INTR_HIGHTOLOW,
13+
INTR_SERIAL,
1314
OPCODE_BRK,
1415
)
1516

16-
from . import bootrom, cartridge, cpu, interaction, lcd, ram, sound, timer
17+
from . import bootrom, cartridge, cpu, interaction, lcd, ram, serial, sound, timer
1718

1819
logger = pyboy.logging.get_logger(__name__)
1920
MAX_CYCLES = 1 << 31
@@ -31,6 +32,8 @@ def __init__(
3132
sound_sample_rate,
3233
cgb,
3334
randomize=False,
35+
serial_address=None,
36+
serial_bind=None,
3437
):
3538
if bootrom_file is not None:
3639
logger.info("Boot-ROM file provided")
@@ -51,6 +54,7 @@ def __init__(
5154
self.interaction = interaction.Interaction()
5255
self.ram = ram.RAM(cgb, randomize=randomize)
5356
self.cpu = cpu.CPU(self)
57+
self.serial = serial.Serial(serial_address, serial_bind)
5458

5559
if cgb:
5660
self.lcd = lcd.CGBLCD(
@@ -224,6 +228,7 @@ def buttonevent(self, key):
224228

225229
def stop(self, save):
226230
self.sound.stop()
231+
self.serial.stop()
227232
if save:
228233
self.cartridge.stop()
229234

@@ -321,7 +326,7 @@ def tick(self):
321326
self.lcd._cycles_to_interrupt, # TODO: Be more agreesive. Only if actual interrupt enabled.
322327
self.lcd._cycles_to_frame,
323328
self.sound._cycles_to_interrupt,
324-
# self.serial.cycles_to_interrupt(),
329+
self.serial.cycles_to_transmit(),
325330
mode0_cycles,
326331
),
327332
)
@@ -336,6 +341,8 @@ def tick(self):
336341

337342
if self.timer.tick(self.cpu.cycles):
338343
self.cpu.set_interruptflag(INTR_TIMER)
344+
if self.serial.tick(cycles):
345+
self.cpu.set_interruptflag(INTR_SERIAL)
339346

340347
if lcd_interrupt := self.lcd.tick(self.cpu.cycles):
341348
self.cpu.set_interruptflag(lcd_interrupt)
@@ -380,7 +387,13 @@ def getitem(self, i):
380387
elif 0xFEA0 <= i < 0xFF00: # Empty but unusable for I/O
381388
return self.ram.non_io_internal_ram0[i - 0xFEA0]
382389
elif 0xFF00 <= i < 0xFF4C: # I/O ports
383-
if 0xFF04 <= i <= 0xFF07:
390+
if i == 0xFF01:
391+
logger.info(f"get SB {self.serial.SB}")
392+
return self.serial.SB
393+
elif i == 0xFF02:
394+
logger.info(f"get SC {self.serial.SC}")
395+
return self.serial.SC
396+
elif 0xFF04 <= i <= 0xFF07:
384397
if self.timer.tick(self.cpu.cycles):
385398
self.cpu.set_interruptflag(INTR_TIMER)
386399

@@ -511,10 +524,16 @@ def setitem(self, i, value):
511524
if i == 0xFF00:
512525
self.ram.io_ports[i - 0xFF00] = self.interaction.pull(value)
513526
elif i == 0xFF01:
527+
self.serial.SB = value
528+
logger.info(f"SB: {value:02x}")
529+
514530
self.serialbuffer[self.serialbuffer_count] = value
515531
self.serialbuffer_count += 1
516532
self.serialbuffer_count &= 0x3FF
517533
self.ram.io_ports[i - 0xFF00] = value
534+
elif i == 0xFF02:
535+
self.serial.SC = value
536+
logger.info(f"SC: {value:02x}")
518537
elif 0xFF04 <= i <= 0xFF07:
519538
if self.timer.tick(self.cpu.cycles):
520539
self.cpu.set_interruptflag(INTR_TIMER)

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
@@ -209,6 +209,8 @@ def __init__(
209209
sound_sample_rate,
210210
cgb,
211211
randomize=randomize,
212+
serial_address=kwargs["serial_address"],
213+
serial_bind=kwargs["serial_bind"],
212214
)
213215

214216
# Validate all kwargs

0 commit comments

Comments
 (0)