Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions fpga_vanc_timecode/README.md
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).

Comment thread
alfskaar marked this conversation as resolved.
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
245 changes: 245 additions & 0 deletions fpga_vanc_timecode/vanc_timecode_inserter.v
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