范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

Linux驱动基础篇hello驱动

  上一篇分享的:从单片机工程师的角度看嵌入式Linux 中有简单提到Linux的三大类驱动:
  我们学习编程的时候都会从hello程序开始。同样的,学习Linux驱动我们也从最简单的hello驱动学起。驱动层和应用层
  还记得实习那会儿我第一次接触嵌入式Linux项目的时候,我的导师让我去学习项目的其它模块,然后尝试着写一个串口相关的应用。那时候知道可以把设备当做文件来操作,但是不知道为什么是这样,就去网上搜了一些代码(驱动代码),然后和我的应用代码放在同一个文件里。给导师看了之后,导师说那些驱动程序不需要我写,那些驱动已经写好被编译到内核里了,可以直接用了,我只需关注应用层就好了。我当时脑子里就在打转…what?STM32用一个串口不就是串口初始化,然后想怎么用就怎么用吗?后来经过学习才知道原来是那么一回事呀。这就是单片机转转嵌入式Linux的思维误区之一。学嵌入式Linux之前我们有必要暂时忘了我们单片机的开发方式,重新梳理嵌入式Linux的开发流程。下面看一下STM32裸机开发与嵌入式Linux开发的一些区别:
  嵌入式Linux的开发方式与STM32裸机开发的方式有点不一样。在STM32的裸机开发中,驱动层与应用层的区分可能没有那么明显,常常都杂揉在一起。当然,有些很有水平的裸机程序分层分得还是很明显的。但是,在嵌入式Linux中,驱动和应用的分层是特别明显的,最直观的感受就是驱动程序是一个.c文件里,应用程序是另一个.c文件。比如我们这个hello驱动实验中,我们的驱动程序为hello_drv.c、应用程序为hello_app.c。驱动模块的加载有两种方式:第一种方式是动态加载的方式,即驱动程序与内核分开编译,在内核运行的过程中加载;第二种方式是静态加载的方式,即驱动程序与内核一同编译,在内核启动过程中加载驱动。在调试驱动阶段常常选用第一种方式,因为较为方便;在调试完成之后才采用第二种方式与内核一同编译。
  STM32裸机开发与嵌入式Linux开发还有一点不同的就是:STM32裸机开发最终要烧到板子的常常只有一个文件(除开含有IAP程序的情况或者其它情况),嵌入式Linux就需要分开编译、烧写。Linux字符设备驱动框架
  我们先看一个图:
  当我们的应用在调用open、close、write、read等函数时,为什么就能操控硬件设备。那是因为有驱动层在支撑着与硬件相关的操作,应用程序在调用打开、关闭、读、写等操作会触发相应的驱动层函数。
  本篇笔记我们以hello驱动做分享,hello驱动属于字符设备。实现的驱动函数大概是怎么样的是有套路 可寻的,这个套路在内核文件include/linux/fs.h 中,这个文件中有如下结构体:
  这个结构体里的成员都是些函数指针变量,我们需要根据实际的设备确定我们需要创建哪些驱动函数实体。比如我们的hello驱动的几个基本的函数(打开/关闭/读/写)可创建为(以下代码来自:百问网):
  (1)打开操作static int hello_drv_open (struct inode *node, struct file *file) {     printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__);     return 0; }
  打开函数的两个形参的类型要与struct file_operations 结构体里open 成员的形参类型一致,里面有一句打印语句,方便直观地看到驱动的运行过程。
  (2)关闭操作static int hello_drv_close (struct inode *node, struct file *file) {     printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__);     return 0; }
  (3)读操作static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) {     int err;     printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__);     err = copy_to_user(buf, kernel_buf, MIN(1024, size));     return MIN(1024, size); }
  copy_to_user 函数的原型为:static inline int copy_to_user(void __user *to, const void *from, unsigned long n);
  用该函数来读取内核空间(kernel_buf )的数据给到用户空间(buf )。 另外,kernel_buf的定义如下:static char kernel_buf[1024];
  MIN为宏:#define MIN(a, b) (a < b ? a : b)
  把MIN(1024, size) 作为copy_to_user 的实参意在对拷贝的数据长度做限制(不能超出kernel_buf的大小)。
  (4)写操作static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) {     int err;     printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__);     err = copy_from_user(kernel_buf, buf, MIN(1024, size));     return MIN(1024, size); }
  copy_from_user函数的原型为:static inline int copy_from_user(void *to,const void __user volatile *from,unsigned long n)
  用该函数来将用户空间(buf )的数据传送到内核空间(kernel_buf )。
  有了这些驱动函数,就可以给到一个struct file_operations 类型的结构体变量hello_drv ,如:static struct file_operations hello_drv =  {     .owner   = THIS_MODULE,     .open    = hello_drv_open,     .read    = hello_drv_read,     .write   = hello_drv_write,     .release = hello_drv_close, };
  有些朋友可能没见过这种结构体初始化的形式(结构体成员前面加个.号),可以去看往期笔记:指定初始化器 进行了解。
  上面这个结构体变量hello_drv 容纳了我们hello 设备的驱动接口,最终我们要把这个hello_drv 注册给Linux内核,套路就是这样的:把驱动程序注册给内核,之后我们的应用程序就可以使用open/close/write/read 等函数来操控我们的设备,Linux内核在这里起到一个中间人的作用,把两头的驱动与应用协调得很好。
  我们前面说了驱动的装载方式之一的动态装载:把驱动程序编译成模块,再动态装载。动态装载的体现就是开发板已经启动运行了Linux内核,我们通过开发板串口终端使用命令来装载驱动。装载驱动有两个命令,比如装载我们的hello驱动:方法一:insmod hello_drv.ko 方法二:modprobe hello_drv.ko
  其中modprobe 命令不仅能装载当前驱动,而且还会同时装载与当前驱动相关的依赖驱动。有了转载就有卸载,也有两种方式:方法一:rmmod hello_drv.ko 方法二:modprobe -r hello_drv.ko
  其中modprobe 命令不仅卸载当前驱动,也会同时卸载依赖驱动。
  我们在串口终端调用装载与卸载驱动的命令,怎么就会执行装载与卸载操作。对应到驱动程序里我们有如下两个函数:module_init(hello_init); //注册模块加载函数 module_exit(hello_exit); //注册模块卸载函数
  这里加载与注册有用到hello_init 、hello_exit 函数,我们前面说的把hello_drv 驱动注册到内核就是在hello_init 函数里做,如:static int __init hello_init(void) { 	int err; 	 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__);     /* 注册hello驱动 */ 	major = register_chrdev(0, 			  /* 主设备号,为0则系统自动分配 */                             "hello", 	  /* 设备名称 */                             &hello_drv);  /* 驱动程序 */      	/* 下面操作是为了在/dev目录中生成一个hello设备节点 */     /* 创建一个类 */ 	hello_class = class_create(THIS_MODULE, "hello_class"); 	err = PTR_ERR(hello_class); 	if (IS_ERR(hello_class)) { 		printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 		unregister_chrdev(major, "hello"); 		return -1; 	} 	     /* 创建设备,该设备创建在hello_class类下面 */ 	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */ 	 	return 0; }
  这里这个驱动程序入口函数hello_init中注册完驱动程序之后,同时通过下面连个创建操作来创建设备节点,即在/dev目录下生成设备文件。据我了解,在之前版本的Linux内核中,设备节点需要手动创建,即通过创建节点命令mknod 在/dev目录下自己手动创建设备文件。既然已经有新的方式创建节点了,这里就不抠之前的内容了。
  以上就是分享关于驱动一些内容,通过以上分析,我们知道,其是有套路(就是常说的驱动框架)可寻的,比如:#include  #include  #include  /* 其她头文件...... */  /* 一些驱动函数 */ static ssize_t xxx_read (struct file *file, char __user *buf, size_t size, loff_t *offset) {  }  static ssize_t xxx_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) {  }  static int xxx_open (struct inode *node, struct file *file) {  }  static int xxx_close (struct inode *node, struct file *file) {  } /* 其它驱动函数...... */  /* 定义自己的驱动结构体 */ static struct file_operations xxx_drv = { 	.owner	 = THIS_MODULE, 	.open    = xxx_open, 	.read    = xxx_read, 	.write   = xxx_write, 	.release = xxx_close, 	/* 其它程序......... */ };  /* 驱动入口函数 */ static int __init xxx_init(void) {  }  /* 驱动出口函数 */ static void __exit hello_exit(void) {  }  /* 模块注册与卸载函数 */ module_init(xxx_init); module_exit(xxx_exit);  /* 模块许可证(必选项) */ MODULE_LICENSE("GPL");
  按照这样的套路来开发驱动程序的,有套路可寻那就比较好学习了,至少不会想着怎么起函数名而烦恼,按套路来就好,哈哈
  关于驱动的知识,这篇笔记中还可以展开很多内容,限于篇幅就不展开了。我们之后再进行学习、分享。下面看一下测试程序/应用程序(hello_drv_test.c中的内容,以下代码来自:百问网):#include  #include  #include  #include  #include  #include   /*  * ./hello_drv_test -w abc  * ./hello_drv_test -r  */ int main(int argc, char **argv) { 	int fd; 	char buf[1024]; 	int len; 	 	/* 1. 判断参数 */ 	if (argc < 2)  	{ 		printf("Usage: %s -w  ", argv[0]); 		printf("       %s -r ", argv[0]); 		return -1; 	}  	/* 2. 打开文件 */ 	fd = open("/dev/hello", O_RDWR); 	if (fd == -1) 	{ 		printf("can not open file /dev/hello "); 		return -1; 	}  	/* 3. 写文件或读文件 */ 	if ((0 == strcmp(argv[1], "-w")) && (argc == 3)) 	{ 		len = strlen(argv[2]) + 1; 		len = len < 1024 ? len : 1024; 		write(fd, argv[2], len); 	} 	else 	{ 		len = read(fd, buf, 1024);		 		buf[1023] = ""; 		printf("APP read : %s ", buf); 	} 	 	close(fd); 	 	return 0; }
  就是一些读写操作,跟我们学习文件操作是一样的。学单片机的有些朋友可能不太熟悉main函数的这种写法:int main(int argc, char **argv)
  main函数在C中有好几种写法,在Linux中常用这种写法。argc与argv这两个值可以从终端(命令行)输入,因此这两个参数也被称为命令行参数。argc 为命令行参数的个数,argv 为字符串命令行参数的首地址。
  最后,我们把编译生成的驱动模块hello_drv.ko 与应用程序hello_drv_test 放到共享目录录nfs_share中,同时在开发板终端挂载共享目录:mount -t nfs -o nolock,vers=4 192.168.1.104:/home/book/nfs_share /mnt
  关于nfs网络文件系统的使用可查看往期笔记:如何挂载网络文件系统? 。
  然后我们通过insmod  命令装载驱动,但是出现了如下错误:
  这是因为我们的驱动的编译依赖与内核版本,编译用的内核版本与当前开发板运行的内核的版本不一致所以会产生该错误,重新编译内核,并把编译生成的Linux内核zImage映像文件与设备树文件*.dts文件拷贝到开发板根文件系统的/boot目录下,然后进行同步操作:#mount -t nfs -o nolock,vers=4 192.168.1.114:/home/book/nfs_share /mnt #cp /mnt/zImage /boot #cp /mnt/.dtb /boot #sync
  最后,重启开发板。最后,成功运行程序:
  下面是完整的hello驱动程序(来源:百问网):// 公众号:嵌入式大杂烩 #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include  #include   /* 1. 确定主设备号                                                                 */ static int major = 0; static char kernel_buf[1024]; static struct class *hello_class;   #define MIN(a, b) (a < b ? a : b)  /* 3. 实现对应的open/read/write等函数,填入file_operations结构体                   */ static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset) { 	int err; 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	err = copy_to_user(buf, kernel_buf, MIN(1024, size)); 	return MIN(1024, size); }  static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) { 	int err; 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	err = copy_from_user(kernel_buf, buf, MIN(1024, size)); 	return MIN(1024, size); }  static int hello_drv_open (struct inode *node, struct file *file) { 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	return 0; }  static int hello_drv_close (struct inode *node, struct file *file) { 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	return 0; }  /* 2. 定义自己的file_operations结构体                                              */ static struct file_operations hello_drv = { 	.owner	 = THIS_MODULE, 	.open    = hello_drv_open, 	.read    = hello_drv_read, 	.write   = hello_drv_write, 	.release = hello_drv_close, };  /* 4. 把file_operations结构体告诉内核:注册驱动程序                                */ /* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */ static int __init hello_init(void) { 	int err; 	 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */   	hello_class = class_create(THIS_MODULE, "hello_class"); 	err = PTR_ERR(hello_class); 	if (IS_ERR(hello_class)) { 		printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 		unregister_chrdev(major, "hello"); 		return -1; 	} 	 	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */ 	 	return 0; }  /* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */ static void __exit hello_exit(void) { 	printk("%s %s line %d ", __FILE__, __FUNCTION__, __LINE__); 	device_destroy(hello_class, MKDEV(major, 0)); 	class_destroy(hello_class); 	unregister_chrdev(major, "hello"); }   /* 7. 其他完善:提供设备信息,自动创建设备节点                                     */  module_init(hello_init); module_exit(hello_exit);  MODULE_LICENSE("GPL");
  嵌入式Linux的学习内容是很多的、坑也是很多的,死磕到底即可。
  1024G 嵌入式资源大放送!包括但不限于C/C++、单片机、Linux等。私信回复 1024,即可免费获取!

当下续航极佳的5款手机,出门不用带充电宝,可满足一整天的使用不知道大家发现一个问题没,那就是现在的手机厂商宣传新品的时候很喜欢以屏幕影像系统快充散热等方面,就连设计也能拿出来得瑟一波,所谓的LikePhone风格早就成为历史了。但是呢,你要RedmiBookPro2022正式发布,12代酷睿H45处理器80W性能释放这两天RedmiK50系列持续出圈,大家把焦点都放在了手机上,其实我们忽略了一款更具性价比的产品,它就是RedmiBookPro2022。无论是处理器,显卡,屏幕,散热,续航较上一早报新能源多重利好,氢能站上万亿风口特高压进入新发展期一水泥涨价全国多地再次上调水泥价格。点评水泥价格波动受成本因素影响较大,基于当前水泥行业的压力,水泥进入高成本时代,与水泥息息相关的便是基建,水泥景气彰显韧性,相关产业链均受影响,SonnocP1T投影机深度评测小巧便携自带电池打造移动家庭影院最近一直想在卧室弄个投影仪。虽然说客厅是有电视机的,看电视挺方便的,但有时候我更喜欢在卧室看电视。尤其是大冬天的,窝在被窝里追剧,那滋味别提有多香了。但是讲真,在卧室放一台电视机的俄地方法院决定禁止脸书及Instagram在俄运营据俄罗斯卫星通讯社报道,俄罗斯莫斯科特维尔区法院当地时间21日禁止Facebook和Instagram在俄运营,原因是上述两个社交网站均有极端主义行为。据报道,法院的决定是对Fac英研究报告揭示最无聊的工作和爱好据美国欧亚评论网站近日报道,英国埃塞克斯大学的研究发现,世界上最无聊的人是一名宗教数据录入人员,他喜欢看电视,住在一个小镇上。这项经过同行评议的无聊科学研究结果揭示了被认为是典型的AppleSwift和瑞士,在俄乌战争中砸碎的那些东西如果是因为战火造成了服务中断,没有太多人会在意,但是Apple卖出去的手机居然限制了俄罗斯人使用一部分软件,这问题性质可就不一样了第一,Apple告诉全世界,它可以这么做。全世界都刷宝短视频APP被约谈中新经纬3月22日电中央网信办举报中心微信号22日消息,因个别用户账号存在发布或传播法律法规禁止发布信息内容,海南省网信办日前依法约谈刷宝短视频APP运营主体相关负责人。海南省网信MIUI系统升级更新完,这17款APP记得卸载,可放心删除在MIUI系统升级更新完,或者新买的手机,有没有发现MIUI系统会预装一些系统APP,这些APP有些你可能几个月或者一年都不会用到几次次,甚至点击一下,而这些APP完成可卸载,既能部分用户称iOS15。4太耗电!有机型续航降半,苹果称将进行调查,网友逼着用户换新款?中国经济周刊经济网讯据媒体报道,上周苹果开始推出iOS15。4正式版。然而,越来越多的人在OTA后报告了糟糕的电池续航。一些网友称,iPhone13ProMax只能维持半天续航时间洗碗机究竟值不值得入手?我使用了半年多,来客观的谈谈感受目前,洗碗机行业正在国内高速的发展。洗碗机到底该不该买,该买什么样的?这是近些年装修业主最纠结的问题之一。对于这个新型的厨房电器,大家对它的评价也是两极分化有的人说它是妥妥地收割智
5G套餐来了,狂欢节买5G手机别纠结NSASA问题一年一度的双11快到了,如果你准备要换部新手机,那么我建议你优先选择5G手机。因为目前5G已经开始商用,三大运营商也都公布了5G套餐价格,明年也会有一大批的5G手机上市,如果你现在golang2021面向对象(9)方法基本介绍在某些情况下,我们要需要声明(定义)方法。比如Person结构体除了有一些字段外(年龄,姓名。。),Person结构体还有一些行为比如可以说话跑步。。,通过学习,还可以做算golang2021面向对象(15)方法的调用和传参机制原理说明方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。案例1画出前面getSum方法的执行过程说明说明1)在golang2021面向对象(14)Go语言为任意类型添加方法Go语言可以对任何类型添加方法,给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。为基本类型添加方法在Go语言中,使用type关键字可以定义出新的自定义类型,之后golang2021面向对象(5)Go语言构造函数Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。其他编程语言构造函数的一些常见功能及特性如下每个类可以添加构造函数,多个构造函数使用golang2021面向对象(18)方法集方法集Golang方法集每个类型都有与之关联的方法集,这会影响到接口实现规则。类型T方法集包含全部receiverT方法。类型T方法集包含全部receiverTT方法。如类型S包含golang2021面向对象(13)方法的声明(定义)方法的声明(定义)func(receviertype)methodName(参数列表)(返回值列表)方法体return返回值1)参数列表表示方法输入2)receviertype表示golang2021面向对象(10)基于指针对象的方法当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了。对应到golang2021面向对象(7)创建结构体变量和访问结构体字段方式1直接声明案例演示varpersonPerson前面我们已经说了。方式2案例演示varpersonPersonPerson方式3案例varpersonPersonnew(Pergolang2021面向对象(19)表达式Golang表达式根据调用者不同,方法分为两种表现形式instance。method(args)。func(instance,args)前者称为methodvalue,后者methrealmeX50Pro玩家版游戏实测还不错在配置方面,作为主打性能的realmeX50Pro玩家版自然毫不吝啬,该机搭载了最新的高通骁龙865处理器,采用了台积电7nm制程工艺和最新的A77架构,主频高达2。84GHz,集