STM32mp157驱动开发--I2C驱动开发实验

2023-11-02 02:31

本文主要是介绍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;}//初始化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)
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;            //要发送的地址长度为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驱动开发实验的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/327479

相关文章

使用Python开发一个带EPUB转换功能的Markdown编辑器

《使用Python开发一个带EPUB转换功能的Markdown编辑器》Markdown因其简单易用和强大的格式支持,成为了写作者、开发者及内容创作者的首选格式,本文将通过Python开发一个Markd... 目录应用概览代码结构与核心组件1. 初始化与布局 (__init__)2. 工具栏 (setup_t

Spring Shell 命令行实现交互式Shell应用开发

《SpringShell命令行实现交互式Shell应用开发》本文主要介绍了SpringShell命令行实现交互式Shell应用开发,能够帮助开发者快速构建功能丰富的命令行应用程序,具有一定的参考价... 目录引言一、Spring Shell概述二、创建命令类三、命令参数处理四、命令分组与帮助系统五、自定义S

Python通过模块化开发优化代码的技巧分享

《Python通过模块化开发优化代码的技巧分享》模块化开发就是把代码拆成一个个“零件”,该封装封装,该拆分拆分,下面小编就来和大家简单聊聊python如何用模块化开发进行代码优化吧... 目录什么是模块化开发如何拆分代码改进版:拆分成模块让模块更强大:使用 __init__.py你一定会遇到的问题模www.

Spring Security基于数据库的ABAC属性权限模型实战开发教程

《SpringSecurity基于数据库的ABAC属性权限模型实战开发教程》:本文主要介绍SpringSecurity基于数据库的ABAC属性权限模型实战开发教程,本文给大家介绍的非常详细,对大... 目录1. 前言2. 权限决策依据RBACABAC综合对比3. 数据库表结构说明4. 实战开始5. MyBA

使用Python开发一个简单的本地图片服务器

《使用Python开发一个简单的本地图片服务器》本文介绍了如何结合wxPython构建的图形用户界面GUI和Python内建的Web服务器功能,在本地网络中搭建一个私人的,即开即用的网页相册,文中的示... 目录项目目标核心技术栈代码深度解析完整代码工作流程主要功能与优势潜在改进与思考运行结果总结你是否曾经

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

Python基于wxPython和FFmpeg开发一个视频标签工具

《Python基于wxPython和FFmpeg开发一个视频标签工具》在当今数字媒体时代,视频内容的管理和标记变得越来越重要,无论是研究人员需要对实验视频进行时间点标记,还是个人用户希望对家庭视频进行... 目录引言1. 应用概述2. 技术栈分析2.1 核心库和模块2.2 wxpython作为GUI选择的优

利用Python开发Markdown表格结构转换为Excel工具

《利用Python开发Markdown表格结构转换为Excel工具》在数据管理和文档编写过程中,我们经常使用Markdown来记录表格数据,但它没有Excel使用方便,所以本文将使用Python编写一... 目录1.完整代码2. 项目概述3. 代码解析3.1 依赖库3.2 GUI 设计3.3 解析 Mark

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

基于Python开发批量提取Excel图片的小工具

《基于Python开发批量提取Excel图片的小工具》这篇文章主要为大家详细介绍了如何使用Python中的openpyxl库开发一个小工具,可以实现批量提取Excel图片,有需要的小伙伴可以参考一下... 目前有一个需求,就是批量读取当前目录下所有文件夹里的Excel文件,去获取出Excel文件中的图片,并