驱动就是获取外设、传感器数据和控制外设。数据会提交给应用程序。
Linux 驱动编译既要编写一个驱动,还要编写一个简单的测试应用程序。
而单片机下驱动和应用都是放在一个文件里,也就是杂在一块。而 Linux 则是分开了。
一、字符设备驱动开发流程
Linux 里一切皆文件,驱动设备表现就是一个/dev/下的文件,/dev/led。应用程序调用 open 函数 打开设备,比如 led。应用程序通过 write 函数向 /dev/led 写数据,比如写1打开,写0关闭。如果要关闭设备就是 close 函数。
字符设备驱动的编写主要是驱动对应的 open、close、read。其实就是 file_operations 结构体的成员变量的实现。
二、驱动模块的加载与卸载
Linux 驱动程序可以编译到 kernel 里,也就是 zImage。也可以编译成模块ko。测试的时候只需要加载ko即可。
1. 驱动编写
编写驱动的注意事项!
编译驱动的时候需要用到 linux 内核源码!因此需要解压缩 Linux 内核源码,编译 Linux 内核源码。得到 zImage 和 dtb。需要使用编译后得到的 zImage 和 dtb 启动系统。这部分不懂的回去看 Linux 内核移植部分。
先编写一个简单的源码,用于测试驱动。
cpp
#include <linux/module.h>
static int __init chrdevbase_init(void)
{
return 0;
}
static void __exit chrdevbase_exit(void)
{
}
/*
模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
Makefile 的编写
bash
KERNELDIR := /home/prover/linux/linux_ok
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build : kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
需要隐藏的文件
{
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.o":true,
"**/*.su":true,
"**/*.cmd":true,
"Documentation":true,
},
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/*.o":true,
"**/*.su":true,
"**/*.cmd":true,
"Documentation":true,
}
}
指定内核源码路径
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include",
"/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include",
"/home/prover/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
编译后,.ko就是我们需要的驱动文件了。
![](https://i-blog.csdnimg.cn/direct/e7c8daea0932486cacd53dce215033df.png)
2. 驱动模块的加载和卸载
开发板上使用命令 modprobe
![](https://i-blog.csdnimg.cn/direct/a11d47d726ea46b6802a19afffcffced.png)
发现需要创建/lib/modules。
![](https://i-blog.csdnimg.cn/direct/15e24ed0e00f49aba51b62767f640961.png)
将 .ko文件和可执行文件 chrdevbase.o 拷贝到该目录下
![](https://i-blog.csdnimg.cn/direct/bca6d7b5622b414789f3690cb4579069.png)
对于一个新的模块使用modprobe,需要先使用depmod命令,否则报下面错误:
![](https://i-blog.csdnimg.cn/direct/fb04764327fb49e783221fd2d7046ce0.png)
如果报下面错误,说明内核和你驱动不是同源的。
![](https://i-blog.csdnimg.cn/direct/f8ee202f27a244c6a57a54945fc26e61.png)
成功后,还有个 license 的警告。
![](https://i-blog.csdnimg.cn/direct/f948ac4484c04f7ba960ebec47311671.png)
在源码中添加 License,还可以再加个作者。当然我们还在函数中添加了printk语句,用于观察:
cpp
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init chrdevbase_init(void)
{
printk("chrdevbase_init\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");
make编译后,再拷贝到指定目录下。然后modprobe加载驱动,最后再rmmod卸载驱动。
![](https://i-blog.csdnimg.cn/direct/7021621893ee481f8e59bb9c51c31d57.png)
3. 字符设备注册与注销
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,同样,卸载驱动模 块的时候也需要注销掉字符设备。
函数原型为:
cpp
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
register_chrdev 函数用于注册字符设备,此函数一共有三个参数,这三个参数的含义如下:
major:主设备号,Linux 下每个设备都有一个设备号,设备号分为主设备号和次设备号两 部分,关于设备号后面会详细讲解。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
unregister_chrdev 函数用户注销字符设备,此函数有两个参数,这两个参数含义如下:
major:要注销的设备对应的主设备号。 name:要注销的设备对应的设备名。
先查看下存在的设备号,我们觉得设置为200。
![](https://i-blog.csdnimg.cn/direct/64899cf52ae84a96997d0e2f8f9bbd25.png)
cpp
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static struct file_operations test_fops;
static int __init chrdevbase_init(void)
{
//入口函数具体内容
int retvalue = 0;
//注册字符设备驱动
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
//字符设备注册失败
}
//printk("chrdevbase_init\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(200, "chrtest");
//printk("chrdevbase_exit\r\n");
}
/*
模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");
4. 实现设备的具体操作函数
file_operations 结构体就是设备的具体操作函数。
需要实现的基本功能:打开和关闭,读写。
cpp
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
//打开设备
static int chrtest_open(struct inode* inode, struct file* filp)
{
//用户实现具体功能
}
//从设备读取
static ssize_t chrtest_read(struct file* filp, char __user* buf,
size_t cnt, loff_t* offt)
{
//用户实现具体功能
return 0;
}
//向设备写数据
static ssize_t chrtest_write(struct file* filp,
const char __user* buf,
size_t cnt, loss_t *offt)
{
//用户实现具体功能
return 0;
}
//关闭/释放设备
static int chrtest_release(struct inode *inode, struct file* filp)
{
//用户实现具体功能
return 0;
}
static struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
static int __init chrdevbase_init(void)
{
//入口函数具体内容
int retvalue = 0;
//注册字符设备驱动
retvalue = register_chrdev(200, "chrtest", &test_fops);
if(retvalue < 0){
//字符设备注册失败
}
//printk("chrdevbase_init\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(200, "chrtest");
//printk("chrdevbase_exit\r\n");
}
/*
模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");
三、Linux 设备号
为了方便管理,Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分 组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了 一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面,定义如下:
cpp
typedef __u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;
可以看出 dev_t 是__u32 类型的,而__u32 定义在文件 include/uapi/asm-generic/int-ll64.h 里 面,定义如下:
cpp
typedef unsigned int __u32;
dev_t 其实就是 unsigned int 类型,是一个 32 位的数据类型。其中高 12 位为主设备号,低 20 位为次设备号。
设备号操作函数:
cpp
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
宏 MINORBITS 表示次设备号位数,一共是 20 位。
宏 MINORMASK 表示次设备号掩码。
宏 MAJOR 用于从 dev_t 中获取主设备号,将 dev_t 右移 20 位即可。
宏 MINOR 用于从 dev_t 中获取次设备号,取 dev_t 的低 20 位的值即可。
宏 MKDEV 用于将给定的主设备号和次设备号的值组合成 dev_t 类型的设备号。
前面自己分配的200这个设备号,其实算静态分配。当然也有提供动态分配设备号的方式,设备号的申请函数如下:
cpp
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这 些设备号的主设备号一样,但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递 增。一般 baseminor 为 0,也就是说次设备号从 0 开始。
count:要申请的设备号数量。
name:设备名字。
注销字符设备之后要释放掉设备号,设备号释放函数如下:
cpp
void unregister_chrdev_region(dev_t from, unsigned count)
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。
四、字符设备驱动开发实验
1. 完善驱动程序
第二节将驱动的框架写好了,接下来要完善设备号等一系列东西。
cpp
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/types.h>
#define CHRDEVBASE_MAJOR 200 //主设备号
#define CHRDEVBASE_NAME "chrdevbase" //设备名
static char readbuf[100];
static char writebuf[100];
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打开设备
* @param -- inode : 传递给驱动的 inode
* @param - filp : 设备文件,file 结构体有个叫做 private_data 的成员变量
* 一般在 open 的时候将 private_data 指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrtest_open(struct inode* inode, struct file* filp)
{
//用户实现具体功能
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrtest_read(struct file* filp, char __user* buf,
size_t cnt, loff_t* offt)
{
//用户实现具体功能
int retvalue = 0;
//向用户空间发送数据
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
return 0;
}
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrtest_write(struct file* filp,
const char __user* buf,
size_t cnt, loff_t *offt)
{
//用户实现具体功能
int retvalue = 0;
//接收用户空间传递给内核的数据并且打印出来
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n",writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
return 0;
}
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrtest_release(struct inode *inode, struct file* filp)
{
//用户实现具体功能
return 0;
}
/*
* 设备操作函数结构体
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrtest_open,
.read = chrtest_read,
.write = chrtest_write,
.release = chrtest_release,
};
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
//入口函数具体内容
int retvalue = 0;
//注册字符设备驱动
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0){
//字符设备注册失败
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase_init()\r\n");
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase_exit()\r\n");
}
/*
模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
//LICENSE 和 作者信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Prover");
2. 编写测试APP
这部分,如果有 Linux C 编程的基础就更好了。调用一些 C 库文件操作基本函数。
chrdevbaseApp.c
cpp
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/*
* 使用方法
*./chrdevbaseApp /dev/chrdevbase <1>|<2>
* argv[2] 1:读文件
* argv[2] 2:写文件
*/
static char usrdata[] = {"usr data!"};
/*
* @description : main 主程序
* @param - argc : argv 数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}
if(atoi(argv[2]) == 1){ /* 从驱动文件读取数据 */
retvalue = read(fd, readbuf, 50);
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
/* 读取成功,打印出读取成功的数据 */
printf("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
使用交叉编译器编译
cpp
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
3. 加载驱动模块
将驱动文件和App文件放入根文件的lib/modules/4.1.15下
cpp
sudo cp chrdevbase.ko chrdevbaseApp /home/prover/linux/nfs/rootfs/lib/modules/4.1.15/ -f
用modprobe驱动 .ko 后,查看设备号。
cpp
cat /proc/devices
![](https://i-blog.csdnimg.cn/direct/76cf937cdd794c79bdeb65e1bd0a6f27.png)
当前系统存在 chrdevbase 这个设备,主设备号为 200,跟我们设置 的主设备号一致。
4. 创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操 作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节 点文件:
cpp
mknod /dev/chrdevbase c 200 0
然后查看
![](https://i-blog.csdnimg.cn/direct/8b7ac93090c8458a853b9d89a7d91282.png)
5. 设备操作测试
cpp
./chrdevbaseApp /dev/chrdevbase 1
![](https://i-blog.csdnimg.cn/direct/24110bf9d493456db0ba7a0951170ccc.png)
第一行是 chrdevbase_read 函数 输出的信息。第二行则是APP中输出的接收到的数据:kernel data!
刚才的 1 是读文件操作,现在输入 2 来实现写文件操作:
cpp
./chrdevbaseApp /dev/chrdevbase 2
既然读写都没问题,说明我们编写 的 chrdevbase 驱动是没有问题的。
6. 卸载驱动模块
不再使用某个设备的话,驱动卸载即可。
cpp
rmmod chrdevbase.ko
![](https://i-blog.csdnimg.cn/direct/a0d20ab72de8455d8bfa069631f06156.png)