hub_port_wait_reset在/drivers/usb/core/hub.c中
 

static int hub_port_wait_reset(struct usb_hub *hub, int port1,
                struct usb_device *udev, unsigned int delay)
{
    int delay_time, ret;
    u16 portstatus;
    u16 portchange;
    //重试到500
    for (delay_time = 0;delay_time < HUB_RESET_TIMEOUT;delay_time += delay)
    {
        /* wait to give the device a chance to reset */
        msleep(delay);
        /* read and decode port status */
        //读取并解析端口状态
        //读取端口
        ret = hub_port_status(hub, port1, &portstatus, &portchange);
        if (ret < 0)
            return ret;
        /* Device went away? */
        //检测端口的连接位
        if (!(portstatus & USB_PORT_STAT_CONNECTION))
            return -ENOTCONN;
        /* bomb out completely if the connection bounced */
        //检测端口的最近连接状态改变位
        if ((portchange & USB_PORT_STAT_C_CONNECTION))
            return -ENOTCONN;
        /* if we`ve finished resetting, then break out of the loop */
        //检测端口的复位位及端口有效位
        if (!(portstatus & USB_PORT_STAT_RESET) && (portstatus & USB_PORT_STAT_ENABLE))
        {
            //设备为wsu
            if (hub_is_wusb(hub))
                udev->speed = USB_SPEED_VARIABLE;
            //检测端口的高速设备位
            else if (portstatus & USB_PORT_STAT_HIGH_SPEED)
                udev->speed = USB_SPEED_HIGH;
            //检测端口的低速设备位
            else if (portstatus & USB_PORT_STAT_LOW_SPEED)
                udev->speed = USB_SPEED_LOW;
            //设备为全速设备
            else
                udev->speed = USB_SPEED_FULL;
            return 0;
        }
        /* switch to the long delay after two short delay failures */
        if (delay_time >= 2 * HUB_SHORT_RESET_TIME)
            delay = HUB_LONG_RESET_TIME;
        dev_dbg (hub->intfdev,"port %d not reset yet, waiting %dms\n",port1, delay);
    }
    return -EBUSY;
}

hub_port_status负责第4步
之后的代码负责第5步
真快,眨眼就到第6步了
回到hub_port_init中,if (USE_NEW_SCHEME(retry_counter)) 表示是否使用微软的方案,什么是微软的方案呢?
在usb规范中,为获取端点0包的大小所发送的控制传输中数据包的大小为8字节,也就是得到设备描述符的前8字节,众所周知,微软是有份参与usb规范的制定的,微软也理应遵循这个规范,但是不然,在微软的操作系统中,这个请求的大小不为8字节,为64字节,虽然发送8字节也可以,但是一些厂商也发送18字节,把整个设备描述符全部传输了出来,这在微软的操作系统中没有问题,但是当到了LINUX中时就不行了,LINUX只要求8字节,但设备却发送18字节,由于主机一直不握手,设备就一直在重试,导致错误的发生,所以后来为了兼容微软的方案,LINUX也提供了64字节接收的方式

现在我们就来看看微软的方案是如何实现的
r = usb_control_msg(udev, usb_rcvaddr0pipe(),
     USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,
     USB_DT_DEVICE << 8, 0,
     buf, GET_DESCRIPTOR_BUFSIZE,
     USB_CTRL_GET_TIMEOUT);
为发起控制传输,包的大小在之前中有设定,高速和全速设备为64字节,低速设备为8字节
现在来分析UHCI是如何使用td来发送包的,先说一下,由于对FSBR机制理解还不深,暂不谈FSBR
usb_control_msg-> usb_internal_control_msg-> usb_start_wait_urb-> usb_submit_urb-> usb_hcd_submit_urb
又回到了usb_hcd_submit_urb中,不过这次不是根设备了,所以不进入rh_urb_enqueue中,这次的目标为hcd->driver->urb_enqueue,在uhci中为uhci_urb_enqueue
uhci_urb_enqueue在/drivers/usb/host/uhci-q.c中

static int uhci_urb_enqueue(struct usb_hcd *hcd,
        struct urb *urb, gfp_t mem_flags)
{
    int ret;
    struct uhci_hcd *uhci = hcd_to_uhci(hcd);
    unsigned long flags;
    struct urb_priv *urbp;
    struct uhci_qh *qh;
    spin_lock_irqsave(&uhci->lock, flags);
    //连接urb到端点上
    ret = usb_hcd_link_urb_to_ep(hcd, urb);
    if (ret)
        goto done_not_linked;
    ret = -ENOMEM;
    //分配一个urbp结构,并互相连接urbp和urb
    urbp = uhci_alloc_urb_priv(uhci, urb);
    if (!urbp)
        goto done;
    //检测urb所连接的端点是否有qh
    if (urb->ep->hcpriv)
        qh = urb->ep->hcpriv;
    else
    {
        qh = uhci_alloc_qh(uhci, urb->dev, urb->ep);
        if (!qh)
            goto err_no_qh;
    }
    //连接qh到urbp
    urbp->qh = qh;
    //检测qh的类型
    switch (qh->type)
    {
        //控制
        case USB_ENDPOINT_XFER_CONTROL:
            ret = uhci_submit_control(uhci, urb, qh);
            break;
        //块
        case USB_ENDPOINT_XFER_BULK:
            ret = uhci_submit_bulk(uhci, urb, qh);
            break;
        //中断
        case USB_ENDPOINT_XFER_INT:
            ret = uhci_submit_interrupt(uhci, urb, qh);
            break;
        //等时
        case USB_ENDPOINT_XFER_ISOC:
            urb->error_count = 0;
            ret = uhci_submit_isochronous(uhci, urb, qh);
            break;
    }
    if (ret != 0)
        goto err_submit_failed;
    /* Add this URB to the QH */
    //连接qh到urbp中
    urbp->qh = qh;
    //连接urbp进qh中
    list_add_tail(&urbp->node, &qh->queue);
    /* If the new URB is the first and only one on this QH then either
     * the QH is new and idle or else it's unlinked and waiting to
     * become idle, so we can activate it right away. But only if the
     * queue isn't stopped. */
     //检测该qh是否只连接了一个urb
     //检测该qh是否处于停止状态
    if (qh->queue.next == &urbp->node && !qh->is_stopped)
    {
        uhci_activate_qh(uhci, qh);
        uhci_urbp_wants_fsbr(uhci, urbp);
    }
    goto done;
err_submit_failed:
    if (qh->state == QH_STATE_IDLE)
        uhci_make_qh_idle(uhci, qh);    /* Reclaim unused QH */
err_no_qh:
    uhci_free_urb_priv(uhci, urbp);
done:
    if (ret)
        usb_hcd_unlink_urb_from_ep(hcd, urb);
done_not_linked:
    spin_unlock_irqrestore(&uhci->lock, flags);
    return ret;
}

uhci_alloc_qh为urb所连接的端点分配一个qh
uhci_alloc_qh在/drivers/usb/host/uhci-q.c中

static struct uhci_qh *uhci_alloc_qh(struct uhci_hcd *uhci,
        struct usb_device *udev, struct usb_host_endpoint *hep)
{
    dma_addr_t dma_handle;
    struct uhci_qh *qh;
    qh = dma_pool_alloc(uhci->qh_pool, GFP_ATOMIC, &dma_handle);
    if (!qh)
        return NULL;
    memset(qh, 0, sizeof(*qh));
    //保存dma块地址
    qh->dma_handle = dma_handle;
    //设置该qh的连接和部件都为终点
    qh->element = UHCI_PTR_TERM;
    qh->link = UHCI_PTR_TERM;
    INIT_LIST_HEAD(&qh->queue);
    INIT_LIST_HEAD(&qh->node);
    //普通qh
    if (udev)
    {    
        //取得端点的传输类型
        qh->type = hep->desc.bmAttributes & USB_ENDPOINT_XFERTYPE_MASK;
        //检测是否为等时传输
        if (qh->type != USB_ENDPOINT_XFER_ISOC)
        {
            //分配一个虚拟td
            qh->dummy_td = uhci_alloc_td(uhci);
            //检测分配是否成功
            if (!qh->dummy_td)
            {
                dma_pool_free(uhci->qh_pool, qh, dma_handle);
                return NULL;
            }
        }
        //设置qh的状态为空闲
        qh->state = QH_STATE_IDLE;
        //连接端点到qh
        qh->hep = hep;
        //连接usb设备到qh
        qh->udev = udev;
        //连接该qh到端点
        hep->hcpriv = qh;
        //检测qh的类型是否为中断或等时
        if (qh->type == USB_ENDPOINT_XFER_INT || qh->type == USB_ENDPOINT_XFER_ISOC)
            qh->load = usb_calc_bus_time(udev->speed,
                    usb_endpoint_dir_in(&hep->desc),
                    qh->type == USB_ENDPOINT_XFER_ISOC,
                    le16_to_cpu(hep->desc.wMaxPacketSize))
                / 1000 + 1;
    }
     //龙骨qh
    else
    {        /* Skeleton QH */
        qh->state = QH_STATE_ACTIVE;
        qh->type = -1;
    }
    return qh;
}

建立完qh之后的结构如下

 
好~进入到控制传输的建立中uhci_submit_control
uhci_submit_control在/drivers/usb/host/uhci-q.c中
 

static int uhci_submit_control(struct uhci_hcd *uhci, struct urb *urb,
        struct uhci_qh *qh)
{
    struct uhci_td *td;
    unsigned long destination, status;
    //取得端点的包的最大大小
    int maxsze = le16_to_cpu(qh->hep->desc.wMaxPacketSize);
    //取得urb的资料传输长度
    int len = urb->transfer_buffer_length;
    //取得要传输的数据
    dma_addr_t data = urb->transfer_dma;
    __le32 *plink;
    struct urb_priv *urbp = urb->hcpriv;
    int skel;
    /* The "pipe" thing contains the destination in bits 8--18 */
    //设置设备地址和端点并设置为setup事务
    destination = (urb->pipe & PIPE_DEVEP_MASK) | USB_PID_SETUP;
    /* 3 errors, dummy TD remains inactive */
    //设置状态为最大允许3个错误
    status = uhci_maxerr(3);
    //检测urb连接的设备是否为低速设备
    if (urb->dev->speed == USB_SPEED_LOW)
        status |= TD_CTRL_LS;
    /*
     * Build the TD for the control request setup packet
     */
     //取得虚拟td
    td = qh->dummy_td;
    //添加虚拟td到urbp的队列中
    uhci_add_td_to_urbp(td, urbp);
    //填充td结构(控制传输中数据阶段的8字节内容)
    uhci_fill_td(td, status, destination | uhci_explen(8),urb->setup_dma);
    //取得td的连接地址
    plink = &td->link;
    //设置td的状态为有效
    status |= TD_CTRL_ACTIVE;
    /*
     * If direction is "send", change the packet ID from SETUP (0x2D)
     * to OUT (0xE1). Else change it from SETUP to IN (0x69) and
     * set Short Packet Detect (SPD) for all data packets.
     *
     * 0-length transfers always get treated as "send".
     */
    //检测是否为传入或者长度为0
    if (usb_pipeout(urb->pipe) || len == 0)
        //取消setup事务标志并设为out事务
        destination ^= (USB_PID_SETUP ^ USB_PID_OUT);
    else
    {
        //取消setup事务标志并设为in事务
        destination ^= (USB_PID_SETUP ^ USB_PID_IN);
        //设置短包检测位
        status |= TD_CTRL_SPD;
    }
    /*
     * Build the DATA TDs
     */
    while (len > 0)
    {
        int pktsze = maxsze;
        /* The last data packet */
        //最后一个数据包
        if (len <= pktsze)
        {        
            pktsze = len;
            //取消短包检测位
            status &= ~TD_CTRL_SPD;
        }
        //分配一个td
        td = uhci_alloc_td(uhci);
        //分配失败则返回错误
        if (!td)
            goto nomem;
        //连接该td
        *plink = LINK_TO_TD(td);
        /* Alternate Data0/1 (start with Data1) */
        //设置toggle标记
        destination ^= TD_TOKEN_TOGGLE;
        //连接该td进urbp
        uhci_add_td_to_urbp(td, urbp);
        //填充td,也就是连接数据
        uhci_fill_td(td, status, destination | uhci_explen(pktsze),data);
        //更新plink
        plink = &td->link;
        //数据起始位置增加pktsze
        data += pktsze;
        //数据大小减去pktsze
        len -= pktsze;
    }
    /*
     * Build the final TD for control status
     */
     //分配一个td
    td = uhci_alloc_td(uhci);
    //分配出错则返回错误
    if (!td)
        goto nomem;
    //连接该td
    *plink = LINK_TO_TD(td);
    /* Change direction for the status transaction */
    //根据数据阶段的in out事务,设为反方向的事务
    destination ^= (USB_PID_IN ^ USB_PID_OUT);
    //设置toggle标记
    destination |= TD_TOKEN_TOGGLE;        /* End in Data1 */
    //添加该td进urbp
    uhci_add_td_to_urbp(td, urbp);
    //填充该td,数据长度为0
    uhci_fill_td(td, status | TD_CTRL_IOC,destination | uhci_explen(0), 0);

//更新plink
    plink = &td->link;
    /*
     * Build the new dummy TD and activate the old one
     */
     //分配一个td
    td = uhci_alloc_td(uhci);
    //分配出错则返回错误
    if (!td)
        goto nomem;
    //连接该td
    *plink = LINK_TO_TD(td);
    //填充该td,设置为out事务,数据长度为0,状态为0
    //不激活该TD
    uhci_fill_td(td, 0, USB_PID_OUT | uhci_explen(0), 0);
    wmb();
    //设置原虚拟td的状态为有效的td
    qh->dummy_td->status |= __constant_cpu_to_le32(TD_CTRL_ACTIVE);
    //连接新的虚拟td
    qh->dummy_td = td;
    /* Low-speed transfers get a different queue, and won't hog the bus.
     * Also, some devices enumerate better without FSBR; the easiest way
     * to do that is to put URBs on the low-speed queue while the device
     * isn't in the CONFIGURED state. */
    //判断urb所连接的usb设备的传输速度是否为低速
    //并且状态不为配置
    if (urb->dev->speed == USB_SPEED_LOW || urb->dev->state != USB_STATE_CONFIGURED)
        //取得低速龙骨号
        skel = SKEL_LS_CONTROL;
    else
    {
        //取得全速龙骨号
        skel = SKEL_FS_CONTROL;
        uhci_add_fsbr(uhci, urb);
    }
    //如果qh的状态不为有效则设置龙骨队列的号码
    if (qh->state != QH_STATE_ACTIVE)
        qh->skel = skel;
    //剔除setup事务中的8字节命令数据
    urb->actual_length = -8;    /* Account for the SETUP packet */
    return 0;
nomem:
    /* Remove the dummy TD from the td_list so it doesn't get freed */
    uhci_remove_td_from_urbp(qh->dummy_td);
    return -ENOMEM;
}

首先构建了一个控制传输中的setup事务,然后根据数据的大小构建了一个in事务,最后是状态阶段的out事务
在进入uhci_urb_enqueue中的uhci_activate_qh函数之前,先看一下现在qh中的队列结构

 
现在可以看出qh下挂载有4个td,前3个已经说明了,呢第4个是什么呢?观察qh结构中的dummy_td指针指向该td,dummy也就是虚拟的意思,这里的这第4个td也就是虚拟的td,不参加实际传输的td,呢不参加传输要这个td干嘛呢,这个虚拟td等于一个头,为了方便以后在该qh下加任务用的,如果需要添加一个控制传输,就把当前的虚拟td初始化为setup事务,最后再申请1个td作为虚拟td连接到qh中,如此反复
好现在进入uhci_activate_qh
uhci_activate_qh在/drivers/usb/host/uhci-q.c中

static void uhci_activate_qh(struct uhci_hcd *uhci, struct uhci_qh *qh)
{
    WARN_ON(list_empty(&qh->queue));
    /* Set the element pointer if it isn't set already.
     * This isn't needed for Isochronous queues, but it doesn't hurt. */
     //检测该qh的部件是否为终点
     //为终点则将部件连接到qh队列的下一个qh的第一个td
    if (qh_element(qh) == UHCI_PTR_TERM)
    {
        struct urb_priv *urbp = list_entry(qh->queue.next,struct urb_priv, node);
        struct uhci_td *td = list_entry(urbp->td_list.next,struct uhci_td, list);
        qh->element = LINK_TO_TD(td);
    }
    /* Treat the queue as if it has just advanced */
    qh->wait_expired = 0;
    //记录当前时间片到前进计数器中
    qh->advance_jiffies = jiffies;
    //检测qh是否已经启动
    if (qh->state == QH_STATE_ACTIVE)
        return;
    //设置qh的状态为启动
    qh->state = QH_STATE_ACTIVE;
    /* Move the QH from its old list to the correct spot in the appropriate
     * skeleton's list */
     //检测该qh是否为下一个扫描的qh
    if (qh == uhci->next_qh)
        //修改下一个扫描的qh为当前qh队列中的下一个
        uhci->next_qh = list_entry(qh->node.next, struct uhci_qh,node);
    list_del(&qh->node);
    //检测是否为等时传输
    if (qh->skel == SKEL_ISO)
        link_iso(uhci, qh);
    //检测是否为中断传输
    else if (qh->skel < SKEL_ASYNC)
        link_interrupt(uhci, qh);
    //控制传输和块传输
    else
        link_async(uhci, qh);
}

在这段代码的最后检测龙骨号,龙骨号在uhci_submit_control中赋值,对于全速设备来说是SKEL_FS_CONTROL,也就是21
21大于9,理所当然进入了link_async
link_async在/drivers/usb/host/uhci-q.c中

static void link_async(struct uhci_hcd *uhci, struct uhci_qh *qh)
{
    struct uhci_qh *pqh;
    __le32 link_to_new_qh;
    /* Find the predecessor QH for our new one and insert it in the list.
     * The list of QHs is expected to be short, so linear search won't
     * take too long. */
    //反向历遍uhci的9号龙骨qh上的qh
    //寻找适当的龙骨号进行插入
    list_for_each_entry_reverse(pqh, &uhci->skel_async_qh->node, node)
    {
        if (pqh->skel <= qh->skel)
            break;
    }
    //添加该qh到龙骨的qh队列中
    list_add(&qh->node, &pqh->node);
    /* Link it into the schedule */
    //修改该qh的硬件连接为要插入的qh的连接对象
    qh->link = pqh->link;
    wmb();
    //取得该qh的硬件地址
    link_to_new_qh = LINK_TO_QH(qh);
    //连接该qh的硬件地址
    pqh->link = link_to_new_qh;
    /* If this is now the first FSBR QH, link the terminating skeleton
     * QH to it. */
    if (pqh->skel < SKEL_FSBR && qh->skel >= SKEL_FSBR)
        uhci->skel_term_qh->link = link_to_new_qh;
}

好,现在硬件的连接就完成了,连接完成的调度队列如下

 
现在td队列通过qh连接到了帧队列上,之后uhci就会通过这些td和设备进行通信,呢如何知道通信是否成功呢?
这个任务交给了uhci_scan_schedule,又是谁调用了uhci_scan_schedule呢? usb_hcd_poll_rh_status通过uhci_hub_status_data调用了uhci_scan_schedule,也就是永远处于询问状态的usb_hcd_poll_rh_status,这样就真相大白了
好~现在就来看uhci_scan_schedule到底有何神通
uhci_scan_schedule在/drivers/usb/host/uhci-q.c中

static void uhci_scan_schedule(struct uhci_hcd *uhci)
{
    int i;
    struct uhci_qh *qh;
    /* Don't allow re-entrant calls */
    //只能在一个时间的一个线程内运行
    //有别的线程占用则返回
    if (uhci->scan_in_progress)
    {
        uhci->need_rescan = 1;
        return;
    }
    //设置标志位,不允许别的线程进入这个函数
    uhci->scan_in_progress = 1;
rescan:
    //清除重新扫描标志位
    uhci->need_rescan = 0;
    //清除fsbr需要标志位
    uhci->fsbr_is_wanted = 0;
    //正在调度队列,不希望产生td的中断
    uhci_clear_next_interrupt(uhci);
    //取得当前帧号
    uhci_get_current_frame_number(uhci);
    //设置当前扫描的帧号
    uhci->cur_iso_frame = uhci->frame_number;
    /* Go through all the QH queues and process the URBs in each one */
    //历遍0号到9号龙骨qh
    for (i = 0; i < UHCI_NUM_SKELQH - 1; ++i)
    {
        //获取龙骨qh所连接的第一个qh
        uhci->next_qh = list_entry(uhci->skelqh[i]->node.next,struct uhci_qh, node);
        //历遍龙骨qh所连接的qh
        while ((qh = uhci->next_qh) != uhci->skelqh[i])
        {
            //取得下一个qh
            uhci->next_qh = list_entry(qh->node.next,struct uhci_qh, node);
            //检测该qh队列中的td是否处理过
            if (uhci_advance_check(uhci, qh))
            {
                //扫描qh
                uhci_scan_qh(uhci, qh);
                if (qh->state == QH_STATE_ACTIVE)
                {
                    uhci_urbp_wants_fsbr(uhci,list_entry(qh->queue.next, struct urb_priv, node));
                }
            }
        }
    }
    //设置最后扫描的帧号
    uhci->last_iso_frame = uhci->cur_iso_frame;
    //检测是否需要再扫描
    if (uhci->need_rescan)
        goto rescan;
    //清除扫描标志位
    uhci->scan_in_progress = 0;
    if (uhci->fsbr_is_on && !uhci->fsbr_is_wanted && !uhci->fsbr_expiring)
    {
        uhci->fsbr_expiring = 1;
        mod_timer(&uhci->fsbr_timer, jiffies + FSBR_OFF_DELAY);
    }
    //检测0号龙骨qh是否有qh队列
    if (list_empty(&uhci->skel_unlink_qh->node))
        uhci_clear_next_interrupt(uhci);
    else
        uhci_set_next_interrupt(uhci);
}

uhci_advance_check负责检测qh队列中的td是否传输成功
uhci_advance_check在drivers/usb/host/uhci-q.c中

static int uhci_advance_check(struct uhci_hcd *uhci, struct uhci_qh *qh)
{
    struct urb_priv *urbp = NULL;
    struct uhci_td *td;
    int ret = 1;
    unsigned status;
    //不检测同步传输队列
    if (qh->type == USB_ENDPOINT_XFER_ISOC)
        goto done;
    /* Treat an UNLINKING queue as though it hasn't advanced.
     * This is okay because reactivation will treat it as though
     * it has advanced, and if it is going to become IDLE then
     * this doesn't matter anyway. Furthermore it's possible
     * for an UNLINKING queue not to have any URBs at all, or
     * for its first URB not to have any TDs (if it was dequeued
     * just as it completed). So it's not easy in any case to
     * test whether such queues have advanced. */
    //检测qh是否为有效状态
    if (qh->state != QH_STATE_ACTIVE)
    {
        urbp = NULL;
        status = 0;
    }
    else
    {
        //取得qh所连接的第一个urbp
        urbp = list_entry(qh->queue.next, struct urb_priv, node);
        //取得urbp所连接的第一个td
        td = list_entry(urbp->td_list.next, struct uhci_td, list);
        //取得td的状态字段
        status = td_status(td);
        //检测td是否为有效状态
        if (!(status & TD_CTRL_ACTIVE))
        {
            /* We're okay, the queue has advanced */
            //TD为无效状态则说明该TD已经被处理
            //队列前进了
            //设置等待到期标志为0
            qh->wait_expired = 0;
            //记录现在的时间片
            qh->advance_jiffies = jiffies;
            goto done;
        }
        ret = 0;
    }
    /* The queue hasn't advanced; check for timeout */
    //检测qh的等待到期位
    if (qh->wait_expired)
        goto done;
    //检测qh的处理时间是否过期
    if (time_after(jiffies, qh->advance_jiffies + QH_WAIT_TIMEOUT))
    {
        /* Detect the Intel bug and work around it */
        //排除intel的bug
        //检测是否有post_td
        //并且post_td是否为qh的唯一一个td
        if (qh->post_td && qh_element(qh) == LINK_TO_TD(qh->post_td))
        {
            //设置qh的元件为post_td的连接
            qh->element = qh->post_td->link;
            //记录现在的时间片
            qh->advance_jiffies = jiffies;
            ret = 1;
            goto done;
        }
        //设置等待到期位为1
        qh->wait_expired = 1;
        /* If the current URB wants FSBR, unlink it temporarily
         * so that we can safely set the next TD to interrupt on
         * completion. That way we'll know as soon as the queue
         * starts moving again. */
         //检测urbp是否存在
         //检测urbp的fsbr位是否为真
         //检测qh第一个td的IOC位是否为0
        if (urbp && urbp->fsbr && !(status & TD_CTRL_IOC))
            //从uhci硬件队列中剔除该qh
            uhci_unlink_qh(uhci, qh);
    }
    else
    {
        /* Unmoving but not-yet-expired queues keep FSBR alive */
        if (urbp)
            uhci_urbp_wants_fsbr(uhci, urbp);
    }
done:
    return ret;
}

主要就是检测td的status字段,在该字段有一个重要的位,第23位Active,设置该位为1,则使能td,当帧列表调度到该td的时候,会检测该位,为1则进行该td所设置的事务,当完成事务或者收到stall握手包的话就设置该位为0
当检测到有td处理之后,就要详细的检测td的完成情况了
这部分的工作由uhci_scan_qh来完成
uhci_scan_qh在drivers/usb/host/uhci-q.c中

static void uhci_scan_qh(struct uhci_hcd *uhci, struct uhci_qh *qh)
{
    struct urb_priv *urbp;
    struct urb *urb;
    int status;
    //历遍连接到该qh的所有urbp
    while (!list_empty(&qh->queue))
    {
        //取得urbp
        urbp = list_entry(qh->queue.next, struct urb_priv, node);
        //取得对应的urb
        urb = urbp->urb;
        //检测该qh的类型是否为同步传输
        if (qh->type == USB_ENDPOINT_XFER_ISOC)
            status = uhci_result_isochronous(uhci, urb);
        else
            status = uhci_result_common(uhci, urb);
        if (status == -EINPROGRESS)
            break;
        /* Dequeued but completed URBs can't be given back unless
         * the QH is stopped or has finished unlinking. */
         //检测urb的卸载位
        if (urb->unlinked)
        {
            //检测qh的状态是否为卸载中
            //检测uhci的当前扫描帧号加上停止标志是否等于
            //qh的卸载帧号
            if (QH_FINISHED_UNLINKING(qh))
                //设置qh的停止位为真
                qh->is_stopped = 1;
            //检测qh的停止位
            else if (!qh->is_stopped)
                return;
        }
        //返还urb给驱动程序
        uhci_giveback_urb(uhci, qh, urb, status);
        if (status < 0)
            break;
    }
    /* If the QH is neither stopped nor finished unlinking (normal case),
     * our work here is done. */
    //检测qh的状态是否为卸载中
    //检测uhci的当前扫描帧号加上停止标志是否等于
    //qh的卸载帧号
    if (QH_FINISHED_UNLINKING(qh))
        //设置qh的停止位为真
        qh->is_stopped = 1;
    检测qh的停止位
    else if (!qh->is_stopped)
        return;
    /* Otherwise give back each of the dequeued URBs */
restart:
    //历遍连接到qh的所有urbp
    list_for_each_entry(urbp, &qh->queue, node)
    {
        //取得urb
        urb = urbp->urb;
        //检测卸载标识
        if (urb->unlinked)
        {
            /* Fix up the TD links and save the toggles for
             * non-Isochronous queues. For Isochronous queues,
             * test for too-recent dequeues. */
            if (!uhci_cleanup_queue(uhci, qh, urb))
            {
                //设置qh的停止标识为0
                qh->is_stopped = 0;
                return;
            }
            //归还urb给驱动程序
            uhci_giveback_urb(uhci, qh, urb, 0);
            goto restart;
        }
    }
    //设置qh的停止标识为0
    qh->is_stopped = 0;
    /* There are no more dequeued URBs. If there are still URBs on the
     * queue, the QH can now be re-activated. */
     //检测qh的urbp队列是否为空
    if (!list_empty(&qh->queue))
    {
        //检测qh是否需要修正toggle
        if (qh->needs_fixup)
            uhci_fixup_toggles(qh, 0);
        /* If the first URB on the queue wants FSBR but its time
         * limit has expired, set the next TD to interrupt on
         * completion before reactivating the QH. */
         //取得urbp
        urbp = list_entry(qh->queue.next, struct urb_priv, node);
        //检测该urbp是否使用fsbr
        //检测qh是否等待到期
        if (urbp->fsbr && qh->wait_expired)
        {
            //取得urbp的td
            struct uhci_td *td = list_entry(urbp->td_list.next,struct uhci_td, list);
            //设置完成启动中断位
            td->status |= __cpu_to_le32(TD_CTRL_IOC);
        }
        //激活该qh
        uhci_activate_qh(uhci, qh);
    }
    /* The queue is empty. The QH can become idle if it is fully
     * unlinked. */
    else if (QH_FINISHED_UNLINKING(qh))
        uhci_make_qh_idle(uhci, qh);
}

uhci_scan_qh检测挂载在qh下的所有urb,检测这些urb的td是否完成,完成则调用uhci_giveback_urb把urb返回给发送urb的函数,让等待urb的线程继续走下去,检测urb的函数有uhci_result_isochronous和uhci_result_common两种,我们这里为控制传输,当然是进入uhci_result_common了
uhci_result_common在/drivers/usb/host/uhci-h.c中

static int uhci_result_common(struct uhci_hcd *uhci, struct urb *urb)
{
    //取得urbp
    struct urb_priv *urbp = urb->hcpriv;
    struct uhci_qh *qh = urbp->qh;
    struct uhci_td *td, *tmp;
    unsigned status;
    int ret = 0;
    //历遍连接到该urbp的所有td
    list_for_each_entry_safe(td, tmp, &urbp->td_list, list)
    {
        unsigned int ctrlstat;
        int len;
        //取得td的控制状态字段
        ctrlstat = td_status(td);
        //取得控制状态字段的17 18 20 21 22 23位
        status = uhci_status_bits(ctrlstat);
        //检测有效位是否为真
        //为真则说明该td尚未进行传输,完成处理
        if (status & TD_CTRL_ACTIVE)
            return -EINPROGRESS;
        //取得该td实际传输的数据长度
        len = uhci_actual_length(ctrlstat);
        //计算所有事务实际取得的数据长度
        urb->actual_length += len;
        //检测状态字段是否全为0
        if (status)
        {
            //取得令牌字段然后检测传输方向是否为输出
            //检测状态字段的各种错误位
            ret = uhci_map_status(status,uhci_packetout(td_token(td)));
            if ((debug == 1 && ret != -EPIPE) || debug > 1)
            {
                /* Some debugging code */
                dev_dbg(&urb->dev->dev,"%s: failed with status %x\n",__func__, status);

if (debug > 1 && errbuf)
                {
                    /* Print the chain for debugging */
                    uhci_show_qh(uhci, urbp->qh, errbuf,ERRBUF_LEN, 0);
                    
                    lprintk(errbuf);
                }
            }
        /* Did we receive a short packet? */
        }
        //检测传输长度是否小于预期长度(短包)
        else if (len < uhci_expected_length(td_token(td)))
        {
            /* For control transfers, go to the status TD if
             * this isn't already the last data TD */
             //检测qh的传输类型是否为控制
            if (qh->type == USB_ENDPOINT_XFER_CONTROL)
            {
                //因为在控制传输中最后一个td为状态事务
                //最后一个数据td为状态td的上一个
                //所以检测该td是否为最后一个数据td
                if (td->list.next != urbp->td_list.prev)
                    ret = 1;
            }
            /* For bulk and interrupt, this may be an error */
            //检测urb是否接受短包
            else if (urb->transfer_flags & URB_SHORT_NOT_OK)
                ret = -EREMOTEIO;
            /* Fixup needed only if this isn't the URB's last TD */
            //检测该td是否为为队列的最后一个td
            else if (&td->list != urbp->td_list.prev)
                ret = 1;
        }
        //从urbp中移除该td
        uhci_remove_td_from_urbp(td);
        //释放最后一个完成传输的td
        if (qh->post_td)
            uhci_free_td(uhci, qh->post_td);
        //连接该td到qh上
        qh->post_td = td;
        if (ret != 0)
            goto err;
    }
    return ret;
err:
    if (ret < 0)
    {
        /* Note that the queue has stopped and save
         * the next toggle value */
        qh->element = UHCI_PTR_TERM;
        qh->is_stopped = 1;
        qh->needs_fixup = (qh->type != USB_ENDPOINT_XFER_CONTROL);
        qh->initial_toggle = uhci_toggle(td_token(td)) ^ (ret == -EREMOTEIO);
    }
    else        /* Short packet received */
        //进行短包处理
        ret = uhci_fixup_short_transfer(uhci, qh, urbp);
    return ret;
}

这里主要的任务是检测短包,也就是传输长度小于与其长度的,在数据队列的传输中,只有最后一个数据事务中的数据包能为短包,其它都不应为短包,在此次控制事务中我们的数据阶段只有一个包,所以是否为短包都可以
检测无错后便能跳进呢梦寐以久的uhci_scan_qh中的uhci_giveback_urb
在uhci_giveback_urb中接着调用usb_hcd_giveback_urb,在usb_hcd_giveback_urb中会调用urb的complete函数,最终完成一次urb或者说事务的传输
现在我们又回到了微软方案中提交的
r = usb_control_msg(udev, usb_rcvaddr0pipe(),
     USB_REQ_GET_DESCRIPTOR, USB_DIR_IN,
     USB_DT_DEVICE << 8, 0,
     buf, GET_DESCRIPTOR_BUFSIZE,
     USB_CTRL_GET_TIMEOUT);
拿到了日思夜想的18字节设备描述符,咦?你说设备端如何发送设备描述符的?这个等我们完成设备的基本枚举后再慢慢分析~ = 3=
我这里d12所使用的程序为computer00所出品,大家一定要谢谢他写出这么好的代码让我们免费使用
现在看看描述符

code uint8 DeviceDescriptor[0x12]= //设备描述符为18字节

{
//bLength字段。设备描述符的长度为18(0x12)字节
 0x12,
//bDescriptorType字段。设备描述符的编号为0x01
 0x01,
//bcdUSB字段。这里设置版本为USB1.1,即0x0110。
//由于是小端结构,所以低字节在先,即0x10,0x01。
 0x10,
 0x01,
//bDeviceClass字段。我们不在设备描述符中定义设备类,
//而在接口描述符中定义设备类,所以该字段的值为0。
 0x00,
//bDeviceSubClass字段。bDeviceClass字段为0时,该字段也为0。
 0x00,
//bDeviceProtocol字段。bDeviceClass字段为0时,该字段也为0。
 0x00,
//bMaxPacketSize0字段。PDIUSBD12的端点0大小的16字节。
 0x10,
//idVender字段。厂商ID号,我们这里取0x8888,仅供实验用。
//实际产品不能随便使用厂商ID号,必须跟USB协会申请厂商ID号。
//注意小端模式,低字节在先。
 0x88,
 0x88,
//idProduct字段。产品ID号,由于是第一个实验,我们这里取0x0001。
//注意小端模式,低字节应该在前。
 0x01,
 0x00,
//bcdDevice字段。我们这个USB鼠标刚开始做,就叫它1.0版吧,即0x0100。
//小端模式,低字节在先。
 0x00,
 0x01,
//iManufacturer字段。厂商字符串的索引值,为了方便记忆和管理,
//字符串索引就从1开始吧。
 0x01
//iProduct字段。产品字符串的索引值。刚刚用了1,这里就取2吧。
//注意字符串索引值不要使用相同的值。
 0x02,
//iSerialNumber字段。设备的序列号字符串索引值。
//这里取3就可以了。
 0x03,
//bNumConfigurations字段。该设备所具有的配置数。
//我们只需要一种配置就行了,因此该值设置为1。
 0x01
};

可以看见端点0的最大包为16字节,呢么udev->descriptor.bMaxPacketSize0 = buf->bMaxPacketSize0;这一句中就会将包大小设置为16
微软的方案会这时候会再复位一次设备然后才设置地址
这样枚举中的6 7两步就完成了
现在到第8步了
第8步就在不远的地方
retval = usb_get_device_descriptor(udev, USB_DT_DEVICE_SIZE);
这次使用新地址获取完整的设备描述符
hub_port_init就完成了,回到hub_port_connect_change中,经过几个检测来到usb_new_device中,还记得uhci是怎么注册的嘛,现在终于也走到了这一步,枚举中的第9步就在usb_new_device中进行, usb_configure_device还记得这个函数么,回顾一下前面uhci中的注册吧,我就不多废话了

现在终于又开始匹配驱动程序了,这就是第10步
在新的linux版本中,模块驱动的匹配加载是由udev来进行的,我暂时不打算分析这个udev,从结果来看,udev会为我们的usb鼠标加载usbhid这个模块,而hid这个模块是系统启动时或者由udev加载的

不管由谁加载的,现在在usb总线上又多了一个驱动,至于这个驱动是啥,怎么运作,我们放到之后再讲,现在先把d12模拟成鼠标的控制传输的代码分析一下
在d12中事务的传输会引发d12的中断,然后判断一个中断寄存器来检测到底是哪个端口收到了什么样的令牌包,准备进行什么事务
在提供的实例程序中,由51单片机进行一个轮询的循环,检测产生中断的种类
如下:

while(1) //死循环
{
          if(D12GetIntPin()==0) //如果有中断发生
          {
               D12WriteCommand(READ_INTERRUPT_REGISTER); //写读中断寄存器的命令
               InterruptSource=D12ReadByte(); //读回第一字节的中断寄存器
               if(InterruptSource&0x80)UsbBusSuspend(); //总线挂起中断处理
               if(InterruptSource&0x40)UsbBusReset(); //总线复位中断处理
               if(InterruptSource&0x01)UsbEp0Out(); //端点0输出中断处理
               if(InterruptSource&0x02)UsbEp0In(); //端点0输入中断处理
               if(InterruptSource&0x04)UsbEp1Out(); //端点1输出中断处理
               if(InterruptSource&0x08)UsbEp1In(); //端点1输入中断处理
               if(InterruptSource&0x10)UsbEp2Out(); //端点2输出中断处理
               if(InterruptSource&0x20)UsbEp2In(); //端点2输入中断处理
          }
}

以控制传输取得设备描述符为例,看看d12是如何应答的
首先是setup令牌包,引发中断,检测类型为if(InterruptSource&0x01),则进入到UsbEp0Out中,代码如下

void UsbEp0Out(void)
{
#ifdef DEBUG0
     Prints("USB端点0输出中断。\r\n");
#endif
     //读取端点0输出最后传输状态,该操作清除中断标志
     //并判断第5位是否为1,如果是,则说明是建立包
     if(D12ReadEndpointLastStatus(0)&0x20)
     {
        D12ReadEndpointBuffer(0,16,Buffer); //读建立过程数据
         D12AcknowledgeSetup(); //应答建立包
          D12ClearBuffer(); //清缓冲区
          //将缓冲数据填到设备请求的各字段中
          bmRequestType=Buffer[0];
          bRequest=Buffer[1];
          wValue=Buffer[2]+(((uint16)Buffer[3])<<8);
          wIndex=Buffer[4]+(((uint16)Buffer[5])<<8);
          wLength=Buffer[6]+(((uint16)Buffer[7])<<8);
          //下面的代码判断具体的请求,并根据不同的请求进行相关操作
          //如果D7位为1,则说明是输入请求
          if((bmRequestType&0x80)==0x80)
          {
               //根据bmRequestType的D6~5位散转,D6~5位表示请求的类型
               //0为标准请求,1为类请求,2为厂商请求。
               switch((bmRequestType>>5)&0x03)
               {
                    case 0: //标准请求
#ifdef DEBUG0
                          Prints("USB标准输入请求:");
#endif
     //USB协议定义了几个标准输入请求,我们实现这些标准请求即可
     //请求的代码在bRequest中,对不同的请求代码进行散转
     //事实上,我们还需要对接收者进行散转,因为不同的请求接收者
     //是不一样的。接收者在bmRequestType的D4~D0位中定义。
     //我们这里为了简化操作,有些就省略了对接收者的判断。
     //例如获取描述符的请求,只根据描述符的类型来区别。
                         switch(bRequest)
                         {
                              case GET_CONFIGURATION: //获取配置
#ifdef DEBUG0
                                Prints("获取配置。\r\n");
#endif
                                  break;
                              case GET_DESCRIPTOR: //获取描述符
#ifdef DEBUG0
                            Prints("获取描述符——");
#endif
       //对描述符类型进行散转,对于全速设备,
       //标准请求只支持发送到设备的设备、配置、字符串三种描述符
                               switch((wValue>>8)&0xFF)
                                {
                                     case DEVICE_DESCRIPTOR: //设备描述符
#ifdef DEBUG0
                                           Prints("设备描述符。\r\n");
#endif
                                          pSendData=DeviceDescriptor; //需要发送的数据
          //判断请求的字节数是否比实际需要发送的字节数多
          //这里请求的是设备描述符,因此数据长度就是
          //DeviceDescriptor[0]。如果请求的比实际的长,
          //那么只返回实际长度的数据
                                    if(wLength>DeviceDescriptor[0])
                                    {
                                        SendLength=DeviceDescriptor[0];
                                        if(SendLength%DeviceDescriptor[7]==0) //并且刚好是整数个数据包时
                                        {
                                            NeedZeroPacket=1; //需要返回0长度的数据包
                                        }
                                    }
                                    else
                                    {
                                        SendLength=wLength;
                                    }
                                    //将数据通过EP0返回
                                    UsbEp0SendData();
                                    break;
......................
......................
}

首先取得setup事务中数据包呢8字节的内容,从中提取出bmRequestType,bRequest, wValue, wIndex和wLength5部分的内容,先根据bmRequestType来判断接下来要处理的是in事务还是out事务,如果为in事务,则需要准备数据等待发送,这里取得设备描述符为in事务,所以需要提前准备好数据,然后检测请求类型,这里为标准请求,进入到case 0中,接着又是一个switch,这里判断bRequest,也就是具体的请求类型,我们发送的是USB_REQ_GET_DESCRIPTOR,也就是0x6,这里符合的为GET_DESCRIPTOR,则进入到case GET_DESCRIPTOR中,再来一个swtich,这次的目标为wValue>>8,我们在URB中提交的为USB_DT_DEVICE << 8,也就是0x1<<8,呢么这里符合的为DEVICE_DESCRIPTOR,终于进入到了设备描述符中,然后判断数据和包的大小,检测是否需要分成数个数据包进行传输,在微软的方案中是获取64字节的数据,则这里会返回18字节的整个设备描述符,但是请注意,d12端点0的包大小最大只有16字节,所以这里先准备好16字节的数据,然后调用UsbEp0SendData进行发送, UsbEp0SendData的代码如下

void UsbEp0SendData(void)
{
 //将数据写到端点中去准备发送
 //写之前要先判断一下需要发送的数据是否比端点0
 //最大长度大,如果超过端点大小,则一次只能发送
 //最大包长的数据。端点0的最大包长在DeviceDescriptor[7]
     if(SendLength>DeviceDescriptor[7])
     {
          //按最大包长度发送
          D12WriteEndpointBuffer(1,DeviceDescriptor[7],pSendData);
          //发送后剩余字节数减少最大包长
          SendLength-=DeviceDescriptor[7];
          //发送一次后指针位置要调整
          pSendData+= DeviceDescriptor[7];
     }
     else
     {
          if(SendLength!=0)
          {
               //不够最大包长,可以直接发送
               D12WriteEndpointBuffer(1,SendLength,pSendData);
               //发送完毕后,SendLength长度变为0
               SendLength=0;
          }
          else //如果要发送的数据包长度为0
          {
               if(NeedZeroPacket==1) //如果需要发送0长度数据
               {
                    D12WriteEndpointBuffer(1,0,pSendData); //发送0长度数据包
                    NeedZeroPacket=0; //清需要发送0长度数据包标志

}
          }
     }
}
D12WriteEndpointBuffer为真正发送数据的函数, D12WriteEndpointBuffer的代码如下
uint8 D12WriteEndpointBuffer(uint8 Endp,uint8 Len,uint8 * Buf)
{
     uint8 i;
     D12SelectEndpoint(Endp); //选择端点
     D12WriteCommand(D12_WRITE_BUFFER); //写Write Buffer命令
     D12WriteByte(0); //该字节必须写0
     D12WriteByte(Len); //写需要发送数据的长度
#ifdef DEBUG1 //如果定义了DEBUG1,则需要显示调试信息
     Prints("写端点");
     PrintLongInt(Endp/2); //端点号。由于D12特殊的端点组织形式,
                       //这里的0和1分别表示端点0的输出和输入;
                       //而2、3分别表示端点1的输出和输入;
                       // 3、4分别表示端点2的输出和输入。
                       //因此要除以2才显示对应的端点。
     Prints("缓冲区");
     PrintLongInt(Len); //写入的字节数
     Prints("字节。\r\n");
#endif
     D12SetPortOut(); //将数据口设置为输出状态(注意这里为空宏,移植时可能有用)
     for(i=0;i<Len;i++)
     {
          //这里不直接调用写一字节的函数,而直接在这里模拟时序,可以节省时间
          D12ClrWr(); //WR置低
          D12SetData(*(Buf+i)); //将数据放到数据线上
          D12SetWr(); //WR置高,完成一字节写
#ifdef DEBUG1
          PrintHex(*(Buf+i)); //如果需要显示调试信息,则显示发送的数据
          if((i+1)%16==0)Prints("\r\n"); //每16字节换行一次
#endif
      }
#ifdef DEBUG1
     if((Len%16)!=0)
        Prints("\r\n"); //换行

#endif
     D12SetPortIn(); //数据口切换到输入状态
     D12ValidateBuffer(); //使端点数据有效
     return Len; //返回Len
}

d12构建数据包的方法很简单,就是往缓冲写数据,写完之后使缓冲有效,然后等in包来的时候,d12就会自动把数据包给发送出去了,写缓冲的方法如下
先写一个字节,这个字节默认为0,然后第二字节写数据的长度
之后就可以写数据了
写完数据后需要执行D12ValidateBuffer()函数使能缓冲区
现在d12发送了设备描述符的前16字节,呢剩下的2字节怎么办呢?很简单,微软的方案在控制传输完成后会复位设备,当啥都没发生过 = 3=
详细的代码我会提供在附录里

LINUX下USB1.1设备学习小记(5)_uhci与设备(2)相关推荐

  1. LINUX下USB1.1设备学习小记(2)_协…

    LINUX下USB1.1设备学习小记(2)_协议 (2009-03-27 14:40) 分类: 文章转载 USB协议: 先看USB接口 可以看出,在USB使用了4根线,分别为电源线,地线,信号线和差分 ...

  2. linux下rpm,yum学习

    linux下RPM及yum学习 linux中程序管理程序主要分为两类 dpkg(Debian Packager):debian,Ubuntu,Knoppix         rpm(Redhat Pa ...

  3. linux下的加密解密学习

    linux下的加密解密学习 加密/解密:         加密协议:加密解密使用同一秘钥:3des,aes         公钥加密:公钥私钥对         数字签名,密钥交换          ...

  4. linux读写usb host,LINUX下USB1.1设备学习小记(3)_host与device

    各位还记得"任何传输都是由host发起的"这句话么~ 在usb设备插入pc中到拔出usb设备,都是由host进行询问的 一个usb鼠标的工作流程可以表达如下: usb鼠标插入pc中 ...

  5. usb linux 内核,Linux下USB内核之学习笔记

    Linux下USB子系统软件结构为 USB 内核(USB驱动,USBD )处于系统的中心,对于它进行研究是能够进行USB驱动开发(包括客户驱动和主机驱动)的第一步.它为客户端驱动和主机控制器驱动提供了 ...

  6. Linux下高级C编程(学习总结)

    Linux下高级C编程 第一章 unix/linux系统的基本概念 第二章 unix/linux系统下的编程基础和开发方式 第三章 unix/linux系统下的内存管理 第四章 unix/linux系 ...

  7. linux下nginx软件的学习

    参考博客 1.nginx是什么 nginx是一个开源的,支持高性能,高并发的web服务和代理服务软件.它是开源的软件. nginx比它大哥apache性能改进许多,nginx占用的系统资源更少,支持更 ...

  8. 【转载】Linux下套接字学习

    感觉这个系列还不错,学习一下. 先看的是第三篇: http://blog.csdn.net/gatieme/article/details/46334337 < Linux下套接字详解(三)-- ...

  9. Linux下的ELF可执行文件学习总结

    Linux下的ELF可执行文件的格式解析 http://blog.csdn.net/xuchao1229/article/details/8915831 目录(?)[+] ELF(Executable ...

最新文章

  1. crontab工具类 断给定的时间 是否 满足 crontab 表达式.md
  2. python 如何把小数变成百分数格式
  3. 【POJ - 3041】Asteroids (二分图,最小点覆盖)
  4. 机器视觉 光学工程专业_瑞士Idonus MEMS制造设备 创新技术 机器视觉测量(远心光学)...
  5. Linux内存管理:内存描述之内存节点node
  6. python中exit 的作用_Python退出命令-为什么要使用这么多?何时使用每个命令?
  7. HDU3786 找出直系亲属【关系闭包】
  8. Child module pom.xml of pom.xml does not exist @
  9. 苹果手机屏幕镜像搜索不到电视_用手机开热点投屏需要流量吗?
  10. 对口升学计算机基础知识教案,教案河北省计算机专业对口升学讲义--计算机基础知识部分.ppt...
  11. 这可能是史上最全的常用学术网站
  12. java普通分隔符,懂得java的文件4种分隔符
  13. 如何获取微信小程序包
  14. 付费的「小密圈」值不值得我们加入呢?
  15. Chrome浏览器完美保存整个网页的两种方式
  16. 《Effective C++》学习笔记(条款25:考虑写出一个不抛异常的swap函数)
  17. primary key 主键
  18. 设计模式--谈谈Reactive Programming 响应式编程
  19. Lattice系列FPGA入门相关1(Lattice系列FPGA简介)
  20. 亿信华辰|政务行业数据治理存在哪些问题,该如何应对?

热门文章

  1. 计算机专业电脑i5与i7的区别,电脑i5处理器和i7处理器有什么区别
  2. 网桥与交换机的区别,以及和中继器,集线器 之间的联系及各自的功能
  3. HCIP之MPLS中的LDP协议
  4. 3.2 向量的线性关系、线性相关线性无关
  5. 利用Selenium秒填朋友圈各种问卷星调查问卷
  6. JVM调优系列(五)——JVM调优利器
  7. 阿里云国际版ECS云服务器Windows系统手动搭建WordPress
  8. Linux (Ubuntu)c编程 (入门必看)
  9. 【软考】系统集成项目管理工程师(十二)项目沟通管理
  10. 盘点cg设计师遇到的远程办公难题以及解决办法