• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

STM32MP157驱动开发——Linux I2C驱动

武飞扬头像
Amonter
帮助5

相关文章:正点原子教程第四十章——Linux I2C驱动实验

0.前言

  为了简化笔记的编写以及降低工作量,本节开始相关的基础知识部分通过引入原子哥的教材链接来完成,有兴趣的可以进入学习。
  上一节学完 RGB LCD 本来想直接学习 RGB 转 HDMI 实验,但是转换芯片需要一个 I2C 引脚来控制芯片功能,所以还是回来先学习 I2C。

一、原理简述

学新通
SCL:串行时钟线
SDA:串行数据线
标准速度:100Kb/秒
快速模式:400Kb/秒
需要上拉电阻(通常4.7k),空闲时均处于高电平。

1.起始位

SCL 为高电平时,SDA 出现下降沿

2.停止位

SCL 为高电平时,SDA 出现上升沿

3.数据传输

SDA 上的数据变化只能在 SCL 低电平期间发生,这样是为了保证 SCL 高电平时 SDA的数据稳定。
学新通

4.应答信号

由从机发出,但时钟由主机提供。从机通过将 SDA 拉低来表示发出应答信号。

5.I2C写时序

起始信号–>发送 I2C 设备地址–>从机发送应答信号–>重新发送起始信号–>发送写入数据的寄存器地址–>从机应答–>发送写入数据–>从机应答–>停止信号
其中 “发送 I2C 设备地址” 数据时,高7位为设备地址,最后一位为 1 :读操作;0:写操作。

6.I2C读时序

起始信号–>发送 I2C 设备地址(最后一位置1)–>从机发送应答信号–>重新发送起始信号–>发送读取数据的寄存器地址–>从机应答
前半部分仍为先写入数据。后半部分有所不同:
–>重新发送起始信号–>重新发送 I2C 从设备地址(最后一位置0)–>从机应答–>在从机的返回数据里读取–>主机发送 NO ACK,完成读取(从机不应答)–>停止信号

二、设备驱动开发

1.不使用设备树

通过 i2c_board_info 结构体来描述设备:

406 struct i2c_board_info {
407 	char type[I2C_NAME_SIZE];
408 	unsigned short flags;
409 	unsigned short addr;
410 	const char *dev_name;
411 	void *platform_data;
412 	struct device_node *of_node;
413 	struct fwnode_handle *fwnode;
414 	const struct property_entry *properties;
415 	const struct resource *resources;
416 	unsigned int num_resources;
417 	int irq;
418 };

其中 type 和 addr 这两个成员变量必须要设置,一个是 I2C 设备的名字,一个是 I2C 设备的器件地址。
例:arch/arm/mach-imx/mach-armadillo5x0.c 文件中有关 I2C 设备 s35390a 的信息:

246 static struct i2c_board_info armadillo5x0_i2c_rtc = {
247 	I2C_BOARD_INFO("s35390a", 0x30),
248 };

#define I2C_BOARD_INFO(dev_type, dev_addr) \
		.type = dev_type, .addr = (dev_addr)

使用 I2C_BOARD_INFO 来完成 armadillo5x0_i2c_rtc 的初始化,I2C_BOARD_INFO 宏设置 i2c_board_info 的 type 和 addr 这两个成员变量。

2.使用设备树

通过在设备树中创建 I2C 节点来实现。在 STM32MP157 开发板上有一个 I2C 器件 AP3216C,以此为例。
原理图:
学新通
AP3216C 使用了 I2C5,I2C5_SCL 使用的是 PA11,I2C_SDA 使用的是 PA12。 AP3216C 还有个中断引脚,这里没有用到。

1.修改设备树

在 stm32mp15-pinctrl.dtsi 中,将 PA11、PA12 复用为 I2C:(ST官方已写好)
学新通

2.追加子节点

在 stm32mp157d-atk.dts 文件中,添加一个 i2c5 节点,并在节点中添加“ap3216c@1e”子节点:

&i2c5 {
	pinctrl-names = "default", "sleep";
	pinctrl-0 = <&i2c5_pins_a>;
	pinctrl-1 = <&i2c5_pins_sleep_a>;
	status = "okay";

	ap3216c@1e {
		compatible = "alientek,ap3216c";
		reg = <0x1e>;
	};
};

其中 @ 后面的“1e”是 ap3216c 的器件地址,reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。
设备树修改完成以后使用“make dtbs”重新编译,然后使用新的设备树启动 Linux 内核,如果设备树修改正确,会在 /sys/bus/i2c/devices 目录下看到一个名为“0-001e”的子目录,进入目录就可以看到名为"name"的文件,name 文件保存着此设备名字。
注意:在开发完 RGB LCD 驱动之后,每次编译设备树需要使用 make uImage dtbs LOADADDR=0xC2000040 -j8命令,不然有可能会报一些奇奇怪怪的Warning。暂时还没发现这些warning会产生什么影响,后续发现了来填上。
学新通

3.驱动编写

I2C 总线使用的是 I2C 子系统,与 platform 平台驱动的开发流程相似,都是将某一类总线驱动统一为某一个系统的驱动框架,减少冗余代码和冗余开发流程。
新建"21_iic"文件夹,在里面创建 ap3216c.c 和 ap3216creg.h 两个文件,ap3216c.c 为 AP3216C 的驱动代码,ap3216creg.h 是 AP3216C 寄存器头文件。先在 ap3216creg.h 中定义好 AP3216C 的寄存器,输入如下内容:

#ifndef AP3216C_H
#define AP3216C_H

#define AP3216C_ADDR 0X1E /* AP3216C 器件地址 */

/* AP3316C 寄存器 */
#define AP3216C_SYSTEMCONG 0x00 /* 配置寄存器 */
#define AP3216C_INTSTATUS 0X01 /* 中断状态寄存器 */
#define AP3216C_INTCLEAR 0X02 /* 中断清除寄存器 */
#define AP3216C_IRDATALOW 0x0A /* IR 数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR 数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS 数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS 数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS 数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS 数据高字节 */

#endif
学新通

ap3216c.c:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"

#define AP3216C_CNT 1
#define AP3216C_NAME "ap3216c"

struct ap3216c_dev {
    struct i2c_client *client; /* i2c 设备 */
    dev_t devid; /* 设备号 */
    struct cdev cdev; /* cdev */
    struct class *class; /* 类 */
    struct device *device; /* 设备 */
    struct device_node *nd; /* 设备节点 */
    unsigned short ir, als, ps; /* 三个光传感器数据 */
};

/*读取多个寄存器*/
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->client;

    /* msg[0]为发送要读取的首地址 */
    msg[0].addr = client->addr; /* ap3216c 地址 */
    msg[0].flags = 0; /* 标记为发送数据 */
    msg[0].buf = &reg; /* 读取的首地址 */
    msg[0].len = 1; /* reg 长度 */

    /* msg[1]读取数据 */
    msg[1].addr = client->addr; /* ap3216c 地址 */
    msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
    msg[1].buf = val; /* 读取数据缓冲区 */
    msg[1].len = len; /* 要读取的数据长度 */

    ret = i2c_transfer(client->adapter, msg, 2);
    if(ret == 2) {
        ret = 0;
    } else {
        printk("i2c rd failed=%d reg=x len=%d\n",ret, reg, len);
        ret = -EREMOTEIO;
    }
    return ret;
}

/*向多个寄存器写入数据*/
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev->client;
    b[0] = reg; /* 寄存器首地址 */
    memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组 b 里面 */

    msg.addr = client->addr; /* ap3216c 地址 */
    msg.flags = 0; /* 标记为写数据 */

    msg.buf = b; /* 要写入的数据缓冲区 */
    msg.len = len   1; /* 要写入的数据长度 */

    return i2c_transfer(client->adapter, &msg, 1);
}

/*读取 ap3216c 指定寄存器值,读取一个寄存器*/
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
    u8 data = 0;

    ap3216c_read_regs(dev, reg, &data, 1);
    return data;
}

/*向 ap3216c 指定寄存器写入指定的值,写一个寄存器*/
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
    u8 buf = 0;
    buf = data;
    ap3216c_write_regs(dev, reg, &buf, 1);
}

/*
* 读取 AP3216C 的数据,包括 ALS,PS 和 IR, 注意!如果同时
* 打开 ALS,IR PS 两次数据读取的时间间隔要大于 112.5ms
*/
void ap3216c_readdata(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) /* IR_OF 位为 1,则数据无效 */
        dev->ir = 0;
    else /* 读取 IR 传感器的数据 */
        dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);

    dev->als = ((unsigned short)buf[3] << 8) | buf[2];

    if(buf[4] & 0x40) /* IR_OF 位为 1,则数据无效 */
        dev->ps = 0;
    else /* 读取 PS 传感器的数据 */
        dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}

/*打开设备*/
static int ap3216c_open(struct inode *inode, struct file *filp)
{
/* 从 file 结构体获取 cdev 指针, 再根据 cdev 获取 ap3216c_dev 首地址 */
    struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
    struct ap3216c_dev *ap3216cdev = container_of(cdev, struct ap3216c_dev, cdev);

/* 初始化 AP3216C */
    ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x04);
    mdelay(50);
    ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0X03);
    return 0;
}

/*从设备读取数据*/
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
    short data[3];
    long err = 0;

    struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
    struct ap3216c_dev *dev = container_of(cdev, struct ap3216c_dev, cdev);

    ap3216c_readdata(dev);

    data[0] = dev->ir;
    data[1] = dev->als;
    data[2] = dev->ps;
    err = copy_to_user(buf, data, sizeof(data));
    return 0;
}

/*关闭/释放设备*/
static int ap3216c_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* AP3216C 操作函数 */
static const struct file_operations ap3216c_ops = {
    .owner = THIS_MODULE,
    .open = ap3216c_open,
    .read = ap3216c_read,
    .release = ap3216c_release,
};

/*probe函数*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;
    struct ap3216c_dev *ap3216cdev;


    ap3216cdev = devm_kzalloc(&client->dev, sizeof(*ap3216cdev), GFP_KERNEL);
    if(!ap3216cdev)
        return -ENOMEM;

    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    ret = alloc_chrdev_region(&ap3216cdev->devid, 0, AP3216C_CNT, AP3216C_NAME);
    if(ret < 0) {
        pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", AP3216C_NAME, ret);
        return -ENOMEM;
    }

    /* 2、初始化 cdev */
    ap3216cdev->cdev.owner = THIS_MODULE;
    cdev_init(&ap3216cdev->cdev, &ap3216c_ops);

    /* 3、添加一个 cdev */
    ret = cdev_add(&ap3216cdev->cdev, ap3216cdev->devid, AP3216C_CNT);
    if(ret < 0) {
        goto del_unregister;
    }

    /* 4、创建类 */
    ap3216cdev->class = class_create(THIS_MODULE, AP3216C_NAME);
    if (IS_ERR(ap3216cdev->class)) {
        goto del_cdev;
    }

    /* 5、创建设备 */
    ap3216cdev->device = device_create(ap3216cdev->class, NULL, ap3216cdev->devid, NULL, AP3216C_NAME);
    if (IS_ERR(ap3216cdev->device)) {
        goto destroy_class;
    }
    ap3216cdev->client = client;
    /* 保存 ap3216cdev 结构体 */
    i2c_set_clientdata(client,ap3216cdev);

    return 0;
destroy_class:
    device_destroy(ap3216cdev->class, ap3216cdev->devid);
del_cdev:
    cdev_del(&ap3216cdev->cdev);
del_unregister:
    unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
    return -EIO;
}

/*i2c 驱动的 remove 函数*/
static int ap3216c_remove(struct i2c_client *client)
{
    struct ap3216c_dev *ap3216cdev = i2c_get_clientdata(client);
    /* 注销字符设备驱动 */
    /* 1、删除 cdev */
    cdev_del(&ap3216cdev->cdev);
    /* 2、注销设备号 */
    unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
    /* 3、注销设备 */
    device_destroy(ap3216cdev->class, ap3216cdev->devid);
    /* 4、注销类 */
    class_destroy(ap3216cdev->class);
    return 0;
}

/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id ap3216c_id[] = {
    {"alientek,ap3216c", 0},
    {}
};

/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
    { .compatible = "alientek,ap3216c" },
    { /* Sentinel */ }
};

/* i2c 驱动结构体 */
static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = "ap3216c",
        .of_match_table = ap3216c_of_match,
    },
    .id_table = ap3216c_id,
};

/*驱动入口函数*/
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_i2c_driver(ap3216c_driver) */
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("amonter");
MODULE_INFO(intree, "Y");
学新通

①自定义一个 ap3216c_dev 结构体,其中的client 成员变量用来存储从设备树提供的 i2c_client 结构体,ir、als 和 ps 分别存储 AP3216C 的 IR、ALS 和 PS数据
②ap3216c_read_regs 函数实现多字节读取,但是 AP3216C 好像不支持连续多字节读取,此函数在测试其他 I2C 设备的时候可以实现多给字节连续读取,但是在 AP3216C上不能连续读取多个字节, 不过读取一个字节没有问题的
③ap3216c_write_regs 函数实现连续多字节写操作
④ap3216c_read_reg 函数用于读取 AP3216C 的指定寄存器数据,用于一个寄存器的数据读取
⑤ap3216c_write_reg 函数用于向 AP3216C 的指定寄存器写入数据,用于一个寄存器的数据写操作
⑥ap3216c_readdata 读取 AP3216C 的 PS、 ALS 和 IR 等传感器原始数据值
⑦open、read、release、probe等就是标准的 iic 设备驱动框架,是基于字符驱动的一层封装。
ap3216c_dev 结构体里有一个 cdev 的变量成员,open函数中使用filp->f_path.dentry->d_inode->i_cdev获取这个成员变量的地址,再用 container_of 获取ap3216c_dev的首地址。
probe函数中i2c_set_clientdata 函数将 ap3216cdev 变量的地址绑定到 client,进行绑定之后,可以通过 i2c_get_clientdata 来获取 ap3216cdev 变量指针。
remove函数调用 i2c_get_clientdata 函数来得到 ap3216cdev 变量的地址,后面执行的一系列卸载、注销操作。

测试App:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/*
 * @description : main 主程序
 * @param - argc : argv 数组元素个数
 * @param - argv : 具体参数
 * @return : 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd;
    char *filename;
    unsigned short databuf[3];
    unsigned short ir, als, ps;
    int ret = 0;

    if (argc != 2)
    {
        printf("Error Usage!\r\n");
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("can't open file %s\r\n", filename);
        return -1;
    }

    while (1)
    {
        ret = read(fd, databuf, sizeof(databuf));
        if (ret == 0)
        {                     /* 数据读取成功 */
            ir = databuf[0];  /* ir 传感器数据 */
            als = databuf[1]; /* als 传感器数据 */
            ps = databuf[2];  /* ps 传感器数据 */
            printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
        }
        usleep(200000); /*100ms */
    }
    close(fd); /* 关闭文件 */
    return 0;
}
学新通

在 while 循环中不断的读取 AP3216C 的设备文件,从而得到 ir、 als 和 ps 这三个数据值,并输出到终端。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhejif
系列文章
更多 icon
同类精品
更多 icon
继续加载