简介:围绕Sil9136 USB到串行桥接芯片,这份资源包整合了官方参考手册与可编译的驱动源码,面向嵌入式硬件工程师、驱动开发人员以及物联网设备设计者,适用于USB转SPI/I2C/UART等多串口扩展场景。压缩包共24个文件、约13.73MB,包含3份PDF文档(数据手册、编程参考手册及补充文档)、C源码与对应头文件、Makefile、编译生成的.o/ko模块以及配置辅助文件,从芯片电气特性、引脚定义、操作模式到软件初始化、寄存器读写与中断处理均有覆盖。目前已有571人学习下载,内容经过整理与编译验证,便于在Linux环境下直接参考或交叉编译。利用这套资料,开发者可快速搭建Sil9136的驱动框架,解决硬件调试和驱动移植中的常见问题,对芯片选型评估和产品开发都颇有参考价值。
1. 拆包 sil9136 参考手册与驱动源码:一次把视频桥接芯片的驱动链路看清
如果你接过嵌入式显示相关的活,大概率碰到过这种局面:芯片手册几百页,寄存器表比字典还厚,驱动源码散落在各个 BSP 里,拷过来编译一堆报错,最后靠改 GPIO 延时把画面“试”出来。这份 sil9136 参考手册加驱动源码的打包资源,解决的就是这个痛——把一颗视频桥接/转换芯片从硬件管脚定义、寄存器映射到 Linux 驱动框架下的完整实现一次性给全。它适合两类人:一是正在做显示相关板卡定制、需要快速移植驱动的嵌入式工程师;二是想搞懂 I2C 设备驱动怎么写、中断和 EDID 上报机制怎么配合的学生开发者。拿到手不是看手册,而是对着源码把芯片的初始化时序、状态机切换、HPD 事件上报整条链路走一遍,你会明显感觉到比对着 PDF 盲读效率高得多。
2. 参考手册与源码包结构:先搞清这一包里到底放了什么
2.1 文件清单与目录组织:PDF 之外的硬货在哪
解压后第一件事不是翻手册,而是先把目录树列出来。常见做法是用 tree 命令或直接看压缩包注释,通常这类资源会按doc/、driver/、include/、utility/四块组织。doc 目录下面是数据手册和编程指南,PDF 或 odt 格式都有;driver 目录才是主角——Linux 内核驱动源码,包含sil9136.c、sil9136.h以及 Makefile;include 目录是芯片寄存器头文件,按功能块拆成reg_hdmi.h、reg_i2c.h、reg_cbus.h这类;utility 目录一般放厂商提供的调试工具源码或脚本,比如通过 I2C 总线直接读写寄存器的小工具。
我一般会先看sil9136.c的头部注释和MODULE_DESCRIPTION,这能最快确认驱动对应的内核版本和 API 风格。如果源码里用了i2c_driver结构体加probe/remove回调,说明这是标准 I2C 客户端驱动;如果出现spi_driver,那就是走 SPI 接口的变体。sil9136 这类视频转换芯片,绝大多数板卡设计中 I2C 是控制通道,视频数据走并行接口或 LVDS,这点从i2c_device_id表和of_match_table就能确认。
2.2 硬件特性在手册里的分布规律:别从头翻到尾
数据手册几百页,从头读到尾的人是少数,效率也低。我拆过几份这类芯片手册后发现,重点是三个区域:管脚描述表(Pin Description)、寄存器映射表(Register Map)和电气特性表(DC/AC Characteristics)。管脚表决定硬件接线的可行性,比如你的主控 I2C 引脚是否支持 1.8V 电平,HPD 脚有没有内部上拉;寄存器映射表决定驱动代码里那些魔法数字从哪来;电气特性表则是排查信号完整性问题的依据。
以 sil9136 为例,驱动源码里反复出现的0x7F这类寄存器偏移,手册里会有专门一段描述控制通道(CBUS)的访问机制。要看懂源码里sil9136_read_reg这类函数的参数,不需要把整本手册背下来,只需要找到寄存器映射表,按地址区间划分功能块:视频参数块、音频参数块、中断状态块、EDID RAM 块。驱动里读中断状态时用的那一串REG_INT_STATUS_*宏定义,必然对应手册里中断相关寄存器的位定义。对着这个思路去读,源码和手册的对应关系很快就建立起来了。
2.3 技术栈判断:这驱动跑在内核哪个版本上
判断技术栈最直接的方式是看驱动的 API 风格。老内核(2.6.x)里i2c_driver结构体有attach_adapter回调,新内核(4.x 以上)全部走probe函数。sil9136 驱动源码如果用的是devm_kzalloc、device_property_read_u32这类设备管理 API,说明依赖较新的内核机制,至少 3.14 以上。如果出现of_property_read_u32,则是 3.x 时代的老风格。
我遇到过一份移植到 5.10 内核报错的驱动,原因就是源码里还在用i2c_new_device而新版内核改成了i2c_new_client_device。这种 API 变动是移植时最常见的坑。读代码时顺便把 Makefile 里的ccflags-y看一下,有没有开启额外的调试宏,有些版本会带SIL9136_DEBUG这种条件编译开关,开启后会在内核日志里打印大量寄存器读写信息,调试阶段很有用,量产阶段记得关掉。
3. 核心驱动链路拆解:I2C 探测、EDID 读取与 HPD 中断上报
3.1 I2C 客户端注册与设备树匹配:probe 是怎么被调起来的
sil9136 驱动的主入口是标准 I2C 驱动框架。设备树里声明了这个芯片的节点后,内核 I2C 子系统会按compatible字符串匹配of_match_table,匹配上了就调用probe函数。下面是一段典型的设备树节点写法:
&i2c2 { status = "okay"; sil9136: sil9136@39 { compatible = "sil9136,i2c"; reg = <0x39>; reset-gpios = <&gpio4 17 GPIO_ACTIVE_LOW>; hpd-gpios = <&gpio4 18 GPIO_ACTIVE_HIGH>; vdd-supply = <®_1v8>; }; };这段描述里reg = <0x39>是芯片的 I2C 从机地址,reset-gpios和hpd-gpios分别指向复位引脚与热插拔检测引脚所连接的 GPIO 控制器节点。vdd-supply指向一个稳压器节点,驱动在 probe 时需要通过regulator_get获取供电控制句柄。
驱动源码中对应的是:
static const struct i2c_device_id sil9136_id[] = { { "sil9136", 0 }, { } }; MODULE_DEVICE_TABLE(i2c, sil9136_id); static const struct of_device_id sil9136_of_match[] = { { .compatible = "sil9136,i2c" }, { } }; MODULE_DEVICE_TABLE(of, sil9136_of_match);i2c_device_id表用于非设备树平台,比如通过板级文件i2c_register_board_info注册设备;of_device_id表则是设备树平台的匹配入口。两个表都挂着,驱动就能同时兼容新旧两种内核平台,这是很多老驱动移植时容易丢的部分——只改设备树忘了同步i2c_device_id,结果老平台直接 probe 失败。
3.2 寄存器读写函数封装:所有控制操作的地基
芯片驱动到最后就是读寄存器、写寄存器、等状态。sil9136 的寄存器分页机制比较复杂,部分功能块需要先往某个 page 寄存器写入页号,才能访问目标偏移。源码里sil9136_write_reg会把页切换和偏移访问封装到一起,调用的地方不需要关心页细节。
static int sil9136_write_reg(struct i2c_client *client, u8 page, u8 offset, u8 value) { struct i2c_msg msg; u8 buf[3]; int ret; buf[0] = page; buf[1] = offset; buf[2] = value; msg.addr = client->addr; msg.flags = 0; msg.len = 3; msg.buf = buf; ret = i2c_transfer(client->adapter, &msg, 1); if (ret != 1) { dev_err(&client->dev, "write reg failed: page=0x%02x offset=0x%02x\n", page, offset); return -EIO; } return 0; }这个封装的精妙之处在于把页号写入和目标寄存器访问合并成一次 I2C 事务。大部分 I2C 控制器对单次传输长度没有严格限制,合并传输可以避免分开写页号后再写寄存器之间被其他设备插入操作,导致页状态错乱的问题。buf[0]是页号,buf[1]是寄存器偏移,buf[2]是要写入的值。i2c_transfer返回值是成功传输的消息数量,不是 1 就说明总线上出了问题,打印关键字dev_err能帮你在内核日志里快速定位是地址错误还是设备掉线。
读操作封装类似,区别在于要分两次传输:先写页号和偏移,然后读取数据。有些 I2C 控制器不支持 repeat start 操作,这种情况下驱动会退化为两个独立的i2c_transfer调用,中间可能会被抢占,但实际使用中影响不大,因为寄存器访问本身不要求原子性。
3.3 HPD 中断处理与 EDID 缓存:状态上报的完整链路
视频桥接芯片有一个核心职责:检测显示设备是否插入,并把显示器的 EDID 数据读回来供主控解析。sil9136 的 HPD 引脚接到主控 GPIO 后,驱动要处理两种事件:HPD 上升沿表示显示器插入,下降沿表示拔出。
static irqreturn_t sil9136_hpd_irq_handler(int irq, void *dev_id) { struct sil9136_priv *priv = dev_id; int state; state = gpiod_get_value(priv->hpd_gpio); if (state) { sil9136_read_edid(priv->client); sysfs_notify(&priv->client->dev.kobj, NULL, "hpd_state"); dev_info(&priv->client->dev, "HPD high, display connected\n"); } else { priv->edid_ready = false; dev_info(&priv->client->dev, "HPD low, display disconnected\n"); } return IRQ_HANDLED; }这段中断处理代码里,gpiod_get_value读取 HPD GPIO 当前电平,sil9136_read_edid在插入事件发生时立即去读 EDID 数据并缓存在priv结构体里,sysfs_notify通知用户态监听者。这里的关键设计决策是:EDID 读取放在中断上下文里执行还是放到工作队列。EDID 读操作涉及多次 I2C 读取,每读一次要几十微秒,完整读一个 256 字节的 EDID 块大约需要几毫秒,放在中断上下文会阻塞其他中断处理,所以我一般会改成schedule_work或threaded_irq。dev_info打出的日志在调试阶段能看到插入/拔出事件的实时时间戳,对排查 HPD 抖动问题很有价值。
EDID 缓存机制方面,源码里常见的做法是维护一个struct edid *指针和edid_ready标志位。主控通过驱动提供的get_edid回调读取时,先检查标志位,如果为 false 就返回 -EAGAIN 让上层重试,而不是返回空指针。这种设计避免了对 EDID 数据并发读写导致的数据竞争问题。
3.4 视频参数配置序列:初始化时序里的关键寄存器
probe 函数通常会调用一组初始化序列,把芯片从默认状态切换到目标工作模式。初始化序列一般包含:软复位、时钟源选择、视频输出格式配置、I2C 总线速度设置、中断使能。以下是常见的软复位加基本配置代码:
static int sil9136_init(struct sil9136_priv *priv) { int ret; /* 软复位:置位 RESET 寄存器,等待芯片重新初始化 */ ret = sil9136_write_reg(priv->client, 0x00, REG_SOFT_RESET, 0x01); if (ret) return ret; usleep_range(5000, 10000); /* 配置主时钟分频,根据输入时钟频率选择合适的 PLL 参数 */ ret = sil9136_write_reg(priv->client, 0x01, REG_CLK_DIV, 0x08); if (ret) return ret; /* 设置视频极性:默认上升沿采样,可通过设备属性覆盖 */ ret = sil9136_write_reg(priv->client, 0x01, REG_POLARITY, 0x00); if (ret) return ret; /* 使能 HPD 中断,屏蔽其余中断避免频繁唤醒 */ ret = sil9136_write_reg(priv->client, 0x02, REG_INT_MASK, 0x01); if (ret) return ret; return 0; }这段初始化代码中,usleep_range而不是msleep是因为软复位等待时间在毫秒级以下,短延时用usleep_range更适合,因为它允许内核在睡眠期间调度其他任务,同时保持较高的唤醒精度。REG_CLK_DIV的设置直接影响输出时钟的频率,如果分频系数设置错误,画面会出现行场不同步、画面偏移或者完全无信号。设备树里可以增加clock-frequency之类的自定义属性,驱动里用device_property_read_u32读取后覆盖默认值,这就是后面改分辨率兼容性的入口点。
4. 编译、烧录与运行调试:从源码到设备起来
4.1 交叉编译环境搭建与 Makefile 修改要点
拿到驱动源码后,第一步是确认编译环境。如果是 ARM 平台,需要先找到对应的交叉编译工具链,常见的有arm-linux-gnueabihf-或aarch64-linux-gnu-前缀。然后把工具链路径加进 PATH,并在内核源码顶层目录执行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig确认 I2C 支持已开启。
驱动自身的 Makefile 通常长这样:
obj-m += sil9136.o KDIR := /path/to/kernel/source PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) cleanKDIR要改成你实际内核源码的路径,注意不是内核头文件目录,而是完整源码树,编译模块需要用到内核的 Kbuild 系统。执行make后如果生成sil9136.ko,说明编译通过。有些环境需要指定 ARCH 和 CROSS_COMPILE,可以在 make 命令行追加:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KDIR=/home/xxx/kernel-5.10如果遇到include/uapi/linux/...找不到头文件的报错,通常是内核源码没配置过或make modules_prepare没执行。在 KDIR 目录下先跑一次make modules_prepare能解决大部分这类问题。
4.2 设备树配置与模块加载流程
编译只是第一步,真正让驱动跑起来还需要设备树支持。把前面写过的那段设备树节点加到你的板级 dtsi 文件里,注意 I2C 总线编号要和硬件实际连接的控制器对应。如果 I2C2 上还挂着其他设备,确认地址没有冲突——sil9136 的地址跳线引脚可能会改从机地址,我遇到过复位后从 0x39 变成 0x3D 的情况,原因是某个 GPIO 状态影响了地址引脚的电平。
加载模块的命令:
insmod sil9136.ko或者用 modprobe 配合 depmod:
cp sil9136.ko /lib/modules/$(uname -r)/extra/ depmod -a modprobe sil9136设备加载成功后,dmesg | tail会看到 probe 成功的信息。确认 I2C 通信正常可以先用 i2cdetect 扫描总线:
i2cdetect -y 2如果看到39出现在列表里,说明芯片的 I2C 地址正确且响应正常。如果扫描不到,先查硬件:供电有没有到位、复位引脚电平对不对、I2C 上拉电阻是否焊接。这个排查顺序比反复改驱动代码效率高得多。
4.3 用户态验证:读 EDID 和状态切换测试
模块加载后,需要验证驱动功能是否完整。最直观的验证方式是读 EDID:
cat /sys/devices/platform/.../sil9136/edid > edid.bin如果能在设备树中配置好 sysfs 属性或者驱动里实现了show回调,这个命令就会把 EDID 原始数据导出出来。再配合edid-decode工具解析:
edid-decode edid.bin解析结果里能看到显示器名称、分辨率支持列表、色彩深度等关键信息。如果解析失败,大概率是 EDID 读取时序有问题——比如读取时芯片还没稳定,或者 I2C 读操作被 NACK。
模拟拔出和插入可以看中断日志:
echo 0 > /sys/class/gpio/gpioXX/value # 拉低 HPD dmesg | tail -5 # 应该看到 "HPD low, display disconnected"这一步验证的是中断边沿触发配置是否正确。很多板子的问题出在设备树里interrupts属性没有配置,或者 GPIO 管脚复用被其他外设占用,导致中断信号根本到不了处理器。
5. 避坑指南:移植 sil9136 驱动时常见的翻车现场
5.1 现象:probe 失败返回 -ENODEV,但 i2cdetect 能扫到设备
原因:设备树 compatible 字符串与驱动里of_match_table不一致。很多厂商源码里 compatible 写的是"sil9136",而你在设备树里写了"sil9136,i2c",匹配不上直接返回失败。
解决:把设备树的 compatible 改成和驱动源码of_device_id表里完全一致。或者反过来改源码里的匹配表,两者必须精确匹配,差一个字符都不行。排查方法是在probe函数入口加dev_info打印,看函数有没有被调用——没进 probe 说明匹配层就断了。
5.2 现象:模块加载正常但 EDID 读取超时
原因:I2C 时钟配置过快。sil9136 数据手册里对 I2C 时序有明确限制,很多主控 I2C 控制器默认跑 400kHz,但芯片工作电压偏低或者 PCB 走线过长时,400kHz 的实际建立时间不达标,读操作会频繁 NACK。
解决:把 I2C 时钟降到 100kHz 或 200kHz 重试。如果确认是硬件走线问题,检查 I2C 上拉电阻阻值,过大的上拉电阻(10k 以上)会拉慢边沿时间。我一般会在设备树 i2c 节点里设置clock-frequency = <100000>,这种玄学问题改驱动代码没用,先查硬件。
5.3 现象:HPD 中断频繁触发,系统日志被刷屏
原因:HPD 引脚悬空或存在电容耦合噪声,导致 GPIO 电平在临界点反复跳动,每次跳变都触发一次中断。
解决:硬件上在 HPD 引脚加 10k 下拉电阻(或上拉,取决于逻辑极性)和 100nF 电容滤除高频噪声。软件上在中断处理里加防抖逻辑:记录上次触发时间,如果间隔小于某阈值(比如 20ms)就忽略本次事件。我一般会在驱动里用一个jiffies时间戳比较来过滤掉小于 50ms 的连续跳变。
5.4 现象:显示画面偏移、花屏,但寄存器配置看起来没问题
原因:输出分辨率与主控端配置的分辨率不一致,或者视频时序参数(HTotal/VTotal)对不上。sil9136 内部虽然做了格式转换,但输入端的时序参数需要与主控输出严格匹配。
解决:核对主控端 LCD 控制器输出的pixel clock、h_back_porch、v_front_porch等参数。驱动源码里通常有一张预设分辨率表,检查当前配置是否选对了表项。我见过有人在设备树里改了输出分辨率,但驱动初始化序列里依然是默认分辨率,两边对不上导致画面异常。
5.5 现象:写入寄存器总是返回错误,但读取正常
原因:有机会是芯片进入保护模式或写保护使能。某些版本芯片在检测到输入信号异常时会锁定寄存器写入,需要通过特定序列解锁。
解决:检查手册里是否有关于写保护寄存器的描述,执行解锁序列——一般需要连续向某个 magic 地址写入特定值。再看驱动里有没有设置REG_WRITE_ENABLE之类的寄存器。如果源码注释里提到unlock sequence,确认初始化时有没有正确执行。这类问题往往卡住大半天,手册第一遍可能不会注意。
6. 二次开发:从改分辨率支持到扩展其他功能
拿到这份资源后真正值钱的地方在于改造。我建议的改法分三个档次:改参数适配新屏幕、改驱动结构适配新内核、加功能实现自定义逻辑。
改参数适配新屏幕是基础操作。假设你换了一块 1080p 的 LCD 面板,需要确认两处:一是设备树里display-timing节点的参数,包括 active 区域的宽高、前后肩、像素时钟;二是驱动源码里初始化序列中视频输出相关寄存器的配置。我一般习惯先改设备树跑一遍,因为内核的 display timing 解析有日志输出,能看出参数是否被正确读取,再回去改寄存器更稳。
适配新内核是不可避免的功课。把旧版驱动往 5.15 内核移植时,重点看这几个地方:i2c_new_device改成i2s_new_client_device,of_property_read_u32换成device_property_read_u32(兼容 ACPI 和 device tree),gpio_request_one换成devm_gpiod_get。中断申请函数从request_irq改成devm_request_threaded_irq并利用线程化中断做耗时操作。每次内核大版本升级,这些 API 变动都会有文档说明,但在实际项目里最稳妥的方式是找一个同内核版本的最新驱动做参考,对着改。
加入自定义功能这块,如果项目需要支持音量调节或者音频通道切换,可以在sil9136.c里增加对应的 IOCTL 接口或者 sysfs 节点。参考现有属性文件的做法,加一个audio_mute的 sysfs 节点,写 1 时调用寄存器设置静音,读时返回当前状态。这样用户态程序只需要读写文件就能控制音频通断,省去写应用层通讯协议的麻烦。
做这些改动时我有个习惯:每改一个寄存器或者 API 调用,先在旁边注释里写明为什么这么改,关联手册哪个章节。最初是给自己留后路,后来发现这习惯救了周围不少人——有些寄存器配置隔三个月回来看完全想不起当初为什么这么设。从那以后我每次拿到新芯片驱动,都会强制自己先花半小时把目录结构和关键宏定义扫一遍再动手改。希望帮到你。
本文还有配套的精品资源,点击获取