[原创][连载].基于SOPC的简易数码相框 - Nios II SBTE部分(软件部分) - SD卡(SPI模式)驱动

上一讲,我们完成了Nios II SBTE的配置工作。下面讲解如何根据已有参考资料(手册及代码)编写SD卡驱动。

准备工具及资料

1. WinHex

2. Efronc的博文SD/MMC 接口及上电时序SD/MMC 内部寄存器SD/MMC SPI模式下命令集

驱动编写及调试

步骤1 添加sd_card文件夹到APP工程路径

如何添加,请参考[原创][连载].基于SOPC的简易数码相框 – Nios II SBTE部分(软件部分) - 配置工作

image image

步骤2 编写代码

SD卡有很多标准,此处选用最简单的SD 1-线模式,即SPI模式。

代码2.1 sd_card.h

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
#ifndef SD_CARD_H_
#define SD_CARD_H_
 
 
#include "my_types.h"
#include "my_regs.h"
 
 
#define ENABLE_SD_CARD_DEBUG // turn on debug message
 
 
void SD_CARD_Port_Init();
void SD_CARD_Write_Byte(u8 byte);
u8 SD_CARD_Read_Byte();
u8 SD_CARD_Write_CMD(u8 *CMD);
//
u8 SD_CARD_Init();
u8 SD_CARD_Write_Sector(u32 addr,u8 *buf);
u8 SD_CARD_Read_Sector(u8 *CMD,u8 *buf,u16 n_bytes);
u8 SD_CARD_Read_Sector_Start(u32 sector);
void SD_CARD_Read_Data(u16 n_bytes,u8 *buf);
void SD_CARD_Read_Data_LBA(u32 LBA,u16 n_bytes,u8 *buf);
void SD_CARD_Read_Sector_End();
u8 SD_CARD_Read_CSD(u8 *buf);
u8 SD_CARD_Read_CID(u8 *buf);
void SD_CARD_Get_Info(void);
void SD_CARD_DEMO(void);
 
 
#endif /* SD_CARD_H_ */

第5~6行,加入自定义的宏,统一代码风格。第9行,打开调试信息显示开关。调试正确后,可用添加注释的方式的关闭开关。

第12行void SD_CARD_Port_Init(),为SPI接口的初始函数。

第13~14行void SD_CARD_Write_Byte(u8 byte)和u8 SD_CARD_Read_Byte(),为SPI写字节和读字节函数。

第15行u8 SD_CARD_Write_CMD(u8 *CMD),为SD卡写命令函数。

第17行u8 SD_CARD_Init(),为SD卡的初始化函数。这个函数需要特别注意,因为SPI模式的模式的SD卡需要低速率收发数据来初始化SD卡。

第18~19行u8 SD_CARD_Write_Sector(u32 addr,u8 *buf)和u8 SD_CARD_Read_Sector(u8 *CMD,u8 *buf,u16 n_bytes)为SD卡写块和读块函数;需要注意的是,一般的SD卡的块有512字节,而通过WinHex 查看的SD卡的每个扇区也是512字节。为了统一风格,此处一律写作Sector。

第21行void SD_CARD_Read_Data_LBA(u32 LBA,u16 n_bytes,u8 *buf),比较好用,其参数LBA为Winhex中可查看的扇区地址,及逻辑块地址;有了这个函数,我们后续的工作就方便了需要。

第24~25行u8 SD_CARD_Read_CSD(u8 *buf)和u8 SD_CARD_Read_CID(u8 *buf),为读取SD卡的CSD和CID寄存器函数。

其他的函数请参考源代码自行解析。

代码2.2 sd_card.c

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
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#include <unistd.h>
#include "sd_card.h"
 
 
// debug switch
#ifdef ENABLE_SD_CARD_DEBUG
  #include "debug.h"
  #define SD_CARD_DEBUG(x)  DEBUG(x)
#else
  #define SD_CARD_DEBUG(x)
#endif
 
 
// error macro
#define INIT_CMD0_ERROR   0x01
#define INIT_CMD1_ERROR   0x02
#define WRITE_BLOCK_ERROR 0x03
#define READ_BLOCK_ERROR  0x04
 
 
// SD-CARD(SPI mode) initial with low speed
// insert a certain delay
#define SD_CARD_INIT_DELAY usleep(10)
 
 
// CID info structure
typedef union
{
  u8 data[16];
  struct
  {
    u8 MID;   // Manufacture ID; Binary
    u8 OLD[2];// OEM/Application ID; ASCII
    u8 PNM[5];// Product Name; ASCII
    u8 PRV;   // Product Revision; BCD
    u8 PSN[4];// Serial Number; Binary
    u8 MDT[2];// Manufacture Data Code; BCD; upper 4 bits of first byte are reserved
    u8 CRC;   // CRC7_checksum; Binary; LSB are reserved
  };
}CID_Info_STR;
 
 
// CSD info structure
typedef struct
{
  u8 data[16];
  u32 capacity_MB;
  u8 READ_BL_LEN;
  u16 C_SIZE;
  u8 C_SIZE_MULT;
}CSD_Info_STR;
 
 
// flags
u16 gByteOffset=0;       // byte offset in one sector
u16 gSectorOffset=0;     // sector offset in SD-CARD
bool gSectorOpened=FALSE;// set to 1 when a sector is opened.
bool gSD_CARDInit=FALSE; // set it to 1 when SD-CARD is initialized
 
 
// SD-CARD port init
void SD_CARD_Port_Init()
{
  sd_CLK=1;
  sd_DOUT=1;
  sd_nCS=1;
}
 
 
// write a byte to SD-CARD
void SD_CARD_Write_Byte(u8 byte)
{
  u8 i;
  for(i=0;i<8;i++)
  { // MSB First
    sd_DIN=(byte >> (7-i)) & 0x1;
    sd_CLK=0;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
    sd_CLK=1;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
  }
}
 
 
// read a byte to SD-CARD
u8 SD_CARD_Read_Byte()
{
  u8 i,byte;
  byte=0;
  for(i=0;i<8;i++)
  { // MSB First
    sd_CLK=0;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
    byte<<=1;if(sd_DOUT) byte++;
    sd_CLK=1;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
  }
  return byte;
}
 
 
// write a command to SD-CARD
// return: the second byte of response register of SD-CARD
u8 SD_CARD_Write_CMD(u8 *CMD)
{
  u8 temp,retry;
  u8 i;
 
  sd_nCS=1; // set chipselect (disable SD-CARD)
  SD_CARD_Write_Byte(0xFF); // send 8 clock impulse
  sd_nCS=0; // clear chipselect (enable SD-CARD)
 
  // write 6 bytes command to SD-CARD
  for(i=0;i<6;i++) SD_CARD_Write_Byte(*CMD++);
 
  // get 16 bits response
  SD_CARD_Read_Byte(); // read the first byte, ignore it.
  retry=0;
  do
  { // only last 8 bits is valid
    temp=SD_CARD_Read_Byte();
    retry++;
  }
  while((temp==0xff) && (retry<100));
  return temp;
}
 
 
// SD-CARD initialization(SPI mode)
u8 SD_CARD_Init()
{
  u8 retry,temp;
  u8 i;
  u8 CMD[]={0x40,0x00,0x00,0x00,0x00,0x95};
 
  SD_CARD_Port_Init(); 
  usleep(1000);
 
  SD_CARD_DEBUG(("SD-CARD Init!\n"));
  gSD_CARDInit=TRUE; // Set init flag of SD-CARD
   
  for(i=0;i<10;i++) SD_CARD_Write_Byte(0xff);// send 74 clock at least!!!
 
  // write CMD0 to SD-CARD
  retry=0;
  do
  { // retry 200 times to write CMD0
    temp=SD_CARD_Write_CMD(CMD);
    retry++;
    if(retry==200) return INIT_CMD0_ERROR;// CMD0 error!
  }
  while(temp!=1);
 
  //write CMD1 to SD-CARD
  CMD[0]=0x41;// Command 1
  CMD[5]=0xFF;
  retry=0;
  do
  { // retry 100 times to write CMD1
    temp=SD_CARD_Write_CMD(CMD);
    retry++;
    if(retry==100)  return INIT_CMD1_ERROR;// CMD1 error!
  }
  while(temp!=0);
 
  gSD_CARDInit=FALSE; // clear init flag of SD-CARD
 
  sd_nCS=1; // disable SD-CARD
  SD_CARD_DEBUG(("SD-CARD Init Suc!\n"));
  return 0x55;// All commands have been taken.
}
 
 
// writing a Block(512Byte, 1 sector) to SD-CARD
// return 0 if sector writing is completed.
u8 SD_CARD_Write_Sector(u32 addr,u8 *buf)
{
  u8 temp,retry;
  u16 i;
 
  // CMD24 for writing blocks
  u8 CMD[]={0x58,0x00,0x00,0x00,0x00,0xFF};
  SD_CARD_DEBUG(("Write A Sector Starts!!\n"));
 
  addr=addr << 9;// addr=addr * 512
 
  CMD[1]=((addr & 0xFF000000) >>24 );
  CMD[2]=((addr & 0x00FF0000) >>16 );
  CMD[3]=((addr & 0x0000FF00) >>8 );
 
  // write CMD24 to SD-CARD(write 1 block/512 bytes, 1 sector)
  retry=0;
  do
  { // retry 100 times to write CMD24
    temp=SD_CARD_Write_CMD(CMD);
    retry++;
    if(retry==100) return(temp);//CMD24 error!
  }
  while(temp!=0);
 
  // before writing, send 100 clock to SD-CARD
  for(i=0;i<100;i++) SD_CARD_Read_Byte();
 
  // write start byte to SD-CARD
  SD_CARD_Write_Byte(0xFE);
 
  SD_CARD_DEBUG(("\n"));
  // now write real bolck data(512 bytes) to SD-CARD
  for(i=0;i<512;i++) SD_CARD_Write_Byte(*buf++);
 
  SD_CARD_DEBUG(("CRC-Byte\n"));
  SD_CARD_Write_Byte(0xFF);// dummy CRC
  SD_CARD_Write_Byte(0xFF);// dummy CRC
 
  // read response
  temp=SD_CARD_Read_Byte();
  if( (temp & 0x1F)!=0x05 ) // data block accepted ?
  {
    sd_nCS=1; // disable SD-CARD
    return WRITE_BLOCK_ERROR;// error!
  }
 
  // wait till SD-CARD is not busy
  while(SD_CARD_Read_Byte()!=0xff){};
 
  sd_nCS=1; // disable SD-CARD
 
  SD_CARD_DEBUG(("Write Sector suc!!\n"));
  return 0;
}
 
 
// read bytes in a block(normally 512KB, 1 sector) from SD-CARD
// return 0 if no error.
u8 SD_CARD_Read_Sector(u8 *CMD,u8 *buf,u16 n_bytes)
{
  u16 i;
  u8 retry,temp;
 
  // write CMD to SD-CARD
  retry=0;
  do
  { // Retry 100 times to write CMD
    temp=SD_CARD_Write_CMD(CMD);
    retry++;
    if(retry==100) return READ_BLOCK_ERROR;// block read error!
  }
  while(temp!=0);
 
  // read start byte form SD-CARD (0xFE/Start Byte)
  while(SD_CARD_Read_Byte()!=0xfe);
 
  // read bytes in a block(normally 512KB, 1 sector) from SD-CARD
  for(i=0;i<n_bytes;i++)  *buf++=SD_CARD_Read_Byte();
 
  SD_CARD_Read_Byte();// dummy CRC
  SD_CARD_Read_Byte();// dummy CRC
 
  sd_nCS=1; // disable SD-CARD
  return 0;
}
 
 
// return: [0]-success or something error!
u8 SD_CARD_Read_Sector_Start(u32 sector)
{
  u8 retry;
  // CMD16 for reading Blocks
  u8 CMD[]={0x51,0x00,0x00,0x00,0x00,0xFF};
  u8 temp;
 
  // address conversation(logic block address-->byte address)
  sector=sector << 9;// sector=sector * 512
  CMD[1]=((sector & 0xFF000000) >>24 );
  CMD[2]=((sector & 0x00FF0000) >>16 );
  CMD[3]=((sector & 0x0000FF00) >>8 );
 
  // write CMD16 to SD-CARD
  retry=0;
  do
  {
    temp=SD_CARD_Write_CMD(CMD);
    retry++;
    if(retry==100) return READ_BLOCK_ERROR;// READ_BLOCK_ERROR
  }
  while( temp!=0 );
   
  // read start byte form SD-CARD (feh/start byte)
  while (SD_CARD_Read_Byte() != 0xfe);
 
  SD_CARD_DEBUG(("Open a Sector Succ!\n"));
  gSectorOpened=TRUE;
  return 0;
}
 
 
void SD_CARD_Read_Data(u16 n_bytes,u8 *buf)
{
  u16 i;
  for(i=0;((i<n_bytes) && (gByteOffset<512));i++)
  {
    *buf++=SD_CARD_Read_Byte();
    gByteOffset++;// increase byte offset in a sector
  }
  if(gByteOffset==512)
  {
    SD_CARD_Read_Byte(); // Dummy CRC
    SD_CARD_Read_Byte(); // Dummy CRC
    gByteOffset=0;       // clear byte offset in a sector
    gSectorOffset++;     // one sector is read completely
    gSectorOpened=FALSE; // set to 1 when a sector is opened
    sd_nCS=1;            // disable SD-CARD
  }
}
 
 
// read block date by logic block address(sector offset)
void SD_CARD_Read_Data_LBA(u32 LBA,u16 n_bytes,u8 *buf)
{ // if one sector is read completely; open the next sector
  if(gByteOffset==0) SD_CARD_Read_Sector_Start(LBA);
  SD_CARD_Read_Data(n_bytes,buf);
}
 
 
// dummy read out the rest bytes in a sector
void SD_CARD_Read_Sector_End()
{
  u8 temp[1];
  while((gByteOffset!=0x00) | (gSectorOpened==TRUE))
    SD_CARD_Read_Data(1,temp); // dummy read 
}
 
 
// read CSD registers of SD-CARD
// return 0 if no error.
u8 SD_CARD_Read_CSD(u8 *buf)
{ // command for reading CSD registers
  u8 CMD[]={0x49,0x00,0x00,0x00,0x00,0xFF};
  return SD_CARD_Read_Sector(CMD,buf,16);// read 16 bytes
}
 
 
// read CID register of SD-CARD
// return 0 if no error.
u8 SD_CARD_Read_CID(u8 *buf)
{ // command for reading CID registers
  u8 CMD[]={0x4A,0x00,0x00,0x00,0x00,0xFF};
  return SD_CARD_Read_Sector(CMD,buf,16);//read 16 bytes
}
 
 
void SD_CARD_Get_Info(void)
{
  CID_Info_STR CID;
  CSD_Info_STR CSD;
 
  SD_CARD_Read_CID(CID.data);
  SD_CARD_DEBUG(("SD-CARD CID:\n"));
  SD_CARD_DEBUG(("  Manufacturer ID(MID): 0x%.2X\n", CID.MID));
  SD_CARD_DEBUG(("  OEM/Application ID(OLD): %c%c\n", CID.OLD[0], CID.OLD[1]));
  SD_CARD_DEBUG(("  Product Name(PNM): %c%c%c%c%c\n", CID.PNM[0], CID.PNM[1], CID.PNM[2], CID.PNM[3], CID.PNM[4]));
  SD_CARD_DEBUG(("  Product Revision: 0x%.2X\n", CID.PRV));
  SD_CARD_DEBUG(("  Serial Number(PSN): 0x%.2X%.2X%.2X%.2X\n", CID.PSN[0], CID.PSN[1], CID.PSN[2], CID.PSN[3]));
  SD_CARD_DEBUG(("  Manufacture Date Code(MDT): 0x%.1X%.2X\n", CID.MDT[0] & 0x0F, CID.MDT[1]));
  SD_CARD_DEBUG(("  CRC-7 Checksum(CRC7):0x%.2X\n", CID.CRC >> 1));
 
  SD_CARD_Read_CSD(CSD.data);
  CSD.C_SIZE = ((CSD.data[6]&0x03) << 10) | (CSD.data[7] << 2) | ((CSD.data[8]&0xC0) >>6);
  CSD.C_SIZE_MULT = ((CSD.data[9]&0x03) << 1) | ((CSD.data[10]&0x80) >> 7);
  CSD.READ_BL_LEN = (CSD.data[5]&0x0F);
  CSD.capacity_MB = (((CSD.C_SIZE)+1) << (((CSD.C_SIZE_MULT) +2) + (CSD.READ_BL_LEN))) >> 20;
  SD_CARD_DEBUG(("SD-CARD CSD:\n"));
  SD_CARD_DEBUG(("  max.read data block length: %d\n", 1<<CSD.READ_BL_LEN));
  SD_CARD_DEBUG(("  device size: %d\n", CSD.C_SIZE));
  SD_CARD_DEBUG(("  device size multiplier: %d\n", CSD.C_SIZE_MULT));
  SD_CARD_DEBUG(("  device capacity: %d MB\n", CSD.capacity_MB));
}
 
 
void SD_CARD_DEMO(void)
{
  u16 i;
  u8 buf[512];
 
  // init SD-CARD
  while(SD_CARD_Init() != 0x55);
  // Get CID & CSD
  SD_CARD_Get_Info();
  // read the 1st block(sector) of SD-Card
  SD_CARD_Read_Data_LBA(0,512,buf);
  for(i=0; i<512; i++)
  {
    SD_CARD_DEBUG(("%.2X ", buf[i]));
    if((i+1) % 16 == 0) SD_CARD_DEBUG(("\n"));
  }
}

源码很长,我简单说明其中比较重要的几点。

第58行,申明一个bool型的全局变量bool gSD_CARDInit=FALSE;我们在u8 SD_CARD_Init()函数中将此变量置一或清零,然后在函数void SD_CARD_Write_Byte(u8 byte)和u8 SD_CARD_Read_Byte()检测此变量,以实现慢速率SPI初始化SD卡。

我们拿void SD_CARD_Write_Byte(u8 byte)做说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
// read a byte to SD-CARD
u8 SD_CARD_Read_Byte()
{
  u8 i,byte;
  byte=0;
  for(i=0;i<8;i++)
  { // MSB First
    sd_CLK=0;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
    byte<<=1;if(sd_DOUT) byte++;
    sd_CLK=1;if(gSD_CARDInit) SD_CARD_INIT_DELAY;
  }
  return byte;
}

由于是采用GPIO模拟SPI总线,而Nios II(100MHz nios/f)的GPIO比较慢,因此无需延时即可实现25MHz的速率。但是初始化SD卡的时候必须采用

低于400KHz的时钟,需要插入适当延时。我以前也说过Nios II的延时不准,故此延时需要多次调试。我在第23行,使用一个宏来设定需要插入的延时。

由于CID寄存器的信息与字节比较对齐,因此第27~40行,使用了联合体来储存CID寄存器。而CSD寄存器内容比较零散,就没有采用联合体,而是使用了结构体(第44~51行)来存储信息。这样做的目的主要是为了理解方便,但是对存储器是比较浪费的。

第326行void SD_CARD_DEMO(void)函数中,先初始化SD卡,然后读取其第0个块(扇区)的内容。

关于SD(SPI)的寄存器结构、存储结构和指令体系等,请自行认真阅读相关资料,此处不解析。

步骤3 调用SD卡驱动函数

代码3.1 main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>                    // printf()
#include <unistd.h>                   // usleep()
#include "my_types.h"                 // 数据类型
#include "debug.h"                    // debug
#include "sd_card.h"
 
 
#define ENABLE_APP_DEBUG // turn on debug message
#ifdef ENABLE_APP_DEBUG
    #define APP_DEBUG(x)    DEBUG(x)
#else
    #define APP_DEBUG(x)
#endif
 
 
int main(void)
{
  SD_CARD_DEMO();
  while(1)
  {
  }
  return 0;
}

jtag-uart打印的信息截图如下。

image (黄色为SD卡初始化调试信息;绿色为CID寄存器信息;青色为CSD寄存器信息)

image (第0扇区的内容)

下面我们通过WinHex读取SD卡的第一扇区的内容,注意与上图对比。

image 

对比数据显示,SD_CARD_Read_Data_LBA函数可实现SD卡块读取动作。

其他问题

下面讲下如何在SD卡内读取二进制文件。我先使用Notspad++(或记事本)新建一个文件,保存为SD卡的某个位置,命名为test.bin。

简单起见,我直接把sd_card.h另存到我的SD卡内(FAT32格式),命名为test.bin。

image 

image  (查看test.bin的属性)

在FAT16/32内,文件的数据总是从某个扇区的0字节开始连续存储的,若文件较大则需要连续存储n个扇区;需要注意的是最后的一个扇区如果没有存满,则补0。上面我们通过查看属性,得知test.bin的文件大小为718字节,即需要占用718/512=1.4,取2,即2个扇区。下面使用WinHex来查看文件的数据如何存储。Crtl+F7,打开目录查看器,选择test.bin文件。注意到test.bin的标识id和左下角显示的扇区地址移植。拖动

image

拖动文本,直到文本的结尾。 观察imageimage ,即占用了第81336和81337两个扇区。知道了扇区地址和扇区内的字节偏移,即可使用void SD_CARD_Read_Data_LBA(u32 LBA,u16 n_bytes,u8 *buf)函数读取到想要的数据。

image 

源码下载

 lcd_at_nios_nii_part.zip 

目录

[原创][连载].基于SOPC的简易数码相框 -  Quartus II部分(硬件部分)

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  配置工作

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  SD卡(SPI模式)驱动

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  TFT-LCD(控制器为ILI9325)驱动

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  从SD卡内读取图片文件,然后显示在TFT-LCD上

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  优化工作

[原创][连载].基于SOPC的简易数码相框 -  Nios II SBTE部分(软件部分)-  ADS7843触摸屏驱动测试

posted @   _安德鲁  阅读(5785)  评论(3编辑  收藏  举报
点击右上角即可分享
微信分享提示