Skip to content

Commit 9707333

Browse files
committed
Add Program class, type information
An instance of the `Program` class has the `.assembled` property as well as the `pio_kwargs` property. This allows passing information from the assembler to the StateMachine constructor, in a way that can be extended in future versions (e.g., for .wrap / .wrap_target) This re-indents a lot of code so it may be better viewed with whitespace changes hidden. Tests are added for the limits of the side-set and delay values. An off-by-one bug with the sideset_value limit was fixed.
1 parent d43cdd0 commit 9707333

File tree

2 files changed

+212
-162
lines changed

2 files changed

+212
-162
lines changed

adafruit_pioasm.py

Lines changed: 187 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -30,167 +30,193 @@
3030
SET_DESTINATIONS = ["pins", "x", "y", None, "pindirs", None, None, None]
3131

3232

33-
def assemble(text_program):
34-
"""Converts pioasm text to encoded instruction bytes"""
35-
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
36-
assembled = []
37-
program_name = None
38-
labels = {}
39-
instructions = []
40-
sideset_count = 0
41-
sideset_enable = 0
42-
for line in text_program.split("\n"):
43-
line = line.strip()
44-
if not line:
45-
continue
46-
if ";" in line:
47-
line = line.split(";")[0].strip()
48-
if line.startswith(".program"):
49-
if program_name:
50-
raise RuntimeError("Multiple programs not supported")
51-
program_name = line.split()[1]
52-
elif line.startswith(".wrap_target"):
53-
if len(instructions) > 0:
54-
raise RuntimeError("wrap_target not supported")
55-
elif line.startswith(".wrap"):
56-
pass
57-
elif line.startswith(".side_set"):
58-
sideset_count = int(line.split()[1])
59-
sideset_enable = 1 if "opt" in line else 0
60-
elif line.endswith(":"):
61-
label = line[:-1]
62-
if label in labels:
63-
raise SyntaxError(f"Duplicate label {repr(label)}")
64-
labels[label] = len(instructions)
65-
elif line:
66-
# Only add as an instruction if the line isn't empty
67-
instructions.append(line)
68-
69-
max_delay = 2 ** (5 - sideset_count - sideset_enable) - 1
70-
assembled = []
71-
for instruction in instructions:
72-
# print(instruction)
73-
instruction = splitter(instruction.strip())
74-
delay = 0
75-
if instruction[-1].endswith("]"): # Delay
76-
delay = int(instruction[-1].strip("[]"))
77-
if delay > max_delay:
78-
raise RuntimeError("Delay too long:", delay)
79-
instruction.pop()
80-
if len(instruction) > 1 and instruction[-2] == "side":
81-
if sideset_count == 0:
82-
raise RuntimeError("No side_set count set")
83-
sideset_value = int(instruction[-1])
84-
if sideset_value > 2 ** sideset_count:
85-
raise RuntimeError("Sideset value too large")
86-
delay |= sideset_value << (5 - sideset_count - sideset_enable)
87-
delay |= sideset_enable << 4
88-
instruction.pop()
89-
instruction.pop()
90-
91-
if instruction[0] == "nop":
92-
# mov delay y op y
93-
assembled.append(0b101_00000_010_00_010)
94-
elif instruction[0] == "jmp":
95-
# instr delay cnd addr
96-
assembled.append(0b000_00000_000_00000)
97-
target = instruction[-1]
98-
if target[:1] in "0123456789":
99-
assembled[-1] |= int(target)
100-
elif instruction[-1] in labels:
101-
assembled[-1] |= labels[target]
102-
else:
103-
raise SyntaxError(f"Invalid jmp target {repr(target)}")
104-
105-
if len(instruction) > 2:
106-
assembled[-1] |= CONDITIONS.index(instruction[1]) << 5
107-
108-
elif instruction[0] == "wait":
109-
# instr delay p sr index
110-
assembled.append(0b001_00000_0_00_00000)
111-
polarity = int(instruction[1])
112-
if not 0 <= polarity <= 1:
113-
raise RuntimeError("Invalid polarity")
114-
assembled[-1] |= polarity << 7
115-
assembled[-1] |= WAIT_SOURCES.index(instruction[2]) << 5
116-
num = int(instruction[3])
117-
if not 0 <= num <= 31:
118-
raise RuntimeError("Wait num out of range")
119-
assembled[-1] |= num
120-
if instruction[-1] == "rel":
121-
assembled[-1] |= 0x10 # Set the high bit of the irq value
122-
elif instruction[0] == "in":
123-
# instr delay src count
124-
assembled.append(0b010_00000_000_00000)
125-
assembled[-1] |= IN_SOURCES.index(instruction[1]) << 5
126-
count = int(instruction[-1])
127-
if not 1 <= count <= 32:
128-
raise RuntimeError("Count out of range")
129-
assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top
130-
elif instruction[0] == "out":
131-
# instr delay dst count
132-
assembled.append(0b011_00000_000_00000)
133-
assembled[-1] |= OUT_DESTINATIONS.index(instruction[1]) << 5
134-
count = int(instruction[-1])
135-
if not 1 <= count <= 32:
136-
raise RuntimeError("Count out of range")
137-
assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top
138-
elif instruction[0] == "push" or instruction[0] == "pull":
139-
# instr delay d i b zero
140-
assembled.append(0b100_00000_0_0_0_00000)
141-
if instruction[0] == "pull":
142-
assembled[-1] |= 0x80
143-
if instruction[-1] == "block" or not instruction[-1].endswith("block"):
144-
assembled[-1] |= 0x20
145-
if len(instruction) > 1 and instruction[1] in ("ifempty", "iffull"):
146-
assembled[-1] |= 0x40
147-
elif instruction[0] == "mov":
148-
# instr delay dst op src
149-
assembled.append(0b101_00000_000_00_000)
150-
assembled[-1] |= MOV_DESTINATIONS.index(instruction[1]) << 5
151-
source = instruction[-1]
152-
source_split = mov_splitter(source)
153-
if len(source_split) == 1:
154-
assembled[-1] |= MOV_SOURCES.index(source)
155-
else:
156-
assembled[-1] |= MOV_SOURCES.index(source_split[1])
157-
if source[:1] == "!":
158-
assembled[-1] |= 0x08
159-
elif source[:1] == "~":
160-
assembled[-1] |= 0x08
161-
elif source[:2] == "::":
162-
assembled[-1] |= 0x10
163-
else:
164-
raise RuntimeError("Invalid mov operator:", source[:1])
165-
if len(instruction) > 3:
166-
assembled[-1] |= MOV_OPS.index(instruction[-2]) << 3
167-
elif instruction[0] == "irq":
168-
# instr delay z c w index
169-
assembled.append(0b110_00000_0_0_0_00000)
170-
if instruction[-1] == "rel":
171-
assembled[-1] |= 0x10 # Set the high bit of the irq value
33+
class Program: # pylint: disable=too-few-public-methods
34+
"""Encapsulates a program's instruction stream and configuration flags
35+
36+
Example::
37+
38+
program = adafruit_pioasm.Program(...)
39+
state_machine = rp2pio.StateMachine(program.assembled, ..., **program.pio_kwargs)
40+
41+
"""
42+
43+
def __init__(self, text_program: str) -> None:
44+
"""Converts pioasm text to encoded instruction bytes"""
45+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
46+
assembled = []
47+
program_name = None
48+
labels = {}
49+
instructions = []
50+
sideset_count = 0
51+
sideset_enable = 0
52+
for line in text_program.split("\n"):
53+
line = line.strip()
54+
if not line:
55+
continue
56+
if ";" in line:
57+
line = line.split(";")[0].strip()
58+
if line.startswith(".program"):
59+
if program_name:
60+
raise RuntimeError("Multiple programs not supported")
61+
program_name = line.split()[1]
62+
elif line.startswith(".wrap_target"):
63+
if len(instructions) > 0:
64+
raise RuntimeError("wrap_target not supported")
65+
elif line.startswith(".wrap"):
66+
pass
67+
elif line.startswith(".side_set"):
68+
sideset_count = int(line.split()[1])
69+
sideset_enable = 1 if "opt" in line else 0
70+
elif line.endswith(":"):
71+
label = line[:-1]
72+
if label in labels:
73+
raise SyntaxError(f"Duplicate label {repr(label)}")
74+
labels[label] = len(instructions)
75+
elif line:
76+
# Only add as an instruction if the line isn't empty
77+
instructions.append(line)
78+
79+
max_delay = 2 ** (5 - sideset_count - sideset_enable) - 1
80+
assembled = []
81+
for instruction in instructions:
82+
# print(instruction)
83+
instruction = splitter(instruction.strip())
84+
delay = 0
85+
if instruction[-1].endswith("]"): # Delay
86+
delay = int(instruction[-1].strip("[]"))
87+
if delay < 0:
88+
raise RuntimeError("Delay negative:", delay)
89+
if delay > max_delay:
90+
raise RuntimeError("Delay too long:", delay)
91+
instruction.pop()
92+
if len(instruction) > 1 and instruction[-2] == "side":
93+
if sideset_count == 0:
94+
raise RuntimeError("No side_set count set")
95+
sideset_value = int(instruction[-1])
96+
if sideset_value >= 2 ** sideset_count:
97+
raise RuntimeError("Sideset value too large")
98+
delay |= sideset_value << (5 - sideset_count - sideset_enable)
99+
delay |= sideset_enable << 4
172100
instruction.pop()
173-
num = int(instruction[-1])
174-
if not 0 <= num <= 7:
175-
raise RuntimeError("Interrupt index out of range")
176-
assembled[-1] |= num
177-
if len(instruction) == 3: # after rel has been removed
178-
if instruction[1] == "wait":
101+
instruction.pop()
102+
103+
if instruction[0] == "nop":
104+
# mov delay y op y
105+
assembled.append(0b101_00000_010_00_010)
106+
elif instruction[0] == "jmp":
107+
# instr delay cnd addr
108+
assembled.append(0b000_00000_000_00000)
109+
target = instruction[-1]
110+
if target[:1] in "0123456789":
111+
assembled[-1] |= int(target)
112+
elif instruction[-1] in labels:
113+
assembled[-1] |= labels[target]
114+
else:
115+
raise SyntaxError(f"Invalid jmp target {repr(target)}")
116+
117+
if len(instruction) > 2:
118+
assembled[-1] |= CONDITIONS.index(instruction[1]) << 5
119+
120+
elif instruction[0] == "wait":
121+
# instr delay p sr index
122+
assembled.append(0b001_00000_0_00_00000)
123+
polarity = int(instruction[1])
124+
if not 0 <= polarity <= 1:
125+
raise RuntimeError("Invalid polarity")
126+
assembled[-1] |= polarity << 7
127+
assembled[-1] |= WAIT_SOURCES.index(instruction[2]) << 5
128+
num = int(instruction[3])
129+
if not 0 <= num <= 31:
130+
raise RuntimeError("Wait num out of range")
131+
assembled[-1] |= num
132+
if instruction[-1] == "rel":
133+
assembled[-1] |= 0x10 # Set the high bit of the irq value
134+
elif instruction[0] == "in":
135+
# instr delay src count
136+
assembled.append(0b010_00000_000_00000)
137+
assembled[-1] |= IN_SOURCES.index(instruction[1]) << 5
138+
count = int(instruction[-1])
139+
if not 1 <= count <= 32:
140+
raise RuntimeError("Count out of range")
141+
assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top
142+
elif instruction[0] == "out":
143+
# instr delay dst count
144+
assembled.append(0b011_00000_000_00000)
145+
assembled[-1] |= OUT_DESTINATIONS.index(instruction[1]) << 5
146+
count = int(instruction[-1])
147+
if not 1 <= count <= 32:
148+
raise RuntimeError("Count out of range")
149+
assembled[-1] |= count & 0x1F # 32 is 00000 so we mask the top
150+
elif instruction[0] == "push" or instruction[0] == "pull":
151+
# instr delay d i b zero
152+
assembled.append(0b100_00000_0_0_0_00000)
153+
if instruction[0] == "pull":
154+
assembled[-1] |= 0x80
155+
if instruction[-1] == "block" or not instruction[-1].endswith("block"):
179156
assembled[-1] |= 0x20
180-
elif instruction[1] == "clear":
157+
if len(instruction) > 1 and instruction[1] in ("ifempty", "iffull"):
181158
assembled[-1] |= 0x40
182-
# All other values are the default of set without waiting
183-
elif instruction[0] == "set":
184-
# instr delay dst data
185-
assembled.append(0b111_00000_000_00000)
186-
assembled[-1] |= SET_DESTINATIONS.index(instruction[1]) << 5
187-
value = int(instruction[-1])
188-
if not 0 <= value <= 31:
189-
raise RuntimeError("Set value out of range")
190-
assembled[-1] |= value
191-
else:
192-
raise RuntimeError("Unknown instruction:" + instruction[0])
193-
assembled[-1] |= delay << 8
194-
# print(bin(assembled[-1]))
195-
196-
return array.array("H", assembled)
159+
elif instruction[0] == "mov":
160+
# instr delay dst op src
161+
assembled.append(0b101_00000_000_00_000)
162+
assembled[-1] |= MOV_DESTINATIONS.index(instruction[1]) << 5
163+
source = instruction[-1]
164+
source_split = mov_splitter(source)
165+
if len(source_split) == 1:
166+
assembled[-1] |= MOV_SOURCES.index(source)
167+
else:
168+
assembled[-1] |= MOV_SOURCES.index(source_split[1])
169+
if source[:1] == "!":
170+
assembled[-1] |= 0x08
171+
elif source[:1] == "~":
172+
assembled[-1] |= 0x08
173+
elif source[:2] == "::":
174+
assembled[-1] |= 0x10
175+
else:
176+
raise RuntimeError("Invalid mov operator:", source[:1])
177+
if len(instruction) > 3:
178+
assembled[-1] |= MOV_OPS.index(instruction[-2]) << 3
179+
elif instruction[0] == "irq":
180+
# instr delay z c w index
181+
assembled.append(0b110_00000_0_0_0_00000)
182+
if instruction[-1] == "rel":
183+
assembled[-1] |= 0x10 # Set the high bit of the irq value
184+
instruction.pop()
185+
num = int(instruction[-1])
186+
if not 0 <= num <= 7:
187+
raise RuntimeError("Interrupt index out of range")
188+
assembled[-1] |= num
189+
if len(instruction) == 3: # after rel has been removed
190+
if instruction[1] == "wait":
191+
assembled[-1] |= 0x20
192+
elif instruction[1] == "clear":
193+
assembled[-1] |= 0x40
194+
# All other values are the default of set without waiting
195+
elif instruction[0] == "set":
196+
# instr delay dst data
197+
assembled.append(0b111_00000_000_00000)
198+
assembled[-1] |= SET_DESTINATIONS.index(instruction[1]) << 5
199+
value = int(instruction[-1])
200+
if not 0 <= value <= 31:
201+
raise RuntimeError("Set value out of range")
202+
assembled[-1] |= value
203+
else:
204+
raise RuntimeError("Unknown instruction:" + instruction[0])
205+
assembled[-1] |= delay << 8
206+
# print(bin(assembled[-1]))
207+
208+
self.pio_kwargs = {
209+
"sideset_count": sideset_count,
210+
"sideset_enable": sideset_enable,
211+
}
212+
213+
self.assembled = array.array("H", assembled)
214+
215+
216+
def assemble(program_text: str) -> array.array:
217+
"""Converts pioasm text to encoded instruction bytes
218+
219+
In new code, prefer to use the `Program` class so that the extra arguments
220+
such as the details about side-set pins can be easily passsed to the
221+
``StateMachine`` constructor."""
222+
return Program(program_text).assembled

0 commit comments

Comments
 (0)