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)会让你怀疑人生。
我一般遵循这个策略:
- 输入位宽:直接沿用Sensor或ISP的输出位宽。常见的是12bit或14bit。别自作主张截位,那是后期的事。
- 内部运算位宽:在乘法累加路径上,至少保留2bit的裕量。比如输入12bit,内部乘加路径我至少用14bit。
- 输出位宽:根据后端模块需求定。如果输出给显示,通常需要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小,尤其是行数多的时候。
下面这张图,是我常用的行缓冲架构,你一看就明白:
嗯,这里要注意,行缓冲的深度(位宽)一定要跟你的像素格式匹配。如果是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必须有一个确定的初始状态。不然上电后,行缓冲的读写指针乱飞,数据全乱套。
我的习惯:在顶层模块,我会把复位信号先打两拍,再用到内部逻辑。这样能滤掉复位信号上的毛刺。虽然多花两个寄存器,但值得。