照上一期结束的时候所说的,今天开始我们就进入通信专题的学习了。通信这一专题的学习是非常重要的,我们在上一期的结尾也强调过了。所以,废话不多说,我们马上进入今天的正题。开始我们通信专题的第一个实验——IIC通信实验。今天我们的实验按照正点原子的实验来进行。内容有的多,文章有的长,请耐心观看!
IIC简介
IIC,英文全称是Inter-Integrated Circuit,是一种由 PHILIPS 公司开发的两线式串行总线,用于连接微控制器及其外围设备。多用于主控制器与从设备间的主从通信。
IIC总线的构成由数据线SDA和时钟线SCL构成,可以发送和接收数据。注意:IIC是为了与低速设备通信而发明的,所以IIC的传输速率是不及我们后面学习的SPI通信的。
IIC总线在传输数据时共有三种类型的信号,分别是:开始信号、结束信号和应答信号。
我们先来详细了解一下这三个信号。
1)开始信号
SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
作用:开启IIC通信
代码的实现思路就是先把数据线SDA设置成输入模式,把SDA和SCL全部拉高,延时,再把SDA拉低,就实现了开始信号了。
void IIC_Start(void)
{
SDA_OUT();
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;
delay_us(4);
IIC_SCL=0;
}
2)结束信号
SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。
作用:停止IIC通信
- 开始信号和结束信号都是由主设备产生并发出的。
代码的实现思路同开始信号的思路相似,同样先将SDA和SCL全部拉低,延时,再将SCL和SDA先后拉高,就可以实现结束信号了。
void IIC_Stop(void)
{
SDA_OUT();
IIC_SCL=0;
IIC_SDA=0;
delay_us(4);
IIC_SCL=1;
delay_us(4);
IIC_SDA=1;
}
3)应答信号
主机SCL拉高,读取从机SDA的电平,为低电平表示产生应答
- 应答信号为低电平时,规定为有效应答位(ACK),表示接收器已经成功地接收了该字节
- 应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功
一个字节(8bit)按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位, 此时才认为一个字节真正的被传输完成。
并不是所有的字节传输都必须有一个应答位,比如:当从设备不能再接收主设备发送的数据时,从设备将回传一个非应答位。
作用:指示一个字节的数据传输完成
- 应答信号是由从设备产生并发出的。
代码的实现思路就是先把数据线SDA设置为输出,把数据线SDA和时钟线SCL拉低,最后把SCL拉高一段时间后再拉低,就可以实现应答信号的产生了。
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
如果是非应答信号就是先把数据线SDA设置为输出,把数据线SDA拉高,时钟线SCL拉低,最后把SCL拉高一段时间后再拉低,就可以实现非应答信号的产生了。
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
空闲状态
空闲状态就是不进行任何操作的状态,IIC初始化完成后所在的状态就是空闲状态。所以IIC总线在空闲状态下数据线SDA和时钟线SCL都是高电平。
因此,我们在初始化完成GPIO后,要把数据线SDA和时钟线SCL相应的IO口设置为高电平。
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);
}
数据有效性
IIC信号在数据传输过程中,当SCL=1高电平时,数据线SDA必须保持稳定状态,不允许有电平跳变,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
- 在数据传输中,数据是在时钟线SCL的高电平期间被写入从设备,所以数据的变化要在SCL的低电平期间。
数据传输格式和从设备地址选取
数据传送时,输出到SDA线上的每个字节必须是8位,从最高位(MSB)开始到最低位(LSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有9位)。
- 0表示主设备向从设备写数据。
- 1表示主设备向从设备读数据。
实际上通常所说的8位地址其实就是7位地址后面加上了一个读写位。
由图可以看到,在开始信号开启通信后,在传输数据前要先指定从设备的地址,然后再进行数据传输。数据在传输中是一个字节一个字节地传输的,即每8位一个数据包。最后就是结束信号停止通信。
数据格式是每一小包数据由9位bit组成,即每一帧数据由9bit组成。
- 如果是发送数据,则9bit包含了8bit数据+1bit应答位
- 如果是发送设备地址,则9bit包含了7bit地址+1bit读写位+1bit应答位
发送一个字节的数据
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0; //拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7; //获取相应的数据位,相应位为1则数据线拉高,否则相反
txd<<=1;
delay_us(2); //延时是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
读取一个字节的数据
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN(); //SDA设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck(); //发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
IIC发送数据
Start:起始信号
DEVICE_ADDRESS:从设备地址,就是7位从机地址
R/W:W(write)为写,R(read)为读
ACK:应答信号
WORD_ADDRESS:从机中对应的寄存器地址
DATA:发送的数据
STOP:终止信号
主机要向从机写数据时:
- 主机首先产生START信号,开启传输。
- 然后紧跟着发送一个从机地址,这个地址共有7位,紧接着的第8位是数据方 向位(R/W),0表示主机发送数据(写),1表示主机接收数据(读)。
- 主机发送地址时,总线上的每个从机都将这7位地址码与自己的地址进行比较,若相同,则认为自己正在被主机寻址,根据R/T位将自己确定为发送器和接收器。
- 主机等待从机的应答信号(ACK)。
- 当主机收到应答信号时,发送要访问从机的那个地址, 继续等待从机的应答信号。
- 当主机收到应答信号时,发送N个字节的数据,继续等待从机的N次应答信号。
- 主机产生终止信号,结束传输。
IIC读取数据
主机要从从机读数据时:
- 主机首先产生START信号,开启传输。
- 然后紧跟着发送一个从机地址,注意此时该地址的第8位为0,表明是向从机写命令。
- 主机等待从机的应答信号(ACK)。
- 当主机收到应答信号时,发送要访问的地址,继续等待从机的应答信号。
- 当主机收到应答信号后,主机要改变通信模式(主机将由发送变为接收,从机将由接收变为发送)所以主机重新发送一个开始start信号,然后紧跟着发送一个从机地址,注意此时该地址的第8位为1,表明将主机设 置成接收模式开始读取数据。
- 主机等待从机的应答信号,当主机收到应答信号时,就可以接收1个字节的数据,当接收完成后,主机发送非应答信号,表示不在接收数据。
- 主机进而产生停止信号,结束传输。
以24C02为例
24C02是一个2K Bit的串行EEPROM存储器(掉电不丢失),内部含有256个字节。
对于不同大小的24Cxx,具有不同的从器件地址。24C02为2k容量,也就是对应需要参考图中第一行的内容,就可以知道从器件地址。
AT24CXX设备地址为如下,前四位固定为1010,A2~A0为由管脚电平。AT24CXX的EEPROM Board模块中默认为接地。A2~A0为000,最后一位表示读写操作。所以AT24Cxx的读地址为0xA1,写地址为0xA0。
因此
- 读24C02时,从器件地址为10100000(0xA1)
- 写20C02时,从器件地址为10100000(0xA0)
我们可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。在地址上对应地修改A2、A1、A0三位即可。
向24C02写数据
- 主机先发送一个开始信号(START)启动总线
- 接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0)
- 等待应答信号(ACK)
- 发送数据的存储地址。24C02一共有256个字节的存储空间,地址从0x00~0xFF,想把数据存储>在哪个位置,此刻写的就是哪个地址。
- 发送要存储的数据第一字节、第二字节、…注意在写数据的过程中,E2PROM每个字节都会>回应一个“应答位0”,老告诉我们写E2PROM数据成功,如果没有回应答位,说明写入不成功。
- 发送结束信号(STOP)停止总线
向24C02写入一个数据
//在AT24CXX指定地址写入一个数据
//WriteAddr :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址和存储地址+写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送存储地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送数据
IIC_Wait_Ack();
IIC_Stop(); //停止
delay_ms(10);
}
向24C02连续写入数据
//在AT24CXX指定地址写入指定个数数据
//ReadAddr :开始写入的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToRead:要写入数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
向24C02读数据
- 主机先发送一个开始信号(START)启动总线
- 接着跟上首字节,发送器件写操作地址(DEVICE ADDRESS)+写数据(0xA0) (注意:这里写操作是为了要把所要读的数据的存储地址先写进去,告诉E2PROM要读取哪个地址的数据)。
- 发送要读取内存的地址(WORD ADDRESS),通知E2PROM读取要哪个地址的信息。
- 重新发送开始信号(START)
- 发送设备读操作地址(DEVICE ADDRESS)对E2PROM进行读操作 (0xA1)
- E2PROM会自动向主机发送数据,主机读取从器件发回的数据,在读一个字节后,MCU会回应一个应答信号(ACK)后,E2PROM会继续传输下一个地址的数据,MCU不断回应应答信号可以不断读取内存的数据
- 如果不想读了,告诉E2PROM不想要数据了,就发送一个“非应答位NAK(1)”。发送结束信号(STOP)停止总线
向24C02读取一个数据
//在AT24CXX指定地址读取一个数据
//ReadAddr:开始读数的地址
//返回值 :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址和存储地址+写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送存储地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //发送器件地址+读命令
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop(); //停止
return temp;
}
向24C02连续读取数据
//在AT24CXX指定地址读取指定个数数据
//ReadAddr :开始读取的地址 对24c02为0~255
//pBuffer :数据数组首地址
//NumToRead:要读取数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
今天的内容就到这里,咱们下期SPI通信再见!