STM32F103 I2C软件模拟(AT24C02)

news/2025/1/3 10:21:04 标签: stm32, 单片机, 嵌入式硬件

有关I2C通信协议我们在《通信协议-I2C》已经进行了详细的介绍,因此这一节不再重复介绍。

一、软件/硬件I2C

想要控制STM32产生I2C方式的通讯,可以采用软件模拟或硬件I2C这两种方式。

1.1 软件模拟

所谓软件模拟,即直接使用CPU内核按照I2C协议的要求控制GPIO输出高低电平。

如控制产生I2C的起始信号时, 先控制作为SCL线的GPIO引脚输出高电平, 然后控制作为SDA线的GPIO引脚在此期间完成由高电平至低电平的切换,最后再控制SCL线切换为低电平,这样就输出了一个标准的I2C起始信号。

1.2 硬件I2C

硬件I2C是指直接利用STM32芯片中的硬件I2C外设,该硬件I2C外设跟USART串口外设类似,只要配置好对应的寄存器, 外设就会产生标准串口协议的时序。

使用它的I2C外设则可以方便地通过外设寄存器产生I2C协议方式的通讯,如初始化好I2C外设后, 只需要把某寄存器位置1,那么外设就会控制对应的SCLSDA线自动产生I2C起始信号,而不需要内核直接控制引脚的电平。

相对来说,硬件I2C直接使用外设来控制引脚,可以减轻CPU的负担。不过使用硬件I2C时必须使用某些固定的引脚作为SCLSDA, 软件模拟I2C则可以使用任意GPIO引脚,相对比较灵活。

在本开发板中,由于STM32F103RCT6芯片引脚较少,资源比较紧张, 在设计硬件时不方便使用硬件I2C指定的引脚连接外部设备,所以在控制程序上使用软件模拟I2C的方式。

二、AT24C02

2.1 回顾

有关AT24C02)可以参考《通信协议-I2C》小节中的介绍,我们介绍了AT24C0x的读写命令。

后续我们又在《linux驱动移植-I2C驱动移植(OLED SSD1306)》中介绍了I2C设备驱动的编写,并以OLED SSD1306作为实现设备。

2.2 硬件接线

我所使用的STM32F103RTC6开发板上有一块数据存储器,采用的是AT24C02芯片;

AT24C02是一个2K位串行可擦可编程只读存储器(EEPRAM),内部含有2568位字节,AT24C02有一个16字节页写缓冲区,该器件通过I2C总线接口进行操作,有一个专门的写保护功能。

其中I2C_SCL连接到处理器的PC12引脚,I2C_SDA连接到处理器的PC11引脚。

三、AT24C02源码实现

3.1 I2C软件模拟
3.1.1 I2C初始化

配置PC11~PC12引脚,均配置为通用推挽输出,最大速度50MHZ,输出高电平;

//宏定义I2C端口***********************************************
#define I2C_SCL     PCout(12)
#define I2C_SDA     PCout(11)

//宏定义SDA数据方向********************************************
#define SDA_OUT()   gpio_init(PC11,GPO_PUSH_PULL_50,HIGH)  
#define SCL_OUT()   gpio_init(PC12,GPO_PUSH_PULL_50,HIGH)

//宏定义读取SDA数据*******************************************
#define Read_SDA    PCin(11)

void SDA_IN(void)   
{  
   gpio_init(PC11, GPI_DOWN,HIGH); // 输入下拉
   PCout(11)=0;
}
/**********************************************************************************
 *
 *		Description:I2C初始化
 *
 **********************************************************************************/
void I2C_Init(void)			  //I2C初始化
{
  SDA_OUT();          //SDA设置为输出       
  SCL_OUT();		  //SCL设置为输出
  I2C_SCL=1;
  I2C_SDA=1;  
}
3.1.2 起始信号

I2C通信的起始信号由主设备发起,SCL保持高电平,SDA由高电平变为低电平;只有在起始信号发送之后,其它数据才有效;

/**********************************************************************************
 *
 *		Description:I2C起始信号
 *                   SCL=1;  SDA:1->0   
 *
 **********************************************************************************/
void  I2C_Start(void)		 //I2C起始信号
{
   SDA_OUT();
   I2C_SDA =1;				   
   delay_us(5);
   I2C_SCL =1;
   delay_us(5);
   I2C_SDA =0;
   delay_us(5);
   I2C_SCL =0;
}
3.1.3 终止信号

I2C通信的终止信号由主设备发起,SCL保持高电平,SDA由低电平变为高低电平;随着终止信号的出现,所有外部操作就结束;

/**********************************************************************************
 *
 *		Description:I2C终止信号
 *                  SCL=1;  SDA:0->1   
 *
 **********************************************************************************/
void I2C_Stop(void)		   //I2C终止信号
{
   SDA_OUT();
   I2C_SDA =0;
   delay_us(5);
   I2C_SCL =1;
   delay_us(5);
   I2C_SDA =1;
   delay_us(5);
   I2C_SCL =0;
}
3.1.4 应答信号

I2C总线在进行数据传送时,传送的字节数没有限制,但是每个字节长度必须为8位。

数据传送过程中,先传送最高位(MSB),接收端在收到有效数据后向对方相应的信号,发送端每发送一个字节数据(8位),在第9个始终周期释放数据线去接收对方的应答;因此一帧数据共有9位;

  • SDA位低电平位有效应答(ACK),表示接收端已经接收到数据;
  • SDA是高电平位无效应答(NAK),表示接收端没有接收成功;

主设备发送完数据需要等待从设备的应答,GPIO模拟:

/**********************************************************************************
 *
 *		Description:I2C(主机)等待来自从机的应答信号
 *        parameter:返回0:接受失败   1:接收成功  
 *
 **********************************************************************************/
u8 I2C_WaitAck(void)		    //等待来自从机的应答信号
{
  u16 time;
  SDA_IN();
  I2C_SCL =0;
  delay_us(5);
  I2C_SDA =1;
  delay_us(5);
  I2C_SCL =1;
  delay_us(5);
  while(Read_SDA)
  {
    time++;
	if(time>=2500)
	{
	  I2C_Stop();
	  return 0;  
	}
  }
  I2C_SCL =0;
  return 1;
}

主设备接收到从设备发送的数据后,需要向从设备发送方发送应答,GPIO模拟:

/**********************************************************************************
 *
 *		Description:I2C(主机)产生应答信号
 *        parameter: ack:0不应答  1:应答        
 *
 **********************************************************************************/
void I2C_Ack(u8 ack)			//产生应答信号
{
   SDA_OUT();
   I2C_SCL =0;
   delay_us(5);
   if(ack)
      I2C_SDA =0;
   else
      I2C_SDA =1;
   I2C_SCL =1;
   delay_us(5);
   I2C_SCL =0;
   delay_us(5);
}
3.1.5 有效数据

I2C总线进行数据传送时,时钟信号SCL为高电平期间,数据线SDA上的数据必须稳定;只有在SCL上的信号为低电平时,SDA上的高电平或低电平状态才允许变化。

因为当SCL是高电平时,数据线SDA的变化被规定为控制命令(也就是前面的起始信号和终止信号)。

主设备向从设备发送发送一个字节数据,GPIO模型模拟:

/**********************************************************************************
 *
 *		Description:I2C写一个字节数据
 *             byte:一个字节的数据   
 *
 **********************************************************************************/
void I2C_WriteData(u8 byte)			 //I2C写一个字节数据
{
  u8 i=0;
  SDA_OUT();
  I2C_SCL =0;
  for(i=0;i<8;i++)
  {
    I2C_SDA =(byte&0x80)>>7;
	byte <<=1;
	delay_us(5);
	I2C_SCL =1;
	delay_us(5);
	I2C_SCL =0;
	delay_us(5);
  }
}

主设备从从设备读取一个字节数据,GPIO模拟:

/**********************************************************************************
 *
 *		Description:I2C(主机)读取一个数据 
 *        parameter:返回读取的数据  
 *
 **********************************************************************************/
u8 I2C_ReadData(void)			   //I2C读取一个数据 
{
  u8 i=0;
  u8 data=0;
  SDA_IN();
  I2C_SCL =0;
  for(i=0;i<8;i++)
  {
	I2C_SCL =1;
	data <<=1;
	data |=(u8)Read_SDA;
	delay_us(5);
	I2C_SCL =0;
	delay_us(5);
  }
  return data;
}
3.2 AT24C02
3.2.1 按字节写
/**********************************************************************************************
 *
 *   function:向24c02的address地址中写入一字节数据  
 *    address:0~0xFF        
 *
 **********************************************************************************************/
void Write24c02(u8 address,u8 byte)
{
   I2C_Start();
   I2C_WriteData(0xA0);
   I2C_WaitAck();
   I2C_WriteData(address);
   I2C_WaitAck();
   I2C_WriteData(byte);
   I2C_WaitAck();
   I2C_Stop();
   delay_ms(10);       //这个延时一定要足够长,否则会出错。因为24c02在从sda上取得数据后,还需要一定时间的烧录过程。
}
3.2.2 随机读
/**********************************************************************************************
 *
 *   function:从24c02的地址address中读取一个字节数据
 *    address:0~0xFF 
 *
 ***********************************************************************************/
u8 Read24c02(u8 address)
{
   u8 byte;
   I2C_Start();
   I2C_WriteData(0xA0);
   I2C_WaitAck(); 
   I2C_WriteData(address);
   I2C_WaitAck();
   I2C_Start();
   I2C_WriteData(0xA1);
   I2C_WaitAck();
   byte=I2C_ReadData();
   //I2C_Ack(0);
   I2C_Stop();
   delay_ms(10);
   return  byte;
}
3.2.3 连续写
/**********************************************************************************************
 *
 *   function:向24c02的address地址中写入字符串
 *    address:24C02的起始地址    0~0xFF 
 *        str:字符串的指针    
 *
 **********************************************************************************************/
void Write_24c02Buffer(u8 address,const u8 *str)
{
     u8 i=0;
	 u8 length=0;
	 length= strlen((char *)str);
	 for(i=0;i<length;i++)
	 {
	   Write24c02(address++,str[i]);
	 }
}
3.2.3 连续读
/**********************************************************************************************
 *
 *   function:从24c02的address地址中读取字符串
 *    address:24c02的起始地址    0~0xFF 
 *        str: 写入的字符串指针	      
 *     length: 写入的数据长度不包含'\0'              
 *
 **********************************************************************************************/
void  Read_24c02Buffer(u8 address,u8 *str,u8 length)
{
  u8 i=0;
  for(i=0;i<length;i++)
  {
   str[i]=Read24c02(address++);
  }
  str[i]='\0';
}
3.3 实现功能

这里我们通过向AT24C02写入并读取来测试I2C功能。

3.3.1 main函数实现
int main()
{ 
   u8 *time;
   char temp[256];
   STM32_Clock_Init(9);         	                //系统时钟初始化

   STM32_NVIC_Init(2,USART1_IRQn,0,1);		        //串口中断优先级初始化,其中包括中断使能
   usart_init(USART_1,115200);				        //串口1初始化,波特率115200 映射到PA9 PA10

   STM32_NVIC_Init(2,RTC_IRQn,0,1);		            //RTC中断优先级初始化,其中包括中断使能
   while(RTC_Init());                               //RTC初始化

   OLED12864_GPIO_Init();                           //GPIO初始化
   OLED12864_Init();                                //OLED初始化
   OLED_P16x8Str(45,0,"OLED");	                    //调用LCD_P8x16Str字符串显示函数,在第0页即第一行的第45列开始,显示字符串“OLED"

   I2C_Init();                                     //I2C初始化

   OLED_P8x16Chi(16,6,"安徽理工大学");
   Write_24c02Buffer(10,"郑洋是好人");             //连续写入
   while(1)
   {   
       Read_24c02Buffer(10,temp,10);
	   printf("打印-----%s\n",temp);
	   time = RTCTime();
       OLED_P8x6Str(8,4,time);      //显示当前时间
	   delay_ms(1000);
   } 
}
3.3.2 测试

编译程序并下载测试,打开串口查看;

下图是使用逻辑分析器捕捉到的模拟I2C信号;

参考文章

[1] I2C—读写EEPROM


http://www.niftyadmin.cn/n/5806808.html

相关文章

开发运维基本功:无需复杂配置快速实现本地Nginx的公网远程访问

文章目录 前言1. 本地连接测试2. 飞牛云安装Cpolar3. 配置公网连接地址4. 飞牛云APP连接测试5. 固定APP远程地址6. 固定APP地址测试 前言 现在生活和工作中的各种设备都变得越来越智能&#xff0c;而数据存储的需求也随之剧增。想象一下&#xff1a;你正在外地出差&#xff0c…

Lottie动画源码解析

Lottie是一个很成熟的开源动画框架&#xff0c;它支持直接使用从AE导出的动画文件&#xff0c;在不同平台均可快速使用&#xff0c;大大减轻了程序员的工作量&#xff0c;也让复杂的动画成为可能。该动画文件使用Json格式来描述内容&#xff0c;可以大大缩减文件的体积。在Andr…

网络安全离我们不远!

前言 昨天晚上有朋友将公网上的一台 redis 密码设置为 123456&#xff0c;并且觉得没什么影响&#xff0c;再结合我之前毕业设计时被删库勒索&#xff0c;以及工作中碰到的网络安全相关的事情&#xff0c;就有了本篇感想&#xff0c;网络安全离我们并不远&#xff01; 毕设 M…

正则表达式 - 运算符优先级

正则表达式 - 运算符优先级 正则表达式(Regular Expression,简称Regex)是一种用于处理字符串的强大工具,它通过特定的语法规则来匹配、查找和替换文本中的特定模式。在正则表达式中,运算符的优先级决定了表达式各部分的处理顺序,这对于正确理解和编写正则表达式至关重要…

游戏引擎学习第66天

我们上次讨论的是模拟区域 我们刚刚完成了代码更新&#xff0c;采用了模拟区域。虽然完成了更新&#xff0c;但我们还没有机会彻底调试它。实际上&#xff0c;有几个功能仍然需要添加&#xff0c;以确保它能够完整运行并完成所有所需的操作。因此&#xff0c;今天我们需要进行…

SQL如何添加数据?|SQL添加数据示例

在SQL&#xff08;Structured Query Language&#xff09;中&#xff0c;添加数据通常是指向已存在的数据库表中插入新的行记录。这是通过INSERT INTO语句实现的。下面详细的SQL插入数据解析&#xff1a; SQL INSERT INTO 语句的基本形式 INSERT INTO table_name (column1, c…

VScode 只能运行c,运行不了c++的解决问题

原文链接&#xff1a;Vscode只能运行c&#xff0c;运行不了c的解决方法 VScode 只能运行c&#xff0c;运行不了c&#xff0c;怎么回事呢&#xff0c;解决问题&#xff1a; 在tasks.json中加上“"-lstdc"”&#xff0c; 这样之后 要重启VScode&#xff0c;点击链接…

Java [后端] 开发日常记录(1)

目录 1、常用的注解 2、对字符串的处理 3、对JSON串的处理 -- The End -- 详细如下&#xff1a; 1、常用的注解 若返回的字段中有NUll&#xff0c;则不返回 JsonInclude(value JsonInclude.Include.NON_NULL) //在实体类中添加这个注解 JsonInclude(JsonInclude.Include.NON…