一、概况
以Linux5.10内核中USB键盘驱动为例进行解析:https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.tar.gz
文件路径:linux-5.10/drivers/hid/usbhid/usbkbd.c
二、探索
usb_kbd_event
- 灯的主要逻辑在函数
usb_kbd_event
里面,usb_kbd_event
被赋值给了input_dev->event
input_dev->event = usb_kbd_event;
也就是说usbkbd模块提供了控制等亮灭的方法,但是具体的到底哪个灯亮或者哪个灯灭的决定权在input模块。 - 下面我们具体分析下usbkbd模块是如何控制灯的。其核心代码如下:
kbd->newleds = (!!test_bit(LED_KANA, dev->led) << 3) | (!!test_bit(LED_COMPOSE, dev->led) << 3) |
(!!test_bit(LED_SCROLLL, dev->led) << 2) | (!!test_bit(LED_CAPSL, dev->led) << 1) |
(!!test_bit(LED_NUML, dev->led));
-
这里面的
test_bit
的实现如下,其作用是获取addr这块内存的第nr位的值。BITS_PER_LONG
表示一个long类型所占用的bit位数量。在64位系统上BITS_PER_LONG
就是64,32位系统上就是32。BIT_WORD(nr)
用nr整除BITS_PER_LONG
就得到了nr这个比特位所在的元素下标。BITS_PER_LONG-1
意思是在一个unsigned long
类型的变量里面,你最多只能将最左边的位移到最右边,也就是说你最多右移BITS_PER_LONG-1
位,(nr & (BITS_PER_LONG-1))
这里其实就是计算nr在其坐在的元素内要右移的bit位数。- 所以整体来看就是
addr[BIT_WORD(nr)]
找到第nr个bit位所在的元素,(nr & (BITS_PER_LONG-1))
计算出第nr位在元素内的右移位数。(addr[BIT_WORD(nr)] >> (nr & (BITS_PER_LONG-1))
将第nr位移到最右侧。1UL & (addr[BIT_WORD(nr)] >> (nr & (BITS_PER_LONG-1)))
完成位移后,将该位和1进行与操作,判断其是否是1。
#ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */ #define BIT_WORD(nr) ((nr) / BITS_PER_LONG) /** * _test_bit - Determine whether a bit is set * @nr: bit number to test * @addr: Address to start counting from */ static __always_inline bool _test_bit(unsigned long nr, const volatile unsigned long *addr) { return 1UL & (addr[BIT_WORD(nr)] >> (nr & (BITS_PER_LONG-1))); }
-
清楚了
test_bit
,我们在来看看具体的业务逻辑。本次我们只关注LED_NUML
,LED_CAPSL
以及LED_SCROLLL
。下面这块代码的意思就很明确了分别从dev->led
这块内存的LED_SCROLLL
,LED_CAPSL
,及LED_NUML
bit位取出值放在新的变量kbd->newleds
的第2, 1, 0位。也就是说kbd->newleds
变量的第2位如果是1,那么表示点亮键盘的Scroll Lock
灯,如果是0则熄灭Scroll Lock
等,而第1位表示Caps Lock
,第0位表示Num Lock
。(!!test_bit(LED_SCROLLL, dev->led) << 2) | (!!test_bit(LED_CAPSL, dev->led) << 1) | (!!test_bit(LED_NUML, dev->led))
-
然后通过
usb_submit_urb(kbd->led, GFP_ATOMIC)
将新的led值发送给设备。 -
我们知道USB有四种传输方式,控制传输(control)、批量传输(bulk)、中断传输(interrupt )以及等时传输(isochronous)。我们这里只是改了下leds的值,然后就提交了urb,那么我们这里到底用了什么传输方式呢?
usb_fill_control_urb
-
回到我们的
usb_kbd_probe
函数里面,该函数在usbkbd这个驱动被加载时将会被调用;而我们前面的input_dev->event = usb_kbd_event;
操作也是在这个函数里面完成的。 -
下面是和灯相关的核心代码。控制灯使用的是USB控制传输。控制传输比较重要的就是其setup包,其结构如下:
-
bRequestType
字段,我们控制灯的请求,并非USB协议中定义的标准USB请求,而是输入设备类所定义的请求,所以是USB_TYPE_CLASS
。我们这个请求过去之后是控制接口接收,所以接收者是USB_RECIP_INTERFACE
。传输方向是主机向设备发送指令Host-to-device
是零,所以不需要特意去设置,默认就好。 -
bRequest
字段,因为我们这个是输入类设备特有的请求类型。所以需要查看usbhid协议,协议意思是控制LED灯使用Set_Report(Output)
请求。而SET_REPORT
对应的value是0x09,所以我们这里的bRequest
也就是0x09表示这是一个SET_REPORT
请求。
-
Set_Report(Output)
请求定义如下,其wValue
字段包含Report Type
和Report ID
,Report Type
在高字节,我们LED灯的控制指令是从host到device,也就是out,所以Report Type
值是0x02,我们不用Report ID
,所以其值为零。所以我们的wValue
字段的值就是0x0200
:
-
wIndex
字段填写interface,直接填写控制接口的bInterfaceNumber
字段即可。 -
wLength
表示要传输的数据大小,我们传输表述灯状态的一个uint8_t类型,所以其大小为1。 -
usb_fill_control_urb
就是把我们上面填进取的这些数据在填到urb里面,也就是kbd->led
里面。
kbd->cr->bRequestType = USB_TYPE_CLASS | USB_RECIP_INTERFACE;
kbd->cr->bRequest = 0x09;
kbd->cr->wValue = cpu_to_le16(0x200);
kbd->cr->wIndex = cpu_to_le16(interface->desc.bInterfaceNumber);
kbd->cr->wLength = cpu_to_le16(1);
usb_fill_control_urb(kbd->led, dev, usb_sndctrlpipe(dev, 0),
(void *) kbd->cr, kbd->leds, 1,
usb_kbd_led, kbd);
三、总结
- 通过
usb_fill_control_urb
事先准备好urb,然后在usb_kbd_event
里面需要时,直接修改leds数据,然后将该urb发送即可。