I2C总线与EEPROM

发布者:勾剑寒最新更新时间:2015-03-19 来源: diangon关键字:I2C总线  EEPROM  单片机 手机看文章 扫描二维码
随时随地手机看文章
  I2C总线是由PHILIPS公司开发的两线式串行总线,多用于连接微处理器及其外围设备。I2C总线的主要特点是接口方式简单,两条线可以挂多个参与通信的器件,即多机模式,而且任何一个器件都可以作为主机,当然同一时刻只能一个主机。

  从原理上来讲,UART属于异步通信,比如电脑发送给单片机,电脑只负责把数据通过TXD发送出来即可,接收数据是单片机自己的事情。而I2C属于同步通信,SCL时钟线负责收发双方的时钟节拍,SDA数据线负责传输数据。I2C的发送方和接收方都以SCL这个时钟节拍为基准进行数据的发送和接收。

  从应用上来讲,UART通信多用于板间通信,比如单片机和电脑,这个设备和另外一个设备之间的通信。而I2C多用于板内通信,比如单片机和我们本章要学的EEPROM之间的通信。

  1、I2C时序初步认识

  在硬件上,I2C总线是由时钟总线SCL和数据总线SDA两条线构成,连接到总线上的所有的器件的SCL都连到一起,所有的SDA都连到一起。I2C总线是开漏引脚并联的结构,因此我们外部要添加上拉电阻。对于开漏电路外部加上拉电阻的话,那就组成了线“与”的关系。总线上线“与”的关系,那所有接入的器件保持高电平,这条线才是高电平。而任意一个器件输出一个低电平,那这条线就会保持低电平,因此可以做到任何一个器件都可以拉低电平,也就是任何一个器件都可以作为主机,如图1所示,我们添加了R63和R64两个上拉电阻。

I2C总线的上拉电阻

图1 I2C总线的上拉电阻

虽然说任何一个设备都可以作为主机,但绝大多数情况下我们都是用微处理器,也就是我们的单片机来做主机,而总线上挂的多个器件,每一个都像电话机一样有自己唯一的地址,在信息传输的过程中,通过这唯一的地址可以正常识别到属于自己的信息,在我们的KST-51开发板上,就挂接了2个I2C设备,一个是24C02,一个是PCF8591。

我们在学习UART串行通信的时候,知道了我们的通信流程分为起始位、数据位、停止位这三部分,同理在I2C中也有起始信号、数据传输和停止信号,如图2所示。

          I2C时序流程图

图2 I2C时序流程图

  从图上可以看出来,I2C和UART时序流程有相似性,也有一定的区别。UART每个字节中,都有一个起始位,8个数据位和1位停止位。而I2C分为起始信号,数据传输部分,最后是停止信号。其中数据传输部分,可以一次通信过程传输很多个字节,字节数是不受限制的,而每个字节的数据最后也跟了一位,这一位叫做应答位,通常用ACK表示,有点类似于UART的停止位。

  下面我们一部分一部分的把I2C通信时序进行剖析。之前我们学过了UART,所以学习I2C的过程我尽量拿UART来作为对比,这样有助于更好的理解。但是有一点大家要理解清楚,就是UART通信虽然我们用了TXD和RXD两根线,但是实际一次通信,1条线就可以完成,2条线是把发送和接收分开而已,而I2C每次通信,不管是发送还是接收,必须2条线都参与工作才能完成,为了更方便的看出来每一位的传输流程,我们把图2改进成图3。

            I2C通信流程解析

图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部分所示。

  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的电路图,如图4所示。

 24C02原理图

图4 24C02原理图

  从图4可以看出来,我们的A2、A1、A0都是接的GND,也就是说都是0,因此我们的7位地址实际上是二进制的1010000,也就是0x50。我们用I2C的协议来寻址0x50,另外再寻址一个不存在的地址0x62,寻址完毕后,把返回的ACK显示到我们的1602液晶上,大家对比一下。

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

#include 

 

#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 != '')      //连续写入字符串数据,直到检测到结束符

    {

        LcdWriteDat(*str);

        str++;

    }

}

void LcdInit()  //液晶初始化函数

{

    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口

    LcdWriteCmd(0x0C);  //显示器开,光标关闭

    LcdWriteCmd(0x06);  //文字不动,地址自动+1

    LcdWriteCmd(0x01);  //清屏

}

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

#include 

#include 

 

#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] = '';

    LcdShowStr(0, 0, str);     //显示到液晶上

    

    ack = I2CAddressing(0x62); //查询地址为0x62的器件

    str[0] = '6';              //将地址和应答值转换为字符串

    str[1] = '2';

    str[2] = ':';

    str[3] = (unsigned char)ack + '0';

    str[4] = '';

    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之后,我们程序就可以直接使用这个库函数了。

  还有一点要提一下,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位数据依次发送了出去。其他的逻辑大家对照前边讲到的理论知识,认真研究明白就可以了。

  3、EEPROM的学习

  在实际的应用中,保存在单片机RAM中的数据,掉电后数据就丢失了,保存在单片机的FLASH中的数据,又不能随意改变,也就是不能用它来记录变化的数值。但是在某些场合,我们又确实需要记录下某些数据,而它们还时常需要改变或更新,掉电之后数据还不能丢失,比如我们的家用电表度数,我们的电视机里边的频道记忆,一般都是使用EEPROM来保存数据,特点就是掉电后不丢失。我们板子上使用的这个器件是24C02,是一个容量大小是2Kbit位,也就是256个字节的EEPROM。一般情况下,EEPROM拥有30万到100万次的寿命,也就是它可以反复写入30-100万次,而读取次数是无限的。

  24C02是一个基于I2C通信协议的器件,因此从现在开始,我们的I2C和我们的EEPROM就要合体了。但是大家要分清楚,I2C是一个通信协议,它拥有严密的通信时序逻辑要求,而EEPROM是一个器件,只是这个器件采样了I2C协议的接口与单片机相连而已,二者并没有必然的联系,EEPROM可以用其他接口,I2C也可以用在其它很多器件上。

  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显示程序也可以直接拿过去用,大大提高了程序移植的方便性。[page]

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

#include 

#include 

 

#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 

 

#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 != '')      //连续写入字符串数据,直到检测到结束符

    {

        LcdWriteDat(*str);

        str++;

    }

}

void LcdInit()  //液晶初始化函数

{

    LcdWriteCmd(0x38);  //16*2显示,5*7点阵,8位数据接口

    LcdWriteCmd(0x0C);  //显示器开,光标关闭

    LcdWriteCmd(0x06);  //文字不动,地址自动+1

    LcdWriteCmd(0x01);  //清屏

}

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

#include 

 

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] = '';

    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功能模块的一部分是必要的,方便我们这个文件以后移植到其他程序中使用,因此这个警告在这里就不必管它了。

  3.2 EEPROM多字节读写操作时序

  我们读取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 

 

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

    {

        buf[i] = buf[i] + 1 + i;

    }

    E2Write(buf, 0x90, sizeof(buf));      //再写回到E2中

    

    while(1)

    {}

}

[page]

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是否响应,正常响应后才可以写数据。

  3.3 EEPROM的页写入

  如果每个数据都连续写入,像我们上节课那样写的时候,每次都先起始位,再访问一下这个EEPROM的地址,看看是否响应,感觉上效率太低了。因此EEPROM的厂商就想了一个办法,把EEPROM分页管理。24c01、24c02这两个型号是8个字节一个页,而24c04、24c08、24c16是16个字节一页。我们板子上的型号是24C02,一共是256个字节,8个字节一页,那么就一共有32页。

  分配好页之后,如果我们在同一个页内连续写入几个字节后,最后再发送停止位的时序。EEPROM检测到这个停止位后,统一把这一页的数据写到非易失区域,就不需要像上节课那样写一个字节检测一次了,并且页写入的时间也不会超过5ms。如果我们写入的数据跨页了,那么写完了一页之后,我们要发送一个停止位,然后等待并且检测EEPROM的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在一定程度上提高我们的写入效率。

 

 

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

 略

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

/***********************eeprom.c文件程序源代码*************************/

#include 

 

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 

 

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

    {

        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给标注了开始位置和结束位置,如图5和图6所示,右侧显示的|T1-T2|就是最终写入5个字节所耗费的时间。多字节一个一个写入,每次写入后都需要再次通信检测EEPROM是否在“忙”,因此耗费了大量的时间,同样的写入5个字节的数据,一个一个写入用了8.4ms左右的时间,而使用页写入,只用了3.5ms左右的时间。

      多字节写入时间

图5 多字节写入时间

    跨页写入时间

图6 跨页写入时间

  4、I2C和EEPROM的综合实验学习

  电视频道记忆功能,交通灯倒计时时间的设定,户外LED广告的记忆功能,都有可能有类似EEPROM这类存储器件。这类器件的优势是存储的数据不仅可以改变,而且掉电后数据保存不丢失,因此大量应用在各种电子产品上。

  我们这节课的例程,有点类似广告屏。上电后,1602的第一行显示EEPROM从0x20地址开始的16个字符,第二行显示EERPOM从0x40开始的16个字符。我们可以通过UART串口通信来改变EEPROM内部的这个数据,并且同时改变了1602显示的内容,下次上电的时候,直接会显示我们更新过的内容。

  这个程序所有的相关内容,我们之前都已经讲过了。但是这个程序体现在了一个综合程序应用能力上。这个程序用到了1602液晶、UART实用串口通信、EEPROM读写操作等多个功能的综合应用。写个点亮小灯好简单,但是我们想学会真正的单片机,必须得学会这种综合程序的应用,实现多个模块同时参与工作,这个理念在我们的全板子测试视频里已经有所体现。因此同学们,要认认真真的把工程建立起来,一行一行的把程序编写起来,最终巩固下来。

 

 

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

                                       略

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

/***********************eeprom.c文件程序源代码*************************/

/************************uart.c文件程序源代码***************************/

#include 

 

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

    {

        *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 != '')

    {

        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 != '') //拷贝字符串直到输入字符串结束

    {

        *out = *in;

        out++;

        in++;

        i++;

        if (i >= 16)   //当拷贝长度已达到16字节时,强制跳出循环

            break;

    }

    for ( ; i<16; i++) //如不足16个字节则用空格补齐

    {

        *out = ' ';

        out++;

    }

    *out = '';       //最后添加结束符

}

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

        {

            buf[i] = 0;

        }

        len = UartRead(buf, sizeof(buf)); //将接收到的命令读取到缓冲区中

        for (i=0; i

        {

            if (CmdCompare(buf, cmdList[i]) == 1) //检测到相符命令时退出循环,此时的i值就是该命令在列表中的下标值

            {

                break;

            }

        }

        switch (i) //根据比较结果执行相应命令

        {

            case 0:

                buf[len] = '';                       //为接收到的字符串添加结束符

                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] = '';

                TrimString16(str, buf+sizeof(cmd1)-1);

                LcdShowStr(0, 1, str);

                E2Write(str, 0x40, sizeof(str));       //保存字符串2,其E2起始地址为0x40

                break;

            default:  //i大于命令列表最大下标时,即表示没有相符的命令,给上机发送“错误命令”的提示

                UartWrite("bad command. ", sizeof("bad command. ")-1);

                return;

        }

        buf[len++] = ' ';  //有效命令被执行后,在原命令帧之后添加回车换行符后返回给上位机,表示已执行

        buf[len++] = ' ';

        UartWrite(buf, len);

    }

}

[page]

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 

 

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] = '';         //在最后添加字符串结束符,确保字符串可以结束

    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模块的单片机,也应该很轻松的搞定,使用内部的硬件模块,可以提高程序的执行效率。

关键字:I2C总线  EEPROM  单片机 引用地址:I2C总线与EEPROM

上一篇:串行通信的初步认识
下一篇:SPI时序初步认识

推荐阅读最新更新时间:2024-03-16 13:55

单片机延时问题20问及解决方法
1、单片机延时程序的延时时间怎么算的? 答:如果用循环语句实现的循环,没法计算,但是可以通过软件仿真看到具体时间,但是一般精精确延时是没法用循环语句实现的。 如果想精确延时,一般需要用到定时器,延时时间与晶振有关系,单片机系统一般常选用11.059 2 MHz、12 MHz或6 MHz晶振。第一种更容易产生各种标准的波特率,后两种的一个机器周期分别为1 μs和2 μs,便于精确延时。本程序中假设使用频率为12 MHz的晶振。最长的延时时间可达216=65 536 μs。若定时器工作在方式2,则可实现极短时间的精确延时;如使用其他定时方式,则要考虑重装定时初值的时间(重装定时器初值占用2个机器周期)。 2、求个单片机89S51 12
[单片机]
51单片机STC89C52控制LED闪烁(将延时程序写成子函数有形参)
/*-----------------------包含头文件区域-------------------------*/ #include reg52.h //单片机头文件 /*-----------------------端口/引脚定义区域----------------------*/ sbit LED=P2^0; //位定义P2.0引脚名为LED /*-----------------------函数声明区域---------------------------*/ void delay(unsigned int xms);//延时函数声明 /*-----------------------主函数区域-----
[单片机]
AVR单片机教程——数字输入
我们已经学习了如何使用按键和拨动开关,不知你有没有好奇 button_down 和 switch_status 等函数是如何实现的。本篇教程带你一探究竟,让我们从按键的原理开始。 在原理图中,按键的符号如下图所示: 符号很简单,就是两个触点上方有一个动片,当按下时与两个触点接触。实际上按键内部的机械结构大体上就是这样,实现的功能是,没有按下时两端断路,按下时两端短路。 还有一种画法是这样的,即电键: 就按键内部的机械结构来说,第一种更加真实,但从电路角度来看,两者没什么区别。 但是我们的开发板上的按键有4个引脚,这是怎么回事呢?其实上面两个和下面两个分别是连通的,相当于只有两个: 拨动开关,相当于单刀双掷开关: 从
[单片机]
AVR<font color='red'>单片机</font>教程——数字输入
MCU或MPU上生成AI算法,进行对嵌入式设备操控
人工智能浪潮浩浩荡荡,势不可挡。国内外巨头玩家,早已盯准了云端的超级赛道,重金部署。但随着技术的不断迭代演进,大量处理数据的方式和位置在不断变化,硬件和连接性方面也出现了新的发展趋势。 目前大部分方式是通过云端联网和数据中心来进行大规模计算,从而实现人工智能,比如整个城市的智能交通。云端具备连结多方大数据,拥有超强计算力的优势,在人工智能的发展中占据着不可替代的作用。但是真正要让人工智能走进生活,成为我们身边看得见摸得着的智能,就需要将AI从云端拉向终端。 云计算这一术语已经在大多数消费者的消费理念中占据了一席之地,边缘计算可以看做是无处不在的云计算和物联网(IoT)的延伸概念。我们可以将边缘计算看作是远离核心的移动计算。
[网络通信]
基于51系列单片机的通用软件UART的实现
0 引言 嵌入式系统开发已经进入了32位时代,8位MCU市场趋于稳定,32位的MPU代表着嵌入式技术的发展方向。然而,作为嵌入式系统低端应用的代表,8位单片机在家用电器、仪器仪表等领域仍然被广泛应用;而且随着IC技术的不断发展,单片机的扩展能力越来越强,8位单片机的开发、应用仍然受到很大重视。 随着网络技术和通信技术的不断发展,对单片机的通信能力要求越来越高,异步通信技术通信距离远、节约成本、通信可靠,特别是以其通信速度越来越快的特点广泛应用在分级、分层和分布式控制系统以及远程通信中,尤其适合单机转向多机或联网的应用方向。目前普遍应用的MCS-51系列和其他一些专用的单片机通常只具有一个UART异步串行通信接口,而在实际
[单片机]
基于51系列<font color='red'>单片机</font>的通用软件UART的实现
AT240C02中EEPROM保存数据
/*********************** 程序功能: 利用定时器产生一个0~99秒变化的秒表,并且显示在数码管上, 每过一秒将这个变化的数写入板上AT24C02内部。当关闭电源, 并再次打开电源时,单片机先从AT24C02中将原来写入的数读取出来, 接着此数继续变化并显示在数码管上。 ************************/ /************************ CODE: ************************/ #include reg52.h #define uint unsigned int #define uchar unsigned char #define AT
[单片机]
51单片机 矩阵按键控制数码管显示0~F
通过按下4*4矩阵按键让数码管显示对应的数值 #include reg52.h typedef unsigned char u8; typedef unsigned int u16; u8 code smgduan ={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; //0 1 2 3 4 5 6 7 8 9 A b C d E F u16 key_value; void delay(u16 i) { while(i--); } void keyscan() { P1=
[单片机]
意法推出STM32WBA5系列微控制器:强化物联网连接与安全
意法半导体(ST)近日宣布推出全新的STM32WBA5系列微控制器,该系列不仅继承了STM32家族在性能、能效及易用性方面的优良传统,更在无线连接与安全性方面进行了显著的提升,以满足日益增长的物联网设备需求。 STM32WBA5系列微控制器内置了Arm Cortex-M33内核,运行频率高达100MHz,配备了丰富的外设接口和高达1MB的闪存、128kB的静态RAM(SRAM)。此外,该系列还支持Arm TrustZone安全隔离架构,为物联网设备提供了硬件级别的安全保障。 在无线连接方面,STM32WBA5系列整合了蓝牙5.4低功耗(BLE)、Zigbee、Thread和Matter等多种网络协议,实现了与各类物联网设备
[物联网]
意法推出STM32WBA5系列<font color='red'>微控制器</font>:强化物联网连接与安全
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved