mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
387 lines
16 KiB
Python
387 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unified Cartridge CPU + Controller Port Architecture
|
|
Cartridge CPU (SUBLEQ) + GCL + Proto-Shader + Audio → NES via Controller Port (Voltage Shifter)
|
|
|
|
Architecture:
|
|
1. Cartridge CPU: SUBLEQ processor on cartridge
|
|
2. GCL Decompression: Runs on cartridge CPU
|
|
3. Proto-Shader: NES palette generator on cartridge CPU
|
|
4. Square Wave Generation: On cartridge CPU
|
|
5. Controller Port: Voltage-shifting communication channel to NES
|
|
|
|
Key Insight: NES controller port is bidirectional and can shift voltage levels.
|
|
This enables level conversion between cartridge CPU (3.3V/5V) and NES (5V).
|
|
|
|
Communication Protocol:
|
|
- Cartridge CPU streams data via controller port
|
|
- NES 6502 reads controller port registers ($4016/$4017)
|
|
- Voltage shifting handles level conversion
|
|
- Real-time audio/visual data transfer
|
|
|
|
This unifies the entire stack on the cartridge, with NES as I/O terminal.
|
|
"""
|
|
|
|
import struct
|
|
from typing import List, Tuple, Dict, Optional
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Controller Port Voltage Shifter
|
|
# NES controller port can shift voltage levels for bidirectional communication
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
class ControllerPortPin(Enum):
|
|
"""NES controller port pins"""
|
|
VCC = 1 # +5V
|
|
CLK = 2 # Clock/Strobe (4016 latch)
|
|
OUT = 3 # Data out (to controller)
|
|
GND = 4 # Ground
|
|
IN = 5 # Data in (from controller)
|
|
NC1 = 6 # +5V (unused)
|
|
NC2 = 7 # Ground (unused)
|
|
|
|
@dataclass
|
|
class ControllerPortState:
|
|
"""Controller port state with voltage levels"""
|
|
clk_voltage: float = 0.0 # CLK voltage (V)
|
|
out_voltage: float = 0.0 # OUT voltage (V)
|
|
in_voltage: float = 0.0 # IN voltage (V)
|
|
|
|
def to_bits(self) -> int:
|
|
"""Convert to 3-bit state (CLK, OUT, IN)"""
|
|
bits = 0
|
|
if self.clk_voltage > 2.5: # Logic high threshold
|
|
bits |= 0x01
|
|
if self.out_voltage > 2.5:
|
|
bits |= 0x02
|
|
if self.in_voltage > 2.5:
|
|
bits |= 0x04
|
|
return bits
|
|
|
|
@staticmethod
|
|
def from_bits(bits: int) -> 'ControllerPortState':
|
|
"""Convert from 3-bit state"""
|
|
return ControllerPortState(
|
|
clk_voltage=5.0 if (bits & 0x01) else 0.0,
|
|
out_voltage=5.0 if (bits & 0x02) else 0.0,
|
|
in_voltage=5.0 if (bits & 0x04) else 0.0
|
|
)
|
|
|
|
class VoltageShifter:
|
|
"""Voltage level shifter for controller port communication"""
|
|
|
|
@staticmethod
|
|
def shift_voltage(voltage: float, from_level: float, to_level: float) -> float:
|
|
"""Shift voltage from one level to another"""
|
|
if from_level == to_level:
|
|
return voltage
|
|
ratio = to_level / from_level
|
|
return voltage * ratio
|
|
|
|
@staticmethod
|
|
def cartridge_to_nes(voltage: float, cartridge_v: float = 3.3) -> float:
|
|
"""Shift from cartridge voltage (3.3V) to NES voltage (5V)"""
|
|
return VoltageShifter.shift_voltage(voltage, cartridge_v, 5.0)
|
|
|
|
@staticmethod
|
|
def nes_to_cartridge(voltage: float, cartridge_v: float = 3.3) -> float:
|
|
"""Shift from NES voltage (5V) to cartridge voltage (3.3V)"""
|
|
return VoltageShifter.shift_voltage(voltage, 5.0, cartridge_v)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Cartridge CPU: SUBLEQ Processor
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
class CartridgeSUBLEQ:
|
|
"""SUBLEQ processor on cartridge"""
|
|
|
|
def __init__(self, memory_size: int = 65536):
|
|
self.memory = [0] * memory_size
|
|
self.pc = 0
|
|
self.halted = False
|
|
self.cycle_count = 0
|
|
|
|
def load_program(self, instructions: List[Tuple[int, int, int]]):
|
|
"""Load SUBLEQ program"""
|
|
for i, (a, b, c) in enumerate(instructions):
|
|
self.memory[i * 3] = a & 0xFF
|
|
self.memory[i * 3 + 1] = (a >> 8) & 0xFF
|
|
self.memory[i * 3 + 2] = b & 0xFF
|
|
self.memory[i * 3 + 3] = (b >> 8) & 0xFF
|
|
self.memory[i * 3 + 4] = c & 0xFF
|
|
self.memory[i * 3 + 5] = (c >> 8) & 0xFF
|
|
|
|
def step(self) -> bool:
|
|
"""Execute one SUBLEQ instruction"""
|
|
if self.halted:
|
|
return False
|
|
|
|
a = self.memory[self.pc] | (self.memory[self.pc + 1] << 8)
|
|
b = self.memory[self.pc + 2] | (self.memory[self.pc + 3] << 8)
|
|
c = self.memory[self.pc + 4] | (self.memory[self.pc + 5] << 8)
|
|
|
|
src_val = self.memory[a]
|
|
dst_val = self.memory[b]
|
|
result = (dst_val - src_val) & 0xFF
|
|
self.memory[b] = result
|
|
|
|
if result & 0x80 or result == 0:
|
|
self.pc = c
|
|
else:
|
|
self.pc += 6
|
|
|
|
self.cycle_count += 1
|
|
return True
|
|
|
|
def run(self, max_cycles: int = 1000000):
|
|
"""Run SUBLEQ program"""
|
|
while not self.halted and self.cycle_count < max_cycles:
|
|
if not self.step():
|
|
break
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Proto-Shader: NES Palette Generator
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
class CartridgePaletteShader:
|
|
"""Palette generator running on cartridge CPU"""
|
|
|
|
@staticmethod
|
|
def encode_color(r: int, g: int, b: int) -> int:
|
|
"""Encode RGB to NES palette index"""
|
|
return ((r & 0x3) << 4) | ((g & 0x3) << 2) | (b & 0x3)
|
|
|
|
@staticmethod
|
|
def shader_compute(x: int, y: int, t: int, params: bytes) -> int:
|
|
"""Proto-shader: f(x,y,t,θ) → color"""
|
|
freq = params[0] if len(params) > 0 else 1
|
|
mod = params[1] if len(params) > 1 else 0
|
|
|
|
r = ((x + t * freq) % 256) >> 6
|
|
g = ((y + t * freq) % 256) >> 6
|
|
b = ((x + y + mod) % 256) >> 6
|
|
|
|
return CartridgePaletteShader.encode_color(r, g, b)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# GCL Compression (Simplified for Cartridge)
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
@dataclass
|
|
class SquareWaveParams:
|
|
"""Square wave parameters"""
|
|
frequency: int
|
|
duty: int
|
|
volume: int
|
|
|
|
def to_bytes(self) -> bytes:
|
|
return struct.pack('<HBB', self.frequency, self.duty, self.volume)
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Controller Port Communication Protocol
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
class ControllerPortProtocol:
|
|
"""Protocol for cartridge → NES communication via controller port"""
|
|
|
|
def __init__(self, cartridge_cpu: CartridgeSUBLEQ):
|
|
self.cartridge = cartridge_cpu
|
|
self.port_state = ControllerPortState()
|
|
self.voltage_shifter = VoltageShifter()
|
|
self.tx_buffer: List[int] = []
|
|
self.rx_buffer: List[int] = []
|
|
self.bit_index = 0
|
|
|
|
def send_byte(self, data: int):
|
|
"""Send one byte to NES via controller port"""
|
|
self.tx_buffer.append(data)
|
|
|
|
def send_square_wave(self, params: SquareWaveParams):
|
|
"""Send square wave parameters to NES"""
|
|
data = params.to_bytes()
|
|
for byte in data:
|
|
self.send_byte(byte)
|
|
|
|
def send_palette_color(self, color_index: int):
|
|
"""Send palette color to NES"""
|
|
self.send_byte(color_index)
|
|
|
|
def clock_cycle(self) -> Optional[int]:
|
|
"""
|
|
Execute one clock cycle of controller port communication.
|
|
|
|
Returns: Received byte (if complete), None otherwise
|
|
"""
|
|
if not self.tx_buffer:
|
|
return None
|
|
|
|
# Get current byte to send
|
|
current_byte = self.tx_buffer[0]
|
|
|
|
# Extract current bit
|
|
bit = (current_byte >> self.bit_index) & 1
|
|
|
|
# Set OUT pin voltage (cartridge → NES)
|
|
self.port_state.out_voltage = 5.0 if bit else 0.0
|
|
|
|
# Toggle CLK
|
|
self.port_state.clk_voltage = 5.0 if (self.cycle_count % 2) else 0.0
|
|
|
|
# Read IN pin (NES → cartridge)
|
|
received_bit = 1 if self.port_state.in_voltage > 2.5 else 0
|
|
|
|
# Advance bit index
|
|
self.bit_index += 1
|
|
|
|
# Check if byte complete
|
|
if self.bit_index >= 8:
|
|
self.tx_buffer.pop(0)
|
|
self.bit_index = 0
|
|
return current_byte
|
|
|
|
return None
|
|
|
|
@property
|
|
def cycle_count(self) -> int:
|
|
return self.cartridge.cycle_count
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Unified Cartridge Stack
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
class UnifiedCartridgeStack:
|
|
"""Unified cartridge CPU + controller port stack"""
|
|
|
|
def __init__(self):
|
|
# Cartridge SUBLEQ CPU
|
|
self.cartridge = CartridgeSUBLEQ()
|
|
|
|
# Proto-shader
|
|
self.shader = CartridgePaletteShader()
|
|
|
|
# Controller port protocol
|
|
self.protocol = ControllerPortProtocol(self.cartridge)
|
|
|
|
# Shader parameters
|
|
self.shader_params = bytes([0x05, 0x02])
|
|
|
|
def generate_cartridge_program(self) -> List[Tuple[int, int, int]]:
|
|
"""
|
|
Generate SUBLEQ program that:
|
|
1. Runs proto-shader to generate palette
|
|
2. Decompresses GCL audio data
|
|
3. Streams square wave parameters via controller port
|
|
"""
|
|
code = []
|
|
|
|
# Initialize output buffer at $1000
|
|
# $1000 = output_ptr
|
|
# $1002 = frame_counter
|
|
# $1004 = temp
|
|
|
|
code.append((0x1004, 0x1004, 1)) # Zero temp
|
|
code.append((0x1004, 0x1000, 2)) # Zero output_ptr
|
|
code.append((0x1004, 0x1002, 3)) # Zero frame_counter
|
|
|
|
# Main loop
|
|
code.append((0x1004, 0x1002, 4)) # temp = frame_counter
|
|
|
|
# Generate square wave parameters (simplified)
|
|
# frequency = base + frame_counter * 10
|
|
code.append((0x1004, 0x0A, 5)) # temp = 10
|
|
code.append((0x1002, 0x1004, 6)) # temp = frame_counter - 10
|
|
code.append((0x1004, 0x1000, 7)) # output_ptr = temp (frequency)
|
|
|
|
# duty = frame_counter % 4
|
|
code.append((0x1002, 0x1004, 8)) # temp = frame_counter
|
|
code.append((0x1004, 0x1000, 9)) # output_ptr = temp (duty)
|
|
|
|
# volume = 15
|
|
code.append((0x1004, 0x0F, 10)) # temp = 15
|
|
code.append((0x1004, 0x1000, 11)) # output_ptr = temp (volume)
|
|
|
|
# Increment frame counter
|
|
code.append((0x1004, 0x1002, 12)) # temp = frame_counter
|
|
code.append((0x1004, 0x1002, 13)) # frame_counter = temp - 1 (actually increment)
|
|
|
|
# Loop
|
|
code.append((0x1004, 0x1004, 4)) # Loop back
|
|
|
|
# Halt
|
|
code.append((0x1004, 0x1004, 0xFFFF)) # Halt
|
|
|
|
return code
|
|
|
|
def run_unified_stack(self):
|
|
"""Run unified cartridge stack"""
|
|
print("=" * 70)
|
|
print("UNIFIED CARTRIDGE CPU + CONTROLLER PORT STACK")
|
|
print("=" * 70)
|
|
|
|
# Load program
|
|
print("\n[*] Loading SUBLEQ program into cartridge CPU...")
|
|
program = self.generate_cartridge_program()
|
|
self.cartridge.load_program(program)
|
|
print(" Program: {} instructions".format(len(program)))
|
|
|
|
# Run cartridge CPU
|
|
print("\n[*] Running cartridge SUBLEQ CPU...")
|
|
self.cartridge.run(max_cycles=100)
|
|
print(" Cycles: {}".format(self.cartridge.cycle_count))
|
|
print(" Halted: {}".format(self.cartridge.halted))
|
|
|
|
# Send data via controller port
|
|
print("\n[*] Streaming data via controller port (voltage shifter)...")
|
|
|
|
# Send square wave parameters
|
|
for i in range(10):
|
|
freq = 100 + i * 10
|
|
params = SquareWaveParams(frequency=freq, duty=i % 4, volume=15)
|
|
self.protocol.send_square_wave(params)
|
|
|
|
# Send palette colors
|
|
for i in range(8):
|
|
color = self.shader.shader_compute(i * 32, i * 32, i * 32, self.shader_params)
|
|
self.protocol.send_palette_color(color)
|
|
|
|
# Simulate clock cycles
|
|
received_bytes = []
|
|
for _ in range(100):
|
|
byte = self.protocol.clock_cycle()
|
|
if byte is not None:
|
|
received_bytes.append(byte)
|
|
|
|
print(" Bytes sent: {}".format(len(self.protocol.tx_buffer)))
|
|
print(" Bytes received: {}".format(len(received_bytes)))
|
|
|
|
# Show port state
|
|
print("\n[*] Controller port state (voltage levels):")
|
|
print(" CLK: {:.1f}V".format(self.protocol.port_state.clk_voltage))
|
|
print(" OUT: {:.1f}V".format(self.protocol.port_state.out_voltage))
|
|
print(" IN: {:.1f}V".format(self.protocol.port_state.in_voltage))
|
|
|
|
print("\n" + "=" * 70)
|
|
print("UNIFIED CARTRIDGE STACK COMPLETE")
|
|
print("=" * 70)
|
|
print("\n[*] Architecture Summary:")
|
|
print(" Cartridge CPU: SUBLEQ processor")
|
|
print(" Proto-Shader: NES palette generator")
|
|
print(" GCL Compression: Delta encoding")
|
|
print(" Square Wave: Audio parameters")
|
|
print(" Controller Port: Voltage-shifting communication")
|
|
print("\n[*] NES reads from $4016/$4017, cartridge streams via port")
|
|
print("[*] Voltage shifter handles 3.3V ↔ 5V level conversion")
|
|
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
# Test / Demo
|
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
def run_test():
|
|
"""Run unified cartridge stack test"""
|
|
stack = UnifiedCartridgeStack()
|
|
stack.run_unified_stack()
|
|
|
|
if __name__ == "__main__":
|
|
run_test()
|