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_driver
的probe
函数里面,注册一个字符设备,使用字符设备的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
- 在视频里、后面文档里,都是更新/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
环境下使用 - 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
- 可选的函数,功能跟master_xfer一样,在
-
smbus_xfer:实现SMBus传输,如果不提供这个函数,SMBus传输会使用master_xfer来模拟
-
smbus_xfer_atomic:
- 可选的函数,功能跟smbus_xfer一样,在
atomic context
环境下使用 - 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
- 可选的函数,功能跟smbus_xfer一样,在
-
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";};
- 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
- 在视频里、后面文档里,都是更新/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
- IMX6ULL:
- 芯片手册
- IMXX6ULL:
IMX6ULLRM.pdf
Chapter 31: I2C Controller (I2C)
- STM32MP157:
DM00327659.pdf
52 Inter-integrated circuit (I2C) interface
- IMXX6ULL:
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
分析
这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。