; standard driver stuff; version of driver model = 5 format PE DLL native 0.05 entry START DEBUG = 1 ; this is for DEBUGF macro from 'fdo.inc' __DEBUG__ = 1 __DEBUG_LEVEL__ = 1 include '../../struct.inc' ; Compile-time settings. ; If set, the code will dump all descriptors as they are read to the debug board. USB_DUMP_DESCRIPTORS = 1 ; If set, the code will dump any unclaimed input to the debug board. HID_DUMP_UNCLAIMED = 1 ; USB constants DEVICE_DESCR_TYPE = 1 CONFIG_DESCR_TYPE = 2 STRING_DESCR_TYPE = 3 INTERFACE_DESCR_TYPE = 4 ENDPOINT_DESCR_TYPE = 5 DEVICE_QUALIFIER_DESCR_TYPE = 6 CONTROL_PIPE = 0 ISOCHRONOUS_PIPE = 1 BULK_PIPE = 2 INTERRUPT_PIPE = 3 ; USB HID constants HID_DESCR_TYPE = 21h REPORT_DESCR_TYPE = 22h PHYSICAL_DESCR_TYPE = 23h ; USB structures struct config_descr bLength db ? bDescriptorType db ? wTotalLength dw ? bNumInterfaces db ? bConfigurationValue db ? iConfiguration db ? bmAttributes db ? bMaxPower db ? ends struct interface_descr bLength db ? bDescriptorType db ? bInterfaceNumber db ? bAlternateSetting db ? bNumEndpoints db ? bInterfaceClass db ? bInterfaceSubClass db ? bInterfaceProtocol db ? iInterface db ? ends struct endpoint_descr bLength db ? bDescriptorType db ? bEndpointAddress db ? bmAttributes db ? wMaxPacketSize dw ? bInterval db ? ends ; USB HID structures struct hid_descr bLength db ? bDescriptorType db ? bcdHID dw ? bCountryCode db ? bNumDescriptors db ? base_sizeof rb 0 ; now two fields are repeated .bNumDescriptors times: subDescriptorType db ? subDescriptorLength dw ? ends ; Include macro for parsing report descriptors/data. macro workers_globals {} include 'report.inc' ; Driver data for all devices struct usb_device_data hid hid_data ; data of HID layer epdescr dd ? ; endpoint descriptor hiddescr dd ? ; HID descriptor interface_number dd ? ; copy of interface_descr.bInterfaceNumber configpipe dd ? ; config pipe handle intpipe dd ? ; interrupt pipe handle input_transfer_size dd ? ; input transfer size input_buffer dd ? ; buffer for input transfers control rb 8 ; control packet to device ends section '.flat' code readable writable executable include '../../macros.inc' include '../../proc32.inc' include '../../peimport.inc' include '../../fdo.inc' ; The start procedure. proc START virtual at esp dd ? ; return address .reason dd ? .cmdline dd ? end virtual ; 1. Test whether the procedure is called with the argument DRV_ENTRY. ; If not, return 0. xor eax, eax ; initialize return value cmp [.reason], 1 ; compare the argument jnz .nothing ; 2. Register self as a USB driver. ; The name is my_driver = 'usbhid'; IOCTL interface is not supported; ; usb_functions is an offset of a structure with callback functions. invoke RegUSBDriver, my_driver, eax, usb_functions ; 3. Return the returned value of RegUSBDriver. .nothing: ret endp ; This procedure is called when new HID device is detected. ; It initializes the device. proc AddDevice push ebx esi edi ; save used registers to be stdcall virtual at esp rd 3 ; saved registers dd ? ; return address .config_pipe dd ? .config_descr dd ? .interface dd ? end virtual DEBUGF 1,'K : USB HID device detected\n' ; 1. Allocate memory for device data. movi eax, sizeof.usb_device_data invoke Kmalloc test eax, eax jnz @f mov esi, nomemory_msg invoke SysMsgBoardStr jmp .return0 @@: ; zero-initialize it mov edi, eax xchg eax, ebx xor eax, eax movi ecx, sizeof.usb_device_data / 4 rep stosd mov edx, [.interface] ; HID devices use one IN interrupt endpoint for polling the device ; and an optional OUT interrupt endpoint. We do not use the later, ; but must locate the first. Look for the IN interrupt endpoint. ; Also, look for the HID descriptor; according to HID spec, it must be ; located before endpoint descriptors. ; 2. Get the upper bound of all descriptors' data. mov eax, [.config_descr] movzx ecx, [eax+config_descr.wTotalLength] add eax, ecx ; 3. Loop over all descriptors until ; either end-of-data reached - this is fail ; or interface descriptor found - this is fail, all further data ; correspond to that interface ; or endpoint descriptor for IN endpoint is found ; (HID descriptor must be located before the endpoint descriptor). ; 3a. Loop start: edx points to the interface descriptor. .lookep: ; 3b. Get next descriptor. movzx ecx, byte [edx] ; the first byte of all descriptors is length test ecx, ecx jz .cfgerror add edx, ecx ; 3c. Check that at least two bytes are readable. The opposite is an error. inc edx cmp edx, eax jae .cfgerror dec edx ; 3d. Check that this descriptor is not interface descriptor. The opposite is ; an error. cmp [edx+endpoint_descr.bDescriptorType], INTERFACE_DESCR_TYPE jz .cfgerror ; 3e. For HID descriptor, proceed to 4. ; For endpoint descriptor, go to 5. ; For other descriptors, continue the loop. ; Note: bDescriptorType is in the same place in all descriptors. cmp [edx+endpoint_descr.bDescriptorType], ENDPOINT_DESCR_TYPE jz .foundep cmp [edx+endpoint_descr.bDescriptorType], HID_DESCR_TYPE jnz .lookep ; 4a. Check that the descriptor contains all required data and all data are ; readable. The opposite is an error. movzx ecx, [edx+hid_descr.bLength] cmp ecx, hid_descr.base_sizeof + 3 jb .cfgerror add ecx, edx cmp ecx, eax ja .cfgerror ; 4b. Store the pointer in usb_device_data structure for further references. mov [ebx+usb_device_data.hiddescr], edx ; 4c. Continue the loop. jmp .lookep .foundep: ; 5a. Check that the descriptor contains all required data and all data are ; readable. The opposite is an error. cmp byte [edx+endpoint_descr.bLength], sizeof.endpoint_descr jb .cfgerror lea ecx, [edx+sizeof.endpoint_descr] cmp ecx, eax jbe @f ; 6. An error occured during processing endpoint descriptor. .cfgerror: ; 6a. Print a message. mov esi, invalid_config_descr_msg invoke SysMsgBoardStr ; 6b. Free memory allocated for device data. .free: xchg eax, ebx invoke Kfree .return0: ; 6c. Return an error. xor eax, eax .nothing: pop edi esi ebx ; restore used registers to be stdcall ret 12 @@: ; 5b. If this is not IN interrupt endpoint, ignore it and continue the loop. test [edx+endpoint_descr.bEndpointAddress], 80h jz .lookep mov cl, [edx+endpoint_descr.bmAttributes] and cl, 3 cmp cl, INTERRUPT_PIPE jnz .lookep ; 5c. Store the pointer in usb_device_data structure for futher references. mov [ebx+usb_device_data.epdescr], edx ; 5d. Check that HID descriptor was found. If not, go to 6. cmp [ebx+usb_device_data.hiddescr], 0 jz .cfgerror .descriptors_found: ; 6. Configuration descriptor seems to be ok. ; Send SET_IDLE command disabling auto-repeat feature (it is quite useless) ; and continue configuring in SET_IDLE callback. lea edx, [ebx+usb_device_data.control] mov eax, [.interface] mov dword [edx], 21h + \ ; Class-specific request to Interface (0Ah shl 8) + \ ; SET_IDLE (0 shl 16) + \ ; apply to all input reports (0 shl 24) ; disable auto-repeat movzx eax, [eax+interface_descr.bInterfaceNumber] mov [ebx+usb_device_data.interface_number], eax mov [edx+4], eax ; set interface number, zero length mov eax, [.config_pipe] mov [ebx+usb_device_data.configpipe], eax xor ecx, ecx invoke USBControlTransferAsync, eax, edx, ecx, ecx, idle_set, ebx, ecx ; 7. Return pointer to usb_device_data. xchg eax, ebx jmp .nothing endp ; This procedure is called by USB stack when SET_IDLE request initiated by ; AddDevice is completed, either successfully or unsuccessfully. proc idle_set push ebx esi ; save used registers to be stdcall virtual at esp rd 2 ; saved registers dd ? ; return address .pipe dd ? .status dd ? .buffer dd ? .length dd ? .calldata dd ? end virtual ; Ignore status. Support for SET_IDLE is optional, so the device is free to ; STALL the request; config pipe should remain functional without explicit cleanup. mov ebx, [.calldata] ; 1. HID descriptor contains length of Report descriptor. Parse it. mov esi, [ebx+usb_device_data.hiddescr] movzx ecx, [esi+hid_descr.bNumDescriptors] lea eax, [hid_descr.base_sizeof+ecx*3] cmp eax, 100h jae .cfgerror cmp al, [esi+hid_descr.bLength] jb .cfgerror .look_report: dec ecx js .cfgerror cmp [esi+hid_descr.subDescriptorType], REPORT_DESCR_TYPE jz .found_report add esi, 3 jmp .look_report .cfgerror: mov esi, invalid_config_descr_msg .abort_with_msg: invoke SysMsgBoardStr jmp .nothing .found_report: ; 2. Send request for the Report descriptor. ; 2a. Allocate memory. movzx eax, [esi+hid_descr.subDescriptorLength] test eax, eax jz .cfgerror push eax invoke Kmalloc pop ecx ; If failed, say a message and stop initialization. mov esi, nomemory_msg test eax, eax jz .abort_with_msg ; 2b. Submit the request. xchg eax, esi lea edx, [ebx+usb_device_data.control] mov eax, [ebx+usb_device_data.interface_number] mov dword [edx], 81h + \ ; Standard request to Interface (6 shl 8) + \ ; GET_DESCRIPTOR (0 shl 16) + \ ; descriptor index: there is only one report descriptor (REPORT_DESCR_TYPE shl 24); descriptor type mov [edx+4], ax ; Interface number mov [edx+6], cx ; descriptor length invoke USBControlTransferAsync, [ebx+usb_device_data.configpipe], \ edx, esi, ecx, got_report, ebx, 0 ; 2c. If failed, free the buffer and stop initialization. test eax, eax jnz .nothing xchg eax, esi invoke Kfree .nothing: pop esi ebx ; restore used registers to be stdcall ret 20 endp ; This procedure is called by USB stack when the report descriptor queried ; by idle_set is completed, either successfully or unsuccessfully. proc got_report stdcall uses ebx esi edi, pipe, status, buffer, length, calldata locals parse_descr_locals if ~HID_DUMP_UNCLAIMED has_driver db ? rb 3 end if endl ; 1. Check the status; if the request has failed, say something to the debug board ; and stop initialization. cmp [status], 0 jnz .generic_fail ; 2. Subtract size of setup packet from the total length; ; the rest is length of the descriptor, and it must be nonzero. sub [length], 8 ja .has_something .generic_fail: push esi mov esi, reportfail invoke SysMsgBoardStr pop esi jmp .exit .has_something: ; 3. Process descriptor. ; 3a. Dump it to the debug board, if enabled in compile-time setting. if USB_DUMP_DESCRIPTORS mov eax, [buffer] mov ecx, [length] DEBUGF 1,'K : report descriptor:' @@: DEBUGF 1,' %x',[eax]:2 inc eax dec ecx jnz @b DEBUGF 1,'\n' end if ; 3b. Call the HID layer. parse_descr cmp [report_ok], 0 jz got_report.exit mov ebx, [calldata] postprocess_descr ; 4. Stop initialization if no driver is assigned. if ~HID_DUMP_UNCLAIMED cmp [has_driver], 0 jz got_report.exit end if ; 5. Open interrupt IN pipe. If failed, stop initialization. mov edx, [ebx+usb_device_data.epdescr] movzx ecx, [edx+endpoint_descr.bEndpointAddress] movzx eax, [edx+endpoint_descr.bInterval] movzx edx, [edx+endpoint_descr.wMaxPacketSize] invoke USBOpenPipe, [ebx+usb_device_data.configpipe], ecx, edx, INTERRUPT_PIPE, eax test eax, eax jz got_report.exit mov [ebx+usb_device_data.intpipe], eax ; 6. Initialize buffer for input packet. ; 6a. Find the length of input packet. ; This is the maximal length of all input reports. mov edx, [ebx+usb_device_data.hid.input.first_report] xor eax, eax .find_input_size: test edx, edx jz .found_input_size cmp eax, [edx+report.size] jae @f mov eax, [edx+report.size] @@: mov edx, [edx+report.next] jmp .find_input_size .found_input_size: ; report.size is in bits, transform it to bytes add eax, 7 shr eax, 3 ; if reports are numbered, the first byte is report ID, include it cmp [ebx+usb_device_data.hid.input.numbered], 0 jz @f inc eax @@: mov [ebx+usb_device_data.input_transfer_size], eax ; 6b. Allocate memory for input packet: dword-align and add additional dword ; for extract_field_value. add eax, 4+3 and eax, not 3 invoke Kmalloc test eax, eax jnz @f mov esi, nomemory_msg invoke SysMsgBoardStr jmp got_report.exit @@: mov [ebx+usb_device_data.input_buffer], eax ; 7. Submit a request for input packet and wait for input. call ask_for_input got_report.exit: mov eax, [buffer] invoke Kfree ret endp ; Helper procedure for got_report and got_input. ; Submits a request for the next input packet. proc ask_for_input ; just call USBNormalTransferAsync with correct parameters, ; allow short packets invoke USBNormalTransferAsync, \ [ebx+usb_device_data.intpipe], \ [ebx+usb_device_data.input_buffer], \ [ebx+usb_device_data.input_transfer_size], \ got_input, ebx, \ 1 ret endp ; This procedure is called by USB stack when a HID device responds with input ; data packet. proc got_input stdcall uses ebx esi edi, pipe, status, buffer, length, calldata locals parse_input_locals endl ; 1. Validate parameters: fail on error, ignore zero-length transfers. mov ebx, [calldata] cmp [status], 0 jnz .fail cmp [length], 0 jz .done ; 2. Get pointer to report in esi. ; 2a. If there are no report IDs, use hid.input.data. mov eax, [buffer] mov esi, [ebx+usb_device_data.hid.input.data] cmp [ebx+usb_device_data.hid.input.numbered], 0 jz .report_found ; 2b. Otherwise, the first byte of report is report ID; ; locate the report by its ID, advance buffer+length to one byte. movzx eax, byte [eax] mov esi, [esi+eax*4] inc [buffer] dec [length] .report_found: ; 3. Validate: ignore transfers with unregistered report IDs ; and transfers which are too short for the corresponding report. test esi, esi jz .done mov eax, [esi+report.size] add eax, 7 shr eax, 3 cmp eax, [length] ja .done ; 4. Pass everything to HID layer. parse_input .done: ; 5. Query the next input. mov ebx, [calldata] call ask_for_input .nothing: ret .fail: mov esi, transfer_error_msg invoke SysMsgBoardStr jmp .nothing endp ; This function is called by the USB subsystem when a device is disconnected. proc DeviceDisconnected push ebx esi edi ; save used registers to be stdcall virtual at esp rd 3 ; saved registers dd ? ; return address .device_data dd ? end virtual ; 1. Say a message. mov ebx, [.device_data] mov esi, disconnectmsg invoke SysMsgBoardStr ; 2. Ask HID layer to release all HID-related resources. hid_cleanup ; 3. Free the device data. xchg eax, ebx invoke Kfree ; 4. Return. .nothing: pop edi esi ebx ; restore used registers to be stdcall ret 4 ; purge one dword argument to be stdcall endp include 'sort.inc' include 'unclaimed.inc' include 'mouse.inc' include 'keyboard.inc' ; strings my_driver db 'usbhid',0 nomemory_msg db 'K : no memory',13,10,0 invalid_config_descr_msg db 'K : invalid config descriptor',13,10,0 reportfail db 'K : failed to read report descriptor',13,10,0 transfer_error_msg db 'K : USB transfer error, disabling HID device',13,10,0 disconnectmsg db 'K : USB HID device disconnected',13,10,0 invalid_report_msg db 'K : report descriptor is invalid',13,10,0 delimiter_note db 'K : note: alternate usage ignored',13,10,0 align 4 ; Structure with callback functions. usb_functions: dd 12 dd AddDevice dd DeviceDisconnected ; for DEBUGF macro include_debug_strings ; Workers data workers_globals align 4 data fixups end data