STM32学习心得二十九:I2C通讯实验及相关代码解读

记录一下,方便以后翻阅~
主要内容:
1) I2C通讯协议;
2) 24C02芯片介绍;
3) 相关实验代码解读。
实验功能:系统启动后,通过KEY1按键来控制24C02的写入,通过另外一个按键KEY0来控制24C02的读取。并在串口调试助手上面显示相关信息。
官方资料:《STM32中文参考手册V10》第24章——I2C接口
1. I2C通讯协议概念
I2C(IIC,Inter-Integrated Circuit),两线式串行总线,由PHILIPS公司开发用于连接微控制器及其外围设备。
I2C是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。
I2C是半双工通信方式(这个在《学习心得十四:串口通信相关知识及配置方法》有讲)。
I2C总线系统结构如下图所示(这里先提一句,I2C协议里,空闲状态时SDA和SCL都是高电平!):
在这里插入图片描述
2. I2C通信协议及相关代码解读
2.1 空闲状态
I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
2.2 开始信号和停止信号
在这里插入图片描述
起始信号:当SCL为高时,SDA由高到低跳变。启动信号是一种电平跳变时序信号,而不是一个电平信号。
起始信号相关代码:

void IIC_Start(void)
{
  SDA_OUT();              //先设SDA线为输出//
  IIC_SDA=1;              //SDA输出高//         
  IIC_SCL=1;              //SCL输出高,此时为空间状态//
  delay_us(4);
  IIC_SDA=0;              //SDA输出由高变低,IIC开始// 
  delay_us(4);
  IIC_SCL=0;              //钳住I2C总线,准备发送或接收数据// 
}     

停止信号:当SCL为高时,SDA由低到高跳变。停止信号是一种电平跳变时序信号,而不是一个电平信号。
停止信号相关代码:

void IIC_Stop(void)
{
  SDA_OUT();              //先设SDA线为输出//
  IIC_SCL=0;              //SCL输出低//
  IIC_SDA=0;              //SDA输出低//
  delay_us(4);
  IIC_SCL=1;              //SCL输出高//
  IIC_SDA=1;              //SDA输出由低变高,发送I2C总线结束信号//
  delay_us(4);                                                   
}

2.3 应答信号
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。
等待应答信号的相关代码:

u8 IIC_Wait_Ack(void)
{
 u8 ucErrTime=0;
 SDA_IN();                   //SDA设置为输入//  
 IIC_SDA=1;delay_us(1);      //SDA设高电平//   
 IIC_SCL=1;delay_us(1);      //SCL设高电平//
 while(READ_SDA)             //读取PB7的值,0或1//
  {
   ucErrTime++;
   if(ucErrTime>250)
    {
     IIC_Stop();
     return 1;               //失败//         
    }
  }
  IIC_SCL=0;                 //当PB7值为0,则SCL时钟输出低电平,0//           
  return 0;                  //成功//
}

对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平(有效应答应这样编写)。
有效/无效应答相关代码:

void IIC_Ack(void)        //有效应答//
{
  IIC_SCL=0;
  SDA_OUT();
  IIC_SDA=0;
  delay_us(2);
  IIC_SCL=1;
  delay_us(2);
  IIC_SCL=0;
}          
void IIC_NAck(void)       //无效应答//
{
  IIC_SCL=0;
  SDA_OUT();
  IIC_SDA=1;
  delay_us(2);
  IIC_SCL=1;
  delay_us(2);
  IIC_SCL=0;
}            

在这里插入图片描述

2.4 数据的有效性:
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之前必须稳定。
在这里插入图片描述
发送数据相关代码(SCL为高电平时,发送数据):

void IIC_Send_Byte(u8 txd)
{                        
 u8 t;   
 SDA_OUT();       
 IIC_SCL=0;                 //SCL为低电平时,可以改变SDA,以实现数据的有效传输//
 for(t=0;t<8;t++)
  {              
   if((txd&0x80)>>7)        //(txd&0x80)>>7值要么是0要么是1,IIC是从最高位开始传输数据的//
    IIC_SDA=1;              //(txd&0x80)>>7值为1时,SDA给1//
   Else
    IIC_SDA=0;              //(txd&0x80)>>7值为0时,SDA给0//
   txd<<=1;    
   delay_us(2);             //对TEA5767这三个延时都是必须的//
   IIC_SCL=1;               //SCL为高电平时,传输SDA//
   delay_us(2); 
   IIC_SCL=0;      
   delay_us(2);
  }       
}            

读数据相关代码(SCL为高电平时,接收数据):

u8 IIC_Read_Byte(unsigned char ack)
{
 unsigned char
 i,receive=0;                   //无符号字符型,对应整数范围0~255//
 SDA_IN();                      //SDA设置为输入//
 for(i=0;i<8;i++)
  {
   IIC_SCL=0; 
   delay_us(2);
   IIC_SCL=1;                   //SCL由低设高后,开始读信号//
   receive<<=1;                 //先将之前接收的信号左移,最右位补0//
   if(READ_SDA)receive++;       //如果接收SDA为1,则最右位改为1//
   delay_us(1);                 //如果接收SDA为0,则不做操作//
  }                                 
 if (!ack)
  IIC_NAck();                   //发送nACK//
 else
  IIC_Ack();                    //发送ACK//  
 return receive;
}

2.5 数据传输:
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
3. 开发版的硬件连接
如下图所示:
在这里插入图片描述
PB6对应IIC的时钟线,PB7对应IIC的数据线。
在这里插入图片描述
PB6和PB7接在EEPROM(24C02)的SCL和SDA上。
4. EEPROM(24C02)介绍
EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。
24C02是一个2Kbit的串行EEPROM存储芯片,可存储256(2K/8)个字节数据。通过I2C总线通讯读写芯片数据,通讯时钟频率可达400KHz。(备注,1Byte=8bit)
4.1 24C02芯片引脚定义如下图所示:
在这里插入图片描述
4.2 A0-A2的引脚用来设置24C02的地址线,由开发版给的硬件连接图可知,默认A0、A1、A2跟GND连接。
24C02的设备地址由下图的第一行所示:
在这里插入图片描述
上图中,24C02地址的前4位为0b1010,即0xA,后三位由A2、A1、A0决定,即0b000,最后一位根据读或写决定,读至1,写至0,因此24CO2的地址读写地址分别为:0b10100001(0xA1)和0b10100000(0xA0)。
4.3 24C02字节写时序/读时序
在这里插入图片描述
24C02写一个字节数据的驱动程序代码:

//写入一个字节,WriteAddr :写入数据的目的地址;DataToWrite:要写入的数据//
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite) {                                                                                                                                                
 IIC_Start();  
 if(EE_TYPE>AT24C16)
  {
   IIC_Send_Byte(0XA0);                         //发送写命令//
   IIC_Wait_Ack();
   IIC_Send_Byte(WriteAddr>>8);                 //发送高地址//
  }
 else
  {
   IIC_Send_Byte(0XA0+((WriteAddr/256)<<1));    //发送器件地址0XA0,写数据// 
  }
 IIC_Wait_Ack();
 IIC_Send_Byte(WriteAddr%256);                  //发送低地址//
 IIC_Wait_Ack();                                                                                       
 IIC_Send_Byte(DataToWrite);                    //发送字节//                                               
 IIC_Wait_Ack();                            
 IIC_Stop();                                    //产生一个停止条件// 
 delay_ms(10);       
}

在这里插入图片描述
24C02读一个字节数据的驱动程序代码:

//读一个字节, ReadAddr:开始读数的地址,返回值:读到的数据//
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
 {                              
  u8 temp=0;                                                                                                                     
  IIC_Start();  
  if(EE_TYPE>AT24C16)                         //实际用的是24C02,小于AT24C16,所以直接跳到else//
   {
    IIC_Send_Byte(0XA0);                        //发送写命令//
    IIC_Wait_Ack();
    IIC_Send_Byte(ReadAddr>>8);               //发送高地址//
    IIC_Wait_Ack();           
   }
  else
   IIC_Send_Byte(0XA0+((ReadAddr/256)<<1));     //发送设备地址0XA0,写数据,(ReadAddr/256)<<1)值为0//
  IIC_Wait_Ack(); 
  IIC_Send_Byte(ReadAddr%256);                     //发送低地址//
  IIC_Wait_Ack();          
  IIC_Start();                                         
  IIC_Send_Byte(0XA1);                             //进入接收模式//                       
  IIC_Wait_Ack();       
  temp=IIC_Read_Byte(0);           
  IIC_Stop();                                    //产生一个停止条件//      
  return temp;
 }

4.4 其他24C02驱动程序
4.4.1 初始化AT24CXX函数

void AT24CXX_Init(void)
 {
  IIC_Init();
 }

4.4.2 检查AT24C02是否正常函数

u8 AT24CXX_Check(void)
 {
  u8 temp;
  temp=AT24CXX_ReadOneByte(255);            //避免每次开机都写AT24CXX//                        
  if(temp==0X55)return 0;                                //返回0说明正常//
  else                                       //排除第一次初始化的情况//
   {
    AT24CXX_WriteOneByte(255,0X55);
    temp=AT24CXX_ReadOneByte(255);   
    if(temp==0X55)return 0;
   }
  return 1;                                                                          
 }

4.4.3 发送一段数据函数

//WriteAddr :开始写入的地址 对24c02为0~255,pBuffer :数据数组首地址,NumToWrite:要写入数据的个数//
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
 {
  while(NumToWrite--)
   {
    AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
    WriteAddr++;
    pBuffer++;
   }
 }

4.4.4 读取一段数据函数

// ReadAddr :开始读出的地址对24c02为0~255,pBuffer  :数据数组首地址,NumToRead:要读出数据的个数//
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
 {
  while(NumToRead)
   {
    *pBuffer++=AT24CXX_ReadOneByte(ReadAddr++); 
    NumToRead--;
   }
 }  

5. 实验相关代码解读
5.1 myiic.h头文件代码解读

#define __MYIIC_H
#include "sys.h"
#define SDA_IN()  {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}   //上拉输入模式//
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}   //推挽输出,50MHz//  
#define IIC_SCL    PBout(6) //SCL//
#define IIC_SDA    PBout(7) //SDA//  
#define READ_SDA   PBin(7)  //输入SDA// 
//手把手编写IIC所有操作函数//
void IIC_Init(void);                //初始化IIC的IO口//     
void IIC_Start(void);               //发送IIC开始信号//
void IIC_Stop(void);                //发送IIC停止信号//
void IIC_Send_Byte(u8 txd);         //IIC发送一个字节//
u8 IIC_Read_Byte(unsigned char ack);//IIC读取一个字节//
u8 IIC_Wait_Ack(void);              //IIC等待ACK信号//
void IIC_Ack(void);                 //IIC发送ACK信号//
void IIC_NAck(void);                //IIC不发送ACK信号//
void IIC_Write_One_Byte(u8 daddr,u8 addr,u8 data);
u8 IIC_Read_One_Byte(u8 daddr,u8 addr);   
#endif

5.2 myiic.c文件代码解读

#include "myiic.h"
#include "delay.h"
//编写初始化IIC函数//
void IIC_Init(void)
{          
 GPIO_InitTypeDef GPIO_InitStructure;
 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE ); //使能GPIOB时钟//   
 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ;       //推挽输出//
 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 GPIO_Init(GPIOB, &GPIO_InitStructure);
 GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);              //PB6,PB7 输出高//
}
//编写IIC起始信号函数//
void IIC_Start(void)
{
 SDA_OUT();              //SDA线设为输出//
 IIC_SDA=1;              //SDA输出高//     
 IIC_SCL=1;              //SCL输出高,此时空间状态//
 delay_us(4);
 IIC_SDA=0;              //SDA输出低,IIC开始// 
 delay_us(4);
 IIC_SCL=0;              //钳住I2C总线,准备发送或接收数据// 
}   
//编写IIC停止信号函数//
void IIC_Stop(void)
{
 SDA_OUT();              //SDA线设为输出//
 IIC_SCL=0;              //SCL输出低//
 IIC_SDA=0;              //SDA输出低//
 delay_us(4);
 IIC_SCL=1;              //SCL输出高//
 IIC_SDA=1;              //SDA输出高,发送I2C总线结束信号//
 delay_us(4);           
}
//编写IIC等待应答信号函数,返回值:1,接收应答失败;0,接收应答成功//
u8 IIC_Wait_Ack(void)
{
 u8 ucErrTime=0;
 SDA_IN();               //SDA设置为输入//  
 IIC_SDA=1;delay_us(1);  //SDA设高电平//   
 IIC_SCL=1;delay_us(1);  //SCL设高电平//
 while(READ_SDA)         //读取PB7的值,0或1//
 {
  ucErrTime++;
  if(ucErrTime>250)
  {
   IIC_Stop();
   return 1;              //失败//
  }
 }
 IIC_SCL=0;               //当PB7值为0,则SCL时钟输出低电平,0//     
 return 0;                //成功//
} 
//编写IIC产生ACK应答函数//
void IIC_Ack(void)
{
 IIC_SCL=0;
 SDA_OUT();
 IIC_SDA=0;
 delay_us(2);
 IIC_SCL=1;
 delay_us(2);
 IIC_SCL=0;
}
//编写IIC不产生ACK应答函数//      
void IIC_NAck(void)
{
 IIC_SCL=0;
 SDA_OUT();
 IIC_SDA=1;
 delay_us(2);
 IIC_SCL=1;
 delay_us(2);
 IIC_SCL=0;
}               
//编写IIC发送一个字节函数,1,有应答;0,无应答//     
void IIC_Send_Byte(u8 txd)
{                        
    u8 t;   
   SDA_OUT();      
    IIC_SCL=0;               //SCL为低电平时,可以改变SDA,以实现数据的有效传输//
    for(t=0;t<8;t++)
    {              
    //IIC_SDA=(txd&0x80)>>7;
  if((txd&0x80)>>7)         //(txd&0x80)>>7值要么是0要么是1,IIC是从最高位开始传输数据的//
   IIC_SDA=1;               //(txd&0x80)>>7值为1时,SDA给1//
  else
   IIC_SDA=0;               //(txd&0x80)>>7值为0时,SDA给0//
  txd<<=1;    
  delay_us(2);              //对TEA5767这三个延时都是必须的
  IIC_SCL=1;                //SCL为高电平时,传输SDA//
  delay_us(2); 
  IIC_SCL=0; 
  delay_us(2);
    }  
}      
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK   
u8 IIC_Read_Byte(unsigned char ack)
{
 unsigned char i,receive=0;     //无符号字符型,对应整数范围0~255//
 SDA_IN();//SDA设置为输入
    for(i=0;i<8;i++ )
 {
    IIC_SCL=0; 
    delay_us(2);
    IIC_SCL=1;                  //SCL由低设高后,开始读信号//
    receive<<=1;                //先将之前接收的信号左移,最右位补0//
    if(READ_SDA)receive++;      //如果接收SDA为1,则最右位改为1//
    delay_us(1);                //如果接收SDA为0,则不做操作//
    }      
    if (!ack)
        IIC_NAck();             //发送nACK
    else
        IIC_Ack();              //发送ACK   
    return receive;
}

5.3 main.c文件代码解读

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"
#include "24cxx.h"  
//要写入到24c02的字符串数组
const u8 TEXT_Buffer[]={"这是从24C02读取的数据"};
#define SIZE sizeof(TEXT_Buffer)  
int main(void)
 {  
  u8 key;
  u16 i=0;
  u8 datatemp[SIZE];
  delay_init();           //延时函数初始化//   
  uart_init(115200);      //串口初始化为115200//
  LED_Init();             //初始化与LED连接的硬件接口//
  KEY_Init();       
  AT24CXX_Init();         //IIC初始化// 
  while(AT24CXX_Check())  //检测24C02//
 {
  printf("检测不到值\n");                 
  LED0=!LED0;             //DS0闪烁//
  LED1=!LED1;             //DS1闪烁//
  delay_ms(100);
 }
 while(1)
 {
  key=KEY_Scan(0);
  if(key==KEY1_PRES)     //KEY_UP按下,写入24C02数据//
   {
    AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
    printf("向24C02写入数据\n");
   }
  if(key==KEY0_PRES)     //KEY0按下,读取字符串并显示
   {
    AT24CXX_Read(0,datatemp,SIZE);
    printf("从24C02读取数据,内容为:%s\n",datatemp);
   }
  i++;
  delay_ms(10);
  if(i==20)
  {
   LED0=!LED0;           //提示系统正在运行 
   i=0;
  }     
 }
}

6. 实验结果
实验结果如下图所示:
在这里插入图片描述
旧知识点
1)复习如何新建工程模板,可参考STM32学习心得二:新建工程模板
2)复习基于库函数的初始化函数的一般格式,可参考STM32学习心得三:GPIO实验-基于库函数
3)复习寄存器地址,可参考STM32学习心得四:GPIO实验-基于寄存器
4)复习位操作,可参考STM32学习心得五:GPIO实验-基于位操作
5)复习寄存器地址名称映射,可参考STM32学习心得六:相关C语言学习及寄存器地址名称映射解读
6)复习时钟系统框图,可参考STM32学习心得七:STM32时钟系统框图解读及相关函数
7)复习延迟函数,可参考STM32学习心得九:Systick滴答定时器和延时函数解读
8)复习ST-LINK仿真器的参数配置,可参考STM32学习心得十:在Keil MDK软件中配置ST-LINK仿真器
9)复习ST-LINK调试方法,可参考STM32学习心得十一:ST-LINK调试原理+软硬件仿真调试方法
10)复习串口通信相关知识,可参考STM32学习心得十四:串口通信相关知识及配置方法

猜你喜欢

转载自blog.csdn.net/Leisure_ksj/article/details/106575393