51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)

智能手表

  • 项目说明
  • 硬件设计
    • 温度传感器
      • 计算温度
      • 读取温度
    • 定时器中断
      • 工作方式
      • 定时器配置
  • 硬件实物连接
  • 软件设计
    • temp.h
    • temp.c
    • main.c
  • 实验现象与总结

项目说明

用普中51和keil uVision4实现智能手表,功能包括:显示秒表、时间、日期、星期、设置闹钟,读取温度,指示灯亮等。综合的实验主要有独立按键、指示灯亮、温度传感器、定时器、动态数码管显示、蜂鸣器实验,其中本篇博文重要讲解定时器和温度传感器实验,其他结合的实验比较简单,自行查阅资料即可明白。

硬件设计

温度传感器

这个项目我用的是精度较高的外部 DS18B20 数字温度传感器,由于此传感器是单总线接口,所以需要使用 51 单片机的一个 IO 口模拟单总线时序与 DS18B20 通信,将检测的环境温度读取出来。
DS18B20 外观实物如下图所示:
51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第1张图片

**注意:**如果把传感器插反,那么电源将短路,传感器就会发烫,很容易损坏,所以一定要注意传感器方向,通常在开发板上都会标出传感器的凸起出,所以只需要把传感器凸起的方向对着开发板凸起方向插入即可。

DS18B20 数字温度传感器内部的一些高速暂存存储器自行查阅资料,这里就跳过了,直接理解它是怎么计算温度值的吧。

计算温度

51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第2张图片

如果测得的温度大于 0,前5 位为‘ 0’,只要将测到的数值乘以 0.0625(默认精度是 12 位)即可得到实际温度;如果温度小于 0,这 5 位为‘ 1’,测到的数值需要取反加 1 再乘以 0.0625 即可得到实际温度。比如我们要计算+85 度,数据输出十六进制是 0X0550,因为高字节的高 5位为 0,表明检测的温度是正温度,0X0550 对应的十进制为 1360,将这个值乘以 12 位精度 0.0625,所以可以得到+85 度。

读取温度

知道了怎么计算温度,接下来我们就来看看如何读取温度数据,由于DS18B20 是单总线器件,所有的单总线器件都要求采用严格的信号时序,以保证
数据的完整性。DS18B20 时序包括如下几种:初始化时序、写(0 和 1)时序、读(0 和 1)时序。 DS18B20 发送所有的命令和数据都是字节的低位在前。
这里主要有这几个信号的时序:(1) 初始化时序、(2)写时序、(3)读时序。时序的详细介绍自行查阅,其实根据字面意思大致也可以猜到是什么意思的。
DS18B20 的典型温度读取过程为:复位→发 SKIP ROM 命令(0XCC)→发开始转换命令(0X44)→延时→复位→发送 SKIP ROM 命令(0XCC)→发读存储器命令(0XBE)→连续读出两个字节数据(即温度)→结束。

定时器中断

学习定时器前需要明白:
①51 单片机有两组定时器/计数器,因为既可以定时,又可以计数,故称之
为定时器/计数器。
②定时器/计数器和单片机的 CPU 是相互独立的。定时器/计数器工作的过程是自动完成的,不需要 CPU 的参与。
③51 单片机中的定时器/计数器是根据机器内部的时钟或者是外部的脉冲信
号对寄存器中的数据加 1。
有了定时器/计数器之后,可以增加单片机的效率,一些简单的重复加 1 的
工作可以交给定时器/计数器处理。CPU 转而处理一些复杂的事情,同时可以实现精确定时作用。

STC89C5X 单片机内有两个可编程的定时/计数器 T0、T1 和一个特殊功能定时器 T2。

工作方式

这里着重讲解定时器常用的工作方式:
计数位数是 16 位,由 TL0 作为低 8 位,TH0 作为高 8 位,组成了16 位加 1 计数器。
计数初值与计数个数的关系为:X=2(16)-N。

定时器配置

这里以定时器 0 为例介绍配置定时器工作方式 1、设定 1ms 初值,开启定时器计数功能以及总中断,如下:

void Timer0Init()
{
TMOD|=0X01;//选择为定时器 0 模式,工作方式 1,仅用 TR0 打开启动
TH0=0XFC; //给定时器赋初值,定时 1ms
TL0=0X18;
ET0=1;//打开定时器 0 中断允许
EA=1;//打开总中断
TR0=1;//打开定时器
}

定时器赋初值百度上会教你怎么赋值的,对于定时器 1 的使用方法是一样的,只是将上述的 0 变为 1 即可。

硬件实物连接

51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第3张图片
51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第4张图片
由于这个线连接的有点绕,所以直接口述还好些吧。
51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第5张图片
上面就是用到的GPIO口,LSA、LSB、LSC指的就是74HC138模块,k1到k8就是独立按键模块,led到led4指的是LED交通灯模块,beep指蜂鸣器模块,DS18B20我连接的是P3^7接口,动态数码管我连接的是P0口(在普中51单片机上是J22连接到J6,就是上图中的8排杜邦线)。

软件设计

上述DS18B20温度传感器的代码实现如下:

temp.h

#ifndef _TEMP_H_
#define _TEMP_H_

#include 
//---重定义关键词---// 
#ifndef uchar
#define uchar unsigned char
#endif

#ifndef uint
#define uint unsigned int
#endif
//--定义使用的 IO 口--// 
sbit DSPORT=P3^7;
//--声明全局函数--// 
void Delay1ms(uint ); 
uchar Ds18b20Init(); 
void Ds18b20WriteByte(uchar com);
uchar Ds18b20ReadByte(); 
void Ds18b20ChangTemp(); 
void Ds18b20ReadTempCom(); 
int Ds18b20ReadTemp();

#endif

temp.c

#include "temp.h"

void Delay1ms(uint y) //延时函数
{ 
uint x; 
for(;y>0;y--)
{ 
for(x=110;x>0;x--);
}
}

uchar Ds18b20Init() //初始化函数
{ 
 uchar i;
 DSPORT = 0; //将总线拉低 480us~960us  
 i = 70; 
 while(i--);//延时 642us 
 DSPORT = 1; //然后拉高总线,如果 DS18B20 做出反应会将在 15us~60us 后总线拉低 
 i = 0; 
 while(DSPORT) //等待 DS18B20 拉低总线 
 { 
  Delay1ms(1); 
  i++; 
  if(i>5)//等待>5ms 
  { 
  return 0;//初始化失败 
  }
} 
return 1;//初始化成功
}

void Ds18b20WriteByte(uchar dat) //向 18B20 写入一个字节 
{
uint i, j;
for(j=0; j<8; j++) 
{ 
DSPORT = 0; //每写入一位数据之前先把总线拉低 1us  
i++; 
DSPORT = dat & 0x01; //然后写入一个数据,从最低位开始 
i=6; 
while(i--); //延时 68us,持续时间最少 60us
DSPORT = 1; //然后释放总线,至少 1us 给总线恢复时间才能接 着写入第二个数值  
dat >>= 1; 
} 
} 

uchar Ds18b20ReadByte() //读取一个字节 
{ 
uchar byte, bi; 
uint i, j; 
for(j=8; j>0; j--) 
{ 
DSPORT = 0; 
i++; 
DSPORT = 1; 
i++; 
i++;//延时 6us 等待数据稳定  
bi = DSPORT; //读取数据,从最低位开始读取  
/*将 byte 左移一位,然后与上右移 7 位后的 bi,注意移动之后移掉 那位补 0*/ 
byte = (byte >> 1) | (bi << 7); 
i = 4; 
while(i--); 
} 
return byte; 
}

void Ds18b20ChangTemp() //让 18b20 开始转换温度 
{ 
Ds18b20Init(); 
Delay1ms(1); 
Ds18b20WriteByte(0xcc); //跳过 ROM 操作命令 
Ds18b20WriteByte(0x44); //温度转换命令 
//Delay1ms(100); 
//等待转换成功,而如果你是一直刷着的话,就不用这个延时了
}

void Ds18b20ReadTempCom() //发送读取温度命令
{
Ds18b20Init(); 
Delay1ms(1); 
Ds18b20WriteByte(0xcc); //跳过 ROM 操作命令  
Ds18b20WriteByte(0xbe); //发送读取温度命令
}

int Ds18b20ReadTemp() //读取温度
{ 
int temp = 0; 
uchar tmh, tml; 
Ds18b20ChangTemp(); //先写入转换命令 
Ds18b20ReadTempCom(); //然后等待转换完后发送读取温度命令
tml = Ds18b20ReadByte(); //读取温度值共 16 位,先读低字节  
tmh = Ds18b20ReadByte(); //再读高字节 
temp = tmh; 
temp <<= 8; 
temp |= tml;
return temp; 
}

最后在主函数一并实现定时器中断实验:

main.c

#include "reg52.h"    //此文件中定义了单片机的一些特殊功能寄存器
#include "temp.h"
typedef unsigned int u16;   //对数据类型进行声明定义
typedef unsigned char u8;
//--定义使用的 IO 口--//
sbit LSA=P2^2;
sbit LSB=P2^3;
sbit LSC=P2^4;
sbit k1=P3^1;
sbit k2=P3^2;
sbit k3=P3^3;
sbit k4=P3^4;
sbit k5=P3^5;
sbit k6=P3^6;
sbit k7=P3^0;
sbit led=P2^0;
sbit led1=P2^1;
sbit led2=P2^5;
sbit led3=P2^6;
sbit beep=P2^7;
sbit k8=P1^0;
sbit led4=P1^1;

u8 code smgduan[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x070x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};//显示0~F的值

u16 hour=15,minute=32,second=30;//自己设置初始时间,全局变量
u16 year=2020,month=6,day=30,week=2;//自己设置初始日期、星期,全局变量
u8 ssec,sec,min; //毫秒,秒,分
//不同功能的数码管显示//
u8 DisplayData[8];
u8 DisplayData1[8];
u8 DisplayData2[8];
u8 DisplayData3[8];
u8 DisplayData4[8];

void delay(u16 i)
{
 while(i--); 
}

void keypros() //按键处理函数
{ 
if(k1==0) //检测按键 K1 是否按下 
{ 
delay(1000); //消除抖动 一般大约 10ms 
if(k1==0) //再次判断按键是否按下 
{
//--只留当前指示灯亮,其他指示灯灭--//
led1=1;
led=1;
led3=1;
led4=1; 
led=~led; //led 状态取反 
} 
while(!k1); //检测按键是否松开 
}
//--其它按键注释一样,下面就不注释了--//
if(k2==0)  
{
delay(1000); 
if(k2==0)  
{ 
led=1;
led2=1;
led3=1;
led4=1;
led1=~led1;  
} 
while(!k2);  
}
if(k3==0)  
{
delay(1000); 
if(k3==0)  
{ 
led=1;
led1=1;
led3=1;
led4=1;
led2=~led2;  
} 
while(!k3);  
}
if(k4==0)  
{
delay(1000); 
if(k4==0)  
{ 
led=1;
led1=1;
led2=1;
led4=1;
led3=~led3;  
} 
while(!k4);  
}
if(k8==0)  
{
delay(1000); 
if(k8==0)  
{ 
led=1;
led1=1;
led2=1;
led3=1;
led4=~led4;  
} 
while(!k8);  
}
}

void datapros1(int temp) //温度传感器数码管数字设置
{ 
float tp; 
if(temp< 0) //当温度值为负数 
{ 
DisplayData[0] = 0x40; //因为读取的温度是实际温度的补码,所以减 1,再取反求出原码 
temp=temp-1; 
temp=~temp; 
tp=temp; 
temp=tp*0.0625*100+0.5;
}
else 
{ 
DisplayData[0] = 0x00; 
tp=temp;//因为数据处理有小数点所以将温度赋给一个浮点型变量 
//如果温度是正的那么,那么正数的原码就是补码它本身 
temp=tp*0.0625*100+0.5; 
//留两个小数点就*100,+0.5 是四舍五入,因为 C 语言浮点数转换为整型的时候把小数点 //后面的数自动去掉,不管是否大于 0.5,而+0.5 之后大于 0.5 的 就是进 1 了,小于 0.5 的就 //算加上 0.5,还是在小数点后面。 
} 
DisplayData1[1] = smgduan[temp / 10000]; 
DisplayData1[2] = smgduan[temp % 10000 / 1000]; 
DisplayData1[3] = smgduan[temp % 1000 / 100] | 0x80; 
DisplayData1[4] = smgduan[temp % 100 / 10]; 
DisplayData1[5] = smgduan[temp % 10]; 
}

void Timer0Init()//定时器0初始化
{
 TMOD|=0X01;//选择为定时器0模式,工作方式1,仅用TR0打开启动。
 TH0=0Xd8; //给定时器赋初值,定时10ms
 TL0=0Xf0; 
 ET0=1;//打开定时器0中断允许
 EA=1;//打开总中断
 TR0=1;//打开定时器   
}

void DigDisplay1() //温度传感器数码管数字显示
{ 
u8 i; 
for(i=0;i<6;i++) 
{ 
switch(i) //位选,选择点亮的数码管, 
{ 
case(0): LSA=0;LSB=0;LSC=0; break;//显示第 0 位 
case(1): LSA=1;LSB=0;LSC=0; break;//显示第 1 位 
case(2): LSA=0;LSB=1;LSC=0; break;//显示第 2 位 
case(3): LSA=1;LSB=1;LSC=0; break;//显示第 3 位 
case(4): LSA=0;LSB=0;LSC=1; break;//显示第 4 位 
case(5): LSA=1;LSB=0;LSC=1; break;//显示第 5 位
} 
P0=DisplayData1[i];//发送数据 
delay(100); //间隔一段时间扫描 
P0=0x00;//消隐
}
}

void DigDisplay2()//时间数码管数字显示
{
   u8 i;
 for(i=0;i<8;i++)
 {
  switch(i)  //位选,选择点亮的数码管,
  {
   case(0):
    LSA=0;LSB=0;LSC=0; break;//显示第0位
   case(1):
    LSA=1;LSB=0;LSC=0; break;//显示第1位
   case(2):
    LSA=0;LSB=1;LSC=0; break;//显示第2位
   case(3):
    LSA=1;LSB=1;LSC=0; break;//显示第3位
   case(4):
    LSA=0;LSB=0;LSC=1; break;//显示第4位
   case(5):
    LSA=1;LSB=0;LSC=1; break;//显示第5位
   case(6):
    LSA=0;LSB=1;LSC=1; break;//显示第6位
   case(7):
    LSA=1;LSB=1;LSC=1; break;//显示第7位 
  }
  P0=DisplayData2[i];//发送段码
  delay(100); //间隔一段时间扫描 
  P0=0x00;//消隐
 } 
}

void datapros2()//时间数码管数字设置
{
 DisplayData2[0]=smgduan[hour/10];
 DisplayData2[1]=smgduan[hour%10];
 DisplayData2[2]=0x40; 
 DisplayData2[3]=smgduan[minute/10];
 DisplayData2[4]=smgduan[minute%10];
 DisplayData2[5]=0x40;
 DisplayData2[6]=smgduan[second/10];
 DisplayData2[7]=smgduan[second%10]; 
}

void datapros4()//星期数码管数字设置
{
 DisplayData4[0]=0x00;
 DisplayData4[1]=0x00;
 DisplayData4[2]=0x00; 
 DisplayData4[3]=0x00;
 DisplayData4[4]=0x00;
 DisplayData4[5]=0x00;
 DisplayData4[6]=0x00;
 DisplayData4[7]=smgduan[week%10]; 
}

void DigDisplay4()//星期数码管数字显示
{
   u8 i;
 for(i=0;i<8;i++)
 {
  switch(i)  //位选,选择点亮的数码管,
  {
   case(0):
    LSA=0;LSB=0;LSC=0; break;//显示第0位
   case(1):
    LSA=1;LSB=0;LSC=0; break;//显示第1位
   case(2):
    LSA=0;LSB=1;LSC=0; break;//显示第2位
   case(3):
    LSA=1;LSB=1;LSC=0; break;//显示第3位
   case(4):
    LSA=0;LSB=0;LSC=1; break;//显示第4位
   case(5):
    LSA=1;LSB=0;LSC=1; break;//显示第5位
   case(6):
    LSA=0;LSB=1;LSC=1; break;//显示第6位
   case(7):
    LSA=1;LSB=1;LSC=1; break;//显示第7位 
  }
  P0=DisplayData4[i];//发送段码
  delay(100); //间隔一段时间扫描 
  P0=0x00;//消隐
 } 
}

void DigDisplay3()//日期数码管数字显示
{
   u8 i;
 for(i=0;i<8;i++)
 {
  switch(i)  //位选,选择点亮的数码管,
  {
   case(0):
    LSA=0;LSB=0;LSC=0; break;//显示第0位
   case(1):
    LSA=1;LSB=0;LSC=0; break;//显示第1位
   case(2):
    LSA=0;LSB=1;LSC=0; break;//显示第2位
   case(3):
    LSA=1;LSB=1;LSC=0; break;//显示第3位
   case(4):
    LSA=0;LSB=0;LSC=1; break;//显示第4位
   case(5):
    LSA=1;LSB=0;LSC=1; break;//显示第5位
   case(6):
    LSA=0;LSB=1;LSC=1; break;//显示第6位
   case(7):
    LSA=1;LSB=1;LSC=1; break;//显示第7位 
  }
  P0=DisplayData3[i];//发送段码
  delay(100); //间隔一段时间扫描 
  P0=0x00;//消隐
 } 
}

void datapros3()//日期数码管数字设置
{
 DisplayData3[0]=smgduan[year/1000];
 DisplayData3[1]=smgduan[year/100%10];
 DisplayData3[2]=smgduan[year/10%10]; 
 DisplayData3[3]=smgduan[year%10];
 DisplayData3[4]=smgduan[month/10];
 DisplayData3[5]=smgduan[month%10];
 DisplayData3[6]=smgduan[day/10];
 DisplayData3[7]=smgduan[day%10]; 
}

void DigDisplay()//秒表数码管数字显示
{
 u8 i;
 for(i=0;i<8;i++)
 {
  switch(i)  //位选,选择点亮的数码管,
  {
   case(0):
    LSA=0;LSB=0;LSC=0; break;//显示第0位
   case(1):
    LSA=1;LSB=0;LSC=0; break;//显示第1位
   case(2):
    LSA=0;LSB=1;LSC=0; break;//显示第2位
   case(3):
    LSA=1;LSB=1;LSC=0; break;//显示第3位
   case(4):
    LSA=0;LSB=0;LSC=1; break;//显示第4位
   case(5):
    LSA=1;LSB=0;LSC=1; break;//显示第5位
   case(6):
    LSA=0;LSB=1;LSC=1; break;//显示第6位
   case(7):
    LSA=1;LSB=1;LSC=1; break;//显示第7位 
  }
  P0=DisplayData[i];//发送段码
  delay(100); //间隔一段时间扫描 
  P0=0x00;//消隐
 } 
}

void datapros()//秒表数码管数字设置
{
 DisplayData[0]=smgduan[min/10];
 DisplayData[1]=smgduan[min%10];
 DisplayData[2]=0x40; 
 DisplayData[3]=smgduan[sec/10];
 DisplayData[4]=smgduan[sec%10];
 DisplayData[5]=0x40;
 DisplayData[6]=smgduan[ssec/10];
 DisplayData[7]=smgduan[ssec%10]; 
}

void main()
{
//--初始化指示灯全灭--//
 led=1;
 led1=1;
 led2=1;
 led3=1;
 led4=1; 
 Timer0Init();  //定时器0初始化
 while(1)
 {
 keypros();
 while(hour==16 && minute==50)//设置闹钟
 {
    beep=~beep;
    delay(100);
    if(led2==0)
    {
    datapros2();
  DigDisplay2();
    }
 }
 if(led==0)//秒表功能       
 {
  datapros();
  DigDisplay(); 
 }
 if(led1==0)//读取温度
 {
 datapros1(Ds18b20ReadTemp()); //数据处理函数 
    DigDisplay1();//数码管显示函数 
 }
 if(led2==0)//显示时间
 {
    datapros2();
  DigDisplay2();
 }
 if(led3==0)//显示日期
 {
    datapros3();
  DigDisplay3();
 } 
  if(led4==0)//显示星期
 {
    datapros4();
  DigDisplay4();
 } 
 }   
}

void Timer0() interrupt 1//定时器0中断函数
{
  if(k5==0 && led2==0)//小时中断
     {
  delay(1000);
     if(k5==0)  
     {
     hour++;
  if(hour==24)
  {
  hour=0;
  week++;
  day++;
  }
     }
     while(!k5);
     }
     
        if(k6==0 && led2==0)//分钟中断
        {
        delay(1000);
     if(k6==0)  
     {
     minute++;
  if(minute==60)
  {
  minute=0;
  hour++;
  }
     }
     while(!k6);
        }
        
if(k7==0 && led2==0)//秒中断
{
    delay(1000);
 if(k7==0)  
 {
 second++;
 if(second==60)
  {
  second=0;
  minute++;
  }
 }
 while(!k7);
}

if(k5==0 && led3==0)//年中断
{
    delay(1000);
 if(k5==0)  
 {
 year++;
 if(year==10000)
  {
  year=0;
  }
 }
 while(!k5);
}

if(k6==0 && led3==0)//月中断
{
    delay(1000);
 if(k6==0)  
 {
 month++;
 if(month==13)
  {
  month=1;
  year++;
  }
 }
 while(!k6);
}

if(k7==0 && led3==0)//天中断
{
    delay(1000);
 if(k7==0)  
 {
 day++;
 week++;
 if(week==8)//星期中断
 {
 week=1;
 }
 if(month==1 || month==3 || month==5 || month==7 || month==8 || month==10)//月份对应的天数中断
  {
  if(day==32)
  {
  day=1;
  month++;
  }
  }
 if(month==4 || month==6 || month==9 || month==11)
  {
  if(day==31)
  {
  day=1;
  month++;
  }
  } 
  //2月中断,判断闰年和平年//
 if(month==2)
  {
  if(year%4==0 && year%100!=0 &&day==30)
  {
  day=1;
  month++;
  }
  if(year%400==0 && day==30)
  {
  day=1;
  month++;
  }
  if(year%4!=0 && day==29)
  {
   day=1;
  month++;
  }
  if(year%4==0 && year%100==0 && year%400!=0 && day==29)
  {
  day=1;
  month++;
  }
  }
 }
 while(!k7);
}

 TH0=0Xd8; //给定时器赋初值,定时10ms
 TL0=0Xf0;
 ssec++;
 if(ssec>=100)  //100*ms=1s
 {
  ssec=0;
  sec++;
     second++;
  if(second==60)//秒中断
  {
  second=0;
  minute++;
  if(minute==60)
  {
  minute=0;
  hour++;
  if(hour==24)
  {
  hour=0;
  day++;
  }   
  //与上面的注释雷同//
  if(month==1 || month==3 || month==5 || month==7 || month==8 || month==10)
  {
  if(day==32)
  {
  day=1;
  month++;
  }
  }
  if(month==4 || month==6 || month==9 || month==11)
  {
  if(day==31)
  {
  day=1;
  month++;
  }
  } 
  if(month==2)
  {
if(year%4==0 && year%100!=0 &&day==30)
  {
  day=1;
  month++;
  }
  if(year%400==0 && day==30)
  {
  day=1;
  month++;
  }
  if(year%4!=0 && day==29)
  {
   day=1;
  month++;
  }
  if(year%4==0 && year%100==0 && year%400!=0 && day==29)
  {
  day=1;
  month++;
  }
  }
  if(month==12)
  {
  month=1;
  year++;
  if(year==10000)
  {
  year=0;
  }
  }
  }
  }
  if(sec>=60)
  {
   sec=0;
   min++;
   if(min>=60)
   {
    min=0;
   }
  }  
 } 
}

结合主函数main.c和temp.h和temp.c函数在keil4里面运行即可产生hex文件,接下来烧录单片机即可。

实验现象与总结

51单片机实现智能手表(秒表功能、读取温度、显示和修改时间日期、设置闹钟、显示星期)_第6张图片

烧录单片机后,单片机运行:

51单片机实现智能手表

大致实现就这样,觉得本篇博文不错的,也可以给博客点个赞、关注哟!后面也一起学习吧。

你可能感兴趣的:(51,lqb)