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

编写一个通用的i2c控制器驱动框架

往期内容

I2C子系统专栏:

  1. I2C(IIC)协议讲解-CSDN博客
  2. SMBus 协议详解-CSDN博客
  3. I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客
  4. 内核提供的通用I2C设备驱动I2c-dev.c分析:注册篇
  5. 内核提供的通用I2C设备驱动I2C-dev.c分析:file_ops篇

总线和设备树专栏:

  1. 总线和设备树_憧憬一下的博客-CSDN博客
  2. 设备树与 Linux 内核设备驱动模型的整合-CSDN博客

前言

本章需要用到的内核源码:

Linux内核真正的I2C控制器驱动程序

  • IMX6ULL: Linux-4.9.88\drivers\i2c\busses\i2c-imx.c📎i2c-imx.c

1.分辨

1.1 I2C总线和平台总线区别

i2c总线-设备-驱动平台的注册,i2c总线是真实存在的,而bus总线-设备-驱动平台中的bus则是虚拟的

i2c总线-设备-驱动平台管理着多条总线上的i2c_Controller And Devices

i2c控制器也有自己的驱动(I2c_Adapter_drv)

  • 可以自己通过注册万能的bus总线平台,让驱动程序platform_driver来和platform_device设备节点(有控制器的name、属性设置等)匹配,来代表着一个总线,创建i2c_adapter。创建平台只是为了让驱动和设备节点相匹配,使该总线上i2c控制器功能的实现
  • 一个i2c_adapter就代表着一个总线,代表着一个i2c controller
  • 核心函数是master_xfer函数,这个函数是将数据传给总线上的i2c设备

i2c_client上则记录着i2c设备自己被哪一个i2c Controller(i2c_adapter)管理着,也就是在哪一条总线上,以及设备地址,一般是有设备树节点转化而来

i2c_driver上则是为i2c设备注册驱动程序、生成设备节点、设备类等,为app提供打开设备的设备节点、提供对设备的操作函数,比如读取或写入。注意的是要先用i2c_add_driver函数将驱动添加进i2c总线平台中,并且读取和写入是要先经过i2c_controller的

  • 如i2c_transfer函数,会将device_driver的数据(来自app)转发i2c_client中记录的i2c_adapter,也就是i2c控制器,之后i2c控制器对应的驱动程序又会将数据转发给在总线上的I2C设备(i2c_client源)中的存储芯片等
  • i2c_driver和i2c_client之间的匹配则是有i2c总线平台来进行匹配的,匹配成功后才能实现数据传给i2c_controller,进而传给i2c device
  • 当然,也可以自己注册bus总线平台,使用GPIO模拟I2C来让platform_device和platform_driver进行匹配,也就是i2c设备信息client和驱动程序进行匹配,只需要将GPIO引脚模拟I2C引脚输出I2C协议就行。这一种在后面也会出文章讲解。

img

1.2 I2C驱动程序的层次

img

img

2. I2C_Adapter驱动框架

2.1 核心的结构体

i2c_adapter

img

I2C相关结构体讲解:i2c_adapter、i2c_algorithm、i2c_msg-CSDN博客该结构体在之前的文章也有介绍过其相关成员。

i2c_algorithm

\Linux-4.9.88\include\uapi\linux\i2c.h
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

  • smbus_xfer:实现SMBus传输,如果不提供这个函数,SMBus传输会使用master_xfer来模拟

  • functionality: 数通常会返回一个由功能标志位组成的位掩码,表示适配器支持哪些 I2C 或 SMBus 操作(如 I2C_FUNC_I2C, I2C_FUNC_SMBUS_READ_BLOCK_DATA 等等)。

  • reg_slave:

    • 注册一个 I2C 从设备,使得该设备可以响应 I2C 主设备发起的请求。有些I2C Adapter也可工作与Slave模式,用来实现或模拟一个I2C设备
  • unreg_slave:

    • 注销一个 I2C 从设备,使其不再响应 I2C 主设备的请求。

2.2 I2C adapter的相关的API

I2C adapter定义好之后,要把它注册到kernel中去,相关的API如下:

\Linux-4.9.88\Linux-4.9.88\include\linux\i2c.h:
#if defined(CONFIG_I2C) || defined(CONFIG_I2C_MODULE)
extern int i2c_add_adapter(struct i2c_adapter *);
extern void i2c_del_adapter(struct i2c_adapter *);
extern int i2c_add_numbered_adapter(struct i2c_adapter *);
extern struct i2c_adapter *i2c_get_adapter(int nr);
extern void i2c_put_adapter(struct i2c_adapter *adap);
extern unsigned int i2c_adapter_depth(struct i2c_adapter *adapter);
/* must call put_device() when done with returned i2c_adapter device */
extern struct i2c_adapter *of_find_i2c_adapter_by_node(struct device_node *node);

/* Return the functionality mask */
static inline u32 i2c_get_functionality(struct i2c_adapter *adap)
{
	return adap->algo->functionality(adap);
}
/* Return 1 if adapter supports everything we need, 0 if not. */
static inline int i2c_check_functionality(struct i2c_adapter *adap, u32 func)
{
	return (func & i2c_get_functionality(adap)) == func;
}
/* Return the adapter number for a specific adapter */
static inline int i2c_adapter_id(struct i2c_adapter *adap)
{
	return adap->nr;
}

(1)i2c_add_adapter和i2c_add_numbered_adapter是I2C adapter的注册接口,它们的区别是:

  • 前者将一个 i2c_adapter 注册到 I2C 核心框架中,并自动为它分配一个 nr(adapter ID)。内核会为你选择一个合适的 ID,而不需要你手动指定。适用于不需要指定特定 ID 的情况,适合大多数情况下的控制器注册。 ID 自动分配。
  • 后者允许你指定 i2c_adapter 的 nr 字段,也就是适配器的 ID。你可以手动设置 adap->nr,然后注册时内核将使用你指定的这个 ID。如果 nr 不可用或者冲突,则返回错误。适用于需要在系统中保留固定编号的 I2C 适配器,比如为了与用户空间或其他设备进行明确绑定。 ID 可以手动指定。

(2)i2c_del_adapter(struct i2c_adapter *adap):

  • 用于从 I2C 核心中删除适配器。它会取消所有注册到该适配器的从设备,释放与之相关的资源。在驱动卸载或适配器不再需要时使用,确保适配器从系统中安全移除。

(3)i2c_get_functionality(struct i2c_adapter *adap) 和 i2c_check_functionality:

  • 前者 获取指定适配器所支持的功能。返回值是一个 32 位的位掩码,用于表示该适配器支持的特性。这些特性可以包括:标准的 I2C 功能(如 7 位地址、10 位地址等)、SMBus 功能、高级协议特性(如 I2C_FUNC_I2CI2C_FUNC_SMBUS_*)。 用于查询控制器支持的功能。
  • 后者 检查指定控制器是否支持某个特定的功能。传入的 func 是功能的位掩码。该函数会将传入的 func 与适配器实际支持的功能进行比较,如果适配器支持所有的请求功能,则返回 1,否则返回 0。 用于检查控制器是否支持某一特定功能。

(4)i2c_adapter_id(struct i2c_adapter *adap):

  • 返回给定的 i2c_adapter 的 nr 字段,也就是适配器的 ID。这个函数很简单,通常在需要识别适配器或者调试时使用

(5)i2c_get_adapter(int nr) 和 i2c_put_adapter(struct i2c_adapter *adap):

  • 前者: 根据指定的 nr(适配器的 ID)查找并返回对应的 i2c_adapter 结构指针。这个函数会增加适配器所依赖模块的引用计数(内部通过调用 try_module_get)。因此在调用这个函数后,需要确保适配器不会被卸载。 增加引用计数,获取适配器。
  • 后者:当使用完 i2c_adapter 后,必须调用这个函数以减少模块的引用计数。这样可以防止模块被错误卸载,同时释放相关资源。 增加引用计数,获取适配器。

(6)of_find_i2c_adapter_by_node(struct device_node *node):

  • 通过设备树节点(device_node)查找与之关联的 I2C 适配器。该函数通常用于通过设备树动态探测并找到某个 I2C 适配器。在调用该函数后,你还需要通过 put_device 来正确处理设备的引用计数,以确保资源释放。

2.3 驱动程序框架

分配、设置、注册一个i2c_adpater结构体:

  • i2c_adpater的核心是i2c_algorithm
  • i2c_algorithm的核心是master_xfer函数

所涉及的函数

  • 分配struct i2c_adpater *adap = kzalloc(sizeof(struct i2c_adpater), GFP_KERNEL);
  • 设置
adap->owner = THIS_MODULE;
adap->algo = &stm32f7_i2c_algo;
  • 注册:i2c_add_adapter/i2c_add_numbered_adapter
ret = i2c_add_adapter(adap);          // 不管adap->nr原来是什么,都动态设置adap->nr
ret = i2c_add_numbered_adapter(adap); // 如果adap->nr == -1 则动态分配nr; 否则使用该nr
  • 反注册
i2c_del_adapter(adap);

i2c_algorithm示例

img

大致步骤

了解了I2C adapter有关的数据结构和API之后,编写I2C控制器驱动就简单多了,主要步骤如下:

1)定义一个struct i2c_algorithm变量,并根据I2C controller的特性,实现其中的回调函数。

2)在DTS文件(一般都放到DTSI)中,定义I2C controller相关的DTS node

3)在drives/i2c/busses目录下,以i2c-xxx.c的命名方式,编写I2C controller的platform driver,并提供match id、probe、remove等接口。

4)在platform driver的probe接口中,分配一个adapter结构,并进行必要的初始化操作后,调用i2c_add_adapter或者i2c_add_numbered_adapter接口,将其注册到kernel中即可。

2.4 编写框架

这里使用的万能的总线平台:bus+dts 实现,platfrom_driver and platform_device

📎05_i2c_adapter_framework.zip – 里面有设备树

#include <linux/completion.h>
#include <linux/debugfs.h>
#include <linux/delay.h>
#include <linux/gpio/consumer.h>
#include <linux/i2c-algo-bit.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_data/i2c-gpio.h>
#include <linux/platform_device.h>
#include <linux/slab.h>

/* Global pointer to the virtual I2C adapter */
static struct i2c_adapter *g_adapter;

/* 
 * i2c_bus_virtual_master_xfer - I2C message transfer function
 * @i2c_adap: Pointer to I2C adapter
 * @msgs: Array of I2C messages
 * @num: Number of messages
 * 
 * This function simulates the transfer of multiple I2C messages. 
 * Here you would normally interact with the hardware.
 */
static int i2c_bus_virtual_master_xfer(struct i2c_adapter *i2c_adap,
		    struct i2c_msg msgs[], int num)
{
	int i;

	/* Loop through each message and simulate transfer */
	for (i = 0; i < num; i++) {
		/* Normally, the actual hardware transfer would occur here */
		// simulate transfer of msgs[i];
	}
	
	/* Return the number of messages processed */
	return num;
}

/* 
 * i2c_bus_virtual_func - Supported functionality of the virtual I2C bus
 * @adap: Pointer to I2C adapter
 * 
 * This function reports the capabilities of the virtual I2C bus. 
 * It supports standard I2C transactions and SMBus emulation.
 */
static u32 i2c_bus_virtual_func(struct i2c_adapter *adap)
{
	/* Return a combination of supported functionalities */
	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;
}

/* 
 * i2c_bus_virtual_algo - Virtual I2C bus algorithm structure
 * 
 * This structure defines the transfer and functionality operations for
 * the virtual I2C bus. It is used to handle I2C operations for the
 * associated adapter.
 */
const struct i2c_algorithm i2c_bus_virtual_algo = {
	.master_xfer   = i2c_bus_virtual_master_xfer,
	.functionality = i2c_bus_virtual_func,
};

/* 
 * i2c_bus_virtual_probe - Platform driver probe function
 * @pdev: Platform device structure
 * 
 * This function is called when the driver is matched with a device.
 * It sets up and registers the I2C adapter.
 */
static int i2c_bus_virtual_probe(struct platform_device *pdev)
{
	/* Allocate memory for the I2C adapter structure */
	g_adapter = kzalloc(sizeof(*g_adapter), GFP_KERNEL);
	if (!g_adapter)
		return -ENOMEM;

	/* Initialize the I2C adapter structure */
	g_adapter->owner = THIS_MODULE;
	g_adapter->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;  // Adapter supports hardware monitor and SPD classes
	g_adapter->nr = -1;  // Dynamically assign adapter number
	snprintf(g_adapter->name, sizeof(g_adapter->name), "i2c-bus-virtual");  // Set adapter name

	/* Assign the algorithm to the adapter */
	g_adapter->algo = &i2c_bus_virtual_algo;

	/* Register the I2C adapter with the I2C core */
	i2c_add_adapter(g_adapter);  // Can also use i2c_add_numbered_adapter() for fixed numbers
	
	return 0;
}

/* 
 * i2c_bus_virtual_remove - Platform driver remove function
 * @pdev: Platform device structure
 * 
 * This function is called when the driver is removed.
 * It unregisters the I2C adapter and cleans up.
 */
static int i2c_bus_virtual_remove(struct platform_device *pdev)
{
	/* Unregister the I2C adapter */
	i2c_del_adapter(g_adapter);
	
	/* Free the allocated memory */
	kfree(g_adapter);

	return 0;
}

/* 
 * i2c_bus_virtual_dt_ids - Device tree compatible entries
 * 
 * This array defines the compatible strings that match the virtual I2C bus
 * driver to the corresponding device in the device tree.
 */
static const struct of_device_id i2c_bus_virtual_dt_ids[] = {
	{ .compatible = "yyy,i2c-bus-virtual", },
	{ /* sentinel */ }
};

/* 
 * i2c_bus_virtual_driver - Platform driver structure
 * 
 * This structure defines the probe, remove, and other operations for the
 * virtual I2C bus driver.
 */
static struct platform_driver i2c_bus_virtual_driver = {
	.driver		= {
		.name	= "i2c-gpio",  // Name of the driver
		.of_match_table	= of_match_ptr(i2c_bus_virtual_dt_ids),  // Match device tree compatible entries
	},
	.probe		= i2c_bus_virtual_probe,  // Probe function
	.remove		= i2c_bus_virtual_remove,  // Remove function
};

/* 
 * i2c_bus_virtual_init - Driver initialization function
 * 
 * This function is called when the module is loaded. It registers
 * the platform driver with the Linux kernel.
 */
static int __init i2c_bus_virtual_init(void)
{
	int ret;

	/* Register the platform driver */
	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);

/* 
 * i2c_bus_virtual_exit - Driver exit function
 * 
 * This function is called when the module is unloaded. It unregisters
 * the platform driver from the kernel.
 */
static void __exit i2c_bus_virtual_exit(void)
{
	/* Unregister the platform driver */
	platform_driver_unregister(&i2c_bus_virtual_driver);
}
module_exit(i2c_bus_virtual_exit);

MODULE_AUTHOR("www.100ask.net");
MODULE_LICENSE("GPL");

设备树

在设备树里构造控制器的设备节点:

i2c-bus-virtual {
        compatible = "yyy,i2c-bus-virtual";

        //剩下的可以是挂载在该控制器上的i2c设备的设备树节点
  };

platform_driver

分配、设置、注册platform_driver结构体。

核心是probe函数,

它要做这几件事:

  • 根据设备树信息设置硬件(引脚、时钟等)
  • 分配、设置、注册i2c_apdater

这里注册platform只是为了将i2c_adapter_driver和控制器的设备节点能够匹配起来,实现创建i2c_adapter而已

master_xfer

i2c_apdater核心是master_xfer函数,它的实现取决于硬件,在内核提供控制器驱动文件i2c-m中:

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);  // 获取适配器的私有数据结构

    // 输出调试信息,显示函数名
    dev_dbg(&i2c_imx->adapter.dev, "<%s>\n", __func__);

    // 检查运行时电源管理是否已启用,如果没有则启用它
    if (!pm_runtime_enabled(i2c_imx->adapter.dev.parent)) {
        pm_runtime_enable(i2c_imx->adapter.dev.parent);
        enable_runtime_pm = true;  // 标记为启用了电源管理
    }

    // 增加运行时电源的引用计数,确保设备处于活动状态
    result = pm_runtime_get_sync(i2c_imx->adapter.dev.parent);
    if (result < 0)
        goto out;  // 如果获取电源失败,则跳转到 out 标签处理

    // 开始I2C传输,调用底层驱动启动传输过程
    /* 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);  // 尝试重新启动传输
        }
    }

    if (result)  // 如果传输启动仍然失败,跳转到失败处理
        goto fail0;

    // 开始处理消息队列
    /* read/write data */
    for (i = 0; i < num; i++) {
        if (i == num - 1)
            is_lastmsg = true;  // 如果是最后一条消息,标记为true

        if (i) {
            // 如果不是第一条消息,则发送重复开始信号 (Repeated Start)
            dev_dbg(&i2c_imx->adapter.dev, "<%s> repeated start\n", __func__);
            temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);  // 读取I2C控制寄存器
            temp |= I2CR_RSTA;  // 设置重复开始位
            imx_i2c_write_reg(temp, i2c_imx, IMX_I2C_I2CR);  // 写回控制寄存器
            result = i2c_imx_bus_busy(i2c_imx, 1);  // 等待总线忙完成
            if (result)
                goto fail0;  // 如果总线忙碌超时,跳转到失败处理
        }
        
        // 输出调试信息,显示当前正在传输的消息编号
        dev_dbg(&i2c_imx->adapter.dev, "<%s> transfer message: %d\n", __func__, i);

#ifdef CONFIG_I2C_DEBUG_BUS
        // 如果启用了I2C调试,输出控制寄存器和状态寄存器的调试信息
        temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2CR);
        dev_dbg(&i2c_imx->adapter.dev,
            "<%s> CONTROL: IEN=%d, IIEN=%d, MSTA=%d, MTX=%d, TXAK=%d, RSTA=%d\n",
            __func__,
            (temp & I2CR_IEN ? 1 : 0), (temp & I2CR_IIEN ? 1 : 0),
            (temp & I2CR_MSTA ? 1 : 0), (temp & I2CR_MTX ? 1 : 0),
            (temp & I2CR_TXAK ? 1 : 0), (temp & I2CR_RSTA ? 1 : 0));
        temp = imx_i2c_read_reg(i2c_imx, IMX_I2C_I2SR);
        dev_dbg(&i2c_imx->adapter.dev,
            "<%s> STATUS: ICF=%d, IAAS=%d, IBB=%d, IAL=%d, SRW=%d, IIF=%d, RXAK=%d\n",
            __func__,
            (temp & I2SR_ICF ? 1 : 0), (temp & I2SR_IAAS ? 1 : 0),
            (temp & I2SR_IBB ? 1 : 0), (temp & I2SR_IAL ? 1 : 0),
            (temp & I2SR_SRW ? 1 : 0), (temp & I2SR_IIF ? 1 : 0),
            (temp & I2SR_RXAK ? 1 : 0));
#endif

        // 根据消息类型执行读或写操作
        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)  // 如果支持DMA且消息长度大于阈值
                result = i2c_imx_dma_write(i2c_imx, &msgs[i]);  // 使用DMA执行写操作
            else
                result = i2c_imx_write(i2c_imx, &msgs[i]);  // 否则使用常规方式写
        }

        if (result)  // 如果读写操作失败,跳转到失败处理
            goto fail0;
    }

fail0:
    // 停止I2C传输
    /* Stop I2C transfer */
    i2c_imx_stop(i2c_imx);

    // 更新运行时电源状态并减少引用计数
    pm_runtime_mark_last_busy(i2c_imx->adapter.dev.parent);
    pm_runtime_put_autosuspend(i2c_imx->adapter.dev.parent);

out:
    // 如果启用了运行时电源管理,则在结束时禁用它
    if (enable_runtime_pm)
        pm_runtime_disable(i2c_imx->adapter.dev.parent);

    // 输出调试信息,显示函数退出时的状态
    dev_dbg(&i2c_imx->adapter.dev, "<%s> exit with: %s: %d\n", __func__,
            (result < 0) ? "error" : "success msg",
            (result < 0) ? result : num);
    
    // 返回最终的结果,成功则返回消息数量,失败则返回错误码
    return (result < 0) ? result : num;
}

CPU 调用 i2c_transfe(i2c device driver) → 内部调用了通过 i2c_imx_xfer(adap->algo->master_xfer) 与 I2C 控制器通信 → 如果是读操作,则调用i2c_adapter中的 i2c_imx_readi2c_imx_read 从 I2C 控制器接收数据并填充到 msgsbuf 中 → CPU 读取 buf 中的数据

在上面讲的i2c_imx_xfer,是不是有开始信号、停止信号、data。这些都是iic传输协议的格式中有提到的,至于addr、w/r,这些就封装在了i2c_msg中,格式:I2C(IIC)协议讲解-CSDN博客


框架例子:

static int xxx_master_xfer(struct i2c_adapter *adapter,
                           struct i2c_msg *msgs, int num)
{
    for (i = 0; i < num; i++) {
        struct i2c_msg *msg = msgs[i];
        {
            // 1. 发出S信号: 设置寄存器发出S信号
            CTLREG = S;

            // 2. 根据Flag发出设备地址和R/W位: 把这8位数据写入某个DATAREG即可发出信号
            //    判断是否有ACK

            if (!ACK)
                return ERROR;
            else {
                // 3. read / write
                if (read) {
                    STATUS = XXX; // 这决定读到一个数据后是否发出ACK给对方
                    val = DATAREG; // 这会发起I2C读操作
                } else if(write) {
                    DATAREG = val; // 这会发起I2C写操作
                    val = STATUS;  // 判断是否收到ACK
                    if (!ACK)
                        return ERROR;
                }                
            }
            // 4. 发出P信号
            CTLREG = P;
        }
    }

    return i;
}

http://www.kler.cn/news/357042.html

相关文章:

  • Xcode使用Instruments的dsym还原符号堆栈问题
  • 智慧农业案例 (三)- 蔬菜智能温室
  • 高级Sql 技巧
  • Qt优秀开源项目之二十四:EXCEL读写利器QXlsx
  • 电脑端百度网页两个好用的功能
  • 百亿数据量下的多表查询优化策略
  • Android上的AES加密
  • 数据结构 - 树,再探
  • Python 遍历(Python Traversal)
  • STM32应用开发——BH1750光照传感器详解
  • Lucas带你手撕机器学习——线性回归
  • 记录Visio导出图片的文字与latex中文字大小一致的问题,和visio导出适用于论文的高清图片问题
  • Java项目-基于Springboot的应急救援物资管理系统项目(源码+说明).zip
  • 虾​皮​一​面​-​2
  • 数学归纳法——第一数学归纳法、第二数学归纳法步骤和示例
  • SpringBoot中的RedisTemplate对象中的setIfAbsent()方法有什么作用?
  • Mapbox GL 加载GeoServer底图服务器的WMS source
  • 开源的存储引擎--cantian
  • js 字符串与数组的操作
  • python【装饰器】