您的位置:首页 > 其它

Driver:模块参数、系统调用、字符设备驱动框架

2017-02-27 22:54 465 查看
1、模块参数
    ./a.out xxx yyy zzz
    int main (int argc, char** argv) {...}
// 用户空间
    insmod xxx.ko mmm nnn
        1)定义全局变量
        2)将该全局变量声明为模块参数
            'module_param (name, type, perm);
           
// 指定模块参数,用于加载模块或模块加载以后传递参数给模块
                @name:变量的名称
                @type:变量的数据类型,支持类型如下:
                      bool
                      int
                      short
                      long
                      charp // char*
                perm:模块参数的访问权限,类似文件权限
                      rwxrwxrwx 0777 user group other
            'module_param_array (name, type, nump, perm);
            // 将一个数组声明为模块参数
                @type:数组成员成名为模块参数
                @nump:数组元素个数指针(有效成员的地址)
/** 代码演示 - moduleparam.c **/
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE ("GPL");

int irq = 0;
char* pstr = "jiangyuan";
int fish[10];
int nr_fish = 10;

module_param (irq, int, 0600);
module_param (pstr, charp, 0);
module_param_array (fish, int, &nr_fish, 0644);

int __init moduleparam_init (void) {
int i = 0;

printk ("irq = %d\n", irq);
printk ("pstr = %s\n", pstr);

for (i = 0; i < nr_fish; i++) {
printk ("fish[%d] = %d\n", i, fish[i]);
}
return 0;
}

void __exit moduleparam_exit (void) {
int i = 0;

printk ("irq = %d\n", irq);
printk ("pstr = %s\n", pstr);

for (i = 0; i < nr_fish; i++) {
printk ("fish[%d] = %d\n", i, fish[i]);
}

}

module_init (moduleparam_init);
module_exit (moduleparam_exit);
/** Makefile **/
obj-m   += moduleparam.o

all:
make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
cp *.ko ../../rootfs -v

clean:
make -C /home/tarena/jy/driver/kernel M=$(PWD) clean

验证:
#:'
insmod moduleparam.ko
    [  305.153000] irq = 0
    [  305.153000] pstr = jiangyuan
    [  305.153000] fish[0] = 0
    [  305.154000] fish[1] = 0
    [  305.155000] fish[2] = 0
    [  305.156000] fish[3] = 0
    [  305.157000] fish[4] = 0
    [  305.158000] fish[5] = 0
    [  305.159000] fish[6] = 0
    [  305.160000] fish[7] = 0
    [  305.161000] fish[8] = 0
    [  305.162000] fish[9] = 0
#:'
rmmod moduleparam
#:'
insmod moduleparam.ko irq=10 pstr="hello,world" fish=1,2,3,4,5
    [  448.455000] irq = 10
    [  448.455000] pstr = hello,world
    [  448.456000] fish[0] = 1
    [  448.457000] fish[1] = 2
    [  448.459000] fish[2] = 3
    [  448.460000] fish[3] = 4
    [  448.461000] fish[4] = 5

#:' ls /sys/module/moduleparam/parameters/ -l
total 0
-rw-r--r--    1 root     0             4096 Jan  1 00:58 fish
-rw-------    1 root     0             4096 Jan  1 00:58 irq
#:'
cat /sys/module/moduleparam/parameters/irq
10
#:'
cat /sys/module/moduleparam/parameters/fish
1,2,3,4,5
#:'
echo 111 >/sys/module/moduleparam/parameters/irq
#:'
echo 222,333,444,555 >/sys/module/moduleparam/parameters/fish
#:'
cat /sys/module/moduleparam/parameters/irq
111
#:'
cat /sys/module/moduleparam/parameters/fish
222,333,444,555
#:'
rmmod moduleparam
    [  531.125000] irq = 111
    [  531.125000] pstr = hello,world
    [  531.125000] fish[0] = 222
    [  531.126000] fish[1] = 333
    [  531.127000] fish[2] = 444
    [  531.128000] fish[3] = 555

【总结】'以上参数用在驱动代码调试阶段。'比如:特殊功能寄存器的赋值...

    /sys/目录类似于/proc目录:基于内存的虚拟文件系统。
        在2.6内核中引入了/sys/目录
        导出驱动内核模型

2、系统调用
    【注意点】谈谈对系统调用的理解?

2.1 系统调用的作用
    是用户态切换到内核态的一种方式。
    unix的C编程程序大多数情况下运行于用户态,当产生系统调用就进入内核态。
2.2 系统调用时如何实现的
    1.进程先将系统调用号填充寄存器;
    2.调用一个特殊的指令swi;
    3.让用户进程跳转到内核事先定义好的一个位置;
    4.内核位置是ENTRY(vector_swi);//entry-common.s
    5.检查系统调用号,这个号告诉内核请求哪种服务;
    6.查看系统调用表(sys_call_table) 找到所调用的内核函数入口地址;
    7.调用该函数,执行,执行完毕返回到用户进程

    eg:
        open ("a.txt", ...)
            适当的值:如果是open,值就是5
                arch/arm/include/asm/unistd.h  // 定义内核源码 - 共365个
            寄存器:对于ARM处理器 r7
            特殊的指令:
                arm ---> swi / svc 
                x86 ---> int
                (调用后产生一个中断异常,硬件自动做4件事...)
            固定的位置:
                entry-common.S (e:\porting\kernel\arch\arm\kernel)
                    ENTRY (vector_swi):
                        sys_call_table[R7]
                    sys_call_table 存在于calls.s
                        sys_open

2.3 添加一个新的系统调用
    1)在内核中增加一个新的函数
        $:'
cd kernel/
        $:'
vi arch/arm/kernel/sys_arm.c
            135 asmlinkage int sys_add (int x, int y) 
            136 {
            137     printk ("enter %s\n", __func__);
            138     return x + y;
            139 }
    2)更新unistd.h
        $:'
vi arch/arm/include/asm/unistd.h
            407 #define __NR_add                    (__NR_SYSCALL_BASE+378)
    3)更新系统调用表 sys_call_table
        $:'
vi arch/arm/kernel/calls.S
            390         CALL(sys_add)  /* 378 */
    4)重新编译内核
        $:'
make uImage
    5)开发板使用新的内核
        $:'
cp arch/arm/boot/uImage /tftpboot/
        #:'
tftp 48000000 uImage
        #:'
bootm 48000000
    6)编写测试程序,调用编号为378的内核函数
        open ("a.txt", O_RDONLY);
        syscall (5, "a.txt", O_RDONLY);
/** 代码演示 - test.c **/
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

int add (int x, int y) {
return syscall (378, x, y);
}

int main (void)
{
int res = 0;
res = add (100, 200);
printf ("res = %d\n", res);
return 0;
}
$:'
arm-cortex_a9-linux-gnueabi-gcc test.c -o test
$:'
cp test ../../rootfs/
    // nfs网络加载的开发板根文件系统在rootfs里面
$:'
cp /opt/arm-<tab>/arm-<tab>/sysroot/lib/libgcc_s.so* ../../rootfs/lib/ -d
#:'
./test
    [   13.627000] enter sys_add
    res = 300

<tips>
$:'
find ./ -name "libgcc*"
按文件名查找,结果给出找到的所有路径

3、字符设备驱动框架
    字符设备:'读写顺序固定,读写过程中不涉及缓存。'如键盘... (较多)
    块设备:读写顺序不固定,读写过程中有缓存。如磁盘/硬盘...(基本已标准化)
    网络设备:读写顺序固定,读写过程中有缓存。如网卡...(可能涉及一点移植)

    linux内核主要是使用C语言实现的,但是其中运用了大量的面向对象的编程思想。
    使用'结构体'来实现面向对象编程。

    实现一个字符设备驱动,实则就是实例化一个 struct cdev:
    1) 定义struct cdev类型变量;
    2) 初始化变量;
    3) 注册变量。
    
struct cdev {
    dev_t dev; // 设备号(3.1)
    const struct file_operations *ops; // 操作函数集合 (3.2)
    ...
};

3.1 【设备号】
    dev_t 
    ← typedef __kernel_dev_t 
    ← typedef __u32 __kernel_dev_t 
    ← #define __u32 unsigned int

    设备号(32bit) = 主设备号(高12bit) + 次设备号(低20bit)

    主设备号:用来区分不同类型的设备。
    次设备号:用来区分同类设备中的不同个体。
    #:'
ls /dev/ttySAC* -l

3.1.1 静态注册设备号
      选出一个内核中未被使用的主设备号,为我所用。
      主设备的取值范围: 0~255
      如何查看内核哪些主设备号没有被占用?
        1) #:' cat /proc/devices
            显示信息为:[主设备号] [使用设备的模块]
            比如 200 没被占用,就可用。
        2) $:' cat Documentation/devices.txt | less

    【静态注册设备号的方法】
    'register_chrdev_region'
    int register_chrdev_region(dev_t from, unsigned count, const char *name)
    功能:注册一个范围的设备号
    参数:
        @from:要注册的连续多个设备号中的第一个,必须是主设备号
                0x12300000
        @count:要注册的设备号个数
               3 要注册的设备号就是 0x12300000
                                   0x12300001
                                   0x12300002
        @name:名称
    返回值: 0 - 成功,负数 - 失败。

    'unregister_chrdev_region'
    void unregister_chrdev_region(dev_t from, unsigned count)
    功能:注销设备号
    参数:同register_chrdev_region
    返回值:无

/** 代码演示 - char_drv.c **/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>     // 正常情况下找到使用函数的地方,头文件信息可全拷

MODULE_LICENSE ("GPL");

dev_t dev;                // 存储设备号
unsigned int major = 200; // 主设备号
unsigned int minor = 0;   // 次设备号

int __init char_drv_init (void)
{
/* 注册设备号 */
// dev = major << 20 | minor; // 设备号
// #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
dev = MKDEV (major, minor);
register_chrdev_region (dev, 1, "jiangyuan-1610");
return 0;
}

void __exit char_drv_exit (void)
{
/* 注销设备号 */
unregister_chrdev_region (dev, 1);
}

module_init (char_drv_init);
module_exit (char_drv_exit);

/** Makefile **/
obj-m	+= char_drv.o

all:
make -C /home/tarena/jy/driver/kernel M=$(PWD) modules
cp *.ko ../../rootfs -v

clean:
make -C /home/tarena/jy/driver/kernel M=$(PWD) clean
验证:
    #:' insmod char_drv.ko
    #:'
cat /proc/devices
        200 jiangyuan-1610

3.1.2 动态注册设备号
      由内核帮我们挑一个未被使用的设备号,和我们自己选中的次设备号拼成设备号。

    【动态注册设备号的方法】
    'alloc_chrdev_region'
    int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const
char *name)
    功能:动态分配一个设备号范围
    参数:
        @dev:传出参数 → 用于返回注册的多个设备号中的第一个。
        @baseminor:自己规定的起始次设备号
        @count:连续注册的设备号个数
        @name:名称
    返回值: 0 - 成功,负数 - 失败。
    
    假如:
    baseminor = 100
    内核给分配的主设备号假如是 200
    count = 3
    相当于向内核注册以下设备号:
        200 << 20 | 100
        200 << 20 | 100 + 1
        200 << 20 | 100 + 2

    'unregister_chrdev_region'
    void unregister_chrdev_region(dev_t from, unsigned count)
    功能:注销设备号 ('静态和动态均是其注销')
    参数:同register_chrdev_region
    返回值:无

/** 代码演示 - char_drv.c **/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>

MODULE_LICENSE ("GPL");

dev_t dev;                // 存储设备号
unsigned int major = 0;   // 主设备号
unsigned int minor = 100;   // 次设备号

int __init char_drv_init (void)
{
if (major) {
/* 静态注册 */
dev = MKDEV (major, minor);
register_chrdev_region (dev, 1, "jiangyuan-1610");
}
else {
/* 动态注册 */
alloc_chrdev_region (&dev, minor, 1, "jiangyuan-alloc-1610");
}

return 0;
}

void __exit char_drv_exit (void)
{
/* 注销设备号 */
unregister_chrdev_region (dev, 1);
}

module_init (char_drv_init);
module_exit (char_drv_exit);
验证:
    #:' insmod char_drv.ko
    #:'
cat /proc/devices
    #:'
rmmod char_drv.ko
    #:' cat /proc/devices

3.2 【操作函数集合】
    完成一个字符设备驱动就是实例化一个 cdev 结构体。
   
实例化一个cdev过程,主要工作就是实现其对应的操作函数的集合。

    struct file_operations {
        read ()
        write ()
        open ()
        ...
    };
    
    以上数据结构是所有字符设备驱动要实现的函数的合集,再去完成某个具体硬件字符设备驱动时,只需要实现其中的一部分函数就可以了。(根据具体的情况)

3.3 内核中提供操作cdev的API
    struct cdev{
        dev
        file_operation
        ...
    };

    'cdev_init'
// cdev的初始化函数
    void cdev_init(struct cdev *cdev, const struct file_operations *fops)
    功能:初始化一个设备结构体
    参数:
        @cdev:要初始化的设备结构体变量地址
        @fops:要操作的设备的结构体变量地址
    返回值:无

    'cdev_add'  // 注册cdev
    int cdev_add(struct cdev *p, dev_t dev, unsigned count)
    功能:添加一个字符设备到系统
    参数:
        @p:要注册到系统中去的cdev地址
        @dev:对应的设备号
        @count:连续注册的个数,一个就取1
    返回值: 0 - 成功,负数 - 失败。

    'cdev_del'  // 注销cdev
    void cdev_del(struct cdev *p)
    功能:删除一个cdev字符设备
    参数:
        @p:要从系统注销的cdev地址
    返回值:无

/** 代码演示 -  **/

(暂略)

实验步骤:
    1) 安装查看模块
       #:' insmod char_drv.ko
       #:'
cat /proc/devices
       244 ...
    2) 创建设备文件
        #:'
mknod /dev/leds c 244 100
    3) 写一个test.c
        $:' vi test.c
        $:' arm-cortex_a9-linux-gnueabi-gcc test.c -o test
        $:' cp test ../../rootfs/
        $:'
./test
            enter led_open success...
            open /dev/leds successed...
            now is closing device!enter led_release success...
实验总结:
没有增加新的系统调用,但也调用到了open和release函数...

设备文件时用户态程序调用硬件驱动的媒介(c major minor)。

<tips>
linux内核uImage:
'make menuconfig
---> Kernel hacking ---> show timing information on printks
当选中这个选项后,启动内核,会在日志信息前面加上时间戳。取消选中,即可取消时间戳。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  arm 驱动
相关文章推荐