提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
此篇文章是我在学习STM32编程的IIC通信时候的个人所得,若是大佬们看出小弟本篇内容有误,请大佬指点一下我,谢谢!
一、IIC(STM32)
IIC(Inter-Integrated Circuit),中文叫集成电路总线,是一种串行通信总线,使用一主多重架构。
基本特点
①双线制:SCL时钟同步线和SDA数据传输线。
②半双工:数据的传输是由SDA线来完成的,虽然能双向通信,但是发送和接收不能同时进行。
③同步通信:IIC的通讯协议要求严格根据SCL时钟线来完成,SDA必须在SCL的时序下按位传输数据。
④通信为主从机模式:支持一主多从模式,SCL时序线的电平一直由主机控制,SDA数据线则不固定。
⑤从属设备地址:由于支持多从机模式,所以需要依据从机的地址来准确地找到从机,从机设备的地址都是由厂家设置的。
△注意:IIC的通信永远都是由主机发起的!
设备地址
设备地址一般都是由7位(bit)数据组成,在IIC通信开始时将其左移一位然后将第8位作为读写位,0位写模式,1为读模式。
设备地址的作用
①让主机准确找到从机
②确定读写模式
③某些IIC支持在通信过程中通过从机的地址附带一些信息给从机
IIC通信协议分析
1、通信前:
SDA和SCL都设置为高电平,并且保持高电平一段时间。
void MyIIC_Init(void)//此处为模拟IIC初始化,需要将模拟SDA和SCL电平拉高
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;//输出模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;//高速
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_NOPULL;//浮空
GPIO_Init(GPIOB,&GPIO_InitStruct);
SCL = 1;//PBout(8)
SDA_W = 1; //PBout(9)
}
由于通信进行读写操作,或者读写应答操作的时候需要经常改变主机引脚的输入输出模式,所以我参考老师的代码模式写了个换引脚模式的函数,如下:
static void MyIIC_SelectMode(GPIOMode_TypeDef Mode)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = Mode;//输出模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_PP;//推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;//高速
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;//此处设置成上拉
GPIO_Init(GPIOB,&GPIO_InitStruct);
}
2、通信开始
通信的开始需要将引脚模式设置成输出模式,并且将SDA和SCL线拉高,延时5us,先将SDA_W拉低,等待5us再讲SCL拉低,完成模拟信号如下图步骤。
void MyIIC_Start(void)
{
MyIIC_SelectMode(GPIO_Mode_OUT);
SCL = 1;
SDA_W = 1;
delay_us(5);
SDA_W = 0;
delay_us(5);
SCL = 0;
}
3、写入从机设备地址
在通信开始之后就要写入从机设备的地址,发送的内容为从机的7位地址加上末尾表示为“写”的0,由于IIC通信每次都是传8位数据,所以此程序段也是作为数据的发送程序。
程序如下所示:
void MyIIC_WriteByte(u8 WByte)
{
MyIIC_SelectMode(GPIO_Mode_OUT);
int i;
for(i=0; i<8 ;i++)
{
SCL = 0;//SCL拉低之后SDA才能改变电平
delay_us(2);
if(WByte &=(0x80)>>i) SDA_W = 1;//在SCL拉低期间按照传参进来的地址改
else SDA_W = 0; //变SDA的电平,拉低一次SCL传输一位数据
delay_us(3);//此处拉低的总共5us
SCL = 1;
}
SCL = 0;
}
4、等待从机应答
在主机发送从机地址到SDA线之后,需要接收从机的应答,此时SDA的电平是由从机控制的,每次主机发送数据给从机之后都要接收从机的应答,SDA电平为“1”则是无应答,“0”则是有应答,程序如下所示:
char MyIIC_ReadACK(void)
{
MyIIC_SelectMode(GPIO_Mode_IN);
char ACK;
SCL = 0;
delay_us(5);
SCL = 1;
delay_us(2);
if(SDA_R ==1) ACK = 1;
else ACK = 0;
delay_us(3);
SCL = 0;
return ACK;
}
5、读取从机数据
主机可以写入数据给从机,那么也需要读取从机发送的数据,从机发送的数据也同样是8个位,和主机发送数据不同的是从机是在SCL高电平的时候发送数据,程序如下:
u8 MyIIC_ReadByte(void)
{
MyIIC_SelectMode(GPIO_Mode_IN);
u8 tem = 0;
int i;
for(i=0; i<8; i++)
{
SCL = 0;
delay_us(5);
SCL = 1;
delay_us(2);
if(SDA_R = 1) tem |=(0x80>>i);
delay_us(3);
}
SCL = 0;
return tem;
}
6、主机写应答
每次主机接收完从机发送的8位数据后也需要向从机发送应答,从机同样的读取主机的应答是通过判断SDA线的电平,同样“1”是无应答,“0”则是有应答,程序如下:
void MyIIC_WriteACK(char ACK)
{
MyIIC_SelectMode(GPIO_Mode_OUT);
SCL = 0
delay_us(2);
SDA_W = ACK;
delay_us(3);
SCL = 1;
delay_us(5);
SCL = 0;
}
7、停止信号
最后在要结束通信的时候需要主机发送结束信号给从机,但是在停止前主机是读状态的话,在读完之后需要发送“1”应答给从机告诉从机不读了,然后再发送停止信号。
停止程序如下:
void MyIIC_Stop(void)
{
MyIIC_SelectMode(GPIO_Mode_OUT);
SCL = 0;
SDA_W = 0;
delay_us(5);
SCL = 1;
delay_us(5);
SDA_W = 1;
}
二、AT24C02
我开发板上的E方芯片四AT24C02,容量是2Kbit,其中分为32页,每页存放的数据是8个字节。
由图可以看出设备的写模式地址为0xA0,读模式地址为0A1。
AT24C02的写操作如下:
1、单字节写入操作:
对E方进行数据操作其实就是一个完整的IIC通信过程,步骤如以下代码所示,利用主机读从机应答来判断当前操作是否成功。
int AT244Cxx_WriteByte(u8 Addr,u8 Wdata)
{
MyIIC_Start();
MyIIC_WriteByte(0xA0);
if(MyIIC_ReadACK())
{
printf("连接从机地址失败\r\n");
return -1;
}
MyIIC_WriteByte(Addr);
if(MyIIC_ReadACK())
{
printf("连接存储地址失败\r\n");
return -2;
}
MyIIC_WriteByte(Wdata);
if(MyIIC_ReadACK())
{
printf("写入数据失败\r\n");
return -3;
}
MyIIC_Stop();
}
2、多字节写入操作:
多字节连续写入操作的难点在于AT24C02每一页只能存储8个字节的数据,如果满8个字节之后没有跳到下一页则会将前面的数据覆盖成新的数据,比如存入“hello world!”不跳页的话就会变成“rld!o wo”所以连续存储跳页很重要,我自己写了一段连续存储的程序如下,有哪里需要改进的请各位大佬指点一下,谢谢!
int AT24Cxx_WriteBytes(u8 Addr,u8 *Wdata,int len)
{
u8 *p = Wdata;
char i;
while(len)
{
if(len>=8)//当len足够写完一页的时候进入
{
MyIIC_Start();
MyIIC_WriteByte(0xA0);//写模式设备地址
if(MyIIC_ReadACK())
{
printf("device address failed\r\n");
return -1;
}
MyIIC_WriteByte(Addr);//发送要存入的地址
if(MyIIC_ReadACK())
{
printf("write address failed\r\n");
return -2;
}
for(i=0; i<8; i++)
{
MyIIC_WriteByte(*p++);//发送要存入的数据
if(MyIIC_ReadACK())
{
printf("write data success\r\n");
return -3;
}
Addr++;
len--;
}
}
else//剩下不足8个字节的数据另开一页存入,在过页的时候必须重新启动IIC
{
MyIIC_Start();
MyIIC_WriteByte(0xA0);//写模式设备地址
if(MyIIC_ReadACK())
{
printf("device address failed\r\n");
return -1;
}
MyIIC_WriteByte(Addr);//发送要存入的地址
if(MyIIC_ReadACK())
{
printf("write address failed\r\n");
return -2;
}
while(len)
{
MyIIC_WriteByte(*p++);//发送要存入的数据
if(MyIIC_ReadACK())
{
printf("write data success\r\n");
return -3;
}
len--;
}
}
}
MyIIC_Stop();
return 0;
}
3、数据读操作:
读操作相对于写操作就简单很多了,因为读操作没有页的限制,只要持续地读就行了,但是有一点需要注意的地方是每读一个字节需要给从机回应是否要继续读,所以在最后的一个字节读完要给从机发送应答“1”,告诉从机不读了。如下图所示。
所以在程序段中循环读的时候,循环的次数应该是所需要的读的字节数-1,程序如下所示:
int AT24Cxx_ReadBytes(u8 Addr,u8 *buf ,int len)
{
u8 *p = tem;
MyIIC_Start();
MyIIC_WriteByte(0xA0);
if(MyIIC_ReadACK())
{
printf("device address failed\r\n");
return -4;
}
MyIIC_WriteByte(Addr);
if(MyIIC_ReadACK())
{
printf("write address failed\r\n");
return -5;
}
MyIIC_Start();
MyIIC_WriteByte(0xA1);
if(MyIIC_ReadACK())
{
printf("read device address failed\r\n");
return -6;
}
len = len-1;
while(len--)
{
*p = MyIIC_ReadByte();
p++;
MyIIC_WriteACK(0);
}
*p = MyIIC_ReadByte();
MyIIC_WriteACK(1);
printf("read data success\r\n");
MyIIC_Stop();
return 0;
}
结束语
本人程序菜鸟,很希望能得到大佬的指点,望各位大佬不吝赐教!