5 --> pcie初始化枚举和资源分配流程代码分析

本文主要是对PCIe的初始化枚举、资源分配流程进行分析。

  1. PCIe architecture
    1.1 pcie的拓扑结构
    在分析PCIe初始化枚举流程之前,先描述下pcie的拓扑结构。
    如下图所示:
    在这里插入图片描述
    整个PCIe是一个树形的拓扑:
    • Root Complex是树的根,它一般实现了一个主桥设备(host bridge), 一条内部PCIe总线(BUS 0),以及通过若干个PCI bridge扩展出一些root port。host bridge可以完成CPU地址到PCI域地址的转换,pci bridge用于系统的扩展,没有地址转换功能;
    • Switch是转接器设备,目的是扩展PCIe总线。switch中有一个upstream port和若干个downstream port, 每一个端口都相当于一个pci bridge
    • PCIe ep device是叶子节点设备,比如pcie网卡,显卡,nvme卡等。

每个PCIe设备,包括host bridge、pci bridge和ep设备都有一个4k的配置空间。arm使用ecam的方式访问pcie配置空间。
1.2 PCIe的软件层次
PCIe模块涉及到的代码文件很多,在分析pcie的代码前,先对pcie的涉及到的代码梳理下。
这里以arm64架构为例,pcie代码主要分散在三个目录:

drivers/pci/*
driver/acpi/pci*
arch/arm64/kernel/pci.c

将pcie代码按如下层次划分:

    |-->+ pcie hp service driver  +
    |-->+ pcie aer service driver +
    |-->+ pcie pme service driver +
    |-->+ pcie dpc service driver +
    |
+---------------------+   +----------------+
| pcie port bus driver|   | pcie ep driver |
+---------------------+   +----------------+
+------------------------------------------+
|              pcie core driver            |
+------------------------------------------+
+------------------+   +-------------------+
| arch pcie driver |   | acpi pcie driver  |
+------------------+   +-------------------+
____________________________________________
+------------------------------------------+
|               pcie hardware              |
+------------------------------------------+

arch pcie driver:放一些和架构强相关的pcie的函数实现,对应arch/arm64/kernel/pci.c
acpi pcie driver: apci扫描时所涉及到的pcie代码,包括host bridge的解析初始化,pcie bus的创建,ecam的映射等, 对应drivers/acpi/pci*.c
pcie core driver: pcie的子系统代码,包括pcie的枚举流程,资源分配流程,中断流程等,主要对应drivers/pci/
pcie port bus driver: 是pcie port的四个service代码的整合, 四个service主要指的是dpc/pme/hotplug/aer,对应的是drivers/pci/pcie/*
pcie ep driver:是叶子节点的设备驱动,比如显卡,网卡,nvme等。

  1. Linux 内核的实现
    (2.1) pcie初始化流程
    pcie的代码文件这么多,初始化涉及的调用也很多,从哪一个开始看呢?
    内核通过initcore的 level 决定模块的启动顺序。
build_dir/target-aarch64_generic_musl/linux-layerscape_armv8_64b/linux-4.9.63$ cat System.map | grep pci | grep initcall
ffff0000090555b8 t __initcall_its_pci_msi_initearly
ffff000009055790 t __initcall_pcibus_class_init2
ffff000009055798 t __initcall_pci_driver_init2
ffff000009055880 t __initcall_acpi_pci_init3
ffff0000090558b0 t __initcall_register_xen_pci_notifier3
ffff0000090559e0 t __initcall_pci_slot_init4
ffff000009055d30 t __initcall_pci_apply_final_quirks5s
ffff0000090560f0 t __initcall_pci_proc_init6
ffff0000090560f8 t __initcall_pcie_portdrv_init6
ffff000009056108 t __initcall_pcie_pme_service_init6
ffff000009056110 t __initcall_gen_pci_driver_init6
ffff000009056118 t __initcall_ls_pcie_driver_init6
ffff000009056120 t __initcall_ls_pcie_ep_driver_init6
ffff000009056128 t __initcall_hisi_pcie_driver_init6
ffff0000090561d8 t __initcall_virtio_pci_driver_init6
ffff000009056280 t __initcall_serial_pci_driver_init6
ffff000009056350 t __initcall_ahci_pci_driver_init6
ffff000009056360 t __initcall_sil24_pci_driver_init6
ffff000009056510 t __initcall_hilscher_pci_driver_init6
ffff000009056528 t __initcall_pci_driver_init6
ffff000009056530 t __initcall_sercos3_pci_driver_init6
ffff000009056538 t __initcall_uio_pci_driver_init6
ffff000009056540 t __initcall_netx_pci_driver_init6
ffff000009056548 t __initcall_mf624_pci_driver_init6
ffff000009056568 t __initcall_vfio_pci_init6
ffff000009056580 t __initcall_dwc3_pci_driver_init6
ffff0000090565a8 t __initcall_ehci_pci_init6
ffff0000090565c0 t __initcall_ohci_pci_init6
ffff0000090565d8 t __initcall_xhci_pci_init6
ffff000009056ae0 t __initcall_pci_resource_alignment_sysfs_init7
ffff000009056ae8 t __initcall_pci_sysfs_init7

pcibus_class_init(): 注册pci_bus class,完成后创建了/sys/class/pci_bus目录。
pci_driver_init(): 注册pci_bus_type, 完成后创建了/sys/bus/pci目录。
acpi_pci_init(): 注册acpi_pci_bus, 并设置电源管理相应的操作。
acpi_init(): apci启动所涉及到的初始化流程,PCIe基于acpi的启动流程从该接口进入。

对acpi_init()流程展开,主要找和pci初始化相关的调用:

acpi_init() /* subsys_initcall(acpi_init) */
    +-> mmcfg_late_init()
    +-> acpi_scan_init()
        +-> acpi_pci_root_init()
            +-> acpi_scan_add_handler_with_hotplug(&pci_root_handler, "pci_root");
                +-> .attach = acpi_pci_root_add
        /*
         * register pci_link_handler to list: acpi_scan_handlers_list.
         * this handler has relationship with PCI IRQ.
         */
        +-> acpi_pci_link_init()
        /* we facus on PCI-ACPI, ignore other handlers' init */
        ...
        +-> acpi_bus_scan()
            /* create struct acpi_devices for all device in this system */
            --> acpi_walk_namespace()
            --> acpi_bus_attach()
                --> acpi_scan_attach_handler()
                    --> acpi_scan_match_handler()
                    --> handler->attach /* attach is acpi_pci_root_add */

mmcfg_late_init(), acpi先扫描MCFG表,MCFG表定义了ecam的相关资源。
acpi_pci_root_init(),定义pcie host bridge device的attach函数, ACPI的Definition Block中使用PNP0A03表示一个PCI Host Bridge。
acpi_pci_link_init(), 注册pci_link_handler, 主要和pcie IRQ相关。
acpi_bus_scan(), 会通过acpi_walk_namespace()会遍历system中所有的device,并为这些acpi device创建数据结构,执行对应device的attatch函数。根据ACPI spec定义,pcie host bridge device定义在DSDT表中,acpi在扫描过程中扫描DSDT,如果发现了pcie host bridge, 就会执行device对应的attach函数,调用到acpi_pci_root_add()。

acpi_pci_root_add的函数很长,完整代码就不贴了, 它主要做了几个动作
(1)通过ACPI的_SEG参数, 获取host bridge使用的segment号, segment指的就是pcie domain, 主要目的是为了突破pcie最大256条bus的限制。
(2)通过ACPI的_CRS里的BusRange类型资源取得该Host Bridge的Secondary总线范围,保存在root->secondary这个resource中
(3)通过ACPI的_BNN参数获取host bridge的根总线号。
执行到这里如果没有返回失败,硬件设备上会有如下打印:

pr_info(PREFIX "%s [%s](domain %04x %pR)\n",
        acpi_device_name(device), acpi_device_bid(device),
        root->segment, &root->secondary);
...
ACPI: PCI Root Bridge [PCI0](domain 0000 [bus 00-7f])

(2.2) pcie枚举流程

166 struct pci_bus *pci_acpi_scan_root(struct acpi_pci_root *root)
167 {
168         int node = acpi_get_node(root->device->handle);
169         struct acpi_pci_generic_root_info *ri;
170         struct pci_bus *bus, *child;
171         struct acpi_pci_root_ops *root_ops;
172
173         ri = kzalloc_node(sizeof(*ri), GFP_KERNEL, node);    
174         if (!ri)
175                 return NULL;
176
177         root_ops = kzalloc_node(sizeof(*root_ops), GFP_KERNEL, node);
178         if (!root_ops) {
179                 kfree(ri);
180                 return NULL;
181         }
182
183         ri->cfg = pci_acpi_setup_ecam_mapping(root);    -------(1)
184         if (!ri->cfg) {
185                 kfree(ri);
186                 kfree(root_ops);
187                 return NULL;
188         }
189
190         root_ops->release_info = pci_acpi_generic_release_info;
191         root_ops->prepare_resources = pci_acpi_root_prepare_resources;
192         root_ops->pci_ops = &ri->cfg->ops->pci_ops;     ----- (2)
193         bus = acpi_pci_root_create(root, root_ops, &ri->common, ri->cfg);  ---- (3)
194         if (!bus)
195                 return NULL;
196
            ....
202
203         return bus;
204 }

(2.2.1) 通过 pci_acpi_setup_ecam_mapping() 建立ecam映射。 arm64上访问pcie的配置空间都是通过ecam机制进行访问,将ecam的空间进行映射,这样cpu就可以通过访问内存访问到相应设备的配置空间。

118 static struct pci_config_window *
119 pci_acpi_setup_ecam_mapping(struct acpi_pci_root *root)
120 {
121         struct device *dev = &root->device->dev;
122         struct resource *bus_res = &root->secondary;
123         u16 seg = root->segment;
124         struct pci_ecam_ops *ecam_ops;
125         struct resource cfgres;
126         struct acpi_device *adev;
127         struct pci_config_window *cfg;
128         int ret;
129
130         ret = pci_mcfg_lookup(root, &cfgres, &ecam_ops);
131         if (ret) {
132                 dev_err(dev, "%04x:%pR ECAM region not found\n", seg, bus_res);
133                 return NULL;
134         }
135
136         adev = acpi_resource_consumer(&cfgres);
137         if (adev)
138                 dev_info(dev, "ECAM area %pR reserved by %s\n", &cfgres,
139                          dev_name(&adev->dev));
140         else
141                 dev_warn(dev, FW_BUG "ECAM area %pR not reserved in ACPI namespace\n",
142                          &cfgres);
143
144         cfg = pci_ecam_create(dev, &cfgres, bus_res, ecam_ops);
145         if (IS_ERR(cfg)) {
146                 dev_err(dev, "%04x:%pR error %ld mapping ECAM\n", seg, bus_res,
147                         PTR_ERR(cfg));
148                 return NULL;
149         }
150
151         return cfg;
152 }

pci_mcfg_lookup(), 通过该接口可以获取ecam的资源以及访问配置空间的操作ecam_ops.
ecam_ops默认是pci_generic_ecam_ops, 定义在drivers/pci/ecam.c中,但也可以由厂商自定义,厂商自定义的ecam_ops实现在 drivers/pci/controller/目录下, 比如hisi_pcie_ops和ali_pcie_ops,厂商会依据实际的硬件对ecam进行限制。

107 struct pci_ecam_ops ali_pcie_ops = {
108         .bus_shift    = 20,
109         .init         =  ali_pcie_init,
110         .pci_ops      = {
111                 .map_bus    = ali_pcie_map_bus,
112                 .read       = ali_pcie_rd_conf,
113                 .write      = ali_pcie_wr_conf,
114         }
115 };

pci_ecam_create(), 对ecam的地址进行ioremap,如果定义了ecam_ops->init,还会执行到相应的初始化函数中
设置root_ops的pci_ops, 这里的pci_ops就是对应上面说的ecam_ops->pci_ops, 即配置空间的访问接口
bus = acpi_pci_root_create(root, root_ops, &ri->common, ri->cfg);

struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root,
878                                      struct acpi_pci_root_ops *ops,
879                                      struct acpi_pci_root_info *info,
880                                      void *sysdata)
881 {
882         int ret, busnum = root->secondary.start;
883         struct acpi_device *device = root->device;
884         int node = acpi_get_node(device->handle);
885         struct pci_bus *bus;
886         struct pci_host_bridge *host_bridge;
887
            ...
906         bus = pci_create_root_bus(NULL, busnum, ops->pci_ops,
907                                   sysdata, &info->resources);  
908         if (!bus)
909                 goto out_release_info;
910
911         host_bridge = to_pci_host_bridge(bus->bridge);
            ...
923         pci_scan_child_bus(bus);
924         pci_set_host_bridge_release(host_bridge, acpi_pci_root_release_info,
925                                     info);
926         if (node != NUMA_NO_NODE)
927                 dev_printk(KERN_DEBUG, &bus->dev, "on NUMA node %d\n", node);
928         return bus;
929
930 out_release_info:
931         __acpi_pci_root_release_info(info);
932         return NULL;
933 }

pci_create_root_bus()用来创建该{segment: busnr}下的根总线。传递的参数: NULL是host bridge设备的parent节点; busnum是总线号; ops->pci_ops对应的是ecam->pci_ops,即配置空间的操作接口; sysdata私有数据,对应的是pcie_create_ecam()所返回的pci_cfg_window, 包括ecam的地址范围,映射地址等; info->resource是一个resource_list, 用来保存总线号,I/O空间,mem空间等信息。

2914 struct pci_bus *pci_create_root_bus(struct device *parent, int bus,
2915                 struct pci_ops *ops, void *sysdata, struct list_head *resources)
2916 {
2917         int error;
2918         struct pci_host_bridge *bridge;
2919
2920         bridge = pci_alloc_host_bridge(0);  
2921         if (!bridge)
2922                 return NULL;
2923
2924         bridge->dev.parent = parent;
2925
2926         list_splice_init(resources, &bridge->windows);
2927         bridge->sysdata = sysdata;
2928         bridge->busnr = bus;
2929         bridge->ops = ops;
2930
2931         error = pci_register_host_bridge(bridge);
2932         if (error < 0)
2933                 goto err_out;
2934
2935         return bridge->bus;
2936
2937 err_out:
2938         kfree(bridge);
2939         return NULL;
2940 }

2920行: 分配struct pci_host_bridge, 一个pci_host_bridge对应一个pci host bridge设备。
2924行:设置该bridge的parent为NULL, 说明host bridge device是最上层设备。
2931行: pci_register_host_bridge()。 注册host bridge device。 该函数比较长,就不贴具体实现了,主要是为host bridge数据结构注册对应的设备,创建了一个根总线pci_bus, 也为该pci_bus数据结构注册一个设备并填充初始化的数据。

         dev_set_name(&bridge->dev, "pci%04x:%02x", pci_domain_nr(bus),
                      bridge->busnr); 
         err = device_register(&bridge->dev);
         dev_set_name(&bus->dev, "%04x:%02x", pci_domain_nr(bus), bus->number);
         err = device_register(&bus->dev);

2935行:返回 pci_host_bridge 的 bus 成员。
到该函数结束我们已经有了一个root_bus device, 一个host bridge device, 也知道了他们的关系:
在这里插入图片描述
回到上面acpi_pci_root_create()函数中,看923行 pci_scan_child_bus(), 现在开始遍历host bridge主桥下的所有pci设备
函数也比较长,列一些关键的函数调用:

pci_scan_child_bus()
    +-> pci_scan_child_bus_extend()
        +-> for dev range(0, 256)
               pci_scan_slot()
                    +-> pci_scan_single_device()
                        +-> pci_scan_device()
                            +-> pci_bus_read_dev_vendor_id()
                            +-> pci_alloc_dev()
                            +-> pci_setup_device()
                        +-> pci_add_device()
        +-> for each pci bridge
            +-> pci_scan_bridge_extend()

pci_scan_slot(): 一条pcie总线最多32个设备,每个设备最多8个function, 所以这里pci_scan_child_bus枚举了所有的pcie function, 调用了pci_scan_slot 256次, pci_scan_slot调用pci_scan_single_device()配置当前总线下的所有pci设备。

pci_scan_single_device(): 进一步调用 pci_scan_device() 和 pci_add_device() 。pci_scan_device 先去通过配置空间访问接口读取设备的vendor id, 如果60s没读到,说明没有找到该设备。 如果找到该设备,则通过 pci_alloc_dev 创建 pci_dev 数据结构,并对pci的配置空间进行一些配置。pci_add_device 软件将 pci dev 添加到设备list中。

pci_setup_device(): 获取 pci 设备信息,中断号,BAR地址 和 大小 (使用 pci_read_bases 就是往BAR地址写1来计算的),并保存到pci_dev->resources中。

现在我们已经扫描完了 host bridge下的 bus 和 dev, 现在开始扫描 bridge, 一个 bridge 也对应一个 pci_dev。比如switch中的每一个 port 对应一个 pci bridge 。
pci_scan_bridge_extend() 就是用于扫描 pci桥和 pci桥下的所有设备, 这个函数会被调用2次,第一次是处理 BIOS 已经配置好的pci桥, 这个是为了兼容各个架构所做的妥协。通过2次调用 pci_scan_bridge_extend 函数,完成所有的pci桥的处理。

1061 static int pci_scan_bridge_extend(struct pci_bus *bus, struct pci_dev *dev,
1062                                   int max, unsigned int available_buses,
1063                                   int pass)
1064 {
1065         struct pci_bus *child;
             ......
1078         pci_read_config_dword(dev, PCI_PRIMARY_BUS, &buses);
1079         primary = buses & 0xFF;
1080         secondary = (buses >> 8) & 0xFF;
1081         subordinate = (buses >> 16) & 0xFF;
1082
1083         pci_dbg(dev, "scanning [bus %02x-%02x] behind bridge, pass %d\n",
1084                 secondary, subordinate, pass);
1085
             ......
1110         if ((secondary || subordinate) && !pcibios_assign_all_busses() &&
1111             !is_cardbus && !broken) {
                    .......
1145         } else {
1146
1151                 if (!pass) {
1152                         if (pcibios_assign_all_busses() || broken || is_cardbus)
1153
1162                                 pci_write_config_dword(dev, PCI_PRIMARY_BUS,
1163                                                        buses & ~0xffffff);
1164                         goto out;
1165                 }
1166
1167                 /* Clear errors */
1168                 pci_write_config_word(dev, PCI_STATUS, 0xffff);
1169
1175                 child = pci_find_bus(pci_domain_nr(bus), max+1);
1176                 if (!child) {
1177                         child = pci_add_new_bus(bus, dev, max+1);
1178                         if (!child)
1179                                 goto out;
1180                         pci_bus_insert_busn_res(child, max+1,
1181                                                 bus->busn_res.end);
1182                 }
1183                 max++;
1184                 if (available_buses)
1185                         available_buses--;
1186
1187                 buses = (buses & 0xff000000)
1188                       | ((unsigned int)(child->primary)     <<  0)
1189                       | ((unsigned int)(child->busn_res.start)   <<  8)
1190                       | ((unsigned int)(child->busn_res.end) << 16);
1191
                    ......
1200
1201                 /* We need to blast all three values with a single write */
1202                 pci_write_config_dword(dev, PCI_PRIMARY_BUS, buses);
1203
1204                 if (!is_cardbus) {
1205                         child->bridge_ctl = bctl;
1206                         max = pci_scan_child_bus_extend(child, available_buses);
1207                 } else {
                            .......
1239                 }
1240
1241                 /* Set subordinate bus number to its real value */
1242                 pci_bus_update_busn_res_end(child, max);
1243                 pci_write_config_byte(dev, PCI_SUBORDINATE_BUS, max);
1244         }
1245         .......
1268         return max;
1269 }

1078行:一开始读取pci bridge的主bus号,是因为有的体系结构可能已经在BIOS中对这些做过配置。如果需要在kernel中进行scan bridge,就不会进1110行的那个if 分支。
1151行: 第一次pci_scan_bridge的pass参数为0,在这里直接返回。
1187行:生成新的BUS号,准备写入pci配置空间。
1202行: 将该pci bridge的primary bus, secondary bus, subordinate bus写入配置空间。
1206行: 这里又递归调用了pci_scan_child_bus, 扫描该子总线下所有设备。
1242行: 比较关键, 每次递归结束把实际的subordinate bus写入pci桥的配置空间。subordinate bus表示该pci桥下最大的总线号。
最后,在PCIe总线树枚举完成后,返回PCIe总线树中的最后一个pci总线号,PCIe的枚举流程至此结束。

在这里插入图片描述
总的来说,枚举流程分为3步:

发现主桥设备和根总线;
发现主桥设备下的所有pci设备
如果主桥下面的设备是pci bridge, 那么再次遍历这个pci bridge桥下的所有pci设备,并以此递归,直到将当前的pci总线树遍历完毕,并且返回host bridge的subordinate总线号。

(2.3) pcie的资源分配
pcie枚举完成后,pci总线号已经分配,pcie ecam的映射、 pcie设备信息、BAR的个数及大小等也已经ready, 但此时并没有给各个pci device的BAR, pci bridge的mem, I/O, prefetch mem的base/limit寄存器分配资源。
这时就需要走到pcie的资源分配流程,整个资源分配的过程就是从系统的总资源里给每个pci device的bar分配资源,给每个pci桥的base, limit的寄存器分配资源。

pcie的资源分配流程整体比较复杂,主要介绍下总体的流程,对关键的函数再做展开。
pcie资源分配的入口在pci_acpi_scan_root()->pci_bus_assign_resources()
在调用pci_bus_assign_resources()之前,先调用pci_bus_size_bridges()
pci_bus_size_bridges(): 用深度优先递归确定各级pci桥上base/limit的大小,会记录在pci_dev->resource[PCI_BRIDGE_RESOURCES]中。

再进行资源分配pci_bus_assign_resources():

1376 void __pci_bus_assign_resources(const struct pci_bus *bus,
1377                                 struct list_head *realloc_head,
1378                                 struct list_head *fail_head)
1379 {
1380         struct pci_bus *b;
1381         struct pci_dev *dev;
1382
1383         pbus_assign_resources_sorted(bus, realloc_head, fail_head);
1384
1385         list_for_each_entry(dev, &bus->devices, bus_list) {
1386                 pdev_assign_fixed_resources(dev);
1387
1388                 b = dev->subordinate;
1389                 if (!b)
1390                         continue;
1391
1392                 __pci_bus_assign_resources(b, realloc_head, fail_head);
1393
1394                 switch (dev->class >> 8) {
1395                 case PCI_CLASS_BRIDGE_PCI:
1396                         if (!pci_is_enabled(dev))
1397                                 pci_setup_bridge(b);
1398                         break;
1399
1400                 case PCI_CLASS_BRIDGE_CARDBUS:
1401                         pci_setup_cardbus(b);
1402                         break;
1403
1404                 default:
1405                         pci_info(dev, "not setting up bridge for bus %04x:%02x\n",
1406                                  pci_domain_nr(b), b->number);
1407                         break;
1408                 }
1409         }
1410 }

1383行: pbus_assign_resources_sorted, 这个函数先对当前总线下设备请求的资源进行排序

+-> pbus_assign_resources_sorted()
    +-> list_for_each_entry(dev, &bus->devices, bus_list)
            __dev_sort_resources(dev, &head);
+-> __assign_resources_sorted(&head, realloc_head, fail_head);
    +-> assign_requested_resources_sorted(head, fail_head);
        +-> list_for_each_entry(dev_res, head, list)
            pci_assign_resource(dev_res->dev, idx)
                +-> pci_bus_alloc_resource()
                    +-> allocate_resource()
                        +-> find_resource()
                        +-> request_resource()
            +->pci_update_resource()

__dev_sort_resources将pci设备使用的资源进行对齐和排序,然后加入到head流程中。
__assign_resources_sorted中先调用find_resource()获取上游pci bridge的所管理的空间资源范围。 再调用request_resource()为当前pci设备分配pcie地址空间,最后调用pci_update_resource()将初始化pcie bar寄存器,将更新的资源区间写到寄存器。
1392行: 和枚举流程一样,这里也是用深度优先遍历的方法,依次分配各个pcie ep设备的bar资源。
1397行: pci_setup_bridge(),某个总线下所有设备BAR空间分配之后,将初始化该总线桥的配置空间中的memory base寄存器(该总线子树下所有设备使用的PCI总线域地址空间的基地址)和memory limit寄存器(总线子树使用的总地址空间的大小)。

总而言之,pcie的资源枚举过程可以概况如下:

获取上游pci 桥设备所管理的系统资源范围
使用DFS对所有的pci ep device进行bar资源的分配
使用DFS对当前pci桥设备的base和limit的值,并对这些寄存器进行更新。
至此,pci树中所有pci设备的BAR寄存器,以及pci桥的base、limit寄存器都已经初始化完毕。

参考链接:
https://blog.csdn.net/yhb1047818384/article/details/106676548
https://blog.csdn.net/zjy900507/category_7260136.html?spm=1001.2014.3001.5482

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值