多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

TW2868驱动实战:海思Linux平台四路CVBS接入VI的完整指南

TW2868驱动实战:海思Linux平台四路CVBS接入VI的完整指南 简介一套面向海思平台的TW2868视频处理芯片Linux底层驱动源码适合嵌入式Linux驱动开发、系统移植与视频设备调试人员。压缩包内共7个文件包括3个头文件、2个C源文件、1个可加载内核模块.ko以及1个Makefile整体仅17KB结构轻量清晰。头文件用于寄存器映射与数据结构声明C源文件承担设备初始化、中断处理和DMA传输ko模块可直接插入内核验证驱动效果。已有218人学习下载适合用来理解内核驱动的编写流程与硬件交互细节。通过阅读和修改源码可以掌握设备探测、内存映射、视频通道配置等底层逻辑并结合实际板卡调整参数或优化视频编解码性能是一份便于快速上手的海思平台TW2868驱动参考资料。1. TW2868驱动是什么在海思Linux里把四路CVBS接进VI的最小方案做安防DVR的工程师应该都见过这类板子一颗海思主控边上躺着一颗四通道模拟视频解码芯片把四路摄像头的CVBS信号变成数字信号送进海思VI。这颗芯片往往就是TW2868——它没有复杂的固件也不需要什么特殊接口本质就是一个挂在I2C总线上的从设备。所以“TW2868 driver海思Linux底层驱动”这个需求要解决的核心问题不是“怎么操作硬件”而是“怎么在Linux内核里正确地把它配置好再把输出时序和海思VI对齐”。这篇文章就把这条路走一遍从寄存器地图、最小I2C驱动、到海思VI对接、再到排错适合手里有板子需要自己维护驱动源码的工程师也适合第一次接触模拟视频解码芯片的Linux驱动新人。2. 先认识TW2868再写驱动寄存器地图与海思VI的对接关系2.1 一颗四路解码芯片为什么值得写底层驱动TW2868属于模拟视频解码器Video Decoder这一类芯片输入是四路CVBS复合视频信号输出的是数字化的BT656格式数据流。它内部做的事情其实就是把模拟信号做钳位、放大、ADC采样、梳状滤波分离亮色然后按BT656的格式把数字视频流从输出引脚送出去。对海思平台来说VIVideo Input模块刚好支持BT656接口数据线、像素时钟、内嵌同步信号一接硬件链路就通了。刚接触这个方案的工程师最容易问芯片手册里那么多寄存器是不是得全部写完才能出图答案是不需要。TW2868的大多数寄存器上电默认值就能工作驱动要做的其实只有三件事确认芯片在总线上、把它的解码标准和输出格式配对、在通道切换时做软复位。真正决定画面能不能稳定出来的是后端的VI时序参数这反而是很多驱动源码里看不到的部分。选型上TW2868之所以在海思方案里到处都是是因为一颗芯片能替代四颗独立的解码器BOM和PCB面积都省下来了。四路CVBS进一串BT656出正好贴着海思中低端DVR芯片的VI输入能力来设计。哪怕到了现在很多新项目里它依然在用原因也很实际模拟摄像头存量巨大项目要兼容老设备TW2868就是成本最低的桥接方案。2.2 I2C地址和寄存器地图动手前先搞清这五类寄存器写驱动之前第一件事是确认从地址。TW2868的I2C地址由芯片的地址引脚决定常见的是7位地址0x48或者0x49也有0x4A/0x4B等变体。这里要特别注意Linux内核I2C子系统里用的是7位地址而i2c-tools之类的用户态工具有时会显示8位地址也就是左移一位后的0x90。后面排查I2C扫不到设备的问题十有八九都是卡在这个地址进制上。寄存器方面驱动里真正会碰到的寄存器可以分成五类第一类是设备识别寄存器一般偏移在0x00附近读它返回芯片ID用来确认I2C地址对不对、芯片是不是在工作状态。第二类是软复位寄存器初始化或者切换通道后写一次让芯片内部状态机重新同步。第三类是输入通道配置寄存器TW2868有四路输入每路有一段连续的寄存器空间控制信号源选择、模拟增益、同步分离灵敏度等。第四类是解码标准寄存器用来设置PAL/NTSC/SECAM的自动检测还是强制指定。第五类是输出控制寄存器决定BT656输出格式、是否带内嵌同步、数据位宽等。我一般会在驱动里把寄存器地址用宏定义列出来并在每个宏的注释里标注datasheet的页码。这个习惯看起来琐碎但实际维护驱动时帮了大忙——海思SDK里附带的TW2868驱动源码版本很乱有的来自不同芯片批次寄存器的bit定义有细微差别手里有一份自己的寄存器地图才能快速比对差异。2.3 驱动框架选型v4l2_subdev还是裸I2C读写海思Linux平台上驱动TW2868有两条常见路线。一条是使用内核V4L2框架把TW2868注册成v4l2_subdev通过s_stream回调来控制视频流的开启和关闭另一条更“野”的做法是直接在内核模块里做i2c_transfer不注册任何V4L2节点仅仅把寄存器读写封装成函数由平台驱动在初始化时调用。两种方式怎么选如果项目用了标准的媒体框架比如应用层通过V4L2去打开视频设备那必须走v4l2_subdev否则应用层看不到这个设备。如果只是给海思MPP做配套MPP的VI模块自己管理数据流TW2868只需要被配置一次那裸I2C读写反而更直接少了一层框架的复杂度。海思官方SDK里给的样例一般是后者居多因为MPP并不依赖V4L2来采集视频。不过作为底层驱动我更推荐至少注册成v4l2_subdev的形态。原因很简单海思平台的VI驱动本身就是一个video_deviceTW2868的配置动作如果挂在VI的open/close流程之外很容易出现断电重启、休眠唤醒后芯片状态和驱动状态不一致的问题。而subdev框架提供了一套标准的生命周期管理probe、open、s_stream、close都有对应的回调点该初始化的地方初始化该关闭的地方关闭问题可预期得多。3. 最小可跑驱动的I2C读写与初始化序列先把寄存器点亮3.1 I2C读写封装从i2c_transfer到寄存器级函数不管后续用不用V4L2框架寄存器读写函数都是整个驱动的基石。TW2868的I2C协议很简单写操作是发送一个寄存器地址字节再跟一个数据字节读操作是先发送寄存器地址然后重新发起一次读传输。下面是驱动里最常用的两个函数直接基于i2c_transfer实现static int tw2868_write_reg(struct i2c_client *client, u8 reg, u8 val) { struct i2c_msg msg; u8 buf[2] { reg, val }; int ret; msg.addr client-addr; /* 7位I2C地址驱动框架已处理好地址位 */ msg.flags 0; /* 写操作 */ msg.len 2; msg.buf buf; ret i2c_transfer(client-adapter, msg, 1); if (ret ! 1) { dev_err(client-dev, write reg 0x%02x val 0x%02x failed\n, reg, val); return -EIO; } return 0; } static int tw2868_read_reg(struct i2c_client *client, u8 reg, u8 *val) { struct i2c_msg msgs[2]; u8 reg_buf reg; int ret; msgs[0].addr client-addr; msgs[0].flags 0; /* 先写寄存器地址 */ msgs[0].len 1; msgs[0].buf reg_buf; msgs[1].addr client-addr; msgs[1].flags I2C_M_RD; /* 再读数据 */ msgs[1].len 1; msgs[1].buf val; ret i2c_transfer(client-adapter, msgs, 2); if (ret ! 2) { dev_err(client-dev, read reg 0x%02x failed\n, reg); return -EIO; } return 0; }这段代码有两点值得说明。其一i2c_transfer的返回值是成功传输的消息数量所以写操作要判断ret是否等于1读操作要判断ret是否等于2不能只判断是否小于0。很多新手在这里只判断负返回值遇到NACK芯片没应答时i2c_transfer返回0错误就被漏掉了。其二海思某些I2C控制器在总线繁忙时会返回-EAGAIN驱动里最好对这种情况做重试。我一般会在外层包一个带重次数的循环每次重试间隔1ms到5ms重试三五次基本就能覆盖大部分总线异常。3.2 初始化序列软复位、读芯片ID、配置解码标准TW2868的初始化顺序也有讲究。上电之后芯片内部的模拟前端还没稳定如果直接写寄存器有些位会写不进去。常见的顺序是先做一次软复位等待几毫秒再读芯片ID确认通信正常最后才开始逐路配置输入和解码标准。下面这段代码演示了最小初始化流程#define TW2868_REG_ID 0x00 #define TW2868_REG_SYS_CTRL 0x01 #define TW2868_CH0_CTRL_BASE 0x10 static int tw2868_chip_init(struct tw2868_dev *dev) { u8 id 0; int ret; /* 1. 软复位往系统控制寄存器写入复位触发位 */ ret tw2868_write_reg(dev-client, TW2868_REG_SYS_CTRL, 0x03); if (ret) return ret; /* 2. 等芯片内部状态机稳定 */ msleep(10); /* 3. 读ID确认I2C地址和芯片都在线 */ ret tw2868_read_reg(dev-client, TW2868_REG_ID, id); if (ret) return ret; if (id ! TW2868_EXPECT_ID) { dev_err(dev-client-dev, chip ID mismatch: 0x%02x\n, id); return -ENODEV; } /* 4. 四路输入均配成自动检测解码标准 */ for (int ch 0; ch 4; ch) { tw2868_write_reg(dev-client, TW2868_CH0_CTRL_BASE ch * 0x10, TW2868_STD_AUTO); } return 0; }这里要提示一点TW2868的寄存器偏移和bit定义不同芯片批次的手册会有差异上面的宏值是我在常见型号上看到的值你拿到一块新板子时第一件事是翻开datasheet确认TW2868_REG_ID、TW2868_REG_SYS_CTRL这些偏移是否一致不一致的要改过来。不要直接照抄寄存器值这是调这类芯片最容易被坑的地方。解码标准配置成AUTO还是强制PAL取决于项目里的摄像头。如果所有摄像头都是PAL制式强制PAL比自动检测更稳妥因为自动检测在信号弱的场景下会在PAL和NTSC之间来回抖动导致画面间歇性跳变。如果现场混用PAL和NTSC摄像头那就只能选AUTO但要在驱动里把解码状态寄存器读出来把实际检测到的制式上报给应用层。3.3 用sysfs把寄存器翻出来调试驱动的第一件工具驱动开发过程中反复烧写内核模块来调试寄存器效率太低。我通常会在驱动里加一个sysfs节点把寄存器读写能力暴露到用户态这样在板上就可以直接用echo和cat来操作寄存器不用重新编译内核。代码也很简单static ssize_t tw2868_reg_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count) { struct i2c_client *client to_i2c_client(dev); unsigned int reg, val; int ret; if (sscanf(buf, %x %x, reg, val) ! 2) return -EINVAL; ret tw2868_write_reg(client, (u8)reg, (u8)val); if (ret) return ret; return count; } static DEVICE_ATTR_WO(tw2868_reg);使用方式是在板子上执行echo 0x00 0x03 /sys/bus/i2c/devices/0-0048/tw2868_reg这行命令把0x00寄存器写成0x03也就是触发一次软复位。sysfs节点在调试阶段价值极大比如怀疑某路输入没信号时可以直接读对应通道的状态寄存器看看里面的同步锁定位有没有置起来。不过要注意sysfs节点只应该放在调试版本的驱动里正式发布时要么删掉要么用Kconfig开关控制避免用户态随意改写寄存器把芯片配置搞乱。4. 把TW2868挂进海思VIsensor注册、vb_sensor.conf与v4l2_subdev对接4.1 海思VI接入的两条路径内核subdev还是用户态sensor配置TW2868的驱动写完寄存器配置只是完成了芯片本身的初始化真正让它出画面还得让海思VI知道数据从哪里来。海思MPP平台下VI侧对接外部视频源有两条路径。一条是走内核V4L2的subdev方式由驱动注册v4l2_subdev海思VI驱动通过v4l2_subdev_call调用到TW2868的s_stream另一条是海思用户态的sensor_drv方式sensor相关的寄存器操作全放在用户态库或者sample代码里内核只管VI的时序配置。对TW2868这类外挂解码芯片海思SDK里的常规做法其实是用户态方式VI模块按BT656时序直接接收数据TW2868的寄存器配置放在一个单独的初始化函数里由应用在VI开启前调用。这种方式改动最小但有个明显缺点——寄存器配置和应用逻辑耦合在一起换成新平台或者换了芯片批次要重新编译应用。我一般倾向把TW2868做成内核态的v4l2_subdev。虽然前期写代码多一点但换来的是清晰的设备模型probe时初始化芯片s_stream时配置VI对接系统休眠时进suspend回调。而且如果项目里已经有标准的sensor驱动框架把TW2868挂上去只是多填一份寄存器配置表的事。4.2 vb_sensor.conf里的时序参数BT656窗口和像素时钟怎么填海思VI接收BT656数据时需要知道输入视频的行场参数比如一行有多少个像素、帧率是多少、消隐期多长这些参数一般在SDK的vb_sensor.conf或者对应的sensor配置结构体里填。TW2868输出的BT656是内嵌同步VI要从数据流里自己提取行同步和场同步信号所以时序参数必须和TW2868的输出严格一致。这里有一组最常见的配置项字段名以你手里SDK的sensor_cmos.h为准不同版本可能有出入字段名含义TW2868常见值hsa行同步脉冲宽度校准后按实际抓取值hfp行前肩按BT656标准hbp行后肩按BT656标准vfp场前肩按制式标准vbp场后肩按制式标准vr_hblank行消隐是否可见0调试这些参数最直接的手段是海思MPP的VB视频缓冲抓帧工具把VI抓到的原始数据dump下来一行一行看数据的起始位置是否和预期一致。BT656的数据流里同步头是固定的0xFF 0x00 0x00通过查找同步头的位置可以反推hfp和hbp的实际值比对着寄存器猜靠谱得多。4.3 v4l2_subdev的注册代码从probe到s_stream的调用链在内核驱动里把TW2868挂到V4L2框架下需要做三件事定义subdev ops、在probe里初始化subdev、在s_stream回调里实现视频流的开启和关闭。下面是核心的框架代码static int tw2868_s_stream(struct v4l2_subdev *sd, int enable) { struct tw2868_dev *dev v4l2_get_subdevdata(sd); if (enable) { /* 先把芯片内部恢复成已知状态再开始输出 */ tw2868_write_reg(dev-client, TW2868_REG_SYS_CTRL, 0x03); msleep(10); tw2868_configure_channels(dev); } else { /* 停止输出进入低功耗待机 */ tw2868_write_reg(dev-client, TW2868_REG_SYS_CTRL, 0x00); } return 0; } static const struct v4l2_subdev_video_ops tw2868_video_ops { .s_stream tw2868_s_stream, }; static const struct v4l2_subdev_ops tw2868_subdev_ops { .video tw2868_video_ops, }; static int tw2868_probe(struct i2c_client *client) { struct tw2868_dev *dev; int ret; dev devm_kzalloc(client-dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; v4l2_i2c_subdev_init(dev-sd, client, tw2868_subdev_ops); v4l2_set_subdevdata(dev-sd, dev); ret tw2868_chip_init(dev); if (ret) return ret; return 0; } static const struct i2c_device_id tw2868_id[] { { tw2868, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, tw2868_id); static struct i2c_driver tw2868_i2c_driver { .driver { .name tw2868, }, .probe tw2868_probe, .id_table tw2868_id, }; module_i2c_driver(tw2868_i2c_driver);代码的调用链是这样的I2C子系统枚举到对应地址的设备后调用tw2868_probe完成芯片初始化和subdev注册海思VI驱动在应用层打开设备并启动流时通过v4l2_subdev_call找到TW2868的s_stream并调用。这样的好处是数据流的开关和芯片配置天然绑定在一起应用层异常崩溃时VI驱动关闭流的动作也能顺带把TW2868带回待机状态。需要注意海思平台的内核版本和V4L2框架版本。老版本SDK比如基于Linux 3.18里v4l2_i2c_subdev_init的用法没问题但新版本把一些接口改成了v4l2_async方式。如果你手上的SDK比较新probe里要换成v4l2_async_register_subdev同时设备树里要配置对应的端口信息。判断SDK新旧最简单的方式是看VI驱动源码里是直接i2c_new_device还是走fwnode框架。5. TW2868驱动常见问题与排查四条把新手卡死的血泪记录5.1 I2C读不到芯片ID地址到底是0x48还是0x90现象i2cdetect在总线上扫不到任何设备或者驱动probe时报芯片ID不对寄存器读回来全是0xFF。原因TW2868的从地址由硬件引脚决定常见7位地址是0x48但很多资料和旧代码里写的是0x90那其实是8位地址0x48左移一位。I2C通信时地址字节的最低位是读写位0x90表示写操作的地址字节0x91表示读操作。用户态用i2cset命令时可以把0x90直接传给工具工具会自己处理但Linux内核的i2c_client里填的是7位地址如果照抄老代码填成0x90驱动在总线上发的地址就完全错了。解决先用硬件万用表确认TW2868的地址引脚电平对应哪个地址再用i2cdetect扫描总线上实际存在的地址。驱动里的client-addr要填7位地址0x48这样的值。另外海思某些引脚复用的I2C控制器上电后默认处于GPIO模式需要先在设备树里把引脚mux成I2C功能否则总线根本没有时钟信号输出这也是扫不到设备的隐藏原因之一。5.2 图像花屏带雪花解码标准和VI时序窗口没对齐现象出图了但画面满屏雪花或者看起来像信号被拉伸了有时能隐约看到黑白图像轮廓。原因两个可能。一是TW2868的解码标准配置和实际输入制式不匹配比如摄像头输出PAL芯片却设成强制NTSC图像必然花。二是BT656数据已经正确输出但海思VI的行场窗口参数没配对导致VI从数据流中截取到的有效像素区域偏移了。解决先确认芯片解码状态寄存器里的同步锁定位是1标准检测结果是否和摄像头制式一致。然后再抓VI原始数据查找0xFF 0x00 0x00同步头的实际位置和vb_sensor.conf里的hsa/hfp/hbp对比。调整海思时序参数时一次只改一个字段每次改完抓一帧数据验证不要同时改多个参数否则根本分不清是哪个字段引起的偏移。5.3 切换通道后画面迟滞好几秒软复位没放在切换流程里现象四路输入在应用层来回切换切到某一通道后画面要卡顿2到3秒才慢慢恢复正常有时还伴随短暂的黑屏闪断。原因TW2868的模拟前端和同步分离电路在通道切换后会进入重新收敛的过程如果芯片当前还带着上一通道的同步状态新通道的信号进来就会长时间无法锁定。很多驱动只在初始化时做了一次软复位通道切换时直接把配置寄存器改了就完事芯片内部状态机还停在旧通道的位置。解决在每一次通道切换的寄存器写入序列前先触发一次软复位让芯片状态机回到初始状态再写入新通道的配置。用代码表示就是写软复位寄存器等待10ms左右再写通道选择和解码标准寄存器。这10ms的等待不能省我见过有人把msleep改成usleep想图快结果低速信号下锁不住画面抖动更严重。5.4 四路只出两路VI端口没开满四个时隙现象硬件接线没问题TW2868寄存器配置也没问题但VI出来只有通道0和通道1有画面后面两路是黑屏或者花条。原因TW2868在BT656模式下四路通道的数据是时分复用在一组数据线上的每路占用固定行或固定时隙。海思VI接收BT656时如果端口配置里只使能了前两个通道的使能位后面的数据会被VI直接丢弃表现就是只有两路出图。解决回VI端口配置结构体里找通道数目相关的字段把它从2改成4并确认对应的数据宽度、时序模式都配成了BT656内嵌同步。这类配置经常藏在SDK的sample代码里而不是驱动源码里所以搜问题时别只盯着TW2868的驱动还要看VI初始化的整个调用链。排这个坑的时候用海思的抓帧工具一次抓四路原始数据看哪一路根本没有数据进来就能快速定位是TW2868没输出还是VI没接收。6. 把TW2868驱动调到不掉帧帧率验证与缓冲配置的最后一公里TW2868驱动能出图只是开始真正的验收标准是长时间不掉帧。这里分享三个验证和调优手头参数的习惯。第一个习惯是抓手帧率。海思MPP在/dev目录下提供VI的统计信息节点通过读取VI的帧率统计可以知道实际进到编码器的帧率是多少。PAL制式下应用层预期是25fpsNTSC是30fps。如果实际帧率比预期低先排除驱动里有没有在s_stream里做过多的msleep阻塞操作。我曾经在s_stream里加了50ms的延时来等图像稳定结果每开一次流就卡掉两帧。正确的做法是延时只放在初始化阶段s_stream里配置完寄存器就立即返回让VI自己按节奏取帧。第二个习惯是检查缓冲申请大小。VI接收BT656数据时按行存储每行像素数和存储对齐方式由驱动程序决定。TW2868输出BT656时一行可能包含同步头、有效像素和消隐数据如果缓冲区按最小有效宽度申请而VI实际写入时按完整行宽写入就会发生缓冲区溢出表现是画面偶发撕裂甚至驱动直接报错。这个问题的排查方法是看dmesg里有没有buffer overflow或类似的关键字有的话把缓冲宽度按2的幂次对齐重新申请。第三个习惯是复测所有通道。四路输入全部接上真实摄像头每路切换一遍同时用cat连续抓取几百帧确认每路的帧间隔均匀且无丢帧。很多板子在只有一路信号调试时一切正常四路全接上后问题就暴露了多半是TW2868的I2C配置时序在四路同时工作时产生了竞争这时候检查驱动里是否有全局锁保护寄存器访问。最后一个经验是教训换来的调这种外挂解码芯片驱动每次修改的寄存器值都要用日志打出来最好在改动的同时打印修改前后的完整寄存器快照。芯片寄存器状态是个黑匣子出问题时如果没有历史快照只能靠猜。我的习惯是把寄存器快照打印封装成一个函数所有关键流程结束后都调用一次排查问题时翻日志能直接定位到是哪一步把状态改坏了。希望这篇笔记能帮你在海思Linux平台上少走一趟这个弯路。本文还有配套的精品资源点击获取
返回列表