Bootstrap

IIC通信协议详解及编程实现笔记教程(1)

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的数据,代码的运行效果如下图所示。

;