SPI通信

0. 前言

通讯速率较高,适用于ADC、LCD等设备与MCU间通信

1. SPI基础知识

物理层

  • SCL:时钟信号线

  • CS:片选信号线/从设备选择信号线,低电平有效

  • MOSI:主设备输出/从设备输入引脚

  • MISO:主设备输入/从设备输出引脚

通信过程

  • MOSI和MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出同时进行

  • 在SCK 上升沿 时触发,在 下降沿 时采样,即

    在SCK 上升沿 期间,MOSI和MISO的数据变化输出

    在SCK 下降沿 期间,MOSI和MISO的数据有效

  • 一般在SPI通讯时序中选择MSB先行模式

  • SPI每次传输数据以8位或16位为单位

  • 在双线全双工模式下,MOSI与MISO数据传输是同步的

2. SPI应用-stm32

SPI外设

  • 可作为主机或从机
  • 支持最高的SCK时钟频率为fpclk/2
  • 支持SPI协议的4种模式
  • 数据帧长度可设置为8位或16位
  • 可设置MSB先行或LSB先行

通讯模式

SPI模式 CPOL CPHA 空闲时SCK时钟 采样时刻
0 0 0 低电平 奇数边沿
1 0 1 低电平 偶数边沿
2 1 0 高电平 奇数边沿
3 1 1 高电平 偶数边沿

判断SCL信号空闲状态时的电平

  • CPOL=0时,SCL=0
  • CPOL=1时,SCL=1

判断数据采样和触发(转换)

  • CPHA=0时,数据在奇数边沿采样,数据在偶数边沿转换
  • CPHA=1时,数据在偶数边沿采样,数据在奇数边沿转换

可以通过上面判断通讯模式,下面这个是CPOL=0、CPHA=1的SPI通信

在模拟时序中,采样不一定在边沿,但是边沿时刻,MOSI状态要维持好,MISO在边沿之后获取引脚状态就行

Code BLock

1. 结构体

typedef struct
{
    uint16_t SPI_Direction;		//设置SPI的单双向模式
    uint16_t SPI_Mode;			//设置SPI的主/从机端模式
    uint16_t SPI_DataSize;		//设置SPI的数据帧长度
    uint16_t SPI_CPOL;			//设置时钟极性
    uint16_t SPI_CPHA;			//设置时钟相位
    uint16_t SPI_NSS;			//设置NSS引脚由硬件还是软件控制

    uint16_t SPI_BaudRatePrescaler;     //设置时钟分频因子,fpclk/分频数=fsck
    uint16_t SPI_FirstBit;		//设置MSB/LSB先行
    uint16_t SPI_CRCPolynomial;	        //设置CRC检验表达式
}SPI_InitTypeDef;

flash芯片

页写入

使用页写入命令最多可以一次向FLASH传输256个字节的数据,我们把这个单位为页大小

若发送数据超出256个,则会覆盖前面发送的数据

不定量数据写入

在页写入时,常常要写入不定量的数据,代码如下

/**
 * @brief	对flash写入数据,调用本函数写入数据前需要先擦除扇区
 * @param	pBuffer,要写入数据的指针
 		WriteAddr,写入地址
 		NumByteToWrite,写入数据长度
 * @retval	none
 */

u8 W25QXX_BUFFER[4096];	
void SPI_FLASH_BufferWrite(u8 *pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
    u32 secpos;
    u16 secoff;
    u16 secremain;
    u16 i;
    u8 *W25QXX_BUF;
    
    W25QXX_BUF = W25QXX_BUFFER;
    secpos = WriteAddr/4096;	//扇区地址
    secoff = WriteAddr%4096;	//在扇区内的偏移
    secremain = 4096-secoff;	//扇区剩余空间大小
    
    if(NumByteToWrite <= secremain)
        secremain = NumByteToWrite;		    //不大于4096个字节
    
    while(1)
    {
        W25QXX_Read(W25QXX_BUF,secpos*4096,4096);  //读取整个扇区的内容
        for(i=0;i<secremain;i++)	           //校验数据,是否需要擦除
        {
            if(W25QXX_BUF[secoff+i] != 0xFF)
                break;				   //需要擦除
        }
        //需要擦除
        if(i<secremain)
        {
            SPI_FLASH_SectorErase(secpos*4096);
            #if 1
            for(i=0;i<secremain;i++)
            {
                W25QXX_BUF[i+secoff] = pBuffer[i];
	    }
            //写入整个扇区,从扇区起始地址写
            //前secoff数据不用考虑,W25QXX_BUF[i+secoff] = pBuffer[i];
            W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);
            #else
            //也可以用这个
            W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
            #endif
        }
        //可以直接写入扇区剩余区间
        else
            W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);
        
        //写入结束了
        if(NumByteToWrite == secremain)
            break;
        //写入未结束
        else
        {
            secpos++;				//扇区地址增1
            secoff = 0;				//偏移位置为0
            
            pBuffer += secremain;		//指针偏移
            WriteAddr += secremain;		//写地址偏移
            NumByteToWrite -= secremain;	//字节数递减
            //下一个扇区仍写不完
            if(NumByteToWrite>4096)
                secremain = 4096;
            //下一个扇区可以写完
            else
                secremain = NumByteToWrite;
	}
    }
}


//写入时分为两种情况
//1、如果待写入指定的长度数据,在第一个扇区都够写完
if(NumByteToWrite <= secremain)
        secremain = NumByteToWrite;

//2、如果第一个扇区写不完,则,第一个扇区,中间完整的扇区,最后一个扇区分别写入
//参考上面


//4KB = 2^12Byte,即4096个字节
posted @ 2022-08-09 19:14  伯宁君  阅读(475)  评论(1编辑  收藏  举报