目录
一、代码编译环境
二、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)打开工具添加查看的项目 projectnew 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)测试
./应用程序名
精彩文章
发表评论