USB总线-Linux内核USB3.0设备控制器驱动框架分析(四)
1.概述
如下图所示,USB控制器可以呈现出两种不同的状态。USB控制器作为Host时,称为USB主机控制器,使用USB主机控制器驱动。USB控制器作为Device时,称为USB设备控制器,使用UDC(usb device controller)驱动。本节只分析USB控制器作为Device时的驱动框架。
USB控制器作为Device时,驱动框架可分为5层。最上层的是Gadget Function驱动,代表了具体设备的驱动,如大容量存储设备驱动(U盘、移动硬盘等)、通讯类设备驱动(USB串口、USB虚拟网卡等)、UAC驱动(USB麦克风、USB声卡等USB音频类设备)。接下来是Gadget Funcation API层,该层是一个抽象层,向上和向下提供统一的API,屏蔽了差异,提高了驱动的兼容性。Composite层是一个可选的中间层,可通过一种配置或多种配置高效的支持多种功能的设备,简化了USB复合设备驱动的开发。目前最流行的是使用基于Composite和configfs实现的USB gadget configfs,可在用户空间灵活的配置USB设备。UDC驱动直接访问硬件,控制USB设备与USB主机之间的通信。USB设备控制器通过USB线缆连接USB主机控制器,负责USB数据的发送和接收。
2.Gadget Function驱动
Linux内核的USB Gadget Function驱动都在drivers/usb/gadget/function/目录下,有通讯设备类(Communication Device Class)驱动(f_acm.c、f_ecm、f_serial.c等)、USB音频设备类驱动(f_uac1.c、f_uac2.c、u_audio.c)、大容量存储设备驱动(f_mass_storage.c)、USB视频设备类驱动(f_uvc.c)等。
Gadget Function驱动的入口使用usb_function_driver
数据结构描述,驱动需要实现alloc_inst
和alloc_func
函数。alloc_inst
创建usb_function_instance
数据结构并初始化。alloc_func
创建usb_function
并初始化,重点是设置里面的回调函数,通常情况下,不直接使用usb_function
数据结构,而是嵌入到驱动的数据结构中使用。Composite驱动会通过Gadget Function API回调alloc_inst
和alloc_func
函数。usb_function
了描述Gadget Function驱动,Gadget Function驱动的重点是实现这些回调函数。
[include/linux/usb/composite.h]
struct usb_function_driver {const char *name;struct module *mod;struct list_head list;// 创建usb_function_instance并初始化struct usb_function_instance *(*alloc_inst)(void);// 创建usb_function并初始化struct usb_function *(*alloc_func)(struct usb_function_instance *inst);
};
struct usb_function { // 描述了一个gadget Function驱动const char *name; // gadget Function驱动名称struct usb_gadget_strings **strings; // 字符串表,由bind分配和控制请求提供的语言IDsstruct usb_descriptor_header **fs_descriptors; // full speed描述符struct usb_descriptor_header **hs_descriptors; // high speed描述符struct usb_descriptor_header **ss_descriptors; // super speed描述符struct usb_configuration *config; // usb_add_function函数添加的配置// 驱动的bind回调函数,分配驱动所需的资源,如配置、端点、I/O缓冲区等int (*bind)(struct usb_configuration *, struct usb_function *);// 释放bind分配的资源void (*unbind)(struct usb_configuration *, struct usb_function *);void (*free_func)(struct usb_function *f); // 释放usb_function// 设置可选的配置,有时候驱动可能有多个配置,需要使用set_alt进行切换int (*set_alt)(struct usb_function *, unsigned interface, unsigned alt);// 获取当前的设置的可选配置,如果没有多个配置,则默认使用配置0,则返回0int (*get_alt)(struct usb_function *, unsigned interface);// disable gadget function驱动,主机复位、主机重新配置gadget、断开连接时使用void (*disable)(struct usb_function *);// 用于特殊接口的控制请求int (*setup)(struct usb_function *, const struct usb_ctrlrequest *);// 测试某些设备类请求能否被处理bool (*req_match)(struct usb_function *, const struct usb_ctrlrequest *);void (*suspend)(struct usb_function *);void (*resume)(struct usb_function *);/* USB 3.0 additions */// 向GetStatus请求返回当前gadget Function驱动的状态int (*get_status)(struct usb_function *); // 当接收到SetFeature(FUNCTION_SUSPEND)时,回调该函数int (*func_suspend)(struct usb_function *, u8 suspend_opt);/* private: internals */struct list_head list;DECLARE_BITMAP(endpoints, 32); // 端点位图const struct usb_function_instance *fi;unsigned int bind_deactivated:1;
};
usb_function_driver
通常使用DECLARE_USB_FUNCTION_INIT
宏定义并初始化。将宏展开后,其定义了usb_function_driver
结构体实例,主要设置alloc_inst
和alloc_func
成员,前置用于创建usb_function_instance
,表示一个Gadget Function实例,后者用于创建usb_function
并初始化。usb_function
中的方法实现了具体的Gadget Function驱动。usb_function_register
和usb_function_unregister
函数完成usb_function_driver
结构体的注册和注销。
[include/linux/usb/composite.h]
#define DECLARE_USB_FUNCTION_INIT(_name, _inst_alloc, _func_alloc) \DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \static int __init _name ## mod_init(void) \{ \return usb_function_register(&_name ## usb_func); \ // 注册UAC设备驱动} \static void __exit _name ## mod_exit(void) \{ \usb_function_unregister(&_name ## usb_func); \ // 注销UAC设备驱动} \module_init(_name ## mod_init); \ // 模块初始化module_exit(_name ## mod_exit) // 模块卸载#define DECLARE_USB_FUNCTION(_name, _inst_alloc, _func_alloc) \// 定义UAC2.0的Gadget Function驱动,名称为uac2_usb_funcstatic struct usb_function_driver _name ## usb_func = { \.name = __stringify(_name), \ // 驱动名称为uac2.mod = THIS_MODULE, \.alloc_inst = _inst_alloc, \.alloc_func = _func_alloc, \}; \MODULE_ALIAS("usbfunc:"__stringify(_name));
3.Gadget Function API
Gadget Funcation API是一个抽象层,上层的Gadget Function驱动使用Gadget Funcation API注册和注销,下层的Composite驱动使用Gadget Funcation API和Gadget Function驱动绑定和匹配。Gadget Function驱动需要实现usb_function_driver
数据结构并向Gadget Funcation API层注册。
Gadget Function API的主要API如下。usb_function_register
将注册的usb_function_driver
挂到func_list
链表中。usb_function_instance
函数会遍历func_list
链表,将参数name
和usb_function_driver
的name
进行对比,若名称一致,则匹配成功,接着调用匹配成功的usb_function_driver
中的alloc_inst
回调函数获取usb_function_instance
,然后将usb_function_driver
的指针设置到usb_function_instance
中,最后返回usb_function_instance
的指针。usb_get_function
函数通过回调alloc_func
函数获取并初始化usb_function
。其他API可参考源代码。
[drivers/usb/gadget/functions.c]
// 向Gadget Function API层注册Gadget Function驱动
int usb_function_register(struct usb_function_driver *newf)
// 注销Gadget Function驱动
void usb_function_unregister(struct usb_function_driver *fd)
// 从Gadget Function API层获取usb_function_instance
struct usb_function_instance *usb_get_function_instance(const char *name)
// 回调free_func_inst销毁usb_function_instance
void usb_put_function_instance(struct usb_function_instance *fi)
// 从Gadget Function API层获取usb_function_instance
struct usb_function *usb_get_function(struct usb_function_instance *fi)
// 回调free_func销毁usb_function
void usb_put_function(struct usb_function *f)
4.Composite层
USB Composite的核心数据结构为usb_composite_driver
。Composite驱动必须实现设备描述符dev
和bind
回调函数。Composite(复合)设备使用usb_composite_dev
数据结构描述,该数据结构在Composite驱动注册的时候内核会在驱动bind
函数调用之前自动创建,不需要驱动创建。gadget
指向dwc3结构体中的usb_gadget
。req
在Composite驱动注册的时候就提前分配好,用于响应主机发送的控制请求。config
指向当前使用的usb配置。desc
是当前设备的描述符,在Composite驱动注册的时候设置。driver
指向对应的usb_composite_driver
。usb_composite_driver
结构体包含了usb_gadget_driver
数据结构,用来表示usb设备驱动。
[include/linux/usb/composite.h]
struct usb_composite_driver {const char *name; // 驱动名称const struct usb_device_descriptor *dev; // 设备描述符,必须定义struct usb_gadget_strings **strings;enum usb_device_speed max_speed; // 设备支持的最大速度unsigned needs_serial:1;// 用于分配整个设备共享的资源,使用usb_add_config添加配置,必须实现int (*bind)(struct usb_composite_dev *cdev); int (*unbind)(struct usb_composite_dev *); // 销毁资源void (*disconnect)(struct usb_composite_dev *); // 可选的驱动disconnect method/* global suspend hooks */void (*suspend)(struct usb_composite_dev *);void (*resume)(struct usb_composite_dev *);// composite驱动层提供了默认的实现,即composite_driver_templatestruct usb_gadget_driver gadget_driver;
};
struct usb_composite_dev { // 复合设备// 只读,usb设备控制器的抽象,指向dwc3结构体中的usb_gadgetstruct usb_gadget *gadget; struct usb_request *req; // 用于响应控制请求,缓冲区提前分配好struct usb_request *os_desc_req; // 用于响应OS描述符,缓冲区提前分配struct usb_configuration *config; // 当前使用配置// qwSignature part of the OS stringu8 qw_sign[OS_STRING_QW_SIGN_LEN]; u8 b_vendor_code; // bMS_VendorCode part of the OS stringstruct usb_configuration *os_desc_config; // OS描述符使用的配置unsigned int use_os_string:1;unsigned int suspended:1;struct usb_device_descriptor desc; // 设备描述符struct list_head configs;struct list_head gstrings;struct usb_composite_driver *driver; // 指向Composite驱动......
};
4.1.legacy
Linux内核中直接使用Composite层的USB gadget legacy驱动大多都在drivers/usb/gadget/legacy/目录下,如USB音频设备驱动文件audio.c,USB虚拟以太网设备驱动文件ether.c,HID设备驱动文件hid.c。legacy驱动可以直接使用内核提供的module_usb_composite_driver
宏,方便定义Composite驱动。参数为usb_composite_driver
结构体。使用usb_composite_probe
注册Composite驱动。使用usb_composite_unregister
函数注销Composite驱动。
[include/linux/usb/composite.h]
#define module_usb_composite_driver(__usb_composite_driver) \module_driver(__usb_composite_driver, usb_composite_probe, \usb_composite_unregister)[include/linux/device.h]
#define module_driver(__driver, __register, __unregister, ...) \
static int __init __driver##_init(void) \ // 初始化函数
{ \return __register(&(__driver) , ##__VA_ARGS__); \
} \
module_init(__driver##_init); \
static void __exit __driver##_exit(void) \ // 注销函数
{ \__unregister(&(__driver) , ##__VA_ARGS__); \
} \
module_exit(__driver##_exit);
usb_composite_probe
和usb_composite_unregister
函数的定义如下。usb_composite_probe
初始化复合设备驱动,usb_composite_unregister
卸载复合设备驱动。
[include/linux/usb/composite.h]
/*** usb_composite_probe() - register a composite driver* @driver: the driver to register** Context: single threaded during gadget setup** This function is used to register drivers using the composite driver* framework. The return value is zero, or a negative errno value.* Those values normally come from the driver's @bind method, which does* all the work of setting up the driver to match the hardware.** On successful return, the gadget is ready to respond to requests from* the host, unless one of its components invokes usb_gadget_disconnect()* while it was binding. That would usually be done in order to wait for* some userspace participation.*/
int usb_composite_probe(struct usb_composite_driver *driver)
/*** usb_composite_unregister() - unregister a composite driver* @driver: the driver to unregister** This function is used to unregister drivers using the composite* driver framework.*/
void usb_composite_unregister(struct usb_composite_driver *driver)
内核在Composite驱动层实现了usb_gadget_driver
,即composite_driver_template
变量,所有复合设备都使用该数据结构,无需驱动实现。Composite驱动使用usb_composite_probe
注册时,内核会将composite_driver_template
中的数据拷贝到usb_composite_driver
的gadget_driver
成员。
[drivers/usb/gadget/composite.c]
static const struct usb_gadget_driver composite_driver_template = { // 内核实现的usb设备驱动.bind = composite_bind,.unbind = composite_unbind,.setup = composite_setup,.reset = composite_disconnect,.disconnect = composite_disconnect,.suspend = composite_suspend,.resume = composite_resume,.driver = {.owner = THIS_MODULE,},
};
4.2.USB Gadget Configfs
Configfs是一种基于ram的文件系统,可以在用户空间直接控制内核对象,主要适用于内核对象有众多配置的模块,比如USB复合设备。Linux 3.11版本引入了USB Gadget Configfs。在用户层可以通过暴漏出来的API定义USB Gadget设备的任意功能和配置,极大的方便了USB复合设备的配置和使用。该部分内容后面将会详细介绍原理和使用方法。USB Gadget Configfs在drivers/usb/gadget/configfs.c文件中实现。
5.UDC驱动
5.1.函数接口
UDC驱动模块定义如下,内核初始化或模块加载时初始化,创建udc_class
,设置uevent
的回调函数为usb_udc_uevent
。
[drivers/usb/gadget/udc/core.c]
static struct class *udc_class;
static int __init usb_udc_init(void)
{udc_class = class_create(THIS_MODULE, "udc");......udc_class->dev_uevent = usb_udc_uevent;return 0;
}
subsys_initcall(usb_udc_init);static void __exit usb_udc_exit(void)
{class_destroy(udc_class);
}
module_exit(usb_udc_exit);
使用usb_add_gadget_udc
注册UDC驱动,首先分配一个usb_udc
数据结构,初始化相关成员,最后将usb_udc
挂到udc_list
链表中,注册成功后UDC的状态为USB_STATE_NOTATTACHED
。使用usb_del_gadget_udc
删除UDC驱动,首先回调pullup
断开连接,然后回调udc_stop
停止USB设备控制器,最后从udc_list
链表中删除usb_udc
。
[drivers/usb/gadget/udc/core.c]
/*** usb_add_gadget_udc - adds a new gadget to the udc class driver list* @parent: the parent device to this udc. Usually the controller* driver's device.* @gadget: the gadget to be added to the list** Returns zero on success, negative errno otherwise.*/
int usb_add_gadget_udc(struct device *parent, struct usb_gadget *gadget)
/*** usb_del_gadget_udc - deletes @udc from udc_list* @gadget: the gadget to be removed.** This, will call usb_gadget_unregister_driver() if* the @udc is still busy.*/
void usb_del_gadget_udc(struct usb_gadget *gadget)
Composite驱动调用usb_gadget_probe_driver
和UDC驱动匹配,首先遍历udc_list
链表,若有usb_udc
的driver
成员为空,则表示匹配成功,接着Composite驱动和UDC驱动绑定,通过将Composite驱动的usb_composite_driver.gadget_driver
的地址设置到usb_udc.driver
成员中完成绑定,最后回调udc_start
启动USB设备控制器。调用usb_gadget_unregister_driver
解除Composite驱动和UDC驱动的绑定关系。
[drivers/usb/gadget/udc/core.c]
int usb_gadget_probe_driver(struct usb_gadget_driver *driver)
int usb_gadget_unregister_driver(struct usb_gadget_driver *driver)
UDC层还向USB devcie function驱动提供了一些的接口,用来开启和关闭USB设备控制器、使能和禁止端点、queues/dequeues I/O请求、分配和释放usb_request
、匹配端点等。这些函数内部会调用具体的USB设备控制器的UDC驱动。RK3399平台上,就会调用dwc3实现的UDC驱动。至于具体内容,后面章节在分析dwc3的UDC驱动时会详细说明。
[drivers/usb/gadget/udc/core.c]
int usb_ep_enable(struct usb_ep *ep) // 使能端点
int usb_ep_disable(struct usb_ep *ep) // 禁止端点
int usb_ep_queue(struct usb_ep *ep, struct usb_request *req, gfp_t gfp_flags) // queues usb_request
int usb_ep_dequeue(struct usb_ep *ep, struct usb_request *req) // dequeue usb_request
struct usb_request *usb_ep_alloc_request(struct usb_ep *ep, gfp_t gfp_flags) // 分配usb_request
void usb_ep_free_request(struct usb_ep *ep, struct usb_request *req) // 释放usb_request
// 根据描述符,匹配要使用的端点
int usb_gadget_ep_match_desc(struct usb_gadget *gadget, struct usb_ep *ep,struct usb_endpoint_descriptor *desc, struct usb_ss_ep_comp_descriptor *ep_comp)
5.2.数据结构
UDC驱动使用usb_udc
数据结构描述,注册的所有usb_udc
数据结构都会挂到udc_list
链表上。UDC驱动的功能主要由成员gadget
实现,即usb_gadget
数据结构。struct usb_gadget_driver
由composite层实现,用于连接USB Function驱动和UDC驱动。usb_gadget_ops
是USB设备控制器的硬件操作函数,包含启动USB设备控制器、停止USB设备控制器、vbus电源等功能。ep0
表示端点0,驱动注册时会提前分配好,用于响应控制请求。除端点0外,USB设备驱动还会使用其他的端点,这些端点数据结构挂到ep_list
链表中。speed
表示USB设备控制器当前的速度。max_speed
表示USB设备控制器最大的速度。
[drivers/usb/gadget/udc/core.c]
static LIST_HEAD(udc_list);
struct usb_udc { // 描述usb设备控制器// 指向Composite驱动中的usb_gadget_driverstruct usb_gadget_driver *driver;// 实现udc驱动的结构体,包含usb设备控制器硬件操作函数struct usb_gadget *gadget; struct device dev;// usb_udc结构体可以组成一个链表struct list_head list;bool vbus; // 对于不关心vbus状态的udc,该值始终为true
};
[include/linux/usb/gadget.h]
struct usb_gadget {// 用于sysfs_notify的工作队列struct work_struct work;struct usb_udc *udc; // 指向usb_udc// usb设备控制器硬件操作函数,不涉及io操作const struct usb_gadget_ops *ops; struct usb_ep *ep0; // 端点0,用于响应控制读写请求struct list_head ep_list; // 该usb设备驱动所需的所有端点链表enum usb_device_speed speed; // 当前连接usb主机的速度enum usb_device_speed max_speed; // udc驱动支持的最大速度enum usb_device_state state; // 当前的状态const char *name; // udc驱动名称,用与确认控制器硬件类型struct device dev;unsigned out_epnum; // 最近使用的输出端点编号unsigned in_epnum; // 最近使用的输入端点编号unsigned mA; // 最近设置的mA值struct usb_otg_caps *otg_caps; // OTG的能力unsigned sg_supported:1; // 是否支持聚合DMAunsigned is_otg:1; // 是否支持OTG,支持OTG必须提供OTG描述符unsigned is_a_peripheral:1; // 一般为false除非支持OTG// 输出端点的请求缓冲区大小按MaxPacketSize对齐unsigned quirk_ep_out_aligned_size:1; unsigned is_selfpowered:1; // 是否是自供电unsigned connected:1; // 是否连接成功unsigned uvc_enabled:1; // uvc功能是否使能......
};
struct usb_gadget_driver {char *function; // 描述usb_gadget_driver的字符串enum usb_device_speed max_speed; // 该驱动可处理的最大速度// 回调函数,可通过该函数绑定上层的gadget function驱动int (*bind)(struct usb_gadget *gadget, struct usb_gadget_driver *driver);void (*unbind)(struct usb_gadget *);// 端点0控制请求调用,用于描述符和配置的管理,通常在中断中调用,不可睡眠int (*setup)(struct usb_gadget *, const struct usb_ctrlrequest *);// 当主机断开时,所有传输停止后调用,可能会在中断中调用,不可睡眠void (*disconnect)(struct usb_gadget *);void (*suspend)(struct usb_gadget *);void (*resume)(struct usb_gadget *);// usb总线复位时调用,必须实现,在中断中调用void (*reset)(struct usb_gadget *);struct device_driver driver;
};
struct usb_gadget_ops { // usb设备控制器硬件操作函数,不涉及端点和ioint (*get_frame)(struct usb_gadget *);int (*wakeup)(struct usb_gadget *);int (*set_selfpowered) (struct usb_gadget *, int is_selfpowered);int (*vbus_session) (struct usb_gadget *, int is_active);int (*vbus_draw) (struct usb_gadget *, unsigned mA);// 下拉让usb主机感知到usb设备接入usb总线,usb主机会枚举usb设备int (*pullup) (struct usb_gadget *, int is_on); int (*ioctl)(struct usb_gadget *, unsigned code, unsigned long param);void (*get_config_params)(struct usb_dcd_config_params *);int (*udc_start)(struct usb_gadget *, struct usb_gadget_driver *); // 启动udcint (*udc_stop)(struct usb_gadget *); // 停止udc// 匹配usb端点struct usb_ep *(*match_ep)(struct usb_gadget *,struct usb_endpoint_descriptor *, struct usb_ss_ep_comp_descriptor *);
};
参考资料
- Linux内核4.4版本源码(RK官方代码)
USB总线-Linux内核USB3.0设备控制器驱动框架分析(四)相关推荐
- USB总线-Linux内核USB3.0设备控制器中断处理程序分析(九)
1.概述 USB设备枚举.请求处理.数据交互都涉及USB设备控制器中断.当有事件发生时,USB设备控制器首先将事件信息通过DMA写入到事件缓冲区中,然后向CPU发出中断,随后CPU调用中断处理函数开始 ...
- USB总线-Linux内核USB3.0设备控制器复合设备之USB gadget configfs分析(七)
1.简介 configfs是基于ram的文件系统,与sysfs的功能有所不同.sysfs是基于文件系统的kernel对象视图,虽然某些属性允许用户读写,但对象是在kernel中创建.注册.销毁,由ke ...
- USB总线-Linux内核USB3.0设备控制器之UDC驱动分析(六)
1.概述 UDC驱动的接口都定义在drivers/usb/gadget/udc/core.c文件中.USB Function驱动通过调用这些接口匹配及访问USB设备控制器,而底层USB控制器驱动要实现 ...
- USB总线-Linux内核USB3.0设备控制器之dwc3 gadget驱动初始化过程分析(五)
1.概述 USB设备控制器(UDC)驱动的框图如下图所示,由三部分组成.第一部分是UDC驱动核心层,在drivers/usb/gadget/udc/core.c文件中实现,该层是一个兼容层,将USB ...
- Linux内核USB总线--设备控制器驱动框架分析
正文 1.概述 如下图所示,USB控制器可以呈现出两种不同的状态.USB控制器作为Host时,称为USB主机控制器,使用USB主机控制器驱动.USB控制器作为Device时,称为USB设备控制器,使用 ...
- USB总线-Linux内核USB设备驱动之UAC2驱动分析(十)
1.概述 UVC(USB Audio Class)定义了使用USB协议播放或采集音频数据的设备应当遵循的规范.目前,UAC协议有UAC1.0和UAC2.0.UAC2.0协议相比UAC1.0协议,提供了 ...
- usb 系统消息_小米USB3.0分线器发布:四口USB 3.0+USB-C
10月18日消息,小米USB3.0分线器已经上架,搭载四口USB3.0,还有一个USB-C备用供电接口,售价49元. USB 3.0实际读写速度可达350MB/s,并向下兼容USB 2.0 / USB ...
- 【驱动】以太网扫盲(三)PHY的控制器驱动框架分析
1. 概述 PHY芯片为OSI的最底层-物理层(Physical Layer),通过MII/GMII/RMII/SGMII/XGMII等多种媒体独立接口(介质无关接口)与数据链路层的MAC芯片相连,并 ...
- Linux内核4.14版本——DMA Engine框架分析(2)_功能介绍及解接口分析(slave client driver)
1 前言 2 Slave-DMA API和Async TX API 3 dma engine的使用步骤 3.1 申请DMA channel 3.2 配置DMA channel的参数 3.3 获取传输 ...
- Linux内核4.14版本——DMA Engine框架分析(6)-实战(测试dma驱动)
1. dw-axi-dmac驱动 2. dma的测试程序 2.1 内核程序 2.2 用户测试程序 1. dw-axi-dmac驱动 dw-axi-dmac驱动4.14版本没有,是从5.4版本移植的,基 ...
最新文章
- 代码跑得慢?分分钟教你如何给代码提速30%!!!
- JavaScript 设计模式基础(二)
- Android socket 编程 实现消息推送
- Judge Route Circle
- UnpooledHeadByteBuf源码分析
- 前端学习(1508):组件和模块的区别
- Eclipse,MyEclipse 安装SVN插件
- android自动化工程师,自动化工程师应具备哪些技能
- linux svn 设置propertise
- 《剑指offer》青蛙跳台阶
- 2022最新开源分销商城小程序源码系统前端+后端+搭建教程
- 如何删除WORD中的空白行
- php实现秒数倒计时,jQuery网页倒计时代码 显示天、小时、分钟与秒数
- 计算机有没有32进制,32进制(32进制转换十进制)
- 对于DCB的认识---GNSS 误差源
- PHP 图片转base64编码 和 base64编码字符串转换成图片保存
- 互联网日报 | 8月9日 星期一 | 字节跳动否认重启上市计划;TikTok全球下载量去年居首;中国代表团38金32银18铜收官...
- 智汀教你如何用手机远程控制智能门锁
- 【MySQL】MySQL 的连接(内、左、右、全)
- 分公司和子公司的法律地位