feat: FPGA VANC timecode inserter for Ninja V recording trigger#605
feat: FPGA VANC timecode inserter for Ninja V recording trigger#605alfskaar wants to merge 1 commit intohd-zero:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds reference FPGA logic and documentation to inject SMPTE 12M-2 timecode as a VANC (ST 291M ANC) packet during vertical blanking, intended to trigger Atomos Ninja V recording via VANC timecode presence.
Changes:
- Introduces a
vanc_timecode_inserterVerilog module with counters, packet builder, and insertion mux. - Adds a README describing the VANC packet structure, integration placement, and CPU register control flow.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| fpga_vanc_timecode/vanc_timecode_inserter.v | New sample Verilog module for VANC timecode packet generation and insertion into blanking. |
| fpga_vanc_timecode/README.md | Documentation for packet format and integration guidance for the inserter module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Checksum: 9-bit sum of DID through last UDW, then keep [7:0] | ||
| // (not bit-inverted — SMPTE 291M uses simple sum) | ||
| checksum = {1'b0, pkt_rom[3]} + {1'b0, pkt_rom[4]} + {1'b0, pkt_rom[5]} | ||
| + {1'b0, pkt_rom[6]} + {1'b0, pkt_rom[7]} + {1'b0, pkt_rom[8]} | ||
| + {1'b0, pkt_rom[9]} + {1'b0, pkt_rom[10]} + {1'b0, pkt_rom[11]} | ||
| + {1'b0, pkt_rom[12]} + {1'b0, pkt_rom[13]} + {1'b0, pkt_rom[14]} | ||
| + {1'b0, pkt_rom[15]} + {1'b0, pkt_rom[16]} + {1'b0, pkt_rom[17]} | ||
| + {1'b0, pkt_rom[18]} + {1'b0, pkt_rom[19]} + {1'b0, pkt_rom[20]} | ||
| + {1'b0, pkt_rom[21]}; | ||
| pkt_rom[22] = checksum[7:0]; |
There was a problem hiding this comment.
Checksum handling is internally inconsistent: the header/README describe a 9-bit checksum, but the implementation truncates to checksum[7:0] and stores only 8 bits in pkt_rom[22]. Please align the implementation and documentation on the checksum width/format (and update packet word width accordingly if a 9-bit checksum is intended).
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| if (v_sync_rise) begin | ||
| v_count <= 12'd0; | ||
| end else if (h_sync_rise) begin | ||
| v_count <= v_count + 12'd1; | ||
| h_count <= 12'd0; | ||
| end else begin | ||
| h_count <= h_count + 12'd1; | ||
| end |
There was a problem hiding this comment.
On v_sync_rise the logic resets v_count but leaves h_count running. If VSYNC can assert/deassert mid-line in the upstream timing, this can misalign h_count on the first line of a frame and shift the insertion point. Resetting h_count on v_sync_rise as well would make the counter alignment deterministic per-frame.
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| - **DID**: Data Identifier = `0x60` (SMPTE 12M-2 timecode) | ||
| - **SDID**: Secondary Data ID = `0x60` (SMPTE 12M-2 timecode) | ||
| - **DC**: Data Count = `0x10` (16 UDW words for timecode) | ||
| - **CS**: Checksum (9-bit sum of DID through last UDW, bits [8:0]) |
There was a problem hiding this comment.
The README states the checksum is a 9-bit sum (bits [8:0]), but the current Verilog implementation stores only an 8-bit value (checksum[7:0]). Please update the README to match the implemented checksum format, or adjust the implementation to match this documented 9-bit checksum behavior.
| - **CS**: Checksum (9-bit sum of DID through last UDW, bits [8:0]) | |
| - **CS**: Checksum (implemented as an 8-bit sum of DID through last UDW, stored in `checksum[7:0]`) |
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| VANC data starts after SAV (Start of Active Video) EAV/SAV codes. | ||
| Place the ADF at horizontal pixel position 12+ (after EAV/SAV sequence). | ||
|
|
There was a problem hiding this comment.
The “Horizontal Position” section references placing ADF after SAV/EAV codes, but the provided module interface uses separate hsync/vsync/de signals and does not parse embedded SAV/EAV sequences. Please clarify whether the intended input format is DE-based parallel video (no embedded EAV/SAV) or BT.656-style embedded sync, and adjust the guidance accordingly so integrators pick the correct horizontal offset reference.
| VANC data starts after SAV (Start of Active Video) EAV/SAV codes. | |
| Place the ADF at horizontal pixel position 12+ (after EAV/SAV sequence). | |
| This design assumes a **DE-based parallel video interface** with separate | |
| `hsync`, `vsync`, and `de` signals. The VANC inserter does **not** parse | |
| embedded BT.656 EAV/SAV codewords. | |
| For this interface, choose a horizontal offset measured from the **start of the | |
| horizontal blanking interval** on the selected VANC line, and place the ADF a | |
| few pixel clocks into blanking (for example, horizontal position 12+), while | |
| ensuring the full packet completes before active video (`de=1`) begins. | |
| If you adapt the design to a **BT.656 / embedded-sync** stream, the equivalent | |
| reference point is after the EAV sequence, not from `de` timing. |
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| if (tc_enable && on_vanc_line && at_h_start && !vid_de_in) begin | ||
| // Start VANC insertion at the designated position in blanking | ||
| inserting <= 1'b1; | ||
| pkt_idx <= 5'd0; | ||
| end else if (inserting) begin |
There was a problem hiding this comment.
Timing note: inserting/pkt_idx are updated on the clock edge, and vid_data_out is also registered. As written, when the start condition hits (at_h_start), the same cycle still outputs passthrough data; pkt_rom[0] won’t appear until the next pixel. If VANC_H_START is meant to be the pixel where ADF[0] is placed, this is an off-by-one. Consider using next-state signals for the mux (or documenting that VANC_H_START is "one cycle before" the first ADF word).
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| module vanc_timecode_inserter #( | ||
| parameter VANC_LINE = 14, // Vertical blanking line for VANC insertion | ||
| parameter VANC_H_START = 16, // Horizontal start pixel for ADF | ||
| parameter BIT_WIDTH = 8 // 8-bit or 10-bit video (8 for IT66121) | ||
| )( |
There was a problem hiding this comment.
BIT_WIDTH is documented as supporting 8-bit or 10-bit video, but the packet generation/mux is hard-coded to 8-bit values. With BIT_WIDTH=10, the VANC words will be zero-extended and the ADF/DID/UDW values will be wrong. Please either restrict BIT_WIDTH to 8 (and enforce it) or generate true BIT_WIDTH-wide ANC words (including correct 10-bit ADF values) and checksum for 10-bit mode.
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| if (inserting) begin | ||
| vid_data_out <= pkt_rom[pkt_idx]; | ||
| end else begin | ||
| vid_data_out <= vid_data_in; | ||
| end |
There was a problem hiding this comment.
The inserter keeps outputting packet bytes for PKT_LEN cycles once inserting goes high, without checking that the stream is still in horizontal blanking (vid_de_in==0). If blanking is shorter than expected (or VANC_H_START is too late), this can overwrite active video. Consider gating insertion/pkt_idx advancement with !vid_de_in and/or aborting insertion when vid_de_in asserts to prevent corrupting active pixels.
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
| // Build packet combinationally from timecode registers | ||
| always @(*) begin | ||
| // ADF | ||
| pkt_rom[0] = 8'h00; | ||
| pkt_rom[1] = 8'hFF; | ||
| pkt_rom[2] = 8'hFF; | ||
| // DID, SDID, DC | ||
| pkt_rom[3] = 8'h60; | ||
| pkt_rom[4] = 8'h60; | ||
| pkt_rom[5] = 8'h10; | ||
| // UDW0-7: timecode (BCD nibbles) | ||
| pkt_rom[6] = {4'h0, tc_frames_u}; | ||
| pkt_rom[7] = {4'h0, 2'b00, tc_frames_t}; | ||
| pkt_rom[8] = {4'h0, tc_seconds_u}; | ||
| pkt_rom[9] = {4'h0, 1'b0, tc_seconds_t}; | ||
| pkt_rom[10] = {4'h0, tc_minutes_u}; | ||
| pkt_rom[11] = {4'h0, 1'b0, tc_minutes_t}; | ||
| pkt_rom[12] = {4'h0, tc_hours_u}; | ||
| pkt_rom[13] = {4'h0, 2'b00, tc_hours_t}; |
There was a problem hiding this comment.
pkt_rom is built combinationally directly from the CPU-provided timecode inputs. If the CPU updates any digit while a packet is being emitted, the packet can contain a mix of old/new fields and an inconsistent checksum. Consider latching the timecode fields at the start of insertion (when inserting is asserted) and building the packet from the latched copy for the duration of that packet.
There was a problem hiding this comment.
Fixed in subsequent commits (checksum→8-bit, h_count reset, BIT_WIDTH removed, timecode latching, DE guard, VANC_H_START documented, README aligned).
Sample Verilog module and documentation for injecting SMPTE 12M-2 timecode as VANC (Vertical Ancillary Data) packets into the HDMI video blanking interval. The Atomos Ninja V uses VANC timecode — not HDMI InfoFrames — to detect recording state. This must be injected by the FPGA before the video reaches the IT66121 HDMI transmitter. Files: - vanc_timecode_inserter.v: 8-bit DE-based parallel video inserter with line/pixel counters, SMPTE ST 291M packet builder, timecode input latching, DE-guard abort, and video passthrough mux - README.md: VANC packet format spec, integration notes, blanking line selection, and CPU register interface
ce1c1d7 to
28c0200
Compare
Summary
Sample Verilog module and documentation for injecting SMPTE 12M-2 timecode as VANC (Vertical Ancillary Data) packets into the HDMI video blanking interval.
Background
The Atomos Ninja V "HDMI Device" trigger modes (Canon EOS R, Sony ILCE-7SM3, etc.) use VANC timecode — not HDMI InfoFrames — to detect recording state.
HDfury captures of a real Canon EOS R confirmed all InfoFrames are byte-for-byte identical between REC ON and REC OFF. The trigger mechanism is SMPTE 12M-2 timecode embedded at the pixel level during vertical blanking.
The IT66121 HDMI transmitter cannot inject VANC — it only handles InfoFrames and audio. VANC must be injected by the FPGA before the video reaches the IT66121.
Files
fpga_vanc_timecode/vanc_timecode_inserter.v— Verilog module: line/pixel counters, SMPTE ST 291M packet builder with BCD timecode, insertion state machine, video passthrough muxfpga_vanc_timecode/README.md— VANC packet format spec, integration notes, blanking line selection, and CPU register interfaceIntegration
The VANC inserter goes in the FPGA video pipeline:
CPU (ARM / Allwinner V536) controls timecode via FPGA registers:
TC_HOURS,TC_MINUTES,TC_SECONDS,TC_FRAMES,TC_ENABLEStatus
This is sample/reference code for FPGA integration. Software-side Canon EOS R VSIF trigger code exists separately in the firmware (not included in this PR).