新闻  |   论坛  |   博客  |   在线研讨会
深度剖析单片机IO口模拟SPI接口C语言程序设计
machinnneee | 2017-08-22 06:11:39    阅读:33888   发布文章

      SPI的通信和IIC通信接口相似,都会两个模块间进行数据通信的一种方法。相比于IIC通信,SPI的通信原理很简单,一般主从方式工作,这种模式通常有一个主设备和一个或多个从设备,通常采用的是4根线,它们是MISO(数据输入,针对主机来说)、MOSI(数据输出,针对主机来说)、SCLK(时钟,主机产生)、CS/SS(片选,一般由主机发送或者直接使能,通常为低电平有效),典型的硬件连接如下:

0.jpg

        在SPI通信中,时钟作为一项重要的参数,必须保证主设备和从设备必须在相同的时序下工作,因为SCK的时钟频率决定了整个SPI总线的传输速度,所以只有主从双方步调一致才能进行数据的稳定传输。在用MCU作为主设备时,一般可通过对SPI控制寄存器编程.来选择不同的时钟频率,如果采用单片机IO口模拟SPI时序,那么就需要严格按照从机的数据传输格式进行模拟。  

      CS/SS是片选信号,若为低电平有效,只有控制该管脚为低电平从机才能实现使能,利用单片机的IO口可控制总线上连接的多个SPI设备。
      在SPI数据传输中,数据在时钟sclk的作用下按照bit进行传输的。这就是SCLK时钟线存在的原因,因此,完成一个字节(8bit)的数据传输,至少8次 时钟信号的改变(上沿和下沿为一次,依据不同的从机传送协议),就可以完成8位数据的传输。
     在SPI操作中,最重要的两项设置就是时钟极性(CPOL或UCCKPL)和时钟相位(CPHA或UCCKPH)。为了保证主从机正确通信,应使得它们的SPI具有相同的时钟极性和时钟相位。
时钟极性(CPOL)定义了时钟空闲状态的电平,对数据传输的没有太大的影响。
时钟相位(CPHA)定义了数据的传送时间和传送格式。在程序编写时需要注意相位和数据的关系。下图形象的说明了时钟极性和数据传输的关系。

11.jpg

    下面以STM32单片机为主机,SPI存储器作为从机进行程序的解释:

    首先创建GPIO结构体:

 GPIO_InitTypeDef GPIO_InitStructure;
 
 因为SPI有MOSI MISO之分,所需的函数设置如下:
void MOSI_H(void)
{
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;//MOSI 设置1
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  GPIO_SetBits(GPIOE, GPIO_Pin_12);
}
void MOSI_L(void)
{
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;//MOSI 设置0
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  GPIO_ResetBits(GPIOE, GPIO_Pin_12);
}
void SCLK_H(void)
{
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;//scl 设置1
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  GPIO_SetBits(GPIOE, GPIO_Pin_13);
}

void SCLK_L(void)
{
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;//MOSI 设置 0
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
  GPIO_ResetBits(GPIOE, GPIO_Pin_13);
}


    针对SPI的初始化操作,具体如下:
void SPI_Init_IO(void)
{
  GPIO_InitTypeDef GPIO_InitStructure;
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE);

  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;//CS 
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOE, &GPIO_InitStructure);
    GPIO_ResetBits(GPIOE,GPIO_Pin_13);   //高电平,未片选

  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_10;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;   //MISO
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOE, &GPIO_InitStructure);
 
    
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//MOSI
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOE, &GPIO_InitStructure);    
    GPIO_ResetBits(GPIOE,GPIO_Pin_12);
}   


    正如上面描述的,SPI是以字节的格式进行数据传输,因此需要8次循环完成8bit的数据传输。
void SpiFlash_WriteOneByte(u8 byte)
{        
        uint8_t BitCount = 0;  
    SCLK_L();          //clk低电平 存入数据,上升沿读入数据  
    for(BitCount = 0;BitCount < 8; BitCount++)  
    {  
        SCLK_L();          //clk低电平 存入数据,上升沿写入数据
        if(byte & 0x80)  
        {  
            MOSI_H();          //MOSI为1,输写入1
        }  
        else  
        {  
            MOSI_L();  
        }  
        byte <<= 1;  
        SCLK_H();            //时钟高电平,写入数据
    }  
    SCLK_L();  
    MOSI_H();                //写入完成,MOSI保持高电平
}                    
 

    读操作也与写操作相似,但是需要将读取到的数据返回,因此需要return1字节的数据。
u8 SpiFlash_ReadOneByte(void)  
{  
    uint8_t BitCount = 0;  
    uint8_t retValue = 0;  
    SCLK_H();            //clk高电平 存入数据,下降读出数据
 
    for(BitCount = 0;BitCount < 8; BitCount++)  
    {  
        retValue <<= 1;  
        SCLK_H();            //clk高电平
        if(GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_10))  
        {  
            retValue |= 0x01;  
        }  
        else  
        {  
            retValue &= 0xFE;  
        }  
        SCLK_L();            //clk高电平// cs = 0 选中芯片
    }  
    SCLK_H();  
    return (retValue);  
}  

 

     上面的子函数为SPI所需的关键部分,将这些函数嵌入到主函数相关地方可实现SPI数据的读写操作。
      正如我们所了解的,SPI具有接口简单,硬件容易实现;时钟速度快,没有系统开销;
相对抗干扰能力强;支持全双工操作等优点。但是,缺乏流控制机制,传输双方消息不确认,需要在软件上进行数据处理;没有多主机模式,如果需要设置为多主机,需要在软件或者外部逻辑上进行设计;需要占用主机较多的口线(每个从机都需要一根片选线)这些方面也需要注意。

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
倚楼等红颜  2017-11-26 09:10:38 

大神,学习了

推荐文章
最近访客