(基于4.14内核版本)
为了梳理清楚linux内核中的i2c实现框架,从本文开始,博主将分几个章节分别解析i2c总线在linux内核中的造成过程、匹配过程、以及设备驱动程序源码实现。node
在介绍linux内核中i2c框架以前,咱们最好是知道怎么使用它,实现一个相应的i2c设备驱动程序demo,而后从使用去深挖背后的实现原理,先知道怎么用,而后再知道为何能够这么用。linux
回到本文的重点——I2C,作过裸板开发或者是单片机开发的朋友确定对I2C不陌生,I2C是主从结构,主器件使用从机地址进行寻址,它的拓扑结构是这样的:
(图片来自网络,若有侵权,请联系我及时删除)数组
基本的流程是这样的:网络
I2C设备驱动程序框架分为两个部分:driver和device。框架
分别将driver和device加载到内存中,i2c bus在程序加载时会自动调用match函数,根据名称来匹配driver和device,匹配完成时调用probe()dom
在driver中,定义probe()函数,在probe函数中建立设备节点,针对不一样的设备实现不一样的功能。函数
在device中,设置设备I2C地址,选择I2C适配器。测试
I2C适配器:I2C的底层传输功能,通常指硬件I2C控制器。ui
直接来看下面的示例代码:
i2c_bus_driver.c:code
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/i2c.h> #include <linux/err.h> #include <linux/slab.h> #include <linux/uaccess.h> /* 结构体数组 结构体第一个参数为名称,第二个参数为private数据*/ static const struct i2c_device_id downey_drv_id_table[] = { {"downey_i2c",0}, {}, }; static int major; static struct class *i2c_test_cls; static struct device *i2c_test_dev; static const char* CLASS_NAME = "I2C_TEST_CLASS"; static const char* DEVICE_NAME = "I2C_TEST_DEVICE"; static struct i2c_client *downey_client; static int i2c_test_open(struct inode *node, struct file *file) { printk(KERN_ALERT "i2c init \n"); return 0; } static ssize_t i2c_test_read(struct file *file,char *buf, size_t len,loff_t *offset) { int cnt = 0; uint8_t reg = 0; uint8_t val = 0; copy_from_user(®,buf,1); /*i2c读byte,经过这个函数能够从设备中指定地址读取数据*/ val = i2c_smbus_read_byte_data(downey_client,reg); cnt = copy_to_user(&buf[1],&val,1); return 1; } static ssize_t i2c_test_write(struct file *file,const char *buf,size_t len,loff_t *offset) { uint8_t recv_msg[255] = {0}; uint8_t reg = 0; int cnt = 0; cnt = copy_from_user(recv_msg,buf,len); reg = recv_msg[0]; printk(KERN_INFO "recv data = %x.%x\n",recv_msg[0],recv_msg[1]); /*i2c写byte,经过这个函数能够往设备中指定地址写数据*/ if(i2c_smbus_write_byte_data(downey_client,reg,recv_msg[1]) < 0){ printk(KERN_ALERT " write failed!!!\n"); return -EIO; } return len; } static int i2c_test_release(struct inode *node,struct file *file) { printk(KERN_INFO "Release!!\n"); return 0; } static struct file_operations file_oprts = { .open = i2c_test_open, .read = i2c_test_read, .write = i2c_test_write, .release = i2c_test_release, }; /*当i2c bus检测到匹配的device - driver,调用probe()函数,在probe函数中,申请设备号,建立设备节点,绑定相应的file operation结构体。*/ static int downey_drv_probe(struct i2c_client *client, const struct i2c_device_id *id) { /*保存参数client,在i2c读写操做时须要用到这个参数,其中保存了适配器、设备地址等信息*/ printk(KERN_ALERT "addr = %x\n",client->addr); downey_client = client; major = register_chrdev(0,DEVICE_NAME,&file_oprts); if(major < 0 ){ printk(KERN_ALERT "Register failed!!\r\n"); return major; } printk(KERN_ALERT "Registe success,major number is %d\r\n",major); /*以CLASS_NAME建立一个class结构,这个动做将会在/sys/class目录建立一个名为CLASS_NAME的目录*/ i2c_test_cls = class_create(THIS_MODULE,CLASS_NAME); if(IS_ERR(i2c_test_cls)) { unregister_chrdev(major,DEVICE_NAME); return PTR_ERR(i2c_test_cls); } /*以DEVICE_NAME为名,参考/sys/class/CLASS_NAME在/dev目录下建立一个设备:/dev/DEVICE_NAME*/ i2c_test_dev = device_create(i2c_test_cls,NULL,MKDEV(major,0),NULL,DEVICE_NAME); if(IS_ERR(i2c_test_dev)) { class_destroy(i2c_test_cls); unregister_chrdev(major,DEVICE_NAME); return PTR_ERR(i2c_test_dev); } printk(KERN_ALERT "i2c_test device init success!!\r\n"); return 0; } /*Remove :当匹配关系不存在时(device或是driver被卸载),调用remove函数,remove函数是probe函数的反操做,将probe函数中申请的资源所有释放。*/ static int downey_drv_remove(struct i2c_client *client) { printk(KERN_ALERT "remove!!!\n"); device_destroy(i2c_test_cls,MKDEV(major,0)); class_unregister(i2c_test_cls); class_destroy(i2c_test_cls); unregister_chrdev(major,DEVICE_NAME); return 0; } static struct i2c_driver downey_drv = { /*.driver中的name元素仅仅是一个标识,并不做为bus匹配的name识别*/ .driver = { .name = "random", .owner = THIS_MODULE, }, .probe = downey_drv_probe, .remove = downey_drv_remove, /*.id_table中存储driver名称,做为bus匹配时的识别*/ .id_table = downey_drv_id_table, // .address_list = downey_i2c, }; int drv_init(void) { int ret = 0; printk(KERN_ALERT "init!!!\n"); ret = i2c_add_driver(&downey_drv); if(ret){ printk(KERN_ALERT "add driver failed!!!\n"); return -ENODEV; } return 0; } void drv_exit(void) { i2c_del_driver(&downey_drv); return ; } MODULE_LICENSE("GPL"); module_init(drv_init); module_exit(drv_exit);
i2c_bus_driver.c:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/i2c.h> #include <linux/err.h> #include <linux/slab.h> #include <linux/regmap.h> // #include <linux/paltform_device.h> static struct i2c_adapter *adap; static struct i2c_client *client; #define I2C_DEVICE_ADDR 0x68 /**指定i2c device的信息 * downey_i2c 是device中的name元素,当这个模块被加载时,i2c总线将使用这个名称匹配相应的drv。 * I2C_DEVICE_ADDR 为设备的i2c地址 * */ static struct i2c_board_info downey_board = { I2C_BOARD_INFO("downey_i2c",I2C_DEVICE_ADDR), }; int dev_init(void) { /*获取i2c适配器,适配器通常指板上I2C控制器,实现i2c底层协议的字节收发,特殊状况下,用普通gpio模拟I2C也可做为适配器*/ adap = i2c_get_adapter(2); if(IS_ERR(adap)){ printk(KERN_ALERT "I2c_get_adapter failed!!!\n"); return -ENODEV; } /*建立一个I2C device,并注册到i2c bus的device链表中*/ client = i2c_new_device(adap,&downey_board); /*使能相应适配器*/ i2c_put_adapter(adap); if(!client){ printk(KERN_ALERT "Get new device failed!!!\n"); return -ENODEV; } return 0; } void dev_exit(void) { i2c_unregister_device(client); return ; } MODULE_LICENSE("GPL"); module_init(dev_init); module_exit(dev_exit);
driver和device做为两个独立的模块,须要分别编译,分别生成i2c_bus_driver.ko和i2c_bus_device.ko(编译过程我就再也不啰嗦了)。
而后加载driver:
sudo insmod i2c_bus_driver.ko
log信息
Dec 31 07:21:49 beaglebone kernel: [13114.715050] init!!!
加载device:
sudo insmod i2c_bus_device.ko
log信息:
Dec 31 07:21:49 beaglebone kernel: [13114.717420] addr = 68 Dec 31 07:21:49 beaglebone kernel: [13114.726671] Registe success,major number is 241 Dec 31 07:21:49 beaglebone kernel: [13114.739575] i2c_test device init success!!
查看log能够发现,当加载完i2c_bus_device.ko时,driver中probe函数被调用,打印出设备地址,注册的设备号,表示注册成功。接下来就是写一个用户程序来测试驱动。
#include <stdio.h> #include <stdlib.h> #include <error.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <stdint.h> static char buf[256] = {1}; int main(int argc,char *argv[]) { int ret = 0; uint8_t buf[2] = {0}; char cmd[6] = {0}; int reg_addr = 0; int value = 0; int fd = open("/dev/I2C_TEST_DEVICE",O_RDWR); if(fd < 0) { perror("Open file failed!!!\r\n"); } while(1){ /*for example : write 0x00 0x08*/ /*The val should be 0 when the cmd is read.*/ printf("Enter your cmd:<read/write> <reg_addr> <val> : \n"); scanf("%s",cmd); scanf("%x",®_addr); scanf("%x",&value); printf("%s :%x :%x\n",cmd,reg_addr,value); if(0 == memcmp(cmd,"write",5)){ buf[0] = reg_addr; buf[1] = value; int ret = write(fd,buf,2); if(ret < 0){ perror("Failed to write!!\n"); }else{ printf("Write value %x to reg addr %x success\n",value,reg_addr); } } else if(0 == memcmp(cmd,"read",4)){ buf[0] = reg_addr; ret = read(fd,buf,1); if(ret < 0){ perror("Read failed!!\n"); }else{ printf("Read %x from addr %x\n",buf[1],reg_addr); } } else{ printf("Unsupport cmd\n"); } memset(cmd,0,sizeof(cmd)); } close(fd); return 0; }
用户程序实现从终端读取用户指令,而后读写传感器的寄存器,代码都通过测试,本身试试吧!
在上述i2c的device建立中,咱们使用了i2c_new_device()接口,值得一提的是,这个接口并不会检测设备是否存在,只要对应的device-driver存在,就会调用driver中的probe函数。
可是有时候会有这样的需求:在匹配时须要先检测设备是否插入,若是没有i2c设备链接在主板上,就拒绝模块的加载,这样能够有效地管理i2c设备的资源,避免无关设备占用i2c资源。
新的建立方式接口为:
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))
这个函数添加了在匹配模块时的检测功能:
为了一探究竟,咱们来看看i2c_new_probed_device的源代码实现:
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)) { int i; /*若是传入probe为NULL,则使用默认probe函数*/ if (!probe) probe = i2c_default_probe; /*轮询传入的addr_list,检测指定地址列表中地址是否合法*/ for (i = 0; addr_list[i] != I2C_CLIENT_END; i++) { /* Check address validity */ if (i2c_check_7bit_addr_validity_strict(addr_list[i]) < 0) { dev_warn(&adap->dev, "Invalid 7-bit address 0x%02x\n", addr_list[i]); continue; } /*检测地址是否被占用*/ /* Check address availability (7 bit, no need to encode flags) */ if (i2c_check_addr_busy(adap, addr_list[i])) { dev_dbg(&adap->dev, "Address 0x%02x already in use, not probing\n", addr_list[i]); continue; } /*检测对应地址上设备是否正常运行*/ /* Test address responsiveness */ if (probe(adap, addr_list[i])) break; } /*检测不到对应地址的设备,或对应设备正在被占用*/ if (addr_list[i] == I2C_CLIENT_END) { dev_dbg(&adap->dev, "Probing failed, no device found\n"); return NULL; } /*检测到可用地址,将其赋值给board info结构体*/ info->addr = addr_list[i]; return i2c_new_device(adap, info); }
根据源码中的显示,i2c_new_probed_device主要是执行了这样的操做:
看到这里就一目了然了,一切细节在源码面前都无处可藏。其实就在对相应地址作一次检测而已,到最后仍是调用i2c_new_device。
不过不知道你有没有发现,i2c_new_device传入的参数2的addr部分被忽略了,因此board info中的地址实际上是可有可无的,由于函数会在addr list中查找可用的地址而后赋值给board info的addr元素,而本来的addr被覆盖。因此,若是你在写内核代码时感到疑惑,看源码就行了!
好了,关于linux驱动中i2c驱动的讨论就到此为止啦,若是朋友们对于这个有什么疑问或者发现有文章中有什么错误,欢迎留言
原创博客,转载请注明出处!
祝各位早日实现项目丛中过,bug不沾身.