linux驱动开发
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 <linux/init.h>
#include <linux/module.h>
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 <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/io.h>
#include <asm/uaccess.h>
//使用结构体表示一个驱动对象(面向对象的编程思想)
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_NONE0x00000000
#define IRQF_TRIGGER_RISING0x00000001
#define IRQF_TRIGGER_FALLING0x00000002
#define IRQF_TRIGGER_HIGH0x00000004
#define IRQF_TRIGGER_LOW0x00000008
参数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 <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <asm/io.h>
#include <asm/uaccess.h>
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 <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
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);