当前位置: 首页 > article >正文

linux下i2c开发与框架源码分析

目录

1 概述

2 I2c子系统框架

3 I2C的使用流程

3.1 在驱动里使用

3.2 在应用层使用

3.3 I2ctool的使用

4 为硬件i2c注册一个适配器

5 i2c子系统源码流程分析

5.1 i2c device与driver绑定过程

5.1.1 Driver的注册与处理

5.1.2 Client device的生成

5.2 I2c的发送与接收

5.3 i2c总线设备生成过程

6 i2c作为slave的使用


1 概述

        本文主要描述了i2c的使用以及分析了大部分i2c子系统源码和实现原理,本文以读者对i2c硬件原理已掌握为基础来描述,需要读者理解基础的i2c通信过程。

2 I2c子系统框架

        从分层角度上看,i2c子系统大致分为设备驱动层client、i2c核心层和i2c适配器层,如下图大致描述了整个i2c应用和内核框架的关系逻辑,从上到下,用户可通过底层提供的总线设备或者外设设备来访问挂载在总线上的i2c设备。

        i2c子系统向驱动层提供了i2c client,每一个i2c设备将被实现成一个client,设备驱动在拿到i2c_client后,即可通过该对象来读写i2c数据访问i2c设备。I2c核心层向下也提供了i2c适配器层,每一个硬件i2c都被实现成一个i2c adapter,主要负责向i2c核心层提供硬件操作接口

3 I2C的使用流程

        本节讲i2c在驱动层如何被调用使用,没有特殊说明,均认为i2c作为master,后面章节将介绍i2c作为slave的用法。

3.1 在驱动里使用

        在i2c的驱动应用中,比较常见的是先在设备树里的i2c节点挂在硬件上挂在到该i2c总线的i2c设备,例如一个sensor的节点:

&i2c2 {
    status = "okay";
...
    ov5695: ov5695@36 {
        compatible = "ovti,ov5695";
        reg = <0x36>;
        avdd-supply = <&vcc2v8_dvp>;
        clocks = <&cru SCLK_CIF_OUT>;
        clock-names = "xvclk";
        dvdd-supply = <&vcc1v5_dvp>;
        dovdd-supply = <&vcc1v8_dvp>;
        pinctrl-names = "default";
        pinctrl-0 = <&cif_clkout_m0 &mipi_pdn>;
        reset-gpios = <&gpio2 RK_PB6 GPIO_ACTIVE_LOW>;
        ...
    };
};

        然后在sensor的驱动里,调用i2c_register_driver将自己定义好的struct i2c_driver传入该接口,i2c总线将会调用我们自定义好的probe函数,让设备驱动程序加载起来,有时也会用i2c注册宏module_i2c_driver来做。

static struct i2c_driver ov5695_i2c_driver = {
    .driver = {
        .name = "ov5695",
        .pm = &ov5695_pm_ops,
        .of_match_table = of_match_ptr(ov5695_of_match),
    },
    .probe      = ov5695_probe,
    .remove     = ov5695_remove,
};
module_i2c_driver(ov5695_i2c_driver);
----------------------或者-----------------------
static int mpu6050_driver_init(void)
{
  i2c_add_driver(&mpu6050_driver);
  return 0;
}
static void mpu6050_driver_exit(void)
{
  i2c_del_driver(&mpu6050_driver);
}
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);

        之后将进入probe函数,传入的i2c_client结构体,用于i2c数据收发

static int ov5695_probe(struct i2c_client *client)
{
}
static int ov5695_read_reg(struct i2c_client *client, u16 reg, unsigned int len, u32 *val)
{
    struct i2c_msg msgs[2];
   ...
    int ret;
    if (len > 4)
        return -EINVAL;
    data_be_p = (u8 *)&data_be;
    /* Write register address */
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;
    msgs[0].len = 2;
    msgs[0].buf = (u8 *)&reg_addr_be;
    /* Read data from register */
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;
    msgs[1].len = len;
    msgs[1].buf = &data_be_p[4 - len];
    ret = i2c_transfer(client->adapter, msgs, ARRAY_SIZE(msgs));
    ...
    return 0;
}

3.2 在应用层使用

        I2c子系统同时提供了每个i2c总线的设备,应用可以直接打开i2c总线设备,传入从机地址来进行通信

int fd = open("/dev/i2c-0", O_RDWR);
int i2c_write(uint8_t slave, uint8_t reg, uint8_t * data, int len)
{
  unsigned char buf[1024];
  struct i2c_rdwr_ioctl_data i2c_data;
  struct i2c_msg i2c_msg;
  i2c_data.nmsgs = 1;
  i2c_data.msgs = &i2c_msg ;
  ioctl(_fd, I2C_TIMEOUT, 1);
  ioctl(_fd, I2C_RETRIES, 2);
  memset(buf, 0, 1024);
  buf[0] = reg;
  memcpy(&buf[1], buf, len);
  i2c_data.msgs[0].addr = slave;
  i2c_data.msgs[0].flags = 0; 
  i2c_data.msgs[0].buf = &buf[0];
  i2c_data.msgs[0].len = len+1;
  int ret = ioctl(fd, I2C_RDWR, (unsigned long)&i2c_data);
 ...
 return 0;
}
int i2c_read(int8_t slave, int8_t reg, uint8_t * data, int len)
    unsigned char buf[2];
    struct i2c_rdwr_ioctl_data i2c_data;
    struct i2c_msg i2c_msg[2] ;
    i2c_data.nmsgs = 2;
    i2c_data.msgs = &i2c_msg[0] ;
    ioctl(_fd, I2C_TIMEOUT, 1);
    ioctl(_fd, I2C_RETRIES, 2);
    buf[0] = reg ;
    i2c_data.msgs[0].addr = slave;
    i2c_data.msgs[0].flags = 0;     
    i2c_data.msgs[0].buf = &buf[0];
    i2c_data.msgs[0].len = 1;
    i2c_data.msgs[1].addr = slave;
    i2c_data.msgs[1].flags = 1;    
    i2c_data.msgs[1].buf = data;
    i2c_data.msgs[1].len = len;
    int ret = ioctl(_fd, I2C_RDWR, (unsigned long)&i2c_data);
    ....
}
close(fd);

3.3 I2ctool的使用

        有时在调试时,会使用i2ctool命令行进行测试,使用这些命令行接口,需要先把该库打包进行文件系统,或者移植。

        i2cdetect:用于扫描 i2c 总线上的设备,并显示地址。
        i2cset:设置i2c设备某个寄存器的值。
        i2cget:读取i2c设备某个寄存器的值。
        i2cdump:读取某个i2c设备所有寄存器的值。
        i2ctransfer:一次性读写多个字节。

注:参考https://blog.csdn.net/yyz_1987/article/details/131953108

        驱动中常用接口:

        //发送接收消息

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
static inline int i2c_master_recv(const struct i2c_client *client,
                  char *buf, int count)
static inline int i2c_master_send(const struct i2c_client *client,
                  const char *buf, int count)

        //注册与注销:

#define i2c_add_driver(driver) \
    i2c_register_driver(THIS_MODULE, driver)
int i2c_register_driver(struct module *owner, struct i2c_driver *driver) 
void i2c_del_driver(struct i2c_driver *driver)

        另外i2c还提供了SMBus设备相关接口,不在此文讨论范围。

4 为硬件i2c注册一个适配器

        如何注册一个i2c adapter

        以rockchip为例子,分析rockchip注册一个adapter源码,实现在drivers/i2c/busses/i2c-rk3x.c

设备树对i2c2的定义如下:

i2c2: i2c@ff1a0000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
        reg = <0x0 0xff1a0000 0x0 0x1000>;
        clocks = <&cru SCLK_I2C2>, <&cru PCLK_I2C2>;
        clock-names = "i2c", "pclk";
        interrupts = <GIC_SPI 9 IRQ_TYPE_LEVEL_HIGH>;
        pinctrl-names = "default";
        pinctrl-0 = <&i2c2_xfer>;
        #address-cells = <1>;
        #size-cells = <0>;
        status = "disabled";
    };

        将和驱动匹配

static struct platform_driver rk3x_i2c_driver = {
    .probe   = rk3x_i2c_probe,
    .remove_new = rk3x_i2c_remove,
    .driver  = {
        .name  = "rk3x-i2c",
        .of_match_table = rk3x_i2c_match,
        .pm = &rk3x_i2c_pm_ops,
    },
};
module_platform_driver(rk3x_i2c_driver);

        注册过程:

rk3x_i2c_probe
struct rk3x_i2c *i2c;
//adapter结构体的初始化,其中rk3x_i2c_algorithm是硬件接口,后面分析
    strscpy(i2c->adap.name, "rk3x-i2c", sizeof(i2c->adap.name));
    i2c->adap.owner = THIS_MODULE;
    i2c->adap.algo = &rk3x_i2c_algorithm;
    i2c->adap.retries = 3;
    i2c->adap.dev.of_node = np;
    i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;
//后续大部分是获取i2c设备节点的信息,包括基地址、中断申请、时钟获取和配置等
i2c->regs = devm_platform_ioremap_resource(pdev, 0);
ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq,
                   0, dev_name(&pdev->dev), i2c);
ret = clk_prepare(i2c->clk);
//最后调用i2c子系统api,注册一个adapter
ret = i2c_add_adapter(&i2c->adap);
------------------------------------------------------------------------------
注册到adapter的硬件回调如下,这些将提供给i2c子系统操作硬件的接口,主要是发送与接收数据
static const struct i2c_algorithm rk3x_i2c_algorithm = {
    .master_xfer        = rk3x_i2c_xfer,//发送与接收
    .master_xfer_atomic = rk3x_i2c_xfer_polling,//原子发送接收
    .functionality      = rk3x_i2c_func,//当前i2c 适配器支持哪些特性
};

        以上便是一个i2c adapter的注册过程,比较简单,需要注意的是,在设备树定义了多个i2c节点是,这个driver将被调用多次,即将申请多个adapter,这里一个硬件i2c就申请一个adapter。

i2c0: i2c@ff180000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
        ...
    };
    i2c1: i2c@ff190000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
        ....
    };
    i2c2: i2c@ff1a0000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
        ...
    };
    i2c3: i2c@ff1b0000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
       ....
    };

        其他都是I2C硬件接口配置过程,不做详细分析。

5 i2c子系统源码流程分析

        I2c子系统源码实现在Linux内核的drivers/i2c下,这里有芯片产商文件,也有i2c子系统核心文件:

        drivers/i2c/i2c-boardinfo.c//i2c ip信息整理接口

        drivers/i2c/i2c-core-acpi.c//acpi设备接口

        drivers/i2c/i2c-core-base.c//核心文件

        drivers/i2c/i2c-core-of.c//设备树解析相关文件

        drivers/i2c/i2c-core-slave.c//i2c从机接口

        drivers/i2c/i2c-core-smbus.c/smbus设备相关接口

        drivers/i2c/i2c-dev.c//i2c总线设备相关文件

5.1 i2c device与driver绑定过程

        I2c子系统如何生成client device与client驱动匹配后调用client的probe函数的流程

5.1.1 Driver的注册与处理

        以sensor节点为例子,在设备树中,有如下设备树i2c定义,i2c节点相关信息定义还有定义在i2c2下的ov5695节点。

 i2c2: i2c@ff1a0000 {
        compatible = "rockchip,px30-i2c", "rockchip,rk3399-i2c";
        reg = <0x0 0xff1a0000 0x0 0x1000>;
        clocks = <&cru SCLK_I2C2>, <&cru PCLK_I2C2>;
        clock-names = "i2c", "pclk";
        interrupts = <GIC_SPI 9 IRQ_TYPE_LEVEL_HIGH>;
        pinctrl-names = "default";
        pinctrl-0 = <&i2c2_xfer>;
        #address-cells = <1>;
        #size-cells = <0>;
        status = "disabled";
    };
&i2c2 {
    ...
    ov5695: ov5695@36 {
        compatible = "ovti,ov5695";
        reg = <0x36>;
        avdd-supply = <&vcc2v8_dvp>;
        clocks = <&cru SCLK_CIF_OUT>;
        clock-names = "xvclk";
        dvdd-supply = <&vcc1v5_dvp>;
        dovdd-supply = <&vcc1v8_dvp>;
        pinctrl-names = "default";
        pinctrl-0 = <&cif_clkout_m0 &mipi_pdn>;
        reset-gpios = <&gpio2 RK_PB6 GPIO_ACTIVE_LOW>;
        ...
    };
};

        在Linux初始化的时候,这里会被Linux解析成两个device,且i2c2这个device下挂着ov5695这个device,ov5695的driver定义在drivers/media/i2c/ov5695.c,看驱动注册部分:

static struct i2c_driver ov5695_i2c_driver = {
    .driver = {
        .name = "ov5695",
        .pm = &ov5695_pm_ops,
        .of_match_table = of_match_ptr(ov5695_of_match),
    },
    .probe      = ov5695_probe,
    .remove     = ov5695_remove,
};
module_i2c_driver(ov5695_i2c_driver);
MODULE_DESCRIPTION("OmniVision ov5695 sensor driver");
MODULE_LICENSE("GPL v2");

        定义了一个name为ov5695 的sensor driver,通过module_i2c_driver 注册成一个driver,module_i2c_driver 声明

clude/linux/i2c.h
#define module_i2c_driver(__i2c_driver) \
    module_driver(__i2c_driver, i2c_add_driver, \
            i2c_del_driver)
#define i2c_add_driver(driver) \
    i2c_register_driver(THIS_MODULE, driver)

include/linux/device/driver.h
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \
{ \
    return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \
{ \
    __unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
整理如上宏定义,最后调用方式
static int __init ov9282_driver_init(void) 
{ 
	return i2c_register_driver(THIS_MODULE, &(ov9282_driver)); 
} 
module_init(ov9282_driver_init);
static void __exit ov9282_driver_exit(void) 
{ 
	i2c_del_driver(&(ov9282_driver)); 
}
module_exit(ov9282_driver_exit);

        即最后调用i2c_register_driver 来注册,看这个函数如何注册一个driver的:

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)(drivers/i2c/i2c-core-base.c)
  driver->driver.owner = owner;
  driver->driver.bus = &i2c_bus_type;//选择总线,该总线在i2c子系统初始化时加载
  INIT_LIST_HEAD(&driver->clients);
res = driver_register(&driver->driver);//将该driver注册到i2c_bus_type这个总线上。
 	 i2c_for_each_dev(driver, __process_new_driver);//检查是否已有对应device,有则扫描该i2c设备是否在线。

        I2c子系统对driver的注册比较简单,整体来说就是将driver注册到名为i2c_bus_type 的总线上,这条总线是在i2c子系统初始化的时候注册的:

struct bus_type i2c_bus_type = {
    .name = "i2c",
    .match = i2c_device_match,
    .probe = i2c_device_probe,
    .remove = i2c_device_remove,
    .shutdown = i2c_device_shutdown,
};
static int __init i2c_init(void) {(drivers/i2c/i2c-core-base.c)
  retval = bus_register(&i2c_bus_type);//注册一条platform总线
...
retval = i2c_add_driver(&dummy_driver);//添加一个dummy driver,没有做任操作,用来匹配adapter注册时生成的device
}
postcore_initcall(i2c_init);
module_exit(i2c_exit);

一旦总线匹配到device和对应driver,probe将被调用:

static int i2c_device_probe(struct device *dev) {(drivers/i2c/i2c-core-base.c)
struct i2c_client *client = i2c_verify_client(dev);//拿到dev绑定的client
  if (driver->probe)
    status = driver->probe(client);//调用driver的probe,并传入client,这里将调用ov5695里probe函数
  else
    status = -EINVAL;
}

        上面分析了driver是如何注册以及如何被调用,client如何传进driver的probe函数,接下来分析i2c子系统是如何生成对应的device,以匹配bus上的driver调用driver的probe函数的。

5.1.2 Client device的生成

        来看此时i2c子系统是如何解析这个层级关系,以及client如何生成。I2c子系统在添加一个adapter的时候,就会轮询该i2c节点下所有的节点,逐一生成client和device的。

int i2c_add_adapter(struct i2c_adapter *adapter)(drivers/i2c/i2c-core-base.c)
  	struct device *dev = &adapter->dev;

//如下主要寻找i2c adapter记录在i2c_adapter_idr的序号,如果用的是设备节点的方式,则进如下if分支
  	if (dev->of_node) {
    		id = of_alias_get_id(dev->of_node, "i2c");
    		if (id >= 0) {
      		adapter->nr = id;
      		return __i2c_add_numbered_adapter(adapter);
    		}
  	}
//如果用不是设备树方式或者第一次创建,将用随机分配id的方式进行
  	id = idr_alloc(&i2c_adapter_idr, adapter, __i2c_first_dynamic_bus_num, 0,GFP_KERNEL);
  	return i2c_register_adapter(adapter);
=>__i2c_add_numbered_adapter(这里分析的是使用设备树方式)
记录adapter到i2c_adapter_idr后,调用i2c_register_adapter
==>i2c_register_adapter
  	dev_set_name(&adap->dev, "i2c-%d", adap->nr);//生成一个i2c-x的device,匹配bus注册是dummy driver
  	adap->dev.bus = &i2c_bus_type;
  	adap->dev.type = &i2c_adapter_type;
  	res = device_register(&adap->dev);//注册到i2c_bus_type总线。
===>of_i2c_register_devices(drivers/i2c/i2c-core-of.c)
bus = of_node_get(adap->dev.of_node);//找到parent节点,即i2c2这个节点
    	for_each_available_child_of_node(bus, node) {//轮询i2c2节点下的所有子节点
        	if (of_node_test_and_set_flag(node, OF_POPULATED))//该子节点是否被其他驱动使用了
            	continue;
        	client = of_i2c_register_device(adap, node);//注册一个client device
    	}
    	of_node_put(bus);
====>of_i2c_register_device(drivers/i2c/i2c-core-of.c)
    ret = of_i2c_get_board_info(&adap->dev, node, &info);//获取节点的数据
    client = i2c_new_client_device(adap, &info);//生成client device
=====>of_i2c_get_board_info
//找出node这个子节点,compatible标签里的值,并去掉该值‘,’前面的值,取后面的值,在这里取到了ovti,ov5695,只取,	//后面的值,最后type的值是ov5695
    	if (of_alias_from_compatible(node, info->type, sizeof(info->type)) < 0) {
        	dev_err(dev, "of_i2c: modalias failure on %pOF\n", node);
        	return -EINVAL;
}
ret = of_property_read_u32(node, "reg", &addr);//读取i2c设备的从机地址

=====>i2c_new_client_device(drivers/i2c/i2c-core-base.c)
  	struct i2c_client *client;
  	client = kzalloc(sizeof *client, GFP_KERNEL);//申请一个i2c_client
//后续有很多client数据填充
client->adapter = adap;
...
i2c_check_addr_validity(client->addr, client->flags);//检查地址有效性,必须是7bit地址
	i2c_check_addr_busy(adap, i2c_encode_flags_to_addr(client));//检查i2c设备是否在线
//如下是重点,配置dev的属性
  	client->dev.parent = &client->adapter->dev;
  	client->dev.bus = &i2c_bus_type;//该dev要挂载的总线,跟之前driver挂在在同一个总线上
  	client->dev.type = &i2c_client_type;
  	//将ov5695节点赋值到dev下,同时和driver的compitable	一致,所以这里将和上面注册driver匹配
client->dev.of_node = of_node_get(info->of_node);
  	client->dev.fwnode = info->fwnode;
i2c_dev_set_name(adap, client, info);//设置device的名称
 	status = device_register(&client->dev);//注册到总线,此时driver的probe函数将被调用。

        如上分析了client device的生成以及如何和driver匹配,使得driver驱动被加载。

5.2 I2c的发送与接收

        I2c的接收发送接口,最终都调用到adapter提供的硬件通信接口

>i2c_master_send(include/linux/i2c.h)
=>i2c_transfer_buffer_flags(drivers/i2c/i2c-core-base.c)
组装msg
struct i2c_msg msg = {
      	.addr = client->addr,
      	.flags = flags | (client->flags & I2C_M_TEN),
      	.len = count,
      	.buf = buf,
  	};
==>i2c_transfer
申请总线锁
===>__i2c_transfer
分原子操作和非原子操作接口,取决于硬件adapter是否实现了原子操作
adap->algo->master_xfer_atomic
adap->algo->master_xfer adapter的硬件接口
--------------------------------------------------------
>i2c_transfer
=>__i2c_transfer
adap->algo->master_xfer adapter的硬件接口

5.3 i2c总线设备生成过程

        总线设备的生成,主要实现在drivers/i2c/i2c-dev.c这个文件里。


static int __init i2c_dev_init(void) {
  ...
  res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");//申请设备号范围
  i2c_dev_class = class_create("i2c-dev");//创建class
 ...
//如果有设备注册到总线i2c_bus_type,i2cdev_notifier会立即被回调(回调下面分析)
  res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
  ...
//这里实际上跟上一句话目的一样,如果此时已经有设备注册,则轮询所有已注册设备,绑定adapter创建总线设备。
  i2c_for_each_dev(NULL, i2c_dev_attach_adapter);
  ...
}
module_init(i2c_dev_init);

        回调过程:

i2c_dev_attach_adapter / i2cdev_notifier_call->i2cdev_attach_adapter
=>i2cdev_attach_adapter
i2c_dev = get_free_i2c_dev(adap);//获取未注册过的i2c dev
cdev_init(&i2c_dev->cdev, &i2cdev_fops);//初始化操作集相关
res = dev_set_name(&i2c_dev->dev, "i2c-%d", adap->nr);// /dev下的设备名称,nr是适配器序号,即设备树i2cx
 	res = cdev_device_add(&i2c_dev->cdev, &i2c_dev->dev);//生成该设备
其中操作集的定义如下:
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,
};
操作集都将调用i2c子系统的发送接收接口,最终调用adapter的硬件接口
---------------
i2cdev_read
=>i2c_master_recv
-------------
i2cdev_write
=>i2c_master_send
-------------
i2cdev_ioctl
=>i2cdev_ioctl_rdwr
==>i2c_transfer

6 i2c作为slave的使用

        I2c子系统对salve的实现是在drivers/i2c/i2c-core-slave.c,提供了三个api。

        注册一个slave

int i2c_slave_register(struct i2c_client *client, i2c_slave_cb_t slave_cb)

        释放

int i2c_slave_unregister(struct i2c_client *client)

        有数据请求时,将被调用,由控制器驱动实现,最终调用用户在i2c_slave_register注册的slave_cb回调。

int i2c_slave_event(struct i2c_client *client,

            enum i2c_slave_event event, u8 *val)

检查当前设备树配置的设备是否是从机设备

bool i2c_detect_slave_mode(struct device *dev)

http://www.kler.cn/a/407802.html

相关文章:

  • 微信小程序包之加农炮游戏
  • 聚水潭与MySQL数据集成案例分享
  • LeetCode题解:28.找出字符串中第一个匹配项的下标【Python题解超详细,滑动窗口法、内置 find 函数、KMP算法】,知识拓展, KMP算法
  • UVM 验证方法学之interface学习系列文章(七)高级 《bind 操作》(4)级联
  • 字符串的常用函数
  • Rust中Tracing 应用指南
  • 如何利用java爬虫获得淘宝商品评论
  • 网络安全(骇客)—技术学习
  • 【案例分享】图表工具TeeChart在环境研究领域的数据可视化应用
  • vue前端下载某一区域为照片格式
  • leetcode - 1861. Rotating the Box
  • 后端接受大写参数(亲测能用)
  • Elasticsearch面试内容整理-安全与权限管理
  • 安卓InputDispatching Timeout ANR 流程
  • RocketMQ: 客户端使用指南
  • Canvas 前端艺术家
  • Ubuntu20.04 rk3588交叉编译opencv4.10
  • MySQL面试题补
  • DAY1 网络编程(TCP客户端服务器)
  • Spring Boot入门——Spring Boot项目的创建
  • SpringBoot 集成 html2Pdf
  • Qt不同类之间参数的传递
  • ubuntu 配置 多个 git 客户端 账户
  • Modern Effective C++:Item 6 auto推导若非己愿,使用显式类型初始化惯用法
  • PostgreSQL技术内幕19:逻辑备份工具pg_dump、pg_dumpall
  • 【AI系统】GPU 架构回顾(从2010年-2017年)