声明:本人跟随b站江科大学习,本文章是观看完视频后的一些个人总结和经验分享。(仅仅是个人的理解和看法,可能有误,也希望大家能帮忙纠正,谢谢大家❤️)
目录
3)SCL和SDA各添加一个上拉电阻,阻值一般为4.7千欧左右:
相信各位小伙伴看完江科大这期AT24C02(I2C总线)的视频还是会有点疑惑的,首先肯定是不知道iic的电路规范为什么是这样的,其次是时序结构里面出现了“释放总线”这个初学比较抽象的概念,对主机和从机的概念也比较模糊,然后就是iic和AT24C02的数据帧及其代码的敲写。
在介绍下面内容之前先给大家留几个问题,在看完文章之后,可以问一下自己这几个问题,觉得没问题,那恭喜你这篇文章你理解的差不多了💪💪💪
课前问题
- SCL和SDA分别是什么
- “线与”的作用
- 为什么要使用开漏输出和上拉电阻
- 为什么接受字节和接受应答需要释放SDA,发送字节和发送应答呢
- SDA置1为什么就能够释放总线
正文部分
一、IIC和AT24C02理论部分
1.基本介绍
负责同步作用的是我们的SCL串行时间线,半双工则是因为接受和传输都只用SDA串行数据线这一根线,带数据应答则是发送字节和接受字节这里都会产生应答,表示有没有接收到数据(后面还会细说)。
PS:同步和,异步和半双工不太清楚的小伙伴可以去阅读一下这篇文章,以加强理解。单片机学习笔记---串口通信(1)_单片机db9-CSDN博客
2.电路规范
1)所有的I2C设备的SCL连在一起,SDA连在一起:
在这里是为区别于串口通信使TXD和RXD交叉连接
2)设备的SCL和SDA均要配置成开漏输出模式:
在了解开漏输出之前,先给大家介绍一下弱上拉(我们的IO口其实就是一种弱上拉,而且其实也是一种开漏输出,只不过加了一个弱上拉电阻而已),目的是增强IO口的驱动能力,那为什么能够增强IO口的驱动能力呢?
看这个图,其中R为弱上拉电阻,当S处给0时,开关闭合,给1开关断开(可能是因为是三极管中的PNP类型),如果开关闭合,则会连通GND,M端输出则为0,如果开关断开,则直接连接与VCC,M端输出为1,由于高电平驱动力弱,低电平驱动能力强,所以当S处给0时,IO口的驱动能力更强。
那我们的开漏输出其实就是没有这个弱上拉电阻
那这个时候我们S给0,M端输出还是0,但是当S给1时,M端就处于一种名为高阻态的状态,即引脚浮空,输出既不是1也不是0。那么显而易见上拉电阻那部分电路的作用其实就出来了,即实现外部上拉,让我们的IO口具有输出1的能力。
PS:通常开漏输出和推挽输出的区别就在于,推挽输出有上拉电阻那部分的电路,使该输出模式具有输出1的能力。
介绍完弱上拉和开漏输出之后,那究竟为什么要将SCL和SDA全部配置成开漏输出呢?
原来是因为我们的iic能够实现多机通信,当CPU想要和其中一个从机单独进行通信时,其他从机或多或少会对通信产生影响,那么我们在与该从机通讯时,需要将其他从机全部断开,这个时候我们只需要利用开漏模式,将引脚给1,那么该引脚浮空,就当于断开了。
PS:你们可能还会有个疑问,那就是如果SDA和SCL全部采用弱上拉,引脚全部置1,由于高电平驱动能力弱,相当于断开从机,这样看似乎也行。但是要知道全部采取弱上拉的时候,弱上拉电阻不断并联,总阻值变小,上拉能力越强(大概原因是因为当电压U不变时,I=U/R,R越小I越大,上拉能力越强),此时其他从机的上拉电阻多少会对正在通信的从机产生影响,故还是采用开漏输出最好。
3)SCL和SDA各添加一个上拉电阻,阻值一般为4.7千欧左右:
上面说了开漏输出是不具备输出1的能力的,CPU(这里的CPU相当于开关S,M相当于SDA或者SCL)如果想发送1,由于没有东西能把电平拉高,我们的SDA和SCL就不能等于1,但是我们iic作为一种通信,两根通信线不可能不具备输出1的能力(即SDA=1和SCL=1),所以我们在SCL和SDA各加上一个上拉电阻,使具备输出1的能力。所以当其他从机都断开时候,我们CPU如果要发送0(即开关S=0),那么从机(从机连着GND)就连上了,这个时候M端就输出0(即SDA或者SCL等于0),如果要发送1(S=1),那从机就断开连接了,上拉电阻会自动把M端电平拉高,输出1。
我们这里所说的SDLA和SCL=1,电平拉高实际上就是拉高iic总线,SDA,SCL=0就是拉低总线
4)这一点我放在下面讲
其中2)和3)的理解很重要,两个的作用也是为了解决iic多机通信互相干扰的问题
3.线与
上面说开漏输出和上拉电阻共同实现了"线与"功能,那什么是“线与”呢?
这里简单介绍一下线与:是一种存在于总线上的逻辑关系,“与”顾名思义,即逻辑与“&”,举个例子,现在所有从机的输出都为1,那么1&1还是1,但是如果有一个从机输出为0,那么0&任何数都为0,那么其余从机无论输出什么,最后都会变成0。
所以我们使用开漏输出和上拉电阻后断开了其他从机,总线上只有主机和被通信从机的线与关系,由于iic支持多主机和多从机进行通信,那么如何判断谁占用了总线呢?线与机制就是用来实现总线仲裁的,判断谁占用了总线。(这里有个低电平优先原则:哪个设备先发送低电平,谁就控制总线;我是0,那么你们输出是0是1都要看我,差不多就这个意思)
PS:不清楚总线制裁的可以看看这篇文章IIC总线仲裁机制_iic仲裁机制-CSDN博客
4.主机和从机和从机的区别
主机:既能够操作SCL线也能够操作SDA(虽然能够有多个主机,但是由于总线制裁,所以只能有一个主机操作SCL)
从机:只能够操作SDA线
5.时序结构
起始和终止的时序比较简单,需要注意的地方是,“起始”里面的SCL在SDA电平切换完成后,自身需要从高电平下拉到低电平,“停止”则不需要
发送字节需要注意的点:
- SCL低电平期间开始放数据
- 主机将数据位依次放在SDA线上(即主机操纵SDA线)
- 高位在前(即先读取二进制的最高位)
- 拉高SCL,从机读取数据
- 拉高SCL读完数据后,SCL还要拉低
- 图中SDA有两条线,并不是真的有两条,而是有两种电平转换的情况,低转高,或者高转低
接收字节需要注意的点:
- SCL低电平期间开始放数据
- 从机将数据位依次放在SDA线上(即从机操纵SDA线)
- 高位在前(即先读取二进制的最高位)
- 拉高SCL,主机读取数据
- 拉高SCL读完数据后,SCL还要拉低
- 主机在接收之前,需要释放SDA(SDA置1)
解释为什么需要释放SDA,以及为什么SDA置1就是释放总线
上面我们讲过从机和主机都能操纵SDA,之间存在线与关系。但是接受字节时要求是从机操纵SDA,如果不释放SDA将SDA控制权交给从机的话,那么由于线与关系,我们主机若果输出0的话,那么无论从机发送啥,输出都会变为0,这样肯定是不行的。所以我们在主机接受数据之前要将SDA置1,将SDA控制权交给从机,是从机能够正常发送数据。
那么为什么SDA置1就是释放总线呢?
我们刚刚讲过弱上拉,但是在这里SDA线既是M端又是开关S。当SDA置0时,开关闭合,SDA靠的是从机或者主机的VCC将其拉低为低电平,根据低电平优先原则,那就相当于掌握了SDA的控制权;当SDA置1时,开关断开,SDA靠的是上拉电阻外部上拉使其为高电平,那么由于开关断开,你失去将SDA置0的能力了,就是失去对SDA的控制权,那么这个时候其他设备就可以根据总线制裁抢SDA的控制权,所以我们一般说SDA置1就是释放总线
那为什么只有接受字节和接受应答才需要释放总线呢?
其实发送字节和发送应答都也释放了总线,因为我们的SDA控制权不可能给从机之后不归还给主机,从机在发送完数据后,从机内部其实会自动释放总线,控制权就又回到了主机的手里,在代码里面我们不写从机代码,看不到从机释放总线的过程,就会误认为发送不需要释放总线
- 图中SDA有两条线,并不是真的有两条SDA,而是有两种电平转换的情况,低转高,或者高转低
发送应答和接受应答差不多,都是为了让主机(从机)知道从机(主机)接受到了字节,区别有以下几点:
1)发送应答用于接收字节之后,接收应答用于发送字节之后
2)发送应答:主机在SCL低电平时候变换SDA发送应答位,从机在SCL高电平时读取(由于此时主机变换SDA,所以操纵SDA的为主机)
接收应答:从机在SCL低电平时候变换SDA发送应答位,主机在SCL高电平时读取(由于此时从机变换SDA,所以操纵SDA的为从机)
3)由于在发送应答和接收应答中,操纵SDA的设备不一样,所以在进行接收应答是需要对SDA置1释放总线
6.数据帧
iic数据帧
上面的每一个时序下面都有对应方块表示,这里数据帧这样表示更方便
整个数据帧比较容易看懂,即起始(S)->发送从机地址和读写位(S:SLAVE ADDRESS+W)->接收应答(RA:0)->发送我们其他需要传输的字节(S:BYTE)->接收应答(RA:0)->……->终止(P)
需要介绍的就是从机地址和读写位:
前七位就是我们的从机地址,最后一位是读写位(1为读,0为写),从机地址决定我们是读去(写入)哪一个从机。
PS:前四位A6~A3如果是同样的从机,那就是是固定的,我们每个从机申请iic通信时候,公司会给该设备一个固定的数字,AT24C02就是1010,后面A2~A0三位则是用来处理一个主线挂载多个同类型的从机的情况,控制这三位就可以选择多个相同类型从机中的一个进行输出
如图为我们AT24C02里的一种储存介质,这里的E0,E1,E2就是我们的A0,A1,A2,可以看到都接着GND,我们就全配置为0即可
读写位就是控制向从机中输出数据还是读取从机中的数据,发送应答读写位为写
接受的数据帧也是一样的,都一个道理,这里读写位为读而已,最后的(SA:1),发送应答为0为1都可以,复合格式就是移除发送数据帧的P再加上接收数据帧
AT24C02的数据帧
先了解一下字地址(WORD ADDRESS),即AT24C02中存储空间的地址(有2^8个位置,0~255)。
我们发现与iic的数据帧相比,AT24C02多了一个需要发送字地址的过程,其他的都大差不差。
二、IIC和AT24C02代码部分
写代码之前,给大家强调一句话,只写主机代码,不写从机代码,从机程序自动执行
1.iic
- #include
- //引脚定义
- sbit I2C_SCL=P2^1;
- sbit I2C_SDA=P2^0;
先写起始和终止的时序
- /**
- * @brief I2C开始
- * @param 无
- * @retval 无
- */
- void I2C_Start(void)
- {
- I2C_SDA=1;//先给SDA置1,因为不知道上一次结束时候SDA为1还是0,保证时序不出问题
- I2C_SCL=1;//拉高SCL
- I2C_SDA=0;//拉低SDA
- I2C_SCL=0;//最后清零
- }
-
- /**
- * @brief I2C停止
- * @param 无
- * @retval 无
- */
- void I2C_Stop(void)
- {
- I2C_SDA=0;//一样的先给SDA置0
- I2C_SCL=1;//拉高SCL
- I2C_SDA=1;//拉低SDA
- //那么为什么不需要给SCL清零呢,我们一定要对着时序图写代码,终止的时序最后SCL为1
- }
下面写发送和接收一个字节的时序
- /**
- * @brief I2C发送一个字节
- * @param Byte 要发送的字节
- * @retval 无
- */
-
- //这下面写的都是主机代码
- void I2C_SendByte(unsigned char Byte)
- {
- unsigned char i;
- for(i=0;i<8;i++)//根据时序,每次发送一位,循环八次,发送一个字节
- {
- I2C_SDA=Byte&(0x80>>i);//从高到低取出写入字节的每一位赋值给SDA,
- //即主机将数据放在SDA线上让从机读取
-
- I2C_SCL=1;//拉高SCL让从机读取,这里从机是自动读取的,无需写从机代码
- I2C_SCL=0;//拉高后迅速拉低,由于51单片机速度较慢,无需延时迅速拉低没有影响,
- //但是若涉及到运算速度快的单片机,则应当延时
- }
- }
-
-
- /**
- * @brief I2C接收一个字节
- * @param 无
- * @retval 接收到的一个字节数据
- */
- unsigned char I2C_ReceiveByte(void)
- {
- unsigned char i,Byte=0x00;
- I2C_SDA=1;//先释放总线,让从机获得SDA控制权
- for(i=0;i<8;i++)
- {
- I2C_SCL=1;//拉高SCL,在高电平时候从机将数据放在SDA线上
- if(I2C_SDA){Byte|=(0x80>>i);}//如果从机放的是1,则让Byte的第最高位置1
- //如果放的是0,则Byte最高位默认为0
- //用循环实现每一位的判断与赋值
- I2C_SCL=0;最后拉低SCL
- }
- return Byte;
- }
最后是发送应答和接收应答
- /**
- * @brief I2C发送应答
- * @param AckBit 应答位,0为应答,1为非应答
- * @retval 无
- */
-
- void I2C_SendAck(unsigned char AckBit)
- {
- I2C_SDA=AckBit;//主机发送应答位,将应答位放在SDA线上,让从机读取
- I2C_SCL=1; //拉高SCL,让从机读取,从机自动读取,无需写代码
- I2C_SCL=0; //最后拉低SCL
- }
-
-
- /**
- * @brief I2C接收应答位
- * @param 无
- * @retval 接收到的应答位,0为应答,1为非应答
- */
-
-
- unsigned char I2C_ReceiveAck(void)
- {
- unsigned char AckBit;
- I2C_SDA=1; //释放总线,控制权交给从机
- //中间从机自动将应答位放在SDA线上,不写从机代码
- I2C_SCL=1; //拉高SCL让主机读取
- AckBit=I2C_SDA;//主机读取到应答位,存入AckBit
- I2C_SCL=0; //拉低SCL
- return AckBit; //返回应答位
- }
产生代码上的疑惑,一般都是觉得不写从机代码,时序总感觉对不上,这个地方不需要纠结太多,心里知道从机会什么时候发送,什么时候接受数据就行
这样六个部分的代码就都写好了 ,最后组合成数据帧就能够用于各种各样的iic通信中了
2.at24c02
- #include
- #include "I2C.h"
-
- #define AT24C02_ADDRESS 0xA0//宏定义从机地址
-
- /**
- * @brief AT24C02写入一个字节
- * @param WordAddress 存储要写入字节的位置,0~255
- * @param Data 要写入的数据
- * @retval 无
- */
- void AT24C02_WriteByte(unsigned char WordAddress,Data)
- {
- I2C_Start();
- I2C_SendByte(AT24C02_ADDRESS);//写入从机地址和读写位
- I2C_ReceiveAck();
- I2C_SendByte(WordAddress);//写入字地址
- I2C_ReceiveAck();
- I2C_SendByte(Data);
- I2C_ReceiveAck();
- I2C_Stop();
- }
-
- /**
- * @brief AT24C02读取一个字节
- * @param WordAddress 存储要读出字节的位置
- * @retval 读出的数据
- */
- unsigned char AT24C02_ReadByte(unsigned char WordAddress)
- {
- unsigned char Data;
- I2C_Start();
- I2C_SendByte(AT24C02_ADDRESS);
- I2C_ReceiveAck();
- I2C_SendByte(WordAddress);
- I2C_ReceiveAck();
- I2C_Start();
- I2C_SendByte(AT24C02_ADDRESS|0x01);//这里|0x01是为了使最后一位从0变为1,从写变成读
- I2C_ReceiveAck();
- Data=I2C_ReceiveByte();//获得读到字节赋值给Data
- I2C_SendAck(1);
- I2C_Stop();
- return Data;
- }
由于AT24C02读取和写入字节一次都只能读或写一个字节,能够表示数字的大小就2^8=256,这个时候肯定不会不够用,那么如何让int类型变量存入和读取呢?大家可以看下我的上一篇博客,里面有提到51单片机中操作符的妙用-CSDN博客
本节文章就介绍这么多(大部分都是理论介绍,可能有点枯燥哈,不过我写起来倒是觉得挺有趣的),希望对大家能够有所帮助!!!以后还会继续写博客,与大家分享在学习过程中遇到一些难题和趣题,以及个人学习的经验,也欢迎大家在评论区发表自己的意见,相互交流经验,指出不足,最后谢谢大家阅读!( ˘ ³˘)♥
评论记录:
回复评论: