三种通信总线的前世今生对比

  • 时间:
  • 来源:互联网
  • 文章标签:

                                                  人生中的第一篇偏技术的文章    

     这两年主要从事嵌入式应用层的开发,对驱动层了解的不是很透彻,第一篇文章也不知道该写点神魔,那就从几个常用的通信总线开始吧,一方面算是给自己的学习经历添点痕迹(也算是重新学习的过程),二是以后回顾知识点也有个熟悉的资料,方便查阅。以后的内容有自己的想法(可能不对,还请指正,我尽量多查看一下书籍,争取总结正确),也会引用别人的一些见解,我尽量标明出处(链接),防止我断章取义,好让同志们(也可能就我自己看而已,假设有别人浏览吧)查找内容的源头。不BB了,是骡子是马拉出来遛遛吧。

STM32中几种通信方式的区别:

1.UART(USART)

2.IIC总线

3.SPI总线

 

主要参考博客:https://blog.csdn.net/oqqHuTu12345678/article/details/65445338?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase

对博主表示感谢。

UART、SPI、I2C对比

UART、SPI、I2C对比表格
对比项          UART                      SPI             I2C
信号线数目 3根,RX、TX、GND 4根,SDO、SDI、SCLK、SS 2根,SDA、SCLK
设备从属关系            —— 存在主从设备。SPI用片选信号选择从机  存在主从设备。IIC用地址选择从机。
通信方式 全双工通信 全双工通信 半双工通信
通信速率 速度慢 比I2C总线要快,速度可达到几Mbps I2C的速度比SPI慢
应用领域

1、UART常用于控制计算机与串行设备的芯片

2、就是我们经常所说的串口,基本都用于调试。

主要应用在EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间 I2C一般是用在同一个板子上的2个IC之间的通信 ,它可以替代标准的并行总线,连接各种集成电路和功能模块。
传输距离     I2C需要有双向IO的支持,而且使用上拉电阻,抗干扰能力较弱,一般用于同一板卡上芯片之间的通信,较少用于远距离通信
通信特征 异步,一帧可以传5/6/7/8位 同步,SPI允许数据一位一位的传送,甚至允许暂停。从最高位开始传。 同步,电平信号,一次连续8bit。从最高位开始传
协议复杂度 结构比较复杂 SPI实现要比UART简单,UART需要固定的波特率,就是说两位数据的间隔要相等,而SPI则无所谓,因为它是有时钟的协议。 协议比SPI复杂,但是连线比标准的SPI要少
对比  

在点对点的通信中,SPI接口不需要进行寻址操作,且为全双工通信,显得简单高效。

在多个从器件的系统中,每个从器件需要独立的使能信号,硬件上比I2C系统要稍微复杂一些。

 

USART简介:通用同步异步收发器,全双工数据交换,UART,异步通信,为常用外设,用作开发调试。

代码如下:

 /**
  * @brief  USART GPIO 配置,工作参数配置
  * @param  无
  * @retval 无
  */
void USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;

    // 打开串口GPIO的时钟
    DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
    
    // 打开串口外设的时钟
    DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);

    // 将USART Tx的GPIO配置为推挽复用模式
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);

  // 将USART Rx的GPIO配置为浮空输入模式
    GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
    
    // 配置串口的工作参数
    // 配置波特率
    USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
    // 配置 针数据字长
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    // 配置停止位
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    // 配置校验位
    USART_InitStructure.USART_Parity = USART_Parity_No ;
    // 配置硬件流控制
    USART_InitStructure.USART_HardwareFlowControl =
    USART_HardwareFlowControl_None;
    // 配置工作模式,收发一起
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    // 完成串口的初始化配置
    USART_Init(DEBUG_USARTx, &USART_InitStructure);
    
    // 串口中断优先级配置
    NVIC_Configuration();
    
    // 使能串口接收中断
    USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);    
    
    // 使能串口
    USART_Cmd(DEBUG_USARTx, ENABLE);        
}

/**
  * @brief  配置嵌套向量中断控制器NVIC
  * @param  无
  * @retval 无
  */
static void NVIC_Configuration(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;
 
  /* 嵌套向量中断控制器组选择 */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
 
  /* 配置USART为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = DEBUG_USART_IRQ;
  /* 抢断优先级*/
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
  /* 子优先级 */
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
  /* 使能中断 */
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  /* 初始化配置NVIC */
  NVIC_Init(&NVIC_InitStructure);
}

// 串口中断服务函数
void DEBUG_USART_IRQHandler(void)
{
  uint8_t ucTemp;
    if(USART_GetITStatus(DEBUG_USARTx,USART_IT_RXNE)!=RESET)
    {        
        buffer[num] = USART_ReceiveData(DEBUG_USARTx);
   // USART_SendData(DEBUG_USARTx,buffer[num]);
        num ++;
        
        if(num==10)
        {
            for(num=0;num<10;num++)
            {
                Delay(20);//在发送两个数据的间隙需要一定的时间。
                Usart_SendByte(DEBUG_USARTx,buffer[num]);
                Delay(20);
            }
            num = 0;
        }
    }    
}

uint16_t USART_ReceiveData(USART_TypeDef* USARTx)//串口接收数据函数,直接读取的USART的数据寄存器。
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
 
  /* Receive Data */
  return (uint16_t)(USARTx->DR & (uint16_t)0x01FF);
}

/*****************  发送一个字节 **********************/
void Usart_SendByte( USART_TypeDef * pUSARTx, uint8_t ch)
{
    /* 发送一个字节数据到USART */
    USART_SendData(pUSARTx,ch);
        
    /* 等待发送数据寄存器为空 */
    while (USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);    
}

void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data));
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF);
}

串口也就这些东西,自从有了固件库,开发就变得很容易了,只要懂得原理,代码不需要写太多,只要将一些问题想全面,考虑上一些特殊的情况,软件上做一些保护尽量避免发生问题。

SPI简介:高速全双工的串行总线。

(1)四条信号线:串行时钟(SCLK)、串行数据输出(SDO)、串行数据输入(SDI)、片选线(SS)。(所谓的进出,是针对信号进出主机而言)

(2)SPI总线可以实现多SPI设备互相连接。提供时钟的SPI设备为主设备(Master),其他设备为从设备(Slave)。SCLK信号线只由主设备控制,从设备不能控制信号线。

(3)在SPI总线上,某一时刻可以出现多个从机,但只能存在一个主机。主机通过片选线来确定要通信的从机。这就要求从机的MISO口具有三态特性,使得该口线在器件未被选通时表现为高阻抗。

(4)主从设备间可以实现全双工通信,收发独立,操作简单,数据传输速率较高,但需要占用主机较多的口线(每个从机都需要一根片选线),而且只支持单个主机,没有指定的流控制,有应答机制确认是否接收到数据

(5)数据输出通过SDO线,数据在时钟上沿或下沿时改变(即发送),在紧接着的下沿或上沿被读取,从而完成一位数据传输。数据输入也使用同样原理。因此,8位数据的传输,至少需要8次时钟信号的改变(上沿和下沿为一次)。

(6)普通的串行通讯一次连续传送至少8位数据,而SPI允许数据一位一位的传送,甚至允许暂停,因为SCK时钟线由主控设备控制,当没有时钟跳变时,从设备不采集或传送数据。也就是说,主设备通过对SCK时钟线的控制可以完成对通讯的控制。

(7)SPI接口在CPU和外围低速器件之间进行同步串行数据传输,在主器件的移位脉冲下,数据按位传输,高位在前(先传?),低位在后,为全双工通信。

2、数据传输

(1)SPI在数据传输的时候,需要确定两件事情:

  • 其一,数据是在时钟的上升沿采集还是下降沿采集;
  • 其二,时钟的初始(空闲)状态是为高电平还是低电平。
  • 对比:I2C空闲状态时,时钟线(不是数据线?)为高电平,数据采集时,时钟线也为高电平,但SPI给出了更自由的方式。

(2)两个概念

CPOL:时钟极性,表示SPI在空闲时,时钟信号是高电平还是低电平。

CPHA:时钟相位,表示SPI设备是在在时钟的上升沿采集还是下降沿采集。

则SPI数据传输就有四种可能---按照标准的说法,SPI数据传输就有四种模式

(3)四种模式

模式 CPOL CPHA
0 0 0
1 0 1
2 1 0
3 1 1

模式0(杠cs表示片选信号)

模式1

模式2

模式3

代码如下:

/**
  * @brief  SPI_FLASH初始化
  * @param  无
  * @retval 无
  */
void SPI_FLASH_Init(void)
{
  SPI_InitTypeDef  SPI_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;
    
    /* 使能SPI时钟 */
    FLASH_SPI_APBxClock_FUN ( FLASH_SPI_CLK, ENABLE );
    
    /* 使能SPI引脚相关的时钟 */
     FLASH_SPI_CS_APBxClock_FUN ( FLASH_SPI_CS_CLK|FLASH_SPI_SCK_CLK|
                                                                    FLASH_SPI_MISO_PIN|FLASH_SPI_MOSI_PIN, ENABLE );
    
  /* 配置SPI的 CS引脚,普通IO即可 */
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_CS_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_Init(FLASH_SPI_CS_PORT, &GPIO_InitStructure);
    
  /* 配置SPI的 SCK引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_SCK_PIN;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
  GPIO_Init(FLASH_SPI_SCK_PORT, &GPIO_InitStructure);

  /* 配置SPI的 MISO引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MISO_PIN;
  GPIO_Init(FLASH_SPI_MISO_PORT, &GPIO_InitStructure);

  /* 配置SPI的 MOSI引脚*/
  GPIO_InitStructure.GPIO_Pin = FLASH_SPI_MOSI_PIN;
  GPIO_Init(FLASH_SPI_MOSI_PORT, &GPIO_InitStructure);

  /* 停止信号 FLASH: CS引脚高电平*/
  SPI_FLASH_CS_HIGH();

  /* SPI 模式配置 */
  // FLASH芯片 支持SPI模式0及模式3,据此设置CPOL CPHA
  SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
  SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
  SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
  SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;
  SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;
  SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
  SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
  SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
  SPI_InitStructure.SPI_CRCPolynomial = 7;
  SPI_Init(FLASH_SPIx , &SPI_InitStructure);

  /* 使能 SPI  */
  SPI_Cmd(FLASH_SPIx , ENABLE);
    
}

/* 获取 SPI Flash ID */
    FlashID = SPI_FLASH_ReadID();   

函数主体:

 /**
  * @brief  读取FLASH ID
  * @param     无
  * @retval FLASH ID
  */

#define Dummy_Byte                0xFF

#define W25X_JedecDeviceID            0x9F
u32 SPI_FLASH_ReadID(void)
{
  u32 Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;

  /* 开始通讯:CS低电平 */
  SPI_FLASH_CS_LOW();

  /* 发送JEDEC指令,读取ID */
  SPI_FLASH_SendByte(W25X_JedecDeviceID);

  /* 读取一个字节数据 */
  Temp0 = SPI_FLASH_SendByte(Dummy_Byte);

  /* 读取一个字节数据 */
  Temp1 = SPI_FLASH_SendByte(Dummy_Byte);

  /* 读取一个字节数据 */
  Temp2 = SPI_FLASH_SendByte(Dummy_Byte);

 /* 停止通讯:CS高电平 */
  SPI_FLASH_CS_HIGH();

  /*把数据组合起来,作为函数的返回值*/
    Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;

  return Temp;
}

/**
  * @brief  使用SPI发送一个字节的数据
  * @param  byte:要发送的数据
  * @retval 返回接收到的数据
  */
u8 SPI_FLASH_SendByte(u8 byte)
{
     SPITimeout = SPIT_FLAG_TIMEOUT;
  /* 等待发送缓冲区为空,TXE事件 */
  while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_TXE) == RESET)
    {
    if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
   }

  /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
  SPI_I2S_SendData(FLASH_SPIx , byte);

    SPITimeout = SPIT_FLAG_TIMEOUT;
  /* 等待接收缓冲区非空,RXNE事件 */
  while (SPI_I2S_GetFlagStatus(FLASH_SPIx , SPI_I2S_FLAG_RXNE) == RESET)
  {
    if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
   }

  /* 读取数据寄存器,获取接收缓冲区数据 */
  return SPI_I2S_ReceiveData(FLASH_SPIx );
}

这样就可以获取FLASH的ID了,通过ID的读取操作可以检测FLASH连接是否稳妥。

/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
        // 这里擦除4K,即一个扇区,擦除的最小单位是扇区
        SPI_FLASH_SectorErase(0x00);        

 /**
  * @brief  擦除FLASH扇区
  * @param  SectorAddr:要擦除的扇区地址
  * @retval 无
  */
void SPI_FLASH_SectorErase(u32 SectorAddr)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();
  SPI_FLASH_WaitForWriteEnd();
  /* 擦除扇区 */
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 发送扇区擦除指令*/
  SPI_FLASH_SendByte(W25X_SectorErase);
  /*发送擦除扇区地址的高位*/
  SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
  /* 发送擦除扇区地址的中位 */
  SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
  /* 发送擦除扇区地址的低位 */
  SPI_FLASH_SendByte(SectorAddr & 0xFF);
  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();
  /* 等待擦除完毕*/
  SPI_FLASH_WaitForWriteEnd();
}

/**
  * @brief  等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
  * @param  none
  * @retval none
  */
void SPI_FLASH_WaitForWriteEnd(void)
{
  u8 FLASH_Status = 0;

  /* 选择 FLASH: CS 低 */
  SPI_FLASH_CS_LOW();

  /* 发送 读状态寄存器 命令 */
  SPI_FLASH_SendByte(W25X_ReadStatusReg);

  /* 若FLASH忙碌,则等待 */
  do
  {
        /* 读取FLASH芯片的状态寄存器 */
    FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);    
  }
  while ((FLASH_Status & WIP_Flag) == SET);  /* 正在写入标志 */

  /* 停止信号  FLASH: CS 高 */
  SPI_FLASH_CS_HIGH();
}

 /**
  * @brief  对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
  * @param    pBuffer,要写入数据的指针
  * @param WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
  * @retval 无
  */
void SPI_FLASH_PageWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  /* 发送FLASH写使能命令 */
  SPI_FLASH_WriteEnable();

  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();
  /* 写页写指令*/
  SPI_FLASH_SendByte(W25X_PageProgram);
  /*发送写地址的高位*/
  SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
  /*发送写地址的中位*/
  SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
  /*发送写地址的低位*/
  SPI_FLASH_SendByte(WriteAddr & 0xFF);

  if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
  {
     NumByteToWrite = SPI_FLASH_PerWritePageSize;
     FLASH_ERROR("SPI_FLASH_PageWrite too large!");
  }

  /* 写入数据*/
  while (NumByteToWrite--)
  {
    /* 发送当前要写入的字节数据 */
    SPI_FLASH_SendByte(*pBuffer);
    /* 指向下一字节数据 */
    pBuffer++;
  }

  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();

  /* 等待写入完毕*/
  SPI_FLASH_WaitForWriteEnd();
}

 /**
  * @brief  对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
  * @param    pBuffer,要写入数据的指针
  * @param  WriteAddr,写入地址
  * @param  NumByteToWrite,写入数据长度
  * @retval 无
  */
void SPI_FLASH_BufferWrite(u8* pBuffer, u32 WriteAddr, u16 NumByteToWrite)
{
  u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
    
    /*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
  Addr = WriteAddr % SPI_FLASH_PageSize;
    
    /*差count个数据值,刚好可以对齐到页地址*/
  count = SPI_FLASH_PageSize - Addr;
    /*计算出要写多少整数页*/
  NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
    /*mod运算求余,计算出剩余不满一页的字节数*/
  NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
    
    /* Addr=0,则WriteAddr 刚好按页对齐 aligned  */
  if (Addr == 0)
  {
        /* NumByteToWrite < SPI_FLASH_PageSize */
    if (NumOfPage == 0)
    {
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    {
            /*先把整数页都写了*/
      while (NumOfPage--)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }
            /*若有多余的不满一页的数据,把它写完*/
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
    }
  }
    /* 若地址与 SPI_FLASH_PageSize 不对齐  */
  else
  {
        /* NumByteToWrite < SPI_FLASH_PageSize */
    if (NumOfPage == 0)
    {
            /*当前页剩余的count个位置比NumOfSingle小,一页写不完*/
      if (NumOfSingle > count)
      {
        temp = NumOfSingle - count;
                /*先写满当前页*/
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
                
        WriteAddr +=  count;
        pBuffer += count;
                /*再写剩余的数据*/
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
      }
      else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
      }
    }
    else /* NumByteToWrite > SPI_FLASH_PageSize */
    {
            /*地址不对齐多出的count分开处理,不加入这个运算*/
      NumByteToWrite -= count;
      NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
      NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
            
            /* 先写完count个数据,为的是让下一次要写的地址对齐 */
      SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
            
            /* 接下来就重复地址对齐的情况 */
      WriteAddr +=  count;
      pBuffer += count;
            /*把整数页都写了*/
      while (NumOfPage--)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
        WriteAddr +=  SPI_FLASH_PageSize;
        pBuffer += SPI_FLASH_PageSize;
      }
            /*若有多余的不满一页的数据,把它写完*/
      if (NumOfSingle != 0)
      {
        SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
      }
    }
  }
}

 /**
  * @brief  对FLASH读取数据,
  * @retval 无
  */

void SPI_FLASH_BufferRead(u8* pBuffer, u32 ReadAddr, u16 NumByteToRead)
{
  /* 选择FLASH: CS低电平 */
  SPI_FLASH_CS_LOW();

  /* 发送 读 指令 */
  SPI_FLASH_SendByte(W25X_ReadData);

  /* 发送 读 地址高位 */
  SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
  /* 发送 读 地址中位 */
  SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
  /* 发送 读 地址低位 */
  SPI_FLASH_SendByte(ReadAddr & 0xFF);
    
    /* 读取数据 */
  while (NumByteToRead--) /* while there is data to be read */
  {
    /* 读取一个字节*/
    *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
    /* 指向下一个字节缓冲区 */
    pBuffer++;
  }

  /* 停止信号 FLASH: CS 高电平 */
  SPI_FLASH_CS_HIGH();
}

读写FLASH就完成了,要是看着乱,那就仔细梳理吧,鼓捣累了,我就休息休息了,IIC明天继续。--------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------2020.06.08 23:23--------------------------------------------------------------------------------------

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------以下内容编辑于20.06.09 18:40----------------------------------------------------------------------

IIC简介:

1.总线配置:

(1)SCL(serial clock):时钟线,传输CLK信号,一般是I2C主设备向从设备提供时钟的通道。

(2)SDA(serial data):数据线,通信数据(命令、地址、数据)都通过SDA线传输。

2.通信特征:串行、同步、电平、低速率

(1)串行通信。所有的数据以bit为单位在SDA线上串行传输(但每次传输8bit)。

(2)同步通信。即通信双方工作在同一个时钟。A方通过一根CLK信号线传输A的时钟给B,B工作在A传输的时钟下。

(3)非差分。因为I2C通信速率不高,而且通信双方距离很近,所以使用电平信号通信。

(4)低速率。I2C一般用在同一个板子上的2个IC之间,传输的数据量不大,因此本身通信速率很低(一般几百KHz,不同的I2C芯片的通信速率可能不同,具体在编程的时候要看设备允许的I2C通信最高速率)。

特征:

(1)主设备+从设备

    分主设备和从设备。通信由主设备发起和主导,从设备只是按照I2C时序协议被动的接受主设备的通信。
    谁是主从设备,由通信双方来决定(I2C协议并无规定)。

(2)可以多个设备挂在一条总线上(从设备地址)

    主设备负责调度总线,决定某一时间和哪个从设备通信。
    同一时刻,只能有一个从设备和主设备通信,其他从设备处于“冬眠”状态。
    每个I2C从设备在通信中都有一个I2C从设备地址,共7个bit,广播地址全0。
    它是从设备本身固有的属性。通信时主设备需要知道从设备的地址,然后在通信中通过地址来甄别是不是自己要找的那个从设备。这个地址是一个电路板上唯一的,不是全球唯一的。
    系统中可能有多个同种芯片,为此addr分为固定部分和可编程部份,细节视芯片而定,看datasheet。
    理论上7位有128位地址,然而除去保留几个保留地址如广播地址0x00等,数量少于128个,且标准协议里预见了地址的局限性,扩充了10位地址的概念。

电平示意图:

3、数据在总线上的传输协议

 

主设备首先会发送7bit位的slave device地址,和1bit位的rean或者write命令。(这里的读写,讲的是主机对总线的操作。比如写,主机写数据到总线,那么从机是读的;比如读,主机读总线的数据,那么从机是写的。)

(1)write命令

如果为write命令,则主设备释放总线(If the I2C-bus is free, both SDA and SCL lines should be both at High level),即SDA为高位;然后从设备拉低SDA,表示ACK主设备;然后主设备再发送8bit数据,从设备再ACK(A),通信结束(P)。

写寄存器的标准流程:

1.    Master发起START

2.    Master发送I2C addr(7bit)和w操作0(1bit),等待ACK

3.    Slave发送ACK

4.    Master发送reg addr(8bit),等待ACK

5.    Slave发送ACK

6.   Master发送data(8bit),即要写入寄存器中的数据,等待ACK

7.    Slave发送ACK

8.    第6步和第7步可以重复多次,即顺序写多个寄存器

9.    Master发起STOP

(2)read命令

如果为read命令,则从设备先拉低SDA表示ACK主设备,然后再发送8bit数据。主设备拉低SDA表示ACK从设备(我已经读取8bit的数据了),之后结束。

读寄存器的标准流程

1、Master发送I2Caddr(7bit)和 W操作1(1bit),等待ACK

2.    Slave发送ACK

3.    Master发送reg addr(8bit),等待ACK

4.    Slave发送ACK

5.   Master发起START

6.    Master发送I2C addr(7bit)和 R操作1(1bit),等待ACK

7.    Slave发送ACK

8.   Slave发送data(8bit),即寄存器里的值

9.   Master发送ACK

10.    第8步和第9步可以重复多次,即顺序读多个寄存器
 

应用实例:

IIC常运用在读写EEPROM、与传感器进行通信。就拿读写EEPROM为例,整点源码放在这。据说硬件IIC是有问题的,一般都用电平的高低来模拟硬件IIC,达到通信的效果。

/*
*********************************************************************************************************
*    函 数 名: i2c_Start
*    功能说明: CPU发起I2C总线启动信号
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void i2c_Start(void)
{
    /* 当SCL高电平时,SDA出现一个下跳沿表示I2C总线启动信号 */
    EEPROM_I2C_SDA_1();
    EEPROM_I2C_SCL_1();
    i2c_Delay();
    EEPROM_I2C_SDA_0();
    i2c_Delay();
    EEPROM_I2C_SCL_0();
    i2c_Delay();
}

/*
*********************************************************************************************************
*    函 数 名: i2c_Start
*    功能说明: CPU发起I2C总线停止信号
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void i2c_Stop(void)
{
    /* 当SCL高电平时,SDA出现一个上跳沿表示I2C总线停止信号 */
    EEPROM_I2C_SDA_0();
    EEPROM_I2C_SCL_1();
    i2c_Delay();
    EEPROM_I2C_SDA_1();
}

/*
*********************************************************************************************************
*    函 数 名: ee_CheckOk
*    功能说明: 判断串行EERPOM是否正常
*    形    参:无
*    返 回 值: 1 表示正常, 0 表示不正常
*********************************************************************************************************
*/
uint8_t ee_CheckOk(void)
{
    if (i2c_CheckDevice(EEPROM_DEV_ADDR) == 0)
    {
        return 1;
    }
    else
    {
        /* 失败后,切记发送I2C总线停止信号 */
        i2c_Stop();        
        return 0;
    }
}

上电先检查一下EEPROM工作是否正常,确认无误后再进行通信。

/*
*********************************************************************************************************
*    函 数 名: i2c_CheckDevice
*    功能说明: 检测I2C总线设备,CPU向发送设备地址,然后读取设备应答来判断该设备是否存在
*    形    参:_Address:设备的I2C总线地址
*    返 回 值: 返回值 0 表示正确, 返回1表示未探测到
*********************************************************************************************************
*/
uint8_t i2c_CheckDevice(uint8_t _Address)
{
    uint8_t ucAck;

    i2c_CfgGpio();        /* 配置GPIO */
    i2c_Start();        /* 发送启动信号 */

    /* 发送设备地址+读写控制bit(0 = w, 1 = r) bit7 先传 */
    i2c_SendByte(_Address | EEPROM_I2C_WR);
    ucAck = i2c_WaitAck();    /* 检测设备的ACK应答 */

    i2c_Stop();            /* 发送停止信号 */

    return ucAck;
}

/*
*********************************************************************************************************
*    函 数 名: i2c_CfgGpio
*    功能说明: 配置I2C总线的GPIO,采用模拟IO的方式实现
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
static void i2c_CfgGpio(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(EEPROM_RCC_I2C_PORT, ENABLE);    /* 打开GPIO时钟 */

    GPIO_InitStructure.GPIO_Pin = EEPROM_I2C_SCL_PIN | EEPROM_I2C_SDA_PIN;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;      /* 开漏输出 */
    GPIO_Init(EEPROM_GPIO_PORT_I2C, &GPIO_InitStructure);

    /* 给一个停止信号, 复位I2C总线上的所有设备到待机模式 */
    i2c_Stop();
}

/*
*********************************************************************************************************
*    函 数 名: i2c_SendByte
*    功能说明: CPU向I2C总线设备发送8bit数据
*    形    参:_ucByte : 等待发送的字节
*    返 回 值: 无
*********************************************************************************************************
*/
void i2c_SendByte(uint8_t _ucByte)
{
    uint8_t i;

    /* 先发送字节的高位bit7 */
    for (i = 0; i < 8; i++)
    {        
        if (_ucByte & 0x80)
        {
            EEPROM_I2C_SDA_1();
        }
        else
        {
            EEPROM_I2C_SDA_0();
        }
        i2c_Delay();
        EEPROM_I2C_SCL_1();
        i2c_Delay();    
        EEPROM_I2C_SCL_0();
        if (i == 7)
        {
             EEPROM_I2C_SDA_1(); // 释放总线
        }
        _ucByte <<= 1;    /* 左移一个bit */
        i2c_Delay();
    }
}

/*
*********************************************************************************************************
*    函 数 名: ee_WriteBytes
*    功能说明: 向串行EEPROM指定地址写入若干数据,采用页写操作提高写入效率
*    形    参:_usAddress : 起始地址
*             _usSize : 数据长度,单位为字节
*             _pWriteBuf : 存放读到的数据的缓冲区指针
*    返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_WriteBytes(uint8_t *_pWriteBuf, uint16_t _usAddress, uint16_t _usSize)
{
    uint16_t i,m;
    uint16_t usAddr;
    
    /*
        写串行EEPROM不像读操作可以连续读取很多字节,每次写操作只能在同一个page。
        对于24xx02,page size = 8
        简单的处理方法为:按字节写操作模式,没写1个字节,都发送地址
        为了提高连续写的效率: 本函数采用page wirte操作。
    */

    usAddr = _usAddress;    
    for (i = 0; i < _usSize; i++)
    {
        /* 当发送第1个字节或是页面首地址时,需要重新发起启动信号和地址 */
        if ((i == 0) || (usAddr & (EEPROM_PAGE_SIZE - 1)) == 0)
        {
            /* 第0步:发停止信号,启动内部写操作 */
            i2c_Stop();
            
            /* 通过检查器件应答的方式,判断内部写操作是否完成, 一般小于 10ms             
                CLK频率为200KHz时,查询次数为30次左右
            */
            for (m = 0; m < 1000; m++)
            {                
                /* 第1步:发起I2C总线启动信号 */
                i2c_Start();
                
                /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
                i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);    /* 此处是写指令 */
                
                /* 第3步:发送一个时钟,判断器件是否正确应答 */
                if (i2c_WaitAck() == 0)
                {
                    break;
                }
            }
            if (m  == 1000)
            {
                goto cmd_fail;    /* EEPROM器件写超时 */
            }
        
            /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
            i2c_SendByte((uint8_t)usAddr);
            
            /* 第5步:等待ACK */
            if (i2c_WaitAck() != 0)
            {
                goto cmd_fail;    /* EEPROM器件无应答 */
            }
        }
    
        /* 第6步:开始写入数据 */
        i2c_SendByte(_pWriteBuf[i]);
    
        /* 第7步:发送ACK */
        if (i2c_WaitAck() != 0)
        {
            goto cmd_fail;    /* EEPROM器件无应答 */
        }

        usAddr++;    /* 地址增1 */        
    }
    
    /* 命令执行成功,发送I2C总线停止信号 */
    i2c_Stop();
    return 1;

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    i2c_Stop();
    return 0;
}

/*
*********************************************************************************************************
*    函 数 名: i2c_ReadByte
*    功能说明: CPU从I2C总线设备读取8bit数据
*    形    参:无
*    返 回 值: 读到的数据
*********************************************************************************************************
*/
uint8_t i2c_ReadByte(void)
{
    uint8_t i;
    uint8_t value;

    /* 读到第1个bit为数据的bit7 */
    value = 0;
    for (i = 0; i < 8; i++)
    {
        value <<= 1;
        EEPROM_I2C_SCL_1();
        i2c_Delay();
        if (EEPROM_I2C_SDA_READ())
        {
            value++;
        }
        EEPROM_I2C_SCL_0();
        i2c_Delay();
    }
    return value;
}

/*
*********************************************************************************************************
*    函 数 名: ee_ReadBytes
*    功能说明: 从串行EEPROM指定地址处开始读取若干数据
*    形    参:_usAddress : 起始地址
*             _usSize : 数据长度,单位为字节
*             _pReadBuf : 存放读到的数据的缓冲区指针
*    返 回 值: 0 表示失败,1表示成功
*********************************************************************************************************
*/
uint8_t ee_ReadBytes(uint8_t *_pReadBuf, uint16_t _usAddress, uint16_t _usSize)
{
    uint16_t i;
    
    /* 采用串行EEPROM随即读取指令序列,连续读取若干字节 */
    
    /* 第1步:发起I2C总线启动信号 */
    i2c_Start();
    
    /* 第2步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_WR);    /* 此处是写指令 */
    
    /* 第3步:等待ACK */
    if (i2c_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }

    /* 第4步:发送字节地址,24C02只有256字节,因此1个字节就够了,如果是24C04以上,那么此处需要连发多个地址 */
    i2c_SendByte((uint8_t)_usAddress);
    
    /* 第5步:等待ACK */
    if (i2c_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }
    
    /* 第6步:重新启动I2C总线。前面的代码的目的向EEPROM传送地址,下面开始读取数据 */
    i2c_Start();
    
    /* 第7步:发起控制字节,高7bit是地址,bit0是读写控制位,0表示写,1表示读 */
    i2c_SendByte(EEPROM_DEV_ADDR | EEPROM_I2C_RD);    /* 此处是读指令 */
    
    /* 第8步:发送ACK */
    if (i2c_WaitAck() != 0)
    {
        goto cmd_fail;    /* EEPROM器件无应答 */
    }    
    
    /* 第9步:循环读取数据 */
    for (i = 0; i < _usSize; i++)
    {
        _pReadBuf[i] = i2c_ReadByte();    /* 读1个字节 */
        
        /* 每读完1个字节后,需要发送Ack, 最后一个字节不需要Ack,发Nack */
        if (i != _usSize - 1)
        {
            i2c_Ack();    /* 中间字节读完后,CPU产生ACK信号(驱动SDA = 0) */
        }
        else
        {
            i2c_NAck();    /* 最后1个字节读完后,CPU产生NACK信号(驱动SDA = 1) */
        }
    }
    /* 发送I2C总线停止信号 */
    i2c_Stop();
    return 1;    /* 执行成功 */

cmd_fail: /* 命令执行失败后,切记发送停止信号,避免影响I2C总线上其他设备 */
    /* 发送I2C总线停止信号 */
    i2c_Stop();
    return 0;
}

这样就是一个完整的读写EEPROM的过程了,看懂了整个过程,拿来用就行了,本篇文章到此结束。敬礼!

 

 

 

 

 

 

 

 

本文链接http://www.taodudu.cc/news/show-83196.html