Citiroc Frame Transfer
Event collector and PC readout endpoint for the four CITIROC 1A ASICs of the DT5550W-Citiroc1A board. It polls the four Citiroc Analog blocks round-robin, packs each event into a fixed 38-word 32-bit packet with a sync header, timestamps, an event counter and a trailer, and pushes it into a dual-clock FIFO that the host drains through a memory-mapped endpoint. It also generates the C and Python decoder for that packet format.
Introduction
Principle of Operation
Each Citiroc Analog block produces, once per event, a 1024-bit packed frame plus a
32-bit run timestamp and a 64-bit global timestamp, and raises a level DV that it
holds until it is acknowledged. This block is the other half of that handshake for all
four ASICs at once.
A single state machine cycles A → B → C → D → A …, one ASIC per clock cycle. When it
finds a DV_x asserted for an unmasked ASIC it:
- latches the whole event — header, timestamps, counter, 32 channel words, trailer — into a 1216-bit register in one clock cycle;
- pulses
ACK_xso the analog block can release itsDVand start accepting the next trigger; - shifts the 38 words out of that register into the FIFO, one word per clock.
The FIFO is an xpm_fifo_async with independent write and read clocks, sized by the
Samples property. The host reads it through the standard FIFO endpoint, and the
generated CitirocFRAME_<name>_RECONSTRUCT_DATA function turns the raw 32-bit word
stream back into per-channel charges.
The packet format is the real contract between firmware and software, so it is documented in full below — it is what you need if you write your own decoder.
Pin Description
CONTROL[8]. While high it resets the
arbiter, clears the event counter and flushes the FIFO. If left unconnected the
compiler ties it to the board’s FIFO_RESET net, so the block is flushed by the
global FIFO reset.
prog_full flag, asserted one full packet (38 words)
before the real full. While it is high, incoming events are acknowledged but
discarded. Use it to throttle the trigger.
prog_empty flag, so it rises only when
more than one complete packet is buffered. It is the same bit the host reads as
STATUS[0].
TS0_OUT pin of the matching Citiroc
Analog block. Becomes word 1 of the packet and is decoded as Time_Code.
TS_OUT pin of the matching
Citiroc Analog block. Split into words 2 (low) and 3 (high) and decoded as
RunTime_Code.
P_FRAME_DV. Sampled when
the round-robin arbiter reaches that ASIC’s slot; must stay high until the matching
ACK is returned. Masked ASICs (CONTROL[0..3] = 1) are never sampled.
P_FRAME_ACK. It is pulsed even when the
FIFO was too full to actually store the event.
Properties
Set the name of the endpoint
Name of the memory-mapped FIFO endpoint, and the name embedded in every generated
symbol: the registers become SCI_REG_<name>_CONTROL / _STATUS /
_FIFOADDRESS, and the API functions become CitirocFRAME_<name>_START,
_RESET, _GetStatus, _DOWNLOAD, _RECONSTRUCT_DATA (plus _GET_STATUS and
_GET_DATA in Python). Changing it rebuilds the symbol.
Default: CitirocFrame0
Default: CitirocFrame0
Set the number of samples stored for each acquisition
Depth of the readout FIFO, in 32-bit words (not in samples per channel, despite
the caption). It is passed straight to the memLength generic and becomes the XPM
FIFO_WRITE_DEPTH. Capacity in events is Samples / 38.
| Value | Events buffered | Usable? |
|---|---|---|
| 1024 | 26 | yes |
| 4096 | 107 | yes |
| 8192 | 215 | yes |
| 16384 | 431 | yes |
| 32768 | 862 | yes |
| 65536 | 1724 | yes — the largest depth whose word-count field still fits in STATUS |
| 131072 | 3449 | no — the STATUS word-count slice runs past bit 31 |
| 262144 | 6898 | no — same problem |
Default: 8192
Default: 8192
Options: 1024 4096 8192 16384 32768 65536 131072 262144
⚙️ Detailed Operation
Packet layout — 38 words of 32 bits
This is the authoritative layout, cross-checked between CitirocFrameTransfer.vhd,
the generated C decoder and the generated Python decoder.
| Word | Content | Decoder field |
|---|---|---|
| 0 | 0x8000000n — sync header, n = ASIC index (A=0, B=1, C=2, D=3) |
AsicID |
| 1 | TS_T0_x — 32-bit run timestamp |
Time_Code |
| 2 | TS_x[31:0] — global timestamp, low half |
RunTime_Code (low) |
| 3 | TS_x[63:32] — global timestamp, high half |
RunTime_Code (high) |
| 4 | event counter of this block, free running | Pack_Id |
| 5 | ASIC channel 31 data word | ChargeHg[0], ChargeLg[0], Hit[0] |
| 6 | ASIC channel 30 data word | …[1] |
| … | … | … |
| 36 | ASIC channel 0 data word | ChargeHg[31], ChargeLg[31], Hit[31] |
| 37 | 0xC0000000 — trailer |
— |
Each channel data word:
| Bits | Field | Width |
|---|---|---|
| 31:29 | reserved, always 000 |
3 |
| 28 | Hit — channel discriminator |
1 |
| 27:14 | ChargeLg — low-gain charge |
14 |
| 13:0 | ChargeHg — high-gain charge |
14 |
Note the naming: the 32-bit TS_T0 becomes Time_Code and the 64-bit global
timestamp becomes RunTime_Code in the decoded structure — the opposite of what the
names suggest.
Resynchronisation rules used by the decoder
The generated decoder is a 7-state machine over the raw word stream:
stateDiagram-v2 [*] --> HUNT HUNT --> TS_T0 : word>>4 == 0x8000000 HUNT --> HUNT : otherwise (discard) TS_T0 --> TS_LO TS_LO --> TS_HI TS_HI --> PACKID PACKID --> DATA DATA --> DATA : 32 channel words DATA --> TRAIL : after the 32nd TRAIL --> HUNT : word & 0xC0000000 == 0xC0000000 -> emit packet TRAIL --> HUNT : otherwise -> discard packet
Two consequences worth knowing:
- A packet is emitted only if the trailer check passes. A truncated or corrupted packet is silently dropped and the decoder resynchronises on the next header.
- The header test is
word >> 4 == 0x8000000, so any of0x80000000…0x8000000Fis a valid header. The low nibble is the ASIC index.
Round-robin arbiter and event loss
- The poll slot advances every clock, unconditionally, whether or not an event was
found. A
DV_xis therefore serviced within at most 4 clock cycles. BUSYis high only while the 38 words are being written into the FIFO.- The whole event is latched in one cycle, so the analog block may drop
DVas soon as it seesACKwithout any risk of tearing.
FIFO thresholds
With memLength = Samples and MaxPacketSize = 38:
| XPM parameter | Value | Effect |
|---|---|---|
FIFO_WRITE_DEPTH |
Samples |
total depth in 32-bit words |
PROG_FULL_THRESH |
Samples − 38 |
drives FULL / FIFO_FULL, one packet of headroom |
PROG_EMPTY_THRESH |
38 |
drives DATA_AVAL and STATUS[0] |
READ_MODE |
"std", latency 1 |
standard (non-FWFT) read |
CDC_SYNC_STAGES |
2 | write and read clocks are independent |
So DATA_AVAL (and the software “data available” bit) only rises once more than one
full packet is buffered — the host is never woken up for a partial packet. Capacity in
events is Samples / 38: 215 packets at the default 8192.
Control and status registers
CompileMMC declares one FIFO-read endpoint named after the EndpointName property,
with two auto-registers.
CONTROL (write)
| Bit | Function |
|---|---|
| 0 | Mask ASIC A — 1 = ignore DV_A, its events are never collected |
| 1 | Mask ASIC B |
| 2 | Mask ASIC C |
| 3 | Mask ASIC D |
| 8 | Reset / flush — holds the arbiter and the FIFO in reset and clears the event counter |
Note the polarity: a mask bit set disables that ASIC. Writing 0 therefore enables
all four and releases the reset, which is exactly what the generated _START function
does.
STATUS (read)
| Bits | Function |
|---|---|
| 0 | Data available (1 = more than one packet buffered) |
16 + rBits − 1 : 16 |
Words currently in the FIFO |
The FLUSH pin is ORed with CONTROL[8]: both reset the arbiter and flush the FIFO.
Generated software API
The C library gets five functions, named after the endpoint (CitirocFrame0 by
default):
| Function | What it does |
|---|---|
CitirocFRAME_<name>_START(handle) |
writes 0 to CONTROL — releases the reset and unmasks all four ASICs |
CitirocFRAME_<name>_RESET(handle) |
writes 0x100 to CONTROL — asserts the flush and holds it |
CitirocFRAME_<name>_GetStatus(&is_data_available, &word_available, handle) |
reads STATUS; is_data_available = STATUS & 1, word_available = (STATUS >> 16) & 0xFFFF |
CitirocFRAME_<name>_DOWNLOAD(val, size, timeout, handle, &read_data, &valid_data) |
bulk FIFO read; val must be preallocated to size + 16 words |
CitirocFRAME_<name>_RECONSTRUCT_DATA(buffer_handle, &decoded_packets) |
runs the state machine above over a circular buffer and fills a t_ASIC_packet_collection |
RESET only asserts the flush — it does not release it. A RESET must always be
followed by a START before data can flow.
The decoded structure carries, per packet: AsicID, Time_Code, RunTime_Code,
Pack_Id, and three 32-element arrays Hit[], ChargeHg[], ChargeLg[]. Release it
with free_CitirocFRAME_packet_collection.
C usage
c
uint32_t data_frame[100000];
uint32_t read_data_frame, valid_data_frame, valid_data_enqueued;
t_ASIC_packet_collection decoded_packets;
void *BufferDownloadHandler = NULL;
Utility_ALLOCATE_DOWNLOAD_BUFFER(&BufferDownloadHandler, 1024 * 1024);
Citiroc_ALLFIFO_FLUSH(&handle);
CitirocFRAME_CitirocFrame0_START(&handle);
while (1) {
uint32_t is_data_available = 0, word_available = 0;
CitirocFRAME_CitirocFrame0_GetStatus(&is_data_available, &word_available, &handle);
if (!is_data_available) continue;
/* ask for whole packets: 38 words each */
CitirocFRAME_CitirocFrame0_DOWNLOAD(data_frame, N_Packet * 38, 1000,
&handle, &read_data_frame, &valid_data_frame);
Utility_ENQUEUE_DATA_IN_DOWNLOAD_BUFFER(BufferDownloadHandler, data_frame,
valid_data_frame, &valid_data_enqueued);
CitirocFRAME_CitirocFrame0_RECONSTRUCT_DATA(BufferDownloadHandler, &decoded_packets);
for (int p = 0; p < decoded_packets.valid_packets; p++) {
printf("ASIC %u pack %u t0 %u\n",
decoded_packets.packets[p].AsicID,
decoded_packets.packets[p].Pack_Id,
decoded_packets.packets[p].Time_Code);
for (int i = 0; i < 32; i++) {
int ch = 31 - i; /* arrival order is reversed, see above */
printf(" ch%2d hit=%u hg=%5u lg=%5u\n", ch,
decoded_packets.packets[p].Hit[i],
decoded_packets.packets[p].ChargeHg[i],
decoded_packets.packets[p].ChargeLg[i]);
}
}
free_CitirocFRAME_packet_collection(&decoded_packets);
}
Python usage
python
while True:
[err, data, read_data, valid_data] = CitirocFRAME_CitirocFrame0_GET_DATA(16, 1000, handle)
if valid_data > 0:
[AsicId, Time_Code, RunTime_Code, Pack_Id,
ChargeHg, ChargeLg, Hit] = CitirocFRAME_CitirocFrame0_RECONSTRUCT_DATA(data, valid_data)
for i in range(len(Pack_Id)):
print(Pack_Id[i], Time_Code[i], ChargeHg[i])
..._GET_DATA(n_packet, timeout_ms, handle) sizes its request as n_packet * 38 words,
which is the correct packet size for this block.
Typical wiring
Citiroc Analog (A) ─DATA_A/TS_A/TS_T0_A/DV_A─►┌──────────────────┐
◄────────────── ACK_A ─────│ │
Citiroc Analog (B) ──────────────────────────►│ Citiroc Frame │──► FIFO endpoint ──► PC
Citiroc Analog (C) ──────────────────────────►│ Transfer │
Citiroc Analog (D) ──────────────────────────►│ │──► FULL / DATA_AVAL / BUSY
FLUSH ──────►└──────────────────┘
Unused ASIC ports may be left unconnected — just mask them with CONTROL[0..3] so the
arbiter never latches an undriven DV.
Resources & Timing
-
Latency: Up to 4 clock cycles from
DV_xtoACK_x(round-robin poll), then 38 clock cycles to write the packet into the FIFO -
Throughput: One 38-word packet per 38 write clocks, shared by the four ASICs
- Emits
pcores/CitirocFrameTransfer.vhd(entityCitirocFrameTransfer) from the plainCitirocFrameTransfer.vhdresource — not the_NEWvariant, which belongs to the V2 block. xpm_fifo_asyncwith independent write (CLK) and read (READ_CLK) clocks; the compiler currently binds both to the project global clock.- One event = 38 words = 152 bytes on the wire.
_RESETasserts the flush and leaves it asserted; always follow it with_START.- Supported board: DT5550W-Citiroc1A (
97567308-DA16-4E48-BB5E-ACE3E84DF06D). Not available inside subpages or state machines.