Research-Stack/5-Applications/scripts/unified_cartridge_controller_stack.py

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()