3. Verilog基础与图像数据表示
好,咱们进入第三章。这一章我打算带大家快速过一遍Verilog语法,然后重点聊聊图像数据在FPGA里到底长什么样。说实话,很多初学者一上来就写图像算法,结果连像素的二进制表示都没搞明白,最后仿真出来一片花屏——我当年就干过这种事。
3.1 Verilog语法速通——够用就行
我不打算把Verilog当C语言讲一遍。咱们是做图像加速的,不是写CPU。你只需要掌握几个核心语法点,就能写出高效的图像处理模块。
3.1.1 模块与端口
每个Verilog设计都以module开头,以endmodule结束。端口就是模块的“手脚”——数据从哪进,从哪出。
module rgb2gray(
input wire clk,
input wire rst_n,
input wire [7:0] r,
input wire [7:0] g,
input wire [7:0] b,
output reg [7:0] gray
);
// 内部逻辑
endmodule
我个人习惯把输入输出分开写,这样代码结构清晰。你想想看,如果所有端口挤在一行,后期维护时眼睛都要看花。
3.1.2 组合逻辑 vs 时序逻辑
这是FPGA设计的核心概念。组合逻辑就是“输入一变,输出马上变”,没有记忆功能。时序逻辑则依赖时钟,在时钟沿到来时才更新。
做图像处理时,我建议你记住一条黄金法则:像素数据的运算用组合逻辑,像素数据的缓存用时序逻辑。
// 组合逻辑:计算灰度值
wire [7:0] gray_comb;
assign gray_comb = (r + g + b) / 3;
// 时序逻辑:在时钟沿输出
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
gray <= 8'd0;
else
gray <= gray_comb;
end
assign,在时序逻辑里用 always @(posedge clk)。别混着用,否则仿真时容易出“毛刺”,我曾经被这个坑过一整天。
3.1.3 阻塞赋值与非阻塞赋值
这个知识点,我敢说90%的初学者都栽过跟头。简单来说:
- 阻塞赋值(=):用在组合逻辑中,顺序执行
- 非阻塞赋值(<=):用在时序逻辑中,并行执行
为什么会这样?因为FPGA里的寄存器是并行更新的。如果你在时序逻辑里用了阻塞赋值,仿真结果可能对,但综合出来的电路会多出很多不必要的触发器——说白了就是浪费资源。
3.2 图像像素的二进制表示
图像在FPGA里就是一堆二进制数。一个像素点,在计算机里可能是24位(RGB各8位),但在FPGA里,我们得精打细算。
3.2.1 灰度图像的二进制表示
灰度图像每个像素用8位表示,取值范围0~255。0代表纯黑,255代表纯白。在Verilog里,我们这样定义:
reg [7:0] pixel_gray; // 8位灰度像素
8位能表示256个灰度级。你可能会问:够用吗?对于大多数工业视觉应用,256级足够了。但如果你做医学图像处理,可能需要10位或12位——嗯,这个后面会讲到。
3.2.2 彩色图像的二进制表示
彩色图像通常用RGB三通道表示,每个通道8位,共24位。在Verilog里,我们可以这样组织:
reg [23:0] pixel_rgb; // 24位彩色像素
// 或者拆开
reg [7:0] r, g, b;
我个人习惯把RGB拆开处理,这样代码可读性更好。你想想看,如果写pixel_rgb[23:16],别人一看就知道是R通道,多清爽。
3.3 RGB与灰度转换原理
这是图像处理里最基础的算法之一。把彩色图像变成灰度图,说白了就是扔掉颜色信息,只保留亮度信息。
3.3.1 转换公式
常用的转换公式有三种:
| 方法 | 公式 | 特点 |
|---|---|---|
| 平均值法 | Gray = (R + G + B) / 3 | 简单,但不符合人眼感知 |
| 加权平均法 | Gray = 0.299R + 0.587G + 0.114B | 符合人眼对亮度的敏感度 |
| 最大值法 | Gray = max(R, G, B) | 保留最亮通道,常用于特殊效果 |
在FPGA里,我推荐用加权平均法。但浮点数运算太耗资源,怎么办?我们可以用整数近似:
// 加权平均法的整数实现
// Gray = (77*R + 150*G + 29*B) >> 8
assign gray = (77 * r + 150 * g + 29 * b) >> 8;
这里用了移位操作代替除法,效率高得多。我在项目中测试过,这个近似公式的误差不到1%,人眼根本看不出来。
3.3.2 Verilog实现
下面是一个完整的RGB转灰度模块:
module rgb2gray(
input wire clk,
input wire rst_n,
input wire [7:0] r,
input wire [7:0] g,
input wire [7:0] b,
output reg [7:0] gray
);
wire [15:0] sum;
assign sum = 77 * r + 150 * g + 29 * b;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
gray <= 8'd0;
else
gray <= sum[15:8]; // 取高8位,相当于右移8位
end
endmodule
3.4 行缓存与帧缓存概念
这是FPGA图像处理的核心概念。很多算法(比如滤波、边缘检测)都需要同时访问多个像素,但摄像头是一行一行传数据的,怎么办?答案就是缓存。
3.4.1 行缓存(Line Buffer)
行缓存用来存储一行或几行像素数据。比如做3×3的滤波,你需要同时访问当前行、上一行和上上行。这时候就需要两行缓存。
在FPGA里,行缓存通常用Block RAM(BRAM)实现。BRAM是FPGA内部的专用存储资源,读写速度快,适合做行缓存。
// 简单的行缓存实现(使用移位寄存器)
reg [7:0] line_buf [0:1919]; // 存储一行1920个像素
reg [7:0] pixel_d1, pixel_d2; // 延迟两个时钟的像素
always @(posedge clk) begin
pixel_d1 <= pixel_in;
pixel_d2 <= pixel_d1;
end
我个人习惯用Xilinx的FIFO IP核来实现行缓存,这样更稳定。但如果你用的是小规模FPGA,自己写移位寄存器也能凑合。
3.4.2 帧缓存(Frame Buffer)
帧缓存用来存储一帧完整的图像。比如做运动检测,你需要比较当前帧和上一帧的差异,那就得把上一帧存下来。
帧缓存需要的存储空间很大。以1080p图像为例,一帧需要1920×1080×3字节 ≈ 6MB。FPGA内部的BRAM通常不够用,这时候就得外挂DDR内存。
- 行缓存:用BRAM实现,延迟小(几个时钟周期),适合实时处理
- 帧缓存:用DDR实现,延迟大(几十微秒),适合帧间处理
3.4.3 什么时候用行缓存,什么时候用帧缓存?
我总结了一个简单的判断方法:
- 如果算法只需要相邻几行像素(如3×3滤波、Sobel边缘检测),用行缓存
- 如果算法需要整帧图像(如直方图均衡、帧差法),用帧缓存
- 如果算法既需要局部信息又需要全局信息(如自适应阈值),两者都用
嗯,这里要注意:行缓存和帧缓存不是互斥的。很多复杂的图像处理系统,既有行缓存做实时处理,又有帧缓存做后处理。
3.5 本章知识体系
下面这张图展示了本章的核心逻辑关系,我特意用SVG画出来,方便你理解:
这张图把本章的三个核心模块串起来了。你从左往右看:先掌握Verilog语法这个工具,然后理解图像数据在FPGA里怎么表示,最后学会用缓存来组织数据流。说白了,这就是一个从“怎么写代码”到“怎么处理数据”再到“怎么设计系统”的递进过程。
好了,第三章的内容就到这里。记住,Verilog语法只是手段,图像数据才是我们真正要处理的对象。下一章我们会把这些知识用起来,开始写真正的图像处理模块。