1、IIC协议基础
I2C( IIC )属于两线式串行总线,由飞利浦公司开发用于微控制器(MCU)和外围设备(从设备)进行通信的一种总线,属于一主多从即一个主设备(Master),多个从设备(Slave))的总线结构,总线上的每个设备都有一个特定的设备地址,以区分同一I2C总线上的其他设备。
IIC有2根通信线路(SDA、SCL),SCL为时钟同步线,用于主机和从机间数据同步操作;SDA为数据传输线,用于传输通信数据。
IIC通信是半双工通信,所谓半双工就是主机和从机间能进行双向数据传输,但同一时间只能有一个数据发送方和一个数据接收方,即主机发送数据时,从机只能处于接收状态,反之亦然。
因IIC是一主多从通信机制,所以在IIC通信时,必须要提前提供从机的设备地址,但主机不需要设备地址,IIC通信的发起方永远是主机。从机的设备地址是由厂家设定的。
IIC设备地址由7位组成,通信时左移一位,用最低位表示为读写位。当然,也可以理解为,IIC通信地址为8位即一个字节,高7位表示设备的实际通信地址,而最低位为读写位。
IIC设备设址:xxxx xxx(w/r)
读写位用低电平0表示写,高电平1表示读;
以AT24C02为例,其7位设备地址为 1010 000 = 0x50
读模式时,AT24C02的设备地址为 1010 0001 = 0xA1
写模式时,AT24C02的设备地址为 1010 0000 = 0xA0
如下图所示为IIC通信的一主多从示意图,每个从机设备都有唯一设备地址。
2、IIC协议时序图解析
IIC通信逻辑时序图如下所示。
①、空闲状态
空闲状态,就是没有通信时的状态(初始状态)。I2C总线的SDA和SCL两条信号同时处于高电平时,规定为总线的空闲状态。
此时各个器件的输出级场效管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
②、起始信号
SCL为高电平时,SDA从高电平下降到低电平;
起始信号结束后,SDA和SCL都处于低电平状态。
③、停止信号
SCL为高电平时,SDA由低电平上升到高电平;
停止位信号结束后,SDA和SCL都处于高电平状态。
④、应答信号
应答信号为低电平时,规定为有效应答位(ACK,简称应答位),表示接收器已经成功地接收了该字节;
应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
⑤、数据信号
发送数据和接收数据时通用。
逻辑1:当SCL为高电平时,SDA始终为高电平表示逻辑1;
逻辑0:当SCL为高电平时,SDA始终为低电平表示逻辑0。
3、IIC通信源码
在编写程序时考虑到IIC协议代码会运用于各个平台的设备上,如STM32、ESP32、GD32、嵌入式Linux等,且即使是同一家公司生产的芯片,因型号的不同,代码也并不能直接通用。因此IIC通信协议代码必须考虑到硬件移值性的问题,即一套代码,仅仅进行非常小的改动,就可以在其他设备上运行。基于此想法,IIC协议代码实现了,对除了对SDA和SCL的GPIO引脚初始化函数需要根据平台重新编写外,其它的操作代码通过修改对应的宏定义即可让IIC协议代码正常跑起来。
①、iic.c
#include "iic.h"
/**
* @brief IIC初始化
* @param None
* @retval None
*/
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStructure.Pin = SDA_Pin | SCL_Pin;
GPIO_InitStructure.Mode = OUTPUT;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(IIC_Bus, &GPIO_InitStructure);
}
/**
* @brief IIC SDA模式
* @param mode:INPUT--输入模式,OUTPUT--输出模式
* @retval None
*/
void IIC_SDA_Mode(int mode)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitStructure.Pin = SDA_Pin;
GPIO_InitStructure.Mode = mode;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(IIC_Bus, &GPIO_InitStructure);
}
/**
* @brief IIC短延时
* @param None
* @retval None
*/
void IIC_Delay(void)
{
for(int i=0; i<30; i++)
{
;
}
}
/**
* @brief IIC通信开始
SCL为高电平时,SDA从高电平下降到低电平。
起始信号结束后,SDA和SCL都处于低电平状态。
* @param None
* @retval None
*/
void IIC_Start(void)
{
SCL_HIGH;
IIC_Delay();
SDA_HIGH;
IIC_Delay();//保证IIC开始前处于空闲状态,SDA、SCL空闲状态时为高电平
IIC_Delay();
SDA_LOW;
IIC_Delay();
SCL_LOW;
IIC_Delay();
}
/**
* @brief IIC通信停止
SCL为高电平时,SDA由低电平上升到高电平。
停止位信号结束后,SDA和SCL都处于高电平状态。
* @param None
* @retval None
*/
void IIC_Stop(void)
{
SCL_LOW;
SDA_LOW;
IIC_Delay();
SCL_HIGH;
IIC_Delay();
SDA_HIGH;
IIC_Delay();
}
/**
* @brief IIC写应答
应答信号为低电平时,规定为有效应答位
应答信号为高电平时,规定为非应答位
* @param ack 应答信号 HIGH--无效应答、LOW--有效应答
* @retval None
*/
void IIC_Write_Ack(unsigned char ack)
{
if(ack)
{
SDA_HIGH; //无效应答
}else{
SDA_LOW; //有效应答
}
IIC_Delay();
SCL_LOW;//
IIC_Delay();
SCL_HIGH;
IIC_Delay();
SCL_LOW;
IIC_Delay();
}
/**
* @brief IIC等待应答
* @param None
* @retval None
*/
int IIC_Wait_Ack(void)
{
int timeout = 10;
IIC_SDA_Mode(INPUT);
IIC_Delay();
SCL_HIGH;
IIC_Delay();
while(SDA_IN == HIGH) //收到低电平有效应答时跳出循环
{
if(!timeout--)
{
IIC_SDA_Mode(OUTPUT);
IIC_Stop();
return -1; //无效应答
}
IIC_Delay();
}
IIC_SDA_Mode(OUTPUT);
SCL_LOW;
IIC_Delay();
return 0; //有效应答
}
/**
* @brief IIC写1个字节
逻辑1:当SCL为高电平时,SDA始终为高电平
逻辑0:当SCL为高电平时,SDA始终为低电平
* @param None
* @retval None
*/
void IIC_Write_Byte(unsigned char data)
{
IIC_Delay();
for(int i=0; i<8; i++)
{
SCL_LOW;
IIC_Delay();
if((data>>(7-i))&0x01) //高位先发
SDA_HIGH;
else
SDA_LOW;
IIC_Delay();
SCL_HIGH;
IIC_Delay();
}
SCL_LOW;
IIC_Delay();
}
/**
* @brief IIC读1个字节
* @param None
* @retval None
*/
unsigned char IIC_Read_Byte(void)
{
unsigned char data = 0;
IIC_SDA_Mode(INPUT);
SCL_LOW;
for(int i=0; i<8; i++)
{
IIC_Delay();
SCL_HIGH;
IIC_Delay();
if(SDA_IN == HIGH)
data |= (0x01<<(7-i));
SCL_LOW;
}
IIC_SDA_Mode(OUTPUT);
//SCL_LOW;
IIC_Delay();
return data;
}
②、iic.h
#ifndef __IIC_H
#define __IIC_H
#include "stm32g4xx_hal.h"
#include "stdio.h"
#include "i2c_hal.h"
#define IIC_Bus GPIOB
#define SCL_Pin GPIO_PIN_6
#define SDA_Pin GPIO_PIN_7
#define INPUT GPIO_MODE_INPUT
#define OUTPUT GPIO_MODE_OUTPUT_OD //因为输出模式的问题,导致这个bug卡了很久!!!
#define HIGH 1
#define LOW 0
#define SCL_HIGH HAL_GPIO_WritePin(IIC_Bus, SCL_Pin, GPIO_PIN_SET)
#define SCL_LOW HAL_GPIO_WritePin(IIC_Bus, SCL_Pin, GPIO_PIN_RESET)
#define SDA_HIGH HAL_GPIO_WritePin(IIC_Bus, SDA_Pin, GPIO_PIN_SET)
#define SDA_LOW HAL_GPIO_WritePin(IIC_Bus, SDA_Pin, GPIO_PIN_RESET)
#define SDA_IN HAL_GPIO_ReadPin(IIC_Bus, SDA_Pin) ? HIGH : LOW
void IIC_Init(void);
void IIC_SDA_Mode(int mode);
void IIC_Delay(void);
void IIC_Start(void);
void IIC_Stop(void);
void IIC_Write_Ack(unsigned char ack);
int IIC_Wait_Ack(void);
void IIC_Write_Byte(unsigned char data);
unsigned char IIC_Read_Byte(void);
#endif
上述代码已经在AT24C02存储设备上测试通过,可以正常与AT24C02通信,并读写AT24C02的数据,代码的运行效果如下图所示。