之前有做过i2c的小实验:ESP32--i2c驱动触摸屏
在之前的ESP32中,还有STM32F103中其实都已经接触过了I2C,所以对于它的协议已经是很熟悉了,那么I2C在linux驱动开发中,是否也跟之前一样呢?
我们基于虚拟总线进行开发,但是i2c是真实存在的物理总线,它的框架是否有不一样的地方?
这些,都是我做这个实验之前的疑问。
在此之前,先再再再一次复习一下i2c!
I2C是一种常见的同步、串行、低俗、近距离通信接口,用于连接各种IC、传感器等期间,比如陀螺仪、加速度计、触摸屏等。
I2C支持多从机,也就是一个I2C控制器下可以挂多个I2C从设备。
看图。
图来自于正点原子驱动开发教程
SDA和SCL这两根线必须要接一个上拉电阻,一般是4.7K。其余的I2C从器件都挂接到SDA和SCL这两根线上,这样就可以通过SDA和SCL这两根线来访问多个I2C设备。
写时序
写时序比较简单,它是将寄存器地址和数据一起发送的。
- 开始信号
- 发送从机地址,其中高七位是设备地址,最后一位是读写位
- 从机发送应答信号
- 重新发送开始信号
- 发送要写入数据的寄存器地址
- 从机发送应答信号
- 发送要写入寄存器的数据
- 从机发送应答信号
- 停止信号
图来自于正点原子驱动开发教程
读时序
读时序相对来说比较复杂。结束时多了一个非应答信号,以及写入寄存器地址之后要重新发从机的地址。
总体分为四步。
- 发送设备地址
- 发送要读取的寄存器地址
- 重新发送设备地址
- 读取数据
图来自于正点原子驱动开发教程
大致了解了一下,下面来做个实验验证一下吧。
这里要使用到的是一个传感器:AP3216C。
AP3216C支持环境光强度、接近距离和红外线强度这三个环境参数检测,通过i2c5与我们的stm32mp157相连。
下面我们就通过i2c5来获取传感器的参数。
I2C子系统
图来自于正点原子驱动开发教程
它的基本框架如此所示。
主要分为三个部分:I2C核心、I2C总线驱动、I2C设备驱动
我们在编写过程中,重点要关注的是设备驱动i2c_driver。
在I2C总线驱动中,有两个重要的数据结构:i2c_adapter 和 i2c_algorithm。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法,里面有自带的通信函数,不过此次我们是用自己编写的函数进行通信。这一部分先不用关心,一般已经被厂商编写好了。
话不多说,说多了也迷迷糊糊,直接开始写!
实验程序编写
我们先写框架。
/*驱动入口函数*/
static int __init ap3216c_init(void)
{
return 0;
}
/*驱动出口函数*/
static void __exit ap3216c_exit(void)
{
}
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_AUTHOR("dada");
MODULE_LICENSE("GPL");
MODULE_INFO(intree, "Y");
一目了然,入口函数和出口函数我们得有吧。
然后进入我们的内核源码,看看别人是怎么写的。
对于I2C的编写,重点就是构建i2c_driver,构建完成之后需要向I2C子系统注册。
通常使用i2c_add_driver。
它的定义如下:
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
同样的也有注销I2C设备的。
void i2c_del_driver(struct i2c_driver *driver)
再结合源码中的写法,我们可以这么写。
/*驱动入口函数*/
static int __init ap3216c_init(void)
{
int ret = 0;
ret = i2c_add_driver(&ap3216c_driver);
return ret;
}
/*驱动出口函数*/
static void __exit ap3216c_exit(void)
{
i2c_del_driver(&ap3216c_driver);
}
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_AUTHOR("dada");
MODULE_LICENSE("GPL");
MODULE_INFO(intree, "Y");
这样,我们的注册和注销就都完成了。
那么问题来了,这里我们还没有定义ap3216c_driver。
这个就是我们重点需要构建的i2c_driver了。
如果不知道怎么办的话,不妨来看一下源码怎么写的吧。
可以发现,其实是跟我们的虚拟总线差不多的。
probe是与设备树匹配上了之后,要执行的代码。而在虚拟总线里面probe通常是构建我们的字符驱动设备,remove就是一些移除的操作了,是不是很熟悉!
我们一步步来。
static struct i2c_driver ap3216c_driver = {
.driver = {
.name = "ap3216c",
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(ap3216c_of_match),
},
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.id_table = ap3216c_id,
};
先写上。
从上到下,首先是我们的匹配列表。
注意,此时我们并没有修改设备树,所以现在要去内核源码里进行修改。
因为ap3216c是与我们的i2c5连接的,所以找到i2c5。
可以看到原来就已经给我们写好了哎。
再去看一眼原理图。
可以发现,跟我们的硬件原理图也是分毫不差。
那么此时可以在我们自己的设备树文件里,定义一个节点。
//dada2023.4.19
&i2c5{
pinctrl-names = "default","sleep";
pinctrl-0 = <&i2c5_pins_a>;
pinctrl-1 = <&i2c5_pins_sleep_a>;
status = "okay";
ap3216c@1e{
compatible = "dada,ap3216c";
reg = <0x1e>;//器件地址,查看手册
};
};
到此,设备树就已经修改好了。
下面,就可以再次回到我们的驱动编写了。
首先是匹配列表。
/*设备树匹配表*/
static const struct of_device_id ap3216c_of_match[] = {
{
.compatible = "dada,ap3216c",
}, {/* sentinel */}
};
注意compatible属性,一定要跟我们设备树中的一致!!
还有就是要留空一行。
再往下是probe函数,按照一般的情况,通常是构建字符设备驱动。
这个已经很熟悉了!
直接写。
static int ap3216c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
int ret = 0;
/*搭建字符设备驱动框架*/
//创建设备号
if(ap32dev.major)//如果给定了主设备号
{
ap32dev.devid = MKDEV(ap32dev.major,0);
ret = register_chrdev_region(ap32dev.devid,AP3216C_CNT,AP3216C_NAME);
}else{
ret = alloc_chrdev_region(&ap32dev.devid,0,AP3216C_CNT,AP3216C_NAME);
}
if(ret < 0){
printk("ap3216c chrdev_region err!\r\n");
return -ENOMEM;
}
//初始化cdev
ap32dev.cdev.owner = THIS_MODULE;
cdev_init(&ap32dev.cdev, &ap3216c_ops);
//添加一个cdev
ret = cdev_add(&ap32dev.cdev,ap32dev.devid,AP3216C_CNT);
if(ret < 0){
goto fail_devid;
}
//创建类
ap32dev.class = class_create(THIS_MODULE,AP3216C_NAME);
if(IS_ERR(ap32dev.class)){
goto fail_cdev;
}
//创建设备
ap32dev.device = device_create(ap32dev.class,NULL,ap32dev.devid,NULL,AP3216C_NAME);
if(IS_ERR(ap32dev.device)){
goto fail_class;
}
ap32dev.client = client;//保存i2c设备
return ret;
fail_class:
class_destroy(ap32dev.class);
fail_cdev:
cdev_del(&ap32dev.cdev);
fail_devid:
unregister_chrdev_region(ap32dev.devid,AP3216C_CNT);
return -EIO;
}
在之前,有一个问题,就是设备结构体并没有定义。
没关系,根据字符设备的构建需要,一个个往里面填写。
struct ap3216c_dev {
int major; //主设备号
int min; //次设备号
dev_t devid;//设备号
struct cdev cdev;
struct class *class;//类
struct device *device;//设备
struct device_node *nd;//设备节点
unsigned short ir, als, ps;//三个传感器数据
struct i2c_client *client;//i2c设备
};
前面是不是很熟悉,后面多出来了一个i2c_client结构体,肯定有人疑问,其实这个是用来保存我们从机信息的。
在我们与设备树匹配了之后,会传进来一个client,这个里面会有适配器和从机地址等等。
什么?
不信?
不信我们来看一下原函数。
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 init_irq; /* irq set at initialization */
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
};
具体里面是个什么原理,我也不是很清楚,现在先学着吧。
可以看到,这里面是有适配器adapter和从机地址addr的,这个我们后面会用到。
注意,probe函数并没有编写完,因为里面还有一个file_operations我们并没有编写,但是对于我们来说并不陌生,因为这个非常非常关键!!
按照一般情况,我们这么定义。
static int ap3216c_open(struct inode *inode, struct file *filp)
{
filp->private_data = &ap32dev;// 传入私有数据
return 0;
}
static ssize_t ap3216c_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *ppos)
{
return 0;
}
static int ap3216c_release(struct inode *inode, struct file *filp)
{
return 0;
}
static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};
到这里,基本的框架就已经搭完了,可以编译一下。
I2C实现
首先是读时序。
首先介绍一个函数i2c_transfer。
i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数。
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num)
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 */
};
在发送之前,我们需要构建好这个结构体。
直接来看程序。
//读
static int ap3216c_read_regs(struct ap3216c_dev *dev,u8 reg,void *val,int len){
struct i2c_client *client = (struct i2c_client*)dev->client;//传入i2c设备
struct i2c_msg msg[2];
msg[0].addr = client->addr;//从机地址,AP3216c的地址
msg[0].flags = 0; //写入寄存器地址
msg[0].buf = ® //要读取的寄存器,也是此时发送的数据
msg[0].len = 1; //要发送的地址长度为1
msg[1].addr = client->addr;//从机地址
msg[1].flags = I2C_M_RD; //读取
msg[1].buf = val; //接收到的数据,缓冲区
msg[1].len = len; //接收的长度
return i2c_transfer(client->adapter,msg,2);//后面的num说的是msg的数量
}
先传入client,因为里面保存了我们适配器和从机地址,然后构建msgs,最后调用i2c_transfer进行读取。
为什么是两个msg。
因为我们的读时序是先发寄存器地址写入,然后还要再发一次从机地址表明是读取的,所以是两个。
至于写就比较简单了。
//写
static int ap3216c_write_regs(struct ap3216c_dev *dev,u8 reg,void *buf,u8 len){
struct i2c_client *client = (struct i2c_client*)dev->client;//传入i2c设备
struct i2c_msg msg;
u8 b[256];
b[0] = reg;
memcpy(&b[1],buf,len);
msg.addr = client->addr;
msg.flags = 0;
msg.buf = b;
msg.len = len+1; //要发送的数据,加了一个寄存器地址,所以长度加1
return i2c_transfer(client->adapter,&msg,1);
}
OK,这样就已经实现了我们的i2c的发送和接收了。
那么接下来就是读取ap3216c传感器的值了。
查看芯片手册。
可以看到有六个寄存器存放着数据。
还有一个系统配置寄存器。
我们先初始化ap3216c。
tatic int ap3216c_open(struct inode *inode, struct file *filp)
{
filp->private_data = &ap32dev;// 传入私有数据
//初始化ap3216c
ap3216c_write_reg(&ap32dev,AP3216C_SYSTEMCONG,0x04);//复位
mdelay(50);
ap3216c_write_reg(&ap32dev,AP3216C_SYSTEMCONG,0x03);//设置读取三个传感器的值
return 0;
}
然后根据芯片手册,分别读取传感器的值。
void ap3216c_readdate(struct ap3216c_dev *dev)
{
unsigned char i = 0;
unsigned char buf[6];
//循环读取传感器的值
for(i=0;i<6;i++){
buf[i]=ap3216c_read_reg(dev,AP3216C_IRDATALOW + i);
}
if(buf[0] & 0x80)//数据无效
{
dev->ir = 0;
}else{
dev->ir = (((unsigned short)buf[1]<<2) | (buf[0]& 0x03));
}
dev->als = ((unsigned short)buf[3]<<8 | buf[2]);
if(buf[4]& 0x40)
dev->ps = 0;
else
dev->ps = (((unsigned short)(buf[5]&0x3F)<<4) | (buf[4]&0x0F));
}
都是根据手册进行一些运算,最后得到数值,没啥可说的。
最后在read函数里面用copy_to_user传给用户空间。
static ssize_t ap3216c_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *ppos)
{
short data[3];//数据是16位的
long err = 0;
struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
ap3216c_readdate(dev);
data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;
err = copy_to_user(buf,data,sizeof(data));
return 0;
}
驱动就编写好了!!!
是不是还蛮简单的!
最后我们测试一下。
可以看到是有数据的。
拿手机照一下。
数值明显增加!!
用手靠近一下。
也可以看到数值变化!
成功!!
参考正点原子linux驱动开发教程