文章目录
- 前言
- 一、驱动学习预备知识
- 1.什么是设备驱动程序
- 2.向内核添加新功能方法
- 2.1新功能源码与Linux内核源码不在同目录下
- 2.2在Ubuntu下加载和删除ko文件步骤
- 2.3在开发板下加载和删除ko文件步骤
- 2.4内核模块基础代码解析
- 二、字符设备驱动框架
- 2.1Linux内核对设备的分类
- 2.2字符设备驱动开发步骤
- 2.3字符设备驱动代码编写
- 2.4字符设备应用层代码编写
- 2.5字符设备驱动添加读写
前言
记录嵌入式驱动学习笔记一、驱动学习预备知识
学习驱动前要了解的基础内容和命令。
1.什么是设备驱动程序
一种添加到操作系统中的特殊程序,主要作用是协助操作系统完成应用程序与对应硬件设备之间数据传送的功能。简言之,设备驱动程序就是操作系统中“驱动”对应硬件设备使之能正常工作的代码。
一个驱动程序主要完成如下工作:
- 初始化设备,让设备做好开始工作的准备
- 读数据:将设备产生的数据传递给上层应用程序
- 写数据:将上层应用程序交付过来的数据传递给设备
- 获取设备信息:协助上层应用程序获取设备的属性、状态信息
- 设置设备信息:让上层应用程序可以决定设备的一些工作属性、模式
- 其它相关操作:如休眠、唤醒、关闭设备等
其中最核心的工作就是设备数据的输入和输出,因此计算机外部设备(外设)也被称为IO设备
2.向内核添加新功能方法
向内核添加新功能的方法分为两种,一种静态加载方法,一种动态加载方法。
静态加载方法思想:即新功能源码与内核其它代码一起编译进uImage文件内,在学习驱动的阶段我们经常采用动态加载方法因此不过多介绍该方法。
动态加载方法思想:即新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko
2.1新功能源码与Linux内核源码不在同目录下
- cd ~/fs4412
- mkdir mydrivercode
- cd mydrivercode
- cp …/linux-3.14/drivers/char/myhello.c .
- vim Makefile
- make (生成的ko文件适用于主机ubuntu linux)
- make ARCH=arm (生成的ko文件适用于开发板linux,注意此命令执行前,开发板的内核源码已被编译)
#file命令可以查看指定ko文件适用于哪种平台,用法:
file ko文件
#结果带x86字样的适用于主机ubuntu linux,带arm字样的适用于开发板linux
2.2在Ubuntu下加载和删除ko文件步骤
sudo insmod ./???.ko
#此处为内核模块文件名,将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod
#查看已被插入的内核模块有哪些,显示的是插入内核后的模块名
sudo rmmod ???
#此处为插入内核后的模块名,此时将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
sudo dmesg -C #清除内核已打印的信息
dmesg #查看内核的打印信息
2.3在开发板下加载和删除ko文件步骤
#先将生成的ko文件拷贝到/opt/4412/rootfs目录下:
cp ????/???.ko /opt/4412/rootfs
#在串口终端界面开发板Linux命令行下执行
insmod ./???.ko #将内核模块插入正在执行的内核中运行 ----- 相当于安装插件
lsmod #查看已被插入的内核模块有哪些
rmmod ??? #将已被插入的内核模块从内核中移除 ----- 相当于卸载插件
内核随时打印信息,我们可以在串口终端界面随时看到打印信息,不需要dmesg命令查看打印信息
2.4内核模块基础代码解析
Linux内核的插件机制——内核模块
类似于浏览器、eclipse这些软件的插件开发,Linux提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。
代码如下(示例):
#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE/*该函数在模块被插入进内核时调用,主要作用为新功能做好预备工作被称为模块的入口函数__init的作用 :
1. 一个宏,展开后为:__attribute__ ((__section__ (".init.text"))) 实际是gcc的一个特殊链接标记
2. 指示链接器将该函数放置在 .init.text区段
3. 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置
*/
int __init myhello_init(void)
{/*内核是裸机程序,不可以调用C库中printf函数来打印程序信息,Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel)printk不支持浮点数打印*/printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("myhello is running\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");printk("#####################################################\n");return 0;
}/*该函数在模块从内核中被移除时调用,主要作用做些init函数的反操作被称为模块的出口函数__exit的作用:
1.一个宏,展开后为:__attribute__ ((__section__ (".exit.text"))) 实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
*/
void __exit myhello_exit(void)
{printk("myhello will exit\n");
}/*
MODULE_LICENSE(字符串常量);
字符串常量内容为源码的许可证协议 可以是"GPL" "GPL v2" "GPL and additional rights" "Dual BSD/GPL" "Dual MIT/GPL" "Dual MPL/GPL"等, "GPL"最常用其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL协议,如果发现不遵循GPL,则在插入模块时打印抱怨信息:myhello:module license 'unspecified' taints kernelDisabling lock debugging due to kernel taint
也会导致新模块没法使用一些内核其它模块提供的高级功能
*/
MODULE_LICENSE("GPL");/*
module_init 宏
1. 用法:module_init(模块入口函数名)
2. 动态加载模块,对应函数被调用
3. 静态加载模块,内核启动过程中对应函数被调用
4. 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
5. 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
*/
module_init(myhello_init);/*
module_exit宏
1.用法:module_exit(模块出口函数名)
2.动态加载的模块在卸载时,对应函数被调用
3.静态加载的模块可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略
4.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
5.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
*/
module_exit(myhello_exit);
模块三要素:入口函数 出口函数 MODULE__LICENSE
Makefile文件用于生成ko模块,代码如下
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= 目标板linux内核源码顶层目录的绝对路径
ROOTFS ?= 目标板根文件系统顶层目录的绝对路径
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
modules:$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
else
obj-m += hello.o
endif
内核接口头文件查询
- 首先在include/linux 查询指定函数:grep 名称 ./ -r -n
- 找不到则更大范围的include目录下查询,命令同上
二、字符设备驱动框架
2.1Linux内核对设备的分类
Linux内核按驱动程序实现模型框架的不同,将设备分为三类:
- 字符设备:按字节流形式进行数据读写的设备,一般情况下按顺序访问,数据量不大,一般不设缓存
- 块设备:按整块进行数据读写的设备,最小的块大小为512字节(一个扇区),块的大小必须是扇区的整数倍,Linux系统的块大小一般为4096字节,随机访问,设缓存以提高效率。块设备不直接面向应用程序,一般使用一个文件系统去对接。
- 网络设备:针对网络数据收发的设备
2.2字符设备驱动开发步骤
A:设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。Linux 提供了一个名为 dev_t 的数据类型表示设备号。dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型,,其中高 12 位为主设备号,低 20 位为次设备号。应用程序打开一个设备文件时,通过设备号来查找定位内核中管理的设备。
MKDEV宏用来将主设备号和次设备号组合成32位完整的设备号,用法:
dev_t devno;int major = 251;//主设备号int minor = 2;//次设备号devno = MKDEV(major,minor);
MAJOR宏用来从32位设备号中分离出主设备号,用法:
dev_t devno = MKDEV(249,1);int major = MAJOR(devno);
MINOR宏用来从32位设备号中分离出次设备号,用法:
dev_t devno = MKDEV(249,1);int minor = MINOR(devno);
如果已知一个设备的主次设备号,应用层指定好设备文件名,那么可以用mknod命令在/dev目录创建代表这个设备的文件,即此后应用程序对此文件的操作就是对其代表的设备操作,mknod用法如下:
//其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
//字符设备,“200”是设备的主设备号,“0”是设备的次设备号。mknod /dev/chrdevbase c 200 0
B:申请和注销设备号
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模块的时候也需要注销掉字符设备。字符设备的注册和注销函数有两套如下所示:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号
参数:from:自己指定的设备号count:申请的设备数量name:/proc/devices文件中与该设备对应的名字,方便用户层查询主设备号
返回值:成功为0,失败负数,绝对值为错误码int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count, const char *name)
功能:动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号
参数:dev:分配设备号成功后用来存放分配到的设备号baseminior:起始的次设备号,一般为0count:申请的设备数量name:/proc/devices文件中与该设备对应的名字,方便用户层查询主次设备号
返回值:成功为0,失败负数,绝对值为错误码分配成功后在/proc/devices 可以查看到申请到主设备号和对应的设备名,mknod时参数可以参考查到的此设备信息void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放设备号
参数:from:已成功分配的设备号将被释放count:申请成功的设备数量释放后/proc/devices文件对应的记录消失
C:注册字符设备
在 Linux 中使用 cdev 结构体表示一个字符设备,在 cdev 中有两个重要的成员变量:ops 和 dev,这两个就是字符设备文件操作函数集合file_operations 以及设备号 dev_t。编写字符设备驱动之前需要定义一个 cdev 结构体变量,这个变量就表示一个字符设备。
1.定义 cdev 结构体变量
struct cdev test_cdev;
2.用 cdev_init 函数对其进行初始化
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下
//参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
3.cdev_add 函数用于向 Linux 系统添加字符设备
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量),首先使用 cdev_init 函数,完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。cdev_add 函数原型如下
//参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
//数 count 是要添加的设备数量。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
4.使用 cdev_del 函数从 Linux 内核中删除相应的字符设备
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下
//参数 p 就是要删除的字符设备。如果要删除字符设备
void cdev_del(struct cdev *p)
使用 cdev_init 函数初始化 cdev 变量的示例代码如下:
struct cdev testcdev;
/* 设备操作函数 */static struct file_operations test_fops = {.owner = THIS_MODULE,/* 其他具体的初始项 */};testcdev.owner = THIS_MODULE;cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */cdev_add(&testcdev, devid, 1);/* 添加字符设备 */
小结:
字符设备驱动开发步骤:
- 如果设备有自己的一些控制数据,则定义一个包含struct cdev cdev成员的结构体struct mydev,其它成员根据设备需求,设备简单则直接用struct cdev
- 定义一个struct mydev或struct cdev的全局变量来表示本设备;也可以定义一个struct mydev或struct cdev的全局指针(记得在init时动态分配)
- 定义三个全局变量分别来表示主设备号、次设备号、设备数
- 定义一个struct file_operations结构体变量,其owner成员置成THIS_MODULE
- module init函数流程:a. 申请设备号 b. 如果是全局设备指针则动态分配代表本设备的结构体元素 c. 初始化struct cdev成员 d. 设置struct cdev的owner成员为THIS_MODULE e. 添加字符设备到内核
- module exit函数:a. 注销设备号 b. 从内核中移除struct cdev c. 如果如果是全局设备指针则释放其指向空间
- 编写各个操作函数并将函数名初始化给struct file_operations结构体变量
验证操作步骤:
- 编写驱动代码mychar.c
- make生成ko文件
- insmod内核模块
- 查阅字符设备用到的设备号(主设备号):cat /proc/devices | grep 申请设备号时用的名字
- 创建设备文件(设备节点) : mknod /dev/??? c 上一步查询到的主设备号 代码中指定初始次设备号
- 编写app验证驱动(testmychar_app.c)
- 编译运行app,dmesg命令查看内核打印信息
2.3字符设备驱动代码编写
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{printk("mychar_open is called\n");return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{printk("mychar_close is called\n");return 0;
}//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{.owner=THIS_MODULE,.open=mychar_open,.release=mychar_close,
};int __init mychar_init(void)
{//用来接收注册设备函数的返回值int ret = 0;//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);/*1.申请设备号*///静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号//ret = register_chrdev_region(devno,mychar_num,"mychar");ret = register_chrdev_region(devno,mychar_num,mycharname);//分配失败返回负数if(ret){//动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号//ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);if(ret){printk("get devno failed\n");return -1;}//如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号major = MAJOR(devno);}/*2.将struct cdev对象添加到内核对应的数据结构里*///初始化cdev 给struct cdev对象指定操作函数集cdev_init(&mydev,&myops);mydev.owner=THIS_MODULE;//将字符设备添加进内核 哈希管理列表cdev_add(&mydev,devno,1);return 0;
}void __exit mychar_exit(void)
{//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);//注销字符设备驱动 入口参数:设备号 设备名称//unregister_chrdev(devno,"mychar");//将字符设备从内核移除cdev_del(&mydev);unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);
2.4字符设备应用层代码编写
#include <stdio.h>
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"int main(int argc, char *argv[])
{int fd = -1;if(argc < 2){printf("Error Usage!\r\n");return 1;}fd = open(argv[1],O_RDONLY);if(fd < 0){printf("Can't open file %s\r\n", argv[1]);return 2;}close(fd);fd = -1;return 0;
}
2.5字符设备驱动添加读写
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
/*
完成功能:读取设备产生的数据
参数:filp:指向open产生的struct file类型的对象,表示本次read对应的那次openpbuf:指向用户空间一块内存,用来保存读到的数据count:用户期望读取的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置返回值:本次成功读取的字节数,失败返回-1
*/
unsigned long copy_to_user (void __user * to, const void * from, unsigned long n)ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);
/*
完成功能:向设备写入数据
参数:filp:指向open产生的struct file类型的对象,表示本次write对应的那次openpbuf:指向用户空间一块内存,用来保存被写的数据count:用户期望写入的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置返回值:本次成功写入的字节数,失败返回-1
*/
unsigned long copy_from_user (void * to, const void __user * from, unsigned long n)
添加读写的字符设备驱动代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>#define BUF_LEN 100
//主设备号
int major = 11;
//次设备号
int minor = 0;
//注册设备数量
int mychar_num = 1;
//设备名
char mycharname[] ="mychar";
//创建cdev类型的对象 cdevinit函数要用到
struct cdev mydev;
//全局数组 字符设备产生的数据存放
char mydev_buf[BUF_LEN];
//数组下标从零开始
int curlen=0;
int mychar_open(struct inode *pnode, struct file *pfile) //打开设备
{printk("mychar_open is called\n");return 0;
}
int mychar_close(struct inode *pnode, struct file *pfile) //关闭设备
{printk("mychar_close is called\n");return 0;
}
//读数据是对于应用层来说 把内核数据拷贝到应用层来读取
/*
ssize_t xxx_read(struct file *filp, char __user *pbuf, size_t count, loff_t *ppos);
完成功能:读取设备产生的数据
参数:filp:指向open产生的struct file类型的对象,表示本次read对应的那次openpbuf:指向用户空间一块内存,用来保存读到的数据count:用户期望读取的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示读取的起始位置,读完后也需要变更位置指示器的指示位置返回值:本次成功读取的字节数,失败返回-1
*/
ssize_t mychar_read(struct file *pfile, char __user *puser, size_t count, loff_t *p_pos)
{int ret = 0;//要读的数据大于数据的长度 修改要读数据的长度int size = 0;if(count > curlen){size = curlen;}else{size=count;}//将内核空间数据复制到用户空间 用户空间:puser 内核空间:mydev_bufret = copy_to_user(puser,mydev_buf,size);if(ret){printk("copy_to_user failed\n");return -1;}//将剩下的数据移动到数组起始 即删除已读数据memcpy(mydev_buf,mydev_buf + size,curlen - size);curlen = curlen - size;return size;
}
//写数据对于应用层来说 把数据写进驱动
/*
ssize_t xxx_write (struct file *filp, const char __user *pbuf, size_t count, loff_t *ppos);
完成功能:向设备写入数据
参数:filp:指向open产生的struct file类型的对象,表示本次write对应的那次openpbuf:指向用户空间一块内存,用来保存被写的数据count:用户期望写入的字节数ppos:对于需要位置指示器控制的设备操作有用,用来指示写入的起始位置,写完后也需要变更位置指示器的指示位置返回值:本次成功写入的字节数,失败返回-1
*/
ssize_t mychar_write(struct file *pfile, const char __user *puser, size_t count, loff_t *p_pos)
{int size = 0;int ret = 0;//如果剩余数组长度小于要写入的数据长度 则把要写入的数据修改 if(count > BUF_LEN - curlen){size = BUF_LEN - curlen;}else{size = count;}//从用户空间拷贝数据到内核空间ret = copy_from_user(mydev_buf + curlen,puser,size);if(ret){printk("copy_from_user failed\n");return -1;}curlen = curlen + size;return size;
}
//内核驱动操作函数集合 cdevinit函数要用到
struct file_operations myops =
{.owner=THIS_MODULE,.open=mychar_open,.release=mychar_close,.read=mychar_read,.write=mychar_write,
};int __init mychar_init(void)
{//用来接收注册设备函数的返回值int ret = 0;//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);/*1.申请设备号*///静态分配 手动分配设备号,先验证设备号是否被占用,如果没有则申请占用该设备号//ret = register_chrdev_region(devno,mychar_num,"mychar");ret = register_chrdev_region(devno,mychar_num,mycharname);//分配失败返回负数if(ret){//动态分配设备号,查询内核里未被占用的设备号,如果找到则占用该设备号//ret = alloc_chrdev_region(&devno,minor,mychar_num,"mychar");ret = alloc_chrdev_region(&devno,minor,mychar_num,mycharname);if(ret){printk("get devno failed\n");return -1;}//如果是动态分配说明自己设置的设备号被占用了 系统会更换主设备号 重新获取一下主设备号major = MAJOR(devno);}/*2.将struct cdev对象添加到内核对应的数据结构里*///初始化cdev 给struct cdev对象指定操作函数集cdev_init(&mydev,&myops);mydev.owner=THIS_MODULE;//将字符设备添加进内核 哈希管理列表cdev_add(&mydev,devno,1);return 0;
}void __exit mychar_exit(void)
{//主设备号和次设备号合并dev_t devno = MKDEV(major,minor);//注销字符设备驱动 入口参数:设备号 设备名称//unregister_chrdev(devno,"mychar");//将字符设备从内核移除cdev_del(&mydev);unregister_chrdev_region(devno,mychar_num);
}
MODULE_LICENSE("GPL");
module_init(mychar_init);
module_exit(mychar_exit);