1 基础知识
pca9548芯片系统框图如下图1.1所示:
图1.1
如上图所示,通过配置pca9548的寄存器,即可切换开关完成i2c switch。在子总线下设备地址没有冲突的情况下,可以接通多个子总线,如同时接通子总线0和7。
pca9548内核驱动支持自动切换i2c switch,前提是配置好设备树。设备树的配置示例如下图1.2所示:
图1.2
通过上图1.2所示配置后,pca9548的内核驱动程序会注册虚拟的i2c总线16-23,这样一来,上层可使用这些虚拟总线来读取所挂i2c设备的信息。比如,虚拟总线16对应图1.1中子总线0,当上层使用虚拟总线16读时,内核驱动将自动接通图1.1子总线0对应的开关,并将其他开关关闭,也就是说,内核驱动会保证只有一个开关接通。
通过这种方式,我们可以将实际物理总线3,扩展为8条虚拟总线16-23,虚拟总线之间互不干扰,可以解决相同i2c设备地址相同问题。
2 问题描述
以某主板为例(BMC为ARM芯片),其i2c部分拓扑如下图2.1所示。
图2.1
如上图2.1所示,通过配置设备树可将实际物理总线3(BMC为ARM芯片),扩展成24条虚拟总线16-39。现在分析一下多个pca9548并联之后会存在什么问题:
(1)依次读取16-39虚拟总线上0x9e地址对应设备上数据;
(2)在读16-23虚拟总线上0x9e设备时,pca9548内核驱动会依次打开对应通道开关并关闭其他通道开关(图1.1),数据读取正常;
(3)当读取完23号虚拟总线上0x9e设备数据后,便转而去读取24号虚拟总线上0x9e设备,内核驱动会打开0xE2地址对应pca9548的子总线0开关,但并不会去关闭0xE0地址对应pca9548的子总线7开关,此时,图2.1上红线1和红线2都接通,因此设备地址0x9e冲突了。
内核驱动不知道pca9548之间是并联关系,因此不会去关闭23号虚拟总线对应的开关,这便是问题所在。
既然不能由内核自动切换i2c switch通路,那就只能手动来切i2c switch通路了,以往insyde BMC就是使用手动切换方式。
3 手动切换i2c switch
3.1 理论分析
如果手动切i2c switch通道的代码,要实现代码通用,难点在于:如何处理i2c switch芯片之间并联关系?
容易想到的方法是:用一张表描述pca9548芯片之间并联关系,由代码遍历这张表,从而知道读取某个i2c switch子总线前,是否需要关闭其他i2c switch芯片通道。这种方式需要考虑较多,比如第一层i2c switch之间并联可能个数、i2c switch的子总线下面是否有i2c switch并联、i2c switch芯片不同如何处理等。
既然i2c switch芯片并联关系较难梳理清楚,那我们就通过串联方式去处理i2c设备地址冲突问题。一条虚拟i2c总线只有唯一一条通路,比如图2.1虚拟总线32上再接一个地址为0xE6的pca9848,又扩展出8条虚拟总线40-47,但47号虚拟总线对应的i2c switch通路是唯一的,即需要打开0xE4 pca9548子总线7对应开关,同时打开0xE6 pca9548子总线7对应开关,这样才能读取虚拟总线47上的i2c设备,如果在读取完i2c设备数据之后,又反向把通路上i2c switch芯片上开关关闭呢?
这样一来,每次读某个虚拟总线上i2c设备时,先切换i2c switch通路,然后读取i2c设备数据,最后又关闭i2c switch通路。
3.2 代码实现
3.2.1 i2c switch描述结构
首先需要定义一个通用结构,用来描述i2c switch信息,结构如下图3.1所示。
图 3.1
u8HubAddr:i2c switch芯片的8位i2c地址;
u8CloseFlag:此i2c switch是否需要执行关闭操作(有的i2c switch无法关闭);
au8OpenReg:打开i2c子总线所需写的寄存器;
au8CloseReg:关闭i2c子总线所需写的寄存器;
u8RegSize:寄存器大小;
au8OpenData:打开i2c子总线所写寄存器的数据;
au8CloseData:关闭i2c子总线所写寄存器的数据;
u8DataSize:数据大小。
3.2.2 i2c switch实现
i2c switch函数处理流程:
图3.2
i2c switch函数部分代码实现如下图3.3所示:
图3.3
如图3.3所示,将i2c switch信息填充到发送Buf中,再通过i2c读写函数将数据写到到i2c switch寄存器中,进而完成通道切换,值得注意的是,当某一层i2c switch切换失败时,必须把之前所打开的i2c switch通道关闭。
3.2.3 i2c close实现
i2c close函数处理流程图:
图3.4
I2c close函数部分代码实现如下图所示:
图 3.5
如图3.5所示,只有当前i2c switch层设置了关闭标志,才会去执行关闭动作。
注意的是,真正手动切换i2c switch时,需要在切换前加锁,在对i2c设备读写完数据后,关闭i2c switch通路并解锁,以防止线程本次读写完成前,被其他线程切换i2c switch通道。
既然手动切换i2c switch方式可以在读完数据后关闭通道,那自动切换i2c switch方式是否也可以?
4 自动切换i2c switch
4.1 pca954x驱动prebo
以pca954x驱动为例,内核版本为5.2.0,驱动代码简化后如下图4.1所示:
图4.1
如上图所示,省略其他无关代码后,整个probe过程分为6个步骤:
(1)检查当前i2c总线是否支持SMBUS_BYTE读写操作;
(2)为pca954x分配私有数据结构,并填充pca954x_select_chan函数和pca954x_deselect_mux函数;
(3)获取当前pca954x具体芯片的信息,比如pca9548信息内含多少个通道、是否支持中断、是否有使能引脚等;
(4)尝试写一个数据到i2c switch中,以便确定i2c switch真实存在;
(5)设置i2c switch芯片的空闲状态,在pca954x_deselect_mux函数中使用;
(6)为每个i2c switch通道,也就是子总线注册虚拟的总线(adapter),虚拟总线使用的通信方法继承于上一级总线。
以pca9548芯片为例,多级pca9548物理与软件对应关系简图如下图4.2所示。
图4.2
如上图4.2所示,虚拟总线40的传输函数,其实就是在虚拟总线16的传输函数前后增加了select和deselect函数,而虚拟总线16的传输同样是在物理总线3的传输函数前后增加了select和deselect函数,最终还是由物理总线3的传输函数实现对i2c设备读写数据。
4.2 pca954x通道切换
select函数就是pca954x_select_chan,其代码实现如下:
图4.3
Select函数用于在真正的i2c读写数据前,切换pca954x芯片的通道(仅通道不同时切),注意,select操作其实还是调用上一级i2c总线的传输函数(上一级总线可能还是虚拟总线)。
Deselect函数就是pca954x_deselect_mux,其代码实现如下:
图4.4
上图中idle_state默认情况下如图4.1所示为MUX_IDLE_AS_IS(-1),即保持当前通道状态,也就是说,执行pca954x_deselect_mux函数将不做任何改变,原来打开的通道依然保持,这样一来,若pca9548并联就会出现i2c地址冲突问题。
4.3 读写实例
当满足idle_state == MUX_IDLE_DISCONNECT条件时,则执行pca954x_deselect_mux函数时,将会关闭当前pca954x芯片所有通道,下面以图4.5为例分析一下具体读写过程:
图4.5
初始状态0xe0 pca9548通道0和0xe6 pca9548通道0都关闭。当对i2c虚拟总线40上的0x9e设备读写时,将调用40:i2c_transfer函数,该函数分三个步骤:
1.调用select片选函数,而片选的操作本质上就是调用上一级i2c传输函数(16:i2c_transfer)写地址为0xe6的pca9548芯片寄存器(箭头1)。16:i2c_transfer函数内部同样也分三个步骤:
(1)select调用3:i2c_transfer函数写0xe0 pca9548寄存器打开其通道0(箭头2);
(2)再调用3:i2c_transfer函数写0xe6 pca9548寄存器打开其通道0(箭头3);
(3)deselect调用3:i2c_transfer函数写0xe0 pca9548寄存器关闭其通道0(箭头4)。
此时,数据未读写,0xe0 pca9548通道0关闭,0xe6 pca9548通道0打开。
2.调用传输函数读写0x9e设备,实际上是调用上一级i2c总线传输函数(箭头5),即16:i2c_transfer函数,内部又是三次调用:
(1)select调用3:i2c_transfer函数写0xe0 pca9548寄存器打开其通道0(箭头6);
(2)再调用3:i2c_transfer函数读写0x9e设备数据(箭头7);
(3)deselect调用3:i2c_transfer函数写0xe0 pca9548寄存器关闭其通道0(箭头8)。
此时,数据已读写,0xe0 pca9548通道0关闭,0xe6 pca9548通道0打开。
3.调用deselect函数,关闭0xe6 pca9548通道0,同样还是调用上一级i2c总线传输函数(箭头9),即16:i2c_transfer函数,内部又是三次调用:
(1)select调用3:i2c_transfer函数写0xe0 pca9548寄存器打开其通道0(箭头10);
(2)再调用3:i2c_transfer函数写0xe6 pca9548寄存器关闭其通道0(箭头11);
(3)deselect调用3:i2c_transfer函数写0xe0 pca9548寄存器关闭其通道0(箭头12)。
此时,数据已读写,0xe0 pca9548通道0关闭,0xe6 pca9548通道0关闭。
由上分析可知道,当满足idle_state == MUX_IDLE_DISCONNECT条件时,完成一次i2c设备读写操作后,路径上的i2c switch通道都关闭,因此不会出现i2c设备地址冲突问题。
由图4.1中第5步可配置idle_state == MUX_IDLE_DISCONNECT,前提是在设备树中配置“i2c-mux-idle-disconnect”属性。of_property_read_bool函数会去读pca954x设备节点中的“i2c-mux-idle-disconnect”属性,只要其存在就返回TRUE(不需要赋值),配置之后如下图4.6所示。
图 4.6
通过上图所示配置后即可解决第二小节描述问题,并且设备树可灵活配置只让有并联关系pca954x获得“i2c-mux-idle-disconnect”属性(如图4.7红底pca9548),从而让有并联关系的pca954x保持常态关闭,而串联关系的pca954x保持上一次通道状态。
图4.7
5 总结
5.1 自动切换方案与手动切换方案对比
自动切换i2c switch优劣势:
优势:
(1)内核驱动管理切换动作:上层可将扩展出来的虚拟i2c总线当物理i2c总线使用,完全不需要关心i2c switch是如何切换的;
(2)不用考虑竞争关系:pca954x内核驱动带有锁机制;
(3)执行效率高:由内核驱动完成切,不需要用户态与内核态频繁切换。
劣势:
(1)仅支持pca954x芯片:如混用其他i2c switch芯片,将无法配置;
(2)不够灵活,部分情况无法使用:pca954x芯片需要通过设备树提前配置好,并需要在内核启动阶段probe成功,比如某个内含pca954x芯片扩展卡,在主机端上电情况下才会上电,那BMC启动时将probe失败,又比如一个PCIE卡槽既可以插扩展卡,又可以直插PCIE卡时,也无法配置设备树。
手动切换i2c switch优劣势:
优势:
(1)可以i2c switch芯片混用;
(2)代码自己实现,可灵活适配各种场景;
劣势:
(1)需考虑竞争关系:需要在代码中实现锁机制;
(2)执行效率比自动切换方式低:切i2c switch通道、读写i2c设备、关闭i2c switch通道,均由上层下发,因此执行效率不如自动切换方式;
(3)代码复杂度增加。
实际选择哪种方案需根据具体应用场景综合考虑。
5.2 总结
Pca9548通过如下配置,即可扩展i2c总线,并且保证i2c switch常态关闭:
Pca9548下再连pca9548的配置示例如下: