-
Notifications
You must be signed in to change notification settings - Fork 1
driver development summary 26 1
Platform: Agilex 5 SoC (Arrow AXE5-Falcon)
Kernel: Linux 6.12 — 6.18+
IP Core: Altera VVP Suite Video Frame Reader (VFR)
Pipeline: VFR → CVO → FR2CV → TFP410PAP → HDMI → 1920×1080p60
- Linux Display Architecture — DRM/KMS and CMA
- Objective
- Hardware Pipeline
- What We Built
- How the Driver Works
- Memory and Cache Coherency
- CVO Initialisation in Lite Mode
- No-Vblank Design
- Clock Domains
- Pixel Format Journey
- Key Register Values
- FPGA Configuration
- Driver Source Files
- Device Tree
- Kernel Configuration
Linux provides two display driver frameworks:
Legacy fbdev — the original Linux framebuffer interface (/dev/fb0). Simple but limited — no synchronisation, no hardware abstraction, no compositing support. Still used today by the kernel console (fbcon).
DRM/KMS (Direct Rendering Manager / Kernel Mode Setting) — the modern Linux display framework introduced to replace fbdev. All modern user-space display stacks — Wayland, X11, console — use DRM/KMS.
DRM/KMS provides:
- Atomic modesetting — all display parameters change in a single tear-free hardware commit
- GEM buffer management — standardised GPU memory allocation and sharing
- Hardware abstraction — connector/encoder/CRTC object model maps cleanly to physical hardware
- Page flip events — precise timestamps for smooth animation and compositor synchronisation
-
fbdev emulation — legacy
/dev/fb0compatibility layer on top of DRM
For the VFR driver, DRM/KMS was chosen because it is the standard modern interface. Even though the VFR has no GPU and no 2D/3D acceleration, the DRM framework correctly models the display pipeline and provides the fbcon integration needed to show the Linux console on the HDMI display.
DRM models display hardware as a hierarchy of abstract objects:
drm_device — top-level device, owns all objects
│
├── drm_plane — a scanout source (framebuffer backing a display layer)
│ └── drm_framebuffer — pixel format + geometry metadata
│ └── drm_gem_dma_object — actual memory allocation (CMA)
│
├── drm_crtc — display controller / timing engine
│ └── drm_crtc_state — atomic state (mode, active, no_vblank...)
│
├── drm_encoder — signal format converter (LVDS, TMDS, parallel RGB...)
│
└── drm_connector — physical output connector (HDMI, DisplayPort, DSI...)
└── drm_display_mode — timing parameters (H/V sync, pixel clock...)
How these map to the VFR hardware:
| DRM Object | VFR Hardware Equivalent |
|---|---|
drm_plane |
The CMA buffer that BASE_ADDR points to |
drm_crtc |
The VFR core itself — the DMA engine and timing control |
drm_encoder |
Pass-through — HDMI framing handled downstream by TFP410 |
drm_connector |
The HDMI output — always connected, fixed 1080p60 mode |
Modern DRM uses an atomic API. All display state changes are bundled into a drm_atomic_state object and committed in a single operation. The hardware either gets the full new configuration or nothing — no partial updates visible on screen.
The atomic commit path for this driver:
User-space or fbcon requests display update
│
▼
drm_atomic_helper_check() — validates state against hardware constraints
│
▼
altera_vfr_commit_tail() — custom non-blocking commit
│
├── commit_modeset_disables()
├── commit_planes() ──────────────► altera_vfr_plane_atomic_update()
│ └── vfr_program_bufset() or
│ vfr_update_base_addr()
├── commit_modeset_enables()
├── commit_hw_done()
└── cleanup_planes()
The driver installs fbdev emulation via drm_client_setup_with_fourcc() (kernel 6.17+) or drm_fbdev_dma_setup() (kernel 6.12–6.16). This creates a /dev/fb0 device backed by a DRM GEM buffer. The kernel console (fbcon) binds to /dev/fb0 and renders text characters as pixels. The VFR then DMA-reads those pixels continuously at 60 Hz and streams them to the display.
fbcon writes text characters as pixels
│ writes to /dev/fb0 virtual mapping
▼
CMA buffer in DDR (0xFAE00000)
│ VFR AXI DMA reads at 60 Hz
▼
HDMI display shows Linux console
This is why the Linux boot messages and login prompt appear on the HDMI display without any user-space display manager running.
The Contiguous Memory Allocator (CMA) is a Linux kernel subsystem that reserves a pool of physically contiguous memory at boot time. It exists to serve devices that cannot use scatter-gather DMA — devices whose hardware requires a flat, unbroken physical address range.
The VFR is exactly such a device. Its AXI DMA master takes a single BASE_ADDR register value and reads sequentially from BASE_ADDR to BASE_ADDR + (stride × height). There is no scatter-gather address table — the buffer must be contiguous in physical memory.
The CMA pool is reserved in the device tree:
reserved-memory {
cma: linux,cma {
size = <0x0 0x02000000>; /* 32 MiB */
reusable;
linux,cma-default;
};
};
At boot Linux confirms:
cma: Reserved 32 MiB at 0x00000000faa00000 on node -1
This reserves 0xFAA00000–0xFC9FFFFF (32 MB) as a contiguous pool. The framebuffer allocation lands at 0xFAE00000 within this pool.
When drm_client_setup_with_fourcc() (or drm_fbdev_dma_setup() on older kernels) is called, the DRM GEM DMA helper allocates from the CMA pool:
/* Inside drm_gem_dma_create() */
obj->vaddr = dma_alloc_attrs(dev, size, &obj->dma_addr,
GFP_KERNEL | __GFP_NOWARN, attrs);This returns:
-
obj->dma_addr=0xFAE00000— physical address (written to VFRBASE_ADDR) -
obj->vaddr= kernel virtual address (used by CPU to write pixels)
Width: 1920 pixels
Height: 1080 pixels
Format: XRGB8888 (4 bytes per pixel)
Stride: 1920 × 4 = 7680 bytes per line
Total: 7680 × 1080 = 8,294,400 bytes ≈ 7.9 MiB
This fits comfortably within the 32 MiB CMA reservation.
Without CMA the Linux page allocator would allocate the framebuffer as scattered 4 KB pages at arbitrary physical addresses. The VFR cannot follow a scatter-gather list — it would read garbage data from whatever happens to be at consecutive physical addresses after the first page.
With CMA the 8 MB framebuffer occupies a single flat physical address range:
0xFAE00000 ←── VFR BASE_ADDR
0xFAE00000 + 8,294,400 = 0xFAFE8000 ←── end of framebuffer
The VFR reads from 0xFAE00000 to 0xFAFE8000 in one continuous sweep per frame, then loops back and repeats at 60 Hz indefinitely.
The driver sets a 32-bit DMA mask to ensure CMA allocations fall within the VFR's addressable range:
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));This tells the kernel the VFR AXI master can only address 0x00000000–0xFFFFFFFF. All CMA allocations for this device will be within that range. Since the Agilex 5 HPS has 2 GB of DDR at 0x80000000–0xFFFFFFFF, all allocations naturally fall within the 32-bit address space.
After boot the DRM debugfs interface shows the exact allocation:
cat /sys/kernel/debug/dri/0/framebufferOutput:
framebuffer[37]:
allocated by = [fbcon]
format=XR24 little-endian (0x34325258)
size=1920x1080
pitch[0]=7680
dma_addr=0x00000000fae00000 ← physical address written to VFR BASE_ADDR
vaddr=... ← kernel virtual address used by CPU
The dma_addr value matches the VFR LAST_BUF_READ register (devmem2 0x20008144 = 0xFAE00000), confirming the VFR is actively DMA-reading the correct Linux framebuffer.
The goal was to write a Linux DRM/KMS kernel driver that makes the Altera VVP Video Frame Reader (VFR) IP core appear as a standard Linux display device. This allows:
- The kernel framebuffer console (
fbcon) to render the Linux text terminal on the HDMI display - Legacy applications to write pixels via
/dev/fb0 - Future Wayland compositors to use the display via the standard DRM API
The VFR is an FPGA peripheral that performs DMA reads from a Linux-allocated framebuffer in HPS DDR memory and streams the pixel data to a downstream video pipeline ending at an HDMI transmitter.
┌─────────────────────────────────────────────────────────────────┐
│ HPS DDR @ 0xFAE00000 (CMA framebuffer, 8 MB) │
└──────────────────────────┬──────────────────────────────────────┘
│ F2SDRAM AXI bridge
│ 256-bit read master, burst=16
▼
┌─────────────────────────────────────────────────────────────────┐
│ VFR — Video Frame Reader (0x20008000) │
│ Lite mode · 4 colour planes · Pixel Packing · 1 pix/clk │
│ vid_clock: 200 MHz │
└──────────────────────────┬──────────────────────────────────────┘
│ AXI4-S Lite · 32-bit tdata · 200 MHz
▼
┌─────────────────────────────────────────────────────────────────┐
│ CVO — Clocked Video Output (0x20006000) │
│ Lite mode · 4 colour planes · VID_IS_ASYNC=1 │
│ vid_clock: 200 MHz · fr_clock: 148.5 MHz │
│ Internal FIFO depth: 4096 entries │
└──────────────────────────┬──────────────────────────────────────┘
│ AXI4-S full-raster · 148.5 MHz
▼
┌─────────────────────────────────────────────────────────────────┐
│ FR2CV — Full-Raster to Clocked Video Converter │
│ vid_clock: 148.5 MHz │
└──────────────────────────┬──────────────────────────────────────┘
│ 24-bit parallel RGB · DATA[23:0]
│ HSYNC · VSYNC · DE
▼
┌─────────────────────────────────────────────────────────────────┐
│ TFP410PAP — HDMI Transmitter (I2C 0x38, reg 0x08 = 0xBF) │
│ 24-bit RGB input · TMDS encoding │
└──────────────────────────┬──────────────────────────────────────┘
│ HDMI
▼
Dell Monitor 1920×1080@60Hz
| Domain | Frequency | Drives |
|---|---|---|
vid_clock |
200 MHz | VFR, CVO data path, TPG |
fr_clock |
148.5 MHz | CVO VTG, FR2CV, TFP410 pixel clock |
Running vid_clock faster than fr_clock provides headroom for DDR latency. The CVO VID_IS_ASYNC=1 parameter enables the internal CDC FIFO to bridge the two clock domains safely.
The Agilex 5 HPS maps 2 GB of DDR at 0x80000000–0xFFFFFFFF. The relevant address regions for this driver are:
Physical DDR address space (2 GB):
0x80000000 ┌─────────────────────────┐
│ ATF / TF-A secure │ 32 MB (no-map reserved)
0x82000000 ├─────────────────────────┤
│ Linux kernel + heap │
│ Device drivers │
│ Page tables │
│ ... │
0xFAA00000 ├─────────────────────────┤
│ CMA reserved region │ 32 MB
│ (contiguous DMA pool) │
0xFAE00000 │ ┌───────────────────┐ │ ← VFR BASE_ADDR (confirmed)
│ │ DRM framebuffer │ │
│ │ 1920×1080×4 │ │
│ │ = 8,294,400 B │ │
0xFAFE8000 │ └───────────────────┘ │
│ (remainder of CMA) │
0xFCA00000 └─────────────────────────┘
│ SWIOTLB bounce buf │ 2 MB
0xFFFFFFFF └─────────────────────────┘
The VFR MMIO register block sits in the HPS-to-FPGA Lightweight AXI bridge window:
0x20000000 ┌─────────────────────────┐ H2F Lightweight AXI bridge base
│ ...other peripherals...│
0x20006000 ├─────────────────────────┤ CVO registers (0x200 bytes)
0x20008000 ├─────────────────────────┤ VFR registers (0x400 bytes)
│ ... │
0x2FFFFFFF └─────────────────────────┘ H2F Lightweight AXI bridge end
The CMA framebuffer at 0xFAE00000 is within the 32-bit address space (DMA_BIT_MASK(32)) accessible to the VFR AXI master via the F2SDRAM bridge.
A Linux DRM/KMS platform driver consisting of:
| File | Purpose |
|---|---|
altera_vfr_drm.c |
Main driver: probe, KMS objects, hardware control |
altera_vfr_drm.h |
Private struct and container-of helpers |
altera_vfr_regs.h |
VFR and CVO register byte offsets |
Kconfig |
Kernel config entry |
Makefile |
Kbuild rules |
altr,vfr-drm.yaml |
Device tree binding schema |
The driver creates the following DRM objects:
drm_device
├── drm_plane — primary plane backed by CMA GEM buffer
├── drm_crtc — represents the VFR output pipeline (no_vblank=true)
├── drm_encoder — pass-through TMDS encoder
└── drm_connector — always-connected HDMI-A, fixed 1080p60 mode
When the kernel matches compatible = "altr,vfr-drm-1.0":
- Allocate
drm_device+altera_vfrprivate state viadevm_drm_dev_alloc() - Map VFR MMIO registers from DT
reg[0](0x20008000) - Read
VID_PIDregister — abort with-ENODEVif not0x6AF7024A - Map CVO MMIO registers from DT
reg[1](0x20006000) - Stop VFR (
RUN = STOP) - Call
altera_vfr_cvo_init()— program CVO IMG_INFO registers - Initialise DRM KMS objects (plane, CRTC, encoder, connector)
- Call
drm_mode_config_reset()— setsno_vblank=trueon CRTC state - Register DRM device — creates
/dev/dri/card0 - Install fbdev emulation — allocates CMA buffer, creates
/dev/fb0 - Initial atomic commit triggers
vfr_program_bufset()— VFR starts
First enable — vfr_program_bufset():
RUN = STOP
BUFSET[0].BASE_ADDR = CMA physical address (0xFAE00000)
BUFSET[0].NUM_BUFFERS = 1
BUFSET[0].INTER_LINE_OFFS = 7680 (stride = 1920 × 4 bytes)
BUFSET[0].WIDTH = 1920
BUFSET[0].HEIGHT = 1080
BUFSET[0].INTERLACE = 0 (progressive)
BUFSET[0].COLORSPACE = 0 (RGB)
BUFSET[0].SUBSAMPLING = 0 (4:4:4)
BUFSET[0].BPS = 8
BUFSET[0].FIELD_COUNT = 0 (infinite/continuous)
NUM_BUFFER_SETS = 1
BUFFER_MODE = SINGLE_SET
STARTING_BUFSET = 0
COMMIT = 1
RUN = FREE_RUNNING
Page flip — vfr_update_base_addr():
BUFSET[0].BASE_ADDR = new physical address
COMMIT = 1
The ARM Cortex-A55 CPU writes framebuffer pixels into its L1/L2 cache. The VFR AXI DMA master reads directly from DDR via the F2SDRAM bridge, bypassing the CPU cache. Without intervention the VFR reads stale DDR data while current pixel data sits in the CPU cache — resulting in white noise or a frozen image on the display.
Connect the VFR AXI master to the F2SDRAM bridge and mark the framebuffer allocation as non-cacheable via dma-noncoherent in the device tree:
display-controller@20008000 {
compatible = "altr,vfr-drm-1.0";
reg = <0x20008000 0x400>,
<0x20006000 0x200>;
dma-noncoherent;
};
dma-noncoherent tells the Linux DMA framework to allocate the CMA framebuffer as non-cacheable memory. CPU writes bypass the cache and go directly to DDR via the write-combining buffer:
CPU write → write-combining buffer (128-256 bytes) → DDR
VFR reads DDR directly via F2SDRAM bridge → always sees current data
The framebuffer is 8 MB — approximately 4× the size of the Agilex 5 L3 cache (2 MB). Any substantial screen update — scrolling text, redrawing the console, updating large areas — will thrash the L3 cache completely. The framebuffer will push out whatever working data the CPU had cached and replace it with pixel data that the CPU writes once and never reads back. This is the worst possible cache usage pattern.
For applications where the CPU's primary job is not painting graphics — signal processing, control systems, networking, data acquisition — the display is a convenience peripheral. There is no benefit to caching the framebuffer. Non-cacheable write-combining is the correct and efficient choice:
- No cache pollution — the L3 cache remains fully available for real workloads
- No coherency overhead — cacheable DMA transactions require the cluster snoop unit to check for cache hits on every transaction, adding latency and consuming bandwidth. Non-cacheable F2SDRAM reads have no such overhead
- Write-combining efficiency — the write-combining buffer coalesces CPU pixel writes into 64-byte DDR bursts, well matched to the VFR's 32-byte AXI burst size
- Write posting — CPU writes are still posted via the store buffer and do not stall. The write-combining buffer drains to DDR in nanoseconds — far faster than the 16.7ms frame period
A dma_sync_single_for_device() call was added in atomic_update as a one-time cache flush at the moment the VFR is first programmed. This ensures any CPU-cached framebuffer content written before dma-noncoherent took full effect is flushed to DDR before the VFR starts reading:
dma_sync_single_for_device(vfr->dev, paddr,
VFR_FIXED_STRIDE * VFR_FIXED_HEIGHT,
DMA_TO_DEVICE);This is a belt-and-suspenders measure for the initial commit only — once dma-noncoherent is active all subsequent CPU writes go directly to DDR and no explicit flush is needed.
In Lite mode the VFR outputs a simplified AXI4-S stream with no embedded metadata packets. The CVO receives raw pixel data but has no way to determine frame dimensions from the stream itself.
The CVO must be explicitly told the frame width and height via its IMG_INFO registers. Without this:
-
IMG_INFO_WIDTH = 0at power-on - CVO does not know when to assert end-of-line
-
VID_SIZE_ERRfires continuously - Display is garbled or blank
In Full mode the VFR embeds image information packets in the stream containing width, height, colorspace etc. The CVO reads these automatically — no software programming needed.
Called at probe before the VFR is started:
iowrite32(VFR_FIXED_WIDTH, cvo_regs + CVO_REG_IMG_INFO_WIDTH); /* 1920 */
iowrite32(VFR_FIXED_HEIGHT, cvo_regs + CVO_REG_IMG_INFO_HEIGHT); /* 1080 */
iowrite32(0, cvo_regs + CVO_REG_IMG_INFO_INTERLACE); /* progressive */
iowrite32(0, cvo_regs + CVO_REG_IMG_INFO_COLORSPACE);/* RGB */
iowrite32(0, cvo_regs + CVO_REG_IMG_INFO_SUBSAMPLING);/* 4:4:4 */
iowrite32(VFR_FIXED_BPS, cvo_regs + CVO_REG_IMG_INFO_BPS); /* 8 */
iowrite32(CVO_FALLBACK_FORCE_VID, cvo_regs + CVO_REG_FALLBACK); /* force VFR input */Note: The CVO has no COMMIT register — IMG_INFO writes take effect immediately. 0x0190 is REG_TPG_SIZE_ERR_COUNT (read-only diagnostic), not a commit register.
| Offset | Register | Access |
|---|---|---|
| 0x0120 | IMG_INFO_WIDTH | Write-only in Lite mode |
| 0x0124 | IMG_INFO_HEIGHT | Write-only in Lite mode |
| 0x0128 | IMG_INFO_INTERLACE | Write-only in Lite mode |
| 0x012C | IMG_INFO_COLORSPACE | Write-only in Lite mode |
| 0x0130 | IMG_INFO_SUBSAMPLING | Write-only in Lite mode |
| 0x0138 | IMG_INFO_BPS | Write-only in Lite mode |
| 0x0140 | REG_STATUS | Read-only |
| 0x0154 | REG_FALLBACK | Read/Write |
The VFR in Lite mode does not have an accessible IRQ block. The standard DRM commit tail (drm_atomic_helper_commit_tail) calls drm_atomic_helper_wait_for_vblanks() which blocks for 10 seconds waiting for a hardware vblank interrupt that never arrives.
A custom CRTC reset function sets no_vblank=true on the CRTC state:
static void altera_vfr_crtc_reset(struct drm_crtc *crtc)
{
state = kzalloc(sizeof(*state), GFP_KERNEL);
__drm_atomic_helper_crtc_reset(crtc, state);
state->no_vblank = true; /* no hardware vblank available */
}Flip events are delivered immediately in atomic_flush under the event spinlock:
spin_lock_irqsave(&crtc->dev->event_lock, flags);
drm_crtc_send_vblank_event(crtc, new_state->event);
spin_unlock_irqrestore(&crtc->dev->event_lock, flags);A custom non-blocking commit tail avoids all vblank waits:
static void altera_vfr_commit_tail(struct drm_atomic_state *state)
{
drm_atomic_helper_commit_modeset_disables(state->dev, state);
drm_atomic_helper_commit_planes(state->dev, state, 0);
drm_atomic_helper_commit_modeset_enables(state->dev, state);
drm_atomic_helper_commit_hw_done(state);
drm_atomic_helper_cleanup_planes(state->dev, state);
}Three clock domains exist in the VVP pipeline:
| Clock | Frequency | Components |
|---|---|---|
cpu_clock / vid_clock
|
200 MHz | VFR, CVO register interface, CVO data path, TPG |
fr_clock |
148.5 MHz | CVO VTG, CVO full-raster output, FR2CV, TFP410 |
| HPS AXI clock | 400 MHz | VFR AXI read master (internal to VFR, CDC inside) |
Running vid_clock (200 MHz) faster than fr_clock (148.5 MHz) gives 35% headroom for DDR latency spikes. The CVO VID_IS_ASYNC=1 compile-time parameter enables the internal CDC FIFO between the 200 MHz and 148.5 MHz domains.
With vid_clock = fr_clock = 148.5 MHz (same clock) the CVO FIFO has zero headroom — any DDR latency stalls the VFR, drains the FIFO, and causes VID_STALL_ERR. This was the root cause of the stall errors observed during development.
The driver uses XRGB8888 — 32 bits per pixel, 4 bytes per pixel in memory.
XRGB8888 divides evenly into every standard AXI bus width:
256-bit (32 bytes) ÷ 4 bytes = 8 pixels ✓ perfect alignment
128-bit (16 bytes) ÷ 4 bytes = 4 pixels ✓ perfect alignment
This ensures the VFR AXI read master always starts and ends each burst on a pixel boundary — no fractional pixels, no byte misalignment.
The VFR is configured with:
- 4 colour planes (X, R, G, B)
- Pixel Packing mode
Pixel Packing rounds each pixel up to a 32-bit word boundary. With 4 planes × 8 BPS = 32 bits, each XRGB8888 pixel occupies exactly one 32-bit word. The VFR reads 4 bytes from DDR and outputs 32-bit tdata to the AXI4-S stream.
The CVO is also configured with 4 colour planes. The FR2CV outputs 32-bit data to the TFP410. The TFP410 only uses DATA[23:0] — the X byte on DATA[31:24] is physically unconnected on the PCB and harmlessly ignored.
XRGB8888 in DDR (little-endian):
Byte +0: Blue (B)
Byte +1: Green (G)
Byte +2: Red (R)
Byte +3: X=0 (padding, ignored by TFP410)
| Address | Register | Value | Meaning |
|---|---|---|---|
| 0x20008000 | VID_PID | 0x6AF7024A | IP confirmed ✓ |
| 0x20008004 | VERSION | 0x18070001 | major=24, update=7, regmap=1 |
| 0x20008008 | LITE_MODE | 0x00000001 | Lite mode active |
| 0x20008020 | COLOR_PLANES | 0x00000004 | 4 planes (XRGB) |
| 0x20008024 | PIXELS_IN_PARALLEL | 0x00000001 | 1 pixel/clock |
| 0x20008028 | PACKING | 0x00000002 | Pixel Packing |
| 0x20008140 | STATUS | 0x00000001 | Running ✓ |
| 0x20008144 | LAST_BUF_READ | 0xFAE00000 | DMA active ✓ |
| 0x200081A0 | RUN | 0x00000001 | FREE_RUNNING |
| 0x200081B0 | BUFSET[0].BASE_ADDR | 0xFAE00000 | Framebuffer ✓ |
| 0x200081BC | BUFSET[0].STRIDE | 0x00001E00 | 7680 bytes ✓ |
| 0x200081C0 | BUFSET[0].WIDTH | 0x00000780 | 1920 pixels |
| 0x200081C4 | BUFSET[0].HEIGHT | 0x00000438 | 1080 lines ✓ |
| Address | Register | Value | Meaning |
|---|---|---|---|
| 0x20006000 | VID_PID | 0x6AF70244 | CVO confirmed ✓ |
| 0x20006008 | VID_FIFO_DEPTH | 0x00001000 | 4096 entries ✓ |
| 0x20006020 | COLOR_PLANES | 0x00000004 | 4 planes ✓ |
| 0x2000601C | PIXELS_IN_PARALLEL | 0x00000001 | 1 pixel/clock ✓ |
| 0x20006028 | CPU_CLK_FREQ | 0x0BEBC200 | 200 MHz ✓ |
| 0x20006030 | VID_IS_ASYNC | 0x00000001 | Async mode ✓ |
| 0x20006140 | REG_STATUS | 0x00000041 | Locked, no errors ✓ |
| 0x20006154 | REG_FALLBACK | 0x00000008 | Force VFR input ✓ |
| 0x20006250 | REG_TOTALS | 0x04650898 | 2200×1125 total raster ✓ |
| Parameter | Value |
|---|---|
| Lite mode | Enabled |
| Number of colour planes | 4 |
| Bits per symbol | 8 |
| Pixels in parallel | 1 |
| Packing | Pixel |
| AXI master data width | 256-bit |
| Read burst target | 16 |
| Always burst max burst | true |
| Burst on burst boundaries | true |
| Max pending read transactions | 8 |
| Parameter | Value |
|---|---|
| Lite mode | Enabled |
| Number of colour planes | 4 |
| Pixels in parallel | 1 |
| VID_IS_ASYNC | Enabled |
| Input FIFO depth | 4096 |
| vid_clock | 200 MHz |
| fr_clock | 148.5 MHz |
#define VFR_PIXELS_IN_PARALLEL 1
#define VFR_FIXED_WIDTH 1920
#define VFR_FIXED_HEIGHT 1080
#define VFR_FIXED_STRIDE (VFR_FIXED_WIDTH * 4) /* 7680 */
#define VFR_FIXED_VFR_WIDTH VFR_FIXED_WIDTH /* 1920 */
#define VFR_FIXED_BPS 8
#define VFR_VID_PID_EXPECTED 0x6AF7024A
/* CVO registers */
#define CVO_REG_IMG_INFO_WIDTH 0x0120
#define CVO_REG_IMG_INFO_HEIGHT 0x0124
#define CVO_REG_IMG_INFO_INTERLACE 0x0128
#define CVO_REG_IMG_INFO_COLORSPACE 0x012C
#define CVO_REG_IMG_INFO_SUBSAMPLING 0x0130
#define CVO_REG_IMG_INFO_BPS 0x0138
#define CVO_REG_STATUS 0x0140
#define CVO_REG_FALLBACK 0x0154
#define CVO_FALLBACK_FORCE_VID 0x8struct altera_vfr {
struct drm_device drm; /* must be first */
struct device *dev;
void __iomem *regs; /* VFR MMIO */
void __iomem *cvo_regs; /* CVO MMIO */
bool running;
struct drm_plane primary;
struct drm_crtc crtc;
struct drm_encoder encoder;
struct drm_connector connector;
};| Function | Purpose |
|---|---|
vfr_program_bufset() |
Full VFR init on first atomic commit |
vfr_update_base_addr() |
Address-only update on page flip |
altera_vfr_cvo_init() |
Program CVO IMG_INFO registers at probe |
altera_vfr_plane_atomic_update() |
DRM plane callback — main hardware interface |
altera_vfr_crtc_reset() |
Custom reset — sets no_vblank=true |
altera_vfr_crtc_atomic_flush() |
Delivers flip events immediately |
altera_vfr_commit_tail() |
Custom non-blocking commit tail |
altera_vfr_connector_get_modes() |
Returns single fixed 1080p60 mode |
altera_vfr_probe() |
Full initialisation sequence |
display-controller@20008000 {
compatible = "altr,vfr-drm-1.0";
reg = <0x20008000 0x400>, /* VFR registers */
<0x20006000 0x200>; /* CVO registers */
dma-noncoherent;
};
Two reg entries are required:
- Index 0: VFR hardware register block
- Index 1: CVO hardware register block (needed for Lite mode IMG_INFO programming)
CONFIG_DRM=y
CONFIG_DRM_FBDEV_EMULATION=y
CONFIG_DRM_ALTERA_VFR=y
CONFIG_STRICT_DEVMEM=n
CONFIG_IO_STRICT_DEVMEM=n
config DRM_ALTERA_VFR
tristate "Altera VVP Video Frame Reader DRM driver"
depends on DRM && OF && HAS_IOMEM
select DRM_KMS_HELPER
select DRM_GEM_DMA_HELPER
select DRM_CLIENT_LIB
select DRM_CLIENT_SELECTION
select is used instead of depends on for helper symbols because other drivers in the base config (Rockchip, VC4, Meson etc.) select DRM_GEM_DMA_HELPER=m, which prevents it being promoted to =y via depends on. Using select forces =y when our driver is enabled. DRM_CLIENT_LIB and DRM_CLIENT_SELECTION are required from kernel 6.17+ for the new client-based fbdev setup.
# drivers/gpu/drm/Kconfig
source "drivers/gpu/drm/altera-vfr/Kconfig"
# drivers/gpu/drm/Makefile
obj-$(CONFIG_DRM_ALTERA_VFR) += altera-vfr/The Makefile line is the most common casualty of a kernel branch merge — if it is missing the driver directory is silently skipped by Kbuild and CONFIG_DRM_ALTERA_VFR will not appear in .config at all.
The driver has been ported across several kernel releases. The following table documents the changes required at each transition:
| Kernel | Change | Action |
|---|---|---|
| 6.12 | Baseline — CMA helpers renamed CMA→DMA | Use drm_gem_dma_helper.h, drm_fb_dma_helper.h, drm_fbdev_dma.h
|
| 6.12 |
platform_driver.remove returns void
|
Change signature accordingly |
| 6.12 |
devm_drm_dev_alloc() returns private struct |
No container_of needed |
| 6.17 |
.date field removed from drm_driver
|
Remove .date = "20240101"
|
| 6.17 |
drm_fbdev_dma_setup() removed |
Replace with drm_client_setup_with_fourcc(&vfr->drm, DRM_FORMAT_XRGB8888)
|
| 6.17 |
fbdev_probe callback required in drm_driver
|
Add .fbdev_probe = drm_fbdev_dma_driver_fbdev_probe
|
| 6.17 | New include needed | Add #include <drm/clients/drm_client_setup.h>
|
| 6.17 |
DRM_CLIENT_LIB=m causes linker error |
Add select DRM_CLIENT_LIB and select DRM_CLIENT_SELECTION to Kconfig |
| 6.18 |
mode_valid signature — const added |
Change struct drm_display_mode *mode → const struct drm_display_mode *mode
|
Document generated from development session — Agilex 5 AXE5-Falcon, kernel 6.12–6.18, Quartus 26.1