stm32 spi协议_STM32库开发实战指南:基于STM32F4

stm32 spi协议_STM32库开发实战指南:基于STM32F4深入讲解SPI协议通信时序,详细解析SPI读写串行FALSH实验,NorFlash的存储特性以及读写指令的详细介绍!!!

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE稳定放心使用

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
?个人主页:@rivencode的个人主页
?系列专栏:玩转STM32
?推荐一款模拟面试、刷题神器,从基础到大厂面试题?点击跳转刷题网站进行注册学习

一.SPI协议简介

SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设备接口,允许芯片与外部设备以半/全双工、同步、串行方式通信。此接口可以被配置成主模式,并为外部从设备提供通信时钟(SCK)。接口还能以多主配置方式工作。它可用于多种用途,包括使用一条双向数据线的双线单工同步传输,还可使用CRC校验的可靠通信。
它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。

SPI一般采用双向全双工通信方式:

  • 全双工通信:在同一时刻,两个设备之间可以同时收发数据,全双工方式无需进行方向的切换,这种方式要求通讯双方均有发送器和接收器,同时,需要2根数据线。
    在这里插入图片描述

二.SPI物理层

SPI 通讯设备之间的常用连接方式:
在这里插入图片描述
所有主设备或从设备的信号线 SCK、MOSI 及 MISO 同时并联到相同的 SPI 总线上,都共同只使用这 3 条总线;

  • CS片选信号线

(Slave Select):从设备选择信号线,即为片选信号线,也称为 NSS、CS。当有多个 SPI 从设备与 SPI 主机相连时,而每个从设备都有独立的这一条NSS信号线,本信号线独占主机的一个引脚,即有多少个从设备,就有多少条片选信号线。当主机要选择从设备时,把该从设备的 NSS信号线设置为低电平,该从设备即被选中,主机开始与被选中的从设备进行 SPI 通讯,则其它未被选中(NSS引脚为高电平)的从设备会忽略总线上的数据传输
SPI 通讯以 NSS 线置低电平为开始信号,以 NSS 线被拉高作为结束信号。

  • SCK时钟信号线

SCK (Serial Clock):时钟信号线,用于通讯数据同步。只能由通讯主机产生决定了通讯的速率,不同的设备支持的最高时钟频率不一样,STM32 的 SPI 时钟频率最大为fpclk/2,两个设备之间通讯时,通讯速率受限于低速设备。

  • MOSI主机发送从机接收数据线

MOSI (Master Output, Slave Input):主设备输出/从设备输入引脚。
主机向从机发送数据,从机接收主机发送的数据

  • MOSI主机接收从机发送数据线
    MISO(Master Input,,Slave Output):主设备输入/从设备输出引脚。
    从机向主机发送数据,主机接收从机发送的数据

不用刻意去记看英文缩写就是什么意思

三.SPI协议层

1.通讯的起始和停止信号

NSS 是每个从机各自独占的信号线

  • 起始信号:NSS 信号线由高变低。,当从机在自己的 NSS 线检测到起始信号后,片选成功,开始准备与主机通讯
  • 停止信号:NSS 信号由低变高,是 SPI 通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

2.数据有效性

SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。MOSI及 MISO 数据线在 SCK 的每个时钟周期传输一位数据,且数据输入输出是同时进行的,也就是说我们发送一个数据也会同时接收一个数据 数据传输时,MSB 先行或 LSB 先行并没有作硬性规定,可以设置,但通信双向的模式要一致,一般采用MSB先行(高位先行)

SPI协议与其他协议一样,在数据采集的时候数据要保证稳定,触发时可以允许数据发送电平转化准备下一位的数据传输,但具体什么时候数据采集要时钟极性CPOL,与时钟相位CPOL如何设置

3.时钟信号的相位和极性(重点)

SPI_CR寄存器的CPOL和CPHA位,能够组合成四种可能的时序关系。

  • CPOL(clock polarity)时钟极性

CPOL(时钟极性)位控制在没有数据传输时时钟的空闲状态电平,此位对主模式和从模式下的设备都有效。

如果CPOL被清’0’,SCK引脚在空闲状态保持低电平;
如果CPOL被置’1’,SCK引脚在空闲状态保持高电平。

在这里插入图片描述

  • CPHA(clock phase)时钟相位

如果CPHA位被置’1’,SCK时钟的第二个边沿进行数据位的采样,数据在第二个时钟边沿被锁存(保持数据稳定)。
(如果此时CPOL位为’0’时就是下降沿采样,CPOL位为’1’时就是上升沿采样)

如果CPHA位被清’0’,SCK时钟的第一个边沿进行数据位采样,数据在第一个时钟边沿被锁存(保持数据稳定)
(如果此时CPOL位为’1’时就是下降沿采样,CPOL位为’0’时就是上升沿采样)

所以说不能绝对说数据一定在上升或下降沿采样,要看CPOL与CPHA位的值他们组合起来就有四种结果。

  • 当CPHA位为1时 如果此时CPOL位为’0’时就是下降沿采样,CPOL位为’1’时就是上升沿采样

在这里插入图片描述

  • 当CPHA位为0时 如果此时CPOL位为’1’时就是下降沿采样,CPOL位为’0’时就是上升沿采样

参考上图即可,只不过是CPHA为0时,是时钟的第一个边沿采样

  • CPOL与CPHA的寄存器配置

在这里插入图片描述

总结:
在这里插入图片描述
具体采用哪种模式要看通信双方支持哪种模式,本次实验与主机通信的FLASH支持0与3模式我们随机配置一种就好。

四.SPI 特性及架构(重点)

SPI框图:
在这里插入图片描述

1.通信引脚

在这里插入图片描述
不同型号的芯片基本都有3个SPI外设,其中SPI2,SPI3支持I2S通信因为I2S与SPI协议类似,所以他们共用一套逻辑就是上面的SPI框图。
在这里插入图片描述

2.时钟控制逻辑

在这里插入图片描述
SCK 线的时钟信号,由波特率发生器根据“控制寄存器 CR1”中的 BR[0:2]位控制,该位是对 fpclk时钟的分频因子,对 fpclk的分频结果就是 SCK 引脚的输出时钟频率
在这里插入图片描述
STM32 的 SPI 外设可用作通讯的主机及从机,支持最高的 SCK 时钟频率为 fpclk/2 (STM32F103 型号的芯片默认 fpclk1为 72MHz,fpclk2为 36MHz),

SPI1挂载在APB2总线最高通信速率达 fpclk2/2 =36Mbtis/s
SPI2,SPI3挂载在APB1总线最高通信速率为 pclk1/2=18Mbits/s

3.数据控制逻辑(非常重要)

在接收时,接收到的数据被存放在一个内部的接收缓冲器中;
在发送时,在被发送之前,数据将首先被存放在一个内部的发送缓冲器中。
对SPI_DR寄存器的读操作,将返回接收缓冲器的内容;
写入SPI_DR寄存器的数据将被写入发送缓冲器中

理解下面这个图非常重要:
只有主机发送数据才会产生时钟,所以就算是主机只接收数据,我们也要向从机发送数据只不过主机发送的数据从机会忽略。
在这里插入图片描述MOSI脚相互连接,MISO脚相互连接。这样,数据在主和从之间串行地传输(MSB位在前)。通信总是由主设备发起。主设备通过MOSI脚把数据发送给从设备,从设备通过MISO引脚回传数据这意味全双工通信的数据输出和数据输入是用同一个时钟信号同步的;时钟信号由主设备通过SCK脚提供。

总结:
⦁ 串行移位寄存器通过MOSI信号线将字节传送给从机,从机也将自己的串行移位寄存器中的内容通过MISO信号线返回给主机。这样,两个移位寄存器中的内容就被交换。

⦁ 外设的写操作和读操作是同步完成的。
主机向从机发送一个数据,主机必然又会接收到一个字节(虽然这个字节可能没意义)因为数据发送时,相当于两个设备的移位寄存器的数据交换,发送了一个数据也同时接收到一个数据

主机接收从机发送的数据时,由于只有主机发送数据才会产生时钟驱动移位寄存器,所以要向从机发送一个空字节

  • 1.如果只进行写操作,主机只需忽略接收到的字节;
  • 2.若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输,当然从机也会忽略这个数据
    在这里插入图片描述
    在这里插入图片描述

STM32主模式下开始传输
重点看全双工模式:
在这里插入图片描述
整这么多花里胡哨的我们一般用全双工模式,谁会有资源不用呢。

● 全双工模式(BIDIMODE=0并且RXONLY=0)
─ 当写入数据到SPI_DR寄存器(发送缓冲器)后,传输开始;
在传送第一位数据的同时,数据被并行地从发送缓冲器传送到8位的移位寄存器中(这里就可以体现并行传输只需一个时钟周期),然后按顺序被串行地移位送到MOSI引脚上
与此同时,在MISO引脚上接收到的数据,按顺序被串行地移位进入8位的移位寄存器中,然后被并行地传送到SPI_DR寄存器(接收缓冲器)中。

● 单向的只接收模式(BIDIMODE=0并且RXONLY=1)
─ SPE=1时,传输开始;
─ 只有接收器被激活,在MISO引脚上接收到的数据,按顺序被串行地移位进入8位的移位寄存器中,然后被并行地传送到SPI_DR寄存器(接收缓冲器)中。

● 双向模式,发送时(BIDIMODE=1并且BIDIOE=1)
─ 当写入数据到SPI_DR寄存器(发送缓冲器)后,传输开始;
─ 在传送第一位数据的同时,数据被并行地从发送缓冲器传送到8位的移位寄存器中,然后按顺序被串行地移位送到MOSI引脚上;
─ 不接收数据。

● 双向模式,接收时(BIDIMODE=1并且BIDIOE=0)
─ SPE=1并且BIDIOE=0时,传输开始;
─ 在MOSI引脚上接收到的数据,按顺序被串行地移位进入8位的移位寄存器中,然后被并行地传送到SPI_DR寄存器(接收缓冲器)中。
─ 不激活发送器,没有数据被串行地送到MOSI引脚上。

4.全双工发送和接收过程模式(超级重要)

准备工作:
必须要理解下面这张图:

总结一句话:主机发送一个字节数据的同时接收了一个字节数据
在这里插入图片描述
第二理解TXE RXNE位:
数据寄存器SPI_DR对应有两个缓存区:数据发送缓存区与数据接收缓存区

  • 数据的发送

当数据从发送缓冲器传送到移位寄存器时,设置TXE标志(发送缓冲器空),它表示内部的发送缓冲器可以接收下一个数据;如果在SPI_CR2寄存器中设置了TXEIE位,则此时会产生一个中断;写入SPI_DR寄存器即可清除TXE位

注1: 在写入发送缓冲器之前,软件必须确认TXE标志为’1’,否则新的数据会覆盖已经在发送缓冲器中的数据。
注1: 已经在发送缓存区的数据会等待移位寄存器把数据一位一位发送出去才会传送到移位寄存器中,也就是说移位寄存器的数据不会被覆盖

  • 数据的接收

在采样时钟的最后一个边沿,当数据被从移位寄存器传送到接收缓冲器时,设置RXNE标志(接收缓冲器非空);它表示数据已经就绪,可以从SPI_DR寄存器读出;如果在SPI_CR2寄存器中设置了RXNEIE位,则此时会产生一个中断;读出SPI_DR寄存器即可清除RXNIE标志位。在一些配置中,传输最后一个数据时,可以使用BSY标志等待数据传输的结束。

  • 第三理解串行并行的区别
    在这里插入图片描述
    发送数据:数据被并行地从发送缓冲器传送到的移位寄存器中(并行传输只需一个时钟周期)然后按顺序被串行地移位送到MOSI引脚上(串行传输一位需要一个时钟周期)
    接收数据:在MISO引脚上接收到的数据,按顺序被串行地移位进入8位的移位寄存器中,然后被并行地传送到SPI_DR寄存器(接收缓冲器)

总结:移位寄存器到缓存区为并行,移位寄存器发生数据到引脚为串行
重点来了:
在这里插入图片描述
其实你理解了前面的那些内容,这张图没啥大问题

5.SPI 初始化结构体

在这里插入图片描述

  • SPI_Direction
    前面有详细介绍就不多说了
    在这里插入图片描述

  • SPI_Mode
    本成员设置 SPI 工作在主机模式(SPI_Mode_Master)或从机模式(SPI_Mode_Slave ),由主机产生时钟信号,若被配置为从机模式STM32的SPI外设将接受外来的 SCK 信号
    在这里插入图片描述

  • SPI_DataSize
    本成员可以选择 SPI 通讯的数据帧大小是为 8 位(SPI_DataSize_8b)还是 16 位(SPI_DataSize_16b)。
    在这里插入图片描述

  • SPI_CPOL 和 SPI_CPHA
    在这里插入图片描述在这里插入图片描述

  • SPI_NSS
    本成员配置 NSS 引脚的使用模式,可以选择为硬件模(SPI_NSS_Hard )与软件模式(SPI_NSS_Soft ),在硬件模式中的 SPI 片选信号由 SPI 硬件自动产生,而软件模式则需要我们亲自把相应的 GPIO 端口拉高或置低产生非片选和片选信号。实际中软件模式应用比较多。在这里插入图片描述

  • SPI_BaudRatePrescaler
    在这里插入图片描述

  • SPI_FirstBit

STM32 的 SPI 模块可以通过这个结构体成员,对这个特性编程控制。
在这里插入图片描述

  • SPI_CRCPolynomial

这是 SPI 的 CRC 校验中的多项式,若我们使用 CRC 校验时,就使用这个成员的参数(多项式),来计算 CRC 的值。配置完这些结构体成员后,我们要调用 SPI_Init 函数把这些参数写入到寄存器中,实
现 SPI 的初始化,然后调用 SPI_Cmd 来使能 SPI 外设。

下面的作为了解:

状态标志
应用程序通过3个状态标志可以完全监控SPI总线的状态。
发送缓冲器空闲标志(TXE)
此标志为’1’时表明发送缓冲器为空,可以写下一个待发送的数据进入缓冲器中。当写入SPI_DR时,TXE标志被清除。
接收缓冲器非空(RXNE)
此标志为’1’时表明在接收缓冲器中包含有效的接收数据。读SPI数据寄存器可以清除此标志。

忙(Busy)标志
BSY标志由硬件设置与清除(写入此位无效果),此标志表明SPI通信层的状态。

当它被设置为’1’时,表明SPI正忙于通信,但有一个例外:在主模式的双向接收模式下(MSTR=1、BDM=1并且BDOE=0),在接收期间BSY标志保持为低 在软件要关闭SPI模块并进入停机模式(或关闭设备时钟)之前,可以使用BSY标志检测传输是否结束,这样可以避免破坏最后一次传输,因此需要严格按照下述过程执行
BSY标志还可以用于在多主系统中避免写冲突。
除了主模式的双向接收模式(MSTR=1、BDM=1并且BDOE=0),当传输开始时,BSY标志被置’1’。

以下情况时此标志将被清除为’0’:
● 当传输结束(主模式下,如果是连续通信的情况例外);
● 当关闭SPI模块;
● 当产生主模式失效(MODF=1)。

如果通信不是连续的,则在每个数据项的传输之间,BSY标志为低
当通信是连续时:
● 主模式下:在整个传输过程中,BSY标志保持为高;
● 从模式下:在每个数据项的传输之间,BSY标志在一个SPI时钟周期中为低

在这里插入图片描述
关闭SPI
当通讯结束,可以通过关闭SPI模块来终止通讯。清除SPE位即可关闭SPI。
在某些配置下,如果再传输还未完成时,就关闭SPI模块并进入停机模式,则可能导致当前的传输被破坏,而且BSY标志也变得不可信。
为了避免发生这种情况,关闭SPI模块时,建议按照下述步骤操作:
在主或从模式下的全双工模式(BIDIMODE=0,RXONLY=0)
1.等待RXNE=1并接收最后一个数据;
2. 等待TXE=1;
3. 等待BSY=0
4. 关闭SPI(SPE=0),最后进入停机模式(或关闭该模块的时钟)

溢出错误
当主设备已经发送了数据字节,而从设备还没有清除前一个数据字节产生的RXNE时,即为溢出错误。当产生溢出错误时:
● OVR位被置为’1’;当设置了ERRIE位时,则产生中断。
此时,接收器缓冲器的数据不是主设备发送的新数据,读SPI_DR寄存器返回的是之前未读的数据,所有随后传送的数据都被丢弃。
依次读出SPI_DR寄存器和SPI_SR寄存器可将OVR清除

总结:如果发送溢出错误后面接收到的数据将被丢弃,读接收缓存区是原来未读的数据

也是我们为什么只为了发送一个数据时为什么一定还要一定要接收一个数据,就是因为比如后面会讲到我们发送一个指令给flash,flash第一个字节只会返回一个空字节(应该是0xFF),此时RXNE置位,如果我们不读的话后面我们

五.NorFLASH芯片—NM25Q64EV

FLASH简介

FLSAH 存储器又称闪存,它与 EEPROM 都是掉电后数据不丢失的存储器,但 FLASH存储器容量普遍大于 EEPROM,现在基本取代了它的地位。我们生活中常用的 U 盘、SD卡、SSD 固态硬盘以及我们STM32 芯片内部用于存储程序的设备,都是 FLASH 类型的存储器。在存储控制上,最主要的区别是 FLASH 芯片只能一大片一大片地擦写,而在“I2C章节”中我们了解到 EEPROM 可以单个字节擦写。

NM25Q64EV芯片与W25Q64BV 基本一致就是设备地址不一样而已

在这里插入图片描述
在这里插入图片描述
原理图:
在这里插入图片描述
在这里插入图片描述
硬件SPI协议逻辑
在这里插入图片描述
在这里插入图片描述

1.NorFLASH的存储特性(重点)

1.在写入数据之前必须先擦出
2.擦除时把数据的位全部重置为1
3.写入数据时只能把为1的数据为改成0(这也是为什么写入数据前要先进行擦除的原因)
4.擦除的最小单位为扇区(4KB个字节)全部擦除为1

在这里插入图片描述

2.FLASH指令汇总(重点)

用红色标注的是我们常用的指令
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1)读取设备ID号

在这里插入图片描述
在这里插入图片描述

  • ABH指令

在这里插入图片描述
返回的是 16H

  • 9FH指令
    在这里插入图片描述
    返回的是 522217H

  • 90H指令
    在这里插入图片描述
    返回的是 5216H

注意:在我们返回完指令后接收flash返回的ID时,我们主机也必须发送空字节(0x00)给flash,因为只有主机发送数据才能产生时钟才能驱动flash向主机返回数据(这里是ID号)

因为发送与接收是共用一个时钟:主机发送一个空字节的同时接收一个从机发送过来的数据(这里是ID号

2)读取状态寄存器1

读取状态寄存器1判断FLASH芯片是否忙碌(在擦除数据或正在写入数据)
在这里插入图片描述
在这里插入图片描述
对于命令代码“05H”/“35H”/“15H”,将对应输出状态寄存器位S7~S0 / S15-S8 / S16-S23
在这里插入图片描述
接收到数据后我们只要判断第一位(WIP位)就能判断FLASH芯片是否忙碌

在我们写入数据时或者flash擦除自身数据时一定要判断flash芯片是否写入完成或者擦除完成

3)擦除扇区

在这里插入图片描述
为什么是24位地址因为24位地址足够表示8M字节

在这里插入图片描述

3)写使能

向flash写入数据时或者flash擦除自己的数据时一定要先写使能才能进行
flash擦除数据时是将数据都改为1也相当于写操作
在这里插入图片描述

4)页写入数据

在这里插入图片描述
页写入特点:
1.一次性写入的数据不能超过256个字节
2. 超过256个字节后面的字节将不会写入
3. 写入的地址应对齐到256 (Addr%256=0)
4. 若写入的未对齐地址数上写入的数据数量不能超过256否则超出部分写入失败

5)读取数据

在这里插入图片描述
该地址在每个地址之后自动递增到下一个更高的地址数据的字节被移出。因此,整个存储器可以使用单个读取数据字节进行读取(读取)命令。到达最高地址时,地址计数器将滚动到 0。

读取数据可以一直读取只有时钟一直驱动即可

6.FALSH低功耗模式以及唤醒

在这里插入图片描述
进入掉电 (DP) 指令,在深度关断模式下,设备未处于活动状态,并且所有写入/编程/擦除指令都将被忽略。

解除掉电模式
在这里插入图片描述
从断电 (RDP) 和读取设备 ID (RDI) 释放命令是一个多用途命令,它可用于将设备从关机状态释放或获取设备电子识别(ID)号码。

当仅用于获取设备 ID 而不处于关机状态时,将启动该命令通过将 CS# 引脚驱动至低电平,然后移动指令代码“ABH”,后跟 3 个虚拟字节(空字节)。

差不多常用的就是这么多,写代码按照这个逻辑来可以没啥大问题,根据时序图来操作就OK了

六.SPI读写串行FLASH实验

实验目的

根据SPI协议向FLASH芯片写入数据,然后读取出来进行校验**

实验原理

在这里插入图片描述
它的 CS/CLK/DIO/DO 引 脚 分 别 连 接 到 了 STM32 对 应 的 SPI 引 脚NSS/SCK/MOSI/MISO 上,其中 STM32 的 NSS 引脚是一个普通的 GPIO,不是 SPI 的专用NSS 引脚,所以程序中我们要使用软件控制的方式, 这里是使用的PA2当做我们的NSS引脚来控制通信的开始与结束

  • 引脚的工作模式
    在这里插入图片描述

源码

spi_flash.h

#ifndef __SPI_FLASH_H
#define __SPI_FLASH_H
#include "stm32f10x.h"
#include <stdio.h>
//SPI1
#define FLASH_SPI SPI1
#define FLASH_SPI_CLK RCC_APB2Periph_SPI1
#define FLASH_SPI_APBxClkCmd RCC_APB2PeriphClockCmd
// SPI1 GPIO 引脚宏定义
#define FLASH_SPI_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define FLASH_SPI_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
//SCK MOSI MISO CS
#define FLASH_SPI_SCK_GPIO_PORT GPIOA 
#define FLASH_SPI_SCK_GPIO_PIN GPIO_Pin_5
#define FLASH_SPI_MOSI_GPIO_PORT GPIOA
#define FLASH_SPI_MOSI_GPIO_PIN GPIO_Pin_7
#define FLASH_SPI_MISO_GPIO_PORT GPIOA
#define FLASH_SPI_MISO_GPIO_PIN GPIO_Pin_6
#define FLASH_SPI_CS_GPIO_PORT GPIOA
#define FLASH_SPI_CS_GPIO_PIN GPIO_Pin_2
//CS引脚电平控制
#define FLASH_SPI_CS_HIGH() GPIO_SetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_GPIO_PIN )
#define FLASH_SPI_CS_LOW() GPIO_ResetBits(FLASH_SPI_CS_GPIO_PORT,FLASH_SPI_CS_GPIO_PIN)
//NM25Q64EVB id号
#define sFLASH_ID 0x522217
#define SPI_FLASH_PageSize 256
#define Dummy 0x00
#define W25X_WriteEnable 0x06 
#define W25X_WriteDisable 0x04 
#define W25X_ReadStatusReg 0x05 
#define W25X_WriteStatusReg 0x01 
#define W25X_ReadData 0x03 
#define W25X_FastReadData 0x0B 
#define W25X_FastReadDual 0x3B 
#define W25X_PageProgram 0x02 
#define W25X_BlockErase 0xD8 
#define W25X_SectorErase 0x20 
#define W25X_ChipErase 0xC7 
#define W25X_PowerDown 0xB9 
#define W25X_ReleasePowerDown 0xAB 
#define W25X_DeviceID 0xAB 
#define W25X_ManufactDeviceID 0x90 
#define W25X_JedecDeviceID 0x9F 
//等待次数
#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
/*信息输出*/
#define FLASH_DEBUG_ON 0
#define FLASH_INFO(fmt,arg...) printf("<<-SPI-INFO->> "fmt"\n",##arg)
#define FLASH_ERROR(fmt,arg...) printf("<<-SPI-ERROR->> "fmt"\n",##arg)
#define FLASH_DEBUG(fmt,arg...) do{ 
\ if(FLASH_DEBUG_ON)\ printf("<<-SPI-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\ }while(0)
void SPI_FLASH_Config(void);
uint32_t SPI_Read_ID(void);
uint16_t SPI_Flash_ReadID(void);
void SPI_Sector_Erase(uint32_t addr);
uint8_t SPI_FLASH_Send_Byte(uint8_t data);
void SPI_WaitForWriteEnd(void);
void SPI_FLASH_PageWrite(uint32_t addr,uint8_t* WriteBuff,uint16_t NumByteToWrite);
void SPI_FLASH_BufferWrite(u32 WriteAddr,u8* pBuffer, u16 NumByteToWrite);																					
void SPI_FLASH_Read(uint32_t addr,uint8_t* ReadBuff,uint16_t NumByteToRead);	
void SPI_Flash_PowerDown(void);           //进入掉电模式省电
void SPI_Flash_WAKEUP(void);			        //唤醒 
#endif /* __SPI_FLASH_H */

spi_flash.c

#include "spi_flash.h"
#include "stm32f10x.h"
//设置等待时间
static __IO uint32_t  SPITimeout = SPIT_LONG_TIMEOUT;   
//等待超时,打印错误信息
static uint32_t FLASH_TIMEOUT_UserCallback(uint8_t errorCode);
void SPI_FLASH_Config(void)
{ 

GPIO_InitTypeDef    GPIO_InitStuctrue;
SPI_InitTypeDef     SPI_InitStuctrue;
//开启GPIO外设时钟
FLASH_SPI_GPIO_APBxClkCmd(FLASH_SPI_GPIO_CLK,ENABLE);
//开启SPI外设时钟
FLASH_SPI_APBxClkCmd(FLASH_SPI_CLK,ENABLE);
//SCK引脚-复用推挽输出
GPIO_InitStuctrue.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitStuctrue.GPIO_Pin=FLASH_SPI_SCK_GPIO_PIN;
GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(FLASH_SPI_SCK_GPIO_PORT,&GPIO_InitStuctrue);
//MOSI引脚-复用推挽输出
GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStuctrue.GPIO_Pin = FLASH_SPI_MOSI_GPIO_PIN;
GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(FLASH_SPI_MOSI_GPIO_PORT,&GPIO_InitStuctrue);
//MISO引脚-浮空输入
GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStuctrue.GPIO_Pin = FLASH_SPI_MISO_GPIO_PIN;
GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(FLASH_SPI_MISO_GPIO_PORT,&GPIO_InitStuctrue);
//CS引脚-普通推挽输出
GPIO_InitStuctrue.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStuctrue.GPIO_Pin = FLASH_SPI_CS_GPIO_PIN;
GPIO_InitStuctrue.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(FLASH_SPI_CS_GPIO_PORT,&GPIO_InitStuctrue);
//SPI结构体成员配置
SPI_InitStuctrue.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_2;
SPI_InitStuctrue.SPI_CPHA =SPI_CPHA_2Edge;
SPI_InitStuctrue.SPI_CPOL =SPI_CPOL_High;
SPI_InitStuctrue.SPI_CRCPolynomial =0;
SPI_InitStuctrue.SPI_DataSize =SPI_DataSize_8b;
SPI_InitStuctrue.SPI_Direction =SPI_Direction_2Lines_FullDuplex;
SPI_InitStuctrue.SPI_FirstBit =SPI_FirstBit_MSB;
SPI_InitStuctrue.SPI_Mode =SPI_Mode_Master;
SPI_InitStuctrue.SPI_NSS =SPI_NSS_Soft;
SPI_Init(FLASH_SPI,&SPI_InitStuctrue);
//使能SPI
SPI_Cmd(FLASH_SPI,ENABLE);
}
//发送并接收一个字节
uint8_t SPI_FLASH_Send_Byte(uint8_t data)
{ 

SPITimeout =SPIT_FLAG_TIMEOUT;
//等待发送数据寄存器为空
while(SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_TXE)==RESET)
{ 

if( (SPITimeout--)==0)return FLASH_TIMEOUT_UserCallback(0);
}
//发送一个字节
SPI_I2S_SendData(FLASH_SPI,data);
SPITimeout =SPIT_FLAG_TIMEOUT;
//等待接收数据寄存器不为空
while(SPI_I2S_GetFlagStatus(FLASH_SPI, SPI_I2S_FLAG_RXNE)==RESET)
{ 

if( (SPITimeout--)==0)return FLASH_TIMEOUT_UserCallback(1);
}
//读取一个字节并返回
return SPI_I2S_ReceiveData(FLASH_SPI);
}
//读取一个字节
uint8_t SPI_FLASH_Read_Byte(void)
{ 

return SPI_FLASH_Send_Byte(Dummy);
}
uint32_t SPI_Read_ID(void)
{ 

uint32_t flash_id;
//通信开始
FLASH_SPI_CS_LOW();
//发送读取ID的指令
SPI_FLASH_Send_Byte(W25X_JedecDeviceID);
flash_id=SPI_FLASH_Read_Byte();
flash_id<<=0x08;
flash_id|=SPI_FLASH_Read_Byte();
flash_id<<=0x08;
flash_id|=SPI_FLASH_Read_Byte();
//通信结束
FLASH_SPI_CS_HIGH();
return flash_id;
}
uint16_t SPI_Flash_ReadID(void)
{ 

uint16_t Temp = 0;	  
FLASH_SPI_CS_LOW();			    
SPI_FLASH_Send_Byte(W25X_ManufactDeviceID);//发送读取ID命令 
//发送000000H的24位地址
SPI_FLASH_Send_Byte(Dummy); 	    
SPI_FLASH_Send_Byte(Dummy); 	    
SPI_FLASH_Send_Byte(Dummy); 
//读取ID号 
Temp=SPI_FLASH_Read_Byte();
Temp<<=0x08;
Temp|=SPI_FLASH_Read_Byte();	 
FLASH_SPI_CS_HIGH();				    
return Temp;
}   		    
//写使能
void SPI_WriteEnable(void)
{ 

//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_WriteEnable);
//通信结束
FLASH_SPI_CS_HIGH();
}
//擦除扇区(4kb)
void SPI_Sector_Erase(uint32_t addr)
{ 

//写使能
SPI_WriteEnable();
//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_SectorErase);
SPI_FLASH_Send_Byte(addr>>16);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
//通信结束
FLASH_SPI_CS_HIGH();
//等待擦除完成
SPI_WaitForWriteEnd();
}
//等待FLASH擦除或写操作完成
void SPI_WaitForWriteEnd()
{ 

uint8_t Status;
//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_ReadStatusReg);	
//反复读取状态寄存器
do
{ 

Status=SPI_FLASH_Read_Byte();
}while( (Status&0x01)==1 );
//通信结束
FLASH_SPI_CS_HIGH();
}
//读取多个字节
void SPI_FLASH_Read(uint32_t addr,uint8_t* ReadBuff,uint16_t NumByteToRead)
{ 

//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_ReadData);
SPI_FLASH_Send_Byte(addr>>16);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
while(NumByteToRead--)
{ 

*ReadBuff=SPI_FLASH_Read_Byte();
*ReadBuff++;
}
//通信结束
FLASH_SPI_CS_HIGH();
}
//页写入
void SPI_FLASH_PageWrite(uint32_t addr,uint8_t* WriteBuff,uint16_t NumByteToWrite)
{ 

//写使能
SPI_WriteEnable();
//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_PageProgram);
SPI_FLASH_Send_Byte(addr>>16);
SPI_FLASH_Send_Byte((addr>>8)&0xff);
SPI_FLASH_Send_Byte(addr&0xff);
//写入数据不能超过256个字节
if(NumByteToWrite>SPI_FLASH_PageSize)
{ 

NumByteToWrite=SPI_FLASH_PageSize;
}
while(NumByteToWrite--)
{ 

SPI_FLASH_Send_Byte(*WriteBuff);
*WriteBuff++;
}
//通信结束
FLASH_SPI_CS_HIGH();
//等待写入完成
SPI_WaitForWriteEnd();
}
//不能超过4096个字节(一个块)
void SPI_FLASH_BufferWrite(u32 WriteAddr,u8* pBuffer, 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(WriteAddr,pBuffer , NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{ 
 
/*先把整数页都写了*/
while (NumOfPage--)
{ 

SPI_FLASH_PageWrite(WriteAddr, pBuffer, SPI_FLASH_PageSize);
WriteAddr +=  SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
SPI_FLASH_PageWrite(WriteAddr,pBuffer, NumOfSingle);
}
}
/* 若地址与 SPI_FLASH_PageSize 不对齐 */
else 
{ 

/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{ 

/*当前页剩余的count个位置比NumOfSingle小,一页写不完*/
if (NumOfSingle > count) 
{ 

temp = NumOfSingle - count;
/*先写满当前页*/
SPI_FLASH_PageWrite(WriteAddr,pBuffer , count);
WriteAddr +=  count;
pBuffer += count;
/*再写剩余的数据*/
SPI_FLASH_PageWrite( WriteAddr,pBuffer, temp);
}
else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
{ 

SPI_FLASH_PageWrite(WriteAddr,pBuffer , NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{ 

/*地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage =  NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* 先写完count个数据,为的是让下一次要写的地址对齐 */
SPI_FLASH_PageWrite(WriteAddr,pBuffer , count);
/* 接下来就重复地址对齐的情况 */
WriteAddr +=  count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{ 

SPI_FLASH_PageWrite(WriteAddr,pBuffer, SPI_FLASH_PageSize);
WriteAddr +=  SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{ 

SPI_FLASH_PageWrite(WriteAddr, pBuffer, NumOfSingle);
}
}
}
}
//进入掉电模式
void SPI_Flash_PowerDown(void)
{ 

//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_PowerDown);
//通信结束
FLASH_SPI_CS_HIGH();
}
//取消掉电模式
void SPI_Flash_WAKEUP(void)
{ 

//通信开始
FLASH_SPI_CS_LOW();
SPI_FLASH_Send_Byte(W25X_ReleasePowerDown);
//通信结束
FLASH_SPI_CS_HIGH();
}	
static  uint32_t FLASH_TIMEOUT_UserCallback(uint8_t errorCode)
{ 

/* Block communication and all processes */
FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}

main.c

#include "stm32f10x.h"
#include "led.h"
#include "usart.h"
#include "./spi/spi_flash.h"
#define SOFT_DELAY Delay(0x0FFFFF);
void Delay(__IO u32 nCount); 
#define ByteSize 256
#define WriteAddr 200 
#define ReadAddr 200
uint8_t WriteBuff[ByteSize];
uint8_t ReadBuff[ByteSize];
int main(void)
{ 
	
uint16_t i;
uint32_t flash_id=0;
//初始化SPI
SPI_FLASH_Config();
//初始化USART 
Usart_Config();
//初始化LED
LED_GPIO_Config();
printf("SPI测试实验\r\n");
for(i=0;i<ByteSize;i++)
{ 

WriteBuff[i]=i;
}
flash_id=SPI_Read_ID();
printf("flash_id=0x%x\r\n",flash_id);
if(flash_id==sFLASH_ID)
{ 

//擦除0扇区
SPI_Sector_Erase(0);
//从0地址依次写入256个字节
SPI_FLASH_BufferWrite(WriteAddr,WriteBuff,ByteSize);
//读取256个字节
SPI_FLASH_Read(ReadAddr,ReadBuff,ByteSize);
for(i=0;i<ByteSize;i++)
{ 

if(ReadBuff[i]!=WriteBuff[i])
{ 

printf("数据不一致\r\n");
return 0;
}
printf("0x%-2x ",ReadBuff[i]); 
if(i%16==15)
{ 

printf("\r\n");
}
}
LED_G(NO);
printf("SPI读写FLASH成功!!");
}
else
{ 

printf("ID错误\r\n");
LED_R(NO);
}
while(1)
{ 
}
}
void Delay(__IO uint32_t nCount)	 //简单的延时函数
{ 

for(; nCount != 0; nCount--);
}

总结

USATR、I2C、SPI这三个协议基本上非常重要啦,其实这是协议都有很多共同的特性,比如数据的传输方式串行并行,数据寄存器与移位寄存器等等,有着异曲同工之妙,所以说我们学习这些通信协议时要举一反三,学精一个对其他通信协议的学习有很大帮助。

结束语:
最近发现一款刷题神器,如果大家想提升编程水平,玩转C语言指针,还有常见的数据结构(最重要的是链表和队列)后面嵌入式学习操作系统的时如freerots、RT-Thread等操作系统,链表与队列知识大量使用。
大家可以点击下面连接进入牛客网刷题

点击跳转进入网站(C语言方向)
点击跳转进入网站(数据结构算法方向)
在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/181549.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 分子排列不同会导致_《分子生物学》习题答案

    分子排列不同会导致_《分子生物学》习题答案《分子生物学》课后习题第1章绪论1.简述孟德尔、摩尔根和Waston等人对分子生物学发展的主要贡献。孟德尔是遗传学的奠基人,被誉为现代遗传学之父。他通过豌豆实验,发现了遗传学三大基本规律中的两个,分别为分离规律及自由组合规律。摩尔根发现了染色体的遗传机制,创立染色体遗传理论,是现代实验生物学奠基人。于1933年由于发现染色体在遗传中的作用,赢得了诺贝尔生理学或医学奖。Watson于1953年和克里…

  • 彻底卸载Symantec Endpoint Protection之另类办法「建议收藏」

    彻底卸载Symantec Endpoint Protection之另类办法「建议收藏」诺顿卸载需要输入密码,网上一篇文章说终结进程的办法不适合v11,机器是单位的,所以开始并没有想到完全卸载,怕起不来,于是进入安全模式禁用所有服务,下个卡巴斯基安装,结果一安装,卡巴斯基就提示先卸载诺顿,太可爱了,点击确认之后,卸载之后再重启就卸得干干净净了,装上卡巴斯基,好几天也没死一次机,而以前一天要死一两次,诺顿真垃圾,而且卸载也卸不干净。卡巴斯基还有这个妙用啊,即使你不想安卡巴斯基,也可以用它来删诺顿,而且不用输密码,强。

  • Java面试题大全(2020版)[通俗易懂]

    发现网上很多Java面试题都没有答案,所以花了很长时间搜集整理出来了这套Java面试题大全,希望对大家有帮助哈~本套Java面试题大全,全的不能再全,哈哈~一、Java基础1.JDK和JRE有什么区别?JDK:JavaDevelopmentKit的简称,java开发工具包,提供了java的开发环境和运行环境。 JRE:JavaRuntimeEnviron…

  • kali linux2020中文乱码及主题切换

    kali linux2020中文乱码及主题切换2019年11月26日,kaliLinux官网发布了KaliLinux2019.4发行版,此版本做了很大的改动,界面焕然一新。2019.4涉及到的一些新更新内容包括:新的默认桌面环境Xfce新的GTK3主题(适用于Gnome和Xfce)引入“KaliUndercover”模式KaliDocumentation有一个新家,现在由Git驱动公共包装–将您的工具带入KaliK…

  • 织梦中data文件夹是存放什么内容的

    织梦中data文件夹是存放什么内容的

  • 虚拟机和宿主机共享文件_安装vmware的宿主机

    虚拟机和宿主机共享文件_安装vmware的宿主机虚拟机网络共享给宿主机宿主机使用虚拟机的VPN连接需求VMware虚拟机中跑Windows10(下文称Guest),使用某客户端连接公司VPN(还需要启动联软UniAccess才可以正常访问内网),宿主机为Windows10(下文称Host),未安装该VPN客户端。现需要在Guest中连接VPN,然后共享给Host使用。关键词桥接:对应虚拟网卡VMnet0仅主机模式(HostOnly):对应虚拟网卡VMnet1实现步骤1.Guest添加桥接网卡

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号