Linux下的I2C通信
I2C通信:
一.硬件初识:
IIC(inter-intergrated-Circu):内部集成总线
四线通讯:SCL,SDA,GND,VCC,串行,半双工
-
I2C 总线是同步,串行,半双工通信总线。
-
I2C 总线由时钟线 SDA 和 SCL两根信号线构成。并且都有上拉电阻。确保总线空闲状态为高电平。
-
I2C 总线支持多设备连接,允许多主机存在,但同一时刻只允许一台主机。
-
每一个I2C 外设都会对应一个唯一的地址(这个地址可以从 I2C 外设器件的数据手册中得知),主机和从机之间的通信就是通过这个地址来确定主机要和哪个从机进行通信的。
-
I2C 总线上挂载的设备的数量受总线最大电容 400pF 限制。如果挂的是相同型号的器件还受到器件的地址限制。
-
I2C 总线在标准模式下传输速率可达 100kbit/s,在快速模式下可达 400kbit/s,在高速模
式下可达3.4Mbit/s。
-
I2C 总线上的主机和从机之间以字节(8位)为单位进行数据传输。
-
I2C 有硬件12C 和软件 12C。
通讯时序:
高位先传输
I2C子系统框架:
驱动开发,只需实现设备驱动层的Client,和Driver即可:向内核中添加一个描述i2c外设资源的device部分,在driver中调用核心层的API实现I2C的通讯驱动
二.1. I2C_client部分:
用于描述一个I2c外设的资源,地址,GPIO,中断信息等等…表示通信的对象
向内核中添加一个client的方法可以是设备树或者使用c程序添加
设备树:
//挂载在i2c1设备控制器下 &i2c1{ //状态 status = "okay"; //标签@地址 myft5x06:my-ft5x06@38{ //配对属性 compatible = "my-ft5x06"; //regI2c地址 reg = ; }; }
c语言添加:
#include #include #include struct i2c_adapter *i2c_ada; // 定义I2C设备信息,用于描述一个I2C设备的资源 struct i2c_board_info ft5x06[] = { {I2C_BOARD_INFO("my-ft5x06", 0x38)}, {} }; static int __init iic_device_init(void) { i2c_ada = i2c_get_adapter(1); // 获取I2C适配器 i2c_new_device(i2c_ada, ft5x06); // 创建设备 i2c_put_adapter(i2c_ada); // 释放适配器 return 0; } static void __exit iic_device_exit(void) { } module_init(iic_device_init); module_exit(iic_device_exit); MODULE_LICENSE("GPL");t); MODULE_LICENSE("GPL");
2. I2C_driver部分:
驱动框架:
#include #include #include int iic_driver_probe(struct i2c_client *client, const struct i2c_device_id *id) { return 0; } int iic_driver_remove(struct i2c_client *client) { return 0; } // const struct of_device_id my_match_table[] ={ // {.compatible = "my-ft5x06"}, // {} // } const struct i2c_device_id *my_id_table[] = { {"my-ft5x06"}, {} } ; struct i2c_driver my_iic_driver = { .driver = { //.name用于和使用c注册的device名字作匹配 .name = "my-ft5x06", .owner = THIS_MODULE, //.of_match_table用于匹配设备树中的节点 // .of_match_table = my_match_table, }, .probe = iic_driver_probe, .remove = iic_driver_remove, //.id_table和.name用于和使用c注册的device名字作匹配,优先使用table .id_table = my_id_table, }; static int iic_driver_init(void) { int ret = i2c_add_driver(&my_iic_driver); // 添加驱动程序 if (ret3. 获取设备信息:
使用设备树与driver匹配成功后,执行probe函数->获取设备树中资源信息:补全probe函数功能功能:
struct gpio_desc *reset_gpio; struct gpio_desc *irq_gpio; // 中断处理函数 irqreturn_t ft5x06_handler(int irq, void *args) { printk("ft5x06_handler\n"); // 中断处理逻辑 return IRQ_RETVAL(IRQ_HANDLED);//表示中断已经处理完毕 } int iic_driver_probe(struct i2c_client *client, const struct i2c_device_id *id) { int ret = 0; printk("iic_driver_probe\n"); // 获取设备树中的节点信息 // 获取复位引脚信息 reset_gpio = gpiod_get_optional(&client->dev, "reset", 0); if (IS_ERR(reset_gpio)) { printk("get reset gpio failed\n"); return PTR_ERR(reset_gpio); } // 获取中断引脚信息 irq_gpio = gpiod_get_optional(&client->dev, "irq", 0); if (IS_ERR(irq_gpio)) { printk("get irq gpio failed\n"); return PTR_ERR(irq_gpio); } // 复位设备 gpiod_direction_output(reset_gpio, 0); mdelay(5); gpiod_direction_output(reset_gpio, 1); // 申请中断 // IRO_TYPE_EDGE_FALLING |IROF_ONESHOT:下降沿触发,中断可嵌套 ret = request_irq(client->irq, ft5x06_handler, IRO_TYPE_EDGE_FALLING | IROF_ONESHOT, "ft5x06_irq", NULL); if (ret中断的返回信息:
/** * enum irqreturn * @IRQ_NONE interrupt was not from this device * @IRQ_HANDLED interrupt was handled by this device * @IRQ_WAKE_THREAD handler requests to wake the handler thread */ enum irqreturn { IRQ_NONE = (0 u8 data; // 发送读取寄存器的命令,两个数据包 struct i2c_msg msgs[] = { [0] = { .addr = fx5x06_client-addr, .flags = 0, .buf = ®, .len = sizeof(reg), }, [1] = { .addr = fx5x06_client-addr, .flags = I2C_M_RD, .buf = &data, .len = sizeof(data), }}; i2c_transfer(fx5x06_client-adapter, msgs, 2); return data; }i2c写数据:
// 写寄存器数据 void ft5x06_write_reg(u8 reg, u8 data, u8 len) { u8 buff[256]; // 发送写入寄存器的命令,两个数据包 struct i2c_msg msgs[] = { [0] = { .addr = fx5x06_client->addr, .flags = 0, .buf = buff, .len = len + 1, }, }; //buff中存放寄存器地址+数据 buff[0] = reg; memcpy(&buff[1], &data, len); // 发送数据 i2c_transfer(fx5x06_client->adapter, msgs, 1); }源程序:
iic_driver:
#include #include #include #include #include #include #include #include struct gpio_desc *reset_gpio; struct gpio_desc *irq_gpio; struct i2c_client *fx5x06_client; // 读寄存器数据 int ft5x06_read_reg(u8 reg) { u8 data; // 发送读取寄存器的命令,两个数据包 struct i2c_msg msgs[] = { [0] = { .addr = fx5x06_client->addr, .flags = 0, .buf = ®, .len = sizeof(reg), }, [1] = { .addr = fx5x06_client->addr, .flags = I2C_M_RD, .buf = &data, .len = sizeof(data), }}; i2c_transfer(fx5x06_client->adapter, msgs, 2); return data; } // 写寄存器数据 void ft5x06_write_reg(u8 reg, u8 data, u8 len) { u8 buff[256]; // 发送写入寄存器的命令,两个数据包 struct i2c_msg msgs[] = { [0] = { .addr = fx5x06_client->addr, .flags = 0, .buf = buff, .len = len + 1, }, }; // buff中存放寄存器地址+数据 buff[0] = reg; memcpy(&buff[1], &data, len); // 发送数据 i2c_transfer(fx5x06_client->adapter, msgs, 1); } // 中断处理函数 irqreturn_t ft5x06_handler(int irq, void *args) { printk("ft5x06_handler\n"); // 中断处理逻辑 return IRQ_RETVAL(IRQ_HANDLED); // 表示中断已经处理完毕 } int iic_driver_probe(struct i2c_client *client, const struct i2c_device_id *id) { int ret = 0; printk("iic_driver_probe\n"); // 使用全局变量保存client fx5x06_client = client; // 获取设备树中的节点信息 // 获取复位引脚信息 reset_gpio = gpiod_get_optional(&client->dev, "reset", 0); if (IS_ERR(reset_gpio)) { printk("get reset gpio failed\n"); return PTR_ERR(reset_gpio); } // 获取中断引脚信息 irq_gpio = gpiod_get_optional(&client->dev, "irq", 0); if (IS_ERR(irq_gpio)) { printk("get irq gpio failed\n"); return PTR_ERR(irq_gpio); } // 复位设备 gpiod_direction_output(reset_gpio, 0); mdelay(5); gpiod_direction_output(reset_gpio, 1); // 申请中断 // IRO_TYPE_EDGE_FALLING |IROF_ONESHOT:下降沿触发,中断可嵌套 ret = request_irq(client->irq, ft5x06_handler, IRO_TYPE_EDGE_FALLING | IROF_ONESHOT, "ft5x06_irq", NULL); if (ret注:board和client
i2c_board_info和i2c_client都可以描述一个i2c外设的资源信息
i2c_board_info注册板载信息,和i2c_client的结构体相差无几,但成员变量中没有adapter的变量指针,而adapter是底层i2c的重要成员变量所以
i2c_new_device(i2c_ada, ft5x06); // 创建设备
参数需要板载i2c设备信息和i2c_ada适配器指针,向内核注册一个完整的i2c_client设备
在应用层使用i2c通信:
在应用层使用i2c的操作,会调用内核层的i2c通信,使用应用层的通信,不需要向内核里注册client即可直接与从机通信。
Linux下应用层通用的I2c驱动程序在i2c-dev.c中:
i2c外设的驱动程序分成了i2c_client 和i2c_driver,在 Linux 内核中提供了一个通用的 i2c驱动,
这个通用的驱动程序给应用程序提供了操作 i2c 控制器的设备节点。所以在应用程序中可以
直接通过 fd = open("/dev/i2c-1",O_RDWR);进行i2c 通信。这个通用的驱动程序就是 i2c-dev.c。
驱动所在位置:
kernel/drivers/i2c
使能内核支持应用层的I2c通信:
> 编译内核镜像: Device Driver --> i2C support --> i2C device interface在dev目录下就会有i2c的设备节点了
i2c_app.c:(使用ioctl通信)
/* 在应用层使用C语言编写一个IIC通信的程序。该程序应该能够实现IIC通信的各种功能,如读写数据、设置地址等。 */ #include #include #include #include #include #include #include #include #include #define I2C_DEVICE "/dev/i2c-1" // I2C设备文件路径 #define I2C_ADDRESS 0x50 // I2C设备地址 //i2c读数据 int i2c_read(int fd,unsigned char slave_addr, unsigned char reg_addr) { unsigned char data; struct i2c_rdwr_ioctl_data i2c_msg; struct i2c_msg messages [] ={ [0] = { .addr = slave_addr, .flags = 0, .len = sizeof(reg_addr), .buf = ®_addr, }, [1] = { .addr = slave_addr, .flags = I2C_M_RD, .len = sizeof(data), .buf = &data, }, }; i2c_msg.msgs = messages; i2c_msg.nmsgs = 2; int ret = ioctl(fd, I2C_RDWR, &i2c_msg); if(reti2c_app.c(使用write/read)
用户层的读写会调用内核层的读写:
查看内核中i2c的读写函数:
static ssize_t i2cdev_write(struct file *file, const char __user *buf, size_t count, loff_t *offset) { int ret; char *tmp; struct i2c_client *client = file->private_data; if (count > 8192) count = 8192; tmp = memdup_user(buf, count); if (IS_ERR(tmp)) return PTR_ERR(tmp); pr_debug("i2c-dev: i2c-%d writing %zu bytes.\n", iminor(file->f_path.dentry->d_inode), count); ret = i2c_master_send(client, tmp, count); kfree(tmp); return ret; } static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count, loff_t *offset) { char *tmp; int ret; struct i2c_client *client = file->private_data; if (count > 8192) count = 8192; tmp = kmalloc(count, GFP_KERNEL); if (tmp == NULL) return -ENOMEM; pr_debug("i2c-dev: i2c-%d reading %zu bytes.\n", iminor(file->f_path.dentry->d_inode), count); ret = i2c_master_recv(client, tmp, count); if (ret >= 0) ret = copy_to_user(buf, tmp, count) ? -EFAULT : ret; kfree(tmp); return ret; }编写在用户层的读写:
// 使用read和write驱动i2c通信 void i2c_kernel_write(int fd, int reg_addr, unsigned char data) { unsigned char wr_data[2]; wr_data[0] = reg_addr; wr_data[1] = data; int ret = write(fd, wr_data, sizeof(wr_data)); if (ret在调用读写之前需要使用ioctl去设置i2c通信时从机的地址:ioctl(fd,I2C_SLAVE_FORCE,SLAVE_ADDR);
#define I2C_SLAVE_FORCE 0x0706 /* Use this slave address, even if it
is already in use by a driver! */
fd : i2c设备的节点描述符
I2C_SLAVE_FORCE:设置地址的命令
SLAVE_ADDR : 从机的地址
使用ioctl设置地址时:实际的调用部分是设置client的addr:
case I2C_SLAVE_FORCE: /* NOTE: devices set up to work with "new style" drivers * can't use I2C_SLAVE, even when the device node is not * bound to a driver. Only I2C_SLAVE_FORCE will work. * * Setting the PEC flag here won't affect kernel drivers, * which will be using the i2c_client node registered with * the driver model core. Likewise, when that client has * the PEC flag already set, the i2c-dev driver won't see * (or use) this setting. */ if ((arg > 0x3ff) || (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f)) return -EINVAL; if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg)) return -EBUSY; /* REVISIT: address could become busy later */ client->addr = arg; return 0;源程序:
/* 在应用层使用C语言编写一个IIC通信的程序。该程序应该能够实现IIC通信的各种功能,如读写数据、设置地址等。 */ #include #include #include #include #include #include #include #include #include #include #define I2C_DEVICE "/dev/i2c-1" // I2C设备文件路径 #define I2C_ADDRESS 0x50 // I2C设备地址 // 使用ioctl函数i2c读数据 int i2c_read(int fd, unsigned char slave_addr, unsigned char reg_addr) { unsigned char data; struct i2c_rdwr_ioctl_data i2c_msg; struct i2c_msg messages[] = { [0] = { .addr = slave_addr, .flags = 0, .len = sizeof(reg_addr), .buf = ®_addr, }, [1] = { .addr = slave_addr, .flags = I2C_M_RD, .len = sizeof(data), .buf = &data, }, }; i2c_msg.msgs = messages; i2c_msg.nmsgs = 2; int ret = ioctl(fd, I2C_RDWR, &i2c_msg); if (ret