HIDAche - 硬件/软件集成练习
一个简单的 USB 硬件/软件应用程序,用来折磨他人

引言
本文旨在通过将软件与定制硬件相结合,展示其在创造炫酷应用方面的潜力。我将简要介绍 USB,以及如何通过与自定义设备通信来制作一个烦人的小工具。
在阅读本文之前,请允许我介绍一下我为什么会在一个以软件为中心的网站上发表一篇可能被认为是更偏向硬件的文章的背景。我的学位是电气工程,我的热情一直在硬件方面,但我也很享受让我的硬件项目与计算机互动。Levent Saltuklaroglu 关于如何用并口点亮LED的文章启发了我。我看到,尽管这主要是一个软件编程网站,但人们真的很喜欢硬件方面的一些东西。因此,我的目标是通过提供一个 USB 示例,将 Levent 的想法更进一步。注意:我认为自己是一个相当有能力的程序员,但也准备好接受关于我如何实现 Windows 编程方面的批评。请记住,我的学位不是计算机科学,尽管我非常喜欢编程。我欢迎任何建设性的反馈,但请大家手下留情。
决定写一篇涉及硬件和软件集成的文章是容易的部分。困难的部分是决定硬件是什么以及软件将如何与之交互。大约两年前,我曾想创建一个 USB 恶作剧设备,当设备连接时,它会以预定的时间间隔模拟鼠标移动和键盘敲击。嗯,经过大约两分钟的谷歌搜索,我发现我的想法并非原创,但我还是决定实现它,因为我觉得我可以在现有实现的基础上增加一些东西,并且这将是一个让我熟悉 USB 的好项目。我希望你觉得这些信息有用,并且能从中获得一些乐趣。我决定将该设备命名为 HIDAche。
必备组件
我已尽力使这个项目尽可能简单,但因为它涉及软件和硬件两方面,如果你决定自己重现这个项目,你需要一些东西。如果你只想玩玩项目的软件部分,那么你可以使用 Visual C# Express Edition。如果你对硬件部分感兴趣,那么你还需要以下东西:
- 电路图基础知识
- 电路构建基础知识
- ANSI C 编译器(我使用 Microchip C18 免费学生版)
- 微控制器编程器(我使用 PIC-MCP-USB)
以上列表是为那些有兴趣实际重现该项目的人准备的。如果你只是想看看固件代码,我推荐使用 Visual Studio,但你也可以使用任何文本文件查看器。
USB 基础知识
本文的目标是通过一个相当简单的例子,提供有关我们如何制作一个与计算机通信的 USB 设备,以及我们如何创建利用这种通信的酷炫应用程序的信息。由于本文并非旨在深入探讨 USB,我将只涵盖基础知识。如果你想了解更多,我建议以下资源:
- BeyondLogic - 一个很棒的网站,总结了 USB 的基础知识,让你快速入门。内容有点过时。
- Lakeview Research - 另一个很棒的网站,汇编了一些不错的 USB 资源
- USB Complete - 开发自定义 USB 外设所需的一切
- USB 规范 - (不适合胆小者,共 650 页)
- USB HID 规范 - (同样不是轻松的读物,共 97 页)
传输速率
目前 USB 有三种常见的规格:高速(High Speed,最高 480 Mbits/s),全速(Full Speed,最高 12 Mbits/s)和低速(Low Speed,最高 1.5 Mbits/s)。当然,USB 实施者论坛要求我们不要使用高速、全速和低速这些术语,而是建议“对较慢速的产品(1.5 Mb/s 和 12 Mb/s)使用‘USB’,对高速产品(480 Mb/s)使用‘Hi-Speed USB’”。USB 2.0 支持所有三种速度。USB 3.0 规范最近发布,支持新的“超高速”(SuperSpeed),速率为 5Gbps。然而,在撰写本文时,该规范太新,不予考虑。我将在本项目中使用的 Microchip PIC 18F2455 微控制器支持全速和低速传输。我们将把它设置为使用全速通信,作为兼容 USB 2.0 的设备。
USB 的一个巨大优点是布线非常简单。它不像并行电缆甚至串行电缆那样需要你跟踪一堆电线。USB 电缆包含 4 根线:电源、地线和两根数据线(D+ 和 D-)。USB 规范甚至规定了这些电线的颜色,所以你可以指望它们在不同电缆之间是相同的(请寻找 USB 标志以确保)。
引脚编号 | Color | 函数 |
1 | 红色 | 电源(+5 伏) |
2 | 白色 | D- |
3 | 绿色 | D+ |
4 | 黑色 | 地线 |
幂
USB 设备可以通过几种配置供电。如果项目保证消耗电流小于 100mA,我们可以使用电源线和地线作为项目的电源。这是低功耗总线供电模式。如果我们需要更多电力,我们的设备必须在枚举时向主机请求(最高 500 mA)(高功耗总线供电模式)。如果我们需要更多电力,我们可以选择使设备自供电,这意味着它从主机消耗很少或不消耗电力。为简单起见,我们将设备定义为低功耗总线供电设备,这意味着我们将从计算机获取所需的所有电力,并将其保持在 100mA 以下。
沟通
USB 使用 D- 和 D+ 上的差分数据信号进行通信。差分意味着它不是用开和关的电压状态,甚至不是用正和负的电压状态来定义 1 和 0。差分 1 定义为 D+ 比 D- 高 200mV。差分 0 定义为 D+ 比 D- 低至少 200mV。在这种配置下,任一线路在特定时间的电压是多少并不重要,重要的是两条线路在任何给定时间的电压“差”。这似乎很难实现,但我们的微控制器会为我们处理这些底层细节。
设备驱动程序
USB 硬件开发的主要障碍之一是设备驱动程序。快速浏览一下 Toby Opferman 在本网站上的 6 部分系列文章,你就会明白驱动程序开发有多么复杂和危险(大量的蓝屏)。幸运的是,如果我们设计得当,我们可以使用其他人已经编写好的驱动程序。Windows 有几个预装的驱动程序供操作系统使用,我们可以在此基础上构建我们的应用程序。这就是 USB 设备类的概念发挥作用的地方。USB 类是为执行类似任务的一组设备定义标准实现的一种方式。USB 规范下有自己定义的一些类别的例子是:
- 人机接口设备(HID)
- 音频类
- 通信设备类(CDC)
- 图像类
- 大容量存储类
这些类的定义非常完善,以至于在其中创建的不同设备通常可以共享一个驱动程序。例如,在 Windows XP 中,大多数属于音频类的设备都会使用 usbaudio.sys。HID 设备将使用 hidusb.sys。这将在稍后证明非常有用。
微控制器
硬件可能是像这样的项目中最令人恐惧的部分,但一旦你掌握了要领,实现起来其实并不太难。如今许多硬件项目的核心是微控制器。它基本上是一块芯片上的计算机。在这个项目中,我使用了 Microchip 的 PIC 18F2455,但来自不同制造商的其他微控制器也可以工作。当我说“芯片上的计算机”时,我是认真的。只需几美元,18F2455 就有 23 个可能的 I/O 引脚(非常适合打开 LED 等东西)、一个板载串行端口、全速 USB 收发器,并且每秒可以执行高达 1200 万条指令。它可以存储 24 KB 的程序代码,拥有 2KB 的板载 RAM 和 256 字节的 EEPROM(微控制器的硬盘)。这些数字看起来很小,因为今天的标准是谈论 GB,但对于小型硬件项目来说,这已经足够了。我知道你可能在想:“是啊,听起来不错,但谁想用汇编语言编程呢?” 好消息是你不需要。你可以用 C 语言来做(我知道,这仍然不是你最喜欢的,但比汇编好)。我认为 Levent Saltuklaroglu 的文章之所以如此受欢迎,是因为它让任何人都可以回家,剪开他们的并行电缆,然后开始点亮 LED。我承认,微控制器要复杂一些,但并非不可能。使用它们与在 Visual Studio 中编写 C 程序非常相似。
引导加载程序 (Bootloader)
我可以专门写一整篇文章来讨论硬件引导加载程序。这篇文章已经够长了,所以我只给出一个简短的描述。每次需要重新编程时都把微控制器从面包板上拔下来,这很快就会变得令人厌烦。你永远不会第一次就把固件写对,所以每个项目都会有很多次重新编程。为了节省时间和减少挫败感,我使用了一个引导加载程序。基本上,引导加载程序是位于微控制器程序存储器开头的一段代码。上电时,该代码会检查一个按钮是否被按下。如果被按下,它会进入引导加载模式。如果没有被按下,它会跳转到用户程序块并开始执行。在引导加载模式下,一个 GUI 应用程序可以读取一个编译好的 hex 文件,并通过 USB 连接向微控制器发送编程命令。引导加载程序解释这些命令并重新编程设备,而不需要硬件编程器。这使我们可以使用微控制器编程器一次性地用引导加载程序代码对设备进行编程。之后,我们可以通过 USB 连接和一个简单的 GUI 应用程序,随心所欲地用我们的应用程序代码重新编程设备。
好东西
好了,背景信息就介绍到这里。让我们开始看项目和代码。为了创建 HIDAche,我们首先需要知道从硬件角度需要哪些功能。非常简单。我们需要知道 HID 鼠标和键盘的数据包格式,以便我们可以模仿它们;需要 USB 通信将这些信息发送到计算机;还需要一种方法来存储我们的恶作剧设置。我想实现的一些设备功能如下:
- 能够精确指定按键或鼠标移动之间的时间间隔,精确到分钟
- 可以只移动鼠标、只输入键盘,或两者同时进行
- 用于编程恶作剧设置的 GUI
- 将其安装在 U 盘外壳中,以增加隐蔽性
沟通
为了将我们的信息传递给计算机,我们肯定希望利用 USB 类的原则来简化事情。选择这条路线的另一个原因是为了隐蔽。如果你使用自定义驱动程序,用户会看到“发现新硬件”对话框,并且必须完成一系列步骤。这可不行。对于上面描述的许多类,操作系统很可能已经有驱动程序并会自动安装它们,就像你插入一个新的 USB 键盘一样。由于鼠标和键盘功能属于 HID 领域,我决定走这条路。通过使设备符合 HID 标准,操作系统应该会自动识别它并为其加载驱动程序。幸运的是,Microchip 在其网站上有用于创建鼠标的示例固件。我以此为起点进行固件开发。HID 设备通过发送和接收报告进行通信。这有助于标准化所有的通信。以下是固件代码中报告定义的样子:
//class specific descriptor - HID mouse and keyboard ROM struct{BYTE report[HID_RPT01_SIZE];}hid_rpt01={ {0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x02, /* Usage (Mouse) */ 0xA1, 0x01, /* Collection (Application) */ 0x85, 0x4D, /* REPORT_ID (77) for mouse */ 0x09, 0x01, /* Usage (Pointer) */ 0xA1, 0x00, /* Collection (Physical) */ 0x05, 0x09, /* Usage Page (Buttons) */ 0x19, 0x01, /* Usage Minimum (01) */ 0x29, 0x03, /* Usage Maximum (03) */ 0x15, 0x00, /* Logical Minimum (0) */ 0x25, 0x01, /* Logical Maximum (0) */ 0x95, 0x03, /* Report Count (3) */ 0x75, 0x01, /* Report Size (1) */ 0x81, 0x02, /* Input (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x05, /* Report Size (5) */ 0x81, 0x01, /* Input (Constant) ;5 bit padding */ 0x05, 0x01, /* Usage Page (Generic Desktop) */ 0x09, 0x30, /* Usage (X) */ 0x09, 0x31, /* Usage (Y) */ 0x15, 0x81, /* Logical Minimum (-127) */ 0x25, 0x7F, /* Logical Maximum (127) */ 0x75, 0x08, /* Report Size (8) */ 0x95, 0x02, /* Report Count (2) */ 0x81, 0x06, /* Input (Data, Variable, Relative) */ 0xC0, 0xC0, /* Double End Collection */ 0x09, 0x06, /* Usage (Keyboard) */ 0xA1, 0x01, /* Collection (Application) */ 0x85, 0x4B, /* REPORT_ID (75) for keyboard */ 0x05, 0x07, /* Usage Page (Keyboard) */ 0x19, 0xE0, /* Usage Min (Keyboard LeftControl */ 0x29, 0xE7, /* Usage Max (Keyboard Right GUI) */ 0x15, 0x00, /* Logical Min (0) */ 0x25, 0x01, /* Logical Max (1) */ 0x75, 0x01, /* Report Size (1) */ 0x95, 0x08, /* Report Count (8) */ 0x81, 0x02, /* Input (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x08, /* Report Size (8) */ 0x81, 0x01, /* Input (Constant, Array, Absolute) */ 0x95, 0x05, /* Report Count (5) */ 0x75, 0x01, /* Report Size (1) */ 0x05, 0x08, /* Usage Page (LEDs) */ 0x19, 0x01, /* Usage Min (Num Lock) */ 0x29, 0x05, /* Usage Max */ 0x91, 0x02, /* Output (Data, Variable, Absolute) */ 0x95, 0x01, /* Report Count (1) */ 0x75, 0x03, /* Report Size (3) */ 0x91, 0x01, /* Output (Constant, Array, Absolute) */ 0x95, 0x06, /* Report Count (6) */ 0x75, 0x08, /* Report Size (8) */ 0x15, 0x00, /* Logical Min (0) */ 0x25, 0x65, /* Logical Max (101) */ 0x05, 0x07, /* Usage Page (Keyboard) */ 0x19, 0x00, /* Usage Min (Reserved) */ 0x29, 0x65, /* Usage Max (Keyboard App) */ 0x81, 0x00, /* Input (Data, Array, Absolute) */ 0xC0} /* End Collection */ };/* End Collection
看起来很复杂,但一旦你知道如何阅读它们(创建它们是另一回事),其实也不算太糟。“Report Count”项告诉你字段的数量,“Report Size”是每个字段的位数。所以对于 Usage Page (Buttons) 下的按钮,我们看到有三个 1 位的字段,分别表示按钮状态。这后面跟着一个 5 位的字段,用于将按钮信息填充到一个字节。之后,我们用 "Usage (X)" 和 "Usage (Y)" 定义了 X 和 Y 的字节。它们都是 8 位宽。鼠标定义之后是键盘定义。实际可用的报告很难组合起来,我也不是专家。我提出这一点是想说,这类事情一开始可能看起来令人生畏。对我来说就是这样,但网上到处都有关于这类事情的文档,如果你有一点耐心和决心,你很快就能上手。
在报告中需要注意的一个重要事项是键盘和鼠标功能的报告ID。我们的设备实际上不是鼠标或键盘,而是两者的结合。只要计算机知道我们在每个数据包中发送的是哪种信息,这就没问题。报告中的ID允许我们指定这一点。我为键盘命令选择的报告ID是75,鼠标命令是77。这些是任意数字,只要它们不相同就可以。这样做之后,在枚举时,报告会告诉操作系统:“当我想给你发送一个鼠标数据包时,它的ID将是77,如果我想发送一个键盘数据包,它的ID将是75。”现在,当计算机从我们的设备收到一个ID为77的USB数据包时,它就知道将数据包的内容转换成相应的操作系统鼠标移动或点击。注意:报告ID总是数据包的第一个字节。
报告完成后,我们确切地知道发送到计算机的数据包应该是什么样子。下表显示了根据报告中的定义,鼠标和键盘数据包的样子。
鼠标
位 7 | 位 6 | 位 5 | 位 4 | 位 3 | 位 2 | 位 1 | 位 0 | |
字节 0 | 0x4D(鼠标报告ID为77) | |||||||
字节 1 | Y 溢出 | X 溢出 | Y 符号 | X 符号 | 1 | 中键 | 右键 | 左键 |
字节 1 | X 轴移动 | |||||||
字节 2 | Y 轴移动 |
键盘
值 | ||||||||
字节 0 | 0x4B(键盘报告ID为75) | |||||||
字节 1 | 控制字节 0 | |||||||
字节 2 | 控制字节 1 | |||||||
字节 3 | HID 用法 ID | |||||||
字节 4 | HID 用法 ID | |||||||
字节 5 | HID 用法 ID | |||||||
字节 6 | HID 用法 ID | |||||||
字节 7 | HID 用法 ID | |||||||
字节 8 | HID 用法 ID | |||||||
字节 9 | HID 用法 ID |
键盘数据包中的控制字节用于“按下”像 SHIFT、ALT、CTRL 等控制键。其余标记为 HID 用法 ID 的字节用于发送按键。由于我们符合 HID 标准,我们不能在这里只发送 ASCII 字符。稍后会详细介绍。基本上就是这样。操作系统将解释这些数据包并适当地模拟鼠标移动、点击和按键。那么我们如何将值放入我们的字节数组中以发送到计算机呢?
整合
所以现在我们知道如何与计算机通信,也知道我们想要传达什么。我们只需将它们组合在一起。但首先,当我们插入设备时,它需要知道该做什么。为了实现这一点,我们将恶作剧设置存储在微控制器的 EEPROM 中。这就像 PIC 的硬盘,因为即使断电,数据也会保留。我决定将恶作剧信息按如下方式组织在 256 字节的 EEPROM 中:
// EEPROM MEMORY LAYOUT ******************************************** #define DEVICE_STRUCT_LOC 0x00 #define INTERVAL_LOC 0x0A #define DEV_MODE_LOC 0x0B #define MOUSE_MODE_LOC 0x0C #define KEYS_LENGTH_LOC 0x0D #define PHRASE_START_LOC 0x0E
按键可以占据 0x0E 之后 EEPROM 的其余部分。`DEVICE_STRUCT_LOC` 将包含一个唯一的 ID,以便我们的编程软件知道已连接了 HIDAche 设备。当设备插入时,固件将从 EEPROM 中读取所有这些数据。
// Read in the hidache device info // Read in the prank interval ReadEEPROMData(INTERVAL_LOC, (char*)&prankInterval, 1); // Read in the device mode ReadEEPROMData(DEV_MODE_LOC, (char*)&deviceMode, 1); // Read in the mouse mode ReadEEPROMData(MOUSE_MODE_LOC, (char*)&mouseMode, 1); // Read in the number of keystrokes for keyboard mode ReadEEPROMData(KEYS_LENGTH_LOC, (char*)&phraseLength, 1); // Now read in the phrase just once so we don't have to read it every time the // prank interval hits ReadEEPROMData(PHRASE_START_LOC, phraseArray, phraseLength);
我们需要的另一个功能是能够按指定的时间间隔移动鼠标或发送按键。为了实现鼠标移动或按键之间的延迟,我使用了微控制器的另一个方便功能:内部定时器。你可以在代码中查看详细信息,但基本上我做了一些微调,让定时器每秒触发一次中断。我使用这个间隔来执行你在编程设备时指定的分钟间隔的正确操作。
USB 通信有非常严格的时间要求。为了满足这些通信需求,我们的固件代码不能长时间不处理 USB 任务。这是通过调用 `USBDeviceTasks()` 来实现的。这个函数会做一些事情,比如检查总线活动和某些控制数据包。由于这个函数需要非常频繁地被调用,整个固件在一个无限 while 循环中运行。在这个 while 循环中,我们处理 USB,然后通过调用 `ProcessIO()` 来处理我们的恶作剧任务。在 `ProcessIO()` 中,我们检查定时器中断是否被触发,以及恶作剧间隔是否已到期。如果是,我们就调用 `PerformEmulation()`,在那里我们根据我们的设置决定要模拟什么。
switch(deviceMode)
{ case MODE_MOUSE:
// For mouse only mode we can go ahead and set the keyboard
// finished variable up front.
IsKeyboardFinished = TRUE;
// Load the next value in the buffer
FillMouseBuffer();
break;
case MODE_KEYBOARD:
// For keyboard only mode we can go ahead and set the mouse
// finished variable up front;
IsMouseFinished = TRUE;
// Load the next value in the buffer
FillKeyboardBuffer();
break;
case MODE_BOTH:
// Switch back and forth between the two modes until they both
// complete
if(sendType == KEYSTROKE)
FillKeyboardBuffer();
else
FillMouseBuffer();
break;
}
// Send the buffer to the computer
TransmitBuffer();
为了不让文章比现在更长,我将引导您查看代码,了解填充方法中发生了什么,而不是将代码放在这里。对于鼠标模式,我基本上填充了数据包的移动部分来模拟特定的移动设置。键盘模式要复杂一些,但也不是太多。正如我之前所说,我们不能只向计算机发送 ASCII 字符,因为设备枚举为 HID USB 设备。我们必须发送特定按键的 HID 用法代码。你可以从 USB 规范中获得这些代码。我将按键作为 ASCII 字符存储在 EEPROM 中,然后在固件中动态转换它们。
BOOL TranslateAsciiToHID(char asciiKey, char *hidKey, BOOL *needShiftKey)
{
short i = 0;
// Lets initialize this to false and then just set it if we need it
*needShiftKey = FALSE;
// Lowercase ASCII characters
if((asciiKey >= 97) && (asciiKey <= 122))
{
*hidKey = asciiKey - 93;
return TRUE;
}
// Uppercase ASCII characters
else if((asciiKey >= 65) && (asciiKey <= 90))
{
*needShiftKey = TRUE;
*hidKey = asciiKey - 61;
return TRUE;
}
// Numbers
else if((asciiKey >= 49) && (asciiKey <= 57))
{
*hidKey = asciiKey - 19;
return TRUE;
}
// Spacebar. I decided not to include this in the table because it could be
// a regular occurance and looking for it in the table will take more time
else if(asciiKey == 32)
{
*hidKey = 44;
return TRUE;
}
// Custom Sequences
else if(asciiKey > 200)
{
switch(asciiKey)
{
case 201:
*hidKey = 42; //Delete
break;
case 202:
*hidKey = 0;
*needShiftKey = TRUE;
break;
case 203:
*hidKey = 102; // Keyboard Power Button if supported
break;
default:
*hidKey = 0;
}
return TRUE;
}
// Look for the character in the key table defined above
else
{
// Look through the table to see if we can find it there
for(i = 0; i < TABLE_LEN; i++)
{
if(KeyTable[i][0] == asciiKey) // We've found the entry
{
*hidKey = KeyTable[i][1];
*needShiftKey = KeyTable[i][2];
return TRUE;
}
}
// Send a z to indicate an error
*hidKey = 29;
return FALSE;
}
}
`KeyTable` 定义在文件 `TranslateAsciiToHID.c` 中,用于辅助翻译。一旦我们的传输缓冲区被填充,我们就可以通过调用 `TransmitBuffer()` 将信息发送到计算机了。那时,我们将看到鼠标移动或键盘敲击。如果你查看固件,你会发现有很多文件和代码来支持 USB 协议。那些代码是由 Microchip 编写的,虽然了解 USB 任务处理中发生的事情很好,但这并不是必需的。对我们来说,他们提供了一些方法供我们调用以传输数据,而这正是我们真正需要知道的。我们现在可以根据我们编程到设备中的设置来移动鼠标或发送按键。如果你感到不知所措,这是可以理解的。一开始要吸收的东西很多,但在你看了一会儿代码之后,其实很简单。只要记住,我并没有写很多代码。所有的 USB 代码都是由 Microchip 提供的(没有必要重新发明轮子)。我已尽力为我修改的部分代码添加了大量注释,并解释了发生了什么。如果你有问题,请随时发布或给我发邮件。我的大部分代码都在 `hidache.c`、`TranslateAsciiToHID.c`、`HardwareProfile.h` 和 `usb_descriptors.c` 中。
硬件编程部分到此基本结束。我已经尝试对我从 Microchip 原始鼠标示例中更改的代码进行智能注释,以便于理解。从硬件角度来看,唯一剩下的就是电路图了。如你所见,它非常简单。
PIC 可以从其 EEPROM 中读取设置,并根据这些设置执行恶作剧操作。现在我们需要一种便捷的方式,让狡猾的恶作剧者将他们想要的恶作剧设置加载到设备上。这就是软件发挥作用的地方。
GUI HIDAche 编程器
当我开始着手这个项目的 Windows 方面的工作时,我刚读了 Josh Smith 的文章 在 Windows Forms 和 WPF 中创建同一个程序,这引导我采用了数据绑定的方式。我意识到这个实现并不完美,如果你有建设性的反馈,我很乐意听到。GUI 编程器的想法非常简单:提供一种将恶作剧设置写入 HIDAche 设备的方法。由于我在恶作剧设备上使用了引导加载程序,一个简单的解决方案就出现了。我知道引导加载程序能够读写 PIC 上的 EEPROM,而恶作剧设置正好存储在那里。既然我有引导加载程序的源代码,我也知道访问 EEPROM 所需的命令数据包应该是什么样子。所以简单的解决方案是让设备进入引导加载模式,然后像引导加载程序 GUI 程序一样读取和写入这些信息。大部分代码都非常直接,所以我只关注几个关键点。让我们看看它是如何工作的。
设备连接
如果你还记得上面的内容,要进入引导加载模式,我们只需按住设备按钮并插入它。所以,如果 GUI 编程器是打开的,并且我们插入了我们的设备,有几件事需要发生。首先,我们需要能够检测到 USB 设备的连接;其次,我们需要能够确定该设备是否是我们的恶作剧设备。嗯,这个网站上有很多关于如何检测 USB 设备连接的文章,所以我不会在这里过多地讨论。基本上,我们所做的就是在我们的窗体中重写 WndProc 方法,并通过调用位于 USBComm 库中的 `RegisterForHIDDeviceNotifications()` 来注册 USB 设备通知。那个被重写的方法看起来是这样的:
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case WM_DEVICECHANGE:
// The WParam value identifies what is occurring.
// n = (int)m.WParam;
if ((HIDAcheControl)this.flowLayoutPanel1.Controls["hidCtrl"] != null)
{
// Update the connection status graphic and ask the user if they want
// to load the configuration if this change is a connection
((HIDAcheControl)this.flowLayoutPanel1.Controls[
"hidCtrl"]).HidAcheDevice.CheckConnectionStatus();
if ((int)m.WParam == DBT_DEVICEARRIVAL)
{
HidAcheDevice dev = ((HIDAcheControl)this.flowLayoutPanel1.Controls[
"hidCtrl"]).HidAcheDevice;
if (dev.IsConnected)
{
if (MessageBox.Show(this, "A HIDAche device has been connected." +
"Would you like to read the configuration from the device?",
"Device Connected", MessageBoxButtons.YesNo,
MessageBoxIcon.Question) == DialogResult.Yes)
dev.ReadDevice();
}
}
}
break;
}
base.WndProc(ref m);
}
`WM_DEVICECHANGE` 是在连接或移除 USB 设备时会发送的消息。所以,如果我们收到的消息是这个,我们会检查连接状态以显示设备是已连接还是已断开。在我们的 `CheckConnectionStatus()` 方法中,我们验证 VID/PID 是否与我们的设备应该有的相匹配,这样当某个随机的 USB 设备连接时,我们就不会显示已连接状态。如果它是一个 HIDAche 设备,我们提供读取当前设置的选项。非常简单。注意:我意识到这里有一个设计缺陷。你会注意到在重写函数的末尾有一个对 `base.WndProc()` 的调用,它会传递我们的消息。函数中存在一个 `MessageBox.Show()` 意味着消息可能会被无限期地阻塞,这在连接其他 USB 硬件时可能会导致问题。任何关于如何解决这个问题的建议都将不胜感激。这只在编程我们的设置时是个问题,当设备以恶作剧模式连接时则没有问题。
将数据绑定到 HIDacheDevice 对象
为了尽力遵循 Josh Smith 的数据绑定示例,我使用了一个 `BindingSource` 对象来将 HIDAcheControl 的元素连接到一个 `HIDacheDevice` 对象。这是一种将对象与 UI 分离的非常直接的方法。控件上的每个设置都绑定到一个对象成员,当您通过用户界面更改内容时,它会相应地更新。我非常喜欢这种方法,因为它简化了代码。当设置更改时,我们不必编写代码来实际更改对象中的值,绑定会处理好这一切。也许最有趣的部分是将连接状态图像绑定到 `IsConnected` 公共成员。这只需要很少的额外代码。以下代码被添加到了 `HIDAcheControl` 的构造函数中。
Binding connectionBinding = this.lblIsConnected.DataBindings[0];
connectionBinding.Format += this.ConvertIsConnectedToString;
这所做的只是在标签绑定时强制调用 `ConvertIsConnectedToString()`。在该方法中,我们根据连接状态处理设置正确的文本和图像。
USBComm 库
这个项目最后一个真正有趣的部分是用于编程恶作剧设置的设备与计算机之间的软件连接。让恶作剧设备向操作系统发送 USB 命令是一回事,而让应用程序向我们的设备发送 USB 数据包则是另一回事。为此,我们使用了 USBComm 库。我坚信要给予应有的荣誉。这个库的大部分是由 Jan Axelson 编写的,可以从 USB Central 下载。本质上,该库简化了向像恶作剧设备这样的 HID 设备发送数据包的任务。我对该库做了一些更改和补充,以便让这个设备更容易使用,比如处理返回的数据包在一个 65 字节的缓冲区中左边被零填充的情况。为了处理读写设备 EEPROM 的命令,我创建了 `DeviceCommand` 对象,这极大地简化了构建数据包的过程。正如我上面提到的,因为我有引导加载程序的代码,我知道执行 EEPROM 读写操作的命令是什么。要创建一个从 EEPROM 地址 0x0A 开始读取 10 个字节的数据包,代码如下所示。
// Library needs to know the VID/PID of our device. We set those in the firmware.
USBComm.HIDCommObject comm = new USBComm.HIDCommObject(0x003C, 0x04D8);
// Read command is 0x07
// Write command is 0x05
// The address of the DeviceCommand is 4 bytes. The F0 tells the bootloader code
// that we are going to write to EEPROM and the 0A at the end says we will start
// at address 0x0A.
USBComm.DeviceCommand command = new DeviceCommand(0x07, (Int32)0x00F0000A, 10,
new byte[10]);
// Now we send it and get our response
comm.SendCommandAndGetResponseFromDevice(ref command);
因为我们通过引用传递 `DeviceCommand`,`SendCommandAndGetResponseFromDevice()` 会将对象返回给我们,请求的数据位于 `DeviceCommand` 的 Data 成员中。基本上就是这样。代码很容易浏览,所以你可以通过查看代码来获取更深入的细节。如果你有任何具体问题,请随时提出。
关注点
好了,我的第一篇 CodeProject 文章到此结束。希望阅读过程不是太痛苦。这只是利用硬件/软件集成的一种方式。可能性只受你的想象力限制。一旦我们有了在计算机和设备之间进行通信的方法,我们就可以做任何事情:开灯、显示传感器读数、转动伺服电机等等。我想再写一篇涉及 USB 功能的硬件/软件集成的文章,但只有在有兴趣的情况下才会这样做,所以如果你喜欢这篇文章并想看更多,请告诉我。
历史
- 已将引导加载程序代码添加到项目文件中 - 2014年3月12日
- 原文 - 2009年1月21日