前言
...
UINX时间戳定义
UNIX时间戳是一种表示时间的方法,广泛用于计算机系统和网络协议中。它定义的时间起点是1970年1月1日午夜(协调世界时UTC),也就是所谓的“UNIX纪元”开始的时刻。
Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数不考虑闰秒,时间戳是一个计数器数值,从1970年1月1日0时0分0秒开始,到现在总共所经过的秒数,时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量,世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。
UTC与GMT
GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准。
UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致。
时间戳与日历时间转换
在C语言提供的标准库中,time时间模块(time.h),提供了日历时间和时间戳转换的相关函数,在不使用操作系统的逻辑程序中常使用UNIX时间戳实现系统的计时,时间校准等功能,其中需要对获取到的时间做时区和格式的转换。
C语言提供有关时间戳转换的相关函数:
C语言库函数文件中定义好的有关时间戳的格式
部分转换示例函数:
RTC实时时钟
实时时钟 RTC 通常用于日历时钟。 RTC 电路分属于两个电源域。 一部分位于备份域中,该部分包括一个 32 位的累加计数器、一个闹钟、一个预分频器、一个分频器以及 RTC 时钟配置寄存器。这表明系统复位或者从待机模式唤醒时, RTC 的设置和时间都保持不变。另一部分位于VDD 电源域中,该部分只包括 APB 接口以及一组控制寄存器。
RTC 基本硬件结构:
以上包含三个外部时钟,一个是外部高速时钟,一个是外部低速时钟,一个是内部低速时钟,经过RTCCLK实时时钟时候,选择是否对时钟的频率进行分频,包含一个预分频器,分频器对频率进行分频,提供三个中断,分别是秒中断,溢出中断(溢出中断由CNT计数器计数溢出后产生,CNT最大的计数范围是65535个),提供32位闹钟ALRM为系统提供闹钟中断,最后经中断输出控制,进入NVIC(嵌套中断向量控制器,配置通道通道中断优先级,开启中断)。
RTC 基本硬件结构
以上是STM32RTC硬件结构程序框图与GD32大致相同。
RTC 主要特征
32位可编程计数器,用于计数运行时间
可编程的预分频器: 分频系数最高可达 220
独立时钟域:
A) PCLK1 时钟域
B) RTC 时钟域(该时钟必须比 PCLK1 时钟至少慢 4 倍)
RTC 时钟源:
A) HXTAL 时钟除以 128
B) LXTAL 振荡电路时钟
C) IRC40K 振荡电路时钟
可屏蔽的中断源:
A) 闹钟中断
B) 秒中断
C) 溢出中断
RTC 时钟源
在GD32这款MCU中RTC(实时时钟)一共有三个时钟源分别如下所示:
(A) HXTAL 时钟除以 128
(B) LXTAL 振荡电路时钟
(C) IRC40K 振荡电路时钟
BKP 备份寄存器
备份寄存器可在 VDD 电源关闭时由 VBAT 供电,备份寄存器有 42 个16 位(84字节)寄存器可用来存储并保护用户应用数据,从待机模式唤醒或系统复位也不会对这些寄存器造成影响。
此外,BKP 寄存器也可实现侵入检测和 RTC 校准功能。
在复位之后,任何对备份域寄存器的写操作都将被禁止,也就是说,备份寄存器和 RTC 不允许写访问。为使能对备份寄存器和 RTC的写访问,首先通过设置 RCU APB1EN 寄存器的PMUEN 和 BKPIEN 位来打开电源和备份接口时钟,然后再通过设置 PMU CTL 寄存器的BKPWEN 位来使能对备份域中寄存器的写访问。
RTC 复位
APB 接口和 RTC_INTEN 寄存器会随着系统复位进行复位。 RTC 内核(预分频器、分频器、计数器以及闹钟)只会随备份域复位进行复位。通过下面的步骤,可以在复位后访问备份域寄存器以及 RTC 寄存器:
1.通过对 RCU_APB1EN 寄存器中的 PMUEN 和 BKPEN 位进行置位,使能电源以及备份接口时钟。
2.通过对 PMU_CTL 中的 BKPWEN 位进行置位,使能对备份域寄存器和 RTC 的访问。
RTC 读取
APB 接口和 RTC 内核分属于两个不同电源域。
在 RTC 内核中,只有计数器和分频器寄存器为可读寄存器。这两个寄存器的值以及 RTC 标志会在每个 RTC 时钟的上升沿进行内部更新,并与 APB1 时钟进行重新同步。
当 APB 接口从禁用状态使能后,建议不要立即进行读操作,因为这些寄存器的首次内部更新可能尚未完成。这表明,在系统复位、电源复位、从待机/深度睡眠模式下唤醒时, APB 接口是被禁用的,但是 RTC 内核仍然保持运行。在这类情况下,正确的读操作应该先将 RTC_CTL寄存器的 RSYNF 清零并等待其被硬件置位。 WFI 和 WFE 指令对于 RTC 的 APB 接口没有影响
栈,堆,静态区存储
栈存储、静态区存储和堆存储是程序中不同类型的内存分配方式。它们各自有不同的特点和生命周期。下面是这三种存储方式的概述:
1. 栈存储 (Stack Storage)
- 用途: 用于存储局部变量和函数调用时的临时数据。
- 特点:
- 栈上的数据由编译器自动管理。
- 分配速度快,因为栈空间是预先分配好的。
- 栈空间有限,不适合存储大量数据。
- 栈上的数据具有固定的生存期,当函数退出时,这些数据会被自动销毁。
- 生命周期: 栈上的数据在其作用域内存在,当作用域结束时,数据被销毁。
2. 静态区存储 (Static Storage)
- 用途: 用于存储全局变量、静态局部变量以及常量。
- 特点:
- 静态区的数据在整个程序运行期间存在。
- 编译器会在程序启动前分配静态存储空间。
- 静态局部变量仅在其声明的函数内部可见,但其生命周期贯穿整个程序运行过程。
- 全局变量在整个程序中可见。
- 常量通常存储在只读的静态存储区域。
- 生命周期: 静态区的数据在程序启动时创建,在程序结束时销毁。
3. 堆存储 (Heap Storage)
- 用途: 用于动态分配的大块内存。
- 特点:
- 堆空间是在程序运行时动态分配的。
- 分配速度较慢,因为需要搜索合适的内存块。
- 堆空间相对较大,适合存储大量数据。
- 堆上的数据由程序员手动管理,需要显式地分配和释放。
- 堆空间可能会产生内存碎片。
- 生命周期: 堆上的数据由程序员控制,一般通过
malloc
,calloc
,realloc
,free
(C) 或new
,delete
(C++) 进行分配和释放。
代码案例:
#include <stdio.h>
#include <stdlib.h>
void func() {
int stack_var = 1; // 栈存储
static int static_var = 2; // 静态区存储
int *heap_var = malloc(sizeof(int)); // 堆存储
*heap_var = 3;
printf("Stack var: %d\n", stack_var);
printf("Static var: %d\n", static_var);
printf("Heap var: %d\n", *heap_var);
free(heap_var); // 释放堆上的内存
}
int main() {
func();
func(); // 注意静态变量的值不会重置
return 0;
}
注:在这个示例中,stack_var
存储在栈上,每个函数调用时都会重新分配;static_var
存储在静态区,它在两次函数调用间保持相同的值;heap_var
存储在堆上,需要手动分配和释放。
UNIX 时间戳验证
验证采用调用C语言提供的库函数time.h文件的方式,通过时间戳的获取显示验证程序的执行结果为后续的学习做好铺垫。
引入库函数文件:
#include <time.h>
#include <stdlib.h>
main 函数程序:
1.0 版:
int main(void)
{
DrvInit();
AppInit();
// 定义一个时间戳的变量
time_t timeStamp = 100000000;
// 定义一个结构体变量
struct tm* timeInfo = NULL;
// 将时间戳的秒数转换为时间
timeInfo = gmtime(&timeStamp);
printf
(
"gmtime, %d-%d-%d %d:%d:%d\n",
timeInfo->tm_year + 1900,
timeInfo->tm_mon + 1,
timeInfo->tm_mday,
timeInfo->tm_hour,
timeInfo->tm_min,
timeInfo->tm_sec
);
timeInfo = localtime(&timeStamp);
printf
(
"localtime, %d-%d-%d %d:%d:%d\n",
timeInfo->tm_year + 1900,
timeInfo->tm_mon + 1,
timeInfo->tm_mday,
timeInfo->tm_hour,
timeInfo->tm_min,
timeInfo->tm_sec
);
while (1)
{
TaskHandler();
}
}
将获取到的UNIX时间戳转换为指定的格式
2.0 版:
int main(void)
{
DrvInit();
AppInit();
// 定义一个时间戳的变量
time_t timeStamp = 1000000000;
// 定义一个结构体变量
struct tm* timeInfo = NULL;
// 将时间戳的秒数转换为时间
// timeInfo = gmtime(&timeStamp);
// printf
// (
// "gmtime, %d-%d-%d %d:%d:%d\n",
// timeInfo->tm_year + 1900,
// timeInfo->tm_mon + 1,
// timeInfo->tm_mday,
// timeInfo->tm_hour,
// timeInfo->tm_min,
// timeInfo->tm_sec
// );
timeInfo = localtime(&timeStamp);
printf
(
"localtime, %d-%d-%d %d:%d:%d\n",
timeInfo->tm_year + 1900,
timeInfo->tm_mon + 1,
timeInfo->tm_mday,
timeInfo->tm_hour,
timeInfo->tm_min,
timeInfo->tm_sec
);
printf("%s\n",asctime(timeInfo));
//另外一个接口函数
char timeArr[80];
// 根据我们传入的格式解析结构体里面的成员
strftime(timeArr,80,"%Y-%m-%d %H:%M:%S",timeInfo);
printf("%s\n",timeArr);
while (1)
{
TaskHandler();
}
}
获取本地戳转换为对应格式
3.0 版:
int main(void)
{
DrvInit();
AppInit();
// 定义一个时间戳的变量
time_t timeStamp = 1000000000;
// 定义一个结构体变量
struct tm* timeInfo = NULL;
// timeInfo = Test();
// printf("address of timeInfo is 0x%p\n",timeInfo);
// timeInfo = Test();
// printf("address of timeInfo is 0x%p\n",timeInfo);
// 将时间戳的秒数转换为时间
// timeInfo = gmtime(&timeStamp);
// printf
// (
// "gmtime, %d-%d-%d %d:%d:%d\n",
// timeInfo->tm_year + 1900,
// timeInfo->tm_mon + 1,
// timeInfo->tm_mday,
// timeInfo->tm_hour,
// timeInfo->tm_min,
// timeInfo->tm_sec
// );
timeInfo = localtime(&timeStamp);
printf("address of timeInfo is 0x%p\n",timeInfo);
timeInfo = localtime(&timeStamp);
printf("address of timeInfo is 0x%p\n",timeInfo);
printf
(
"localtime, %d-%d-%d %d:%d:%d\n",
timeInfo->tm_year + 1900,
timeInfo->tm_mon + 1,
timeInfo->tm_mday,
timeInfo->tm_hour,
timeInfo->tm_min,
timeInfo->tm_sec
);
printf("%s\n",asctime(timeInfo));
//另外一个接口函数
char timeArr[80];
// 根据我们传入的格式解析结构体里面的成员
strftime(timeArr,80,"%Y-%m-%d %H:%M:%S",timeInfo);
printf("%s\n",timeArr);
while (1)
{
TaskHandler();
}
}
程序的执行结果:
两次打印输出程序的打印输出的地址是一致的,在malloc等函数开辟栈堆空间中的地址存储的位置是一致的,使用堆进行存储需要手动的分配和释放,而使用栈存储的地址,存储数据的地址空间是不一致的。
简单来讲就是静态局部变量指针函数内存管理的区别
RTC 初始化
RTC 软件架构
RTC API接口及数据结构定义
RTC 驱动程序
初始化RTC时钟
// 初始化RTC接口函数
void RtcDrvInit(void)
{
if(bkp_read_data(BKP_DATA_0) != MAGIC_CODE)
{
// 使能RTC访问,使能PMU和BKP时钟
rcu_periph_clock_enable(RCU_PMU);
// 使能BKP时钟(后备寄存器时钟)
rcu_periph_clock_enable(RCU_BKPI);
// 使能对后备寄存器和RTC的写权限
pmu_backup_write_enable();
// 复位后备寄存器
bkp_deinit();
// 使能低速寄存器等待其稳定
rcu_osci_on(RCU_LXTAL);
rcu_osci_stab_wait(RCU_LXTAL);
// 设置rcc的时钟输入源,选择振荡电路时钟
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL);
// 使能RTC时钟
rcu_periph_clock_enable(RCU_RTC);
// 等待APB1接口时钟和RTC时钟同步
rtc_register_sync_wait();
// 等待上次对rtc写操作寄存器操作完成
rtc_lwoff_wait();
// 设置分频值32767。16位最大就是32767
rtc_prescaler_set(32767);
// 等待上次对RTC寄存器写操作完成
rtc_lwoff_wait();
// 设置时间,将计数寄存器里面的时间设置为0,表示为1970-01-01 0:0:0
rtc_counter_set(1722160304);
// 后备寄存器,将数据写入后备寄存器,掉电不丢失
bkp_write_data(BKP_DATA_0,MAGIC_CODE);
return;
}
// 不是第一次初始化设置时钟,使能写权限
// 使能RTC访问,使能PMU和BKP时钟
rcu_periph_clock_enable(RCU_PMU);
// 使能BKP时钟
rcu_periph_clock_enable(RCU_BKPI);
// 使能对后备寄存器和RTC的写权限
pmu_backup_write_enable();
// 等待APB1接口时钟和RTC时钟同步
rtc_register_sync_wait();
// 等待上次对rtc写操作寄存器操作完成
rtc_lwoff_wait();
}
获取和接收时间接口函数
这里使用get和set的方式,有点类似于java当中的get和set方法,
// 设置时间和获取时间的接口函数
void SetRtcTime(RtcTime_t *time)
{
// 设置时间
time_t timeStamp;
struct tm tmInfo;
tmInfo.tm_year = time->year - 1900;
tmInfo.tm_mon = time->month - 1;
tmInfo.tm_mday = time->day;
tmInfo.tm_hour = time->hour;
tmInfo.tm_min = time->minute;
tmInfo.tm_sec = time->second;
// 转换为时间戳,减去8个小时的时间偏差
// mktime 和localtime转换成的都是0时区的时间
timeStamp = mktime(&tmInfo) - 8 * 60 * 60;
// 将时间戳保存在计数寄存器的位置
// 获取标志等待上次写入完成
rtc_lwoff_wait();
// 保存在计数寄存器里面
rtc_counter_set(timeStamp);
}
// 获取时间的接口函数
void GetRtcTime(RtcTime_t *time)
{
// 获取时间的接口函数
time_t timeStamp;
struct tm* tmInfo;
//加上北京时间的偏差
timeStamp = rtc_counter_get() + 8 * 60 * 60;
tmInfo = localtime(&timeStamp);
time->year = tmInfo->tm_year + 1900;
time->month = tmInfo->tm_mon + 1;
time->day = tmInfo->tm_mday;
time->hour = tmInfo->tm_hour;
time->minute = tmInfo->tm_min;
time->second = tmInfo->tm_sec;
}
完整代码
#include <stdint.h>
#include <string.h>
#include <time.h>
#include "gd32f30x.h"
#include "rtc_drv.h"
#define MAGIC_CODE 0x5A5A
// 初始化RTC接口函数
void RtcDrvInit(void)
{
if(bkp_read_data(BKP_DATA_0) != MAGIC_CODE)
{
// 使能RTC访问,使能PMU和BKP时钟
rcu_periph_clock_enable(RCU_PMU);
// 使能BKP时钟(后备寄存器时钟)
rcu_periph_clock_enable(RCU_BKPI);
// 使能对后备寄存器和RTC的写权限
pmu_backup_write_enable();
// 复位后备寄存器
bkp_deinit();
// 使能低速寄存器等待其稳定
rcu_osci_on(RCU_LXTAL);
rcu_osci_stab_wait(RCU_LXTAL);
// 设置rcc的时钟输入源,选择振荡电路时钟
rcu_rtc_clock_config(RCU_RTCSRC_LXTAL);
// 使能RTC时钟
rcu_periph_clock_enable(RCU_RTC);
// 等待APB1接口时钟和RTC时钟同步
rtc_register_sync_wait();
// 等待上次对rtc写操作寄存器操作完成
rtc_lwoff_wait();
// 设置分频值32767。16位最大就是32767
rtc_prescaler_set(32767);
// 等待上次对RTC寄存器写操作完成
rtc_lwoff_wait();
// 设置时间,将计数寄存器里面的时间设置为0,表示为1970-01-01 0:0:0
rtc_counter_set(1722160304);
// 后备寄存器,将数据写入后备寄存器,掉电不丢失
bkp_write_data(BKP_DATA_0,MAGIC_CODE);
return;
}
// 不是第一次初始化设置时钟,使能写权限
// 使能RTC访问,使能PMU和BKP时钟
rcu_periph_clock_enable(RCU_PMU);
// 使能BKP时钟
rcu_periph_clock_enable(RCU_BKPI);
// 使能对后备寄存器和RTC的写权限
pmu_backup_write_enable();
// 等待APB1接口时钟和RTC时钟同步
rtc_register_sync_wait();
// 等待上次对rtc写操作寄存器操作完成
rtc_lwoff_wait();
}
// 设置时间和获取时间的接口函数
void SetRtcTime(RtcTime_t *time)
{
// 设置时间
time_t timeStamp;
struct tm tmInfo;
tmInfo.tm_year = time->year - 1900;
tmInfo.tm_mon = time->month - 1;
tmInfo.tm_mday = time->day;
tmInfo.tm_hour = time->hour;
tmInfo.tm_min = time->minute;
tmInfo.tm_sec = time->second;
// 转换为时间戳,减去8个小时的时间偏差
// mktime 和localtime转换成的都是0时区的时间
timeStamp = mktime(&tmInfo) - 8 * 60 * 60;
// 将时间戳保存在计数寄存器的位置
// 获取标志等待上次写入完成
rtc_lwoff_wait();
// 保存在计数寄存器里面
rtc_counter_set(timeStamp);
}
// 获取时间的接口函数
void GetRtcTime(RtcTime_t *time)
{
// 获取时间的接口函数
time_t timeStamp;
struct tm* tmInfo;
//加上北京时间的偏差
timeStamp = rtc_counter_get() + 8 * 60 * 60;
tmInfo = localtime(&timeStamp);
time->year = tmInfo->tm_year + 1900;
time->month = tmInfo->tm_mon + 1;
time->day = tmInfo->tm_mday;
time->hour = tmInfo->tm_hour;
time->minute = tmInfo->tm_min;
time->second = tmInfo->tm_sec;
}
头文件代码
在头文件中创建了一个结构体,内部包含时间的各个参数方便后续使用。
#ifndef _RTC_DRV_H_
#define _RTC_DRV_H_
#include <stdint.h>
typedef struct {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
} RtcTime_t;
/**
***********************************************************
* @brief RTC硬件初始化
* @param
* @return
***********************************************************
*/
void RtcDrvInit(void);
/**
***********************************************************
* @brief 设置时间
* @param time,输入,日历时间
* @return
***********************************************************
*/
void SetRtcTime(RtcTime_t *time);
/**
***********************************************************
* @brief 获取时间
* @param time,输出,日历时间
* @return
***********************************************************
*/
void GetRtcTime(RtcTime_t *time);
#endif
#include <stdint.h>
#include <stdio.h>
#include "rtc_drv.h"
/**
***********************************************************
* @brief 人机交互任务处理函数
* @param
* @return
***********************************************************
*/
void HmiTask(void)
{
// 这是一个测试函数,获取系统的时间将时间打印出来
RtcTime_t rtcTime;
GetRtcTime(&rtcTime);
printf("%d-%02d-%02d %02d:%02d:%02d\n", rtcTime.year, rtcTime.month, rtcTime.day,
rtcTime.hour,rtcTime.minute, rtcTime.second);
}
main 函数部分代码
此次使用逻辑程序调度框架,在仅仅使用逻辑的情况下,实现驱动层和应用层的分离,在结构上实现程序的独立性,使用回调函数的方式让下层的程序间接的调用上层的应用代码。
初始化时钟接口函数
RtcDrvInit(); 这段代码用于初始化时钟接口函数
static void DrvInit(void)
{
SystickInit();
LedDrvInit();
KeyDrvInit();
DelayInit();
Usb2ComDrvInit();
// 时钟初始化接口函数
RtcDrvInit();
}
任务转换代码满足以上条件后指向对应的函数指针指向的函数
static TaskComps_t g_taskComps[] =
{
{0, 1000, 1000, HmiTask},
/* 添加业务功能模块 */
};
main 函数完整代码
#include <stdint.h>
#include <stdio.h>
#include "led_drv.h"
#include "key_drv.h"
#include "systick.h"
#include "usb2com_drv.h"
#include "rtc_drv.h"
#include "delay.h"
#include "usb2com_app.h"
#include "hmi_app.h"
typedef struct
{
uint8_t run; // 调度标志,1:调度,0:挂起
uint16_t timCount; // 时间片计数值
uint16_t timRload; // 时间片重载值
void (*pTaskFuncCb)(void); // 函数指针变量,用来保存业务功能模块函数地址
} TaskComps_t;
static TaskComps_t g_taskComps[] =
{
{0, 1000, 1000, HmiTask},
/* 添加业务功能模块 */
};
#define TASK_NUM_MAX (sizeof(g_taskComps) / sizeof(g_taskComps[0]))
static void TaskHandler(void)
{
for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
{
if (g_taskComps[i].run) // 判断时间片标志
{
g_taskComps[i].run = 0; // 标志清零
g_taskComps[i].pTaskFuncCb(); // 执行调度业务功能模块
}
}
}
/**
***********************************************************
* @brief 在定时器中断服务函数中被间接调用,设置时间片标记,
需要定时器1ms产生1次中断
* @param
* @return
***********************************************************
*/
static void TaskScheduleCb(void)
{
for (uint8_t i = 0; i < TASK_NUM_MAX; i++)
{
if (g_taskComps[i].timCount)
{
g_taskComps[i].timCount--;
if (g_taskComps[i].timCount == 0)
{
g_taskComps[i].run = 1;
g_taskComps[i].timCount = g_taskComps[i].timRload;
}
}
}
}
static void DrvInit(void)
{
SystickInit();
LedDrvInit();
KeyDrvInit();
DelayInit();
Usb2ComDrvInit();
// 时钟初始化接口函数
RtcDrvInit();
}
static void AppInit(void)
{
TaskScheduleCbReg(TaskScheduleCb);
}
int main(void)
{
DrvInit();
AppInit();
//RtcTime_t rtcTime = {2023, 8, 29, 16, 47, 30};
//SetRtcTime(&rtcTime);
while (1)
{
TaskHandler();
}
}
...
后记
...
time - > 2024-7-29
...