最全的IIC介绍及其设备驱动编写

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了最全的IIC介绍及其设备驱动编写相关的知识,希望对你有一定的参考价值。


参考资料:

​Linux2.6之IIC驱动_月月鸟呀的博客​

​Linux3.4.2之IIC驱动_月月鸟呀的博客​

1、IIC介绍

IIC是通信协议中的一种,为一主多从的结构,对于主从,所有的数据都是从主机这边发起,从机只能接受,不能主动引起数据传输,只有两条总线线路:一条串行数据线(SDA),一条串行时钟线(SCL),IIC有硬件IIC和软件IIC,这里简单解释,硬件IIC为硬件构成的IIC,一般只需要操作相关寄存器即可,对于软件IIC,可以由IO口来模拟IIC总线进行对IIC设备的通信。

对于多个IIC设备来说,每个连接到总线的器件都可以使用软件根据它的唯一地址来识别,当我们要使用一个IIC设备时,主机通过SDA线向各IIC设备寻址,等待地址对应的IIC设备的回应即ACK信号。

IIC是半双工的,即SDA总线是双向传输的。两条线在硬件上都使用了开极电路,且都接有上拉电阻(即空闲时,SDA和SCL都为高电平)。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCKvuXrc-1628932720637)(C:\\Users\\liang\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210812203900555.png)]

  • 传输过程:

当主机A要发送数据时,A拉低SDA总线,稍后向SCL输送时钟信号,开始传输数据(在每个时钟周期中,当SCL为高电平时,从机从SDA线上采样(获得)数据;当SCL为低电平时,主机向SDA传送(更新)数据)。主机每传完8个比特位,就将SDA释放(SDA恢复到高电平),若从机正常接收完8个比特位,就将SDA线拉低,以表示向主机回送一个ACK信号,若没有接收到8个比特位,就不会拉低SDA线,主机就收不到ACK信号,所以主机会发送一个终止或开始(重传)信号给从机。当主机已经完成数据传输时,会先释放SCL线,然后再释放SDA线。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3XsSO80t-1628932720640)(https://i.loli.net/2021/08/12/ieORwNA9xzGIbyW.png)]

并非每传输8位数据之后,都会有ACK信号,有以下3种例外:

  1. 当从机不能响应从机地址时(例如它正忙于其他事而无法响应I²总线的操作,或者这个地址没有对应的从机),在第9个SCL周期内SDA线没有被拉低,即没有ACK信号。这时,主机发出一个Р信号终止传输或者重新发出一个S信号开始新的传输.
  2. 如果从机接收器在传输过程中不能接收更多的数据时,它也不会发出ACK信号。这样,主机就可以意识到这点,从而发出一个Р信号终止传输或者重新发出一个S信号开始新的传输.
  3. 主机接收器在接收到最后一个字节后,也不会发出ACK信号。于是,从机发送器释放SDA线,以允许主机发出Р信号结束传输
  • 使用IIC协议的芯片介绍—AT24C02/04/08/16

AT24Cxx系列芯片是采用IIC协议的EEPROM芯片,其读写过程如下:

写过程:主机先向芯片发出设备地址,再发出写地址后就可以开始传输数据;

读过程:主机先向芯片发出设备地址,再发出读地址,然后需要再发出一次设备地址,才能读数据,是因为AT24Cxx容量各有不同,有1K,2K,4K,8K,16K等大小,对于4K芯片来说其架构为512*8bit,而512为2的9次方,因此只传输一次读地址是不够的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fUFMLsrP-1628932720642)(https://i.loli.net/2021/08/12/3aHObDrZy2GxMLC.png)]

2、IIC驱动框架

I²C驱动框架:

App:open()、 read() 、write()

IIC设备驱动程序:drv_open()、 drv_read() 、drv_write() 知道数据含义

IIC总线驱动程序:1.识别设备; 2.提供读写函数 。 知道怎么收发数据

IIC硬件:如AT24C02/AT24C08

  • 这三层通过总线设备驱动模型联系在一块,在内核中有很多虚拟的总线例如platfoem_bus_type,对于IIC来说是i2c_bus_type总线,在这条总线中有i2c_clienti2c_driver两条链表,当i2c_client加到链表中后,会先跟i2c_driver链表的每一项比较,如果能匹配的话就调用相对应的i2c_driver的probe函数;对于i2c_driver加到链表中后,一样会先跟i2c_client链表的每一项比较,如果能匹配的话就调用i2c_driver的probe函数。匹配是调用i2c_bus_type总线中的match函数进行比较,其中比较的是id_table,而在id_table中比较的是两者链表中的name,因此简单描述如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-49sBhfqD-1628932720646)(C:\\Users\\liang\\AppData\\Roaming\\Typora\\typora-user-images\\image-20210813194017090.png)]

  1. 左边注册一个设备:i2c_client
  2. 右边注册一个驱动:i2c_driver
  3. 比较他们的名字,如果相同,则调用probe函数
  4. probe函数里,可以注册字符设备驱动

在i2c_bus_type总线中,dev链表不仅有i2c_client还有i2c_adapter,后面再解释。

  • 对于裸板程序来说,可以直接对寄存器进行映射使用,但在内核中有完善的IIC总线驱动程序,会帮我们去发出起始信号,识别IIC设备参考i2c-s3c2410.c,在入口函数中注册了平台总线,在probe函数中设置adap并注册添加了adap(IIC适配器:i2c_add_adapter>i2c_register_adapter),在设置adap中的algo(算法)设置s3c24xx_i2c_xfer函数,这个函数实现了识别IIC设备的功能(发S信号,设备地址,等待应答)
  • IIC总线驱动程序在内核源码中drivers\\i2c\\busses中参考i2c-s3c2410.c,在入口函数中注册了平台总线,在probe函数中设置adap并注册添加了adap(IIC适配器:i2c_add_adapter>i2c_register_adapter),在设置adap中的algo(算法)设置s3c24xx_i2c_xfer函数,这个函数实现了识别IIC设备的功能(发S信号,设备地址,等待应答)
static const struct i2c_algorithm s3c24xx_i2c_algorithm = 
.master_xfer = s3c24xx_i2c_xfer,
.functionality = s3c24xx_i2c_func,
;

static struct s3c24xx_i2c s3c24xx_i2c =
.lock = __SPIN_LOCK_UNLOCKED(s3c24xx_i2c.lock),
.wait = __WAIT_QUEUE_HEAD_INITIALIZER(s3c24xx_i2c.wait),
.tx_setup = 50,
.adap =
.name = "s3c2410-i2c",
.owner = THIS_MODULE,
.algo = &s3c24xx_i2c_algorithm,
.retries = 2,
.class = I2C_CLASS_HWMON,
,
;
...

static int __init i2c_adap_s3c_init(void)

int ret;

ret = platform_driver_register(&s3c2410_i2c_driver);
if (ret == 0)
ret = platform_driver_register(&s3c2440_i2c_driver);
if (ret)
platform_driver_unregister(&s3c2410_i2c_driver);


return ret;


...

static int s3c24xx_i2c_probe(struct platform_device *pdev)

...

ret = s3c24xx_i2c_init(i2c);
if (ret != 0)
goto err_iomap;

/* find the IRQ for this unit (note, this relies on the init call to
* ensure no current IRQs pending
*/

res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
if (res == NULL)
dev_err(&pdev->dev, "cannot find IRQ\\n");
ret = -ENOENT;
goto err_iomap;


ret = request_irq(res->start, s3c24xx_i2c_irq, IRQF_DISABLED,
pdev->name, i2c);

if (ret != 0)
dev_err(&pdev->dev, "cannot claim IRQ\\n");
goto err_iomap;


i2c->irq = res;

dev_dbg(&pdev->dev, "irq resource %p (%lu)\\n", res,
(unsigned long)res->start);

ret = i2c_add_adapter(&i2c->adap);
...


...
static int __init i2c_adap_s3c_init(void)

int ret;

ret = platform_driver_register(&s3c2410_i2c_driver);
if (ret == 0)
ret = platform_driver_register(&s3c2440_i2c_driver);
if (ret)
platform_driver_unregister(&s3c2410_i2c_driver);


return ret;
  • 参考IIC设备驱动\\drivers\\i2c\\chips\\eeprom.c,在入口函数中i2c_add_driver添加driver设备,attach_adapter会加到适配器中去,在i2c_add_driver过程中总线为i2c_bus_type,会先把i2c_driver放入总线的dri链表,从adap链表中取出"适配器"并调用attach_adapter,而attach_adapter会调用i2c_probe,其中会使用master_xfer函数发S信号,发设备地址,如果能收到ACK信号,则会调用i2c_probe参数中的function函数,这其中我们可以注册字符设备驱动来完善IIC设备驱动程序,对于总线驱动程序中i2c_add_adapter函数会把adap(适配器)放入链表,一样会调用dri的attach_adapter然后调用master_xfer函数。
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)

int res;

...
if (driver->attach_adapter)
struct i2c_adapter *adapter;

list_for_each_entry(adapter, &adapters, list)
driver->attach_adapter(adapter);



mutex_unlock(&core_lists);
return 0;


static int eeprom_attach_adapter(struct i2c_adapter *adapter)

return i2c_probe(adapter, &addr_data, eeprom_detect);


static struct i2c_driver eeprom_driver =
.driver =
.name = "eeprom",
,
.id = I2C_DRIVERID_EEPROM,
.attach_adapter = eeprom_attach_adapter,
.detach_client = eeprom_detach_client,
;
...
static int __init eeprom_init(void)

return i2c_add_driver(&eeprom_driver);

i2c_probe函数过程如下

  • i2c_probe(adapter, &addr_data, eeprom_detect);
  • i2c_probe_address // 发出S信号,发出设备地址(来自addr_data)
  • i2c_smbus_xfer
  • i2c_smbus_xfer_emulated
  • i2c_transfer
  • adap->algo->master_xfer // s3c24xx_i2c_xfer
  • i2c_add_driver:
  • 1.把i2c_driver放入链表;
  • 2.从adap链表里取出适配器,调用drv的attch_adapter函数;
  • 3.调用i2c_probe(adapter, &addr_data, function),master_xfer函数用adapter参数发信号,确定有无设备,有则调用function;
  • 4.构造i2c_client结构体(.address .adapter(指向左边的adap) .driver(指向右边的dri) )
  • i2c_add_adapter:
  • 1.把adap适配器放入链表;
  • 2.调用drv的attch_adapter函数;
  • 3.同上

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sU9Thfsj-1628932720648)(https://i.loli.net/2021/08/12/bEoTn5YNORfzluM.png)]

3、自己编写IIC驱动程序(Linux2.6)

## 3.1基本框架

以I2C设备(EEPROM芯片)AT24CXX为例,编写其驱动程序的基本步骤如下:

  • 1 分配一个i2c_driver结构体
  • 2 设置
  • attach_adapter // 它直接调用 i2c_probe(adap, 设备地址, 发现这个设备后要调用的函数);
  • detach_client // 卸载这个驱动后,如果之前发现能够支持的设备,则调用它来清理
  • 3 注册:i2c_add_driver

参考设备m41t00.c中设备地址怎么写(由AT24CXX的数据手册查得其地址为0xA0),在normal_i2c参数中地址值只需要7位,若识别设备先加入打印语句测试:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/i2c.h>
#include <linux/mutex.h>

static unsigned short ignore[] = I2C_CLIENT_END ;
static unsigned short normal_addr[] = 0x50, I2C_CLIENT_END ; /* 地址值是7位,取0xA0的高7位 */

static struct i2c_client_address_data addr_data =
.normal_i2c = normal_addr, /* 要发出S信号和设备地址并得到ACK信号,才能确定存在这个设备 */
.probe = ignore,
.ignore = ignore,
;

static int at24cxx_detect(struct i2c_adapter *adapter, int address, int kind)

printk("at24cxx_detected.\\n");
return 0;


static int at24cxx_attach(struct i2c_adapter *adapter)

return i2c_probe(adapter, &addr_data, at24cxx_detect);


static int at24cxx_detach(struct i2c_client *client)

printk("at24cxx_detached.\\n");
return 0;



/* 1. 分配一个i2c_driver结构体 */
/* 2. 设置i2c_driver结构体 */
static struct i2c_driver at24cxx_driver =
.driver =
.name = "at24cxx",
,
.attach_adapter = at24cxx_attach,
.detach_client = at24cxx_detach,
;

static int at24cxx_init(void)

i2c_add_driver(&at24cxx_driver);
return 0;


static void at24cxx_exit(void)

i2c_del_driver(&at24cxx_driver);


module_init(at24cxx_init);
module_exit(at24cxx_exit);
MODULE_LICENSE("GPL");

编译加载驱动,若出现以下打印语句,则表示框架正确!

# insmod at24cxx.ko

at24cxx_detected.

3.2 强制发现IIC设备

当设备还未发现后面才发现,我们可以强制发现设备,添加forces属性,参考i2c_probe函数,ANY_I2C_BUS为任何i2c总线,0x60是没有设备的地址,修改addr_data:

static unsigned short force_addr[] = ANY_I2C_BUS, 0x60, I2C_CLIENT_END;
static unsigned short * forces[] = force_addr, NULL;

static struct i2c_client_address_data addr_data =
.normal_i2c = ignore, /* 要发出S信号和设备地址并得到ACK信号,才能确定存在这个设备 */
.probe = ignore,
.ignore = ignore,
.forces = forces, /* 强制认为存在这个设备 */
;

编译测试,得到一样的情况:

# insmod at24cxx.ko
at24cxx_detected.

# rmmod at24cxx

3.3 添加client(设备)

上面在卸载IIC驱动的时候,没有打印出”at24cxx_detached.”。参考eeprom.c得知,需要发现设备后,在detect函数中分配/设置/注册client结构体,收发数据时会用到。

所以现在修改调用函数at24cxx_detect,并且在at24cxx_detach中卸载client,设备地址重新改回0x50:

static unsigned short ignore[]      =  I2C_CLIENT_END ;
static unsigned short normal_addr[] = 0x50, I2C_CLIENT_END ;
static struct i2c_driver at24cxx_driver; //声明一下

static struct i2c_client_address_data addr_data =
.normal_i2c = normal_addr, /* 要发出S信号和设备地址并得到ACK信号,才能确定存在这个设备 */
.probe = ignore,
.ignore = ignore,
;

static int at24cxx_detect(struct i2c_adapter *adapter, int address, int kind)

struct i2c_client *new_client;

printk("at24cxx_detected.\\n");

/* 构造一个i2c_client结构体: 以后收发数据时会用到它 */
new_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
new_client->addr = address;
new_client->adapter = adapter;
new_client->driver = &at24cxx_driver;
strcpy(new_client->name, "at24cxx");

i2c_attach_client(new_client);
return 0;


static int at24cxx_detach(struct i2c_client *client)

printk("at24cxx_detached.\\n");
i2c_detach_client(client);
kfree(i2c_get_clientdata(client));

return 0;

编译测试,卸载驱动打印出信息

# insmod at24cxx.ko
at24cxx_detect
# rmmod at24cxx
at24cxx_detach

3.4 完善驱动程序

  • 在调用函数at24cxx_detect()中注册字符设备,修改at24cxx_detect函数,在at24cxx_detach函数中卸载,并添加相应头文件:
#include <linux/fs.h>

static int major;

static ssize_t at24cxx_read(struct file *file, char __user *buf, size_t size, loff_t * offset)

return 0;


static ssize_t at24cxx_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)

return 0;



static struct file_operations at24cxx_fops =
.owner = THIS_MODULE,
.read = at24cxx_read,
.write = at24cxx_write,
;

static struct class *cls;
struct i2c_client *at24cxx_client; //修改为全局变量

static int at24cxx_detect(struct i2c_adapter *adapter, int address, int kind)

printk("at24cxx_detected.\\n");

/* 构构一个i2c_client结构体: 以后收改数据时会用到它 */
at24cxx_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
at24cxx_client->addr = address;
at24cxx_client->adapter = adapter;
at24cxx_client->driver = &at24cxx_driver;
strcpy(at24cxx_client->name, "at24cxx");
i2c_attach_client(at24cxx_client);

major = register_chrdev(0, "at24cxx", &at24cxx_fops);
cls = class_create(THIS_MODULE, "at24cxx");
class_device_create(cls, NULL, MKDEV(major, 0), NULL, "at24cxx"); /* /dev/at24cxx */
return 0;


static int at24cxx_detach(struct i2c_client *client)

printk("at24cxx_detached.\\n");
class_device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, "at24cxx");

i2c_detach_client(client);
kfree(i2c_get_clientdata(client));

return 0;
  • 利用i2c_transfer进行传输,i2c_transfer参数一位client中的适配器,以i2c_msg结构体传输,在i2c_msg结构体中含有数据传输三要素,其中addr为设备地址,完整代码如下:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/jiffies.h>
#include <linux/i2c.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

static unsigned short ignore[] = I2C_CLIENT_END ;
static unsigned short normal_addr[] = 0x50, I2C_CLIENT_END ; /* 地址值是7位 */
/* 改为0x60的话, 由于不存在设备地址为0x60的设备, 所以at24cxx_detect不被调用 */

//static unsigned short force_addr[] = ANY_I2C_BUS, 0x60, I2C_CLIENT_END;
//static unsigned short * forces[] = force_addr, NULL;

static struct i2c_client_address_data addr_data =
.normal_i2c = normal_addr, /* 要发出S信号和设备地址并得到ACK信号,才能确定存在这个设备 */
.probe = ignore,
.ignore = ignore,
//.forces = forces, /* 强制认为存在这个设备 */
;

static struct i2c_driver at24cxx_driver;

static int major;
static struct class *cls;
struct i2c_client *at24cxx_client;

static ssize_t at24cxx_read(struct file *file, char __user *buf, size_t size, loff_t * offset)

unsigned char address;
unsigned char data;
struct i2c_msg msg[2];
int ret;

/* address = buf[0]
* data = buf[1]
*/
if (size != 1)
return -EINVAL;

copy_from_user(&address, buf, 1);

/* 数据传输三要素: 源,目的,长度 */

/* 读AT24CXX时,要先把要读的存储空间的地址发给它 */
msg[0].addr = at24cxx_client->addr; /* 目的 */
msg[0].buf = &address; /* 源 */
msg[0].len = 1; /* 地址=1 byte */
msg[0].flags = 0; /* 表示写 */

/* 然后启动读操作 */
msg[1].addr = at24cxx_client->addr; /* 源 */
msg[1].buf = &data; /* 目的 */
msg[1].len = 1; /* 数据=1 byte */
msg[1].flags = I2C_M_RD; /* 表示读 */


ret = i2c_transfer(at24cxx_client->adapter, msg, 2);/* 2代表2个数组项 */
if (ret == 2)

copy_to_user(buf, &data, 1);
return 1;

else
return -EIO;


static ssize_t at24cxx_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)

unsigned char val[2];
struct i2c_msg msg[1];
int ret;

/* address = buf[0]
* data = buf[1]
*/
if (size != 2)
return -EINVAL;

copy_from_user(val, buf, 2);

/* 数据传输三要素: 源,目的,长度 */
msg[0].addr = at24cxx_client->addr; /* 目的(从设备地址) */
msg[0].buf = val; /* 源 */
msg[0].len = 2; /* 地址+数据=2 byte */
msg[0].flags = 0; /* 表示写 */

ret = i2c_transfer(at24cxx_client->adapter, msg, 1);/* 1代表一个数组项 */
if (ret == 1) /* msg一项,返回1表示成功 */
return 2;
else
return -EIO;



static struct file_operations at24cxx_fops =
.owner = THIS_MODULE,
.read = at24cxx_read,
.write = at24cxx_write,
;

static int at24cxx_detect(struct i2c_adapter *adapter, int address, int kind)

printk("at24cxx_detect\\n");

/* 构构一个i2c_client结构体: 以后收改数据时会用到它 */
at24cxx_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);
at24cxx_client->addr = address;
at24cxx_client->adapter = adapter;
at24cxx_client->driver = &at24cxx_driver;
strcpy(at24cxx_client->name, "at24cxx");
i2c_attach_client(at24cxx_client);

major = register_chrdev(0, "at24cxx", &at24cxx_fops);

cls = class_create(THIS_MODULE, "at24cxx");
class_device_create(cls, NULL, MKDEV(major, 0), NULL, "at24cxx"); /* /dev/at24cxx */

return 0;


static int at24cxx_attach(struct i2c_adapter *adapter)

return i2c_probe(adapter, &addr_data, at24cxx_detect);


static int at24cxx_detach(struct i2c_client *client)

printk("at24cxx_detach\\n");
class_device_destroy(cls, MKDEV(major, 0));
class_destroy(cls);
unregister_chrdev(major, "at24cxx");

i2c_detach_client(client);
kfree(i2c_get_clientdata(client));

return 0;



/* 1. 分配一个i2c_driver结构体 */
/* 2. 设置i2c_driver结构体 */
static struct i2c_driver at24cxx_driver =
.driver =
.name = "at24cxx",
,
.attach_adapter = at24cxx_attach,
.detach_client = at24cxx_detach,
;

static int at24cxx_init(void)

i2c_add_driver(&at24cxx_driver);
return 0;


static void at24cxx_exit(void)

i2c_del_driver(&at24cxx_driver);


module_init(at24cxx_init);
module_exit(at24cxx_exit);
MODULE_LICENSE("GPL");

3.5 测试程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>


/* i2c_test r addr
* i2c_test w addr val
*/

void print_usage(char *file)

printf("%s r addr\\n", file);
printf("%s w addr val\\n", file);


int main(int argc, char **argv)

int fd;
unsigned char buf[2];

if ((argc != 3) && (argc != 4))

print_usage(argv[0]);
return -1;


fd = open("/dev/at24cxx", O_RDWR);
if (fd < 0)

printf("cant open /dev/at24cxx\\n");
return -1;


if (strcmp(argv[1], "r") == 0)

buf[0] = strtoul(argv[2], NULL, 0); //把字符串转化为整型
read(fd, buf, 1);
printf("data: %c, %d, 0x%2x\\n", buf[0], buf[0], buf[0]);

else if (strcmp(argv[1], "w") == 0)

buf[0] = strtoul(argv[2], NULL, 0);
buf[1] = strtoul(argv[3], NULL, 0);
write(fd, buf, 2);

else

print_usage(argv[0]);
return -1;


return 0;

4、编写IIC驱动(Linux3.4.2)

Linux下IIC驱动编写,介绍IIC子系统框架的使用

Exynos4412 IIC总线驱动开发—— IIC 基础概念及驱动架构分析

linux中iic驱动编写—有设备树&没有设备树

IIC总线硬件工作原理(待完善)

IIC协议详解+软件模拟IIC

Linux驱动开发-编写PCF8591(ADC)芯片驱动