四汐 发表于 前天 22:36

I2C协议基础及应用-AT24Cxx篇(支持任意型号读写)



【虹星宝典】记录单片机学习之旅——目录


    I2C协议,也称iic协议。常用于短距离、低速设备间的数据传输,是有飞利浦(NXP)开发的同步串行通信协议,仅需两根线(SDA:数据线;SCL:时钟线)即可进行多设备的半双工互联通信。
    基本的通讯时序分为:起始信号(IIC_Start)、结束信号(IIC_Stop)、发送ACK应答信号(IIC_SendACK)、发送NACK应答信号(IIC_SendNoACK)、接收ACK/NACK应答信号(IIC_WaitACK)和读/写1字节数据,并可以衍生出读/写N字节数据等。
    本篇作为IIC协议入门实验的器件是AT24C128(实验箱上为AT24C02)。



一、IIC部分
【时刻周期】
以SCL的脉冲信号为标准(这个可以决定IIC的通讯速率)。
当SCL=0时,SDA的电平无效,此时可以随意改变SDA的电平,即从蓝色到黄色这段时间。
当SCL=1时,SDA的电平需要保持稳定,持续到再一次SCL=0的瞬间(及SCL维持信号稳定的一小段时间),此时数据有效,将被接收方读取,
即从黄色到红色这段时间。一个SCL周期,传输1bit数据。
void IIC_Wait(void)
{
    Delay2us();
}




【起始信号】和【结束信号】
起始信号:先拉低SDA总线,再拉低SCL总线
void IIC_Start(void)
{
    IIC_SCL = 1;
    IIC_SDA = 1;
    IIC_Wait();
    IIC_SDA = 0;
    IIC_Wait();
    IIC_SCL = 0;
    IIC_Wait();
}

结束信号:先拉高SCL总线,再拉高SDA总线
void IIC_Stop(void)
{
    IIC_SCL = 0;
    IIC_SDA = 0;
    IIC_Wait();
    IIC_SCL = 1;
    IIC_Wait();
    IIC_SDA = 1;
    IIC_Wait();
}




【发送ACK应答信号】和【发送NACK应答信号】
发送ACK信号:1位数据,SDA=0
void IIC_SendACK(void)
{
    IIC_SDA = 0;
    IIC_Wait();
    IIC_SCL = 1;
    IIC_Wait();
    IIC_SCL = 0;
    IIC_Wait();
}

发送NACK信号:1位数据,SDA=1
void IIC_SendNoACK(void)
{
    IIC_SDA = 1;
    IIC_Wait();
    IIC_SCL = 1;
    IIC_Wait();
    IIC_SCL = 0;
    IIC_Wait();
}




【接收ACK/NACK应答信号】
接收和发送原理一致。区别是作为接收方。
u8 IIC_WaitACK(void)
{
    u8 ack = 0;
    IIC_SDA = 1;
    IIC_Wait();
    IIC_SCL = 1;
    IIC_Wait();
    ack = IIC_SDA;// 0:ACK 1:NACK
    IIC_Wait();
    IIC_SCL = 0;
    IIC_Wait();
    return !ack;    // 取反后 1:有应答 0:无应答
}

【读/写1字节数据】
一般8bit数据+1bitACK信号作为一包数据。
u8 IIC_R_Byte(void)
{
    u8 i = 8, dat = 0;
    IIC_SDA = 1;
    IIC_Wait();
    do
    {
      IIC_SCL = 1;
      IIC_Wait();
      dat <<= 1;
      if (IIC_SDA)
            dat |= 1;
      IIC_SCL = 0;
      IIC_Wait();
    } while (--i);

    return dat;
}




void IIC_W_Byte(u8 dat)
{
    u8 i = 8;
    do
    {
      if (dat & 0x80)
            IIC_SDA = 1;
      else
            IIC_SDA = 0;
      IIC_Wait();
      dat <<= 1;
      IIC_SCL = 1;
      IIC_Wait();
      IIC_SCL = 0;
      IIC_Wait();
    } while (--i);
}




二、AT24Cxx部分
主要用途
AT24CXX是一种EEPROM存储器,主要用于失去电源时,仍可以储存处理器的重要数据。使用I2C协议进行数据通信,具有宽电压(约1.7V to 5.5V)


从机地址

AT24CXX最多有3个从机地址配置引脚(A0/A1/A2),即最多挂载8个从机。
        01 = A0/A1/A2        *08-byte Page Write mode*
        02 = A0/A1/A2        *08-byte Page Write mode*
        04 = NC/A1/A2        *16-byte Page Write Mode*
        08 = NC/NC/A2        *16-byte Page Write Mode*
        16 = NC/NC/NC        *16-byte Page Write Mode*
        32 = A0/A1/A2        *32-byte Page Write Mode*
        64 = A0/A1/A2        *32-byte Page Write Mode*
        128 = A0/A1/A2        *64-byte Page Write Mode*
        256 = A0/A1/A2        *64-byte Page Write Mode*
        512 = A0/A1/A2        *128-byte Page Write Mode*
        1024 = NC/A1/NC       



命名含义

其中XX可代表存储的容量大小,一般为XX*128byte:
        01 = 128 Byte = 1K bit
        02 = 256 Byte = 2K bit
        04 = 512 Byte = 4K bit
        08 = 1024 Byte = 8K bit
        16 = 2048 Byte = 16K bit
        32 = 4096 Byte = 32K bit
        64 = 8192 Byte = 64K bit
        128 = 16384 Byte = 128K bit
        256 = 32768 Byte = 256K bit
        512 = 65536 Byte = 512K bit
        1024 = 131072 Byte = 1M bit



数据读写

每个数据地址可以保存1Byte数据(即char/u8类型)。
一般读写的协议格式为:(具体请看对应的手册)
​        对于01/02/04/08/16为【从机地址】【数据地址】【数据】....
​        对于32/64/128/256.等为【从机地址】【数据地址高位】【数据地址低位】【数据】....
由于1字节(8bit)最多描述2^8=256个,所以01(128)/02(256)有3个地址配置引脚可用。
04(512),需要9bit,所有只有2个地址配置引脚,而将【从机地址】中对应的NC位作为第9bit
08(1024),需要10bit,同理2个NC位作为第9/10bit
16(2048),需要11bit,3个地址配置引脚均为NC。
而从32(4096)开始,【数据地址】使用2Byte(16bit)表示,最多可以描述2^16=65536个,因此不需要向地址配置引脚借位。1024(131072)需要借1位,当手册中有2个NC位,这个用的少不深入讨论了。
注:AT24Cxx每次连续写入,最多写1页数据,便要等AT24Cxx完成1次擦写周期,一般为5-10ms。



u8 AT24Cxx_R_Byte(u32 AT24Cxx_Type, u8 AT24Cxx_id, u16 AT24Cxx_addr)
{
        u8 R_Byte;
        if (AT24Cxx_addr > AT24Cxx_Type)
                return 0xFF;

        // 1字节地址长度
    if (AT24Cxx_Type <= AT24C16)
        {
                IIC_R_NByte(AT24Cxx_id | (u8)((AT24Cxx_addr >> 8) << 1), AT24Cxx_addr, IIC_REG_TYPE_1Byte, &R_Byte, 1);
        }
        // 2字节地址长度
        else
        {
                IIC_R_NByte(AT24Cxx_id, AT24Cxx_addr, IIC_REG_TYPE_2Byte, &R_Byte, 1);
        }
        return R_Byte;
}

void AT24Cxx_R_NByte(u32 AT24Cxx_Type, u8 AT24Cxx_id, u16 AT24Cxx_addr, u8 *R_buff, u32 num)
{
        if ((AT24Cxx_addr + num - 1) > AT24Cxx_Type)
                return;

        // 1字节地址长度
    if (AT24Cxx_Type <= AT24C16)
        {
                IIC_R_NByte(AT24Cxx_id | (u8)((AT24Cxx_addr >> 8) << 1), AT24Cxx_addr, IIC_REG_TYPE_1Byte, R_buff, num);
        }
        // 2字节地址长度
        else
        {
                IIC_R_NByte(AT24Cxx_id, AT24Cxx_addr, IIC_REG_TYPE_2Byte, R_buff, num);
        }
}

#define Write_Cycle_Time 400        // 400us

void AT24Cxx_W_Byte(u32 AT24Cxx_Type, u8 AT24Cxx_id, u16 AT24Cxx_addr, u8 W_Byte)
{
        if (AT24Cxx_addr > AT24Cxx_Type)
                return ;

        // 1字节地址长度
    if (AT24Cxx_Type <= AT24C16)
        {
                IIC_W_NByte(AT24Cxx_id | (u8)((AT24Cxx_addr >> 8) << 1), AT24Cxx_addr, IIC_REG_TYPE_1Byte, &W_Byte, 1);
        }
        // 2字节地址长度
        else
        {
                IIC_W_NByte(AT24Cxx_id, AT24Cxx_addr, IIC_REG_TYPE_2Byte, &W_Byte, 1);
        }
        delay_us(Write_Cycle_Time);        // 等待一段写周期 一般为5-10ms 测试最短为400-500us
}

void AT24Cxx_W_NByte(u32 AT24Cxx_Type, u8 AT24Cxx_id, u16 AT24Cxx_addr, u8 *W_buff, u32 num)
{
        u16 addr_offset = 0;        // 地址偏移量
        u16 page = 0;                        // 计算要写多少页
        u8 Remainder = 0;                // 不满1页
       
        if ((AT24Cxx_addr + num - 1) > AT24Cxx_Type)
                return;

        switch (AT24Cxx_Type)
        {
        // 08-byte Page Write mode
        case AT24C01:
        case AT24C02:
                for (page = 0; page < (num / 8); page++)
                {
                        addr_offset = AT24Cxx_addr + page * 8;
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_1Byte, &W_buff, 8);
                        delay_us(Write_Cycle_Time);
                }
                addr_offset = AT24Cxx_addr + page * 8;
                Remainder = num % 8;
                // 如果有空余数据
                if (Remainder)
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_1Byte, &W_buff, Remainder);
                break;
        // 16-byte Page Write mode
        case AT24C04:
        case AT24C08:
        case AT24C16:
                for (page = 0; page < (num / 16); page++)
                {
                        addr_offset = AT24Cxx_addr + page * 16;
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_1Byte, &W_buff, 16);
                        delay_us(Write_Cycle_Time);
                }
                addr_offset = AT24Cxx_addr + page * 16;
                Remainder = num % 16;
                // 如果有空余数据
                if (Remainder)
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_1Byte, &W_buff, Remainder);
                break;
        // 32-byte Page Write mode
        case AT24C32:
        case AT24C64:
                for (page = 0; page < (num / 32); page++)
                {
                        addr_offset = AT24Cxx_addr + page * 32;
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, 32);
                        delay_us(Write_Cycle_Time);
                }
                addr_offset = AT24Cxx_addr + page * 32;
                Remainder = num % 32;
                // 如果有空余数据
                if (Remainder)
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, Remainder);
                break;
        // 64-byte Page Write mode
        case AT24C128:
        case AT24C256:
                for (page = 0; page < (num / 64); page++)
                {
                        addr_offset = AT24Cxx_addr + page * 64;
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, 64);
                        delay_us(Write_Cycle_Time);
                }
                addr_offset = AT24Cxx_addr + page * 64;
                Remainder = num % 64;
                // 如果有空余数据
                if (Remainder)
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, Remainder);
                break;
        // 128-byte Page Write mode
        case AT24C512:
                for (page = 0; page < (num / 128); page++)
                {
                        addr_offset = AT24Cxx_addr + page * 128;
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, 128);
                        delay_us(Write_Cycle_Time);
                }
                addr_offset = AT24Cxx_addr + page * 128;
                Remainder = num % 128;
                // 如果有空余数据
                if (Remainder)
                        IIC_W_NByte(AT24Cxx_id | (u8)((addr_offset >> 8) << 1), addr_offset, IIC_REG_TYPE_2Byte, &W_buff, Remainder);
                break;
        default:
                break;
        }
        delay_us(Write_Cycle_Time);
}

三、实验结果

简单搞了一个串口读写和擦除的指令。
W(写指令):往地址0-254写入0-254,对于AT24C02(ai8051u实验箱)的地址255是校验码。
R(读指令):把地址0-255的数据读出来
E(擦除指令):往地址0-254的数据写0

// 写 W(0x57)
      if (RX1_Buffer == 'W')
      {
            // at24c02 实验箱
            // Uart1_printf("开始写入\r\n");
            // for ( i = 0; i < 128; i++)
            // {
            //   TX1_Buffer = i;
            // }
            // AT24Cxx_W_NByte(AT24C02, AT24C02_ID1, 0, TX1_Buffer, 128);
            // for ( i = 128; i < 255; i++)
            // {
            //   TX1_Buffer = i;
            // }
            // AT24Cxx_W_NByte(AT24C02, AT24C02_ID1, 128, TX1_Buffer, 127);
            // Uart1_printf("写入完成\r\n");
            
            // at24128
            Uart1_printf("开始写入\r\n");
            for ( i = 0; i < 128; i++)
            {
                TX1_Buffer = i;
            }
            AT24Cxx_W_NByte(AT24C128, AT24C128_ID1, 0, TX1_Buffer, 128);
            for ( i = 128; i < 255; i++)
            {
                TX1_Buffer = i;
            }
            AT24Cxx_W_NByte(AT24C128, AT24C128_ID1, 128, TX1_Buffer, 127);
            Uart1_printf("写入完成\r\n");
      }
      // 读 R(0x52)
      else if (RX1_Buffer == 'R')
      {
            // at24c02 实验箱
            // for ( i = 0; i < 96; i++)
            // {
            //   TX1_Buffer = AT24Cxx_R_Byte(AT24C02, AT24C02_ID1, i);
            // }
            // Uart1_Send_NByte(TX1_Buffer, 96);
            // for ( i = 0; i < 96; i++)
            // {
            //   TX1_Buffer = AT24Cxx_R_Byte(AT24C02, AT24C02_ID1, 96 + i);
            // }
            // Uart1_Send_NByte(TX1_Buffer, 96);
            // for ( i = 0; i < 64; i++)
            // {
            //   TX1_Buffer = AT24Cxx_R_Byte(AT24C02, AT24C02_ID1, 192 + i);
            // }
            // Uart1_Send_NByte(TX1_Buffer, 64);

            // at24c128
            for ( i = 0; i < 96; i++)
            {
                TX1_Buffer = AT24Cxx_R_Byte(AT24C128, AT24C128_ID1, i);
            }
            Uart1_Send_NByte(TX1_Buffer, 96);
            for ( i = 0; i < 96; i++)
            {
                TX1_Buffer = AT24Cxx_R_Byte(AT24C128, AT24C128_ID1, 96 + i);
            }
            Uart1_Send_NByte(TX1_Buffer, 96);
            for ( i = 0; i < 64; i++)
            {
                TX1_Buffer = AT24Cxx_R_Byte(AT24C128, AT24C128_ID1, 192 + i);
            }
            Uart1_Send_NByte(TX1_Buffer, 64);
      }
      // 擦除 E(0x45)
      else if (RX1_Buffer == 'E')
      {
            // at24c02 实验箱
            // Uart1_printf("开始擦除\xfd\r\n");
            // for ( i = 0; i < 0xff; i++)
            // {
            //   AT24Cxx_W_Byte(AT24C02, AT24C02_ID1, i, 0);
            //   delay_us(500);
            // }
            // Uart1_printf("擦除\xfd完成\r\n");

            // at24c128
            Uart1_printf("开始擦除\xfd\r\n");
            for ( i = 0; i < 0xff; i++)
            {
                AT24Cxx_W_Byte(AT24C128, AT24C128_ID1, i, 0);
                delay_us(500);
            }
            Uart1_printf("擦除\xfd完成\r\n");
      }


结果如下:
先读一下,数据全为0。


再执行写命令

再读一下,数据正常

擦除指令

再读一下,数据已全部擦除



番外-有趣的实验

实验箱中测试2.5M速率和50K速率。从发送读取指令,到第1次接收串口反馈的时间差。约读96字节数据

2.5M:约20ms
50K:约90ms





好奇又测了几组:
1M,约20ms




500K,约20ms



400K,约20ms



250K,约30ms



125K,约40ms



100K,约50-60ms



50K,约90-100ms



结果表明,400K大约就是最大速率了,再高也会因为从机跟不上而限速了。


如何使用?

config.h里选择软硬件模式



iic.h里修改引脚定义,注意修改GPIO初始化



主函数初始化GPIO和IIC即可。
具体任务在task_uart1_polling.c中
















国芯人工智能 发表于 前天 22:38

在嵌入式系统与微控制器应用中,I²C(Inter-Integrated Circuit)协议因其结构简单、通信稳定、支持多主多从架构等优点,被广泛应用于各类低速外设的通信场景。AT24Cxx系列EEPROM芯片作为典型的I²C接口器件,常用于数据存储、配置信息保存等应用场景。本文将围绕I²C协议的基本原理、通信时序及在AT24Cxx系列芯片上的具体应用进行详细解析,并提供通用读写函数的设计思路,以支持任意型号的AT24Cxx芯片。

一、I²C协议基础

1.1 协议概述

I²C协议是由飞利浦公司(现NXP)提出的一种同步串行通信协议,主要用于短距离、低速设备之间的数据传输。其核心特点包括:
双线制通信:仅需SDA(数据线)和SCL(时钟线)两根信号线;
半双工通信:同一时刻只能单向传输;
多主多从架构:支持多个主设备和多个从设备连接在同一总线上;
硬件开漏结构:需外接上拉电阻,确保信号稳定;
地址识别机制:每个从设备具有唯一地址,主设备通过地址选择通信对象;
应答机制:通信过程中通过ACK/NACK进行数据确认,提高通信可靠性。

1.2 通信时序

I²C通信由起始信号、数据传输、应答机制和结束信号组成,具体时序如下:

1.2.1 起始信号(Start)
SCL为高电平时,SDA由高变低,表示通信开始。

1.2.2 结束信号(Stop)
SCL为高电平时,SDA由低变高,表示通信结束。

1.2.3 数据传输
每次传输1字节(8位),高位(MSB)先传;
每字节后必须跟随一个应答位(ACK/NACK)。

1.2.4 应答机制(ACK/NACK)
ACK:接收方在第9个SCL周期将SDA拉低,表示接收成功;
NACK:SDA保持高电平,表示接收失败或数据结束。

1.2.5 重复起始信号(Repeated Start)
在不发送Stop信号的情况下,再次发送Start信号,用于切换读写方向或访问不同设备。

二、AT24Cxx系列EEPROM简介

AT24Cxx是Atmel(现Microchip)推出的一系列I²C接口的EEPROM存储芯片,常见的型号包括AT24C01(1Kbit)、AT24C02(2Kbit)、AT24C128(128Kbit)等。其主要特性如下:
支持I²C总线接口,频率支持100kHz/400kHz;
宽电压范围(1.7V~5.5V);
内部页写缓冲(Page Write)功能;
高达1百万次擦写寿命;
数据保持时间可达100年;
支持硬件地址选择,最多可挂载多个芯片。

2.1 地址格式

AT24Cxx的从机地址为7位,其中高4位固定为1010,接下来的3位为硬件地址引脚A2、A1、A0的电平组合。例如:

地址格式 = 1 0 1 0 A2 A1 A0
当A2=0, A1=0, A0=0时,地址为0xA0(写)和0xA1(读);
实际通信中,地址的最低位表示读写方向(0为写,1为读)。

2.2 读写操作流程

2.2.1 写操作

1. 发送Start信号;
2. 发送从机地址(写地址);
3. 等待ACK;
4. 发送存储地址(高字节可选);
5. 继续发送数据;
6. 每发送一个字节等待ACK;
7. 发送Stop信号;
8. 延迟10ms(确保写入完成)。
注意:AT24Cxx支持页写操作,一页大小为16/32/64字节(依型号而定),若写入数据超过页边界,将覆盖当前页开头数据。

2.2.2 读操作

1. 发送Start信号;
2. 发送从机地址(写地址);
3. 等待ACK;
4. 发送存储地址(高字节可选);
5. 再次发送Start信号(Re-Start);
6. 发送从机地址(读地址);
7. 接收数据;
8. 每接收一个字节发送ACK/NACK;
9. 发送Stop信号。

三、通用读写函数设计(适用于任意AT24Cxx型号)

为实现对任意AT24Cxx芯片的兼容读写,函数设计需具备以下特性:
可配置从机地址;
支持高位地址(如AT24C128/256);
自动处理页写边界;
提供字节读写与多字节读写接口。

3.1 写函数(单字节写)

c
void AT24CXXWriteByte(uint8t devAddr, uint16t memAddr, uint8t data)
{
    IICStart();
    IICSendByte(devAddr > 8)); // 发送高地址
      IICWaitACK();
    }

    IICSendByte((uint8t)memAddr);    // 发送低地址
    IICWaitACK();

    IICSendByte(data);                // 发送数据
    IICWaitACK();

    IICStop();
    DelayMs(10);                     // 等待写入完成
}

3.2 读函数(单字节读)

```c
uint8t AT24CXXReadByte(uint8t devAddr, uint16t memAddr)
{
    uint8t data;

    IICStart();
    IICSendByte(devAddr > 8));
      IICWaitACK();
    }

    IICSendByte((uint8t)memAddr);
    IICWaitACK();

    IICStart();
    IICSendByte((devAddr

[本文内容由国芯人工智能辅助生成,仅供参考]
页: [1]
查看完整版本: I2C协议基础及应用-AT24Cxx篇(支持任意型号读写)