当前位置: 首页 > 科技观察

Linux驱动-重新认识一波设备驱动_0

时间:2023-03-14 15:57:48 科技观察

先说结论:这些年我接触到的linux驱动教程,大部分都是从0开始写的,所以对初学者最大的好处就是可以接触有更多的基本原则。但在实际工作场景中,应该尽量避免从0开始构建自己的设备驱动,更好的做法是在高度模块化的驱动框架中添加自己的设备驱动。这样做的好处是最大程度的复用了内核已有的代码,获得了极大的灵活性和可维护性,为应用程序提供了统一的访问接口。下面就来详细说说吧。什么是设备驱动程序?设备驱动程序是硬件的抽象:操作系统内核的职责之一是为编写和运行设备驱动程序提供底层框架。尽管可以在用户空间中运行设备驱动程序(通过一些内核接口,如UIO或I2CDEV),但更常见的是让它们在内核空间中运行。以字符设备驱动程序为例:字符设备(chardevice)是最常见的硬件抽象之一。/dev目录下的设备节点文件是内核导出到用户空间访问设备驱动程序的接口。设备节点文件中包含三个基本信息:Type,用于标识是块设备还是字符设备;Majornumber,用于表示是哪种类型的字符设备;Minornumber,用来表示是哪个char设备;编写字符设备驱动流程:1.分配设备号,通过register_chrdev_region()或alloc_chrdev_region()完成;2、实现文件操作(open、read、write、ioctl)等。3、使用cdev_init()和cdev_add()在内核中注册字符设备。以LED字符设备驱动为例,如果按照从0开始构建的思路编写驱动,伪代码如下:硬件访问相关:staticstruct{dev_tdevnum;结构cdevcdev;无符号整数led_status;void__iomem*regbase;}drvled_data;staticvoiddrvled_setled(unsignedintstatus){u32val;/*设置值*/val=readl(drvled_data.regbase+GPIO1_REG_DATA);如果(状态==LED_ON)val|=GPIO_BIT;elseif(status==LED_OFF)val&=~GPIO_BIT;写(val,drvled_data.regbase+GPIO1_REG_DATA);/*更新状态*/drvled_data.led_status=status;}staticvoiddrvled_setdirection(void){...}文件操作相关:staticssize_tdrvled_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos){...}staticssize_tdrvled_write(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos){charkbuf=0;如果(copy_from_user(&kbuf,buf,1))返回-EFAULT;如果(kbuf=='1'){drvled_setled(LED_ON);pr_info("LED亮起!\n");}elseif(kbuf=='0'){drvled_setled(LED_OFF);pr_info("LED熄灭!\n");}返回计数;}staticconststructfile_operationsdrvled_fops={.owner=THIS_MODULE,.write=drvled_write,.read=drvled_read,};注册表和负载符设备相关:staticint__initdrvled_init(void){intresult=0;if(!request_mem_region(GPIO1_BASE,GPIO1_SIZE,DRIVER_NAME)){pr_err("%s:ErrorrequestingI/O!\n",DRIVER_NAME);结果=-EBUSY;转到ret_err_request_mem_region;}drvled_data.regbase=ioremap(GPIO1_BASE,GPIO1_SIZE);if(!drvled_data.regbase){pr_err("%s:错误映射I/O!\n",DRIVER_NAME);结果=-ENOMEM;转到err_ioremap;}结果=alloc_chrdev_region(&drvled_data.devnum,0,1,DRIVER_NAME);if(result){pr_err("%s:分配设备号失败!\n",DRIVER_NAME);转到ret_err_alloc_chrdev_region;}cdev_init(&drvled_data.cdev,&drvled_fops);结果=cdev_add(&drvled_data.cdev,drvled_data.devnum,1);if(result){pr_err("%s:字符设备注册失败!\n",DRIVER_NAME);转到ret_err_cdev_add;}drvled_setdirection();drvled_setled(LED_OFF);pr_info("%s:已初始化。\n",DRIVER_NAME);gotoret_ok;ret_err_cdev_add:unregister_chrdev_region(drvled_data.devnum,1);,GPIO1_SIZE);ret_err_request_mem_region:ret_ok:returnresult;}staticvoid__exitdrvled_exit(void){...}module_init(drvled_init);module_exit(drvled_exit);运行效果:$installledrv.ko$ls/dev/led#lighton$echo1>/dev/led#lightsoff$echo1>/dev/led三题列表从功能上看,上述程序完全满足控制一个LED的需求。但是,它不是一个好的驱动程序,这里有3个问题。问题1:它创建的接口是/dev/led,不是通用接口,会增加上层开发人员的学习成本。为了解决这个问题,需要在LEDchardriver之上增加一层LEDframework。LED框架负责为用户空间提供标准化的访问接口,并添加可重用的逻辑功能。基本上各种设备驱动都有自己的框架,比如input、IIO、ALSA、V2L2、RTC、watchdog等,使用这些框架的驱动工程师不需要考虑提供给用户空间的接口,应用开发者只需要学习一次标准的硬件访问接口接口。问题2:只控制1个gpio,却申请使用2个寄存器,负责控制芯片的8个gpio。这意味着其他7个gpio不能再被其他驱动应用程序使用。解决这个问题需要引入一个gpio管理器:gpiolib。gpiolib负责统一管理和分配gpio资源。问题3:包含硬件信息。如果我们要控制另一个gpio或者多个gpio,就得改源代码,代码维护工作量巨大。为了解决这个问题,我们需要从代码中提取硬件信息,具体来说就是总线、设备和驱动模型的引入。更好的LED驱动我们利用上面的思路来写一个更合理的LED驱动。LED框架介绍:1.初始化led_classdev结构体。2.提供一个回调函数来改变LED的状态。3.使用led_classdev_register()向LED框架注册驱动程序。介绍gpiolib:内核管理gpio的思想是典型的生产者/消费者模型。GPIO控制器驱动是生产者,LED驱动是消费者。下面是几个常用的gpiolibAPI,它们的功能一目了然:*con_id,枚举gpiod_flags标志);voidgpiod_put(structgpio_desc*desc);intgpiod_direction_input(structgpio_desc*desc);intgpiod_direction_output(structgpio_desc*desc,int值);voidgpiod_set_value(structgpio_desc*desc,intvalue_context)_value(结构gpio_desc*desc);介绍总线、设备、驱动模型:该模型包含4个部分。总线核心:硬件总线的抽象。不同的总线有不同的Buscores,如USBcores、SPIcores、I2Ccores、PCIcores,在内核中用bus_type结构体表示。总线适配器:总线控制器驱动程序,在内核中由device_driver结构表示。总线驱动程序:负责管理连接到总线上的设备的驱动程序,在内核中以device_driver结构体表示。总线设备:连接在总线上的设备,在内核中用结构体device表示。内核虚拟出一个总线叫Platform,用来适配不属于任何总线的设备,比如LED。看修改后的代码:设备信息:LED{<&gpio19>}硬件控制:taticstructdrvled_data_st*drvled_data;staticvoiddrvled_setled(unsignedintstatus){//控制gpioif(status==LED_ON)gpiod_set_value(drvled_data->描述,1);否则gpiod_set_value(drvled_data->desc,0);}staticvoiddrvled_change_state(structled_classdev*led_cdev,enumled_brightnessbrightness){if(brightness)drvled_setled(LED_ON);elsedrvled_setled(LED_OFF);}框架注册:staticintdrvled_probe(structplatform_device*pdev){structdevice_node*np=pdev->dev.of_node;结构device_node*child=NULL;int结果,gpio;child=of_get_next_child(np,NULL);drvled_data=devm_kzalloc(&pdev->dev,sizeof(*drvled_data),GFP_KERNEL);如果(!drvled_data)返回-ENOMEM;//从设备号获取硬件信息gpio=of_get_gpio(child,0);结果=devm_gpio_request(&pdev->dev,gpio,pdev->name);if(result){dev_err(&pdev->dev,"ErrorrequestingGPIO\n");返回结果;}drvled_data->desc=gpio_to_desc(gpio);drvled_data->led_cdev.name=of_get_property(child,"label",NULL);drvled_data->led_cdev.brightness_set=drvled_change_state;//注册进入LED框架结果=devm_led_classdev_register(&pdev->dev,&drvled_data->led_cdev);if(result){dev_err(&pdev->dev,"错误注册led\n");返回结果;}gpiod_direction_output(drvled_data->desc,0);dev_info(&pdev->dev,"已初始化。\n");return0;}staticintdrvled_remove(structplatform_device*pdev){dev_info(&pdev->dev,"exiting.\n");return0;}staticconststructof_device_idof_drvled_match[]={{.compatible="labworks,drvled"},{},};staticstructplatform_driverdrvled_driver={.driver={.name="drvleds",.owner=THIS_MODULE,.of_match_table=of_drvled_match,},.probe=drvled_probe,.remove=drvled_remove,};module_platform_driver(drvled_driver);修改后,应用总是通过下面这种标准的接口访问LED:#$/sys/class/leds//brightness#lightsoff$echo0>/sys/class/leds//brightness有很多触发器可用,比如让LED在心跳状态下heartbeattrigger:$echoheartbeat>/sys/class/leds//trigger假设你想使用gpioexpander芯片来控制LED:只需添加gpioexpander驱动代码并修改设备tree就这样,其他部分根本不用改:gpioexp{I2C0,0x10}LED{<&gpioexp3>}现在,你知道如何为Linux添加设备驱动了吗?