-
Notifications
You must be signed in to change notification settings - Fork 94
feat: FPGA VANC timecode inserter for Ninja V recording trigger #607
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
alfskaar
wants to merge
2
commits into
hd-zero:main
Choose a base branch
from
alfskaar:fpga-vanc-timecode-v2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+378
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| # FPGA VANC Timecode Injection — Atomos Ninja V Trigger | ||
|
|
||
| ## Problem | ||
|
|
||
| The Atomos Ninja V "HDMI Device" trigger modes (Canon EOS R, Sony ILCE-7SM3, etc.) | ||
| use **VANC (Vertical Ancillary Data)** embedded in the video blanking interval to | ||
| detect recording state, not HDMI InfoFrames. | ||
|
|
||
| HDfury captures (log 1590) of a real Canon EOS R confirmed: | ||
| - All InfoFrames (AVI, SPD, VSIF) are **byte-for-byte identical** between REC ON and REC OFF | ||
| - The trigger mechanism is VANC timecode: SMPTE 12M-2 / SMPTE ST 291M ancillary data packets | ||
| injected 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. | ||
|
|
||
| ## VANC Timecode Format | ||
|
|
||
| ### SMPTE ST 291M Ancillary Data Packet Structure | ||
|
|
||
| VANC packets are embedded in the video blanking lines as pixel-level data: | ||
|
|
||
| ``` | ||
| ADF (3 words) | DID | SDID | DC | UDW[0..DC-1] | CS | ||
| ``` | ||
|
|
||
| - **ADF**: Ancillary Data Flag — `0x000 0x3FF 0x3FF` (10-bit) or `0x00 0xFF 0xFF` (8-bit) | ||
| - **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 — raw modulo-256 sum of DID through last UDW: `CS = (DID + SDID + DC + UDW[0] + ... + UDW[DC-1]) & 0xFF`. Not complemented or two's-complemented. Matches the Verilog module implementation. | ||
|
|
||
| ### SMPTE 12M-2 Timecode UDW Layout (16 words) | ||
|
|
||
| | UDW | Content | BCD Value | | ||
| |-----|---------------------------|-------------------------| | ||
| | 0 | Frames units (bits 3:0) | 0x0–0x9 | | ||
| | 1 | Frames tens (bits 1:0) + flags | bit2=drop, bit3=CF | | ||
| | 2 | Seconds units (bits 3:0) | 0x0–0x9 | | ||
| | 3 | Seconds tens (bits 2:0) + flag | bit3=field_phase | | ||
| | 4 | Minutes units (bits 3:0) | 0x0–0x9 | | ||
| | 5 | Minutes tens (bits 2:0) + flag | bit3=BGF0 | | ||
| | 6 | Hours units (bits 3:0) | 0x0–0x9 | | ||
| | 7 | Hours tens (bits 1:0) + flags | bit2=BGF1, bit3=BGF2| | ||
| | 8–15| Binary group data (BG1–BG8) | User bits (typically 0)| | ||
|
|
||
| ### Timecode Example: 00:01:23:15 | ||
|
|
||
| ``` | ||
| UDW[0] = 0x05 (frames units = 5) | ||
| UDW[1] = 0x01 (frames tens = 1, no flags) | ||
| UDW[2] = 0x03 (seconds units = 3) | ||
| UDW[3] = 0x02 (seconds tens = 2) | ||
| UDW[4] = 0x01 (minutes units = 1) | ||
| UDW[5] = 0x00 (minutes tens = 0) | ||
| UDW[6] = 0x00 (hours units = 0) | ||
| UDW[7] = 0x00 (hours tens = 0) | ||
| UDW[8..15] = 0x00 (binary groups, unused) | ||
| ``` | ||
|
|
||
| ## Integration Notes | ||
|
|
||
| ### FPGA Video Pipeline | ||
|
|
||
| The VANC inserter must be placed in the FPGA video pipeline **after** the frame | ||
| buffer / scaler and **before** the IT66121 parallel video input: | ||
|
|
||
| ``` | ||
| Camera RX → Frame Buffer → Scaler → [VANC Inserter] → IT66121 Parallel Input | ||
| ``` | ||
|
|
||
| ### Blanking Line Selection | ||
|
|
||
| - **720p**: Lines 1–25 are vertical blanking. Use line 10–15 for VANC. | ||
| - **1080p**: Lines 1–41 are vertical blanking. Use line 10–15 for VANC. | ||
| - **1080i**: Lines 1–20 (field 1), 564–583 (field 2). Use line 10–15. | ||
|
|
||
| The Ninja V typically expects VANC on lines 12–15 (standard broadcast position). | ||
|
|
||
| Note: The line numbers above are **1-based video line numbers**. In the sample | ||
| Verilog module, `v_count` resets to `0` on the rising edge of `vsync` and then | ||
| increments on each rising edge of `hsync`, so the `VANC_LINE` parameter follows | ||
| that internal counter, not the 1-based numbering used here. In other words, if | ||
| `vsync` marks the start-of-frame boundary before the first counted line, then | ||
| broadcast line **N** typically maps to `VANC_LINE = N - 1` (for example, | ||
| broadcast lines 12–15 map to `VANC_LINE` 11–14). Verify this against your | ||
| exact sync timing if your pipeline defines `vsync` differently. | ||
| Note: The line numbers above are **1-based video line numbers**. In the sample | ||
| Verilog module, `v_count` resets to `0` on the rising edge of `vsync` and then | ||
| increments on each rising edge of `hsync`, so the `VANC_LINE` parameter follows | ||
| that internal counter, not the 1-based numbering used here. In other words, if | ||
| `vsync` marks the start-of-frame boundary before the first counted line, then | ||
| broadcast line **N** typically maps to `VANC_LINE = N - 1` (for example, | ||
| broadcast lines 12–15 map to `VANC_LINE` 11–14). Verify this against your | ||
| exact sync timing if your pipeline defines `vsync` differently. | ||
|
|
||
| ### Horizontal Position | ||
|
|
||
| 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 23-byte packet completes before active video (`de=1`) begins. | ||
|
|
||
| Note: The Verilog module has a 1-cycle output register pipeline, so | ||
| `VANC_H_START` should be set to `desired_ADF_pixel - 1`. | ||
|
|
||
| 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. | ||
|
|
||
| ### Software Interface | ||
|
|
||
| The ARM CPU (Allwinner V536) controls the FPGA timecode via: | ||
| 1. SPI or I2C registers exposed by the FPGA | ||
| 2. Registers: `TC_HOURS`, `TC_MINUTES`, `TC_SECONDS`, `TC_FRAMES`, `TC_ENABLE` | ||
| 3. When `TC_ENABLE=1`, the FPGA inserts VANC packets on every frame | ||
| 4. The ARM updates the timecode registers once per second | ||
|
|
||
| ### Trigger Protocol (Record-Run) | ||
|
|
||
| 1. **Idle**: `TC_ENABLE=0` → no VANC packets → Ninja V sees no timecode | ||
| 2. **Record Start**: ARM sets timecode to 00:00:00:00, sets `TC_ENABLE=1` | ||
| 3. **Recording**: ARM increments timecode every second. FPGA inserts VANC. | ||
| 4. **Record Stop**: ARM sets `TC_ENABLE=0` → VANC packets stop | ||
| 5. Ninja V detects timecode start/stop transitions to trigger recording | ||
|
|
||
| ## Files | ||
|
|
||
| - `vanc_timecode_inserter.v` — Sample Verilog module for VANC injection | ||
| - `README.md` — This file | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| // ========================================================================== | ||
| // vanc_timecode_inserter.v | ||
| // | ||
| // SMPTE ST 291M VANC Timecode Inserter for Atomos Ninja V Trigger | ||
| // | ||
| // Inserts SMPTE 12M-2 timecode as a VANC ancillary data packet into | ||
| // the vertical blanking interval of the video stream. The Ninja V | ||
| // reads this timecode to trigger recording start/stop. | ||
| // | ||
| // Interface: | ||
| // - 8-bit parallel video in/out with DE/HSYNC/VSYNC (before IT66121) | ||
| // - CPU-writable timecode registers (directly mapped or via SPI/I2C) | ||
| // | ||
| // Counter origin: v_count and h_count are 0-based. v_count resets to 0 | ||
| // on vsync rising edge and increments on each hsync rising edge. | ||
| // h_count resets to 0 on hsync (or vsync) rising edge and increments | ||
| // every pixel clock. So broadcast line N (1-based) maps to | ||
| // VANC_LINE = N - 1, and broadcast pixel P maps to VANC_H_START = P - 2 | ||
| // (the extra -1 accounts for the 1-cycle output register pipeline: | ||
| // VANC_H_START is the cycle *before* ADF[0] appears on vid_data_out). | ||
| // | ||
| // IMPORTANT: This is SAMPLE code — adapt to the actual FPGA video | ||
| // pipeline, clock domain, and register interface. | ||
| // ========================================================================== | ||
|
|
||
| module vanc_timecode_inserter #( | ||
| parameter [12:0] VANC_LINE = 13'd14, // 0-based line number for VANC insertion | ||
| parameter [12:0] VANC_H_START = 13'd15 // 0-based pixel cycle before ADF[0] output | ||
| )( | ||
| input wire clk, // Pixel clock | ||
| input wire rst_n, // Active-low reset | ||
|
|
||
| // Video input — 8-bit DE-based parallel interface (no embedded EAV/SAV) | ||
| input wire [7:0] vid_data_in, | ||
| input wire vid_hsync_in, | ||
| input wire vid_vsync_in, | ||
| input wire vid_de_in, | ||
|
|
||
| // Video output (to IT66121) | ||
| output reg [7:0] vid_data_out, | ||
| output reg vid_hsync_out, | ||
| output reg vid_vsync_out, | ||
| output reg vid_de_out, | ||
|
|
||
| // Timecode registers (directly mapped from CPU or via SPI/I2C bridge) | ||
| input wire tc_enable, // 1=insert VANC timecode, 0=passthrough | ||
| input wire [3:0] tc_frames_u, // Frames units (0-9) | ||
| input wire [1:0] tc_frames_t, // Frames tens (0-2) | ||
| input wire [3:0] tc_seconds_u, // Seconds units (0-9) | ||
| input wire [2:0] tc_seconds_t, // Seconds tens (0-5) | ||
| input wire [3:0] tc_minutes_u, // Minutes units (0-9) | ||
| input wire [2:0] tc_minutes_t, // Minutes tens (0-5) | ||
| input wire [3:0] tc_hours_u, // Hours units (0-9) | ||
| input wire [1:0] tc_hours_t // Hours tens (0-2) | ||
| ); | ||
|
|
||
| // -------------------------------------------------------------------------- | ||
| // Line and pixel counters | ||
| // -------------------------------------------------------------------------- | ||
| reg [12:0] h_count; // Horizontal pixel counter (13-bit: supports Htotal up to 8191) | ||
| reg [12:0] v_count; // Vertical line counter (13-bit: supports Vtotal up to 8191) | ||
| reg prev_hsync; | ||
| reg prev_vsync; | ||
|
|
||
| wire h_sync_rise = vid_hsync_in & ~prev_hsync; | ||
| wire v_sync_rise = vid_vsync_in & ~prev_vsync; | ||
|
|
||
| always @(posedge clk or negedge rst_n) begin | ||
| if (!rst_n) begin | ||
| h_count <= 13'd0; | ||
| v_count <= 13'd0; | ||
| prev_hsync <= 1'b0; | ||
| prev_vsync <= 1'b0; | ||
| end else begin | ||
| prev_hsync <= vid_hsync_in; | ||
| prev_vsync <= vid_vsync_in; | ||
|
|
||
| if (v_sync_rise) begin | ||
| v_count <= 13'd0; | ||
| h_count <= 13'd0; // Reset h_count on frame start for deterministic alignment | ||
| end else if (h_sync_rise) begin | ||
| v_count <= v_count + 13'd1; | ||
| h_count <= 13'd0; | ||
| end else begin | ||
| h_count <= h_count + 13'd1; | ||
| end | ||
| end | ||
| end | ||
|
|
||
| // -------------------------------------------------------------------------- | ||
| // Timecode latch — snapshot inputs at insertion start to prevent | ||
| // mid-packet changes if the CPU updates timecode during emission. | ||
| // -------------------------------------------------------------------------- | ||
| reg [3:0] lat_frames_u; | ||
| reg [1:0] lat_frames_t; | ||
| reg [3:0] lat_seconds_u; | ||
| reg [2:0] lat_seconds_t; | ||
| reg [3:0] lat_minutes_u; | ||
| reg [2:0] lat_minutes_t; | ||
| reg [3:0] lat_hours_u; | ||
| reg [1:0] lat_hours_t; | ||
|
|
||
| // -------------------------------------------------------------------------- | ||
| // VANC packet ROM — SMPTE ST 291M with SMPTE 12M-2 timecode | ||
| // | ||
| // Packet structure (8-bit mode): | ||
| // [0] ADF0 = 0x00 | ||
| // [1] ADF1 = 0xFF | ||
| // [2] ADF2 = 0xFF | ||
| // [3] DID = 0x60 (SMPTE 12M-2) | ||
| // [4] SDID = 0x60 (SMPTE 12M-2) | ||
| // [5] DC = 0x10 (16 data words) | ||
| // [6] UDW0 = frames units | ||
| // [7] UDW1 = frames tens + flags | ||
| // [8] UDW2 = seconds units | ||
| // [9] UDW3 = seconds tens + flags | ||
| // [10] UDW4 = minutes units | ||
| // [11] UDW5 = minutes tens + flags | ||
| // [12] UDW6 = hours units | ||
| // [13] UDW7 = hours tens + flags | ||
| // [14..21] UDW8-15 = binary groups (zeros) | ||
| // [22] CS = checksum (8-bit sum of DID through last UDW) | ||
| // -------------------------------------------------------------------------- | ||
|
|
||
| localparam PKT_LEN = 23; // Total packet words (3 ADF + DID + SDID + DC + 16 UDW + CS) | ||
|
|
||
| reg [7:0] pkt_rom [0:PKT_LEN-1]; | ||
| reg [7:0] checksum; // 8-bit sum (SMPTE ST 291M section 6 for 8-bit interfaces) | ||
|
|
||
| // Build packet combinationally from latched 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) — from latched values | ||
| pkt_rom[6] = {4'h0, lat_frames_u}; | ||
| pkt_rom[7] = {4'h0, 2'b00, lat_frames_t}; | ||
| pkt_rom[8] = {4'h0, lat_seconds_u}; | ||
| pkt_rom[9] = {4'h0, 1'b0, lat_seconds_t}; | ||
| pkt_rom[10] = {4'h0, lat_minutes_u}; | ||
| pkt_rom[11] = {4'h0, 1'b0, lat_minutes_t}; | ||
| pkt_rom[12] = {4'h0, lat_hours_u}; | ||
| pkt_rom[13] = {4'h0, 2'b00, lat_hours_t}; | ||
| // UDW8-15: binary groups (zeros) | ||
| pkt_rom[14] = 8'h00; | ||
| pkt_rom[15] = 8'h00; | ||
| pkt_rom[16] = 8'h00; | ||
| pkt_rom[17] = 8'h00; | ||
| pkt_rom[18] = 8'h00; | ||
| pkt_rom[19] = 8'h00; | ||
| pkt_rom[20] = 8'h00; | ||
| pkt_rom[21] = 8'h00; | ||
| // Checksum: 8-bit sum of DID through last UDW (SMPTE ST 291M section 6, 8-bit interface) | ||
| checksum = pkt_rom[3] + pkt_rom[4] + pkt_rom[5] | ||
| + pkt_rom[6] + pkt_rom[7] + pkt_rom[8] | ||
| + pkt_rom[9] + pkt_rom[10] + pkt_rom[11] | ||
| + pkt_rom[12] + pkt_rom[13] + pkt_rom[14] | ||
| + pkt_rom[15] + pkt_rom[16] + pkt_rom[17] | ||
| + pkt_rom[18] + pkt_rom[19] + pkt_rom[20] | ||
| + pkt_rom[21]; | ||
| pkt_rom[22] = checksum; | ||
| end | ||
|
|
||
| // -------------------------------------------------------------------------- | ||
| // VANC insertion state machine | ||
| // | ||
| // Latches timecode at insertion start. Aborts if vid_de_in asserts | ||
| // mid-packet (blanking shorter than expected) to prevent corrupting | ||
| // active video pixels. | ||
| // -------------------------------------------------------------------------- | ||
| reg inserting; // 1 while overwriting pixels with VANC data | ||
| reg [4:0] pkt_idx; // Current byte index into pkt_rom | ||
|
|
||
| wire on_vanc_line = (v_count == VANC_LINE); | ||
| wire at_h_start = (h_count == VANC_H_START); | ||
|
|
||
| always @(posedge clk or negedge rst_n) begin | ||
| if (!rst_n) begin | ||
| inserting <= 1'b0; | ||
| pkt_idx <= 5'd0; | ||
| lat_frames_u <= 4'd0; | ||
| lat_frames_t <= 2'd0; | ||
| lat_seconds_u <= 4'd0; | ||
| lat_seconds_t <= 3'd0; | ||
| lat_minutes_u <= 4'd0; | ||
| lat_minutes_t <= 3'd0; | ||
| lat_hours_u <= 4'd0; | ||
| lat_hours_t <= 2'd0; | ||
| end else begin | ||
| if (tc_enable && on_vanc_line && at_h_start && !vid_de_in && !inserting) begin | ||
| // Latch timecode at insertion start — consistent packet contents | ||
| lat_frames_u <= tc_frames_u; | ||
| lat_frames_t <= tc_frames_t; | ||
| lat_seconds_u <= tc_seconds_u; | ||
| lat_seconds_t <= tc_seconds_t; | ||
| lat_minutes_u <= tc_minutes_u; | ||
| lat_minutes_t <= tc_minutes_t; | ||
| lat_hours_u <= tc_hours_u; | ||
| lat_hours_t <= tc_hours_t; | ||
| // Start VANC insertion | ||
| inserting <= 1'b1; | ||
| pkt_idx <= 5'd0; | ||
| end else if (inserting) begin | ||
| if (vid_de_in) begin | ||
| // Active video started — abort insertion to avoid pixel corruption | ||
| inserting <= 1'b0; | ||
| pkt_idx <= 5'd0; | ||
| end else if (pkt_idx == PKT_LEN - 1) begin | ||
| inserting <= 1'b0; | ||
| pkt_idx <= 5'd0; | ||
| end else begin | ||
| pkt_idx <= pkt_idx + 5'd1; | ||
| end | ||
| end | ||
| end | ||
| end | ||
|
|
||
| // -------------------------------------------------------------------------- | ||
| // Video output mux — insert VANC or pass through | ||
| // -------------------------------------------------------------------------- | ||
| always @(posedge clk or negedge rst_n) begin | ||
| if (!rst_n) begin | ||
| vid_data_out <= 8'd0; | ||
| vid_hsync_out <= 1'b0; | ||
| vid_vsync_out <= 1'b0; | ||
| vid_de_out <= 1'b0; | ||
| end else begin | ||
| vid_hsync_out <= vid_hsync_in; | ||
| vid_vsync_out <= vid_vsync_in; | ||
| vid_de_out <= vid_de_in; | ||
|
|
||
| if (inserting) begin | ||
| vid_data_out <= pkt_rom[pkt_idx]; | ||
| end else begin | ||
| vid_data_out <= vid_data_in; | ||
| end | ||
| end | ||
| end | ||
|
|
||
| endmodule |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.