引言
阅读标题后,可能会出现一个逻辑问题:为什么当今有许多便宜的带有硬件模块的控制器,为什么要研究低速USB的软件实现? 事实是,隐藏逻辑级别交换级别的硬件模块将USB协议变成了一种魔术。 为了感觉到这种“魔术”的工作原理,没有什么比从最低的层次开始从头开始复制它更好的了。
为此,我们将尝试使基于ATmega8控制器的设备假装为USB-HID。 与广泛的文献不同,在每一步检查代码是否按预期工作之后,我们将不再从理论到实践,从最低层次到最高层次,从逻辑电压到结论,再到同一发明的“发明”结束。 另外,我注意到我并没有发明该库的替代品,而是一贯地再现其源代码,并尽可能保留原始结构和名称,并解释了为何提供本节或该节。 但是,我通常的代码编写风格与vusb作者的风格不同。 坦率地说,我立即承认,除了利他主义的兴趣(向他人讲一个困难的话题)之外,我也有一种自私的兴趣-自己研究这个话题并为自己争取最大的好处。 随之而来的是,某些重要的观点可能会被遗漏,或者某些主题没有完全公开。
为了更好地理解代码,我尝试用注释突出显示已更改的部分,并将其从前面讨论的部分中删除。 实际上,源代码将是主要的信息源,并且文本将解释执行的操作,执行的原因以及预期的结果。
我还注意到,即使没有提及,也仅考虑了低速USB,从而区别了更多高速品种。
步骤0。熨斗和其他准备
作为测试,让我们以一个基于ATmega8并带有12 MHz石英的自制调试板为例。 我不会给出该方案,它是相当标准的(请参见vusb官方网站),唯一值得一提的是所使用的结论。 在我的情况下,输出D +对应于PD2,输出D- PD3,并且悬挂器悬挂在PD4上。 原则上,可以将上拉电阻连接到电源,但是手动控制似乎与该标准更加一致。
USB连接器提供5 V电源,但是信号线上的电压不能超过3.6 V(为什么这对我来说是个谜)。 因此,您需要降低控制器的功率,或者将齐纳二极管放在信号线上。 我选择了第二个选项,但是总的来说没关系。
由于我们是在“发明”实现,因此很高兴看到控制器的大脑中发生了什么,也就是说,至少需要某种调试信息。 就我而言,这是PD6,PD7上的两个LED,最重要的是PD0,PD1上的UART(在115200上配置),以便您可以通过常规屏幕或其他用于COM端口的程序来收听控制器的颤动:
$ screen /dev/ttyUSB0 115200
另外,带有适当模块的Wireshk最终将成为USB调试的有用工具(它并不总是从包装盒开始的,但是解决此类问题在Internet上已经很成功了,这不是本文的任务)。
在这里,有可能在程序员的描述,makefile和其他内容上花费几千字节的文本,但这几乎没有道理。 同样,我不会关注与USB无关的外围设备设置。 如果有人甚至无法弄清楚这一点,那么进入软件USB的肠道还为时过早吗?
Github上提供了所有步骤的源代码。
步骤1.至少接受一些东西
根据文档,USB支持几种固定速度,其中AVR仅会拉低最低速度:每秒1.5兆位。 它由上拉电阻和后续通信确定。 对于我们选择的频率,电阻器应将D-与3.3 V电源连接,标称值为1.5 kOhm,但实际上可以与+5 V连接,并且标称值可能会略有变化。 控制器频率为12 MHz时,每个位只有8个时钟周期。 显然,只有在汇编程序中才能实现这种准确性和速度,因此我们将打开drvasm.S文件。 这也意味着需要使用中断来捕获字节的开头。 我很高兴通过USB传输的第一个字节始终相同,即SYNC,因此,如果您从头开始,就可以了。 结果,从字节的开始到字节的结尾,仅发生了64个控制器周期(实际上,裕度甚至更小),因此您不应使用其他非USB中断。
立即将配置放入单独的usbconfig.h文件中。 在那里,将设置负责USB的引脚以及所使用的位,常数和寄存器。
理论插入
通过USB传输以每个字节数个包的形式进行。 第一个字节始终是SYNC同步字节,等于0b10000000,第二个字节是PID数据包的字节标识符。 每个字节的传输都是使用NRZI编码从最低有效位到最高有效位(这并不完全正确,但是在vusb中,忽略了其他地方给出的细微差别)。 该方法的事实在于,通过将逻辑电平改变为相反的值来发送逻辑零,并且通过不改变来发送逻辑单元。 此外,还从信号源和接收器的不同步(我们将不使用,但必须将其考虑在内)引入保护:如果在传输序列中连续有六个单元,即终端的状态连续六个周期没有变化,则将强制反转添加到传输中,就像零被发送。 因此,字节大小可以是8位或9位。
还值得一提的是,USB中的数据线是差分的,也就是说,当D +为高电平时,D-为低电平(这称为K状态),反之亦然(J状态)。 这样做是为了更好地抵抗高频干扰。 的确,有一个例外:数据包末尾的信号(称为SE0)通过将两条信号线都拉到地面来传输(D + = D- = 0)。 通过在D +线上保持低电压并在D +线上保持高电压不同的时间,可以传输另外两个信号。 如果时间很小(一个字节的长度或更长),则为空闲,即数据包之间的暂停,如果时间较长,则为复位信号。
因此,传输是在差分对上进行的,没有计算SE0的特殊情况,但我们暂不考虑。 因此,要确定USB总线的状态,我们只需要一条线D +或D-。 总的来说,选择哪一个没有区别,但是为了确定起见,让D-成为。
较长的空闲时间后,可以通过接收SYNC字节来确定数据包的开头。 空闲状态对应于D线上的log.1(它也是J状态),并且SYNC字节为0b100000,但是它从最低有效位传输到最高有效位,而且它以NRZI编码,即,每个零表示信号反相,而一个表示保持相同的水平。 因此状态D-的顺序如下:
在下降沿最容易检测到数据包的开头,我们将在其上配置一个中断。 但是,如果控制器在接收开始时很忙,而无法立即进入中断,该怎么办? 为了避免在这种情况下丢失轨道计数,我们将SYNC字节用于其预期目的。 它完全由位边界处的前沿组成,因此我们可以等待其中一个,再等待另一个半位,然后直接进入下一个的中间。 但是,等待“某个”前沿不是一个好主意,因为我们不仅需要进入中间,而且还需要知道我们进入得分的那一部分。 对于此SYNC也很合适:它的末尾连续有两个零位(它们是K状态)。 在这里,我们将抓住他们。 因此,在drvasm.S文件中,从中断条目到foundK出现了一段代码。 而且,由于检查端口状态,进行无条件转换等的时间很长,我们到达标记的位置不是在位的开头,而是在中间。 但是检查同一位是没有意义的,因为我们已经知道它的含义。 因此,我们等待8个时钟周期(到目前为止为空nop'ami),然后检查下一位。 如果它也为零,那么我们已经发现SYNC的结尾,可以继续接收有效位。
实际上,所有其他代码都旨在读取另外两个字节,并随后输出到UART。 好吧,等待SE0的状态,以免意外进入下一个软件包。
现在,您可以编译生成的代码,并查看我们的设备接受哪些字节。 就个人而言,我有以下顺序:
4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00 4E 55 00 00
记住,我们正在输出原始数据,不包括增量零和NRZI解码。 让我们尝试从低位开始手动解码:
解码零是没有意义的,因为包中不能包含一行中的16个相同值。
因此,尽管到目前为止,我们尚未解码,但我们能够写出可接收数据包前两个字节的固件。
步骤2. NRZI的演示版
为了不进行手动重新编码,您可以将其委托给控制器本身:XOR操作完全可以满足您的需求,尽管结果是反转的,因此请在其后添加另一个反转:
mov temp, shift lsl shift eor temp, shift com temp rcall uart_hex
结果是非常预期的:
2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF 2D 00 FF FF
步骤3.摆脱字节接收周期
让我们再走一步,扩大线性代码中接收第一个字节的周期。 因此,事实证明很多小插曲,只需要等待下一位的开始即可。 您可以使用NRZI解码器来代替其中的一些,其他的将在以后派上用场。
前一个选项的结果相同。
步骤4.读取缓冲区
当然,在单独的寄存器中读取数据既快速又美观,但是当数据过多时,最好使用位于RAM中某个位置的缓冲区条目。 为此,我们将在main中声明一个足够大的数组,并在中断中将其写入。
理论插入
USB中的数据包结构是标准化的,由以下部分组成:SYNC字节,PID + CHECK字节(2个字段,每个字段4位),数据字段(有时11位,但更常见的是8位字节的任意数量)和CRC校验和5个( (用于11位数据字段)或16(其余)位。 最后,数据包指示(EOP)的结尾是两个暂停位,但这不再是数据。
在使用阵列之前,您仍然需要配置寄存器,并在第一个位还不够之前释放nop。 因此,您必须将前两位的读取放入代码的线性部分,在这两个命令之间,我们将插入初始化代码,然后跳到读取周期的中间,到达rxbit2标签。 说到缓冲区大小。 根据文档,在一个数据包中不可能传输超过8个字节的数据。 我们将服务字节PID和CRC16相加,得到的缓冲区大小为11个字节。 SYNC字节和EOP状态将不会被写入。 我们将无法控制来自主机的请求的间隔,但是我们也不想丢失它们,因此我们将为阅读留出双倍的利润。 现在,我们不会使用整个缓冲区,但是为了以后不再返回,最好立即分配所需的卷。
步骤5.人工处理缓冲区
我们编写的代码不是直接读取数组的第一个字节,而是读取与实际写入数组一样多的字节。 并同时在软件包之间添加分隔符。
现在输出看起来像这样:
>03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF >03 2D 00 10 >01 FF
步骤6.添加一个零添加剂
最后,是时候完成将比特流读取到标准位置了。 我们成功管理的最后一个项目是每六个连续单位添加一个伪零。 由于我们已将字节接收部署到循环的线性主体中,因此您必须在所有八个位的每个位之后进行检查。 以前两位为例:
unstuff0: ;1 ( breq) andi x3, ~(1<<0) ;1 [15] 0- . mov x1, x2 ;1 [16] () in x2, USBIN ;1 [17] <-- 1- . ori shift, (1<<0) ;1 [18] 0- .1 rjmp didUnstuff0 ;2 [20] ;<---//---> rxLoop: eor shift, x3 ;1 [0] in x1, USBIN ;1 [1] st y+, shift ;2 [3] ldi x3, 0xFF ;1 [4] nop ;1 [5] eor x2, x1 ;1 [6] bst x2, USBMINUS ;1 [7] 0- shift bld shift, 0 ;1 [8] in x2, USBIN ;1 [9] <-- 1- (, ) andi x2, USBMASK ;1 [10] breq se0 ;1 [11] andi shift, 0xF9 ;1 [12] didUnstuff0: breq unstuff0 ;1 [13] eor x1, x2 ;1 [14]; bst x1, USBMINUS ;1 [15] 1- shift bld shift, 1 ;1 [16] rxbit2: in x1, USBIN ;1 [17] <-- 2- (, ) andi shift, 0xF3 ;1 [18] breq unstuff1 ;1 [19] didUnstuff1:
为了导航方便,所描述命令的地址将由右侧的标签计数。 请注意,引入它们是为了计数控制器的时钟周期,因此它们的顺序不正确。 在rxLoop标签上读取下一个字节,将前一个字节取反并写入缓冲区[0,3]。 接下来,在标签[1]上,读取D-线的状态,根据与先前接受状态的XOR进行解码,我们解码NRZI(我记得普通的XOR加上了它的取反,以确定我们输入了掩码寄存器x3,以0xFF为单位进行初始化)并写入0-移位寄存器[7,8]的第i位。 然后乐趣开始了-我们检查接收到的位是否是第六位不变。 用D-接收到的恒定位对应于在寄存器中写入零(不是1!我们将在最后将其更改为1,即XOR)。 因此,您需要检查位0、7、6、5、4、3是否为零。 其余两位无关紧要,它们保留在前一个字节中,并已进行了较早的检查。 为了摆脱它们,我们用掩码[12]切断了寄存器,其中我们感兴趣的所有位都设置为1:0b11111001 = 0xF9。 如果在应用掩码后所有位均变为零,则添加位的情况已固定,并且存在到unstuff0标签的过渡。 在其他操作之间的间隔中,再读取一个位[17],而不是先前读取的多余位[9]。 我们还交换当前值和先前值x1,x2的寄存器。 事实是,在每个位上读取一个寄存器中的值,然后与另一个寄存器进行异或,然后交换寄存器。 因此,在读取增量寄存器时,也需要执行此操作。 但是最有趣的是,在移位数据寄存器中,我们写的不是诚实的零,而是主机尝试传输的单位[18]。 这是由于以下事实:在接收下一个比特时,还必须考虑零值,并且如果我们记录为零,则掩码检查将无法发现已经考虑了额外的比特。 因此,在移位寄存器中,所有位都被反转(相对于主机发送的位),而零不是。 为防止缓冲区中发生此类混乱,我们将根据XOR而不是0xFF [0],而是使用0xFE来执行反向反转,即,将相应位重置为0的寄存器,因此不会导致反转。 为此,在样本[15]上将零位复位。
比特1-5发生类似的情况。 说,第1位对应于校验1、0、7、6、5、4,而第2、3位被忽略。 这对应于掩码0xF3。
但是6位和7位的处理不同:
didUnstuff5: andi shift, 0x3F ;1 [45] 5-0 breq unstuff5 ;1 [46] ;<---//---> bld shift, 6 ;1 [52] didUnstuff6: cpi shift, 0x02 ;1 [53] 6-1 brlo unstuff6 ;1 [54] ;<---//---> bld shift, 7 ;1 [60] didUnstuff7: cpi shift, 0x04 ;1 [61] 7-2 brsh rxLoop ;3 [63] unstuff7:
第6位的掩码为数字0b01111110(0x7E),但您不能将其叠加在移位寄存器上,因为它将重置第0位,必须将其写入阵列。 另外,在倒数[45]时,已经叠加了一个掩码,重置了7位。 因此,如果位1-6等于零,并且第0个无所谓,则有必要处理额外的位。 也就是说,寄存器的值应为0或1,可以通过比较“小于2”来完美检查[53,54]。
第7位使用相同的原理:不是应用0xFC掩码,而是检查“小于4” [61,63]。
步骤7.对包裹排序
由于我们可以接收到第一个字节(PID)等于0x2D(SETUP)的数据包,因此我们将尝试对接收到的数据包进行排序。 顺便说一句,为什么当它似乎是ACK时,为什么要调用程序包0x2D SETUP? 事实是,从最低有效位到最高有效位的USB传输是在每个字段(而不是字节)内进行的,而我们则逐字节地进行接收。 第一个有效字段PID仅占用4位,随后是另外4个CHECK位,表示PID字段按位倒置。 因此,接收到的第一个字节将不是PID + CHECK,而是CHECK + PID。 但是,由于所有值都是预先知道的,因此差别不大,并且容易在各个位置重新排列半字节。 立即,我们将在usbconfig.h文件中编写可能对我们有用的主要代码。
我们尚未开始添加PID处理代码,请注意它应该是快速的(即在汇编器中),但是由于我们已经接受了数据包,因此不需要按时钟进行对齐。 因此,此部分随后将被转移到asmcommon.inc文件,该文件将包含与频率无关的汇编代码。 同时,只需突出显示评论即可。
现在让我们继续分类接收到的数据包。
理论插入
USB总线上的数据包将合并为事务。 每个事务都从主机发送一个特殊的标记数据包开始,该数据包携带有关主机要对设备执行的操作的信息:配置(SETUP),发送数据(OUT)或接收数据(IN)。 在发送标记分组之后,紧接着是两位的暂停。 随后是数据包(DATA0或DATA1),主机和设备均可发送数据包,具体取决于标记包。 接下来,是另外两个长度为两位的停顿,答案是握手,一个确认包(ACK,NAK,STALL,我们将在另一时间考虑它们)。
由于交换是在同一条线路上进行,因此主机和设备必须不断在发送和接收之间切换。 显然,两位延迟正是为此目的而设计的,以使它们在尝试同时将一些数据传输到总线时不会开始播放推-推。
因此,我们知道了交换所需的所有包装类型。 我们检查接收到的PID字节是否与每个字节一致。 目前,该设备甚至无法将诸如ACK之类的原始数据包写入总线,这意味着它无法告诉主机它是什么。 因此,不能期望像IN这样的命令。 因此,我们将仅检查SETUP和OUT命令的接收情况,为此我们将指示在相应的分支中包含相应的LED。
此外,值得将日志发送到中断之外的main中。
进行这些更改后,我们使用发生的情况刷新设备,并观察以下接收字节序列:
2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00 2D|80|06|00|01|00|00|40|00 C3|80|06|00|01|00|00|40|00
而且-都点亮LED。 因此,我们找到了SETUP和OUT。
步骤8.阅读信封上的地址
理论插入
标记数据包(SETUP,IN,OUT)不仅用于显示设备所需的内容,还用于寻址总线上的特定设备以及内部的特定端点。 需要端点才能在功能上突出显示设备的特定子功能。 它们的轮询频率,汇率和其他参数可能会有所不同。 说,如果该设备似乎是USB-COM适配器,则其主要任务是从总线接收数据并将其传输到端口(第一个端点),然后从端口接收数据并将其发送到总线(第二个)。 在意义上,这些点旨在用于大量非结构化数据。 但是除此之外,设备还必须不时与主机交换控制线的状态(各种RTS,DTR等)并交换设置(速度,奇偶校验)。 在这里,预计不会有大量数据。 另外,当服务信息不与数据混合时,这是方便的。 因此,事实证明,为USB-COM适配器至少使用3个端点很方便。 当然,实际上,它以不同的方式发生...
一个同样有趣的问题是为什么向设备发送地址,因为除了它,您仍然无法将任何东西粘贴到该特定端口中。 这样做是为了简化USB集线器的开发。 它们可能非常“笨拙”,并且可以简单地将信号从主机广播到所有设备,而无需担心排序。 设备本身会解决它,处理数据包或忽略它。
因此,设备地址和端点地址都包含在标记数据包中。 此类软件包的结构如下:
领域
, - ( - PID = SETUP OUT) (IN) , .
, (-) (Handshake) :
- : , , NAK
- -: SETUP OUT, , IN — ,
- . , , ,
« — » . PID', , . «PID» . usbCurrentTok. PID' (DATA0, DATA1) , . , ? : , ( 0 usbCurrentTok ), , . ( SE0) , - , D+, D- . , SYNC, . , , . «» , . .
, . x3, (, , , ).
, USB , , . , , , CRC ( ). , [21]. 0- . , [26]. , CRC, .
9.
, , « », ACK. NAK', ( cnt — ). USB , , SYNC PID. Y, cnt ( ). , — ACK. x3 — 1 , . x3 ( r20) 20.
( SETUP, ), ACK' , , , . , .
, D+, D- ( ), — . XOR , , , , - .
, , , , . , , , . . vusb : txBitloop 2 ([00], [08]). 3 , 6 . , . 1 3 : 171. ( 171, 11 , ), — , . cnt=4:
4 — 171 = -167 = ( ) 89 (+ )
89 — 171 = -82 = ( ) 174 (+ )
174 — 171 = 3. ,
, .
, 3 , 1. 6 , , x4. D+, D- , . .
:
2D|80|06|00|01|00|00|40|00 69|00|10|00|01|00|00|40|00
C3 . , , UART . , , IN , . , .
10. NAK
NAK , . , . , - .
, . , , - , . usbRxBuf, . , — , USB_BUFSIZE. usbInputBufOffset, . .
NAK handleData , [22]. (usbRxLen), - . ( — ), usbRxLen, , — usbRxToken, SETUP OUT - . : , , ACK .
. , , - , -, . ? , , , , - .
,
2D|80|06|00|01|00|00|40|00
, NAK`, , .
11.
, , . — . , , , , , . . . , USB, usbPoll. — , . — . SETUP , PID CRC, SETUP 5- , 16-. 3 «» . «» PID usbRxToken, CRC , , . usbProcessRx, , .
, , — , SE0. , USB .
. SETUP, . . SETUP usbRequest_t 8 . : ( USB-) , - . , . .
, , , .
12. SETUP'
, , . . usbDriverSetup, . , . , ( , , ) . , : ACK NAK, .
13.
, SETUP + DATAx, DATAx 8 . IN DATAx, . , . , ACK NAK. , . — usbTxBuf, , usbTxLen . low-speed USB 8 ( PID, CRC), usbTxLen 11. PID, , . , 16, , 0x0F, . PID , . IN, , (handshake , ).
:
SETUP + DATAx, ACK NAK . , , usbPoll, , ( PID=DATA1 ( DATA0 DATA1 , , DATA1). CRC . , , - . — 4 . , 3 , 4. , SYNC . « IN NAK?» NAK. , , DATA1 .
, — USBRQ_SET_ADDRESS ( , ). . (drvsdm.S, make SE0). , , , DATA1 , , . , , , , , . , , .
14.
, . , USBRQ_GET_DESCRIPTOR USBRQ_SET_ADDRESS, , . usbDriverDescriptor, . , USBRQ_GET_DESCRIPTOR. , , :
USBDESCR_DEVICE — : USB (1.1 ), , , . .
USBDESCR_CONFIG — , , . .
USBDESCR_STRING — , .
, , USBDESCR_DEVICE, , .
15.
. -, . , - - , , HID, , . Vendor ID Product ID, USB, . , vusb .
, , - . , , , (, ) usbMsgPtr, — len, usbMsgLen. ( ) 18 , 8. , , 3 . - , STALL.
usbDeviceRead. , memcpy_P, , , .
, , , . , , .
, , .
PID' DATA0 DATA1 . PID' , , - .
, DATA0 / DATA1 ( ), , , 3 , . XOR PID', . , , XOR' . PID DATA1, XOR PID , XOR DATA0 .
, , USBDESCR_CONFIG.
16. - !
USBDESCR_CONFIG USBDESCR_DEVICE. ( , ) . , - USB-, , D+, D-.
, : , , . , ( , ). , UTF-16, . USB UTF-8 .
vusb , lsusb . VID, PID , . , VID, PID, — .
, , ( ). SETUP: , , . 0, , — . , , , .
.
17. (HID)
HID — human interface device, , , . HID , . , , , , , . «» . HID ( low-speed 800 ), .
HID , USBDESCR_HID_REPORT. vusb, . , usbDriverSetup ( ) usbFunctionSetup ( ). , SETUP, OUT. , , , usbFunctionWrite.
, usbDeviceRead usbFunctionRead, . , , usbFunctionSetup ( , ) USB_FLG_USE_USER_RW, usbDriverSetup .
— — usbFunctionWrite usbFunctionRead. . — , .
usbDriverSetup.
18.
, , . HID, , , ( udev - ). , , . , , , .
UPD: ramzes2 , HIDAPI
.
19. vusb
vusb , .
drvasm.S - usbdrvasm.S asmcommon.inc, -, , usbdrvasm12.inc — usbdrvasm20.inc.
main.c main.c ( ) usbdrv.c ( vusb)
usbconfig.h ( ), , , usbconfig.h.
结论
vusb, , , . , , . . , , , USB-HID. , , , vusb, , , , .
https://www.obdev.at/products/vusb/index.html ( vusb)
http://microsin.net/programming/arm-working-with-usb/usb-in-a-nutshell-part1.html
.. USB:
https://radiohlam.ru/tag/usb/
http://we.easyelectronics.ru/electro-and-pc/usb-dlya-avr-chast-1-vvodnaya.html
http://usb.fober.net/cat/teoriya/
PS - (, ) ,