Linux下的虚拟串口驱动(二)

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

前言

上一篇,简单介绍了裸机驱动与设备驱动的区别,以及Linux内核下字符设备的驱动架构;无操作系统时,上层应用直接访问驱动接口,应用工程师需要知道每一个设备的驱动接口。需要访问Flash时,就调用Drv_FlashRead()/Drv_FlashWrite()等函数;需要使用串口时,就调用Drv_SerialSend()/Drv_SerialWrite()等接口,简单说挂载了多少种不同的读写设备,上层应用就要使用多少种不同的读写接口函数。与之对应,在Linux操作系统下,用户层无需知道这些分类,对应用工程师来说,读、写操作只用read()/write()两个函数,无需关心底层有多少种读写接口。读Flash时,使用read(“flash”,…,…);读串口时,使用read(“serial”,…,…)(引号内代表驱动反映到上层时的文件名/设备名,省略号代表其它参数),操作系统和文件系统会根据传入的不同参数,去区分该调用底层哪一个驱动函数。这个过程就会使用到前面提到的主次设备号和file_oprations这个结构体。

裸机驱动下的LED接口

这里我们先假设LED是通过GPIO口来控制的。

#include ...                                    /* 一系列需要包含的头文件                          */
#define     GPIO_REG_CTRL       0x00000000      /* GPIO控制寄存器的物理地址(具体地址看相应数据手册)  */
#define     GPIO_REG_DATA       0x00000010      /* GPIO数据寄存器的物理地址(具体地址看相应数据手册)  */

extern void Drv_LedInit( void );                /* LED初始化函数,外部声明,供应用层或其它模块调用    */
extern void Drv_LedOn( void );                  /* LED点亮函数,外部声明,供应用层或其它模块调用     */
extern void Drv_LedOff( void );                 /* LED关闭函数,外部声明,供应用层或其它模块调用     */

void Drv_LedInit( void )
{
    GPIO_REG_CTRL |= ( 1 << n );                /* 假设将控制寄存器的第n为置1,可设置GPIO为输出模式  */
}

void Drv_LedOn( void )
{
    GPIO_REG_DATA |= ( 1 << n );                /* 假设将数据寄存器的第n为置1,可设置GPIO为输出模式  */
}

void Drv_LedOff( void )
{
    GPIO_REG_DATA &= ~( 1 << n );              /* 假设将数据寄存器的第n为清0,可设置GPIO为输出模式  */
}

当上层应用,需要操作LED时,就可以调用这三个函数。看起来好像挺简单的,底层只要实现这三个函数就可以。但上层应用必须明确的知道这三个函数的作用。这还只是LED,其它设备也有自己的接口函数,所以就需要应用工程师清楚地知道每个设备的驱动接口,显然不是很方便。

Linux下的设备驱动

前篇提到,有操作系统时的驱动,需要按照操作系统给出的驱动架构来实现。所以Linux下的设备驱动也要按照一定的架构来实现。
同样是前面的实现LED驱动,也要实现LED的初始化、点亮、熄灭操作;但操作系统要求按照统一的驱动格式来实现,所以就不能直接给上层应用提供Drv_LedInit()、Drv_LedOn()、Drv_LedOff()这样的驱动接口;人在屋檐下,不能不低头嘛,想在操作系统下工作,就必须按操作系统要求的来。
结合前篇的驱动架构图,可以知道,用户空间是通过系统调用来访问底层设备的。操作系统需要特殊的信息(设备号)来识别应用层想要访问的是哪个设备,所以操作系统要求驱动都有自己的设备号,这样操作系统才能找到你,不然上层应用只是一个单纯的read()系统调用,操作瞬间就慌乱了,底层这么多的设备,它如何能快速定位到相应接口。如果她们能对话的话,应该是这样的一副场景:
应用层:嗨,操作系统!帮我呼叫一下Flash吧,我要从它那拿些资料。
操作系统:哦,好的!我帮你找它,你等一下。
然后操作系统去打开自己的管理资料,发现这任务根本完不成,因为它根本不知道谁是Flash,好多设备长得都差不多,它区分不开谁是Flash,谁是LED,谁是串口;更别说一些双胞胎兄弟(不同型号的同种设备),它发现也存在同名同姓的情况。这时候操作系统想了一个办法,为所有的设备都分配一个ID,就像身份证号码一样,作为唯一的标识符。而且为了提高效率,它规定所有同姓(同种)的设备,主设备号都一样,然后次设备号来具体区分某个设备,就像身份证号用前三位区分省份,紧接着三位区分城市一样。
所以在设计Linux下的设备驱动时,我们要先分配设备号。

    dev_t dev = MKDEV( DRV_LEDMAJOR, 0 );               /* 将主次设备号,生成dev_t数据类型       */
    /* 前面提到过,Linux内核会为每个设备注册一个固定的设备号,上层应用访问驱动时,内核就可以知道*/
    /* 该访问哪一个驱动*/

然后操作系统为了更加方便的管理这些设备,为每一类设备都定义了一个用来管理设备的数据结构(可以理解为表格),驱动在注册进系统时,要先填这张表,这样操作系统在找到驱动时,就可以很快的了解到设备信息和设备的功能(操作接口)。

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

在设计驱动时,要完成对你这张表的填写,将设备拥有的技能展现给操作系统(struct file_operations),然后将这张表提供给操作系统,这样操作系统就可以方便高效的去管理设备。这样当上层应用想要访问某个设备时,操作系统就可以根据设备号,快速的找到这张表,定位到相应的操作(通过struct file_operations)。

#include 
#include 
#include ...

#define     DRV_LEDMAJOR        0                       /* 主设备号                             */

/* LED的操作,都会映射在这个结构体中,最后填充到cedv这个结构体里                               */
struct file_operations led_oprations = {
    .owner = THIS_MODULE,
    .read = led_read,
    .write = led_write,
    .open = led_open,
    .ioctl = led_ioctl,
}

/* 可以理解为上诉所说的Drv_LedInit()函数                                                        */
int led_open( struct incode *incode, struct file *filp)
{
    return 1;
}
/* 读写函数,此处可要可不要                                                                     */
ssize_t led_read( struct file *filp, char _user *buf, size_t count, loff_t *f_pos )
{
    return 1;
}
/* 读写函数,此处可要可不要                                                                     */
ssize_t led_write( struct file *filp, const char _user *buf, size_t count, loff_t *f_pos )
{
    return 1;
}
/* ioctl函数,对LED的控制操作,在这个函数内                                                     */
int led_ioctl( struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg )
{
    struct led_dev *dev = filp->private_data;

    switch( cmd )                                       /* 上层使用ioctl这个系统调用时,传进来的参数*/
    {
        case LED_ON:
            Drv_LedOn();                                /* 类似于上面提到了,LED点亮函数         */
            break;
        case LED_OFF:
            Drv_LedOff();                               /* 类似于上面提到了,LED关闭函数         */
            break;
        default:
            return -ENOTTY;
    }

    return 0;
}
/* 驱动初始化                                                                                    */
int Drv_LedInit( void )
{
    int ret = 0;
    struct cdev *led_devp = NULL;

    dev_t dev = MKDEV( DRV_LEDMAJOR, 0 );               /* 将主次设备号,生成dev_t数据类型       */
    /* 前面提到过,Linux内核会为每个设备注册一个固定的设备号,上层应用访问驱动时,内核就可以知道*/
    /* 该访问哪一个驱动*/

    if( 0 != DRV_LEDMAJOR )                             /* 当主设备号为0时,主设备号将由操作系统分配*/
    {
        ret = register_chrdev_region( dev, 1, "LED");   /* 指定设备号                                */
    }
    else
    {
        register_chrdev_region( &dev, 0, 1, "LED");     /* 系统分配设备号                      */
    }

    if( ret < 0 )
    {
        return ret;
    }

    led_devp = kmalloc( sizeof( struct dev ), GFP_KERNEL );/* 为cdev结构分配空间               */
    if (led_devp == NULL)
    {
        dev_err(&dev->dev, "No memory for device\n");
        return -ENOMEM;
    }
    memset( led_devp, 0,  sizeof( struct dev ) );

    cdev_init( &led_devp->cdev, &led_oprations );       /* 初始化cdev结构体                       */
    led_devp->cdev.owner = THIS_MODULE;

    /* 具体的函数实现,都在此结构体中                                                          */
    led_devp->cdev.ops = &led_oprations;                /* 将设备操作函数,映射到cdev中         */
    ret = cdev_add( &led_devp->cdev, devno, 1 );        /* 将cdev结构体,添加到系统中          */
}

/* 驱动卸载                                                                                     */
int Drv_LedExit( void )
{
    cdev_del( &led_devp->cdev );
    kfree( led_devp->cdev );
    unregister_chrdev_region( MKDEV( DRV_LEDMAJOR, 0 ), 1);
}

module_init( Drv_LedInit );                         /* (insmode)加载驱动时,会自动识别         */
module_exit( Drv_LedExit );                         /* (rmmode)卸载驱动时,会自动识别          */

上面给出了相对完整的驱动代码,当我们通过insmode加载驱动时,系统会自动识别去执行module_init()函数,然后驱动转而去执行Drv_LedInit ()函数,完成主次设备号的分配和cdev结构体的填充,将cdev添加到系统中。从代码中可以看到,原来在裸机驱动中的Drv_LedOn()、Drv_LedOff()函数,放在了ioctl()函数中,这样当上层应用使用ioctl()这个系统调用,就可以完成对LED的控制,而不在关心底层LED的开关操作是如何命名以及实现的。
所以到此我们应该有个大致的了解,有操作系统时的驱动,要比裸机驱动做的工作相对较多,这主要是为了实现操作系统的统一接口,来给上层应用制造便利。

你可能感兴趣的:(Linux串口驱动)