芯游记之MCU:SPI+8位寄存器代码+踩坑注意
前情提要:二师兄忘记拉高害的USART苦苦等待暂且不提,师徒四人来到了名为“埃斯皮埃”的大路上,这条大路可真是豪华呀,居然有4个车道,车水马龙的,可热闹了。
SPI简介
SPI最初由摩托罗拉公司在20世纪80年代中期开发,并很快发展成为一种行业标准。 SPI的设计初衷是为了提供一种简单、高效的通信方式,用于连接微控制器和各种外设,如闪存、EEPROM、SD卡和液晶显示器等。 SPI接口以其独特的特点在电子设备中得到了广泛应用。 它采用主从模式,通常有一个主设备(Master)和一个或多个从设备(Slave)。
SPI的优点:速度快,自带时钟不需要恢复,支持双工。
SPI结构
RXFIFO是32位,TXFIFO是24位。

SPI通过4个专用引脚与外部器件通讯。
1、MISO:主输入/从输出数据引脚。通常情况下,该引脚在从模式下发送数据,在主模式 下接收数据。
2、MOSI:主输出/从输入数据引脚。通常情况下,该引脚在主模式下发送数据,在从模式 下接收数据。
3、SCK:该引脚在主模式下发送数据,在从模式下接收数据。
4、NSS/CS:从器件选择引脚。根据 SPI 和 NSS 设置,该引脚可用于:
– 选择单个从器件进行通信
– 同步数据帧
– 检测多个主器件之间是否存在冲突
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)
{
}
}
作者:翻江倒海金箍棒