? ? ? ? 由于STM32的硬件IIC有点问题,且模拟IIC能够更好的帮助我们理解IIC的通信协议,所以下面给出了以STM32F103RCT6为硬件的模拟IIC基本代码(基于HAL库)。
目录
一、IIC通信的基本介绍
?二、模拟IIC的基本代码及其分析
1、基本us延时程序
2、切换SDA口的输入和输出状态
3、主机IIC起始、终止信号的发送
?4、主机等待应答
5、主机发送是否应答信号
6、主机发送一个字节
7、主机接收一个字节
三、总结
四、附录
一、IIC通信的基本介绍
? ? ? ? IIC(Inter-Integrated Circuit)其实是IICBus简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,I2C串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上(接线图如下)。所以IIC工作模式是一种半双工通信。
?????????通信过程如下图(其中起始、终止、数据、应答等会在下面结合代码与时序图一起分析):
????????主机发送和接收数据流程如下图:
?二、模拟IIC的基本代码及其分析
1、基本us延时程序
????????由于STM32HAL库最小只提供了ms级的延时,所以需要自己创建一个us级延时的子程序以满足IIC时序要求。
void RCCdelay_us(uint32_t udelay)
{
__IO uint32_t Delay = udelay * 72 / 8; //72M主频
do
{
__NOP(); //空语句
}
while (Delay --);
}
2、切换SDA口的输入和输出状态
????????由于IIC是半双工通信,SDA数据线对于主机来说有时候需要输出数据,有时候需要接受数据,故此处创建一个子函数方便调用切换SDA口状态。
void I2C_SDA_Mode(uint8_t addr) //输入参数:1表示配置SDA为输出模式,0表示配置SDA为输入模式
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(addr) //1 表示 out
{
GPIO_InitStruct.Pin = IIC_SDA_Pin; //IIC的SDA引脚
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; //开漏输出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; //上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; //SDA口速度
HAL_GPIO_Init(LM75A_SDA_GPIO_Port, &GPIO_InitStruct); //设置SDA口
}
else //0 表示 input
{
GPIO_InitStruct.Pin = IIC_SDA_Pin; //IIC的SDA引脚
GPIO_InitStruct.Mode = GPIO_MODE_INPUT; //输入模式
GPIO_InitStruct.Pull = GPIO_PULLUP; //上拉
HAL_GPIO_Init(LM75A_SDA_GPIO_Port, &GPIO_InitStruct); //设置SDA口
}
}
3、主机IIC起始、终止信号的发送
????????起始信号:在SCL高电平期间,SDA由高电平跳变为低电平,表示主机发送起始信号。(且要求SDA的下降沿到SCL的下降沿时间大于4us)。
????????终止信号:在SCL高电平期间,SDA由低电平跳变为高电平,表示主机发送终止信号。(且要求SCL的上升沿到SDA的上升沿时间大于4us)。
????????如下图分别所示为起始信号和终止信号的要求:
void I2C_Start(void) //IIC起始信号
{
I2C_SDA_Mode(OUT); //先配置SDA为输出
RESET_SCL; //先拉低SCL为下面拉高SDA做准备(在SCL低时变化SDA信号可防止误操作)
SET_SDA; //拉高SDA
SET_SCL; //拉高SCL
RCCdelay_us(5); //延时5us(满足时序中在SCL高期间SDA高电平时间保持4.7us以上)
RESET_SDA; //拉低SDA,产生下降沿
RCCdelay_us(5); //延时5us(满足时序中在SCL高电平期间SDA低电平保持4us以上)
RESET_SCL; //拉低SCL
}
void I2C_Stop(void) //IIC终止信号
{
I2C_SDA_Mode(OUT); //先配置SDA为输出
RESET_SCL; //先拉低SCL为下面拉低SDA做准备(在SCL低时变化SDA信号可防止误操作)
RESET_SDA; //拉低SDA
SET_SCL; //拉高SCL
RCCdelay_us(5); //延时5us(满足时序中在SCL高期间SDA低电平时间保持4us以上)
SET_SDA; //拉高SDA
RCCdelay_us(5); //延时5us(满足时序中在SCL高电平期间SDA高电平保持4.7us以上)
RESET_SCL; //拉低SCL
}
?4、主机等待应答
????????由于IIC每发送一个字节都会收到一个应答信号,所以主机需要有程序来判断是否接受到了这个应答信号。
????????应答信号:低电平表示应答,高电平表示无应答。
uint8_t I2C_Wait_Ack(void) //主机等待应答
{
uint8_t Time_Ack=0; //定义变量用来标志应答超时
I2C_SDA_Mode(IN); //主机接收应答信号,此时应该设置SDA为输出模式
RESET_SCL; //拉低SCL(因为从机发送应答信号是在SCL低电平期间允许改变)
RCCdelay_us(4); //延时4us确保SDA数据完全改变
SET_SCL; //拉高SCL(因为从机发送的信号是在SCL高电平期间被采样接受)
RCCdelay_us(4); //SCL为高期间SDA数据保持不变
while(HAL_GPIO_ReadPin(LM75A_SDA_GPIO_Port, LM75A_SDA_Pin)) //当读取SDA为高电平时候进入
{
if(++Time_Ack > 250)//高电平也就是无应答,一直在这等待,直到状态变量大于250认为超时
{
I2C_Stop();return 1; //从机无应答,停止IIC,返回1
}
}
RESET_SCL;
RCCdelay_us(4); //如果跳出了while循环就是有应答信号,拉低SCL
return 0; //有应答返回0
}
5、主机发送是否应答信号
????????当主机读取从机数据时,有时候不止一个字节的数据,那么每读取一个字节就要求主机给从机发送一个应答信号。
void I2C_Send_Ack(uint8_t ack)//主机发送是否应答信号 1表示不应答 0表示应答
{
if(ack) // 1 表示不应答 NoAck
{
RESET_SCL;
RCCdelay_us(5);
SET_SDA;
RCCdelay_us(5);
SET_SCL; //SCL拉高,保持4us以上,此时SDA为高,表示不应答
RCCdelay_us(5);
RESET_SCL; //SCL拉低,表示不应答信号发送结束
RCCdelay_us(5);
RESET_SDA;
}
else //0 表示应答 Ack
{
RESET_SCL;
RCCdelay_us(5);
RESET_SDA;
RCCdelay_us(5);
SET_SCL; //SCL拉高,保持4us以上,此时SDA为低,表示应答
RCCdelay_us(5);
RESET_SCL; //SCL拉低,表示应答信号发送结束
RCCdelay_us(5);
SET_SDA;
}
}
6、主机发送一个字节
????????注意IIC是先发送高位,后发送低位。
void I2C_Write_Byte(uint8_t Data) //主机发送一个字节数据
{
I2C_SDA_Mode(OUT); //配置SDA为输出模式
RESET_SCL; //先拉低SCL,为SDA第一个数据可变化做准备
RCCdelay_us(1); //延时1us,确保完全拉低SCL
for(uint8_t i=0;i<8;i++) //循环8此讲Data数据发送出去
{
if((Data<<i) & 0x80) //Data循环次数左移,保证循环从最高位到最低位发送数据
SET_SDA;
else
RESET_SDA;
RCCdelay_us(4); //延时4us,确保SDA的数据被稳定接收
SET_SCL; //SCL拉高,准备发送数据
RCCdelay_us(4); //延时4us,确保SDA的数据被稳定接收
RESET_SCL; //SCL拉低,允许SDA数据变化
}
}
7、主机接收一个字节
????????注意接收是先接收最高位,再接收最低位。
uint8_t I2C_Read_Data(void) //主机接收一个字节
{
uint8_t Data; //定义一个变量接收数据
I2C_SDA_Mode(IN); //设置SDA为输入模式
RESET_SCL; //拉低SCL,允许SDA的数据变化
RCCdelay_us(4); //延时4us,确保SDA数据稳定
for(uint8_t i=0;i<8;i++) //循环8次接收一个字节数据
{
SET_SCL; //拉高SCL,读取SDA数据
RCCdelay_us(1); //延时1us确保SCL被完全拉高
Data = Data<<1; //将得到的数据每次左移一位,满足第一个数据最终在最高位
if(HAL_GPIO_ReadPin(LM75A_SDA_GPIO_Port, LM75A_SDA_Pin))//读SDA的电平
Data |= 0x01; //高电平则把最低为置1,低电平则不用操作(因为左移自动补0)
RESET_SCL; //拉低SCL,允许下一个SDA的数据变化
RCCdelay_us(4); //延时4us,确保SDA数据稳定
}
return Data; //返回读到的一个字节数据
}
三、总结
????????以上就是基于STM32F103RCT6的模拟IIC基础程序(基于HAL库),后面我会利用这个代码配合实际的IIC外设进行通信来更好的使用这组代码(比如与LM75A温度传感器通信)。当然上述代码有些地方可以改进,比如可以不切换SDA的输入输出模式,可以利用GPIO寄存器来读取SDA的状态,就可以省去SDA模式切换等等。
四、附录
????????上述代码是全部IIC.c文件的内容(还需自行添加iic.h的头文件),下面给出对应的IIC.h的代码。(注意,所有代码并没有SDA和SCL的GPIO口配置,因为在HAL库中会自动生成gpio.c,所以SDA和SCL的GPIO口的配置都在gpio.c中,只需要在CUBE中将其配置成GPIO_OUT即可)。
#ifndef _IIC_H_
#define _IIC_H_
#include "stdint.h"
/*表明SDA是输入还是输出*/
#define OUT 1
#define IN 0
/*表明应答还是不应答*/
#define Ack 0
#define NoAck 1
/*将IIC的SDA和SCL和你自己设定的IO口对应*/
#define PORT_SCL IIC_SCL_GPIO_Port
#define PIN_SCL IIC_SCL_Pin
#define PORT_SDA IIC_SDA_GPIO_Port
#define PIN_SDA IIC_SDA_Pin
#define SET_SCL HAL_GPIO_WritePin(PORT_SCL, PIN_SCL, GPIO_PIN_SET)
#define RESET_SCL HAL_GPIO_WritePin(PORT_SCL, PIN_SCL, GPIO_PIN_RESET)
#define SET_SDA HAL_GPIO_WritePin(PORT_SDA, PIN_SDA, GPIO_PIN_SET)
#define RESET_SDA HAL_GPIO_WritePin(PORT_SDA, PIN_SDA, GPIO_PIN_RESET)
/*函数声明*/
void I2C_Start(void);
void I2C_Stop(void);
uint8_t I2C_Wait_Ack(void);
void I2C_Send_Ack(uint8_t ack);
void I2C_Write_Byte(uint8_t Data);
uint8_t I2C_Read_Data(void);
void I2C_SDA_Mode(uint8_t addr);
#endif
|