嵌入式驱动开发详解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 = ®
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);
}
参考文献
- 个人专栏系列文章
- 正点原子嵌入式驱动开发指南
- Linux驱动:I2C驱动看这一篇就够了
- 对代码有兴趣的同学可以查看链接https://github.com/NUAATRY/imx6ull_dev
原文地址:https://blog.csdn.net/weixin_63699001/article/details/144231156
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!