本文主要是介绍STM32mp157驱动开发--I2C驱动开发实验,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
之前有做过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;}//初始化cdevap32dev.cdev.owner = THIS_MODULE;cdev_init(&ap32dev.cdev, &ap3216c_ops);//添加一个cdevret = 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; //要发送的地址长度为1msg[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; //要发送的数据,加了一个寄存器地址,所以长度加1return i2c_transfer(client->adapter,&msg,1);}
OK,这样就已经实现了我们的i2c的发送和接收了。
那么接下来就是读取ap3216c传感器的值了。
查看芯片手册。
可以看到有六个寄存器存放着数据。
还有一个系统配置寄存器。
我们先初始化ap3216c。
tatic int ap3216c_open(struct inode *inode, struct file *filp)
{filp->private_data = &ap32dev;// 传入私有数据//初始化ap3216cap3216c_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;elsedev->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驱动开发教程
这篇关于STM32mp157驱动开发--I2C驱动开发实验的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!