经过对linux代码的解读和在windows下面的简单测试,已经总结出来PL2303的基本使用方式了,接下来就可以进行windows平台下的驱动开发了,同时还需要一个com驱动的例子,幸运的是之前已经写过了:
windows 驱动实例分析系列-PL2303芯片开发实战之一
可以参考上面的文章。
驱动创建
之前的COM驱动是umdf的,但是这里也许涉及中断,毕竟Linux代码中有中断的处理,和之前的日志驱动不一样,这次的驱动是需要真实解决问题的,所以似乎使用kmdf更好一些,虽然我不觉得这个GPS产品有什么好中断的,但它确实存在中断端点。
下面是创建一个kmdf USB驱动:
数据上报问题使用com端口上报即可,还是可以使用通用串口软件打开就行。不过,似乎设置波特率的接口暴露出来没什么意义,因为它只在波特率等于115200(0x1C200)的时候可以上报数据,其它波特率会一直输出0,那么直接屏蔽波特率的输出就可以了。
KMDF框架解释
在KMDF的USB框架中,驱动的初始化流程如下:
- 1. DriverEntry代码比较简单,填充AddDevice之后调用WdfDriverCreate函数即可;
- 2. AddDevice函数则比较复杂:
- 2.1. 填充并调用WdfDeviceInitSetPnpPowerEventCallbacks函数设置回调;
- 2.2 调用WdfDeviceInitSetIoType设置I/O模式,这一步是可选的;
- 2.3 调用WdfDeviceCreate函数创建设备对象;
- 2.4 调用WdfDeviceSetPnpCapabilities设置PNP;
- 2.5 初始化并填充设备扩展;
- 2.6 调用WdfDeviceCreateDeviceInterface来创建基于GUID_DEVINTERFACE_COMPORT的设备接口;
- 2.7 调用WdfDeviceOpenRegistryKey函数获取系统分配的COM编号,并使用这个编号生成符号链接字符串;
- 2.8 调用WdfDeviceCreateSymbolicLink创建符号链接;
- 2.9 调用WdfIoQueueCreate创建默认队列;
- 2.10 调用WdfIoQueueCreate作为手动读队列;
- 2.11 调用WdfIoQueueCreate作为等待掩码队列;
- 3. 系统紧接着调用EvtDevicePrepareHardware,这里会初始化USB相关的部分:
- 3.1 调用WdfUsbTargetDeviceCreateWithParameters函数创建WDFUSBDEVICE;
- 3.2 调用WdfUsbTargetDeviceSelectConfig选择配置,由于这个设备的特殊性,只有一个配置一个接口,所以无需设置使用的接口;
- 3.3 调用WdfUsbInterfaceGetConfiguredPipe来获取所有的端点并保存下来,这个设备有三个端点,分别是读、写、中断三个端点;
- 3.4 调用WdfDeviceAssignS0IdleSettings设置S0;
- 3.5 调用WdfDeviceAssignSxWakeSettings设置Sx唤醒;
- 3.6 分配并初始化可能使用的缓冲区;
- 4. D0Entry函数意味着正式开始I/O处理:
- 4.1 根据Linux的定义设置配置,写那些命令;
- 4.2 根据定义设置波特率等参数;
- 4.3 创建内核线程连续读取端点上的数据;
COM编号
在INF文件安装之后,系统会根据总线驱动程序和安装驱动程序的处理,在注册表中注册一些值,这些值将可以在调用DriverEntry时被驱动文件访问。
在我们现在的案例中,就是如何根据COM编号来创建符号链接:
NTSTATUS CreateSymbolName(WDFDEVICE Device)
{
NTSTATUS status = 0;
UNICODE_STRING portName;
UNICODE_STRING comPort;
UNICODE_STRING symbolicLinkName;
WDFKEY key;
WCHAR Buffer[COM_NAME_SIZE] = { 0 };
WCHAR NameBuffer[SYMBOL_NAME_SIZE] = { 0 };
LPGUID guid = (LPGUID)&GUID_DEVINTERFACE_COMPORT;
// 1. 创建设备接口
status = WdfDeviceCreateDeviceInterface(
Device,
guid,
NULL);
if (!NT_SUCCESS(status))
{
goto Exit;
}
// 2. 从注册表中读取COM端口号,如果INF文件显示Class=Ports;
// 则由MsPorts!PortsClassInstaller创建
status = WdfDeviceOpenRegistryKey(
Device,
PLUGPLAY_REGKEY_DEVICE,
KEY_QUERY_VALUE,
WDF_NO_OBJECT_ATTRIBUTES,
&key);
if (!NT_SUCCESS(status))
{
goto Exit;
}
RtlInitUnicodeString(
&portName,
L"PortName");
// 注意,如果不手动初始化UNICODE_STRING结构,下面的很多函数都会失败
RTL_INIT_UNICODE(comPort, 0, Buffer, COM_NAME_SIZE);
status = WdfRegistryQueryUnicodeString(
key,
&portName,
NULL,
&comPort);
if (!NT_SUCCESS(status))
{
goto Exit;
}
// 注意,如果不手动初始化UNICODE_STRING结构,下面的很多函数都会失败
RTL_INIT_UNICODE(symbolicLinkName, 0, NameBuffer, SYMBOL_NAME_SIZE);
RtlUnicodeStringPrintf(
&symbolicLinkName,
L"%s%s",
L"\\DosDevices\\Global\\",
comPort.Buffer);
// 3. 创建符号链接
status = WdfDeviceCreateSymbolicLink(
Device,
&symbolicLinkName);
Exit:
return status;
}
唯一需要注意的是,需要手动给UNICODE_STRING分配内存,否则函数会失败:
// 初始化unicode字符串
#define RTL_INIT_UNICODE(_obj, _length, _buff, _max_size) \
{ \
_obj.Length = _length;\
_obj.Buffer = _buff; \
_obj.MaximumLength = _max_size;\
}
内核线程的创建
为什么这里使用内核线程,原因如下:
1. 如果使用定时器,那么代码可能运行在DISPATCH_LEVEL级,虽然可以创建使用PASSIVE_LEVEL的定时器,但是那样除了增加了难度外并无意义;
2. 如果使用工作项,那么需要单独为了退出工作项做出一些额外操作,工作项更适合那种在DPC级别创建的临时线程,不需要为其同步的,长时间驻留内核的线程用工作项意义并不大;
那还是使用内核线程吧!下面是内核线程的创建代码:
typedef struct _tagThreadContext
{
PKEVENT ExitEvent;
PKTHREAD ThreadObject;
HANDLE ThreadHandle;
}ThreadContext,*PThreadContext;
NTSTATUS CreateKernelThread(PThreadContext pContext)
{
NTSTATUS status;
pContext->ExitEvent = ExAllocatePool2(POOL_FLAG_NON_PAGED, sizeof(KEVENT), 'KBSU');
if (NULL == pContext->ExitEvent)
{
return STATUS_UNSUCCESSFUL;
}
KeInitializeEvent(pContext->ExitEvent, SynchronizationEvent, FALSE);
// 创建内核线程
status = PsCreateSystemThread(
&pContext->ThreadHandle, // 返回的线程句柄
THREAD_ALL_ACCESS, // 线程的访问权限
NULL, // 对象属性
NULL, // 线程隶属的进程 (NULL 表示系统进程)
NULL, // ClientId (不需要)
ThreadRoutine, // 线程入口函数
pContext // 传递给线程的上下文参数
);
if (!NT_SUCCESS(status))
{
ExFreePoolWithTag(pContext->ExitEvent, 'KBSU');
pContext->ExitEvent = NULL;
return status;
}
// 解析对象句柄
status = ObReferenceObjectByHandle(
pContext->ThreadHandle,
THREAD_ALL_ACCESS,
*PsThreadType,
KernelMode,
&pContext->ThreadObject,
NULL);
if (!NT_SUCCESS(status))
{
PsTerminateSystemThread(status);
pContext->ThreadHandle = NULL;
ExFreePoolWithTag(pContext->ExitEvent, 'KBSU');
pContext->ExitEvent = NULL;
return status;
}
return status;
}
NTSTATUS StopKernalThread(PThreadContext pContext)
{
NTSTATUS status = -1;
if (pContext->ExitEvent != NULL)
{
KeSetEvent(pContext->ExitEvent, 0, FALSE);
if (pContext->ThreadObject != NULL)
{
LARGE_INTEGER timeout;
// 设置超时时间(例如 1 秒)
timeout.QuadPart = ( - 10 * 1000 * 1000); // 1 秒,单位为 100 纳秒
// 等待事件或超时
status = KeWaitForSingleObject(
pContext->ThreadObject, // 等待的对象
Executive, // 等待类型
KernelMode, // 运行模式
FALSE, // 是否可警报
&timeout // 超时时间
);
if (status == STATUS_SUCCESS)
{
ZwClose(pContext->ThreadHandle);
ObDereferenceObject(pContext->ThreadObject);
// 如果事件被设置,退出循环
pContext->ThreadObject = NULL;
pContext->ThreadHandle = NULL;
}
}
ExFreePoolWithTag(pContext->ExitEvent, 'KBSU');
pContext->ExitEvent = NULL;
return status;
}
return status;
}
这里的代码看起来和应用层线程差不多,首先初始化内核线程和事件,然后在内核线程中等待退出事件,唯一不同的是,在内核中需要把引用解析为内核对象来等待。
对控制端点的读写
USB对控制端点的读写比较简单,但WDF在这里的封装也有问题,对于不同类型的USB命令居然仅仅采用不同的ID简单区分:
WDF_USB_CONTROL_SETUP_PACKET_INIT
WDF_USB_CONTROL_SETUP_PACKET_INIT_CLASS
WDF_USB_CONTROL_SETUP_PACKET_INIT_FEATURE
WDF_USB_CONTROL_SETUP_PACKET_INIT_GET_STATUS
WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR
如果看这些宏的实现,就会发现它们的差别非常细微。
主要使用的是下面两个宏:
// USB类定义
WDF_USB_CONTROL_SETUP_PACKET_INIT_CLASS
// 厂商定义
WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR
使用同步传输即可,封装同步传输为下面的函数:
NTSTATUS SynchControlCommand(
int type,
int Direction,
int request,
int value,
int index,
char* bytes,
int size,
PDEVICE_CONTEXT pContext)
{
NTSTATUS status;
WDF_USB_CONTROL_SETUP_PACKET controlSetupPacket;
WDF_REQUEST_SEND_OPTIONS sendOptions;
WDF_MEMORY_DESCRIPTOR memoryDescriptor;
// 1. 初始化内存区域
WDF_MEMORY_DESCRIPTOR_INIT_BUFFER(&memoryDescriptor, (PVOID)bytes, size);
// 2.设置超时时间
WDF_REQUEST_SEND_OPTIONS_INIT(&sendOptions, WDF_REQUEST_SEND_OPTION_TIMEOUT);
WDF_REQUEST_SEND_OPTIONS_SET_TIMEOUT(&sendOptions, WDF_REL_TIMEOUT_IN_SEC(3));
// 3. 初始化请求类型
if (type == VENDOR_COMMAND)
{
WDF_USB_CONTROL_SETUP_PACKET_INIT_VENDOR(&controlSetupPacket,
BmRequestHostToDevice, // Direction of the request
Direction, // Recipient
(BYTE)request, // Vendor command
(USHORT)value, // Value
(USHORT)index); // Index
}
else
{
WDF_USB_CONTROL_SETUP_PACKET_INIT_CLASS(&controlSetupPacket,
BmRequestHostToDevice, // Direction of the request
Direction, // Recipient
(BYTE)request, // class command
(USHORT)value, // Value
(USHORT)index); // Index
}
// 4. 同步发送请求
status = WdfUsbTargetDeviceSendControlTransferSynchronously(
pContext->UsbDevice,
WDF_NO_HANDLE, // Optional WDFREQUEST
&sendOptions,
&controlSetupPacket,
&memoryDescriptor, // MemoryDescriptor
NULL); // BytesTransferred
if (!NT_SUCCESS(status))
{
TraceEvents(TRACE_LEVEL_ERROR, DBG_PNP,
"WdfUsbTargetDeviceSendControlTransferSynchronously failed %x\n", status);
}
return status;
}
以上函数的参数解释如下:
- type:请求类型
- Direction:请求方向
- request:请求码
- value:值
- index:索引
- bytes:可能的缓冲区
- size:缓冲区大小
- pContext:设备上下文
再封装读写模式的函数如下:
// 对0号配置端点进读操作
NTSTATUS ReadConfigPipe(int value, int index, char* bytes, int size, PDEVICE_CONTEXT pContext)
{
return SynchControlCommand(VENDOR_COMMAND, DEVICE2HOST, CONTROL_COMMAND, value, index, bytes, size, pContext);
}
// 对0号配置端点进写操作
NTSTATUS WriteConfigPipe(int value, int index, char* bytes, int size, PDEVICE_CONTEXT pContext)
{
return SynchControlCommand(VENDOR_COMMAND, HOST2DEVICE, CONTROL_COMMAND, value, index, bytes, size, pContext);
}
批量读数据
另外一个要解决的问题是数据传输,USB是主从模式,那意味着需要主机主动发送读请求来从设备读取数据,整理的函数如下:
NTSTATUS SynchBulkRead(char* bytes, int size, ULONG *pRetSize, PDEVICE_CONTEXT pContext)
{
NTSTATUS status = -1;
WDF_MEMORY_DESCRIPTOR readBufDesc;
// 1. 初始化缓冲区描述
WDF_MEMORY_DESCRIPTOR_INIT_BUFFER(
&readBufDesc,
bytes,
size
);
// 2. 同步发送数据读取
status = WdfUsbTargetPipeReadSynchronously(
pContext->BulkReadPipe,
NULL,
NULL,
&readBufDesc,
pRetSize
);
if (!NT_SUCCESS(status) || (int)*pRetSize > size)
{
return status;
}
return status;
}
同步读也许存在隐患,但是现在先不管这个问题,毕竟同步改异步只是时间和精力问题。
INF文件修改
代码完成之后,还有两个问题,第一个是创建的是USB驱动类型,但是COM口使用的不是这个类型的;另外是生成的INF文件并未指定PID和VID,需要手动修改,我们需要对INF进行下面的修改:
; 1. 注释原先的USB类
;Class=USBDevice
;ClassGuid={88BAE032-5A81-49f0-BC3D-A4FF138216D6}
; 2. 添加Ports类
Class=Ports
ClassGuid={4D36E978-E325-11CE-BFC1-08002BE10318}
; 3. 注释原先的PID和VID
;%USBKmDriver.DeviceDesc%=USBKmDriver_Device, USB\VID_vvvv&PID_pppp USB\VID_067B&PID_2303
; 4. 添加PL2303的PID和VID
%USBKmDriver.DeviceDesc%=USBKmDriver_Device, USB\VID_067B&PID_2303
其它INF文件内容暂时不修改了,毕竟这个仅仅是为了调试。
安装流程和测试
下面为运行的结果:
项目开源地址为: USB PL2303 Windows Driver
标签:status,USB,WDF,windows,pContext,PL2303,之二,线程,NULL From: https://blog.csdn.net/m0_72813396/article/details/144573227