目录

一、代码编译环境

二、Linux驱动开发分类

三、字符设备驱动开发流程

1.编译驱动模块代码(Makefile)

2.驱动模块的加载与卸载

四、字符设备驱动编写

1.驱动模块

2. 字符设备驱动编写步骤

2.1 设备号的注册与注销

2.2 设备节点的注册与注销

2.3 实现硬件初始化

2.4 在驱动中实现文件io的接口

2.5 应用程序需要传递数据给驱动

3.应用程序和驱动程序扮演什么角色

4.应用程序编写

5.规范

6.测试

一、代码编译环境

1.使用什么工具来写驱动代码:安装source insight工具

(1)找到软件提示把工具安装激活 (2)把Linux内核代码解压到windows目录中 (3)打开工具添加查看的项目 projectnew project: 第一个对话框: 第一个文本框:输入工程名字

第二个文本框:路径(默认)

第二个对话框: 在项目源码位置,选择 Linux内核代码位置 点击OK 第三个对话框: 选择需要查看的Linux内核代码的包含的源码文件(要查看的内核目录),单击 add tree。 点击close 退出, 然后 重新打开刚才创建的工程 project→open project,如果提示 同步,点击确定,可能 需要很长时间

(4)编写代码 使用source insight工具编写代码,然后把这个程序拷贝到ubuntu系统中,使用交叉编译工 具进行编译,生成适配与开发板的程序。

二、Linux驱动开发分类

1、字符设备驱动,最多的。

2、块设备驱动,存储。

3、网络设备驱动。

一个设备不说是一定只属于某一个类型。比如USB WIFI,SDIO WIFI,属于网络设备驱动,因为他又有USB和SDIO,因此也属于字符设备驱动。

三、字符设备驱动开发流程

1.编译驱动模块代码(Makefile)

KERNEL_PATH=/home/yky/Code/linux-3.14-fs4412 #内核中的Makefile需要配置交叉编译工具链

obj-m += 模块文件名.o #要编译为模块的文件

all:

make modules -C $(KERNEL_PATH) M=$(shell pwd) #借助已经编译好的内核,编译模块

# -C 指定内核路径

# M:当前模块的位置

2.驱动模块的加载与卸载

Linux驱动程序可以编译到kernel里面,也就是zImage,也可以编译为模块,.ko。测试的时候只需要加载.ko模块就可以。

编写驱动的时候注意事项!

1、编译驱动的时候需要用到linux内核源码!因此要解压缩linux内核源码,编译linux内核源码。得到zImage和.dtb。需要使用编译后的到的zImage和dtb启动系统。

2、从SD卡启动,SD卡烧写了uboot。uboot通过tftp从ubuntu里面获取zimage和dtb,rootfs也是通过nfs挂载。

3、设置bootcmd和bootargs(根据自身情况进行调整)

set bootargs root=/dev/nfs nfsroot=192.168.3.201:/home/xwq/nfshome/rootfs rw console=ttySAC2,115200 init=/linuxrc ip=192.168.3.6

setenv bootcmd tftp 41000000 uImage\;tftp 42000000 exynos4412-fs4412.dtb\;bootm 41000000 - 42000000

4、将编译出来的.ko文件放到根文件系统里面。加载驱动会用到加载命令:insmod,modprobe。移除驱动使用命令rmmod。对于一个新的模块使用modprobe加载的时候需要先调用一下depmod命令。驱动模块加载成功以后可以使用lsmod查看一下。卸载模块使用rmmod命令

(1)insmod:加载模块

insmod 模块路径

(2)rmmod:卸载模块

mkdir /lib/modules mkdir /lib/modules/3.14.0 rmmod 模块名

(3)lsmod:查看已经加载的模块

四、字符设备驱动编写

1.驱动模块

 (1)符号导出

如果模块中内容需要在其他模块中使用,可以把内容进行导出:添加导出声明 EXPORT_SYMBOL(内容名字); 注:如果模块只用于进行导出,可以不写入口声明以及定义

(2)参数传递 

在编写驱动时,有些变量是不确定的,是根据驱动具体加载到哪个设备才确定,在进行 insmod装载驱动模块时再传递这些参数值。

如:加载模块:insmod perm.ko a=10 b=5 p="okokok"

在驱动代码中如何处理参数传递: module_param(name,type,perm);

参数1:参数的名字,变量名

参数2:参数的类型,int,char

参数3:/sys/modules 文件的权限,0666

2. 字符设备驱动编写步骤

1.实现模块加载和卸载入口函数

/*

1、头文件

2、驱动模块装载入口和卸载入口声明

3、模块装载函数和卸载函数

4、GPL声明

装载入口:当内核加载这个驱动模块时,从那个函数执行(声明,实现)

*/

//头文件

#include

#include

//模块加载入口函数实现

static int __init 函数名1(void)

{

//资源的创建,申请,创建驱动

return 0;

}

//模块卸载入口函数实现

static void __exit 函数名2(void)

{

//资源的释放,删除驱动

}

//模块入口声明

//装载声明(内核加载的入口指定)

module_init(函数名1);//只要加载就执行其中声明的函数

//卸载声明(内核卸载的入口指定)

module_exit(函数名2);

//GPL开源声明

MODULE_LICENSE("GPL");

2.在模块加载入口 3. 申请设备号(内核中用于区分和管理不同的字符设备驱动) 4. 创建设备节点(为用户提供一个可操作的文件接口---用户操作文件) 5. 实现硬件初始化 (1) 地址的映射 (2) 实现硬件的寄存器的初始化 (3) 中断的申请 6.实现文件io接口(struct file_operations fops结构体)

字符设备驱动要素

a. 必须有一个设备号,用于在内核中,众多的设备驱动进行区分 b. 用户(应用)必须知道设备驱动对应到哪个设备文件(设备节点)——Linux一切皆文件(把所有的设备都看作是文件) c. 对设备进行操作(驱动),其实就是对文件进行操作,应用空间操作open、read、write等文 件IO时,实际上驱动代码中对应执行的open、read、write函数

2.1 设备号的注册与注销

//申请设备号 int register_chrdev(unsigned int major, const char *name,const structfile_operations *fops)

参数1:

unsigned int major:主设备号

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

主设备号:表示同一类型的设备

次设备号:表示同一类型中的不同设备

参数有两种设置:

静态:指定一个整数:250----主设备号为250

动态:让内核随机指定,参数为0

参数2:

const char *name:一个字符串,描述设备信息,自定义

参数3:

const struct file_operations *fops:结构体指针,结构体变量的地址(结构体中就是应

用程序和驱动程序函数关联,open、read、write)---文件操作对象,提供open、read、write等驱动

中的函数

返回值:

如果是静态指定主设备号,返回0表示申请成功,返回负数表示申请失败

如果是动态申请,返回值就是申请成功的主设备号

/proc/devices:文件中包含了所有注册到内核的设备

//销毁注销设备号 void unregister_chrdev(unsigned int major,const char * name)

参数1:

unsigned int major:主设备号

参数2:

const char * name:设备信息,自定义

2.2 设备节点的注册与注销

 1.手动创建

创建设备节点---手动 mknod 设备节点名 设备类型 主设备号 次设备号 #如: mknod /dev/xxx c 250 0

2. 自动创建(通过udev/mdev机制)

//创建一个类(信息) struct class * class_create(owner,name)

参数1:

owner:一般填写 THIS_MODULE

参数2:

name:字符串首地址,名字,自定义

返回值:

返回值就返回信息结构体的地址

//创建设备节点(设备文件) 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:设备号(主设备+次设备)

Linux内核将设备号分为两部分:主设备号和次设备号。主设备号占用前12位,次设备号占用低20位。

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))

#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))

参数4:

void *drvdata:私有数据,一般填NULL

参数5、参数6:

const char *fmt, ...:可变参数,表示字符设备文件名(设备节点名)

//销毁设备节点 void device_destroy(struct class * class,dev_t devt)

 2.3 实现硬件初始化

1.控制外设,其实就是控制地址,内核驱动中通过虚拟地址操作,需要把外设控制的物理地址,映射到内核空间

//虚拟地址映射 void *ioremap(phys_addr_t offset, unsigned long size)

参数1:

phys_addr_t offset:物理地址

参数2:

unsigned long size:映射大小

返回值:

映射之后的虚拟地址

//解除映射 void iounmap(void *addr)

 2.操作寄存器地址的方式

(1)通过指针取 *

例如:

volatile unsigned int * gpx1con;

*gpx1con---进行操作

(2)IO内存访问函数

        使用ioremap函数将寄存器的物理地址映射到虚拟地址后,我们就可以直接通过指针访问这些地址,但是Linux内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

 例如:

我们32bit就采用readl()、writel()

//从地址中读取地址空间的值

u32 readl(const volatile void *addr)

//将value值,存储到对应地址中

void writel(u32 value, volatile void *addr)

2.4 在驱动中实现文件io的接口

1.在驱动中实现文件io的接口操作 const struct file_operations fops;结构体中就是 驱动 与 应用程序文件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);

};

//函数指针集合,其实就是接口,表示文件IO函数用函数指针来表示,存储驱动中的关联函数

// 如: read函数指针 = xxxx_ok;表示 read函数,在驱动中的关联函数为xxxx_ok函数

2.  应用调用去调用文件io去控制驱动

open(); read(); write();

2.5 应用程序需要传递数据给驱动

1.将驱动空间拷贝数据给应用空间

//这个功能一般用于驱动中的 read long copy_to_user(void __user *to,const void *from, unsigned long n)

参数1:

void __user *to:目标地址,应用空间地址

参数2:

const void *from:源地址,内核空间地址

参数3:

unsigned long n:个数

返回值:

成功返回0,失败返回大于0,表示还有多少个没有拷贝完

2.将应用空间数据拷贝到驱动空间

long copy_from_user(void * to,const void __user * from,unsigned long n)

参数1:

void *to:目标地址,内核空间地址

参数2:

const void *from:源地址,应用空间地址

参数3:

unsigned long n:个数

返回值:

成功返回0,失败返回大于0,表示还有多少个没有拷贝完

3.应用程序和驱动程序扮演什么角色

4.应用程序编写

Linux下一切皆文件,首先要open

例如:

#include

#include

#include

#include

#include

int main()

{

int fd = open("/dev/led0",O_RDWR);

int value = 0;

while(1)

{

scanf("%d",&value);

if(value > 1)

break;

write(fd,&value,4);

}

close(fd);

return 0;

}

5.规范

 (1)在加载入口实现资源申请,需要在卸载入口实现资源释放

//申请资源 register_chrdev();//申请设备号 class_create(); device_create();//创建设备节点 ioremap();//硬件资源映射 ------------------------ //释放资源 iounmap();//解除硬件资源映射 device_destroy();//释放设备节点 class_destroy();//释放节点信息类 unregister_chrdev();//释放设备号

(2)出错处理 在某个位置出错,要将之前申请的资源进行释放

static int __init key_init(void)

{

int ret;

//申请设备号

key.major = register_chrdev(0,"key",&fops);

if(key.major < 0)

{

printk("register_chrdev error\n");

ret = -1;

goto err_0;

}

//创建设备节点

key.cls = class_create(THIS_MODULE,"key cls");

if(IS_ERR(key.cls))

{

printk("class_create error\n");

ret = -2;

goto err_1;

}

key.dev = device_create(key.cls,NULL,MKDEV(key.major,0),NULL,"key3");

if(IS_ERR(key.dev))

{

printk("device_create error\n");

ret = -3;

goto err_2;

}

//硬件设备初始化

//中断初始化

struct device_node * np = of_find_node_by_path("/key_int_node");

if(np == NULL)

{

printk("of_find_node_by_path error\n");

ret = -4;

goto err_3;

}

key.irqno = irq_of_parse_and_map(np,0);

//申请中断

if( request_irq(key.irqno,key_handler,IRQF_TRIGGER_FALLING,"this is key","hello world") != 0 )

{

printk("request_irq error\n");

ret = -5;

goto err_3;

}

return 0;

err_3:

device_destroy(key.cls,MKDEV(key.major,0));

err_2:

class_destroy(key.cls);

err_1:

unregister_chrdev(key.major,"key");

err_0:

return ret;

}

(3) 面向对象编程思想

用一个结构体来表示一个对象 设计一个类型,描述一个设备的信息

例如:

struct BEEP

{

unsigned int major;

struct class * cls;

struct device * dev;

unsigned int * pwmtcfg0;

};

struct BEEP beep;//表示一个设备对象

 小妙招:文件私有数据

1.在open函数里面设置file->private_data为设备变量(结构体变量);

2.在read、write里要访问设备的时候,直接读取私有数据;

如:  struct newcharled_dev *dev=file->private_data;

6.测试

(1)加载驱动。

modprobe 驱动.ko

(2)进入/dev查看设备文件。

mknod /dev/........

(3)测试

./应用程序名

精彩文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: