Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions pyboy/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ def valid_file_path(path):
parser.add_argument("--sound", action="store_true", help="Enable sound (beta)")
parser.add_argument("--no-renderer", action="store_true", help="Disable rendering (internal use)")

parser.add_argument("--serial-bind", action="store_true", help="Bind to this TCP addres for using Link Cable")
parser.add_argument(
"--serial-address", default=None, type=str, help="Connect (or bind) to this TCP address for using Link Cable"
)
parser.add_argument(
"--serial-interrupt-based", action="store_true", help="Use only interrupt-based transfers for using Link Cable"
)

gameboy_type_parser = parser.add_mutually_exclusive_group()
gameboy_type_parser.add_argument(
"--dmg", action="store_const", const=False, dest="cgb", help="Force emulator to run as original Game Boy (DMG)"
Expand All @@ -106,6 +114,9 @@ def valid_file_path(path):
def main():
argv = parser.parse_args()

if (argv.serial_bind or argv.serial_interrupt_based) and not argv.serial_address:
parser.error("--serial-bind and --serial-interrupt-based requires --serial-address")

print(
"""
The Game Boy controls are as follows:
Expand Down
8 changes: 7 additions & 1 deletion pyboy/core/mb.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ cimport pyboy.core.cpu
cimport pyboy.core.interaction
cimport pyboy.core.lcd
cimport pyboy.core.ram
cimport pyboy.core.serial
cimport pyboy.core.sound
cimport pyboy.core.timer

from threading import Thread

from pyboy.logging.logging cimport Logger
from pyboy.utils cimport IntIOInterface, WindowEvent

Expand All @@ -36,6 +40,8 @@ cdef class Motherboard:
cdef pyboy.core.timer.Timer timer
cdef pyboy.core.sound.Sound sound
cdef pyboy.core.cartridge.base_mbc.BaseMBC cartridge
cdef pyboy.core.serial.Serial serial
cdef object serial_thread
cdef bint bootrom_enabled
cdef char[1024] serialbuffer
cdef uint16_t serialbuffer_count
Expand All @@ -60,7 +66,7 @@ cdef class Motherboard:
cdef void buttonevent(self, WindowEvent) noexcept
cdef void stop(self, bint) noexcept
@cython.locals(cycles=int64_t, mode0_cycles=int64_t, breakpoint_index=int64_t)
cdef bint tick(self) noexcept nogil
cdef bint tick(self) noexcept with gil

cdef void switch_speed(self) noexcept nogil

Expand Down
36 changes: 27 additions & 9 deletions pyboy/core/mb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
# GitHub: https://github.com/Baekalfen/PyBoy
#

from pyboy import utils
from pyboy.utils import STATE_VERSION

from . import bootrom, cartridge, cpu, interaction, lcd, ram, sound, timer
from . import bootrom, cartridge, cpu, interaction, lcd, ram, serial, sound, timer

INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW = [1 << x for x in range(5)]
OPCODE_BRK = 0xDB
Expand All @@ -27,6 +26,9 @@ def __init__(
sound_emulated,
cgb,
randomize=False,
serial_address=None,
serial_bind=None,
serial_interrupt_based=False,
):
if bootrom_file is not None:
logger.info("Boot-ROM file provided")
Expand All @@ -42,6 +44,7 @@ def __init__(
self.bootrom = bootrom.BootROM(bootrom_file, cgb)
self.ram = ram.RAM(cgb, randomize=randomize)
self.cpu = cpu.CPU(self)
self.serial = serial.Serial(self, serial_address or None, serial_bind or None, serial_interrupt_based)

if cgb:
self.lcd = lcd.CGBLCD(
Expand Down Expand Up @@ -204,6 +207,7 @@ def buttonevent(self, key):

def stop(self, save):
self.sound.stop()
# self.serial.stop()
if save:
self.cartridge.stop()

Expand Down Expand Up @@ -296,10 +300,8 @@ def tick(self):
cycles = max(
0,
min(
self.lcd.cycles_to_interrupt(),
self.timer.cycles_to_interrupt(),
# self.serial.cycles_to_interrupt(),
mode0_cycles
self.lcd.cycles_to_interrupt(), self.timer.cycles_to_interrupt(),
self.serial.cycles_to_transmit(), mode0_cycles
)
)

Expand All @@ -316,15 +318,18 @@ def tick(self):
if self.timer.tick(cycles):
self.cpu.set_interruptflag(INTR_TIMER)

if self.serial.tick(cycles):
self.cpu.set_interruptflag(INTR_SERIAL)

lcd_interrupt = self.lcd.tick(cycles)
if lcd_interrupt:
self.cpu.set_interruptflag(lcd_interrupt)

if self.breakpoint_singlestep:
break

# TODO: Move SDL2 sync to plugin
self.sound.sync()
# TODO: Move SDL2 sync to plugin
self.sound.sync()

return self.breakpoint_singlestep

Expand Down Expand Up @@ -364,7 +369,15 @@ def getitem(self, i):
elif 0xFEA0 <= i < 0xFF00: # Empty but unusable for I/O
return self.ram.non_io_internal_ram0[i - 0xFEA0]
elif 0xFF00 <= i < 0xFF4C: # I/O ports
if i == 0xFF04:
if i == 0xFF01:
# logger.info(f"get SB {self.serial.SB}")
# if not self.serial:
# return 0xFF
return self.serial.SB
elif i == 0xFF02:
# logger.info(f"get SC {self.serial.SC}")
return self.serial.SC
elif i == 0xFF04:
return self.timer.DIV
elif i == 0xFF05:
return self.timer.TIMA
Expand Down Expand Up @@ -478,10 +491,15 @@ def setitem(self, i, value):
if i == 0xFF00:
self.ram.io_ports[i - 0xFF00] = self.interaction.pull(value)
elif i == 0xFF01:
self.serial.SB = value

self.serialbuffer[self.serialbuffer_count] = value
self.serialbuffer_count += 1
self.serialbuffer_count &= 0x3FF
self.ram.io_ports[i - 0xFF00] = value
elif i == 0xFF02:
self.serial.SC = value
self.ram.io_ports[i - 0xFF00] = value
elif i == 0xFF04:
self.timer.reset()
elif i == 0xFF05:
Expand Down
28 changes: 28 additions & 0 deletions pyboy/core/serial.pxd
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# serial.pxd

cdef int INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW
cdef int SERIAL_FREQ, CPU_FREQ
cdef object async_recv

cdef class Serial:
cdef object mb
cdef int SC
cdef int SB
cdef object connection
cdef object recv
cdef object recv_t
cdef bint quitting
cdef int trans_bits
cdef int cycles_count
cdef int cycles_target
cdef int serial_interrupt_based
cdef bint waiting_for_byte
cdef int byte_retry_count
cdef object binding_connection
cdef int is_master
cdef bint transfer_enabled

cpdef send_bit(self)
cpdef bint tick(self, int cycles) noexcept with gil
cpdef int cycles_to_transmit(self) noexcept with gil
cpdef stop(self)
150 changes: 150 additions & 0 deletions pyboy/core/serial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import logging
import os
import platform
import queue
import select
import socket
import sys
import threading
import time

from colorama import Fore

import pyboy

logger = pyboy.logging.get_logger(__name__)

INTR_VBLANK, INTR_LCDC, INTR_TIMER, INTR_SERIAL, INTR_HIGHTOLOW = [1 << x for x in range(5)]
SERIAL_FREQ = 8192 # Hz
CPU_FREQ = 4194304 # Hz # Corrected CPU Frequency


class Serial:
"""Gameboy Link Cable Emulation"""
def __init__(self, mb, serial_address, serial_bind, serial_interrupt_based):
self.mb = mb
self.SC = 0b00000000 # Serial transfer control
self.SB = 0b00000000 # Serial transfer data
self.connection = None

self.trans_bits = 0 # Number of bits transferred
self.cycles_count = 0 # Number of cycles since last transfer
self.cycles_target = CPU_FREQ // SERIAL_FREQ
self.serial_interrupt_based = serial_interrupt_based

self.recv = queue.Queue()

self.quitting = False

if not serial_address:
logger.info("No serial address supplied. Link Cable emulation disabled.")
return

if not serial_address.count(".") == 3 and serial_address.count(":") == 1:
logger.info("Only IP-addresses of the format x.y.z.w:abcd is supported")
return

address_ip, address_port = serial_address.split(":")
address_tuple = (address_ip, int(address_port))

self.is_master = True
self.transfer_enabled = False
self.waiting_for_byte = False
self.byte_retry_count = 0

if serial_bind:
self.binding_connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.binding_connection.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
logger.info(f"Binding to {serial_address}")
self.binding_connection.bind(address_tuple)
self.binding_connection.listen(1)
self.connection, _ = self.binding_connection.accept()
logger.info(f"Client has connected!")
else:
self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
logger.info(f"Connecting to {serial_address}")
self.connection.connect(address_tuple)
logger.info(f"Connection successful!")
self.is_master = False
self.connection.setblocking(False)
self.recv_t = threading.Thread(target=lambda: self.recv_thread())
self.recv_t.daemon = True
self.recv_t.start()

def recv_thread(self):
while not self.quitting:
try:
data = self.connection.recv(1)
self.recv.put(data)
except BlockingIOError as e:
pass
except ConnectionResetError as e:
print(f"Connection reset by peer: {e}")
break

def send_bit(self):
send_bit = bytes([(self.SB >> 7) & 1])
try:
self.connection.send(send_bit)
except ConnectionResetError:
self.SB = 0xFF

def tick(self, cycles):
if self.connection is None:
# No connection, no serial
self.SB = 0xFF
return False

if self.SC & 0x80 == 0: # Check if transfer is enabled
return False

self.cycles_count += cycles # Accumulate cycles

if self.cycles_to_transmit() == 0:
if not self.waiting_for_byte:
self.send_bit()
time.sleep(1 / SERIAL_FREQ)

try:
rb = self.recv.get_nowait()
self.waiting_for_byte = False
self.byte_retry_count = 0
self.trans_bits += 1
except queue.Empty as e:
# This part prevents indefinite lockup
# while waiting for bytes
self.waiting_for_byte = True
self.byte_retry_count += 1
if self.byte_retry_count >= 8:
self.byte_retry_count = 0
self.cycles_count = 0 # Reset cycles
return False
self.SB = ((self.SB << 1) & 0xFF) | rb[0]

self.cycles_count = 0 # Reset cycle count after transmission

if self.trans_bits == 8:
self.trans_bits = 0
# Clear transfer start flag
self.SC &= 0x7F
return True
return False

def cycles_to_transmit(self):
if self.connection:
if self.SC & 0x80:
return max(self.cycles_target - self.cycles_count, 0)
else:
return 1 << 16
else:
return 1 << 16

def stop(self):
self.quitting = True
if self.connection:
self.connection.close()
if hasattr(self, "binding_connection"):
self.binding_connection.close()
self.connection = None
self.binding_connection = None
self.recv_t.join()
Loading