# 起源
I2C 支持设备之间短距离通信,用于处理器和外设之间,只需要两根线来完成信息交互. I2C 出现于飞利浦公司,开始只允许 100kHz, 7-bit 标准地址。直到 1992 年,I2C 第一个公共规范,增加了 400kHz 的快速模式和 10-bit 扩展地址.
在 I2C 的基础上,1995 年 Intel 提出了 SMBus 用于低速设备通信,SMBus 把时钟频率限制在 10KHz~100KHz, 但目前的 I2C 可以支持 0KHz~5MHz 的外设:
- 普通模式 100KHz
- 快速模式 (Fm) 400KHz
- 快速模式 +(Fm+) 1MHz
- 高速模式 (Hs) 3.4MHz
- 超高速模式 (UFm) 5MHz
PS: 基于 IIC 是 Master 于 Slave 模式,故两者间的通信要保持时钟的一致,IIC 为串行同步半双工
# 应用
IIC 适用于外围设备,其简单性和低制造成本比速度更重要. I2C 总线的常简应用包括:
- 通过小型 BOM 配置表描述可连接设备,以实现 "即插即用" 操作,例如:
- 双列直插内存模块 (DIMM) 上的串行存在检测 EEPROM
- 通过 VGA, DVI 和 HDMI 连接器为显示器提供扩展显示识别数据 (EDID)
- 通过 SMBus 对 PC 系统进行系统管理
- SMBus 引脚分配在常规 PCI 和 PCI Express 连接器中
- 访问保持用户设置的实时时钟和 NVRAM 芯片
- 访问低俗 DAC 和 ADC
- 更改显示器中的对比度,色调和色彩平衡设置 (通过显示数据通道)
- 改变智能扬声器的音量
- 控制小型 (例如功能手机) OLED 或 LCD 显示器
- 读取硬件监视器和诊断传感器,例如风扇的速度
- 打开和关闭系统组件的电源
# 协议概述
I2C 协议把传输的消息分为两种类型的帧:
- 地址帧 用于 Master 和 Slave 建立连接
- 数据帧 由 Master 发往 Slave 的数据 (也可能是 Slave 发送给 Master 的数据), 一帧 8-bit.
PS: 通常我们所说的 IIC 读写是相对于 Master 来说的
SCL 产生下降沿后,数据在 SDA 上传输.
SCL 产生上升沿后,在 SDA 上对电平进行采样,转化为数据
PS: 时钟边沿和数据读 / 写之间的时间由总线上的器件定义,并在芯片和芯片之间会有所不同
# IIC 写入
- Master 发送起始信号
- Master 发送从机地址 (7bit) 和写操作位 (1bit), 等待 ACK
- Slave 发送 ACK
- Master 发送寄存器地址,等待 ACK
- Slave 发送 ACK
- Master 发送 Data (8bit), 要写入寄存器中的数据,等待 ACK
- Slave 发送 ACK
- Master 发送停止信号
PS: 6 7 步可以重复多次,即顺序写入多个寄存器
# IIC 读取
- Master 发送起始信号
- Master 发送从机地址 (7bit) 和 写操作 (1bit), 等待 ACK
- Master 发送寄存器地址 (8-bit), 等待 ACK
- Slave 发送 ACK
- Master 发送起始信号
- Master 发送 I2C 地址 (7-bit) 和 读操作 (1bit), 等待 ACK
- Slave 发送 ACK
- Slave 发送 数据 (8-bit), 要读出的寄存器中的值
- Master 发送 ACK (连续读取)/NACK (终止读取)
- Master 发送停止信号
8 和 9 可以重复多次,顺序读取多个寄存器
# 协议详解
# 起始信号
官方规定,当 SCL 线为高电平,且 SDA 出现下降沿时,表示 Master 与 Slave 开始通信。开始信号发出后,所有的 IIC 总线上所有的 Slave 都会接收到开始信号,准备与主机进行连接.
PS: 如果存在两个 Master 希望一次获得总线的所有权限,则无论哪一个设备将 SDA 拉低,第一个拉低 SDA 的 Master 将获得对总线的控制权
# 地址帧
无论是读操作还是写操作,地址帧是第一次要发送的数据。对于 7-bit 地址来说,地址帧先输出最高有效位,然后输出读写位 (1 读 0 写).
地址帧的第 9 位是 NACK/ACK 位,当然所有帧都是这种格式。当接受设备收到发送帧的前 8 位,接受设备就可以控制 SDA 来产生一次应答。如果接收设备在第 9 个时钟脉冲之前没有将 SDA 拉低,则代表接受设备没有收到数据或者无法解析数据,也有可能是没有从机。这种情况下,Master 就需要进行超时处理.
# 数据帧
发送地址帧之后,可以开始传输数据. Master 将以恒定的间隔继续生成时钟,数据由 Master 或 Slave 放置在 SDA 商进行传输。读和写具体决定于 R/W 位的指示。数据的总数是任意的,并且大部分器件将自动递增内部寄存器,这意味着后续读取或写入将来自下一个寄存器.
写多个寄存器
读多个寄存器
读写混合操作
# 停止信号
一旦发送了所有数据帧,主设备将生成停止信号。停止信号由 SCL 产生一个上升沿后,SDA 产生一个上升沿定义,SCL 保持高电平。在正常的数据写操作时,SDA 上的值应该不会当 SCL 为高电平改变,以避免错误的停止条件
# 代码实现
# 硬件 IIC
以 STM32F401 为例:
正常版本
//iic 写数据 | |
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); | |
//iic 读数据 | |
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout); | |
// 发送多个字节 | |
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout); | |
// 读取多个字节 | |
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout); |
PS: 使用 HAL_I2C_Mem_Write
等于先使用 HAL_I2C_Master_Transmit
传输第一个寄存器地址,再用 HAL_I2C_Master_Transmit
传输写入第一个寄存器的数据
中断版本
// 中断写入单字节 | |
HAL_StatusTypeDef HAL_I2C_Master_Transmit_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size); | |
// 中断读取单字节 | |
HAL_StatusTypeDef HAL_I2C_Master_Receive_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size); | |
// 中断写入多字节 | |
HAL_StatusTypeDef HAL_I2C_Mem_Read_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size); | |
// 中断读取多字节 | |
HAL_StatusTypeDef HAL_I2C_Mem_Write_IT(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size); |
DMA 版本
// | |
HAL_StatusTypeDef HAL_I2C_Mem_Read_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size); | |
// | |
HAL_StatusTypeDef HAL_I2C_Mem_Write_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size); | |
// | |
HAL_StatusTypeDef HAL_I2C_Master_Transmit_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size); | |
// | |
HAL_StatusTypeDef HAL_I2C_Master_Receive_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size); |
# 软件 IIC
# 起始信号
void IIC_Start(void) | |
{ | |
IIC_SDA_SET; | |
IIC_SCL_SET; | |
delay_us(4); | |
IIC_SDA_CLS; | |
delay_us(4); | |
IIC_SCL_CLS; | |
} |
# 停止信号
void IIC_Stop(void) | |
{ | |
IIC_SCL_CLS; | |
IIC_SDA_CLS; | |
delay_us(4); | |
IIC_SCL_SET; | |
delay_us(4); | |
IIC_SDA_SET; | |
delay_us(4); | |
} |
# 等待应答
static uint8_t I2C_WaiteAck(void) | |
{ | |
uint8_t i; | |
IIC_SDA_SET; | |
delay_us(1); | |
IIC_SCL_SET; | |
delay_us(1); | |
while (OLED_GPIO_PORT_I2C->IDR & OLED_I2C_SDA_PIN) | |
{ | |
i++; | |
if (i > 128) | |
{ | |
IIC_Stop(); | |
return 1; | |
} | |
} | |
IIC_SCL_CLS; | |
return 0; | |
} |
# 发送一个字节
void IIC_Send_Byte(uint8_t IIC_Byte) | |
{ | |
unsigned char i; | |
IIC_SCL_CLS; | |
for (i = 0; i < 8; i++) | |
{ | |
if (IIC_Byte & 0x80) | |
IIC_SDA_SET; | |
else | |
IIC_SDA_CLS; | |
IIC_Byte <<= 1; | |
delay_us(2); | |
IIC_SCL_SET; | |
delay_us(2); // 必须有保持 SCL 脉冲的延时 | |
IIC_SCL_CLS; | |
delay_us(2); | |
} | |
// 原程序这里有一个拉高 SDA,根据 OLED 的要求,此句必须去掉。 | |
// IIC_SDA_SET; | |
// IIC_SCL_SET; | |
// delay_us(1); | |
// IIC_SCL_CLS; | |
} |
# 写入数据帧
void I2C_WriteByte(uint8_t addr, uint8_t data) | |
{ | |
IIC_Start(); | |
IIC_Send_Byte(OLED_ADDRESS); | |
I2C_WaiteAck(); | |
IIC_Send_Byte(addr); //write data | |
I2C_WaiteAck(); | |
IIC_Send_Byte(data); | |
I2C_WaiteAck(); | |
IIC_Stop(); | |
} |
# 关于软件 IIC 的重构
Black Sheep 于 2021/12/17 8:11 修改
有没有感觉到软件 iic 让人感觉到很不适应,移植的时候虽然说改几个宏定义即可,但是总感觉还是欠些什么.
例如,eeprom、oled、ADC 等一系列 IIC 的驱动设备,抽象 ADT 的时候,目前的 iic 永远只能是一个底层函数,组成 read 和 write 函数,无法像硬件设备那样作为一个对象放置在结构体中
所以我在试着把软件 iic 抽象成出一个通用的 ADT.
# 对数据结构的思考
个人觉得类似于 C 来说,本身是一个面向过程的语言,要用于实现面向对象的编程会较为繁杂一些,而且需要一定的 C 语言底子.
一般来说 一个基础类会分为 成员对象 和 成员函数,成员对象一般记录 对象的固有属性,成员函数往往被称为方法,用于实现对成员的操作.
在 C 语言 中我们 对应的使用 结构体来代替 class , 用字段来描述 对象的属性,用函数指针来充当方法.
所以在 C 语言一般 ADT 一般以这种形式出现
struct xxxObject{ | |
// 属性字段 | |
int attr_xx; | |
int attr_yy; | |
int attr_zz; | |
// 方法 | |
void (*op1)(xxx_op1_t op); | |
void (*op2)(xxx_op2_t op); | |
void (*op3)(xxx_op3_t op); | |
} |
基于这种想法我就有了,重构 iic 的想法
# 对于 IIC 的对象的构建
对于一个 软件 iic 一般来说会有什么呢?
SCL, SDA? 不不不,那都是最基本物理硬件而已。在我看来,IIC 更多的是 电平的变化,而不是某个引脚.
从之前的代码里可以看到,IIC 的 SCL 和 SDA 的操作才是 IIC 的固有的特性.
这次 我打算将物理层和应用层之间割裂开来,做一层驱动层.
通过上面的思考就有了结构体的第一版
struct softwareI2C{ | |
void (*set_SCLPin)(uint8_t op); | |
void (*set_SDAPin)(uint8_t op); | |
uint8_t(*read_SDAPin)(void); | |
}; |
除了这些还有什么呢?钳位,IIC 为了保持电平会有一定的延时时间,这个电平的保持时间也决定了 IIC 的通信速率,不同的从机设备会有不同速率的要求.
虽然 模拟 iic 无法提高极限速率但是还是可以调整最低速率的
所以还得有一个 延时函数和一个延时的周期
所以 第二稿的结构体就诞生了
struct SimuI2CObject{ | |
uint32_t period; | |
void (*set_SCLPin)(uint8_t op); | |
void (*set_SDAPin)(uint8_t op); | |
uint8_t(*read_SDAPin)(void); | |
void (*delayus)(volatile volatile uint32_t period); | |
}; |
IIC 的基本类型就抽象出来了,但是我还是感觉 uint8_t 通用性不够强,所以放置了一些枚举变量
// I2C 引脚状态值 | |
typedef enum SimuI2CPinValue{ | |
SimuI2CPin_Set = 1, | |
SimuI2CPin_Reset = (!SimuI2CPin_Set), | |
}SimuI2CPinValue | |
// IIC 通信状态 | |
typedef enum SimuI2CStatus{ | |
I2C_OK=0, | |
I2C_ERROR=1, | |
I2C_PARAMRTER_INVAIL, | |
}SimuI2CStatus; |
根据上面的枚举变量我们来就得到了 SimuI2CObject
结构体的最终版本
typedef struct SimuI2CObject{ | |
uint32_t period; // I2C 传输速率 (Range:0~400K, Default: 100K) | |
uint32_t timeOut; // ACK 超时时间 | |
void (*set_SCLPin)(SimuI2CPinValue_t op); // SCL 电平操作 | |
void (*set_SDAPin)(SimuI2CPinValue_t op); // SDA 电平操作 | |
unsigned char (*read_SDAPin)(void); // SDA 读取数据 | |
void (*DelayUs)(volatile uint32_t period); // I2C 延时函数 | |
}SimuI2CObject_t; |
这些代码都会在最后一起贴出来
# 初始化函数
在面向对象的语言里这一步由构造函数完成,而对于 C 而言,我们需要手动调用.
当然在这之前我们需要先声明几个类型,用简化参数声明
typedef void (*SimuI2CPinOPfunc_t)(SimuI2CPinValue_t op); // SDA, SCL 电平操作函数指针 | |
typedef unsigned char (*SimuI2CReadfunc_t)(void); // SDA 读取电平函数指针 | |
typedef void (simuI2CDelayusfunc_t)(volatile uint32_t period); // I2C 延时函数指针 |
初始化函数的作用是 配置相关属性,本地方法到对象中。所以 初始化函数的参数需要包含 大部分需要自定义的属性
void SimuI2C_Initialization(SimuI2CObject_t *Instance, // I2C 结构体实例 | |
uint32_t speed, // I2C 传输速率 | |
SimuI2CPinOPfunc_t setSCL, // SCL 电平操作函数 | |
SimuI2CPinOPfunc_t setSDA, // SDA 电平操作函数 | |
SimuI2CReadfunc_t readSDA, // SDA 读取数据函数 | |
simuI2CDelayusfunc_t delayus // I2C 延时函数 | |
); |
相对而言 初始化的函数无非就是赋值的过程实现起来会比其他接口函数更为简单
/** | |
* @brief I2C 初始化 | |
* | |
* @param Instance I2C 结构体实例 | |
* @param speed I2C 传输速率 | |
* @param setSCL SCL 电平操作函数 | |
* @param setSDA SDA 电平操作函数 | |
* @param readSDA SDA 读取数据函数 | |
* @param delayus I2C 延时函数 | |
* | |
* @retval 初始化状态 | |
*/ | |
SimuI2CStatus SimuI2C_Initialization(SimuI2CObject_t *Instance, | |
uint32_t speed, | |
SimuI2CPinOPfunc_t setSCL, | |
SimuI2CPinOPfunc_t setSDA, | |
SimuI2CReadfunc_t readSDA, | |
simuI2CDelayusfunc_t delayus) | |
{ | |
// 参数检测 | |
if (!(Instance && setSCL && setSDA && readSDA && delayus)) | |
return I2C_PARAMRTER_INVAIL; | |
Instance->setSCLPin = setSCL; | |
Instance->setSDAPin = setSDA; | |
Instance->read_SDAPin = readSDA; | |
Instance->DelayUs = delayus; | |
Instance->timeOut = 5000; | |
if (speed > 0 && speed <= 400) | |
{ // 延时时长,500 非固定值自行计算 | |
Instance->period = 500 / speed; | |
} | |
else | |
{ // 默认 100 k | |
Instance->period = 5; | |
} | |
// 拉高总线,使处于空闲状态 | |
Instance->setSDAPin(I2C_PIN_SET); | |
Instance->setSCLPin(I2C_PIN_SET); | |
return I2C_OK; | |
} |
至于底层的高低电平变化,gpio 配置等,应该属于 bsp 的初始化或者是 gpio 的 ADT 的任务,而非 iic 的特性.
# 接口函数
IIC 用的最多的 对应用层的接口是什么呢?
毫无疑问是读写,所以对外的接口只需要由 下面几个即可
有些时候会有 读写混合操作,所以我这里多留了一个接口
SimuI2CStatus_t WriteDataBySimuI2C(SimuI2CObject_t *Instance, // I2C 结构体实例 | |
unsigned char deviceAddress, // I2C 驱动设备地址 | |
unsigned char *data, // I2C 写数据数组 | |
uint16_t size // I2C 写数据大小 | |
); | |
SimuI2CStatus_t ReadDataBySimuI2C(SimuI2CObject_t *Instance, // I2C 结构体实例 | |
unsigned char deviceAddress, // I2C 驱动设备地址 | |
unsigned char *data, // I2C 读数据数组 | |
uint16_t size // I2C 读数据大小 | |
); | |
SimuI2CStatus_t WriteReadDataBySimuI2C(SimuI2CObject_t *Instance, // I2C 结构体实例 | |
unsigned char deviceAddress, // I2C 驱动设备地址 | |
unsigned char *wData, // I2C 写数据数组 | |
uint16_t wSize, // I2C 写数据大小 | |
unsigned char *rData, // I2C 读数据数组 | |
uint16_t rSize); // I2C 读数据大小 |
由于逻辑上面也说过了,这里就不重复啰嗦了
# 读数据
/** | |
* @brief I2C 读数据 | |
* | |
* @param Instance I2C 结构体实例 | |
* @param deviceAddress I2C 驱动设备地址 | |
* @param data I2C 读数据数组 | |
* @param size I2C 读数据大小 | |
* @retval I2C 通信状态 | |
*/ | |
SimuI2CStatus_t ReadDataBySimuI2C(SimuI2CObject_t *Instance, | |
unsigned char deviceAddress, | |
unsigned char *data, | |
uint16_t size) | |
{ | |
// 启动通信 | |
SimuI2CStart(Instance); | |
// 发送地址 | |
sendByteBySimuI2C(Instance, deviceAddress); | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)){ | |
return I2C_ER; | |
} | |
Instance->DelayUs(1000); | |
while(size--){ | |
*data = recieveByteBySimuI2C(Instance); | |
data++; | |
if (!data){ | |
SimuI2C_NAck(Instance); | |
} | |
else{ | |
SimuI2C_Ack(Instance); | |
Instance->DelayUs(1000); | |
} | |
} | |
return I2C_OK; | |
} |
# 写数据
/** | |
* @brief I2C 写数据 | |
* | |
* @param Instance I2C 结构体实例 | |
* @param deviceAddress I2C 驱动设备地址 | |
* @param data I2C 写数据数组 | |
* @param size I2C 写数据大小 | |
* @retval I2C 通信状态 | |
*/ | |
SimuI2CStatus_t WriteDataBySimuI2C(SimuI2CObject_t *Instance, | |
unsigned char deviceAddress, | |
unsigned char *data, | |
uint16_t size) | |
{ | |
// 起始信号 | |
SimuI2CStart(Instance); | |
// 发送地址 | |
sendByteBySimuI2C(Instance, deviceAddress); | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)); | |
{ | |
return I2C_ER; | |
} | |
while (size--){ | |
sendByteBySimuI2C(Instance, *data); | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)){ | |
return I2C_ER; | |
} | |
data++; | |
Instance->DelayUs(10); | |
} | |
SimuI2CStop(Instance); | |
return I2C_OK; | |
} |
# 读写数据混合
/** | |
* @brief I2C 读写混合操作 | |
* | |
* @param Instance I2C 结构体实例 | |
* @param deviceAddress I2C 驱动设备地址 | |
* @param wData I2C 写数据数组 | |
* @param wSize I2C 写数据大小 | |
* @param rData I2C 读数据数组 | |
* @param rSize I2C 读数据大小 | |
* @retval I2C 通信状态 | |
*/ | |
SimuI2CStatus_t WriteReadDataBySimuI2C(SimuI2CObject_t *Instance, | |
unsigned char deviceAddress, | |
unsigned char *wData, | |
uint16_t wSize, | |
unsigned char *rData, | |
uint16_t rSize) | |
{ | |
// 启动 通信 | |
SimuI2CStart(Instance); | |
sendByteBySimuI2C(Instance, deviceAddress); | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)){ | |
return I2C_ER; | |
} | |
while (wSize--){ | |
sendByteBySimuI2C(Instance, *wData); | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)){ | |
return I2C_ER; | |
} | |
Instance->DelayUs(10); | |
} | |
// 再启动 | |
SimuI2CStart(Instance); | |
sendByteBySimuI2C(Instance, deviceAddress+1);// 写 | |
if (SimuI2CWaitAck(Instance, Instance->timeOut)){ | |
return I2C_ER; | |
} | |
while (rSize--){ | |
*rData = recieveByteBySimuI2C(Instance);\ | |
rData++; | |
if (!rSize){ | |
SimuI2C_NAck(Instance); | |
} | |
else{ | |
SimuI2C_Ack(Instance); | |
} | |
} | |
// I2C 通信停止 | |
SimuI2CStop(Instance); | |
return I2C_OK; | |
} |
# IIC 基础函数
这里 我们会去实现 IIC 的 start
, stop
, waitAck
, ACK
, NACK
, sendByte
, readByte
等函数
这里需要注意:
I2C 除 STOP 每个阶段跳变 SCL 都处于低电平,否则会被视为发送结束
# 起始信号
这里需要注意一下的就是 SCL 的钳位,保持电平,出发送信号
/** | |
* @brief I2C 起始信号 | |
* | |
* @param Instance I2C 实例结构体 | |
*/ | |
static void SimuI2CStart(SimuI2CObject_t *Instance) | |
{ | |
// 释放总线 | |
Instance->setSDAPin(I2C_PIN_SET); | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
// 开始信号,SDA 下降沿 | |
Instance->setSDAPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
// 钳住 SCL 准备发送数据 | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
} |
# 结束信号
/** | |
* @brief I2C 终止信号 | |
* | |
* @param Instance I2C 实例结构体 | |
*/ | |
static void SimuI2CStop(SimuI2CObject_t *Instance) | |
{ | |
// I2C 拉低 SCL 允许 SDA 电平变化 | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSDAPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSDAPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
} |
# 等待应答
/** | |
* @brief I2C 等待应答 | |
* | |
* @param Instance I2C 实例结构体 | |
* @param timeOut 应答超时时间 | |
* @retval I2C_OK: 收到应答 | |
* I2C_ER: 未收到应答信号 | |
*/ | |
static SimuI2CStatus_t SimuI2CWaitAck(SimuI2CObject_t *Instance, uint16_t timeOut) | |
{ | |
/** | |
* 当 SCL 低电平,SDA 拉高,拉高 SCL | |
* 检测 SDA 是否为低电平 | |
*/ | |
Instance->setSDAPin(I2C_PIN_SET); // 拉高 SDA | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_SET); // 拉高 SCL | |
Instance->DelayUs(Instance->period); | |
while (Instance->read_SDAPin()){ | |
if (--timeOut){ | |
SimuI2CStop(Instance); // 异常直接 Stop | |
return I2C_ER; | |
} | |
Instance->DelayUs(Instance->period); | |
} | |
// I2C 阶段跳变 SCL 默认拉低 | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
return I2C_OK; | |
} |
# 产生应答
/** | |
* @brief I2C 应答信号 | |
* | |
* @param Instance I2C 实例结构体 | |
*/ | |
static void SimuI2C_Ack(SimuI2CObject_t *Instance) | |
{ | |
// SCL 低,SDA 低,SCL 高,SCL 低 | |
Instance->setSDAPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSDAPin(I2C_PIN_SET); | |
} |
# 产生 NAck
/** | |
* @brief I2C 非应答信号 | |
* | |
* @param Instance I2C 实例结构体 | |
*/ | |
static void SimuI2C_NAck(SimuI2CObject_t *Instance) | |
{ | |
// SCL 低,SDA 高,SCL 高,SCL 低 | |
// Instance->SetSCLPin(I2C_PIN_RESET); | |
// SDA 拉高产生,NACK | |
Instance->setSDAPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
// 发送 1bit 数据 注意是 bit 不是 Byte | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
} |
# 发送一个字节
/** | |
* @brief I2C 发送一个字节 | |
* | |
* @param Instance I2C 实例结构体 | |
* @param byte 发送的数据 | |
*/ | |
static void sendByteBySimuI2C(SimuI2CObject_t *Instance, unsigned char byte) | |
{ | |
unsigned char i = 0; | |
// 拉低 SCL 准备传输数据 | |
Instance->setSCLPin(I2C_PIN_RESET); | |
// 逐位发送,模拟移位发送器 | |
for (i = 0; i < 8; i++){ | |
if (byte & 0x80){ | |
Instance->setSDAPin(I2C_PIN_SET); | |
} | |
else | |
{ | |
Instance->setSDAPin(I2C_PIN_RESET); | |
} | |
byte <<= 1; // 数据左移一位 | |
// 发送数据 | |
Instance->DelayUs(Instance->period); | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
} | |
} |
# 读一个字节
/** | |
* @brief I2C 读一个字节 | |
* | |
* @param Instance I2C 实例结构体 | |
* @retval 读到的数据 | |
*/ | |
static unsigned char recieveByteBySimuI2C(SimuI2CObject_t *Instance) | |
{ | |
unsigned char receive = 0; | |
unsigned char i = 0; | |
Instance->setSDAPin(I2C_PIN_SET); | |
// 读各个位 | |
for (i = 0 ; i < 8; i++){ | |
Instance->setSCLPin(I2C_PIN_SET); | |
Instance->DelayUs(Instance->period); | |
receive <<= 1; | |
// 判断是 1 则 累加,最低为置位和 ++ 效果一致 | |
if (Instance->read_SDAPin()){ | |
receive++; | |
} | |
Instance->setSCLPin(I2C_PIN_RESET); | |
Instance->DelayUs(Instance->period); | |
} | |
return receive; | |
} |
# 一些想法
其实两种实现本质上没有什么区别,甚至可以说第一种看上去会更加简洁,深得 C 语言设计核心,简洁高效
但是对我来说,第一种方法在想在应用层去继续实现抽象的话会很麻烦.
例如 oled, 抽象的时候就必须在抽出一层特有的 iic 结构体作为父类,或者放一个 write 指针到 oled 中,写 iic.
这种风格不是我希望看见的,虽然我现在没有很好的分层的思想,但是终究分层的实现一个设备类耦合度会低很多很多的,而且重构会更加方便.
虽然说单片机就那点功能,没有必要搞那么多花里胡哨的的东西.
但是毕竟代码毕竟是给人的,出问题了重构还是自己来搞,所以无论是什么的代码都还是需要认认真真的去编写.
对待代码,对待产品是需要有敬畏之心的.
PS: 关于新实现的 iic 的代码我会尽快上传到 github 和 Gitee 上,并且尽快对 oled 项目的软件 iic 进行重构
# 相关项目
oled 驱动包含 软硬件 IIC 和 DMA 传输,基于 HAL 库实现
Github 链接:https://github.com/to-ywz/OLED_Device
大道五十,天衍四十九,人遁其一!