Last week I got a Varmilo VA87 RGB (it's exactly the same as the VA87M, except for the RGB lightning). I chose it because of the great build quality (among the best for prebuilt keyboards). And also for the aesthetics (this is more subjective, but at least for me its design is truly gorgeous).
The Varmilo is awesome but I've found some firmware issues with NKRO. I've been researching them during the past days and would like to share my findings.
My unit is a VA87 RGB and is using the latest firmware available (version 1.4 from September 2020). I guess my findings would also apply to the VA87M and VA88M units, since these are pretty much identical to mine.
There are 3 usual ways for implementing NKRO in USB keyboards. This reference describe these 3 methods:
https://github.com/qmk/qmk_firmware/blob/master/docs/usb_nkro.txt. The Varmilo implements USB NKRO using a mix of the first and third methods. It appears to the OS as 2 keyboards. The first one implements the boot keyboard, so it's limited to 8 control keys plus 6 normal keys. The second implements a full NKRO keyboard, using a bitmap report. This second keyboard only sends events when you press more than 6 keys at once.
The Varmilo appears to the OS as 4 USB interfaces. One implements the boot keyboard (with 6KRO), one the second keyboard (with full NKRO) and the other 2 are used (I think) for firmware updates.
We can see, on a Linux machine, the Varmilo's USB configurations, interfaces and endpoints:
lsusb -d 04d9:8008 -v
Bus 005 Device 110: ID 04d9:8008 Holtek Semiconductor, Inc.
Device Descriptor:
bLength 18
bDescriptorType 1
bcdUSB 1.10
bDeviceClass 0
bDeviceSubClass 0
bDeviceProtocol 0
bMaxPacketSize0 64
idVendor 0x04d9 Holtek Semiconductor, Inc.
idProduct 0x8008
bcdDevice 1.04
iManufacturer 1 HOLTEK
iProduct 2 USB-HID Keyboard
iSerial 3 AP0000000003
bNumConfigurations 1
Configuration Descriptor:
bLength 9
bDescriptorType 2
wTotalLength 0x007b
bNumInterfaces 4
bConfigurationValue 1
iConfiguration 0
bmAttributes 0xa0
(Bus Powered)
Remote Wakeup
MaxPower 500mA
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 1 Boot Interface Subclass
bInterfaceProtocol 1 Keyboard
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 59
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0008 1x 8 bytes
bInterval 1
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 1
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 0
bInterfaceProtocol 0
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 34
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x83 EP 3 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x04 EP 4 OUT
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 2
bAlternateSetting 0
bNumEndpoints 1
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 0
bInterfaceProtocol 0
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 181
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x82 EP 2 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
Interface Descriptor:
bLength 9
bDescriptorType 4
bInterfaceNumber 3
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 3 Human Interface Device
bInterfaceSubClass 0
bInterfaceProtocol 0
iInterface 0
HID Device Descriptor:
bLength 9
bDescriptorType 33
bcdHID 1.11
bCountryCode 0 Not supported
bNumDescriptors 1
bDescriptorType 34 Report
wDescriptorLength 31
Report Descriptors:
** UNAVAILABLE **
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x85 EP 5 IN
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
Endpoint Descriptor:
bLength 7
bDescriptorType 5
bEndpointAddress 0x06 EP 6 OUT
bmAttributes 3
Transfer Type Interrupt
Synch Type None
Usage Type Data
wMaxPacketSize 0x0040 1x 64 bytes
bInterval 1
can't get debug descriptor: Resource temporarily unavailable
Device Status: 0x0000
(Bus Powered)
These are the HID USB report descriptors for the Varmilo. There are 4 descriptors, one for each USB interface.
usbhid-dump -d 04d9:8008
005:110:003:DESCRIPTOR 1608293927.780856
06 01 FF 09 01 A1 01 15 00 26 FF 00 75 08 95 40
09 20 81 02 09 21 91 02 09 22 95 08 B1 02 C0
005:110:002:DESCRIPTOR 1608293927.782320
05 01 09 02 A1 01 85 01 09 01 A1 00 05 09 15 00
25 01 19 01 29 05 75 01 95 05 81 02 95 03 81 01
05 01 16 01 80 26 FF 7F 09 30 09 31 75 10 95 02
81 06 15 81 25 7F 09 38 75 08 95 01 81 06 05 0C
0A 38 02 95 01 81 06 C0 C0 05 01 09 80 A1 01 85
02 19 81 29 83 15 00 25 01 75 01 95 03 81 02 95
05 81 01 C0 05 0C 09 01 A1 01 85 03 19 00 2A FF
07 15 00 26 FF 07 95 01 75 10 81 00 C0 06 02 FF
09 01 A1 01 85 04 15 00 26 FF 00 09 03 75 08 95
03 81 00 C0 05 01 09 06 A1 01 85 05 05 07 95 01
75 08 81 03 95 E8 75 01 15 00 25 01 05 07 19 00
29 E7 81 00 C0
005:110:001:DESCRIPTOR 1608293927.783889
06 00 FF 09 01 A1 01 09 02 15 00 26 FF 00 75 08
95 40 81 02 09 03 15 00 26 FF 00 75 08 95 40 91
02 C0
005:110:000:DESCRIPTOR 1608293927.784818
05 01 09 06 A1 01 05 08 15 00 25 01 19 01 29 03
75 01 95 03 91 02 95 05 91 01 05 07 19 E0 29 E7
75 01 95 08 81 02 95 08 81 01 15 00 26 FF 00 19
00 2A FF 00 75 08 95 06 81 00 C0
We are gonna decode the USB descriptors, to get something more meaningful.
This is the interface #0 descriptor (the one that implements the first keyboard). It's similar to the USB HID boot keyboard report specification. We see it sends an 8-bit bitmap for the 8 modifier keys and a 6-field array for 6 non-modifier keys.
hid-decode /dev/hidraw1
0x05, 0x01, // Usage Page (Generic Desktop) 0
0x09, 0x06, // Usage (Keyboard) 2
0xa1, 0x01, // Collection (Application) 4
0x05, 0x08, // Usage Page (LEDs) 6
0x15, 0x00, // Logical Minimum (0) 8
0x25, 0x01, // Logical Maximum (1) 10
0x19, 0x01, // Usage Minimum (1) 12
0x29, 0x03, // Usage Maximum (3) 14
0x75, 0x01, // Report Size (1) 16
0x95, 0x03, // Report Count (3) 18
0x91, 0x02, // Output (Data,Var,Abs) 20
0x95, 0x05, // Report Count (5) 22
0x91, 0x01, // Output (Cnst,Arr,Abs) 24
0x05, 0x07, // Usage Page (Keyboard) 26
0x19, 0xe0, // Usage Minimum (224) 28
0x29, 0xe7, // Usage Maximum (231) 30
0x75, 0x01, // Report Size (1) 32
0x95, 0x08, // Report Count (8) 34
0x81, 0x02, // Input (Data,Var,Abs) 36
0x95, 0x08, // Report Count (8) 38
0x81, 0x01, // Input (Cnst,Arr,Abs) 40
0x15, 0x00, // Logical Minimum (0) 42
0x26, 0xff, 0x00, // Logical Maximum (255) 44
0x19, 0x00, // Usage Minimum (0) 47
0x2a, 0xff, 0x00, // Usage Maximum (255) 49
0x75, 0x08, // Report Size (8) 52
0x95, 0x06, // Report Count (6) 54
0x81, 0x00, // Input (Data,Arr,Abs) 56
0xc0, // End Collection 58
This is the report for USB interface #2. This one is more complex. It cointains 5 different HID reports. The "System Control" report implements the Wake computer function (i.e. the computer wakes from sleeping mode when pressing any key of the keyboard). The "Consumer Control" report implements the media controls. We can see a "Vendor defined" report, which I don't know what it's used for. And even a HID mouse report (although the Varmilo doesn't implement, AFAIK, any mouse function).
hid-decode /dev/hidraw3
0x05, 0x01, // Usage Page (Generic Desktop) 0
0x09, 0x02, // Usage (Mouse) 2
0xa1, 0x01, // Collection (Application) 4
0x85, 0x01, // Report ID (1) 6
0x09, 0x01, // Usage (Pointer) 8
0xa1, 0x00, // Collection (Physical) 10
0x05, 0x09, // Usage Page (Button) 12
0x15, 0x00, // Logical Minimum (0) 14
0x25, 0x01, // Logical Maximum (1) 16
0x19, 0x01, // Usage Minimum (1) 18
0x29, 0x05, // Usage Maximum (5) 20
0x75, 0x01, // Report Size (1) 22
0x95, 0x05, // Report Count (5) 24
0x81, 0x02, // Input (Data,Var,Abs) 26
0x95, 0x03, // Report Count (3) 28
0x81, 0x01, // Input (Cnst,Arr,Abs) 30
0x05, 0x01, // Usage Page (Generic Desktop) 32
0x16, 0x01, 0x80, // Logical Minimum (-32767) 34
0x26, 0xff, 0x7f, // Logical Maximum (32767) 37
0x09, 0x30, // Usage (X) 40
0x09, 0x31, // Usage (Y) 42
0x75, 0x10, // Report Size (16) 44
0x95, 0x02, // Report Count (2) 46
0x81, 0x06, // Input (Data,Var,Rel) 48
0x15, 0x81, // Logical Minimum (-127) 50
0x25, 0x7f, // Logical Maximum (127) 52
0x09, 0x38, // Usage (Wheel) 54
0x75, 0x08, // Report Size (8) 56
0x95, 0x01, // Report Count (1) 58
0x81, 0x06, // Input (Data,Var,Rel) 60
0x05, 0x0c, // Usage Page (Consumer Devices) 62
0x0a, 0x38, 0x02, // Usage (AC Pan) 64
0x95, 0x01, // Report Count (1) 67
0x81, 0x06, // Input (Data,Var,Rel) 69
0xc0, // End Collection 71
0xc0, // End Collection 72
0x05, 0x01, // Usage Page (Generic Desktop) 73
0x09, 0x80, // Usage (System Control) 75
0xa1, 0x01, // Collection (Application) 77
0x85, 0x02, // Report ID (2) 79
0x19, 0x81, // Usage Minimum (129) 81
0x29, 0x83, // Usage Maximum (131) 83
0x15, 0x00, // Logical Minimum (0) 85
0x25, 0x01, // Logical Maximum (1) 87
0x75, 0x01, // Report Size (1) 89
0x95, 0x03, // Report Count (3) 91
0x81, 0x02, // Input (Data,Var,Abs) 93
0x95, 0x05, // Report Count (5) 95
0x81, 0x01, // Input (Cnst,Arr,Abs) 97
0xc0, // End Collection 99
0x05, 0x0c, // Usage Page (Consumer Devices) 100
0x09, 0x01, // Usage (Consumer Control) 102
0xa1, 0x01, // Collection (Application) 104
0x85, 0x03, // Report ID (3) 106
0x19, 0x00, // Usage Minimum (0) 108
0x2a, 0xff, 0x07, // Usage Maximum (2047) 110
0x15, 0x00, // Logical Minimum (0) 113
0x26, 0xff, 0x07, // Logical Maximum (2047) 115
0x95, 0x01, // Report Count (1) 118
0x75, 0x10, // Report Size (16) 120
0x81, 0x00, // Input (Data,Arr,Abs) 122
0xc0, // End Collection 124
0x06, 0x02, 0xff, // Usage Page (Vendor Usage Page 0xff02) 125
0x09, 0x01, // Usage (Vendor Usage 0x01) 128
0xa1, 0x01, // Collection (Application) 130
0x85, 0x04, // Report ID (4) 132
0x15, 0x00, // Logical Minimum (0) 134
0x26, 0xff, 0x00, // Logical Maximum (255) 136
0x09, 0x03, // Usage (Vendor Usage 0x03) 139
0x75, 0x08, // Report Size (8) 141
0x95, 0x03, // Report Count (3) 143
0x81, 0x00, // Input (Data,Arr,Abs) 145
0xc0, // End Collection 147
0x05, 0x01, // Usage Page (Generic Desktop) 148
0x09, 0x06, // Usage (Keyboard) 150
0xa1, 0x01, // Collection (Application) 152
0x85, 0x05, // Report ID (5) 154
0x05, 0x07, // Usage Page (Keyboard) 156
0x95, 0x01, // Report Count (1) 158
0x75, 0x08, // Report Size (8) 160
0x81, 0x03, // Input (Cnst,Var,Abs) 162
0x95, 0xe8, // Report Count (232) 164
0x75, 0x01, // Report Size (1) 166
0x15, 0x00, // Logical Minimum (0) 168
0x25, 0x01, // Logical Maximum (1) 170
0x05, 0x07, // Usage Page (Keyboard) 172
0x19, 0x00, // Usage Minimum (0) 174
0x29, 0xe7, // Usage Maximum (231) 176
0x81, 0x00, // Input (Data,Arr,Abs) 178
0xc0, // End Collection 180
The #5 report (Report ID 5) is the one that implements the NKRO keyboard and the truly relevant for the issue in question. We see the Report Size is 1 (1 bit), and the Report Count 232. I.e, the keyboard will be sending a 232-bit bitmap (29 bytes in total). But the HID item is declared as "Input (Data,Arr,Abs)". And here is the problem. In the USB HID specification (
https://usb.org/sites/default/files/hid1_11.pdf), we can see, on page 30, the following definitions for "Array" and "Variable":
Indicates whether the item creates variable or array data fields in reports.
In variable fields, each field represents data from a physical control.
The number of bits reserved for each field is determined by preceding Report Size/ReportCount items.
For example, a bank of eight on/off switches could be reported in 1 byte declared by a variable Input item where each bit represents one switch,on(1)or off (0) (Report Size = 1, Report Count = .
Alternatively, a variable Input item could add 1 report byte used to represent the state of four three-position buttons, where the state of each button is represented by two bits (Report Size = 2, Report Count = 4).
Or 1 byte from a variable Input item could represent the x position of a joystick (Report Size = 8, Report Count =1).
An array provides an alternate means for describing the data returned from a group of buttons.
Arrays are more efficient, if less flexible than variable items.
Rather than returning a single bit for each button in the group, an array returns an index in each field that corresponds to the pressed button (like keyboard scan codes).
An out-of range value in and array field is considered no controls asserted.
Buttons or keys in an array that are simultaneously pressed need to be reported in multiple fields.
Therefore, the number of fields in an array input item (Report Count) dictates the maximum number of simultaneous controls that can be reported.
A keyboard could report up to three simultaneous keys using an array with three 8-bit fields (Report Size = 8, Report Count = 3).
This is. In USB HID parlance, a "Variable" item is a bitmap, in which each bit asserts the state of every control or key. Instead an "Array" is a series of fields, each one with the key or control value codes. The Varmilo, on the wire, is sending a 232-bit bitmap (each one asserting the status of 1 key, so we have a 232-key limit. As the keyboard has less than 232 keys, the USB HID reporting format is actually NKRO). But the USB HID report descriptor is wrong.
It contains this part:
0x95, 0xe8, // Report Count (232) 164
0x75, 0x01, // Report Size (1) 166
0x15, 0x00, // Logical Minimum (0) 168
0x25, 0x01, // Logical Maximum (1) 170
0x05, 0x07, // Usage Page (Keyboard) 172
0x19, 0x00, // Usage Minimum (0) 174
0x29, 0xe7, // Usage Maximum (231) 176
0x81, 0x00, // Input (Data,Arr,Abs) 178
0xc0, // End Collection 180
But it should be this instead:
0x95, 0xe8, // Report Count (232) 164
0x75, 0x01, // Report Size (1) 166
0x15, 0x00, // Logical Minimum (0) 168
0x25, 0x01, // Logical Maximum (1) 170
0x05, 0x07, // Usage Page (Keyboard) 172
0x19, 0x00, // Usage Minimum (0) 174
0x29, 0xe7, // Usage Maximum (231) 176
0x81, 0x02, // Input (Data,Var,Abs) 178
0xc0, // End Collection 180
This is, I think, a firmware error in the keyboard that could have an impact on its NKRO functionality. On the Linux platform, for example, the kernel would take the data sent by the Varmilo as an HID array, when actually it's a bitmap. The end result being that on Linux the Varmilo is limited to 6KRO, as the kernel can only register the events sent by the first keyboard (the one with 6KRO).
I've also tried the Varmilo on a Windows 10 virtual machine. On this OS, the NKRO functionality works fine. Although the Varmilo's firmware is not complying with the USB HID specification, the Windows kernel is able to interpret the data sent by the keyboard. Why does this happen? Well, I guess that the Windows kernel is seeing that the HID descriptor is describing an array with 1-bit elements, and that this doesn't make sense. So it takes the array to actually be a bitmap. (On the Windows platform, which is desktop oriented, the kernel has to deal with a lot of buggy hardware, which is supposed to "just work". So it has to use a lot of hacks in the code in order to get that. I guess this one with USB keyboards is one of them).
What are your thoughts on all of this? I've spent a lot of time the past days researching this issue, learning a lot about themes that were completely new to me. I think my conclusions are right but I would like to see some feedback and comments.