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设备。

写时序

写时序比较简单,它是将寄存器地址和数据一起发送的。

  1. 开始信号
  2. 发送从机地址,其中高七位是设备地址,最后一位是读写位
  3. 从机发送应答信号
  4. 重新发送开始信号
  5. 发送要写入数据的寄存器地址
  6. 从机发送应答信号
  7. 发送要写入寄存器的数据
  8. 从机发送应答信号
  9. 停止信号

  图来自于正点原子驱动开发教程

读时序

读时序相对来说比较复杂。结束时多了一个非应答信号,以及写入寄存器地址之后要重新发从机的地址

总体分为四步。

  1. 发送设备地址
  2. 发送要读取的寄存器地址
  3. 重新发送设备地址
  4. 读取数据

   图来自于正点原子驱动开发教程

大致了解了一下,下面来做个实验验证一下吧。

这里要使用到的是一个传感器: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)
adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。
msgs:I2C 要发送的一个或多个消息。
num:消息数量,也就是 msgs 的数量。
返回值:负值,失败,其他非负值,发送的 msgs 数量。
接下来的一切都是根据这个函数进行编写,其实自带的函数也是要调用 i2c_transfer的。
先看msgs这个结构体。
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 = &reg;         //要读取的寄存器,也是此时发送的数据
    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驱动开发教程

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值