65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 传感器驱动程序和 WinUSB - iNemo 的传感器驱动程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (21投票s)

2011 年 11 月 2 日

CPOL

15分钟阅读

viewsIcon

52324

downloadIcon

4777

本文将介绍传感器 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_QueryDeviceInformationWinUsb_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_ReadPipeWinUsb_WritePipe。它相当直接,所以我看不到深入研究的必要。

异步读取

异步读取实际上并不复杂,不是因为 WinUSB,而是因为线程和同步问题。我不会深入研究同步问题,因为它们相当通用,而不是此用例特有的。在这里,我通过在初始化/释放期间使用同步读/写,或者在运行时使用异步读取来避免了竞用和冲突,绝不一起使用。下面的代码片段由工作线程运行,与同步读/写的主要区别在于使用了事件。您在while循环之前看到的所有内容只是初始化,但您应该注意稍后使用的heventsoOverlap。有趣的内容发生在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 文档,您会找到一些更深入的解释。来自该文档的以下图表很好地总结了整个流程。

设备到达

device arrival callbacks flow

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

设备移除

device removal callbacks flow

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

设备意外移除

device surprise removal callbacks flow

这与标准移除非常相似,因此没有必要深入研究。

传感器 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 个不同的传感器或传感器对象。所以您不需要在设备管理器中有单独的条目,有一个就足够了,让它处理所有您想要的各种传感器类型。

OnGetSupportedPropertiesOnGetPropertiesOnGetSupportedDataFieldsOnSetProperties相当直接。它们基本上声明设备属性和数据字段,并在尝试更改它们时返回“不支持”(当然您可以支持更改,我在此示例中不这样做)。OnProcessWpdMessage如您所见,只是返回S_OKOnGetSupportedEvents同样是微不足道的,它声明了支持的事件。

OnClientSubscribeToEventsOnClientUnsubscribeFromEventsOnClientConnectOnClientDisconnect在我们的示例中并不太有趣,但如果您想实现某些电源管理,它们可能会很有趣。如果您没有任何客户端,您就不需要让设备运行,也不需要触发事件。这是开始进行削减的好地方。

最后一个,也许最重要的是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_ConnectiNEMO_Set_Output_ModeiNEMO_Start_Acquisition。玩转命令参数也相当直接,我只是设置固件以 50Hz 的速率向我发送加速度计、陀螺仪和磁力计的数据,并告诉它开始发送。此时,我将继续异步读取数据,直到设备被禁用/移除。在那里,如果设备仍然连接(同步),我只会发送iNEMO_Stop_Acquisition,这就完成了!

结论

在本文中,我试图为您提供一些关于 WinUSB、传感器 API、UMDF 等方面的良好参考。希望我成功地做到了,并且对您有所帮助。欢迎您联系我,或者只是留下反馈的回复,以便我能添加/更改所需的内容。就这些了,各位!

历史

  • 2011 年 11 月 1 日:初始版本
© . All rights reserved.