Windows RT 上的辐射计数器






4.67/5 (6投票s)
首个直接连接到 Windows RT 上 ARM 处理器的辐射计数器
- 下载 GM_cnt_simple_testapp.zip - 3.6 KB
- 下载 radiation_counter_static_lib.zip - 9.9 KB
- 下载 radation_counter_DLL_middle_layer.zip - 10.1 KB
- 下载 radiation_counter_device_driver.zip - 29.9 KB
- 下载 radation_counter_Win32_test.zip - 173.3 KB
- 下载 radiation_counter.Net_test_app.zip - 215 KB
- 下载 radiation_counter_Java_test.zip - 813.4 KB
引言
本文演示了首款基于Windows RT的辐射计数器,它直接连接到ARM处理器(即可以内置于Windows RT平板电脑中并且不使用任何传输层,例如不使用I2C、USB、SPI或串行通信)。文章中还描述了Windows RT设备驱动程序和示例测试应用程序。
过去,我曾有一个研究项目,当时正在调查类似于Raspberry Pi的原型Windows RT平台处理GPIO中断的速度。之后,我决定让这个研究项目的结果发挥实际作用,于是就有了这个Windows RT辐射计数器项目。
重要提示
这是一个实验性的软硬件混合工程项目,最终可能涉及测量放射性。在任何情况下,本项目都不应用于实际生活应用。
背景
您一定听说过辐射探测器,它们通常是独立的、基于微控制器的、小型便携式设备,带有电池供电,通常能够测量β和γ电离辐射,配有简易数字显示屏,甚至可能与PC通信以发送结果供以后处理、生成精美的图表以及其他共享数据的方式。
通常,辐射探测器通过某种传输层与PC或智能手机通信,该传输层可以实现为串行、USB、I2C、SPI或其他通信方法。然后,PC或智能手机运行一些软件应用程序,如Windows的Radiation Logger,MacOS的Geiger Bot等,以从辐射探测器获取数据。通常这意味着每隔几秒钟将辐射探测器固件中的CPM值发送到PC或智能手机。
与这些不同,我的原型产品在Geiger计数器硬件和Windows RT之间不使用任何传输层——没有串行,没有USB,没有I2C。它只是将Geiger计数器的负载电阻直接连接到ARM处理器的GPIO引脚。因此,Geiger计数器检测到的每一个粒子都会实时在ARM处理器上产生中断。由于Windows RT运行在同一个ARM处理器上,我们现在可以直接将辐射计数器内置到Windows RT平台中。
这是一个显示组件布局的硬件图。该项目使用了一台Windows RT开发平板电脑平台。Geiger计数器通过由同一台平板电脑的锂离子电池供电的微型电源获得高电压。LND712 Geiger管被用作测试,它太大,无法装进零售尺寸的平板电脑中,但如果使用小型Geiger管,则可以将其内置到零售Windows RT平板电脑的外壳中。
为了使用现有硬件,我利用了brhogan的部分组装的Geiger DIY套件,省略了微控制器,只使用了套件中的高压电源部分。
所以,我们现在已经具备了所有的硬件。剩下的是要编写软件。幸运的是,我能够获得一台解锁的Windows RT开发平板电脑平台,因此我可以在设备驱动程序和应用程序层面毫无障碍地实现我的目标。
我们需要三个部分——首先是新的计数器ACPI资源,然后是用于处理GPIO中断的Windows RT设备驱动程序,最后是使用我们的驱动程序来测试辐射计数器的Windows RT应用程序。
第一部分 Windows RT辐射计数器ACPI资源
首先,我们需要告诉Windows RT我们有新的硬件资源,这样它才能让我们加载相应的Windows设备驱动程序。我通过ACPI表来实现这一点,将以下部分添加到系统ACPI表中。我们的辐射计数器资源非常简单——我们只需要一个资源,那就是中断。为了告诉ACPI哪个GPIO引脚是我们的,我们将其显式设置(在我这个例子中是GPIO引脚142)。我们还指定它必须是高电平有效,即零伏特表示没有中断,高于1.8伏特表示有中断。为了正确完成新的设备ACPI部分,必须根据ACPI规则提供某些设备名称,因此我们的辐射计数器设备驱动程序应该显示为一个名为GMCT0001的设备。
Device(GMC1)
{
Name(_ADR, 0)
Name(_HID, "GMCT0001")
Name(_CID, "GMCT0001")
Name(_UID, 1)
Method(_CRS, 0x0, NotSerialized)
{
Name(RBUF, ResourceTemplate()
{
// GPIO Interrupt Resource
// Geiger counter load resistor to GPIO 142
GpioInt(Edge, ActiveHigh, Shared, PullDown, 0, "\\_SB.GPI2", 0, ResourceConsumer,, RawDataBuffer() {0x15}) {142}
})
Return (RBUF)
}
Method(_STA, 0x0, NotSerialized)
{
Return(0xf)
}
} // end of Device (GMC1) section
要使上述更改生效,需要将此代码片段插入平台ACPI表,重新构建ACPI二进制文件,然后使用新的ACPI更新平台平板电脑固件。然后重新启动Windows RT,以验证在Windows RT设备管理器中是否看到新的未知设备。如果我们想确保它确实是我们的辐射计数器设备,那么可以在设备管理器中检查这个未知设备的详细信息,中断号应该与我们的匹配,ACPI设备名称GMCT0001也应该匹配。
第二部分 Windows RT辐射计数器KMDF设备驱动程序
开发Windows RT设备驱动程序有时可能会很棘手,但这个很简单。
首先,我们的辐射计数器Windows设备驱动程序的.inf文件应该有与我们之前在ACPI部分中设置的资源描述相匹配的资源描述。下面是.inf文件的一个片段,其中注释提醒要有一个匹配的ACPI设备名称
[Standard.NT$ARCH$] %GM_counter_Driver1.DeviceDesc%=GM_counter_Driver1_Device, ACPI\GMCT0001 ; in ACPI table described as GMCT0001
在功能方面,我们在Windows RT设备驱动程序中需要做的就是响应Geiger计数器在GPIO 142上脉冲产生的每一个中断,并处理该中断。由于我们的设备驱动程序将在内核模式下运行,我们不能阻塞或执行任何耗时的任务。然而,我们可以通过计算60秒间隔内的脉冲来计算CPM,然后将此信息暴露给用户程序。但是,我们可以做得更多(与在PC和智能手机上通过传输层运行的辐射日志软件相比)——我们可以实时通知Windows RT(例如,任务栏图标)以及任何用户程序(如果它正在运行,因为设备驱动程序总是运行)我们从Geiger计数器收到的每一个脉冲。这意味着不仅有CPM,还有检测到的每个辐射粒子的时间戳,精度为100纳秒。
接下来,我们需要一种方法让我们的辐射计数器Windows设备驱动程序在每次粒子到达时通知用户应用程序。
内核模式Windows设备驱动程序和用户程序之间的这种交互可以通过多种方式实现。
推荐的方法是实现用户程序对设备驱动程序的DeviceIOControl阻塞调用:这样,用户程序调用设备驱动程序,然后一直等待,直到Geiger计数器检测到一个粒子,设备驱动程序将处理GPIO中断,然后用户应用程序之前阻塞的DeviceIOControl调用将立即返回,用户程序将恢复运行,从而对实际的Geiger脉冲提供快速响应。用户应用程序然后计算这个粒子,并准备下一次调用以等待下一个粒子到达通知。
我理解这是微软Windows设备驱动程序开发指南推荐的做事方式。要正确实现这一点需要一些工作,而我在此项目上花费的时间有限,所以我采用了另一种更简单的方法。
请注意,我的实现方式不同,被认为是架构上不正确的,不推荐这样做。尽管如此,它在我的Windows RT测试中对于演示目的来说效果很好。
我编写的这个设备驱动程序的简单方法是基于共享Windows事件句柄:用户应用程序启动时,会创建一个事件句柄,然后将其告知Windows驱动程序。从那时起,Windows设备驱动程序每次检测到粒子时都会在事件句柄上发出信号;用户应用程序需要等待该事件句柄,并根据需要频繁响应事件信号。在退出用户应用程序之前,会将其事件句柄从我们的设备驱动程序中注销。
因此,我们的辐射计数器Windows设备驱动程序将只有两个Device I\O控制码:一个用于向设备驱动程序提供用户程序的事件句柄,另一个用于告知驱动程序我们不再需要该事件句柄。
// set event handle #define IOCTL_GM_CNTR_SET_EVTHNDL CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTL_INDEX+1, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS) // release event handle #define IOCTL_GM_CNTR_RELEASE_EVTHNDL CTL_CODE(FILE_DEVICE_UNKNOWN, IOCTL_INDEX+2, \ METHOD_BUFFERED, \ FILE_ANY_ACCESS)
下面是一段代码片段,其中驱动程序接受了来自用户应用程序的事件句柄。注意需要进行句柄转换,因为用户应用程序提供的是用户模式句柄,而我们的辐射计数器设备驱动程序是KMDF驱动程序,因此需要内核模式句柄。
…..
case IOCTL_GM_CNTR_SET_EVTHNDL:
length=sizeof(pDevContext->hUserEvent1particle);
status = WdfRequestRetrieveInputBuffer(Request, length, &ioBuffer, &bufLength);
if(!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, "GM_cnt_DevIoCtrl(ERR!) WdfRequestRetrieveInputBuffer2 failed=%Xh\r\n", status);
length=0;
status = STATUS_BUFFER_TOO_SMALL;
pDevContext->hUserEvent1particle=NULL; // we did not get a valid user handle, so invalidate our copy
}
else
{
// copy user mode event handle into our variable
RtlCopyMemory(&pDevContext->hUserEvent1particle, ioBuffer, length);
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, " GM_cnt_DevIoCtrl() UserEvtHnd=%ph\r\n", (PVOID)(pDevContext->hUserEvent1particle));
status=ObReferenceObjectByHandle(pDevContext->hUserEvent1particle, // input : user mode event handle
DELETE | SYNCHRONIZE, // desired access
*ExEventObjectType, // pObjectType,
UserMode, &pDevContext->pkevtOneParticle, NULL); // output: kernel mode khandle
if(!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_QUEUE, " GM_cnt_DevIoCtrl(ERR=%Xh) UserEvtHnd to KEvt failed! \r\n", status);
}
else
{
TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_QUEUE, " GM_cnt_DevIoCtrl() PKEvt=%ph\r\n", pDevContext->pkevtOneParticle);
};
status = STATUS_SUCCESS;
};
KMDF Windows设备驱动程序的ISR中需要完成的工作量应保持在最低限度,因为 such driver runs in kernel mode at high priority, therefore in our ISR all we do is acknowledge an interrupt and then schedule a DPC where we do some more work.
下面是一个简化的辐射计数器Windows设备驱动程序ISR代码版本。
BOOLEAN GMcntDrvIsr(_In_ WDFINTERRUPT Interrupt, _In_ ULONG MessageID) // note runs at IRQL=11
{
PDEVICE_CONTEXT pDeviceContext=NULL;
BOOLEAN bResult=FALSE;
UNREFERENCED_PARAMETER(Interrupt);
UNREFERENCED_PARAMETER(MessageID);
pDeviceContext = DeviceGetContext(WdfInterruptGetDevice(Interrupt));
// just count particles inside of ISR. Do all other work in DPC
if(NULL!=pDeviceContext)
{
pDeviceContext->ulCountInterrupts+=1;
// schedule DPC to do all the work at lower IRQL
bResult=WdfInterruptQueueDpcForIsr(Interrupt);
if(FALSE==bResult)
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "GMcntDrvIsr(ERR) cannot schedule DPC !\r\n");
};
}
else
{
TraceEvents(TRACE_LEVEL_ERROR, TRACE_DRIVER, "GMcntDrvIsr(ERR) pDeviceContext is Null\r\n");
};
bResult=TRUE;
return bResult;
}
在设备驱动程序的.inf文件中,我将此辐射计数器设备类指定为“Sensors”。这是任意的,Windows RT已经有自己的“Sensors”设备类,但我的设备驱动程序不符合他们的Sensor模型,为了避免Windows RT对我的辐射计数器设备的混淆,我给它一个不同的类GUID,它出现在设备管理器中的“Sensors”下,但不是在Microsoft Sensors组中。下面辐射计数器设备驱动程序的.inf文件部分展示了这一点。
; GM_counter_Driver1.inf ; [Version] Signature="$WINDOWS NT$" Class=System ClassGuid={78A1C341-4539-11d3-B88D-00C04FAD5171} ; // Microsoft Sensors use {4D36E97D-E325-11CE-BFC1-08002BE10318} …. [Strings] SPSVCINST_ASSOCSERVICE= 0x00000002 ManufacturerName="SergeiR" ; ClassName="Sensors" ;
Windows RT辐射计数器Windows设备驱动程序VS2012项目可以单独在此处下载。请注意,它只能为ARM构建,不能为Win32或X64构建。
仅测试设备驱动程序(在Windows RT上)
首先,我们要确保我们的Windows设备驱动程序能够正常加载并顺利启动。为此,首先可以使用Windows设备管理器。在下面的截图中,Windows设备管理器显示我们的辐射计数器是一个正常工作的设备。
接下来,为了更深入地了解我们的驱动程序加载和操作,可以使用TraceView实用程序:在辐射计数器驱动程序的C代码中放置了大量的跟踪调试语句,因此通过运行TraceView实用程序,可以在运行时方便地检查我们设备驱动程序的输出,而无需Windows内核调试器。
下面的屏幕截图演示了驱动程序在运行时的一个示例跟踪调试输出。为了验证我们是否确实接收到了Geiger计数器检测到的每一个粒子,我首先使用脉冲发生器来确保每个脉冲都产生一个中断,并且我们设备驱动程序处理了每一个这样的中断。
到目前为止,我们所取得的成果看起来是这样的
我们能达到多高的频率?使用我正在研究的特定Windows RT原型平台,我的设备驱动程序可以可靠地运行,最高速率约为每秒400次中断。请注意,实际上,当Geiger计数器通常用作辐射计数器时,产生的中断会少得多,因为每秒400次中断相当于每分钟24000次计数,这是一个危险的辐射计数。
最后,我断开了脉冲发生器与GPIO引脚的连接,并连接了实际的Geiger计数器负载电阻,如上方图示所示。几秒钟内,我的设备驱动程序就开始将背景辐射的每个粒子事件倾倒到TraceView中。
所以,我们的辐射计数器Windows设备驱动程序在Windows RT上目前运行良好,并且甚至适合未来实际应用:可能的动态范围为0到24000 CPM,并且直接内置在Windows RT操作系统中。
第三部分 Windows RT辐射计数器应用程序
虽然用户应用程序可以做得非常花哨,但让我们从一个简单的应用程序开始,然后逐步演示可以使用我们辐射计数器Windows设备驱动程序实现的更高级功能。
Windows RT上的简单控制台测试程序
现在我们可以创建一个简单的、极简的C控制台用户应用程序来验证与我们驱动程序的交互。另外,由于我们的用户应用程序在每次辐射粒子事件时都会收到通知,因此将由我们的用户应用程序来计算CPM(而不是设备驱动程序),并将其打印到屏幕上(如果选择,也可以打印到日志文件中)。
架构上,我们的简单控制台测试应用程序与设备驱动程序看起来是这样的
用户应用程序可以通过两种方式打开我们的辐射计数器——通过其DOS设备名称或通过由设备驱动程序类GUID确定的完全限定名称。这两种方式都可以与我们的辐射计数器设备驱动程序正常工作。
我们最简单的C测试程序看起来是这样的(为清晰起见,省略了错误检查)
#define DOS_DEV_NAMEW L"\\\\.\\G-McntDevice" // our radiation counter Windows device driver DOS name
const TCHAR* cszParticleEventName=_T("Global\\GMCntPartEvent"); // global space event name
int _cdecl main(_In_ int argc, _In_reads_(argc) char *argv[])
{
int iret=0;
HANDLE hGMCntDev=NULL;
HANDLE hEvtParticle=NULL;
BOOL bResult=FALSE;
LPSECURITY_ATTRIBUTES psaEvent=NULL;
// open our radiation counter device by its DOS name
hGMCntDev=CreateFile(DOS_DEV_NAMEW, GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
// we need to other processes full access this user app event
SetUp_ParticleEventAllAccess(psaEvent);
hEvtParticle=CreateEvent(psaEvent, FALSE, FALSE, cszParticleEventName);
// register event handle with driver
bResult=DeviceIoControl(hGMCntDev,
IOCTL_GM_CNTR_SET_EVTHNDL,
&hEvtParticle, sizeof(hEvtParticle), // handle is input to driver
NULL, NULL, // no output from driver
(PULONG)(&nBytes), NULL);
// now wait for any events on this handle - our device driver will set event on it
// whenever radiation particle is detected
// listen to the next 3 particle events, with 10 sec time out
for(int i=0; i<3; i++)
{
dwRet=WaitForSingleObject(hEvtParticle, 10000);
if(WAIT_OBJECT_0==dwRet)
{
printf(" Particle event %d!\r\n", i);
iret+=1;
}
else if(WAIT_TIMEOUT==dwRet)
{
printf(" Particle event %d timed out\r\n", i);
}
// compute CPM here
};
// done waiting for radiation particle events, so
// notify radiation counter device driver to forget our event handle
bResult=DeviceIoControl(hGMCntDev,
IOCTL_GM_CNTR_RELEASE_EVTHNDL,
&hEvtParticle, sizeof(hEvtParticle), // handle is input to driver
NULL, NULL, // no output from driver
(PULONG)(&nBytes), NULL);
// and close device driver
CloseHandle(hGMCntDev);
return iret;
}
这是在Windows RT上运行的辐射计数器简单控制台测试程序的示例输出,其中前三个粒子事件是预期的,然而,在10秒内只收到了两个。我工作区域的背景辐射约为18 CPM,从统计学上平均相当于大约3秒钟才有一个粒子事件,我们有一个间隔要长得多,但这就是随机的。
C:\GM_cnt_drv>GM_cnt_testapp.exe Particle event 0! Particle event 1! Particle event 2 timed out C:\GM_cnt_drv>...
控制台简单测试应用程序的项目可以单独在此处下载。虽然您可以为任何Windows平台(Win32、X64或ARM)构建此测试应用程序,但它只能在Windows RT上正常运行,因为它可以查找的设备驱动程序只能在Windows RT上运行。
Win32和.Net辐射计数器应用程序(Windows RT)
由于我们的辐射计数器C测试控制台程序运行良好但交互性不强,让我们更进一步,制作另一个更高级的带UI的测试程序,仍然使用C作为编程语言。下一步是测试(并对)Windows RT上的设备驱动程序,使用一个简单的辐射计数器C测试Win32程序,该程序包含CPM计算、CPM数值和类似模拟仪表盘的显示。此外,让我们还在UI中包含中断速率,这在测试程序增加辐射粒子计数器时就已经计算出来了。
需要两个线程——一个线程等待来自辐射计数器设备驱动程序的事件信号,另一个线程每隔几秒钟更新一次UI,当CPM较低时,如果CPM较高则更新更快。Win32编程的细节如今已不那么重要,所以我只包含一个屏幕截图。与辐射计数器设备驱动程序的交互与控制台测试程序完全相同。
我们的Win32测试程序的最后一件事将是压力测试。我发现设备驱动程序可以处理高达28000 CPM的速率而不会对Windows RT性能产生太大影响,所以为什么不进行一次真正的Win32压力测试,看看KMDF驱动程序和用户程序之间的交互是否可靠,以及我们是否会丢失任何粒子事件。
脉冲发生器是测试我们设置的唯一方法,而无需涉及任何真正的放射性物质。使用脉冲发生器,可以轻松检测并显示超过16000 CPM的等效速率,而没有问题,下面是一个屏幕截图
Win32辐射计数器项目可以单独从这里下载。它可以为任何Windows 8平台(ARM、Win32或X64)构建。
尽管Win32旧式程序的UI看起来很简单,但拥有更现代化的东西会更好。C#自然会是首选的编程语言,因为它将提供丰富的图形UI,并且.NET程序也将运行在Windows RT上。
.NET将不得不利用P/invoke机制来调用我们的设备驱动程序。然而,C#在处理本机Windows对象,特别是要传递给驱动程序的事件句柄方面,没有简便的方法。因此,C#必须改用回调机制。这样我们就来到了一个使用C编写的通用中间层最有用的点——然后所有的测试程序都将通过它以统一的方式工作,而不是从C和C#进行不同的访问。
还有一个附带好处是,辐射计数器中间层可以做得跨平台——在ARM上,它将通过实际的辐射计数器Windows设备驱动程序工作;而在Win32和X64上,它将模拟辐射计数器设备驱动程序生成的事件。
用于托管应用程序的辐射计数器中间层库(所有Windows平台)
考虑到这一点,我们测试的最终架构将是这样的
以下是一些注释来解释布局
- 控制台测试程序和Win32测试程序静态链接到中间层静态库
- .NET测试程序动态加载中间层DLL
- .NET测试程序使用P/Invoke和通用、标准数据类型调用中间层的C函数
- Java测试程序使用JNI机制和通用、标准数据类型调用中间层的C函数
现在,由中间层创建用户模式事件句柄并将其注册到辐射计数器设备驱动程序(在Windows RT上)或启动一个模拟器线程(在Win32或X64上)。因此,相同的.NET测试应用程序可以在ARM、Win32或X64上运行,唯一的区别是,在ARM上是真实的辐射计数器事件,而在Win32和X64上是模拟的。
此外,由于中间层处理设备驱动程序交互,它与所有使用该中间层的应用程序共享一个单一的用户模式事件句柄:例如,Win32测试程序可以与.NET测试程序同时运行并计算相同的辐射粒子。下面的屏幕截图演示了这种情况,但是,请注意,由于C#虚拟机的原因,C#程序中存在任意延迟。因此,Win32测试程序显示的CPM可能与C#测试程序显示的CPM不相等,尤其是在较高速率下。
这是在Windows RT上运行的C#测试程序的截图。这里使用的表盘控件来自NextUI仪表库。
最后,另一张截图展示了两个测试程序如何使用我们Windows辐射计数器驱动程序的单个实例同时运行。第一个启动的测试程序创建并注册事件句柄,并将其传递给设备驱动程序。第二个(以及所有后续的)测试程序只是重用第一个测试程序启动创建的事件句柄。
这是全屏复制,您可以在右下角看到一些Windows RT信息,以确保本文所述的软件确实可以在Windows RT上运行良好。C测试程序和C#测试程序之间的CPM差异是由于.NET运行时延迟干扰了程序运行。
这些项目的源代码可以单独下载
- 辐射计数器静态库(所有Windows 8平台,在Win32和X64上自动切换到Geiger模拟器模式)
- 辐射计数器DLL(所有Windows 8平台,在Win32和X64上自动切换到Geiger模拟器模式)
- 辐射计数器C#测试应用程序(所有Windows 8平台)
- 辐射计数器Java测试应用程序(Win32和X64,仅限Geiger模拟器模式)
也可以提供预编译的二进制文件,以防您想感受一下——在Win32或X64版本的Windows上运行(我怀疑您是否能获得解锁的Windows RT开发平台,但ARM二进制文件也有)。然而,CodeProject不允许在文章中包含二进制文件,所以如果对二进制文件有相当大的兴趣,我可能会提出替代方法。
关注点
本项目在Windows RT上演示了几项实验性甚至架构上不正确但完全可以正常工作的技术。
内置于Windows RT的原型辐射计数器在一些不相关的测试中帮助我微调了Windows RT的其他一些组件,当时操作系统正在以高速率服务GPIO中断。
即使是原型版本,这款辐射计数器的动态范围也可能达到0到24000计数/分钟,而不会丢失任何单个粒子事件,并且可以带来基于Windows RT平台的其他有趣的应用。
修订历史
最初为Windows RT开发。然后在Windows RT 8.1上测试并运行良好。在Geiger模拟器模式下,上述测试程序也可以在Windows 7和Windows 8 Win32或X64上运行。