Linux驱动深入万字解析(一):USB驱动

序章


USB驱动几乎可以说是Linux驱动工程师必会技能之一了,笔者在BOSS直骗上发现许多驱动工程师的岗位都需要掌握USB,PCle,I2C,SPI这些常见协议和LCD驱动等,音视频领域的公司还会要求掌握I2S协议和具有海思等相关芯片与摄像头开发的相关经验。然而现在网上绝大部分的USB驱动教程都讲的比较浅显,不是简单讲一下usbmouse.c看完后等到自己编写驱动时又不知道从哪下手(我就是这样),就是没有深入到底层,讲的人越看问题越多,越多越看不下去。所以,我才决定更新这个系列,我会从USB协议开始编写不同协议与驱动的教程,这些都是绝大部分教程不会去深度讲解的偏进阶性内容。当然笔者也只是一个在校大二学生,很多地方也是在自己尝试摸索,有什么意见或者想法也欢迎大家在评论或私信,讲错的地方也请指出,避免我误人子弟。

本系列选择的开发板是正点原子的I.MX6ULL,资料参考来自韦东山老师的Linux驱动视频大全(穷学生没钱买韦老师的课,先看盗版的,未来一定给韦老师补上)。本系列教程是默认读者已经具有了一定的驱动开发经验,初学者建议看原子哥或者韦东山老师等的基础教程后用OLED,MPU6050等先练习一段时间后阅读。

USB拓扑


在讲拓扑之前,我们先来讲一下USB协议的一些内容,USB从诞生至今主要有四个版本,分别是USB1.0(最古早的版本),USB1.1,USB2.0和USB3.0,当然最近也出了USB3.1。在嵌入式中,我们常用的是USB2.0,本章也是基于USB2.0(USB3.0其实也只是举一反三的事情)。

和I2C,SPI协议一样,USB也是主从结构,主机我们叫做Host,从机分为Hub和Function,其中Hub就是集线器,可以在Hub下面拓展出Function(在生活中最常见的Hub就是USB拓展坞),Function直译过来就是函数,但它的实际意义则是USB设备(比如鼠标,键盘等)。有点抽象,对吗?没有关系,我们举个常见的例子,主机(Host)就像你的轻薄本,设备(Function)就像你精心挑选的鼠标键盘,等到货了才发现笔记本没有那么多接口,怎么办?有钱的人就会去氪一个拓展坞(Hub),这样两个设备就能同时插入了。那么,可不可以不需要拓展坞(Hub)呢?当然也可以,比如我的电脑可以同时插入三个设备(Function),那我就不需要拓展坞(Hub)了。

说了那么久,到底什么是拓扑结构?拓扑结构是一个金字塔形,一共7层,最上层按照协议要求必须是主机,最下面一层必须是Function。

中间层你可以随意选择放入Hub还是放入Function,或者你比较有钱,拓展坞(Hub)上面再插一个拓展坞(Hub)(这段写得我都想买十几个组成一个拓扑结构)。这里又衍生出一个问题,Host是怎么识别出每一个设备的?实际上每一个USB设备都有自己的7位地址,范围是0~127(笔者推测是因为只有6层能接入设备,2的6次方刚好128),当设备接入Host时,E人设备会自我介绍自己的地址是多少,可主机(Hub)是个脸盲,就对设备说:”你别用那个名字(就是设备的地址),我给你个名字(就是从0-127随机选一个)。”就这样,设备失去了自己的名字,但成功让主机记住它了(可怜的设备被主机玩弄于股掌之间)。前面我们说过,主机给设备的名字是从0-127随机选一个,也就是说在USB2.0协议下,主机最多只能记住127个设备(主机不仅脸盲,记忆力也不好)。看到这,有的读者可能开笑了:”0-127一个128个设备,博主讲错了。”实则笔者这波在臭氧层,0地址是一个非常特殊的地址,在设备自我介绍前,主机就已经认识它了,但又不知道名字,就先用0来代替设备了,等设备自我介绍完,主机再从1-127挑一个名字给设备。
在这里插入图片描述

USB协议


接下来我们来了解USB协议,前面我们简单介绍了USB的几种协议,我们在这里补充一些知识。首先是有的读者可能会问的:“USB协议那么多,驱动工程师连写五种协议会不会太多了。”这个问题很简单,USB的高版本协议会兼容低版本协议,比如USB2.0就兼容1.0设备,笔者的鼠标就是1.1,但我编写的是2.0驱动(以后cod马枪了都有借口说鼠标速率不行了)。其次,每次协议更新都会带来速率变化,比如2.0协议就多了一个高速,3.0和3.1也有速率提升,3.1好像已经到5Gb/s的速率了。在2.0协议中,主要有低速(1.5Mb/s),全速(12Mb/s),高速(480Mb/s)。

USB原理

我们来简单了解一下USB的硬件原理(没错,优秀的驱动工程师就是要善学硬件又兼修软件)。我们来思考一下,当我们把设备插入电脑时,电脑很快就能够反应过来有设备插入了,那么电脑是怎么知道的?(不要和我说吧电脑插疼了肯定知道)

我们首先来看一下下面这张USB2.0的原理图,不难发现关键该接口只有四个引脚,其中两个是5V和GND,中间是D+和D-,有的教程叫DP和DM。对于USB2.0来说,要想了解USB硬件原理,还需要分为低速和非低速。我们首先来看非低速(也就是全速和高速)情况。
在这里插入图片描述

在高速和全速情况下,主机端的D+和D-都接有15K下拉电阻,这使得在默认情况下,D+和D-的值为低电平,而对USB设备,则是在D+端接有一个1.5K上拉电阻,使得在设备端D+的值为高电平,当USB设备接入主机时,主机的D+会由低电平变为高电平,主机检测到这个电平变化就会明白:”哦,有个设备接进来了。”

在低速情况下,主机端的D+和D-依然接入15K的下拉电阻使得D+和D-都是低电平,而在设备端,这次我们把1.5K上拉电阻接在D-,这样在主机和设备相连后,D-会由低电平跳到高电平,主机检测D-出现电平变化明白:”哦,又有个设备接进来了。”
在这里插入图片描述

至此,你已经学会了主机端检测USB设备插入的基本原理了,别再说是把主机插疼了才知道设备接入了。要说也可以,别说是看的我的博客。

USB数据传输内容

我们首先来了解一下Host和Device之间的传输。双方之间通过包进行传输,一次完整的传输是由多个包完成的。包的格式是 SOP SYNC PID DATA (CRC) EOP。看到这里,有的读者可能已经放弃思考了:”SOP EOP是什么?PID是什么,PID算法吗?USB也要保持主机平衡吗?”笔者第一次看到这些也是一样的反应,不过不用慌,我们一个一个来看。
· SOP: 可以理解为一个数据包的开头,有了SOP才能告诉对方,我要开始了,你准备好。
· SYNC: 学过Input子系统的应该不陌生,每次上报完事件后都要sync同步一下。没学过的也不用担心,这么细心的博主当然…不会去讲啦。简单来说,sync的作用类似串口的波特率,双方约定一个频率传输。打个比方,就像两人三足比赛,你和队友一定会商量好先迈左脚还是右脚,不然的话整个比赛就是两个人交替拖着对方走,赛道留下拖拽痕迹不说,远处观众还以为是在杂技表演呢。这里我们拓展一下思路:”为什么I2C不需要sync?”懂的朋友可以留言告诉一下无知的…笔者。开个玩笑,I2C有一个很巧妙的地方就是它们用了同一个时钟信号作为参考,这样双方的传输肯定是有规律的。
· PID:这个PID主要是用了KP,KI,KD算法来让数据传输速率保持稳定。开个玩笑,PID其实是指packet identifier,在PID里面我们规定了这个数据包的类型:令牌包,数据包,握手包,特殊包。以Host向Device写入数据为例,我们在PID中选择OUT指令,如果是读取数据的话,PID中就选择IN。所以不难看出,PID其实就是一个指令,规定了数据传输的方向是什么,告诉接收端要干什么。
· DATA:光是看名字相信很多人已经猜出来了,前面的PID告诉了接收方怎么处理数据,DATA就把要处理的数据给接收方。我们还是以Host向Device写入数据为例,DATA里面就是Host想写入的数据;反过来Device向Host写入数据,也就是Host读取数据,DATA里面就是读取到的数据。(这里笔者在啰嗦一句,令牌包里面DATA的前半部分是要通信的USB设备的地址,后半段是与USB设备进行通信的端点,不知道端点的不要急,一步步来后面有讲)
·CRC:校验位,我们还是以串口为例,每次发送或者接收完数据后,我们可以选择奇校验或者偶校验。同样的,USB传输也需要校验。值得一提的是,笔者在翻看USB2.0协议时发现,并非所有类型的数据包都有CRC(怕大家接受不了太多,包的类型待会讲),比如握手包就没有CRC校验。
·EOP:前面我们说过SOP的作用是标志开始传输,S代表start,同理,EOP位于一个包的末尾,代表数据传输的结束,E代表end。

前面一下子讲了这么多东西,不知道大家能不能接受,接受不了的也没有关系,毕竟本教程是面向驱动开发,在实际编写驱动中并不会用到此内容,只是作为拓展希望大家了解的更全面。
接下来我们来填一下之前挖的包的类型的坑,其实与其说是包的类型,确切的说是PID的类型(看完前面应该知道每个包里面都有PID了吧),PID不同的值会对应不同的包,我们把PID分为一下四类(兜兜转转又回到了PID,笔者实在想不到怎么既不显得像抄袭韦老师教案的同时保证质量,再就是我觉得了解PID后再讲会比较好接受)
1.令牌PID(Host发送):标志此包为令牌包。令牌PID主要与数据的传输有关,它分为四类:SOF(用于同步和帧定位),IN(通知主机从特定设备读取数据),OUT(通知主机向特定设备写入数据),SETUP(用于USB控制传输)。
至此如果你觉得已经有点晕了,可以选择暂时放弃此部分,去 看描述符。
2.数据PID:标志此包为数据包,DATA里面会放入对应的数据
3.握手PID:标志此包为握手包,类似I2C的应答,当主机写入或者接收数据的流程中需要在数据传输完成后由接收方发送应答告诉另一方数据接收到了。
4.特殊PID:标志此包为特殊包,报告错误,USB重新连接等情况需要使用。

至此,我们已经掌握了一个包的内容和包的类型,笔者也隔着屏幕都闻到CPU烧掉的香味了(笔者写的时候也反应了一段时间),接下来我们用一个示例来让读者更好的了解,以下是一个Host向Device发送数据的流程图。
在这里插入图片描述
首先我们梳理一下,USB传输数据依赖包进行传输,一个包的结构是SOP SYNC PID DATA CRC EOP。而根据PID的不同类型(令牌,数据,握手,特殊)又把包分为了四种类型,不同类型之间结构可能有小调整(握手包没有CRC),而一次完整的数据传输,是需要多个包来完成的。
第一步,数据的传输需要指出传输方向(就像导航要告诉你朝哪开,不是上XX路一直走,不然你可能想去北京结果一路开到广东,看天安门的朋友圈变成了大海真美)。所以说第一步我们需要一个令牌包(令牌PID可以指出是IN还是OUT),
第二步,指出方向之后,就可以把数据发出来了,也就是需要一个数据包(用数据PID)
第三步,数据发送完成后需要握手包来同步一下数据,省的我在发送时你在睡眠。(就像放假时期宅在房间里的笔者,父母工作时我起床,回家时我睡觉,晚饭就是一家人的同步信号依据)。
至此,一个Host向Device发送数据的流程就基本完成了。这一部分也就正式结束了(写这段之前1K

USB描述符


在日常生活中,我们会发现一个现象,当我的电脑插入USB设备时,电脑会自动识别出USB设备是什么,有什么功能,它是主机还是从机…
在上面的内容中,我们知道电脑在和USB设备连通后,USB设备会发送自身的相关信息去让电脑了解自己,而我们把USB设备发送的这些信息做了分类,并把这些数据叫做描述符。

知到描述符的概念之后,我们来讲一下我们把USB设备的信息分成了哪几类。
1.设备描述符(唯一):表示设备ID,有多少配置,端点0一次最大传输多少数据。
2.配置描述符(一个或多个):有多少接口,供电方式,最大电流。
3.接口描述符(一个或多个):是哪类接口,有几个设置和端点。
4.端点描述符(通常有很多个):端点号,输入/输出,数据传输类型(批量/中断/实时)。
细心的读者可能已经发现了,设备描述符有描述配置,配置描述符有描述接口,接口描述符在描述端点。没错,这四种描述符存在包含关系。具体来讲,设备描述符有且只有一个,它衍生出了配置描述符,而配置描述符衍生出了接口描述符,接口描述符又衍生出了端点描述符,它们之间的关系就好像是太爷爷到孙子,亦或是俄罗斯套娃。同时,每个上层都是由一个到多个下层组成的,比如一个配置由一个或多个接口组成。笔者也简单画了个表格来表示。
在这里插入图片描述
除此之外,还有两个小的知识点,”端点是什么?”和”接口是什么?”首先回答第一个问题,端点是唯一识别USB设备的一部分,这点从端点描述符的内容也不难看出。第二个问题,接口是什么?简单来说,接口就是USB设备的功能,有无数个端点组成。拿耳机来说,一个耳机具有播放和录音功能(这里只是拿一个最简单的耳机),它有两个功能,那就有两个接口,而每一种功能都需要一个驱动程序,也就是说,一个最简单的耳机有两种功能,而这两种功能需要两个驱动程序(从这里也能理解功能越多的耳机为什么越贵了)。

USB数据传输方式

看到这一节的标题不知道有没有读者觉得很奇怪,明明刚讲完数据传输怎么又讲传输了?其实本节的内容需要数据传输和描述符的基础,所以单独拿出来;另一方面是考虑到USB数据传输的内容过多,有的读者一次看一节,结果在数据传输看了两三个小时还没看完就放弃了。
言归正传,上一节我们讲过,端点是唯一可识别USB设备的部分,也就是说主机和设备之间最终是通过端点来传输数据的。如果把主机和端点看作两个端点(数学意义上),那么两个端点(数学意义上)之间就构成了一个线段,数据在线段上进行传输,我们为体现专业性,就把这个线段叫做管道(在驱动程序里称为pipe)。也就是说,主机通过管道与USB设备的某一端点进行数据传输。
之前我们讲过,主机与USB设备通过包进行传输,而包有不同的类型,那么数据传输是否也有不同的方式?(这里区分一下,之前讲的是数据传输内容的几种类型,这个讲的是数据传输方式的几种类型,大家不要混淆了,笔者自己都差点写晕了)我们把USB传输分为四种类型:控制传输,等时传输,中断传输和控制传输。分为四种传输方式是因为USB设备的类型有很多,不同的类型有不同的功能,我们不能总用一种方式传输所有类型,这样不太现实,就好像一个餐馆不可能只会做一个菜,只会做一个菜的餐馆现在一天也可能只能吃的上一个菜了。
·批量传输(U盘):数据可靠,非实时传输。
·中断传输(鼠标键盘):数据可靠,实时传输。
·等时传输(摄像头):数据不一定可靠,实时传输。
·控制传输:不管它怎么样,所有USB设备必须支持这种传输方式(《霸道控制传输爱上主机》?)。

在这里插入图片描述
我们来一个个讲解一下(笔者比较啰嗦),批量传输,有的教程叫块传输,简单来说就是慢工出细活,适合U盘,SD卡这些读取内容很多,但不着急的设备;中断传输并不会真的进入中断,事实上只是定期的去轮询访问,简单来说就是平时爱答不理,用时疯狂追捧,适合鼠标这种没人用时不需要管,用到时就要不断获取的USB设备;等时传输简单来说就是效率高,但正确率有点低的那种,比如说摄像头,有一帧两帧的数据错误你也不会发现什么,98帧和100帧并不会改变你马枪的事实;至于我们的USB霸总控制传输,因为涉及到请求和响应,通常也是突发而没有规律且一般与端点0的通信有关,所以要求所有USB设备必须支持。值得一提的是,在USB2.0的协议手册中允许厂商的自定义命令也是控制传输。
本小节的最后,解答一个好奇宝宝的问题,”对USB设备选择了错误的传输方式会怎么样?”这个问题很简单,数据在主机与端点形成的线段上传输,数据传输的一生如履薄冰,方式错误当然走不到对岸啊!

鼠标协议

本小节是USB驱动理论部分的最后一节,由于本教程的最终目的是教会读者编写一个自己的USB鼠标驱动,所以我们在这一节来简单了解一下鼠标的相关协议。

有读者就要发问了,”鼠标不就是简单的几个按键吗?有什么协议可讲?”这个说法其实只能半对,现在市面上的鼠标主要有两种协议,如果协议错误的话你会发现你的代码能驱动,但数据总是不对。(笔者就是这样,算是usbmouse.c的一个坑)

首先鼠标有两种协议,第一种是Boot Protocol,第二种是Report Protocol。我们接下来从数据存储的角度看一下两种协议的区别。

·Report Protocol:鼠标的数据主要存储在3个字节中,第一个字节存储按键状态,第二个字节存储了X位移,也就是鼠标在X轴方向的移动数据,第三个字节存储了Y位移,即鼠标Y轴的移动数据。可我们会发现,日常生活中的鼠标似乎不只是这么点功能啊?没错,上述协议的内容是规定好的必须遵守的规则,保证了一个鼠标设备的基本功能,而由于不同厂商的鼠标面对不同市场需求需要不同的鼠标功能,所以在这个协议中允许从第四个字节开始为开发者的自定义功能,也就是放开手让你们自由操作。聊完这些,我们再深入一下,不知道读者们有没有想过第1个字节里面到底存储了什么按键的状态?明明鼠标按键很多(笔者的鼠标就有七个按键)?事实上,在第一个字节中我们规定第0位(bit)存储长度为1bit的鼠标左键数据,第1位是长度1bit的鼠标右键数据,第2位不用猜都知道是鼠标中键的数据,长度也是1bit,后面的5bit就比较有意思了,在该协议的手册中并没有规定内容,也就是说这部分还是留给厂商做自定义了。这样看来,鼠标的协议是不是比USB协议要自由简单很多?

·Boot Protocol:其实当你理解了Report Protocol之后,这个协议就没什么好讲的了,最大的区别就是data[1]表示按键状态,其余的没什么了解的必要了(绝不是FW笔者没有查到相关资料,绝不是)。

最后,作为驱动开发人员,我们该怎么知道这个鼠标到底是那个协议呢?难道要和鼠标兄彻夜畅谈理想?当然没那么麻烦,这里笔者给出一个笔者写的十个鼠标驱动的经验:”绝大多数鼠标都是Report Protocol,当你发现选择Report Protocol时data[0]没有变化,再换为data[1],或者使用Set_Protocol()。”

附上本小节知识框架:

在这里插入图片描述

USB设备驱动


经过前面七节知识点的轰炸,不知道各位读者现在的感觉如何?笔者是很高兴,因为马上就能写完和大家分享了!现在,我们已经较透彻的学习了整个USB鼠标驱动的相关内容,前面都是些纸上谈兵,接下来就是实践部分了。再次声明,本次USB驱动的教程是为了最终能够编写一个USB鼠标驱动,参考的源码在内核的/drivers/hid/usbhid/usbmouse.c。

在实际编写之前,我们还是和学习其他驱动一样,先从USB驱动的整体架构看起(不要觉得没用,驱动都是从学习他人架构开始的)。这里笔者将内核示例代码的内容抽了出来,如下图所示。

在这里插入图片描述
其实我们会发现,很多驱动的架构都差不多,这个USB协议的架构和I2C,SPI,Platform几乎如出一辙,都是清一色的注册一个usb_driver,然后完善usb_driver里面的内容,当id_table匹配成功后执行probe,退出时执行disconnect。说完这些一样的部分,我们来说一下不同的地方,首先看open和close函数,在这里我们因为对于开发板来说鼠标是输入设备,所以我们用了Input子系统,把open和close放入input_dev结构体里面的open和close。至于irq不知道大家还记不记得笔者说过鼠标是中断传输,而实际上只是周期性轮询,大家思考一下,轮询的内容是什么?没错,就是这个usb_mouse_irq,我们把上报时间放在irq函数里面进行。接着关于module_usb_driver函数大家可能很陌生,但如果有仔细看过linux内核关于platform驱动的话,对这个就应该有些想法了。笔者把它换个样子,叫module_init和module_exit是不是一下子就熟悉了,没错,这就是驱动的入口和出口函数,只不过在内核中更爱用这种一行代码完成的格式,platform的内核源码中也有类似的操作,这里细心的笔者已经帮大家找好了!
在这里插入图片描述
另外,不难发现MODULE_DEVICE_TABLE这个宏也经常在内核源码中出现,在本章节的作用是表明设备支持热插拔,从而在设备插入时就可以自动加载驱动(插一句,借鉴内核源码在linux驱动中很常见)。

看完要完成的部分,我们现在来具体的讲一下整个驱动的思路:

首先我们完成usb_driver的注册,填写好相应的内容(也就是probe,disconnect,id_table这些)。然后我们完成id_table部分来告诉内核自己的驱动适合什么样的设备匹配成功。

随后,内核根据你的描述找到相应的USB设备后,probe函数执行。在probe函数里,我们按照标准的注册USB设备流程注册USB并完成input子系统搭建。代码中的open和release函数则是存放在input_dev的open和close成员中。之后内核就会按照中断传输的方式定期轮询USB设备实现数据获取。

在我们注销这个驱动时,disconnect函数执行,我们在里面将申请的内存和注册的东西一并释放。至此,整个框架我们就已经讲完了。

具体的代码笔者放在如下链接中了,文件的许多信息笔者写在readme里面了,readme很重要,一定要看!!!同时在test.c文件里面说明了所有你会见到的函数并进行了分析,usb_mouse.c就是我们的驱动程序,注释也都也在上面了,后续笔者会考虑录视频带大家从0开始写USB驱动的代码,也希望大家支持一下,看到这里的各位读者已经很不错了,不过笔者温馨提示:代码开源了,但只是参考,还是需要各位自己尝试的哦。

链接奉上:通过网盘分享的文件:CSDN
链接: https://pan.baidu.com/s/1jBcrHW0Hx6pnUXrhpvaYww?pwd=2222 提取码: 2222
–来自百度网盘超级会员v2的分享

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值