目录
IIC的原理介绍移步B站江科大,本文介绍如何模拟IIC时序
IIC需要有起始条件、传输一个字节、应答、结束条件四个,将这四个时序组合起来就可以得到IIC完整的通讯时序。
IIC由SCL时钟线和SDA数据线组成,是同步半双工。
可以将一整个时序拆分开来。
预定义
为了提高代码可读性,提前定义一下SDA和SCL的操作,在头文件中定义好SDA和SCL是用哪两个引脚,就可以方便移植。
#define SCL_H GPIOB->BSRR|=1<<6
#define SCL_L GPIOB->BRR|=1<<6
#define SDA_H GPIOB->BSRR|=1<<7
#define SDA_L GPIOB->BRR|=1<<7
#define Read_SDA GPIOB->IDR&1<<7
/*
如果用野火的sys.h可以这么写
#define SCL_H PBout(6)=1
#define SCL_L PBout(6)=0
#define SDA_H PBout(7)=1
#define SDA_L PBout(7)=0
#define Read_SDA PBin(7)
*/
/*
如果写库函数,可以这么写
#define SCL_H GPIO_SetBits(GPIOB, GPIO_PIN_6)
#define SCL_L GPIO_ResetBits(GPIOB, GPIO_PIN_6)
#define SDA_H GPIO_SetBits(GPIOB, GPIO_PIN_7)
#define SDA_L GPIO_ResetBits(GPIOB, GPIO_PIN_7)
#define Read_SDA GPIO_ReadInputDataBit(GPIOB, GPIO_PIN_7)
*/
定义一个IIC_Delay()函数,用于时序的延时。
void IIC_Delay()
{
delay_us(4);
}
引脚初始化
这里PB6-SCL PB7-SDA,外接了上拉电阻,所以这里使用开漏输出,开漏输出的时候直接读取ODR寄存器可以读到引脚的状态,不需要再转换引脚的输入输出状态。
void IIC_Init(void) //初始化引脚
{
RCC->APB2ENR |= 1<<3;
GPIOB->CRL &= 0x00FFFFFF;
GPIOB->CRL |= 0x77000000; //通用开漏输出,PB6-SCL PB7-SDA
GPIOB->ODR |= 3<<6; //PB6 PB7给高
}
起始条件
主机发送一个起始条件,目的是告诉从机要准备开始传输数据了。
起始条件是在SCL高电平期间拉低SDA,如下图:
那把这个时序用代码写出来就是这样:
SCL_H; //先释放总线
SDA_H; //释放总线
IIC_Delay();
SDA_L; //在SCL高电平期间拉低SDA
IIC_Delay();
SCL_L; //为了和后面时序对齐
结束条件
主机发送一个结束条件,目的是告诉从机停止交流
结束条件是在SCL高电平期间拉高SDA,也就是说SCL本身要先处于高电平,SDA处于低电平,然后再拉高SDA。如下图:
把这个时序用代码写出来就是这样:
void IIC_Stop(void) //发送结束条件
{
SDA_L; //初始让SDA处于低电平
SCL_H; //让SCL处于高电平
IIC_Delay();
SDA_H; //在SCL高期间拉高SDA
}
发送一个字节
IIC发送字节很简单,就是在SCL高电平期间(主机或从机)读取SDA,在SCL低电平期间改变SDA状态。比如说想发送1,那就先把SCL拉低,然后再让SDA拉低,再把SCL拉高,接收方在SCL拉高之后读取SDA状态就可以了。依次发送八位就是一个字节。
IIC是高位先发送,用for循环依次发送即可,代码可以是这样:
void IIC_SendByte(unsigned char byte) //发送一个字节
{
int i;
for(i=0; i<8; i++)
{
SCL_L; //拉低时钟线,允许改变SDA
if(byte&1<<(7-i)) //判断发送的那一位是0还是1
{
SDA_H;
}else
{
SDA_L;
}
SCL_H; //拉高SCL,让接收方读取
IIC_Delay(); //延时一下,让接收方有足够时间读取SDA
}
SCL_L; //发送结束,拉低SCL
}
接收应答
IIC通信上是有应答位的,目的是告诉发送方接收到了数据。应答位紧跟着数据位,一般主机在发送完一个字节之后,是要接收应答的。
读取应答位,首先需要释放SDA,让从机控制SDA,然后拉高SCL,等待,读取SDA,若从机有应答,SDA会被拉低,若从机无应答SDA将不会被拉低
代码可以是这样:
unsigned char IIC_ReadBck(void) //接收应答
{
u8 ack;
SDA_H; //释放SDA
SCL_H; //拉高SCL
IIC_Delay(); //等待
ack = Read_SDA; //读取
SCL_L; //这里拉低回来是为了对齐时序
SDA_L;
return ack;
}
发送应答
同样的,从机每发送一个字节过来,主机也是要给应答的,除非不想再继续传输数据。
从机可以连续发送多个字节,这需要主机在接收到每个字节之后给应答,给应答的时序和接收应答差不多,区别只是这个应答是主机发从机读
代码可以是这样:
void IIC_SendBck(void) //发送应答
{
u8 ack;
SCL_L;
SDA_L; //拉低SDA,表示应答
SCL_H; //拉高SCL
IIC_Delay(); //等待从机读取
SCL_L; //这里拉低回来是为了对齐时序
}
接收一个字节
前面有发送字节,就会有接收字节,接收字节时序和发送字节时序一样,只不过是主机读,从机发,主机要释放SDA,将SDA交给从机控制。
代码可以是这样:
unsigned char IIC_ReadByte() //从总线上读一个字节
{
int i;
unsigned char byte=0;
SDA_H; //释放SDA
for(i=0; i<8; i++)
{
SCL_H; //拉高,然后读
IIC_Delay();
if(Read_SDA)
{
byte |= (1<<(7-i));
}
SCL_L;
IIC_Delay();
}
return byte;
}
那么IIC总共就有这么些个函数:
void IIC_Delay(void);
void IIC_Init(void); //初始化引脚
void IIC_Start(void); //发送起始条件
void IIC_Stop(void); //发送结束条件
unsigned char IIC_ReadBck(void); //接收应答
void IIC_SendBck(void); //发送应答
void IIC_SendByte(unsigned char byte); //发送一个字节
unsigned char IIC_ReadByte(void); //读一个字节