一、USB固件和USB传输方式

USB固件:

USB固件一般不需要我们编写,在此不做程序分析。

USB固件中包含USB设备的出厂信息,如厂商ID、产品ID、主版本号和次版本号等。这就是为什么当我们把U盘插入USB口的时候主机可以知道这是一个U盘设备。

除包含出厂信息外,固件中还包含处理USB协议和设备读写操作的程序,如将数据从设备发送到总线上或从总线中将数据读取到设备中。驱动程序只是将USB规范定义的请求发送给固件程序,固件程序负责将数据写入设备中。USB规范定义了USB设备间的通信方式。

USB结构:

USB是主从结构的。所有的USB传输,都是从USB主机这方发起,USB设备没有“主动”通知USB主机的能力。

如USB鼠标滑动一下产生了数据,但它没有能力通知PC机来读数据,只能被动地等待PC来读。

USB传输类型:

1. 控制传输:可靠,时间有保证。如:USB设备的识别过程

2. 批量传输: 可靠, 时间没有保证。如:U盘

3. 中断传输:可靠,实时。如:USB鼠标

4. 实时传输:不可靠,实时。如:USB摄像头

二、Linux USB设备驱动模型

USB设备驱动模型采用总线设备驱动模型,所以它具有三部分:

1. USB Bus

2. USB Device

3. USB Driver

usb_bus和usb_bus_type:

每一条USB总线对应一个struct usb_bus。

 struct usb_bus {
struct device *controller; /* host/master side hardware */
int busnum; /* Bus number (in order of reg) */
const char *bus_name; /* stable id (PCI slot_name etc) */
u8 uses_dma; /* Does the host controller use DMA? */
u8 uses_pio_for_control; /*
* Does the host controller use PIO
* for control transfers?
*/
u8 otg_port; /* 0, or number of OTG/HNP port */
unsigned is_b_host:; /* true during some HNP roleswitches */
unsigned b_hnp_enable:; /* OTG: did A-Host enable HNP? */
unsigned sg_tablesize; /* 0 or largest number of sg list entries */ int devnum_next; /* Next open device number in
* round-robin allocation */ struct usb_devmap devmap; /* device address allocation map */
struct usb_device *root_hub; /* Root hub */
struct usb_bus *hs_companion; /* Companion EHCI bus, if any */
struct list_head bus_list; /* list of busses */ int bandwidth_allocated; /* on this bus: how much of the time
* reserved for periodic (intr/iso)
* requests is used, on average?
* Units: microseconds/frame.
* Limits: Full/low speed reserve 90%,
* while high speed reserves 80%.
*/
int bandwidth_int_reqs; /* number of Interrupt requests */
int bandwidth_isoc_reqs; /* number of Isoc. requests */ #ifdef CONFIG_USB_DEVICEFS
struct dentry *usbfs_dentry; /* usbfs dentry entry for the bus */
#endif #if defined(CONFIG_USB_MON) || defined(CONFIG_USB_MON_MODULE)
struct mon_bus *mon_bus; /* non-null when associated */
int monitored; /* non-zero when monitored */
#endif
};

usb_bus_type定义了一种usb总线类型。在软件层次中,usb_bus属于usb_bus_type。

usb_bus_type通过bus_register(&usb_bus_type)函数向内核注册usb总线。

struct bus_type usb_bus_type = {
.name = "usb",
.match = usb_device_match,
.uevent = usb_uevent,
.pm = &usb_bus_pm_ops,
};

其中,match()函数通过id_table来匹配device和driver。

 static int usb_device_match(struct device *dev, struct device_driver *drv)
{
/* devices and interfaces are handled separately */
if (is_usb_device(dev)) { /* interface drivers never match devices */
if (!is_usb_device_driver(drv))
return ; /* TODO: Add real matching code */
return ; } else if (is_usb_interface(dev)) {
struct usb_interface *intf;
struct usb_driver *usb_drv;
const struct usb_device_id *id; /* device drivers never match interfaces */
if (is_usb_device_driver(drv))
return ; intf = to_usb_interface(dev);
usb_drv = to_usb_driver(drv); /* 匹配interface和driver->id_table */
id = usb_match_id(intf, usb_drv->id_table);
if (id)
return ; id = usb_match_dynamic_id(intf, usb_drv);
if (id)
return ;
} return ;
}

USB总线的任务有:

1. 识别USB设备

  a. 分配地址并将地址告诉USB设备

  b. 发出命令获取描述符

2. 查找并安装对应的设备驱动程序

3. 提供USB读写接口

我们现在来分析总线框架:

usb_init()
-> bus_register(&usb_bus_type); /* 注册usb总线 */
-> bus_register_notifier(&usb_bus_type, &usb_bus_nb);
-> usb_major_init()
-> register_chrdev(, "usb", &usb_fops);
-> usb_register(&usbfs_driver); /* usbfs驱动 */
-> usb_hub_init(); /* 初始化USB集线器 */
-> usb_register(&hub_driver);
-> kthread_run(hub_thread, NULL, "khubd"); /* 执行线程 */
-> hub_events();
/* 如果有插拔USB设备操作 */
-> hub_port_connect_change(hub, i, portstatus, portchange);
-> usb_alloc_dev(hdev, hdev->bus, port1); /* 分配usb内存 */
-> choose_address(udev); /* 设置地址 */
-> hub_port_init(hub, udev, port1, i); /* 获取设备描述符 */
-> usb_deregister(&hub_driver);
-> usb_register_device_driver(&usb_generic_driver, THIS_MODULE);

usb_device:

每一个USB设备对应一个struct usb_device。

 struct usb_device {
int devnum; /* 设备号 */
char devpath[]; /* 设备路径 */
u32 route;
enum usb_device_state state;
enum usb_device_speed speed; struct usb_tt *tt;
int ttport; unsigned int toggle[]; struct usb_device *parent;
struct usb_bus *bus; /* 设备所属总线 */
struct usb_host_endpoint ep0; /* 设备端点 */ struct device dev; struct usb_device_descriptor descriptor;
struct usb_host_config *config; /* 设备配置 */ struct usb_host_config *actconfig;
struct usb_host_endpoint *ep_in[];
struct usb_host_endpoint *ep_out[]; char **rawdescriptors; unsigned short bus_mA;
u8 portnum;
u8 level; unsigned can_submit:;
unsigned persist_enabled:;
unsigned have_langid:;
unsigned authorized:;
unsigned authenticated:;
unsigned wusb:;
int string_langid; /* static strings from the device */
char *product;
char *manufacturer;
char *serial; struct list_head filelist;
...
int maxchild;
struct usb_device *children[USB_MAXCHILDREN]; u32 quirks;
atomic_t urbnum;
...
struct wusb_dev *wusb_dev;
int slot_id;
};

其注册函数为int usb_new_device(struct usb_device *udev)。

在USB设备的逻辑组织中,包含设备、配置、接口和端点四个层次。在此我使用宋宝华老师书中图片作为解释:

在Linux内核中,这四个层次通过一组标准的描述符来描述,如下所示。

1. 设备描述符:usb_device用于定义USB设备,usb_device_descriptor用于定义产品ID等。

struct usb_device_descriptor {
__u8 bLength; /* 描述符长度 */
__u8 bDescriptorType; /* 描述符类型编号 */ __le16 bcdUSB; /* USB版本号 */
__u8 bDeviceClass; /* USB分配的设备类 */
__u8 bDeviceSubClass; /* USB分配的子类 */
__u8 bDeviceProtocol; /* USB分配的协议 */
__u8 bMaxPacketSize0; /* 端点0的最大包大小 */
__le16 idVendor; /* 厂商编号 */
__le16 idProduct; /* 产品编号 */
__le16 bcdDevice; /* 出厂编号 */
__u8 iManufacturer;
__u8 iProduct;
__u8 iSerialNumber;
__u8 bNumConfigurations;
} __attribute__ ((packed));

当一个USB设备插入时,默认的设备编号为0,在未分配新的编号前,PC使用编号0与它通信。

由于PC可能拥有多个USB设备,因此在PC需要访问某个USB设备时,发出的命令含有此USB设备对应的编号地址。

2. 配置描述符:usb_host_config用于定义USB配置,usb_config_descriptor用于定义此配置下的接口数,供电模式和功率要求等。

struct usb_config_descriptor {
__u8 bLength; /* 描述符长度 */
__u8 bDescriptorType; /* 描述符类型编号 */ __le16 wTotalLength; /* 配置返回的所有数据大小 */
__u8 bNumInterfaces; /* 配置支持的接口数 */
__u8 bConfigurationValue;
__u8 iConfiguration; /* 配置索引值 */
__u8 bmAttributes; /* 供电模式 */
__u8 bMaxPower; /* 最大电流 */
} __attribute__ ((packed));

3. 接口描述符:usb_interface用于定义USB接口,usb_interface_descriptor用于定义此接口下的端点数,协议等。

struct usb_interface_descriptor {
__u8 bLength; /* 描述符长度 */
__u8 bDescriptorType; /* 描述符类型 */ __u8 bInterfaceNumber; /* 接口编号 */
__u8 bAlternateSetting;
__u8 bNumEndpoints; /* 端点数 */
__u8 bInterfaceClass; /* 接口类型 */
__u8 bInterfaceSubClass;
__u8 bInterfaceProtocol;
__u8 iInterface; /* 接口索引值 */
} __attribute__ ((packed));

4. 端点描述符:usb_host_endpoint用于定义USB端点,usb_endpoint_descriptor用于定义此端点下的端点地址,方向和类型等。

struct usb_endpoint_descriptor {
__u8 bLength; /* 描述符长度 */
__u8 bDescriptorType; /* 描述符类型 */ __u8 bEndpointAddress; /* 端点地址 */
__u8 bmAttributes; /* 端点属性 */
__le16 wMaxPacketSize; /* 最大信息包大小 */
__u8 bInterval; /* NOTE: these two are _only_ in audio endpoints. */
/* use USB_DT_ENDPOINT*_SIZE in bLength, not sizeof. */
__u8 bRefresh;
__u8 bSynchAddress;
} __attribute__ ((packed));

我们可以使用“lsusb -v -s 总线号:设备号”查看设备的详细信息

usb_driver:

每一个USB设备驱动对应一个struct usb_driver。

 struct usb_driver {
const char *name; int (*probe) (struct usb_interface *intf,
const struct usb_device_id *id); void (*disconnect) (struct usb_interface *intf); int (*unlocked_ioctl) (struct usb_interface *intf, unsigned int code,
void *buf); int (*suspend) (struct usb_interface *intf, pm_message_t message);
int (*resume) (struct usb_interface *intf);
int (*reset_resume)(struct usb_interface *intf); int (*pre_reset)(struct usb_interface *intf);
int (*post_reset)(struct usb_interface *intf); const struct usb_device_id *id_table; struct usb_dynids dynids;
struct usbdrv_wrap drvwrap;
unsigned int no_dynamic_id:;
unsigned int supports_autosuspend:;
unsigned int soft_unbind:;
};

其注册函数为int usb_register(struct usb_driver *driver)。

 #define usb_register(driver) \
usb_register_driver(driver, THIS_MODULE, KBUILD_MODNAME)

三、USB数据传输

USB设备驱动和USB设备通信所有的结构体为USB请求块(USB Request Block,URB)。

struct urb {
/* private: usb core and host controller only fields in the urb */
struct kref kref; /* reference count of the URB */
void *hcpriv; /* private data for host controller */
atomic_t use_count; /* 用于计数,URB若提交则加1,URB若返回驱动程序减1 */
atomic_t reject; /* submissions will fail */
int unlinked; /* unlink error code */ /* public: documented fields in the urb that can be used by drivers */
struct list_head urb_list; /* list head for use by the urb's
* current owner */
struct list_head anchor_list; /* the URB may be anchored */
struct usb_anchor *anchor;
struct usb_device *dev; /* URB需要发送到的设备 */
struct usb_host_endpoint *ep; /* (internal) pointer to endpoint */
unsigned int pipe; /* (in) pipe information */
unsigned int stream_id; /* (in) stream ID */
int status; /* (return) non-ISO status */
unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*/
void *transfer_buffer; /* 缓冲区,其数据可以从设备发送到主机,也可以从主机发送到设备 */
dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */
struct scatterlist *sg; /* (in) scatter gather buffer list */
int num_sgs; /* (in) number of entries in the sg list */
u32 transfer_buffer_length; /* 表示transfer_buffer或transfer_dma的长度 */
u32 actual_length; /* (return) actual transfer length */
unsigned char *setup_packet; /* (in) setup packet (control only) */
dma_addr_t setup_dma; /* (in) dma addr for setup_packet */
int start_frame; /* (modify) start frame (ISO) */
int number_of_packets; /* (in) number of ISO packets */
int interval; /* (modify) transfer interval
* (INT/ISO) */
int error_count; /* (return) number of ISO errors */
void *context; /* (in) context for completion */
usb_complete_t complete; /* (in) completion routine */
struct usb_iso_packet_descriptor iso_frame_desc[];
/* (in) ISO ONLY */
};

URB的处理流程:

在USB设备的逻辑层次中,interface是逻辑上的设备,它使用endpoint传输urb。

也就是说,USB设备中每个endpoint都处理一个urb队列,在队列清空之前,一个urb的传输过程如下:

当要进行数据传输时,需要分配、设置和提交一个urb给USB核心。核心对urb进行解析,将控制信息提交给主机控制器,由主机控制器完成数据到设备的传输。此时,驱动程序只需等待。当数据回传到主机控制器后,会转发给USB核心,唤醒等待的驱动程序。

具体函数操作如下图:

各函数声明如下:

struct urb *usb_alloc_urb(int iso_packets, gfp_t mem_flags)

inline void usb_fill_int_urb (struct urb *urb,    /* 要初始化的urb指针 */
struct usb_device *dev, /* 所要访问的设备 */
unsigned int pipe, /* 要访问的端点所对应的管道,使用usb_sndintpipe()或usb_rcvintpipe()创建 */
void *transfer_buffer, /* 要传输的数据缓冲区 */
int buffer_length, /* 缓冲区长度 */
usb_complete_t complete_fn, /* 当完成该urb所请求的操作时,要调用的回调函数 */
void *context, /* complet_fn函数所需的上下文,通常取值为dev */
int interval) /*urb被调度的时间间隔 */
inline void usb_fill_bulk_urb(struct urb *urb, struct usb_device *dev, unsigned int pipe, void *transfer_buffer, int buffer_length,
usb_complete_t complete_fn, void *context)
void usb_fill_control_urb(struct urb *urb, struct usb_device *dev, unsigned int pipe, unsigned char *setup_packet, void *transfer_buffer,
int buffer_length, usb_complete_t complete_fn, void *context) int usb_submit_urb(struct urb *urb, gfp_t mem_flags); void usb_kill_urb(struct urb *urb); void usb_free_urb(struct urb *urb);

其中complete_fn()就是传输完成函数

四、USB鼠标设备驱动

在熟悉各个函数后,我们在本节完成USB鼠标设备驱动,步骤如下:

1. 分配设置usb_driver

.id_table

.probe

.disconnect

2. 使用usb_register()注册usb_driver

3. 在probe()函数中完成输入子系统和urb设置,最后提交urb

4. 在传输完成函数函数中判断数据,上报数据,重新提交urb

5. 在disconnect()函数中注销urb和输入子系统

6. 使用usb_deregister()注销usb_driver

在编写驱动时,可参考例子drivers/hid/usbhid/usbmouse.c

USB鼠标驱动源代码:

 #include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/usb/input.h>
#include <linux/hid.h> #include <asm/uaccess.h> static struct usb_device_id mouse_table [] = {
{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,
USB_INTERFACE_PROTOCOL_MOUSE) },
{ } /* Terminating entry */
}; static struct input_dev *usbinput;
static dma_addr_t usb_buf_phys;
static char *buf_mem;
static struct urb *usb_urb;
static int len; static unsigned int cnt; static void usb_mouse_irq(struct urb *purb)
{
#if 0
/* 用于测试哪个bit是用于表示左、中、右键
* 我所使用的鼠标使用bit[0]表示左、中、右键
*/
int i; printk("cnt: %02d, ", cnt++);
for(i = ; i < len; ++i)
{
printk("%02x ", buf_mem[i]);
}
printk("\n");
#else
static unsigned char preval = ; /* 用于存储上一个数据 */
if((preval & ) != (buf_mem[] & )) { /* 鼠标左键数据有变化 */
input_report_key(usbinput, KEY_L, (buf_mem[] & ) ? : );
input_sync(usbinput);
}
if((preval & ( << )) != (buf_mem[] & ( << ))) {
input_report_key(usbinput, KEY_S, (buf_mem[] & ( << )) ? : );
input_sync(usbinput);
}
if((preval & ( << )) != (buf_mem[] & ( << ))) {
input_report_key(usbinput, KEY_ENTER, (buf_mem[] & ( << )) ? : );
input_sync(usbinput);
}
preval = buf_mem[];
#endif usb_submit_urb(usb_urb, GFP_KERNEL); } static int mouse_probe(struct usb_interface *intf, const struct usb_device_id *id)
{
struct usb_device *dev = interface_to_usbdev(intf);
struct usb_host_interface *interface;
struct usb_endpoint_descriptor *endpoint;
unsigned int pipe;
int ret; interface = intf->cur_altsetting;
endpoint = &interface->endpoint[].desc; /* 1. 输入子系统设置 */
usbinput = input_allocate_device(); set_bit(EV_KEY, usbinput->evbit);
set_bit(EV_REP, usbinput->evbit); set_bit(KEY_L, usbinput->keybit);
set_bit(KEY_S, usbinput->keybit);
set_bit(KEY_ENTER, usbinput->keybit); ret = input_register_device(usbinput); /* 2. urb设置 */
usb_urb = usb_alloc_urb(, GFP_KERNEL);
pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress); len = endpoint->wMaxPacketSize;
buf_mem = usb_alloc_coherent(dev, len, GFP_ATOMIC, &usb_buf_phys); usb_fill_int_urb(usb_urb, dev, pipe, buf_mem, len,
usb_mouse_irq, NULL, endpoint->bInterval);
usb_urb->transfer_dma = usb_buf_phys;
usb_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP; return usb_submit_urb(usb_urb, GFP_KERNEL);
} static void mouse_disconnect(struct usb_interface *intf)
{
struct usb_device *dev = interface_to_usbdev(intf); usb_kill_urb(usb_urb);
usb_free_urb(usb_urb); usb_free_coherent(dev, len, buf_mem, usb_buf_phys);
input_unregister_device(usbinput);
input_free_device(usbinput);
} static struct usb_driver mouse_driver = {
.name = "itop4412_mouse",
.probe = mouse_probe,
.disconnect = mouse_disconnect,
.id_table = mouse_table,
}; static int __init usbmouse_init(void)
{
return usb_register(&mouse_driver);
} static void __exit usbmouse_exit(void)
{
usb_deregister(&mouse_driver);
} /* 声明段属性 */
module_init(usbmouse_init);
module_exit(usbmouse_exit); MODULE_LICENSE("GPL");

Makefile:

 KERN_DIR = /work/itop4412/tools/linux-3.5

 all:
make -C $(KERN_DIR) M=`pwd` modules clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order obj-m += usbmouse.o

测试:

在编译并在开发板上insmod后,会出现如下信息:

input: Unspecified device as /devices/virtual/input/input4

插入USB鼠标后,会出现如下信息:

sbcore: registered new interface driver itop4412_mouse

接下来执行:

# ls /dev/event*

最后一个/dev/event2为USB鼠标对应事件

执行:

#hexdump /dev/event2

接下来按键,效果如下图:

在此我以第一行数据为例解释一下各个数字的意义:

0000000 0393 0000 55a4 0008   0001    001f    0001 0000

次数    秒    微秒    按键类  哪个按键    按下

下一章  十七、块设备驱动

十六、USB驱动的更多相关文章

  1. STC8H开发(十六): GPIO驱动XL2400无线模块

    目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...

  2. S3C2416裸机开发系列十六_sd卡驱动实现

    S3C2416裸机开发系列十六 sd卡驱动实现 象棋小子    1048272975 SD卡(Secure Digital Memory Card)具有体积小.容量大.传输数据快.可插拔.安全性好等长 ...

  3. 【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十六:IIC储存模块

    IIC储存器是笔者用来练习精密控时的经典例子.<整合篇>之际,IIC储存器的解释,笔者也自认变态.如今笔者回头望去,笔者也不知道自己当初到底发什么神经,既然将IIC的时序都解释一番.由于开 ...

  4. USB2.0学习笔记连载(十四):USB驱动安装及固件程序的编写

    在之前的博客中已经讲过,驱动程序最核心的两个文件,一个是xxx.sys文件,一个是xxx.inf文件,主机是寻找xxx.inf文件. 在下面的文件中有相关关于USB驱动的说明.对于用户来说,xxx.s ...

  5. STC8H开发(十二): I2C驱动AT24C08,AT24C32系列EEPROM存储

    目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...

  6. STC8H开发(十四): I2C驱动RX8025T高精度实时时钟芯片

    目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...

  7. STC8H开发(十五): GPIO驱动Ci24R1无线模块

    目录 STC8H开发(一): 在Keil5中配置和使用FwLib_STC8封装库(图文详解) STC8H开发(二): 在Linux VSCode中配置和使用FwLib_STC8封装库(图文详解) ST ...

  8. VMware vSphere 服务器虚拟化之十六 桌面虚拟化之VMware Horizon View

       VMware vSphere服务器虚拟化之十六 桌面虚拟化之VMware Horizon View  VMware Horizon View (原VMware View的升级版现在版本5.2)是 ...

  9. Android系统--输入系统(十六)APP跟输入系统建立联系_InputChannel和Connection

    Android系统--输入系统(十六)APP跟输入系统建立联系_InputChannel和Connection 0. 核心:socketpair机制 1. 回顾Dispatch处理过程: 1.1 放入 ...

  10. Linux时间子系统之(十六):clockevent

    专题文档汇总目录 Notes:介绍struct clocke_event_device及其功能feature.模式:触发event接口clockevents_program_event:clockev ...

随机推荐

  1. GWAS 全基因组关联分析 | summary statistic 概括统计 | meta-analysis 综合分析

    有很多概念需要明确区分: 人有23对染色体,其中22对常染色体autosome,另外一对为性染色体sex chromosome,XX为女,XY为男. 染色体区带命名:在标示一特定的带时需要包括4项:① ...

  2. 从Windows命令行启动MySQL

    SERVER: 从Windows命令行启动MySQL 可以从命令行手动启动MySQL服务器.可以在任何版本的Windows中实现. 要想从命令行启动mysqld服务器,你应当启动控制台窗口(或“DOS ...

  3. 如何在 Linux 中更改 swappiness

    交换空间是 RAM 内存已满时使用的硬盘的一部分.交换空间可以是专用交换分区或交换文件.当 Linux 系统耗尽物理内存时,非活动页面将从 RAM 移动到交换空间.Swappiness 是一个 Lin ...

  4. python读写csv文件的方法(还没试,先记录一下)

    该csv模块定义了以下功能: csv.reader(csvfile,dialect ='excel',** fmtparams ) 返回一个reader对象,它将迭代给定csvfile中的行. csv ...

  5. word xml 各个标签含义

    @参考文章 <w:p> <!--表示一个段落--> <w:val > <!--表示一个值--> <w:r> <!--表示一个样式串,指 ...

  6. Spring MVC 数据模型与视图

      从控制器获取数据后,会装载数据到数据模型和视图中,然后将视图名称转发到视图解析器中,通过解析器解析后得到最终视图,最后将数据模型渲染到视图中,展示最终的结果给用户. 用ModelAndView来定 ...

  7. div定位relative和absolute测试1

    div里的position定位也是比较常见的,relative是相对定位,absolute是绝对定位.如本文测试:body自带8px的margin,这里不对其进行清空.蓝色的div和红色的div分别设 ...

  8. Sublime Text 3能用支持的插件推荐

    从二月份用测试版本build 3012开始用sublime text 3,虽然很多插件在sublime text 3不工作了,因为sublime text 3修复了2的一些bug.提升了性能并集成了不 ...

  9. 【Leetcode_easy】976. Largest Perimeter Triangle

    problem 976. Largest Perimeter Triangle solution: class Solution { public: int largestPerimeter(vect ...

  10. ELK之Kibana的可视化监控报警插件sentinl的配置

    参考:https://www.bbsmax.com/A/gGdXbgXmJ4/ https://www.deathearth.com/333.html  https://www.cnblogs.com ...