最近测试STM32与铁电读取数据,记录如下:
1. 说明:
1)软件:STM32CubeMX + Keil v5;
2)  硬件:STM32F103VCT6 + MD85RC16V
 

2. 采用I2C通讯连接,硬件如下:

3. 铁电存储器说明:

设备地址组成:

各部分详解

  1. 设备类型码

    • 固定为 1010,用于标识这是一颗富士通的FRAM存储器。

  2. 存储器高地址码

    • 这3位 (A2, A1, A0不是 通过芯片外部引脚设置的从机地址选择位。

    • 它们实际上是 11位存储器地址的最高3位

    • 因此,这个芯片的I²C总线地址从设备地址字的角度看是固定的,但实际访问的存储单元由这3位和后续发送的8位低地址共同决定。

    • 也就是说,这款芯片的设备地址不可编辑,采用HAL库时,直接输入固定的0xA0写入设备地址。

    • 最终的设备地址是 固定的4位类型码 1010 + 3位存储器地址高三位 (A2, A1, A0) + 1位0 组成(读写HAL库函数自己处理)。

如果需要安装多颗铁电存储芯片,就需要采取不同的I2C端口,或者换其他型号。

4. STM32CubeMX配置
直接选取对应I2C,我这里连接的是I2C2,

配置完直接创建代码即可。

5. 读写函数
eeprom.h

#ifndef __EEPROM_h__
#define __EEPROM_h__

#include "i2c.h"
#include "stdint.h"

#define FALSE 0X00
#define TRUE 0X01

// 读取多个字节从FRAM
extern unsigned char FRAM_ReadStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length);
// 写入多个字节到FRAM
extern unsigned char FRAM_WriteStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length);

#endif

eeprom.c

#include "eeprom.h"

#define MB85RC16

/**
 * @brief  读取FRAM数据
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据接收缓冲区
 * @param  read_length: 读取数据长度
 * @retval 0 - 读取失败, 1 - 读取成功
 */
unsigned char FRAM_ReadStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length)
{
	HAL_StatusTypeDef status;
	uint8_t mem_addr_high = (memory_address >> 8) & 0x07;
    unsigned char dev_addr = (unsigned char)(device_address | (mem_addr_high << 1));
	status = HAL_I2C_Mem_Read(&hi2c2, dev_addr, memory_address & 0xFF, I2C_MEMADD_SIZE_8BIT, buffer_point, read_length, 100);
	return (status == HAL_OK) ? TRUE : FALSE;
}

/**
 * @brief  写入FRAM数据
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据发送缓冲区
 * @param  write_length: 写入数据长度
 * @retval 0 - 写入失败, 1 - 写入成功
 */
unsigned char FRAM_WriteStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length)
{
	HAL_StatusTypeDef status;
	uint8_t mem_addr_high = (memory_address >> 8) & 0x07;
    unsigned char dev_addr = (unsigned char)(device_address | (mem_addr_high << 1));
	status = HAL_I2C_Mem_Write(&hi2c2, dev_addr, memory_address & 0xFF, I2C_MEMADD_SIZE_8BIT, buffer_point, write_length, 100);
	return (status == HAL_OK) ? TRUE : FALSE;
}

/**
 * @brief  写入多个字节到FRAM
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据发送缓冲区
 * @param  write_length: 写入数据长度
 * @param  verify_type: 校验类型
 * @retval 0 - 写入失败, 1 - 写入成功
 */
unsigned char EE_WriteNByte(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length, unsigned char verify_type)
{
	return FRAM_WriteStr(device_address, memory_address, buffer_point, write_length);
}

/**
 * @brief  读取多个字节从FRAM
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据接收缓冲区
 * @param  read_length: 读取数据长度
 * @retval 0 - 读取失败, 1 - 读取成功
 */
unsigned char EE_ReadNByte(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length)
{
	return FRAM_ReadStr(device_address, memory_address, buffer_point, read_length);
}

调用参考:

其中,0xA0是铁电需要传入的固定地址,实际传入到HAL_I2C_Mem_Read()的地址计算如下:

 uint8_t mem_addr_high = (memory_address >> 8) & 0x07;
 unsigned char dev_addr = (unsigned char)(0xA0 | (mem_addr_high << 1));

其中,memory_adress是传入的需要读取或写入的内存地址,实际地址需要1010 + 内存地址的高8位(实际就三位,因为最大存储是2048个字节(2^11=2048,11=8+3))。

最后实际测试读写均正常。
为什么不用模拟I2C?

  1. 效率:硬件I2C由专门的硬件电路实现,通常使用DMA或中断,不占用CPU太多时间。而模拟I2C需要CPU不断翻转GPIO,占用大量CPU资源。

  2. 稳定性:硬件I2C的时序由硬件保证,非常精确,不受其他中断或任务影响。模拟I2C的时序由软件延时实现,容易受中断和其他任务干扰。

  3. 复杂性:使用HAL库可以简化编程,只需调用库函数即可完成I2C通信。而模拟I2C需要自己实现起始条件、停止条件、应答、数据读写等所有时序。

  4. 功能完整性:硬件I2C通常支持I2C协议的所有特性,如时钟延展、多主机仲裁等。模拟I2C实现这些功能较为复杂。

  5. 可维护性:使用标准的HAL库函数,代码更易于理解和维护。模拟I2C的代码需要自己维护,且可能因不同处理器平台或引脚而异。

  6. 性能:硬件I2C通常可以支持更高的时钟频率(如1MHz),而模拟I2C由于软件延时限制,通常速度较低。

如果,你的硬件引脚被占用,或者你就想纯手搓I2C,可以参考以下模拟I2C。模拟I2C不需要在STM32CubeMX中配置I2C,硬件上只需要连接任意GPIO口即可。

eeprom.h

#ifndef __EEPROM_h__
#define __EEPROM_h__
#include "main.h"
// #include "adc.h"
// #include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

#define FALSE 0X00
#define TRUE 0X01

#define SDA_OUT                                            \
	{                                                      \
		GPIO_InitTypeDef GPIO_InitStruct = {0};            \
		GPIO_InitStruct.Pin = EE_SDA_Pin;                  \
		GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;        \
		GPIO_InitStruct.Pull = GPIO_PULLUP;                \
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;       \
		HAL_GPIO_Init(EE_SDA_GPIO_Port, &GPIO_InitStruct); \
		SDA_H;                                             \
	}

#define SDA_IN                                             \
	{                                                      \
		GPIO_InitTypeDef GPIO_InitStruct = {0};            \
		GPIO_InitStruct.Pin = EE_SDA_Pin;                  \
		GPIO_InitStruct.Mode = GPIO_MODE_INPUT;            \
		GPIO_InitStruct.Pull = GPIO_NOPULL;                \
		HAL_GPIO_Init(EE_SDA_GPIO_Port, &GPIO_InitStruct); \
	}

#define SCL_OUT                                            \
	{                                                      \
		GPIO_InitTypeDef GPIO_InitStruct = {0};            \
		GPIO_InitStruct.Pin = EE_SCL_Pin;                  \
		GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;        \
		GPIO_InitStruct.Pull = GPIO_PULLUP;                \
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;      \
		HAL_GPIO_Init(EE_SCL_GPIO_Port, &GPIO_InitStruct); \
		SCL_H;                                             \
	}

#define SDA_L                                                            \
	{                                                                    \
		HAL_GPIO_WritePin(EE_SDA_GPIO_Port, EE_SDA_Pin, GPIO_PIN_RESET); \
	}
#define SDA_H                                                          \
	{                                                                  \
		HAL_GPIO_WritePin(EE_SDA_GPIO_Port, EE_SDA_Pin, GPIO_PIN_SET); \
	}

#define SCL_L                                                            \
	{                                                                    \
		HAL_GPIO_WritePin(EE_SCL_GPIO_Port, EE_SCL_Pin, GPIO_PIN_RESET); \
	}
#define SCL_H                                                          \
	{                                                                  \
		HAL_GPIO_WritePin(EE_SCL_GPIO_Port, EE_SCL_Pin, GPIO_PIN_SET); \
	}

#define READ_SDA_EE HAL_GPIO_ReadPin(EE_SDA_GPIO_Port, EE_SDA_Pin)

extern unsigned char EE_WriteNByte(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length, unsigned char verify_type);

// 读取多个字节从FRAM
extern unsigned char FRAM_ReadStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length);
// 写入多个字节到FRAM
extern unsigned char FRAM_WriteStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length);

// IIC初始化
extern void I2C_Init(void);
extern void Start_I2c(void);
extern void Stop_I2c(void);
#endif

eeprom.c

#include "eeprom.h"

#define MB85RC16

void delay_ms(unsigned int t)
{
	unsigned int i = 0;
	unsigned int j = 0;
	for (i = t; i > 0; i--)
	{
		j = 100;
		while (j--)
			;
	}
}

void delay_us(unsigned int t)
{
	unsigned int i = 0;
	unsigned int j = 0;
	for (i = t; i > 0; i--)
	{
		j = 10;
		while (j--)
			;
	}
}

unsigned char ack; /*应答标志位*/

/**
 * @brief  产生I2C总线启动信号
 * @note   SCL高电平时,SDA由高变低
 */
void Start_I2c()
{
	SDA_OUT;
	SDA_H; // SDA线拉高
	delay_us(1);
	SCL_H;
	delay_us(5); // 起始信号持续时间约为4.7us,建议延时
	SDA_L;		 // 产生起始信号
	delay_us(5); // 起始信号持续时间约为4us
	SCL_L;		 // 释放I2C总线,准备接收从机应答
	delay_us(2);
}

 /**
 * @brief  产生I2C总线停止信号
 * @note   SCL高电平时,SDA由低变高
 */
void Stop_I2c()
{
	SDA_OUT;
	SCL_L;
	delay_us(2);
	SDA_L;		 // 产生停止信号
	delay_us(3); // 停止信号持续时间约为4us
	SCL_H;		 // 释放总线时钟信号约为4us
	delay_us(5);
	SDA_H; // 释放I2C总线
	delay_us(4);
}

 /**
 * @brief  产生I2C_Ack
 * @note   该函数用于产生I2C应答信号
 * @param  None
 * @retval None
 */
void I2C_Ack(void) // IIC应答信号
{
	SDA_OUT;
	SDA_L;
	delay_us(3);
	SCL_H;
	delay_us(5);
	SCL_L;
	delay_us(2);
	SDA_H;
}

 /**
 * @brief  产生I2C_NoAck
 * @note   该函数用于产生I2C非应答信号
 * @param  None
 * @retval None
 */
void I2C_NoAck(void) // IIC非应答信号
{
	SDA_OUT;
	SDA_H;
	delay_us(3);
	SCL_H;
	delay_us(5);
	SCL_L;
	delay_us(2);
}

/**
 * @brief  发送一个字节
 * @param  c: 要发送的字节
 * @retval None
 */
void SendByte(unsigned char c)
{
	unsigned char BitCnt;

	SDA_OUT;
	SCL_L;
	for (BitCnt = 0; BitCnt < 8; BitCnt++) // 发送的字节长度为8位
	{
		if (c & 0x80)
		{
			SDA_H; // 判断高位
		}
		else
		{
			SDA_L;
		}
		delay_us(6);
		SCL_H;		 // 产生时钟信号,通知从机准备接收数据
		delay_us(6); // 确保时钟线稳定在高电平
		SCL_L;
		delay_us(6);
		c = c << 1;
	}

	SDA_H;
	SDA_IN;
	// 8位数据接收准备,等待应答位
	delay_us(1);
	SCL_H;
	delay_us(5);
	if (READ_SDA_EE == 1)
	{
		ack = 0; // 0应答位1非应答位
	}
	else
	{
		ack = 1; // 判断是否收到应答信号
	}
	SCL_L;
	delay_us(2);
}

/**
 * @brief  接收一个字节
 * @param  None
 * @retval 接收到的字节
 */
unsigned char RcvByte()
{
	unsigned char retc, j;
	unsigned char BitCnt;

	retc = 0; // 接收数据
	SDA_IN;	  // SDA线设置为输入模式
	SDA_H;

	for (BitCnt = 0; BitCnt < 8; BitCnt++)
	{
		SCL_H; // 产生时钟信号,确保数据有效
		delay_us(5);
		retc = retc << 1;
		SDA_IN;
		j = READ_SDA_EE;
		if (j != 0)
		{
			retc = retc | 0x01; // 判断最低位, 如果接收到高电平则retc |= 0x01
		}
		delay_us(2);
		SCL_L;
		delay_us(2);
	}
	return (retc);
}

/**
 * @brief  产生I2C应答信号
 * @param  a: 0 - 发送应答信号, 1 - 发送非应答信号
 * @retval None
 */
void Ack_I2c(unsigned char a)
{
	SDA_OUT;
	if (a == 0)
	{
		SDA_L; // 产生应答信号
	}
	else
	{
		SDA_H;
	}
	delay_us(4);
	SCL_H;
	delay_us(7); // 确保时钟线稳定在高电平
	SCL_L;		 // 释放时钟信号,准备接收I2C从机的应答
	delay_us(3);
}

/**
 * @brief  读取FRAM数据
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据接收缓冲区
 * @param  read_length: 读取数据长度
 * @retval 0 - 读取失败, 1 - 读取成功
 */
unsigned char FRAM_ReadStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length)
{
	unsigned int i;
	I2C_Init();
	Start_I2c();																// 产生起始信号
	SendByte((unsigned char)(device_address | ((memory_address >> 7) & 0x0E))); // 发送设备地址

	if (ack == 0)
	{
		Stop_I2c();
		return (0);
	}
	SendByte(memory_address & 0xFF); // 发送内存地址低8位
	if (ack == 0)
	{
		Stop_I2c();
		return (0);
	}
	delay_us(10);
	Start_I2c();																	   // 产生起始信号
	SendByte((unsigned char)(device_address | ((memory_address >> 7) & 0x0E) | 0x01)); // 发送设备地址
	if (ack == 0)
	{
		Stop_I2c();
		return (0);
	}
	for (i = 0; i < read_length - 1; i++)
	{
		*buffer_point = RcvByte(); // 接收数据
		Ack_I2c(0);				   // 发送应答信号
		buffer_point++;
	}
	*buffer_point = RcvByte(); // 接收数据
	Ack_I2c(1);				   // 发送非应答信号
	delay_us(5);
	Stop_I2c(); // 产生停止信号
	return (1);
}

/**
 * @brief  写入FRAM数据
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据发送缓冲区
 * @param  write_length: 写入数据长度
 * @retval 0 - 写入失败, 1 - 写入成功
 */
unsigned char FRAM_WriteStr(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length)
{
	unsigned int i;
	I2C_Init();
	Start_I2c();																// 产生起始信号
	SendByte((unsigned char)(device_address | ((memory_address >> 7) & 0x0E))); // 发送设备地址

	if (ack == 0)
	{
		Stop_I2c();
		return (FALSE);
	}
	SendByte(memory_address & 0xFF); // 发送内存地址低8位
	if (ack == 0)
	{
		Stop_I2c();
		return (FALSE);
	}
	for (i = 0; i < write_length; i++)
	{
		SendByte(*buffer_point); // 发送数据
		if (ack == 0)
		{
			Stop_I2c();
			return (FALSE);
		}
		buffer_point++;
	}
	Stop_I2c(); // 产生停止信号
	return (TRUE);
}

/**
 * @brief  写入多个字节到FRAM
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据发送缓冲区
 * @param  write_length: 写入数据长度
 * @param  verify_type: 校验类型
 * @retval 0 - 写入失败, 1 - 写入成功
 */
unsigned char EE_WriteNByte(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int write_length, unsigned char verify_type)
{
	FRAM_WriteStr(device_address, memory_address, buffer_point, write_length);
	return 1;
}

/**
 * @brief  读取多个字节从FRAM
 * @param  device_address: I2C设备地址
 * @param  memory_address: FRAM内存地址
 * @param  buffer_point: 数据接收缓冲区
 * @param  read_length: 读取数据长度
 * @retval 0 - 读取失败, 1 - 读取成功
 */
unsigned char EE_ReadNByte(unsigned char device_address, unsigned int memory_address, unsigned char *buffer_point, unsigned int read_length)
{
	unsigned char check_flag = 0;
	FRAM_ReadStr(device_address, memory_address, buffer_point, read_length);
	return check_flag;
}

/**
 * @brief  初始化IIC
 * @param  None
 * @retval None
 */
void I2C_Init(void) // 初始化IIC
{

	SCL_OUT; // SCL线设置为输出
	SDA_OUT; // SDA线设置为输出
}

两种函数最终调用时通用。具体请自测。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐