介绍

本项目是利用GPIO模拟I2C的从机

网上常见的是模拟I2C主机

本项目是作为一个两个单片机之间低速通信的用法

从机

功能

实现I2C从机端读写寄存器

编程思路

I2C的从机实现比起主机来麻烦一些

因为SCL的时序是由主机发送,从机需要响应

注意:整个过程不考虑应答码

思路是检测SCLSDA的边沿(上升沿和下降沿)中断

SDA的边沿检测SCL的电平,如果SCL为高电平,则根据协议开始(SDA上升沿),或结束(SDA下降沿)I2C通信,在SCL低电平则无需动作

SCL上升沿是检测来自SDA的数据(来自主机),SCL下降沿通过SDA发送数据(发给主机)

整体使用状态机的思想:

  1. 在SDA上升沿,SCL高电平时进入空闲态(0),之后转入准备态(1)
  2. 在SCL下降沿时,清空中间变量的数据,转入器件地址解码(2)
  3. 在之后的8个上升沿是数据,第9个上升沿是应答吗,这里用于状态切换,如果是写入器件地址(最低位为0)则转移为寄存器读取态(3),如果是读取器件地址(最低位为1)则转换为数据发送态(5)
  4. 如果寄存器读取态(3)则同样在SCL上升沿动作,读取8个上升沿的数据,当作寄存器地址,并转移为数据读取态(4)
  5. 数据读取态(4),还是在SCL上升沿动作,读取数据放入缓冲区即可
  6. 这边说一下器件地址是读取器件地址时寄存器地址的获取手段,因为发送寄存器地址是按照写入器件地址发送的,之后需要再发送一次读取器件地址,这时又触发了一次起始信号,因此可以即在第一次发送写入器件地址并转移到寄存器读取态(3)读取寄存器地址后被打断了,重新进入了 准备态(1)->器件地址解码(2)->数据发送态(5)
  7. 数据发送态(5),此态是在SCL下降沿动作的(在SCL低电平时改变SDA),按顺序依次发送数据即可
  8. 结束后进入空闲态,等待下次触发

HAL设置

需要设置两个GPIO的上升沿和下降沿中断

如下图,设置为边沿中断,上拉

stm32cubeMX GPIO模拟IIC_嵌入式硬件

程序

GPIO基本输出函数

#define I2C_Address 0x54
#define I2C_SCL_GPIOx GPIOA
#define I2C_SCL_Pin GPIO_PIN_0
#define I2C_SDA_GPIOx GPIOA
#define I2C_SDA_Pin GPIO_PIN_1
/**
 * @brief 一段延迟
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-07-27 08:53:30
 */
void I2C_Delay(void)
{
	int z = 0xff;
	while (z--)
		;
}
/**
 * @brief 写SDA
 * @param H_L:高低电平
 * @return 无
 * @author HZ12138
 * @date 2022-10-21 18:07:18
 */
void I2C_Write_SDA(GPIO_PinState H_L)
{
	HAL_GPIO_WritePin(I2C_SDA_GPIOx, I2C_SDA_Pin, H_L);
}
/**
 * @brief 写SCL
 * @param H_L:高低电平
 * @return 无
 * @author HZ12138
 * @date 2022-10-21 18:07:40
 */
void I2C_Write_SCL(GPIO_PinState H_L)
{
	HAL_GPIO_WritePin(I2C_SCL_GPIOx, I2C_SCL_Pin, H_L);
}
/**
 * @brief 读取SDA
 * @param 无
 * @return SDA的状态
 * @author HZ12138
 * @date 2022-10-21 18:07:56
 */
uint16_t I2C_Read_SDA(void)
{
	return HAL_GPIO_ReadPin(I2C_SDA_GPIOx, I2C_SDA_Pin);
}
/**
 * @brief 读取SCL
 * @param 无
 * @return SDA的状态
 * @author HZ12138
 * @date 2022-10-21 18:07:56
 */
uint16_t I2C_Read_SCL(void)
{
	return HAL_GPIO_ReadPin(I2C_SCL_GPIOx, I2C_SCL_Pin);
}

切换GPIO模式

首先我们分析一下GPIO的模式

SCL一直保持边沿中断即可

SDA需要在空闲主机写(从机接收数据)状态保持边沿中断,而在主机读(从机发送数据)在边沿中断开漏上拉输出状态切换

为了让通信速率不算太低,此处切换需要较高速率,不建议使用HAL函数

这边选择直接操作寄存器来实现

#define I2C_SDA_Pinx 1 // GPIO_PIN_x 写x
/**
 * @brief 设置SDA为中断模式
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 19:56:29
 */
void I2C_Slave_Set_SDA_IT(void)
{
	I2C_SDA_GPIOx->MODER &= ~(3 << (I2C_SDA_Pinx * 2));
	I2C_SDA_GPIOx->MODER |= 0 << I2C_SDA_Pinx * 2;
}
/**
 * @brief 设置SDA为开漏上拉输出
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 19:56:54
 */
void I2C_Slave_Set_SDA_Out(void)
{
	I2C_SDA_GPIOx->MODER &= ~(3 << (I2C_SDA_Pinx * 2));
	I2C_SDA_GPIOx->MODER |= 1 << I2C_SDA_Pinx * 2;
}

SDA和SCL边沿服务函数

本函数主要用于区分上升和下降沿

/**
 * @brief 在SCL中断服务函数中调用
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 21:28:32
 */
void I2C_Slave_IRQ_SCL(void)
{

	if (I2C_Read_SCL() == GPIO_PIN_SET)
	{ // 上升沿
		I2C_Slave_IRQ_SCL_Rising();
	}
	else
	{ // 下降沿
		I2C_Slave_IRQ_SCL_Falling();
	}
}
/**
 * @brief 在SDA中断服务函数中调用
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 21:28:32
 */
void I2C_Slave_IRQ_SDA(void)
{
	if (I2C_Slave_SDA_IRQ_EN == 1)
	{
		if (I2C_Read_SCL() == GPIO_PIN_SET)
		{
			if (I2C_Read_SDA() == GPIO_PIN_SET)
			{					   // SDA上升沿
								   // 完成态
				I2C_Slave_Ins = 0; // 到空闲态
				I2C_Slave_Set_SDA_IT();
			}
			else
			{ // SDA下降沿
				I2C_Slave_Set_SDA_IT();
				if (I2C_Slave_Ins == 0) // 空闲态
					I2C_Slave_Ins = 1;	// 到准备态
				else
				{
					I2C_Slave_Ins = 1;
				}
			}
		}
	}
}

SCL上升沿服务函数

本函数是核心之一

根据编程思路一节的内容来编写上升沿服务函数

/**
 * @brief SCL上升沿服务函数
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 21:29:43
 */
void I2C_Slave_IRQ_SCL_Rising(void)
{ // SCL上升沿
	switch (I2C_Slave_Ins)
	{
	case 2: // 器件地址解码

		I2C_Slave_zj <<= 1;
		I2C_Slave_zj |= I2C_Read_SDA();
		I2C_Slave_num++;
		if (I2C_Slave_num == 8) // 数据码
		{
			I2C_Slave_Add = I2C_Slave_zj;
			I2C_Slave_zj = 0;
		}
		else if (I2C_Slave_num == 9) // 应答码
		{
			I2C_Slave_num = 0;
			if (I2C_Slave_Add == (I2C_Address & 0xfe))
				I2C_Slave_Ins = 3; // 到寄存器地址读取态
			else if (I2C_Slave_Add == (I2C_Address | 0x01))
			{
				I2C_Write_SDA(GPIO_PIN_SET);
				I2C_Slave_Set_SDA_Out();
				I2C_Slave_Ins = 5;
				I2C_Slave_zj = 0xaa;
				I2C_Slave_num = 0;
				I2C_Slave_SDA_IRQ_EN = 0;
			}
		}

		break;
	case 3: // 寄存器地址读取

		I2C_Slave_zj <<= 1;
		I2C_Slave_zj |= I2C_Read_SDA();
		I2C_Slave_num++;
		if (I2C_Slave_num == 8) // 数据码
		{
			Reg_Add = I2C_Slave_zj;
		}
		else if (I2C_Slave_num == 9) // 应答码
		{
			I2C_Slave_Ins = 4; // 数据读取
			I2C_Slave_zj = 0;
			I2C_Slave_num = 0;
		}
		break;
	case 4: // 数据读取(主机写)
		I2C_Slave_zj <<= 1;
		I2C_Slave_zj |= I2C_Read_SDA();
		I2C_Slave_num++;
		if (I2C_Slave_num == 9) // 应答码
		{
			I2C_Slave_zj = 0;
			I2C_Slave_num = 0;
		}
		break;

	default:
		break;
	}
}

SCL上升沿服务函数

本函数是核心之一

根据编程思路一节的内容来编写下降沿服务函数

/**
 * @brief SCL下降沿服务函数
 * @param 无
 * @return 无
 * @author HZ12138
 * @date 2022-12-29 21:29:43
 */
void I2C_Slave_IRQ_SCL_Falling(void)
{ // SCL下降沿
	switch (I2C_Slave_Ins)
	{
	case 1: // 准备态
		I2C_Slave_zj = 0;
		I2C_Slave_num = 0;
		I2C_Slave_Ins = 2; // 到器件地址解码
		break;
	case 5: // 数据发送(主机读)

		if (I2C_Slave_zj & 0x80)
			I2C_Write_SDA(GPIO_PIN_SET);
		else
			I2C_Write_SDA(GPIO_PIN_RESET);
		I2C_Slave_zj <<= 1;
		I2C_Slave_num++;
		if (I2C_Slave_num == 9) // 应答码
		{
			I2C_Slave_num = 0;
			I2C_Write_SDA(GPIO_PIN_SET);
			I2C_Slave_Set_SDA_IT();
			I2C_Slave_SDA_IRQ_EN = 1;
			I2C_Slave_Ins = 0;
		}
		break;
	default:
		break;
	}
}

成品

I2C_Write_SDA(GPIO_PIN_SET);
	else
		I2C_Write_SDA(GPIO_PIN_RESET);
	I2C_Slave_zj <<= 1;
	I2C_Slave_num++;
	if (I2C_Slave_num == 9) // 应答码
	{
		I2C_Slave_num = 0;
		I2C_Write_SDA(GPIO_PIN_SET);
		I2C_Slave_Set_SDA_IT();
		I2C_Slave_SDA_IRQ_EN = 1;
		I2C_Slave_Ins = 0;
	}
	break;
default:
	break;
}

}

# 成品

[GitHub](https://github.com/HZ1213825/HAL_STM32F4_IIC)