嵌入式Linux I2C驱动开发
嵌入式Linux I2C驱动开发
1. I2C总线协议基础
1.1 I2C总线概述
I2C(Inter-Integrated Circuit)总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线即可在连接于总线上的器件之间传送信息:串行数据线(SDA)和串行时钟线(SCL)。
I2C总线的主要特点:
- 简单的双线接口(SDA和SCL)
- 支持多主控和多从设备
- 支持不同速率模式(标准模式100kbps,快速模式400kbps,高速模式3.4Mbps)
- 设备通过唯一的7位或10位地址进行寻址
- 支持数据流控制和仲裁机制
1.2 I2C通信协议
I2C通信的基本单位是字节,数据传输以字节为单位进行。通信过程包含以下几个关键要素:
起始和停止条件:
- 起始条件:当SCL为高电平时,SDA从高电平向低电平跳变
- 停止条件:当SCL为高电平时,SDA从低电平向高电平跳变
数据有效性:
- SDA线上的数据必须在SCL高电平期间保持稳定
- 数据在SCL低电平期间可以改变
应答机制:
- 每传输一个字节后,接收方必须发送一个应答位(ACK)
- 应答位为低电平表示应答,高电平表示非应答(NACK)
1.3 I2C数据传输格式
I2C数据传输遵循特定的格式:
-
主设备发送数据到从设备:
- 主设备发送起始条件
- 主设备发送从设备地址(7位)+ 写位(0)
- 从设备应答
- 主设备发送寄存器地址
- 从设备应答
- 主设备发送数据
- 从设备应答
- 主设备发送停止条件
-
主设备从从设备读取数据:
- 主设备发送起始条件
- 主设备发送从设备地址(7位)+ 写位(0)
- 从设备应答
- 主设备发送寄存器地址
- 从设备应答
- 主设备发送重复起始条件
- 主设备发送从设备地址(7位)+ 读位(1)
- 从设备应答
- 从设备发送数据
- 主设备应答(除最后一个字节外)
- 主设备发送非应答(NACK)和停止条件
2. Linux内核I2C子系统架构
2.1 I2C子系统层次结构
Linux内核中的I2C子系统采用分层架构设计,主要包含以下层次:
-
I2C核心层(I2C Core):
- 提供统一的API接口
- 管理I2C适配器和I2C设备
- 处理设备注册和注销
- 提供I2C通信的基本功能
-
I2C适配器层(I2C Adapter):
- 对应具体的I2C控制器硬件
- 实现底层硬件操作
- 提供与I2C核心层的接口
-
I2C设备驱动层(I2C Device Driver):
- 针对具体的I2C设备
- 实现设备特定的功能
- 通过I2C核心层与硬件交互
2.2 I2C核心数据结构
Linux内核中定义了几个关键的数据结构来管理I2C系统:
struct i2c_adapter:
struct i2c_adapter {struct module *owner;unsigned int class; /* classes to allow probing for */const struct i2c_algorithm *algo; /* the algorithm to access the bus */void *algo_data;/* --- administration stuff. */int (*client_register)(struct i2c_client *);int (*client_unregister)(struct i2c_client *);/* --- i2c_adapter is shared between several drivers */struct mutex bus_lock;/* --- settings related to the transaction */u32 timeout; /* in jiffies */u32 retries;struct device dev; /* the adapter device */int nr;char name[48];struct completion dev_released;
};
struct i2c_algorithm:
struct i2c_algorithm {int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,int num);int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,unsigned short flags, char read_write,u8 command, int size, union i2c_smbus_data *data);/* To determine what the adapter supports */u32 (*functionality)(struct i2c_adapter *);
};
struct i2c_client:
struct i2c_client {unsigned short flags; /* div., see below */unsigned short addr; /* chip address - NOTE: 7bit */char name[I2C_NAME_SIZE];struct i2c_adapter *adapter; /* the adapter we sit on */struct i2c_driver *driver; /* and our access routines */struct device dev; /* the device structure */int irq; /* irq issued by device (or -1) */struct list_head detected;
};
struct i2c_driver:
struct i2c_driver {unsigned int class;/* Standard driver model interfaces */int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);int (*remove)(struct i2c_client *client);/* driver model interfaces that don't relate to enumeration */void (*shutdown)(struct i2c_client *client);int (*suspend)(struct i2c_client *client, pm_message_t mesg);int (*resume)(struct i2c_client *client);/* Old style "attach" slot, for use by drivers that don't use hot* plugging*/int (*attach_adapter)(struct i2c_adapter *adapter);/* a ioctl like command that can be used to perform specific functions* with the device.*/int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);struct device_driver driver;const struct i2c_device_id *id_table;/* Device detection callback for automatic device creation */int (*detect)(struct i2c_client *client, struct i2c_board_info *info);const unsigned short *address_list;struct list_head clients;
};
3. I2C设备驱动开发详解
3.1 I2C设备驱动框架
I2C设备驱动遵循Linux内核的标准驱动框架,主要包括以下几个部分:
- 模块初始化和退出函数
- I2C驱动结构体定义
- probe和remove函数实现
- 文件操作函数实现
3.2 AP3216C传感器介绍
AP3216C是一款集成的环境光传感器、接近传感器和红外传感器模块。它通过I2C接口与主控制器通信,提供以下功能:
- 环境光传感器(ALS):测量环境光照强度
- 接近传感器(PS):检测物体接近程度
- 红外传感器(IR):测量红外光强度
AP3216C的主要特性:
- 工作电压:2.4V-3.6V
- I2C地址:0x1E(7位地址)
- 通信速率:支持标准模式(100kbps)和快速模式(400kbps)
- 低功耗设计
- 集成ADC和数字信号处理
3.3 驱动代码详细分析
3.3.1 头文件包含和宏定义
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/delay.h>#include "ap3216creg.h"
这些头文件包含了驱动开发所需的基本功能:
<linux/module.h>
:模块相关功能<linux/kernel.h>
:内核基本定义<linux/init.h>
:初始化相关宏<linux/fs.h>
:文件系统相关功能<asm/io.h>
:I/O操作<asm/uaccess.h>
:用户空间访问<linux/cdev.h>
:字符设备相关<linux/device.h>
:设备模型<linux/of.h>
:设备树相关<linux/slab.h>
:内存分配<linux/gpio.h>
:GPIO操作<linux/i2c.h>
:I2C相关功能<linux/delay.h>
:延时函数
3.3.2 设备结构体定义
struct ap3216c_dev
{dev_t devid;int major;int minor;struct cdev cdev;struct class *class;struct device *device;struct device_node *node;void *private_data;unsigned short ir, als, ps;
};
struct ap3216c_dev ap3216cdev;
这个结构体定义了AP3216C设备的所有相关信息:
devid
:设备号major
和minor
:主次设备号cdev
:字符设备结构体class
和device
:设备模型相关node
:设备树节点private_data
:私有数据,用于存储i2c_client指针ir
、als
、ps
:存储传感器读取的数据
3.3.3 I2C读写函数实现
ap3216c_read_regs函数:
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{struct i2c_client *client = dev->private_data;struct i2c_msg msg[] = {{.addr = client->addr,.flags = 0,.buf = ®,.len = 1,},{.addr = client->addr,.flags = I2C_M_RD,.buf = val,.len = len,}};return i2c_transfer(client->adapter, msg, 2);
}
这个函数实现了多字节读取功能:
- 创建两个i2c_msg结构体,第一个用于发送寄存器地址,第二个用于读取数据
- 使用i2c_transfer函数执行I2C传输
- 返回传输结果
ap3216c_write_regs函数:
static int ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, int len)
{u8 b[256];struct i2c_msg msg;struct i2c_client *client;client = dev->private_data;b[0] = reg;memcpy(&b[1], buf, len);msg.addr = client->addr;msg.flags = 0;msg.buf = b;msg.len = len + 1;return i2c_transfer(client->adapter, &msg, 1);
}
这个函数实现了多字节写入功能:
- 将寄存器地址和数据合并到一个缓冲区中
- 创建一个i2c_msg结构体
- 使用i2c_transfer函数执行I2C传输
ap3216c_read_reg和ap3216c_write_reg函数:
这两个函数是单字节读写函数的封装,分别调用多字节读写函数实现单字节操作。
3.3.4 文件操作函数实现
ap3216c_open函数:
static int
ap3216c_open(struct inode *inode, struct file *filp)
{u8 data = 0;filp->private_data = &ap3216cdev;ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x4);mdelay(50);ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x3);ap3216c_read_reg(&ap3216cdev, AP3216C_SYSTEMCONG, &data);return 0;
}
这个函数在设备打开时被调用:
- 设置文件私有数据
- 向AP3216C写入配置值0x4,进入软件复位状态
- 延时50ms
- 写入配置值0x3,进入正常工作状态
ap3216c_readdata函数:
void ap3216c_readdata(struct ap3216c_dev *dev)
{unsigned char buf[6];unsigned char i;/* 循环读取所有传感器数据 */for (i = 0; i < 6; i++){ap3216c_read_reg(dev, AP3216C_IRDATALOW + i, &buf[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]; /* 读取ALS传感器的数据 */if (buf[4] & 0x40) /* IR_OF位为1,则数据无效 */dev->ps = 0;else /* 读取PS传感器的数据 */dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}
这个函数读取所有传感器数据:
- 从AP3216C_IRDATALOW开始连续读取6个寄存器的数据
- 根据数据格式解析IR、ALS和PS传感器的值
- 对于IR和PS传感器,需要检查数据有效性标志位
ap3216c_read函数:
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *loff)
{unsigned short data[3], ret = 0;struct ap3216c_dev *dev = filp->private_data;ap3216c_readdata(dev);data[0] = dev->ir;data[1] = dev->ps;data[2] = dev->als;printk("User: ir %d, ps %d, als %d\r\n", data[0], data[1], data[2]);ret = copy_to_user(buf, data, sizeof(data));if (ret){ret = -EINVAL;printk("Driver: copy_to_user");}return sizeof(data);
}
这个函数实现了read系统调用:
- 调用ap3216c_readdata读取传感器数据
- 将数据复制到用户空间
- 返回读取的数据大小
3.3.5 I2C驱动结构体实现
probe函数:
static int
ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{/* 1、设置设备号 */if (ap3216cdev.major){ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);}else{alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);ap3216cdev.major = MAJOR(ap3216cdev.devid);}/* 2、注册设备 */cdev_init(&ap3216cdev.cdev, &ap3216c_fops);cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);/* 3、创建类 */ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);if (IS_ERR(ap3216cdev.class)){return PTR_ERR(ap3216cdev.class);}/* 4、创建设备 */ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);if (IS_ERR(ap3216cdev.device)){return PTR_ERR(ap3216cdev.device);}ap3216cdev.private_data = client;return 0;
}
probe函数在设备匹配成功时被调用:
- 分配和注册字符设备号
- 初始化和添加字符设备
- 创建设备类和设备节点
- 保存i2c_client指针到私有数据
remove函数:
static int ap3216c_remove(struct i2c_client *client)
{cdev_del(&ap3216cdev.cdev); /* 删除cdev */unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT); /* 注销设备号 */device_destroy(ap3216cdev.class, ap3216cdev.devid);class_destroy(ap3216cdev.class);return 0;
}
remove函数在设备移除时被调用:
- 删除字符设备
- 注销设备号
- 销毁设备和类
3.3.6 I2C设备ID和设备树匹配表
static struct i2c_device_id ap3216c_i2c_id[] = {{"alientek,ap3216c", 0},{}};static struct of_device_id ap3216c_of_id[] = {{.compatible = "alientek,ap3216c",},{/* sentinel */}};
这两个表用于设备匹配:
- i2c_device_id用于传统的I2C设备匹配
- of_device_id用于设备树匹配
3.3.7 I2C驱动注册
struct i2c_driver ap3216c_driver = {.probe = ap3216c_probe,.remove = ap3216c_remove,.id_table = ap3216c_i2c_id,.driver = {.name = "ap3216c",.owner = THIS_MODULE,.of_match_table = of_match_ptr(ap3216c_of_id),},
};static int __init
ap3216c_init(void)
{int ret = 0;ret = i2c_add_driver(&ap3216c_driver);if (ret < 0){ret = -EINVAL;goto fail_i2c_add;}return 0;
fail_i2c_add:printk("Driver: fail_i2c_add\r\n");return 0;
}static void __exit
ap3216c_exit(void)
{i2c_del_driver(&ap3216c_driver);
}module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("alientek");
驱动的初始化和退出函数:
- 使用i2c_add_driver注册I2C驱动
- 使用i2c_del_driver注销I2C驱动
- 使用module_init和module_exit宏指定初始化和退出函数
4. 设备树配置详解
4.1 设备树基本概念
设备树(Device Tree)是一种描述硬件配置的数据结构,它将硬件信息从内核代码中分离出来,使得同一个内核可以支持不同的硬件平台而无需重新编译。
4.2 I2C总线设备树配置
在提供的设备树文件imx6ull-alientek-emmc.dts中,I2C总线的配置如下:
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";ap3216c@1e {compatible = "alientek,ap3216c";reg = <0x1e>;};
};
clock-frequency:设置I2C总线的时钟频率为100kHz,对应标准模式。
pinctrl-names和pinctrl-0:配置I2C引脚的复用功能和电气特性。
status:设置为"okay"表示启用该I2C总线。
ap3216c@1e节点:
- 节点名称:ap3216c@1e,其中1e是设备的I2C地址
- compatible:匹配字符串,用于驱动匹配
- reg:设备在I2C总线上的地址
4.3 引脚复用配置
pinctrl_i2c1: i2c1grp {fsl,pins = <MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0>;
};
这段配置将UART4的TX和RX引脚复用为I2C1的SCL和SDA:
- MX6UL_PAD_UART4_TX_DATA__I2C1_SCL:将UART4_TX_DATA引脚复用为I2C1_SCL
- MX6UL_PAD_UART4_RX_DATA__I2C1_SDA:将UART4_RX_DATA引脚复用为I2C1_SDA
- 0x4001b8b0:引脚配置参数,包含驱动强度、上拉/下拉等设置
5. 应用程序开发
5.1 用户空间程序分析
int main(int argc, char *argv[])
{int cnt = 0;if (argc != 2){fprintf(stderr, "Usage: %s <led_device> <0|1>\n", argv[0]);return -1;}char *fileanme;unsigned short databuf[3];fileanme = argv[1];int fd = 0;fd = open(fileanme, O_RDWR);if (fd < 0){perror("User: open file device error\r\n");return -1;}while (1){int ret = read(fd, databuf, sizeof(databuf));if (ret > 0){printf("User: ir %d, ps %d, als %d\r\n", databuf[0], databuf[1], databuf[2]);}// sleep(200);}close(fd);return 0;
}
这个应用程序的主要功能:
- 检查命令行参数
- 打开设备文件
- 循环读取传感器数据
- 打印读取到的数据
- 关闭设备文件
5.2 编译和运行
-
编译驱动:
make
-
加载驱动:
insmod ap3216c.ko
-
编译应用程序:
gcc ap3216cAPP.c -o ap3216cAPP
-
运行应用程序:
./ap3216cAPP /dev/ap3216c
源码仓库位置:https://gitee.com/dream-cometrue/linux_driver_imx6ull