LCD1602驱动完成测试:仿真、硬件皆已通过,8线、4线皆可用,代码、仿真全部奉上
前段时间发了一个LCD1602的驱动(http://www.ourdev.cn/bbs/bbs_content.jsp?bbs_sn=4491097&bbs_page_no=1&search_mode=3&search_text=XIAN1987& bbs_id=9999),可惜没有实物没进行测试,后来感觉很惭愧:没测试的东东都发上去,确实太不地道了。所以周末特地坐了4个小时的地铁去中发买了一个LCD1602,将驱动代码进行实物及Proteus仿真的测试,现在已经全部通过,特上传所有代码和部分图片(手机拍的,不很清晰,包涵):http://cache.amobbs.com/bbs_upload782111/files_35/ourdev_610162B9RZMA.jpg
(原文件名:P100111_10.52.jpg)
http://cache.amobbs.com/bbs_upload782111/files_35/ourdev_610163AHBOGK.jpg
(原文件名:P100111_10.53.jpg)
http://cache.amobbs.com/bbs_upload782111/files_35/ourdev_610164YQ23WI.png
(原文件名:QQ截图未命名1.png)
http://cache.amobbs.com/bbs_upload782111/files_35/ourdev_610165G0JKJA.png
(原文件名:QQ截图未命名.png)
/**************************************************************************************************************
* 文件名称: hd44780.c
* 功能说明: (控制器为HD44780的)LCD1602字符液晶的底层驱动程序
* 注意事项: 1、由于8051单片机的本身操作速度较慢,所以在产生驱动时序时不需要考虑电平转换间的延时问题,但是,将此
* 程序移植到其他如AVR、PIC或者新型的1T 51等单片机时,必须在认真参考HD44780手册中关于时序的内容,对此
* 程序进行相应延时修改
* 2、为了提高上层函数的灵活性,这里的写入函数都没有检查HD44780的忙状态,请在调用这些函数前进行处理,你
* 可以等待忙状态结束,也可以进行合适长度的软件延时,或者在多任务系统中挂起该任务,把执行时间让给其他
* 任务
* 3、由于LCD_DAT既要用于输入又要用于输出,所以一定要注意IO口方向的适时切换,对于51单片机,虽然没有IO口
* 设定,但是其IO口最为输入时有一个限制,即:在读取之前必须先向IO口写1,请千万注意这一点。
* 4、不要以为仅仅通过修改LCD_BUS_WIDTH就可以完成8位传输与4位传输的切换,对于IO口定义LCD_RS、LCD_RW等的
* 定义肯定也是必须的。并且需要注意以下语句:
#define LCD_R_8b(B) { \
byte tmp=0; \
LCD_DAT|=0xF0;\ -----注意这里,这就是51中读取前必须先写1的地方,但是0xF0是因为LCD_DAT
LCD_R_4b(B); \ -是P1的高四位,所以这个数值应该根据你将LCD的4根数据线接的位置来确
tmp = B; \ -定。最可能的变化是你将LCD的4根数据线接到了P1的低四位,那么这个值
LCD_R_4b(B); \ -就应该是0x0F了。 当然了,如果你使用了8位数据线模式,就不存在这个
tmp += B>>4; \ -问题了。
B = tmp; \
}
* 版本日期: 2010年12月31日
* 作者邮件: xian0901@sina.cn
* 升级记录:
***************************************************************************************************************/
#include "typedef.h"
#include "8052Hal.h"
#include "hd44780.h"
/***************************************************************************************************************
--------------------------------------------连接引脚说明--------------------------------------------------------
*
* LCD与51引脚的连接情况,本应用中使用P1.4--P1.7与1602进行四位的数据传输,从而减少MCU引脚的消耗。
* 请根据你的应用中实际的电路连接情况对此处的定义进行修改
***************************************************************************************************************/
sbit LCD_VE = P1^0; //LCD对比调整电压,接正电源时对比度最弱,接地时对比度最高,可使用此引脚产生PWM波形调节对比度——此功能未实现
sbit LCD_RS = P1^1; //数据、命令选通
sbit LCD_RW = P1^2; //读、写操作选通
sbit LCD_EN = P1^3; //使能控制
#define LCD_DAT P1 //数据端口,使用P1口的高四位进行数据传输
#define LCD_RS_C() LCD_RS = 0 //Register Selected:IR Instruction Register,Can only be Written(AC Address Counter 、BF Busy Flag Can Be Read)
#define LCD_RS_D() LCD_RS = 1 //Register Selected:DR Data Register,Can be both Written into and Read from
#define LCD_RW_R() LCD_RW = 1 //选择读操作
#define LCD_RW_W() LCD_RW = 0 //选择写操作
#define LCD_EN_E() LCD_EN = 1 //Data Exchange is Enable
#define LCD_EN_D() LCD_EN = 0 //Data Exchange is Disable,and internal operation starts on the falling edge
#define LCDDelay() _nop_() //至少保证450ns
#if LCD_BUS_WIDTH == 4 //MCU与LCD1602间使用DB7-DB4这4根数据线进行传输
#define LCD_R_4b(B) LCD_EN_E(); \
LCDDelay(); \
B =LCD_DAT; \
LCD_EN_D(); \
LCDDelay();
#define LCD_R_8b(B) { \
byte tmp=0; \
LCD_DAT|=0xF0;\
LCD_R_4b(B); \
tmp = B; \
LCD_R_4b(B); \
tmp += B>>4; \
B = tmp; \
}
#define LCD_W_4b(B) LCD_DAT =((B&0xF0)|(LCD_DAT&0x0F)); \
LCD_EN_E(); \
LCDDelay(); \
LCD_EN_D(); \
LCDDelay();
#define LCD_W_8b(B) LCD_W_4b(B); \
LCD_W_4b(B<<4);
#elif LCD_BUS_WIDTH == 8 //MCU与LCD1602间使用DB7-DB0这8根数据线进行传输
#define LCD_R_8b(B) LCD_EN_E(); \
LCDDelay(); \
LCD_DAT=0xFF; \
B =LCD_DAT; \
LCD_EN_D(); \
LCDDelay();
#define LCD_W_8b(B) LCD_DAT =B; \
LCD_EN_E(); \
LCDDelay(); \
LCD_EN_D(); \
LCDDelay();
#endif
/**************************************************************************************************************
* 函数名称: LCDBusInit()
* 功能说明: LCD1602数据传输数据线宽度设置和显示屏字符显示行数设置
* 输 入:
* 输 出: ui08初始化是否成功 0为成功 其他为不成功
* 注意事项:
***************************************************************************************************************/
ui08 LCDBusInit(void)
{
ui08 cmd = 0x00;
if(!((LCD_BUS_WIDTH == 4) ||
(LCD_BUS_WIDTH == 8)))
{
return RET_ERR;
}
if(!((LCD_LINE_NUMS == 2) ||
(LCD_LINE_NUMS == 1)))
{
return RET_ERR;
}
LCD_EN_D();
#if LCD_BUS_WIDTH == 4
LCD_RS_C();
LCD_RW_W();
LCD_W_4b(0x20); //LCD1602上电默认是8-bit数据传输,本条指令用于将其设定为4-bit传输,注意有且仅有这一条指令是半字节的
Delay50us(); //指令0x20需要40us,此时无法使用忙查询指令,必须通过延时等待该指令执行完成
cmd = 0x20 | (((LCD_LINE_NUMS == 2) ? 1 : 0) << 3);
LCD_WR_CMD(cmd);
while(LCD_IsBusy() == 1);
#elif LCD_BUS_WIDTH == 8
cmd = 0x30 | (((LCD_LINE_NUMS == 2) ? 1 : 0) << 3);
LCD_WR_CMD(cmd);
while(LCD_IsBusy() == 1);
#endif
LCD_WR_CMD(0x01); //这里验证初始化是否成功使用了一个技巧,即0x01本身执行需要1.52ms,那么执行完本指令立即检测Busy位应该得到1,否则初始化不成功
if(LCD_IsBusy() == 1) //另外,检测到Busy位为1后,还要LCD1602能够退出Busy状态才是真正表明成功初始化了。
{
while(LCD_IsBusy() == 1);
return RET_OK;
}
else
{
return RET_ERR;
}
}
/**************************************************************************************************************
* 函数名称: LCD_WR_CMD()
* 功能说明: 向LCD1602写入控制指令
* 输 入: byte cmd指令字,详细指令请参考LCD1062用户手册
* 输 出:
* 注意事项:
***************************************************************************************************************/
void LCD_WR_CMD(byte cmd)
{
LCD_RS_C();
LCD_RW_W();
LCD_W_8b(cmd);
}
/**************************************************************************************************************
* 函数名称: LCD_WR_DAT()
* 功能说明: 向LCD1602的CGRAM或DDRAM当前光标位置写入数据
* 输 入: byte dat要在当前光标位置写入的数据
* 输 出:
* 注意事项: 具体是操作CGRAM还是DDRAM要看你最近一次写地址指令是写的CGRAM地址还是DDRAM地址
***************************************************************************************************************/
void LCD_WR_DAT(byte dat)
{
LCD_RS_D();
LCD_RW_W();
LCD_W_8b(dat);
}
/**************************************************************************************************************
* 函数名称: LCD_IsBusy()
* 功能说明: 忙检测,忙,即LCD1602正在进行内部操作,在操作完场前无法接受新的指令或数据
* 输 入:
* 输 出: ui080:表示LCD1602现在处于空闲状态,可以立即接受指令或数据; 1:表示LCD1602正在进行内部操作
* 注意事项:
***************************************************************************************************************/
ui08 LCD_IsBusy(void)
{
ui08 flg=0;
LCD_RS_C();
LCD_RW_R();
LCD_R_8b(flg);
flg &= 0x80;
if(flg == 0)
{
return 0;
}
else
{
return 1;
}
}
/**************************************************************************************************************
* 函数名称: LCD_GET_AC()
* 功能说明: 获取LCD1602的地址计数器数值,即光标的当前位置,
* 输 入:
* 输 出: ui08地址计数器的数值
* 注意事项:
***************************************************************************************************************/
ui08 LCD_GET_AC(void)
{
ui08 val=0;
LCD_RS_C();
LCD_RW_R();
LCD_R_8b(val);
val &= 0x7F;
returnval;
}
/**************************************************************************************************************
* 函数名称: LCD_RD_DAT()
* 功能说明: 从LCD1602的CGRAM或DDRAM的当前光标位置读取数据
* 输 入:
* 输 出: byte从当前光标位置读取到的数据
* 注意事项: 具体是操作CGRAM还是DDRAM要看你最近一次写地址指令是写的CGRAM地址还是DDRAM地址
***************************************************************************************************************/
byte LCD_RD_DAT(void)
{
byte dat=0;
LCD_RS_D();
LCD_RW_R();
LCD_R_8b(dat);
return dat;
}
/**************************************************************************************************************
* 文件名称: hd44cmd.h
* 功能说明: 为方便HD44780控制命令的使用而定义的宏和结构体
* 注意事项: 易混淆点说明: 指令IorDShift控制的是当读写DDRAM/CGRAM的时候的屏幕和光标的移动
* 指令CorDShift则是让屏幕或光标立即移动
* 版本日期: 2010年12月31日
* 作者邮件: xian0901@sina.cn
* 升级记录:
***************************************************************************************************************/
#ifndef __HD44CMD_H__
#define __HD44CMD_H__
ui08 LCD_ClrScreen(ui08 wait);
ui08 LCD_RetunHome(ui08 wait);
ui08 LCD_BlinkSW(ui08 sw,ui08 wait);
ui08 LCD_CursorSW(ui08 sw,ui08 wait);
ui08 LCD_ScreenSW(ui08 sw,ui08 wait);
ui08 LCD_FontSel(ui08 sel,ui08 wait);
ui08 LCD_CursorDir(ui08 dir,ui08 wait);
ui08 LCD_ScreenDir(ui08 dir,ui08 wait);
ui08 LCD_CursorMov(ui08 dir,ui08 wait);
ui08 LCD_ScreenMov(ui08 dir,ui08 wait);
ui08 LCD_CGRAMAddr(ui08 addr,ui08 wait);
ui08 LCD_DDRAMAddr(ui08 addr,ui08 wait);
/***************************************************************************************************************
--------------------------------------------结构体功能说明------------------------------------------------------
*
由于LCD1602控制器HD44780的每条指令可以执行多个操作,比如指令0x20,既可以选择字体,还可以设定数据总线宽度及液
* 晶显示行数,这样当我们想要使用本条指令只完成一个操作,如字体选择时,为了不改变数据总线宽度和液晶显示行数的设定,
* 我们就不得不使用“读-修改-写”这样的操作。
* 但不幸的是,HD44780仅仅给了我们写命令的能力,却没有提供读设置(状态)的操作,为了解决这个问题,我定义了这个结
* 构体作为HD44780内部寄存器的映像,每个被写入HD44780的设定值都会被同步存入这个结构体的变量中,这样我们就可以通过
* 读取此结构体变量的值来了解LCD1602的当前设定和状态,从而可以完成”读-修改-写“这样的局部修改指令了。
*
* 注意,在HD44780的8条设置指令中,前两条“清显示”和“光标归位”完成的操作单一而明确,没有记录状态的必要;而最后两条
* 指令“设置DDRAM地址”和“设置CGRAM地址”也没有记录状态的意义,因为,一则AC的地址是会随着数据的读写自动调整的,二则H-
* D44780提供的对AC的读取操作,根本没有必要软件跟踪记录。
所以,这个结构体仅仅记录了HD44780中8条设置的中间4条
*
* 说明:指令IorDShift控制的是当读写DDRAM/CGRAM的时候的屏幕和光标的移动
* 指令CorDShift则是让屏幕或光标立即移动
***************************************************************************************************************/
struct HDRegisters {
union {
struct { //字符含义:S Screen I Increment
byte S : 1; //S=1 写数据到DDRAM时整个显示屏右移(I=0)或左移(I=1)从而使得光标好像是静止的
//S=0 写数据到DDRAM时整个显示屏没有移动,通常情况下设置是这样子的
byte I : 1; //I=1 从DDRAM、CGRAM读取数据 或 向DDRAM、CGRAM写入数据时光标右移
//I=0 从DDRAM、CGRAM读取数据 或 向DDRAM、CGRAM写入数据时光标左移
byte : 6;
}bits;
byte value;
}IorDShift;
union {
struct { //字符含义:B Blink C Cursor D Display
byte B : 1; //B=1 显示闪烁 B=0 不显示闪烁
byte C : 1; //C=1 显示光标 C=0 不显示光标
byte D : 1; //D=1 开显示 D=0 关显示
byte : 5;
}bits;
byte value;
}DorCorBOn;
union {
struct {
byte : 2; //字符含义:R Right S Screen
byte R : 1; //R=1 右移 R=0 左移
byte S : 1; //S=1 左移或右移的是屏幕而不是光标 S=0 进行左移或右移的不是屏幕而是光标
byte : 4;
}bits;
byte value;
}CorDShift;
union {
struct {
byte : 2; //字符含义:FA Font 10*5 N2 Number of Lines is 2 I8 Interface Bus 8-bit
byte FA : 1; //FA=1选择字体10*5 FA=0 选择字体8*5
byte N2 : 1; //N2=1两行显示 N2=0 一行显示
byte I8 : 1; //I8=1使用8位IO进行传输 I8=0 使用4位IO进行传输
byte : 3;
}bits;
byte value;
}N4or8Font;
};
extern const struct HDRegisters * const pLCDStat;
#define HD_CMD_ClrScreen 0x01 //Clear display by writting space code 20H into all DDRAM addresses.
//And then do all what command "HD_CMD_RetunHome" do
#define HD_CMD_RetunHome 0x02 //Sets DDRAM address 0 in AC, and returns display from being shifted to original position,
//The cursor or blinking go to the left edge of the display
#define HD_CMD_IorDShift 0x04 //Increments (I/D = 1) or decrements (I/D = 0) the DDRAM address by 1 when a character code
//is written into or read from DDRAM. Shifts the entire display either to the right (I/D = 0) or to the left (I/D = 1) when S is 1
#define HD_CMD_DorCorBOn 0x08 //Sets entire Display(D) ,Cursor(C) and Blinking(B) of cursor position character ON/Off.
#define HD_CMD_CorDShift 0x10 //Shifts the Cursor position or Display to the right or left without writing or reading display data
#define HD_CMD_4or8NFont 0x20 //Sets Interface data length 4-bit or 8-bit, Number of display lines (N), and character Font (F)
#define HD_CMD_CGRAMAddr 0x40 //Sets CGRAM address
#define HD_CMD_DDRAMAddr 0x80 //Sets DDRAM address
#endif //__HD44CMD_H__
/**************************************************************************************************************
* 文件名称: hd44cmd.c
* 功能说明: HD44780的基本命令集函数包装
* 注意事项:
* 版本日期: 2010年12月31日
* 作者邮件: xian0901@sina.cn
* 升级记录:
***************************************************************************************************************/
#include "typedef.h"
#include "8052Hal.h"
#include "hd44780.h"
#include "hd44cmd.h"
static struct HDRegisters hdreg; //用于记录LCD1602内部设定状态
const struct HDRegisters * const pLCDStat = &hdreg; //hdreg的一个只读映像,使得上层函数能够访问到LCD的状态却又
//不能够随意更改,从而避免产生破坏
/**************************************************************************************************************
* 函数名称: LCD_ClrScreen()
* 功能说明: 清显示,即将显示器上的显示内容全部清除
* 输 入: ui08 wait 1 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则等待LCD忙结束然后发送本
* 命令给LCD进行执行
* 0 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则不等待LCD而是立即返回
* 输 出: ui08 报告操作执行的情况,可能的取值有:RET_OK 完成操作,RET_ERR 出错未完成,RET_BSY 忙未完成
* 注意事项: 注意,除了清显示之外,本函数本身还会执行LCD_RetunHome()函数的全部操作
***************************************************************************************************************/
ui08 LCD_ClrScreen(ui08 wait)
{
if(!((wait == 0) ||
(wait == 1)))
{
return RET_ERR;
}
if(wait == 1) //等待LCD忙状态结束,从而尽量完成此次操作
{
while(LCD_IsBusy() == 1)
;
}
else //不等待LCD忙状态结束,如果LCD忙,则立即退出返回
{
if(LCD_IsBusy() == 1)
return RET_BSY;
}
LCD_WR_CMD(HD_CMD_ClrScreen);
return RET_OK;
}
/**************************************************************************************************************
* 函数名称: LCD_RetunHome()
* 功能说明: 将光标移到显示屏最左侧,如果显示字符有被移动的,则将其移到最初位置
* 输 入: ui08 wait 1 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则等待LCD忙结束然后发送本
* 命令给LCD进行执行
* 0 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则不等待LCD而是立即返回
* 输 出: ui08 报告操作执行的情况,可能的取值有:RET_OK 完成操作,RET_ERR 出错未完成,RET_BSY 忙未完成
* 注意事项:
***************************************************************************************************************/
ui08 LCD_RetunHome(ui08 wait)
{
if(!((wait == 0) ||
(wait == 1)))
{
return RET_ERR;
}
if(wait == 1) //等待LCD忙状态结束,从而尽量完成此次操作
{
while(LCD_IsBusy() == 1)
;
}
else //不等待LCD忙状态结束,如果LCD忙,则立即退出返回
{
if(LCD_IsBusy() == 1)
return RET_BSY;
}
LCD_WR_CMD(HD_CMD_RetunHome);
return RET_OK;
}
/**************************************************************************************************************
* 函数名称: LCD_BlinkSW()
* 功能说明: 是否开启闪烁显示
* 输 入: ui08 sw 1 开启闪烁显示 0 关闭闪烁显示
* ui08 wait 1 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则等待LCD忙结束然后发送本
* 命令给LCD进行执行
* 0 如果执行本函数时LCD正在执行内部操作(即忙)而无法接受新命令,则不等待LCD而是立即返回
* 输 出: ui08 报告操作执行的情况,可能的取值有:RET_OK 完成操作,RET_ERR 出错未完成,RET_BSY 忙未完成
* 注意事项:
***************************************************************************************************************/
ui08 LCD_BlinkSW(ui08 sw,ui08 wait)
{
if(!((sw == 0) ||
(sw == 1)))
{
return RET_ERR;
}
if(!((wait == 0) ||
(wait == 1)))
{
return RET_ERR;
}
if(wait == 1) //等待LCD忙状态结束,从而尽量完成此次操作
{
while(LCD_IsBusy() == 1)
;
}
else //不等待LCD忙状态结束,如果LCD忙,则立即退出返回
{
if(LCD_IsBusy() == 1)
return RET_BSY;
}
hdreg.DorCorBOn.bits.B = sw;
LCD_WR_CMD(HD_CMD_DorCorBOn | hdreg.DorCorBOn.value);
return RET_OK;
}
点击此处下载 ourdev_610166H1YV2V.rar(文件大小:681K) (原文件名:LCD1602.rar) 声明:以前发的那个没有验证的代码确实有问题,请下载过的尽快删掉吧(./emotion/em010.gif)! 额一次要包含3个文件,是不是有些繁琐了 顶一个 jh 楼主无私,坚决顶起 回复【2楼】elecfun熊
-----------------------------------------------------------------------
俺的原则是,可重用组件就要写的易懂、易改、分层、可移植。至于实际做产品的话,可以先把这个组件放到产品中让它正常工作起来,然后再对代码进行优化。 很不错! 谢谢牛仔和lz。 精神可嘉 回复【9楼】dalchemist
-----------------------------------------------------------------------
谢谢支持 mark 不错回去试试我的1602 多谢分享 4线的有时间整整 我竟然是从百度绕弯进来的 40脚 1T单片观可以同时做到,2片74HC595接2个4位共阳数码管(占用3个IO),串行2线12864(2个IO.注意RW请接VCC),串行4线1602(占用7个IO). 谢谢楼主分享! mark 回复【15楼】vernalwind
-----------------------------------------------------------------------
很多人都是这么绕弯进来的! mark sbit LCD_VE = P1^0; //LCD对比调整电压,接正电源时对比度最弱,接地时对比度最高,可使用此引脚产生PWM波形调节对比度——此功能未实现
PWM 调15脚背光强弱,这个更加实用.在STC12的40脚下面,可以用PWM0 P1.3口和PWM1 P1.4口实现调背光. 谢谢分享!看看! 谢谢分享!!!! 回复【楼主位】XIAN1987
-----------------------------------------------------------------------
受教了~~~正在尝试自己的1602屏 MARK! MARK,谢谢楼主分享 好。 好久没来学习了,上次那个我好像还回复过。不过我都没弄出来。呵呵呵 不错的驱动,找了半天呢 mark xia 留着备用 不错,一直在找4线的 mark MARK,受教了~~~正在尝试自己的1602屏 ,谢谢楼主分享 不错,一直使用4线 支持一下! 谢谢白娘子 谢谢白娘子 好,最近也在做这个。 哈哈,非常感谢,这下子省IO口了 用了个sed-1278f驱动的屏试了能行,谢谢 注释很清楚,支持一下,以前自己从百度下过1692的资料,后来发现不能用,我觉得最好还是看下1602的使用手册。 写的不错,文档做的也超好,不支持都不行!~ mark 参考一下! 载来看看,最近弄12864还没弄出 mark 感谢分享! mark! 谢谢楼主分享 标记一下吧。用着的时候,拿来就用。 楼主无私,坚决顶起 MARKWell done! 以前看坛子里帖子基于M8和LM2576的数控电源驱动1602就用了4线方案
当时一直没想明白如何实现
4线方案值得好好研究
谢谢楼主分享 cool ! {:handshake:}{:handshake:}{:handshake:} 不错,可参考。 下载看看吖
好东西收藏了~ 准备在STM8上试试,谢了! 这个要顶,经典呀。{:handshake:}收了谢谢了 谢谢分享~!! 多些楼主 先收藏 学习,谢谢!!!{:smile:} mark。。 这类文章较多,不过楼主的可复用率很高,谢谢楼主了 路过。。。。。。 感谢共享,学习一下~~~~~~~~~~~~~~~~~~~ 顶一下!! mark 一下 呵呵 4线有个问题,时序一定要注意,曾经就在这上面卡过壳,ISIS仿真没问题,结果实物上有问题,现象是通电显示混乱,十次只有一两次进入正常模式,在网上请教了几个大师,给了不少有用的建议,
、后来,自已调试了很久,才发现原来是延时过长的原因。(一直认为是3.3V单片机驱动5V液晶屏原因,原来不是) 学习了呢 不错,模块化比较明显!!!!! 多谢分享 不错,感谢分享 不错,MARK一下 这个太好了,谢谢,收藏了 谢谢分享~收藏了 mark,学习了
页:
[1]