此篇博客为学习笔记,用来总结学到的知识。
IIC总线是飞利浦公司开发的一种双线、半双工的同步串行总线。
IIC串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。
IIC总线是各种总线中使用信号线最少,并具有自动寻址、多主机时钟同步和仲裁等功能的总线。因此,使用I2C总线设计计算机系统十分方便灵活,体积也小,因而在各类实际应用中得到广泛应用。
(图片来自百度)
MCU与其他IIC总线上的设备通过SCL(IIC时钟线)与SDA(IIC数据线)相连,只有两线就可以连接多个设备这就突出了IIC串口总线结构简单的优点。作为同步通信总线,所有IIC总线上的设备都由SCL时钟线提供时钟信号。IIC总线上的所有的数据都通过SDA数据线传输,若IIC总线连接多个设备,往往在数据发送前,先要发送片选地址。
IIC通信由主机进行控制,遵循一定的时序。通常包含三种信号,开始信号、停止信号和应答信号,其中开始信号为必须,其余可省略。下面介绍三种信号,以及数据传输过程。
开始信号:当SCL为高电平期间,SDA由高到低跳变。由主机向从机发送开始信息数据开始传输。
停止信号:与开始信号相反,当SCL为高期间,SDA由低向高跳变。由主机向从机发送停止信号数据传输结束。
(对应的时序图)
开始信号
/*
函数名称:IIC_Strat()
函数功能:IIC总线发送开始信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Strat()
{
//笔记
/*
开始信号: SCL 为高电平时,
SDA 由高电平向低电平跳变,开始传送数据。
*/
SDA_OUT(); //IIC数据线SDA输出
//拉高两线
IIC_SDA = 1;
IIC_SCL = 1;
//拉低SDA线发出起始信号
delay_us(4);
IIC_SDA = 0;
delay_us(4);
IIC_SCL = 0; //钳位SCL线为数据发送做准备
}
停止信号
/*
函数名称:IIC_Stop()
函数功能:IIC总线发送停止信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Stop()
{
//笔记
/*
结束信号: SCL 为高电平时,
SDA 由低电平向高电平跳变,结束传送数据。
*/
SDA_OUT(); //IIC数据线SDA输出
//拉低两线
IIC_SCL = 0;
IIC_SDA = 0;
//拉高SCL
IIC_SCL = 1;
delay_us(4);
//拉低SDA线产生停止信号
IIC_SDA = 1;
delay_us(4);
}
注意:这里的延时是必须的,IIC通信对时序有较为严格的要求。
分为两种:ACK和NACK,ACK代表传输的数据有效,NACK则认为传输的输出数据无效或者传输失败。
下面是对应答信号的解释:
发送器每发发送一个字节,就会在第九个时钟周期(发送一字节数据需要8个时钟周期应答信号在第九个时钟周期),由接收器反馈一个应答信号。应答信号为低电平时(ACK),规定为有效应答位(ACK),表示已经成功接收到该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器没有接收成功或数据无效。
(对应的时序图)
(图片来自百度)
产生应答信号
/*
函数名称:IIC_Ack()
函数功能:IIC总线产生应答信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Ack()
{
//笔记
/*
发送器每发送一个字节(8个bit),
就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
应答信号为低电平时,规定为有效应答位(ACK,简称应答位),
表示接收器已经成功地接收了该字节;
*/
//先拉低SCL线作为第九个时钟周期的开始
IIC_SCL = 0;
SDA_OUT(); //设置SDA为输出
//拉低SDA线产生应答信号
IIC_SDA = 0;
delay_us(2);
//拉高SCL线
IIC_SCL = 1;
delay_us(2);
//拉低SCL线作为第九个时钟周期的结束
IIC_SCL = 0;
}
产生应答信号
/*
函数名称:IIC_NAck()
函数功能:IIC总线产生非应答信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_NAck()
{
//笔记
/*
发送器每发送一个字节(8个bit),
就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
应答信号为高电平时,规定为非应答位(NACK),
一般表示接收器接收该字节没有成功。
*/
//先拉低SCL线作为第九个时钟周期的开始
IIC_SCL = 0;
SDA_OUT(); //设置SDA为输出
//拉低SDA线产生非应答信号
IIC_SDA = 1;
delay_us(2);
//拉高SCL线
IIC_SCL = 1;
delay_us(2);
//拉低SCL线作为第九个时钟周期的结束
IIC_SCL = 0;
}
检测应答或非应答信号
/*
函数名称:IIC_Wait_Ack()
函数功能:等待应答或非应答信号
输入:
输出:1:非应答信号 0:应答信号
修改日期:
*/
u8 IIC_Wait_Ack()
{
/*
等待应答
等待SDA线被拉低若无则无应答有则应答
*/
//定义出计算时间的变量
u8 ucErrTime = 0;
SDA_IN(); //SDA线为输入
IIC_SDA = 1;
delay_us(1);
IIC_SCL = 1;
delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime > 250)
{
IIC_Stop();
return 1;
}
}
//拉低SCL线
IIC_SCL = 0;
return 0;
}
从时序图中看到,IIC总线传输是MSB(也就是高位在前低位在后)形式按位发送的,在发送一字节数据后,在第九个时钟周期会等待应答,若为ACK则继续传输,若为NACK则发送停止信号(也可不发送)。
同样IIC数据接收时也是MSB形式按位接收,在接收一字节数据后,主机就会发送ACK或NACK,作为数据接收的标志,决定接下来是否继续进行接收数据。
(图片来自百度)
IIC通信协议规定:在SCL高电平期间,SDA上的数据必须稳定不能发生跳变:在SCL为低电平期间,SDA上的数据才能发生跳变。
可以理解为:数据必须在SCL上升沿之前准备好,在下降沿之前保持稳定,这样数据才是有效的。在一个SCL周期只能发送一位,而且必须在下一个周期来之前就准备好数据,拉低或拉高SDA。必须按照这个时序进行编写程序。
MCU可作为从机或主机,但是遵循的时序是不变的
作为主机IIC发送一个字节并返回从机的应答情况。
/****************作为主机*****************/
/*
函数名称:IIC_Send_Byte(u8 txd)
函数功能:IIC发送一个字节并返回从机有无应答
输入:
输出:从机的应答情况 1,有 0,无
修改日期:2021.1.24
*/
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
//拉低时钟线开始数据传输
IIC_SCL = 0;
//按从高位到低位顺序传输一个字节
for(t = 0;t < 8;t++)
{
//发送最高位
IIC_SDA = (txd & 0x80) >> 7;
//移动下一位为最高位
txd <<= 1;
//保证数据的有效性
//在SCL线为高电平时要保证SDA数据稳定
//一个时钟周期
delay_us(2);
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;
delay_us(2);
}
}
作为从机IIC接收一字节数据并给出应答或应答
/****************作为从机*****************/
/*
函数名称:IIC_Read_Byte(unsigned char ack)
函数功能:从机IIC接收一字节数据并给出应答或应答
输入:要给出的应答情况 1 为ACK 0 为NACK
输出:接收的数据
修改日期:2021.1.24
*/
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i, receive = 0;
SDA_IN(); //SDA线为输入
//按从高位到低位的顺序读取一个字节
for(i = 0;i < 8;i++)
{
//拉低SCL线
IIC_SCL = 0;
delay_us(2);
//拉高SCL线
IIC_SCL=1;
//把要接收的位移动至最高位
receive <<= 1;
//判断SDA线状态
//若是高就加一
//若是低则不加
if(READ_SDA)
{
receive++;
}
delay_us(2);
}
//产生应答或非应答信号
if(ack == 1)
{
IIC_Ack();
}
if(ack == 0)
{
IIC_NAck();
}
return receive;
}
IIC总线在STM32是有硬件支持的,但是这里采用软件模拟的方式。
硬件IIC复杂且稳定性较差,所以采用软件模拟的方式。
IIC.H
/********************
文件名称:IIC.h
文件描述:作为IIC驱动的头文件
修改日期:
*********************/
#ifndef __IIC_H_
#define __IIC_H_
#include "sys.h"
//IO方向设置
#define SDA_IN() {GPIOB -> CRL &= 0X0FFFFFFF; GPIOB -> CRL |= (u32) 8 << 28;}
#define SDA_OUT() {GPIOB -> CRL &= 0X0FFFFFFF; GPIOB -> CRL |= (u32) 3 << 28;}
//IO操作
#define IIC_SCL PBout(6) //IIC时钟线
#define IIC_SDA PBout(7) //IIC数据线(写)
#define READ_SDA PBin(7) //IIC数据线(读)
//IIC所有操作函数
//IIC初始化函数
void IIC_Init(void);
//IIC起始信号
void IIC_Start(void);
//IIC停止信号
void IIC_Stop(void);
//IIC发送一个字节
void IIC_Send_Byte(u8 txd);
//IIC读取一个字节
u8 IIC_Read_Byte(unsigned char ack);
//IIC等待应答
u8 IIC_Wait_Ack(void);
//IIC应答信号
void IIC_Ack(void);
//IIC不应答信号
void IIC_NAck(void);
#endif
iic.c
/********************
文件名称:iic.c
文件描述:IIC驱动
修改日期:2021.1.24
*********************/
#include "IIC.h"
#include "delay.h"
#include "stm32f10x.h"
/*
函数名称:IIC_Init()
函数功能:IIC初始化GPIOB6为IIC时钟线GPIOB7为数据线
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Init()
{
//笔记
/*
IIC初始化
空闲状态
拉高时钟线和数据线
*/
//使能GPIOB时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//GPIO初始化参数结构体
GPIO_InitTypeDef GPIO_InitStruct;
//GPIOB6 推挽输出
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
//GPIOB7 推挽输出
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
//设置GPIOB6 GPIOB7输出高
GPIO_SetBits(GPIOB, GPIO_Pin_6 | GPIO_Pin_7);
}
/*
函数名称:IIC_Strat()
函数功能:IIC总线发送开始信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Strat()
{
//笔记
/*
开始信号: SCL 为高电平时,
SDA 由高电平向低电平跳变,开始传送数据。
*/
SDA_OUT(); //IIC数据线SDA输出
//拉高两线
IIC_SDA = 1;
IIC_SCL = 1;
//拉低SDA线发出起始信号
delay_us(4);
IIC_SDA = 0;
delay_us(4);
IIC_SCL = 0; //钳位SCL线为数据发送做准备
}
/*
函数名称:IIC_Stop()
函数功能:IIC总线发送停止信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Stop()
{
//笔记
/*
结束信号: SCL 为高电平时,
SDA 由低电平向高电平跳变,结束传送数据。
*/
SDA_OUT(); //IIC数据线SDA输出
//拉低两线
IIC_SCL = 0;
IIC_SDA = 0;
//拉高SCL
IIC_SCL = 1;
delay_us(4);
//拉低SDA线产生停止信号
IIC_SDA = 1;
delay_us(4);
}
/*
函数名称:IIC_Wait_Ack()
函数功能:等待应答或非应答信号
输入:
输出:1:非应答信号 0:应答信号
修改日期:
*/
u8 IIC_Wait_Ack()
{
/*
等待应答
等待SDA线被拉低若无则无应答有则应答
*/
//定义出计算时间的变量
u8 ucErrTime = 0;
SDA_IN(); //SDA线为输入
IIC_SDA = 1;
delay_us(1);
IIC_SCL = 1;
delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime > 250)
{
IIC_Stop();
return 1;
}
}
//拉低SCL线
IIC_SCL = 0;
return 0;
}
/**********************单片机作为从机或主机可产生应答与非应答信号****************************/
/*
函数名称:IIC_Ack()
函数功能:IIC总线产生应答信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_Ack()
{
//笔记
/*
发送器每发送一个字节(8个bit),
就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
应答信号为低电平时,规定为有效应答位(ACK,简称应答位),
表示接收器已经成功地接收了该字节;
*/
//先拉低SCL线作为第九个时钟周期的开始
IIC_SCL = 0;
SDA_OUT(); //设置SDA为输出
//拉低SDA线产生应答信号
IIC_SDA = 0;
delay_us(2);
//拉高SCL线
IIC_SCL = 1;
delay_us(2);
//拉低SCL线作为第九个时钟周期的结束
IIC_SCL = 0;
}
/*
函数名称:IIC_NAck()
函数功能:IIC总线产生非应答信号
输入:
输出:
修改日期:2021.1.24
*/
void IIC_NAck()
{
//笔记
/*
发送器每发送一个字节(8个bit),
就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。
应答信号为高电平时,规定为非应答位(NACK),
一般表示接收器接收该字节没有成功。
*/
//先拉低SCL线作为第九个时钟周期的开始
IIC_SCL = 0;
SDA_OUT(); //设置SDA为输出
//拉低SDA线产生非应答信号
IIC_SDA = 1;
delay_us(2);
//拉高SCL线
IIC_SCL = 1;
delay_us(2);
//拉低SCL线作为第九个时钟周期的结束
IIC_SCL = 0;
}
/***************************************************************/
/****************作为主机*****************/
/*
函数名称:IIC_Send_Byte(u8 txd)
函数功能:IIC发送一个字节并返回从机有无应答
输入:
输出:从机的应答情况 1,有 0,无
修改日期:2021.1.24表格7yv
*/
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
//拉低时钟线开始数据传输
IIC_SCL = 0;
//按从高位到低位顺序传输一个字节
for(t = 0;t < 8;t++)
{
//发送最高位
IIC_SDA = (txd & 0x80) >> 7;
//移动下一位为最高位
txd <<= 1;
//保证数据的有效性
//在SCL线为高电平时要保证SDA数据稳定
//一个时钟周期
delay_us(2);
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;
delay_us(2);
}
}
/****************作为从机*****************/
/*
函数名称:IIC_Read_Byte(unsigned char ack)
函数功能:从机IIC接收一字节数据并给出应答或应答
输入:要给出的应答情况 1 为ACK 0 为NACK
输出:接收的数据
修改日期:2021.1.24
*/
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i, receive = 0;
SDA_IN(); //SDA线为输入
//按从高位到低位的顺序读取一个字节
for(i = 0;i < 8;i++)
{
//拉低SCL线
IIC_SCL = 0;
delay_us(2);
//拉高SCL线
IIC_SCL=1;
//把要接收的位移动至最高位
receive <<= 1;
//判断SDA线状态
//若是高就加一
//若是低则不加
if(READ_SDA)
{
receive++;
}
delay_us(2);
}
//产生应答或非应答信号
if(ack == 1)
{
IIC_Ack();
}
if(ack == 0)
{
IIC_NAck();
}
return receive;
}
IIC总线通信协议这里就介绍完了。
如有错误还请指出。
共同进步。