ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Sensor帧率调整详解:HTS/VTS计算与驱动联动

Sensor帧率调整详解:HTS/VTS计算与驱动联动 做摄像头驱动调试最常接到的一个需求就是改sensor帧率客户说要省带宽、尾端抓帧要25fps、功耗要压到15fps。多数人的第一反应是去调MIPI时钟或者PCLK这是一个很典型的误区。sensor的帧率其实不是由数据输出时钟直接决定的而是由一帧图像在内部走完全程需要的总像素开销决定的这个总开销就是HTS和VTS的乘积。本文就用GC4653这颗4M sensor把调整帧率的完整链路走一遍VTS/HTS到底是什么、目标帧率怎么反推寄存器值、驱动里怎么写、改完以后哪些联动不跟上就会翻车。适合刚接手sensor驱动的嵌入式工程师也适合做摄像头方案选型时评估帧率余量的同学收藏着备用。1. 帧率公式的本质一帧图像在sensor内部怎么走完的1.1 先分清有效像素和总开销图像传感器读出一幅画面时不是只把有效像素点读完就完事的。每个像素时钟周期sensor内部的像素计数器都会往前走一步但这些步子里面一部分落到了真正的图像数据上另一部分落到了同步、消隐、行切换这些看不见但必须存在的时间上。我常用的类比是快递分拣每个包裹就像是有效像素分拣员在一个班次里总要处理包裹但包裹和包裹之间有空车、有交接、有打卡这些就是消隐。整个班次的时长取决于总干活的量除以单位时间能处理的量而不是只取决于包裹数量。对应到sensor上就引出了两个最核心的参数HTSHorizontal Total Size水平总尺寸单位是像素时钟周期。它等于一行的有效像素宽度加上水平消隐Horizontal Blanking的像素数。VTSVertical Total Size垂直总尺寸单位是行。它等于一帧的有效行数加上垂直消隐Vertical Blanking的像素行数。一帧图像的总开销就是HTS × VTS单位是 PCLK 周期数。知道每秒钟有多少个时钟周期就能算出每秒钟能出多少帧fps PCLK / (HTS × VTS)这个公式是所有帧率调整的地基后面所有计算都围绕它转。1.2 VTS和HTS在物理上对应什么我再说细一点方便你脑子里有个画面。sensor内部的时序发生器可以理解成一个二维计数器行计数器从0数到VTS-1每行内部的像素计数器从0数到HTS-1。当行计数器走到第height行时有效像素开始输出走到VTS-1后行计数归零同时产生一个帧同步信号开始下一帧。所以VTS height VBlank也就是有效行数加垂直消隐行数。HTS width HBlank也就是有效像素宽度加水平消隐像素数。如果你把VTS调大相当于在有效行输出完之后强制sensor多空转几行才开始下一帧把HTS调大则是每一行末尾多空转几个像素周期。两种做法都会让单帧时间变长帧率降低。水平消隐、垂直消隐还有另一个名字行消隐、场消隐早期CRT显示器里就有的概念。做摄像头调试的理解这个时间结构比死记寄存器地址重要得多。1.3 为什么不直接改PCLK/MIPI时钟很多刚接触sensor的同事会问既然帧率是PCLK / (HTS × VTS)那把PCLK加倍帧率不就上去了吗逻辑上没错但工程上问题很大。PCLK通常由外部MCLK经过内部PLL倍频产生它的频率同时决定了MIPI data rate、ADC采样节奏、内部数字core电压下的最高工作频率。你贸然去调PLL相关寄存器可能PCLK是上去了但MIPI信号不稳定、误码率升高、图像出现花点sensor的模拟前端也可能吃不消。而且很多sensor的datasheet里会给出每档分辨率的PCLK上限超了就是硬件可靠性问题。相比之下HTS和VTS只是修改行/像素计数器的上限PCLK和MIPI时钟都不动纯粹是在时序上插入空闲时间风险小得多。所以软件调帧率的首选操作对象一定是VTS其次是HTS。这个顺序后面还会反复提到。2. 反推计算从目标fps到VTS/HTS寄存器值的完整链路2.1 动手前先抄好四样参数改寄存器之前先把下面四样参数放在手边最好抄在笔记本上参数含义从哪里拿PCLK像素输出时钟datasheet的推荐配置或PLL寄存器计算width × height当前有效分辨率当前mode的设定值HTS 当前值水平总尺寸当前初始化序列寄存器值VTS_min / VTS_maxVTS的合法范围datasheet的blanking章节GC4653这颗sensor不同版本、不同工程可能有差异但一般datasheet会给出推荐时序表。本文以一组贴近实际的值来做示例算的时候一定要用你自己的配置替换分辨率2560 × 14404M模式 PCLK254.4 MHz 当前HTS34600x0D84 当前VTS24520x0994先验算一下当前默认帧率是否合理fps 254,400,000 / (3460 × 2452) ≈ 254,400,000 / 8,483,920 ≈ 29.98 fps基本就是30fps说明这组参数自洽。接下来以它作为出发点做调整。2.2 单改VTS降帧率的完整计算25fps实例假设需求是把帧率从30fps降到25fpsPAL制式抓帧常用。第一步确认当前HTS不变只调整VTS。因为HTS直接影响行时间能不动尽量不动理由后面专门讲。第二步由公式反推需要的VTSVTS PCLK / (HTS × fps) 254,400,000 / (3460 × 25) 254,400,000 / 86,500 ≈ 2941.04第三步取整。sensor寄存器值必须是整数有些sensor还要求偶数或4字节对齐具体看datasheet。这里取2941。第四步检查范围。2941大于有效行数1440也远大于常规VTS下限属于合法值。第五步回代计算实际帧率确认误差可接受fps 254,400,000 / (3460 × 2941) ≈ 254,400,000 / 10,175,860 ≈ 25.0005 fps偏差只有0.0005fps完全够用。到这里寄存器目标值已经出来了VTS 2941 0x0B7D。写入位置就是后续讲的0x07/0x08寄存器。2.3 提帧率遇阻VTS到下限后改HTS的联合计算60fps实例再来看提帧率的场景目标60fps。还是保持HTS3460不变先算VTS 254,400,000 / (3460 × 60) ≈ 1225.43问题来了这个值小于有效行数1440。VTS必须大于等于有效行数加最小垂直消隐否则行计数器还没把有效行扫完就进入下一帧图像必然错位、花屏。所以只靠调VTS在这个PCLK和HTS组合下根本到不了60fps。这时候有两个方向降低分辨率到1080p或720p减少有效行数给VTS留出余量。保持分辨率压缩HTS减少每行耗时让同样多的行数用时更短。方向2更符合在不改分辨率的前提下提帧率的诉求。先给VTS定一个可行的目标假设垂直消隐留170行VTS 1440 170 1610反推HTSHTS PCLK / (VTS × fps) 254,400,000 / (1610 × 60) ≈ 2633.54取2634回代验算fps 254,400,000 / (2634 × 1610) ≈ 254,400,000 / 4,240,740 ≈ 59.99 fps这样HTS和VTS都有了新值HTS 26340x0A4AVTS 16100x064A。注意把HTS从3460压到2634后水平消隐时间大幅缩短对MIPI带宽、曝光步进的影响下面会专门展开不是改完就完事的。把几个场景放在一起方便你对照场景HTSVTS理论fps可行性当前默认3460245229.98正常降帧到25fps3460294125.00只改VTS即可提帧到60fps3460122560VTS越界不可行提帧到60fps2634161059.99需要同时改HTS和VTS2.4 把计算结果转换为寄存器字节计算完十进制值最后一步是拆成寄存器字节。绝大多数sensor的HTS/VTS寄存器是16位分成高字节和低字节两个8位寄存器连续存放。转换规则就是除以256取整作为高字节取余作为低字节。以VTS 2941为例2941 / 256 11 余 125 高字节 11 0x0B 低字节 125 0x7D写成寄存器值表示是0x0B7D。HTS同理3460对应0x0D84。实际写驱动时千万别忘了先高字节后低字节格科微系sensor的寄存器映射通常是低位地址存高字节但也见过反过来存的芯片动手前一定对着datasheet确认字节序。3. GC4653驱动实操寄存器访问、init表修改与V4L2映射3.1 GC4653的HTS/VTS寄存器位置与位宽GC4653不同版本、不同工程点寄存器地址可能不完全一致但格科微很多sensor的映射习惯很接近。以我这边接触过的GC4653调试来看常见布局是寄存器地址作用说明0x03 / 0x04曝光行数高字节 / 低字节0x05 / 0x06HTS高字节 / 低字节0x07 / 0x08VTS高字节 / 低字节这里必须强调不管从网上哪个工程拿到的参考代码一定要和你手里的GC4653 datasheet核对。有的批次HTS/VTS寄存器地址会变尤其是sensor做了功能迭代之后。照抄寄存器地址踩的坑比我吃过的盐还多。3.2 在驱动中实现set_vts/set_hts驱动里最需要的是一个运行时改VTS的函数。直接上代码I2C读写部分按你所在平台的接口封装替换即可static int gc4653_write_reg(struct i2c_client *client, u8 reg, u8 val) { u8 buf[2] { reg, val }; struct i2c_msg msg { .addr client-addr, .flags 0, .len 2, .buf buf, }; return i2c_transfer(client-adapter, msg, 1) 1 ? 0 : -EIO; } static int gc4653_set_vts(struct i2c_client *client, u16 vts) { int ret; ret gc4653_write_reg(client, 0x07, (vts 8) 0xff); if (ret) return ret; return gc4653_write_reg(client, 0x08, vts 0xff); } static int gc4653_set_hts(struct i2c_client *client, u16 hts) { int ret; ret gc4653_write_reg(client, 0x05, (hts 8) 0xff); if (ret) return ret; return gc4653_write_reg(client, 0x06, hts 0xff); }调用时注意hts、vts传进来的是总尺寸的完整值不是消隐值。我在代码评审里见过有人把V4L2的vblank值直接传进来结果VTS少了1440图像下半屏全黑这个坑要避开。3.3 改初始化序列还是运行时写改帧率有两个时机对应两种不同需求第一种产品固定某个帧率比如批量出货就是25fps。这种情况直接改初始化寄存器表最稳。初始化序列是sensor上电后逐条写入的寄存器配置集合里面会有一行写0x07/0x08的配置。把对应的值改成计算出来的新值即可sensor一上电就是这个时序省心。static const struct reg_value gc4653_init_regs[] { /* 省略其他配置 */ { 0x05, 0x0D }, { 0x06, 0x84 }, /* HTS 3460 */ { 0x07, 0x0B }, { 0x08, 0x7D }, /* VTS 2941 */ /* 省略其他配置 */ };第二种运行时动态切帧率比如应用层根据场景切换25fps/30fps/60fps。这种情况建议提供一个set_fps接口内部先算好该调到多少VTS/HTS再调用上面的函数动态写入。动态写入要注意sensor是否支持在stream on状态下直接改blanking寄存器。保守做法是在stream off时改改完再stream on如果确认sensor支持运行时更新也尽量在帧同步中断后的安全窗口去写避免改到一半sensor正在读一帧数据导致画面撕裂。有group hold寄存器的话多个相关寄存器要一起改时先挂起group hold全部写完后释放保证硬件在同一帧配置生效。3.4 用V4L2控制更方便VBLANK/HBLANK的映射在Linux V4L2框架下很多驱动会把sensor的blanking能力暴露成标准控制项V4L2_CID_VBLANK值是垂直消隐行数等于VTS - heightV4L2_CID_HBLANK值是水平消隐像素数等于HTS - width这里特别容易搞混V4L2里存的不是总尺寸而是消隐量。比如VTS2941有效行1440那么向V4L2用户空间暴露的vblank值应该是1501。驱动中给sensor加一个标准vblank控制用户态就能直接调v4l2_ctrl_new_std(sensor-ctrl_handler, sensor_ctrl_ops, V4L2_CID_VBLANK, min_vts - height, max_vts - height, 1, default_vts - height);ctrl_ops的s_ctrl回调里把val加上有效行数就是真正的VTSstatic int gc4653_s_ctrl(struct v4l2_ctrl *ctrl) { struct gc4653 *sensor container_of(ctrl-handler, struct gc4653, ctrl_handler); u16 vts; switch (ctrl-id) { case V4L2_CID_VBLANK: vts (u16)(ctrl-val sensor-cur_mode-height); return gc4653_set_vts(sensor-client, vts); default: return 0; } }用户态测试命令v4l2-ctl -d /dev/video0 --set-ctrl vblank1501 v4l2-ctl -d /dev/video0 --get-ctrl vblank用V4L2而不是直接改init表的好处是上层ISP的3A库能感知到帧率变化曝光映射可以跟着更新不会出现sensor帧率改了、AE还在按旧帧率算曝光这种两边不同步的尴尬局面。4. 改完VTS/HTS后曝光、3A和带宽的联动设计4.1 曝光行数必须被VTS钳住这是改帧率后最容易翻车的地方。滚动快门的sensor曝光时间本质上是曝光寄存器里的行数 × 每行耗时而行耗时又是HTS / PCLK。但是曝光行数不能无限大它必须小于一帧的总行数VTS通常还要留几行余量给sensor做上下帧交接、像素复位这些内部操作。当你把VTS从2452降到1610去提帧率而曝光寄存器还保持着原来允许的大数值时有两种坏情况sensor内部把曝光钳到新上限画面亮度突然掉一档。曝光时序穿越了消隐期出现图像撕裂、偏色。正确做法是改VTS之后立即读取当前曝光值并做钳位曝光上限设为VTS - safety_lines。这个safety一般建议留8到16行具体看sensor datasheet对vertical blanking最小值的说明。static void gc4653_clamp_exposure(struct gc4653 *sensor, u16 vts) { u16 exposure gc4653_get_exposure(sensor); u16 max_exp vts - 16; if (exposure max_exp) gc4653_set_exposure(sensor, max_exp); }我在实际项目里是把set_fps和clamp_exposure封装成一个接口来调用的避免上层只调其中一个导致状态不一致。4.2 line_time变了AE和抗频闪映射要跟着变直接改HTS影响的不只是帧率还有每行的耗时这一点很多新人会漏。比如HTSPCLKline_time3460254.4 MHz13.60 us2634254.4 MHz10.35 us如果ISP的AE算法还在用旧的line_time13.60us去计算曝光时间实际曝光时间变成曝光行数 × 10.35us整体亮度误差直接到20%以上。再有是防频闪。室内灯光一般是50Hz或60Hz为了不出现明暗条纹曝光时间最好是工频周期的整数倍。改HTS导致line_time变化后原来AE算好的曝光行数对应的绝对曝光时间就变了原本防频闪的曝光组合失效画面上出现横条纹滚动。这些都说明一旦动了HTS就不是sensor自己的事了必须让ISP的3A配置跟着重新初始化。反过来说这也是为什么降帧率时优先只改VTS——VTS变了frame周期变了但line_time不变AE表里的曝光时间映射基本不用大动。4.3 提帧率先算MIPI带宽够不够调高帧率之前先按原始带宽算一笔账省得到时候图像花屏还不知道为什么。不损失细节的估算公式是raw_data_rate width × height × fps × bpp以GC4653 10bit输出、2560×1440为例30fps时raw_data_rate 2560 × 1440 × 30 × 10 1,105,920,000 bit/s ≈ 1.11 Gbps60fps时直接翻倍约2.21Gbps。这还不算CSI-2协议本身的包头、包尾、ECC等开销。如果sensor是2-lane MIPI每lane要扛1.1Gbps以上的有效数据再加上编码开销基本逼近甚至超过很多SoC的MIPI接收能力。所以我在项目里提帧率前一定会先查SoC的MIPI D-PHY规格和sensor的接口lane数确认带宽有余量再动手。有人会说raw data rate变高但PCLK没变MIPI链路不是一样吗不是的每秒要送出去的帧数多了MIPI lane上实际传输的数据量必然变多。PCLK不变指的是像素读出节奏不变但MIPI HS传输的总时长占比会变高瞬时带宽需求照样上去。5. 帧率没变、花屏、亮度异常实测排查路径5.1 先确认帧率到底改没改排查一切问题前先确认sensor输出的帧率真实值别凭感觉猜。在Linux环境下最快的确认方法v4l2-ctl -d /dev/video0 --get-parm它输出里的timeperframe就是驱动上报的当前帧间隔。如果驱动没有正确上报也可以从sensor的VSYNC断点、或者用示波器量MIPI clock附近的数据包头间隔来估算真实帧率。更土但有效的办法抓一段视频用播放器看帧时间戳。确认了改没改再往下面查可以省掉一大堆自我怀疑的时间。5.2 排查链路1改完帧率纹丝不动症状寄存器改成了25fps的值实际还是30fps。按下面顺序查基本能覆盖大多数情况确认寄存器真写进去了。用I2C工具回读i2cget -y bus addr 0x07。读出来还是旧值说明写入失败了检查I2C地址、供电、时序。确认地址和值的语义没错。有些sensor的HTS/VTS寄存器存的不是total值而是blanking值。如果把total当blanking写进去实际VTS比你想的少1440帧率自然不对。确认写入时机。sensor在stream on阶段可能忽略blanking寄存器的写入必须stream off再写。查datasheet里blanking control一节看有没有hold、reload之类的使能位。确认没有被init表覆盖。Linux media框架在set_format的时候经常会重新加载init register table而这张表里的VTS还是老值把你后来写的覆盖掉了。这种情况要改init表本身或者在set_fps的同时去更新内存里的init表数据。确认AE没有把帧率拉回去。个别sensor开了auto blanking功能后AE为了控制曝光会自动调整VTS把帧率拖回旧值。检查寄存器里有没有类似auto_blanking_en的位有就关掉。5.3 排查链路2提帧率花屏/图像错位症状想从30fps提到60fps寄存器写了图像上半屏正常、下半屏错位或者整屏撕裂。最直接的原因就是VTS算出来比有效行数还小。我前面算过HTS3460时60fps需要VTS约为1225可有效行就有1440行计数器根本没扫完有效行就进下一帧了画面必然错乱。排查路径回读VTS和有效行数对比。如果VTS height基本可以定性。恢复到一个宽裕的值比如VTS height 100看花屏是否消失。确认花屏不是MIPI带宽不足导致的。如果VTS合法、分辨率没变、但60fps时图像有几率出现马赛克或花点优先怀疑带宽用示波器或SoC的CSI error counter确认。如果VTS已经合法、带宽也够还花屏检查HTS是否低于datasheet标的最小值。HTS过小会导致sensor内部某些模块来不及完成行处理。调帧率时我习惯二分逼近先改到和目标值差50行的位置确认图像稳定再逐步逼近目标值。不要一次性跳到一个极限值出问题都不知道是哪个参数闯的祸。5.4 排查链路3亮度跳变和闪烁症状帧率改成功了但画面整体亮度掉了一档或者室内灯光下出现明暗横条纹。亮度掉档十有八九是曝光没跟上。检查路径查看当前曝光寄存器值看是否撞在VTS - safety这个上限上。如果是说明曝光被钳制了。确认AE库的曝光时间映射表是否还在用旧的VTS/HTS。如果平台是V4L2架构确认应用层有没有重新枚举一遍vblank控制。出现横条纹闪烁优先怀疑抗频闪配置。改完HTS后line_time变了AE原有一批满足50Hz/60Hz整数倍的曝光行数组合全部失效。症状就是预览亮度正常但某几档曝光下图像明显在闪。处理办法是让3A重新加载抗频闪表或者把line_time这个参数暴露给AE库动态更新。还有些ISP的3A库在统计帧率变化时会自动重算前提是驱动正确上报了VTS/HTS对应的vblank/hblank控制。5.5 通用回滚与保护习惯调sensor相关寄存器一定要养成两个习惯第一改之前先备份初始值。我一般把一个mode下HTS、VTS、曝光、增益、MIPI相关的寄存器全部dump一份保存出问题恢复现场只需要一条命令导入。第二新值先用小步长逼近不要一步到位。尤其从30fps往60fps这种大跨度调整中间先验证45fps、55fps确认每一档都没有花屏再上目标帧率。# 示例把需要的寄存器值备份到文件 i2cdump -y bus addr 0x00 0xff gc4653_backup_$(date %Y%m%d).txt最后再分享一个小技巧动态改VTS时如果目标帧率和当前帧率差距不大可以直接在stream on状态下写但要趁VSYNC中断处理完之后立刻写让新值在下一帧开始前生效。如果差距大或者图像有任何怪异就老老实实stream off→改寄存器→stream on折腾不了几毫秒比排查花屏省时间多了。改帧率这件事公式本身不难难的是你改完之后和它绑定的曝光、3A、带宽这些周边有没有一起跟上。把本文这套计算和排查链路跑一次下次再有人跟你说sensor帧率调一下你至少知道第一步该翻开datasheet的哪一页。
返回列表