@@ -735,6 +735,85 @@ def construct(ports):
735735 esi .ChannelService .to_host (AppID ("data" ), p2s .serial_out )
736736
737737
738+ # Multi-burst read: the list is longer than the serial encoder's data FIFO, so
739+ # `ListWindowToSerial` emits it as several header/data bursts (via its
740+ # split-on-full path) terminated by a single count==0 footer, rather than one
741+ # big burst. The host-side `SerialListTypeDeserializer` must stitch those
742+ # bursts back into a single list. The count field stays 16 bits (byte-aligned,
743+ # header fills the frame), so this exercises the multi-burst *reassembly*
744+ # without depending on the sub-byte frame layout the C++ facade codegen does
745+ # not yet model.
746+ _MULTIBURST_READ_TAG = 0xF00D
747+ _MULTIBURST_READ_ITEMS = [0x1000 + i for i in range (10 )]
748+ _MULTIBURST_READ_FIFO_DEPTH = 4
749+
750+
751+ class ChannelMultiBurstListRead (Module ):
752+ """To-host channel that emits a list split across multiple serial bursts.
753+
754+ Same shape as `ChannelWindowedListRead`, but the ten-element list exceeds
755+ the serial encoder's data FIFO (depth 4), so `ListWindowToSerial` emits the
756+ list as several header/data bursts (4 + 4 + 2) followed by a single count==0
757+ footer. Verifies that the host-side `SerialListTypeDeserializer` reassembles
758+ a multi-burst transfer back into one list. A write to offset ``0x10`` of the
759+ ``trigger`` MMIO region arms one transfer.
760+ """
761+
762+ clk = Clock ()
763+ rst = Reset ()
764+
765+ @generator
766+ def construct (ports ):
767+ clk = ports .clk
768+ rst = ports .rst
769+
770+ # MMIO trigger: any write to offset 0x10 arms one transfer.
771+ trigger_bundle = esi .MMIO .read_write (appid = AppID ("trigger" ))
772+ resp_chan = Wire (Channel (Bits (64 )))
773+ cmd_chan = trigger_bundle .unpack (data = resp_chan )["cmd" ]
774+ cmd_xact , cmd = cmd_chan .snoop_xact ()
775+ resp_chan .assign (cmd_chan .transform (lambda c : Bits (64 )(c .data )))
776+ trigger_xact = cmd_xact & (cmd .offset == UInt (32 )(0x10 ))
777+
778+ n_items = len (_MULTIBURST_READ_ITEMS )
779+ burst_end = Wire (Bits (1 ))
780+
781+ # ``armed`` is high for the duration of one list: set on a trigger write,
782+ # cleared on the cycle the final item handshakes. The producer streams all
783+ # ten items with a single ``last`` on item 9 -- the split into bursts
784+ # happens entirely inside `ListWindowToSerial`.
785+ armed = ControlReg (clk , rst , asserts = [trigger_xact ], resets = [burst_end ])
786+
787+ par_ready = Wire (Bits (1 ))
788+ handshake = armed & par_ready
789+ idx_counter = Counter (4 )(clk = clk ,
790+ rst = rst ,
791+ clear = burst_end ,
792+ increment = handshake ,
793+ instance_name = "multiburst_read_idx" )
794+ idx = idx_counter .out
795+ last_bit = (idx == UInt (4 )(n_items - 1 ))
796+ burst_end .assign (handshake & last_bit )
797+ item_value = Array (Bits (32 ), n_items )(_MULTIBURST_READ_ITEMS )[idx .as_bits ()]
798+
799+ parallel_window_type = Window .default_of (_window_probe_struct )
800+ par_struct = parallel_window_type .lowered_type ({
801+ "tag" : Bits (16 )(_MULTIBURST_READ_TAG ),
802+ "items" : item_value ,
803+ "last" : last_bit ,
804+ })
805+ par_window = parallel_window_type .wrap (par_struct )
806+ parallel_chan , parallel_ready = Channel (parallel_window_type ).wrap (
807+ par_window , armed )
808+ par_ready .assign (parallel_ready )
809+
810+ p2s = ListWindowToSerial (parallel_window_type , _WINDOW_PROBE_BULK_WIDTH ,
811+ _WINDOW_PROBE_ITEMS_PER_FRAME ,
812+ _MULTIBURST_READ_FIFO_DEPTH )(
813+ clk = clk , rst = rst , parallel_in = parallel_chan )
814+ esi .ChannelService .to_host (AppID ("data" ), p2s .serial_out )
815+
816+
738817class ChannelWindowedListWrite (Module ):
739818 """From-host channel of ``window<{tag, list<si32>}>``.
740819
@@ -812,6 +891,99 @@ def construct(ports):
812891 lambda _ : final_match .as_bits (1 ).pad_or_truncate (64 ).as_bits (64 )))
813892
814893
894+ # Multi-burst bare-list window. The bulk count is 8 bits, so each burst can
895+ # carry at most 255 items; a 256-element list must therefore be split by the
896+ # host serializer into two bursts (255 + 1). Hardware reassembles the bursts
897+ # via `ListWindowToParallel` and validates every item in order, proving the
898+ # write-side burst chunking round-trips through hardware.
899+ #
900+ # The window is a *bare* list of byte-sized items (no static header fields)
901+ # with an 8-bit count, so both the header frame (a single ui8 count) and each
902+ # data frame (one ui8) fill exactly one byte with no frame padding. That keeps
903+ # the on-wire layout unambiguous: a narrower (sub-byte) count would introduce
904+ # sub-byte frame padding, which the C++ facade codegen does not currently
905+ # model.
906+ _MULTIBURST_N_ITEMS = 256
907+ _MULTIBURST_BULK_WIDTH = 8
908+ _MULTIBURST_ITEMS_PER_FRAME = 1
909+ _multiburst_struct = StructType ([("items" , List (Bits (8 )))])
910+ _multiburst_window = Window .serial_of (_multiburst_struct ,
911+ _MULTIBURST_BULK_WIDTH ,
912+ _MULTIBURST_ITEMS_PER_FRAME )
913+
914+
915+ class ChannelMultiBurstListWrite (Module ):
916+ """From-host channel of ``window<{list<ui8>}>`` with an 8-bit bulk count.
917+
918+ The 8-bit bulk count caps each burst at 255 items, so the host splits the
919+ 256-element list into two bursts (255 + 1). Hardware reassembles the bursts
920+ via `ListWindowToParallel`, AND-reduces per-beat equality against the
921+ expected ``item[i] == i`` pattern, and latches the result into the ``match``
922+ MMIO region. This is the end-to-end check that the host-side multi-burst
923+ serialization is correct.
924+ """
925+
926+ clk = Clock ()
927+ rst = Reset ()
928+
929+ @generator
930+ def construct (ports ):
931+ clk = ports .clk
932+ rst = ports .rst
933+
934+ chan = esi .ChannelService .from_host (AppID ("data" ), _multiburst_window )
935+ s2p = ListWindowToParallel (_multiburst_window )(clk = clk ,
936+ rst = rst ,
937+ serial_in = chan )
938+ par_ready = Wire (Bits (1 ))
939+ par_window , par_valid = s2p .parallel_out .unwrap (par_ready )
940+ par_struct = par_window .unwrap ()
941+ par_ready .assign (Bits (1 )(1 ))
942+
943+ handshake = par_valid
944+ last_bit = par_struct ["last" ].as_bits (1 )
945+
946+ # Counter cycles 0..255, clears on the burst-end beat. Each item's value
947+ # equals its position, so the expected value is simply the index.
948+ idx_clr = (handshake & last_bit ).as_bits (1 )
949+ idx_counter = Counter (8 )(clk = clk ,
950+ rst = rst ,
951+ clear = idx_clr ,
952+ increment = handshake ,
953+ instance_name = "multiburst_write_idx" )
954+ idx = idx_counter .out
955+
956+ beat_ok = (par_struct ["items" ].as_bits (8 ) == idx .as_bits (8 )).as_bits (1 )
957+
958+ # Running AND-reduce across the whole (reassembled) list; latches into
959+ # ``final_match`` on the last item's beat.
960+ running_match = Wire (Bits (1 ))
961+ running_match_reg = Reg (Bits (1 ),
962+ clk = clk ,
963+ rst = rst ,
964+ rst_value = 1 ,
965+ ce = handshake ,
966+ name = "multiburst_match_running" )
967+ running_match .assign ((running_match_reg & beat_ok ).as_bits (1 ))
968+ running_match_reg .assign (running_match )
969+
970+ final_match = Reg (Bits (1 ),
971+ clk = clk ,
972+ rst = rst ,
973+ rst_value = 0 ,
974+ ce = idx_clr ,
975+ name = "multiburst_match_final" )
976+ final_match .assign (running_match )
977+
978+ # Expose the latched flag via MMIO read.
979+ mmio_bundle = esi .MMIO .read (appid = AppID ("match" ))
980+ resp_chan = Wire (Channel (Bits (64 )))
981+ addr_chan = mmio_bundle .unpack (data = resp_chan )["offset" ]
982+ resp_chan .assign (
983+ addr_chan .transform (
984+ lambda _ : final_match .as_bits (1 ).pad_or_truncate (64 ).as_bits (64 )))
985+
986+
815987class Top (Module ):
816988 clk = Clock ()
817989 rst = Reset ()
@@ -870,9 +1042,16 @@ def construct(ports):
8701042 ChannelWindowedListRead (clk = ports .clk ,
8711043 rst = ports .rst ,
8721044 appid = AppID ("channel_windowed_list_read_inst" ))
1045+ ChannelMultiBurstListRead (clk = ports .clk ,
1046+ rst = ports .rst ,
1047+ appid = AppID ("channel_multiburst_list_read_inst" ))
8731048 ChannelWindowedListWrite (clk = ports .clk ,
8741049 rst = ports .rst ,
8751050 appid = AppID ("channel_windowed_list_write_inst" ))
1051+ ChannelMultiBurstListWrite (
1052+ clk = ports .clk ,
1053+ rst = ports .rst ,
1054+ appid = AppID ("channel_multiburst_list_write_inst" ))
8761055 CallbackWindowedList (clk = ports .clk ,
8771056 rst = ports .rst ,
8781057 appid = AppID ("callback_windowed_list_inst" ))
0 commit comments