1 嵌入式驱动开发到底学什么
嵌入式大体分为以下四个方向:
一、嵌入式硬件开发:熟悉电路等知识,非常熟悉各种常用元器件,掌握模拟电路和数字电路设计的开发能力。熟练掌握嵌入式硬件知识,熟悉硬件开发模式和设计模式,熟悉ARM 32位处理器嵌入式硬件平台开发、并具备产品开发经验。精通常用的硬件设计工具:Protel/PADS(PowerPCB)/Cadence/OrCad。一般需要有4~8层高速PCB设计经验。
二、嵌入式驱动开发:熟练掌握Linux操作系统、系统结构、计算机组成原理、数据结构相关知识。熟悉嵌入式ARM开发,至少掌握Linux字符驱动程序开发。具有单片机、ARM嵌入式处理器的移植开发能力,理解硬件原理图,能独立完成相关硬件驱动调试,具有扎实的硬件知识,能够根据芯片手册编写软件驱动程序。
三、嵌入式系统开发:掌握Linux系统配置,精通处理器体系结构、编程环境、指令集、寻址方式、调试、汇编和混合编程等方面的内容;掌握Linux文件系统制作,熟悉各种文件系统格式(YAFFS2、JAFFS2、RAMDISK等);熟悉嵌入式Linux启动流程,熟悉Linux配置文件的修改;掌握内核裁减、内核移植、交叉编译、内核调试、启动程序Bootloader编写、根文件系统制作和集成部署Linux系统等整个流程;、熟悉搭建Linux软件开发环境(库文件的交叉编译及环境配置等);
四、嵌入式软件开发:精通Linux操作系统的概念和安装方法、Linux下的基本命令、管理配置和编辑器,包括VI编辑器,GCC编译器,GDB调试器和 Make 项目管理工具等知识;精通C语言的高级编程知识,包括函数与程序结构、指针、数组、常用算法、库函数的使用等知识、数据结构的基础内容,包括链表、队列等;掌握面向对象编程的基本思想,以及C++语言的基础内容;精通嵌入式Linux下的程序设计,精通嵌入式Linux开发环境,包括系统编程、文件I/O、多进程和多线程、网络编程、GUI图形界面编程、数据库;熟悉常用的图形库的编程,如QT、GTK、miniGUI、fltk、nano-x等。
公司的日常活动还是看公司的规模,大一点的一般只是让你负责一个模块,这样你就要精通一点。若是公司比较小的话估计要你什么都做一点。还要了解点硬件的东西。
那么看了这么多,嵌入式和纯软最大的区别在于:
纯软学习的是一门语言,例如C,C++,java,甚至Python,语言说到底只是一门工具,就像学会英语法语日语一样。
但嵌入式学习的是软件+硬件,通俗的讲,它学的是做系统做产品,讲究的是除了具体的语言工具,更多的是如何将一个产品分解为具体可实施的软件和硬件,以及更小的单元。
不少人问,将来就业到底是选驱动还是选应用?只能说凭兴趣,并且驱动和应用并不是截然分开的。
▍PART 01
我们说的驱动,其实并不局限于硬件的操作,还有操作系统的原理、进程的休眠唤醒调度等概念。想写出一个好的应用,想比较好的解决应用碰到的问题,这些知识大家应该都懂。
▍PART 02
做应用的发展路径个人认为就是业务纯熟。比如在通信行业、IPTV行业、手机行业,行业需求很了解。
▍PART 03
做驱动,其实不能称为“做驱动”,而是可以称为“做底层系统”,做好了这是通杀各行业。比如一个人工作几年,做过手机、IPTV、会议电视,但是这些产品对他毫无差别,因为他只做底层。当应用出现问题,解决不了时,他就可以从内核角度给他们出主意,提供工具。做底层的发展方向,应该是技术专家。
▍PART 04
其实,做底层还是做应用,之间并没有一个界线,有底层经验,再去做应用,会感觉很踏实。有了业务经验,再了解一下底层,很快就可以组成一个团队。
2 嵌入式Linux底层系统包含哪些东西?嵌入式LINUX里含有bootloader, 内核, 驱动程序、根文件系统这4大块。一、bootloader它就是一个稍微复杂的裸板程序。但是要把这裸板程序看懂写好一点都不容易。Windows下好用的工具弱化了我们的编程能力。很多人一玩嵌入式就用ADS、KEIL。能回答这几个问题吗?Q:一上电,CPU从哪里取指令执行?A:一般从Flash上指令。Q:但是Flash一般是只能读不能直接写的,如果用到全局变量,这些全局变量在哪里?A:全局变量应该在内存里。Q:那么谁把全局变量放到内存里去?A:长期用ADS、KEIL的朋友,你能回答吗?这需要"重定位"。在ADS或KEIL里,重定位的代码是制作这些工具的公司帮你写好了。你可曾去阅读过?Q:内存那么大,我怎么知道把"原来存在Flash上的内容"读到内存的"哪个地址去"?A:这个地址用"链接脚本"决定,在ADS里有scatter文件,KEIL里也有类似的文件。但是,你去研究过吗?Q:你说重定位是把程序从Flash复制到内存,那么这个程序可以读Flash啊?A:是的,要能操作Flash。当然不仅仅是这些,还有设置时钟让系统运行得更快等等。先自问自答到这里吧,对于bootloader这一个裸板程序,其实有3部分要点:①对硬件的操作对硬件的操作,需要看原理图、芯片手册。这需要一定的硬件知识,不要求能设计硬件,但是至少能看懂; 不求能看懂模拟电路,但是要能看懂数字电路。这方面的能力在学校里都可以学到,微机原理、数字电路这2本书就足够了。想速成的话,就先放掉这块吧,不懂就GOOGLE、发贴。另外,芯片手册是肯定要读的,别去找中文的,就看英文的。开始是非常痛苦,以后就会发现那些语法、词汇一旦熟悉后,读任何芯片手册都很容易。②对ARM体系处理器的了解对ARM体系处理器的了解,可以看杜春蕾的想成为高手,内核必须深刻了解。注意,是了解,要对里面的调度机制、内存管理机制、文件管理机制等等有所了解。推荐两本书:
1. 通读
又是硬件,还是要看得懂原理图、读得懂芯片手册,多练吧。①硬件本身的操作说到驱动框架,有一些书介绍一下。LDD3,即
当你写过Flash驱动,可能会知道Flash的性能有时候有多重要。3) C程序的自我修炼,是否考虑到软件工程方面的一些东西,程序的可维护性和扩展性,譬如LCD驱动,是不是从Sharp到NEC的只需要集中修改很少的几个地方?
对于不同品牌的Flash,如果使得Flash的驱动做的更具有灵活性。4) 如果有时间结余,可以关注Linux内核的发展。譬如LCD的驱动有没有考虑到V4L2通用架构,譬如网络驱动用到了NAPI了吗?当然在此之前,假设已经对LDD3, ULK2理解的比较熟了。5) 现在所作的这些驱动还算不得非常核心的东西。如果你想有更好的发展,可以考虑往audio,video,net方面发展,你应该多注意真个行业需要什么样的人才,上述每一项都需要很厚的底蕴,譬如video,需要了解MPEG4, H264等,怎么也要个1到2年才能算个入行阿,所以我建议不要只顾闷头做东西,要适当关注目前的一些应用。6) 对硬件知识的补给,做嵌入式Linux这一行不可能不读硬件的Spec,如果你对硬件的工作机制理解的比较透,会有助你写出性能好的驱动程序。
顺便提一点,适时的提高你的英语水平,对你的职业生涯绝对有帮助。(不要等需要的时候再补,来不及)7) 如果有时间,平时注意对Linux应用程序编写的了解/积累,也将有助于你写出很好功能很好的驱动程序。8) 永远不能以为自己做了很多东西,就驱动而言,像TVIN/TVOUT, USB, SDIO等等,好多未知领域呢。在问题还没有解决之前很难说清是哪里不对了。
有时候是datasheet里面的一句话没有注意,还有好几次调不出来最后查到是PCB的问题,所以有时候特别晕。5 嵌入式驱动自学者的感受经过了多年的嵌入式自学,可谓是不断在绝望中求生。性格使然,我是一个我也不知这种性格的学名叫什么,就是学习一种东西,非得想要能理解每一处的含义作用为什么,要这样做没有其他办法了吗等等问题。并且当一个问题找不到让我能接受的解释时,那么我的学习路程也就几乎要停在这里了,大概是因为我讨厌一知半解。可能是小时候被老师教导不要做书呆子的教育有关,小时候,听话孩子,认真,长辈的教育对孩子的影响真的是非常的大,很多影响如果你不细心的观察自己,你根本不能察觉这些进入了你骨子的观念,在我成长过程中,这些长辈的教育除了某些让我自己经历到并彻底认识到某个观念并不正确时,我才会形成自己的观点,自己的观念,但这些自己的观念在所有的价值观中,犹如沧海一粟。这种讨厌一知半解的性格,在现在这个社会来说,可以说是极端的,因为现在你学习使用的很多东西,他都不是从零开始的,就好比,你编程使用的是高级语言而不是低级语言不是机器码,所以我的整个学习过程是非常缓慢缓慢地进行着,这么说吧,前面说我经过了半年多的学习,但是到现在为止,我接触嵌入式已经有两个年头了,也就是说,学习期间,我有一年多是在停滞着。学习嵌入式,或者说学习现代的计算机编程,如果你想学好,有一个比较要求,那就是你能接受它的设定、它的模式。反过来说,当你真正接受它的设定、它的模式,并记住它们时,我认为,你已经学好了。昨天,我又置之死地而后生了一次。最近一直在搞驱动,一个LCD驱动搞得我几乎要放弃继续走嵌入式这条路。昨夜,睡不觉,打开嵌入学习视频,躺在已关灯很久的房间的床上,大概凌晨3,4点吧。之前我一直都是学习着驱动自编源码的教学,是那种几乎和裸板程序没多大区别的编程方式,只是多使用了一些向内核注册的接口函数。而最近我想换一下,因为很多设备驱动,内核都是自带的,而且是各种平台的设备驱动都有,我想如果能熟悉掌握内核自带驱动的编程,那以后要做某个设备的驱动时,我只需要在自带驱动中修改一下便好了,通过学习LCD平台设备的驱动,我了解了其编程想法,同时也认同这种想法,甚至让我疑惑,学习资料中教自编驱动的意义,为何不直接教如果修改内核源码驱动?于是,继续按着书去修改内核驱动源码,但问题是,书中说他们这种修改,代码成功运行了,但我这,无论怎么调试都失败,我反复检测,我的修改是否与书中一致,检测了很多遍依然没发现哪一步不同,不过,有一点发现是,书中的内核源码和我内核使用的源码有一点点区别(当然书里并没有把所有的源码都贴上,只是修改部分附近会联带着一些,这就是发现,这些联带的没需要修改的源码和我的源码有点区别,比如,我的源码中多了一些设置(看似无关紧要的设置))。与书核对无误但失败后,我又与成功运行的自编驱动核对,我陆续发现我修改的内涵源码中,没有去启动设备,也更没有去点亮背光,而在显存分配后的寄存器设置似乎也有问题,因为这里的地址使用各种宏定义不同的累加或计算,最后算得地址和我的寄存器地址也不知是否吻合,因为驱动源码中最后计算得到的是虚拟地址。于是我对比自编驱动,一点点修改尝试,到睡觉前都没成功。 我是想学得理直气壮一点的,最后是能一眼就能找到问题,并迅速轻松解决问题的,我也承认自己确实是有些浮躁。但是经过了昨晚床上的一点绝望的思考挣扎后,我好像想通了:为什么嵌入式学习视频老师要教自编驱动。下面我说下自编驱动与内核驱动源码各自的问题:自便驱动:程序简单简洁,它只能驱动特定的某个设备。如果设备换了需要支持另一款设备,那么你需要重新修改该驱动;如果需要系统同时支持两种LCD,那么它就会变成复杂并且对于内核驱动的简洁优势会削弱不少;如果你想驱动支持多种设备,那自编驱动,相对了内核驱动源码的简洁优势会变成了劣势,因为编程思想的适用范围不同而产生的结果。内核自带驱动源码: ①从系统层次去考量,变量、宏定义使用多,甚至有些宏定义的值为了方便能让各种在不同的阶段需要不同的值调用,把简单的一个赋值调用变成了需要进行多次运算才能检测到该值是否满足使用要求,因为我们不是该驱动的编码者,不清楚这样做的好处,也或许是内核驱动源码的开发者从整个系统的编程简洁性去考量,这样做或许也是为了让整个系统代码更少,简洁的一种做法,因为每个设备你都给它赋具体的值的话,整个系统中有几百种驱动设备源码,给所有设备的这个位置参数都赋一个值的话,那各设备关于这个值的代码就要多了几百行了,所以还不如,让各设备根据各种平台去对某个宏进行各自的计算来得到合适的值,但某些计算中相同的算法的也整合在一起,这样就减少了系统不少行代码。所以系统中驱动源码是系统开发者对系统源码的整合,是基于系统层的整合。所以,对于我这种对单个设备驱动编码的人,就会觉得系统源码有好多不人性化的地方,会觉得简单的地方也被弄得很复杂。②内核自带驱动还有一些代码是为了兼容以前的版本而添加了,比如以前硬件内存资源稀少,需要使用调色板的方法来减少程序运行时的内存使用量,这也会真假代码的复杂性,这一步虽不是必要的,但如果没弄好,那LCD驱动也不能正常使用。③程序复杂,为了适用在多种设备型号,更简单地添加不同型号的设备驱动,内核对驱动抽象分离,把驱动分为平台管理部分,驱动代码部分(与硬件无关码),和设备代码部分(硬件相关代码)。用户添加新型号设备驱动时,只需要在平台管理部分检查添加设备的匹配信息,和提供一个硬件设备相关的代码(有格式)文件即可。 现在,站在驱动开发者而非系统开发者的角度去衡量。 ①自编的驱动,简洁,要点明确。这个对于驱动开发者的用处就是:无论你使用的是哪个版本的内核,哪个芯片平台,你可以通过自编码比较简单方便地就可以确认硬件设备的情况,是否正常。如果自编码通过,那可以试用自编码上使用的参数去与内核进行核对、修改,然后再去测试。如果不成功,对于内核中多余的设置(这些大多可能是提供内核用做基本判断的变量)可以先屏蔽,编译出错了,根据提示,找到出错的位置修改添加。因为这些多余的设置,设置对了还行,设置错了,你又不好去定位错在哪。 自编的驱动在此处的用处,调试时,可以让你排除多余的失败可能性问题,在较少的代码去查出错误位置,如果你确定你的设置满足了该设备的必需设置,还是失败,你可以比较放心地去怀疑是硬件问题了。如果自编码成功,那个又可以当做你修改内核驱动的一个标准。②内核驱动源码支持管理多种型号的设备的优势是我用使用它的原因。先了解本版本本平台的设备驱动结构,如果是添加型号支持,那就根据自编驱动的参数与设置即可,如果是第一次启动这类设备,你就还需要检测结构是完整性,如果结构完整,参数无误依旧错误,那就把内核驱动源码精简到自编码的简单粗暴设置吧。最终就变成了在基于内核驱动架构下的自编驱动。如果还不行,那无疑是结构性问题了。所以自编驱动,还是有其存在价值的。内核驱动源码内容会变,平台会变,但自编驱动是变得最小的一个,也是最容易实现驱动目的的一个。是一码打天下不可缺少的重要组成部分。6 如何编写嵌入式Linux设备驱动程序?一、Linux device driver 的概念系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:1、对设备初始化和释放;2、把数据从内核传送到硬件和从硬件读取数据;3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;4、检测和处理设备出现的错误。在linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。二、实例剖析我们来写一个最简单的字符设备驱动程序。虽然它什么也不做,但是通过它可以了解Linux的设备驱动程序的工作原理。把下面的C代码输入机器,你就会获得一个真正的设备驱动程序。由于用户进程是通过设备文件同硬件打交道,对设备文件的操作方式不外乎就是一些系统调用,如 open,read,write,close…, 注意,不是fopen, fread,但是如何把系统调用和驱动程序关联起来呢?这需要了解一个非常关键的数据结构:
[cpp] view plain copy1. struct file_operations { 2. struct module *owner; 3. loff_t (*llseek) (struct file *, loff_t, int); 4. ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 5. ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); 6. ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); 7. ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); 8. int (*readdir) (struct file *, void *, filldir_t); 9. unsigned int (*poll) (struct file *, struct poll_table_struct *); 10. int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 11. long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); 12. long (*compat_ioctl) (struct file *, unsigned int, unsigned long); 13. int (*mmap) (struct file *, struct vm_area_struct *); 14. int (*open) (struct inode *, struct file *); 15. int (*flush) (struct file *, fl_owner_t id); 16. int (*release) (struct inode *, struct file *); 17. int (*fsync) (struct file *, int datasync); 18. int (*aio_fsync) (struct kiocb *, int datasync); 19. int (*fasync) (int, struct file *, int); 20. int (*lock) (struct file *, int, struct file_lock *); 21. ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 22. unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 23. int (*check_flags)(int); 24. int (*flock) (struct file *, int, struct file_lock *); 25. ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); 26. ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 27. int (*setlease)(struct file *, long, struct file_lock **); 28. }; 对于这个结构体中的元素来说,大家可以看到每个函数名前都有一个“*”,所以它们都是指向函数的指针。目前我们只需要关心
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);这几条,因为这篇文章就叫简单驱动。就是读(read)、写(write)、控制(ioctl)、打开(open)、卸载(release)。这个结构体在驱动中的作用就是把系统调用和驱动程序关联起来,它本身就是一系列指针的集合,每一个都对应一个系统调用。但是毕竟file_operation是针对文件定义的一个结构体,所以在写驱动时,其中有一些元素是用不到的,所以在2.6版本引入了一个针对驱动的结构体框架:platform,它是通过结构体platform_device来描述设备,用platform_driver描述设备驱动,它们都在源代码目录下的include/linux/platform_device.h中定义,内容如下:
[cpp] view plain copy 1. struct platform_device { 2. const char * name; 3. int id; 4. struct device dev; 5. u32 num_resources; 6. struct resource * resource; 7. const struct platform_device_id *id_entry; 8. /* arch specific additions */ 9. struct pdev_archdata archdata; 10. }; 11. struct platform_driver { 12. int (*probe)(struct platform_device *); 13. int (*remove)(struct platform_device *); 14. void (*shutdown)(struct platform_device *); 15. int (*suspend)(struct platform_device *, pm_message_t state); 16. int (*resume)(struct platform_device *); 17. struct device_driver driver; 18. const struct platform_device_id *id_table; 19. }; 对于第一个结构体来说,它的作用就是给一个设备进行登记作用,相当于设备的身份证,要有姓名,身份证号,还有你的住址,当然其他一些东西就直接从旧身份证上copy过来,这就是其中的struct device dev,这是传统设备的一个封装,基本就是copy的意思了。对于第二个结构体,因为Linux源代码都是C语言编写的,对于这里它是利用结构体和函数指针,来实现了C语言中没有的“类”这一种结构,使得驱动模型成为一个面向对象的结构。对于其中的struct device_driver driver,它是描述设备驱动的基本数据结构,它是在源代码目录下的include/linux/device.h中定义的,内容如下:
[cpp] view plain copy1. struct device_driver { 2. const char *name; 3. struct bus_type *bus; 4. struct module *owner; 5. const char *mod_name; /* used for built-in modules */ 6. bool suppress_bind_attrs; /* disables bind/unbind via sysfs */ 7. #if defined(CONFIG_OF) 8. const struct of_device_id *of_match_table; 9. #endif 10. int (*probe) (struct device *dev); 11. int (*remove) (struct device *dev); 12. void (*shutdown) (struct device *dev); 13. int (*suspend) (struct device *dev, pm_message_t state); 14. int (*resume) (struct device *dev); 15. const struct attribute_group **groups; 16. const struct dev_pm_ops *pm; 17. struct driver_private *p; 18. }; 依然全部都是以指针的形式定义的所有元素,对于驱动这一块来说,每一项肯定都是需要一个函数来实现的,如果不把它们集合起来,是很难管理的,而且很容易找不到,而且对于不同的驱动设备,它的每一个功能的函数名必定是不一样的,那么我们在开发的时候,需要用到这些函数的时候,就会很不方便,不可能在使用的时候去查找对应的源代码吧,所以就要进行一个封装,对于函数的封装,在C语言中一个对好的办法就是在结构体中使用指向函数的指针,这种方法其实我们在平时的程序开发中也可以使用,原则就是体现出一个“类”的感觉,就是面向对象的思想。在Linux系统中,设备可以大致分为3类:字符设备、块设备和网络设备,而每种设备中又分为不同的子系统,由于具有自身的一些特殊性质,所以有不能归到某个已经存在的子类中,所以可以说是便于管理,也可以说是为了达到同一种定义模式,所以linux系统把这些子系统归为一个新类:misc ,以结构体miscdevice描述,在源代码目录下的include/linux/miscdevice.h中定义,内容如下:
[cpp] view plain copy1. struct miscdevice { 2. int minor; 3. const char *name; 4. const struct file_operations *fops; 5. struct list_head list; 6. struct device *parent; 7. struct device *this_device; 8. const char *nodename; 9. mode_t mode; 10. };
对于这些设备,它们都拥有一个共同主设备号10,所以它们是以次设备号来区分的,对于它里面的元素,大应该很眼熟吧,而且还有一个我们更熟悉的list_head的元素,这里也可以应证我之前说的list_head就是一个桥梁的说法了。其实对于上面介绍的结构体,里面的元素的作用基本可以见名思意了,所以不用赘述了。其实写一个驱动模块就是填充上述的结构体,根据设备的功能和用途写相应的函数,然后对应到结构体中的指针,然后再写一个入口一个出口(就是模块编程中的init和exit)就可以了,一般情况下入口程序就是在注册platform_device和platform_driver(当然,这样说是针对以platform模式编写驱动程序)。8 嵌入式书籍推荐1. 硬件方面的书: 微机原理、数字电路,高校里的教材。
2. Linux方面的书:
<嵌入式Linux应用开发完全手册>