【嵌入式环境下linux内核及驱动学习笔记-(15)linux总线、设备、驱动模型之I2C总线】

news/2024/11/24 4:57:40/

目录

  • 1、 I2C总线机制
    • 1.1 导入
    • 1.2 时序
    • 1.3 地址格式
  • 2、华清fs4412上I2C的实现
    • 2.1 寄存器
    • 2.2 寄存器位具体含义
    • 2.3 fs4412上针对具本设备的I2C工作逻辑
      • 2.3.1 主机读写工作流程**
        • 2.3.1.1 主机发送时序及操作流程
        • 2.3.1.2 主机接收的时序及流程
      • 2.3.2 从机读写工作流程
  • 3、LINUX内核对I2C总线的支持框架
    • 3.1 Linux内核中I2C的驱动层次逻辑
    • 3.2 Linux内核提供的I2C软件接口
  • 4、I2C外设-MPU6050
    • 4.1 MPU6050的环境
      • 4.1.1 电路原理
      • 4.1.2 I2C 引脚
      • 4.1.3 MPU6050的I2C地址
      • 4.1.4 linux设备树信息
        • 4.1.4.1 背影知识
        • 4.1.4.2 fs4412上的设备树及menuconfig操作
  • 5、操作MPU6050
    • 5.1 应用层通过标准文件访问方式操作MPU6050
      • 5.1.1 驱动 i2c-dev.c对应操作函数行为分析
      • 5.1.2 标准文件IO操作步骤
      • 5.13 例程
    • 5.2 I2C设备驱动
      • 5.2.1 linux开发手册阅读
      • 5.2.2 I2C驱动client 端相关的数据结构及函数
        • struct i2c_client
        • struct i2c_board_info
        • 宏 I2C_BOARD_INFO
        • i2c_register_board_info函数
        • struct i2c_adapter
        • i2c_get_adapter函数
        • i2c_put_adapter函数
        • i2c_add_adapter函数 与 i2c_del_adapter函数
        • i2c_adapter_id函数
        • i2c_new_probed_device函数
        • i2c_new_device函数
        • i2c_unregister_device
      • 5.2.3 driver端相关的数据结构及函数
        • struct i2c_driver
        • struct i2c_device_id
        • struct i2c_msg
        • 宏 i2c_add_driver 与 函数 i2c_register_driver
        • i2c_del_driver函数
        • i2c_transfer函数
        • i2c_master_recv函数
        • i2c_master_send
      • 5.2.4 I2C的驱动框架
        • 5.2.4.1 driver 驱动端框架
          • module_i2c_driver宏
        • 5.2.4.2 client 设备端框架
      • 5.2.5 i2c两种匹配方式的例程

1、 I2C总线机制

1.1 导入

\qquad IIC总线是Philips公司在八十年代初推出的一种串行、半双工总线主要用于近距离、低速的芯片之间的通信;IIC总线有两根双向的信号线一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步;IIC总线硬件结构简单,成本较低,因此在各个领域得到了广泛的应用
在这里插入图片描述

  • IIC总线是一种多主机总线,连接在IIC总线上的器件分为主机和从机。
  • 主机可以发起和结束一次通信,而从机只能被主机呼叫。
  • 每个器件都可以作为主机,也可以作为从机。同一时刻只能选其一。
  • 发送数据的器件叫发送器,接收数据的器件叫接收器。显然,任一设备会在发送器与接收器之间转换角色。
  • 每个器件都要有一个唯一的地址(7bit)。

在这里插入图片描述

1.2 时序

在这里插入图片描述

  • 空闲时,SCL与SDA都是高电平。起始信号与停止信号都是由主机发出的。
    在这里插入图片描述
  • 主机->从机,主机对从机发一个字节之后,主机要读取从机的响应信号(主机读SDA线)

A) 主机读SDA为高电平,说明从机无应答(意味着从机接收完毕,主机发送停止信号)
B) 主机读SDA为低电平,说明从机有应答。(可继续发送下一个字节)

  • 从机->主机, 主机读取从机一个字节之后,主机要向从机发送一个响应信号(主机写SDA线)

A) 主机写SDA为高电平,从机收到主机的无应答信号之后,从机停止传输,等待主机的停止信号。
B) 主机写SDA为低电平,从机收到主机的应答信号之后,从机继续输出下一字节

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1.3 地址格式

在这里插入图片描述

2、华清fs4412上I2C的实现

2.1 寄存器

在fs4412的数据手册中,关于I2C的寄存器主要涉及如下四个:
在这里插入图片描述
在手册中,可以相到这些寄存器对应的地址:
在这里插入图片描述

2.2 寄存器位具体含义

在这里插入图片描述

  • 第7位:决定是否允许产生应答信号,无论发送还是接收前,需置1
  • 第6位:传输时时钟线分频,一般选置1
  • 第5位:决定是否开启 发送或接收结束时发通知,无论发送还是接收前,需置1,就是发送或接收结束后要发通知,而实际这个通知就是发送就是给第4位。
  • 第4位:接收或发送是否完毕可以通过检查此位是否为1,接收或发送完毕后需置0。状态位,通过检查是否为1来判断是否已完成接怍或发送。(实际编程中,第4位置0后,就开始再次传送。)

在这里插入图片描述
I2CSTAT寄存器:状态寄存器

  • 第6、7位:每次传输前需选择传输模式
  • 第5位: 1、在写模式下,置0产生将产生终止信号,传输前置1产生起始信号。 2、在读模式下,置0表读不忙,置1表读忙
  • 第4位:使能数据输出,传输前需置1
    (编程中,第4位置1后,就开始传送,之后这位就不动了,关键靠判断I2CCON的第4位是否置0来决定是否再次传送)

在这里插入图片描述

2.3 fs4412上针对具本设备的I2C工作逻辑

在这里插入图片描述

  • 从上图可看出,I2C的收发逻辑都是通过I2CDS寄存器。

\qquad 一般I2C从机设备中,也是由一组属于从机的寄存器在操作从机设备的。这些寄存器,在从机内部也是有地址的,是通过从机内部的地址进行读写操作的。
\qquad 因此,I2C总线通讯时,传送的数据,是一种广义的说法,这些数据可能是从机的地址,从机中的寄存器地址,从机的数据等。这就使得在具体从机设备的通讯时,总线的传送时序存在着人为的定义逻辑,不同从机设备可能不同,具体在传送完成从机的设备地址后,会紧跟着一个要读写的从机的内部工作寄存器地址。如下:

2.3.1 主机读写工作流程**

2.3.1.1 主机发送时序及操作流程

由于从机本身都自带寄存器,与从机设备通信的目的就是读写这些寄存器,而这些寄存器自身也是有地址编码的。所以在具体从机设备的IIC操作过程中,还需要把寄存器地址编码(RA)当成数据来传送,但逻辑上,需要很清楚这是代表了从机寄存器地址编码。因此针对具体从机设备的IIC读、写顺序如下:
在这里插入图片描述
在这里插入图片描述

主机发送一次数据的伪代码实现:
在这里插入图片描述

2.3.1.2 主机接收的时序及流程

由于从机本身都自带寄存器,与从机设备通信的目的就是读写这些寄存器,而这些寄存器自身也是有地址编码的。所以在具体从机设备的IIC操作过程中,还需要把寄存器地址编码(RA)当成数据来传送,但逻辑上,需要很清楚这是代表了从机寄存器地址编码。因此针对具体从机设备的IIC读、写顺序如下:
在这里插入图片描述
在这里插入图片描述
以上注意,NACK表示不给应答信号了,然后直接发一个P(停止位)。
在这里插入图片描述

主机接收一次数据的伪代码实现:

在这里插入图片描述

2.3.2 从机读写工作流程

在这里插入图片描述

3、LINUX内核对I2C总线的支持框架

\qquad 由于I2C的适配器对SOC来说,是一个二级外设控制器。每个I2C二级外设适配器adapter会提供两路的I2C总线(SDA、SCL)。这两路I2C总线可以外挂多个(二级)外设。这是I2C在硬件层的结构,如下图:
在这里插入图片描述

3.1 Linux内核中I2C的驱动层次逻辑

\qquad 根据I2C的硬件逻辑,LINUX在底层的驱动架构上充分应用了软件的分层思想。首先,Linux在内核中对应硬件提供了I2C总线(适配器)驱动层。用于直接驱动I2C Adaptor,这部分已由厂家完成,我们只需要使用即可。其次,I2C core核心层为驱动与设备等提供了对应的接口。最后,则是最上层的I2C从设备驱动层,这层过错成I2C上外接设备的驱动,需要由开发者来实现。以上三层的作用和对应关系详见下面这张图:

在这里插入图片描述
\qquad 对于开发者而言,最关心的是I2C从设备驱动层的编写方法。该层的驱动架构与上一章的platform虚拟总线驱动类似。

  • client相当于platform总线平台中的device,叫法不一样而已。
  • driver和platform总线平台中的driver对应(可以1个driver对应n个client)。
  • 同时每个client 又需要与I2C适配器adapter进行逻辑对应(1对1),这样driver才能通过client而取得适配器adapter的具体操作方法。
  • 核心层core则负责提供接口,完成client与driver的配对关联。

在这里插入图片描述
我们的驱动编写主要关注的是这张图里的i2c_client和i2c_driver这两个对象。

3.2 Linux内核提供的I2C软件接口

4、I2C外设-MPU6050

在fs4412板上,有一个MPU6050陀螺仪。这个陀螺仪的驱动接口是I2C。可以拿来做I2C总线驱动的试验。

4.1 MPU6050的环境

4.1.1 电路原理

在这里插入图片描述

4.1.2 I2C 引脚

MPU6050芯片的SCL 引脚接MCU的-> I2C_SCL5 , SDA 引脚接MCU的-> I2C_SDA5。 中断INT引脚接MCU的-> GYRO_INT。查询芯片的数据手册,如下:
在这里插入图片描述
mpu6050的中断引脚如图,连接的是MCU的gpx3-3引脚。
在这里插入图片描述
SDA、SCL引脚复用了GPB_2 与 GPB_3脚。查询MCU的数据手册,可以知道对应的寄存器数据定义如下。
在这里插入图片描述

4.1.3 MPU6050的I2C地址

另外,ADO这个地址控制引脚接地,意味着AD0 = 0 ,查MPU6050的数据手册,如下:
在这里插入图片描述
意味着,MPU6050的I2C地址为1101 000(左移一位转成8位为0xD0)。

MPU6050的常用寄存器地址编号
MPU6050的工作需要其内的寄存器充分配合。在MPU6050中每个寄存器都要相应的地址编号,这些地址编号将在I2C的通信中使用。因此,程序中预先把这些地址编号处理成宏,如下(具体含义要查询MPU6050的数据手册):

#define SMPLRT_DIV  0x19 //陀螺仪采样率,典型值:0x07(125Hz)
#define CONFIG   0x1A //低通滤波频率,典型值:0x06(5Hz)
#define GYRO_CONFIG  0x1B //陀螺仪自检及测量范围,典型值:0xF8(不自检,+/-2000deg/s)
#define ACCEL_CONFIG 0x1C //加速计自检、测量范围,典型值:0x19(不自检,+/-G)
#define ACCEL_XOUT_H 0x3B  //X方向的加速度高8位
#define ACCEL_XOUT_L 0x3C  //X方向的加速度低8位
#define ACCEL_YOUT_H 0x3D  //Y方向的加速度高8位
#define ACCEL_YOUT_L 0x3E  //Y方向的加速度低8位
#define ACCEL_ZOUT_H 0x3F  //Z方向的加速度高8位
#define ACCEL_ZOUT_L 0x40  //Z方向的加速度低8位
#define TEMP_OUT_H  0x41   //温度的高8位
#define TEMP_OUT_L  0x42   //温度的低8位
#define GYRO_XOUT_H  0x43  //x轴角速度的高8位
#define GYRO_XOUT_L  0x44  //x轴角速度的低8位
#define GYRO_YOUT_H  0x45  //Y轴角速度的高8位
#define GYRO_YOUT_L  0x46  //Y轴角速度的低8位
#define GYRO_ZOUT_H  0x47  //Z轴角速度的高8位
#define GYRO_ZOUT_L  0x48  //Z轴角速度的低8位
#define PWR_MGMT_1  0x6B //电源管理,典型值:0x00(正常启用)

4.1.4 linux设备树信息

4.1.4.1 背影知识

由于这里所用的fs4412开发板,是samsung 的exynos4系列的开发板系列。因此,对于设备树的含义,需要找linux3.14版本中的binding文档。这里在/Documentation/devicetree/bindings/i2c/i2c-s3c2410.txt文件里有说明:如下:

Samsung’s I2C controller(三星I2C控制器),三星的I2C控制器用于与I2C设备接口。
一、设备树节点所需属性:

  • compatible:value应为以下值之一。
    (a) “samsung, s3c2410-i2c” ------用于与s3c2410 i2c兼容的i2c。
    (b) “samsung, s3c2440-i2c” ------用于与s3c2440 i2c兼容的i2c。
    (c ) “samsung, s3c2440-hdmiphy-i2c” ------用于在多个三星SoC上的hdmiphy块中使用的类似于i2c的s3c2440
    (d) “samsung, exynos5440-i2c” ------用于s3c2440,如exynos5440上使用的i2c,不需要GPIO配置。
    (e) “samsung, exynos5-sata-phy-i2c”------用于类似i2c的s3c2440,用作内部总线上sata phy控制器的主机。
  • reg:控制器的物理基址和内存映射区域的长度
  • interrupts: cpu的中断号。
  • samsung,i2c-sda-delay: 应用于数据线(sda)边缘的延迟(以ns为单位)。

二、除“三星,s3c2440-hdmiphy-i2c”之外的所有情况都需要:
a、 Samsung GPIO变体(已弃用):

  • gpios:gpios的顺序应如下:<SDA,SCL>。gpio说明符取决于gpio控制器。在所有情况下都需要,但“三星,s3c2440-hdmiphy-i2c”除外,其输入/输出线路永久连接到相应的客户端。

b、Pinctrl变体(首选,如果可用):

  • pinctrl-0: 用于此控制器的引脚控制组。
  • pinctrl-names: 应该只包含一个值-“default”。

三、Optional properties(可选属性:)

  • samsung,i2c-slave-addr: 多主机环境中的从机地址。如果指定,默认值为0。
  • samsung,i2c-max-bus-freq: 总线的期望频率(Hz)。如果不指定时,默认值(Hz)为100000。

四、示例:

i2c@13870000 {compatible = "samsung,s3c2440-i2c";reg = <0x13870000 0x100>;interrupts = <345>;samsung,i2c-sda-delay = <100>;samsung,i2c-max-bus-freq = <100000>;/* Samsung GPIO variant begins here */gpios = <&gpd1 2 0 /* SDA */&gpd1 3 0 /* SCL */>;/* Samsung GPIO variant ends here *//* Pinctrl variant begins here */pinctrl-0 = <&i2c3_bus>;pinctrl-names = "default";/* Pinctrl variant ends here */#address-cells = <1>;#size-cells = <0>;wm8994@1a {compatible = "wlf,wm8994";reg = <0x1a>;};};

4.1.4.2 fs4412上的设备树及menuconfig操作

下面涉及到的设备树为i2c总线的节点,用于内核自带的驱动使用:
在这里插入图片描述

exynos4412平台每个i2c通道的信息是通过设备树提供的,因此需要首先在exynos4412-fs4412.dts中增加5通道的节点:

  1. 回内核源码顶层目录执行:make dtbs
  2. 将新生成的dtb拷贝到/tftpboot

实际编译过后,是这样子的,参考一下:
在这里插入图片描述

为了使用内核提供的i2c总线驱动代码,需要进行menuconfig操作,使设备驱动代码i2c-dev.c编译入内核:

在这里插入图片描述

5、操作MPU6050

有了以上的条件准备。接下来可以开始编程操作MPU6050了。操作方式有两种,

  • 一种为应用层直接操作MPU6050,使用内核的i2c驱动
  • 另一种为手动编写I2C设备驱动来操作MPU6050。

5.1 应用层通过标准文件访问方式操作MPU6050

5.1.1 驱动 i2c-dev.c对应操作函数行为分析

i2c-dev.c 做为官方提供的i2c设备驱动函数,其中定义了应用层文件标准函数open,read,write,ioctl所对应的操作函数i2cdev_open、i2cdev_read、i2cdev_write、i2cdev_ioctl。由于i2c的通讯过程分成不同阶段,因此操作函数如何对应不同阶段。在使用这些函数时就要明确。通过查看源码,把对应的操作对应如下:

应用层驱动对应说明
openi2cdev_open \qquad 构造了i2c驱动需要的数据结构,没有对i2c设备进行任何操作。打开该字符特殊文件的实例后,文件描述符开始时仅与i2c_adapter(和总线)关联。
ioctli2cdev_ioctl \qquad 使用I2C_RDWR ioctl(),然后可以立即向该适配器使用的总线上的任何设备发出I2C_msg流量。这是因为i2c_msg向量嵌入了它们需要的所有寻址信息,并直接提交给i2c_adapter。然而,仅SMBus适配器不支持该接口。
\qquad 要在该文件描述符上使用read()/write()系统调用,或使用SMBus接口(并仅与SMBus主机一起工作!),必须首先发出I2C_SLAVE(或I2C_SLAVE_FORCE)ioctl,这会把设备地址写入到i2c_client结构中去,供接下来的read 、write使用。
readi2cdev_read该操作完成了发送从设备地址,读标志,并从I2c总线上读取指定的字节数。也就是说read函数完成了如下的操作。在这里插入图片描述
writei2cdev_write该操作完成将数据发送到从设备的操作。即如图的操作在这里插入图片描述

ioctrl的request参数
这里要专门解释一下ioctrl函数的第二个参数request,由于在i2c_dev.h中有专门的定义,这样实际使用时,需要根据标准定义来使用:

#define I2C_RETRIES	0x0701	/* number of times a device address should	   be polled when not acknowledging */
#define I2C_TIMEOUT	0x0702	/* set timeout in units of 10 ms *//* NOTE: Slave address is 7 or 10 bits, but 10-bit addresses  are NOT supported! (due to code brokenness)*/
#define I2C_SLAVE	0x0703	        /* Use this slave address */
#define I2C_SLAVE_FORCE	0x0706	/* Use this slave address, even if it 	   is already in use by a driver! */
#define I2C_TENBIT	0x0704	        /* 0 for 7 bit addrs, != 0 for 10 bit */#define I2C_FUNCS	0x0705	/* Get the adapter functionality mask */#define I2C_RDWR	0x0707	/* Combined R/W transfer (one STOP only) */#define I2C_PEC		0x0708	/* != 0 to use PEC with SMBus */
#define I2C_SMBUS	0x0720	/* SMBus transfer */

5.1.2 标准文件IO操作步骤

所以通过标准文件I/O接口操作I2C设备。步骤如下:

  1. 找到I2C设备节点。I2C设备节点通常在/dev/i2c-X目录下,其中X是I2C总线编号。可以通过ls /dev/i2c-*命令查看。
  2. 打开I2C设备节点。使用open()系统调用打开对应I2C设备节点的文件描述符。例如打开I2C总线1上的设备:
int fd = open("/dev/i2c-1", O_RDWR); 
  1. 设置I2C器件地址。使用ioctl()系统调用,设置要访问的I2C器件地址。例如设置 address为0x50:
int addr = 0x50;
if (ioctl(fd, I2C_SLAVE, addr) < 0) {/* Error */ 
}
  1. 读写I2C数据。可以使用read()和write()系统调用,向I2C设备读写数据。例如:
char buf[2] = {0x01, 0x02};
if (write(fd, buf, 2) != 2) { /* Error */ 
}char recv[2] = {0};
if (read(fd, recv, 2) != 2) {/* Error */
}
  1. 关闭文件描述符。使用close()系统调用关闭I2C设备节点的文件描述符。
close(fd);

通过这种标准文件I/O方式,我们可以很轻松地操作Linux系统中的I2C总线和I2C器件,实现I2C的读写控制。

5.13 例程

因为篇幅的原因,把例程放到了
【嵌入式环境下linux内核及驱动学习笔记-(15-1)例程】里了。
请查看其第1节。

5.2 I2C设备驱动

5.2.1 linux开发手册阅读

以下为翻译了linux3.14源代码中的 /Documentation/i2c/instantiating-devices 文件。

如何实例化I2C设备

\qquad 与PCI或USB设备不同,I2C设备不在硬件级别枚举。相反,软件必须知道每个I2C总线段上连接了哪些设备,以及这些设备使用的地址。因此,内核代码必须显式实例化I2C设备。根据上下文和需求,有几种方法可以实现这一点。

方法1a:通过总线编号声明I2C设备

\qquad 当I2C总线是系统总线时,这种方法适用于许多嵌入式系统。在这样的系统上,每个I2C总线具有预先已知的编号。因此,可以预先声明该总线上的I2C设备。这是通过调用i2c_register_board_info()注册的结构体i2c_board_info的数组来完成的。

示例(来自omap2 h4):

static struct i2c_board_info h4_i2c_board_info[] __initdata = {{I2C_BOARD_INFO("isp1301_omap", 0x2d),.irq		= OMAP_GPIO_IRQ(125),},{	/* EEPROM on mainboard */I2C_BOARD_INFO("24c01", 0x52),.platform_data	= &m24c01,},{	/* EEPROM on cpu card */I2C_BOARD_INFO("24c01", 0x57),.platform_data	= &m24c01,},
};static void __init omap_h4_init(void)
{(...)i2c_register_board_info(1, h4_i2c_board_info , ARRAY_SIZE(h4_i2c_board_info));(...)
}

\qquad 上面的代码声明I2C总线1上的3个设备,包括它们各自的地址和它们的驱动程序所需的自定义数据。当讨论的I2C总线注册时,I2C设备将由I2C内核自动实例化。
\qquad 当设备所在的I2C总线消失时,设备将自动解除绑定并销毁(如果有)

方法1b:通过devicetree声明I2C设备

\qquad 该方法与方法1a具有相同的含义。这里通过设备树将I2C设备声明为主控制器的子节点。

i2c1: i2c@400a0000 {/* ... master properties skipped ... */clock-frequency = <100000>;flash@50 {compatible = "atmel,24c256";reg = <0x50>;};pca9532: gpio@60 {compatible = "nxp,pca9532";gpio-controller;#gpio-cells = <2>;reg = <0x60>;};};

\qquad 这里,使用100kHz的速度将两个设备连接到总线。有关设置设备可能需要的其他属性,请参阅documentation/devicetree/bindings/中的devicetree文档。

方法1c:通过ACPI声明I2C设备

\qquad ACPI还可以描述I2C设备。对此有专门的文档,目前位于documentation/acpi/enumeration.txt。

方法2:显式实例化设备

\qquad 当较大的设备使用I2C总线进行内部通信时,该方法是适当的。典型的情况是电视适配器。它们可以具有调谐器、视频解码器、音频解码器等,通常通过I2C总线连接到主芯片。您不会预先知道I2C总线的编号,因此无法使用上面描述的方法1。相反,您可以显式实例化I2C设备。这是通过填充结构i2c_board_info并调用i2c_new_device()来完成的。


static struct i2c_board_info sfe4001_hwmon_info = {I2C_BOARD_INFO("max6647", 0x4e),
};int sfe4001_init(struct efx_nic *efx)
{(...)efx->board_info.hwmon_client =i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);(...)
}

\qquad 上面的代码在I2C总线上实例化1个I2C设备,该总线位于所讨论的网络适配器上。

\qquad 这种情况的一种变体是当您不确定I2C设备是否存在时(例如,对于可选功能部件,该功能部件不存在于电路板的廉价变体上,但您无法区分它们),或者它可能在不同的电路板之间具有不同的地址(制造商在未通知的情况下更改其设计)。在这种情况下,您可以调用i2c_new_probed_device(),而不是i2c_new_device.()。

static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END };static int usb_hcd_nxp_probe(struct platform_device *pdev)
{(...)struct i2c_adapter *i2c_adap;struct i2c_board_info i2c_info;(...)i2c_adap = i2c_get_adapter(2);memset(&i2c_info, 0, sizeof(struct i2c_board_info));strlcpy(i2c_info.type, "isp1301_nxp", I2C_NAME_SIZE);isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info,normal_i2c, NULL);i2c_put_adapter(i2c_adap);(...)
}

\qquad 上述代码在OHCI适配器上的I2C总线上实例化多达1个I2C设备。它首先在地址0x2c处尝试,如果在那里找不到任何东西,则尝试地址0x2d,如果仍然找不到,则简单地放弃。

\qquad 实例化I2C设备的驱动程序负责在清理时销毁它。这是通过对先前由i2c_new_device()或i2c_new_probed_device.()返回的指针调用i2c_unregister_device。

方法3:探测某些设备的I2C总线

\qquad 有时,您没有关于I2C设备的足够信息,甚至无法调用I2C_new_probed_device()。典型的例子是PC主板上的硬件监控芯片。有几十种型号,可以存在25个不同的地址。考虑到那里有大量的主板,建立一个正在使用的硬件监控芯片的详尽列表几乎是不可能的。幸运的是,大多数芯片都有制造商和设备ID寄存器,因此可以通过探测来识别它们。

\qquad 在这种情况下,I2C设备既不显式声明也不显式实例化。相反,一旦加载了这些设备的驱动程序,i2c-core将立即探测这些设备,如果找到任何设备,i2c设备将自动实例化。为了防止该机制的任何不当行为,适用以下限制:

*I2C设备驱动程序必须实现detect()方法,该方法通过从任意寄存器读取来标识支持的设备。

*只有可能具有支持的设备并同意被探测的总线才会被探测。例如,这避免了在电视适配器上探测硬件监控芯片。

示例:
\qquad 请参阅drivers/hwmon/lm90.c中的lm90_driver和lm90_detect()
\qquad 当检测到I2C设备的驱动程序被删除时,或当底层I2C总线本身被破坏时,作为这种成功探测的结果实例化的I2C器件将自动销毁,以先发生的为准。

\qquad 熟悉2.4内核和早期2.6内核的i2c子系统的人会发现,这种方法3在本质上类似于在那里所做的工作。两个显著差异是:

*探测现在只是实例化I2C设备的一种方法,而它是当时唯一的方法。在可能的情况下,应首选方法1和2。方法3只能在没有其他方法的情况下使用,因为它可能会产生不良的副作用。

*I2C总线现在必须显式地指出哪些I2C驱动程序类可以探测它们(通过类位字段),而在当时默认情况下探测所有I2C母线。默认值是空类,这意味着不会发生探测。类位字段的目的是限制上述不良副作用。

\qquad 同样,应尽可能避免使用方法3。显式设备实例化(方法1和2)更受欢迎,因为它更安全、更快。

方法4:从用户空间实例化

\qquad 一般来说,内核应该知道连接了哪些I2C设备以及它们所在的地址。然而,在某些情况下,它不知道,因此添加了一个sysfs接口,让用户提供信息。该接口由在每个I2C总线目录中创建的2个属性文件组成:new_device和delete_device。这两个文件都是只写的,您必须将正确的参数写入它们,以便正确地实例化(分别删除)I2C设备。

\qquad 文件new_device采用2个参数:I2C设备的名称(字符串)和I2C设备的地址(数字,通常以十六进制表示,以0x开头,但也可以以十进制表示。)

\qquad 文件delete_device采用单个参数:I2C设备的地址。由于在给定的I2C段上没有两个设备可以位于相同的地址,因此该地址足以唯一地标识要删除的设备。

示例:
# echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device

\qquad 虽然此接口应仅在无法进行内核内设备声明时使用,但在各种情况下,它都是有帮助的:

  • I2C驱动程序通常检测设备(上面的方法3),但设备所在的总线段没有真正的类比特装置,因此不会触发检测。
  • I2C驱动程序通常检测设备,但您的设备位于意外的地址。
  • I2C驱动程序通常会检测设备,但不会检测到您的设备,这可能是因为检测例程太严格,或者是因为您的设备尚未得到正式支持,但您知道它是兼容的。
  • 您正在测试板上开发驱动程序,在那里您自己焊接了I2C设备。

该接口是一些I2C驱动程序实现的force_*模块参数的替代。它在i2c-core中实现,而不是单独在每个设备驱动程序中实现,效率要高得多,并且还有一个优点,即您不必重新加载驱动程序来更改设置。您还可以在加载驱动程序之前或甚至在可用之前实例化设备,并且不需要知道设备需要什么驱动程序。

5.2.2 I2C驱动client 端相关的数据结构及函数

struct i2c_client

所在的头文件 /include/linux/i2c.h

struct i2c_client {unsigned short flags;		 一些标志位,用于表示客户端的一些属性。默认是7位地址。其它值见下面的解释unsigned short addr;		 该客户端的 I2C 地址,注意 I2C 使用 7 位地址,所以 addr 的低 7 位存储实际地址char name[I2C_NAME_SIZE];    该客户端的名称,最大长度为 I2C_NAME_SIZEstruct i2c_adapter *adapter;	该客户端所连接的 I2C 控制器(adapter)struct device dev;		       该客户端的设备结构(用于内核的设备模型)int irq;			          该客户端发出的中断号struct list_head detected;    用于 I2C 检测使用的链表
};
#define to_i2c_client(d) container_of(d, struct i2c_client, dev);,用于从device结构体获取i2c_client结构体

这个结构体表示Linux内核中一个I2C客户端设备。分析如下:

  • flags: 用于存放一些标志位,类型的取值如下:

这个结构体表示一个I2C从设备,它包含了该设备必要的信息,并且作为设备模型的一部分,可以被统一检测和管理。
当一个I2C设备被检测到时,会分配一个i2c_client结构体来表示它,填充必要信息如地址、适配器、名称等。然后将其添加到适配器上的从设备列表中,以便内核进行管理。
驱动在probe回调中会接收此结构体,并根据信息进一步检测和初始化设备。之后它被作为驱动与设备的连接点,驱动通过它来控制和访问设备。
所以,i2c_client结构体的主要作用是:

  1. 作为设备模型的一部分,代表一个I2C从设备
  2. 包含这个从设备的必要信息,如地址、名称、所在总线等
  3. 作为驱动与从设备的连接点,由驱动接收和持有
  4. 能够被统一添加到I2C总线适配器的从设备列表进行管理
    它抽象表示了一个I2C从设备,并具有作为设备模型设备的特性,是I2C驱动适配I2C从设备的关键。

struct i2c_board_info

所在的头文件 /include/linux/i2c.h

struct i2c_board_info {char		type[I2C_NAME_SIZE];unsigned short	flags;unsigned short	addr;void		*platform_data;struct dev_archdata	*archdata;struct device_node *of_node;struct acpi_dev_node acpi_node;int		irq;
};

\qquad 在I2C子系统不直接创建i2c_client结构,只是提供struct i2c_board_info结构体息,让子系统动态创建,并注册。因此i2c_board_info是用来协助内核创建i2c_client对象的,所以两个数据结构的成员有一一对应的关系,如下:

  • type : 用来初始化i2c_client结构中的name成员。
  • flags:用来初始化i2c_client结构中的flags成员。
  • addr:用来初始化i2c_client结构中的addr成员。
  • platform_data:用业初始化i2c_client结构中的.dev.platform_data成员。
  • archdata:用来初始化i2c_client结构中的.dev.archdata成员。
  • irq:用来初台化i2c_client结构中的irq成员。
  • of_node: 如果这个 I2C 设备是由设备树(Open Firmware)描述的,这个成员保存其设备树节点(struct device_node *)。设备树是一种描述硬件的树形数据结构,用于替代 ACPI 表。
  • acpi_node: 如果这个 I2C 设备是由 ACPI(高级配置与电源接口)描述的,这个成员保存其 ACPI 设备节点(struct acpi_dev_node)。ACPI 用于描述系统硬件配置和电源管理功能。
    注意:
    \qquad of_node与acpi_node这两个成员是互斥的,一个 I2C 设备要么由设备树描述,要么由 ACPI 描述,不会两者都有。所以这两个成员是为了支持依赖设备树或 ACPI 来描述 I2C 设备的系统准备的。

这个结构体包含了一个 I2C 设备的所有基本信息,如其类型、地址、中断号、以及一些设备专有的数据等。
内核使用 i2c_board_info 结构体的数组来表示主板上所有的 I2C 设备信息。这样,通过检测 I2C 总线,内核 I2C 核心可以将检测到的设备与这些信息相匹配,选择正确的驱动来管理这些设备。

struct i2c_board_info 和 struct i2c_client 之间的关联:

  • i2c_board_info 结构体通常在系统启动阶段由 bootloader 或固件填充,它们描述了主板上的 I2C 设备信息,以供内核初始化使用。
  • 内核 I2C 核心通过检测 I2C 总线来发现设备,当发现一个设备时会自动去匹配 i2c_board_info 表中的信息。
  • 如果找到一个匹配,就会为这个新发现的设备创建一个 i2c_client 结构体,并填充相应信息(设置与 i2c_board_info 中一致的 addr, name 等)。
  • 然后内核就可以使用这个 i2c_client 结构体来进一步管理和访问这个 I2C 客户端设备了。
  • 所以 i2c_board_info 提供设备信息让内核初始化使用,并最终转化为代表一个实际 I2C 客户端的 i2c_client 结构体。i2c_client 包含运行时关于该设备的更丰富的信息。
    可以说,i2c_board_info 是编译时的静态设备信息,而 i2c_client 则是运行时动态创建和维护的设备信息。i2c_board_info 的信息在创建 i2c_client 时作为基础被使用,但 i2c_client 包含的信息会更丰富一些。

宏 I2C_BOARD_INFO

#include <linux/i2c.h>
#define I2C_BOARD_INFO(dev_type, dev_addr) \.type = dev_type, .addr = (dev_addr)

这个宏用于静态定义一个i2c_board_info结构体,提供I2C设备的硬件信息。

它有两个参数:

  • dev_type: 设备类型名称,将赋值给i2c_board_info.type
  • dev_addr: 设备的7bit I2C地址,将赋值给i2c_board_info.addr
    这个宏的展开实际上是:
{             .type = dev_type, .addr = (dev_addr)
}

所以它被用来方便地定义一个i2c_board_info结构体,只需要提供设备类型名称和地址两个信息,其他成员使用默认值。
例如:

static struct i2c_board_info mx25xx_info = {I2C_BOARD_INFO("mx25xx", 0x35)
};

这会定义一个mx25xx_info,它的值是:

{.type = "mx25xx", .addr = 0x35 

i2c_register_board_info函数

#include <linux/i2c.h>
int
i2c_register_board_info(int busnum, struct i2c_board_info const *info,	unsigned n);

这个函数用于向内核静态注册I2C设备信息。它接收三个参数:

  • busnum: I2C总线编号,设备所在的总线
  • info: 待注册的I2C设备信息,由i2c_board_info结构体定义
  • n: info数组的大小,要注册的设备信息数

该函数会将info中提供的n个I2C设备的硬件信息注册到内核中,供内核在系统启动阶段来检测和创建设备。

主要实现步骤为:

  1. 检查busnum总线是否存在,如果不存在则返回错误
  2. 为这n个设备分配i2c_board_info结构体
  3. 将结构体添加到内核维护的board_info列表
  4. 返回0表示成功
    在系统启动早期,板级初始化函数会调用该函数来注册设备信息,内核会据此在所有注册的I2C总线上检测设备,并为检测到的设备动态创建i2c_client来完成设备初始化。

例如:

static struct i2c_board_info info[] = {{ I2C_BOARD_INFO("dev1", 0x10) },{ I2C_BOARD_INFO("dev2", 0x20) }
};i2c_register_board_info(2, info, ARRAY_SIZE(info));

这会在I2C总线2上注册两个设备的信息,内核随后会在0x10和0x20地址检测"dev1"和"dev2"两个设备,并创建对应的i2c_client来管理。
这种静态注册信息的方式是Linux内核管理I2C从设备的两种主要方式之一,另一种是 dynamicaly创建设备。两个方式的关键步骤分别由i2c_register_board_info和i2c_new_device两个函数来实现。
理解这个函数,有助于加深对I2C设备管理机制的理解,特别是静态方式下,内核如何根据注册的硬件信息来自动检测和创建设备。这也是Linux内核自动枚举硬件设备的一个途径。
作为板级初始化代码来说,调用这个函数注册I2C设备信息是非常必要的一步,否则内核无法获得设备的存在信息,自然也无法完成自动检测和初始化


struct i2c_adapter

所在的头文件 /include/linux/i2c.h

struct i2c_adapter {struct module *owner;unsigned int class;		  /* classes to allow probing for */const struct i2c_algorithm *algo; /* the algorithm to access the bus */void *algo_data;/* data fields that are valid for all devices	*/struct rt_mutex bus_lock;int timeout;			/* in jiffies */int retries;struct device dev;		/* the adapter device */int nr;char name[48];struct completion dev_released;struct mutex userspace_clients_lock;struct list_head userspace_clients;struct i2c_bus_recovery_info *bus_recovery_info;
};
#define to_i2c_adapter(d) container_of(d, struct i2c_adapter, dev);

struct i2c_adapter 表示一个 I2C 适配器(主控制器)。它包含以下主要字段:

  • owner: 该适配器的模块 owner
  • class: 该适配器所支持的设备类,用于确定可以在其上检测什么类型的 I2C 设备
  • algo: 该适配器所使用的 I2C 传输算法(例如 bit-banging 或者 bus 特定的算法)
  • algo_data: 供该算法使用的私有数据
  • bus_lock: 用于同步对 I2C 总线的访问
  • timeout: I2C 传输的超时值(jiffies)
  • retries: I2C 传输的重试次数
  • dev: 该适配器的设备结构
  • nr: 该适配器的号码(由 I2C 核心分配)
  • name: 该适配器的名称
  • userspace_clients: 用于 userspace 客户端设备的链表
  • bus_recovery_info: 总线恢复信息,如果支持的话
    \qquad 这个结构体包含了描述一个 I2C 主控制器(adapter)所需的所有信息,如其支撑的设备类型、所使用的算法、超时和重试参数、用户空间客户端列表等。
    \qquad 内核使用 i2c_adapter 结构体来表示系统中所有的 I2C 控制器,并负责在这些控制器上检测设备和处理传输。
    所以,与 i2c_client 结构体表示 I2C 总线上的一个客户端设备相对应,i2c_adapter 结构体表示一个控制整个 I2C 总线的适配器(主控制器)。

对于 i2c_adapter.class成员的取值定义:

#define I2C_CLASS_HWMON (1<<0) 用于硬件监控设备(如 lm_sensors)
#define I2C_CLASS_DDC (1<<3) 用于显示器的数据通道(Display Data Channel)
#define I2C_CLASS_SPD (1<<7) 用于存储模块(如内存模块)

i2c_get_adapter函数

该函数是根据传入的适配器编号 nr 获取对应的 i2c_adapter 结构体。
原型:

#include <linux/i2c.h>
struct i2c_adapter *i2c_get_adapter(int nr);   

参数:

  • nr 参数指定要获取的 i2c_adapter 的编号。这个编号由 I2C 核心在注册 i2c_adapter 时分配。

返回值:
NULL:没有找到指定总线编号适配器结构
非NULL:指定nr的适配器结构内存地址*/

该函数会遍历内核的 i2c_adapter 链表,找出编号匹配 nr 的 i2c_adapter,并返回一个指向它的指针。然后用户可以使用这个结构地址就可以给i2c_client结构使用,从而实现i2c_client进行总线绑定,从而增加适配器引用计数。

这个函数在以下情况下典型被调用:

  1. 一个 I2C 设备驱动在启动时会调用该函数根据设备信息(如 i2c_board_info)中指定的 i2c_adapter 来获取对应的适配器,以便将自己的 i2c_client 注册到该适配器上。
  2. 用户空间应用通过 sysfs 迭代 i2c_adapter 后,拿到一个 i2c_adapter 的 nr,然后调用该函数来获取对应的 i2c_adapter 结构,以进行进一步访问。
  3. 内核其他子系统获取 i2c_adapter 以执行针对该 I2C 总线的操作,例如设备驱动model 将自己的设备与一个 I2C 总线相关联时。
    所以,该函数为内核各部分以及用户空间提供了根据 i2c_adapter 编号获取其结构体的能力,是访问和使用 i2c_adapter 的基础。

注意:
当使用·i2c_get_adapter·后,需要使用 void i2c_put_adapter(struct i2c_adapter *adap);函数来减少引用计数。当然如果你的适配器驱动不需要卸载,可以不使用)

i2c_put_adapter函数

i2c_put_adapter() 函数的作用是释放之前通过 i2c_get_adapter() 获取的 i2c_adapter 结构体。
它的函数原型如下:

#include <linux/i2c.h>
void i2c_put_adapter(struct i2c_adapter *adap);

参数:

  • adap 参数指定要释放的 i2c_adapter 结构体。

当外部通过 i2c_get_adapter() 获取了一个 i2c_adapter 结构体之后,使用完毕需要调用 i2c_put_adapter() 来释放它。

释放一个 i2c_adapter 主要完成以下工作:

  1. 如果这个 i2c_adapter 正在被卸载(标志 I2C_ADAP_UNREGISTERING 已设置),则递减其引用计数。一旦引用计数降为 0,就可以完全释放它。
  2. 如果设置了 I2C_ADAP_DYING 标志,则唤醒等待这个 i2c_adapter 释放的进程。
  3. 如果该 i2c_adapter 支持 rtnl_link_ops,则调用其 unlink() 操作以断开设备与 I2C 总线的关联。
  4. 如果设备释放回调(release)已设置,则调用它。
    所以简单来说,这个函数的作用是递减对一个 i2c_adapter 的引用,并在引用计数降为 0 时执行必要的资源清理工作,完全释放这个 i2c_adapter。
    这有助于内核跟踪 i2c_adapter 的使用,防止资源泄漏。当外部不再需要访问一个 i2c_adapter 时调用这个函数释放对它的引用是很重要的。

另外,关于i2c_adapter->flags成员的解释:
I2C_ADAP_UNREGISTERING 和 I2C_ADAP_DYING 都是 i2c_adapter->flags 中的标志位。它们的意思如下:

  • I2C_ADAP_UNREGISTERING: 表示这个 i2c_adapter 正在被注销(通过 i2c_del_adapter())。当这个标志被设置时,i2c_put_adapter() 会递减 i2c_adapter 的引用计数,并在它降为 0 时完成注销操作。
  • I2C_ADAP_DYING: 表示这个 i2c_adapter 正在退出,有其他进程正在等待它被完全释放。当 i2c_put_adapter() 看到这个标志时,会唤醒等待这个 i2c_adapter 退出的进程。
    这两个标志都是由 i2c 核心内部在处理 i2c_adapter 注销或退出时设置的。外部不需要也不应该手动设置或修改这两个标志。

它们存在的目的在于:

  • I2C_ADAP_UNREGISTERING: 告知 i2c_put_adapter() 这个 i2c_adapter 正在被注销,以便它可以帮助完成注销操作。这是一个信号,用于同步 i2c_del_adapter() 和 i2c_put_adapter() 之间的注销流程。
  • I2C_ADAP_DYING: 告知 i2c_put_adapter() 有进程正在等待这个 i2c_adapter 的完全释放,这样它可以在合适时机唤醒等待进程。这是一种简单的同步机制,用于处理 i2c_adapter 退出时的进程间同步。

i2c_add_adapter函数 与 i2c_del_adapter函数

#include <linux/i2c.h>
int i2c_add_adapter(struct i2c_adapter *);
void i2c_del_adapter(struct i2c_adapter *);
int i2c_add_numbered_adapter(struct i2c_adapter *);

这三个函数分别用于向内核注册一个新的I2C总线适配器、注销一个已经注册的总线适配器以及注册一个编号指定的总线适配器。
首先是i2c_add_adapter(),它接收一个I2C总线适配器结构体作为参数,用于向内核注册一个新的I2C总线适配器。

主要实现步骤如下:

  1. 检查adap是否为空
  2. adap增加其用户计数adap->users
  3. 将adap添加到内核维护的adapter_list适配器列表
  4. 如果定义了adap的算法和主控程序方法,调用它们完成必要的初始化工作
  5. 返回adap->nr,即该新适配器的编号

其次是i2c_del_adapter(),它也接收一个I2C总线适配器作为参数,用于注销一个已经注册的总线适配器。

主要实现步骤如下:

  1. 检查adap是否为空
  2. adap减少其用户计数adap->users
  3. 如果adap->users减到0,从adapter_list中将其移除
  4. 如果定义了adap的算法和主控程序方法,调用它们完成清理工作
  5. 释放与adap相关的资源

最后是i2c_add_numbered_adapter(),它也接收一个I2C总线适配器作为参数,但是还会接收一个参数nr来指定要注册的适配器编号。除了改变注册后返回的adap->nr编号外,其它实现步骤与i2c_add_adapter()相同。

这三个函数实现了I2C总线适配器的注册、注销和编号指定注册。驱动程序通过调用i2c_add_adapter()或i2c_add_numbered_adapter()来向内核注册一个新的I2C总线,并在不再使用时通过i2c_del_adapter()将其注销。
理解这三个接口有助于理解内核是如何管理I2C总线资源的,特别是总线适配器的注册及注销流程。这也是驱动开发中必不可少的一部分,熟练使用这些函数可以更好地理解I2C子系统中的总线管理机制。
I2C总线驱动的开发工作首先需要向内核注册一个对应的总线适配器,才可以支持该总线上的I2C从设备管理与访问。所以这三个函数的使用也是I2C总线驱动开发中最为基础和必要的部分。


i2c_adapter_id函数

i2c_adapter_id()函数用于根据I2C总线适配器指针获取其ID号。

#include <linux/i2c.h>
static inline int i2c_adapter_id(struct i2c_adapter *adap)
{return adap->nr;
}

i2c_new_probed_device函数

i2c_new_probed_device() 函数的作用是根据传入的 i2c_adapter 和 i2c_board_info 在地址列表 addr_list 中检测设备,并为找到的第一个设备分配一个 i2c_client 结构体。并进行注册。

#include <linux/i2c.h>
struct i2c_client *i2c_new_probed_device(struct i2c_adapter *adap,struct i2c_board_info *info,unsigned short const *addr_list,int (*probe)(struct i2c_adapter *  ,   unsigned short addr));

参数:**

  • adap: I2C 适配器,需要开发者根据实际外设的挂接在哪个adapter进行设置。
  • info:挂接的设备的基本信息,需要开发者自行写入 i2c_board_info 结构体
  • addr_list: 挂接到adapter适配器上的外设的可能的地址列表,(地址定义形式是固定的,一般是定义一个数组,数组必须以I2C_CLIENT_END结束,示例:unsigned short ft5x0x_i2c[]={0x38,I2C_CLIENT_END};
  • probe: 可选的回调函数,回调函数指针,当创建好i2c_client后,会调用该函数,一般没有什么特殊需求传递NULL,使用系统默认的回调函数。

返回值:

  • 非NULL:创建成功,返回创建好的i2c_client结构地址
  • NULL:创建失败

该函数的工作过程如下:

  1. 遍历 addr_list 中的地址,在 adap 上检测每个地址是否存在设备。
  2. 如果 probe 回调函数提供,则对每个地址调用 probe 函数进行更加具体的检测,直到 probe 返回正值,表示找到了设备。
  3. 如果找到了设备,则为它创建一个 i2c_client 结构体,填充 info 中提供的信息,并将 i2c_client 注册到 adap 上。
  4. 返回新创建的 i2c_client 结构体。如果没有找到设备,则返回 NULL。
  5. 如果未提供 probe 回调,则简单的在 addr_list 中的第一个设备地址上创建 i2c_client。
    所以这个函数的主要作用是:在一个 I2C 适配器 adap 上,根据 addr_list 中提供的地址列表和 info 中的设备信息,检测是否存在这个设备,如果找到则为它创建一个 i2c_client 并注册到 adap 上,并返回这个新创建的 i2c_client。

实例
在这里插入图片描述
另一个例子:

static const unsigned short addr_list[] = { 0x10, 0x15, 0x20 };
static int probe_func(struct i2c_adapter *adap, unsigned short addr)
{// 实现对addr地址的检测操作...return 1;   // 检测成功
}struct i2c_client *client;
client = i2c_new_probed_device(adapter, info, addr_list, probe_func);

这会在0x10、0x15和0x20三个地址上调用probe_func进行检测,如果在0x15成功检测到设备,则会利用info信息动态创建一个i2c_client,并将其添加到adapter上,client指向该新设备。
该函数用于真实I2C总线上动态检测和创建设备,它结合了静态的设备信息和运行时的地址扫描及检测机制。这是Linux内核管理I2C从设备的重要方式之一。
理解这个函数,可以加深对内核如何动态管理I2C从设备的理解。它利用静态信息和运行时检测相结合的方式来实现设备的动态创建

i2c_new_device函数

i2c_new_device() 函数的作用是根据传入的 i2c_adapter 和 i2c_board_info 在 info 指定的设备地址上创建一个新的 i2c_client,并注册到 adap 上。这个函数用于动态创建一个I2C从设备。

#include <linux/i2c.h>
struct i2c_client *i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info);

参数:

  • adap: I2C 适配器对象
  • info: 要在 adap 上创建的 i2c_client 对应的 i2c_board_info 结构体

该函数的主要工作是:

  1. 在 info->addr 指定的 I2C 地址上,为 info 描述的 I2C 设备分配一个新的 i2c_client 结构体。
  2. 初始化 i2c_client,填充 info 中提供的信息,如设备名称、地址、配备的硬件等数据。
  3. 将新的 i2c_client 注册到 adap 上,使其与 info 指定的 I2C 设备关联。
  4. 如果注册成功,返回新分配的 i2c_client。如果地址已被占用或其他错误,返回 NULL。
    所以该函数的作用就是根据 i2c_board_info 的描述在一个 I2C 总线上为设备创建一个 i2c_client,并使其与这个设备关联,这允许在驱动中通过 i2c_client 来访问这个设备。

例如:

struct i2c_board_info info = {I2C_BOARD_INFO("abc", 0x50),
};
struct i2c_client *client;client = i2c_new_device(adapter, &info);

这会为0x50地址上的"abc"类型设备,动态创建一个i2c_client,并将其添加到adapter总线上。
该函数是动态创建I2C从设备的关键接口,内核利用它根据info中的硬件信息来动态分配和初始化一个i2c_client结构体,代表一个I2C从设备,并将其加入内核设备管理体系。
理解这个函数,可以加深对i2c_board_info和i2c_client结构体作用的理解,以及它们如何协同工作来动态创建I2C从设备的认识。
动态创建设备和静态定义信息后由内核自动检测创建设备,是Linux内核管理I2C从设备的两个主要方式。这个函数则实现了前者的关键步骤。

i2c_unregister_device

i2c_unregister_device() 函数的作用是注销一个已注册的 i2c_client 设备。
它的函数原型如下:

#include <linux/i2c.h>
void i2c_unregister_device(struct i2c_client *client); 

其中 client 指定要注销的 i2c_client 结构体。

该函数会完成以下工作:

  1. 从 i2c_client 所属的 i2c_adapter 上注销这个 i2c_client,取消它与 adapter 和目标 I2C 设备的关联。
  2. 如果 i2c_client 还挂接在 i2c_driver 上(意味着仍有设备驱动绑定它),则先调用 i2c_driver->remove() 解绑定驱动。
  3. 释放与 i2c_client 相关的所有内存资源,包括其私有数据等。
  4. 将 i2c_client 的引用计数减一,如果减到 0 则完全释放它。
    所以这个函数的主要作用是:注销一个已注册的 i2c_client,取消它与 I2C 适配器和设备的关联,释放所有相关资源,并在引用计数降为 0 时完全释放它。

5.2.3 driver端相关的数据结构及函数

struct i2c_driver

#include <linux/i2c.h>
struct i2c_driver {unsigned int class;int (*attach_adapter)(struct i2c_adapter *) __deprecated;/* Standard driver model interfaces */int (*probe)(struct i2c_client *, const struct i2c_device_id *);int (*remove)(struct i2c_client *);/* driver model interfaces that don't relate to enumeration  */void (*shutdown)(struct i2c_client *);int (*suspend)(struct i2c_client *, pm_message_t mesg);int (*resume)(struct i2c_client *);void (*alert)(struct i2c_client *, unsigned int data);int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);struct device_driver driver;const struct i2c_device_id *id_table;/* Device detection callback for automatic device creation */int (*detect)(struct i2c_client *, struct i2c_board_info *);const unsigned short *address_list;struct list_head clients;
};
#define to_i2c_driver(d) container_of(d, struct i2c_driver, driver)

i2c_driver 结构体代表一个 I2C 设备驱动。它包含了驱动的所有信息,以及不同阶段需要实现的回调函数。
i2c_driver 的主要成员如下:

  • class:驱动类型,一般设置为 I2C_CLASS_HWMON | I2C_CLASS_SPD 等,表示 i2c_driver 所支持的设备类型,用于匹配
  • attach_adapter:过时的回调,接下来的版本将删除,不应使用
  • probe: 当一个匹配的 i2c_client 被发现时,在i2c_client与i2c_driver匹配后,i2c 核心会调用该函数,供驱动进行初始化。
  • remove: 当 i2c_client 被注销时调用,供驱动进行清理
  • shutdown: 系统关闭回调,系统关闭时调用,供驱动执行关闭操作
  • suspend/resume: 系统睡眠/唤醒回调,系统睡眠/唤醒时调用,用于执行低功耗操作
  • alert: SMBus 警报回调,如果设备支持 SMBus 警报,则在警报事件时调用
  • command: 设备专有的 ioctl 样命令回调,如果设备支持专有 ioctl 命令,则由此函数负责处理
  • driver: 包含驱动模型需要的所有字段,包含一个 Linux 设备驱动模型下的 device_driver,以被 Linux 驱动模型管理
  • id_table: 支持的设备 ID 列表,用于与新发现的 i2c_client 匹配
  • detect: 可选的设备检测回调,如果提供则由 i2c 核心调用来检测新的 i2c_client
  • address_list: 可选的要扫描的设备地址列表
  • clients: 由该驱动绑定的所有 i2c_client 链表,记录由本 i2c_driver 所管理的所有 i2c_client

所以,i2c_driver 封装了一个 I2C 设备驱动模型需要的所有信息 - 支持的设备类型、回调函数、设备列表等,它代表一个完整的 I2C 设备驱动。
I2C 核心根据 i2c_driver 中提供的信息来匹配新发现的 I2C 设备,调用相关回调以初始化设备,并管理整个设备生命周期。

关于i2c_client 与 i2c_driver的匹配:

  • id_table:用来实现i2c_client与i2c_driver匹配绑定。
  • 当i2c_client中的name成员和i2c_driver中id_table中name成员相同的时候,就匹配上了。
  • i2c_client指定的信息在物理上真实存放对应的硬件,并且工作是正常的才会绑定上,并执行其中的probe接口函数。这点要求和platform模型匹配有区别,platform模型不要求设备层指定信息在物理上真实存在就能匹配。

struct i2c_device_id

该数据结构在include/linux/mod_devicetable.h头文件里定义

struct i2c_device_id {char name[I2C_NAME_SIZE];kernel_ulong_t driver_data;	/* Data private to the driver */
};
  1. name:I2C 设备的名字,最大长度是 I2C_NAME_SIZE,定义为 20 个字符。这个名字通常与设备的类型对应,比如 “mpu6050” 代表 MPU6050 运动传感器。
  2. driver_data:这是一个无类型的长整型,由 I2C 驱动程序使用,通常包含一些该 I2C 设备私有的数据。

当 I2C 主设备检测到一块名为 “mpu6050” 的从设备时,会查找已经注册的哪个 I2C 驱动程序也支持名为 “mpu6050” 的设备,并调用该驱动程序来访问这个从设备。
driver_data 字段允许驱动程序将一些私有数据与每个设备实例相关联。当驱动程序被调用来处理一个设备时,这个字段的数据可被提取,以帮助驱动程序区分不同的设备对象并调用正确的常规方法。

struct i2c_msg

#include <linux/i2c.h>
struct i2c_msg {__u16 addr;	/* slave address			*/__u16 flags;#define I2C_M_TEN		0x0010	/* this is a ten bit chip address */#define I2C_M_RD		0x0001	/* read data, from slave to master */#define I2C_M_STOP		0x8000	/* if I2C_FUNC_PROTOCOL_MANGLING */#define I2C_M_NOSTART		0x4000	/* if I2C_FUNC_NOSTART */#define I2C_M_REV_DIR_ADDR	0x2000	/* if I2C_FUNC_PROTOCOL_MANGLING */#define I2C_M_IGNORE_NAK	0x1000	/* if I2C_FUNC_PROTOCOL_MANGLING */#define I2C_M_NO_RD_ACK		0x0800	/* if I2C_FUNC_PROTOCOL_MANGLING */#define I2C_M_RECV_LEN		0x0400	/* length will be first received byte */__u16 len;		/* msg length				*/__u8 *buf;		/* pointer to msg data			*/
};

i2c_msg 结构体代表一个 I2C 消息,它包含执行一个 I2C 操作所需要的所有信息。
i2c_msg 的主要字段如下:

  • addr: 从设备地址,7 位或 10 位
  • flags: 消息标志,指定消息方向、10 位地址等
  • len: 消息长度,即 buf 的长度
  • buf: 消息数据缓存区

flags 可以包含以下值:

  • I2C_M_TEN: 10 位从设备地址
  • I2C_M_RD: 读消息,从从设备读取数据
  • I2C_M_STOP: 停止信号,只在 I2C_FUNC_PROTOCOL_MANGLING 情况下需要
  • I2C_M_NOSTART: 无启动信号,只在 I2C_FUNC_NOSTART 情况下需要
  • I2C_M_REV_DIR_ADDR: 只在 I2C_FUNC_PROTOCOL_MANGLING 情况下需要
  • I2C_M_IGNORE_NAK: 忽略 NAK,只在 I2C_FUNC_PROTOCOL_MANGLING 情况下需要
  • I2C_M_NO_RD_ACK: 读操作不需要 ACK,只在 I2C_FUNC_PROTOCOL_MANGLING 情况下需要
  • I2C_M_RECV_LEN: 第一个字节是消息长度,只在 I2C_FUNC_PROTOCOL_MANGLING 情况下需要
    所以 i2c_msg 包含了构造一个 I2C 操作所需的所有信息: 消息方向(读/写)、 从设备 7/10 位地址、 消息长度和数据、 以及一些特殊的功能标志(这些标志都是高级特性,一般不需要使用)

I2C 核心提供的 API 中会使用 i2c_msg 来指定一个 I2C 操作,比如:

  • i2c_transfer(): 执行一组 i2c_msg,即一组 I2C 操作
  • i2c_smbus_xfer(): 通过 i2c_msg 实现 SMBus 协议的数据操作
  • etc.
    所以理解了 i2c_msg 结构体,就理解了 I2C 核心 API 如何通过这一结构体来指定一个 I2C 操作,实现读写数据。
    这是使用 I2C 核心 API 的基础,通过构造好的 i2c_msg,可以执行相应的 I2C 读写操作,实现与设备之间的数据交互。

宏 i2c_add_driver 与 函数 i2c_register_driver

i2c_register_driver() 函数的作用是注册一个 i2c_driver,将其加入 I2C 核心的管理。
它的函数原型如下:

#include <linux/i2c.h>
int i2c_register_driver(struct module *, struct i2c_driver *);#define i2c_add_driver(driver)   	i2c_register_driver(THIS_MODULE, driver)

参数:

  • owner: 驱动的模块对象,一般用 THIS_MODULE
  • driver: 要注册的 i2c_driver 结构体

该函数会完成以下工作:

  1. 检查 i2c_driver 的正确性,比如 probe/remove 回调等是否存在。如果检查失败则返回错误。
  2. 如果 i2c_driver 中包含地址列表 address_list,则在每一个地址上都尝试创建一个 i2c_client,并调用 probe 回调进行初始化。
  3. 如果 i2c_driver 中包含设备 ID 列表 id_table,则遍历之前已创建的 i2c_client,找到所有与其匹配的 i2c_client 并调用 probe 回调进行初始化。
  4. 将 i2c_driver 添加到 I2C 核心管理的 i2c_driver 链表中。
  5. 返回成功注册的 i2c_driver 数量。

当一个 I2C 设备驱动在初始化时调用该函数,将它的 i2c_driver 注册到 I2C 核心,就完成了驱动与设备的匹配和初始化,驱动就可以开始正常工作了。所以这个函数是实现一个 I2C 设备驱动的关键 - 通过它来注册 i2c_driver,实现与设备的匹配和初始化,使驱动可以开始运行。

对应的,有 i2c_del_driver() 用来注销一个 i2c_driver。
所以这两个函数与 i2c_driver 的生命周期直接相关:

  • 初始化阶段调用 i2c_register_driver() 注册 i2c_driver 以开始工作
  • 退出或卸载阶段调用 i2c_del_driver() 注销 i2c_driver

而 i2c_add_driver(driver)是一个宏,可以简化对i2c_register_driver函数的使用。

/* use a define to avoid include chaining to get THIS_MODULE */
#define i2c_add_driver(driver) \i2c_register_driver(THIS_MODULE, driver)

i2c_del_driver函数

i2c_del_driver() 函数的作用是注销一个已注册的 i2c_driver。
它的函数原型如下:

#include <linux/i2c.h>void i2c_del_driver(struct i2c_driver *);

参数:
driver 指定要注销的 i2c_driver。

该函数会完成以下工作:

  1. 从 I2C 核心的 i2c_driver 管理链表中删除指定的 i2c_driver。
  2. 遍历该 i2c_driver 所管理的所有 i2c_client,并调用 remove 回调对每个 i2c_client 进行注销。
  3. 释放与 i2c_driver 相关的所有资源。
  4. 将 i2c_driver 的引用计数减一,如果减为 0 则释放 i2c_driver。

驱动代码如下:

static struct i2c_device_id my_id_table[] = {{ "mydev", 0 },{ }
};static int my_probe(struct i2c_client *client, const struct i2c_device_id *id)
{// 实现设备初始化 ...
}static int my_remove(struct i2c_client *client)
{// 实现设备注销 ... 
}struct i2c_driver my_driver = {.driver = {.name  = "mydriver",},.probe      = my_probe,.remove     = my_remove, .id_table   = my_id_table, 
};static int __init my_init(void)
{return i2c_register_driver(THIS_MODULE , &my_driver);
}static void __exit my_exit(void)
{i2c_del_driver(&my_driver);
}module_init(my_init);
module_exit(my_exit);

这个驱动的主要步骤是:

  1. 定义id_table用于I2C设备匹配
  2. 定义probe和remove回调来初始化和注销设备
  3. 定义并填充i2c_driver结构体
  4. 在模块初始化函数中调用i2c_register_driver()注册驱动
  5. 在模块注销函数中调用i2c_del_driver()注销驱动
    当该模块被加载后,内核会自动为其匹配id_table中的I2C设备,并调用probe完成初始化。而当模块被卸载时,会调用remove来注销设备并清理资源。
    这是一个典型的I2C驱动示例,展示了如何通过i2c_register_driver()和i2c_del_driver()来注册和注销一个I2C驱动,以及建立和断开与从设备的关联。

i2c_transfer函数

i2c_transfer() 函数的作用是执行一组 i2c_msg 中描述的 I2C 操作。这个函数实现了在一个 I2C 适配器上执行一组较复杂 I2C 操作的简单机制,通过一组 i2c_msg 进行描述,它会依次执行里面的每个消息,直到全部成功或遇错误。
它的函数原型如下:

#include <linux/i2c.h>
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);

参数

  • adap: I2C 适配器,指定要在其上执行这组 I2C 操作
  • msgs:i2c_msg 消息链表,指定要执行的一组 I2C 操作
  • num: msgs 链表中的 i2c_msg 消息数量

返回值:

  • 成功:返回0。 num变为0。
  • 失败:出现错误时返回错误码。 num变为还未执行的消息数据

机制:

  • 该函数会依次在 adap 上执行 msgs 链表中的所有 i2c_msg 消息描述的 I2C 操作。
  • 每执行一个 i2c_msg 中描述的操作,就将 num 减一。如果所有消息都执行成功,则 num 最终变为 0,函数返回 0 表示成功。
  • 如果在执行过程中出现错误,则立即返回错误码,num 返回还未执行的消息数量。
    该函数通过一个 i2c_msg 链表来描述一组要执行的 I2C 操作 - 这可能是多个发送消息,或者先发送后接收等组合。它使得我们可以通过一组简单的 i2c_msg 来描述较复杂的 I2C 数据交互过程。

使用这个函数的典型步骤是:

  1. 定义需要执行的 I2C 操作,并以 i2c_msg 的形式描述出来,构成一个 i2c_msg 链表 msgs
  2. 调用 i2c_transfer() 在 adap 上执行这个 msgs 链表中的所有操作
  3. 根据返回值判断操作是否成功执行
  4. 如果不成功,则根据 num 得到未执行的消息,可以选择重新执行或其他处理

它使我们可以通过简单的 i2c_msg 来描述一系列的 I2C 读写操作,并统一通过这个函数执行,而不必编写循环来逐个调用 read/write 函数,简化了 I2C 数据交互过程的实现难度。

这是 I2C 核心 API 中一个重要的函数,简化和统一了执行一组复杂 I2C 操作的实现过程,理解并熟练运用这个函数是使用 I2C 核心的基础。
综上,我们只需要构造好所需执行的 I2C 操作描述(以 i2c_msg 形式),然后调用这个函数,就可以在一个 I2C 适配器上执行该组操作,简化了 I2C 数据通信过程的实现。

i2c_master_recv函数

i2c_master_recv() 函数的作用是从 I2C 总线上的一个从设备读取数据。
它的函数原型如下:

#include <linux/i2c.h>
int i2c_master_recv(const struct i2c_client *client, char *buf,int count);

参数:

  • client: 从设备的 i2c_client,指定要读取数据的从设备
  • buf: 读数据缓存区
  • count: 要读的数据长度

返回值:

  • 失败:返回负数失败码。
  • 成功:返回成功读取的字节数

该函数的作用就是在一个 I2C 从设备上执行一个简单的读操作,它发送地址后开始接收数据,直到 count 个字节的数据被读取或出现错误为止。
使用这个函数的基本步骤是:

  1. 分配一个缓存区 buf,大小至少为 count
  2. 调用 i2c_master_recv() 在 client 指定的从设备上读取 count 个字节的数据到 buf
  3. 根据返回值判断操作是否成功及实际读取的数据长度
  4. 如果成功,则 buf 中前 return 个字节就是读到的数据,如果不成功,则需要相应地进行重试或错误处理
    该函数是 I2C 核心 API 进行读操作的基本接口,通过它我们可以轻易在一个 I2C 从设备上读取指定长度的数据,简化了 I2C 读操作的实现过程。
    相比于自行构造 i2c_msg 然后调用 i2c_transfer() 来执行一次 I2C 读操作,这个函数的用法更简单清晰,专注于执行一般的简单读操作,这也是它的主要用途。
    当需要在驱动中执行一个简单的从一个 I2C 设备读取某长度数据的操作时,这个函数是一个很好的选择。它隐藏了消息构造及传输过程,使读操作的实现变得异常简单。

i2c_master_send

i2c_master_send() 函数的作用是向 I2C 总线上的一个从设备发送数据。
它的函数原型如下:

#include <linux/i2c.h>
int i2c_master_send(const struct i2c_client *client, const char *buf,int count);

参数:

  • client: 从设备的 i2c_client,指定要发送数据的从设备
  • buf: 发送数据缓存区
  • count: 要发送的数据长度

返回值:
失败:负数
成功:成功发送的字节数

该函数的作用就是在一个 I2C 从设备上执行一个简单的写操作,它发送从 buf 中读取的 count 个字节的数据,然后结束操作。

使用这个函数的基本步骤是:

  1. 构造要发送的数据,放入缓存区 buf
  2. 调用 i2c_master_send() 在 client 指定的从设备上发送 buf 中的数据
  3. 根据返回值判断操作是否成功
  4. 如果不成功,则需要进行重试或错误处理
    该函数是 I2C 核心 API 进行写操作的基本接口,通过它我们可以轻易在一个 I2C 从设备上发送指定长度的数据,简化了 I2C 写操作的实现过程。
    相比于自行构造 i2c_msg 然后调用 i2c_transfer() 来执行一次 I2C 写操作,这个函数的用法更简单清晰,专注于执行一般的简单写操作,这也是它的主要用途。

5.2.4 I2C的驱动框架

5.2.4.1 driver 驱动端框架


//其它struct file_operations函数实现原理同硬编驱动static int mpu6050_probe(struct i2c_client *pclt,const struct i2c_device_id *pid)
{//做硬编驱动模块入口函数的活
}static int mpu6050_remove(struct i2c_client *pclt)
{//做硬编驱动模块出口函数的活
}以下,把两种匹配方式(a,名称及id匹配;b,设备树匹配)都写在这个框架里了。但实际使用一般只保留对应的方式即可,即以下要么用a方式,要么用b方式。
a、<名称匹配或ID匹配>时定义struct i2c_device_id数组
static struct i2c_device_id mpu6050_ids[] = 
{{"mpu6050",0},//.....{}
};b、<设备树>匹配时定义struct of_device_id数组
static struct of_device_id mpu6050_dts[] =
{{.compatible = "invensense,mpu6050"},//....{}
};通过定义struct i2c_driver类型的全局变量来创建i2c_driver对象,同时对其主要成员进行初始化
struct i2c_driver mpu6050_driver = 
{.driver = {.name = "mpu6050",.owner = THIS_MODULE,.of_match_table = mpu6050_dts,},.probe = mpu6050_probe,.remove = mpu6050_remove,.id_table = mpu6050_ids,
};以下其实是个宏,展开后相当于实现了模块入口函数和模块出口函数
module_i2c_driver(mpu6050_driver);MODULE_LICENSE("GPL");
module_i2c_driver宏

框架里有一个宏 module_i2c_driver(mpu6050_driver);展开以后相当于如下代码:

static int __init mpu6050_driver_init(void) 
{return i2c_add_driver(&mpu6050_driver);
}static void __exit mpu6050_driver_exit(void) 
{i2c_del_driver(&mpu6050_driver); 
}module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

5.2.4.2 client 设备端框架

设备端根据对I2C的设备地址是否确定可知,又分为两种框架

  • 一种框架为明确知道I2C设备的地址。
  • 一种是只知道I2C设备的可能地址
    下面分别解释

在这里插入图片描述

在这里插入图片描述

5.2.5 i2c两种匹配方式的例程

因为篇幅的原因,把例程放到了
【嵌入式环境下linux内核及驱动学习笔记-(15-1)例程】里了。
请查看其第2节及第3节。


http://www.ppmy.cn/news/292854.html

相关文章

【Android】WMS(二)Window的添加

软件盘相关模式 在 Android 应用开发中&#xff0c;软键盘的显示与隐藏是一个经常出现的问题&#xff0c;而 WindowManager 的 LayoutParams 中定义的软键盘相关模式则为开发者提供了一些解决方案。 其中&#xff0c;SoftInputMode 就是用于描述软键盘的显示方式和窗口的调整…

【Flutter】如何移除 Flutter 右上角的 DEBUG 标识

文章目录 一、前言二、什么是 DEBUG 标识三、为什么我们需要移除 DEBUG 标识四、如何移除 DEBUG 标识五、完整代码六、总结 一、前言 欢迎来到 Flutter 的世界&#xff01;在这篇文章中&#xff0c;我们将探索 Flutter 的一些基础知识。但是&#xff0c;你知道吗&#xff1f;这…

诺基亚n9支不支持java_诺基亚N9支持720p播放吗

诺基亚N9可以播放720p视频。诺基亚N9采用1GHz德州仪器OMAP3630处理器&#xff0c;GPU型号为Imagination PowerVR SGX530&#xff0c;搭配1GB的运行内存 RAM&#xff0c;可以播放MP4和H.264格式的高清视频。 但是诺基亚N9有的视频还播不了&#xff0c;要另下播放器&#xff0c;同…

BlackBerry 多媒体播放编程

概述 移动多媒 体包括 使用移动 终端播放 音乐&#xff0c; 视频&#xff0c;拍 照&#xff0c;录制 视频&#xff0c; 和在线影 音 。 Bla ck Berry 支持 移动多媒 体&#xff0c;你 可以通 过 Bla ckB erry Java 或 Bla ck Berry 浏览器 来创建 自己 的媒体应 用。功 能包括…

专家看衰Windows Phone 8前景 诺基亚又要悲剧?

尽管诺基亚连续三个季度出现10亿美元以上的亏损&#xff0c;不过近来诺基亚的股价出现了强势反弹&#xff0c;股票价格目前已经在第二季度财报发布之后翻了两番。然而 Research的分析师Pierre Ferragu认为&#xff0c;股价之所以上涨&#xff0c;是受下周即将召开的诺基亚世界大…

诺基亚N97沃达丰多媒体设备无缝连接选件

诺基亚N97沃达丰多媒体设备无缝连接选件   诺基亚世界顶级手机品牌一直带给我们惊喜与它的辉煌和最新技术的产品。它已经抓住了移动通信市场的N系列。现在&#xff0c;这个品牌已经想出了另一个多媒体惊叹诺基亚N97手机是现在挤满了世界领先的沃达丰的网络。该网络连接方便了…

用 Windows Media Encoder 9 架设网上直播

网上的电台发展得越来越快&#xff0c;其中之一的原因是网络的迅速发展&#xff0c;但用来架设电台的软件&#xff0c;无非还是几个:Winamp ShoutCast - 构建MP3流媒体Real Server - 构建Real Media流媒体Windows Media Encoder - 构建Windows Media流媒体QuickTime Broadcaste…

Windows Phone7成为诺基亚核心目标

在上周三&#xff08;4月27日&#xff09;&#xff0c;诺基亚对外正式宣布&#xff1a;诺基亚&#xff08;Nokia&#xff09;将Symbian软件研发工作外包给一家名为“埃森哲&#xff08;Accenture&#xff09;”的咨询公司&#xff0c;相关的约3000名研发人员也将随后转移到埃森…