STM32F103(二十二)一篇文章精通《IIC通信》

Posted 独独白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32F103(二十二)一篇文章精通《IIC通信》相关的知识,希望对你有一定的参考价值。

学习板:STM32F103ZET6

前言

在接下来的几篇博客中,总结一下STM32通信方面的几大块内容。由于板子用到24C02这个外设,故IIC的内容以这个外设的参考文档为主,一步步的用代码解释各类时序。

一个建议:代码中各函数的延时一定要自己去调试,初次写代码时延时可以稍微长一点,保证通信能够完成,然后逐渐减小延时,提高通信速度。

通过本博写的代码,完全脱离STM32寄存器的束缚,代码可以移植的51、F4、Arduino等各类单片机。

通过这种完全靠延时调试时序来完成通信的方法,以后也会应用在其他实验中,如OV摄像头、LCD显示,之后有时间会慢慢总结这部分内容。

一、IIC简介

IIC(Inter Integrated Circuit),顾名思义,是集成电路总线。它是一种串行通信总线。注意读法:“I方C”,平方的“方”,IIC有时候也写成I2C

IIC 用于连接微控制器及其外围设备,它由一条双向传输的数据线SDA和一条时钟线SCL组成。数据线SDA同一时间只能发送或接收,时钟线SCL只能输出,故是半双工通信。

SDA输出时一般采用开漏输出,即需要外接上拉电阻输出1,输出0时直接接GND。当然根据外围电路设计灵活变通,推挽输出也是可以实现的。

二、24C02时序的代码实现

1、IIC基础函数编写

这部分内容开始前,先对GPIO和一些基本函数编写。

首先GPIO的配置,定义一个IIC初始化函数,主要对PB6和PB7的初始化,设置为推挽输出即可。

代码:

void IIC_Init()
{
	// PB6  SCL
	//PB7   SDA
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); 	
}

传输过程中,由于SDA是双向传输的,所以要时刻改变PB7的模式是输出还是输入,故写俩个设置PB7的函数:

void SDA_IN(void)
{	
	// PB6  SCL
	//PB7   SDA
	GPIOB->CRL&=0x0fffffff;
	GPIOB->CRL|=0x80000000;

}

void SDA_OUT(void)
{
	GPIOB->CRL&=0x0fffffff;
	GPIOB->CRL|=0x30000000;//推挽输出
	//GPIOB->CRL|=0x70000000;//开漏输出

SCL只有输出,之前IIC_Init()函数中已经设置,不用再设置,程序运行全过程都是输出。

为了程序中方便SDA和SCL置高低电平,采用宏定义:

#define  IIC_SDA  PBout(7)
#define  IIC_SCL  PBout(6)
#define  IIC_READ_SDA  PBin(7)

2、IIC数据有效性(Data Validity)

上图说明了数据传输的有效性,得到以下信息:
①SCL一个时钟周期,传输一个数

IIC通信时每8位为一个传输周期,每个SCL时钟周期传输1位,传输8次完成一个字节传输
②传输完1位数据后,需要将SCL时钟置低电平,即在SCL时钟低电平期间才能进行每1位数据的更替。

③之所以要在SCL时钟低电平期间进行数据更新,是因为在SCL高电平时,如果SDA的传输数据发生跳变,会产生开始信号或停止信号,导致通信出错。

3、IIC开始和停止的定义( Start and Stop Definition)


上图可以得到信息:

(1)开始

传输开始时,表示SDA为输出模式(只有读时才是输入),先将SDA与SCL置高电平,然后先将SDA置低电平,注意这里SDA从1跳变到0时,图中是一条倾斜的直线,以后碰上这种倾斜直线都要去延时,再将SCL置低电平。触发一个开始信号。

思路:
SDA设置为输出——>SDA与SCL都置1——>SDA置0(+延时)——>SCL置0(+延时)

代码:

void IIC_Start(void)
{
	SDA_OUT();//设置为SDA输出
	IIC_SDA=1;
	IIC_SCL=1; //SDA与SCL都设置为高电平
	delay_us(2);//---------------这里延时,待调
	
	IIC_SDA=0;//在SCL高电平期间SDA由高到低跳变,表示开始传送
	delay_us(2);
	IIC_SCL=0; //注意这里必须置0,否则数据传输时,容易触发开始和停止信号
	delay_us(2);
}

(2)停止

先将SDA设置为输出模式,根据上图,将SDA与SCL都设置为低电平,然后SCL置1,SDA再置1 。

思路:
SDA设置为输出模式——>SDA、SCL置0——>SCL置1(+延时)——>SDA置1(+延时)

代码:

void IIC_Stop(void)
{
	SDA_OUT();//设置为SDA输出
	IIC_SCL=0; //SDA与SCL都设置为低电平
	IIC_SDA=0;
	delay_us(2);//---------------这里延时,待调
	IIC_SCL=1;
	delay_us(2);
	IIC_SDA=1; //SDA在SCL高电平期间从低电平跳变到高电平
	delay_us(2);
}

4、IIC输出确认(Output Acknowledge)(ACK)

IIC在每个SCL周期传输1位数,传输8次完成1bit数的传输,完成后会产生一个应答信号。所以SCL的第9个周期为应答信号。

注意下图所示,首先SCL与SDA都置0,然后SCL置高电平,一端时间后,SCL置0,SDA置1(注意应答信号结束时,一定要先将SCL置0再将SDA置1,即使他们是同时发生的。时刻注意只有SCL低电平时SDA才能跳变!!!

思路:

SDA与SCL都置0(+延时)——>SCL置1、SDA仍保持0(+延时)——>SCL置0、SDA置1(可+延时)

void IIC_Ack()
{
	SDA_OUT();
	IIC_SCL=0;
	IIC_SDA=0;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
	IIC_SDA=1;
	delay_us(2);
}

不产生应答信号函数就是破坏这个应答时序,首先SCL是不能破坏的,因为要识别信号必须要有SCL时钟信号,所以要破坏SDA时序,只需SDA全程高电平即可

代码:

void IIC_NAck(void)
{
	SDA_OUT();
	IIC_SCL=0;
	IIC_SDA=1;
	delay_us(2);
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
	delay_us(2);
}

还有就是应答信号等待,发送1bit的数或者接收1bit的数,需要等待发送、接收的完成,判断完成的条件可以根据应答信号来完成。所以写一个等待应答信号的函数。

等待应答的思想就是判断SDA在一定时间内是否跳变为低电平,再跳变为高电平,即下图中标注2。而SCL是通过软件置高低电平实现,只是用来传输数据。为了检测出SDA的电平跳变,要把SDA设置为输入。

还是按照上图的时序,先将SDA设置为低电平(上图红框中的标注1,注意标注2的SDA跳变比SCL跳变早一点点),然后等待SDA低电平的到来,在while循环中,如果SDA一直处于高电平,一段时间后,没有等到低电平的到来,表示没有等到应答,结束本函数,并返回1 。如果等到SDA低电平的到来,跳出while循环(标注2的位置),然后将SCL置高电平,延时后(标注3,SDA=0、SCL=1),再将SDA置1、SCL置0。

代码:

u8 IIC_Wait_Ack()//等待应答,成功返回0,失败返回1
{
	u8 temp=0;
	SDA_IN();
	IIC_SDA=1;
	delay_us(2);// 这个延时必须调!!!
	while(IIC_READ_SDA)
	{
		temp++;
		if(temp>200)//待调
		{
			IIC_Stop();
			return 1;
		}
			
	}
	IIC_SCL=1;
	delay_us(2);
	IIC_SCL=0;
	IIC_SDA=1;
	return 0;
}

上述代码的倒数第二行:IIC_SDA=1;可以不用设置,因为已经等待到SDA低电平了,表示已经得到应答,不过对照时序,加上这句比较严谨。

5、IIC发送一个字节数据的函数

根据应答信号的时序,写一个发送1bit数的函数,之后给24C02写数据、从24C02读数据,都可以调用该函数

从上面这个时序可以看到,开始后(注意之前写的开始信号函数,结束后SDA与SCL都是低电平),先将SCL置低电,利用循环来发送每一位数。从高位开始发送,如果高位是1,则SDA输出1,高位是0,SDA为0。发送完成后,整个数左移1位,此时高7位变为高8位。然后时钟SCL置1,开始发送,一段时间后,SCL置0,完成一个SCL周期的1位数发送。循环8次,完成1bit数发送。

代码:

void IIC_Send_Byte(u8 data)
{
	u8 t;
	SDA_OUT();
	IIC_SCL=0;//注意数据的每一位传输时,数据更换一定要在SCL低电平期间,否则触发开始和停止
	for(t=0;t<8;t++)
	{
		if(data&0x80)//最高位为1,每次都传输最高位
			IIC_SDA=1;
		else 
			IIC_SDA=0;//此时高位数据被发送到传输线上
		data<<=1;
		delay_us(2);
		IIC_SCL=1;//原来SCL=0,这里需要给它一个时钟脉冲
		delay_us(2);
		IIC_SCL=0;
		delay_us(2);
	}
	
}

6、IIC接收一个字节数据的函数

还是上面的那个时序图,每一个SCL高电平,接收一位数,接收1bit的数,需要8次循环。先将SCL置0,一段时间后置1,此时接收到传输线上的1位数据。接收的数据也是从高位开始接收,故每次循环数据需要左移一位。

代码:

u8 IIC_Read_Byte(u8 ack)//ack=1,表示发送应答,ack=0,表示不发送应答
{
	
	u8 t,receive=0;
	SDA_IN();
	for(t=0;t<8;t++)
	{
		IIC_SCL=0;
		delay_us(2);//这个延时期间来的信号
		IIC_SCL=1;
		receive<<=1;
		if(IIC_READ_SDA)
			receive++;  //接收到了1
		//receive<<=1;写前面,如第一个数为1,写后面后变为10,是错的
		delay_us(2);
	}
	if(!ack)
		IIC_NAck();
	else 
		IIC_Ack();
	return receive;	
}

7、24C02设备地址(Device Address)与字地址(Word Address )

根据24C02参考手册的前言部分,可知24C02为2Kbit,即存储2048个8位数据。

我们用到的设备地址:


这个地址:1010 A2 A1 A0 R/W 共八位

再看一下元件封装:

设备地址中的A2、A1、A0,与外部电路有关系;WP为写保护,低电平有效,外部电路中接GND ;VCC为3.3V供电(个人习惯,信号类器件供电都会外接一个旁路电容)。SDA、SCL接芯片任意俩个IO就行。

如外部电路中A2、A1、A0都接GND,则设备地址为:1010 000R/W 。

参考手册:

所以进行读操作时第8位(即最低位)为1,进行写操作时第8位为0 。

我的板子A2、A1、A0都接GND,故:

读操作时设备地址:10100001 即0xA1

写操作时设备地址:10100000 即0xA0

24C02中,一个储存单元储存8位的数,字地址从0开始依次递增,24C02最大储存2Kbit数,所以字地址为0~2047,表示储存的单元序号。

注意一个非常有意思的东西:这个字地址是自动归零的。如存储时,从2048字地址开始存储,实际上是从字地址0开始储存;从2049字地址开始储存,实际上是从字地址1开始储存; 同理,读的时候也一样,读字地址2048的内容,其实是在读字地址0的内容…。

8、24C02写字节(Byte Write)

根据上图的时序,可以得到给24C02写一个字节的思路:

开始信号——>写设备地址——>等待应答——>写字地址——>等待应答——>写数据——>等待应答——>停止信号

注意停止后,一定要延时一段较长的时间,让数据完全储存到24C02中
代码:

void HG24C02_WriteOneByte(u16 WordAddress ,u8 Data)
{
	IIC_Start();
	IIC_Send_Byte(0xA0);//写操作时地址为0xA0
	IIC_Wait_Ack();
	IIC_Send_Byte(WordAddress);
	IIC_Wait_Ack();
	IIC_Send_Byte(Data);
	IIC_Wait_Ack();
	IIC_Stop();
	delay_ms(5);//注意这里延时!一定要调试
}

9、24C02读字节(Random Read)

根据上图时序,可以得到24C02读一个字节的思路:

开始信号——>设备地址——>等待应答——>字地址——>等待应答——>开始信号——>设备地址——>等待应答——>写数据——>(不应答)停止信号

代码:

u8  HG24C02_ReadOneByte(u16 WordAddress)
{

	u8 temp=0;
	IIC_Start();
	IIC_Send_Byte(0xA0);//写地址
	IIC_Wait_Ack();
	IIC_Send_Byte(WordAddress);
	IIC_Wait_Ack();
	
	IIC_Start();
	IIC_Send_Byte(0xA1);//读地址
	IIC_Wait_Ack(); //一定要等待
	temp=IIC_Read_Byte(0);//不需要应答
	IIC_Stop();
	delay_ms(5);//注意这里延时!一定要调试
	return temp;
}

10、24C02写一个16位、32位数的函数

对于16位、32位数,可以先存高8位,再存中间8位,最后存低8位。也可以从低位开始存。只需注意,从高8位开始存时,读取的时候也必须从高8位开始读。

以低8位优先储存为例,每次字地址++后,数据需要右移8位,将中间8位变为低8位。下面代码中与0xff的与运算是必要的,必须将除了低8位的其它位都置0 。

代码:

void HG24C02_WriteLenByte(u16 WordAddress,u32 Data,u8 Len)
{
	u8 t;
	for(t=0;t<Len;t++)
	HG24C02_WriteOneByte(WordAddress+t,(Data>>(8*t))&0xff);
}

11、24C02读一个16位、32位数

上面写数据的函数中,低8位优先写,所以在24C02中,低字地址存储数据的低8位,高字地址储存数据的高8位,所以需要从高字地址开始8位—8位的去读取,然后左移8位继续取下一个8位。

代码:

u32 HG24C02_ReadLenByte(u16 WordAddress ,u8 Len)  
{
	u8 t;
	u32 temp=0;
	for(t=0;t<Len;t++)
	{
		temp<<=8;
		temp+=HG24C02_ReadOneByte(WordAddress+Len-t-1);
	}
	return temp;
}

12、24C02顺序读数据(Sequential Read)

传递过来一个空数组,读取一系列数据后,存放在数组里面返回,直接调用上面写的 《9、24C02读字节》的函数即可。

注意这个数组一定要是全局的,在函数外面定义,否则作用域会出错

代码:

void IIC_Sequential_Read(u16 WordAddress,u8 *arr1,u16 Len)//顺序读
{
	
	u8 t;
	for(t=0;t<Len;t++)
		arr1[t]=HG24C02_ReadOneByte(WordAddress+t);
}

13、24C02顺序写数据(Sequential Write)

传递过来一个带数据的数组,将数组中的值写入24C02指定地址区域。直接调用《8、24C02写字节》函数

void IIC_Sequential_Write(u16 WordAddress,u8 *arr2 ,u16 Len)//顺序写
{
	u8 t;
	for(t=0;t<Len;t++)
		HG24C02_WriteOneByte(WordAddress+t,arr2[t]);
}

14、24C02当前字地址读取函数(Current Address Read)

根据上述时序,读取当前地址的思路:

开始信号——>读设备地址——>等待应答——>读数据——>(不需要应答)停止信号。

代码:

u8 Current_Address_Read()
{
	u8 temp;
	IIC_Start();
	IIC_Send_Byte(0xA1);//读地址
	IIC_Wait_Ack();
	temp=IIC_Read_Byte(0);
	IIC_Stop();
	return temp;
}

三、例题

1、题

将数组buffer[]={"Hello World!"}内的数据存储在24C02中,然后读取24C02数据,并在串口中打印出来。

2、完整代码

(1)iic.h代码

#ifndef  _IIC_
#define  _IIC_
#include "stm32f10x.h"
void IIC_Init(void);
void SDA_IN(void);
void SDA_OUT(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_Ack(void);
u8 IIC_Wait_Ack(void);
void IIC_NAck(void);
void IIC_Send_Byte(u8);
u8 IIC_Read_Byte(u8);
void  HG24C02_WriteOneByte(u16,u8 );
u8  HG24C02_ReadOneByte(u16);
void HG24C02_WriteLenByte(u16,u32,u8);
u32 HG24C02_ReadLenByte(u16 ,u8);
void IIC_Sequential_Read(u16,u8 *,u16);
void IIC_Sequential_Write(u16,u8 *,u16);
u8 Current_Address_Read();
#endif

(2)iic.c代码

#include "iic.h"
#include "stm32f10x.h"
#include "delay.h"
#include "usart.h"
#define  IIC_SDA  PBout(7)
#define  IIC_SCL  PBout(6)
#define  IIC_READ_SDA  PBin(7)

void IIC_Init()  //IIC初始化
{
	// PB6  SCL
	//PB7   SDA
	GPIO_InitTypeDef GPIO_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Pin=GPIO_Pin_6|GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_InitStructure);
	GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7); 	
}

void SDA_IN(void) //设置SDA线为输入模式
{	
	// PB6  SCL
	//PB7   SDA
	GPIOB->CRL&=0x0fffffff;
	GPIOB->CRL|=0x80000000;

}
void SDA_OUT(void) //设置SDA线为输出模式
{
	GPIOB->CRL&=0x0fffffff;
	GPIOB->CRL|=0x30000000;//推挽输出
	//GPIOB->CRL|=0x70000000;//开漏输出
}


void IIC_Start(void)	//开始信号
{
	SDA_OUT();//设置为SDA输出
	IIC_SDA=1;
	IIC_SCL=1; //SDA与SCL都设置为高电平
	delay_us(2);//---------------这里延时,待调
	
	IIC_SDA=0;//在SCL高电平期间SDA由高到低跳变,表示开始传送
	delay_us(2);
	IIC_SCL=0; //注意这里必须置0,否则数据传输时,容易触发开始和停止信号
	delay_us(2);
}


void IIC_Stop(void)		//结束信号
{
	SDA_OUT();//设置为SDA输出
	IIC_SCL=0; //SDA与SCL都设置为低电平
	IIC_SDA=0;
	delay_us(2);//---------------这里延时,待调
	IIC_SCL=1;
	delay_us(2);
	IIC_SDA=1; //SDA在SCL高电平期间从低电平跳变到高电平
	delay_us精通《IIC通信》

STM32F103(二十四)一篇博客精通《485通信》

STM32F103(二十四)一篇博客精通《485通信》

STM32F103(二十四)一篇博客精通《485通信》

STM32F103(二十五)完美解决USART发送接收floatu16u32数据

STM32F103(二十)DAC(贼详细)