I2C和EEPROM

前几章我们学了一种通信协议叫做UART异步串口通信,这节课我们要来学习第二种常用的通信协议I 2 CI 2 C总线是由PHILIPS公司开发的两线式串行总线,多用于连接微处理器及其外围设备。I 2 C总线的主要特点是接口方式简单,两条线可以挂多个参与通信的器件,即多机模式,而且任何一个器件都可以作为主机,当然同一时刻只能一个主机。

从原理上来讲, UART 属于异步通信,比如电脑发送给单片机,电脑只负责把数据通过 TXD 发送出来即可,接收数据是单片机自己的事情。而I2C 属于同步通信, SCL 时钟线负责收发双方的时钟节拍, SDA 数据线负责传输数据。I2C 的发送方和接收方都以 SCL 这个时钟节拍为基准进行数据的发送和接收。
从应用上来讲, UART 通信多用于板间通信,比如单片机和电脑,这个设备和另外一个设备之间的通信。而I2C 多用于板内通信,比如单片机和我们本章要学的 EEPROM 之间的通信。
14.1 I2C时序初步认识
在硬件上,I2C 总线是由时钟总线 SCL 和数据总线 SDA 两条线构成,连接到总线上的所有的器件的 SCL 都连到一起,所有的 SDA 都连到一起。I2C 总线是开漏引脚并联的结构,因此我们外部要添加上拉电阻。对于开漏电路外部加上拉电阻的话,那就组成了线“与”的关系。总线上线“与”的关系,那所有接入的器件保持高电平,这条线才是高电平。而任意一个器件输出一个低电平,那这条线就会保持低电平,因此可以做到任何一个器件都可以拉低电平,也就是任何一个器件都可以作为主机,如图 14-1 所示,我们添加了 R63 R64 两个上拉电阻。
 
14-1 I2C 总线的上拉电阻
虽然说任何一个设备都可以作为主机,但绝大多数情况下我们都是用微处理器,也就是我们的单片机来做主机,而总线上挂的多个器件,每一个都像电话机一样有自己唯一的地址,在信息传输的过程中,通过这唯一的地址可以正常识别到属于自己的信息,在我们的 KST-51 开发板上,就挂接了 2 I2C 设备,一个是 24C02 ,一个是 PCF8591
我们在学习 UART 串行通信的时候,知道了我们的通信流程分为起始位、数据位、停止位这三部分,同理在I2C 中也有起始信号、数据传输和停止信号,如图 14-2 所示。
           
14-2 I2C 时序流程图
从图上可以看出来,I2C UART 时序流程有相似性,也有一定的区别。 UART 每个字节中,都有一个起始位, 8 个数据位和 1 位停止位。而I2C 分为起始信号,数据传输部分,最后是停止信号。其中数据传输部分,可以一次通信过程传输很多个字节,字节数是不受限制的,而每个字节的数据最后也跟了一位,这一位叫做应答位,通常用 ACK 表示,有点类似于 UART 的停止位。
下面我们一部分一部分的把I2C 通信时序进行剖析。之前我们学过了 UART ,所以学习I2C 的过程我尽量拿 UART 来作为对比,这样有助于更好的理解。但是有一点大家要理解清楚,就是 UART 通信虽然我们用了 TXD RXD 两根线,但是实际一次通信, 1 条线就可以完成, 2 条线是把发送和接收分开而已,而I2C 每次通信,不管是发送还是接收,必须 2 条线都参与工作才能完成,为了更方便的看出来每一位的传输流程,我们把图 14-2 改进成图 14-3
             
图14-3 I2C 通信流程解析
起始信号: UART 通信是从一直持续的高电平出现一个低电平标志起始位;而I2C 通信的起始信号的定义是 SCL 为高电平期间, SDA 由高电平向低电平变化产生一个下降沿,表示起始信号,如图 14-3 中的 start 部分所示。
数据传输:首先, UART 是低位在前,高位在后;而I2C 通信是高位在前,低位在后。第二, UART 通信数据位是固定长度,波特率分之一,一位一位固定时间发送完毕就可以了。而I2C 没有固定波特率,但是有时序的要求,要求当 SCL 在低电平的时候, SDA 允许变化,也就是说,发送方必须先保持 SCL 是低电平,才可以改变数据线 SDA ,输出要发送的当前数据的一位;而当 SCL 在高电平的时候, SDA 绝对不可以变化,因为这个时候,接收方要来读取当前 SDA 的电平信号是 0 还是 1 ,因此要保证 SDA 的稳定不变化,如图 14-3 中的每一位数据的变化,都是在 SCL 的低电平位置。 8 为数据位后边跟着的是一位响应位,响应位我们后边还要具体介绍。
停止信号: UART 通信的停止位是一位固定的高电平信号;而I2C 通信停止信号的定义是 SCL 为高电平期间, SDA 由低电平向高电平变化产生一个上升沿,表示结束信号,如图 14-3 中的 stop 部分所示。
14.2 I2C寻址模式
上一节介绍的是I2C 每一位信号的时序流程,而I2C 通信在字节级的传输中,也有固定的时序要求。I2C 通信的起始信号 (Start) 后,首先要发送一个从机的地址,这个地址一共有 7 位,紧跟着的第 8 位是数据方向位 (R/W) ,‘ 0 ’表示接下来要发送数据 ( ) ,‘ 1 ’表示接下来是请求数据 ( )
我们知道,打电话的时候,当拨通电话,接听方捡起电话肯定要回一个“喂”,这就是告诉拨电话的人,这边有人了。同理,这个第九位 ACK 实际上起到的就是这样一个作用。当我们发送完了这 7 位地址和 1 位方向位,如果我们发送的这个地址确实存在,那么这个地址的器件应该回应一个 ACK 0 ’,如果不存在,就没“人”回应 ACK
那我们写一个简单的程序,访问一下我们板子上的 EEPROM 的地址,另外在写一个不存在的地址,看看他们是否能回一个 ACK ,来了解和确认一下这个问题。
我们板子上的 EEPROM 器件型号是 24C02 ,在 24C02 的数据手册 3.6 部分说明了, 24C02 7 位地址中,其中高 4 位是固定的 1010 ,而低 3 位的地址取决于我们电路的设计 , 由芯片上的 A2 A1 A0 3 个引脚的实际电平决定,来看一下我们的 24C02 的电路图,如图 14-4 所示。
14-4 24C02 原理图
从图 14-4 可以看出来,我们的 A2 A1 A0 都是接的 GND ,也就是说都是 0 ,因此我们的 7 位地址实际上是二进制的 1010000 ,也就是 0x50 。我们用I2C 的协议来寻址 0x50 ,另外再寻址一个不存在的地址 0x62 ,寻址完毕后,把返回的 ACK 显示到我们的 1602 液晶上,大家对比一下。
/***********************lcd1602.c 文件程序源代码 *************************/
#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

void LcdWaitReady()  // 等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; // 读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}
void LcdWriteCmd(unsigned char cmd)  // 写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  // 写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  // 显示字符串,屏幕起始坐标 (x,y) ,字符串指针 str
{
    unsigned char addr;

    // 由输入的显示坐标计算显示 RAM 的地址
    if (y == 0)
        addr = 0x00 + x; // 第一行字符地址从 0x00 起始
    else
        addr = 0x40 + x; // 第二行字符地址从 0x40 起始

    // 由起始显示 RAM 地址连续写入字符串
    LcdWriteCmd(addr | 0x80); // 写入起始地址
    while (*str != '\0')      // 连续写入字符串数据,直到检测到结束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdInit()  // 液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2 显示, 5*7 点阵, 8 位数据接口
    LcdWriteCmd(0x0C);  // 显示器开,光标关闭
    LcdWriteCmd(0x06);  // 文字不动,地址自动 +1
    LcdWriteCmd(0x01);  // 清屏
}
/*************************main.c 文件程序源代码 **************************/
#include <reg52.h>
#include <intrins.h>

#define I2CDelay()  {_nop_();_nop_();_nop_();_nop_();}

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

bit I2CAddressing(unsigned char addr);
extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);

void main ()
{
    bit ack;
    unsigned char str[10];

    LcdInit();        // 初始化液晶

    ack = I2CAddressing(0x50); // 查询地址为 0x50 的器件
    str[0] = '5';              // 将地址和应答值转换为字符串
    str[1] = '0';
    str[2] = ':';
    str[3] = (unsigned char)ack + '0';
    str[4] = '\0';
    LcdShowStr(0, 0, str);     // 显示到液晶上

    ack = I2CAddressing(0x62); // 查询地址为 0x62 的器件
    str[0] = '6';              // 将地址和应答值转换为字符串
    str[1] = '2';
    str[2] = ':';
    str[3] = (unsigned char)ack + '0';
    str[4] = '\0';
    LcdShowStr(8, 0, str);     // 显示到液晶上

    while(1)
    {}
}

void I2CStart()  // 产生总线起始信号
{
    I2C_SDA = 1; // 首先确保 SDA SCL 都是高电平
    I2C_SCL = 1;
    I2CDelay();
    I2C_SDA = 0; // 先拉低 SDA
    I2CDelay();
    I2C_SCL = 0; // 再拉低 SCL
}
void I2CStop()   // 产生总线停止信号
{
    I2C_SCL = 0; // 首先确保 SDA SCL 都是低电平
    I2C_SDA = 0;
    I2CDelay();
    I2C_SCL = 1; // 先拉高 SCL
    I2CDelay();
    I2C_SDA = 1; // 再拉高 SDA
    I2CDelay();
}
bit I2CWrite(unsigned char dat) //I2C 总线写操作,待写入字节 dat ,返回值为从机应答位的值
{
    bit ack;  // 用于暂存应答位的值
    unsigned char mask;  // 用于探测字节内某一位值的掩码变量

    for (mask=0x80; mask!=0; mask>>=1) // 从高位到低位依次进行
    {
        if ((mask&dat) == 0)  // 该位的值输出到 SDA
            I2C_SDA = 0;
        else
            I2C_SDA = 1;
        I2CDelay();
        I2C_SCL = 1;          // 拉高 SCL
        I2CDelay();
        I2C_SCL = 0;          // 再拉低 SCL ,完成一个位周期
    }
    I2C_SDA = 1;   //8 位数据发送完后,主机释放 SDA ,以检测从机应答
    I2CDelay();
    I2C_SCL = 1;   // 拉高 SCL
    I2CDelay();
    ack = I2C_SDA; // 读取此时的 SDA 值,即为从机的应答值
    I2C_SCL = 0;   // 再拉低 SCL 完成应答位,并保持住总线

    return ack;    // 返回从机应答值
}
bit I2CAddressing(unsigned char addr) //I2C 寻址函数,即检查地址为 addr 的器件是否存在,返回值为其应答值,即应答则表示存在,非应答则表示不存在
{
    bit ack;

    I2CStart();  // 产生起始位,即启动一次总线操作
    ack = I2CWrite(addr<<1);  // 器件地址需左移一位,因寻址命令的最低位为读写位,用于表示之后的操作是读或写
    I2CStop();   // 不需进行后续读写,而直接停止本次总线操作

    return ack;
}
我们把这个程序在 KST-51 开发板上运行完毕,会在液晶上边显示出来我们预想的结果,主机发送一个存在的从机地址,从机会回复一个应答位;主机如果发送一个不存在的从机地址,就没有从机应答。
前边我有提到过有一个利用库函数_nop_()来进行精确延时,一个_nop_()的时间就是一个机器周期,这个库函数是包含在了intrins.h这个库文件中,我们如果要使用这个库函数,只需要在程序最开始,和包含reg52.h 一样, include<intrins.h>之后,我们程序就可以直接使用这个库函数了。
还有一点要提一下,I2C 通信分为低速模式 100kbit/s ,快速模式 400kbit/s 和高速模式 3.4Mbit/s 。因为所有的I2C 器件都支持低速,但却未必支持另外两种速度,所以作为通用的I2C 程序我们选择 100k 这个速率来实现,也就是说实际程序产生的时序必须小于等于 100k 的时序参数,很明显也就是要求 SCL 的高低电平持续时间都不短于 5us ,因此我们在时序函数中通过插入I2CDelay() 这个总线延时函数(它实际上就是 4 NOP 指令,用 define 在文件开头做了定义),加上改变 SCL 值语句本身占用的至少一个周期,来达到这个速度限制。如果以后需要提高速度,那么只需要减小这里的总线延时时间即可。
此外我们要学习一个发送数据的技巧,就是 I2C 通信时如何将一个字节的数据发送出去。大家注意写函数中,我用的那个 for 循环的技巧。for (mask=0x80; mask!=0; mask>>=1),由于I2C 通信是从高位开始发送数据,所以我们先从最高位开始, 0x80 dat 进行按位与运算,从而得知 dat 7 位是 0 还是 1 ,然后右移一位,也就是变成了用 0x40 dat 按位与运算,得到第 6 位是 0 还是 1 ,一直到第 0 位结束,最终通过 if 语句,把 dat 8 位数据依次发送了出去。其他的逻辑大家对照前边讲到的理论知识,认真研究明白就可以了。
1.3 EEPROM的学习
在实际的应用中,保存在单片机 RAM 中的数据,掉电后数据就丢失了,保存在单片机的 FLASH 中的数据,又不能随意改变,也就是不能用它来记录变化的数值。但是在某些场合,我们又确实需要记录下某些数据,而它们还时常需要改变或更新,掉电之后数据还不能丢失,比如我们的家用电表度数,我们的电视机里边的频道记忆,一般都是使用 EEPROM 来保存数据,特点就是掉电后不丢失。我们板子上使用的这个器件是 24C02 ,是一个容量大小是 2Kbit 位,也就是 256 个字节的 EEPROM 。一般情况下, EEPROM 拥有 30 万到 100 万次的寿命,也就是它可以反复写入 30-100 万次,而读取次数是无限的。
24C02 是一个基于 I2C 通信协议的器件,因此从现在开始,我们的I2C 和我们的 EEPROM 就要合体了。但是大家要分清楚,I2C 是一个通信协议,它拥有严密的通信时序逻辑要求,而 EEPROM 是一个器件,只是这个器件采样了I2C 协议的接口与单片机相连而已,二者并没有必然的联系, EEPROM 可以用其他接口,I2C 也可以用在其它很多器件上。
14.3.1 EEPROM单字节读写操作时序
1、EEPROM 写数据流程
第一步,首先是I2C 的起始信号,接着跟上首字节,也就是我们前边讲的I2C 的器件地
(EERPOM) ,并且在读写方向上选择“写”操作。
第二步,发送数据的存储地址。我们 24C02 一共 256 个字节的存储空间,地址从 0x00 0xFF ,我们想把数据存储在哪个位置,此刻写的就是哪个地址。
第三步,发送要存储的数据第一个字节,第二个字节 ...... 注意在写数据的过程中, EEPROM 每个字节都会回应一个“应答位 0 ”,来告诉我们写 EEPROM 数据成功,如果没有回应答位,说明写入不成功。
在写数据的过程中,每成功写入一个字节, EEPROM 存储空间的地址就会自动加 1 ,当加到 0xFF 后,再写一个字节,地址会溢出又变成了 0x00
2、EEPROM 读数据流程
第一步,首先是I2C 的起始信号,接着跟上首字节,也就是我们前边讲的I2C 的器件地
(EERPOM) ,并且在读写方向上选择“写”操作。这个地方可能有同学会诧异,我们明明是读数据为何方向也要选“写”呢?刚才说过了,我们 24C02 一共有 256 个地址,我们选择写操作,是为了把所要读的数据的存储地址先写进去,告诉 EEPROM 我们要读取哪个地址的数据。这就如同我们打电话,先拨总机号码 (EEPROM 器件地址 ) ,而后还要继续拨分机号码 ( 数据地址 ) ,而拨分机号码这个动作,主机仍然是发送方,方向依然是“写”。
第二步,发送要读取的数据的地址,注意是地址而非存在 EEPROM 中的数据,通知 EEPROM 我要哪个分机的信息。
第三步,重新发送I2C 起始信号和器件地址,并且在方向位选择“读”操作。
这三步当中,每一个字节实际上都是在“写”,所以每一个字节 EEPROM 都会回应一个“应答位 0 ”。
第四步,读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位 ACK(0) ”,如果不想读了,告诉 EEPROM ,我不想要数据了,别再发数据了,那就发送一个“非应答位 NACK(1) ”。
和写操作规则一样,我们每读一个字节,地址会自动加 1 ,那如果我们想继续往下读,给 EEPROM 一个 ACK(0) 低电平,那再继续给 SCL 完整的时序, EEPROM 会继续往外送数据。如果我们不想读了,要告诉 EEPROM 不要数据了,那我们直接给一个 NAK(1) 高电平即可。这个地方大家要从逻辑上理解透彻,不能简单的靠死记硬背了,一定要理解明白。梳理一下几个要点: A 、在本例中单片机是主机, 24C02 是从机; B 、无论是读是写, SCL 始终都是由主机控制的; C 、写的时候应答信号由从机给出,表示从机是否正确接收了数据; D 、读的时候应答信号则由主机给出,表示是否继续读下去。
那我们下面写一个程序,读取 EEPROM 0x02 这个地址上的一个数据,不管这个数据之前是多少,我们都再将读出来的数据加 1 ,再写到 EEPROM 0x02 这个地址上。此外我们将I2C 的程序建立一个文件,写一个 I2C.c 程序文件,形成我们又一个程序模块。大家也可以看出来,我们连续的这几个程序, lcd1602.c 文件里的程序都是一样的,今后我们大家写 1602 显示程序也可以直接拿过去用,大大提高了程序移植的方便性。
/*************************I2C.c 文件程序源代码 ***************************/
#include <reg52.h>
#include <intrins.h>

#define I2CDelay()  {_nop_();_nop_();_nop_();_nop_();}

sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;

void I2CStart()  // 产生总线起始信号
{
    I2C_SDA = 1; // 首先确保 SDA SCL 都是高电平
    I2C_SCL = 1;
    I2CDelay();
    I2C_SDA = 0; // 先拉低 SDA
    I2CDelay();
    I2C_SCL = 0; // 再拉低 SCL
}
void I2CStop()   // 产生总线停止信号
{
    I2C_SCL = 0; // 首先确保 SDA SCL 都是低电平
    I2C_SDA = 0;
    I2CDelay();
    I2C_SCL = 1; // 先拉高 SCL
    I2CDelay();
    I2C_SDA = 1; // 再拉高 SDA
    I2CDelay();
}
bit I2CWrite(unsigned char dat) //I2C 总线写操作,待写入字节 dat ,返回值为应答状态
{
    bit ack;  // 用于暂存应答位的值
    unsigned char mask;  // 用于探测字节内某一位值的掩码变量

    for (mask=0x80; mask!=0; mask>>=1) // 从高位到低位依次进行
    {
        if ((mask&dat) == 0)  // 该位的值输出到 SDA
            I2C_SDA = 0;
        else
            I2C_SDA = 1;
        I2CDelay();
        I2C_SCL = 1;          // 拉高 SCL
        I2CDelay();
        I2C_SCL = 0;          // 再拉低 SCL ,完成一个位周期
    }
    I2C_SDA = 1;   //8 位数据发送完后,主机释放 SDA ,以检测从机应答
    I2CDelay();
    I2C_SCL = 1;   // 拉高 SCL
    ack = I2C_SDA; // 读取此时的 SDA 值,即为从机的应答值
    I2CDelay();
    I2C_SCL = 0;   // 再拉低 SCL 完成应答位,并保持住总线

    return (~ack); // 应答值取反以符合通常的逻辑: 0= 不存在或忙或写入失败, 1= 存在且空闲或写入成功
}
unsigned char I2CReadNAK() //I2C 总线读操作,并发送非应答信号,返回值为读到的字节
{
    unsigned char mask;
    unsigned char dat;

    I2C_SDA = 1;  // 首先确保主机释放 SDA
    for (mask=0x80; mask!=0; mask>>=1) // 从高位到低位依次进行
    {
        I2CDelay();
        I2C_SCL = 1;      // 拉高 SCL
        if(I2C_SDA == 0)  // 读取 SDA 的值
            dat &= ~mask; // 0 时, dat 中对应位清零
        else
            dat |= mask;  // 1 时, dat 中对应位置 1
        I2CDelay();
        I2C_SCL = 0;      // 再拉低 SCL ,以使从机发送出下一位
    }
    I2C_SDA = 1;   //8 位数据发送完后,拉高 SDA ,发送非应答信号
    I2CDelay();
    I2C_SCL = 1;   // 拉高 SCL
    I2CDelay();
    I2C_SCL = 0;   // 再拉低 SCL 完成非应答位,并保持住总线

    return dat;
}
unsigned char I2CReadACK() //I2C 总线读操作,并发送应答信号,返回值为读到的字节
{
    unsigned char mask;
    unsigned char dat;

    I2C_SDA = 1;  // 首先确保主机释放 SDA
    for (mask=0x80; mask!=0; mask>>=1) // 从高位到低位依次进行
    {
        I2CDelay();
        I2C_SCL = 1;      // 拉高 SCL
        if(I2C_SDA == 0)  // 读取 SDA 的值
            dat &= ~mask; // 0 时, dat 中对应位清零
        else
            dat |= mask;  // 1 时, dat 中对应位置 1
        I2CDelay();
        I2C_SCL = 0;      // 再拉低 SCL ,以使从机发送出下一位
    }
    I2C_SDA = 0;   //8 位数据发送完后,拉低 SDA ,发送应答信号
    I2CDelay();
    I2C_SCL = 1;   // 拉高 SCL
    I2CDelay();
    I2C_SCL = 0;   // 再拉低 SCL 完成应答位,并保持住总线

    return dat;
}
/***********************lcd1602.c 文件程序源代码 *************************/
#include <reg52.h>

#define LCD1602_DB   P0

sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E  = P1^5;

void LcdWaitReady()  // 等待液晶准备好
{
    unsigned char sta;

    LCD1602_DB = 0xFF;
    LCD1602_RS = 0;
    LCD1602_RW = 1;
    do
    {
        LCD1602_E = 1;
        sta = LCD1602_DB; // 读取状态字
        LCD1602_E = 0;
    } while (sta & 0x80); //bit7 等于 1 表示液晶正忙,重复检测直到其等于 0 为止
}
void LcdWriteCmd(unsigned char cmd)  // 写入命令函数
{
    LcdWaitReady();
    LCD1602_RS = 0;
    LCD1602_RW = 0;
    LCD1602_DB = cmd;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdWriteDat(unsigned char dat)  // 写入数据函数
{
    LcdWaitReady();
    LCD1602_RS = 1;
    LCD1602_RW = 0;
    LCD1602_DB = dat;
    LCD1602_E  = 1;
    LCD1602_E  = 0;
}
void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str)  // 显示字符串,屏幕起始坐标 (x,y) ,字符串指针 str
{
    unsigned char addr;

    // 由输入的显示坐标计算显示 RAM 的地址
    if (y == 0)
        addr = 0x00 + x; // 第一行字符地址从 0x00 起始
    else
        addr = 0x40 + x; // 第二行字符地址从 0x40 起始

    // 由起始显示 RAM 地址连续写入字符串
    LcdWriteCmd(addr | 0x80); // 写入起始地址
    while (*str != '\0')      // 连续写入字符串数据,直到检测到结束符
    {
        LcdWriteDat(*str);
        str++;
    }
}
void LcdInit()  // 液晶初始化函数
{
    LcdWriteCmd(0x38);  //16*2 显示, 5*7 点阵, 8 位数据接口
    LcdWriteCmd(0x0C);  // 显示器开,光标关闭
    LcdWriteCmd(0x06);  // 文字不动,地址自动 +1
    LcdWriteCmd(0x01);  // 清屏
}
/************************main.c 文件程序源代码 **************************/
#include <reg52.h>

extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
unsigned char E2ReadByte(unsigned char addr);
void E2WriteByte(unsigned char addr, unsigned char dat);

void main ()
{
    unsigned char dat;
    unsigned char str[10];

    LcdInit();   // 初始化液晶
    dat = E2ReadByte(0x02);    // 读取指定地址上的一个字节
    str[0] = (dat/100) + '0';  // 转换为十进制字符串格式
    str[1] = (dat/10%10) + '0';
    str[2] = (dat%10) + '0';
    str[3] = '\0';
    LcdShowStr(0, 0, str);     // 显示在液晶上
    dat++;                     // 将其数值 +1
    E2WriteByte(0x02, dat);    // 再写回到对应的地址上

    while(1)
    {}
}

unsigned char E2ReadByte(unsigned char addr) // 读取 EEPROM 中的一个字节,字节地址 addr
{
    unsigned char dat;

    I2CStart();
    I2CWrite(0x50<<1); // 寻址器件,后续为写操作
    I2CWrite(addr);    // 写入存储地址
    I2CStart();        // 发送重复启动信号
    I2CWrite((0x50<<1)|0x01); // 寻址器件,后续为读操作
    dat = I2CReadNAK();       // 读取一个字节数据
    I2CStop();

    return dat;
}

void E2WriteByte(unsigned char addr, unsigned char dat) // EEPROM 中写入一个字节,字节地址 addr
{
    I2CStart();
    I2CWrite(0x50<<1); // 寻址器件,后续为写操作
    I2CWrite(addr);    // 写入存储地址
    I2CWrite(dat);     // 写入一个字节数据
    I2CStop();
}
/***********************************************************************/
这个程序,以同学们现在的基础,独立分析应该不困难了,遇到哪个语句不懂可以及时问问别人或者搜索一下,把该解决的问题理解明白。大家把这个程序复制过去后,编译一下会发现 Keil 软件提示了一个警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS ,这个警告的意思是有我们代码中存在没有被调用过的变量或者函数。
大家仔细观察一下,这个程序,我们读取 EEPROM 的时候,只读了一个字节我们就要告诉 EEPROM 不需要再读数据了,因此我们读完后直接回复一个“ NAK ”,因此我们只调用了 I2CReadNAK() 这个函数,而并没有调用I2CReadACK()这个函数。我们今后很可能读数据的时候要连续读几个字节,因此这个函数写在了I2C.c 文件中,作为 I2C功能模块的一部分是必要的,方便我们这个文件以后移植到其他程序中使用,因此这个警告在这里就不必管它了。
14.3.2 EEPROM多字节读写操作时序[size=14.0000pt]
我们读取 EEPROM 的时候很简单, EEPROM 根据我们所送的时序,直接就把数据送出来了,但是写 EEPROM 却没有这么简单。我们如果给 EEPROM 发送数据后,先保存在了 EEPROM 的缓存, EEPROM 必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区域写需要一定的时间,每种器件不完全一样, ATMEL 公司的 24C02 的这个写入时间最高不超过 5ms 。在往非易失区域写的过程, EEPROM 是不会再响应我们的访问的,不仅接收不到我们的数据,我们即使用I2C 标准的寻址模式去寻址, EEPROM 都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后, EEPROM 再次恢复正常,可以正常读写了。
细心的同学,在看上一节程序的时候会发现,我们写数据的那段代码,实际上我们有去读应答位 ACK ,但是读到了应答位我们也没有做任何处理。这是因为我们一次只写一个字节的数据进去,等到下次重新上电再写的时候,时间肯定远远超过了 5ms ,但是如果我们是连续写入几个字节的时候,我们就必须得考虑到应答位的问题了。写入一个字节后,再写入下一个字节之前,我们必须要等待 EEPROM 再次响应才可以,大家注意我的程序的写法,可以学习一下。
之前我们知道编写多 .c 文件移植的方便性了,本节程序和上一节的 lcd1602.c 文件和 I2C.c 文件完全是一样的,因此这次我们只把 main.c 文件给大家发出来,帮大家分析明白。而同学们却不能这样,同学们是初学,很多知识和技巧需要多练才能巩固下来,因此每个程序还是建议大家在你的 Keil 软件上一个代码一个代码的敲出来。

        
                /***********************lcd1602.c 文件程序源代码 *************************/
        
         略
        
                /************************I2C.c 文件程序源代码 ***************************/
        
        略
        
                /************************main.c 文件程序源代码 **************************/


#include <reg52.h>

extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len);

void main ()
{
    unsigned char i;
    unsigned char buf[5];
    unsigned char str[20];

    LcdInit();   // 初始化液晶
    E2Read(buf, 0x90, sizeof(buf));       // E2 中读取一段数据
    ArrayToHexStr(str, buf, sizeof(buf)); // 转换为十六进制字符串
    LcdShowStr(0, 0, str);                // 显示到液晶上
    for (i=0; i<sizeof(buf); i++)        // 数据依次 +1,+2,+3...
    {
        buf[ i] = buf[ i] + 1 + i;
    }
    E2Write(buf, 0x90, sizeof(buf));      // 再写回到 E2

    while(1)
    {}
}

void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len) // 把一个字节数组转换为十六进制字符串的格式
{
    unsigned char tmp;

    while (len--)
    {
        tmp = *array >> 4;         // 先取高 4
        if (tmp <= 9)              // 转换为 0-9 A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       // 再取低 4
        if (tmp <= 9)              // 转换为 0-9 A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                // 转换完一个字节添加一个空格
        str++;
        array++;
    }
}
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2 读取函数,数据接收指针 buf E2 中的起始地址 addr ,读取长度 len
{
    do {                       // 用寻址操作查询当前是否可进行读写操作
        I2CStart();
        if (I2CWrite(0x50<<1)) // 器件应答则跳出循环,继续执行,非应答则进行下一次查询
            break;
        I2CStop();
    } while(1);
    I2CWrite(addr);           // 写入起始地址
    I2CStart();               // 发送重复启动信号
    I2CWrite((0x50<<1)|0x01); // 寻址器件,后续为读操作
    while (len > 1)           // 连续读取 len-1 个字节
    {
        *buf = I2CReadACK();  // 最后字节之前为读取操作 + 应答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      // 最后一个字节为读取操作 + 非应答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2 写入函数,源数据指针 buf E2 中的起始地址 addr 写入长度 len
{
    while (len--)
    {
        do {                       // 用寻址操作查询当前是否可进行读写操作,即等待上一次写入操作完成
            I2CStart();
            if (I2CWrite(0x50<<1)) // 器件应答则跳出循环,继续执行,非应答则进行下一次查询
                break;
            I2CStop();
        } while(1);
        I2CWrite(addr);           // 写入起始地址
        I2CWrite(*buf);           // 写入一个字节数据
        I2CStop();                // 结束写操作,以等待写入完成
        buf++;                    // 数据指针递增
        addr++;                   //E2 地址递增
    }
}

函数ArrayToHexStr:这是一个把数组转换成十六进制字符串的形式。由于我们从EEPROM 读出来的是正常的数据,而 1602 液晶接收的是 ASCII 码字符,因此我们要通过液晶把数据显示出来必须先通过一步转换。算法倒是很简单,就是把每一个字节的数据高 4 位和低 4 位分开,和 9 进行比较,如果小于等于 9 ,则通过数字加’0’转 ASCII 码发送;如果大于 9 ,则通过加’A’转 ASCII 码发送出去。
函数 E2Read :我们在读之前,要查询一下当前是否可以进行读写操作, EEPROM 正常响应才可以进行。进行后,最后一个字节之前的,全部给出 ACK ,而读完了最后一个字节,我们要给出一个 NAK
函数 E2Write :每次写操作之前,我们都要进行查询判断当前 EEPROM 是否响应,正常响应后才可以写数据。
14.3.3 EEPROM的页写入
如果每个数据都连续写入,像我们上节课那样写的时候,每次都先起始位,再访问一下这个 EEPROM 的地址,看看是否响应,感觉上效率太低了。因此 EEPROM 的厂商就想了一个办法,把 EEPROM 分页管理。 24c01 24c02 这两个型号是 8 个字节一个页,而 24c04 24c08 24c16 16 个字节一页。我们板子上的型号是 24C02 ,一共是 256 个字节, 8 个字节一页,那么就一共有 32 页。
分配好页之后,如果我们在同一个页内连续写入几个字节后,最后再发送停止位的时序。 EEPROM 检测到这个停止位后,统一把这一页的数据写到非易失区域,就不需要像上节课那样写一个字节检测一次了,并且页写入的时间也不会超过 5ms 。如果我们写入的数据跨页了,那么写完了一页之后,我们要发送一个停止位,然后等待并且检测 EEPROM 的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在一定程度上提高我们的写入效率。     
        
                /***********************eeprom.c 文件程序源代码 *************************/


#include <reg52.h>

extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);

void E2Read(unsigned char *buf, unsigned char addr, unsigned char len) //E2 读取函数,数据接收指针 buf E2 中的起始地址 addr ,读取长度 len
{
    do {                       // 用寻址操作查询当前是否可进行读写操作
        I2CStart();
        if (I2CWrite(0x50<<1)) // 器件应答则跳出循环,继续执行,非应答则进行下一次查询
            break;
        I2CStop();
    } while(1);
    I2CWrite(addr);           // 写入起始地址
    I2CStart();               // 发送重复启动信号
    I2CWrite((0x50<<1)|0x01); // 寻址器件,后续为读操作
    while (len > 1)           // 连续读取 len-1 个字节
    {
        *buf = I2CReadACK();  // 最后字节之前为读取操作 + 应答
        buf++;
        len--;
    }
    *buf = I2CReadNAK();      // 最后一个字节为读取操作 + 非应答
    I2CStop();
}

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len) //E2 写入函数,源数据指针 buf E2 中的起始地址 addr ,写入长度 len
{
    while (len > 0)
    {
        // 等待上次写入操作完成
        do {
            I2CStart();
            if (I2CWrite(0x50<<1)) // 器件应答则跳出循环,继续执行,非应答则进行下一次查询
                break;
            I2CStop();
        } while(1);
        // 按页写模式连续写入字节
        I2CWrite(addr);           // 写入起始地址
        while (len > 0)
        {
            I2CWrite(*buf);       // 写入一个字节数据
            len--;                // 待写入长度计数递减
            buf++;                // 数据指针递增
            addr++;               //E2 地址递增
            if ((addr&0x07) == 0) // 检查地址是否到达页边界, 24C02 每页 8 字节,所以检测低 3 位是否为零即可
                break;            // 到达页边界时,跳出循环,结束本次写操作
        }
        I2CStop();
    }
}
这个 eeprom.c 文件中的程序,单独做一个文件,用来管理 eeprom 的访问。其中E2Read函数和上一节是一样的,因为读操作和是否同一页无关。重点是E2Write函数,我们在写入数据的时候,要计算下一个要写的数据的地址是否是一个页的起始地址,如果是的话,则必须跳出循环,等待EEPROM 上一页写入到非易失区域后,再进行继续写入。
而写了 eeprom.c 后, main.c 文件里的程序就要变的简单多了,大家可以自己看一下,不需要过多解释了。
/************************main.c 文件程序源代码 **************************/
#include <reg52.h>

extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len);

void main ()
{
    unsigned char i;
    unsigned char buf[5];
    unsigned char str[20];

    LcdInit();   // 初始化液晶
    E2Read(buf, 0x8E, sizeof(buf));       // E2 中读取一段数据
    ArrayToHexStr(str, buf, sizeof(buf)); // 转换为十六进制字符串
    LcdShowStr(0, 0, str);                // 显示到液晶上
    for (i=0; i<sizeof(buf); i++)         // 数据依次 +1,+2,+3...
    {
        buf[ i] = buf[ i] + 1 + i;
    }
    E2Write(buf, 0x8E, sizeof(buf));      // 再写回到 E2

    while(1)
    {}
}

void ArrayToHexStr(unsigned char *str, unsigned char *array, unsigned char len) // 把一个字节数组转换为十六进制字符串的格式
{
    unsigned char tmp;

    while (len--)
    {
        tmp = *array >> 4;         // 先取高 4
        if (tmp <= 9)              // 转换为 0-9 A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        tmp = *array & 0x0F;       // 再取低 4
        if (tmp <= 9)              // 转换为 0-9 A-F
            *str = tmp + '0';
        else
            *str = tmp - 10 + 'A';
        str++;
        *str = ' ';                // 转换完一个字节添加一个空格
        str++;
        array++;
    }
}
多字节写入和页写入程序都编写出来了,而且页写入的程序我们还特地跨页写的数据,他们的写入时间到底差别多大呢。我们用一些工具可以测量一下,比如示波器,逻辑分析仪等工具。我现在把两次写入时间用逻辑分析仪给抓了出来,并且用时间标签 T1 T2 给标注了开始位置和结束位置,如图 14-5 和图 14-6 所示,右侧显示的 |T1-T2| 就是最终写入 5 个字节所耗费的时间。多字节一个一个写入,每次写入后都需要再次通信检测 EEPROM 是否在“忙”,因此耗费了大量的时间,同样的写入 5 个字节的数据,一个一个写入用了 8.4ms 左右的时间,而使用页写入,只用了 3.5ms 左右的时间。
      
图14-5  多字节写入时间
   
图14-6  跨页写入时间
14.4 I2C和EEPROM的综合实验学习[size=14.0000pt]
电视频道记忆功能,交通灯倒计时时间的设定,户外 LED 广告的记忆功能,都有可能有类似 EEPROM 这类存储器件。这类器件的优势是存储的数据不仅可以改变,而且掉电后数据保存不丢失,因此大量应用在各种电子产品上。
我们这节课的例程,有点类似广告屏。上电后, 1602 的第一行显示 EEPROM 0x20 地址开始的 16 个字符,第二行显示 EERPOM 0x40 开始的 16 个字符。我们可以通过 UART 串口通信来改变 EEPROM 内部的这个数据,并且同时改变了 1602 显示的内容,下次上电的时候,直接会显示我们更新过的内容。
这个程序所有的相关内容,我们之前都已经讲过了。但是这个程序体现在了一个综合程序应用能力上。这个程序用到了 1602 液晶、 UART 实用串口通信、 EEPROM 读写操作等多个功能的综合应用。写个点亮小灯好简单,但是我们想学会真正的单片机,必须得学会这种综合程序的应用,实现多个模块同时参与工作,这个理念在我们的全板子测试视频里已经有所体现。因此同学们,要认认真真的把工程建立起来,一行一行的把程序编写起来,最终巩固下来。

        
                /***********************lcd1602.c 文件程序源代码 *************************/
        
                                                       略
        
                /************************I2C.c 文件程序源代码 ***************************/
        
        略
        
                /***********************eeprom.c 文件程序源代码 *************************/
        
        略
        
                /************************uart.c 文件程序源代码 ***************************/


#include <reg52.h>

bit flagOnceTxd = 0;  // 单次发送完成标志,即发送完一个字节
bit cmdArrived = 0;   // 命令到达标志,即接收到上位机下发的命令
unsigned char cntRxd = 0;
unsigned char pdata bufRxd[40]; // 串口接收缓冲区

extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);

void ConfigUART(unsigned int baud)  // 串口配置函数, baud 为波特率
{
    SCON = 0x50;   // 配置串口为模式 1
    TMOD &= 0x0F;  // 清零 T1 的控制位
    TMOD |= 0x20;  // 配置 T1 为模式 2
    TH1 = 256 - (11059200/12/32) / baud;  // 计算 T1 重载值
    TL1 = TH1;     // 初值等于重载值
    ET1 = 0;       // 禁止 T1 中断
    ES  = 1;       // 使能串口中断
    TR1 = 1;       // 启动 T1
}
unsigned char UartRead(unsigned char *buf, unsigned char len) // 串口数据读取函数,数据接收指针 buf ,读取数据长度 len ,返回值为实际读取到的数据长度
{
    unsigned char i;

    if (len > cntRxd) // 读取长度大于接收到的数据长度时,
    {
        len = cntRxd; // 读取长度设置为实际接收到的数据长度
    }
    for (i=0; i<len; i++) // 拷贝接收到的数据
    {
        *buf = bufRxd[ i];
        buf++;
    }
    cntRxd = 0;  // 清零接收计数器

    return len;  // 返回实际读取长度
}
void UartWrite(unsigned char *buf, unsigned char len) // 串口数据写入函数,即串口发送函数,待发送数据指针 buf ,数据长度 len
{
    while (len--)
    {
        flagOnceTxd = 0;
        SBUF = *buf;
        buf++;
        while (!flagOnceTxd);
    }
}

bit CmdCompare(unsigned char *buf, const unsigned char *cmd) // 命令比较函数,缓冲区数据与指定命令比较,相同返回 1 ,不同返回 0
{
    while (*cmd != '\0')
    {
        if (*cmd != *buf) // 遇到不相同字符时即刻返回 0
        {
            return 0;
        }
        else // 当前字符相等时,指针递增准备比较下一字符
        {
            cmd++;
            buf++;
        }
    }
    return 1; // 到命令字符串结束时字符都相等则返回 1
}
void TrimString16(unsigned char *out, unsigned char *in) // 将一字符串整理成 16 字节的固定长度字符串,不足部分补空格
{
    unsigned char i = 0;

    while (*in != '\0') // 拷贝字符串直到输入字符串结束
    {
        *out = *in;
        out++;
        in++;
        i++;
        if (i >= 16)   // 当拷贝长度已达到 16 字节时,强制跳出循环
            break;
    }
    for ( ; i<16; i++) // 如不足 16 个字节则用空格补齐
    {
        *out = ' ';
        out++;
    }
    *out = '\0';       // 最后添加结束符
}
void UartDriver() // 串口驱动函数,检测接收到的命令并执行相应动作
{
    unsigned char i;
    unsigned char len;
    unsigned char buf[30];
    unsigned char str[17];
    const unsigned char code cmd0[] = "showstr1 ";
    const unsigned char code cmd1[] = "showstr2 ";
    const unsigned char code *cmdList[] = {cmd0, cmd1};

    if (cmdArrived) // 有命令到达时,读取处理该命令
    {
        cmdArrived = 0;
        for (i=0; i<sizeof(buf); i++) // 清零命令接收缓冲区
        {
            buf[ i] = 0;
        }
        len = UartRead(buf, sizeof(buf)); // 将接收到的命令读取到缓冲区中
        for (i=0; i<sizeof(cmdList)/sizeof(cmdList[0]); i++) // 与所支持的命令列表逐一进行比较
        {
            if (CmdCompare(buf, cmdList[ i]) == 1) // 检测到相符命令时退出循环,此时的 i 值就是该命令在列表中的下标值
            {
                break;
            }
        }
        switch (i) // 根据比较结果执行相应命令
        {
            case 0:
                buf[len] = '\0';                       // 为接收到的字符串添加结束符
                TrimString16(str, buf+sizeof(cmd0)-1); // 整理成 16 字节的固定长度字符串,不足部分补空格
                LcdShowStr(0, 0, str);                 // 显示字符串 1
                E2Write(str, 0x20, sizeof(str));       // 保存字符串 1 ,其 E2 起始地址为 0x20
                break;
            case 1:
                buf[len] = '\0';
                TrimString16(str, buf+sizeof(cmd1)-1);
                LcdShowStr(0, 1, str);
                E2Write(str, 0x40, sizeof(str));       // 保存字符串 2 ,其 E2 起始地址为 0x40
                break;
            default:  //i 大于命令列表最大下标时,即表示没有相符的命令,给上机发送“错误命令”的提示
                UartWrite("bad command.\r\n", sizeof("bad command.\r\n")-1);
                return;
        }
        buf[len++] = '\r';  // 有效命令被执行后,在原命令帧之后添加回车换行符后返回给上位机,表示已执行
        buf[len++] = '\n';
        UartWrite(buf, len);
    }
}

void UartRxMonitor(unsigned char ms)  // 串口接收监控函数
{
    static unsigned char cntbkp = 0;
    static unsigned char idletmr = 0;

    if (cntRxd > 0)  // 接收计数器大于零时,监控总线空闲时间
    {
        if (cntbkp != cntRxd)  // 接收计数器改变,即刚接收到数据时,清零空闲计时
        {
            cntbkp = cntRxd;
            idletmr = 0;
        }
        else
        {
            if (idletmr < 30)  // 接收计数器未改变,即总线空闲时,累积空闲时间
            {
                idletmr += ms;
                if (idletmr >= 30)  // 空闲时间超过 30ms 即认为一帧命令接收完毕
                {
                    cmdArrived = 1; // 设置命令到达标志
                }
            }
        }
    }
    else
    {
        cntbkp = 0;
    }
}
void InterruptUART() interrupt 4  //UART 中断服务函数
{
if (RI)  // 接收到字节
    {
RI = 0;   // 手动清零接收中断标志位
        if (cntRxd < sizeof(bufRxd)) // 接收缓冲区尚未用完时,
        {
            bufRxd[cntRxd++] = SBUF; // 保存接收字节,并递增计数器
        }
}
if (TI)  // 字节发送完毕
    {
        TI = 0;   // 手动清零发送中断标志位
        flagOnceTxd = 1;  // 设置单次发送完成标志
     }
}
/************************main.c 文件程序源代码 **************************/

#include <reg52.h>

unsigned char T0RH = 0;  //T0 重载值的高字节
unsigned char T0RL = 0;  //T0 重载值的低字节

extern void LcdInit();
extern void LcdShowStr(unsigned char x, unsigned char y, const unsigned char *str);
extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
extern void ConfigUART(unsigned int baud);
extern void UartRxMonitor(unsigned char ms);
extern void UartDriver();
void ConfigTimer0(unsigned int ms);
void InitShowStr();

void main ()
{
    EA = 1;           // 开总中断
    ConfigTimer0(1);  // 配置 T0 定时 1ms
    ConfigUART(9600); // 配置波特率为 9600
    LcdInit();        // 初始化液晶
    InitShowStr();    // 初始显示内容

    while(1)
    {
        UartDriver();
    }
}

void InitShowStr()  // 处理液晶屏初始显示内容
{
    unsigned char str[17];

    str[16] = '\0';         // 在最后添加字符串结束符,确保字符串可以结束
    E2Read(str, 0x20, 16);  // 读取第一行字符串,其 E2 起始地址为 0x20
    LcdShowStr(0, 0, str);  // 显示到液晶屏
    E2Read(str, 0x40, 16);  // 读取第二行字符串,其 E2 起始地址为 0x40
    LcdShowStr(0, 1, str);  // 显示到液晶屏
}
void ConfigTimer0(unsigned int ms)  //T0 配置函数
{
    unsigned long tmp;

    tmp = 11059200 / 12;      // 定时器计数频率
    tmp = (tmp * ms) / 1000;  // 计算所需的计数值
    tmp = 65536 - tmp;        // 计算定时器重载值
    tmp = tmp + 18;           // 修正中断响应延时造成的误差

    T0RH = (unsigned char)(tmp >> 8);  // 定时器重载值拆分为高低字节
    T0RL = (unsigned char)tmp;
    TMOD &= 0xF0;   // 清零 T0 的控制位
    TMOD |= 0x01;   // 配置 T0 为模式 1
    TH0 = T0RH;     // 加载 T0 重载值
    TL0 = T0RL;
    ET0 = 1;        // 使能 T0 中断
    TR0 = 1;        // 启动 T0
}
void InterruptTimer0() interrupt 1  //T0 中断服务函数
{
    TH0 = T0RH;  // 定时器重新加载重载值
    TL0 = T0RL;
    UartRxMonitor(1);  // 串口接收监控
}
我们在学习 UART 通信的时候,刚开始也是用的 IO 口去模拟 UART 通信过程,最终实现和电脑的通信。而后我们的 STC89C52RC 由于内部具备了 UART 硬件通信模块,所以我们直接可以通过配置寄存器就可以很轻松的实现单片机的 UART 通信。同样的道理,我们这个 I2C 通信,如果我们单片机内部有硬件模块的话,单片机可以直接自动实现I2C 通信了,就不需要我们再进行 IO 口模拟起始、模拟发送、模拟结束,配置好寄存器,单片机就会把这些工作全部做了。
不过我们的 STC89C52RC 单片机内部不具备I2C 的硬件模块,所以我们使用 STC89C52RC 单片机进行I2C 通信必须用 IO 口来模拟。使用 IO 口模拟I2C ,实际上更有利于我们彻底理解透彻I2C 通信的实质。当然了,通过学习 IO 口模拟通信,今后我们如果遇到内部带I2C 模块的单片机,也应该很轻松的搞定,使用内部的硬件模块,可以提高程序的执行效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值