江协科技STM32学习笔记(第11章 RTC实时时钟)

第11章 RTC实时时钟

实时时钟本质上是一个定时器,但是这个定时器是专门用来产生年月日时分秒,这种日期和时间信息的。学会了RTC实时时钟,就可以在STM32内部拥有一个独立运行的钟表。想要记录或读取日期和时间,就可以通过操作RTC来实现。RTC这个外设比较特殊,它和备份寄存器BKP、电源控制PWR、这两章关联性比较强,在RTC这一章,BKP和PWR会经常来串门。

11.1 Unix时间戳

11.1.1 Unix时间戳

Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒;

时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量;

世界上所有时区的秒计数器相同,不同时区通过添加偏移来得到当地时间。

11.1.2 UTC/GMT

GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准;

GMT是以前的时间标准,这是因为GMT有一个棘手的问题,就是地球自转一周的时间,其实不是固定的,由于潮汐力、地球活动等原因,地球目前是越来越慢的,这时再根据一天的时间来定义时间基准,这个时间基准就是在不断变化的。

UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致.

11.1.3 时间戳转换

C 标准库 -- | 菜鸟教程 (runoob.com)https://www.runoob.com/cprogramming/c-standard-library-time-h.html

C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换。

|-------------------------------------------------------------------|---------------------|
| 函数 | 作用 |
| time_t time(time_t*); | 获取系统时钟 |
| struct tm* gmtime(const time_t*); | 秒计数器转换为日期时间(格林尼治时间) |
| struct tm* localtime(const time_t*); | 秒计数器转换为日期时间(当地时间) |
| time_t mktime(struct tm*); | 日期时间转换为秒计数器(当地时间) |
| char* ctime(const time_t*); | 秒计数器转换为字符串(默认格式) |
| char* asctime(const struct tm*); | 日期时间转换为字符串(默认格式) |
| size_t strftime(char*, size_t, const char*, const struct tm*); | 日期时间转换为字符串(自定义格式) |

11.2 BKP备份寄存器和RTC实时时钟

11.2.1 BKP备份寄存器

11.2.1.1 BKP简介

BKP(Backup Registers)备份寄存器;

BKP可用于存储用户应用程序数据。当VDD(2.0~3.6V)电源被切断,他们仍然由VBAT(1.8~3.6V)维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位;

VBAT(V Battery):备用电池电源;

上图中VSS_i和VDD_i(i=1~3)是数字部分电路的供电, VSSA和VDDA是内部模拟部分电路的供电。这四组以VDD开头的供电,都是系统的主电源。在正常使用STM32时,这四组供电,全部都需要接到3.3V的电源上。

VBAT:就是备用电池供电引脚,如果要使用STM32内部的BKP和RTC实时时钟,这个引脚就必须接备用电池,用来维持BKP和RTC,在VDD主电源掉电后的供电。这里备用电池只有一根正极的供电引脚,接电池时,电池正极接到VBAT,电池负极和主电源的负极接在一起,共地就行了。如果VDD断电,VBAT也没电,那BKP里的数据就会清零,因为BKP本质上就是RAM存储器,没有掉电不丢失的能力。

TAMPER引脚产生的侵入事件将所有备份寄存器内容清除;

TAMPER是一个接到STM32外部的引脚,是一个安全保障设计,比如做一个安全系数非常高的设备,设备需要有防拆功能,然后BKP里也存储了一些敏感数据,这些数据不能被别人窃取或篡改,那就可以使用这个TAMPER引脚的侵入检测功能。设计电路时,TAMPER引脚可以先加一个默认的上拉或下拉电阻,然后引一根线,到设备外壳的防拆开关或触点,别人一拆开设备,触发开关,就会在TAMPER引脚产生上升沿或者下降沿,这样STM32就检测到侵入事件了,这时BKP的数据就会自动清零,并且申请中断,在中断里,还可以继续保护设备,比如清除其它存储器数据,然后设备锁死,这样来保证设备的安全。另外,主电源断电后,侵入检测任然有效,这样即使设备关机,也能防拆,这就是TAMPER侵入检测的功能。

RTC引脚输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲;

存储RTC时钟校准寄存器;

用户数据存储容量:

20字节(中容量和小容量)/ 84字节(大容量和互联型)。

我们使用的芯片BKP是20字节,BKP一般只能用来存储少量的参数。

11.2.1.2 BKP基本结构

图中橙色部分我们可以叫做后备区域, BKP处于后备区域,但后备区域不止有BKP、还有RTC的相关电路,也位于后备区域,STM32后备区域的特性就是,当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电,当VDD主电源上电时,后备区域供电会由VBAT切换到VDD,也即是主电源有电时,VBAT不会用到,这样可以节省电池电量。BKP是位于后备区域的,BKP主要有数据寄存器、控制寄存器、状态寄存器和RTC实时时钟校准寄存器这些东西。其中数据寄存器是主要部分,用来存储数据的,每个数据寄存器都是16位的,也就是,一个数据寄存器可以存2个字节,那对于中容量和小容量的设备,里面有DR1、DR2,一直到DR10,总共10个数据寄存器。那一个寄存器存2个字节,所以容量是20个字节。对于大容量和互联型设备,里面除了DR1到DR10,还有DR11到DR12,一直到DR42,总共42个数据寄存器,容量是84个字节。

侵入检测,可以从PC13位置的TAMPER引脚引入一个检测信号,当TAMPER产生上升沿或下降沿时,清除BKP所有的内容,以保证安全。

时钟输出可以把RTC的相关时钟,从PC13位置的RTC引脚输出出去,供外部使用,其中输出校准时钟时,再配合校准寄存器,可以对RTC的误差进行校准。

11.2.2 RTC实时时钟

11.2.2.1 RTC简介

RTC(Real Time Clock)实时时钟;

RTC是一个独立的定时器,可为系统提供时钟和日历的功能;

RTC实时时钟,一般就指提供年月日时分秒这种日期时间信息的计时装置。在51单片机时有DS1302这个芯片,DS1302是外置的RTC芯片,这个芯片可以独立计时,我们需要设置时间或读取时间,就通过通信协议向它发送或接收数据来完成。在我们STM32内部,有这个RTC的外设,所以STM32可以在内部直接实现RTC的功能,这样就不用外挂RTC芯片了,当然RTC芯片所必要的元件,比如备用电池、RTC晶振这些东西就要接到STM32上了。

RTC和时钟配置系统处于后备区域,系统复位时数据不清零,VDD(2.0~3.6V)断电后可借助VBAT(1.8~3.6V)供电继续走时;

为了保持时钟能一直连续运行不出错,在主电源断电后,RTC走时肯定不能停下来,在系统复位时,RTC时间值肯定也不能复位。为了实现这些功能,VBAT接上备用电池就是必须的了。主电源断电后,VBAT的电池可以继续维持BKP和RTC的运行。

32位的可编程计数器,可对应Unix时间戳的秒计数器;

这个32位可编程计数器就是对应的是时间戳里的秒计数器,在读取时间时,我们先得到这个秒数,然后使用time.h模块里的localtime函数,就能立刻知道年月日时分秒的信息了。在写入时间时,我们先填充年月日时分秒信息到struct tm结构体,然后用mktime函数,得到秒数,再先写入到32位计数器即可。这样,操作这个秒计数器的思路就不是很清晰了。得益于时间戳的设计,这个硬件电路就得到了极大的简化,要想实现年月日时分秒的计时,只需要一个32位的秒计数器即可。

20位的可编程预分频器,可适配不同频率的输入时钟;

32位的秒计数器,1秒需要自增一次,所以这个地方驱动计数器的时钟,需要是一个1Hz的信号,但是实际提供给RTC模块的时钟,也就是RTCCLK,一般频率都比较高,所以,显然,我们需要在这之间,加一个分频器,给RTCCLK降一下频率,保证分频器输出给计数器的频率位1Hz。为了适配各种频率的RTCCLK,这里就加了一个20位的分频器,可以选择对输入时钟进行1~2^20这么大范围的分频,这样就可以适配不同频率的输入时钟。

可选择三种RTC时钟源:

HSE时钟除以128(通常为8MHz/128)

LSE振荡器时钟(通常为32.768KHz)

LSI振荡器时钟(40KHz)

这三个时钟可以选择其中一个接入到RTCCLK。

高速时钟,一般供内部程序运行和主要外设使用, 低速时钟,一般供RTC、看门狗这些东西使用。我们最常用的是LSE OSC这一路外部32.768KHz的晶振,提供RTCCLK的时钟。第一个原因就是,中间这一路32.768KHz的晶振,本身就是专供RTC使用的,上面这两路其实是有各自的任务,上面这一路,主要作为系统主时钟,下面这一路主要作为看门狗时钟。它们只是顺带备选当作RTC的时钟。另外是只有这一路的时钟,可以通过VBAT备用电池供电,上下两路时钟,在主电源断电后,是停止运行的。所以要想实现RTC主电源掉电继续走时的功能,必须得选择中间这一路的RTC专用时钟。如果选择上下两路时钟,主电源断电后,时=时钟就暂停了,这样显然会出错。

11.2.2.2 RTC框图
11.2.2.3 RTC基本结构

最左边是RTCCLK时钟来源,这一块需要在RCC里配置, 3个时钟,选择一个当作RTCCLK,之后RTCCLK通过预分频器,对时钟进行分频。余数寄存器是一个自减计数器,存储当前的计数值,重装寄存器是计数目标,决定分频值,分频之后得到1Hz的秒计数信号,通向32位计数器,1秒自增1次,下面还有一个32位的闹钟值,可以设定闹钟,如果不需要的话,下面可以不用管。右边有三个信号可以触发中断,分别是秒信号,计数器溢出信号和闹钟信号,三个信号先通过中断输出控制,进行中断使能,使能的中断才能通向NVIC,然后向CPU申请中断。在程序中,我们可以配置数据选择器选择时钟来源,配置重装寄存器,可以选择分频系数,配置32位计数器,可以进行日期时间的读写。需要闹钟的话,配置32位闹钟值即可,需要中断的话,先允许中断,再配置NVIC。最后写对应的中断函数即可。

11.2.2.4 硬件电路

在最小电路上,外部电路还要再额外加两部分,第一部分就是备用电池供电, 第二部分就是外部低速晶振。

11.2.2.5 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寄存器。

11.3 读写备份寄存器

11.3.1 硬件电路

11.3.2 软件部分

(1)复制《OLED显示屏》并改为《读写备份寄存器》

(2)BKP库函数

cpp 复制代码
void BKP_DeInit(void);             //恢复缺省配置,手动清空BKP所有的数据寄存器
void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);    //用于配置TAMPER侵入检测功能,高电平还是低电平触发
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校准寄存器
void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);  //写备份寄存器,第一参数指定卸载哪个DR里,第二个参数指定要写入的数据
uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);              //读备份寄存器
FlagStatus BKP_GetFlagStatus(void);
void BKP_ClearFlag(void);
ITStatus BKP_GetITStatus(void);
void BKP_ClearITPendingBit(void);

(3) PWR库函数

cpp 复制代码
void PWR_DeInit(void);
void PWR_BackupAccessCmd(FunctionalState NewState);         // 备份寄存器访问使能
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);

(4)main.c

cpp 复制代码
#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);                         //使能对BKP和RTC的访问
//	BKP_WriteBackupRegister(BKP_DR1,0x1234);
//	OLED_ShowHexNum(1,1,BKP_ReadBackupRegister(BKP_DR1),4);
	while(1)
	{	
		KeyNum = Key_GetNum();
		if(KeyNum == 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);
	}
}

11.4 实时时钟

11.4.1 硬件电路

11.4.2 软件部分

(1)复制复制《OLED显示屏》并改为《实时时钟》

(2)添加驱动文件

(3)使用的RCC库函数

cpp 复制代码
void RCC_LSEConfig(uint8_t RCC_LSE);                   //配置LSE外部低速时钟
void RCC_LSICmd(FunctionalState NewState);             //配置LSI内部低速时钟
void RCC_RTCCLKConfig(uint32_t RCC_RTCCLKSource);      //选择RTCCLK的时钟源
void RCC_RTCCLKCmd(FunctionalState NewState);          //启动RTCCLK


FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG);    //获取标志位,LSE时钟不是说让它启动就能立刻启动,还需要1等待标志位LSEREADY置1

(4)RTC库函数

cpp 复制代码
void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);   //配置中断输出
void RTC_EnterConfigMode(void);                                 //进入配置模式,置CRL的CNF位为1,进入配置模式
void RTC_ExitConfigMode(void);                                  //退出配置模式,把CNF位清零
uint32_t  RTC_GetCounter(void);                                 //获取CNT计数器的值,读取时钟,就靠这个函数
void RTC_SetCounter(uint32_t CounterValue);                     //写入CNT计数器的值,设置时间,就靠这个函数    
void RTC_SetPrescaler(uint32_t PrescalerValue);                 //写入预分频器,这个值会写入的预分频器的PRL重装寄存器中,用来配置预分频器的分频系数
void RTC_SetAlarm(uint32_t AlarmValue);                         //写入闹钟值
uint32_t  RTC_GetDivider(void);                                 //读取预分频器中的DIV余数寄存器,余数寄存器是一个自减计数器,一般是为了得到更细致的时间
void RTC_WaitForLastTask(void);                                 //等待上次操作完成
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);

(5)MyRTC.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include <time.h>
void MyRTC_SetTime(void);

uint16_t MyRTC_Time[]= {2024,8,15,11,13,55};            //十进制前面千万不要补0,因为8进制以0开头

/*RTC初始化函数*/
void MyRTC_Init(void)
{
	/*第1步:开启PWR和BKP的时钟,使能BKP和RTC的访问*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
	PWR_BackupAccessCmd(ENABLE);
	if(BKP_ReadBackupRegister(BKP_DR1 != 0xA5A5))
	{
		/*第2步:开启LSE时钟,并等待LSE时钟启动完成*/
		RCC_LSEConfig(RCC_LSE_ON);                        //启动LSE晶振
		while(RCC_GetFlagStatus(RCC_FLAG_LSERDY)!=SET);   //等待LSE状态标志位置1
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);           //选择RTCCLK时钟源
		RCC_RTCCLKCmd(ENABLE);                            //使能时钟                        
		
		RTC_WaitForSynchro();                             //等待同步 
		RTC_WaitForLastTask();                            //等待上一次操作完成	
		
		RTC_SetPrescaler(32768-1);                        //配置分频系数得到1Hz
		RTC_WaitForLastTask();                            //等待上一次操作完成
	//	RTC_SetCounter(1723690874);	
		MyRTC_SetTime();
		
		/*有的板子RTC晶振起振不了,RTC晶振不起振,就会卡死在RCC_GetFlagStatus(RCC_FLAG_LSERDY)这里
		,这时侯只能备选LSI,进行以下修改*/
	//	RCC_LSICmd(ENABLE);                        //启动LSI晶振
	//	while(RCC_GetFlagStatus(RCC_FLAG_LSIRDY)!=SET);   //等待LSI状态标志位置1
	//	RCC_RTCCLKConfig(RCC_RTCCLKSource_LSI);           //选择RTCCLK时钟源
	//	RCC_RTCCLKCmd(ENABLE);                            //使能时钟                        
	//	
	//	RTC_WaitForSynchro();                             //等待同步 
	//	RTC_WaitForLastTask();                            //等待上一次操作完成	
	//	
	//	RTC_SetPrescaler(40000-1);                        //配置分频系数得到1Hz
	//	RTC_WaitForLastTask();                            //等待上一次操作完成
	//	RTC_SetCounter(1723690874);
		BKP_WriteBackupRegister(BKP_DR1,0xA5A5);
	}
	else
		{
			RTC_WaitForSynchro();                             //等待同步 
			RTC_WaitForLastTask();                            //等待上一次操作完成	
		}
}

/*将时间转换位Unix时间戳形式函数*/
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;
	RTC_SetCounter(time_cnt);
	RTC_WaitForLastTask();
}

/*读取时间函数*/
void MyRTC_ReadTime(void)
{
	time_t time_cnt;
	struct tm time_date;
	time_cnt = RTC_GetCounter() + 8*60*60;
	time_date = *localtime(&time_cnt);
	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;
}

(6)MyRTC.h

cpp 复制代码
#ifndef __MYRTC_
#define __MYRTC_
extern uint16_t MyRTC_Time[];  
void MyRTC_Init(void);
void MyRTC_SetTime(void);
void MyRTC_ReadTime(void);
#endif
 

(7)main.c

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "MyRTC.h"

uint8_t KeyNum;

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	MyRTC_Init();
	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();
		OLED_ShowNum(1,6,MyRTC_Time[0],4);
		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);
		OLED_ShowNum(2,12,MyRTC_Time[5],2);
		OLED_ShowNum(3,6,RTC_GetCounter(),10);
		OLED_ShowNum(4,6,(32767-RTC_GetDivider())/32767.0*999,10);
	}
}
相关推荐
PegasusYu7 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组
朝九晚五ฺ7 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
猫爪笔记9 小时前
前端:HTML (学习笔记)【1】
前端·笔记·学习·html
pq113_69 小时前
ftdi_sio应用学习笔记 3 - GPIO
笔记·学习·ftdi_sio
澄澈i9 小时前
设计模式学习[8]---原型模式
学习·设计模式·原型模式
爱米的前端小笔记10 小时前
前端八股自学笔记分享—页面布局(二)
前端·笔记·学习·面试·求职招聘
alikami11 小时前
【前端】前端学习
学习
一只小菜鸡..11 小时前
241118学习日志——[CSDIY] [ByteDance] 后端训练营 [06]
学习
文弱书生65612 小时前
输出比较简介
stm32
Hacker_Oldv12 小时前
网络安全的学习路线
学习·安全·web安全