与之前的笔记一样,为了让自己更好的总结,也为了和大家一起交流学习,写下了这些学习总结。如果能帮助到学习中的各位,我不胜欢喜。如果笔记中有错误,欢迎大家指出。欢迎大家私聊一起学习进步噢
学习过程中,个人认为《手把手教你51单片机》真的是一本很好的书,有很详细的解析和代码,推荐给大家。
如果不想看一大堆的知识点,可以直接翻到后面的模块化代码。
知识点
在每次我们学习新知识之前,必须要先了解这个东西是什么,之前对于I2C和Uart都是一个模模糊糊的认识,没有一个比较明确的概念,查询资料后总结如下:
微型计算机、单片机系统大都采用总线结构,这种结构就是采用一组公共的信号线来作为微型计算机各部件之间的通信线,这个公共信号线就被称作为总线,单片机常用总线就是并行总线和串行总线,其一个区别就是串行总线就是一位一位发送数据,而并行总线是一次性发送多位数据。
问题就紧接着来了:我们怎么能够保证我的信号能准备送到我需要的那一个部件上呢?
这样就有了不同的串行总线,其实就是不同种类的使用规则,来实现我们的目的。
一般我们使用的串线总线有四种:UART,1-wire,I2C,SPI总线
Uart:异步通信(一条数据输出线,一条数据输出线)
1-wire:单总线
I2C:同步串行2线方式进行通信(一条时钟线,一条数据线)
SPI:同步串行3线方式进行通信(一条时钟线,一条数据输入线,一条数据输出线)
我们这里学习的是I2C总线,首先一定要记住两条总线的名称!!
SDA(serial data I/O) 数据线
SCL(serial clock) 时钟线
这两个名字记住了,我们之后学才能看得懂
在清翔单片机里面学到,I2C总线上可以挂多个器件,每一个器件又有唯一的地址,于是我们就可以用这个地址来找到我们需要的部件。而数据之间的方式采用的是,主机主动联系从机,从机被动的返回数据。
(注:多机系统中,可能会有几个主机要同时要求从机返回信号,而总线数据线只有一条,这时候就需要用到总线仲裁来确定哪一个主机控制从机)
I2C总线通过上拉电阻连接正电源。当总线空闲的时候,两根线均为高电平。连到总线的任一器件输出的都是低电平,都将使总线的信号变低,各器件的SDA和SCL都是线“与”的关系,即SDA = 1 才能够释放总线,一旦SDA = 0,那么总线被占,其他器件发1发0都会失效。
采用了漏极接线模式
传输协议
知道了知识点之后,我们就需要来了解如何传输信号了。
SCL为高电平期间,SDA数据保持稳定,即稳定传输一个数据
SCL为低电平期间,SDA才可以变,即发送数据
起始和终止信号
SCL为高电平期间,SDA产生 高–>低 的信号表示起始信号
SCL为高电平期间,SDA产生 低–>高 的信号表示终止信号
(注:SDA、SCL接在I/O口上,需要我们自行调高调低,所以起始和终止信号都需要我们自己控制产生,当起始信号产生之后,总线就被占用,当终止信号产生后,总线就处于空闲状态)
传送和应答
传输按照一个字节一个字节传输,需要保证8位长度,并且每一个字节后面必须要跟一个应答位(所以一帧信号有9位)
模块化代码
但是80C51芯片的单片机上是没有I2C接口的,所以我们要采用软件来模仿I2C通信协议。
I2C的最底层操作有五个,起始信号,终止信号,字节写,字节读+应答,字节读+非应答。字节读+应答就是告诉部件,我还想继续读,你继续发数据。字节读+非应答就是告诉部件,我不想读了,你不用发了数据了。
//pbdata.h
#ifndef __PBDATA_H__
#define __PBDATA_H__
#define uchar unsigned char
#define uint unsigned int
#include
#include "I2C.h"
/*I2C*/
sbit I2C_SCL = P2^1;
sbit I2C_SDA = P2^0;
#endif
//I2C.h
#ifndef __I2C_H__
#define __I2C_H__
void I2CStart();
void I2CStop();
bit I2CWrite(unsigned char dat);
unsigned char I2CReadNAK();//读+非应答
unsigned char I2CReadACK();//读+应答
#endif
//I2C.c
#include
#include "pbdata.h"
#define I2Delay(){_nop_;_nop_;_nop_;_nop_;}
/* 产生总线起始信号 */
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-待写入字节,返回值-从机应答位的值 */
bit I2CWrite(unsigned char 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=存在且空闲或写入成功
}
/* 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;
}
/* I2C 总线读操作,并发送应答信号,返回值-读到的字节 */
unsigned char I2CReadACK()
{
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;
}
如果不想看一大堆的知识点,可以直接翻到后面的模块化代码。
知识点
我们如果用电脑来推及到单片机,其实E2Prom就相当于电脑的硬盘,它可以在单片机掉电之后仍然保存数据,达到存储数据的目的。特性就是掉电不丢失。
E2Prom和I2C两个其实是需要合体一起使用的,但是E2Prom != I2C,I2C是通信协议,而E2Prom是使用I2C通信协议来使用的一个硬件。
使用的芯片
要重点注意的是,AT24C系列的E2Prom芯片地址的固定部分是1010,然后A2.A1.A0引脚接高低电平后得到确定的3位编码,形成的7位编码就是地址码
写数据流程
读数据的过程
注:在读数据和写数据的过程中,E2Prom的地址会自动+1。
代码
//E2Prom.h
#ifndef __E2Prom_H__
#define __E2Prom_H__
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);
#endif
#include "pbdata.h"
//调用I2C.c中的函数
extern void I2CStart();
extern void I2CStop();
extern unsigned char I2CReadACK();
extern unsigned char I2CReadNAK();
extern bit I2CWrite(unsigned char dat);
/* E2 读取函数,buf-数据接收指针,addr-E2 中的起始地址,len-读取长度 */
void E2Read(unsigned char *buf, unsigned char addr, unsigned char len)
{
do { //用寻址操作查询当前是否可进行读写操作
I2CStart(); //给一个起始信号
if (I2CWrite(0x50<<1))//0x50=0101 //应答则跳出循环,非应答则进行下一次查询
{ //0x50<<1即1010,也就是24C02的前面固定位
break;
}
I2CStop(); //终止信号
} while(1);
I2CWrite(addr); //写入起始地址,因为是从循环出来,I2CStart已经调用过一次
I2CStart(); //发送重复启动信号
I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作
while (len > 1) //连续读取len-1 个字节
{
*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
len--;
}
*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
I2CStop();
}
/* E2 写入函数,buf-源数据指针,addr-E2 中的起始地址,len-写入长度 */
void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)
{
while (len > 0)
{
//等待上次写入操作完成
do { //用寻址操作查询当前是否可进行读写操作
I2CStart();
if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询
{
break;
}
I2CStop();
} while(1);
//按页写模式连续写入字节
I2CWrite(addr); //写入起始地址
while (len > 0)
{
I2CWrite(*buf++); //写入一个字节数据
len--; //待写入长度计数递减
addr++; //E2 地址递增
if ((addr&0x07) == 0) //检查地址是否到达页边界,24C02 每页8 字节,
{ //所以检测低3 位是否为零即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}