介绍
本项目是利用GPIO模拟I2C的主机
网上常见的是模拟I2C主机
本项目是作为一个两个单片机之间低速通信的用法
协议介绍请看,传送门
模拟从机请看这里
主机
功能描述
- I2C按字节(Byte)读写
- I2C读写寄存器
- I2C连续读写
编程思路解析
主机是时钟信号的发起方,起始和中止信号均来自主机
I2C的主机编程相对简单
我们只需要按照协议发送信号即可,具体在实现各个功能时细说
HAL初始化
I2C主机需要使用2个开漏上拉输出的GPIO
分别是SDA 和 SCL
我们对其操作时序,模拟出I2C协议即可
程序设计
GPIO控制
将HAL库的GPIO控制的函数做了一个封装,用于改变GPIO时的快速使用
#define I2C_SCL_GPIOx GPIOA
#define I2C_SCL_Pin GPIO_PIN_0
#define I2C_SDA_GPIOx GPIOA
#define I2C_SDA_Pin GPIO_PIN_1
/**
* @brief 一段延迟
* @param 无
* @return 无
* @author HZ12138
* @date 2022-07-27 08:53:30
*/
void I2C_Delay(void)
{
int z = 0xff;
while (z--)
;
}
/**
* @brief 写SDA
* @param H_L:高低电平
* @return 无
* @author HZ12138
* @date 2022-10-21 18:07:18
*/
void I2C_Write_SDA(GPIO_PinState H_L)
{
HAL_GPIO_WritePin(I2C_SDA_GPIOx, I2C_SDA_Pin, H_L);
}
/**
* @brief 写SCL
* @param H_L:高低电平
* @return 无
* @author HZ12138
* @date 2022-10-21 18:07:40
*/
void I2C_Write_SCL(GPIO_PinState H_L)
{
HAL_GPIO_WritePin(I2C_SCL_GPIOx, I2C_SCL_Pin, H_L);
}
/**
* @brief 读取SDA
* @param 无
* @return SDA的状态
* @author HZ12138
* @date 2022-10-21 18:07:56
*/
uint16_t I2C_Read_SDA(void)
{
return HAL_GPIO_ReadPin(I2C_SDA_GPIOx, I2C_SDA_Pin);
}
/**
* @brief 读取SCL
* @param 无
* @return SDA的状态
* @author HZ12138
* @date 2022-10-21 18:07:56
*/
uint16_t I2C_Read_SCL(void)
{
return HAL_GPIO_ReadPin(I2C_SCL_GPIOx, I2C_SCL_Pin);
}
启动中止信号
I2C的启动中止信号是在SCL高电平时,SDA发生跳变
这样只需要先拉高SCL,再改变SDA即可
必要的地方加入了延迟,避免过高的速度出现问题
/**
* @brief 产生I2C起始信号
* @param 无
* @return 无
* @author HZ12138
* @date 2022-07-27 08:54:48
*/
void I2C_Start(void)
{
I2C_Write_SDA(GPIO_PIN_SET); // 需在SCL之前设定
I2C_Write_SCL(GPIO_PIN_SET); // SCL->高
I2C_Delay(); // 延时
I2C_Write_SDA(GPIO_PIN_RESET); // SDA由1->0,产生开始信号
I2C_Delay(); // 延时
I2C_Write_SCL(GPIO_PIN_RESET); // SCL->低
}
/**
* @brief 产生I2C结束信号
* @param 无
* @return 无
* @author HZ12138
* @date 2022-07-27 08:57:03
*/
void I2C_End(void)
{
I2C_Write_SDA(GPIO_PIN_RESET); // 在SCL之前拉低
I2C_Write_SCL(GPIO_PIN_SET); // SCL->高
I2C_Delay(); // 延时
I2C_Write_SDA(GPIO_PIN_SET); // SDA由0->1,产生结束信号
I2C_Delay(); // 延时
}
应答信号
对于主机来说,有两种应答,
一个是自己发送应答信号,主机控制SDA和SCL
另一个是接收从机的应答信号,主机控制SCL,从机控制SDA
这边分成了2个控制函数
注意:ACK信号在很多设备中会被省略,这边主机只保证有第9个周期即可,不用过多关注应答
/**
* @brief 发送应答码
* @param ack:0 应答 1 不应达
* @return 无
* @author HZ12138
* @date 2022-07-27 09:03:38
*/
void I2C_Send_ACK(uint8_t ack)
{
if (ack == 1)
I2C_Write_SDA(GPIO_PIN_SET); // 产生应答电平
else
I2C_Write_SDA(GPIO_PIN_RESET);
I2C_Delay();
I2C_Write_SCL(GPIO_PIN_SET); // 发送应答信号
I2C_Delay(); // 延时至少4us
I2C_Write_SCL(GPIO_PIN_RESET); // 整个期间保持应答信号
}
/**
* @brief 接受应答码
* @param 无
* @return 应答码 0 应答 1 不应达
* @author HZ12138
* @date 2022-07-27 09:04:28
*/
uint8_t I2C_Get_ACK(void)
{
uint8_t ret; // 用来接收返回值
I2C_Write_SDA(GPIO_PIN_SET); // 电阻上拉,进入读
I2C_Delay();
I2C_Write_SCL(GPIO_PIN_SET); // 进入应答检测
I2C_Delay(); // 至少延时4us
ret = I2C_Read_SDA(); // 保存应答信号
I2C_Write_SCL(GPIO_PIN_RESET);
return ret;
}
写一个字节
控制SDA向外部发送1Byte数据
从高位到低位发送,SDA的电平改变需要在SCL低电平时
发送8个bit后,第9个周期需要读取ASK信号
所以编程思路如下:
搞一个循环,从0到7做计数,每次发送数据的最高位,并将数据向右移动一位,同时在SDA改变后发送一个SCL周期
在发送完成后,读取ASK信号并保存下来
/**
* @brief I2C写1Byte
* @param dat:1Byte数据
* @return 应答结果 0 应答 1 不应达
* @author HZ12138
* @date 2022-07-27 09:05:14
*/
uint8_t I2C_SendByte(uint8_t dat)
{
uint8_t ack;
for (int i = 0; i < 8; i++)
{
// 高在前低在后
if (dat & 0x80)
I2C_Write_SDA(GPIO_PIN_SET);
else
I2C_Write_SDA(GPIO_PIN_RESET);
I2C_Delay();
I2C_Write_SCL(GPIO_PIN_SET);
I2C_Delay(); // 延时至少4us
I2C_Write_SCL(GPIO_PIN_RESET);
dat <<= 1; // 低位向高位移动
}
ack = I2C_Get_ACK();
return ack;
}
读一个字节
控制SDA向外部发送1Byte数据
从高位到低位读取,SCL高电平时,读取SDA
发送8个bit后,第9个周期需要发送ASK信号
和写一样,同样搞一个循环
在SCL高电平时读取SDA即可
在读取完1Byte后,向外发送ASK信号
/**
* @brief I2C读取1Byte数据
* @param ack:应答 0 应答 1 不应达
* @return 接受到的数据
* @author HZ12138
* @date 2022-07-27 09:06:13
*/
uint8_t I2C_ReadByte(uint8_t ack)
{
uint8_t ret = 0;
I2C_Write_SDA(GPIO_PIN_SET);
for (int i = 0; i < 8; i++)
{
ret <<= 1;
I2C_Write_SCL(GPIO_PIN_SET);
I2C_Delay();
// 高在前低在后
if (I2C_Read_SDA())
{
ret++;
}
I2C_Write_SCL(GPIO_PIN_RESET);
I2C_Delay();
}
I2C_Send_ACK(ack);
return ret;
}
连续写
用于写大量数据给从机
流程:
- 发送起始信号
- 发送器件地址(最低位是0)
- 发送寄存器地址
- 将缓冲区数据按顺序发送出去
- 发送结束信号
/**
* @brief I2C连续写
* @param addr:器件地址
* @param reg:寄存器地址
* @param len:长度
* @param buf:缓冲区地址
* @return 状态 0成功 其他失败
* @author HZ12138
* @date 2022-08-08 15:47:11
*/
uint8_t I2C_Write_Len(uint8_t reg, uint8_t len, uint8_t *buf)
{
uint8_t i;
I2C_Start();
I2C_SendByte(I2C_Address | 0); // 发送器件地址+写命令
I2C_SendByte(reg); // 写寄存器地址
for (i = 0; i < len; i++)
{
I2C_SendByte(buf[i]); // 发送数据
}
I2C_End();
return 0;
}
连续读
用于从从机读取大量数据
流程:
- 发送起始信号
- 发送器件地址(最低为为0)
- 发送寄存器地址
- 发送起始信号
- 发送器件地址(最低为为1)
- 按顺序读取数据写入缓冲区
- 发送结束信号
/**
* @brief I2C连续读
* @param addr:器件地址
* @param reg:寄存器地址
* @param len:长度
* @param buf:缓冲区地址
* @return 状态 0成功 其他失败
* @author HZ12138
* @date 2022-08-08 15:47:11
*/
uint8_t I2C_Read_Len(uint8_t reg, uint8_t len, uint8_t *buf)
{
I2C_Start();
I2C_SendByte(I2C_Address | 0); // 发送器件地址+写命令
I2C_SendByte(reg); // 写寄存器地址
I2C_Start();
I2C_SendByte(I2C_Address | 1); // 发送器件地址+读命令
while (len)
{
if (len == 1)
*buf = I2C_ReadByte(1); // 读数据,发送nACK
else
*buf = I2C_ReadByte(0); // 读数据,发送ACK
len--;
buf++;
}
I2C_End(); // 产生一个停止条件
return 0;
}
写寄存器
写寄存器的一个字节
流程:
- 发送起始信号
- 发送器件地址(最低位是0)
- 发送寄存器地址
- 将缓冲区数据发送出去
- 发送结束信号
/**
* @brief I2C写一个字节
* @param reg:寄存器地址
* @param data:数据
* @return 状态 0成功 其他失败
* @author HZ12138
* @date 2022-08-08 15:47:11
*/
uint8_t I2C_Write_Reg(uint8_t reg, uint8_t data)
{
I2C_Start();
I2C_SendByte(I2C_Address | 0); // 发送器件地址+写命令
I2C_SendByte(reg); // 写寄存器地址
I2C_SendByte(data); // 发送数据
I2C_End();
return 0;
}
读寄存器
读取寄存器的一个字节
流程:
- 发送起始信号
- 发送器件地址(最低为为0)
- 发送寄存器地址
- 发送起始信号
- 发送器件地址(最低为为1)
- 读取数据写入缓冲区
- 发送结束信号
/**
* @brief I2C读一个字节
* @param reg:寄存器地址
* @return 读取到的数据
* @author HZ12138
* @date 2022-08-08 15:47:11
*/
uint8_t I2C_Read_Reg(uint8_t reg)
{
uint8_t res;
I2C_Start();
I2C_SendByte(I2C_Address | 0); // 发送器件地址+写命令
I2C_SendByte(reg); // 写寄存器地址
I2C_Start();
I2C_SendByte(I2C_Address | 1); // 发送器件地址+读命令
res = I2C_ReadByte(1); // 读取数据,发送nACK
I2C_End(); // 产生一个停止条件
return res;
}
成品
GitHub