i.MX283開發板有兩個I2C接口,其中I2C0接了一個DS2460加密芯片,本文介紹Linux下如何編寫I2C驅動程序讀寫DS2460。
Linux上I2C架構可以分爲I2C核心、I2C總線驅動、I2C設備驅動三個部分:
I2C核心:主要爲總線驅動和設備驅動提供各種API,比如設備探測、註冊、註銷,設備和驅動匹配等函數。它在I2C架構中處於中間的位置。
I2C總線驅動:I2C總線驅動維護了I2C適配器數據結構(i2c_adapter)和適配器的通信方法數據結構(i2c_algorithm)。所以I2C總線驅動可控制I2C適配器產生start、stop、ACK等。I2C總線驅動在整個架構中是處於最底層的位置,它直接和真實的物理設備相連,同時它也是受CPU控制。
I2C設備驅動:I2C設備驅動主要負責和用戶層交互,此處的設備是一個抽象的概念,並非真實的物理設備,它是掛在I2C適配器上,通過I2C適配器與真實的物理設備通信。
下圖是整個I2C驅動框架:
實際上,Linux經過這麼多年的發展,已經形成了一套完善的I2C驅動框架,現在編寫I2C驅動,我們只需要完成上面所說的I2C設備驅動部分就可以,其他的芯片廠商已經爲我們做好了。
根據bus-dev-drv框架模型,我們的主要工作是實現設備文件和驅動文件,也就是上圖中的i2c_client和i2c_driver,i2c_client作用是完成設備和適配器的綁定 ,以確定設備驅動需要和哪個適配器下面的真實物理設備通信,i2c_driver的作用就是實現用戶層的open、write、read等調用。
下面將詳細說明整個過程:
注意:i2c適配器就是cpu中的i2c接口,cpu有幾個i2c接口,就代表有幾個適配器,又稱i2c主機。
由於i.mx283開發板有兩個i2c接口,所以這裏就有兩個適配器。首先假設有4個E2PROM掛在兩個適配器下面,現在用戶想要調
用設備驅動2來讀寫E2PROM3,根據上面提到的設備驅動模型,設備驅動2分爲i2c_client2和i2c_driver2,首先client要爲自己取
一個名字,假設叫做“E2PROM3”,然後它需要把自己和適配器2綁定(因爲E2PROM3是掛在適配器2下面的),最後向內核注
冊自己,I2c總線就知道自己下面多了一個設備——“E2PROM3”,i2c_client2部分的工作就做完了。
接着,需要實現i2c_driver2部分的功能,首先,i2c_driver2也需要爲自己取一個名字,也必須叫“E2PROM3”,然後它需要實現
open、close、write、read等這些文件接口,對於write和read,需要使用I2C核心層提供的I2C讀寫數據API接口。接着,需要
實現probe和remove函數接口,probe函數裏面實現就是字符設備驅動註冊的那一套流程,remove函數正好相反,最後,它也
需要向內核註冊自己,也就是告訴I2C總線,有一個新的驅動需要添加——“E2PROM3”,i2c_driver2部分的工作就做完了。
i2c總線:當向內核註冊i2c驅動時,會將i2c驅動添加到總線的鏈表中,遍歷總線上所有設備,通i2c_client>name
, i2c_driver-
>i2c_device_id->name
進行字符串匹配,如果匹配,就調用驅動程序的probe函數。上面已經將client和driver的名字都設置爲
"E2PROM3",所以它們是匹配的,然後,I2C總線會調用驅動的probe函數,並把client結構體通過形參傳給driver,然後執行
probe函數,註冊字符設備驅動,client結構體裏主要保存了適配器的信息,這個非常重要,當用戶APP進行read和write調用
時,首先會進入driver裏面的write和read函數,剛剛提到driver的write和read,需要使用I2C核心層提供的I2C讀寫數據API接
口,這些接口是用來和適配器通信的,所以需要指定哪個適配器,適配器信息就在剛剛保存的client結構體裏面,這樣,用戶層
和適配器就是通了,最後,適配器再和掛在它下面的真實物理設備通信,這個部分是不需要我們操心的,芯片廠家已經做好了這
部分的工作,至此,整個I2C通信流程就走完了。
Linux編寫I2C驅動程序的一般流程爲:
- 創建i2c_client,並向內核註冊這個client,註冊的方法有圖一中提到的4種方法。
- 創建i2c_driver,並向內核註冊driver,一般使用i2c_add_driver註冊driver。
- 註冊driver時,總線會自動調用match函數,匹配client和driver,如果匹配成功,會調用driver裏面的probe函數。
- probe函數裏面完成字符設備驅動那些工作,一個I2C驅動程序基本完成。
一、設備註冊
圖一中提到4種註冊方法,我們這裏僅介紹第一種,利用i2c_new_probed_device或者i2c_new_device註冊,這兩個函數的區別是後者必須指定真實設備的從機地址,前者是指定一個地址範圍,內核會一個個探測(發送起始信號,看是否有ACK)地址是否有效,若探測成功,則內核記錄這個地址,再調用i2c_new_device註冊設備。
這裏會使用一個重要的結構體i2c_board_info:
struct i2c_board_info {
char type[I2C_NAME_SIZE];
unsigned short flags;
unsigned short addr;
void *platform_data;
struct dev_archdata *archdata;
#ifdef CONFIG_OF
struct device_node *of_node;
#endif
int irq;
};
它的作用是描述物理設備信息,主要是name和addr,但內核不會探測這個addr的真實性,這個結構體是你已知真實物理設備的從機地址的情況下,可以直接指定設備信息,然後調用i2c_new_device註冊設備。
我們今天使用的是i2c_new_probed_device註冊設備,所以還需要給定一個地址範圍。
static const unsigned short addr_list[] =
{
0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};
addr_list數組裏面就定義了地址範圍,這裏的0x40是ds2460的真實地址,內核會從0x30一直探測到0x50,若某個地址探測成功,它會把這個地址保存到i2c_board_info.addr成員中,然後調用i2c_new_device註冊設備。
注意:無論使用哪種方式,都需指定設備的名稱,即i2c_board_info.type成員。使用i2c_new_probed_device註冊的好處是當
設備地址不正確時,設備是無法註冊成功的!
下面是ds2460_dev.c:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
static struct i2c_board_info ds2460_info=
{
I2C_BOARD_INFO("ds2460",0x40),//設備名稱+設備地址7bit
};
static const unsigned short addr_list[] =
{
0x30,0x35,0x40,0x50,I2C_CLIENT_END,
};
struct i2c_client *ds2460_client = NULL;//定義一個client
static int ds2460_dev_init(void)
{
struct i2c_adapter *adapter=NULL;
/*獲取i2c適配器0 */
adapter = i2c_get_adapter(0);
/*創建一個client 並綁定適配器*/
ds2460_client = i2c_new_probed_device(adapter,&ds2460_info,addr_list);
//ds2460_client = i2c_new_device(adapter,&ds2460_info);
if(ds2460_client != NULL)
{
i2c_put_adapter(adapter);
printk("module init ok\n");
return 0;
}
else
{
printk("device not exist\n");
return -EPERM;
}
}
static void ds2460_dev_exit(void)
{
i2c_unregister_device(ds2460_client);
printk("module exit ok\n");
}
module_init(ds2460_dev_init);
module_exit(ds2460_dev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");
二、驅動註冊
驅動實際就是字符設備驅動那一套流程,open、read、write等文件接口以及設備號申請、自動創建設備節點等操作。
只不過設備號申請、自動創建設備節點等操作需要放到probe函數裏面去,註銷操作則需要放到remove函數裏面去。
這裏主要講講read和write調用的實現:
驅動是和適配器通信的,而I2C核心爲我們提供了很多和適配器通信的接口函數,具體可見/linux-2.6.35.3/Documentation/i2c
裏面i2c-protocol和smbus-protocol兩則文檔。
下面是標準I2C協議的通信函數:
int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
除此之外,還有SMBUS協議的通信函數,SMBUS是I2C協議的子集:
SMBus Receive Byte: i2c_smbus_read_byte()
SMBus Send Byte: i2c_smbus_write_byte()
SMBus Read Byte: i2c_smbus_read_byte_data()
SMBus Read Word: i2c_smbus_read_word_data()
實際上很多I2C器件用的協議都是SMBus協議,它們的時序和SMBus完全一樣,所以這裏我們選擇SMBus的通信函數與DS2460通信。這裏主要用到i2c_smbus_read_byte_data()和i2c_smbus_read_word_data()兩個函數。
s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value)
/*寫一個字節數據到指定的地址,地址通過command字節傳送*/
/* S Addr Wr [A] Comm [A] Data [A] P */
/*===========================================================================*/
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command)
/*從指定的地址讀取一個字節,地址通過command字節傳送,返回值是讀到的字節*/
/* S Addr Wr [A] Comm [A] S Addr Rd [A] [Data] NA P */
i2c_driver驅動還有個非常重要的結構體i2c_driver :
struct i2c_driver {
unsigned int class;
/* Notifies the driver that a new bus has appeared or is about to be
* removed. You should avoid using this if you can, it will probably
* be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *);
int (*detach_adapter)(struct i2c_adapter *);
/* 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 *);
/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
*/
void (*alert)(struct i2c_client *, unsigned int data);
/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
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;
};
其中,成員const struct i2c_device_id *id_table記錄了驅動的名字和私有數據,驅動的名字必須和設備的名字一致,否則內核會匹配失敗。
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_t driver_data /* Data private to the driver */
__attribute__((aligned(sizeof(kernel_ulong_t))));
};
i2c_driver 結構體填充如下:
/*配置驅動的名稱和私有數據*/
static const struct i2c_device_id ds2460_id_table=
{
"ds2460",0//名稱爲“ds2460”需要和設備保持一致內核纔會調用驅動的probe函數
//0 表示沒有私有數據
};
/*創建i2c_driver結構體*/
static struct i2c_driver ds2460_driver =
{
.driver={
.name ="ds2460_driver",//這個名字無所謂
.owner=THIS_MODULE,
},
.probe = ds2460_probe,
.remove= __devexit_p(ds2460_remove),
.id_table = &ds2460_id_table,//這裏的名字纔是和設備進行匹配的
};
最後,在probe函數裏註冊常規字符設備驅動,在remove函數裏註銷字符設備驅動,i2c_driver工作就基本結束了。
ds2460_drv.c:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/i2c.h>
#include <linux/err.h>
#include <linux/slab.h>
#include<linux/fs.h>
#include<asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/delay.h>
#define SEQUENT_RW 0 //讀寫模式 0:讀寫不連續 1:連續讀寫
#define DEVICE_NAME "ds2460_drv"//驅動名稱
static struct i2c_client *ds2460_client =NULL;
static struct cdev *ds2460_cdev=NULL;
static struct class *ds2460_class = NULL;
static struct device *ds2460_device = NULL;
static dev_t device_id;
static int ds2460_open(struct inode *inode, struct file *fp)
{
return 0;
}
static int ds2460_release(struct inode * inode, struct file * file)
{
return 0;
}
static int ds2460_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
unsigned char address;//讀取的地址
unsigned char *data = NULL;//需要讀取的數據
unsigned char i;
int ret;
#if (SEQUENT_RW == 1)
else if(count == 1)
{
ret = copy_from_user(&address, buf, 1);
if(ret < 0)
{
printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
return -EINVAL;
}
ret = i2c_smbus_read_byte_data(ds2460_client,address);
if(ret < 0)
{
printk("i2c read error %d\n",ret);
return ret;
}
copy_to_user(buf, &ret, 1);
}
else
{
data = kmalloc(count, GFP_KERNEL);//申請count字節內存
ret = copy_from_user(data, buf, 1);//第1個字節是地址
if(ret < 0)
{
printk("write param num error \n");
kfree(data);
return -EINVAL;
}
address = data[0];
ret = i2c_smbus_read_i2c_block_data(ds2460_client,address,count,data);
if(ret < 0)
{
printk("i2c_smbus_read_i2c_block_data error %d\n",ret);
kfree(data);
return ret;
}
copy_to_user(buf, data, count);
kfree(data);
}
#else
/*申請內存*/
data = kmalloc(count,GFP_KERNEL);
ret = copy_from_user(data, buf, 1);
if(ret < 0)
{
printk("read param num error ret = %d ,buf = %d\n",ret,(int)buf[0]);
return -EINVAL;
}
/*用戶buf第一個字節是需要讀取的地址*/
address = data[0];
/*依次讀取數據*/
for(i=0;i<count;i++,address++)
{
/*非連續讀取 每次讀取產生一次完整的I2
C通信*/
data[i] = i2c_smbus_read_byte_data(ds2460_client,address);
if(data[i] < 0)
{
printk("i2c read error %d\n",data[i]);
return data[i];
}
}
/*將數據拷貝到用戶層buf*/
copy_to_user(buf, data, count);
/*釋放內存*/
kfree(data);
#endif
return 0;
}
ssize_t ds2460_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
unsigned char address;//需要寫入的地址
unsigned char *data = NULL;//需要寫入的數據
unsigned char i;
int ret;
/*至少寫入1個數據 即count至少等於2*/
if(count < 2)
{
printk("write param num error count = %d\n",count);
return -EINVAL;
}
#if (SEQUENT_RW == 1)
else if(count == 2)
{
data = kmalloc(2,GFP_KERNEL);
ret = copy_from_user(data, buf, 2);
if(ret < 0)
{
printk("write param num error \n");
kfree(data);
return -EINVAL;
}
address = data[0];
ret=i2c_smbus_write_byte_data(ds2460_client,address,data[1]);
if(ret < 0)
{
printk("i2c_smbus_write_byte_data error %d\n",ret);
kfree(data);
return ret;
}
kfree(data);
}
else
{
data = kmalloc(count,GFP_KERNEL);
ret = copy_from_user(data, buf, count);
if(ret < 0)
{
printk("write param num error %d\n",ret);
kfree(data);
return -EINVAL;
}
address = data[0];
ret=i2c_smbus_write_i2c_block_data(ds2460_client,address,count-1,&data[1]);
if(ret < 0)
{
printk("i2c_smbus_write_i2c_block_data error %d\n",ret);
kfree(data);
return ret;
}
kfree(data);
}
#else
/*申請內存*/
data = kmalloc(count,GFP_KERNEL);
/*拷貝count個字節到剛剛申請的內存中*/
ret = copy_from_user(data, buf, count);
if(ret < 0)
{
printk("write param num error %d\n",ret);
kfree(data);
return -EINVAL;
}
/*用戶buf第一個字節是需要寫入的地址*/
address = data[0];
/*依次寫入數據*/
for(i=1;i<=count-1;i++,address++)
{
/*非連續讀寫 每寫入1個字節產生一次完整的I2C通信*/
ret=i2c_smbus_write_byte_data(ds2460_client,address,data[i]);
if(ret < 0)
{
printk("i2c_smbus_write_byte_data error %d\n",ret);
kfree(data);
return ret;
}
/*E2PROM寫入需要延時10ms*/
mdelay(10);
}
/*釋放內存*/
kfree(data);
#endif
return 0;
}
static struct file_operations ds2460_fops =
{
.owner = THIS_MODULE,
.open = ds2460_open,
.release = ds2460_release,
.read = ds2460_read,
.write = ds2460_write,
};
static int __devinit ds2460_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret;
/*獲取當前操作的設備 */
ds2460_client = client;
/*申請設備號*/
ret = alloc_chrdev_region(&device_id, 0, 1, DEVICE_NAME);
if(ret < 0)
{
printk(KERN_ERR "alloc dev_id error %d \n", ret);
return ret;
}
/*分配一個cdev結構體*/
ds2460_cdev = cdev_alloc();
if(ds2460_cdev != NULL)
{
/*初始化cdev結構體*/
cdev_init(ds2460_cdev, &ds2460_fops);
/*向內核添加該cdev結構體*/
ret = cdev_add(ds2460_cdev, device_id, 1);
if(ret != 0)
{
printk("cdev add error %d \n",ret);
goto error;
}
}
else
{
printk("cdev_alloc error \n");
return -1;
}
/*創建一個class*/
ds2460_class = class_create(THIS_MODULE, "ds2460_class");
if(ds2460_class != NULL)
{
ds2460_device = device_create(ds2460_class, NULL, device_id, NULL, DEVICE_NAME);
}
else
{
printk("class_create error\n");
return -1;
}
return 0;
error:
cdev_del(ds2460_cdev);
unregister_chrdev_region(device_id, 1);
return -1;
}
static int __devexit ds2460_remove(struct i2c_client *client)
{
/*刪除cdev結構體*/
cdev_del(ds2460_cdev);
/*釋放設備號*/
unregister_chrdev_region(device_id, 1);
/*刪除設備*/
device_del(ds2460_device);
/*刪除類*/
class_destroy(ds2460_class);
return 0;
}
/*配置驅動的名稱和私有數據*/
static const struct i2c_device_id ds2460_id_table=
{
"ds2460",0//名稱爲“ds2460”需要和設備保持一致內核纔會調用驅動的probe函數
//0 表示沒有私有數據
};
/*創建i2c_driver結構體*/
static struct i2c_driver ds2460_driver =
{
.driver={
.name ="ds2460_driver",//這個名字無所謂
.owner=THIS_MODULE,
},
.probe = ds2460_probe,
.remove= __devexit_p(ds2460_remove),
.id_table = &ds2460_id_table,//這裏的名字纔是和設備進行匹配的
};
static int ds2460_drv_init(void)
{
/*向內核註冊驅動 如果和設備匹配 會執行probe函數*/
i2c_add_driver(&ds2460_driver);
printk("module init ok \n");
return 0;
}
static void ds2460_drv_exit(void)
{
/*向內核註銷驅動 如果和設備匹配 會執行remove函數*/
i2c_del_driver(&ds2460_driver);
printk("module exit ok \n");
}
module_init(ds2460_drv_init);
module_exit(ds2460_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");
最後,編寫測試函數:
DS2460是一個SHA加密芯片,但是它內部包含有一個112字節的E2PROM區域,其E2PROM區域的首地址是0x80,我們首先向這個位置寫入50個數據,再讀取50個數據,對比看下是否正確,最後再讀下芯片ID。
ds2460_test.c:
#include<stdio.h> /* using printf() */
#include<stdlib.h> /* using sleep() */
#include<fcntl.h> /* using file operation */
#include<sys/ioctl.h> /* using ioctl() */
#include <asm/ioctls.h>
#include <unistd.h> //sleep write read close
int main(int argc, const char * argv [ ])
{
int fd,i;
int value = 0;
unsigned char txbuf[51],rxbuf[50];
fd = open("/dev/ds2460_drv",O_RDWR);
if(fd < 0)
{
printf("open ds2460_drv error %d\n",fd);
return 0;
}
txbuf[0] = 0x80;//需要寫入的地址
for(i = 1;i<=50;i++)
{
txbuf[i] = i;//填充寫入的數據
}
printf("write:\n");
for(i=0;i<=50;i++)
{
printf("%d ",txbuf[i]);
}
printf("\n");//打印要寫入的數據
write(fd,txbuf,51);//寫入芯片0x80的位置
rxbuf[0] = 0x80;//讀取的地址
read(fd,rxbuf,50);//從0x80讀取50個字節
printf("read ds2460:\n");
for(i = 0; i < 50;i++)
{
printf("%d ",rxbuf[i]);//打印讀到的數據
}
printf("\n");
rxbuf[0] = 0xF0;//讀芯片ID
read(fd,rxbuf,8);
printf("ID:\n");
for(i = 0; i < 8;i++)
{
printf("%x ",rxbuf[i]);
}
printf("\n");
return 0;
}
編譯ds2460_dev.c,ds2460_drv.c,ds2460_test,得到三個文件:ds2460_dev.ko /ds2460_drv.ko /ds2460_test.
在開發板上加載前面兩個驅動模塊,再執行最後一個測試程序:
可以看到,驅動加載成功,寫入和讀取的數據也是一致的,芯片ID(低字節在前)爲:3C 53 7F 3e 0 0 0 39
我們編寫的I2C驅動沒有問題。
後記:
1.在linux系統下編寫I2C驅動,目前主要有兩種方法,一種是把I2C設備當作一個普通的字符設備來處理,另一種是利用linux下I2C驅動體系結構來完成。下面比較下這兩種方法:
第一種方法:
優點:思路比較直接,不需要花很多時間去了解linux中複雜的I2C子系統的操作方法。
缺點: 要求工程師不僅要對I2C設備的操作熟悉,而且要熟悉I2C的適配器(I2C控制器)操作。
要求工程師對I2C的設備器及I2C的設備操作方法都比較熟悉,最重要的是寫出的程序可以移植性差。
對內核的資源無法直接使用,因爲內核提供的所有I2C設備器以及設備驅動都是基於I2C子系統的格式。
第一種方法的優點就是第二種方法的缺點,
第一種方法的缺點就是第二種方法的優點。
2.E2PROM支持連續取,對於DS2460,發送一次起始信號最多可連續讀取8字節,但是本文沒有采用這一方式(ds2460_drv.c文件 中有宏定義開關可以打開),本文采用的是最原始的方式,即每讀寫一個字節產生一次完整的I2C通信,這種方式會大大影響速度。
3.linux內核源碼/linux-2.6.35.3/drivers/i2c中有I2C相關代碼
busses/i2c-mxs.c:總線驅動文件即I2C適配器的驅動文件,包含I2C基本通信函數。
i2c-core.c:I2C核心文件,主要提供API,與硬件無關。
i2c-dev.c:通用設備驅動文件,它不針對某一款I2C芯片,它提供通用的方式讓用戶操作I2C設備,同時也是一個字符設備驅動。
用戶直接open這個設備驅動就是上面講的把I2C設備當作一個普通的字符設備來處理,I2C所有通信細節都需要用戶自己完成。
下面推薦幾篇寫的比較好的Linux I2C驅動框架文章: