转自: .html 作者:神秘网友 发布时间:2021-02-04 15:50:05
一.URT介绍前面介绍了Linux USB Gadget的软件结构与各软件层的整合过程。经过各种注册函数,Gadget功能驱动层,USB设备层与UDC底层结合在了一起形成了一个完整的USB设备。而这个设备已经准备好了接受主机的枚举。在介绍USB设备枚举之前。先熟悉一下各层通信所用的数据结构,在USB主机端编写USB设备驱动程序,最重要的结构就是URB了,我们只需要将各种URB提交给USB核心,核心就会自动给我们的数据发送到指定的设备。而对于设备端也有这样一个类似的重要的数据结构。这个数据结构就是urt--usb_request。每一个端点都有一个urt链表,上面挂着各种urt。在底层的UDC的中断处理程序中,针对不同的端点调用不同的处理函数,总之是处理端点上的urt链表,处理完一个urt就调用预先设置好的回调函数。这就是设备端数据处理的流程。下面分析一下usb_request结构:
struct usb_request {void *buf;unsigned length;dma_addr_t dma;struct scatterlist *sg; //用于DMA可以一次DMA接入一串不连续的物理地址unsigned num_sgs;unsigned num_mapped_sgs;unsigned stream_id:16;unsigned no_interrupt:1;unsigned zero:1;unsigned short_not_ok:1;unsigned dma_mapped:1;void (*complete)(struct usb_ep *ep,struct usb_request *req);void *context;struct list_head list;unsigned frame_number; /* ISO ONLY */int status;unsigned actual;
};
(1)buf 字段是要接受或者发送数据存储的地方,而length代表了数据的长度。
(2)dma 是dma_addr_t类型的,有DMA传输有关。虽然s3c2440的USB设备控制器支持DMA操作,但是底层UDC驱动没有实现,所以不用管这个字段了。
(3)三个位域分别代表了:
(4)(*complete)(struct usb_ep *ep, struct usb_request *req); 这个是回调函数,在端点处理完一个urt的时候调用,非常重要
(5)context
(6)list 作用是将自己链接在端点链表
(7)status 状态
(8)actual 实际传输的字节
二.USB设备枚举分析完urt,那么就实际进入主机识别USB设备的最关键的设备枚举。这里主要分析设备怎么相应主机,对于主机究竟是怎么完成这些操作的还的找一种主机控制器来研究一下。首先先回顾一下USB设备枚举都要完成那些步骤吧:
(1)设备插入主机,主机检测到设备。复位设备
(2)主机向设备控制端点发送Get_Descriptor来了解设备默认管道的大小。
(3)主机指定一个地址,发送Set_Address标准请求设置设备的地址
(4)主机使用新的地址,再次发送Get_Descriptor或得各种描述符
(5)主机加载一个USB设备驱动
(6)USB设备驱动再发送Set_Confuration标准设备请求配置设备
以上就是USB设备枚举的过程。USB设备必须正确的相应主机的要求才能顺利的完成设备枚举。我们知道USB是主从式总线结构,全部通信都是由主机发起,设备没有一点自主权。Imx8的USB设备控制器,当主机向USB设备发送一个包时,USB设备控制器就会产生相应的中断。当出现传输错误的时候,也会以中断的形式来通知。所以理解USB设备控制器的中断是理解USB通信过程的关键。
imx8的dwc3 USB设备控制器驱动在《Linux USB子系统(十)——Gadget UDC驱动分析》已经有分析了。
中断函数的细节,我们再这里继续分析。主要工作都由下半部dwc3_thread_interrupt完成:dwc3_thread_interrupt - dwc3_process_event_buf - dwc3_process_event_entry在dwc3_process_event_entry函数中会对中断的不同调用不同的函数
static void dwc3_process_event_entry(struct dwc3 *dwc,const union dwc3_event *event)
{trace_dwc3_event(event-raw, dwc);if (!event-type.is_devspec) //这里主要是端点相关的eventdwc3_endpoint_interrupt(dwc, event-depevt); else if (pe == DWC3_EVENT_TYPE_DEV) //这里是gadget设备相关的eventdwc3_gadget_interrupt(dwc, event-devt);elsedev_err(dwc-dev, "UNKNOWN IRQ type %dn", event-raw); usb_request
}
我们这里先分析gadget设备相关的event,这里有设备枚举的(1):复位
static void dwc3_gadget_interrupt(struct dwc3 *dwc,const struct dwc3_event_devt *event)
{switch (event-type) {case DWC3_DEVICE_EVENT_DISCONNECT: dwc3_gadget_disconnect_interrupt(dwc);break;case DWC3_DEVICE_EVENT_RESET: //复位对应这设备枚举的(1)dwc3_gadget_reset_interrupt(dwc);break;case DWC3_DEVICE_EVENT_CONNECT_DONE: //连接完成dwc3_gadget_conndone_interrupt(dwc);break;case DWC3_DEVICE_EVENT_WAKEUP:dwc3_gadget_wakeup_interrupt(dwc); //唤醒break;case DWC3_DEVICE_EVENT_HIBER_REQ: //休眠if (dev_WARN_ONCE(dwc-dev, !dwc-has_hibernation,"unexpected hibernation eventn"))break;dwc3_gadget_hibernation_interrupt(dwc, event-event_info);break;case DWC3_DEVICE_EVENT_LINK_STATUS_CHANGE:dwc3_gadget_linksts_change_interrupt(dwc, event-event_info);break;case DWC3_DEVICE_EVENT_EOPF:/* It changed to be suspend event for version 2.30a and above */if (dwc-revision = DWC3_REVISION_230A) {/** Ignore suspend event until the gadget enters into* USB_STATE_CONFIGURED state.*/if (dwc-gadget.state = USB_STATE_CONFIGURED)dwc3_gadget_suspend_interrupt(dwc,event-event_info);}break;case DWC3_DEVICE_EVENT_SOF:case DWC3_DEVICE_EVENT_ERRATIC_ERROR:case DWC3_DEVICE_EVENT_CMD_CMPL:case DWC3_DEVICE_EVENT_OVERFLOW:break;default:dev_WARN(dwc-dev, "UNKNOWN IRQ %dn", event-type);}
}
我们再来分析端点相关的event处理过程
static void dwc3_endpoint_interrupt(struct dwc3 *dwc,const struct dwc3_event_depevt *event)
{if (epnum == 0 || epnum == 1) { //如果是端点0,就是控制端点,枚举阶段通信的端点dwc3_ep0_interrupt(dwc, event);return;}switch (event-endpoint_event) {case DWC3_DEPEVT_XFERINPROGRESS: //传输进行中dwc3_gadget_endpoint_transfer_in_progress(dep, event);break;case DWC3_DEPEVT_XFERNOTREADY: //传输not readydwc3_gadget_endpoint_transfer_not_ready(dep, event);break;case DWC3_DEPEVT_EPCMDCMPLT: cmd = DEPEVT_PARAMETER_CMD(event-parameters);if (cmd == DWC3_DEPCMD_ENDTRANSFER) {dep-flags = ~DWC3_EP_TRANSFER_STARTED;dwc3_gadget_ep_cleanup_cancelled_requests(dep);}break;case DWC3_DEPEVT_STREAMEVT:case DWC3_DEPEVT_XFERCOMPLETE:case DWC3_DEPEVT_RXTXFIFOEVT:break;}
}
dwc3_ep0_interrupt函数根据不同的event调用,我们这里不是not ready。
void dwc3_ep0_interrupt(struct dwc3 *dwc,const struct dwc3_event_depevt *event)
{switch (event-endpoint_event) {case DWC3_DEPEVT_XFERCOMPLETE:dwc3_ep0_xfer_complete(dwc, event);break;case DWC3_DEPEVT_XFERNOTREADY:dwc3_ep0_xfernotready(dwc, event);break;case DWC3_DEPEVT_XFERINPROGRESS:case DWC3_DEPEVT_RXTXFIFOEVT:case DWC3_DEPEVT_STREAMEVT:case DWC3_DEPEVT_EPCMDCMPLT:break;}
}
按照USB设备枚举的过程,最先发生的中断是复位。然后USB主机就会发起一次控制传输来获得设备描述符。这个控制传输是Get_Descriptor标准设备请求。一次完整控制传输可以分为三个阶段(也可能两个阶段):初始设置阶段---数据阶段(不必须)---状态信息阶段。而Get_Descriptor是有数据传输的,USB设备要返回设备描述符号。所以有三个阶段:分别是建立阶段,数据阶段,状态阶段。建立阶段分为三个USB数据包:分别是setup包,data包,与握手包。当建立阶段完毕后,data包的数据会写入端点0的FIFO,USB设备控制器就会产生中断。这时可以判断这个状态。然后调用相应的函数读取在FIFO的数据,判断是控制传输的类型,然后针对不同的类型采取不同的操作,或接受数据,或发送数据。现在针对Get_Descriptor这个USB标准请求来分析一下dwc3_ep0_xfer_complete函数中代码的执行:
static void dwc3_ep0_xfer_complete(struct dwc3 *dwc,const struct dwc3_event_depevt *event)
{struct dwc3_ep *dep = dwc-eps[event-endpoint_number];dep-flags = ~DWC3_EP_TRANSFER_STARTED;dep-resource_index = 0;dwc-setup_packet_pending = false;switch (dwc-ep0state) {case EP0_SETUP_PHASE: //建立阶段dwc3_ep0_inspect_setup(dwc, event);break;case EP0_DATA_PHASE: //数据阶段dwc3_ep0_complete_data(dwc, event);break;case EP0_STATUS_PHASE: //状态阶段dwc3_ep0_complete_status(dwc, event);break;default:WARN(true, "UNKNOWN ep0state %dn", dwc-ep0state);}
}
后面调用dwc3_ep0_inspect_setup,我们这合理是三个阶段的建立
static void dwc3_ep0_inspect_setup(struct dwc3 *dwc,const struct dwc3_event_depevt *event)
{len = le16_to_cpu(ctrl-wLength);if (!len) {dwc-three_stage_setup = false;dwc-ep0_expect_in = false;dwc-ep0_next_event = DWC3_EP0_NRDY_STATUS;} else {dwc-three_stage_setup = true;dwc-ep0_expect_in = !!(ctrl-bRequestType USB_DIR_IN);dwc-ep0_next_event = DWC3_EP0_NRDY_DATA;}if ((ctrl-bRequestType USB_TYPE_MASK) == USB_TYPE_STANDARD) //我们这里是标准usbret = dwc3_ep0_std_request(dwc, ctrl);elseret = dwc3_ep0_delegate_req(dwc, ctrl);if (ret == USB_GADGET_DELAYED_STATUS)dwc-delayed_status = true;}
这个函数所做的主要工作就是读取端点0 FIFO中的数据,这里的数据就是控制传输的类型。然后通过一个switch语句来判断到底是什么控制传输。根据控制传输的不同类型采用不同的操作。这里我们假设的是Get_Descriptor。那么switch语句都不执行,执行下函数dwc3_ep0_delegate_req。
static int dwc3_ep0_std_request(struct dwc3 *dwc, struct usb_ctrlrequest *ctrl)
{switch (ctrl-bRequest) {case USB_REQ_GET_STATUS:ret = dwc3_ep0_handle_status(dwc, ctrl);break;case USB_REQ_CLEAR_FEATURE:ret = dwc3_ep0_handle_feature(dwc, ctrl, 0);break;case USB_REQ_SET_FEATURE:ret = dwc3_ep0_handle_feature(dwc, ctrl, 1);break;case USB_REQ_SET_ADDRESS:ret = dwc3_ep0_set_address(dwc, ctrl);break;case USB_REQ_SET_CONFIGURATION:ret = dwc3_ep0_set_config(dwc, ctrl);break;case USB_REQ_SET_SEL:ret = dwc3_ep0_set_sel(dwc, ctrl);break;case USB_REQ_SET_ISOCH_DELAY:ret = dwc3_ep0_set_isoch_delay(dwc, ctrl);break;default:ret = dwc3_ep0_delegate_req(dwc, ctrl);break;}return ret;
}
dwc3_ep0_delegate_req函数里面调用dwc-gadget_driver-setup,已经初始化好了是composite_setup,在composite.c中定义,如下:
int composite_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
{/* partial re-init of the response message; the function or the* gadget might need to a control-OUT completion* when we delegate to it.*/req-zero = 0;req-context = cdev;req-complete = composite_setup_complete;req-length = 0;gadget-ep0-driver_data = cdev;switch (ctrl-bRequest) {/* we handle all standard USB descriptors */case USB_REQ_GET_DESCRIPTOR: //获取描述符if (ctrl-bRequestType != USB_DIR_IN)goto unknown;switch (w_value 8) {case USB_DT_DEVICE:cdev-desc.bNumConfigurations =count_configs(cdev, USB_DT_DEVICE);cdev-desc.bMaxPacketSize0 =cdev-gadget-ep0-maxpacket;if (gadget_is_superspeed(gadget)) {if (gadget-speed = USB_SPEED_SUPER) {cdev-desc.bcdUSB = cpu_to_le16(0x0320);cdev-desc.bMaxPacketSize0 = 9;} else {cdev-desc.bcdUSB = cpu_to_le16(0x0210);}} else {if (gadget-lpm_capable)cdev-desc.bcdUSB = cpu_to_le16(0x0201);elsecdev-desc.bcdUSB = cpu_to_le16(0x0200);}value = min(w_length, (u16) sizeof cdev-desc);memcpy(req-buf, cdev-desc, value);break;case USB_DT_DEVICE_QUALIFIER:if (!gadget_is_dualspeed(gadget) ||gadget-speed = USB_SPEED_SUPER)break;device_qual(cdev);value = min_t(int, w_length,sizeof(struct usb_qualifier_descriptor));break;case USB_DT_OTHER_SPEED_CONFIG:if (!gadget_is_dualspeed(gadget) ||gadget-speed = USB_SPEED_SUPER)break;/* FALLTHROUGH */case USB_DT_CONFIG:value = config_desc(cdev, w_value);if (value = 0)value = min(w_length, (u16) value);break;case USB_DT_STRING:value = get_string(cdev, req-buf,w_index, w_value 0xff);if (value = 0)value = min(w_length, (u16) value);break;case USB_DT_BOS:if (gadget_is_superspeed(gadget) ||gadget-lpm_capable) {value = bos_desc(cdev);value = min(w_length, (u16) value);}break;case USB_DT_OTG:if (gadget_is_otg(gadget)) {struct usb_configuration *config;int otg_desc_len = 0;if (cdev-config)config = cdev-config;elseconfig = list_first_entry(cdev-configs,struct usb_configuration, list);if (!config)goto done;if (gadget-otg_caps (gadget-otg_caps-otg_rev = 0x0200))otg_desc_len += sizeof(struct usb_otg20_descriptor);elseotg_desc_len += sizeof(struct usb_otg_descriptor);value = min_t(int, w_length, otg_desc_len);memcpy(req-buf, config-descriptors[0], value);}break;}break;/* any number of configs can work */case USB_REQ_SET_CONFIGURATION:if (ctrl-bRequestType != 0)goto unknown;if (gadget_is_otg(gadget)) {if (gadget-a_hnp_support)DBG(cdev, "HNP availablen");else if (gadget-a_alt_hnp_support)DBG(cdev, "HNP on another portn");elseVDBG(cdev, "HNP inactiven");}spin_lock(cdev-lock);value = set_config(cdev, ctrl, w_value);spin_unlock(cdev-lock);break;case USB_REQ_GET_CONFIGURATION:if (ctrl-bRequestType != USB_DIR_IN)goto unknown;if (cdev-config)*(u8 *)req-buf = cdev-config-bConfigurationValue;else*(u8 *)req-buf = 0;value = min(w_length, (u16) 1);break;/* function drivers must handle get/set altsetting */case USB_REQ_SET_INTERFACE:if (ctrl-bRequestType != USB_RECIP_INTERFACE)goto unknown;if (!cdev-config || intf = MAX_CONFIG_INTERFACES)break;f = cdev-config-interface[intf];if (!f)break;/** If there's no get_alt() method, we know only altsetting zero* works. There is no need to check if set_alt() is not NULL* as we check this in usb_add_function().*/if (w_value !f-get_alt)break;spin_lock(cdev-lock);value = f-set_alt(f, w_index, w_value);if (value == USB_GADGET_DELAYED_STATUS) {DBG(cdev,"%s: interface %d (%s) requested delayed statusn",__func__, intf, f-name);cdev-delayed_status++;DBG(cdev, "delayed_status count %dn",cdev-delayed_status);}spin_unlock(cdev-lock);break;case USB_REQ_GET_INTERFACE:if (ctrl-bRequestType != (USB_DIR_IN|USB_RECIP_INTERFACE))goto unknown;if (!cdev-config || intf = MAX_CONFIG_INTERFACES)break;f = cdev-config-interface[intf];if (!f)break;/* lots of interfaces only need */value = f-get_alt f-get_alt(f, w_index) : 0;if (value 0)break;*((u8 *)req-buf) = value;value = min(w_length, (u16) 1);break;case USB_REQ_GET_STATUS:if (gadget_is_otg(gadget) gadget-hnp_polling_support (w_index == OTG_STS_SELECTOR)) {if (ctrl-bRequestType != (USB_DIR_IN |USB_RECIP_DEVICE))goto unknown;*((u8 *)req-buf) = gadget-host_request_flag;value = 1;break;}/** USB 3.0 additions:* Function driver should handle get_status request. If such cb* wasn't supplied we respond with default value = 0* Note: function driver should supply such cb only for the* first interface of the function*/if (!gadget_is_superspeed(gadget))goto unknown;if (ctrl-bRequestType != (USB_DIR_IN | USB_RECIP_INTERFACE))goto unknown;value = 2; /* This is the length of the get_status reply */put_unaligned_le16(0, req-buf);if (!cdev-config || intf = MAX_CONFIG_INTERFACES)break;f = cdev-config-interface[intf];if (!f)break;status = f-get_status f-get_status(f) : 0;if (status 0)break;put_unaligned_le16(status 0x0000ffff, req-buf);break;/** Function drivers should handle SetFeature/ClearFeature* (FUNCTION_SUSPEND) request. function_suspend cb should be supplied* only for the first interface of the function*/case USB_REQ_CLEAR_FEATURE:case USB_REQ_SET_FEATURE:if (!gadget_is_superspeed(gadget))goto unknown;if (ctrl-bRequestType != (USB_DIR_OUT | USB_RECIP_INTERFACE))goto unknown;switch (w_value) {case USB_INTRF_FUNC_SUSPEND:if (!cdev-config || intf = MAX_CONFIG_INTERFACES)break;f = cdev-config-interface[intf];if (!f)break;value = 0;if (f-func_suspend)value = f-func_suspend(f, w_index 8);if (value 0) {ERROR(cdev,"func_suspend() returned error %dn",value);value = 0;}break;}break;default:
unknown:/** OS descriptors handling*/if (cdev-use_os_string cdev-os_desc_config (ctrl-bRequestType USB_TYPE_VENDOR) ctrl-bRequest == cdev-b_vendor_code) {struct usb_configuration *os_desc_cfg;u8 *buf;int interface;int count = 0;req = cdev-os_desc_req;req-context = cdev;req-complete = composite_setup_complete;buf = req-buf;os_desc_cfg = cdev-os_desc_config;w_length = min_t(u16, w_length, USB_COMP_EP0_OS_DESC_BUFSIZ);memset(buf, 0, w_length);buf[5] = 0x01;switch (ctrl-bRequestType USB_RECIP_MASK) {case USB_RECIP_DEVICE:if (w_index != 0x4 || (w_value 8))break;buf[6] = w_index;/* Number of ext compat interfaces */count = count_ext_compat(os_desc_cfg);buf[8] = count;count *= 24; /* 24 B/ext compat desc */count += 16; /* header */put_unaligned_le32(count, buf);value = w_length;if (w_length 0x10) {value = fill_ext_compat(os_desc_cfg, buf);value = min_t(u16, w_length, value);}break;case USB_RECIP_INTERFACE:if (w_index != 0x5 || (w_value 8))break;interface = w_value 0xFF;buf[6] = w_index;count = count_ext_prop(os_desc_cfg,interface);put_unaligned_le16(count, buf + 8);count = len_ext_prop(os_desc_cfg,interface);put_unaligned_le32(count, buf);value = w_length;if (w_length 0x0A) {value = fill_ext_prop(os_desc_cfg,interface, buf);if (value = 0)value = min_t(u16, w_length, value);}break;}goto check_value;}VDBG(cdev,"non-core control req%02x.%02x v%04x i%04x l%dn",ctrl-bRequestType, ctrl-bRequest,w_value, w_index, w_length);/* functions always handle their interfaces * punt other recipients (other, WUSB, ...) to the current* configuration code.*/if (cdev-config) {list_for_each_entry(f, cdev-config-functions, list)if (f-req_match f-req_match(f, ctrl, false))goto try_fun_setup;} else {struct usb_configuration *c;list_for_each_entry(c, cdev-configs, list)list_for_each_entry(f, c-functions, list)if (f-req_match f-req_match(f, ctrl, true))goto try_fun_setup;}f = NULL;switch (ctrl-bRequestType USB_RECIP_MASK) {case USB_RECIP_INTERFACE:if (!cdev-config || intf = MAX_CONFIG_INTERFACES)break;f = cdev-config-interface[intf];break;case USB_RECIP_ENDPOINT:if (!cdev-config)break;endp = ((w_index 0x80) 3) | (w_index 0x0f);list_for_each_entry(f, cdev-config-functions, list) {if (test_bit(endp, f-endpoints))break;}if (f-list == cdev-config-functions)f = NULL;break;}
try_fun_setup:if (f f-setup)value = f-setup(f, ctrl);else {struct usb_configuration *c;c = cdev-config;if (!c)goto done;/* try current config's setup */if (c-setup) {value = c-setup(c, ctrl);goto done;}/* try the only function in the current config */if (!list_is_singular(c-functions))goto done;f = list_first_entry(c-functions, struct usb_function,list);if (f-setup)value = f-setup(f, ctrl);}goto done;}check_value:/* respond with data transfer before status phase */if (value = 0 value != USB_GADGET_DELAYED_STATUS) {req-length = value;req-context = cdev;req-zero = value w_length;value = composite_ep0_queue(cdev, req, GFP_ATOMIC);if (value 0) {DBG(cdev, "ep_queue -- %dn", value);req-status = 0;composite_setup_complete(gadget-ep0, req);}} else if (value == USB_GADGET_DELAYED_STATUS w_length != 0) {WARN(cdev,"%s: Delayed status not supported for w_length != 0",__func__);}done:/* device either stalls (value 0) or reports success */return value;
}
这个函数首先提取出USB控制请求的各个字段,然后初始化了端点0的struct usb_request结构。设置了完成回调函数composite_setup_complete。这时通过switch语句来判断是何种控制传输。所以下面的代码执行:
cdev-desc.bNumConfigurations = count_configs(cdev, USB_DT_DEVICE);cdev-desc.bMaxPacketSize0 = cdev-gadget-ep0-maxpacket;if (gadget_is_superspeed(gadget)) {if (gadget-speed = USB_SPEED_SUPER) {cdev-desc.bcdUSB = cpu_to_le16(0x0320);cdev-desc.bMaxPacketSize0 = 9;} else {cdev-desc.bcdUSB = cpu_to_le16(0x0210);}} else {if (gadget-lpm_capable)cdev-desc.bcdUSB = cpu_to_le16(0x0201);elsecdev-desc.bcdUSB = cpu_to_le16(0x0200);}value = min(w_length, (u16) sizeof cdev-desc);memcpy(req-buf, cdev-desc, value);
这段代码就是复制设备描述符到req的缓冲区。然后执行下面的代码:
if (value = 0 value != USB_GADGET_DELAYED_STATUS) {req-length = value;req-context = cdev;req-zero = value w_length;value = composite_ep0_queue(cdev, req, GFP_ATOMIC); //调用到dwc3_gadget_ep0_queueif (value 0) {DBG(cdev, "ep_queue -- %dn", value);req-status = 0;composite_setup_complete(gadget-ep0, req);}}
dwc3_gadget_ep0_queue主要作用就是将req中的数据写入到FIFO中, 是udc dwc3实现的,我们这里不分析了。
以上分析了USB设备枚举过程中的第二步:Get_Descriptor阶段的控制传输。其他的步骤大同小异,都是主机端发起,然后USB设备通过中端来处理。依次经过文中最前面提到的六步,USB主机就识别咱们的设备了 。
参考:Linux USB Gadget--设备枚举_窗外云天的专栏-CSDN博客
本文发布于:2024-02-02 18:02:28,感谢您对本站的认可!
本文链接:https://www.4u4v.net/it/170686837245503.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
留言与评论(共有 0 条评论) |