Linux开发讲课28---USB驱动实例

        USB 是连接计算机系统与外部设备的一种串口总线标准,也是一种输入输出接口的技术规范,被广泛地应用于个人电脑和移动设备等信息通讯产品,USB 就是简写,中文叫通用串行总线。

        我们知道总线是用来通信的,所以USB总线就是一个种通信协议,你的设备支持什么协议就得使用哪一种总线协议与之通信。比如: EEPROM支持IIC协议,那么我们就得使用IIC总线与之通信,而EEPROM设备在制造过程中也必须遵循IIC协议设计。​

        在学习驱动开发实例之前,先了解几个USB总线通信的问题:​

问题1: USB设备那么多,他们怎么分类的?​USB设备按照传输类型分,主要分为4类:

        a、控制传输;

        b、中断传输;

        c、等时传输;

        d、批量传输。​


        其中控制传输时每个USB设备都必须支持的,通常用来获取设备描述符,设置设备的状态等。从USB设备插入到拔出的过程中一定为产生控制传输,不管当前设备是否被主机支持。​
        中断传输的经典代表是USB鼠标和USB键盘,这里说的中断不是真正硬件发出的中断,而是一种轮询机制,​
        USB设备驱动程序里可以设置轮询时间的间隔,也就是主机可以按照这个间隔时间来轮询设备。​
        批量传输的经典代表是U盘,数据可靠,时间不可靠。​
        等时传输的经典代表是摄像头,数据不可靠,时间可靠。​


问题2: 当USB设备插入系统时(USB主机),系统怎么知道这是什么设备?

        当USB设备插入系统之后,根据硬件设计的特性,会被USB主机控制器第一时间知道,然后主机控制器就会问当前插入的设备是什么设备。这里就引入了一个概念叫做描述符。
        描述符有很多种,最基本的有4种:设备描述符、配置描述符、接口描述符、端点描述符。一个∪SB设备必须同时支持这四大描述符,一般这些描述符都存放在USB设备的EEPROM里。设备描述符包含了设备遵循的 USB 的版本号、设备类、设备子类、制造商、产品编号等信息,主机会通过控制传输的方式获取这个设备描述符,通过这个设备描述符就能知道当前是什么设备了。


USB鼠标与键盘驱动编写实例​
        Linux内核默认是支持鼠标驱动的,想要自己重新编写鼠标驱动,需要先将内核自带的鼠标驱动先去除掉。

[root@wbyq linux-3.5]# make menuconfig​

Device Drivers ---> ​

HID support --->​

USB HID support --->​

< > USB HID transport layer //传输层​

Linux内核里自带的鼠标驱动源码: \drivers\hid\usbhid\usbmouse.c​

Linux内核里自带的键盘驱动:源码 \drivers/usb/input/usbkbd.c​

        USB键盘和USB鼠标都属于HID人机交互类,都使用的是中断方式传输数据。代码区别只是匹配的类型不一样而已,其他处理代码通用。

下面是鼠标和键盘的模板:
#include <linux/init.h>​

#include <linux/module.h>​

#include <linux/usb.h>​

#include <linux/usb/input.h>​

#include <linux/hid.h>​

/*​

本程序为USB鼠标驱动程序,要安装本驱动,需要先将内核自带的USB驱动程序卸载掉​

*/​

//定义USB的IDTAB 24ae:2002​

static const struct usb_device_id tiny4412_usb_id[] = {​

{//148f:7601​

USB_DEVICE(0x148f,0x7601),/*360WIFI的制造商ID和产品ID */​

USB_DEVICE(0x1c4f,0x0051),/*当前鼠标的ID 1c4f:0051*/​

},​

};​

//USB鼠标的ID​

static struct usb_device_id usb_mouse_id[] = {​

{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,​

USB_INTERFACE_PROTOCOL_MOUSE) },​

{ }/* 终止进入 */​

};​

//USB键盘的ID​

static struct usb_device_id usb_kbd_id_table [] = {​

{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,​

USB_INTERFACE_PROTOCOL_KEYBOARD) },​

{ }​

};​

int size;​

static unsigned char *buf =NULL;​

static struct urb *myurb=NULL;​

dma_addr_t buf_phy;​

/*USB中断处理程序*/​

static void usb_complete(struct urb *urb)​

{​

int i;​

for(i=0;i<size;i++)​

{​

printk("0x%x ",buf[i]);​

}​

printk("\n");​

/* 重新提交异步请求*/​

usb_submit_urb(myurb, GFP_KERNEL);​

}​

//USB设备信息与驱动端匹配成功的时候调用。​

static int usb_probe(struct usb_interface *intf,const struct usb_device_id *id)​

{​

printk("USB驱动匹配成功! ID: 0x%X,0x%X\n",id->idVendor,id->idProduct);​

/*通过接口获取设备信息*/​

struct usb_device *dev = interface_to_usbdev(intf);​

/*获取当前接口设置*/​

struct usb_host_interface *interface=intf->cur_altsetting;​

/*获取端点描述符*/​

struct usb_endpoint_descriptor *endpoint = &interface->endpoint[0].desc;​

/*中断传输:创建输入管道*/​

int pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);​

/*从端点描述符中获取传输的数据大小 */​

size = endpoint->wMaxPacketSize;​

printk("设备传输数据包大小:%d\n",size);​

/*分配数据传输缓冲区*/​

buf = usb_alloc_coherent(dev,size,GFP_ATOMIC,&buf_phy);​

/*分配新的urb,urb是usb设备驱动中用来描述与usb设备通信所用的基本载体和核心数据结构*/​

myurb = usb_alloc_urb(0,GFP_KERNEL);​

/*中断方式初始化urb*/​

usb_fill_int_urb(myurb,dev,pipe,buf,size,usb_complete,NULL,endpoint->bInterval);​

myurb->transfer_dma = buf_phy;​

myurb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;​

/*为端点提交异步传输请求*/​

usb_submit_urb(myurb, GFP_KERNEL);​

return 0;​

}​

//USB断开的时候调用​

static void usb_disconnect(struct usb_interface *intf)​

{​

struct usb_device *dev = interface_to_usbdev(intf);​

usb_kill_urb(myurb);​

usb_free_urb(myurb);​

usb_free_coherent(dev,size,buf, buf_phy);​

printk("USB 设备释放成功!\n"); ​

}​

//定义USB驱动结构体 ​

static struct usb_driver tiny4412_usb_driver = {​

.name = "tiny4412_usb",​

.id_table = usb_kbd_id_table,​

.probe = usb_probe,​

.disconnect = usb_disconnect​

};​

static int __init tiny4412_usb_init(void)​

{​

//注册USB设备驱动​

usb_register(&tiny4412_usb_driver);​

return 0;​

}​

static void __exit tiny4412_usb_exit(void)​

{​

注销USB设备驱动​

usb_deregister(&tiny4412_usb_driver);​

}​

module_init(tiny4412_usb_init);​

module_exit(tiny4412_usb_exit);​

MODULE_AUTHOR("xiaolong");​

MODULE_LICENSE("GPL");​

1.3 USB电子扫码枪驱动编写实例​
USB电子扫码枪的驱动与USB键盘驱动通用,只是数据包的大小是64字节,匹配的类型也是使用键盘的类型。​

说明: USB电子扫码枪和USB键盘输出的数据都是以掩码的值输出。​

//USB键盘的ID​

static struct usb_device_id usb_kbd_id_table [] = {​

{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,​

USB_INTERFACE_PROTOCOL_KEYBOARD) },​

{ }​

};​

要测试电子扫码枪的驱动,也需要先将内核自带的USB键盘去掉先去掉在测试。​

#include <linux/init.h>​

#include <linux/module.h>​

#include <linux/usb.h>​

#include <linux/usb/input.h>​

#include <linux/hid.h>​

/*​

本程序为USB鼠标驱动程序,要安装本驱动,需要先将内核自带的USB驱动程序卸载掉​

*/​

//定义USB的IDTAB 24ae:2002​

static const struct usb_device_id tiny4412_usb_id[] = {​

{//148f:7601​

USB_DEVICE(0x148f,0x7601),/*360WIFI的制造商ID和产品ID */​

USB_DEVICE(0x1c4f,0x0051),/*鼠标的ID 1c4f:0051*/​

USB_DEVICE(0x0483,0x0011),/*电子扫描枪的ID 1c4f:0051*/​

},​

};​

//USB鼠标的ID​

static struct usb_device_id usb_mouse_id[] = {​

{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,​

USB_INTERFACE_PROTOCOL_MOUSE) },​

{ }/* 终止进入 */​

};​

//USB键盘的ID​

static struct usb_device_id usb_kbd_id_table [] = {​

{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,​

USB_INTERFACE_PROTOCOL_KEYBOARD) },​

{ }​

};​

int size;​

static unsigned char *buf =NULL;​

static struct urb *myurb=NULL;​

dma_addr_t buf_phy;​

/*USB中断处理程序*/​

static void usb_complete(struct urb *urb)​

{​

int i;​

/*​

for(i=0;i<size;i++)​

{​

if(buf[i]!=0)printk("%d,%d\n",buf[i],i);​

}​

printk("\n");​

*/​

//每包数据都是存放在buf[2]里,并且以掩码的形式存放,如果需要得到真实的​

//按键值,需要根据键盘的规则找到对应的码值​

if(buf[2]!=0)printk("0x%x\n",buf[2]);​

/* 重新提交异步请求*/​

usb_submit_urb(myurb, GFP_KERNEL);​

}​

//USB设备信息与驱动端匹配成功的时候调用。​

static int usb_probe(struct usb_interface *intf,const struct usb_device_id *id)​

{​

printk("USB驱动匹配成功! ID: 0x%X,0x%X\n",id->idVendor,id->idProduct);​

/*通过接口获取设备信息*/​

struct usb_device *dev = interface_to_usbdev(intf);​

/*获取当前接口设置*/​

struct usb_host_interface *interface=intf->cur_altsetting;​

/*获取端点描述符*/​

struct usb_endpoint_descriptor *endpoint = &interface->endpoint[0].desc;​

/*中断传输:创建输入管道*/​

int pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);​

/*从端点描述符中获取传输的数据大小 */​

size = endpoint->wMaxPacketSize;​

printk("设备传输数据包大小:%d\n",size);​

/*分配数据传输缓冲区*/​

buf = usb_alloc_coherent(dev,size,GFP_ATOMIC,&buf_phy);​

/*分配新的urb,urb是usb设备驱动中用来描述与usb设备通信所用的基本载体和核心数据结构*/​

myurb = usb_alloc_urb(0,GFP_KERNEL);​

/*中断方式初始化urb*/​

usb_fill_int_urb(myurb,dev,pipe,buf,size,usb_complete,NULL,endpoint->bInterval);​

myurb->transfer_dma = buf_phy;​

myurb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;​

/*为端点提交异步传输请求*/​

usb_submit_urb(myurb, GFP_KERNEL);​

return 0;​

}​

//USB断开的时候调用​

static void usb_disconnect(struct usb_interface *intf)​

{​

struct usb_device *dev = interface_to_usbdev(intf);​

usb_kill_urb(myurb);​

usb_free_urb(myurb);​

usb_free_coherent(dev,size,buf, buf_phy);​

printk("USB 设备释放成功!\n"); ​

}​

//定义USB驱动结构体 ​

static struct usb_driver tiny4412_usb_driver = {​

.name = "tiny4412_usb",​

.id_table =usb_kbd_id_table,//,​

.probe = usb_probe,​

.disconnect = usb_disconnect​

};​

static int __init tiny4412_usb_init(void)​

{​

//注册USB设备驱动​

usb_register(&tiny4412_usb_driver);​

return 0;​

}​

static void __exit tiny4412_usb_exit(void)​

{​

注销USB设备驱动​

usb_deregister(&tiny4412_usb_driver);​

}​

module_init(tiny4412_usb_init);​

module_exit(tiny4412_usb_exit);​

MODULE_AUTHOR("xiaolong");​

MODULE_LICENSE("GPL");​

1.4 USB摄像头编写实例​
要自己编写自己的UVC摄像头驱动,需要先将内核自带的驱动去掉。​

Device Drivers --->
<*> Multimedia support --->​

[*] Video capture adapters --->​

[] V4L USB devices ---> //将*号去掉即可​

示例:​

#include <linux/init.h>​

#include <linux/module.h>​

#include <linux/usb.h>​

#include <linux/usb/input.h>​

#include <linux/hid.h>​

#define UVC_MAX_STATUS_SIZE16​

static unsigned char *buf =NULL;​

static struct usb_device_id uvc_ids[] = {​

{ USB_INTERFACE_INFO(USB_CLASS_VIDEO, 1, 0) },​

{}​

};​

static struct urb *video_urb=NULL;​

/*USB中断处理程序*/​

static void usb_complete(struct urb *urb)​

{​

int len, ret;​

switch (urb->status) {​

case 0:​

break;​

case -ENOENT:/* usb_kill_urb() called. */​

case -ECONNRESET:/* usb_unlink_urb() called. */​

case -ESHUTDOWN:/* The endpoint is being disabled. */​

case -EPROTO:/* Device is disconnected (reported by some​

* host controller). */​

return;​

default:​

printk("Non-zero status (%d) in status completion handler.\n", urb->status);​

return;​

}​

len = urb->actual_length;​

printk("len=%d\n",len);​

//urb->interval = dev->int_ep->desc.bInterval;​

usb_submit_urb(urb, GFP_ATOMIC);​

}​

//USB设备信息与驱动端匹配成功的时候调用。​

static int usb_probe(struct usb_interface *intf,const struct usb_device_id *id)​

{​

struct usb_device *udev = interface_to_usbdev(intf);​

int ret;​

if (id->idVendor && id->idProduct)​

{​

printk("%s,%d,%d\n",udev->devpath, id->idVendor,id->idProduct);​

}​

else​

{​

printk("通用UVC设备:%s\n",udev->devpath);​

}​

/*分配新的urb,urb是usb设备驱动中用来描述与usb设备通信所用的基本载体和核心数据结构*/​

video_urb=usb_alloc_urb(0, GFP_KERNEL);​

/*通过接口获取设备信息*/​

struct usb_device *dev = interface_to_usbdev(intf);​

/*获取当前接口设置*/​

struct usb_host_interface *interface=intf->cur_altsetting;​

/*获取端点描述符*/​

struct usb_endpoint_descriptor *endpoint = &interface->endpoint[0].desc;​

/*中断传输:创建输入管道*/​

int pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);​

buf=kzalloc(UVC_MAX_STATUS_SIZE, GFP_KERNEL);​

/*中断方式初始化urb*/​

usb_fill_int_urb(video_urb,dev,pipe,buf,UVC_MAX_STATUS_SIZE,usb_complete,NULL,endpoint->bInterval);​

/*使能自动暂停*/​

usb_submit_urb(video_urb, GFP_ATOMIC);​

//usb_enable_autosuspend(udev);​

return 0;​

}​

//USB断开的时候调用​

static void usb_disconnect(struct usb_interface *intf)​

{​

struct usb_device *dev = interface_to_usbdev(intf);​

usb_kill_urb(video_urb);​

usb_free_urb(video_urb);​

usb_set_intfdata(intf, NULL);​

kfree(buf);​

printk("USB 设备释放成功!\n"); ​

}​

//定义USB驱动结构体 ​

static struct usb_driver tiny4412_usb_driver = {​

.name = "tiny4412_usb",​

.id_table =uvc_ids,​

.probe = usb_probe,​

.disconnect = usb_disconnect​

};​

static int __init tiny4412_usb_init(void)​

{​

//注册USB设备驱动​

usb_register(&tiny4412_usb_driver);​

return 0;​

}​

static void __exit tiny4412_usb_exit(void)​

{​

注销USB设备驱动​

usb_deregister(&tiny4412_usb_driver);​

}​

module_init(tiny4412_usb_init);​

module_exit(tiny4412_usb_exit);​

MODULE_AUTHOR("xiaolong");​

MODULE_LICENSE("GPL");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/760060.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

QListView自定义item(结合QSqlQueryModel)

QListView:绘制自定义List&#xff08;一&#xff09;——设置ItemDelegate_qt_繁星执着-开放原子开发者工作坊 (csdn.net) QListView自定义Item_qlistview 自定义item-CSDN博客 结合我写的上一篇文章&#xff1a; QTableView与QSqlQueryModel的简单使用-CSDN博客 这次尝试…

webStorm debug vue项目的两种方案

一、前言 本文将介绍通过webstorm对vue项目进行debugger调试的两种方案。 但是&#xff0c;不管通过那种方案&#xff0c;都无法达到类似后端idea调试的体验&#xff0c;感觉十分难受&#xff0c;不过&#xff0c;比起用console.log还是好一些。如果各位有更好的方案&#xf…

扩展阅读:什么是中断

如果用一句话概括操作系统的原理,那就是:整个操作系统就是一个中断驱动的死循环,用最简单的代码解释如下: while(true){doNothing(); } 其他所有事情都是由操作系统提前注册的中断机制和其对应的中断处理函数完成的。我们点击一下鼠标,敲击一下键盘,执行一个程序,…

马斯克的SpaceX发展历史:从濒临破产到全球领先

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 Space Exploration Technologies Corp.&#xff0c;简称SpaceX&#xff0c;是由埃隆马斯克&#xff08;Elon Musk&#xff09;于2002年创办的一…

观察者模式在金融业务中的应用及其框架实现

引言 观察者模式&#xff08;Observer Pattern&#xff09;是一种行为设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;使得多个观察者对象同时监听某一个主题对象。当这个主题对象发生变化时&#xff0c;会通知所有观察者对象&#xff0c;使它们能够自动更新。…

淀山湖之行随笔

我们仰望清新&#xff0c;但又不得不被世俗所伴。 近日上海开始进入梅雨季节&#xff0c;每天大大小小的雨水不断&#xff0c;整个环境也格外的潮湿&#xff0c;不过已经逐渐习惯这种气候&#xff0c;所谓的见怪不怪。 今日是周日&#xff0c;思绪好久&#xff0c;准备去淀山湖…

混合专家模型(MoE)的前世今生

在文章《聊聊最近很火的混合专家模型&#xff08;MoE&#xff09;》中&#xff0c;我们简单介绍了MoE模型的定义和设计&#xff0c;并且比较了MoE和Dense模型的区别&#xff0c;今天我们继续来回顾一下MoE模型发展的历史和最新的发展现状。 从去年GPT-4发布至今&#xff0c;MoE…

Crontab命令详解:轻松驾驭Linux定时任务,提升系统效率

​&#x1f308; 个人主页&#xff1a;danci_ &#x1f525; 系列专栏&#xff1a;《设计模式》《MYSQL》 &#x1f4aa;&#x1f3fb; 制定明确可量化的目标&#xff0c;坚持默默的做事。 引言&#xff1a; crond是Linux系统中用来定期执行命令或指定程序任务的一种服务或软件…

C++ | Leetcode C++题解之第199题二叉树的右视图

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> rightSideView(TreeNode* root) {unordered_map<int, int> rightmostValueAtDepth;int max_depth -1;stack<TreeNode*> nodeStack;stack<int> depthStack;nodeStack.push(ro…

【数据结构】(C语言):二叉搜索树

二叉搜索树&#xff1a; 树不是线性的&#xff0c;是层级结构。基本单位是节点&#xff0c;每个节点最多2个子节点。有序。每个节点&#xff0c;其左子节点都比它小&#xff0c;其右子节点都比它大。每个子树都是一个二叉搜索树。每个节点及其所有子节点形成子树。可以是空树。…

leetCode.98. 验证二叉搜索树

leetCode.98. 验证二叉搜索树 题目描述 代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(n…

鱼叉式钓鱼

鱼叉式网络钓鱼&#xff1a; 鱼叉式网络钓鱼是一种网络钓鱼形式&#xff0c;它针对特定个人或组织发送定制消息&#xff0c;旨在引发特定反应&#xff0c;例如泄露敏感信息或安装恶意软件。这些攻击高度个性化&#xff0c;使用从各种来源收集的信息&#xff0c;例如社交媒体资…

sky18流水线设计

1.最大时钟频率确定 时钟周期要大于等于组合逻辑的delay&#xff08;最大的那条delay&#xff09; Freq_max(Mhz) 1000/T_delay(ns); 数据吞吐率Throughput Freq_max *Toggle_rate;//Toggle_rate&#xff1a;如两个时钟&#xff0c;输入变一次&#xff0c;就是50%&#xff1b…

【考研408计算机组成原理】微程序设计重要考点指令流水线考研真题+考点分析

苏泽 “弃工从研”的路上很孤独&#xff0c;于是我记下了些许笔记相伴&#xff0c;希望能够帮助到大家 目录 微指令的形成方式 微指令的地址形成方式 对应考题 题目&#xff1a;微指令的地址形成方式 - 断定方式 解题思路&#xff1a; 答题&#xff1a; 分析考点&…

大模型系列课程学习-基于2080TI-22G魔改卡搭建双卡大模型训练平台(双系统)

1.选择合适的硬件配置 再配置电脑之前&#xff0c;需要确认自己需要的显存大小、主板、内存条、电源、散热等核心配件。经过前期调研&#xff0c;选择的硬件配置如下&#xff1a; &#xff08;1&#xff09;主板&#xff1a;华南X99_F8D(DDR4主板)&#xff0c;因为需要支持双卡…

1Panel运维利器:功能详解与实操指南

官网地址:https://1panel.cn/ 1Panel简介 1Panel是杭州飞致云信息科技有限公司旗下产品&#xff0c;是一款现代化、开源的Linux服务器运维管理面板&#xff0c;于2023年3月推出。 名称&#xff1a;1Panel开源Linux面板 所属公司&#xff1a;杭州飞致云信息科技有限公司 编写语…

基于HarmonyOS NEXT开发智能提醒助手

目录 目录 目录 前言 关于HarmonyOS NEXT 智能提醒助手需求分析 智能提醒助手设计 1、系统架构 2、功能模块 智能提醒助手的应用场景 智能提醒助手的竞争力 具体技术实现 未来展望 结束语 前言 随着智能设备的普及和物联网技术的飞速发展&#xff0c;人们对于智能…

忙忙碌碌的混沌之中差点扑了个空而错过年中这条线

文章目录 前言初见端倪混沌初始力不从心心力交瘁拾遗补缺总结 前言 突然意识到过完这个周末已经7月份了&#xff0c;他预示着我的2024年已经过半了&#xff0c;过年回家仿佛还是昨天的事情&#xff0c;怎么转眼间已经到了年中了。心里还是不愿承认这件事&#xff0c;翻开自己2…

Nacos配置中心客户端源码分析(一): 客户端如何初始化配置

本文收录于专栏 Nacos 推荐阅读&#xff1a;Nacos 架构 & 原理 文章目录 前言一、NacosConfigBeanDefinitionRegistrar二、NacosPropertySourcePostProcessor三、AbstractNacosPropertySourceBuilder总结「AI生成」 前言 专栏前几篇文章主要讲了Nacos作为服务注册中心相关…

github主页这样优化,让人眼前一亮

我的主页&#xff08;一之十六&#xff09; 1. 创建与账户ID同名的仓库 注意&#xff1a;记得勾选Add a README file 2. markdown语法自定义README.md 3. 辅助工具 优秀profile&#xff1a;https://zzetao.github.io/awesome-github-profile/动态文字&#xff1a;https://r…