江科大STM32学习笔记(上)
江科大STM32学习笔记(下)
前置知识
相当于专有名词解释
串行与并行
数字数据通信接口可以分为两大类:串行接口和并行接口。
串行通信,又称为逐位传输(Bit-by-Bit Transmission),是指按顺序逐个传输数据位的通信方式。在串行通信中,数据位按照顺序逐一传输,通过传输线进行数据传输。虽然传输速度较慢,但实现简单。串行通信常用于长距离的数据传输,如串口、USB接口等。
并行通信是一种同时传输多个数据位的通信方式,也称为同时传输多个数据位(Word-by-Word Transmission)。在并行通信中,数据被分成多个并行传输,同时通过多个传输线进行数据传输。虽然传输速度快,但实现起来较为复杂。并行通信常用于短距离的数据传输,如计算机内部数据总线等。
并行数据传输,可以将一个完整的字节(单词或更大的数据)一下子从发送器传输到了接收器。如你所料,并行接口比串行接口快得多,因为并行-串行和串行-并行的解/译码步骤被省略了。而并行传输的缺点是:需要足够数量的传输线(导线)来传输单独的数字。
同步与异步通讯
根据通讯的数据同步方式,又分为同步和异步两种,可以根据通讯过程中是否有使用到时钟信号进行简单的区分。
在同步通讯中,收发设备双方会使用一根信号线表示时钟信号,在时钟信号的驱动下双方进行协调, 同步数据,见图 同步通讯 。 通讯中通常双方会统一规定在时钟信号的上升沿或下降沿对数据线进行采样。
同步通信的数据帧组成一般是:同步信号+若干数据。在最前面是个同步信号,接收端接收数据分析出同步信号之后,就认为后边的数据都是实际传输的数据了。理论上来说同步通信一个数据帧里面的若干数据的位数是不受限制的。
同步通信中,数据之间是不能有间隔的,因为双方在同一个时钟下工作,这边接收的,必然是另一边发送的。在同步信号之后,认为所有的数据都是实际数据,所以当没有信息要传输是,同步信号要填上空字符。
异步通信是一种常用的通信方式,发送字符之间的时间间隔可以是任意的。在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些同步用的信号位,或者把主体数据进行打包, 以数据帧的格式传输数据,某些通讯中还需要双方约定数据的传输速率,以便更好地同步。
异步通信在发送字符时,所发送的字符之间的时间间隔可以是任意的。因为每一帧的数据都有开始和停止位,他们之间的数据位才是实际数据。所以接收方评判数据是否为完整的一帧数据的方式就是分析这一堆数据中的开始位和停止位。发送端可以在任意时刻开始发送字符,接收端必须时刻做好接收的准备。因为每传输一个数据帧都会有一个开始位和一个停止位,实际数据一般只占到5-8位,这就导致了异步通信的传输效率较低。
同步与异步通信区别:
1.同步通信要求接收端和发送端时钟频率一致,而异步通信不要求时钟同步。
2.同步通信效率高,异步通信效率较低。
3.同步通信较复杂,时钟允许误差较小,而异步通信相对简单,时钟可允许一定误差。
4.同步通信可用于点对多点,而异步通信只适用于点对点。
补充:I2C和SPI由于具有独立的时钟线,因此它们是同步的。在时钟信号的指引下,接收方可以采样数据。然而,串口、CAN和USB没有时钟线,因此需要双方约定一个采样频率,这就是异步通信。为了对齐采样位置,还需要添加一些帧头和帧尾等标识。
同步靠时钟线,异步靠比特率
通讯速率
衡量通讯性能的一个非常重要的参数就是通讯速率,通常以比特率(Bitrate)来表示,即每秒钟传输的二进制位数, 单位为比特每秒(bit/s)。
容易与比特率混淆的概念是波特率(Baudrate),它表示每秒钟传输了多少个码元。 而码元是通讯信号调制的概念,通讯中常用时间间隔相同的符号来表示一个二进制数字,这样的信号称为码元。 如常见的通讯传输中,用0V表示数字0,5V表示数字1,那么一个码元可以表示两种状态0和1,所以一个码元等于一个二进制比特位, 此时波特率的大小与比特率一致;如果在通讯传输中,有0V、2V、4V以及6V分别表示二进制数00、01、10、11, 那么每个码元可以表示四种状态,即两个二进制比特位,所以码元数是二进制比特位数的一半,这个时候的波特率为比特率的一半。
因为很多常见的通讯中一个码元都是表示两种状态,人们常常直接以波特率来表示比特率,虽然严格来说没什么错误,但希望您能了解它们的区别。
在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设;STM32标准库则是在寄存器与用户代码之间的软件层。 对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层 。物理层规定通讯系统中具有机械、电子功能部分的特性, 确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准。 简单来说物理层规定我们用嘴巴还是用肢体来交流,协议层则规定我们用中文还是英文来交流。
USART串口
I2C通信协议
BKP备份寄存器&RTC实时时钟
Unix时间戳
这小节的内容属于计算机领域的一个通用知识点,不特别应用在STM32中。
GMT
(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准
UTC
(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192.631.770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致
润秒
,也称为闰秒,是国际地球自转服务(IERS)为了使协调世界时(UTC)与地球自转时间保持一致而插入或删除的额外一秒钟。由于地球自转的不均匀性,如潮汐摩擦和地球内部的变化,平均日长会发生变化。为了保持协调世界时与原子时标(International Atomic Time, TAI)的一致性,需要不定期地调整闰秒。
时间戳转换
#include <stdio.h>
#include <time.h>
int main() {
// 获取当前时间
time_t now;
time(&now); // 获取当前时间戳
// 使用localtime将时间戳转换为本地时间结构体
struct tm *local = localtime(&now);
if (local == NULL) {
fprintf(stderr, "Error in localtime\n");
return 1;
}
// 打印当前时间
printf("当前时间: %d-%d-%d %d:%d:%d\n", local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec);
// 使用asctime将时间结构体转换为字符串
char *asctime_str = asctime(local);
printf("时间字符串: %s", asctime_str);
// 使用difftime计算两个时间之间的差值
time_t future_time = now + 10; // 假设的未来时间(10秒后)
double diff = difftime(future_time, now);
printf("当前时间与未来时间相差: %f 秒\n", diff);
// 使用strftime格式化时间字符串
char formatted_time[100];
strftime(formatted_time, sizeof(formatted_time), "%Y-%m-%d %H:%M:%S", local);
printf("格式化后的时间: %s\n", formatted_time);
return 0;
}
BKP
简介
基本结构
- 电池供电:图中橙色区域可以称作后备区域,STM32的后备区域特性是当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电;当VDD主电源上电时,后备区域由VBAT切换到VDD。
- 侵入检测:这个功能可以用来检测对单片机封装的物理攻击,如打开封装、温度变化、电压干扰等。当TAMPER引脚检测到侵入事件时,单片机可以触发一个中断,或者将后备寄存器中的特定数据清零,以保护存储在其中的敏感信息。
- 时钟输出:可以将RTC的相关时钟,从PC13位置的RTC引脚输出出去,供外部使用。其中,输出校准时钟时,再配合这个校准寄存器,可以对RTC的误差进行校准。
单片机的后备区域(Backup Domain)是指在单片机中,为了在系统主电源失效时仍能保持数据不丢失的区域。这个区域通常由一个专门的电源域组成,该电源域由一个纽扣电池或超级电容供电,以确保在主电源断电时,后备区域中的数据仍然能够得到保存。
后备区域通常包括以下部分:
后备寄存器(Backup Registers):这是一组存储单元,可以在主电源断电时保持其内容。它们通常用于存储关键的数据,如系统配置参数、实时时钟(RTC)的配置和状态等。
实时时钟(RTC):实时时钟是一个能够在主电源断电时继续运行的时钟模块,它通常有自己的电源域和振荡器,可以在后备电源的支持下继续计时。
后备SRAM:部分单片机在后备区域中包含一定量的SRAM,这些SRAM可以在主电源断电时保持数据不丢失。
唤醒电路:有些单片机的后备区域还包含唤醒电路,可以在后备电源的支持下检测外部事件或定时器事件,并在需要时重新启动系统。
后备区域的设计是为了在主电源断电或系统复位的情况下,保持关键数据不丢失,并能够在适当的时刻恢复系统的运行状态
。这在需要高可靠性和数据保持的应用中非常重要,如医疗设备、工业控制系统、汽车电子等。
RTC
简介
RTCCLK是STM32微控制器中RTC模块的时钟源,它有三种可能的来源:HSE时钟除以128、LSE振荡器和LSI振荡器。下面是这三种时钟源的详细讲解和工作机制:
-
HSE时钟除以128
HSE(High-Speed External)时钟是STM32的一个外部时钟源,通常由一个晶振或陶瓷谐振器提供。HSE时钟的频率可以是4MHz、8MHz、16MHz等,但最常用的是8MHz。
工作机制
:当选择HSE作为RTCCLK源时,HSE时钟首先被分频器除以128,得到62.5kHz的时钟信号。然后,这个信号再通过RTC的预分频器进一步分频,以产生1Hz的RTC时钟。由于HSE时钟的频率较高,这种配置可以提供较快的时钟初始化时间。
优点
:不需要额外的晶振,节省成本和空间。
缺点
:在VDD电源断电时,HSE时钟也会停止,因此需要VBAT电源来维持RTC运行。 -
LSE振荡器
LSE(Low-Speed External)振荡器是一个32.768kHz的晶振,专门用于提供低功耗的时钟信号给RTC模块。
工作机制
:LSE振荡器直接提供32.768kHz的时钟信号给RTC。这个频率非常适合RTC,因为它可以被精确地分频为1Hz。RTC的预分频器设置为32768,这样每32.768kHz的周期就对应于1秒的RTC时钟。【一秒32768hz,那么这个频率完成2^15次计数产生溢出所用的时间就是1s,也就是1s一次自然溢出。这样就不用额外设计一个计数目标】
优点
:频率稳定,适合长时间计时;只有这一路在VDD电源断电时,LSE振荡器可以由VBAT电源供电,保证RTC的持续运行。 -
LSI振荡器
LSI(Low-Speed Internal)振荡器是一个内置的RC振荡器,其频率为40kHz。【内部RC振荡器一般没有外部晶振高】
工作机制
:LSI振荡器提供40kHz的时钟信号给RTC。由于这个频率不是标准的RTC时钟频率,因此需要通过RTC的预分频器进行分频,以得到接近1Hz的RTC时钟。由于LSI的频率并不非常稳定,因此它通常不用于对时间精度要求很高的应用。
优点
:不需要外部晶振,降低了成本和电路复杂性。
缺点
:频率不稳定,可能导致时间误差;在VDD电源断电时,LSI振荡器也会停止,因此需要VBAT电源来维持RTC运行。
最常用选择LSE(Low-Speed External)振荡器作为RTC(Real Time Clock)的时钟来源的原因主要有以下几点:
- 本身就专供RTC使用的,其余都有各自的任务:HSE主要作为系统主时钟,LSI主要作为看门狗时钟。
最重要的原因只有它可以通过VBAT备用电池供电,其余两路时钟,在主电源断电后,是停止运行的
。
框图
详细讲解,有需求可以看视频,更好学习。
RTC基本结构
首先,我们需要确定RTC的时钟源。随后,RTCCLK将通过预分频器进行分频处理。在这个过程中,余数寄存器充当一个自减计数器,负责记录当前的计数值,而重装寄存器则设定了计数的目标值,从而决定了分频的比率。完成分频后,我们得到一个1Hz的秒脉冲信号,该信号被送入一个32位的计数器,该计数器每秒钟递增一次。此外,还存在一个32位的闹钟寄存器,用于设置闹钟时间;如果不使用闹钟功能,可以忽略这一部分。
在右侧,有三个中断信号源,分别是秒脉冲信号、计数器溢出信号和闹钟信号。这些信号首先需要通过中断输出控制进行使能,只有被使能的中断信号才能传递到NVIC(嵌套向量中断控制器),进而向CPU发出中断请求。
在编程过程中,我们首先设置数据选择器以选择时钟源,接着配置重装寄存器以确定分频系数;配置32位计数器以进行日期和时间的读写操作;如果需要闹钟功能,配置32位闹钟值即可;对于中断功能,首先启用中断,然后配置NVIC,并编写相应的中断服务函数。这些步骤构成了RTC外设配置的核心内容。
RTC硬件电路
在最小系统上,外部电路还要额外额外加两部分:第一部分是备用电池供电;第二部分是外部低速晶振。
备用电池供电
-
简单连接
就是直接使用一个3V的电池,负极和系统供地,正极直接引到STM32的VBAT引脚,参考是手册中的内容: -
推荐连接
在VBAT引脚和备用电池之间,通常会放置一个二极管(如1N4148)。这个二极管的作用是在主电源存在时阻止电池向系统供电,并在主电源断电时允许电池为系统供电。在VBAT引脚附近,通常会放置一个退耦电容(如10uF),用于平滑电源电压,减少电源噪声,确保RTC的稳定运行。
这个方案参考手册中:
外部低速晶振
RTC操作注意事项
- 首先,值得注意的是,虽然大多数外设只需开启时钟即可使用,但BKP和RTC的操作相对复杂。若需使用这两个外设,必须遵循以下两步:第一步是开启PWR和BKP的时钟,第二步是通过PWR使能对BKP和RTC的访问。在初始化过程中,务必按照这一流程操作。
- 当我们读取RTC寄存器时,需要特别留意,如果RTC的APB1接口之前处于禁止状态,我们必须等待RTC_CRL寄存器中的RSF位被硬件置1,这一步在代码中对应的是调用RTC等待同步的库函数。通常,这个函数会在设备刚上电时执行一次。为什么要有这一步呢?可以看看框图,是因为存在两个不同的时钟域:PCLK1(36MHz)和RTCCLK((32KHz)。PCLK1在主电源掉电时会停止,而RTCCLK则不会,它由RTC的晶振驱动,确保RTC在电源掉电时仍能正常工作。
在读取RTC寄存器时,由于PCLK1和RTCCLK的频率不同,会出现时钟不同步的问题。RTC寄存器的值是在RTCCLK的上升沿更新的,但PCLK1的频率远高于RTCCLK。如果在APB1接口刚刚启用时就立即读取RTC寄存器,可能会读取到还未同步的旧值,通常会是0。因此,在APB1总线刚开机时,我们需要等待RTCCLK的上升沿,以确保RTC寄存器的值已经同步到APB1总线上。这个过程是自动的,只需调用等待同步的函数即可。 - 接下来,必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。这一操作虽然简单,但它是RTC设置时间的关键步骤。在库函数中,每个写寄存器的函数都会自动执行这一操作,因此无需单独调用函数进入配置模式。
- 最后,对RTC任何寄存器的写操作,都应在前一次写操作完成后进行。通过查询RTC_CR寄存器中的RTOFF状态位,可以判断RTC寄存器是否处于更新中。只有当RTOFF状态位为1时,才能进行下一次写入操作。这一步骤在代码中同样通过调用一个等待函数实现。这与读写flash芯片的操作类似,旨在确保写入操作的完整性。原因在于PCLK1和RTCCLK的时钟频率不同,写入操作完成后需等待RTCCLK的上升沿,以确保值正确更新到RTC寄存器。了解这一操作后,在代码中只需调用相应的等待函数即可。
代码实战:读写备份寄存器&事实时钟
- 读写备份寄存器接线图
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
uint8_t KeyNum; //定义用于接收按键键码的变量
uint16_t ArrayWrite[] = {0x1234, 0x5678}; //定义要写入数据的测试数组
uint16_t ArrayRead[2]; //定义要读取数据的测试数组
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
Key_Init(); //按键初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "W:");
OLED_ShowString(2, 1, "R:");
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
while (1)
{
KeyNum = Key_GetNum(); //获取按键键码
if (KeyNum == 1) //按键1按下
{
ArrayWrite[0] ++; //测试数据自增
ArrayWrite[1] ++;
BKP_WriteBackupRegister(BKP_DR1, ArrayWrite[0]); //写入测试数据到备份寄存器
BKP_WriteBackupRegister(BKP_DR2, ArrayWrite[1]);
OLED_ShowHexNum(1, 3, ArrayWrite[0], 4); //显示写入的测试数据
OLED_ShowHexNum(1, 8, ArrayWrite[1], 4);
}
ArrayRead[0] = BKP_ReadBackupRegister(BKP_DR1); //读取备份寄存器的数据
ArrayRead[1] = BKP_ReadBackupRegister(BKP_DR2);
OLED_ShowHexNum(2, 3, ArrayRead[0], 4); //显示读取的备份寄存器数据
OLED_ShowHexNum(2, 8, ArrayRead[1], 4);
}
}
- 实时时钟接线图
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int 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位的秒计数器
OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器
}
}
My_RTC.c
#include "stm32f10x.h" // Device header
#include <time.h>
uint16_t MyRTC_Time[] = {2023, 1, 1, 23, 59, 55}; //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒
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时钟
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); //等待LSE准备就绪
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //选择RTCCLK来源为LSE
RCC_RTCCLKCmd(ENABLE); //RTCCLK使能
RTC_WaitForSynchro(); //等待同步
RTC_WaitForLastTask(); //等待上一次操作完成
RTC_SetPrescaler(32768 - 1); //设置RTC预分频器,预分频后的计数频率为1Hz
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;
}
PWR电源控制
在电子设备中,待机(Standby)和睡眠(Sleep)是两种不同的省电模式。 1. 待机模式(Standby Mode):在待机模式下,设备仍然保持一定程度的活动,但大部分功能处于暂停状态。
电源
STM32的工作电压(VDD)为2.0~3.6V。通过内置的电压调节器提供所需的1.8V电源
当主电源VDD掉电后,通过VBAT脚为实时时钟(RTC)和备份寄存器提供电源
这张图展示了STM32微控制器(MCU)内部的电源分配方案。整个系统可分为三大主要供电区域:模拟部分供电(VDDA)、数字部分供电包括VDD供电区域和1.8v供电区域、后备供电(VBAT)。
-
首先来看模拟部分供电,即VDDA区域,负责为模拟功能如AD转换器、温度传感器、复位模块及PLL锁相环提供电力。这些组件的正极连接至VDDA,而负极则连接至VSSA。特别地,AD转换器的参考电压输入端VREF+和VREF-通常会在在引脚多的型号里会单独引出来;而在像C8T6这样的少引脚型号中,它们已在芯片内部直接连接到VDDA和VSSA上。
-
接下来是数字部分供电,该部分分为两个子区域:VDD供电区和1.8V供电区。VDD供电区包含了I/O电路、待机电路、唤醒逻辑和独立看门狗等功能。这部分电路的工作电压通常是3.3V。为了提高能效,STM32的设计采用了低压策略,因此大部分关键的内部电路,例如CPU、存储器和数字外设,实际上是以1.8V的较低电压运行。
关于1.8V供电区,它是由VDD通过内置的电压调节器降压得到的,提供给CPU核心、存储器和内置数字外设。当这些外设需要与外界进行交流时,才会通过I/O电路转换到3.3V。这种设计有助于显著降低系统的功耗,因为较低的电压意味着更低的功率消耗。
需要注意的是,STM32的工作电压(VDD)范围为2.0~3.6V,而1.8V电源是通过内置的电压调节器提供的。
- 最后讨论的是后备供电区域。此区域包括了LSE 32K晶体振荡器、后备寄存器、RCC BDCR寄存器和实时时钟(RTC)。RCC BDCR是RTC的控制寄存器之一,也属于后备区域的一部分,因此同样可以通过VBAT供电。此外,图中还显示了一个低电压检测器,它可以监测主电源VDD的状态,并在VDD失效时自动切换到VBAT供电模式,确保RTC和其他关键的后备功能即使在主电源断开的情况下也能继续工作。
电源管理器
上电复位和掉电复位,还有可编程电压监测器这两个内容了解即可。
上电复位和掉电复位
上电复位和掉电复位的功能在于,当VDD或VDDA的电压降至一定水平时,内部电路会自动触发复位操作,防止STM32在电压不稳定时进行错误操作。为此,系统设置了一个40毫伏的迟滞电压,以避免电压波动导致的不稳定。具体来说,当电压超过上限(POR阈值,即1.92V)时,系统解除复位状态;而当电压低于下限(PDR阈值,即1.88V)时,系统进入复位状态。这种设计采用了迟滞比较器,通过设定上下两个阈值,有效防止了电压在阈值附近波动时引起的输出抖动。
需要注意的是,复位信号是低电平有效,意味着在电压过低(前后两种情况)时,系统会进入复位状态,而在电压正常(中间状态)时,系统则不会复位。关于具体的电压阈值和复位滞后时间,可以参考STM32的数据手册,具体信息在5.3.3节“内嵌复位和电源控制模块特性”中有详细说明。根据数据手册,PDR的典型下限阈值是1.88V,而POR的典型上限阈值是1.92V,这40毫伏的迟滞阈值确保了电压稳定。简而言之,电压大于1.9V时系统上电,低于1.9V时系统掉电。此外,复位持续时间(TRSTTEMPO)的典型值为2.5毫秒。
这个复位持续时间很重要,实际产品这里经常出偶发问题又难以排查
可编程电压监测器
PVD的工作原理与前述的上电复位和掉电复位类似,都是监测VDD和VDDA的供电电压。然而,PVD的独特之处在于其阈值电压是可以编程设定的,提供了更大的灵活性。
根据数据手册中的相关表格,可以通过配置PLS寄存器的3个位来选择PVD的阈值。这些阈值的选择范围大约在2.2V到2.9V之间,且PVD的上限和下限阈值之间的迟滞电压为100毫伏。值得注意的是,PVD的监测电压范围高于上电和掉电复位的阈值。
为了更直观地理解,可以想象一个电压从3.3V的正常供电逐渐降低的情景。当电压降至2.9V至2.2V之间时,就进入了PVD的监测范围。在这个范围内,您可以设置一个警告线,以便在电压进一步降低至1.9V以下,即复位电路的检测范围时,系统可以采取行动。
当PVD被触发时,微控制器仍然可以正常工作,但这是对用户的一个提醒,表明电源电压已经过低。PVD的输出是正逻辑,即电压过低时输出为1,电压正常时输出为0。这个信号可以用来申请中断,在电压上升或下降时触发,从而提醒程序进行相应的处理。
关于PVD的中断申请,它通过外部中断实现。在EXTI(外部中断)的基本结构图中,可以看到PVD的输出信号是如何接入的。因此,如果想要使用PVD,记得正确配置外部中断。
另外,尽管RTC有自己的中断系统,但为了在低功耗模式下唤醒停止模式,只有外部中断能够实现这一点。这也是为什么其他设备,如USB和ETH的唤醒信号,也要通过外部中断来唤醒停止模式。理解这一点对于低功耗设计至关重要。
低功耗模式
STM32F10xxx有三种低功耗模式:
睡眠模式
要进入睡眠模式,只需直接调用WFI(等待中断)或WFE(等待事件)指令,这两个都是内核指令,对应的库函数中也提供了相应的调用方法。WFI的作用是让CPU进入睡眠状态,直到有中断发生时唤醒;而WFE则是让CPU等待一个特定的事件来唤醒。唤醒条件分别是:WFI模式下,任何中断都能唤醒CPU;WFE模式下,则需要一个特定的事件来唤醒。唤醒后,WFI通常需要处理中断服务函数,而WFE则可能直接继续执行。
睡眠模式对电路的影响有限,主要表现在关闭了CPU时钟,而其他时钟和ADC时钟不受影响。电压调节器保持开启状态,因此,睡眠模式主要是通过停止CPU时钟来降低功耗。关闭时钟意味着所有的运算和时序操作暂停,但寄存器和存储器中的数据得以保持。睡眠模式的唤醒条件相对宽松,任何中断都能唤醒CPU,因此,它相当于是在保持身体其他部分工作的情况下,大脑稍作休息,省电程度评为一般。
停机模式
要进入停机模式,首先需要将sleepdeep位设置为1,指示CPU进入深度睡眠。PDDS位用于区分停机模式或待机模式,PDDS为0时进入停机模式。之后LPDS位用于控制电压调节器是保持开启还是进入低功耗模式(RPDS等于电压调节器开启,RPDS等于1电压调节器进入低功耗v)。
设置好这些位后,调用WFI或WFE即可进入停机模式。停机模式的唤醒条件比睡眠模式苛刻,只有外部中断能够唤醒。这意味着,如PVD、RTC闹钟、USB唤醒、ETH唤醒等通过外部中断的信号可以唤醒系统。停机模式关闭了所有1.8伏区域的时钟,包括HSI和HSE振荡器,但LSI和LSE振荡器保持运行。电压调节器可以选择开启或低功耗模式,后者更省电但唤醒时间更长。停机模式相当于整个人的工作完全停止,只有外部中断才能唤醒,省电程度评为非常省电。
注意:
系统从停止模式被中断或唤醒事件唤醒时,HSI(内部高速时钟)会被自动选为系统时钟。这是因为,在我们的程序中,默认在SystemInit函数里配置的是使用HSE(外部高速时钟),并通过PLL(锁相环)倍频来获得72MHz的主频。然而,一旦进入停止模式,PLL和HSE都会停止工作。
因此,当系统从停止模式唤醒时,它不会自动通过PLL倍频来恢复到原来的72MHz主频,而是直接使用HSI的8MHz作为主频。如果忽略这一点,就可能会出现以下现象:程序刚上电时运行在72MHz的主频,但进入停止模式并在唤醒之后,主频会降为8MHz。不止慢9倍这么简单,带时序的外设基本上都出问题。
为了避免这种情况,我们通常需要在停止模式唤醒后的第一时间重新启动HSE,并将主频重新配置为72MHz。这一操作并不复杂,因为相关的配置函数已经为我们准备好了。我们只需要在唤醒后调用SystemInit函数,即可完成主频的重新配置。这样,系统就能恢复到停止模式之前的工作状态,确保程序的正常运行。
待机模式
进入待机模式的步骤与停机模式相似,但PDDS位需设置为1。待机模式的唤醒条件最为严格,普通外设中断和外部中断都无法唤醒,只有特定的信号,如wake up引脚的上升沿、RTC闹钟事件、NRST引脚的外部复位和IWDG独立看门狗复位,才能唤醒。待机模式几乎关闭了所有电路,包括1.8伏区域的时钟和电压调节器,这意味着内部存储器和寄存器的数据会丢失。但与停机模式一样,LSI和LSE振荡器保持运行以支持RTC和独立看门狗。待机模式相当于彻底下班,除非有紧急事项,否则不会返回工作,省电程度评为极为省电。
在之前的讨论中,我们提到了多个与低功耗模式相关的寄存器位,这些模式还有一些更细致的划分。例如,睡眠模式中有SLEEP-NOW和SLEEP-ON-EXIT的区别,停机模式中则有电压调节器开启与低功耗模式的区别。了解如何配置这些模式对我们理解程序有很大帮助。
首先这里有一句,执行WFI等待中断或者WFE等待事件指令后,STM32进入低功耗模式,就说这两个指令是最终开启低功耗模式的触发条件,配置其他的寄存器都要在这两个指令之前 。
以下是基于配置流程的详细说明:
- 执行WFI或WFE指令:这两个指令是启动低功耗模式的触发点。在执行这两个指令之前,需要配置好相关的寄存器。
- 判断sleep deep位:这一位决定了是进入浅睡眠还是深度睡眠模式。
- 如果sleep deep位为0,则进入睡眠模式。
- 如果sleep deep位为1,则进入深度睡眠模式,即停机模式或待机模式。
- 睡眠模式的细分:在睡眠模式下,SLEEPONEXIT位可以进一步细分模式。
- 当SLEEPONEXIT位为0时,执行WFI或WFE后立即进入睡眠模式。
- 当SLEEPONEXIT位为1时,执行WFI或WFE后会等待当前中断处理完成后才进入睡眠模式。这种情况适用于中断处理中还有一些紧急任务需要完成。
- 深度睡眠模式的判断:对于深度睡眠模式,需要进一步判断PDDS位。
- 如果PDDS位为0,则进入停机模式。
- 如果PDDS位为1,则进入待机模式。
- 停机模式的进一步配置:在停机模式下,LPDS位决定了电压调节器的工作状态。
- 如果LPDS位为0,则电压调节器保持开启状态。
- 如果LPDS位为1,则电压调节器进入低功耗模式,虽然更省电,但唤醒延迟更长。
代码实战:修改主频&睡眠模式&停止模式&待机模式
- 修改主频
最好不要去中途改主频,因为那些外设初始化时都是根据SystemCoreClock来的,就运行一次,主频改完后得重新配置外设初始化那些
- 睡眠模式+串口发送+接收
main.c
#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之后程序在暂停的地方继续运行
}
}
- 停止模式+对射式红外传感器计次
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "CountSensor.h"
int 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,所以唤醒之后的程序运行就会明显变慢
}
}
- 待机模式+实时时钟
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int 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; //闹钟为唤醒后当前时间的后10s
RTC_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循环去掉也是可以的
}
}
WDR看门狗
STM32有两个看门狗,一个是独立看门狗另外一个是窗口看门狗,独立看门狗号称宠物狗,窗口看门狗号称警犬,本章我们主要分析独立看门狗的功能框图和它的应用。
独立看门狗
独立看门狗用通俗一点的话来解释就是一个12位的递减计数器,当计数器的值从某个值一直减到0的时候,系统就会产生一个复位信号,即IWDG_RESET。 如果在计数没减到0之前,刷新了计数器的值的话,那么就不会产生复位信号,这个动作就是我们经常说的喂狗。看门狗功能由 VDD 电压域供电, 在停止模式和待机模式下仍能工作。
在键寄存器(IWDG_KR)中写入0xCCCC,开始启用独立看门狗;此时计数器开始从其复位值0xFFF递减计数。当计数器计数到末尾0x000时,会产生一个复位信号(IWDG_RESET)。
无论何时,只要在键寄存器IWDG_KR中写入0xAAAA, IWDG_RLR中的值就会被重新加载到计数器,从而避免产生看门狗复位 。
IWDG功能框图剖析
1. 独立看门狗时钟
独立看门狗的时钟由独立的RC振荡器LSI提供,即使主时钟发生故障它仍然有效,非常独立。LSI的频率一般在30~60KHZ之间, 根据温度和工作场合会有一定的漂移,我们一般取40KHZ,所以独立看门狗的定时时间并不一定非常精确,只适用于对时间精度要求比较低的场合。
2. 计数器时钟
递减计数器的时钟由LSI经过一个8位的预分频器得到,我们可以操作预分频器寄存器IWDG_PR来设置分频因子, 分频因子可以是:[4,8,16,32,64,128,256,256]。
3. 计数器
独立看门狗的计数器是一个12位的递减计数器,最大值为0XFFF,当计数器减到0时,会产生一个复位信号:IWDG_RESET, 让程序重新启动运行,如果在计数器减到0之前刷新了计数器的值的话,就不会产生复位信号,重新刷新计数器值的这个动作我们俗称喂狗
。
4. 重装载寄存器
重装载寄存器是一个12位的寄存器,里面装着要刷新到计数器的值,这个值的大小决定着独立看门狗的溢出时间。
T
L
S
I
=
1
/
F
L
S
I
=
0.025
m
s
T _{LSI}=1/F_{LSI}=0.025ms
TLSI=1/FLSI=0.025ms
T
I
W
D
G
=
0.025
m
s
×
P
R
预分频系数
×
(
R
L
+
1
)
T_{IWDG}= 0.025ms × PR预分频系数 × (RL + 1)
TIWDG=0.025ms×PR预分频系数×(RL+1)
RL计数目标自己定义(范围:0x0000~0xFFFF),计数器的最大值为4095。
5. 键寄存器
键寄存器IWDG_KR可以说是独立看门狗的一个控制寄存器,主要有三种控制方式,往这个寄存器写入下面三个不同的值有不同的效果。
通过写往键寄存器写0XCCCC来启动看门狗是属于软件启动的方式,一旦独立看门狗启动,它就关不掉,只有复位才能关掉。
无论何时,只要在键寄存器IWDG_KR中写入0xAAAA, IWDG_RLR中的值就会被重新加载到计数器,从而避免产生看门狗复位 。
6. 状态寄存器
状态寄存器SR只有位0:PVU和位1:RVU有效,这两位只能由硬件操作,软件操作不了。RVU:看门狗计数器重装载值更新, 硬件置1表示重装载值的更新正在进行中,更新完毕之后由硬件清0。PVU:看门狗预分频值更新,硬件置’1’指示预分频值的更新正在进行中, 当更新完成后,由硬件清0。所以只有当RVU/PVU等于0的时候才可以更新重装载寄存器/预分频寄存器。
IWDG键寄存器扩展
键寄存器实际上是一种特殊的控制寄存器,它用于触发硬件电路的特定操作。例如,执行喂狗操作时,我们通过向键寄存器写入特定的值(如0XAAAA)来完成。下面解释为什么使用键寄存器而不是简单的控制位:
键寄存器的作用:
- 使用键寄存器而非单个控制位的原因在于,键寄存器提供了一种更可靠的硬件控制方式。在可能存在干扰的环境中,如程序跑飞或受到电磁干扰,单独的控制位可能会因误操作而意外改变状态。
- 通过在整个键寄存器中写入一个特定的值来执行控制操作,可以显著降低因干扰导致的误操作风险。例如,如果键寄存器是16位的,只有当写入特定的数值0XAAAA时,才会执行喂狗操作,这样就减少了误触发喂狗操作的可能性。
写保护逻辑:
- 为了进一步增强指令的抗干扰能力,键寄存器的设计包含了写保护逻辑。这意味着执行任何写操作前,必须先写入一个指定的键值。
- 在我们的系统中,除了键寄存器,还有PR(预分频器寄存器)、SR(状态寄存器)和RLR(重装载寄存器)等其他寄存器。由于SR是只读的,不需要写保护,但PR和RLR需要防止误写操作。
- 为了保护PR和RLR,可以设置一个写保护机制。只有在键寄存器中写入特定的值(如5555)后,才能解除写保护。如果写入其他值,PR和RLR将再次受到保护,从而防止了误写操作。
- 这种设计确保了PR和RLR与键寄存器一起,得到了有效的保护,从而提高了系统的稳定性和可靠性。
通过这种方式,键寄存器不仅作为控制硬件电路的关键元素,还作为保护系统免受意外干扰的重要机制。
怎么用IWDG
独立看门狗一般用来检测和解决由程序引起的故障,比如一个程序正常运行的时间是50ms, 在运行完这个段程序之后紧接着进行喂狗,我们设置独立看门狗的定时溢出时间为60ms,比我们需要监控的程序50ms多一点, 如果超过60ms还没有喂狗,那就说明我们监控的程序出故障了,跑飞了,那么就会产生系统复位,让程序重新运行。
- 开启LSI时钟:
虽然独立看门狗的时钟源是LSI(低速内部时钟),但这一步通常不需要手动操作。根据手册6.2.9的说明,一旦独立看门狗被使能,LSI时钟会自动开启,并且稳定后自动为IWDG提供时钟。因此,这一步在代码中通常不需要显式地开启LSI时钟。 - 解除写保护:
在写入预分频器和重装载寄存器之前,必须先解除写保护。这通过写入特定的键值0X5555到IWDG_KR(键寄存器)来完成。
- 写入预分频器和重装载值:
解除写保护后,可以写入预分频器值到IWDG_PR(预分频器寄存器)和重装载值到IWDG_RLR(重装载寄存器)。
- 启动独立看门狗:
配置完预分频器和重装载值后,通过向IWDG_KR写入0xCCCC来启动独立看门狗。这一步将使能看门狗并开始计数。
- 喂狗:
在主循环中,为了防止看门狗超时导致系统复位,需要定期向IWDG_KR写入0xAAAA来刷新计数器。这被称为“喂狗”。
喂狗和使能的时候会在键寄存器写入0x5555之外的值,这时就顺便给寄存器写保护了,所以写完寄存器器后我们不用手动执行写保护了。
核心示例代码:
窗口独立狗
主要特性
- 可编程的自由运行递减计数器
- 条件复位
- 当递减计数器的值小于0x40,(若看门狗被启动)则产生复位。
- 当递减计数器在窗口外被重新装载,(若看门狗被启动)则产生复位。
- 如果启动了看门狗并且允许中断,当递减计数器等于0x40时产生早期唤醒中断(EWI),它可以被用于重装载计数器以避免WWDG复位
功能描述
窗口看门狗的设计和操作流程与独立看门狗有显著差异,这可能是由于它们的设计理念和应用重点不同。以下是窗口看门狗的关键组成部分和工作流程:
时钟源和预分频器:
- 窗口看门狗的时钟源是PCLK1,通常是APB1的时钟,默认频率为36MHz。
- 时钟源右侧是预分频器,称为WDGTB。它与独立看门狗的PR和定时器的PSC功能相同,用于调整计数器的时钟频率,从而影响计数器溢出时间。
6位递减计数器CNT:
- 计数器CNT位于控制寄存器CR内,它是一个6位递减计数器,尽管标记为T6到T0共七个位,但只有T5到T0这六位是有效的计数值。
- 最高位T6用作溢出标志位。当T6位为1时,表示计数器未溢出;当T6位为0时,表示计数器已溢出,这时会触发系统复位。
窗口看门狗(WWDG)的6位递减计数器CNT的工作原理如下:
- 初始化:
- 在窗口看门狗被启用之前,必须首先设置计数器的初始值。这个值通常是一个大于0x40的数(因为0x40是计数器的最小可设置值),以确保计数器在开始递减之前有足够的计数周期。
时钟源: - 计数器CNT的时钟源通常是PCLK1(APB1时钟),这个时钟源经过预分频器(WDGTB)分频后,提供给CNT作为计数脉冲。
- 预分频器(WDGTB):
- 预分频器用于调整计数器的递减速率。它可以设置不同的分频系数,以改变计数器溢出的时间。
- 递减计数:
- 一旦窗口看门狗被启用,并且计数器的初始值被设置,计数器CNT开始在每个时钟周期递减。由于CNT是6位递减计数器,它的最大值为0x3F(二进制111111)。
- 溢出标志(T6位):
- 计数器的最高位T6用作溢出标志。当计数器的值从0x40(二进制1000000)递减到0x3F(二进制0111111)时,T6位从1变为0,这表示计数器溢出。
- 喂狗操作:
- 为了防止计数器溢出导致系统复位,必须在计数器值小于窗口值时更新计数器的值。这个过程称为“喂狗”。通过向控制寄存器CR写入一个新的计数值来更新CNT。
- 复位条件:
- 如果计数器的值递减到0x3F并且T6位变为0,而没有及时“喂狗”,窗口看门狗会产生一个复位信号,导致系统复位。
- 如果在窗口期内(即在计数器的值大于窗口值时)“喂狗”,也会产生复位,因为这是不允许的操作。
喂狗操作:
- 窗口看门狗没有单独的重装寄存器。要“喂狗”,即重置计数器,可以直接向CNT写入一个值。
- 计数器的窗口值W6到W0用于设置喂狗的最早时间界限。这个值一旦设置,就不会改变。
复位信号输出逻辑:
- WDGA是窗口看门狗的激活位,写入1以启用窗口看门狗。
- 当T6位为0时,表示计数器溢出,通过或门产生复位信号。在正常运行状态下,需要保持T6位为1以避免复位。
- 如果不及时“喂狗”,计数器减到0后会产生复位。
喂狗时间窗口的实现流程:
- 在“喂狗”时,系统会比较当前计数器的值和预设的窗口值。
- 如果当前计数器的值大于窗口值,表示“喂狗”得太早,比较器输出1,通过或门可以申请复位。
- 这确保了只有在特定的时间窗口内“喂狗”才是有效的,从而提高了系统的安全性。
递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位
以下是这个过程的具体步骤和用途:
- 早期唤醒中断(EWI)触发:
- 当计数器的值递减到0x40时,如果启用了EWI功能,则会触发一个中断。这个中断是在计数器溢出(即T6位从1变为0)之前触发的,因此它提供了一个“最后的机会”来执行一些紧急操作。
- 中断服务例程(ISR):
- 在中断服务例程中,可以执行以下操作:
- 保存重要数据: 如果系统中有正在处理的重要数据,可以在中断中将其保存到非易失性存储器中。
- 关闭危险设备: 如果系统控制着某些可能造成伤害或损坏的设备,可以在中断中安全地关闭这些设备。
- 执行紧急任务: 可以执行一些紧急的任务,以防止系统因超时而复位。
- 喂狗操作:
- 在中断服务例程中,可以选择重新加载计数器的值(即“喂狗”),以防止系统复位。这样做可以允许系统继续运行,即使它已经接近了超时阈值。
- 如果超时不是非常严重的问题,或者在某些情况下,系统可以容忍轻微的超时,可以在中断中执行喂狗操作,并可能只是记录一个错误或者向用户显示一个警告信息,而不是让系统复位。
- 防止系统复位:
- 通过在中断中执行喂狗操作,可以防止系统复位,这对于那些不希望因看门狗超时而重启的系统来说非常有用。
WWDG超时时间
这里要多乘一个4096,是因为这里PCLK1进来之后,其实是先执行了一个固定的4096分频,这里框图没画出来,实际上是有的,因为36M的频率还是太快了,先来个固定分频给降一降。
IWDG和WWDG对比
代码实战:独立看门狗&窗口看门狗
- 独立看门狗
main.c
#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);
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,独立看门狗的超时时间为1000ms
IWDG_ReloadCounter(); //重装计数器,喂狗
IWDG_Enable(); //独立看门狗使能
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);
}
}
- 窗口看门狗
- 开启窗口看门狗的APB1时钟:
由于窗口看门狗的时钟来源是PCLK1(APB1时钟),因此第一步是开启APB1时钟。这与独立看门狗不同,后者使用LSI时钟,会自动开启。开启APB1时钟通常通过RCC(Reset and Clock Control)寄存器来完成。 - 配置寄存器:
第二步是配置窗口看门狗的各个寄存器,包括预分频器和窗口值。窗口看门狗没有写保护机制,因此可以直接写入这些寄存器。
预分频器(WWDG_CFR的WDGTB位)用于设置计数器的分频系数,窗口值(WWDG_CFR的W位)用于设置喂狗的时间窗口。 - 写入控制寄存器(CR):
第三步是写入控制寄存器(WWDG_CR),这个寄存器包含了看门狗使能位(WDGA)、计数器溢出标志位(WDGON)和计数器有效位(T6)等。这些位需要一起设置,以使能窗口看门狗并开始计数。 - 喂狗:
在系统运行过程中,需要定期向计数器写入新的重装载值来喂狗。这通过向WWDG_CR写入特定的值来完成,同时这个操作也必须在设定的时间窗口内执行,以避免系统复位。
main.c
#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的时钟 PCLK1时钟
/*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); //重装计数器,喂狗
}
}
FLASH闪存
介绍
读写FLASH的用途:
- 存储用户数据:
在C8T6芯片中,可以利用程序存储器的剩余空间来保存掉电不丢失的用户数据。选择存储区域时,应避免覆盖原有程序代码。
对于我们这个C8T6芯片来说,它的程序存储器容量是64K,一般我们写个简单的程序,可能就只占前面的很小一部分空间,剩下的大片空余空间我们就可以加以利用,比如存储一些我们自定义的数据,这样就非常方便,而且可以充分利用资源,不过这里要注意我们在选取存储区域时,一定不要覆盖了原有的程序,要不然程序自己把自己给破坏了,
一般存储少量的参数,我们就选最后几页存储就行了
- 程序自我更新(IAP):
通过在应用程序中编程,可以实现程序的自我更新。这涉及到编写一个BOOTLOADER程序,并将其存放在程序更新时不会覆盖的地方。
闪存模块组织
对于小容量产品和大容量产品,闪存的分配方式有些区别,这个可以参考一下手册,那首先提醒一下闪存这一章的内容在手册里是单独列出来的,并不在之前的参考手册里,我们需要打开这个闪存编程参考手册,这里以中容量产品为例来讲解。
在闪存编程参考手册中,我们可以看到C8T6的闪存分为三个主要部分:主存储器、信息块和闪存存储器接口寄存器。
- 主存储器:这是闪存中容量最大的部分,用于存放程序代码。主存储器被划分为多个页,每页大小为1K。C8T6共有64页。
- 信息块:这部分包含系统存储器和用户选择字节(也称为选项字节)。系统存储器的起始地址是0x1FFFF000,容量为2K,用于存放原厂写入的BOOTLOADER,以便通过串口下载。用户选择字节的起始地址是0x1FFFF800,容量为16字节,用于存储配置参数。需要注意的是,虽然系统存储器和用户选择字节属于闪存的一部分,但它们通常不计入我们常说的64K或128K闪存容量中。
- 闪存存储器接口寄存器:这些寄存器不属于闪存本身,而是作为外设存在,其地址以0x4002开头,表明它们是普通的外设寄存器,类似于GPIO、定时器、串口等。这些寄存器包括KEYR、SR、CR等,用于控制闪存的擦除和编程过程。
对于主存储器,这里对它进行了分页,分页是为了更好的管理闪存,擦除和写保护都是以页为单位的,这点和之前W25Q64芯片的闪存一样,同为闪存它们的特性基本一样,写入前必须擦除,擦除必须以最小单位进行,擦除后数据位全变为1,数据只能1写0,不能0写1,擦除和写入之后都需要等待忙,这些都是一样的,学习这节之前,大家可以再复习一下W25Q64,再学这一节就会非常轻松了,那W25Q64的分配方式是先分为块block,再分为扇区sector比较复杂,这里就比较简单了,它只有一个基本单位就是页,每一页的大小都是1K,0到127总共128页,总量就是128K,对于C8T6来说,它只有64K,所以C8T6的页只有一半0~63总共64页共64K,
FLASH基本结构
接下来理一下这个基本结构图,整个闪存分为程序存储器
、系统存储器
和选项字节
三部分,这里程序存储器为以C8T6为例,它是64K的,所以总共只有64页,最后一页的起始地址是0800FC00;左边这里是闪存存储器接口(闪存编程和擦除控制器LPEC),然后这个控制器就是闪存的管理员,他可以对程序存储器进行擦除和编程,也可以对选项字节进行擦除和编程,系统存储器是不能擦除和编程的,这个选项字节里面有很大一部分配置位,其实是配置主程序存储器的读写保护的,所以右边画的写入选项字节,可以配置程序存储器的读写保护,当然选项字节还有几个别的配置参数,这个待会再讲,那这就是整个闪存的基本结构。
FLASH解锁
解锁过程:
- 复位后保护状态:在微控制器复位后,FPEC(Flash Programming and Erase Controller)默认是被保护的,此时不能写入FLASH_CR(Flash Control Register)。
- 写入解锁键值:要解锁FPEC,需要按照正确的顺序在FLASH_KEYR(Flash Key Register)中写入特定的键值。
- KEY1:首先写入0x45670123。
- KEY2:然后写入0xCDEF89AB。
- 安全性设计:这种两步解锁过程提高了安全性,因为需要连续写入两个正确的键值才能解锁。这减少了因程序异常而意外解锁的风险。
- 错误操作保护:如果解锁序列错误,比如没有按照先KEY1后KEY2的顺序写入,FPEC和FLASH_CR将会在下次复位前被锁死,防止了进一步的误操作。
加锁过程:
- 操作完成后加锁:在完成所有必要的闪存操作后,为了防止意外写入,需要重新锁定FPEC。
- 设置LOCK位:通过在FLASH_CR寄存器中设置LOCK位来重新锁定FPEC。通常,向LOCK位写入1即可完成加锁操作。
注意事项:
解锁和加锁操作都需要谨慎进行,以确保系统的稳定性和安全性。
在进行任何解锁操作之前,确保理解了相关寄存器的功能和操作步骤,以避免不必要的风险。
接着看下一个知识点,这个地方我们要学习的是,如何使用指针访问存储器,因为STM32内部的存储器是直接挂在总线上的,所以这时在读写某个存储器就非常简单了,直接使用C语言的指针来访问即可。
使用指针访问存储器
如果你这个地址写的是SRAM的地址,比如0X20000000,那可以直接写入了,因为SRAM在程序运行时是可读可写的,这是使用指针访问存储器的C语言代码,0X08000000,其中读取可以直接读,写入需要解锁,并且执行后面的流程。
程序存储器全擦除
下面我们来详细审视以下三个流程图的内容。首先是编程流程,亦即数据写入过程。其次是页擦除流程,值得注意的是,在STM32的闪存操作中,写入数据前需进行擦除操作。完成擦除后,该页的所有数据位将统一变为1,页擦除的操作单元是1K,即1024字节。最后是全擦除流程,这一过程涉及对所有页面的擦除。关于这些流程的细节,库函数已经为我们封装好了相应的操作,我们只需调用一个总函数即可,操作便捷
- 检查锁状态:首先,读取芯片的LOCK位,以确定芯片是否处于锁定状态。若LOCK位为1,表明芯片已被锁定,此时需要执行解锁操作。
- 解锁操作(如果需要):
如果芯片锁定(LOCK位等于1),则需在KEYR寄存器中依次写入KEY1和KEY2以执行解锁。这一步骤在流程图中有所体现,即锁定了才需要解锁。
若芯片未锁定,则无需执行解锁操作。然而,库函数的设计是直接执行解锁过程,不考虑芯片是否实际锁定。这种方法虽然简单直接,但最终效果是相同的。 - 启动全擦除:
将控制寄存器中的MER(Mass Erase)位置1,以指示全擦除操作。
接着,将STRT(Start)位置1。STRT位为1时,将触发芯片开始执行操作。当芯片检测到MER位为1时,它会识别接下来的操作是全擦除,并自动执行全擦除流程。 - 等待擦除完成:
全擦除操作需要一定时间,因此程序需等待擦除过程结束。这通过检查状态寄存器的BSY(Busy)位来实现。
如果BSY位为1,表示芯片正忙于擦除操作,程序将继续循环检查,直到BSY位变为0,表明擦除操作完成。 - 验证擦除结果(可选):
流程的最后一步是读取并验证所有页的数据。这一步骤通常用于测试程序,以确保擦除操作的成功。
在正常操作中,全擦除完成后,我们可以默认操作成功。由于全读出并验证所有页的数据工作量巨大,因此在实际应用中,这一步骤通常可以省略。
程序存储器页擦除
接下来,我们来看看页擦除的过程,这一过程与全擦除类似,包含以下步骤:
- 解锁操作:首先执行与全擦除相同的解锁流程。如果芯片处于锁定状态,需要在KEYR寄存器中依次写入KEY1和KEY2来解锁。
- 设置页擦除模式:
将控制寄存器中的PER(Page Erase)位置1,指示接下来要执行的是页擦除操作。
在AR(Address Register)地址寄存器中写入要擦除的页的起始地址。这一步是必要的,因为闪存包含多个页,而页擦除操作需要明确指出具体要擦除哪一页。 - 启动擦除操作:
将控制寄存器的STRT(Start)位置1,这是触发条件,告诉芯片开始执行擦除操作。
当芯片检测到PER位为1时,它会识别接下来的操作是页擦除,并且会参考AR寄存器中的地址来确定要擦除的具体页。 - 等待擦除完成:
擦除操作开始后,程序需要等待操作完成。这同样是通过检查状态寄存器的BSY(Busy)位来实现的。
如果BSY位为1,表示芯片正在执行擦除操作,程序将持续检查,直到BSY位变为0,表明擦除操作已经完成。 - 验证擦除结果(可选):
最后一步是读取并验证擦除页的数据。这一步骤在测试程序中可能需要执行,以确保擦除操作的成功。
在实际应用中,由于验证所有数据的工作量较大,通常可以省略这一步骤,假设擦除操作已经成功完成。
总结来说,页擦除过程包括解锁、设置擦除模式、启动擦除、等待操作完成,以及可选的数据验证步骤。通过这些步骤,我们可以确保指定的页被正确擦除。
程序存储器编程
最后,我们来探讨闪存的写入流程。在擦除操作之后,我们就可以进行数据写入。以下是写入流程的详细步骤:
- 解锁操作:与擦除操作类似,写入流程的第一步是对闪存进行解锁,确保可以执行写入操作。
- 设置编程模式:
将控制寄存器中的PG(Programming)位置1,这表示即将进行数据写入操作。 - 写入数据:
在指定的地址写入半字(16位数据)。这一步骤通过指针操作实现,可以直接在指定的内存地址写入想要的数据。
需要注意的是,STM32的闪存写入操作仅支持半字写入。在STM32中,数据单位有字(32位)、半字(16位)和字节(8位)。因此,写入时必须以半字为单位,即每次写入16位数据。如果需要写入32位数据,则需要分两次写入;而写入8位数据时,则需要额外的处理。
处理字节写入:
如果需要单独写入一个字节且保留另一个字节的原始数据,必须将整页数据读取到SRAM中,修改SRAM中的数据,然后擦除整页闪存,并将修改后的整页数据写回。这种方法虽然繁琐,但能实现类似SRAM的灵活读写。
- 触发写入操作:
写入数据后,芯片将自动进入忙状态,开始执行写入操作。与擦除操作不同,写入操作不需要显式设置STRT位,写入半字即可触发。
等待写入完成:
写入过程中,程序需要等待状态寄存器的BSY(Busy)位清0,这表示写入操作已经完成。 - 重复写入流程:
每次执行上述流程,只能写入一个半字。若需要写入大量数据,则需要循环调用写入流程,直到所有数据写入完成。
总结来说,闪存的写入流程包括解锁、设置编程模式、写入数据、等待写入完成,并根据需要重复写入流程以写入多个数据。STM32的闪存写入有一定的限制,需要按照半字单位进行,并且在特定情况下需要采取额外的步骤来保证数据的正确写入。
选项字节
现在,让我们进一步了解选项字节的相关内容。对此有一个基本的认识就足够了。首先,我们要关注的是选项字节的结构和它们的作用。
在图表中,可以看到选项字节的起始地址
,即我们之前提到的0x1FFF8000。这一区域包含的数据,正如表格所示,总共只有16个字节。这些字节在图中被详细展示,它们中的每一个都有一个对应的名称。值得注意的是,其中一半的名称带有“N”前缀,例如RDP和nRDP,USER和nUSER。这表示在写入数据到RDP等存储器时,必须同时在对应的nRDP存储器中写入数据的反码。这样的操作确保了写入的有效性。如果芯片检测到这些存储器中的数据不是反码关系,那么数据将被视为无效,相关的功能也不会被执行。这是一种安全特性,旨在防止错误操作。幸运的是,硬件会自动处理反码的写入过程,因此在使用库函数时,我们只需直接调用相应的函数即可,无需手动干预。
接下来看看这些存储器的具体功能
。排除带有“N”前缀的字节后,我们剩下八个字节存储器。首先是RDP(读保护配置位
),通过向RDP存储器写入特定的RDPRT键(例如0xA5),可以解除读保护。如果RDP不包含0xA5,则闪存将处于读保护状态,防止调试器读取程序代码,从而保护代码不被未授权访问。第二个字节是USER
,它包含了一些零碎的配置位,可以用来配置硬件看门狗以及停机待机模式是否产生复位等。
接着是Data0/1
这两个字节,它们在芯片中没有预设功能,用户可以根据自己的需求进行自定义。最后四个字节,WRP0/1/2/3
,用于配置写保护。在中容量产品中,每个位对应保护四个存储页,总共32位,可以保护128页,这与中容量产品的最大页数相匹配。
对于小容量和大容量产品,写保护配置有所不同。根据手册中的2.5节,小容量产品每个位同样对应保护四个存储页,但由于其最大容量只有32K,因此只需使用一个字节WRP0,即8位,足以保护32页。其他三个字节WRP1、WRP2、WRP3在此不被使用。而对于大容量产品,每个位仅能保护两个存储页,因此四个字节不足以覆盖所有页。为此,规定WRP3的最高位用于保护剩余的所有页,从而确保了写保护功能的完整性。
然后看一下如何去写入这些位呢,这里两页PPT展示的就是选项字节的擦除和编程,因为选项字节本身也是闪存,所以它也得擦除,这里参考手册并没有给流程图,我们看一下这个文字流程,这个文字流程和流程图细节上有些出入,我们知道关键部分就行。
首先,我们来探讨选项字节的擦除流程。虽然第一步在文字描述中未明确提及,但实际上,它同样是解锁闪存。接着,我们看到文字版流程中包含了额外的步骤,即检查状态寄存器(SR)的BSY位,以确保没有其他闪存操作正在进行。这一步骤实际上是一个预先等待的过程:如果检测到BSY位为忙状态,我们需要等待直到操作完成。这一步骤在先前的流程图中并未展示。
下一步是解锁控制寄存器(CR)的OPTWRE(Option Write Enable)位,这是专门针对选项字节的解锁操作。在解锁整个闪存之后,我们还需要单独解锁选项字节,才能对其进行操作。关于解锁选项字节,我们可以参考之前的寄存器组织图。整个闪存的解锁是通过KEYR寄存器完成的,而选项字节的小锁则是通过OPTKEYR(Option Key Register)寄存器来解锁。解锁这个小锁的流程如下:首先在OPTKEYR中写入KEY1,然后写入KEY2,这样就可以成功解锁选项字节。
解锁选项字节的小锁之后,接下来的步骤与之前的擦除操作类似。首先,我们需要将CR的OPTER(Option Erase)位置1,这表示我们准备擦除选项字节。然后,设置CR的STRT位为1,这一操作将触发芯片开始擦除选项字节的过程。在设置STRT位后,我们等待BUSY位变为0,这表明擦除选项字节的过程已经完成。一旦擦除操作完成,我们就可以进行后续的写入操作了。
和普通的闪存写入也差不多,先检测BSY,然后解除小锁,之后设置CR的OPTPG(Option Programming)位为1,表示即将写入选项字节,再之后写入要编程的半字到指定的地址,这个是指针写入操作,最后等待忙,这样写入选项字节就完成了。
最后我们花几分钟学一下器件电子签名,这个非常简单,既然讲到闪存了,就顺便学习一下吧
看一下电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写不可更改,使用指针读指定地址下的存储器,可获取电子签名,电子签名其实就是STM32的id号,它的存放区域是系统存储器,它不仅有BOOTLOADER程序,还有几个字节的id号,系统存储器起始地址是1FFFF000,看下这里,这里有两段数据,第一个是闪存容量存储器,基地址是1FFF F7E0,通过地址也可以确定它的位置,就是系统存储器,这个存储器的大小是16位,它的值就是闪存的容量单位是KB,然后第二个是产品唯一身份标识寄存器,就是每个芯片的身份证号,这个数据存放的基地址是1FFFF7E8,大小是96位,每一个芯片的这96位数据都是不一样的,使用这个唯一id号可以做一些加密的操作,比如你想写入一段程序,只能在指定设备运行,那也可以在程序的多处加入id号判断,如果不是指定设备的id号,就不执行程序功能,这样即使你的程序被盗,在别的设备上也难以运行,这是STM32的电子签名。
代码实战:读写内部FLASH&读取芯片 ID
建议观看视频:15-2 读写内部FLASH&读取芯片 ID
-
读写内部FLASH
-
读取芯片 ID