Files
CBE/scripts/build-disk.py
T
2026-06-04 03:22:17 +00:00

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