I2C 总线
I2C 的英文拼写是“Inter — Integrate Circuit”,即内置集成电路。I2C 是一种由 Philips 公司开发的两线式串行总线,用于连接微控制器及其外围设备。I2C 总线只有两根线分别为:时钟线 SCL(Serial Clock)和数据线 SDA(Serial Data)。总线空闲时,上拉电阻使 SDA 和 SCL 线都保持高电平。I2C 总线上任意器件输出低电平都会使相应总线上的信号变低。
I2C 总线简单而实用,占用的 PCB(印刷电路板)空间很小,芯片引脚数量少,设计成本低。I2C 总线支持多主控(Multi-Mastering)模式,任何能够进行发送和接收的设备都可以成为主设备。主控能够控制数据的传输和时钟频率,在任意时刻只能有一个主控。
I2C 总线原理
I2C 设备上的串行数据线 SDA 接口是双向的,用于向总线上发送或接收数据。串行时钟线 SCL 也是双向的,作为控制总线数据传输的主机通过 SCL 接口发送时钟信号提供给从设备;作为接受主机命令的从设备按照 SCL 上的信号发送或接收 SDA 上的信号。
I2C 总线在传输数据的过程中,主要有三种控制信号:起始信号,结束信号,应答信号。
起始信号:当 SCL 为高电平时,SDA 由高电平转为低电平时,开始传输数据。
结束信号:当 SCL 为高电平时,SDA 由低电平转为高电平时,结束数据传输。
应答信号:接收数据的器件在接收到 8bit 数据后,向发送数据的器件反馈一个应答信号,表示已经收到数据。应答信号为低电平时,规定为有效应答位(ACK 简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位 ACK 的要求是,接收器在第 9 个时钟脉冲之前的低电平期间将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。接收器如果是主控器,则在它收到最后一个字节后,发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放 SDA 线,以便主控接收器发送一个停止信号。
开始位和停止位都由 I2C 主机产生。在选择从设备时,如果从设备采用 7 位地址,则主设备在发起传输过程前,需先发送 1 字节的设备地址信息,前 7 位为设备地址,最后 1 位为读写标志。之后,I2C 每次传输的数据也是 1 字节,并从 MSB 开始传输(最高有效位)。每个字节传完后,在 SCL 的第 9 个上升沿到来之前,接收方应该发出一个 ACK 位。在 SCL 的时钟脉冲由 I2C 主控方发出,在第 8 个时钟周期之后,主控方应该释放 SDA。I2C 总线的时序如下图所示。
SylixOS I2C 总线框架分析
SylixOS 的 I2C 体系结构分为 3 个组成部分。
- I2C 核心驱动程序
I2C 核心提供 I2C 总线驱动和设备驱动的注册、注销方法,I2C 通信方法(即 Algorithm),具体适配器无关的代码以及探测设备、检测设备地址等。I2C 核心驱 动程序可管理多个 I2C 总线适配器(控制器)和多个 I2C 从设备。每个 I2C 从设备驱动都能找到和它相连的 I2C 总线适配器。
- I2C 总线驱动
I2C 总线驱动主要包括 I2C 适配器结构 lw_i2c_adapter 和 I2C 适配器的 Algorithm 数据结构。
开发者可以通过 I2C 总线驱动的代码,控制 I2C 适配器以主控方式产生开始位、停止位、读写周期,或者以从设备方式被读写、产生 ACK。
- I2C 设备驱动
I2C 设备驱动是对 I2C 设备端的实现,设备一般挂接在受 CPU 控制的 I2C 适配器上,通过 I2C 适配器与 CPU 交换数据。每一条 I2C 总线对应一个 Adapter。在 内核中,每一个 Adapter 提供了一个描述的结构,也定义了 Adapter 支持的操作。再通过 I2C 核心层将 I2C 设备与 I2C 适配器关联起来。
I2C 总线适配器相关信息位于“libsylixos/SylixOS/system/device/i2c”下,其适配器创建函数原型如下:
#include <SylixOS.h>
INT API_I2cAdapterCreate (CPCHAR pcName,
PLW_I2C_FUNCS pi2cfunc,
ULONG ulTimeout,
INT iRetry)
函数 API_I2cAdapterCreate 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pcName 是 I2C 适配器的名称,即 shell 命令 buss 显示的名称。
- 参数 pi2cfunc 是 I2C 总线传输函数的指针。
- 参数 ulTimeout 是操作超时时间。
- 参数 iRetry 是传输出错时的重试次数。
函数 API_I2cAdapterCreate 使用结构体 PLW_I2C_FUNCS 来向内核提供传输函数集合,其详细描述如下:
#include <SylixOS.h>
typedef struct lw_i2c_funcs {
INT (*I2CFUNC_pfuncMasterXfer)(PLW_I2C_ADAPTER pi2cadapter,
PLW_I2C_MESSAGE pi2cmsg,
INT iNum);
INT (*I2CFUNC_pfuncMasterCtl)(PLW_I2C_ADAPTER pi2cadapter,
INT iCmd,
LONG lArg);
} LW_I2C_FUNCS;
typedef LW_I2C_FUNCS *PLW_I2C_FUNCS;
- I2CFUNC_pfuncMasterXfer :I2C 传输函数,I2C 设备会直接调用此函数实现消息发送。第一个参数 pi2cadapter 为 I2C 总线适配器指针,第二个参数 pi2cmsg 为 I2C 设备需要传输的消息结构体首地址指针,第三个参数 iNum 为需要传输的消息个数,通过以上三个参数即可知道 I2C 设备如何调用此函数实现消息传输。
- I2CFUNC_pfuncMasterCtl :I2C 适配器控制函数,用来实现与硬件控制器相关的控制。第一个参数 pi2cadapter 为 I2C 总线适配器指针,第二个参数 iCmd 为控制命令,第三个参数 lArg 与 iCmd 相关。
注意:
I2CFUNC_pfuncMasterCtl 函数是按照开发人员的需求实现,通常情况下不实现。
注册到内核的传输函数集合中要用到多种结构体, PLW_ I2C _ADAPTER 总线适配器结构体指针主要包含当前总线适配器节点信息, PLW_ I2C _DEVICE 总线设备结构体指针主要包含当前 I2C 设备的相关信息, PLW_ I2C _MESSAGE 消息请求结构体指针作用是指向需要发送的消息缓冲区,提供以上三种结构体后控制器即可知道如何进行发送,各种结构体的详细描述如下:
首先介绍 I2C 总线适配器结构体,该结构体详细描述如下:
#include <SylixOS.h>
typedef struct lw_i2c_adapter {
LW_BUS_ADAPTER I2CADAPTER_pbusadapter; /* 总线节点 */
struct lw_i2c_funcs *I2CADAPTER_pi2cfunc; /* 总线适配器操作函数 */
LW_OBJECT_HANDLE I2CADAPTER_hBusLock; /* 总线操作锁 */
ULONG I2CADAPTER_ulTimeout; /* 操作超时时间 */
INT I2CADAPTER_iRetry; /* 重试次数 */
LW_LIST_LINE_HEADER I2CADAPTER_plineDevHeader; /* 设备链表 */
} LW_I2C_ADAPTER;
typedef LW_I2C_ADAPTER *PLW_I2C_ADAPTER;
- I2CADAPTER_pbusadapter :系统总线节点结构体。
- I2CADAPTER_pi2cfunc :指向总线适配器的操作函数,即 API_I2cAdapterCreate 函数注册到核心层的操作函数集指针。
- I2CADAPTER_hBusLock :I2C 总线锁,不需要手动处理。
- I2CADAPTER_ulTimeout :操作超时时间。
- I2CADAPTER_iRetry :传输出错时的重试次数。
- I2CADAPTER_plineDevHeader :指向此适配器下挂载的设备链表,不需要手动处理。
I2C 设备结构体的详细描述如下:
#include <SylixOS.h>
typedef struct lw_i2c_device {
UINT16 I2CDEV_usAddr; /* 设备地址 */
UINT16 I2CDEV_usFlag; /* 标志, 仅支持 10bit 地址选项 */
#define LW_I2C_CLIENT_TEN 0x10 /* 与 LW_I2C_M_TEN 相同 */
PLW_I2C_ADAPTER I2CDEV_pi2cadapter; /* 挂载的适配器 */
LW_LIST_LINE I2CDEV_lineManage; /* 设备挂载链 */
atomic_t I2CDEV_atomicUsageCnt; /* 设备使用计数 */
CHAR I2CDEV_cName[LW_CFG_OBJECT_NAME_SIZE]; /* 设备的名称 */
} LW_I2C_DEVICE;
typedef LW_I2C_DEVICE *PLW_I2C_DEVICE;
- I2CDEV_usAddr :设备地址。
- I2CDEV_usFlag :若从设备的地址为 10bit,则将该标志位值置为 LW_I2C_CLIENT_TEN;否则置为 0。
- I2CDEV_pi2cadapter :设备挂载的 I2C 总线适配器。
- I2CDEV_lineManage :设备挂载的链表。
- I2CDEV_atomicUsageCnt :设备使用的计数。
- I2CDEV_cName :设备名称。
I2C 消息结构体是 I2C 主机和从机通信的消息格式,该结构体的详细描述如下:
#include <SylixOS.h>
typedef struct lw_i2c_message {
UINT16 I2CMSG_usAddr; /* 器件地址 */
UINT16 I2CMSG_usFlag; /* 传输控制参数 */
UINT16 I2CMSG_usLen; /* 长度(缓冲区大小) */
UINT8 *I2CMSG_pucBuffer; /* 缓冲区 */
} LW_I2C_MESSAGE;
typedef LW_I2C_MESSAGE *PLW_I2C_MESSAGE;
- I2CMSG_usAddr :器件地址。
- I2CMSG_usFlag :传输控制参数,其取值见下表。
- I2CMSG_usLen :存放消息内容的缓存区大小。
- I2CMSG_pucBuffer :存放消息内容的缓存区。
传输控制参数的值 | 含义 |
---|---|
LW_I2C_M_TEN | 使用 10bit 设备地址 |
LW_I2C_M_RD | 为读操作,否则为写 |
LW_I2C_M_NOSTART | 不发送 start 标志 |
LW_I2C_M_REV_DIR_ADDR | 读写标志位反转 |
LW_I2C_M_IGNORE_NAK | 忽略 ACK NACK |
LW_I2C_M_NO_RD_ACK | 读操作时不发送 ACK |
EEPROM 驱动实现
一个具体的 I2C 设备驱动以 lw_i2c_device 结构体的形式进行组织,用于将设备挂接于 I2C 总线,组织好了后,再完成设备本身所属类型的驱动。下面以 EEPROM 为例介绍 I2C 设备驱动的实现。
如下所示,当用户在上层调用 open 函数打开 EEPROM 的设备文件时,会调用到此函数。该函数的主要作用是调用 API_I2cDeviceCreate 函数,在指定的 I2C 适配器上创建一个 I2C 设备。函数 API_I2cDeviceCreate 原型如下:
#include <SylixOS.h>
PLW_I2C_DEVICE API_I2cDeviceCreate (CPCHAR pcAdapterName,
CPCHAR pcDeviceName,
UINT16 usAddr,
UINT16 usFlag)
函数 API_I2cDeviceCreate 原型分析:
- 此函数成功返回 pi2cdevice (I2C 设备结构体类型),失败返回 LW_NULL 。
- 参数 pcAdapterName 是设备挂载的适配器名称。
- 参数 pcDeviceName 是设备名称。
- 参数 usAddr 是设备地址。
- 参数 usFlag 是设备标志。
#include <SylixOS.h>
LONG eepromOpen (EEPROM *peeprom, PCHAR pcName, INT iFlags, INT iMode)
{
if (!peeprom) {
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
if (LW_DEV_INC_USE_COUNT(&peeprom->EEP_devhdr) == 1) {
peeprom->EEP_i2cdev = API_I2cDeviceCreate(EEPROM_I2C_NAME, pcName, EEPROM_ADDR, 0);
if (peeprom->EEP_i2cdev == LW_NULL) {
return (PX_ERROR);
}
}
return ((LONG)peeprom);
}
如下所示,当用户在上层调用 close 函数关闭 EEPROM 的设备文件时,会调用到此函数。该函数的主要作用是调用 API_I2cDeviceDelete 函数,删除指定的 I2C 设备。函数 API_I2cDeviceDelete 原型如下:
#include <SylixOS.h>
INT API_I2cDeviceDelete (PLW_I2C_DEVICE pi2cdevice)
函数 API_I2cDeviceCreate 原型分析:
- 此函数成功返回 ERROR_NONE ,失败返回 PX_ERROR 。
- 参数 pi2cdevice 是指定的 I2C 设备结构体。
#include <SylixOS.h>
INT eepromClose (EEPROM *peeprom)
{
if (!peeprom) {
_ErrorHandle(EINVAL);
return (PX_ERROR);
}
if (LW_DEV_DEC_USE_COUNT(&peeprom->EEP_devhdr) == 0) {
API_I2cDeviceDelete(peeprom->EEP_i2cdev);
}
return (ERROR_NONE);
}
EEPROM 的传输函数的实现,可以分为两个部分。第一部分,将需要发送的数据封装成 I2C 消息结构体类型;第二部分,调用 I2C 的传输函数 API_I2cDeviceTransfer,将数据传输出去。具体实现如下所示。函数 API_I2cDeviceTransfer 原型如下:
#include <SylixOS.h>
INT API_I2cDeviceTransfer (PLW_I2C_DEVICE pi2cdevice,
PLW_I2C_MESSAGE pi2cmsg,
INT iNum)
函数 API_I2cDeviceTransfer 原型分析:
- 此函数成功返回 iRet (完成传输的消息数量),失败返回 PX_ERROR 。
- 参数 pi2cdevice 是指定的 I2C 设备结构体。
- 参数 pi2cmsg 是传输消息结构体组。
- 参数 iNum 是传输消息结构体中消息的数量。
#include <SylixOS.h>
INT eepromByteRead (PLW_I2C_DEVICE pi2c, UINT8 ucByteAddr, UINT8 *pucData)
{
INT iRet;
if (!pi2c) {
return (PX_ERROR);
}
LW_I2C_MESSAGE msgs[] = {
{
.I2CMSG_usAddr = pi2c->I2CDEV_usAddr,
.I2CMSG_usFlag = 0,
.I2CMSG_usLen = 1,
.I2CMSG_pucBuffer = &ucByteAddr,
}, {
.I2CMSG_usAddr = pi2c->I2CDEV_usAddr,
.I2CMSG_usFlag = LW_I2C_M_RD,
.I2CMSG_usLen = 1,
.I2CMSG_pucBuffer = pucData,
}
};
iRet = API_I2cDeviceTransfer(pi2c, msgs, 2);
if (iRet < 0) {
EEPROM_DBG("I2c msg read error: %d\n", iRet);
return (PX_ERROR);
}
return (iRet);
}