# FPGA 零乘法器 Sobel:用 15% LEs 跑 640×480 实时边缘检测
本文介绍了一个在入门级FPGA(EP4CE6,6,272个逻辑单元)上实现完整图像处理流水线的项目。该系统通过四级流水线(RGB转灰度、3x3滑动窗口、Sobel边缘检测、阈值二值化)实现从摄像头采集到HDMI输出的实时边缘检测,且完全不使用DSP乘法器。关键设计包括:采用FIFO直通替代SDRAM帧缓冲(节省40%资源),选择Sobel算法替代Canny,以及通过移位加法等技巧实现零乘法器运算。
从零开始,在 6,272 个逻辑单元里塞进一个完整的摄像头采集 -> 边缘检测 -> HDMI 输出的图像流水线。
一、为什么要做这个?
2026 年,FPGA 开发板的门槛已经很低了。小梅哥 AC620 V2,基于 Altera Cyclone IV EP4CE6,一百多块就能买到。但问题来了:
EP4CE6 只有 6,272 个 LEs、30 个 9-bit 乘法器,连一个 SDRAM 控制器都能吃掉 40% 的逻辑资源。而大部分 FPGA 教程上的图像处理例程,都默认你有一块带 DDR 的 SoC 开发板。
这个项目的目标很明确:
在资源极度受限的入门级 FPGA 上,实现完整的实时边缘检测流水线,并且一个 DSP 乘法器都不碰。
二、硬件平台
| 器件 | 型号 | 关键参数 |
|---|---|---|
| FPGA | 小梅哥 AC620 V2 | EP4CE6F17C8, 6,272 LEs, 270Kb RAM |
| 摄像头 | OV7725 | DVP 8-bit, 640x480@30fps, RGB565 |
| 输出接口 | HDMI (TMDS) | 640x480@60Hz |
| 开发工具 | Quartus II 13.0 | Cyclone IV E 系列 |
三、架构全景
整个系统的数据流:
四级流水线:
第1级: rgb2gray - RGB565 转 8-bit 灰度, 1 cycle
第2级: line_buffer_3x3 - 双行 RAM + 移位寄存器, ~2行延迟
第3级: sobel_3x3 - 3x3 Sobel 梯度计算, 1 cycle
第4级: threshold - 阈值二值化, 1 cycle
总延迟: ~67 cycles | 理论吞吐: 162 FPS (瓶颈在摄像头 30fps)
四、三个关键设计决策
决策 1:FIFO 直通 vs SDRAM 帧缓冲
| 方案 | LE 消耗 | 复杂度 |
|---|---|---|
| SDRAM 帧缓冲 | ~2,500 LEs (40%) | 高(控制器+仲裁) |
| FIFO 直通 | ~200 LEs (3%) | 低(单 DCFIFO) |
EP4CE6 放一个 SDRAM 控制器基本就告别图像处理了。FIFO 直通方案利用了一个观察:
DVP 写入速度约 24MHz,VGA 读出速度约 25.175MHz,两者几乎相等。1024x16bit 的 DCFIFO 足够缓冲一行像素。
每帧开始时 fifo_aclr 清空 FIFO,保证帧同步,不需要帧缓冲。
决策 2:Sobel vs Canny
| 算法 | 乘法器 | 流水线复杂度 | 边缘效果 |
|---|---|---|---|
| Sobel | 0 | 4 级 | 清晰,对噪声有抑制 |
| Canny | 多个 | 7+ 级(高斯+NMS+双阈值+连通) | 最优但放不下 |
| Laplacian | 0 | 3 级 | 对噪声极敏感 |
EP4CE6 只有 15 个 9-bit DSP,Canny 的高斯滤波就要吃掉一半。Sobel 是资源-效果平衡的唯一选择。
决策 3:640x480 vs 更大分辨率
OV7725 原生输出就是 640x480。强行缩放只会浪费 LE。VGA 分辨率对于边缘检测演示完全足够,且 HDMI 640x480@60Hz 恰好是标准时序。
五、模块详解:零乘法器怎么做到的?
5.1 rgb2gray -- 移位加法替代乘法
BT.601 标准灰度公式:
Y = (R x 77 + G x 150 + B x 29) / 256
用移位加法展开:
// R*77 = R*(64+8+4+1) = R<<6 + R<<3 + R<<2 + R
// G*150 = G*(128+16+4+2) = G<<7 + G<<4 + G<<2 + G<<1
// B*29 = B*(16+8+4+1) = B<<4 + B<<3 + B<<2 + B
wire [14:0] r_weight = (r << 6) + (r << 3) + (r << 2) + r;
wire [15:0] g_weight = (g << 7) + (g << 4) + (g << 2) + (g << 1);
wire [12:0] b_weight = (b << 4) + (b << 3) + (b << 2) + b;
wire [16:0] lum_sum = r_weight + g_weight + b_weight;
wire [7:0] gray = lum_sum[15:8]; // 除以 256 = 取高 8 位
零个乘法器,一个 cycle 出结果。
5.2 line_buffer_3x3 -- 双行 RAM + 移位寄存器
3x3 滑动窗口是整个 Pipeline 中最复杂的部分。设计方案:
col_cnt ->
+----+----+----+----+-----
| P0 | P1 | P2 | P3 | ...
+----+----+----+----+-----
line0 RAM -> 上一行的像素缓存 (640x8bit)
line1 RAM -> 再上一行的像素缓存
当前行 -> 移位寄存器 [P_cur, P_prev1, P_prev2]
输出 3x3 窗口:
w00=line1[col] w01=line1[col-1] w02=line1[col-2]
w10=line0[col] w11=line0[col-1] w12=line0[col-2]
w20=shift[0] w21=shift[1] w22=shift[2]
双行 RAM 在 AC620 上被 Quartus 自动推断为 M9K Block RAM,不消耗 LE。
line_sel 信号交替标记当前写入的行,避免复杂的指针管理:
// 写入当前行
if (!line_sel)
line0[col_cnt] <= gray_in;
else
line1[col_cnt] <= gray_in;
// 行尾切换
if (col_cnt == IMG_WIDTH - 1) begin
col_cnt <= 0;
line_sel <= ~line_sel;
end
5.3 sobel_3x3 -- 核心亮点
标准 Sobel 核:
Gx = | -w00 + w02 - 2*w10 + 2*w12 - w20 + w22 |
Gy = | -w00 - 2*w01 - w02 + w20 + 2*w21 + w22 |
G = sqrt(Gx^2 + Gy^2) -> 近似为 |Gx| + |Gy| (L1 范数)
两个核心技巧:
- 乘以 2 = 左移 1 位 -- 标准 Sobel 核里唯一的乘法用移位替代
- L1 范数替代 L2 范数 -- sqrt(Gx^2 + Gy^2) 需要乘法器和开方 IP,|Gx| + |Gy| 只需要加法器和取绝对值(符号位判断)
// Gx = (w02 + 2*w12 + w22) - (w00 + 2*w10 + w20)
wire [9:0] gx_pos = w02 + (w12 << 1) + w22;
wire [9:0] gx_neg = w00 + (w10 << 1) + w20;
wire [9:0] gx_raw = gx_pos - gx_neg;
wire [9:0] gx_abs = (gx_raw[9]) ? (~gx_raw + 1'b1) : gx_raw;
// Gy = (w20 + 2*w21 + w22) - (w00 + 2*w01 + w02)
wire [9:0] gy_pos = w20 + (w21 << 1) + w22;
wire [9:0] gy_neg = w00 + (w01 << 1) + w02;
wire [9:0] gy_raw = gy_pos - gy_neg;
wire [9:0] gy_abs = (gy_raw[9]) ? (~gy_raw + 1'b1) : gy_raw;
// L1 norm: G = |Gx| + |Gy|
wire [10:0] grad = gx_abs + gy_abs;
梯度范围 0~2040(255x4 + 255x4 = 2040),占用 11bit。取绝对值用符号位判断 ~x + 1,比调用 LPM_ABS IP 更简洁。
5.4 threshold -- 阈值二值化
binary_out <= (grad_in > {3'd0, thresh}) ? 8'd255 : 8'd0;
默认阈值 64(可配置),11bit 梯度与 8bit 阈值对齐。后续可以升级为 Otsu 自适应阈值。
5.5 top -- 流水线串联
四级流水通过 pixel_valid 信号精确对齐:
rgb2gray (.pixel_valid_in(data_valid), .pixel_valid_out(gray_valid));
line_buf (.pixel_valid_in(gray_valid), .pixel_valid_out(window_valid));
sobel (.pixel_valid_in(window_valid), .pixel_valid_out(grad_valid));
threshold(.pixel_valid_in(grad_valid), .pixel_valid_out(final_valid));
每一级的 valid 信号精确传递,保证流水线各级数据对齐。不需要复杂的握手协议。
六、仿真验证
6.1 测试环境
| 配置项 | 值 |
|---|---|
| 仿真工具 | Icarus Verilog 12.0 + Python 3.10 (参考对比) |
| 测试图像 | 32x32 像素,5 种边缘模式 |
| 阈值 | 64/255 |
| 时钟 | 50MHz |
6.2 测试图像设计
+------------+------------+
| 垂直边界 | 水平边界 | Row 0~15
| (V-edge) | (H-edge) |
+------------+------------+
| 45度对角线 | L形拐角 | Row 16~31
| (Diag) | (L-corn) |
+------------+------------+
中心: 十字交叉线
这张 32x32 测试图覆盖了图像处理中最常见的五种边缘形态,比单条水平线或垂直线更能暴露流水线 bug。
6.3 仿真结果
| 区域 | 总像素 | 边缘像素 | 边缘占比 | 判定 |
|---|---|---|---|---|
| A. 垂直边界 | 225 | 6 | 2.7% | 通过 |
| B. 水平边界 | 225 | 4 | 1.8% | 通过 |
| C. 对角线 | 225 | 28 | 12.4% | 通过 |
| D. L形拐角 | 225 | 35 | 15.6% | 通过 |
| 总计 | 1024 | 79 | 7.7% | 通过 |
Python/SciPy 参考结果与 Verilog 仿真输出一致,Pipeline 工作正常。
6.4 仿真运行日志节选
Time=1590000ns valid_out=1 binary=000 gray=000 <- 平坦区
Time=5710000ns valid_out=1 binary=255 gray=255 <- 边缘检测到
Time=5750000ns valid_out=1 binary=000 gray=000 <- 平坦区
binary=255 即边缘检测成功。
七、资源消耗实测
Quartus 13.0 Full Compilation 完整编译结果:
Fitter Status : Successful
Device : EP4CE6F17C8
Family : Cyclone IV E
| 资源 | 总量 | 已用 | 占用率 |
|---|---|---|---|
| Logic Elements | 6,272 | 923 | 15% |
| Combinational Functions | 6,272 | 729 | 12% |
| Dedicated Registers | 6,272 | 521 | 8% |
| Memory Bits | 276,480 | 30,720 | 11% |
| Embedded Multipliers | 30 | 0 | 0% |
| PLLs | 2 | 2 | 100% |
| Pins | 180 | 26 | 14% |
解读
- 923 个 LEs (15%) -- 剩下 85% 的资源,足够加直方图均衡、Gamma 校正、甚至第二个视频通道
- 0 个 DSP 乘法器 -- Sobel Pipeline 本身一个乘法器都没碰
- 11% Memory Bits -- 行缓冲用了 4 个 M9K (2行 x 640x8bit),剩余 RAM 非常充裕
- 2/2 PLL -- 全部用完,一个给系统时钟 (50M+24M),一个给 HDMI (25.175M+125.875M)
方案对比
| 方案 | LEs | DSP | Memory |
|---|---|---|---|
| SDRAM + Canny (估算) | ~5,000+ | ~12 | ~200Kb |
| SDRAM + Sobel (估算) | ~3,300 | 0 | ~100Kb |
| 本设计: FIFO + Sobel | 923 | 0 | 30Kb |
用 SDRAM 方案做 Sobel 是别人两倍的资源消耗,做 Canny 直接放不下。本设计用纯粹的数据流思路,把入门级 FPGA 的每一份资源用到了刀刃上。
八、写在最后
这个项目教会我的
-
"能跑"和"用最少资源跑"是两回事。把 Sobel 跑起来,CS 专业的学生用 Python 加 OpenCV 五行代码就行。但在一个只有 6,272 LEs 的芯片上,把摄像头采集、跨时钟域、流水线处理、HDMI 输出全塞进去,且一个乘法器不动 -- 这才是 FPGA 工程师的价值。
-
移位操作是 FPGA 世界里最便宜的东西。乘以 2 =
<<1,除以 256 = 取高位。掌握了这些基本功,很多复杂运算都能化简。 -
仿真先行。用 Python 生成测试图 -> Icarus Verilog 仿真 -> Python 对比验证,这条链路跑通后,上板只是最后一步。
更多推荐



所有评论(0)