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

相关文章

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【区块链 + 人才服务】区块链集成开发平台 | FISCO BCOS应用案例

随着区块链技术的快速发展,越来越多的企业开始将其应用于实际业务中。然而,区块链技术的专业性使得其集成开发成为一项挑战。针对此,广东中创智慧科技有限公司基于国产开源联盟链 FISCO BCOS 推出了区块链集成开发平台。该平台基于区块链技术,提供一套全面的区块链开发工具和开发环境,支持开发者快速开发和部署区块链应用。此外,该平台还可以提供一套全面的区块链开发教程和文档,帮助开发者快速上手区块链开发。

Vue3项目开发——新闻发布管理系统(六)

文章目录 八、首页设计开发1、页面设计2、登录访问拦截实现3、用户基本信息显示①封装用户基本信息获取接口②用户基本信息存储③用户基本信息调用④用户基本信息动态渲染 4、退出功能实现①注册点击事件②添加退出功能③数据清理 5、代码下载 八、首页设计开发 登录成功后,系统就进入了首页。接下来,也就进行首页的开发了。 1、页面设计 系统页面主要分为三部分,左侧为系统的菜单栏,右侧

v0.dev快速开发

探索v0.dev:次世代开发者之利器 今之技艺日新月异,开发者之工具亦随之进步不辍。v0.dev者,新兴之开发者利器也,迅速引起众多开发者之瞩目。本文将引汝探究v0.dev之基本功能与优势,助汝速速上手,提升开发之效率。 何谓v0.dev? v0.dev者,现代化之开发者工具也,旨在简化并加速软件开发之过程。其集多种功能于一体,助开发者高效编写、测试及部署代码。无论汝为前端开发者、后端开发者

pico2 开发环境搭建-基于ubuntu

pico2 开发环境搭建-基于ubuntu 安装编译工具链下载sdk 和example编译example 安装编译工具链 sudo apt install cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib 注意cmake的版本,需要在3.17 以上 下载sdk 和ex