Linux IIC核心、总线与设备驱动

I^{^{2}}C 总线仅仅使用SCL、SDA这两根信号线就实现了设备之间的数据交互,极大地简化了对硬件资源和PCB版布线空间的占用。因此,I^{^{2}}C总线非常广泛的使用在EEPROM、实时钟、小型LCD等与CPU的接口中。Linux系统定义了I^{^{2}}C驱动体系结构。在Linux系统中,I^{^{2}}C驱动由 3部分组成,即IIC核心、I^{^{2}}C总线和I^{^{2}}C设备驱动。这3部分相互协作,形成了非常通用、可适应性很强的I^{^{2}}C框架。

一、Linux I^{^{2}}C体系结构

Linux的I^{^{2}}C体系结构分为3个部分:

  • I^{^{2}}C核心     提供了I^{^{2}}C总线驱动和设备驱动的注册、注销方法,I^{^{2}}C通信方法上层的与具体适配器无关的代码以及探测设备、检测设备地址的上层代码。结构图如下:

  • I^{^{2}}C总线驱动     是对I^{^{2}}C硬件体系结构中适配器的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。I^{^{2}}C总线驱动主要包括I^{^{2}}C适配器数据结构体i2c_adapter、I^{^{2}}C适配器的Algorithm数据结构i2c_algorithm和控制I^{^{2}}C适配器产生通信信号的函数。  经由I^{^{2}}C总线驱动的代码,我们可以控制I^{^{2}}C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。
  • I^{^{2}}C设备驱动   也称客户程序,是对I^{^{2}}C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I^{^{2}}C适配器上,通过I^{^{2}}C适配器与CPU交换数据。I^{^{2}}C设备驱动主要包含数据结构i2c_driver和i2c_client,需要更具具体设备实现其中的成员函数。

在Linux内核源代码中的drivers目录下有一个i2c目录,而在i2c目录下又有如下文件和文件夹。

  1. i2c-core.c   这个文件实现了I^{^{2}}C核心的功能以及/proc/bus/i2c*接口。
  2. i2c-dev.c     实现了I^{^{2}}C适配器设备文件的功能,每一个I^{^{2}}C适配器被分配一个设备。通过适配器访问设备时的主设备号都为89,次设备号为0~255.应用程序通过“i2c-%d”(i2c-0,i2c-1,...)文件名并使用文件接口open() write() read() ioctl() 和 close() 等来访问这个设备。i2c-dev.c 并不是针对特定的设备而设计的,只是提供了通用的read()  write() 和 ioctl() 等接口,应用层可以借用这些接口访问挂在适配器上的I^{^{2}}C设备的存储空间或寄存器,并控制I^{^{2}}C设备的工作方式。
  3. busses文件夹    这个文件夹包含了一些I^{^{2}}C主机控制器的驱动,如i2c-tegra.c ,i2c-omap.c , i2c-versatile.c , i2c-s3c2410.c 等。
  4. algos文件夹     实现了一些I^{^{2}}C总线适配器的通信方式。

此外,内核中的i2c.h头文件中对对i2c_adapter、i2c_algorithm、i2c_driver和i2c_client这4个数据结构进行了定义。他们在inxlude/linux/i2c.h文件中。

struct i2c_adapter{
    struct module *owner;
    unsigned int class;
    const struct i2c_algorithm *algo;
    void *algo_data;
    struct rt_mutix bus_look;
    int timeout;
    int retries;
    struct device dev;
    int nr;
    char name[48];
    struct mutex userspace_clients_lock;
    struct list_head usersapce_clients;
    struct i2c_bus_recovery_info *bus_recovery_info;
}

struct i2c_algorithm{
    int (*master_xfer)(struct i2c_adapter *adap,struct i2c_msg *msgs,int num);
    int (*smbus_xfer)(struct i2c_adapter *adap,u16 addr,unsigned short flags,char read_write,u8 command,int size,union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *);
}

struct i2c_driver{
    unsigned int class;
    int (*attach_adapter)(struct i2c_adapter *) __deprecated;
    int (*probe)(struct i2c_client *,const struct i2c_device_id *);
    int (*remove)(struct i2c_client *);
    void (*shurtdown)(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;
    int (*detect)(struct i2c_client *,struct i2c_board_info *);
    struct list_head clients;
}

struct i2c_client{
    unsigned short flags;
    unsigned short addr;
    char name[I2C_NAME_SIZE];
    struct i2c_adapter *adapter;
    struct device dev;
    int irq;
    struct list_head detected;
}

下面分析i2c_adapter、i2c_algorithm、i2c_driver和i2c_client这4个数据结构的作用及其盘根错节的关系。

(1)i2c_adapter与i2c_algorithm

i2c_adapter对应与物理上的一个适配器,而i2c_algorithm对应一套通信方法。一个I^{^{2}}C适配器需要i2c_algorithm提供的通信函数来控制适配器产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做了,因此i2c_adapter中包含所使用的i2c_algorihm的指针。

(2)i2c_driver与i2c_client

i2c_driver对应一套驱动方法,其只要成员函数是probe(),remove(),suspend(),resume()等,另外,struct i2c_device_id形式的id_table是该驱动所支持的I^{^{2}}C设备的ID表。i2c_client对应与真实的物理设备,每个I^{^{2}}C设备都需要一个i2c_client来描述。i2c_driver与i2c_client的关系是一对多,一个i2c_driver可以支持多个同类型的i2c_client.,i2c_client的信息通常在BSP的板级文件中通过i2c_board_info填充。

(3)i2c_adapter与i2c_client

i2c_adapter与i2c_client的关系与I^{^{2}}C硬件系统中适配器和设备的关系一致,即i2c_client依附于i2c_adapter。由于一个适配器可以链接多个I^{^{2}}C设备,所有一个i2c_adapter也可以被多个i2c_client依附,i2c_adapter中包含依附于它的i2c_client的链表。

假设I^{^{2}}C总线适配器xxx上有两个使用相同驱动程序的yyyI^{^{2}}C设备,再打开该I^{^{2}}C总线的设备节点后,相关数据结构之间的逻辑组织关系如下图:

 从上图分析可知,虽然I^{^{2}}C硬件体系结构比较简单,但是I^{^{2}}C体系结构在Linux中的实现却相当复杂。当工程师拿到实际的电路板时,面对复杂的LinuxI^{^{2}}C子系统,应该如何下手写驱动了?究竟哪些是需要亲自做的,那些时内核已经提供得了?理清这个问题,可以使我们在面对具体问题时快速抓住重点。

一方面,适配器驱动可能是Linux内核本身还不包括的;另一方面,挂接在适配器上的具体设备驱动可能是Linux内核还不包括的。因此,工程师要实现的主要工作如下(前两项属于I^{^{2}}C总线驱动,后两项属于I^{^{2}}C设备驱动):

  • 提供I^{^{2}}C适配器的硬件驱动,探测、初始化I^{^{2}}C适配器(如申请I^{^{2}}C的I/O地址和中断号)、驱动CPU控制I^{^{2}}C适配器从硬件上产生各种信号以及处理I^{^{2}}C中断等。
  • 提供I^{^{2}}C适配器的Algorithm,用具体适配器的xxx_xfer()函数填充i2c_algorithm的master_xfer指针,并把i2c_algorithm指针赋值给i2c_adapter的algo指针。
  • 实现I^{^{2}}C设备驱动中的i2c_driver接口,用具体设备yyy的yyy_prob()、yyy_remove()等函数指针和i2c_device_id设备ID表赋值给i2c_driver的probe、remove等指针。
  • 实现I^{^{2}}C设备所对应类型的具体驱动,i2c_driver只是实现设备与总线的挂接,而挂接在总线上的设备则千差万别。例如,如果是字符设备,就要实现文件操作接口,即实现具体设备yyy的yyy_read()、yyy_write()和yyy_ioctl()函数等;如果是声卡。就实现ALSA驱动。

二、LinuxI^{^{2}}C核心

I^{^{2}}C核心(driver/i2c/i2c-core.c)中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解齐整的主要函数非常关键,因为I^{^{2}}C总线驱动和设备驱动之间以I^{^{2}}C核心作为纽带,I^{^{2}}C核心的主要函数如下:

(1)增加/删除i2c_adapter

int i2c_add_adapter(struct i2c_adapter *adap);
void i2c_del_adapter(srtruct i2c_adapter *adap);

(2)增减/删除i2c_driver

int i2c_register_driver(struct module *owner,struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
#define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE,driver)

(3)I^{^{2}}C传输、发送和接收

int i2c_transfer(struct i2c_adapter *adap,struct i2c_mags *msgs,int num)
{
    int ret;
    if(adap->algo->master_xfer){
        ...
        ret = adap->algo->master_xfer(adap,mashs,num);
        ...
        return ret;
    }else{
        dev_sbg(&adap->dev,"I2C level transfers not supported\n");
        return -ENOSYS;
    }
}
int i2c_master_send(struct i2c_client *client,const char *buf,int count)
{
    int ret;
    struct i2c_adapter *adap = client-?adapter;
    struct i2c_msg msg;
    msg.addr = client->addr;
    msg.flags = client->flags&I2C_M_TEH;
    msg.len = count;
    msg.buf = (char *)buf;

    ret = i2c_transfer(adap,&msg,1);
    return (ret == 1) count : ret;
}
int i2c_master_recv(struct i2c_client *client,char *buf,int count)
{
    struct i2c_adapter *adap = client->adapter;
    struct i2c_msg msg;
    int ret;
    msg.addr = client->addr;
    msg.flags = client->flags&I2C_M_TEH;
    msg.flags |= I2C_M_RD;
    msg.len = count;
    msg.buf = buf;

    ret = i2c_transfer(adap,&msg,1);
    return (ret == 1) count : ret;
}

i2c_transfer()函数用于进行I^{^{2}}C适配器和I^{^{2}}C设备之间的一组消息交互,其中第二个参数是一个指向i2c_msg数组的指针,所以i2c_transfer()一次可以传输多个i2c_msg。而对于时序比较复杂的外设,i2c_master_send()函数和i2c_master_recv()函数内部会调用i2c_transfer()函数分别完成一条写信息和一条读信息。

三、LinuxI^{^{2}}C适配器驱动

I^{^{2}}C适配器驱动的注册与注销

由于I^{^{2}}C总线控制器通常是在内存上,所以它本身也连接在platfrom总线上,要通过plantfrom_driver和plantfrom_device的匹配来执行。因此尽管I^{^{2}}C适配器给别人提供了总线,它自己也被认为是接在plantform总线上的一个客户。Linux的总线、设备和驱动模型实际上是一个树形结构,每个节点可能成为别人的总线控制器,但是自己也被认为是从上一级总线枚举出来的。

通常我们会在I^{^{2}}C适配器所对应的platfrom_driver的probe()函数中完成两个工作:

  1. 初始化I^{^{2}}C适配器所使用的硬件资源,如申请I/O地址、中断号、时钟等。
  2. 通过i2c_add_adapter()添加i2c_adapter的数据结构,当然这个i2c_adapter数据结构的成员已经被xxx适配器的相应函数指针锁初始化。

通常我们会在platfrom_driver的remove()函数中完成与加载函数相反的工作:

  1. 释放I^{^{2}}C适配器所使用的硬件资源,如释放I/O地址、中断号、实钟等。
  2. 通过i2c_del_adapter()函数删除i2c_adapter的数据结构。

I^{^{2}}C适配驱动的注册与注销模板

static int xxx_i2c_probe(struct platform_device *pdev)
{
    struct i2c_adapter *adap;
    ...
    xxx_adapter_hw_init();
    adap->dev.parent = %pdev->dev;
    adap->dev.of_node = pdev->dev.of_node;
    
    rc = i2c_add_adapter(adap);
    ...
}

static int xxx_i2c_remove(struct platform_device *pdev)
{
    ...
    xxx_adapter_hw_free();
    i2c_del_adapter(&dev->adapter);

    return 0;
}

static const struct of_device_id xxx_i2c_of_match[]={
    {.compatible = "vendor,xxx-i2c",},
    {},
};

MODULE_DEVICE_TABLE(of,xxx_i2c_of_match);

static struct platform_driver xxx_i2c_driver={
    .driver = {
                .name = "xxx-i2c",
                .owner = THIS_MODULE,
                .of_match_table = xxx_i2c_of_match,    
                },
    .probe = xxx_i2c_probe,
    .remove = xxx_i2c_remove,
};

module_platfrom_driver(xxx_i2c_driver);

上述代码中的xxx_adpater_hw_init()和xxx_adpater_hw_free()函数的实现都与具体的CPU和I^{^{2}}C适配器硬件直接相关。

I^{^{2}}C总线的通信方法

我们需要为特定的I^{^{2}}C适配器实现通信方法,主要是实现i2c_algorithm的functionality()函数和master_xfer()函数。functionlity()函数非常简单,用于返回algorithm所持有的通信协议,如I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR、I2C_FUNC_SMBUS_READ_BYTE、I2C_FUNC_SMBUS_WRITE_BYTE等。master_xfer()函数在I^{^{2}}C适配器上完成传递给他的i2c_msg数据中的每个I^{^{2}}C消息。master_xfer()函数模板如下:

static int i2c_adapter_xxx_xfer(struct i2c_adapter *adap,struct i2c_msg *msgs,int num)
{
    ...
    for(i=0;i<num;i++){
        i2c_adapter_xxx_start();
        if(msgs[i]->flags & I2C_M_RD){
            i2c_adapter_xxx_setaddr((msg->addr << 1) | 1);
            i2c_adapter_xxx_wait_ask();
            i2c_adapter_xxx_readbytes(msgs[i]->buf,msgs[i]->len);
        }else{
            i2c_adapter_xxx_setaddr(msg->addr << 1);
            i2c_adapter_xxx_wait_ask();
            i2c_adapter_xxx_writebytes(msgs[i]->buf,msgs[i]->len);
        }
    }
    i2c_adapter_xxx_atop();
}

上述代码实际上给出了一个master_xfer()函数处理I^{^{2}}C消息数组的流程,对于数组中的每个消息,先判断消息类型,若为读信息,则赋值设备地址为(msg->addr<<1)|1 ,否则为msg->addr<<1 。对每个消息产生一个开始位,紧接着传送从设备地址,然后开始数据的发送和接收,且对最后的消息还产生一个停止位。下图为整个master_xfer()完成的时序:

 master_xfer()模板中带xxx的函数用于完成适配器的底层硬件操作,与I^{^{2}}C适配器和CPU的具体硬件直接相关,需要工程师根据芯片的数据手册来实现。

i2c_adapter_xxx_readbytes()用于从从设备上接收一串数据,i2c_adapter_xxx_writebytes()用于向从设备写入一串数据,这两个函数的内部也会涉及到I^{^{2}}C总线协议中的ACK应答。

master_xfer()函数的实现形式会很多,多数驱动以中断方式来完成这个流程,比如发起硬件请求后,将自己调度出去,因此中间会伴随着睡眠的动作。

多数I^{^{2}}C总线驱动会定义一个xxx_i2c结构体,作为i2c_adapter的algo_data(类似“私有数据”),其中包含I^{^{2}}C消息数组指针、数组索引及I^{^{2}}C适配器Algorithm访问控制用的自旋锁、等待队列等,而master_xfer()函数在完成i2c_msg数组中消息的处理时,也经常访问xxx_i2c结构体的成员以获取寄存器基地址、锁等信息。下面是xxx_i2c结构模板。、

struct xxx_i2c{
    spinlock_t lock;
    wait_queue_head_t wait;
    struct i2c_msg *msg;
    unsigned int msg_num;
    unsigned int msg_idx;
    unsigned int msg_ptr;
    ...
    struct i2c_adapter adap;
}

四、LinuxI^{^{2}}C设备驱动

I^{^{2}}C设备驱动要使用i2c_driver和i2c_client数据结构并填充i2c_driver中的成员函数。i2c_client一般包括在设备的私有信息结构体yyy_data中,而i2c_driver则适合被定义为全局变量并初始化,下面代码为已经初始化的i2c_driver:

static struct i2c_driver yyy_driver{
    .driver = {
        .name = "yyy",
    },
    .probe = yyy_probe,
    .remove = yyy_remove,
    .id_table = yyy_id,
}

      LinuxI^{^{2}}C设备驱动的模块加载与卸载

I^{^{2}}C设备驱动的模块加载函数通过I^{^{2}}C核心的i2c_add_driver()API函数添加i2c_driver的工作,而模块卸载函数需要做相反的工作:通过I^{^{2}}C核心的i2c_del_driver()函数删除i2c_driver。模块加载和卸载模板如下:

static int __init yyy_init(void)
{
    return i2c_add_driver(&yyy_driver);
}

module_initcall(yyy_init);

static void __exit yyy_exit(void)
{
    i2c_del_driver(&yyy_driver);
}

module_exit(yyy_exit);

LinuxI^{^{2}}C设备驱动的数据传输

I^{^{2}}C设备上读写数据的时序且通常通过i2c_msg数组进行组织,最后通过i2c_transfer()函数完成,

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值