教你手写IIC协议(看完这篇你就会手动写啦,保姆级讲解)---- 2020.2.20

前几篇关于IIC协议理论方面的文章

1.嵌入式stm32 复习(工作用)—IIC通讯协议 原理部分 2020.2.16
添加链接描述
2.嵌入式stm32 复习(工作用)—手写IIC协议之前应该掌握知识点 2020.2.19
添加链接描述

先上完整模拟iic代码,基本上复制粘贴就能用!!!

iic.c文件

#include "iic.h"

#define SDA_IN()	{GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x80000000;}	
#define SDA_OUT()	{GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x30000000;}	

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


void IIC_Init(void)
{

	RCC->APB2ENR|=1<<3;	
	GPIOB->CRL&=0x00FFFFFF;	
	GPIOB->CRL|=0x33000000;	
	
	IIC_SCL=0;
	IIC_SDA=0;

}


void IIC_Start(void)
{
	SDA_OUT();	
	IIC_SCL=1;
	IIC_SDA=1;
	sleep_us(4);
	IIC_SDA=0;
	sleep_us(4);
	IIC_SCL=0;	
}


void IIC_SendByte(u8 dat)
{
	u8 i=0;
	
	SDA_OUT();	
	IIC_SCL=0;	
	
	for(i=0;i<8;i++){
		IIC_SDA=(dat>>7)&0x01;
		sleep_us(2);
		IIC_SCL=1;
		sleep_us(2);
		IIC_SCL=0;
		dat<<=1;
	}
}


void IIC_Stop(void)
{
	SDA_OUT();	
	IIC_SCL=0;
	IIC_SDA=0;
	sleep_us(4);
	IIC_SCL=1;
	sleep_us(1);
	IIC_SDA=1;
	sleep_us(4);
}


u8 IIC_Wait_Ask(void)
{
	u8 time=0;
	
	SDA_IN();
	IIC_SDA=1;
	sleep_us(1);
	IIC_SCL=1;
	sleep_us(1);
	while(SDA_READ){
		time++;
		if(time>=200){
			IIC_Stop();
			return 1;	
		}
	}
	IIC_SCL=0;
	
	return 0;	
}


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


u8 IIC_ReadByte(void)
{
	u8 i=0;
	u8 tmp=0;
	SDA_IN();
	for(i=0;i<8;i++){
		IIC_SCL=0;
		sleep_us(2);
		IIC_SCL=1;
		tmp|= SDA_READ<<(7-i);
	}
	IIC_Ack();
	
	return tmp;
}

iic.h文件

#ifndef __IIC_H
#define __IIC_H

#include "ext.h"

void IIC_Init(void);
void IIC_Start(void);
void IIC_SendByte(u8 dat);
u8 IIC_Wait_Ask(void);
void IIC_Stop(void);

#endif

#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 

#define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
#define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
#define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
#define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
#define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
#define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
#define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    

#define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
#define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
#define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
#define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
#define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
#define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
#define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
 

#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  

#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  
#define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  

#define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  
#define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n) 

#define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)   
#define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  

#define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  
#define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  

#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  

#define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  
#define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  

好!代码部分完事,接下来开始详细讲解每行代码的用处,以及为什么这样写!

iic.c部分
宏定义部分

#define SDA_IN()	{GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x80000000;}	
#define SDA_OUT()	{GPIOB->CRL&=0x0FFFFFFF;GPIOB->CRL|=0x30000000;}	

//由前两篇文章我们可以知道,SDA(数据线)在应答信号时是处于输入模式的,而在其他阶段则是处于输出模式。那么这两个宏定义就是为了更方便的切SDA(数据线)的模式。
//一般我们都会先将这MODE7,CNF7这两位进行清零,所以此时MODE7,CNF7这两位为00,其余位都为1;
//由下面那张图可知,SDA输入模式时,此时MODE7,CNF7,MODE6,CNF6这四位的状态是10001111,GPIOB->CRL=0x8FFFFFFF;则SDA输出模式时,此时MODE7,CNF7,MODE6,CNF6这四位的状态是00111111,GPIOB->CRL=0x3FFFFFFF。

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

//便于设置SDA线和SCL线为高电平还是低电平。

iic.c部分
初始化部分

RCC->APB2ENR|=1<<3;	

//使能APB2下的GPIOB的时钟,这个就不用解释了,具体看32参考手册即可。

GPIOB->CRL&=0x00FFFFFF;	
GPIOB->CRL|=0x33000000;	

//因为在这个代码当中使用的是SCL(时钟线)为PB6,SDA(数据线)为PB7;
所以我们根据下面这张图片可以得知:

在这里插入图片描述
初始化当中,我们将SCL,SDA都设置为推挽输出模式,且最大速度一般设置为50MHz。那么我们怎样通过上图来设置呢?
首先通过图中可知,该寄存器设置为32位寄存器,且只需设置的是MODE7,CNF7,MODE6,CNF6这四位,由图可知为00110011
那么此时我们可以借助电脑上自带的计算器这个工具,最终结果如下:
在这里插入图片描述换算成16位如图为:33FFFFFFFF。
//在上一部分的宏定义部分讲解中我们已经知道一般都会讲这几位进行清零,所以我们才会进行GPIOB->CRL&=0x00FFFFFF这一个操作。

IIC_SCL=0;
IIC_SDA=0;

//初始化状态完成后,一般先将SDA线和SCL线置为低电平。
//至此,初始化就全部完成。

扫描二维码关注公众号,回复: 9286696 查看本文章

iic.c部分
iic开始传输部分

在讲这个部分之前,我们就得搬出前几篇文章当中提到的那个IIC时序图,这个尤为重要!!!这里再贴一次。
在这里插入图片描述

SDA_OUT();	

//将SDA设置为输出模式

IIC_SCL=1;
IIC_SDA=1;

//由时序图可知,SDA和SCL都设置为高电平。

sleep_us(4);
IIC_SDA=0;

//因为在上几篇文章当中提到过:
传输起始条件:数据线(SDA)从高电平到低电平,时钟线(SCL)处于高电平
//有因为SDA(数据线)从高电平到低电平的过程当中不是瞬间的,而是经过延长一段时间后才变成低电平的,而且IIC延时时间都是比较短的,这个也在前几篇中提到过,一般都是us级别的,一般都少于5us。

sleep_us(4);
IIC_SCL=0;	

//这里我们知道当SCL(时钟线)变成低电平时,IIC此时就不能传输数据了,所以这里加了一段延时之后,将SCL变成低电平,此时相当于钳住了IIC。

iic.c部分
iic发送数据部分

SDA_OUT();	
IIC_SCL=0;	

//此时将SDA(数据线)设置为输出模式,同时记不记得上回将SCL(时钟线)设置为低电平吗?这里再次将SCL(时钟线)设置为低电平,减少IIC传输过程中的错误。

for(i=0;i<8;i++){
IIC_SDA=(dat>>7)&0x01;
sleep_us(2);
IIC_SCL=1;
sleep_us(2);
IIC_SCL=0;
dat<<=1;
}

//在上一篇文章当中我重点讲解了下关于如何将数据挂载到SDA(数据线)上,同时是怎样传输数据的
//这部分代码我觉得结合上一篇文章应该就可以看得懂了,这里不再赘述。

iic.c部分
iic停止传输数据部分

SDA_OUT();	
IIC_SCL=0;
IIC_SDA=0;
sleep_us(4);
IIC_SCL=1;
sleep_us(1);
IIC_SDA=1;
sleep_us(4);

//这里我们也是先将SDA(数据线)和SCL(时钟线)都设置为低电平,钳住IIC,在前几篇文章当中我们可以得知:
传输结束条件:数据线(SDA)从低电平到高电平,时钟线(SCL)处于高电平
//所以IIC传输过程中的电平转换状态不是瞬间的,而是经过一段时间才会完成电平转换。
//根据时序图可以得知,在SCL(时钟线)变成高电平之后,在SDA(数据线)从低电平变成高电平之前,SCL(时钟线)有一部分延时时间,尽管这段时间很短很短。但是为了IIC传输成功,所以在代码中我们也加上这段延时时间。这里我们为了让大家更加清晰理解这歌部分,所以我这里假设这部分延时时间为 “ t ”。
在这里插入图片描述
iic.c部分
iic等待应答部分

SDA_IN();

//由前几篇文章我们可以得知,IIC传输过程当中应答阶段SDA(数据线)是输入模式。

IIC_SDA=1;
sleep_us(1);

//先将SDA(数据线)拉到高电平,确保此时SDA(数据线)处于上拉输入方式。

IIC_SCL=1;
sleep_us(1);	

//由时序图可知,SDA(数据线)上在应答阶段时是在第九个时钟周期内,所以此时我们应该将SCL(数据线)变成高电平,并且延时一段时间。

while(SDA_READ){
	time++;
	if(time>=200){
		IIC_Stop();
		return 1;	
	}
}
IIC_SCL=0;
return 0;		

//这部分代码个人感觉是这个代码的重点吧!!!
//首先我们应该知道怎么样才能识别到SDA(数据线)上的低电平呢?
//这部分代码的逻辑就是:如果单片机等待这个应答信号,迟迟未等到的话,总不能一直等待下去,那样的话就会卡死在这个while循环当中,事实这样是不可以的。
//那么我们应该限制一个时间段,如果系统等待的时间超过这个时间段的话,比如代码中的200,那么我们直接停止传输IIC,并且返回传输失败标志1;
//如果系统等待的时间小于这个时间段的话,那么就代表数据传输成功。此时将SCL(时钟线)拉到低电平,并且返回传输成功标志0。

iic.c部分
iic产生应答信号部分

IIC_SCL=0;
SDA_OUT();

//与IIC协议中的发送数据方式一样,区别就在于SDA模式不同。此时产生应答信号SDA为输出模式。同理,在读取数据过程中SDA(数据线)为输入模式。
//根据时序图可知,在产生应答信号之前,时钟线是处于低电平状态。

IIC_SDA=0;
sleep_us(1);

//此时SDA(数据线)为低电平。并且延时一段时间。

IIC_SCL=1;
sleep_us(1);
IIC_SCL=0;

//由时序图可知,此时应该让SCL(时钟线)产生第九个时钟周期。所以先将SCL(时钟线)拉高,然后延时一段时间,然后再将SCL(时钟线)拉低即成功产生应答信号。

iic.c部分
iic读取数据部分

u8 tmp=0;

//设置一个读取数据的缓存区。

SDA_IN();

//设置SDA(数据线)为输入模式。

for(i=0;i<8;i++){
	IIC_SCL=0;
	sleep_us(2);
	IIC_SCL=1;
	tmp|= SDA_READ<<(7-i);
}

//这个部分我会用一张图片讲解,大家看完应该就能理解!!!
在这里插入图片描述

IIC_Ack();
return tmp;

//最终读取完8位数据后,并且成功发出应答信号后,最终返回数据缓存区。
//最终读取数据完成!!!

iic.h部分
这个部分我感觉就不用讲解了吧,大家应该都会,我这里就偷下小懒吧~~,哈哈!!!

结束语

啊!写到这里我足足写了3个小时,顿时感觉手都要废了,哈哈,但是还算有点收获,个人认为大家如果细心看完这篇文章,并且结合前几篇文章一起看(在文章的刚开始会将前几篇关于IIC协议的文章链接发出来),我相信大家会彻底掌握IIC协议了!!!如果觉得这篇文章还不错的话,记得点赞 ,支持下!!!也算不枉费我三个小时的努力啊,哈哈!!!

以后我会继续推出关于嵌入式(stm32)的协议方面的讲解,IIC只是刚开始,大家敬请期待!!!

**我先休息去了~~╭(╯^╰)╮

发布了11 篇原创文章 · 获赞 21 · 访问量 978

猜你喜欢

转载自blog.csdn.net/qq_40544107/article/details/104408496