diff --git a/fpga_vanc_timecode/README.md b/fpga_vanc_timecode/README.md new file mode 100644 index 00000000..b88953e7 --- /dev/null +++ b/fpga_vanc_timecode/README.md @@ -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 diff --git a/fpga_vanc_timecode/vanc_timecode_inserter.v b/fpga_vanc_timecode/vanc_timecode_inserter.v new file mode 100644 index 00000000..d65760a6 --- /dev/null +++ b/fpga_vanc_timecode/vanc_timecode_inserter.v @@ -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