自学内容网 自学内容网

嵌入式驱动开发详解13(IIC驱动架构实现)

前言

本内容主要讲解在linux系统上开发I2C设备驱动,先讲解I2C相关的基础知识,再讲解I2C设备驱动框架,最后讲解I2C设备驱动具体实现。

I2C简介

I2C初始

I2C 是很常见的一种总线协议,I2C 是 NXP 公司设计的,I2C 使用两条线在主控制器和从 机之间进行数据通信。一条是 SCL(串行时钟线),另外一条是 SDA(串行数据线),这两条数据 线需要接上拉电阻,总线空闲的时候 SCL 和 SDA 处于高电平。I2C 总线标准模式下速度可以 达到 100Kb/S,快速模式下可以达到 400Kb/S。
I2C 是支持多从机的,也就是一个 I2C 控制器下可以挂多个 I2C 从设备,这些不同的 I2C 从设备有不同的器件地址,这样 I2C 主控制器就可以通过 I2C 设备的器件地址访问指定的 I2C 设备,SDA 和 SCL 这两根线必须要接一个上拉电阻,一般是 4.7K。
在这里插入图片描述

I2C基本时序

  • 起始位
    I2C 通信起始标志,通过这个起始位就可以告诉 I2C 从机,“我”要开始 进行 I2C 通信了。在 SCL 为高电平的时候,SDA 出现下降沿就表示为起始位。
    在这里插入图片描述

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

  • 数据传输
    I2C 总线在数据传输的时候要保证在 SCL 高电平期间,SDA 上的数据稳定,因此 SDA 上 的数据变化只能在 SCL 低电平期间发生。
    在这里插入图片描述

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

I2C读写时序

  • I2C 写时序
    在这里插入图片描述注意带*号的START大多数情况下不需要
    1)、开始信号。
    2)、发送 I2C 设备地址,每个 I2C 器件都有一个设备地址,通过发送具体的设备地址来决 定访问哪个 I2C 器件。这是一个 8 位的数据,其中高 7 位是设备地址,最后 1 位是读写位,为 1 的话表示这是一个读操作,为 0 的话表示这是一个写操作。
    3)、 I2C 器件地址后面跟着一个读写位,为 0 表示写操作,为 1 表示读操作。
    4)、从机发送的 ACK 应答信号。
    5)、重新发送开始信号。 *
    6)、发送要写写入数据的寄存器地址。
    7)、从机发送的 ACK 应答信号。
    8)、发送要写入寄存器的数据。
    9)、从机发送的 ACK 应答信号。
    10)、停止信号。
    如果需要写多个数据的话,直接在从机产生应答信号之后继续发送第2,3,4…数据即可,最后不需要发送数据时用stop来结束,具体能连续写入多少数据需要看对应的I2C器件的芯片手册,如果最多能写16个数据,但是发送了17个数据,第17个数据就会覆盖第一个数据

  • I2C 读时序
    I2C 单字节读时序比写时序要复杂一点,读时序分为 4 大步,第一步是发送设备地址,第 二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是 I2C 从器件输出要取的寄存器值。
    在这里插入图片描述
    注意带*号的START大多数情况下不需要
    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 通信。
    如果需要读多个数据的话,主机在数据接收后发送一个应答信号,而不是非应答信号,因为发生应答信号后从机会继续发送第2,3,4…数据,直到接收到对应的数量的数据,就可以发送非应答信号停止告诉从机不用再发数据了,最后发送停止信号即可,便于下一次的传输。

Linux I2C驱动框架简介

Linux 内核也将 I2C 驱动分为两部分:
①、I2C 总线驱动,I2C 总线驱动就是 SOC 的 I2C 控制器驱动,也叫做 I2C 适配器驱动。
②、I2C 设备驱动,I2C 设备驱动就是针对具体的 I2C 设备而编写的驱动。
I2C总线核心(I2C Core)
【系统厂编写】I2C总线核心是Linux内核中的一个核心模块,主要负责管理所有注册到系统的I2C总线适配器和设备提供与设备通信的API函数。它提供了一些基本的操作函数,如启动总线、停止总线、发送起始信号、发送停止信号等。
I2C适配器驱动(I2C Adapter Driver):
【芯片厂提供】I2C适配器驱动负责与硬件的I2C控制器进行交互,完成硬件层面的初始化、配置和操作。它将底层硬件的特定接口与I2C总线核心驱动进行连接,使得核心驱动能够通过适配器驱动来访问硬件。
I2C设备驱动(I2C Device Driver):
【开发者编写】I2C设备驱动是针对特定类型的I2C设备编写的驱动程序。它包含了对具体设备的操作和控制逻辑,通过调用I2C总线核心驱动提供的API函数与设备进行通信。设备驱动的主要任务包括初始化设备、读写数据、配置设备参数等。
在这里插入图片描述

I2C适配器驱动

I2C 适配器驱动就是 SOC 的 I2C 控制器驱动,一般都是 SOC 厂商去编写的,比如 NXP 就编写好了 I.MX6U 的 I2C 适配器驱动。
在 imx6ull.dtsi 文件中找到 I.MX6U 的 I2C1 控制器节点,节点内容如下所示:

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";
}

一般情况下根据 i2c1 节点的 compatible 即可找到对应的驱动文件。I.MX6U 的 I2C 适配器驱动驱动文件为 drivers/i2c/busses/i2c-imx.c。

static struct platform_driver i2c_imx_driver = {
.probe = i2c_imx_probe,
.remove = i2c_imx_remove,
.driver= {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
.of_match_table = i2c_imx_dt_ids,
.pm = IMX_I2C_PM,
},
.id_table= imx_i2c_devtype,
};

在该文件中,I2C适配器驱动通过platform架构实现,platform_driver_register函数开始执行,设备和驱动就会进行匹配,成功以后 i2c_imx_probe 函数就会执行,i2c_imx_probe 函数就会完成 I2C 适配器初始化工作。具体的初始化内容可以参考对应的.c文件,不同的芯片对应不同的.c文件,因为这个是芯片厂商写的。

I2C设备驱动

I2C 适配器驱动 SOC 厂商已经替我们编写好了,我们需要做的就是编写具体的设备驱动
设备驱动可以用设备树描述,也可以不用设备树
1、不用设备树的话需要定义好i2c_board_info结构体,其中type和addr是必须要设置的
2、用设备树的话在设备树下面创建对应的节点即可,可以参考Document中设备树的绑定文档,如下所示为设备树设置方式。

&i2c2 {
clock_frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c2>;
status = "okay";

ov5640: ov5640@3c {
compatible = "ovti,ov5640";
reg = <0x3c>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_csi1>;
clocks = <&clks IMX6UL_CLK_CSI>;
clock-names = "csi_mclk";
pwn-gpios = <&gpio_spi 6 1>;
rst-gpios = <&gpio_spi 5 0>;
csi_id = <0>;
mclk = <24000000>;
mclk_source = <0>;
status = "okay";
port {
ov5640_ep: endpoint {
remote-endpoint = <&csi1_ep>;
};
};
};
};

I2C设备驱动通过一个类似platform的框架实现,由i2c_core内核实现。

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,
};

通过i2c_add_driver函数即可实现设备与驱动的匹配,进而执行.probe函数。一般需要在 probe 函数里面初始化 I2C 设备,要初始化 I2C 设备就 必须能够对 I2C 设备寄存器进行读写操作,这里就要用到 i2c_transfer 函数了,i2c_transfer 函数 最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数。
i2c_transfer 函数原型如下:

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)

函数参数和返回值含义如下:
adap:所使用的 I2C 适配器,i2c_client 会保存其对应的 i2c_adapter。
msgs:I2C 要发送的一个或多个消息。
num:消息数量,也就是 msgs 的数量。
返回值:负值,失败,其他非负值,发送的 msgs 数量。

Linux 内核使用 i2c_msg 结构体来描述一个消息。i2c_msg 结构体定义 在 include/uapi/linux/i2c.h 文件中,结构体内容如下:

struct i2c_msg {
__u16 addr;/* slave address*/
__u16 flags;
#define I2C_M_TEN0x0010/* this is a ten bit chip address */
#define I2C_M_RD0x0001/* read data, from slave to master */
#define I2C_M_STOP0x8000/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART0x4000/* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR0x2000/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK0x1000/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK0x0800/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN0x0400/* length will be first received byte */
__u16 len;/* msg length*/
__u8 *buf;/* pointer to msg data*/
};

下面这个是对发送函数的封装:

static int ap3216c_read_regs(struct ap3216c_dev *dev, unsigned char reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;
    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].buf = &reg;
    msg[0].len = 1;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].buf = val;
    msg[1].len = len;

    ret = i2c_transfer(client->adapter,msg,2);

    if(ret == 2){
        return 0;
    }else{
        printk("i2c read error\r\n");
        return -EREMOTEIO;
    }
}

下面这个是对接收函数的封装:

static int 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);
}

参考文献

  1. 个人专栏系列文章
  2. 正点原子嵌入式驱动开发指南
  3. Linux驱动:I2C驱动看这一篇就够了
  4. 对代码有兴趣的同学可以查看链接https://github.com/NUAATRY/imx6ull_dev

原文地址:https://blog.csdn.net/weixin_63699001/article/details/144231156

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!