欢迎转载,转载请注明出处。

前言

最近准备在Linux下,实现虚拟串口驱动;但因为毕业后,一直从事的是裸机驱动开发,所以Linux下的设备驱动,就慢慢忘记了;为了实现这一功能,在网上也查找了很多资料,但大多只是讲解理论,或者直接贴代码;对于没接触过Linux驱动或者初学者来说,理解起来比较吃力;所以小弟我,打算将这几天整理的资料和自己的理解,结合相关代码,分享出自己的想法,希望能给有此需求的人,贡献微薄之力。

裸机、设备驱动

前面提到裸机驱动和设备驱动,在这里简单说明一下两者的区别。设备驱动,顾名思义就是“驱使设备行动”,这里的裸机驱动也应该属于设备驱动的范畴,但因为笔者的习惯,将无操作系统下的驱动,称为裸机驱动,所以这里做了区分。在没有操作系统的情况下,我们可以根据硬件设备的特点自行定义接口,如对串口定义Drv_SerialSend()、Drv_SerialRecv(),对RTC(实时时钟)定义Drv_TimeSet()、Drv_TimeGet()等。但在有操作系统时,驱动的架构就应该由操作系统来定义,我们只有按照相应的架构设计驱动,这样,驱动才能更好的整合入操作系统内核。


从图片中我们可以看到,有了操作系统后,不仅没让驱动变得简单,反而更麻烦了。为什么不能像裸机驱动那样,需要什么操作就直接调用哪个函数接口,例如将数据写入Flash时,就直接调用Drv_FlashWrite(),需要往串口发送数据时,就直接调用Drv_SerialSend(),这样不是更直接,更简单吗;听起来好像挺有道理的,比起通过系统调用接口,文件系统,间接的去访问驱动接口,直接调用驱动函数,好像更高效;但存在即是合理的,设计师们设计这样一套驱动架构,肯定是有理由的。我们先来看一张图。

我们可以看到,在drivers/serial目录下,列出了Linux2.6内核支持的串口设备,只是三星公司的s3c系列就分了好几种,区分的原因肯定是三星公司在处理器的设计结构,或者串口设备的架构,驱动电路,访问方式等做了修改。
试想一下,如果三星公司为了区分这些设备,在驱动命名上做了区分,分别以处理器类型来命名相应的驱动接口,如Drv_S3c2440SerialSend();Drv_S3c2410SerialSend();Drv_S3c6410SerialSend();这时上层应用工程师,在S2C2440这款处理器上,以直接访问驱动接口的形式,写了一个串口应用程序;某天因为业务需要,要求在S3C6410的处理器上实现相同的功能,本来以为只是个简单的移植工作,后来发现底层驱动函数名字全变了,那他还需要把所有的Drv_S3c2440SerialSend()函数,改成Drv_S3c6410SerialSend(),所有相关的初始化,发送,接收等操作都要修改。这将是多么头疼的一件事。
再举个例子,如果我们的设备搭载的多个不同的Flash存储器,这时候一个简单的Drv_FlashWrite()很明显就不够了,不同的Flash存储器,操作方式不同,为了正确的使用这些存储器,底层驱动就需要作出区分,提供出不同的函数接口,并且上层应用必须理解这些不同的驱动接口,这样才能知道分别操作的是哪一类Flash存储器。
与之相比,有操作系统时,在Linux下,上层应用只需要通过read()、write()这两个函数,就可访问任何设备,即使是不同的处理器或者不同的设备,只要通过传入的参数(文件名/设备名)就可以完成对不同设备的访问,上层应用工程师,完全不用考虑底层驱动接口是什么样的,给他们的印象就是,不管设备上搭载了多少种不同的Flash存储器,一个read()函数,就可访问所有存储器;而且即使更换了处理器,在S2C2440这款处理器上实现的串口应用程序,在S3C6410的处理器上也可以直接使用,因为操作系统和文件系统给上层提供了统一的接口,所以上层应用不用去关心,底层做了哪些修改。
其实操作系统就是通过给设备驱动制造麻烦,进而给上层应用提供便利。驱动按照操作系统提供的驱动框架和统一接口来设计,这样上层应用就可以通过操作系统提供的统一的系统调用,来访问驱动。了解过Linux的都知道,Linux下一切设备皆文件,我们可以通过read()、write()这两个函数,去访问任何设备,这就为上层应用提供了极大地便利。

Linux下的驱动架构

前面说到,有操作系统时,驱动应该按照操作系统给的驱动架构来设计。那么在Linux下的驱动架构应该是什么样的呢?首先结合我们前面说的,Linux的上层用户空间,可以通过操作文件的形式来访问设备;那这里我们应该有这样的疑问:为什么访问文件时,就可以访问设备?文件和设备是如何绑定的?上层应用同样调用read()、write()两个函数,操作系统如何知道具体是哪个驱动的读、写函数?这些问题,我都会在后面一一阐述。
在Linux下,设备可以分为三类:字符设备、块设备和网络设备
字符设备:一种能像字节流一样进行串行访问的设备,对设备的存取只能按顺序按字节存取,不能随机访问;并且字符设备没有请求缓存区,所以必须按顺序执行所有的访问请求,例如键盘,在当前输入未完成前,无法响应下一个输入。
块设备:具有请求缓冲区,可以从任意位置读取任意长度,即支持随机访问,例如硬盘,我们可以随机访问任意扇区,任意长度。
网络设备:网络设备是面向数据报文的,字符设备是面向字符流的,网络设备和字符设备一样,不支持随机访问,也没有缓冲区,交换机,路由器等都是网络设备,它们都是以数据报的形式进行数据处理。
三类不同设备,在驱动架构上,又略微有所区别,下面以字符设备为例,阐述一下Linux下的驱动架构。

图中的ssize_t (read) (struct file , char __user , size_t, loff_t );
ssize_t (write) (struct file , const char __user , size_t, loff_t )等函数就类似于前面提到的Drv_FlashRead(),Drv_FlashWrite()等函数,Linux内核把这些函数统一“映射”到file_operation这个结构体中,这样通过操作系统和文件系统给上层统一成read(),write()这些系统调用。所以在有操作系统时,底层驱动还是要实现Drv_FlashRead(),Drv_FlashWrite()这样的函数,只不过是多了一些操作。

struct cdev {struct kobject kobj;                       /* 内嵌的kobject对象  */struct module *owner;                      /* 所属模块           */const struct file_operations *ops;         /* 文件操作结构体      */struct list_head list;                     /* 内核链表           */dev_t dev;                                 /* 设备号             */unsigned int count;
};

在Linux2.6的内核中,使用cdev这样一个结构体来管理字符设备。这里我先阐述一下,dev_t dev和const struct file_oprations *ops这两个成员,其他的后面再做解释。

从图中我们可以看到,在时间信息前,有两列数字;其中靠左的一列代表主设备号,靠右的一列代表次设备号;仔细看这张图,会发现“scd0”的次设备号都是4,“fd0u……”的主设备号相同,次设备号不同,所以这里可以做个简单的解释就是,Linux内核用主设备号来判定某种设备的驱动,次设备号标识具体的设备;或者说主设备号标识某一特定的驱动程序,次设备号表示使用该驱动程序的各设备。

#if defined(DJGPP) || defined(__CYGWIN32__)
#ifdef KERNEL
typedef unsigned long u_long;
typedef unsigned int u_int;
typedef unsigned short u_short;
typedef u_long ino_t;
typedef u_long dev_t;
typedef void * caddr_t;
#ifdef DOS
typedef unsigned __int64 u_quad_t;
#else
typedef unsigned long long u_quad_t;
#endif

在Linux里,dev_t被定义成unsigned long,为32位,其中12位主设备号20位次设备号,在我们编写驱动时,会将设备名(对应上层文件名),与设备号“绑定”,这样当上层应用对设备(文件)进行操作时,内核就可以根据设备号,知道该调用哪一驱动。

/** NOTE:* read, write, poll, fsync, readv, writev, unlocked_ioctl and compat_ioctl* can be called without the big kernel lock held in all filesystems.*/
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int (*readdir) (struct file *, void *, filldir_t);unsigned int (*poll) (struct file *, struct poll_table_struct *);int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, struct dentry *, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **);
};

从这个结构体中,可以看到llseek()、read()、write()等函数,这就与我们在用户空间经常使用到的系统调用*llseek()、read()、write(),或者fseek()、fread()、fwrite()等库函数*相对应(实际库函数最终也会通过系统调用访问设备)。
到此,我们可以了解到,Linux内核里,驱动的主要工作有两个部分,一是为驱动分配主次设备号,将设备名(文件名)与主次设备号绑定;二是填充file_operations结构体,即实现结构体中的相应函数实体。这与裸机驱动中只是实现相应接口函数,区别还是挺大的。
暂时就写到这吧!明天再通过实例,讲解驱动实现过程。

Linux下的虚拟串口驱动(一)相关推荐

  1. Linux下自定义虚拟串口驱动

    前些天给新的板子修改BUG的时候,发现这块板子的串口是接在板载MCU上,我们的主SOC(海思HI3520D)上已经没有多余的串口.于是问题来了- 我们很多的上层应用都是通过串口和传感器通讯的,而且程序 ...

  2. ARM Linux下安装CH341串口驱动

    在arm-Linux环境下安装CH341串口驱动需要单独编译串口的驱动.本人编译环境Ubuntu 14.04 gcc编译工具arm-linux-gnueabihf-gcc. 1.代码检查 查看内核目录 ...

  3. Linux下添加虚拟串口,接收和发送数据

    之前写的那虚拟串口有点问题,只能读取,不能接收.今天再来改一下.将python的内容改为如下: 先新建一个文档,内容如下 #! /usr/bin/env python#coding=utf-8impo ...

  4. linux下Qt编写串口调试助手,如何在linux下用QT写一个简单的串口调试助手

    如何在linux下用QT写一个简单的串口调试助手 QT5串口类 在QT5以前,编写串口一般使用的是qextserialport类,但在QT5之后有了QT自带的串口类SerialPort(串口基础类)和 ...

  5. ESP32在WIN7下USB调试串口驱动安装 解决USB JTAG/serial debug unit (Interface 0)无法安装驱动的问题

    使用WIN7开发ESP32, 但一直不能解决USB烧录问题,  就是USB serial 总是无法安装, 虽然可以在WIN10下使用, 但切换虚拟机也麻烦 经常仔细对比查找分析, 问题解决, 顺利安装 ...

  6. linux下c的串口收发

    linux下c的串口收发录 转自: https://blog.csdn.net/weixin_41471318/article/details/116230465 文章目录 linux下c的串口收发录 ...

  7. 什么是 Linux 下的 platform 设备驱动

    Linux下的字符设备驱动一般都比较简单,只是对IO进行简单的读写操作.但是I2C.SPI.LCD.USB等外设的驱动就比较复杂了,需要考虑到驱动的可重用性,以避免内核中存在大量重复代码,为此人们提出 ...

  8. Linux下PCI转串口卡驱动安装方法

    Linux下PCI转串口卡驱动安装方法 ----------------------------------- 由于公司产品要做行业市场,而产品与行业用户间PC的通讯为RS232串口方式.而行业用户那 ...

  9. Linux下使用虚拟光驱

    在Linux下使用虚拟光驱 其实根本不需要什么虚拟光驱软件,用mount命令就可以完成. 1. 把光盘制作成iso文件 cp /dev/cdrom XXXXX.iso XXXXX.iso为你所命名的镜 ...

最新文章

  1. 一次心惊肉跳的服务器误删文件的恢复过程
  2. 安装多个版本的jdk
  3. 【事故反演】配置过程(变位)
  4. scrapy item引用时报错
  5. 数字图像处理技术的应 用领域
  6. 那些年我们用过神级的代码注释
  7. 把字符串变为变量_python学习第10课--列表和字符串的可变性
  8. GC 年轻代 老年代 持久代
  9. Solr相关概念详解:SolrRequestHandler
  10. 【优化调度】基于matlab粒子群算法求解梯级水电站调度优化问题【含Matlab源码 065期】
  11. 数字信号处理教程答案及解析(第五版)
  12. wps中有公式如何调整间距
  13. 网络基础-应用层:E-mail应用:SMTP协议,POP协议,IMAP协议
  14. 高数——齐次方程中齐次的解释
  15. 专访李明远,理清直达号九大疑问
  16. 2021年茶艺师(初级)考试题库及茶艺师(初级)试题及解析
  17. ip ban linux,linux – fail2ban:unban ip如何(使用fail2ban-client)
  18. 微型计算机基础知识答案,第1章 微型计算机基础知识 题库和答案.doc
  19. 本地缓存之王-Caffeine
  20. 【排序专训】练习题 士兵站队(中位数应用) 解题报告

热门文章

  1. JDK 18 / Java 18 GA 来了
  2. Python 密码生成及密码复杂度检测
  3. 面向服务软件测试,面向服务的网格软件测试环境.PDF
  4. 富士康500万iphone遭退货
  5. C语言中使用printf()打印漂亮的颜色字体
  6. 保研数学知识复习总结
  7. “中国李宁“,能否救李宁?
  8. 【分享】电子价格标签系统方案
  9. Vue2.0不可忽视的变化
  10. android5.1.1 触摸震动,基于android5.1无显示触摸系统的系统镜像裁剪