Skip to content

Commit

Permalink
Merge pull request #29 from jepler/program-object
Browse files Browse the repository at this point in the history
Add `Program` class, type information
  • Loading branch information
tannewt authored Jan 12, 2022
2 parents fbafe00 + 68a5881 commit 00e6216
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 173 deletions.
366 changes: 197 additions & 169 deletions adafruit_pioasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading

0 comments on commit 00e6216

Please sign in to comment.