510 likes | 625 Views
第 8 章 嵌入式 Linux 驱动程序设计. 本章目标. 了解 Linux 设备驱动程序的基础知识; 掌握 Linux 驱动模块的构造和装载方法; 掌握字符设备驱动程序的基本结构和开发方法; 掌握用户空间调用设备驱动程序的方法. 本章结构. 设备驱动程序简介. 字符设备. 设备的分类和特点. 块设备. Linux 驱动程序概述. 网络设备. 设备驱动的 Hello World 模块. 构造和运行模块. 内核驱动模块和应用程序对比. 编译和装载驱动模块. 字符设备驱动. 8.1 设备驱动程序简介. 驱动程序的特点
E N D
本章目标 • 了解Linux设备驱动程序的基础知识; • 掌握Linux驱动模块的构造和装载方法; • 掌握字符设备驱动程序的基本结构和开发方法; • 掌握用户空间调用设备驱动程序的方法.
本章结构 设备驱动程序简介 字符设备 设备的分类和特点 块设备 Linux驱动程序概述 网络设备 设备驱动的Hello World模块 构造和运行模块 内核驱动模块和应用程序对比 编译和装载驱动模块 字符设备驱动
8.1 设备驱动程序简介 • 驱动程序的特点 • 驱动程序直接操控硬件,是应用和硬件设备之间的一个软件层 • 这个软件层一般在内核中实现,但也可以在用户空间实现,也就是所说的内核态驱动和用户态驱动。我们主要讨论内核态驱动。 • 设备驱动程序的作用在于提供机制,而不是提供策略,编写驱动程序时不要给用户强加任何策略 • 机制:驱动程序能实现什么功能 • 策略:用户如何使用这些功能 • 通过驱动程序,应用软件可以安全高效的访问硬件 • 驱动程序作为一个隔离的中间层软件,将底层细节隐藏起来,提高了软件的可移植性
8.2 设备的分类和特点(1) • 设备分类 • 字符设备(char device) • 块设备(block device) • 网络设备(network device)
8.2 设备的分类和特点(2) • 各类设备的特点 • 字符设备特点 • 字符设备是个能够像字节流一样来存取的设备( 如同一个文件 ) • 字符设备通过/dev下的文件系统结点来访问。字符设备驱动通常至少需要实现 open, close, read, 和 write 等系统调用 • 字符设备和普通文件的不同之处在于,大部分字符设备仅仅是只能顺序访问的数据通道,不能前后移动访问指针。然而,也存在和数据区或者文件特性类似的字符设备,访问它们时也可前后移动访问指针。比如framebuffer设备就是这样的设备,应用程序可以使用mmap或lseek访问图像的各个区域
8.2 设备的分类和特点(3) • 块设备特点 • 块设备通过位于 /dev 目录的文件系统结点来存取 • 字符设备通过文件系统结点来访问。块设备和字符设备的区别仅仅在于内核内部管理数据的方式,也就是内核和驱动程序的接口不同 • 块设备有专门的接口,块设备的接口必须支持挂装(mount)文件系统。应用程序一般通过文件系统来访问块设备上的内容
8.2 设备的分类和特点(4) • 网络设备特点 • 网络设备驱动不同于字符设备和块设备驱动,不是通过/dev下的文件节点来访问,而是通过单独的网络接口来访问 • 任何一个网络事务都通过一个网络接口,即一个能够和其他主机交换数据的设备。通常该接口代表一个硬件设备(如网卡),但也可能是一个纯粹的软件设备,比如环回接口(loopback) • 内核与网络驱动程序间的通讯完全不同于内核和字符设备以及块设备驱动程序之间的通信,内核调用一套和数据包传输相关的函数
8.3 构造和运行模块 8.3.1、设备驱动的Hello World模块 8.3.2、内核驱动模块与应用程序对比 8.3.3、编译和装载驱动模块
驱动程序加入内核的方法 • 把所有需要的功能都编译到内核中 导致的问题: 1)生成的内核镜像(Image)文件会很大 2)如果我们要在现有的内核中新增或删除功能,将不得不重新编译和装载内核。 有没有一种机制使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码可被动态地加载到内核中呢 • Linux提供了这样的一种机制可以实现以上效果,这种机制被称为: 模块(Module) 1) Linux 内核提供了对许多模块类型的支持, 包括但不限于, 设备驱动 2)每个模块由目标代码组成( 没有连接成一个完整可执行程序 ), 我们可以使用insmod 工具将模块动态加载到正在运行内核中,也可以及通过 rmmod 程序移除模块
8.3.1 设备驱动的Hello World模块(1) 自由许可证 #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static int __init hello_init(void) { printk(KERN_ALERT "Hello world\n"); return 0; } static void __exit hello_exit(void) { printk(KERN_ALERT " Hello world exit\n"); } module_init(hello_init); module_exit(hello_exit); 用法类似于printf,但它有优先级(比如KERN_ALERT) 宏,告诉内核这两个函数只会在加载和卸载模块时使用 模块初始化宏 模块卸载宏
8.3.1 设备驱动的Hello World模块(2) • 编译内核模块 #gcc –DMODULE –D__KERNEL__ -c hello.c –I/your_kernel_path/linux/include 会生成hello.ko • 加载内核模块:insmod <模块名称.ko> #insmod ./hello.ko Hello world • 查看内核中已装载的模块 #lsmod | grep hello • 卸载内核模块:rmmod <模块名称> #rmmod hello Hello world exit
8.3.1 设备驱动的Hello World模块(3) • Linux内核模块的程序结构 • module_init()---模块加载函数(必须) 当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核 执行,完成模块的相关初始化工作 • module_exit()---模块卸载函数(必须) 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块装载函数相反的功能 • MODULE_LICENSE()---模块许可证声明(必须) 模块许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(kernel tainted)的警告 • module_param()---模块参数(可选) 模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。 • EXPORT_SYMBOL()---模块导出符号(可选) 内核模块可以导出符号(symbol,对应于函数或变量),这样其他模块可以使用本模块中的变量或函数 • 其他一些声明MODULE_XXXXX()---模块声明(可选)
8.3.1 设备驱动的Hello World模块(4) • 模块加载函数 static int __init initialization_function(void) { /* 初始化代码 */ } module_init(initialization_function); • 初始化函数应当声明成静态的(static), 因为它们不会在特定文件之外可见 • __init 标志表明该函数只是在初始化时使用。模块加载器在模块加载后会丢掉这个初始化函数, 这样可将该函数占用的内存释放出来,以作他用。 原型:#define __init __attribute__ ((__section__(“.init.text”))) • moudle_init的使用是强制性的, 这个宏定义会在模块目标代码中增加一个特殊的段, 用于说明内核模块初始化函数所在的位置。没有这个定义, 初始化函数不会被调用。
8.3.1 设备驱动的Hello World模块(5) • 模块卸载函数 static void __exit cleanup_function(void) { /* 释放资源 */ } module_exit(cleanup_function); • 该函数在模块被移除前注销接口并释放所有所占用的系统资源 • 清理函数没有返回值, 因此它被声明为void • __exit 修饰符标识这个代码是只用于模块卸载( 通过使编译器把它放在特殊的 ELF 段) 原型:#define __exit __attribute__ ((__section__(“.exit.text”))) • moudle_exit 声明对于使得内核能够找到模块的清理函数是必要的
8.3.1 设备驱动的Hello World模块 • 模块参数 为了增加驱动程序的灵活性,内核允许对驱动程序指定参数,而这些参数可在加载驱 动程序模块时改变。如下面实例: #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); static char *whom = "world"; static int howmany = 1; static int hello_init(void) { int i; for(i=0;i<howmany;i++) { printk(KERN_ALERT "Hello %s\n",whom); } return 0; } static void hello_exit(void) { printk(KERN_ALERT " Hello world exit\n"); } module_init(hello_init); module_exit(hello_exit); module_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO); module_param(参数名, 参数类型,参数读/写权限)
8.3.1 设备驱动的Hello World模块 module_param(参数名,参数类型,参数读/写权限) static char *whom = "world"; static int howmany = 1; module_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO); 内核支持的模块参数类型包括: byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),以‘u’开头的为无符号值。 模块也可以拥有参数数组 形式为“module_param_array(数组名,数组类型,数组长,参数读/写权限)”。运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。 装载模块时改变参数:可通过insmod或modprobe insmod hello_ext.ko howmany=5 whom="Students" modprobe也可以从它的配置文件(/etc/modprobe.conf)读取参数的值
8.3.1 设备驱动的Hello World模块 • 模块导出符号 Linux 内核头文件提供了一个方便的方法用来管理符号的对模块外部的可见性, 因此减少了命名空间的污染(命名空间的名称可能会与内核其他地方定义的名称冲突), 并且适当信息隐藏。如果模块需要输出符号给其他模块使用, 应当使用下面的宏定义: EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name); 注: 1)_GPL 版本的宏定义的导出符号只能对 GPL 许可的模块可用 2)符号必须在模块文件的全局部分导出, 不能在函数中导出
8.3.1 设备驱动的Hello World模块(6) • 模块声明与描述 MODULE_AUTHOR(author); ---声明模块的作者 MODULE_DESCRIPTION(description); ---声明模块的描述 MODULE_VERSION(version_string); ---声明模块的版本 MODULE_DEVICE_TABLE(table_info); ---声明模块的设备表 MODULE_ALIAS(alternate_name); ---声明模块的别名
8.3.1 设备驱动的Hello World模块(7) • 模块的使用计数 Linux2.4内核中 模块自身通过 MOD_INC_USE_COUNT(加一计数) 和 MOD_DEC_USE_COUNT(减一计数)宏来管理自己被使用的计数 Linux2.6内核中 提供了模块计数管理接口 int try_module_get(struct module *module); 和 void module_put(struct module *module); 取代Linux2.4内核中的模块使用计数管理宏 注: 在Linux2.6内核下,对于设备驱动工程师而言,很少需要亲自调用try_module_get()和 module_put(),因为模块的计数管理由内核里更底层的代码(如总线驱动或是此类设 备共用的核心模块)来实现,从而简化了设备驱动的开发
8.3.2 内核驱动模块与应用程序对比 • 应用程序是一个进程 • 编程从主函数main()开始 • 主函数main返回即是进程结束 • 驱动程序是一系列内核函数 • 驱动程序向内核添加了一些函数是内核的一部分 • Open() • Release() • Read() • Write() • 这些函数由内核在适当的时候来调用 • 这些函数可以用来完成硬件访问等操作
8.3.3 编译和装载驱动模块(1) • 编译模块 • 编译模块的makefile样例: ifneq ($(KERNELRELEASE),) obj-m := hello.o else KERNELDIR ?= /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KERNELDIR) M=$(PWD) modules endif 对于本章所将的“hello world”程序的makefile,下面一行就够了: obj-m := hello.o 如果我们想由两个源文件(比如file1.c和file2.c )构造出一个名称为module.ko的模块, 则正确的makefile可如下编写: obj-m := module.o module-objs := file1.o file2.o
8.3.3 编译和装载驱动模块(2) • 装载和卸载模块 • 装载模块 Insmod和modprobe可以用来装载模块,其中Insmod和modprobe主要区别: modprobe会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有这类 引用,modprobe会在当前模块路径中搜索定义了这些符号的其他模块,并同时将这些模 块也装载到内核。如果在这种情况下使用insmod,该命令则会失败,并在系统日志文件 中记录“unresolved symbols(未解析的符号)”消息。 • 卸载模块 从内核中卸载模块可以用rmmod工具。 注意,如果内核认为该模块任然在使用状态,或者内核被禁止移除该模块,则无法 移除该模块。
8.4 字符设备驱动程序基本结构 8.4.1 主要的概念和结构体 8.4.2 Linux字符设备驱动程序的组成 8.4.3 添加驱动程序到内核中
8.4.1 主要的概念和结构体(1) • 什么是主设备号/次设备号 • 主设备号是内核识别一个设备的标识。它是一个整数,范围从0到4095,通常使用1到255 • 次设备号由内核使用,用于正确确定设备文件所指的设备。它是一个整数,范围从0到1048575,但一般使用0到255 • 设备编号的内部表达 • dev_t类型(32位):用来保存设备编号(包括主设备号(12位)和次设备号(20位)) • 从dev_t获得主设备号和次设备号: MAJOR(dev_t); MINOR(dev_t); • 将主设备号和次设备号转换成dev_t类型: MKDEV(int major,int minor);
8.4.1 主要的概念和结构体(2) 分配主设备号 • 手工分配主设备号:找一个内核没有使用的主设备号来使用。 • 动态分配主设备号: int alloc_chrdev_resion(dev_t *dev,unsigned int firstminor, unsigned int count,char *name); 参数: dev: 输出的设备号 firstminor:应该是要使用的被请求的第一个次设备号 count:所请求的连续设备编号的个数 name:和该编号范围关联的设备名称
8.4.1 主要的概念和结构体(3) • cdev 结构体 struct cdev { struct kobject kobj; /* 内嵌的kobject 对象 */ struct module *owner; /*所属模块*/ struct file_operations *ops; /*文件操作结构体*/ struct list_head list; dev_t dev; /*设备号*/ unsigned int count; }; • 操作cdev的函数 void cdev_init( struct cdev *, struc t file_operations *); struct cdev *cdev_alloc(void) ; int cdev_add(st ruct cdev *, dev_t, unsigned) ; void cdev_del(struct cdev *);
8.4.1 主要的概念和结构体(4) • file_operations 结构体 • 字符驱动和内核的接口:file_operations结构体(在include/linux/fs.h定义)。 • 字符驱动只要实现一个file_operations结构体,并注册到内核中,内核就有了操作此设备的能力。 file_operations的主要成员: struct module *owner:指向模块自身 open:打开设备 release:关闭设备 read:从设备上读数据 write:向设备上写数据 ioctl:I/O控制函数 llseek:定位读写指针 mmap:映射设备空间到进程的地址空间
8.4.1 主要的概念和结构体(5) • file 结构体 • file结构:和file_operations结构相关的一个结构体。描述 一个正在打开的设备文件。 • loff_t f_pos: 当前读/写位置 • unsigned int f_flags:标识文件打开时,是否可读或可写,如0_RDONLY、O_NONBLOCK和O_SYNC • struct file_operations *f_op: 与文件相关的操作,指向所实现的struct file_operations • void *private_data: 私有数据指针。驱动程序可以将这个字段用于任何目的或者忽略这个字段。
8.4.1 主要的概念和结构体(6) • inode 结构体 • 内核用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。 Inode结构中的两个主要字段: dev_t i_rdev; 对表示设备文件的inode结构,该字段包含了真正的设备编号。 struct cdev *i_cdev; struct cdev是表示字符设备的内核的内部结构。当inode指向一个字符设备文 件时,该字段包含了指向struct cdev结构的指针。 从一个inode中获得主设备号和次设备号: unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);
8.4.2 Linux字符设备驱动程序的组成(1) • 注册和注销设备 • 向内核注册字符设备,在模块或者驱动初始化的时候调用。 int register_chrdev(unsigned int major, const char *name, struct file_operations *fops); (Linux2.4或以前版本) int register_chrdev_region(dev_t first,unsigned int count, char *name); int alloc_chrdev_resion(dev_t *dev,unsigned int firstminor, unsigned int count,char *name); • 注销设备驱动程序,在模块或者驱动销毁的时候调用 int unregister_chrdev(unsigned int major, const char *name); (Linux2.4或以前版本) void unregister_chrdev_region(dev_t first, unsigned int count);
8.4.2 Linux字符设备驱动程序的组成(2) • 字符设备驱动模块加载模板 / /设备驱动模块加载函数 static int __init xxx_init(void) { ... cdev_init(&xxx_dev.cdev, &xxx_fops); / /初始化cdev xxx_dev.cdev.owner = THIS_MODULE; / /获取字符设备号 if (xxx_major) { register_chrdev_region(xxx_dev_no, 1, DEV_NAME); } else { alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME); } ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1 ) ; / /注册设备 ...}
8.4.2 Linux字符设备驱动程序的组成(3) • 字符设备驱动模块卸载模板 /*设备驱动模块卸载函数*/ static void __exit xxx_exit(void) { unregister_chrdev_region(xxx_dev_no, 1); / /释放占用的设备号 cdev_del(&xxx_dev.cdev); / /注销设备 ... }
8.4.2 Linux字符设备驱动程序的组成(4) • 打开和关闭(1) • open int open(struct inode *inode, struct file *filp) ; • 模块使用计数加1 • 识别次设备号,如有必要更新f_op指针。分配并填写置于filp>private_data里的数据结构。 • 硬件操作: 检查设备相关错误(诸如设备未就绪或类似的硬件问题); 如果设备是首次打开,则对其初始化; 如果有中断操作,申请中断处理程序;
8.4.2 Linux字符设备驱动程序的组成(5) • 打开和关闭(2) • release int release(struct inode *inode, struct file *filp) ; • 模块使用计数减1 • 释放由open分配的,保存在filp>private_data里的所有内容。 • 硬件操作: 如果申请了中断,则释放中断处理程序。 在最后一次关闭操作时关闭设备。
8.4.2 Linux字符设备驱动程序的组成(6) • read/write(1) ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp); ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp); • 参数说明 参数filp是文件指针 参数buff是指向用户空间的缓冲区,这个缓冲区或者保存将写入的数据,或者是一个存放新读入数据的空缓冲区。 参数count是请求传输的数据长度。 最后的offp是一个指向“long offset type(长偏移量类型)”对象的指针,这个对象指明用户在文件中存取操作的位置。
8.4.2 Linux字符设备驱动程序的组成(7) • read/write(2) • 用户空间和内核空间之间的数据拷贝过程,不能简单的用指针操作或者memcpy来进行数据拷贝。原因: • 用户空间的数据是可以被换出的,会产生一个页面失效异常。 • 用户空间的地址无法在内核空间中使用。 • 用户空间和内核空间之间进行数据拷贝的函数: unsigned long copy_from_user(void *to, const void __user *from, unsigned long count) ; unsigned long copy_to_user(void __user *to, const void *from, unsigned long count ); • 如果要复制的内存是简单类型,如char、int、long 等,则可以使用简单的put_user()和get_user()
8.4.2 Linux字符设备驱动程序的组成(8) • read/write(3) • 读设备模板 ssize_t xxx_read(struct file *filp, char __user *buf, size_t count , loff_t*f_pos) { ... copy_to_user(buf, ..., ... ); ... } • 写设备模板 ssize_t xxx_write(struct file *fil p, const char __user *buf , size_t count , loff_t *f_pos) { ... copy_from_user(..., buf, ... ); ... }
8.4.2 Linux字符设备驱动程序的组成(9) • ioctl函数(1) 为设备驱动程序执行“命令”提供了一个特有的入口点,用来设置或者读取设备的属性信息。 int ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg); cmd:事先定义的IO控制命令 arg:arg为对应于cmd命令的参数
8.4.2 Linux字符设备驱动程序的组成(10) • ioctl函数(2) • cmd参数 不推荐用0x1,0x2,0x3之类的值,Linux对ioctl()的cmd参数有特殊的定义如下: 构造命令编号的宏: _IO(type,nr)用于构造无参数的命令编号; _IOR(type,nr,datatype)用于构造从驱动程序中读取数据的命令编号; _IOW(type,nr,datatype)用于写入数据的命令; _IOWR(type,nr,datatype)用于双向传输。type和number位字段通过参数传入,而 size位字段通过对datatype参数取sizeof获得。 样例: #define XXX_IOC_MAGIC ‘k’ /*使用“k”作为幻数*/ /* XXX_IOCXXXx是定义的对应的ioctl 命令*/ #define XXX_IOCXXX1 _IO(XXX_IOC_MAGIC,0) #define XXX_IOCXXX2 _IOW(XXX_IOC_MAGIC,1,int) #define XXX_IOCXXX3 _IOR(XXX_IOC_MAGIC,2,int)
8.4.2 Linux字符设备驱动程序的组成(11) • ioctl函数(3) • Ioctl函数模板 int xxx_ioctl( struct inode *inode, struct f ile *filp, unsigned int cmd, unsigned long arg) { ... switch (cmd) { case XXX_CMD1: ... break; case XXX_CMD2: ... break; default: ///*不能支持的命令 */ return - ENOTTY; } return 0; }
阶段总结 • 本节介绍了字符设备驱动结构
8.4.3 添加驱动程序到内核中(1) • 配置内核 • 编译内核 • 添加驱动程序到内核中
8.4.3 添加驱动程序到内核中(2) • 配置内核 • 配置命令包括: make config make menuconfig make xconfig make gconfig 可通过“上”、“下”、“左”、“右”键移动菜单,选择某项按“Y”,取消 选择按“N”,如果选择某项编译为模块按“M”,进入子菜单按“Enter”,返回 上一级菜单按 “Esc” 使用make config、make menuconfig等命令后,会生成一个.config配置文件(是 隐身文件,通过ls –a才能看到)
8.4.3 添加驱动程序到内核中(3) • 编译内核 • 可用如下命令编译内核: make ARC=arm CROSS_COMPILE=arm-linux- zImage • 也可在源代码根目录的Makefile中将ARCH和CROSS_COMPILE直接指定为arm和arm-linux-,如: ARCH ?= arm CROSS_COMPILE ?= arm-linux- 这样就没有必要每次编译的时候都指定体系结构和交叉编译器了,只须使用下面命令就可以了: make zImage
8.4.3 添加驱动程序到内核中(4) • 添加驱动程序到内核(1) • Linux 2.6内核的配置系统由以下3个部分组成。 • Makefile:分布在Linux内核源代码中的Makefite,定义Linux内核的编译规则 • 配置文件(Kconfig):给用户提供配置选择的功能。 • 配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供字符界面和图形界面)。这些配置工具都是使用脚本语言编写的,如Tcl/TK、Perl等。 • 在Linux内核中增加程序需要完成以下3项工作。 • 将编写的源代码复制到Linux内核源代码的相应目录。 • 在目录的Keonfig文件中增加新源代码对应项目的编译配置选项。 • 在目录的Makefile文件中增加对新源代码的编译条目。
8.4.3 添加驱动程序到内核中(5) • 添加驱动程序到内核(2) • 实例:在内核源代码drivers目录下为ARM体系结构新增test driver test driver的树形目录: 步骤: 1、拷贝test到drivers路径下 2、为新增目录创建Kconfig和Makefile 3、修改新增目录父目录的Kconfig和 Makefile,以便新增的Kconfig和 Makefile能够被引用 4、在arch/arm/Kconfig里增加 source “drivers/test/Kconfig”
8.4.3 添加驱动程序到内核中(6) • 添加驱动程序到内核(3) • 步骤: 1、拷贝test到drivers路径下 cp –fr test linux_kernel_path/drivers 2、为新增目录创建Kconfig和Makefile
8.4.3 添加驱动程序到内核中(7) • 添加驱动程序到内核(4) • 步骤: 3、修改新增目录的父目录的Kconfig和Makefile 在drivers/Kconig中加入:source "drivers/test/Kconfig“ 在drivers/Makefile中加入:obj-$(CONFIG_TEST) += test/ 4、在arch/arm/Kconfig里加入:source “drivers/test/Kconfig” 增加了Kconfig和Makefile文件之后的新的test树型目录如下所示:
本章总结(1) 字符设备驱动主要结构和开发方法 主要概念和结构体 字符驱动的主要组成 字符设备驱动基本结构 实例 如何配置、编译内核,以及如何把驱动程序添加到内核 内核配置和编译方法 字符设备驱动程序 添加驱动程序到内核 添加驱动程序到内核中 如何在用户空间调用驱动 用户空间调用设备驱动程序