driver development summary 26 1 - ArrowElectronics/Agilex-5 GitHub Wiki

Altera VVP Video Frame Reader โ€” Linux DRM Driver

Development Summary

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


Table of Contents

  1. Linux Display Architecture โ€” DRM/KMS and CMA
  2. Objective
  3. Hardware Pipeline
  4. What We Built
  5. How the Driver Works
  6. Memory and Cache Coherency
  7. CVO Initialisation in Lite Mode
  8. No-Vblank Design
  9. Clock Domains
  10. Pixel Format Journey
  11. Key Register Values
  12. FPGA Configuration
  13. Driver Source Files
  14. Device Tree
  15. Kernel Configuration

1. Linux Display Architecture โ€” DRM/KMS and CMA

1.1 Why DRM/KMS?

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/fb0 compatibility 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.

1.2 DRM Object Model

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

1.3 Atomic Modesetting

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()

1.4 fbdev Emulation

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.


1.5 CMA โ€” Contiguous Memory Allocator

What is CMA?

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.

CMA Reservation

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.

CMA Allocation for the Framebuffer

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 VFR BASE_ADDR)
  • obj->vaddr = kernel virtual address (used by CPU to write pixels)

Framebuffer Size

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.

Why Contiguous Memory Matters for the VFR

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.

DMA Mask

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.

Verifying the CMA Allocation

After boot the DRM debugfs interface shows the exact allocation:

cat /sys/kernel/debug/dri/0/framebuffer

Output:

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.


2. Objective

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.


3. Hardware Pipeline

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  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

Clock Domains

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.

Hardware Address Boundaries

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

5. How the Driver Works

Probe Sequence

When the kernel matches compatible = "altr,vfr-drm-1.0":

  1. Allocate drm_device + altera_vfr private state via devm_drm_dev_alloc()
  2. Map VFR MMIO registers from DT reg[0] (0x20008000)
  3. Read VID_PID register โ€” abort with -ENODEV if not 0x6AF7024A
  4. Map CVO MMIO registers from DT reg[1] (0x20006000)
  5. Stop VFR (RUN = STOP)
  6. Call altera_vfr_cvo_init() โ€” program CVO IMG_INFO registers
  7. Initialise DRM KMS objects (plane, CRTC, encoder, connector)
  8. Call drm_mode_config_reset() โ€” sets no_vblank=true on CRTC state
  9. Register DRM device โ€” creates /dev/dri/card0
  10. Install fbdev emulation โ€” allocates CMA buffer, creates /dev/fb0
  11. Initial atomic commit triggers vfr_program_bufset() โ€” VFR starts

Hardware Programming

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

6. Memory and Cache Coherency

The Problem

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.

Solution โ€” F2SDRAM Bridge with Non-Cacheable Memory

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

Why Non-Cacheable is the Right Choice

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

dma_sync_single_for_device()

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.


7. CVO Initialisation in Lite Mode

Why It 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 = 0 at power-on
  • CVO does not know when to assert end-of-line
  • VID_SIZE_ERR fires 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.

altera_vfr_cvo_init()

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.

CVO Register Map (key offsets from UG-20344 Table 327, page 353)

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

8. No-Vblank Design

Why No Interrupt

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.

Solution

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);
}

9. Clock Domains

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.


10. Pixel Format

The driver uses XRGB8888 โ€” 32 bits per pixel, 4 bytes per pixel in memory.

Why XRGB8888

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.

Pixel Packing with 4 Colour Planes

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.

Memory Layout

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)

11. Key Register Values

VFR Registers (base 0x20008000)

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 โœ“

CVO Registers (base 0x20006000)

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 โœ“

12. FPGA Configuration

VFR Platform Designer Parameters

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

CVO Platform Designer Parameters

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

13. Driver Source Files

altera_vfr_regs.h โ€” Key Constants

#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       0x8

altera_vfr_drm.h โ€” Private Struct

struct 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;
};

Key Functions in altera_vfr_drm.c

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

14. Device Tree

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)

15. Kernel Configuration

Config Fragment

CONFIG_DRM=y
CONFIG_DRM_FBDEV_EMULATION=y
CONFIG_DRM_ALTERA_VFR=y
CONFIG_STRICT_DEVMEM=n
CONFIG_IO_STRICT_DEVMEM=n

Kconfig

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.

Kernel Build System Integration

# 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.

Kernel API Changes by Version

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