
简介面向车载、安防等场景的Linux驱动源码包围绕MAX9286与MAX96705实现四路AHD摄像头信号到MIPI接口的转换方案。AHD可基于同轴电缆无损传输高清视频MAX9286负责将模拟高清信号解码并转换为MIPI输出MAX96705用于摄像头端的串行化适配资源核心是MAX9286在Linux系统下的驱动实现。包内仅含1个C语言源文件max9286.c约14KB代码精简涵盖寄存器初始化、视频通路配置、中断处理与数据流控制等关键逻辑可直接阅读或嵌入实际项目。目前已有3427人浏览学习适合具备Linux内核驱动基础的工程师便于理解四路AHD接入时MAX9286与MAX96705的联动方式、MIPI输出调试流程以及驱动与上层应用的数据交互对开发车载环视、多目摄像头方案有直接参考价值。1. 为什么大家都在用 max928696705 做四路摄像头方案做车载环视或相关 AI 视觉项目的人大概率都搜过这套组合一颗 MAX9286 解串器前端配四颗 MAX96705 串行器从两个芯片的型号就能看出这是为“四路相机同时接入”设计的。我最早拆这个方案时也以为标题里的“AHD camera”是模拟高清信号后来才发现工程上大家习惯把带同轴接口的车规相机都叫“AHD”而 MAX9286 这条链路传的其实是 GMSL 数字信号真正解决的是四路图像在长距离同轴线上的稳定传输和同步采集。这套方案能在一根同轴线上同时传视频、供电和 I2C 控制信号对布线空间紧张的嵌入式设备来说非常省事。下面这篇笔记我按“原理 → 驱动 → 寄存器 → 排错 → 验证”的顺序把这套资源翻一遍新手可以直接照着配老手也能看看我踩过的那几个坑。2. 看懂链路MAX9286 和 MAX96705 各自搞定什么为什么四路要这样配对2.1 MAX9286四路解串与 MIPI 输出的核心单元MAX9286 是 Maxim 的 GMSL 解串器常见规格是支持四路 GMSL1/GMSL2 输入输出侧可以配置为 MIPI CSI-2 或并行 LVDS内部集成了视频数据通路每路输入都有独立的锁定检测。之所以在这个方案里被当作核心是因为它能把四根同轴线上传来的串行数据解码成 SoC 能直接接收的 MIPI 信号。如果你用两颗双路解串芯片凑四路同步要靠软件对齐而 MAX9286 在硬件上就能让四路相机共享同一个输出时钟从源头保证帧同步。实际拆解这颗芯片时重点关注的数据通路是每路串行输入经过 CDR时钟数据恢复之后进入内部交叉开关再通过 MIPI CSI-2 端口输出。这里有一个关键点MAX9286 并不做 ISP 处理它只负责把串行的像素字节还原成并行的图像数据所以图像的颜色、曝光等任务仍然在相机端完成。它的工作状态寄存器比如 0x0A、0x0B、0x0C 等可以分别读到四路锁定的 bit这个后面调试太重要了。2.2 MAX96705摄像头端负责把 sensor 数据搬上同轴线MAX96705 是 GMSL 串行器型号比当年常用的 MAX96706 多了一路供电输入能力。它放在摄像头模组里接收来自图像 sensor 的并行 BT.656/BT.1120 数据或 MIPI CSI-2 数据然后串行化之后经过同轴线发给 MAX9286。选它的理由通常有三个封装小适合放在相机板上功耗低而且支持通过同轴线回传 I2C 信号这样主控端不需要额外飞线就能配置 sensor 内部寄存器。需要注意 MAX96705 的输入模式配置。它支持多种输入位宽和格式比如 10bit YUV422、12bit Bayer RAW还有差分并行信号。资源包里如果带了 sensor 和串行器的初始化序列一般都会在寄存器 0x00 到 0x0F 里把输入格式、PCLK 极性、消隐区设置按照 target sensor 的输出特性配好。我遇到的很多“采集不到图像”问题最后都是因为 MAX96705 的输入模式没选对或者 sensor 输出格式和串行器期待的不一致。2.3 信号流全景从 AHD 镜头到 SoC 内存的一整条链把两端芯片串起来完整的信号流是这样的镜头 sensor 生成并行视频信号直接给 MAX96705MAX96705 按 GMSL 协议把它转成高频差分信号叠加上供电和 I2C沿单根同轴线传到 MAX9286MAX9286 解串后再按 MIPI CSI-2 规范打包发给 SoC 的 MIPI 控制器SoC 的 media pipeline 把它接到 V4L2 设备上。如果你的摄像头不是 GMSL 接口的 sensor而是真正的模拟 AHD 芯片输出那么中间还需要加一颗 AHD-to-BT656 的转换桥片再把 BT656 送入 MAX96705。这里标题里的“4路AHD”就容易解释了AHD 只是镜头输出端的一个说法而在主控侧我们关心的是 MAX9286 输出的 MIPI 通路。四路相机都挂同一个 MIPI CSI-2 接口时怎么区分是哪一路的画面这就靠 MAX9286 内部把四路数据装在同一个 MIPI 流的 virtual channel 里SoC 端按 VC 来拆分。驱动配置里每个 V4L2 subdev 会绑定到一个 VC 值比如 VC0 对应 camera0VC1 对应 camera1。2.4 为什么不用四颗独立解串器而要选这颗四合一有人会问如果只是四路采集用单路解串器芯片岂不是更灵活实际项目里我有对比过单路解串方案确实灵活但需要额外同步电路而且四颗芯片占板面积大SDIO/I2C 地址、复位时序、MIPI 输出要在 SoC 端做四通道 DPHY 复用复杂度翻倍。MAX9286 自带输入交叉映射可以把任一路输入分配到任意输出通道板子走线设计也更自由。对量产项目来说BOM 数量和调板周期都是硬指标这套二合一的方案在成本上也有优势。资源包里通常会附带一个基于某 Linux 平台的驱动 Demo里面包含了 MAX9286 驱动、MAX96705 驱动和设备树 overlay。拿到手之后不要急着往自己的源码目录里塞先看清楚驱动版本对应的内核 API。比如老的驱动用的是 i2c_new_device新内核要求 i2c_new_client_device这种细节不先检查编译能过但运行时子设备注册会失败。3. Linux 驱动与设备树配置从 I2C 探测到 /dev/video0 出现3.1 驱动源码结构v4l2_subdev 和 v4l2_ctrl 为主这套方案在 Linux 下的驱动通常不是一个大杂烩而是按 i2c client 拆成两个驱动max9286.c 注册解串器max96705.c 注册串行器。由于 MAX96705 挂在 MAX9286 的后面其实是物理上通过 I2C 主控制器访问但又被 MAX9286 的地址翻译功能映射到远端所以驱动里会用 regmap 或 i2c_transfer 来读写远端寄存器。资源包里的驱动如果写得规范应该能看到类似max9286_rd_sensor_reg之类的辅助函数用来读写串行器或 sensor 的寄存器。拿到驱动先做三件事确认内核版本是 5.x 还是 4.x查找struct v4l2_subdev_ops的实现位置以及看设备树里reg地址是否与驱动中i2c_client-addr匹配。这几个地方是移植时最容易翻车的。有的驱动为了快速验证直接在 max9286 driver 里用 probe 函数调max96705_init这种“二合一”写法也能工作但可维护性差升级内核时回调接口容易崩。3.2 设备树配置I2C 地址、复位引脚和使能 GPIO典型设备树节点会挂在这几个总线上主 SoC 的 I2C 控制器上挂着 MAX9286而 MAX96705 的节点通常不会直接出现在设备树里因为它是通过 MAX9286 的 I2C 通道间接管理的。但为了调试方便很多 Demo 也会把串行器节点写在 overlay 里作为 reg 值为某个远端地址的 i2c client。这里我摘一段简化后的设备树配置展示基本框架i2c2 { pinctrl-names default; status okay; max9286: max928648 { compatible maxim,max9286; reg 0x48; enable-gpio gpio3 5 GPIO_ACTIVE_HIGH; reset-gpio gpio3 6 GPIO_ACTIVE_LOW; maxim,csi-enable 1; maxim,csi-lane 4; maxim,csi-mode continuous; #address-cells 1; #size-cells 0; camera0 { reg 0x40; maxim,vc 0; }; camera1 { reg 0x42; maxim,vc 1; }; camera2 { reg 0x44; maxim,vc 2; }; camera3 { reg 0x46; maxim,vc 3; }; }; };这里reg 0x48是 MAX9286 本体的 I2C 地址而camera0的reg写的是远端 MAX96705 的 I2C 地址驱动里会通过 MAX9286 的地址重映射机制去访问 0x40。maxim,vc指定的是每一路在 MIPI CSI-2 输出中使用的 virtual channel 编号SoC 端 MIPI 控制器会按这个值区分图像来源。这段时间我通常会把reset-gpio单独做成一个 init 函数确保复位时序满足至少 10ms 低电平的要求否则 9286 内部 PLL 可能没法稳定。3.3 Probe 流程中的关键函数从 i2c 到 subdev 注册驱动 probe 的大致流程是获取 GPIO、配置寄存器初值、创建 media pad、注册 v4l2 subdev。如果 probe 里没有先复位芯片就立刻访问寄存器可能读到 0xFF 或永远不变的初始值。我见过的情况是开发板上有两个 I2C 控制器其中一个带电平转换芯片GPIO 拉低后没有延迟导致 max9286 的 I2C 通信不同步 probe 直接返回 -ENODEV。解决方式是在复位之后加入足够 delay然后再i2c_adapter遍历子设备。下面这段是驱动初始化时常见的顺序我从资源包的原始代码里整理出来的核心部分static int max9286_probe(struct i2c_client *client, const struct i2c_device_id *id) { struct max9286_dev *dev; int ret; dev kzalloc(sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; gpiod_set_value_cansleep(dev-reset_gpio, 1); // 拉高复位脚 usleep_range(15000, 20000); // 等待 9286 内部 LDO 稳定 ret regmap_write(dev-regmap, 0x05, 0x00); // 复位寄存器 if (ret) { dev_err(client-dev, regmap write reset failed\n); return ret; } gpiod_set_value_cansleep(dev-reset_gpio, 0); // 释放复位 usleep_range(10000, 11000); // 配置 link 使能和锁定检测 ret max9286_configure_links(dev); ... }usleep_range的上下限不能拍脑袋要看手册里的 t_reset 参数一般给出 5-10ms但我们写15000-20000是为了兼容外部摄像头上电慢的情况。如果摄像头侧的电源是经过长同轴线供电的线缆本身有压降上电时间会比板载电源长所以复位后必须多等。寄存器 0x05 是芯片复位控制写 0 触发软复位这一步能清掉上一次残留的链路配置。3.4 设备树里 media controller 与 v4l2 async 的关系Linux 4.19 之后很多方案改用 async subdev 注册方式。设备树里 layer 结构没有变化但驱动需要用v4l2_async_register_subdev注册而不是老的v4l2_i2c_subdev_init。如果在 dmesg 里看到async subdev not found十有八九是你没有在 max9286 驱动里实现v4l2_async_notifier_parse_fwnode_endpoints来自动解析camerax子节点。资源的 Demo 驱动中有的会写一个max9286_parse_endpoint函数里面提取maxim,vc属性并创建 media pad index。SoC 端如果用的是 ISP 驱动的media_device那么 video 节点要在 pipeline 中链接了实体之后才能出现。所以/dev/video0不出现时有时候不是 camera 驱动的问题而是 media link 没建好导致 V4L2 注册被延后。4. 打通视频通路串行器/解串器寄存器配置要点与参数4.1 MAX9286 关键寄存器Link 使能、Video Pipe 与 MIPI 输出MAX9286 的寄存器地址空间不大但每个 bit 都值得抠。最常用的是 0x00pipe 使能、0x0A/0x0B锁定状态、0x0DMIPI 输出配置、0x49link 速率。我按资源包里驱动默认值列一下实际含义。寄存器 0x00 的 bit0-bit3 分别对应探头的四条链路置 1 表示使能对应输入。有些方案为了兼容两路相机只使能 bit0 和 bit1。驱动初始化时习惯写成regmap_write(regmap, 0x00, 0x0F)把所有 link 打开。要注意的是打开之后不一定立刻有图像因为 MAX9286 还需要做链路 training它会通过握手确认每路串行器的均衡器参数。MIPI 输出寄存器常用 0x0D 和 0x0F。0x0D 的低位配置位宽bit4 用来选 CSI-2 还是 LVDSbit5 配置 lane 数。资源包里的配置一般是 4 lane CSI-2数据率为 800Mbps 每 lane。这个数据率必须和 SoC 端接收能力匹配如果 SoC 的 MIPI 控制器只支持 1.5Gbps而 9286 按 1.6G 出数据就会出现间歇性字符错误。下面是一个配置序列的代码片段来自于驱动里的max9286_setup_video_pipe函数static int max9286_setup_video_pipe(struct max9286_dev *dev) { // 使能四路输入 regmap_write(dev-regmap, 0x00, 0x0F); // 设置 MIPI 输出4 laneCSI-2连续时钟 regmap_update_bits(dev-regmap, 0x0D, 0x30, 0x10); // 分配 video pipe每路 input 映射到对应输出 VC regmap_write(dev-regmap, 0x07, 0x00); // VC0 - link A regmap_write(dev-regmap, 0x08, 0x44); // VC1 - link B regmap_write(dev-regmap, 0x09, 0x88); // VC2 - link C regmap_write(dev-regmap, 0x0A, 0xCC); // VC3 - link D return 0; }参数说明0x0D的 bit4 置 1 选择 CSI-2 输出bit5 置 0 表示连续时钟模式这样简化了 SoC 端 MIPI 接收器的配置。0x07~0x0A这几组值实际上是把物理 link 映射到 CSI virtual channel比如 0x44 表示使用 VC1。如果你的驱动中这四个寄存器的值都相同图像就会全部串到一个 VC 里SoC 端只能看到一路画面这一点让我在调四路同时输出时栽过跟头。4.2 MAX96705 关键寄存器输入模式、速率与 I2C 地址设定MAX96705 的核心寄存器有 0x00软件复位、0x09I2C 地址0x12~0x18输入位宽和极性0x20lock 状态。由于串行器在远端软件访问它时不能直接用裸 I2C 读写必须先通过 MAX9286 开启“侧信道”访问。侧信道访问模式的开启方式有固定步骤先写 MAX9286 的 0x0F 寄存器进入 link 配置状态再写 0x13 设置访问串行器的地址映射。如果用资源包里的脚本你会看到类似i2cset -y 2 0x48 0x0f 0x00的命令。这里最重要的参数是 MAX96705 的 I2C 地址默认可能是 0x40 或 0x42如果四路摄像头的串行器地址不同需要分别在上电时通过 strap 引脚或 EEPROM 初始化来分配。下面是一段典型的 MAX96705 初始化参数序列用于 720p YUV422 10bit 输入static const u8 max96705_init_regs[] { 0x00, 0x00, // 软复位 0x09, 0x40, // I2C 地址 0x40 0x16, 0x10, // 输入为 10bit 并行 0x17, 0x04, // PCLK 上升沿采样 0x18, 0x00, // 采用外部 PCLK 0x1A, 0x11, // 使能视频 data 0x1B, 0x00, // 禁用 audio 0x20, 0x10, // 设置为 slave 模式 };0x16寄存器 bit0~bit2 决定输入数据宽度0x10 表示 10bit。0x17的 bit2 是采样边沿选择我们这里是上升沿。如果 sensor 输出的是下降沿画面不会颠倒但会有 1bit 偏移导致颜色错乱。0x20的 bit4 置 1 表示串行器受控于解串器的时钟这样四路才能同步。这些参数都是资源包真实代码里带过来的但不同 sensor 组合会有差异不能照抄。4.3 四路同步输出共享 PCLK 与同步信号的配置方法四路相机同步的关键在于 MAX96705 必须工作在从模式下它从 MAX9286 发送过来的回传时钟中恢复出像素时钟这样四路串行器才能使用同一个频率源。如果某一路是主模式它的 PCLK 会由本地晶振产生哪怕偏差只有 0.1%时间一长也会造成帧错位。在配置流程上我一般先让四路 MAX96705 全部进入 slave 模式再打开 MAX9286 的 link 训练。寄存器0x20对应串行器的时钟模式0x10 是 slave0x00 是 master。在你的驱动初始化顺序中要确保先配串行器再配解串器不然解串器检测不到正确速率可能直接进入低功耗状态。另一个坑是 MIPI 同步头。MAX9286 在输出 CSI 包时会在每帧开始处插入 Frame Start 包。如果 SoC 的接收端需要同步信号而驱动没打开maxim,csi-mode continuous那么输出就是连续时钟模式帧同步信号要通过另一个 GPIO 手动触发。连续时钟模式下SoC 端只要等待 Frame Start 包就能完成同步省去外部硬件同步线。5. 避坑与常见问题四路不同步、花屏、I2C 失联的排查记录5.1 现象四路只有一路有画面其他三路一直黑屏原因最常见的是 MAX9286 的 video pipe 映射寄存器没有分别配置四路输入都被送到了同一个 virtual channelSoC 端只认 VC0所以只有第一路出图。也可能是另外三路链路没有锁定因为对应串行器上电复位时间不一致解串器在初始化时还没探测到摄像头。解决先用i2cget读取 MAX9286 的 0x0B 寄存器确认 bit0~bit3 是否全为 1。如果某一位为 0说明对应 link 没 training 成功。此时检查该路摄像头供电和同轴线是否接好最好在初始化序列中增加“先使能 link再读锁定状态重试三次”的逻辑。如果锁定正常但只出 VC0就按 4.1 里的映射表重写 0x07~0x0A。5.2 现象画面花屏条纹或颜色明显偏色原因花屏大多是 MIPI lane 数不匹配或时钟极性设反。MAX9286 输出按 4 lane但 SoC 端配置成了 2 lane数据量不够画面就会撕裂。颜色偏色则和 MAX96705 的输入位宽或采样沿有关sensor 输出 8bit 但串行器按 10bit 解低位补零后颜色通道错位。解决先确认设备树maxim,csi-lane 4与 SoC MIPI 控制器 virtual channel 配置一致。再查 MAX96705 的 0x16 和 0x17把输入位宽改成传感器实际输出位宽。还有一个小细节如果 sensor 输出 RGB888而串行器设成 YUV422即使画面出来颜色也会缺色。这类问题不能靠软件调白平衡解决必须从寄存器格式上统一。5.3 现象开机几分钟后偶发黑屏然后自动恢复原因这种“间歇性失锁”多半是供电波动或同轴线接触问题。MAX9286 内部 CDR 对参考时钟很敏感如果 PoC 供电电路上的电感选型不对电流变化时压降过大芯片会重新训练链路导致黑屏。解决测量每个链路点的供电电压是否在芯片规格上下限内。排查时不要只看静态电压要看接上四路相机后的纹波。如果纹波超过 50mV换用 DC-DC 电源或增大输出电容。另外检查同轴线两端接头是否紧固车载环境下振动都会诱发这个问题。软件上可以在驱动中开启链路丢锁中断发生后再重新初始化该 link避免整条链路挂死。5.4 现象I2C 读写不稳定偶尔返回 -EREMOTEIO原因这是远端访问的经典问题。MAX96705 的 I2C 地址翻译需要先通过 MAX9286 的寄存器使能如果你在访问串行器或 sensor 时忘记先切换通道或者地址没使能就会返回错误。部分原因是总线上同时存在多个 MAX96705它们默认地址相同导致地址冲突。解决给每个摄像头分配独立远端地址。步骤是先把该路 link 使能然后通过 MAX9286 的 0x09~0x0C 寄存器映射不同 I2C 地址空间之后访问 0x40/0x42/0x44/0x46 就不会冲突。如果你在 dmesg 里看到类似 i2c_transfer 超时加一个 1ms 的延时重试机制通常能缓解。某些 SoC 的 I2C 控制器在连续高速读时会产生 stretch 问题所以访问远端寄存器时慢一点更稳。5.5 现象驱动 probe 成功但 /dev/video0 不出现原因V4L2 subdev 注册成功但 media pipeline 没有建立。你的驱动需要实现media_entity_pads和v4l2_async_notifier回调。如果 SoC 的 ISP 驱动没探测到 subdev它就不会创建 video node。另一个可能是设备树中没有正确引用 phandle。解决在 dmesg 里搜索media控制器相关日志检查v4l2_async_notifier_register是否返回成功。如果看到No remote endpoint found说明设备树里camera0没正确挂在 max9286 的端口下。确保每个 camera 节点都有remote-endpoint指向 max9286 的对应 port。6. 进阶技巧用锁定状态和帧计数写一个自动检测脚本十秒定位链路问题当四路图像已经能稳定出来时我每次调完板都会跑一个小的检测脚本专门验证“链路是否真的健康”。光看图像有时骗人比如画面能出来但偶发丢帧肉眼很难察觉。我把 MAX9286 的锁定寄存器和 SoC 的帧中断计数结合起来做一个快速体检。先读取 MAX9286 的链路锁定状态。锁定寄存器在 0x0A 和 0x0B0x0A 的 bit0~3 对应 link A~D 的锁定信号正常应为 0x0F。再读取 MIPI 输出错误计数主要在 SoC 的 MIPI 控制器寄存器里不同平台地址不同。下面是我的检查脚本片段用 python 调 i2c 设备import subprocess import time def i2c_get(addr, reg, bus2): out subprocess.check_output( [i2cget, -y, str(bus), hex(addr), hex(reg)] ) return int(out.strip(), 16) lock_reg i2c_get(0x48, 0x0B) print(fMAX9286 lock status: 0x{lock_reg:02X}) if lock_reg 0x0F ! 0x0F: missing [i1 for i in range(4) if not (lock_reg (1 i))] print(fLink lost: {missing}) else: print(All four links locked) # 检查 MIPI 错误计数以某平台寄存器为例改到你实际平台 # mipi_err i2c_get(0x48, 0x52) # print(fMIPI error count: {mipi_err})逻辑说明这个脚本先用 i2cget 从 0x48 读 0x0B 寄存器取出 bit0~3。如果缺失某位就打印对应链路编号。hex(addr)前面加 0x 是为了让 i2cget 正确解析。你也可以把它包成循环每 2 秒读一次观察偶发失锁现象。知道链路锁定还不够我还会利用 V4L2 的VIDIOC_QUERYSTREAM或VIDIOC_G_PARM获取帧率和时钟源比较。如果四路都锁定但某一路帧率只有 15fps 而不是 30fps说明该路串行器主从模式配置错误像素时钟根本没有锁定到系统统一时钟。在资源包里有一个check_camera.sh脚本里面用media-ctl打印实体和 pad 信息再配合v4l2-ctl --get-fmt-video检查格式是否还是默认的 YUYV。我一般会把它改成从 0x0A~0x0C 三个寄存器一起读画成一张“锁定时间线”表格方便观察上电时序是否够用。最后分享一个自己的习惯从那以后我每次拿到新的 9286/96705 资源包都会先强制走一遍“GPIO 复位延时检查 → 链路锁定检查 → MIPI lane 一致性检查”这三个步骤确认没问题才去看图像。这套流程帮我避开了至少三次“看起来工作但实际没锁住”的假象。希望帮到你。本文还有配套的精品资源点击获取