芯游记之MCU:SPI+8位寄存器代码+踩坑注意

前情提要:二师兄忘记拉高害的USART苦苦等待暂且不提,师徒四人来到了名为“埃斯皮埃”的大路上,这条大路可真是豪华呀,居然有4个车道,车水马龙的,可热闹了。

SPI简介

SPI最初由摩托罗拉公司在20世纪80年代中期开发,并很快发展成为一种行业标准。 SPI的设计初衷是为了提供一种简单、高效的通信方式,用于连接微控制器和各种外设,如闪存、EEPROM、SD卡和液晶显示器等。 SPI接口以其独特的特点在电子设备中得到了广泛应用。 它采用主从模式,通常有一个主设备(Master)和一个或多个从设备(Slave)。

SPI的优点:速度快,自带时钟不需要恢复,支持双工。

SPI结构

RXFIFO是32位,TXFIFO是24位。

ST官方提供的SPI结构

SPI通过4个专用引脚与外部器件通讯。

1、MISO:主输入/从输出数据引脚。通常情况下,该引脚在从模式下发送数据,在主模式 下接收数据。

2、MOSI:主输出/从输入数据引脚。通常情况下,该引脚在主模式下发送数据,在从模式 下接收数据。

3、SCK:该引脚在主模式下发送数据,在从模式下接收数据。

4、NSS/CS:从器件选择引脚。根据 SPI 和 NSS 设置,该引脚可用于:

         – 选择单个从器件进行通信

         – 同步数据帧

         – 检测多个主器件之间是否存在冲突

SPI时序

SPI时序示意图

首先片选信号NSS拉低,告诉从机准备接受信号。然后在时钟沿发送数据,通过MOSI发一个就会同时从MISO收到一个。 一般设置为先发送最高有效位MSB,最后发送最低有效位LSB。最后再拉高NSS,结束通信。

转换成伪代码是:先拉低NSS。发送前先检查TXE是否空闲,如果空闲使用MOSI输数据。再检测RXNE,如果为1代表MISO收到数据读取数据即可。最后再拉高NSS,表明通讯完成。

SPI寄存器版配置

1、主从机选择

主机我用的STMG071RBTx,CortexM0的,从机用的SPI Flash,兆易的GD25Q16C

2、SPI配置

BIDIMODE、BIDIOE、RXONLY:非双工模式,本文不考虑

CRCEN、CRCNEXT、CRCL:CRC计算,本文不考虑,保持默认值即可。

SSM:1为软件模式,0为硬件模式

        SSI:软件模式下代替NSS作为片选

个人理解:硬件模式用NSS引脚来输出片选信号,需要单独的GPIO引脚。而软件模式通过SSI调整片选信号,一般用作主从一对一,可以省下外部的NSS引脚去干别的事情。

LSBFIRST:0为MSB先,1为LSB先,一般都是0.

SPE:使能SPI,配置完寄存器再使能。

BR[2:0]:波特率,根据挂载的外设总线时钟分频,0-7分别代表2^1-2^8。

MSRT:1为主机,0为从机

CPOL/CPHA:时钟极性,推荐0模式or3模式

个人理解:

CPOL=0,时钟空闲idle时候的电平是低电平,active-high

CPOL=1,时钟空闲idle时候的电平是高电平,active-low

CPHA=0,数据采样是在第一个边沿

CPHA=0,数据采样是在第二个边沿

LDMA_TX/RX、TX/RXDMAEN:DMA有关,暂时不管

FRXTH:RXFIFO产生RXNE事件的阈值,1为1/4(8bit),0为1/2(16bit)

DS:数据位宽,最低4位,DS=7为8位数据,DS=15为16位数据。

TXEIE、RXNEIE、ERRIE:中断使能开关,暂时不管

FRF:帧格式,motorola或者ti模式,暂时不管

NSSP:在两个连续数据间发送脉冲,暂时不管

SSOE:置1使能NSS输出,不能在多主情况下工作。

本文使用硬件模式,阈值为8bit,数据位宽8位,波特率为Fpclk/8,CPOL=CPHA=1( 模式3),SSOE置1。

3、GPIO配置

SCK、MISO、MOSI:复用推挽输出+超高速。

NSS:通用输出,记得往BSRR寄存器里将输出电平置1。

硬件连接注意MISO连MISO,MOSI连MOSI,不需要串联!

小坑:GPIO选择与配置

可以看到AF0这里有很多个SCK/MOSI/MISO/NSS引脚,最开始我以为是PA4567,结果是PA1246。推荐大家先用CubeMX看一下一组SPI的引脚是哪几个。

NSS引脚一定要配成通用IO,不要配成复用。其他三个直接配复用输出,不用配成什么上拉输入。

4、代码实现

实现SPI FLASH的方法是发送特定的8位,他就会根据预设的表给你反馈。比如读、写等等。太详细的比如FLASH具体时序我就不讲了,我也只是拿来用。

#define FLASH_WriteEnable                0x06
#define FLASH_WriteDisable               0x04
#define FLASH_ReadStatusReg              0x05
#define FLASH_WriteStatusReg             0x01
#define FLASH_ReadData                   0x03
#define FLASH_FastReadData               0x0B
#define FLASH_FastReadDual               0x3B
#define FLASH_PageProgram                0x02
#define FLASH_BlockErase                 0xD8
#define FLASH_SectorErase                0x20
#define FLASH_ChipErase                  0xC7
#define FLASH_PowerDown                  0xB9
#define FLASH_ReleasePowerDown           0xAB
#define FLASH_DeviceID                   0xAB
#define FLASH_ManufactDeviceID           0x90
#define FLASH_JedecDeviceID              0x9F

#define Dummy_Byte                      0xFF
#define WIP_Flag                        0x01
#define RESET                           0x01
#define SET                             0x01

一些常用的指令都放在上面了,直接用变量就行了。

SPI发送byte

uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
    /* TXE */
    while ((SPI1->SR & (0x01<<1)) == 1);

    /* SEND DATA */
    *(__IO uint8_t *)&SPI1->DR = byte;

    /* RXNE */
    while ((SPI1->SR & 0x01) == 0);

    /* READ DAT */
    return (*(__IO uint8_t *)&SPI1->DR);
}
超级大坑:8位和16位

有的朋友会发现如上图所示的情况。本来应该是0x03+0x00+0x70+0x00,莫名其妙变成了0x3000+0x0000+0x7000+0x0000。而FLASH的时序则是在0x03后跟着24位地址,然后开始传回数据,很明显产生了错误。意味着本身8位的数据被莫名其妙的补全成16位。这是为什么呢?我搜遍了整个网络根本没有答案,虽然早在12年就有人提出这个问题,但是大部分人用的都是HAL因此根本0个人在意。后来我反过来去看了HAL库的8bit发送代码,终于找到了原因。

我们知道寄存器是16位的,那么在发送8位数据的时候,虽然我们设置成8位但DR寄存器不会变还是16位。这时候我们就要加上*(__IO uint8_t *)将寄存器强制只传输低8位。这样传出的数据就是正确的。

 SPI等待写动作结束

void SPI_FLASH_WaitForWriteEnd(void)
{
    uint8_t FLASH_Status = 0;

    /*  FLASH: CS  */
    SPI_FLASH_CS_LOW();

    /*    */
    SPI_FLASH_SendByte(FLASH_ReadStatusReg);

    /* FLASH, */
    do
    {
        /* FLASH */
        FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
    }
    while ((FLASH_Status & WIP_Flag) == SET);  /*  */

    /*   FLASH: CS  */
    SPI_FLASH_CS_HIGH();
}

发送0x05指令读取FLASH的状态寄存器,第一位WIP代表FLASH是否Busy(正在读或者写),不busy再进行下一步。

为什么要等待写动作结束?

其实flash的擦除和写动作都要很久,这边我只写了11个字符,从逻辑分析仪可以看出花了将近20ms。所以一定要等到写动作结束才行。

SPI写数据

void SPI_FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
    /* FLASH */
    SPI_FLASH_WriteEnable();

    /* FLASH: CS */
    SPI_FLASH_CS_LOW();
    /* */
    SPI_FLASH_SendByte(FLASH_PageProgram);
    /**/
    SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
    /**/
    SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
    /**/
    SPI_FLASH_SendByte(WriteAddr & 0xFF);

    /* */
    while (NumByteToWrite--)
    {
        /*  */
        SPI_FLASH_SendByte(*pBuffer);
        /*  */
        pBuffer++;
    }

   /*  FLASH: CS  */
    SPI_FLASH_CS_HIGH();
		
	/* */
    SPI_FLASH_WaitForWriteEnd();
}

 以写页操作为例,伪代码如下:

1、使能Write

2、拉低NSS电平

3、发送指令

4、发送24位地址

5、发送数据,根据传进来的参数

6、拉高NSS

7、等待写动作的结束

SPI总结

比较基础+简单的一个模块,配置也不复杂,搞清楚时序即可,网上代码基本都如出一辙,直接用就行了。就是遇到问题也很少见,因此不好解决。

附录:代码

spi.c

#include "../include/STM32G07x.h"
#include <stdint.h>
#include "spi.h"

/*FLASH*/
#define FLASH_WriteEnable                0x06
#define FLASH_WriteDisable               0x04
#define FLASH_ReadStatusReg              0x05
#define FLASH_WriteStatusReg             0x01
#define FLASH_ReadData                   0x03
#define FLASH_FastReadData               0x0B
#define FLASH_FastReadDual               0x3B
#define FLASH_PageProgram                0x02
#define FLASH_BlockErase                 0xD8
#define FLASH_SectorErase                0x20
#define FLASH_ChipErase                  0xC7
#define FLASH_PowerDown                  0xB9
#define FLASH_ReleasePowerDown           0xAB
#define FLASH_DeviceID                   0xAB
#define FLASH_ManufactDeviceID           0x90
#define FLASH_JedecDeviceID              0x9F

#define Dummy_Byte                      0xFF
#define WIP_Flag                        0x01
#define RESET                           0x01
#define SET                             0x01

void SPI_INIT(void)
{ 
	 RCC->IOPENR  |=  BIT0;
	 RCC->APBENR2 |=  BIT12;
	 GPIOA->MODER &=  ~(BIT5 + BIT3 + BIT4+ BIT2 + BIT9 + BIT8 + BIT13 + BIT12);
	 GPIOA->MODER |= (BIT5 + BIT3 + BIT8 + BIT13 );
	 //GPIOA->PUPDR |= (BIT12);
	 GPIOA->OSPEEDR |= (BIT2 + BIT3 + BIT4 + BIT5);
	GPIOA->BSRR |= (0x01 << 4);
	
	 SPI1->CR1 |= (BIT3 + BIT2 + BIT1 + BIT0);
	 SPI1->CR2 |= (BIT9 + BIT8 + BIT10 + BIT2 + BIT12);
	
	 SPI1->CR1 |= BIT6;
	  
	 	
}

void SPI_FLASH_CS_LOW(void)
{
	 GPIOA->BSRR |= (0x01 << 20);	
}

void SPI_FLASH_CS_HIGH(void)
{
	 GPIOA->BSRR |= (0x01 << 4);	
}

uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
    /* ,TXE */
    while ((SPI1->SR & (0x01<<1)) == 1);

    /* , */
    *(__IO uint8_t *)&SPI1->DR = byte;

    /* ,RXNE */
    while ((SPI1->SR & 0x01) == 0);

    /* , */
return (*(__IO uint8_t *)&SPI1->DR);
}

/**
* @brief  SPI
* @param  
* @retval 
*/
uint8_t SPI_FLASH_ReadByte(void)
{
    return (SPI_FLASH_SendByte(Dummy_Byte));
}

uint32_t SPI_FLASH_ReadID(void)
{
    uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;

    /* :CS */
    SPI_FLASH_CS_LOW();

    /* JEDEC,ID */
    SPI_FLASH_SendByte(FLASH_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;
}

void SPI_FLASH_WriteEnable(void)
{
    /* :CS */
    SPI_FLASH_CS_LOW();

    /* */
    SPI_FLASH_SendByte(FLASH_WriteEnable);

    /*:CS */
    SPI_FLASH_CS_HIGH();
}

void SPI_FLASH_WaitForWriteEnd(void)
{
    uint8_t FLASH_Status = 0;

    /*  FLASH: CS  */
    SPI_FLASH_CS_LOW();

    /*    */
    SPI_FLASH_SendByte(FLASH_ReadStatusReg);

    /* FLASH, */
    do
    {
        /* FLASH */
        FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
    }
    while ((FLASH_Status & WIP_Flag) == SET);  /*  */

    /*   FLASH: CS  */
    SPI_FLASH_CS_HIGH();
}

void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
    /* FLASH */
    SPI_FLASH_WriteEnable();

    /*  */
    /* FLASH: CS */
    SPI_FLASH_CS_LOW();
    /* */
    SPI_FLASH_SendByte(FLASH_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();
}

void SPI_FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
    /* FLASH */
    SPI_FLASH_WriteEnable();

    /* FLASH: CS */
    SPI_FLASH_CS_LOW();
    /* */
    SPI_FLASH_SendByte(FLASH_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   ReadAddr,
* @param   NumByteToRead,
* @retval 
*/
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
    /* FLASH: CS */
    SPI_FLASH_CS_LOW();

    /*    */
    SPI_FLASH_SendByte(FLASH_ReadData);

    /*    */
    SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
    /*    */
    SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
    /*    */
    SPI_FLASH_SendByte(ReadAddr & 0xFF);

    /*  */
    while (NumByteToRead--)
    {
        /* */
        *pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
        /*  */
        pBuffer++;
    }

    /*  FLASH: CS  */
    SPI_FLASH_CS_HIGH();
}

spi.h

#ifndef __spi_h
#define __spi_h
extern void SPI_INIT(void);

extern void SPI_FLASH_CS_LOW(void);

extern void SPI_FLASH_CS_HIGH(void);

extern uint8_t SPI_FLASH_SendByte(uint8_t byte);

extern uint8_t SPI_FLASH_ReadByte(void);

extern uint32_t SPI_FLASH_ReadID(void);

extern void SPI_FLASH_WriteEnable(void);

extern void SPI_FLASH_WaitForWriteEnd(void);

extern void SPI_FLASH_SectorErase(uint32_t SectorAddr);

extern void SPI_FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);

extern void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead);

#endif

main.c

// Simple SPI example
#include "../include/STM32G07x.h"
#include <stdint.h>
#include "serial.h"
#include "spi.h"
//#include "core_cm0plus.h"
#define  Tempaddr 0x000700

void delay(uint32_t dly)
{
    while(dly--);
}
void initClocks()
{
    // NOTE: this function assumes MCU has just come out of reset with the
    // registers defaulted to values as described in the reference manual.
    
// Use the HSI16 clock as the system clock - allows operation down to 1.5V
    RCC->CR &= ~(1 << 24); // PLL off
    // To get a 64MHz CPU core clock (R) need to run the VCO at 128MHz at least 
    // The minimum divisor for the R clock is 2
    
    FLASH->ACR |= (1 << 1); // 2 wait states for FLASH at 64MHz
    RCC->PLLSYSCFGR = (1 << 29) + (1 << 28); // select minimum R divisor of 2 and enable R output
    RCC->PLLSYSCFGR |= (1 << 12) + (1 << 4); // Divide HSI by 2 and multiply result by 16 to give 128MHz
    RCC->PLLSYSCFGR |= (1 << 1);  // clock source for PLL is HSI16
    RCC->PLLSYSCFGR |= (1 << 16); // enable PLL output
    RCC->CR |= (1 << 24); // PLL on.
    while ((RCC->CR & (1 << 25))==0); // wait for PLL to be ready
    // set PLL R output as CPU core clock source 
    RCC->CFGR |= (1 << 1);
    
}

void setup()
{
    
    initClocks();       
}

int main()
{
    char c;
	  uint8_t buffer[20];

    setup();  

	  SPI_INIT();

			  SPI_FLASH_ReadID();
			  SPI_FLASH_SectorErase(Tempaddr);
			  SPI_FLASH_BufferRead(buffer,Tempaddr,11);
			
			  SPI_FLASH_PageWrite((uint8_t *)"ABCDEFG",Tempaddr,11);
			  SPI_FLASH_BufferRead(buffer,Tempaddr,11);
			
				SPI_FLASH_SectorErase(Tempaddr);
			  SPI_FLASH_BufferRead(buffer,Tempaddr,11);

    while(1)
    {
    }
}

作者:翻江倒海金箍棒

物联沃分享整理
物联沃-IOTWORD物联网 » 芯游记之MCU:SPI+8位寄存器代码+踩坑注意

发表回复