跳到主要内容位置

I2C子系统-内核视角

I2C 驱动层级

内核自带的通用 I2C 驱动程序 i2c-dev

编写一个设备驱动程序

控制器驱动程序 I2C Adapter 框架

GPIO 模拟 I2C,实现 I2C_Adapter 驱动

具体芯片下 I2C_Adapter 驱动

I2C 驱动层级#

一张图整理,可以看完后面的具体内容再回来看这张图:

接下来,会按照从上到下的顺序介绍整个驱动架构。

内核自带的通用 I2C 驱动程序 i2c-dev#

1.i2c-dev.c 注册过程#

入口函数中,请求字符设备号,并注册已存在 adapters 下面的所有 i2c 设备

同时也会生成对应的设备节点 i2c-X,以后只要打开这个设备节点,就是访问该设备,并且该设备的次设备号也绑定了对应的 adapter。

2.file_operations 函数分析#

回忆我们在 I2C-TOOLS 中调用 open、ioctl,最终就会调用到以下驱动结构体的函数。

所以我们就查看 open、ioctl。(read、write 提供了 i2c 简易写,读写一个字节)

static const struct file_operations i2cdev_fops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.read = i2cdev_read,
.write = i2cdev_write,
.unlocked_ioctl = i2cdev_ioctl,
.compat_ioctl = compat_i2cdev_ioctl,
.open = i2cdev_open,
.release = i2cdev_release,
};
2.1 i2cdev_open#

open 函数里面可以看到上面,入口函数为什么把 adap 和次设备号绑定,这里就可以使用次设备号访问对应的 i2c_adapter;

然后分配一个 i2c_client 结构体,把它放入 file 的私有数据

2.2 i2cdev_ioctl#
  • 设置从机地址 I2C_SLAVE/I2C_SLAVE_FORCE

    通过 file 的私有数据就可以获得 open 函数里面放入的 i2c_client

  • 读写 I2C_RDWR/I2C_SMBUS:最终就是调用 i2c-core 提供的函数

3.总结#

来自韦东山课程

编写一个设备驱动程序#

参考资料:

  • Linux 内核文档:
    • Documentation\i2c\instantiating-devices.rst
    • Documentation\i2c\writing-clients.rst
  • Linux 内核驱动程序示例:
    • drivers/eeprom/at24.c

1.I2C 总线-设备-驱动模型#

类似于通用字符设备总线-设备-驱动模型,I2C 设备也有一套I2C 总线-设备-驱动模型。整体结构如下图:

2.i2c_driver 设备驱动#

分配、设置、注册一个 i2c_driver 结构体,类似drivers/eeprom/at24.c

static struct i2c_driver at24_driver = {
.driver = {
.name = "at24",
.of_match_table = of_match_ids_example,
.acpi_match_table = ACPI_PTR(at24_acpi_ids),
},
.probe = at24_probe,
.remove = at24_remove,
.id_table = at24_ids,
};
  • 在 probe_new 函数中,分配、设置、注册 file_operations 结构体。
  • 在 file_operations 的函数中,使用 i2c_transfer 等函数发起 I2C 传输。

参考 at24.c 的代码,可以给出一个 i2c_driver 的模板:

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/mutex.h>
#include <linux/mod_devicetable.h>
#include <linux/bitops.h>
#include <linux/jiffies.h>
#include <linux/property.h>
#include <linux/acpi.h>
#include <linux/i2c.h>
#include <linux/nvmem-provider.h>
#include <linux/regmap.h>
#include <linux/pm_runtime.h>
#include <linux/gpio/consumer.h>
static const struct of_device_id of_match_ids_example[] = {
{.compatible = "com_name,chip_name", .data = NULL}, // data is private data
{/* END OF LIST */},
};
static const struct i2c_device_id example_ids[] = {
{"chip_name", (kernel_ulong_t)NULL},
{/* END OF LIST */},
};
static int i2c_driver_example_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
return 0;
}
static int i2c_driver_example_remove(struct i2c_client *client)
{
return 0;
}
static struct i2c_driver i2c_example_driver = {
.driver = {
.name = "example",
.of_match_table = of_match_ids_example,
},
.probe = i2c_driver_example_probe,
.remove = i2c_driver_example_remove,
.id_table = example_ids,
};
static int __init i2c_driver_example_init(void)
{
return i2c_add_driver(&i2c_example_driver);
}
module_init(i2c_driver_example_init);
static void __exit i2c_driver_example_exit(void)
{
i2c_del_driver(&i2c_example_driver);
}
module_exit(i2c_driver_example_exit);
MODULE_AUTHOR("wanghaicheng.online");
MODULE_LICENSE("GPL");

3.编写 I2C 设备-AP3216C 传感器的 i2c_driver 设备驱动#

AP3216C 是红外、光强、距离三合一的传感器,设备地址是 0x1E。

以读出光强、距离值为例,步骤如下:

  • 复位:往寄存器 0 写入 0x4
  • 使能:往寄存器 0 写入 0x3
  • 读红外:读寄存器 0xA、0xB 得到 2 字节的红外数据
  • 读光强:读寄存器 0xC、0xD 得到 2 字节的光强
  • 读距离:读寄存器 0xE、0xF 得到 2 字节的距离值

(1)利用上面的 i2c 驱动框架,先实现入口与出口函数:入口函数里面添加总线驱动 i2c_driver,出口函数里面删除。同时提供 of_match_ids_ap3216c 匹配 i2c_client。

static const struct of_device_id of_match_ids_ap3216c[] = {
{.compatible = "lite-on,ap3216c", .data = NULL}, // data is private data
{/* END OF LIST */},
};
static const struct i2c_device_id ap3216c_ids[] = {
{"ap3216c", (kernel_ulong_t)NULL},
{/* END OF LIST */},
};
static struct i2c_driver ap3216c_driver = {
.driver =
{
.name = "ap3216c",
.of_match_table = of_match_ids_ap3216c,
},
.probe = i2c_ap3216c_probe,
.remove = i2c_ap3216c_remove,
.id_table = ap3216c_ids,
};
static int __init ap3216c_driver_init(void) {
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
return i2c_add_driver(&ap3216c_driver);
}
module_init(ap3216c_driver_init);
static void __exit ap3216c_driver_exit(void) {
i2c_del_driver(&ap3216c_driver);
}
module_exit(ap3216c_driver_exit);
MODULE_AUTHOR("wanghaicheng.online");
MODULE_LICENSE("GPL");

(2)在i2c_driverprobe函数里面,注册一个字符设备,使用字符设备的 file_operations 操作 ap3126c。

static int i2c_ap3216c_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
ap3216c_client = client;
/* 注册字符设备 */
major = register_chrdev(major, "ap3216c_drv", &ap3216c_fops);
/* 创建设备节点 */
ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");
device_create(ap3216c_class, NULL, MKDEV(major, 0), NULL, "ap3216c_dev");
return 0;
}

(3)实现字符设备的file_operations

static int major = 0;
static struct class *ap3216c_class;
static struct i2c_client *ap3216c_client;
static int ap3216c_open(struct inode *node, struct file *file) {
/* 初始化ap3216c */
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x4);
mdelay(20);
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x3);
mdelay(250);
return 0;
}
static ssize_t ap3216c_read(struct file *file, char __user *buf, size_t size,
loff_t *offset) {
int err;
char kernel_buf[6];
int val;
if (size != 6)
return -EINVAL;
val = i2c_smbus_read_word_data(ap3216c_client, 0xA); /* read IR */
kernel_buf[0] = val & 0xff;
kernel_buf[1] = (val >> 8) & 0xff;
val = i2c_smbus_read_word_data(ap3216c_client, 0xC); /* read 光强 */
kernel_buf[2] = val & 0xff;
kernel_buf[3] = (val >> 8) & 0xff;
val = i2c_smbus_read_word_data(ap3216c_client, 0xE); /* read 距离 */
kernel_buf[4] = val & 0xff;
kernel_buf[5] = (val >> 8) & 0xff;
err = copy_to_user(buf, kernel_buf, size);
return size;
}
static struct file_operations ap3216c_fops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
};

4.i2c_client 生成#

(1)在用户态生成#

示例:

// 在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
(2)编写代码#
  • i2c_new_device

  • i2c_new_probed_device

  • i2c_register_board_info

    • 内核没有EXPORT_SYMBOL(i2c_register_board_info)
      • 使用这个函数的驱动必须编进内核里去
(3)使用设备树生成#
  • IMX6ULL

在某个 I2C 控制器的节点下,添加如下代码:

ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
  • STM32MP157

修改arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts,添加如下代码:

&i2c1 {
ap3216c@1e {
compatible = "lite-on,ap3216c";
reg = <0x1e>;
};
};

注意:设备树里 i2c1 就是 I2C BUS0。

  • 编译设备树: 在 Ubuntu 的 STM32MP157 内核目录下执行如下命令, 得到设备树文件:arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb

    make dtbs
  • 复制到 NFS 目录:

    $ cp arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb ~/nfs_rootfs/
  • 确定设备树分区挂载在哪里

    由于版本变化,STM32MP157 单板上烧录的系统可能有细微差别。 在开发板上执行cat /proc/mounts后,可以得到两种结果:

    • mmcblk2p2 分区挂载在/boot 目录下:无需特殊操作,下面把文件复制到/boot 目录即可

    • mmcblk2p2 挂载在/mnt 目录下

      • 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
        • mount /dev/mmcblk2p2 /boot
  • 更新设备树

    [root@100ask:~]# cp /mnt/stm32mp157c-100ask-512d-lcd-v1.dtb /boot
    [root@100ask:~]# sync
  • 重启开发板

5.补充:查看驱动和设备信息的命令#

lsmod
cat /proc/devices //查看设备

查看设备节点:

[root@100ask:~]# ls /dev/
adxl345 input pps1 stderr tty30 tty56
ap3216c_dev irda ptmx stdin tty31 tty57
...
[root@100ask:~]# ls -l /dev/ap3216c_dev
crw------- 1 root root 240, 0 Jan 1 00:14 /dev/ap3216c_dev

查看 i2c client:

[root@100ask:/sys/bus/i2c/devices/i2c-0]# ls
0-001e i2c-dev new_device power uevent
delete_device name of_node subsystem
[root@100ask:/sys/bus/i2c/devices/i2c-0]# cd 0-001e/
[root@100ask:/sys/bus/i2c/devices/i2c-0/0-001e]# ls
modalias name power subsystem uevent
[root@100ask:/sys/bus/i2c/devices/i2c-0/0-001e]# cat name
ap3216c

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;
/* data fields that are valid for all devices */
const struct i2c_lock_operations *lock_ops;
struct rt_mutex bus_lock;
struct rt_mutex mux_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_algorithm 传输算法和 int nr 编号

struct i2c_algorithm {
/* If an adapter algorithm can't do I2C-level access, set master_xfer
to NULL. If an adapter algorithm can do SMBus access, set
smbus_xfer. If set to NULL, the SMBus protocol is simulated
using common I2C messages */
/* master_xfer should return the number of messages successfully
processed, 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_msg

  • master_xfer_atomic:

    • 可选的函数,功能跟 master_xfer 一样,在atomic context环境下使用
    • 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
  • smbus_xfer:实现 SMBus 传输,如果不提供这个函数,SMBus 传输会使用 master_xfer 来模拟

  • smbus_xfer_atomic:

    • 可选的函数,功能跟 smbus_xfer 一样,在atomic context环境下使用
    • 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
  • functionality:返回所支持的 flags:各类 I2CFUNC*

  • reg_slave/unreg_slave:

    • 有些 I2C Adapter 也可工作在 Slave 模式,用来实现或模拟一个 I2C 设备
  • reg_slave 就是让把一个 i2c_client 注册到 I2C Adapter,换句话说就是让这个 I2C Adapter 模拟该 i2c_client

    • unreg_slave:反注册

驱动程序框架#

平台总线驱动模型:

分配、设置、注册 platform_driver 结构体。

核心是 probe 函数,它要做这几件事:

  • 根据设备树信息设置硬件(引脚、时钟等)

  • 分配、设置、注册一个 i2c_adpater 结构体:

    • i2c_adpater 的核心是 i2c_algorithm

    • i2c_algorithm 的核心是 master_xfer 函数

平台总线相关的都是老套路了,不赘述:

static const struct of_device_id i2c_bus_virtual_dt_ids[] = {
{
.compatible = "100ask,i2c-bus-virtual",
},
{/* sentinel */}};
static struct platform_driver i2c_bus_virtual_driver = {
.driver =
{
.name = "i2c-gpio",
.of_match_table = of_match_ptr(i2c_bus_virtual_dt_ids),
},
.probe = i2c_bus_virtual_probe,
.remove = i2c_bus_virtual_remove,
};
/* -------------------------------------------------------------- */
static int __init i2c_bus_virtual_init(void) {
int ret;
ret = platform_driver_register(&i2c_bus_virtual_driver);
if (ret)
printk(KERN_ERR "i2c-gpio: probe failed: %d\n", ret);
return ret;
}
module_init(i2c_bus_virtual_init);
static void __exit i2c_bus_virtual_exit(void) {
platform_driver_unregister(&i2c_bus_virtual_driver);
}
module_exit(i2c_bus_virtual_exit);
MODULE_AUTHOR("wanghaicheng.online");
MODULE_LICENSE("GPL");

关键在于 probe 函数里面:

static int i2c_bus_virtual_probe(struct platform_device *pdev) {
/* 1.get info from device tree, to set i2c_adapter/hardware */
//这里是虚拟的i2c_adapter,不需要设置硬件寄存器
/* 2.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_bus_virtual_algo 结构体,以及里面的核心函数 master_xfer 函数(这里给出模板):

const struct i2c_algorithm i2c_bus_virtual_algo = {
.master_xfer = i2c_bus_virtual_master_xfer,
.functionality = i2c_bus_virtual_func,
};
static int i2c_bus_virtual_master_xfer(struct i2c_adapter *i2c_adap,
struct i2c_msg msgs[], int num) {
int i;
for (i = 0; i < num; i++) {
// do transfer msgs[i];
}
return num;
}
static u32 i2c_bus_virtual_func(struct i2c_adapter *adap) {
return I2C_FUNC_I2C | I2C_FUNC_NOSTART | I2C_FUNC_SMBUS_EMUL |
I2C_FUNC_SMBUS_READ_BLOCK_DATA | I2C_FUNC_SMBUS_BLOCK_PROC_CALL |
I2C_FUNC_PROTOCOL_MANGLING;
}

实现 master_xfer 函数#

在虚拟的 I2C_Adapter 驱动程序里,只要实现了其中的 master_xfer 函数,这个 I2C Adapter 就可以使用了。 在 master_xfer 函数里,我们模拟一个 EEPROM,思路如下:

  • 分配一个 512 自己的 buffer,表示 EEPROM
  • 对于 slave address 为 0x50 的 i2c_msg,
    • 对于写:把 i2c_msg 的数据写入 eeprom_buffer,写到 buf[0]指定的起始位置
    • 对于读:从 eeprom_buffer 中把数据读到 i2c_msg->buf
  • 对于 slave address 为其他值的 i2c_msg,返回错误-EIO

编程:

static unsigned char eeprom_buffer[512];
static int eeprom_cur_addr = 0;
static void eeprom_emulate_xfer(struct i2c_adapter *i2c_adap,
struct i2c_msg *msg) {
int i;
if (msg->flags & I2C_M_RD) {
for (i = 0; i < msg->len; i++) {
msg->buf[i] = eeprom_buffer[eeprom_cur_addr++];
if (eeprom_cur_addr == 512)
eeprom_cur_addr = 0;
}
} else {
if (msg->len >= 1) {
eeprom_cur_addr = msg->buf[0];
for (i = 0; i < msg->len - 1; i++) {
eeprom_buffer[eeprom_cur_addr++] = msg->buf[i + 1];
if (eeprom_cur_addr == 512)
eeprom_cur_addr = 0;
}
}
}
}
static int i2c_bus_virtual_master_xfer(struct i2c_adapter *i2c_adap,
struct i2c_msg msgs[], int num) {
int i;
// emulate eeprom, addr = 0x50
for (i = 0; i < num; i++) {
if (msgs[i].addr == 0x50) {
eeprom_emulate_xfer(i2c_adap, &msgs[i]);
} else {
i = -EIO;
break;
}
}
return i;
}

设备树节点#

在设备树根节点下,添加如下代码:

i2c-bus-virtual {
compatible = "100ask,i2c-bus-virtual";
};
  1. STM32MP157
  • 修改arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dts,添加如下代码:

    / {
    i2c-bus-virtual {
    compatible = "100ask,i2c-bus-virtual";
    };
    };
  • 编译设备树: 在 Ubuntu 的 STM32MP157 内核目录下执行如下命令, 得到设备树文件:arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb

    make dtbs
  • 复制到 NFS 目录:

    $ cp arch/arm/boot/dts/stm32mp157c-100ask-512d-lcd-v1.dtb ~/nfs_rootfs/
  • 开发板上挂载 NFS 文件系统

    • vmware 使用 NAT(假设 windowsIP 为 192.168.1.100)

      [root@100ask:~]# mount -t nfs -o nolock,vers=3,port=2049,mountport=9999
      192.168.1.100:/home/book/nfs_rootfs /mnt
    • vmware 使用桥接,或者不使用 vmware 而是直接使用服务器:假设 Ubuntu IP 为 192.168.1.137

      [root@100ask:~]# mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
  • 确定设备树分区挂载在哪里

    由于版本变化,STM32MP157 单板上烧录的系统可能有细微差别。 在开发板上执行cat /proc/mounts后,可以得到两种结果:

    • mmcblk2p2 分区挂载在/boot 目录下:无需特殊操作,下面把文件复制到/boot 目录即可

    • mmcblk2p2 挂载在/mnt 目录下

      • 在视频里、后面文档里,都是更新/boot 目录下的文件,所以要先执行以下命令重新挂载:
        • mount /dev/mmcblk2p2 /boot
    • 更新设备树

      [root@100ask:~]# cp /mnt/stm32mp157c-100ask-512d-lcd-v1.dtb /boot
      [root@100ask:~]# sync
  • 重启开发板

编译安装驱动与测试#

Makefile:

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
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 += i2c_adapter_drv.o

安装:

  • 在开发板上

  • 挂载 NFS,复制文件,insmod,类似如下命令:

    mount -t nfs -o nolock,vers=3 192.168.1.137:/home/book/nfs_rootfs /mnt
    // 对于IMX6ULL,想看到驱动打印信息,需要先执行
    echo "7 4 1 7" > /proc/sys/kernel/printk
    insmod /mnt/i2c_adapter_drv.ko

使用 i2c-tools 测试:

  • 列出 I2C 总线

    i2cdetect -l

    结果类似下列的信息:

    i2c-1 i2c 21a4000.i2c I2C adapter
    i2c-4 i2c i2c-bus-virtual I2C adapter
    i2c-0 i2c 21a0000.i2c I2C adapter

    注意:不同的板子上,i2c-bus-virtual 的总线号可能不一样,上问中总线号是 4。

  • 检查虚拟总线下的 I2C 设备

    // 假设虚拟I2C BUS号为4
    [root@100ask:~]# i2cdetect -y -a 4
    0 1 2 3 4 5 6 7 8 9 a b c d e f
    00: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    50: 50 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
    70: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
  • 读写模拟的 EEPROM

    // 假设虚拟I2C BUS号为4
    [root@100ask:~]# i2cset -f -y 4 0x50 0 0x55 // 往0地址写入0x55
    [root@100ask:~]# i2cget -f -y 4 0x50 0 // 读0地址
    0x55

GPIO 模拟 I2C,实现 I2C_Adapter 驱动#

参考资料:

  • i2c_spec.pdf
  • Linux 文档
    • Linux-5.4\Documentation\devicetree\bindings\i2c\i2c-gpio.yaml
    • Linux-4.9.88\Documentation\devicetree\bindings\i2c\i2c-gpio.txt
  • Linux 驱动源码
    • Linux-5.4\drivers\i2c\busses\i2c-gpio.c
    • Linux-4.9.88\drivers\i2c\busses\i2c-gpio.c

写在前面:

怎么使用 i2c-gpio?

我们只需要设置设备树,在里面添加一个节点即可。

  • 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
  • GPIO 设为输出、开极/开漏(open collector/open drain)
  • 要有上拉电阻

设备树#

i2c_gpio_100ask {
compatible = "i2c-gpio";
gpios = <&gpio4 20 0 /* sda */
&gpio4 21 0 /* scl */
>;
i2c-gpio,delay-us = <5>; /* ~100 kHz = 1 / (2*delay-us) */
#address-cells = <1>;
#size-cells = <0>;
};

内核的 i2c-gpio.c#

i2c-gpio 的层次:

  • 解析设备树
  • i2c-gpio.c:i2c_bit_add_numbered_bus
  • i2c-algo-bit.c:adap->algo = &i2c_bit_algo;

在 i2c-gpio.c 中是老套路,probe函数注册驱动程序到总线平台,读取设备树节点信息进行配置(获得时间参数、SDA/SCL 引脚)。

probe函数最后会使用i2c_bit_add_numbered_bus注册adapter,这个函数在drivers/i2c/algos/i2c-algo-bit.c定义:

int i2c_bit_add_numbered_bus(struct i2c_adapter *adap)
{
return __i2c_bit_add_bus(adap, i2c_add_numbered_adapter);
}
static int __i2c_bit_add_bus(struct i2c_adapter *adap,
int (*add_adapter)(struct i2c_adapter *))
{
struct i2c_algo_bit_data *bit_adap = adap->algo_data;
int ret;
if (bit_test) {
ret = test_bus(adap);
if (bit_test >= 2 && ret < 0)
return -ENODEV;
}
/* register new adapter to i2c module... */
adap->algo = &i2c_bit_algo;
adap->retries = 3;
if (bit_adap->getscl == NULL)
adap->quirks = &i2c_bit_quirk_no_clk_stretch;
ret = add_adapter(adap);
if (ret < 0)
return ret;
/* Complain if SCL can't be read */
if (bit_adap->getscl == NULL) {
dev_warn(&adap->dev, "Not I2C compliant: can't read SCL\n");
dev_warn(&adap->dev, "Bus may be unreliable\n");
}
return 0;
}

关键在于adap->algo = &i2c_bit_algo;传输算法,里面有bit_xfer使用 gpio 传输 i2c 消息。

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)
{
struct i2c_msg *pmsg;
struct i2c_algo_bit_data *adap = i2c_adap->algo_data;
int i, ret;
unsigned short nak_ok;
if (adap->pre_xfer) {
ret = adap->pre_xfer(i2c_adap);
if (ret < 0)
return ret;
}
bit_dbg(3, &i2c_adap->dev, "emitting start condition\n");
i2c_start(adap);
for (i = 0; i < num; i++) {
pmsg = &msgs[i];
nak_ok = pmsg->flags & I2C_M_IGNORE_NAK;
if (!(pmsg->flags & I2C_M_NOSTART)) {
if (i) {
bit_dbg(3, &i2c_adap->dev, "emitting "
"repeated start condition\n");
i2c_repstart(adap);
}
ret = bit_doAddress(i2c_adap, pmsg);
if ((ret != 0) && !nak_ok) {
bit_dbg(1, &i2c_adap->dev, "NAK from "
"device addr 0x%02x msg #%d\n",
msgs[i].addr, i);
goto bailout;
}
}
if (pmsg->flags & I2C_M_RD) {
/* read bytes into buffer*/
ret = readbytes(i2c_adap, pmsg);
if (ret >= 1)
bit_dbg(2, &i2c_adap->dev, "read %d byte%s\n",
ret, ret == 1 ? "" : "s");
if (ret < pmsg->len) {
if (ret >= 0)
ret = -EIO;
goto bailout;
}
} else {
/* write bytes from buffer */
ret = sendbytes(i2c_adap, pmsg);
if (ret >= 1)
bit_dbg(2, &i2c_adap->dev, "wrote %d byte%s\n",
ret, ret == 1 ? "" : "s");
if (ret < pmsg->len) {
if (ret >= 0)
ret = -EIO;
goto bailout;
}
}
}
ret = i;
bailout:
bit_dbg(3, &i2c_adap->dev, "emitting stop condition\n");
i2c_stop(adap);
if (adap->post_xfer)
adap->post_xfer(i2c_adap);
return ret;
}

具体分析i2c-algo-bit.c#

从上面的 bit_xfer 函数开始看。

i2c_start

static void i2c_start(struct i2c_algo_bit_data *adap)
{
/* assert: scl, sda are high */
setsda(adap, 0);
udelay(adap->udelay);
scllo(adap);
}

发生消息:

i2c_outb函数中,发送一个字节的数据,从 bit[7]到 bit[0]

  • setsda
  • 延迟 udelay/2
  • sclhi 拉高 scl
  • 延迟 udelay
  • scllo 拉低 scl
  • 延迟 udelay/2

由此可以得出传送一位数据需要 2*udelay 的时间

static int sendbytes(struct i2c_adapter *i2c_adap, struct i2c_msg *msg)
{
const unsigned char *temp = msg->buf;
int count = msg->len;
unsigned short nak_ok = msg->flags & I2C_M_IGNORE_NAK;
int retval;
int wrcount = 0;
while (count > 0) {
retval = i2c_outb(i2c_adap, *temp); //发送一个字节数据
...
static int i2c_outb(struct i2c_adapter *i2c_adap, unsigned char c)
{
int i;
int sb;
int ack;
struct i2c_algo_bit_data *adap = i2c_adap->algo_data;
/* assert: scl is low */
for (i = 7; i >= 0; i--) {
sb = (c >> i) & 1;
setsda(adap, sb);
udelay((adap->udelay + 1) / 2);
if (sclhi(adap) < 0) { /* timed out */
bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
"timeout at bit #%d\n", (int)c, i);
return -ETIMEDOUT;
}
/* FIXME do arbitration here:
* if (sb && !getsda(adap)) -> ouch! Get out of here.
*
* Report a unique code, so higher level code can retry
* the whole (combined) message and *NOT* issue STOP.
*/
scllo(adap);
}
sdahi(adap);
if (sclhi(adap) < 0) { /* timeout */
bit_dbg(1, &i2c_adap->dev, "i2c_outb: 0x%02x, "
"timeout at ack\n", (int)c);
return -ETIMEDOUT;
}
/* read ack: SDA should be pulled down by slave, or it may
* NAK (usually to report problems with the data we wrote).
*/
ack = !getsda(adap); /* ack: sda is pulled low -> success */
bit_dbg(2, &i2c_adap->dev, "i2c_outb: 0x%02x %s\n", (int)c,
ack ? "A" : "NA");
scllo(adap);
return ack;
/* assert: scl is low (sda undef) */
}

具体芯片下 I2C_Adapter 驱动#

参考资料:

  • Linux 内核真正的 I2C 控制器驱动程序
    • IMX6ULL: Linux-4.9.88\drivers\i2c\busses\i2c-imx.c
    • STM32MP157: Linux-5.4\drivers\i2c\busses\i2c-stm32f7.c
  • 芯片手册
    • IMXX6ULL:IMX6ULLRM.pdf
      • Chapter 31: I2C Controller (I2C)
    • STM32MP157:DM00327659.pdf
      • 52 Inter-integrated circuit (I2C) interface

I2C 控制器的通用结构#

一般含有以下寄存器:

  • 控制寄存器
  • 发送寄存器、移位寄存器
  • 接收寄存器、移位寄存器
  • 状态寄存器
  • 中断寄存器

数据放入发送寄存器后,cpu 就可以返回继续执行了,i2c 控制器会通过移位寄存器一位一位地发送出去。

接收则是从移位寄存器放入接收寄存器,cpu 只需要一次性读取接收寄存器的数据即可。

IMX6ULL 和 MP157 的 I2C 控制器#

IMX6ull:

STM32MP157:

08_i2c_block_stm32mp157

分析代码#

1.设备树#

  • IMX6ULL: arch/arm/boot/dts/imx6ull.dtsi

    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"; // 在100ask_imx6ull-14x14.dts把它改为了"okay"
    };
  • STM32MP157: arch/arm/boot/dts/stm32mp151.dtsi

    i2c1: i2c@40012000 {
    compatible = "st,stm32mp15-i2c";
    reg = <0x40012000 0x400>;
    interrupt-names = "event", "error";
    interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
    <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&rcc I2C1_K>;
    resets = <&rcc I2C1_R>;
    #address-cells = <1>;
    #size-cells = <0>;
    dmas = <&dmamux1 33 0x400 0x80000001>,
    <&dmamux1 34 0x400 0x80000001>;
    dma-names = "rx", "tx";
    power-domains = <&pd_core>;
    st,syscfg-fmp = <&syscfg 0x4 0x1>;
    wakeup-source;
    status = "disabled"; // 在stm32mp15xx-100ask.dtsi把它改为了"okay"
    };

2.驱动程序分析#

读 I2C 数据时,要先发出设备地址,这是写操作,然后再发起读操作,涉及写、读操作。所以以读 I2C 数据为例讲解核心代码。

同样也是看 adapter 里面的algo成员master_xfer

static struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer,
.functionality = i2c_imx_func,
};
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;
bool enable_runtime_pm = false;
struct imx_i2c_struct *i2c_imx = i2c_get_adapdata(adapter);
...
/* Start I2C transfer */
result = i2c_imx_start(i2c_imx);
if (result) {
if (i2c_imx->adapter.bus_recovery_info) {
i2c_recover_bus(&i2c_imx->adapter);
result = i2c_imx_start(i2c_imx);
}
}
...
/* read/write data */
for (i = 0; i < num; i++) {
...
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]);
else
result = i2c_imx_write(i2c_imx, &msgs[i]);
}
if (result)
goto fail0;
}
...
}

i2c_imx_start:设置状态寄存器、控制寄存器,开启 I2C 传输

static int i2c_imx_start(struct imx_i2c_struct *i2c_imx)
{
unsigned int temp = 0;
int result;
dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);
i2c_imx_set_clk(i2c_imx);
imx_i2c_write_reg(i2c_imx->ifdr, i2c_imx, IMX_I2C_IFDR);
/* Enable I2C controller */
imx_i2c_write_reg(i2c_imx->hwdata->i2sr_clr_opcode, i2c_imx, IMX_I2C_I2SR);
imx_i2c_write_reg(i2c_imx->hwdata->i2cr_ien_opcode, i2c_imx, IMX_I2C_I2CR);
/* Wait controller to be stable */
usleep_range(50, 150);
/* Start I2C transaction */
temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
temp |= I2CR_MSTA;
imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);
result = i2c_imx_bus_busy(i2c_imx, 1);
if (result)
return result;
i2c_imx->stopped = 0;
temp |= I2CR_IIEN | I2CR_MTX | I2CR_TXAK;
temp &= ~I2CR_DMAEN;
imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);
return result;
}

i2c_imx_read:

static int i2c_imx_read(struct imx_i2c_struct *i2c_imx, struct i2c_msg *msgs, bool is_lastmsg)
{
int i, result;
unsigned int temp;
int block_data = msgs->flags & I2C_M_RECV_LEN;
...
/* write slave address: 写数据寄存器,即写入读取的地址 */
imx_i2c_write_reg((msgs->addr << 1) | 0x01, i2c_imx, IMX_I2C_I2DR);
result = i2c_imx_trx_complete(i2c_imx);
if (result)
return result;
result = i2c_imx_acked(i2c_imx); // 等待应答
if (result)
return result;
/* setup bus to read data */
...
/* read data */
for (i = 0; i < msgs->len; i++) {
u8 len = 0;
// 等待传输完成
result = i2c_imx_trx_complete(i2c_imx);
if (result)
return result;
...
msgs->buf[i] = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2DR);
...
}
return 0;
}

i2c_imx_stop(i2c_imx);

STM32MP157:函数stm32f7_i2c_xfer分析 这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。

(太累了,过几天补充分析 - 2024.7.4)