Linux驱动开发--IIC子系统

news2025/4/2 6:00:16

1.1 简介

I2C 是很常见的一种总线协议, I2C 是 NXP 公司设计的, I2C 使用两条线在主控制器和从机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据线需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。 I2C 总线标准模式下速度可以达到 100Kb/S,快速模式下可以达到 400Kb/S。 I2C 总线工作是按照一定的协议来运行的,接下来就看一下 I2C 协议。

I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C设备了,一个 I2C 总线连接多个 I2C 设备如图 所示:

1、起始位

顾名思义,也就是 I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始进行 I2C 通信了。在 SCL 为高电平的时候, SDA 出现下降沿就表示为起始位

2、停止位

停止位就是停止 I2C 通信的标志位,和起始位的功能相反。在 SCL 位高电平的时候, SDA出现上升沿就表示为停止位

3、数据传输

I2C 总线在数据传输的时候要保证在 SCL 高电平期间, SDA 上的数据稳定,因此 SDA 上的数据变化只能在 SCL 低电平期间发生

4、响应ACK(Acknowledge)和非响应NACK(Not Acknowledge)

当 I2C 主机发送完 8 位数据以后会将 SDA 设置为输入状态,等待 I2C 从机应答,也就是等到 I2C 从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完 8 位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将 SDA 拉低来表示发出应答信号,表示通信成功,否则表示通信失败。

5、 I2C 写时序

I2C 总线单字节写时序如图 26.1.1.5 所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

写时序的具体步骤:

1)、开始信号。

2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址7bit,通过发送具体的设备地址来决定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。

3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。

4)、从机发送的 ACK 应答信号。

5)、重新发送开始信号。

6)、发送要写写入数据的寄存器地址。

7)、从机发送的 ACK 应答信号。

8)、发送要写入寄存器的数据。

9)、从机发送的 ACK 应答信号。

10)、主机发送ACK停止信号。

6、 I2C 读时序

I2C 总线单字节读时序如图 26.1.1.6 所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要读取的寄存器值,我们具体来看一下这几步。

1)、主机发送起始信号。

2)、主机发送要读取的 I2C 从设备地址。

3)、读写控制位,因为是向 I2C 从设备发送数据,因此是写信号。

4)、从机发送的 ACK 应答信号。

5)、重新发送 START 信号

6)、主机发送要读取的寄存器地址。

7)、从机发送的 ACK 应答信号。

8)、重新发送 START 信号。

9)、重新发送要读取的 I2C 从设备地址。

10)、读写控制位,这里是读信号,表示接下来是从 I2C 从设备里面读取数据。

11)、从机发送的 ACK 应答信号。

12)、从 I2C 器件里面读取到的数据。

13)、主机发出 NO ACK 信号,表示读取完成,不需要从机再发送 ACK 信号了。

14)、主机发出 STOP 信号,停止 I2C 通信。

7、 I2C 多字节读写时序

有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。

1.2 驱动框架

1.2.1 I²C 体系结构

Linux 的 I²C 体系结构分为 3 个组成部分。

(1)I²C 核心
I²C 核心提供了 I²C 总线驱动和设备驱动的注册、注销方法,I²C 通信方法(即 Algorithm)上层的与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等,如图 15.1 所示。

(2)I²C 总线驱动
I²C 总线驱动是对 I²C 硬件体系结构中适配器端的实现适配器可由 CPU 控制,甚至可以直接集成在 CPU 内部。I²C 总线驱动主要包含 I²C 适配器数据结构 i2c_adapter、I²C 适配器的 Algorithm 数据结构 i2c_algorithm 和控制 I²C 适配器产生通信信号的函数。经由 I²C 总线驱动的代码,我们可以控制 I²C 适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生 ACK 等。

(3)I²C 设备驱动
I²C 设备驱动(也称为客户端驱动)是对 I²C 硬件体系结构中设备端的实现设备一般挂接在受 CPU 控制的 I²C 适配器上,通过 I²C 适配器与 CPU 交换数据。 I²C 设备驱动主要包含数据结构 i2c_driveri2c_client,我们需要根据具体设备实现其中的成员函数。
在 Linux 2.6 内核中,所有的 I²C 设备都在 sysfs 文件系统中显示,存于 /sys/bus/i2c/ 目录下,以适配器地址和芯片地址的形式列出,例如:

1.2.2 重要的数据结构

在 Linux 内核源代码中的 drivers 目录下有一个 i2c 目录,而在 i2c 目录下又包含如下文件和文件夹。

(1)i2c-core.c
这个文件实现了 I²C 核心的功能以及 /proc/bus/i2c* 接口。

(2)i2c-dev.c
实现了 I²C 适配器设备文件的功能,每一个 I²C 适配器都被分配一个设备。通过适配器访问设备时的主设备号都为 89,次设备号为 0 ~ 255。应用程序通过 “i2c-%d”(i2c-0, i2c-1,…, i2c-10,…)文件名并使用文件操作接口 open()、write()、read()、ioctl() 和 close() 等来访问这个设备。

i2c-dev.c 并不是针对特定的设备而设计的,只是提供了通用的 read()、write() 和 ioctl() 等接口,应用层可以借用这些接口访问挂接在适配器上的 I²C 设备的存储空间或寄存器,并控制 I²C 设备的工作方式。

i2c-dev是一种通用的i2c设备驱动,我们也可以自己写专门的设备驱动;

(3)busses 文件夹
这个文件包含了一些 I²C 主机控制器的驱动,如 i2c-tegra.c、i2c-omap.c、i2c-versatile.c、i2c-s3c2410.c 等。

(4)algos 文件夹
实现了一些 I²C 总线适配器的通信方法。


此外,内核中的 i2c.h 头文件对 i2c_adapter、i2c_algorithm、i2c_driver 和 i2c_client 这 4 个数据结构进行了定义。理解这 4 个结构体的作用十分重要,它们的定义位于 include/linux/i2c.h 文件中.

1. I2C 总线–i2c适配器,其实也是一种设备dev,基类仍然是struct device dev

首先来看一下 I2C 总线,在讲 platform 的时候就说过, platform 是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于 I2C 而言,不需要虚拟出一条总线,直接使用 I2C总线即可。 I2C 总线驱动重点是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动,这里要用到两个重要的数据结构: i2c_adapter 和 i2c_algorithm, Linux 内核将 SOC 的 I2C 适配器(控制器)抽象成 i2c_adapter, i2c_adapter 结构体定义在 include/linux/i2c.h 文件中,结构体内容如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

综上所述, I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapteri2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapter,这两个函数的原型如下:

int i2c_add_adapter(struct i2c_adapter *adapter)
adapter 或 adap:要添加到 Linux 内核中的 i2c_adapter,也就是 I2C 适配器。
返回值: 0,成功;负值,失败。
    
int i2c_add_numbered_adapter(struct i2c_adapter *adap)

这两个函数的区别在于 i2c_add_adapter 使用动态的总线号,而 i2c_add_numbered_adapter使用静态总线号。如果要删除 I2C 适配器的话使用 i2c_del_adapter 函数即可,函数原型如下:

void i2c_del_adapter(struct i2c_adapter * adap)

2.设备驱动

I2C 设备驱动重点关注两个数据结构: i2c_client 和 i2c_driver,根据总线、设备和驱动模型, I2C 总线上一小节已经讲了。还剩下设备和驱动, i2c_client 就是描述设备信息的, i2c_driver 描 述驱动内容,类似于 platform_driver。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

对于我们 I2C 设备驱动编写人来说,重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。 i2c_driver 注册函数为 int i2c_register_driver,此函数原型如下:

int i2c_register_driver(struct module *owner,struct i2c_driver *driver)
owner: 一般为 THIS_MODULE。
driver:要注册的 i2c_driver。
返回值: 0,成功;负值,失败。

#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)

void i2c_del_driver(struct i2c_driver *driver)
driver:要注销的 i2c_driver。
返回值: 无

i2c_adapter、i2c_algorithm、i2c_driver 和 i2c_client 这 4 个数据结构的作用及其盘根错节的关系:

(1)i2c_adapter 与 i2c_algorithm
i2c_adapter 对应于物理上的一个适配器,而 i2c_algorithm 对应一套通信方法。一个 I²C 适配器需要 i2c_algorithm 提供的通信函数来控制适配器产生特定的访问周期。缺少 i2c_algorithm 的 i2c_adapter 什么也做不了,因此 i2c_adapter 中包含所使用的 i2c_algorithm 的指针。

​ i2c_algorithm 中的关键函数 master_xfer() 用于产生 I²C 访问周期需要的信号,以 i2c_msg(即 I²C 消息)为单位。i2c_msg 结构体也是非常重要的,它定义于 include/uapi/linux/i2c.h(在 uapi 目录下,证明用户空间的应用也可能使用这个结构体)中,下面给出了它的定义,其中的成员表明了 I²C 的传输地址、方向、缓冲区、缓冲区长度等信息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(2)i2c_driver 与 i2c_client
i2c_driver 对应于一套驱动方法,其主要成员函数是 probe()、remove()、suspend()、resume() 等,另外,struct i2c_device_id 形式的 id_table 是该驱动所支持的 I²C 设备的 ID 表。i2c_client 对应于真实的物理设备,每个 I²C 设备都需要一个 i2c_client 来描述。i2c_driver 与 i2c_client 的关系是一对多,一个 i2c_driver 可以支持多个同类型的 i2c_client。
i2c_client 的信息通常在 BSP 的板文件中通过 i2c_board_info 填充(或者设备树给出),如下面的代码就定义了一个 I²C 设备的 ID 为 “ad7142_joystick”,地址为 0x2C、中断号为 IRQ_PFS 的 i2c_client:

static struct i2c_board_info __initdata xxx_i2c_board_info[] = {
    #if defined(CONFIG_JOYSTICK_AD7142) || defined(CONFIG_JOYSTICK_AD7142_MODULE)
    {
        I2C_BOARD_INFO("ad7142_joystick", 0x2C),
        .irq = IRQ_PFS,
    },
    …
};

​ 在 I²C 总线驱动 i2c_bus_typematch() 函数 i2c_device_match() 中,会调用 i2c_match_id() 函数匹配在板文件中定义的 ID 和 i2c_driver 所支持的 ID 表。

(3)i2c_adapter 与 i2c_client
i2c_adapter 与 i2c_client 的关系与 I²C 硬件体系中适配器和设备的关系一致,即 i2c_client 依附于 i2c_adapter。由于一个适配器可以连接多个 I²C 设备,所以一个 i2c_adapter 也可以被多个 i2c_client 依附,i2c_adapter 中包括依附于它的 i2c_client 的链表。
假设 I²C 总线适配器 xxx 上有两个使用相同驱动程序的 yyy I²C 设备,在打开该 I²C 总线的设备节点后,相关数据结构之间的逻辑组织关系将如图 15.2 所示。

从上面的分析可知,虽然下IIC硬件体系结构比较简单,但是IIC体系结构在Linux中的实现却相当复杂。当工程师拿到实际的电路板时,面对复杂的Linux IIC子系统,应该如何下手写驱动呢?究竟有哪些是需要亲自做的,哪些是内核已经提供的呢?理清这个问题非常有意义,可以使我们在面对具体问题时迅速抓住重点。

**一方面,适配器驱动可能是 Linux 内核本身还不包含的;另一方面,挂接在适配器上的具体设备驱动可能也是 Linux 内核还不包含的。**因此,工程师要实现的主要工作如下。
提供 I²C 适配器的硬件驱动,探测、初始化 I²C 适配器(如申请 I²C 的 I/O 地址和中断号)、驱动 CPU 控制的 I²C 适配器;从硬件上产生各种信号以及处理 I²C 中断等。
提供 I²C 适配器的 Algorithm,用具体适配器的 xxx_xfer() 函数填充 i2c_algorithm 的 master_xfer 指针,并把 i2c_algorithm 指针赋值给 i2c_adapter 的 algo 指针。

​ ● 实现 I²C 设备驱动中的 i2c_driver 接口,用具体设备 yyy 的 yyy_probe()、yyy_remove()、yyy_suspend()、yyy_resume() 函数指针和 i2c_device_id 设备 ID 表赋值给 i2c_driver 的 probe、remove、suspend、resume 和 id_table 指针。
​ ● 实现 I²C 设备所对应类型的具体驱动,i2c_driver 只是实现设备与总线的挂接,而挂接在总线上的设备则千差万别。例如,如果是字符设备,就实现文件操作接口,即实现具体设备 yyy 的 yyy_read()、yyy_write() 和 yyy_ioctl() 函数等;如果是声卡,就实现 ALSA 驱动。

上述工作中前两个属于 I²C 总线驱动,后两个属于 I²C 设备驱动。

1.3 Linux IIC核心

I²C 核心(drivers/i2c/i2c-core.c)中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解其中的主要函数非常关键,因为 I²C 总线驱动和设备驱动之间以 I²C 核心作为纽带。I²C 核心中的主要函数如下。

(1)增加 / 删除 i2c_adapter

int i2c_add_adapter(struct i2c_adapter *adap);
void i2c_del_adapter(struct i2c_adapter *adap);

(2)增加 / 删除 i2c_driver

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

(3)I²C 传输、发送和接收

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

i2c_transfer() 函数用于进行 I²C 适配器和 I²C 设备之间的一组消息交互,其中第 2 个参数是一个指向 i2c_msg 数组的指针,所以 i2c_transfer() 一次可以传输多个 i2c_msg(考虑到很多外设的读写波形比较复杂,比如读寄存器可能要先写,所以需要两个以上的消息)。而对于时序比较简单的外设,i2c_master_send() 函数和 i2c_master_recv() 函数内部会调用 i2c_transfer() 函数分别完成一条写消息和一条读消息。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

i2c_transfer() 函数本身不具备驱动适配器物理硬件以完成消息交互的能力,它只是寻找到与 i2c_adapter 对应的 i2c_algorithm,并使用 i2c_algorithmmaster_xfer() 函数真正驱动硬件流程:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1.4 I2C总线驱动

(1) IIC适配器驱动的注册与注销

**由于 I²C 总线控制器通常是在内存上的,所以它本身也连接在 platform 总线上,要通过 platform_driver 和 platform_device 的匹配来执行。因此尽管 I²C 适配器给别人提供了总线,它自己也被认为是接在 platform 总线上的一个客户。**Linux 的总线、设备和驱动模型实际上是一个树形结构,每个节点虽然可能成为别人的总线控制器,但是自己也被认为是从上一级总线枚举出来的。

通常我们会在与 I²C 适配器所对应的 platform_driverprobe() 函数中完成两个工作。
● 初始化 I²C 适配器所使用的硬件资源,如申请 I/O 地址、中断号、时钟等。
● 通过 i2c_add_adapter() 添加 i2c_adapter 的数据结构,当然这个 i2c_adapter 数据结构的成员已经被 xxx 适配器的相应函数指针所初始化。

通常我们会在 platform_driver 的 remove() 函数中完成与加载函数相反的工作。
● 释放 I²C 适配器所使用的硬件资源,如释放 I/O 地址、中断号、时钟等。
● 通过 i2c_del_adapter() 删除 i2c_adapter 的数据结构。

// 代码清单 15.9 I²C 适配器驱动的注册和注销模板
static int xxx_i2c_probe(struct platform_device *pdev)
{
    struct i2c_adapter *adap;

    ...
    xxx_adapter_hw_init()
    adap->dev.parent = &pdev->dev;
    adap->dev.of_node = pdev->dev.of_node;
    rc = i2c_add_adapter(adap);
    ...
}

static int xxx_i2c_remove(struct platform_device *pdev)
{
    ...
    xxx_adapter_hw_free()
    i2c_del_adapter(&pdev->adapter);
    return 0;
}

static const struct of_device_id xxx_i2c_of_match[] = {
    {.compatible = "vendor,xxx-i2c",},
    {},
};
MODULE_DEVICE_TABLE(of, xxx_i2c_of_match);

static struct platform_driver xxx_i2c_driver = {
    .driver = {
        .name = "xxx-i2c",
        .owner = THIS_MODULE,
        .of_match_table = xxx_i2c_of_match,
    },
    .probe = xxx_i2c_probe,
    .remove = xxx_i2c_remove,
};
module_platform_driver(xxx_i2c_driver);
(2) IIC总线的通信方式

我们需要为特定的 I²C 适配器实现通信方法,主要是实现 i2c_algorithm 的 functionality() 函数和 master_xfer() 函数。
functionality() 函数非常简单,用于返回 algorithm 所支持的通信协议,如 I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR、I2C_FUNC_SMBUS_READ_BYTE、I2C_FUNC_SMBUS_WRITE_BYTE 等。

master_xfer() 函数在 I²C 适配器上完成传递给它的 i2c_msg 数组中的每个 I²C 消息,代码清单 15.10 所示为 xxx 设备的 master_xfer() 函数模板。

static int i2c_adapter_xxx_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs,int num)
{
    ...
    for (i = 0; i < num; i++) {
        i2c_adapter_xxx_start(); /* 产生开始位 */
        /* 是读消息 */
        if (msgs[i]->flags & I2C_M_RD) {
            i2c_adapter_xxx_setaddr((msg->addr << 1) | 1); /* 发送从设备读地址 */
            i2c_adapter_xxx_wait_ack(); /* 获得从设备的 ack */
            i2c_adapter_xxx_readbytes(msgs[i]->buf, msgs[i]->len); /* 读取 msgs[i]->len 长的数据到 msgs[i]->buf */
         } else { /* 是写消息 */
             i2c_adapter_xxx_setaddr(msgs->addr << 1); /* 发送从设备写地址 */
            i2c_adapter_xxx_wait_ack(); /* 获得从设备的 ack */
            i2c_adapter_xxx_writebytes(msgs[i]->buf, msgs[i]->len); /* 读取 msgs[i]->len 长的数据到 msgs[i]->buf */
         }
     }
     i2c_adapter_xxx_stop(); /* 产生停止位 */
 }

上述代码实际上给出了一个 master_xfer() 函数处理 I²C 消息数组的流程,对于数组中的每个消息,先判断消息类型,若为读消息,则赋从设备地址为(msg->addr << 1)| 1,否则为 msg->addr << 1。对每个消息产生一个开始位,紧接着传送从设备地址,然后开始数据的发送或接收,且对最后的消息还需产生一个停止位。图 15.3 所示为整个 master_xfer() 完成的时序。

master_xfer() 函数模板中的 i2c_adapter_xxx_start()、i2c_adapter_xxx_setaddr()、i2c_adapter_xxx_wait_ack()、i2c_adapter_xxx_readbytes()、i2c_adapter_xxx_writebytes() 和 i2c_adapter_xxx_stop() 函数用于完成适配器的底层硬件操作,与 I²C 适配器和 CPU 的具体硬件直接相关,需要由工程师根据芯片的数据手册来实现。

i2c_adapter_xxx_readbytes() 用于从从设备上接收一串数据,i2c_adapter_xxx_writebytes() 用于向从设备写入一串数据,这两个函数的内部也会涉及 I²C 总线协议中的 ACK 应答。

master_xfer() 函数的实现形式会很多种,多数驱动以中断方式完成这个过程,比如发起硬件操作请求后,将自己调度出去,因此中间会伴随着睡眠的动作。

在 I²C 通信中,如果使用中断方式,master_xfer() 函数可能会执行以下步骤:

  1. 发起硬件操作请求master_xfer() 函数首先向硬件发送一个操作请求,比如开始一个数据传输。
  2. 将自己调度出去:在等待硬件操作完成的过程中,master_xfer() 函数可能会让出 CPU 的控制权,这样 CPU 就可以去执行其他任务。这通常涉及到某种形式的睡眠或等待状态,直到硬件操作完成。
  3. 中断处理:当硬件操作完成后,硬件设备会发送一个中断信号给 CPU,告诉它操作已经完成。CPU 然后会暂停当前正在执行的代码,转而执行与该中断相关的中断服务例程(ISR)。
  4. 完成数据传输:在中断服务例程中,master_xfer() 函数会继续完成剩余的数据传输工作,比如读取数据或发送数据。
  5. 唤醒等待的进程:如果其他进程或函数在等待 master_xfer() 函数完成,那么在数据传输完成后,这些进程或函数会被唤醒,继续执行。

多数 I²C 总线驱动会定义一个 xxx_i2c 结构体,作为 i2c_adapter 的 algo_data(类似“私有数据”),其中包含 I²C 消息数组指针、数组索引及 I²C 适配器 Algorithm 访问控制用的自旋锁、等待队列等,而 master_xfer() 函数在完成 i2c_msg 数组中消息的处理时,也经常需要访问 xxx_i2c 结构体的成员以获取寄存器基地址、锁等信息。代码清单 15.11 所示为一个典型的 xxx_i2c 结构体的定义,与图 15.2 中的 xxx_i2c 是对应的,具体的实现因硬件而异。

struct xxx_i2c {
	spinlock_t lock;
	wait_queue_head_t wait;
	struct i2c_msg *msg;
	unsigned int msg_num;
	unsigned int msg_idx;
	unsigned int msg_ptr;
	...
	struct i2c_adapter adap;
};

1.5 I2C设备驱动

I²C 设备驱动要使用 i2c_driver 和 i2c_client 数据结构并填充 i2c_driver 中的成员函数。i2c_client 一般被包含在设备的私有信息结构体 yyy_data 中,而 i2c_driver 则适合被定义为全局变量并初始化

static struct i2c_driver yyy_driver = {
    .driver = {
        .name = "yyy",
    },
    .probe    = yyy_probe,
    .remove   = yyy_remove,
    .id_table = yyy_id,
};
(0) I2C 设备信息描述

1、未使用设备树的时候

首先肯定要描述 I2C 设备节点信息,先来看一下没有使用设备树的时候是如何在 BSP 里面描述 I2C 设备信息的,在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备。 i2c_board_info 结构体如下:

struct i2c_board_info {
	char		type[I2C_NAME_SIZE];/* I2C 设备名字 */
	unsigned short	flags;			/* 标志 */
	unsigned short	addr;			/* I2C 器件地址 */
	void		*platform_data;
	struct dev_archdata	*archdata;
	struct device_node 	*of_node;
	struct fwnode_handle *fwnode;
	int		irq;
};

type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的器件地址。打开 arch/arm/mach-imx/mach-mx27_3ds.c 文件,此文件中关于 OV2640 的 I2C

设备信息描述如下:

static struct i2c_board_info mx27_3ds_i2c_camera = { I2C_BOARD_INFO("ov2640", 0x30),
};

示例代码中使用 I2C_BOARD_INFO 来完成 mx27_3ds_i2c_camera 的初始化工作, I2C_BOARD_INFO 是一个宏,定义如下:

 #define I2C_BOARD_INFO(dev_type, dev_addr) \
 	.type = dev_type, 						\
	.addr = (dev_addr)

可以看出, I2C_BOARD_INFO 宏其实就是设置 i2c_board_info 的 type 和 addr 这两个成员变量,因此示例代码的主要工作就是设置 I2C 设备名字为 ov2640, ov2640 的器件地址为 0X30。

大家可以在 Linux 源码里面全局搜索 i2c_board_info,会找到大量以 i2c_board_info 定义的I2C 设备信息,这些就是未使用设备树的时候 I2C 设备的描述方式,当采用了设备树以后就不会再使用 i2c_board_info 来描述 I2C 设备了。

2、使用设备树的时候

使用设备树的时候 I2C 设备信息通过创建相应的节点就行了,比如 NXP 官方的 EVK 开发板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息。打开 imx6ull-14x14-evk.dts 这个设备树文件,然后找到如下内容:

&i2c1 {
	clock-frequency = <100000>; 
	pinctrl-names = "default"; 
	pinctrl-0 = <&pinctrl_i2c1>; 
	status = "okay";  
	mag3110@0e {
        compatible = "fsl,mag3110";
        reg = <0x0e>;
        position = <2>;
	};
};

第 7~11 行,向 i2c1 添加 mag3110 子节点,第 7 行“mag3110@0e”是子节点名字,“@”后面的“0e”就是 mag3110 的 I2C 器件地址。第 8 行设置 compatible 属性值为“fsl,mag3110”。第 9 行的 reg 属性也是设置 mag3110 的器件地址的,因此值为 0x0e。 I2C 设备节点的创建重点是 compatible 属性和 reg 属性的设置,一个用于匹配驱动,一个用于设置器件地址。

(1) Linux I²C 设备驱动的模块加载与卸载

I²C 设备驱动的模块加载函数通过 I²C 核心的 i2c_add_driver() API 函数添加 i2c_driver 的工作,而模块卸载函数需要做相反的工作:通过 I²C 核心的 i2c_del_driver() 函数删除 i2c_driver。

static int __init yyy_init(void)
{
    return i2c_add_driver(&yyy_driver);
}
module_initcall(yyy_init);

static void __exit yyy_exit(void)
{
    i2c_del_driver(&yyy_driver);
}
module_exit(yyy_exit);
(2) Linux I²C 设备驱动的数据传输

在 I²C 设备上读写数据的时序且数据通常通过 i2c_msg 数组进行组织,最后通过 i2c_transfer() 函数完成,代码 所示为一个读取指定偏移 offs 的寄存器。

struct i2c_msg msg[2];
/* 第一条消息是写消息 */
msg[0].addr = client->addr;
msg[0].flags = 0;
msg[0].len = 1;
msg[0].buf = &offs;
/* 第二条消息是读消息 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD;
msg[1].len = sizeof(buf);
msg[1].buf = &buf[0];

i2c_transfer(client->adapter, msg, 2);
(3) Linux的i2c-dev.c文件分析

[I2C驱动(五)–通用驱动i2c-dev.c分析-CSDN博客](https://blog.csdn.net/zpz979994367/article/details/145805750

1.6 实例

以百问网 I.MX6U 开发板上的 AP3216C 这个三合一环境光传感器为例

(1)修改设备树

首先肯定是要修改 IO, AP3216C 用到了 I2C1 接口 ;这里将 UART4_TXD 和 UART4_RXD 这两个 IO 分别复用为 I2C1_SCL 和 I2C1_SDA,电气属性都设置为 0x4001b8b0。

&iomuxc {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_hog_1>;
    imx6ul-evk {
    ...
    ...
       pinctrl_i2c1: i2c1grp {
            fsl,pins = <
                MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
                MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
            >;
        };
    ...
    ...
};

在 i2c1 节点追加 ap3216c 子节点 ;@后面的“1e”是 ap3216c 的器件地址。 reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。

&i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";
	
    ap3216c@1e{
    	compatible = "alientek,ap3216c";
		reg = <0x1e>;
    };
};
(2)AP3216c设备驱动编写

先在 ap3216creg.h 中定义好 AP3216C 的寄存器,输入如下内容 :

#ifndef AP3216C_H
#define AP3216C_H


#define AP3216C_ADDR    	0X1E	/* AP3216C器件地址  */

/* AP3316C寄存器 */
#define AP3216C_SYSTEMCONG	0x00	/* 配置寄存器       */
#define AP3216C_INTSTATUS	0X01	/* 中断状态寄存器   */
#define AP3216C_INTCLEAR	0X02	/* 中断清除寄存器   */

#define AP3216C_IRDATALOW	0x0A	/* IR数据低字节     */
#define AP3216C_IRDATAHIGH	0x0B	/* IR数据高字节     */
#define AP3216C_ALSDATALOW	0x0C	/* ALS数据低字节    */
#define AP3216C_ALSDATAHIGH	0X0D	/* ALS数据高字节    */
#define AP3216C_PSDATALOW	0X0E	/* PS数据低字节     */
#define AP3216C_PSDATAHIGH	0X0F	/* PS数据高字节     */

#endif

然后在 ap3216c.c 输入如下内容:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include "ap3216creg.h"
 
#define AP3216C_CNT	1
#define AP3216C_NAME	"ap3216c"


struct ap3216c_dev {
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	int major;					/* 主设备号 */
	void *private_data;			/* 私有数据 */
	unsigned short ir, als, ps;	/* 三个光传感器数据 */
};
 
static struct ap3216c_dev ap3216cdev;
 
/*
 * @description	: 从ap3216c读取多个寄存器数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器首地址
 * @param - val:  读取到的数据
 * @param - len:  要读取的数据长度
 * @return 		: 操作结果
 */
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
	int ret;
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
 
	/* msg[0]为发送要读取的首地址 */
	msg[0].addr = client->addr;			/* ap3216c地址 */
	msg[0].flags = 0;					/* 标记为发送数据 */
	msg[0].buf = &reg;					/* 读取的首地址 */
	msg[0].len = 1;						/* reg长度*/
 
	/* msg[1]读取数据 */
	msg[1].addr = client->addr;			/* ap3216c地址 */
	msg[1].flags = I2C_M_RD;			/* 标记为读取数据*/
	msg[1].buf = val;					/* 读取数据缓冲区 */
	msg[1].len = len;					/* 要读取的数据长度*/
 
	ret = i2c_transfer(client->adapter, msg, 2);
	if(ret == 2) {
		ret = 0;
	} else {
		printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
		ret = -EREMOTEIO;
	}
	return ret;
}
 
/*
 * @description	: 向ap3216c多个寄存器写入数据
 * @param - dev:  ap3216c设备
 * @param - reg:  要写入的寄存器首地址
 * @param - val:  要写入的数据缓冲区
 * @param - len:  要写入的数据长度
 * @return 	  :   操作结果
 */
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	
	b[0] = reg;					/* 寄存器首地址 */
	memcpy(&b[1],buf,len);		/* 将要写入的数据拷贝到数组b里面 */
		
	msg.addr = client->addr;	/* ap3216c地址 */
	msg.flags = 0;				/* 标记为写数据 */
 
	msg.buf = b;				/* 要写入的数据缓冲区 */
	msg.len = len + 1;			/* 要写入的数据长度 */
 
	return i2c_transfer(client->adapter, &msg, 1);
}
 
/*
 * @description	: 读取ap3216c指定寄存器值,读取一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要读取的寄存器
 * @return 	  :   读取到的寄存器值
 */
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
	u8 data = 0;
 
	ap3216c_read_regs(dev, reg, &data, 1);
	return data;
 
#if 0
	struct i2c_client *client = (struct i2c_client *)dev->private_data;
	return i2c_smbus_read_byte_data(client, reg);
#endif
}
 
/*
 * @description	: 向ap3216c指定寄存器写入指定的值,写一个寄存器
 * @param - dev:  ap3216c设备
 * @param - reg:  要写的寄存器
 * @param - data: 要写入的值
 * @return   :    无
 */
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
	u8 buf = 0;
	buf = data;
	ap3216c_write_regs(dev, reg, &buf, 1);
}
 
/*
 * @description	: 读取AP3216C的数据,读取原始数据,包括ALS,PS和IR, 注意!
 *				: 如果同时打开ALS,IR+PS的话两次数据读取的时间间隔要大于112.5ms
 * @param - ir	: ir数据
 * @param - ps 	: ps数据
 * @param - ps 	: als数据 
 * @return 		: 无。
 */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
	unsigned char i =0;
    unsigned char buf[6];
	
	/* 循环读取所有传感器数据 */
    for(i = 0; i < 6; i++)	
    {
        buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);	
    }
 
    if(buf[0] & 0X80) 	/* IR_OF位为1,则数据无效 */
		dev->ir = 0;					
	else 				/* 读取IR传感器的数据   		*/
		dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03); 			
	
	dev->als = ((unsigned short)buf[3] << 8) | buf[2];	/* 读取ALS传感器的数据 			 */  
	
    if(buf[4] & 0x40)	/* IR_OF位为1,则数据无效 			*/
		dev->ps = 0;    													
	else 				/* 读取PS传感器的数据    */
		dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F); 
}
 
/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &ap3216cdev;
 
	/* 初始化AP3216C */
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0x04);		/* 复位AP3216C 			*/
	mdelay(50);														/* AP3216C复位最少10ms 	*/
	ap3216c_write_reg(&ap3216cdev, AP3216C_SYSTEMCONG, 0X03);		/* 开启ALS、PS+IR 		*/
	return 0;
}
 
/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
	short data[3];
	struct ap3216c_dev *dev = (struct ap3216c_dev *)filp->private_data;
	
	ap3216c_readdata(dev);
 
	data[0] = dev->ir;
	data[1] = dev->als;
	data[2] = dev->ps;
	
	/* 检查copy_to_user返回值 */
	if (copy_to_user(buf, data, sizeof(data))) {
		return -EFAULT; // 返回错误码
	}
	
	/* 返回实际传输的字节数 */
	return sizeof(data); 
}
 
/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int ap3216c_release(struct inode *inode, struct file *filp)
{
	return 0;
}
 
/* AP3216C操作函数 */
static const struct file_operations ap3216c_ops = {
	.owner = THIS_MODULE,
	.open = ap3216c_open,
	.read = ap3216c_read,
	.release = ap3216c_release,
};
 
 /*
  * @description     : i2c驱动的probe函数,当驱动与
  *                    设备匹配以后此函数就会执行
  * @param - client  : i2c设备
  * @param - id      : i2c设备ID
  * @return          : 0,成功;其他负值,失败
  */
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
	/* 1、构建设备号 */
	if (ap3216cdev.major) {
		ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
		register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
	} else {
		alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
		ap3216cdev.major = MAJOR(ap3216cdev.devid);
	}
 
	/* 2、注册设备 */
	cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
	cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);
 
	/* 3、创建类 */
	ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.class)) {
		return PTR_ERR(ap3216cdev.class);
	}
 
	/* 4、创建设备 */
	ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
	if (IS_ERR(ap3216cdev.device)) {
		return PTR_ERR(ap3216cdev.device);
	}
 
	ap3216cdev.private_data = client;
 
	return 0;
}
 
/*
 * @description     : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
 * @param - client 	: i2c设备
 * @return          : 0,成功;其他负值,失败
 */
static int ap3216c_remove(struct i2c_client *client)
{
	/* 删除设备 */
	cdev_del(&ap3216cdev.cdev);
	unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
 
	/* 注销掉类和设备 */
	device_destroy(ap3216cdev.class, ap3216cdev.devid);
	class_destroy(ap3216cdev.class);
	return 0;
}
 
/* 传统匹配方式ID列表 */
static const struct i2c_device_id ap3216c_id[] = {
	{"alientek,ap3216c", 0},  
	{}
};
 
/* 设备树匹配列表 */
static const struct of_device_id ap3216c_of_match[] = {
	{ .compatible = "alientek,ap3216c" },		/* 需要和设备树下的保持一致 */
	{ /* Sentinel */ }
};
 
/* i2c驱动结构体 */	
static struct i2c_driver ap3216c_driver = {
	.probe = ap3216c_probe,
	.remove = ap3216c_remove,
	.driver = {						/* 设备树的匹配方法 */
			.owner = THIS_MODULE,
		   	.name = "ap3216c",
		   	.of_match_table = ap3216c_of_match, 	
		   },
	.id_table = ap3216c_id,			/* 传统驱动匹配方法 */
};
		   
/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init ap3216c_init(void)
{
	int ret = 0;
 
	ret = i2c_add_driver(&ap3216c_driver);
	return ret;
}
 
/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit ap3216c_exit(void)
{
	i2c_del_driver(&ap3216c_driver);
}
 
/* module_i2c_driver(ap3216c_driver) */
 
module_init(ap3216c_init);
module_exit(ap3216c_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("sneak");
(3)测试程序编写

然后在 ap3216ctest.c 输入如下内容:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>


int main(int argc,char * argv[])
{

	int fd;
	char *filename;
	short databuf[3];
	unsigned short ir, als, ps;
	int ret = 0;


	if (argc != 2) {
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];
	fd = open(filename, O_RDWR);
	if(fd < 0) {
		printf("can't open file %s\r\n", filename);
		return -1;
	}
	
	while (1) {
		ret = read(fd, databuf, sizeof(databuf));
		if(ret == sizeof(databuf)) { 			/* 数据读取成功 */
			ir =  databuf[0]; 	/* ir传感器数据 */
			als = databuf[1]; 	/* als传感器数据 */
			ps =  databuf[2]; 	/* ps传感器数据 */
			printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
		}
		usleep(200000); /*100ms */
	}
	close(fd);	/* 关闭文件 */	
	return 0;
	

}

《Linux设备驱动开发详解:基于最新的Linux 4.0内核 (宋宝华)》
《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.81》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何应对硬件测试覆盖率不足导致量产故障

硬件测试覆盖率不足导致的量产故障是硬件制造领域的一大痛点。要有效应对&#xff0c;必须从提高测试覆盖率、优化测试方案、引入风险管理机制三个方面入手。其中&#xff0c;优化测试方案尤为关键&#xff0c;应从产品设计阶段开始&#xff0c;通过精确的测试用例规划、详细的…

Centos7 安装 TDengine

Centos7 安装 TDengine 1、简介 官网&#xff1a; https://www.taosdata.com TDengine 是一款开源、高性能、云原生的时序数据库&#xff08;Time Series Database, TSDB&#xff09;, 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计。同时它还带有内建的缓…

Kafka 多线程开发消费者实例

目前&#xff0c;计算机的硬件条件已经大大改善&#xff0c;即使是在普通的笔记本电脑上&#xff0c;多核都已经是标配了&#xff0c;更不用说专业的服务器了。如果跑在强劲服务器机器上的应用程序依然是单线程架构&#xff0c;那实在是有点暴殄天物了。不过&#xff0c;Kafka …

Linux线程池实现

1.线程池实现 全部代码&#xff1a;whb-helloworld/113 1.唤醒线程 一个是唤醒全部线程&#xff0c;一个是唤醒一个线程。 void WakeUpAllThread(){LockGuard lockguard(_mutex);if (_sleepernum)_cond.Broadcast();LOG(LogLevel::INFO) << "唤醒所有的休眠线程&q…

Linux《进程概念(上)》

在之前的Linux学习当中我们已经了解了基本的Linux指令以及基础的开发工具的使用&#xff0c;那么接下来我们就要开始Linux当中一个非常重要的部分的学习——进程&#xff0c;在此进程是我们之后Linux学习的基础&#xff0c;并且通过进程的学习会让我们了解更多的操作系统的相关…

【算法】并查集基础讲解

一、定义 一种树型的数据结构&#xff0c;用于处理一些不相交集合的合并及查询问题。思想是用一个数组表示了整片森林&#xff08;parent&#xff09;&#xff0c;树的根节点唯一标识了一个集合&#xff0c;只要找到了某个元素的的树根&#xff0c;就能确定它在哪个集合里。 …

C++ STL常用算法之常用集合算法

常用集合算法 学习目标: 掌握常用的集合算法 算法简介: set_intersection // 求两个容器的交集 set_union // 求两个容器的并集 set_difference // 求两个容器的差集 set_intersection 功能描述: 求两个容器的交集 函数原型: set_intersection(iterator beg1, iterat…

日程公布| 第八届地球空间大数据与云计算前沿大会与集中学习(3号通知)

日程公布| 第八届地球空间大数据与云计算前沿大会与集中学习&#xff08;3号通知&#xff09; 日程公布| 第八届地球空间大数据与云计算前沿大会与集中学习&#xff08;3号通知&#xff09;

Linux C语言调用第三方库,第三方库如何编译安装

在 Linux 环境下使用 C 语言调用第三方库时&#xff0c;通常需要先对第三方库进行编译和安装。以下为你详细介绍一般的编译安装步骤&#xff0c;并给出不同类型第三方库&#xff08;如使用 Makefile、CMake 构建系统&#xff09;的具体示例。 一般步骤 1. 获取第三方库源码 …

leetcode -编辑距离

为了求解将 word1 转换成 word2 所需的最少操作数&#xff0c;可以使用动态规划。以下是详细的解决方案&#xff1a; ### 方法思路 1. **定义状态** dp[i][j] 表示将 word1 的前 i 个字符转换成 word2 的前 j 个字符所需的最少操作数。 2. **状态转移方程** - 如果 word1[…

字节开源版Manus来袭

字节开源版Manus来袭 项目地址&#xff1a;https://github.com/langmanus/langmanus/blob/main/README_zh.md 在人工智能领域&#xff0c;Manus的出现无疑是一颗重磅炸弹&#xff0c;它凭借强大的通用Agent能力&#xff0c;迅速吸引了全球开发者和AI爱好者的目光。然而&#…

论文阅读笔记——PointVLA: Injecting the 3D World into Vision-Language-Action Models

PointVLA 论文 现有的 VLA 基于 2D 视觉-语言数据表现良好但缺乏 3D 几何先验导致空间推理缺陷。传统方案&#xff1a;1&#xff09;3D->2D 投影&#xff0c;造成几何信息损失&#xff1b;2&#xff09;3D 数据集少。PointVLA 保留原有 VLA&#xff0c;提取点云特征&#xf…

在win11 环境下 新安装 WSL ubuntu + 换国内镜像源 + ssh + 桌面环境 + Pyhton 环境 + vim 设置插件安装

在win11 环境下 新安装 WSL ubuntu ssh gnome 桌面环境 Pyhton 环境 vim 设置插件安装 简单介绍详细流程换国内镜像源安装 ssh 桌面环境python 环境vim 设置插件安装 简单介绍 内容有点长&#xff0c;这里就先简单描述内容了。主要是快速在 Win11 搭建一个 wsl 的 linux 环…

基于springboot课程学习与互动平台(源码+lw+部署文档+讲解),源码可白嫖!

摘要 随着我国经济的高速发展与人们生活水平的日益提高&#xff0c;人们对生活质量的追求也多种多样。尤其在人们生活节奏不断加快的当下&#xff0c;人们更趋向于足不出户解决生活上的问题&#xff0c;线上管理系统展现了其蓬勃生命力和广阔的前景。与此同时&#xff0c;在此…

通俗易懂的大模型原理

十分钟揭秘DeepSeek原理&#xff0c;通俗易懂的大语言模型科普&#xff01;_哔哩哔哩_bilibili 最基础原理&#xff0c;x是输入&#xff0c;y是输出。上百万和上百亿的参数 将一句话转化为数字向量 一句话就是向量矩阵 输入矩阵和参数矩阵进行计算得出输出矩阵&#xff0c;因为…

热门索尼S-Log3电影感氛围旅拍LUTS调色预设 Christian Mate Grab - Sony S-Log3 Cinematic LUTs

热门索尼S-Log3电影感氛围旅拍LUTS调色预设 Christian Mate Grab – Sony S-Log3 Cinematic LUTs 我们最好的 Film Look S-Log3 LUT 的集合&#xff0c;适用于索尼无反光镜相机。无论您是在户外、室内、风景还是旅行电影中拍摄&#xff0c;这些 LUT 都经过优化&#xff0c;可为…

【jQuery】插件

目录 一、 jQuery插件 1. 瀑布流插件&#xff1a; jQuery 之家 http://www.htmleaf.com/ 2. 图片懒加载&#xff1a; jQuery 插件库 http://www.jq22.com/ 3. 全屏滚动 总结不易~ 本章节对我有很大收获&#xff0c;希望对你也是~~~ 一、 jQuery插件 jQuery 功能…

MATLAB导入Excel数据

假如Excel中存在三列数据需要导入Matlab中。 保证该Excel文件与Matlab程序在同一目录下。 function [time, voltage, current] test(filename)% 读取Excel文件并提取时间、电压、电流数据% 输入参数:% filename: Excel文件名&#xff08;需包含路径&#xff0c;如C:\data\…

孤码长征:破译PCL自定义点云注册机制源码迷局——踩坑实录与架构解构

在之前一个博客《一文搞懂PCL中自定义点云类型的构建与函数使用》中&#xff0c;清晰地介绍了在PCL中点云的定义与注册方法。我的一个读者很好奇其内部注册的原理以及机制&#xff0c;再加上最近工作中跟猛男开发自定义点云存储的工作&#xff0c;借着这些需求&#xff0c;我也…

Centos 7 搭建 jumpserver 堡垒机

jumpserver 的介绍 1、JumpServer 是完全开源的堡垒机, 使用 GNU GPL v2.0 开源协议, 是符合4A 的专业运维审计系统 1)身份验证 / Authentication 2)授权控制 / Authorization 3)账号管理 / Accounting 4)安全审计 / Auditing 2、JumpServer 使用 Python / Django 进行开…