第三章 Verilog基础与图像数据流
说实话,很多初学者一上来就盯着Verilog语法看,什么阻塞赋值非阻塞赋值,背得滚瓜烂熟。但真到了写图像处理模块的时候,连个像素时钟都分不清。我个人觉得,学Verilog不是为了考试,是为了描述硬件。图像数据流,恰恰是检验你Verilog功底最好的试金石。
3.1 Verilog语法速览:够用就行
我不打算给你列一本语法手册。那玩意儿网上到处都是。我只说图像处理里最常用的几个点。
3.1.1 模块化设计的基本骨架
每个模块,说白了就是一个黑盒子。输入进去,输出出来。我习惯这么写:
module image_proc (
input wire clk, // 像素时钟
input wire rst_n, // 异步复位,低有效
input wire vsync, // 场同步
input wire hsync, // 行同步
input wire de, // 数据使能
input wire [7:0] pixel_in, // 输入像素
output reg [7:0] pixel_out // 输出像素
);
// 你的逻辑写在这里
endmodule
注意看,我用了 rst_n 而不是 rst。为什么?因为大部分FPGA开发板,复位按键按下时输出的是低电平。你想想看,如果非要用高电平复位,你还得在外面加个反相器,多此一举。
3.1.2 阻塞与非阻塞:一个老生常谈的问题
我在项目中遇到过不止一次,有人把组合逻辑和时序逻辑的赋值搞混了。记住一句话:组合逻辑用阻塞赋值(=),时序逻辑用非阻塞赋值(<=)。这是铁律,别问为什么。
举个例子,你要做一个像素延迟一拍:
// 正确写法
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
pixel_dly <= 8'd0;
else
pixel_dly <= pixel_in;
end
如果你写成 pixel_dly = pixel_in,仿真可能没问题,但综合出来的电路大概率会出bug。嗯,这里要注意。
3.2 同步复位 vs 异步复位
这个问题,面试的时候经常被问到。我直接说结论:图像处理模块,我推荐用异步复位。
| 类型 | 优点 | 缺点 |
|---|---|---|
| 同步复位 | 抗毛刺,时序分析简单 | 需要时钟才能复位,复位信号宽度必须大于一个时钟周期 |
| 异步复位 | 立即生效,不依赖时钟 | 容易产生亚稳态,需要做复位同步 |
为什么图像处理推荐异步复位?因为图像数据流是连续不断的。如果复位要等时钟沿,那第一个像素可能已经过去了,你还没复位完。我曾经在一个项目中,就因为用了同步复位,导致上电后第一帧图像总是错位的。后来改成异步复位,问题立刻解决。
当然,异步复位也有坑。最常见的坑是复位释放时的亚稳态。我的做法是加一个复位同步器:
reg rst_sync1, rst_sync2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rst_sync1 <= 1'b0;
rst_sync2 <= 1'b0;
end
else begin
rst_sync1 <= 1'b1;
rst_sync2 <= rst_sync1;
end
end
// 然后用 rst_sync2 作为模块内部的复位信号
这样既保留了异步复位的快速响应,又解决了亚稳态问题。
3.3 图像数据流的硬件描述
这才是本章的重点。图像数据流,说白了就是三个信号:像素时钟(pclk)、行同步(hsync)、场同步(vsync),再加上一个数据使能(de)。
3.3.1 像素时钟
像素时钟是图像数据流的脉搏。每个时钟沿,传输一个像素。比如一个1080p60的视频,像素时钟大约是148.5MHz。你想想看,每个时钟周期只有6.7纳秒,你的逻辑必须在这段时间内完成所有运算。
我见过有人把像素时钟和系统时钟混用。结果呢?时序跑不过,图像花屏。记住:图像处理模块,一定要用像素时钟域。
3.3.2 行同步与场同步
行同步表示一行开始了。场同步表示一帧开始了。但不同协议,这两个信号的有效电平可能不一样。有的高有效,有的低有效。我建议你写模块的时候,加一个参数来控制:
parameter HSYNC_ACTIVE = 1'b1; // 行同步有效电平
parameter VSYNC_ACTIVE = 1'b1; // 场同步有效电平
这样换一个摄像头或者显示器,改参数就行,不用改代码。
3.3.3 数据使能(de)
这个信号太重要了。它告诉你,当前时钟沿上的像素数据是有效的。为什么需要它?因为行同步和场同步之间,还有消隐区。消隐区的数据是无效的,你不能把它当成像素来处理。
我刚开始做图像处理的时候,就犯过这个错。直接拿行同步来当数据有效信号,结果图像边缘多了一堆黑边。后来才明白,必须用de来判断。
3.4 实战:一个简单的像素延迟模块
说了这么多,我们来写一个实际的模块。这个模块的功能很简单:把输入像素延迟一行。
module line_buffer (
input wire clk,
input wire rst_n,
input wire de_in,
input wire [7:0] pixel_in,
output reg [7:0] pixel_out,
output reg de_out
);
// 行缓存,深度为1920(假设一行1920个像素)
reg [7:0] line_mem [0:1919];
reg [10:0] wr_addr, rd_addr;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_addr <= 11'd0;
rd_addr <= 11'd1; // 读地址比写地址快一拍
de_out <= 1'b0;
end
else begin
if (de_in) begin
line_mem[wr_addr] <= pixel_in;
pixel_out <= line_mem[rd_addr];
de_out <= 1'b1;
wr_addr <= wr_addr + 1'b1;
rd_addr <= rd_addr + 1'b1;
end
else begin
de_out <= 1'b0;
end
end
end
endmodule
这个模块的核心思想是:写地址和读地址相差1。这样,当写入第N个像素时,读出的就是第N-1个像素,也就是上一行的对应位置。嗯,说白了就是一个简单的行缓存。
重要提示:上面的代码只是一个示例。实际项目中,行缓存通常用FPGA内部的Block RAM来实现,而不是用寄存器数组。Block RAM的读写时序和寄存器不一样,需要额外处理。
3.5 本章知识体系
下面这张图,是我自己画的。它把本章的核心知识点串在了一起。你可以把它当成一个思维导图来看。
我的小技巧:刚开始写图像处理模块的时候,不要急着上复杂算法。先把数据流打通。你写一个模块,输入de_in,输出de_out,中间延迟几拍。然后用示波器或者逻辑分析仪看波形。de信号对不对,一眼就能看出来。
注意:不同厂家的FPGA,对复位信号的处理可能不一样。Xilinx推荐同步复位,Altera推荐异步复位。但我的经验是,在图像处理这种高速数据流场景下,异步复位加同步器是最稳妥的方案。
好了,这一章的内容就到这里。Verilog语法只是工具,图像数据流才是灵魂。你把这些基础打牢了,后面学什么滤波、缩放、边缘检测,都会轻松很多。
可可精品资料库