人若无名 便可潜心练剑.|

hazy1k

园龄:7个月粉丝:14关注:0

2024-09-04 10:43阅读: 63评论: 0推荐: 0

第19章 模拟I2C

第十九章 模拟I2C

1. 硬件设计

我们使用GPIO来模拟I2C,无其他硬件资源

2. 软件设计

2.1 编程大纲

  1. 模拟I2C宏定义配置

  2. 根据时序模拟I2C

  3. 沿用上一节的EEPROM读写代码

  4. 主函数测试

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 中国大陆许可协议进行许可。

posted @   hazy1k  阅读(63)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起