diff --git a/adafruit_pioasm.py b/adafruit_pioasm.py index 07137fe..85a5deb 100644 --- a/adafruit_pioasm.py +++ b/adafruit_pioasm.py @@ -30,178 +30,206 @@ SET_DESTINATIONS = ["pins", "x", "y", None, "pindirs", None, None, None] -def assemble(text_program): - """Converts pioasm text to encoded instruction bytes""" - # pylint: disable=too-many-branches,too-many-statements,too-many-locals - assembled = [] - program_name = None - labels = {} - instructions = [] - sideset_count = 0 - sideset_enable = 0 - for line in 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(".wrap_target"): - if len(instructions) > 0: - raise RuntimeError("wrap_target not supported") - elif line.startswith(".wrap"): - pass - elif line.startswith(".side_set"): - sideset_count = int(line.split()[1]) - sideset_enable = 1 if "opt" in line else 0 - 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) - - 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("[]")) - 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]) - 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) - elif instruction[-1] in labels: - assembled[-1] |= labels[target] - else: - raise SyntaxError(f"Invalid jmp target {repr(target)}") +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) -> None: + """Converts pioasm text to encoded instruction bytes""" + # pylint: disable=too-many-branches,too-many-statements,too-many-locals + assembled = [] + program_name = None + labels = {} + instructions = [] + sideset_count = 0 + sideset_enable = 0 + for line in 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(".wrap_target"): + if len(instructions) > 0: + raise RuntimeError("wrap_target not supported") + elif line.startswith(".wrap"): + pass + elif line.startswith(".side_set"): + sideset_count = int(line.split()[1]) + sideset_enable = 1 if "opt" in line else 0 + 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) + + 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("[]")) + 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]) + 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) + elif instruction[-1] in labels: + assembled[-1] |= labels[target] + else: + raise SyntaxError(f"Invalid jmp target {repr(target)}") - if len(instruction) > 2: + 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]) + 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]) + 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]) + 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]) + 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]) + 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] |= CONDITIONS.index(instruction[1]) << 5 + assembled[-1] |= SET_DESTINATIONS.index(instruction[1]) << 5 except ValueError as exc: raise ValueError( - f"Invalid jmp condition '{instruction[1]}'" + f"Invalid set destination '{instruction[1]}'" ) from exc - - elif instruction[0] == "wait": - # instr delay p sr index - assembled.append(0b001_00000_0_00_00000) - polarity = int(instruction[1]) - 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]) - 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]) - 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]) - 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 + value = int(instruction[-1]) + if not 0 <= value <= 31: + raise RuntimeError("Set value out of range") + assembled[-1] |= value 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]) - 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]) - 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])) - - return array.array("H", assembled) + raise RuntimeError("Unknown instruction:" + instruction[0]) + assembled[-1] |= delay << 8 + # print(bin(assembled[-1])) + + self.pio_kwargs = { + "sideset_count": sideset_count, + "sideset_enable": sideset_enable, + } + + self.assembled = array.array("H", assembled) + + +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 diff --git a/examples/txuart.py b/examples/txuart.py index e12b5ab..e3382ee 100644 --- a/examples/txuart.py +++ b/examples/txuart.py @@ -5,7 +5,7 @@ import rp2pio import adafruit_pioasm -code = adafruit_pioasm.assemble( +code = adafruit_pioasm.Program( """ .program uart_tx .side_set 1 opt @@ -26,7 +26,7 @@ class TXUART: def __init__(self, *, tx, baudrate=9600): self.pio = rp2pio.StateMachine( - code, + code.assembled, first_out_pin=tx, first_sideset_pin=tx, frequency=8 * baudrate, @@ -34,7 +34,7 @@ def __init__(self, *, tx, baudrate=9600): initial_sideset_pin_direction=1, initial_out_pin_state=1, initial_out_pin_direction=1, - sideset_enable=True, + **code.pio_kwargs, ) @property diff --git a/tests/testpioasm.py b/tests/testpioasm.py index 8a3743f..0069b0e 100644 --- a/tests/testpioasm.py +++ b/tests/testpioasm.py @@ -35,6 +35,10 @@ def assertAssemblyFails(self, source, match=None, errtype=RuntimeError): else: self.assertRaises(errtype, adafruit_pioasm.assemble, source) + def assertPioKwargs(self, source, **kw): + program = adafruit_pioasm.Program(source) + self.assertEqual(kw, program.pio_kwargs) + class TestNop(AssembleChecks): def testNonsense(self): @@ -46,8 +50,9 @@ def testNop(self): "nop\nnop", [0b101_00000_010_00_010, 0b101_00000_010_00_010] ) self.assertAssemblesTo("nop [1]", [0b101_00001_010_00_010]) + self.assertAssemblesTo("nop [31]", [0b101_11111_010_00_010]) self.assertAssemblesTo(".side_set 1\nnop side 1", [0b101_10000_010_00_010]) - self.assertAssemblesTo(".side_set 1\nnop side 1 [1]", [0b101_10001_010_00_010]) + self.assertAssemblesTo(".side_set 1\nnop side 1 [15]", [0b101_11111_010_00_010]) def testSidesetOpt(self): self.assertAssemblesTo(".side_set 1 opt\nnop side 1", [0b101_11000_010_00_010]) @@ -56,6 +61,13 @@ def testSidesetOpt(self): ".side_set 1 opt\nnop side 0 [1]", [0b101_10001_010_00_010] ) self.assertAssemblesTo(".side_set 1 opt\nnop [1]", [0b101_00001_010_00_010]) + self.assertAssemblesTo(".side_set 1 opt\nnop [7]", [0b101_00111_010_00_010]) + self.assertAssemblesTo( + ".side_set 1 opt\nnop side 1 [1]", [0b101_11001_010_00_010] + ) + self.assertAssemblesTo( + ".side_set 1 opt\nnop side 0 [7]", [0b101_10111_010_00_010] + ) def testSet(self): # non happy path @@ -91,6 +103,18 @@ def testWait(self): self.assertAssemblesTo("wait 1 irq 0", [0b001_00000_1_10_00000]) self.assertAssemblesTo("wait 0 irq 1 rel", [0b001_00000_0_10_10001]) + def testLimits(self): + self.assertAssemblyFails(".side_set 1\nnop side 2") + self.assertAssemblyFails(".side_set 1\nnop side 2 [1]") + self.assertAssemblyFails("nop [32]") + self.assertAssemblyFails(".side_set 1\nnop side 0 [16]") + self.assertAssemblyFails(".side_set 1 opt\nnop side 0 [8]") + + def testCls(self): + self.assertPioKwargs("", sideset_count=0, sideset_enable=False) + self.assertPioKwargs(".side_set 1", sideset_count=1, sideset_enable=False) + self.assertPioKwargs(".side_set 3 opt", sideset_count=3, sideset_enable=True) + class TestMov(AssembleChecks): def testMovNonHappy(self):