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 提前输入 y
  • i2cdetect 返回的位置上,UU 表明有设备并且有驱动,1e 这样的非 UU 表明有设备没驱动。
  • i2cdetect -l 列出当前总线列表。

smbus 传输数据

对于 UU 这样已经有驱动的设备,不能直接操作,必须加上 -f 才能进行操作。

  • i2cset -f -y 0 0x1e 0 0x4 : 向 0号总线的 0x1e 设备的 0 号地址写入 0x4
  • i2cget -f -y 0 0x1e 0xc w : 从 0号总线的 0x1e 设备的 0xc 号地址读取一个 word

i2c 传输数据

  • i2ctransfer -f -y 0 w2@0x1e 0 0x4 : 向 0号总线的 0x1e 设备的 0 号地址写入2个字节 0x4
  • i2ctransfer -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_devicei2c_of_match_device ; 有些是 of_match_device -> of_match_node -> __of_match_node -> __of_device_is_compatible 然后对比顺序为 compatible > type > name
  • acpi_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_createdevice_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->basereg << 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, &reg_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-* 操作

发表评论