# Demo
A camera streaming demo running on RVComp. Source code is available in the [`feature/v1.1.0-demo`](https://github.com/archlab-sciencetokyo/RVComp/tree/feature/v1.1.0-demo) branch of the [RVComp](https://github.com/archlab-sciencetokyo/RVComp) repository.
## Overview
This demo uses an [OV7670 (OmniVision)](https://www.ovt.com/press-releases/omnivision-launches-seventh-generation-vga-camerachip-for-mobile-applications/) camera module connected to a Nexys 4 DDR board. The camera captures 320×240 frames, converts them to 8-bit grayscale on the FPGA, and transmits them to a host PC over Ethernet. The demo runs on a local network (LAN).
```
OV7670 → FPGA (Gray8 capture) → Linux (cam_tx, UDP) → Host (relay.py, WebSocket) → Browser
```
## Demo Video
The demo is filmed in a bright indoor environment. Because frames are encoded as 8-bit grayscale, good ambient lighting improves image quality.
```{raw} html
```
## Running the Demo
### On RVComp (Linux)
After booting Linux, assign an IP address and bring up the Ethernet interface:
```sh
$ ip addr add / dev eth0
$ ip link set eth0 up
```
Then start the camera transmitter:
```sh
$ cam_tx --host
```
```
Usage: cam_tx [--dev PATH] [--host IP] [--port N] [--payload N]
Defaults: --dev /dev/rvcomp_cam0 --host 192.168.0.2 --port 5000 --payload 1200
```
### On the Host PC
Run `relay.py` from `tools/camera/` using `uv`:
```sh
$ cd tools/camera
$ uv run relay.py --port 5000 --ws-port 8000
```
Then open `http://localhost:8000` in a browser to view the live camera stream.
```
usage: relay.py [-h] [--udp-host UDP_HOST] [--udp-port UDP_PORT] [--ws-host WS_HOST] [--ws-port WS_PORT]
RVComp camera relay
options:
-h, --help show this help message and exit
--udp-host UDP_HOST UDP bind host
--udp-port UDP_PORT UDP bind port
--ws-host WS_HOST HTTP/WS bind host
--ws-port WS_PORT HTTP/WS bind port
```
## System Configuration
### External Pins
The OV7670 is connected to the PMOD JA and JB headers on the Nexys 4 DDR board.
| Signal | Direction | Description |
|:------------------|:----------|:------------|
| `xclk` | Output | Camera input clock (25 MHz) |
| `pclk` | Input | Pixel clock |
| `camera_h_ref` | Input | Horizontal sync |
| `camera_v_sync` | Input | Vertical sync |
| `din[7:0]` | Input | 8-bit pixel data |
| `sioc` | Output | I2C clock (SCCB) |
| `siod` | Inout | I2C data (SCCB) |
### Camera Driver
An FPGA block captures frames from the OV7670 via I2C (SCCB). Pixel data is converted to 8-bit grayscale and stored in two frame buffers in BRAM (ping-pong). 8-bit grayscale was chosen because fitting two full-resolution color frame buffers in BRAM would exceed the available capacity.
A Linux misc driver (`rvcomp-camera`) exposes the device as `/dev/rvcomp_cam0`. The driver accesses the camera hardware via MMIO polling (no IRQ). It supports `read`, `poll`, and `ioctl` (`RVCOMP_CAM_IOC_G_INFO`, `RVCOMP_CAM_IOC_G_META`).
#### MMIO Map
| Region | Start | End | Description |
|:---------------|:-------------:|:-------------:|:------------|
| CSR | `0xB000_0000` | `0xB000_0FFF` | Control and status registers |
| Frame aperture | `0xB001_0000` | `0xB002_FFFF` | Ping-pong frame buffer window |
#### CSR Map
All CSRs are 32-bit wide.
| Offset | Name | Access | Description |
|:------:|:-------------------|:------:|:------------|
| 0x00 | `CAM_REG_ID` | R | Device ID |
| 0x04 | `CAM_REG_CTRL` | R/W | Control (bit 0: capture enable) |
| 0x08 | `CAM_REG_STATUS` | R | Status |
| 0x0C | `CAM_REG_WIDTH` | R | Image width in pixels |
| 0x10 | `CAM_REG_HEIGHT` | R | Image height in pixels |
| 0x14 | `CAM_REG_STRIDE` | R | Row stride in bytes |
| 0x18 | `CAM_REG_FRAME_BYTES` | R | Total frame size in bytes |
| 0x1C | `CAM_REG_SEQ` | R | Frame sequence counter |
| 0x20 | `CAM_REG_READY_BANK` | R | Bank holding the latest complete frame |
| 0x24 | `CAM_REG_READ_BANK` | R/W | Bank selected for readout |
| 0x28 | `CAM_REG_DROP_COUNT` | R | Number of dropped frames |
| 0x2C | `CAM_REG_GAIN` | R/W | Gain control |
### Transmission Program (`cam_tx`)
`cam_tx` is a userspace program that reads frames from `/dev/rvcomp_cam0` and transmits them to the host as UDP datagrams. Each frame is split into chunks with a custom header (`RVCP` magic, sequence number, width, height, chunk index/count) to allow reassembly on the host.
### Client Program (`relay.py`)
`relay.py` runs on the host and acts as a relay between `cam_tx` and the browser. It listens for UDP datagrams from `cam_tx`, reassembles the frame chunks, and broadcasts each completed frame to all connected WebSocket clients. The browser connects to the WebSocket endpoint (`/ws`) and renders each frame on an HTML5 Canvas element.
## Going Further
The current implementation uses CPU-driven (PIO) reads from the BRAM frame buffer and UDP transmission, which limits the achievable frame rate.
Since this runs on an FPGA, the hardware is fully reconfigurable. For example:
- **DMA**: offload frame readout from the CPU entirely
- **FPGA-side Ethernet framing**: generate UDP/IP headers in hardware and push frames to the MAC without CPU involvement
- **Pipeline**: overlap capture and transmission in the FPGA fabric
Changes that would require a full chip respin on an ASIC can be explored here simply by modifying the RTL — feel free to experiment.