FPGA 摄像头采集处理显示指南:从 OV5640 到 HDMI 实时显示
概述
在当今的视频监控、工业检测、医疗成像等领域,实时图像采集和显示已成为必不可少的功能。FPGA 因其高并行处理能力和低延迟特性,成为实现高性能视频处理系统的首选方案。
本文将详细介绍如何使用 FPGA 实现一个完整的摄像头采集、处理、显示系统,重点讲解 OV5640 摄像头的驱动、图像数据采集、缓存管理以及 HDMI 显示输出的全流程。
基于 FPGA 的摄像头采集与显示系统设计方案。涵盖 OV5640 摄像头驱动、SCCB 配置、DVP 数据采集、图像缓存(SDRAM/DDR)、以及 HDMI 输出时序与 TMDS 编码。提供了完整的 Verilog 模块实现框架及调试技巧,适用于视频监控、工业检测等场景。
在当今的视频监控、工业检测、医疗成像等领域,实时图像采集和显示已成为必不可少的功能。FPGA 因其高并行处理能力和低延迟特性,成为实现高性能视频处理系统的首选方案。
本文将详细介绍如何使用 FPGA 实现一个完整的摄像头采集、处理、显示系统,重点讲解 OV5640 摄像头的驱动、图像数据采集、缓存管理以及 HDMI 显示输出的全流程。
一个典型的 FPGA 摄像头采集处理显示系统由以下几个主要部分组成:
┌───────────────────────────────────────┐
│ FPGA 摄像头采集处理显示系统架构 │
├───────────────────────────────────────┤
│ ├─ 1️⃣ 摄像头采集模块 │
│ │ ├─ OV5640 摄像头驱动 │
│ │ ├─ SCCB 配置接口 │
│ │ └─ DVP 数据采集 │
│ ├─ 2️⃣ 图像处理模块 │
│ │ ├─ 数据格式转换 │
│ │ ├─ 图像缩放/裁剪 │
│ │ └─ 色彩空间转换 │
│ ├─ 3️⃣ 存储缓存模块 │
│ │ ├─ SDRAM/DDR3 │
│ │ ├─ 双端口 RAM │
│ │ └─ 帧缓冲管理 │
│ ├─ 4️⃣ 显示输出模块 │
│ │ ├─ VGA 时序生成 │
│ │ ├─ HDMI 驱动 │
│ │ └─ TMDS 编码 │
│ └─ 5️⃣ 时钟管理模块 │
│ ├─ PLL 时钟生成 │
│ ├─ 多时钟域同步 │
│ └─ 时序约束 │
└───────────────────────────────────────┘
| 模块名称 | 功能描述 | 关键参数 |
|---|---|---|
| OV5640 驱动 | 摄像头初始化、寄存器配置、数据采集 | 分辨率、帧率、数据格式 |
| SCCB 控制器 | I2C 兼容的摄像头配置接口 | 时钟频率、地址宽度 |
| DVP 采集 | 并行数据采集、时序同步 | 像素时钟、行列同步 |
| 图像缓存 | 帧数据存储、读写管理 | 缓存大小、带宽 |
| VGA 驱动 | 显示时序生成、数据输出 | 分辨率、刷新率 |
| HDMI 驱动 | HDMI 信号编码、差分输出 | 分辨率、色深 |
摄像头 (OV5640) ↓ [DVP 接口:PCLK, HREF, VSYNC, Y[7:0]]
FPGA 采集模块 ↓ [16 位 RGB565 或 YUV422]
图像处理模块 ↓ [处理后的图像数据]
SDRAM/DDR3 缓存 ↓ [读取请求]
VGA/HDMI 驱动 ↓ [HDMI 差分信号]
显示器
1. 视频监控系统
特点:
FPGA 优势: ✓ 低延迟 (毫秒级) ✓ 支持并行处理 ✓ 灵活的数据流管理 ✓ 可扩展性强
2. 工业检测系统
特点:
FPGA 优势: ✓ 支持高帧率处理 ✓ 可实现复杂的图像算法 ✓ 多摄像头同步容易 ✓ 处理延迟可预测
3. 医疗成像系统
特点:
FPGA 优势: ✓ 可实现高质量图像处理 ✓ 支持多种数据格式 ✓ 可靠性高 ✓ 易于集成
| 分辨率 | 帧率 | 像素时钟 | 缓存大小 | 适用场景 |
|---|---|---|---|---|
| 640×480 | 60fps | 25MHz | 600KB | 低端监控、测试 |
| 800×600 | 60fps | 40MHz | 960KB | 通用应用 |
| 1024×768 | 60fps | 65MHz | 1.5MB | 工业检测 |
| 1280×720 | 60fps | 74.25MHz | 1.8MB | 高清监控 |
| 1920×1080 | 60fps | 148.5MHz | 4.1MB | 全高清应用 |
1️⃣ 需求分析
├─ 确定分辨率和帧率
├─ 选择摄像头型号
└─ 评估 FPGA 资源
2️⃣ 硬件设计
├─ 摄像头接口设计
├─ 电源管理设计
├─ 时钟分配设计
└─ 显示接口设计
3️⃣ 软件设计
├─ 摄像头驱动开发
├─ 图像采集模块
├─ 缓存管理
└─ 显示驱动
4️⃣ 集成与调试
├─ 模块集成
├─ 时序验证
├─ 功能测试
└─ 性能优化
5️⃣ 部署与维护
├─ 系统集成
├─ 可靠性测试
└─ 文档完善
1. 时钟管理
// 关键时钟信号
// - 摄像头输入时钟 (XCLK): 24MHz
// - 像素时钟 (PCLK): 根据分辨率变化
// - HDMI 像素时钟:148.5MHz(1080p@60Hz)
// - HDMI 串行时钟:5 倍像素时钟
// 时钟约束示例
create_clock -period 10.000 -name clk_sys [get_ports sys_clk]
create_generated_clock -name clk_hdmi \n -source [get_pins pll_inst/CLKIN1] \n -multiply_by 3 \n [get_pins pll_inst/CLKOUT0]
2. 跨时钟域同步
摄像头时钟域 (PCLK) ←→ 系统时钟域 (sys_clk)
↓ CDC 同步器 (双触发器)
↓ HDMI 时钟域 (clk_hdmi)
关键:使用格雷码或双触发器进行同步
3. 内存带宽管理
写入带宽:PCLK × 16bit (像素时钟 × 数据宽度)
读取带宽:clk_hdmi × 16bit (显示时钟 × 数据宽度)
例如 1080p@60Hz:
- 写入:148.5MHz × 16bit = 2.376Gbps
- 读取:148.5MHz × 16bit = 2.376Gbps
- 需要 DDR3-1600 以上支持
| 挑战 | 原因 | 解决方案 |
|---|---|---|
| 时序收敛困难 | 多时钟域、高频率 | 合理的时钟约束、流水线设计 |
| 缓存不足 | 高分辨率、高帧率 | 使用 DDR3、优化数据格式 |
| 图像撕裂 | 读写不同步 | 双缓冲、帧同步控制 |
| 色彩失真 | 格式转换错误 | 正确的色彩空间转换 |
| 延迟过高 | 处理流程复杂 | 流水线并行处理 |
设计建议:
OV5640 是豪威 (OmniVision) 公司推出的一款高性能 CMOS 图像传感器,广泛应用于监控、手机、平板等消费类电子产品。
主要特性:
| 特性 | 参数 |
|---|---|
| 最大分辨率 | 2592×1944(500 万像素) |
| 输出格式 | YUV422/420、RGB565、JPEG |
| 帧率 | 15-60fps(可配置) |
| 工作时钟 | 6-54MHz(推荐 24MHz) |
| 功耗 | 150-200mW |
| 接口 | DVP(并行)、SCCB(I2C 兼容) |
| 自动功能 | 自动对焦、自动曝光、自动白平衡 |
优势:
劣势:
OV5640 采用 CSP(芯片级封装),共有 60 个引脚。主要引脚如下:
┌─────────────────────────────────────┐
│ OV5640 CSP60 引脚分布 │
├─────────────────────────────────────┤
│ 电源引脚: │
│ DVDD(1.8V) - 数字电源 │
│ AVDD(2.8V) - 模拟电源 │
│ DOVDD(1.8V) - 输出驱动电源 │
│ │
│ 时钟引脚: │
│ XCLK - 外部时钟输入 (24MHz) │
│ PCLK - 像素时钟输出 │
│ │
│ 同步信号: │
│ VSYNC - 帧同步信号 (行场同步) │
│ HREF - 行同步信号 │
│ │
│ 数据引脚: │
│ Y[9:0] - 10 位像素数据 │
│ (通常使用 Y[9:2] 作为 8 位数据) │
│ │
│ 控制引脚: │
│ RESETB - 复位信号 (低有效) │
│ PWDN - 掉电/省电 (高有效) │
│ │
│ 通信引脚: │
│ SIO_C - SCCB 时钟线 │
│ SIO_D - SCCB 数据线 │
└─────────────────────────────────────┘
1. 电源引脚
DVDD (1.8V ±5%)
├─ 数字核心电源
├─ 需要 100nF+10μF 滤波
└─ 对噪声敏感,需要单独的电源层
AVDD (2.8V ±5%)
├─ 模拟电源
├─ 需要 100nF+10μF 滤波
└─ 必须与 DVDD 同时上电
DOVDD (1.8V ±5%)
├─ 输出驱动电源
├─ 驱动 Y[9:0]、PCLK、VSYNC、HREF
└─ 需要 100nF 滤波
2. 时钟引脚
XCLK (外部时钟输入)
├─ 频率范围:6-54MHz
├─ 推荐频率:24MHz
├─ 占空比:45%-55%
├─ 可来自晶振或 FPGA 输出
└─ 必须稳定,抖动< 100ps
PCLK (像素时钟输出)
├─ 频率:根据分辨率和帧率变化
├─ 例如:1280×720@60Hz 时约 74.25MHz
├─ 由 OV5640 内部 PLL 生成
├─ 用于同步像素数据采集
└─ 需要在 FPGA 中采样
3. 同步信号
VSYNC (帧同步信号)
├─ 低电平表示帧有效
├─ 用于帧边界检测
├─ 周期 = 1/帧率
└─ 例如:60fps 时周期为 16.67ms
HREF (行同步信号)
├─ 高电平表示行有效
├─ 用于行边界检测
├─ 周期 = 1/(帧率×行数)
└─ 例如:1280×720@60Hz 时约 13.9μs
4. 数据引脚
Y[9:0] (10 位像素数据)
├─ 通常使用 Y[9:2] 作为 8 位数据
├─ 数据格式:YUV422、RGB565 等
├─ 在 PCLK 上升沿采样
├─ 需要与 HREF、VSYNC 同步
└─ 输出电平:DOVDD(1.8V)
5. 控制引脚
RESETB (复位信号,低有效)
├─ 低电平时复位芯片
├─ 需要保持低电平至少 1ms
├─ 上升沿后需要等待 20ms 才能配置
└─ 通常由 FPGA GPIO 驱动
PWDN (掉电/省电,高有效)
├─ 高电平时进入低功耗模式
├─ 低电平时正常工作
├─ 需要在 RESETB 之前拉低
└─ 通常由 FPGA GPIO 驱动
┌──────────────────────────────────────────────────┐
│ OV5640 内部结构框图 │
├──────────────────────────────────────────────────┤
│ │
│ XCLK(24MHz) │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ PLL/时钟生成 │ │
│ │ (生成各种内部时钟) │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ 感光矩阵 (2592×1944) │ │
│ │ (CMOS 传感器,光电转换) │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ 模拟前端 (AFE) │ │
│ │ (放大、滤波、模数转换) │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ ISP(图像信号处理) │ │
│ │ ├─ 自动曝光 (AE) │ │
│ │ ├─ 自动白平衡 (AWB) │ │
│ │ ├─ 自动对焦 (AF) │ │
│ │ ├─ 伽玛校正 │ │
│ │ ├─ 色彩矩阵 │ │
│ │ └─ 降噪处理 │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ 格式转换 │ │
│ │ (YUV422/420、RGB565、JPEG 等) │ │
│ └─────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────┐ │
│ │ 输出接口 │ │
│ │ ├─ DVP(并行): Y[9:0]、PCLK、HREF、VSYNC│ │
│ │ └─ SCCB(I2C): 寄存器配置 │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────┘
1️⃣ 上电初始化
├─ RESETB 拉低,复位芯片
├─ PWDN 拉低,退出低功耗
├─ 等待 20ms,PLL 稳定
└─ 芯片就绪
2️⃣ 寄存器配置
├─ 通过 SCCB 写入配置参数
├─ 设置分辨率、帧率、输出格式
├─ 配置自动曝光、白平衡等
└─ 配置完成
3️⃣ 图像采集
├─ 感光矩阵采集光信号
├─ ISP 处理图像
├─ 格式转换
└─ 通过 DVP 接口输出
4️⃣ 数据输出
├─ PCLK: 像素时钟 (采样时钟)
├─ VSYNC: 帧同步 (低电平表示帧有效)
├─ HREF: 行同步 (高电平表示行有效)
├─ Y[9:0]: 像素数据
└─ 循环输出每一帧
SCCB(Serial Camera Control Bus) 是豪威公司定义的摄像头控制总线,与 I2C 协议类似,但有细微差别。
SCCB 特点:
写操作时序:
┌─────────────────────────────────────────────────┐
│ START | SLAVE_ADDR | ACK | H_ADDR | ACK | L_ADDR│
│ | (8bit) | | (8bit) | | (8bit) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ ACK | DATA | ACK | STOP |
│ |(8bit)| | | └─────────────────────────────────────────────────┘
读操作时序:
┌─────────────────────────────────────────────────┐
│ START | SLAVE_ADDR | ACK | H_ADDR | ACK | L_ADDR│
│ | (8bit) | | (8bit) | | (8bit) │
└─────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────┐
│ ACK | START | SLAVE_ADDR | ACK | DATA | NACK | STOP│
│ | | (8bit) | |(8bit)| | │
└─────────────────────────────────────────────────┘
| 特性 | SCCB | I2C |
|---|---|---|
| 寻址方式 | 16 位地址 | 7/10 位地址 |
| 时钟频率 | 100-400kHz | 100-400kHz |
| 从机应答 | 可选 | 必须 |
| 读操作 | 需要重复 START | 支持 |
| 兼容性 | 不完全兼容 I2C | 标准协议 |
实际应用中: FPGA 通常使用 I2C 控制器驱动 SCCB,因为两者在时序上基本兼容。
寄存器地址:16 位 (高字节 + 低字节)
寄存器数据:8 位
写寄存器示例:
地址:0x3008 (高字节 0x30, 低字节 0x08)
数据:0x82
SCCB 写操作:START → SLAVE_ADDR(0x78) → ACK → 0x30 → ACK → 0x08 → ACK → 0x82 → ACK → STOP
读寄存器示例:
地址:0x300A
SCCB 读操作:START → SLAVE_ADDR(0x78) → ACK → 0x30 → ACK → 0x0A → ACK → START → SLAVE_ADDR(0x79) → ACK → DATA → NACK → STOP
OV5640 的正确上电时序对系统稳定性至关重要。不正确的上电时序可能导致芯片无法正常工作。
上电时序流程:
时间轴:0ms
├─ 电源上电 (DVDD、AVDD、DOVDD)
│ └─ 所有电源应同时上电或按规定顺序上电
│ 5ms
├─ PWDN 拉低 (退出低功耗模式)
│ └─ 保持低电平
│ 10ms
├─ RESETB 拉低 (复位芯片)
│ └─ 保持低电平至少 1ms
│ 15ms
├─ RESETB 拉高 (释放复位)
│ └─ 芯片开始初始化
│ 35ms
└─ 等待 PLL 稳定 (约 20ms)
└─ 可以开始配置寄存器
1. 电源上电
推荐上电顺序:
1️⃣ AVDD (2.8V) 先上电
2️⃣ DVDD (1.8V) 后上电
3️⃣ DOVDD (1.8V) 最后上电
或者同时上电 (更常见)
电源稳定要求:
├─ 上升时间:< 100ms
├─ 纹波:< 100mV
├─ 稳定度:±5%
└─ 需要充分的滤波电容
2. PWDN 信号控制
PWDN (掉电/省电控制)
├─ 低电平:正常工作
├─ 高电平:低功耗模式
│ 上电时序:
├─ 电源上电后,PWDN 应拉低
├─ 保持低电平至少 5ms
└─ 然后进行 RESETB 复位
FPGA 实现示例:
always @(posedge clk)
begin
if (power_on_counter < 32'd5_000_000)
begin
pwdn <= 1'b1; // 保持低功耗
power_on_counter <= power_on_counter + 1;
end
else
begin
pwdn <= 1'b0; // 退出低功耗
end
end
3. RESETB 复位信号
RESETB (复位信号,低有效)
├─ 低电平:芯片复位
├─ 高电平:芯片工作
│ 复位时序:
├─ PWDN 拉低后,等待 5ms
├─ RESETB 拉低,保持至少 1ms
├─ RESETB 拉高,释放复位
└─ 等待 20ms,PLL 稳定
FPGA 实现示例:
always @(posedge clk)
begin
if (reset_counter < 32'd1_000_000)
begin
resetb <= 1'b0; // 保持复位
reset_counter <= reset_counter + 1;
end
else if (reset_counter < 32'd21_000_000)
begin
resetb <= 1'b1; // 释放复位
reset_counter <= reset_counter + 1;
end
else
begin
init_done <= 1'b1; // 初始化完成
end
end
OV5640 有数百个寄存器,这里介绍最常用的几个:
| 寄存器地址 | 寄存器名称 | 功能 | 典型值 |
|---|---|---|---|
| 0x3008 | SYSTEM_CTRL0 | 系统控制 | 0x82 |
| 0x3009 | SYSTEM_CTRL1 | 系统控制 | 0x02 |
| 0x3103 | CLOCK_CTRL | 时钟控制 | 0x11 |
| 0x3017 | MIPI_CTRL00 | MIPI 控制 | 0x00 |
| 0x3018 | MIPI_CTRL01 | MIPI 控制 | 0x00 |
| 0x3034 | PLL_CTRL0 | PLL 控制 | 0x18 |
| 0x3035 | PLL_CTRL1 | PLL 控制 | 0x11 |
| 0x3036 | PLL_CTRL2 | PLL 控制 | 0x54 |
| 0x3037 | PLL_CTRL3 | PLL 控制 | 0x13 |
| 0x3108 | SCCB_CTRL | SCCB 控制 | 0x01 |
| 0x3630 | ANALOG_CTRL | 模拟控制 | 0x2e |
| 0x3a00 | AE_CTRL | 自动曝光 | 0x78 |
| 0x3a02 | AE_CTRL02 | 自动曝光 | 0x00 |
| 0x3a03 | AE_CTRL03 | 自动曝光 | 0x9c |
设置分辨率的关键寄存器:
1️⃣ 输出大小控制 (0x3808-0x380B)
├─ 0x3808: 输出宽度高字节
├─ 0x3809: 输出宽度低字节
├─ 0x380A: 输出高度高字节
└─ 0x380B: 输出高度低字节
2️⃣ 时序控制 (0x3800-0x3807)
├─ 0x3800: 水平起始位置高字节
├─ 0x3801: 水平起始位置低字节
├─ 0x3802: 垂直起始位置高字节
├─ 0x3803: 垂直起始位置低字节
├─ 0x3804: 水平结束位置高字节
├─ 0x3805: 水平结束位置低字节
├─ 0x3806: 垂直结束位置高字节
└─ 0x3807: 垂直结束位置低字节
3️⃣ 格式控制 (0x4300)
├─ 0x4300: 输出格式选择
│ ├─ 0x00: YUV422
│ ├─ 0x10: RGB565
│ └─ 0x20: RGB444
640×480@30fps 配置:
寄存器配置表:
┌──────────┬──────────┬─────────────────────┐
│ 地址 │ 数据 │ 说明 │
├──────────┼──────────┼─────────────────────┤
│ 0x3008 │ 0x82 │ 系统复位 │
│ 0x3034 │ 0x18 │ PLL 倍频系数 │
│ 0x3035 │ 0x11 │ PLL 分频系数 │
│ 0x3036 │ 0x54 │ PLL 分频系数 │
│ 0x3037 │ 0x13 │ PLL 分频系数 │
│ 0x3800 │ 0x00 │ 水平起始位置 │
│ 0x3801 │ 0x00 │ │
│ 0x3802 │ 0x00 │ 垂直起始位置 │
│ 0x3803 │ 0x00 │ │
│ 0x3804 │ 0x0a │ 水平结束位置 │
│ 0x3805 │ 0x3f │ (2592) │
│ 0x3806 │ 0x07 │ 垂直结束位置 │
│ 0x3807 │ 0x9f │ (1944) │
│ 0x3808 │ 0x02 │ 输出宽度 │
│ 0x3809 │ 0x80 │ (640) │
│ 0x380a │ 0x01 │ 输出高度 │
│ 0x380b │ 0xe0 │ (480) │
│ 0x4300 │ 0x30 │ YUV422 输出 │
└──────────┴──────────┴─────────────────────┘
1280×720@60fps 配置:
关键寄存器配置:
0x3808 = 0x05 (宽度高字节)
0x3809 = 0x00 (宽度低字节) → 1280
0x380a = 0x02 (高度高字节)
0x380b = 0xd0 (高度低字节) → 720
PLL 配置:
0x3034 = 0x18
0x3035 = 0x21
0x3036 = 0x69
0x3037 = 0x13
SCCB 控制器是 FPGA 中用于与 OV5640 通信的模块,负责:
┌─────────────────────────────────────────┐
│ SCCB 控制器框图 │
├─────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────┐ │
│ │ 状态机 │ │
│ │ ├─ IDLE │ │
│ │ ├─ START │ │
│ │ ├─ ADDR_H │ │
│ │ ├─ ADDR_L │ │
│ │ ├─ DATA │ │
│ │ ├─ ACK │ │
│ │ └─ STOP │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ 时钟分频器 │ │
│ │ (生成 SCCB 时钟) │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ I/O 驱动 │ │
│ │ ├─ SIO_C (时钟线) │ │
│ │ └─ SIO_D (数据线) │ │
│ └──────────────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
module sccb_ctrl (
input clk,
input rst_n, // 控制接口
input [15:0] reg_addr, // 寄存器地址
input [7:0] reg_data_w, // 写数据
output [7:0] reg_data_r, // 读数据
input reg_wr, // 写使能
input reg_rd, // 读使能
output reg_done, // 操作完成
// SCCB 接口
inout sio_c, // 时钟线 (开漏)
inout sio_d // 数据线 (开漏)
);
// 状态定义
localparam IDLE = 4'd0;
localparam START = 4'd1;
localparam ADDR_H = 4'd2;
localparam ADDR_L = 4'd3;
localparam DATA = 4'd4;
localparam ACK = 4'd5;
localparam STOP = 4'd6;
reg [3:0] state, next_state;
reg [7:0] bit_counter;
reg [7:0] data_buffer;
// 时钟分频 (100kHz SCCB 时钟)
// 假设系统时钟为 50MHz
// 分频系数 = 50MHz / (100kHz × 4) = 125
localparam CLK_DIV = 7'd125;
reg [6:0] clk_counter;
reg sccb_clk;
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
clk_counter <= 0;
sccb_clk <= 0;
end
else if (clk_counter == CLK_DIV - 1)
begin
clk_counter <= 0;
sccb_clk <= ~sccb_clk;
end
else
begin
clk_counter <= clk_counter + 1;
end
end
// 状态机
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always @(*)
begin
case (state)
IDLE: begin
if (reg_wr || reg_rd)
next_state = START;
else
next_state = IDLE;
end
START: next_state = ADDR_H;
ADDR_H: next_state = ADDR_L;
ADDR_L: next_state = (reg_wr) ? DATA : ACK;
DATA: next_state = ACK;
ACK: next_state = STOP;
STOP: next_state = IDLE;
default: next_state = IDLE;
endcase
end
// I/O 驱动 (开漏输出)
assign sio_c = (state == IDLE) ? 1'bz : 1'b0;
assign sio_d = (state == IDLE) ? 1'bz : 1'b0;
endmodule
写操作时序示例:
CLK: ─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
SIO_C:─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─
SIO_D: ─┐ ┌─────────────────────────────────────────────────────────────
│ │ START
└─┘ ┌─────────────────────────────────────────────────────────────┐
SIO_D:─┤ SLAVE_ADDR(0x78) │ ACK │ ADDR_H │ ACK │ ADDR_L │ ACK │ DATA │ ACK │ STOP
└─────────────────────────────────────────────────────────────┘
DVP(Digital Video Port) 是 OV5640 的并行数据输出接口,用于传输图像数据。
DVP 接口特点:
DVP 接口信号:
┌─────────────────────────────────────────┐
│ DVP 接口信号定义 │
├─────────────────────────────────────────┤
│ │
│ 1️⃣ 时钟信号: │
│ PCLK - 像素时钟 (采样时钟) │
│ 频率:6-148.5MHz │
│ 用于同步数据采样 │
│ │
│ 2️⃣ 同步信号: │
│ VSYNC - 帧同步 (低电平表示帧有效) │
│ HREF - 行同步 (高电平表示行有效) │
│ │
│ 3️⃣ 数据信号: │
│ Y[9:0] - 10 位像素数据 │
│ 通常使用 Y[9:2] 作为 8 位数据 │
│ │
│ 4️⃣ 其他: │
│ XCLK - 输入时钟 (24MHz) │
│ RESETB - 复位信号 │
│ PWDN - 掉电控制 │
│ │
└─────────────────────────────────────────┘
一帧图像的采集时序:
VSYNC: ─┐ ┌─
│ (低电平表示帧有效)
│ └────────────────────────────────────┘
HREF: ─┬─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─
│ │ │ │ │ │ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─
│ (高电平表示行有效)
PCLK: ─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─
Y[9:0]: ┌─────┬─────┬─────┬─────┬─────┬─────┐
│ P0 │ P1 │ P2 │ P3 │ P4 │ P5 │
└─────┴─────┴─────┴─────┴─────┴─────┘
↑ ↑ ↑ ↑ ↑ ↑
在 PCLK 上升沿采样
时序说明:
┌──────────────────────────────────────────────┐
│ 图像采集模块框图 │
├──────────────────────────────────────────────┤
│ │
│ OV5640 摄像头 │
│ ├─ PCLK │
│ ├─ VSYNC │
│ ├─ HREF │
│ └─ Y[9:0] │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ 采集控制模块 │ │
│ │ ├─ 帧同步检测 (VSYNC) │ │
│ │ ├─ 行同步检测 (HREF) │ │
│ │ ├─ 像素计数 │ │
│ │ └─ 行列计数 │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────┐ │
│ │ 数据缓冲 │ │
│ │ ├─ 像素数据缓存 │ │
│ │ ├─ 行缓冲 │ │
│ │ └─ 帧缓冲 │ │
│ └───────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ 输出接口 │ │
│ │ ├─ 像素数据输出 │ │
│ │ ├─ 行有效信号 │ │
│ │ ├─ 帧有效信号 │ │
│ │ └─ 数据有效信号 │ │
│ └──────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
module dvp_capture (
input clk,
input rst_n,
// DVP 接口输入
input pclk,
input vsync,
input href,
input [9:0] data_in,
// 输出接口
output reg [7:0] data_out,
output reg data_valid,
output reg line_valid,
output reg frame_valid,
output reg [11:0] pixel_x,
output reg [11:0] pixel_y
);
// 状态定义
localparam IDLE = 2'd0;
localparam FRAME_ACTIVE = 2'd1;
localparam LINE_ACTIVE = 2'd2;
reg [1:0] state;
reg vsync_r1, vsync_r2;
reg href_r1, href_r2;
// 同步 VSYNC 和 HREF 到系统时钟
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
vsync_r1 <= 1'b1;
vsync_r2 <= 1'b1;
href_r1 <= 1'b0;
href_r2 <= 1'b0;
end
else
begin
vsync_r1 <= vsync;
vsync_r2 <= vsync_r1;
href_r1 <= href;
href_r2 <= href_r1;
end
end
// 采集控制状态机
always @(posedge pclk or negedge rst_n)
begin
if (!rst_n)
begin
state <= IDLE;
frame_valid <= 1'b0;
line_valid <= 1'b0;
pixel_x <= 12'd0;
pixel_y <= 12'd0;
end
else
begin
case (state)
IDLE: begin
if (!vsync)
begin
state <= FRAME_ACTIVE;
frame_valid <= 1'b1;
pixel_y <= 12'd0;
end
end
FRAME_ACTIVE: begin
if (vsync)
begin
state <= IDLE;
frame_valid <= 1'b0;
pixel_y <= 12'd0;
end
else if (href)
begin
line_valid <= 1'b1;
pixel_x <= pixel_x + 1;
end
else
begin
line_valid <= 1'b0;
if (pixel_x > 0)
begin
pixel_y <= pixel_y + 1;
pixel_x <= 12'd0;
end
end
end
default: state <= IDLE;
endcase
end
end
// 数据采样
always @(posedge pclk or negedge rst_n)
begin
if (!rst_n)
begin
data_out <= 8'd0;
data_valid <= 1'b0;
end
else
begin
if (href && frame_valid)
begin
data_out <= data_in[9:2]; // 使用高 8 位
data_valid <= 1'b1;
end
else
begin
data_valid <= 1'b0;
end
end
end
endmodule
1. 时钟域处理
PCLK 时钟域 (摄像头时钟)
↓ 采集数据
↓
系统时钟域 (FPGA 系统时钟)
↓ 处理数据
关键:使用 CDC(Clock Domain Crossing) 同步
2. 像素计数
像素计数逻辑:
├─ 在 HREF 高电平时计数像素
├─ HREF 下降沿时,行计数 +1
├─ VSYNC 下降沿时,帧计数 +1
└─ 用于生成行列坐标
3. 数据有效性判断
数据有效条件:
├─ VSYNC 低电平 (帧有效)
├─ HREF 高电平 (行有效)
├─ 在 PCLK 上升沿采样
└─ 三个条件同时满足时,数据有效
帧同步时序:
VSYNC: ─┐ ┌─
│ 帧有效 (低电平)
│ └────────────────────────────────────┘
frame_valid: ─┐ ┌─
│ 帧有效标志
│ └────────────────────────────────────┘
pixel_y: ─┬─────────────────────────────┬─
│ 0→1→2→...→(height-1)→0
│ └─────────────────────────────┘
行同步时序:
HREF: ─┬─────┐ ┬─────┐ ┬─────┐
│ │ │ │ │ │ └─────┘ └─────┘ └─────┘
line_valid: ─┬─────┐ ┬─────┐ ┬─────┐
│ │ │ │ │ │ └─────┘ └─────┘ └─────┘
pixel_x: ─┬─────────────┬─────────────┬─
│ 0→1→...→639 │ 0→1→...→639
│ └─────────────┴─────────────┘
测试场景:
1️⃣ 单帧采集测试
├─ 采集一帧完整图像
├─ 验证像素数据正确性
└─ 检查行列坐标
2️⃣ 连续采集测试
├─ 采集多帧图像
├─ 验证帧同步
└─ 检查数据连续性
3️⃣ 时序验证
├─ 使用逻辑分析仪
├─ 验证 PCLK、HREF、VSYNC 时序
└─ 检查数据采样点
在 FPGA 摄像头系统中,缓存的作用是:
缓存大小计算:
缓存大小 = 分辨率 × 像素深度 × 帧数
例如 1280×720@60fps:
├─ 单帧大小 = 1280 × 720 × 2 字节 (RGB565) = 1.8MB
├─ 双缓冲 = 1.8MB × 2 = 3.6MB
└─ 三缓冲 = 1.8MB × 3 = 5.4MB
常见缓存方案:
├─ 单缓冲:最小化延迟,但易出现撕裂
├─ 双缓冲:平衡延迟和稳定性 (推荐)
└─ 三缓冲:最大稳定性,但延迟最大
1. 片内 RAM 缓存
优点:
✅ 速度快 (单周期访问)
✅ 延迟低
✅ 易于集成
缺点:
❌ 容量有限 (通常< 1MB)
❌ 只适合低分辨率
适用场景:
├─ 640×480 以下分辨率
├─ 行缓冲
└─ 临时数据存储
2. SDRAM 缓存
优点:
✅ 容量大 (通常 128MB-512MB)
✅ 成本低
✅ 易于扩展
缺点:
❌ 延迟高 (10-20 个周期)
❌ 需要复杂的控制器
❌ 需要刷新
适用场景:
├─ 中等分辨率 (720p)
├─ 成本敏感应用
└─ 需要大容量存储
3. DDR3 缓存
优点:
✅ 容量大 (通常 512MB-2GB)
✅ 带宽高 (可达 25.6GB/s)
✅ 性能好
缺点:
❌ 成本高
❌ 功耗高
❌ 控制复杂
适用场景:
├─ 高分辨率 (1080p 及以上)
├─ 高帧率 (60fps 及以上)
└─ 需要高性能
双端口 RAM 允许同时进行读写操作,是 FPGA 图像处理的关键组件。
双端口 RAM 结构:
┌──────────────────────────────────────┐
│ 双端口 RAM │
├──────────────────────────────────────┤
│ │
│ 端口 A (写端口) 端口 B (读端口) │
│ ├─ 地址:addr_a │
│ ├─ 地址:addr_b │
│ ├─ 数据:data_in │
│ ├─ 数据:data_out │
│ ├─ 写使能:we_a │
│ └─ 读使能:re_b │
│ └─ 时钟:clk_a │
│ 时钟:clk_b │
│ │
│ 可以同时: │
│ ├─ 在地址 A 写入数据 │
│ ├─ 在地址 B 读出数据 │
│ └─ 两个操作独立进行 │
│ ────── │
└──────────────────────────────────────┘
module dual_port_ram #(
parameter ADDR_WIDTH = 12,
parameter DATA_WIDTH = 16,
parameter DEPTH = 4096
) (
// 写端口 (采集端)
input clk_a,
input [ADDR_WIDTH-1:0] addr_a,
input [DATA_WIDTH-1:0] data_in,
input we_a,
// 读端口 (显示端)
input clk_b,
input [ADDR_WIDTH-1:0] addr_b,
output reg [DATA_WIDTH-1:0] data_out,
input re_b
);
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
// 写操作 (采集端)
always @(posedge clk_a)
begin
if (we_a)
mem[addr_a] <= data_in;
end
// 读操作 (显示端)
always @(posedge clk_b)
begin
if (re_b)
data_out <= mem[addr_b];
end
endmodule
双缓冲帧管理:
┌─────────────────────────────────────────┐
│ 帧缓冲管理 │
├─────────────────────────────────────────┤
│ │
│ 缓冲区 A (写入缓冲) │
│ ├─ 采集模块写入新帧 │
│ ├─ 显示模块不读取 │
│ └─ 写入完成后切换 │
│ │
│ 缓冲区 B (显示缓冲) │
│ ├─ 显示模块读取显示 │
│ ├─ 采集模块不写入 │
│ └─ 显示完成后切换 │
│ │
│ 切换机制: │
│ ├─ 采集完成 → 切换写入缓冲 │
│ ├─ 显示完成 → 切换显示缓冲 │
│ └─ 防止撕裂 │
│ │
└─────────────────────────────────────────┘
SDRAM(Synchronous Dynamic RAM) 是常用的外部存储器。
SDRAM 特点:
SDRAM 时序:
SDRAM 读操作时序:
CLK: ─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─
CMD: ─┬─────────┬─────────┬─────────┬─
│ ACTIVE │ READ │ PRECHARGE
└─────────┴─────────┴─────────┴─
ADDR: ─┬─────────┬─────────┬─────────┬─
│ ROW │ COL │ (X)
└─────────┴─────────┴─────────┴─
DATA: ─────────────────────┬─────────┬─
│ DATA_OUT│ └─────────┘ (延迟:CAS Latency)
┌──────────────────────────────────────────┐
│ SDRAM 控制器框图 │
├──────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────┐ │
│ │ 命令生成器 │ │
│ │ ├─ ACTIVE 命令 │ │
│ │ ├─ READ/WRITE 命令 │ │
│ │ └─ PRECHARGE 命令 │ │
│ └────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────┐ │
│ │ 地址复用器 │ │
│ │ ├─ 行地址 (ROW) │ │
│ │ └─ 列地址 (COL) │ │
│ └────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────┐ │
│ │ 数据通路 │ │
│ │ ├─ 写数据缓冲 │ │
│ │ └─ 读数据缓冲 │ │
│ └────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────┐ │
│ │ SDRAM 接口 │ │
│ │ ├─ 地址线 (A[12:0]) │ │
│ │ ├─ 数据线 (DQ[15:0]) │ │
│ │ ├─ 控制线 (CS, RAS, CAS, WE) │ │
│ │ └─ 时钟 (CLK) │ │
│ └────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────┘
module sdram_ctrl (
input clk,
input rst_n,
// 用户接口
input [23:0] addr, // 24 位地址 (支持 16MB)
input [15:0] data_in,
output [15:0] data_out,
input we, // 写使能
input re, // 读使能
output busy, // SDRAM 接口
// SDRAM 接口
output [12:0] sdram_addr,
output [1:0] sdram_ba, // Bank 地址
inout [15:0] sdram_dq,
output sdram_cs_n,
output sdram_ras_n,
output sdram_cas_n,
output sdram_we_n,
output sdram_clk
);
// 状态定义
localparam IDLE = 3'd0;
localparam ACTIVE = 3'd1;
localparam READ = 3'd2;
localparam WRITE = 3'd3;
localparam PRECHARGE = 3'd4;
reg [2:0] state, next_state;
reg [3:0] cmd_counter;
// 地址分解
wire [1:0] bank = addr[23:22];
wire [12:0] row = addr[21:9];
wire [8:0] col = addr[8:0];
// 状态机
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
state <= IDLE;
cmd_counter <= 0;
end
else
begin
state <= next_state;
if (state != next_state)
cmd_counter <= 0;
else
cmd_counter <= cmd_counter + 1;
end
end
always @(*)
begin
case (state)
IDLE: begin
if (we || re)
next_state = ACTIVE;
else
next_state = IDLE;
end
ACTIVE: begin
if (cmd_counter >= 2)
next_state = (we) ? WRITE : READ;
else
next_state = ACTIVE;
end
READ, WRITE: begin
if (cmd_counter >= 3)
next_state = PRECHARGE;
else
next_state = state;
end
PRECHARGE: begin
if (cmd_counter >= 2)
next_state = IDLE;
else
next_state = PRECHARGE;
end
default: next_state = IDLE;
endcase
end
// 命令输出
assign sdram_cs_n = 1'b0;
assign sdram_clk = clk;
always @(*)
begin
case (state)
ACTIVE: begin
sdram_addr = row;
sdram_ba = bank;
sdram_ras_n = 1'b0;
sdram_cas_n = 1'b1;
sdram_we_n = 1'b1;
end
READ: begin
sdram_addr = {4'b0, col};
sdram_ba = bank;
sdram_ras_n = 1'b1;
sdram_cas_n = 1'b0;
sdram_we_n = 1'b1;
end
WRITE: begin
sdram_addr = {4'b0, col};
sdram_ba = bank;
sdram_ras_n = 1'b1;
sdram_cas_n = 1'b0;
sdram_we_n = 1'b0;
end
PRECHARGE: begin
sdram_addr = 13'b0;
sdram_ba = 2'b0;
sdram_ras_n = 1'b0;
sdram_cas_n = 1'b1;
sdram_we_n = 1'b0;
end
default: begin
sdram_addr = 13'b0;
sdram_ba = 2'b0;
sdram_ras_n = 1'b1;
sdram_cas_n = 1'b1;
sdram_we_n = 1'b1;
end
endcase
end
assign busy = (state != IDLE);
endmodule
1️⃣ 格式转换
├─ YUV422 → RGB565
├─ YUV422 → YUV420
└─ RGB565 → RGB888
2️⃣ 图像缩放
├─ 最近邻插值 (快速)
├─ 双线性插值 (质量好)
└─ 双三次插值 (质量最好)
3️⃣ 图像滤波
├─ 高斯滤波 (平滑)
├─ 中值滤波 (去噪)
└─ Sobel 滤波 (边缘检测)
4️⃣ 色彩调整
├─ 亮度调整
├─ 对比度调整
└─ 饱和度调整
module yuv422_to_rgb565 (
input clk,
input [7:0] y,
input [7:0] u,
input [7:0] v,
output [15:0] rgb565
);
wire [15:0] r, g, b;
// YUV 到 RGB 转换公式
// R = Y + 1.402 * (V - 128)
// G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
// B = Y + 1.772 * (U - 128)
assign r = (y + ((v - 8'd128) * 9'd179) >> 8);
assign g = (y - (((u - 8'd128) * 9'd44) >> 8) - (((v - 8'd128) * 9'd91) >> 8));
assign b = (y + ((u - 8'd128) * 9'd227) >> 8);
// 限幅到 0-255
wire [7:0] r_clamp = (r > 255) ? 8'd255 : (r < 0) ? 8'd0 : r[7:0];
wire [7:0] g_clamp = (g > 255) ? 8'd255 : (g < 0) ? 8'd0 : g[7:0];
wire [7:0] b_clamp = (b > 255) ? 8'd255 : (b < 0) ? 8'd0 : b[7:0];
// 转换为 RGB565
assign rgb565 = {r_clamp[7:3], g_clamp[7:2], b_clamp[7:3]};
endmodule
VGA(Video Graphics Array) 是显示器的标准接口,HDMI 兼容 VGA 时序。
VGA 时序参数:
VGA 时序包含:
├─ 水平同步 (HSYNC): 行同步信号
├─ 垂直同步 (VSYNC): 帧同步信号
├─ 像素时钟 (PCLK): 采样时钟
└─ RGB 数据:颜色数据
VGA 时序关键参数:
├─ 分辨率:宽度×高度
├─ 刷新率:通常 60Hz
├─ 像素时钟:根据分辨率计算
└─ 同步脉宽:HSYNC 和 VSYNC 的宽度
1280×720@60Hz 时序参数:
┌──────────────────────────────────────────┐
│ 1280×720@60Hz VGA 时序参数 │
├──────────────────────────────────────────┤
│ 像素时钟 (PCLK) │ 74.25MHz │
│ 水平总像素 │ 1650 │
│ 水平有效像素 │ 1280 │
│ 水平前沿 (HFP) │ 110 │
│ 水平同步宽度 (HSW) │ 40 │
│ 水平后沿 (HBP) │ 220 │
│ │
│ 垂直总行数 │ 750 │
│ 垂直有效行数 │ 720 │
│ 垂直前沿 (VFP) │ 5 │
│ 垂直同步宽度 (VSW) │ 5 │
│ 垂直后沿 (VBP) │ 20 │
│ │
│ 刷新率 │ 60Hz │
│ 帧周期 │ 16.67ms │
└──────────────────────────────────────────┘
1920×1080@60Hz 时序参数:
像素时钟 (PCLK) │ 148.5MHz
水平总像素 │ 2200
水平有效像素 │ 1920
水平前沿 (HFP) │ 88
水平同步宽度 (HSW) │ 44
水平后沿 (HBP) │ 148
垂直总行数 │ 1125
垂直有效行数 │ 1080
垂直前沿 (VFP) │ 4
垂直同步宽度 (VSW) │ 5
垂直后沿 (VBP) │ 36
一行的 VGA 时序:
PCLK: ─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─
HSYNC: ─┐ ┌─
│ (低电平表示同步)
│ └────────────────────────────────────┘
RGB: ─┬─────────────────────┬─────────────┬─
│ 有效数据 (1280 像素) │ 无效数据
│ └─────────────────────┴─────────────┘
↑ ↑ ↑
HBP HFP HSW
HDMI(High-Definition Multimedia Interface) 是高清多媒体接口。
HDMI 特点:
HDMI 信号:
HDMI 接口信号:
┌──────────────────────────────────────┐
│ HDMI 接口信号定义 │
├──────────────────────────────────────┤
│ │
│ 1️⃣ 视频信号 (差分): │
│ ├─ TMDS_CLK+ / TMDS_CLK- │
│ ├─ TMDS_D0+ / TMDS_D0- │
│ ├─ TMDS_D1+ / TMDS_D1- │
│ └─ TMDS_D2+ / TMDS_D2- │
│ │
│ 2️⃣ 音频信号 (差分): │
│ ├─ TMDS_D3+ / TMDS_D3- │
│ └─ (与视频复用) │
│ │
│ 3️⃣ 控制信号: │
│ ├─ DDC_SDA (I2C 数据) │
│ ├─ DDC_SCL (I2C 时钟) │
│ ├─ CEC (消费电子控制) │
│ └─ HPD (热插拔检测) │
│ │
│ 4️⃣ 电源: │
│ ├─ +5V (电源) │
│ └─ GND (地) │
│ │
└──────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ HDMI 驱动框图 │
├──────────────────────────────────────────┤
│ │
│ VGA 时序生成器 │
│ ├─ HSYNC、VSYNC 生成 │
│ ├─ 像素计数 │
│ └─ 行列计数 │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ TMDS 编码器 │ │
│ │ ├─ 8bit RGB → 10bit TMDS │ │
│ │ ├─ 差分编码 │ │
│ │ └─ 时钟编码 │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────┐ │
│ │ 差分驱动器 │ │
│ │ ├─ TMDS_CLK+/- │ │
│ │ ├─ TMDS_D0+/- │ │
│ │ ├─ TMDS_D1+/- │ │
│ │ └─ TMDS_D2+/- │ │
│ └──────────────────────────────────┘ │
│ ↓ │
│ HDMI 连接器 │
│ │
└──────────────────────────────────────────┘
module vga_timing_gen #(
parameter H_TOTAL = 1650,
parameter H_ACTIVE = 1280,
parameter H_FP = 110,
parameter H_SYNC = 40,
parameter V_TOTAL = 750,
parameter V_ACTIVE = 720,
parameter V_FP = 5,
parameter V_SYNC = 5
) (
input clk,
input rst_n,
output reg hsync,
output reg vsync,
output reg [11:0] pixel_x,
output reg [11:0] pixel_y,
output reg data_valid
);
reg [11:0] h_counter, v_counter;
// 水平计数器
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
h_counter <= 0;
else if (h_counter == H_TOTAL - 1)
h_counter <= 0;
else
h_counter <= h_counter + 1;
end
// 垂直计数器
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
v_counter <= 0;
else if (h_counter == H_TOTAL - 1)
begin
if (v_counter == V_TOTAL - 1)
v_counter <= 0;
else
v_counter <= v_counter + 1;
end
end
// HSYNC 生成
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
hsync <= 1'b1;
else
begin
if (h_counter >= (H_ACTIVE + H_FP) && h_counter < (H_ACTIVE + H_FP + H_SYNC))
hsync <= 1'b0;
else
hsync <= 1'b1;
end
end
// VSYNC 生成
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
vsync <= 1'b1;
else
begin
if (v_counter >= (V_ACTIVE + V_FP) && v_counter < (V_ACTIVE + V_FP + V_SYNC))
vsync <= 1'b0;
else
vsync <= 1'b1;
end
end
// 像素坐标和数据有效信号
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
pixel_x <= 0;
pixel_y <= 0;
data_valid <= 1'b0;
end
else
begin
pixel_x <= h_counter;
pixel_y <= v_counter;
if (h_counter < H_ACTIVE && v_counter < V_ACTIVE)
data_valid <= 1'b1;
else
data_valid <= 1'b0;
end
end
endmodule
TMDS(Transition Minimized Differential Signaling) 是 HDMI 的编码方式。
TMDS 编码特点:
TMDS 编码流程:
8bit RGB 数据
↓
第一阶段:XOR 编码
├─ 计算转换数
└─ 选择编码方式
↓
第二阶段:直流平衡
├─ 调整输出
└─ 保持直流平衡
↓
10bit TMDS 数据
↓
差分驱动
├─ TMDS+
└─ TMDS-
module tmds_encoder (
input clk,
input [7:0] data_in,
input ctrl_in,
output reg [9:0] tmds_out
);
wire [8:0] xor_result;
wire [3:0] ones_count;
// 第一阶段:XOR 编码
assign xor_result[0] = data_in[0];
assign xor_result[1] = xor_result[0] ^ data_in[1];
assign xor_result[2] = xor_result[1] ^ data_in[2];
assign xor_result[3] = xor_result[2] ^ data_in[3];
assign xor_result[4] = xor_result[3] ^ data_in[4];
assign xor_result[5] = xor_result[4] ^ data_in[5];
assign xor_result[6] = xor_result[5] ^ data_in[6];
assign xor_result[7] = xor_result[6] ^ data_in[7];
assign xor_result[8] = 1'b0;
// 计算 1 的个数
assign ones_count = xor_result[0] + xor_result[1] + xor_result[2] + xor_result[3] + xor_result[4] + xor_result[5] + xor_result[6] + xor_result[7];
// 第二阶段:直流平衡
always @(posedge clk)
begin
if (ctrl_in)
begin
// 控制字符编码
tmds_out <= 10'b1010101100; // 简化处理
end
else
begin
// 数据编码
if (ones_count > 4 || (ones_count == 4 && xor_result[0] == 0))
tmds_out <= {~xor_result[8], xor_result[7:0], 1'b0};
else
tmds_out <= {xor_result[8], xor_result[7:0], 1'b1};
end
end
endmodule
┌──────────────────────────────────────────────┐
│ HDMI 显示系统完整框图 │
├──────────────────────────────────────────────┤
│ │
│ 图像缓存 (SDRAM/DDR3) │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ 读取控制器 │ │
│ │ ├─ 地址生成 │ │
│ │ ├─ 数据读取 │ │
│ │ └─ 缓冲管理 │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ VGA 时序生成器 │ │
│ │ ├─ HSYNC、VSYNC │ │
│ │ ├─ 像素坐标 │ │
│ │ └─ 数据有效信号 │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ TMDS 编码器 │ │
│ │ ├─ RGB → TMDS 编码 │ │
│ │ ├─ 时钟编码 │ │
│ │ └─ 控制信号编码 │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────┐ │
│ │ 差分驱动器 │ │
│ │ ├─ TMDS_CLK+/- │ │
│ │ ├─ TMDS_D0+/- │ │
│ │ ├─ TMDS_D1+/- │ │
│ │ └─ TMDS_D2+/- │ │
│ └──────────────────────────────────────┘ │
│ ↓ │
│ HDMI 连接器 → 显示器 │
│ │
└──────────────────────────────────────────────┘
一帧显示的完整时序:
VSYNC: ─┐ ┌─
│ (低电平表示帧有效)
│ └────────────────────────────────────┘
HSYNC: ─┬─────┐ ┬─────┐ ┬─────┐ ┬─────┐ ┬─
│ │ │ │ │ │ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ └─
RGB: ─┬─────────────────────┬─────────────┬─
│ 有效数据 │ 无效数据
│ └─────────────────────┴─────────────┘
TMDS: ─┬─────────────────────┬─────────────┬─
│ 编码后的数据 │ 编码后的数据
│ └─────────────────────┴─────────────┘
module camera_hdmi_system (
input sys_clk,
input rst_n,
// 摄像头接口
output camera_xclk,
output camera_resetb,
output camera_pwdn,
inout camera_sio_c,
inout camera_sio_d,
input camera_pclk,
input camera_vsync,
input camera_href,
input [9:0] camera_data,
// HDMI 接口
output hdmi_clk_p,
output hdmi_clk_n,
output [2:0] hdmi_d_p,
output [2:0] hdmi_d_n,
// 调试接口
output [7:0] led,
input [3:0] btn
);
// 时钟生成
wire clk_100m, clk_74m25, clk_148m5;
pll_clk_gen pll_inst (
.clk_in(sys_clk),
.clk_100m(clk_100m),
.clk_74m25(clk_74m25),
.clk_148m5(clk_148m5)
);
// 摄像头初始化
wire camera_init_done;
camera_init camera_init_inst (
.clk(clk_100m),
.rst_n(rst_n),
.sio_c(camera_sio_c),
.sio_d(camera_sio_d),
.resetb(camera_resetb),
.pwdn(camera_pwdn),
.init_done(camera_init_done)
);
// 图像采集
wire [7:0] pixel_data;
wire pixel_valid;
wire frame_valid;
dvp_capture capture_inst (
.pclk(camera_pclk),
.vsync(camera_vsync),
.href(camera_href),
.data_in(camera_data),
.data_out(pixel_data),
.data_valid(pixel_valid),
.frame_valid(frame_valid)
);
// 图像缓存 (SDRAM)
wire [23:0] sdram_addr;
wire [15:0] sdram_data_w, sdram_data_r;
wire sdram_we, sdram_re;
sdram_ctrl sdram_inst (
.clk(clk_100m),
.rst_n(rst_n),
.addr(sdram_addr),
.data_in(sdram_data_w),
.data_out(sdram_data_r),
.we(sdram_we),
.re(sdram_re)
);
// VGA 时序生成
wire [11:0] pixel_x, pixel_y;
wire hsync, vsync;
wire vga_data_valid;
vga_timing_gen vga_inst (
.clk(clk_74m25),
.rst_n(rst_n),
.hsync(hsync),
.vsync(vsync),
.pixel_x(pixel_x),
.pixel_y(pixel_y),
.data_valid(vga_data_valid)
);
// HDMI 输出
hdmi_tx hdmi_inst (
.clk_pixel(clk_74m25),
.clk_tmds(clk_148m5),
.rst_n(rst_n),
.hsync(hsync),
.vsync(vsync),
.rgb_data(sdram_data_r),
.data_valid(vga_data_valid),
.hdmi_clk_p(hdmi_clk_p),
.hdmi_clk_n(hdmi_clk_n),
.hdmi_d_p(hdmi_d_p),
.hdmi_d_n(hdmi_d_n)
);
// 状态指示
assign led[0] = camera_init_done;
assign led[1] = frame_valid;
assign led[2] = vga_data_valid;
assign led[7:3] = 5'b0;
endmodule
采集模块 → 缓存模块 → 显示模块
采集模块输出:
├─ pixel_data[7:0]: 像素数据
├─ pixel_valid: 数据有效
├─ frame_valid: 帧有效
└─ pixel_x, pixel_y: 像素坐标
缓存模块:
├─ 输入:采集数据
├─ 输出:显示数据
└─ 管理:双缓冲切换
显示模块输入:
├─ rgb_data[15:0]: RGB565 数据
├─ hsync, vsync: 同步信号
└─ data_valid: 数据有效
问题 1: 摄像头初始化失败
排查步骤: 1️⃣ 检查电源 ├─ 测量 DVDD、AVDD、DOVDD 电压 ├─ 检查电源纹波 └─ 确保电源稳定
2️⃣ 检查时钟 ├─ 测量 XCLK 频率 ├─ 检查时钟占空比 └─ 确保时钟稳定
3️⃣ 检查复位时序 ├─ 使用逻辑分析仪 ├─ 验证 RESETB、PWDN 时序 └─ 确保时序正确
4️⃣ 检查 SCCB 通信 ├─ 使用逻辑分析仪 ├─ 验证 SIO_C、SIO_D 波形 └─ 检查从机应答
问题 2: 图像采集无数据
排查步骤: 1️⃣ 检查 DVP 接口 ├─ 测量 PCLK 频率 ├─ 检查 HREF、VSYNC 波形 └─ 验证数据线连接
2️⃣ 检查采集模块 ├─ 仿真验证时序 ├─ 检查状态机 └─ 添加调试信号
3️⃣ 使用逻辑分析仪 ├─ 捕获完整帧数据 ├─ 验证数据有效性 └─ 检查时序关系
问题 3: HDMI 无显示
排查步骤: 1️⃣ 检查 HDMI 连接 ├─ 确保连接牢固 ├─ 尝试不同显示器 └─ 检查 HDMI 线质量
2️⃣ 检查 VGA 时序 ├─ 仿真验证时序 ├─ 检查 HSYNC、VSYNC └─ 验证像素时钟
3️⃣ 检查 TMDS 编码 ├─ 使用示波器 ├─ 测量差分信号 └─ 检查信号幅度
4️⃣ 检查显示数据 ├─ 验证 RGB 数据 ├─ 检查数据有效信号 └─ 确保数据连续
推荐工具: ├─ 逻辑分析仪 │ ├─ 捕获时序信号 │ ├─ 验证协议 │ └─ 分析数据 │ ├─ 示波器 │ ├─ 测量模拟信号 │ ├─ 检查信号质量 │ └─ 验证时序 │ ├─ 万用表 │ ├─ 测量电压 │ ├─ 检查连接 │ └─ 验证电源 │ └─ 仿真工具 ├─ ModelSim ├─ VCS └─ Vivado 仿真
优化策略:
1️⃣ 减少不必要的访问 ├─ 使用行缓冲 ├─ 批量读写 └─ 避免随机访问
2️⃣ 提高缓存命中率 ├─ 合理分配缓存 ├─ 预取数据 └─ 优化访问模式
3️⃣ 使用高速存储 ├─ 优先使用片内 RAM ├─ 使用 DDR3 代替 SDRAM └─ 增加缓存大小
优化策略:
1️⃣ 流水线设计 ├─ 采集、处理、显示并行 ├─ 减少阻塞 └─ 提高吞吐量
2️⃣ 时钟优化 ├─ 提高时钟频率 ├─ 使用多时钟域 └─ 优化时序约束
3️⃣ 算法优化 ├─ 简化处理算法 ├─ 使用查表法 └─ 并行处理
优化策略:
1️⃣ 时钟门控 ├─ 关闭未使用模块 ├─ 动态调整频率 └─ 减少时钟树
2️⃣ 电压优化 ├─ 降低工作电压 ├─ 使用低功耗工艺 └─ 优化电源管理
3️⃣ 存储优化 ├─ 减少存储访问 ├─ 使用压缩格式 └─ 优化存储结构
本文详细介绍了 FPGA 摄像头采集、处理、显示系统的完整设计流程:
第一部分:系统概述
第二部分:摄像头驱动
第三部分:摄像头初始化
第四部分:图像采集
第五部分:图像处理与缓存
第六部分:HDMI 显示
第七部分:实战案例
进阶主题:
相关技术:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online