3、RTL设计输入输出规范:像素数据流格式与架构策略

各位同学,今天我们来聊聊HDR合成RTL设计的“门面”——输入输出规范。说白了,就是你的模块怎么跟外界打交道。我见过太多项目,算法仿真跑得飞起,一到RTL集成就崩,十有八九是数据流格式和位宽没对齐。这一章,咱们就把这些坑填平。

3.1 像素数据流格式:RAW vs RGB vs YUV

HDR合成模块的输入,通常来自图像信号处理器(ISP)的前级。你拿到的像素格式,直接决定了后续的运算复杂度。我个人习惯,在设计之初就先把数据流格式定死,不然后面改起来真要命。

格式 典型位宽 适用场景 我的建议
RAW 10/12/14 bit Sensor直出,HDR融合前级 保留最多动态范围,但处理复杂
RGB 8/10/12 bit per channel 显示、色彩处理 适合做Gamma校正和色彩矩阵
YUV 8/10 bit (Y), 8/10 bit (UV) 视频编码、传输 亮度色度分离,节省带宽

核心原则:HDR合成阶段,我强烈建议使用RAW或高位宽RGB格式。因为HDR的核心就是保留高动态范围,YUV的色度下采样会丢失细节,得不偿失。

我在一个项目中遇到过,前端ISP给了YUV420格式的数据,我们直接拿来做HDR合成。结果合成后的图像在边缘区域出现了明显的色彩伪影。查了两天才发现,是UV分量的亚像素采样跟合成算法的像素对齐没做好。从那以后,我坚持在HDR合成模块内部使用RGB或RAW流水线。

3.2 位宽选择策略:精度与面积的博弈

位宽选多少?这是个老生常谈的问题。你想想看,位宽每增加1 bit,乘法器面积就增加约20%。但位宽不够,HDR合成后的条带效应(Banding)会让你怀疑人生。

我一般遵循这个策略:

  1. 输入位宽:直接沿用Sensor或ISP的输出位宽。常见的是12bit或14bit。别自作主张截位,那是后期的事。
  2. 内部运算位宽:在乘法累加路径上,至少保留2bit的裕量。比如输入12bit,内部乘加路径我至少用14bit。
  3. 输出位宽:根据后端模块需求定。如果输出给显示,通常需要dithering到10bit或8bit。

我的小技巧:在做RTL之前,先用C模型跑一遍全精度仿真,统计出每个节点的动态范围。然后根据统计结果,在保证不溢出的前提下,选择最小的位宽。这叫“数据驱动设计”,比拍脑袋靠谱多了。

举个例子,一个简单的HDR融合公式:out = (w1 * img1 + w2 * img2) >> shift。如果img1和img2都是12bit,权重w1和w2是8bit定点数,那么乘法结果就是20bit。两个20bit相加,需要21bit。最后右移shift位,得到最终输出。你看,每一步的位宽都得算清楚。

3.3 行缓冲(Line Buffer)架构设计

行缓冲,说白了就是用来存几行像素的RAM。HDR合成算法通常需要空间邻域信息,比如做引导滤波或者局部色调映射,这时候行缓冲就派上用场了。

设计行缓冲,核心就三个参数:行数、宽度、深度。

  • 行数:取决于算法需要的邻域大小。3x3的窗口需要3行,5x5需要5行。别多存,浪费面积。
  • 宽度:就是一行像素的个数,等于图像宽度。
  • 深度:每个像素的位宽。

我曾经在一个4K分辨率的项目中,为了省面积,把行缓冲的宽度设成了图像宽度的一半,然后用双缓冲乒乓操作。结果时序跑不过,因为读写冲突太频繁。最后老老实实用了全宽行缓冲,加了一级流水线,问题才解决。

避坑指南:行缓冲的读端口和写端口一定要分开!如果你用单端口RAM做行缓冲,读写冲突会让你抓狂。要么用双端口RAM,要么用Shift Register。我个人偏好用双端口RAM,面积比Shift Register小,尤其是行数多的时候。

下面这张图,是我常用的行缓冲架构,你一看就明白:

行缓冲(Line Buffer)典型架构 像素输入 Line Buffer 0 Line Buffer 1 Line Buffer 2 写指针 读指针 控制逻辑(FSM) 像素输出 写指针循环写入,读指针滞后N行 实现滑动窗口效果

嗯,这里要注意,行缓冲的深度(位宽)一定要跟你的像素格式匹配。如果是RAW格式,每个像素可能只有12bit;如果是RGB,每个像素就是36bit(12*3)。别搞混了。

3.4 时钟域与复位策略

时钟域和复位,是RTL设计的“水电煤”。搞不好,整个系统都会瘫痪。

时钟域:HDR合成模块通常工作在像素时钟域。但如果你的模块需要跟DDR控制器或者CPU总线交互,那就涉及跨时钟域了。

  • 单时钟域:所有逻辑都用同一个时钟。简单,但可能时序紧张。
  • 多时钟域:像素时钟、总线时钟、DDR时钟分开。需要异步FIFO或握手信号。

我建议,如果可能,尽量把HDR合成模块做成单时钟域。跨时钟域处理不仅增加面积,还容易出CDC(Clock Domain Crossing)问题。我在一个项目中,就因为异步FIFO的深度没算对,导致数据丢帧,查了整整一周。

复位策略:我推荐使用异步复位、同步释放。这样既能保证复位信号的及时响应,又能避免复位撤销时的亚稳态问题。

代码模板如下:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        // 异步复位
        reg_a <= 'd0;
        reg_b <= 'd0;
    end else begin
        // 同步逻辑
        reg_a <= data_in;
        reg_b <= reg_a;
    end
end

另外,别忘了给行缓冲也加上复位。虽然RAM的初始值无所谓,但控制逻辑的FSM必须有一个确定的初始状态。不然上电后,行缓冲的读写指针乱飞,数据全乱套。

我的习惯:在顶层模块,我会把复位信号先打两拍,再用到内部逻辑。这样能滤掉复位信号上的毛刺。虽然多花两个寄存器,但值得。