285 lines
9.2 KiB
Python
285 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
CBE System Disk Builder - Clean Build
|
|
Builds a 10MB disk image with a TinyCPU OS kernel.
|
|
"""
|
|
import os, shutil, subprocess, struct
|
|
|
|
OUT = "/root/CBE/CBE"
|
|
BUILD = os.path.join(OUT, 'build')
|
|
DISK_SIZE = 10 * 1024 * 1024
|
|
|
|
OPCODES = {
|
|
'nop':0x00,'mova_b':0x01,'movia':0x02,'add':0x03,
|
|
'sub':0x04,'jmpa':0x05,'jza':0x06,'jca':0x07,
|
|
'cmp':0x08,'movib':0x09,'store':0x0A,'load':0x0B,
|
|
'jmpi':0x0C,'jzi':0x0D,'jci':0x0E,'inc':0x0F,
|
|
'call':0x10,'ret':0x11,'hlt':0xFF,
|
|
}
|
|
|
|
GPU=0xC0; KST=0xFA; KDT=0xFB; DSK=0xB0; DRD=0xB1; DDT=0xB2
|
|
VC=0x50; VT=0x51; LB=0x60
|
|
|
|
class Asm:
|
|
def __init__(self):
|
|
self.ops = []; self.labels = {}
|
|
def label(self, n): self.ops.append(('_label', n))
|
|
def op(self, m, a=None): self.ops.append((m, a))
|
|
def db(self, d):
|
|
if isinstance(d, int): d = bytes([d])
|
|
elif isinstance(d, str): d = d.encode()
|
|
self.ops.append(('_db', bytes(d)))
|
|
|
|
def assemble(self, origin=0, max_size=1024):
|
|
pos = origin
|
|
for m, a in self.ops:
|
|
if m == '_label': self.labels[a] = pos
|
|
elif m == '_db': pos += len(a)
|
|
else: pos += 2 if a is not None else 1
|
|
total = pos - origin
|
|
if total > max_size: raise RuntimeError(f"Overflow: {total}>{max_size}")
|
|
buf, pos2, err = bytearray(max_size), origin, False
|
|
for m, a in self.ops:
|
|
if m == '_label': continue
|
|
o = pos2 - origin
|
|
if m == '_db':
|
|
for b in a: buf[o]=b; o+=1; pos2+=1
|
|
continue
|
|
if m not in OPCODES: print(f" Bad op '{m}'"); err=True; pos2+=1; continue
|
|
buf[o] = OPCODES[m]; pos2+=1
|
|
if a is not None:
|
|
o2 = pos2 - origin
|
|
if isinstance(a, str):
|
|
if a in self.labels: buf[o2] = self.labels[a] & 0xFF
|
|
else: print(f" Unresolved '{a}'"); buf[o2]=0; err=True
|
|
else: buf[o2] = a & 0xFF
|
|
pos2+=1
|
|
return bytes(buf[:total]), err
|
|
|
|
|
|
def build_kernel():
|
|
a = Asm()
|
|
|
|
# === START (entry at 0x00) ===
|
|
# CPU boots at PC=0, jump to main immediately
|
|
a.label('start')
|
|
a.op('jmpi', 'main')
|
|
|
|
# === PRINT SUBROUTINE ===
|
|
# Call with: B=0, write string addr to print_oper, CALL print_sub
|
|
a.label('print_sub')
|
|
a.op('load') # LOAD_A with self-modifying operand
|
|
a.label('print_oper')
|
|
a.db(0x00) # operand byte = address of current char
|
|
a.op('cmp') # A vs B (0 = null)
|
|
a.op('jzi', 'print_end')
|
|
a.op('store', GPU) # output char
|
|
a.op('load', 'print_oper')# load pointer value
|
|
a.op('inc')
|
|
a.op('store', 'print_oper')
|
|
a.op('jmpi', 'print_sub')
|
|
a.label('print_end')
|
|
a.op('ret')
|
|
|
|
# === SELF-MODDING STORE ===
|
|
# Jump here with: A=char, store target addr in store_oper
|
|
# After execution, continues at after_store
|
|
a.label('sma_store')
|
|
a.op('store') # STORE_A with self-modifying operand
|
|
a.label('store_oper')
|
|
a.db(0x00) # operand = target address
|
|
# Continuation at sma_store+2:
|
|
a.label('after_store')
|
|
a.op('load', VC)
|
|
a.op('inc')
|
|
a.op('store', VC)
|
|
a.op('jmpi', 'kbd')
|
|
|
|
# === CMD: help ===
|
|
a.label('cmd_help')
|
|
a.op('movib', 0)
|
|
a.op('movia', 'str_help')
|
|
a.op('store', 'print_oper')
|
|
a.op('call', 'print_sub')
|
|
a.op('jmpi', 'prompt')
|
|
|
|
# === CMD: clear ===
|
|
a.label('cmd_clear')
|
|
a.op('movia', 0x0C); a.op('store', GPU)
|
|
a.op('jmpi', 'prompt')
|
|
|
|
# === CMD: reboot ===
|
|
a.label('cmd_reboot')
|
|
a.op('jmpi', 0x00)
|
|
|
|
# === CMD: cat (read sector 3 and print) ===
|
|
a.label('cmd_cat')
|
|
a.op('movia', 3); a.op('store', DSK)
|
|
a.label('cat_wait')
|
|
a.op('load', DRD); a.op('movib', 1); a.op('cmp')
|
|
a.op('jzi', 'cat_ready'); a.op('jmpi', 'cat_wait')
|
|
a.label('cat_ready')
|
|
a.op('movib', 0); a.op('movia', DDT)
|
|
a.op('store', 'print_oper'); a.op('call', 'print_sub')
|
|
a.op('jmpi', 'prompt')
|
|
|
|
# === CMD: ls (read sector 2 directory and print) ===
|
|
a.label('cmd_ls')
|
|
a.op('movia', 2); a.op('store', DSK)
|
|
a.label('ls_wait')
|
|
a.op('load', DRD); a.op('movib', 1); a.op('cmp')
|
|
a.op('jzi', 'ls_ready'); a.op('jmpi', 'ls_wait')
|
|
a.label('ls_ready')
|
|
a.op('movib', 0); a.op('movia', DDT)
|
|
a.op('store', 'print_oper'); a.op('call', 'print_sub')
|
|
a.op('jmpi', 'prompt')
|
|
|
|
# === MAIN ===
|
|
a.label('main')
|
|
a.label('prompt')
|
|
a.op('movia', 0x0D); a.op('store', GPU)
|
|
a.op('movia', 0x0A); a.op('store', GPU)
|
|
a.op('movia', ord('>')); a.op('store', GPU)
|
|
a.op('movia', ord(' ')); a.op('store', GPU)
|
|
a.op('movib', 0)
|
|
a.op('store', VC)
|
|
|
|
# === KBD LOOP ===
|
|
a.label('kbd')
|
|
a.op('load', KST); a.op('cmp'); a.op('jzi', 'kbd')
|
|
a.op('load', KDT)
|
|
a.op('store', VT); a.op('store', GPU)
|
|
a.op('movia', 0); a.op('store', KST) # ack
|
|
|
|
# Check Enter (VK_ENTER=0x0A)
|
|
a.op('load', VT); a.op('movia', 0x0A); a.op('cmp')
|
|
a.op('jzi', 'process')
|
|
|
|
# Check Backspace (VK_BACK_SPACE=0x08)
|
|
a.op('load', VT); a.op('movia', 0x08); a.op('cmp')
|
|
a.op('jzi', 'backspace')
|
|
|
|
# Buffer full?
|
|
a.op('load', VC); a.op('movia', 15); a.op('cmp')
|
|
a.op('jzi', 'kbd')
|
|
|
|
# Store char: self-modding via sma_store
|
|
a.op('load', VC); a.op('movib', LB); a.op('add')
|
|
a.op('store', 'store_oper')
|
|
a.op('load', VT)
|
|
a.op('jmpi', 'sma_store')
|
|
# after_store continues with cursor++ and jmpi kbd
|
|
|
|
# === BACKSPACE ===
|
|
a.label('backspace')
|
|
a.op('load', VC); a.op('cmp'); a.op('jzi', 'kbd')
|
|
a.op('load', VC); a.op('movib', 1); a.op('sub')
|
|
a.op('store', VC)
|
|
a.op('jmpi', 'kbd')
|
|
|
|
# === PROCESS LINE ===
|
|
a.label('process')
|
|
a.op('movia', 0x0D); a.op('store', GPU)
|
|
a.op('movia', 0x0A); a.op('store', GPU)
|
|
|
|
# Dispatch (VK codes: H=72, C=67, R=82, D=68, L=76)
|
|
a.op('load', LB)
|
|
a.op('movia', 72); a.op('cmp'); a.op('jzi', 'cmd_help')
|
|
a.op('load', LB)
|
|
a.op('movia', 67); a.op('cmp'); a.op('jzi', 'cmd_clear')
|
|
a.op('load', LB)
|
|
a.op('movia', 82); a.op('cmp'); a.op('jzi', 'cmd_reboot')
|
|
a.op('load', LB)
|
|
a.op('movia', 68); a.op('cmp'); a.op('jzi', 'cmd_cat')
|
|
a.op('load', LB)
|
|
a.op('movia', 76); a.op('cmp'); a.op('jzi', 'cmd_ls')
|
|
|
|
# Unknown
|
|
a.op('movia', ord('?')); a.op('store', GPU)
|
|
a.op('movia', 0x0A); a.op('store', GPU)
|
|
a.op('jmpi', 'prompt')
|
|
|
|
# === STRINGS ===
|
|
a.label('str_help')
|
|
a.db(b'Commands: H=help C=clear R=reboot D=cat L=ls\0')
|
|
|
|
return a
|
|
|
|
|
|
def build_disk(kernel):
|
|
if kernel is None: return None
|
|
disk = bytearray(DISK_SIZE)
|
|
disk[510] = 0x55; disk[511] = 0xAA
|
|
ks = min(len(kernel), 512)
|
|
disk[512:512+ks] = kernel[:ks]
|
|
# Sector 2: directory text
|
|
dir_txt = b"FILES: README HELLO\0"
|
|
disk[1024:1024+len(dir_txt)] = dir_txt
|
|
# Sector 3: README
|
|
readme = b"CBE/OS v2.0 for TinyCPU\r\nCommands: H C R D L\r\n\0"
|
|
disk[1536:1536+len(readme)] = readme
|
|
# Sector 4: HELLO
|
|
hello = b"Hello from CBE/OS!\r\n\0"
|
|
disk[2048:2048+len(hello)] = hello
|
|
return bytes(disk)
|
|
|
|
|
|
def main():
|
|
os.makedirs(BUILD, exist_ok=True)
|
|
print("CBE Disk Builder - Clean Build")
|
|
print("=" * 40)
|
|
|
|
a = build_kernel()
|
|
print(f"Ops: {len(a.ops)}")
|
|
code, err = a.assemble(0, 1024)
|
|
if err:
|
|
for n, addr in sorted(a.labels.items(), key=lambda x: x[1] if x[1] is not None else 0):
|
|
print(f" {n}: 0x{addr:02X}" if addr is not None else f" {n}: ???")
|
|
return 1
|
|
|
|
print(f"Kernel: {len(code)} bytes")
|
|
for n, addr in sorted(a.labels.items(), key=lambda x: x[1]):
|
|
print(f" 0x{addr:02X}: {n}")
|
|
|
|
# Check for label positions
|
|
if a.labels.get('start') != 0:
|
|
print(f"ERROR: start should be 0x00, got 0x{a.labels.get('start', -1):02X}")
|
|
return 1
|
|
if a.labels.get('print_oper', -1) != a.labels.get('print_sub', -1) + 1:
|
|
print(f"ERROR: print_oper not at print_sub+1")
|
|
return 1
|
|
if a.labels.get('store_oper', -1) != a.labels.get('sma_store', -1) + 1:
|
|
print(f"ERROR: store_oper not at sma_store+1")
|
|
return 1
|
|
|
|
disk = build_disk(code)
|
|
disk_path = os.path.join(BUILD, 'data.bin')
|
|
with open(disk_path, 'wb') as f: f.write(disk)
|
|
|
|
module_path = os.path.join(BUILD, 'big-disk.disk', 'banks', 'data.bin')
|
|
os.makedirs(os.path.dirname(module_path), exist_ok=True)
|
|
with open(module_path, 'wb') as f: f.write(disk)
|
|
|
|
jar = os.path.join(OUT, 'modules', 'gui', 'build', 'stage', 'cbe-emu.jar')
|
|
if os.path.exists(jar):
|
|
src_dir = os.path.join(BUILD, 'big-disk.disk')
|
|
out_name = os.path.join(BUILD, 'big-disk.cbeplugin')
|
|
print(f"Compiling...")
|
|
r = subprocess.run(['java', '-cp', jar, 'com.cbe.cbecc.Main', 'build', src_dir, '-o', out_name],
|
|
capture_output=True, text=True, timeout=30)
|
|
if r.returncode == 0: print(" OK")
|
|
else: print(f" FAIL: {r.stderr[:200]}")
|
|
|
|
android_dir = os.path.join(OUT, 'android', 'app', 'src', 'main', 'assets', 'plugins')
|
|
os.makedirs(android_dir, exist_ok=True)
|
|
src = os.path.join(BUILD, 'big-disk.cbeplugin')
|
|
if os.path.exists(src): shutil.copy(src, os.path.join(android_dir, 'big-disk.cbeplugin'))
|
|
else:
|
|
print(f" JAR not found at {jar}")
|
|
|
|
print("Done!")
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
exit(main())
|