【STM32F4系列】【HAL库】【自制库】模拟IIC从机

21 篇文章 10 订阅
20 篇文章 30 订阅

介绍

本项目是利用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的上升沿和下降沿中断

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Lo8mb4v-1672321810773)(图片/2.png)]

程序

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)
  • 4
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
hal_i2c_mem_read函数是一个用于读取I2C设备中指定地址的数据的函数。该函数的详细解释如下: 函数原型:esp_err_t hal_i2c_mem_read(i2c_port_t i2c_num, uint8_t dev_addr, uint16_t mem_addr, uint8_t *data, size_t size, TickType_t ticks_to_wait) 参数说明: i2c_num:I2C总线的编号,取值为I2C_NUM_或I2C_NUM_1。 dev_addr:I2C设备的地址。 mem_addr:要读取的寄存器地址。 data:读取到的数据将存储在该指针所指向的缓冲区中。 size:要读取的数据的字节数。 ticks_to_wait:等待I2C总线空闲的时间,单位为系统时钟节拍数。 返回值说明: 该函数返回一个esp_err_t类型的错误码,如果执行成功,则返回ESP_OK。 函数功能: 该函数用于读取I2C设备中指定地址的数据。在读取数据之前,需要先向I2C设备发送一个寄存器地址,然后再读取该地址中存储的数据。该函数会自动处理I2C总线的起始和停止信号,并等待I2C总线空闲后再执行读取操作。 使用示例: 以下是一个使用hal_i2c_mem_read函数读取I2C设备数据的示例代码: ```c #include "driver/i2c.h" #define I2C_NUM I2C_NUM_ #define I2C_ADDR x50 #define MEM_ADDR x00 #define DATA_SIZE 4 void i2c_master_init() { i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = 21; conf.sda_pullup_en = GPIO_PULLUP_ENABLE; conf.scl_io_num = 22; conf.scl_pullup_en = GPIO_PULLUP_ENABLE; conf.master.clk_speed = 100000; i2c_param_config(I2C_NUM, &conf); i2c_driver_install(I2C_NUM, conf.mode, , , ); } void i2c_mem_read() { uint8_t data[DATA_SIZE]; esp_err_t ret = hal_i2c_mem_read(I2C_NUM, I2C_ADDR, MEM_ADDR, data, DATA_SIZE, 100 / portTICK_RATE_MS); if (ret == ESP_OK) { printf("Read data: "); for (int i = ; i < DATA_SIZE; i++) { printf("%02x ", data[i]); } printf("\n"); } else { printf("Read data failed\n"); } } void app_main() { i2c_master_init(); i2c_mem_read(); } ``` 该示例代码中,首先调用i2c_master_init函数初始化I2C总线,然后调用i2c_mem_read函数读取I2C设备中地址为x00的4个字节数据。如果读取成功,则将读取到的数据打印出来,否则打印读取失败的提示信息。
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值