• Linux驱动开发(十四)---USB驱动开发学习(键盘+鼠标)


    前文回顾

    《Linux驱动开发(一)—环境搭建与hello world》
    《Linux驱动开发(二)—驱动与设备的分离设计》
    《Linux驱动开发(三)—设备树》
    《Linux驱动开发(四)—树莓派内核编译》
    《Linux驱动开发(五)—树莓派设备树配合驱动开发》
    《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
    《Linux驱动开发(七)—树莓派按键驱动开发》
    《Linux驱动开发(八)—树莓派SR04驱动开发》
    《Linux驱动开发(九)—树莓派I2C设备驱动开发(BME280)》
    《Linux驱动开发(十)—树莓派输入子系统学习(红外接收)》
    《Linux驱动开发(十一)—树莓派SPI驱动学习(OLED)》
    《Linux驱动开发(十二)—树莓派framebuffer学习(改造OLED)》
    《Linux驱动开发(十三)—USB驱动HID开发学习(鼠标)》
    继续宣传一下韦老师的视频

    70天30节Linux驱动开发快速入门系列课程【实战教学、技术讨论、直播答疑】

    在这里插入图片描述
    昨天学习了一下usb鼠标的简单识别,今天来完整的写一套键盘和鼠标的驱动,起码能够支持树莓派使用的。
    在这里插入图片描述
    先来写一下键盘的驱动。

    键盘驱动框架

    框架部分很简单,和昨天的鼠标基本一样,但是今天这里的table_id,要用一个通用定义,保证识别出所有的usb键盘。

    #include 
    #include 
    #include 
    #include 
    #include 
    
    //通用ID
    static const struct usb_device_id usb_kbd_id_table[] = {
    	{ USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT,USB_INTERFACE_PROTOCOL_KEYBOARD) },
    	{ }	/* Terminating entry */
    };
    
    /*
    MODULE_DEVICE_TABLE 有两个功能。
    一是:将设备加入到外设队列中,
    二是告诉程序阅读者该设备是热插拔设备或是说该设备支持热插拔功能。
    该宏定义在下
    这个宏有两个参数,第一个参数设备名,第二个参数该设备加入到模块中时对应产生的设备搜索符号,这个宏生成了一个名为__mod_pci_device_table
    局部变量,这个变量指向第二个参数
    */
    MODULE_DEVICE_TABLE (usb,usb_kbd_id_table);
     
    //USB设备信息与驱动端匹配成功的时候调用。
    static int myusbkbd_probe(struct usb_interface *intf,const struct usb_device_id *id)  //资源探索函数
    {
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    	printk("USB驱动匹配成功! ID: 0x%X,0x%X\n",id->idVendor,id->idProduct);
    	return 0;
    }
     
    //USB断开的时候调用
    static void myusbkbd_disconnect(struct usb_interface *intf)
    {
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    	printk("USB 设备释放成功!\n"); 
    }
     
    //定义USB驱动结构体 
    static struct usb_driver myusbkbd_driver = 
    {
        .name = "myusbkbd_drv",
        .probe = myusbkbd_probe,
        .disconnect = myusbkbd_disconnect,
        .id_table = usb_kbd_id_table,
    };
     
    static int __init myusbkbd_init(void)
    {
        //注册USB设备驱动
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
        usb_register(&myusbkbd_driver);
        return 0;
    }
     
    static void __exit myusbkbd_exit(void)
    {
         //注销USB设备驱动
    	 printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
         usb_deregister(&myusbkbd_driver);
    }
     
    module_init(myusbkbd_init);
    module_exit(myusbkbd_exit);
    MODULE_AUTHOR("PGG");
    MODULE_LICENSE("GPL");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    还是来测试一下效果,能否顺利加载

    [  194.723576] drivers/char/myusbkbd.c myusbkbd_init 54
    [  194.723771] usbcore: registered new interface driver myusbkbd_drv
    [  200.588535] usb 1-1.3: new low-speed USB device number 6 using dwc_otg
    [  200.733864] usb 1-1.3: New USB device found, idVendor=0c45, idProduct=760b, bcdDevice= 1.05
    [  200.733901] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=0
    [  200.733921] usb 1-1.3: Product: USB Keyboard
    [  200.733937] usb 1-1.3: Manufacturer: SONiX
    [  200.735207] drivers/char/myusbkbd.c myusbkbd_probe 29
    [  200.735228] USB驱动匹配成功! ID: 0x0,0x0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    加载在成功!!不过不清楚为啥ID没有读出来。
    在这里插入图片描述

    尝试一下,修改为按照ID匹配

    static const struct usb_device_id my_usbkbd_id_table[] = 
    {
        {USB_DEVICE(0x0c45,0x760b)},//键盘
        {}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结果又执行了两次probe函数

    [  124.011262] drivers/char/myusbkbd.c myusbkbd_init 58
    [  124.011457] usbcore: registered new interface driver myusbkbd_drv
    [  130.851294] usb 1-1.3: new low-speed USB device number 5 using dwc_otg
    [  130.996576] usb 1-1.3: New USB device found, idVendor=0c45, idProduct=760b, bcdDevice= 1.05
    [  130.996613] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=0
    [  130.996633] usb 1-1.3: Product: USB Keyboard
    [  130.996650] usb 1-1.3: Manufacturer: SONiX
    [  130.997809] drivers/char/myusbkbd.c myusbkbd_probe 33
    [  130.997829] USB驱动匹配成功! ID: 0xC45,0x760B
    [  130.998194] drivers/char/myusbkbd.c myusbkbd_probe 33
    [  130.998211] USB驱动匹配成功! ID: 0xC45,0x760B
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    着实有点费解。不去想了。
    在这里插入图片描述

    丰富probe函数

    切换回通用识别的方式,先修改probe函数。这里主要增加两部分内容:

    • 消息获取
    • 输入子系统的调用。

    首先,我们先增加获取一些用得到的数据的内容

    struct usb_device *dev = NULL;
    struct usb_host_interface *interface;
    struct usb_endpoint_descriptor *endpoint;
    
    struct input_dev *input_dev;
    int i, pipe, maxp, maxps;
    
    printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    printk("USB驱动匹配成功! ID: 0x%X,0x%X\n",id->idVendor,id->idProduct);
    
    //获取设备信息,主机接口信息,终端描述符
    dev = interface_to_usbdev(intf);
    interface = intf->cur_altsetting;
    endpoint = &interface->endpoint[0].desc;
    
    //建立终端传输管道
    pipe = usb_rcvintpipe(dev, endpoint->bEndpointAddress);
    
    maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));
    /*从端点描述符中获取传输的数据大小 */
    maxps = endpoint->wMaxPacketSize;
    
    printk("maxp[%d] maxps[%d]\n",maxp,maxps);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    获取传输数据最大包数,发现了两种写法,标准驱动采用的上面的写法。其实都可用。

    maxp = usb_maxpacket(dev, pipe, usb_pipeout(pipe));
    maxps = endpoint->wMaxPacketSize;
    
    • 1
    • 2

    然后就开始结合输入子系统,创建input_dev,注册各个按键。

    //创建输入子系统
    input_dev = input_allocate_device();
    
    input_dev->name = "myusb_kbd";
    input_dev->phys = "myusb_kbd";
    
    input_dev->evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
    for (i = 0; i < 255; i++)
    {
    	set_bit(usb_kbd_keycode[i], input_dev->keybit);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    到这里的话,输入设备会出现,但是如何与usb终端进行数据交互,这里就用到了urb这个重要操作了

    USB请求块(USB request block,URB)是USB设备驱动中用来描述与USB设备通信所用的基本载体和核心数据结构,与网络设备驱动中的sk_buff结构体类似,是USB主机与设备之间传输数据的封装。
    一个urb包含了执行usb传输所需要的所有信息。当要进行数据传输时,需要分配一个urb结构体,对其进行初始化,然后将其提交给usb核心。USB核心对urb进行解析,将控制信息提交给主机控制器,由主机控制器负责数据到设备的传输。这时,驱动程序只需等待,当数据回传到主机控制器后,会转发给USB核心,唤醒等待的驱动程序,由驱动程序完成剩下的工作。
    更为具体地说,Linux中的设备驱动程序只要为每一次请求准备一个urb结构体,然后把它填充好,就可以调用函数usb_submit_urb()提交给USB核心。然后USB核心将urb传递给USB主机控制器,最终传递给USB设备。USB设备获得urb结构体后,会解析这个结构体,并以相反的路线将数据返回给Linux内核。

    那么简单来说,这个urb就是负责穿梭于驱动和设备之间,将用户数据带回来的单元。
    在这里插入图片描述

    用法如下,注意:这里有个缓存大小的问题

    USB支持4种基本的数据传输模式:控制传输、同步传输、中断传输、批量传输。控制传输方式支持双向传输,用来处理主端口到USB从端口的数据传输,包括设备控制指令、设备状态查询及确认命令。对于高速设备,允许数据包最大容量为8,16,32或64字节,对于低速设备只有8字节一种选择。

    //urb申请使用
    my_kbd_data = usb_alloc_coherent(dev, 8, GFP_ATOMIC, &my_kbd_data_dma);
    my_kbd_urb = usb_alloc_urb(0, GFP_KERNEL);
    
    usb_fill_int_urb(my_kbd_urb, dev, pipe, my_kbd_data,
    		 		(maxp > 8 ? 8 : maxp),
    				usb_mouse_irq, NULL, endpoint->bInterval);
    my_kbd_urb->transfer_dma = my_kbd_data_dma;
    my_kbd_urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最后,注册输入设备,并且开始传输urb

    input_register_device(input_dev);
    usb_submit_urb(my_kbd_urb, GFP_KERNEL);
    
    • 1
    • 2

    在这里插入图片描述

    中断函数先添加打印,看看前面的分析对不对。

    static void my_usb_kbd_irq(struct urb *urb)
    {
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    	usb_submit_urb(my_kbd_urb, GFP_ATOMIC);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    上机调试

    root@raspberrypi:/home/pgg/work/driver# insmod myusbkbd.ko 
    root@raspberrypi:/home/pgg/work/driver# dmesg 
    [ 5296.645084] drivers/char/myusbkbd.c myusbkbd_init 144
    [ 5296.647678] usbcore: registered new interface driver myusbkbd_drv
    root@raspberrypi:/home/pgg/work/driver# dmesg 
    [ 5296.645084] drivers/char/myusbkbd.c myusbkbd_init 144
    [ 5296.647678] usbcore: registered new interface driver myusbkbd_drv
    [ 5309.228696] usb 1-1.3: new low-speed USB device number 7 using dwc_otg
    [ 5309.375217] usb 1-1.3: New USB device found, idVendor=0c45, idProduct=760b, bcdDevice= 1.05
    [ 5309.375255] usb 1-1.3: New USB device strings: Mfr=1, Product=2, SerialNumber=0
    [ 5309.375275] usb 1-1.3: Product: USB Keyboard
    [ 5309.375292] usb 1-1.3: Manufacturer: SONiX
    [ 5309.386724] drivers/char/myusbkbd.c myusbkbd_probe 73
    [ 5309.386755] USB驱动匹配成功! ID: 0x0,0x0
    [ 5309.386769] maxp[8] maxps[8]
    [ 5309.387055] input: myusb_kbd as /devices/virtual/input/input4
    [ 5309.891936] drivers/char/myusbkbd.c my_usb_kbd_irq 59
    [ 5310.388930] drivers/char/myusbkbd.c my_usb_kbd_irq 59
    [ 5310.893929] drivers/char/myusbkbd.c my_usb_kbd_irq 59
    [ 5311.398930] drivers/char/myusbkbd.c my_usb_kbd_irq 59
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    看来流程是对的,不过这个中断,在没有按键的时候,还是会一直触发。我们来调试一下是不是有数据。
    在这里插入图片描述

    调试中断信息

    通过打印一下status,来看一下有没有数据

    	printk("%s %s %d urb->status[%d]\n", __FILE__, __FUNCTION__, __LINE__,urb->status);
    
    • 1

    倒是一直有这个打印,然后status一直为0,说明一直有中断上来
    间隔大约0.5秒。那这里暂时不分析为什么会一直上报。

    解析按键并上报

    增加一下按键分析,参考usbkbd.c的代码

    static void my_usb_kbd_irq(struct urb *urb)
    {
    	int i=0;
    	
    	//printk("%s %s %d urb->status[%d]\n", __FILE__, __FUNCTION__, __LINE__,urb->status);
    
    	for (i = 0; i < 8; i++)
    	{
    		input_report_key(my_kbd_input_dev, usb_kbd_keycode[i + 224], (my_kbd_data[0] >> i) & 1);
    	}
    	for (i = 2; i < 8; i++) 
    	{
    	
    		if (my_kbd_data_old[i] > 3 && memscan(my_kbd_data + 2, my_kbd_data_old[i], 6) == my_kbd_data + 8) 
    		{
    			if (usb_kbd_keycode[my_kbd_data_old[i]])
    			{
    				input_report_key(my_kbd_input_dev, usb_kbd_keycode[my_kbd_data_old[i]], 0);
    			}
    			else
    			{
    				hid_info(urb->dev, "Unknown key (scancode %#x) released.\n", my_kbd_data_old[i]);
    			}
    		}
    
    		if (my_kbd_data[i] > 3 && memscan(my_kbd_data_old + 2, my_kbd_data[i], 6) == my_kbd_data_old + 8) 
    		{
    			if (usb_kbd_keycode[my_kbd_data[i]])
    			{
    				input_report_key(my_kbd_input_dev, usb_kbd_keycode[my_kbd_data[i]], 1);
    			}
    			else
    			{
    				hid_info(urb->dev, "Unknown key (scancode %#x) pressed.\n", my_kbd_data[i]);
    			}
    		}
    	}
    
    	input_sync(my_kbd_input_dev);
    
    	memcpy(my_kbd_data_old, my_kbd_data, 8);
    	usb_submit_urb(urb, GFP_KERNEL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    更新驱动,然后监听一下生成的event1,按下按键A
    在这里插入图片描述
    只有按下的时候有数据,说明还是对的。
    在这里插入图片描述

    用户侧获取按键

    那么怎么知道是A呢。那么我们用之前遥控器的用户侧程序,来解析一下事件
    分别按下A和B
    在这里插入图片描述
    看来event中定义的数值是吻合的
    在这里插入图片描述
    那么我们就创建一个数组,在用户侧解析一下具体是什么按键。
    在这里插入图片描述
    成功获取按键的值。
    在这里插入图片描述

    完成鼠标驱动

    在昨天的基础上,给鼠标也增加了输入系统,然后设备装载一下我自己写的两个驱动,看看能不能让鼠标动起来,键盘敲起来。
    结果遇到一个有意思的问题。就是鼠标只能向右下方向移动,最终只能落到右下角。
    原因就是键盘发送相对坐标的时候,应该是有符号的
    在这里插入图片描述
    所以数组定义的时候,应该也是有符号的。
    最终成功让我的树莓派,用上了自己的驱动。
    在这里插入图片描述

    代码下载

    《下载地址》
    其实建议自己根绝usbkbd和usbmouse的源码自己理解和重写一遍,印象就会更加深刻。也更能理解usb对于hid设备的驱动流程。
    在这里插入图片描述

    结束语

    今天的大事就是一个年近八旬的老太太,让中国的热血青年们夜不能寐。不过还是那首歌中唱的:
    朋友来了,有好酒;若是那豺狼来了,迎接他的有,biubiubiu。
    在这里插入图片描述
    马上就七夕了,别忘了过节啊
    在这里插入图片描述

  • 相关阅读:
    react创建项目后运行npm run eject无法暴露配置文件(已解决!!!)
    探花交友_第10章_实现推荐功能
    【UN-JS-工具类】懒加载的实现 -- 两种方式 --- 一种5行JS实现懒加载
    【虚拟化生态平台】树莓派安装虚拟化平台操作流程
    stable diffusion十七种controlnet详细使用方法总结
    gma 1.x 气候气象指数计算源代码(分享)
    前端框架—Vue
    gitee/github上传远程仓库错误usage: git remote add [<options>] <name> <url>
    Jmeter连接数据库jdbc
    Android 网络动态监听和是否联网
  • 原文地址:https://blog.csdn.net/baidu_19348579/article/details/126114307