字符驱动程序框架与应用测试程序编写

该文章主要目的为了学习并掌握以下几个方面:

  1. 熟悉并编写字符设备驱动框架
  2. 学习应用层的测试程序编写
  3. 内核空间与用户空间的数据传输
  4. 驱动安装与卸载、设备文件查看、应用程序运行等相关命令使用。

嵌入式系统框架简单介绍

嵌入式系统由硬件、驱动、操作系统、应用,这几部分层次构成。其中,驱动程序是硬件层与系统层之间的交互层,主要作用是操作底层硬件,实现硬件控制,而应用层位于操作系统层之上,应用层以操作系统为中介,对驱动层的相关主要函数进行调用(如open(),read(),write()等函数),进而实现对硬件控制。
通常,驱动程序是编译进操作系统内核中的,与内核融为一体,区别在于,驱动程序可以以模块化的形式动态编译进内核中,相当于拼图一样,灵活可拆卸。操作系统可以理解为庞大的函数库,提供驱动和应用程序的调用。
应用程序的存放在文件系统中,文件系统可以理解为一个目录系统,由很多目录组成,而这些应用程序就可以存放在某目录下某文件中(相当于Windows电脑下各个盘符和目录一样,存在各个应用程序和文件),因此应用程序可以方便被查找和运行。
(以上均为个人理解o( ̄︶ ̄)o,有不足之处请指教哦。下面正式进入实操)


实验内容与源码

编写驱动程序和测试程序,实现:开发板任意2个按键按下,3个led灯反转,并在shell终端显示开发板运行的相关信息。

前期条件:开发板烧录好u-boot,linux内核,文件系统
芯片:S3C440
开发板:韦老大的JZ2440
引脚说明 KEY1:GPP0,KEY2:GPG3 LED1:GPP4,LED2:GPP5,LED3:GPP6

本实验源码下载链接点击此处


字符驱动程序框架与编写

驱动程序很多中,本文章以字符驱动程序为入门。学完驱动程序后,就感觉还算蛮简单,主要是熟悉它的整体结构形式,以及底层寄存器的控制。下面为编写流程:

1. 新建以驱动源码文件,名为:drv_keyled.c ,然后找好一个已经写好的字符驱动源码,作为模板参考。
2. 将需要的头文件写进来,可以直接从模板中copy过来(这些头文件就是系统内核的源码库,驱动程序会从中调用),头文件如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

3.定义需要用的寄存器名称等

typedef  unsigned long Uint32;
volatile unsigned long *gpiog_con=NULL;     //寄存器
volatile unsigned long *gpiog_data=NULL;   //寄存器

volatile unsigned long *gpiof_con=NULL;  //寄存器
volatile unsigned long *gpiof_data=NULL;//寄存器

int major;//主设备号

//类和设备类,可以帮助用于自动创建设备文件
static struct class* drv_keyled_class;  //定义类
static struct class_device* drv_keyled_class_device;  //定义一类设备

4.绑定驱动有关的重要函数到内核中。关联绑定后,使得应用层(用户层)调用open(),read(),write()等函数时,系统能找到驱动对应的xxx_open(),xxx_read(),xxx_write()函数,代码如下:

//关键函数绑定(结构体)
static struct file_operations drv_key_fops=
{
    .owner = THIS_MODULE,
    .write = drv_keyled_write,
    .read = drv_keyled_read,
    .open = drv_keyled_open,

};

//初始化、卸载函数绑定(宏)
module_init(drv_keyled_init);
module_exit(drv_keyled_exit);

5.关键驱动函数编写——初始化和卸载函数(即drv_keyled_init,drv_keyled_exit)。

初始化函数主要实现功能如下几点:(卸载函数与初始化函数相反,略述)

  • 向内核注册驱动,即将结构体绑定的关键函数告诉内核,同时从内核中获取主设备号:major,主设备号用于应用程序对具体哪个驱动的识别。
  • 创建类和类设备,运行后,系统能自动创建设备文件:/dev/keyled,相当于应用层上手动完成“mknod“命令操作。
  • 物理地址映射到虚拟地址VA(ioremap函数),由于系统开启了MMU,因此程序都是在虚拟地址运行,如果要完成指定寄存器的控制,就需要将该寄存器的物理地址进行映射,通过虚拟地址完成寄存器控制。

初始化和卸载函数调用方法:

  • 当应用层运行命令“insmod”进行驱动装载时,系统自动调用初始化函数。
  • 当应用层运行命令“rmmod”进行驱动卸载时,系统自动调用卸载函数。

源码如下:


/*
函数名:drv_keyled_init
功能:初始化模块功能(insmod装载驱动时调用)
*/
static int drv_keyled_init(void)
{

    //注册主设备号,由fops结构体告诉内核绑定的函数
    major = register_chrdev(0, "drv_keyled", &drv_key_fops);

    //创建类
    drv_keyled_class = class_create(THIS_MODULE, "drv_keyled");
    //创建类设备
    drv_keyled_class_device = class_device_create(drv_keyled_class, NULL, MKDEV(major, 0), NULL, "keyled");//   "/dev/keyled"

    //虚拟地址VA映射
    gpiof_con = (unsigned long*)ioremap(0x56000050, 12); //映射物理地址的起始地址与长度,返回虚拟地址
    gpiof_data =  gpiof_con+1;//地址在类型长度上加1(即+4地址)

    gpiog_con = (unsigned long*)ioremap(0x56000060, 12); //映射物理地址的起始地址与长度,返回虚拟地址
    gpiog_data =  gpiog_con+1;

    return 0;
}

/*
函数名:drv_keyled_exit
功能:卸载模块功能(rmmod卸载驱动时调用)
*/
static void drv_keyled_exit(void)
{
    unregister_chrdev(major, "drv_keyled");
    class_device_unregister(drv_keyled_class_device);
    class_destroy(drv_keyled_class);

    iounmap(gpiof_con);
    iounmap(gpiog_con);

}

6.关键驱动函数编写—–xxx_open(),xxx_read()等函数(即drv_keyled_open,drv_keyled_open等)。

该部分的编写是编写驱动源码的核心内容,实现了对寄存器配置与控制操作。当应用层调用open(),read(),系统就得调用这些对应的函数,因此,这些函数里的内容以及要实现什么样的功能,由用户自行发挥。该实验将这些函数定义成如下功能:

  • drv_keyled_open(): 实现按键和LED的引脚配置与初始化操作。
  • drv_keyled_read(): 实现对按键电平状态的读取。
  • drv_keyled_write(): 实现对LED灯的亮灭控制。 (注:实验只用到了open,read,write三个常用函数,系统其实还提供了很多其他函数,可以见file_operations结构体里的内容)

源码如下:

/************驱动关键函数实现************/
/*
函数名:drv_keyled_open
功能:配置引脚功能
*/
int drv_keyled_open(struct inode *inode, struct file *file)
{
    char key_dat=1;
    printk("drv_keyled_open2\n");

    //GPF4.5.6
    *gpiof_con |= (1<<2*4) | (1<<2*5) | (1<<2*6) ;    //led 输出
    *gpiof_data &= ((~(1<<4)) & (~(1<<5)) & (~(0<<6)));      //初始化2个点亮

    //GPG3,GPP0,GPP2
    *gpiog_con |= (0x3<<2*11) ;    //按键

    return 0;
}


/*
函数名:drv_keyled_write
功能:控制led亮灭,实现反转
*/
static ssize_t drv_keyled_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    unsigned char key_data[2]={1,1};
    Uint32 data=*gpiof_data;

    //测试:读取用户传入的按键值
    copy_from_user(key_data, buf, count);
    printk("kernel:the key is:%d , %d\n",key_data[0] ,key_data[1] ); //检验用户空间传输的按键值

    //实现反转led电平(反转指定位电平同时其他的位不影响)
    *gpiof_data |= (1<<4) | (1<<5) |(1<<6);
    *gpiof_data &= ~(data & ((1<<4) | (1<<5) |(1<<6))); //

    return 0;

}

/*
函数名:drv_keyled_read
功能:读key电平
*/
static ssize_t drv_keyled_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
    unsigned char key_data[2]={1,1};
    unsigned char keybuf=0;

    if(size != sizeof(key_data))//用户空间(应用程序)要读取的字节与内核空间存的字节数一致
    {
        printk("read size has err\n");
        return -EINVAL; //返回错误
    }

    //把读取的按键值发给用户空间(应用端)
    key_data[0] = (*gpiof_data & (1<<0)) ? 1 : 0;
    key_data[1] = (*gpiog_data & (1<<3)) ?  1 : 0;

    if( key_data[0] == 0)
    {
         printk("key1 press \n");
        while(!keybuf)//按下弹出
        {
          keybuf= (*gpiof_data & (1<<0)) ? 1 : 0;
        }

    }
    if( key_data[1] == 0)
    {
        printk("key2 press \n");
        while(!keybuf)//按下弹出
        {
          keybuf= (*gpiog_data & (1<<3)) ? 1 : 0;
        }

    }

    copy_to_user(buf, key_data, sizeof(key_data));//copy_to_user(用户空间,内核空间,字节数)

    return sizeof(key_data);
}

用户空间与内核空间进行消息传递的关键函数:copy_from_user()和copy_to_user()。

  • copy_from_user():通常在xxx_write()函数内调用,实现用户空间的数据到内核空间的传递,传递的数据存在buf的形参中。copy_from_user()可以将buf中的数据读出来。

  • copy_to_user(): 通常在xxx_read()函数内调用,实现用户空间读取内核空间的数据,copy_to_user()是将内核里的数据存于buf中,提供用户层read()的读取。

7.声明驱动(模块的许可证声明),声明后,某块才能被正常安装到系统内核。固定格式如下:

MODULE_LICENSE(“GPL”);

8.编写驱动的makefile文件
同样地,找一写好的驱动makefile模板,修改主机上存储开发板的linux源码目录(第一行),并更改obj-m选项(最后一行),代码如下:

KERN_DIR = /work/mysystem/linux-2.6.22.6

all:
    make -C $(KERN_DIR) M=`pwd` modules 

clean:
    make -C $(KERN_DIR) M=`pwd` modules clean
    rm -rf modules.order

obj-m   += drv_keyled.o

obj-m的主要功能:将驱动代码编译成模块(.ko文件),与obj-m对立的是:obj-y,是将代码直接编译到内核中。


应用测试程序编写

1.新建以应用程序文件,名为:keyled_test.c ,同样找一应用程序模板,把必要头文件包含进来,如下:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

2.编写内容,根据驱动提供的几个关键函数功能,用户层需要调取这些功能函数,完成最终实现。实验目的是按下按键,反转LED灯,因此需要调用read函数,读出内核传递过来的数据(按键值),根据按键值是否按下,调用write函数,实现对led控制。
程序开始需要通过设备文件,打开对应的驱动,然后返回一句柄fd,程序可以通过fd完成设备的读写操作。

形参:int argc, char **argv的作用:

当系统启动应用程序时,可以在启动程序文件的后面加上要传入的参数,这些参数就会传进int argc, char **argv的形参中,其中:
(注:文件名的本身也是个参数,因此参数至少1个,启动程序文件方式见后面操作)

  • argc:显示的是传入参数的个数
  • 指针argv[i]:显示第i个参数的字符串内容

源码如下:

int main(int argc, char **argv)
{
    int fd;
    unsigned char key_data[2]={1,1};

    fd = open("/dev/keyled", O_RDWR);  //应用端通过识别设备文件来识别驱动
    if (fd < 0)
    {
        printf("can't open!\n");
    }


    if(argc == 2)//argc[0]为文件名本身
    {
        printf("argv=%s\n",argv[1]);
    }
    else if(argc == 3)
    {
        printf("argv=%s , %s\n",argv[1],argv[2]);
    }

    printf("hello word fd=%d\n",fd);

    while(1)
    {

        read(fd,key_data,sizeof(key_data));
        if(key_data[0] == 0 || key_data[1] == 0)
        {
            printf("user:k1=%d k2=%d\n",key_data[0],key_data[1]);//检验内核到用户空间的数据传输
            write(fd,key_data,sizeof(key_data));
        }

    }

    return 0;

}

3.测试程序的makefile
采用arm-linux-gcc编译器进行编译,生成可执行程序文件keyled_test ,内容如下:

CROSS.=arm-linux-

keyled_test : keyled_test.c
    $(CROSS)gcc -o  $@  $<
clean:
    rm keyled_test

装载驱动程序与运行测试程序

make编译驱动模块以及测试程序后,分别生成.ko文件和可执行文件,这两个文件需要存放在根文件系统上,分为完成驱动装载和运行测试程序。
开发板挂载根文件系统,通常采用NFS网络方式直接挂载到PC主机上,简单方便,并大大减少程序调试时间,但是笔者由于路由器问题,无法建立主机和开发板的网络连接,只能采用直接下载根文件系统到开发板上运行勒,很麻烦,每一次调试都得重新下一次,简直崩溃o(╥﹏╥)o,下面是驱动装载与测试程序运行具体步骤:

1.在制作好的根文件系统上新建以任意名称的文件夹:/moduel,将.ko文件和应用程序执行文件存在该目录中。
这里写图片描述

2.如果采用NFS挂载方式,可以省略此步。该步主要将文件系统下载在开发板中,因此需要在主机上把制作好的根文件系统转成映像文件(采用mkyaffs2image工具),然后由dnw工具,通过usb方式 下载到开发板中。(篇幅有限,具体过程略述啦^_^,能用NFS最好咯)。

3.启动开发板(uboot,kernel,文件系统都下载并装载好),操作系统启动后,在终端上进行驱动装载(关键命令:insmod),如下:
这里写图片描述

insmod为驱动装载,lsmod为查看装载情况

4.运行测试程序,如下图所示,图中可以观察到如下几个结论:

  • 运行的程序名称后可加传入的参数,如图所示的参数为:“iu”和“tr”。
  • 按下按键时,可以终端打印出相关信息,这些信息都是编写程序时实现的。
  • 图中显示的kernel:…. 和user:…. 分别是内核空间与用户空间之间传递的数据信息,可以看出它们传递的数据是按键值,并且结果正确的。

这里写图片描述

4.开发板测试结果,激动人心的当然是开发板的实际运行效果啦,随着两个按键的任意按下,3个LED等就会翻转,看看效果(^▽^):
这里写图片描述
这里写图片描述


其它工作:驱动卸载与设备、进程查看等

1.查看设备与设备文件。设备显示在/proc/devices虚拟文件系统内,设备文件可以在/dev目录下查看。
可以看出主设备号为252,次设备号为0
这里写图片描述

2.查看当前测试程序的进程和cpu占有率,此时启动的应用程序需要在后台运行(末位加上&字符),才能到前端查看进程和cpu占有率。
可以看出:进程号为791,cpu占有率为98%。

这里写图片描述
1.驱动卸载(从内核中卸载掉该驱动)与关闭进程(退出测试程序运行,通过进程号关闭)。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/ludaoyi88/article/details/80474662