Windows 传感器驱动程序和 WinUSB - iNemo 的传感器驱动程序
本文将介绍传感器 API 和 WinUSB 的基础知识。
引言
在本文中,我将尝试为 Windows 传感器设备驱动程序提供一个初学者级别的示例。我无法详尽解释我所做的每一件事的原因和方式,但我会尽量提供我找到信息的参考。此示例中的驱动程序用于操作 ST 的 iNemo 评估板。此示例中的传感器驱动程序是一个用户模式驱动程序 (UMD),它堆叠在 WinUSB 之上作为功能驱动程序,并根据 Microsoft 传感器和位置 API 公开其功能。
背景
Windows 传感器和位置 API 相对较新,据我个人经验而言,它不像其他 Microsoft API 那样成熟。这实际上是我撰写本文的主要原因。当我开始编写代码时,我能找到的唯一两个示例是 CodeProject 上的 WiiMote 驱动程序和 Windows 驱动程序工具包 (WinDDK/WDK) 中的内置示例。我个人更喜欢 WinDDK 的编码风格,而不是 WiiMote 的风格,所以你会发现我的代码与 WinDDK 的代码非常相似。
iNemo 是 ST 公司的一款不错的评估板,如果您想玩转传感器,我觉得它非常友好。它通过 USB 连接到主机,其固件将其用作虚拟 COM 端口 (VCP)。从驱动程序的角度来看,这使得处理起来非常容易,因为它就像对文件进行 I/O 一样简单。
谈论传感器驱动程序还需要一两句话来谈论传感器本身。关于每个传感器以及使用其数据的不同方法,有很多话要说,但关于传感器要记住的主要事情是它们是传感器——一个易于出错的物理设备,并且在同一时刻可能因情况不同而表现不同。我说这句话是因为许多程序员没有意识到这一点,并将传感器视为确定性设备,就像其他系统设备一样。这种方法通常会导致非常糟糕的用户体验。更具体地说,我将举一些例子:
- 气压计(压力传感器)- 被视为高度计,但会受到当前天气的影响,更糟的是,还会受到风的影响。
- 加速度计(G 传感器)- 被视为 G 传感器,但实际上测量的是设备上的特定力。它对振动非常敏感。
- 陀螺仪 - 通常测量角速度。即使设备存在最小的偏差(并且总会有这种偏差),除非进行算法处理,否则也会导致漂移。
- 磁力计(指南针)- 提供磁通量/磁场读数,但与任何标准磁罗盘一样,它对附近的电流和磁铁很敏感。
Using the Code
驱动程序架构
Windows 驱动程序编程有多种方法。您可以使用 KMDF、UMDF、两者结合,并以各种方式组织您的驱动程序堆栈。与大多数驱动程序开发人员一样,我认为任何可以在用户空间完成的工作都不应该放在内核空间。这是 Microsoft 开发 UMDF 的主要原因。它减少了系统崩溃,并且更容易开发和调试。在这个示例中,我们处理的是一个非常简单的设备,它通过 USB 与主机通信,没有网络、图形、中断等。这是一个应该完全是 UMD 的驱动程序的经典案例。我在这个示例中展示的是一个 UMD,它堆叠在 WinUSB 之上。如果您不熟悉 UMDF,我认为以下文档非常有用且易于上手:UMDF-arch。如果您是驱动程序编程的绝对新手,您可以从 Microsoft 的 DrvDev_Intro 文档或 Toby Opferman 的文章 开始,两者都非常翔实。
源代码文件包含什么?
您会发现文件结构与 WDK 源目录中的结构非常相似,我在这里将其作为参考。功能划分如下:
- Driver.cpp - 我认为所有或至少大多数 UMD 都共有的初始化。您可以在 WDK 源示例中找到其他代码风格的相同初始化。在这里,我选择了我觉得更方便和现代的一种,即使用 COM 宏。
- Device.cpp - 所有与操作 USB 设备本身相关的功能都在这里。
- SensorDdi.cpp - 顾名思义,它公开了传感器 API。
- 所有其他文件要么是通用的,要么用于构建。这些文件在 WDK 示例源代码中广泛使用,因此您可以找到相关信息。
- MySensor.inx - 用于构建生成的.inf文件的源文件。我特别提到这个文件,因为没有它,您就无法安装驱动程序,而编写.inx/.inf文件对初学者来说并非易事。
代码的作用是什么?
本文中的代码用作 iNemo 评估板的 UMD 传感器设备驱动程序。它初始化设备并以恒定的 50Hz 速率异步获取传感器数据,直到设备被移除/卸载。它绝不是“生产就绪”的代码,所以不要抱怨它没有检查每一个细节或管理功耗,因为它并没有这样做。它是最基本的驱动程序,可以让设备识别自己为传感器并根据传感器 API 以 50Hz 的速率输出数据。
构建代码
要构建代码,您需要安装 WDK。要构建它,请根据目标系统打开相关的构建环境,然后在源目录中输入“build -ceZ
”。
安装驱动程序
安装驱动程序总共需要 5 个文件:
- MySensor.inf - 来自构建输出
- MySensor.dll - 来自构建输出
- WdfCoInstaller01009.dll - 来自WDK /redist/wdf目录
- WUDFUpdate_01009.dll - 来自WDK /redist/wdf目录
- winusbcoinstaller2.dll - 来自WDK /redist/winusb目录
将所有文件放在同一个目录中,然后像其他驱动程序一样,将 Windows 驱动程序更新向导指向.inf文件。
WinUSB
WinUSB 是一个通用的 USB 设备(内核)驱动程序,它根据您的需求提供了多种使用选项。您可以将其用作功能驱动程序(如本示例中所做),作为其中的一部分,来自 UMDF 或 KMDF,或者将其用作整个驱动程序,并且只为其编写.inf文件。要了解如何将其集成到您的.inf文件中,您可以查看附加源代码中的.inx文件,或者构建后生成的.inf文件,它相当直接。在我开始介绍代码之前,您可以找到有关 WinUSB 用法的更详细参考,当然 MSDN 有所有内容。特别是,我推荐以下文档:WinUSB HowTo。有关功能文档,请参考 WDK 帮助文件 - 在这方面它足够好。
初始化 WinUSB
//////////creating a file for I/O management with WinUsb/////////////
PWSTR deviceName = NULL;
DWORD deviceNameCch = 0;
// Get the length of the device name to allocate a buffer
hr = m_pWdfDevice->RetrieveDeviceName(NULL, &deviceNameCch);
// Allocate the buffer
deviceName = new WCHAR[deviceNameCch];
if (deviceName == NULL) {
hr = E_OUTOFMEMORY;
}
// Get the device name
if (SUCCEEDED(hr))
{
hr = m_pWdfDevice->RetrieveDeviceName(deviceName, &deviceNameCch);
}
// Open the device and get the handle
if (SUCCEEDED(hr))
{
m_Handle = CreateFile( deviceName,
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE | FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
}
BOOL CMyDevice::Initialize_Device()
{
m_bResult = FALSE;
USB_INTERFACE_DESCRIPTOR ifaceDescriptor = {0,0,0,0,0,0,0,0,0};
WINUSB_PIPE_INFORMATION pipeInfo;
UCHAR speed = 0;
ULONG length = 0;
m_bResult = WinUsb_Initialize(m_Handle, &m_UsbHandle[0]);
if(m_bResult)
{
m_bResult = WinUsb_GetAssociatedInterface(m_UsbHandle[0],0,&m_UsbHandle[1]);
}
for( int k =0; k<2;k++)
{
if(m_bResult)
{
length = sizeof(UCHAR);
m_bResult = WinUsb_QueryDeviceInformation(m_UsbHandle[k],
DEVICE_SPEED,
&length,
&speed);
}
if(m_bResult)
{
m_Speed = speed;
m_bResult = WinUsb_QueryInterfaceSettings(m_UsbHandle[k],
0,
&ifaceDescriptor);
}
if(m_bResult)
{
for(int i=0;i<ifaceDescriptor.bNumEndpoints;i++)
{
m_bResult = WinUsb_QueryPipe(m_UsbHandle[k],
0,
(UCHAR) i,
&pipeInfo);
if(pipeInfo.PipeType == UsbdPipeTypeBulk &&
USB_ENDPOINT_DIRECTION_IN(pipeInfo.PipeId))
{
m_bulkInPipe = pipeInfo.PipeId;
m_bulkInPipePacketSize =
pipeInfo.MaximumPacketSize;
}
else if(pipeInfo.PipeType == UsbdPipeTypeBulk &&
USB_ENDPOINT_DIRECTION_OUT(pipeInfo.PipeId))
{
m_bulkOutPipe = pipeInfo.PipeId;
}
else if(pipeInfo.PipeType == UsbdPipeTypeInterrupt)
{
m_interruptPipe = pipeInfo.PipeId;
}
else
{
m_bResult = FALSE;
break;
}
}
}
}
return m_bResult;
}
这里的第一个代码片段实际上只是为初始化流程做准备。我们在这里所做的就是创建一个文件用于与 WinUSB 进行 I/O 管理。文件句柄稍后由WinUsb_Initialize
使用。如果您从 KMD 使用 WinUSB,您可能需要以其他方式创建文件。
第二个代码片段是 WinUSB 的实际初始化,它由几个简单的操作组成,这些操作可能会根据您使用的硬件而有所不同 - 取决于接口/管道的数量及其类型。我们进行的主要操作是:
- 使用
WinUsb_Initialize
获取PWINUSB_INTERFACE_HANDLE
。 - 使用
WinUsb_GetAssociatedInterface
获取我们设备的第二个接口的另一个PWINUSB_INTERFACE_HANDLE
。当然,这在其他设备上可能会有所不同。 - 使用
WinUsb_QueryDeviceInformation
和WinUsb_QueryInterfaceSettings
获取不同管道的句柄,并将它们标识为 bulk-in、bulk-out 或 interrupt。这部分是为该设备量身定制的,其他设备可能有更多接口/管道。
基本读写
在Device.cpp中有几个地方我使用了对设备的普通读写操作,我们可以以以下为例:
BOOL CMyDevice::iNemoFrameWrite(UCHAR MsgId)
{
USHORT bufSize = 3;
UCHAR szBuffer[3];
ULONG bytesWritten = 0;
ULONG bytesRead = 0;
m_bResult = FALSE;
//init command parameters
szBuffer[FRM_CTL] = 0x20; //frame type: control, ack: required,
//last fragment, Qos normal.
szBuffer[LENGTH] = 0x01;
szBuffer[MSG_ID] = MsgId;
//write command
m_bResult = WinUsb_WritePipe(m_UsbHandle[1],
m_bulkOutPipe,
szBuffer,
bufSize,
&bytesWritten,
NULL);
if(m_bResult && (bytesWritten == bufSize))
{
//read response
m_bResult = WinUsb_ReadPipe(m_UsbHandle[1],
m_bulkInPipe,
szBuffer,
bufSize,
&bytesRead,
NULL);
}
if(m_bResult)
{
//ACK
if( (szBuffer[FRM_CTL] == FRM_CTL_ACK) &&
(szBuffer[LENGTH] == 0x01) && (szBuffer[MSG_ID] == MsgId))
{
m_bResult = TRUE;
}
//NACK
else if( (szBuffer[FRM_CTL] == FRM_CTL_NACK) &&
(szBuffer[LENGTH] == 0x02) && (szBuffer[MSG_ID] == MsgId))
{
//read the error code byte for debug purposes,
//no check on error code
m_bResult = WinUsb_ReadPipe(m_UsbHandle[1],
m_bulkInPipe,
szBuffer,
1,
&bytesRead,
NULL);
if(m_bResult)
{
m_hr = szBuffer[0];
}
m_bResult = FALSE;
}
//unexpected
else
{
m_bResult = FALSE;
}
}
return m_bResult;
}
您可以忽略所有 ACK/NACK 和其他协议内容,因为它们与 USB 无关(iNemo 固件和驱动程序之间的通信协议)。读写操作实际上与读写任何其他文件一样简单,使用WinUsb_ReadPipe
和WinUsb_WritePipe
。它相当直接,所以我看不到深入研究的必要。
异步读取
异步读取实际上并不复杂,不是因为 WinUSB,而是因为线程和同步问题。我不会深入研究同步问题,因为它们相当通用,而不是此用例特有的。在这里,我通过在初始化/释放期间使用同步读/写,或者在运行时使用异步读取来避免了竞用和冲突,绝不一起使用。下面的代码片段由工作线程运行,与同步读/写的主要区别在于使用了事件。您在while
循环之前看到的所有内容只是初始化,但您应该注意稍后使用的hevents
和oOverlap
。有趣的内容发生在while
循环中。您可以看到它是一个无限循环的WaitForMultipleObjects
调用。我们等待的是来自主驱动程序线程的事件,告诉我们停止此线程,或者来自 WinUSB 的读取事件。我对读取事件的处理与 iNemo 通信协议有关,您可以在这里根据需要进行处理。读取事件根据我定义的条件触发,即读取 3 个字节,它们是 iNemo 协议头。
BOOL CMyDevice::ReadHeader(IN PUCHAR Buffer, IN ULONG bufSize,
OUT PULONG bytesRead, IN LPOVERLAPPED Overlapped)
{
return ReadBuffer(Buffer, bufSize, bytesRead, Overlapped);
}
BOOL CMyDevice::ReadBuffer(IN PUCHAR Buffer, IN ULONG bufSize,
OUT PULONG bytesRead, IN LPOVERLAPPED Overlapped)
{
return WinUsb_ReadPipe(m_UsbHandle[1],
m_bulkInPipe,
Buffer,
bufSize,
bytesRead,
Overlapped);
}
DWORD WINAPI CMyDevice::AsyncReadThreadProc(__in LPVOID pvData)
{
OVERLAPPED oOverlap;
HANDLE hEvents[2];
DWORD dwWait = 0;
DWORD dwNum = 0;
const DWORD STOP_THREAD = WAIT_OBJECT_0;
const DWORD READ_EVENT = WAIT_OBJECT_0+1;
USHORT headerSize = 3;
UCHAR buffer[3];
ULONG bytesRead = 0;
BOOL b = FALSE;
// Cast the argument to the correct type
CMyDevice* pThis = static_cast<cmydevice* />(pvData);
// New threads must always CoInitialize
HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
pThis->m_hReadAsync = CreateEvent(
NULL, // default security attribute
TRUE, // manual-reset event
FALSE, // initial state = not signaled
NULL); // unnamed event object
hEvents[0] = pThis->m_hCloseThread;
hEvents[1] = pThis->m_hReadAsync;
oOverlap.hEvent = pThis->m_hReadAsync;
// Initialize the rest of the OVERLAPPED structure to zero.
oOverlap.Internal = 0;
oOverlap.InternalHigh = 0;
oOverlap.Offset = 0;
oOverlap.OffsetHigh = 0;
if (SUCCEEDED(hr))
{
while (true)
{
pThis->m_bResult = ResetEvent(pThis->m_hReadAsync);
pThis->ReadHeader(buffer, headerSize, &bytesRead, &oOverlap);
dwWait = WaitForMultipleObjects( 2, // number of event objects
hEvents, // array of event objects
FALSE, // does not wait for all
INFINITE // waits indefinitely
);
b = WinUsb_GetOverlappedResult(pThis->m_UsbHandle[1],
&oOverlap,&dwNum,FALSE);
switch(dwWait)
{
case STOP_THREAD:
CoUninitialize();
CloseHandle(pThis->m_hReadAsync);
return 0;
//break;
case READ_EVENT:
//reads the rest of the message if exist,
//throws events if needed
if(b)
{
pThis->ParseDataMessage(buffer);
}
else
{
//error
WinUsb_FlushPipe (pThis->
m_UsbHandle[1],pThis->m_bulkInPipe);
}
break;
default:
break;
}
}
}
//code not reached
return 1;
};
释放 WinUSB
当我们完成后,剩下的就是使用WinUsb_Free
释放 WinUSB。请注意,这里也区分了同步和异步 WinUSB 活动。设备将继续向我发送数据,直到收到“stop
”命令,但这无关紧要,因为我们不再关心这些数据。
HRESULT
CMyDevice::OnD0Exit(
__in IWDFDevice* pWdfDevice,
__in WDF_POWER_DEVICE_STATE previousState
)
{
UNREFERENCED_PARAMETER(pWdfDevice);
UNREFERENCED_PARAMETER(previousState);
if(NULL != m_hCloseThread)
{
// Stop the event thread.
::SetEvent(m_hCloseThread);
// Wait for the thread to end.
::WaitForSingleObject(m_hEventThread, INFINITE);
if (NULL != m_hEventThread)
{
CloseHandle(m_hEventThread);
m_hEventThread = NULL;
}
if(m_iNemoStarted)
{
m_iNemoStarted = FALSE;
Stop_iNemo();
}
CloseHandle(m_hCloseThread);
m_hCloseThread = NULL;
if(m_UsbHandle[0])
{
WinUsb_Free(m_UsbHandle[0]);
}
if(m_UsbHandle[1])
{
WinUsb_Free(m_UsbHandle[1]);
}
}
return S_OK;
}
电源状态和 UMDF 回调
与简单的“hello world
”代码相比,驱动程序编程中烦人的事情之一是,您在所有这些回调中找不到方向。起初很难理解哪个函数首先被调用,之后是什么,以及顺序如何。了解回调在这里很重要,因为我们处理的是一个可能出现和消失、可能在未通知的情况下被移除的 USB 设备,并且实现了电源管理技术,所以我将尝试重点介绍主要回调。如果您查看 UMDF-arch 文档,您会找到一些更深入的解释。来自该文档的以下图表很好地总结了整个流程。
设备到达

IDriverEntry::OnDeviceAdd
是您创建设备实例的地方,创建实例自然会调用设备类的构造函数,因此您可以猜测它何时运行。PnpCallbackHardware::OnPrepareHardware
是设备的主要初始化应该进行的地方,而不是在设备类的构造函数中。在此示例中,我们还声明使用IPnPCallback
接口来支持在不拔出设备的情况下启用/禁用设备。因此,您可以看到在我们的案例中,初始化被分到了OnPrepareHardware
和OnD0Entry
。从这个阶段开始,其他回调对于我们所需的基本功能来说不是必需的。
设备移除

在这里,您可以看到它相当直接,按相反的顺序调用回调。您可能已经明白,无论您如何在设备到达回调之间划分初始化,如果您希望它正常工作,您都需要在此处保持相同的划分。
设备意外移除

这与标准移除非常相似,因此没有必要深入研究。
传感器 API
关于sensor
API 本身有很多可以谈论的,因为它是一个相对较新的 API,每个供应商都试图在他们的操作系统中推广自己的 API,所以您可以争论 Microsoft 的做法是正确还是错误。您可以在 MSDN 论坛 上看到我的一些抱怨。我可以告诉您,我们已经通过适当的渠道联系了 Microsoft 的人员,他们已经意识到了这些问题,并希望在下一代 API(Win8?)中至少修复其中一些问题。无论我对 API 本身的评论如何,让我们看一些代码...
设备作为对应于 API 的注册在SensorDdi.h文件中进行,如下所示:
BEGIN_COM_MAP(CSensorDdi)
COM_INTERFACE_ENTRY(ISensorDriver)
END_COM_MAP()
ISensorDriver
的实现需要实现以下函数:
HRESULT STDMETHODCALLTYPE OnGetSupportedSensorObjects(
__out IPortableDeviceValuesCollection** ppSensorObjectCollection
);
HRESULT STDMETHODCALLTYPE OnGetSupportedProperties(
__in LPWSTR pwszObjectID,
__out IPortableDeviceKeyCollection** ppSupportedProperties
);
HRESULT STDMETHODCALLTYPE OnGetSupportedDataFields(
__in LPWSTR pwszObjectID,
__out IPortableDeviceKeyCollection** ppSupportedDataFields
);
HRESULT STDMETHODCALLTYPE OnGetSupportedEvents(
__in LPWSTR pwszObjectID,
__out GUID** ppSupportedEvents,
__out ULONG* pulEventCount
);
HRESULT STDMETHODCALLTYPE OnGetProperties(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID,
__in IPortableDeviceKeyCollection* pProperties,
__out IPortableDeviceValues** ppPropertyValues
);
HRESULT STDMETHODCALLTYPE OnGetDataFields(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID,
__in IPortableDeviceKeyCollection* pDataFields,
__out IPortableDeviceValues** ppDataValues
);
HRESULT STDMETHODCALLTYPE OnSetProperties(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID,
__in IPortableDeviceValues* pPropertiesToSet,
__out IPortableDeviceValues** ppResults
);
HRESULT STDMETHODCALLTYPE OnClientConnect(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID
);
HRESULT STDMETHODCALLTYPE OnClientDisconnect(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID
);
HRESULT STDMETHODCALLTYPE OnClientSubscribeToEvents(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID
);
HRESULT STDMETHODCALLTYPE OnClientUnsubscribeFromEvents(
__in IWDFFile* pClientFile,
__in LPWSTR pwszObjectID
);
HRESULT STDMETHODCALLTYPE OnProcessWpdMessage(
__in IUnknown* pUnkPortableDeviceValuesParams,
__in IUnknown* pUnkPortableDeviceValuesResults
);
代码本身有点难以阅读,因为大量使用了集合等内容,这对初学者来说并不直观,所以我将尝试在此突出一些主要有趣的内容。
OnGetSupportedSensorObjects
是实际告诉世界您有多少传感器对象的地方。为了更清楚地说,我有一个单一的 USB 设备,但在代码中,它声明拥有 3 个不同的传感器或传感器对象。所以您不需要在设备管理器中有单独的条目,有一个就足够了,让它处理所有您想要的各种传感器类型。
OnGetSupportedProperties
、OnGetProperties
、OnGetSupportedDataFields
和OnSetProperties
相当直接。它们基本上声明设备属性和数据字段,并在尝试更改它们时返回“不支持”(当然您可以支持更改,我在此示例中不这样做)。OnProcessWpdMessage
如您所见,只是返回S_OK
。OnGetSupportedEvents
同样是微不足道的,它声明了支持的事件。
OnClientSubscribeToEvents
、OnClientUnsubscribeFromEvents
、OnClientConnect
和OnClientDisconnect
在我们的示例中并不太有趣,但如果您想实现某些电源管理,它们可能会很有趣。如果您没有任何客户端,您就不需要让设备运行,也不需要触发事件。这是开始进行削减的好地方。
最后一个,也许最重要的是OnGetDataFields
,毕竟传感器就是为此而存在的。在这里要特别注意的一点是,您必须为您的样本关联一个时间戳。如果您查阅 WDK 源中的TimeSensor
示例并尝试将其用于您自己的用途,您可能会想“嘿!我不需要这个,我又不测量时间……” 请不要这样。另一个有趣的点是每个数据字段的返回类型。迄今为止,如果您比较 WDK、MSDN 和 Windows API 代码包示例,其中一些之间存在矛盾。无论如何,我坚持使用 WDK 文档,据我所知,该文档在这场辩论中具有最终决定权。
返回值在 USB 数据到达时(在Device.cpp中)预先计算,以提高效率。计算本身可能对您的硬件用户体验影响最大。您可以拥有最先进的硬件,但如果您的算法很糟糕,用户体验也会很糟糕。我个人认为,API 应该暴露最基本测量而无需任何应用程序操作,并可选地暴露额外的算法数据,以便应用程序程序员可以选择最适合他们需求的内容。然而,Microsoft 的人似乎不这么想,所以对于罗盘数据,您必须融合来自磁力计和加速度计的数据。在代码中,您可以看到这种数据融合的基本方法(加上一些防止除以 0 的安全措施),位于CMyDevice::ParseDataMessage
。顺便说一句,如果所有您需要的是例如温度读数,我上面说的可能与您完全无关。我假设大多数读者更关心方向传感。此处显示的技术对于基本应用程序应该足够了,您可以轻松地用这种质量的数据来飞行 AR.Drone。但是,您可能想阅读更多关于更高级的融合技术。我推荐扩展卡尔曼滤波器 (EKF) 作为入门。请注意,将 EKF 写入代码并不复杂,但要生成 EKF 的方程并校准参数,您应该了解其背后的理论。编写一个值得的 EKF 本身就是一门艺术。
我认为有些读者会对这段代码感兴趣,但又懒得下载源代码,所以您可以在下面看到。请注意,我不得不勉强做一些不那么漂亮的事情,比如近似导数之类的,只是为了适应 API。我真的认为 Microsoft 的人应该改变这一点。请注意,我使用航空航天 NASA 惯例处理我的数据,这是您能找到的最标准的方式,所以当您看到我在这里和那里插入一些“-”符号时,是为了将数据调整到我想要的那个方向。如果您使用其他硬件,您可能需要重新定义符号。代码
void CMyDevice::ParseDataMessage(IN PUCHAR HeaderBuffer)
{
USHORT bufSize = 20;//20
UCHAR buffer[20];//20
ULONG bytesRead = 0;
BOOL bres = FALSE;
BOOL MagRawData = FALSE;
DOUBLE accX = 0;
DOUBLE accY = 0;
DOUBLE accZ = 0;
DOUBLE gyrX = 0;
DOUBLE gyrY = 0;
DOUBLE gyrZ = 0;
FLOAT magX = 0;
FLOAT magY = 0;
FLOAT magZ = 0;
if((HeaderBuffer[0] == 0x40) && (HeaderBuffer[1] == 0x15) &&
(HeaderBuffer[2] == 0x52))
{
bres = ReadBuffer(buffer, bufSize, &bytesRead, NULL);
if(bres)
{
SwapBytes(buffer, bufSize);
PSHORT raw_data = (PSHORT)buffer;
accX = -(DOUBLE)raw_data[1] * ACC_SCALE;
accY = (DOUBLE)raw_data[2] * ACC_SCALE;
accZ = (DOUBLE)raw_data[3] * ACC_SCALE;
gyrX = (-(DOUBLE)raw_data[4] - m_gyrXold) /
DeltaT;//approximate derivative to fit API
gyrY = (-(DOUBLE)raw_data[5] - m_gyrYold) /
DeltaT;//approximate derivative to fit API
gyrZ = (-(DOUBLE)raw_data[6] - m_gyrZold) /
DeltaT;//approximate derivative to fit API
m_gyrXold = -(DOUBLE)raw_data[4];
m_gyrYold = -(DOUBLE)raw_data[5];
m_gyrZold = -(DOUBLE)raw_data[6];
//TODO calculate the angles for the compass API
magX = (FLOAT)raw_data[7];
magY = -(FLOAT)raw_data[8];
magZ = -(FLOAT)raw_data[9];
if(!MagRawData)
{
DOUBLE teta = 0;
DOUBLE phy = 0;
DOUBLE psi = 0;
DOUBLE acc_norm = sqrt
(accX * accX + accY * accY + accZ * accZ);
DOUBLE accXnormed = accX / acc_norm;
DOUBLE accYnormed = accY / acc_norm;
//DOUBLE accZnormed = accZ / acc_norm;
DOUBLE mag_norm = sqrt
(magX * magX + magY * magY + magZ * magZ);
DOUBLE magXnormed = magX / mag_norm;
DOUBLE magYnormed = magY / mag_norm;
DOUBLE magZnormed = magZ / mag_norm;
DOUBLE Xh = 0;
DOUBLE Yh = 0;
DOUBLE rad2deg = 180/PI;
teta = asin(-accXnormed);
if (teta < PI/2 - 0.001 || teta > -PI/2 + 0.001)
{
phy = asin(accYnormed / cos(teta));
m_phyold = phy;
}
else
{
phy = m_phyold;
}
Xh = magXnormed * cos(teta) + magZnormed * sin(teta);
Yh = magXnormed * sin(phy) * sin(teta) +
magYnormed * cos(phy) - magZnormed *
sin(phy) * cos(teta);
if (Xh < 0.001 && Xh > -0.001)
{
if (Yh > 0)
{
psi = PI / 2;
}
else
{
psi = 3 * PI / 2;
}
}
else if (Yh == 0)
{
if (Xh > 0)
{
psi = 0;
}
else
{
psi = PI;
}
}
else
{
if (Xh > 0 && Yh > 0)
{
psi = 2*PI-atan(Yh / Xh);
}
else if (Xh > 0 && Yh < 0)
{
psi = atan(-Yh / Xh);
}
else if (Xh < 0 && Yh > 0)
{
psi = PI + atan(Yh / (-Xh));
}
else //Xh<0 && Yh <0
{
psi = PI - atan(-Yh / (-Xh));
}
}
magX = (FLOAT)(phy*rad2deg);
magY = (FLOAT)(teta*rad2deg);
magZ = (FLOAT)(psi*rad2deg);
}
//updating data in SensorDdi and posting events
m_pSensorDdi->SetDataTimeStamp();
m_pSensorDdi->SetAccData(accX,accY,accZ);
m_pSensorDdi->SetGyrData(gyrX,gyrY,gyrZ);
m_pSensorDdi->SetMagData(magX,magY,magZ);
m_pSensorDdi->PostDataEvent(0);
m_pSensorDdi->PostDataEvent(1);
m_pSensorDdi->PostDataEvent(2);
}
else
{
//error
WinUsb_FlushPipe (m_UsbHandle[1],m_bulkInPipe);
}
}
else
{
//error
WinUsb_FlushPipe (m_UsbHandle[1],m_bulkInPipe);
}
return;
}
iNemo 协议
我不想深入探讨这一点,因为 iNemo 通信协议 在 ST 文档中定义得很清楚且易于理解,我只会介绍我通常使用的流程。嗯,它相当直接,在初始化 WinUSB 之后,我发送序列:iNEMO_Connect
、iNEMO_Set_Output_Mode
、iNEMO_Start_Acquisition
。玩转命令参数也相当直接,我只是设置固件以 50Hz 的速率向我发送加速度计、陀螺仪和磁力计的数据,并告诉它开始发送。此时,我将继续异步读取数据,直到设备被禁用/移除。在那里,如果设备仍然连接(同步),我只会发送iNEMO_Stop_Acquisition
,这就完成了!
结论
在本文中,我试图为您提供一些关于 WinUSB、传感器 API、UMDF 等方面的良好参考。希望我成功地做到了,并且对您有所帮助。欢迎您联系我,或者只是留下反馈的回复,以便我能添加/更改所需的内容。就这些了,各位!
历史
- 2011 年 11 月 1 日:初始版本