I2C 介绍
- i2c device driver: 如何读写数据,组织命令数据等,比如命令中需要包含命令、地址等数据,还有收到原始数据后如何解析。
- i2c controller driver: 如何按照 i2c 的协议,来传输 device driver 的数据。
i2c 数据通道
- 普通用法, 芯片有 i2c 控制器,使用 i2c 设备驱动: app <-> some driver <-> adapt driver <-> device
- 芯片没有 i2c 控制器,使用 gpio 模拟i2c 控制,使用 i2c 设备驱动: app <-> some driver <-> i2c-gpio.c <-> device
- app 希望自己组织数据,跳过 i2c 设备驱动,来使用 i2c 控制器驱动: app <-> i2c-dev.c <-> adapt driver <-> device
- app 希望自己组织数据,跳过 i2c 设备驱动,使用 gpio 模拟i2c驱动: app <-> i2c-dev.c <-> i2c-gpio.c <-> device
app 使用 i2c-dev.c
自己组织数据,这种一般成为用户态驱动程序。
SMBus 协议
SMBus 是 i2c 的子集。在以下方面要求更加严格:
- 电压范围,1.8 - 5V
- 最小时钟频率 10KHz,最大的 SCL 拉低占用时间也有限制
- 开始的地址匹配时,必须 ack,用于判断设备状态是否正常。
- 增加 restart 信号,可以省略停止信号。
- SMBus 和 i2c 的最大区别,是 block r/w 的时候,SMBus 要求在寄存器地址后,正式数据前,必须增加一个长度数据。
i2c 系统的重要结构体
i2c_adapter
controller 对应的结构体是 i2c_adapter, 主要的成员是:第几个i2c控制器, 传输函数。
struct i2c_adapter {
// 传输函数
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
// 第几个 bus 上的控制器
int nr;
i2c_client
device 对应的结构体是 i2c_client,最重要的成员是: 设备地址。
struct i2c_client {
// 可以用来表示少部分的 10位地址,或者其他情况。
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
// device 具体挂载在哪个总线对应的 adapter 下面?
struct i2c_adapter *adapter; /* the adapter we sit on */
i2c_msg
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
传输函数中,第一个参数 adap 指明了是第几个 i2c 控制器;第二个参数是传输数据,其中包含了芯片地址、数据 buf、数据长度;第三个参数 num 是 msg 的个数,比如读取数据,那么就需要两个 msg,前一个是寄存器地址 msg,第二个是数据读取命令 msg,所以需要一次创建好多个 msg.
struct i2c_msg {
__u16 addr; /* slave address */
// flags 标识 read / write
__u16 flags;
__u16 len; /* msg length */
__u8 *buf; /* pointer to msg data */
};
i2c_transfer
直接使用 master_xfer
传输比较麻烦,可以使用 int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
来传输,内部调用了 master_xfer
.
无需驱动直接访问 i2c
- 正常情况,需要自己写 i2c 驱动程序,然后 app 通过驱动程序来工作。
- 但是,如果不像自己写 i2c 驱动程序,那么可以通过内核自带驱动
i2c-dev.c
再通过i2c_adapter
就可以和 i2c 设备进行通信。 - app 如何使用
i2c-dev.c
, 可以参考i2c-tools
这个软件。
i2c-tools
操作 i2c,需要确定三个东西: 哪个 adapter, 哪个 address, 具体操作数据?
i2cdetect
i2cdetect
命令用于检测总线上是否有 i2c 设备。i2cdetect 0
用于检测 0号总线上的设备。i2cdetect -y 0
提前输入 yi2cdetect
返回的位置上,UU
表明有设备并且有驱动,1e
这样的非UU
表明有设备没驱动。i2cdetect -l
列出当前总线列表。
smbus 传输数据
对于 UU
这样已经有驱动的设备,不能直接操作,必须加上 -f
才能进行操作。
i2cset -f -y 0 0x1e 0 0x4
: 向 0号总线的 0x1e 设备的 0 号地址写入 0x4i2cget -f -y 0 0x1e 0xc w
: 从 0号总线的 0x1e 设备的 0xc 号地址读取一个 word
i2c 传输数据
i2ctransfer -f -y 0 w2@0x1e 0 0x4
: 向 0号总线的 0x1e 设备的 0 号地址写入2个字节 0x4i2ctransfer -f -y 0 w1@0x1e 0xc r2
: 从 0号总线的 0x1e 设备的 0xc 号地址读取2个字节。
访问设备
open
ls /dev/i2c-*
得到的是 i2c_adapter 控制器,是和i2cdetect -l
一样的,只是比较简略。
ioctl
ioctl(file, I2C_SLAVE, address)
ioctl(file, I2C_SLAVE_FORCE, address)
指定具体是哪个 i2c 设备。force 针对的是有驱动的 i2c 设备。ioctl(file, I2C_SMBUS, &args)
使用 smbus 传输数据。ioctl(file, I2C_RDWR, &rdwr)
使用 i2c 传输数据。
源码
include\uapi\linux\i2c-dev.h
中的struct i2c_rdwr_ioctl_data
用于封装使用 ioctl 通过 i2c 来传输的数据。
使用 i2c 的源码流程
使用 smbus 的源码流程
i2c-tools 交叉编译
i2c-tools-4.2.tar.xz
源码- 修改 Makefile
CC = $(CROSS_COMPILE)gcc AR = $(CROSS_COMPILE)ar STRIP = $(CROSS_COMPILE)strip
- export 相关环境变量
export ARCH=arm export CROSS_COMPILE=arm-linux-gnueabihf- export PATH=$PATH:/home/book/100ask_imx6ull-sdk/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin
- 执行make时,是动态链接,需要把libi2c.so也放到单板上;静态链接的话,执行:`make USE_STATIC_LIB=1
编写 app 访问 eeprom
app 可以使用 i2c-tools 里面提供的 api:
int open_i2c_dev(int i2cbus, char *filename, size_t size, int quiet);
int set_slave_addr(int file, int address, int force);
extern __s32 i2c_smbus_write_byte(int file, __u8 value);
extern __s32 i2c_smbus_read_i2c_block_data(int file, __u8 command, __u8 length, __u8 *values);
...
编译的时候,需要增加下面这些文件: smbus.c
smbus.h
, i2cbusses.c
i2cbusses.h
.
app 中需要注意,当连续读写的时候,需要增加 nanosleep
增加延时,满足 i2c 设备的时序要求。
通用驱动 i2c-dev 分析
i2c_dev_init
register_chrdev_region
注册一个区域,res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");
res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
注册 i2c bus 通知的回调函数结构体。在static struct notifier_block i2cdev_notifier
中,定义了.notifier_call = i2cdev_notifier_call,
在static int i2cdev_notifier_call(struct notifier_block *nb, unsigned long action, void *data)
中,会识别 action 是增加还是减少设备,从而调用i2cdev_attach_adapter
或者i2cdev_detach_adapter
i2c_for_each_dev(NULL, i2cdev_attach_adapter);
绑定所有已经存在的 i2c 总线控制设备。
i2cdev_attach_adapter
cdev_init(&i2c_dev->cdev, &i2cdev_fops);
res = cdev_add(&i2c_dev->cdev, MKDEV(I2C_MAJOR, adap->nr), 1);
主设备号: I2C_MAJOR, 次设备号: adap->nr, 也就是 /dev/i2c-n 中的 n.- 字符设备注册到内核中,使得内核可以识别和管理该设备, 提供设备的内核层面功能(如 file_operations 的实现)。 关联字符设备与设备号(dev_t), 使得设备可以通过 /dev 目录下的设备文件进行访问。
i2c_dev->dev = device_create(i2c_dev_class, &adap->dev, MKDEV(I2C_MAJOR, adap->nr), NULL, "i2c-%d", adap->nr);
- 在用户空间创建设备文件(如 /dev/my_device),使得应用程序可以访问设备。
- 在 /sys 目录下创建设备的属性文件,提供设备的管理接口。
- 负责设备的用户空间接口创建。使得设备可以通过用户空间工具(如 udev)进行管理。
i2cdev_open
static int i2cdev_open(struct inode *inode, struct file *file)
adap = i2c_get_adapter(minor);
从 inode 中拿到次设备号,然后获取相应的 adapter.client = kzalloc(sizeof(*client), GFP_KERNEL);
client->adapter = adap;
adap 放到 client 里面。file->private_data = client;
把 client 放到 file 私有数据里。
i2cdev_ioctl
static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
- cmd
I2C_SLAVE
,struct i2c_client *client = file->private_data;
client->addr = arg;
设置i2c设备地址。 - cmd
I2C_RDWR
,static noinline int i2cdev_ioctl_rdwr(struct i2c_client *client, unsigned long arg)
i2c 模式读写数据。 res = i2c_transfer(client->adapter, rdwr_pa, rdwr_arg.nmsgs);
i2c 模式传输数据。- cmd
I2C_SMBUS
,static noinline int i2cdev_ioctl_smbus(struct i2c_client *client, unsigned long arg)
smbus 模式读写数据 res = i2c_smbus_xfer(client->adapter, client->addr, client->flags, data_arg.read_write, data_arg.command, data_arg.size, &temp);
smbus 模式传输数据。
I2C系统驱动程序模型
参考资料
- 内核文档:
Documentation\i2c\instantiating-devices.rst
Documentation\i2c\writing-clients.rst
- 内核驱动程序示例:
drivers/eeprom/at24.c
层次图
总线设备驱动模型
源码
i2c_driver
static struct i2c_driver at24_driver
中.driver.of_match_table
和.id_table
用于匹配。- 匹配函数来源于
i2c-core.c
中的struct bus_type i2c_bus_type
中的.match = i2c_device_match,
匹配的顺序是i2c_driver
中的.driver.of_match_table
>.driver.acpi_match_table
>.id_table
static int i2c_device_match(struct device *dev, struct device_driver *drv) { ... /* 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; ... }
static inline int of_driver_match_device(struct device *dev, const struct device_driver *drv)
调用的函数,不同的 linux 内核版本也不一样,有些是调用i2c_of_match_device
里面包含of_match_device
和i2c_of_match_device
; 有些是of_match_device
->of_match_node
->__of_match_node
->__of_device_is_compatible
然后对比顺序为 compatible > type > nameacpi_driver_match_device
正常不用关心static const struct i2c_device_id *i2c_match_id(const struct i2c_device_id *id, const struct i2c_client *client)
直接使用 name 进行比较。
创建一个 client
- 通过设备树创建,或者通过
i2c_register_board_info
创建. 但是i2c_register_board_info
不会导出内核符号表,不能作为模块编译,只能编译进内核。 - 通过
i2c_new_device
或者i2c_new_probed_device
来创建,建议使用i2c_new_probed_device
会检查设备是否存在。 - 用户空间使用命令来创建,方便调试
// 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3 # echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device // 删除一个i2c_client # echo 0x50 > /sys/bus/i2c/devices/i2c-3/delete_device
编写设备驱动 i2c_driver
i2c_driver 的框架,类似于普通的字符设备的框架,compatible 匹配,probe 里面,先是 register_chrdev
, 然后是 class_create
和 device_create
. 在 remove 当中,就是反过来的操作。
编写设备驱动 i2c_client
编译为模块
增加 Makefile.
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88/
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ap3216c_drv.o
obj-m += ap3216c_client.o
命令生成 client
// 在I2C BUS0下创建i2c_client
# echo ap3216c 0x1e > /sys/bus/i2c/devices/i2c-0/new_device
// 删除i2c_client
# echo 0x1e > /sys/bus/i2c/devices/i2c-0/delete_device
源码 client
使用 i2c_new_device
或者 i2c_new_probed_device
来创建
设备树 client
设备树里i2c1就是I2C BUS0。
&i2c1 {
ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
};
make dtbs
cp /mnt/100ask_imx6ull-14x14.dtb /boot
sync
I2C_Adapter驱动框架
i2c_adapter
struct i2c_adapter
中,algo 是传输方法,nr 是第几条总线。
struct i2c_adapter {
const struct i2c_algorithm *algo; /* the algorithm to access the bus */
...
int nr;
...
};
i2c_algorithm
struct i2c_algorithm
中,最重要的是 master_xfer
,这个是 i2c 的最基础的传输函数; smbus_xfer
用于 smbus 传输,如果不提供这个函数,会使用 master_xfer
来模拟传输 smbus; functionality
用于获取支持的功能,返回值通过位标识功能,一般在 i2c.h
以宏 I2C_FUNC_
开头。
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);
...
u32 (*functionality) (struct i2c_adapter *);
};
/* To determine what functionality is present */
#define I2C_FUNC_I2C 0x00000001
#define I2C_FUNC_10BIT_ADDR 0x00000002
#define I2C_FUNC_PROTOCOL_MANGLING 0x00000004 /* I2C_M_IGNORE_NAK etc. */
#define I2C_FUNC_SMBUS_PEC 0x00000008
#define I2C_FUNC_NOSTART 0x00000010 /* I2C_M_NOSTART */
#define I2C_FUNC_SLAVE 0x00000020
#define I2C_FUNC_SMBUS_BLOCK_PROC_CALL 0x00008000 /* SMBus 2.0 */
#define I2C_FUNC_SMBUS_QUICK 0x00010000
#define I2C_FUNC_SMBUS_READ_BYTE 0x00020000
#define I2C_FUNC_SMBUS_WRITE_BYTE 0x00040000
#define I2C_FUNC_SMBUS_READ_BYTE_DATA 0x00080000
#define I2C_FUNC_SMBUS_WRITE_BYTE_DATA 0x00100000
#define I2C_FUNC_SMBUS_READ_WORD_DATA 0x00200000
#define I2C_FUNC_SMBUS_WRITE_WORD_DATA 0x00400000
#define I2C_FUNC_SMBUS_PROC_CALL 0x00800000
#define I2C_FUNC_SMBUS_READ_BLOCK_DATA 0x01000000
#define I2C_FUNC_SMBUS_WRITE_BLOCK_DATA 0x02000000
#define I2C_FUNC_SMBUS_READ_I2C_BLOCK 0x04000000 /* I2C-like block xfer */
#define I2C_FUNC_SMBUS_WRITE_I2C_BLOCK 0x08000000 /* w/ 1-byte reg. addr. */
#define I2C_FUNC_SMBUS_HOST_NOTIFY 0x10000000
源码
可以参考: i2c-gpio.c
这种模拟i2c控制的源码去写。也可以参考 i2c-imx.c
.
static int i2c_bus_virtual_probe(struct platform_device *pdev)
{
/* get info from device tree, to set i2c_adapter/hardware */
/* alloc, set, register i2c_adapter */
g_adapter = kzalloc(sizeof(*g_adapter), GFP_KERNEL);
g_adapter->owner = THIS_MODULE;
g_adapter->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
g_adapter->nr = -1;
snprintf(g_adapter->name, sizeof(g_adapter->name), "i2c-bus-virtual");
g_adapter->algo = &i2c_bus_virtual_algo;
i2c_add_adapter(g_adapter); // i2c_add_numbered_adapter(g_adapter);
return 0;
}
完善虚拟的I2C_Adapter
使用GPIO模拟I2C
设备树
不同版本的内核,设备树的写法也有点差别。
源码
i2c-gpio.c
中的 probe,在设备树相关的读取之后,调用了 i2c-algo-bit.c
中的 i2c_bit_add_numbered_bus
来做传输函数的添加。
i2c-algo-bit.c
static int __i2c_bit_add_bus(struct i2c_adapter *adap, int (*add_adapter)(struct i2c_adapter *))
把传输函数给关联上了。adap->algo = &i2c_bit_algo;
const struct i2c_algorithm i2c_bit_algo = { .master_xfer = bit_xfer, .functionality = bit_func, };
static int bit_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg msgs[], int num)
中的i2c_start(adap);
也非常简单。static void i2c_start(struct i2c_algo_bit_data *adap) { /* assert: scl, sda are high */ setsda(adap, 0); udelay(adap->udelay); scllo(adap); }
使用 i2c-gpio
如果想用 i2c-gpio 来控制一个新的设备,只需要在设备树中,增加一个新的 i2c 节点。
compatible = "i2c-gpio";
- 使用pinctrl把 SDA、SCL所涉及引脚配置为GPIO、开极
- 可选
- 指定SDA、SCL所用的GPIO
- 指定频率(2种方法):
i2c-gpio,delay-us = <5>; /* ~100 kHz */
clock-frequency = <400000>;
#address-cells = <1>;
#size-cells = <0>;
i2c-gpio,sda-open-drain
:- 它表示其他驱动、其他系统已经把SDA设置为open drain了
- 在驱动里不需要在设置为open drain
- 如果需要驱动代码自己去设置SDA为open drain,就不要提供这个属性
i2c-gpio,scl-open-drain
:- 它表示其他驱动、其他系统已经把SCL设置为open drain了
- 在驱动里不需要在设置为open drain
- 如果需要驱动代码自己去设置SCL为open drain,就不要提供这个属性
使用GPIO操作I2C设备
使用 gpio,需要确定内核里面的 i2c gpio 已经开启,或者作为模块开启才行。
具体芯片的I2C_Adapter
imx6ull 对应的是 i2c-imx.c
i2c_imx_start
imx_i2c_write_reg(i2c_imx->ifdr, i2c_imx, IMX_I2C_IFDR);
这样的写入寄存器的语句,可以看到寄存器的地址来源于i2c_imx->base
和reg << i2c_imx->hwdata->regshift
, 不同芯片,每个寄存器之间的地址间隔不一样,所以i2c_imx->hwdata->regshift
是按照具体芯片地址间隔设定的结构体,所以,在写入寄存器的时候,只需要提供偏移多少位的 reg 即可。static inline void imx_i2c_write_reg(unsigned int val, struct imx_i2c_struct *i2c_imx, unsigned int reg) { writeb(val, i2c_imx->base + (reg << i2c_imx->hwdata->regshift)); }
static const struct imx_i2c_hwdata imx1_i2c_hwdata = { .devtype = IMX1_I2C, .regshift = IMX_I2C_REGSHIFT, .clk_div = imx_i2c_clk_div, .ndivs = ARRAY_SIZE(imx_i2c_clk_div), .i2sr_clr_opcode = I2SR_CLR_OPCODE_W0C, .i2cr_ien_opcode = I2CR_IEN_OPCODE_1, };
i2c_imx_xfer
if (msgs[i].flags & I2C_M_RD)
判断这个 flag,然后决定 i2c_imx_read
或者 i2c_imx_write
,数据收发完成之后,就会调用 i2c_imx_stop(i2c_imx)
i2c_imx_read
static int i2c_imx_read(struct imx_i2c_struct *i2c_imx, struct i2c_msg *msgs, bool is_lastmsg)
- 使用
imx_i2c_write_reg((msgs->addr << 1) | 0x01, i2c_imx, IMX_I2C_I2DR);
之后,就会用result = i2c_imx_trx_complete(i2c_imx);
来等待完成 static int i2c_imx_trx_complete(struct imx_i2c_struct *i2c_imx)
中,调用wait_event_timeout(i2c_imx->queue, i2c_imx->i2csr & I2SR_IIF, HZ / 10);
来等待。- for 循环读取数据的时候,也会调用
i2c_imx_trx_complete
来等待。
总结
i2c 其实主要是两层,上层是 i2c_adapter, 下层是 i2c_client.
- i2c_adapter 对应的是芯片的 i2c 控制器, i2c_client 对应的是实际的 i2c 设备。
- i2c_adapter 有相应的驱动,比如 i2c-imx.c,i2c_client 也有相应的驱动,比如 at24.c
- 设备树中,i2c 控制器本身的节点有 compatible 用于和 i2c_adapter 适配,i2c 的子节点也有 compatible 用于和 i2c_client 适配。
- i2c_adapter 驱动中实现了
master_xfer
,functionality
函数。用于比如说 ARM,使用自身 i2c 控制器的方法。 - 在适配设备树的时候,根据设备树的父子节点,来自动把 client 挂载在 adapter 上面。比如, i2c2 这个会根据
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
匹配到 adapter 上面; wm8960 会根据"wlf,wm8960"
匹配到 client 上面,并且会自动把 adapter 放到client->adapter
, 这个自动匹配是在i2c_new_device()
里面的,由系统自动调用。i2c2: i2c@021a4000 { #address-cells = <1>; #size-cells = <0>; compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; reg = <0x021a4000 0x4000>; interrupts = <GIC_SPI 37 IRQ_TYPE_LEVEL_HIGH>; clocks = <&clks IMX6UL_CLK_I2C2>; status = "disabled"; };
&i2c2 { clock_frequency = <100000>; pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c2>; status = "okay"; codec: wm8960@1a { compatible = "wlf,wm8960"; reg = <0x1a>; clocks = <&clks IMX6UL_CLK_SAI2>; clock-names = "mclk"; wlf,shared-lrclk; };
- 如果在 at24.c 这里面实现了 sysfs 获得 devfs,那么就可以直接使用文件接口来操作;如果没有实现这些接口,那么就需要 app 自己处理协议组织数据,然后通过 ioctl 来操作,间接调用了 i2c_transfer。
- 如果 app 直接通过
i2c-dev.c
来操作,一般需要通过open_i2c_dev
set_slave_addr
i2c_smbus_write_byte_data
直接操作,当然中间需要通过 adapter 的驱动。可以参考: at24c02_test. - 如果 app 如果专门的设备驱动来操作,那么这种设备驱动其实就是在 open, read, write 中,调用
open_i2c_dev
set_slave_addr
i2c_smbus_write_byte_data
这些函数。设备驱动向上提供了 sysfs 或者 devfs 接口。这样 app 就可以通过 open, read, write 这样的 fs 接口来操作。可以参考: ap3216c_ok
总之,首先要确认的就是有没有 soc 的总线控制器也就是 adapter 的驱动,然后才能决定下一步。
有 adapter 驱动
不需要编写任何驱动,只需通过用户空间应用程序(如 i2c-tools 或自定义程序)直接操作 /dev/i2c-* 设备文件即可。
- 启用 I2C 控制器驱动
- 确保内核已包含对应 I2C 控制器的驱动(例如树莓派的 i2c-bcm2835)
- 在设备树(Device Tree)中启用 I2C 控制器节点(如设置 status = "okay")。
- 加载 i2c-dev 模块
modprobe i2c-dev # 生成 /dev/i2c-0, /dev/i2c-1 等设备文件
- 用户空间应用程序直接操作:
使用 ioctl 调用(如 I2C_RDWR)发送 I2C 消息。
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("/dev/i2c-1", O_RDWR); // 打开 I2C 总线设备文件
if (fd < 0) {
perror("open");
return -1;
}
// 设置从设备地址(例如 0x50)
if (ioctl(fd, I2C_SLAVE, 0x50) < 0) {
perror("ioctl");
close(fd);
return -1;
}
// 写入数据到设备(例如写入寄存器地址 0x00)
unsigned char reg_addr = 0x00;
if (write(fd, ®_addr, 1) != 1) {
perror("write");
close(fd);
return -1;
}
// 从设备读取数据(例如读取 2 字节)
unsigned char buf[2];
if (read(fd, buf, 2) != 2) {
perror("read");
close(fd);
return -1;
}
printf("Data: 0x%02x 0x%02x\n", buf[0], buf[1]);
close(fd);
return 0;
}
adapter 驱动不存在
如果目标 I2C 控制器(如某款自定义硬件)的驱动未在内核中实现,则需要 编写 I2C 适配器驱动,但 不需要编写具体设备的驱动。
- I2C 适配器驱动(i2c_adapter)
- 实现 i2c_algorithm 结构体中的 master_xfer 方法,定义如何通过硬件发送 I2C 消息。
- 注册适配器(i2c_add_adapter),使其出现在
/sys/bus/i2c/devices/
并生成/dev/i2c-*
设备文件。
- 设备树或平台设备配置:
- 在设备树中定义 I2C 控制器的寄存器地址、中断号等硬件信息。
- 或通过平台设备(platform_device)静态注册。
#include <linux/i2c.h>
#include <linux/platform_device.h>
static struct i2c_algorithm my_i2c_algo = {
.master_xfer = my_i2c_xfer, // 实现 I2C 传输函数
};
static int my_i2c_probe(struct platform_device *pdev) {
struct i2c_adapter *adap;
adap = devm_kzalloc(&pdev->dev, sizeof(*adap), GFP_KERNEL);
adap->owner = THIS_MODULE;
adap->algo = &my_i2c_algo;
adap->dev.parent = &pdev->dev;
snprintf(adap->name, sizeof(adap->name), "my-i2c-adapter");
i2c_add_adapter(adap); // 注册适配器
return 0;
}
static struct platform_driver my_i2c_driver = {
.driver = {
.name = "my-i2c-controller",
},
.probe = my_i2c_probe,
};
module_platform_driver(my_i2c_driver);
是否需要编写具体设备驱动?
不需要:i2c-dev 的用途正是为了绕过内核驱动,允许用户空间直接操作 I2C 设备。设备驱动(如传感器驱动)的作用是将硬件功能封装为内核接口(如 sysfs、字符设备),但若你选择直接通过 /dev/i2c-* 操作,则无需在内核中实现这些逻辑。
总结
场景 | 需实现内容 | 用户空间操作方式 |
---|---|---|
I2C 控制器驱动已存在 | 无需编写任何驱动 | 直接通过 /dev/i2c-* 操作 |
I2C 控制器驱动不存在 | 需编写适配器驱动 | 适配器驱动注册后,通过 /dev/i2c-* 操作 |