手把手教你用STM32 GPIO模拟I2C时序读写EEPROM

本实验目的:用MCU的GPIO口模拟I2C时序对存储器EEPROM进行数据读出和写入
所用开发板:STM32F103ZET6
所用库函数:STM32 HAL 库
开发环境— :Keil u5.27
参考文章—I2C时序参考

  1. I2C时序介绍

时序可以阅读大佬所写的I2C介绍,我这里不在过多讲解。 I2C时序参考

  1. I2C时序代码编写

  • 起始信号:
 /**
 * @brief       I2C起始信号
 **/
void I2C_start()
{
 _SDA(1);        //拉高SDA
 delay_us(10);   //SDA持续时间大于4.7us 
 _SCL(1);        //拉高SCL
 delay_us(10);
 _SDA(0);        //拉低SDA
 delay_us(10);   //大于4us后SCL拉低
 _SCL(0);        //拉低SCL,起始信号已发出,准备发送数据(如果不拉低的话,SDA跳变就是起始信号或者停止信号,就不是数据信号了)
}
  • 停止信号:
 /**
 * @brief       I2C停止信号
 **/
void I2C_stop()
{
 _SDA(0);        //拉低SDA
 delay_us(10);   //SDA持续时间大于4.7us 
 _SCL(1);        //拉高SCL
 delay_us(10);
 _SDA(1);        //拉高SDA产生应答信号
 delay_us(10);    
}
  • MCU产生应答信号:
 /**
 * @brief       产生应答
 **/
void ack_R()
{
_SCL(0);        //确保SDA跳变不是产生开始信号和停止信号
 delay_us(10);
_SDA(0);        //拉低SDA产生应答
_SCL(1);        //拉高SCL
delay_us(10);
_SCL(0);        //拉低SCL
}
  • MCU产生非应答信号:
 /**
 * @brief       产生非应答
 **/
void ack_N()
{
_SCL(0);        //确保SDA跳变不是产生开始信号和停止信号
 delay_us(10);
_SDA(1);        //拉高SDA产生非应答
_SCL(1);        //拉高SCL
delay_us(10);
_SCL(0);        //拉低SCL
}
}
  • MCU等待EEPROM返回应答信号:
 /**
 * @brief       等待应答
 * @retval      1:非应答 0:应答
 **/
uint8_t ACK_wait()
{
uint16_t count; //等待应答计数
uint8_t  ack;
_SDA(1);         //主机拉高SDA,使得SDA引脚可以作为输入
delay_us(10);
_SCL(1);         //主机拉高SCL,等待应答信号产生。
delay_us(10);
while(I2C_READ_SDA==1) //等待应答产生
 {
  count++;
  if(count>2000)
  {
  lcd_show_string(0,0,100,50,16,(char*)"ack is nack",BLACK);
  I2C_stop();   //通讯异常,立即停止
  ack = 1;      //从机产生了非应答信号
  }
 }
 lcd_show_string(0,0,100,50,16,(char*)"ack is ack",BLACK);
 _SCL(0);       //拉低SCL,等待应答结束,开始发送下一字节数据
return ack;
}
  • MCU向EEPROM写一字节数据:
/**
 * @brief       主机向从机写一个字节数据
 * @param       data:发送的字节数据
 **/
void I2C_write_byte(uint8_t data)
{
uint8_t i;
for(i=0;i<8;i++)
  {
   _SCL(0);         //拉低SCL开始发送数据
   if(data&0x80)    //发送数据的最高位
   {
   _SDA(1); 
   }
   else
   {
   _SDA(0);
   } 
   data<<=1;        //次高位变成高位
   delay_us(10);    
   _SCL(1);         //拉高SCL将数据发送出去
   delay_us(10);
  }
  _SCL(0);          //拉低SCL准备接受应答信号
}
  • MCU向EEPROM读一字节数据:
 /**
 * @brief       主机向从机读字节数据
 * @param       ack 1:产生非应答 0:产生应答
 * @retval      主机读取的数据
 **/
uint8_t I2C_read_byte(uint8_t ack)
{
  uint8_t i,res ;
for(i=0;i<8;i++)
  {
   _SDA(1);          //主机拉高SDA,使得SDA引脚可以作为输入
   _SCL(0);          //拉低SCL
   delay_us(10);
   _SCL(1);          //拉高SCL读取一位数据
   res<<=1;    
   if(I2C_READ_SDA==1){res++;}//如果读取的SDA为1,则接受的数据+1 
   delay_us(10); 
  }
  if(ack)
  {
    ack_N();
  }
  else
  {
    ack_R();
  }
  return res;
}
  1. I2C总体代码

  • I2C.c文件:
/**
****************************************************************************************************
* @file        I2C.c
* @author      TGU_LINBO
* @version     V1.0
* @date        2023-02-01
* @brief       软件模拟I2C程序
* @GPIO        SCL:PB6 SDA: PB7 
****************************************************************************************************
**/
#include "I2C.h"
/**
* @brief       I2C初始化函数
**/
void I2C_init()
{
   GPIO_InitTypeDef gpio_init_struct;
   C02_GPIO_CLK_ENABLE();                                   //GPIOB时钟使能

   gpio_init_struct.Pin = C02_SCL;                          //SCL引脚
   gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;             //推挽输出
   gpio_init_struct.Pull = GPIO_PULLUP;                     //上拉
   gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;           //高速 
   HAL_GPIO_Init(C02_GPIO_PORT, &gpio_init_struct);         //初始化SCL引脚 

   gpio_init_struct.Pin =C02_SDA;                           //SDA引脚
   gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;             //开漏输出
   HAL_GPIO_Init(C02_GPIO_PORT, &gpio_init_struct);         //初始化SDA引脚
   /*****空闲状态*****/
   _SCL(0);
   _SDA(0);
   delay_ms(10);
}
/**
* @brief       I2C起始信号
**/
void I2C_start()
{
_SDA(1);        //拉高SDA
delay_us(10);   //SDA持续时间大于4.7us 
_SCL(1);        //拉高SCL
delay_us(10);
_SDA(0);        //拉低SDA
delay_us(10);   //大于4us后SCL拉低
_SCL(0);        //拉低SCL,起始信号已发出,准备发送数据(如果不拉低的话,SDA跳变就是起始信号或者停止信号,就不是数据信号了)
}

/**
* @brief       I2C停止信号
**/
void I2C_stop()
{
_SDA(0);        //拉低SDA
delay_us(10);   //SDA持续时间大于4.7us 
_SCL(1);        //拉高SCL
delay_us(10);
_SDA(1);        //拉高SDA产生应答信号
delay_us(10);    
}
/**
* @brief       等待应答
* @retval      1:非应答 0:应答
**/
uint8_t ACK_wait()
{
uint16_t count; //等待应答计数
uint8_t  ack;
_SDA(1);         //主机拉高SDA,使得SDA引脚可以作为输入
delay_us(10);
_SCL(1);         //主机拉高SCL,等待应答信号产生。
delay_us(10);
while(I2C_READ_SDA==1) //等待应答产生
{
 count++;
 if(count>2000)
 {
 lcd_show_string(0,0,100,50,16,(char*)"ack is nack",BLACK);
 I2C_stop();   //通讯异常,立即停止
 ack = 1;      //从机产生了非应答信号
 }
}
lcd_show_string(0,0,100,50,16,(char*)"ack is ack",BLACK);
_SCL(0);       //拉低SCL,等待应答结束,开始发送下一字节数据
return ack;
}
/**
* @brief       产生非应答
**/
void ack_N()
{
_SCL(0);        //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(1);        //拉高SDA产生非应答
_SCL(1);        //拉高SCL
delay_us(10);
_SCL(0);        //拉低SCL
}
/**
* @brief       产生应答
**/
void ack_R()
{
_SCL(0);        //确保SDA跳变不是产生开始信号和停止信号
delay_us(10);
_SDA(0);        //拉低SDA产生应答
_SCL(1);        //拉高SCL
delay_us(10);
_SCL(0);        //拉低SCL
}
/**
* @brief       主机向从机写一个字节数据
* @param       data:发送的字节数据
**/
void I2C_write_byte(uint8_t data)
{
uint8_t i;
for(i=0;i<8;i++)
 {
  _SCL(0);         //拉低SCL开始发送数据
  if(data&0x80)    //发送数据的最高位
  {
  _SDA(1); 
  }
  else
  {
  _SDA(0);
  } 
  data<<=1;        //次高位变成高位
  delay_us(10);    
  _SCL(1);         //拉高SCL将数据发送出去
  delay_us(10);
 }
 _SCL(0);          //拉低SCL准备接受应答信号
}
/**
* @brief       主机向从机读字节数据
* @param       ack 1:产生非应答 0:产生应答
* @retval      主机读取的数据
**/
uint8_t I2C_read_byte(uint8_t ack)
{
 uint8_t i,res ;
for(i=0;i<8;i++)
 {
  _SDA(1);          //主机拉高SDA,使得SDA引脚可以作为输入
  _SCL(0);          //拉低SCL
  delay_us(10);
  _SCL(1);          //拉高SCL读取一位数据
  res<<=1;    
  if(I2C_READ_SDA==1){res++;}//如果读取的SDA为1,则接受的数据+1 
  delay_us(10); 
 }
 if(ack)
 {
   ack_N();
 }
 else
 {
   ack_R();
 }
 return res;
}
  • I2C.h文件:
/**
 ****************************************************************************************************
 * @file        I2C.h
 * @author      TGU_LINBO
 * @version     V1.0
 * @date        2023-02-01
 ****************************************************************************************************
 **/
#ifndef __I2C_H
#define __I2C_H
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LCD/lcd.h"
/********引脚宏定义********/
#define C02_SCL GPIO_PIN_6  //24C02 SCL引脚
#define C02_SDA GPIO_PIN_7  //24C02 SDA引脚
#define C02_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* 所在IO口时钟使能 */
#define C02_GPIO_PORT GPIOB

/* 24C02端口定义 */
#define _SCL(x)   do{ x ? \
                      HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SCL,GPIO_PIN_SET) : \
                      HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SCL,GPIO_PIN_RESET); \
                  }while(0)      /* SCL翻转 */
#define _SDA(x)   do{ x ? \
                      HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SDA,GPIO_PIN_SET) : \
                      HAL_GPIO_WritePin(C02_GPIO_PORT,C02_SDA,GPIO_PIN_RESET); \
                  }while(0)      /* SDA翻转 */
#define I2C_READ_SDA     HAL_GPIO_ReadPin(C02_GPIO_PORT,C02_SDA) /* 读取SDA */
/********函数声明********/
void I2C_init(void);             //I2C初始化
void I2C_start(void);            //起始信号
void I2C_stop(void);             //停止信号
uint8_t ACK_wait(void);          //主机等待应答
void ack_N(void);                //主机产生非应答
void ack_R(void);                //主机产生应答
void I2C_write_byte(uint8_t data);//主机向从机写一个字节数据
uint8_t I2C_read_byte(uint8_t ack);//主机向从机读取一个字节数据                  
#endif
  1. EEPROM介绍

EEPROM 24C02 阅读大佬的文章即可AT24C02参考

  1. EEPROM代码编写

  • 24C02.c:
/**
****************************************************************************************************
* @file        24c02.c
* @author      TGU_LINBO
* @version     V1.0
* @date        2023-02-01
* @brief       AT24C02 I2C 读写程序
* @GPIO        SCL:PB6 SDA: PB7 
****************************************************************************************************
**/

#include "24c02.h"
#include "I2C.h"
#include <string.h>
/**
* @brief 主机向24c02选中地址写入一字节数据
* @param add:写入的地址 data 写入的数据
**/
void write_byte(uint8_t add,uint8_t data)
{
I2C_start();            //发送开始信号
I2C_write_byte(0XA0);   //发送24C02的从设备地址,为写操作
ACK_wait();             //等待应答
I2C_write_byte(add);    //发送要写入的地址
ACK_wait();
I2C_write_byte(data);   //发送要写入的数据
ACK_wait();
I2C_stop();             //发送停止信号
delay_ms(10);           //注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 
}
 /**
* @brief  主机向24c02选中地址读入一字节数据
* @param  add:需要读出的地址 *buf 写入的数据存入buf数组中
* @retval 读取的数据
**/
uint8_t read_byte(uint8_t add)
{
uint8_t res;
I2C_start();            //发送开始信号
I2C_write_byte(0XA0);   //发送24C02的从设备地址,为写操作
ACK_wait();             //等待应答
I2C_write_byte(add);    //发送要读取的地址
ACK_wait();
I2C_start();            //发送开始信号
I2C_write_byte(0XA1);   //发送24C02的从设备地址,为读操作
ACK_wait();   
res=I2C_read_byte(0);//开始读取
I2C_stop();             //发送停止信号
return res;
delay_ms(10);           //注意: EEPROM 读取比较慢,必须等到10ms后再写下一个字节 
}
 /**
* @brief 主机向24c02选中地址开始写入一连串数据
* @param add:写入的起始地址 buf:写入的数据
**/
void write_byte_con(uint8_t add,uint8_t *buf)
{
 uint8_t i,count,addr;
 addr=add;
 count=sizeof(buf);         //计算写入的数组长度
 for(i=0;i<15;i++)
     {
       write_byte(addr+i,buf[i]);
       delay_ms(10);
     }
}
  /**
* @brief 主机向24c02选中地址开始读取一连串数据
* @param add:开始读取的起始地址 buf:读取的数据存放数组
* @param data_len:需要读取的数据长度
**/
void read_byte_con(uint8_t add,uint8_t *buf,uint8_t data_len)
{

I2C_start();            //发送开始信号
I2C_write_byte(0XA0);   //发送24C02的从设备地址,为写操作
ACK_wait();             //等待应答
I2C_write_byte(add);    //发送要读取的地址
ACK_wait();
I2C_start();            //发送开始信号
I2C_write_byte(0XA1);   //发送24C02的从设备地址,为读操作
ACK_wait();
 for(int i=0;i<data_len;i++)
  {
   buf[i]=I2C_read_byte(0);//开始读取
   delay_ms(10);           //注意: EEPROM 读取比较慢,必须等到10ms后再写下一个字节
  }                        
I2C_read_byte(1);          //发送一个非应答信号告诉从机不在读取数据了
I2C_stop();                //发送停止信号
delay_ms(10);               
}
  • 24C02.h:
/**
****************************************************************************************************
* @file        24c02.h
* @author      TGU_LINBO
* @version     V1.0
* @date        2023-02-01
* @brief       AT24C02 I2C 读写程序
* @GPIO        SCL:PB6 SDA: PB7 
****************************************************************************************************
**/
#ifndef __24C02_H
#define __24C02_H
#include "I2C.h"
/********函数声明********/
void write_byte(uint8_t add,uint8_t data);    //主机向24C02指定地址写入数据
uint8_t read_byte(uint8_t add);               //主机向24C02指定地址读取数据 
void write_byte_con(uint8_t add,uint8_t *buf);//主机向24c02选中地址开始写入一连串数据
void read_byte_con(uint8_t add,uint8_t *buf,uint8_t data_len); //主机向24c02选中地址开始读取一连串数据
#endif
  1. 实验现象演示

这里我将通过STM32F103ZET6将数据存储在EEPROM中,然后再从EEPROM中读取出来。

  • main.c:
/**
****************************************************************************************************
* @file        main.c
* @author      TGU_LINBO
* @version     V1.0
* @date        2023-02-01
* @brief       软件模拟I2C程序读写2C402存储器程序  
* @GPIO        SCL:PB6 SDA: PB7 
****************************************************************************************************
**/

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "I2C.h"
#include "24C02.h"
#include "key.h"
int main(void)
{
   uint8_t x= 0;
   uint8_t lcd_id[12];
   uint8_t addres = 0x1f;                              /* 写入的地址 */
     uint8_t Data[] = {'I','L','O','V','E','Y','O','U','B','A','B','Y','Y','E','S'}; /* 写入的数据 */
   uint8_t R_Data[]={0};                               /* 初始化HAL库 */
   uint8_t key;
   HAL_Init();                                         /* 初始化HAL库 */
   sys_stm32_clock_init(RCC_PLL_MUL9);                 /* 设置时钟, 72Mhz */
   delay_init(72);                                     /* 延时初始化 */
   usart_init(115200);                                 /* 串口初始化为115200 */
   led_init();                                         /* 初始化LED */
   lcd_init();                                         /* 初始化LCD */
     I2C_init();                                         /* 初始化I2C */
   key_init();                                         /* 初始化按键 */
   g_point_color = BLACK;
   sprintf((char *)lcd_id, "LCD ID:%04X", lcddev.id);  /* 将LCD ID打印到lcd_id数组 */
   lcd_clear(WHITE);
   
  
   
    /****多字节读写字节数据测试****/                
   printf("写入的数据为:\r\n");
   write_byte_con(addres,Data);
   for(int p=0;p<sizeof(Data);p++)
   {
    printf("%c ",Data[p]);
   }
   printf("\r\n");

   printf("读入的数据为:\r\n");  
   read_byte_con(addres,R_Data,sizeof(Data));
   for(int p=0;p<sizeof(Data);p++)
   {
    printf("%c ",R_Data[p]);
   }
   printf("\r\n");
   
   
   while (1)
   {	
   }
}

下载到单片机后,单片机通过串口助手打印实验信息在这里插入图片描述
代码整个工程在我的GitHub上本实验工程代码下载免费

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
1. 硬件连接 首先,我们需要将EEPROM连接到STM32微控制器的I2C总线上。在本示例中,我们将使用STM32F4Discovery开发板,并将EEPROM连接到其I2C1总线。以下是硬件连接图: ![image.png](attachment:image.png) 2. STM32CubeIDE设置 在STM32CubeIDE中,我们需要启用I2C总线并配置它。以下是步骤: - 打开STM32CubeIDE并创建一个新项目。 - 选择“STM32F4xx”系列微控制器和您的开发板型号。 - 启用I2C总线。在“Pinout & Configuration”选项卡中,选择“I2C1”并启用它。 - 配置I2C总线。在“Configuration”选项卡中,选择“I2C1”并进行必要的配置,例如时钟速度和地址模式等。 3. 代码实现 现在,我们可以开始编代码来EEPROM。以下是基本的代码框架: ```c #include "stm32f4xx_hal.h" #include "stdio.h" I2C_HandleTypeDef hi2c1; #define EEPROM_ADDR 0xA0 void EEPROM_Write(uint16_t addr, uint8_t data); uint8_t EEPROM_Read(uint16_t addr); int main(void) { HAL_Init(); __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_I2C1_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF4_I2C1; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 100000; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 = 0; hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK) { Error_Handler(); } uint8_t data = 0x55; EEPROM_Write(0x0001, data); uint8_t read_data = EEPROM_Read(0x0001); while (1) { } } void EEPROM_Write(uint16_t addr, uint8_t data) { uint8_t tx_data[3] = {addr >> 8, addr & 0xFF, data}; HAL_I2C_Master_Transmit(&hi2c1, EEPROM_ADDR, tx_data, 3, 1000); } uint8_t EEPROM_Read(uint16_t addr) { uint8_t tx_data[2] = {addr >> 8, addr & 0xFF}; uint8_t rx_data[1]; HAL_I2C_Master_Transmit(&hi2c1, EEPROM_ADDR, tx_data, 2, 1000); HAL_I2C_Master_Receive(&hi2c1, EEPROM_ADDR, rx_data, 1, 1000); return rx_data[0]; } ``` 在此示例中,我们定义了两个函数:`EEPROM_Write()`和`EEPROM_Read()`。`EEPROM_Write()`函数将一个字节EEPROM,`EEPROM_Read()`函数从EEPROM取一个字节。这两个函数使用STM32 HAL库中的I2C主机传输函数。 4. 测试 现在,我们可以将代码下载到STM32F4Discovery开发板上并测试代码。在本示例中,我们将向地址0x0001入0x55,并从该地址取数据。如果一切正常,我们应该能够入的数据(0x55)。 以上就是使用STM32CubeIDE进行I2C接口EEPROM的步骤和代码示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁芜~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值