某通訊模組裝置,通過USB提供RDNIS和ECM網路卡功能。在實際應用中發現,USB RNDIS網路卡模式下,當使用AT指令以不同的CID撥號的時候,在Windows主機上能正常撥號成功,但在Linux主機上卻會發生撥號失敗的情況。作為對比,在同樣的測試環境和測試方法下,USB ECM網路卡則沒有這種異常。
測試流程概括如下:
- 裝置側已經配置為USB RNDIS模式
- 主機側通過AT指令以CID1撥號成功
- 測試網路功能,主機和裝置側可以ping通
- 主機側通過AT指令斷開CID1撥號
- 主機側通過AT指令以CID2撥號失敗,主機和裝置側不能ping通
以上是問題背景。
USB ECM是USB-IF定義的CDC類規範下的一個子類規範,全稱Ethernet Networking Control Model;RNDIS是微軟為即插即用的乙太網裝置制定的一種規範。實現這兩種協議的USB裝置,通過USB線接入主機後,會在主機側和裝置側各生成一張網路卡。兩側的網路卡處在同一個網段,進行網路通訊,資料承載通路是USB。下圖是從微軟官網摘抄的RNDIS框架圖:
https://docs.microsoft.com/en-us/windows-hardware/drivers/network/overview-of-remote-ndis--rndis-
經調查,Linux主機上RNDIS撥號測試失敗,主要原因在於:當第一次撥號成功後,斷開撥號時,Linux主機上的USB網路卡IP地址並沒有消失,後續以不同CID撥號後,Linux也沒有發起DHCP請求包,DHCP過程失敗,IP地址未更新。此時,如果將USB拔插一下就可恢復正常。
通過AT指令通知裝置端斷開撥號時,裝置側會有down USB網路卡的動作,down USB網路卡的過程中,裝置側RNDIS會上報rndis disconnect訊息來通知主機側。主機側可以根據這個訊息做相應處理。
在Ubuntu主機和Windows主機上測試斷開撥號操作時,在裝置端抓取的kernel log片段如下。可以看到,無論是在Windows主機上還是Ubuntu主機上,裝置端確實在斷開撥號時上報了rndis disconnect訊息。
Ubuntu主機環境,裝置端log: root@udx710-module:~# [ 324.516525] c0 configfs-gadget gadget: rndis_close [ 324.521239] c0 rndis_set_param_medium: 0 0 [ 324.525296] c0 rndis_signal_disconnect [ 324.529023] c0 rndis_indicate_status_msg: status 1073807372 Windows主機環境,裝置端log: root@udx710-module:~# [ 191.340507] c1 configfs-gadget gadget: rndis_close [ 191.345223] c1 rndis_set_param_medium: 0 0 [ 191.349285] c1 rndis_signal_disconnect [ 191.353008] c1 rndis_indicate_status_msg: status 1073807372 [ 191.364621] c1 configfs-gadget gadget: rndis reqa1.01 v0000 i0000 l4096
裝置端動作
裝置端關鍵程式碼如下:
rndis_close -> rndis_signal_disconnect -> rndis_indicate_status_msg
drivers/usb/gadget/function/rndis.c int rndis_signal_disconnect(struct rndis_params *params) { params->media_state = RNDIS_MEDIA_STATE_DISCONNECTED; return rndis_indicate_status_msg(params, RNDIS_STATUS_MEDIA_DISCONNECT); } /* * Device to Host Comunication */ static int rndis_indicate_status_msg(struct rndis_params *params, u32 status) { rndis_indicate_status_msg_type *resp; rndis_resp_t *r; if (params->state == RNDIS_UNINITIALIZED) return -ENOTSUPP; r = rndis_add_response(params, sizeof(rndis_indicate_status_msg_type)); if (!r) return -ENOMEM; resp = (rndis_indicate_status_msg_type *)r->buf; resp->MessageType = cpu_to_le32(RNDIS_MSG_INDICATE); resp->MessageLength = cpu_to_le32(20); resp->Status = cpu_to_le32(status); resp->StatusBufferLength = cpu_to_le32(0); resp->StatusBufferOffset = cpu_to_le32(0); params->resp_avail(params->v); return 0; } drivers/usb/gadget/function/f_rndis.c static void rndis_response_available(void *_rndis) { struct f_rndis *rndis = _rndis; struct usb_request *req = rndis->notify_req; struct usb_composite_dev *cdev = rndis->port.func.config->cdev; __le32 *data = req->buf; int status; if (atomic_inc_return(&rndis->notify_count) != 1) return; /* Send RNDIS RESPONSE_AVAILABLE notification; a * USB_CDC_NOTIFY_RESPONSE_AVAILABLE "should" work too * * This is the only notification defined by RNDIS. */ data[0] = cpu_to_le32(1); data[1] = cpu_to_le32(0); status = usb_ep_queue(rndis->notify, req, GFP_ATOMIC); if (status) { atomic_dec(&rndis->notify_count); DBG(cdev, "notify/0 --> %d\n", status); } }
rndis_indicate_status_msg 會做兩件事:
1)把RNDIS_MSG_INDICATE message放入response queue,message中帶了status(RNDIS_STATUS_MEDIA_DISCONNECT)。
2)呼叫 resp_avail 發 RESPONSE_AVAILABLE notification(最終呼叫到rndis_response_available @ f_rndis.c),通過interrupt IN端點發出去,這個資料中沒有包含rndis連線狀態資訊,只是單純的上報RESPONSE_AVAILABLE (0x00000001)。
主機端動作
1)按微軟的RNDIS規範,Host收到RESPONSE_AVAILABLE後,接下來需要往control端點發 GET_ENCAPSULATED_RESPONSE request 才能讀取裝置端的 response,得到rndis連線狀態。
Upon receiving the RESPONSE_AVAILABLE notification, the host reads the control message from the Control endpoint using a GET_ENCAPSULATED_RESPONSE transfer, defined in the following table.
https://docs.microsoft.com/en-us/windows-hardware/drivers/network/control-channel-characteristics
BmRequestType bRequest wValue wIndex 0xA1 0x01 0x0000
2)再看rndis host驅動:
rndis_status @ rndis_host.c 函式沒有做實際動作。
drivers/net/usb/rndis_host.c void rndis_status(struct usbnet *dev, struct urb *urb) { netdev_dbg(dev->net, "rndis status urb, len %d stat %d\n", urb->actual_length, urb->status); // FIXME for keepalives, respond immediately (asynchronously) // if not an RNDIS status, do like cdc_status(dev,urb) does } static const struct driver_info rndis_info = { .description = "RNDIS device", .flags = FLAG_ETHER | FLAG_POINTTOPOINT | FLAG_FRAMING_RN | FLAG_NO_SETINT, .bind = rndis_bind, .unbind = rndis_unbind, .status = rndis_status, .rx_fixup = rndis_rx_fixup, .tx_fixup = rndis_tx_fixup, };
也就是說,Linux主機RNDIS驅動沒有嚴格按照RNDIS協議流程去讀取RNDIS_STATUS_MEDIA_DISCONNECT訊息,導致它無法獲知裝置端RNDIS網路卡斷開的狀態,進而無法正確作出網路狀態改變的相關處理。
而且作者也在程式碼註釋中表明瞭態度:強烈建議不要使用RNDIS,而應使用CDC乙太網(ECM,NCM,EEM等)這類非專有(non-proprietary)的替代方案。USB CDC規範是USB-IF制定的,RNDIS是微軟制定的。
/* * RNDIS is NDIS remoted over USB. It's a MSFT variant of CDC ACM ... of * course ACM was intended for modems, not Ethernet links! USB's standard * for Ethernet links is "CDC Ethernet", which is significantly simpler. * * NOTE that Microsoft's "RNDIS 1.0" specification is incomplete. Issues * include: * - Power management in particular relies on information that's scattered * through other documentation, and which is incomplete or incorrect even * there. * - There are various undocumented protocol requirements, such as the * need to send unused garbage in control-OUT messages. * - In some cases, MS-Windows will emit undocumented requests; this * matters more to peripheral implementations than host ones. * * Moreover there's a no-open-specs variant of RNDIS called "ActiveSync". * * For these reasons and others, ** USE OF RNDIS IS STRONGLY DISCOURAGED ** in * favor of such non-proprietary alternatives as CDC Ethernet or the newer (and * currently rare) "Ethernet Emulation Model" (EEM). */
最後順便看看為什麼ECM沒有問題。
ECM裝置側,ecm_close -> ecm_notify -> ecm_do_notify -> 通過interrupt IN端點發出去,這個資料中直接帶了ecm連線狀態,無需Host專門再發另外的request讀取這個狀態。
static void ecm_do_notify(struct f_ecm *ecm) { struct usb_request *req = ecm->notify_req; struct usb_cdc_notification *event; ... event = req->buf; switch (ecm->notify_state) { ... case ECM_NOTIFY_CONNECT: event->bNotificationType = USB_CDC_NOTIFY_NETWORK_CONNECTION; if (ecm->is_open) event->wValue = cpu_to_le16(1); else event->wValue = cpu_to_le16(0); event->wLength = 0; req->length = sizeof *event; DBG(cdev, "notify connect %s\n", ecm->is_open ? "true" : "false"); ecm->notify_state = ECM_NOTIFY_SPEED; break; ... }
ECM主機側,usbnet_cdc_status @ cdc_ether.c 函式中有處理ecm connection訊息並呼叫usbnet_link_change。
void usbnet_cdc_status(struct usbnet *dev, struct urb *urb) { struct usb_cdc_notification *event; ... event = urb->transfer_buffer; switch (event->bNotificationType) { case USB_CDC_NOTIFY_NETWORK_CONNECTION: netif_dbg(dev, timer, dev->net, "CDC: carrier %s\n", event->wValue ? "on" : "off"); usbnet_link_change(dev, !!event->wValue, 0); break; ... } } static const struct driver_info cdc_info = { .description = "CDC Ethernet Device", .flags = FLAG_ETHER | FLAG_POINTTOPOINT, .bind = usbnet_cdc_bind, .unbind = usbnet_cdc_unbind, .status = usbnet_cdc_status, .set_rx_mode = usbnet_cdc_update_filter, .manage_power = usbnet_manage_power, };
版權所有,轉載請註明出處。
文章會同步到“大魚嵌入式”,歡迎關注,一起交流。