个人网站空间多大合适,搜索广告排名,商派商城网站建设二次开发,做旅行社业务的网站都有哪些目录 SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件外设读写W25Q64 BKP备份寄存器、PER电源控制器、RTC实时时钟11.0 Unix时间戳代码示例#xff1a;读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR电源控制12.1 PWR简介代码示例#xff1a;修改主频12.3 串… 目录 SPI协议10.1 SPI简介W25Q64简介10.3 SPI软件读写W25Q6410.4 SPI硬件外设读写W25Q64 BKP备份寄存器、PER电源控制器、RTC实时时钟11.0 Unix时间戳代码示例读写备份寄存器BKP11.2 RTC实时时钟 十二、PWR电源控制12.1 PWR简介代码示例修改主频12.3 串口数据收发睡眠模式12.4 停止模式12.5 待机模式 十三、看门狗WDG13.1 WDG简介13.2 窗口看门狗WWDG代码示例实现IWDG13.4 实现WWDG STM32内部FLASH闪存STM32内部FLASH闪存简介示例代码读写内部FLASH读取芯片ID SPI协议
10.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的上升沿或下降沿进行的这样数据位的收发时刻就可以明确的确定并且同步时序时钟快点慢点或者中途暂停一会儿都是没问题的这是同步时序的好处。 硬件电路 所有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对这个可能的冲突做出的规定当然这个切换过程都是在从机里我们一般都写主机的程序所以我们主机的程序中并不需要关注这个问题。
移位示意图 这个移位示意图是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的数据收发都是基于字节交换当主机需要发送一个字节并且同时需要接收一个字节时就可以执行一下字节交换的时序这样主机要发送的数据跑到从机主机要从从机接收的数据跑到主机这就完成了发送同时接收的目的。
那你可能会问如果只想发送不想接收怎么办呢其实很简单我们仍然调用交换字节的时序发送同时接收只是这个接收到的数据我们不看它就行了那如果我只想接收不想发送怎么办呢同理我们还是调用交换自己的时序发送同时接收只是我们会随便发送一个数据只要能把从机的数据置换过来就行了我们读取置换过来的数据不就是接收了吗这里我们随便发过去的数据啊从机也不会去看它当然这个随便数据我们不会真的随便发啊一般在接收的时候我们会统一发送0x00或0x ff以上就是spi的基本原理。
总结一下就是spi通信的基础是交换一个字节有了交换一个字节就可以实现发送一个字节、接收一个字节和发送同时接收一个字节这三种功能可以看出spi在只执行发送或只执行接收的时候会存在一些资源浪费现象不过全双工本来就会有浪费的情况发生spi表示我不在乎好了。
SPI时序基本单元 接下来就是数据传输的基本单元了这个基本单元什么时候开始移位是上升沿移位还是下降沿移位spi并没有限定死可以配置选择这样的话spi就可兼容更多的芯片那在这里spi有两个可以配置的位分别叫做cpol、cpha每一位可以配置为一或零总共组合起来就有模式零模式一模式二模式三这四种模式当然模式虽然多但是它们的功能都是一样的在实际使用的时候我们主要学习其中一种就可以了剩下的模式你知道有这些东西可以配置如果到时候真的需要用再过来了解一下就行了。 那么先看一下模式一因为这个模式和我们刚才讲的移位模型是对应的这个时序的基本功能是交换一个字节也就是刚在这里我们展示的现象这里cpol等于零表示空闲状态时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等于1sck第一个边沿移出数据所以这里可以看出来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位移入完成整个字节交换完成之后sca还有一个下降沿如果主机只需要交换一个字节就结束那在这个下降沿时mosi可以置回默认电平或者不去管它miso也会变化一次这一位实际上是下一个字节的b7因为这个相位提前了所以下一个字节的b7会露个头如果不需要的话ss上升沿之后从机miso置回高阻态这是交换一个字节就结束如果主机想交换多个字节的话那就继续调用从这里到这里的时序在最后一个下降沿主机放下一个字节的b7 从机也放下一个字节的b7 skc上升沿正好接着采样第二个字节的b7这样时序才能拼接得上就是spi交换一个字节模式零模式零和模式一的区别就在于模式零把这个数据变化的时机给提前了在实际应用中模式零的应用是最多的所以我们重点掌握模式零即可后续的程序都是基于spi模式零来讲解的不过这里我感觉模式一是不是更符合常理但实际确实是模式零用的最多可能是spi设计的时候为了兼容现存设备吧或者是模式0在实际应用时确实有什么优势或者因为模式零排在最前面大家都默认最前面的模式吗这个原因大家感兴趣的话可以调研一下。 这个cpha表示的是时钟相位决定是第一个时钟采样移入还是第二个时钟采样移入并不是规定上升沿采样还是下降沿采样的当然在cpol确定的情况下cpha确实会改变采样时刻的上升沿和下降沿 比如模式0的时候是sck上升沿采样移入模式1的时候是sck下降沿采样移入cpha决定的是第几个边沿采样并不能单独决定是上升沿还是下降沿在这四种模式里模式零和模式三都是sck上升沿采样模式一和模式二都是sck下降沿采样。
看几个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第一个上升沿进行数据采样我这里画了一条绿线从机采样输入得到零主机采样输入得到一之后继续第二个时钟主机数据仍然是零所以波形仍然没有变化然后这样一位一位的发送接收发送接收到这一位数据才开始变化主机要发送数据一下降沿数据移出主机将一移出到mosimosi变为高电平这里因为是软件模拟的时序所以mosi的数据变化有些延迟没有紧贴sck的下降沿不过这也没关系时钟是主机控制的我们只要在下一个sck上升沿之前完成变化就行了然后sck上升沿数据采样输入在最后一位呢下降沿数据变化mosi变为零上升沿数据采样从机接收数据0sck低电平是变化的时期高电平是读取的时期这一块是不是和I2C差不多那时序sck最后一个上升沿结束一个字节就交换完毕了因为写使能是单独的指令不需要跟随数据spi只需要交换一个字节就完事了所以最后在sck下降沿之后ss置回高电平结束通信那这个交换我们统计一下mos i和miso的电平总结一下就是主机用0x06换来了从机的0xff但实际上从机并没有输出这个0xf f是默认的高电平不过这个0xf f没有意义我们不用管那整个时序的功能就是发送指令指令码是0x06 从机一比对事先定义好的指令集发现0x06是写使能的指令那从机就会控制硬件进行写使能这样一个指令从发送到执行就完成了就是发送单字节指令的时序。 指定地址写 向SS指定的设备发送写指令0x02随后在指定地址Address[23:0]下写入指定数据 Data
我们这个w25q64 芯片有8M字节的存储空间一个字节的八位地址肯定不够所以这里地址是24位的 分三个字节传输我们看一下时序首先ss下降沿开始时序mosi空闲时是高电平所以在下降沿之后sck第一个时钟之前可以看到mosi变换数据由高电平变为低电平然后sck上升沿数据采样输入后面还是一样下降沿变换数据上升沿采样数据八个时钟之后一个字节交换完成我们用0x02换来了0xff其中发送的0x02是一条指令代表这是一个写数据的时序接收到0x ff不需要看那既然是写数据的时序后面必然还要跟着写的地址和数据所以在最后一个下降沿时刻因为我们后续还需要继续交换字节所以在这个下降沿我们要把下一个字节的最高位放到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 x34 0x56 组合到一起就是0x123456代表24位地址最后这个地方就是关键点因为我们是读取数据指定地址之后显然我们就要开始接收数据所以这里三个字节的地址交换完之后我们要把从机的数据搞过来怎么搞过来呢我们还是交换一个数据 来个抛砖引玉我们随便给从机一个数据一般给ff就行了从机就会乖乖的把0x123456地址下的数据通过miso发给主机可以看到这样的波形就表示指定地址下的数据是0x55 这样主机就实现了指定地址读一个字节的目的然后如果我们继续抛砖引玉那么从机内部的地址指针自动加一从机就会继续把指定地址下一个位置的数据发过来这样依次进行就可以实现指定地址接收多个字节的目的了最后数据传输完毕ss置回高电平时序结束当然时序这里也会有些细节比如由于miso是硬件控制的波形所以它的数据变化都可以紧贴时钟的下降沿另外我们可以看到miso数据的最高位实际上是在上一个字节最后一个下降沿提前发生的因为这是spi模式零所以数据变化都要提前半个周期。 W25Q64简介 低成本也就是说这个芯片一般也就几块钱。更换不通过型号我们的硬件电路和底层驱动程序都不需要更改所以我们学会了其中一个型号在应用同系列的其他型号就很容易上手了。
字库存储这个可以应用到一些显示屏上比如我们这个oled显示屏或者lcd液晶屏你如果想在屏幕上显示汉字就得把汉字的点阵数据存起来当然简单的方法是把字库直接存在stm32内部 这样适合少量汉字显示的情况如果汉字非常多再直接存在s t m32 中就不合适了所以我们可以用这个芯片来存储汉字在显示某个汉字之前先读取芯片查询字库再在显示屏上显示对应的点阵数据。 固件程序存储这个就相当于直接把程序文件下载到外挂芯片里需要执行程序的时候直接读取外挂芯片的程序文件来执行这就是xip就地执行比如我们电脑里的bios固件就可以存储在这个系列的芯片里。
这个芯片的存储介质是Nor Flashflash就是闪存存储器像我们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模式很显然就是一个时钟发送或接收四位等效的频率就是80x4320MHz在我们这个芯片里啊除了spi通信引脚还有两个引脚一个是wp写保护另一个是hold这两个引脚如果不需要的话也可以拉过来充当数据传输引脚加上mosi和miso就可以四个数据位同时收发了就是四重spi其实这就有点并行传输的意思了串行是根据时钟一位一位的发并行是一个时钟八位同时发送所以这个四重spi模式其实就是四位并行的模式这个大概了解一下就行。
这个芯片使用的是24位的地址24位地址是三个字节因为我们在进行读写的时候肯定得把每个字节都分配一个地址这样才能找到它们上小节讲时序的时候也提到过这里在指定地址时需要一次性指定三个字节24位的地址然后我们可以用计算器算一下24位的地址最大能分配多少个字节呢 这里2的24次方等于这么多个字节数那除1024等于这么多kb再除102416MB所以24位地址的最大寻址空间是16MB那ppt中w25q40到q128使用三字节24位的地址都是足够的但是这个w25q256就比较尴尬了24位地址对于32MB来说是不够的所以这最后一个型号比较特殊根据手册里描述w25q256分为三字节地址模式和四字节地址模式在三字节地址模式下只能读写前16MB的数据后面16MB 3个字节的地址够不着要想读写到所有存储单元可以进入四字节地址的模式这样就行了。 看一下这个芯片的硬件电路当我们拿到这个八脚的芯片后怎么把它和stm32连接在一起呢我们看一下左边这个图是我们这个小模块的原理图右上角这个图就是这个芯片的引脚定义右下角这个表就是每个引脚定义的功能呢首先看一下引脚定义VCC、GND是电源供电引脚供电电压是2.7~3.6V是一个典型的3.3V供电设备不能直接接入5V电压然后1号脚cs这个cs左边画了个斜杠代表是低电平有效或者这边cs上面画了个横线也是低电平有效那这里cs对应之前我们讲spi的名称就是SS意思是spi的片选引脚6号脚clk对应就是sck是spi的时钟线然后5号引脚di对应mosi是spi主机输出从机输入2号do对应miso是spi主机输入从机输出这四个引脚就是spi通信的四个引脚。
然后这个芯片还有两个引脚3号引脚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显然是一个电源指示灯通电就亮那这些就是这个芯片的硬件电路了。
W25Q64框图
我们看一下w25q64 是怎么划分的首先右边这一整个矩形空间里是所有的存储器存储器以字节为单位每个字节都有唯一的地址这样说了w25q64的地址宽度是24位3个字节所以可以看到左下角第一个字节它的地址是00 00 00hh代表16进制之后的空间地址依次自增直到最后一个字节地址是7F FF FF h那最后一个字节为啥是7f开头不是f f开头呢因为24位地址最大寻址范围是16MB我们这个芯片只有8MB所以地址空间我们只用了一半8MB排到最后一个字节就是7F FF FF h那这是整个地址空间从000000~7F FF FF然后在这整个空间里我们以64kb为一个基本单元把它划分为若干的块block从前往后依次是块0块1块2等等一直分到最后一块那整块蛋糕是8MB以64kb为一块进行划分最后分得的快数就是8MB除以64kB这里可以分得128块那块序号就是块0一直到最后一个是块127然后观察一下块内地址值的变化规律比如块0的起始地址是000000结束地址是00f f f f之后块31起始是1f0000 结束是1f f f f f之后的都观察一下可以发现在每一块内它的地址变化范围就是最低的两个字节每个块的起始地址是XX0000结束是XXf f f f这是块内地址的变化规律到这里这一块大蛋糕我们就分好块了64kb为一块总共128块之后看一下左边这个示意图我们还要再对每一块进行更细的划分分为多个扇区sector这里的虚线看到没指向了右边的各个块也就是告诉你每一块里面都是这个样子的那在每个块里它的起始地址是XX0000结束地址是XXf f f f在一块里我们再以4kb为一个单元进行切分一块是64kb我4kb一切总共16份所以在每一块里都可以分为扇区0一直到扇区15观察一下地址规律可以发现每个扇区内的地址范围是XXX000到XXXf f f地址划分啊到扇区就结束了但是当我们在写入数据时啊还会有个更细的划分就是页Page页是对整个存储空间划分的当然你也可以把它看作在扇区里再进行划分都一样那页的大小是256个字节一个扇区是4kb所以一个扇区里可以分为16页然后页的地址规律呢我们也看一下在这里每一行就是一页左边这里指了个箭头写的是页地址的开始右边这里也指了个箭头写的是页地址的结束在一页中地址变化范围是XXXX00到XXXXFF一页内的地址变化仅限于地址的最低一个字节这就是页的划分那这个存储器的地址划分啊我就讲完了 我们需要记住的是一整个存储空间首先划分为若干块对于每一块又划分为若干扇区然后对于整个空间会划分为很多很多页每页256字节这个我们需要记住。
左下角这是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字节所以一页内的字节地址就取决于最低一个字节而高位的两个字节就对应的是页体质所以在这里我们发的三个字节地址前两个字节会进到这个页地址锁存计数器里最后一个字节会进到这个字节地址锁存计数器里然后页地址通过这个写保护和行解码来选择我要操作哪一页字节地址通过这个列解码和256字节页缓存来进行指定地址的读写操作那就因为我们这个地址锁存都是有个计数器的所以这个地址指针在读写之后可以自动加1这样就可以很容易实现从指定地址开始连续读写多个字节的目的了那最后右边这里有个256字节的页缓存区它其实是一个256字节的ram存储器这个稍微留个印象等会儿还会提到然后我们数据读写就是通过这个ram缓冲区域来进行的我们写入数据会先放到缓存区里然后在时序结束后芯片再将缓冲区的数据复制到对应的flash里进行永久保存那为啥要弄个缓冲区呢我们直接往flash里写不好吗那这是因为我们的spi写入的频率是非常高的而flash的写入由于需要掉电不丢失留下刻骨铭心的印象他就比较慢所以这个芯片的设计思路就是你写入的数据我先放在缓存区里存着因为缓存区是ram所以它的速度非常快啊可以跟得上spi总线的速度但这里有个小问题就这个缓冲区只有256字节所以写入的时序有个限制条件就是写入了一个时序连续写入的数据量不能超过256字节然后等你写完了我芯片再慢慢的把数据从缓冲区转移到flash存储器里那么数据从缓存区转到flash里需要一定的时间哈所以在写入时序结束后芯片会进入一段忙的状态在这里它就会有一条线哈通往状态寄存器给状态接容器的busy位置1表示芯片当前正在搬砖呢很忙那在忙的时候芯片就不会响应新的读写时序了哈就是写入的执行流程然后我们读取数据虽然这里画的话应该也是会通过缓冲区来读句但是由于读取只看一下电路的状态就行了它基本不花时间所以读取的限制就很少了速度也非常快。 flash的写入和读取并不像ram那样简单直接 ram是指哪打哪想在哪写就在哪写想写多少就写多少并且ram是可以覆盖写入的但是flash并没有这个特性啊总之flash的读写有很多要求其中写入的要求是非常多的需要我们掌握读取的要求就比较少了还是那个原因因为读取啊只是看一下电路的状态不对电路做出实质性的改变所以读取一般都比较快而且没有什么限制那我们看一下flash写入操作时需要注意些什么呢
第一点写入操作前必须先进行写使能这个是一种保护措施防止你误操作的就像我们使用手机一样先解锁再操作这样可以防止手机在你裤兜里到处点点点对吧写使能的话我们就使用spi发送一个写使能的指令就可以完成了然后下一条每个数据位只能由1改写为0不能由0改写为1这个意思就是说flash并没有像ram那样的直接完全覆盖改写的能力比如在某一个字节的存储单元里面存储了0xAA这个数据对应的二进制位就是10101010如果我直接再次在这个存储单元写入一个新的数据比如我再次写入一个0x55 那写完之后这个存储单元里存的是x55实际上并不是因为0x55的二进制是01010101当这个01010101要覆盖原来的10101010时就会受到这里第二条规定的限制每个数据位只能由一改写为零不能由零改写为一你要问为啥会有这个限制那只能说是成本原因或者技术原因所以这里写入01010101之后依次来看啊最高位由原来的1改写为0是可以的 所以写出之后新的最高位就是零但是第二位原来是零现在我要改写成1这是不行的所以写入之后新的第二位仍然是零之后第三位要改写为零可以结果为零第四位零改写为1不可以 结果仍然是零那以这个规律进行下去0xaa在覆盖写入0x55 之后这个存储单元最终的数据是什么啊0x00也就是八位全为零这就出现问题了对吧所以为了弥补这个只能1改0不能0改1的缺陷 我们就引出了第三条规定就是写入数据前必须先擦除擦除后所有数据位变为一在这里flash是有一个擦除的概念的擦除会有专门的擦除电路进行我们只要给他发送擦除的指令就行了那通过擦除电路擦除之后所有的数据位都变成一这样我们是不是就可以弥补第二条限制的缺陷了当我们写出一个数据之前无论原来存的是什么我直接给它擦除掉擦除之后所有的位变成1也就是16进制的f f这样我无论再写入什么样的数据就都可以正确的写入了。
那总结一下就是flash中数据位为一的数据拥有单项改成零的权利一旦改写为0之后就不能反悔 再改写成1了要想反悔就必须得先擦除所有的位先统一都变成一然后再重新来过这是flash改写的特性。
如果你说我非不擦除直接改写这样的操作可以执行但是存储的数据极有可能是错的这个注意一下那拆除之后所有的位变1就是16进制的ff所以有时候你读取flash会发现数据全是f f那就说明这一段有可能是擦除之后还没有写入数据的空白空间在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这种非易失性存储器目前的市场竞争力还是非常大的尽管它有这么多不方便但是这些不方便可以用软件来弥补而它的优点是其他存储器比不了的比如容量大价格低。
芯片手册 看出来这个flash芯片的写入时间一般情况下大概处于一个毫秒的时间级别。 10.3 SPI软件读写W25Q64
接线图
软件模拟的spi这四根线是可以接到stm32的任意GPIO口软件模拟的通信端口灵活性高这里我是这样来接的cs片选接到PA4 DO从机输出接到PA6 CLK时钟接到PA5 DI从机输入接到PA7当然我这里引脚其实并不是任意选的实际上是接到了硬件spi的引脚上这样的话软件spi和硬件spi都可以任意切换。 代码
main.c
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_Hvoid MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);#endifMySPI.c
#include stm32f10x.h // Device header/*引脚配置层*//*** 函 数SPI写SS引脚电平* 参 数BitValue 协议层传入的当前需要写入SS的电平范围0~1* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置SS为低电平当BitValue为1时需要置SS为高电平*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue设置SS引脚的电平//SPI的操作速度非常快暂时不需要加延时
}/*** 函 数SPI写SCK引脚电平* 参 数BitValue 协议层传入的当前需要写入SCK的电平范围0~1* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置SCK为低电平当BitValue为1时需要置SCK为高电平*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue设置SCK引脚的电平
}/*** 函 数SPI写MOSI引脚电平* 参 数BitValue 协议层传入的当前需要写入MOSI的电平范围0~0xFF* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置MOSI为低电平当BitValue非0时需要置MOSI为高电平*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue设置MOSI引脚的电平BitValue要实现非0即1的特性
}/*** 函 数I2C读MISO引脚电平* 参 数无* 返 回 值协议层需要得到的当前MISO的电平范围0~1* 注意事项此函数需要用户实现内容当前MISO为低电平时返回0当前MISO为高电平时返回1*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回
}/*** 函 数SPI初始化* 参 数无* 返 回 值无* 注意事项此函数需要用户实现内容实现SS、SCK、MOSI和MISO引脚的初始化*/
void MySPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA6引脚初始化为上拉输入/*设置默认电平*/MySPI_W_SS(1); //SS默认高电平 不选中MySPI_W_SCK(0); //SCK默认低电平 SPI模式0
}/*协议层*//*** 函 数SPI起始* 参 数无* 返 回 值无*/
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS开始时序
}/*** 函 数SPI终止* 参 数无* 返 回 值无*/
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS终止时序
}/*** 函 数SPI交换传输一个字节使用SPI模式0* 参 数ByteSend 要发送的一个字节* 返 回 值接收的一个字节*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)//有的地方定义成 readwrite()
{uint8_t i, ByteReceive 0x00; //定义接收的数据并赋初值0x00此处必须赋初值0x00后面会用到for (i 0; i 8; i ) //循环8次依次交换每一位数据{MySPI_W_MOSI(ByteSend (0x80 i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线MySPI_W_SCK(1); //拉高SCK上升沿移出数据if (MySPI_R_MISO() 1){ByteReceive | (0x80 i);} //读取MISO数据并存储到Byte变量//当MISO为1时置变量指定位为1当MISO为0时不做处理指定位为默认的初值0MySPI_W_SCK(0); //拉低SCK下降沿移入数据}return ByteReceive; //返回接收到的一个字节数据
}W25Q64_Ins.h
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3#define W25Q64_DUMMY_BYTE 0xFF#endifW25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_Hvoid W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);#endifW25Q64.c
#include stm32f10x.h // Device header
#include MySPI.h
#include W25Q64_Ins.h/*** 函 数W25Q64初始化* 参 数无* 返 回 值无*/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底层的SPI
}/*** 函 数MPU6050读取ID号* 参 数MID 厂商ID使用输出参数的形式返回* 参 数DID 设备ID使用输出参数的形式返回* 返 回 值无*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令*MID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID通过输出参数返回 随便发送一个数据oxFF交换过来就行了*DID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位*DID 8; //高8位移到高位*DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位通过输出参数返回MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64写使能* 参 数无* 返 回 值无*/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 0x06MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64等待忙 读状态寄存器1* 参 数无* 返 回 值无*/
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 0x05Timeout 100000; //给定超时计数时间while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01) //寄存器最低位busy位表示忙碌 循环等待忙标志位{Timeout --; //等待时计数值自减if (Timeout 0) //自减到0后等待超时{/*超时的错误处理代码可以添加到此处*/break; //跳出等待不等了}}MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64页编程* 参 数Address 页编程的起始地址范围0x000000~0x7FFFFF* 参 数DataArray 用于写入数据的数组* 参 数Count 要写入数据的数量范围0~256* 返 回 值无* 注意事项写入的地址范围不能跨页*/ //一页256字节 uint8_t最大255 所以定义成uint16_t
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i 0; i Count; i ) //循环Count次{MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据}MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙 事后等待也可以放到函数最开始事前等待
}/*** 函 数W25Q64扇区擦除4KB 其他擦除都是类似的* 参 数Address 指定扇区的地址范围0x000000~0x7FFFFF* 返 回 值无*/ //所在字节的整个扇区都擦除
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数W25Q64读取数据* 参 数Address 读取数据的起始地址范围0x000000~0x7FFFFF* 参 数DataArray 用于接收读取数据的数组通过输出参数返回* 参 数Count 要读取数据的数量范围0~0x800000* 返 回 值无*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 0X03MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i 0; i Count; i ) //循环Count次 可以跨页读 想读多少读多少{DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE); //存储器芯片内部地址指针自动自增 依次在起始地址后读取数据}MySPI_Stop(); //SPI终止
}main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include W25Q64.huint8_t MID; //定义用于存放MID号的变量
uint16_t DID; //定义用于存放DID号的变量uint8_t ArrayWrite[] {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化/*显示静态字符串*/OLED_ShowString(1, 1, MID: DID:);OLED_ShowString(2, 1, W:);OLED_ShowString(3, 1, R:);/*显示ID号*/W25Q64_ReadID(MID, DID); //获取W25Q64的ID号OLED_ShowHexNum(1, 5, MID, 2); //显示MIDOLED_ShowHexNum(1, 12, DID, 4); //显示DID/*W25Q64功能函数测试*/W25Q64_SectorErase(0x000000); //扇区擦除 字节所在扇区最好传入扇区起始地址W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中/*显示数据*/OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //显示写入数据的测试数组OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //显示读取数据的测试数组OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}不擦除直接写读出的数据原始数据写入的数据 跨页写回到此页页首覆盖页的最前面写入。 如果你确实有一个很大的数字要连续写入那就只能自己从软件上分批次进行写入了就是先计算你的数据总共需要跨多少页然后该擦除的擦除最后再分批次一页一写这个操作可以封装成一个函数之后调用封装的函数就可以跨页连续写入了。 10.4 SPI硬件外设读写W25Q64 跟之前I2C的思路一样软件spi就是我们用代码手动翻转电平来实现时序硬件spi就是使用stm32内部的spi外设来实现时序两种实现方法各有优势软件实现主打的是方便灵活硬件实现主打的是高性能、节省软件资源。
stm32内部spi外设的一些功能和技术参数其实手册里介绍这些功能还是比较繁杂的这是因为硬件电路不像软件那么灵活硬件电路一旦设计出来它的功能基本上就定死了之后只能通过一些开关电路、数据选择器等等来微调电路的运行不像软件那样改代码就行所以stm32 设计时就要考虑最全面的应用场景把各种可能的结构都设计出来放在那以免你用的时候找不到那这样就会导致外设电路的结构和知识点非常多而且有很多功能我们基本上很少用到所以stm32我们要使用主线加分支的学习方法我们先把最常用最简单的主线知识点给贯通给他学会了然后再逐渐细化在实践中去慢慢探索这些分支这样学习起来才是比较容易所以大家在看手册有一些感觉非常偏又非常难的知识点可以先不必深究先把主线任务学习好其他的可以之后再研究。
时钟频率就是sck波形的频率一个sck时钟交换一个bit所以时钟频率一般体现的是传输速度单位是Hz或者bit/s那这里的时钟频率是fPCLK除以一个分频系数分频系数可以配置为2或4或8、16、32、64、128、256所以可以看出来spi的时钟其实就是由pclk分频得来的pclk就是外设时钟APB2的pclk就是72MHzAPB1的pclk是36MHz比如我们的spi1是APB2的外设pclk等于72MHz 那它的spi时钟频率最大就是只进行二分频36MHz像我们之前I2C的频率最大就只有400KH所以这里spi的最大频率比I2C快了90倍然后这里频率有些注意事项一是这个频率数值并不是任意指定的它只能是pclk执行分频后的数值就只有这八个选项最低频率是pclk的256分频二是spi1和spi2 挂载的总线是不一样的spi1挂载在APB2pclk是72MHzspi1挂载在APB1pclk是36MHz所以同样的配置spi1的时钟频率要比spi2的大一倍。
SPI框图
接下来看一下spi的框图我们可以大致把它分成两部分左上角这一部分就是数据寄存器和移位寄存器打配合的过程这个和串口、I2C那里的设计思路都是异曲同工的主要是为了实现连续的数据流一个个数据前仆后继的一个效果然后剩下右下角这一部分就是一些控制逻辑寄存器的哪些位控制哪些部分会产生哪些效果这个可以通过手册的寄存器描述来得知至于执行细节这里也没详细画我们就知道功能就行了。
那我们接着来详细看一下每部分的功能首先左上角核心部分就这个移位寄存器右边的数据低位一位一位的从mosi移出去然后miso的数据一位一位的移入到左边的数据高位显然移位寄存器应该是一个右移的状态所以目前图上表示的是低位先行的配置对应右下角有一个LSBFIRST的控制位这一位可以控制是低位先行还是高位先行手册里寄存器描述可以查一下这里LSBFIRST帧格式给0先发送msbmsb就是高位的意思给1先发送lsb lsb就是低位的意思那ppt这里目前的状态LSBFIRST的应该是1低位先行如果LSBFIRST给零高位先行的话这个图还要变动一下就是移位寄存器变为左移输出从左边移出去输入从右边移进来这样才符合逻辑然后继续看左边这一块这里画了个方框里面把mosi和miso做了个交叉这一块主要是用来进行主从模式引脚变换的我们这个spi外设可以做主机也可以做从机做主机时这个交叉就不用mosi为mo主机输出miso为mi主机输入这是主机的情况如果我们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全双工发送和接收同步进行所以它的数据寄存器发送和接收是分离的而移位寄存器发送和接收可以共用然后看一下前面iphone c的框图因为I2C是半双工发送和接收不会同时进行所以它的数据寄存器和移位寄存器 发送和接收都可以是共用的。串口是全双工并且发送和接收可以异步进行所以这就要求它的数据寄存器发送和接收是分离的移位寄存器发送和接收也得是分离的。
然后接下来我们看一下右下角这些内容这就是一些控制逻辑首先是波特率发生器这个主要就是用来产生sck时钟的它的内部主要就是一个分频器输入时钟是pclk72M或36M经过分频器之后输出到sck引脚当然这里生成的时钟肯定是和移位寄存器同步的每产生一个周期的时钟移入移出一个bit然后右边CR1寄存器的三个位BR0、BR1、BR2用来控制分频系数从这里可以看一下手册这里看到BR[2:0] 是波特率控制这三位写入下面这些值可以对pclk时钟执行2~ 256的分频分频之后就是sck时钟所以这一块就对于来之前这里说的时钟频率是fpclk的2~256分频那这就是波特率发生器的部分。
接着后面这些通信电路和各种寄存器都是一些黑盒子电路如果你要具体研究可以看一下这些位的寄存器描述我挑几个重点的讲一下比如lsb first的刚才说过决定高位先行还是低位先行spe是spi使能就是SPI_Cmd函数配置的位BR配置波特率就是sck时钟频率MSTR(Master)配置主从模式1是主模式0是从模式我们一般用主模式cpul和cpha这个之前讲过用来选择spi的四种模式然后这里sr状态计算器最后两个txe发送寄存器空rxne接收寄存器非空这两个比较重要我们发送接收数据的时候需要关注这两位最后CR2寄存器就是一些使能位了,比如中断使能dma使能等然后剩下的一些位用的不多大家可以在自行研究那最后这里还有一个NSS引脚ss就是从机选择低电平有效所以这里前面加了个n这个nss和我们想象的从机选择可能不太一样我们想象的应该是用来指定某个从机对吧但是根据手册里的描述,我也研究了一下这里的nss设计可能更偏向于实现这里说的多主机模型总的来说啊这个NSS我们并不会用到SS引脚我们直接使用一个gpo模拟就行因为ss引脚很简单就置一个高低电平就行了而且从机的情况下ss还会有多个这里硬件的nss也完成不了我们想要的功能那这个nss是如何实现多主机切换的功能呢我简单介绍一下啊大家听一听就行不用掌握假如这里有三个stm32设备我们需要把这三个设备的nss全都连接在一起首先这个nss可以配置为输出或者输入当配置为输出时可以输出电平告诉别的设备我现在要变为主机你们其他设备都给我变从机不要过来捣乱当配置为输入时可以接收别设备的信号当有设备是主机拉低nss后,我就无论如何也变不成主机了这就是它的作用然后内部电路的设计当这里这个ssoe等于1时nss作为输出引脚并在当前设备变为主设备时给nss输出低电平这个输出的低电平就是告诉其他设备我现在是主机了当主机结束后ssoe要清0nss变为输入这时输入信号就会跑到右边这里这个数据选择器ssm位决定选择哪一路当选择上面一路时是硬件nss模式也就是说这时外部如果输入了低电平那当前的设备就进入不了主模式了因为nss低电平肯定是外部已经有设备进入了主模式他已经提前告诉我他是主模式了我就不能再跟大家抢了当数据选择器选择下面一路时是软件管理nss输入nss是1还是2由这一位SSI来决定这个就是nss实现多主机的思路但这个设计是n s s作为多从机选择的作用消失了揪出所有人的小辫子之后主机发送的数据就只能是广播发送给所有人的如果想实现指定设备通信可能还需要再加入寻址机制所以实现起来还是比较复杂的但我自己其实也没试过这种玩法这里是根据我看手册的理解我觉得应该是这样玩的哈不过spi最多的情况还是一主多从或者一主一从我们掌握一主多从就行多主机的情况了解即可好。
SPI基本结构 那看完了详细的框图我们再看一下这里我总结了一个简化结构图这个结构我把上面这个框图 无关的东西都去掉了这样看起来是不是就更容易理解其中核心部分当然就是这个数据寄存器和移位寄存器这里发送和接收我直接叫做发送数据寄存TDR器和接收数据寄存器RDR了因为我觉得这样表示更清晰之前串口框图里也是这样表示的哈但是spi框图这里它又叫发送缓冲区和接收缓冲区命名可能不太统一因为这个手册可能是多个人分工写最后整合到一起的所以有时候我就发现手册不同的章节描述手法和词汇可能都不一样但是大家要有自己的判断知道他们其实是一个东西就行然后这里移位寄存器我画的是左移高位移出去通过GPIO到MOSI从MOSI输出显然这是spi的主机对之后引入的数据从miso进来通过gpio到移位寄存器的低位这样循环八次就能实现主机和从机交换一个字节然后tdr和rdr的配合可以实现连续的数据流这刚才和之前的课程已经分析过很多次了另外tdr数据整体转入移位寄存器的时刻置txe标志位移位寄存器数据整体转入RDR的时刻置RXNE标志位tdr、txe 、rxne这几个词再记一下等会儿会经常提到然后剩下的波特率发生器产生时钟输出到sck引脚数据控制器就看成是一个管理员它控制着所有电路的运行最后开关控制就是SPI_Cmd初始化之后给个ENABLE初始化整个外设另外这里我并没有画ss从机选择引脚这个引脚我们还是使用普通的GPIO来模拟即可在一主多从的模型下GPIO模拟的ss是最佳选择这就是spi的系统框图和简化的结构了我们在写代码的时候会用一个结构体来统一配置这些部分。
那初始化部分解决之后我们就要来看一些运行控制的部分了如何来产生具体的时序呢什么时候写dr什么时候读dr呢这是我们接下来学习的知识点读写dr产生时序的流程我们主要看这两个时序图即可。
第一个是主模式全双工连续传输这个图演示的是借助缓冲区数据前仆后继实现连续数据流的过程但是这个流程稍微比较复杂也不太方便封装所以在实际过程中如果对性能没有极致的追求 我们更倾向使用下面这个非连续传输的示意图这个非连续传输使用起来更加简单实际用的话只需要四行代码就能完成任务了那参考网上别人的代码呢基本上都是非连续传输的方式我们课程也使用非连续传输的代码非连续传输的好处就是容易封装好理解好用但是会损失一丢丢性能连续传输呢传输更快但是操作起来相对复杂那我们来分别具体分析一下。 先看一下主模式全双工连续传输的意图首先第一行是sck时钟线这里cpol等于1cpha等于1示例使用的是spi模式三所以sck默认是高电平然后在第一个下降沿mosi和miso移出数据之后上升沿引入数据依次这样来进行那下面第二行是mosi和mi o输出的波形跟随sck时钟变化数据位依次出现这里从前到后依次出现的是b0b1一直到b7 所以这里示例演示的是低位先行的模式啊实际spi高位先行用的多一些最后第三行是txe发送寄存器空标志位波形是这样的等会儿再分析下面继续看是发送缓冲器括号写入SPI_DR实际上就是这里的TDR然后bsy busy是由硬件自动设置和清除的当有数据传输时busy置1那上面这部分演示的就是输出的流程和现象然后下面是输入的流程和现象第一个是miso/mosi的输入数据之后是RXNE接收数据寄存器非空标志位最后是接收缓冲器读出SPI_DR显然就是这里的RDR了了解完各个信号的定义了。
我们来从左到右依次分析首先ss置低电平开始时序这个没画但是必须得有的在刚开始时TXE为1 表示TDR空可以写入数据开始传输然后下面指示的第一步就是软件写入0xf1至SPI_DR0xf1就是要发送的第一个数据之后可以看到写入之后TDR变为0xf1 同时txe变为0表示tdr已经有数据了那此时d r是等候区移位寄存器才是真正的发送区移位寄存器刚开始肯定没有数据所以在等候区TDR里的f1 就会立刻转入移位寄存器开始发送转入瞬间置txe标志位为1表示发送寄存器空然后移位寄存器有数据了波形就自动开始生成当然我感觉这里画的数据波形时机可能有点早应该是在这个时刻b0的波形才开始产生在这之前数据还没有转入移位进器所以感觉b0出现的可能过早了不过这个也不影响我们理解大家知道这意思就行好了这样数据转入移位寄存器之后数据F1的波形就开始产生了在移位产生f1波形的同时等候区tdr是空的为了移位完成时下一个数据能不间断的跟随这里我们就要提早把下一个数据写入到TDR里等着了所以下面只是第二步的操作是写入F1之后软件等待TXE等于1在这个位置一旦tdr空了我们就写入F2至SPI_DR写入之后可以看到tdr的内容就变成F2了也就是把下一个数据放到tdr里后者之后的发送流程也是同理最后在这里如果我们只想发送三个数据F3转入移位寄存器之后TXE等于1我们就不需要继续写入了txe之后一直是1注意在最后一个TXE等于1之后还需要继续等待一段时间f3的波形才能完整发送完等波形全部完整发送之后busy的标志由硬件清除这才表示波形发送完成了那这些就是发送的流程然后继续看一下下面接收的流程SPI是全双工发送的同时还有接收所以可以看到在第一个字节发送完成后第一个字节的接收也完成了接收到的数据1是A1 这时移位寄存器的数据整体转入RDRRDR随后存储的就是A1 转入的同时按RXNE标志位也置1表示收到数据了我们的操作是下面这里写的软件等待RXNE等于11表示收到数据了然后从SPI_DR也是RDR读出数据A1 这是第一个接收到的数据接收之后软件清除RXNE标志位然后当下一个数据2收到之后RXNE重新置1我们监测到RXNE等于1时就继续读出RDR这是第二个数据A2 最后在最后一个字节时序完全产生之后数据三才能收到所以数据3直到这里才能读出来然后注意,一个字节波形收到后移位寄存器的数据自动转入RDR会覆盖原有的数据所以我们读出rdr要及时比如A1这个数据收到之后最迟你也要在这里把它读走否则下一个数据A2覆盖A1就不能实现连续数据流的接收了这是整个发送和接收的流程这个交换的流程是交错的对我们程序设计不太友好总之如果你对效率要求很高就研究下这个否则的话我们更推荐下面这个非连续传输。 非连续传输对于程序设计非常友好只需要四行代码就可以完成那它是怎么执行的呢我们来看一下这个就是非连续传输发送的示意图下面这里只有发送的一些波形接收部分的波形没画出来但是我们也可以想象得到接收是什么样子的等会儿我也会给大家展示一下接收的波形那我们看一下这个非连续传输和连续传输有什么区别呢首先这个配置还是spi模式三sck默认高电平我们想发送数据时如果检测到TXE等于1了TDR为空就软件写入0xF1至SPI_DR这时TDR的值变为F1 TXE变为0目前移位寄存器也是空所以这个F1会立刻转入移位寄存器开始发送波形产生并且TXE置回1表示你可以把下一个数据放在tdr里侯着了但是现在区别就来了在连续传输这里一旦TXE等于1了我们就会把下个数据写到tdr里侯着这样是为了连续传输数据衔接更紧密但是刚才说了 这样的话流程就比较混乱程序写起来比较复杂所以在非连续传输这里TXE等于1了我们不着急把下一个数据写进去而是一直等待等第一个字节时序结束在这个位置时序结束了意味着接收第一个字节也完成了这时接收的RXNE会置一我们等待RXNE置1后先把第一个接收到的数据读出来之后再写入下一个字节数据也就是这里的软件等待TXE等于1但是较晚写入0xf2SPI_DR 较晚写入TDR后数据二开始发送我们还是不着急写数据三等到了这里先把接收的数据二收着 再继续写入数据3数据3时序结束后最后再接收数据三置换回来的数据你看按照这个流程的话我们的整个步骤就是第一步等待TXE为一第二步写入发送的数据至TDR第三步等待RXNE为一 第四步读取RDR接收的数据之后交换第二个字节重复这四步那这样我们就可以把这四部分装到一个函数调用一次交换一个字节这样程序逻辑是不是就非常简单了和之前软件spi的流程基本上是一样的我们只需要稍作修改就可以把软件spi改成硬件spi那非连续算出缺点就是在这个位置没有及时把下一个数据写入TDR侯着所以等到第一个字节时序完成后第二个字节还没有送过来那这个数据传输就会在这里等着所以这里时钟和数据的时序在字节与字节之间会产生间隙拖慢了整体数据传输的速度这个间隙在sck频率低的时候影响不大但是在sck频率非常高时隙拖后腿的现象就比较严重了比如我这里用示波器看了一下不同sck频率间隙的影响。
这里有四个波形sck分频系数分别是264、128、28、56先看一下最慢的256分频这个SCK频率是72M/26大概280k图示上面是sk信号这里使用spi模式0所以默认低电平下面是SS信号低电平表示选中重击这个波形是spi非连续传输交换五个字节的时序主要看一下sck线这里连续交换了五个字节但是你几乎看不出字节与字节之间的间隙对因为这个时钟频率比较慢间隙时长也不大所以在这个比较慢的波形看来间隙对它的影响就可以忽略了。 下一个图是128分频sck频率大概560k这时就更明显的看出来字节之间的间隙了字节和字节之间并不是严丝合缝的这会降低整体的字节传输速度但是从这个比例上看啊这一点点间隙也可以忽略不计的。 继续加快时钟64分频SCK频率大概1M多点因为频率增大时间尺度缩小这样看来间隙就更加明显了进一步加快时钟频率我们直接看一下最快的二分频最后一张图这个sck时钟频率是72M/236M频率非常快了已经超过这个示波器的采样频率所以每个字节的时钟已经看不完整了 但是哪里在传输哪里是间隙还是可以区分的这里可以看到间隙所占的时间比例已经是数据传输的好几倍了这时再忽略间隙就不合适了如果你忽略了间隙那计算一下二分频的数据传输速率 应该是256分频的128倍当你实测一下它肯定达不到这么高因为这个二分频虽然干活效率高,但他每干一个时间单位就要休息好几个时间单位这怎么能达到它所生成的效率呢所以通过看这个波形我们就清楚了如果你想在极限频率下进一步提高数据传输速率追求最高性能那最好使用连续传输的操作逻辑或者还要进一步采用dma自动转运这些方法效率都是非常高的。 简单看一下软硬件波形对比这里上面是软件波形下面是硬件波形这些和I2C的软件件波形对比其实都是差不多的首先他们的数据变化趋势肯定是一样的采样得到的数据也是一样的区别就是硬件波形数据线的变化是紧贴sck边沿的而软件波形数据线的变化在边沿后有一些延迟实际上我们还可以发现哈I2C所描述的scl低电平期间数据变化高电平期间数据采样和spi所描述的sck下降沿数据移出上升沿数据移入最终波形的表现形式都是一样的无论是下降沿变化还是低电平期间变化它们都是一个意思下降沿和低电平期间都可以作为数据变化的时刻只是硬件波形一般会紧贴边缘软件波形一般只能在电平期间不过最终都不会影响数据传输不过软件波形如果能贴近边缘我们还是贴近边缘否则如果你等太久比较靠近下一个边沿那数据也容易出错。
手册 接线图和软件SPI一样 这个nss上小节说过我们一般可以继续使用软件模拟的方式来实现所以nss没必要必须接在PA4其他三个引脚的话就必须得是PA567。
如果SPI1的复用引脚被占用了可以重定义到这个位置。
常用库函数
void SPI_I2S_DeInit(SPI_TypeDef* SPIx);//恢复缺省配置
void SPI_Init(SPI_TypeDef* SPIx, SPI_InitTypeDef* SPI_InitStruct);//初始化
void I2S_Init(SPI_TypeDef* SPIx, I2S_InitTypeDef* I2S_InitStruct);
void SPI_StructInit(SPI_InitTypeDef* SPI_InitStruct);//结构体变量初始化
void I2S_StructInit(I2S_InitTypeDef* I2S_InitStruct);
void SPI_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);// 外设使能
void I2S_Cmd(SPI_TypeDef* SPIx, FunctionalState NewState);
void SPI_I2S_ITConfig(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT, FunctionalState
NewState);//中断使能
void SPI_I2S_DMACmd(SPI_TypeDef* SPIx, uint16_t SPI_I2S_DMAReq,
FunctionalState NewState);//DMA使能
void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data); //写DR数据寄存器
uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx); //读DR
//状态标志
FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);//获取TXE和RXNE标志位的状态
void SPI_I2S_ClearFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG);
ITStatus SPI_I2S_GetITStatus(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);
void SPI_I2S_ClearITPendingBit(SPI_TypeDef* SPIx, uint8_t SPI_I2S_IT);代码:
MySPI.h
#ifndef __MYSPI_H
#define __MYSPI_Hvoid MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);#endif
MySPI.c
#include stm32f10x.h // Device header/*** 函 数SPI写SS引脚电平SS仍由软件模拟* 参 数BitValue 协议层传入的当前需要写入SS的电平范围0~1* 返 回 值无* 注意事项此函数需要用户实现内容当BitValue为0时需要置SS为低电平当BitValue为1时需要置SS为高电平*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue设置SS引脚的电平
}/*** 函 数SPI初始化* 参 数无* 返 回 值无*/
void MySPI_Init(void)
{/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA4引脚初始化为推挽输出软件模拟SSGPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; //Input Pull UpGPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz;GPIO_Init(GPIOA, GPIO_InitStructure); //将PA6引脚初始化为上拉输入 MISO/*SPI初始化*/SPI_InitTypeDef SPI_InitStructure; //定义结构体变量SPI_InitStructure.SPI_Mode SPI_Mode_Master; //模式选择为SPI主模式SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; //方向选择双线全双工SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; //数据宽度选择为8位SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; //先行位选择高位先行SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_128; //波特率分频选择128分频SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; //SPI极性选择空闲默认低极性 SPI模式0SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; //SPI相位选择第一个时钟边沿采样(数据移入)极性和相位决定选择SPI模式0SPI_InitStructure.SPI_NSS SPI_NSS_Soft; //NSS选择由软件控制SPI_InitStructure.SPI_CRCPolynomial 7; //CRC多项式暂时用不到给默认值7SPI_Init(SPI1, SPI_InitStructure); //将结构体变量交给SPI_Init配置SPI1/*SPI使能*/SPI_Cmd(SPI1, ENABLE); //使能SPI1开始运行/*设置默认电平*/MySPI_W_SS(1); //SS默认高电平
}/*** 函 数SPI起始* 参 数无* 返 回 值无*/
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS开始时序
}/*** 函 数SPI终止* 参 数无* 返 回 值无*/
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS终止时序
}/*** 函 数SPI交换传输一个字节使用SPI模式0* 参 数ByteSend 要发送的一个字节* 返 回 值接收的一个字节*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) ! SET); //等待发送数据寄存器空//这里一直卡死的概率不大我们就不加超时机制了SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器开始产生时序while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) ! SET); //等待接收数据寄存器非空return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回
}W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_Hvoid W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);#endifW25Q64.c
#include stm32f10x.h // Device header
#include MySPI.h
#include W25Q64_Ins.h/*** 函 数W25Q64初始化* 参 数无* 返 回 值无*/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底层的SPI
}/*** 函 数MPU6050读取ID号* 参 数MID 工厂ID使用输出参数的形式返回* 参 数DID 设备ID使用输出参数的形式返回* 返 回 值无*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令*MID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID通过输出参数返回*DID MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位*DID 8; //高8位移到高位*DID | MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位通过输出参数返回MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64写使能* 参 数无* 返 回 值无*/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64等待忙* 参 数无* 返 回 值无*/
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令Timeout 100000; //给定超时计数时间while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) 0x01) 0x01) //循环等待忙标志位{Timeout --; //等待时计数值自减if (Timeout 0) //自减到0后等待超时{/*超时的错误处理代码可以添加到此处*/break; //跳出等待不等了}}MySPI_Stop(); //SPI终止
}/*** 函 数W25Q64页编程* 参 数Address 页编程的起始地址范围0x000000~0x7FFFFF* 参 数DataArray 用于写入数据的数组* 参 数Count 要写入数据的数量范围0~256* 返 回 值无* 注意事项写入的地址范围不能跨页*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i 0; i Count; i ) //循环Count次{MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据}MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数W25Q64扇区擦除4KB* 参 数Address 指定扇区的地址范围0x000000~0x7FFFFF* 返 回 值无*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable(); //写使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位MySPI_Stop(); //SPI终止W25Q64_WaitBusy(); //等待忙
}/*** 函 数W25Q64读取数据* 参 数Address 读取数据的起始地址范围0x000000~0x7FFFFF* 参 数DataArray 用于接收读取数据的数组通过输出参数返回* 参 数Count 要读取数据的数量范围0~0x800000* 返 回 值无*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令MySPI_SwapByte(Address 16); //交换发送地址23~16位MySPI_SwapByte(Address 8); //交换发送地址15~8位MySPI_SwapByte(Address); //交换发送地址7~0位for (i 0; i Count; i ) //循环Count次{DataArray[i] MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据}MySPI_Stop(); //SPI终止
}这里的硬件spi必须是发送同时接收要想接收必须得先发送因为只有你给TDR写东西才会触发时序的生成如果你不发送只调用接收函数那时序是不会动的然后还有一个注意事项TXE和RXNE是不是会自动清楚的问我在手册这个图上看到这里写的是TXE标志由硬件设置并由软件清除下面RXNE写的也是由硬件设置由软件清除这个有软件清除就比较迷惑是不是要求我们在标志位置1之后还需要我们手动调用ClearFlag函数清除呢实际上这个并不需要我们手动清除我们可以参考一下手册在状态标志这一节,这里写了发送缓冲器空闲标志TXE此标志为1时表明发送缓冲器为空可以写下一个待发送的数据进入缓冲器中当写入SPI_DR时TXE标志被清除所以在程序这里我们等待TXE标志置1之后不需要再手动调用一下ClearFlag函数,清除TXE标志位 因为写入DR时会顺便执行清除TXE的操作而我们下一句代码就正好是写入DR所以这个标志位不需要我们手动清除了然后RXNE标志位也是一样不确定就看下手册就行了。
main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include W25Q64.huint8_t MID; //定义用于存放MID号的变量
uint16_t DID; //定义用于存放DID号的变量uint8_t ArrayWrite[] {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组
uint8_t ArrayRead[4]; //定义要读取数据的测试数组int main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化/*显示静态字符串*/OLED_ShowString(1, 1, MID: DID:);OLED_ShowString(2, 1, W:);OLED_ShowString(3, 1, R:);/*显示ID号*/W25Q64_ReadID(MID, DID); //获取W25Q64的ID号OLED_ShowHexNum(1, 5, MID, 2); //显示MIDOLED_ShowHexNum(1, 12, DID, 4); //显示DID/*W25Q64功能函数测试*/W25Q64_SectorErase(0x000000); //扇区擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中W25Q64_ReadData(0x000000, ArrayRead, 4); //读取刚写入的测试数据到读取数据的测试数组中/*显示数据*/OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //显示写入数据的测试数组OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //显示读取数据的测试数组OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}BKP备份寄存器、PER电源控制器、RTC实时时钟
实时时钟这个东西本质上是一个定时器但这个定时器是专门用来产生年月日时分秒这种日期和时间信息的所以学会了stm32的RTC你就可以在stm32内部拥有一个独立运行的钟表想要记录或读取日期和时间就可以通过操作RTC来实现那rtc这个外设呢比较特殊它和备份寄存器BKP、电源控制器PWR这两章的关联性比较强在rtc这一章bkp和pwr也会经常来串门所以我们这节就把bkp和rtc放在一起讲这样整体思路会比较清晰pwr电源控制我们下一节再讲。
实验现象 读写备份寄存器 这里我们要在stlink上再引出一根3.3v的电源接到VBAT引脚这根线就模拟一个电池的电源一般情况下VBAT是电池供电口需要接备用电池但是我们目前套件里没有电池所以就直接引出一根3.3v电源线了也是一样的效果那看一下显示屏这个程序的目的是在bkp备份进器写入两个数据然后再把它们读出来显示一下。 bkp备份寄存器和上一节学的flash存储器类似都是用来存储数据的只是flash的数据是真正的掉电不丢失而bkb的数据是需要VBAT引脚接上备用电池来维持的本质是RAM只要VBAT有电池供电即使stm32主电源断电bkp的值也可以维持原状如果VBAT断电了那备份寄存器的数据就清零了。
其实备份寄存器和VBAT引脚的存在更多的是为了服务RTC的所以我们接着看第二个代码实时时钟下载看一下。
11.0 Unix时间戳 32位有符号数所能表示的最大数字是2^32/2-1这个数是21亿多这其实是有溢出风险的因为目前到2023年时间戳已经计到16亿了32位有符号数的时间戳会在2038年的1月19号溢出64位的时间戳能存储的时间范围非常非常的大看下手册STM32它核心的计时部分是一个32位的可编程计数器这说明我们这款stm32 它的时间戳是32位的数据类型这表示我们这个s t m32 也会在2038年出现bug吗实际上并不会啊因为根据我的研究这个时间戳在stm32程序中定义的其实是无符号的要到2106年才会溢出。 为什么说GMT是以前的时间标准呢这是因为GMT有一个棘手的问题就是地球自转一周的时间其实是不固定的由于潮汐力、地球活动等原因地球目前是越转越慢的。
原子钟是当前计时最精确的装置上千万年才误差一秒那现在问题又来了我们以一个恒定不变的秒来计时但是地球自转越来越慢这样记下去计时的一天和自转的一天就会出现偏差时间长一些可能中午12点太阳就不是最高的位置所以在原子钟计时系统的基础上我们得加入闰秒的机制。 time.h
#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 1695695610Date *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;
}BKP简介 TAMPER引脚产生的侵入事件将所有备份寄存器内容清除TAMPER是一个接到stm32外部的引脚它的位置可以看一下引脚定义这个TAMPER是一个安全保障设计比如如果你做一个安全系数非常高的设备设备需要有防拆功能然后bkp里也存储了一些敏感数据这些数据不能被别人窃取或者篡改那你就可以使用这个TAMPER引脚的侵入检测功能设计电路时TAMPER引脚可以先加一个默认的上拉或者下拉电阻然后引一根线到你的设备外壳的防拆开关或触点别人一拆开你的设备触发开关就会在TAMPER引脚产生上升沿或者下降沿这样STM32就检测到侵入事件了这时BKP的数据会自动清零并且申请中断你在中断里还可以继续保护设备比如清除其他存储器数据然后设备锁死这样来保障设备的安全另外主电源断电后侵入检测仍然有效这样即使设备关机也能防拆。
RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲RTC引脚刚才看过了也是在PC13这个位置 这就是RTC时钟输出的功能RTC的校准时钟闹钟或者秒脉冲的信号可以通过RTC引脚输出其中外部用设备测量RTC校准时钟可以对内部RTC微小的误差进行校准然后闹钟脉冲或者秒脉冲可以输出出来为别的设备提供这些信号这是RTC时钟输出的功能因为PC13、temple和RTC这三个引脚共用一个端口所以这三个功能同一时间只能使用一个。
接下来存储RTC时钟校准寄存器这个可以配合上面这个校准时钟输出的功能结合一些测量方法可以对RTC进行校准那这两个功能实际上就是RTC的配置我觉得放在RTC那个外设的地方应该比较合适当然RTC和BKP关联程度比较高设计者目前就是把这两个RTC的功能放在BKP里了。
最后看一下BKP中用户数据的存储容量在中容量和小容量设备里BKP是20个字节在大容量和互联型设备里BKP是84个字节我们使用的c8t6是中容量设备所以可以看出BKP的容量其实非常小一般只能用来存储少量的参数。
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的误差进行校准。 20位的可编程预分频器可适配不同频率的输入时钟保证分频器输出给计数器的频率为1Hz这样计时才正确。 记住H开头是高速L开头是低速E结尾是外部I结尾是内部这里高速时钟一般供内部程序运行和主要外设使用低速时钟一般供RTC看门狗这些东西使用那对于我们本节的RTC呢我们可以看到这一块这里指向的箭头通往RTC就是RTCCLKRTCCLK有三个来源第一个是OSC引脚接的HSE外部高速晶振这个晶振是主晶振我们一般都用的8MHZ通过128分频可以产生RTCCLK信号为什么要先128分频这是因为这个8MHz的晶振太快了如果不提前分频直接给RTCCLK后续即使再通过RTC的20位分频器也分不到1Hz这么低的频率。 然后中间这一路时钟来源是LSE外部低速晶振我们在oc32这两个引脚接上外部低速晶振这个晶振产生的时钟可以直接提供给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的这是第二路最后看第三路时钟源来自于LSI内部低速rc振荡器LSI固定是40KHz如果选择LSI当做RTCCLK后续再经过40k的分频就能得到1Hz的计数时钟了当然内部的RC振荡器一般精度没有外部晶振高所以LSI给RTCCLKl可以当做一个备选方案另外LSI还可以提供给看门狗我们最常用的就是中间这一路外部32.768KHz的晶振提供RTCCLK的时钟第一个原因就是中间这一路 32.768KHz的晶振本身就是专供rtc使用的上下这两路其实是有各自的任务上面这一路主要作为系统主时钟下面这一路主要作为看门狗时钟他们只是顺带可以备选当做rdc的时钟另外一个更重要的原因只有中间这一路的时钟可以通过VBAT备用电池供电上下两路时钟在主电源断电后是停止运行的所以要想实现rtc主电源掉电继续走时的功能必须得选择中间这一路的rtc专用时钟如果选择的是上下两路时钟主电源断电后时钟就暂停了这显然会导致走时出错。 接下来我们来看一下这个rtc的框图先整体上划分一下左边这一块是核心的分频和计数计时部分右边这一块是中断输出使能和NVIC部分上面这一块是APB1总线读写部分下面这块是PWR关联的部分意思就是RTC的闹钟可以唤醒设备退出待机模式然后在图中我们看到有灰色填充的部分都处于后备区域这些电路在主电源掉电后可以使用备用电池维持工作另外这里还写了这些模块在待机时都会继续维持供电其他未被填充的部分就是待机时不供电有关睡眠停机待机在低功耗相关的内容我们下节学pwr的时候再来细讲。
然后我们依次详细看一下首先看分频和计数器计数部分这一块的输入时钟是RTCCLK这刚才说过RTCCLK的来源需要在RCC里进行配置可以选择的选项是这三个我们主要选择中间一路那因为这三路时钟频率各不相同而且都远大于我们所需要的1Hz的秒计数频率所以RTCCLK进来首先需要经过RTC预分频器进行分频这个分频器由两个计算器组成上面这个是重装载寄存器RTC_PRL下面这个RTC_DIV手册里叫做余数寄存器但实际上这一块跟我们之前定时器时机单元里的计数器CNT和重装值ARR是一样的可能是右边已经有一个计数器cnt了所以这个名字就比较奇怪叫做余数寄存器但实际上它还是计数器的作用分频器其实就是一个计数器记几个数溢出一次就几分频所以对于可编程的分频器来说需要有两个寄存器一个寄存器用来不断的计数另一个寄存器我们写入一个计数目标值用来配置是几分频那在这里上面这个PRL就是计数目标我们写入六那就是七分频写九那就是十分频因为计数指包含了零然后下面这个DIV就是每来一个时钟计一个数的用途了当然这个DIV计数器啊是一个自减计数器每来一个输入时钟DIV的值自减一次 自减到0时再来一个输入时钟DIV输出一个脉冲产生溢出信号同时DIV从PRL获取重装值回到重装值继续自减。
然后看一下计数计时部分这一块就比较简单了32位可编程计数器RTC_CNT就是计时最核心的部分我们可以把这个计数器看作是Unix时间戳的秒计数器这样借助time.h的函数就可以很方便地得到年月日时分秒了然后在下面这里这个RTC还设计的有一个闹钟寄存器RTC_ALR这个ALR也是一个32位的寄存器和上面这个cnt是等宽的它的作用顾名思义就是设置闹钟我们可以在ALR写一个秒数设定闹钟当cnt的值跟ALR设定的闹钟值一样时也是这里画了等号啊如果他俩值相等就代表闹钟响了这时就会产生RTC_Alarm闹钟信号通往右边的中断系统在中断函数里你可以执行相应的操作同时这个闹钟还兼具一个功能就下面这里的闹钟信号可以让STM32退出待机模式 这个就可以对应一些用途比如你设计一个数据采集设备需要在环境非常恶劣的地方工作比如海底高原深井这些地方然后要求是每天中午12点采集一次环境数据其他时间为了节省电量避免频繁换电池芯片都必须处于待机模式这样的话我们就可以用这个rtc自带的闹钟功能定一个中午12点的闹钟闹钟一响芯片唤醒采集数据完成后继续待机另外这个闹钟值是一个定值只能响一次所以如果你想实现周期性的闹钟大家每次闹钟响之后都需要再重新设置一下下一个闹钟时间就是这个闹钟和闹钟唤醒的一个用途。
那继续往右看这是中段部分的在左边这里有三个信号可以触发中断第一个是RTC_Second秒中断它的来源就是cnt的输入时钟如果开启这个中断那么程序就会每秒进一次rtc中断第二个是RTC_Overflow溢出中断它的来源是cnt的右边意思就是cnt的32位计数器计满溢出来了会触发一次中断所以这个中段一般不会触发我们上一节说过这个cnt定义的是无符号数到2106年才会溢出所以这个中段在2106年会触发一次如果你想程序更完善一些可以开启这个中段到2106年就是一溢出为了避免不必要的错误你可以让芯片罢工然后提示当前设备过老请及时更换但在2106年之后这个stm32的rtc就不太好用了到时候或许可以通过打补丁的方式继续运行或者直接淘汰32位的时间戳这个问题就留给后人解决吧。
来继续看下面第三个RTC_Alarm闹钟中断刚才说过当计数器和闹钟值相等时触发中断同时闹钟信号可以把设备从待机模式唤醒这是这三个中断信号中断信号到右边这里这一块就是中断标志位和中段输出控制这些f结尾的是对应的中断标志位ie结尾Interrupt ENABLE的是中断使能最后三个信号通过一个或门汇聚到NVIC中断控制器这个地方是不是漏画了一根线中间这个应该也是要通过或门的好这是右边的中断部分然后上面这部分APB1总线和APB1接口就是我们程序读写寄存器的地方读写计算器可以通过APB1总线来完成另外也可以看出RTC是APB1总线上的设备最后下面这一块退出待机模式还有一个wake up引脚闹钟信号和wake up引脚都可以唤醒设备 wake up引脚可以看一下接线图就这里PA0的位置它兼具唤醒的功能这个我们下一节再学习。
RTC基本结构
接下来看一下我这里给的基本结构再总结一下rtc的核心部分如图所示最左边是RTCCLK时钟来源这块需要在RCC里配置三个时钟选择一个当做RTCCLK之后RTCCLK先通过预分频器对时钟进行分频余数寄存器是一个自减计数器存储当前的计数值重装寄存器是计数目标决定分频值 分频之后得到1Hz的秒计数信号通向32位计数器一秒自增一次下面还有个32位的闹钟值可以设定闹钟如果不需要闹钟的话下面这一块可以不用管然后右边有三个信号可以触发中断分别是秒信号、计数器溢出信号和闹钟信号三个信号先通过中断输出控制进行中断使能使能的中断才能通向NVIC然后向cpu申请中断在程序中我们配置这个数据选择器可以选择时钟来源配置重装寄存器可以选择分频系数配置32位计数器可以进行日期时间的读写需要闹钟的话配置32位闹钟值即可需要中断的话先允许中断再配置nvic最后写对应的中断函数即可这是RTC外设的主要内容。 为了配合stm32 rtc外部还是需要有一些电路的在最小系统电路上外部电路还要额外加两部分 第一部分就是备用电池第二部分就是外部低速晶振首先备用电池供电部分我这里给了两个参考电路第一个是简单连接就使用一个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.768k这个也是一种样式的RTC晶振然后旁边这个金属壳柱状体是8MHz的外部高速晶振不过我们这个板子没有自带备用电池vbat引脚直接通过右上角的这个端口引出来了如果需要备用电池的话可以接在这里以上就是RTC的硬件电路部分。 最后我们再看一些RTC的一些操作注意事项这些注意事项都是从手册里复制过来的文字写程序的时候需要注意这些问题。
设置RCC_APB1ENR的PWREN和BKPEN使能PWR和BKP时钟设置PWR_CR的DBP使能对BKP和RTC的访问。 这几条就提醒一下正常的外设第一步开启时钟就能用了但是BKP和RTC这两个外设开启稍微复杂一些如果你要使用BKP或者RTC都要先执行这两步第一步开启PWR和BKP的时钟第二步使用PWR使能BKP和RTC的访问这个我们在初始化的时候需要注意一下按照这个流程来就行了。
若在读取RTC寄存器时RTC的APB1接口曾经处于禁止状态则软件首先必须等待RTC_CRL寄存器中的RSF位寄存器同步标志被硬件置1 这一步对应代码里的一个库函数就是RTC等待同步一般在刚上电的时候调用一下这个函数就行了为什么要有这一步呢可以看看框图在这里会有两个时钟PCLK1和PCLK2PCLK1在主电源掉电时会停止所以为了保证RTC主电源掉电正常工作RTC里的寄存器都是在RTCCLK的同步下变更的当我们用PCLK驱动的总线去读取RTCCLK驱动的寄存器时就会有个时钟不同步的问题RTC寄存器只有在RTCCLK的上升沿更新但是PCLK1的频率36MHz远大于RTCCLK的频率32KHz如果我们在APB1刚开启时就立刻读取RTC寄存器有可能RTC寄存器还没有更新到APB1总线上这样我们读到的值就是错误的通常来说就读取到0所以这就要求我们在APB1总线刚开机时要等一下RTCCLK只要RTCCLK来一个上升沿rtc把它的寄存器的值同步到APB1总线上这样之后读取的值就都是没问题的了这是设计细节的一个问题当然我们其实也不用管那么多了只需要在初始化时调用一个等待同步的函数就行了。
必须设置RTC_CRL寄存器中的CNF位使RTC进入配置模式后才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器 这一条其实比较简单就是RTC会有一个进入配置模式的标志位把这一位置1才能设置时间其实这个操作在库函数中每个写寄存器的函数它都自动帮我们加上了这个操作所以我们就不用再单独调用函数进入配置模式了。
对RTC任何寄存器的写操作都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时才可以写入RTC寄存器 这个操作也是调用一个等待的函数就行了跟我们之前读写flash芯片是类似的就写入之前要等待一下如果上一次的写入还没完成你就别急着写下一次了或者说每次写入之后你要等待RTOFF为1只有RTOFF为1才表示写完成为什么要有这个操作呢其实还是因为这里的PCLK1和RTCCLKk时钟频率不一样你用PCLK1的频率写入之后这个值还不能立刻更新到RTC的寄存器里因为RTC寄存器是由RTCCLK驱动的所以PCLK1写完之后得等一下RTCCLK的时钟RTCCLK来个上升沿 值更新到RTC寄存器里整个写作过程才算结束了这个操作了解一下在代码里也就是调用一个等待函数的事。
手册 代码示例读写备份寄存器BKP
读写备份寄存器接线图 常用库函数
void BKP_DeInit(void); // 恢复缺省配置将所有配置清0 纽扣电池一直有电后不会自动清0的
// 用于Tamper侵入检测功能
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel); // 侵入检测功能 配置有效电平高电平、低电平触发
void BKP_TamperPinCmd(FunctionalState NewState); // 是否开启侵入检测功能
void BKP_ITConfig(FunctionalState NewState); // 配置中断
void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource); // 时钟输出功能的配置 可以选择在RTC引脚上输出时钟信号 输出RTC校准时钟RTC闹钟脉冲或者秒脉冲
void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue); // 设置RTC校准值(写入RTC校准寄存器)// 读写BKP
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState); //备份寄存器访问使能 前面讲的设置PWR_CR的DBP使能对BKP和RTC的访问
void PWR_PVDCmd(FunctionalState NewState);
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);
void PWR_WakeUpPinCmd(FunctionalState NewState);
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);
void PWR_EnterSTANDBYMode(void);
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);代码 main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.h
uint16_t Write[] {0x1234, 0x5678};
uint16_t Read[2];
uint8_t KeyNum;
int main(void)
{OLED_Init();KEY_Init();OLED_ShowString(1, 1, W:);OLED_ShowString(2, 1, R:);// BKP初始化RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);// 备份寄存器访问使能PWR_BackupAccessCmd(ENABLE);while (1){KeyNum Key_GetNum();if (KeyNum 1){BKP_WriteBackupRegister(BKP_DR1, Write[0]); // DR 1-10 c8t6BKP_WriteBackupRegister(BKP_DR2, Write[1]);Write[0];Write[1];OLED_ShowHexNum(1, 3, Write[0], 4);OLED_ShowHexNum(2, 3, Write[0], 4);}Read[0] BKP_ReadBackupRegister(BKP_DR1);Read[1] BKP_ReadBackupRegister(BKP_DR2);OLED_ShowHexNum(1, 3, Read[0], 4);OLED_ShowHexNum(2, 3, Read[1], 4);}
}11.2 RTC实时时钟
实时时钟接线图
RTC这个程序我们暂时没用到按键然后RTC的外部低速晶振上小节说过晶振电路板上自带的就有所以RTC晶振部分我们接线就不用管了。
常用函数
void RCC_LSEConfig(uint8_t RCC_LSE); // 启动LSE时钟
void RCC_LSICmd(FunctionalState NewState); // 配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource); // CLK时钟源数据选择器
void RCC_RTCCLKCmd(FunctionalState NewState); // 启动RCCCLK
void RCC_GetClocksFreq(RCC_ClocksTypeDef *RCC_Clocks);
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalStateNewState);
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalStateNewState);
void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalStateNewState);
FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG); // 获取标志位LSE时钟不是调用完就立马启动的
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState); // RTC中断
void RTC_EnterConfigMode(void); // 进入配置模式 前面讲的必须设置RTC_CRL寄存器中的CNF位使RTC进入配置模式后才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
RTC_CRL_CNF 1 void RTC_ExitConfigMode(void); // 退出配置模式
uint32_t RTC_GetCounter(void); // 获取CNT计数器的值
void RTC_SetCounter(uint32_t CounterValue); // 写入CNT计数器的值设置时间
void RTC_SetPrescaler(uint32_t PrescalerValue); // PSC 预分频器分频系数
void RTC_SetAlarm(uint32_t AlarmValue); // 闹钟值
uint32_t RTC_GetDivider(void); // 读取余数寄存器 为了得到更细致的时间因为CNT计数间隔最短就是1S,分秒、毫秒要用到
void RTC_WaitForLastTask(void); // 等待上一次操作完成(前面讲的等待RTOFF状态为1)
void RTC_WaitForSynchro(void); // 等待同步(前面讲的等待RSF标志位置1)
FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);
void RTC_ClearFlag(uint16_t RTC_FLAG);
ITStatus RTC_GetITStatus(uint16_t RTC_IT);
void RTC_ClearITPendingBit(uint16_t RTC_IT);代码 main.c
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include MyRTC.hint main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化MyRTC_Init(); //RTC初始化/*显示静态字符串*/OLED_ShowString(1, 1, Date:XXXX-XX-XX);OLED_ShowString(2, 1, Time:XX:XX:XX);OLED_ShowString(3, 1, CNT :);OLED_ShowString(4, 1, DIV :);while (1){MyRTC_ReadTime(); //RTC读取时间最新的时间存储到MyRTC_Time数组中OLED_ShowNum(1, 6, MyRTC_Time[0], 4); //显示MyRTC_Time数组中的时间值年OLED_ShowNum(1, 11, MyRTC_Time[1], 2); //月OLED_ShowNum(1, 14, MyRTC_Time[2], 2); //日OLED_ShowNum(2, 6, MyRTC_Time[3], 2); //时OLED_ShowNum(2, 9, MyRTC_Time[4], 2); //分OLED_ShowNum(2, 12, MyRTC_Time[5], 2); //秒OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //显示32位的秒计数器CNT值OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器}
}MyRTC.h
#ifndef __MYRTC_H
#define __MYRTC_Hextern uint16_t MyRTC_Time[];void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);#endifMyRTC.c
#include stm32f10x.h // Device header
#include time.huint16_t MyRTC_Time[] {2023, 1, 1, 23, 59, 55}; //定义全局的时间数组数组内容分别为年、月、日、时、分、秒 刷新到RTC外设里
//注意数组里这些数据前面不要补0C语言会默认为八进制 if(123 0123)这就不成立void MyRTC_SetTime(void); //函数声明/*** 函 数RTC初始化* 参 数无* 返 回 值无*/
void MyRTC_Init(void)
{/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟/*备份寄存器访问使能*/PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) //通过写入备份寄存器的标志位判断RTC是否是第一次配置 不然每次复位时间都会重置//if成立则执行第一次的RTC配置{RCC_LSEConfig(RCC_LSE_ON); //开启LSE时钟(默认是关闭的省电) 这里有个可选参数LSE_Bypass表示LSE时钟旁路时钟旁路的意思就是不要接晶振直接从OSE32_IN这个引脚输入一个指定频率的信号这样也可以当做时钟源比较方便不过用的不多while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) ! SET); //等待LSE振荡器时钟启动完成//我实测有些板子是有问题就是RTC晶振启动不了不起振,这个程序就会卡死在这个位置//一直等待LSERDY标志位,解决方案时钟源改为备选内部晶振LSIRCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK时钟来源为LSERCC_RTCCLKCmd(ENABLE); //RTCCLK使能//因为这个RTC比较简单所以库函数并没有使用结构体来配置开启时钟就能自动运行了RTC_WaitForSynchro(); //等待同步 防止时钟不同步造成bugRTC_WaitForLastTask(); //等待上一次操作完成RTC_SetPrescaler(32768 - 1); //设置RTC预分频器预分频后的计数频率为1Hz,LSE的频率是32.768KHzRTC_WaitForLastTask(); //等待上一次操作完成//RTC_SetCounter(1672588795);//RTC_WaitForLastTask();MyRTC_SetTime(); //设置时间调用此函数全局数组里时间值刷新到RTC硬件电路BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); //在备份寄存器写入自己规定的标志位用于判断RTC是不是第一次执行配置}else //RTC不是第一次配置{RTC_WaitForSynchro(); //等待同步RTC_WaitForLastTask(); //等待上一次操作完成}
}//如果LSE无法起振导致程序卡死在初始化函数中
//可将初始化函数替换为下述代码使用LSI当作RTCCLK
//LSI无法由备用电源供电故主电源掉电时RTC走时会暂停
/*
void MyRTC_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);PWR_BackupAccessCmd(ENABLE);if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5){RCC_LSICmd(ENABLE);while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) ! SET);RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();RTC_SetPrescaler(40000 - 1);RTC_WaitForLastTask();MyRTC_SetTime();BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);}else{RCC_LSICmd(ENABLE); //即使不是第一次配置也需要再次开启LSI时钟while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) ! SET);RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);RCC_RTCCLKCmd(ENABLE);RTC_WaitForSynchro();RTC_WaitForLastTask();}
}*//*** 函 数RTC设置时间* 参 数无* 返 回 值无* 说 明调用此函数后全局数组里时间值将刷新到RTC硬件电路*/
void MyRTC_SetTime(void)
{time_t time_cnt; //定义秒计数器数据类型struct tm time_date; //定义日期时间数据类型time_date.tm_year MyRTC_Time[0] - 1900; //将数组的时间赋值给日期时间结构体time_date.tm_mon MyRTC_Time[1] - 1;time_date.tm_mday MyRTC_Time[2];time_date.tm_hour MyRTC_Time[3];time_date.tm_min MyRTC_Time[4];time_date.tm_sec MyRTC_Time[5];time_cnt mktime(time_date) - 8 * 60 * 60; //调用mktime函数将日期时间转换为秒计数器格式//- 8 * 60 * 60为东八区的时区调整RTC_SetCounter(time_cnt); //将秒计数器写入到RTC的CNT中RTC_WaitForLastTask(); //等待上一次操作完成
}/*** 函 数RTC读取时间* 参 数无* 返 回 值无* 说 明调用此函数后RTC硬件电路里时间值将刷新到全局数组*/
void MyRTC_ReadTime(void)
{time_t time_cnt; //定义秒计数器数据类型struct tm time_date; //定义日期时间数据类型time_cnt RTC_GetCounter() 8 * 60 * 60; //读取RTC的CNT获取当前的秒计数器// 8 * 60 * 60为东八区的时区调整time_date *localtime(time_cnt); //使用localtime函数将秒计数器转换为日期时间格式MyRTC_Time[0] time_date.tm_year 1900; //将日期时间结构体赋值给数组的时间MyRTC_Time[1] time_date.tm_mon 1;MyRTC_Time[2] time_date.tm_mday;MyRTC_Time[3] time_date.tm_hour;MyRTC_Time[4] time_date.tm_min;MyRTC_Time[5] time_date.tm_sec;
}最后一个DIV正在快速的自减自减的范围是32767~0DIV每自减一轮CNT秒数加1有了这个数我们就可以对秒数进行更细的划分获取分秒厘秒毫秒这些参数了。
十二、PWR电源控制
实验现象 1、修改主频 修改主频不属于三种低功耗模式但是也是降低STM32功耗的一种方法。 2、睡眠模式串口发送接收
睡眠模式加串口的发送和接收这个程序就是从串口那一节直接复制过来的这个代码的功能就是当收到一个字节时中断触发置标志位主循环查询到标志位时读取数据并用串口发送数据在这个功能后面又新加了一段代码这个就是用来配置睡眠模式的代码执行芯片就进入睡眠睡眠的目的是如果STM32一直没收到数据那这个主循环也会一直查询标志位那不如就让它睡眠收到数据后自动退出睡眠模式执行一遍任务后继续睡眠这样在空闲时芯片一直在睡眠可以降低系统功耗。 另外还要重点提醒一下芯片在三种低功耗模式下是没法直接下载程序的如果直接点下载就会提示报错不会理你调试端口了解决方法也很简单需要我们有一些操作第一步我们按住复位键不放第二步点下载按钮第三步及时松开复位键这样就能下载成功了在我们本节三种低功耗模式下都需要这样下载程序大家注意一下另外如果你不小心禁用了调试端口其实也可以这样来解决。
只有在我们发送数据时刻OLED才会显示一次running在空闲时芯片一直都在睡眠这样就是在不影响程序功能的前提下使用睡眠模式节约电量。
3、停止模式对射式红外传感器计次
每次遮挡一次执行一次记次也显示一下running在没有外部中断信号时STM32处于停止模式可以省电。
4、待机模式实时时钟
这个程序是在实时时钟的基础上加入了待机模式目前这个程序我使用的是LSE外部低速时钟如果你没有RTC晶振或者RTC晶振不起振也可以使用LSI内部低速时钟LSI在待机模式下可以继续工作然后在这个位置可以加入唤醒后要执行的功能在进入待机模式之前可以关闭各个外部连接的模块以最大化省电我目前是用Oled_Clear模拟了一下那这个程序会用实时时钟设定闹钟每隔一段时间会自动唤醒一次这里我演示的是每隔十秒唤醒一次唤醒之后执行一遍程序任务然后继续待机。
可以看到OLED上显示了当前时钟和闹钟随后进入待机然后等一会儿闹钟触发之后自动唤醒一次设定新的那种执行程序功能之后继续待机等待下一次唤醒这是使用RTC和闹钟配合待机模式的自动唤醒程序非常适合那种需要每隔一段时间操作一次空闲时间又需要最大化省电的设备。 12.1 PWR简介 可编程电压监测器PVD可以监控VDD电源电压当VDD下降到PVD阀值以下或上升到PVD阀值之上时PVD会触发中断用于执行紧急关闭任务。 这个功能预想的场景应该是使用电池供电或者对安全要求比较高的设备如果供电电压在逐渐下降 在电压过低的情况下可能会导致内部或外部电路发生不确定的错误为了避免不确定的因素在电源电压低于设定的阈值时我们可以主动出击提前发出警告并且关闭比较危险的设备这是这个PVD的设计不过PVD这个功能不是我们本节课的重点哈我们暂时也不演示代码。
在低功耗模式下我们也需要保留必要的唤醒电路比如串口接收数据的中断唤醒外部中断唤醒 RTC闹钟唤醒等在需要设备工作时STM32能够立刻重新投入工作如果你只考虑进入低功耗而不考虑唤醒STM32那不就跟直接断电没区别了吗所以低功耗模式我们要考虑关闭哪些硬件保留哪些硬件以及如何去唤醒当然关闭越多的硬件设备越省电唤醒就越麻烦。
电源框图 这个图就是STM32内部的供电方案整体上看这个图可以分为三个部分最上面是模拟部分供电叫做VDDA中间是数字部分供电包括两块区域VDD供电区域和1.8v供电区域下面是后备供电叫做VBAT。
依次看一下VADDA供电区域主要负责模拟部分的供电其中包括AD转换器、温度传感器、复位模块、PLL锁相环这些电路的供电正极是VDDA负极是VSSA其中AD转换器还有两个参考电压的供电脚叫做VREF和VREF-这两个脚在引脚多的型号里会单独引出来在引脚少的型号比如我们这个C8T6VREF和VREF-在内部就已经分别接到了VDDA和VSSA了。
然后看中间部分的供电这一块由两部分组成左边部分是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区域和电压调节器最下面就是我们上一节提到的VBAT后备供电区域了其中包括LSE 32K晶体振荡器、后备寄存器RCC BDCR计寄存器和RTCRCC BDCR是RTC的寄存器啊叫做备份域控制寄存器也是和后备区域有关的寄存器所以也可以有VBAT供电然后这里有个低电压检测器可以控制这个开关VDD有电时由VDD供电VDD没电时由VBAT供电。
上电复位和掉电复位 上电复位和掉电复位还有可编程电压监测器这两个内容了解即可首先是上电复位和掉电复位这个意思是当VDD或者VDDA电压过低时内部电路直接产生复位让STM32复位住不要乱操作这个复位和不复位的界限之间设置了一个40毫伏的迟滞电压大于上限POR(Power On Reset)时解除复位小于下限PDR(Power Down Reset)时复位这是一个典型的迟滞比较器设置两个阈值的作用 就是防止电压在某个阈值附近波动时造成输出也来回抖动下面的复位信号reset是低电平有效的 所以在前面和后面电压过低时是复位的中间电压正常的时候不复位那这个电压上限和下限具体是多少伏呢还有这里解除复位还有个滞后时间是多久呢这些参数可以看一下STM32数据手册 在5.3.3内嵌复位和电源控制模块特性里有这个表这里写了上电或掉电复位阈值下降沿也就是PDR掉电复位的阈值下限典型值是1.88V,上升沿也就是POR上电复位的阈值上限典型值是1.92V1.92-1.88就是迟滞的阈值40毫伏所以如果忽略迟滞的话简单来说就是大于1.9V上电低于1.9V掉电然后最后一行就是TRSTTEMPO复位持续时间典型值是2.5ms就是这个上电复位和掉电复位知道一下就行了也不需要我们操作啥的。
可编程电压监测器
然后下面这个是可编程电压监测器简称PVD他的工作流程和上面这个差不多哈都是监测VDD和VDDA的供电电压但是PVD的区别就是首先它这个阈值电压是可以使用程序指定的可以自定义调节调节的范围可以看一下数据手册在这个表的上面就是PVD的阈值配置PLS寄存器的3个位 可以选择右边这么多的阈值因为这里也同样是迟滞比较所以有两个阈值可选范围是2.2V到2.9V左右PVD上限和下限之间的迟滞电压是100毫伏可以看到PVD的电压是比上电掉电复位的电压要高的画个图就是3.3伏是正常的供电当这个电压降低在2.9伏到2.2伏之间属于PVD监测的范围 可以通过PVD设置一个警告线之后再降低到1.9伏就是复位电路的检测范围,低于1.9伏直接复位住不让动就是这两个电压监测的工作任务那当然PVD触发之后芯片还是能正常工作的只不过是电源电压过低该提醒一下用户了所以看一下下面这个PVD输出这个是正逻辑哈电压过低时为1电压正常值为0这个信号可申请中断在上升沿或者下降沿时触发中断一是提醒程序进行适当的处理另外这个PVD的中断申请是通过外部中断实现的我们可以看一下外部中断这一节这个图(EXTI基本结构图)可以看到PVD输出的信号是跑到这里来了所以如果要使用PVD的话记得要配置外部中断然后下面这里还有RTC(EXTI基本结构图)这个是RTC的闹钟信号也有接到外部中断其实RTC自己是有中断的那为啥还要借到外部中断这个等会就知道了因为低功耗模式设计的是只有外部中断可以唤醒停止模式其他这些设备也想唤醒停止模式的话都可以通过借道外部中断来实现其实后面这两个USB和ETH也都只有他们的wake up唤醒信号接过来了目的也是为了唤醒停止模式这个了解一下。 低功耗模式
接着我们来看看本节课的重点 低功耗模式这三种模式从上到下关闭的电路越来越多对应的从上到下是越来越省电同时从上到下也是越来越难唤醒的首先看一下睡眠模式这是浅睡眠如何进入呢这里写了直接调用WFI或者WFE即可进入这两个东西是内核的指令对应库函数里也有对应的函数直接调用函数即可其中WFI的意思是wait for interrupt等待中断意思就是我先睡了如果有中断发生的话再叫我起来所以对应的唤醒条件是任意中断调用WIFI进入的睡眠模式任何外设发生任何中断时芯片都会立刻醒来因为中断发生了醒来之后的第一件事一般就是处理中断函数然后下面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用来设置最后这个电压调节器是开启还是进入低功耗模式RPDS等于0电压调节器开启RPDS等于1电压调节器进入低功耗最后当我们把这些位提前设置好了最后再调用WFA或者WFE芯片就可以进入停止模式了然后停止模式的唤醒因为这个模式下芯片睡得更深关的东西更多所以唤醒条件就苛刻一些是任一外部中断刚才睡眠模式是任一中断所有外设的中断都行现在停止模式要求就是只有外部中断才能唤醒其他中断唤醒不了刚才我们还提到了PVD、RTC闹钟、USB唤醒、ETH唤醒借道了外部中断所以这四个信号也可以唤醒停止模式因为这里并没有区分WFI和WFE其实也可以想象得到WFI要用外部中断的中断模式唤醒WFE要用外部中断的事件模式唤醒这是对应的最后看停止模式对电路有哪些操作呢首先关闭所有1.8伏区域的时钟这意思就是不仅CPU不能运行外设也运行不了定时器在定时的会暂停串口收发数据也会暂停不过由于没关闭电源所以CPU和外设的寄存器数据都是维持原状的之后下一个HSI和HSE的振荡器关闭既然CPU和外设时钟都关了那这两个高速时钟显然也没用了,所以HSI内部高速时钟和HSE外部高速时钟会关闭当然他没提到的LSI内部低速时钟和LSE外部低速时钟这两个并不会主动关闭如果开启过这两个时钟还可以继续运行最后电压调节器这里可以选择是开启或者处于低功耗模式刚才说了这个电压调节器是由这个LPDS位控制的 这个开启和低功耗模式有啥区别呢其实区别也不大电压调节器无论是开启还是低功耗都可以维持1.8伏区域寄存器和存储器的数据内容区别就是低功耗模式更省电一些同时低功耗模式在唤醒时要花更多的时间相反电压调压器开启的话就是更耗电一些唤醒更快了那这些就是停止模式的介绍主要操作就是把运行的高速时钟都关了CPU和外设都暂停工作但是电压调节器并没有关存储器和寄存器数据可以维持原样它的唤醒条件比较苛刻只能通过外部中断唤醒所以停止模式相当于整个人都罢工了脑子不工作身体也不工作只有有人用外部中断过来敲我,我才会醒来干活 在省电程度上为非常省电。
最后我们看第三种待机模式进入的话和停机模式差不多首先sleep deep也是置1即将深度睡眠然后PDDS置1表示即将进入待机模式最后调用WFI或者WFE就可以进入待机模式了然后看一下唤醒条件普通外设的中断和外部中断都无法唤醒待机模式待机模式只有这几个指定的信号才能唤醒第一个是wake up引脚的上升沿wake up引脚可以看一下引脚定义这里PA0-WKUP指示了引脚的位置就是PA0的位置第二个是RTC闹钟事件这个我们的示例代码和上一节RTC提到过RTC闹钟可以唤醒待机模式应用场景就是芯片每隔一段时间自动工作一次第三个是NRST引脚上的外部复位意思就是按一下复位键它也是能唤醒的最后一个IWDG独立看门狗复位这个了解一下就行了看门狗我们最后介绍可以看出待机模式只有这指定的四个信号能唤醒,其他信号都唤醒不了 唤醒条件最为苛刻之后待机模式对电路的操作基本上是能关的全都关了1.8伏区域的时钟关闭两个高速时钟关闭电压调节器关闭这意味着1.8伏区域的电源关闭内部的存储器和寄存器的数据全部丢失但是和停止模式一样它并不会主动关闭LSI和LSE两个低速时钟因为这两个时钟还要维持RTC和独立看门狗的运行所以不会关闭这是待机模式的介绍主要操作就是把能关的全都关掉只保留几个唤醒的功能当然配合RTC和独立看门狗的低速时钟也可以正常工作所以待机模式相当于这个人直接下班回家睡觉了没有指定的这几个事他是不会轻易回来工作的在省电程度上待机模式评级为极为省电。
模式选择
接下来我们对这里的一些细节问题再额外补充和总结一下首先是模式选择的问题刚才这里的表述出现了很多寄存器的位其中这些模式又有一些更细的划分比如睡眠模式有SLEEP-NOW和SLEEP-ON-EXIT的区别停机模式有电压调节器开启和处于低功耗的区别我们如何配置才能指定某个模式呢那看这个图就比较清晰了当然这些寄存器实际上库函数已经帮我们封装好了不用我们自己配置的但是多了解一些对我们理解程序还是有很大帮助的首先这里有一句,执行WFI等待中断或者WFE等待事件指令后STM32进入低功耗模式就说这两个指令是最终开启低功耗模式的触发条件配置其他的寄存器都要在这两个指令之前看下面这个图首先一旦WFI或者WFE执行了芯片咋知道他要进入哪种低功耗模式呢那它就会按照这个流程来判断首先看看sleep deep位是1还是0如果sleep deep等于0 就是浅睡眠对应的就是睡眠模式如果sleep deep等于1表示要进入深度睡眠模式对应的是停机或者待机模式停机和待机都可以叫做深度睡眠模式在普通的睡眠模式还有个细分的功能通过SLEEPONEXIT位来决定这一位等于0时无论程序在哪里调用WFI或WFE都会立刻进入睡眠这位等于1时执行WFI或WFE之后它会等待中断退出等所有中断处理完成之后再进入睡眠这个可能考虑到中断还有一些紧急的任务最好不要被睡眠打断了所以先等等也无妨当然这两个细分模式我们一般可以不用管只要我们不在中断函数里调用WFI或WFE那其实它们的效果是一样的我们WFI、WFE可以放在主程序里如果主程序执行到了自然也代表中断处理完成了如果你想在中断函数里调用WFI、WFE并且想中断结束后再睡眠才需要考虑下面这个模式然后继续看进入深度睡眠模式它会继续判断PDDS这一位如果PDDS等于0就进入的是停机模式如果PDDS等于1就进入的是待机模式在停机模式下它会继续判断LPDS位如果LPDS等于0就是停机模式且电压调节器开启如果LPDS等于1,就是停机模式且电压调节器低功耗 电压调节器低功耗的特性就是更省电但是唤醒延迟更高那这些就是模式选择的一个判断流程。 这个事件唤醒还是有点麻烦的你要是觉得麻烦直接使用中断唤醒的方式也是可以的。 当一个中断或唤醒事件导致退出停止模式时HSI被选为系统时钟我们的程序默认在SystemInit函数里的配置是使用的HSE外部高速时钟通过PLL倍频得到72MHz主频但是进入停止模式后PLL和HSE都停止了而现在退出停止模式时它并不会再自动帮速时钟通过PLL倍频得到8MHz直接作为主频所以如果你忽略了这个问题那么就会出现一个现象你程序刚上电是72MHz的主频但是进入停止模式在唤醒之后就变成8MHz的主频了所以我们一般在停止模式唤醒后,第一时间就是重新启动HSE配置主频为72MHz这个操作也不麻烦配置的函数他都帮我们写好了我们只需要再调用下SystemInit就行。 数据手册和参考手册 代码示例修改主频
接线图修改主频 接线图睡眠模式串口发送接收 接线图停止模式对射式红外传感器计次 接线图待机模式实时时钟
PA0这里我引出来了一根线这个线的意思就是我们可以手动把PA0接到3.3V或者断开因为PA0还有一个功能wake up可以唤醒待机模式wake up引脚上升沿有效但是这个地方也不好接按键的所以我们可以直接用一根线短接到3.3V来手动产生一个上升沿测试wake up引脚是不是有效果然后实时时钟还有一个备用电源VBAT这个接不接都行因为我们需要在待机模式下唤醒唤醒之后没有主电源程序运行不了所以主电源是不能断电的那主电源不断电备用电源接不接就都是一样的现象了。 system_stm32f10x.h、system_stm32f10x.c文件是用来配置系统时钟的也就是配置RCC时钟树这个RCC时钟树我们可以看一下PPT(前面的RCC时钟树)在这里这个图就是RCC时钟树的全部电路左边是四个时钟源HSI、HSE、LSE、LSI用于提供时钟右边就是各个外设就是使用时钟的地方我们用的最多的就是AHB时钟、APB1时钟、APP2时钟,另外还有一些时钟它们的来源不是AHB和APB比如I2S的时钟直接来源于system clockUSB的时钟直接来源于PLL当然这些特例我们就不过多关心了我们主要关心的就是这个外部的8MHz晶振它如何进行选择如何倍频才能得到这个72MHz的SYSCLK系统主频然后系统主频如何去分配才能得到AHB、APB1和APB2的时钟频率在我们之前的课程里我们一直保持着默认的配置就是晶振接的是8M主频是72MAHB和APB2是72MAPP1是36M但其实这些都不是绝对的可以根据自己的需求进行更改当然我建议一般还是不要改毕竟目前绝大多数程序都是按照默认的配置来写的随意更改可能会造成一些问题回到程序我们来看一下它这里是怎么配置这个RCC时钟树的这个点C文件最开始写了一堆注释这个注释就是对这个system文件的介绍读懂这些注释就差不多能理解这个文件了。详细看视频讲解…
代码
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
int main(void)
{OLED_Init();OLED_ShowString(1, 1, SYSCLK:);OLED_ShowNum(1, 8, SystemCoreClock, 8);//显示当前主频while (1){OLED_ShowString(2, 1, Running...);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(500);}
}12.3 串口数据收发睡眠模式
代码
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Serial.h
#include LED.h
uint8_t RxData;
uint8_t Pin_9, Pin_10;
int main(void)
{OLED_Init();Serial_Init();OLED_ShowString(1, 1, RxData:);while (1){if (Serial_GetRxFlag() 1){RxData Serial_GetRxData();Serial_SendByte(RxData);OLED_ShowHexNum(1, 8, RxData, 2);}// 没有数据要发送但代码一直执行所以可以采用睡眠模式OLED_ShowString(2, 1, Running...);Delay_ms(500);OLED_ShowString(2, 1, );Delay_ms(500);__WFI(); // 进入睡眠中断唤醒//执行WFI这时CPU会立刻睡眠,程序就停在了WFI指令这里,但是各个外设比如USRT还是工作状态//等到我们用串口助手发送数据时USRT外设收到数据产生中断唤醒CPU之后程序在暂停的地方继续运行}
}12.4 停止模式
停止模式只能通过外部中断触发(唤醒)所以和停止模式相关的代码肯定得用外部中断。
因为这个代码可以使用外部中断触发唤醒所以我们可以让它进入更为省电的停止模式在停止模式下1.8V区域的时钟关闭CPU和外设都没有时钟了但是外部中断的工作是不需要时钟的这一点从代码里也可以看出来你看初始化的时候根本就没有开启EXTI时钟的参数这也是EXTI能在时钟关闭的情况下工作的原因。
刚才讲的睡眠模式其实都只是内核的操作睡眠模式涉及的几个寄存器也都是在内核里跟PWR外设关系不大所以刚才我们都没用到PWR的库函数不过现在停止模式涉及到内核之外的电路操作这就需要用到PWR外设了我们看一下库函数。
常用函数
void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState); // 使能后备区域的访问
// 配置PVD使能电压
void PWR_PVDCmd(FunctionalState NewState); //使能PVD功能
void PWR_PVDLevelConfig(uint32_t PWR_PVDLevel);//配置PVD的阈值电压
// 使能WKUP引脚唤醒功能在待机模式下使用
void PWR_WakeUpPinCmd(FunctionalState NewState);//使能位于PA0位置的WKUP引脚 配合待机模式使用
// 进入停止模式
void PWR_EnterSTOPMode(uint32_t PWR_Regulator, uint8_t PWR_STOPEntry);
// 进入待机模式
void PWR_EnterSTANDBYMode(void);
FlagStatus PWR_GetFlagStatus(uint32_t PWR_FLAG);
void PWR_ClearFlag(uint32_t PWR_FLAG);#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include CountSensor.hint main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化CountSensor_Init(); //计数传感器初始化/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟//停止模式和待机模式一定要记得开启/*显示静态字符串*/OLED_ShowString(1, 1, Count:);while (1){OLED_ShowNum(1, 7, CountSensor_Get(), 5); //OLED不断刷新显示CountSensor_Get的返回值OLED_ShowString(2, 1, Running); //OLED闪烁Running指示当前主循环正在运行Delay_ms(100);OLED_ShowString(2, 1, );Delay_ms(100);PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); //STM32进入停止模式并等待中断唤醒SystemInit(); //唤醒后要重新配置时钟重启HSE配置72M主频//退出停止模式时HSI被选为系统时钟也就是在我们首次复位后SystemInit函数里配置的是HSE*9倍频的72M主频//所以复位后第一次Running闪烁很快而之后进入停止模式再退出时默认时钟就变成HSI了HSI是8M所以唤醒之后的程序运行就会明显变慢}
}12.5 待机模式
实时时钟待机
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include MyRTC.hint main(void)
{/*模块初始化*/OLED_Init(); //OLED初始化MyRTC_Init(); //RTC初始化/*开启时钟*/RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟//停止模式和待机模式一定要记得开启虽然MyRTC_Init里开启了多次开启无所谓防止其他没调用MyRTC_Init的场景 但时钟没开启外设就不会工作/*显示静态字符串*/OLED_ShowString(1, 1, CNT :);//秒计数器OLED_ShowString(2, 1, ALR :);//闹钟值OLED_ShowString(3, 1, ALRF:);//闹钟标志位/*使能WKUP引脚*/PWR_WakeUpPinCmd(ENABLE); //使能位于PA0的WKUP引脚WKUP引脚上升沿唤醒待机模式//手册里PWR_CSR的寄存器描述,这里写了使能wake up引脚后,wake up引脚被强制为输入下拉的配置,所以不用再GPIO初始化了/*设定闹钟*/uint32_t Alarm RTC_GetCounter() 10; //闹钟为唤醒后当前时间的后10sRTC_SetAlarm(Alarm); //写入闹钟值到RTC的ALR寄存器 这个寄存器只写不可读所以使用变量Alarm显示到OLED上OLED_ShowNum(2, 6, Alarm, 10); //显示闹钟值while (1){OLED_ShowNum(1, 6, RTC_GetCounter(), 10); //显示32位的秒计数器OLED_ShowNum(3, 6, RTC_GetFlagStatus(RTC_FLAG_ALR), 1); //显示闹钟标志位OLED_ShowString(4, 1, Running); //OLED闪烁Running指示当前主循环正在运行Delay_ms(100);OLED_ShowString(4, 1, );Delay_ms(100);OLED_ShowString(4, 9, STANDBY); //OLED闪烁STANDBY指示即将进入待机模式Delay_ms(1000);OLED_ShowString(4, 9, );Delay_ms(100);OLED_Clear(); //OLED清屏模拟关闭外部所有的耗电设备以达到极度省电PWR_EnterSTANDBYMode(); //STM32进入停止模式并等待指定的唤醒事件WKUP上升沿或RTC闹钟/*待机模式唤醒后程序会重头开始运行*///待机模式之后的代码执行不到下次继续从头开始 在程序刚开始的时候自动调用SystemInit初始化时钟所以待机模式我们就不用像停止模式那样自己调用SystemInit了//并且这个while循环实际上也只有执行一遍的机会把这个while循环去掉也是可以的}
}最后的最后我还进行了一个小实验就是验证一下待机模式到底是不是像手册里说的那样省电手册里说待机模式的电流只有三微安左右这个是不是真的呢为此我进行了测试这里我单独找了个板子把这个电源供电线正极给剪断串联一个万用表测电流当然最开始我直接测试哈待机模式的电流高达一点多毫安远超手册里说的3微安但首先很明显这个电源指示灯肯定是耗电的所以我先把这个电源指示灯去掉了再测试电流仍然有几百微安那说明还有别的东西耗电之后我就把板子背面的这个LDO稳压器去掉了这个LDO虽然我们并没有用到哈但接上它就会有电流损耗最后去掉电源指示灯和LDO之后待机模式的电流就下降到3微安了。 十三、看门狗WDG
13.1 WDG简介 独立看门狗IWDG独立工作对时间精度要求较低 窗口看门狗WWDG要求看门狗在精确计时窗口起作用 独立看门狗它的特点就是独立运行对时间精度要求较低独立运行就是独立看门狗的时钟是专用的LSI内部低速时钟即使主时钟出现问题了看门狗也能正常工作这也是独立看门狗独立的得名原因对时间精度要求较低就是独立看门狗只有一个最晚时间界限你喂狗间隔只要不超过这个最晚界限就行了你说很快的喂、疯狂的喂、连续不断的喂那都没问题之后另一个是窗口看门狗它相比较独立看门狗就严格一些了要求看门狗在精确计时窗口起作用意思就是喂狗的时间有个最晚的界限也有个最早的界限必须在这个界限的窗口内喂狗这是窗口开门口窗口的得名原因因为对于独立看门狗来说可能程序就卡死在喂狗的部分了或者程序跑飞但是喂狗代码也意外执行了或者程序有时候很快喂狗有时候又比较慢喂狗那这些状态独立看门狗就检测不到了但是窗口看门狗是可以检测到这些问题的因为它对喂狗的时间窗口可以卡的很死快了慢了都不行最后窗口开门口使用的是APB1的时钟它没有专用的时钟所以不算是独立。
IWDG框图:
它的结构和定时器是非常相似的只不过是定时器溢出产生中断而看门狗定时器溢出直接产生复位信号然后喂狗操作其实也就是重置这个计数器这是一个递减计数器减到零之后就复位那程序正常运行时为了避免复位就得在这个计数器减到零之前及时把记数值加大点这个操作就是喂狗如果你程序卡死了没有及时加大这个记数值那减到零之后就自动复位了就是看门狗的工作逻辑。
这个框图大家可以类比定时器的时机单元来看我们看一下定时器这一块是时机单元由预分频器、计数器和重装载寄存器组成左边是输入时钟比如这里是72M首先经过分频比如现在2分频 那么计数器的驱动时钟就是72M/236M这个计数器可以自增也可以自减看门狗使用的是自减运行那自减到零后定时器产生更新事件和中断而看门狗是直接产生复位另外重装值定时器是在更新事件重装而看门狗需要我们在自减到零之前手动重装因为减到零就复位了那这个手动重装计数器的操作就是喂狗看完了定时器接着来看这个独立看门狗这一块是预分频器这一块是计数器这一块是重装寄存器这基本就是一样的结构那预分频器之前输入时钟是LSI内部低速时钟 时钟频率为40KHz之后时钟进入预分频器进行分频这个预分频器只有8位所以它最大只能进行256分频上面这个预分频寄存器IWDG_PR可以配置分频系数这个PR和定时器的PSC是一个意思他们都是Prescaler的缩写可能不是一个人设计的所以这手册里很多缩写都不太一样不过大家要知道他们其实是一个意思后面经过预分配器分频之后时钟驱动递减计数器每来一个时钟自减一个数另外这个计数器是12位的所以最大值是2^12-14095然后当自减到0之后产生IEDG复位正常运行时为了避免复位我们可以提前在重装寄存器写个值IWDG_RLR和定时器的ARR是一样的RLR是reloaderARR是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超时时间: 13.2 窗口看门狗WWDG 窗口看门狗从功能上来说和独立看门狗还是比较像的大体上来看只是比独立看门狗多个最早喂狗时间的限制但是等会儿学的时候你就会发现这个窗口看门狗无论是框图的设计还是寄存器的分布和命名规则或者程序的操作流程和独立看门狗都不是一个思路可能是两个看门狗侧重点不一样吧当然我感觉应该还是因为这两个外设不是同一个人设计的所以设计的思路有所不同。
左下角是时钟源部分这个时钟源是PCLK1右边这个是预分频器它这个预分频器名字又变了 叫WDGTB实际上和独立开门狗的PR定时器的PSC都是一个东西上面这个是6位递减计数器CNT这个计数器是位于控制寄存器CR里的计数器和控制寄存器合二为一了然后窗口看门狗没有重装寄存器那如何重装计数器进喂狗呢这个我们直接在CNT写个数据就行了想写多少就写多少这上面这一块是窗口值由此喂狗的最早时间界限就写到这里存起来最后左边就是输出信号的操作逻辑什么情况下会产生复位就这几个逻辑门来确定。
我们来详细看一下它的工作流程首先还是从左下角开始看时钟来源是PCLK1也就是APB1的时钟这个时钟默认是36MHz所以就是36MHz的时钟进来之后还是先经过一个预分频器进分频这个和独立看门狗的预分频器定时器的预分频器都是一个作用就是灵活的调节后面计数器的时钟频率同时预分频系数也是计算计数器溢出时间的重要参数那接着分频之后的时钟驱动这个计数器进行计数这个计数器和独立看门狗一样也是一个递减计数器每来一个时钟自减一次不过这个计数器比较特殊从图上来看这里写了T6到T0总共是七个位但下面却写的是六位递减计数器这是为什么呢那这其实是因为这个计数器只有T5到T0这六位是有效的就值最高位T6这里用来当做溢出标志位第六位等于1时表示计数器没溢出T6位等于0时表示计数器溢出不过对于硬件电路来说T6位其实也是计数器的一部分只不过是T6位被单独拎出来当做标志位了而已。 总结一下就是如果你把T6位看作是计数器的一部分那要是整个计数器值减到0X40之后移出而如果你把T6位当成溢出标志位低6位当做计数器那就是低6位的计数值减到0之后溢出这一点尤其要搞清楚。
左边的复位信号输出部分首先这个WDGA是窗口看门狗的激活位也就是使能WDGA写入1启用窗口看门狗使能位作用于这个与门。
下面这一路或门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的频率还是太快了先来个固定分频给降一降。 手册 代码示例实现IWDG
接线图按键用于阻塞喂狗。 根据我们上小节的知识点我们总结一下独立看门狗的配置流程首先看一下这个框图从左到右第一步应该就是开启时钟了只有这个LSI时钟开启了独立看门狗才能运行所以初始化独立看门狗之前LSI必须得开启但是这个开启LSI的代码并不需要我们来写我们看一下手册6.2.9如果我们开启了独立看门狗那么LSI就会跟随强制打开等LSI稳定后就可以自动为独立看门狗提供时钟了所以我们这里的第一步开启LSI的时钟就不需要我们再写代码来执行了下一步我们就是写入预分频器和重装寄存器了当然在写入这两个寄存器之前不要忘了这里的写保护首先写入这个键值0X5555解除写保护然后再写入预分频和重装值所以这里的第二步就是解除写保护第三步是写入预分频和重装值预分频和重装值具体写入多少我们可以通过这里的超时时间公式来计算最后当这些配置工作做完之后我们就可以执行这条指令来启动独立看门狗了(写入0xCCCC)然后在主循环里我们可以不断执行这条指令来进行喂狗(0xAAAA)这是独立看门狗的配置流程。 常用函数
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);代码
#include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.hint 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);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); //设置预分频为16IWDG_SetReload(2499); //设置重装值为2499独立看门狗的超时时间为1000msIWDG_ReloadCounter(); //重装计数器喂狗IWDG_Enable(); //独立看门狗使能//喂狗或使能的时候会在键寄存器写入5555之外的值这时就顺便给寄存器写保护了就不用再手动执行写保护了while (1){Key_GetNum(); //调用阻塞式的按键扫描函数模拟主循环卡死 按住按键不放主循环就会阻塞不能执行后面喂狗IWDG_ReloadCounter(); //重装计数器喂狗OLED_ShowString(4, 1, FEED); //OLED闪烁FEED字符串Delay_ms(200); //喂狗间隔为200600800msOLED_ShowString(4, 1, );Delay_ms(600);}
}13.4 实现WWDG 先看一下窗口看门狗的框图这里因为窗户看门狗的时钟来源是PCLK1所以第一步我们需要开启窗户看门狗APB1的时钟这个第一步需要我们自己来执行不会像独立看门狗自动开启第二步就是配置各个寄存器了比如预分频和窗口值窗口看门狗没有写保护所以第二步就可以直接写这些寄存器了第三步写入控制性器CR控制寄存器包含看门狗使能位、计数器溢出标志位和计数器有效位这些东西需要一起设置放在第三步统一执行之后在运行过程中我们不断向计数器写入想要的重装值这样就可以进行喂狗了这是窗口看门狗的操作流程。
常用函数
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);函数WWDG_Enable()为何要传递一个参数为了使能的同时顺便喂一下狗 #include stm32f10x.h // Device header
#include Delay.h
#include OLED.h
#include Key.hint 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的时钟 PCLK1时钟/*WWDG初始化*/WWDG_SetPrescaler(WWDG_Prescaler_8); //设置预分频为 8WWDG_SetWindowValue(0x40 | 21); //设置窗口值窗口时间为30ms T6位也要设置成1所以或上0x40WWDG_Enable(0x40 | 54); //使能并第一次喂狗超时时间为50ms T6位也要设置成1所以或上0x40while (1){Key_GetNum(); //调用阻塞式的按键扫描函数模拟主循环卡死OLED_ShowString(4, 1, FEED); //OLED闪烁FEED字符串Delay_ms(20); //喂狗间隔为202040msOLED_ShowString(4, 1, );Delay_ms(20);WWDG_SetCounter(0x40 | 54); //重装计数器喂狗}
}STM32内部FLASH闪存
程序现象
1、读写内部FLASH 这个代码的目的就是利用内部flash程序存储器的剩余空间来存储一些掉电不丢失的参数如果你有一些配置参数需要掉电不丢失的保存再外挂一个存储器芯片的话显然会增加硬件成本那STM32本身不就是有掉电不丢失的程序存储器吗我们直接把参数存在这里是不是就又方便又节省成本所以这里的程序是按下K1变换一下测试数据然后存储到内部FLASH按下K2把所有参数清0最后OIED显示一下。 2、
STM32内部FLASH闪存简介 我们本节的任务就是对这些存储器进行读写那我们怎么操作这些存储器呢这需要用到这个闪存存储器接口闪存存储器接口是一个外设是这个闪存的管理员毕竟闪存的操作很麻烦涉及到擦除、编程、等待、解锁等等操作所以这里我们需要把我们的指令和数据写入到这个外设的相应寄存器然后这个外设就会自动去操作对应的存储空间那后面写的是这个外设可以对程序存储器和选项字节这两部分进行擦除和编程对比上面的三个部分呢少了系统存储器这个区域因为系统处理器是原厂写入的BOOTLOADER程序这个是不允许我们修改的。
读写FLASH的用途 利用程序存储器的剩余空间来保存掉电不丢失的用户数据 通过在程序中编程IAP实现程序的自我更新 第一个用途对于我们这个C8T6芯片来说它的程序存储器容量是64K一般我们写个简单的程序可能就只占前面的很小一部分空间剩下的大片空余空间我们就可以加以利用比如存储一些我们自定义的数据这样就非常方便而且可以充分利用资源不过这里要注意我们在选取存储区域时一定不要覆盖了原有的程序要不然程序自己把自己给破坏了一般存储少量的参数我们就选最后几页存储就行了关于如何查看程序所占用空间的大小这个我们下小节也会介绍然后第二个用途就是通过在程序中编程IAP实现程序的自我更新刚才说了我们在存储用户数据时要避开程序本身 以免破坏程序但如果我们就非要修改程序本身这会发生什么呢那这就是第二点提到的功能在程序中编程利用程序来修改程序本身实现程序的自我更新这个在程序中编程就是IAP在数码圈也有个可能大家更熟悉的技术叫OTA这俩是类似的东西都是用来实现程序升级的但这个IAP升级程序的功能比较复杂我们本课程暂时就不涉及了之后有缘再说吧。
在线编程In-Circuit Programming – ICP用于更新程序存储器的全部内容它通过JTAG、SWD协议或系统加载程序Bootloader下载程序 ICP英文直译过来也可以叫在电路中编程意思就是下载程序你只需要留几个引脚就行不用拆芯片了就叫在电路中进行编程ICP的作用是用于更新程序存储器的全部内容它通过JTAG SWD协议或系统加载程序BOOTLOADER下载程序这个JTAG SWD就是仿真器下载程序就是我们目前用的stlink使用SWD下载程序每次下载都是把整个程序完全更新掉那系统加载程序就是系统存储器的BOOTLOADER也就是串口下载串口下载也是更新整个程序这就是我们一直在用的ICP下载方式之后更高级的下载方式就是在程序中编程In-Application Programming – IAP简称IAP它可以使用微控制器支持的任意一种通信接口下载程序怎么实现呢那比如这是整个程序存储器我们首先需要自己写一个BOOTLOADER程序并且存放在程序更新时不会覆盖的地方比如我们放在最后面然后需要更新程序时我们控制程序跳转到这个自己写的BOOTLOADER这里来在这里面我们就可以接收任意一种通讯接口传过来的数据比如串口、USB、蓝牙转串口、WIFI转串口等等这个传过来的数据就是待更新的程序然后我们控制flash读写把收到的程序写入到前面程序正常运行的地方写完之后再控制程序跳转回正常运行的地方或者直接复位这样程序就完成了自我升级这个过程其实就是和系统存储器这个的BOOTLOADER一样因为程序要实现自我升级在升级过程中肯定需要布置一个辅助的小机器人来临时干活只不过是系统存储器的BOOTLOADER写死了只能用串口下载到指定位置启动方式也不方便只能配置boot引脚触发启动而我们自己写BOOTLOADER的话就可以想怎么收怎么收想写到哪就写到哪想怎么启动就怎么启动并且在整个升级过程程序都可以自主完成实现在程序中编程更进一步就可以直接实现远程升级了非常灵活方便。
接下来的内容我们就只涉及最基本的对flash进行读写这也是实现IAP的基础。 我们C8T6芯片的闪存容量是64K属于重容量产品对于小容量产品和大容量产品闪存的分配方式有些区别这个可以参考一下手册那首先提醒一下闪存这一章的内容在手册里是单独列出来的并不在之前的参考手册里我们需要打开这个闪存编程参考手册这里以中容量产品为例来讲解首先看一下第一列的几个块这里分为了三个块第一个是主存储器也是我们刚才说的程序存储器用来存放程序代码的这是最主要也是容量最大的一块下面第二个是信息块里面又可以分为启动程序代码和用户选择字节其中启动程序代码就是刚才说的系统存储器存放的是原厂写入BOOTLOADER用于串口下载这个手册的名称经常会有不同的表述方式但大家要知道某些名称描述的其实是一个东西然后下面这个用户选择字节就是刚才说的选项字节存放一些独立的参数这个选项字节在手册里一直都称作选择字节可能是翻译的问题英文是option bytes我们一般都叫选项字节然后最后一块是闪存存储器接口寄存器这一块的存储器实际上并不属于闪存你看那个地址就知道地址都是40开头的说明这个存储器接口寄存器就是一个普通的外设和之前讲的GPIO定时器、串口等等都是一个性质的东西这些存储器它们的存储介质也都是sram这个闪存存储器接口就上面这些闪存的管理员这些寄存器就是用来控制擦除和编程这个过程的那到这里这个表的整体我们就清楚了我们擦除和编程就通过读写这些寄存器来完成当然这里只有擦除和编程并没有读取这是因为读取指定存储器直接使用指针读即可用不到这个外设。
继续看这个表对于主存储器这里对它进行了分页分页是为了更好的管理闪存擦除和写保护都是以页为单位的这点和之前W25Q64芯片的闪存一样同为闪存它们的特性基本一样写入前必须擦除擦除必须以最小单位进行擦除后数据位全变为1数据只能1写0不能0写1擦除和写入之后都需要等待忙这些都是一样的学习这节之前大家可以再复习一下W25Q64再学这一节就会非常轻松了那W25Q64的分配方式是先分为块block再分为扇区sector比较复杂这里就比较简单了它只有一个基本单位就是页每一页的大小都是1K0到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位。 接下来看一下我总结的这个基本结构图整个闪存分为程序存储器、系统存储器和选项字节三部分这里程序存储器为以C8T6为例它是64K的所以总共只有64页最后一页的起始地址是0800FC00左边这里是闪存存储器接口手册里还有个名称闪存编程和擦除控制器LPEC大家也知道这两个名称其实是一个东西就行然后这个控制器就是闪存的管理员他可以对程序存储器进行擦除和编程也可以对选项字节进行擦除和编程系统容器是不能擦除和编程的这个选项字节里面有很大一部分配置位其实是配置主程序存储器的读写保护的所以右边画的写入选项字节可以配置程序存储器的读写保护当然选项字节还有几个别的配置参数这个待会再讲那这就是整个闪存的基本结构。
接下来我们来看一下细节问题如何操作这个控制器FPEC来对程序存储器和选项字节进行擦除和编程。 首先第一步是flash解锁这和之前W25Q64一样W25Q64操作之前需要写使能这个flash操作之前需要解锁目的都是为了防止误操作那这里解锁的方式和之前独立看门狗一样都是通过在键寄存器写入指定的键值来实现使用键寄存容器的好处就是更能防止误操作每一个指令必须输密码才能完成通过英文名称也能看出来键的英文是KEY直译是不是钥匙的意思所以这个更形象的翻译我们可以把它叫做钥匙寄存器密钥寄存器首先LPEC共有三个键值也就是三把开锁的钥匙RDPRT键是解除读保护的密钥值是0XA5KEY1键值是0X45670123KEY2键值是0XCDEF89AB为什么是这些值呢实际上是随便定义的只要你定义的不是很简单就行 继续看怎么解锁呢第一个是复位后FPEC被保护不能写入FLASH_CR也就是复位后 flash默认是锁着的然后在FLASH_KEYR键寄存器中先写入KEY1再写入KEY2解锁我们找到了锁这个锁是KEYR寄存器怎么解呢要先用K1钥匙解再用K2钥匙解最终才能解锁成功所以这个锁的安全性非常高有两道锁即使程序跑飞了歪打正着正好写入了KEY1那也难以保证下一次又歪打正着写入了KEY2所以非人为情况下基本不可能解锁然后第三条还有进一步的保护措施 就是错误的操作序列会在下次复位前锁死FPEC和FLASH_CR于是他发现有程序在尝试撬锁时一旦没有先写入KEY1再写入KEY2整个模块就会完全锁死除非复位这是整个解锁操作可以看到安全性非常高接着继续看解锁之后如何加锁呢我们操作完成之后要尽快把flash重新加锁以防止意外情况加锁的操作是设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR这个比较简单就是控制寄存器里面有个LOCK位我们在这一位写1就能重新锁住闪存。 接着看下一个知识点这个地方我们要学习的是如何使用指针访问存储器因为STM32内部的存储器是直接挂在总线上的所以这时在读写某个存储器就非常简单了直接使用C语言的指针来访问即可。
如果你这个地址写的是SRAM的地址比如0X20000000那可以直接写入了因为SRAM在程序运行时是可读可写的这是使用指针访问存储器的C语言代码0X08000000其中读取可以直接读写入需要解锁并且执行后面的流程。
接下来就来看一下下面这三个流程图第一个是编程也就是写入第二个是页擦除STM32的闪存也是写入前必须擦除擦除之后所有的数据位变为1擦除的最小单位就一页1K1024字节第三个是全删除把所有页都给擦除掉那首先说一下这个详细的流程库函数已经帮我们都写好了我们直接调用一个整体的函数就行非常简单这里我们只大概的了解一下详细步骤研究得越深操作越得心应手。 全擦除 第一步是读取lock位看一下芯片锁没锁下面如果lock位等于1锁住了就执行解锁过程解锁过程就是在KEYR寄存器先写入KEY1再写入KEY2这里如果它当前没锁住就不用解锁了这是流程图里给的解锁步骤如果锁住了就解锁如果没锁住就不用解锁但是在库函数中并没有这个判断库函数是直接执行解锁过程管你锁没锁都执行解锁这个比较简单直接不过效果都一样然后继续解锁之后首先置控制寄存器里的MER(Mass Erase)位为1然后再置STRT(Start)位为1其中STRT为1是触发条件,STRT为1之后芯片开始干活然后现在看到MER位是1它就知道接下来要干的活就是全删除这样内部电路就会自动执行全擦除的过程然后继续擦除也是需要花一段时间的所以擦除过程开始后程序要执行等待判断状态寄存器的BSY(Busy)位是否为1BSY位表示芯片是否处于忙状态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这样写入数据的过程就完成了那每执行这样一个流程只能写入一个半字如果要写出很多数据要不断循环调用这个流程就可以了。 接下来我们再介绍一下选项字节这块内容大概了解一下就行了首先这里是选项字节的组织和用途 图里的起始地址就是我们刚才说的选项字节的起始地址1FFF800这块的这些数据就前面这里这个表的这一行里面总共只有16个字节把这些存储器给展开就这个图这里是对应的16个字节 其中有一半的名称前面都带了个N比如RDP和nRDP USER和nUSER等这个意思就是你在写入RDP数据时要同时在NRDP写入数据的反码其他的这些都是一样写这个存储器时要在带N的对应的存储器写入反码这样写入操作才是有效的如果芯片检测到这两个存储器不是反码的关系那就代表数据无效有错误对应的功能就不执行这是一个安全保障措施但这个写入反码的过程 硬件会自动计算并写入不需要我们操心使用库函数的话那就更简单了,函数都给我们分装好了 直接调用函数就行那然后看一下每个存储器的功能去掉所有带n的就剩下八个字节存储器了第一个RDP(Read Protect)是读保护配置位下面有解释在RDP存储器写入RDPRT键就刚才说的A5 然后解除读保护如果RDP不是A5那闪存就是读保护状态无法通过调试器读取程序避免程序被别人窃取接着看第二个字节USER这个是一些零碎的配置位可以配置硬件看门狗和进入停机待机模式是否产生复位这个了解即可然后第三个和第四个字节data0和data1这个在芯片中没有定义功能用户可自定义使用最后四个字节WRP(Write Protect)0、1、2、3这四个字节配置的是写保护在中容量产品里是每一个位对应保护四个存储页四个字节总共32位一位对应保护四页总共保护32×4等于128页正好对应中容量量的最大128页那对于小容量和大容量产品呢可以看一下手册2.5选项字节说明这里对于小容量产品也是每一位对应保护四个存储页但小容量产品最大只有32K所以只需要一个字节WRP0就行4×832其他三个字节没用到然而对于大容量产品每一个位只能保护两个存储页这样的话四个字节就不够用了所以这里规定WRP3的最高位这一位直接把剩下的所有页一起都保护了这是写保护的定义。
然后看一下如何去写入这些位呢这里两页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表示即将写入选项字节再之后写入要编程的半字到指定的地址这个是指针写入操作最后等待忙这样写入选项字节就完成了。 最后我们花几分钟学一下器件电子签名这个非常简单既然讲到闪存了就顺便学习一下吧 看一下电子签名存放在闪存存储器模块的系统存储区域包含的芯片识别信息在出厂时编写不可更改 使用指针读指定地址下的存储器可获取电子签名电子签名其实就是STM32的id号它的存放区域是系统存储器它不仅有BOOTLOADER程序还有几个字节的id号系统存储器起始地址是1FFFF000看下这里这里有两段数据第一个是闪存容量存储器基地址是1FFF F7E0通过地址也可以确定它的位置就是系统存储器这个存储器的大小是16位它的值就是闪存的容量单位是KB然后第二个是产品唯一身份标识寄存器就是每个芯片的身份证号这个数据存放的基地址是1FFFF7E8大小是96位每一个芯片的这96位数据都是不一样的使用这个唯一id号可以做一些加密的操作比如你想写入一段程序只能在指定设备运行那也可以在程序的多处加入id号判断如果不是指定设备的id号就不执行程序功能这样即使你的程序被盗在别的设备上也难以运行这是STM32的电子签名。
手册 在编程过程中任何读写闪存的操作都会使CPU暂停直到此闪存编程结束这是读写内部闪存存储数据的一个弊端忙的时候代码执行会暂停因为执行代码需要读闪存闪存在忙没法读所以CPU也就没法运行了程序就会暂停这会导致什么问题呢假如你使用内部闪存存储数据同时你的中断代码又在频繁执行这样读写闪存的时候中断代码就无法执行了这可能会导致中断无法及时响应。 示例代码读写内部FLASH读取芯片ID