第三章 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 本章知识体系

下面这张图,是我自己画的。它把本章的核心知识点串在了一起。你可以把它当成一个思维导图来看。

Verilog与图像数据流 Verilog语法速览 模块化设计骨架 阻塞 vs 非阻塞赋值 复位策略 同步复位 vs 异步复位 复位同步器设计 图像数据流 像素时钟 (pclk) 行同步 (hsync) 场同步 (vsync) 数据使能 (de) 实战:行缓存模块设计

我的小技巧:刚开始写图像处理模块的时候,不要急着上复杂算法。先把数据流打通。你写一个模块,输入de_in,输出de_out,中间延迟几拍。然后用示波器或者逻辑分析仪看波形。de信号对不对,一眼就能看出来。

注意:不同厂家的FPGA,对复位信号的处理可能不一样。Xilinx推荐同步复位,Altera推荐异步复位。但我的经验是,在图像处理这种高速数据流场景下,异步复位加同步器是最稳妥的方案。

好了,这一章的内容就到这里。Verilog语法只是工具,图像数据流才是灵魂。你把这些基础打牢了,后面学什么滤波、缩放、边缘检测,都会轻松很多。

可可精品资料库