RYMCU

NO14.I2C总线与AT24C256

devcui 3 年前
# nebula-vscode

所属作品集

1.什么是总线

两线式通信线路

  • SCL(clock) 时钟线
  • SDA(data) 数据线

1.1 总线应用图

image.png

所有基于 I2C 总线的外围
器件都是在这五种底层信号的基础上进行数据的读写,这五中信号分别是:

  • 1.起始信号
  • 2.停止信号
  • 3.写字节信号
  • 4.读字节并发送应答信号
  • 5.读字节并发送非应答信号

1.2 起始停止信号

image.png

  • 1.起始信号: SDA 下降沿在 SCL 下降沿之前
  • 2.结束信号: SDA 上升沿在 SCL 上升沿之后
SBIT(SCL_I2C, _P0, 1); //总线管脚定义
SBIT(SDA_I2C, _P0, 2);

// 模拟延时
void Delay_I2C(void)
{
    NOP();
    NOP();
    NOP();
    NOP();
}

void Start_I2C(void)
{
    // 准备阶段
    SCL_I2C = 0;
    SDA_I2C = 1;
    Delay_I2C();
    SCL_I2C = 1;
    Delay_I2C();
    // 先将SDA拉低
    SDA_I2C = 0;
    Delay_I2C();
    // 再将SCL拉低
    SCL_I2C = 0;
    Delay_I2C();
}

void Stop_I2C(void)
{
    // 准备阶段
    SCL_I2C = 0;
    SDA_I2C = 0;
    Delay_I2C();
    // 拉高SCL
    SCL_I2C = 1;
    Delay_I2C();
    // 拉高SDA
    SDA_I2C = 1;
    Delay_I2C();
    SCL_I2C = 0;
}

1.3 写字节信号

image.png

如图所示

  • 1.bit 位倒序
  • 2.第九位为应答位

image.png

时序图

  • 1.提前把数据装入 SDA
  • 2.SCL 高位期间写数据
unsigned char Wr_I2C(unsigned char dat)
{
    // 存储应答位
    unsigned char ack;
    // 探测字节内某一位的掩饰码变量
    unsigned char mask;
    // 高位到低位
    // 0x80 = 0b10000000
    // 0x80 >>1 = 0b0100 0000 = 0x40
    // 0x80 >>2 = 0b0010 0000 = 0x20
    // 0x80 >>7 = 0b0000 0001 = 0x01
    // 0x80 >>8 = 0b0000 0000 = 0x00
    for (mask = 0x80; mask != 0; mask >>= 1)
    {
        // 假设dat = 0b1010 1010
        // 0b1000 0000 & 0b1010 1010 = 0b1000 0000
        // 0b0100 0000 & 0b1010 1010 = 0b0000 0000
        // 0b0010 0000 & 0b1010 1010 = 0b0010 0000
        // 查看当前数据位置上是否为0或者不为0
        if ((mask & dat) == 0)
        {
            SDA_I2C = 0;
        }
        else
        {
            SDA_I2C = 1;
        }
        // 延时
        Delay_I2C();
        // 拉高SCL,准备写数据
        SCL_I2C = 1;
        // 延时
        Delay_I2C();
        // 拉低SCL,一位数据写入完成
        // 进入下一次循环后mask右移,探测第二位继续写入
        SCL_I2C = 0;
    }
    // 主机释放总线
    SDA_I2C = 1;
    Delay_I2C();
    // 准备获取应答位
    SCL_I2C = 1;
    // 获取ack应答位
    ack = SDA_I2C;
    Delay_I2C();
    // 获取应答位结束
    SCL_I2C = 0;
    return ack;
}

1.4 读字节并发送应答信号

image.png
与写字节基本相同,

  • 1.不同的是 bit7-bit0 由 I2C 上绑定的器件写出
  • 2.SCL 高电平器件我们去读取 SDA 的数据
  • 3.第九位由主机给出,ack=0 表示继续读,ack=1 表示不读了

1.5 读字节并发送非应答信号

// 参数 ack 给0表示SDA拉低,继续读
// 给1表示SDA拉高,不读了
unsigned char RdACK_I2C(unsigned char ack)
{
    unsigned char mask;
    unsigned char dat;
    // 确保主机释放SDA
    SDA_I2C = 1;
    for (mask = 0x80; mask != 0; mask >>= 1)
    {
        // 延时
        Delay_I2C();
        // 拉高SCL准备读取SDA数据
        SCL_I2C = 1;
        if (SDA_I2C == 0)
        {
            // 比如需要读取的数据为0b0101 0101
            // 第一次循环
            // ~mask = ~0b1000 0000 = 0b0111 1111
            // 读取 0
            // 0b0000 0000 & 0b0111 1111 = 0b0000 0000
            // 假如SDA=0 那么 mask反一下当前位为0,其他位为1
            // & 运算符可以让dat保持当前位不变的情况下 将SDA的0 给dat
            dat &= ~mask;
        }
        else
        {
            // 第二次循环
            // mask = 0b0100 0000 
            // 读取1
            // 0b0000 0000 | 0b0100 0000 = 0b0100 0000
            // 整体大概意思就是,每次mask向右走一位
            // 假如SDA=1 那么 mask当前位为1,无论dat当前位是否为1
            // | 运算符可以让dat保持不变的情况下将当SDA前位的1 给dat
            dat |= mask;
        }
        // 延时
        Delay_I2C();
        // 拉低SCL完成1位读取
        SCL_I2C = 0;
    }
    // 8位传递完成后,第九位ack,由参数传递
    SDA_I2C = ack;
    // 延时
    Delay_I2C();
    // 拉高SCL准备发送第九位ack
    SCL_I2C = 1;
    // 延时
    Delay_I2C();
    // 发送SDA ack完毕
    SCL_I2C = 0;
    return dat;
}

将上面的函数封装成头文件样式

image.png

image.png

1.6 一次通讯时序

image.png

  • 1.这是一张完整的时序图
  • 2.起始信号-> 一字节的读取或写入-> 结束信号
  • 3.在起始信号与停止信号之间的读取或写入操作,与 I2C 器件本身的通信协议有关
  • 4.接下来了解一下基于 I2C 总线通信技术的 E2PROM 存储器 AT24C256 的通信协议和使用例子

2.E2PROM

image.png

如图,这是 AT24C256 器件,下图为线路图

image.png

由图可知

    1. 除去 VCC 和 GND,SCL,SDA 还剩下 A0,A1,A2,WP 四个 IO
    1. A0-A2 为地址输入引脚,每一个 AT24C256 可以设置一个独立器件,通过 A0-A2 的高低电平来设置,单片机通过这个器件选址来区分挂在总线上的 AT24C256
    1. SDA,SCL 连接到了 P01,P02 上
    1. WP 为写保护,WP=1 时不可以写数据,WP=0 可写可读

AT24C256 容量为 32768Byte,即 0x8000Byte,器件内每个 8 位空前都有地址值
最大为 0x8000-1 = 0x7FFF,这个地址需要两个字节存储,高位为 FIRSTWORD ADDRESS,低位为 SECOND WORD ADDRESS

3.AT24T256

3.1 写入

image.png

由上图可知:

  • 1.写入过程由 START 起始信号开始
  • 2.经过 DEVICE ADDRESS(写入元器件地址)
  • 3.写入存储地址 高位(FIRSTWORD ADDRESS)
  • 4.写入存储地址 低位(SECONDWORD ADDRESS)
  • 5.写入数据 DATA
  • 6.写入应答位 ACK
  • 7.写入 STOP 结束地址

当 I2C 总线上挂载多个元器件时,单片机通过 Device Address 设备地址来区分器件
我们开发板上 AT24C256 器件地址如下图所示

image.png

  • 1.高四位固定 1010
  • 2.后四位由 A2-A0,以及 R/W 决定,RYMCU-51 默认 A2-A0 接地,所以地址为 1010000 R/W
  • 3.R/W = Read/Write R/W=1 为 read,R/W=0 为 Write
  • 4.综上所述 最后得出地址为 1010 000 0 最后设备地址 0xA0
  • 5.第三步为存储器地址,0x0000~0x7FFF 任意写吧
void WrByte_AT24C256(unsigned int addr, unsigned char dat)
{
    // 第一步START信号
    Start_I2C();    
    // 写入设备地址 0b1010 0000 = 0xA0
    Wr_I2C(0xA0);
    // 写入高位地址,传入16位地址
    Wr_I2C(addr >> 8);
    // 写入低位地址,由于一次只能写入8位,所以前8位直接被过掉了
    Wr_I2C(addr);
    // 写入数据
    Wr_I2C(dat);
    // 最后一步STOP信号
    Stop_I2C();
}

3.2 读取

image.png

如图可知

  • 1.读取第一步开始信号
  • 2.元器件地址,写地址
  • 3.高位地址
  • 4.低位地址
  • 5.起始信号
  • 6.元器件地址,读地址
  • 7.读取 1byte 数据,发送非应答信号
  • 8.停止信号
unsigned char RdByte_AT24C256(unsigned int addr)
{
    unsigned char dat;
    // 开始信号
    Start_I2C();
    Wr_I2C(0xA0);
    Wr_I2C(addr >> 8);
    Wr_I2C(addr);
    // 开始信号
    Start_I2C();
    // 1010 0000 -> 1010 0001 = 0xA1
    Wr_I2C(0xA1);
    //  从addr读取数据发送非应答信号
    dat = RdACK_I2C(1);
    // 结束信号
    Stop_I2C();
    return dat;
}

封装

image.png

4.i2c 和 AT24C256 应用

/*
 * @Author: cuihaonan
 * @Email: devcui@outlook.com
 * @Date: 2021-04-04 19:56:15
 * @LastEditTime: 2021-04-04 21:10:28
 * @LastEditors: cuihaonan
 * @Description: 将数据由I2C写入AT24C26,然后读取AT24C26的数据并显示在1602上
 * @FilePath: /sdcc-include/src/i2c/main.c
 * @LICENSE: NONE
 */

#include "../../include/STC89xx.h"
#include "./include/1602.h"
#include "./include/I2C.h"
#include "./include/AT24C256.h"


void delayms(unsigned int z){
    unsigned int x,y;
    for(x=z;x>0;x--){
        for(y=78;y>0;y--);
    }
}

void main()
{
    unsigned char d = 0;
    unsigned char dat[10] = "";
    Init_1602();
    WrByte_AT24C256(0x0000, 1);
    Disp_1602_str(1, 2, "ACT24C0X TEST!");
    delayms(10);
    d = RdByte_AT24C256(0x0000);
    dat[0] = d / 100 + '0';
    dat[1] = d % 100 / 10 + '0';
    dat[2] = d % 10 + '0';
    Disp_1602_str(2, 3, dat);
    while (1)
        ;
}

5.at24c256 多字节通信

AT24C256 提供了另一种读写模式,页模式

32768bytes 分为 512 页,每页 64bytes
第一页范围为 0x0000~0x0040 依次递增

下图为页模式的通信时序图

image.png

  • 1.START
  • 2.DEVICE ADDRESS
  • 3.FIRST WORD ADDRESS
  • 4.SECOND WORD ADDRESS
  • 5.DATA
  • 6.STOP
WrStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
    // 检测上一次是否写完了,如果越页了,那么继续写
    while (len > 0)
    {
        // 循环检测元器件应答信号
        while (1)
        {
            Start_I2C();
            // 如果ack === 0 跳出,进行下面的写入
            if (0 == Wr_I2C(0xA0))
            {
                // 跳出循环
                break;
            }
            // 否则结束
            Stop_I2C();
        }
        // 高位
        Wr_I2C(addr >> 8);
        // 低位
        Wr_I2C(addr);
        // 开始写
        while (len > 0)
        {
            // 写一个字节,指针指向下一个自负
            Wr_I2C(*str++);
            // 长度--
            len--;
            // 存储地址+1
            addr++;
            // 是否达到了下一页
            if (0 == (addr % 64))
            {
                // 上一个字节到本页的边界
                // 跳出停止继续写
                break;
            }
        }
        Stop_I2C();
    }
}

image.png

读取类似前面说的,唯一不同的是,读多字节,ack=0,要发送应答信号表示我还需要继续读。

void RdStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
    // 循环检测ack是否为1
    while (1)
    {
        Start_I2C();
        // 如果为0跳出进行读取
        if (0 == Wr_I2C(0xA0))
        {
            break;
        }
        Stop_I2C();
    }
    // 高低位
    Wr_I2C(addr >> 8);
    Wr_I2C(addr);
    // 第二个Start信号
    Start_I2C();
    // 现在是读
    Wr_I2C(0xA1);
    // 如果长度大于1
    while (len > 1)
    {
        // 读取,应答为0
        *str++ = RdACK_I2C(0);
        // 长度-1
        len--;
    }
    // 如果长度没了那么读取无应答
    *str = RdACK_I2C(1);
    // 结束读取
    Stop_I2C();
}

最后的例子

多字节读写显示到 1602 上

/*
 * @Author: cuihaonan
 * @Email: devcui@outlook.com
 * @Date: 2021-04-04 19:12:01
 * @LastEditTime: 2021-04-04 22:09:53
 * @LastEditors: cuihaonan
 * @Description: Basic description
 * @FilePath: /sdcc-include/src/i2c/include/AT24C256.c
 * @LICENSE: NONE
 */
#include "./AT24C256.h"
#include "./I2C.h"

void WrByte_AT24C256(unsigned int addr, unsigned char dat)
{
    // 第一步START信号
    Start_I2C();
    // 写入设备地址 0b1010 0000 = 0xA0
    Wr_I2C(0xA0);
    // 写入高位地址,传入16位地址
    Wr_I2C(addr >> 8);
    // 写入低位地址,由于一次只能写入8位,所以前8位直接被过掉了
    Wr_I2C(addr);
    // 写入数据
    Wr_I2C(dat);
    // 最后一步STOP信号
    Stop_I2C();
}

unsigned char RdByte_AT24C256(unsigned int addr)
{
    unsigned char dat;
    // 开始信号
    Start_I2C();
    Wr_I2C(0xA0);
    Wr_I2C(addr >> 8);
    Wr_I2C(addr);
    // 开始信号
    Start_I2C();
    // 1010 0000 -> 1010 0001 = 0xA1
    Wr_I2C(0xA1);
    //  从addr读取数据发送非应答信号
    dat = RdACK_I2C(1);
    // 结束信号
    Stop_I2C();
    return dat;
}

void WrStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
    // 检测上一次是否写完了,如果越页了,那么继续写
    while (len > 0)
    {
        // 循环检测元器件应答信号
        while (1)
        {
            Start_I2C();
            // 如果ack === 0 跳出,进行下面的写入
            if (0 == Wr_I2C(0xA0))
            {
                // 跳出循环
                break;
            }
            // 否则结束
            Stop_I2C();
        }
        // 高位
        Wr_I2C(addr >> 8);
        // 低位
        Wr_I2C(addr);
        // 开始写
        while (len > 0)
        {
            // 写一个字节,指针指向下一个自负
            Wr_I2C(*str++);
            // 长度--
            len--;
            // 存储地址+1
            addr++;
            // 是否达到了下一页
            if (0 == (addr % 64))
            {
                // 上一个字节到本页的边界
                // 跳出停止继续写
                break;
            }
        }
        Stop_I2C();
    }
}

void RdStr_AT24CPAGE(unsigned char *str, unsigned int addr, unsigned char len)
{
    // 循环检测ack是否为1
    while (1)
    {
        Start_I2C();
        // 如果为0跳出进行读取
        if (0 == Wr_I2C(0xA0))
        {
            break;
        }
        Stop_I2C();
    }
    // 高低位
    Wr_I2C(addr >> 8);
    Wr_I2C(addr);
    // 第二个Start信号
    Start_I2C();
    // 现在是读
    Wr_I2C(0xA1);
    // 如果长度大于1
    while (len > 1)
    {
        // 读取,应答为0
        *str++ = RdACK_I2C(0);
        // 长度-1
        len--;
    }
    // 如果长度没了那么读取无应答
    *str = RdACK_I2C(1);
    // 结束读取
    Stop_I2C();
}

所属作品集

后发布评论