stm32学习
三.通信
10.BKP和RTC
BKP(backup registers)备份寄存器
BKP可以存储数据,掉电丢失,平时用外部电源供电,外部电源切断后用VBAT维持供电,当系统在待机状态下被唤醒,或系统复位或电源复位时,它们也不会被复位
TAMPER引脚可以产生的侵入事件,将所有备份寄存器的内容清除
RTC引脚可以输出RTC校准时钟(对内部RTC微小的误差进行校准)、RTC闹钟脉冲、秒脉冲(可以输出出来,为别的设备提供信号)
PC13、TAMPER、RTC共用一个引脚,同时只能使用一个功能
存储RTC时钟校准寄存器,联合RTC校准时钟,对RTC进行校准
用户数据存储容量:20字节(中容量和小容量)/84字节(大容量和互联型)
RTC实时时钟,是一个独立的定时器,可为系统提供时钟和日历的功能
RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD断电后可借助VBAT供电
32位的可编程计数器,可对应Unix时间戳的秒计数器
20位的可编程预分频器,可适配不同频率的输入时钟
可选择的三种RTC时钟源:
- HSE时钟(除以128,因为本身8MHz,预分频不够用)
- LSE振荡器时钟(32.768kHz)
- LSI振荡器时钟(40kHz)
RTC框图:
RTC基本结构:
硬件电路:
RTC操作注意事项:
- 执行以下操作将使能对BKP和RTC的访问
- 设置RCC_APB1ENR的PWREN和BKPEN,使能PWR和BKP时钟
- 设置PWR_CR的DBP,使能对BKP和RTC的访问
- 若在读取RTC寄存器时,RTC的APB1接口曾经处于禁止状态,则软件首先必须等待RTC_CRL寄存器中的RSF位(寄存器同步标志)被硬件置1
- 必须设置RTC_CRL寄存器中的CNF位,使RTC进入配置模式后,才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器
- 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。仅当RTOFF状态位是1时,才可以写入RTC寄存器
读写备份寄存器的初始化只需要
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); //开启PWR的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); //开启BKP的时钟
/*备份寄存器访问使能*/
PWR_BackupAccessCmd(ENABLE); //使用PWR开启对备份寄存器的访问
然后就可以用BKP_ReadBackupRegister和BKP_WriteBackupRegister来读写寄存器
RTC初始化和RTC读写时间的函数:
#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;
}
然后在main.c的while里面调用MyRTC_ReadTime就能读取实时时钟了
11.PWR
PWR电源控制:PWR负责管理stm32内部的电源供电部分,可以实现可编程电压监测器和低功耗模式的功能
可编程电压监测器(PVD)可以监控VDD电源电压,当VDD下降到PVD阈值以下或者之上时,PVD会触发中断,用于执行紧急关闭任务
低功耗功能包括睡眠模式(sleep)、停机模式(stop)、和待机模式(standby),可在系统空闲时,降低stm32的功耗,延长设备的使用时间,睡眠模式一般省电,停机模式比较省电,待机模式极为省电
电源框图:
上电复位和掉电复位的波形图:
PVD的门限:
低功耗模式一览:
模式细分:
睡眠模式:
- 执行完WFI/WFE指令后,stm32进入睡眠模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- SLEEPONEXIT位决定stm32执行完WFI或WFE后,是立刻进入睡眠,还是等stm32从最低优先级的中断处理程序中退出时进入睡眠
- 在睡眠模式下,所有的I/O引脚都保持它们在运行模式时的状态
- WFI指令进入睡眠模式,可被任何一个NVIC响应的中断唤醒
- WFE指令进入睡眠模式,可被唤醒事件唤醒
停止模式:
- 执行完WFI/WFE指令后,stm32进入停止模式,程序暂停运行,唤醒后程序从暂停的地方继续运行
- 1.8v供电区域的所有时钟都被停止,PLL、HSI和HSE被禁止,SRAM和寄存器内容被保留下来
- 在停止模式下,所有的I/O引脚都保持它们在运行模式时的状态
- 当一个中断或唤醒事件导致退出停止模式时,HSI被选为系统时钟
- 当电压调节器处于低功耗模式下,系统从停止模式退出,会有一段额外的启动延时
- WFI指令进入停止模式,可被任何一个EXTI中断唤醒
- WFE指令进入停止模式,可被任何一个EXTI事件唤醒
待机模式:
- 执行完WFI/WFE指令后,stm32进入待机模式,唤醒后程序从头开始运行
- 整个1.8v供电区域被断电,PLL、HSI和HSE也被断电,SRAM和寄存器内容丢失,只有备份的寄存器和待机电路维持供电
- 在待机模式下,所有的I/O引脚变为高阻态(浮空输入)
- WKUP引脚的上升沿、RTC闹钟事件的上升沿、NRST引脚上外部复位、IWDG复位退出待机模式
代码:
修改主频:
(在system_stm32f10x.c中将其它的注释掉就行)
/* #define SYSCLK_FREQ_HSE HSE_VALUE */
/* #define SYSCLK_FREQ_24MHz 24000000 */
#define SYSCLK_FREQ_36MHz 36000000
/* #define SYSCLK_FREQ_48MHz 48000000 */
/* #define SYSCLK_FREQ_56MHz 56000000 */
/* #define SYSCLK_FREQ_72MHz 72000000 */
库函数中已经宏定义了SystemCoreClock:时钟频率
启动睡眠模式:
(串口发送接收会打断睡眠模式)
__WFI(); //执行WFI指令,CPU睡眠,并等待中断唤醒,WFI的内核是汇编代码
//__WFE(); //中断事件,配置比WFI难,所以一般用上面的
启动停止模式:
(烧录前需要按住复位键,烧录完之后松开,外部中断能中断停止模式)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
//开启PWR的时钟
//停止模式和待机模式一定要记得开启
PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI);
//STM32进入停止模式,并等待中断唤醒
SystemInit(); //唤醒后,要重新配置时钟
待机模式:
(同样要按住复位键)
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
//开启PWR的时钟
//停止模式和待机模式一定要记得开启
PWR_EnterSTANDBYMode();
//STM32进入停止模式,并等待指定的唤醒事件(WKUP上升沿或RTC闹钟)
四.看门狗
1.独立看门狗
WDG(Watchdog)看门狗:看门狗可以监控程序的运行状态,当程序因为设计漏洞、硬件故障、电磁干扰等原因,出现卡死或者跑飞现象时,看门狗能及时复位程序,避免程序陷入长时间的罢工状态,保证系统的可靠性和安全性
看门狗本质上是一个定时器,当指定时间范围内,程序没有执行喂狗(重置计数器)操作时,看门狗硬件电路就自动产生复位信号
stm32内置两个看门狗:
独立看门狗(IWDG):独立工作,对时间精度要求较低
窗口看门狗(WWDG):要求看门狗在精确计时窗口起作用
IWDG框图:
IWDG键寄存器:键寄存器本质上是控制寄存器,用于控制硬件电路的工作
在可能存在干扰的情况下,一般通过在整个键寄存器写入特定值来代替控制寄存器写入一位的功能,以降低硬件电路收到干扰的概率
超时时间: T I W D G = T L S I × P R 预分频系数 × ( R L + 1 ) T_{IWDG} = T_{LSI} \times PR预分频系数 \times (RL + 1) TIWDG=TLSI×PR预分频系数×(RL+1)
其中: T L S I = 1 F L S I T_{LSI} = \frac{1}{F_{LSI}} TLSI=FLSI1
2.窗口看门狗

WWDG工作特性:
- 递减计数器T[6:0]的值小于0x40时,WWDG产生复位
- 递减计数器T[6:0]在窗口W[6:0]外被重新装载时,WWDG产生复位(不能过早喂狗)
- 递减计数器T[6:0]等于0x40时可以产生早期唤醒中断(EWI),用于重装载计数器以避免WWDG复位(可以保存重要数据等)
- 定期写入WWDG_CR寄存器(喂狗)以避免WWDG复位

WWDG超时时间:
超时时间: T W W D G = T P C L K 1 × 4096 × W D G T B 预分频系数 × ( T [ 5 : 0 ] + 1 ) T_{WWDG} = T_{PCLK1} \times 4096 \times WDGTB预分频系数 \times (T[5:0] + 1) TWWDG=TPCLK1×4096×WDGTB预分频系数×(T[5:0]+1)
窗口时间: T W I N = T P C L K 1 × 4096 × W D G T B 预分频系数 × ( T [ 5 : 0 ] − W [ 5 : 0 ] ) T_{WIN} = T_{PCLK1} \times 4096 \times WDGTB预分频系数 \times (T[5:0] - W[5:0]) TWIN=TPCLK1×4096×WDGTB预分频系数×(T[5:0]−W[5:0])
其中: T P C L K 1 = 1 F P C L K 1 T_{PCLK1} = \frac{1}{F_{PCLK1}} TPCLK1=FPCLK11
IWDG和WWDG对比:
3.独立看门狗的代码
看门狗的代码较为简单,所以就直接放在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);
}
}
4.窗口看门狗
窗口看门狗的代码与独立看门狗的代码相似,最大区别是窗口看门狗需要使能APB1的WWDG时钟
// 省略的代码与独立看门狗相同
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); //开启WWDG的时钟
/*WWDG初始化*/
WWDG_SetPrescaler(WWDG_Prescaler_8); //设置预分频为8
WWDG_SetWindowValue(0x40 | 21); //设置窗口值,窗口时间为30ms
WWDG_Enable(0x40 | 54); //使能并第一次喂狗,超时时间为50ms
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); //重装计数器,喂狗
}
五.闪存
1.内部flash
stm32f1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分,通过闪存存储器接口(外设)可以对程序存储器和选项字节进行擦除和编程
读写FLASH的用途:
- 利用程序存储器的剩余空间来保存掉电不丢失的用户数据
- 通过在程序中编程(IAP),实现程序的自我更新
在线编程(ICP):用于更新程序存储器的全部内容,它通过JTAG、SWD协议或系统加载程序(Bootloader)下载程序
在程序中编程(IAP)可以使用微控制器支持的任一种通信接口下载程序
闪存模块组织:
FLASH基本结构:
FLASH解锁:
FPEC共有三个键值:
- RDPRT = 0x00000045
- KEY1 = 0x45670123
- KEY2 = 0xCDEF89AB
解锁:
- 复位后,FPEC被保护,不能写入FLASH_CR
- 在FLASH_KEYR先写入KEY1,再写入KEY2,解锁
- 错误的操作序列会在下次复位前锁死FPEC和FLASH_CR
加锁:设置FLASH_CR中的LOCK位锁住FPEC和FLASH_CR
使用指针访问存储器:
#define __IO volatile
//volatile的作用是防止编译器优化
uint16_t Data = *((__IO uint16_t *)(0x08000000));
使用指针写指定地址下的存储器:
*((__IO uint16_t *)(0x08000000)) = 0x1234;
程序存储器编程:
程序存储器页擦除:
程序存储器全擦除:
选项字节编程:
- 检查FLASH_SR的BSY位,以确认没有其它正在进行的编程操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTPG位为1
- 写入要编程的半字到指定的地址
- 等待BSY位变为0
- 读出写入的地址并验证数据
选项字节擦除:
- 检查FLASH_SR的BSY位,以确认没有其它正在进行的闪存操作
- 解锁FLASH_CR的OPTWRE位
- 设置FLASH_CR的OPTER位为1
- 设置FLASH_CR的STAT位为1
- 等待BSY位变为0
- 读出被擦除的选择字节并做验证
器件电子签名:电子签名存放在闪存存储器模块的系统存储区域,包含的芯片识别信息在出厂时编写,不可更改,使用指针读指定地址下的存储器可获取电子签名
闪存容量存储器:基地址:0x1FFF F7E0,大小:16位
产品唯一身份标识寄存器:基地址:0x1FFF F7E8,大小:96位