# SPDX-FileCopyrightText: Copyright (c) 2021 Scott Shawcroft for Adafruit Industries LLC # # SPDX-License-Identifier: MIT """ `adafruit_pioasm` ================================================================================ Simple assembler to convert pioasm to bytes * Author(s): Scott Shawcroft """ import array import re splitter = re.compile(r",\s*|\s+(?:,\s*)?").split mov_splitter = re.compile("!|~|::").split __version__ = "0.0.0+auto.0" __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_PIOASM.git" CONDITIONS = ["", "!x", "x--", "!y", "y--", "x!=y", "pin", "!osre"] IN_SOURCES = ["pins", "x", "y", "null", None, None, "isr", "osr"] OUT_DESTINATIONS = ["pins", "x", "y", "null", "pindirs", "pc", "isr", "exec"] WAIT_SOURCES = ["gpio", "pin", "irq", None] MOV_DESTINATIONS = ["pins", "x", "y", None, "exec", "pc", "isr", "osr"] MOV_SOURCES = ["pins", "x", "y", "null", None, "status", "isr", "osr"] MOV_OPS = [None, "~", "::", None] SET_DESTINATIONS = ["pins", "x", "y", None, "pindirs", None, None, None] class Program: # pylint: disable=too-few-public-methods """Encapsulates a program's instruction stream and configuration flags Example:: program = adafruit_pioasm.Program(...) state_machine = rp2pio.StateMachine(program.assembled, ..., **program.pio_kwargs) """ def __init__(self, text_program: str, *, build_debuginfo=False) -> None: """Converts pioasm text to encoded instruction bytes""" # pylint: disable=too-many-branches,too-many-statements,too-many-locals assembled = [] program_name = None labels = {} linemap = [] instructions = [] sideset_count = 0 sideset_enable = 0 wrap = None wrap_target = None offset = -1 for i, line in enumerate(text_program.split("\n")): line = line.strip() if not line: continue if ";" in line: line = line.split(";")[0].strip() if line.startswith(".program"): if program_name: raise RuntimeError("Multiple programs not supported") program_name = line.split()[1] elif line.startswith(".offset"): offset = int(line.split()[1], 0) elif line.startswith(".wrap_target"): wrap_target = len(instructions) elif line.startswith(".wrap"): if len(instructions) == 0: raise RuntimeError("Cannot have .wrap as first instruction") wrap = len(instructions) - 1 elif line.startswith(".side_set"): sideset_count = int(line.split()[1], 0) sideset_enable = "opt" in line elif line.endswith(":"): label = line[:-1] if label in labels: raise SyntaxError(f"Duplicate label {repr(label)}") labels[label] = len(instructions) elif line: # Only add as an instruction if the line isn't empty instructions.append(line) linemap.append(i) max_delay = 2 ** (5 - sideset_count - sideset_enable) - 1 assembled = [] for instruction in instructions: # print(instruction) instruction = splitter(instruction.strip()) delay = 0 if instruction[-1].endswith("]"): # Delay delay = int(instruction[-1].strip("[]"), 0) if delay < 0: raise RuntimeError("Delay negative:", delay) if delay > max_delay: raise RuntimeError("Delay too long:", delay) instruction.pop() if len(instruction) > 1 and instruction[-2] == "side": if sideset_count == 0: raise RuntimeError("No side_set count set") sideset_value = int(instruction[-1], 0) if sideset_value >= 2**sideset_count: raise RuntimeError("Sideset value too large") delay |= sideset_value << (5 - sideset_count - sideset_enable) delay |= sideset_enable << 4 instruction.pop() instruction.pop() if instruction[0] == "nop": # mov delay y op y assembled.append(0b101_00000_010_00_010) elif instruction[0] == "jmp": # instr delay cnd addr assembled.append(0b000_00000_000_00000) target = instruction[-1] if target[:1] in "0123456789": assembled[-1] |= int(target, 0) elif instruction[-1] in labels: assembled[-1] |= labels[target] else: raise SyntaxError(f"Invalid jmp target {repr(target)}") if len(instruction) > 2: try: assembled[-1] |= CONDITIONS.index(instruction[1]) << 5 except ValueError as exc: raise ValueError( f"Invalid jmp condition '{instruction[1]}'" ) from exc elif instruction[0] == "wait": # instr delay p sr index assembled.append(0b001_00000_0_00_00000) polarity = int(instruction[1], 0) if not 0 <= polarity <= 1: raise RuntimeError("Invalid polarity") assembled[-1] |= polarity << 7 assembled[-1] |= WAIT_SOURCES.index(instruction[2]) << 5 num = int(instruction[3], 0) if not 0 <= num <= 31: raise RuntimeError("Wait num out of range") assembled[-1] |= num if instruction[-1] == "rel": assembled[-1] |= 0x10 # Set the high bit of the irq value elif instruction[0] == "in": # instr delay src count assembled.append(0b010_00000_000_00000) assembled[-1] |= IN_SOURCES.index(instruction[1]) << 5 count = int(instruction[-1], 0) if not 1 <= count <= 32: raise RuntimeError("Count out of range") assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top elif instruction[0] == "out": # instr delay dst count assembled.append(0b011_00000_000_00000) assembled[-1] |= OUT_DESTINATIONS.index(instruction[1]) << 5 count = int(instruction[-1], 0) if not 1 <= count <= 32: raise RuntimeError("Count out of range") assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top elif instruction[0] == "push" or instruction[0] == "pull": # instr delay d i b zero assembled.append(0b100_00000_0_0_0_00000) if instruction[0] == "pull": assembled[-1] |= 0x80 if instruction[-1] == "block" or not instruction[-1].endswith("block"): assembled[-1] |= 0x20 if len(instruction) > 1 and instruction[1] in ("ifempty", "iffull"): assembled[-1] |= 0x40 elif instruction[0] == "mov": # instr delay dst op src assembled.append(0b101_00000_000_00_000) assembled[-1] |= MOV_DESTINATIONS.index(instruction[1]) << 5 source = instruction[-1] source_split = mov_splitter(source) if len(source_split) == 1: try: assembled[-1] |= MOV_SOURCES.index(source) except ValueError as exc: raise ValueError(f"Invalid mov source '{source}'") from exc else: assembled[-1] |= MOV_SOURCES.index(source_split[1]) if source[:1] == "!": assembled[-1] |= 0x08 elif source[:1] == "~": assembled[-1] |= 0x08 elif source[:2] == "::": assembled[-1] |= 0x10 else: raise RuntimeError("Invalid mov operator:", source[:1]) if len(instruction) > 3: assembled[-1] |= MOV_OPS.index(instruction[-2]) << 3 elif instruction[0] == "irq": # instr delay z c w index assembled.append(0b110_00000_0_0_0_00000) if instruction[-1] == "rel": assembled[-1] |= 0x10 # Set the high bit of the irq value instruction.pop() num = int(instruction[-1], 0) if not 0 <= num <= 7: raise RuntimeError("Interrupt index out of range") assembled[-1] |= num if len(instruction) == 3: # after rel has been removed if instruction[1] == "wait": assembled[-1] |= 0x20 elif instruction[1] == "clear": assembled[-1] |= 0x40 # All other values are the default of set without waiting elif instruction[0] == "set": # instr delay dst data assembled.append(0b111_00000_000_00000) try: assembled[-1] |= SET_DESTINATIONS.index(instruction[1]) << 5 except ValueError as exc: raise ValueError( f"Invalid set destination '{instruction[1]}'" ) from exc value = int(instruction[-1], 0) if not 0 <= value <= 31: raise RuntimeError("Set value out of range") assembled[-1] |= value else: raise RuntimeError("Unknown instruction:" + instruction[0]) assembled[-1] |= delay << 8 # print(bin(assembled[-1])) self.pio_kwargs = { "sideset_enable": sideset_enable, } if offset != -1: self.pio_kwargs["offset"] = offset if sideset_count != 0: self.pio_kwargs["sideset_pin_count"] = sideset_count if wrap is not None: self.pio_kwargs["wrap"] = wrap if wrap_target is not None: self.pio_kwargs["wrap_target"] = wrap_target self.assembled = array.array("H", assembled) if build_debuginfo: self.debuginfo = (linemap, text_program) else: self.debuginfo = None def print_c_program(self, name, qualifier="const"): """Print the program into a C program snippet""" if self.debuginfo is None: linemap = None program_lines = None else: linemap = self.debuginfo[0][:] # Use a copy since we destroy it program_lines = self.debuginfo[1].split("\n") print( f"{qualifier} int {name}_wrap = {self.pio_kwargs.get('wrap', len(self.assembled)-1)};" ) print( f"{qualifier} int {name}_wrap_target = {self.pio_kwargs.get('wrap_target', 0)};" ) sideset_pin_count = self.pio_kwargs.get("sideset_pin_count", 0) print(f"{qualifier} int {name}_sideset_pin_count = {sideset_pin_count};") print( f"{qualifier} bool {name}_sideset_enable = {self.pio_kwargs['sideset_enable']};" ) print(f"{qualifier} uint16_t {name}[] = " + "{") last_line = 0 if linemap: for inst in self.assembled: next_line = linemap[0] del linemap[0] while last_line < next_line: line = program_lines[last_line] if line: print(f" // {line}") last_line += 1 line = program_lines[last_line] print(f" 0x{inst:04x}, // {line}") last_line += 1 while last_line < len(program_lines): line = program_lines[last_line] if line: print(f" // {line}") last_line += 1 else: for i in range(0, len(self.assembled), 8): print( " " + ", ".join("0x%04x" % i for i in self.assembled[i : i + 8]) ) print("};") print() def assemble(program_text: str) -> array.array: """Converts pioasm text to encoded instruction bytes In new code, prefer to use the `Program` class so that the extra arguments such as the details about side-set pins can be easily passsed to the ``StateMachine`` constructor.""" return Program(program_text).assembled