基于立创梁山派GD32F470ZGT6——LVGL移植
目录
一、前言
移植LVGL前要先准备好相关屏幕的驱动,本文虽然说是基于GD32F470ZGT6进行移植,但是其移植方法适用各种单片机。如果是使用立创梁山派开发板官方拓展屏的话可以参考我之前的另外一篇文章有详细介绍了如何配置其拓展屏。
基于立创梁山派GD32F470ZGT6移植RGB屏幕拓展板例程
https://blog.csdn.net/m0_69554930/article/details/150592109?spm=1001.2014.3001.5501
二、LVGL库获取及精简
(一)、LVGL库获取
因为LVGL 8 已广泛应用于实际项目,生态成熟、社区活跃、文档全面。相比刚发布不久的 LVGL 9,LVGL 8 更适合希望快速搭建 UI 原型或需要高稳定性的工业项目。而且选择一个成熟版本,可以有效规避移植中可能出现的兼容性问题和 bug,提升开发效率与项目成功率。同时接下来准备使用GUI-Guider1.9.0-GA来设计LVGL图形用户界面,而GUI-Guider1.9.0-GA目前只支持v8.3.10和v9.2.1这两个版本的LVGL,综上考虑这里使用LVGL的版本为v8.3.10。
下载地址为:https://github.com/lvgl/lvgl。

选择好自己所需要的版本之后,点击下载即可。

(二)、LVGL工程精简
将下载好的文件解压,LVGL源码中包含许多与移植无关的文件可将其精简,仅保留移植会用到的demos、examples、src文件及其lv_conf_template.h和lvgl.h,如下图所示。

为了文件的规范化命名将lv_conf_template.h名称修改为lv_conf.h。

点击进入example文件夹,再进入porting文件夹同理为了文件的规范化管理将目录下的文件重命名去掉_template如下所示。

回到example文件夹删除掉除porting文件之外的所以文件,仅保留porting文件如下所示。

三、LVGL移植
(一)、将LVGL库加入工程
在已经准备好屏幕代码工程的文件里名为Middlewares的文件夹,Middlewares文件专门用来存放中间层的相关程序,例如LVGL、FreeRTOS、FatFs等。然后在Middlewares文件中新建一个中名为LVGL的文件夹,将刚才所精简的LVGL库文件存放在该文件夹下。

使用Keil打开该工程,按照如下步骤新建工程目录管理:LVGL_Porting、LVGL_Core、LVGL_Draw、LVGL_Extra、LVGL_Font、LVGL_Gpu、LVGL_Hal、LVGL_Misc、LVGL_Widgets

接下来就是往新建的组添加相关程序。
LVGL_Porting:添加example\porting文件夹中的lv_port_disp.c和lv_port_indev.c

LVGL_Core: 添加src/core下的全部.c文件

LVGL_Draw: 添加src/draw下的全部.c文件和src/draw/sw下的所有全部.c文件(其他文件夹里的文件不需要添加)

LVGL_Extra: 添加src/extra下的全部.c文件,包括所有的子文件夹,不要遗漏!!!

LVGL_Font:添加\src\font下所有.c文件

LVGL_Gpu:添加\src\draw\sdl和\src\draw\stm32_dma2d下的所有.c文件

LVGL_Hal:添加\src\hal下所有.c文件

LVGL_Misc:添加\src\misc下所有.c文件

LVGL_Widgets:添加\src\widgets下所有.c文件

添加好以上文件后,还要记得包含其头文件路径,按照下图所示添加

添加好文件之后还要对Keil进行相关设置,要不然LVGL无法正常运行,打开Keil设置界面,选择Target标签,取消勾选Use MicroLIB。

然后选择C/C++标签,将优化选择到0,同时开启C99模式。

完成上面设置,对程序进行编译,没有报错只有警告则说明LVGL库添加成功,接下来就需要对接口进行配置了。

(二)、修改配置文件
1.启用LVGL
首先是启用LVGL,打开lv_conf.h文件,在第15行的条件宏处改为1。

2.显示接口配置
(1)lv_port_disp.c修改
默认lv_port_disp.c条件编译是关闭的,需要把它打开也就是将0改为1,并且修改头文件名称(之前为了规范将_template删除了),按照下图所示进行修改。

在lv_port_disp.c的INCLUDES划分区添加和屏幕相关的头文件(这里要添加的也就是自己工程里和屏幕相关的头文件)。
#include "lcd_ui.h"
#include "lcd.h"
然后在DEFINES划分区据屏幕信息调整尺寸,我的屏幕信息定义在自己的屏幕驱动文件lcd.h中的宏定义LCD_WIDTH和LCD_HEIGHT中。这里有两种方式一个是自己重新定义宏但是要注意水平要使用MY_DISP_HOR_RES命名,垂直要使用MY_DISP_VER_RES命名,如下所示(这里需要特别注意我这里是嵌套关系,我这里的LCD_WIDTH和LCD_HEIGHT在lcd.h中已经规定了其大小,如果嫌麻烦可以直接替换为自己准备的屏幕尺寸)。
#define MY_DISP_HOR_RES LCD_WIDTH
#define MY_DISP_VER_RES LCD_HEIGHT
或者也可以直接利用lv_port_disp.c原来就有的宏定义只要把屏幕尺寸改一下。

由于LVGL支持多个缓冲区,在函数lv_port_disp_init中定义了三种方式,MCU使用一般就用一个,我们注释掉其他两种写缓存方式,我这里选择的是第二种方式。
方式一:单缓存显示(10行),主控内存较小时选用此方式。
方式二:双缓存显示(两个10行),此方式支持DMA交替传输,缓存区越大,显示效果越好(有条件两个满屏缓存)。
方式三:双满屏缓存显示,相当于有条件的方式二

由于我这里选择的是第二种显示方式,所有要将这里disp_drv.draw_buf = &draw_buf_dsc_1;改为disp_drv.draw_buf = &draw_buf_dsc_2;表示显示双缓冲。

再往下的STATIC FUNCTIONS区域就是添加自己的屏幕初始化函数和画点函数,在disp_init函数中添加屏幕初始化函数。
/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
/*You code here*/
lcd_disp_config();//自己屏幕的初始化函数
}
在disp_flush函数中添加屏幕画点函数。
/*Flush the content of the internal buffer the specific area on the display
*You can use DMA or any hardware acceleration to do this operation in the background but
*'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
if(disp_flush_enabled) {
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
/*Put a pixel to the display. For example:*/
/*put_px(x, y, *color_p)*/
tli_draw_point(x,y,color_p->full);//在此添加自己的画点函数
color_p++;
}
}
}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
(2)lv_port_disp.h修改
lv_port_disp.h的条件编译也是是关闭的,需要把它打开也是0改为1,然后将其头文件改为
/*********************
* INCLUDES
*********************/
#if defined(LV_LVGL_H_INCLUDE_SIMPLE)
#include "lvgl.h"
#else
#include "lvgl.h" //就是将原来的#include "lvgl/lvgl.h"改为#include "lvgl.h"
#endif
最终结果如下图所示。

完成以上步骤进行编译,没有报错就代表显示接口配置成功。
3.输入设备配置
(1)lv_port_indev.c修改
默认lv_port_indev.c条件编译是关闭的,需要把它打开也就是将0改为1,并且修改头文件名称(之前为了规范将_template删除了),按照下图所示进行修改。

在lv_port_indev.c的INCLUDES划分区添加和触摸相关的头文件(这里要添加的也就是自己工程里和屏幕触摸相关的头文件)。
#include "touch.h"
由于LVGL可以适配多种接口,都定义在lv_port_indev.c文件的 lv_port_indev_init函数中如:触摸板、鼠标、键盘、编码器、按键等代码,这里只使用到了触摸板,将屏蔽鼠标Mouse、键盘Keypad、编码器Encoder、按键Button等相关代码,只保留Touchpad(触摸板)设备。即把下面这些代码屏蔽或者删除。
/*------------------
* Mouse
* -----------------*/
/*Initialize your mouse if you have*/
mouse_init();
/*Register a mouse input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = mouse_read;
indev_mouse = lv_indev_drv_register(&indev_drv);
/*Set cursor. For simplicity set a HOME symbol now.*/
lv_obj_t * mouse_cursor = lv_img_create(lv_scr_act());
lv_img_set_src(mouse_cursor, LV_SYMBOL_HOME);
lv_indev_set_cursor(indev_mouse, mouse_cursor);
/*------------------
* Keypad
* -----------------*/
/*Initialize your keypad or keyboard if you have*/
keypad_init();
/*Register a keypad input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_KEYPAD;
indev_drv.read_cb = keypad_read;
indev_keypad = lv_indev_drv_register(&indev_drv);
/*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
*add objects to the group with `lv_group_add_obj(group, obj)`
*and assign this input device to group to navigate in it:
*`lv_indev_set_group(indev_keypad, group);`*/
/*------------------
* Encoder
* -----------------*/
/*Initialize your encoder if you have*/
encoder_init();
/*Register a encoder input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_ENCODER;
indev_drv.read_cb = encoder_read;
indev_encoder = lv_indev_drv_register(&indev_drv);
/*Later you should create group(s) with `lv_group_t * group = lv_group_create()`,
*add objects to the group with `lv_group_add_obj(group, obj)`
*and assign this input device to group to navigate in it:
*`lv_indev_set_group(indev_encoder, group);`*/
/*------------------
* Button
* -----------------*/
/*Initialize your button if you have*/
button_init();
/*Register a button input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_BUTTON;
indev_drv.read_cb = button_read;
indev_button = lv_indev_drv_register(&indev_drv);
/*Assign buttons to points on the screen*/
static const lv_point_t btn_points[2] = {
{10, 10}, /*Button 0 -> x:10; y:10*/
{40, 100}, /*Button 1 -> x:40; y:100*/
};
lv_indev_set_button_points(indev_button, btn_points);
在STATIC FUNCTIONS这个区域我们也只保留Touchpad(触摸板)。即把下面这些代码屏蔽或者删除。
/*------------------
* Mouse
* -----------------*/
/*Initialize your mouse*/
static void mouse_init(void)
{
/*Your code comes here*/
}
/*Will be called by the library to read the mouse*/
static void mouse_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
/*Get the current x and y coordinates*/
mouse_get_xy(&data->point.x, &data->point.y);
/*Get whether the mouse button is pressed or released*/
if(mouse_is_pressed()) {
data->state = LV_INDEV_STATE_PR;
}
else {
data->state = LV_INDEV_STATE_REL;
}
}
/*Return true is the mouse button is pressed*/
static bool mouse_is_pressed(void)
{
/*Your code comes here*/
return false;
}
/*Get the x and y coordinates if the mouse is pressed*/
static void mouse_get_xy(lv_coord_t * x, lv_coord_t * y)
{
/*Your code comes here*/
(*x) = 0;
(*y) = 0;
}
/*------------------
* Keypad
* -----------------*/
/*Initialize your keypad*/
static void keypad_init(void)
{
/*Your code comes here*/
}
/*Will be called by the library to read the mouse*/
static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint32_t last_key = 0;
/*Get the current x and y coordinates*/
mouse_get_xy(&data->point.x, &data->point.y);
/*Get whether the a key is pressed and save the pressed key*/
uint32_t act_key = keypad_get_key();
if(act_key != 0) {
data->state = LV_INDEV_STATE_PR;
/*Translate the keys to LVGL control characters according to your key definitions*/
switch(act_key) {
case 1:
act_key = LV_KEY_NEXT;
break;
case 2:
act_key = LV_KEY_PREV;
break;
case 3:
act_key = LV_KEY_LEFT;
break;
case 4:
act_key = LV_KEY_RIGHT;
break;
case 5:
act_key = LV_KEY_ENTER;
break;
}
last_key = act_key;
}
else {
data->state = LV_INDEV_STATE_REL;
}
data->key = last_key;
}
/*Get the currently being pressed key. 0 if no key is pressed*/
static uint32_t keypad_get_key(void)
{
/*Your code comes here*/
return 0;
}
/*------------------
* Encoder
* -----------------*/
/*Initialize your keypad*/
static void encoder_init(void)
{
/*Your code comes here*/
}
/*Will be called by the library to read the encoder*/
static void encoder_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
data->enc_diff = encoder_diff;
data->state = encoder_state;
}
/*Call this function in an interrupt to process encoder events (turn, press)*/
static void encoder_handler(void)
{
/*Your code comes here*/
encoder_diff += 0;
encoder_state = LV_INDEV_STATE_REL;
}
/*------------------
* Button
* -----------------*/
/*Initialize your buttons*/
static void button_init(void)
{
/*Your code comes here*/
}
/*Will be called by the library to read the button*/
static void button_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static uint8_t last_btn = 0;
/*Get the pressed button's ID*/
int8_t btn_act = button_get_pressed_id();
if(btn_act >= 0) {
data->state = LV_INDEV_STATE_PR;
last_btn = btn_act;
}
else {
data->state = LV_INDEV_STATE_REL;
}
/*Save the last pressed button's ID*/
data->btn_id = last_btn;
}
/*Get ID (0, 1, 2 ..) of the pressed button*/
static int8_t button_get_pressed_id(void)
{
uint8_t i;
/*Check to buttons see which is being pressed (assume there are 2 buttons)*/
for(i = 0; i < 2; i++) {
/*Return the pressed button's ID*/
if(button_is_pressed(i)) {
return i;
}
}
/*No button pressed*/
return -1;
}
/*Test if `id` button is pressed or not*/
static bool button_is_pressed(uint8_t id)
{
/*Your code comes here*/
return false;
}
回到在STATIC FUNCTIONS区域的Touchpad下,touchpad_init函数为触摸的初始化(添加自己屏幕的触摸初始化)。
/*Initialize your touchpad*/
static void touchpad_init(void)
{
/*Your code comes here*/
FT5206_Init();//添加自己的触摸初始化函数
}
在touchpad_is_pressed函数内添加判断是否按下。
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
FT5206_Scan(0);
if(tp_dev.sta & TP_PRES_DOWN)
{
return true;
}
return false;
}
在touchpad_get_xy函数内添加坐标获取。
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
/*Your code comes here*/
(*x) = tp_dev.x[0];
(*y) = tp_dev.y[0];
}
(2)lv_port_indev.h修改
lv_port_disp.h的条件编译也是是关闭的,需要把它打开也是0改为1,然后将其头文件改为
/*********************
* INCLUDES
*********************/
#include "lvgl.h" //就是将原来的#include "lvgl/lvgl.h"改为#include "lvgl.h"
最终结果如下图所示。

完成以上步骤进行编译,没有报错就代表输入设备配置成功。
(三)、提供心跳
lv_tick_inc() 是LVGL中的一个函数,主要用于模拟系统时钟的滴答(tick)更新。在LVGL中,系统时钟被用于动画、延时处理等定时任务。
1.LVGL 所有动画、过渡效果、事件延迟、定时器等功能,都依赖内部 tick
2.LVGL 不会自己生成时间,你必须提供
该函数的主要作用是将LVGL内部维护的系统 tick 计数器加1,表示系统时间已经向前推进了一个单位的时间间隔(通常是毫秒级别 )。我这里直接在系统滴答定时器中断中添加心跳每隔1ms进行一次,主要步骤在gd32f4xx_it.c文件中添加LVGL相关头文件#include "lvgl.h"和#include "lv_port_disp.h",然后在SysTick_Handler函数中添加函数lv_tick_inc。
#include "lvgl.h"
#include "lv_port_disp.h"
/*!
\brief this function handles SysTick exception
\param[in] none
\param[out] none
\retval none
*/
void SysTick_Handler(void)
{
delay_decrement();
lv_tick_inc(1);
}
(四)、任务处理
要处理 LVGL 的任务,我们需要定期调用lv_timer_handler函数。lv_timer_handler()是调度器和刷新器,LVGL 不像 RTOS 那样自动运行任务,它需要你周期性手动调用,需要每 5~10ms 调用一次,它就处理定时任务、界面刷新等,这里是直接放在main函数的while循环里面(当然要调用LVGL的相关函数不要忘记添加相关的头文件)。
#include "lvgl.h"
/*!
\brief main function
\param[in] none
\param[out] none
\retval none
*/
int main(void)
{
/*
其他调用的函数
*/
while(1) {
lv_timer_handler(); /* LVGL计时器 */
delay_1ms(5);
}
}
完成好上面所以的步骤代表LVGL已经移植好了,这个时候就要验证LVGL是否移植成功。
(五)、LVGL移植验证
移植好LVGL后要移植是否移植成功,可以编写一个小demo测试一下,首先先添加头文件
#include "lv_port_disp.h"
#include "lv_port_indev.h"
然后编写一个小demo测试屏幕是否能正常显示和能否实现触摸功能。
/*!
\brief main function
\param[in] none
\param[out] none
\retval none
*/
int main(void)
{
ErrStatus init_state;
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
systick_config();
led_gpio_config();
usart_gpio_config(9600U); // 串口0初始化
nvic_irq_enable(SDIO_IRQn, 0, 0);
init_state = exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
if (ERROR == init_state)
{
printf("exmc error\r\n");
}
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl触摸接口初始化,放在lv_init()的后面 */
//LVGL小demo,实现一个滑动按键功能
lv_obj_t* switch_obj = lv_switch_create(lv_scr_act());
lv_obj_set_size(switch_obj, 120, 60);
lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
while(1) {
lv_timer_handler(); /* LVGL计时器 */
delay_1ms(5);
}
}
编译下载,发现屏幕能够正常显示,如下所示。

按动按键也会有反应。

(六)、lv_conf.h中宏定义简要说明
1.常用的宏说明
lv_conf.h中定义了LVGL中的各种宏,其所对应的功能也各不相同,这里只是对一些常用的进行说明。
LV_COLOR_DEPTH:屏幕的色彩深度,支持1bit、8bit、16bit、32bit
LV_COLOR_16_SWAP:字节交换,使用SPI或者DMA刷屏的时候需要置1
LV_MEM_SIZE:GUI可支配的内存空间,根据使用的功能调节
LV_USE_PERF_MONITOR:显示CPU使用率和FPS计数
LV_USE_MEM_MONITOR:显示已使用的内存和内存碎片
LV_FONT_MONTSERRAT_*:字体大小,太小会很模糊
其中的LV_USE_PERF_MONITOR 、LV_USE_MEM_MONITOR可以就调试用(有点破坏美观)。
2.LVGL帧率限制
在lv_conf.h里LVGL是有一个帧率刷新周期的宏定义LV_DISP_DEF_REFR_PERIOD。LVGL会通过LVGL内部的tick,定时去刷屏幕,也就是说该宏定义限定了LVGL刷屏帧率的上限,默认满帧33帧。这里的30即1000ms/30ms=33FPS,这里我们直接改成10ms刷新一次,满帧100帧。满帧是指达到显示器刷新率的上限。
/*Default display refresh period. LVG will redraw changed areas with this period time*/
#define LV_DISP_DEF_REFR_PERIOD 10 /*[ms]*/
3.配置DPI
DIP即设备独立像素,虽然LVGL的作者说这个没这么重要,但他会严重影响到LVGL的动画效果,在lv_conf.h里也要属于它的宏LV_DPI_DEF。DPI需要手动计算,例如800*480分辨率4.3英寸的屏幕,那么 DPI = (800*800+480*480)^0.5 / 4.3 ≈ 217。
/*Default Dot Per Inch. Used to initialize default sizes such as widgets sized, style paddings.
*(Not so important, you can adjust it to modify default sizes and spaces)*/
#define LV_DPI_DEF 217 /*[px/inch]*/
四、官方demo验证
LVGL官方也准备了各种运行测试的demo,之前在\Middlewares\LVGL里已经有了相关的demo文件这里只需要在keil新建相关组,将需要加入的demo.c文件加入到lvgl_demo项目中(这里只添加了两个官方demo,以压力测试为例)。

添加相关的头文件路径。

在main.c文件添加相关demo的头文件
#include "lv_demo_stress.h"
在main.c的main函数的lvgl初始化后面加入相关demo函数。
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init();
//一定要添加在LVGL初始化后面
lv_demo_stress();
在lv_conf.h文件里开启相关的宏定义,将0改为1。
/*Stress test for LVGL*/
#define LV_USE_DEMO_STRESS 1
完成上述操作之后编译下载,LVGL正常运行。

五、其他
(一)、程序卡死在HardFault
1.方法一
由于前面关掉了Use MicroLIB,如果要使用如果需要通过 printf 打印一些信息需要加入下列语句避免使用半主机模式导致程序卡死在HardFault。

在串口函数中加入下面这个程序告诉连接器不从C库链接使用半主机的函数。
/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 标准库需要的支持类型 */
struct __FILE
{
int handle;
};
FILE __stdout;
2.方法二
根据以上修改还是有HardFault、死机、显示异常等问题,就修改程序运行时主栈的大小Stack_Size,在 GD32 的启动汇编文件 startup_gd32f450_470.s 中修改。

(二)、问题
我在移植过程中发现触摸不是特别灵敏,但是只要在main函数的while循环中加入FT5206_Scan(0);函数屏幕触摸就会变的灵敏,可是之前在配置lv_port_indev.c文件中的touchpad_is_pressed函数已经加入了FT5206_Scan(0);效果触摸触发的不灵敏,这是什么情况有没有大佬解答一下?
/*!
\brief main function
\param[in] none
\param[out] none
\retval none
*/
int main(void)
{
ErrStatus init_state;
nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 优先级分组
systick_config();
led_gpio_config();
usart_gpio_config(9600U); // 串口0初始化
nvic_irq_enable(SDIO_IRQn, 0, 0);
init_state = exmc_synchronous_dynamic_ram_init(EXMC_SDRAM_DEVICE0);
if (ERROR == init_state)
{
printf("exmc error\r\n");
}
lv_init(); /* lvgl系统初始化 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl触摸接口初始化,放在lv_init()的后面 */
//LVGL小demo,实现一个滑动按键功能
lv_obj_t* switch_obj = lv_switch_create(lv_scr_act());
lv_obj_set_size(switch_obj, 120, 60);
lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
while(1) {
FT5206_Scan(0);//再一次调用屏幕扫描函数!!!!!
lv_timer_handler(); /* LVGL计时器 */
delay_1ms(5);
}
}
更多推荐


所有评论(0)