目录
一、I2C 初始化
二、产生开始 / 停止信号
1、开始信号
2、重复开始信号
3、停止信号
三、向总线上发送数据(阻塞模式)
四、从总线上读取数据(阻塞模式)
五、整合:数据读写统一调用接口
一、I2C 初始化
初始化步骤如下:
- 时钟源配置。PERCLK_CLK_ROOT 的时钟源是 66MHz
- 关闭 I2C。在配置 I2C 相关寄存器时,我们需要先将 I2C 关闭
- 分频。分频的目的是控制 I2C 的传输速率(如果要控制 I2C 的速率为 100K,可以640 分频)
- 开启 I2C。
/* i2c 初始化 */
void i2c_init()
{
// 时钟源的初始化放到了时钟初始化(CCM_init)
// 关闭I2C
I2C1_I2CR &= ~(1 << 7);
// 设置分频值(分频值为 640)
I2C1_IFDR = 0x15;
// 打开I2C
I2C1_I2CR |= (1 << 7);
}
二、产生开始 / 停止信号
1、开始信号
当总线空闲时,总线上的设备可以发送开始信号来占用总线。如果存在多个设备同时占用总线,此时就会发生冲裁。
- 判断总线是否空闲
- 设为主机模式,设为发送模式
- 发送要通信的设备地址和通信方向
/* 产生开始信号 */
i2c_status_t i2c_master_start(unsigned char address, i2c_direction direction)
{
/* 总线是否被占用 */
if (is_bus_busy())
{
return Status_I2C_Busy;
}
/*
* 发送开始信号
* bit 5: 1 主机
* bit 4: 1 发送
*/
I2C1_I2CR |= ((1 << 4) | (1 << 5));
/* 发送通信地址和通信方向 */
I2C1_I2DR = (((unsigned int)address << 1) | (direction == I2C_Read ? 1 : 0));
return Status_I2C_Success;
}
2、重复开始信号
当某个设备在通信时,可能会需要临时改变通信方向,此时就可以发送一个重复开始信号来改变通信方向。发送重复开始信号要求总线空闲,且当前设备是主机,只要有一者不满足,就会发送失败
- 判断主机是否空闲,且当前设备是否为主机模式
- 发送一个重复开始信号
- 发送通信地址和通信方向
/* 重复产生开始信号(可能是修改通信方向、通信设备的地址) */
i2c_status_t i2c_master_repeated_start(unsigned char address, i2c_direction direction)
{
/*
* 总线是否被占用
* - 如果是其他设备占用总线,直接返回
* - 如果是当前设备占用总线,发送一个重复的开始信号
*/
if (is_bus_busy() && is_master() == 0)
{
return Status_I2C_Busy;
}
/* 产生一个重复开始信号 */
I2C1_I2CR |= ((1 << 2) | (1 << 4));
/* 发送通信地址和通信方向 */
I2C1_I2DR = (((unsigned int)address << 1U) | (direction & 0x01));
return Status_I2C_Success;
}
3、停止信号
当主机打算继续通信时,可以发送一个停止信号来结束通信;如果从机不想继续通信,可以选择不回应主机表示想结束通信。
- 产生一个停止信号
- 等待总线被释放
/* 产生停止信号 */
i2c_status_t i2c_master_stop()
{
unsigned short timeout = UINT16_MAX;
/*
* 产生一个停止信号
* bit 3: 0 产生一个ACK
* bit 4: 0 接收(要产生一个ACK,当前设备必须为接收模式)
* bit 5: 0 从机
*/
I2C1_I2CR &= ~((1 << 3) | (1 << 4) | (1 << 5));
/* 等待总线被释放 */
while (is_bus_busy() && (timeout--));
// 等待超时
if (timeout == 0)
{
return Status_I2C_Timeout;
}
return Status_I2C_Success;
}
三、向总线上发送数据(阻塞模式)
只有当上一次的数据全部发送完,才能开始下一次通信。其本质是,每次传输 8 bit(需要 8 个时钟周期),当第 9 个时钟周期的边沿触发时,我们认为数据传输完毕。
- 等待 DR 寄存器准备就绪
- 清除中断、仲裁标志位
- 状态设为发送状态
- while 循环逐字节发送
- 每发送一个字节,都要将数据保存到 DR 寄存器
- 等待传输完成(传输完成时会触发中断)
- 清除中断标志位
- 判断仲裁是否失败(可能存在多主机占用总线的情况)
- 是否收到 NACK(从机是否想继续通信)
- 如果遇到其他问题,直接跳出while循环
/*
* @description: 阻塞式发送
* @param - sendbuf: 保存要发送的数据(允许多字节)
* @param - len: 发送数据的长度
* @param - stopflag: 下一次是否还要继续发送
*/
i2c_status_t i2c_master_write_blocking(unsigned char* sendbuf, unsigned int len, unsigned int stopflag)
{
i2c_status_t i2c_stat = Status_I2C_Success;
/* 等待DR寄存器准备就绪 */
while (((I2C1_I2SR >> 7) & 0x01) == 0);
/* 清除中断标志位 */
clear_intpending_flag();
/* 状态设为发送 */
I2C1_I2CR |= (1 << 4);
/* 逐字节发送 */
while (len--)
{
// 将要发送的数据保存到寄存器
I2C1_I2DR = *(sendbuf++);
// 等待传输完成(传输完成时会触发中断)
while (((I2C1_I2SR >> 1) & 0x01) == 0);
// 清除中断标志位
clear_intpending_flag();
// 仲裁是否失败
if(((I2C1_I2SR >> 4) & 0x01))
{
i2c_stat = Status_I2C_Lost_Arbitration;
}
// 是否收到 NACK
if (((I2C1_I2SR >> 0) & 0x01))
{
i2c_stat = Status_I2C_NAK;
}
if (i2c_stat != Status_I2C_Success)
{
break;
}
}
// 从机不想收了 或者 主机不想发了
if ((i2c_stat == Status_I2C_NAK) || stopflag)
{
clear_intpending_flag();
i2c_stat = i2c_master_stop();
}
return i2c_stat;
}
/* 清除中断标志位 */
void clear_intpending_flag()
{
I2C1_I2SR &= ~(1 << 1);
}
四、从总线上读取数据(阻塞模式)
在DR寄存器准备就绪以后,要将当前状态设为接收,逐字节接收(需要考虑接收缓冲区的大小),一旦接收缓冲区大小不足以接收后续的数据,需要提前发送停止信号告诉对方,不要再继续发了。
接收缓冲区大小 = 0,表示本次接收继续,下一次接收无法进行,直接发送停止信号
接收缓冲区大小 = 1,表示下一次接收继续,但是后续接收无法继续,发送 NACK
- 等待 DR 寄存器准备就绪
- 清除中断标志位
- 状态设为接收
- 循环读取
- 等待数据传输完毕(对方将一个字节的数据全部发送到总线上时会触发中断)
- 清除标志位
- 如果接收缓冲区大小为 0,直接发送停止信号
- 如果接收缓冲区大小为 1,发送 NACK
- 读取 DR 寄存器数据
/*
* @description : 阻塞式读取
* @param - recvbuf : 读取到数据
* @param - len : 要读取的数据大小(单位:字节)
*/
i2c_status_t i2c_master_read_blocking(unsigned char* recvbuf, unsigned int len)
{
i2c_status_t result = Status_I2C_Success;
volatile unsigned char dummy = 0;
/* 使用这个变量的目的是避免编译报错 */
dummy++;
/* 等待DR寄存器准备就绪 */
while (((I2C1_I2SR >> 1) & 0x01) == 0);
/* 清除中断标志位 */
clear_intpending_flag();
/* 状态设为接收 */
I2C1_I2CR &= ~((1 << 3) | (1 << 4));
/* 后续只想再接收一个字节的数据 */
if (len == 1)
{
I2C1_I2CR |= (1 << 3);
}
/* 假读 */
dummy = I2C1_I2DR;
while (len--)
{
/* 有一个字节的数据被传输到总线上,中断触发 */
while (((I2C1_I2SR >> 1) & 0x01) == 0);
/* 清除中断标志位 */
clear_intpending_flag();
/* 下一次没有足够的空间去读取 */
if (len == 0)
{
result = i2c_master_stop();
}
// 后续只能再接收一个字节的数据,告诉对方不要再发数据了
if (len == 1)
{
I2C1_I2CR |= (1 << 3);
}
/* 从寄存器中读取 */
*(recvbuf++) = I2C1_I2DR & 0xFF;
}
return result;
}
/* 清除中断标志位 */
void clear_intpending_flag()
{
I2C1_I2SR &= ~(1 << 1);
}
五、整合:数据读写统一调用接口
实际上我们真正要调用的接口,只需要这一个即可,上述内容其实都是在为这个接口做铺垫。当数据开始传输的时候,
- 等待 DR 寄存器准备就绪
- 清除中断、仲裁标志位
- 判断是否为读时序,如果是读时序,需要临时修改通信方向(因为要先发送寄存器地址让对方知道自己要读取哪个寄存器)
- 发送开始信号(发送设备地址和通信方向)
- 等待信号发送完毕
- 发送寄存器地址(考虑到寄存器地址可能不止一个字节)
- 发送 / 读取数据
这里需要分别参考读时序和写时序的通信过程:I2C 通信时序
写时序:
读时序:
typedef struct
{
unsigned char slaveAddress; // 要通信的设备地址
i2c_direction direction; // 传输方向
unsigned char reg; // 要读写的寄存器地址
unsigned int reg_size; // 寄存器地址大小(单位: 字节, 一般是1个字节,也有可能大于1个字节)
unsigned char stopFlag; // 下一次是否要停止发送
unsigned char* buf; // 缓冲区
unsigned int buf_size; // 缓冲区大小
} i2c_transfer_t;
/* 数据传输(发送 / 接收) */
i2c_status_t i2c_master_transfer(i2c_transfer_t* xfer)
{
i2c_direction direction = xfer->direction;
/* 传输之前清除所有的标志位(仲裁、中断挂起) */
I2C1_I2SR &= ~((1 << 1) | (1 << 4));
/* DR 寄存器是否准备就绪 */
while (!((I2C1_I2SR >> 7) & 0x01));
/* 如果是读时序,需要先临时修改通信方向 */
if ((xfer->reg_size > 0) && (xfer->direction == I2C_Read))
{
direction = I2C_Write;
}
/* 1、发送开始信号 */
i2c_status_t ret = i2c_master_start(xfer->slaveAddress, direction);
if (ret != Status_I2C_Success)
{
return ret;
}
/* 等待传输完成 */
while (!((I2C1_I2SR >> 1) & 0x01));
/* 检查传输是否发生错误 */
if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
{
i2c_master_stop();
return ret;
}
/* 2、发送寄存器地址 */
if (xfer->reg_size > 0)
{
do
{
clear_intpending_flag();
xfer->reg_size --;
/* 写入地址 */
I2C1_I2DR = ((xfer->reg) >> (xfer->reg_size * 8)); // 先传输高字节
/* 等待传输完成 */
while (((I2C1_I2SR >> 1) & 0x01) == 0);
/* 检查传输是否发生错误 */
if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
{
i2c_master_stop();
return ret;
}
} while (xfer->reg_size > 0);
/* 如果是读时序,需要先发送重复开始信号;如果是写时序,直接发送数据 */
if (xfer->direction == I2C_Read)
{
clear_intpending_flag();
/* 发送重复开始信号 */
ret = i2c_master_repeated_start(xfer->slaveAddress, I2C_Read);
if (ret != Status_I2C_Success)
{
i2c_master_stop();
return ret;
}
/* 等待传输完成 */
while (((I2C1_I2SR >> 1) & 0x01) == 0);
/* 检查传输是否发生错误 */
if ((ret = i2c_check_and_clear_error()) != Status_I2C_Success)
{
i2c_master_stop();
return ret;
}
}
}
/* 到此,准备工作完毕 */
/* 发送数据 */
if (xfer->direction == I2C_Write)
{
ret = i2c_master_write_blocking(xfer->buf, xfer->buf_size, xfer->stopFlag);
}
if (xfer->direction == I2C_Read)
{
ret = i2c_master_read_blocking(xfer->buf, xfer->buf_size);
}
return ret;
}