相关文章:正点原子教程第四十章——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 = ® /* 读取的首地址 */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=%06x 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 这三个数据值,并输出到终端。