闹钟唤醒待机模式

DAY14

01_复习回顾:电源管理与实时时钟知识梳理

在嵌入式开发的扩展篇学习中,我们接触了许多底层系统级别的管理知识。这些内容对于深入理解嵌入式系统的运行机制、优化系统性能以及实现特定功能至关重要。接下来,我们将详细回顾这些知识点。

电源控制相关知识

电压检测

  1. 上电复位与掉电复位:当系统的电压无法维持正常工作时,系统会进入复位状态。这就好比人在体力不支时无法正常工作,需要休息调整一样。在嵌入式系统中,电压不足会导致系统不稳定,通过复位能让系统重新初始化,尝试恢复正常运行。
  2. PVD可编程电压检测:如果电压尚未低到需要复位的程度,但我们希望对电压变化进行监控并发出报警提示,这时PVD可编程电压检测就派上用场了。比如在一些对电压稳定性要求较高的设备中,通过PVD检测,当电压接近临界值时,系统可以提前采取措施,如保存重要数据,避免因电压波动导致的数据丢失。

低功耗模式

  1. 睡眠模式:睡眠模式下,只有CPU的时钟关闭,其他时钟和外设仍正常工作,就像公司里老板休息了,员工们还在照常上班。进入睡眠模式通常使用WFI(Wait For Interrupt)或WFE(Wait For Event)指令,一般采用中断方式唤醒,即使用WFI。任何一个中断都能将系统从睡眠模式唤醒,这为系统在节能的同时快速响应外部事件提供了可能。例如,在一个带有按键输入的嵌入式设备中,系统处于睡眠模式,当用户按下按键产生中断时,系统就能迅速从睡眠中唤醒,处理按键事件。
  2. 停机模式:停机模式是一种深睡眠模式,需要将sleepdeep位置1。其唤醒条件较为严格,必须是任一外部中断或内部中断。在停机模式下,芯片核心区域的所有时钟都停止,相当于芯片处于“停工停产”状态。电压调节器有正常工作模式和低功耗模式两种。正常工作模式下,所有1.8V供电区域保持供电,但芯片不工作;低功耗模式下,仅为寄存器、SRAM等核心部分供电,其他模块断电,从而实现节能。以智能手表为例,在用户长时间不操作时,进入停机模式,仅保留关键数据存储区域的供电,减少功耗。
  3. 待机模式:待机模式需要将sleepdeepPDDS位置1。其唤醒方式特殊,只有WKUP引脚PA0的上升沿、RTC闹钟事件、NRST复位引脚以及IWDG独立看门狗的复位信号这四种情况能唤醒。待机模式下,时钟和1.8V区域的供电全部关闭,电压调节器也停止工作,理论上是功耗最低的模式。比如在一些电池供电的设备中,当设备长时间不使用时,进入待机模式,只有在特定唤醒条件触发时才恢复工作,大大延长了电池续航时间。

实时时钟(RTC)与备份寄存器

后备供电区域

实时时钟和备份寄存器都属于电源管理模块的后备供电区域。当主电源VDD稳定时,由VDD供电;当VDD掉电且低于阈值时,自动切换到电池VBAT供电,确保这部分模块在掉电情况下仍能正常工作,实现数据的掉电不丢失。就像我们的时钟,即使家里停电了,若有时钟配备电池,依然能正常走时。

备份寄存器

  1. 数据存储功能:备份寄存器可用于存储希望掉电后不丢失的数据,且不同于存储在flash中的数据。我们的芯片中有DR1到DR42共42个备份数据寄存器,每个寄存器为16位。在实际应用中,比如存储设备的一些配置信息,即使设备掉电重启,这些配置依然有效。
  2. 其他功能:备份寄存器还具备侵入检测功能和RTC校准功能。可以将RTC时钟校准后的64分频信号从侵入检测引脚PC13输出,也能输出RTC闹钟的秒脉冲和闹钟脉冲。不过这些功能在实际应用中使用较少。

实时时钟(RTC)

  1. 时钟源选择:RTC的时钟源有三种选择,分别是HSE的128分频、LSE和LSI。通常选择LSE,因为它比较稳定,计数精度高,频率为32768Hz。就像在制作高精度计时设备时,稳定的时钟源能保证计时的准确性。
  2. 核心寄存器及功能
    • PRL预分频重装载寄存器:写入初始重装载值。以产生秒脉冲为例,由于LSE频率为32768Hz,将PRL设为32767,当DIV寄存器从该值减到0时,刚好经过32768个周期,产生一个秒脉冲。这就如同一个倒计时器,每经过一个时钟周期减1,减到0时触发特定事件。
    • CNT计数值寄存器:可写入初始计数值,如Unix时间戳,表示从1970年1月1日0点0分0秒到现在的总秒数。之后每隔一秒,在秒脉冲的驱动下加1,从而实现计时功能,就像普通时钟的秒针,每秒走动一格。
    • ALR闹钟值寄存器:当CNT的值与ALR的值相等时,产生闹钟信号。比如我们设置早上7点的闹钟,当计时到达7点,即CNT计数到对应的值,闹钟就会响起。
  3. 中断与唤醒功能:RTC的秒脉冲、闹钟脉冲和溢出标志都有对应的中断标志位,由NVIC统一管理。同时,RTC闹钟事件可直接用于唤醒待机模式,与WKUP引脚PA0的上升沿唤醒待机模式是或的关系。在代码实现中,设置闹钟时,需先等待RTOFF变为1,将CNF置1进入配置模式,设置相关寄存器,再将CNF清零退出配置模式,最后等待RTOFF变为1确认操作完成。这一过程与RTC初始化时配置PRL类似,按照标准流程操作即可。

mermaid

Code 经典 手绘

Created with Raphaël 2.3.0 开始 等待RTOFF变为1 将CNF置1进入配置模式 设置CNT、ALR等寄存器 将CNF清零退出配置模式 等待RTOFF变为1确认操作完成 结束
			HTML
			
				
			
			
					
			
		​

思维导图

通过本次复习,我们对电源控制和实时时钟(RTC)的知识有了更深入的理解。RTC模块作为STM32中的一个重要组件,在数据存储、安全性保障以及与RTC协同工作方面发挥着重要作用。希望大家通过这次学习,对RTC模块有更深入的掌握,在今后的开发中能够灵活运用。

image-20250115112629568

2RTC实验:闹钟唤醒待机模式的HAL库实现

在嵌入式开发的学习道路上,我们已经用寄存器方式实现了RTC闹钟唤醒待机模式的功能。今天呢,咱们再用HAL库的方式来实现一遍这个经典案例,对比对比两种方式有啥不一样。话不多说,咱们马上开始。

工程创建与配置

新建工程

打开STM32CubeMX创建新工程,在收藏夹里选择STM32F103ZET6微控制器。接着开始一系列常规配置,在“System Core”中选择“Serial Wire”单线调试模式,这能方便我们后续调试程序。然后到“RCC”选项,选择外部晶振“HSE”。进入时钟配置页面,将“HSE”作为时钟源,经过PLL 9倍频后选择PLL时钟作为系统时钟,使其达到72MHz ,再把APB1选择二分频得到36MHz。因为我们后续需要串口输出测试,所以在“Connectivity”里选择“USART1”并设置为异步模式,波特率115200,数据位8位,无校验位,1位停止位,这些参数用默认设置就好。

RTC配置

image-20250115113916890 image-20250115113933804

RTC模块在“Timers”里,把“RTC”勾选上并激活时钟源。RTC时钟源有HSE 128分频、LSE和LSI三种选择,不过这里直接选择可不行,得在时钟配置页“Clock Configuration”里设置。启用RTC后,这里就可以选择时钟源了,默认是LSI(内部40kHz RC振荡器产生的时钟),我们把它改成LSE(外部晶振32768Hz)。下面有个“启用日历功能”选项,由于我们这次主要设置闹钟,不涉及日历,所以不用勾选。还有“Tamper功能”,它和PC13引脚作为侵入检测引脚相关,我们也用不到,不用管。“RTC Output”默认是“Disable”,这里我们也不用配置。日期时间配置部分,因为没开启日历功能,所以也不用设置。通用配置里有个“自动预分频计算”,开启它后,RTC会根据选择的时钟源自动分频产生1Hz的秒脉冲信号,我们就不用手动设置预分频器的值了,非常方便。要是把它改成“Disable”,就得手动设置预分频器的值,范围是0到1048575(约为2的20次方 - 1),例如选择LSE作为时钟源时,手动设置就需把值设为32767。但这次我们直接开启自动预分频计算就行。

LED配置

我们打算用LED灯来显示当前工作状态,选择PA1对应的蓝灯LED2。将其设置为“GPIO Output”通用推挽输出模式,默认高电平(关灯状态),输出速度选高速,再给它添加一个“LED2”的标签。

工程命名与代码生成

在“Project Manager”里给工程命名为“二十四RTCalarmstandbyHAL”,选择“MDK-ARM”工具链,勾选“Generate Code”生成代码。之后关闭STM32CubeMX,打开工程进入下一步操作。

工程设置

打开工程后,进入魔法棒设置。因为我们要用printf打印输出,所以勾选“Use MicroLIB”。“C/C++”选项卡不用添加文件,保持默认。“Debug”选项里,点开“Settings”,勾选“Flash Download”,去掉“Pack”的勾选。设置完成后,就可以在VScode里打开代码进行修改了。

flow复制

Created with Raphaël 2.3.0 开始 选择STM32F103ZET6 选择Serial Wire调试模式 配置时钟源及频率 配置USART1为异步模式 勾选并配置RTC 配置PA1为LED输出 命名工程并生成代码 魔法棒设置相关选项 在VScode中打开工程 结束

代码实现

代码移植与修改

我们可以参考之前用PA0引脚唤醒待机模式的代码,把主函数代码复制过来。因为要用RTC相关功能,所以得引入“RTC.h”头文件。复制过来的代码里,RTC初始化函数“RTC_Init”是必须有的,要把它补回来。另外,清除唤醒标志位这一步很关键。之前用PA0引脚唤醒时需要清除标志位,现在用RTC闹钟唤醒同样需要。查看手册可知,电源控制寄存器CSR中的WUF位,在wakeup引脚发生唤醒事件或出现RTC闹钟事件时都会置1 。如果不清除这个标志位,可能会导致一进入待机模式就立刻被唤醒,所以要保留清除标志位的代码。

设置RTC闹钟函数

在主函数中,我们要设置RTC闹钟来唤醒待机模式。因为HAL库提供了丰富的函数,我们可以直接调用。不过直接调用设置闹钟函数HAL_RTC_SetAlarm时,参数是一个结构体指针,设置起来有点麻烦。所以我们自己定义一个RTC_SetAlarm函数来简化操作。 在“RTC.c”文件中定义RTC_SetAlarm函数,函数接收一个uint32_t s参数,表示设置s秒后的闹钟。在函数内部,首先要获取当前时间。HAL库提供了HAL_RTC_GetTime函数来获取时间,它需要传入HRTC的地址、用于保存时间的结构体指针以及时间格式参数。我们选择二进制格式RTC_FORMAT_BIN。获取当前时间后,创建一个RTC_AlarmTypeDef结构体对象,初始化其成员。将AlarmID设为0 ,小时Hours和分钟Minutes设为当前时间的对应值,秒Seconds设为当前时间的秒数加上s - 1。这里减1是因为计数从0开始。最后调用HAL_RTC_SetAlarm函数设置闹钟,传入HRTC的地址、设置好的闹钟结构体指针以及时间格式参数。在“RTC.h”头文件中声明RTC_SetAlarm函数,这样在主函数中就可以调用它了。在主函数里,我们调用RTC_SetAlarm(5)设置5秒后的闹钟。

c复制

// RTC.c文件中
void RTC_SetAlarm(uint32_t s)
{
    RTC_TimeTypeDef currentTime;
    HAL_RTC_GetTime(&hrtc, &currentTime, RTC_FORMAT_BIN);

    RTC_AlarmTypeDef sAlarm;
    sAlarm.AlarmID = 0;
    sAlarm.AlarmTime.Hours = currentTime.Hours;
    sAlarm.AlarmTime.Minutes = currentTime.Minutes;
    sAlarm.AlarmTime.Seconds = currentTime.Seconds + s - 1;

    HAL_RTC_SetAlarm(&hrtc, &sAlarm, RTC_FORMAT_BIN);
}

c复制

// RTC.h文件中
void RTC_SetAlarm(uint32_t s);

c复制

// 主函数中
RTC_SetAlarm(5);

串口打印输出配置

为了方便调试,我们可能还需要串口打印输出功能。在主函数所在文件引入“stdio.h”头文件,然后在“usart.c”文件中重写fputc函数。在fputc函数中,使用HAL_UART_Transmit函数发送一个字节数据,实现串口打印输出。

c复制

// usart.c文件中
#include "stdio.h"
#include "usart.h"

int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
    return ch;
}

测试与结果

完成代码编写后,进行编译,确保没有语法错误。编译通过后烧写代码到开发板。我们可以看到,程序运行后,3秒后进入待机模式,蓝灯熄灭。等待5秒后,RTC闹钟唤醒系统,蓝灯亮起,然后又重复这个过程,每3秒进入待机模式,5秒后被闹钟唤醒。这表明我们使用HAL库成功实现了RTC闹钟唤醒待机模式的功能。

flow复制

Created with Raphaël 2.3.0 开始 编译代码 烧写代码到开发板 程序运行,等待进入待机 3秒后进入待机模式,蓝灯熄灭 5秒后RTC闹钟唤醒,蓝灯亮起 重复进入待机和唤醒过程 结束

思维导图

image-20250115113157619

3RTC实验二:实时时钟的寄存器方式实现

在嵌入式开发中,实时时钟(RTC)是一个非常重要的功能模块,它能够提供掉电不丢失的时间信息,就像我们日常生活中的手表或电子台历一样。今天,我们就来详细讲解如何使用寄存器方式实现RTC实时时钟功能。

实时时钟功能概述

本次实验的实时时钟需要具备两个核心功能:一是能够设置时间戳,即从1970年1月1日00:00:00到当前时刻的总秒数;二是能够将时间戳转换为人们易于理解的年月日时分秒的日历形式进行展示。这两个功能相互配合,使得我们的RTC模块可以准确地记录和显示时间。

时间戳概念

Unix时间戳是从1970年1月1日零点零分零秒到现在为止的总秒数。在Windows系统中,我们可以通过PowerShell获取Unix时间戳。打开PowerShell,输入命令(Get-Date -UFormat %s) -as [int],即可得到当前时间的时间戳。例如,执行该命令后得到的时间戳大概为十七亿多秒。这个时间戳将作为我们设置RTC初始时间的依据。

image-20250116111323463 image-20250116111344479 image-20250116111433482

image-20250116112642737

日历时间转换

要将时间戳转换为年月日时分秒的日历时间,手动计算较为复杂,因为涉及到月份天数不固定、闰年等规则。幸运的是,C语言的time.h库为我们提供了便利。该库中的localtime函数可以将时间戳转换为struct tm结构体,其中包含了年月日时分秒等信息。不过,struct tm结构体的月份是从0开始计数,年份是从1900年开始计数,与我们日常习惯不太一致。因此,我们自定义了DateTime结构体,使其更符合我们的使用习惯。

image-20250116113850412

c复制

typedef struct
{
    uint16_t year;
    uint8_t month;
    uint8_t day;
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
} DateTime;

工程搭建与配置

工程复制与重命名

基于之前RTC闹钟唤醒待机模式的代码,将其复制一份并改名为“RTCcalendar”。这样可以复用之前工程中RTC的相关配置,减少工作量。复制完成后,删除新工程中不必要的文件,保持工程结构简洁。

工程配置

打开工程的魔法棒设置,在debug选项中,选择stlink,并去掉packed enable的勾选。这一步的设置是为了确保在调试过程中,工程能够正确地与调试工具进行通信。

代码编写

头文件定义

rtc.h头文件中,引入time.h库,用于时间相关的操作。同时,声明了RTC_InitRTC_SetAlarmRTC_SetTimestampRTC_GetDateTime等函数,以及自定义的DateTime结构体。这些声明为后续在其他文件中使用这些函数和结构体提供了必要的信息。

RTC初始化函数(RTC_Init

该函数用于初始化RTC,分为三个主要步骤。

  • 后备域统一配置:首先开启PWR时钟,放开后备域的写保护。在设置RTC闹钟时,每次上电都对备份域复位是可行的,因为设置CNT计数值默认是零。但对于实时时钟,若每次复位备份域,之前写入的时间戳就会丢失。所以,备份域复位操作要么在第一次上电时执行,之后注释掉;要么直接注释掉该操作。

c复制

// 1.1 开启PWR时钟
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
// 1.2 放开后备域的写保护
PWR->CR |= PWR_CR_DBP;
// // 1.3 软件复位整个备份域(根据需求决定是否执行)
// RCC->BDCR |= RCC_BDCR_BDRST;
// // 1.4 解除备份域复位
// RCC->BDCR &= ~RCC_BDCR_BDRST;
  • 配置RTC时钟源以及开启RTC:开启RTC时钟,打开LSE并等待其启动完成,然后选择LSE作为RTC的时钟源。LSE(低速外部晶振)通常为32.768kHz,能够提供较为稳定的时钟信号,适合用于RTC计时。

c复制

// 2.1 开启RTC时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
// 2.2 打开LSE并等待启动完成
RCC->BDCR |= RCC_BDCR_LSEON;
while (!(RCC->BDCR & RCC_BDCR_LSERDY))
{
}
// 2.3 选择LSE作为RTC的时钟源
RCC->BDCR &= ~RCC_BDCR_RTCSEL;
RCC->BDCR |= RCC_BDCR_RTCSEL_0;
  • RTC寄存器的配置:查询RTOFF位,直到其变为1,然后进入配置模式,设置预分频系数为32767,以产生秒脉冲,最后退出配置模式并再次查询RTOFF位。预分频系数的设置决定了RTC计数器的计数频率,这里设置为32767,使得RTC计数器每秒钟增加1 ,实现了秒计时功能。

c复制

// 3.1 查询RTOFF位,直到变为1
while (!(RTC->CRL & RTC_CRL_RTOFF))
{
}
// 3.2 进入配置模式
RTC->CRL |= RTC_CRL_CNF;
// 3.3 设置预分频系数 32767,产生秒脉冲
RTC->PRLH = 0;
RTC->PRLL = 0x7fff;
// 3.4 退出配置模式
RTC->CRL &= ~RTC_CRL_CNF;
// 3.5 查询RTOFF位,直到变为1
while (!(RTC->CRL & RTC_CRL_RTOFF))
{
}
设置时间戳函数(RTC_SetTimestamp

该函数用于设置RTC的时间戳,即向CNT寄存器写入从1970年1月1日零点零分零秒开始的总秒数。具体操作是先查询RTOFF位,直到其变为1,进入配置模式,将传入的时间戳值分别写入CNTH和CNTL寄存器,最后退出配置模式并再次查询RTOFF位。由于CNTH和CNTL各是16位,而传入的时间戳是32位,所以需要进行移位操作来分别设置高位和低位寄存器。

c复制

void RTC_SetTimestamp(uint32_t ts)
{
    // 1. 查询RTOFF位,直到变为1
    while (!(RTC->CRL & RTC_CRL_RTOFF))
    {
    }
    // 2. 进入配置模式
    RTC->CRL |= RTC_CRL_CNF;
    // 3. 设置CNT寄存器
    RTC->CNTH = (ts >> 16) & 0xffff;
    RTC->CNTL = (ts >> 0) & 0xffff;
    // 4. 退出配置模式
    RTC->CRL &= ~RTC_CRL_CNF;
    // 5. 查询RTOFF位,直到变为1
    while (!(RTC->CRL & RTC_CRL_RTOFF))
    {
    }
}
获取日历时间函数(RTC_GetDateTime

该函数用于获取当前的日历时间,将RTC计数器的值(秒数)转换为自定义的DateTime结构体。具体步骤如下:

  • 等待寄存器同步:RTC在进行读操作前,需要等待寄存器同步标志RSF被硬件置1,以确保要读取的CNT、ALR和PRL寄存器已经同步。通过循环判断RTC->CRL & RTC_CRL_RSF的值,当该值为1时,表示寄存器已同步。

c复制

while ( !(RTC->CRL & RTC_CRL_RSF) )
{}
  • 读取当前计数值(秒数):将CNTH和CNTL寄存器的值组合起来,得到当前的秒数。由于CNTH是高位寄存器,所以需要将其左移16位,然后与CNTL进行按位或操作,得到32位的秒数。

c复制

uint32_t second = RTC->CNTH << 16 | RTC->CNTL;
  • 将秒数转换成tm结构体对象:调用localtime函数,将秒数的地址作为参数传入,得到一个指向struct tm结构体对象的指针ptmlocaltime函数根据传入的时间戳,将其转换为包含年月日时分秒等信息的struct tm结构体。

c复制

struct tm* ptm = localtime(&second);
  • 基于tm构建自定义的结构体对象:根据ptm指向的struct tm结构体对象,构建自定义的DateTime结构体对象。将ptm->tm_year加上1900得到年份,ptm->tm_mon加上1得到月份,以及其他成员的赋值,完成从struct tmDateTime的转换。

c复制

dateTime->year = ptm->tm_year + 1900;
dateTime->month = ptm->tm_mon + 1;
dateTime->day = ptm->tm_mday;
dateTime->hour = ptm->tm_hour;
dateTime->minute = ptm->tm_min;
dateTime->second = ptm->tm_sec;

主函数实现

在主函数中,首先进行串口和RTC的初始化,然后设置一次当前的时间戳(在实际运行时,需要将获取到的系统时间戳填入RTC_SetTimestamp函数的参数中)。接着进入一个无限循环,在循环中每隔1秒调用RTC_GetDateTime函数获取当前时间,并将其以特定格式打印输出。通过Delay_ms(1000)函数实现1秒的延迟,使得程序每隔1秒更新并显示一次时间。

c复制

int main(void)
{
    // 初始化
    USART_Init();
    RTC_Init();
    printf("尚硅谷RTC实验:RTC实时时钟...\n");
    // 设置一次当前的时间戳
    // RTC_SetTimestamp(1736160789);
    DateTime dateTime;
    while (1)
    {
        // 每隔1s获取当前时间打印输出一次
        RTC_GetDateTime(&dateTime);
        printf("%04d年%02d月%02d日 %02d:%02d:%02d\n",
               dateTime.year, dateTime.month, dateTime.day, dateTime.hour, dateTime.minute, dateTime.second);
        Delay_ms(1000);
    }
}

实验测试

编译与烧写

完成代码编写后,对工程进行编译,确保代码没有语法错误。编译成功后,将程序烧写到开发板中。

功能测试

烧写完成后,我们可以看到实时时钟开始运行,显示当前的时间。由于在烧写过程中会消耗一定时间,所以显示的时间可能与真实时间存在十几秒的误差。按下复位键,如果不注释掉设置时间戳和备份域复位的代码,实时时钟会回退到初始设置的时间。为了使实时时钟在复位后能够继续计时,需要注释掉main.c中设置时间戳的代码,以及rtc.c中备份域复位的代码,然后重新编译烧写。再次按下复位键,实时时钟将不受影响,继续往后走。如果开发板上放置了电池,拔掉电源后,RTC仍会依靠电池供电继续计时。重新上电后,实时时钟会继续之前的时间往后走,实现了掉电不丢失的功能。

通过本次实验,我们成功地使用寄存器方式实现了RTC实时时钟功能,掌握了时间戳的设置和日历时间的转换与显示方法。这对于嵌入式系统中需要时间功能的应用场景,如定时任务、日志记录等,具有重要的参考价值。

image-20250116161710288 image-20250116161817857 image-20250116162009453

总结思维导图

image-20250116112529749

4RTC实验二扩展练习:LCD显示实时时钟

在嵌入式开发中,实时时钟的应用十分广泛。此前,我们通过计算器的方式实现了实时时钟功能,并在主函数中每隔一秒获取当前日期时间并打印输出到串口控制台。然而,在实际应用里,单纯使用串口输出时间并不常见。鉴于我们的开发板配备了液晶屏幕,能否将实时时钟的时间展示在液晶屏上呢?这便是本次的扩展练习,接下来,我们一起详细探讨实现过程。

创建工程

复制工程并重命名

为了展示不同的实现方式,我们将原工程复制一份。将复制后的工程命名为 EX01_RTCcalendar_LCD,作为本次扩展练习的第一个项目。同时,删除原工程中不必要的内容,对目录和文件进行相应调整。

添加液晶相关代码

要在液晶屏上显示内容,底层需要有液晶相关的接口以及其驱动代码。在之前的 SDM32base 进阶篇中,我们实现过液晶的输出显示功能。我们需要将相关代码复制到当前工程目录。具体来说,复制 hardware 下的 FS 文件夹以及 interface 下的 LCD 文件夹到当前工程的 hardwareinterface 目录(若不存在相应目录则创建)。在复制 interface 下的内容时,只保留 LCD 相关文件即可。

工程配置

  • 添加文件到工程:打开工程,在工程管理界面中,将新增的 hardware/FS/FSMC.cinterface/LCD/LCD.c 文件添加到工程中。
  • 添加目录到工程配置:点击魔法棒图标进入工程配置界面,在 C/C++ 选项卡中,添加 hardware/FSMCinterface/LCD 目录,让编译器能够找到相关头文件。
  • 调试配置:在 Debug 选项卡中,选择 SDlink,并勾选 Debug Settings 相关选项,完成工程的调试配置。

flow复制

Created with Raphaël 2.3.0 开始 复制原工程并重命名为EX01_RTCcalendar_LCD 复制hardware下的FSMC和interface下的LCD到当前工程对应目录 添加FSMC.c和LCD.c到工程 在C/C++选项卡添加相关目录 在Debug选项卡选择SDlink并勾选Debug Settings 结束

代码修改

引入头文件与初始化液晶

打开VScode进入主函数,引入液晶相关头文件 #include LCD.h。在主函数的初始化部分,添加液晶初始化函数 LCDinit,确保液晶能够正常工作。

处理时间显示

  • 定义字符串:使用 sprintf 函数将时间信息包装到一个字符串中。在主函数外定义一个足够长度的字符串,例如 char info[100];,用于存储要显示的时间信息。
  • 格式化时间信息:由于直接打印中文字符涉及取模等复杂操作,这里我们使用连字符来简单表示时间格式。使用 sprintf 函数将时间信息按照 “年月日 - 时分秒” 的格式写入 info 字符串。
  • 在液晶屏显示字符串:调用液晶显示字符串的函数 LCD_write_string(函数名可能因实际代码而异),将格式化后的时间字符串显示在液晶屏上。设置显示的坐标为左上角稍微空开一点的位置,如 (20, 20),字体高度设置为最大的 32,字体颜色设为 blue,背景颜色设为 white。函数调用示例如下:LCD_write_string(20, 20, 32, info, blue, white);

flow复制

Created with Raphaël 2.3.0 开始 在主函数引入#include LCD.h 在主函数初始化部分添加LCDinit 在主函数外定义char info[100]; 使用sprintf将时间信息格式化写入info 调用LCD_write_string显示info到液晶屏 结束

编译与烧写

完成代码修改后,对工程进行编译。确保代码没有语法错误和链接错误后,将程序烧写到开发板中。此时,由于我们已经不再通过串口打印输出时间,所以无需关注串口信息。烧写成功后,开发板的液晶屏将实时显示当前的时间,并且每秒更新一次。

总结

通过本次扩展练习,我们基于之前实现的实时时钟代码,通过简单的修改,成功地将实时时钟的时间显示在液晶屏上。这不仅增强了我们对嵌入式系统中时间显示功能的理解,还让我们熟悉了如何在开发板的液晶屏上展示信息。在实际的嵌入式应用中,这种时间显示功能非常实用,比如在智能仪表、电子时钟等设备中都有广泛的应用。希望大家通过本次练习,能够更加熟练地掌握嵌入式开发中时间相关功能的实现以及液晶显示的应用。

思维导图

image-20250116162606432

5RTC实验二:实时时钟HAL库方式实现

在之前的学习中,我们通过寄存器的方式实现了实时时钟的日历功能。这次,我们来探索如何用HAL库达成同样的效果,这能让大家接触到不同的开发思路,对理解实时时钟的实现原理很有帮助。

创建工程与配置

复制工程

我们以之前RTC闹钟唤醒待机模式的代码为基础进行复制,把复制后的工程命名为RTCcalendar。随后,将原工程和生成的代码删除,为后续重新配置做准备。这一步就像为我们搭建了一个新的实验环境,方便我们开展HAL库实现实时时钟功能的工作。

配置RTC日历功能

进入图形化配置界面后,我们先开启RTC功能,并选择LSE作为时钟源,这能为实时时钟提供稳定的计时基础。接着,勾选“activate calendar”来启用日历功能。此时,配置界面会出现新的选项,“calendartime”用于配置时分秒,“calendardate”用于配置年月日以及星期。

在填写时间时,年份要填后两位,比如2025年就填“25”。这是因为HAL库兼容传统时间表达,让我们可以自由定义前两位,灵活性更高。数据格式必须选BCD码,这是为了和操作系统中时间的表达格式保持一致。要是选二进制格式,由于底层宏定义的问题,会导致时间展示出错,像把十月显示成“十六月”。完成时间和日期的填写后,直接在工程管理器中生成代码。

flow复制

Created with Raphaël 2.3.0 开始 复制RTC闹钟唤醒待机模式代码并命名为RTCcalendar,删除原工程及代码 图形化界面开启RTC,选LSE为时钟源,勾选activate calendar 配置calendartime和calendardate,选BCD码格式 工程管理器生成代码 结束

工程配置

打开工程,在魔法棒中勾选“USMCROEB”,并在“debug”选项里进行相应勾选。之后,用VScode打开工程。因为是基于之前的工程复制的,一些操作,像FDC重写,要是已经有对应代码了,就不用再做。

代码编写与分析

主函数代码修改

在主函数所在文件里,我们要删除和RTC闹钟唤醒待机模式相关的全局变量和代码,避免干扰。由于在图形化界面已经配置好了时间和日期,在RTC初始化过程中就会完成设置,所以在主函数里不需要再设置时间戳。

在主函数中,定义保存时间和日期的结构体对象RTC_DateTypeDef dateRTC_TimeTypeDef time。通过调用HAL_RTC_GetDateHAL_RTC_GetTime函数获取当前日期和时间,注意获取时要用二进制格式RTC_FORMAT_BIN。最后,用printf函数把时间和日期打印输出,同时把delayMS改成HAL库中的HAL_Delay函数,实现每秒输出一次,实时展示时间变化。

flow复制

Created with Raphaël 2.3.0 开始 删除无关全局变量和代码 定义date和time结构体对象 调用HAL_RTC_GetDate获取日期 调用HAL_RTC_GetTime获取时间 使用printf输出时间和日期,用HAL_Delay实现每秒输出 结束

时间和日期的存储机制

在HAL库中,时间(时分秒)存放在RTC的CNT计数器里,日期(年月日)存放在句柄的DateToUpdate结构体对象中。这种分离存储的方式解决了时间戳溢出的问题,比如“二零三八问题”。当秒数达到一天的最大值,多出的天数会进位到日期部分,这样CNT计数器里存的始终是当天的秒数,不会溢出。

遇到的问题及解决

年份显示问题

一开始输出的年份只有两位且前面是“00”,不符合我们的日常习惯。解决办法是在输出时手动添加“20”,让年份显示正常。

复位和掉电问题

按下复位键后,时间会回到初始配置时间,日期会变成“2000年1月1日”,这是因为日期结构体对象在复位后数据丢失。掉电时也存在同样问题,数据会丢失。为解决这个问题,我们可以利用备份寄存器来保存日期数据,确保复位和掉电时数据不丢失,这部分内容我们将在下一章节详细讲解。

思维导图

image-20250116170554965

7 尚硅谷嵌入式技术之STM32单片机:独立看门狗总概述

在嵌入式系统开发中,看门狗是一个非常重要的组件,它就像是我们系统的守护者,时刻监控着程序的运行状态。今天,咱们就来深入了解一下STM32单片机中的独立看门狗。

看门狗概述

看门狗英文叫watchdog,从名字就能看出来,它的作用是给我们的系统、应用程序“看家护院”,对系统状态进行监控。本质上,它是一种计时硬件电路,也可以看作是一种定时器。其工作原理是计数器不停地做递减操作,减到一定程度就会溢出,进而产生特殊事件,这个事件通常能让系统复位。

在复杂恶劣的系统环境中,程序运行可能会出现各种不可预料的错误,比如卡死、崩溃或者跑飞。如果没有自动重启机制,就只能靠人工检修,手动复位或者断电再上电,这在很多嵌入式场景中是不现实的,成本代价太大。所以我们引入看门狗,一旦程序运行不正常,它能及时产生复位信号,让程序重新运行。

STM32的看门狗分类

STM32中的看门狗主要有两种:独立看门狗和窗口看门狗。它们还有有趣的外号,独立看门狗号称“宠物狗”,窗口看门狗号称“警犬”。这是按照时间控制精度划分的,窗口看门狗时间控制更精确,所以是“警犬”,独立看门狗时间精度相对较低,就是“宠物狗”啦。

独立看门狗详解

独立看门狗英文简写为IWDG(Independent Watchdog),它本质上就是一个计时器,核心是一个12位的向下递减计数器。当计数器减到0时,系统就会产生复位信号IWDG_RESET。如果想让系统不复位,正常往下执行,就要在计数器减到0之前,刷新计数器的值,这个操作形象地称为“喂狗”。就好比我们养宠物,定时喂它,它就知道主人一切正常;要是长时间不喂,它就会“报警”,这里就是触发复位操作。

flow复制

Created with Raphaël 2.3.0 系统启动 初始化独立看门狗 计数器递减 计数器减到0? 系统复位 喂狗(刷新计数器值) yes no

计数器时钟

独立看门狗的时钟由内部低速时钟LSI提供。LSI是一个独立的RC振荡电路,即使主时钟发生故障,它依然有效,这也是独立看门狗“独立”的原因。LSI的频率一般在30 - 60KHZ之间,通常取40KHZ。不过由于它是RC振荡电路,受温度、湿度等环境因素影响,频率会有一定漂移,不太精准,所以独立看门狗适用于对时间精度要求较低的场合。

预分频寄存器(PR)

预分频寄存器PR基于40KHZ的LSI时钟频率,可以定义分频值,从而控制计数器递减的频率。PR寄存器只有低3位有效,有8种不同取值,代表不同的分频系数,从最小的4分频到最大的256分频。比如,当PR取值为000时,是4分频,40KHZ的LSI时钟经过4分频后,计数器的时钟频率就变为10KHZ。

12位递减计数器

独立看门狗的计数器是12位的,最大值为0XFFF。计数器每过一个时钟周期就减1,当减到0时,就会产生复位信号,让程序重新运行。为了避免系统频繁复位,我们需要及时“喂狗”。

重装载寄存器(RLR)

重装载寄存器RLR是12位的,里面存放的是要刷新到计数器的值,这个值决定了独立看门狗的溢出时间。溢出时间与预分频系数和RLR的值都有关系,计算公式为:

T_{IWDG}=\frac{1}{时钟频率} * 预分频系数 * (RL + 1)

TIWDG=1时钟频率∗预分频系数∗(RL+1)

,其中时钟频率一般取40KHZ,RL是重装载寄存器的值。例如,当预分频系数为4,RLR的值为4095时,最长超时时间约为409.6毫秒。

重装载寄存器值为 0:若重装载寄存器值 n 为 0,计数器从 0 开始递减,由于已经是最小值,减到 0 只需 1 个计数周期。根据计数周期为 0.1ms,所以经过 0.1ms 就会产生复位信号 。这意味着如果程序在 0.1ms 内没有进行 “喂狗” 操作,独立看门狗就会认为程序出现异常,触发系统复位。在一些对响应速度要求极高的简单任务场景中,如果任务执行时间理论上极短,就可以将重装载寄存器值设为 0,快速检测程序是否正常运行。

键寄存器(KR)

键寄存器KR是独立看门狗的控制寄存器,只能写入0 - 15位,读操作返回值永远是0。往KR寄存器写入不同的值有不同的作用:

  • 写入0xCCCC:启动看门狗,且一旦启动无法停止。
  • 写入0xAAAA:执行“喂狗”操作,将重装载计数器的值添加到计数器中。
  • 写入0x5555:允许访问IWDG_PR(预分频计数器)和IWDG_RLR(重装载计数器)寄存器,以便进行配置。

状态寄存器(SR)

状态寄存器SR只有位0(PVU)和位1(RVU)有效,这两位只能由硬件操作,软件无法修改。PVU表示预分频值更新状态,RVU表示重装载值更新状态。当PVU或RVU为1时,分别表示预分频值或重装载值正在更新,更新结束后会由硬件清0。

Created with Raphaël 2.3.0 程序开始 初始化串口、按键、独立看门狗 打印"程序正常执行...." 延迟3s flag为真? 延迟4s flag = 0 喂狗(IDWDG_Refresh) yes no

思维导图

独立看门狗在嵌入式系统中起着至关重要的作用,能够有效防止程序因异常而陷入死锁或崩溃状态。通过合理配置预分频系数和重装载值

image-20250115155641441

BCD 的区别

8看门狗_窗口看门狗介绍

在前文里,咱们已经认识了独立看门狗这位系统的“小卫士”。今天呢,咱们再来结识另一位同样重要的“小伙伴”——窗口看门狗。它的英文缩写是 WWDG,这里的“W”代表“window”,也就是窗口的意思。窗口看门狗和独立看门狗一样,核心都是一个递减计数器,但在工作方式上,它有着自己独特的一套规则,接下来咱们就好好唠唠。

窗口看门狗独特的工作规则

特别的计数复位机制

窗口看门狗的计数器在计数的时候,和独立看门狗有很大不同。它不是减到 0 才产生复位信号,而是当计数器减到一个固定值 0x40(二进制表示为 01000000)时,如果再减一次,最高位就会从 1 变成 0,这时候要是还没“喂狗”,就会触发复位信号。所以啊,0x40 这个值就像是一个固定的“红线”,被称为窗口下限,这个可是不能改变的。

更有意思的是,窗口看门狗对“喂狗”的时间要求非常严格。在计数器减到某一个设定值之前进行“喂狗”操作,同样会产生复位信号。也就是说,“喂狗”既不能太早,也不能太晚,得在一个特定的时间窗口内进行才行。早于上限或者晚于下限“喂狗”,都会让系统产生复位信号,只有在这个窗口范围内“喂狗”,程序才能正常运行,这也是它叫窗口看门狗的原因。

比如说,我们设定窗口上限为 0x60。程序开始运行后,计数器从初始值开始递减。当计数器的值大于 0x60 的时候,如果我们进行“喂狗”操作,系统就会认为“喂狗”过早,然后产生复位信号;当计数器的值减到 0x40 及以下的时候,如果还没“喂狗”,系统就会觉得“喂狗”过晚,同样会产生复位信号。只有计数器的值在 0x60 到 0x40 之间的时候进行“喂狗”,系统才能正常运转。

flow复制

Created with Raphaël 2.3.0 程序启动 计数器递减 计数器值 > 窗口上限? 产生复位信号(喂狗过早) 计数器值 <= 窗口下限? 产生复位信号(喂狗过晚) 正常运行,继续计数 yes no yes no

功能框图大揭秘

窗口看门狗的功能框图主要有两个寄存器在起关键作用,一个是看门狗配置寄存器(CFG),另一个是看门狗控制寄存器(CR)。

控制寄存器 CR 的最高位 WDGA 是窗口看门狗的使能位。当这一位为 1 的时候,窗口看门狗就被启用啦。CR 里面的低七位 t0 - t6 就是窗口看门狗的计数器,这个计数器和配置寄存器 CFG 里面的七位 w0 - w6 会在一个比较器里进行比较。要是计数器的值 t6 - t0 大于设定的窗口值 w0 - w6,比较的结果就会是 1,经过位与操作之后,就有可能产生复位信号。

还有啊,计数器的最高位 t6 有个特殊的功能,它必须一直保持为 1。一旦 t6 从 1 变成 0,经过取反之后,通过或门就会直接产生复位信号。这就意味着当计数器的值减到 0x40 以下的时候,就会通过这条路径产生复位信号。而当“喂狗”操作过早,也就是计数器的值大于窗口值的时候进行“喂狗”(往控制寄存器 CR 的 t0 - t6 写入新值),同样会产生复位信号。

flow复制

Created with Raphaël 2.3.0 窗口看门狗启用 检查WDGA位是否为1

窗口看门狗的关键组成部分

独特的时钟来源与预分频设置

窗口看门狗的时钟来源和独立看门狗不一样哦。它用的是系统时钟,具体来说,是从 72MHz 的系统时钟分频之后,连接到 APB1 总线上的 PCLK1 时钟,默认频率是 36MHz。这个频率有点高,实际使用的时候,咱们得给它再分分频。

预分频的设置是在配置寄存器 CFG 里完成的。CFG 里面有一个两位的字段 WDGTB,也就是 Time Base 的意思,它用来确定预分频系数。这个预分频系数是 2 的整数次幂,取值有 0 - 3,分别对应 2 的 0 次方(不分频)、2 的 1 次方(2 分频)、2 的 2 次方(4 分频)、2 的 3 次方(8 分频)。窗口看门狗默认会先进行 4096 分频,然后再根据 WDGTB 的值进一步分频,这样就能得到计数器的时钟频率啦。

比如说,当 WDGTB 设置为 0 的时候,计数器的时钟频率就是 36MHz÷4096÷2 的 0 次方;当 WDGTB 设置为 2 的时候,计数器的时钟频率就是 36MHz÷4096÷2 的 2 次方。通过这样灵活的分频设置,咱们就可以根据实际需要来调整窗口看门狗的计时精度。

flow复制

Created with Raphaël 2.3.0 系统启动 72MHz系统时钟 分频得到PCLK1(默认36MHz) 读取CFG中WDGTB值 先4096分频,再依WDGTB值分频 得到计数器时钟频率

特别的计数器

窗口看门狗的计数器就在控制寄存器 CR 里面。除了使能位 WDGA,计数器实际上是七位(t0 - t6),它的最大值是 0x7F,最小值是 0x40。当计数器的值减到 0x3F 的时候,马上就会产生复位信号。

这里有个特别的地方,计数器的最高位 t6 必须一直是 1。一旦 t6 从 1 变成 0,就会产生复位信号,后面也就不再计数了。所以啊,实际上真正用来计数的有效位只有后六位,这也就是为什么有些资料会把窗口看门狗叫做六位定时器或者六位计数器。

flow复制

Created with Raphaël 2.3.0 窗口看门狗初始化 设置计数器初始值(0x40 - 0x7F) 计数器递减 检查t6是否为1

配置寄存器的其他重要功能

配置寄存器 CFG 里面还有个控制位 EWI,也就是 Early Wakeup Interrupt,翻译过来就是提前唤醒中断位。当计数器的值快要从 0x40 减到 0x3F 的时候,也就是“喂狗”过晚,马上要产生复位信号之前,就会触发这个中断。

这个中断可有用啦,它可以让我们在系统复位之前,赶紧保存一些重要的数据。比如说,在一个数据采集系统里,当检测到系统马上要复位了,我们就可以利用这个中断,把当前采集到的数据及时存到非易失性存储器里,这样数据就不会丢失了。

另外,CFG 里面的 w0 - w6 字段是用来设定窗口值的,这个窗口值决定了“喂狗”的上限。因为窗口下限是固定的 0x40,所以窗口值要在计数器的上限(0x7F)和下限(0x40)之间。在计数器从初始值递减到窗口值之前,是不允许进行“喂狗”操作的,不然就会产生复位信号。只有当计数器的值小于窗口值的时候,才可以“喂狗”。

flow复制

Created with Raphaël 2.3.0 窗口看门狗运行 计数器递减 计数器值是否接近0x3F?

窗口看门狗的实际应用与超时计算

应用场景举例

窗口看门狗在实际应用中,可以用来监控程序的运行时间。因为程序正常运行的时间一般是比较稳定的,我们可以通过设置合适的窗口值,在程序正常执行结束后,及时进行“喂狗”操作。

要是“喂狗”过早,那就说明程序跑得太快了,可能有些逻辑没有正常执行;要是“喂狗”过晚,那就说明程序执行时间比正常的要长,可能是受到了干扰,或者卡死、跑飞了。通过窗口看门狗的这种机制,我们就能有效地检测出程序运行过程中的异常情况,保证系统的稳定。

比如说,在一个实时数据处理系统里,每次数据采集和处理的正常时间范围是 10 - 20ms。我们可以设置窗口看门狗的窗口值,让它在 10ms 前进行“喂狗”就会触发复位(表示程序执行过快),在 20ms 后还没“喂狗”也会触发复位(表示程序执行过慢或者出现异常)。这样,就能保证数据处理程序一直在正常的时间范围内运行。

flow复制

Created with Raphaël 2.3.0 程序启动 执行数据处理程序 程序执行结束,尝试喂狗 喂狗时间 < 10ms? 产生复位信号(程序执行过快) 喂狗时间 > 20ms? 产生复位信号(程序执行过慢) 正常运行,等待下一次处理 yes no yes no

超时时间的计算方法

窗口看门狗的超时时间是可以通过公式算出来的。它的基准时钟频率是 PCLK1(默认 36MHz),对应的周期就是 1÷36MHz,也就是三十六分之一微秒,我们用 TPCLK1 来表示。经过预分频之后,计数器的时钟频率是 PCLK1 先除以 4096,再除以 2 的 n 次方(n 就是由 WDGTB 那两位表示的),那么一次计数的周期就是这个频率的倒数,也就是 TPCLK1×4096×2 的 n 次方。

计数器从初始值(假设是 t0 - t6,实际上有效计数是 t0 - t5)减到 0 需要的计数个数是 t0 - t5 对应的数值再加 1。所以,超时时间就是一次计数周期乘以总计数个数。

当 WDGTB 分别是 0、1、2、3 的时候,对应的最小超时值和最大超时值是不一样的。比如说,当 WDGTB 为 0 的时候,最小超时值能精确到 113 微秒,这说明窗口看门狗的计量精度很高,这也是它被叫做“警犬”的原因之一。

思维导图

image-20250115160547471

9独立看门狗案例_需求描述和寄存器介绍

在嵌入式系统开发中,看门狗是保障系统稳定运行的重要组件。今天咱们就以独立看门狗为例,详细讲讲它在实际项目中的应用,看看如何通过配置让它按照我们的预期工作,保证程序的稳定运行。

案例需求描述

我们的目标是验证独立看门狗是否能按预期产生复位信号。假设正常情况下,程序执行一次需要三秒钟。为了确保程序正常执行,我们要在每次程序执行完毕后,立刻进行喂狗操作。这里我们把独立看门狗的超时时间设置为四秒钟。

在正常情况下,程序每三秒执行完并喂狗,不会触发复位信号。但要是环境变得糟糕,程序执行时间延长,一旦超过四秒钟还没喂狗,独立看门狗就会产生复位信号,让程序重新运行。

为了模拟程序执行时间延长的异常情况,我们增加一个按键。当按键被按下时,在程序执行完到喂狗之前,插入一个三秒钟的延长操作。每次按下按键,程序执行时间就会延长三秒。这样一来,若延长后再喂狗,必然会超时,四秒钟一到,程序就会被独立看门狗复位。

举个生活中的例子,我们可以把独立看门狗想象成一个负责检查作业的老师,程序执行就像是学生写作业。正常情况下,学生三秒就能完成作业并交给老师检查(喂狗),老师不会有意见。但如果学生因为外界干扰(按键按下模拟的环境干扰),写作业时间变长,超过了四秒还没交作业,老师就会让学生重新写(复位程序)。

flow复制

Created with Raphaël 2.3.0 程序启动 执行程序(3秒) 按键是否按下? 延迟3秒 产生复位信号 喂狗操作 程序正常运行 yes no

涉及的寄存器及配置

在实现这个案例的过程中,我们主要会用到独立看门狗的三个寄存器:键寄存器(KR)、预分频寄存器(PR)和重装载寄存器(RLR)。接下来咱们详细看看在代码里如何对它们进行配置。

  1. 启动独立看门狗:首先要启动独立看门狗,这需要向键寄存器(KR)写入0xCCCC。一旦启动,独立看门狗就无法停止,就像启动了一台一旦开启就不停运转的机器。而且有意思的是,当我们向KR写入0xCCCC启动独立看门狗时,它会自动开启内部低速时钟(LSI)。这就好比我们打开了一个设备,这个设备自带的电源(LSI)也会跟着启动。不像之前配置实时时钟(RTC)时,使用外部低速时钟(LSE)还得单独打开LSE。在这里,独立看门狗帮我们自动搞定了时钟的开启。
  2. 打开寄存器写保护:启动看门狗后,我们要对预分频寄存器(PR)和重装载寄存器(RLR)进行配置,在此之前,需要先打开它们的写保护。方法是向键寄存器(KR)写入0x5555,这就像给寄存器的大门打开了一把锁,允许我们对里面的内容进行修改。
  3. 计算并配置预分频系数和重装载值:我们设定独立看门狗的超时时间为四秒钟,而超时时间与时钟频率、预分频系数以及重装载寄存器的值有关,计算公式为:超时时间 = (预分频系数 ×(重装载寄存器值 + 1))÷ 时钟频率。已知时钟频率采用LSI,为40kHz(即40000Hz),我们要让超时时间等于四秒钟,将数据代入公式可得:预分频系数 ×(重装载寄存器值 + 1) = 40000 × 4 = 160000。预分频系数的取值范围是4到256,重装载寄存器(RLR)是12位,最大值为4095,加1后为4096。如果预分频系数取最小值4,那么重装载寄存器值 + 1就需要达到40000,但重装载寄存器的最大值无法满足,所以我们要根据重装载寄存器的最大值来推算预分频系数的最小值。用160000除以4096,结果约为39.0625,所以预分频系数最小约为39.0625,小于这个值就无法满足四秒钟的超时时间设定。考虑到取值的合理性,我们取预分频系数为64,此时重装载寄存器值为160000 ÷ 64 - 1 = 2499。需要注意的是,预分频寄存器(PR)中写入的值并非预分频系数本身,而是对应预分频系数的编码。64分频对应的编码是4,所以我们向PR寄存器写入4,向重装载寄存器(RLR)写入2499,这样就完成了寄存器的配置,实现了四秒钟的超时时间设定。
  4. image-20250115191723440

flow复制

Created with Raphaël 2.3.0 开始配置 向KR写入0xCCCC启动看门狗 向KR写入0x5555打开写保护 根据超时时间计算预分频和重装载值 向PR写入4 向RLR写入2499 配置完成

通过以上配置,我们就能在代码中利用独立看门狗监控程序的运行过程。程序正常执行完后进行喂狗操作,同时在程序中插入按键判断逻辑,一旦按键按下,程序执行时间延长,导致喂狗超时,独立看门狗就会产生复位信号,从而验证了独立看门狗的功能。

image-20250115193414148

思维导图

image-20250115172439655

10独立看门狗案例_课堂练习_IWDG唤醒待机模式

在嵌入式系统开发中,独立看门狗除了能保障程序稳定运行,还能在低功耗模式的唤醒机制中发挥重要作用。之前我们学习了低功耗模式里待机模式的多种唤醒方式,今天咱们就聚焦于独立看门狗,看看它是如何将STM32从待机模式中唤醒的,把这个当作一个扩展的课堂练习。

工程搭建与准备

咱们的第一步,是搭建好测试工程。这里的操作比较简单,先把之前的代码复制一份,原工程名假设是“EX02”,我们把复制后的工程重命名为“IWDGstandbyregister”,这个名字代表我们要用独立看门狗(IWDG)来唤醒待机(standby)模式。接着,把工程里一些暂时用不上的文件删掉。

在主函数“main.c”的处理流程上,我们可以借鉴之前RTC闹钟唤醒或者PA0引脚唤醒待机模式的代码。这里我们选择“alarmstandbyregister”工程里的“main.c”,把它复制到当前案例目录下,直接替换掉原来的“main.c”文件。在工程配置方面,由于这次没有新文件添加,我们只需直奔调试(debug)相关设置进行修改就好。完成这些后,我们就在VScode中打开工程,准备修改核心代码。

主函数代码修改

打开工程后,进入主函数进行修改。首先,待机模式的进入函数肯定还是要定义的,不过之前的初始化函数得改成独立看门狗的初始化函数,即“IWDG_Init”。

接着,之前代码中对唤醒标志位的判断部分可以删掉。大家回忆一下,在手册里我们了解到,只有PA0作为唤醒引脚产生唤醒事件,或者RTC闹钟事件发生时,唤醒标志位(WUF)才会置1。而独立看门狗与这个标志位没有关系,不会将其置1,所以这部分判断代码就没用啦,直接删掉。

然后,就是使用独立看门狗来唤醒待机模式的关键部分。之前我们通过开启LED灯模拟正常程序执行过程,延迟两秒,这部分保留。但之前延迟三秒再进入待机模式的代码得改。因为我们设定的独立看门狗超时时间是四秒,如果还按原来的延迟三秒,可能还没进入待机模式,独立看门狗就超时触发复位了,这就达不到唤醒待机模式的目的,而是让程序复位重启了。所以,我们把延迟三秒进入待机模式的代码删掉,直接在延迟两秒后进入待机模式。进入待机模式前,我们先延迟一毫秒,并且不需要设置闹钟了,直接进入待机模式等待独立看门狗的复位信号来唤醒。

最后,进入待机模式的函数设置步骤要保留,包括设置深睡眠模式、将PDES置为1 ,以及使用WFI进入待机模式。另外,由于我们配置电源(PWR)相关寄存器时需要开启PWR时钟,之前使用RTC闹钟时在其他地方做过时钟开启配置,现在那些代码删掉了,所以在这里要补上。通过“RCC_APB1ENR |= RCC_APB1ENR_PWREN;”这条语句开启PWR时钟。

Created with Raphaël 2.3.0 开始 复制工程并改名 删除无关文件 复制原main.c文件替换当前文件 在VScode中打开工程 修改主函数 - 定义待机进入函数和IWDG初始化函数 - 删除标志位判断代码 - 修改进入待机模式前的延迟和设置 - 保留并补充进入待机模式的函数设置 编译工程 烧写程序 测试独立看门狗唤醒待机模式 结束

代码调试与测试

代码修改完后,我们进行编译。编译时发现报错,检查后发现是之前代码中与按键相关的“flag”定义在“k.c”里,而我们这次测试用不到按键,所以把与按键相关的代码注释掉,包括中断服务程序。再次编译,没有错误后进行烧写。

烧写完成后,我们就可以看到测试效果。开发板上的LED灯每隔两秒钟就会变亮,亮两秒后灭掉,如此循环。这是因为程序先执行两秒模拟正常运行,然后进入待机模式,独立看门狗超时时间为四秒,减去前面正常运行的两秒,再过两秒独立看门狗就会产生复位信号,将STM32从待机模式中唤醒,从而形成了两秒待机、两秒唤醒的周期性变化过程,成功实现了独立看门狗唤醒待机模式的功能。

思维导图

通过这个简单的测试,我们掌握了独立看门狗唤醒待机模式的方法。在实际的嵌入式项目中,合理运用独立看门狗的这个特性,可以更好地管理系统功耗,提高系统的灵活性和稳定性。大家在学习过程中,可以多尝试对代码进行修改和优化,加深对独立看门狗和低功耗模式的理解。

image-20250115204206797

image-20250115194310274

11独立看门狗唤醒待机模式:课堂练习案例剖析

在嵌入式开发的低功耗领域中,待机模式是一种常用的降低系统功耗的方式,而如何唤醒处于待机模式的系统是一个关键问题。我们知道,STM32微控制器待机模式有四种唤醒方式,其中独立看门狗复位也是唤醒待机模式的一种方法。接下来,我们就通过一个课堂练习,来深入了解独立看门狗是如何唤醒待机模式的。

工程搭建与准备

工程复制与重命名

我们以之前的工程为基础进行本次练习。先将名为“EX02”的工程复制一份,然后把复制后的工程名修改为“IWDJstandbyregister”。这样做的好处是,我们可以利用之前工程的一些基础设置,减少重复劳动,快速进入到本次功能的开发中。

文件清理与代码替换

将新工程中不必要的文件删掉,减轻工程负担,使结构更加清晰。然后,找到之前使用RTC闹钟唤醒待机模式的工程(比如“alarmstandbyregister”工程)中的main.c文件,将其复制到当前案例目录下,直接替换掉新工程中的main.c文件。这一步就像是我们搭建房子时,先搬来一个类似的框架,后续再根据需求进行改造。

工程配置与代码修改

工程配置

本次实验不需要添加任何新文件,我们只需进入工程的debug选项进行一些修改即可。虽然文档中没有详细说明具体修改内容,但这一步往往是为了确保工程在调试过程中能够按照我们预期的方式运行。

主函数代码修改

  • 定义待机模式进入函数与初始化独立看门狗:和之前待机模式唤醒流程一样,我们要定义待机模式进入函数,同时将独立看门狗初始化函数命名为IWDG_Init。在iwdg.c文件中,IWDG_Init函数的实现如下:

c复制

// 初始化
void IWDG_Init(void)
{
    // 1. 开启看门狗
    IWDG->KR = 0xCCCC;
    // 2. 允许访问寄存器
    IWDG->KR = 0x5555;
    // 3. 设置预分频系数 64 - PR = 100
    IWDG->PR = 4;
    // 4. 设置重装载值,2499
    IWDG->RLR = 2499;
    // 5. 更新计数器的值(喂狗)
    IWDG_Refresh();
}

这个函数首先开启看门狗,接着允许访问相关寄存器,然后设置预分频系数为64(通过设置IWDG->PR = 4实现),重装载值为2499 ,最后进行喂狗操作。预分频系数和重装载值决定了看门狗的溢出时间,这里设置的溢出时间大约为4秒(具体时间可根据时钟频率计算得出)。

  • 删除不必要的标志位判断代码:在之前使用PA0引脚唤醒或RTC闹钟唤醒的代码中,需要判断WUF标志位,但对于独立看门狗唤醒待机模式来说,WUF标志位与独立看门狗无关,所以这部分判断标志位的代码可以直接删掉。这就好比我们在做新菜时,发现之前菜谱里的某些步骤不适用,直接去掉就好。
  • 调整进入待机模式的代码逻辑:原本进入待机模式前有3秒延迟,由于独立看门狗超时时间只有4秒,如果不调整,可能还没进入待机模式,系统就因看门狗超时复位重启了,达不到唤醒待机模式的目的。所以我们直接删除这3秒延迟的代码,在开启LED灯模拟正常程序执行2秒后,直接进入待机模式。进入待机模式前,还需要开启PWR时钟,代码如下:

c复制

RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);

这行代码通过RCC_APB1PeriphClockCmd函数开启PWR时钟,确保在配置PWR相关寄存器时,时钟是正常工作的。之后,通过设置深睡眠模式、将PDES置为1 ,最后使用WFI指令进入待机模式。

实验效果验证

完成代码修改后,我们对工程进行编译。编译过程中,如果发现有错误,比如原本代码中与按键相关的flag在本次实验中不再使用,我们可以将与按键相关的代码注释掉,包括中断服务程序等。再次编译无误后,将程序烧写到开发板中。

烧写成功后,我们可以观察到开发板上的LED灯每隔2秒钟变亮一次,然后熄灭进入待机模式,再过2秒钟又被独立看门狗唤醒,LED灯再次变亮,如此循环。这表明我们成功实现了独立看门狗唤醒待机模式的功能,系统按照我们预期的方式,在正常运行与待机模式之间周期性切换。

通过这个实验,我们深入了解了独立看门狗在唤醒待机模式方面的应用,这不仅丰富了我们在低功耗系统设计中的手段,也让我们对STM32微控制器的功能有了更全面的认识。在实际项目中,合理运用独立看门狗唤醒待机模式,可以在保证系统功能的同时,有效降低功耗,提高系统的整体性能。

总结思维导图

image-20250116102039206

12基于HAL库的独立看门狗案例实现

在嵌入式开发中,看门狗是一个非常重要的功能,它可以监控系统的运行状态,当系统出现异常时,自动复位系统,保证系统的稳定性。前面我们使用寄存器方式实现了独立看门狗的测试案例,接下来我们将使用HAL库的方式再次实现,相比寄存器方式,HAL库方式配置过程更加简便。下面我们就一步步来看具体的实现过程。

工程创建与基础配置

创建新工程

打开相关开发工具(文中提到的STM32CubeMX),在收藏夹里选择STM32F103Z76创建一个新的工程。这是整个项目的起点,就好比我们要建造一座房子,首先得选好一块地,在这里就是确定我们要基于哪个芯片来开发。

调试与时钟配置

在SYS中选择单线调试,这样方便我们后续对程序进行调试。RCC部分选择外部晶振HSE作为时钟源,并将HSE进行9倍频得到72MHz的系统时钟。同时,APB1进行二分频,得到36MHz的时钟。这就像是给整个系统确定了一个运行的“节奏”,不同的外设可能需要不同频率的时钟来保证其正常工作。另外,由于我们当前没有用到RTC,所以这部分先不用管。

串口配置

因为我们需要串口打印输出信息,所以要对串口一进行配置。选择异步串口模式,波特率设置为115200。这样我们就可以通过串口在电脑端看到程序运行过程中输出的各种信息,方便我们了解程序的运行状态。

按键配置

为了模拟程序的异常执行,我们添加一个按键。这里选择PF10引脚,并将其定义为中断引脚,触发方式为上升沿触发。同时,设置下拉电阻,这样在按键未按下时,PF10引脚为低电平,按下按键时,引脚变为高电平,产生上升沿中断。给这个按键对应的用户标签命名为“k”,方便我们在程序中识别和使用。

独立看门狗配置

独立看门狗位于System Core模块中,因为它要监控整个系统的正常执行。打开IWDG选项,勾选启动它。然后配置两个关键参数,预分频系数选择64,重装载值设置为2499。预分频系数和重装载值决定了看门狗的溢出时间,通过合理设置这两个值,可以确保在系统正常运行时,程序能够及时喂狗,避免看门狗溢出复位系统。

NVIC配置

由于PF10按键会产生中断,所以还需要在NVIC中使能EXTI line15 - 10中断。这里因为代码中没有用到消抖延时,并且与SysTick中断没有关系,所以中断优先级可以保持默认值不变。

生成代码

完成上述所有配置后,我们就可以生成代码了。工程名字借鉴之前寄存器实现的案例,命名为“二十八IWDKtestHAL”,选择工具链为MDK - ARM,然后点击生成代码按钮,开发工具会根据我们的配置自动生成相应的代码框架。

工程设置与代码编写

工程设置

打开生成的工程,进入魔法棒设置界面。因为有串口打印输出,所以要勾选Use MicroLIB。同时,在Debug Settings中,将Reserved Row的Enable勾去掉,这样可以避免一些不必要的调试信息干扰。并且,由于我们不需要添加额外的文件,所以工程结构保持不变。

添加fputc函数

在使用串口打印输出时,需要在usart.c文件中添加fputc函数。如果觉得麻烦,也可以直接复制之前代码中的相关部分。在usart.c文件的最下边实现fputc函数,代码如下:

c复制

int fputc(int ch, FILE *f)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 1000);
    return ch;
}

这个函数的作用是将字符通过串口发送出去,当我们在程序中使用printf函数时,就会调用这个fputc函数来实现串口输出。

主函数编写

在主函数中,初始化部分已经由生成的代码完成。我们首先打印一句“尚硅谷独立看门狗实验:超时复位…”,用于提示实验开始。然后进入主循环,在循环中模拟正常程序运行,每隔3秒打印“程序正常执行…”,并调用HAL_Delay(3000)函数进行延时。如果按键按下(通过全局变量flag判断),则再增加3秒延迟,并将flag置0。最后,在每次循环中调用HAL_IWDG_Refresh(&hiwdg)函数进行喂狗操作,并打印“程序正常执行,喂狗成功”。代码如下:

c复制

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_IWDG_Init();
    MX_USART1_UART_Init();

    printf("尚硅谷独立看门狗实验:超时复位...\n");

    while (1)
    {
        printf("程序正常执行........\n");
        HAL_Delay(3000);
        if (flag)
        {
            flag = 0;
            HAL_Delay(3000);
        }
        HAL_IWDG_Refresh(&hiwdg);
        printf("程序正常执行,喂狗成功\n");
    }
}
中断回调函数编写

在IT.c文件中,EXTI15 - 10的中断处理函数会调用HAL库的GPIO EXTI的handler,最后会执行一个回调函数。我们在主函数下方实现这个回调函数HAL_GPIO_EXTI_Callback,当按键按下产生中断时,将flag置1,并打印“按键按下 增加3s延迟”。代码如下:

c复制

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    flag = 1;
    printf("按键按下 增加3s延迟\n");
}

实验效果验证

完成代码编写后,我们对程序进行编译,确保代码没有语法错误。然后将程序烧写到开发板中,独立看门狗实验程序开始正常执行。在主循环中,每隔3秒钟,程序会正常执行并喂狗成功,我们可以通过串口输出看到相应的提示信息。当我们按下按键时,程序会增加3秒延迟。如果在这个延迟时间内没有及时喂狗,4秒钟之后,独立看门狗就会超时,产生复位信号,程序重新开始执行。这就验证了我们使用独立看门狗来监控程序状态的功能是正常的。

通过这个基于HAL库的独立看门狗案例,我们可以看到HAL库极大地简化了开发过程,让我们能够更快速地实现功能。在实际开发中,合理运用看门狗可以有效提高系统的稳定性,避免因程序跑飞等异常情况导致系统死机。希望大家通过这个案例,对独立看门狗和HAL库的使用有更深入的理解。

image-20250116101715313

思维导图

image-20250116101447031

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值