第19章 模拟I2C
第十九章 模拟I2C
1. 硬件设计
我们使用GPIO来模拟I2C,无其他硬件资源
2. 软件设计
2.1 编程大纲
-
模拟I2C宏定义配置
-
根据时序模拟I2C
-
沿用上一节的EEPROM读写代码
-
主函数测试
2.2 代码设计
2.2.1 模拟I2C宏定义
#define I2C_WR 0 // 写控制位
#define I2C_RD 1 // 读控制位
// GPIO端口定义
#define I2C_PORT GPIOB
#define I2C_SCL_PIN GPIO_Pin_6
#define I2C_SDA_PIN GPIO_Pin_7
// 读写SCL和SDA
#define I2C_SCL_1() GPIO_SetBits(I2C_PORT, I2C_SCL_PIN);
#define I2C_SCL_0() GPIO_ResetBits(I2C_PORT, I2C_SCL_PIN);
#define I2C_SDA_1() GPIO_SetBits(I2C_PORT, I2C_SDA_PIN);
#define I2C_SDA_0() GPIO_ResetBits(I2C_PORT, I2C_SDA_PIN);
#define I2C_SDA_READ() GPIO_ReadInputDataBit(I2C_PORT, I2C_SDA_PIN);
2.2.2 模拟I2C实现
2.2.2.1 I2C_GPIO初始化
void i2c_gpio_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
i2c_stop();
}
2.2.2.2 模拟I2C总线延迟
// 模拟I2C总线位延迟,最快400KHz
static void i2c_delay(void)
{
uint8_t i;
for(i = 0; i < 10; i++);
}
2.2.2.3 MCU发送I2C起始和停止信号
// MCU发起I2C总线启动信号
void i2c_start(void)
{
// 当SCL高电平时,SDA出现一个下降沿表示I2C总线启动信号
I2C_SCL_1();
I2C_SDA_1();
i2c_delay();
I2C_SDA_0(); // 拉低SDA,产生一个启动信号
i2c_delay();
I2C_SCL_0(); // 拉低SCL,产生一个时钟
i2c_delay();
}
// MCU发起I2C总线停止信号
void i2c_stop(void)
{
// 当SCL高电平,SDA低电平时,SDA出现一个上升沿表示I2C总线停止信号
I2C_SCL_1();
I2C_SDA_0();
i2c_delay();
I2C_SDA_1(); // 拉高SDA,产生一个停止信号
}
根据I2C的工作时序来编写,代码注释也有讲解
2.2.2.4 MCU向I2C总线设备发送/接收8bit数据
// MCU向I2C总线设备发送8bit数据
void i2c_SendByte(uint8_t Byte)
{
uint8_t i;
for(i = 0; i < 8; i++ ) // 先发送高7位
{
if(Byte & 0x80) // 若最高位为1,则SDA为高电平
{
I2C_SDA_1();
}
else
{
I2C_SDA_0();
}
i2c_delay();
I2C_SCL_1();
i2c_delay();
I2C_SCL_0();
if(i == 7)
{
I2C_SDA_1(); // 释放总线
}
Byte <<= 1; // 左移一位
i2c_delay();
}
}
// MCU从I2C总线设备接收8bit数据
uint8_t i2c_ReadByte(void)
{
uint8_t i, value = 0;
for(i = 0; i < 8; i++)
{
value <<= 1; // 左移一位
I2C_SCL_1();
i2c_delay();
if (I2C_SDA_READ())
{
value++;
}
I2C_SCL_0();
i2c_delay();
}
return value;
}
2.2.2.5 读取器件的ACK应答信号
// 等待MCU产生一个时钟,读取器件的ACK应答信号
uint8_t i2c_WaitAck(void)
{
uint16_t STA;
I2C_SDA_1();
i2c_delay();
I2C_SCL_1();
i2c_delay();
if(I2C_SDA_READ())
{
STA = 1;
}
else
{
STA = 0;
}
I2C_SCL_0();
i2c_delay();
return STA;
}
2.2.2.6 MCU产生一个ACK/NACK信号
// MCU产生一个ACK信号
void i2c_Ack(void)
{
I2C_SDA_0();
i2c_delay();
I2C_SCL_1();
i2c_delay();
I2C_SCL_0();
i2c_delay();
I2C_SDA_1();
}
// MCU产生一个NACK信号
void i2c_Nack(void)
{
I2C_SDA_1();
i2c_delay();
I2C_SCL_1();
i2c_delay();
I2C_SCL_0();
i2c_delay();
}
2.2.2.7 判断设备是否存在
// 检测I2C总线设备,MCU发送设备地址,读取设备应答状况判断设备是否存在
uint8_t i2c_CheckDevice(uint8_t Device_Address)
{
uint8_t ucAck;
i2c_gpio_Init();
i2c_start();
i2c_SendByte(Device_Address | I2C_WR); // 发送设备地址+写位
ucAck = i2c_WaitAck(); // 等待ACK应答
i2c_stop();
return ucAck;
}
2.2.3 EEPROM基本操作函数
#include "I2C.h"
#include "i2c_gpio.h"
#include "usart.h"
// 判断串行EERPOM是否正常
uint8_t EEPROM_CheckOk(void)
{
if(i2c_CheckDevice(EEPROM_ADDR) == 0)
{
return 1;
}
else
{
i2c_stop(); // 失败后,切记发送I2C总线停止信号
return 0;
}
}
// 从串行EEPROM指定地址处开始读取若干数据
uint8_t EEPROM_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize) // 函数参数:读缓冲区指针,起始地址,数据长度
{
uint16_t i;
i2c_start(); // 第1步:发起I2C总线启动信号
// 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
i2c_SendByte(EEPROM_ADDR | I2C_WR); // 此处是写指令
if (i2c_WaitAck() != 0) // 第3步:等待ACK
{
goto cmd_fail; // EEPROM器件无应答
}
// 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址
i2c_SendByte((uint8_t)_usAddress);
if (i2c_WaitAck() != 0) // 第5步:等待ACK
{
goto cmd_fail;
}
// 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据
i2c_start();
// 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读
i2c_SendByte(EEPROM_ADDR | I2C_RD);
if (i2c_WaitAck() != 0) // 第8步:等待ACK
{
goto cmd_fail;
}
// 第9步:循环读取数据
for (i = 0; i < _usSize; i++)
{
_pReadBuf[i] = i2c_ReadByte(); // 读1个字节
// 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack
if (i != _usSize - 1)
{
i2c_Ack(); // 中间字节读完后,CPU产生ACK信号(驱动SDA = 0)
}
else
{
i2c_Nack(); // 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1)
}
}
i2c_stop();
return 1;
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
i2c_stop();
return 0;
}
// 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
uint8_t EEPROM_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize) // 函数参数:写缓冲区指针,起始地址,数据长度
{
uint16_t i,m;
uint16_t usAddr;
/*
写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
对于24xx02,page size = 8
简单的处理方法为:按字节写操作模式,每写1个字节,都发送地址
为了提高连续写的效率: 本函数采用page wirte操作。
*/
usAddr = _usAddress;
for (i = 0; i < _usSize; i++)
{
// 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址
if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
{
i2c_stop();
/* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms
CLK频率为200KHz时,查询次数为30次左右
*/
for (m = 0; m < 1000; m++)
{
i2c_start();
i2c_SendByte(EEPROM_ADDR | I2C_WR);
// 发送一个时钟,判断器件是否正确应答
if (i2c_WaitAck() == 0)
{
break;
}
}
if (m == 1000)
{
goto cmd_fail; // EEPROM器件写超时
}
i2c_SendByte((uint8_t)usAddr);
if (i2c_WaitAck() != 0)
{
goto cmd_fail; // EEPROM器件无应答
}
}
i2c_SendByte(_pWriteBuf[i]);
if (i2c_WaitAck() != 0)
{
goto cmd_fail;
}
usAddr++;
}
i2c_stop();
return 1;
cmd_fail: // 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备
i2c_stop();
return 0;
}
void EEPROM_Erase(void) // EEPROM擦除数据
{
uint16_t i;
uint8_t buf[EEPROM_SIZE];
for (i = 0; i < EEPROM_SIZE; i++)
{
buf[i] = 0xFF;
}
// 写EEPROM, 起始地址 = 0,数据长度为 256
if(EEPROM_WriteBytes(buf, 0, EEPROM_SIZE) == 0)
{
printf("擦除eeprom出错!\r\n");
return;
}
else
{
printf("擦除eeprom成功!\r\n");
}
}
2.2.4 EEPROM读写测试函数
// EEPROM测试函数
uint8_t EEPROM_Test(void)
{
uint16_t i;
uint8_t write_buf[EEPROM_SIZE];
uint8_t read_buf[EEPROM_SIZE];
if(EEPROM_CheckOk() == 0)
{
printf("没有找到EEPROM\r\n");
return 0;
}
for(i=0;i<EEPROM_SIZE;i++)
{
write_buf[i] = i;
}
if(EEPROM_WriteBytes(write_buf, 0, EEPROM_SIZE) == 0)
{
printf("EEPROM写入失败\r\n");
return 0;
}
else
{
printf("EEPROM写入成功\r\n");
}
ee_Delay(0x0FFFFF);
if(EEPROM_ReadBytes(read_buf, 0, EEPROM_SIZE) == 0)
{
printf("EEPROM读取失败\r\n");
return 0;
}
else
{
printf("EEPROM读取成功,数据如下:\r\n");
}
for(i=0;i<EEPROM_SIZE;i++)
{
if(read_buf[i]!= write_buf[i])
{
printf("0x%02X", read_buf[i]);
printf("错误,两次读取数据不一致\r\n");
return 0;
}
printf("0x%02X ", read_buf[i]);
if((i & 15) == 15)
{
printf("\r\n");
}
}
printf("EEPROM测试通过\r\n");
return 1;
}
2.2.5 主函数
int main()
{
LED_Init();
USART_Config();
LED_BLUE();
if(EEPROM_Test() == 1)
{
LED_GREEN();
}
else
{
LED_RED();
}
while(1)
{
}
}
3. 小结
模拟I2C就是模拟I2C的通讯过程,我们手动进行SDA、SCL拉高拉低,下面我们可以简单的进行复盘一下基本的I2C模拟:
3.1. 初始化GPIO
确保GPIO引脚被配置为推挽输出模式(SCL和SDA)和上拉模式(如果需要)。
3.2. 产生起始条件
将SDA从高电平拉到低电平时,SCL必须保持高电平。
void I2C_Start(void) {
GPIO_SetBits(GPIOx, SDA_PIN); // 确保SDA高电平
GPIO_SetBits(GPIOx, SCL_PIN); // 确保SCL高电平
Delay(); // 确保稳定性
GPIO_ResetBits(GPIOx, SDA_PIN); // 将SDA拉低
Delay();
GPIO_ResetBits(GPIOx, SCL_PIN); // 将SCL拉低
}
3.3 产生停止条件
将SDA从低电平拉到高电平时,SCL必须保持高电平。
void I2C_Stop(void) {
GPIO_ResetBits(GPIOx, SDA_PIN); // 确保SDA低电平
GPIO_SetBits(GPIOx, SCL_PIN); // 确保SCL高电平
Delay(); // 确保稳定性
GPIO_SetBits(GPIOx, SDA_PIN); // 将SDA拉高
Delay();
}
3.4 写入位数据
将数据位写到SDA线,SCL线拉高以确认数据位。
void I2C_WriteBit(uint8_t bit) {
if (bit) {
GPIO_SetBits(GPIOx, SDA_PIN); // 写入高电平
} else {
GPIO_ResetBits(GPIOx, SDA_PIN); // 写入低电平
}
GPIO_SetBits(GPIOx, SCL_PIN); // 拉高SCL以读取数据
Delay();
GPIO_ResetBits(GPIOx, SCL_PIN); // 拉低SCL以准备下一位
}
3.5 读取位数据
将SDA线配置为输入,并在SCL线拉高时读取数据。
uint8_t I2C_ReadBit(void) {
GPIO_SetBits(GPIOx, SCL_PIN); // 拉高SCL以读取数据
Delay();
uint8_t bit = GPIO_ReadInputDataBit(GPIOx, SDA_PIN); // 读取SDA线的值
GPIO_ResetBits(GPIOx, SCL_PIN); // 拉低SCL以准备下一位
return bit;
}
3.6 写入字节
写入一个字节,通过逐位调用 I2C_WriteBit
函数。
void I2C_WriteByte(uint8_t byte) {
for (int i = 0; i < 8; i++) {
I2C_WriteBit((byte & 0x80) >> 7); // 写入最高位
byte <<= 1; // 移动到下一位
}
}
3.7 读取字节
读取一个字节,通过逐位调用 I2C_ReadBit
函数。
uint8_t I2C_ReadByte(void) {
uint8_t byte = 0;
for (int i = 0; i < 8; i++) {
byte <<= 1; // 移动到下一位
byte |= I2C_ReadBit(); // 读取当前位
}
return byte;
}
2024.9.4 第一次修订,后期不再维护
2025.1.17 优化了一下程序和正文结构,便于阅读
本文作者:hazy1k
本文链接:https://www.cnblogs.com/hazy1k/p/18396000
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步