/drivers/usb/usbhid/keyboard.inc |
---|
0,0 → 1,475 |
; HID keyboard driver, part of USBHID driver. |
; Global constants. |
; They are assembled in a macro to separate code and data; |
; the code is located at the point of "include 'keyboard.inc'", |
; the data are collected when workers_globals is instantiated. |
macro workers_globals |
{ |
; include global constants from previous workers |
workers_globals |
align 4 |
; Callbacks for HID layer. |
keyboard_driver: |
dd keyboard_driver_add_device |
dd keyboard_driver_disconnect |
dd keyboard_driver_begin_packet |
dd keyboard_driver_array_overflow? |
dd keyboard_driver_input_field |
dd keyboard_driver_end_packet |
; Callbacks for keyboard layer. |
kbd_functions: |
dd 12 |
dd CloseKeyboard |
dd SetKeyboardLights |
; Kernel keyboard layer takes input in form of PS/2 scancodes. |
; data for keyboard: correspondence between HID usage keys and PS/2 scancodes. |
EX = 80h ; if set, precede the scancode with special scancode 0xE0 |
label control_keys byte |
; Usages 700E0h ... 700E7h: LCtrl, LShift, LAlt, LWin, RCtrl, RShift, RAlt, RWin |
db 1Dh, 2Ah, 38h, 5Bh+EX, 1Dh+EX, 36h, 38h+EX, 5Ch+EX |
; Usages 70004h ... 70004h + normal_keys_number - 1 |
label normal_keys byte |
db 1Eh, 30h, 2Eh, 20h, 12h, 21h, 22h, 23h, 17h, 24h, 25h, 26h, 32h, 31h, 18h, 19h |
db 10h, 13h, 1Fh, 14h, 16h, 2Fh, 11h, 2Dh, 15h, 2Ch, 02h, 03h, 04h, 05h, 06h, 07h |
db 08h, 09h, 0Ah, 0Bh, 1Ch, 01h, 0Eh, 0Fh, 39h, 0Ch, 0Dh, 1Ah, 1Bh, 2Bh, 2Bh, 27h |
db 28h, 29h, 33h, 34h, 35h, 3Ah, 3Bh, 3Ch, 3Dh, 3Eh, 3Fh, 40h, 41h, 42h, 43h, 44h |
db 57h, 58h,37h+EX,46h,0,52h+EX,47h+EX,49h+EX,53h+EX,4Fh+EX,51h+EX,4Dh+EX,4Bh+EX,50h+EX,48h+EX,45h |
db 35h+EX,37h,4Ah,4Eh,1Ch+EX,4Fh,50h, 51h, 4Bh, 4Ch, 4Dh, 47h, 48h, 49h, 52h, 53h |
db 56h,5Dh+EX,5Eh+EX |
normal_keys_number = $ - normal_keys |
} |
; Data that are specific for one keyboard device. |
struct keyboard_device_data |
handle dd ? ; keyboard handle from RegKeyboard |
timer dd ? ; auto-repeat timer handle |
repeatkey db ? ; auto-repeat key code |
rb 3 ; padding |
usbdev dd ? ; pointer to device_data of USB and HID layers |
modifiers dd ? ; state of LCtrl ... RWin |
led_report dd ? ; output report for LEDs state |
numlock_bit dd ? ; position of NumLock bit in LED output report |
capslock_bit dd ? |
scrolllock_bit dd ? ; guess what |
ends |
; This procedure is called when HID layer detects a new keyboard. |
; in: ebx -> usb_device_data, edi -> collection |
; out: eax = device-specific data or NULL on error |
proc keyboard_driver_add_device |
; 1. Allocate memory for keyboard_device_data. If failed, return NULL. |
movi eax, sizeof.keyboard_device_data |
invoke Kmalloc |
test eax, eax |
jz .nothing |
; 2. Initialize keyboard_device_data: store pointer to USB layer data, |
; zero some fields, initialize bit positions to -1. |
mov [eax+keyboard_device_data.usbdev], ebx |
xor ecx, ecx |
mov [eax+keyboard_device_data.timer], ecx |
mov [eax+keyboard_device_data.repeatkey], cl |
mov [eax+keyboard_device_data.modifiers], ecx |
mov [eax+keyboard_device_data.led_report], ecx |
dec ecx |
mov [eax+keyboard_device_data.numlock_bit], ecx |
mov [eax+keyboard_device_data.capslock_bit], ecx |
mov [eax+keyboard_device_data.scrolllock_bit], ecx |
; 3. Look for LED report and bits corresponding to indicators. |
; For now, assume that all LEDs are set by the same report. |
; 3a. Save registers. |
push ebx esi |
; 3b. Prepare for loop over output reports: get the first output report. |
; If there are no output records, skip step 3; |
; default values of led_report and *_bit were set in step 2. |
mov edx, [edi+collection.output.first_report] |
test edx, edx |
jz .led_report_set |
.scan_led_report: |
; Process one output report. |
; 3c. Prepare for loop over field groups in the current report: |
; get the first field group. |
mov ecx, [edx+report.first_field] |
.scan_led_field: |
; Process one field group. |
; 3d. If there are no more field groups, exit the loop over field groups. |
test ecx, ecx |
jz .next_led_report |
; For now, assume that all LEDs are plain variable fields, not arrays. |
; 3e. Ignore array field groups. |
test byte [ecx+report_field_group.flags], HID_FIELD_VARIABLE |
jz .next_led_field |
; 3f. Loop over all fields in the current group. |
push [ecx+report_field_group.count] |
; esi = pointer to usage of the current field |
lea esi, [ecx+report_field_group.common_sizeof] |
; ebx = bit position of the current field |
mov ebx, [ecx+report_field_group.offset] |
; if report is numbered, add extra byte in the start of report |
cmp [edx+report.id], 0 |
jz .scan_led_usage |
add ebx, 8 |
.scan_led_usage: |
; for USAGE_LED_*LOCK, store the current bit position in the corresponding field |
; and store the current report as the LED report |
cmp dword [esi], USAGE_LED_NUMLOCK |
jz .numlock |
cmp dword [esi], USAGE_LED_CAPSLOCK |
jz .capslock |
cmp dword [esi], USAGE_LED_SCROLLLOCK |
jnz .next_field |
.scrolllock: |
mov [eax+keyboard_device_data.scrolllock_bit], ebx |
jmp @f |
.capslock: |
mov [eax+keyboard_device_data.capslock_bit], ebx |
jmp @f |
.numlock: |
mov [eax+keyboard_device_data.numlock_bit], ebx |
@@: |
mov [eax+keyboard_device_data.led_report], edx |
.next_field: |
add esi, 4 |
add ebx, [ecx+report_field_group.size] |
dec dword [esp] |
jnz .scan_led_usage |
pop ebx |
.next_led_field: |
; 3g. Continue loop over field groups: get next field group. |
mov ecx, [ecx+report_field_group.next] |
jmp .scan_led_field |
.next_led_report: |
; 3h. If the LED report has been set, break from the loop over reports. |
; Otherwise, get the next report and continue if the current report is not |
; the last for this collection. |
cmp [eax+keyboard_device_data.led_report], 0 |
jnz .led_report_set |
cmp edx, [edi+collection.output.last_report] |
mov edx, [edx+report.next] |
jnz .scan_led_report |
.led_report_set: |
; 3i. Restore registers. |
pop esi ebx |
; 4. Register keyboard in the kernel. |
; store pointer to keyboard_device_data in the stack |
push eax |
; call kernel API |
invoke RegKeyboard, kbd_functions, eax |
; restore pointer to keyboard_device_data from the stack, |
; putting keyboard handle from API to the stack |
xchg eax, [esp] |
; put keyboard handle from API from the stack to keyboard_device_data field |
pop [eax+keyboard_device_data.handle] |
; If failed, free keyboard_device_data and return NULL. |
cmp [eax+keyboard_device_data.handle], 0 |
jz .fail_free |
; 5. Return pointer to keyboard_device_data. |
.nothing: |
ret |
.fail_free: |
invoke Kfree |
xor eax, eax |
ret |
endp |
; This procedure is called when HID layer detects disconnect of a previously |
; connected keyboard. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
proc keyboard_driver_disconnect |
; 1. If an autorepeat timer is active, stop it. |
cmp [edi+keyboard_device_data.timer], 0 |
jz @f |
invoke CancelTimerHS, [edi+keyboard_device_data.timer] |
@@: |
; 2. Unregister keyboard in the kernel. |
invoke DelKeyboard, [edi+keyboard_device_data.handle] |
; We should free data in CloseKeyboard, not here. |
ret |
endp |
; This procedure is called when HID layer starts processing a new input packet |
; from a keyboard. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
proc keyboard_driver_begin_packet |
; Nothing to do. |
ret |
endp |
; This procedure is called when HID layer processes every non-empty array field group. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
; in: ecx = fields count (always nonzero), edx = pointer to fields values |
; in: esi -> report_field_group |
; out: CF set => group is ok, CF cleared => group should be ignored |
proc keyboard_driver_array_overflow? |
; The keyboard signals array overflow by filling the entire array with |
; USAGE_KBD_ROLLOVER codes. |
mov eax, [edx] ; eax = first field in the array |
sub eax, USAGE_KBD_ROLLOVER ; eax = 0 if overflow, nonzero otherwise |
neg eax ; CF cleared if eax was zero, CF set if eax was nonzero |
ret |
endp |
; This procedure is called from HID layer for every field. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
; in: ecx = field usage, edx = value, esi -> report_field_group |
proc keyboard_driver_input_field |
if HID_DUMP_UNCLAIMED |
.unclaimed = default_driver_input_field |
end if |
; 1. Process normal keys: |
; from USAGE_KBD_FIRST_KEY to USAGE_KBD_FIRST_KEY + normal_keys_number - 1, |
; excluding zeroes in [normal_keys]. |
; 1a. Test whether usage is in the range. |
lea eax, [ecx-USAGE_KBD_FIRST_KEY] |
cmp eax, normal_keys_number |
jae .not_normal_key |
; 1b. If the corresponding entry in [normal_keys] is zero, |
; pass this field to the default handler - if HID_DUMP_UNCLAIMED is enabled, |
; default handler is default_driver_input_field, otherwise just ignore the field. |
cmp [normal_keys + eax], 0 |
jz .unclaimed |
; 1c. Get the scancode. |
movzx ecx, [normal_keys + eax] |
; 1d. Further actions are slightly different for key press and key release. |
; Decide what to do. |
test edx, edx |
jz .normal_key_released |
.normal_key_pressed: |
; The key is pressed. |
; 1e. Store the last pressed key for autorepeat. |
mov [edi+keyboard_device_data.repeatkey], cl |
; 1f. Copy bit 7 to CF and send scancode with bit 7 cleared. |
btr ecx, 7 |
call .send_key |
; 1g. Stop the previous autorepeat timer, if any. |
mov eax, [edi+keyboard_device_data.timer] |
test eax, eax |
jz @f |
invoke CancelTimerHS, eax |
@@: |
; 1h. Start the new autorepeat timer with 250 ms initial delay |
; and 50 ms subsequent delays. |
invoke TimerHS, 25, 5, autorepeat_timer, edi |
mov [edi+keyboard_device_data.timer], eax |
if ~HID_DUMP_UNCLAIMED |
.unclaimed: |
end if |
ret |
.normal_key_released: |
; The key is released. |
; 1i. Stop the autorepeat timer if it is autorepeating the released key. |
cmp [edi+keyboard_device_data.repeatkey], cl |
jnz .no_stop_timer |
push ecx |
mov [edi+keyboard_device_data.repeatkey], 0 |
mov eax, [edi+keyboard_device_data.timer] |
test eax, eax |
jz @f |
invoke CancelTimerHS, eax |
mov [edi+keyboard_device_data.timer], 0 |
@@: |
pop ecx |
.no_stop_timer: |
; 1j. Copy bit 7 to CF and send scancode with bit 7 set. |
bts ecx, 7 |
call .send_key |
ret |
.not_normal_key: |
; 2. USAGE_KBD_NOEVENT is simply a filler for free array fields, |
; ignore it. |
cmp ecx, USAGE_KBD_NOEVENT |
jz .nothing |
; 3. Process modifiers: 8 keys starting at USAGE_KBD_LCTRL. |
; 3a. Test whether usage is in range. |
; If not, we don't know what this field means, so pass it to the default handler. |
lea eax, [ecx-USAGE_KBD_LCTRL] |
cmp eax, 8 |
jae .unclaimed |
; 3b. Further actions are slightly different for modifier press |
; and modifier release. Decide what to do. |
test edx, edx |
jz .modifier_not_pressed |
.modifier_pressed: |
; The modifier is pressed. |
; 3c. Set the corresponding status bit. |
; If it was not set, send the corresponding scancode to the kernel |
; with bit 7 cleared. |
bts [edi+keyboard_device_data.modifiers], eax |
jc @f |
movzx ecx, [control_keys+eax] |
btr ecx, 7 |
call .send_key |
@@: |
.nothing: |
ret |
.modifier_not_pressed: |
; The modifier is not pressed. |
; 3d. Clear the correspodning status bit. |
; If it was set, send the corresponding scancode to the kernel |
; with bit 7 set. |
btr [edi+keyboard_device_data.modifiers], eax |
jnc @f |
movzx ecx, [control_keys+eax] |
bts ecx, 7 |
call .send_key |
@@: |
ret |
; Helper procedure. Sends scancode from cl to the kernel. |
; If CF is set, precede it with special code 0xE0. |
.send_key: |
jnc @f |
push ecx |
mov ecx, 0xE0 |
invoke SetKeyboardData |
pop ecx |
@@: |
invoke SetKeyboardData |
ret |
endp |
; This procedure is called when HID layer ends processing a new input packet |
; from a keyboard. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
proc keyboard_driver_end_packet |
; Nothing to do. |
ret |
endp |
; Timer callback for SetTimerHS. |
proc autorepeat_timer |
virtual at esp |
dd ? ; return address |
.data dd ? |
end virtual |
; Just resend the last pressed key. |
mov eax, [.data] |
movzx ecx, [eax+keyboard_device_data.repeatkey] |
; Copy bit 7 to CF and send scancode with bit 7 cleared. |
btr ecx, 7 |
call keyboard_driver_input_field.send_key |
ret 4 |
endp |
; This function is called from the keyboard layer |
; when it is safe to free keyboard data. |
proc CloseKeyboard |
virtual at esp |
dd ? ; return address |
.device_data dd ? |
end virtual |
mov eax, [.device_data] |
invoke Kfree |
ret 4 |
endp |
; This function is called from the keyboard layer |
; to update LED state on the keyboard. |
proc SetKeyboardLights stdcall uses ebx esi edi, device_data, led_state |
locals |
size dd ? |
endl |
; 1. Get the pointer to the LED report. |
; If there is no LED report, exit from the function. |
mov ebx, [device_data] |
mov esi, [ebx+keyboard_device_data.led_report] |
test esi, esi |
jz .nothing |
; 2. Get report size in bytes. |
; report.size is size in bits without possible report ID; |
; if an ID is assigned, the size is one byte greater. |
mov eax, [esi+report.size] |
add eax, 7 |
shr eax, 3 |
cmp [esi+report.id], 0 |
jz @f |
inc eax |
@@: |
mov [size], eax |
; 3. Allocate memory for report + 8 bytes for setup packet. |
; Dword-align size for subsequent rep stosd and bts. |
; If failed, exit from the function. |
add eax, 8 + 3 |
and eax, not 3 |
push eax |
invoke Kmalloc |
pop ecx |
test eax, eax |
jz .nothing |
; 4. Zero-initialize output report. |
push eax |
mov edi, eax |
shr ecx, 2 |
xor eax, eax |
rep stosd |
pop edi |
add edi, 8 |
; 5. Store report ID, if assigned. If not assigned, that would just write zero |
; over zeroes. |
mov edx, [esi+report.id] |
mov [edi], edx |
; 6. Set report bits corresponding to active indicators. |
mov eax, [led_state] |
test al, 1 ; PS/2 Scroll Lock |
jz @f |
mov ecx, [ebx+keyboard_device_data.scrolllock_bit] |
test ecx, ecx |
js @f |
bts [edi], ecx |
@@: |
test al, 2 ; PS/2 Num Lock |
jz @f |
mov ecx, [ebx+keyboard_device_data.numlock_bit] |
test ecx, ecx |
js @f |
bts [edi], ecx |
@@: |
test al, 4 ; PS/2 Caps Lock |
jz @f |
mov ecx, [ebx+keyboard_device_data.capslock_bit] |
test ecx, ecx |
js @f |
bts [edi], ecx |
@@: |
; 7. Fill setup packet. |
shl edx, 16 ; move Report ID to byte 2 |
or edx, 21h + \ ; Class-specific request to Interface |
(9 shl 8) + \ ; SET_REPORT |
(2 shl 24) ; Report Type = Output |
lea eax, [edi-8] |
mov ebx, [ebx+keyboard_device_data.usbdev] |
mov dword [eax], edx |
mov edx, [size] |
shl edx, 16 ; move Size to last word |
or edx, [ebx+usb_device_data.interface_number] |
mov [eax+4], edx |
; 8. Submit output control request. |
invoke USBControlTransferAsync, [ebx+usb_device_data.configpipe], \ |
eax, edi, [size], after_set_keyboard_lights, ebx, 0 |
; If failed, free the buffer now. |
; If succeeded, the callback will free the buffer. |
test eax, eax |
jnz .nothing |
lea eax, [edi-8] |
invoke Kfree |
.nothing: |
ret |
endp |
; This procedure is called from the USB subsystem when the request initiated by |
; SetKeyboardLights is completed, either successfully or unsuccessfully. |
proc after_set_keyboard_lights |
virtual at esp |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
; Ignore status, just free the buffer allocated by SetKeyboardLights. |
mov eax, [.buffer] |
sub eax, 8 |
invoke Kfree |
ret 20 |
endp |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbhid/mouse.inc |
---|
0,0 → 1,163 |
; HID mouse driver, part of USBHID driver. |
; Global constants. |
; They are assembled in a macro to separate code and data; |
; the code is located at the point of "include 'mouse.inc'", |
; the data are collected when workers_globals is instantiated. |
macro workers_globals |
{ |
; include global constants from previous workers |
workers_globals |
align 4 |
; Callbacks for HID layer. |
mouse_driver: |
dd mouse_driver_add_device |
dd mouse_driver_disconnect |
dd mouse_driver_begin_packet |
dd mouse_driver_array_overflow? |
dd mouse_driver_input_field |
dd mouse_driver_end_packet |
} |
; Data that are specific for one mouse device. |
struct mouse_device_data |
buttons dd ? ; buttons that are currently pressed |
dx dd ? ; current x moving |
dy dd ? ; current y moving |
wheel dd ? ; current wheel moving |
hwheel dd ? |
ends |
; This procedure is called when HID layer detects a new mouse. |
; in: ebx -> device_data from USB layer, edi -> collection |
; out: eax = device-specific data or NULL on error |
proc mouse_driver_add_device |
; Just allocate memory; no initialization needed. |
movi eax, sizeof.mouse_device_data |
invoke Kmalloc |
ret |
endp |
; This procedure is called when HID layer detects disconnect of a previously |
; connected mouse. |
; in: edi -> mouse_device_data (pointer returned from mouse_driver_add_device) |
proc mouse_driver_disconnect |
; Free the allocated memory. |
mov eax, edi |
invoke Kfree |
ret |
endp |
; This procedure is called when HID layer starts processing a new input packet |
; from a mouse. |
; in: edi -> mouse_device_data (pointer returned from mouse_driver_add_device) |
proc mouse_driver_begin_packet |
; Zero all variables describing the current state. |
mov [edi+mouse_device_data.buttons], 0 |
mov [edi+mouse_device_data.dx], 0 |
mov [edi+mouse_device_data.dy], 0 |
mov [edi+mouse_device_data.wheel], 0 |
mov [edi+mouse_device_data.hwheel], 0 |
ret |
endp |
; This procedure is called when HID layer processes every non-empty array field group. |
; in: edi -> mouse_device_data (pointer returned from mouse_driver_add_device) |
; in: ecx = fields count (always nonzero), edx = pointer to fields values |
; in: esi -> report_field_group |
; out: CF set => array is ok, CF cleared => array should be ignored |
proc mouse_driver_array_overflow? |
; no array fields, no overflows |
stc |
ret |
endp |
; This procedure is called from HID layer for every field. |
; in: edi -> mouse_device_data (pointer returned from mouse_driver_add_device) |
; in: ecx = field usage, edx = value, esi -> report_field_group |
proc mouse_driver_input_field |
; 1. Determine the handler. We process x/y moving, wheel and up to 32 buttons. |
; Pass other fields to the default handler - default_driver_input_field if |
; HID_DUMP_UNCLAIMED is enabled, just ignore otherwise. |
cmp ecx, USAGE_GD_X |
jz .x |
cmp ecx, USAGE_GD_Y |
jz .y |
cmp ecx, USAGE_GD_WHEEL |
jz .wheel |
cmp ecx, 0xC0238 |
jz .hwheel |
sub ecx, USAGE_BUTTON_PAGE + 1 |
jb .unclaimed |
cmp ecx, 32 |
jae .unclaimed |
; 2. This is a button. |
; If a button is pressed, set the corresponding bit in the state. |
; If a button is not pressed, do nothing. |
test edx, edx |
jz @f |
bts [edi+mouse_device_data.buttons], ecx |
@@: |
if ~HID_DUMP_UNCLAIMED |
.unclaimed: |
end if |
ret |
if HID_DUMP_UNCLAIMED |
.unclaimed: |
add ecx, USAGE_BUTTON_PAGE + 1 |
jmp default_driver_input_field |
end if |
.x: |
; 3. This is x moving. For relative fields, store the value in the state. |
; Pass absolute field to the default handler. |
test byte [esi+report_field_group.flags], HID_FIELD_RELATIVE |
jz .absolute_x |
mov [edi+mouse_device_data.dx], edx |
ret |
.y: |
; 4. This is y moving. For relative fields, store the value in the state, |
; changing the sign: HID uses "mathematics" scheme with Y axis increasing from |
; bottom to top, the kernel expects "programming" PS/2-style with Y axis |
; increasing from top to bottom. |
; Pass absolute fields to the default handler. |
test byte [esi+report_field_group.flags], HID_FIELD_RELATIVE |
jz .absolute_y |
neg edx |
mov [edi+mouse_device_data.dy], edx |
ret |
.wheel: |
; 5. This is wheel event. For relative fields, store the value in the state, |
; changing the sign. Pass absolute fields to the default handler. |
test byte [esi+report_field_group.flags], HID_FIELD_RELATIVE |
jz .unclaimed |
neg edx |
mov [edi+mouse_device_data.wheel], edx |
ret |
.hwheel: |
test byte [esi+report_field_group.flags], HID_FIELD_RELATIVE |
jz .unclaimed |
mov [edi+mouse_device_data.hwheel], edx |
ret |
.absolute_x: |
mov [edi+mouse_device_data.dx], edx |
or [edi+mouse_device_data.buttons], 0x80000000 |
ret |
.absolute_y: |
mov [edi+mouse_device_data.dy], edx |
or [edi+mouse_device_data.buttons], 0x40000000 |
ret |
endp |
; This procedure is called when HID layer ends processing a new input packet |
; from a mouse. |
; in: edi -> mouse_device_data (pointer returned from mouse_driver_add_device) |
proc mouse_driver_end_packet |
; Call the kernel, passing collected state. |
invoke SetMouseData, \ |
[edi+mouse_device_data.buttons], \ |
[edi+mouse_device_data.dx], \ |
[edi+mouse_device_data.dy], \ |
[edi+mouse_device_data.wheel], \ |
[edi+mouse_device_data.hwheel] |
ret |
endp |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbhid/report.inc |
---|
0,0 → 1,1444 |
; Parser of HID structures: parse HID report descriptor, |
; parse/generate input/output/feature reports. |
; ============================================================================= |
; ================================= Constants ================================= |
; ============================================================================= |
; Usage codes from HID specification |
; Generic Desktop usage page |
USAGE_GD_POINTER = 10001h |
USAGE_GD_MOUSE = 10002h |
USAGE_GD_JOYSTICK = 10004h |
USAGE_GD_GAMEPAD = 10005h |
USAGE_GD_KEYBOARD = 10006h |
USAGE_GD_KEYPAD = 10007h |
USAGE_GD_X = 10030h |
USAGE_GD_Y = 10031h |
USAGE_GD_Z = 10032h |
USAGE_GD_RX = 10033h |
USAGE_GD_RY = 10034h |
USAGE_GD_RZ = 10035h |
USAGE_GD_SLIDER = 10036h |
USAGE_GD_DIAL = 10037h |
USAGE_GD_WHEEL = 10038h |
; Keyboard/Keypad usage page |
USAGE_KBD_NOEVENT = 70000h |
USAGE_KBD_ROLLOVER = 70001h |
USAGE_KBD_POSTFAIL = 70002h |
USAGE_KBD_FIRST_KEY = 70004h ; this is 'A', actually |
USAGE_KBD_LCTRL = 700E0h |
USAGE_KBD_LSHIFT = 700E1h |
USAGE_KBD_LALT = 700E2h |
USAGE_KBD_LWIN = 700E3h |
USAGE_KBD_RCTRL = 700E4h |
USAGE_KBD_RSHIFT = 700E5h |
USAGE_KBD_RALT = 700E6h |
USAGE_KBD_RWIN = 700E7h |
; LED usage page |
USAGE_LED_NUMLOCK = 80001h |
USAGE_LED_CAPSLOCK = 80002h |
USAGE_LED_SCROLLLOCK = 80003h |
; Button usage page |
; First button is USAGE_BUTTON_PAGE+1, second - USAGE_BUTTON_PAGE+2 etc. |
USAGE_BUTTON_PAGE = 90000h |
; Flags for input/output/feature fields |
HID_FIELD_CONSTANT = 1 ; if not, then Data field |
HID_FIELD_VARIABLE = 2 ; if not, then Array field |
HID_FIELD_RELATIVE = 4 ; if not, then Absolute field |
HID_FIELD_WRAP = 8 |
HID_FIELD_NONLINEAR = 10h |
HID_FIELD_NOPREFERRED= 20h ; no preferred state |
HID_FIELD_HASNULL = 40h ; has null state |
HID_FIELD_VOLATILE = 80h ; for output/feature fields |
HID_FIELD_BUFBYTES = 100h; buffered bytes |
; Report descriptor can easily describe gigabytes of (meaningless) data. |
; Keep report size reasonable to avoid excessive memory allocations and |
; calculation overflows; 1 Kb is more than enough (typical size is 3-10 bytes). |
MAX_REPORT_BYTES = 1024 |
; ============================================================================= |
; ================================ Structures ================================= |
; ============================================================================= |
; Every meaningful report field group has one or more associated usages. |
; Usages can be individual or joined into continuous ranges. |
; This structure describes one range or one individual usage in a large array; |
; individual usage is equivalent to a range of length 1. |
struct usage_range |
offset dd ? |
; Sum of range sizes over all previous array items. |
; Size of range a equals |
; [a + sizeof.usage_range + usage_range.offset] - [a + usage_range.offset]. |
; The total sum over all array items immediately follows the array, |
; this field must be the first so that the formula above works for the last item. |
first_usage dd ? |
; Usage code for first item in the range. |
ends |
; This structure describes one group of report fields with identical properties. |
struct report_field_group |
next dd ? |
; All field groups in one report are organized in a single-linked list. |
; This is the next group in the report or 0 for the last group. |
size dd ? |
; Size in bits of one field. Cannot be zero or greater than 32. |
count dd ? ; field count, cannot be zero |
offset dd ? ; offset from report start, in bits |
; Following fields are decoded from report descriptor, see HID spec for details. |
flags dd ? |
logical_minimum dd ? |
logical_maximum dd ? |
physical_minimum dd ? |
physical_maximum dd ? |
unit_exponent dd ? |
unit dd ? |
; Following fields are used to speedup extract_field_value. |
mask dd ? |
; Bitmask for all data bits except sign bit: |
; (1 shl .size) - 1 for unsigned fields, (1 shl (.size-1)) - 1 for signed fields |
sign_mask dd ? |
; Zero for unsigned fields. Bitmask with sign bit set for signed fields. |
common_sizeof rd 0 |
; Variable and Array field groups differ significantly. |
; Variable field groups are simple. There are .count fields, each field has |
; predefined Usage, the content of a field is its value. Each field is |
; always present in the report. For Variable field groups, we just keep |
; additional .count dwords with usages for individual fields. |
; Array field groups are complicated. There are .count uniform fields. |
; The content of a field determines Usage; Usages which are currently presented |
; in the report have value = 1, other Usages have value = 0. The number of |
; possible Usages is limited only by field .size; 32-bit field could encode any |
; Usage, so it is unreasonable to keep all Usages in the plain array, as with |
; Variable fields. However, many unrelated Usages in one group are meaningless, |
; so usually possible values are grouped in sequential ranges; number of ranges |
; is limited by report descriptor size (max 0xFFFF bytes should contain all |
; information, including usage ranges and field descriptions). |
; Also, for Array variables we pass changes in state to drivers, not the state |
; itself, because sending information about all possible Usages is inpractical; |
; so we should remember the previous state in addition to the current state. |
; Thus, for Array variables keep the following information, in this order: |
; * some members listed below; note that they do NOT exist for Variable groups; |
; * array of usage ranges in form of usage_range structures, including |
; an additional dword after array described in usage_range structure; |
; * allocated memory for current values of the report; |
; * values of the previous report. |
num_values_prev dd ? ; number of values in the previous report |
num_usage_ranges dd ? ; number of usage_range, always nonzero |
usages rd 0 |
ends |
; This structure describes one report. |
; All reports of one type are organized into a single-linked list. |
struct report |
next dd ? ; pointer to next report of the same type, if any |
size dd ? ; total size in bits |
first_field dd ? ; pointer to first report_field_group for this report |
last_field dd ? |
; pointer to last report_field_group for this report, if any; |
; address of .first_field, if .first_field is 0 |
id dd ? |
; Report ID, if assigned. Zero otherwise. |
top_level_collection dd ? ; top-level collection for this report |
ends |
; This structure describes a set of reports of the same type; |
; there are 3 sets (possibly empty), input, output and feature. |
struct report_set |
data dd ? |
; If .numbered is zero, this is zero for the empty set and |
; a pointer to the (only) report structure otherwise. |
; If .numbered is nonzero, this is a pointer to 256-dword array of pointers |
; to reports organized by report ID. |
first_report dd ? |
; Pointer to the first report or 0 for the empty set. |
numbered db ? |
; If zero, report IDs are not used, there can be at most one report in the set. |
; If nonzero, first byte of the report is report ID. |
rb 3 ; padding |
ends |
; This structure describes a range of reports of one type that belong to |
; some collection. |
struct collection_report_set |
first_report dd ? |
first_field dd ? |
last_report dd ? |
last_field dd ? |
ends |
; This structure defines driver callbacks which are used while |
; device is active; i.e. all callbacks except add_device. |
struct hid_driver_active_callbacks |
disconnect dd ? |
; Called when an existing HID device is disconnected. |
; |
; Four following functions are called when a new input packet arrives |
; in the following order: .begin_packet, then .input_field several times |
; for each input field, interleaved with .array_overflow? for array groups, |
; then .end_packet. |
begin_packet dd ? |
; edi -> driver data |
array_overflow? dd ? |
; edi -> driver data |
; out: CF cleared <=> ignore this array |
input_field dd ? |
; edi -> driver data, ecx = usage, edx = value |
end_packet dd ? |
; edi -> driver data |
ends |
; This structure describes one collection. |
struct collection |
next dd ? ; pointer to the next collection in the same level |
; must be the first field |
parent dd ? ; pointer to nesting collection |
first_child dd ? ; pointer to the first nested collection |
last_child dd ? ; pointer to the last nested collection |
; or to .first_child, if .first_child is zero |
type dd ? ; Application, Physical etc |
usage dd ? ; associated Usage code |
; Next fields are filled only for top-level collections. |
callbacks hid_driver_active_callbacks |
driver_data dd ? ; value to be passed as is to driver callbacks |
input collection_report_set |
output collection_report_set |
feature collection_report_set |
ends |
; This structure keeps all data used by the HID layer for one device. |
struct hid_data |
input report_set |
output report_set |
feature report_set |
first_collection dd ? |
ends |
; This structure defines callbacks required from the driver. |
struct hid_driver_callbacks |
add_device dd ? |
; Called when a new HID device is connected. |
active hid_driver_active_callbacks |
ends |
; Two following structures describe temporary data; |
; the corresponding objects cease to exist when HID parser completes |
; state of Global items |
struct global_items |
next dd ? |
usage_page dd ? |
logical_minimum dd ? |
logical_maximum dd ? |
physical_minimum dd ? |
physical_maximum dd ? |
unit_exponent dd ? |
unit dd ? |
report_size dd ? |
report_id dd ? |
report_count dd ? |
ends |
; one range of Usages |
struct usage_list_item |
next dd ? |
first_usage dd ? |
num_usages dd ? |
ends |
; ============================================================================= |
; =================================== Code ==================================== |
; ============================================================================= |
macro workers_globals |
{ |
workers_globals |
; Jump tables for switch'ing in the code. |
align 4 |
; jump table for two lower bits which encode size of item data |
parse_descr_label.fetch_jumps: |
dd parse_descr_label.fetch_none ; x0, x4, x8, xC |
dd parse_descr_label.fetch_byte ; x1, x5, x9, xD |
dd parse_descr_label.fetch_word ; x2, x6, xA, xE |
dd parse_descr_label.fetch_dword ; x3, x7, xB, xF |
; jump table for two next bits which encode item type |
parse_descr_label.type_jumps: |
dd parse_descr_label.parse_main |
dd parse_descr_label.parse_global |
dd parse_descr_label.parse_local |
dd parse_descr_label.parse_reserved |
; jump table for 4 upper bits in the case of Main item |
parse_descr_label.main_jumps: |
dd parse_descr_label.input ; 80...83 |
dd parse_descr_label.output ; 90...93 |
dd parse_descr_label.collection ; A0...A3 |
dd parse_descr_label.feature ; B0...B3 |
dd parse_descr_label.end_collection ; C0...C3 |
parse_descr_label.num_main_items = ($ - parse_descr_label.main_jumps) / 4 |
; jump table for 4 upper bits in the case of Global item |
parse_descr_label.global_jumps: |
dd parse_descr_label.usage_page ; 04...07 |
dd parse_descr_label.logical_minimum ; 14...17 |
dd parse_descr_label.logical_maximum ; 24...27 |
dd parse_descr_label.physical_minimum ; 34...37 |
dd parse_descr_label.physical_maximum ; 44...47 |
dd parse_descr_label.unit_exponent ; 54...57 |
dd parse_descr_label.unit ; 64...67 |
dd parse_descr_label.report_size ; 74...77 |
dd parse_descr_label.report_id ; 84...87 |
dd parse_descr_label.report_count ; 94...97 |
dd parse_descr_label.push ; A4...A7 |
dd parse_descr_label.pop ; B4...B7 |
parse_descr_label.num_global_items = ($ - parse_descr_label.global_jumps) / 4 |
; jump table for 4 upper bits in the case of Local item |
parse_descr_label.local_jumps: |
dd parse_descr_label.usage ; 08...0B |
dd parse_descr_label.usage_minimum ; 18...1B |
dd parse_descr_label.usage_maximum ; 28...2B |
dd parse_descr_label.item_parsed ; 38...3B = designator item; ignore |
dd parse_descr_label.item_parsed ; 48...4B = designator minimum; ignore |
dd parse_descr_label.item_parsed ; 58...5B = designator maximum; ignore |
dd parse_descr_label.item_parsed ; 68...6B not assigned |
dd parse_descr_label.item_parsed ; 78...7B = string index; ignore |
dd parse_descr_label.item_parsed ; 88...8B = string minimum; ignore |
dd parse_descr_label.item_parsed ; 98...9B = string maximum; ignore |
dd parse_descr_label.delimiter ; A8...AB |
parse_descr_label.num_local_items = ($ - parse_descr_label.local_jumps) / 4 |
} |
; Local variables for parse_descr. |
macro parse_descr_locals |
{ |
cur_item_size dd ? ; encoded size of data for current item |
report_ok db ? ; 0 on error, 1 if everything is ok |
field_type db ? ; 0/1/2 for input/output/feature fields |
rb 2 ; alignment |
field_data dd ? ; data for current item when it describes a field group |
last_reports rd 3 ; pointers to last input/output/feature records |
usage_minimum dd ? ; current value of Usage Minimum |
usage_list dd ? ; list head of usage_list_item |
usage_tail dd ? ; list tail of usage_list_item |
num_usage_ranges dd ? ; number of usage ranges, size of usage_list |
delimiter_depth dd ? ; normally 0; 1 inside of Delimiter(); |
; nested Delimiter()s are not allowed |
usage_variant dd ? ; 0 outside of Delimiter()s and for first Usage inside Delimiter(), |
; incremented with each new Usage inside Delimiter() |
cur_collection dd ? ; current collection |
last_collection dd ? ; last top-level collection |
} |
; Parse report descriptor. The caller should provide local variables |
; [buffer] = pointer to report descriptor, [length] = length of report descriptor, |
; [calldata] = pointer to hid_data (possibly wrapped in a large structure). |
macro parse_descr |
{ |
parse_descr_label: |
; 1. Initialize. |
; 1a. Set some variables to initial values. |
xor edi, edi |
mov dword [report_ok], edi |
mov [usage_list], edi |
mov [cur_collection], edi |
mov eax, [calldata] |
add eax, hid_data.input.first_report |
mov [last_reports+0*4], eax |
add eax, hid_data.output.first_report - hid_data.input.first_report |
mov [last_reports+1*4], eax |
add eax, hid_data.feature.first_report - hid_data.output.first_report |
mov [last_reports+2*4], eax |
add eax, hid_data.first_collection - hid_data.feature.first_report |
mov [last_collection], eax |
; 1b. Allocate state of global items. |
movi eax, sizeof.global_items |
invoke Kmalloc |
test eax, eax |
jz .memory_error |
; 1c. Zero-initialize it and move pointer to edi. |
push eax |
xchg eax, edi |
movi ecx, sizeof.global_items / 4 |
rep stosd |
pop edi |
; 1d. Load pointer to data into esi and make [length] point to end of data. |
mov esi, [buffer] |
add [length], esi |
; 2. Clear all local items. |
; This is needed in the beginning and after processing any Main item. |
.zero_local_items: |
mov eax, [usage_list] |
@@: |
test eax, eax |
jz @f |
push [eax+usage_list_item.next] |
invoke Kfree |
pop eax |
jmp @b |
@@: |
lea ecx, [usage_list] |
mov [usage_tail], ecx |
mov [ecx], eax |
mov [delimiter_depth], eax |
mov [usage_variant], eax |
mov [usage_minimum], eax |
mov [num_usage_ranges], eax |
; 3. Parse items until end of data found. |
cmp esi, [length] |
jae .parse_end |
.fetch_next_item: |
; --------------------------------- Parse item -------------------------------- |
; 4. Parse one item. |
; 4a. Get item data. eax = first item byte = code+type+size (4+2+2 bits), |
; ebx = item data interpreted as unsigned, |
; ecx = item data interpreted as signed. |
movzx eax, byte [esi] |
mov ecx, eax |
and ecx, 3 |
mov [cur_item_size], ecx |
jmp dword [.fetch_jumps+ecx*4] |
.invalid_report: |
mov esi, invalid_report_msg |
jmp .end_str |
.fetch_none: |
xor ebx, ebx |
xor ecx, ecx |
inc esi |
jmp .fetched |
.fetch_byte: |
add esi, 2 |
cmp esi, [length] |
ja .invalid_report |
movzx ebx, byte [esi-1] |
movsx ecx, bl |
jmp .fetched |
.fetch_word: |
add esi, 3 |
cmp esi, [length] |
ja .invalid_report |
movzx ebx, word [esi-2] |
movsx ecx, bx |
jmp .fetched |
.fetch_dword: |
add esi, 5 |
cmp esi, [length] |
ja .invalid_report |
mov ebx, dword [esi-4] |
mov ecx, ebx |
.fetched: |
; 4b. Select the branch according to item type. |
; For every type, select the concrete handler and go there. |
mov edx, eax |
shr edx, 2 |
and edx, 3 |
shr eax, 4 |
jmp dword [.type_jumps+edx*4] |
; -------------------------------- Main items --------------------------------- |
.parse_main: |
sub eax, 8 |
cmp eax, .num_main_items |
jae .item_parsed |
jmp dword [.main_jumps+eax*4] |
; There are 5 Main items. |
; Input/Output/Feature items create new field groups in the corresponding report; |
; Collection item opens a new collection (possibly nested), |
; End Collection item closes the most nested collection. |
.output: |
mov [field_type], 1 |
jmp .new_field |
.feature: |
mov [field_type], 2 |
jmp .new_field |
.input: |
mov [field_type], 0 |
.new_field: |
; Create a new field group. |
mov [field_data], ebx |
movzx ebx, [field_type] |
if sizeof.report_set = 12 |
lea ebx, [ebx*3] |
shl ebx, 2 |
else |
err Change the code |
end if |
add ebx, [calldata] |
; 5. Sanity checks: field size and fields count must be nonzero, |
; field size cannot be more than 32 bits, |
; if field count is more than MAX_REPORT_SIZE * 8, the report would be more than |
; MAX_REPORT_SIZE bytes, so it is invalid too. |
; More precise check for size occurs later; this check only guarantees that |
; there will be no overflows during subsequent calculations. |
cmp [edi+global_items.report_size], 0 |
jz .invalid_report |
cmp [edi+global_items.report_size], 32 |
ja .invalid_report |
; There are devices with Report Count(0) + Input(Constant Variable), |
; zero-length padding. Thus, do not consider descriptors with Report Count(0) |
; as invalid; instead, just ignore fields with Report Count(0). |
cmp [edi+global_items.report_count], 0 |
jz .zero_local_items |
cmp [edi+global_items.report_count], MAX_REPORT_BYTES * 8 |
ja .invalid_report |
; 6. Get the pointer to the place for the corresponding report in ebx. |
; 6a. If report ID is not assigned, ebx already points to report_set.data, |
; so go to 7. |
cmp [edi+global_items.report_id], 0 |
jz .report_ptr_found |
; 6b. If table for reports was already allocated, |
; go to 6d skipping the next substep. |
cmp [ebx+report_set.numbered], 0 |
jnz .report_set_allocated |
; 6c. This is the first report with ID; |
; allocate and zero-initialize table for reports. |
; Note: it is incorrect but theoretically possible that some fields were |
; already allocated in report without ID; if so, abort processing with error. |
cmp [ebx+report_set.data], 0 |
jnz .invalid_report |
mov eax, 256*4 |
invoke Kmalloc |
test eax, eax |
jz .memory_error |
mov [ebx+report_set.data], eax |
inc [ebx+report_set.numbered] |
push edi |
mov edi, eax |
mov ecx, 256 |
xor eax, eax |
rep stosd |
pop edi |
; 6d. Report ID is assigned, report table is allocated, |
; get the pointer to the corresponding item in the report table. |
.report_set_allocated: |
mov ebx, [ebx+report_set.data] |
mov ecx, [edi+global_items.report_id] |
lea ebx, [ebx+ecx*4] |
; 7. If the field group is the first one in the report, |
; allocate and initialize report without fields. |
.report_ptr_found: |
; 7a. Check whether the report has been allocated. |
cmp dword [ebx], 0 |
jnz .report_allocated |
; 7b. Allocate. |
movi eax, sizeof.report |
invoke Kmalloc |
test eax, eax |
jz .memory_error |
; 7c. Initialize. |
xor edx, edx |
lea ecx, [eax+report.first_field] |
mov [ebx], eax |
mov [eax+report.next], edx |
mov [eax+report.size], edx |
mov [ecx], edx |
mov [eax+report.last_field], ecx |
mov [eax+report.top_level_collection], edx |
mov ecx, [edi+global_items.report_id] |
mov [eax+report.id], ecx |
; 7d. Append to the overall list of reports. |
movzx edx, [field_type] |
lea edx, [last_reports+edx*4] |
mov ecx, [edx] |
mov [edx], eax |
mov [ecx], eax |
.report_allocated: |
mov ebx, [ebx] |
; ebx points to an already existing report; add new field. |
; 8. Calculate total size of the group and |
; check that the new group would not overflow the report. |
mov eax, [edi+global_items.report_size] |
mul [edi+global_items.report_count] |
mov ecx, [ebx+report.size] |
add ecx, eax |
cmp ecx, MAX_REPORT_BYTES * 8 |
ja .invalid_report |
; 9. If there are no usages for this group, this is padding; |
; add it's size to total report size and stop processing. |
cmp [num_usage_ranges], 0 |
jz .padding |
; 10. Allocate memory for the group: this includes field group structure |
; and additional fields depending on field type. |
; See comments in report_field_group structure. |
push eax |
mov edx, [edi+global_items.report_count] |
lea eax, [report_field_group.common_sizeof+edx*4] |
test byte [field_data], HID_FIELD_VARIABLE |
jnz @f |
lea eax, [eax+edx*4] |
mov edx, [num_usage_ranges] |
lea eax, [eax+edx*sizeof.usage_range+4] |
@@: |
invoke Kmalloc |
pop edx |
test eax, eax |
jz .memory_error |
; 11. Update report data. |
; Field offset is the current report size; |
; get the current report size and update report size. |
; Also store the pointer to new field in the previous last field |
; and update the last field. |
mov ecx, [ebx+report.last_field] |
xadd [ebx+report.size], edx |
mov [ebx+report.last_field], eax |
mov [ecx], eax |
; 12. Initialize field data: offset was calculated in the previous step, |
; copy other characteristics from global_items data, |
; calculate .mask and .sign_mask. |
mov [eax+report_field_group.offset], edx |
xor edx, edx |
mov [eax+report_field_group.next], edx |
mov [eax+report_field_group.sign_mask], edx |
inc edx |
mov ecx, [edi+global_items.report_size] |
mov [eax+report_field_group.size], ecx |
shl edx, cl |
cmp [edi+global_items.logical_minimum], 0 |
jge .unsigned |
shr edx, 1 |
mov [eax+report_field_group.sign_mask], edx |
.unsigned: |
dec edx |
mov [eax+report_field_group.mask], edx |
mov ecx, [edi+global_items.report_count] |
mov [eax+report_field_group.count], ecx |
mov ecx, [field_data] |
mov [eax+report_field_group.flags], ecx |
irps field, logical_minimum logical_maximum physical_minimum physical_maximum unit_exponent unit |
\{ |
mov ecx, [edi+global_items.\#field] |
mov [eax+report_field_group.\#field], ecx |
\} |
; 13. Update the current collection; nesting collections will be updated by |
; end-of-collection handler. |
movzx edx, [field_type] |
if sizeof.collection_report_set = 16 |
shl edx, 4 |
else |
err Change the code |
end if |
mov ecx, [cur_collection] |
test ecx, ecx |
jz .no_collection |
lea ecx, [ecx+collection.input+edx] |
mov [ecx+collection_report_set.last_report], ebx |
mov [ecx+collection_report_set.last_field], eax |
cmp [ecx+collection_report_set.first_field], 0 |
jnz .no_collection |
mov [ecx+collection_report_set.first_report], ebx |
mov [ecx+collection_report_set.first_field], eax |
.no_collection: |
; 14. Transform usage ranges. The target format depends on field type. |
test byte [eax+report_field_group.flags], HID_FIELD_VARIABLE |
jz .transform_usages_for_array |
; For Variable field groups, expand all ranges to array with .count Usages. |
; If total number of Usages in all ranges is too large, ignore excessive. |
; If total number of Usages in all ranges is too small, duplicate the last |
; Usage up to .count Usages (e.g. group of several indicators can have one usage |
; "Generic Indicator" assigned to all fields). |
mov ecx, [eax+report_field_group.count] |
mov ebx, [usage_list] |
.next_usage_range_for_variable: |
mov edx, [ebx+usage_list_item.first_usage] |
push [ebx+usage_list_item.num_usages] |
.next_usage_for_variable: |
mov [eax+report_field_group.common_sizeof], edx |
dec ecx |
jz @f |
add eax, 4 |
inc edx |
dec dword [esp] |
jnz .next_usage_for_variable |
dec edx |
inc dword [esp] |
cmp [ebx+usage_list_item.next], 0 |
jz .next_usage_for_variable |
pop edx |
mov ebx, [ebx+usage_list_item.next] |
jmp .next_usage_range_for_variable |
@@: |
pop ebx |
jmp .zero_local_items |
.transform_usages_for_array: |
; For Array field groups, leave ranges unexpanded, but recode in the form |
; more convenient to value lookup, see comments in report_field_group structure. |
mov ecx, [num_usage_ranges] |
mov [eax+report_field_group.num_usage_ranges], ecx |
and [eax+report_field_group.num_values_prev], 0 |
mov ecx, [usage_list] |
xor ebx, ebx |
@@: |
mov edx, [ecx+usage_list_item.first_usage] |
mov [eax+report_field_group.usages+usage_range.offset], ebx |
add ebx, [ecx+usage_list_item.num_usages] |
jc .invalid_report |
mov [eax+report_field_group.usages+usage_range.first_usage], edx |
add eax, sizeof.usage_range |
mov ecx, [ecx+usage_list_item.next] |
test ecx, ecx |
jnz @b |
mov [eax+report_field_group.usages], ebx |
; New field is initialized. |
jmp .zero_local_items |
.padding: |
mov [ebx+report.size], ecx |
jmp .zero_local_items |
; Create a new collection, nested in the current one. |
.collection: |
; Actions are quite straightforward: |
; allocate, zero-initialize, update parent, if there is one, |
; make it current. |
movi eax, sizeof.collection |
invoke Kmalloc |
test eax, eax |
jz .memory_error |
push eax edi |
movi ecx, sizeof.collection / 4 |
xchg edi, eax |
xor eax, eax |
rep stosd |
pop edi eax |
mov edx, [cur_collection] |
mov [eax+collection.parent], edx |
lea ecx, [last_collection] |
test edx, edx |
jz .no_parent |
lea ecx, [edx+collection.last_child] |
.no_parent: |
mov edx, [ecx] |
mov [ecx], eax |
mov [edx], eax |
lea ecx, [eax+collection.first_child] |
; In theory, there must be at least one usage. |
; In practice, some nested collections don't have any. Use zero in this case. |
mov edx, [usage_list] |
test edx, edx |
jz @f |
mov edx, [edx+usage_list_item.first_usage] |
@@: |
mov [eax+collection.last_child], ecx |
mov [eax+collection.type], ebx |
mov [eax+collection.usage], edx |
mov [cur_collection], eax |
jmp .zero_local_items |
; Close the current collection. |
.end_collection: |
; There must be an opened collection. |
mov eax, [cur_collection] |
test eax, eax |
jz .invalid_report |
; Make parent collection the current one. |
mov edx, [eax+collection.parent] |
mov [cur_collection], edx |
; Add field range of the closing collection to field range for nesting collection, |
; if there is one. |
test edx, edx |
jz .zero_local_items |
push 3 ; for each type: input, output, feature |
.update_ranges: |
mov ecx, [eax+collection.input.last_report] |
test ecx, ecx |
jz .no_fields |
mov [edx+collection.input.last_report], ecx |
mov ecx, [eax+collection.input.last_field] |
mov [edx+collection.input.last_field], ecx |
cmp [edx+collection.input.first_report], 0 |
jnz .no_fields |
mov ecx, [eax+collection.input.first_report] |
mov [edx+collection.input.first_report], ecx |
mov ecx, [eax+collection.input.first_field] |
mov [edx+collection.input.first_field], ecx |
.no_fields: |
add eax, sizeof.collection_report_set |
add edx, sizeof.collection_report_set |
dec dword [esp] |
jnz .update_ranges |
pop eax |
jmp .zero_local_items |
; ------------------------------- Global items -------------------------------- |
.parse_global: |
cmp eax, .num_global_items |
jae .item_parsed |
jmp dword [.global_jumps+eax*4] |
; For most global items, just store the value in the current global_items structure. |
; Note 1: Usage Page will be used for upper word of Usage[| Minimum|Maximum], so |
; shift it in advance. |
; Note 2: the HID specification allows both signed and unsigned values for |
; logical and physical minimum/maximum, but does not give a method to distinguish. |
; Thus, hope that minimum comes first, parse the minimum as signed value always, |
; if it is less than zero, assume signed values, otherwise assume unsigned values. |
; This covers both common cases Minimum(0)/Maximum(FF) and Minimum(-7F)/Maximum(7F). |
; Note 3: zero value for Report ID is forbidden by the HID specification. |
; It is quite convenient, we use report_id == 0 for reports without ID. |
.usage_page: |
shl ebx, 16 |
mov [edi+global_items.usage_page], ebx |
jmp .item_parsed |
.logical_minimum: |
mov [edi+global_items.logical_minimum], ecx |
jmp .item_parsed |
.logical_maximum: |
cmp [edi+global_items.logical_minimum], 0 |
jge @f |
mov ebx, ecx |
@@: |
mov [edi+global_items.logical_maximum], ebx |
jmp .item_parsed |
.physical_minimum: |
mov [edi+global_items.physical_minimum], ecx |
jmp .item_parsed |
.physical_maximum: |
cmp [edi+global_items.physical_maximum], 0 |
jge @f |
mov ebx, ecx |
@@: |
mov [edi+global_items.physical_maximum], ebx |
jmp .item_parsed |
.unit_exponent: |
mov [edi+global_items.unit_exponent], ecx |
jmp .item_parsed |
.unit: |
mov [edi+global_items.unit], ebx |
jmp .item_parsed |
.report_size: |
mov [edi+global_items.report_size], ebx |
jmp .item_parsed |
.report_id: |
test ebx, ebx |
jz .invalid_report |
cmp ebx, 0x100 |
jae .invalid_report |
mov [edi+global_items.report_id], ebx |
jmp .item_parsed |
.report_count: |
mov [edi+global_items.report_count], ebx |
jmp .item_parsed |
; Two special global items: Push/Pop. |
.push: |
; For Push, allocate new global_items structure, |
; initialize from the current one and make it current. |
movi eax, sizeof.global_items |
invoke Kmalloc |
test eax, eax |
jz .memory_error |
push esi eax |
movi ecx, sizeof.global_items / 4 |
mov esi, edi |
xchg eax, edi |
rep movsd |
pop edi esi |
mov [edi+global_items.next], eax |
jmp .item_parsed |
.pop: |
; For Pop, restore the last global_items structure and free the current one. |
mov eax, [edi+global_items.next] |
test eax, eax |
jz .invalid_report |
push eax |
xchg eax, edi |
invoke Kfree |
pop edi |
jmp .item_parsed |
; -------------------------------- Local items -------------------------------- |
.parse_local: |
cmp eax, .num_local_items |
jae .item_parsed |
jmp dword [.local_jumps+eax*4] |
.usage: |
; Usage tag. |
; If length is 0, 1, 2 bytes, append the global item Usage Page. |
cmp [cur_item_size], 2 |
ja @f |
or ebx, [edi+global_items.usage_page] |
@@: |
; If inside Delimiter(), ignore everything except the first tag. |
cmp [delimiter_depth], 0 |
jz .usage.write |
inc [usage_variant] |
cmp [usage_variant], 1 |
jnz .item_parsed |
.usage.write: |
; Add new range with start = item data and length = 1. |
mov [usage_minimum], ebx |
push 1 |
.new_usage: |
movi eax, sizeof.usage_list_item |
invoke Kmalloc |
pop edx |
test eax, eax |
jz .memory_error |
inc [num_usage_ranges] |
mov ecx, [usage_minimum] |
and [eax+usage_list_item.next], 0 |
mov [eax+usage_list_item.first_usage], ecx |
mov [eax+usage_list_item.num_usages], edx |
mov ecx, [usage_tail] |
mov [usage_tail], eax |
mov [ecx], eax |
jmp .item_parsed |
.usage_minimum: |
; Usage Minimum tag. Just store in the local var. |
; If length is 0, 1, 2 bytes, append the global item Usage Page. |
cmp [cur_item_size], 2 |
ja @f |
or ebx, [edi+global_items.usage_page] |
@@: |
mov [usage_minimum], ebx |
jmp .item_parsed |
.usage_maximum: |
; Usage Maximum tag. |
; If length is 0, 1, 2 bytes, append the global item Usage Page. |
cmp [cur_item_size], 2 |
ja @f |
or ebx, [edi+global_items.usage_page] |
@@: |
; Meaningless inside Delimiter(). |
cmp [delimiter_depth], 0 |
jnz .invalid_report |
; Add new range with start = saved Usage Minimum and |
; length = Usage Maximum - Usage Minimum + 1. |
sub ebx, [usage_minimum] |
inc ebx |
push ebx |
jmp .new_usage |
.delimiter: |
; Delimiter tag. |
test ebx, ebx |
jz .delimiter.close |
; Delimiter(Opened). |
; Store that we are inside Delimiter(), |
; say a warning that only preferred Usage will be used. |
cmp [delimiter_depth], 0 |
jnz .invalid_report |
inc [delimiter_depth] |
push esi |
mov esi, delimiter_note |
invoke SysMsgBoardStr |
pop esi |
jmp .item_parsed |
.delimiter.close: |
; Delimiter(Closed). |
; Store that we are not inside Delimiter() anymore. |
dec [delimiter_depth] |
js .invalid_report |
and [usage_variant], 0 |
jmp .item_parsed |
.parse_reserved: |
; Ignore reserved items, except that tag 0xFE means long item |
; with first data byte = length of additional data, |
; second data byte = long item tag. No long items are defined yet, |
; so just skip them. |
cmp eax, 0xF |
jnz .item_parsed |
cmp [cur_item_size], 2 |
jnz .item_parsed |
movzx ecx, bl |
add esi, ecx |
cmp esi, [length] |
ja .invalid_report |
.item_parsed: |
cmp esi, [length] |
jb .fetch_next_item |
.parse_end: |
;-------------------------------- End of parsing ------------------------------ |
; If there are opened collections, it is invalid report. |
cmp [cur_collection], 0 |
jnz .invalid_report |
; There must be at least one input field. |
mov eax, [calldata] |
add eax, hid_data.input.first_report |
cmp [last_reports+0*4], eax |
jz .invalid_report |
; Everything is ok. |
inc [report_ok] |
jmp .end |
.memory_error: |
mov esi, nomemory_msg |
.end_str: |
invoke SysMsgBoardStr |
.end: |
; Free all global_items structures. |
test edi, edi |
jz @f |
push [edi+global_items.next] |
xchg eax, edi |
invoke Kfree |
pop edi |
jmp .end |
@@: |
; Free the last Usage list, if any. |
mov eax, [usage_list] |
@@: |
test eax, eax |
jz @f |
push [eax+usage_list_item.next] |
invoke Kfree |
pop eax |
jmp @b |
@@: |
} |
; Assign drivers to top-level HID collections. |
; The caller should provide ebx = pointer to hid_data and a local variable |
; [has_driver], it will be initialized with 0 if no driver is present. |
macro postprocess_descr |
{ |
postprocess_report_label: |
; Assign drivers to top-level collections. |
; Use mouse driver for Usage(GenericDesktop:Mouse), |
; use keyboard driver for Usage(GenericDesktop:Keyboard) |
; and Usage(GenericDesktop:Keypad) |
; 1. Prepare for the loop: get the pointer to the first collection, |
; store that no drivers were assigned yet. |
mov edi, [ebx+hid_data.first_collection] |
if ~HID_DUMP_UNCLAIMED |
mov [has_driver], 0 |
end if |
.next_collection: |
; 2. Test whether there is a collection to test; if no, break from the loop. |
test edi, edi |
jz .postprocess_done |
; 3. Get pointer to driver callbacks depending on [collection.usage]. |
; If [collection.usage] is unknown, use default driver if HID_DUMP_UNCLAIMED |
; and do not assign a driver otherwise. |
mov esi, mouse_driver |
cmp [edi+collection.usage], USAGE_GD_POINTER |
jz .has_driver |
cmp [edi+collection.usage], USAGE_GD_MOUSE |
jz .has_driver |
mov esi, keyboard_driver |
cmp [edi+collection.usage], USAGE_GD_KEYBOARD |
jz .has_driver |
cmp [edi+collection.usage], USAGE_GD_KEYPAD |
jz .has_driver |
if HID_DUMP_UNCLAIMED |
mov esi, default_driver |
else |
xor esi, esi |
end if |
; 4. If no driver is assigned (possible only if not HID_DUMP_UNCLAIMED), |
; go to 7 with driver data = 0; |
; other code uses this as a sign that driver callbacks should not be called. |
.has_driver: |
xor eax, eax |
if ~HID_DUMP_UNCLAIMED |
test esi, esi |
jz .set_driver |
end if |
; 5. Notify the driver about new device. |
call [esi+hid_driver_callbacks.add_device] |
; 6. If the driver has returned non-zero driver data, |
; store that is an assigned driver. |
; Otherwise, if HID_DUMP_UNCLAIMED, try to assign the default driver. |
if HID_DUMP_UNCLAIMED |
test eax, eax |
jnz .set_driver |
mov esi, default_driver |
call [esi+hid_driver_callbacks.add_device] |
else |
test eax, eax |
jz @f |
mov [has_driver], 1 |
jmp .set_driver |
@@: |
xor esi, esi |
end if |
.set_driver: |
; 7. Store driver data. If a driver is assigned, copy driver callbacks. |
mov [edi+collection.driver_data], eax |
test esi, esi |
jz @f |
push edi |
lodsd ; skip hid_driver_callbacks.add_device |
add edi, collection.callbacks |
repeat sizeof.hid_driver_active_callbacks / 4 |
movsd |
end repeat |
pop edi |
@@: |
; 8. Store pointer to the collection in all input reports belonging to it. |
; Note that the HID spec requires that reports should not cross top-level collections. |
mov eax, [edi+collection.input.first_report] |
test eax, eax |
jz .reports_processed |
.next_report: |
mov [eax+report.top_level_collection], edi |
cmp eax, [edi+collection.input.last_report] |
mov eax, [eax+report.next] |
jnz .next_report |
.reports_processed: |
mov edi, [edi+collection.next] |
jmp .next_collection |
.postprocess_done: |
} |
; Cleanup all resources allocated during parse_descr and postprocess_descr. |
; Called when the corresponding device is disconnected |
; with ebx = pointer to hid_data. |
macro hid_cleanup |
{ |
; 1. Notify all assigned drivers about disconnect. |
; Loop over all top-level collections and call callbacks.disconnect, |
; if a driver is assigned. |
mov esi, [ebx+hid_data.first_collection] |
.notify_drivers: |
test esi, esi |
jz .notify_drivers_done |
mov edi, [esi+collection.driver_data] |
test edi, edi |
jz @f |
call [esi+collection.callbacks.disconnect] |
@@: |
mov esi, [esi+collection.next] |
jmp .notify_drivers |
.notify_drivers_done: |
; 2. Free all collections. |
mov esi, [ebx+hid_data.first_collection] |
.free_collections: |
test esi, esi |
jz .collections_done |
; If a collection has childen, make it forget about them, |
; kill all children; after last child is killed, return to |
; the collection as a parent; this time, it will appear |
; as childless, so it will be killed after children. |
mov eax, [esi+collection.first_child] |
test eax, eax |
jz .no_children |
and [esi+collection.first_child], 0 |
xchg esi, eax |
jmp .free_collections |
.no_children: |
; If a collection has no children (maybe there were no children at all, |
; maybe all children were already killed), kill it and proceed either to |
; next sibling (if any) or to the parent. |
mov eax, [esi+collection.next] |
test eax, eax |
jnz @f |
mov eax, [esi+collection.parent] |
@@: |
xchg eax, esi |
invoke Kfree |
jmp .free_collections |
.collections_done: |
; 3. Free all three report sets. |
push 3 |
lea esi, [ebx+hid_data.input] |
; For every report set, loop over all reports, |
; for every report free all field groups, then free report itself. |
; When all reports in one set have been freed, free also report list table, |
; if there is one (reports are numbered). |
.report_set_loop: |
mov edi, [esi+report_set.first_report] |
.report_loop: |
test edi, edi |
jz .report_done |
mov eax, [edi+report.first_field] |
.field_loop: |
test eax, eax |
jz .field_done |
push [eax+report_field_group.next] |
invoke Kfree |
pop eax |
jmp .field_loop |
.field_done: |
mov eax, [edi+report.next] |
xchg eax, edi |
invoke Kfree |
jmp .report_loop |
.report_done: |
cmp [esi+report_set.numbered], 0 |
jz @f |
mov eax, [esi+report_set.data] |
invoke Kfree |
@@: |
add esi, sizeof.report_set |
dec dword [esp] |
jnz .report_set_loop |
pop eax |
} |
; Helper for parse_input. Extracts value of one field. |
; in: esi -> report_field_group |
; in: eax = offset in bits from report start |
; in: report -> report data |
; out: edx = value |
; Note: it can read one dword past report data. |
macro extract_field_value report |
{ |
mov ecx, eax |
shr eax, 5 |
shl eax, 2 |
add eax, report |
and ecx, 31 |
mov edx, [eax] |
mov eax, [eax+4] |
shrd edx, eax, cl |
mov ecx, [esi+report_field_group.sign_mask] |
and ecx, edx |
and edx, [esi+report_field_group.mask] |
sub edx, ecx |
} |
; Local variables for parse_input. |
macro parse_input_locals |
{ |
count_inside_group dd ? |
; Number of fields left in the current field. |
field_offset dd ? |
; Offset of the current field from report start, in bits. |
field_range_size dd ? |
; Size of range with valid values, Logical Maximum - Logical Minimum + 1. |
cur_usage dd ? |
; Pointer to current usage for Variable field groups. |
num_values dd ? |
; Number of values in the current instantiation of Array field group. |
values_base dd ? |
; Pointer to memory allocated for array with current values. |
values_prev dd ? |
; Pointer to memory allocated for array with previous values. |
values_cur_ptr dd ? |
; Pointer to the next value in [values_base] array. |
values_end dd ? |
; End of data in array with current values. |
values_prev_ptr dd ? |
; Pointer to the next value in [values_prev_ptr] array. |
values_prev_end dd ? |
; End of data in array with previous values. |
} |
; Parse input report. The caller should provide esi = pointer to report, |
; local variables parse_input_locals and [buffer] = report data. |
macro parse_input |
{ |
; 1. Ignore the report if there is no driver for it. |
mov ebx, [esi+report.top_level_collection] |
mov edi, [ebx+collection.driver_data] |
test edi, edi |
jz .done |
; 2. Notify the driver that a new packet arrived. |
call [ebx+collection.callbacks.begin_packet] |
; Loop over all field groups. |
; Report without fields is meaningless, but theoretically possible: |
; parse_descr does not create reports of zero size, but |
; a report can consist of "padding" fields without usages and have |
; no real fields. |
mov esi, [esi+report.first_field] |
test esi, esi |
jz .packet_processed |
.field_loop: |
; 3. Prepare for group handling: initialize field offset, fields count |
; and size of range for valid values. |
mov eax, [esi+report_field_group.offset] |
mov [field_offset], eax |
mov ecx, [esi+report_field_group.count] |
mov [count_inside_group], ecx |
mov eax, [esi+report_field_group.logical_maximum] |
inc eax |
sub eax, [esi+report_field_group.logical_minimum] |
mov [field_range_size], eax |
; 4. Select handler. Variable and Array groups are handled entirely differently; |
; for Variable groups, advance to 5, for Array groups, go to 6. |
test byte [esi+report_field_group.flags], HID_FIELD_VARIABLE |
jz .array_field |
; 5. Variable groups. They are simple. Loop over all .count fields, |
; for every field extract the value and get the next usage, |
; if the value is within valid range, call the driver. |
lea eax, [esi+report_field_group.common_sizeof] |
mov [cur_usage], eax |
.variable_data_loop: |
mov eax, [field_offset] |
extract_field_value [buffer] ; -> edx |
mov ecx, [cur_usage] |
mov ecx, [ecx] |
call [ebx+collection.callbacks.input_field] |
add [cur_usage], 4 |
mov eax, [esi+report_field_group.size] |
add [field_offset], eax |
dec [count_inside_group] |
jnz .variable_data_loop |
; Variable group is processed; go to 12. |
jmp .field_done |
.array_field: |
; Array groups. They are complicated. |
; 6. Array group: extract all values in one array. |
; memory was allocated during group creation, use it |
; 6a. Prepare: get data pointer, initialize num_values with zero. |
mov eax, [esi+report_field_group.num_usage_ranges] |
lea edx, [esi+report_field_group.usages+eax*sizeof.usage_range+4] |
mov eax, [esi+report_field_group.count] |
mov [values_cur_ptr], edx |
mov [values_base], edx |
lea edx, [edx+ecx*4] |
mov [values_prev], edx |
mov [values_prev_ptr], edx |
mov [num_values], 0 |
; 6b. Start loop for every field. Note that there must be at least one field, |
; parse_descr does not allow .count == 0. |
.array_getval_loop: |
; 6c. Extract the value of the current field. |
mov eax, [field_offset] |
extract_field_value [buffer] ; -> edx |
; 6d. Transform the value to the usage with binary search in array of |
; usage_ranges. started at [esi+report_field_group.usages] |
; having [esi+report_field_group.num_usage_ranges] items. |
; Ignore items outside of valid range. |
sub edx, [esi+report_field_group.logical_minimum] |
cmp edx, [field_range_size] |
jae .array_skip_item |
; If there are too few usages, use last of them. |
mov ecx, [esi+report_field_group.num_usage_ranges] ; upper bound |
xor eax, eax ; lower bound |
cmp edx, [esi+report_field_group.usages+ecx*sizeof.usage_range+usage_range.offset] |
jae .array_last_usage |
; loop invariant: usages[eax].offset <= edx < usages[ecx].offset |
.array_find_usage: |
lea edi, [eax+ecx] |
shr edi, 1 |
cmp edi, eax |
jz .array_found_usage_range |
cmp edx, [esi+report_field_group.usages+edi*sizeof.usage_range+usage_range.offset] |
jae .update_low |
mov ecx, edi |
jmp .array_find_usage |
.update_low: |
mov eax, edi |
jmp .array_find_usage |
.array_last_usage: |
lea eax, [ecx-1] |
mov edx, [esi+report_field_group.usages+ecx*sizeof.usage_range+usage_range.offset] |
dec edx |
.array_found_usage_range: |
sub edx, [esi+report_field_group.usages+eax*sizeof.usage_range+usage_range.offset] |
add edx, [esi+report_field_group.usages+eax*sizeof.usage_range+usage_range.first_usage] |
; 6e. Store the usage, advance data pointer, continue loop started at 6b. |
mov eax, [values_cur_ptr] |
mov [eax], edx |
add [values_cur_ptr], 4 |
inc [num_values] |
.array_skip_item: |
mov eax, [esi+report_field_group.size] |
add [field_offset], eax |
dec [count_inside_group] |
jnz .array_getval_loop |
; 7. Array group: ask driver about array overflow. |
; If driver says that the array is invalid, stop processing this group |
; (in particular, do not update previous values). |
mov ecx, [num_values] |
test ecx, ecx |
jz .duplicates_removed |
mov edx, [values_base] |
mov edi, [ebx+collection.driver_data] |
call [ebx+collection.callbacks.array_overflow?] |
jnc .field_done |
; 8. Array group: sort the array with current values. |
push esi |
mov ecx, [num_values] |
mov edx, [values_base] |
call sort |
pop esi |
; 9. Array group: remove duplicates. |
cmp [num_values], 1 |
jbe .duplicates_removed |
mov eax, [values_base] |
mov edx, [eax] |
add eax, 4 |
mov ecx, eax |
.duplicates_loop: |
cmp edx, [eax] |
jz @f |
mov edx, [eax] |
mov [ecx], edx |
add ecx, 4 |
@@: |
add eax, 4 |
cmp eax, [values_cur_ptr] |
jb .duplicates_loop |
mov [values_cur_ptr], ecx |
sub ecx, [values_base] |
shr ecx, 2 |
mov [num_values], ecx |
.duplicates_removed: |
; 10. Array group: compare current and previous values, |
; call driver for differences. |
mov edi, [ebx+collection.driver_data] |
mov eax, [values_cur_ptr] |
mov [values_end], eax |
mov eax, [values_base] |
mov [values_cur_ptr], eax |
mov eax, [esi+report_field_group.num_values_prev] |
shl eax, 2 |
add eax, [values_prev] |
mov [values_prev_end], eax |
.find_common: |
mov eax, [values_cur_ptr] |
cmp eax, [values_end] |
jae .cur_done |
mov ecx, [eax] |
mov eax, [values_prev_ptr] |
cmp eax, [values_prev_end] |
jae .prev_done |
mov edx, [eax] |
cmp ecx, edx |
jb .advance_cur |
ja .advance_prev |
; common item in both arrays; ignore |
add [values_cur_ptr], 4 |
add [values_prev_ptr], 4 |
jmp .find_common |
.advance_cur: |
; item is present in current array but not in previous; |
; call the driver with value = 1 |
add [values_cur_ptr], 4 |
mov edx, 1 |
call [ebx+collection.callbacks.input_field] |
jmp .find_common |
.advance_prev: |
; item is present in previous array but not in current; |
; call the driver with value = 0 |
add [values_prev_ptr], 4 |
mov ecx, edx |
xor edx, edx |
call [ebx+collection.callbacks.input_field] |
jmp .find_common |
.prev_done: |
; for all items which are left in current array |
; call the driver with value = 1 |
mov eax, [values_cur_ptr] |
@@: |
add [values_cur_ptr], 4 |
mov ecx, [eax] |
mov edx, 1 |
call [ebx+collection.callbacks.input_field] |
mov eax, [values_cur_ptr] |
cmp eax, [values_end] |
jb @b |
jmp .copy_array |
.cur_done: |
; for all items which are left in previous array |
; call the driver with value = 0 |
mov eax, [values_prev_ptr] |
add [values_prev_ptr], 4 |
cmp eax, [values_prev_end] |
jae @f |
mov ecx, [eax] |
xor edx, edx |
call [ebx+collection.callbacks.input_field] |
jmp .cur_done |
@@: |
.copy_array: |
; 11. Array group: copy current values to previous values. |
push esi edi |
mov ecx, [num_values] |
mov [esi+report_field_group.num_values_prev], ecx |
mov esi, [values_base] |
mov edi, [values_prev] |
rep movsd |
pop edi esi |
; 12. Field group is processed. Repeat with the next group, if any. |
.field_done: |
mov esi, [esi+report_field_group.next] |
test esi, esi |
jnz .field_loop |
.packet_processed: |
; 13. Packet is processed, notify the driver. |
call [ebx+collection.callbacks.end_packet] |
} |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbhid/usbhid.asm |
---|
0,0 → 1,552 |
; 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 |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbhid/sort.inc |
---|
0,0 → 1,60 |
; Sort array of unsigned dwords in non-decreasing order. |
; ecx = array size, edx = array pointer. |
; Destroys eax, ecx, esi, edi. |
sort: |
test ecx, ecx |
jz .done |
mov eax, ecx |
@@: |
push eax |
call .restore |
pop eax |
dec eax |
jnz @b |
@@: |
cmp ecx, 1 |
jz .done |
mov esi, 1 |
mov edi, ecx |
call .exchange |
dec ecx |
mov eax, 1 |
call .restore |
jmp @b |
.done: |
ret |
.exchange: |
push eax ecx |
mov eax, [edx+esi*4-4] |
mov ecx, [edx+edi*4-4] |
mov [edx+esi*4-4], ecx |
mov [edx+edi*4-4], eax |
pop ecx eax |
ret |
.restore: |
lea esi, [eax+eax] |
cmp esi, ecx |
ja .doner |
mov edi, [edx+eax*4-4] |
cmp [edx+esi*4-4], edi |
ja .need_xchg |
cmp esi, ecx |
jae .doner |
mov edi, [edx+eax*4-4] |
cmp [edx+esi*4], edi |
jbe .doner |
.need_xchg: |
cmp esi, ecx |
jz .do_xchg |
mov edi, [edx+esi*4-4] |
cmp [edx+esi*4], edi |
sbb esi, -1 |
.do_xchg: |
mov edi, eax |
call .exchange |
mov eax, esi |
jmp .restore |
.doner: |
ret |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbhid/unclaimed.inc |
---|
0,0 → 1,60 |
; HID default driver, part of USBHID driver. |
; Present only if compile-time setting HID_DUMP_UNCLAIMED is on. |
; Active for those devices when we do not have a specialized driver. |
; Just dumps everything to the debug board. |
if HID_DUMP_UNCLAIMED |
; Global constants. |
; They are assembled in a macro to separate code and data; |
; the code is located at the point of "include 'unclaimed.inc'", |
; the data are collected when workers_globals is instantiated. |
macro workers_globals |
{ |
; include global constants from previous workers |
workers_globals |
align 4 |
; Callbacks for HID layer. |
default_driver: |
dd default_driver_add_device |
dd default_driver_disconnect |
dd default_driver_begin_packet |
dd default_driver_array_overflow? |
dd default_driver_input_field |
dd default_driver_end_packet |
} |
; This procedure is called when HID layer detects a new driverless device. |
; in: ebx -> usb_device_data, edi -> collection |
; out: eax = device-specific data or NULL on error |
default_driver_add_device: |
; just return something nonzero, no matter what |
xor eax, eax |
inc eax |
ret |
; This procedure is called when HID layer processes every non-empty array field group. |
; in: edi -> keyboard_device_data (pointer returned from keyboard_driver_add_device) |
; in: ecx = fields count (always nonzero), edx = pointer to fields values |
; in: esi -> report_field_group |
; out: CF set => group is ok, CF cleared => group should be ignored |
default_driver_array_overflow?: |
; parse everything |
stc |
ret |
; This procedure is called from HID layer for every field. |
; in: ecx = field usage, edx = value, esi -> report_field_group |
default_driver_input_field: |
; Do not dump zero values in Variable fields, |
; they are present even if the corresponding control is inactive. |
test edx, edx |
jnz @f |
test byte [esi+report_field_group.flags], HID_FIELD_VARIABLE |
jnz .nodump |
@@: |
DEBUGF 1,'K : unclaimed HID input: usage=%x, value=%x\n',ecx,edx |
.nodump: |
; pass through |
; Three nothing-to-do procedures. |
default_driver_disconnect: |
default_driver_begin_packet: |
default_driver_end_packet: |
ret |
end if |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |
/drivers/usb/usbstor.asm |
---|
0,0 → 1,1599 |
; standard driver stuff; version of driver model = 5 |
format PE DLL native 0.05 |
entry START |
DEBUG = 1 |
DUMP_PACKETS = 0 |
; this is for DEBUGF macro from 'fdo.inc' |
__DEBUG__ = 1 |
__DEBUG_LEVEL__ = 1 |
include '../struct.inc' |
; 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 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 |
; Mass storage protocol constants, USB layer |
REQUEST_GETMAXLUN = 0xFE ; get max lun |
REQUEST_BORESET = 0xFF ; bulk-only reset |
; Mass storage protocol structures, USB layer |
; Sent from host to device in the first stage of an operation. |
struct command_block_wrapper |
Signature dd ? ; the constant 'USBC' |
Tag dd ? ; identifies response with request |
Length dd ? ; length of data-transport phase |
Flags db ? ; one of CBW_FLAG_* |
CBW_FLAG_OUT = 0 |
CBW_FLAG_IN = 80h |
LUN db ? ; addressed unit |
CommandLength db ? ; the length of the following field |
Command rb 16 |
ends |
; Sent from device to host in the last stage of an operation. |
struct command_status_wrapper |
Signature dd ? ; the constant 'USBS' |
Tag dd ? ; identifies response with request |
LengthRest dd ? ; .Length - (size of data which were transferred) |
Status db ? ; one of CSW_STATUS_* |
CSW_STATUS_OK = 0 |
CSW_STATUS_FAIL = 1 |
CSW_STATUS_FATAL = 2 |
ends |
; Constants of SCSI layer |
SCSI_REQUEST_SENSE = 3 |
SCSI_INQUIRY = 12h |
SCSI_READ_CAPACITY = 25h |
SCSI_READ10 = 28h |
SCSI_WRITE10 = 2Ah |
; Result of SCSI REQUEST SENSE command. |
SENSE_UNKNOWN = 0 |
SENSE_RECOVERED_ERROR = 1 |
SENSE_NOT_READY = 2 |
SENSE_MEDIUM_ERROR = 3 |
SENSE_HARDWARE_ERROR = 4 |
SENSE_ILLEGAL_REQUEST = 5 |
SENSE_UNIT_ATTENTION = 6 |
SENSE_DATA_PROTECT = 7 |
SENSE_BLANK_CHECK = 8 |
; 9 is vendor-specific |
SENSE_COPY_ABORTED = 10 |
SENSE_ABORTED_COMMAND = 11 |
SENSE_EQUAL = 12 |
SENSE_VOLUME_OVERFLOW = 13 |
SENSE_MISCOMPARE = 14 |
; 15 is reserved |
; Structures of SCSI layer |
; Result of SCSI INQUIRY request. |
struct inquiry_data |
PeripheralDevice db ? ; lower 5 bits are PeripheralDeviceType |
; upper 3 bits are PeripheralQualifier |
RemovableMedium db ? ; upper bit is RemovableMedium |
; other bits are for compatibility |
Version db ? ; lower 3 bits are ANSI-Approved version |
; next 3 bits are ECMA version |
; upper 2 bits are ISO version |
ResponseDataFormat db ? ; lower 4 bits are ResponseDataFormat |
; bit 6 is TrmIOP |
; bit 7 is AENC |
AdditionalLength db ? |
dw ? ; reserved |
Flags db ? |
VendorID rb 8 ; vendor ID, big-endian |
ProductID rb 16 ; product ID, big-endian |
ProductRevBE dd ? ; product revision, big-endian |
ends |
struct sense_data |
ErrorCode db ? ; lower 7 bits are error code: |
; 70h = current error, |
; 71h = deferred error |
; upper bit is InformationValid |
SegmentNumber db ? ; number of segment descriptor |
; for commands COPY [+VERIFY], COMPARE |
SenseKey db ? ; bits 0-3 are one of SENSE_* |
; bit 4 is reserved |
; bit 5 is IncorrectLengthIndicator |
; bits 6 and 7 are used by |
; sequential-access devices |
Information dd ? ; command-specific |
AdditionalLength db ? ; length of data starting here |
CommandInformation dd ? ; command-specific |
AdditionalSenseCode db ? ; \ more detailed error code |
AdditionalSenseQual db ? ; / standard has a large table of them |
FRUCode db ? ; which part of device has failed |
; (device-specific, not regulated) |
SenseKeySpecific rb 3 ; depends on SenseKey |
ends |
; Device data |
; USB Mass storage device has one or more logical units, identified by LUN, |
; logical unit number. The highest value of LUN, that is, number of units |
; minus 1, can be obtained via control request Get Max LUN. |
struct usb_device_data |
ConfigPipe dd ? ; configuration pipe |
OutPipe dd ? ; pipe for OUT bulk endpoint |
InPipe dd ? ; pipe for IN bulk endpoint |
MaxLUN dd ? ; maximum Logical Unit Number |
LogicalDevices dd ? ; pointer to array of usb_unit_data |
; 1 for a connected USB device, 1 for each disk device |
; the structure can be freed when .NumReferences decreases to zero |
NumReferences dd ? ; number of references |
ConfigRequest rb 8 ; buffer for configuration requests |
LengthRest dd ? ; Length - (size of data which were transferred) |
; All requests to a given device are serialized, |
; only one request to a given device can be processed at a time. |
; The current request and all pending requests are organized in the following |
; queue, the head being the current request. |
; NB: the queue must be device-wide due to the protocol: |
; data stage is not tagged (unlike command_*_wrapper), so the only way to know |
; what request the data are associated with is to guarantee that only one |
; request is processing at the time. |
RequestsQueue rd 2 |
QueueLock rd 3 ; protects .RequestsQueue |
InquiryData inquiry_data ; information about device |
; data for the current request |
Command command_block_wrapper |
DeviceDisconnected db ? |
Status command_status_wrapper |
Sense sense_data |
ends |
; Information about one logical device. |
struct usb_unit_data |
Parent dd ? ; pointer to parent usb_device_data |
LUN db ? ; index in usb_device_data.LogicalDevices array |
DiskIndex db ? ; for name "usbhd<index>" |
MediaPresent db ? |
db ? ; alignment |
DiskDevice dd ? ; handle of disk device or NULL |
SectorSize dd ? ; sector size |
; For some devices, the first request to the medium fails with 'unit not ready'. |
; When the code sees this status, it retries the command several times. |
; Two following variables track the retry count and total time for those; |
; total time is currently used only for debug output. |
UnitReadyAttempts dd ? |
TimerTicks dd ? |
ends |
; This is the structure for items in the queue usb_device_data.RequestsQueue. |
struct request_queue_item |
Next dd ? ; next item in the queue |
Prev dd ? ; prev item in the queue |
ReqBuilder dd ? ; procedure to fill command_block_wrapper |
Buffer dd ? ; input or output data |
; (length is command_block_wrapper.Length) |
Callback dd ? ; procedure to call in the end of transfer |
UserData dd ? ; passed as-is to .Callback |
; There are 3 possible stages of any request, one of them optional: |
; command stage (host sends command_block_wrapper to device), |
; optional data stage, |
; status stage (device sends command_status_wrapper to host). |
; Also, if a request fails, the code queues additional request |
; SCSI_REQUEST_SENSE; sense_data from SCSI_REQUEST_SENSE |
; contains some information about the error. |
Stage db ? |
ends |
section '.flat' code readable writable executable |
include '../proc32.inc' |
include '../peimport.inc' |
include '../fdo.inc' |
include '../macros.inc' |
; The start procedure. |
proc START |
virtual at esp |
dd ? ; return address |
.reason dd ? ; DRV_ENTRY or DRV_EXIT |
.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. Initialize: we have one global mutex. |
mov ecx, free_numbers_lock |
invoke MutexInit |
; 3. Register self as a USB driver. |
; The name is my_driver = 'usbstor'; IOCTL interface is not supported; |
; usb_functions is an offset of a structure with callback functions. |
invoke RegUSBDriver, my_driver, 0, usb_functions |
; 4. Return the returned value of RegUSBDriver. |
.nothing: |
ret |
endp |
; Helper procedures to work with requests queue. |
; Add a request to the queue. Stdcall with 5 arguments. |
proc queue_request |
push ebx esi |
virtual at esp |
rd 2 ; saved registers |
dd ? ; return address |
.device dd ? ; pointer to usb_device_data |
.ReqBuilder dd ? ; request_queue_item.ReqBuilder |
.Buffer dd ? ; request_queue_item.Buffer |
.Callback dd ? ; request_queue_item.Callback |
.UserData dd ? ; request_queue_item.UserData |
end virtual |
; 1. Allocate the memory for the request description. |
movi eax, sizeof.request_queue_item |
invoke Kmalloc |
test eax, eax |
jnz @f |
mov esi, nomemory |
invoke SysMsgBoardStr |
pop esi ebx |
ret 20 |
@@: |
; 2. Fill user-provided parts of the request description. |
push edi |
xchg eax, ebx |
lea esi, [.ReqBuilder+4] |
lea edi, [ebx+request_queue_item.ReqBuilder] |
movsd ; ReqBuilder |
movsd ; Buffer |
movsd ; Callback |
movsd ; UserData |
pop edi |
; 3. Set stage to zero: not started. |
mov [ebx+request_queue_item.Stage], 0 |
; 4. Lock the queue. |
mov esi, [.device] |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexLock |
; 5. Insert the request to the tail of the queue. |
add esi, usb_device_data.RequestsQueue |
mov edx, [esi+request_queue_item.Prev] |
mov [ebx+request_queue_item.Next], esi |
mov [ebx+request_queue_item.Prev], edx |
mov [edx+request_queue_item.Next], ebx |
mov [esi+request_queue_item.Prev], ebx |
; 6. Test whether the queue was empty |
; and the request should be started immediately. |
cmp [esi+request_queue_item.Next], ebx |
jnz .unlock |
; 8. If the step 6 shows that the request is the first in the queue, |
; start it. |
sub esi, usb_device_data.RequestsQueue |
call setup_request |
jmp .nothing |
.unlock: |
invoke MutexUnlock |
; 9. Return. |
.nothing: |
pop esi ebx |
ret 20 |
endp |
; The current request is completed. Call the callback, |
; remove the request from the queue, start the next |
; request if there is one. |
; esi points to usb_device_data |
proc complete_request |
; 1. Print common debug messages on fails. |
if DEBUG |
cmp [esi+usb_device_data.Status.Status], CSW_STATUS_FAIL |
jb .normal |
jz .fail |
DEBUGF 1, 'K : Fatal error during execution of command %x\n', [esi+usb_device_data.Command.Command]:2 |
jmp .normal |
.fail: |
DEBUGF 1, 'K : Command %x failed\n', [esi+usb_device_data.Command.Command]:2 |
.normal: |
end if |
; 2. Get the current request. |
mov ebx, [esi+usb_device_data.RequestsQueue+request_queue_item.Next] |
; 3. Call the callback. |
stdcall [ebx+request_queue_item.Callback], esi, [ebx+request_queue_item.UserData] |
; 4. Lock the queue. |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexLock |
; 5. Remove the request. |
lea edx, [esi+usb_device_data.RequestsQueue] |
mov eax, [ebx+request_queue_item.Next] |
mov [eax+request_queue_item.Prev], edx |
mov [edx+request_queue_item.Next], eax |
; 6. Free the request memory. |
push eax edx |
xchg eax, ebx |
invoke Kfree |
pop edx ebx |
; 7. If there is a next request, start processing. |
cmp ebx, edx |
jnz setup_request |
; 8. Unlock the queue and return. |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexUnlock |
ret |
endp |
; Start processing the request. Called either by queue_request |
; or when the previous request has been processed. |
; Do not call directly, use queue_request. |
; Must be called when queue is locked; unlocks the queue when returns. |
proc setup_request |
xor eax, eax |
; 1. If DeviceDisconnected has been run, then all handles of pipes |
; are invalid, so we must fail immediately. |
; (That is why this function needs the locked queue: this |
; guarantee that either DeviceDisconnected has been already run, or |
; DeviceDisconnected will not return before the queue is unlocked.) |
cmp [esi+usb_device_data.DeviceDisconnected], al |
jnz .fatal |
; 2. If the previous command has encountered a fatal error, |
; perform reset recovery. |
cmp [esi+usb_device_data.Status.Status], CSW_STATUS_FATAL |
jb .norecovery |
; 2a. Send Bulk-Only Mass Storage Reset command to config pipe. |
lea edx, [esi+usb_device_data.ConfigRequest] |
mov word [edx], (REQUEST_BORESET shl 8) + 21h ; class request |
mov word [edx+6], ax ; length = 0 |
invoke USBControlTransferAsync, [esi+usb_device_data.ConfigPipe], edx, eax, eax, recovery_callback1, esi, eax |
; 2b. Fail here = fatal error. |
test eax, eax |
jz .fatal |
; 2c. Otherwise, unlock the queue and return. recovery_callback1 will continue processing. |
.unlock_return: |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexUnlock |
ret |
.norecovery: |
; 3. Send the command. Fail (no memory or device disconnected) = fatal error. |
; Otherwise, go to 2c. |
call request_stage1 |
test eax, eax |
jnz .unlock_return |
.fatal: |
; 4. Fatal error. Set status = FATAL, unlock the queue, complete the request. |
mov [esi+usb_device_data.Status.Status], CSW_STATUS_FATAL |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexUnlock |
jmp complete_request |
endp |
; Initiate USB transfer for the first stage of a request (send command). |
proc request_stage1 |
mov ebx, [esi+usb_device_data.RequestsQueue+request_queue_item.Next] |
; 1. Set the stage to 1 = command stage. |
inc [ebx+request_queue_item.Stage] |
; 2. Generate the command. Zero-initialize and use the caller-provided proc. |
lea edx, [esi+usb_device_data.Command] |
xor eax, eax |
mov [edx+command_block_wrapper.CommandLength], 12 |
mov dword [edx+command_block_wrapper.Command], eax |
mov dword [edx+command_block_wrapper.Command+4], eax |
mov dword [edx+command_block_wrapper.Command+8], eax |
mov dword [edx+command_block_wrapper.Command+12], eax |
inc [edx+command_block_wrapper.Tag] |
stdcall [ebx+request_queue_item.ReqBuilder], edx, [ebx+request_queue_item.UserData] |
; 4. Initiate USB transfer. |
lea edx, [esi+usb_device_data.Command] |
if DUMP_PACKETS |
DEBUGF 1,'K : USBSTOR out:' |
mov eax, edx |
mov ecx, sizeof.command_block_wrapper |
call debug_dump |
DEBUGF 1,'\n' |
end if |
invoke USBNormalTransferAsync, [esi+usb_device_data.OutPipe], edx, sizeof.command_block_wrapper, request_callback1, esi, 0 |
test eax, eax |
jz .nothing |
; 5. If the next stage is data stage in the same direction, enqueue it here. |
cmp [esi+usb_device_data.Command.Flags], 0 |
js .nothing |
cmp [esi+usb_device_data.Command.Length], 0 |
jz .nothing |
mov edx, [esi+usb_device_data.RequestsQueue+request_queue_item.Next] |
if DUMP_PACKETS |
DEBUGF 1,'K : USBSTOR out:' |
mov eax, [edx+request_queue_item.Buffer] |
mov ecx, [esi+usb_device_data.Command.Length] |
call debug_dump |
DEBUGF 1,'\n' |
end if |
invoke USBNormalTransferAsync, [esi+usb_device_data.OutPipe], [edx+request_queue_item.Buffer], [esi+usb_device_data.Command.Length], request_callback2, esi, 0 |
.nothing: |
ret |
endp |
if DUMP_PACKETS |
proc debug_dump |
test ecx, ecx |
jz .done |
.loop: |
test ecx, 0Fh |
jnz @f |
DEBUGF 1,'\nK :' |
@@: |
DEBUGF 1,' %x',[eax]:2 |
inc eax |
dec ecx |
jnz .loop |
.done: |
ret |
endp |
end if |
; Called when the Reset command is completed, |
; either successfully or not. |
proc recovery_callback1 |
virtual at esp |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
cmp [.status], 0 |
jnz .error |
; todo: reset pipes |
push ebx esi |
mov esi, [.calldata+8] |
call request_stage1 |
pop esi ebx |
test eax, eax |
jz .error |
ret 20 |
.error: |
DEBUGF 1, 'K : error %d while resetting', [.status+24h] |
jmp request_callback1.common_error |
endp |
; Called when the first stage of request is completed, |
; either successfully or not. |
proc request_callback1 |
virtual at esp |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
; 1. Initialize. |
mov ecx, [.calldata] |
mov eax, [.status] |
; 2. Test for error. |
test eax, eax |
jnz .error |
; No error. |
; 3. Increment the stage. |
mov edx, [ecx+usb_device_data.RequestsQueue+request_queue_item.Next] |
inc [edx+request_queue_item.Stage] |
; 4. Check whether we need to send the data. |
; 4a. If there is no data, skip this stage. |
cmp [ecx+usb_device_data.Command.Length], 0 |
jz ..request_get_status |
; 4b. If data were enqueued in the first stage, do nothing, wait for request_callback2. |
cmp [ecx+usb_device_data.Command.Flags], 0 |
jns .nothing |
; 5. Initiate USB transfer. If this fails, go to the error handler. |
invoke USBNormalTransferAsync, [ecx+usb_device_data.InPipe], [edx+request_queue_item.Buffer], [ecx+usb_device_data.Command.Length], request_callback2, ecx, 0 |
test eax, eax |
jz .error |
; 6. The status stage goes to the same direction, enqueue it now. |
mov ecx, [.calldata] |
jmp ..enqueue_status |
.nothing: |
ret 20 |
.error: |
; Error. |
; 7. Print debug message and complete the request as failed. |
DEBUGF 1,'K : error %d after %d bytes in request stage\n',eax,[.length+24h] |
; If device is disconnected and data stage is enqueued, do nothing; |
; data stage callback will do everything. |
cmp eax, 16 |
jnz .common_error |
cmp [ecx+usb_device_data.Command.Flags], 0 |
js .common_error |
cmp [ecx+usb_device_data.Command.Length], 0 |
jz .common_error |
ret 20 |
.common_error: |
; TODO: add recovery after STALL |
mov ecx, [.calldata] |
mov [ecx+usb_device_data.Status.Status], CSW_STATUS_FATAL |
push ebx esi |
mov esi, ecx |
call complete_request |
pop esi ebx |
ret 20 |
endp |
; Called when the second stage of request is completed, |
; either successfully or not. |
proc request_callback2 |
virtual at esp |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
if DUMP_PACKETS |
mov eax, [.calldata] |
mov eax, [eax+usb_device_data.InPipe] |
cmp [.pipe], eax |
jnz @f |
DEBUGF 1,'K : USBSTOR in:' |
push eax ecx |
mov eax, [.buffer+8] |
mov ecx, [.length+8] |
call debug_dump |
pop ecx eax |
DEBUGF 1,'\n' |
@@: |
end if |
; 1. Initialize. |
mov ecx, [.calldata] |
mov eax, [.status] |
; 2. Test for error. |
test eax, eax |
jnz .error |
; No error. |
; If the previous stage was in same direction, do nothing; status request is already enqueued. |
cmp [ecx+usb_device_data.Command.Flags], 0 |
js .nothing |
..request_get_status: |
; 3. Increment the stage. |
mov edx, [ecx+usb_device_data.RequestsQueue+request_queue_item.Next] |
inc [edx+request_queue_item.Stage] |
; 4. Initiate USB transfer. If this fails, go to the error handler. |
..enqueue_status: |
lea edx, [ecx+usb_device_data.Status] |
invoke USBNormalTransferAsync, [ecx+usb_device_data.InPipe], edx, sizeof.command_status_wrapper, request_callback3, ecx, 0 |
test eax, eax |
jz .error |
.nothing: |
ret 20 |
.error: |
; Error. |
; 5. Print debug message and complete the request as failed. |
DEBUGF 1,'K : error %d after %d bytes in data stage\n',eax,[.length+24h] |
; If device is disconnected and data stage is enqueued, do nothing; |
; status stage callback will do everything. |
cmp [ecx+usb_device_data.Command.Flags], 0 |
js .nothing |
jmp request_callback1.common_error |
endp |
; Called when the third stage of request is completed, |
; either successfully or not. |
proc request_callback3 |
virtual at esp |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
if DUMP_PACKETS |
DEBUGF 1,'K : USBSTOR in:' |
mov eax, [.buffer] |
mov ecx, [.length] |
call debug_dump |
DEBUGF 1,'\n' |
end if |
; 1. Initialize. |
mov eax, [.status] |
; 2. Test for error. |
test eax, eax |
jnz .transfer_error |
; Transfer is OK. |
; 3. Validate the status. Invalid status = fatal error. |
push ebx esi |
mov esi, [.calldata+8] |
mov ebx, [esi+usb_device_data.RequestsQueue+request_queue_item.Next] |
cmp [esi+usb_device_data.Status.Signature], 'USBS' |
jnz .invalid |
mov eax, [esi+usb_device_data.Command.Tag] |
cmp [esi+usb_device_data.Status.Tag], eax |
jnz .invalid |
cmp [esi+usb_device_data.Status.Status], CSW_STATUS_FATAL |
ja .invalid |
; 4. The status block is valid. Check the status code. |
jz .complete |
; 5. If this command was not REQUEST_SENSE, copy status data to safe place. |
; Otherwise, the original command has failed, so restore the fail status. |
cmp byte [esi+usb_device_data.Command.Command], SCSI_REQUEST_SENSE |
jz .request_sense |
mov eax, [esi+usb_device_data.Status.LengthRest] |
mov [esi+usb_device_data.LengthRest], eax |
cmp [esi+usb_device_data.Status.Status], CSW_STATUS_FAIL |
jz .fail |
.complete: |
call complete_request |
.nothing: |
pop esi ebx |
ret 20 |
.request_sense: |
mov [esi+usb_device_data.Status.Status], CSW_STATUS_FAIL |
jmp .complete |
.invalid: |
; 6. Invalid status block. Say error, set status to fatal and complete request. |
push esi |
mov esi, invresponse |
invoke SysMsgBoardStr |
pop esi |
mov [esi+usb_device_data.Status.Status], CSW_STATUS_FATAL |
jmp .complete |
.fail: |
; 7. The command has failed. |
; If this command was not REQUEST_SENSE, schedule the REQUEST_SENSE command |
; to determine the reason of fail. Otherwise, assume that there is no error data. |
cmp [esi+usb_device_data.Command.Command], SCSI_REQUEST_SENSE |
jz .fail_request_sense |
mov [ebx+request_queue_item.ReqBuilder], request_sense_req |
lea eax, [esi+usb_device_data.Sense] |
mov [ebx+request_queue_item.Buffer], eax |
call request_stage1 |
test eax, eax |
jnz .nothing |
.fail_request_sense: |
DEBUGF 1,'K : fail during REQUEST SENSE\n' |
mov byte [esi+usb_device_data.Sense], 0 |
jmp .complete |
.transfer_error: |
; TODO: add recovery after STALL |
DEBUGF 1,'K : error %d after %d bytes in status stage\n',eax,[.length+24h] |
jmp request_callback1.common_error |
endp |
; Builder for SCSI_REQUEST_SENSE request. |
; edx = first argument = pointer to usb_device_data.Command, |
; second argument = custom data given to queue_request (ignored). |
proc request_sense_req |
mov [edx+command_block_wrapper.Length], sizeof.sense_data |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_IN |
mov byte [edx+command_block_wrapper.Command+0], SCSI_REQUEST_SENSE |
mov byte [edx+command_block_wrapper.Command+4], sizeof.sense_data |
ret 8 |
endp |
; This procedure is called when new mass-storage device is detected. |
; It initializes the device. |
; Technically, initialization implies sending several USB queries, |
; so it is split in several procedures. The first is AddDevice, |
; other are callbacks which will be called at some time in the future, |
; when the device will respond. |
; The general scheme: |
; * AddDevice parses descriptors, opens pipes; if everything is ok, |
; AddDevice sends REQUEST_GETMAXLUN with callback known_lun_callback; |
; * known_lun_callback allocates memory for LogicalDevices and sends |
; SCSI_TEST_UNIT_READY to all logical devices with test_unit_ready_callback; |
; * test_unit_ready_callback checks whether the unit is ready; |
; if not, it repeats the same request several times; |
; if ok or there were too many attempts, it sends SCSI_INQUIRY with |
; callback inquiry_callback; |
; * inquiry_callback checks that a logical device is a block device |
; and the unit was ready; if so, it notifies the kernel about new disk device. |
proc AddDevice |
push ebx esi |
virtual at esp |
rd 2 ; saved registers ebx, esi |
dd ? ; return address |
.pipe0 dd ? ; handle of the config pipe |
.config dd ? ; pointer to config_descr |
.interface dd ? ; pointer to interface_descr |
end virtual |
; 1. Check device type. Currently only SCSI-command-set Bulk-only devices |
; are supported. |
; 1a. Get the subclass and the protocol. Since bInterfaceSubClass and |
; bInterfaceProtocol are subsequent in interface_descr, just one |
; memory reference is used for both. |
mov esi, [.interface] |
xor ebx, ebx |
mov cx, word [esi+interface_descr.bInterfaceSubClass] |
; 1b. For Mass-storage SCSI-command-set Bulk-only devices subclass must be 6 |
; and protocol must be 50h. Check. |
cmp cx, 0x5006 |
jz .known |
; There are devices with subclass 5 which use the same protocol 50h. |
; The difference is not important for the code except for this test, |
; so allow them to proceed also. |
cmp cx, 0x5005 |
jz .known |
; 1c. If the device is unknown, print a message and go to 11c. |
mov esi, unkdevice |
invoke SysMsgBoardStr |
jmp .nothing |
; 1d. If the device uses known command set, print a message and continue |
; configuring. |
.known: |
push esi |
mov esi, okdevice |
invoke SysMsgBoardStr |
pop esi |
; 2. Allocate memory for internal device data. |
; 2a. Call the kernel. |
mov eax, sizeof.usb_device_data |
invoke Kmalloc |
; 2b. Check return value. |
test eax, eax |
jnz @f |
; 2c. If failed, say a message and go to 11c. |
mov esi, nomemory |
invoke SysMsgBoardStr |
jmp .nothing |
@@: |
; 2d. If succeeded, zero the contents and continue configuring. |
xchg ebx, eax ; ebx will point to usb_device_data |
xor eax, eax |
mov [ebx+usb_device_data.OutPipe], eax |
mov [ebx+usb_device_data.InPipe], eax |
mov [ebx+usb_device_data.MaxLUN], eax |
mov [ebx+usb_device_data.LogicalDevices], eax |
mov dword [ebx+usb_device_data.ConfigRequest], eax |
mov dword [ebx+usb_device_data.ConfigRequest+4], eax |
mov [ebx+usb_device_data.Status.Status], al |
mov [ebx+usb_device_data.DeviceDisconnected], al |
; 2e. There is one reference: a connected USB device. |
inc eax |
mov [ebx+usb_device_data.NumReferences], eax |
; 2f. Save handle of configuration pipe for reset recovery. |
mov eax, [.pipe0] |
mov [ebx+usb_device_data.ConfigPipe], eax |
; 2g. Save the interface number for configuration requests. |
mov al, [esi+interface_descr.bInterfaceNumber] |
mov [ebx+usb_device_data.ConfigRequest+4], al |
; 2h. Initialize common fields in command wrapper. |
mov [ebx+usb_device_data.Command.Signature], 'USBC' |
mov [ebx+usb_device_data.Command.Tag], 'xxxx' |
; 2i. Initialize requests queue. |
lea eax, [ebx+usb_device_data.RequestsQueue] |
mov [eax+request_queue_item.Next], eax |
mov [eax+request_queue_item.Prev], eax |
lea ecx, [ebx+usb_device_data.QueueLock] |
invoke MutexInit |
; Bulk-only mass storage devices use one OUT bulk endpoint for sending |
; command/data and one IN bulk endpoint for receiving data/status. |
; Look for those endpoints. |
; 3. Get the upper bound of all descriptors' data. |
mov edx, [.config] ; configuration descriptor |
movzx ecx, [edx+config_descr.wTotalLength] |
add edx, ecx |
; 4. 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 both endpoint descriptors found. |
; 4a. Loop start: esi points to the interface descriptor, |
.lookep: |
; 4b. Get next descriptor. |
movzx ecx, byte [esi] ; the first byte of all descriptors is length |
add esi, ecx |
; 4c. Check that at least two bytes are readable. The opposite is an error. |
inc esi |
cmp esi, edx |
jae .errorep |
dec esi |
; 4d. Check that this descriptor is not interface descriptor. The opposite is |
; an error. |
cmp byte [esi+endpoint_descr.bDescriptorType], INTERFACE_DESCR_TYPE |
jz .errorep |
; 4e. Test whether this descriptor is an endpoint descriptor. If not, continue |
; the loop. |
cmp byte [esi+endpoint_descr.bDescriptorType], ENDPOINT_DESCR_TYPE |
jnz .lookep |
; 5. Check that the descriptor contains all required data and all data are |
; readable. The opposite is an error. |
cmp byte [esi+endpoint_descr.bLength], sizeof.endpoint_descr |
jb .errorep |
lea ecx, [esi+sizeof.endpoint_descr] |
cmp ecx, edx |
ja .errorep |
; 6. Check that the endpoint is bulk endpoint. The opposite is an error. |
mov cl, [esi+endpoint_descr.bmAttributes] |
and cl, 3 |
cmp cl, BULK_PIPE |
jnz .errorep |
; 7. Get the direction of this endpoint. |
movzx ecx, [esi+endpoint_descr.bEndpointAddress] |
shr ecx, 7 |
; 8. Test whether a pipe for this direction is already opened. If so, continue |
; the loop. |
cmp [ebx+usb_device_data.OutPipe+ecx*4], 0 |
jnz .lookep |
; 9. Open pipe for this endpoint. |
; 9a. Save registers. |
push ecx edx |
; 9b. Load parameters from the descriptor. |
movzx ecx, [esi+endpoint_descr.bEndpointAddress] |
movzx edx, [esi+endpoint_descr.wMaxPacketSize] |
movzx eax, [esi+endpoint_descr.bInterval] ; not used for USB1, may be important for USB2 |
; 9c. Call the kernel. |
invoke USBOpenPipe, [ebx+usb_device_data.ConfigPipe], ecx, edx, BULK_PIPE, eax |
; 9d. Restore registers. |
pop edx ecx |
; 9e. Check result. If failed, go to 11b. |
test eax, eax |
jz .free |
; 9f. Save result. |
mov [ebx+usb_device_data.OutPipe+ecx*4], eax |
; 10. Test whether the second pipe is already opened. If not, continue loop. |
xor ecx, 1 |
cmp [ebx+usb_device_data.OutPipe+ecx*4], 0 |
jz .lookep |
jmp .created |
; 11. An error occured during processing endpoint descriptor. |
.errorep: |
; 11a. Print a message. |
DEBUGF 1,'K : error: invalid endpoint descriptor\n' |
.free: |
; 11b. Free the allocated usb_device_data. |
xchg eax, ebx |
invoke Kfree |
.nothing: |
; 11c. Return an error. |
xor eax, eax |
jmp .return |
.created: |
; 12. Pipes are opened. Send GetMaxLUN control request. |
lea eax, [ebx+usb_device_data.ConfigRequest] |
mov byte [eax], 0A1h ; class request from interface |
mov byte [eax+1], REQUEST_GETMAXLUN |
mov byte [eax+6], 1 ; transfer 1 byte |
lea ecx, [ebx+usb_device_data.MaxLUN] |
if DUMP_PACKETS |
DEBUGF 1,'K : GETMAXLUN: %x %x %x %x %x %x %x %x\n',[eax]:2,[eax+1]:2,[eax+2]:2,[eax+3]:2,[eax+4]:2,[eax+5]:2,[eax+6]:2,[eax+7]:2 |
end if |
invoke USBControlTransferAsync, [ebx+usb_device_data.ConfigPipe], eax, ecx, 1, known_lun_callback, ebx, 0 |
; 13. Return with pointer to device data as returned value. |
xchg eax, ebx |
.return: |
pop esi ebx |
ret 12 |
endp |
; This function is called when REQUEST_GETMAXLUN is done, |
; either successful or unsuccessful. |
proc known_lun_callback |
push ebx esi |
virtual at esp |
rd 2 ; saved registers |
dd ? ; return address |
.pipe dd ? |
.status dd ? |
.buffer dd ? |
.length dd ? |
.calldata dd ? |
end virtual |
; 1. Check the status. If the request failed, assume that MaxLUN is zero. |
mov ebx, [.calldata] |
mov eax, [.status] |
test eax, eax |
jz @f |
DEBUGF 1, 'K : GETMAXLUN failed with status %d, assuming zero\n', eax |
mov [ebx+usb_device_data.MaxLUN], 0 |
@@: |
; 2. Allocate the memory for logical devices. |
mov eax, [ebx+usb_device_data.MaxLUN] |
inc eax |
DEBUGF 1,'K : %d logical unit(s)\n',eax |
imul eax, sizeof.usb_unit_data |
push ebx |
invoke Kmalloc |
pop ebx |
; If failed, print a message and do nothing. |
test eax, eax |
jnz @f |
mov esi, nomemory |
invoke SysMsgBoardStr |
pop esi ebx |
ret 20 |
@@: |
mov [ebx+usb_device_data.LogicalDevices], eax |
; 3. Initialize logical devices and initiate TEST_UNIT_READY request. |
xchg esi, eax |
xor ecx, ecx |
.looplun: |
mov [esi+usb_unit_data.Parent], ebx |
mov [esi+usb_unit_data.LUN], cl |
xor eax, eax |
mov [esi+usb_unit_data.MediaPresent], al |
mov [esi+usb_unit_data.DiskDevice], eax |
mov [esi+usb_unit_data.SectorSize], eax |
mov [esi+usb_unit_data.UnitReadyAttempts], eax |
push ecx |
invoke GetTimerTicks |
mov [esi+usb_unit_data.TimerTicks], eax |
stdcall queue_request, ebx, test_unit_ready_req, 0, test_unit_ready_callback, esi |
pop ecx |
inc ecx |
add esi, sizeof.usb_unit_data |
cmp ecx, [ebx+usb_device_data.MaxLUN] |
jbe .looplun |
; 4. Return. |
pop esi ebx |
ret 20 |
endp |
; Builder for SCSI INQUIRY request. |
; edx = first argument = pointer to usb_device_data.Command, |
; second argument = custom data given to queue_request. |
proc inquiry_req |
mov eax, [esp+8] |
mov al, [eax+usb_unit_data.LUN] |
mov [edx+command_block_wrapper.Length], sizeof.inquiry_data |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_IN |
mov [edx+command_block_wrapper.LUN], al |
mov byte [edx+command_block_wrapper.Command+0], SCSI_INQUIRY |
mov byte [edx+command_block_wrapper.Command+4], sizeof.inquiry_data |
ret 8 |
endp |
; Called when SCSI INQUIRY request is completed. |
proc inquiry_callback |
; 1. Check the status. |
mov ecx, [esp+4] |
cmp [ecx+usb_device_data.Status.Status], CSW_STATUS_OK |
jnz .fail |
; 2. The command has completed successfully. |
; Print a message showing device type, ignore anything but block devices. |
mov al, [ecx+usb_device_data.InquiryData.PeripheralDevice] |
and al, 1Fh |
DEBUGF 1,'K : peripheral device type is %x\n',al |
test al, al |
jnz .nothing |
DEBUGF 1,'K : direct-access mass storage device detected\n' |
; 3. We have found a new disk device. Increment number of references. |
lock inc [ecx+usb_device_data.NumReferences] |
; Unfortunately, we are now in the context of the USB thread, |
; so we can't notify the kernel immediately: it would try to do something |
; with a new disk, those actions would be synchronous and would require |
; waiting for results of USB requests, but we need to exit this callback |
; to allow the USB thread to continue working and handling those requests. |
; 4. Thus, create a temporary kernel thread which would do it. |
mov edx, [esp+8] |
push ebx ecx esi edi |
movi ebx, 1 |
mov ecx, new_disk_thread |
; edx = parameter |
invoke CreateThread |
pop edi esi ecx ebx |
cmp eax, -1 |
jnz .nothing |
; on error, reverse step 3 |
lock dec [ecx+usb_device_data.NumReferences] |
.nothing: |
ret 8 |
.fail: |
; 4. The command has failed. Print a message and do nothing. |
push esi |
mov esi, inquiry_fail |
invoke SysMsgBoardStr |
pop esi |
ret 8 |
endp |
; Builder for SCSI TEST_UNIT_READY request. |
; edx = first argument = pointer to usb_device_data.Command, |
; second argument = custom data given to queue_request. |
proc test_unit_ready_req |
mov eax, [esp+8] |
mov al, [eax+usb_unit_data.LUN] |
mov [edx+command_block_wrapper.Length], 0 |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_IN |
mov [edx+command_block_wrapper.LUN], al |
ret 8 |
endp |
; Called when SCSI TEST_UNIT_READY request is completed. |
proc test_unit_ready_callback |
virtual at esp |
dd ? ; return address |
.device dd ? |
.calldata dd ? |
end virtual |
; 1. Check the status. |
mov ecx, [.device] |
mov edx, [.calldata] |
cmp [ecx+usb_device_data.Status.Status], CSW_STATUS_OK |
jnz .fail |
; 2. The command has completed successfully, |
; possibly after some repetitions. Print a debug message showing |
; number and time of those. Remember that media is ready and go to 4. |
DEBUGF 1,'K : media is ready\n' |
invoke GetTimerTicks |
sub eax, [edx+usb_unit_data.TimerTicks] |
DEBUGF 1,'K : %d attempts, %d ticks\n',[edx+usb_unit_data.UnitReadyAttempts],eax |
inc [edx+usb_unit_data.MediaPresent] |
jmp .inquiry |
.fail: |
; 3. The command has failed. |
; Retry the same request up to 3 times with 10ms delay; |
; if limit of retries is not reached, exit from the function. |
; Otherwise, go to 4. |
inc [edx+usb_unit_data.UnitReadyAttempts] |
cmp [edx+usb_unit_data.UnitReadyAttempts], 3 |
jz @f |
push ecx edx esi |
movi esi, 10 |
invoke Sleep |
pop esi edx ecx |
stdcall queue_request, ecx, test_unit_ready_req, 0, test_unit_ready_callback, edx |
ret 8 |
@@: |
DEBUGF 1,'K : media not ready\n' |
.inquiry: |
; 4. initiate INQUIRY request. |
lea eax, [ecx+usb_device_data.InquiryData] |
stdcall queue_request, ecx, inquiry_req, eax, inquiry_callback, edx |
ret 8 |
endp |
; Temporary thread for initial actions with a new disk device. |
proc new_disk_thread |
sub esp, 32 |
virtual at esp |
.name rb 32 ; device name |
.param dd ? ; contents of edx at the moment of int 0x40/eax=51 |
dd ? ; stack segment |
end virtual |
; We are ready to notify the kernel about a new disk device. |
mov esi, [.param] |
; 1. Generate name. |
; 1a. Find a free index. |
mov ecx, free_numbers_lock |
invoke MutexLock |
xor eax, eax |
@@: |
bsf edx, [free_numbers+eax] |
jnz @f |
add eax, 4 |
cmp eax, 4*4 |
jnz @b |
invoke MutexUnlock |
push esi |
mov esi, noindex |
invoke SysMsgBoardStr |
pop esi |
jmp .drop_reference |
@@: |
; 1b. Mark the index as busy. |
btr [free_numbers+eax], edx |
lea eax, [eax*8+edx] |
push eax |
invoke MutexUnlock |
pop eax |
; 1c. Generate a name of the form "usbhd<index>" in the stack. |
mov dword [esp], 'usbh' |
lea edi, [esp+5] |
mov byte [edi-1], 'd' |
push eax |
push -'0' |
movi ecx, 10 |
@@: |
cdq |
div ecx |
push edx |
test eax, eax |
jnz @b |
@@: |
pop eax |
add al, '0' |
stosb |
jnz @b |
pop ecx |
mov edx, esp |
; 3d. Store the index in usb_unit_data to free it later. |
mov [esi+usb_unit_data.DiskIndex], cl |
; 4. Notify the kernel about a new disk. |
; 4a. Add a disk. |
; stdcall queue_request, ecx, read_capacity_req, eax, read_capacity_callback, eax |
invoke DiskAdd, disk_functions, edx, esi, 0 |
mov ebx, eax |
; 4b. If it failed, release the index and do nothing. |
test eax, eax |
jz .free_index |
; 4c. Notify the kernel that a media is present. |
invoke DiskMediaChanged, eax, 1 |
; 5. Lock the requests queue, check that device is not disconnected, |
; store the disk handle, unlock the requests queue. |
mov ecx, [esi+usb_unit_data.Parent] |
add ecx, usb_device_data.QueueLock |
invoke MutexLock |
cmp byte [ecx+usb_device_data.DeviceDisconnected-usb_device_data.QueueLock], 0 |
jnz .disconnected |
mov [esi+usb_unit_data.DiskDevice], ebx |
invoke MutexUnlock |
jmp .exit |
.disconnected: |
invoke MutexUnlock |
stdcall disk_close, ebx |
jmp .exit |
.free_index: |
mov ecx, free_numbers_lock |
invoke MutexLock |
movzx eax, [esi+usb_unit_data.DiskIndex] |
bts [free_numbers], eax |
invoke MutexUnlock |
.drop_reference: |
mov esi, [esi+usb_unit_data.Parent] |
lock dec [esi+usb_device_data.NumReferences] |
jnz .exit |
mov eax, [esi+usb_device_data.LogicalDevices] |
invoke Kfree |
xchg eax, esi |
invoke Kfree |
.exit: |
or eax, -1 |
int 0x40 |
endp |
; This function is called when the device is disconnected. |
proc DeviceDisconnected |
push ebx esi |
virtual at esp |
rd 2 ; saved registers |
dd ? ; return address |
.device dd ? |
end virtual |
; 1. Say a message. |
mov esi, disconnectmsg |
invoke SysMsgBoardStr |
; 2. Lock the requests queue, set .DeviceDisconnected to 1, |
; unlock the requests queue. |
; Locking is required for synchronization with queue_request: |
; all USB callbacks are executed in the same thread and are |
; synchronized automatically, but queue_request can be running |
; from any thread which wants to do something with a filesystem. |
; Without locking, it would be possible that queue_request has |
; been started, has checked that device is not yet disconnected, |
; then DeviceDisconnected completes and all handles become invalid, |
; then queue_request tries to use them. |
mov esi, [.device] |
lea ecx, [esi+usb_device_data.QueueLock] |
invoke MutexLock |
mov [esi+usb_device_data.DeviceDisconnected], 1 |
invoke MutexUnlock |
; 3. Drop one reference to the structure and check whether |
; that was the last reference. |
lock dec [esi+usb_device_data.NumReferences] |
jz .free |
; 4. If not, there are some additional references due to disk devices; |
; notify the kernel that those disks are deleted. |
; Note that new disks cannot be added while we are looping here, |
; because new_disk_thread checks for .DeviceDisconnected. |
mov ebx, [esi+usb_device_data.MaxLUN] |
mov esi, [esi+usb_device_data.LogicalDevices] |
inc ebx |
.diskdel: |
mov eax, [esi+usb_unit_data.DiskDevice] |
test eax, eax |
jz @f |
invoke DiskDel, eax |
@@: |
add esi, sizeof.usb_unit_data |
dec ebx |
jnz .diskdel |
; In this case, some operations with those disks are still possible, |
; so we can't do anything more now. disk_close will take care of the rest. |
.return: |
pop esi ebx |
ret 4 |
; 5. If there are no disk devices, free all resources which were allocated. |
.free: |
mov eax, [esi+usb_device_data.LogicalDevices] |
test eax, eax |
jz @f |
invoke Kfree |
@@: |
xchg eax, esi |
invoke Kfree |
jmp .return |
endp |
; Disk functions. |
DISK_STATUS_OK = 0 ; success |
DISK_STATUS_GENERAL_ERROR = -1; if no other code is suitable |
DISK_STATUS_INVALID_CALL = 1 ; invalid input parameters |
DISK_STATUS_NO_MEDIA = 2 ; no media present |
DISK_STATUS_END_OF_MEDIA = 3 ; end of media while reading/writing data |
; Called when all operations with the given disk are done. |
proc disk_close |
push ebx esi |
virtual at esp |
rd 2 ; saved registers |
dd ? ; return address |
.userdata dd ? |
end virtual |
mov esi, [.userdata] |
mov ecx, free_numbers_lock |
invoke MutexLock |
movzx eax, [esi+usb_unit_data.DiskIndex] |
bts [free_numbers], eax |
invoke MutexUnlock |
mov esi, [esi+usb_unit_data.Parent] |
lock dec [esi+usb_device_data.NumReferences] |
jnz .nothing |
mov eax, [esi+usb_device_data.LogicalDevices] |
invoke Kfree |
xchg eax, esi |
invoke Kfree |
.nothing: |
pop esi ebx |
ret 4 |
endp |
; Returns sector size, capacity and flags of the media. |
proc disk_querymedia stdcall uses ebx esi edi, \ |
userdata:dword, mediainfo:dword |
; 1. Create event for waiting. |
xor esi, esi |
xor ecx, ecx |
invoke CreateEvent |
test eax, eax |
jz .generic_fail |
push eax |
push edx |
push ecx |
push 0 |
push 0 |
virtual at ebp-.localsize |
.locals: |
; two following dwords are the output of READ_CAPACITY |
.LastLBABE dd ? |
.SectorSizeBE dd ? |
.Status dd ? |
; two following dwords identify an event |
.event_code dd ? |
.event dd ? |
rd 3 ; saved registers |
.localsize = $ - .locals |
dd ? ; saved ebp |
dd ? ; return address |
.userdata dd ? |
.mediainfo dd ? |
end virtual |
; 2. Initiate SCSI READ_CAPACITY request. |
mov eax, [userdata] |
mov ecx, [eax+usb_unit_data.Parent] |
mov edx, esp |
stdcall queue_request, ecx, read_capacity_req, edx, read_capacity_callback, edx |
; 3. Wait for event. This destroys it. |
mov eax, [.event] |
mov ebx, [.event_code] |
invoke WaitEvent |
; 4. Get the status and results. |
pop ecx |
bswap ecx ; .LastLBA |
pop edx |
bswap edx ; .SectorSize |
pop eax ; .Status |
; 5. If the request has completed successfully, store results. |
test eax, eax |
jnz @f |
DEBUGF 1,'K : sector size is %d, last sector is %d\n',edx,ecx |
mov ebx, [mediainfo] |
mov [ebx], eax ; flags = 0 |
mov [ebx+4], edx ; sectorsize |
add ecx, 1 |
adc eax, 0 |
mov [ebx+8], ecx |
mov [ebx+12], eax ; capacity |
mov eax, [userdata] |
mov [eax+usb_unit_data.SectorSize], edx |
xor eax, eax |
@@: |
; 6. Restore the stack and return. |
pop ecx |
pop ecx |
ret |
.generic_fail: |
or eax, -1 |
ret |
endp |
; Builder for SCSI READ_CAPACITY request. |
; edx = first argument = pointer to usb_device_data.Command, |
; second argument = custom data given to queue_request, |
; pointer to disk_querymedia.locals. |
proc read_capacity_req |
mov eax, [esp+8] |
mov eax, [eax+disk_querymedia.userdata-disk_querymedia.locals] |
mov al, [eax+usb_unit_data.LUN] |
mov [edx+command_block_wrapper.Length], 8 |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_IN |
mov [edx+command_block_wrapper.LUN], al |
mov byte [edx+command_block_wrapper.Command+0], SCSI_READ_CAPACITY |
ret 8 |
endp |
; Called when SCSI READ_CAPACITY request is completed. |
proc read_capacity_callback |
; Transform the status to return value of disk_querymedia |
; and set the event. |
mov ecx, [esp+4] |
xor eax, eax |
cmp [ecx+usb_device_data.Status.Status], al |
jz @f |
or eax, -1 |
@@: |
mov ecx, [esp+8] |
mov [ecx+disk_querymedia.Status-disk_querymedia.locals], eax |
push ebx esi edi |
mov eax, [ecx+disk_querymedia.event-disk_querymedia.locals] |
mov ebx, [ecx+disk_querymedia.event_code-disk_querymedia.locals] |
xor edx, edx |
xor esi, esi |
invoke RaiseEvent |
pop edi esi ebx |
ret 8 |
endp |
disk_write: |
mov al, SCSI_WRITE10 |
jmp disk_read_write |
disk_read: |
mov al, SCSI_READ10 |
; Reads from the device or writes to the device. |
proc disk_read_write stdcall uses ebx esi edi, \ |
userdata:dword, buffer:dword, startsector:qword, numsectors:dword |
; 1. Initialize. |
push eax ; .command |
mov eax, [userdata] |
mov eax, [eax+usb_unit_data.SectorSize] |
push eax ; .SectorSize |
push 0 ; .processed |
mov eax, [numsectors] |
mov eax, [eax] |
; 2. The transfer length for SCSI_{READ,WRITE}10 commands can not be greater |
; than 0xFFFF, so split the request to slices with <= 0xFFFF sectors. |
max_sectors_at_time = 0xFFFF |
.split: |
push eax ; .length_rest |
cmp eax, max_sectors_at_time |
jb @f |
mov eax, max_sectors_at_time |
@@: |
sub [esp], eax |
push eax ; .length_cur |
; 3. startsector must fit in 32 bits, otherwise abort the request. |
cmp dword [startsector+4], 0 |
jnz .generic_fail |
; 4. Create event for waiting. |
xor esi, esi |
xor ecx, ecx |
invoke CreateEvent |
test eax, eax |
jz .generic_fail |
push eax ; .event |
push edx ; .event_code |
push ecx ; .status |
virtual at ebp-.localsize |
.locals: |
.status dd ? |
.event_code dd ? |
.event dd ? |
.length_cur dd ? |
.length_rest dd ? |
.processed dd ? |
.SectorSize dd ? |
.command db ? |
rb 3 |
rd 3 ; saved registers |
.localsize = $ - .locals |
dd ? ; saved ebp |
dd ? ; return address |
.userdata dd ? |
.buffer dd ? |
.startsector dq ? |
.numsectors dd ? |
end virtual |
; 5. Initiate SCSI READ10 or WRITE10 request. |
mov eax, [userdata] |
mov ecx, [eax+usb_unit_data.Parent] |
stdcall queue_request, ecx, read_write_req, [buffer], read_write_callback, esp |
; 6. Wait for event. This destroys it. |
mov eax, [.event] |
mov ebx, [.event_code] |
invoke WaitEvent |
; 7. Get the status. If the operation has failed, abort. |
pop eax ; .status |
pop ecx ecx ; cleanup .event_code, .event |
pop ecx ; .length_cur |
test eax, eax |
jnz .return |
; 8. Otherwise, continue the loop started at step 2. |
add dword [startsector], ecx |
adc dword [startsector+4], eax |
imul ecx, [.SectorSize] |
add [buffer], ecx |
pop eax |
test eax, eax |
jnz .split |
push eax |
.return: |
; 9. Restore the stack, store .processed to [numsectors], return. |
pop ecx ; .length_rest |
pop ecx ; .processed |
mov edx, [numsectors] |
mov [edx], ecx |
pop ecx ; .SectorSize |
pop ecx ; .command |
ret |
.generic_fail: |
or eax, -1 |
pop ecx ; .length_cur |
jmp .return |
endp |
; Builder for SCSI READ10 or WRITE10 request. |
; edx = first argument = pointer to usb_device_data.Command, |
; second argument = custom data given to queue_request, |
; pointer to disk_read_write.locals. |
proc read_write_req |
mov eax, [esp+8] |
mov ecx, [eax+disk_read_write.userdata-disk_read_write.locals] |
mov cl, [ecx+usb_unit_data.LUN] |
mov [edx+command_block_wrapper.LUN], cl |
mov ecx, [eax+disk_read_write.length_cur-disk_read_write.locals] |
imul ecx, [eax+disk_read_write.SectorSize-disk_read_write.locals] |
mov [edx+command_block_wrapper.Length], ecx |
mov cl, [eax+disk_read_write.command-disk_read_write.locals] |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_OUT |
cmp cl, SCSI_READ10 |
jnz @f |
mov [edx+command_block_wrapper.Flags], CBW_FLAG_IN |
@@: |
mov byte [edx+command_block_wrapper.Command], cl |
mov ecx, dword [eax+disk_read_write.startsector-disk_read_write.locals] |
bswap ecx |
mov dword [edx+command_block_wrapper.Command+2], ecx |
mov ecx, [eax+disk_read_write.length_cur-disk_read_write.locals] |
xchg cl, ch |
mov word [edx+command_block_wrapper.Command+7], cx |
ret 8 |
endp |
; Called when SCSI READ10 or WRITE10 request is completed. |
proc read_write_callback |
; 1. Initialize. |
push ebx esi edi |
virtual at esp |
rd 3 ; saved registers |
dd ? ; return address |
.device dd ? |
.calldata dd ? |
end virtual |
mov ecx, [.device] |
mov esi, [.calldata] |
; 2. Get the number of sectors which were read. |
; If the status is OK or FAIL, the field .LengthRest is valid. |
; Otherwise, it is invalid, so assume zero sectors. |
xor eax, eax |
cmp [ecx+usb_device_data.Status.Status], CSW_STATUS_FAIL |
ja .sectors_calculated |
mov eax, [ecx+usb_device_data.LengthRest] |
xor edx, edx |
div [esi+disk_read_write.SectorSize-disk_read_write.locals] |
test edx, edx |
jz @f |
inc eax |
@@: |
mov edx, eax |
mov eax, [esi+disk_read_write.length_cur-disk_read_write.locals] |
sub eax, edx |
jae .sectors_calculated |
xor eax, eax |
.sectors_calculated: |
; 3. Increase the total number of processed sectors. |
add [esi+disk_read_write.processed-disk_read_write.locals], eax |
; 4. Set status to OK if all sectors were read, to ERROR otherwise. |
cmp eax, [esi+disk_read_write.length_cur-disk_read_write.locals] |
setz al |
movzx eax, al |
dec eax |
mov [esi+disk_read_write.status-disk_read_write.locals], eax |
; 5. Set the event. |
mov eax, [esi+disk_read_write.event-disk_read_write.locals] |
mov ebx, [esi+disk_read_write.event_code-disk_read_write.locals] |
xor edx, edx |
xor esi, esi |
invoke RaiseEvent |
; 6. Return. |
pop edi esi ebx |
ret 8 |
endp |
; strings |
my_driver db 'usbstor',0 |
disconnectmsg db 'K : USB mass storage device disconnected',13,10,0 |
nomemory db 'K : no memory',13,10,0 |
unkdevice db 'K : unknown mass storage device',13,10,0 |
okdevice db 'K : USB mass storage device detected',13,10,0 |
transfererror db 'K : USB transfer error, disabling mass storage',13,10,0 |
invresponse db 'K : invalid response from mass storage device',13,10,0 |
fatalerr db 'K : mass storage device reports fatal error',13,10,0 |
inquiry_fail db 'K : INQUIRY command failed',13,10,0 |
;read_capacity_fail db 'K : READ CAPACITY command failed',13,10,0 |
;read_fail db 'K : READ command failed',13,10,0 |
noindex db 'K : failed to generate disk name',13,10,0 |
align 4 |
; Structure with callback functions. |
usb_functions: |
dd usb_functions_end - usb_functions |
dd AddDevice |
dd DeviceDisconnected |
usb_functions_end: |
disk_functions: |
dd disk_functions_end - disk_functions |
dd disk_close |
dd 0 ; closemedia |
dd disk_querymedia |
dd disk_read |
dd disk_write |
dd 0 ; flush |
dd 0 ; adjust_cache_size: use default cache |
disk_functions_end: |
data fixups |
end data |
free_numbers_lock rd 3 |
; 128 devices should be enough for everybody |
free_numbers dd -1, -1, -1, -1 |
; for DEBUGF macro |
include_debug_strings |
Property changes: |
Added: svn:eol-style |
+native |
\ No newline at end of property |