字符设备框架搭建
(1)创建工程
新建驱动开发项目文件目录
1、添加头文件路径
因为是编写 Linux 驱动,因此会用到 Linux 源码中的函数。我们需要在 VSCode 中添加 Linux
源码中的头文件路径。打开 VSCode,按下“Crtl+Shift+P”打开 VSCode 的控制台,然后输入
“C/C++: Edit configurations(JSON) ”,打开 C/C++编辑配置文件
打开以后会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件
对该文件进行修改,修改后的c_cpp_properties.json 为
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/f3161/linux/IMX6ULL/linux/include",
"/home/f3161/linux/IMX6ULL/linux/arch/arm/include",
"/home/f3161/linux/IMX6ULL/linux/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
其中 includPath内为内核目录,修改成自己具体的目录
2、Makefile文件
新建Makefile文件,输入内容如下
KERNELDIR := /home/f3161/linux/IMX6ULL/linux
CURRENT_PATH := $(shell pwd)
obj-m := xxx.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第一行为内核路径
第三行是生成的.ko文件名,需要与.c文件对应。
(2)编写驱动程序xxx.c
1、头文件
xxx.c头文件(包含了后续所有常见实验的头文件在内)如下
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fcntl.h>
#define XXX_CNT 1 //设备号个数
#define XXX_NAME "xxx" //设备号名字
2、注册驱动
/* 驱动入口函数 */
static int __init xxx_init(void)
{
int ret = 0;
return ret;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
}
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FANG");
注意驱动入口出口函数的参数。
3、添加设备结构体
//设备结构体
struct xxx_dev{
dev_t devid;//设备号
int major;//主设备号
int minor;//次设备号
struct cdev cdev; //字符设备
struct class *class;//类
struct device *device;//设备
struct device_node *nd;//设备树节点
int xxx_gpio; //GPIO编号
};
struct xxx_dev xxx;
devid:每个设备都有一个设备号,设备号由主设备号(major)和次设备号两部分组成
cdev:用来注册字符设备
class,device:来于自动创建设备节点
xxx_gpio: 设备所用的GPIO编号(设备树需要)
4、申请设备号
在入口函数static int __init xxx_init(void)中添加
xxx.major = 0;
if(xxx.major){ //给定设备号
xxx.devid = MKDEV(xxx.major, 0);
ret = register_chrdev_region(xxx.devid, DTSLED_CNT, XXX_NAME);
}else{ // 申请设备号
ret = alloc_chrdev_region(&xxx.devid, 0, DTSLED_CNT, XXX_NAME);
xxx.major = MAJOR(xxx.devid);
}
if(ret<0){
goto fail_devid;
}
其中,DTSLED_CNT表示设备号个数,需要.c文件开头添加宏 #define DTSLED_CNT 1
XXX_NAME表示设备号名字,添加宏 #define XXX_NAME "xxx"
MKDEV用于将给定的主设备号和次设备号的值(这里是0)组合成 dev_t 类型的设备号。
register_chrdev_region函数使用给定的主设备号和次设备号注册设备号
函数原型:int register_chrdev_region(dev_t from, unsigned count, const char *name)
参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一般都是一个;参数 name 是设备名字。
alloc_chrdev_region函数是在没有指定设备号时申请设备号
函数原型:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数dev是申请到的设备号,baseminor是次设备号起始地址(一般用0),count和name同上。
如果申请失败,则goto fail_devid(后面提及)
在驱动出口函数static void __exit disled_exit(void)中,需要释放设备号
unregister_chrdev_region(xxx.devid, DTSLED_CNT);
5、添加字符设备操作集
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.write = xxx_write,
.open = xxx_open,
.read = xxx_read,
.release = xxx_release,
};
添加字符设备操作集对应的函数
static int xxx_open(struct inode *inode, struct file *filp){
filp->private_data = &xxx;
return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count,loff_t *ppos){
struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
return 0;
}
static int xxx_release(struct inode *inode, struct file *filp){
struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
return 0;
}
static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
return 0;
}
其中设置了文件的私有属性,将设备结构体作为私有数据添加到设备文件中。在上述函数中使用dev变量代替xxx_dev
xxx_write函数
xxx_write函数表示用户空间(应用程序)传递给内核(驱动程序)的数据并且打印出来,因为用户空间内存不能直接访问内核空间的内存,所以需要借助函数 copy_from_user 将用户空间的数据复制到 writebuf 这个内核空间中。
函数原型:
static inline long copy_from_user(void __user *to, const void *from, unsigned long n)
参数 to 表示目的(驱动程序数据),参数 from 表示源(应用程序数据),参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。
所以修改后的xxx_write()函数:——>将应用程序中的数据传递给驱动程序
static char writebuf[100];
static ssize_t xxx_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;
}
xxx_read函数
xxx_read函数表示内核(驱动程序)向用户空间(应用程序)传递的数据 ,因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
函数原型:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
参数 to 表示目的(应用程序数据),参数 from 表示源(驱动程序数据),参数 n 表示要复制的数据长度。如果复制成功,返回值为 0,如果复制失败则返回负数。
所以修改后的xxx_read()函数:——>将驱动程序中的kerneldata变量的数据传递给应用程序
static char kerneldata[] = {"kernel data!"};
static char readbuf[100];
static ssize_t xxx_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;
}
6、注册字符设备
在入口函数static int __init xxx_init(void)中添加
xxx.cdev.owner = THIS_MODULE;
cdev_init(&xxx.cdev, &xxx_fops);
ret = cdev_add(&xxx.cdev, xxx.devid, XXX_CNT);
if(ret<0){
goto fail_cdev;
}
cdev_init函数对字符设备进行初始化。
函数原型:void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合(标题5中内容)。
cdev_add函数用于向 Linux 系统添加字符设备(cdev 结构体变量)。
函数原型:int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参
数 count 是要添加的设备数量。
注:如果字符设备注册失败,则goto fail_cdev
在驱动出口函数static void __exit disled_exit(void)中,需要删除字符设备
cdev_del(&xxx.cdev);
7、自动创建设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点。
需要在入口函数static int __init xxx_init(void)中创建设备节点,
xxx.class = class_create(THIS_MODULE,XXX_NAME);
if(IS_ERR(xxx.class)){
ret = PTR_ERR(xxx.class);
goto fail_class;
}
xxx.device = device_create(xxx.class, NULL, xxx.devid, NULL, XXX_NAME);
if(IS_ERR(xxx.device)){
ret = PTR_ERR(xxx.device);
goto fail_device;
}
class_create函数用于创建类
函数原型:struct class *class_create (struct module *owner, const char *name)
参数 owner 一般为 THIS_MODULE,参数 name 是类名字。
返回值是个指向结构体 class 的指针,也就是创建的类。
注:如果类创建失败,需要goto _class
在驱动出口函数static void __exit disled_exit(void)中,需要摧毁类
class_destroy(xxx.class);
device_create函数用于创建设备
函数原型:struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
参数 class 就是设备要创建哪个类;
参数 parent 是父设备,一般为 NULL,也就是没有父设备;
参数 devt 是设备号;
参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件;
返回值就是创建好的设备。
注:如果创建失败,则goto fail_device
在驱动出口函数static void __exit disled_exit(void)中,需要摧毁设备
device_destroy(xxx.class, xxx.devid);
8、创建失败goto和return
在入口函数static int __init xxx_init(void)最后中添加goto内容
return 0;
fail_rs:
fail_findnd:
device_destroy(xxx.class, xxx.devid);
fail_device://创建设备失败
class_destroy(xxx.class);
fail_class://创建类失败
cdev_del(&xxx.cdev);
fail_cdev://字符设备失败
unregister_chrdev_region(xxx.devid, XXX_CNT);
fail_devid://设备号失败
return ret;
如果创建成功,则返回 0,否则返回ret为负数,其中各种goto顺序不能乱,比如如果申请字符设备失败,说明设备号已经申请成功,需要释放设备号;如果创建类失败,说明字符设备和设备号创建成功,需要释放。代码依次执行。
9、驱动出口函数编写
该函数中需要释放字符设备,设备号,类,设备
static void __exit xxx_exit(void){
//删除设备
cdev_del(&xxx.cdev);
//释放设备号
unregister_chrdev_region(xxx.devid, XXX_CNT);
//摧毁设备
device_destroy(xxx.class, xxx.devid);
//摧毁类
class_destroy(xxx.class);
}
函数原型:void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。
函数原型是:void unregister_chrdev_region(dev_t from, unsigned count)
参数from:要释放的设备号。count:表示从 from 开始,要释放的设备号数量。
函数原型:void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。(在驱动出口函数中,需要先摧毁设备,再摧毁类)
函数原型:void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
10、xxx.c整体代码
xxx.c文件
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/timer.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#define XXX_CNT 1 //设备号个数
#define XXX_NAME "xxx" //设备号名字
//设备结构体
struct xxx_dev{
dev_t devid;//设备号
int major;//主设备号
struct cdev cdev; //字符设备
struct class *class;//类
struct device *device;//设备
struct device_node *nd;//设备树节点
int xxx_gpio; //GPIO编号
};
struct xxx_dev xxx;
static int xxx_open(struct inode *inode, struct file *filp){
filp->private_data = &xxx;
return 0;
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos){
struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
return 0;
}
static int xxx_release(struct inode *inode, struct file *filp){
struct xxx_dev *dev = (struct xxx_dev*)filp->private_data;
return 0;
}
static ssize_t xxx_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{
struct xxx_dev *dev = (struct xxx_dev *)filp->private_data;
return 0;
}
static const struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.write = xxx_write,
.open = xxx_open,
.release = xxx_release,
.read = xxx_read,
};
//驱动入口函数
static int __init xxx_init(void){
int ret = 0;
//注册设备号
xxx.major = 0;
if(xxx.major){ //给定设备号
xxx.devid = MKDEV(xxx.major, 0);
ret = register_chrdev_region(xxx.devid, XXX_CNT, XXX_NAME);
}else{ // 申请设备号
ret = alloc_chrdev_region(&xxx.devid, 0, XXX_CNT, XXX_NAME);
xxx.major = MAJOR(xxx.devid);
}
if(ret<0){
goto fail_devid;
}
//添加字符设备
xxx.cdev.owner = THIS_MODULE;
cdev_init(&xxx.cdev, &xxx_fops);
ret = cdev_add(&xxx.cdev, xxx.devid, XXX_CNT);
if(ret<0)
goto fail_cdev;
//自动创建设备节点
xxx.class = class_create(THIS_MODULE,XXX_NAME);
if(IS_ERR(xxx.class)){
ret = PTR_ERR(xxx.class);
goto fail_class;
}
xxx.device = device_create(xxx.class, NULL,xxx.devid, NULL, XXX_NAME);
if(IS_ERR(xxx.device)){
ret = PTR_ERR(xxx.device);
goto fail_device;
}
return 0;
fail_device:
class_destroy(xxx.class);
fail_class:
cdev_del(&xxx.cdev);
fail_cdev:
unregister_chrdev_region(xxx.devid, XXX_CNT);
fail_devid:
return ret;
}
//驱动出口函数
static void __exit xxx_exit(void){
//删除设备
cdev_del(&xxx.cdev);
//释放设备号
unregister_chrdev_region(xxx.devid, XXX_CNT);
//摧毁设备
device_destroy(xxx.class, xxx.devid);
//摧毁类
class_destroy(xxx.class);
}
//注册驱动
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FANG");
(3)应用程序编写
编写xxx.c文件对应的应用程序代码文件xxxApp.c,用于读取设备驱动文件
1、头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "linux/ioctl.h"
2、 main函数
int main(int argc, char *argv[]){
return 0;
}
其中,argc是应用程序参数个数
argv是具体的参数内容(字符串形式)
3、open函数
int fd = 0,ret=0;
char *filename;
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("can not open file %s\n",filename);
return -1;
}
open()函数原型为 int open(const char *pathname, int flags);
参数 pathname:要打开的设备或者文件名,这里是第二个参数argv[1]
flags:文件打开模式,以下三种模式必选其一:
O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
如果文件打开成功的话返回文件的文件描述符(int),即fd,后续代码通过这个文件描述符完成对文件的操作。
4、read函数
char readbuf[100];//从驱动读取到的数据存到readbuf中
ret = read(fd, radbuf, 10);
if(ret < 0){
printf("read file %s failed!\n", filename);
return -1;
}
read() 函数原型:ssize_t read(int fd, void *buf, size_t count)
参数 fd:要读取的文件描述符,读取文件之前要先用 open 函数打开文件,open 函数打开文件成
功以后会得到文件描述符。
buf:数据读取到此 buf 中。
count:要读取的数据长度,也就是字节数。
返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返
回负值,表示读取失败。
5、write函数
char writebuf[100];//应用程序向驱动写的数据
ret = write(fd,writebuf,sizeof(writebuf));
if(ret<0){
printf("write file %s failed!\n" ,filename);
close(fd);
return -1;
}
write()函数原型:ssize_t write(int fd, const void *buf, size_t count);
参数 fd:要进行写操作的文件描述符,写文件之前要先用 open 函数打开文件,open 函数打开文
件成功以后会得到文件描述符。
buf:要写入的数据。
count:要写入的数据长度,也就是字节数。
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
6、close函数
close(fd);
close() 函数原型:int close(int fd);
参数: fd:要关闭的文件描述符。
返回值:0 表示关闭成功,负值表示关闭失败。
(4)程序测试
1、驱动程序编译
驱动程序文件xxx.c目录下终端运行make编译,生成xxx.ko文件
make
已知:开发版根文件系统通过NFS挂载到ubuntu目录下。
将编译得到的xxx.ko文件拷贝到ubuntu的挂载目录nfs下的lib/modules/4.1.15/中
cp xxx.ko /home/f3161/linux/nfs/lib/modules/4.1.15/ -f
2、应用程序编译
对应用程序xxxApp.c进行编译,生成xxxApp文件
arm-linux-gnueabihf-gcc xxxApp.c -o xxxApp
将编译得到的xxxApp文件拷贝到ubuntu的挂载目录nfs下的lib/modules/4.1.15/中
cp xxxApp /home/f3161/linux/nfs/lib/modules/4.1.15/ -f
3、开发板终端运行
在windows电脑下使用putty软件与开发板串口通信,作为串口终端使用。
第一次加载驱动的时候需要运行命令depmod
depmod
运行modprobe xxx.ko命令加载驱动
modprobe xxx.ko
输入“lsmod”命令即可查看当前系统中存在的模块
lsmod
输入命令查看当前系统中的设备
cat /proc/devices
加载应用程序,其中后面可以添加参数
./xxxApp /dev/xxx
卸载驱动
rmmod xxx.ko