资源描述
摘 要
本论文设计一个基于单片机、液晶1602显示和DS12C887高精度时钟芯片控制于一体的高精度电子时钟。计时器是人类发展以来对于时间观念认知的伟大发明,不少机器设备上也少不了高精度计时器的支持,工业上计时器的应用无处不在,生活中人们根据时间上班,工作,生活,学习……所以高精度稳定的计时器扮演着非常重要的角色。本设计以宏晶公司的STC89C52单片机为控制核心,以液晶屏LCD1602显示器为显示模块,依靠DS12C887芯片高精度计时的特点,可以设计出一个具有显示年月日,星期,时分秒,以及定时闹钟功能为一体的高精度电子时钟,本时钟具有如下特点:
(1) 计时准确,基本无误差,运行10年误差仅1秒;
(2) 可以随意设置时间,包括年月日,星期,时分秒,闹钟开/关,时间;
(3) 系统掉电后,时钟仍可精确计时10年;
(4) 系统上电后,自动恢复正常时间;
(5) 本系统运行稳定,其优点是硬件电路简单,软件功能完善,控制系统可靠,性价比较高等,具有一定的实用和参考价值。
关键词:STC89S52单片机 LCD1602 DS12C887 精确计时
目 录
摘 要 1
目 录 2
1 引 言 3
2 总体设计 4
2.1基本原理 4
2.2系统总体框图及设计思路 8
3 详细设计 9
3.1 硬件设计 9
3.2 软件设计. 13
3.2.1程序设计思路 13
3.2.2 程序流程图 13
3.2.3 程序代码 14
4 系统调试及分析 26
5 心得体会 28
参考文献 29
1 引 言
单片微型计算机是大规模集成电路技术发展的产物,属第四代电子计算机,它具有高性能、高速度、体积小、价格低廉、稳定可靠、应用广泛的特点。它的应用必定导致传统的控制技术从根本上发生变革。因此,单片机的开发应用已成为高科技和工程领域的一项重大课题。
计时器是人类发展以来对于时间观念认知的伟大发明,不少机器设备上也少不了高精度计时器的支持,工业上计时器的应用无处不在,生活中人们根据时间上班,工作……所以高精度稳定的计时器扮演着非常重要的角色。
本文主要对使用单片机设计电子时钟进行了分析,并介绍了基于单片机电子时钟硬件组成。利用单片机为控制核心,以液晶屏LCD1602显示器为显示模块,依靠DS12C887芯片高精度计时的特点,可以设计出一个具有显示年月日,星期,时分秒,以及定时闹钟功能为一体的高精度电子时钟。并且本文分别从原理图,主要芯片,以及程序的调试来详细阐述。
如果直接使用单片机进行定时、计时,那么单片机运行代码时,难免会因环境、人为操作等因素导致时间不能准确,产生一定的误差等,如果没有特殊的方法,系统意外掉电时,时间数据会丢失,重启系统时还需重设时间,所以本设计采取使用高精度计时芯片DS12C887方案。
2 总体设计
2.1基本原理
1.单片机控制原理:
它是一种在线式实时控制计算机,在线式就是现场控制,需要的是有较强的抗干扰能力,较低的成本,这也是和离线式计算机的(比如家用PC)的主要区别。
目前最常用的单片机为MCS-51,是由美国INTEL公司(生产CPU的英特尔)生产的,89C51是这几年在我国非常流行的单片机,它是由美国ATMEL公司开发生产的,其内核兼容MCS-51单片机。
单片微型计算机简称单片机,是典型的嵌入式微控制器(Microcontroller Unit),
常用英文字母的缩写MCU表示单片机,单片机又称单片微控制器,它不是完成某一个逻辑功能的芯片,而是把一个计算机系统集成到一个芯片上。单片机由运算器,控制器,存储器,输入输出设备构成,相当于一个微型的计算机(最小系统),和计算机相比,单片机只缺少了I/O设备。概括的讲:一块芯片就成了一台计算机。它的体积小、质量轻、价格便宜、为学习、应用和开发提供了便利条件。同时,学习使用单片机是了解计算机原理与结构的最佳选择。它最早是被用在工业控制领域。
由于单片机在工业控制领域的广泛应用,单片机由芯片内仅有CPU的专用处理器发展而来。最早的设计理念是通过将大量外围设备和CPU集成在一个芯片中,使计算机系统更小,更容易集成进复杂的而对体积要求严格的控制设备当中。
2.DS12C887工作原理:
·在DS12C887内有11字节RAM用来存储时间信息,4字节用来存储控制信息,其具体地址及取值如表1所列。
表1 DS12C887的存储功能
地址
功能
取值范围(十进制)
取值范围
二进制
BCD码
0
秒
0~59
00~3B
00~59
1
秒闹铃
0~59
00~3B
00~59
2
分
0~59
00~3B
00~59
3
分闹铃
0~59
00~3B
00~59
4
12小时模式
1~12
01~0C AM
81~8C PM
01~12 AM
81~92 PM
24小时模式
0~23
00~17
00~23
5
时闹铃,12小时制
1~12
01~0C AM
81~8C PM
01~12 AM
81~92 PM
时闹铃,24小时制
0~23
00~17
00~23
6
星期(星期日=1)
1~7
01~07
01~07
7
日
1~31
01~1F
01~31
8
月
1~12
01~0C
01~12
9
年
0~99
00~63
00~99
10
控制寄存器A
11
控制寄存器B
12
控制寄存器C
13
控制寄存器D
50
世纪
0~99
NA
19,20
3.液晶LCD1602工作原理:
LCD指令表
指令功能
控制线
数据线
RS
R/W
D7
D6
D5
D4
D3
D2
D1
D0
清除屏幕
0
0
0
0
0
0
0
0
0
1
清除屏幕,并把光标移至左上角
光标回到原点
0
0
0
0
0
0
0
0
1
x
光标移至左上角,显示内容不变
设定进入模式
0
0
0
0
0
0
0
1
I/D
S
I/D=1:地址递增,I/D=0:地址递减 S=1:开启显示屏,S=0:关闭显示屏
显示器开关
0
0
0
0
0
0
1
D
C
B
D=1:开启显示幕 C=1:开启光标 B=1:光标所在位置的字符闪烁
移位方式
0
0
0
0
0
1
S/C
R/L
x
x
S/C=0、R/L=0:光标左移;S/C=0、R/L=1:光标右移 S/C=1、R/L=0:字符和光标左移;S/C=1、R/L=1:字符和光标右移
功能设定
0
0
0
0
1
DL
N
F
x
x
DL=1:数据长度为8位,DL=0:数据长度为4位 N=1:双列字,N=0:单列字;F=1:5x10字形,F=0:5x7字形
CG RAM地址设定
0
0
0
1
CG RAM地址
将所要操作的CG RAM地址放入地址计数器
DD RAM地址设定
0
0
1
DD RAM地址
将所要操作的DD RAM地址放入地址计数器
忙碌标志位BF
0
1
BF
地址计数器内容
读取地址计数器,并查询LCM是否忙碌,BF表示LCM忙碌
写入数据
1
0
写入数据
将数据写入CG RAM或DD RAM
读取数据
1
1
读取数据
读取CG RAM或DD RAM的数据
图 10-57 1602LCD 内部显示地址
例如第二行第一个字符的地址是 40H,那么是否直接写入 40H 就可以将光标定位在第二行
第一个字符的位置呢?这样不行,因为写入显示地址时要求最高位 D7恒定为高电平 1 所以
实际写入的数据应该是 01000000B(40H)+10000000B(80H)=11000000B(C0H)。
在对液晶模块的初始化中要先设置其显示模式,在液晶模块显示字符时光标是自动右移的,
无需人工干预。每次输入指令前都要判断液晶模块是否处于忙的状态。
1602 液晶模块内部的字符发生存储器(CGROM)已经存储了 160 个不同的点阵字符图形,
如图 10-58 所示,这些字符有:阿拉伯数字、英文字母的大小写、常用的符号、和日文假名
等,每一个字符都有一个固定的代码,比如大写的英文字母“A”的代码是 01000001B (41H),
显示时模块把地址 41H中的点阵字符图形显示出来,我们就能看到字母“A”
2.2系统总体框图及设计思路
电子时钟
液晶1602显示
人机键盘交互
计时+闹钟
总体设计思路:
本设计利用单片机P0和P2作为并行数据输入输出口,P3.0、 P3.1、 P3.2为功能控制键。其中,按键功能分别控制为时钟功能选择键,增加键和减少键。
3 详细设计
3.1 硬件设计
1.芯片及原理介绍
(一) STC89C52
STC89C52与MCS-51单片机产品兼容 、8K字节在系统可编程Flash存储器、 1000次擦写周期、 全静态操作:0Hz~33MHz 、 三级加密程序存储器 、 32个可编程I/O口线 、三个16位定时器/计数器 八个中断源 、全双工UART串行通道、 低功耗空闲和掉电模式 、掉电后中断可唤醒 、看门狗定时器 、双数据指针 、掉电标识符 。
P1口引脚
特殊功能
P1.0
T2(定时器T2外部输入)
P3口引脚
P3.0
RXD(串行输入口)
P3.1
TXD(串行输出口)
P3.2
(外部中断0)
P3.3
(外部中断1)
P3.4
T0(定时器0外部输入)
P3.5
T1(定时器1外部输入)
P3.6
WR(外部数据存储器写选通)
P3.7
RD(外部数据存储器读先通)
STC89C52的一些特殊功能口,如下表所示:
(二) DS12C887
日历时钟芯片选用DS12C887,其引脚分布如图4所示。
图4 DS12C887引脚分布图
DS12C887的内部结构框图如图5所示。
图5 日历时钟芯片DS12C887内部结构框图
由图5可知,DS12C887内部可看成由电源、日历时钟信息、寄存器和存储器,以及总线接口四部分构成,四部分配合工作,共同实现芯片的功能。[7]
DS12C887的具体引脚功能如下:
·GND、VCC:直流电源,其中VCC接+5V输入,GND接地,当VCC输入为+5V时,用户可以访问DS12C887内RAM中的数据,并可对其进行读、写操作;
当VCC的输入小于+4.25V时,禁止用户对内部RAM进行读、写操作,此时用户不能正确获取芯片内的时间信息;当VCC的输入小于+3V时,DS12C887会自动将电源发换到内部自带的锂电池上,以保证内部的电路能够正常工作。
·MOT:模式选择脚,DS12C887有两种工作模式,即Motorola模式和Intel模式,当 MOT接VCC时,选用的工作模式是Motorola模式,当MOT接GND时,选用的是Intel模式。本设计选用其Intel模式,所以电路图中MOT端接GND。
·SQW:方波输出脚,当供电电压VCC大于4.25V时,SQW脚可进行方波输出,此时用户可以通过对控制寄存器编程来得到13种方波信号的输出。
·AD0~AD7:复用地址数据总线,该总线采用时分复用技术,在总线周期的前半部分,出现在AD0~AD7上的是地址信息,可用以选通DS12C887内的RAM,总线周期的后半部分出现在AD0~AD7上的数据信息。
·AS:地址选通输入脚,在进行读写操作时,AS的上升沿将AD0~AD7上出现的地址信息锁存到DS12C887上,而下一个下降沿清除AD0~AD7上的地址信息,不论是否有效,DS12C887都将执行该操作。
·DS/RD:数据选择或读输入脚,该引脚有两种工作模式,当MOT接VCC时,选用Motorola工作模式,在这种工作模式中,每个总线周期的后一部分的DS为高电平,被称为数据选通。
在读操作中,DS的上升沿使DS12C887将内部数据送往总线AD0~AD7上,以供外部读取。
在写操作中,DS的下降沿将使总线 AD0~AD7上的数据锁存在DS12C887中;当MOT接GND时,选用Intel工作模式,在该模式中,该引脚是读允许输入脚,即Read Enable。因为本设计选用Intel工作模式,所以该引脚是读允许输入脚。
·R/W:读/写输入端,该管脚也有2种工作模式,当MOT接VCC时,R/W工作在Motorola模式。此时,该引脚的作用是区分进行的是读操作还是写操作,当R/W为高电平时为读操作,R/W为低电平时为写操作。
当MOT接GND时,该脚工作在Intel模式,此时该脚作为写允许输入,即Write Enable。
·CS:片选输入,低电平有效。
·IRQ:中断请求输入,低电平有效,该脚有效对DS12C887内的时钟、日历和RAM中的内容没有任何影响,仅对内部的控制寄存器有影响,在典型的应用中,RESET可以直接接到VCC,这样可以保证DS12C887在掉电时,其内部控制寄存器不受影响。
(三) 液晶LCD1602显示器
1602LCD 分为带背光和不带背光两种,基控制器大部分为 HD44780,带背光的比不带背光
的厚,是否带背光在应用中并无差别
1602LCD 主要技术参数:
显示容量:16×2 个字符
芯片工作电压:4.5—5.5V
工作电流:2.0mA(5.0V)
模块最佳工作电压:5.0V
字符尺寸:2.95×4.35(W×H)mm
引脚功能说明
1602LCD 采用标准的 14脚(无背光)或 16脚(带背光)接口,各引脚接口说明如表 10-13
所示:
编号 符号 引脚说明 编号 符号 引脚说明
1 VSS 电源地 9 D2 数据
2 VDD 电源正极 10 D3 数据
3 VL 液晶显示偏压 11 D4 数据
4 RS 数据/命令选择 12 D5 数据
5 R/W 读/写选择 13 D6 数据
6 E 使能信号 14 D7 数据
7 D0 数据 15 BLA 背光源正极
8 D1 数据 16 BLK 背光源负极
表 10-13:引脚接口说明表
第 1 脚:VSS 为地电源。
第 2 脚:VDD接 5V正电源。
第 3 脚:VL为液晶显示器对比度调整端,接正电源时对比度最弱,接地时对比度最高,对
比度过高时会产生“鬼影”,使用时可以通过一个 10K 的电位器调整对比度。
第 4 脚:RS 为寄存器选择,高电平时选择数据寄存器、低电平时选择指令寄存器。
第 5 脚:R/W 为读写信号线,高电平时进行读操作,低电平时进行写操作。当 RS和 R/W
共同为低电平时可以写入指令或者显示地址,当 RS 为低电平 R/W 为高电平时可以读忙信号,当 RS 为高电平 R/W为低电平时可以写入数据。
第 6 脚:E端为使能端,当 E 端由高电平跳变成低电平时,液晶模块执行命令。
第 7~14脚:D0~D7为 8 位双向数据线。
第 15脚:背光源正极。
第 16脚:背光源负极。
LCD寄存器的选择
E
R/W
RS
功能说明
1
0
0
写入命令寄存器
1
0
1
写入数据寄存器
1
1
0
读取忙碌标志及RAM地址
1
1
1
读取RAM数据
0
X
不动作
2.硬件原理图
Sch原理图如图所示:
P0口通过连接lcd作为并行数据输入端,P2口连接DS12C887作为数据输入/输出端口,开关s1、s2、s3作为人机交互接口,时钟的控制端。其他端口功能及控制管脚将在软件设计中提及,在此不再赘述。
3.2 软件设计.
3.2.1程序设计思路
首先,程序启动应先对单片机资源进行初始化操作,其过程包括:打开中断,对蜂鸣器标志位初始化,向ds12c887芯片写入控制字,读取芯片內相应寄存器的时间数据,并将lcd1602进行初始化,写入控制字,写入数据,完成时间日期的显示,然后进入循环中不断重复以下过程:扫描键盘,有键盘按下则执行相应的操作,没有键盘按下则检查闹钟标志位有没有被中断触发,有的话执行响铃函数,否则进行ds12c887芯片的寄存器数据读取,向lcd1602发送相应数据并显示。闹钟模块采用ds12c887的IRQ管脚在闹钟触发时产生低电平,触发外部中断1,蜂鸣器发声。
3.2.2 程序流程图
3.2.3 程序代码
1、预定义部分
//此部分定义了管脚,用到的数据表,数据格式和函数声明
#include<reg52.h> //头文件
#define uchar unsigned char //数据格式宏定义
#define uint unsigned int //数据格式宏定义
sbit rs=P3^5; //lcd寄存器选择
sbit lcden=P3^4; //lcd使能端
sbit s1=P3^0; //开关s1
sbit s2=P3^1; //开关s2
sbit s3=P3^2; //开关s3
sbit beep=P1^2; //蜂鸣器
sbit dscs=P1^4; //ds12c887片选
sbit dsas=P1^5; // ds12c887地址选通输入脚
sbit dsrw=P1^6; // ds12c887读/写输入端
sbit dsds=P1^7; // ds12c887数据选择或读输入脚
sbit dsirq=P3^3; // ds12c887中断请求输入
uchar count,s1num,flag,flag1; //状态变量
char miao,shi,fen,nian,yue,ri,xingqi,amiao,afen,ashi; //数据变量
uchar code table[]=" 20 - -"; //年月日显示格式
uchar code table1[]=" : : "; //时间显示格式
uchar code table2[]="MONTHUWENTHRFRISTASUN"; //星期表,每3位为一个
uchar code table3[]=" ALARM ON "; //开闹钟提示
uchar code table4[]=" ALARM OFF "; //关闹钟提示
uchar code table5[]="SET ALARM PUSH "; //闹钟状态选择提示
void write_ds(uchar,uchar); //函数声明
void set_alarm(uchar,uchar,uchar);
uchar read_ds(uchar);
2、功能控制
void delay(uint z) //延时子函数
{ uint x,y;
for(x=z;x>0;x--)
for(y=110;y>0;y--);
}
void beezzer() //蜂鸣器发声控制函数(闹钟响铃),beep=0发声
{
beep=0;
delay(50);
beep=1;
delay(100);
beep=0;
delay(50);
beep=1;
}
void write_com(uchar com) //向lcd写入控制字com,根据lcd1602时序图进行操作,rs为寄存器选择
{
rs=0;
lcden=0;
P0=com;
delay(5);
lcden=1;
delay(5);
lcden=0;
}
void write_date(uchar date) //向lcd写入数据date,根据lcd1602时序图进行操作
{
rs=1;
lcden=0;
P0=date;
delay(5);
lcden=1;
delay(5);
lcden=0;
}
void init() //初始化单片机
{
uchar num;
EA=1; //开总中断
EX1=1; //允许外部中断1
IT1=1; //低电平触发
beep=1;
flag1=0;
lcden=0;
write_ds(0x0A,0x20); //向ds12c887控制寄存器A发送初始化控制字,启动振荡器
write_ds(0x0B,0x06); //向ds12c887控制寄存器B发送初始化控制字,设定工作模式bcd,24小时制
read_ds(0x0c); //读芯片时间数据
write_com(0x38);
write_com(0x0c); //初始化lcd
write_com(0x06);
write_com(0x01);
write_com(0x80); //在第一行显示数据
for(num=0;num<15;num++)
{
write_date(table[num]);
delay(5);
}
write_com(0x80+0x40); //在第二行显示数据
for(num=0;num<12;num++)
{
write_date(table1[num]);
delay(5);
}
}
void write_sfm(uchar add,uchar date) //向lcd写入时分秒数据
{
uchar shi,ge; //shi:待发数据十位;ge:待发数据个位
shi=date/10;
ge=date%10;
write_com(0x80+0x40+add);
write_date(0x30+shi);
write_date(0x30+ge);
}
void write_nyr(uchar add,uchar date) //向lcd写入年月日数据
{
uchar shi,ge; //shi:待发数据十位;ge:待发数据个位
shi=date/10;
ge=date%10;
write_com(0x80+add);
write_date(0x30+shi);
write_date(0x30+ge);
}
void write_xingqi(uchar add,uchar date) //向lcd写入星期数据
{
write_com(0x80+add);
date=(date-1)*3; //如星期一读表123个字母MON显示,星期二读456字母THU等等
write_date(table2[date]);
write_date(table2[++date]);
write_date(table2[++date]);
}
void keyscan() //键盘扫描子程序
{
if(flag1==1)
{
if(s2==0) //“加”键
{
delay(5);
if(s2==0)
{
while(!s2);
flag1=0;
}
}
if(s3==0) //“减”键
{
delay(5);
if(s3==0)
{
while(!s3);
flag1=0;
}
}
}
if(s1==0) //功能选择键,统计按下次数在s1num中
{
delay(5);
if(s1==0)
{
s1num++;
flag=1;
flag1=0;
while(!s1);
if(s1num==1)
{
TR0=0;
write_com(0x80+0x40+11);
write_com(0x0f);
}
}
if(s1num==2) //以下if语句是使光标在对应位置闪烁
{
write_com(0x80+0x40+8);
}
if(s1num==3)
{
write_com(0x80+0x40+5);
}
if(s1num==4)
{
write_com(0x80+14);
}
if(s1num==5)
{
write_com(0x80+10);
}
if(s1num==6)
{
write_com(0x80+7);
}
if(s1num==7)
{
write_com(0x80+4);
}
if(s1num==8) //此判断语句显示table5表内容,闹钟状态显示
{
uchar n;
write_com(0x80);
for(n=0;n<15;n++)
{
write_date(table5[n]);
}
write_com(0x80+14);
}
if(s1num==9)
{
write_com(0x80+0x40+11);
}
if(s1num==10)
{
write_com(0x80+0x40+8);
}
if(s1num==11)
{
write_com(0x80+0x40+5);
}
if(s1num==12) //执行时间写入ds12c887芯片操作
{
uchar num;
s1num=0;
write_com(0x0c); //设置控制寄存器,状态设置为写
flag=0;
write_ds(0,miao);
write_ds(2,fen);
write_ds(4,shi);
write_ds(6,xingqi);
write_ds(7,ri);
write_ds(8,yue);
write_ds(9,nian);
set_alarm(ashi,afen,amiao);
write_com(0x80);
for(num=0;num<15;num++)
{
write_date(table[num]);
delay(5);
}
write_com(0x80+0x40);
for(num=0;num<12;num++)
{
write_date(table1[num]);
delay(5);
}
}
}
if(s1num!=0) //根据s1num按下次数执行相应的数据设置
{
if(s2==0) //当s2按下时,如果
{
delay(1);
if(s2==0)
{
while(!s2);
if(s1num==1) //按s1按键1次改秒
{
miao++;
if(miao==60)
miao=0;
write_sfm(10,miao);
write_com(0x80+0x40+11);
}
if(s1num==2) //按s1按键2次改分
{
fen++;
if(fen==60)
fen=0;
write_sfm(7,fen);
write_com(0x80+0x40+8);
}
if(s1num==3) //按s1按键3次改时
{
shi++;
if(shi==24)
shi=0;
write_sfm(4,shi);
write_com(0x80+0x40+5);
}
if(s1num==4) //按s1按键4次改星期
{
xingqi++;
if(xingqi==8)
xingqi=1;
write_xingqi(12,xingqi);
write_com(0x80+14);
}
if(s1num==5) //按s1按键5次改日期
{
ri++;
if(ri==32)
ri=1;
write_nyr(9,ri);
write_com(0x80+10);
}
if(s1num==6) //按s1按键6次改月
{
yue++;
if(yue==13)
yue=1;
write_nyr(6,yue);
write_com(0x80+7);
}
if(s1num==7) //按s1按键7次改年
{
nian++;
if(nian==100)
nian=0;
write_nyr(3,nian);
write_com(0x80+4);
}
if(s1num==8) //按s1按键8次改闹钟状态开还是关
{
uchar n;
write_ds(0x0B,0x26); //向B控制寄存器写入控制字
write_com(0x80);
for(n=0;n<15;n++)
{
write_date(table3[n]);
}
write_com(0x80+14);
}
if(s1num==9) //按s1按键9次改闹钟时间秒
{
amiao++;
if(amiao==60)
amiao=0;
write_sfm(10,amiao);
write_com(0x80+0x40+11);
}
if(s1num==10) //按s1按键10次改闹钟时间分
{
afen++;
if(afen==60)
afen=0;
write_sfm(7,afen);
write_com(0x80+0x40+8);
}
if(s1num==11) //按s1按键11次改闹钟时间时
{
ashi++;
if(ashi==24)
ashi=0;
write_sfm(4,ashi);
write_com(0x80+0x40+5);
}
}
}
if(s3==0)
{
delay(1);
if(s3==0) //当s3按下时,如果:
{
while(!s3);
if(s1num==1) //按s1按键1次改秒
{
miao--;
if(miao==-1)
miao=59;
write_sfm(10,miao);
write_com(0x80+0x40+11);
}
if(s1num==2) //按s1按键2次改分
{
fen--;
if(fen==-1)
fen=59;
write_sfm(7,fen);
write_com(0x80+0x40+8);
}
if(s1num==3) //按s1按键3次改时
{
shi--;
if(shi==-1)
shi=23;
write_sfm(4,shi);
write_com(0x80+0x40+5);
}
if(s1num==4) //按s1按键4次改星期
{
xingqi--;
if(xingqi==0)
xingqi=7;
write_xingqi(12,xingqi);
write_com(0x80+14);
}
if(s1num==5) //按s1按键5次改日期
{
ri--;
展开阅读全文