12 SPI通信协议

12.1 SPI的基本概念

12.1.1 SPI简介

I2C和SPI两者是各有优势和劣势的,在某些芯片呢我们用I2C更好,在另一些芯片呢我们用SPI更好,上一节我们学习I2C的时候,可以发现,I2C无论是硬件电路还是软件时序,设计的都是相对比较复杂的,硬件上我们要配置为开漏外加上拉的模式,软件上我们有很多功能和要求,比如一根通信线兼具数据收发,应答位的收发寻址机制的设计等等,最终通过这么多的设计,就使得I2C通信的性价比非常高,I2C可以在消耗最低硬件资源的情况下,实现最多的功能,在硬件上无论挂载多少个设备,都只需要两根通讯线,在软件上数据双向通信,应答位都可以实现,既实现硬件上最少的通讯线,又实现软件上最多的功能,也隐藏了一个缺点,就是I2C开漏外加上拉电阻的电路结构,使得通信线高电平的驱动能力比较弱,这就会导致通信线由低电平变到高电平的时候,这个上升沿耗时比较长,这会限制I2C的最大通讯速度,所以I2C的标准模式只有100KHz的时钟频率,I2C的快速模式也只有400KHz,虽然I2C协议之后通过改进电路的方式,设计出高速模式可以达到3.4MHz,但是高速模式目前普及程度不是很高,一般情况下我们认为I2C的时钟速度最多就是400KHz,这个速度相比较SPI而言还是慢了很多的。

  • 简单概括几点SPI相对于I2C的优缺点
    • 首先SPI传输更快,SPI协议并没有严格规定最大传输速度,这个最大传输速度取决于芯片厂商的设计需求,比如说我们这个w25q64 存储器芯片,手册里写的spi时钟频率最大可达80MHz,这比stm32f1的主频还要高;
    • 其次SPI的设计比较简单粗暴,实现的功能I2C那么多,所以学习起来SPI还是比I2C简单很多的,最后SPI的硬件开销比较大,通信线的个数比较多。

SPI的基本特征是同步全双工,首先这是同步时序,肯定就得有时钟线了,所以这个SCK硬件就是用来提供时钟信号的,数据位的输出和输入,都是在SCK的上升沿或下降沿进行的,这样数据位的收发时刻就可以明确的确定,并且同步时序时钟快点慢点或者中途暂停一会儿都是没问题的,这是同步时序的好处。

12.1.2 SPI硬件电路连接示例

  • 硬件电路连接
    • 所有SPI设备的SCK、MOSI、MISO分别连在一起,主机另外引出多条SS控制线,分别接到各从机的SS引脚。
  • 通讯引脚介绍
    • 首先SCL时钟线,时钟线完全由主机掌控,所以对于主机来说,时钟线为输出,对于所有从机来说,时钟线都为输入,这样主机的同步时钟就能送到各个从机了;
    • 其次MOSI主机输出从机输入,这边左边是主机,所以就对应MO主机输出,下面三个都是从机,所以就对应SI从机输入,数据传输方向是主机通过MOSI输出,所有从机通过MOSI输入;
    • 接着下一个MISO主机输入从机输出,左边是主机对应MI,下面三个是从机,对应SO,数据传输方向是三个从机,通过MISO输出,主机通过MISO输入。
    • 从机选择,为了确定通信的目标,主机就要另外引出多条SS控制线,分别接到各从机的SS引脚下面。下面这里有三个从机,需要主机另外引出三根SS选择线,主机的SS线都是输出,从机的SS线都是输入,SS线是低电平有效,主机想指定谁,就把对应的SS输出线置低电平就行了,比如主机初始化之后,所有的SS都输出高电平,这样就是谁也不指定,当主机需要和比如从机1进行通信了,主机就把SS1线输出低电平,不需要像I2C一样进行寻址,是不是挺简单的。
  • 引脚配置
    • 输出引脚配置为推挽输出
      • 对输出我们配置推挽输出,高低电平均有很强的驱动能力,这将使得SPI引脚信号的下降沿非常迅速,上升沿也非常迅速,不像I2C那样下降沿非常迅速,但是上升沿就比较缓慢了,那得益于推换输出的驱动能力,SPI信号变化的快,那自然它就能达到更高的传输速度,一般SPI信号都能轻松达到兆赫兹的速度级别,I2C并不是不想使用更快的推挽输出,而是I2C要实现半双工,经常要切换输入输出,而且I2C又要实现多主机的时钟同步和总线仲裁,这些功能都不允许I2C使用推挽输出,要不然你不小心就电源短路了,所以I2C选择了更多的功能,自然就要放弃更强的性能了,对SPI来说,首先SPI不支持多主机,然后SPI就是全双工,SPI的输出引脚始终是输出,输入引脚始终是输入,基本不会出现冲突,所以SPI可以大胆的使用推挽输出;
      • 不过SPI还是有一个冲突点的,就是图上的MISO引脚,主机一个是输入但是三个从机全都是输出,如果三个引脚都始终是推挽输出,势必会导致冲突,所以在SPI协议里有一条规定,就是当从机的SS引脚为高电平,也就是从机未被选中时,他的MISO引脚必须切换为高阻态,高阻态就相当于引脚断开,不输出任何电平,这样就可以防止一条线有多个输出,而导致的电平冲突的问题了,在SS为低电平时,MISO才允许变为推挽输出,这是SPI对这个可能的冲突做出的规定,当然这个切换过程都是在从机里,我们一般都写主机的程序,所以我们主机的程序中并不需要关注这个问题
    • 输入引脚配置为浮空或上拉输入

12.1.3 SPI数据移位示意

这个移位示意图是SPI硬件电路设计的核心,我们看一下SPI的基本收发电路,就是使用了这样一个移位的模型,左边是SPI主机,里面有一个八位的移位寄存器,右边是SPI从机,里面也有一个八位的移位寄存器,这里移位寄存器有一个时钟输入端,因为SPI一般都是高位先行,所以每来一个时钟,移位寄存器就会向左进行移位,从机也是同理,然后移位寄存器的时钟源是由主机提供的,这里叫做波特率发生器,它产生的时钟驱动主机的移位寄存器进行移位,同时这个时钟也通过SCK引脚进行输出接到从机的移位寄存器里,之后上面移位寄存器的接法是主机移位寄存器左边移出去的数据,通过MOSI引脚输入到从机移位寄存器的右边,从机移位寄存器左边移出去的数据通过MISO引脚输入到主机移位寄存器的右边,这样组成一个圈。

这里注释用文字说明一下这个电路如何工作:

首先我们规定波特率发生器时钟的上升沿,所有移位寄存器向左移动一位,移出去的位放在引脚上,波特率发射器时钟的下降沿,引脚上的位采样输入到移位寄存器的最低位

假设主机有个数据,10101010要发送到从机,同时从机有个数据01010101要发送到主机,那我们就可以驱动时钟,先产生一个上升沿,这时所有的位就会像这样往左移动一次,那从最高位移出去的数据,就会这样放到通信线上,数据放到通信线上实际上是放到了输出数据寄存器,则此时MOSI数据是1,所以MOSI的电平就是高电平,MISO的数据是0,所以MISO的电平就是低电平,就是第一个时钟上升沿执行的结果,就是把主机和从机中移位寄存器的最高位,分别放到MOSI和MISO的通信线上,这就是数据的输出,之后时钟继续运行,上升沿之后,下一个边沿就是下降沿,在下降沿时,主机和从机内都会进行数据采样输入,也就是MOSI的1,会采样输入到从机这里的最低位,MISO的0会采样输入到主机这里的最低位,这是第一个时钟结束后的现象。

那时钟继续运行下一个上升沿,同样的操作,移位输出,主机现在的最高位,也就是原始数据的次高位输出到MISO,从机现在的最高位输出到MISO,随后下降沿数据采样输入,MOSI数据到这里,MISO数据到这里,一直到第八个时钟都是同样的过程,就实现了主机和从机一个字节的数据交换,实际上spi的运行过程就是这样,spi的数据收发都是基于字节交换,当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这样主机要发送的数据跑到从机,主机要从从机接收的数据跑到主机,这就完成了发送同时接收的目的。

那你可能会问,如果只想发送,不想接收怎么办呢,其实很简单,我们仍然调用交换字节的时序,发送同时接收,只是这个接收到的数据,我们不看它就行了,那如果我只想接收,不想发送怎么办呢,同理我们还是调用交换自己的时序,发送同时接收,只是我们会随便发送一个数据,只要能把从机的数据置换过来就行了,我们读取置换过来的数据不就是接收了吗,这里我们随便发过去的数据啊,从机也不会去看它,当然这个随便数据我们不会真的随便发啊,一般在接收的时候,我们会统一发送0x000xff,以上就是SPI的基本原理。

总结一下就是SPI通信的基础是交换一个字节,有了交换一个字节就可以实现,发送一个字节、接收一个字节和发送同时接收一个字节这三种功能,可以看出SPI在只执行发送或只执行接收的时候会存在一些资源浪费现象,不过全双工本来就会有浪费的情况发生,SPI表示我不在乎好了。

12.1.4 SPI的通信时序

(一)起始和终止时序

(二)基本通信时序

接下来就是数据传输的基本单元了,这个基本单元什么时候开始移位,是上升沿移位还是下降沿移位,SPI并没有限定死,可以配置选择,这样的话SPI就可兼容更多的芯片,那在这里SPI有两个可以配置的位,分别叫做CPOL、CPHA,每一位可以配置为一或零,总共组合起来就有模式零模式一模式二模式三这四种模式,当然模式虽然多,但是它们的功能都是一样的,在实际使用的时候,我们主要学习其中一种就可以了,剩下的模式你知道有这些东西可以配置,如果到时候真的需要用,再过来了解一下就行了。

  • 模式一

那么先看一下模式一,因为这个模式和我们刚才讲的移位模型是对应的,这个时序的基本功能是交换一个字节,也就是刚在这里我们展示的现象,这里CPOL=0,表示空闲状态时SCK为低电平,下面可以看到在SS未被选中时,SCK默认是低电平的。

然后CPHA=1,表示SCK第一个边沿移出数据,第二个边沿移入数据,但这句话也有不同的描述方式,有的地方写的是CPHA=1表示SCK的第二个边沿进行数据采样,或者是SCK的偶数边沿进行数据采样,这些不同的描述意思都是一样,我这里为了照应刚才的移位模型,我就写的是SCK第一个边沿移出数据,第二个边沿移入数据,来看一下下面的时序图,第一个SS从机选择,在通信开始前,SS为高电平,在通信过程中SS始终保持低电平,通信结束SS恢复高电平。

然后最下面一个MISO,这是主机输入从机输出,刚才说了这里因为有多个从机输出连在了一起,如果同时开启输出会造成冲突,所以我们的解决方法是在SS未被选中的状态,从机的MISO引脚必须关断输出,即配置输出为高阻状态,那在这里SS高电平时MISO用一条中间的线表示高阻态,SS下降沿之后,从机的MISO被允许开启输出,SS上升沿之后呢,从机的MISO必须置回高阻态,这是这一块的设计啊。

再看一下移位传输的操作,因为CPHA=1,SCK第一个边沿移出数据,所以这里可以看出来,SCK第一个边沿就是上升沿,主机和从机同时移出数据,主机通过MOSI移出最高位,此时MOSI的电平就表示了主机要发送数据的b7 ,从机通过MISO移出最高位,此时MISO表示从机要发送数据的b7 ,然后时钟运行产生下降沿,此时主机和从机同时移入数据,也就是进行数据采样,这里主机移出的b7进入从机移位寄存器的最低位,从机移出的b7进入主机移位寄存器的最低位,这样一个时钟脉冲产生完毕,一个数据位传输完毕,接下来就是同样的过程,上升沿主机和从机同时输出当前移位寄存器的最高位,第二次的最高位就是原始数据的b6,然后下降沿主机和从机移入数据,b6 传输完成之后时钟继续运行,数据依次移出移入移出移入,最后一个下降沿数据b0传输完成,至此主机和从机就完成了一个字节的数据交换。

如果主机只想交换一个字节,那这时候就可以置SS为高电平结束通信了,在SS的上升沿MOSI还可以再变化一次,将MOSI制造一个默认的高电平或低电平,当然也可以不去管它,因为SPI没有硬性规定MOSI的默认电平。

然后MISO从机必须得置回高组态,此时如果主机的MISO为上拉输入的话,那MISO引脚的电平就是默认的高电平,如果主机MISO为浮空输入,那MISO引脚的电平不确定,这是交换一个字节就结束了流程,那如果主机还想继续交换,在此时主机就不必把SS置回高电平,直接重复一下从这里到这里交换一个字节的时序,这样就可以交换多个字节了,就是SPI传输数据的流程。

  • 模式零

我们继续看一下模式0[使用最多,重点掌握],这个模式0和模式1的区别,就是模式0的CPHA=0,模式1的CPHA=1

在时序上的区别对比一下,模式0的数据移出移入的时机会提前半个时钟,也就是相位提前了。

模式0的CPHA=0,表示SCK第一个边沿移入数据,第二个边沿移出数据,模式0在SCK第一个边沿就要移入数据,但数据总得先移出才能移入对吧,所以在模式0的配置下,SCK第一个边沿之前就要提前开始移出数据了,或者把它称作是在第零个边沿移出在第一个边沿移入,看一下时序,首先SS下降沿开始通信,现在SCK还没有变化,但是SCK一旦开始变化,就要移入数据了,所以此时趁SCK还没有变化,SS下降沿时就要立刻触发移位输出,所以这里MOSI和MISO的输出,是对齐到SS的下降沿的,或者说这里把SS的下降沿也当作时钟的一部分了,那SS下降沿触发的输出,SCK上升沿就可以采样输入数据了,这样b7就传输完毕。之后SCK下降沿移出b6 ,SCK上升沿移入b6 ,然后继续,下降沿移出数据,上升沿移入数据,最终在第八个上升沿时,b0位移入完成,整个字节交换完成。

之后SCK还有一个下降沿,如果主机只需要交换一个字节就结束,那在这个下降沿时MOSI可以置回默认电平或者不去管它,MISO也会变化一次,这一位实际上是下一个字节的b7,因为这个相位提前了,所以下一个字节的b7会露个头,如果不需要的话,SS上升沿之后从机MISO置回高阻态,这是交换一个字节就结束。

如果主机想交换多个字节的话,那就继续调用从这里到这里的时序,在最后一个下降沿主机放下一个字节的b7 ,从机也放下一个字节的b7, SCK上升沿正好接着采样第二个字节的b7,这样时序才能拼接得上,就是SPI交换一个字节的模式零。

模式零和模式一的区别就在于,模式零把这个数据变化的时机给提前了,在实际应用中,模式零的应用是最多的,所以我们重点掌握模式零即可,后续的程序都是基于SPI模式零来讲解的,不过这里我感觉模式一是不是更符合常理,但实际确实是模式零用的最多,可能是spi设计的时候,为了兼容现存设备吧,或者是模式0在实际应用时确实有什么优势,或者因为模式零排在最前面,大家都默认最前面的模式吗,这个原因大家感兴趣的话可以调研一下。

12.1.5 SPI完整波形示例

每个芯片对SPI时序字节流功能的定义不一样,在这里我是以我们本节课使用的芯片,w25q64它的时序为例进行讲解。

SPI对字节流功能的规定不像I2C那样,I2C的规定一般是,有效数据流第一个字节是寄存器地址,之后依次是读写的数据,使用的是读写寄存器器的模型,而在SPI中,通常采用的是指令码加读写数据的模型

这个过程就是SPI起始后,第一个交换发送给从机的数据一般叫做指令码,在从机中,对应的会定义一个指令集,当我们需要发送什么指令时,就可以在起始后第一个字节发送指令集里面的数据,这样就能指导从机完成相应的功能了,不同的指令可以有不同的数据个数,有的指令只需要一个字节的指令码就可以完成,比如w25q64的写使能写失能等指令,而有的指令后面就需要再跟要读写的数据,比如w25q64的写数据读数据等,写数据指令后面就得跟上我要在哪里写我要写什么对吧,读数据指令后面就得跟上我要在哪里读我读到的是什么,这是指令码加读写数据的模型,在SPI从机的芯片手册里都会定义好指令集,什么指令对应什么功能,什么指令后面得跟上什么数据这些内容,我们下一小节学习芯片的时候再具体分析。

那这里我简单的抓了几个指令的波形,我们先来看一下这些波形是什么样的。

(一)发送单字节指令

发送指令,向SS指定的设备发送指令(0x06

指令0x06到底是什么意思呢,可以由芯片厂商自己规定,在w25q64芯片中,这个0x06代表的是写使能,我们看一下这个模型,在这里我们使用的是SPI模式0,在空闲状态是SS为高电平,SCK为低电平,MOSI和MISO的默认电平没有严格规定,然后SS产生下降沿时序开始,在这个下降沿时刻,MOSI和MISO就要开始变换数据了,MOSI由于指令码最高位仍然是0,所以这里保持低电平不变,MISO从机现在没有数据发给主机,引脚电平没有变化,实际上w25q64不需要回弹数据时,手册里规定的是MISO仍然是高阻态,从机并没有开启输出,不过这也没问题,反正这个数据我们也不要看,那这里因为STM32的MISO是上拉输入,所以这里MISO呈现高电平。

之后SCK第一个上升沿进行数据采样,我这里画了一条绿线,从机采样输入得到0,主机采样输入得到1,之后继续第二个时钟,主机数据仍然是0,所以波形仍然没有变化,然后这样一位一位的发送接收发送接收,到这一位数据才开始变化,主机要发送数据1,下降沿数据移出,主机将1移出到MOSI,MOSI变为高电平,这里因为是软件模拟的时序,所以MOSI的数据变化有些延迟,没有紧贴SCK的下降沿,不过这也没关系,时钟是主机控制的,我们只要在下一个SCK上升沿之前完成变化就行了,然后SCK上升沿数据采样输入,在最后一位呢下降沿数据变化MOSI变为0,上升沿数据采样,从机接收数据0。SCK低电平是变化的时期,高电平是读取的时期,这一块是不是和I2C差不多,那时序SCK最后一个上升沿结束,一个字节就交换完毕了。

因为写使能是单独的指令,不需要跟随数据,SPI只需要交换一个字节就完事了,所以最后在SCK下降沿之后,SS置回高电平结束通信。

总结一下就是,主机用0x06换来了从机的0xff,但实际上从机并没有输出,这个0xff是默认的高电平,所以这个0xff没有意义,我们不用管,那整个时序的功能就是发送指令,指令码是0x06,从机一比对事先定义好的指令集,发现0x06是写使能的指令,那从机就会控制硬件进行写使能,这样一个指令从发送到执行就完成了,就是发送单字节指令的时序。

(二)指定地址写

向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)

我们这个w25q64 芯片有8M字节的存储空间,一个字节的八位地址肯定不够,所以这里地址是24位的分三个字节传输,我们看一下时序:

首先SS下降沿开始时序,MOSI空闲时是高电平,所以在下降沿之后,SCK第一个时钟之前可以看到MOSI变换数据由高电平变为低电平,然后SCK上升沿数据采样输入,后面还是一样下降沿变换数据上升沿采样数据,八个时钟之后一个字节交换完成,我们用0x02换来了0xff,其中发送的0x02是一条指令,代表这是一个写数据的时序,接收到0xff不需要看。

那既然是写数据的时序,后面必然还要跟着写的地址和数据,所以在最后一个下降沿时刻,因为我们后续还需要继续交换字节,所以在这个下降沿,我们要把下一个字节的最高位放到MOSI上,当然下一个字节的最高位仍然是零,所以这里数据没有变化,最后还是同样的流程交换一个字节,第二个字节我们用0x12换来了0xff,根据w25q64 芯片的规定,写指令之后的字节定义为地址高位,所以这个0x12就表示发送地址的23~16位,继续看一下交换一个字节,发送的是0x34这个就表示发送地址的15~8位,最后还是交换一个字节发送的是0x56 ,这个表示发送地址的7~0位,通过三个字节的交换,24位的地址就发送完毕了,从机收到的24位地址是0x123456

那三位地址结束后,就要发送写入指定地址的内容了,我们继续调用交换一个字节,发送数据这里的波形是0x55,这个表示我要在0x123456地址下,写入0x55这个数据,最后如果只想写出一个数据的话,就可以SS置高电平结束通信了。

当然这里也可以继续发送数据,SPI里也会有和I2C一样的地址指针,每读写一个字节地址指针自动加一,如果发送一个字节之后不终止继续发送的字节就会依次写入到后续的存储空间里,这样就可以实现从指定地址开始写入多个字节了,这就是SPI写入的时序,由于SPI没有应答机制,所以交换一个字节后,就立刻交换下一个字节就行了。

然后这条指令我们还可以看出啊,由于整个流程我们只需要发送的功能,并没有接收的需求,所以MISO这条接收的线路就始终处于挂机的状态,我们并没有用到,当然不同的芯片肯定有不同的规定,我们这个存储器的容量大,所以需要连续制定三个字节的地址,如果容量小的话,可能一个字节的地址就够了,或者有的芯片会直接把地址融合到指令码里去,这也是可以的哈,至于具体怎么操作的,还是得仔细分析一下芯片手册。

(三)指定地址读

向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data),功能是向SS指定的设备先发送读指令,这里芯片定义0x03为读指令,随后在指定地址下读取从机数据,我们看一下时序:

起始之后第一个字节主机发送指令0x03,表示我要读取数据了,最后还是一样,主机在依次交换三个字节,分别是0x12、0x34、0x56,组合到一起就是0x123456代表24位地址,最后这个地方就是关键点,因为我们是读取数据,指定地址之后,显然我们就要开始接收数据,所以这里三个字节的地址交换完之后,我们要把从机的数据搞过来,怎么搞过来呢,我们还是交换一个数据来个抛砖引玉,我们随便给从机一个数据,一般给0xff就行了,从机就会乖乖的把0x123456地址下的数据通过MISO发给主机,可以看到这样的波形就表示指定地址下的数据是0x55,这样主机就实现了指定地址读一个字节的目的。

然后如果我们继续抛砖引玉,那么从机内部的地址指针自动加一,从机就会继续把指定地址下一个位置的数据发过来,这样依次进行,就可以实现指定地址接收多个字节的目的了,最后数据传输完毕,SS置回高电平,时序结束,当然时序这里也会有些细节,比如由于MISO是硬件控制的波形,所以它的数据变化都可以紧贴时钟的下降沿,另外我们可以看到MISO数据的最高位实际上是在上一个字节,最后一个下降沿提前发生的,因为这是SPI模式零,所以数据变化都要提前半个周期

12.2 W25Q64存储芯片

12.2.1 W25Q64简介

低成本,也就是说这个芯片一般也就几块钱。更换不同的型号,我们的硬件电路和底层驱动程序都不需要更改,所以我们学会了其中一个型号,在应用同系列的其他型号就很容易上手了。

举例应用1

字库存储这个可以应用到一些显示屏上,比如我们这个OLED显示屏或者LCD液晶屏,你如果想在屏幕上显示汉字,就得把汉字的点阵数据存起来,当然简单的方法是,把字库直接存在STM32内部,这样适合少量汉字显示的情况,如果汉字非常多,再直接存在STM32中就不合适了,所以我们可以用这个芯片来存储汉字,在显示某个汉字之前,先读取芯片查询字库,再在显示屏上显示对应的点阵数据。

举例应用2

固件程序存储,这个就相当于直接把程序文件下载到外挂芯片里,需要执行程序的时候,直接读取外挂芯片的程序文件来执行,这就是XIP(eXecute In Place)就地执行,比如我们电脑里的bios固件,就可以存储在这个系列的芯片里。

这个芯片的存储介质是Nor Flash,Flash就是闪存存储器,像我们STM32的程序存储器、u盘、电脑里的固态硬盘等使用的都是Flash闪存闪存分为Nor Flash和Nand Flash,两者各有优势和劣势,适用领域不同,这个感兴趣的话可以百度了解一下。

时钟频率,我们这个芯片使用的是SPI通信,其中SPI的SCK线就是时钟线,这个时钟线的最大频率是80MHz,这个频率相比较STM32是非常快的,所以我们在写程序的时候翻转引脚就不用再加延时了,即使不延时这个GPIO的翻转频率也不可能达到80MHz,所以可以放心使用,然后后面这还有两个频率,分别是160MHz这个是双重SPI模式等效的频率,320MHz这个是四重SPI模式等效的频率。

注意:

这个双重SPI和四重SPI大家了解一下即可,我们本课程不会用到,那他们是什么意思呢,就是我们之前说的MOSI用于发送,MISO用于接收,是全双工通信,在只发或只收时有资源浪费,但是这个w25q芯片的厂商不忍心浪费,所以就对SPI做出了一些改进,就是我在发的时候,我可以同时用MOSI和MISO发送,在收的时候也可以同时用MOSI和MISO接收,MOSI和MISO同时兼具发送和接收的功能,一个SCK时钟我同时发送或接收两位数据,就是双重SPI模式,那你一个时钟收发两位相比较一位一位的普通SPI数据传输率就是二倍了,所以这里写的是在双重SPI模式下,等效的时钟频率就是80MHz的二倍就是16MHz,但实际上这个频率最大还是80MHz,只是我一个时钟发两位而已。然后四重SPI模式,很显然就是一个时钟发送或接收四位,等效的频率就是80x4=320MHz,在我们这个芯片里啊,除了SPI通信引脚,还有两个引脚,一个是wp写保护,另一个是hold,这两个引脚如果不需要的话,也可以拉过来充当数据传输引脚,加上MOSI和MISO就可以四个数据位同时收发了,就是四重SPI,其实这就有点并行传输的意思了,串行是根据时钟一位一位的发,并行是一个时钟八位同时发送,所以这个四重SPI模式,其实就是四位并行的模式,这个大概了解一下就行。

这个芯片使用的是24位的地址,24位地址是三个字节,因为我们在进行读写的时候,肯定得把每个字节都分配一个地址,这样才能找到它们,上小节讲时序的时候也提到过,这里在指定地址时需要一次性指定三个字节,24位的地址,然后我们可以用计算器算一下,24位的地址最大能分配多少个字节呢,这里2的24次方等于这么多个字节数,那除1024等于这么多kb,再除1024=16MB,所以24位地址的最大寻址空间是16MB,那ppt中w25q40到q128使用三字节24位的地址都是足够的,但是这个w25q256就比较尴尬了,24位地址对于32MB来说是不够的,所以这最后一个型号比较特殊,根据手册里描述w25q256分为三字节地址模式和四字节地址模式,在三字节地址模式下,只能读写前16MB的数据,后面16MB 3个字节的地址够不着,要想读写到所有存储单元,可以进入四字节地址的模式这样就行了。

12.2.2 W25Q64硬件电路

看一下这个芯片的硬件电路,当我们拿到这个八脚的芯片后,怎么把它和STM32连接在一起呢,我们看一下,左边这个图是我们这个小模块的原理图,右上角这个图就是这个芯片的引脚定义,右下角这个表就是每个引脚定义的功能了。

首先看一下引脚定义:

  1. VCC、GND是电源供电,引脚供电电压是2.7~3.6V,是一个典型的3.3V供电设备不能直接接入5V电压;
  2. CS引脚,这个CS左边画了个斜杠或者CS上面画了个横线表示低电平有效,那这里CS对应之前我们讲SPI的名称就是SS,意思是SPI的片选引脚,
  3. CLK引脚,对应就是SCK,是SPI的同步时钟线;
  4. DI引脚,对应MOSI,是SPI主机输出从机输入;
  5. DO引脚,对应MISO,是SPI主机输入从机输出;
  6. WP引脚,他的意思是写保护配合内部的寄存器器配置,可以实现硬件的写保护,写保护低电平有效,WP接低电平保护住不让写,WP接高电平不保护可以;
  7. HOLD脚,意思就是数据保持,低电平有效,这个用的不多了解一下,就是如果你在进行正常读写时突然产生中断,然后想用SPI通信线去操控其他器件,这时如果把CS置回高电平,那时序就终止,但如果你又不想终止总线,又想操作其他器件,这就可以HOLD引脚置低电平,这样芯片就HOLD住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来哈,HOLD置回高电平,然后继续HOLD之前的时序,相当于SPI总线进来一次中断,并且在中断里还可以用SPI干别的事情,这就是HOLD的功能。

我们注意到:

这个DI、DO、WP和HOLD,旁边都有括号,写了lO0、lO1、lO2、lO3 ,这个就对应我们刚才这里说的双重SPI和四重SPI,如果是普通的SPI模式,那括号里的都不用看,如果是双重SPI,那DI和DO就变成lO0和lO1,也就是数据同时收和同时发的两个数据位,如果是四重SPI,那就再加上WP当做lO2 ,HOLD当做lO3 ,这四个引脚都作为数据收发引脚,一个时钟四个数据位,了解一下即可暂时不用。

最后看一下左边模块原理图,这个U1就是W25QXX的芯片,J1是一个六脚的排针,然后芯片的VCC电源正极,通过VCC引脚标号接到排针6号脚,芯片GND电源负极,通过GND编号接到排针3号脚,然后芯片SPI通信的四个脚,就直接通过排针引出来就行了,之后HOLD的和WP这两个都是直接接到的VCC,低电平有效,那都接到VCC就这两个功能我们都不用,然后这个C1直接接到VCC和GND,显然是一个电源滤波,R1和D1也是直接接到VCC和GND,显然是一个电源指示灯,通电就亮,那这些就是这个芯片的硬件电路了。

12.2.3 W25Q64框图

(一) W25Q64空间划分

看一下w25q64是怎么划分的。首先右边这一整个矩形空间里是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址,这样说了w25q64的地址宽度是24位3个字节,所以可以看到左下角第一个字节,它的地址是00 00 00h,h代表16进制,之后的空间地址依次自增,直到最后一个字节,地址是7F FF FF h,那最后一个字节为啥是7F开头,不是FF开头呢,因为24位地址最大寻址范围是16MB,我们这个芯片只有8MB,所以地址空间我们只用了一半,8MB排到最后一个字节,就是7F FF FF h,那这是整个地址空间,从000000 ~ 7FFFFF

然后在这整个空间里,我们以64KB为一个基本单元,把它划分为若干的块block,从前往后依次是块0块1块2等等,一直分到最后一块,那整块蛋糕是8MB,以64KB为一块进行划分,最后分得的快数就是8MB除以64kB,这里可以分得128块,那块序号就是块0一直到最后一个是块127。然后观察一下块内地址值的变化规律,比如块0的起始地址是000000,结束地址是00FFFF,之后块31起始是1F0000 ,结束是1FFFFF,之后的都观察一下,可以发现在每一块内,它的地址变化范围就是最低的两个字节,每个块的起始地址是XX0000,结束是XXFFFF,这是块内地址的变化规律,到这里这一块大蛋糕我们就分好块了,总共8MB以64KB为一块总共128块

再看一下左边这个示意图,我们还要再对每一块进行更细的划分,分为多个扇区sector,这里的虚线看到没指向了右边的各个块,也就是告诉你每一块里面都是这个样子的,那在每个块里,它的起始地址是XX0000,结束地址是XXFFFF,在一块里我们再以4KB为一个单元进行切分,一块是64KB,我4KB一切总共16份,所以在每一块里都可以分为扇区0一直到扇区15,观察一下地址规律,可以发现每个扇区内的地址范围是XXX000XXXFFF,地址划分啊到扇区就结束了。

但是当我们在写入数据时啊,还会有个更细的划分,就是页Page,页是对整个存储空间划分的,当然你也可以把它看作在扇区里再进行划分都一样,那页的大小是256个字节,一个扇区是4KB,所以一个扇区里可以分为16页,然后页的地址规律呢我们也看一下,在这里每一行就是一页,左边这里指了个箭头,写的是页地址的开始,右边这里也指了个箭头,写的是页地址的结束,在一页中,地址变化范围是XXXX00XXXXFF,一页内的地址变化,仅限于地址的最低一个字节,这就是页的划分,那这个存储器的地址划分啊我就讲完了,我们需要记住的是一整个存储空间,首先划分为若干块,对于每一块又划分为若干扇区,然后对于整个空间会划分为很多很多页,每页256字节,这个我们需要记住。

(二) W25Q64控制逻辑

左下角是SPI控制逻辑,是芯片内部进行地址锁存、数据读写等操作,都可以由控制逻辑来自动完成,这个不用我们操心,控制逻辑就是整个芯片的管理员,我们有什么事只需要告诉这个管理员就行了。

然后控制逻辑左边就是SPI的通信引脚,有WP、HOLD、CLK、CS、DI和DO,这些引脚就和主控芯片相连,主控芯片通过SPI协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能。

然后去看控制逻辑上面有个状态寄存器器,这个状态寄存器是比较重要的,比如芯片是否处于忙状态、是否写使能、是否写保护等都可以在这个状态寄存器里体现,这个我们等会看手册的时候再来分析。

然后上面是写控制逻辑和外部的WP引脚相连,显然这个是配合WP引脚实现硬件写保护的

注意:右边这里有一个高电压生成器,这个是配合Flash进行编程的,因为Flash是掉电不丢失的,如何实现掉电不丢失呢,比如你点亮一个LED表示1,熄灭LED表示0,但如果整个系统电都没有,那1和0就无从说起了,所以要想掉电不丢失,就要我们在存储器里产生一些刻骨铭心的变化,比如一个LED我给他加很高的电压,那LED就烧坏了,我们用烧坏的LED表示1没烧坏的LED表示0然后再断电,烧坏的LED还是烧坏的,有电没电它都是坏的,这个烧没烧坏的状态,不受有电还是没电的影响,所以它就是掉电不丢失。那对于我们的非易失性存储器来说也是一样,我们要让它产生即使断电也不会消失的状态,一般都需要一个比较高的电压去刺激它,所以这种掉电不丢失的存储器,一般都需要一个高压源,那这里芯片内部集成了高电压发生器,所以就不需要我们在外接高电压了,比较方便哈,当然我这里只是举例简单描述一下掉电不丢失的存储原理,至于Flash的原理,大家可以再例行研究。

然后继续看下面,这里是页地址锁存/计数器,然后下面还有一个字节地址锁存/计数器,这两个地址锁存/计数器就是用来指定地址的,我们通过SPI总共发过来三个字节的地址,因为一页是256(2^8)字节,所以一页内的字节地址就取决于最低一个字节,而高位的两个字节就对应的是页地址,所以在这里我们发的三个字节地址,前两个字节会进到这个页地址锁存计数器里,最后一个字节会进到这个字节地址锁存计数器里。然后页地址通过这个写保护和行解码来选择我要操作哪一页,字节地址通过这个列解码和256字节页缓存,来进行指定地址的读写操作。因为我们这个地址锁存,都是有个计数器的,所以这个地址指针在读写之后可以自动加1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了

那最后右边这里有个256字节的页缓存区,它其实是一个256字节的RAM存储器,这个稍微留个印象,等会儿还会提到,然后我们数据读写,就是通过这个RAM缓冲区域来进行的,我们写入数据会先放到缓存区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的Flash里,进行永久保存。

那为啥要弄个缓冲区呢?我们直接往Flash里写不好吗?

这是因为我们的SPI写入的频率是非常高的,而Flash的写入由于需要掉电不丢失,留下刻骨铭心的印象,它就比较慢,所以这个芯片的设计思路就是你写入的数据,我先放在缓存区里存着,因为缓存区是RAM,所以它的速度非常快啊,可以跟得上SPI总线的速度,但这里有个小问题,就这个缓冲区只有256字节,所以写入的时序有个限制条件,就是写入了一个时序,连续写入的数据量不能超过256字节,然后等你写完了,我芯片再慢慢的把数据从缓冲区转移到Flash存储器里,那么数据从缓存区转到Flash里,需要一定的时间哈,所以在写入时序结束后,芯片会进入一段忙的状态,在这里它就会有一条线哈,通往状态寄存器给状态接容器的busy位置1表示芯片当前正在搬砖呢很忙,那在忙的时候,芯片就不会响应新的读写时序了哈,就是写入的执行流程.

然后我们读取数据,虽然这里画的话应该也是会通过缓冲区来读句,但是由于读取只看一下电路的状态就行了,它基本不花时间,所以读取的限制就很少了,速度也非常快。

12.2.4 Flash操作注意事项

Flash的写入和读取并不像RAM那样简单直接 ,RAM是指哪打哪,想在哪写就在哪写,想写多少就写多少,并且RAM是可以覆盖写入的,但是Flash并没有这个特性啊,总之Flash的读写有很多要求,其中写入的要求是非常多的,需要我们掌握,读取的要求就比较少了,还是那个原因,因为读取啊只是看一下电路的状态,不对电路做出实质性的改变,所以读取一般都比较快,而且没有什么限制,那我们看一下Flash写入操作时需要注意些什么呢

  • 第一点写入操作前必须先进行写使能
    • 这是一种保护措施,防止你误操作的,就像我们使用手机一样,先解锁再操作,这样可以防止手机在你裤兜里到处点点点对吧,写使能的话我们就使用SPI发送一个写使能的指令,就可以完成了,
  • 第二点每个数据位只能由1改写为0,不能由0改写为1
    • 这个意思就是说,Flash并没有像RAM那样的直接完全覆盖改写的能力,比如在某一个字节的存储单元里面,存储了0xAA这个数据,对应的二进制位就是10101010,如果我直接再次在这个存储单元写入一个新的数据,比如我再次写入一个0x55,那写完之后这个存储单元里存的是0x00,因为0x55的二进制是01010101,当这个01010101要覆盖原来的10101010时,就会受到这里第二条规定的限制,每个数据位只能由一改写为零,不能由零改写为一,你要问为啥会有这个限制,那只能说是成本原因或者技术原因,所以这里写入01010101之后,依次来看啊,最高位由原来的1改写为0是可以的,所以写出之后新的最高位就是零,但是第二位原来是零,现在我要改写成1,这是不行的,所以写入之后,新的第二位仍然是零,之后第三位要改写为零,可以,结果为零,第四位零改写为1,不可以,结果仍然是零,那以这个规律进行下去,0xAA在覆盖写入0x55 之后,这个存储单元最终的数据是0x00也就是八位全为零,这就出现问题了对吧,所以为了弥补这个只能1改0,不能0改1的缺陷,就引出了下面的第三条规定。
  • 第三点就是写入数据前必须先擦除,擦除后所有数据位变为一
    • 在这里Flash是有一个擦除的概念的,擦除会有专门的擦除电路进行,我们只要给他发送擦除的指令就行了,那通过擦除电路擦除之后,所有的数据位都变成一,这样我们是不是就可以弥补第二条限制的缺陷了,当我们写出一个数据之前,无论原来存的是什么,我直接给它擦除掉,擦除之后所有的位变成1,也就是16进制的0xFF,这样我无论再写入什么样的数据,就都可以正确的写入了。

那总结一下就是Flash中数据位为一的数据,拥有单项改成零的权利,一旦改写为0之后,就不能反悔再改写成1了,要想反悔就必须得先擦除,所有的位先统一都变成一,然后再重新来过,这是Flash改写的特性。

如果你说我非不擦除,直接改写,这样的操作可以执行,但是存储的数据极有可能是错的,这个注意一下,那拆除之后所有的位变1,就是16进制的FF,所以有时候你读取Flash会发现数据全是FF,那就说明这一段有可能是擦除之后,还没有写入数据的空白空间,在Flash中FF代表空白,那这个改写和擦除的注意事项我们就了解了。

  • 第四点是擦除必须按最小拆除单元进行
    • 这个应该也是为了成本而做出的妥协,就是说你写入前要进行擦除,这我知道,所以如果我想在00这个地址下写入数据,那我就先把00地址擦除,再写入数据到00地址不就行了吗,但是这个方案有个问题啊,Flash的擦除有最小擦除单元的限制,你不能指定某一个直接去擦除,要擦就得一大片一起擦,那在我们这个芯片里,你可以选择整个芯片擦除,也可以选择按块擦除或者按扇区擦除,然后再小就没有了,所以最小的擦除单元就是一个扇区,刚才我们看了一个扇区是4KB就是4096个字节,所以你擦除最少就得4096个字节一起擦,我只想擦除某一个字节怎么办呢,这没办法你只能把那个字节所在扇区的4096个字节全都擦掉,那你又说这个扇区其他的地方我还存的有数据怎么办呢,这也没办法,要想不丢失数据,你只能先把4096个字节都读出来,再把4096个字节的扇区擦掉,改写完读出来的数据后,再把4096个字节全都写回去,这感觉是不是挺麻烦的哈,但是如果你确实就想单独改写某一个字节,那只能这样来操作,当然实际情况下,我们还有别的方法可以优化一下这个流程,比如上电后,我先把Flash的数据读出来放到RAM里,当有数据变动时,我再统一把数据备份到Flash里,或者我把使用频繁的扇区放在RAM里,当使用频率降低时,我再把整个扇区备份到Flash里,或者如果你的数据量确实非常少,只想存几个字节的参数就行了,那直接一个字节占一个扇区不就行了吗,尽显奢靡之风啊。
  • 第五点是连续写入多字节时,最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入
    • 这个意思就是说你在写入的时候,一次性不能写太多了,一个写入时序最多只能写一页的数据也就是256字节,为什么有这个限制呢,这是因为在这里有一个页缓冲区,它只有256字节,为什么有缓冲区呢,这是因为Flash的写入太慢了,跟不上SPI的频率,所以写入的数据会先放在RAM里暂存,等时序结束后,芯片再慢慢的把数据写入到Flash里,所以这里会有个限制,每个时序最多写入一页的数据,你再写多缓冲区存不下了,如果你非要写,那超过页尾位置的数据会回到页首覆盖写入,另外我们这个页缓存区是和Flash的页对应的,你必须得从页起始位置开始写,才能最大写入256字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱哈,所以我们在进行多字节写入时,一定要注意这个地址范围不能跨越页的边缘,否则会地址错乱。
  • 第六点写入操作结束后,芯片进入忙状态,不响应新的读写操作
    • 我们的写入操作都是对缓存区进行的,等时序结束后芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下不要进行新的读写操作,否则芯片是不会响应我们的,要想知道芯片什么时候结束忙状态,我们可以使用读状态寄存器器的指令,看一下状态寄存器的busy位是否为1,为0时芯片就不忙了,我们再进行操作,另外注意这个写入操作,包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等忙状态结束后才能进行后续操作。

继续看读取操作的注意事项,这个就相对宽松很多了,在读取时我们直接调用读取时序,无需使能没有页的限制,也就是这一条连续读取多个字节时,想读多少就读多少,不用担心地址错位或者覆盖的问题,读取操作结束后不会进入忙状态,但不能在忙状态时读取

Flash这种非易失性存储器,目前的市场竞争力还是非常大的,尽管它有这么多不方便,但是这些不方便可以用软件来弥补,而它的优点是其他存储器比不了的,比如容量大价格低。

12.3 软件SPI实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/**
*@brief 从机选择SS
*/
void MySPI_W_SS(uint8_t bit)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)bit);
}
/**
*@brief SPI的时钟SCK
*/
void MySPI_W_SCK(uint8_t bit)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)bit);
}
/**
*@brief 主机输出、从机输入MOSI
*/
void MySPI_W_MOSI(uint8_t bit)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)bit);
}
/**
*@brief 主机输入、从机输出MISO
*/
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

/**
*@brief 软件SPI初始化配置
*/
void MySPI_Init(void)
{
// 1. 开启GPIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

// 2. 初始化配置GPIO
// SS(CS) 片选 —— PA4
// MISO(DO) 从机输出 —— PA6
// MOSI(DI) 从机输入 —— PA7
// SLK(CLK) 时钟 —— PA5
//
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 3. 初始化引脚电平
MySPI_W_SS(1); // 默认不选中从机
MySPI_W_SCK(0); // 使用SPI的模式0,时钟线默认低电平
}

/**
*@brief 软件SPI的起始时序
*/
void MySPI_Start(void)
{
// SS置低电平
MySPI_W_SS(0);
}

/**
*@brief 软件SPI的终止时序
*/
void MySPI_End(void)
{
// SS置高电平
MySPI_W_SS(1);
}

/**
*@brief 软件SPI的交换一个字节时序
*/
uint8_t MySPI_Swap_Byte(uint8_t data)
{
uint8_t i = 0;
uint8_t out_data = 0;
for(i=0; i<8; i++)
{
// 模式0,SS下降沿后主机移出数据到MOSI线上,从机移出数据到MISO线上
// 高位先行,先变化MOSI的电平
MySPI_W_MOSI(data & (0x80>>i));

// 时钟线高电平/上升沿,从机移入MOSI线上数据
MySPI_W_SCK(1);
// 读取从机发送给主机的MISO线上的数据
out_data = out_data | (MySPI_R_MISO() << (8-i));
// 时钟线低电平下降沿移出数据
MySPI_W_SCK(0);
}
return out_data;
}

12.4 SPI外设电路

12.4.1 SPI外设基本介绍

跟之前I2C的思路一样,软件SPI就是我们用代码手动翻转电平来实现时序,硬件SPI就是使用STM32内部的SPI外设来实现时序,两种实现方法各有优势,软件实现主打的是方便灵活,硬件实现主打的是高性能、节省软件资源。

上图是STM32内部SPI外设的一些功能和技术参数的简单介绍,其实手册里介绍这些功能还是比较繁杂的,这是因为硬件电路不像软件那么灵活,硬件电路一旦设计出来,它的功能基本上就定死了,之后只能通过一些开关电路、数据选择器等等来微调电路的运行,不像软件那样改代码就行,所以STM32设计时就要考虑最全面的应用场景,把各种可能的结构都设计出来放在那,以免你用的时候找不到,那这样就会导致外设电路的结构和知识点非常多,而且有很多功能我们基本上很少用到,所以STM32我们要使用主线加分支的学习方法,我们先把最常用最简单的主线知识点给贯通给他学会了,然后再逐渐细化在实践中去慢慢探索这些分支,这样学习起来才是比较容易,所以大家在看手册,有一些感觉非常偏又非常难的知识点,可以先不必深究,先把主线任务学习好,其他的可以之后再研究。

  • 第1条:可配置8位/16位数据帧、高位先行/低位先行
    • SPl最常用的配置就是8位数据帧,高位先行
  • 第2条:时钟频率就是SCK波形的频率
    • 一个SCK时钟交换一个bit,所以时钟频率一般体现的是传输速度,单位是Hz或者bit/s,那这里的时钟频率是fPCLK除以一个分频系数,分频系数可以配置为2或4或8、16、32、64、128、256,所以可以看出来,SPI的时钟其实就是由PCLK分频得来的,PCLK就是外设时钟,APB2的PCLK就是72MHz,APB1的PCLK是36MHz,比如我们的SPI1是APB2的外设,PCLK等于72MHz,那它的SPI时钟频率最大就是只进行二分频=36MHz,像我们之前I2C的频率最大就只有400KH,所以这里SPI的最大频率比I2C快了90倍;
    • 注意频率有些注意事项,一是这个频率数值并不是任意指定的,它只能是PCLK执行分频后的数值就只有这八个选项,最低频率是PCLK的256分频,二是SPI1和SPI2挂载的总线是不一样的,SPI1挂载在APB2,PCLK是72MHz,SPI2挂载在APB1,PCLK是36MHz,所以同样的配置,SPI1的时钟频率要比SPI2的大一倍。
  • 第3条:支持多主机模型、主或从操作
  • 第4条:可精简为半双工/单工通信
  • 第5条:支持DMA
  • 第6条:兼容I2S(一种音频传输协议,与I2C没有什么关系)

12.4.2 SPI外设框图

接下来看一下SPI的框图,我们可以大致把它分成两部分,左上角这一部分就是数据寄存器和移位寄存器打配合的过程,这个和串口、I2C那里的设计思路都是异曲同工的,主要是为了实现连续的数据流,一个个数据前仆后继的一个效果,然后剩下右下角这一部分,就是一些控制逻辑,寄存器的哪些位控制哪些部分会产生哪些效果,这个可以通过手册的寄存器描述来得知,至于执行细节,这里也没详细画,我们就知道功能就行了。

首先左上角核心部分就这个移位寄存器,右边的数据低位,一位一位的从MOSI移出去,然后MISO的数据一位一位的移入到左边的数据高位,显然移位寄存器应该是一个右移的状态,所以目前图上表示的是低位先行的配置,对应右下角有一个LSBFIRST的控制位,这一位可以控制是低位先行还是高位先行,手册里寄存器描述可以查一下,这里LSBFIRST帧格式,给0先发送MSB(MSB就是高位的意思),给1先发送LSB(LSB就是低位的意思),那ppt这里目前的状态LSBFIRST的应该是1,低位先行,如果LSBFIRST给零高位先行的话,这个图还要变动一下,就是移位寄存器变为左移,输出,从左边移出去,输入,从右边移进来,这样才符合逻辑。

然后继续看左边这一块,这里画了个方框,里面把MOSI和MISO做了个交叉,这一块主要是用来进行主从模式引脚变换的,我们这个SPI外设可以做主机,也可以做从机,做主机时这个交叉就不用,MOSI为MO,主机输出,MISO为MI,主机输入,这是STM32作为主机的情况。如果STM32作为从机的话,MOSI为SI,从机输入,这时他就要走交叉的这一路,输入到移位寄存器,同理MISO为SO,从机输出,这时输出的数据也走交叉的这一路输出到MISO,但这里如果这样理解没错的话,这个箭头可能是画错方向了,应该是往下走的,这样才符合逻辑,那这就是这个交叉的作用,简而言之就是主机和从机的输入输出模式不同,如果要切换主机和从机的话,线路就需要交叉一下,当然如果我们始终做主机的话,那这个交叉就不用看了。

接下来上下两个缓冲区实际上就是数据寄存器DR,下面发送缓冲区就是发送数据寄存器TDR,上面接收缓冲区就是接收数据寄存器RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR。写入DR时数据从地址和数据总线写入到TDR,读取DR时,数据从RDR读出到地址和数据总线,数据寄存器和移位寄存器打配合,可以实现连续的数据流。

具体流程就是:

比如我们需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE=1,表示发送寄存器空,当我们检查TXE=1后,紧跟着下一个数据就可以提前写入到TDR里侯着了,一旦上个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器,这里一旦有数据过来了,它就会自动产生时钟将数据移出去,在移出的过程中,MISO的数据也会移入,一旦数据移出完成,数据移入是不是也完成了,这时移入的数据就会整体的从移位寄存器,转入到接收缓冲区RDR,这个时刻会置状态寄存器器的RXNE=1,表示接收计寄存器器非空,当我们检查RXNE=1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR就可以实现连续接收,否则如果下一个数据已经收到了,上个数据还没从RDR读出来,那RDR的数据就会被覆盖,就不能实现连续的数据流了。

和之前串口、I2C的都差不多的,当然这三者也是有一些区别的,比如这里SPI全双工发送和接收同步进行,所以它的数据寄存器发送和接收是分离的,而移位寄存器发送和接收可以共用,然后看一下前面I2C的框图,因为I2C是半双工,发送和接收不会同时进行,所以它的数据寄存器和移位寄存器,发送和接收都可以是共用的。串口是全双工,并且发送和接收可以异步进行,所以这就要求它的数据寄存器,发送和接收是分离的,移位寄存器发送和接收也得是分离的。

然后接下来我们看一下右下角这些内容,这就是一些控制逻辑。

首先是波特率发生器,这个主要就是用来产生SCK时钟的,它的内部主要就是一个分频器,输入时钟是PCLK72M或36M,经过分频器之后输出到SCK引脚,当然这里生成的时钟肯定是和移位寄存器同步的,每产生一个周期的时钟移入移出一个bit,然后右边CR1寄存器的三个位BR0、BR1、BR2,用来控制分频系数,从这里可以看一下,手册这里看到BR[2:0]是波特率控制,这三位写入下面这些值,可以对PCLK时钟执行2~ 256的分频,分频之后就是SCK时钟,所以这一块就对于来之前这里说的时钟频率是fPCLK的2~256分频,那这就是波特率发生器的部分。

接着后面这些通信电路和各种寄存器,都是一些黑盒子电路,如果你要具体研究,可以看一下这些位的寄存器描述,我挑几个重点的讲一下:

  1. LSBFIRST的刚才说过,决定高位先行还是低位先行;
  2. SPE是SPI使能,就是SPI_Cmd()函数配置的位;
  3. BR配置波特率即SCK时钟频率;
  4. MSTR(Master)配置主从模式,1是主模式,0是从模式,一般用主模式;
  5. CPOLCPHA,这个之前讲过,用来选择SPI的四种模式;
  6. SR状态计算器的最后两个个标志位TXE发送寄存器空,RXNE接收寄存器非空,这两个比较重要,我们发送接收数据的时候需要关注这两位;
  7. 最后CR2寄存器就是一些使能位,比如中断使能、DMA使能等
  8. 然后剩下的一些位用的不多,大家可以在自行研究

图示左下角还有一个NSS引脚,其中SS是从机选择,N表示低电平有效,这个NSS和我们想象的从机选择可能不太一样,我们想象的应该是用来指定某个从机对吧,但是根据手册里的描述,这里的NSS设计,可能更偏向于实现这里说的多主机模型,总的来说啊,这个NSS我们并不会用到,SS引脚我们直接使用一个GPIO模拟就行

那这个NSS是如何实现多主机切换的功能呢,我简单介绍一下啊,大家听一听就行,不用掌握

假如这里有三个STM32设备,我们需要把这三个设备的NSS全都连接在一起。首先这个NSS可以配置为输出或者输入,当配置为输出时,可以输出电平告诉别的设备,我现在要变为主机,你们其他设备都给我变从机,不要过来捣乱,当配置为输入时,可以接收别设备的信号,当有设备是主机拉低NSS后,我就无论如何也变不成主机了,这就是它的作用,然后内部电路的设计,当CR2寄存器的SSOE=1时,NSS作为输出引脚,并在当前设备变为主设备时,给NSS输出低电平,这个输出的低电平,就是告诉其他设备,我现在是主机了,当主机结束后,SSOE要清0,NSS变为输入,这时输入信号就会跑到右边这里,这个数据选择器SSM位决定选择哪一路,当选择上面一路时是硬件NSS模式,也就是说这时外部如果输入了低电平,那当前的设备就进入不了主模式了,因为NSS低电平肯定是,外部已经有设备进入了主模式,他已经提前告诉我他是主模式了,我就不能再跟大家抢了,当数据选择器选择下面一路时,是软件管理NSS输入,NSS是1还是2,由SSI来决定,这个就是NSS实现多主机的思路,但这个设计是NSS作为多从机选择的作用消失了,揪出所有人的小辫子之后,主机发送的数据就只能是广播发送给所有人的,如果想实现指定设备通信,可能还需要再加入寻址机制,所以实现起来还是比较复杂的,但我自己其实也没试过这种玩法,这里是根据我看手册的理解,我觉得应该是这样玩的哈,不过SPI最多的情况还是一主多从或者一主一从,我们掌握一主多从就行,多主机的情况了解即可好。

12.4.3 SPI外设抽象框图

那看完了详细的框图,我们再看一下,这里我总结了一个简化结构图,这个结构我把上面这个框图无关的东西都去掉了,这样看起来是不是就更容易理解,其中核心部分,当然就是这个数据寄存器和移位寄存器,这里发送和接收我直接叫做发送数据寄存TDR器,和接收数据寄存器RDR了,因为我觉得这样表示更清晰,之前串口框图里也是这样表示的哈,但是SPI框图这里,它又叫发送缓冲区和接收缓冲区,命名可能不太统一,因为这个手册可能是多个人分工写,最后整合到一起的,所以有时候我就发现手册不同的章节,描述手法和词汇可能都不一样,但是大家要有自己的判断,知道他们其实是一个东西就行。

然后这里移位寄存器,我画的是左移,高位移出去,通过GPIO到MOSI,从MOSI输出,显然这是SPI的主机对,之后引入的数据从MISO进来,通过GPIO到移位寄存器的低位,这样循环八次,就能实现主机和从机交换一个字节,然后TDR和RDR配合,可以实现连续的数据流,这刚才和之前的课程已经分析过很多次了。

另外TDR数据,整体转入移位寄存器的时刻,置TXE标志位,移位寄存器数据整体转入RDR的时刻,置RXNE标志位。

然后剩下的波特率发生器,产生时钟输出到SCK引脚,数据控制器就看成是一个管理员,它控制着所有电路的运行,最后开关控制就是SPI_Cmd(),初始化之后给个ENABLE初始化整个外设。

注意上图中没有画SS从机选择引脚,这个引脚我们还是使用普通的GPIO来模拟即可,在一主多从的模型下,GPIO模拟的SS是最佳选择,这就是SPI的系统框图和简化的结构了,我们在写代码的时候,会用一个结构体来统一配置这些部分。

那初始化部分解决之后,我们就要来看一些运行控制的部分了,如何来产生具体的时序呢,什么时候写DR,什么时候读DR呢,这是我们接下来学习的知识点,读写DR产生时序的流程,我们主要看这两个时序图即可。

12.4.4 SPI外设时序

(一)主模式全双工连续传输

上面的这个图演示的是借助缓冲区数据前仆后继,实现连续数据流的过程,但是这个流程稍微比较复杂,也不太方便封装,所以在实际过程中,如果对性能没有极致的追求,我们更倾向使用下面这个非连续传输的示意图,这个非连续传输使用起来更加简单,实际用的话只需要四行代码就能完成任务了,那参考网上别人的代码呢,基本上都是非连续传输的方式,我们课程也使用非连续传输的代码,非连续传输的好处就是容易封装好理解好用,但是会损失一丢丢性能,连续传输呢传输更快,但是操作起来相对复杂,那我们来分别具体分析一下。

先看一下主模式全双工连续传输的图,图中最上面显示CPOI=1CPHA=1,示例使用的是SPI模式三,所以SCK默认是高电平。

首先SS置低电平开始时序,这个图中没画但是必须得有的,在刚开始时TXE=1,表示TDR空可以写入数据开始传输,然后下面指示的第一步就是软件写入0xF1至TDR/SPI_DR,0xF1就是要发送的第一个数据,之后可以看到写入之后TDR/SPI_DR变为0xF1 ,同时TXE变为0,表示TDR已经有数据了,那此时DR是等候区,移位寄存器才是真正的发送区,移位寄存器刚开始肯定没有数据,所以在等候区TDR里的0xF1 ,就会立刻转入移位寄存器开始发送,转入瞬间置TXE标志位为1,表示发送寄存器空,然后移位寄存器有数据了,波形就自动开始生成。

当然我感觉这里画的数据波形时机可能有点早,应该是在这个时刻b0的波形才开始产生,在这之前数据还没有转入移位进器,所以感觉b0出现的可能过早了,不过这个也不影响我们理解,大家知道这意思就行好了,

这样数据转入移位寄存器之后,数据0xF1的波形就开始产生了,在移位产生0xF1波形的同时,等候区TDR是空的,为了移位完成时,下一个数据能不间断的跟随,这里我们就要提早把下一个数据写入到TDR里等着了,所以下面只是第二步的操作,是写入0xF1之后,软件等待TXE等于1,在这个位置,一旦TDR空了,我们就写入0xF2至TDR/SPI_DR,写入之后可以看到TDR的内容就变成0xF2了,也就是把下一个数据放到TDR里,后者之后的发送流程也是同理,最后在这里如果我们只想发送三个数据,0xF3转入移位寄存器之后,TXE等于1,我们就不需要继续写入了,TXE之后一直是1。注意在最后一个TXE等于1之后,还需要继续等待一段时间,最后一个数据0xF3的波形才能完整发送完,等波形全部完整发送之后,busy的标志由硬件清除,这才表示波形发送完成了。那这些就是发送的流程。

然后继续看一下下面接收的流程,SPI是全双工,发送的同时还有接收,所以可以看到在第一个字节发送完成后,第一个字节的接收也完成了,图中接收到的数据1是0xA1 ,这时移位寄存器的数据整体转入RDR,RDR随后存储的就是0xA1 ,转入的同时按RXNE标志位也置1,表示收到数据了,我们的操作是下面这里写的,软件等待RXNE等于1,=1表示收到数据了,然后从SPI_DR也是RDR读出数据A1 ,这是第一个接收到的数据,接收之后软件清除RXNE标志位,然后当下一个数据2收到之后,RXNE重新置1,我们监测到RXNE等于1时就继续读出RDR,这是第二个数据0xA2 ,最后在最后一个字节时序完全产生之后,数据三才能收到,所以数据3,直到这里才能读出来。

注意一个字节波形收到后,移位寄存器的数据自动转入RDR,会覆盖原有的数据,所以我们读出RDR要及时,比如A1这个数据收到之后,最迟你也要在这里把它读走,否则下一个数据A2覆盖A1,就不能实现连续数据流的接收了。

这是整个发送和接收的流程这个交换的流程是交错的,对我们程序设计不太友好,总之如果你对效率要求很高,就研究下这个,否则的话,我们更推荐下面这个非连续传输。

(二)非连续传输

非连续传输对于程序设计非常友好,只需要四行代码就可以完成,那它是怎么执行的呢,我们来看一下,上图是非连续传输发送的示意图,下面这里只有发送的一些波形,接收部分的波形没画出来,但是我们也可以想象得到接收是什么样子的,等会儿我也会给大家展示一下接收的波形,看一下这个非连续传输和连续传输有什么区别:

首先这个配置还是SPI模式三,SCK默认高电平,我们想发送数据时如果检测到TXE等于1了,TDR为空,就软件写入0xF1至TDR/SPI_DR,这时TDR的值变为0xF1TXE变为0,目前移位寄存器也是空,所以这个0xF1会立刻转入移位寄存器,开始发送,波形产生并且,TXE置回1,表示你可以把下一个数据放在TDR里侯着了,但是现在区别就来了,在连续传输这里一旦TXE等于1了,我们就会把下个数据写到TDR里侯着这样是为了连续传输数据衔接更紧密,但是刚才说了,这样的话,流程就比较混乱,程序写起来比较复杂。

所以在非连续传输这里,TXE等于1了,我们不着急把下一个数据写进去,而是一直等待,等第一个字节时序结束,在这个位置时序结束了,意味着接收第一个字节也完成了,这时接收的RXNE会置一,我们等待RXNE置1后,先把第一个接收到的数据读出来,之后再写入下一个字节数据,也就是这里的软件等待TXE等于1,但是较晚写入0xF2到TDR/SPI_DR,之后数据二开始发送,我们还是不着急写数据三,等到了这里,先把接收的数据二收着,再继续写入数据3,数据3时序结束后,最后再接收数据三置换回来的数据。

你看按照这个流程的话,我们的整个步骤就是

  1. 第一步等待TXE=1
  2. 第二步写入发送的数据至TDR;
  3. 第三步等待RXNE=1
  4. 第四步读取RDR接收的数据,之后交换第二个字节

重复这四步,那这样我们就可以把这四部分装到一个函数,调用一次交换一个字节,这样程序逻辑是不是就非常简单了,和之前软件SPI的流程基本上是一样的,我们只需要稍作修改,就可以把软件SPI改成硬件SPI,那非连续算出缺点,就是在这个位置没有及时把下一个数据写入TDR侯着,所以等到第一个字节时序完成后,第二个字节还没有送过来,那这个数据传输就会在这里等着,所以这里时钟和数据的时序,在字节与字节之间会产生间隙,拖慢了整体数据传输的速度这个间隙在SCK频率低的时候影响不大,但是在SCK频率非常高时隙拖后腿的现象就比较严重了,比如我这里用示波器看了一下,不同SCK频率间隙的影响。

12.5 硬件SPI实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...

/**
*@brief 硬件SPI的交换一个字节时序
*/
uint8_t MySPI_Swap_Byte(uint8_t data)
{
uint8_t out_data = 0;
// 等待TXE标志位为1
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
// 写入数据到TDR
SPI_I2S_SendData(SPI1, data);
// 等待RXNE标志位为1
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);
// 读取RDR的数据
out_data = SPI_I2S_ReceiveData(SPI1);

return out_data;
}

...

13 BKP存储+RTC实时时钟

这一章节主要是讲解RTC实时时钟这个外设的使用。实时时钟本质上是一个定时器,但是这个定时器有专门的用途——是专门用来产生年月日时分秒这种日期和时间信息的。所以RTC相当于STM32内部一个独立运行的钟表,想要记录和读取日期时间信息,就可以通过RTC实现。

RTC这个集成在STM32内的外设电路比较特殊,它和备份寄存器BKP外设、电源控制PWR外设的关联性比较强,所以把BKP和RTC放在一起讲解,PWR电源控制下一节单独讲。

其实,备份寄存器BKP和上一节学习的Flash存储器类似,都是用来存储数据的,只是Flash的数据是真正的掉电不丢失,而BKP的数据需要VBAT引脚接上备用电池来维持的,只要VBAT有电,即使STM32主电源断电,BKP的值也可以保存。也就是说BKP本质是RAM存储器

13.1 Unix时间戳

13.1.1 Unix时间戳简介

Unix时间戳最早是在Unix系统使用的,之后很多由Unix演变而来的系统也都继承了Unix时间戳的规定,目前Linux、Windows、安卓都是使用Unix时间戳。

根据上图中的第一条可知,Unix时间戳是一个数值,这个数值表示的是一个从1970年1月1日0时0分0秒开始到现在总共所经过的秒数,所以时间戳这个计时系统和我们常用的年月日时分秒这个计时系统有很大差别。

时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量,计算机为了存储这样一个永不进位的秒数,这个数据变量类型还是要定义大一些。

32位有符号数所能表示的最大数字是2^32/2-1这个数是21亿多,这其实是有溢出风险的,因为目前到2023年时间戳已经计到16亿了,32位有符号数的时间戳会在2038年的1月19号溢出,64位的时间戳能存储的时间范围非常非常的大,看下手册STM32它核心的计时部分,是一个32位的可编程计数器,这说明我们这款stm32 ,它的时间戳是32位的数据类型,这表示我们这个STM32也会在2038年出现bug吗,实际上并不会啊,因为根据我的研究,这个时间戳在STM32程序中定义的其实是无符号的,要到2106年才会溢出。

地球上不同经度,它的时间是不一样的,穿过英国伦敦的经线叫作本初子午线,这个位置的时间是一个时间标准,时间戳所说的1970年1月1日0时0分0秒也是指的伦敦时间的0时0分0秒。其他地方可以分为24个时区,每偏差一个时区,时间就要加或减一个小时(伦敦的东侧加1,伦敦的西侧减1)。

部分知识点补充说明:

为什么说GMT是以前的时间标准呢?这是因为GMT有一个棘手的问题,就是地球自转一周的时间其实是不固定的,由于潮汐力、地球活动等原因,地球目前是越转越慢的。

UTC协调世界时是一种以原子钟为基础的时间计量系统,原子钟是当前计时最精确的装置,上千万年才误差一秒,那现在问题又来了,我们以一个恒定不变的秒来计时,但是地球自转越来越慢,这样记下去,计时的一天和自转的一天就会出现偏差,时间长一些,可能中午12点太阳就不是最高的位置,所以在原子钟计时系统的基础上,我们得加入闰秒的机制,来消除计时一天和地球自转一周的误差。

闰秒的操作流程是:当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致(简单说,因为根据当前天文观察,地球是越转越慢的,因此以UTC这种固定的计时系统,不加入闰秒动态调整的话,一天的时间就会超过24小时,不加以处理,长此以往时间就乱了,因此每超过0.9s,计时系统就多走1s来等一下地球自转)。

恒定的时间标准加上闰秒机制的设计,就能保证UTC既满足科学研究的需要,又满是人类生活的需要。但是闰秒机制的设计,可能也会造成一些程序bug,所以大家要有这个准备,就是1分钟可能会出现61秒的情况。

STM32一般不用于精确的科学研究,因此不考虑闰秒,这样就导致每次产生闰秒时,STM32的时间戳的时间和国家授时中心的标准时间就会产生1秒的偏差,这个了解一下。

13.1.2 C语言time.h库的使用介绍

使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// struct tm {
// int tm_sec; /* 秒,范围从0 到59 */
// int tm_min; /* 分,范围从0 到59 */
// int tm_hour; /* 小时,范围从0 到23 */
// int tm_mday; /* 一月中的第几天,范围从1 到31 */
// int tm_mon; /* 月,范围从0 到11 */
// int tm_year; /* 自1900 年起的年数 */
// int tm_wday; /* 一周中的第几天,范围从0 到6 */
// int tm_yday; /* 一年中的第几天,范围从0 到365 */
// int tm_isdst; /* 夏令时 */
// };
int main(int argc, char const *argv[])
{
time_t Time; // 时间戳
struct tm Date;
// Time = time(NULL);
time(&Time);
printf("Time = %ld\n", Time); // Time = 1695695610
Date = *localtime(&Time);
printf("%d\n", Date.tm_year + 1900);
printf("%d\n", Date.tm_mon + 1);
printf("%d\n", Date.tm_mday);
printf("%d\n", Date.tm_hour);
printf("%d\n", Date.tm_min);
printf("%d\n", Date.tm_sec);
printf("%d\n", Date.tm_wday);
printf("%d\n", Date.tm_yday);
printf("%s\n", ctime(&Time));
printf("%s\n", asctime(&Time));
puts(asctime(&Date));
char t[50];
strftime(t, 50, "%H-%M-\%S", &Date);
printf(t);
return 0;
}

13.2 BKP外设

13.2.1 BKP简介

TAMPER引脚检测到产生的侵入事件将所有备份寄存器内容清除,TAMPER是一个接到STM32外部的引脚,它的位置可以看一下引脚定义,这个TAMPER是一个安全保障设计,比如如果你做一个安全系数非常高的设备,设备需要有防拆功能,然后BKP里也存储了一些敏感数据,这些数据不能被别人窃取或者篡改,那你就可以使用这个TAMPER引脚的侵入检测功能,设计电路时,TAMPER引脚可以先加一个默认的上拉或者下拉电阻,然后引一根线到你的设备外壳的防拆开关或触点,别人一拆开你的设备触发开关,就会在TAMPER引脚产生上升沿或者下降沿,这样STM32就检测到侵入事件了,这时BKP的数据会自动清零,并且申请中断,你在中断里还可以继续保护设备,比如清除其他存储器数据,然后设备锁死,这样来保障设备的安全,另外主电源断电后,侵入检测仍然有效,这样即使设备关机也能防拆。

RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲,RTC引脚刚才看过了也是在PC13这个位置,这就是RTC时钟输出的功能,RTC的校准时钟、闹钟或者秒脉冲的信号可以通过RTC引脚PC13输出,其中外部用设备测量RTC校准时钟,可以对内部RTC微小的误差进行校准,然后闹钟脉冲或者秒脉冲可以输出出来,为别的设备提供这些信号,这是RTC时钟输出的功能,因为PC13、TAMPER和RTC这三个引脚共用一个端口,所以这三个功能同一时间只能使用一个

存储RTC时钟校准寄存器,这个可以配合上面这个校准时钟输出的功能,结合一些测量方法,可以对RTC进行校准,那这两个功能实际上就是RTC的配置,我觉得放在RTC那个外设的地方应该比较合适,当然RTC和BKP关联程度比较高,设计者目前就是把这两个RTC的功能放在BKP里了。

最后看一下BKP中用户数据的存储容量,在中容量和小容量设备里,BKP是20个字节,在大容量和互联型设备里,BKP是84个字节,我们使用的C8T6是中容量设备,所以可以看出BKP的容量其实非常小,一般只能用来存储少量的参数。

13.2.2 BKP的抽象结构

看一下BKP的基本结构,这个图中橙色部分我们可以叫做后备区域,BKP处于后备区域,但后备区域不只有BKP,还有RTC的相关电路也位于后备区域,STM32后备区域的特性就是当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会切换到VDD,主电源有电时VBAT不会用的,这样可以节省电池电量

然后BKP是位于后备区域的,BKP里主要有数据寄存器、控制寄存器、状态寄存器和RTC时钟校准寄存器这些东西,其中数据寄存器是主要部分,用来存储数据的,每个数据寄存器都是16位的,也就是一个数据寄存器可以存两个字节,那对于中容量和小容量的设备,里面有dr1、dr 2一直到dr10总共十个数据寄存器,那一个寄存器存两个字节,所以容量是20个字节。

然后BKP还有几个功能,一是图中的侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或者下降沿时,清除BKP所有的内容以保证安全;二是图中的时钟输出,可以把RTC的相关时钟,从PC13位置的RTC引脚输出数据供外部使用,其中输出较准时钟时,再配合这个校准寄存器,可以对RTC的误差进行校准。

13.3 RTC外设

13.3.1 RTC简介

之前我们51单片机学过DS1302这个芯片,DS1302是外置的RTC芯片,这个芯片可以独立计时,需要设置时间或读取时间就通过通信协议向它发送或接收数据来完成。在STM32内部有这个RTC的外设硬件电路,所以STM32可以在内部直接实现RTC的功能,无需外挂芯片。

20位的可编程预分频器,可适配不同频率的输入时钟,保证分频器输出给计数器的频率为1Hz,这样计时才正确。

RTC的时钟源有3个,可选择其中一个接入到下图中的RTCCLK引脚。

补充,下面是时钟树中接入到RTC部分的框图:

RTCCLK有三个来源:

  1. 第一个是OSC引脚接的HSE外部高速晶振,这个晶振是主晶振,我们一般都用的8MHZ,通过128分频可以产生RTCCLK信号,为什么要先128分频,这是因为这个8MHz的晶振太快了,如果不提前分频,直接给RTCCLK,后续即使再通过RTC的20位分频器,也分不到1Hz这么低的频率。
  2. 然后中间这一路时钟来源是LSE,外部低速晶振,我们在OSC32IN和OSC32OUT这两个引脚,接上外部低速晶振,这个晶振产生的时钟,可以直接提供给RTCCLK,这个OSC32的晶振是内部RTC的专用时钟,这个晶振的值也不是随便选的,通常跟RTC有关的晶振都是统一的数值,就是32.768KHz,为什么选择这个数值呢,一方面是32.768KHz这个值附近的频率是晶振工艺比较合适的频率,你要说非要做一个1Hz的晶振,那可能是做不出来或者做出来了但体积很大性能很差,另一方面是32768这是一个二的次方数2的15次方等于32768,所以32.768KHz经过一个15位分频器的自然溢出,就能很方便地得到1Hz的频率,自然溢出的意思就是设计一个15位的计数器,这个计数器不用设置计数目标,直接从0计到最大值,就计得32767,计满后自然溢出,这个溢出信号就是1Hz,自然溢出的好处就是不用再额外设计一个计数目标了,也不用比较计数是不是计到目标值,这样可以简化电路设计,所以目前在RTC电路中,基本都是清一色的32.768KHz的晶振,你只要看到32.768KHz的晶振八成就是提供给RTC的,这是第二路。
  3. 最后看第三路时钟源,来自于LSI,内部低速RC振荡器,LSI固定是40KHz,如果选择LSI当做RTCCLK,后续再经过40k的分频,就能得到1Hz的计数时钟了,当然内部的RC振荡器,一般精度没有外部晶振高,所以LSI给RTCCLK,l可以当做一个备选方案,另外LSI还可以提供给看门狗。

我们最常用的就是中间这一路,外部32.768KHz的晶振,提供RTCCLK的时钟,第一个原因就是中间这一路,32.768KHz的晶振本身就是专供RTC使用的,上下这两路其实是有各自的任务,上面这一路主要作为系统主时钟,下面这一路主要作为看门狗时钟,他们只是顺带可以备选当做RTC的时钟,另外一个更重要的原因,只有中间这一路的时钟可以通过VBAT备用电池供电,上下两路时钟在主电源断电后是停止运行的,所以要想实现RTC主电源掉电继续走时的功能,必须得选择中间这一路的RTC专用时钟,如果选择的是上下两路时钟,主电源断电后时钟就暂停了,这显然会导致走时出错。

13.3.2 RTC电路框图

  • RTC由两部分组成
    • APB1接口:用来和APB1总线相连。通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值,闹钟值)。
    • RTC核心:由一组可编程计数器组成。分两个主要模块。 第一个是RTC预分频模块,它可以编程产生最长1秒的RTC时间基TR_CLK。如果设置了秒中断允许位,可以产生秒中断。 第二个是32位的可编程计数器,可被初始化为当前时间。系统时间按TR_CLK周期累加并与存储在RTC_ALR寄存器中的可编程时间相比,当匹配时候如果设置了闹钟中断允许位,可以产生闹钟中断。

RTC内核完全独立于APB1接口,软件通过APB1接口对RTC相关寄存器访问。但是相关寄存器只在RTC APB1时钟进行重新同步的RTC时钟的上升沿被更新。所以软件必须先等待寄存器同步标志位(RTC_CRL的RSF位)被硬件置1才读。

接下来我们来看一下这个RTC的框图,先整体上划分一下:

  • 左边这一块是核心的分频和计数计时部分
  • 右边这一块是中断输出使能和NVIC部分
  • 上面这一块是APB1总线读写部分
  • 下面这块是PWR关联的部分,意思就是RTC的闹钟可以唤醒设备,退出待机模式

然后在图中我们看到有灰色填充的部分,都处于后备区域,这些电路在主电源掉电后,可以使用备用电池维持工作,另外这里还写了,这些模块在待机时都会继续维持供电,其他未被填充的部分就是待机时不供电,有关睡眠停机待机在低功耗相关的内容,我们下节学PWR的时候再来细讲。

  1. 首先,看分频和计数器计数部分
    • 这一块的输入时钟是RTCCLK,RTCCLK的来源需要在RCC里进行配置,有3种选择,主要选择中间一路,那因为这三路时钟频率各不相同,而且都远大于我们所需要的1Hz的秒计数频率,所以RTCCLK进来,首先需要经过RTC预分频器进行分频,这个分频器由两个寄存器组成,上面这个是重装载寄存器RTC_PRL,下面这个RTC_DIV手册里叫做余数寄存器,但实际上这一块跟我们之前定时器时基单元里的计数器CNT和重装值ARR是一样的,可能是右边已经有一个计数器CNT了,所以这个名字就比较奇怪,叫做余数寄存器,但实际上它还是计数器的作用。
    • 分频器其实就是一个计数器,记几个数溢出一次就几分频,所以对于可编程的分频器来说,需要有两个寄存器,一个寄存器用来不断的计数,另一个寄存器我们写入一个计数目标值,用来配置是几分频,那在这里上面这个RTC_PRL就是计数目标,我们写入六那就是七分频,写九那就是十分频,因为计数指包含了零,然后下面这个RTC_DIV就是每来一个时钟计一个数的用途了,当然这个RTC_DIV计数器啊是一个自减计数器,每来一个输入时钟,RTC_DIV的值自减一次,自减到0时再来一个输入时钟,RTC_DIV输出一个脉冲产生溢出信号,同时RTC_DIV从RTC_PRL获取重装值,回到重装值继续自减。
  2. 然后,看一下计数计时部分
    • 这一块就比较简单了,32位可编程计数器,RTC_CNT就是计时最核心的部分,可以把这个计数器看作是Unix时间戳的秒计数器,这样借助time.h的函数,就可以很方便地得到年月日时分秒了;
    • 然后在下面,这个RTC还设计的有一个闹钟寄存器RTC_ALR,这个RTC_ALR也是一个32位的寄存器,和上面这个RTC_CNT是等宽的,它的作用顾名思义就是设置闹钟,我们可以在RTC_ALR写一个秒数设定闹钟,当RTC_CNT的值跟RTC_ALR设定的闹钟值一样时,也是这里画了等号啊,若它俩值相等就代表闹钟响了,这时就会产生RTC_Alarm闹钟信号,通往右边的中断系统,在中断函数里你可以执行相应的操作。
    • 同时这个闹钟还兼具一个功能,就下面这里的闹钟信号,可以让STM32退出待机模式,这个就可以对应一些用途,比如你设计一个数据采集设备,需要在环境非常恶劣的地方工作,比如海底高原深井这些地方,然后要求是每天中午12点采集一次环境数据,其他时间为了节省电量避免频繁换电池,芯片都必须处于待机模式,这样的话我们就可以用这个RTC自带的闹钟功能,定一个中午12点的闹钟,闹钟一响芯片唤醒采集数据完成后继续待机,另外这个闹钟值是一个定值,只能响一次,所以如果你想实现周期性的闹钟,大家每次闹钟响之后都需要再重新设置一下下一个闹钟时间,就是这个闹钟和闹钟唤醒的一个用途。
  3. 接着是中断部分,在左边这里有三个信号可以触发中断:
    • 第一个是RTC_Second秒中断,它的来源就是CNT的输入时钟,如果开启这个中断,那么程序就会每秒进一次RTC中断;
    • 第二个是RTC_Overflow溢出中断,它的来源是CNT的右边,意思就是CNT的32位计数器计满溢出来了,会触发一次中断,所以这个中段一般不会触发,我们上一节说过,这个CNT定义的是无符号数,到2106年才会溢出,所以这个中段在2106年会触发一次,如果你想程序更完善一些,可以开启这个中断,到2106年就是一溢出,为了避免不必要的错误,你可以让芯片罢工,然后提示当前设备过老,请及时更换,但在2106年之后这个STM32的RTC就不太好用了,到时候或许可以通过打补丁的方式继续运行,或者直接淘汰32位的时间戳,这个问题就留给后人解决吧。
    • 第三个RTC_Alarm闹钟中断,刚才说过,当计数器和闹钟值相等时触发中断,同时闹钟信号可以把设备从待机模式唤醒,这是这三个中断信号.
    • 中断信号到右边这里,这一块就是中断标志位和中段输出控制,这些F结尾的是对应的中断标志位IE结尾(Interrupt ENABLE)的是中断使能,最后三个信号通过一个或门汇聚到NVIC中断控制器,这个地方是不是漏画了一根线,中间这个应该也是要通过或门的,好这是右边的中断部分;
  4. 上面这部分APB1总线和APB1接口,就是我们程序读写寄存器的地方
    • 读写计算器可以通过APB1总线来完成,另外也可以看出,RTC是APB1总线上的设备;
  5. 最后,下面这一块退出待机模式
    • 有一个Wake Up引脚(WKUP Pin),闹钟信号和Wake Up引脚都可以唤醒设备,Wake Up引脚可以看一下接线图,就这里PA0的位置它兼具唤醒的功能,这个我们下一节再学习。

13.3.3 RTC抽象结构

接下来看一下我这里给的基本结构,再总结一下,RTC的核心部分如上图所示。

  1. 最左边是RTCCLK时钟来源,这块需要在RCC里配置,三个时钟选择一个当做RTCCLK,之后RTCCLK先通过预分频器对时钟进行分频,余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后得到1Hz的秒计数信号通向32位计数器,一秒自增一次。
  2. 下面还有个32位的闹钟值,可以设定闹钟,如果不需要闹钟的话,下面这一块可以不用管。
  3. 然后右边有三个信号可以触发中断,分别是秒信号、计数器溢出信号和闹钟信号,三个信号先通过中断输出控制进行中断使能,使能的中断才能通向NVIC,然后向CPU申请中断。

在程序中我们配置这个数据选择器,可以选择时钟来源,配置重装寄存器可以选择分频系数,配置32位计数器可以进行日期时间的读写,需要闹钟的话,配置32位闹钟值即可,需要中断的话,先允许中断,再配置NVIC,最后写对应的中断函数即可,这是RTC外设的主要内容。

13.3.4 RTC外部辅助电路

为了配合STM32,RTC外部还是需要有一些电路的,在最小系统电路上,外部电路还要额外加两部分:

  1. 第一部分就是备用电池
  2. 第二部分就是外部低速晶振
  • 备用电池供电部分

上图这里给了两个参考电路,第一个是简单连接,就使用一个3V的电池,负极和系统工地,正极直接接到STM32的VBAT引脚,这样就行了,这个供电方案非常简单,参考来源是STM32的数据手册,在5.1.6供电方案这里就给出来这个图,图上画的就是直接建一个1.8~3.6V的电池到VBAT就行了。

注意,图中可以看到:

在内部是有一个供电开关的,当VDD有电时,开关拨到下面,后备电路由VDD供电,当VDD没电时,开关拨到上面,后备电路由VBAT供电,然后VBAT供电的设备,在这里写了,VBAT供电的后备电路有32KHz振荡器、RTC、唤醒电路和后备寄存器,那这就是根据数据手册里设计的VBAT供电方案,这个设计非常简单一般来说也没问题。

然后我这里还给了第二种方案是推荐连接,这种连接方法是电池通过二极管D1向VBAT供电,另外主电源的3.3V,也通过二极管D2向VBAT供电,最后VBAT再加一个0.1uf的电源滤波电容,这个供电方案的参考来源是STM32的参考手册,在这个4.1.2电池备份区域这一节有这样描述,大家可以都看看,其中手册里有几个建议,一个是在这些这些情况下,电流可能通过VDD和VBAT之间的内部二极管注入到VBAT,如果与VBAT连接的电源或者电池,不能承受这样的注入电流,强烈建议在外部,VBAT和电源之间连接一个低压降的二极管,另一个是如果在应用中没有外部电池,建议VBAT在外部连接到VDD,并连接一个100nf的陶瓷滤波电容,所以综合这两条建议,我们可以设计出右边的推荐连接,电池和主电源都加一个二极管,防止电流倒灌,VBAT加一个0.1uf的电源滤波电容,0.1uf就是100nf,如果没有备用电池,就3.3V的主电源供电,如果接了备用电池,3.3v没电时,就是备用电池供电,这是根据参考手册设计的推荐电路,如果你只是进行实验,那使用左边的简单连接就行了,如果你要画板子设计产品,那还是推荐使用右边的连接,这样更保险,这是VBAT供电部分。

  • 外部低速晶振部分

然后继续看一下右边的外部低速晶振部分,这是一个典型的晶振电路了,这里X1是一个32.768KHz的RTC晶振,这个晶振不分正负极,两端分别接在OSC32 这两个引脚上,然后进这两端再分别接一个启动电容到GND,这个电路的设计,参考来源还是stm32的数据手册,在5.3.6外部时钟源特性这里有参考电路,使用一个晶体或陶瓷谐振器产生的低速外部时钟,下面这里就是典型电路,晶振是32.768KHz,CL1和CL2上面这里写了,对于CL1和CL2,建议使用高质量的5pF~15pF之间的瓷介电容器,所以对于硬件电路的设计,但还是得多看看手册,手册看多了自然就会了,所以在这里我给出的晶振电路是这样的,起振电容给的是10pF。

最后看一下右边的图片,这个备用电池,我们一般可以选择这样的3V纽扣电池,型号是CR2032,这是一个非常常用的纽扣电池型号,另外注意这个纽扣电池印制的这一面是正极,这里也有个正号标注,另一面比较小的那个电极是负极,然后32.768KHz的晶振,我们可以选择这样的一个金属壳柱状体的晶振,这个晶振也是比较常见,大家拆开钟表电子表基本上都能找到这样一个元件,这是32.768KHz的晶振,晶振的全称是石英晶体振荡器,所以我们常说的石英钟,名称就来源于这样一个元件,然后下面这个是我们的最小系统板,这个板子自带的有RTC晶振电路,这里这个黑色的元件写的有32.768KHz,这个也是一种样式的RTC晶振,然后旁边这个金属壳柱状体是8MHz的外部高速晶振,不过我们这个板子没有自带备用电池,VBAT引脚直接通过右上角的这个端口引出来了,如果需要备用电池的话,可以接在这里,以上就是RTC的硬件电路部分。

13.3.5 RTC的操作注意事项

  • 注意点1
    • 设置寄存器RCC_APB1ENRPWRENBKPEN位,使能PWR和BKP这两个外设的时钟,设置PWR_CRDBP,使能对BKP和RTC的访问。
    • 该注意点就是提醒一下,正常的外设第一步开启时钟就能用了,但是BKP和RTC这两个外设,开启稍微复杂一些,如果你要使用BKP或者RTC,都要先执行这两步,第一步开启PWR和BKP的时钟,第二步使用PWR使能BKP和RTC的访问,对应库函数PWR_BackupAccessCmd(),这个我们在初始化的时候需要注意一下,按照这个流程来就行了。
  • 注意点2
    • 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1,这一步对应代码里的一个库函数就是RTC等待同步,一般在刚上电的时候调用一下这个函数就行了。
    • 为什么要有这一步呢,可以看看框图,在这里会有两个时钟,PCLK1和RTCCLK,PCLK1在主电源掉电时会停止,所以为了保证RTC主电源掉电正常工作,RTC里的寄存器,都是在RTCCLK的同步下变更的,当我们用PCLK驱动的总线去读取RTCCLK驱动的寄存器时,就会有个时钟不同步的问题,RTC寄存器只有在RTCCLK的上升沿更新,但是PCLK1的频率36MHz远大于RTCCLK的频率32KHz。如果我们在APB1刚开启时,就立刻读取RTC寄存器,有可能RTC寄存器还没有更新到APB1总线上,这样我们读到的值就是错误的。通常来说就读取到0,所以这就要求我们在APB1总线刚开机时,要等一下RTCCLK,只要RTCCLK来一个上升沿,RTC把它的寄存器的值同步到APB1总线上,这样之后读取的值就都是没问题的了,这是设计细节的一个问题,当然我们其实也不用管那么多了,只需要在初始化时调用一个等待同步的函数就行了。
  • 注意点3
    • 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器,这一条其实比较简单,就是RTC会有一个进入配置模式的标志位,把这一位置1才能设置时间,其实这个操作在ST公司提供的库函数中,每个写寄存器的函数都自动帮我们加上了这个操作,所以我们就不用再单独调用函数进入配置模式了。
  • 注意点4
    • 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器,这个操作也是调用一个等待的函数就行了,跟我们之前读写Flash芯片是类似的,就写入之前要等待一下,如果上一次的写入还没完成,你就别急着写下一次了,或者说每次写入之后,你要等待RTOFF为1,只有RTOFF为1才表示写完成。
    • 为什么要有这个操作呢,其实还是因为这里的PCLK1和RTCCLK时钟频率不一样,你用PCLK1的频率写入之后,这个值还不能立刻更新到RTC的寄存器里,因为RTC寄存器是由RTCCLK驱动的,所以PCLK1写完之后,得等一下RTCCLK的时钟,RTCCLK来个上升沿,值更新到RTC寄存器里,整个写作过程才算结束了,这个操作了解一下,在代码里也就是调用一个等待函数的事。

13.4 RTC实验

上图来自链接:STM32 入门系列教程——实时时钟RTC编程

14 PWR电源控制

14.1 PWR简介

PWR有一部分是硬件电路的介绍,就是告诉你,内部供电电路的结构是啥样的,这些是设计硬件电路时要考虑的,暂时不涉及程序。涉及程序的内容,主要就是两个:① 可编程电压监测器、② 低功耗模式的功能。

可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阀值以下或上升到PVD阀值之上时,PVD会触发中断,用于执行紧急关闭任务。这个功能预想的场景应该是使用电池供电,或者对安全要求比较高的设备,如果供电电压在逐渐下降,在电压过低的情况下,可能会导致内部或外部电路发生不确定的错误,为了避免不确定的因素,在电源电压低于设定的阈值时,我们可以主动出击,提前发出警告,并且关闭比较危险的设备,这是这个PVD的设计,不过PVD这个功能不是我们本节课的重点哈,我们暂时也不演示代码。

低功耗模式包括睡眠模式(Sleep)停机模式(Stop)待机模式(Standby)。在低功耗模式下,我们也需要保留必要的唤醒电路,比如串口接收数据的中断唤醒,外部中断唤醒,RTC闹钟唤醒等,在需要设备工作时,STM32能够立刻重新投入工作,如果你只考虑进入低功耗,而不考虑唤醒STM32,那不就跟直接断电没区别了吗,所以低功耗模式我们要考虑关闭哪些硬件,保留哪些硬件以及如何去唤醒,当然关闭越多的硬件设备越省电,唤醒就越麻烦。

14.2 STM32内部供电

这个图就是STM32内部的供电方案,整体上看这个图可以分为三个部分,最上面是模拟部分供电叫做VDDA,中间是数字部分供电,包括两块区域,VDD供电区域和1.8V供电区域,最下面是后备供电,叫做VBAT。下面依次看一下:

  1. VDDA供电区域
    • 主要负责模拟部分的供电,其中包括AD转换器、温度传感器、复位模块、PLL锁相环,这些电路的供电正极是VDDA,负极是VSSA,其中AD转换器还有两个参考电压的供电脚,叫做VREF+VREF-,这两个脚在引脚多的型号里会单独引出来,在引脚少的型号,比如我们这个C8T6,VREF+VREF-在内部就已经分别接到了VDDA和VSSA了。
  2. 数字部分供电
    • 这一块由两部分组成,左边部分是VDD供电区域,其中包括IO电路、待机电路、唤醒逻辑和独立看门狗,右边部分是VDD通过电压调节器降压到1.8V,提供给后面这一块的1.8V供电区域,1.8V区域包括CPU核心、存储器和内置数字外设。
    • 可以看出来STM32内部的大部分关键电路,CPU、存储器和外设其实都是以1.8V的低电压运行的,当这些外设需要与外界进行交流时,才会通过IO电路转换到3.3V,所以我们从外部看,好像STM32内部全是3.3V,但实际上它内部的CPU、外设等都是以1.8V供电运行,使用低电压运行的主要目的是降低功耗,电压越低内部电路运行的功耗就相对越低。
    • 注意:电压调节器的作用是给1.8V区供电(可以视为1.8V区域的电源),后面会提到这个1.8V区域和电压调节器。
  3. VBAT后备供电区域
    • 包括LSE 32K晶体振荡器、后备寄存器,RCC BDCR计寄存器和RTC。
    • RCC BDCR是RTC的寄存器啊,叫做备份域控制寄存器,也是和后备区域有关的寄存器,所以也可以有VBAT供电。
    • 然后这里有个低电压检测器,可以控制接在VBAT引脚上的开关,VDD有电时由VDD供电,VDD没电时由VBAT供电。

14.3 上电/掉电复位和可编程电压监测

上电复位和掉电复位,还有可编程电压监测器这,两个内容了解即可。

14.3.1 上电/掉电复位

首先是上电复位和掉电复位,这个意思是当VDD或者VDDA电压过低时,内部电路直接产生复位,让STM32复位住不要乱操作,这个复位和不复位的界限之间,设置了一个40毫伏的迟滞电压,大于上限POR(Power On Reset)时解除复位,小于下限PDR(Power Down Reset)时复位,这是一个典型的迟滞比较器,设置两个阈值的作用,就是防止电压在某个阈值附近波动时,造成输出也来回抖动。

下面的复位信号reset是低电平有效的,所以在前面和后面电压过低时是复位的,中间电压正常的时候不复位,那这个电压上限和下限具体是多少伏呢,还有这里解除复位,还有个滞后时间是多久呢,这些参数可以看一下STM32数据手册,在5.3.3内嵌复位和电源控制模块特性里有这个表,这里写了上电或掉电复位阈值,下降沿,也就是PDR掉电复位的阈值下限,典型值是1.88V,上升沿,也就是POR上电复位的阈值上限,典型值是1.92V,1.92-1.88就是迟滞的阈值40毫伏,所以如果忽略迟滞的话,简单来说就是大于1.9V上电,低于1.9V掉电,然后最后一行就是TRSTTEMPO,复位持续时间,典型值是2.5ms,就是这个上电复位和掉电复位,知道一下就行了,也不需要我们操作啥的。

14.3.2 可编程电压监测器

然后下面这个是可编程电压监测器,简称PVD,他的工作流程和上面这个差不多哈都是监测VDD和VDDA的供电电压,但是PVD的区别就是,首先它这个阈值电压是可以使用程序自定义调节,调节的范围可以看一下数据手册,在这个表的上面就是PVD的阈值,配置PLS寄存器的3个位可以选择右边这么多的阈值,因为这里也同样是迟滞比较,所以有两个阈值,可选范围是2.2V到2.9V左右,PVD上限和下限之间的迟滞电压是100毫伏,可以看到PVD的电压是比上电掉电复位的电压要高的。

画个图就是3.3伏是正常的供电,当这个电压降低在2.9V到2.2V之间,属于PVD监测的范围,可以通过PVD设置一个警告线,之后再降低到1.9V,就是复位电路的检测范围,低于1.9V直接复位住不让动,就是这两个电压监测的工作任务。

那当然PVD触发之后,芯片还是能正常工作的,只不过是电源电压过低,该提醒一下用户了,所以看一下下面这个PVD输出,这个是正逻辑哈,电压过低时为1,电压正常值为0,这个信号可申请中断,在上升沿或者下降沿时触发中断,一是提醒程序进行适当的处理。另外PVD的中断申请是通过外部中断实现的,我们可以看一下外部中断这一节,这个图(EXTI基本结构图)可以看到PVD输出的信号是跑到这里来了,所以如果要使用PVD的话,记得要配置外部中断,然后下面这里还有RTC(EXTI基本结构图),这个是RTC的闹钟信号,也有接到外部中断。

其实RTC自己是有中断的,那为啥还要借到外部中断,这个等会就知道了,因为低功耗模式设计的是,只有外部中断可以唤醒停止模式,其他这些设备也想唤醒停止模式的话,都可以通过借道外部中断来实现,其实后面这两个USB和ETH,也都只有他们的wake up唤醒信号接过来了,目的也是为了唤醒停止模式,这个了解一下。

14.4 低功耗模式

下面的这个表,是低功耗模式一览,对3种模式进行了详细的对比。表的第一列表示有哪几种低功耗模式,第二列是如何配置才能进入设定的模式,第三列是对于这些模式进入之后如何去唤醒,最后三列,是每种模式对电路的操作(关团了哪些电路,就是哪些电路不能用了,保留了哪些电路,就是哪些电路还是正常工作的)。

低功耗模式,这三种模式从上到下关闭的电路越来越多,对应的从上到下是越来越省电,同时从上到下也是越来越难唤醒的。

  • 睡眠模式
    • 这是浅睡眠,如何进入呢,表中写了直接调用WFI或者WFE即可进入,这两个东西是内核的指令,对应库函数里也有对应的函数,直接调用函数即可:
      • 其中WFI的意思是wait for interrupt等待中断,意思就是我先睡了,如果有中断发生的话再叫我起来,所以对应的唤醒条件是任意中断,调用WFI进入的睡眠模式,任何外设发生任何中断时,芯片都会立刻醒来,因为中断发生了,醒来之后的第一件事一般就是处理中断函数;
      • 下面WFE意思是wait for event等待事件,对应的唤醒条件是唤醒事件,这个事件可以是外部中断配置为事件模式,也可以是使能了中断但是没有配置NVIC,调用WFE进入的睡眠模式,产生唤醒事件时会立刻醒来,醒来之后一般不需要进中断函数,直接从睡的地方继续运行,这是WFI和WFE的作用,相同点是调用任意一个之后,芯片都进入睡眠,不同点是WFI进入的得用中断唤醒,WFE进入的得用事件唤醒。
    • 最后看一下睡眠模式对电路的影响:
      • 对1.8V区域时钟的影响是,CPU时钟关,对其他时钟和ADC时钟无影响;
      • 对VDD区域时钟的影响是无;
      • 对电压调节器的操作是开;
      • 所以睡眠模式对电路的影响就是,只把CPU时钟关了,对其他电路没有任何操作,CPU时钟关了,程序就会暂停,不会继续运行了,CPU不运行芯片功耗就会降低。
    • 另外这里还可以看出,关闭电路通常有两个做法,一个是关闭时钟,另一个是关闭电源:
      • 关闭时钟,所有的运算和涉及时序的操作都会暂停,但是寄存器和存储器里面保存的数据还可以维持不会消失;
      • 关闭电源就是电路直接断电,电路的操作和数据都会直接丢失,所以关闭电源比关闭时钟更省电,这个表里的这两点,就对1.8V区域和VDD区域的时钟控制,然后这个电压调节器它实际上就是1.8V区域的电源,如果电压调节器关,就代表直接把1.8V区域断电

睡眠模式小结

睡眠模式它唤醒条件也是比较宽松,任何的风吹草动,CPU都会醒来开始干活,所以睡眠模式相当于大佬打了个盹儿,身体还在工作。在省电程度上评级为一般省电

  • 停机模式
    • 如何进入停机模式呢,首先SLEEPDEEP位设置为1,告诉CPU可以放心进入深度睡眠模式,另外PDDS这一位用来区分它是停机模式,还是下面的待机模式,PDDS=0进入停机模式,PDDS=1进入待机模式。LPDS用来设置最后这个电压调节器,是开启还是进入低功耗模式,LPDS=0电压调节器开启,LPDS=1电压调节器进入低功耗,最后当我们把这些位提前设置好了,最后再调用WFI或者WFE,芯片就可以进入停止模式了。
    • 如何唤醒停止模式呢,因为这个模式下芯片睡得更深,关的东西更多,所以唤醒条件就苛刻一些,是任一外部中断。刚才睡眠模式是任一中断,所有外设的中断都行,现在停止模式,要求就是只有外部中断才能唤醒,其他中断唤醒不了,刚才我们还提到了,PVD、RTC闹钟、USB唤醒、ETH唤醒借道了外部中断,所以这四个信号也可以唤醒停止模式,因为这里并没有区分WFI和WFE,其实也可以想象得到,WFI要用外部中断的中断模式唤醒,WFE要用外部中断的事件模式唤醒。
    • 最后看停止模式对电路有哪些操作呢
      • 首先关闭所有1.8V区域的时钟,这意思就是不仅CPU不能运行,外设也运行不了,定时器在定时的会暂停,串口收发数据也会暂停,不过由于没关闭电源,所以CPU和外设的寄存器数据都是维持原状的;
      • 其次,HSI和HSE的振荡器关闭,既然CPU和外设时钟都关了,那这两个高速时钟显然也没用了,所以HSI内部高速时钟和HSE外部高速时钟会关闭,当然表中没提到的LSI内部低速时钟和LSE外部低速时钟,这两个并不会主动关闭,如果开启过这两个时钟还可以继续运行;
      • 最后,电压调节器,这里可以选择是开启或者处于低功耗模式,刚才说了,这个电压调节器是由这个LPDS位控制的,这个开启和低功耗模式有啥区别呢,其实区别也不大,电压调节器无论是开启还是低功耗,都可以维持1.8伏区域寄存器和存储器的数据内容,区别就是低功耗模式更省电一些,同时低功耗模式在唤醒时要花更多的时间

停机模式小结

主要操作就是把运行的高速时钟都关了,CPU和外设都暂停工作,但是电压调节器并没有关,存储器和寄存器数据可以维持原样,它的唤醒条件比较苛刻,只能通过外部中断唤醒,所以停止模式相当于整个人都罢工了,脑子不工作,身体也不工作,只有有人用外部中断过来敲我,我才会醒来干活。在省电程度上评级为非常省电

  • 待机模式
    • 进入的话和停机模式差不多,首先SLEEPDEEP也是置1即进入深度睡眠,然后PDDS置1表示即将进入待机模式,最后调用WFI或者WFE就可以进入待机模式了。
    • 看一下唤醒条件,普通外设的中断和外部中断都无法唤醒待机模式,待机模式只有4个指定的信号才能唤醒,唤醒条件最为苛刻:
      • 第一个是WKUP引脚的上升沿,wake up引脚,可以看一下引脚定义,这里PA0-WKUP指示了引脚的位置,就是PA0的位置;
      • 第二个是RTC闹钟事件,这个我们的示例代码和上一节RTC提到过,RTC闹钟可以唤醒待机模式,应用场景就是芯片每隔一段时间自动工作一次;
      • 第三个是NRST引脚上的外部复位,意思就是按一下复位键,它也是能唤醒的;
      • 最后一个IWDG独立看门狗复位,这个了解一下就行了,看门狗我们最后一节再介绍。
    • 之后待机模式对电路的操作基本上是能关的全都关了
      • 1.8V区域的时钟关闭,两个高速时钟关闭,电压调节器关闭,这意味着1.8V区域的电源关闭,内部的存储器和寄存器的数据全部丢失,但是和停止模式一样,它并不会主动关闭LSI和LSE两个低速时钟,因为这两个时钟还要维持RTC和独立看门狗的运行,所以不会关闭。

待机模式小结

主要操作就是把能关的全都关掉,只保留几个唤醒的功能,当然配合RTC和独立看门狗的低速时钟,也可以正常工作,所以待机模式相当于这个人直接下班回家睡觉了,没有指定的这几个事,他是不会轻易回来工作的,在省电程度上,待机模式评级为极为省电

上面3种低功耗模式的配置可总结为下图

3种低功耗模式模式的一些特性小结

14.5 PWR实验

14.5.1 睡眠模式+串口发送+接收

14.5.2 停止模式+对射式红外传感计次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int counter = 0;

/**
*@brief 初始化GPIOB的14号引脚,并进行中断设置
*@note 模式为
*/
void PB14_Init(void)
{
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

// 初始化GPIOB的14号引脚
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);

// 配置AFIO
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

// 配置EXTI
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line14;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStruct);

// 配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);
}

int main(void)
{
// 开启PWR外设时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

/*模块初始化*/
OLED_Init(); //OLED初始化

// 初始化配置
PB14_Init();

while (1)
{
OLED_ShowNum(1, 1, counter, 5);

OLED_ShowString(3, 1, "RUNNING");
Delay_ms(100);
OLED_ShowString(3, 1, " ");
Delay_ms(100);
// 进入STOP低功耗模式,等待中断唤醒
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);

// 从停止模式唤醒后使用的时钟是:HSI
// 重新开启外部高速时钟:HSE
SystemInit();

}
}


// 中断函数
void EXTI15_10_IRQHandler(void)
{
// 这个函数EXTI10~EXTI15都能进来,所以需要判断具体是哪个
if(EXTI_GetITStatus(EXTI_Line14) == SET)
counter++;

// 中断函数一定要记得清除中断标志位,否则会一直进入中断
EXTI_ClearITPendingBit(EXTI_Line14);
}

14.5.3 待机模式+实时时钟RTC

15 看门狗WDG

15.1 WDG简介

看门狗WDG简单来说就是程序运行的一个保障措施,我们需要定期地“喂狗”,如果程序卡死了,没有在规定的时间里喂狗,那么看门狗对应的硬件电路就会自动帮我们复位程序(这就相当于代替了我们当程序卡死时手动按动复位按钮,可类比手机、电脑卡死重启的操作)。看门狗就是完成这样一个操作的硬件电路

  1. 独立看门狗(IWDG):独立工作,对时间精度要求较低
    • 它的特点就是独立运行,对时间精度要求较低,独立运行就是独立看门狗的时钟是专用的LSI内部低速时钟,即使主时钟出现问题了,看门狗也能正常工作,这也是独立看门狗独立的得名原因,对时间精度要求较低,就是独立看门狗只有一个最晚时间界限,你喂狗间隔只要不超过这个最晚界限就行了,你说很快的喂、疯狂的喂、连续不断的喂,那都没问题,
  2. 窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用,喂狗早了和晚了都不行
    • 它相比较独立看门狗就严格一些了,要求看门狗在精确计时窗口起作用,意思就是喂狗的时间有个最晚的界限,也有个最早的界限,必须在这个界限的窗口内喂狗,这是窗口开门口窗口的得名原因,因为对于独立看门狗来说,可能程序就卡死在喂狗的部分了,或者程序跑飞,但是喂狗代码也意外执行了,或者程序有时候很快喂狗,有时候又比较慢喂狗,那这些状态独立看门狗就检测不到了,但是窗口看门狗是可以检测到这些问题的,因为它对喂狗的时间窗口可以卡的很死,快了慢了都不行;
    • 注意:窗口看门狗使用的是APB1的时钟,它没有专用的时钟,所以不算是独立,主时钟出现问题就失效了

15.2 两种看门狗的学习

15.2.1 独立看门狗IWDG

它的结构和定时器是非常相似的,只不过是定时器溢出产生中断,而看门狗定时器溢出直接产生复位信号。

喂狗操作其实也就是重置12位递减计数器,该计数器减到零之后就复位,那程序正常运行时,为了避免复位,就得在这个计数器减到零之前,及时把记数值加大点,这个操作就是喂狗,如果你程序卡死了,没有及时加大这个记数值,那减到零之后就自动复位了,就是看门狗的工作逻辑。

这个框图大家可以类比定时器的时基单元来看,我们首先回顾一下定时器,这一块是时基单元,由预分频器、计数器和重装载寄存器组成,左边是输入时钟,比如这里是72M,首先经过分频,比如现在2分频,那么计数器的驱动时钟就是72M/2=36M,这个计数器可以自增,也可以自减,看门狗使用的是自减运行,那自减到零后,定时器产生更新事件和中断,而看门狗是直接产生复位,另外重装值,定时器是在更新事件重装,而看门狗需要我们在自减到零之前手动重装,因为减到零就复位了,那这个手动重装计数器的操作就是喂狗。

看完了定时器,接着来看这个独立看门狗,这一块是预分频器,这一块是计数器,这一块是重装寄存器,这基本就是一样的结构,那预分频器之前输入时钟是LSI内部低速时钟,时钟频率为40KHz,之后时钟进入预分频器进行分频,这个预分频器只有8位,所以它最大只能进行256分频,上面这个预分频寄存器,IWDG_PR可以配置分频系数,这个PR和定时器的PSC是一个意思,他们都是Prescaler的缩写,可能不是一个人设计的,所以这手册里很多缩写都不太一样,不过大家要知道他们其实是一个意思。后面经过预分配器分频之后,时钟驱动递减计数器,每来一个时钟自减一个数,另外这个计数器是12位的,所以最大值是2^12-1=4095,然后当自减到0之后,产生IWDG复位。正常运行时,为了避免复位,我们可以提前在重装寄存器写个值,IWDG_RLR和定时器的ARR是一样的,RLR是reloader,ARR是auto reloader,那当我们预先写好值之后,在运行过程中,我们在这个键寄存器里写个特定数据,控制电路进行喂狗,这时重装值就会复制到当前的计数器中,这样计数器就会回到重装值,重新自减运行了。然后这里有个状态寄存器SR,就是标志电路运行的状态了,其实这个SR里没什么东西,就只有两个更新同步位,基本不用看。

最后上面这些寄存器位于1.8V供电区,下面主要的工作电路都位于VDD供电区,所以这下面写了看门狗功能处于VDD供电区,即在停机和待机模式时仍能正常工作,上节我们也说过,独立开门口也是唤醒待机模式的四个条件之一。

  • IWDG键寄存器

键寄存器本质上是控制寄存器,用于控制硬件电路的工作,比如上面说的喂狗操作,就是通过在键寄存器写入0xAAAA完成的,那为什么要用键寄存器呢,我直接定义一个控制寄存器,其中再定义一个位,这一位写入1就喂狗,这样不也行吗?

我们继续看第二条,在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制电容器写入移位的功能,以降低硬件电路受到干扰的概率,为什么能降低干扰呢,你看独立看门狗工作的环境是什么,是程序可能跑飞,可能受到电磁干扰,程序做出任何操作都是有可能的,如果你只在寄存器中设置一个位,那这1位就有可能在误操作中变成1,或者变成0,这个概率是比较大的,所以单独设置1位就来执行控制,在这里比较危险,这时我们就可以通过,在整个寄存器写入一个特定值来代替,写入1个位的操作,比如这里键寄存器是16位的,只有在键寄存器写入0xAAAA这个特定的数,才会执行喂狗的操作,这样就会降低误操作的概率。

最后两条是写保护的逻辑,意思就是执行指令必须写入指定的键值,所以指令抗干扰能力是很强的,但这里(上面框图里)还有PR、SR和RLR 3个寄存器,它们也要有防止误操作的功能,SR是只读的这个不用保护,剩下的PR和RLR的写操作,可以设置一个写保护措施,然后只有在键寄存器写入5555,才能解除写保护,一旦写入其他值,PR和RLR再次被保护,这样PR和RLR就跟随键寄存器一起,被保护了起来,防止误操作,这是键寄存器设计的用途。

  • IWDG超时时间

15.2.2 窗口看门狗WWDG

窗口看门狗从功能上来说,和独立看门狗还是比较像的,大体上来看只是比独立看门狗多个最早喂狗时间的限制,但是等会儿学的时候,你就会发现,这个窗口看门狗无论是框图的设计,还是寄存器的分布和命名规则,或者程序的操作流程,和独立看门狗都不是一个思路,可能是两个看门狗侧重点不一样吧,当然我感觉应该还是因为这两个外设不是同一个人设计的,所以设计的思路有所不同

上图左下角是时钟源部分,这个时钟源是PCLK1,右边对应预分频器(这个预分频器名字又变了,叫WDGTB,实际上和独立开门狗的PR,定时器的PSC都是一个东西),上面这个是6位递减计数器CNT,这个计数器是位于控制寄存器CR里的,计数器和控制寄存器合二为一了然后窗口看门狗没有重装寄存器,那如何重装计数器进喂狗呢,这个我们直接在CNT写个数据就行了,想写多少就写多少,这上面这一块是窗口值,由此喂狗的最早时间界限就写到这里存起来,最后左边就是输出信号的操作逻辑,什么情况下会产生复位,就这几个逻辑门来确定。

  • WWDG的工作流程
    • 首先,从左下角开始看,时钟来源是PCLK1,也就是APB1的时钟,这个时钟默认是36MHz,所以就是36MHz的时钟进来,之后还是先经过一个预分频器进分频,这个和独立看门狗的预分频器,定时器的预分频器都是一个作用,就是灵活的调节后面计数器的时钟频率,同时预分频系数也是计算计数器溢出时间的重要参数;
    • 接着,分频之后的时钟驱动这个计数器进行计数,这个计数器和独立看门狗一样,也是一个递减计数器,每来一个时钟自减一次,不过这个计数器比较特殊,从图上来看,这里写了T6到T0,总共是七个位,但下面却写的是六位递减计数器,这是为什么呢?那这其实是因为这个计数器只有T5到T0这六位是有效的就值,最高位T6这里用来当做溢出标志位,第6位等于1时,表示计数器没溢出,T6位等于0时表示计数器溢出,不过对于硬件电路来说,T6位其实也是计数器的一部分,只不过是T6位被单独拎出来,当做标志位了而已。
      • 总结一下,就是如果你把T6位看作是计数器的一部分,那要是整个计数器值减到0X40之后移出,而如果你把T6位当成溢出标志位,低6位当做计数器,那就是低6位的计数值减到0之后溢出,这一点尤其要搞清楚。
    • 然后,左边的复位信号输出部分,首先这个WDGA是窗口看门狗的激活位,也就是使能,WDGA写入1启用窗口看门狗,使能位作用于这个与门。
    • 继续,与门的右侧是复位信号的来源,图中可以看到有两个来源,使用或门连接
      • 其中,下面这一路来源于溢出标志位T6,T6位一旦等于零(小圆圈取反),就表示计数器溢出,产生复位信号,那在程序正常运行状态下,我们必须始终保证T6位为一,这样才能避免复位,下面这一块实现的功能和独立看门狗基本是一样的,如果不及时喂狗,6位的计数器减到0后就产生复位。
      • 接下来,或门的另一路,看门狗时间的最早界限,由上面这一块来实现,首先我们要计算一个最早界限的计数值,写到这里的W6~W0中,写入之后是固定不变的,在这里一旦我们执行写入CR操作时,这个与门开关就会打开,写入CR其实就是写入计数器,也就是喂狗,在喂狗时,这个比较器开始工作,一旦它比较我们当前的计数器,T6:0>窗口值W6:0,比较结果就等于1,这个一通过或门也可以去申请复位,这是喂狗最早时间窗口的实现流程,就是喂狗的时候,我把当前记数值和预设的窗口值进行比较,如果发现你的狗余粮还非常充足,你喂的这么频繁,那肯定是有问题,我要给你复位一下,不让你喂太早了。

窗口看门狗的工作特性

递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位,这里的意思就是,减到0x40时产生中断,然后再减一个数到0x3F时产生复位,那这个中断其实就是在溢出的前一刻发生,所以这个中断也可以称作死前中断,马上就要溢出复位了,再提醒一下你要不要干点啥,我们一般可以用来执行一些紧急操作,比如保存重要数据,关闭危险设备等等,或者还有一种写法,就虽然超时喂狗了,但是我们可以在中断里执行一些代码,进行解决,或者这个任务不是很危险,超时了我就只想做一些提示,不想让他复位了,这样的话我们就可以在这个早期唤醒中断里直接执行喂狗,阻止系统复位,然后提示一下信息就完事儿了。

WWDG的超时时间

注意

上面公式中多乘一个4096,是因为这里PCLK1进来之后,其实是先执行了一个固定的4096分频,上面的框图没画出来,实际上是有的,因为36M的频率还是太快了,先来个固定分频给降一降。

15.2.3 IWDG和WWDG的区别

15.3 WDG实验

15.3.1 看门狗的库函数

IWDG的常用函数:

1
2
3
4
5
6
7
8
9
10
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess); // 写使能控制 写入0x5555/0x0000
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler); // 写预分频器PR寄存器
void IWDG_SetReload(uint16_t Reload); // 写重装值RLR寄存器
void IWDG_ReloadCounter(void); // 重新装载寄存器0xAAAA 喂狗
void IWDG_Enable(void); // 启用IWDG 0xCCCC
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG); // 查看看门狗标志位

//rcc.h里查看标志位函数 上电复位、软件复位、独立看门狗、窗口看门狗复位等
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);
void RCC_ClearFlag(void);

WWDG的常用函数:

1
2
3
4
5
6
7
8
void WWDG_DeInit(void);
void WWDG_SetPrescaler(uint32_t WWDG_Prescaler);//写入预分频器
void WWDG_SetWindowValue(uint8_t WindowValue); //写入窗口值
void WWDG_EnableIT(void); //使能中断
void WWDG_SetCounter(uint8_t Counter); //写入计数器(喂狗)
void WWDG_Enable(uint8_t Counter); //使能启动窗口看门狗
FlagStatus WWDG_GetFlagStatus(void);
void WWDG_ClearFlag(void);

15.3.2 独立看门狗实验

接线图如下:按键用于阻塞喂狗。独立看门狗&窗口开门狗接线一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

int main(void)
{
/*模块初始化*/
OLED_Init(); // OLED初始化
Key_Init(); // 按键初始化

/*显示静态字符串*/
OLED_ShowString(1, 1, "IWDG TEST");

/*判断复位信号来源*/
if (RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET) //如果是独立看门狗复位
{
OLED_ShowString(2, 1, "IWDGRST"); // OLED闪烁IWDGRST字符串
Delay_ms(500);
OLED_ShowString(2, 1, " "); // 显示空格,把字符串清除
Delay_ms(100);
// 清除标志位 必须要清除标志位,因为IWDGRST标志位,即使按下复位键,也不会自动清0
RCC_ClearFlag();
}
else //否则,即为其他复位
{
OLED_ShowString(3, 1, "RST"); // OLED闪烁RST字符串
Delay_ms(500);
OLED_ShowString(3, 1, " ");
Delay_ms(100);
}

/*IWDG初始化*/
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 独立看门狗写使能,解除写保护
IWDG_SetPrescaler(IWDG_Prescaler_16); // 设置预分频为16
IWDG_SetReload(2499); // 设置重装值为2499,IWDG超时时间为1000ms
IWDG_ReloadCounter(); // 重装计数器,喂狗
IWDG_Enable(); // 独立看门狗使能
// 喂狗或使能的时候,会在键寄存器写入5555之外的值,这时就顺便给寄存器写保护了,无需再手动写保护了

while (1)
{
// 调用阻塞式的按键扫描函数,模拟主循环卡死,
// 按住按键不放,主循环就会阻塞,不能执行后面喂狗,独立看门狗就会复位。
Key_GetNum();
// 重装计数器,在程序中及时喂狗,防止看门狗复位程序
IWDG_ReloadCounter();

OLED_ShowString(4, 1, "FEED"); // OLED闪烁FEED字符串
Delay_ms(200); // 喂狗间隔为200+600=800ms
OLED_ShowString(4, 1, " ");
Delay_ms(600);
}
}

15.3.3 窗口看门狗实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"

int main(void)
{
/*模块初始化*/
OLED_Init(); // OLED初始化
Key_Init(); // 按键初始化

/*显示静态字符串*/
OLED_ShowString(1, 1, "WWDG TEST");

/*判断复位信号来源*/
if (RCC_GetFlagStatus(RCC_FLAG_WWDGRST) == SET) // 如果是窗口看门狗复位
{
OLED_ShowString(2, 1, "WWDGRST"); // OLED闪烁WWDGRST字符串
Delay_ms(500);
OLED_ShowString(2, 1, " ");
Delay_ms(100);

RCC_ClearFlag(); // 清除标志位
}
else // 否则,即为其他复位
{
OLED_ShowString(3, 1, "RST"); // OLED闪烁RST字符串
Delay_ms(500);
OLED_ShowString(3, 1, " ");
Delay_ms(100);
}

/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // 开启WWDG的时钟

/*WWDG初始化*/
WWDG_SetPrescaler(WWDG_Prescaler_8); // 设置预分频为8
WWDG_SetWindowValue(0x40 | 21); // 设置窗口值,窗口时间为30ms,T6位也要设置成1,所以或上0x40
WWDG_Enable(0x40 | 54); // 使能并第一次喂狗,超时时间为50ms,T6位也要设置成1,所以或上0x40

while (1)
{
Key_GetNum(); //调用阻塞式的按键扫描函数,模拟主循环卡死

OLED_ShowString(4, 1, "FEED"); // OLED闪烁FEED字符串
Delay_ms(20); // 喂狗间隔为20+20=40ms
OLED_ShowString(4, 1, " ");
Delay_ms(20);

WWDG_SetCounter(0x40 | 54); // 重装计数器,喂狗
}
}

16 STM内部闪存FLASH

本节学习STM32的FLASH闪存(闪存是一个通用名词,表示的是一种非易失性、掉电不丢失的存储器),比如之前学习SPI通信协议的时候用的W25Q64芯片就是一种闪存存储器芯片。

本节我们学习的闪存是特指STM32的内部闪存,也就是我们下载程序的时候程序所存储的位置。

16.1 FLASH简介

我们本节的任务就是对这些存储器进行读写,那我们怎么操作这些存储器呢,这需要用到这个闪存存储器接口,闪存存储器接口是一个外设(有对应的硬件电路),是这个闪存的管理员,毕竟闪存的操作很麻烦,涉及到擦除、编程、等待、解锁等等操作,所以这里我们需要把我们的指令和数据,写入到这个外设的相应寄存器,然后这个外设就会自动去操作对应的存储空间,那后面写的是这个外设可以对程序存储器和选项字节,这两部分进行擦除和编程,对比上面的三个部分呢少了系统存储器这个区域,因为系统存储器是原厂写入的BOOTLOADER程序,这个是不允许我们修改的。

  • 读写FLASH的用途
    • 利用程序存储器的剩余空间来保存掉电不丢失的用户数据
      • 第一个用途,对于我们这个C8T6芯片来说,它的程序存储器容量是64K,一般我们写个简单的程序,可能就只占前面的很小一部分空间,剩下的大片空余空间我们就可以加以利用,比如存储一些我们自定义的数据,这样就非常方便,而且可以充分利用资源,不过这里要注意我们在选取存储区域时,一定不要覆盖了原有的程序,要不然程序自己把自己给破坏了,一般存储少量的参数,我们就选最后几页存储就行了,关于如何查看程序所占用空间的大小,这个我们下小节也会介绍。
    • 通过在程序中编程(IAP),实现程序的自我更新
      • 上面说了,我们在存储用户数据时要避开程序本身,以免破坏程序,但如果我们就非要修改程序本身,这会发生什么呢,那这就是第二点提到的功能,在程序中编程,利用程序来修改程序本身,实现程序的自我更新,这个在程序中编程就是IAP,在数码圈也有个可能大家更熟悉的技术叫OTA,这俩是类似的东西,都是用来实现程序升级的,但这个IAP升级程序的功能比较复杂,我们本课程暂时就不涉及了,之后有缘再说吧。

在线/电路编程(In-Circuit Programming – ICP)

用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序, ICP英文直译过来也可以叫在电路中编程,意思就是下载程序,你只需要留几个引脚就行,不用拆芯片了,就叫在电路中进行编程,ICP的作用是用于更新程序存储器的全部内容,它通过JTAG SWD协议或系统加载程序BOOTLOADER下载程序,这个JTAG SWD就是仿真器下载程序,就是我们目前用的stlink,使用SWD下载程序,每次下载都是把整个程序完全更新掉,那系统加载程序就是系统存储器的BOOTLOADER,也就是串口下载,串口下载也是更新整个程序,这就是我们一直在用的ICP下载方式,下面的IAP是更高级的下载方式。

程序中编程(In-Application Programming – IAP)

它可以使用微控制器支持的任意一种通信接口下载程序,怎么实现呢,那比如这是整个程序存储器,我们首先需要自己写一个BOOTLOADER程序,并且存放在程序更新时不会覆盖的地方,比如我们放在最后面,然后需要更新程序时,我们控制程序跳转到这个自己写的BOOTLOADER这里来,在这里面我们就可以接收任意一种通讯接口传过来的数据,比如串口、USB、蓝牙转串口、WIFI转串口等等,这个传过来的数据就是待更新的程序,然后我们控制flash读写,把收到的程序写入到前面程序正常运行的地方,写完之后再控制程序跳转回正常运行的地方,或者直接复位,这样程序就完成了自我升级,这个过程其实就是和系统存储器这个的BOOTLOADER一样,因为程序要实现自我升级,在升级过程中,肯定需要布置一个辅助的小机器人来临时干活,只不过是系统存储器的BOOTLOADER写死了,只能用串口下载到指定位置,启动方式也不方便,只能配置boot引脚触发启动,而我们自己写BOOTLOADER的话,就可以想怎么收怎么收,想写到哪就写到哪,想怎么启动就怎么启动,并且在整个升级过程,程序都可以自主完成,实现在程序中编程,更进一步就可以直接实现远程升级了,非常灵活方便。

接下来的内容,我们就只涉及最基本的对Flash进行读写,这也是实现IAP的基础。

我们C8T6芯片的闪存容量是64K,属于中容量产品,对于小容量产品和大容量产品,闪存的分配方式有些区别,这个可以参考一下手册,那首先提醒一下闪存这一章的内容在手册里是单独列出来的,并不在之前的参考手册里,我们需要打开这个闪存编程参考手册,这里以中容量产品为例来讲解。

首先看一下第一列的几个块,这里分为了三个块:

  1. 第一个是主存储器,也是我们刚才说的程序存储器,用来存放程序代码的,这是最主要也是容量最大的一块;
  2. 第二个是信息块,里面又可以分为启动程序代码和用户选择字节,其中启动程序代码就是刚才说的系统存储器,存放的是原厂写入BOOTLOADER用于串口下载,这个手册的名称经常会有不同的表述方式,但大家要知道某些名称描述的其实是一个东西,然后下面这个用户选择字节,就是刚才说的选项字节存放一些独立的参数,这个选项字节在手册里一直都称作选择字节,可能是翻译的问题,英文是option bytes,我们一般都叫选项字节;
  3. 最后一块是闪存存储器接口寄存器,这一块的存储器实际上并不属于闪存,你看那个地址就知道地址都是40开头的,说明这个存储器接口寄存器就是一个普通的外设,和之前讲的GPIO定时器、串口等等都是一个性质的东西,这些存储器它们的存储介质也都是sram,这个闪存存储器接口就上面这些闪存的管理员,这些寄存器就是用来控制擦除和编程这个过程的,那到这里这个表的整体我们就清楚了,我们擦除和编程就通过读写这些寄存器来完成,当然这里只有擦除和编程,并没有读取,这是因为读取指定存储器直接使用指针读即可,用不到这个外设。

继续看这个表,对于主存储器,这里对它进行了分页,分页是为了更好的管理闪存,擦除和写保护都是以页为单位的,这点和之前W25Q64芯片的闪存一样,同为闪存它们的特性基本一样,写入前必须擦除,擦除必须以最小单位进行,擦除后数据位全变为1,数据只能1写0,不能0写1,擦除和写入之后都需要等待忙,这些都是一样的,学习这节之前,可以再复习一下W25Q64,再学这一节就会非常轻松了,那W25Q64的分配方式是先分为块block,再分为扇区sector比较复杂,这里就比较简单了,它只有一个基本单位就是页,每一页的大小都是1K,0到127总共128页,总量就是128K,对于C8T6来说,它只有64K,所以C8T6的页只有一半0~63总共64页共64K,然后看一下页的地址范围。

第一个页的起始地址就程序存储器的起始地址0x08000000,之后就是一个字节一个地址依次线性分配的,看一下每页起始地址的规律,首先是0000然后0400、0800、0400,再之后1000、1400、1800,最后一直到1FC00,所以地址只要以000、400、800、400,结尾的都一定是页的起始地址,这个稍微记一下。

然后继续系统存储器,它的起始地址是0x1FFFF000,这个之前介绍过的,它的容量是2K,下面选项字节起始地址是0x1FFFF800,容量是16个字节,里面只有几个字节的配置参数,这个后面还会继续说的,那这里还可以发现我们平时说的芯片闪存容量是64K128K,它指的只是主存储器的容量,下面信息快的这两个东西虽然也是闪存,但是并不统计在这个容量里,这是闪存的分配方式,那最后就是这个闪存接口寄存器,里面包括KEYR键寄存器,SR状态寄存器,CR控制寄存器,外设的起始地址是0X4002 2000,每个寄存器都是四个字节也就是32位。

16.2 Flash的抽象结构图

接下来看一下我总结的这个基本结构图,整个闪存分为程序存储器、系统存储器和选项字节三部分,这里程序存储器为以C8T6为例,它是64K的,所以总共只有64页,最后一页的起始地址是0x0800FC00,左边这里是闪存存储器接口,手册里还有个名称,闪存编程和擦除控制器FPEC,大家也知道这两个名称其实是一个东西就行,然后这个控制器就是闪存的管理员,他可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程,系统存储器是不能擦除和编程的,这个选项字节里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的写入选项字节,可以配置程序存储器的读写保护,当然选项字节还有几个别的配置参数,这个待会再讲,那这就是整个闪存的基本结构。

接下来我们来看一下细节问题,如何操作这个控制器FPEC,来对程序存储器和选项字节进行擦除和编程。

16.3 Flash外设的使用

  1. Flash解锁操作
    • 这和之前W25Q64一样,W25Q64操作之前需要写使能,这个Flash操作之前需要解锁,目的都是为了防止误操作,那这里解锁的方式和之前独立看门狗一样,都是通过在键寄存器写入指定的键值来实现,使用键寄存容器的好处就是更能防止误操作,每一个指令必须输密码才能完成,通过英文名称也能看出来,键的英文是KEY,直译是不是钥匙的意思,所以这个更形象的翻译我们可以把它叫做钥匙寄存器,密钥寄存器,首先FPEC共有三个键值,也就是三把开锁的钥匙:
      • RDPRT键是解除读保护的密钥,值是0x000000A5
      • KEY1键值是0x45670123
      • KEY2键值是0xCDEF89AB
      • 为什么是这些值呢,实际上是随便定义的,只要你定义的不是很简单就行
    • 继续看怎么解锁呢,第一个是复位后FPEC被保护,不能写入FLASH_CR,也就是复位后Flash默认是锁着的,然后在FLASH_KEYR键寄存器中,先写入KEY1,再写入KEY2解锁,我们找到了锁,这个锁是KEYR寄存器,怎么解呢,要先用K1钥匙解,再用K2钥匙解,最终才能解锁成功,所以这个锁的安全性非常高,有两道锁,即使程序跑飞了,歪打正着正好写入了KEY1,那也难以保证下一次又歪打正着写入了KEY2,所以非人为情况下基本不可能解锁;
    • 然后第三条还有进一步的保护措施,就是错误的操作序列会在下次复位前锁死FPEC和FLASH_CR,也就是它发现有程序在尝试撬锁时,一旦没有先写入KEY1,再写入KEY2,整个模块就会完全锁死,除非复位,这是整个解锁操作,可以看到安全性非常高;
  2. Flash加锁操作
    • 我们操作完成之后,要尽快把Flash重新加锁,以防止意外情况,加锁的操作是设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR,这个比较简单,就是控制寄存器里面有个LOCK位,我们在这一位写1就能重新锁住闪存。

16.4 补充:使用指针访问存储器

接着看下一个知识点,这个地方我们要学习的是,如何使用指针访问存储器,因为STM32内部的存储器是直接挂在总线上的,所以这时在读写某个存储器就非常简单了,直接使用C语言的指针来访问即可。

对与闪存的读取来说,是不需要进行解锁的,因为读取只是看看存储器,不对存储器的内容进行修改,所需权限很低,不用解锁就可直接读取。

上图中指定地址读的示例语句指定的是闪存的地址,闪存在程序运行的时候是只读的不能修改,而本节需要对闪存进行修改,所需权限就比较高,需要对闪存解锁。但如果这个地址写的是SRAM的地址,比如0x20000000,那可以直接写入了,因为SRAM在程序运行时是可读可写的,这是使用指针访问存储器的C语言代码。

接下来就来看一下下面这三个流程图,第一个是编程,也就是写入,第二个是页擦除,STM32的闪存也是写入前必须擦除,擦除之后所有的数据位变为1,擦除的最小单位就一页,1K,1024字节,第三个是全擦除,把所有页都给擦除掉,那首先说一下这个详细的流程,库函数已经帮我们都写好了,我们直接调用一个整体的函数就行,非常简单,这里我们只大概的了解一下详细步骤,研究得越深,操作越得心应手。

第一步是读取Lock位,看一下芯片锁没锁,如果Lock位等于1锁住了,就执行解锁过程,解锁过程就是在KEYR寄存器先写入KEY1,再写入KEY2,这里如果它当前没锁住,就不用解锁了,这是流程图里给的解锁步骤,如果锁住了就解锁,如果没锁住就不用解锁,但是在库函数中并没有这个判断,库函数是直接执行解锁过程,管你锁没锁都执行解锁,这个比较简单直接,不过效果都一样;

然后解锁之后,首先置控制寄存器里的MER(Mass Erase)位为1,然后再置STRT(Start)位为1,其中STRT为1是触发条件,STRT为1之后芯片开始干活,然后现在看到MER位是1,它就知道接下来要干的活就是全删除,这样内部电路就会自动执行全擦除的过程;

然后继续擦除也是需要花一段时间的,所以擦除过程开始后,程序要执行等待,判断状态寄存器的BSY(Busy)位是否为1,BSY位表示芯片是否处于忙状态,BSY位为1表示芯片忙,所以这里如果判断BSY位等于1,就跳转回来继续循环判断,直到BSY位等于0跳出循环,最后一步这里写的是读出并验证所有页的数据,这个是测试程序才要做的,正常情况下全删除完成了,我们默认就成功了,如果还要再全读出来验证一下,这个工作量太大了,所以这里的最后一步我们就不管了,这是全擦除的流程。

然后看一下页擦除,这个也是类似的过程,第一步一样的是解锁的流程,第二步,这个方框里的置控寄存器的PER(Page Erase)位为1,然后在AR(Address Register)地址寄存器中选择要擦除的页,最后置控制寄存器的STRT位为1,也是触发条件,芯片开始干活,然后芯片看到PER等于1,它就知道接下来要执行页擦除,然后闪存不止一页,页擦除芯片就要知道要具体擦哪一页,所以它会继续看AR寄存器的数据,AR寄存器我们要提前写入一个页的起始地址,这样芯片就会把我们指定的一页给擦除掉,然后擦除开始之后,我们也要等待BSY位,最后读出并验证数据,这个就不用看了。

最后看一下闪存的写入,擦除之后我们就可以执行写入的流程了,另外说明一下,STM32的闪存在写入之前会检查指定地址有没有擦除,如果没有擦除就写入STM32则不执行写入操作,除非写入的全是0,这个数据是例外,因为不擦除就写入,可能会写入错误,但全写入0的话,写入肯定是没问题的,下面来看一下流程图。

写入的第一步也是解锁;

然后第二步我们需要置控制寄存器的PG(Programming)位为1,表示我们即将写入数据;

第三步就在指定的地址写入半字,这一步我们需要用到刚才介绍的使用指针访问存储器代码,使用指针在指定地址写入数据,想写入什么数据,在这里指定即可,另外这里注意一下,写入操作只能以半字的形式写入。

在STM32中有几个术语,字、半字和字节,其中字word就是32位数据,半字half word就是16位数据,字节byte就是8位数据,那这里只能以半字写入,意思就是只能以16位的形式写入,一次性写入两个字节,如果你要写入32位,就分两次完成,如果你只要写入八位,这个就比较麻烦了,如果你想单独写入一个字节,还要保留另一个字节的原始数据的话,那只能把整页数据都读到SRAM,再随意修改SRAM数据修改全部完成之后,再把整页都擦除,最后再把整页都写回去,所以如果你想像SRAM一样随心所欲的读写,那最好的办法就先把闪存的一页读到SRAM中,读写完成后再擦除一页,整体写回去

那回到流程图这里,写入数据这个代码就触发开始的条件,不需要像擦除一样置STRT位了,写了半字之后,芯片会处于忙状态,我们等待一下BUSY清0,这样写入数据的过程就完成了,那每执行这样一个流程,只能写入一个半字,如果要写出很多数据,要不断循环调用这个流程就可以了。

16.5 选项字节

接下来我们再介绍一下选项字节这块内容,大概了解一下就行了。

16.5.1 选项字节的组织和用途

图里的起始地址,就是我们刚才说的选项字节的起始地址0x1FFFF800,这块的这些数据,就前面这里这个表的这一行,里面总共只有16个字节,把这些存储器给展开,就这个图,这里是对应的16个字节,其中有一半的名称前面都带了个N,比如RDP和nRDP、USER和nUSER等,这个意思就是你在写入RDP数据时,要同时在NRDP写入数据的反码,其他的这些都是一样,写这个存储器时,要在带N的对应的存储器写入反码,这样写入操作才是有效的,如果芯片检测到这两个存储器不是反码的关系,那就代表数据无效有错误,对应的功能就不执行,这是一个安全保障措施,但这个写入反码的过程,硬件会自动计算并写入,不需要我们操心,使用库函数的话,那就更简单了,函数都给我们分装好了,直接调用函数就行,那然后看一下每个存储器的功能,去掉所有带n的就剩下八个字节存储器了:

  1. 第一个RDP(Read Protect)是读保护配置位,下面有解释,在RDP存储器写入RDPRT键,就刚才说的A5,然后解除读保护,如果RDP不是A5,那闪存就是读保护状态,无法通过调试器读取程序,避免程序被别人窃取;
  2. 第二个字节USER,这个是一些零碎的配置位,可以配置硬件看门狗和进入停机待机模式是否产生复位,这个了解即可;
  3. 第三个和第四个字节Data0和Data1,这个在芯片中没有定义功能,用户可自定义使用;
  4. 最后四个字节,WRP(Write Protect)0、1、2、3这四个字节配置的是写保护,在中容量产品里是每一个位对应保护四个存储页,四个字节总共32位,一位对应保护四页,总共保护32×4等于128页,正好对应中容量量的最大128页,那对于小容量和大容量产品呢,可以看一下手册,2.5选项字节说明这里,对于小容量产品,也是每一位对应保护四个存储页,但小容量产品最大只有32K,所以只需要一个字节WRP0就行,4×8=32,其他三个字节没用到,然而对于大容量产品,每一个位只能保护两个存储页,这样的话四个字节就不够用了,所以这里规定WRP3的最高位,这一位直接把剩下的所有页一起都保护了,这是写保护的定义。

16.5.2 选项字节的编程

看一下如何去写入这些位呢,这里两页PPT展示的就是选项字节的擦除和编程,因为选项字节本身也是闪存,所以它也得擦除,这里参考手册并没有给流程图,我们看一下这个文字流程,这个文字流程和流程图细节上有些出入,我们知道关键部分就行。

先看一下选项字节擦除,第一步其实也是解锁闪存,上图中文字并没有写,然后第二步这里文字版的流程多了一步,检查SR的BSY位,以确认没有其他正在进行的闪存操作,这个实际上就是事前等待,如果当前已经在忙了,我先等一下,这一步在刚才的流程图里并没有体现,然后下一步解锁CR的OPTWRE(Option Write Enable)位,这一步是选项字节的解锁,选项字节里面还有一个单独的锁,在解锁闪存后,还需要再解锁选项字节的锁,之后才能操作选项字节,解锁选项字节的话看一下前面的寄存器(前面闪存模块组织图),整个闪存的锁是KEYR,里面选项字节的小锁是下面的OPTKEYR(Option Key Register),解锁这个小锁也是类似的流程,我们需要在OPTKEYR里先写入KEY1,再写入KEY2,这样就能解锁选项字节的小锁了,然后继续解除小锁之后和之前的擦除类似,先设置CR的OPTER(Option Erase)位为1,表示即将擦除选项字节,之后设置CR的STRT位为1,触发芯片开始干活,这样芯片就会启动擦除选项字节的工作,之后等待BUSY位变为0,擦除选项字节就完成了,擦除之后就可以看写入了。

和普通的闪存写入也差不多,先检测BSY,然后解除小锁,之后设置CR的OPTPG(Option Programming)位为1,表示即将写入选项字节,再之后写入要编程的半字到指定的地址,这个是指针写入操作,最后等待忙,这样写入选项字节就完成了。

16.6 补充:器件电子签名

最后我们花几分钟学一下器件电子签名,这个非常简单,既然讲到闪存了,就顺便学习一下吧。

看一下电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写不可更改,使用指针读指定地址下的存储器,可获取电子签名,电子签名其实就是STM32的id号,它的存放区域是系统存储器,它不仅有BOOTLOADER程序,还有几个字节的id号,系统存储器起始地址是0x1FFFF000

上图中显示有两段数据,第一个是闪存容量存储器,基地址是0x1FFFF7E0,通过地址也可以确定它的位置,就是系统存储器,这个存储器的大小是16位,它的值就是闪存的容量单位是KB,然后第二个是产品唯一身份标识寄存器,就是每个芯片的身份证号,这个数据存放的基地址是0x1FFFF7E8,大小是96位,每一个芯片的这96位数据都是不一样的,使用这个唯一id号可以做一些加密的操作,比如你想写入一段程序,只能在指定设备运行,那也可以在程序的多处加入id号判断,如果不是指定设备的id号,就不执行程序功能,这样即使你的程序被盗,在别的设备上也难以运行,这是STM32的电子签名。

16.7 Flash实验

本文参考链接:江科大STM32最全笔记整理『终篇』-CSDN博客