忙了几天,终于可以让ds18b20在自己的开发板的linux系统上跑了!虽然ds18b20不是什么新鲜玩意,但是想想知己可以给linux系统添加模块了还是有点小鸡冻呢!
虽然说现在硬件的资源非常丰富而且剩余很多,可以用软件资源来代替硬件资源,比如说可以用视频编解码软件取代硬件来工作。但有很多模块需要实时的采集数据这都是软件永远无法代替的,而且随着互联网的进一步发展,智能化也是一个必然的趋势,因此大量的传感器和控制芯片将被应用到生产生活中,所以个人觉得驱动开发还是个不错的方向。同时,作为学习者,再进行嵌入式linux开发过程中,可以接触到软件和硬件,对它们如何协同工作有了更加深刻的理解。这些东西在计算机的世界里应该算是内功了,修炼好了这些,在练习那些上乘的武功也许就更快了。
虽然成功了,但是通过这次开发我的确有一点小担忧,因为这次有意想不参考其他前辈写过的的资料,想自己靠数据手册来编写这个驱动,可是问题来了,突然自己一下傻了,不知道改怎么下手了,从那里下手了?怎么会这样呢?接触代码这么长时间,突然想自己“裸写”,居然没有思路,或者说有大致的思路,细节的地方有点模糊,总之下不了笔。后来想想互联网真是一把双刃剑呀!自己平时凭借这深厚的百度技巧几乎解决掉了学习中遇到了八成问题,于是对网络上的资源依赖便不知不觉中与日俱增,特别是一些直男前辈上来先贴代码,以前我很喜欢这样的前辈,现在想想有点忧虑了!还是自己的自控力不够啊!因此,鉴于此,本人以后的博客中将不在完整的贴代码了,只描述实现过程和想法。
好了,还是老毛病废话太多,下面就进入正题吧
关于DS18B20————————微电子间的“语言”
本次要想给linux添加的模块正是恶俗的ds18b20,关于ds18b20和嵌入式下的驱动网上资料都很多,基本的都不说了。就说说ds18b20与我们的核心芯片的是如何交流的。ds18b20和cpu都是半导体芯片,如果要通过ds18b20采集温度给cpu,他们之间必须存在有效的沟通和对话,人与人之间说话靠语言,它们之间交流也有语言,他们的语言便是对着连接他们的那根线进行高低电平控制以及控制的时间,那么他们拉高拉低电平也必须有规则吧!我们人类也有语法啊!它们之间确实存在语法,那便是datasheet中的时序图,这个时序图可是他们对话的语法,想hold住ds18b20,核心就在时序图。
以ds18b20的写时序为例描述这个对话过程:当我们的单片机(主控芯片)想要ds18b20执行任务的时候必须要告诉他任务,这个任务就是通过写命令发出的,单片机先拉低与ds18b20相连接IO口电平然后持续15us,它就好比给ds18b20说:“孩子,有任务 来了,过来取任务吧!“,ds18b20听到后就过来了,然后看看它与单片机相互链接的”专线“现在是高电平还是低电平,是高电平记为1,低电平是0.这样它反复接了8次就是一个完整的命令,ds18b20一看这个命令数据是0xbe,就知道单片机要来读温度数据了,赶紧做好准备。
ds18b20的故事就先聊到这里吧!毕竟这些东西时需要几次反复看和揣摩的。
不过在linux 系统下给ds18b20写驱动还是有不少方便的,ds18b20是一个对时间很敏感的器件,linux 中的udelay/ndelay为我们提供了微秒以及纳秒级别的精确延时,这点很方便。
另外系统给我提供了,s3c2410_gpio_setpin,s3c2410_gpio_getpin,等IO口操作函数,可以让我们方便的设置IO口的高低电平。它们中尤其值得注意的是s3c_2410_getpin函数,这个函数当IO口设置为input时,也就是输出的时候,它的返回值是0或者大于1的数,这里千万不要误以为返回值为1.特别是在编写读一个字节用到时,需特别注意。
关于嵌入式linux驱动————链接和规范
linux驱动的核心还在于器件本身,建议编写某个模块的 器件时,先熟悉器件在裸板上的驱动,先把这个器件的驱动每个细节搞清楚,然后在linux下只要注意规范和提供接口就可以了。
上一篇文章主要详细介绍了,应用程序到内核的接口,即系统函数与file_operations的关系映射。这一篇详细介绍设备与file_operations的链接,这样就形成了一个完整的linux下驱动链条。即 设备———>file_operations————>系统函数接口。
ds18b20是字符型设备,所以下面就详细介绍下如何创建一个字符设备以及这个字符设备使如何与file_operations发生关系的。
开始我们的工作前,先拍拍脑门清醒清醒,我们要干什么。。。。。。。
首先,要明确linux是一个有纪律有组织的机构,我们想在linux下添加一个新的模块,linux当然有这个机制了,所以在驱动编写中,一定要牢记一点规则很重要,既然我们添加的ds18b20是一个字符设备,那么你想linux这么大一个系统只有三种设备类型,它能不为字符类型的设备提供一个标准的样表吗?必须有啊!这货就是cdev,可以看出它就是英文的字符设备的缩写,它是一个结构体,所以必须要好好看看它。
1 struct cdev { 2 struct kobject kobj; 3 struct module *owner; 4 const struct file_operations *ops; 5 struct list_head list; 6 dev_t dev; 7 unsigned int count; 8 };
不看不知道,一看吓一跳,原来这字符设备内部的成员函数虽然不是很多,但是很管用啊!这第一个和四个我们就不说了,那都是系统自动填充的。作为小白我们把剩下的搞明白就棒棒的啦!首先看到了file_operatition这个成员函数,心中非常兴奋,因为file_operactions可是系统应用函数的接口啊,它可是驱动函数中的交通枢纽啊!它出现在字符设备的结构体里面,用屁股想一下得出了结论,只要我们初始化了这个file_operations这个成员函数,我们的字符设备和file_operations就建立了对应关系。那样当我们使用一个字符设备时,就知道调用哪一个file_operatitions了。
在看dev_t,这个是字符设备的设备号字符类型,就是我们的设备必须有一个设备号,就像我们学生要有一个学号一样,那为什么我们不直接使用名字,而要弄一个设备号呢!个人觉得第一数字更有调理,更容易检测设备号是否再用,第二linux系统中一个主设备号可以对应多个同类型的设备,鉴于此,设备还必须需要设备号了,还是那句话我们说这个没用,linux驱动开发最大的一个特点你就要适应规则,人家有这个成员函数,咱们就必须填写符合要求的。
经过一番激烈的分析,我们明白鸟,其实关于建立这个新的设备并要得到内核的承认,我们的核心任务就是要把cdev这个结构体给初始化完毕。这样才能达到,有了设备节点名称我们能知道设备的设备号,有了设备号我们知道系统函数调用的时候我们该提供那个设备的file_operactions函数,
ao,搜得死那! 下面就一起来填空把!
我们需要为这个填空来做准备:首先是获得dev_t dev;设备号
step1:要创建linux系统可接纳的驱动,我们必须向linux申请一个设备号以給我们的设备 用。
创建设备在2.6内核以前的版本是很easy的,只需要一个函数,便可申请/分配设备内存/注册设备,但是以后的内核机制中,这种方便的东西就被废除了,因为以前是我们设定一个设备号,然后申请,这种做法在linux大量应用时容易造成设备号重复冲突。因此在新的内核机制中建议大家动态的申请设备号,这样以来系统便会自动找到一个没有使用的设备号。linux 中比较经典的处理方法来自ldd书中的这段:
1 dev_t dev; 2 dev=MKDEV(ds18b20_major,ds18b20_minor); 3 if(ds18b20_major) 4 { 5 ret=register_chrdev_region(dev,ds18b20_devs,"ds18b20"); 6 } 7 else 8 { 9 ret=alloc_chrdev_region(&dev,ds18b20_minor,ds18b20_devs,"ds18b20"); 10 ds18b20_major=MAJOR(dev); 11 }
dev_t是用来保存设备编号的类型,MKDEV是主次设备号转换成dev_t类型。
然后就是检测主设备号是否为0,为0则标示要进行动态申请设备号。这里一般建议采用动态申请的方式。
申请完毕后可以用MAJOR(dev)来获取申请的主设备号。
step2:file_operactions函数的书写,这个也可是重中之重,它的写法由于前面已经接受过了,这里就不多提了。
1 static struct file_operations ds18b20_fops={ 2 .owner=THIS_MODULE, 3 .open=ds18b20_open, 4 .read=ds18b20_read, 5 };
当然这个结构体中的open,read函数就不罗列在这坑害后人了。
有了上面的两步之后那我们就开始填空啦!
这次填空分为两步,需要两个函数,第一个是cdev_init,第二个是cdev_add;
有了这两个函数我们进行填空:1 首先调用cdev_init;将我们的file_operactions填进去。
1 void cdev_init(struct cdev *cdev, const struct file_operations *fops) 2 3 参数: 4 5 cdev:之前我定义的cdev结构体; 6 7 fops:设备对应的文件操作结构体。 8 9 返回值:(函数有可能失败,查看返回值是必须的) 10 11 成功返回0,示范返回对应的错误码
2 下面填写设备的设备号 调用的函数是cdev_add
1 int cdev_add(struct cdev *cdev, dev_t dev, unsigned count) 2 3 参数: 4 5 cdev:指定要被添加的cdev结构体; 6 7 dev:对应的设备号 8 9 count:从设备号dev开始添加count个设备. 10 11 返回值: 12 13 成功返回0,失败返回对应的错误码。
这个函数就将我们的设备号同设备关联起来了,这样以来我们的字符设备结构体就填充完毕了,我们就将这个字符设备加入到内核里面了。
这样以来我们的字符设备——设备编号——file_operactions就建立起了联系,而file_operactions又是系统函数的枢纽,所以这样在编写系统的应用函数的时候,我们就可以通过联系设备的节点而知道该操作那个设备的file_operactions了,因为它们变成了一个环,通过其中的一节一定能够找到环中的另一节。
这样看来还少设备节点啊!下面还需要做的就是创建一个设备节点, 创建设备节点的时候我们一般采用自动创建设备节点的办法,linux中为我们提供了struct class结构体,这个结构体对应一个类,我们用class_create函数来创建一个存放于sysfs里面的类,然后调用device_create就可以创建一个设备节点了。
1 static struct class *ds18b20_class; 2 static struct class_device *ds18b20_class_dev;
/
..........................................................
/
3 ds18b20_class=class_create(THIS_MODULE,"ds18b20_sys_class"); 4 if(IS_ERR(ds18b20_class)) 5 return PTR_ERR(ds18b20_class); 6 ds18b20_class_dev=device_create(ds18b20_class,NULL,MKDEV(ds18b20_major,0),NULL,"ds18b20"); 7 if(unlikely(IS_ERR(ds18b20_class_dev))) 8 return PTR_ERR(ds18b20_class_dev);
在做这个驱动的过程中,遇到很多好的文章,分享给大家
第一是位是jammy前辈的:
jammy的代码中大量使用了预处理,使得代码程序中的代码可读性非常强,非常值得学习,不过程序中有处小小的错误。而且jammy是在09年写的这篇文章,文中很多设备的注册在新版内核中是不被注册的。
第二位是小白前辈的:
http://blog.chinaunix.net/uid-25014876-id-59416.html
不多说了,小白前辈的文章灰常认真,灰常全面,条理清楚,看的出我很多的东东都是他哪儿的。
第三位是sg131971前辈的:
http://www.linuxidc.com/Linux/2012-02/54677.htm
这位前辈是直男,直接上代码,它的文章字符注册采用新的机制,可以供参考。