基于FPGA的数字视频信号处理器设计(中)

2020-12-30 11:04:53 浏览数 (1)

大侠好,欢迎来到FPGA技术江湖,江湖偌大,相见即是缘分。大侠可以关注FPGA技术江湖,在“闯荡江湖”、"行侠仗义"栏里获取其他感兴趣的资源,或者一起煮酒言欢。

今天给大侠带来基于FPGA的数字视频信号处理器设计,由于篇幅较长,分三篇。今天带来第二篇,中篇,视频信号概述和视频信号处理的框架。话不多说,上货。

之前也有图像处理相关方面的文章,这里超链接几篇,给各位大侠作为参考。

《冈萨雷斯数字图像处理MATLAB版》中文版(第二版) 电子版

荐读:FPGA设计经验之图像处理

基于FPGA的实时图像边缘检测系统设计(下)

FPGA设计中 Verilog HDL实现基本的图像滤波处理仿真

导读

图像是用各种观测系统以不同形式和手段观测客观世界而获得的,可以直接或间接作用于人眼进而产生视知觉的实体。

随着电子技术和计算机技术的飞速发展,数字图像技术近年来得到极大的重视和长足的发展,并在科学研究、工业生产、医疗卫生、通信等方面得到广泛的应用。

视频信号由一系列连续的图像组成。对视频信号的处理已经成为数字图像处理领域中重要的一部分。例如机器人模式识别的过程就是一个视频信号处理的过程,电视制导导弹识别目标就是充分利用视频信号处理技术不断判断目标是否和预先设定目标图像一致。本篇将讲解如何用 FPGA 技术实现基本的视频信号处理。本篇的例子可以作为各位大侠进行视频信号处理时的一个参考,也可以在这个基础上根据需要进行扩展。

第二篇内容摘要:本篇会介绍视频信号处理的电路,包括中央控制器 FPGA、电视信号转换模块电路、图像缓存部分电路;还会介绍视频处理程序的具体实现,包括主体程序的实现、视频图像数据采集程序的实现以及SRAM 的读写控制等相关内容。

三、视频信号处理的电路

下面将详细介绍视频转换模块和视频数据计算模块的电路。

3.1 中央控制器 FPGA

FPGA 芯片作为中央控制器控制整个视频信号的处理,如图 6 所示。

图 6 FPGA 实现的主要功能

FPGA 芯片实现的主要功能如下:

• 提供电源管理逻辑。

• 提供系统状态指示灯的管理,这些指示灯用来显示当前各个部分的状态。

• 提供视频数据处理的管理。当开始采样时,FPGA 在此后到来的第一个帧同步信号到来时启动采样,并将这帧数据存放在 SRAM 中,采样结束后开始处理过程,同时开始采集下一幅图像并保存到另一块 SRAM 中。

• 模拟 I2C 接口,完成对 SAA7113 参数的配置。

3.2 电视信号转换模块电路

电视信号转换模块的电路设计如图 7 所示。

图 7 电视信号转换模块电路设计图

其中 SAA7113 的时钟信号由一片 24.576MHz的晶体提供,产生内部所需的 27MHz 的 LLC(line-locked system clock output,行锁定系统时钟)信号及其二分频信号 LLC2(频率 13.5MHz)。其中 LLC2 信号用来同步整个图像采集系统,即一个 LLC2 周期采集一个像素的图像数据,如图 8 所示。

图 8 一个 LLC2 周期采集一个像素数据

SAA7113 内部包括模拟电路和数字电路两部分。模拟电路部分具有对电视信号放大、抗混叠滤波(anti-alias filter)等功能,数字电路部分具有对模数转换以后图像数据各种参数的处理等功能。

1)输出数据的格式

摄像头输出的图像信号通过 RCA-JACK 插座连接到 SAA7113,经过 A/D 转换及其他相关处理后得到数字格式的图像数据。SAA7113 能提供以下输出格式的数据:

(1)标准的 ITU 656 YUV 4:2:2(8 位)格式的数据;

(2)增强的 ITU 656 标准格式的各种数据,如 active video、raw CVBS 等。

标准 ITU656 格式是 ITU(International Telecommunications Union,国际电信同盟)推荐的数字视频数据格式。按照 ITU 656 YUV 4:2:2 标准组成的数字电视信号如图 9 所示。

图 9 SAA7113 输出数字视频信号数据格式

YUV(亦称 YCrCb)是欧洲电视系统采用的一种颜色编码方法(属于 PAL 制)。采用 YUV 主要目的在于优化彩色视频信号的传输,使其在传输中占用较少的带宽,并能兼容老式黑白电视。如果直接采用 RGB 视频信号传输,将要求 RGB 3 个独立的视频信号同时传输,占用带宽要多得多。YUV 中“Y”表示明亮度(Luminance 或 Luma),也就是灰阶值;而“U”和“V”表示色度(Chrominance 或 Chroma),其作用是描述影像色彩及饱和度,用于指定像素的颜色。“亮度”是由 RGB 输入信号来创建的,方法是将 RGB 信号的特定部分叠加到一起。“色度”则定义了颜色的两个方面——色调与饱和度,分别用 Cr 和 Cb 来表示。其中,Cr 反映 RGB 输入信号中红色部分与 RGB 信号亮度值之间的差异,而 Cb 反映 RGB 输入信号蓝色部分与 RGB 信号亮度值之同的差异。

从 SAA7113 数字视频信号输出总线 VPO 输出的数据格式如图 9 所示。

“80 10”表示当前视频信号处于行消隐阶段。“FF 00 00 SAV”是时间参考代码,标志有效视频数据的开始。其中“SAV”(Start of Active Video)意思是有效视频数据的开始。“CB0Y0 CR0 Y1 CB2 Y2…CR718 Y719”是有效视频数据。“FF 00 00 EAV”是时间参考代码,标志有效视频数据的结束。其中“EAV”(End of Active Video)意思是有效视频数据的结束。“80 10”表示下一行视频数据的行消隐信号开始。

用下面公式可以把传输用的 ITU 656 YUV 格式的数据还原为需要的 RGB 格式:

2)SAA7113 的配置

使用 FPGA 模拟的 I2C 接口可以对 SAA7113 提供的多个控制字进行读写,从而完全控制SAA7113 的运行。对 SAA7113 控制字的写过程如图 10 所示。

图 10 对 SAA7113 控制字的写操作

I2C 协议中器件的地址是 7 位,加上读写位(写为“0”,读为“1”)构成一个完成的字节。SAA7113 的器件地址是 0100,101,加上读写位后:器件地址+写=0100 1010 即 4AH;器件地址+读=0100,1011 即 4BH。从地址是器件内部分配的地址。SAA7113 提供 00~1F、40~62 共43 个控制字。

SAA7113 读控制字的过程如图 11 所示。

图 11 对 SAA7113 控制的读操作

I2C 协议中的读操作比写操作复杂:首先完成对器件地址和从地址的写操作;然后发送一个 4BH(器件地址+读),可以开始接收数据;数据传输结束时发送终止信号。

整个 SAA7113 配置的流程如图 12 所示。

图 12 SAA7113 配置流程

应用 I2C 程序对 SAA7113 配置。具体可以参考之前的 I2C 相关文章,后续会更新模拟I2C配置程序,这里先超链接一篇,仅供参考。

源码系列:基于FPGA的 IIC 设计(附源工程)

3.3 图像缓存部分电路

FPGA 将采集到的图像数据保存到缓存中,为后端对图像的进一步处理提供数据。下面提供一种图像缓存的设计方案:采用两块 SRAM(型号为 CY7C1049)作为图像缓存,FPGA 把从SAA7113 接收的一帧图像的数据保存到 SRAM 中,同时后端部分的处理器如 DSP 等可以从另一块 SRAM 中读取数据进行处理,电路如图 13 所示。

图 13 SRAM 的电路图

第一次采样时,FPGA 将从 SAA7113 接收到的一帧图像数据保存到 SRAM 中,此时 DSP 在等待;第一次采样结束后,DSP 与 FPGA 进行总线切换,分别连接到与上次不同的 SRAM 上,DSP开始读取数据,FPGA 开始采集数据。每当 DSP 和 FPGA 都完成各自的任务时,就进行总线切换,交换连接的 SRAM。

四、视频处理程序的具体实现

4.1 主体程序的实现

FPGA 整体控制程序的流程如图 14 所示。

图 14 FPGA 整体控制程序流程图

整体控制过程中有关状态机的代码如下:

代码语言:javascript复制
//状态机
always @(posedge clk or negedge BRD_RST_)
    //缺省状态
    if (!BRD_RST_)
        begin
            presState <= stIdle;
            capture <= 1'b0;
            toggle <= 1'b1;
            dsprst <= 1'b0;
            EXT_INT4 <= 1'b0;
        end
    else
        begin
            case (presState)
              //初始状态
              stIdle:
                  //DSP 初始化完成
                  if(DSP_INIT_FLAG)
                      begin
                          presState <= stWaitforDspInit;
                      end
                  else
                          presState <= stIdle;
              //等待 SAA7113 初始化
                stWaitforDspInit:
                    if(setup_flag && !saareset)
                        begin
                            capture <= 1'b1;
                            presState <= stStartGrabData;
                        end
                    else
                        begin
                            presState <= stWaitforDspInit;
                        end
            //开始采集数据
                stStartGrabData://
                    if(!SAA_DATA_PRE)
                        begin
                            presState <= stToggleBus;
                            capture <= 1'b0;
                            EXT_INT4 <= 1'b0;
                        end
                    else
                        presState <= stStartGrabData;
            //切换总线
                stToggleBus://
                    if(!DSP_DATA_PRE)
                        begin
                            toggle <= !toggle;
                            presState <= stStartGrabData;
                            capture <= 1'b1;
                            //外部中断给 DSP
                            EXT_INT4 <= 1'b1;
                        end
                    else
                        presState <= stToggleBus;
            endcase
        end

4.2 视频图像数据采集程序的实现

SAA7113 输出的视频图像数据通过 8 位总线 VPO 传输给 FPGA,FPGA 需要将数据保存到 SRAM中。由于 PAL 制电视信号是隔行扫描,分为奇数场和偶数场分别传输,数字化以后仍然格式不变,因此在将数据保存到 SRAM 以前,需要将奇数场和偶数场的数据还原成一幅完整的图像。

如果传输格式是 YUV,需要转化为便于 DSP 处理的 RGB 格式。FPGA 需要完成如下任务:

(1)将所有按行排列的 8 位数据还原成一幅完整图像;

(2)根据需要进行格式转换。

SAA7113 输出的数据格式如图 15 所示。

图 15 SAA7113 输出视频数据格式

SAV 和 EAV 分别是“有效视频数据的开始”(Start of Active Video)和“有效视频数据的结束”(End of Active Video)。SAA7113 对 SAV 和 EAV 数据格式的定义如表 1 所示。

表 1 SAV 和 EAV 数据格式

从表 1 中可以看到在完整的一帧图像数据中第一场场消隐阶段 SAV 为“101XXXXX”,第一场有效数据阶段 SAV 为“1000XXXX”。“X”表示该位的状态没有作用。其他场 SAV 和 EAV 状态类推。

进行视频数据处理的流程如图 16 所示。

图 16 视频数据处理流程图

数字视频数据处理的主要代码如下:

代码语言:javascript复制
//SAA7113 输出的 27MHz 的时钟信号
always @(posedge llck or negedge reset)
    //设置 reset 后的缺省状态
    if (!reset)
        begin
            //下一状态
            presState <= stIdle;
            //采集标志
            grab <= 1'b0;
            //和下一状态相关的返回状态
            returnState <= stIdle;
            //场标志
            field <= 1'b0;
            //像素计数器和行计数器清零
            grab_cntr_hori <= 1'b0;
            grab_cntr_vert <= 1'b0;
        end
    else
        //缺省状态
        begin
            //保存视频数据
            vpoLatch <= vpo;
            
            //进入下一状态
            presState <= nextState;
           
            //用来设置延时
            grab <= nextGrab;//delay so colour can be calculated
            
            //设置和下一状态相关的返回状态
            returnState <= nextReturnState;
            
            //保存场信号
            field <= nextField;
            
            //保存各个数据分量
            chrominanceR <= nextChrominanceR;
            chrominanceB <= nextChrominanceB;
            luminanceR <= nextLuminanceR;
            luminanceB <= nextLuminanceB;
            
            //如果下一个状态是获取数据的状态,准备写数据到 SRAM 的操作
            if (nextGrab)
                //给予写地址一些建立时间
                write_addr <= grab_addr;
                
            //如果当前状态就是获取数据,开始写数据到 SRAM 的操作
            if (grab)
                begin
                    //由于是对黑白图像进行处理,只需要获取亮度信息即可;如果需要对彩色图像进行处理,可以在这里加入保存彩色图像数据的代码
                    writeData <= luminanceR;
                    write <= 1'b1;
                end
            else
                write <= 1'b0;
                
            // 像素计数和行计数
            //开始计数
            if (clr_grab_cntr)
                begin
                    grab_cntr_hori <= 1'b0;
                    grab_cntr_vert <= 1'b0;
                end
            else
                //像素计数
                if (inc_grab_hori == 1)
                    grab_cntr_hori <= grab_cntr_hori   1;
                //行计数
                if (inc_grab_vert == 1)
                    begin
                        grab_cntr_vert <= grab_cntr_vert   1;
                        grab_cntr_hori <= 1'b0; // clear horizontal counter
                        with each new line
                    end
            end
                
//从 vpo 总线获得数据
always @(presState or vpoLatch or returnState or field or luminanceB or luminanceRor chrominanceB or chrominanceR or capture)
    begin
        //缺省信号值
        clr_grab_cntr <= 1'b0;
        inc_grab_hori <= 1'b0;
        inc_grab_vert <= 1'b0;
        nextGrab <= 1'b0;
        nextReturnState <= returnState;
        nextField <= field;
        nextLuminanceB <= luminanceB;
        nextLuminanceR <= luminanceR;
        nextChrominanceB <= chrominanceB;
        nextChrominanceR <= chrominanceR;
        error <= 1'b0;
        //状态机
        case (presState)
            stIdle://
                //确认开始获取数据
                if (capture == 1'b1)
                    begin
                        //下一状态
                        nextState <= stWaitForEscape;
                        //下一状态的返回状态
                        nextReturnState <= stCheckForNewPage;
                    end
                else
                    nextState <= stIdle;
                    
                //等待时间参考代码的开始
                //等待“FF”
                stWaitForEscape:
                    if (vpoLatch == 8'hFF)
                        nextState <= stCheckEscape1;
                    else
                        nextState <= stWaitForEscape;
                        
                //等待“00”
                stCheckEscape1:
                    if (vpoLatch == 8'h00)
                        nextState <= stCheckEscape2;
                    else
                        nextState <= stError;
                        
                //等待“00”
                stCheckEscape2:
                    if (vpoLatch == 8'h00)
                        nextState <= returnState;
                    else
                        nextState <= stError;
                
                //根据 SAV 和 EAV 内容判断,进行下一步操作
                stCheckForNewPage:
                //开始获取新的一行
                    if (vpoLatch[6:5] == 2'b01)
                        begin
                            nextState <= stWaitForEscape;
                            nextReturnState <= stCheckForFirstLine;
                            //初始化计数器
                            clr_grab_cntr <= 1'b1;
                        end
                    //重新开始
                    else
                        begin
                            nextState <= stWaitForEscape;
                            nextReturnState <= stCheckForNewPage;
                        end
                        
                //根据 SAV 和 EAV 的内容进行下一步操作
                stCheckForFirstLine:
                    //开始接收数据
                    if (vpoLatch[6:4] == 3'b000)
                        begin
                            //接收 Cb 数据
                            nextState <= stChromaBlue;
                            //初始化场记录标志
                            nextField <= 1'b0;
                        end
                    //继续等待
                    else
                        begin
                            nextState <= stWaitForEscape;
                            nextReturnState <= stCheckForFirstLine;
                        end  
                        
              //记录 Cb 数据
              stChromaBlue:
                  //如果数据是“FF”,说明是 EAV 的开始
                  if (vpoLatch == 8'hFF)
                      begin
                          //下一状态开始等待“00”
                          nextState <= stCheckEscape1;
                          //检查是否为当前一场数据的最后一行
                          nextReturnState <= stCheckForEndLine;//Check if this  is the last line of the field
                      end
                  //如果是“00”,状态有错
                  else if (vpoLatch == 8'h00)
                      nextState <= stError;
                  //记录数据,并进入后面的过程
                  else
                      begin
                          nextState <= stLumaBlue;
                          nextChrominanceB <= vpoLatch;
                      end
                      
            //记录 Yb 数据
            stLumaBlue:
                //如果数据不等于“FF”和“00”,数据有效
                if ((vpoLatch !== 8'hFF) && (vpoLatch !== 8'h00))
                    begin
                        nextState <= stChromaRed;
                        nextLuminanceB <= vpoLatch;
                    end
                //错误处理
                else
                    nextState <= stError;
            
            //记录 Cr 数据
            stChromaRed:
                //如果不等于“FF”和“00”,数据有效
                if ((vpoLatch !== 8'hFF) && (vpoLatch !== 8'h00))
                    begin
                        nextState <= stLumaRed;
                        nextChrominanceR <= vpoLatch;
                    end
                //错误处理
                else
                    nextState <= stError;
                    
            //记录 Yr 数据
            stLumaRed:
                //如果不等于“FF”和“00”,数据有效
                if ((vpoLatch !== 8'hFF) && (vpoLatch !== 8'h00))
                    begin
                        nextState <= stChromaBlue;
                        nextLuminanceR <= vpoLatch;
                        nextGrab <= 1'b1;//Set up a write after a delay (see clocked process)
                        inc_grab_hori <= 1'b1;//Increment horizontal counter every two pixels
                    end
                //错误处理
                else
                  nextState <= stError;
                  
              //检查是否为一行的结束
              stCheckForEndLine://possible conditions here are the end of field 0,end of field 1,or an EAV code indicating a new line in the active region.
                  //等于“111”说明为第二场的结束
                  if (vpoLatch[6:4] == 3'b111)
                      begin
                          nextState <= stIdle;
                      end
                      
                 //等于“011”说明为第一场的结束
                else if (vpoLatch[6:4] == 3'b011) //end of field 0
                    begin
                        //清空计数器,为接收第二场数据做好准备
                        clr_grab_cntr <= 1'b1;
                        nextState <= stWaitForEscape;
                        //检查新的一行开始
                        nextReturnState <= stCheckForNewLine;
                    end
                    
                //一行数据结束
                else if (vpoLatch[5:4] == 2'b01)
                    begin
                        //下一行的开始
                        inc_grab_vert <= 1'b1;
                        nextState <= stWaitForEscape;
                        //接收下一行数据
                        nextReturnState <= stCheckForNewLine;
                    end
                //错误处理
                else
                    nextState <= stError;

            //新的一行数据开始
            stCheckForNewLine:
                if (vpoLatch[5:4] == 2'b00)
                    begin
                        //接收 Cb 数据
                        nextState <= stChromaBlue;
                        nextField <= vpoLatch[6];
                    end
                //重新开始接收数据
                else
                    begin
                        nextState <= stWaitForEscape;
                        nextReturnState <= stCheckForNewLine;
                    end
            
            //错误状态
            stError:
                //重新开始接收数据
                if (capture == 1'b1)
                    begin
                        nextState <= stWaitForEscape;
                        nextReturnState <= stCheckForNewPage;
                    end
                    //继续等待
                    else
                        begin
                            nextState <= stError;
                            //错误提示,二极管亮
                            error <= 1'b1;
                        end
            //缺省状态
            default:
                nextState <= stError;
        endcase
    end                  

4.3 SRAM 的读写控制

SRAM 在读写上有严格的时序要求,用WE、OE、CE 三个信号控制完成写数据,具体时序如图 17所示。

图 17 SRAM 的写时序

具体过程是:首先输出并保持地OE 址信号,然后片选信号CE置低,同时把输出有效信号OE 置高,最后把写有效信号WE 置低,并开始写数据。控制 SRAM 写数据的程序如下:

代码语言:javascript复制
//写操作发生在当前状态为 stWrite1 并且处于时钟的低电平时
//第一个时钟周期里开始一个写操作,OE 信号置为高电平。这是为了
//使 SRAM 停止驱动双向数据线,使其数据线处于高阻态。
//在等待半个时钟周期后,FPGA 可以驱动 SRAM 的数据线,开始写数据
//写数据的状态机
always @(writeDataReg or presState or clk)
    if (((presState == stWrite1) && (clk == 1'b0)) ||(presState == stWrite2))
        SRAM_DATA <= writeDataReg;
    else
        SRAM_DATA <= 8'hzz;
        
//输出 WE 信号的状态机
always @(presState or clk)
    if (((presState == stWrite1) && (clk == 1'b0)) || ((presState == stWrite2) &&(clk == 1'b1)))
        SRAM_WE_ <= 1'b0;
    else
        SRAM_WE_ <= 1'b1;
    
//获得写操作地址和数据的状态机
always @(posedge clk or negedge reset)
    //缺省状态
    if(!reset)
        begin
            presState <= stIdle;
            SRAM_CE_ <= 1'b1;
            SRAM_ADDR <= 8'h00;
            writeDataReg <= 8'h00;
        end
    else
        begin
            SRAM_CE_ <= 1'b0;
            //获得地址
            if (regWriteAddr == 1'b1 ) //Handle the clock-enabling of each register
                SRAM_ADDR <= writeAddr;
            //获得数据内容
                  if (regWriteData == 1'b1) //Handle the clock-enabling of each register:
                  writeDataReg <= writeData;
                presState <= nextState;
        end
    
//完成写操作的状态机
always @(presState or doWrite)
    begin
        case (presState)
            //等待状态
            stIdle://
                begin
                    SRAM_OE_ <= 1'b0;
                    regWriteAddr <= 1'b0;
                    regWriteData <= 1'b0;
                    nextState <= stIdle;
                    //如果开始写数据
                    if (doWrite == 1'b1)
                        begin
                            nextState <= stWrite1;
                            regWriteAddr <= 1'b1;
                            regWriteData <= 1'b1;
                        end
                end
            //这个状态完成 OE 的输出
            stWrite1://
                begin
                    nextState <= stWrite2;
                    SRAM_OE_ <= 1'b1;
                end
            //这个状态完成写操作
            stWrite2://
                begin
                    nextState <= stIdle;
                    if(doWrite == 1'b1)
                        begin
                            nextState <= stWrite1;
                            regWriteAddr <= 1'b1;
                            regWriteData <= 1'b1;
                        end
                    SRAM_OE_ <= 1'b1;
            end
        endcase
    end       

系统中两块 SRAM 分别由 DSP 和 FPGA 控制。当 DSP 和 FPGA 完成对相应 SRAM 的操作后,需要进行总线切换。总线切换后,DSP 和 FPGA 开始对另一块 SRAM 进行相应操作。主要代码如下:

代码语言:javascript复制
    //控制信号 toggle =1,C6711 的 EMIF 连接到 SRAM1,从 SRAM1 中读取视频数据
    //控制信号 toggle =0 时,切换到 SRAM2
    assign ED_SRAM = toggle ? SRAM_1_IN_ED : SRAM_2_IN_ED;
    assign SRAM_1_O_ED = toggle ? 8'hzz : ld;
    assign SRAM_1_OEN = toggle ? 1'b0 : 1'b1;
    assign SRAM_1_EA = toggle ? EA_SRAM : la;
    assign SRAM_1_CE_ = toggle ? CE_SRAM : SRAM_CE_;//toggle =1 ,output dsp to sram1;0,output saa7113 to sram1
    assign SRAM_1_OE_ = toggle ? OE_SRAM : SRAM_OE_;
    assign SRAM_1_WE_ = toggle ? WE_SRAM : SRAM_WE_;
    //控制信号 toggle =1,SAA7113 连接到 SRAM2,SAA7113 写数据到 SRAM2
    //控制信号 toggle =0 时,切换到 SRAM1
    assign SRAM_2_O_ED = toggle ? ld : 8'hzz;
    assign SRAM_2_OEN = toggle ? 1'b1 : 1'b0;
    assign SRAM_2_EA = toggle ? la : EA_SRAM;
    assign SRAM_2_CE_ = toggle ? SRAM_CE_: CE_SRAM ;//toggle =1 ,output SAA7113 to sram1;0,output DSP to sram1
    assign SRAM_2_OE_ = toggle ? SRAM_OE_: OE_SRAM ;
    assign SRAM_2_WE_ = toggle ? SRAM_WE_: WE_SRAM ;

本篇到此结束,下一篇带来基于FPGA的数字视频信号处理器设计(下),介绍程序测试与运行,包括测试程序、测试结果以及总结等相关内容。

END

后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。

大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!

0 人点赞