1、驱动开发环境
完成系统移植的三步:u-boot启动引导程序、内核镜像、文件系统,u-boot启动引导程序最好固化到开发板上,内核镜像通过tftp服务从ubuntu下载,文件系统通过nfs服务从ubuntu共享到开发板,开发板启动计数时按任意键进入u-boot命令模式设置bootcmd和bootargs
# setenv serverip 192.168.3.120 # setenv ipaddr 192.168.3.233 # setenv bootcmd tftp 41000000 uImage\;tftp 42000000 exynos4412-fs4412.dtb\;bootm 41000000 – 42000000\; #setenv bootargs root=/dev/nfs\; nfsroot=192.168.3.120:/source/rootfs rw console=ttySAC2,115200 init=/linuxrc ip=192.168.3.233 # saveenv
注意:192.168.3.120 对应Ubuntu的ip
192.168.3.233 对应板子的ip
这两个ip应该根据自己的实际情况适当修改
重启开发板进入自启动模式自动加载内核和文件系统
2、驱动开发工具使用source insight软件和vim工具,当然如果不想频繁地从Windows复制到ubuntu可以使用共享文档,这样就可以在Windows上写好代码在ubuntu中直接编译。
设置共享文档步骤:
打开VMware,选择虚拟机设置选择选项选择共享文件夹,点击总是启用,点击添加就可以设置共享文件夹了
设置完成后可以在ls /mnt/hgfs查看共享目录是否挂载
如果没有挂载可以通过vmware-hgfsclient查看共享文件夹
挂载共享文件夹命令vmhgfs-fuse .host:/my /mnt/hgfs其中my是查看共享文件夹时显示的名字,/mnt/hgfs是挂载路径,挂载路径必须是空的文件夹否则可能失败
source insight---查看和编写代码工具
将ubuntu中的linux内核代码复制到Windows中
在source insight中新建项目
在第一个对话框中,第一个文本框(行编辑器),输入工程的名字
在第二个对话框中,第一个本文框中选择刚解压的Linux内核源码目录(顶层linux-3.14),点击ok
在第三个对话框中,在对话框中选择要查看的目录/文件 需要选择的目录文件: include init kernel arch/arm/kernel arch/arm/include/asm driver/base driver/char driver/i2c driver/spi fs/char_dev.c 点击close关闭
重新选择project---->open project 选择刚才创建的工程名 ok 如果提示同步,则选择确认进行同步
开始编写驱动代码
驱动代码必须包含四部分:
a.头文件
#include#include
b.加载和卸载时的函数定义
static int __init hello_init(void) { return 0; } static void __exit hello_exit(void) { }
c.加载和卸载的入口声明
module_init(hello_init);//当使用insmod 驱动名称.ko 加载驱动时执行函数hello_init() module_exit(hello_exit);//当使用remod 驱动名称 卸载驱动时执行函数hello_exit()
d.协议选择GPL
MODULE_LICENSE("GPL");
3、驱动操作
a.加载驱动使用命令
insmod 驱动程序路径.ko
b.加载好驱动后可以通过命令查看驱动
lsmod
c.卸载驱动命令(不用加.ko)
rmmod 驱动程序名
1、申请设备号
int register_chrdev(unsigned int major,const char *name,const struct file_operations *fops) 参数1:unsigned int major------大于0则是申请对应的主设备号;等于0则是由内核分配主设备号 参数2:const char *name------是注册时的名字 参数3:const struct file_operations *fops------是用来关联文件IO接口的结构体 返回值:当参数1大于0时,正确返回0,失败返回负数 当参数1等于0时,正确返回主设备号,失败返回负数
2、创建设备节点(生成对应的驱动文件)
创建文件信息结构体
struct class * class_create(owner,name); 参数1:owner-----拥有者,一般THIS_MODULE 参数2:name-----字符串,描述信息 返回值:struct class *------信息结构体
创建字符驱动设备文件(节点),一般默认建在/dev下,但是可以在任意目录下创建
struct device *device_create( struct class *class,struct device *parent,dev_t devt,void *drvdata,const char *fmt, ...) 参数1:struct class *class--------class结构体,创建的设备文件的信息内容,通过 class_create()函数创建 参数2:struct device *parent--------表示父类对象,一般直接写NULL,结构体地址 参数3:dev_t devt--------设备号可以有函数MKDEV(ma, mi)获得,ma------主设备号, mi------次设备号,也可以由主设备号左移20位或上一个数字得到这个数字不能大于2^20,例:major<<20|0 参数4:void *drvdata-------私有数据,一般填NULL 参数5:const char *fmt, ...--------设备文件名字符串首地址 返回值:struct device *---------设备节点对象(设备文件描述),成功返回地址,失败返回NULL
文件IO接口层实现,应用程序调用文件io时,驱动程序也调用对应的文件io接口函数 在结构体 struct file_operations 每一个成员变量都代表绑定一个系统调用(文件io)函数,只要对结构体中的成员赋值,就代表值绑定上一个文件io函数
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 (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); 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 *, loff_t, loff_t, 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 **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); int (*show_fdinfo)(struct seq_file *m, struct file *f); };//函数指针的集合,
驱动控制硬件,控制外设,其实就是控制地址,通过地址往寄存器写入、读出控制 内核驱动是通过虚拟地址操作 初始化硬件
地址映射: void * ioremap(cookie,size); 参数1:cookie-----物理地址 参数2:size-----映射内容大小,字节 返回值:返回映射成功后的虚拟内存地址 操作虚拟内存地址中的内容就是操作对应的物理地址空间内容
字符设备驱动卸载时需要的函数
在卸载入口中实现,清除 与初始化逆序过程进行卸载
//1、映射释放(中断释放) iounmap(映射的虚拟内存地址);----释放映射地址 //2、释放设备文件 void device_destroy(struct class * class,dev_t devt); //3、释放设备文件结构体 void class_destroy(struct class * cls) //4、释放设备号 void unregister_chrdev(unsigned int major,const char * name)
字符驱动模型举例:
通过字符设备驱动控制一颗LED灯
//头文件包含 #include#include #include #include #include #include //使用结构体表示一个驱动对象(面向对象的编程思想) struct Led_Dev { unsigned int *gpx1con; unsigned int *gpx1dat; struct class * class; struct device * dev; unsigned int major; unsigned int dev_no; }; struct Led_Dev led; //4、文件IO功能与设备功能实现绑定 ssize_t led_read (struct file * flie, char __user * data, size_t size, loff_t * ops) { return 0; } ssize_t led_write (struct file * file, const char __user * data, size_t size, loff_t * ops) { //data是应用程序传递的数据的地址,size 传递的大小 int num; copy_from_user(&num,data,size);//从应用程序获取数据存到num中 if(num==1) { *(led.gpx1dat) |= 1; } else { *(led.gpx1dat) &= ~1; } return 0; } int led_open (struct inode * inode, struct file * file) { printk("open ok\n"); return 0; } int led_close (struct inode * inode, struct file * file) { printk("close ok\n"); return 0; } const struct file_operations fops= { .open = led_open, .release = led_close, .write = led_write }; //驱动加载与卸载函数实现 static int __init led_init(void) { led.major = 250; led.dev_no = led.major<<20|0; //1、申请设备号 int res = register_chrdev(led.major,"led_dev",&fops); if(res !=0 ) { printk("register dev error\n"); goto err_1; //return -1; } //2、创建设备文件 led.class = class_create(THIS_MODULE,"led_cls"); if (IS_ERR(led.class)) { printk("class create error\n"); goto err_2; //unregister_chrdev(major,name); //return -1; } led.dev = device_create(led.class, NULL, led.dev_no,NULL,"led"); if(IS_ERR(led.dev)) { printk("device create error\n"); goto err_3; //class_destroy(class); //unregister_chrdev(major,name); //return -1; } //3、驱动设备控制硬件 //硬件寄存器地址映射 led.gpx1con = ioremap(0x11000c20, 4); if(led.gpx1con==NULL) { printk("gpx1con ioremap error\n"); goto err_4; //device_destroy(class, dev_no); //class_destroy(class); //unregister_chrdev(major,name); //return -1; } led.gpx1dat = ioremap(0x11000c24, 4); if(led.gpx1dat==NULL) { printk("gpx1dat ioremap error\n"); goto err_5; //iounmap(gpx1con); //device_destroy(class, dev_no); //class_destroy(class); //unregister_chrdev(major,name); //return -1; } //硬件初始化 *(led.gpx1con) = *(led.gpx1con) & ~0xf | 1; *(led.gpx1dat) |= 1; return 0; //出错处理的代码 err_5: //1、映射地址释放 iounmap(led.gpx1con); err_4: //2、设备文件释放 device_destroy(led.class,led.dev_no);//释放设备文件 err_3: class_destroy(led.class);//释放文件信息结构体 err_2: //3、驱动设备号注销 unregister_chrdev(led.major,"led_dev"); err_1: return -1; } static void __exit led_exit(void) { //卸载驱动,与初始化逆序 //1、映射地址释放 iounmap(led.gpx1con); iounmap(led.gpx1dat); //2、设备文件释放 device_destroy(led.class, led.dev_no);//释放设备文件 class_destroy(led.class);//释放文件信息结构体 //3、驱动设备号注销 unregister_chrdev(led.major,"led_dev"); } //驱动加载与卸载 module_init(led_init); module_exit(led_exit); //协议包含GPL MODULE_LICENSE("GPL");
中断驱动---检测外部中断 获取外设的数据内容,通过中断信号进行获取 在驱动中设置外设为中断模式:当外设产生设定的特定信号(就是中断) 在驱动中实现中断处理操作(函数)
本文以按键中断为例
需要使用按键设备,需要先在设备树中说明使用的按键是一个中断设备,我使用的板子是Samsung的Exynos系列,按键使用的是KEY3
开发板管脚
查看数据手册得到GPX1_2使用的中断是XEINT_10
在设备树中:arch/arm/boot/dts/exynos4x12-pinctrl.dtsi
gpx1: gpx1 { gpio-controller; #gpio-cells = <2>; interrupt-controller; interrupt-parent = <&gic>; interrupts = <0 24 0>, <0 25 0>, <0 26 0>, <0 27 0>, <0 28 0>, <0 29 0>, <0 30 0>, <0 31 0>; #interrupt-cells = <2>; }; 在设备树中添加自己的硬件设备信息---添加key3节点-----描述当前设备的的信息内容(中断号) arch/arm/boot/dts/exynos4412-fs4412.dts:实现硬件描述(中断号) key3_node { compatible = "key3"; interrupt-parent = <&gpx1>; interrupts = <2 4>;//26 };
在驱动中申请中断,实现中断处理
a、获取到中断号 获取设备树节点,返回值就是从设备树中找到的节点 struct device_node *of_find_node_by_path(const char *path); 从节点中获取到中断号,返回值就是中断号 unsigned int irq_of_parse_and_map(struct device_node *dev,int index); b、申请中断 int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev) 参数1: unsigned int irq:申请中断的中断号 参数2: irqreturn_t (*)(int, void *) ---- irq_handler_t irq_handler_t handler:函数指针,进行注册中断,当产生中断时调用对应的函数进行处理 参数3: unsigned long flags:中断处理的触发方式 #define IRQF_TRIGGER_NONE 0x00000000 #define IRQF_TRIGGER_RISING 0x00000001 #define IRQF_TRIGGER_FALLING 0x00000002 #define IRQF_TRIGGER_HIGH 0x00000004 #define IRQF_TRIGGER_LOW 0x00000008 参数4: const char *name:字符串首地址,中断的描述信息 /proc/inruppter 参数5: void *dev:传递给参数2的函数进行自动调用的(作为参数2这个函数的参数) 返回值: 成功返回0,失败返回非0
释放中断: void free_irq(unsigned int irq,void * dev_id) 参数1: unsigned int irq 中断号 参数2: void * dev_id:与申请中断第五个参数保持一致
中断分上下两部分:
上部分处理时间短、不费时间的中断处理
下部分处理一些费时间的中断
下部分就是将耗时操作延后处理,一般在上半部分的处理函数中调用
1、softirq:软中断,处理级别比较高,在内核机制中,需要修改内核源码功能 2、tasklet:实际上就是内部调用了softirq 3、workqueue:工作队列
驱动中申请中断举例:
#include#include #include #include #include #include #include #include #include #include #include char c; struct key_desc{ unsigned int major; struct class * cls; dev_t devno; struct device * dev; }key; irqreturn_t key_irq_handler(int i, void * j) { c='q'; printk("irqno : %d;input char %c\n",i,c); return IRQ_HANDLED; } //3、文件IO接口功能关联 ssize_t key_read (struct file * file, char __user * data, size_t size, loff_t * ops) { printk("key read\n"); int n = copy_to_user(data, &c, 1); c='w'; return 0; } int key_open (struct inode * inode , struct file * file) { printk("key open\n"); return 0; } int key_release (struct inode * inode , struct file *file) { printk("key close\n"); return 0; } const struct file_operations fops = { .read = key_read, .release = key_release, .open = key_open }; static int __init keydev_init(void) { key.major = 252; //1、申请设备号 int res = register_chrdev( key.major,"key", &fops); if(res<0) { goto err1; } //2、设备文件 key.cls = class_create(THIS_MODULE, "cls"); if(IS_ERR(key.cls)) { goto err2; } key.devno = key.major << 20 | 0; key.dev = device_create(key.cls, NULL,key.devno ,NULL,"key_dev"); if(IS_ERR(key.dev)) { goto err3; } //4、硬件初始化 //a.获取中断号 struct device_node * node = of_find_node_by_path("/key3");//查找设备树中节点为key_3 if(IS_ERR(node)) { goto err4; } int irqno = irq_of_parse_and_map(node, 0);//申请中断号 if(irqno<0) { goto err5; } //b.申请中断 res = request_irq(irqno, key_irq_handler,IRQF_TRIGGER_FALLING,"key_in", NULL); if(res<0) { goto err6; } return 0; err6: irqno = -1; err5: node=NULL; err4: device_destroy(key.cls, key.devno); err3: class_destroy(key.cls); err2: unregister_chrdev(key.major, "key"); err1: return -1; } static void __exit key_exit(void) { device_destroy(key.cls, key.devno); class_destroy(key.cls); unregister_chrdev(key.major, "key"); } module_init(keydev_init); module_exit(key_exit); MODULE_LICENSE("GPL");
测试程序
#include#include #include #include int main() { char buf; int num; int fd = open("/dev/key_dev",O_RDONLY); while(1) { num=0; num = read(fd,&buf,1); printf("%d%c\n",num,buf); } return 0; }
Makefile
#编译驱动代码 KERNEL_PATH = /home/ubuntu/code/kernel/linux-3.14 #APP=beep_test#测试程序的名字不包括后缀 MODULES_PATH = $(shell pwd) obj-m += key_int.o #把.c编译为.o文件注意.o前的名字与驱动文件名字一致 #编译为驱动程序.ko 要借助已经编译过的内核 all: make modules -C $(KERNEL_PATH) M=$(MODULES_PATH) # arm-none-linux-gnueabi-gcc $(APP).c -o $(APP) install: # cp *.ko $(APP) /home/ubuntu/rootfs cp *.ko /home/ubuntu/rootfs
驱动中的阻塞IO与非阻塞IO实现
驱动中实现阻塞: 要创建等待队列头: wait_queue_head_t head; init_waitqueue_head(&head);
1、在需要等待的位置(没有数据),就阻塞等待 wait_event_interruptible(wq,condition)-----根据参数是否进行阻塞等待,完成阻塞等待 参数1: wq:等待队列头,把当前进程加入到哪个等待队列中 参数2: condition:是否执行阻塞等待的条件 condition:真---不进行阻塞 condition:假---进行阻塞 2、合适位置进行阻塞唤醒 wake_up_interruptible(&head);
非阻塞:在进行读写操作时,如果没有数据,就立即返回,如果有数据读取数据然后立即返回 应用程序:设置为非阻塞打开 int fd = open("/dev/key3",O_RDONLY | O_NONBLOCK);
驱动文件中在阻塞前面添加: if((file->f_flags & O_NONBLOCK != 0) && (condition == 0)) return -1;
驱动总线模型: 驱动框架: 0、声明实现入口函数(module_init、module_exit) 1、申请设备号(register_chrdev) 2、创建设备节点(class_create、device_create) 3、硬件初始化 ioremap地址映射 中断申请 4、实现文件IO接口
总线模型: 总线bus 驱动driver 设备device
总线bus: struct bus_type:总线对象,描述一条总线,管理device、driver,进行匹配
struct bus_type { const char *name;:总线名字 int (*match)(struct device *dev, struct device_driver *drv);总线调用匹配设备和驱动,返回值就表示匹配成功与否 };
注册总线: int bus_register(struct bus_type * bus); 参数: struct bus_type * bus:总线对象 注销总线: void bus_unregister(struct bus_type * bus);
驱动driver: struct device_driver :驱动对象,描述一个驱动,对驱动进行说明
struct device_driver { const char *name;:驱动的名字 struct bus_type bus;总线对象,表示要把驱动注册到哪条总线 int (probe) (struct device dev);如果匹配成功,则调用该驱动的probe函数,创建驱动(申请设备号。。。) int (remove) (struct device *dev);当设备对象和驱动对象移除总线时会调用 } 注册驱动到总线: int driver_register(struct device_driver * drv); 从总线上注销: void driver_unregister(struct device_driver * drv);
设备device: struct device { struct kobject kobj;//所有对象的父类 const char *init_name;设备名 struct bus_type *bus;总线对象,表示要把设备注册到哪条总线 void platform_data;自定义数据,指向任意类型,可以存储设备信息 void (release)(struct device *dev);设备对象从总线移除时会调用 }; 注册设备到总线: int device_register(struct device * dev); 从总线上注销: void device_unregister(struct device * dev);