i2c子系统
i2c 硬件协议
Linux 应用层读写i2c 数据
在Linux系统上,不仅可以在内核中使用 i2c 总线发送、接收数据,同时也支持应用层使用i2c 总线发送、接收。
如果在内核中使能了drivers/i2c/i2c-dev.c 配置,内核就会为每一个i2c 控制器生成一个/dev/i2c-x 的字符设备节点,使用文件IO向设备节点读写数据就可以完成i2c 数据的发送和接收。
应用层读写i2c 方法有两种:1、利用read、write函数读写;2、利用ioctl 函数读写。参考I2C编程应用开发-韦东山
ioctl的方法是使用数据包(struct i2c_msg)的方式来描述数据,使用时需要填充i2c_msg 各个成员,将i2c_msg 添加到i2c_rdwr_ioctl_data,然后调用ioctl 将i2c_rdwr_ioctl_data传入即可。
ioctl(fd, I2C_RDWR, &ioctl_data)
//读数据:需要构建两个i2c_msg,第一个msg 发送要写入的reg地址(写方向),第二个msg 读取reg中的值(读方向)
//写数据:只需要一个i2c_msg, msg->buf[0]=reg地址,msg->buf 其余空间存放要写入reg的数据
//include/linux/i2c-dev.h
struct i2c_rdwr_ioctl_data {struct i2c_msg __user *msgs; /* pointers to i2c_msgs */__u32 nmsgs; /* number of i2c_msgs */
};
//include/linux/i2c.h
struct i2c_msg {__u16 addr; /* slave address */__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */ //读标志位,写标志为0
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */__u16 len; /* msg length */ //要写和读的数据长度(buf长度)__u8 *buf; /* pointer to msg data */
};
如下代码为应用层i2c总线读、写 i2c数据的方法(ioctl 方法)。
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h> //支持uint16_t 类型
#include <sys/ioctl.h>
#include <unistd.h>
#include <linux/i2c-dev.h>
#include <linux/i2c.h>#define help printf("Usage: \n read:./i2c_reg dev slave_addr reg_addr\n write:./i2c_reg dev slave_addr reg_addr value\n")/*函数名:reg_write
**功能:向i2c 器件写数据
**参数:fd:i2c器件对应I2C控制器设备节点句柄
** dev_addr:i2c从设备地址
** reg_addr:要写入的寄存器地址
** data_buf:要写入的数据buf
** len:要写多少个字节。
**返回值:负数表示操作失败,其他为成功
*/
int reg_write(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char * data_buf,int len)
{int ret;unsigned char msg_buf[9];struct i2c_rdwr_ioctl_data data;struct i2c_msg messages;/* 1. 构建msg_buf*//* 1.1. 将要操作的寄存器首地址赋给要进行I2C数据通信的首字节数据 */msg_buf[0] = reg_addr;/* 1.2. 将要向从机写的数据放入buf 中 */memcpy((void *) &msg_buf[1], data_buf, len); //第1位之后是数据/* 2. 构建 struct i2c_msg messages *//* 2.1. 赋值从设备地址 */messages.addr = dev_addr; /* 2.2. 赋值flags为本次I2C通信完成写功能 */messages.flags = 0; /* 2.3. 赋值len为数据buf的长度 + 寄存器地址的数据长度 */messages.len = len+1;/* 2.4. 构建消息包的数据buf*/messages.buf = msg_buf; /* 3. 构建struct i2c_rdwr_ioctl_data data *//* 3.1. 将准备好的消息包赋值给i2c_rdwr_ioctl_data中的msgs消息*/data.msgs = &messages;/* 3.2. 由于本次I2C通信只有写动作,所以消息数为1次 */data.nmsgs = 1;/* 4. 调用驱动层的读写组合的I2C数据传输 */if(ioctl(fd, I2C_RDWR, &data) < 0){printf("I2C_RDWR err \n");return -1;}return 0;
}/*函数名:reg_read
**功能:从i2c 器件读取数据
**参数:fd:从设备对应的I2C控制器设备节点的文件句柄
** dev_addr:i2c从设备地址
** reg_addr:要读取的寄存器地址
** data_buf: 读数据的buf
** len:要读多少个字节。
**返回值:负数表示操作失败,其他为成功
*/
int reg_read(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char * data_buf,int len)
{int ret;unsigned char msg_buf[9];struct i2c_rdwr_ioctl_data data;struct i2c_msg messages[2];/* 1. 构建 struct i2c_msg messages *//* 1.1. 构建第一条消息 messages[0] *//* 1.1.1. 赋值I2C从设备地址 */messages[0].addr = dev_addr; /* 1.1.2. 赋值flags为本次I2C通信完成写动作 */messages[0].flags = 0; /* 1.1.3. 赋值len为eeprom寄存器地址的数据长度是1 */messages[0].len = 1;/* 1.1.4. 本次写动作的数据是要读读取的寄存器首地址*/messages[0].buf = ®_addr; /* 1.2. 构建第二条消息 messages[1] *//* 1.2.1. 赋值I2C从设备地址 */messages[1].addr = dev_addr; /* 1.1.2. 赋值flags为本次I2C通信完成读动作 */messages[1].flags = I2C_M_RD; /* 1.1.3. 赋值len为要读取寄存器数据长度len */messages[1].len = len;/* 1.1.4. 本次读动作的数据要存放的buf位置*/messages[1].buf = data_buf; /* 2. 构建struct i2c_rdwr_ioctl_data data *//* 2.1. 将准备好的消息包赋值给i2c_rdwr_ioctl_data中的msgs消息*/data.msgs = messages;/* 2.2. 由于本次I2C通信既有写动作也有读动作,所以消息数为2次 */data.nmsgs = 2;/* 3. 调用驱动层的读写组合的I2C数据传输 */if(ioctl(fd, I2C_RDWR, &data) < 0){printf("I2C_RDWR err \n");return -1;}return 0;
}int main(int argc,char *argv[])
{int fd,ret;uint16_t slave_addr,reg_addr,value;struct i2c_rdwr_ioctl_data data;struct i2c_msg messages;unsigned char data_buf[1];if(argc < 4 || !strcmp(argv[1],"-h")){help;return -1;} slave_addr = (uint16_t)strtoul(argv[2], NULL, 0);reg_addr = (uint16_t)strtoul(argv[3], NULL, 0);if(argc == 5)value = (uint16_t)strtoul(argv[4], NULL, 0);fd = open(argv[1],O_RDWR);if(fd < 0){perror("open");return -1;}if(argc ==4){ret = reg_read(fd,slave_addr,reg_addr,data_buf,1); if(ret < 0)printf("read error\n");printf("reg 0x%x value 0x%x\n",reg_addr,data_buf[0]);}else{data_buf[0] = value & 0xff;ret = reg_write(fd,slave_addr,reg_addr,data_buf,1);if(ret < 0)printf("write error\n");printf("write succeed\n");}close(fd);return 0;
}
i2c 子系统中的重要结构体
i2c_adapter
如上图为i2c 硬件框架图,一个soc 中可能有多个i2c控制器,每个控制器下可能挂有多个i2c 从设备(用scl、sda两条线相连)。
那么在内核中我们怎么来描述一个i2c 控制器呢?用struct i2c_adapter
。
i2c_adapter 中比较重要的成员:
nr : 表示它在soc 中是第几个i2c 控制器(或者说第几条i2c总线)。
algo: 算法,它表示我们如何使用这条i2c总线 (向i2c总线上发送/接收数据)。在struct i2c_algorithm 中有个master_xfer 回调函数,它用来实现向总线发送、接收数据(这个函数应该由i2c 控制器驱动实现)。
struct i2c_adapter {struct module *owner;unsigned int class; /* classes to allow probing for */const struct i2c_algorithm *algo; /* the algorithm to access the bus */void *algo_data;/* data fields that are valid for all devices */struct rt_mutex bus_lock;int timeout; /* in jiffies */int retries;struct device dev; /* the adapter device */int nr;char name[48]; //名字struct completion dev_released;struct mutex userspace_clients_lock;struct list_head userspace_clients;struct i2c_bus_recovery_info *bus_recovery_info;const struct i2c_adapter_quirks *quirks;
};struct i2c_algorithm {int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, //i2c 总线传输函数int num);int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr, //smbus 协议:基于i2c 协议的另外一种协议,与i2c协议基本类似 unsigned short flags, char read_write,u8 command, int size, union i2c_smbus_data *data);/* To determine what the adapter supports */u32 (*functionality) (struct i2c_adapter *);#if IS_ENABLED(CONFIG_I2C_SLAVE)int (*reg_slave)(struct i2c_client *client);int (*unreg_slave)(struct i2c_client *client);
#endif
};
i2c_client
知道了如何描述i2c 控制器,那么怎么描述挂在i2c 总线下的从设备呢? 用 struct i2c_client
。
与一个i2c 从设备进行通信,我们需要确定以下几点信息:
- 从设备挂在哪条i2c 总线上。
- 从设备的器件地址是多少。
所以i2c_client 中比较重要的成员是:
adapter: 指向一个i2c_adapter 结构体地址,它表示这个从设备挂在这个控制器下。
addr: 器件地址,7位有效。
flags: 一些重要的标志。比如I2C_CLIENT_TEN,一般的i2c器件地址都是7位数据,所以默认按7位处理,但是使能了这个标志就表示我的地址是10位有效。
struct i2c_client {unsigned short flags; /* div., see below */
#define I2C_CLIENT_PEC 0x04 /* Use Packet Error Checking */
#define I2C_CLIENT_TEN 0x10 /* we have a ten bit chip address *//* Must equal I2C_M_TEN below */
#define I2C_CLIENT_SLAVE 0x20 /* we are the slave */
#define I2C_CLIENT_HOST_NOTIFY 0x40 /* We want to use I2C host notify */
#define I2C_CLIENT_WAKE 0x80 /* for board_info; true iff can wake */
#define I2C_CLIENT_SCCB 0x9000 /* Use Omnivision SCCB protocol *//* Must match I2C_M_STOP|IGNORE_NAK */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;
#if IS_ENABLED(CONFIG_I2C_SLAVE)i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};
i2c_msg
知道了如何描述 i2c控制器 和i2c从设备,我们还需要知道怎么描述总线上的数据。用struct i2c_msg
来描述。
addr: 从设备器件地址。
flags: 一些重要的标志。比如flags 的bit0 表示传输方向,bit 0等于I2C_M_RD表示读,bit 0等于0表示写。
len: 要写或是要读的数据长度。
buf: 用于存储数据的buffer。
struct i2c_msg {__u16 addr; /* slave address */__u16 flags;
#define I2C_M_RD 0x0001 /* read data, from slave to master *//* I2C_M_RD is guaranteed to be 0x0001! */
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_DMA_SAFE 0x0200 /* the buffer of this message is DMA safe *//* makes only sense in kernelspace *//* userspace buffers are copied anyway */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */__u16 len; /* msg length */__u8 *buf; /* pointer to msg data */
};
写操作
写入寄存器只需要一步操作就可以完成,所以写操作只需要构建一个i2c_msg 来描述:
发送器件地址+方向,接着发送reg地址 和 数据。
u8 reg_addr = 0x10;
unsigned char data_buf[10];
struct i2c_msg msgs[1];data_buf[0] = reg_addr; //将buf的第一个字节设置为要写的寄存器地址
memcpy(data_buf,data,len) //后面的buf 存入要写入的寄存器的数据msgs[0].addr = 0x50; //器件地址
msgs[0].flags = 0; //写
msgs[0].len = len+1; //数据长度+器件地址长度(1字节)
msgs[0].buf = &data_buf; //设置存放数据的buf
读操作
而读取寄存器则需要两步操作来完成,所以读需要用两个i2c_msg 来描述:
第一步:发送要读取的寄存器地址。(这是写方向,由第一个msg来描述 设备地址+方向 +reg_addr)
第二步:再次向总线发送器件地址+方向,然后将sda 信号线交由从设备处理,等待数据的返回。(读方向,由第二个msg 来描述)
u8 data_addr = 0x10;
i8 data;
struct i2c_msg msgs[2];msgs[0].addr = 0x50; //器件地址
msgs[0].flags = 0; //写
msgs[0].len = 1;
msgs[0].buf = &data_addr;msgs[1].addr = 0x50;
msgs[1].flags = I2C_M_RD; //读
msgs[1].len = 1;
msgs[1].buf = &data; //设置一个空的、承载数据的buf,读取到的数据会放入buf 中。
知道了怎么描述要传输的数据,那么发送、接收时就在把msg交给传输函数来完成。它就是i2c_adapter ->algo->master_xfer。
类型如下:
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);
那么每次传输数据都要调用i2c_adapter ->algo->master_xfer这么深,岂不是很烦,所以内核封装了一个函数i2c_transfer:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
它会取出 i2c_adapter ->algo->master_xfer,然后调用这个回调函数来传输 msg。
i2c-dev 驱动分析
driver/i2c/i2c-dev.c 即我们在应用层使用文件IO 访问/dev/i2c-x 设备节点,完成i2c 通信的底层代码。
观察/dev/i2c-x 的特性,毫无疑问的是它们都是字符设备,拥有相同的主设备号、依次递增的次设备号。每一个设备节点对应一个i2c 控制器。
那么猜测在i2c-dev.c 中就会为每个i2c 控制器注册一个字符设备、创建设备节点,实现file_operations。在应用层调用open、read、write… 时自然就会调用到file_operations 中的open、read、write… 。
应用层可以调用read、write、ioctl 来进行i2c 总线数据传输—— 所以在底层read、write、ioctl 就会实现i2c 总线的数据传输,将应用层下发的数据进行硬件的发送操作。
接下来实际看看i2c-dev.c 到底做了什么。
i2c-dev 注册
Linux 字符设备详解
首先从入口函数开始看
调用register_chrdev_region 申请了一批设备号。(major = I2C_MAJOR(89),baseminor = 0,count 是I2C_MINORS)
创建类 struct class。 (i2c-x 设备节点对应的所有struct device 都属于同一个类,i2c_dev_class)
i2c_for_each_dev 会为每个存在的 i2c_adapter,注册一个cdev 和 device(设备节点)。
i2c_for_each_dev
->bus_for_each_dev
在bus_for_each_dev 函数中会循环的调用传入的回调函数fn,即i2cdev_attach_adapter。
i2cdev_attach_adapter 会创建cdev (保存在i2c_adapter->i2c_dev->cdev)、device(保存在i2c_adapter->i2c_dev->dev),并初始化它们(cdev->ops、device->dev_t、device->class(这几个设备节点都属于同一类)),最后向内核注册cdev 和device。
完成了字符设备的注册和设备节点创建。
(minor 来自i2c_adapter->nr,device->name 也按照i2c-%d——i2c_adapter->nr 来定义)
i2c-dev open
了解了i2c_dev.c 的注册过程,已经注册好了字符设备(cdev) 以及设备节点 (device)。
现在就可以用open、read、write… 来访问底层的file_operations 啦,那么就来看看底层的open、read、write… 是如何实现的。
每一个i2c-x 节点都对应一个i2c控制器,打开一个节点就是指定了要使用的控制器。
那么open 是怎么找对应的i2c_adapter 呢?
利用minor,前面注册的过程中讲过minor 和i2c_adapter->nr 是相等的,而每个i2c_adapter 的nr都是不同的。所以可以利用nr找到 i2c_adapter。
调用i2c_get_adapter(minor)
找到对应的i2c_adapter 。
传输数据,还要确定一个从设备,给谁发啊?
但是对于应用层的i2c 设备驱动来说,底层是不知道从设备是谁的。所以在open中新创建一个i2c_client——管他是谁,反正都用这个来描述。
然后将i2c_client->adapter 设置为获取到的i2c_adapter,表示i2c_client 挂在这个控制器下面(绑定好了i2c_client 与i2c_adapter)。
最后设置i2c_client 为file的私有数据,之后的read、write… 函数都可以从私有数据获取到client。
利用nr 从i2c_adapter_idr 中找到对应的i2c_adapter。(可以把i2c_adapter_idr 想象成一个链表,里面包含了内核中所有的i2c_adapter,并且它们的nr都是唯一的)
ioctl
在前面的应用示例程序中,用ioctl 来读写i2c 总线。
其实它不仅可以用ioctl 来读写,还可以用read/write 来读写,但是在read/write之前需要设置i2c_client->addr(从设备地址),在open中注册的这个空的,并没有设置地址。
设置从设备地址也是调用ioctl 来完成的ioctl(fd,I2C_SLAVE,slave_addr)
或ioctl(fd,I2C_SLAVE_FORCE,slave_addr)
。
ioclt 读数据、写数据的实现ioctl(fd,I2C_RDWR,data)
应用层传入的i2c_rdwr_ioctl_data 从应用空间拷贝至内核空间。同时data->msgs 这里只是一个指针,真正的i2c_msg数据还在应用空间内存里,所以需要再一次将i2c_msg 数据从应用空间拷贝至内核空间(memdup_user)。
memdup_user 就是申请一段内存空间,然后调用copy_from_user 将数据拷贝到内核内存中。
继续跟踪i2cdev_ioctl_rdwr,它调用i2c_transfer 向i2c总线传输数据。如果是读的话,还要把读到的数据返回给应用层。
(前面说过i2c_transfer 是调用i2c_adapter->algo->master_xfer)
read
read 是调用i2c_master_recv 来读取数据,其实它的实现也是调用了i2c_transfer()。
i2c_transfer_buffer_flags 中调用了i2c_transfer 向i2c总线传输数据。
为什么这里只要一个msg呢?很简单,read只用来读,要读的reg_addr 会调用write 先一步发送出去,所以这里只需要接收数据的msg就够了。
write
write 与read 基本类似,只不过读改成写,调用i2c_master_send 向i2c 总线写数据
依旧调用i2c_transfer_buffer_flags ,与接收不同的是,flag 改为写。
close
close 对应的底层函数是release,在release中与open的操作相反——释放open 创建的i2c_client,清除私有数据。
I2C驱动程序的层次
i2c 驱动主要分为i2c 控制器驱动(包括i2c 控制器驱动 + i2c 核心层)、i2c 从机驱动 和i2c 字符设备驱动。
i2c控制器驱动: 初始化i2c 控制器硬件,实现i2c控制器的传输功能(master_xfer 函数),根据核心层提供的方法注册i2c 控制器(i2c_adapter)。
i2c 核心层: 提供下层i2c控制器的管理方法(i2c_adapter 注册方法),向上层给出统一的i2c 传输方法(i2c_transfer)(Linux需要兼容多个平台的i2c 控制驱动,所以由核心层来管理它们,给出统一的传输方法);核心层还需要注册i2c_bus_type 总线模型,负责i2c_driver 与i2c_client 的匹配工作。
i2c 从机驱动: 调用i2c_transfer 实现从设备功能。(从设备:rtc、eeprom、switch…)。
i2c 字符设备驱动: 给每个i2c 控制器创建一个字符设备和设备节点,向应用层提供i2c 控制器的读写方法(通过read/write、ioctl 中调用i2c_transfer 实现)。
i2c控制器驱动 (i2c_adapter 注册流程)
(除了soc 自带的硬件i2c 控制器外,还可以使用gpio模拟i2c,内核提供了通用的gpio 模拟i2c 驱动——drivers\i2c\busses\i2c-gpio.c (除了硬件函数不同,软件框架(i2c_adapter) 与普通的硬件i2c控制器驱动没有任何区别))
一个soc 上可能会有多个i2c 控制器,可以用设备树节点来描述一个i2c 控制器的硬件信息。
以imx6ull 为例:
设备树节点会由内核解析为一个platform_device 并注册到内核中,在i2c 控制器驱动中会注册platform_driver,当它们匹配时就会调用platform_driver->probe。(i2c 控制器驱动是由platform总线管理的,而非i2c总线)
在probe 函数中会获取设备树节点中i2c 控制器的硬件信息(irq、寄存器地址范围、时钟…),然后设置i2c 控制器硬件寄存器、注册中断等等;
另外在内核中i2c 控制器使用一个i2c_adapter 来描述,所以在probe 函数中需要创建、设置、注册i2c_adapter (只介绍i2c 框架,其它硬件设置代码忽略)。
如下为imx6ull 的i2c 控制器驱动 drivers\i2c\busses\i2c-imx.c:
创建一个struct imx_i2c_struct (nxp i2c驱动自己定义的结构体,其中包含i2c_adapter)
设置i2c_adapter 中的各个成员(nr、algo 为关键,每条i2c 总线都有一个唯一的number 来标识):
其中i2c_adapter->algo (struct i2c_algorithm)是最关键的,algo->master_xfer 就是i2c 控制器的传输函数。
可以使用master_xfer 向i2c 总线传输数据;
functionality 函数可以确定i2c 控制器驱动支持的功能。
I2C_FUNC_I2C:表示支持i2c 典型的传输功能;
I2C_FUNC_SMBUS_EMUL、I2C_FUNC_SMBUS_READ_BLOCK_DATA:smbus 总线相关的读写功能。
可以使用i2c_set_adapdata 来设置驱动数据。(i2c_adapter->dev->driver_data = i2c_imx(随便任何常用的数据结构),类似于platform_set_drvdata)
最后调用i2c_add_numbered_adapter 或i2c_add_adapter 向注册一个i2c_adapter。
(i2c_add_numbered_adapter 与i2c_add_adapter 的区别是,前者为驱动指定好了i2c_adapter->nr,后者指定nr = -1 由内核来分配合适的number)
i2c-core (i2c核心层)
i2c_add_adapter (i2c_adapter 注册函数)
kernel v4.1:drivers\i2c\i2c-core.c
kernel v5.4:drivers\i2c\i2c-core-base.c
i2c_add_adapter 会做如下操作:
1、注册i2c_adapter 会注册i2c_adapter->dev
2、遍历i2c 控制器节点下的所有子节点,为每个子节点创建一个i2c_client,并注册i2c_client。
(所谓的注册i2c_client 其实就是注册i2c_client->dev (device),将i2c_client->dev 添加到bus->p->klist_devices,注册过程中会查找bus->p->klist_drivers 中的device_driver 是否与其匹配,匹配则调用bus->probe,最终调用到i2c_driver->probe)
device 与device_driver 注册与匹配流程
of_alias_get_id 可以获取到设备树别名上的序号,使用别名序号作为i2c_adapter->nr。
如图中的节点 i2c1 别名为i2c0,nr 就是0。
nr 获取成功后调用__i2c_add_numbered_adapter 注册i2c_adapter。
__i2c_add_numbered_adapter
->i2c_register_adapter
i2c_register_adapter 首先检测i2c_bus_type 是否有效,判断i2c_adapter->name、i2c_adapter->algo 是否有效,无效直接返回失败。
设置i2c_adapter->dev.bus 为i2c_bus_type,注册i2c_adapter->dev。
重点关注of_i2c_register_devices(adap) ,它会遍历i2c 控制器节点下的所有子节点,根据节点生成i2c_client 并注册i2c_client->dev
i2c_adapter->dev->of_node 在前面的probe 中已经被设为了platform_device->dev->of_node,即i2c 控制器的设备树节点。
所以for_each_available_child_of_node 是遍历i2c 控制器的所有子节点。
i2c_new_device 以i2c_board_info 生成一个i2c_client,并注册它
i2c_transfer (i2c 传输函数)
为了给从机驱动 和i2c-dev 等驱动更容易的使用i2c 总线传输数据,i2c 核心层封装了一个所有i2c 控制器通用的i2c 传输函数——i2c_transfer。
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
它其实就是调用的i2c 控制器驱动中实现的master_xfer 函数,i2c_adapter->algo->master_xfer。
i2c_transfer
->__i2c_transfer
i2c 设备-驱动-总线 模型
kernel v4.1:drivers\i2c\i2c-core.c
kernel v5.4:drivers\i2c\i2c-core-base.c
为了实现i2c 从设备的设备信息与驱动分离,Linux 内核引入了i2c 总线模型。
在i2c-core.c 的i2c_init 函数中注册了i2c_bus_type 总线模型。(postcore_initcall 在内核启动过程中会较早的调用,在注册i2c_driver、i2c_client时,总线模型已经被注册好了)
i2c总线模型和platform 总线其实都是类似的,它们都基于device、device_driver 设备驱动模型。
判断匹配的方式也是类似的,主要关注设备树匹配方式(比较compatible 属性) 和传统的id_table 匹配方式(比较i2c_device_id->name 与i2c_client->name 是否相等)。
不同的是i2c_bus_type 提供了probe 函数,会优先调用到bus_type->probe 即i2c_device_probe。
(platform_bus_type 没有提供bus_type->probe,调用的是device_driver->probe)
在i2c_device_probe 最终调用i2c_driver->probe 也就是从设备驱动中的probe 函数。
(这里获取了从设备节点的irq,一般从设备不需要使用irq,所以在从设备节点中没有描述irq,这里返回失败client->irq 被设为0)
i2c_add_driver (i2c_driver 注册函数)
内核中用一个i2c_driver 来表示一个i2c从设备驱动,用i2c_client 来描述一个i2c 从设备的硬件信息。
使用i2c_add_driver 可以注册一个struct i2c_driver
,当它与i2c_client 匹配时就会调用i2c_driver->probe,那么看看它的注册过程是怎么样的。
i2c_add_driver 只是一个宏,实则调用的是i2c_register_driver。
在i2c_register_driver 中最关键的就是设置 i2c_driver->driver->bus 的总线类型,然后注册i2c_driver->driver(这里就是device 与device_driver 那一套注册过程了,可以参考device、device_driver 注册、匹配过程)。
在i2c 控制器驱动中调用i2c_add_adapter 注册i2c_adapter时,会搜索i2c 控制器dtb 节点下的所有子节点(每一个子节点都是一个从设备),为每个子节点创建一个i2c_client,并且注册i2c_client->dev (struct device)。
所以当i2c_driver 注册时,假如i2c_driver->driver 与某个i2c_client->dev 匹配完成,就会调用i2c_driver->probe 来执行从设备的驱动代码。
i2c_for_each_dev 会遍历所有存在的i2c_adapter,并未每个adapter 调用__process_new_driver (其实这一步没啥用,可以直接忽略)
__process_new_driver
->i2c_do_add_adapter
->i2c_detect
__process_new_driver 最终会调用到i2c_detect,它检查i2c_driver->detect 和i2c_driver->address_list 如果为空直接返回,观察很多从设备驱动中这两个成员都没有设置,所以这里直接返回(对于这些驱动来说i2c_for_each_dev(driver, __process_new_driver);
这句代码没啥用,可以直接忽略)。
i2c 从设备驱动框架
什么是i2c 从设备驱动:比如i2c 接口的rtc芯片、加密芯片、phy芯片等等,它们都需要通过i2c 总线来设置寄存器,所以i2c 从设备驱动主要就是调用i2c 读写函数来实现功能。
i2c 从设备驱动框架如下:
i2c 从设备驱动使用一个i2c_driver 来描述,所以在驱动中需要构建一个i2c_driver,并使用i2c_add_driver 注册它。
i2c 从设备的硬件信息使用一个i2c_client 来描述,在kernel v3.x 以上的版本会用设备树来描述从设备的硬件信息,最终会由内核解析成一个i2c_client 并向内核注册。
当i2c_client 与i2c_driver 匹配时,就会调用i2c_driver->probe,在probe函数中可以编写从设备驱动代码实现功能(调用i2c_transfer 等等函数设置寄存器,你也可以做其它事情任何事情,自由发挥)。
static const struct of_device_id of_match_ids_ap3216c[] = {{ .compatible = "lite-on,ap3216c", .data = NULL },{ /* END OF LIST */ },
};static const struct i2c_device_id ap3216c_ids[] = {{ "ap3216c", (kernel_ulong_t)NULL },{ /* END OF LIST */ }
};static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{return 0;
}static int ap3216c_remove(struct i2c_client *client)
{return 0;
}static struct i2c_driver i2c_ap3216c_driver = {.driver = {.name = "ap3216c",.of_match_table = of_match_ids_ap3216c,},.probe = ap3216c_probe,.remove = ap3216c_remove,.id_table = ap3216c_ids,
};static int __init i2c_driver_ap3216c_init(void)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);return i2c_add_driver(&i2c_ap3216c_driver);
}static void __exit i2c_driver_ap3216c_exit(void)
{i2c_del_driver(&i2c_ap3216c_driver);
}module_init(i2c_driver_ap3216c_init);
module_exit(i2c_driver_ap3216c_exit);
MODULE_LICENSE("GPL");
比较好用的寄存器读写函数
在从设备驱动中你肯定需要读写从设备的寄存器,除了i2c_transfer 外还有其它的读写函数:
读取i2c 从设备寄存器1个字节,command 就是寄存器地址,返回寄存器1 个字节的值 (u8)
s32 i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command)
向寄存器写入1个字节的值。
s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value)
读取寄存器4个字节,返回2个字节 (u16)。
s32 i2c_smbus_read_word_data(const struct i2c_client *client, u8 command)
写入寄存器2个字节。
s32 i2c_smbus_write_word_data(const struct i2c_client *client, u8 command,u16 value)
读取寄存器一块数据,返回数据的首地址(u8*)。
s32 i2c_smbus_read_block_data(const struct i2c_client *client, u8 command, u8 *values)
写入寄存器一块数据。
s32 i2c_smbus_write_block_data(const struct i2c_client *client, u8 command,u8 length, const u8 *values)
相关文章:

i2c子系统
i2c 硬件协议 Linux 应用层读写i2c 数据 在Linux系统上,不仅可以在内核中使用 i2c 总线发送、接收数据,同时也支持应用层使用i2c 总线发送、接收。 如果在内核中使能了drivers/i2c/i2c-dev.c 配置,内核就会为每一个i2c 控制器生成一个/dev/…...

【K3s】第17篇 Helm版本和支持的Kubernetes版本对照表
目录 Helm版本和支持的Kubernetes版本对照表 Helm版本和支持的Kubernetes版本对照表 描述了在Helm和Kubernetes之间支持的最大版本偏差。 Helm的版本用 x.y.z 描述,x是主版本,y是次版本,z是补丁版本。 当一个Helm的新版本发布时࿰…...

如何自己搭建一个ai画图系统? 从0开始云服务器部署novelai
如何自己搭建一个ai画图系统? 从0开始云服务器部署novelai 上面两张图都是通过ai生成的,是不是有以假乱真的感觉。 本教程提供的是自己搭建一个可以外网访问的ai系统的方法,需要采购gpu服务器(后续会出白嫖的方式)&…...

SpringSecurity过滤请求导致的系统bug
背景 今天开发一个新的会员管理系统,继承了SpringSecurity的,用以控制权限。结果无论怎么配置,都会报错:An Authentication object was not found in the SecurityContext 这句话的意思很明确:指的就是在SecurityCon…...

css\js\vue知识点
1.css3新特性 css3新特性 1)选择器 2)阴影 3)形状转换(2D <-> 3D) 4)变形 5)动画(过渡动画、帧动画) 6)边框 7)多重背景 8)反…...

在vue项目中使用video.js实现视频播放和视频进度条打点
一、用video.js实现视频播放 1、安装video.js插件 // 安装video.js插件 npm install video.js -S // 如果需要播放rtmp直播流,需安装一下插件 npm install videojs-flash -S 2、在组件代码里使用 <template><div data-vjs-player><video ref&quo…...

【代码训练营】day41 | 01背包问题 416. 分割等和子集
所用代码 java 01背包理论 背包最大重量为:4 重量价值物品0115物品1320物品2430 暴力:O(2^n) 动态规划: 1、二维dp数组 dp[i] [j] dp数组含义:[0, i]物品,任取放进容量为j的背包里的最大价值 递推公式:…...

linux网络编程-多进程实现TCP并发服务器
服务端流程步骤socket函数创建监听套接字lfdbind函数将监听套接字绑定ip和端口listen函数设置服务器为被动监听状态,同时创建一条未完成连接队列(没走完tcp三次握手流程的连接),和一条已完成连接队列(已完成tcp三次握手…...

C语言的学习小结——数组
一、一维数组的创建与初始化 1、格式: type_t arr_name[const_n];//type_t 是指数组的元素类型 //const_n 是一个常量表达式,用来指定数组的大小 注: 数组是使用下标来访问的,下标从0开始。 数组的大小可以通过计算得到&…...

HTB-Photobomb
HTB-Photobomb信息收集开机提权对于问题的思考信息收集 端口扫描 目标首页 有一个http Authorization 目录扫描 在查看源码的时候发现了一个js文件。 并且发现了访问不存在的目录会出现错误提示。 通过搜索得知 Sinatra 是一个基于 Ruby 语言的 DSL(领域…...

【LSTM】2 多因素单步骤预测
基于时间序列的预测,一定要明白它的原理,不是工作原理,而是工程落地原因。 基于时间序列,以已知回归未知----这两句话是分量很重的。 多因素单步单输出组合 时间序列:t1 是 特征 1,2,3 预测t2 的回归值41 多因素单步多…...

ChatGPT从下游应用“火”到了上游芯片厂,国内谁将受益?
因库存陷入低迷周期的半导体市场近日因ChatGPT的火热而重新受到外界关注。 原文链接:ChatGPT从下游应用“火”到了上游芯片厂,国内谁将受益? 由于ChatGPT属于生成式AI,被誉为“AI芯片”第一股的英伟达应声而涨。2月13日收盘&#…...

算法单调栈—Java版
单调栈 概念:维护栈中元素的单调性,单调增或者单调减。 什么时候用? 要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置。单调栈的本质是空间换时间,在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元…...

在Linux中进行rocketmq及rocketmq控制台安装与配置
rocketmq下载安装的版本:rocketmq-rocketmq-all-5.0.0.tar.gz rocketmq控制台下载安装的版本:rocketmq-externals-rocketmq-console-1.0.0.tar.gz rocketmq安装 第一步,下载server-jre-8u202-linux-x64.tar.gz安装包。 登录网址ÿ…...

2023年全国最新食品安全管理员精选真题及答案4
百分百题库提供食品安全管理员考试试题、食品安全员考试预测题、食品安全管理员考试真题、食品安全员证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。 31.国家对食品添加剂生产实行____制度。 A.产品注册 B.产品备案 C.登…...

es-07脚本查询
脚本查询 概念 Scripting是Elasticsearch支持的一种专门用于复杂场景下支持自定义编程的强大的脚本功能,ES支持多种脚本语言,如painless,其语法类似于Java,也有注释、关键字、类型、变量、函数等,其就要相对于其他脚本高出几倍的性…...

JM员工福利与健康平台,企业关怀Always Online
庄信万丰(Johnson Matthey, JM),全球性专用化学品公司,是可持续发展技术的全球领导者。在30多个国家和地区拥有13000多名员工。 JM的价值观之一是保护人类和地球。在生产过程中,JM保持对环境保护和能源清洁的高度关注;在员工福利…...

如何使用U-Mail搭建企业邮件服务器?
在当今的信息时代,企业也应该跟上时代的步伐。做好企业信息化建设,对企业事业单位尤为重要。电子邮件作为企业信息化过程中的重要组成部分,在企业内部沟通和外部沟通中发挥着重要作用。目前,有实力的企业已经开始倾向于自己搭建邮…...

用规则来搭建团队:写周报不一定是坏事
你好,我是Smile,一位有二十年工作经验的技术专家。今天我会结合我的经历,和你聊聊搭建技术团队这个话题。 众所周知,技术团队很大程度上决定了一个公司业务的生命力和生命周期,因此技术团队的投入成本往往很高&#x…...

Apollo使用方法
Apollo使用方法1.Apollo相关原理1.Apollo启动方法1.1 软件包方式1.2 脚本方式2.播放数据包2.1 软件包方式2.2 脚本方式3.试验planning模块4.从官网下载场景集其他工具1.Apollo相关原理 cyber / mainboard / mainboard.cc 是Apollo入口 cyber / mainboard / module_argument.cc…...

科研快讯 | 14篇论文被信号处理领域顶级国际会议ICASSP录用
ICASSP 2023 近日,2023年IEEE声学、语音与信号处理国际会议(2023 IEEE International Conference on Acoustics, Speech, and Signal Processing,ICASSP 2023)发布录用通知,清华大学人机语音交互实验室(TH…...

设计模式—策略(Strategy)模式
一、概述策略模式的用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化使用策略模式可以把行为和环境分割开来。环境类负责维持和查询行为类,…...

STM32 触摸屏移植GUI控制控件
目录 1、emWin 支持指针输入设备。 2、 模拟触摸屏驱动 3、实现触摸屏的流程 3.1 实现硬件函数 3.2 实现对GUI_TOUCH_Exec()的定期调用 3.3 使用上一步确定的值,在初始化函数LCD_X_Config()当中添加对GUI_TOUCH_Calibrate()的调用 4、…...

数仓模型之维度建模
目录 1、数仓架构原则 2、如何搭建一个好的数仓 2.1 建模方法 2.2 建模解决的痛点 2.3 数仓系统满足的特性 2.4 数仓架构设计 3、维度建模 4、案例 5、问题讨论 今天我们来聊聊在数仓模型中举足轻重的维度建模。 简单而言,数据仓库的核心目标是为展现层提…...

Servlet笔记(9):Cookie处理
一、Cookies处理 1、Cookies概念 Cookies是存储在客户端计算机上的文本文件,并保留各种跟踪信息。 识别返回用户的三个步骤 服务器脚本向浏览器发送一组Cookies。例如姓名、年龄或识别号码等。浏览器将这些信息存储在本地计算机上。当下一次浏览器向Web服务器发送…...

骨传导耳机是怎么传声的,选择骨传导耳机的时候需要注意什么?
骨传导耳机之所以能够成为当下最火的耳机,骨传导技术将声音转化为震动感,通过骨头进行传播,不会堵塞耳朵,就不会影响到周围环境音。这种技术也让骨传导耳机比传统入耳式耳机更安全,无需入耳式设计,避免了…...

达梦数据库DSC集群部署
一、概述 1.1 DSC 集群架构 1.2 架构说明 1、DMDSC 集群是一个多实例、单数据库的系统。 多个数据库实例可以同时访问、修改同一个数据库的数据。 2、数据文件、控制文件在集群系统中只有一份,不论有几个节点,这些节点都平等地使用这些文件, 这些文件保存在共享存储上。 3…...

java 系列之Mybatis
java 系列文章 文章目录java 系列文章前言一、Mybatis 入门1.1 认识 框架(了解)1.2 认识 ORM(要知道)1.3 认识 Mybatis(要知道)二、Mybatis 使用2.1 创建maven项目并导入依赖2.2 准备数据库,包和…...

OBS 进阶 之 摄像头操作
目录 一、摄像头 1、win-dshow插件中,摄像头枚举操作 1)、视频源ID 2)、注册视频源信息...

Linux操作系统基础知识命令参数详解
Linux操作系统 RAID分组 RAID JBOD RAID JBOD的意思是Just a Bunch Of Disks,是将多块硬盘串联起来组成一个大的存储设备,从某种意义上说这种类型不被算作RAID,在维基百科里JBOD同时也被归入非RAID架构。RAID JBOD将所有的磁盘串联成一个单…...