接上文,文中的图片,大多数来自视频的截图(来自洋桃电子)。
欢迎大家批评指正!

STM32学习笔记-专栏

文章目录

  • 一、蜂鸣器驱动
    • 1、蜂鸣器介绍
    • 2、蜂鸣器电路
    • 3、蜂鸣器程序
  • 二、 MIDI 音乐播放
    • 1、MIDI 简介
    • 2、MIDI 放映原理
    • 3、MIDI 例程
  • 三、USART 串口通信
    • 1、简介
    • 2、串口电路
    • 3 收发程序
      • 3.1 相关固件及函数
      • 3.2 程序控制
    • 4、控制程序
    • 5、超级终端
      • 5.1 简介
      • 5.2 安装
      • 5.3 配置
      • 5.4 控制程序
  • 四、RTC原理驱动程序和日历功能
    • 1、RTC(实时时钟)和后备寄存器
    • 2、控制程序
    • 3、RTC库函数
    • 4、超级终端显示日历
  • 五、RCC(复位和时钟设置)
    • 1、RCC寄存器
    • 2、RCC相关函数
    • 3、时钟树

一、蜂鸣器驱动

1、蜂鸣器介绍

有源蜂鸣器 无源蜂鸣器
内置频率发生电路 内部没有发生电路
通电就能发出声音 需要外部给予频率
声音频率固定 可产生不同频率声音
成本较高 成本低
  • 两种蜂鸣器外观上无区别

2、蜂鸣器电路


核心板上的蜂鸣器

核心板蜂鸣器电路

  • 不用蜂鸣器时,应对 PB5PB5PB5 口拉高(此处电路接有上拉电阻),控制蜂鸣器断开,不工作。
  • 给 PB5PB5PB5口拉低电平,蜂鸣器上电导通,但由于是无源蜂鸣器,单纯导通只会发热,不会发声。需要给 PB5PB5PB5 不断高低变换的电平(方波),才可以发出声音。

3、蜂鸣器程序

在 hardwarehardwarehardware 导入 buzzer.cbuzzer.cbuzzer.c 文件

  • buzzer.hbuzzer.hbuzzer.h :

    • 宏定义:BUZZERBUZZERBUZZER 的端口组和 IOIOIO 端口。

      #define BUZZERPORT   GPIOB   //定义IO接口
      #define BUZZER  GPIO_Pin_5  //定义IO接口
      
    • 声明:初始化函数和响声函数。
      void BUZZER_Init(void);   //初始化
      void BUZZER_BEEP1(void);  //响一声
      
  • buzzer.cbuzzer.cbuzzer.c :

    • 初始化:

      void BUZZER_Init(void){                                                //蜂鸣器接口初始化GPIO_InitTypeDef  GPIO_InitStructure;  GPIO_InitStructure.GPIO_Pin = BUZZER;            //选择端口号                        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //选择IO接口工作方式       GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//设置IO接口速度(2/10/50MHz)    GPIO_Init(BUZZERPORT, &GPIO_InitStructure); GPIO_WriteBit(BUZZERPORT,BUZZER,(BitAction)(1)); //蜂鸣器接口输出高电平1  ,断开蜂鸣器,保护}
      
    • 响声函数:
      void BUZZER_BEEP1(void)                                  //1kHz{                                                    //蜂鸣器响一声u16 i;for(i=0;i<200;i++){GPIO_WriteBit(BUZZERPORT,BUZZER,(BitAction)(0)); //蜂鸣器接口输出0delay_us(500);                                   //延时       GPIO_WriteBit(BUZZERPORT,BUZZER,(BitAction)(1)); //蜂鸣器接口输出高电平1delay_us(500);                                   //延时     }}
      

      最后一条语句必须是,拉高电平,断开蜂鸣器,保护电路

  • main.cmain.cmain.c:

    int main (void)
    {//主程序u16 a;              //定义变量//初始化程序RCC_Configuration();    //时钟设置LED_Init();             //LED初始化KEY_Init();             //按键初始化BUZZER_Init();          //蜂鸣器初始化BUZZER_BEEP1();         //蜂鸣器音1a = FLASH_R(FLASH_START_ADDR); //从指定页的地址读FLASHGPIO_Write(LEDPORT,a|0xfffc&GPIO_ReadOutputData(LEDPORT));  //直接数值操作将变量值写入LED(LED在GPIOB组的PB0和PB1上)//主循环while(1){                                           //示例4:有锁存if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){                                        //读按键接口的电平delay_ms(20);                           //延时20ms去抖动if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){                                     //读按键接口的电平//在2个LED上显示二进制加法a++;                                  //变量加1if(a>3){                                   //当变量大于3时清0a=0; }GPIO_Write(LEDPORT,a|0xfffc&GPIO_ReadOutputData(LEDPORT));   //直接数值操作将变量值写入LED(LED在GPIOB组的PB0和PB1上)         BUZZER_BEEP1();                              //蜂鸣器音1FLASH_W(FLASH_START_ADDR,a);                 //从指定页的地址写入FLASHwhile(!GPIO_ReadInputDataBit(KEYPORT,KEY1)); //等待按键松开 }}}}
    

    功能为:上电响一声,按键每按下一次,响一声,多次按下,灯亮循环

二、 MIDI 音乐播放

1、MIDI 简介

MIDI (MusicalInstrumentDigitalInterface)(Musical Instrument Digital Interface)(MusicalInstrumentDigitalInterface) 乐器数字接口,是20世纪80年代初为解决电声乐器之间的通信问题而提出的。MIDI 是编曲界最广泛的音乐标准格式,可称为“计算机能理解的乐谱”。它用音符的数字控制信号来记录音乐。一首完整的 MIDI 音乐只有几十 KB 大,而能包含数十条音乐轨道。几乎所有的现代音乐都是用 MIDI 加上音色库来制作合成的。MIDI 传输的不是声音信号,而是音符、控制参数等指令,它指示 MIDI 设备要做什么,怎么做,如演奏哪个音符、多大音量等。它们被统一表示成 MIDI 消息。
传输时采用异步串行通信,标准通信波特率为 31.25×( 1±0.01) KBaud。

2、MIDI 放映原理


音符频率对照

  • 按音符控制发生频率
  • 按节拍调整一个音符的播放时间

3、MIDI 例程

  • buzzer.cbuzzer.cbuzzer.c :

    uc16 music1[78]=
    { //音乐1的数据表(奇数是音调,偶数是长度)
    330,750,
    440,375,
    494,375,
    523,750,
    587,375,
    659,375,
    587,750,
    494,375,
    392,375,
    440,1500,
    330,750,
    440,375,
    494,375,
    523,750,
    587,375,
    659,375,
    587,750,
    494,375,
    392,375,
    784,1500,
    659,750,
    698,375,
    784,375,
    880,750,
    784,375,
    698,375,
    659,750,
    587,750,
    659,750,
    523,375,
    494,375,
    440,750,
    440,375,
    494,375,
    523,750,
    523,750,
    494,750,
    392,750,
    440,3000
    };void MIDI_PLAY(void){                                         //MIDI音乐u16 i,e;for(i=0;i<39;i++){for(e=0;e<music1[i*2]*music1[i*2+1]/1000;e++){GPIO_WriteBit(BUZZERPORT,BUZZER,(BitAction)(0)); //蜂鸣器接口输出0delay_us(500000/music1[i*2]);                    //延时       GPIO_WriteBit(BUZZERPORT,BUZZER,(BitAction)(1)); //蜂鸣器接口输出高电平1delay_us(500000/music1[i*2]);                    //延时 }   }}
    

通过修改数组数据(左频率,右时长),修改循环长度,可以任意放映曲谱。

三、USART 串口通信

1、简介

利用串口芯片,可以实现USB与串口之间的转换,实现计算机和单片机的通讯。需安装串口驱动。
F103C8T6有三组串口,USART1、USART2、USART3

2、串口电路


核心板串口芯片

核心板串口电路

3 收发程序

3.1 相关固件及函数

函数名 描述
USART_DeInit 将外设 USARTx寄存器重设为缺省值
USART_Init 根据 USART_InitStruct 中指定的参数初始化外设 USARTx 寄存器
USART_StructInit 把 USART_InitStruct 中的每一个参数按缺省值填入
USART_Cmd 使能或者失能 USART 外设
USART_ITConfig 使能或者失能指定的 USART 中断
USART_DMACmd 使能或者失能指定 USART 的 DMA 请求
USART_SetAddress 设置 USART 节点的地址
USART_WakeUpConfig 选择 USART 的唤醒方式
USART_ReceiverWakeUpCmd 检查 USART 是否处于静默模式
USART_LINBreakDetectLengthConfig 设置 USART LIN 中断检测长度
USART_LINCmd 使能或者失能 USARTx 的 LIN 模式
USART_SendData 通过外设 USARTx 发送单个数据

3.2 程序控制

  • 加入相关 usart 的c,h文件,在 lib 文件夹加入固件 stm32f10x_usrat.c。

  • usart.husart.husart.h :

    #ifndef __USART_H
    #define __USART_H
    #include <stdarg.h>
    #include <stdlib.h>
    #include <string.h>
    #include "stdio.h"
    #include "sys.h" #define USART_n      USART1  //定义使用printf函数的串口,其他串口要使用USART_printf专用函数发送#define USART1_REC_LEN            200     //定义USART1最大接收字节数
    #define USART2_REC_LEN              200     //定义USART2最大接收字节数
    #define USART3_REC_LEN              200     //定义USART3最大接收字节数//不使用某个串口时要禁止此串口,以减少编译量
    #define EN_USART1           1       //使能(1)/禁止(0)串口1
    #define EN_USART2           0       //使能(1)/禁止(0)串口2
    #define EN_USART3           0       //使能(1)/禁止(0)串口3extern u8  USART1_RX_BUF[USART1_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
    extern u8  USART2_RX_BUF[USART2_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符
    extern u8  USART3_RX_BUF[USART3_REC_LEN]; //接收缓冲,最大USART_REC_LEN个字节.末字节为换行符extern u16 USART1_RX_STA;                //接收状态标记
    extern u16 USART2_RX_STA;               //接收状态标记
    extern u16 USART3_RX_STA;               //接收状态标记    //函数声明
    void USART1_Init(u32 bound);//串口1初始化并启动
    void USART2_Init(u32 bound);//串口2初始化并启动
    void USART3_Init(u32 bound);//串口3初始化并启动
    void USART1_printf(char* fmt,...); //串口1的专用printf函数
    void USART2_printf(char* fmt,...); //串口2的专用printf函数
    void USART3_printf(char* fmt,...); //串口3的专用printf函数#endif
    
    • 宏定义
    • 串口选择使能
    • 基本配置
  • usart.cusart.cusart.c :
    USART1 串口相关程序:(2、3与1基本相同

    /*USART1串口相关程序*/
    #if EN_USART1   //USART1使用与屏蔽选择
    u8 USART1_RX_BUF[USART1_REC_LEN];     //接收缓冲,最大USART_REC_LEN个字节.//接收状态//bit15,   接收完成标志//bit14,   接收到0x0d//bit13~0,    接收到的有效字节数目
    u16 USART1_RX_STA=0;                  //接收状态标记   /*USART1专用的printf函数
    当同时开启2个以上串口时,printf函数只能用于其中之一,其他串口要自创独立的printf函数
    调用方法:USART1_printf("123"); //向USART2发送字符123*/
    void USART1_printf (char *fmt, ...)
    { char buffer[USART1_REC_LEN+1];  // 数据长度u8 i = 0;    va_list arg_ptr;va_start(arg_ptr, fmt);  vsnprintf(buffer, USART1_REC_LEN+1, fmt, arg_ptr);while ((i < USART1_REC_LEN) && (i < strlen(buffer))){USART_SendData(USART1, (u8) buffer[i++]);while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); }va_end(arg_ptr);
    }void USART1_Init(u32 bound){ //串口1初始化并启动//GPIO端口设置GPIO_InitTypeDef GPIO_InitStructure;USART_InitTypeDef USART_InitStructure;NVIC_InitTypeDef NVIC_InitStructure;    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);    //使能USART1,GPIOA时钟//USART1_TX   PA.9GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出GPIO_Init(GPIOA, &GPIO_InitStructure);  //USART1_RX   PA.10GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入GPIO_Init(GPIOA, &GPIO_InitStructure); //Usart1 NVIC 配置NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;     //子优先级3NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;         //IRQ通道使能NVIC_Init(&NVIC_InitStructure);    //根据指定的参数初始化VIC寄存器 //USART 初始化设置USART_InitStructure.USART_BaudRate = bound;//一般设置为9600;USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式USART_Init(USART1, &USART_InitStructure); //初始化串口USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启ENABLE/关闭DISABLE中断USART_Cmd(USART1, ENABLE);                    //使能串口
    }void USART1_IRQHandler(void)
    {                       //串口1中断服务程序(固定的函数名不能修改)   u8 Res;//以下是字符串接收到USART1_RX_BUF[]的程序,(USART1_RX_STA&0x3FFF)是数据的长度(不包括回车)//当(USART1_RX_STA&0xC000)为真时表示数据接收完成,即超级终端里按下回车键。//在主函数里写判断if(USART1_RX_STA&0xC000),然后读USART1_RX_BUF[]数组,读到0x0d 0x0a即是结束。//注意在主函数处理完串口数据后,要将USART1_RX_STA清0if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){                                     //接收中断(接收到的数据必须是0x0d 0x0a结尾)        Res =USART_ReceiveData(USART1);     //(USART1->DR); //读取接收到的数据printf("%c",Res);                   //把收到的数据以 a符号变量 发送回电脑       if((USART1_RX_STA&0x8000)==0){                                   //接收未完成          if(USART1_RX_STA&0x4000){                                 //接收到了0x0d                if(Res!=0x0a){USART1_RX_STA=0;              //接收错误,重新开始}else{USART1_RX_STA|=0x8000;      //接收完成了 }}else{                                 //还没收到0X0D                  if(Res==0x0d)USART1_RX_STA|=0x4000;else{USART1_RX_BUF[USART1_RX_STA&0X3FFF]=Res ; //将收到的数据放入数组USART1_RX_STA++;                //数据长度计数加1if(USART1_RX_STA>(USART1_REC_LEN-1)){USART1_RX_STA=0;            //接收数据错误,重新开始接收    }}         }}          }
    }
    #endif
    
    • 基本配置——状态
    • printf 函数定义
    • 串口初始化
      • 串口的 TX 和 RX 口,需要进行 IO 的配置,如 IO 初始化、IO端口模式配置等
      • 波特率、数据长度、特殊标志位、模式配置、开启中断、使能 等
    • 串口中断
  • main.cmain.cmain.c :

    int main (void)
    {                        //主程序u8 a=7,b=8;//初始化程序RCC_Configuration();   //时钟设置USART1_Init(115200);   //串口初始化(参数是波特率)//主循环while(1){/* 发送方法1 */USART_SendData(USART1 , 0x55);    //发送单个数值while(USART_GetFlagStatus(USART1, USART_FLAG_TC)==RESET); //检查发送中断标志位/* 发送方法2 *///  printf("STM32F103 ");          //纯字符串发送数据到串口//    printf("STM32 %d %d ",a,b);    //纯字符串和变量发送数据到串口,a符号变量/* 发送方法3 *///    USART1_printf("STM32 %d %d ",a,b);delay_ms(1000);                    //延时}
    }
    
    • printf - c语言自带的显示函数,在单片机中默认发送给串口
    • 固件库函数 —— USART_ SendData
    函数名 USART_ SendData
    函数原形 void USART_SendData(USART_TypeDef* USARTx, u8 Data)
    功能描述 通过外设 USARTx 发送单个数据
    输入参数 1 USARTx:x 可以是 1,2 或者 3,来选择 USART 外设
    输入参数 2 Data: 待发送的数据
    输出参数
    返回值
    先决条件
    被调用函数

4、控制程序

  • main.cmain.cmain.c :
    主循环:

    while(1)
    {//查询方式接收if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) != RESET){                                                               //查询串口待处理标志位a =USART_ReceiveData(USART1);                           //读取接收到的数据switch (a){case '0':GPIO_WriteBit(LEDPORT,LED1,(BitAction)(0));                 //LED控制printf("%c:LED1 OFF ",a); //break;case '1':GPIO_WriteBit(LEDPORT,LED1,(BitAction)(1));                 //LED控制printf("%c:LED1 ON ",a); //break;case '2':BUZZER_BEEP1();                                 //蜂鸣一声printf("%c:BUZZER ",a);                         //把收到的数据发送回电脑break;default:break;}          }//按键控制if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){                                                        //读按键接口的电平delay_ms(20);                                    //延时20ms去抖动if(!GPIO_ReadInputDataBit(KEYPORT,KEY1)){                                                      //读按键接口的电平while(!GPIO_ReadInputDataBit(KEYPORT,KEY1)); //等待按键松开 printf("KEY1 "); //}}       if(!GPIO_ReadInputDataBit(KEYPORT,KEY2)){                                                        //读按键接口的电平delay_ms(20);                                    //延时20ms去抖动if(!GPIO_ReadInputDataBit(KEYPORT,KEY2)){                                                      //读按键接口的电平while(!GPIO_ReadInputDataBit(KEYPORT,KEY2)); //等待按键松开 printf("KEY2 "); //}}
    //      delay_ms(1000); //延时
    }
    
    • 发送串口指令,控制LED、蜂鸣器,并串口显示
    • 按键按下,串口显示

5、超级终端

5.1 简介

可以进行更强大的串口开发

5.2 安装

安装 hyper_terminal_latest

5.3 配置

  • 设置串口,波特率,编码类型(可中文)
  • 设置文本、背景等颜色

5.4 控制程序

  • usart.cusart.cusart.c:
    中断函数:

    void USART1_IRQHandler(void)
    { //串口1中断服务程序(固定的函数名不能修改) u8 Res;//以下是字符串接收到USART_RX_BUF[]的程序,(USART_RX_STA&0x3FFF)是数据的长度(不包括回车)//当(USART_RX_STA&0xC000)为真时表示数据接收完成,即超级终端里按下回车键。//在主函数里写判断if(USART_RX_STA&0xC000),然后读USART_RX_BUF[]数组,读到0x0d 0x0a即是结束。//注意在主函数处理完串口数据后,要将USART_RX_STA清0if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET){  //接收中断(接收到的数据必须是0x0d 0x0a结尾)     Res =USART_ReceiveData(USART1);//(USART1->DR);  //读取接收到的数据printf("%c",Res); //把收到的数据以 a符号变量 发送回电脑     if((USART1_RX_STA&0x8000)==0){//接收未完成         if(USART1_RX_STA&0x4000){//接收到了0x0d             if(Res!=0x0a){USART1_RX_STA=0;//接收错误,重新开始}else{USART1_RX_STA|=0x8000;    //接收完成了 }}else{ //还没收到0X0D                  if(Res==0x0d){USART1_RX_STA|=0x4000;}else{USART1_RX_BUF[USART1_RX_STA&0X3FFF]=Res ; //将收到的数据放入数组USART1_RX_STA++;  //数据长度计数加1if(USART1_RX_STA>(USART1_REC_LEN-1)){USART1_RX_STA=0;//接收数据错误,重新开始接收    }}         }}}
    }
    
  • main.cmain.cmain.c :
    主函数:

    int main (void)
    {//主程序RCC_Configuration();LED_Init();//LED初始化KEY_Init();//按键初始化BUZZER_Init();//蜂鸣器初始化USART1_Init(115200); //串口初始化,参数中写波特率USART1_RX_STA=0xC000; //初始值设为有回车的状态,即显示一次欢迎词while(1){if(USART1_RX_STA&0xC000){                                         //如果标志位是0xC000表示收到数据串完成,可以处理。if((USART1_RX_STA&0x3FFF)==0){                                       //单独的回车键再显示一次欢迎词printf("\033[1;47;33m\r\n");          //设置颜色(参考超级终端使用)printf(" 1y--开LED1灯      1n--关LED1灯 \r\n");printf(" 2y--开LED2灯      2n--关LED2灯 \r\n");printf(" 请输入控制指令,按回车键执行! \033[0m\r\n");}else if((USART1_RX_STA&0x3FFF)==2 && USART1_RX_BUF[0]=='1' && USART1_RX_BUF[1]=='y'){                                       //判断数据是不是2个,第一个数据是不是“1”,第二个是不是“y”GPIO_SetBits(LEDPORT,LED1);           //LED灯都为高电平(1)printf("1y -- LED1灯已经点亮!\r\n");}else if((USART1_RX_STA&0x3FFF)==2 && USART1_RX_BUF[0]=='1' && USART1_RX_BUF[1]=='n'){GPIO_ResetBits(LEDPORT,LED1); //      //LED灯都为低电平(0)printf("1n -- LED1灯已经熄灭!\r\n");}else if((USART1_RX_STA&0x3FFF)==2 && USART1_RX_BUF[0]=='2' && USART1_RX_BUF[1]=='y'){GPIO_SetBits(LEDPORT,LED2);           //LED灯都为高电平(1)printf("2y -- LED2灯已经点亮!\r\n");}else if((USART1_RX_STA&0x3FFF)==2 && USART1_RX_BUF[0]=='2' && USART1_RX_BUF[1]=='n'){GPIO_ResetBits(LEDPORT,LED2);         //LED灯都为低电平(0)printf("2n -- LED2灯已经熄灭!\r\n");}else{                                       //如果以上都不是,即是错误的指令。printf("指令错误!\r\n"); }USART1_RX_STA=0;                        //将串口数据标志位清0}}
    }
    
    • if,elseif,elseif,else 判断: 判断数据是不是2个,第一个数据是不是 “1” ,第二个是不是 “y”。

四、RTC原理驱动程序和日历功能


RTC

1、RTC(实时时钟)和后备寄存器

RTC 和后备寄存器通过一个开关供电,在 VDDV_{DD}VDD​ 有效时该开关选择 VDDV_{DD}VDD​ 供电,否则由 VBATV_{BAT}VBAT​ 引脚供电。后备寄存器(10个16位的寄存器)可以用于在关闭 VDDV_{DD}VDD​ 时,保存 20 个字节的用户应用数据。RTC 和后备寄存器不会被系统或电源复位源复位;当从待机模式唤醒时,也不会被复位。
实时时钟具有一组连续运行的计数器,可以通过适当的软件提供日历时钟功能,还具有闹钟中断和阶段性中断功能。RTC 的驱动时钟可以是一个使用外部晶体的 32.768kHz 的振荡器、内部低功耗 RC 振荡器或高速的外部时钟经 128 分频。内部低功耗 RC 振荡器的典型频率为 40KHz。为补偿天然晶体的偏差,可以通过输出一个 512Hz 的信号对 RTC 的时钟进行校准。RTC 具有一个 32 位的可编程计数器,使用比较寄存器可以进行长时间的测量。有一个 20 位的预分频器用于时基时钟,默认情况下时钟为 32.768kHz 时,它将产生一个 1秒长的时间基准。

2、控制程序

  • 时间读写设置说明:

    • main 函数开头放入 RTC_Config(); 就可以使能时钟了。
      RTC_Config(); 函数中自带判断是不是首次使用RTC
    • 使用 RTC_Get(); 读出时间。读出的数据存放在:
      年 ryear(16位) 月 rmon(以下都是8位) 日 rday 时 rhour 分 rmin 秒 rsec 周 rweek
    • 使用 RTC_Set(4位年,2位月,2位日,2位时,2位分,2位秒); 写入时间。 例如: RTC_Get(2017,08,06,21,34,00);
  • rtc.crtc.crtc.c :

    • rtc 启动配置(初次启动和初始化)):

      void RTC_First_Config(void)
      { //首次启用RTC的设置RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);//启用PWR和BKP的时钟(from APB1)PWR_BackupAccessCmd(ENABLE);                                            //后备域解锁BKP_DeInit();                                                           //备份寄存器模块复位RCC_LSEConfig(RCC_LSE_ON);                                              //外部32.768KHZ晶振开启   while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);                    //等待稳定    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);                                 //RTC时钟源配置成LSE(外部低速晶振32.768KHZ)    RCC_RTCCLKCmd(ENABLE);                                                  //RTC开启    RTC_WaitForSynchro();                                                   //开启后需要等待APB1时钟与RTC时钟同步,才能读写寄存器    RTC_WaitForLastTask();                                                  //读写寄存器前,要确定上一个操作已经结束RTC_SetPrescaler(32767);//设置RTC分频器,使RTC时钟为1Hz,RTC period = RTCCLK/RTC_PR = (32.768 KHz)/(32767+1)   RTC_WaitForLastTask();                                                  //等待寄存器写入完成  //当不使用RTC秒中断,可以屏蔽下面2条RTC_ITConfig(RTC_IT_SEC, ENABLE);//使能秒中断   RTC_WaitForLastTask();//等待写入完成
      }void RTC_Config(void)
      {   //实时时钟初始化//在BKP的后备寄存器1中,存了一个特殊字符0xA5A5//第一次上电或后备电源掉电后,该寄存器数据丢失,表明RTC数据丢失,需要重新配置if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5){ //判断寄存数据是否丢失       RTC_First_Config();                         //重新配置RTC        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);   //配置完成后,向后备寄存器中写特殊字符0xA5A5}else{//若后备寄存器没有掉电,则无需重新配置RTC//这里我们可以利用RCC_GetFlagStatus()函数查看本次复位类型if (RCC_GetFlagStatus(RCC_FLAG_PORRST) != RESET){//这是上电复位}else if (RCC_GetFlagStatus(RCC_FLAG_PINRST) != RESET){//这是外部RST管脚复位}       RCC_ClearFlag();                            //清除RCC中复位标志//虽然RTC模块不需要重新配置,且掉电后依靠后备电池依然运行//但是每次上电后,还是要使能RTCCLKRCC_RTCCLKCmd(ENABLE);                      //使能RTCCLK        RTC_WaitForSynchro();                       //等待RTC时钟与APB1时钟同步//当不使用RTC秒中断,可以屏蔽下面2条RTC_ITConfig(RTC_IT_SEC, ENABLE);           //使能秒中断        RTC_WaitForLastTask();                      //等待操作完成}#ifdef RTCClockOutput_Enable   RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);PWR_BackupAccessCmd(ENABLE);   BKP_TamperPinCmd(DISABLE);   BKP_RTCOutputConfig(BKP_RTCOutputSource_CalibClock);#endif
      }```
      
  • 中断:

    void RTC_IRQHandler(void)
    { //RTC时钟1秒触发中断函数(名称固定不可修改)
    if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
    {}
    RTC_ClearITPendingBit(RTC_IT_SEC);
    RTC_WaitForLastTask();
    }void RTCAlarm_IRQHandler(void)
    {   //闹钟中断处理(启用时必须调高其优先级)
    if(RTC_GetITStatus(RTC_IT_ALR) != RESET)
    {}
    RTC_ClearITPendingBit(RTC_IT_ALR);
    RTC_WaitForLastTask();
    }
    
  • 时间读写计算:

    //判断是否是闰年函数
    //月份   1  2  3  4  5  6  7  8  9  10 11 12
    //闰年   31 29 31 30 31 30 31 31 30 31 30 31
    //非闰年 31 28 31 30 31 30 31 31 30 31 30 31
    //输入:年份
    //输出:该年份是不是闰年.1,是.0,不是
    u8 Is_Leap_Year(u16 year)
    {                    if(year%4==0){ //必须能被4整除if(year%100==0){       if(year%400==0){return 1;//如果以00结尾,还要能被400整除          } else{ return 0;             } }else{return 1; } }else{ return 0;             }
    }
    //设置时钟
    //把输入的时钟转换为秒钟
    //以1970年1月1日为基准
    //1970~2099年为合法年份//月份数据表
    u8 const table_week[12]={0,3,3,6,1,4,6,2,5,0,3,5}; //月修正数据表
    const u8 mon_table[12]={31,28,31,30,31,30,31,31,30,31,30,31};//平年的月份日期表//写入时间
    u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec)
    { //写入当前时间(1970~2099年有效),u16 t;u32 seccount=0;if(syear<2000||syear>2099){return 1;//syear范围1970-2099,此处设置范围为2000-2099       }for(t=1970;t<syear;t++){ //把所有年份的秒钟相加if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数else seccount+=31536000;                    //平年的秒钟数}smon-=1;for(t=0;t<smon;t++){         //把前面月份的秒钟数相加seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加if(Is_Leap_Year(syear)&&t==1){seccount+=86400;//闰年2月份增加一天的秒钟数        }}seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加seccount+=(u32)hour*3600;//小时秒钟数seccount+=(u32)min*60;      //分钟秒钟数seccount+=sec;//最后的秒钟加上去RTC_First_Config(); //重新初始化时钟BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5RTC_SetCounter(seccount);//把换算好的计数器值写入RTC_WaitForLastTask(); //等待写入完成return 0; //返回值:0,成功;其他:错误代码.
    }//读出时间
    u8 RTC_Get(void)
    {//读出当前时间值 //返回值:0,成功;其他:错误代码.static u16 daycnt=0;u32 timecount=0;u32 temp=0;u16 temp1=0;timecount=RTC_GetCounter();       temp=timecount/86400;   //得到天数(秒钟数对应的)if(daycnt!=temp){//超过一天了daycnt=temp;temp1=1970;  //从1970年开始while(temp>=365){if(Is_Leap_Year(temp1)){//是闰年if(temp>=366){temp-=366;//闰年的秒钟数}else {temp1++;break;} }else {temp-=365;       //平年}temp1++; }  ryear=temp1;//得到年份temp1=0;while(temp>=28){//超过了一个月if(Is_Leap_Year(ryear)&&temp1==1){//当年是不是闰年/2月份if(temp>=29){temp-=29;//闰年的秒钟数}else {break;}}else{if(temp>=mon_table[temp1]){temp-=mon_table[temp1];//平年}else {break;}}temp1++; }rmon=temp1+1;//得到月份rday=temp+1;  //得到日期}temp=timecount%86400;     //得到秒钟数      rhour=temp/3600;     //小时rmin=(temp%3600)/60; //分钟     rsec=(temp%3600)%60; //秒钟rweek=RTC_Get_Week(ryear,rmon,rday);//获取星期  return 0;
    }    u8 RTC_Get_Week(u16 year,u8 month,u8 day)
    { //按年月日计算星期(只允许1901-2099年)//已由RTC_Get调用    u16 temp2;u8 yearH,yearL;yearH=year/100;     yearL=year%100;// 如果为21世纪,年份数加100 if (yearH>19){yearL+=100;}// 所过闰年数只算1900年之后的 temp2=yearL+yearL/4;temp2=temp2%7;temp2=temp2+day+table_week[month-1];if (yearL%4==0&&month<3){temp2--;}return(temp2%7); //返回星期值
    }
    

3、RTC库函数

函数名 描述
RTC_ITConfig 使能或者失能指定的RTC中断
RTC_EntcrConfigModc 进入RTC配置模式
RTC_ExitConfigMode 退出RTC配置模式
RTC_GetCounter 获取RTC计数器的值
RTC_SetCounter 设置RTC计数器的值
RTC_SetPrescaler 设置RTC预分频的值
RTC_SetAlarm 设置RTC闹钟的值
RTC_GetDivider 获取RTC预分频分频因子的值
RTC_WaitForLastTask 等待最近一次对RTC寄存器的写操作完成
RTC_WaitForSynchro 等待RTC寄存器(RTC_CNT,RTC_ALR and RTC_PRL)与RTC的APB时钟同步
RTC_GetFlagStatus 检查指定的RTC标志位设置与否
RTC_ClearFlag 清除RTC的待处理标志位
RTC_GetITStatus 检查指定的RTC中断发生与否
RTC_ClearITPendingBit 清除RTC的中断待处理位

4、超级终端显示日历

  • main.cmain.cmain.c :

    int main (void){//主程序u8 bya;RCC_Configuration(); //系统时钟初始化RTC_Config(); //实时时钟初始化LED_Init();//LED初始化KEY_Init();//按键初始化BUZZER_Init();//蜂鸣器初始化USART1_Init(115200); //串口初始化,参数中写波特率USART1_RX_STA=0xC000; //初始值设为有回车的状态,即显示一次欢迎词while(1){if(USART1_RX_STA&0xC000){ //如果标志位是0xC000表示收到数据串完成,可以处理。if((USART1_RX_STA&0x3FFF)==0){ //单独的回车键再显示一次欢迎词if(RTC_Get()==0){ //读出时间值,同时判断返回值是不是0,非0时读取的值是错误的。printf(" 洋桃开发板STM32实时时钟测试程序   \r\n");printf(" 现在实时时间:%d-%d-%d %d:%d:%d  ",ryear,rmon,rday,rhour,rmin,rsec);//显示日期时间if(rweek==0)printf("星期日   \r\n");//rweek值为0时表示星期日if(rweek==1)printf("星期一   \r\n");if(rweek==2)printf("星期二   \r\n");if(rweek==3)printf("星期三   \r\n");if(rweek==4)printf("星期四   \r\n");if(rweek==5)printf("星期五   \r\n");if(rweek==6)printf("星期六   \r\n");printf(" 单按回车键更新时间。输入字母C初始化时钟 \r\n");printf(" 请输入设置时间,格式20170806120000,按回车键确定! \r\n");}else{printf("读取失败!\r\n");}}else if((USART1_RX_STA&0x3FFF)==1){ //判断数据是不是2个if(USART1_RX_BUF[0]=='c' || USART1_RX_BUF[0]=='C'){RTC_First_Config(); //键盘输入c或C,初始化时钟BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5printf("初始化成功!      \r\n");//显示初始化成功}else{printf("指令错误!          \r\n"); //显示指令错误!} }else if((USART1_RX_STA&0x3FFF)==14){ //判断数据是不是14个//将超级终端发过来的数据换算并写入RTCryear = (USART1_RX_BUF[0]-0x30)*1000+(USART1_RX_BUF[1]-0x30)*100+(USART1_RX_BUF[2]-0x30)*10+USART1_RX_BUF[3]-0x30;rmon = (USART1_RX_BUF[4]-0x30)*10+USART1_RX_BUF[5]-0x30;//串口发来的是字符,减0x30后才能得到十进制0~9的数据rday = (USART1_RX_BUF[6]-0x30)*10+USART1_RX_BUF[7]-0x30;rhour = (USART1_RX_BUF[8]-0x30)*10+USART1_RX_BUF[9]-0x30;rmin = (USART1_RX_BUF[10]-0x30)*10+USART1_RX_BUF[11]-0x30;rsec = (USART1_RX_BUF[12]-0x30)*10+USART1_RX_BUF[13]-0x30;bya=RTC_Set(ryear,rmon,rday,rhour,rmin,rsec); //将数据写入RTC计算器的程序if(bya==0)printf("写入成功!      \r\n");//显示写入成功 else printf("写入失败!       \r\n"); //显示写入失败}else{ //如果以上都不是,即是错误的指令。printf("指令错误!          \r\n"); //如果不是以上正确的操作,显示指令错误!}USART1_RX_STA=0; //将串口数据标志位清0}}
    }
    

五、RCC(复位和时钟设置)

1、RCC寄存器

寄存器 描述
CR 时钟控制寄存器
CFGR 时钟配置寄存器
CIR 时钟中断寄存器
APB2RSTR APB2外设复位寄存器
APB1RSTR APB1外设复位寄存器
AHBENR AHB外设时钟使能寄存器
APB2ENR APB2外设时钟使能寄存器
APB1ENR APB1外设时钟使能寄存器
BDCR 备份域控制寄存器
CSR 控制/状态寄存器

2、RCC相关函数

函数名 描述
RCC_DeInit 将外设RCC寄存器重设为缺省值
RCC_HSEConfig 设置外部高速晶振(HSE)
RCc_WaitForHSEStartUp 等待HSE起振
RCC_AdjustHSICalibrationValue 调整内部高速晶振(HSI)校准值
RCC_ HSICmd 使能或者失能内部高速晶振(HSI)
RCC_PLLConfig 设置PLL时钟源及倍频系数
RCC_PLLCmd 使能或者失能PLL
RCC_sYsCLKConfig 设置系统时钟(sYSCLK)
RCC GetsYSCLKSource 返回用作系统时钟的时钟源
RCC_HCLKConfig 设置AHB时钟(HCLK)
RCC_PCLK1Config 设置低速AHB时钟(PCLK1)
RCC_PCLK2Config 设置高速AHB时钟(PCLK2)
RCC_ITConfig 使能或者失能指定的RCC中断
RCC_USBCLKConfig 设置USB时钟(USBCLK)
RCC_ADCCLKConfig 设置ADC时钟(ADCCLK)
RCC_LSEConfig 设置外部低速晶振(LSE)
RCC_LSICmd 使能或者失能内部低速晶振(LSI)
RCC_RTCCLKConfig 设置RTC时钟(RTCCLK)
RCC_RTCCLKCmd 使能或者失能RTC时钟
RCC_GetClocksFreq 返回不同片上时钟的频率
RCC_AHBPeriphClockCmd 使能或者失能AHB外设时钟
RCC_APB2PeriphClockCmd 使能或者失能APB2外设时钟
RCC_APB1PeriphClockCmd 使能或者失能APB1外设时钟
RCC_APB2PeriphResetCmd 强制或者释放高速APB (APB2)外设复位
RCC_APBlPeriphResetCmd 强制或者释放低速APB (APB1)外设复位
RCC_BackupResetCmd 强制或者释放后备域复位
RCC_ClockSecuritySystemCmd 使能或者失能时钟安全系统
RCC_MCOConfig 选择在MCO管脚上输出的时钟源
RCC_GetFlagStatus 检查指定的RCC标志位设置与否
RCC_ClearFlag 清除RCC的复位标志位
RCC_GetITStatus 检查指定的RCC中断发生与否
RCC_ClearITPendingBit 清除RCC的中断待处理位
  • RCC_Configuration(void)RCC\_ \ Configuration(void){}RCC_ Configuration(void) 配置时钟树:

      void RCC_Configuration(void){ //RCC时钟的设置  ErrorStatus HSEStartUpStatus;   RCC_DeInit();              /* RCC system reset(for debug purpose) RCC寄存器恢复初始化值*/   RCC_HSEConfig(RCC_HSE_ON); /* Enable HSE 使能外部高速晶振*/   HSEStartUpStatus = RCC_WaitForHSEStartUp(); /* Wait till HSE is ready 等待外部高速晶振使能完成*/   if(HSEStartUpStatus == SUCCESS){   /*设置PLL时钟源及倍频系数*/   RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); //RCC_PLLMul_x(枚举2~16)是倍频值。当HSE=8MHZ,RCC_PLLMul_9时PLLCLK=72MHZ   /*设置AHB时钟(HCLK)*/   RCC_HCLKConfig(RCC_SYSCLK_Div1); //RCC_SYSCLK_Div1——AHB时钟 = 系统时钟(SYSCLK) = 72MHZ(外部晶振8HMZ)   /*注意此处的设置,如果使用SYSTICK做延时程序,此时SYSTICK(Cortex System timer)=HCLK/8=9MHZ*/   RCC_PCLK1Config(RCC_HCLK_Div2); //设置低速AHB时钟(PCLK1),RCC_HCLK_Div2——APB1时钟 = HCLK/2 = 36MHZ(外部晶振8HMZ)   RCC_PCLK2Config(RCC_HCLK_Div1); //设置高速AHB时钟(PCLK2),RCC_HCLK_Div1——APB2时钟 = HCLK = 72MHZ(外部晶振8HMZ)   /*注:AHB主要负责外部存储器时钟。APB2负责AD,I/O,高级TIM,串口1。APB1负责DA,USB,SPI,I2C,CAN,串口2,3,4,5,普通TIM */  FLASH_SetLatency(FLASH_Latency_2); //设置FLASH存储器延时时钟周期数   /*FLASH时序延迟几个周期,等待总线同步操作。   推荐按照单片机系统运行频率:0—24MHz时,取Latency_0;   24—48MHz时,取Latency_1;   48~72MHz时,取Latency_2*/   FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); //选择FLASH预取指缓存的模式,预取指缓存使能   RCC_PLLCmd(ENABLE);    //使能PLLwhile(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); //等待PLL输出稳定   RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); //选择SYSCLK时钟源为PLLwhile(RCC_GetSYSCLKSource() != 0x08); //等待PLL成为SYSCLK时钟源   }  }
    
  • 开启需要使用的外设时钟

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOC| RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE, ENABLE); //APB2外设时钟使能
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); //APB1外设时钟使能
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    

3、时钟树


RTC

左——产生频率
右——分配频率

《STM32学习笔记》4——核心功能电路与编程(下)相关推荐

  1. 《STM32学习笔记》3——核心功能电路与编程(上)

    接上文,文中的图片,大多数来自视频的截图(来自洋桃电子). 欢迎大家批评指正! STM32学习笔记-专栏 文章目录 一.核心板电路分析 二.点灯 LED 1.LED电路 2.LED功能相关初始化配置 ...

  2. STM32学习笔记——CH340一键下载电路

    今天看了一下STM32板子的一键下载功能,对电路理解了一下,做个说明. 1.启动模式 STM32上电后的BOOT1/0的电平状态会决定它的运行模式.很明显,我们如果上电后要进入自己写的程序并运行,那么 ...

  3. STM32学习笔记1——软硬件基础之keil5编程与GPIO开发

    目录 前言 一.Keil uVision5 MDK软件的安装以及一些问题的解决方案 1.1 软件安装 1.2 注意事项 二. keil5编程与GPIO开发 2.1 第一步:使能IO口时钟 2.2 第二 ...

  4. STM32学习笔记(四)丨TIM定时器及其应用(定时中断、内外时钟源选择)

    本篇文章包含的内容 一.TIM 定时器 1.1 TIM 定时器简介 1.2 TIM 定时器类型及其工作原理简介 1.2.1 基本定时器工作原理及其结构 1.2.2 通用定时器工作原理及其结构 1.2. ...

  5. 《STM32学习笔记》2——开发环境的建立

    接上文,文中的图片知识,大多数来自视频的截图(来自洋桃电子). 欢迎大家批评指正! STM32学习笔记-专栏 文章目录 一.接口定义 1 芯片引脚整体简介 2 各部分引脚介绍 二.开发板简介 三.IS ...

  6. STM32学习笔记 | 引起电源和系统异常复位的原因

    关注+星标公众号,不错过精彩内容 每一块处理器都有复位的功能,不同处理器复位的类型可能有差异,引起复位的原因也可能有多种. STM32的复位功能非常强大,可通过软件.硬件和一些事件触发系统复位,而且通 ...

  7. 【STM32学习笔记-点亮LED灯】

    STM32学习笔记-点亮LED灯 文章目录 STM32学习笔记-点亮LED灯 一.原理图分析 二.代码分析 1.mian函数 2.led.c函数 3.led.h函数 4.函数文件整理 5.LED_In ...

  8. STM32学习笔记(六)丨TIM定时器及其应用(输入捕获丨测量PWM波形的频率和占空比)

    本篇文章包含的内容 一.输入捕获 1.1 输入捕获简介 1.2 输入捕获通道的工作原理 1.3 输入捕获的主从触发模式 1.4 输入捕获和PWMI结构 二.频率的测量方法 2.1 测频法 2.2 测周 ...

  9. STM32学习笔记(八)丨ADC模数转换器(ADC单、双通道转换)

    本篇文章包含的内容 一.ADC 模数转换器 1.1 ADC简介 1.2 逐次逼近型ADC工作原理 1.3 STM32中的ADC基本结构 1.4 STM32中ADC的输入通道 1.5 STM32中的AD ...

最新文章

  1. LeetCode 817. Linked List Components
  2. C# winform引用com组件,创建AXHOST组件失败解决方案
  3. ideal2018提示内存不足_IntelliJ IDEA 2018 设置代码超出限制自动换行(最新版)
  4. 总结了C#中string.format用法。分享给大家供大家参考。具体分析如下:
  5. 信号与系统郑君里上册pdf_信号与系统(上册)
  6. php 仿电脑桌面系统,EonerCMS——做一个仿桌面系统的CMS(十-附最新源码)
  7. Python网络编程——使用TCP方式传输文件
  8. html img图片等比例缩放_我掏空了各大搜索引擎,整理了HTML图片标签笔记,满满干货...
  9. PaddlePaddle飞桨OCR文本检测——识别模型训练(三)
  10. Big Sur系统更新后,运行变慢?使macOS Big Sur更快运行的12个技巧
  11. knx智能照明控制系统电路图_智能照明控制系统(KNX)讲解
  12. 版本控制工具——subversion
  13. 华为云devops认证考试课堂笔记2
  14. java+mysql实现图书管理系统
  15. windriver 自动生成pcie驱动
  16. 2 二十五项反措--防止火灾事故
  17. 稳定同位素(stable isotope)标记氨基酸,聚氨基酸,PEI,聚合物,抑制剂,离子液体,石墨烯,黑磷,透明质酸,荧光材料,脂质体复合物
  18. 工信部发布《2018中国区块链产业白皮书》:量子计算机将给密码体系带来重大安全威胁
  19. 【聚沙成塔】-MYSQL全文索引使用MATCH AGAINST
  20. Five I/O Models

热门文章

  1. linux升级之后黑屏,fedora升级到28之后gnome登录黑屏的解决方法
  2. MIT 18.06 +线性代数的几何意义+3Blue1Brown 笔记
  3. C语言示例分析-乒乓球抽取
  4. MySQL 连接挂死了!该如何排查?
  5. [SSM框架]—Mybatis入门
  6. 80端口和443端口的区别
  7. 动物名称日语单词集合
  8. 如何在应用中打开系统播放器
  9. OBS第三方推流直播教程
  10. 单例模式——国庆收心