diff --git a/software/docs/source/commands/structs.md b/software/docs/source/commands/structs.md index bedd7e3b..a738706c 100644 --- a/software/docs/source/commands/structs.md +++ b/software/docs/source/commands/structs.md @@ -10,6 +10,11 @@ Set of all available [low-level commands](./low_level_commands.md) ``` ### Output Modes +```{eval-rst} +.. autoenum:: obi.commands.structs.OutputEnable + :members: +``` + ```{eval-rst} .. autoenum:: obi.commands.structs.OutputMode :members: diff --git a/software/frametest.py b/software/frametest.py deleted file mode 100644 index 94991439..00000000 --- a/software/frametest.py +++ /dev/null @@ -1,74 +0,0 @@ -import asyncio - -from obi.transfer import TCPConnection, TCPStream, setup_logging, dump_hex -from obi.commands import * -from obi.macros import FrameBuffer, RasterScanCommand -from obi.macros.blank_external import RelaySetupCommand, RelayTeardownCommand - -import logging -setup_logging({"Command": logging.DEBUG, "Connection": logging.DEBUG, "Stream": logging.DEBUG}) - -import os -cwd = os.getcwd() - -import time - -from rich import print - - -async def atask(): - for n in range(5): - print(f"task: {n}") - await asyncio.sleep(1) - - -async def mock_transfer(): - asyncio.create_task(atask()) - for n in range(5): - yield n - await asyncio.sleep(1) - - -async def main(): - conn = TCPConnection("localhost", 2224) - dac_range = DACCodeRange.from_resolution(16384) - - #await conn.transfer(RelayExternalCtrlCommand(enable=True, beam_type=BeamType.Ion)) - #await conn.transfer(RelaySetupCommand(beam_type=BeamType.Ion)) - await conn.transfer(ExternalCtrlCommand(enable=True)) - await asyncio.sleep(1) - await conn.transfer(ExternalCtrlCommand(enable=False)) - await asyncio.sleep(1) - # await conn.transfer(BeamSelectCommand(beam_type=BeamType.Ion)) - # - - def get_cmd(): - return RasterScanCommand(x_range=dac_range, y_range=dac_range, dwell_time=1, cookie=123) - - # start = time.time() - # cmd = get_cmd() - - # async for chunk in conn.transfer_multiple(cmd, latency=65536): - # print(f"got chunk1: {dump_hex(chunk)}") - # # async for n in mock_transfer(): - # now = time.time() - # elapsed = now-start - # #print(f"{n=}, {elapsed=:04f}") - # if elapsed > 1: - # cmd.abort.set() - # print("done1") - - # #print(asyncio.all_tasks(loop=None)) - - # time.sleep(3) - # cmd = get_cmd() - # async for chunk in conn.transfer_multiple(cmd, latency=65536): - # print(f"got chunk: {dump_hex(chunk)}") - - #await conn.transfer(RelayTeardownCommand()) - - # while True: - # pass - - -asyncio.run(main()) diff --git a/software/obi/applet/open_beam_interface/__init__.py b/software/obi/applet/open_beam_interface/__init__.py index ba2f874a..671d5402 100644 --- a/software/obi/applet/open_beam_interface/__init__.py +++ b/software/obi/applet/open_beam_interface/__init__.py @@ -20,6 +20,8 @@ PipelinedLoopbackAdapter, BusController, FastBusController, Supersampler, RasterScanner, CommandParser) +from glasgow.simulation.assembly import SimulationPipe + # Overview of (linear) processing pipeline: # 1. PC software (in: user input, out: bytes) # 2. Glasgow software/framework (in: bytes, out: same bytes; vendor-provided) @@ -52,7 +54,7 @@ class CommandExecutor(wiring.Component): blank_enable: Out(1, init=1) #Input to Serializer - output_mode: Out(2) + output_mode: Out(OutputMode) def __init__(self, *, out_only:bool=False, adc_latency=8, ext_delay_cyc=960000, @@ -90,7 +92,7 @@ def elaborate(self, platform): vector_stream = stream.Signature(DACStream).create() raster_mode = Signal() - output_mode = Signal(2) + output_mode = Signal(OutputMode) command = Signal.like(self.cmd_stream.payload) with m.If(raster_mode): wiring.connect(m, self.raster_scanner.dac_stream, self.supersampler.dac_stream) @@ -223,10 +225,12 @@ def elaborate(self, platform): m.d.comb += [ self.raster_scanner.dwell_stream.valid.eq(1), self.raster_scanner.dwell_stream.payload.dwell_time.eq(command.payload.raster_pixel.dwell_time), + self.raster_scanner.dwell_stream.payload.output_en.eq(command.payload.raster_pixel.output_en), self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank) ] with m.If(self.raster_scanner.dwell_stream.ready): - m.d.comb += submit_pixel.eq(1) + with m.If(command.payload.raster_pixel.output_en==OutputEnable.Enabled): + m.d.comb += submit_pixel.eq(1) m.next = "Fetch" with m.Case(CmdType.RasterPixelRun): @@ -234,10 +238,12 @@ def elaborate(self, platform): m.d.comb += [ self.raster_scanner.dwell_stream.valid.eq(1), self.raster_scanner.dwell_stream.payload.dwell_time.eq(command.payload.raster_pixel_run.dwell_time), + self.raster_scanner.dwell_stream.payload.output_en.eq(command.payload.raster_pixel_run.output_en), self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank) ] with m.If(self.raster_scanner.dwell_stream.ready): - m.d.comb += submit_pixel.eq(1) + with m.If(command.payload.raster_pixel_run.output_en==OutputEnable.Enabled): + m.d.comb += submit_pixel.eq(1) with m.If(run_length == command.payload.raster_pixel_run.length): m.d.sync += run_length.eq(0) m.next = "Fetch" @@ -254,7 +260,8 @@ def elaborate(self, platform): m.d.comb += [ self.raster_scanner.dwell_stream.valid.eq(1), self.raster_scanner.dwell_stream.payload.dwell_time.eq(command.payload.raster_pixel_fill.dwell_time), - self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank) + self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank), + self.raster_scanner.dwell_stream.payload.output_en.eq(OutputEnable.Enabled), ] @@ -263,7 +270,8 @@ def elaborate(self, platform): m.d.comb += [ self.raster_scanner.roi_stream.payload.eq(raster_region), self.raster_scanner.dwell_stream.payload.dwell_time.eq(command.payload.raster_pixel.dwell_time), - self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank) + self.raster_scanner.dwell_stream.payload.blank.eq(sync_blank), + self.raster_scanner.dwell_stream.payload.output_en.eq(OutputEnable.Enabled), ] with m.If(self.cmd_stream.valid): m.d.comb += self.raster_scanner.abort.eq(1) @@ -280,11 +288,15 @@ def elaborate(self, platform): with m.Case(CmdType.VectorPixel, CmdType.VectorPixelMinDwell): m.d.comb += vector_stream.valid.eq(1) - m.d.comb += vector_stream.payload.blank.eq(sync_blank) - m.d.comb += vector_stream.payload.delay.eq(inline_delay_counter) + m.d.comb += [ + vector_stream.payload.blank.eq(sync_blank), + vector_stream.payload.output_en.eq(command.payload.vector_pixel.output_en), + vector_stream.payload.delay.eq(inline_delay_counter) + ] with m.If(vector_stream.ready): m.d.sync += inline_delay_counter.eq(0) - m.d.comb += submit_pixel.eq(1) + with m.If(command.payload.vector_pixel.output_en==OutputEnable.Enabled): + m.d.comb += submit_pixel.eq(1) m.next = "Fetch" with m.FSM(): @@ -334,18 +346,15 @@ def elaborate(self, platform): with m.FSM(): with m.State("High"): - with m.If(self.output_mode == OutputMode.NoOutput): - m.d.comb += self.img_stream.ready.eq(1) #consume and destroy image stream - with m.Else(): - m.d.comb += self.usb_stream.payload.eq(self.img_stream.payload[8:16]) - m.d.comb += self.usb_stream.valid.eq(self.img_stream.valid) - m.d.comb += self.img_stream.ready.eq(self.usb_stream.ready) - with m.If(self.output_mode == OutputMode.SixteenBit): - m.d.sync += low.eq(self.img_stream.payload[0:8]) - with m.If(self.usb_stream.ready & self.img_stream.valid): - m.next = "Low" - with m.If(self.output_mode == OutputMode.EightBit): - m.next = "High" + m.d.comb += self.usb_stream.payload.eq(self.img_stream.payload[8:16]) + m.d.comb += self.usb_stream.valid.eq(self.img_stream.valid) + m.d.comb += self.img_stream.ready.eq(self.usb_stream.ready) + with m.If(self.output_mode == OutputMode.SixteenBit): + m.d.sync += low.eq(self.img_stream.payload[0:8]) + with m.If(self.usb_stream.ready & self.img_stream.valid): + m.next = "Low" + with m.If(self.output_mode == OutputMode.EightBit): + m.next = "High" with m.State("Low"): m.d.comb += self.usb_stream.payload.eq(low) @@ -569,6 +578,7 @@ async def flush(self): await self.pipe.flush() async def readuntil(self, separator=b'\n', *, flush=True, max_count=False): + self._logger.debug("reading until %s", separator) def find_sep(buffer, separator=b'\n', offset=0): if buffer._chunk is None: if not buffer._queue: @@ -596,6 +606,7 @@ def find_sep(buffer, separator=b'\n', offset=0): # Check if we now have enough data in the buffer for `separator` to fit. if buflen >= seplen: + self._logger.debug("Looking for the separator in the buffer") isep = find_sep(self.pipe._in_buffer, separator) if isep != -1: print(f"found {isep=}") @@ -603,9 +614,9 @@ def find_sep(buffer, separator=b'\n', offset=0): # to retrieve the data. break else: - while len(self.pipe_in_buffer) < seplen: - print(f"{len(self.pipe._in_tasks)=}") - self._logger.debug("FIFO: need %d bytes", seplen - len(self.lower._in_buffer)) + self._logger.debug("Need more data before we can look for the separator") + while len(self.pipe._in_buffer) < seplen: + self._logger.debug("FIFO: need %d bytes", seplen - len(self.pipe._in_buffer)) await self.pipe._in_tasks.wait_one() async with self.pipe._in_pushback: @@ -640,10 +651,9 @@ async def benchmark(self): sync_cmd = SynchronizeCommand(cookie=123, output=OutputMode.EightBit, raster=True) flush_cmd = FlushCommand() await self.pipe.send(bytes(sync_cmd)) - await self.pipe.send(bytes(flush_cmd)) await self.pipe.flush() - await self.pipe.recv(4) - print(f"got cookie!") + data = await self.pipe.recv(4) + print(f"got cookie!: {data.tobytes()[2:]}") commands = bytearray() print("generating block of commands...") for _ in range(131072*16): diff --git a/software/obi/applet/open_beam_interface/modules/bus_controller.py b/software/obi/applet/open_beam_interface/modules/bus_controller.py index cbbc13cc..e207d1ba 100644 --- a/software/obi/applet/open_beam_interface/modules/bus_controller.py +++ b/software/obi/applet/open_beam_interface/modules/bus_controller.py @@ -3,7 +3,7 @@ from amaranth.lib.wiring import In, Out, flipped from amaranth.lib.fifo import SyncFIFOBuffered -from obi.applet.open_beam_interface.modules.structs import SuperDACStream, BusSignature, BlankRequest, Transforms +from obi.applet.open_beam_interface.modules.structs import SuperDACStream, BusSignature, BlankRequest, Transforms, OutputEnable class SkidBuffer(wiring.Component): def __init__(self, data_layout, *, depth): @@ -139,7 +139,8 @@ def elaborate(self, platform): # Transmit blanking state from input stream m.d.comb += self.inline_blank.eq(self.dac_stream.payload.blank) # Schedule ADC sample for these DAC codes to be output. - m.d.sync += accept_sample.eq(Cat(1, accept_sample)) + with m.If(self.dac_stream.payload.output_en==OutputEnable.Enabled): + m.d.sync += accept_sample.eq(Cat(1, accept_sample)) # Carry over the flag for last sample [of averaging window] to the output. m.d.sync += last_sample.eq(Cat(self.dac_stream.payload.last, last_sample)) with m.Else(): diff --git a/software/obi/applet/open_beam_interface/modules/command_parser.py b/software/obi/applet/open_beam_interface/modules/command_parser.py index 969126ef..8e5e7bb6 100644 --- a/software/obi/applet/open_beam_interface/modules/command_parser.py +++ b/software/obi/applet/open_beam_interface/modules/command_parser.py @@ -15,8 +15,10 @@ def elaborate(self, platform): m.d.comb += self.cmd_stream.payload.eq(self.command) self.command_reg = Signal(Command) array_length = Signal(16) + is_array_command = Signal() self.is_started = Signal() + with m.FSM() as fsm: m.d.comb += self.is_started.eq(fsm.ongoing("Type")) def goto_first_deserialized_state(from_type=self.command.type): @@ -58,10 +60,12 @@ def Deserialize(target, state, next_state): with m.State("Submit"): m.d.comb += self.command.eq(self.command_reg) with m.If(self.command.type == CmdType.Array): - m.d.sync += self.command_reg.type.eq(self.command.payload.array.cmdtype) - m.d.sync += self.command_reg.as_value()[4:].eq(0) + # m.d.sync += self.command_reg.eq(self.command.payload.array.command) + m.d.sync += self.command_reg.type.eq(self.command.payload.array.command[4:8]) + m.d.sync += self.command_reg.payload.as_value()[0:4].eq(self.command.payload.array.command[0:4]) + m.d.sync += self.command_reg.payload.as_value()[8:].eq(self.command.payload.array.command[8:]) m.d.sync += array_length.eq(self.command.payload.array.array_length) - goto_first_deserialized_state(from_type=self.command.payload.array.cmdtype) + goto_first_deserialized_state(from_type=self.command.payload.array.command[4:8]) with m.Else(): with m.If(self.cmd_stream.ready): m.d.comb += self.cmd_stream.valid.eq(1) diff --git a/software/obi/applet/open_beam_interface/modules/raster_scanner.py b/software/obi/applet/open_beam_interface/modules/raster_scanner.py index 89b0da22..61f417cd 100644 --- a/software/obi/applet/open_beam_interface/modules/raster_scanner.py +++ b/software/obi/applet/open_beam_interface/modules/raster_scanner.py @@ -2,7 +2,7 @@ from amaranth.lib import data, stream, wiring from amaranth.lib.wiring import In, Out, flipped -from obi.applet.open_beam_interface.modules.structs import RasterRegion, DACStream, DwellTime, BlankRequest +from obi.applet.open_beam_interface.modules.structs import RasterRegion, DACStream, DwellTime, BlankRequest, OutputEnable class RasterScanner(wiring.Component): """ @@ -23,6 +23,7 @@ class RasterScanner(wiring.Component): dwell_stream: In(stream.Signature(data.StructLayout({ "dwell_time": DwellTime, "blank": BlankRequest, + "output_en": OutputEnable }))) abort: In(1) @@ -43,7 +44,8 @@ def elaborate(self, platform): self.dac_stream.payload.dac_x_code.eq(x_accum >> self.FRAC_BITS), self.dac_stream.payload.dac_y_code.eq(y_accum >> self.FRAC_BITS), self.dac_stream.payload.dwell_time.eq(self.dwell_stream.payload.dwell_time), - self.dac_stream.payload.blank.eq(self.dwell_stream.payload.blank) + self.dac_stream.payload.blank.eq(self.dwell_stream.payload.blank), + self.dac_stream.payload.output_en.eq(self.dwell_stream.payload.output_en) ] with m.FSM(): diff --git a/software/obi/applet/open_beam_interface/modules/structs.py b/software/obi/applet/open_beam_interface/modules/structs.py index 6af870d6..416299bd 100644 --- a/software/obi/applet/open_beam_interface/modules/structs.py +++ b/software/obi/applet/open_beam_interface/modules/structs.py @@ -5,6 +5,8 @@ from dataclasses import dataclass +from obi.commands.structs import OutputEnable + class BlankRequest(data.Struct): enable: 1 request: 1 @@ -32,6 +34,7 @@ class DACStream(data.Struct): padding_x: 2 dwell_time: 16 blank: BlankRequest + output_en: OutputEnable delay: 3 @@ -41,7 +44,8 @@ class SuperDACStream(data.Struct): dac_y_code: 14 padding_y: 2 blank: BlankRequest - last: 1 + output_en: OutputEnable + last: 1 delay: 3 class RasterRegion(data.Struct): diff --git a/software/obi/applet/open_beam_interface/modules/supersampler.py b/software/obi/applet/open_beam_interface/modules/supersampler.py index 5475c4ff..ff3da92d 100644 --- a/software/obi/applet/open_beam_interface/modules/supersampler.py +++ b/software/obi/applet/open_beam_interface/modules/supersampler.py @@ -91,6 +91,7 @@ def elaborate(self, platform): self.super_dac_stream.payload.dac_x_code.eq(self.dac_stream_data.dac_x_code), self.super_dac_stream.payload.dac_y_code.eq(self.dac_stream_data.dac_y_code), self.super_dac_stream.payload.blank.eq(self.dac_stream_data.blank), + self.super_dac_stream.payload.output_en.eq(self.dac_stream_data.output_en), self.super_dac_stream.payload.delay.eq(self.dac_stream_data.delay), last.eq(dwell_counter == self.dac_stream_data.dwell_time) #self.super_dac_stream.payload.last.eq(dwell_counter == self.dac_stream_data.dwell_time), diff --git a/software/obi/commands/__init__.py b/software/obi/commands/__init__.py index 337d7d7c..b1d8132a 100644 --- a/software/obi/commands/__init__.py +++ b/software/obi/commands/__init__.py @@ -10,8 +10,8 @@ BIG_ENDIAN = (struct.pack('@H', 0x1234) == struct.pack('>H', 0x1234)) __all__ = [] -from .structs import CmdType, OutputMode, BeamType, u14, u16, fp8_8, DwellTime, DACCodeRange -__all__ += ["CmdType", "OutputMode", "BeamType", "u14", "u16", "fp8_8", "DwellTime", "DACCodeRange"] +from .structs import CmdType, OutputMode, OutputEnable, BeamType, u14, u16, fp8_8, DwellTime, DACCodeRange +__all__ += ["CmdType", "OutputMode", "OutputEnable", "BeamType", "u14", "u16", "fp8_8", "DwellTime", "DACCodeRange"] class BaseCommand(metaclass = ABCMeta): def __init_subclass__(cls): @@ -45,21 +45,18 @@ async def transfer(self, stream): ... async def recv_res(self, pixel_count, stream, output_mode:OutputMode): - self._logger.debug(f"waiting to receive {pixel_count} pixels, {output_mode=}") - if output_mode == OutputMode.NoOutput: - await asyncio.sleep(0) - pass - else: - if output_mode == OutputMode.SixteenBit: - res = array.array('H', bytes(await stream.read(pixel_count * 2))) - if not BIG_ENDIAN: - res.byteswap() - await asyncio.sleep(0) - return res - if output_mode == OutputMode.EightBit: - res = array.array('B', await stream.read(pixel_count)) - await asyncio.sleep(0) - return res + if output_mode == OutputMode.SixteenBit: + self._logger.debug(f"waiting to receive {pixel_count} pixels, ({pixel_count*2} bytes)") + res = array.array('H', bytes(await stream.read(pixel_count * 2))) + if not BIG_ENDIAN: + res.byteswap() + await asyncio.sleep(0) + return res + if output_mode == OutputMode.EightBit: + self._logger.debug(f"waiting to receive {pixel_count} pixels, ({pixel_count} bytes)") + res = array.array('B', await stream.read(pixel_count)) + await asyncio.sleep(0) + return res __all__ += ["BaseCommand"] from .low_level_commands import (SynchronizeCommand, AbortCommand, FlushCommand, ExternalCtrlCommand, diff --git a/software/obi/commands/low_level_commands.py b/software/obi/commands/low_level_commands.py index 806a1b6a..b94c10ad 100644 --- a/software/obi/commands/low_level_commands.py +++ b/software/obi/commands/low_level_commands.py @@ -1,4 +1,4 @@ -from .structs import BitLayout, ByteLayout, CmdType, OutputMode, BeamType, u14, u16, DwellTime, DACCodeRange +from .structs import BitLayout, ByteLayout, CmdType, OutputMode, OutputEnable, BeamType, u14, u16, DwellTime, DACCodeRange from . import BaseCommand from amaranth import * @@ -32,6 +32,15 @@ def as_struct_layout(cls): :class: data.Struct """ return data.StructLayout({**cls.bitlayout.as_struct_layout(), **cls.bytelayout.as_struct_layout()}) + @classmethod + def header(cls, **kwargs): + """Generate just the header (first byte) of a command sequence + Returns: + int + """ + header_funcstr = cls.bitlayout.pack_fn(cls.cmdtype) + func = eval(f"lambda value_dict: {header_funcstr}") + return func(kwargs) def __init__(self, **kwargs): self.__dict__.update(kwargs) def __bytes__(self): @@ -162,15 +171,15 @@ class RasterPixelCommand(LowLevelCommand): One pixel dwell value. The position at which this pixel is executed depends on the current :class:`RasterRegionCommand`. ''' + bitlayout = BitLayout({"output_en": OutputEnable}) bytelayout = ByteLayout({"dwell_time" : 2}) - def __init__(self, *, dwell_time:DwellTime): - super().__init__(dwell_time=dwell_time) + def __init__(self, dwell_time:DwellTime, output_en: OutputEnable=OutputEnable.Enabled): + super().__init__(output_en=output_en, dwell_time=dwell_time) class ArrayCommand(LowLevelCommand): - bitlayout = BitLayout({"cmdtype": CmdType}) - bytelayout = ByteLayout({"array_length": 2}) - def __init__(self, cmdtype: CmdType, array_length: u16): - super().__init__(cmdtype=cmdtype, array_length=array_length) + bytelayout = ByteLayout({"command": 1, "array_length": 2}) + def __init__(self, command, array_length: u16): + super().__init__(command=command, array_length=array_length) class RasterPixelFillCommand(LowLevelCommand): bytelayout = ByteLayout({"dwell_time" : 2}) @@ -183,9 +192,10 @@ class RasterPixelRunCommand(LowLevelCommand): The position at which these pixels are executed depends on the current :class:`RasterRegionCommand`. ''' + bitlayout = BitLayout({"output_en": OutputEnable}) bytelayout = ByteLayout({"length": 2, "dwell_time" : 2}) - def __init__(self, length: u16, dwell_time: DwellTime): - super().__init__(length=length, dwell_time=dwell_time) + def __init__(self, length: u16, dwell_time: DwellTime, output_en: OutputEnable=OutputEnable.Enabled): + super().__init__(output_en=output_en, length=length, dwell_time=dwell_time) class RasterPixelFreeRunCommand(LowLevelCommand): ''' @@ -201,9 +211,10 @@ class VectorPixelCommand(LowLevelCommand): ''' Sets DAC output to the coordinate X, Y for the specified dwell time. ''' + bitlayout = BitLayout({"output_en": OutputEnable}) bytelayout = ByteLayout({"x_coord": 2, "y_coord": 2, "dwell_time": 2}) - def __init__(self, x_coord:u14, y_coord:u14, dwell_time:u16): - super().__init__(x_coord=x_coord, y_coord=y_coord, dwell_time=dwell_time) + def __init__(self, x_coord:u14, y_coord:u14, dwell_time:u16, output_en: OutputEnable=OutputEnable.Enabled): + super().__init__(output_en=output_en, x_coord=x_coord, y_coord=y_coord, dwell_time=dwell_time) def pack(self): if vars(self)["dwell_time"] <= 1: return VectorPixelMinDwellCommand(**vars(self)).pack() @@ -220,6 +231,7 @@ async def transfer(self, stream, output_mode=OutputMode.SixteenBit): return await self.recv_res(1, stream, output_mode) class VectorPixelMinDwellCommand(LowLevelCommand): + bitlayout = BitLayout({"output_en": OutputEnable}) bytelayout = ByteLayout({"dac_stream": {"x_coord": 2, "y_coord": 2}}) all_commands = [SynchronizeCommand, diff --git a/software/obi/commands/structs.py b/software/obi/commands/structs.py index e3dd2537..7313e2c0 100644 --- a/software/obi/commands/structs.py +++ b/software/obi/commands/structs.py @@ -128,10 +128,15 @@ def pack_fn(self, header_funcstr): ##### start commands -class OutputMode(enum.IntEnum, shape = 2): +class OutputMode(enum.IntEnum, shape = 1): SixteenBit = 0 EightBit = 1 - NoOutput = 2 + +class OutputEnable(enum.IntEnum, shape = 1): + Enabled = 0 #Because enabled is default + Disabled = 1 + + class BeamType(enum.IntEnum, shape = 2): NoBeam = 0 diff --git a/software/obi/macros/raster.py b/software/obi/macros/raster.py index d5d88aaa..031eaa76 100644 --- a/software/obi/macros/raster.py +++ b/software/obi/macros/raster.py @@ -35,11 +35,18 @@ def _iter_chunks(self, latency): def append_command(pixel_count): while pixel_count > 65536: - cmd = RasterPixelRunCommand(dwell_time = self._dwell, length=65535) + cmd = RasterPixelRunCommand(dwell_time = self._dwell, length=65535, output_en=OutputEnable.Enabled) commands.extend(bytes(cmd)) pixel_count -= 65536 - cmd = RasterPixelRunCommand(dwell_time = self._dwell, length=pixel_count-1) + cmd = RasterPixelRunCommand(dwell_time = self._dwell, length=pixel_count-1, output_en=OutputEnable.Enabled) commands.extend(bytes(cmd)) + + def fly_back(): + if self.frame_blank: + commands.extend(bytes(BlankCommand(enable=True, inline=False))) + commands.extend(bytes(VectorPixelCommand(output_en=False, x_coord=self._x_range.start, y_coord=self._y_range.start, dwell_time=1))) + if self.frame_blank: #unblank at the next provided pixel position + commands.extend(bytes(BlankCommand(enable=False, inline=True))) pixel_count = 0 total_dwell = 0 @@ -48,9 +55,8 @@ def append_command(pixel_count): total_dwell += self._dwell if total_dwell >= latency: append_command(pixel_count) - ## blank at the end of the last pixel - if self.frame_blank and n + 1 == self._x_range.count * self._y_range.count: - commands.extend(bytes(BlankCommand(enable=True, inline=False))) + if n + 1 == self._x_range.count * self._y_range.count: # end of frame + fly_back() yield(commands, pixel_count) commands = bytearray() pixel_count = 0 @@ -75,9 +81,12 @@ async def sender(): if tokens == 0: await FlushCommand().transfer(stream) await token_fut - if self.frame_blank and self.abort.is_set(): - ## go to a blanked state after an aborted frame - commands.extend(bytes(BlankCommand(enable=True, inline=False))) + if self.abort.is_set(): + if self.frame_blank: ## go to a blanked state after an aborted frame + commands.extend(bytes(BlankCommand(enable=True, inline=False))) + commands.extend(bytes(VectorPixelCommand(output_en=False, x_coord=self._x_range.start, y_coord=self._y_range.start, dwell_time=1))) + if self.frame_blank: #unblank at the next provided pixel position + commands.extend(bytes(BlankCommand(enable=False, inline=True))) await stream.write(commands) tokens -= 1 if self.abort.is_set(): @@ -85,11 +94,10 @@ async def sender(): await asyncio.sleep(0) await FlushCommand().transfer(stream) - await SynchronizeCommand(cookie=self._cookie, raster=True, output = self._output_mode).transfer(stream) await RasterRegionCommand(x_range=self._x_range, y_range=self._y_range).transfer(stream) asyncio.create_task(sender()) - cookie = await stream.read(4) #just assume these are exactly FFFF + cookie, and discard them + # cookie = await stream.read(4) #just assume these are exactly FFFF + cookie, and discard them ## TODO: assert against synchronization result for commands, pixel_count in self._iter_chunks(latency): tokens += 1 @@ -101,8 +109,6 @@ async def sender(): break self._logger.debug(f"recver: tokens={tokens}") yield await self.recv_res(pixel_count, stream, self._output_mode) - ## fly back - # await VectorPixelCommand(x_coord=self._x_range.start, y_coord=self._y_range.start, dwell_time=1).transfer(stream) diff --git a/software/obi/macros/vector.py b/software/obi/macros/vector.py index 747bd095..873add38 100644 --- a/software/obi/macros/vector.py +++ b/software/obi/macros/vector.py @@ -39,7 +39,7 @@ def _iter_chunks(self, latency): def get_command(pixel_count): nonlocal commands - cmd = ArrayCommand(cmdtype=CmdType.VectorPixel, array_length=pixel_count-1) + cmd = ArrayCommand(command=VectorPixelCommand.header(output_en=OutputEnable.Enabled), array_length=pixel_count-1) return bytes(cmd) pixel_count = 0 diff --git a/software/tests/gateware/test_open_beam_interface.py b/software/tests/gateware/test_open_beam_interface.py index 9fd9d21d..ddfcd992 100644 --- a/software/tests/gateware/test_open_beam_interface.py +++ b/software/tests/gateware/test_open_beam_interface.py @@ -148,7 +148,7 @@ def test_bus_controller_streams(self): def test_one_cycle(): dut = BusController(adc_half_period=3, adc_latency=6) async def put_testbench(ctx): - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 123, "dac_y_code": 456, "last": 1}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 123, "dac_y_code": 456, "output_en": OutputEnable.Enabled, "last": 1}) async def get_testbench(ctx): await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 1}, timeout_steps=100) @@ -158,15 +158,15 @@ async def get_testbench(ctx): def test_multi_cycle(): dut = BusController(adc_half_period=3, adc_latency=6) async def put_testbench(ctx): - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "last": 0}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) await ctx.tick().repeat(1) - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "last": 0}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) await ctx.tick().repeat(1) - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "last": 0}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) await ctx.tick().repeat(1) - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "last": 1}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 1}) await ctx.tick().repeat(1) - await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "last": 0}) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "output_en": OutputEnable.Enabled, "last": 0}) async def get_testbench(ctx): await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 0}, timeout_steps=100) @@ -176,8 +176,37 @@ async def get_testbench(ctx): self.simulate(dut, [put_testbench, get_testbench], name="bus_controller_4") + def test_disable_output(): + dut = BusController(adc_half_period=3, adc_latency=6) + async def put_testbench(ctx): + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 1, "dac_y_code": 1, "output_en": OutputEnable.Enabled, "last": 1}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "output_en": OutputEnable.Disabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "output_en": OutputEnable.Disabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "output_en": OutputEnable.Disabled, "last": 0}) + await ctx.tick().repeat(1) + await put_stream(ctx, dut.dac_stream, {"dac_x_code": 2, "dac_y_code": 2, "output_en": OutputEnable.Disabled, "last": 1}) + await ctx.tick().repeat(1) + + async def get_testbench(ctx): + await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 0}, timeout_steps=100) + await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 0}, timeout_steps=100) + await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 0}, timeout_steps=100) + await get_stream(ctx, dut.adc_stream, {"adc_code": 0, "last": 1}, timeout_steps=100) + assert ctx.get(dut.adc_stream.valid) == 0 + + self.simulate(dut, [put_testbench, get_testbench], name="bus_controller_disable_output") test_one_cycle() test_multi_cycle() + test_disable_output() def test_bus_controller_transforms(self): def test_xflip(xin: int, xout: int): @@ -188,7 +217,7 @@ async def put_testbench(ctx): assert trans_x == xout, f"flipped x {trans_x} != expected {xout}" assert trans_y == 0, f"non-flipped y {trans_y} != expected 0" - self.simulate(dut, [put_testbench], name="xflip_{xin}_{xout}") + self.simulate(dut, [put_testbench], name=f"xflip_{xin}_{xout}") def test_yflip(yin: int, yout: int): dut = BusController(adc_half_period=3, adc_latency=6, transforms = Transforms(xflip=False, yflip=True, rotate90=False)) @@ -198,7 +227,7 @@ async def put_testbench(ctx): assert trans_x == 0, f"non-flipped x {trans_x} != expected 0" assert trans_y == yout, f"flipped y {trans_y} != expected {yout}" - self.simulate(dut, [put_testbench], name="xflip_{xin}_{xout}") + self.simulate(dut, [put_testbench], name=f"yflip_{yin}_{yout}") def test_rotate90(): dut = BusController(adc_half_period=3, adc_latency=6, transforms = Transforms(xflip=False, yflip=False, rotate90=True)) @@ -208,7 +237,7 @@ async def put_testbench(ctx): assert trans_x == 456, f"rotated x {trans_x} != expected 456" assert trans_y == 123, f"rotated y {trans_y} != expected 123" - self.simulate(dut, [put_testbench], name="xflip_{xin}_{xout}") + self.simulate(dut, [put_testbench], name=f"rotate90") test_xflip(0, 16383) test_xflip(16383, 0) @@ -378,7 +407,7 @@ async def get_testbench(ctx): self.assertEqual(ctx.get(dut.cmd_stream.valid),0) self.simulate(dut, [get_testbench,put_testbench], name="parse_" + name) - test_cmd(SynchronizeCommand(cookie=1234, raster=True, output=OutputMode.NoOutput),"cmd_sync") + test_cmd(SynchronizeCommand(cookie=1234, raster=True, output=OutputMode.EightBit),"cmd_sync") test_cmd(AbortCommand(), "cmd_abort") @@ -397,18 +426,16 @@ async def get_testbench(ctx): test_cmd(RasterRegionCommand(x_range=x_range, y_range=y_range), "cmd_rasterregion") - test_cmd(RasterPixelRunCommand(length=5, dwell_time= 6),"cmd_rasterpixelrun") - + test_cmd(RasterPixelRunCommand(length=5, dwell_time=6),"cmd_rasterpixelrun") test_cmd(RasterPixelFreeRunCommand(dwell_time = 10), "cmd_rasterpixelfreerun") - test_cmd(VectorPixelCommand(x_coord=4, y_coord=5, dwell_time= 6),"cmd_vectorpixel") test_cmd(VectorPixelCommand(x_coord=4, y_coord=5, dwell_time= 1),"cmd_vectorpixelmin") def test_raster_pixels_cmd(): - command = ArrayCommand(cmdtype = CmdType.RasterPixel, array_length = 5) + command = ArrayCommand(command = RasterPixelCommand.header(output_en=1), array_length = 5) dwells = [1,2,3,4,5,6] async def put_testbench(ctx): for byte in bytes(command): @@ -418,7 +445,7 @@ async def put_testbench(ctx): await put_stream(ctx, dut.usb_stream, byte) async def get_testbench(ctx): for dwell in dwells: - await get_stream(ctx, dut.cmd_stream, RasterPixelCommand(dwell_time=dwell).as_dict(), timeout_steps=len(command)*2 + len(dwells)*2 + 2) + await get_stream(ctx, dut.cmd_stream, RasterPixelCommand(dwell_time=dwell,output_en=OutputEnable.Enabled).as_dict(), timeout_steps=len(command)*2 + len(dwells)*2 + 2) self.assertEqual(ctx.get(dut.cmd_stream.valid),0) self.assertEqual(ctx.get(dut.is_started),1) self.simulate(dut, [get_testbench,put_testbench], name="parse_cmd_rasterpixel") @@ -434,7 +461,7 @@ def test_sync_exec(): async def put_testbench(ctx): await put_stream(ctx, dut.cmd_stream, - SynchronizeCommand(raster=True, output=OutputMode.NoOutput, cookie=cookie).as_dict()) + SynchronizeCommand(raster=True, output=OutputMode.EightBit, cookie=cookie).as_dict()) async def get_testbench(ctx): await get_stream(ctx, dut.img_stream, 65535) # FFFF @@ -466,7 +493,7 @@ def test_rasterpixel_exec(): async def put_testbench(ctx): await put_stream(ctx, dut.cmd_stream, - RasterPixelCommand(dwell_time=5).as_dict()) + RasterPixelCommand(dwell_time=5, output_en=OutputEnable.Enabled).as_dict()) async def get_testbench(ctx): data = await ctx.tick().sample(dut.raster_scanner.dwell_stream.payload).until(dut.raster_scanner.dwell_stream.valid == 1) payload = { @@ -474,7 +501,9 @@ async def get_testbench(ctx): "blank": { "enable": 0, "request": 0 - }} + }, + "output_en": OutputEnable.Enabled + } wrapped_payload = dut.raster_scanner.dwell_stream.payload.shape().const(payload) assert data[0] == wrapped_payload, f"{prettier_diff(data[0], payload)}" @@ -511,7 +540,8 @@ async def get_stream(ctx, stream, payload): "blank": { "enable": 0, "request": 0 - }}) + }, + "output_en": OutputEnable.Enabled}) self.simulate(dut, [get_testbench,put_testbench], name = "exec_rasterpixelrun") @@ -545,7 +575,8 @@ async def get_stream(ctx, stream, payload): "blank": { "enable": 0, "request": 0 - }}) + }, + "output_en": OutputEnable.Enabled}) self.simulate(dut, [get_testbench,put_testbench], name = "exec_rasterpixelfill") @@ -671,6 +702,10 @@ class TestSyncCommand(TestCommand, command=SynchronizeCommand): @property def response(self): return [65535, self.command.cookie] # FFFF, cookie + @property + def exec_cycles(self): + # Wait as long as it takes to synchronize + return 10000 class TestFlushCommand(TestCommand, command=FlushCommand): pass @@ -728,7 +763,10 @@ async def put_testbench(self, ctx, dut): class TestRasterPixelRunCommand(TestCommand, command=RasterPixelRunCommand): @property def response(self): - return [0]*(self.command.length+1) + if self.command.output_en == OutputEnable.Enabled: + return [0]*(self.command.length+1) + else: + return [] @property def exec_cycles(self): return self.command.dwell_time*self.command.length*BUS_CYCLES @@ -760,19 +798,19 @@ async def put_testbench(self, ctx, dut): break await ctx.tick().until(dut.supersampler.dac_stream.ready == 1) n += 1 - logger.debug(f"{n}/{self.test_samples} valid samples") - + logger.debug(f"{n}/{self.test_samples} valid samples") class TestVectorPixelCommand(TestCommand, command=VectorPixelCommand): @property def response(self): - return [0] + if self.command.output_en == OutputEnable.Enabled: + return [0] + else: + return [] @property def exec_cycles(self): return self.command.dwell_time*BUS_CYCLES - - def test_exec_1(): test_seq = TestCommandSequence() test_seq.add(TestSyncCommand(cookie=502, raster=True, output=OutputMode.SixteenBit)) @@ -832,12 +870,12 @@ def test_exec_4(): def test_exec_5(): test_seq = TestCommandSequence() - test_seq.add(TestSyncCommand(cookie=502, raster=False, output=OutputMode.NoOutput)) + test_seq.add(TestSyncCommand(cookie=502, raster=False, output=OutputMode.SixteenBit)) test_seq.add(TestFlushCommand()) for n in range(100): - test_seq.add(TestVectorPixelCommand(x_coord=1, y_coord=1, dwell_time=1)) - test_seq.add(TestVectorPixelCommand(x_coord=16384, y_coord=16384, dwell_time=1)) - test_seq.add(TestSyncCommand(cookie=502, raster=False, output=OutputMode.NoOutput)) + test_seq.add(TestVectorPixelCommand(x_coord=1, y_coord=1, dwell_time=1, output_en=OutputEnable.Disabled)) + test_seq.add(TestVectorPixelCommand(x_coord=16384, y_coord=16384, dwell_time=1, output_en=OutputEnable.Disabled)) + test_seq.add(TestSyncCommand(cookie=503, raster=False, output=OutputMode.SixteenBit)) self.simulate(test_seq.dut, [test_seq.put_testbench, test_seq.get_testbench], name="exec_5") @@ -848,7 +886,6 @@ def test_exec_6(): test_seq.add(TestVectorPixelCommand(x_coord=1, y_coord=1, dwell_time=1)) self.simulate(test_seq.dut, [test_seq.put_testbench, test_seq.get_testbench], name="exec_6") - test_exec_1() test_exec_2() test_exec_3() @@ -856,9 +893,7 @@ def test_exec_6(): test_exec_5() test_exec_6() - # @unittest.skip("Skipped applet test case") def test_all(self): - #TODO: Fix this simulation from amaranth import Module from obi.applet.open_beam_interface import OBIApplet from glasgow.applet import GlasgowAppletV2TestCase, synthesis_test, applet_v2_simulation_test @@ -937,6 +972,7 @@ async def test_vector_blank(self, applet, ctx): await applet.obi_iface.write(bytes(SynchronizeCommand(cookie=4, output=2, raster=0))) await applet.obi_iface.read(6) + test_case = OBIApplet_TestCase() test_case.test_build() test_case.test_loopback_vector()