文章目录
一、IIC协议简介
1.1 IIC总线简介
I2C(Inter Integrated Circuit)总线是 PHILIPS 公司开发的一种半双工、双向二线制同步串行总线。I2C 总线传输数据时只需两根信号线,一根是双向数据线 SDA(serial data),另一根是双向时钟线 SCL(serial clock)。SPI 总线有两根线分别用于主从设备之间接收数据和发送数据,而 I2C 总线只使用一根线进行数据收发。
I2C 和 SPI 一样以主从的方式工作,不同于 SPI 一主多从的结构,它允许同时有多个主设备存在,每个连接到总线上的器件都有唯一的地址,主设备启动数据传输并产生时钟信号,从设备被主设备寻址(SPI通过CS片选引脚选择目标从设备,IIC通过发送从设备地址以寻址方式选择目标从设备),同一时刻只允许有一个主设备。如下图所示:
SDA 线上的数据必须在时钟的高电平周期保持稳定,数据线的高或低电平状态只有在 SCL 线的时钟信号是低电平时才能改变。换言之,SCL为高电平时表示有效数据,SDA为高电平表示“1”,低电平表示“0”;SCL为低电平时表示无效数据,此时SDA会进行电平切换,为下次数据表示做准备。下图为数据有效性的时序图:
I2C 总线主要的数据传输格式如下图所示:
当总线空闲时,SDA 和 SCL 都处于高电平状态,当主机要和某个从机通讯时,会先发送一个开始条件,然后发送从机地址和读写控制位,接下来传输数据(主机发送或者接收数据),数据传输结束时主机会发送停止条件。传输的每个字节为8位,高位在前,低位在后。数据传输过程中的不同名词详解如下所示:
- 开始条件: SCL 为高电平时,主机将 SDA 拉低,表示数据传输即将开始,总线在开始条件后处于busy的状态,在停止条件的某段时间后,总线才再次处于空闲状态,下图为开始和停止条件的信号产生时序图:
- 从机地址: 主机发送的第一个字节为从机地址,高 7 位为地址,最低位为 R/W 读写控制位,1 表示读操作,0 表示写操作。一般从机地址有 7 位地址模式和 10 位地址模式两种,如果是 10 位地址模式,第一个字节的头 7 位 是 11110XX 的组合,其中最后两位(XX)是 10 位地址的两个最高位,第二个字节为 10 位从机地址的剩下8位,如下图所示:
- 应答信号: 每传输完成一个字节的数据,接收方就需要回复一个 ACK(acknowledge)。写数据时由从机发送ACK,读数据时由主机发送 ACK。数据接收方收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权,将SDA电平拉高,由接收方控制。若希望继续,则给出“应答(ACK)”信号,即SDA为低电平;反之给出“非应答(NACK)”信号,即SDA为高电平,应答信号产生的时序图如下:
- 数据: 从机地址发送完后可能会发送一些指令,依从机而定,然后开始传输数据,由主机或者从机发送,每个数据为 8 位,数据的字节数没有限制;
- 重复开始条件: 在一次通信过程中,主机可能需要和不同的从机传输数据或者需要切换读写操作时,主机可以再发送一个开始条件;
- 停止条件: 在 SDA 为低电平时,主机将 SCL 拉高并保持高电平,然后在将 SDA 拉高,表示传输结束。
1.2 硬件IIC与软件模拟IIC
在正点原子的教程中说,STM32的硬件IIC设计比较复杂,而且稳定性不佳(貌似是ST为了规避飞利浦IIC的版权问题),所以在CPU资源不紧张的情况下,很多人一般会选择GPIO模拟I2C。硬件IIC与软件模拟IIC有何区别呢?
- 硬件IIC:跟之前介绍的SPI外设与USART外设类似,物理层有专门的电路支持,IIC引脚自然也是专用的,借助芯片厂商提供的固件库函数实现对IIC外设寄存器的访问,工作效率较高;
- 软件模拟IIC:物理层借助GPIO外设,并不使用固件库的IIC函数访问IIC寄存器,可以根据需要配置模拟IIC通信的GPIO引脚,协议层需要自己实现,而且软件模拟IIC工作效率比硬件IIC低不少。
STM32平台由于软件模拟IIC比较常用,且方便移植,RT-Thread的IIC设备驱动也是使用的软件模拟方式实现的,所以下面就不介绍硬件IIC的功能框图、IIC固件库等内容了(自然也不需要CubeMX配置IIC外设了),下面开始介绍RT-Thread IIC驱动框架的实现。
二、IIC设备对象管理
介绍IIC设备对象管理前,再展示下RT-Thread I / O设备模型框架,按照模型框架一层层解析:
2.1 IIC设备驱动框架层
- IIC总线控制块
先看IIC总线在驱动框架层是如何描述的:
// rt-thread-4.0.1\components\drivers\include\drivers\i2c.h
/*for i2c bus driver*/
struct rt_i2c_bus_device
{
struct rt_device parent;
const struct rt_i2c_bus_device_ops *ops;
rt_uint16_t flags;
rt_uint16_t addr;
struct rt_mutex lock;
rt_uint32_t timeout;
rt_uint32_t retries;
void *priv;
};
struct rt_i2c_bus_device_ops
{
rt_size_t (*master_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_size_t (*slave_xfer)(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num);
rt_err_t (*i2c_bus_control)(struct rt_i2c_bus_device *bus,
rt_uint32_t,
rt_uint32_t);
};
struct rt_i2c_msg
{
rt_uint16_t addr;
rt_uint16_t flags;
rt_uint16_t len;
rt_uint8_t *buf;
};
// flags
#define RT_I2C_WR 0x0000
#define RT_I2C_RD (1u << 0)
#define RT_I2C_ADDR_10BIT (1u << 2) /* this is a ten bit chip address */
#define RT_I2C_NO_START (1u << 4)
#define RT_I2C_IGNORE_NACK (1u << 5)
#define RT_I2C_NO_READ_ACK (1u << 6) /* when I2C reading, we do not ACK */
IIC总线访问IIC设备的操作函数主要有master_xfer、slave_xfer与i2c_bus_control三种,这几个访问函数由下面的IIC设备驱动层实现,这也是IIC协议软件模拟实现的重点。
IIC总线传输数据按照IIC协议数据帧格式,封装成结构体rt_i2c_msg,该结构体成员包含了IIC协议数据帧中从机地址、各标志位、传输数据的起始地址及长度等内容。
- IIC总线接口函数
I/O设备管理层要想访问某设备,需要在下面的设备驱动层创建设备实例,并将该设备注册到I/O设备管理层,下面先看看IIC总线的创建与注册过程:
// rt-thread-4.0.1\components\drivers\i2c\i2c_core.c
rt_err_t rt_i2c_bus_device_register(struct rt_i2c_bus_device *bus,
const char *bus_name)
{
rt_err_t res = RT_EOK;
rt_mutex_init(&bus->lock, "i2c_bus_lock", RT_IPC_FLAG_FIFO);
if (bus->timeout == 0) bus->timeout = RT_TICK_PER_SECOND;
res = rt_i2c_bus_device_device_init(bus, bus_name);
i2c_dbg("I2C bus [%s] registered\n", bus_name);
return res;
}
// rt-thread-4.0.1\components\drivers\i2c\i2c_dev.c
rt_err_t rt_i2c_bus_device_device_init(struct rt_i2c_bus_device *bus,
const char *name)
{
struct rt_device *device;
RT_ASSERT(bus != RT_NULL);
device = &bus->parent;
device->user_data = bus;
/* set device type */
device->type = RT_Device_Class_I2CBUS;
/* initialize device interface */
#ifdef RT_USING_DEVICE_OPS
device->ops = &i2c_ops;
#else
device->init = RT_NULL;
device->open = RT_NULL;
device->close = RT_NULL;
device->read = i2c_bus_device_read;
device->write = i2c_bus_device_write;
device->control = i2c_bus_device_control;
#endif
/* register to device manager */
rt_device_register(device, name, RT_DEVICE_FLAG_RDWR);
return RT_EOK;
}
#ifdef RT_USING_DEVICE_OPS
const static struct rt_device_ops i2c_ops =
{
RT_NULL,
RT_NULL,
RT_NULL,
i2c_bus_device_read,
i2c_bus_device_write,
i2c_bus_device_control
};
#endif
static rt_size_t i2c_bus_device_read(rt_device_t dev,
rt_off_t pos,
void *buffer,
rt_size_t count)
{
......
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
......
return rt_i2c_master_recv(bus, addr, flags, buffer, count);
}
static rt_size_t i2c_bus_device_write(rt_device_t dev,
rt_off_t pos,
const void *buffer,
rt_size_t count)
{
......
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
......
return rt_i2c_master_send(bus, addr, flags, buffer, count);
}
static rt_err_t i2c_bus_device_control(rt_device_t dev,
int cmd,
void *args)
{
rt_err_t ret;
struct rt_i2c_priv_data *priv_data;
struct rt_i2c_bus_device *bus = (struct rt_i2c_bus_device *)dev->user_data;
RT_ASSERT(bus != RT_NULL);
switch (cmd)
{
/* set 10-bit addr mode */
case RT_I2C_DEV_CTRL_10BIT:
bus->flags |= RT_I2C_ADDR_10BIT;
break;
case RT_I2C_DEV_CTRL_ADDR:
bus->addr = *(rt_uint16_t *)args;
break;
case RT_I2C_DEV_CTRL_TIMEOUT:
bus->timeout = *(rt_uint32_t *)args;
break;
case RT_I2C_DEV_CTRL_RW:
priv_data = (struct rt_i2c_priv_data *)args;
ret = rt_i2c_transfer(bus, priv_data->msgs, priv_data->number);
if (ret < 0)
return -RT_EIO;
break;
default:
break;
}
return RT_EOK;
}
// rt-thread-4.0.1\components\drivers\include\drivers\i2c_dev.h
struct rt_i2c_priv_data
{
struct rt_i2c_msg *msgs;
rt_size_t number;
};
IIC驱动框架层向上层注册的操作函数集合i2c_ops最终通过调用rt_i2c_master_recv、rt_i2c_master_send与rt_i2c_transfer三个函数实现,IIC设备驱动层将这三个函数开放给用户了,用户除通过I / O设备统一访问接口访问IIC外,还可通过这三个函数直接访问IIC设备,这三个函数的实现代码如下:
// rt-thread-4.0.1\components\drivers\i2c\i2c_core.c
rt_size_t rt_i2c_transfer(struct rt_i2c_bus_device *bus,
struct rt_i2c_msg msgs[],
rt_uint32_t num)
{
rt_size_t ret;
if (bus->ops->master_xfer)
{
rt_mutex_take(&bus->lock, RT_WAITING_FOREVER);
ret = bus->ops->master_xfer(bus, msgs, num);
rt_mutex_release(&bus->lock);
return ret;
}
else
return 0;
}
rt_size_t rt_i2c_master_send(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
const rt_uint8_t *buf,
rt_uint32_t count)
{
rt_err_t ret;
struct rt_i2c_msg msg;
msg.addr = addr;
msg.flags = flags & RT_I2C_ADDR_10BIT;
msg.len = count;
msg.buf = (rt_uint8_t *)buf;
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
rt_size_t rt_i2c_master_recv(struct rt_i2c_bus_device *bus,
rt_uint16_t addr,
rt_uint16_t flags,
rt_uint8_t *buf,
rt_uint32_t count)
{
rt_err_t ret;
struct rt_i2c_msg msg;
RT_ASSERT(bus != RT_NULL);
msg.addr = addr;
msg.flags = flags & RT_I2C_ADDR_10BIT;
msg.flags |= RT_I2C_RD;
msg.len = count;
msg.buf = buf;
ret = rt_i2c_transfer(bus, &msg, 1);
return (ret > 0) ? count : ret;
}
从上面的实现代码可以看出,rt_i2c_master_send与rt_i2c_master_recv最终是通过调用rt_i2c_transfer函数实现的,前两个函数对rt_i2c_transfer函数进行了再封装,不需要用户构造rt_i2c_msg结构体,调用更方便。
函数rt_i2c_transfer的实现最终是通过调用bus->ops->master_xfer函数来实现的,在前面介绍IIC总线设备控制块时介绍过,IIC总线操作函数集合rt_i2c_bus_device_ops需要IIC设备驱动层实现。
- 软件模拟IIC协议实现
RT-Thread IIC设备采用软件模拟方式实现,通过GPIO设备模拟IIC通信协议实际上是通过控制GPIO引脚电平的置位操作实现的,STM32描述模拟IIC设备的数据结构如下:
// libraries\HAL_Drivers\drv_soft_i2c.h
/* stm32 i2c dirver class */
struct stm32_i2c
{
struct rt_i2c_bit_ops ops;
struct rt_i2c_bus_device i2c2_bus;
};
/* stm32 config class */
struct stm32_soft_i2c_config
{
rt_uint8_t scl;
rt_uint8_t sda;
const char *bus_name;
};
// rt-thread-4.0.1\components\drivers\include\drivers\i2c-bit-ops.h
struct rt_i2c_bit_ops
{
void *data; /* private data for lowlevel routines */
void (*set_sda)(void *data, rt_int32_t state);
void (*set_scl)(void *data, rt_int32_t state);
rt_int32_t (*get_sda)(void *data);
rt_int32_t (*get_scl)(void *data);
void (*udelay)(rt_uint32_t us);
rt_uint32_t delay_us; /* scl and sda line delay */
rt_uint32_t timeout; /* in tick */
};
结构体stm32_i2c相当于STM32 I2C设备驱动类,包含前面介绍的IIC总线设备rt_i2c_b