当前位置:   article > 正文

多功能电子钟_基于stm32的电子钟课程设计

基于stm32的电子钟课程设计

【嵌入式课程设计】基于STM32F4的多功能电子钟设计

一、课程设计目的

1、进一步掌握嵌入式系统课程所学STM32F4各功能模块的工作原理;
2、进一步熟练掌握STM32F4各功能模块的配置与使用方法;
3、进一步熟练掌握开发环境Keil MDK5的使用与程序调试技巧;
4、自学部分功能模块的原理、配置与使用方法,培养自学能力;
5、培养设计复杂嵌入式应用软、硬件系统的分析与设计能力。

二、课程设计内容

1、查阅资料,自学STM32F4的RTC模块,完成RTC的配置;
2、查阅资料,学习STM32F4与LCD的接口设计,完成LCD液晶屏驱动程序的设计,将时间、日期、星期等日历信息显示在LCD上;
3、能进行正常的日期、时间、星期显示;
4、有校时、校分功能,可以使用按键校时、校分,也可以通过串口调试助手由主机传送时间参数进行校时、校分;
5、能进行整点报时并有闹钟功能,闹钟时间可以设置多个;
6、系统关机后时间能继续运行,下次开机时间应准确;
7、查阅资料,学习STM32F4内部温度传感器的配置,采集、计算片内温度并显示在LCD上;

三、实验方案分析与设计

此模块描述课程设计中用到的硬件资源或模块工作原理,硬件电路的连接、模块的库函数配置方法(用到的主要库函数、配置步骤等)。

3.1 RTC实时时钟

3.1.1 工作原理

实时时钟 (RTC) 是一个独立的 BCD 定时器/计数器。RTC 提供一个日历时钟、两个可编程闹钟中断,以及一个具有中断功能的周期性可编程唤醒标志。RTC 还包含用于管理低功耗模式的自动唤醒单元。两个 32 位寄存器包含二进码十进数格式 (BCD) 的秒、分钟、小时(12 或 24 小时制)、星期几、日期、月份和年份。此外,还可提供二进制格式的亚秒值。系统可以自动将月份的天数补偿为 28、29(闰年)、30 和 31 天。并且还可以进行夏令时补偿。其它 32 位寄存器还包含可编程的闹钟亚秒、秒、分钟、小时、星期几和日期。此外,还可以使用数字校准功能对晶振精度的偏差进行补偿。上电复位后,所有 RTC 寄存器都会受到保护,以防止可能的非正常写访问。无论器件状态如何(运行模式、低功耗模式或处于复位状态),只要电源电压保持在工作范围内,RTC 便不会停止工作。RTC工作原理图如下图1所示。
图1 RTC工作原理框图

3.2 温度传感器

1、温度传感器可用于测量器件的环境温度 (TA)
对于STM32F40x和STM32F41x器件,温度传感器内部连接到ADC1_IN16通道,而ADC1用于将传感器输出电压转换为数字值。
对于STM32F42x 和 STM32F43x 器件,温度传感器内部连接到与VBAT共用的输入通道ADC1_IN18:ADC1_IN18用于将传感器输出电压或 VBAT 转换为数字值。一次只能选择一个转换(温度传感器或 VBAT)。同时设置了温度传感器和VBAT转换时,将只进行VBAT转换。不使用时可将传感器置于掉电模式。
图2 温度传感器框图

3.3 外部中断/ 事件控制器 (EXTI)

外部中断/事件控制器包含多达23个用于产生事件/中断请求的边沿检测器。每根输入线都可单独进行配置,以选择类型(中断或事件)和相应的触发事件(上升沿触发、下降沿触发或边沿触发)。每根输入线还可单独屏蔽。挂起寄存器用于保持中断请求的状态线。
图3 外部中断/事件控制器框图

3.4 通用同步异步收发器 (USART)

3.4.1 工作原理

通用同步异步收发器 (USART) 能够灵活地与外部设备进行全双工数据交换,满足外部设备对工业标准 NRZ 异步串行数据格式的要求。USART 通过小数波特率发生器提供了多种波特率。它支持同步单向通信和半双工单线通信;还支持 LIN(局域互连网络)、智能卡协议与 IrDA(红外线数据协会)SIR ENDEC 规范,以及调制解调器操作 (CTS/RTS)。而且,它还支持多处理器通信。通过配置多个缓冲区使用 DMA 可实现高速数据通信。

3.5 BEEP

3.5.1 工作原理

蜂鸣器是一种一体化结构的电子讯响器。开发板板载的蜂鸣器是电磁式的有源蜂鸣器。我们是通过三极管扩流后再驱动蜂鸣器,IO 只需要提供不到 1mA 的电流就足够了。

四、具体实现过程描述

4.1 RTC具体配置代码

//RTC时间设置
ErrorStatus RTC_Set_Time(u8 hour,u8 min,u8 sec,u8 ampm)
{
	RTC_TimeTypeDef RTC_TimeTypeInitStructure;	
	RTC_TimeTypeInitStructure.RTC_Hours=hour;
	RTC_TimeTypeInitStructure.RTC_Minutes=min;
	RTC_TimeTypeInitStructure.RTC_Seconds=sec;
	RTC_TimeTypeInitStructure.RTC_H12=ampm;
	return RTC_SetTime(RTC_Format_BIN,&RTC_TimeTypeInitStructure);
}

//RTC日期设置
ErrorStatus RTC_Set_Date(u8 year,u8 month,u8 date,u8 week)
{
	RTC_DateTypeDef RTC_DateTypeInitStructure;
	RTC_DateTypeInitStructure.RTC_Date=date;
	RTC_DateTypeInitStructure.RTC_Month=month;
	RTC_DateTypeInitStructure.RTC_WeekDay=week;
	RTC_DateTypeInitStructure.RTC_Year=year;
	return RTC_SetDate(RTC_Format_BIN,&RTC_DateTypeInitStructure);
}

//RTC初始化
u8 My_RTC_Init(void)
{
	RTC_InitTypeDef RTC_InitStructure;
	u16 retry=0X1FFF; 
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);//使能PWR时钟
	PWR_BackupAccessCmd(ENABLE);	//使能后备寄存器访问 
	if(RTC_ReadBackupRegister(RTC_BKP_DR0)!=0x5050)		//是否第一次配置?
	{
		RCC_LSEConfig(RCC_LSE_ON);//LSE 开启    
		while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)	//检查指定的RCC标志位设置与否,等待低速晶振就绪
			{
			retry++;
			delay_ms(10);
			}
		if(retry==0)return 1;		//LSE 开启失败. 
		RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);		//设置RTC时钟(RTCCLK),选择LSE作为RTC时钟    
		RCC_RTCCLKCmd(ENABLE);	//使能RTC时钟 

        RTC_InitStructure.RTC_AsynchPrediv = 0x7F;//RTC异步分频系数(1~0X7F)
        RTC_InitStructure.RTC_SynchPrediv  = 0xFF;//RTC同步分频系数(0~7FFF)
        RTC_InitStructure.RTC_HourFormat   = RTC_HourFormat_24;//RTC设置为,24小时格式
        RTC_Init(&RTC_InitStructure);
 
		RTC_Set_Time(23,59,56,RTC_H12_AM);	//设置时间
		RTC_Set_Date(14,5,5,1);		//设置日期
	 
		RTC_WriteBackupRegister(RTC_BKP_DR0,0x5050);	//标记已经初始化过了
	} 
	return 0;
}
//设置闹钟时间(按星期闹铃,24小时制)
//week:星期几(1~7) @ref  RTC_Alarm_Definitions
//hour,min,sec:小时,分钟,秒钟
void RTC_Set_AlarmA(u8 week,u8 hour,u8 min,u8 sec)
{ 
	EXTI_InitTypeDef   EXTI_InitStructure;
	RTC_AlarmTypeDef RTC_AlarmTypeInitStructure;
	RTC_TimeTypeDef RTC_TimeTypeInitStructure;
	RTC_AlarmCmd(RTC_Alarm_A,DISABLE);//关闭闹钟A 
	
    RTC_TimeTypeInitStructure.RTC_Hours=hour;//小时
	RTC_TimeTypeInitStructure.RTC_Minutes=min;//分钟
	RTC_TimeTypeInitStructure.RTC_Seconds=sec;//秒
	RTC_TimeTypeInitStructure.RTC_H12=RTC_H12_AM;
  
	RTC_AlarmTypeInitStructure.RTC_AlarmDateWeekDay=week;//星期
	RTC_AlarmTypeInitStructure.RTC_AlarmDateWeekDaySel=RTC_AlarmDateWeekDaySel_WeekDay;//按星期闹
	RTC_AlarmTypeInitStructure.RTC_AlarmMask=RTC_AlarmMask_None;//精确匹配星期,时分秒
	RTC_AlarmTypeInitStructure.RTC_AlarmTime=RTC_TimeTypeInitStructure;
    RTC_SetAlarm(RTC_Format_BIN,RTC_Alarm_A,&RTC_AlarmTypeInitStructure);
	RTC_ClearITPendingBit(RTC_IT_ALRA);//清除RTC闹钟A的标志
    EXTI_ClearITPendingBit(EXTI_Line17);//清除LINE17上的中断标志位 
	RTC_ITConfig(RTC_IT_ALRA,ENABLE);//开启闹钟A中断
	RTC_AlarmCmd(RTC_Alarm_A,ENABLE);//开启闹钟A 
	
EXTI_InitStructure.EXTI_Line = EXTI_Line17;//LINE17
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断事件
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发 
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;//使能LINE17
    EXTI_Init(&EXTI_InitStructure);//配置

	NVIC_InitStructure.NVIC_IRQChannel = RTC_Alarm_IRQn; 
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
    NVIC_Init(&NVIC_InitStructure);//配置
}
//cnt:自动重装载值.减到0,产生中断.
void RTC_Set_WakeUp(u32 wksel,u16 cnt)
{ 
	EXTI_InitTypeDef   EXTI_InitStructure;
	RTC_WakeUpCmd(DISABLE);//关闭WAKE UP
	RTC_WakeUpClockConfig(wksel);//唤醒时钟选择
	RTC_SetWakeUpCounter(cnt);//设置WAKE UP自动重装载寄存器
	RTC_ClearITPendingBit(RTC_IT_WUT); //清除RTC WAKE UP的标志
    EXTI_ClearITPendingBit(EXTI_Line22);//清除LINE22上的中断标志位 
	RTC_ITConfig(RTC_IT_WUT,ENABLE);//开启WAKE UP 定时器中断
	RTC_WakeUpCmd( ENABLE);//开启WAKE UP 定时器 
	
EXTI_InitStructure.EXTI_Line = EXTI_Line22;//LINE22
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断事件
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发 
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;//使能LINE22
    EXTI_Init(&EXTI_InitStructure);//配置
 	
NVIC_InitStructure.NVIC_IRQChannel = RTC_WKUP_IRQn; 
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级1
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
    NVIC_Init(&NVIC_InitStructure);//配置
}

//RTC闹钟中断服务函数
void RTC_Alarm_IRQHandler(void)
{    
	if(RTC_GetFlagStatus(RTC_FLAG_ALRAF)==SET)//ALARM A中断?
	{
		RTC_ClearFlag(RTC_FLAG_ALRAF);//清除中断标志
		sign=1;
		printf("ALARM A!\r\n");
					x=clockw[0];
					clockw[0]=clockw[1];
					clockw[1]=x;
					x=clockh[0];
					clockh[0]=clockh[1];
					clockh[1]=x;
					x=clockm[0];
					clockm[0]=clockm[1];
					clockm[1]=x;
					x=clocks[0];
					clocks[0]=clocks[1];
					clocks[1]=x;
					x=clock[0];
					clock[0]=clock[1];
					clock[1]=x;
					RTC_Set_AlarmA(clockw[0],clockh[0],clockm[0],clocks[0]);
	}   
	EXTI_ClearITPendingBit(EXTI_Line17);	//清除中断线17的中断标志 					 
}
//RTC WAKE UP中断服务函数
void RTC_WKUP_IRQHandler(void)
{    
	if(RTC_GetFlagStatus(RTC_FLAG_WUTF)==SET)//WK_UP中断?
	{ 
		RTC_ClearFlag(RTC_FLAG_WUTF);	//清除中断标志
	}   
	EXTI_ClearITPendingBit(EXTI_Line22);//清除中断线22的中断标志 						
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151

4.2 ADC具体配置代码(温度传感器)

//初始化ADC
//开启温度传感器通道																   
void  Adc_Init(void)
{    
     GPIO_InitTypeDef  GPIO_InitStructure;
	 ADC_CommonInitTypeDef ADC_CommonInitStructure;
	 ADC_InitTypeDef       ADC_InitStructure;
    
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//使能GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//使能ADC1时钟

    //先初始化IO口
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;//模拟输入
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;// 上拉
    GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化  
	
RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,ENABLE);	//ADC1复位
	RCC_APB2PeriphResetCmd(RCC_APB2Periph_ADC1,DISABLE);	//复位结束	 
    
ADC_TempSensorVrefintCmd(ENABLE);//使能内部温度传感器
    ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent;//独立模式
    ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles;
    ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; //DMA失能
ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div4; //ADCCLK=PCLK2/4=84/4=21Mhz,ADC时钟最好不要超过36Mhz  
ADC_CommonInit(&ADC_CommonInitStructure);
    ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;//12位模式
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;//非扫描模式	
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_None;//禁止触发检测,用软件触发
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//右对齐	
    ADC_InitStructure.ADC_NbrOfConversion = 1;//1个转换在规则序列中 也就是只转换规则序列1 
    ADC_Init(ADC1, &ADC_InitStructure);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_480Cycles );	//ADC5,ADC通道,480个周期,提高采样时间可以提高精确度		
	ADC_RegularChannelConfig(ADC1, ADC_Channel_16, 1, ADC_SampleTime_480Cycles );	//ADC16,ADC通道,480个周期,提高采样时间可以提高精确度		
	ADC_Cmd(ADC1, ENABLE);//开启AD转换器	 			
}				  
//获得ADC值
u16 Get_Adc(u8 ch)   
{
	 //设置指定ADC的规则组通道,一个序列,采样时间
	ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_480Cycles );	//ADC1,ADC通道,480个周期,提高采样时间可以提高精确度			     
	ADC_SoftwareStartConv(ADC1);		//使能指定的ADC1的软件转换启动功能		 
	while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
	return ADC_GetConversionValue(ADC1);	//返回最近一次ADC1规则组的转换结果
}
//获取通道ch的转换值,取times次,然后平均 
//ch:通道编号,times:获取次数,返回值:通道ch的times次转换结果平均值
u16 Get_Adc_Average(u8 ch,u8 times)
{
	u32 temp_val=0;
	u8 t;
	for(t=0;t<times;t++)
	{
		temp_val+=Get_Adc(ch);//获取通道转换值
		delay_ms(5);
	}
	return temp_val/times;
} 
//得到温度值
//返回值:温度值(扩大了100倍,单位:℃.)
short Get_Temprate(void)
{
	u32 adcx;
	short result;
 	double temperate;
	adcx=Get_Adc_Average(ADC_Channel_16,10);	//读取通道16内部温度传感器通道,10次取平均
	temperate=(float)adcx*(3.3/4096);		//电压值
	temperate=(temperate-0.76)/0.0025 + 25; //转换为温度值 
	result=temperate*=100;					//扩大100倍.
	return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

4.3 外部中断具体配置代码

1、定义的变量含义
extern int sign; //标志位,用于区分钟表变化状态
extern int choice; //选择,代表KEY0与KEY2按键的左右移动
extern int th,tm,ts,dy,dm,dd,w; //分别表示当前小时、分钟、秒、
extern int ah[3],am[3],as[3]; //数组,存储闹钟的小时、分钟、秒
extern int aw[3]; //数组存储周数
2、计算周数函数
这段代码是一个用于计算某个日期对应的星期几的函数。具体的实现方法是使用了蔡勒公式(Zeller’s congruence),这是一种用于计算日期与星期对应关系的算法。
具体的实现过程如下:
首先判断月份是否小于3,如果是,则将月份加上12,年份减1(这是为了在计算时方便处理)。
然后根据蔡勒公式计算该日期对应的星期数。公式为:
(day + 2 * month + 3 * (month + 1) / 5 + year + year / 4 - year / 100 + year / 400) % 7
其中day为天数,month为月份(如果月份小于3,则需要加上12),year为年份。%7表示对7取余数,得到的结果就是该日期对应的星期数。需要注意的是,这个星期数是从0开始的,0表示星期日,1表示星期一,以此类推,直到6表示星期六,因此函数中需要将结果加上1才能得到正确的星期数。这个函数的返回值就是该日期对应的星期数。

int CalculateWeekday(int year, int month, int day) {   //计算相应日期对应的周数
    if (month < 3) {
        month += 12;
        year--;
    }
    return 1+ (day + 2 * month + 3 * (month + 1) / 5 + year + year / 4 - year / 100 + year / 400) % 7;
		
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3、外部中断的配置

//外部中断0服务程序
void EXTI0_IRQHandler(void)
{
	delay_ms(10);	//消抖
	if(WK_UP==1)	 
	{
		switch(choice){
			case 0:sign=0;BEEP=0;break;
			case 1:if(th==23){th=0;}else{th++;}break;
			case 2:if(tm==59){tm=0;}else{tm++;}break;
			case 3:dy++;break;
			case 4:if(dm==12){dm=1;}else{dm++;}break;
			case 5:/*if(dd==30){dd=1;}else{dd++;}break;*/
				     if ((dm == 4 || dm == 6 || dm == 9 || dm == 11) && dd == 30) {
					dd = 1;
				} else if (dm == 2) {
					if ((dy % 4 == 0 && dy % 100 != 0) || dy % 400 == 0) {
						// 闰年
						if (dd == 29) {
							dd = 1;
						} else {
							dd++;
						}
					} else {
						// 平年
						if (dd == 28) {
							dd = 1;
						} else {
							dd++;
						}
					}
				} else if (dd == 31) {
					dd = 1;
					if (dm == 12) {
            dm = 1;
            dy++;
        } else {
            dm++;
        }
				} else {
					dd++;
				}
				w = CalculateWeekday(dy, dm, dd);
				break;
			case 6:if(w==7){w=1;}else{w++;}break;
			case 7:if(ah[0]==23){ah[0]=0;}else{ah[0]++;}break;
			case 8:if(am[0]==59){am[0]=0;}else{am[0]++;}break;
			case 9:if(as[0]==59){as[0]=0;}else{as[0]++;}break;
			case 10:if(ah[1]==23){ah[1]=0;}else{ah[1]++;}break;
			case 11:if(am[1]==59){am[1]=0;}else{am[1]++;}break;
			case 12:if(as[1]==59){as[1]=0;}else{as[1]++;}break;
			case 13:if(ah[2]==23){ah[2]=0;}else{ah[2]++;}break;
			case 14:if(am[2]==59){am[2]=0;}else{am[2]++;}break;
			case 15:if(as[2]==59){as[2]=0;}else{as[2]++;}break;
			case 16:RTC_Set_AlarmA(aw[0],ah[0],am[0],as[0]);
		}
		RTC_Set_Time(th,tm,ts,RTC_H12_AM);	//设置时间
	  RTC_Set_Date(dy,dm,dd,w);		//设置日期	}		 
	 EXTI_ClearITPendingBit(EXTI_Line0); //清除LINE0上的中断标志位
}	
//外部中断2服务程序
void EXTI2_IRQHandler(void)
{	
		
	delay_ms(10);	//消抖
	if(KEY2==0)	  
	{			
		if(choice>0)choice--; //选择递减
	}		 
	 EXTI_ClearITPendingBit(EXTI_Line2); //清除LINE2上的中断标志位  
}
//外部中断3服务程序
void EXTI3_IRQHandler(void)
{
	delay_ms(10);	//消抖
	if(KEY1==0)	 
	{
		switch(choice){
			case 1:if(th==0){th=23;}else{th--;}break;
			case 2:if(tm==0){tm=59;}else{tm--;}break;
			case 3:dy--;break;
			case 4:if(dm==1){dm=12;}else{dm--;}break;
			case 5:/*if(dd==1){dd=30;}else{dd--;}break;*/
				 if ((dm == 4 || dm == 6 || dm == 9 || dm == 11) && dd == 1) {
					dd = 30;
				} else if (dm == 2) {
					if ((dy % 4 == 0 && dy % 100 != 0) || dy % 400 == 0) {
						// 2月有29天,即闰年
						if (dd == 1) {
							dd = 29;
						} else {
							dd--;
						}
					} else {
						// 2月有28天,即平年
						if (dd == 1) {
							dd = 28;
						} else {
							dd--;
						}
					}
				} else if (dd == 1) {
					dd = 31;
					if (dm == 1) {
            dm = 12;
            dy--;
        } else {
            dm--;
        }
				} else {
					dd--;
				}
				w = CalculateWeekday(dy, dm, dd);
				break;
			case 6:if(w==1){w=7;}else{w--;};break;
			case 7:if(ah[0]==0){ah[0]=23;}else{ah[0]--;}break;
			case 8:if(am[0]==0){am[0]=59;}else{am[0]--;}break;
			case 9:if(as[0]==0){as[0]=59;}else{as[0]--;}break;
			case 10:if(ah[1]==0){ah[1]=23;}else{ah[1]--;}break;
			case 11:if(am[1]==0){am[1]=59;}else{am[1]--;}break;
			case 12:if(as[1]==0){as[1]=59;}else{as[1]--;}break;
			case 13:if(ah[2]==0){ah[2]=23;}else{ah[2]--;}break;
			case 14:if(am[2]==0){am[2]=59;}else{am[2]--;}break;
			case 15:if(as[2]==0){as[2]=59;}else{as[2]--;}break;
		}
		RTC_Set_Time(th,tm,ts,RTC_H12_AM);	//设置时间
	  RTC_Set_Date(dy,dm,dd,w);		//设置日期
	}		 
	 EXTI_ClearITPendingBit(EXTI_Line3);  //清除LINE3上的中断标志位  
}
//外部中断4服务程序
void EXTI4_IRQHandler(void)
{
	delay_ms(10);	//消抖
	if(KEY0==0)	 
	{				 
		if(choice<15)choice++;   //选择递加
	}		 
	 EXTI_ClearITPendingBit(EXTI_Line4); //清除LINE4上的中断标志位
}   
//外部中断初始化程序
//初始化PE2~4,PA0为中断输入.
void EXTIX_Init(void)
{
	NVIC_InitTypeDef   NVIC_InitStructure;
	EXTI_InitTypeDef   EXTI_InitStructure;
	KEY_Init(); //按键对应的IO口初始化 
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);//使能SYSCFG时钟

SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource2);//PE2 连接到中断线2
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource3);//PE3 连接到中断线3
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOE, EXTI_PinSource4);//PE4 连接到中断线4
SYSCFG_EXTILineConfig(EXTI_PortSourceGPIOA, EXTI_PinSource0);//PA0 连接到中断线0
  
/* 配置EXTI_Line0 */
  EXTI_InitStructure.EXTI_Line = EXTI_Line0;//LINE0
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断事件
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //上升沿触发 
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;//使能LINE0
  EXTI_Init(&EXTI_InitStructure);//配置
/* 配置EXTI_Line2,3,4 */
EXTI_InitStructure.EXTI_Line = EXTI_Line2 | EXTI_Line3 | EXTI_Line4;
  EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;//中断事件
  EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发
  EXTI_InitStructure.EXTI_LineCmd = ENABLE;//中断线使能
  EXTI_Init(&EXTI_InitStructure);//配置

NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;//外部中断0
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x00;//抢占优先级0
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
  NVIC_Init(&NVIC_InitStructure);//配置
	
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;//外部中断2
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x03;//抢占优先级3
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
  NVIC_Init(&NVIC_InitStructure);//配置
		
NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn;//外部中断3
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;//抢占优先级2
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
  NVIC_Init(&NVIC_InitStructure);//配置
	
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn;//外部中断4
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x01;//抢占优先级1
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;//子优先级2
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//使能外部中断通道
  NVIC_Init(&NVIC_InitStructure);//配置	   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190

4.7 主函数程序

1、参数定义

u8 set;
int sign;
int choice=0;
int th,tm,ts;//当前时、分、秒
int dy,dm,dd;//当前年、月、日
int w;//星期几
int ah[3],am[3],as[3],aw[3];//存储闹钟
int clocks[2]; //串口输出闹钟
int clockm[2];
int clockh[2];
int clockw[2];
int clock[2];
int x;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2、库函数初始化

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
	delay_init(168);      //初始化延时函数
	uart_init(76800);		//初始化串口波特率115200
 	LCD_Init();					  //初始化LCD
	My_RTC_Init();		 		//初始化RTC
  EXTIX_Init();       //初始化外部中断输入 
	BEEP_Init();
	Adc_Init();         //内部温度传感器ADC初始化
	RTC_Set_WakeUp(RTC_WakeUpClock_CK_SPRE_16bits,0);		//配置WAKE UP中断,1秒钟中断一次
	BEEP=0;  //初始化为0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3、绘制钟表盘

if ((t % 18) == 0){
		 LCD_Fill(140, 400, 340, 600, WHITE);
     LCD_Draw_Circle(240, 500, 100);
		 LCD_Draw_Circle(240, 500, 101);
		 LCD_Draw_Circle(240, 500, 102);
     LCD_Draw_Circle(240, 500, 1);
		 LCD_Draw_Circle(240, 500, 2);
	     LCD_DrawLine(240, 400, 240, 420);  // 12...
     LCD_ShowString(240, 420, 10, 16, 16, "12");
     LCD_DrawLine(240, 600, 240, 580);
     LCD_ShowString(240, 580, 5, 16, 16, "6");
     LCD_DrawLine(140, 500, 160, 500);
     LCD_ShowString(160, 500, 5, 16, 16, "9");
     LCD_DrawLine(340, 500, 320, 500);
     LCD_ShowString(320, 500, 5, 16, 16, "3");
     LCD_DrawLine(290, 587, 285, 578);  // 5...
     LCD_ShowString(275, 570, 5, 16, 16, "5");
     LCD_DrawLine(290, 413, 285, 422);  // 1...
     LCD_ShowString(285, 422, 5, 16, 16, "1");
     LCD_DrawLine(190, 587, 195, 578);  // 7...
     LCD_ShowString(190, 570, 5, 16, 16, "7");
     LCD_DrawLine(190, 413, 195, 422);  // 11..
     LCD_ShowString(195, 422, 10, 16, 16, "11");
     LCD_DrawLine(327, 550, 318, 545);  // 4...
     LCD_ShowString(310, 540, 5, 16, 16, "4");
     LCD_DrawLine(153, 450, 162, 455);  // 10..
     LCD_ShowString(162, 455, 10, 16, 16, "10");
     LCD_DrawLine(327, 450, 318, 455);  // 2...
     LCD_ShowString(318, 455, 5, 16, 16, "2");
     LCD_DrawLine(153, 550, 162, 545);  // 8...
     LCD_ShowString(162, 540, 5, 16, 16, "8");
		 for ( i = 0; i < 60; i++) {
        if (i % 5 != 0) {
            int x1 = 240 + (int)(90 * sin(i * 6 * 3.14 / 180));
            int y1 = 500 - (int)(90 * cos(i * 6 * 3.14 / 180));
            int x2 = 240 + (int)(95 * sin(i * 6 * 3.14 / 180));
            int y2 = 500 - (int)(95 * cos(i * 6 * 3.14 / 180));
            LCD_DrawLine(x1, y1, x2, y2);
        }
    }
		 for ( j = 0; j < 60; j++) {
        if (j % 5 != 0) {
            int x1 = 240 + (int)(90 * sin(j * 6 * 3.14 / 180));
            int y1 = 500 - (int)(90 * cos(j * 6 * 3.14 / 180));
            int x2 = 240 + (int)(95 * sin(j * 6 * 3.14 / 180));
            int y2 = 500 - (int)(95 * cos(j * 6 * 3.14 / 180));
            LCD_DrawLine(x1, y1, x2, y2);
					}
    }
    drawh = th;
    drawm = tm;
    draws = ts;
    if (drawh > 11) drawh -= 12;
    switch (drawh) {
        case 0: LCD_DrawLine(240, 500, 240, 460); break;
        case 1: LCD_DrawLine(240, 500, 260, 465); break;
        case 2: LCD_DrawLine(240, 500, 275, 480); break;
        case 3: LCD_DrawLine(240, 500, 280, 500); break;
        case 4: LCD_DrawLine(240, 500, 275, 520); break;
        case 5: LCD_DrawLine(240, 500, 260, 535); break;
        case 6: LCD_DrawLine(240, 500, 240, 540); break;
        case 7: LCD_DrawLine(240, 500, 220, 535); break;
        case 8: LCD_DrawLine(240, 500, 205, 520); break;
        case 9: LCD_DrawLine(240, 500, 200, 500); break;
        case 10: LCD_DrawLine(240, 500, 205, 480); break;
        case 11: LCD_DrawLine(240, 500, 220, 465); break;
    }
		switch (drawm) {
      case 0: LCD_DrawLine(240, 500, 240, 440); break;
      case 1: LCD_DrawLine(240, 500, 246, 440); break;
      case 2: LCD_DrawLine(240, 500, 252, 441); break;
      case 3: LCD_DrawLine(240, 500, 258, 443); break;
      case 4: LCD_DrawLine(240, 500, 264, 445); break;
      case 5: LCD_DrawLine(240, 500, 270, 448); break;
      case 6: LCD_DrawLine(240, 500, 275, 451); break;
      case 7: LCD_DrawLine(240, 500, 280, 455); break;
      case 8: LCD_DrawLine(240, 500, 284, 460); break;
      case 9: LCD_DrawLine(240, 500, 288, 465); break;
     ……
		
		switch (draws) {
    case 0: LCD_DrawLine(240, 500, 240, 440); break;
    case 1: LCD_DrawLine(240, 500, 246, 440); break;
    case 2: LCD_DrawLine(240, 500, 252, 441); break;
    case 3: LCD_DrawLine(240, 500, 258, 443); break;
    case 4: LCD_DrawLine(240, 500, 264, 445); break;
    case 5: LCD_DrawLine(240, 500, 270, 448); break;
……
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88

4、闹钟模块
将当前时间与设置的闹钟时间进行比较,若相一致,则改变标志,使得蜂鸣器响,并控制其响10秒钟,同时在这个模块包含了整点报时的功能。

th=RTC_TimeStruct.RTC_Hours;   //获取rtc中存储的小时
	tm=RTC_TimeStruct.RTC_Minutes;  //获取rtc中存储的分钟
	ts=RTC_TimeStruct.RTC_Seconds;   //获取rtc中存储的秒
	dy=RTC_DateStruct.RTC_Year;   //获取rtc中存储的年份
	dm=RTC_DateStruct.RTC_Month;  //获取rtc中存储的月份
	dd=RTC_DateStruct.RTC_Date;  //获取rtc中存储的天数
	w=RTC_DateStruct.RTC_WeekDay;  //获取rtc中存储的周数
	for(k=0;k<3;k++)    //系统可设置3个闹钟
	{
		if(ah[k]==th&&am[k]==tm&&as[k]==ts)   //当闹钟时间等于当前时间时,sign置为1
        {sign=1;}
			}
			
		if(RTC_TimeStruct.RTC_Minutes==0&&RTC_TimeStruct.RTC_Seconds==0){sign=1;}
		if(RTC_TimeStruct.RTC_Minutes==0&&RTC_TimeStruct.RTC_Seconds==10){sign=10;}
		if(ah[0]==th&&am[0]==tm&&as[0]+10==ts)  //响到10秒则修改标志为10,使其停止蜂鸣
        {sign=10;}
		if(ah[1]==th&&am[1]==tm&&as[1]+10==ts)
        {sign=10;}
		if(ah[2]==th&&am[2]==tm&&as[2]+10==ts)
        {sign=10;}
		if(sign==1){BEEP=!BEEP;delay_ms(20);BEEP=!BEEP;}//整点报时	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

5、串口调试模块
可以通过串口调试助手输入代表相应功能的字母指令来调用相关功能。

if(USART_RX_STA&0x8000)
		{
			lenth=USART_RX_STA&0x3fff;  //用来获取接收到的数据的长度,将USARTRXSTA的高位清零
			ch=USART_RX_BUF[lenth-1];  //获取了缓冲区中最后一个接收到的数据
			USART_SendData(USART1,USART_RX_BUF[lenth-1]);	  //将ch变量中的数据通过USART1串口发送出去		
			switch(ch){   //不同的输入代表不同的功能
				case 'h':rth=3;printf("\r\nAdjustment hour:");break;
				case 'm':rtm=3;printf("\r\nAdjustment minute:");break;
				case 'a':sa=8;printf("\r\nSet AlarmClock:");break;
				case 't':ttt=3;tttt=0;timing=0;printf("\r\nSet Timing:");break;
			}			
			ic=(int)(ch-'0');
			if(rth==3){th=-56;}
			if(rth==2){th+=ic*10;}
			if(rth==1){th+=ic;printf("\r\n");}
			if(rtm==3){tm=-96;}
			if(rtm==2){tm+=ic*10;}
			if(rtm==1){tm+=ic;printf("\r\n");}
			RTC_Set_Time(th,tm,ts,RTC_H12_AM);		
				if(clock[0]>clock[1])  //比较设置的闹钟优先级
				{
					x=clockw[0];
					clockw[0]=clockw[1];
					clockw[1]=x;
					x=clockh[0];
					clockh[0]=clockh[1];
					clockh[1]=x;
					x=clockm[0];
					clockm[0]=clockm[1];
					clockm[1]=x;
					x=clocks[0];
					clocks[0]=clocks[1];
					clocks[1]=x;
					x=clock[0];
					clock[0]=clock[1];
					clock[1]=x;
					RTC_Set_AlarmA(clockw[0],clockh[0],clockm[0],clocks[0]);  
				}	
			switch(ttt){       //倒计时模块,显示倒计时
				case 3:timing=0;LCD_Fill(280,150,360,200,WHITE);break;
				case 2:timing+=ic*100;LCD_ShowxNum(300,160,timing,3,16,0);break;
				case 1:timing+=ic*10;LCD_ShowxNum(300,160,timing,3,16,0);break;
				case 0:timing+=ic;LCD_ShowxNum(300,160,timing,3,16,0);printf("\r\n");tttt=1;break;
			}		
			rth--;rtm--;sa--;ttt--;
		}USART_RX_STA=0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

6、显示温度模块

temp=Get_Temprate();	//得到温度值
		if(temp<0)
		{
			temp=-temp;
			LCD_ShowString(30+10*8,220,16,16,16,"-");	    //显示符号
		}else LCD_ShowString(30+10*8,220,16,16,16," ");	//无符号
		
		LCD_ShowxNum(30+11*8,220,temp/100,2,16,0);		//显示整数部分
		LCD_ShowxNum(30+14*8,220,temp%100,2,16,0);		//显示小数部分
		delay_ms(10);
	}	
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

五、实现效果

5.1 时钟显示和设置界面显示

下图为电子钟的初始界面。
在这里插入图片描述

5.2 设置时间和日期

通过四个按键可以调节日期和时间,并在钟表盘上显示出当前的时间。
在这里插入图片描述

5.3 闹钟设置

可以同时设置三个闹钟。
在这里插入图片描述

5.4 串口调试结果

在这里插入图片描述

六、总结

6.1 已实现的功能

1、表盘、日历、时间、内部温度显示在LCD上;
2、整点报时;
3、通过按键或者串口调试助手设置时钟时间:时、分;
4、通过按键或者串口调试助手设置日历:年、月、日;
5、闹钟设置,可以同时设置三个闹钟,可以随意修改闹钟的时间或者取消;

6.2 欠缺的功能

1、在系统关机后,不能存储设置的闹钟时间记录,并在下次开机时显示。
解决方法:在中断函数exti.c中将设置的闹钟时间存入RTC模块中,并在rtc.c中编写读取闹钟时间的函数,以便于开始后的输出显示。
2、设计较为简单,界面以及输出格式等需要进一步优化。
3、调节日期时间的操作需要通过四个按键来完成,来回切换过程较为繁复,应采取GUI框架来提高界面与操作的便捷性。

6.3 心得体会

在嵌入式的课程设计学习过程中,虽然选择了功能最为明确且较为简单的多能电子钟这个课设项目,但我发现很多硬件设施的原理同样较为难理解,课设的前几天,都在弄明白RTC实时时钟模块的工作原理,后面才开始修改程序的过程。开始的设计是一头雾水,在网上搜寻别人完成的一些基于stm32的电子钟后,并在一个已实现部分功能的项目的基础上,继续完善同时增加新的功能,最终基本实现了相应的功能。
我发现在课设中,很多地方需要重新去学习,即使在期末考试时已经复习过了,但在真正去运用知识时却会无从下手。只有不断学习并掌握新的知识,才能完成更难的项目。

(看到文章最后,如果有需要设计源码和设计报告的小伙伴可以评论或私信我取得哦)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/运维做开发/article/detail/859387
推荐阅读
相关标签
  

闽ICP备14008679号