我们这一讲简要讲解如何往EEPROM的地址0x55写入一个数据,然后读出这个数据的内容。本讲代码围绕的是宋老师的lesson14_2例程代码讲解。
1.写入一个字节内容
如果要在EEPROM的某个地址里写入内容,那么步骤的实现归为:
起始信号→找到这个器件是否存在(寻址),发送的字节最低位要为0意为要往这个器件写内容→选择EEPROM的哪个地址去写→写入8位的数据→停止信号。
宋老师写的“void E2WriteByte(unsigned char addr, unsigned char dat)”函数里面,上一讲都讲解过里面的函数了,写入一个字节内容的讲解我们就介绍完了。
2.读出一个字节内容
在“unsigned char E2ReadByte(unsigned char addr)”中前面三个函数与“void E2WriteByte(unsigned char addr, unsigned char dat)”都是一样的操作步骤,选定好要读出哪个地址的内容,然后还需再重新发送起始信号,接着是把寻址的字节最低位设置为1意为要读出EEPROM的某个地址里面的内容,因为只读一个字节,所以单片机在接收完EEPROM发送回来的数据(这个数据就是当初写进去的数据)之后,不产生拉低应答(是单片机不产生应答,不是说EEPROM不产生应答),这样EEPROM就不会再发送数据回来了,达到了只读一个字节的功能。
所谓单片机读EEPROM的数据出来,其实就是EEPROM在SDA线上不停地拉高拉低变化,而单片机就是不断地判断第一位是0或者1,第二位是0或者1······,这个细节过程大家有能力的话可以一步步去解读宋老师写的“unsigned char I2CReadNAK()”和“unsigned char I2CReadACK()”,其实这两个函数只有一处不同,那就是“I2C_SDA = 1;”和“I2C_SDA = 0;”是否产生应答。如果大家一句句地很难理解这个函数,那么不妨直接省去理解,拿来运用就可以了。
3.独立代码
在main.c里复制以下代码,编译下载进开发板,我们做的实验就是先在EEPROM的0x55的地址里写入数据71(也就是字符‘G’的ASCII码值),然后再去读出这个数据,将字符‘G’显示在液晶屏上。
之前的章节有提过为了兼容性我们不打算使用bit型的数据类型,所以修改了一下宋老师的“bit I2CWrite(unsigned char dat)”函数,改为unsigned char类型。
那么我们就不能像宋老师那样写为“return (~ack);”,而是写为“return (!ack);”。unsigned char类型下,ack如果是0,那么~ack就为0xFF;ack如果是1,~ack就为0xFE,所以“~ack”的书写方式会导致返回值不能限定在0或者1之间,也就会使代码功能失效。
而“!ack”的表达就是说,当ack等于0时,!ack只会等于1,不会等于0xFF这些。当ack不等于0时(比如“ack=0xFE;”这些),那么!ack只会等于0,这样保证了返回值只有0和1。
#include <reg52.h> #include <function.h>//详见第六章第8讲 #include <lcd.h> //详见第十一章第3讲 #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(); } /* I2C总线写操作,dat-待写入字节,返回值-从机应答位的值 */ u8 I2CWrite(unsigned char dat) { u8 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=存在且空闲或写入成功 } /* I2C总线读操作,并发送非应答信号,返回值-读到的字节 */ unsigned char I2CReadNAK() { 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; } void main() { u8 str[2]; InitLcd1602();//初始化液晶屏 while (1) { I2CStart(); I2CWrite(0x50<<1); //寻址器件,后续为写操作 I2CWrite(0x55); //写入要存储的地址为0x55 I2CWrite('G'); //写入71这个数据,'G'的表达更加直观地表示要在液晶屏上显示的内容 I2CStop(); delay_ms(1000); //等待一秒之后,准备读出这个数据 I2CStart(); I2CWrite(0x50<<1); //寻址器件,后续为写操作 I2CWrite(0x55); //选择要读出内容的地址为0x55 I2CStart(); //发送重复启动信号 I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作 str[0] = I2CReadNAK(); //读取这个器件0x55的地址里面的内容存放在数组的0号元素里 I2CStop(); LcdShowStr_len(7, 0, str, 1);//显示str[0]的内容 while (1);//程序暂停运行 } }
为了更加直观了解这些过程,笔者并没有像宋老师那样给大家封装好写函数和读函数,这是为了大家不用在各个函数中跳来跳去地分析,在主函数里直接一步到位地讲解这些操作步骤,方便大家对这些细节的了解。
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程