Files
stack/uasm.py
2026-03-07 23:45:11 -05:00

355 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Microcode Assembler for microcoded CPU
Parses .src files and generates Verilog hex files
"""
import sys
import re
class Field:
"""Represents a microcode field definition"""
def __init__(self, name, width, default, enums):
self.name = name
self.width = width
self.default = default # Can be int or str
self.enums = enums # Dict of name -> value
self.position = 0 # Bit position (set later)
def encode(self, value):
"""Encode a value for this field"""
if isinstance(value, str):
if value in self.enums:
return self.enums[value]
else:
raise ValueError(f"Unknown enum '{value}' for field '{self.name}'. Valid: {list(self.enums.keys())}")
return int(value)
class MicrocodeAssembler:
def __init__(self):
self.fields = []
self.field_map = {}
self.subroutines = []
self.sub_addresses = {}
# Config
self.microcode_length = 512
self.microcode_width = 36
self.microcode_output = 'ucode.hex'
self.mapping_length = 32
self.mapping_output = 'umap.hex'
self.mapping_entries = [0] * 32 # Default all to 0 (fetch)
def parse_config(self, line):
"""Parse configuration lines"""
parts = line.split()
if len(parts) < 3:
return
category, key, value = parts[0], parts[1], parts[2]
if category == 'microcode':
if key == 'length':
self.microcode_length = int(value)
elif key == 'width':
self.microcode_width = int(value)
elif key == 'output':
self.microcode_output = value
elif category == 'mapping':
if key == 'length':
self.mapping_length = int(value)
self.mapping_entries = [0] * self.mapping_length
elif key == 'output':
self.mapping_output = value
def parse_field(self, line):
"""
Parse field definition: field <name> <width> <default> [enums]
Example: field rd 3 imm pcp=0,dsp=1,rsp=2,tmp=3,mdr=4,tos=5,acc=6,imm=7
"""
parts = line.split(None, 3) # Split into max 4 parts
if len(parts) < 4:
raise ValueError(f"Invalid field: {line}")
name = parts[1]
width = int(parts[2])
rest = parts[3] # "default [enums...]"
# Split rest into tokens
tokens = rest.split()
default_str = tokens[0]
# Parse enums (everything after first token, comma-separated)
enums = {}
if len(tokens) > 1:
enum_str = ' '.join(tokens[1:])
for i, item in enumerate(enum_str.split(',')):
item = item.strip()
if '=' in item:
k, v = item.split('=')
enums[k.strip()] = int(v.strip())
else:
enums[item] = i
# Resolve default
if default_str.isdigit():
default_value = int(default_str)
elif default_str in enums:
default_value = enums[default_str]
else:
# Default is a symbolic name (like 'imm') that should be in enums
# If not in enums yet, we'll handle it during encoding
default_value = default_str
field = Field(name, width, default_value, enums)
self.fields.append(field)
self.field_map[name] = field
def layout_fields(self):
"""Assign bit positions to fields (LSB first)"""
pos = 0
for field in self.fields:
field.position = pos
pos += field.width
if pos != self.microcode_width:
print(f"Warning: Fields total {pos} bits, microcode width is {self.microcode_width}")
def parse_subroutine(self, lines, start_idx):
"""Parse a subroutine block, returns (name, address, instructions, next_idx)"""
# sub <name> [address]
sub_match = re.match(r'sub\s+(\w+)(?:\s+([0-9A-Fa-fx]+h?))?', lines[start_idx])
if not sub_match:
raise ValueError(f"Invalid sub: {lines[start_idx]}")
name = sub_match.group(1)
address = None
if sub_match.group(2):
addr_str = sub_match.group(2).replace('h', '').replace('x', '')
address = int(addr_str, 16)
# Collect instructions until 'end'
instructions = []
idx = start_idx + 1
while idx < len(lines):
line = lines[idx].strip()
if line.startswith('end'):
break
if line and not line.startswith(';'):
instructions.append(line)
idx += 1
return name, address, instructions, idx + 1
def parse_map(self, line):
"""Parse map statement"""
# map entry1,entry2,entry3*count,...
map_content = line[3:].strip()
entries = [e.strip() for e in map_content.split(',')]
map_idx = 0
for entry in entries:
if '*' in entry:
# Repeat entry
name, count_str = entry.split('*')
name = name.strip()
count = int(count_str.strip())
if name not in self.sub_addresses:
raise ValueError(f"Unknown subroutine '{name}' in map")
addr = self.sub_addresses[name]
for _ in range(count):
if map_idx >= self.mapping_length:
raise ValueError("Map overflow")
self.mapping_entries[map_idx] = addr
map_idx += 1
else:
# Single entry
if entry not in self.sub_addresses:
raise ValueError(f"Unknown subroutine '{entry}' in map")
self.mapping_entries[map_idx] = self.sub_addresses[entry]
map_idx += 1
def assemble_instruction(self, inst_line):
"""Assemble a microcode instruction line into a word"""
# Initialize with defaults
field_values = {}
for field in self.fields:
if isinstance(field.default, str):
# Resolve symbolic default
if field.default in field.enums:
field_values[field.name] = field.enums[field.default]
else:
raise ValueError(f"Default '{field.default}' not in enums for field '{field.name}'")
else:
field_values[field.name] = field.default
# Parse assignments
for assign in inst_line.split(','):
assign = assign.strip()
if '=' in assign:
field_name, value_str = assign.split('=', 1)
field_name = field_name.strip()
value_str = value_str.strip()
if field_name not in self.field_map:
raise ValueError(f"Unknown field '{field_name}': {inst_line}")
field = self.field_map[field_name]
# Parse value
if value_str.endswith('h'):
value = int(value_str[:-1], 16)
elif value_str.startswith('0x'):
value = int(value_str, 16)
elif value_str.isdigit():
value = int(value_str)
else:
# Symbolic value
value = field.encode(value_str)
field_values[field_name] = value
else:
# Just field name -> set to 1
if assign not in self.field_map:
raise ValueError(f"Unknown field '{assign}': {inst_line}")
field_values[assign] = 1
# Pack into word
word = 0
for field in self.fields:
value = field_values[field.name]
if value >= (1 << field.width):
raise ValueError(f"Value {value} too large for field '{field.name}' (width {field.width})")
word |= (value << field.position)
return word
def assemble(self, source_file):
"""Main assembly"""
with open(source_file, 'r') as f:
lines = [line.strip() for line in f.readlines()]
# Pass 1: Config and fields
idx = 0
while idx < len(lines):
line = lines[idx]
if not line or line.startswith(';'):
idx += 1
continue
if line.startswith('microcode') or line.startswith('mapping'):
self.parse_config(line)
elif line.startswith('field'):
self.parse_field(line)
elif line.startswith('sub'):
break
idx += 1
self.layout_fields()
# Pass 2: Subroutines
while idx < len(lines):
line = lines[idx]
if not line or line.startswith(';'):
idx += 1
continue
if line.startswith('sub'):
name, address, instructions, next_idx = self.parse_subroutine(lines, idx)
self.subroutines.append((name, address, instructions))
idx = next_idx
elif line.startswith('map'):
break
else:
idx += 1
# Assign addresses - FIXED ALGORITHM
# Build a set of all occupied addresses from explicit subroutines
occupied = set()
for name, address, instructions in self.subroutines:
if address is not None:
self.sub_addresses[name] = address
# Mark all addresses this subroutine occupies
for i in range(len(instructions)):
occupied.add(address + i)
# Now assign auto addresses to remaining subroutines
auto_addr = 0
for name, address, instructions in self.subroutines:
if address is None:
# Find next sequence of free addresses long enough for this subroutine
while any((auto_addr + i) in occupied for i in range(len(instructions))):
auto_addr += 1
self.sub_addresses[name] = auto_addr
# Mark these addresses as occupied
for i in range(len(instructions)):
occupied.add(auto_addr + i)
auto_addr += len(instructions)
# Pass 3: Map
while idx < len(lines):
line = lines[idx]
if not line or line.startswith(';'):
idx += 1
continue
if line.startswith('map'):
self.parse_map(line)
idx += 1
# Generate microcode ROM
microcode_rom = [0] * self.microcode_length
for name, _, instructions in self.subroutines:
addr = self.sub_addresses[name]
for inst in instructions:
if addr >= self.microcode_length:
raise ValueError(f"Address {addr} overflow")
microcode_rom[addr] = self.assemble_instruction(inst)
addr += 1
# Write outputs
self.write_hex(self.microcode_output, microcode_rom, self.microcode_width)
self.write_hex(self.mapping_output, self.mapping_entries, 9)
print(f"Assembly complete!")
print(f" Microcode: {self.microcode_output}")
print(f" Mapping: {self.mapping_output}")
print(f"\nSubroutines:")
for name, addr in sorted(self.sub_addresses.items(), key=lambda x: x[1]):
# Find instruction count
inst_count = next(len(inst) for n, a, inst in self.subroutines if n == name)
print(f" {name:12s} @ 0x{addr:03X} ({inst_count} instructions)")
def write_hex(self, filename, data, width):
"""Write Verilog hex file"""
with open(filename, 'w') as f:
hex_digits = (width + 3) // 4
for word in data:
f.write(f"{word:0{hex_digits}X}\n")
def main():
if len(sys.argv) != 2:
print(f"Usage: {sys.argv[0]} <source.src>")
sys.exit(1)
asm = MicrocodeAssembler()
try:
asm.assemble(sys.argv[1])
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()