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_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
环境下使用 - 比如在关机之前、所有中断都关闭的情况下,用来访问电源管理芯片
- 可选的函数,功能跟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 = 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";
};
- 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 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
- 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]);
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
分析
这函数完全由中断程序来驱动:启动传输后,就等待;在中断服务程序里传输下一个数据,知道传输完毕。