总线相关

什么是总线?

设备模型包含总线,设备,驱动和类四概念

总线:总线是CPU和设备进行信息交互的通道,所有的设备都要连接到总线上。总线包含虚拟总线和外设总线

总线的作用?

总线管理着设备链表和设备驱动链表两个链表,当注册一个驱动时会插入到总线的设备驱
动链表,当注册一个设备时会插入到总线的设备链表。在插入设备或驱动时,总线会执行
bus_type 结构体中的 match 函数对新插入的设备或驱动进行匹配,如以名字的方式进行匹配。
匹配成功后会调用驱动中的 probe()函数,即 device_driver 结构体中的 probe()函数。
I2C总线?
如下图,I2C总线式真实的数据线路,而paltform总线式虚拟的总线

设备控制器直接和CPU连接,CPU可以通过寻址操作访问它们,但设备模型应该具备普适性,因此Linux就虚构了一条Platform总线,供这些设备链接。

I2C子系统框架

I2C适配器驱动层:

I2C适配器驱动层的软件已经被原厂工程师写好了,并且这部分是包含时序相关的内容的。

然后,我们只是编写i2c_driver,在这个部分里面,不再涉及时序相关内容。

I2C设备驱动层:

向上提供接口,向下进行硬件操作

I2C核心层:

这个部分包含时序的内容,并且不需要我们编写。是内核准备好的,我们编写i2c_driver的时候,对这个部分的API进行调用

驱动

I2C驱动分为I2C_client和I2C_driver两个部分,或者说I2C设备模型 = client(设备) + driver(驱动),解析来分开进行讲解。i2c_client:描述“设备是谁”  ; i2c_driver:描述“怎么驱动这个设备”

i2c_client(设备)

i2c-core 匹配

i2c_driver(驱动)

调用 probe(client)

I2C_client框架

在平台总线模型中,驱动分为device和driver部分,然后设备树的出现替代了device部分,这里的i2c_client部分也是对设备硬件资源的描述,所以在这里,i2c_client也可以用设备树的节点替代。

需要一提的是,原来i2c_client代码可以用C编写,现在以及不用了,用的都是设备树。

C语言编写的i2c_client如下

struct i2c_client {
    unsigned short addr;
    struct i2c_adapter *adapter;
};

设备树编写的I2C_client如下

&i2c1{
    status = "okay";
    myft5x06:my-ft5x06@38{
        compatible = "my-ft5x06";
        reg = <0x38>;
    }
}

I2C1:使用已经定义的控制器节点,表示“我要在 I2C1 这条总线上挂设备”。

status = "okay": 表示启用这个I2Cd的控制器。

在这个i2C1总线上挂载了一个节点myft5x06,名字是my -ft5x06 以及地址 reg = <0x38>,这里先写这些基本的信息,后面再补全。

I2C_dirver框架:

让I2C设备工作起来的代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/of_device.h>

// probe → 驱动接管设备
int ft5x06_probe(struct i2c_client *client, const struct i2c_device_id *id) {
    printk("This is ft5x06 probe\n");
    return 0;
}

// ft5x06 设备的移除函数
int ft5x06_remove(struct i2c_client *client) {
    return 0;
}

// 设备树匹配表
static const struct of_device_id ft5x06_id[] = {
    { .compatible = "my-ft5x06" }, 
    { }, 
};

// 驱动注册 → 提供 driver
static struct i2c_driver ft5x06_driver = {
        .driver = {
        .owner = THIS_MODULE, 
        .name = "my-ft5x06", 
        .of_match_table = ft5x06_id, //添加设备树匹配表
    },
    .probe = ft5x06_probe, 
    .remove = ft5x06_remove, 
};

// 驱动初始化函数
static int __init ft5x06_driver_init(void) {
    int ret;
    // 注册 i2c 设备驱动
    ret = i2c_add_driver(&ft5x06_driver);
    if (ret < 0) {
        printk("i2c_add_driver is error\n");
        return ret;
    }
    return 0;
}

// 驱动退出函数
static void __exit ft5x06_driver_exit(void) {
    // 注销 i2c 设备驱动
    i2c_del_driver(&ft5x06_driver);
}

module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");

代码功能:

这段代码的功能可以用一句话总结:把设备树里的 my-ft5x06 设备“接管”,并在 probe 里开始控制它

函数执行流程:

模块加载

module_init(ft5x06_driver_init)

ft5x06_driver_init()

i2c_add_driver()---内核有,必须调用

I2C子系统开始匹配

probe()  ---- 自己填完整,必须实现

执行流程:

模块卸载(rmmod)

ft5x06_driver_exit()

i2c_del_driver()  ----内核有,必须调用

I2C core 解绑 driver 和 client

调用 ft5x06_remove() ---- 自己写,必须实现

函数关系:

init → i2c_add_driver()
exit → i2c_del_driver()

probe ← i2c_add_driver() 触发
remove ← i2c_del_driver() 触发

ft5x06_driver_init把驱动“交给 I2C 子系统”,然后内核开始找 ,有没有设备可以匹配这个 driver?

i2c_add_driver负责真正执行这个动作,把 driver 挂到 I2C 总线, 遍历所有 i2c_client,尝试匹配 compatible,如果匹配成功,调用probe,调用probe 后驱动正式“接管设备”。ft5x06_driver_exit把dirver从子系统中移除,i2c_del_driver触发remove函数进行资源清理。

i2C_client补全

&i2c1{
    status = "okay";
    myft5x06:my-ft5x06@38{
        compatible = "my-ft5x06";
        reg = <0x38>;
        //复位引脚描述
        reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
        //中断引脚描述
        interrupt-parent = <&gpio3>;
        interrupts-gpio = <&gpio3 RK_PA5 GPIO_ACTIVE_LOW>;
        interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;
        //引脚复用配置
        pinctrl-names = "default";
        pinctrl-0 = <&myft5x06_pins>;
    };
};

reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>; //复位引脚

&gpio0指的是gpio控制器,RK_PB6是引脚编号, GPIO_ACTIVE_LOW表示低电平有效。

含义:复位脚接在 gpio0_B6,上电拉低才是“有效复位”

interrupt-parent = <&gpio3>;

中断来自 gpio3 控制器
interrupts-gpio = <&gpio3 RK_PA5 GPIO_ACTIVE_LOW>;

中断脚接在 gpio3_A5,低电平有效
interrupts = <RK_PA5 IRQ_TYPE_LEVEL_LOW>;

使用 GPIO A5 作为中断源,触发方式:低电平触发

引脚复用配置,设置使用默认配置,然后默认配置是字节设定的m&myft5x06_pins

pinctrl-names = "default";
pinctrl-0 = <&myft5x06_pins>;

&pinctrl {
        myft5x06 {
            myft5x06_pins: myft5x06-pins {
            rockchip,pins =
                <3 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>,
                <0 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>;
            };
        };
};

<3 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>,

gpio3控制器下的,A组第五个引脚,RK_FUNC_GPIO,设置位GPIO模式,pcfg_pull_none不上拉也不下拉。

pinctrl子系统使用位置

设备树加载

驱动 probe 前

pinctrl 子系统生效

执行 rockchip,pins 配置

寄存器被设置

引脚功能改变

i2c_driver补全

#include <linux/init.h>
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
//定义 ft5x06 设备的 GPIO 描述符
struct gpio_desc *reset_gpio, *irq_gpio;
// ft5x06 中断处理函数
irqreturn_t ft5x06_handler(int irq, void *args)
{
    printk("This is ft5x06 handler\n");
    //返回中断已处理标志
    return IRQ_RETVAL(IRQ_HANDLED);
}


// ft5x06 设备的探测函数
int ft5x06_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;
    printk("This is ft5x06 probe\n");
    //获取 reset GPIO 描述符
    reset_gpio = gpiod_get_optional(&client->dev, "reset", 0);
    if (!reset_gpio) {
        printk("gpiod_get_optional reset gpio is error\n");
        return -1;
    }

    //设置 reset GPIO 为输出,并拉低 5ms 后拉高
    gpiod_direction_output(reset_gpio, 0);
    msleep(5);
    gpiod_direction_output(reset_gpio, 1);
     
    //获取 irq GPIO 描述符
    irq_gpio = gpiod_get_optional(&client->dev, "interrupts", 0);
    if (!irq_gpio) {
        printk("gpiod_get_optional irq gpio is error\n");
        return -1;
    }
    //请求中断,设置为下降沿触发,单次触发
    ret = request_irq(client->irq, ft5x06_handler, IRQ_TYPE_EDGE_FALLING | IRQF_ONESHOT,"ft5x06 irq", NULL);
    
    if (ret < 0) {
        printk("request irq is error\n");
        return -2;
    }
    return 0;
  }

// ft5x06 设备的移除函数
int ft5x06_remove(struct i2c_client *client)
{
    free_irq(client->irq, NULL);
    gpiod_put(reset_gpio);
    gpiod_put(irq_gpio);
    return 0;
}

//定义 i2c_device_id 结构体数组,用于标识 ft5x06 设备
static const struct i2c_device_id ft5x06_id[] = {
    { "my-ft5x06", 0 }, { }
};


//定义 i2c_driver 结构体,描述 ft5x06 设备驱动
static struct i2c_driver ft5x06_driver = {
    .driver = {
        .name = "my-ft5x06", 
        .owner = THIS_MODULE, 
    },
    .probe = ft5x06_probe, 
    .remove = ft5x06_remove, 
    .id_table = ft5x06_id, 
};

//驱动初始化函数
static int __init ft5x06_driver_init(void)
{
    int ret;
    //注册 i2c 设备驱动
    ret = i2c_add_driver(&ft5x06_driver);
    if (ret < 0) {
        printk("i2c_add_driver is error\n");
        return ret;
    }
    return 0;
}

//驱动退出函数
static void __exit ft5x06_driver_exit(void)
{
    //注销 i2c 设备驱动
    i2c_del_driver(&ft5x06_driver);
}

module_init(ft5x06_driver_init);
module_exit(ft5x06_driver_exit);
MODULE_LICENSE("GPL");

I2C读写函数

读函数:

这部分主要是调用内核提供的i2c_tarnsfer函数,并且提供该函数需要的参数,adapter,msg,flages。一共四个内容,函数就编写完成。其中adapter是I2C控制器,msg是传输的数据,是对data的封装,flags是表示本次操作是读还是写,因为读写都是用的I2C_transfer函数。

读函数的msg 有两个成员,是因为i2c读数据,需要先写一次告诉设备我要读哪个寄存器,然后读数据

//读函数
int ft5x06_read_reg(u8 reg_addr)
{
    u8 data;
    struct i2c_msg msgs[2]={
        [0]={
            .addr = ft5x06_client->addr, //设备地址
            .flags = 0, //写操作
            .len = sizeof(reg_addr), 
            .buf = &reg_addr, //写入要读取的寄存器地址
        } 
        [1]={
             .addr = ft5x06_client->addr, 
             .flags = i2c_M_RD, //读操作
             .len = sizeof(data), 
             .buf = &data, //读取到的数据存储位置           
        }
    {

    i2c_transfer(ft5x06_client->adapter,msgs,2);

    return data;
}

写函数:

整体逻辑和读数据一样,都是嗲用i2c_transfer,然后函数内部需要提供 data ,flags,msgs。其中

len = len+1是指除了数据本身,还需要发送寄存器地址,8bit,一个字节。memcpy(&buff[1],&data,len);表示,把data的内容复制到buff[1],长度位len。

buff[0]和buff[1]的内容区别在于。
buff[0]    寄存器地址    告诉芯片操作哪个寄存器
buff[1]    要写入的数据    写进寄存器的值

在msg中,.buf = buff是数组的首地址,然后buff[0]和buff[1]自然就被i2c_transfer拿到了

//写函数
void ft5x06_write_reg(u8 reg_addr,u8 data , u16 len)
{
    u8 buff[256];
    struct i2c_msg msgs[]={
        [0]={
            .addr = ft5x06_client->addr, //设备地址
            .flags = 0, //写操作
            .len = len+1;
            .buf = buff, //写入要读取的寄存器地址
        }
    {
    buff[0] = reg_addr;
    memcpy(&buff[1],&data,len);
    i2c transfer(ft5x06_client->adapter,msgs,2);
}

提问:

子系统框架部分:

SCL和SDA信号是谁产生的?

是I2C控制器产生的。

如果多个应用程序同时想调用一个硬件设备,怎么办?

I2C核心层会 处理这个问题,给APP排列使用这个硬件设备的顺序。

I2C基础知识部分:

什么是 I2C?

I2C 是一种同步串行通信总线,由 SCL(时钟线)和 SDA(数据线)组成,支持多主多从,
通过设备地址区分不同从设备。但是一般不怎么用多主。

追问:什么叫串行通信,什么叫并行通信?

串行通信,是指一个bit一个bit的发数据,因为只有一根数据线

并行通信,是指,多个bit一起发数据,一般有多根数据线

追问: 什么叫同步通信,什么叫异步通信?

通信双方共用同一个时钟,以约定好的规则,进行数据的读取,发送和接收按照同一个节拍工作。比如I2C中就是约定,在高电平读数据。

对应的,异步就是没有时钟,比如串口,就是异步通信。用的物理线路是RX,TX,GND。双方提前约定“传输速度”。也就是波特率,常用的是115200。双方自己“按时间推算”。

UART默认线路是高电平。当发送开始,TX 拉低,接收方看到”高到低的变化“,就知道发送开始了。因为双方以及约定好速率,115200 baud,也就是差多T = 1/115200 = 8.68 微秒. 接受方每隔 8.68us 采样一次,也就是说接收方,“自己推时间”。

这种没有公共时钟,各自用自己时钟的的通信方式就是异步通信。异步通信只能做到频率尽量一致,并不能完全一致。

I2C 为什么需要上拉电阻?

因为 I2C 使用开漏输出,设备只能拉低总线,不能主动输出高电平,所以必须通过上拉电阻得到高电平。

追问:为啥那么I2C设备都是只能输出低电平呢?

追问:  为什么I2C用的是开漏输出?

这种方式保证了所有I2C设备都是只能有一个操作,就是输出低电平,不会导致I2C总线短路。也就是I2C总线可以挂多个设备的原因。

为什么是用电阻进行上拉呢?

防止总线悬空,不直接接VCC拉高是,怕VCC接地,直接短路。

I2C 的设备地址是多少位?

7位,第8位要表示读写

什么是 ACK/NACK?

ACK是从设备的应答信号,通过再第九位高电平期间拉低电平实现。NACK就是不拉低,不应答的意思

Linux I2C 框架相关:

Linux I2C 驱动分哪几层?

① I2C core
② I2C adapter(控制器驱动)
③ I2C client(设备)
④ I2C driver(设备驱动)

其中,I2C core和I2C driver 是没有实体的,是软件。I2C core就是linux内核中的软件管理层。负责。i2c-core 是 Linux I2C 子系统的核心管理层,负责管理 I2C 设备和驱动、完成设备与驱动匹配、调用 probe/remove,并向上提供统一的 I2C 通信框架,真正的硬件时序则由 i2c_adapter 完成。

记住一句话,i2c-core 的本质:就是“管理 I2C 总线上的 device 和 driver”。

工作流程:

① 注册 adapter(I2C控制器)
② 创建 client(I2C设备)
③ 注册 driver(设备驱动)
④ core 自动匹配
⑤ 调用 probe
⑥ 驱动通过 core 进行 I2C 通信

i2c_client 和 i2c_driver 的区别?

i2c_client 描述设备,
包含设备地址、总线等信息。

i2c_driver 描述驱动,
包含 probe/remove 等回调。

probe 什么时候调用?

当match函数把设备和驱动匹配成功后调用

remove 什么时候调用?

当卸载驱动模块的时候调用,用于资源的释放

compatible 的作用是什么?

用于设备树和驱动匹配。

设备树:
compatible = "xxx"

驱动:
of_match_table

设备树相关:

这部分主要是对属性的提问

reg 属性的作用?

表示设备地址,比如reg = <0x38>;

interrupts 属性作用?

描述设备使用的中断号,和触发方式。

pinctrl 的作用?

pinctrl子系统专门用于描述引脚复用,defult 表示使用默认配置。

驱动开发相关:

I2C 设备驱动的开发流程?(重点)

整体流程:

设备出现 → 驱动加载 → probe → 通信

标准开发流程:

① 编写设备树(描述设备)
② 编写 i2c_driver(驱动)
③ 注册 i2c_driver
④ i2c-core 自动匹配
⑤ probe 初始化设备
⑥ I2C通信
⑦ 注册子系统(input/char等)
⑧ remove 释放资源

整体架构:

设备树
   ↓
i2c_client(设备)

驱动代码
   ↓
i2c_driver(驱动)

      ↓
   i2c-core
      ↓
compatible 匹配
      ↓
probe()
      ↓
驱动接管设备

流程:

编写设备树,告诉linux系统I2C 总线上挂了什么设备,最重要的属性是compatibles属性,这个是用于驱动匹配的。然后编写,i2c_driver,告诉内核“我能驱动谁”,of_match_tables是关键成员,用于设备树匹配。下一步,i2c_add_driver(r);注册 i2c_driver,把 driver 注册到 i2c-core。i2c-core 自动匹配client 和driver。probe 初始化设备,进行中断申请,GPIO获取等操作。I2C 通信,函数编写,注册功能子系统,把把功能提供给用户空间

request_irq 和 free_irq 的关系?

request_irq 申请中断,free_irq 释放中断。

为什么 remove 里要释放资源?

防止内存泄漏、中断残留、GPIO资源占用。

I2C 通信相关:

Linux 中常用哪些 I2C API?(重点)

i2c_transfer()
i2c_master_send()
i2c_master_recv()

为什么 probe 不执行?

设备树方面:设备树修改后,没有重新编译加载,或者匹配失败

地址方面:I2C设备地不对

总线方面:总线没有使能

client->irq 从哪里来的?

来自设备树 interrupts 属性。

为什么中断里不能睡眠?

中断上下文没有进程调度,无法恢复,睡眠会导致系统异常

i2c_add_driver 内部做了什么?

① 注册 driver
② 挂到 I2C bus
③ 遍历 client
④ 调用匹配函数
⑤ 匹配成功后执行 probe

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐