由于3G/4G时代蜂窝网络速率的提升,原来modem与AP连接使用的普通UART接口已经不能满足速率需求, 在EXYNOS系列AP上主要使用HSIC(High Speed Inter Chip)接口外接modem,HSIC与USB2.0接口软件协议栈相同, 底层物理层直接使用TTL逻辑电平表示数据,是PCB板间芯片高速互连的专用接口。 关于HSIC具体信息可以参考《USB HSIC IP》。
modem一般使用USB CDC-ACM协议,协议主要是规定了USB转UART接口的一些细则,包括流控、波特率等等, modem的USB接口主要使用协议的数据传输功能,其他参数都可以使用默认值, 波特率并不会影响传输速率,底层速率由USB HOST的性能与URB包的大小决定,流控功能也是软件上阻塞包的接收与发送, 以下也主要讨论HSIC驱动中数据的传输过程。
modem通常只有一个CDC-ACM接口(interface),USB协议规定一个接口最多可以支持16个端点(endpoint), 其中端点0只用于传输控制信息,是双向端点,其余端点可以传输普通数据,但都只能单向的传输。 一个IO(UART or NET)接口需要一个输出和一个输入端点,PXA1802除端点0外使能了7个输出和7个输入端点, 最多在HSIC接口驱动层就直接7个IO设备访问,XMM6260只使用了其余4个输出和4个输入端点。
与普通USB驱动相同,HSIC驱动也是使用usb_register接口注册驱动结构, USB CORE识别到有对应pid/vid接口的设备连接后就会绑定在驱动结构,关于USB CORE的细节可以参考其他章节。
static struct usb_driver if_hsic_driver = {
.name = "nezha_modem",
.probe = if_hsic_probe,
.disconnect = if_hsic_disconnect,
.id_table = if_hsic_ids,
.suspend = if_hsic_suspend,
.resume = if_hsic_resume,
.reset_resume = if_hsic_reset_resume,
.supports_autosuspend = 1,
};
modem设备在上电后会在HSIC总线上发connect信号,USB HOST HUB检测到有设备连接后会发起枚举, 识别设备的基本信息并创建USB设备节点,USB设备驱动再根据设备的配置信息,接口信息等绑定到USB接口驱动, 就会进到上面if_hsic_probe,具体流程在USB CORE有说明。 绑定到接口需要根据端点创建IO通道,通常情况时这里的IO通道会被更上一层usespace IO层复用,并不直接提供IO接口。 因此驱动中使用pipe_data表示一个UART逻辑通道,包含一个输出和一个输入端点,以及数据包URB的传输队列等。 上层IO接口的数据封装到skb结构中再添加到pipe_data的传输队列,同时逻辑通道接收到的数据提交到上层复用接口, 根据通道使用的具体复用协议解析再路由到具体的IO接口。
for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {
pipe_data = &usb_ld->devdata[i/2];
endpoint = &iface_desc->endpoint[i].desc;
if (usb_endpoint_is_bulk_in(endpoint))
pipe_data->rx_pipe = usb_rcvbulkpipe(usbdev,
endpoint->bEndpointAddress);
if (usb_endpoint_is_bulk_out(endpoint))
pipe_data->tx_pipe = usb_sndbulkpipe(usbdev,
endpoint->bEndpointAddress);
mif_info("endpoint %d maxp %d\n", i, usb_endpoint_maxp(endpoint));
}
for (i = 0; i < usb_ld->num_link_ch; ++i) {
pipe_data = &usb_ld->devdata[i];
pipe_data->idx = i;
pipe_data->iod = &ld->iods[i];
pipe_data->rx_buf_size = (16 * 1024);
skb_queue_purge(&pipe_data->free_rx_q);
ret = usb_init_rx_skb_pool(pipe_data);
if (ret < 0) {
mif_err("init rx skb pool failed\n");
goto error_exit;
}
ret = hsic_rx_submit(usb_ld, pipe_data, GFP_ATOMIC);
if (ret < 0) {
mif_err("rx skb failed\n");
goto error_exit;
}
}
上面在绑定时建立逻辑通道pipe_data,记录输入输出端点号,同时在输入端点上提交一次读请求到USB HOST, 后面只要modem在该端点上发送数据,都会回调到驱动的读请求回调。具体读请求使用下面代码实现。
static int hsic_rx_submit(struct usb_link_device *usb_ld,
struct if_usb_devdata *pipe_data, gfp_t gfp_flags)
{
int ret = 0;
struct urb *urb;
struct sk_buff *skb;
int delay = 0;
skb = skb_dequeue(&pipe_data->free_rx_q); /* free queue first */
if (!skb) {
skb = alloc_skb((pipe_data->rx_buf_size),
GFP_ATOMIC); /* alloc new skb with GFP_ATOMIC */
if (!skb) {
mif_debug("alloc skb fail\n");
pipe_data->defered_rx = true;
delay = msecs_to_jiffies(20);
goto defered_submit;
}
}
skbpriv(skb)->context = pipe_data;
urb = pipe_data->urb;
urb->transfer_flags = 0; // NOTE::
usb_fill_bulk_urb(urb, usb_ld->usbdev, pipe_data->rx_pipe,
(void *)skb->data, pipe_data->rx_buf_size, hsic_rx_complete,
(void *)skb);
if (!usb_ld->if_usb_connected)
return -ENOENT;
usb_mark_last_busy(usb_ld->usbdev);
ret = usb_submit_urb(urb, gfp_flags);
if (ret)
mif_err("submit urb fail with ret (%d)\n", ret);
defered_submit:
/* Hold L0 until rx sumit complete */
usb_mark_last_busy(usb_ld->usbdev);
schedule_delayed_work(&pipe_data->rx_defered_work, delay);
return ret;
}
驱动中使用一个链表管理空闲读请求内存,这样确保每次发送读请求时都有空闲内存可用。 提交到USB HOST的读请求URB在读到modem发来的数据或出错时都会回调hsic_rx_complete, 这里再把读到的数据发往上层,由上层根据具体协议解析处理。
驱动中读请求的数据发往上层、绑定到设备、唤醒等情形时都会再次提交读请求到USB HOST以便再次接收modem发送的数据。 这里同时提交多个读请求URB并不会提高传输效率,所有同向的URB会依次顺序处理, 当然如果驱动在处理数据延时较大时也可提交多个读URB使用流水线方式提高效率。 这里可以暂停读请求URB提交阻塞modem数据发送达到流控的效果。
上层接口的数据添加结构头等内容后发送到逻辑通道,驱动构建URB请求, 填充待发送数据,提交到USB HOST依次发送到modem。
static int hsic_send(struct link_device *ld, int idx_link, struct sk_buff *skb)
{
size_t tx_size;
int ret, rpm_state;
struct urb *urb;
gfp_t mem_flags = in_interrupt() ? GFP_ATOMIC : GFP_KERNEL;
struct usb_link_device *usb_ld = to_usb_link_device(ld);
struct if_usb_devdata *pipe_data = &usb_ld->devdata[idx_link];
/* store the tx size before run the tx_delayed_work*/
tx_size = skb->len;
/* drop packet, when link is not online */
if (ld->com_state != COM_ONLINE && ld->com_state != COM_BOOT) {
mif_err("drop packet, size=%d, com_state=%d\n",
skb->len, ld->com_state);
return -ENODEV;
}
/* get active async */
rpm_state = link_pm_runtime_get_active_async(usb_ld->link_pm_data);
if (!rpm_state)
pm_runtime_get_noresume(&usb_ld->usbdev->dev);
urb = usb_alloc_urb(0, mem_flags);
if (!urb) {
mif_err("alloc urb error\n");
ret = -ENOMEM;
goto done;
}
usb_fill_bulk_urb(urb, usb_ld->usbdev, pipe_data->tx_pipe, skb->data,
skb->len, hsic_tx_complete, (void *)skb);
skbpriv(skb)->context = pipe_data;
skbpriv(skb)->ld = ld;
skbpriv(skb)->urb = urb; /* kill urb when suspend if tx not complete*/
if (usb_get_rpm_status(&usb_ld->usbdev->dev) != RPM_ACTIVE) {
usb_anchor_urb(urb, &pipe_data->tx_deferd_urbs);
ret = 0;
goto done;
}
skb_queue_tail(&pipe_data->sk_tx_q, skb);
ret = usb_submit_urb(urb, mem_flags);
if (ret < 0) {
mif_err("usb_submit_urb with ret(%d)\n", ret);
skb_unlink(skb, &pipe_data->sk_tx_q);
usb_anchor_urb(urb, &pipe_data->tx_deferd_urbs);
goto done;
}
done:
if (!rpm_state) {
usb_mark_last_busy(usb_ld->usbdev);
pm_runtime_put(&usb_ld->usbdev->dev);
}
return tx_size;
}
发送流程看起来代码比较多,但关键部分很少,大部分都是一些状态判断,暂时不用关注USB设备的动态休眠功能,这个会单独更新。在设备ACTIVE时,直接提交填充好的URB请求到USB HOST,这里是发到输出端点, 如果设备在休眠还需要唤醒设备,为减少发送延时,这里使用异步唤醒,当不在ACTIVE状态时添加到tx_deferd_urbs在唤醒时再次提交发送。
static int hsic_defered_tx_from_anchor(struct if_usb_devdata *pipe_data)
{
struct urb *urb;
struct sk_buff *skb;
int cnt = 0;
int ret = 0;
while ((urb = usb_get_from_anchor(&pipe_data->tx_deferd_urbs))) {
usb_put_urb(urb);
skb = (struct sk_buff *)urb->context;
/* drop package when disconnect */
if (!pipe_data->usb_ld->if_usb_connected) {
usb_free_urb(urb);
dev_kfree_skb_any(skb);
continue;
}
/* refrash the urb */
usb_fill_bulk_urb(urb, pipe_data->usb_ld->usbdev, pipe_data->tx_pipe,
skb->data, skb->len, hsic_tx_complete, (void *)skb);
skb_queue_tail(&pipe_data->sk_tx_q, skb);
ret = usb_submit_urb(urb, GFP_ATOMIC);
if (ret < 0) {
/* TODO: deferd TX again */
mif_err("resume deferd TX fail(%d)\n", ret);
skb_unlink(skb, &pipe_data->sk_tx_q);
usb_anchor_urb_head(urb, &pipe_data->tx_deferd_urbs);
goto exit;
}
cnt++;
}
exit:
if (cnt)
mif_info("deferd tx urb=%d(CH%d)\n", cnt, pipe_data->idx);
return ret;
}
发送时异步唤醒的方式可以节省usespace IO时间,发送与唤醒同时进行,在设备唤醒后再提交缓存的URB。接收时设备一定是处在ACTIVE状态,如果不在ACTIVE状态,modem会先主动唤醒AP再发送数据。关于AP与modem休眠唤醒流程会单独介绍。
上面CDC-ACM协议都是使用bulk-in/out类型端点发送数据,驱动中只需要注册USB接口驱动结构,实现bulk类型端点的数据传输,数据结构设计、数据传输流程等都可以参考内核中usb cdc驱动。