一:Linux I2C驱动框架
Linux内核将I2C驱动分为两部分:
(1)I2C总线驱动,I2C总线驱动就是SOC的I2C控制器驱动,也叫做I2C适配器驱动
(2)I2C设备驱动,I2C设备驱动就是针对具体的I2C设备而编写的驱动。
作为SOC开发者来讲无需关心i2c总线驱动,只需关注i2c设备驱动即可。
1、I2C总线驱动
I2C设备与platform设备不同的是,I2C拥有自己的总线即I2C总线而不需要虚拟出一条来。I2C总线驱动重点是I2C适配器(SOC的I2C接口控制器)驱动,其中有两个重要的数据结构:i2c_adapter和i2c_algorithm,Linux内核将I2C适配器抽象成i2c_adapter结构体:
include/linux/i2c.hstruct 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;/* data fields that are valid for all devices */struct rt_mutex bus_lock;int timeout; /* in jiffies */int retries;struct device dev; /* the adapter device */int nr;char name[48];struct completion dev_released;struct mutex userspace_clients_lock;struct list_head userspace_clients;struct i2c_bus_recovery_info *bus_recovery_info;const struct i2c_adapter_quirks *quirks;
};
对于一个i2c适配器,需要对外提供读写API函数,设备驱动程序就可以使用这些API函数来完成读写操作,其中i2c_algorithm类型的指针变量algo就是i2c适配器与i2c设备进行通信的方法。
struct i2c_algorithm {/* If an adapter algorithm can't do I2C-level access, set master_xferto NULL. If an adapter algorithm can do SMBus access, setsmbus_xfer. If set to NULL, the SMBus protocol is simulatedusing common I2C messages *//* master_xfer should return the number of messages successfullyprocessed, or a negative value on error */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 *);#if IS_ENABLED(CONFIG_I2C_SLAVE)int (*reg_slave)(struct i2c_client *client);int (*unreg_slave)(struct i2c_client *client);
#endif
};
master_xfer:i2c适配器的传输函数,可以通过此函数来完成与i2c设备之间的通信。
smbus_xfer:SMBUS总线的传输函数。
i2c总线驱动的主要工作任务就是初始化i2c_adapter结构体变量,然后设置i2c_algorithm中的master_xfer函数。完成后通过i2c_add_numberd_adapter和i2c_add_adapter这两个函数向系统注册设置好的i2c_adapter,原型如下:
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
其中adapter 和 adap:要添加到 Linux 内核中的 i2c_adapter,也就是 I2C 适配器。
2、I2C设备驱动
i2c设备驱动重点关注两个数据结构:i2c_client和i2c_driver。根据总线,设备,驱动模型划分,i2c_client描述设备信息,i2c_driver描述驱动内容,类似于platform_driver。
(1)i2c_client:一个设备对应一个i2c_client,每检测到一个i2c设备就会分配一个该数据
struct i2c_client {unsigned short flags; /* div., see below */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 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
};
(2)i2c_driver:类似于platform_driver,当i2c设备与驱动匹配成功后probe函数就会执行。
struct i2c_driver {unsigned int class;int (*attach_adapter)(struct i2c_adapter *) __deprecated;/* Standard driver model interfaces */int (*probe)(struct i2c_client *, const struct i2c_device_id *);int (*remove)(struct i2c_client *);void (*shutdown)(struct i2c_client *);void (*alert)(struct i2c_client *, unsigned int data);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 *, struct i2c_board_info *);const unsigned short *address_list;struct list_head clients;
};
如果使用设备树的话,需要设置device_driver的of_match_table成员变量,也就是驱动的兼容属性compatible。
对于i2c设备驱动来讲,重点工作就是构建i2c_driver,构建完成以后需要向linux内核注册这个i2c_driver。注册函数为i2c_register_driver:
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
或者使用注册宏定义:i2c_add_driver
#define i2c_add_driver(driver) \i2c_register_driver(THIS_MODULE, driver)
注销函数:i2c_del_driver
void i2c_del_driver(struct i2c_driver *driver)
i2c_driver注册示例:
/* i2c 驱动的 probe 函数 */
static int xxx_probe(struct i2c_client *client, const struct i2c_device_id *id)
{/* 函数具体程序 */return 0;
}/* i2c 驱动的 remove 函数 */
static int ap3216c_remove(struct i2c_client *client)
{/* 函数具体程序 */return 0;
}/* 传统匹配方式 ID 列表 */
static const struct i2c_device_id xxx_id[] = {{"xxx", 0},{}
};/* 设备树匹配列表 */
static const struct of_device_id xxx_of_match[] = {{ .compatible = "xxx" },{ /* Sentinel */ }
};/* i2c 驱动结构体 */
static struct i2c_driver xxx_driver = {.probe = xxx_probe,.remove = xxx_remove,.driver = {.owner = THIS_MODULE,.name = "xxx",.of_match_table = xxx_of_match,},.id_table = xxx_id,
};/* 驱动入口函数 */
static int __init xxx_init(void)
{int ret = 0;ret = i2c_add_driver(&xxx_driver);return ret;
}/* 驱动出口函数 */
static void __exit xxx_exit(void)
{i2c_del_driver(&xxx_driver);
}module_init(xxx_init);
module_exit(xxx_exit);
3、I2C设备和驱动匹配
设备和驱动的匹配过程是由I2C总线完成的,I2C总线的数据结构为i2c_bus_type:
struct bus_type i2c_bus_type = {.name = "i2c",.match = i2c_device_match,.probe = i2c_device_probe,.remove = i2c_device_remove,.shutdown = i2c_device_shutdown,
};
EXPORT_SYMBOL_GPL(i2c_bus_type);
成员函数match就是i2c总线的设备和驱动的匹配函数: i2c_device_match
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{struct i2c_client *client = i2c_verify_client(dev);struct i2c_driver *driver;if (!client)return 0;/* Attempt an OF style match */if (of_driver_match_device(dev, drv))return 1;/* Then ACPI style match */if (acpi_driver_match_device(dev, drv))return 1;driver = to_i2c_driver(drv);/* match on an id table if there is one */if (driver->id_table)return i2c_match_id(driver->id_table, client) != NULL;return 0;
}
二:I2C适配器驱动分析
一般情况i2c适配器驱动不需要我们编写,做个简单分析。
在对应SOC设备树共用文件(imx6ull.dtsi)中找到i2c1控制器节点:i2c1
i2c1: i2c@021a0000 {#address-cells = <1>;#size-cells = <0>;compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";reg = <0x021a0000 0x4000>;interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_I2C1>;status = "disabled";
};
搜寻compatible属性即可找到i2c适配器驱动文件:
static struct platform_device_id imx_i2c_devtype[] = {{.name = "imx1-i2c",.driver_data = (kernel_ulong_t)&imx1_i2c_hwdata,}, {.name = "imx21-i2c",.driver_data = (kernel_ulong_t)&imx21_i2c_hwdata,}, {/* sentinel */}
};
MODULE_DEVICE_TABLE(platform, imx_i2c_devtype);static const struct of_device_id i2c_imx_dt_ids[] = {{ .compatible = "fsl,imx1-i2c", .data = &imx1_i2c_hwdata, },{ .compatible = "fsl,imx21-i2c", .data = &imx21_i2c_hwdata, },{ .compatible = "fsl,vf610-i2c", .data = &vf610_i2c_hwdata, },{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, i2c_imx_dt_ids);static struct platform_driver i2c_imx_driver = {.probe = i2c_imx_probe,.remove = i2c_imx_remove,.driver = {.name = DRIVER_NAME,.owner = THIS_MODULE,.of_match_table = i2c_imx_dt_ids,.pm = IMX_I2C_PM,},.id_table = imx_i2c_devtype,
};static int __init i2c_adap_imx_init(void)
{return platform_driver_register(&i2c_imx_driver);
}
subsys_initcall(i2c_adap_imx_init);static void __exit i2c_adap_imx_exit(void)
{platform_driver_unregister(&i2c_imx_driver);
}
module_exit(i2c_adap_imx_exit);
分析适配器驱动可以看出i2c适配器驱动是个标准的platform驱动框架,而i2c设备驱动用的是i2c总线驱动框架。
当设备和驱动匹配成功后,i2c_imx_probe函数就会执行:
static int i2c_imx_probe(struct platform_device *pdev)
i2c_imx_probe函数主要的工作有两点:
(1)初始化i2c_adapter,设置i2c_algorithm 为 i2c_imx_algo,最后想Linux内核注册i2c_adapter
(2)初始化i2c1控制器的相关寄存器
i2c_imx->adapter.algo = &i2c_imx_algo;static struct i2c_algorithm i2c_imx_algo = {.master_xfer = i2c_imx_xfer,.functionality = i2c_imx_func,
};
.functionality用于返回i2c适配器支持什么样的通讯协议:
static u32 i2c_imx_func(struct i2c_adapter *adapter)
{return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL| I2C_FUNC_SMBUS_READ_BLOCK_DATA;
}
.master_xfer用来完成与i2c设备通信
static int i2c_imx_xfer(struct i2c_adapter *adapter,struct i2c_msg *msgs, int num)
{unsigned int i, temp;int result;bool is_lastmsg = false;struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);/* Start I2C transfer */result = i2c_imx_start(i2c_imx);if (result)goto fail0;/* read/write data */for (i = 0; i < num; i++) {if (i == num - 1)is_lastmsg = true;if (i) {dev_dbg(&i2c_imx->adapter.dev,"<%s> repeated start\n", __func__);temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);temp |= I2CR_RSTA;imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);result = i2c_imx_bus_busy(i2c_imx, 1);if (result)goto fail0;}dev_dbg(&i2c_imx->adapter.dev,"<%s> transfer message: %d\n", __func__, i);/* write/read data */
... ...if (msgs[i].flags & I2C_M_RD)result = i2c_imx_read(i2c_imx, &msgs[i], is_lastmsg);else {if (i2c_imx->dma && msgs[i].len >= DMA_THRESHOLD)result = i2c_imx_dma_write(i2c_imx, &msgs[i]);elseresult = i2c_imx_write(i2c_imx, &msgs[i]);}if (result)goto fail0;}fail0:/* Stop I2C transfer */i2c_imx_stop(i2c_imx);dev_dbg(&i2c_imx->adapter.dev, "<%s> exit with: %s: %d\n", __func__,(result < 0) ? "error" : "success msg",(result < 0) ? result : num);return (result < 0) ? result : num;
}
其中i2c_imx_start,i2c_imx_read,i2c_imx_write,i2c_imx_stop这些函数就是i2c寄存器的具体操作函数。
三:I2C设备驱动流程
1、未使用设备树
未使用设备树的时候需要在BSP里面使用i2c_board_info结构体来描述一个具体的I2C设备。
struct i2c_board_info {char type[I2C_NAME_SIZE];unsigned short flags;unsigned short addr;void *platform_data;struct dev_archdata *archdata;struct device_node *of_node;struct fwnode_handle *fwnode;int irq;
};
type和addr这两个成员变量是必须要设置的,一个是I2C设备的名字,一个是I2C设备的器件地址。举例OV2640设备
static struct i2c_board_info mx27_3ds_i2c_camera = {I2C_BOARD_INFO("ov2640", 0x30),
};#define I2C_BOARD_INFO(dev_type, dev_addr) \.type = dev_type, .addr = (dev_addr)
2、使用设备树
使用设备树时i2c设备信息通过创建相应的节点就可以,例如 imx6ull-14x14-evk.dts
&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";mag3110@0e {compatible = "fsl,mag3110";reg = <0x0e>;position = <2>;};... ...
};
"mag3110@0e"为子节点名称,“@”后面的“0e”为mag3110的i2c设备地址,与"reg = <0x0e>"相同。
compatible = "fsl,mag3110";用于设置i2c设备属性用于匹配驱动。
3、设备数据收发处理
第一章节中已经讲过,i2c设备驱动首先要做的就是初始化i2c_driver并想Linux内核注册。当设备和驱动匹配以后i2c_driver里面的probe函数就会执行,probe函数里面所做的就是字符设备驱动的那一套,一般需要初始化i2c设备,初始化i2c设备就需要对i2c设备寄存器进行读写操作,就需要要用到i2c_transfer函数,该函数最终会调用i2c适配器中i2c_algorithm里面的master_xfer函数,对于imx6u来说就是i2c_imx_xfer函数。
int i2c_transfer(struct i2c_adapter *adap,struct i2c_msg *msgs,int num)
其中msgs这个参数是一个i2c_msg类型的指针参数,Linux内核中i2c设备的数据收发消息就用i2c_msg结构体来描述:
struct i2c_msg {__u16 addr; /* slave address */__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* read data, from slave to master */
#define I2C_M_STOP 0x8000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */__u16 len; /* msg length */__u8 *buf; /* pointer to msg data */
};
使用i2c_transfer函数发送数据之前要先构建好i2c_msg。
除过i2c_transfer函数还有两个API可以使用:i2c_master_send和i2c_master_recv,最终还是会调用到i2c_transfer
int i2c_master_send(const struct i2c_client *client,const char *buf,int count)int i2c_master_recv(const struct i2c_client *client,char *buf,int count)
4、举例说明:mag3110设备
根据设备节点compatible属性搜寻:
#define MAG3110_DRV_NAME "mag3110"static const struct i2c_device_id mag3110_id[] = {{MAG3110_DRV_NAME, 0},{}
};MODULE_DEVICE_TABLE(i2c, mag3110_id);
static struct i2c_driver mag3110_driver = {.driver = {.name = MAG3110_DRV_NAME,.owner = THIS_MODULE,.pm = MAG3110_DEV_PM_OPS,},.probe = mag3110_probe,.remove = mag3110_remove,.id_table = mag3110_id,
};static int __init mag3110_init(void)
{return i2c_add_driver(&mag3110_driver);
}static void __exit mag3110_exit(void)
{i2c_del_driver(&mag3110_driver);
}
分析probe函数结构:mag3110_probe
static int mag3110_probe(struct i2c_client *client,const struct i2c_device_id *id)
{... .../* Initialize mag3110 chip */mag3110_init_client(client);
}-->>mag3110_init_client(client);-->> mag3110_write_reg(client, MAG3110_CTRL_REG2, val);-->> i2c_smbus_write_byte_data(client, reg, value);-->> i2c_smbus_xfer(client->adapter, client->addr, client->flags,I2C_SMBUS_WRITE, command,I2C_SMBUS_BYTE_DATA, &data);-->> i2c_smbus_xfer_emulated(adapter, addr, flags, read_write,command, protocol, data);-->> i2c_transfer(adapter, msg, num);
接着第三小节举例说明i2c_transfer函数中msg参数的构建过程:具体含义参考第三小节i2c_msg结构体
static s32 i2c_smbus_xfer_emulated(struct i2c_adapter *adapter, u16 addr,unsigned short flags,char read_write, u8 command, int size,union i2c_smbus_data *data)
{unsigned char msgbuf0[I2C_SMBUS_BLOCK_MAX+3];unsigned char msgbuf1[I2C_SMBUS_BLOCK_MAX+2];struct i2c_msg msg[2] = {{.addr = addr,.flags = flags,.len = 1,.buf = msgbuf0,}, {.addr = addr,.flags = flags | I2C_M_RD,.len = 0,.buf = msgbuf1,},};... ...case I2C_SMBUS_PROC_CALL:num = 2; /* Special case */read_write = I2C_SMBUS_READ;msg[0].len = 3;msg[1].len = 2;msgbuf0[1] = data->word & 0xff;msgbuf0[2] = data->word >> 8;break;... ...status = i2c_transfer(adapter, msg, num);
}
四:示例代码
1、修改设备树
pinctrl_i2c1: i2c1grp {fsl,pins = <MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0>;
};&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";ap3216c@1e {compatible = "alientek,ap3216c";reg = <0x1e>;};
};
2、驱动
#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 {dev_t devid;struct cdev cdev;struct class *class;struct device *device;struct device_node *nd;int major;void *private_data;unsigned short ir, als, ps;
};struct ap3216c_dev ap3216cdev;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->private_data;/* 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->private_data;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);
}static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{u8 data = 0;ap3216c_read_regs(dev, reg, &data, 1);return data;
}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);
}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)dev->ir = 0;elsedev->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;elsedev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}static int ap3216c_open(struct inode *inode, struct file *filp)
{filp->private_data = &ap3216cdev;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 ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;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;
}static struct file_operations ap3216c_fops = {.owner = THIS_MODULE,.open = ap3216c_open,.read = ap3216c_read,.release = ap3216c_release,
};static int ap3216c_probe(struct i2c_client *client,const struct i2c_device_id *id)
{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);}cdev_init(&ap3216cdev.cdev, &ap3216c_fops);cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);ap3216cdev.private_data = client;return 0;
}static int ap3216c_remove(struct i2c_client *client)
{device_destroy(ap3216cdev.class, ap3216cdev.devid);class_destroy(ap3216cdev.class);cdev_del(&ap3216cdev.cdev);unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);return 0;
}static const struct i2c_device_id ap3216c_id[] = {{"ap3216c", 0},{}
};static const struct of_device_id ap3216c_of_match[] = {{.compatible = "alientek, ap3216c"},{}
};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 ap3216c_init(void)
{int ret = 0;ret = i2c_add_driver(&ap3216c_driver);return ret;
}static void ap3216c_exit(void)
{i2c_del_driver(&ap3216c_driver);
}// module_i2c_driver(ap3216c_driver);
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");