欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 教育 > 锐评 > I2C子系统-内核视角

I2C子系统-内核视角

2024/10/24 4:34:18 来源:https://blog.csdn.net/ObviouslyCheng/article/details/140450097  浏览:    关键词: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_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_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 = 0x50for (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 cleanrm -rf modules.orderobj-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/printkinsmod /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 40  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]);elseresult = 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分析
这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com