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:各类I2C_FUNC_*

  • 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:
在这里插入图片描述

分析代码

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)
_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`:

```c
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分析
这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。

相关推荐

  1. 模拟I2C通信

    2024-07-16 08:58:04       49 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-16 08:58:04       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-16 08:58:04       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-16 08:58:04       58 阅读
  4. Python语言-面向对象

    2024-07-16 08:58:04       69 阅读

热门阅读

  1. 北京交通大学学报-社会科学版

    2024-07-16 08:58:04       21 阅读
  2. 【AI应用探讨】—生成对抗网络(GAN)应用场景

    2024-07-16 08:58:04       25 阅读
  3. QT教程-十四, QSpacerItem(可伸缩的空间项)

    2024-07-16 08:58:04       20 阅读
  4. 初学者指南:如何搭建和配置 Nginx 服务器

    2024-07-16 08:58:04       22 阅读
  5. Canvas

    2024-07-16 08:58:04       23 阅读
  6. 增加扫地机器人的智能化功能

    2024-07-16 08:58:04       24 阅读
  7. 网络爬虫Scrapy shell 的使用和介绍

    2024-07-16 08:58:04       26 阅读
  8. 卡码网语言基础课 | 10. 平均绩点

    2024-07-16 08:58:04       24 阅读
  9. RoCE-SAN和RoCE区别

    2024-07-16 08:58:04       21 阅读
  10. CSS基础

    2024-07-16 08:58:04       20 阅读