IIC设备驱动分析
一.前言
之前调试 3520D 的时候,我们使用的 IT6801 驱动是直接在我们的应用程序中进行驱动。这样做虽然简单,但是可移植性移植不是很好。
后来,我们在制作 3521D 的板子的时候,我们找了一个海思的方案商,他可以帮我们一起调试驱动。于是我们就把 IT6801 的驱动程序给了他,然后他根据我们提供的应用驱动写了一个 kernel 的驱动程序。
之前一直想要学习一下 kernel 的驱动方法,但是,由于我们的项目中一直没有需要我们自己写 kernel 驱动的,因此,一直没有机会学习。这次刚好有机会可以学习一下。
二.驱动框架
驱动框架是驱动程序所具备的最基本的特征,驱动程序具备几个不同于我们应用程序的函数特点。
这里先将一个驱动程序最基本的特征列出来:
module_init(it6801_init);
module_exit(it6801_exit);
MODULE_LICENSE("GPL");
上面的三个部分是每个驱动程序必备的三个部分。
-
module_init(it6801_init);
这个我们的驱动的入口,也就是我们在 insmod 加载驱动的时候我们的 kernel 是由这个函数入口进去的。
-
module_exit(it6801_exit);
这个函数是我们的驱动出口,也就是当我们使用 remod 命令移除驱动的时候,我们的内核会调用这个函数去销毁我们的驱动配置。
-
MODULE_LICENSE(“GPL”);
这个宏是我们用来告诉我们的内核我们的驱动模块带有一个 GPL 的许可证,这样内核在加载的时候就不会认为其实非法的了。
三.驱动初始化分析
static int __init it6801_init(void)
{
unsigned char ret;
ret = i2c_client_init(); //初始化IIC从设备
if(ret != 0)
{
printk("i2c_client_init error\n");
return -1;
}
if(IT6802_fsm_init() != 0) //
{
printk("IT6802_fsm_init failed\n");
return -1;
}
it6802PortSelect(F_PORT_SEL_0);
it6802HPDCtrl(1, 1);
//xxxxx
if (misc_register(&it6801_dev)) //注册一个 miscdevice 对象
{
printk("ERROR: could not register it6801 devices\n");
return -1;
}
mutex_init(&gs_mutex_lock); //初始化全局互斥锁
gs_irq_wq = create_singlethread_workqueue("it6801_0"); //创建内核线程
init_timer(&gs_timer); //初始化定时器参数
gs_timer.function = (void *)tick_query_std; //设置定时器超时函数
gs_timer.expires = HZ / 3 + jiffies; //设置定时时间
add_timer(&gs_timer); //添加定时器,定时器开始生效
return 0;
}
这个就是我们上面的 module_init 函数调用的函数。这个函数就是我们的所有的操作入口,下面分析一下这个函数。
-
static int __init it6801_init(void)
可以看到,这个函数的函数名使用的是static int类型,而且还带了一个*__init*的标记,这里的类型定义不是强制的,但是,我们习惯上还是使用这样的命名方式。因为这样我们可以很方便的知道这个是我们驱动的入口。
-
i2c_client_init();
这个是我们的i2c设备的初始化的操作,这里先省略,我们后面分析。
-
if(IT6802_fsm_init() != 0)
这里其实是我们的具体业务逻辑,这里不对其进行分析,它里面的所有操作都是调用我的i2c接口函数i2c_write_byte i2c_read_byte进行的具体操作。
-
it6802PortSelect(F_PORT_SEL_0);
-
**it6802HPDCtrl(1, 1) ; **
这两个也都是我们的具体业务操作,这里不对它们进行分析。
-
**if (misc_register(&it6801_dev)) **
这里我们注册里一个miscdevice类型的函数,也就是说我们把我们的it6801当作了miscdevice设备进行住注册了。这里我们之所以把IIC设备使用定义为miscdevice设备,主要是我们的内核把驱动设备分了以下几类:
- 字符设备
- 块设备
- 网络设备
- 杂项设备
而我们的miscdevice设备就是我们的杂项设备,因为IIC不在这个这些设备范围内,因此,这里我们将其归类为其他设备的范围内。
-
mutex_init(&gs_mutex_lock);
这个是初始化了一个互斥锁,主要是防止我们的多个驱动同时调用我们的IIC设备。
-
**gs_irq_wq = create_singlethread_workqueue(“it6801_0”); **
创建一个内核线程,并给其命名为it6801_0,这里返回了内核给这个线程分配的内存地址空间。
-
**init_timer(&gs_timer); **
初始化timer_list的结构成员。
-
*gs_timer.function = (void )tick_query_std;
设置定时超时函数。
-
gs_timer.expires = HZ / 3 + jiffies;
设置定时超时时间。
-
add_timer(&gs_timer);
将timer加入内核timer列表中,等待处理。
四.驱动具体操作分析
1.IIC设备初始化
int i2c_client_init(void)
int i2c_client_init(void)
{
struct i2c_adapter* i2c_adap;
i2c_adap = i2c_get_adapter(0); //得到IIC设备0的适配器(IIC模块)
//强制创建一个新的设备对象 it6801
it6801_client = i2c_new_device(i2c_adap, &hi_info);
i2c_put_adapter(i2c_adap); //释放IIC的适配器
if(it6801_client == NULL)
return -1;
return 0;
}
struct i2c_adapter* i2c_adap;
定义一个 i2c 适配器指针,这里是我们 kernel 进行管理的模块,这里我们定义一个新的模块,以方便后续的操作。
这里把这个结构体的成员列出来:
struct i2c_adapter {
struct module *owner; // 所有者
unsigned int id;
unsigned int class; // 该适配器支持的从设备的类型
const struct i2c_algorithm *algo; // 该适配器与从设备的通信算法
void *algo_data;
/* data fields that are valid for all devices */
struct rt_mutex bus_lock;
int timeout; // 超时时间
int retries;
struct device dev; // 该适配器设备对应的device
int nr; // 适配器的编号
char name[48]; // 适配器的名字
struct completion dev_released;
struct list_head userspace_clients; // 用来挂接与适配器匹配成功的从设备i2c_client的一个链表头
};
什么是adapter:
这里我们需要知道什么是 adapter,其实 adapter 是我们的芯片内部自带的一个硬件设备,而这类设备由于在我们的芯片内部,因此,我们的 CPU 可以直接通过总线的方式对其进行操作。因此,我们的内核也就可以直接对其进行管理。这一部分是在我们的内核代码里面的,我们通过内核编译选项的方式可以加载这个设备。
这里我们使用的是 i2c 的 adapter,因为我们要使用这个设备来实现我们对外部芯片的操作,因此,这里我们就需要先定义一个这个设备。
-
i2c_adap = i2c_get_adapter(0);
获得一个 i2c适配器,并将其关联给我们创建的适配器的指针。
这里的参数 0 是我们的 /dev 下面的 i2c 的设备号,这里我们 /dev 下只有一个 i2c-0 设备,因此,这里的参数 0 即是获取到的这个设备。
-
it6801_client = i2c_new_device(i2c_adap, &hi_info);
i2c_new_device函数原型:
**struct i2c_client * i2c_new_device(struct i2c_adapter adap, struct i2c_board_info const info);
上面可以看到,我们这个函数的原型中有两个参数:
-
*struct i2c_adapter adap;
这个参数我们上面已经知道了,这里不再解释。
-
*struct i2c_board_info const 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; };
这里我们使用的参数:
//我们的驱动代码 static struct i2c_board_info hi_info = { I2C_BOARD_INFO("it6801", 0x90), }; //内核include/linux/i2c.h中 #define I2C_BOARD_INFO(dev_type, dev_addr) \ .type = dev_type, .addr = (dev_addr)
由我们定义的结构体可以看出,我们的这个参数其实就是定义了我的 i2c 设备的设备地址和设备名。然后将这个设备的设备信息给我们的 i2c_adapter。
我们通过 i2c_new_device 这个函数,将给我们自己的创建的 i2c_adapter 和我们的要操作的对象i2c_client关联了起来。然后返回 i2c_client 的指针。
-
it6801_client:
这里的返回值是 struct i2c_client 类型的结构体指针,这里我们看一下这个结构体的内容:
struct i2c_client { unsigned short flags; /* div., see below */ unsigned short addr; /* chip address - NOTE: 7bit */ /* addresses are stored in the */ /* _LOWER_ 7 bits */ char name[I2C_NAME_SIZE]; struct i2c_adapter *adapter; /* the adapter we sit on */ struct device dev; /* the device structure */ int irq; /* irq issued by device */ struct list_head detected; };
可以看到这个结构体中有 i2c_adapter 这个成员,其实我们的 IT6801 最终就是要实例化为一个 i2c_client 的对象。这个对象包含了它的名字,地址,以及它所使用的 i2c_adapter 等信息,这样我们就可以直接通过这个对象来获取或操作我们想要的信息。
-
-
i2c_put_adapter(i2c_adap);
释放 i2c 的适配器,我们将获得的设备属性给赋给了i2c_client了,因此,这里我们的 i2c_get_adapter 函数的功能完成了,这里我们需要给它释放掉。
adapter和client:
这里有我们有两个概念,一个是 adapter 一个是 client。这两个其实分别对应了我们驱动的两个层次,一个是我们的片内驱动,一个是片外驱动。
adapter 属于片内驱动,它和我们的 CPU 是通过总线的方式通信的,而它对于我们的 CPU 而言也是一个设备,不过这个设备是集成在我们的 kernel 内部的,我们可以通过配置 kernel 的方式来启用它。
client 属于片外驱动,它相当于我们的芯片外部的设备,它是通过 IIC 总线的方式和我们的芯片相连接的。我们使用这个设备其实本质是我们的 CPU 通过控制 adapter 进而间接的控制它。
2.client设备初始化和控制:
if(IT6802_fsm_init() != 0)
{
printk("IT6802_fsm_init failed\n");
return -1;
}
it6802PortSelect(F_PORT_SEL_0);
it6802HPDCtrl(1, 1) ;
这一部分是我们对我们的 IT6801 进行的初始化和操作。之前的初始化只是创建了一个 client 这个设备对象,这里的初始化才开始真正的对这个设备进行初始化操作。
-
if(IT6802_fsm_init() != 0)
int IT6802_fsm_init(void) { struct it6802_dev_data *it6802data = get_it6802_dev_data(); IT680X_DEBUG_PRINTF(("IT6802_fsm_init( ) \n")); //FIX_ID_002 xxxxx Check IT6802 chip version Identify for TogglePolarity and Port 1 Deskew if(IT6802_Identify_Chip() == 0) { return -1; } IT6802_Rst(it6802data); ......... ......... //FIX_ID_041 xxxxx Add EDID reset // fo IT6803 EDID fail issue hdmirxset(REG_RX_0C0, 0x20, 0x20); //xxxxx 2014-0731 [5] 1 for reset edid delay1ms(1); hdmirxset(REG_RX_0C0, 0x20, 0x00); ........ ........ }
这里的初始化代码比较长,我们不对功能进行分析。这里主要分析我们如何通过这个函数来操作 i2c 总线的。
这里我们通过对代码进行追踪,最后发现所有的操作其实都是调用了下面的函数:
BOOL i2c_write_byte( BYTE address,BYTE offset,BYTE byteno,BYTE *p_data,BYTE device ) { int ret; int idx = 0; for (idx = 0; idx < byteno; idx++) { ret = __I2CWriteByte8(address, offset + idx, &p_data[idx]); if (ret <= 0) { return 0; } } return 1; } BOOL i2c_read_byte( BYTE address,BYTE offset,BYTE byteno,BYTE *p_data,BYTE device ) { int ret; int idx = 0; for (idx = 0; idx < byteno; idx++) { ret = __I2CReadByte8(address, offset + idx, &p_data[idx]); if (ret <= 0) { printk("ret: %d\n", ret); return 0; } } return 1; }
通过上面两个函数可以看到,这两个函数主要的实现方式是使用了以下两个函数:
int __I2CWriteByte8(unsigned char chip_addr, unsigned char reg_addr, unsigned char *pdata) { int ret; unsigned char buf[2]; struct i2c_client* client = it6801_client; it6801_client->addr = chip_addr; buf[0] = reg_addr; buf[1] = *pdata; ret = i2c_master_send(client, buf, 2); udelay(300); return ret; } int __I2CReadByte8(unsigned char chip_addr, unsigned char reg_addr, unsigned char *pdata) { int ret; struct i2c_client* client = it6801_client; unsigned char buf[2]; it6801_client->addr = chip_addr; buf[0] = reg_addr; ret = i2c_master_recv(client, buf, 1); if (ret >= 0) { *pdata = buf