引言
我工作时喜欢有点背景音乐,会打开Windows Media Player,按下 PLAY键来播放音乐。但有电话打进来时,我希望能不用鼠标在桌面上到处找Media Player图标来调低音量或按暂停键。另外,我还有一个朋友抱怨在用PC机玩视频游戏时,游戏会占据整个屏幕,因此调整音量非常麻烦。
为解决这一问题,本文提供了一个新方案来实现Windows操作系统下的音量控制。 该方案使用一个连接USB端口的旋钮来控制音量,转动旋钮调节音量,按下旋钮暂停播放,再按一次恢复播放。按下并旋转旋钮可以进行选曲。由于它控制整个Windows的音量,因此也控制所有正在被播放的音乐,包括你的Email告警和游戏音乐,这样即使Windows Media Player不是当前活动窗口也可以被控制。
我把这一设计称为USB HID (人机接口设备) (如图1),不需驱动程序,只需将该HID兼容设备插入Windows系统就可以工作。对Windows系统来说,我的HID控制器就像一个遥控器,可以在 HID 报告中看到它 (当然不止这一个)。目前,这个有趣的控制器在我的键盘旁边有了一个固定位置
低成本 USB
设计需要挑选一套低成本的微控制器 + USB方案。我在网上找到几款集成了USB控制器的微控制器,但都很昂贵,而且需要我不具备的开发工具。看来我只能选择Atmel的AVR微控制器了,因为我已经有了一套Atmel的 JTAGICE-II,一个低成本的全功能 ICE (在线仿真器)。在Atmel AVR 产品线的低端,从Digi-Key得到AtTiny13的单片报价仅为$1.29,而且提供小型的8脚SOIC封装,非常适合这一应用。Atmel 提供了很好的低成本开发工具,其中包括一个叫作AVRStudio的汇编编译器,可被用来编写汇编代码。
AtTiny13不包括 USB控制器,但没问题,Maxim的MAX3420E是一个包括了USB收发器的低成本USB控制器,其与mC的接口仅需几根SPI(串行外设接口)数据线,它将占用AtTiny五个I/O 接口中的4个,但MAX3420E自己还有 I/O,可以补偿被占用的I/O,原理图如图2:
U1 是USB控制器,通过SPI接口配置寄存器。U2连接USB接头上的 5V VBUS 信号,将其降压到3.3V给MAX3420E 和AtTiny13供电。旋钮编码器和LED接MAX3420E的通用输入输出引脚。J2用来给AtTiny调试和装载程序。电阻R4 用来隔离系统复位信号与可编程/装载复位信号。当装载器或ICE使用J2时,上拉电阻R3用来关断MAX3420E 的SPI接口。
为了节省一个引脚,AtTiny13采用半双工模式与MAX3420E 的SPI接口中的一根双向数据线通信(既做 MOSI又作MISO)。电阻R7 用来避免读取MAX3420E寄存器数据时造成冲突。在第8个SCLK 上升沿,MAX3420E SPI接口采样命令字节第8位,随后开始在数据线上输出第1位数据。由于采用bit-bang 的SPI接口无法在数据第8位将SCLK拉高,然后在同一指令内立即放弃对数据线的控制,所以有一段时间 AtTiny13和MAX3420E都在驱动数据线 。电阻R7用来在此期间限制电流,限制到大约3mA。
SPI通信
AtTiny13并没有一个硬件的SPI单元,但幸运的是MAX3420E的SPI接口非常简单,可以任何速度运行。这意味着AtTiny13可通过软件利用bit-bang方式的I/O模拟SPI接口。MAX3420E可支持3线, 4线或5线的SPI接口。不能减少的信号是SCLK (串行时钟), SS# (从器件选择),以及一个双向数据引脚MOSI/MISO (主出从入/主入从出)。我用了AtTiny13的第4个输入引脚连接MAX3420E 的INT 引脚,以节省代码空间,降低SPI总线流量。
关于HID
USB HID的规格书定义了一系列传感器、按钮、灯光等用户希望连接到应用程序的外设。 “Universal Serial Bus HID Usage Tables”(www.usb.org)详细介绍了数百种建议的“应用”。如果你希望建立一个弹球仿真程序的控制器,你可以在 “Game Controls”那一页(在HID中每一类设备都有一“页”)看到挡板的“Usage ID”是0x2A。当你建立自己的HID报告格式时,可以将报告字节中的一位对应该页代码,这样,该弹球仿真程序就可以自动搜寻该HID设备,在器件报告描述符中找到该页代码,如果匹配 ,会将该位对应成一个弹球挡板。
这并不意味着每一个弹球仿真程序会检查所有应用。HID是一个双向协议:HID设备和 Windows应用必须使用HID规范中指定的代码。
HID文档很粗略,是一个典型的复杂规范。你仅需要弄懂1%就可以让你的设备工作,但问题在于这1%在哪里,如何实现HID在规范中没有很好的体现,但确实需要了解清楚(a)你需要规范的那一部分, (b) Windows支持什么。你无法在HID规范中找到(b)的答案,你需要检查Microsoft的文档 。例如,当我试图插入一个USB音量控制器来启动Windows Media Player时,通过查找HID规范,我找到一系列具有“启动应用程序”(AL)功能的应用代码,其中一个应用ID是“A/V Capture/Playback”。我想这个应该可以启动Media Player,但通过发送该代码给Windows居然没有反应,记住:HID定义的只是可能的应用。实际上,在HID规范中列出的所有能启动的应用程序中,Windows XP唯一支持的是启动“邮件阅读”。
HID兼容外设通过发送报告与Windows通信,HID设计的主要任务是用一个报告格式将你的控制内容发给OS。本设计需要实现以下功能,每个都在HID应用列表中有一个入口:
* 音量增加
*
音量降低
* 暂停
* 播放
* 下一曲
* 上一曲
表1是我的HID报告描述符,我用www.usb.org下载的HID_tool程序生成,不用试图去弄明白实际HEX值,让工具来帮你的忙。
当主机发送一个Get_Descriptor-Report 请求时,AtTiny13软件将该数据发给主机,报告描述符定义了该单字节报告的格式,如表2所示。从“Usage(VOL+)”这行开始,字节中始于LSB(USB总是低位在先)的每一位都有描述。控制仅需6位,所以后两位补0 [“Input(Constant)” 行]。
一旦Windows识别出该设备,OS开始周期性的发送 IN请求给 Endpoint 3-IN,期待表2中定义的一个字节的响应。软件监视旋钮的变化,有动作时(如音量增加)将报告字节的相应位置。若没有变化,软件返回一个全0的字节。MAX3420E可以使这样的USB更新非常容易实现。 SPI主机 (AtTiny13)将报告字节写入一个叫做EP3INFIFO(Endpoint 3 IN FIFO)的寄存器,然后向EP3INBC (Endpoint 3-IN byte count)寄存器写入1,意味着下次响应USB IN请求时发送一个字节。发送结束后,MAX3420E产生一个中断请求,表示下一个字节可以被装入EP3INFIFO了。程序如表3所示:
非常简单,但你可以注意到代码包括很多页。剩下的代码用来循环检测旋钮变化,并闪烁LED。其余是一些USB外设所必须的辅助代码。辅助代码管理设备枚举,暂停和恢复,以及USB复位。
优化代码
AtTiny13只有512个字节的程序存储空间。我担心不能满足一个完整的USB应用程序所需,因为程序需要做如下工作:
1. 将设备枚举为HID类。包括解释主机的各种 “Get_Descriptor”请求,找出对应数据,装载Endpoint Zero FIFO,将数据发往主机。
2. 检查随时都有可能发生的USB总线复位。
3. 检查USB总线挂起和恢复。
4. 周期性的读取旋钮位置和按键信息。
5. 当主机通过EP3-IN (用来发送HID控制数据)接收一个IN包后,装载数据进行下一次IN传输。
6. 闪烁 “活动状态”LED。我通过3个途径来压缩代码,首先,我忽略了可选的USB字符串描述符。这些文本字符串用来说明设备,如供应商和设备类型。这些字符串仅能提供一些信息,对于USB外设工作不起任何作用。其次,我并没有使用可选的USB遥控唤醒功能。尽管实现并不困难(主要由MAX3420E来实现),但是很消耗代码空间。最后,我把HID报告描述符放进E2PROM而不是程序存储器 (flash)。AtTiny13有64字节的E2PROM,数据放进E2PROM可以节省程序存储器 (flash)。
加载代码
project zip文件包括两个加载模块, “max3420e_code.hex”和 “max3420e_code.eep”,如果你使用我提供的原理图,可以用6针的 J2连接在线编程器AVRISP2 (Digi-Key ATAVRISP2-ND)加载代码。如果你想修改代码,也可以用J2连接AVR系列的在线仿真器ATJTAGICE2-ND。无论用哪种方式加载,都别忘了要加载 .hex文件 (程序代码)和.eep文件 (E2PROM数据)。
两个汇编细节
编写描述符表数据时,AVR的架构和汇编器有两点需要特别注意。
字对齐
AVR程序存储空间为16位宽,每个. DB (定义字节)中字节数为偶数。为程序易读,最好每行写一个.DB及说明。下面的代码段说明汇编器自动为每个字节补一个字节“0”来实现字对齐:
DX:
0001e6 0012 .DB 0x12
0001e7 0001 .DB 0x01
0001e8 0000 .DB 0x00
0001e9 0002 .DB 0x02
一个方法是在每个.DB中将两个单字节描述符分为一组,如下所示:
DY:
0001e6 0112 .DB 0x12,0x01
0001e7 0200 .DB 0x00,0x02
以上仅适用于16位宽的程序存储器,用来存储HID报告描述符的8位宽E2PROM则不存在这一问题。
指向一个字节
第二点有关如何建立一个指向USB描述符数据的字节指针,AVR有单个的索引寄存器X, Y 和Z,指向代码存储器字节,字节寻址通过将16位闪存地址乘2实现。然后偶字节LSB置0,奇字节置1,如将Z寄存器指向描述符的第一个字节 (地址为DD),则代码如下所示:
ldi
闪烁的蓝色LED
我喜欢用一个闪烁的LED来表明一切正常。我本来希望每秒闪烁一次,但编程过程中改成了用一个蓝色LED缓慢改变亮度,以产生一个心跳脉冲的效果。AtTiny13的定时器/计数器做了大部分工作。我将其设为快速PWM模式,此时 8位定时器从0连续计到0xFF,再回到0,然后我用计数器的比较寄存器OCC0B来回切换 LED状态。通过逐步改变比较门限 ,产生的PWM输出可以连续改变LED亮度。
友好的主机可以节省代码
最大的问题在于如何用512个字节的程序存储空间满足一个全功能USB设备的需求。一开始我就想到代码空间会是一个问题,因此我一直考虑对主机做一些假设来节省代码。这是每个编程人员都应该考虑的哲学问题。秘密在于假设主机越友好,就越能节省代码。
这里有一个例子,在枚举期间,我们回送的接口描述符报告我们有一个接口 (=0),和一个备用选项(=0)。现在假设主机发出一个Set_Interface请求。我们需要检查主机是否仅是在要求我们的接口(0)和备用设置(0)吗? 友好的主机不会设置一个我们在枚举期间没有定义的特性。实际上,如果仅有一个接口和备用设置,主机甚至都不应该发送Set_Interface请求,因为没有其它可选的设置。因此只要我们假设主机是友好的,就可以不必验证Set_Interface请求是否IF=0, AS="0"。对于PC机的主流操作系统,这是一个安全的假设。
我开始的时候通过这种假设来节省代码,但后来通过将一些USB数据写入E2PROM,我又复原了这些检查,而且实际上还有一些剩余的代码空间。这一应用使用了512字节代码空间的90%,你可以自己再添加一些代码。
结论
USB已成为了PC机上替代串口的标准接口,本文说明尽管USB比串口要复杂,但USB 也不一定总需要复杂的代码和昂贵的处理器。 大部分代码都是USB模版文件,可以在不同项目中复用。USB的优势很明显,如电缆供电,自动握手和错误检查,以及与操作系统的接口(类似HID钩子函数)。如果你还想使用现有的微控制器和开发工具,你仍可以使用基于SPI的USB控制器MAX3420E来开发你的USB外设。
用户210741 2008-3-16 21:09