• Linux gadget驱动分析2------设备识别过程


    设备连上主机之后,设备驱动做了的事.

    设备连上host的port之后,主机端会有一套策略发送请求获取device的一系列描述符.进行枚举过程.找到适合该device的驱动. 这样就可以与device进行通信.

    usb device这边收到主机的请求,会进入中断处理函数.可以说usb device 控制器的活动都是从中断开始的.

    #号之间的都是udc驱动中做的事,大部分与硬件相关,原理应该差不多.

    ########################nuc900_udc##########################

    nuc9xx系列的中断处理函数:

     1 static irqreturn_t nuc900_udc_irq(int irq, void *_dev)
     2 {
     3         struct nuc900_udc *dev;
     4         struct nuc900_ep *ep;
     5         u32 volatile IrqStL, IrqEnL;
     6         u32 volatile  IrqSt, IrqEn;
     7         int i=0, j;
     8 
     9         dev=(struct nuc900_udc *)(_dev);
    10 
    11         IrqStL = __raw_readl(controller.reg + REG_USBD_IRQ_STAT_L);    /* 0x000 register get interrupt status */
    12         IrqEnL = __raw_readl(controller.reg + REG_USBD_IRQ_ENB_L);
    13 
    14         IrqStL = IrqStL & IrqEnL ;
    15         if (!IrqStL) {
    16                 printk("Not our interrupt !
    ");
    17                 return IRQ_HANDLED;
    18         }
    19 
    20         if (IrqStL & IRQ_USB_STAT) {
    21                 IrqSt = __raw_readl(controller.reg + REG_USBD_IRQ_STAT);
    22                 IrqEn = __raw_readl(controller.reg + REG_USBD_IRQ_ENB);
    23                 __raw_writel(IrqSt, controller.reg + REG_USBD_IRQ_STAT);
    24 
    25                 IrqSt = IrqSt & IrqEn ;
    26 
    27                 if (IrqSt && dev->driver) {
    28                         for (i=0; i<6; i++) {
    29                                 if (IrqSt&(1<<i)) {
    30                                         paser_irq_stat(1<<i,dev);
    31                                         break;
    32                                 }
    33                         }
    34                 }
    35 
    36         }//end IRQ_USB_STAT
    37 
    38 
    39         if (IrqStL & IRQ_CEP) {
    40                 IrqSt = __raw_readl(controller.reg + REG_USBD_CEP_IRQ_STAT);
    41                 IrqEn = __raw_readl(controller.reg + REG_USBD_CEP_IRQ_ENB);
    42                 //printk("cep:%x, %x
    ", IrqSt, IrqEn);
    43                 IrqSt = IrqSt & IrqEn ;
    44                 __raw_writel(IrqSt, controller.reg + REG_USBD_CEP_IRQ_STAT);
    45 
    46                 if (IrqSt && dev->driver) {
    47                         //for(i=12;i>=0;i--)
    48                         if (IrqSt&CEP_STS_END) { //deal with STS END
    49                                 if (dev->ep0state == EP0_OUT_DATA_PHASE)
    50                                         IrqSt &= 0x1BF7;
    51                                 paser_irq_cep(CEP_STS_END,dev,IrqSt);
    52                         }
    53                         for (i=0; i<13; i++) {
    54                                 if (i == 10)
    55                                         continue;
    56                                 if (IrqSt&(1<<i)) {
    57                                         paser_irq_cep(1<<i,dev,IrqSt);
    58                                         //break;
    59                                 }
    60                         }
    61                 }
    62         }
    63 
    64         if (IrqStL & IRQ_NCEP) {
    65                 IrqStL >>= 2;
    66 
    67                 for (j = 0; j < 6; j++) { //6 endpoints
    68                         if (IrqStL & (1 << j)) {
    69                                 //in-token and out token interrupt can deal with one only
    70                                 IrqSt = __raw_readl(controller.reg + REG_USBD_EPA_IRQ_STAT + 0x28 * j);
    71                                 IrqEn = __raw_readl(controller.reg + REG_USBD_EPA_IRQ_ENB + 0x28 * j);
    72 
    73                                 IrqSt = IrqSt & IrqEn ;
    74                                 if (IrqSt && dev->driver) {
    75                                         ep = &dev->ep[j+1];
    76 
    77                                         for (i=12; i>=0; i--) {
    78                                                 if (IrqSt&(1<<i)) {
    79                                                         if ((1<<i) == EP_BO_SHORT_PKT)
    80                                                                 IrqSt &= 0x1FCF;//clear out token/RxED intr
    81                                                         if ((ep->EP_Type == EP_TYPE_BLK) || (ep->EP_Type == EP_TYPE_ISO))
    82                                                                 paser_irq_nep(1<<i, ep, IrqSt);
    83                                                         else if (ep->EP_Type == EP_TYPE_INT)
    84                                                                 paser_irq_nepint(1<<i, ep, IrqSt);
    85                                                         break;
    86                                                 }
    87                                         }
    88                                 }
    89                         }
    90                 }
    91         }//if end
    92 
    93         return IRQ_HANDLED;
    94 
    95 
    96 }

    基本原理就是从一些相关寄存器中读取出中断的类型,分别进入相应的处理流程.

    20行  if (IrqStL & IRQ_USB_STAT) {

      就是usb Suspend Resume 等中断发生时会进入该if

    39行  if (IrqStL & IRQ_CEP) {

      中断是控制端口中断,进入该if语句.

      usb host会通过控制端口获取一系列描述符. 所以device这边会先进入这里.

      然后再读取中断的子类型

      进入      51或57行的                            

       paser_irq_cep(1<<i,dev,IrqSt);

      

      1 void paser_irq_cep(int irq, struct nuc900_udc *dev, u32 IrqSt)
      2 {
      3         struct nuc900_ep    *ep = &dev->ep[0];
      4         struct nuc900_request    *req;
      5         int        is_last=1;
      6 
      7         if (list_empty(&ep->queue)) {
      8                 req = 0;
      9         } else {
     10                 req = list_entry(ep->queue.next, struct nuc900_request, queue);
     11         }
     12 
     13         switch (irq) {
     14         case CEP_SUPPKT://receive setup packet
     15                 dev->ep0state=EP0_IDLE;
     16                 dev->setup_ret = 0;
     17 
     18                 udc_isr_ctrl_pkt(dev);
     19                 break;
     20 
     21         case CEP_DATA_RXD:
     22 
     23 
     24                 if (dev->ep0state == EP0_OUT_DATA_PHASE) {
     25                         if (req)
     26                                 is_last = read_fifo(ep,req, 0);
     27 
     28                         __raw_writel(0x400, controller.reg + REG_USBD_CEP_IRQ_STAT);
     29 
     30                         if (!is_last)
     31                                 __raw_writel(0x440, controller.reg + REG_USBD_CEP_IRQ_ENB);//enable out token and status complete int
     32                         else { //transfer finished
     33                                 __raw_writel(0x04C, controller.reg + REG_USBD_CEP_IRQ_STAT);
     34                                 __raw_writel(CEP_NAK_CLEAR, controller.reg + REG_USBD_CEP_CTRL_STAT);    // clear nak so that sts stage is complete
     35                                 __raw_writel(0x400, controller.reg + REG_USBD_CEP_IRQ_ENB);        // suppkt int//enb sts completion int
     36                                 dev->ep0state = EP0_END_XFER;
     37                         }
     38                 }
     39 
     40                 return;
     41 
     42         case CEP_IN_TOK:
     43 
     44                 if ((IrqSt & CEP_STS_END))
     45                         dev->ep0state=EP0_IDLE;
     46 
     47                 if (dev->setup_ret < 0) { // == -EOPNOTSUPP)
     48                         printk("CEP send zero pkt
    ");
     49                         __raw_writel(CEP_ZEROLEN, controller.reg + REG_USBD_CEP_CTRL_STAT);
     50                         __raw_writel(0x400, controller.reg + REG_USBD_CEP_IRQ_ENB);        //enb sts completion int
     51                 }
     52 
     53                 else if (dev->ep0state == EP0_IN_DATA_PHASE) {
     54 
     55 
     56                         if (req) {
     57                                 is_last = write_fifo(ep,req);
     58                         }
     59 
     60 
     61                         if (!is_last)
     62                                 __raw_writel(0x408, controller.reg + REG_USBD_CEP_IRQ_ENB);
     63                         else {
     64                                 if (dev->setup_ret >= 0)
     65                                         __raw_writel(CEP_NAK_CLEAR, controller.reg + REG_USBD_CEP_CTRL_STAT);    // clear nak so that sts stage is complete
     66                                 __raw_writel(0x402, controller.reg + REG_USBD_CEP_IRQ_ENB);        // suppkt int//enb sts completion int
     67 
     68                                 if (dev->setup_ret < 0)//== -EOPNOTSUPP)
     69                                         dev->ep0state=EP0_IDLE;
     70                                 else if (dev->ep0state != EP0_IDLE)
     71                                         dev->ep0state=EP0_END_XFER;
     72                         }
     73                 }
     74 
     75                 return;
     76 
     77         case CEP_PING_TOK:
     78 
     79                 __raw_writel(0x402, controller.reg + REG_USBD_CEP_IRQ_ENB);        // suppkt int//enb sts completion int
     80                 return;
     81 
     82         case CEP_DATA_TXD:
     83                 return;
     84 
     85         case CEP_STS_END:
     86 
     87                 __raw_writel(0x4A, controller.reg + REG_USBD_CEP_IRQ_ENB);
     88                 udc_isr_update_dev(dev);
     89                 dev->ep0state=EP0_IDLE;
     90                 dev->setup_ret = 0;
     91 
     92                 break;
     93 
     94         default:
     95                 break;
     96 
     97         }
     98 
     99         return ;
    100 
    101 }

    很简单的switch语句,device控制器硬件会根据host发来的包类型设置相应寄存器. 中断处理函数再从该寄存器中获得类型,进入不同的switch分支.

    host请求设备描述符是usb控制传输,并通过device的控制端口进行,接下去会进入哪个case分支跟控制传输的流程有关

    大体上是 :

    1,host发送 setup 到device         .device收到后准备相关data

    2,host发送 in      到device     device收到后发送data到host

    3,host 发送 out   到device       host 把状态数据发到device

    详细流程可参考网上文章.

    case setup中

    18行  udc_isr_ctrl_pkt(dev);

      1 static void udc_isr_ctrl_pkt(struct nuc900_udc *dev)
      2 {
      3         u32    temp;
      4         u32    ReqErr=0;
      5         struct nuc900_ep *ep = &dev->ep[0];
      6         struct usb_ctrlrequest    crq;
      7         struct nuc900_request    *req;
      8         int ret;
      9 
     10         if (list_empty(&ep->queue)) {
     11                 //printk("ctrl ep->queue is empty
    ");
     12                 req = 0;
     13         } else {
     14                 req = list_entry(ep->queue.next, struct nuc900_request, queue);
     15                 //printk("req = %x
    ", req);
     16         }
     17 
     18         temp = __raw_readl(controller.reg + REG_USBD_SETUP1_0);
     19 
     20         Get_SetupPacket(&crq,temp);
     21 
     22         dev->crq = crq;
     23 
     24         switch (dev->ep0state) {
     25         case EP0_IDLE:
     26                 switch (crq.bRequest) {
     27 
     28                 case USBR_SET_ADDRESS:
     29                         ReqErr = ((crq.bRequestType == 0) && ((crq.wValue & 0xff00) == 0)
     30                                   && (crq.wIndex == 0) && (crq.wLength == 0)) ? 0 : 1;
     31 
     32                         if ((crq.wValue & 0xffff) > 0x7f) {    //within 7f
     33                                 ReqErr=1;    //Devaddr > 127
     34                         }
     35 
     36                         if (dev->usb_devstate == 3) {
     37                                 ReqErr=1;    //Dev is configured
     38                         }
     39 
     40                         if (ReqErr==1) {
     41                                 break;        //break this switch loop
     42                         }
     43 
     44                         if (dev->usb_devstate == 2) {
     45                                 if (crq.wValue == 0)
     46                                         dev->usb_devstate = 1;        //enter default state
     47                                 dev->usb_address = crq.wValue;    //if wval !=0,use new address
     48                         }
     49 
     50                         if (dev->usb_devstate == 1) {
     51                                 if (crq.wValue != 0) {
     52                                         dev->usb_address = crq.wValue;
     53                                         dev->usb_devstate = 2;
     54                                 }
     55                         }
     56 
     57                         break;
     58 
     59                 case USBR_SET_CONFIGURATION:
     60                         ReqErr = ((crq.bRequestType == 0) && ((crq.wValue & 0xff00) == 0) &&
     61                                   ((crq.wValue & 0x80) == 0) && (crq.wIndex == 0) &&
     62                                   (crq.wLength == 0)) ? 0 : 1;
     63 
     64                         if (dev->usb_devstate == 1) {
     65                                 ReqErr=1;
     66                         }
     67 
     68                         if (ReqErr==1) {
     69                                 break;    //break this switch loop
     70                         }
     71 
     72                         if (crq.wValue == 0)
     73                                 dev->usb_devstate = 2;
     74                         else
     75                                 dev->usb_devstate = 3;
     76                         break;
     77 
     78                 case USBR_SET_INTERFACE:
     79                         ReqErr = ((crq.bRequestType == 0x1) && ((crq.wValue & 0xff80) == 0)
     80                                   && ((crq.wIndex & 0xfff0) == 0) && (crq.wLength == 0)) ? 0 : 1;
     81 
     82                         if (!((dev->usb_devstate == 0x3) && (crq.wIndex == 0x0) && (crq.wValue == 0x0)))
     83                                 ReqErr=1;
     84 
     85                         if (ReqErr == 1) {
     86                                 break;    //break this switch loop
     87                         }
     88 
     89                         break;
     90 
     91                 default:
     92                         break;
     93                 }//switch end
     94 
     95                 if (crq.bRequestType & USB_DIR_IN) {
     96                         dev->ep0state = EP0_IN_DATA_PHASE;
     97                         __raw_writel(0x08, controller.reg + REG_USBD_CEP_IRQ_ENB);
     98                 } else {
     99                         dev->ep0state = EP0_OUT_DATA_PHASE;
    100                         __raw_writel(0x40, controller.reg + REG_USBD_CEP_IRQ_ENB);
    101                 }
    102 
    103                 ret = dev->driver->setup(&dev->gadget, &crq);
    104                 dev->setup_ret = ret;
    105                 if (ret < 0) {
    106 
    107                         __raw_writel(0x400, controller.reg + REG_USBD_CEP_IRQ_STAT);
    108                         __raw_writel(0x448, controller.reg + REG_USBD_CEP_IRQ_ENB);        // enable in/RxED/status complete interrupt
    109                         __raw_writel(CEP_NAK_CLEAR, controller.reg + REG_USBD_CEP_CTRL_STAT);    //clear nak so that sts stage is complete
    110 
    111 
    112                         if (ret == -EOPNOTSUPP)
    113                                 printk("Operation %x not supported
    ", crq.bRequest);
    114                         else {
    115                                 printk("dev->driver->setup failed. (%d)
    ",ret);
    116                         }
    117                 } else if (ret > 1000) { //DELAYED_STATUS
    118                         printk("DELAYED_STATUS:%p
    ", req);
    119                         dev->ep0state = EP0_END_XFER;
    120                         __raw_writel(0, controller.reg + REG_USBD_CEP_IRQ_ENB);
    121                 }
    122 
    123                 break;
    124 
    125         case EP0_STALL:
    126                 break;
    127         default:
    128                 break;
    129         }
    130 
    131         if (ReqErr == 1) {
    132                 __raw_writel(CEP_SEND_STALL, controller.reg + REG_USBD_CEP_CTRL_STAT);
    133                 dev->ep0state = EP0_STALL;
    134         }
    135 
    136 }

    18 20行就是从寄存器拿出来setup 包的数据,放到  struct usb_ctrlrequest    crq;

    struct usb_ctrlrequest {
     __u8 bRequestType;
     __u8 bRequest;
     __le16 wValue;
     __le16 wIndex;
     __le16 wLength;
    } __attribute__ ((packed));

     这个数据结构与usb协议里的定义相一致.

    下面的操作全是围绕这个结构进行的.

    如果请bRequest是USBR_SET_ADDRESS, USBR_SET_CONFIGURATION,USBR_SET_INTERFACE.就地处理.

    如果不是,进入103行的 调用gadget驱动的setup函数.

    ########################nuc900_udc##########################

    这个setup就是上一篇

    static struct usb_gadget_driver composite_driver = {
     .speed  = USB_SPEED_HIGH,

     .bind  = composite_bind,
     .unbind  = composite_unbind,

     .setup  = composite_setup,
     .disconnect = composite_disconnect,

     .suspend = composite_suspend,
     .resume  = composite_resume,

     .driver = {
      .owner  = THIS_MODULE,
     },
    };

    中的setup,就是composite_setup

      1 /*
      2  * The setup() callback implements all the ep0 functionality that's
      3  * not handled lower down, in hardware or the hardware driver(like
      4  * device and endpoint feature flags, and their status).  It's all
      5  * housekeeping for the gadget function we're implementing.  Most of
      6  * the work is in config and function specific setup.
      7  */
      8 static int
      9 composite_setup(struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
     10 {
     11     struct usb_composite_dev    *cdev = get_gadget_data(gadget);
     12     struct usb_request        *req = cdev->req;
     13     int                value = -EOPNOTSUPP;
     14     u16                w_index = le16_to_cpu(ctrl->wIndex);
     15     u8                intf = w_index & 0xFF;
     16     u16                w_value = le16_to_cpu(ctrl->wValue);
     17     u16                w_length = le16_to_cpu(ctrl->wLength);
     18     struct usb_function        *f = NULL;
     19     u8                endp;
     20 
     21     /* partial re-init of the response message; the function or the
     22      * gadget might need to intercept e.g. a control-OUT completion
     23      * when we delegate to it.
     24      */
     25     req->zero = 0;
     26     req->complete = composite_setup_complete;
     27     req->length = USB_BUFSIZ;
     28     gadget->ep0->driver_data = cdev;
     29 
     30     switch (ctrl->bRequest) {
     31 
     32     /* we handle all standard USB descriptors */
     33     case USB_REQ_GET_DESCRIPTOR:
     34         if (ctrl->bRequestType != USB_DIR_IN)
     35             goto unknown;
     36         switch (w_value >> 8) {
     37 
     38         case USB_DT_DEVICE:  // 1
     39             cdev->desc.bNumConfigurations =
     40                 count_configs(cdev, USB_DT_DEVICE);
     41             value = min(w_length, (u16) sizeof cdev->desc);
     42             memcpy(req->buf, &cdev->desc, value);
     43             break;
     44         case USB_DT_DEVICE_QUALIFIER:
     45             if (!gadget_is_dualspeed(gadget))
     46                 break;
     47             device_qual(cdev);
     48             value = min_t(int, w_length,
     49                 sizeof(struct usb_qualifier_descriptor));
     50             break;
     51         case USB_DT_OTHER_SPEED_CONFIG:
     52             if (!gadget_is_dualspeed(gadget))
     53                 break;
     54             /* FALLTHROUGH */
     55         case USB_DT_CONFIG: // 2 若干次跟config个数有关
     56             value = config_desc(cdev, w_value);
     57             if (value >= 0)
     58                 value = min(w_length, (u16) value);
     59             break;
     60         case USB_DT_STRING: // 3 若干次跟function个数有关
     61             value = get_string(cdev, req->buf,
     62                     w_index, w_value & 0xff);
     63             if (value >= 0)
     64                 value = min(w_length, (u16) value);
     65             break;
     66         }
     67         break;
     68 
     69     /* any number of configs can work */
     70     case USB_REQ_SET_CONFIGURATION:  // 4 
     71         if (ctrl->bRequestType != 0)
     72             goto unknown;
     73         if (gadget_is_otg(gadget)) {
     74             if (gadget->a_hnp_support)
     75                 DBG(cdev, "HNP available
    ");
     76             else if (gadget->a_alt_hnp_support)
     77                 DBG(cdev, "HNP on another port
    ");
     78             else
     79                 VDBG(cdev, "HNP inactive
    ");
     80         }
     81         spin_lock(&cdev->lock);
     82         value = set_config(cdev, ctrl, w_value);
     83         spin_unlock(&cdev->lock);
     84         break;
     85     case USB_REQ_GET_CONFIGURATION:
     86         if (ctrl->bRequestType != USB_DIR_IN)
     87             goto unknown;
     88         if (cdev->config)
     89             *(u8 *)req->buf = cdev->config->bConfigurationValue;
     90         else
     91             *(u8 *)req->buf = 0;
     92         value = min(w_length, (u16) 1);
     93         break;
     94 
     95     /* function drivers must handle get/set altsetting; if there's
     96      * no get() method, we know only altsetting zero works.
     97      */
     98     case USB_REQ_SET_INTERFACE:
     99         if (ctrl->bRequestType != USB_RECIP_INTERFACE)
    100             goto unknown;
    101         if (!cdev->config || w_index >= MAX_CONFIG_INTERFACES)
    102             break;
    103         f = cdev->config->interface[intf];
    104         if (!f)
    105             break;
    106         if (w_value && !f->set_alt)
    107             break;
    108         value = f->set_alt(f, w_index, w_value);
    109         break;
    110     case USB_REQ_GET_INTERFACE:
    111         if (ctrl->bRequestType != (USB_DIR_IN|USB_RECIP_INTERFACE))
    112             goto unknown;
    113         if (!cdev->config || w_index >= MAX_CONFIG_INTERFACES)
    114             break;
    115         f = cdev->config->interface[intf];
    116         if (!f)
    117             break;
    118         /* lots of interfaces only need altsetting zero... */
    119         value = f->get_alt ? f->get_alt(f, w_index) : 0;
    120         if (value < 0)
    121             break;
    122         *((u8 *)req->buf) = value;
    123         value = min(w_length, (u16) 1);
    124         break;
    125     default:
    126 unknown:
    127         VDBG(cdev,
    128             "non-core control req%02x.%02x v%04x i%04x l%d
    ",
    129             ctrl->bRequestType, ctrl->bRequest,
    130             w_value, w_index, w_length);
    131 
    132         /* functions always handle their interfaces and endpoints...
    133          * punt other recipients (other, WUSB, ...) to the current
    134          * configuration code.
    135          *
    136          * REVISIT it could make sense to let the composite device
    137          * take such requests too, if that's ever needed:  to work
    138          * in config 0, etc.
    139          */
    140         switch (ctrl->bRequestType & USB_RECIP_MASK) {
    141         case USB_RECIP_INTERFACE:
    142             f = cdev->config->interface[intf];
    143             break;
    144 
    145         case USB_RECIP_ENDPOINT:
    146             endp = ((w_index & 0x80) >> 3) | (w_index & 0x0f);
    147             list_for_each_entry(f, &cdev->config->functions, list) {
    148                 if (test_bit(endp, f->endpoints))
    149                     break;
    150             }
    151             if (&f->list == &cdev->config->functions)
    152                 f = NULL;
    153             break;
    154         }
    155 
    156         if (f && f->setup)
    157             value = f->setup(f, ctrl);
    158         else {
    159             struct usb_configuration    *c;
    160 
    161             c = cdev->config;
    162             if (c && c->setup)
    163                 value = c->setup(c, ctrl);
    164         }
    165 
    166         goto done;
    167     }
    168 
    169     /* respond with data transfer before status phase? */
    170     if (value >= 0) {
    171         req->length = value;
    172         req->zero = value < w_length;
    173         value = usb_ep_queue(gadget->ep0, req, GFP_ATOMIC);
    174         if (value < 0) {
    175             DBG(cdev, "ep_queue --> %d
    ", value);
    176             req->status = 0;
    177             composite_setup_complete(gadget->ep0, req);
    178         }
    179     }
    180 
    181 done:
    182     /* device either stalls (value < 0) or reports success */
    183     return value;
    184 }

     这个函数做的就是根据host发来的请求,填充相应的req->buf,然后在最后173行,把req挂到req list上.

    按照usb控制传输,setup阶段之后,是datain阶段. host会发送intoken包, device接到该包,会进到上面贴的paser_irq_cep函数的42行的case中调用 write_fifo.

    write_fifo把数据写到缓冲区,并把req从req list上拿掉.

    这个函数157行调用function的setup,处理特殊的setup请求.

    之前的内核没有composite这一层,每一种gadget设备,都要有一个setup函数,后来的内核把通用的内容都放到了composite.c. 写gadget 驱动就简化了些.

    设备识别时device这边是跟着host的请求做相应的动作,函数大体流程就是这样,细节部分以后再慢慢研究

  • 相关阅读:
    卡片选项页面 JTabbedPane 的使用
    下拉列表 JComboBox 的使用
    单选按钮 JradioButton 和复选框 JcheckBox 的使用
    标签 JLable 类
    文本区 JTextArea 的使用
    密码框JPasswordField 的使用
    JHDU 2601 An easy problem (数学 )
    HDU 2554 N对数的排列问题 ( 数学 )
    LaTeX初识 新手入门 Texlive和Texmaker学习
    [leetcode-387-First Unique Character in a String]
  • 原文地址:https://www.cnblogs.com/fengeryi/p/3374590.html
Copyright © 2020-2023  润新知