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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/799602.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Docker的虚拟化安装、常用命令和使用案例

文章目录 一、Docker的虚拟机安装1、完成虚拟机的更新2、完成Docker安装3、配置镜像加速器 二、Docker常用命令三、Docker的容器创建四、理解虚拟机中的Docker容器 一、Docker的虚拟机安装 1、完成虚拟机的更新 详见我的文章。 2、完成Docker安装 yum list installed|grep …

第二周周日学习总结

题目总结 1. 给你一个仅由数字组成的字符串 s&#xff0c;在最多交换一次 相邻 且具有相同 奇偶性 的数字后&#xff0c;返回可以得到的 字典序最小的字符串 。 如果两个数字都是奇数或都是偶数&#xff0c;则它们具有相同的奇偶性。例如&#xff0c;5 和 9、2 和 4 奇偶性…

2024年7月9日~2024年7月15日周报

目录 一、前言 二、完成情况 2.1 特征图保存方法 2.1.1 定义网络模型 2.1.2 定义保存特征图的钩子函数 2.1.3 为模型层注册钩子 2.1.4 运行模型并检查特征图 2.2 实验情况 三、下周计划 一、前言 本周的7月11日~7月14日参加了机器培训的学习讨论会&#xff0c;对很多概…

六、 SpringBoot 配置⽂件 ★ ✔【value的引号注意事项、@ConfigurationProperties 、】

六、 SpringBoot 配置⽂件 本节⽬标1. 配置⽂件作⽤2. 配置⽂件快速⼊⼿3. 配置⽂件的格式4. properties 配置⽂件说明4.1 properties 基本语法4.2 读取配置⽂件4.3 properties 缺点分析 5. yml 配置⽂件说明5.1 yml 基本语法5.2 yml 使⽤进阶5.2.1 yml 配置不同数据类型及 nul…

记录些MySQL题集(1)

Innodb 是如何实现事务的&#xff1f; InnoDB是MySQL数据库的一个存储引擎&#xff0c;它支持事务处理。事务处理是数据库管理系统执行过程中的一个逻辑单位&#xff0c;由一个或多个SQL语句组成&#xff0c;这些语句要么全部执行&#xff0c;要么全部不执行&#xff0c;是一个…

Poetry2Image:专为中文古诗词图像生成,忠于原诗意境和语义。

直接基于文本的图像生成通常会导致丢失图像中的关键元素。为了解决此问题&#xff0c;哈工大提出Poetry2Image&#xff0c;通过实施有针对性的图像校正解决这个问题&#xff0c;有效地捕捉这首诗所传达的语义和艺术精髓。 Poetry2Image流程分为如下几步&#xff1a; 搜索和翻译…

免费进销存软件哪个好用?首选象过河

在快节奏的商业环境中&#xff0c;进销存管理一直是不可忽视的重要环节&#xff0c;关乎着企业的运营成本控制和运营效率的高低。传统的纸质记录已难以满足企业发展需求&#xff0c;很多企业管理者为了节约成本&#xff0c;都想寻找一款免费进销存软件。那么&#xff0c;免费进…

(day18) leetcode 204.计数质数

描述 给定整数 n &#xff0c;返回 所有小于非负整数 n 的质数的数量 。 示例 1&#xff1a; 输入&#xff1a;n 10 输出&#xff1a;4 解释&#xff1a;小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 。示例 2&#xff1a; 输入&#xff1a;n 0 输出&#xff1a;0示例 3…

计算机系统复习——文件系统和目录

文件系统 存储管理&#xff1a; 文件系统负责将文件存储在存储设备&#xff08;如硬盘、固态硬盘&#xff09;中&#xff0c;并记录文件的位置和大小。 文件访问控制&#xff1a; 文件系统管理文件的访问权限&#xff0c;确保只有授权用户可以读取、写入或执行文件。 数据…

PostgreSQL 中如何解决因长事务阻塞导致的其他事务等待问题?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 PostgreSQL 中如何解决因长事务阻塞导致的其他事务等待问题&#xff1f;一、了解长事务阻塞的原因&…

Qt进阶版五子棋

五子棋是一种两人对弈的棋类游戏&#xff0c;目标是在横、竖、斜任意方向上连成五个子。在Qt中实现五子棋程序&#xff0c;你需要设计棋盘界面、处理下棋逻辑、判断胜负等。以下是实现一个基本五子棋程序的步骤&#xff1a; 创建项目和界面 使用Qt Creator创建一个新的Qt Widge…

人工智能大模型讲师培训老师叶梓介绍及多模态大模型原理与实践提纲

培训需要解决的问题 通过本次培训&#xff0c;拓展对多模态AI应用领域的视野&#xff0c;帮助团队聚焦AI赋能创新突破&#xff0c;提升对AI服务的技术认知与理解&#xff0c;更好地助力业务智能化业务建设。 培训时长 1天 培训老师介绍 叶梓&#xff0c;工学博士&#xff0…

【人工智能】Transformers之Pipeline(二):自动语音识别(automatic-speech-recognition)

​​​​​​​ 目录 一、引言 二、自动语音识别&#xff08;automatic-speech-recognition&#xff09; 2.1 概述 2.2 技术原理 2.2.1 whisper模型 2.2.2 Wav2vec 2.0模型 2.3 pipeline参数 2.3.1 pipeline对象实例化参数​​​​​​​ 2.3.2 pipeline对象使用参数…

HiFi Asia 2024圆满落幕,琛蓝生物分享企业成果

一个时代&#xff0c;一个风口。其中有些风口切中时代脉搏&#xff0c;顺势而为的人因此成功登顶&#xff0c;有些风口则被证伪&#xff0c;热潮散去之后留下一地鸡毛。在当今时代&#xff0c;倘若要寻找下一个时代风口&#xff0c;那么“大健康”毫无疑问是确定性最强大那一个…

BernNet Learning Arbitrary Graph Spectral Filters via Bernstein Approximation

发表于:neurips21 推荐指数: #paper/⭐⭐ 设定:在本文中,h是过滤器. bernstein 多项式逼近(这个证明有点稀里糊涂的,反正我觉得一点点问题,可能因为我水平低) p K ( t ) : ∑ k 0 K θ k ⋅ b k K ( t ) ∑ k 0 K f ( k K ) ⋅ ( K k ) ( 1 − t ) K − k t k . p_K(t):…

下载利器:IDM绿色版/一款Windows平台多线程下载工具

大家好&#xff01;我是闷声轻创&#xff01;今天给你们分享一款神器Internet Download Manager&#xff08;简称IDM&#xff09;这款软件是需要激活需要付费的【免注册激活&#xff0c;无假冒序列号弹窗】适用于Windows 系统&#xff0c;对于经常需要下载大量数据的用户来说&a…

自定义方法耗时监控告警

自定义方法耗时监控告警 用于记录代码耗时&#xff0c;当代码耗时超过指定阈值时打印告警日志 自定义注解 通过自定义注解的方式可以更方便的使用&#xff0c;只需要在需要做耗时兼容的方法上增加上该注解即可 package com.huakai.springenv.aspect.profiler;import java.lan…

Python与自动化脚本编写

Python与自动化脚本编写 Python因其简洁的语法和强大的库支持&#xff0c;成为了自动化脚本编写的首选语言之一。在这篇文章中&#xff0c;我们将探索如何使用Python来编写自动化脚本&#xff0c;以简化日常任务。 一、Python自动化脚本的基础 1. Python在自动化中的优势 Pyth…

i18n、L10n、G11N 和 T9N 的含义

注&#xff1a;机翻&#xff0c;未校对。 Looking into localization for the first time can be terrifying, if only due to all of the abbreviations. But the meaning of i18n, L10n, G11N, and T9N, are all very easy to understand. 第一次研究本地化可能会很可怕&…

Leetcode3202. 找出有效子序列的最大长度 II

Every day a Leetcode 题目来源&#xff1a;3202. 找出有效子序列的最大长度 II 解法1&#xff1a;动态规划 本题是选与不选的子序列问题&#xff0c;可以尝试给出这样的状态定义&#xff1a; dp[i][j]&#xff1a;以 nums[i] 结尾模 k 后值为 j 的最长子序列的长度。 那么…